Running totals and differences
The reductions that don’t reduce — cumsum, diff, and their friends.
.cumsum.cumprodnp.diffnp.gradientnp.maximum.accumulateprepend=Watch it happen
Play it through, or step back and forth yourself.
xx.sum()x.sum() gives one number: 22. Everything about the journey is gone — you can't tell whether it climbed steadily or spiked once.
The idea
sum collapses an axis to one number and throws away the journey. Cumulative operations keep the journey: same length out, showing the value at every point along the way.
x = np.array([3, 5, 2, 8, 4])
x.sum() # 22
x.cumsum() # [ 3 8 10 18 22] <- same length, last value is the sumAnywhere you'd otherwise write a loop with a running variable — a bank balance, a cumulative share of the total, a race position over time — this is the one-liner.
diff goes the other way
np.diff(x) # [ 2 -3 6 -4] <- gaps between neighboursNote the length: one shorter. Five values have four gaps between them. That catches people out when they try to put the result back next to the original, and there's a fix built in — np.diff(x, prepend=0) keeps the length by assuming a zero in front.
diff and cumsum undo each other, which is a genuinely useful relationship: it's how you convert between "total so far" and "change this period", the two ways every dataset of running numbers gets stored.
The rest of the family
x.cumprod() # running product — compound growth
np.maximum.accumulate(x) # running maximum — the peak so far
np.minimum.accumulate(x) # running minimum
np.diff(x, n=2) # differences of the differences
np.gradient(x) # central differences — same length outnp.maximum.accumulate is worth knowing by name. Combined with cumsum, it's how you compute a drawdown — the gap between the running peak and the current value — in two lines and no loops:
balance = x.cumsum()
peak = np.maximum.accumulate(balance)
drawdown = peak - balanceThat .accumulate suffix works on any ufunc that takes two arguments, so np.add.accumulate is just cumsum spelled the long way.
Along an axis
On 2-D data these take axis like any reduction — but since nothing collapses, the output shape matches the input:
cups.cumsum(axis=0) # running total down the days, per stall
cups.cumsum(axis=1) # running total across the stalls, per day
cups.cumsum() # flattens first, then accumulates — rarely what you wantThat last one is the trap: with no axis, the array is flattened before accumulating, so you get one long run across all the rows joined end to end. Always name the axis on 2-D data.
Practice
Write it yourself. The answer is there when you want it.
Putting the kettle on…
Starting up…
Write it yourself
not gradedPrint x, its sum, its cumsum, then np.diff(x) and np.diff(x, prepend=0) so you can see which one keeps the length. Then treat the running total as a balance and compute the drawdown: the running peak with np.maximum.accumulate, minus the balance. Finish with cups.cumsum(axis=0).
Your turn
4 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
Return the running total of x.
Return the day-to-day changes in x — the gaps between consecutive values.
Return the running total of cups down the days — so each column accumulates independently, and the shape stays (4, 3).
Return the running maximum of x — the highest value seen so far at each position.
