Skip to content

Rounding That Lies: round each line then sum, and the total is wrong by cents

Before you start

New to this? Money only comes in whole cents, but arithmetic on money usually doesn’t: 7.25% tax on $234.32 is $16.9882 — a fraction of a cent. Somewhere that has to be rounded to a real cent amount. The trap this project is about: when you have many such fractional amounts, the order you round in changes the answer. Round each line to the cent and then add the lines up, and you can get a different total than if you add the exact amounts first and round once at the end — different by whole cents on a big invoice. The same trap has a sibling: split a fixed pot (a bill, a budget) into shares, round each share, and the rounded shares no longer add back to the pot. This project teaches you to round on purpose — where the rule says, and so the pieces reconcile — instead of wherever the code happens to.

1. The Brief

You’re handed one month’s invoice — about 180 line items, each with its own tax — and asked to check the tax total. Add it up the obvious way (round the tax on each line, then sum) and you get a number four cents higher than the finance team’s books, which sum the exact tax first and round once. Neither total is a typo; the two orders of rounding genuinely disagree, and the gap is the pile of leftover half-cents you rounded away, 180 times over. This is not a toy: rounding order in billing, tax, and payroll is a standard source of reconciliation breaks, and a total that’s “off by a few cents for no reason” is exactly what an auditor stops on. You’ll build both totals, see where the cents come from, learn which order a stated rule actually requires — and then, in Part B, meet the same trap wearing different clothes: splitting a fixed bill so the parts add back up.

2. Difficulty & time

Difficulty 2/10. One concept (where you round changes the total), a couple of tools (pandas to load, numpy to round), Part A’s path fully specified — the low end of the scale. It sits level with averaging-averages (2) and days-of-cover (2) — each turns on a single idea applied cleanly — and below silent-row-loss (3) and benford-fraud (3), which carry more moving parts (a join to validate; a leading-digit distribution to compute and test against a reference). Like those level-2s it is a genuine judgment skill — knowing rounding order matters and picking the right one — not a syntax drill, which is what 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

  • Explain, with a number, why rounding each line then summing can differ from summing then rounding — and compute both.
  • Look at a stated rule (tax on the whole invoice vs a rounded amount printed per line) and say which rounding order it requires.
  • Allocate a fixed total across shares so the rounded parts sum exactly to the total, using the largest-remainder method.
  • Spot the general pattern: whenever fractional amounts must reconcile to a whole, decide the rounding scheme deliberately instead of rounding piecemeal.

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 columns; 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 a fixed seed (byte-identical every run) — see Sources. Generate it once:

Terminal window
python data/make_data.py

That writes data/ledger_a.csv (Part A — 180 invoice lines) and data/shares_b.csv (Part B — 7 departments). Nothing is downloaded; there is no external source. Synthetic is the right call here: the lesson is whether your total (and your split) matches the exact figure, and only generated data lets you know that figure 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):

Terminal window
python part_a/reconcile_tax.py

It walks six steps and writes part_a/tax_reconciliation_a.csv. The shape of the argument:

  • STEP 1–2. Load the 180 lines and compute the exact tax per line (amount × tax_rate). Checkpoint: subtotal $23,608.71; the exact tax carries fractions of a cent (e.g. 14.7510, 16.9882) — so something has to round.
  • STEP 3. The billing software’s total: round each line to the cent, then sum. Checkpoint: $1,739.83.
  • STEP 4. Finance policy’s total: sum the exact tax, then round once. Checkpoint: $1,739.79 — four cents lower. The billing software overcharged tax by $0.04 on one invoice.
  • STEP 5. See why: the 180 per-line rounding errors sum to $0.0448, and that leftover pile is exactly the gap. assert proves the accumulated error equals the difference.
  • STEP 6. Write a small reconciliation summary you can eyeball.

Why this and not that

  • Why is neither total “the bug”? They answer different questions. Round-each-line answers “what tax is printed on each line, added up”; sum-then-round answers “what is the tax on this invoice.” Which one is correct is set by the rule, not the arithmetic — here, finance policy taxes the invoice as a whole, so sum-then-round wins. Under a rule that requires a rounded tax on every printed line, the total is defined as the sum of those rounded lines, and the other order is correct. Know the rule, then pick the order.
  • Why does the gap grow with the number of lines? Each line’s rounding is off by up to half a cent. Sum-then-round makes one such error; round-then-sum makes 180 and adds them. They don’t fully cancel, so more lines means a bigger pile of leftover cents — the error accumulates.

Functions you’ll meet (and where to read more)

FunctionWhat it does here
pandas.read_csvLoad ledger_a.csv into a DataFrame
numpy.roundRound each line’s exact tax to the cent (STEP 3)
pandas.Series.sumTotal the tax — before rounding (STEP 4) or after (STEP 3)

If something looks off

  • If your two totals are identical, check that tax_exact really has sub-cent decimals — if every line’s tax already landed on a whole cent, there’s nothing to round and no gap.
  • If the gap is much larger than a few cents, you probably rounded to the wrong number of places (dollars, not cents) — round to 2 decimals.
  • A four-cent gap 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 # TODO it 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 trap, new setting. A $10,000.00 monthly cloud bill (shares_b.csv) must be split across seven departments by how many vCPU-hours each used. The starter divides the pot by each department’s usage share, rounds each share on its own — and the parts come to $9,999.98, two cents short of the bill. Finance can’t post a split that doesn’t reconcile. The # TODO is the same idea as Part A (round on purpose, not piecemeal), but here the fix is the largest-remainder method: give each department the floor of its share, then hand out the leftover cents one each to the departments with the largest fractional remainder, so the parts sum to the pot exactly.

Acceptance criteria (checkable)

Your part_b/starter.py writes part_b/out/report.json with:

  • allocation — a {department: cents} map, and
  • total_allocated_cents — the sum of your allocation.

Then:

Terminal window
python part_b/starter.py
pytest part_b/test_solution.py -q

You’re done when both tests pass: the parts sum to exactly 1,000,000 cents ($10,000.00), and every department’s cents match the largest-remainder allocation. The naive round-each-share starter is two cents short and fails the first test.

Constraints — what must be true of your solution, not how to get there

  • The parts must sum to exactly the pot — 1,000,000 cents. Not 999,998; not 1,000,001.
  • Every department gets a whole number of cents (no fractional cents in the output).
  • Don’t drop or merge any department, and don’t just dump the leftover on the biggest one — the leftover cents go to the largest fractional remainders.

Hints

Hint 1 — why naive rounding can't reconcile

Each department’s exact share is fractional cents. Round each independently and you’ve made seven separate rounding decisions with no coordination, so their sum can land a cent or two off the pot. To hit the total exactly, the rounding has to be decided together, not per department.

Hint 2 — floors plus a leftover

Give every department floor(exact_share) cents. Now TOTAL - sum(floors) cents are left over (here, a small number). Those are the cents you must distribute — one per department — to make the sum exact. The only question is which departments get them.

Hint 3 — which departments get the extra cent

Rank the departments by their fractional remainder exact_share - floor(exact_share), largest first, and give the leftover cents to the top of that list. numpy.argsort on the negative remainders gives you the order. That’s the largest-remainder (Hamilton) method.

8. Self-check

  • Part A: your two totals should differ by a small number of cents (single digits), and sum-then-round should be the lower one here. If they’re identical, nothing was fractional; if they differ by dollars, you rounded to the wrong place.
  • Part B: sum your allocation and confirm it equals 1,000,000 exactly before you even run the test. If it doesn’t, the largest-remainder step isn’t distributing every leftover cent.
  • Re-running python data/make_data.py never changes the data (it’s seeded), so your numbers shouldn’t move between runs.
  • If you can state, in one sentence, why the order of rounding changes the answer, you’ve got the transferable skill — the specific cents are secondary.

9. Stretch

  • In Part A, add a by_category breakdown: compute round-then-sum and sum-then-round within each tax category and see which categories contribute most of the four-cent gap.
  • In Part B, make the allocation stable: if usage changes slightly month to month, does the set of departments that receive a leftover cent jump around? Compare largest-remainder against always rounding the same departments up, and note which is fairer over a year.
  • (Genuinely hard.) Largest-remainder has a famous flaw — the Alabama paradox: increasing the total pot can decrease a department’s allocation. Construct a small example where raising the pot by one cent takes a cent away from a department, and explain why it happens. (Then read about divisor methods, which avoid it.)

10. Ship it

Put this in a repo README so it counts as a portfolio piece: one sentence on the trap (“rounding each of 180 invoice lines before summing overcharged tax by four cents versus rounding the total once”), the two totals side by side, and the one-line rule (round where the rule says; the order changes the answer). Add a short note on Part B showing you transferred the same idea to a different problem — splitting a fixed bill so the parts reconcile — with the largest-remainder method. Recognising the same skill under a different surface is what an interviewer is checking.

11. Sources

See SOURCES.md. The data is synthetic, generated by data/make_data.py from a fixed seed; there is no external source, and nothing is downloaded or committed.

Next up

Finished this one? Continue the Data Analytics track:

Top-N and the Tail: your best-seller list depends on the metric, and hides most of the money  ·  Browse all courses