Average of ratios against ratio of sums
Simulate a modelNeeds Python with NumPy
Problem
A ratio is not additive: you cannot get a year's margin rate by averaging daily margin rates. The average of ratios gives every day the same weight, so a small day counts as much as a big one; the ratio of sums (total margin over total revenue) weights each day by its revenue, which is what the business means by "our margin". The two agree only if the ratio does not vary with the size of the day. Averaging averages at another level (monthly averages, then their average) gives yet another number.
Take any table of daily revenue and margin from your own data (or generate 365 random rows with revenues between 100 and 10,000). Compute AVG(margin/revenue) and SUM(margin)/SUM(revenue). Then compute both at month level and at year level, and show that averaging the monthly averages gives a third answer.
The script generates a year in which bigger days run on a thinner margin, as discount-driven days often do.
The question. How far apart are the average of daily margin rates and total margin over total revenue, and does averaging monthly averages give a third answer?
Predict
Expect a gap whenever revenue varies across days.
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide the code
average-of-ratios-against-ratio-of.py # /// script # requires-python = ">=3.10" # dependencies = ["numpy"] # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Average of ratios against ratio of sums. A year of daily revenue and margin. The margin rate computed as the average of the daily rates and as total margin over total revenue, at day, month and year level, and as the average of the monthly averages. """ import argparse, json import numpy as np p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--days", type=int, default=365, help="Days of data.") p.add_argument("--seed", type=int, default=0, help="Random seed.") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() rng = np.random.default_rng(args.seed) revenue = rng.uniform(100, 10_000, args.days) rate = np.clip(0.35 - 0.15 * (revenue - 100) / 9_900 + rng.normal(0, 0.05, args.days), 0.02, 0.6) # big days run on thinner margin margin = revenue * rate month = np.minimum(np.arange(args.days) * 12 // args.days, 11) avg_of_ratios = float(np.mean(margin / revenue)) ratio_of_sums = float(margin.sum() / revenue.sum()) monthly_avg = [float(np.mean(margin[month == m] / revenue[month == m])) for m in range(12)] monthly_ratio = [float(margin[month == m].sum() / revenue[month == m].sum()) for m in range(12)] avg_of_monthly = float(np.mean(monthly_avg)) avg_of_monthly_ratios = float(np.mean(monthly_ratio)) rows = [{"method": "AVG(margin / revenue) over days", "margin_rate_pct": avg_of_ratios * 100}, {"method": "SUM(margin) / SUM(revenue)", "margin_rate_pct": ratio_of_sums * 100}, {"method": "average of the monthly averages", "margin_rate_pct": avg_of_monthly * 100}, {"method": "average of the monthly ratios of sums", "margin_rate_pct": avg_of_monthly_ratios * 100}] for r in rows: print(f"{r['method']:<40} {r['margin_rate_pct']:6.2f}%") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"methods": rows, "months": [{"month": m + 1, "avg_of_ratios_pct": a * 100, "ratio_of_sums_pct": b * 100} for m, (a, b) in enumerate(zip(monthly_avg, monthly_ratio))], "summary": [{"gap_pts": (avg_of_ratios - ratio_of_sums) * 100}]}}, f)3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.
Terminal uv run average-of-ratios-against-ratio-of.py uv run average-of-ratios-against-ratio-of.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --days365 | Days of data. |
| --seed0 | Random seed. |
Our run
One year of margin rate, four ways
| Method | Margin rate |
|---|---|
| AVG(margin / revenue) over days | 27% |
| SUM(margin) / SUM(revenue) | 24.7% |
| average of the monthly averages | 27% |
| average of the monthly ratios of sums | 24.7% |
AVG(margin / revenue) over days 27.01%
SUM(margin) / SUM(revenue) 24.72%
average of the monthly averages 27.02%
average of the monthly ratios of sums 24.73%What we found
The average of daily rates said 27%; total margin over total revenue said 24.7%, a gap of 2.29 percentage points, because the big days had the thin margins. The average of monthly averages (27%) was barely different from the daily average: months have nearly the same number of days, so averaging by month re-weights almost nothing. It becomes a genuinely third answer when the groups differ in size, as weeks split by month boundaries or regions of very different volume do.
Read it
You cannot sum (or average) a measure across a dimension it is not additive over. Store the numerator and the denominator, add them up at whatever level the question asks, and divide last. A ratio stored as a column invites someone to average it.
When two dashboards disagree on a rate and no check fails, the first thing to look for is a difference in how the ratio was aggregated, not a difference in code.