Percent-Change Traps: why +20% then -20% doesn't get you back
Before you start
New to this? A percentage change rebases a number: “+20%” means multiply by 1.20, “-20%”
means multiply by 0.80. The catch this project is about: when several percentage changes happen
one after another, each one applies to the result of the last one, not to the original number.
So they don’t add up, and they don’t cancel out. Up 20% then down 20% is not back to the start:
100 → 120 → 96, because the 20% cut came off 120, not off 100 — you land at 96% of where you began.
The right way to combine successive percent changes is to multiply the factors (1.20 × 0.80 = 0.96), not add the percents (+20 − 20 = 0). This project teaches you to get the combined result
right every time, and to spot when someone added percents that should have been chained.
1. The Brief
You’re handed a product’s price history as twelve monthly percentage changes and asked one innocent question: what’s the price now? You can add the twelve percentages in your head — they come to +31% — and say the $100 item is now $131. It isn’t; it’s $123.93, because each month’s percent applied to that month’s price, not to the original $100. This is the compounding trap, and it shows up everywhere percentages stack: investment returns, inflation over several years, a discount on top of a discount, “we grew 10% a month.” Anyone who adds the percents when they should multiply the factors is off by an amount that grows with every step and with every swing. You’ll build the correct number, watch exactly where the naive one drifts, and learn the one rule that fixes the whole family of cases.
2. Difficulty & time
Difficulty 2/10. One tool (pandas), one file, one concept, and Part A’s path is fully
specified — the low end of the scale. It sits level with averaging-averages (2) — both turn
on a single arithmetic idea a plain average or sum gets wrong — and below silent-row-loss (3)
and cohort-retention (3), each of which has more moving parts (a join to validate, a cohort
matrix to pivot) where this one turns on a single move: multiply the factors, don’t add the
percents. It’s a genuine judgment skill, not a syntax drill, which keeps it off the floor of the
scale. Part A is a short read-and-run; Part B is where you do the work.
3. What you’ll be able to do after
- Combine a sequence of percentage changes correctly by chaining the factors
(1 + pct/100), and explain in one sentence why adding the percents overstates or understates the result. - Show why “+20% then -20%” lands at 96% of the start, not 100% — and generalise it.
- Reconstruct a true end value (a price, a portfolio balance) from a start value and a run of percentage changes, and turn it back into a cumulative % change.
- State the one condition under which adding the percents is fine (when every change is applied to the same base — i.e. there’s only one step).
4. Prereqs & time box
Part A: ~30 minutes. Part B: ~1.5 hours. Both are hard caps — if Part A runs long, you’re overthinking it. You need to read a CSV with pandas and do arithmetic on a column; nothing more.
For where to write and run Python (install, virtual environment, per-OS terminal, running a script step by step), see Start Here guide. No account needed, and no GPU.
5. Setup & data
The data is synthetic, generated locally by data/make_data.py from
fixed seeds (byte-identical every run) — see Sources. Generate it once:
python data/make_data.pyThat writes data/prices_a.csv (Part A) and data/returns_b.csv (Part B). Nothing is
downloaded; there is no external source. Synthetic is the right call here: the lesson is whether
your chained answer matches the true compounded value, and only generated data lets you know
that truth in advance.
6. Part A — Guided Build
Prefer the browser? If this project shows a ▶ Run button, you can run Part A right on the page — no setup. The commands below are for running on your own computer.
▶ Try it now — run the real Part A right here in your browser. This one button makes the data and runs Part A for you — you do not need to install anything, and you do not need to type any of the python … commands anywhere on this page (those are only for running on your own computer instead). First run loads Python + this project’s libraries: usually ~10–20 s, a little longer for machine-learning projects.
Run the guided script (or paste it cell-by-cell — each # %% is one step):
python part_a/analyze_prices.pyIt walks seven steps and writes part_a/price_path_a.csv. The shape of the argument:
- STEP 1–2. Load the twelve monthly percent changes and take the naive answer — add the percents (+31%) and apply the total once to the $100 start. Checkpoint: 131.00.
- STEP 3. Take the true answer — chain the twelve factors
(1 + pct/100)and multiply by the start. Checkpoint: 123.93. About $7 below the naive number, and it’s the price you’d actually ring up. - STEP 4. Watch the running price and the trap in the raw: after Jan (+10) it’s 110.00, after Feb (+20) 132.00, after Mar (−20) 105.60 — not back to 110, because the −20% came off 132.
- STEP 5–6. See why they disagree (adding percents pretends every month shared one base),
then the rule, verified in code with
assert: the product of the factors equals the last running price, exactly.
Why this and not that
- Why not just add the percents? Because a percent has no meaning without its base, and the base moves every month. Adding them answers “what if each change applied to the original $100” — a question nobody asked. The gap is all the cross-terms (a percent of another percent’s dollars) that “just add them” throws away.
- Why does the gap appear here and not always? Only when there’s more than one step and the changes aren’t all zero. With a single change, adding and multiplying agree. With many, the more the price swings, the wider the gap — which is exactly what Part B pushes on.
Functions you’ll meet (and where to read more)
| Function | What it does here |
|---|---|
pandas.read_csv | Load prices_a.csv into a DataFrame |
Series.sum | The naive “add up the percents” (STEP 2) |
Series.prod | Chain all twelve factors into the true multiplier (STEP 3) |
Series.cumprod | The running price after each month, so you can see where the answers part (STEP 4) |
If something looks off
- If your chained and naive numbers are equal, your percents are probably all the same sign and
tiny — or you added the factors instead of multiplying them. Multiply
(1 + pct/100), don’t sum. - If the true final looks higher than the naive sum instead of lower, that’s not a bug: with the right mix of ups and downs the naive sum can understate too (you’ll see exactly that in Part B). The point isn’t the direction — it’s that adding percents is simply the wrong operation.
- A number that surprises you is the point of this project, not a bug — read it against the checkpoints above before assuming you broke something.
7. Part B — Your Turn
Part B is the optional “your turn” half — a guided fill-in-the-blank, not a blank page. You get a working starter (
part_b/starter.py) that already handles the easy version; your job is to finish the# TODOit marks out. More open than Part A’s step-by-step, but you are never staring at an empty file. If you finished Part A and want to prove the skill, this is where it happens. New to Python? It’s completely fine to stop after Part A and come back later.
Same skill, new setting. A $10,000 investment’s returns_b.csv gives 24 monthly returns (with one
crash month and one big rally). Someone reports the year by adding the monthly returns — “+23.2%,
so you have $12,320.” The starter prints exactly that naive answer, which is wrong, because each
month’s return compounds on the previous month’s balance, not on the original $10,000. The # TODO
is the same move as Part A: chain the factors instead of adding the percents. The twist — you must
also turn the compounded end value back into a cumulative return %.
Acceptance criteria (checkable)
Your part_b/starter.py writes part_b/out/report.json with:
final_value— the compounded end value of the $10,000 (dollars),cumulative_return_pct— the true cumulative return,(final_value / 10000 − 1) × 100,n_months— the number of monthly returns.
Then:
python part_b/starter.pypytest part_b/test_solution.py -qYou’re done when the tests pass: n_months is exact, and both final_value and
cumulative_return_pct land within 0.5% of the true compounded figures (the reference hits
them exactly). The naive add-the-returns answer is about $829 too high ($12,320 vs the true
$11,491.14) and fails.
Constraints — what must be true of your solution, not how to get there
- The end value must chain all 24 monthly factors — no adding the percents and applying once.
- Keep every month, including the crash and the rally; they belong in the chain in order.
Hints
Hint 1 — what "chain the factors" means here
In Part A each month multiplied the price by (1 + pct/100). Here each month multiplies the
balance by (1 + return_pct/100). Same operation, new column name.
Hint 2 — get the whole multiplier in one step
You don’t need a loop. Build the factor column, then take the product of all 24 of them — that single number is the total multiplier. Multiply the $10,000 by it.
Hint 3 — the shape of the answer
factors = 1 + returns['return_pct'] / 100, then
final_value = 10000 * factors.prod(), and
cumulative_return_pct = (final_value / 10000 - 1) * 100. It should come out below the naive
sum, because the −30% crash bites a big balance and the rally can’t fully undo it.
8. Self-check
- Your compounded end value should be below the naive add-the-returns figure here (the crash compounds against you), and the cumulative return should read like a believable year — a modest gain, not the +23.2% the naive sum claims.
- Re-running
python data/make_data.pynever changes the data (it’s seeded), so your number shouldn’t move between runs. - If you can state, in one sentence, why you multiply the factors instead of adding the percents, you’ve got the transferable skill — the specific numbers are secondary.
9. Stretch
- Add an annualised return to Part B: turn the 24-month compounded multiplier into a monthly rate (its 24th root) and then a 12-month figure, and confirm it’s not the average of the monthly returns.
- Compute the “reversal” price for Part A: after all twelve changes, what single percentage
change would bring the price back to exactly $100? Show it isn’t
−31%. - (Genuinely hard.) Two series can have the same sum of percentage changes yet different compounded results. Construct a second 12-month series whose percents also sum to +31% but whose chained final price differs from Part A’s by more than $2, and explain in one line what property of the series (hint: how much it swings) drives the gap. This is volatility drag.
10. Ship it
Put this in a repo README so it counts as a portfolio piece: one sentence on the trap (“adding twelve monthly percent changes overstated the price by ~$7 on a $100 item — they compound, they don’t add”), the two numbers side by side, and the one-line rule (multiply the factors). Add a short note on Part B showing you transferred the same idea to investment returns and reconstructed both the end value and the cumulative return yourself. Recognising the same skill under a different surface — prices, returns, inflation, stacked discounts — is what an interviewer is actually checking for.
11. Sources
See SOURCES.md. The data is synthetic, generated by
data/make_data.py from fixed seeds; there is no external source, and
nothing is downloaded or committed.
Next up
Finished this one? Continue the Data Analytics track:
Rounding That Lies: round each line then sum, and the total is wrong by cents → · Browse all courses