CSV to Pivot

Drop a CSV, pick a field for the rows and one for the columns, and get a cross-tab with margins you can actually quote. Every total is recomputed from the source records rather than added up from the cells beside it, which is the difference between a correct average and a plausible one. Export it as CSV, XLSX or Markdown.

Nothing loaded yet? Try with sample sales data · 180 orders, pivoted the moment they land.

Want the same cross-tab as a picture? The heatmap page shades it instead.

The totals row is where pivots go wrong

Here is the whole argument for this page in one sentence: a total is a statement about records, not a statement about the cells printed above it. Almost every pivot implementation I have opened treats it as the second thing, because the second thing is fast and, for a sum, indistinguishable from the first. Sum four cells or sum the underlying records and you land on the same number. So the shortcut passes its only obvious test and then ships.

Switch the aggregate to Average and the shortcut breaks quietly. Suppose a region sold through three channels: 90 orders averaging $120, 3 orders averaging $4,000, and 7 orders averaging $310. The true average across that region's 100 records is roughly $350. Average the three cell averages instead and you get about $1,477, a number four times larger that belongs to no order, no channel and no region. Nothing on screen flags it. The row still looks like a row, the total still looks like a total, and somebody quotes $1,477 in a meeting.

Distinct counts fail in the opposite direction and just as silently. If a customer bought through both Direct and Retail, they are correctly counted once in each of those two cells. Add the cells and that customer becomes two customers. Do it across four channels and a base of 400 real customers reports as 700. Sums inflate distinct counts; averages of averages distort in whichever direction the record counts are lopsided; medians of medians are not even approximately meaningful, since the median of a set is not recoverable from the medians of its parts under any weighting at all.

So this pivot does the slower thing. Every margin cell gets its own pass over the source table. A row total collects every record whose row field matches that label and aggregates them from scratch. A column total does the same down its column. The grand total aggregates the whole file. The average of a row is therefore the average of that row's records, and a distinct count of a row is the count of values distinct across that row's records rather than the sum of its cells. On a sum you will never notice the difference. On everything else, the difference is the reason the table is worth trusting.

Seven controls

  • Rows. The field running down the left. There is no cap here, because a long table scrolls fine.
  • Columns. The field running across the top, with (none) as an option. Leave it at (none) for a one-way summary, which is often all you need and is far easier to read.
  • Values. The column being aggregated, with Count rows as its empty state. Set it to a numeric column to get a measure; leave it alone to count records.
  • Aggregate. Sum, Average, Count rows, Count distinct, Minimum, Maximum, Median. Minimum and Maximum are the underrated pair here: a min of zero in a duration column is usually a data quality finding rather than a fast delivery.
  • Totals. On or off. On adds a Total column at the right of each row and a Total row along the bottom, both recomputed as described above. Off gives a bare rectangle, which is what you want when the pivot is feeding another tool.
  • Shade. On or off. Tints each cell's background by value so the table can be scanned like a heatmap while keeping its numbers legible. Off for grayscale printing.
  • The column guard. Not a control, but it acts like one. A column field with more than 60 distinct values is refused, with a message naming the field and stating exactly how many distinct values it found, and suggesting you use it as the row field instead.

Both axes are sorted with a natural-order comparison, so item 2 lands before item 10 rather than after it, which plain alphabetical sorting gets wrong every time.

Worked example: revenue by region and channel

Seven columns, 180 orders spanning 2024. The opening rows:

date,region,product,channel,units,unit_price,revenue
2024-01-02,West,Doohickey,Direct,58,211.64,12275.12
2024-01-04,West,Sprocket,Partner,13,40.73,529.49
2024-01-06,North,Sprocket,Retail,31,147.97,4587.07
2024-01-08,East,Gizmo,Direct,7,172.02,1204.15

Set Rows to region, Columns to channel, Values to revenue, leave Aggregate on Sum and Totals on. The panel shows a table titled revenue by region and channel with a kicker reading PIVOT and a caption reading Sum of revenue · 4 rows x 3 columns · totals recomputed from source rows. The header row reads region, then Direct, Partner, Retail in natural order, then Total. Four body rows follow, sorted the same way, and a Total row closes the table. Every figure is formatted as money, because revenue was profiled as money rather than as a plain number.

Now the interesting move. Switch Aggregate to Average. Every cell becomes the mean order value for that region and channel, and every total becomes the mean order value across that region's own orders. Take a pen to it: add the three cells in the West row, divide by three, and compare against the Total cell at the end of that row. They will not match, and the Total is the one that is right, because the three channels do not carry equal numbers of orders. That mismatch is not a rendering artifact, it is the arithmetic that most pivot tables get wrong without saying anything.

Switch Values to product and Aggregate to Count distinct and the same shape now counts how many distinct products each region and channel pairing sold. Add the West row's cells across and you will usually get a larger number than the West Total, because a product sold through two channels is one distinct product for the region and two cells' worth of counting. Again, the smaller recomputed figure is the true one.

Two notes may appear under the table depending on the file. If any row has an empty region, those records group under (blank) and a note says so rather than letting them vanish out of the grand total. If any revenue cell holds something unreadable, a note reports how many values could not be read as a number and were left out; they are excluded from the aggregation and never counted as zero, which would silently pull every average down.

Four exports

  • Download .csv. The plain rectangle, header row first, totals row last when Totals is on. This is the format to hand to another script, load into a database, or diff against last month's version of the same summary.
  • Download .xlsx. A real workbook rather than a CSV wearing an extension. It opens in Excel, Numbers or Google Sheets with the header and the totals row already in place and no import dialog in the way. Useful precisely because the numbers arrive already correct: pasting a CSV into a spreadsheet and rebuilding the margins there reintroduces the bug this page exists to avoid.
  • Download .md. A pipe table. Drop it in a pull request description, a wiki page, a README or a ticket and it renders as a table everywhere Markdown is understood.
  • Copy markdown. The same pipe table straight onto the clipboard, so a summary can go into a chat message without a file ever landing on your disk.

All four are generated in the tab from the numbers already on screen, so the exported file and the table you are reading cannot disagree. If what you actually want is a shaded picture of the same cross-tab, the heatmap page takes the same fields and paints them instead, with a sequential or diverging ramp.

What this page will not do

The honest edges, so you meet them here rather than halfway through a report:

  • One field per axis. No nested row groups and no stacked column headers. If you need region inside country inside continent, build the composite key first in the app.
  • One measure at a time. A single Values field with a single Aggregate. Sum of revenue and average of units side by side means two passes and two exports.
  • A column cap of 60. A column field with more than 60 distinct values is refused, with the count stated. Use it as the row field instead.
  • No calculated fields. There is no formula box, so margin percent or revenue per unit has to exist as a column in the file before it arrives here.
  • No percent-of-total mode. Cells hold the aggregate, never a share of the row or column. Divide in the export.
  • No filtering and no drill-down. Every row in the file is included, and clicking a cell does not open the records behind it. Both live in the app.
  • A header row is required. Without one, the first data line becomes the field names and both dropdowns will offer nonsense that looks plausible.

Comma, semicolon, tab and pipe delimited files are all read without configuration, so a block of cells copied out of a spreadsheet works in the paste box.

Frequently Asked Questions

How are the totals calculated?

From the source rows, every time. Each total gets its own pass over the original records: a row total collects every record whose row field matches that label and aggregates them fresh, a column total does the same down the column, and the grand total aggregates everything. No total is ever produced by combining the cells next to it. For a sum the two approaches happen to agree, which is why the bug hides so well. For an average, a median or a distinct count they do not agree, and the recomputed answer is the correct one.

Why is the average of a row not the average of its cells?

Because averaging four averages weights each cell equally regardless of how many records sit behind it. A region with 90 orders in one channel and 3 in another has a true average dominated by the 90, but the average of the two cell averages treats them as equal partners and lands somewhere no order actually is. Recomputing over the row's own 93 records gives the number you would get if you filtered the source file to that region and averaged the column, which is what anyone reading a total believes they are looking at.

Why is a distinct count total not the sum of the cells?

Because distinct counts do not add up. If a customer bought through both Direct and Retail, that customer is counted once in each cell, and adding the cells counts them twice. The row total is computed by collecting the distinct values across every record in that row, so the same customer contributes once no matter how many cells they appear in. The consequence is that a distinct-count total is usually smaller than the sum of the cells beside it, and that is not an error, it is the point.

Which aggregates can I use?

Sum, Average, Count rows, Count distinct, Minimum, Maximum and Median, from the Aggregate dropdown. Count rows ignores the value column entirely and counts records, which is the reading you want for a file with no measures in it. Median is worth reaching for whenever a handful of very large records is dragging an average away from anything a typical record looks like, and it is a good demonstration of the totals point: the median of a row is the median of that row's records, which is not derivable from the medians of its cells at all.

Why does it refuse my column field?

Because the field has more than 60 distinct values, which would be more than 60 columns. The message names the column, says how many distinct values it found, and suggests either picking a column with at most 60 distinct values or leaving Columns empty and using that field as the row field instead. Rows have no such cap, since a long table scrolls perfectly well while a table 400 columns wide is unusable in every direction.

What happens to blank values and unreadable numbers?

Rows with nothing in the row field are grouped under the label (blank) and a note under the table says so, because dropping them silently changes the grand total without telling anyone. Values that cannot be read as a number are excluded from the aggregation and counted in their own note, which reports how many values in that column could not be read and were left out. They are never treated as zero. Currency symbols, thousands separators, trailing percent signs and accounting parentheses are all read as numbers, so a cell holding $1,234.50 counts and a cell holding N/A does not.

What can I export the pivot as?

Four buttons: Download .csv, Download .xlsx, Download .md and Copy markdown. The CSV is the plain rectangle for another tool to read. The XLSX is a real workbook with the header row and the totals row in place, so it opens in Excel or Sheets without an import step. The Markdown is a pipe table for a pull request, a wiki page or a ticket, and Copy markdown puts the same text on the clipboard so it never touches your filesystem.

What does the Shade control do?

It tints each cell's background by its value, so the table reads like a heatmap while staying a table with legible numbers in it. Turn it off when the pivot is going into a document that will be printed in grayscale, or when the shading competes with a color meaning you are already using elsewhere on the page. Turn it on when you are scanning for the outlier rather than reading specific figures. For a full picture-first grid with a diverging option, use the heatmap page instead.

Does my file get uploaded?

No. The CSV is parsed in your browser tab, the grouping and every totals pass run in that same tab, and the exports are generated locally too. There is no upload endpoint behind this page and nothing carrying your rows to a server. Close the tab and the data is gone, and once the page has loaded it keeps working with the network disconnected.

Pivot your file

Two fields, one measure, margins you can quote without checking them first. No sign-up and no upload.

Back to the pivot builder