Explore, Select & Filter - C#

Quote

“If we have data, let’s look at data. If all we have are opinions, let’s go with mine.”

Jim Barksdale

Suppress CS1701/CS1702 assembly version warnings in .NET Interactive. Run this cell once before any cells that use NuGet packages.

using System.Reflection;
using Microsoft.DotNet.Interactive;
using Microsoft.DotNet.Interactive.CSharp;
 
var csharpKernel = (CSharpKernel)Kernel.Root.FindKernelByName("csharp");
var optionsField = typeof(CSharpKernel).GetField("_scriptOptions",
    BindingFlags.NonPublic | BindingFlags.Instance);
 
var scriptOptions = optionsField.GetValue(csharpKernel);
var withWarningLevel = scriptOptions.GetType().GetMethod("WithWarningLevel");
var newOptions = withWarningLevel.Invoke(scriptOptions, new object[] { 0 });
optionsField.SetValue(csharpKernel, newOptions);

Install NuGet packages and import namespaces. Alias Microsoft.Data.Analysis as MDA so DataFrame continues to refer to Polars.NET inside mixed examples.

#r "nuget: Polars.NET, 0.4.0"
#r "nuget: Polars.NET.Native.win-x64, 0.4.0"
#r "nuget: Microsoft.Data.Analysis, 0.23.0"
 
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
using Polars.CSharp;
using static Polars.CSharp.Polars;
using MDA = Microsoft.Data.Analysis;
using Microsoft.DotNet.Interactive.Formatting;
 
Formatter.Register<Polars.CSharp.DataFrame>((df, writer) =>
{
    var html = df.ToHtml();
    html = System.Text.RegularExpressions.Regex.Replace(html, @"(&gt;|>)(.+?)(&lt;|<)", @"$1$2$3");
    html = System.Text.RegularExpressions.Regex.Replace(html, @">""(.+?)""<", @">$1<");
    writer.Write(html);
}, "text/html");
Formatter.Register<Polars.CSharp.Series>((s, writer) =>
    writer.Write($"<pre style='font-size:14px'>{s}</pre>"), "text/html");
 
var DATA = Path.Combine("..", "data");
Console.WriteLine($"Data directory: {Path.GetFullPath(DATA)}");
 
static Type[] OhlcvCsvTypes() => new[]
{
    typeof(long), typeof(string), typeof(DateTime), typeof(decimal), typeof(decimal),
    typeof(decimal), typeof(decimal), typeof(decimal), typeof(long), typeof(decimal), typeof(decimal), typeof(bool)
};
 
static decimal? ToNullableDecimal(object value)
{
    if (value is null) return null;
    if (value is decimal d) return d;
    if (value is string s)
    {
        if (string.IsNullOrWhiteSpace(s)) return null;
        return decimal.Parse(s, NumberStyles.Float, CultureInfo.InvariantCulture);
    }
    return Convert.ToDecimal(value, CultureInfo.InvariantCulture);
}
 
static MDA.DataFrame ConvertColumnsToDecimal(MDA.DataFrame df, params string[] columnNames)
{
    foreach (var columnName in columnNames)
    {
        var index = df.Columns.IndexOf(columnName);
        if (index < 0) continue;
        var source = df.Columns[index];
        var target = new MDA.PrimitiveDataFrameColumn<decimal>(columnName, df.Rows.Count);
        for (long row = 0; row < df.Rows.Count; row++)
        {
            var value = ToNullableDecimal(source[row]);
            if (value.HasValue) target[row] = value.Value;
        }
        df.Columns.Remove(columnName);
        df.Columns.Insert(index, target);
    }
    return df;
}
 
static MDA.DataFrame LoadOhlcvCsv(string dataDir) =>
    MDA.DataFrame.LoadCsv(Path.Combine(dataDir, "eurostoxx50_ohlcv.csv"), dataTypes: OhlcvCsvTypes());
 
static MDA.DataFrame LoadScoresDailyCsv(string dataDir)
{
    var df = MDA.DataFrame.LoadCsv(Path.Combine(dataDir, "scores_daily.csv"));
    return ConvertColumnsToDecimal(
        df,
        "pe_zscore", "pb_zscore", "ev_ebitda_zscore", "yield_zscore", "relative_value_score",
        "relative_strength", "sma_50_ratio", "sma_200_ratio", "dist_from_52w_high", "momentum_score",
        "implied_upside", "recommendation_mean", "sentiment_score", "composite_score", "sma_30_close",
        "sma_90_close", "market_cap", "index_weight", "current_price", "day_change_pct",
        "five_day_change_pct", "ytd_change_pct"
    );
}
Data directory: c:\Users\aperi\DEV\LANG\data

Load the primary datasets used throughout this notebook. Both libraries read the same CSV files, so the rest of the page compares API shape and workflow rather than data differences.

var dfP = Polars.CSharp.DataFrame.ReadCsv(Path.Combine(DATA, "eurostoxx50_ohlcv.csv"), tryParseDates: true);
var dimP = Polars.CSharp.DataFrame.ReadCsv(Path.Combine(DATA, "dim_country.csv"));
 
display($"OHLCV: {dfP.Shape}  |  DimCountry: {dimP.Shape}");
 
var df = LoadOhlcvCsv(DATA);
var dim = MDA.DataFrame.LoadCsv(Path.Combine(DATA, "dim_country.csv"));
 
display($"OHLCV: ({df.Rows.Count}, {df.Columns.Count})  |  DimCountry: ({dim.Rows.Count}, {dim.Columns.Count})");
OHLCV: (66355, 12)  |  DimCountry: (212, 2)
OHLCV: (66355, 12)  |  DimCountry: (212, 2)

Current API and execution-model check | 2026-04

Microsoft’s DataFrame API documents Head, Tail, Description, Info, Filter, OrderBy, LoadCsv, and IDataView interoperability. Polars’ lazy optimization guide documents predicate, projection, and slice pushdown. In practice, the same explore/select/filter logic can stay closer to an optimizable query plan in Polars, while MDA remains an eager in-memory dataframe API.


Data Exploration

The first step after loading data is exploration: previewing rows, inspecting schema, profiling nulls, and understanding value distributions before you write downstream transformations. Polars.NET tends to surface these operations as concise dataframe methods; Microsoft.Data.Analysis exposes the same work through managed DataFrame and DataFrameColumn APIs.

Head, Tail, and Sample

Polars.NET | Preview first and last rows with Head and Tail

.Head(n) and .Tail(n) return the first and last N rows. .Sample(n) returns N random rows. These are the most common entry points for data exploration.

Calls Head(5) and Tail(5) on the 66,355-row OHLCV DataFrame, displaying 5-row previews from each end to confirm schema, date range (2021-01-04 to 2026-03-12), and that all 12 columns are present.

display("Head(5):");
display(dfP.Head(5));
display("Tail(5):");
dfP.Tail(5)

Head(5):

idsymboldateopenhighlowcloseadj_closevolumedividendsstock_splitsis_filled
21160ABI.BR2021-01-0458.1558.8556.7857.2153.5761151393700false
21161ABI.BR2021-01-0556.957.9856.7557.1853.548138272200false
21162ABI.BR2021-01-0657.9658.9457.3958.7755.037137020400false
21163ABI.BR2021-01-0758.6858.8657.8858.454.6905146991100false
21164ABI.BR2021-01-0858.1658.457.4357.8654.1848142868100false

Tail(5):

idsymboldateopenhighlowcloseadj_closevolumedividendsstock_splitsis_filled
64828WKL.AS2026-03-0669.0269.3667.8268.5268.52114372900false
66875WKL.AS2026-03-0968.7869.1667.6468.6468.6484150300false
66876WKL.AS2026-03-1068.869.1666.3467.1667.16135564500false
66877WKL.AS2026-03-1167.569.667.0267.2267.22114253100false
66929WKL.AS2026-03-126767.5466.2867.3267.3221037900false

Microsoft.Data.Analysis | Preview first and last rows with Head and Tail

Microsoft.Data.Analysis exposes Head(n) and Tail(n) directly on DataFrame, so the first preview workflow is very close to Polars for this use case.

Calls Head(5) and Tail(5) on the 66,355-row OHLCV DataFrame to verify that the dataset loaded correctly, the row ordering is intact, and all 12 columns are present in both previews.

// Microsoft.Data.Analysis — Head / Tail
display("Head(5):");
display(df.Head(5));
display("Tail(5):");
df.Tail(5)
Head(5):
idsymboldateopenhighlowcloseadj_closevolumedividendsstock_splitsis_filled
21160ABI.BR2021-01-04 00:00:00Z58.1558.8556.7857.2153.576115139370.00.0False
21161ABI.BR2021-01-05 00:00:00Z56.957.9856.7557.1853.54813827220.00.0False
21162ABI.BR2021-01-06 00:00:00Z57.9658.9457.3958.7755.03713702040.00.0False
21163ABI.BR2021-01-07 00:00:00Z58.6858.8657.8858.454.690514699110.00.0False
21164ABI.BR2021-01-08 00:00:00Z58.1658.457.4357.8654.184814286810.00.0False
Tail(5):
idsymboldateopenhighlowcloseadj_closevolumedividendsstock_splitsis_filled
64828WKL.AS2026-03-06 00:00:00Z69.0269.3667.8268.5268.5211437290.00.0False
66875WKL.AS2026-03-09 00:00:00Z68.7869.1667.6468.6468.648415030.00.0False
66876WKL.AS2026-03-10 00:00:00Z68.869.1666.3467.1667.1613556450.00.0False
66877WKL.AS2026-03-11 00:00:00Z67.569.667.0267.2267.2211425310.00.0False
66929WKL.AS2026-03-12 00:00:00Z67.067.5466.2867.3267.322103790.00.0False

Microsoft.Data.Analysis | Random sample with an explicit mask

The current API surface documents Sample(Int32), but the explicit helper below is still useful because it shows the row-filtering model MDA falls back to in more complex cases: build a boolean mask and pass it into Filter().

Builds a Sample(DataFrame data, int n) helper that randomly selects row indices, turns them into a boolean mask, and returns data.Filter(mask) to produce a 5-row exploratory sample.

// Microsoft.Data.Analysis — Sample(n) returns n random rows
// (No native method, using boolean mask generation)
DataFrame Sample(DataFrame data, int n)
{
    var rand = new Random();
    var indices = new HashSet<long>();
    while(indices.Count < n && indices.Count < data.Rows.Count)
    {
        indices.Add(rand.NextInt64(0, data.Rows.Count));
    }
 
    var mask = new PrimitiveDataFrameColumn<bool>("mask", data.Rows.Count);
    for(long i = 0; i < data.Rows.Count; i++) mask[i] = indices.Contains(i);
 
    return data.Filter(mask);
}
 
Sample(df, 5)
idsymboldateopenhighlowcloseadj_closevolumedividendsstock_splitsis_filled
62110ADS.DE2021-02-03 00:00:00Z279.9279.9274.2275.0262.79584053620.00.0False
47784BMW.DE2021-11-24 00:00:00Z94.2394.8391.8192.4970.787814038390.00.0False
65828DSY.PA2024-11-21 00:00:00Z32.1732.2831.8332.1731.9218453980.00.0False
27022EL.PA2023-03-24 00:00:00Z158.5725158.915156.8601158.6215150.2494589880.00.0False
43193ENI.MI2024-06-21 00:00:00Z14.0614.15613.90613.93612.5348297043260.00.0False

Microsoft.Data.Analysis | Inspect shape with row counts and Info

MDA exposes row and column counts directly from Rows.Count and Columns.Count, and Info() provides the concise schema-like view used for quick inspection.

Prints the OHLCV shape as (66355, 12) and then calls Info() to summarize the 12 columns, their CLR data types, and basic completeness metadata before any downstream selection or filtering logic.

// Microsoft.Data.Analysis — Shape and schema
display($"Shape: ({df.Rows.Count}, {df.Columns.Count})  |  Height: {df.Rows.Count}  |  Width: {df.Columns.Count}");
df.Info();
Shape: (66355, 12)  |  Height: 66355  |  Width: 12

Microsoft.Data.Analysis | Summary statistics with Description

Description() is the MDA equivalent for numeric summary statistics. It returns another DataFrame, which keeps the result easy to inspect or reuse in notebook workflows.

Computes descriptive statistics for the numeric columns in df, returning a summary DataFrame that highlights counts, minima, maxima, and mean values for the OHLCV data.

// Microsoft.Data.Analysis — Description() returns a summary DataFrame
df.Description()
Descriptioniddateopenhighlowcloseadj_closevolumedividendsstock_splits
Length (excluding null values)66355663556635566355663556635566355663556635566355
Max66930<null>29262957281328392802.938237639155022.55
Min1<null>1.6011.66281.58421.60661.2013000
Mean33179.734<null>197.04053199.36412194.58578197.0349190.494925942123.50.0117566730.00017203268

Microsoft.Data.Analysis | Count nulls per column

Null counts live on each DataFrameColumn via NullCount, so the standard pattern is to iterate columns and record the fields that actually contain missing data.

Loads scores_daily.csv, iterates through its columns, and prints only the fields whose NullCount is greater than zero so that missing-value hot spots stand out immediately.

// Microsoft.Data.Analysis — NullCount is a property on DataFrameColumn
// Use scores_daily which has real nulls
var scP = LoadScoresDailyCsv(DATA);
Console.WriteLine($"scores_daily: ({scP.Rows.Count}, {scP.Columns.Count})");
foreach (var col in scP.Columns)
{
    var nc = col.NullCount;
    if (nc > 0)
        Console.WriteLine($"  {col.Name,-28} {nc,4} nulls");
}
scores_daily: (466, 36)
  pe_zscore                       3 nulls
  pb_zscore                       6 nulls
  ev_ebitda_zscore               71 nulls
  yield_zscore                   35 nulls
  recommendation_mean            14 nulls

Microsoft.Data.Analysis | Frequency distribution with ValueCounts

ValueCounts() is available on a column and returns a new DataFrame of unique values plus their counts. This is the fastest way to inspect categorical distributions in MDA.

Computes ValueCounts() for the symbol column, returning one row per ticker together with its observation count across the 66,355-row OHLCV dataset.

// Microsoft.Data.Analysis — ValueCounts() on a Column returns a DataFrame
df.Columns["symbol"].ValueCounts()
ValuesCounts
ABI.BR1331
AD.AS1331
ADS.DE1324
ADYEN.AS1331
AI.PA1331
AIR.PA1331
ALV.DE1324
ARGX.BR1331
ASML.AS1331
BAS.DE1324
BAYN.DE1324
BBVA.MC1329
BMW.DE1324
BN.PA1331
BNP.PA1331
CS.PA1331
DB1.DE1324
DG.PA1331
DHL.DE1324
DSY.PA1331
DTE.DE1324
EL.PA1331
ENEL.MI1321
ENI.MI1321
ENR.DE1324
IBE.MC1329
IFX.DE1324
INGA.AS1331
ISP.MI1321
ITX.MC1329
MBG.DE1324
MC.PA1331
MUV2.DE1324
NDA-FI.HE1306
OR.PA1331
PRX.AS1331
RACE.MI1321
RHM.DE1324
RMS.PA1331
SAF.PA1331
SAN.MC1329
SAN.PA1331
SAP.DE1324
SGO.PA1331
SIE.DE1324
SU.PA1331
TTE.PA1331
UCG.MI1321
VOW.DE1324
WKL.AS1331

Microsoft.Data.Analysis | Distinct values with LINQ Distinct

When you want the set of unique values without the counts, the MDA pattern is usually to cast the column to a typed enumerable and use LINQ Distinct() before materializing the result back into a DataFrame.

Casts the symbol column to string, computes the distinct tickers with LINQ, prints the unique-count result, and materializes the distinct values into a one-column DataFrame.

// Microsoft.Data.Analysis — Unique counting and distinct values using LINQ
var symSeries = df.Columns["symbol"].Cast<string>();
var distinctSymbols = symSeries.Distinct().ToArray();
display($"NUnique: {distinctSymbols.Length}");
 
new DataFrame(new StringDataFrameColumn("symbol", distinctSymbols))
NUnique: 50
symbol
ABI.BR
AD.AS
ADS.DE
ADYEN.AS
AI.PA
AIR.PA
ALV.DE
ARGX.BR
ASML.AS
BAS.DE
BAYN.DE
BBVA.MC
BMW.DE
BN.PA
BNP.PA
CS.PA
DB1.DE
DG.PA
DHL.DE
DSY.PA
DTE.DE
EL.PA
ENEL.MI
ENI.MI
ENR.DE
IBE.MC
IFX.DE
INGA.AS
ISP.MI
ITX.MC
MBG.DE
MC.PA
MUV2.DE
NDA-FI.HE
OR.PA
PRX.AS
RACE.MI
RHM.DE
RMS.PA
SAF.PA
SAN.MC
SAN.PA
SAP.DE
SGO.PA
SIE.DE
SU.PA
TTE.PA
UCG.MI
VOW.DE
WKL.AS

Microsoft.Data.Analysis | Memory estimate via CLR-type heuristic

MDA does not expose a direct equivalent to Polars EstimatedSize(), so a practical approximation is to estimate bytes from the inferred CLR type and multiply by column length.

Estimates dataframe size by summing column length x assumed byte width across all columns, then reports the approximate byte and megabyte footprint alongside the dataframe shape.

// Microsoft.Data.Analysis — estimate size
long estBytes = 0;
foreach (var col in df.Columns)
{
    estBytes += col.Length * (col.DataType.Name switch
    {
        "Int32" or "Single" => 4,
        "Int64" or "Double" or "DateTime" => 8,
        "Decimal" => 16,
        "Boolean" => 1,
        "String" => 40,
        _ => 8
    });
}
display($"Estimated size: {estBytes:N0} bytes ({estBytes / 1_048_576.0:F2} MB)");
display($"Shape: ({df.Rows.Count}, {df.Columns.Count})  |  {df.Rows.Count:N0} rows x {df.Columns.Count} cols");
Estimated size: 11'744'835 bytes (11.20 MB)
Shape: (66355, 12)  |  66'355 rows x 12 cols

Microsoft.Data.Analysis | Reusable quick-profile function

When basic exploration needs to become repeatable, build a small profiling function that emits column name, CLR type, null count, and distinct count for any DataFrame.

Defines ProfileDataFrame(DataFrame d), computes per-column type, null count, and unique count, and applies it to both eurostoxx50_ohlcv and dim_country to produce reusable profile tables.

// Microsoft.Data.Analysis — reusable profiler that returns a summary DataFrame
DataFrame ProfileDataFrame(DataFrame d)
{
    var cols = d.Columns.ToArray();
    var colNames = new string[cols.Length];
    var types = new string[cols.Length];
    var nulls = new long[cols.Length];
    var uniques = new long[cols.Length];
 
    for (int j = 0; j < cols.Length; j++)
    {
        var s = d.Columns[j];
        colNames[j] = s.Name;
        types[j] = s.DataType.Name;
        nulls[j] = s.NullCount;
        uniques[j] = s.ValueCounts().Rows.Count;
    }
 
    return new DataFrame(
        new StringDataFrameColumn("column", colNames),
        new StringDataFrameColumn("type", types),
        new PrimitiveDataFrameColumn<long>("nulls", nulls),
        new PrimitiveDataFrameColumn<long>("unique", uniques)
    );
}
 
display($"eurostoxx50_ohlcv: ({df.Rows.Count}, {df.Columns.Count})");
display(ProfileDataFrame(df));
display($"dim_country: ({dim.Rows.Count}, {dim.Columns.Count})");
display(ProfileDataFrame(dim));
eurostoxx50_ohlcv: (66355, 12)
columntypenullsunique
idInt64066355
symbolString050
dateDateTime01331
openDecimal029671
highDecimal031651
lowDecimal031695
closeDecimal031505
adj_closeDecimal057739
volumeInt64065199
dividendsDecimal0216
stock_splitsDecimal06
is_filledBoolean02
dim_country: (212, 2)
columntypenullsunique
country_nameString0212
iso_alpha2String0212

Microsoft.Data.Analysis | Select a single column by name

Selecting one column returns a DataFrameColumn, not a one-column DataFrame. That is convenient for direct vector operations, but downstream code needs to stay clear about whether it expects a column or a frame.

Selects close and symbol from df.Columns[...], then prints each column’s name, length, and data type metadata to confirm the object returned is a DataFrameColumn.

// Microsoft.Data.Analysis — single column returns a DataFrameColumn
var closeSeries = df.Columns["close"];
display($"Name: {closeSeries.Name}  |  Length: {closeSeries.Length}  |  Type: {closeSeries.DataType.Name}");
 
var symbolSeries = df.Columns["symbol"];
display($"Name: {symbolSeries.Name}  |  Length: {symbolSeries.Length}");
Name: close  |  Length: 66355  |  Type: Decimal
Name: symbol  |  Length: 66355

Microsoft.Data.Analysis | Select multiple columns into a new DataFrame

The standard MDA pattern is to construct a new DataFrame from the selected column objects. This is explicit and readable, but less declarative than Polars projection expressions when the selection logic becomes dynamic.

Builds a new DataFrame from date, symbol, close, and volume, then previews the first five rows to confirm the four-column projection.

// Microsoft.Data.Analysis — Select specific columns to form a new DataFrame
var selected = new DataFrame(df.Columns["date"], df.Columns["symbol"], df.Columns["close"], df.Columns["volume"]);
selected.Head(5)
datesymbolclosevolume
2021-01-04 00:00:00ZABI.BR57.211513937
2021-01-05 00:00:00ZABI.BR57.181382722
2021-01-06 00:00:00ZABI.BR58.771370204
2021-01-07 00:00:00ZABI.BR58.41469911
2021-01-08 00:00:00ZABI.BR57.861428681

Microsoft.Data.Analysis | Compute columns explicitly and rename with SetName

MDA supports vectorized column arithmetic, but computed projections are assembled explicitly: clone the source columns you want to keep, compute the derived column, rename it, and build a new DataFrame from those pieces.

Clones symbol and close, renames close to price, computes range = high - low, and materializes the result as a three-column DataFrame for inspection.

// Microsoft.Data.Analysis — compute-on-select and renaming
var symbolCol = df.Columns["symbol"].Clone();
var priceCol = df.Columns["close"].Clone();
priceCol.SetName("price");
var highCol = (PrimitiveDataFrameColumn<decimal>)df.Columns["high"];
var lowCol = (PrimitiveDataFrameColumn<decimal>)df.Columns["low"];
var rangeCol = new PrimitiveDataFrameColumn<decimal>("range", df.Rows.Count);
for (long i = 0; i < df.Rows.Count; i++) if (highCol[i].HasValue && lowCol[i].HasValue) rangeCol[i] = highCol[i].Value - lowCol[i].Value;
var computed = new DataFrame(symbolCol, priceCol, rangeCol);
computed.Head(5)
symbolpricerange
ABI.BR57.212.07
ABI.BR57.181.23
ABI.BR58.771.55
ABI.BR58.40.98
ABI.BR57.860.97

Microsoft.Data.Analysis | Exclude columns by building the keep-set

Dropping columns is usually written as the inverse problem: define the columns to exclude, then build a new DataFrame from the columns that remain.

Defines a dropCols set, selects every column not in that set, prints the resulting column list, and previews the trimmed frame.

// Microsoft.Data.Analysis — Drop columns by selecting the ones to keep
var dropCols = new HashSet<string> { "id", "adj_close", "dividends", "stock_splits", "is_filled" };
var keepCols = df.Columns.Where(c => !dropCols.Contains(c.Name)).ToArray();
var trimmed = new DataFrame(keepCols);
display($"Columns: {string.Join(", ", trimmed.Columns.Select(c => c.Name))}");
trimmed.Head(3)
Columns: symbol, date, open, high, low, close, volume
symboldateopenhighlowclosevolume
ABI.BR2021-01-04 00:00:00Z58.1558.8556.7857.211513937
ABI.BR2021-01-05 00:00:00Z56.957.9856.7557.181382722
ABI.BR2021-01-06 00:00:00Z57.9658.9457.3958.771370204

Microsoft.Data.Analysis | Select columns matching a regex pattern

Regex-based selection in MDA is a Columns.Where(...) operation followed by a new DataFrame constructor. This keeps the logic flexible when column names follow domain conventions.

Uses the regex ^(open|high|low|close)$ to isolate the OHLC price columns, prints the matched column names, and previews the resulting four-column DataFrame.

// Microsoft.Data.Analysis — select columns by regex (match OHLC price columns)
var pattern = new Regex("^(open|high|low|close)$");
var priceCols = df.Columns.Where(c => pattern.IsMatch(c.Name)).ToArray();
var regexDf = new DataFrame(priceCols);
display($"Matched: {string.Join(", ", regexDf.Columns.Select(c => c.Name))}");
regexDf.Head(5)
Matched: open, high, low, close
openhighlowclose
58.1558.8556.7857.21
56.957.9856.7557.18
57.9658.9457.3958.77
58.6858.8657.8858.4
58.1658.457.4357.86

Microsoft.Data.Analysis | Rename columns with cloned columns and SetName

Renaming usually means cloning the source columns, mutating their names, and then materializing the renamed set into a new frame.

Clones symbol, close, and volume, renames them to ticker, price, and vol, prints the new schema, and previews the renamed DataFrame.

// Microsoft.Data.Analysis — Rename via SetName on cloned columns
var renamed = new DataFrame(df.Columns["symbol"].Clone(), df.Columns["close"].Clone(), df.Columns["volume"].Clone());
renamed.Columns["symbol"].SetName("ticker");
renamed.Columns["close"].SetName("price");
renamed.Columns["volume"].SetName("vol");
 
display($"Columns: {string.Join(", ", renamed.Columns.Select(c => c.Name))}");
renamed.Head(3)
Columns: ticker, price, vol
tickerpricevol
ABI.BR57.211513937
ABI.BR57.181382722
ABI.BR58.771370204

Microsoft.Data.Analysis | Reorder columns by constructor order

Column order is simply the order in which the selected columns are passed into the new DataFrame. That makes reordering straightforward, but still a manual projection step.

Constructs a new DataFrame in the order symbol, date, volume, open, high, low, close, prints the resulting column order, and previews the reordered frame.

// Microsoft.Data.Analysis — Reorder by selecting them in order into a new DataFrame
var reordered = new DataFrame(
    df.Columns["symbol"], df.Columns["date"], df.Columns["volume"],
    df.Columns["open"], df.Columns["high"], df.Columns["low"], df.Columns["close"]
);
display($"Column order: {string.Join(", ", reordered.Columns.Select(c => c.Name))}");
reordered.Head(3)
Column order: symbol, date, volume, open, high, low, close
symboldatevolumeopenhighlowclose
ABI.BR2021-01-04 00:00:00Z151393758.1558.8556.7857.21
ABI.BR2021-01-05 00:00:00Z138272256.957.9856.7557.18
ABI.BR2021-01-06 00:00:00Z137020457.9658.9457.3958.77

Microsoft.Data.Analysis | Boolean filter with elementwise comparison

Numeric predicates are expressed as vectorized elementwise comparisons on a typed column, and the resulting boolean column is passed into DataFrame.Filter(...).

Casts close to MDA.PrimitiveDataFrameColumn<decimal>, builds the predicate close > 500, filters the dataframe, prints the row count, and previews the first five matching rows.

// Microsoft.Data.Analysis — Filter with elementwise expressions
var closeCol = (PrimitiveDataFrameColumn<decimal>)df.Columns["close"];
var expensiveMask = new PrimitiveDataFrameColumn<bool>("close_gt_500", df.Rows.Count);
for (long i = 0; i < df.Rows.Count; i++) if (closeCol[i].HasValue) expensiveMask[i] = closeCol[i].Value > 500.0m;
var expensive = df.Filter(expensiveMask);
display($"Rows where close > 500: {expensive.Rows.Count}");
expensive.Head(5)
Rows where close > 500: 6155
idsymboldateopenhighlowcloseadj_closevolumedividendsstock_splitsis_filled
60763ADYEN.AS2021-01-04 00:00:00Z1900.01921.51856.01859.51859.5994080.00.0False
60764ADYEN.AS2021-01-05 00:00:00Z1848.51857.01814.01829.01829.0862560.00.0False
60765ADYEN.AS2021-01-06 00:00:00Z1822.01824.01706.51733.01733.01568440.00.0False
60766ADYEN.AS2021-01-07 00:00:00Z1735.01754.01708.51714.51714.5901830.00.0False
60767ADYEN.AS2021-01-08 00:00:00Z1730.01764.51715.01756.51756.5971760.00.0False

Microsoft.Data.Analysis | Compound filters with And, Or, and explicit negation

Compound filters are composed from boolean columns with .And() and .Or(). In practice, the mask construction is explicit, which keeps the logic readable but gets verbose faster than a Polars predicate chain.

Builds boolean masks for ASML.AS, SAP.DE, and close > 600, then evaluates ASML AND close > 600, ASML OR SAP, and a non-ASML filter to show the three common boolean compositions.

// Microsoft.Data.Analysis — AND / OR / NOT elementwise operators
var symbolColStr = df.Columns["symbol"];
var isAsml = symbolColStr.ElementwiseEquals("ASML.AS");
var isSap = symbolColStr.ElementwiseEquals("SAP.DE");
var closeOver600 = new PrimitiveDataFrameColumn<bool>("close_over_600", df.Rows.Count);
for (long i = 0; i < df.Rows.Count; i++) if (closeCol[i].HasValue) closeOver600[i] = closeCol[i].Value > 600.0m;
var filtered = df.Filter((PrimitiveDataFrameColumn<bool>)isAsml.And(closeOver600));
display($"ASML AND close > 600: {filtered.Rows.Count} rows");
display(filtered.Head(3));
var orFilter = df.Filter((PrimitiveDataFrameColumn<bool>)isAsml.Or(isSap));
display($"ASML OR SAP: {orFilter.Rows.Count} rows");
var notFilter = df.Filter((PrimitiveDataFrameColumn<bool>)isAsml.ElementwiseNotEquals(true));
display($"NOT ASML: {notFilter.Rows.Count} rows");
ASML AND close > 600: 840 rows
idsymboldateopenhighlowcloseadj_closevolumedividendsstock_splitsis_filled
136ASML.AS2021-07-14 00:00:00Z599.5611.8597.2609.1582.97086415850.00.0False
142ASML.AS2021-07-22 00:00:00Z610.0625.9608.2620.8594.1697880990.00.0False
143ASML.AS2021-07-23 00:00:00Z622.9639.0617.5638.8611.39678337370.00.0False
ASML OR SAP: 2655 rows
NOT ASML: 65024 rows

Microsoft.Data.Analysis | Filter by membership with a manual boolean mask

Membership filtering is usually implemented by testing each value against a HashSet<T> and writing the result into a boolean mask column.

Builds a ticker set for ASML.AS, SAP.DE, and SIE.DE, evaluates membership row by row into a boolean mask, filters the dataframe, and previews the first five matching rows.

// Microsoft.Data.Analysis — IsIn requires manual evaluation into a boolean mask
var techTickers = new HashSet<string> { "ASML.AS", "SAP.DE", "SIE.DE" };
var isInMask = new PrimitiveDataFrameColumn<bool>("mask", df.Rows.Count);
 
// Cast to StringDataFrameColumn instead of using .Cast<string>()
var symbols = (StringDataFrameColumn)df.Columns["symbol"];
 
for(long i = 0; i < df.Rows.Count; i++)
{
    // Now indexing with [i] works correctly
    isInMask[i] = techTickers.Contains(symbols[i]);
}
 
var techRows = df.Filter(isInMask);
display($"Tech tickers: {techRows.Rows.Count} rows");
techRows.Head(5)
Tech tickers: 3979 rows
idsymboldateopenhighlowcloseadj_closevolumedividendsstock_splitsis_filled
1ASML.AS2021-01-04 00:00:00Z404.0411.0402.25406.25387.7097895020.00.0False
2ASML.AS2021-01-05 00:00:00Z406.55412.05401.15406.9388.32947987870.00.0False
3ASML.AS2021-01-06 00:00:00Z406.8407.2399.2402.85384.46448757110.00.0False
4ASML.AS2021-01-07 00:00:00Z404.8407.8400.35403.9385.46648747800.00.0False
5ASML.AS2021-01-08 00:00:00Z414.25419.1413.4416.05397.06189752430.00.0False

Microsoft.Data.Analysis | Range filter with composed boolean masks

Range filters are just boolean mask composition: generate the lower-bound and upper-bound comparisons, combine them with .And(), and pass the result into Filter().

Builds a mask for 100 <= close <= 200, filters the dataframe, prints the matching row count, and previews the first five results.

// Microsoft.Data.Analysis — Range filtering using logical And()
var midRangeMask = new PrimitiveDataFrameColumn<bool>("mid_range", df.Rows.Count);
for (long i = 0; i < df.Rows.Count; i++)
{
    if (closeCol[i].HasValue)
    {
        var value = closeCol[i].Value;
        midRangeMask[i] = value >= 100.0m && value <= 200.0m;
    }
}
var midRange = df.Filter(midRangeMask);
display($"Close between 100 and 200: {midRange.Rows.Count} rows");
midRange.Head(5)
Close between 100 and 200: 12013 rows
idsymboldateopenhighlowcloseadj_closevolumedividendsstock_splitsis_filled
62387ADS.DE2022-03-04 00:00:00Z196.5197.64187.0187.0180.591813198910.00.0False
62388ADS.DE2022-03-07 00:00:00Z177.1183.4170.08176.9170.837923456560.00.0False
62389ADS.DE2022-03-08 00:00:00Z172.18187.06172.0184.94178.602419373460.00.0False
62391ADS.DE2022-03-10 00:00:00Z211.35211.8196.68197.08190.326413751290.00.0False
62415ADS.DE2022-04-13 00:00:00Z198.52199.74194.16197.76190.98317555730.00.0False

Microsoft.Data.Analysis | Null checks with ElementwiseIsNull and ElementwiseIsNotNull

MDA exposes null predicates as elementwise boolean columns. The main pattern is straightforward: generate the mask, call Filter(), and count or inspect the result.

Filters once for rows with null volume and once for rows with non-null volume, then prints the row counts for each result set.

// Microsoft.Data.Analysis — IsNull / IsNotNull
var withNulls = df.Filter(df.Columns["volume"].ElementwiseIsNull());
display($"Rows with null volume: {withNulls.Rows.Count}");
 
var noNulls = df.Filter(df.Columns["volume"].ElementwiseIsNotNull());
display($"Rows with non-null volume: {noNulls.Rows.Count}");
Rows with null volume: 0
Rows with non-null volume: 66355

Microsoft.Data.Analysis | String predicates through explicit mask evaluation

String filtering often becomes manual mask construction over a MDA.StringDataFrameColumn, especially when you need EndsWith or Contains semantics without a dedicated string-expression namespace.

Builds one mask for Paris-listed symbols ending in .PA and another for symbols containing BN, filters the dataframe with each mask, prints the row counts, and materializes the distinct matching symbols.

// Microsoft.Data.Analysis — string predicates require manual boolean mask evaluation
var isPaMask = new PrimitiveDataFrameColumn<bool>("mask", df.Rows.Count);
var hasBnMask = new PrimitiveDataFrameColumn<bool>("mask", df.Rows.Count);
 
for(long i = 0; i < df.Rows.Count; i++)
{
    var val = symbols[i];
    isPaMask[i] = val != null && val.EndsWith(".PA");
    hasBnMask[i] = val != null && val.Contains("BN");
}
 
var parisStocks = df.Filter(isPaMask);
display($"Paris-listed (.PA): {parisStocks.Rows.Count} rows");
display(new DataFrame(new StringDataFrameColumn("symbol", parisStocks.Columns["symbol"].Cast<string>().Distinct())));
 
var containsB = df.Filter(hasBnMask);
display($"Symbol contains BN: {containsB.Rows.Count} rows");
new DataFrame(new StringDataFrameColumn("symbol", containsB.Columns["symbol"].Cast<string>().Distinct()))
Paris-listed (.PA): 21296 rows
symbol
AI.PA
AIR.PA
BN.PA
BNP.PA
CS.PA
DG.PA
DSY.PA
EL.PA
MC.PA
OR.PA
RMS.PA
SAF.PA
SAN.PA
SGO.PA
SU.PA
TTE.PA
Symbol contains BN: 2662 rows
symbol
BN.PA
BNP.PA

Microsoft.Data.Analysis | Date predicates with an explicit DateTime mask

The date column is evaluated row by row, coercing each value to DateTime where possible and writing the final result into reusable boolean masks. This is clear and robust, but more procedural than Polars’ date-expression API.

Builds one mask for rows in calendar year 2023 and another for Q1 2024, prints the resulting row counts, and previews the first five rows from the Q1 2024 subset.

// Microsoft.Data.Analysis — date predicates evaluated manually
var dates = df.Columns["date"];
var yearMask = new PrimitiveDataFrameColumn<bool>("yearMask", df.Rows.Count);
var q1Mask = new PrimitiveDataFrameColumn<bool>("q1Mask", df.Rows.Count);
 
for(long i = 0; i < df.Rows.Count; i++)
{
    DateTime d;
    if(dates[i] is DateTime dt) d = dt;
    else if (dates[i] is string s && DateTime.TryParse(s, out var p)) d = p;
    else continue;
 
    yearMask[i] = d.Year == 2023;
    q1Mask[i] = d >= new DateTime(2024, 1, 1) && d <= new DateTime(2024, 3, 31);
}
 
var year2023 = df.Filter(yearMask);
display($"Year 2023: {year2023.Rows.Count} rows");
 
var dateRange = df.Filter(q1Mask);
display($"Q1 2024: {dateRange.Rows.Count} rows");
dateRange.Head(5)
Year 2023: 12741 rows
Q1 2024: 3150 rows
idsymboldateopenhighlowcloseadj_closevolumedividendsstock_splitsis_filled
21930ABI.BR2024-01-02 00:00:00Z58.7258.9558.1758.7756.758210491450.00.0False
21931ABI.BR2024-01-03 00:00:00Z58.6459.3458.2458.3756.371912470000.00.0False
21932ABI.BR2024-01-04 00:00:00Z58.3658.9258.358.8156.796810095260.00.0False
21933ABI.BR2024-01-05 00:00:00Z58.2658.9158.1658.8656.845112360000.00.0False
21934ABI.BR2024-01-08 00:00:00Z58.4759.558.3959.3857.347310332380.00.0False

Microsoft.Data.Analysis | Access a single row by position

For quick inspection, Head(1) returns a one-row frame while column indexers retrieve specific scalar values from the same row position.

Displays the first row with Head(1), then reads symbol and close from row position 0 via the column indexers to show row-level inspection without materializing a dedicated row object.

// Microsoft.Data.Analysis — single row access
display("Row 0:");
display(df.Head(1));
 
// Access individual values from a row by column indexer
display($"Row 0, symbol: {df.Columns["symbol"][0]}");
display($"Row 0, close:  {df.Columns["close"][0]}");
Row 0:
idsymboldateopenhighlowcloseadj_closevolumedividendsstock_splitsis_filled
21160ABI.BR2021-01-04 00:00:00Z58.1558.8556.7857.2153.576115139370.00.0False
Row 0, symbol: ABI.BR
Row 0, close:  57.21

Microsoft.Data.Analysis | Slice a row range with a positional mask

The slice pattern shown here creates a positional boolean mask, marks the desired row interval, and filters on that mask.

Builds a mask for row positions 100..104, filters the dataframe on that mask, and displays the resulting five-row slice.

// Microsoft.Data.Analysis — Slice using an index boolean mask
display("Rows 100..104:");
var sliceMask = new PrimitiveDataFrameColumn<bool>("mask", df.Rows.Count);
for(long i = 100; i < 105 && i < df.Rows.Count; i++)
{
    sliceMask[i] = true;
}
df.Filter(sliceMask)
Rows 100..104:
idsymboldateopenhighlowcloseadj_closevolumedividendsstock_splitsis_filled
21260ABI.BR2021-05-26 00:00:00Z61.9962.3961.8362.1258.67019401860.00.0False
21261ABI.BR2021-05-27 00:00:00Z61.862.6461.7362.1358.679517964770.00.0False
21262ABI.BR2021-05-28 00:00:00Z62.1462.5861.9662.3458.877910041250.00.0False
21263ABI.BR2021-05-31 00:00:00Z62.2762.3161.5161.5658.14128515570.00.0False
21264ABI.BR2021-06-01 00:00:00Z62.3562.4861.9862.3858.915711716460.00.0False

Microsoft.Data.Analysis | Sort rows with OrderBy and OrderByDescending

MDA provides dataframe-level OrderBy(...) and OrderByDescending(...) for single-column sorting. Chained sorts work, but they are still eager materializations rather than lazy plan transformations.

Sorts the dataframe by close descending to show the top five closing prices, then chains a second sort to inspect symbol-first ordering with descending close values inside the sorted result.

// Microsoft.Data.Analysis — Sort ascending and descending
display("Top 5 by close (descending):");
display(df.OrderByDescending("close").Head(5));
 
// Multi-column sort: Microsoft.Data.Analysis doesn't natively support multi-sort chained via OrderBy().ThenBy()
// We chain OrderBy() calls (which sorts the result of the previous sort).
display("Sort by symbol ASC, then close DESC:");
df.OrderByDescending("close").OrderBy("symbol").Head(5)
Top 5 by close (descending):
idsymboldateopenhighlowcloseadj_closevolumedividendsstock_splitsis_filled
3708RMS.PA2025-02-14 00:00:00Z2926.02957.02813.02839.02802.93821056510.00.0False
3707RMS.PA2025-02-13 00:00:00Z2770.02816.02765.02816.02780.2302800870.00.0False
3709RMS.PA2025-02-17 00:00:00Z2825.02858.02803.02809.02776.7424538523.50.0False
3710RMS.PA2025-02-18 00:00:00Z2816.02827.02780.02806.02773.7771654690.00.0False
60927ADYEN.AS2021-08-24 00:00:00Z2725.02766.02711.52766.02766.0614310.00.0False
Sort by symbol ASC, then close DESC:
idsymboldateopenhighlowcloseadj_closevolumedividendsstock_splitsis_filled
21512ABI.BR2022-05-17 00:00:00Z54.6555.1853.9654.4451.910925790.00.0False
21814ABI.BR2023-07-19 00:00:00Z51.5952.1551.3452.050.219912282080.00.0False
21408ABI.BR2021-12-20 00:00:00Z51.6952.3250.7152.049.112121233170.00.0False
22457ABI.BR2026-01-26 00:00:00Z59.059.4458.9459.059.08360590.00.0False
21240ABI.BR2021-04-28 00:00:00Z58.0959.658.0259.055.252412503470.00.0False

Microsoft.Data.Analysis | Distinct counts from ValueCounts

For entity-style dedup checks, the quickest MDA path is usually ValueCounts() or LINQ Distinct() on the key column. That gives you the unique-cardinality view you need even when you do not build a full dataframe-level dedup helper.

Computes ValueCounts() for symbol, prints the number of unique tickers relative to the total row count, and previews the first ten unique symbols with their counts.

// Microsoft.Data.Analysis — Get unique values and counts using ValueCounts
var symbolCounts = df.Columns["symbol"].ValueCounts();
 
display($"Unique symbols: {symbolCounts.Rows.Count} (from {df.Rows.Count} total rows)");
 
// ValueCounts returns a DataFrame with the unique values and their counts
symbolCounts.Head(10)
Unique symbols: 50 (from 66355 total rows)
ValuesCounts
ABI.BR1331
AD.AS1331
ADS.DE1324
ADYEN.AS1331
AI.PA1331
AIR.PA1331
ALV.DE1324
ARGX.BR1331
ASML.AS1331
BAS.DE1324

C# Explore, Select and Filter Warnings

MDA Filter requires an explicit boolean column, not an expression

Unlike Polars.NET where df.Filter(Col("x").Gt(5)) works directly, MDA requires constructing a PrimitiveDataFrameColumn<bool> first — more verbose and error-prone.

Column selection by string index in MDA returns a reference, not a copy

df["col"] in MDA returns a reference to the column — mutations propagate to the original DataFrame.

C# Explore, Select and Filter Recommendations

  1. Profile before transforming — run Describe(), null counts, and unique counts on every new dataset.
  2. Filter early — reduce row count before expensive operations (joins, group_by).
  3. Prefer Polars.NET expressions for complex filters — the expression API is more composable and readable than MDA’s manual boolean column construction.
  4. Validate column existence before selection — check df.Columns to avoid runtime KeyNotFoundException.

Troubleshooting and failure modes

SymptomLikely causeFix
KeyNotFoundException on column selectColumn name not in DataFrameCheck df.Columns or df.Schema before selecting
Filter returns empty DataFrameCondition too restrictive or type mismatch in comparisonVerify filter values match column dtype
Describe() missing columnsMDA excludes non-numeric columns by defaultHandle separately with manual aggregation