C# DataFrame
last modified October 18, 2026
In this article we work with a DataFrame in C#. The
DataFrame class is located in the Microsoft.Data.Analysis
namespace.
A DataFrame is a data structure that organizes data into a 2-dimensional table of rows and columns, much like a spreadsheet or a database table. Each column has a name and a type, and all values in a column are of that type. DataFrames are popular in data analysis, and the idea comes from R and Python's pandas.
$ dotnet add package Microsoft.Data.Analysis
We add the library to the project. The examples use version 0.23.
id,product_name,category,unit_price,units_in_stock 1,Chai,Beverages,18.0,39 2,Chang,Beverages,19.0,17 3,Aniseed Syrup,Condiments,10.0,13 4,Chef Anton's Cajun Seasoning,Condiments,22.0,53 5,Chef Anton's Gumbo Mix,Condiments,21.35,0 6,Grandma's Boysenberry Spread,Condiments,25.0,120 7,Uncle Bob's Organic Dried Pears,Produce,30.0,15 8,Northwoods Cranberry Sauce,Condiments,40.0,6 9,Mishi Kobe Niku,Meat/Poultry,97.0,29 10,Ikura,Seafood,31.0,31 11,Queso Cabrales,Dairy Products,21.0,22 12,Queso Manchego La Pastora,Dairy Products,38.0,86 13,Konbu,Seafood,6.0,24 14,Tofu,Produce,23.25,35 15,Genen Shouyu,Condiments,15.5,39 16,Pavlova,Confections,17.45,29 17,Alice Mutton,Meat/Poultry,39.0,0 18,Carnarvon Tigers,Seafood,62.5,42 19,Teatime Chocolate Biscuits,Confections,9.2,25 20,Sir Rodney's Marmalade,Confections,81.0,40 21,Sir Rodney's Scones,Confections,10.0,3 22,Gustaf's Knäckebröd,Grains/Cereals,21.0,104 23,Tunnbröd,Grains/Cereals,9.0,61 24,Guaraná Fantástica,Beverages,4.5,20 25,NuNuCa Nuß-Nougat-Creme,Confections,14.0,76 26,Gumbär Gummibärchen,Confections,31.23,15 27,Schoggi Schokolade,Confections,43.9,49 28,Rössle Sauerkraut,Produce,45.6,26 29,Thüringer Rostbratwurst,Meat/Poultry,123.79,0 30,Nord-Ost Matjeshering,Seafood,25.89,10 31,Gorgonzola Telino,Dairy Products,12.5,0 32,Mascarpone Fabioli,Dairy Products,32.0,9 33,Geitost,Dairy Products,2.5,112 34,Sasquatch Ale,Beverages,14.0,111 35,Steeleye Stout,Beverages,18.0,20 36,Inlagd Sill,Seafood,19.0,112 37,Gravad lax,Seafood,26.0,11 38,Côte de Blaye,Beverages,263.5,17 39,Chartreuse verte,Beverages,18.0,69 40,Boston Crab Meat,Seafood,18.4,123 41,Jack'England Clam Chowder,Seafood,9.65,85 42,Singaporean Hokkien Fried Mee,Grains/Cereals,14.0,26 43,Ipoh Coffee,Beverages,46.0,17 44,Gula Malacca,Condiments,19.45,27 45,Rogede sild,Seafood,9.5,5 46,Spegesild,Seafood,12.0,95 47,Zaanse koeken,Confections,9.5,36 48,Chocolade,Confections,12.75,15 49,Maxilaku,Confections,20.0,10 50,Valkoinen suklaa,Confections,16.25,65 51,Manjimup Dried Apples,Produce,53.0,20 52,Filo Mix,Grains/Cereals,7.0,38 53,Perth Pasties,Meat/Poultry,32.8,0 54,Tourtière,Meat/Poultry,7.45,21 55,Pâté chinois,Meat/Poultry,24.0,115 56,Gnocchi di nonna Alice,Grains/Cereals,38.0,21 57,Ravioli Angelo,Grains/Cereals,19.5,36 58,Escargots de Bourgogne,Seafood,13.25,62 59,Raclette Courdavault,Dairy Products,55.0,79 60,Camembert Pierrot,Dairy Products,34.0,19 61,Sirop d'érable,Condiments,28.5,113 62,Tarte au sucre,Confections,49.3,17 63,Vegie-spread,Condiments,43.9,24 64,Wimmers gute Semmelknödel,Grains/Cereals,33.25,22 65,Louisiana Fiery Hot Pepper Sauce,Condiments,21.05,76 66,Louisiana Hot Spiced Okra,Condiments,17.0,4 67,Laughing Lumberjack Lager,Beverages,14.0,52 68,Scottish Longbreads,Confections,12.5,6 69,Gudbrandsdalsost,Dairy Products,36.0,26 70,Outback Lager,Beverages,15.0,15 71,Flotemysost,Dairy Products,21.5,26 72,Mozzarella di Giovanni,Dairy Products,34.8,14 73,Röd Kaviar,Seafood,15.0,101 74,Longlife Tofu,Produce,10.0,4 75,Rhönbräu Klosterbier,Beverages,7.75,125 76,Lakkalikööri,Beverages,18.0,57 77,Original Frankfurter grüne Soße,Condiments,13.0,32
In the examples, we use this CSV file. It must be copied to the output directory of
the project, which is done with an item in the project file:
<None Update="products.csv" CopyToOutputDirectory="PreserveNewest" />;
with dotnet run, the working directory is the project directory, so the
relative path works as well.
C# DataFrame simple example
The following is a simple DataFrame example.
using Microsoft.Data.Analysis;
var df = DataFrame.LoadCsv("products.csv");
Console.WriteLine($"{df.Rows.Count} rows, {df.Columns.Count} columns");
Console.WriteLine(df.Info());
Console.WriteLine(df.Description());
We load data from the products.csv file and call the Info
and Description methods on the dataframe.
var df = DataFrame.LoadCsv("products.csv");
The data is loaded from the CSV file with the DataFrame.LoadCsv method.
The first line is taken as the header with the column names. The
Rows.Count and Columns.Count properties give the size of
the table.
Console.WriteLine(df.Info()); Console.WriteLine(df.Description());
The Info method shows the column types and the number of values.
Description gives the basic statistics of the numeric columns: the
maximum, the minimum, and the mean.
$ dotnet run 77 rows, 5 columns Info id product_name category unit_price units_in_stock DataType System.Single System.String System.String System.Single System.Single Length (excluding null values)77 77 77 77 77 Description id unit_price units_in_stock Length (excluding null values)77 77 77 Max 77 263.5 125 Min 1 2.5 0 Mean 39 28.866365 40.506493
Note that all the numbers were inferred as Single (float),
even the ids and the stock counts.
The DataFrame.LoadCsv method infers the column types automatically, but
the guess is not always what we need. We can specify the column data types with the
dataTypes option, and the column names with columnNames.
using Microsoft.Data.Analysis;
var df = DataFrame.LoadCsv("products.csv",
dataTypes: [typeof(int), typeof(string), typeof(string), typeof(decimal), typeof(int)]);
Console.WriteLine(df.Info());
// the same with explicit column names
var df2 = DataFrame.LoadCsv("products.csv", header: true, columnNames: ["Id", "Name", "Category", "Price", "Stock"],
dataTypes: [typeof(int), typeof(string), typeof(string), typeof(float), typeof(int)]);
Console.WriteLine(string.Join(", ", df2.Columns.Select(c => $"{c.Name}:{c.DataType.Name}")));
In the example, we specify the data types for the five columns. The
unit_price becomes decimal, which is the better type for
money, and the ids and counts become integers. The second call also renames the
columns.
$ dotnet run Info id product_name category unit_price units_in_stock DataType System.Int32 System.String System.String System.Decimal System.Int32 Length (excluding null values)77 77 77 77 77 Id:Int32, Name:String, Category:String, Price:Single, Stock:Int32
C# DataFrame Head/Tail
The Head method returns the first n rows, while the Tail
method returns the last n rows.
using Microsoft.Data.Analysis;
var df = DataFrame.LoadCsv("products.csv");
Console.WriteLine(df.Head(4));
Console.WriteLine(df.Tail(4));
The example shows the first four and the last four rows of the dataframe.
$ dotnet run id product_name category unit_price units_in_stock 1 Chai Beverages 18 39 2 Chang Beverages 19 17 3 Aniseed Syrup Condiments 10 13 4 Chef Anton's Cajun SeasoningCondiments 22 53 id product_name category unit_price units_in_stock 74 Longlife Tofu Produce 10 4 75 Rhönbräu KlosterbierBeverages 7.75 125 76 Lakkalikööri Beverages 18 57 77 Original Frankfurter grüne SoßeCondiments 13 32
The printed table uses fixed-width columns, so long values run into the next column. For nicer output we format the rows ourselves, as shown in the next sections, or use a library such as Spectre.Console (see the Spectre.Console tutorial).
C# DataFrame access to data
The cells are available through indexers: df[row, column], or a column
by name or position followed by the row. A row is a DataFrameRow, which
we can enumerate; the values are object instances, so they must be cast
or formatted.
using Microsoft.Data.Analysis;
var df = DataFrame.LoadCsv("products.csv");
// access to the cells
Console.WriteLine(df[0, 1]); // row 0, column 1
Console.WriteLine(df["product_name"][2]); // column by name, row 2
DataFrameRow row = df.Rows[8];
Console.WriteLine(string.Join(" | ", row));
// a typed column gives typed access
var prices = (SingleDataFrameColumn)df["unit_price"];
Console.WriteLine($"{prices.Length} prices, first: {prices[0]}");
foreach (var r in df.Rows.Take(3))
{
Console.WriteLine($"{r[0],3} {r[1],-15} {r[3]:0.00}");
}
The program reads individual cells, a row, and a typed column.
var prices = (SingleDataFrameColumn)df["unit_price"];
Columns are objects of the typed classes, such as Int32DataFrameColumn,
SingleDataFrameColumn, DecimalDataFrameColumn, and
StringDataFrameColumn. After a cast, we get typed values and the
operations of that type. A cell can be null, so the values are nullable.
$ dotnet run Chai Aniseed Syrup 9 | Mishi Kobe Niku | Meat/Poultry | 97 | 29 77 prices, first: 18 1 Chai 18.00 2 Chang 19.00 3 Aniseed Syrup 10.00
C# DataFrame Filter
We use the Filter method to filter data. It takes a boolean column,
which we create by comparing a column with a value using the
Elementwise... methods.
using Microsoft.Data.Analysis;
var df = DataFrame.LoadCsv("products.csv");
PrimitiveDataFrameColumn<bool> expensive = df["unit_price"].ElementwiseGreaterThan(100);
Console.WriteLine(df.Filter(expensive));
Console.WriteLine(df.Filter((PrimitiveDataFrameColumn<bool>)df["units_in_stock"].ElementwiseLessThan(5)).Rows.Count);
// several conditions are combined with And / Or
PrimitiveDataFrameColumn<bool> isBeverage = df["category"].ElementwiseEquals("Beverages");
PrimitiveDataFrameColumn<bool> isCheap = df["unit_price"].ElementwiseLessThan(15);
Console.WriteLine(df.Filter((PrimitiveDataFrameColumn<bool>)isBeverage.And(isCheap)));
In the program, we apply three filters.
PrimitiveDataFrameColumn<bool> expensive = df["unit_price"].ElementwiseGreaterThan(100); Console.WriteLine(df.Filter(expensive));
In the first filter, we show all products whose price is greater than 100. The
comparison returns a column of booleans, one per row, and Filter keeps
the rows where the value is true.
Console.WriteLine(df.Filter((PrimitiveDataFrameColumn<bool>)df["units_in_stock"].ElementwiseLessThan(5)).Rows.Count);
The second filter counts the products with a stock of less than five items. The
result of the generic ElementwiseLessThan must be cast to the boolean
column type.
Console.WriteLine(df.Filter((PrimitiveDataFrameColumn<bool>)isBeverage.And(isCheap)));
Boolean columns are combined with And, Or, and
Xor. Here we select the cheap beverages. The C# operators
&& and || do not work on columns.
$ dotnet run id product_name category unit_price units_in_stock 29 Thüringer RostbratwurstMeat/Poultry 123.79 0 38 Côte de Blaye Beverages 263.5 17 8 id product_name category unit_price units_in_stock 24 Guaraná FantásticaBeverages 4.5 20 34 Sasquatch Ale Beverages 14 111 67 Laughing Lumberjack LagerBeverages 14 52 75 Rhönbräu KlosterbierBeverages 7.75 125
C# DataFrame sort
We sort data with OrderBy and OrderByDescending. They
return a new dataframe and leave the original unchanged.
using Microsoft.Data.Analysis;
var df = DataFrame.LoadCsv("products.csv");
Console.WriteLine(df.OrderBy("unit_price").Head(5));
Console.WriteLine(df.OrderByDescending("unit_price").Head(3));
// sort by several columns by sorting twice, starting with the less important column
var sorted = df.OrderBy("unit_price").OrderBy("category");
Console.WriteLine(sorted.Head(5));
In the program, we sort the data by unit price, from the cheapest and from the most expensive, and then by category and price.
var sorted = df.OrderBy("unit_price").OrderBy("category");
There is no overload that takes several keys. The idea is to sort first by the less important key and then by the more important one, which relies on the second sort keeping the order of equal rows.
$ dotnet run id product_name category unit_price units_in_stock 33 Geitost Dairy Products 2.5 112 24 Guaraná FantásticaBeverages 4.5 20 13 Konbu Seafood 6 24 52 Filo Mix Grains/Cereals 7 38 54 Tourtière Meat/Poultry 7.45 21 id product_name category unit_price units_in_stock 38 Côte de Blaye Beverages 263.5 17 29 Thüringer RostbratwurstMeat/Poultry 123.79 0 9 Mishi Kobe NikuMeat/Poultry 97 29 id product_name category unit_price units_in_stock 38 Côte de Blaye Beverages 263.5 17 34 Sasquatch Ale Beverages 14 111 70 Outback Lager Beverages 15 15 76 Lakkalikööri Beverages 18 57 35 Steeleye Stout Beverages 18 20
The last block shows that this does not work here. Within the beverages, the
Côte de Blaye at 263.5 comes before the Sasquatch Ale at 14, so the earlier
order by price was not preserved: the sort does not guarantee to keep the order of
equal rows. When the combined ordering matters, sort the rows with LINQ
(OrderBy(...).ThenBy(...)) over a list of objects, or sort by a computed
single key column.
C# DataFrame GroupBy
The GroupBy method groups the rows of the DataFrame by the
unique values in a column. It returns a GroupBy object, on which we
call the aggregate operations Count, Max,
Min, Mean, Sum, and Product.
using Microsoft.Data.Analysis;
var df = DataFrame.LoadCsv("products.csv");
GroupBy g = df.GroupBy("category");
Console.WriteLine(g.Count());
Console.WriteLine(g.Max("unit_price"));
Console.WriteLine(g.Mean("unit_price"));
Console.WriteLine(g.Sum("units_in_stock"));
In the example, we group the products by category and compute the number of products, the maximum and the average price, and the total stock for each category.
GroupBy g = df.GroupBy("category");
We group the data by category. The grouping column becomes the first column of every result.
Console.WriteLine(g.Mean("unit_price"));
The aggregate methods take the name of the column to compute; Count
counts the rows of each group in all columns.
$ dotnet run category id product_name unit_price units_in_stock Beverages 12 12 12 12 Condiments 12 12 12 12 Produce 5 5 5 5 Meat/Poultry 6 6 6 6 Seafood 12 12 12 12 Dairy Products 10 10 10 10 Confections 13 13 13 13 Grains/Cereals 7 7 7 7 category unit_price Beverages 263.5 Condiments 43.9 Produce 53 Meat/Poultry123.79 Seafood 62.5 Dairy Products55 Confections81 Grains/Cereals38 category unit_price Beverages 37.979166666666664 Condiments 23.0625 Produce 32.370001220703124 Meat/Poultry54.00666809082031 Seafood 20.682498931884766 Dairy Products28.729998779296874 Confections25.159998967097355 Grains/Cereals20.25 category units_in_stock Beverages 559 Condiments 507 Produce 100 Meat/Poultry 165 Seafood 701 Dairy Products 393 Confections 386 Grains/Cereals 308
The counts show that the largest category is Confections with 13 products. The means of the float prices show the floating-point
noise (32.370001220703124); format them when presenting, or use the
decimal type for the column.
C# DataFrame computed columns
Columns support arithmetic with other columns and with values. The result is a new column, which we can add to the dataframe. We can also compute values in a loop and fill a new column, rename and remove columns, and use the statistics methods.
using Microsoft.Data.Analysis;
var df = DataFrame.LoadCsv("products.csv");
// a computed column: the value of the stock of each product
DataFrameColumn stockValue = df["unit_price"] * df["units_in_stock"];
stockValue.SetName("stock_value");
df.Columns.Add(stockValue);
foreach (var r in df.Rows.Take(3))
{
Console.WriteLine($"{r["product_name"],-15} {r["stock_value"]}");
}
// column statistics
var price = df["unit_price"];
Console.WriteLine($"max {price.Max()}, min {price.Min()}, sum {price.Sum():F2}");
// a string column from a computation
var label = new StringDataFrameColumn("label", df.Rows.Count);
for (long i = 0; i < df.Rows.Count; i++)
{
label[i] = (float)df["unit_price"][i] > 50 ? "premium" : "regular";
}
df.Columns.Add(label);
PrimitiveDataFrameColumn<bool> premium = df["label"].ElementwiseEquals("premium");
Console.WriteLine(df.Filter(premium).Rows.Count);
// removing and renaming
df.Columns.Remove("label");
df.Columns["unit_price"].SetName("price");
Console.WriteLine(string.Join(", ", df.Columns.Select(c => c.Name)));
The program computes the value of the stock of each product, labels expensive products, and renames columns.
DataFrameColumn stockValue = df["unit_price"] * df["units_in_stock"];
stockValue.SetName("stock_value");
df.Columns.Add(stockValue);
The multiplication works elementwise on two columns of the same length. The new column has an automatic name, so we set it before adding it.
var price = df["unit_price"];
Console.WriteLine($"max {price.Max()}, min {price.Min()}, sum {price.Sum():F2}");
Numeric columns have the Max, Min, and Sum
methods, among others; the results are object values.
var label = new StringDataFrameColumn("label", df.Rows.Count);
We create a string column of the right length and fill it row by row. The 7 products with a price above 50 are the premium ones.
$ dotnet run Chai 702 Chang 323 Aniseed Syrup 130 max 263.5, min 2.5, sum 2222.71 7 id, product_name, category, price, units_in_stock, stock_value
C# DataFrame create and save
A dataframe can be built in code from typed columns, extended with rows, and saved to a CSV file.
using Microsoft.Data.Analysis;
// a dataframe built in code
var ids = new Int32DataFrameColumn("Id", [1, 2, 3, 4]);
var names = new StringDataFrameColumn("Name", ["Audi", "Skoda", "Volvo", "Bentley"]);
var prices = new DoubleDataFrameColumn("Price", [52642, 9000, 29000, 350000]);
var df = new DataFrame(ids, names, prices);
Console.WriteLine(df);
// append rows
df.Append(new List<KeyValuePair<string, object>>
{
new("Id", 5), new("Name", "Citroen"), new("Price", 21000.0)
}, inPlace: true);
Console.WriteLine(df.Rows.Count);
// save to CSV
DataFrame.SaveCsv(df, "cars.csv");
Console.WriteLine(File.ReadAllText("cars.csv"));
The program creates a dataframe of cars, appends a row, and writes it to
cars.csv.
var ids = new Int32DataFrameColumn("Id", [1, 2, 3, 4]);
var names = new StringDataFrameColumn("Name", ["Audi", "Skoda", "Volvo", "Bentley"]);
Each column class takes the name and the values. All columns of a dataframe must have the same length.
df.Append(new List<KeyValuePair<string, object>> { ... }, inPlace: true);
The Append method adds a row, given as a list of column name and value
pairs. With inPlace: true the dataframe itself is changed; otherwise a
new one is returned. Appending rows one at a time is slow for a big dataframe.
$ dotnet run Id Name Price 1 Audi 52642 2 Skoda 9000 3 Volvo 29000 4 Bentley 350000 5 Id,Name,Price 1,Audi,52642 2,Skoda,9000 3,Volvo,29000 4,Bentley,350000 5,Citroen,21000
C# DataFrame missing values
Real data has gaps. Every column can hold null, and the library has
methods to count, drop, and fill them.
using Microsoft.Data.Analysis;
var id = new Int32DataFrameColumn("Id", [1, 2, 3]);
var name = new StringDataFrameColumn("Name", ["Audi", null, "Volvo"]);
var price = new DoubleDataFrameColumn("Price", [52642, 9000, 29000]);
price[1] = null;
var df = new DataFrame(id, name, price);
Console.WriteLine(df);
Console.WriteLine(df["Price"].NullCount);
Console.WriteLine(df.DropNulls().Rows.Count);
df["Price"].FillNulls(0.0, inPlace: true);
df["Name"].FillNulls("unknown", inPlace: true);
Console.WriteLine(df);
The program creates a dataframe with two missing values and handles them.
Console.WriteLine(df["Price"].NullCount); Console.WriteLine(df.DropNulls().Rows.Count);
NullCount tells how many values are missing in a column; the price has
one. DropNulls returns a dataframe without the rows that contain any
null. The second row lacks both the name and the price, so it is
removed and two rows remain.
df["Price"].FillNulls(0.0, inPlace: true);
FillNulls replaces the missing values with a given value, here in place.
$ dotnet run Id Name Price 1 Audi 52642 2 null null 3 Volvo 29000 1 2 Id Name Price 1 Audi 52642 2 unknown 0 3 Volvo 29000
C# DataFrame merge
Two dataframes can be combined on key columns, like a SQL join. The
Merge method supports the inner, left, right and full outer joins.
using Microsoft.Data.Analysis;
var cars = new DataFrame(
new Int32DataFrameColumn("Id", [1, 2, 3]),
new StringDataFrameColumn("Name", ["Audi", "Skoda", "Volvo"]));
var orders = new DataFrame(
new Int32DataFrameColumn("CarId", [1, 1, 3, 4]),
new StringDataFrameColumn("Customer", ["John", "Jane", "Roger", "Lucia"]));
// inner join on the key columns
var joined = cars.Merge<int>(orders, "Id", "CarId", joinAlgorithm: JoinAlgorithm.Inner);
Console.WriteLine(joined);
var left = cars.Merge<int>(orders, "Id", "CarId", joinAlgorithm: JoinAlgorithm.Left);
Console.WriteLine(left.Rows.Count);
The program joins a table of cars with a table of orders.
var joined = cars.Merge<int>(orders, "Id", "CarId", joinAlgorithm: JoinAlgorithm.Inner);
The type argument is the type of the key columns; the next two arguments are the key column names in the left and in the right dataframe. The inner join keeps only the cars that have orders, and the Audi appears twice because it has two orders. The Skoda has no orders and the order of Lucia has no car, so both are missing.
$ dotnet run Id Name CarId Customer 1 Audi 1 John 1 Audi 1 Jane 3 Volvo 3 Roger 4
The left join keeps all four rows: the three matches and the Skoda without an order, with null values for the order columns.
Source
Getting started with DataFrames
In this article we have worked with the Microsoft.Data.Analysis DataFrame in C#.
Author
List all C# tutorials.