Missing Data, Strings & DateTime - C#
Quote
“Life is dirty. So is your data. Get used to it.”
— Oz du Soleil
Summary
Covers three failure-prone dataframe domains in one C# reference: missing-value handling, string cleanup, and datetime/time-series work across Polars.NET and Microsoft.Data.Analysis. The note exists because these are the places where row data stops being “clean columns” and starts exposing the real differences between Arrow-native vectorized expressions and explicit CLR-oriented column loops.
Setup
- Configure the notebook, load Polars.NET and MDA packages, and establish the shared datasets used to compare null, string, and datetime behavior side by side
Missing Data
- Detect nulls, understand how each library represents absence, and compare dropping, literal fill, forward fill, backward fill, and statistic-based fill patterns
- Keep the library boundary clear: Polars.NET exposes missing-data operations as expressions like
FillNull, while MDA usually requires explicit typed-column scans and replacement loopsString Operations
- Apply case transforms, substring search, replace, slicing, split, regex extraction, padding, and concatenation over identifier-style text columns
- Contrast Polars
.Strvectorization with MDA’s materialized string arrays and manual column reconstructionDateTime Operations
- Parse and round-trip dates, extract components with
.Dt, apply date arithmetic, shift and lag values, filter by range, generate date sequences, and compute rolling or resampled time-series outputs- Make the ordering requirement explicit for rolling windows and related financial calculations, especially when translating the same logic between Polars expressions and MDA loops
Operations and safety
- Warnings: the current warning/recommendation sections in this note still emphasize the broader Polars-versus-MDA transform model, including immutability, conditional-expression API differences, and type mismatches across Arrow and CLR types
- Recommendations: 4 practices covering expression-first analytical transforms, MDA for ML.NET-oriented boundaries, schema validation after transforms, and Parquet for type-safe intermediates
- Troubleshooting: 3 failure modes covering missed reassignment in Polars, cast failures, and MDA typed-column mismatches
Glossary
Null
An absent value that represents missingness rather than a real business value.
It matters because every fill, drop, string, and datetime operation in the note depends on understanding whether a value is actually missing or merely present in a different form.
MDA may encode missingness as
NaNIn floating-point MDA columns, missing values may appear as
NaNinstead of CLRnull. Treating those as ordinary numbers silently breaks data-quality checks.
FillNull/ Fill strategy
A replacement operation that substitutes missing values with a constant, carried value, or derived statistic.
It matters because filling is one of the main choices that changes how downstream aggregates and models interpret incomplete data.
MDA has no single built-in equivalent
In Microsoft.Data.Analysis, fill logic is usually a manual scan that builds a new typed column. The operation is conceptually simple but operationally explicit.
Forward fill
A strategy that replaces each missing value with the most recent prior non-missing value in sequence.
It matters because time-ordered data often needs continuity without inventing a new constant or dropping the row entirely.
Order defines correctness
Forward fill only makes sense after the rows are ordered on the intended sequence key, usually time. On unsorted data it manufactures nonsense.
Backward fill
A strategy that replaces each missing value with the next available non-missing value later in the sequence.
It matters because some analytical pipelines prefer future-known carryback logic when filling leading gaps or preparing aligned windows.
Reverse scan is intentional
Backward fill is not just forward fill with a different name. The implementation direction changes which neighboring value becomes authoritative.
.Straccessor
The Polars string-expression namespace for vectorized text operations such as lowercase conversion, contains checks, replace, split, and extraction.
It matters because the note’s string-cleaning section is largely a contrast between
.Str-native column logic and manual CLR string materialization in MDA.Text stays inside the engine
As long as the logic remains in
.Str, Polars can keep the work columnar instead of forcing a per-row C# string loop.
Regex extract
Pulling a substring from text by matching a regular-expression pattern and returning a capture group.
It matters because ticker or exchange-code cleanup often depends on pattern-based extraction rather than fixed-position slicing.
Pattern mismatch returns missing output
Extraction logic is only as stable as the identifier format. When the pattern stops matching, the result becomes null-like output instead of a clean token.
.Dtaccessor
The Polars datetime-expression namespace for extracting and transforming date/time components.
It matters because the note uses
.Dtto derive year, month, weekday, offsets, and range-based filters without leaving the dataframe engine.Datetime logic stays composable
.Dtmakes date operations behave like any other expression, so filtering, projection, and derived columns can share the same pipeline structure.
DateTime/DateOnly
CLR-native date representations used in .NET code and often bridged to Arrow-backed dataframe types.
It matters because the note repeatedly crosses between dataframe-native datetime operations and explicit .NET parsing or materialization logic.
Precision and semantics differ
A .NET
DateTimeand an Arrow datetime may represent similar concepts with different precision defaults and conversion behavior.
Date range
A generated ordered sequence of dates used for filtering, joining, calendar construction, or time-axis expansion.
It matters because time-series workflows often need explicit date scaffolding rather than only the dates already present in the source data.
Useful beyond filtering
Generated ranges are often the bridge between sparse event rows and complete calendar-aware analysis.
Rolling window
A fixed-size moving slice over ordered rows used to compute smoothed or local statistics such as moving averages.
It matters because the note uses rolling windows for financial-style indicators where current values depend on the immediately preceding history.
Early rows are expected to be incomplete
The first
N-1rows in anN-period rolling calculation lack enough history, so null or partial results are a feature, not a bug.
Resampling
Converting time-series data from one temporal grain to another, such as daily to monthly.
It matters because raw event frequency is often wrong for reporting, trend comparison, or downstream modeling windows.
Aggregation rule is part of the metric
Resampling is not just grouping by calendar period. You must decide whether the target metric wants sum, mean, last value, OHLC, or something else.
Current API and execution-model check | 2026-04
Polars documents missing-data, string, and time-series workflows in its expressions guide and missing-data guide. Microsoft documents
DataFrame,DataFrame.LoadCsv,PrimitiveDataFrameColumn<T>, andStringDataFrameColumnas an eager typed-column API.
C# Missing Data, Strings and DateTime Setup
Warning Suppression
Suppresses assembly version warnings before any NuGet-dependent cells run.
// Suppress CS1701/CS1702 assembly version warnings in .NET Interactive.
// NuGet packages targeting .NET 8/9 trigger these on .NET 10 — harmless.
// 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
Loads the packages, imports namespaces, and registers the notebook formatters used below.
#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;
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<DataFrame>((df, writer) =>
{
var html = df.ToHtml();
html = System.Text.RegularExpressions.Regex.Replace(html, @"(>|>)(.+?)(<|<)", @"$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"
);
}
static MDA.DataFrame LoadSignalsDailyCsv(string dataDir)
{
var df = MDA.DataFrame.LoadCsv(Path.Combine(dataDir, "signals_daily.csv"));
return ConvertColumnsToDecimal(
df,
"current_price", "forward_pe", "price_to_book", "ev_to_ebitda", "dividend_yield",
"market_cap", "beta", "fifty_two_week_change", "sandp_52_week_change", "fifty_day_average",
"two_hundred_day_average", "dist_from_52_week_high", "target_median_price", "recommendation_mean",
"upside_potential"
);
}Data directory: c:\Users\aperi\DEV\LANG\dataLoad the datasets used throughout this notebook
Example for Load the datasets used throughout this notebook.
var dfP = DataFrame.ReadCsv(Path.Combine(DATA, "eurostoxx50_ohlcv.csv"), tryParseDates: true);
var dfM = LoadOhlcvCsv(DATA);
display($"OHLCV - Polars: {dfP.Shape} | MDA: ({dfM.Rows.Count}, {dfM.Columns.Count})");
var scP = DataFrame.ReadCsv(Path.Combine(DATA, "scores_daily.csv"), tryParseDates: true);
var scM = LoadScoresDailyCsv(DATA);
display($"Scores - Polars: {scP.Shape} | MDA: ({scM.Rows.Count}, {scM.Columns.Count})");
var dimP = DataFrame.ReadCsv(Path.Combine(DATA, "index_dim.csv"));
var dimM = MDA.DataFrame.LoadCsv(Path.Combine(DATA, "index_dim.csv"));
display($"index_dim - Polars: {dimP.Shape} | MDA: ({dimM.Rows.Count}, {dimM.Columns.Count})");OHLCV - Polars: (66355, 12) | MDA: (66355, 12)Scores - Polars: (466, 36) | MDA: (466, 36)index_dim - Polars: (169, 26) | MDA: (169, 26)Missing Data
Real-world datasets almost always contain missing values — sensor gaps, optional fields, failed joins, or upstream ETL issues. How you detect, quantify, and resolve nulls determines whether downstream aggregations and models produce correct results or silently propagate errors.
Polars.NET null model vs MDA typed nulls
Polars.NET keeps null handling, fill strategies, and interpolation inside expressions. Microsoft.Data.Analysis keeps the same semantics explicit through
NullCount,ElementwiseIsNull(), typed repair columns, andFilter(...).
Fix null generation upstream when possible
As Fundamentals of Data Engineering.epub emphasizes, null checks and data-quality guards are strongest near ingestion and transformation boundaries. Use local fills when the rule is analytical, not as a substitute for upstream contracts.
Which null strategy to use?
Pick the least misleading repair strategy for the data-generation pattern, not just the shortest code path.
Maps the missing-data decision path used in this section.
flowchart TD A["Rows with nulls"] --> B{"Nulls random<br/>and few?"} B -- Yes --> C["Drop rows<br/>DropNulls / Filter(mask)"] B -- No --> D{"Known default<br/>value?"} D -- Yes --> E["Fill literal<br/>FillNull / typed fill column"] D -- No --> F{"Ordered or time-series<br/>data?"} F -- Yes --> G{"Continuous numeric<br/>signal?"} G -- Yes --> H["Interpolate<br/>Interpolate / manual loop"] G -- No --> I["Carry nearest value<br/>Forward/Backward scan"] F -- No --> J["Fill with statistic or flag<br/>Mean/median or missing-indicator"]
Detect Nulls
Null detection is the first step in any data quality check. Scan each column for missing values to understand the scope of the problem before choosing a fill or drop strategy.
Polars.NET | Detect null rows with IsNull() filter
The IsNull() expression returns a boolean mask that can be passed to Filter() to isolate rows where a specific column is null. Iterating over Columns and checking the NullCount property on each Series gives a quick per-column summary without constructing a full filtered DataFrame.
Iterates over all columns in scP to print null counts for the 5 columns with missing values, then filters with IsNull() to display the 5 rows where ev_ebitda_zscore is null alongside their pe_zscore values.
display("Null counts per column:");
foreach (var col in scP.Columns)
{
var nc = scP.Column(col).NullCount;
if (nc > 0)
Console.WriteLine($" {col,-28} {nc,4} nulls");
}
display("Rows where ev_ebitda_zscore IS null (first 5):");
scP.Filter(Col("ev_ebitda_zscore").IsNull()).Head(5)
.Select("symbol", "score_date", "ev_ebitda_zscore", "pe_zscore")Null counts per column:
pe_zscore 3 nulls pb_zscore 6 nulls ev_ebitda_zscore 71 nulls yield_zscore 35 nulls recommendation_mean 14 nulls
Rows where ev_ebitda_zscore IS null (first 5):
| symbol | score_date | ev_ebitda_zscore | pe_zscore |
|---|---|---|---|
| BNP.PA | 2026-03-04 | null | 0.9133885393 |
| SAN.MC | 2026-03-04 | null | 0.4585988913 |
| ISP.MI | 2026-03-04 | null | 0.3666188825 |
| UCG.MI | 2026-03-04 | null | 0.45250099 |
| INGA.AS | 2026-03-04 | null | 0.3799166699 |
Microsoft.Data.Analysis | Detect nulls with NullCount and ElementwiseIsNull
MDA uses direct NullCount metadata and boolean masks for null inspection.
Prints sparse-column null counts and previews the first five null ev_ebitda_zscore rows.
// Microsoft.Data.Analysis — Detect nulls in scores_daily (has real nulls)
display("Null counts per column:");
foreach (var col in scP.Columns)
{
var nc = col.NullCount;
if (nc > 0)
Console.WriteLine($" {col.Name,-28} {nc,4} nulls");
}
display("Rows where ev_ebitda_zscore IS null (first 5):");
var nullMask = (PrimitiveDataFrameColumn<bool>)scP.Columns["ev_ebitda_zscore"].ElementwiseIsNull();
var filteredNulls = scP.Filter(nullMask);
new DataFrame(filteredNulls.Columns["symbol"], filteredNulls.Columns["score_date"], filteredNulls.Columns["ev_ebitda_zscore"], filteredNulls.Columns["pe_zscore"]).Head(5)Null counts per column: pe_zscore 3 nulls
pb_zscore 6 nulls
ev_ebitda_zscore 71 nulls
yield_zscore 35 nulls
recommendation_mean 14 nullsRows where ev_ebitda_zscore IS null (first 5):| symbol | score_date | ev_ebitda_zscore | pe_zscore |
|---|---|---|---|
| BNP.PA | 2026-03-04 00:00:00Z | <null> | 0.9133886 |
| SAN.MC | 2026-03-04 00:00:00Z | <null> | 0.4585989 |
| ISP.MI | 2026-03-04 00:00:00Z | <null> | 0.3666189 |
| UCG.MI | 2026-03-04 00:00:00Z | <null> | 0.452501 |
| INGA.AS | 2026-03-04 00:00:00Z | <null> | 0.3799167 |
Microsoft.Data.Analysis | Count nulls with DataFrameColumn.NullCount
NullCount is direct in MDA, so completeness checks are simpler than indirect present-value counting patterns.
Reports null counts for the five known sparse score columns.
// Microsoft.Data.Analysis — Count nulls per column with NullCount property
var nullCols = new[] { "pe_zscore", "pb_zscore", "ev_ebitda_zscore", "yield_zscore", "recommendation_mean" };
foreach (var col in nullCols)
{
if(scP.Columns.IndexOf(col) >= 0)
Console.WriteLine($" {col,-28} {scP.Columns[col].NullCount,4} / {scP.Rows.Count}");
}
display($"Total rows: {scP.Rows.Count}"); pe_zscore 3 / 466
pb_zscore 6 / 466
ev_ebitda_zscore 71 / 466
yield_zscore 35 / 466
recommendation_mean 14 / 466Total rows: 466Microsoft.Data.Analysis | Drop rows by building a validity mask
The notebook uses an explicit boolean mask plus Filter(...) for whole-row null dropping.
Scans each score row for nulls, filters valid rows, and previews the first five survivors.
// Microsoft.Data.Analysis — Drop rows where any column has null
var validMask = new PrimitiveDataFrameColumn<bool>("mask", scP.Rows.Count);
for (long i = 0; i < scP.Rows.Count; i++)
{
bool hasNull = false;
foreach (var col in scP.Columns)
{
if (col[i] == null) { hasNull = true; break; }
}
validMask[i] = !hasNull;
}
var scPDropped = scP.Filter(validMask);
display($"Before: {scP.Rows.Count} rows | After DropNulls: {scPDropped.Rows.Count} rows");
new DataFrame(scPDropped.Columns["symbol"], scPDropped.Columns["score_date"], scPDropped.Columns["ev_ebitda_zscore"], scPDropped.Columns["pe_zscore"]).Head(5)Before: 466 rows | After DropNulls: 346 rows| symbol | score_date | ev_ebitda_zscore | pe_zscore |
|---|---|---|---|
| DTE.DE | 2026-03-04 00:00:00Z | 0.3795319 | 0.3265871 |
| IFX.DE | 2026-03-04 00:00:00Z | 0.6770676 | 0.5093979 |
| ENR.DE | 2026-03-04 00:00:00Z | -1.693212 | -0.9027377 |
| ABI.BR | 2026-03-04 00:00:00Z | 0.5527391 | 0.4740837 |
| TTE.PA | 2026-03-04 00:00:00Z | 0.4496105 | 0.6911064 |
Microsoft.Data.Analysis | Fill nulls with an explicit typed replacement column
MDA repairs a column by materializing a typed output column and swapping it back into the frame.
Fills null ev_ebitda_zscore values with 0.0 and confirms the null count drops to zero.
// Microsoft.Data.Analysis — Fill null ev_ebitda_zscore with 0.0
var filledCol = new PrimitiveDataFrameColumn<decimal>("ev_ebitda_zscore_filled", scP.Rows.Count);
var origCol = scP.Columns["ev_ebitda_zscore"];
for(long i = 0; i < scP.Rows.Count; i++) filledCol[i] = origCol[i] != null ? Convert.ToDecimal(origCol[i]) : 0.0m;
var scPFilled = scP.Clone(); scPFilled.Columns.Remove("ev_ebitda_zscore"); filledCol.SetName("ev_ebitda_zscore"); scPFilled.Columns.Add(filledCol);
display($"Nulls after FillNull(0.0): {scPFilled.Columns["ev_ebitda_zscore"].NullCount}");
new DataFrame(scPFilled.Columns["symbol"], scPFilled.Columns["score_date"], scPFilled.Columns["ev_ebitda_zscore"]).Head(5)Nulls after FillNull(0.0): 0| symbol | score_date | ev_ebitda_zscore |
|---|---|---|
| BNP.PA | 2026-03-04 00:00:00Z | 0.0 |
| DTE.DE | 2026-03-04 00:00:00Z | 0.3795319 |
| IFX.DE | 2026-03-04 00:00:00Z | 0.6770676 |
| ENR.DE | 2026-03-04 00:00:00Z | -1.693212 |
| ABI.BR | 2026-03-04 00:00:00Z | 0.5527391 |
Microsoft.Data.Analysis | Forward fill with a carry-forward loop
Forward fill in MDA is usually an ordered scan with explicit state.
Carries the last observed ev_ebitda_zscore value forward through later gaps.
// Microsoft.Data.Analysis — Forward fill: propagate last valid value forward
var ffillCol = new PrimitiveDataFrameColumn<decimal>("ev_ebitda_zscore", scP.Rows.Count);
decimal? lastValid = null;
for(long i = 0; i < scP.Rows.Count; i++) { if (origCol[i] != null) lastValid = Convert.ToDecimal(origCol[i]); if (lastValid.HasValue) ffillCol[i] = lastValid.Value; }
var scPFfill = scP.Clone(); scPFfill.Columns.Remove("ev_ebitda_zscore"); scPFfill.Columns.Add(ffillCol);
display($"Nulls after ForwardFill: {scPFfill.Columns["ev_ebitda_zscore"].NullCount}");
new DataFrame(scPFfill.Columns["symbol"], scPFfill.Columns["score_date"], scPFfill.Columns["ev_ebitda_zscore"]).Head(5)Nulls after ForwardFill: 1| symbol | score_date | ev_ebitda_zscore |
|---|---|---|
| BNP.PA | 2026-03-04 00:00:00Z | <null> |
| DTE.DE | 2026-03-04 00:00:00Z | 0.3795319 |
| IFX.DE | 2026-03-04 00:00:00Z | 0.6770676 |
| ENR.DE | 2026-03-04 00:00:00Z | -1.693212 |
| ABI.BR | 2026-03-04 00:00:00Z | 0.5527391 |
Microsoft.Data.Analysis | Backward fill with a reverse scan
Backward fill is the same idea in reverse order.
Propagates the next observed ev_ebitda_zscore value backward into earlier gaps.
// Microsoft.Data.Analysis — Backward fill: propagate next valid value backward
var bfillCol = new PrimitiveDataFrameColumn<decimal>("ev_ebitda_zscore", scP.Rows.Count);
decimal? nextValid = null;
for(long i = scP.Rows.Count - 1; i >= 0; i--) { if (origCol[i] != null) nextValid = Convert.ToDecimal(origCol[i]); if (nextValid.HasValue) bfillCol[i] = nextValid.Value; }
var scPBfill = scP.Clone(); scPBfill.Columns.Remove("ev_ebitda_zscore"); scPBfill.Columns.Add(bfillCol);
display($"Nulls after BackwardFill: {scPBfill.Columns["ev_ebitda_zscore"].NullCount}");
new DataFrame(scPBfill.Columns["symbol"], scPBfill.Columns["score_date"], scPBfill.Columns["ev_ebitda_zscore"]).Head(5)Nulls after BackwardFill: 0| symbol | score_date | ev_ebitda_zscore |
|---|---|---|
| BNP.PA | 2026-03-04 00:00:00Z | 0.3795319 |
| DTE.DE | 2026-03-04 00:00:00Z | 0.3795319 |
| IFX.DE | 2026-03-04 00:00:00Z | 0.6770676 |
| ENR.DE | 2026-03-04 00:00:00Z | -1.693212 |
| ABI.BR | 2026-03-04 00:00:00Z | 0.5527391 |
Microsoft.Data.Analysis | Fill nulls with the column mean
A common MDA pattern is compute-then-materialize: first the statistic, then the repaired column.
Computes the mean of non-null values and fills gaps with that mean.
// Microsoft.Data.Analysis — Fill null with column mean
decimal sum = 0m; int count = 0;
for(long i = 0; i < scP.Rows.Count; i++) if(origCol[i] != null) { sum += Convert.ToDecimal(origCol[i]); count++; }
decimal mean = count > 0 ? sum / count : 0m;
var meanFillCol = new PrimitiveDataFrameColumn<decimal>("ev_ebitda_zscore", scP.Rows.Count);
for(long i = 0; i < scP.Rows.Count; i++) meanFillCol[i] = origCol[i] != null ? Convert.ToDecimal(origCol[i]) : mean;
var scPMeanFill = scP.Clone(); scPMeanFill.Columns.Remove("ev_ebitda_zscore"); scPMeanFill.Columns.Add(meanFillCol);
display($"Nulls after FillNull(mean): {scPMeanFill.Columns["ev_ebitda_zscore"].NullCount}");
new DataFrame(scPMeanFill.Columns["symbol"], scPMeanFill.Columns["score_date"], scPMeanFill.Columns["ev_ebitda_zscore"]).Head(5)Nulls after FillNull(mean): 0| symbol | score_date | ev_ebitda_zscore |
|---|---|---|
| BNP.PA | 2026-03-04 00:00:00Z | 0.0380461401518987341772151899 |
| DTE.DE | 2026-03-04 00:00:00Z | 0.3795319 |
| IFX.DE | 2026-03-04 00:00:00Z | 0.6770676 |
| ENR.DE | 2026-03-04 00:00:00Z | -1.693212 |
| ABI.BR | 2026-03-04 00:00:00Z | 0.5527391 |
Microsoft.Data.Analysis | Interpolate missing values manually
MDA has no interpolation expression, so the notebook computes linear interpolation explicitly.
Searches backward and forward for neighboring values and linearly interpolates each gap.
// Microsoft.Data.Analysis — Linear interpolation of missing values
var interpCol = new PrimitiveDataFrameColumn<decimal>("ev_ebitda_zscore", scP.Rows.Count);
for(long i = 0; i < scP.Rows.Count; i++)
{
if(origCol[i] != null) interpCol[i] = Convert.ToDecimal(origCol[i]);
else {
decimal? prev = null; long prevIdx = -1;
for(long j = i - 1; j >= 0; j--) if(origCol[j] != null) { prev = Convert.ToDecimal(origCol[j]); prevIdx = j; break; }
decimal? next = null; long nextIdx = -1;
for(long j = i + 1; j < scP.Rows.Count; j++) if(origCol[j] != null) { next = Convert.ToDecimal(origCol[j]); nextIdx = j; break; }
if(prev.HasValue && next.HasValue) { decimal ratio = (decimal)(i - prevIdx) / (nextIdx - prevIdx); interpCol[i] = prev.Value + ratio * (next.Value - prev.Value); }
else if (prev.HasValue) interpCol[i] = prev.Value;
else if (next.HasValue) interpCol[i] = next.Value;
}
}
var scPInterp = scP.Clone(); scPInterp.Columns.Remove("ev_ebitda_zscore"); scPInterp.Columns.Add(interpCol);
display($"Nulls after Interpolate: {scPInterp.Columns["ev_ebitda_zscore"].NullCount}");
new DataFrame(scPInterp.Columns["symbol"], scPInterp.Columns["score_date"], scPInterp.Columns["ev_ebitda_zscore"]).Head(10)Nulls after Interpolate: 0| symbol | score_date | ev_ebitda_zscore |
|---|---|---|
| BNP.PA | 2026-03-04 00:00:00Z | 0.3795319 |
| DTE.DE | 2026-03-04 00:00:00Z | 0.3795319 |
| IFX.DE | 2026-03-04 00:00:00Z | 0.6770676 |
| ENR.DE | 2026-03-04 00:00:00Z | -1.693212 |
| ABI.BR | 2026-03-04 00:00:00Z | 0.5527391 |
| VOW.DE | 2026-03-04 00:00:00Z | 0.3798302 |
| TTE.PA | 2026-03-04 00:00:00Z | 0.4496105 |
| DG.PA | 2026-03-04 00:00:00Z | 0.9287967 |
| SAN.MC | 2026-03-04 00:00:00Z | 0.434032285 |
| SU.PA | 2026-03-04 00:00:00Z | -0.06073213 |
Microsoft.Data.Analysis | Coalesce columns with the null-coalescing operator
MDA emulates coalesce by testing candidate columns in order and writing the first non-null value.
Combines primary, secondary, and fallback into a single best column.
// Microsoft.Data.Analysis — Coalesce columns
var primaryCol = new PrimitiveDataFrameColumn<decimal>("primary", new decimal?[] { 100.0m, null, 300.0m, null });
var secondaryCol = new PrimitiveDataFrameColumn<decimal>("secondary", new decimal?[] { null, 200.0m, null, 400.0m });
var fallbackCol = new PrimitiveDataFrameColumn<decimal>("fallback", new decimal?[] { 50.0m, 50.0m, 50.0m, 50.0m });
var coalDf = new DataFrame(primaryCol, secondaryCol, fallbackCol);
var bestCol = new PrimitiveDataFrameColumn<decimal>("best", coalDf.Rows.Count);
for(long i = 0; i < coalDf.Rows.Count; i++) bestCol[i] = primaryCol[i] ?? secondaryCol[i] ?? fallbackCol[i];
var coalResult = coalDf.Clone(); coalResult.Columns.Add(bestCol); coalResult| primary | secondary | fallback | best |
|---|---|---|---|
| 100.0 | <null> | 50.0 | 100.0 |
| <null> | 200.0 | 50.0 | 200.0 |
| 300.0 | <null> | 50.0 | 300.0 |
| <null> | 400.0 | 50.0 | 400.0 |
Microsoft.Data.Analysis | Convert to upper and lower case with StringDataFrameColumn
MDA uses CLR string transforms plus new typed string columns.
Builds uppercase and lowercase symbol columns and previews the first ten rows.
var symbolsM = dfM.Columns["symbol"].Cast<string>().Distinct().ToArray();
var upperColM = new MDA.StringDataFrameColumn("upper", symbolsM.Select(s => s?.ToUpper()));
var lowerColM = new MDA.StringDataFrameColumn("lower", symbolsM.Select(s => s?.ToLower()));
var caseDemoM = new MDA.DataFrame(new MDA.StringDataFrameColumn("symbol", symbolsM), upperColM, lowerColM);
caseDemoM.Head(10)| symbol | upper | lower |
|---|---|---|
| ABI.BR | ABI.BR | abi.br |
| AD.AS | AD.AS | ad.as |
| ADS.DE | ADS.DE | ads.de |
| ADYEN.AS | ADYEN.AS | adyen.as |
| AI.PA | AI.PA | ai.pa |
Contains, StartsWith, EndsWith
Pattern matching on string columns is the primary way to filter by exchange code, country suffix, or naming convention. These operations return boolean masks suitable for Filter().
Polars.NET | Filter with Str.Contains()
Str.Contains(pattern) takes a plain string argument (not wrapped in Lit()) and returns a boolean expression. Pass it to Filter() to keep only matching rows.
Filters the OHLCV DataFrame to rows where symbol contains ".DE", then selects unique symbols — returning the 16 German-listed stocks from the 50-member index.
var germanP = dfP.Filter(Col("symbol").Str.Contains(".DE"))
.Select(new[] { "symbol" }).Unique();
display("German exchange symbols (.DE):");
germanPGerman exchange symbols (.DE):
| symbol |
|---|
| ADS.DE |
| ALV.DE |
| BAS.DE |
| BAYN.DE |
| BMW.DE |
Microsoft.Data.Analysis | Filter symbols with .Contains()
For light string filters, MDA often materializes a string array and filters it with CLR predicates.
Filters unique symbols to the German exchange tickers containing .DE.
var germanSymbolsM = symbolsM.Where(s => s != null && s.Contains(".DE")).ToArray();
display("German exchange symbolsM (.DE):");
new MDA.DataFrame(new MDA.StringDataFrameColumn("symbol", germanSymbolsM))German exchange symbols (.DE):| symbol |
|---|
| ADS.DE |
| ALV.DE |
| BAS.DE |
| BAYN.DE |
| BMW.DE |
Polars.NET | Filter with Str.StartsWith() and Str.EndsWith()
Str.StartsWith() and Str.EndsWith() take plain string arguments, like Str.Contains(). They can be combined with Filter() to select rows matching a prefix or suffix pattern.
Applies Str.StartsWith("S") and Str.EndsWith(".BR") in separate filter passes, rendering both result sets side by side as HTML — 7 symbols starting with S and 2 Brussels-listed symbols.
var startsS = dfP.Filter(Col("symbol").Str.StartsWith("S"))
.Select(new[] { "symbol" }).Unique();
var endsBR = dfP.Filter(Col("symbol").Str.EndsWith(".BR"))
.Select(new[] { "symbol" }).Unique();
// Display side by side using raw HTML
string StripQuotes(string h) => h.Replace("", "").Replace("\"", "");
var leftHtml = StripQuotes(startsS.ToHtml());
var rightHtml = StripQuotes(endsBR.ToHtml());
display(HTML($"<div style='display:flex;gap:40px'><div><b>StartsWith S</b>{leftHtml}</div><div><b>EndsWith .BR</b>{rightHtml}</div></div>"));| symbol |
|---|
| SAF.PA |
| SAN.MC |
| SAN.PA |
| SAP.DE |
| SGO.PA |
| symbol |
|---|
| ABI.BR |
| ARGX.BR |
Microsoft.Data.Analysis | Filter with StartsWith and EndsWith
Prefix and suffix filters follow the same CLR-first pattern.
Displays symbols starting with S and symbols ending with .BR.
var startsSM = symbolsM.Where(s => s != null && s.StartsWith("S")).ToArray();
var endsBRM = symbolsM.Where(s => s != null && s.EndsWith(".BR")).ToArray();
display("StartsWith S:");
display(new MDA.DataFrame(new MDA.StringDataFrameColumn("symbol", startsSM)));
display("EndsWith .BR:");
new MDA.DataFrame(new MDA.StringDataFrameColumn("symbol", endsBRM))StartsWith S:| symbol |
|---|
| SAF.PA |
| SAN.MC |
| SAN.PA |
| SAP.DE |
| SGO.PA |
EndsWith .BR:| symbol |
|---|
| ABI.BR |
| ARGX.BR |
Replace
Substring replacement is used for cleaning identifiers, normalizing naming conventions, or masking sensitive parts of strings. Polars provides both single-match Replace() and global ReplaceAll().
Polars.NET | Replace substrings with Str.ReplaceAll()
Str.ReplaceAll(old, new) replaces every occurrence of the pattern in each string. For single-match replacement, use Str.Replace(). Both accept plain strings (not Lit()).
Applies Str.ReplaceAll(".DE", "_GER") to all 50 symbols, then filters to the 16 replaced entries — showing ADS.DE → ADS_GER, ALV.DE → ALV_GER, etc.
var replaced = dfP.Select(new[] { "symbol" }).Unique()
.WithColumns(
Col("symbol").Str.ReplaceAll(".DE", "_GER").Alias("replaced")
);
display("Replace '.DE' with '_GER':");
replaced.Filter(Col("replaced").Str.Contains("_GER"))Replace ‘.DE’ with ‘_GER’:
| symbol | replaced |
|---|---|
| ADS.DE | ADS_GER |
| ALV.DE | ALV_GER |
| BAS.DE | BAS_GER |
| BAYN.DE | BAYN_GER |
| BMW.DE | BMW_GER |
Microsoft.Data.Analysis | Replace substrings with CLR string replacement
Replacement is explicit managed-code work over the string values.
Replaces .DE with _GER and shows only the affected identifiers.
var replacedArrM = symbolsM.Select(s => s?.Replace(".DE", "_GER")).ToArray();
var replacedDfM = new MDA.DataFrame(new MDA.StringDataFrameColumn("symbol", symbolsM), new MDA.StringDataFrameColumn("replaced", replacedArrM));
display("Replace '.DE' with '_GER':");
var maskM = new MDA.PrimitiveDataFrameColumn<bool>("maskM", replacedDfM.Rows.Count);
for(long i = 0; i < replacedDfM.Rows.Count; i++) maskM[i] = replacedArrM[i]?.Contains("_GER") == true;
replacedDfM.Filter(maskM)Replace '.DE' with '_GER':| symbol | replaced |
|---|---|
| ADS.DE | ADS_GER |
| ALV.DE | ALV_GER |
| BAS.DE | BAS_GER |
| BAYN.DE | BAYN_GER |
| BMW.DE | BMW_GER |
Length and Slicing
Measuring string length and extracting fixed-position substrings are building blocks for parsing structured identifiers like ticker symbols, ISINs, or fixed-width codes.
Polars.NET | Measure string length
In Polars.NET 0.4.0, the Str.LenChars() method is not yet exposed. As a workaround, extract the column to a C# array, compute lengths with LINQ, and stack the result back onto the DataFrame.
Extracts symbols to a C# array, computes each string’s .Length, stacks the result back as a char_len series, then sorts descending to show that NDA-FI.HE (9 chars) is the longest ticker in the index.
var symDf = dfP.Select(new[] { "symbol" }).Unique();
var symArr = symDf.Column("symbol").ToArray<string>();
var lenArr = symArr.Select(s => (double)s.Length).ToArray();
var lenSeries = Polars.CSharp.Series.From("char_len", lenArr);
var lengths = symDf.HStack(lenSeries);
lengths.Sort("char_len", descending: true).Head(10)| symbol | char_len |
|---|---|
| NDA-FI.HE | 9 |
| ADYEN.AS | 8 |
| ARGX.BR | 7 |
| ASML.AS | 7 |
| BAYN.DE | 7 |
Microsoft.Data.Analysis | Measure string length with a typed numeric column
String length in MDA is usually projected into a numeric typed column.
Computes symbol lengths and orders the result by descending length.
var lengthsM = symbolsM.Select(s => s != null ? (double)s.Length : 0).ToArray();
var lenDfM = new MDA.DataFrame(new MDA.StringDataFrameColumn("symbol", symbolsM), new MDA.PrimitiveDataFrameColumn<decimal>("char_len", lengthsM));
lenDfM.OrderByDescending("char_len").Head(10)| symbol | char_len |
|---|---|
| NDA-FI.HE | 9 |
| ADYEN.AS | 8 |
| INGA.AS | 7 |
| ENEL.MI | 7 |
| ARGX.BR | 7 |
Polars.NET | Extract substrings with Str.Slice()
Str.Slice(offset, length) extracts a fixed-position substring from each value. The offset is zero-based. This is useful for fixed-width parsing but not for variable-length identifiers — use Str.Split() or Str.Extract() with regex for those.
Applies Str.Slice(0, 3) to all unique symbols, creating a first_3 column — the first 10 rows show three-character prefixes like ABI, AD., ADS, ADY.
var sliced = dfP.Select(new[] { "symbol" }).Unique()
.WithColumns(
Col("symbol").Str.Slice(0, 3).Alias("first_3")
);
sliced.Head(10)| symbol | first_3 |
|---|---|
| ABI.BR | ABI |
| AD.AS | AD. |
| ADS.DE | ADS |
| ADYEN.AS | ADY |
| AI.PA | AI. |
Microsoft.Data.Analysis | Slice strings with Substring()
Fixed-position slicing uses CLR substring logic before materialization.
Extracts the first three characters of each symbol into first_3.
var first3ArrM = symbolsM.Select(s => s != null ? (s.Length >= 3 ? s.Substring(0, 3) : s) : null).ToArray();
var slicedM = new MDA.DataFrame(new MDA.StringDataFrameColumn("symbol", symbolsM), new MDA.StringDataFrameColumn("first_3", first3ArrM));
slicedM.Head(10)| symbol | first_3 |
|---|---|
| ABI.BR | ABI |
| AD.AS | AD. |
| ADS.DE | ADS |
| ADYEN.AS | ADY |
| AI.PA | AI. |
Split
Splitting strings by a delimiter decomposes composite identifiers into their parts — for example, splitting "ASML.AS" on "." yields the ticker (ASML) and the exchange code (AS). Polars returns a list column; Microsoft.Data.Analysis usually materializes a display-friendly string or a custom typed projection instead.
Polars.NET | Split strings with Str.Split()
Str.Split(separator) splits each string into a list of substrings. The result is a column of type List[Str]. Access individual elements using list indexing expressions in downstream operations.
Splits all unique symbols on ".", producing a List[Str] column where each cell contains the ticker and exchange code as a two-element list — e.g., ABI.BR → [ABI, BR].
var split = dfP.Select(new[] { "symbol" }).Unique()
.WithColumns(
Col("symbol").Str.Split(".").Alias("parts")
);
split.Head(10)| symbol | parts |
|---|---|
| ABI.BR | [ABI, BR] |
| AD.AS | [AD, AS] |
| ADS.DE | [ADS, DE] |
| ADYEN.AS | [ADYEN, AS] |
| AI.PA | [AI, PA] |
Microsoft.Data.Analysis | Split strings and materialize a display column
MDA does not expose a list-typed split result, so the notebook stores a readable serialized form.
Splits each symbol on . and stores the rendered parts string for inspection.
var splitArrM = symbolsM.Select(s => s != null ? $"[\"{string.Join("\", \"", s.Split('.'))}\"]" : null).ToArray();
var splitDfM = new MDA.DataFrame(new MDA.StringDataFrameColumn("symbol", symbolsM), new MDA.StringDataFrameColumn("parts", splitArrM));
splitDfM.Head(10)| symbol | parts |
|---|---|
| ABI.BR | ["ABI", "BR"] |
| AD.AS | ["AD", "AS"] |
| ADS.DE | ["ADS", "DE"] |
| ADYEN.AS | ["ADYEN", "AS"] |
| AI.PA | ["AI", "PA"] |
Regex Extract
Regular expressions provide flexible pattern matching for extracting structured components from strings. Use regex when the delimiter is inconsistent or when you need to match a specific pattern (e.g., “the part after the last dot”).
Polars.NET | Extract capture group with Str.Extract()
Str.Extract(pattern, groupIndex) applies a regex to each string and returns the specified capture group. Group index 1 refers to the first parenthesized group. Returns null for non-matching strings.
Applies the regex \.(\w+) with Str.Extract(pattern, 1) to extract the exchange code suffix from all 50 unique symbols, returning null for any symbol without a dot — the first 10 rows show BR, AS, DE, etc.
var extracted = dfP.Select(new[] { "symbol" }).Unique()
.WithColumns(
Col("symbol").Str.Extract(@"\.(\w+)", 1).Alias("exchange")
);
extracted.Head(10)| symbol | exchange |
|---|---|
| ABI.BR | BR |
| AD.AS | AS |
| ADS.DE | DE |
| ADYEN.AS | AS |
| AI.PA | PA |
Microsoft.Data.Analysis | Extract regex groups with Regex.Match
Regex extraction is standard .NET regex work over the materialized string values.
Captures the exchange code after the dot and previews the first ten results.
var regexM = new Regex(@"\.(\w+)");
var exchangeArrM = symbolsM.Select(s => s != null && regexM.IsMatch(s) ? regexM.Match(s).Groups[1].Value : null).ToArray();
var extractedM = new MDA.DataFrame(new MDA.StringDataFrameColumn("symbol", symbolsM), new MDA.StringDataFrameColumn("exchange", exchangeArrM));
extractedM.Head(10)| symbol | exchange |
|---|---|
| ABI.BR | BR |
| AD.AS | AS |
| ADS.DE | DE |
| ADYEN.AS | AS |
| AI.PA | PA |
Padding
Padding strings to a fixed width is common when generating fixed-width output files, aligning display columns, or creating zero-padded identifiers (e.g., "0000ABI.BR").
Polars.NET | Pad strings with PadLeft (C# workaround)
In Polars.NET 0.4.0, Str.PadStart() is not yet exposed. As a workaround, extract values to a C# array, apply string.PadLeft(), and stack the result back.
Extracts unique symbols to a C# array, pads each to 10 characters with leading zeros using PadLeft(10, '0'), and stacks the result back — showing ABI.BR → 0000ABI.BR and ADYEN.AS → 00ADYEN.AS.
var symDfPad = dfP.Select(new[] { "symbol" }).Unique();
var symArrPad = symDfPad.Column("symbol").ToArray<string>();
var paddedArr = symArrPad.Select(s => s.PadLeft(10, '0')).ToArray();
var paddedSeries = Polars.CSharp.Series.From("padded", paddedArr);
symDfPad.HStack(paddedSeries).Head(10)| symbol | padded |
|---|---|
| ABI.BR | 0000ABI.BR |
| AD.AS | 00000AD.AS |
| ADS.DE | 0000ADS.DE |
| ADYEN.AS | 00ADYEN.AS |
| AI.PA | 00000AI.PA |
Microsoft.Data.Analysis | Pad strings with PadLeft
Padding fits naturally with MDA’s CLR-centric string workflow.
Pads each symbol to width 10 with leading zeroes.
var paddedArrM = symbolsM.Select(s => s?.PadLeft(10, '0')).ToArray();
var symDfPadM = new MDA.DataFrame(new MDA.StringDataFrameColumn("symbol", symbolsM), new MDA.StringDataFrameColumn("padded", paddedArrM));
symDfPadM.Head(10)| symbol | padded |
|---|---|
| ABI.BR | 0000ABI.BR |
| AD.AS | 00000AD.AS |
| ADS.DE | 0000ADS.DE |
| ADYEN.AS | 00ADYEN.AS |
| AI.PA | 00000AI.PA |
Concatenation
Combining values from multiple string columns into a single formatted string — for example, building display labels like "ASML HOLDING (Netherlands)". Polars.NET 0.4.0 does not expose ConcatStr, so extract columns to C# arrays and use string interpolation.
Polars.NET | Concatenate strings via C# Zip
Extract string columns to arrays, combine with Zip and string interpolation, then stack the result back as a new series.
Extracts short_name and country arrays from dimP, zips them with string interpolation to produce "ASML HOLDING (Netherlands)" style labels, and stacks the result back as a display_name column.
var nameArr = dimP.Column("short_name").ToArray<string>();
var countryArr = dimP.Column("country").ToArray<string>();
var displayNames = nameArr.Zip(countryArr, (n, c) => $"{n} ({c})").ToArray();
var dnSeries = Polars.CSharp.Series.From("display_name", displayNames);
dimP.Select("short_name", "country").HStack(dnSeries).Head(10)| short_name | country | display_name |
|---|---|---|
| ASML HOLDING | Netherlands | ASML HOLDING (Netherlands) |
| LVMH | France | LVMH (France) |
| HERMES INTL | France | HERMES INTL (France) |
| L'OREAL | France | L'OREAL (France) |
| SAP SE | Germany | SAP SE (Germany) |
Microsoft.Data.Analysis | Concatenate columns into a display label
String interpolation over source columns is the common MDA pattern for labels and reporting fields.
Builds display_name = short_name + " (country)" from index_dim and previews the first ten rows.
// Microsoft.Data.Analysis — Build display name "SHORT_NAME (COUNTRY)"
var shortNameCol = dimP.Columns["short_name"];
var countryCol = dimP.Columns["country"];
var displayNames = new StringDataFrameColumn("display_name", dimP.Rows.Count);
for(long i = 0; i < dimP.Rows.Count; i++)
{
displayNames[i] = $"{shortNameCol[i]} ({countryCol[i]})";
}
new DataFrame(shortNameCol, countryCol, displayNames).Head(10)| short_name | country | display_name |
|---|---|---|
| ASML HOLDING | Netherlands | ASML HOLDING (Netherlands) |
| LVMH | France | LVMH (France) |
| HERMES INTL | France | HERMES INTL (France) |
| L'OREAL | France | L'OREAL (France) |
| SAP SE | Germany | SAP SE (Germany) |
| SIEMENS AG | Germany | SIEMENS AG (Germany) |
| INDUSTRIA DE DISE...O TEXTIL S. | Spain | INDUSTRIA DE DISE...O TEXTIL S. (Spain) |
| DEUTSCHE TELEKOM AG | Germany | DEUTSCHE TELEKOM AG (Germany) |
| BANCO SANTANDER S.A. | Spain | BANCO SANTANDER S.A. (Spain) |
| SCHNEIDER ELECTRIC SE | France | SCHNEIDER ELECTRIC SE (France) |
Microsoft.Data.Analysis | Trim whitespace with CLR string methods
Whitespace stripping is straightforward once values are already materialized as CLR strings.
Builds a small demo frame and trims leading and trailing whitespace from each value.
// Microsoft.Data.Analysis — Trim whitespace
var dirtyArr = new[] { " ASML ", " SAP ", " MC" };
var dirtySeries = new StringDataFrameColumn("name", dirtyArr);
var dirtyDf = new DataFrame(dirtySeries);
var trimSeries = new StringDataFrameColumn("stripped", dirtyArr.Select(s => s?.Trim()));
dirtyDf.Columns.Add(trimSeries);
dirtyDf| name | stripped |
|---|---|
| ASML | ASML |
| SAP | SAP |
| MC | MC |
Microsoft.Data.Analysis | Extract all regex matches with Regex.Matches
For all-match extraction, MDA relies on the CLR regex engine and explicit output columns.
Extracts all numeric substrings, stores the joined matches, and records their count.
// Microsoft.Data.Analysis — Extract all numbers from text using Regex
var textArr = new[] { "ASML closed at 900.5 up from 895.2", "No numbers", "PE: 45.3, PB: 12.1" };
var numRegex = new Regex(@"[0-9]+\.?[0-9]*");
var textDf = new DataFrame(new StringDataFrameColumn("text", textArr));
var numbersArr = textArr.Select(s => string.Join(", ", numRegex.Matches(s).Select(m => m.Value))).ToArray();
var countArr = textArr.Select(s => numRegex.Matches(s).Count).ToArray();
textDf.Columns.Add(new StringDataFrameColumn("numbers", numbersArr));
textDf.Columns.Add(new PrimitiveDataFrameColumn<int>("count", countArr));
textDf| text | numbers | count |
|---|---|---|
| ASML closed at 900.5 up from 895.2 | 900.5, 895.2 | 2 |
| No numbers | 0 | |
| PE: 45.3, PB: 12.1 | 45.3, 12.1 | 2 |
Microsoft.Data.Analysis | Rely on LoadCsv inference or parse into DateTime
MDA often lands dates as CLR DateTime values during LoadCsv; reparsing is explicit when needed.
Prints the inferred type, materializes a string version, reparses it, and shows the columns together.
display($"date column type: {dfM.Columns["date"].DataType.Name}");
var dateStrColM = new MDA.StringDataFrameColumn("date_str", dfM.Rows.Count);
var dateReparsedColM = new MDA.PrimitiveDataFrameColumn<DateTime>("date_reparsed", dfM.Rows.Count);
for(long i = 0; i < dfM.Rows.Count; i++)
{
if(dfM.Columns["date"][i] is DateTime dt)
{
string s = dt.ToString("yyyy-MM-dd");
dateStrColM[i] = s;
if(DateTime.TryParse(s, out var p)) dateReparsedColM[i] = p;
}
}
display($"Cast to string: {dateStrColM.DataType.Name}");
display($"After Parse: {dateReparsedColM.DataType.Name}");
new MDA.DataFrame(dfM.Columns["symbol"], dfM.Columns["date"], dateStrColM, dateReparsedColM).Head(5)date column type: DateTimeCast to string: StringAfter Parse: DateTime| symbol | date | date_str | date_reparsed |
|---|---|---|---|
| ABI.BR | 2021-01-04 00:00:00Z | 2021-01-04 | 2021-01-04 00:00:00Z |
| ABI.BR | 2021-01-05 00:00:00Z | 2021-01-05 | 2021-01-05 00:00:00Z |
| ABI.BR | 2021-01-06 00:00:00Z | 2021-01-06 | 2021-01-06 00:00:00Z |
| ABI.BR | 2021-01-07 00:00:00Z | 2021-01-07 | 2021-01-07 00:00:00Z |
| ABI.BR | 2021-01-08 00:00:00Z | 2021-01-08 | 2021-01-08 00:00:00Z |
Extract Date Components
Extracting year, month, day, and weekday from date columns enables time-based grouping (monthly aggregation, weekday analysis), filtering (Q1 only, weekdays only), and feature engineering for models.
Polars.NET | Extract year, month, weekday with Dt accessor
The .Dt accessor provides .Year(), .Month(), .Day(), .Weekday(), and other component extractors. These return integer expressions. Polars weekday numbering: Monday = 1, Sunday = 7 (ISO 8601).
Adds year, month, and weekday columns to the OHLCV DataFrame using .Dt.Year(), .Dt.Month(), and .Dt.Weekday(), showing the first 10 rows for ABI.BR in January 2021 — weekdays 1–5 confirming sequential trading day ordering.
var dateComponents = dfP.WithColumns(
Col("date").Dt.Year().Alias("year"),
Col("date").Dt.Month().Alias("month"),
Col("date").Dt.Weekday().Alias("weekday")
);
dateComponents.Select(new[] { "symbol", "date", "year", "month", "weekday" }).Head(10)| symbol | date | year | month | weekday |
|---|---|---|---|---|
| ABI.BR | 2021-01-04 | 2021 | 1 | 1 |
| ABI.BR | 2021-01-05 | 2021 | 1 | 2 |
| ABI.BR | 2021-01-06 | 2021 | 1 | 3 |
| ABI.BR | 2021-01-07 | 2021 | 1 | 4 |
| ABI.BR | 2021-01-08 | 2021 | 1 | 5 |
Microsoft.Data.Analysis | Extract year, month, and weekday with DateTime
Temporal feature extraction uses DateTime properties and typed target columns.
Adds year, month, and ISO-like weekday columns derived from the typed date column.
var yearColM = new MDA.PrimitiveDataFrameColumn<int>("year", dfM.Rows.Count);
var monthColM = new MDA.PrimitiveDataFrameColumn<int>("month", dfM.Rows.Count);
var weekdayColM = new MDA.PrimitiveDataFrameColumn<int>("weekday", dfM.Rows.Count);
for(long i = 0; i < dfM.Rows.Count; i++)
{
if(dfM.Columns["date"][i] is DateTime dt)
{
yearColM[i] = dt.Year;
monthColM[i] = dt.Month;
weekdayColM[i] = (int)dt.DayOfWeek == 0 ? 7 : (int)dt.DayOfWeek; // Standardize to 1-7 (Mon-Sun)
}
}
new MDA.DataFrame(dfM.Columns["symbol"], dfM.Columns["date"], yearColM, monthColM, weekdayColM).Head(10)| symbol | date | year | month | weekday |
|---|---|---|---|---|
| ABI.BR | 2021-01-04 00:00:00Z | 2021 | 1 | 1 |
| ABI.BR | 2021-01-05 00:00:00Z | 2021 | 1 | 2 |
| ABI.BR | 2021-01-06 00:00:00Z | 2021 | 1 | 3 |
| ABI.BR | 2021-01-07 00:00:00Z | 2021 | 1 | 4 |
| ABI.BR | 2021-01-08 00:00:00Z | 2021 | 1 | 5 |
Date Arithmetic
Adding or subtracting durations from date columns is essential for computing settlement dates, lookback windows, and expiration dates. Polars uses string-encoded duration offsets ("7d", "1mo"). Microsoft.Data.Analysis relies on CLR DateTime.AddDays() and related .NET temporal APIs.
Polars.NET | Add days with Dt.OffsetBy()
Dt.OffsetBy("7d") adds a duration string to every value in a date column. Supported units: d (days), w (weeks), mo (months), y (years), h (hours), m (minutes), s (seconds). Returns a new date expression.
Uses Dt.OffsetBy("7d") to add exactly 7 calendar days to each trading date, producing date_plus_7 — the first row shows 2021-01-04 → 2021-01-11, confirming the offset is calendar days, not trading days.
var dfPlus7 = dfP.WithColumns(
Col("date").Dt.OffsetBy("7d").Alias("date_plus_7")
);
dfPlus7.Select(new[] { "symbol", "date", "date_plus_7" }).Head(5)| symbol | date | date_plus_7 |
|---|---|---|
| ABI.BR | 2021-01-04 | 2021-01-11 |
| ABI.BR | 2021-01-05 | 2021-01-12 |
| ABI.BR | 2021-01-06 | 2021-01-13 |
| ABI.BR | 2021-01-07 | 2021-01-14 |
| ABI.BR | 2021-01-08 | 2021-01-15 |
Microsoft.Data.Analysis | Add days with DateTime.AddDays
Date arithmetic in MDA is direct CLR date logic written into a typed target column.
Adds seven calendar days to each date and previews the first five rows.
var plus7ColM = new MDA.PrimitiveDataFrameColumn<DateTime>("date_plus_7", dfM.Rows.Count);
for(long i = 0; i < dfM.Rows.Count; i++)
{
if(dfM.Columns["date"][i] is DateTime dt) plus7ColM[i] = dt.AddDays(7);
}
new MDA.DataFrame(dfM.Columns["symbol"], dfM.Columns["date"], plus7ColM).Head(5)| symbol | date | date_plus_7 |
|---|---|---|
| ABI.BR | 2021-01-04 00:00:00Z | 2021-01-11 00:00:00Z |
| ABI.BR | 2021-01-05 00:00:00Z | 2021-01-12 00:00:00Z |
| ABI.BR | 2021-01-06 00:00:00Z | 2021-01-13 00:00:00Z |
| ABI.BR | 2021-01-07 00:00:00Z | 2021-01-14 00:00:00Z |
| ABI.BR | 2021-01-08 00:00:00Z | 2021-01-15 00:00:00Z |
Shift and Lag
Shifting a column by N positions creates lagged (previous) or lead (future) versions of the data. This is the foundation for computing day-over-day returns, comparing consecutive values, and building time-series features. A shift of 1 gives the previous row’s value; -1 gives the next row’s value.
Polars.NET | Shift a column with Shift()
Shift(n) offsets the column values by n positions. Positive n shifts down (lag — previous values), negative shifts up (lead — future values). The resulting nulls at the edges represent the missing boundary values.
Filters to ABI.BR rows, shifts close by 1 to create prev_close, and displays the first 5 rows — the first row shows null in prev_close (no preceding value), while row 2 shows 57.21 (the row-1 close) appearing in both columns.
var abiPrices = dfP.Filter(Col("symbol") == Lit("ABI.BR"));
var abiShifted = abiPrices.WithColumns(
Col("close").Shift(1).Alias("prev_close")
);
display("ABI.BR with lagged close (first 5):");
abiShifted.Select(new[] { "date", "close", "prev_close" }).Head(5)ABI.BR with lagged close (first 5):
| date | close | prev_close |
|---|---|---|
| 2021-01-04 | 57.21 | null |
| 2021-01-05 | 57.18 | 57.21 |
| 2021-01-06 | 58.77 | 57.18 |
| 2021-01-07 | 58.4 | 58.77 |
| 2021-01-08 | 57.86 | 58.4 |
Microsoft.Data.Analysis | Build a lag column with an explicit shift loop
Lagging a column in MDA means reading the typed source and writing each previous value into a target column.
Filters to ABI.BR, shifts close by one row, and previews the first five lagged values.
// Microsoft.Data.Analysis — Lagged close values via an explicit shift loop
var abiPricesM = dfM.Filter((MDA.PrimitiveDataFrameColumn<bool>)((MDA.StringDataFrameColumn)dfM.Columns["symbol"]).ElementwiseEquals("ABI.BR")).OrderBy("date");
var prevCloseColM = new MDA.PrimitiveDataFrameColumn<decimal>("prev_close", abiPricesM.Rows.Count);
for(long i = 1; i < abiPricesM.Rows.Count; i++)
{
if (abiPricesM.Columns["close"][i - 1] != null)
prevCloseColM[i] = Convert.ToDecimal(abiPricesM.Columns["close"][i - 1]);
}
new MDA.DataFrame(abiPricesM.Columns["date"], abiPricesM.Columns["close"], prevCloseColM).Head(5)| date | close | prev_close |
|---|---|---|
| 2021-01-04 00:00:00Z | 57.21 | null |
| 2021-01-05 00:00:00Z | 57.18 | 57.21 |
| 2021-01-06 00:00:00Z | 58.77 | 57.18 |
| 2021-01-07 00:00:00Z | 58.4 | 58.77 |
| 2021-01-08 00:00:00Z | 57.86 | 58.4 |
Microsoft.Data.Analysis | Cumulative volume with a running accumulator
Running totals are explicit stateful scans in MDA.
Accumulates ABI.BR volume into cum_volume and previews the first five rows.
var abiPricesM = dfM.Filter((MDA.PrimitiveDataFrameColumn<bool>)((MDA.StringDataFrameColumn)dfM.Columns["symbol"]).ElementwiseEquals("ABI.BR")).OrderBy("date");
var abiVolM = abiPricesM.Columns["volume"];
var cumVolColM = new MDA.PrimitiveDataFrameColumn<decimal>("cum_volume", abiPricesM.Rows.Count);
decimal currentCumM = 0m;
for(long i = 0; i < abiPricesM.Rows.Count; i++)
{
if (abiVolM[i] != null)
currentCumM += Convert.ToDecimal(abiVolM[i]);
cumVolColM[i] = currentCumM;
}
new MDA.DataFrame(abiPricesM.Columns["date"], abiPricesM.Columns["volume"], cumVolColM).Head(5)| date | volume | cum_volume |
|---|---|---|
| 2021-01-04 00:00:00Z | 1513937 | 1513937 |
| 2021-01-05 00:00:00Z | 1382722 | 2896659 |
| 2021-01-06 00:00:00Z | 1370204 | 4266863 |
| 2021-01-07 00:00:00Z | 1469911 | 5736774 |
| 2021-01-08 00:00:00Z | 1428681 | 7165455 |
Filter by Date Range
Filtering rows by date range is the most common datetime operation — selecting a specific month, quarter, or year for analysis. Polars uses expression-based filtering with Dt component comparisons. Microsoft.Data.Analysis uses explicit predicates over typed DateTime values.
Polars.NET | Filter by date components
Combine Dt.Year(), Dt.Month(), and column equality expressions with & (and) to build complex date filters. Each component comparison returns a boolean expression; combine with & for intersection.
Chains three expression filters with & to select SAP.DE rows in January 2024, returning all 22 trading days in that month with close prices ranging from 137.34 to 160.76.
var jan2024 = dfP.Filter(
(Col("date").Dt.Year() == Lit(2024))
& (Col("date").Dt.Month() == Lit(1))
& (Col("symbol") == Lit("SAP.DE"))
);
display("SAP.DE in January 2024:");
jan2024.Select(new[] { "symbol", "date", "close", "volume" })SAP.DE in January 2024:
| symbol | date | close | volume |
|---|---|---|---|
| SAP.DE | 2024-01-02 | 137.34 | 1442435 |
| SAP.DE | 2024-01-03 | 137.12 | 1311703 |
| SAP.DE | 2024-01-04 | 136.44 | 1114133 |
| SAP.DE | 2024-01-05 | 137.08 | 1171604 |
| SAP.DE | 2024-01-08 | 138.78 | 992579 |
Microsoft.Data.Analysis | Filter a typed DateTime column by year and month
Date filters in MDA are CLR predicates over typed DateTime values.
Builds a mask for SAP.DE rows in January 2024 and renders the matching records.
var jan24MaskM = new MDA.PrimitiveDataFrameColumn<bool>("maskM", dfM.Rows.Count);
var dfSymbolM = (MDA.StringDataFrameColumn)dfM.Columns["symbol"];
var dfDateM = dfM.Columns["date"];
for(long i = 0; i < dfM.Rows.Count; i++)
{
if (dfSymbolM[i] == "SAP.DE" && dfDateM[i] is DateTime dt)
{
jan24MaskM[i] = (dt.Year == 2024 && dt.Month == 1);
}
}
var jan2024M = dfM.Filter(jan24MaskM);
display("SAP.DE in January 2024:");
new MDA.DataFrame(jan2024M.Columns["symbol"], jan2024M.Columns["date"], jan2024M.Columns["close"], jan2024M.Columns["volume"])SAP.DE in January 2024:| symbol | date | close | volume |
|---|---|---|---|
| SAP.DE | 2024-01-02 00:00:00Z | 137.34 | 1442435 |
| SAP.DE | 2024-01-03 00:00:00Z | 137.12 | 1311703 |
| SAP.DE | 2024-01-04 00:00:00Z | 136.44 | 1114133 |
| SAP.DE | 2024-01-05 00:00:00Z | 137.08 | 1171604 |
| SAP.DE | 2024-01-08 00:00:00Z | 138.78 | 992579 |
Date Range Generation
Generate a sequence of dates between a start and end point. Useful for building trading calendars, creating time-axis DataFrames, or filling date gaps in sparse data. Polars.NET 0.4.0 does not expose DateRange — generate dates in C# and parse to Polars Date type via Str.ToDate.
Polars.NET | Generate date range via C# Enumerable
Build date strings with Enumerable.Range and AddDays, then parse to Polars Date type.
Generates a 10-element date sequence from 2026-01-01 to 2026-01-10 using Enumerable.Range with AddDays, formats them as strings, then parses back to Polars Date type with Str.ToDate("%Y-%m-%d").
var start = new DateTime(2026, 1, 1);
var end = new DateTime(2026, 1, 10);
var dates = Enumerable.Range(0, (end - start).Days + 1)
.Select(i => start.AddDays(i).ToString("yyyy-MM-dd"))
.ToArray();
var dateRangeSeries = Polars.CSharp.Series.From("date_str", dates);
var dateRangeDf = DataFrame.FromSeries(dateRangeSeries)
.WithColumns(Col("date_str").Str.ToDate("%Y-%m-%d").Alias("date"));
display($"Date range: {dates.Length} days from {dates.First()} to {dates.Last()}");
dateRangeDf.Select("date")Date range: 10 days from 2026-01-01 to 2026-01-10
| date |
|---|
| 2026-01-01 |
| 2026-01-02 |
| 2026-01-03 |
| 2026-01-04 |
| 2026-01-05 |
Microsoft.Data.Analysis | Generate a DateTime range explicitly
Date-range generation in MDA is explicit CLR date arithmetic written into a typed date column.
Builds a 10-day inclusive DateTime range from 2026-01-01 through 2026-01-10.
var startM = new DateTime(2026, 1, 1);
var endM = new DateTime(2026, 1, 10);
var totalDaysM = (endM - startM).Days + 1;
var datesColM = new MDA.PrimitiveDataFrameColumn<DateTime>("date", totalDaysM);
for(int i = 0; i < totalDaysM; i++) datesColM[i] = startM.AddDays(i);
display($"Date range: {totalDaysM} days from {startM:yyyy-MM-dd} to {endM:yyyy-MM-dd}");
new MDA.DataFrame(datesColM)Date range: 10 days from 2026-01-01 to 2026-01-10| date |
|---|
| 2026-01-01 00:00:00Z |
| 2026-01-02 00:00:00Z |
| 2026-01-03 00:00:00Z |
| 2026-01-04 00:00:00Z |
| 2026-01-05 00:00:00Z |
Rolling Windows
Compute statistics over a sliding window of N consecutive rows — moving averages, rolling standard deviations, or rolling correlations. The window slides one row at a time, producing a smoothed series. The first N-1 rows are null since there aren’t enough preceding values to fill the window.
Rolling windows for financial analysis
Short-term moving averages (SMA-7) respond quickly to price changes; long-term averages (SMA-30) smooth out noise. Crossover of short-over-long is a classic trading signal.
Polars.NET | Rolling mean with RollingMean()
RollingMean("7") computes the 7-period simple moving average. The window size is passed as a string in Polars.NET 0.4.0. Combine with WithColumns and Alias for multiple rolling columns.
Filters to ASML.AS sorted by date, computes both a 7-period and 30-period simple moving average on close, and displays the last 10 rows — showing SMA-7 (1181) tracking below SMA-30 (1204) in the declining price period.
var asmlP = dfP.Filter(Col("symbol") == Lit("ASML.AS")).Sort("date");
var asmlRolling = asmlP.WithColumns(
Col("close").RollingMean("7").Alias("sma_7"),
Col("close").RollingMean("30").Alias("sma_30")
);
asmlRolling.Select(new[] { "date", "close", "sma_7", "sma_30" }).Tail(10)| date | close | sma_7 | sma_30 |
|---|---|---|---|
| 2026-02-27 | 1233.4 | 1251.514286 | 1201.4 |
| 2026-03-02 | 1210.4 | 1247.542857 | 1204.4 |
| 2026-03-03 | 1161.8 | 1234.142857 | 1205.126667 |
| 2026-03-04 | 1199.8 | 1227.085714 | 1206.626667 |
| 2026-03-05 | 1186 | 1216.028571 | 1206.946667 |
Microsoft.Data.Analysis | Rolling means with explicit sliding windows
Rolling windows are explicit loops over ordered rows in MDA.
Computes 7-row and 30-row moving averages for ASML.AS close prices.
var asmlPM = dfM.Filter((MDA.PrimitiveDataFrameColumn<bool>)((MDA.StringDataFrameColumn)dfM.Columns["symbol"]).ElementwiseEquals("ASML.AS")).OrderBy("date");
var sma7ColM = new MDA.PrimitiveDataFrameColumn<decimal>("sma_7", asmlPM.Rows.Count);
var sma30ColM = new MDA.PrimitiveDataFrameColumn<decimal>("sma_30", asmlPM.Rows.Count);
var rcM = asmlPM.Columns["close"];
for(long i = 0; i < asmlPM.Rows.Count; i++)
{
double sum7 = 0; int count7 = 0;
for(long j = 0; j < 7 && (i - j) >= 0; j++) {
if (rcM[i - j] != null) { sum7 += Convert.ToDouble(rcM[i - j]); count7++; }
}
if (count7 > 0) sma7ColM[i] = sum7 / count7;
double sum30 = 0; int count30 = 0;
for(long j = 0; j < 30 && (i - j) >= 0; j++) {
if (rcM[i - j] != null) { sum30 += Convert.ToDouble(rcM[i - j]); count30++; }
}
if (count30 > 0) sma30ColM[i] = sum30 / count30;
}
var asmlRollingM = asmlPM.Clone();
asmlRollingM.Columns.Add(sma7ColM);
asmlRollingM.Columns.Add(sma30ColM);
new MDA.DataFrame(asmlRollingM.Columns["date"], asmlRollingM.Columns["close"], sma7ColM, sma30ColM).Tail(10)| date | close | sma_7 | sma_30 |
|---|---|---|---|
| 2026-02-27 00:00:00Z | 1233.4 | 1251.5142822265625 | 1201.400008138021 |
| 2026-03-02 00:00:00Z | 1210.4 | 1247.5428641183037 | 1204.400008138021 |
| 2026-03-03 00:00:00Z | 1161.8 | 1234.1428745814733 | 1205.1266764322916 |
| 2026-03-04 00:00:00Z | 1199.8 | 1227.0857456752233 | 1206.6266764322916 |
| 2026-03-05 00:00:00Z | 1186 | 1216.028599330357 | 1206.9466756184895 |
Resampling
Change time frequency by downsampling daily data to monthly OHLC bars. Polars.NET 0.4.0 does not expose GroupByDynamic, so the examples use extracted year/month keys. Microsoft.Data.Analysis has no dynamic resampling API either, so monthly bars become explicit grouping logic.
Polars.NET | Monthly OHLC via GroupBy year and month
Extract year and month components with the .Dt accessor, group by both, then aggregate with First (open), Max (high), Min (low), Last (close), and Sum (volume).
Groups ASML.AS by extracted year and month components, aggregating open (first), high (max), low (min), close (last), and volume (sum) — the last 6 months show the stock’s rally from close 918 (Oct 2025) to 1190 (Mar 2026).
var asmlSorted = dfP.Filter(Col("symbol") == Lit("ASML.AS")).Sort("date");
var asmlMonthly = asmlSorted
.WithColumns(
Col("date").Dt.Year().Alias("year"),
Col("date").Dt.Month().Alias("month")
)
.GroupBy("year", "month")
.Agg(
Col("open").First().Alias("open"),
Col("high").Max().Alias("high"),
Col("low").Min().Alias("low"),
Col("close").Last().Alias("close"),
Col("volume").Sum().Alias("volume")
)
.Sort(new[] { "year", "month" });
display("ASML.AS — Monthly OHLC (last 6 months):");
asmlMonthly.Tail(6)ASML.AS — Monthly OHLC (last 6 months):
| year | month | open | high | low | close | volume |
|---|---|---|---|---|---|---|
| 2025 | 10 | 818 | 938.6 | 812.1 | 918.1 | 16383868 |
| 2025 | 11 | 917 | 930.9 | 822.2 | 903.4 | 12064891 |
| 2025 | 12 | 910 | 977.1 | 866.4 | 921.4 | 10360738 |
| 2026 | 1 | 919.4 | 1309 | 919.2 | 1215.6 | 16549130 |
| 2026 | 2 | 1178.6 | 1312.8 | 1117.6 | 1233.4 | 11528098 |
Microsoft.Data.Analysis | Monthly OHLC with explicit monthly grouping
MDA has no dynamic time-window grouping API, so monthly OHLC becomes explicit grouping state over ordered rows.
Groups ASML.AS observations by calendar month and shows the last six monthly bars.
// Microsoft.Data.Analysis — Monthly OHLC resampling (GroupBy Dynamic replacement using LINQ)
var grouped = new List<(int Year, int Month, decimal Open, decimal High, decimal Low, decimal Close, decimal Volume)>();
var currentGroup = new List<(DateTime Date, decimal Open, decimal High, decimal Low, decimal Close, decimal Volume)>();
void ProcessGroup() { if (!currentGroup.Any()) return; var ordered = currentGroup.OrderBy(x => x.Date).ToList(); grouped.Add((ordered.First().Date.Year, ordered.First().Date.Month, ordered.First().Open, ordered.Max(x => x.High), ordered.Min(x => x.Low), ordered.Last().Close, ordered.Sum(x => x.Volume))); }
for(long i = 0; i < asmlP.Rows.Count; i++)
{
if(asmlP.Columns["date"][i] is DateTime dt)
{
if (currentGroup.Any() && (currentGroup.First().Date.Year != dt.Year || currentGroup.First().Date.Month != dt.Month)) { ProcessGroup(); currentGroup.Clear(); }
currentGroup.Add((dt, Convert.ToDecimal(asmlP.Columns["open"][i] ?? 0m), Convert.ToDecimal(asmlP.Columns["high"][i] ?? 0m), Convert.ToDecimal(asmlP.Columns["low"][i] ?? 0m), Convert.ToDecimal(asmlP.Columns["close"][i] ?? 0m), Convert.ToDecimal(asmlP.Columns["volume"][i] ?? 0m)));
}
}
ProcessGroup();
var asmlMonthly = new DataFrame(new PrimitiveDataFrameColumn<int>("year", grouped.Select(g => g.Year)), new PrimitiveDataFrameColumn<int>("month", grouped.Select(g => g.Month)), new PrimitiveDataFrameColumn<decimal>("open", grouped.Select(g => g.Open)), new PrimitiveDataFrameColumn<decimal>("high", grouped.Select(g => g.High)), new PrimitiveDataFrameColumn<decimal>("low", grouped.Select(g => g.Low)), new PrimitiveDataFrameColumn<decimal>("close", grouped.Select(g => g.Close)), new PrimitiveDataFrameColumn<decimal>("volume", grouped.Select(g => g.Volume)));
display("ASML.AS — Monthly OHLC (last 6 months):");
asmlMonthly.Tail(6)ASML.AS — Monthly OHLC (last 6 months):| year | month | open | high | low | close | volume |
|---|---|---|---|---|---|---|
| 2025 | 10 | 818.0 | 938.6 | 812.1 | 918.1 | 16383868 |
| 2025 | 11 | 917.0 | 930.9 | 822.2 | 903.4 | 12064891 |
| 2025 | 12 | 910.0 | 977.1 | 866.4 | 921.4 | 10360738 |
| 2026 | 1 | 919.4 | 1309.0 | 919.2 | 1215.6 | 16549130 |
| 2026 | 2 | 1178.6 | 1312.8 | 1117.6 | 1233.4 | 11528098 |
| 2026 | 3 | 1192.8 | 1231.4 | 1060.2 | 1190.8 | 6344179 |
Microsoft.Data.Analysis | Running high, low, and cumulative volume with explicit state
Running extrema use the same explicit state pattern as cumulative totals.
Tracks cumulative volume together with the running high and running low for ASML.AS.
// Microsoft.Data.Analysis — Cumulative max, min (running high / running low)
var cumVol = new PrimitiveDataFrameColumn<decimal>("cum_volume", asmlP.Rows.Count);
var runHigh = new PrimitiveDataFrameColumn<decimal>("running_high", asmlP.Rows.Count);
var runLow = new PrimitiveDataFrameColumn<decimal>("running_low", asmlP.Rows.Count);
decimal currentV = 0m; decimal? highV = null; decimal? lowV = null;
for(long i = 0; i < asmlP.Rows.Count; i++)
{
currentV += asmlP.Columns["volume"][i] != null ? Convert.ToDecimal(asmlP.Columns["volume"][i]) : 0m; cumVol[i] = currentV;
if (asmlP.Columns["close"][i] != null) { decimal c = Convert.ToDecimal(asmlP.Columns["close"][i]); highV = highV == null ? c : Math.Max(highV.Value, c); lowV = lowV == null ? c : Math.Min(lowV.Value, c); }
if (highV.HasValue) runHigh[i] = highV.Value; if (lowV.HasValue) runLow[i] = lowV.Value;
}
var asmlCum = asmlP.Clone(); asmlCum.Columns.Add(cumVol); asmlCum.Columns.Add(runHigh); asmlCum.Columns.Add(runLow);
new DataFrame(asmlCum.Columns["date"], asmlCum.Columns["close"], asmlCum.Columns["volume"], cumVol, runHigh, runLow).Tail(10)| date | close | volume | cum_volume | running_high | running_low |
|---|---|---|---|---|---|
| 2026-02-27 00:00:00Z | 1233.4 | 1010698 | 938726541 | 1288.4 | 397.45 |
| 2026-03-02 00:00:00Z | 1210.4 | 871267 | 939597808 | 1288.4 | 397.45 |
| 2026-03-03 00:00:00Z | 1161.8 | 941945 | 940539753 | 1288.4 | 397.45 |
| 2026-03-04 00:00:00Z | 1199.8 | 714587 | 941254340 | 1288.4 | 397.45 |
| 2026-03-05 00:00:00Z | 1186.0 | 778081 | 942032421 | 1288.4 | 397.45 |
| 2026-03-06 00:00:00Z | 1147.0 | 857271 | 942889692 | 1288.4 | 397.45 |
| 2026-03-09 00:00:00Z | 1147.6 | 689086 | 943578778 | 1288.4 | 397.45 |
| 2026-03-10 00:00:00Z | 1200.0 | 800815 | 944379593 | 1288.4 | 397.45 |
| 2026-03-11 00:00:00Z | 1198.8 | 562904 | 944942497 | 1288.4 | 397.45 |
| 2026-03-12 00:00:00Z | 1190.8 | 128223 | 945070720 | 1288.4 | 397.45 |
C# Missing Data, Strings and DateTime Warnings
Polars.NET DataFrames are immutable — every operation returns a new DataFrame
Forgetting to assign the result of
WithColumns(),Filter(), orSort()silently discards the work. MDA is mutable — column assignment modifies the original.
IfElsein Polars.NET is notWhen/Then/OtherwiseThe C# API uses
Col("x").Gt(0).IfElse(trueVal, falseVal)— notWhen().Then().Otherwise(). Translating from Python literally produces compile errors.
Type mismatches between Polars.NET and MDA are common
Polars.NET uses Arrow types (Int64, Float64, Utf8). MDA uses .NET types (int, double, string). Converting between libraries requires explicit type mapping.
C# Missing Data, Strings and DateTime Recommendations
- Prefer Polars.NET expressions for analytical transforms — the optimizer can fuse and reorder operations.
- Use MDA when ML.NET integration is the goal — MDA DataFrame implements
IDataViewfor direct ML.NET handoff. - Validate output schemas after transforms — assert column names and types match expectations.
- Prefer Parquet for intermediate data — lossless type preservation between transform steps.
Troubleshooting and failure modes
df.WithColumns(...) appears to do nothing
Polars.NET returns a new DataFrame. If you do not assign the result, the transform is discarded.
Example for df.WithColumns(...) appears to do nothing.
df = df.WithColumns(Col("x").FillNull(Lit(0)));Assign the returned frame so the update persists.ComputeError on Cast
Cast failures usually mean the source column still contains values that do not fit the target type.
Example for ComputeError on Cast.
// Clean or coerce the source column before casting it.Handle missing or malformed values before the cast.MDA column type mismatch with Int32DataFrameColumn
Microsoft.Data.Analysis requires the CLR column type to match the source values exactly.
Example for MDA column type mismatch with Int32DataFrameColumn.
var counts = new MDA.Int32DataFrameColumn("count", rowCount);Use the typed column that matches the underlying CLR values.