← All guides
Guide by Arif Aslam 11 min read

CSV variance analysis: explaining a change, not just reporting it

"Revenue is down nine percent" is not analysis, it is arithmetic. The question that gets asked next, immediately and every time, is why. Variance analysis is the discipline of answering that in a way that reconciles.

It is also full of small traps that produce confident, wrong answers: percentages computed against a zero baseline, categories that appear or vanish between periods, and driver lists that do not add up to the total they claim to explain.

Step one: get both periods onto one grain

You need a baseline and a current period, aggregated to exactly the same grain, joined on the category key. That sounds obvious and it is where most of the errors enter.

If the two periods came from different extracts, check that the filters match. A baseline that quietly includes cancelled orders and a current period that excludes them will produce a driver ranking that is entirely about cancellations and says nothing about the business.

WITH base AS (
  SELECT category, SUM(revenue) AS revenue
  FROM baseline
  WHERE status <> 'cancelled'
  GROUP BY category
),
curr AS (
  SELECT category, SUM(revenue) AS revenue
  FROM current
  WHERE status <> 'cancelled'
  GROUP BY category
)
SELECT COALESCE(base.category, curr.category) AS category,
       COALESCE(base.revenue, 0)              AS baseline,
       COALESCE(curr.revenue, 0)              AS current
FROM base
FULL OUTER JOIN curr USING (category);

The FULL OUTER JOIN is the important part and it is the one people get wrong. An inner join silently drops every category that exists in only one period, which is precisely the set of categories most likely to explain the change. A left join drops the new ones. Only a full outer join keeps both sides.

Fix it: join two files on a key column, keeping unmatched rows from both →

Step two: rank by contribution, not by percentage

This is the single most common presentation error. A category that went from 4 to 8 is up 100 percent, and a category that went from 400,000 to 360,000 is down 10 percent. Sorted by percentage, the first one leads your report. It contributed 4 to a change of tens of thousands.

Rank by absolute change. Show the percentage as context, in a column, where it belongs.

SELECT category,
       baseline,
       current,
       current - baseline AS change,
       CASE WHEN baseline = 0 THEN NULL
            ELSE ROUND(100.0 * (current - baseline) / baseline, 1)
       END AS pct_change
FROM combined
ORDER BY ABS(current - baseline) DESC;

Two details. ORDER BY ABS(...) rather than by the signed change, because the biggest mover might be up or down and you want both at the top. And the CASE around the percentage, which is the subject of the next section.

Step three: handle the zero denominators honestly

A category with zero in the baseline has no percentage change. Not infinity, not 100 percent, not a large number. Undefined. Any tool that shows you a percentage there is lying, and the lie is usually a division that silently produced a very large float.

The right treatment is to split the categories into three groups and present them separately.

Group Condition What to show
Continuing Present in both periods Absolute change and percentage change
New Zero or absent in the baseline Absolute contribution only, labelled new
Disappeared Zero or absent in the current period Absolute contribution only, labelled gone

Labelling matters beyond arithmetic. A disappeared category is frequently not a business event at all: it is a spelling change, a re-categorization, or a filter that started excluding it. Every time I have seen a suspiciously large "disappeared" line, the cause was upstream renaming rather than lost revenue. Check the spelling before you write the narrative.

Fix it: list the distinct category values in both files and compare them →

Step four: make it reconcile

The test that separates a variance analysis from a list of numbers: the drivers you show, plus everything you did not show, must add up to the total change. Exactly.

SELECT SUM(current) - SUM(baseline) AS total_change,
       SUM(current - baseline)     AS sum_of_changes
FROM combined;
-- These two must be identical. If they are not, a category is
-- missing from one side of the join.

Then, in the presentation, show the top drivers and an explicit remainder line that closes the gap. A report reading "Enterprise down 41,000, EMEA down 18,000, all other categories net plus 6,000, total change minus 53,000" is complete. A report showing two drivers and a total that does not follow from them invites the only question you do not want: what else is in there?

Step five, when the headline is confusing: volume, rate and mix

Sometimes every category looks fine and the total still moved. That means the mix changed: the same customers bought a different blend of products, and the blend has different unit values.

Decompose the change into three parts. Volume, holding the price constant. Rate, holding the volume constant. And mix, the interaction between them.

SELECT category,
       (curr_units - base_units) * base_price          AS volume_effect,
       (curr_price - base_price) * base_units          AS rate_effect,
       (curr_units - base_units) * (curr_price - base_price) AS mix_effect
FROM combined;

The three effects sum to the total change for that category, which is why this decomposition is the standard one rather than a cleverer alternative. It also produces a sentence people can act on: "volumes held, average price fell four percent, and the mix shifted toward the entry tier" is a different problem from "we sold less".

Be careful with the mix term. It is a residual, and when both volume and price moved a lot it can be large enough to dominate, which usually means the categories are too coarse and the analysis should be done one level down.

Fix it: cross-tabulate category against period before decomposing →

Step six: present it as a sentence

The output of a variance analysis is not a table. It is a sentence, with a table underneath it for whoever wants to check.

Five rules for that sentence. Lead with the total change in absolute terms and then the percentage. Name at most three drivers. Say explicitly what did not change, because ruling things out is half the value. Flag any driver you suspect is a data issue rather than a business event. And state the period boundaries, because "last month" means different things to different readers.

A worked example of the shape:

Revenue for August 2026 was 1.42 million, down 53,000 or 3.6 percent against July. Two categories account for almost all of it: Enterprise fell 41,000 on four fewer renewals, and EMEA fell 18,000 with volumes flat and average price down 6 percent. Everything else was net positive by 6,000. One caveat: the category "Enterprise Plus" disappeared entirely this month, and I believe that is a renaming rather than a loss, because "Enterprise Premium" is new with a similar value. Confirming that before we treat the Enterprise number as real.

That last sentence is the one that builds trust. Naming your uncertainty before somebody finds it costs you nothing and buys you the benefit of the doubt on everything else in the report.

Fix it: diff the two periods by key and see exactly which rows changed →

Questions people actually ask

Why rank by absolute change rather than percentage?

Because a category that went from 4 to 8 is up 100 percent and moved the total by 4. Percentages flatter small numbers. Rank by the absolute contribution to the change, then show the percentage as context.

What do I do about a category with zero in the baseline?

Do not compute a percentage; the denominator is zero and the answer is not infinity, it is undefined. Report it as new, with its absolute contribution. The same applies in reverse to a category that disappeared.

How many drivers should a variance report show?

Enough to cover most of the change, usually five to eight, plus an explicit remainder line. A list of forty categories is not an explanation, it is the data again.

Does the sum of the drivers equal the total change?

It should, and checking that it does is the single best test of a variance report. If the drivers plus the remainder do not reconcile to the total, something has been double counted or dropped.

What about mix versus rate?

A total can move because volumes changed, because prices changed, or because the mix between high and low value categories shifted. Decomposing those three is the next level of the analysis and it is worth doing when the headline number is confusing.

AA

Arif Aslam

Staff engineer in Bangalore. By day at Mammoth Analytics; building ExploreMyData on the side. More on my author page or LinkedIn.

Compare two periods

Load both files, join them on the category key, and rank the drivers in one query.

Open the workbench