Skip to content
About
Main content

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

Does averaging the monthly averages give a third answer, apart from the daily average and total ÷ total?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    average-of-ratios-against-ratio-of.py45 linesDownload
    View 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)
    
  3. 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
Knobs
Knob · defaultWhat it changes
--days365 Days of data.
--seed0 Random seed.

Our run

One year of margin rate, four ways

One year of margin rate, four ways
MethodMargin rate
AVG(margin / revenue) over days27%
SUM(margin) / SUM(revenue)24.7%
average of the monthly averages27%
average of the monthly ratios of sums24.7%
Reference runApple M2 · 8 cores · 8 GB
Output · uv run average-of-ratios-against-ratio-of.py
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%
macOS 26.6.2 · Python 3.13.3 · numpy 2.5.4 · run 2026-10-20

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 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 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.