How do I calculate a running total within each group in pandas?
Short answer
Sort the data in order, then use groupby with cumsum so the running total restarts for each group. Forgetting to sort, or forgetting the groupby, are the two mistakes that give wrong totals.
The short version
Sort the rows into the order the total should accumulate in, then use groupby(key)["value"].cumsum(). The groupby makes the running total restart at each new group rather than carrying across group boundaries. Miss the sort and the total accumulates in the wrong order; miss the groupby and it runs across every group as one.
A worked example
import pandas as pd
orders = pd.DataFrame({
"city": ["Leeds", "Leeds", "Bath", "Leeds"],
"day": [1, 2, 1, 3],
"amount": [10, 20, 5, 30],
})
orders = orders.sort_values(["city", "day"])
orders["running"] = orders.groupby("city")["amount"].cumsum()
print(orders)
city day amount running
2 Bath 1 5 5
0 Leeds 1 10 10
1 Leeds 2 20 30
3 Leeds 3 30 60
Leeds accumulates 10, 30, 60 in day order, while Bath starts its own total from scratch.
Why the sort comes first
cumsum adds values in the order the rows currently sit, so it trusts your sort entirely. If the frame is not sorted, you get a running total of rows in some arbitrary order, which is meaningless. Always sort by the group key and then the ordering column (usually a date) before the cumsum.
Why the groupby matters
Without groupby, orders["amount"].cumsum() produces one total that never resets, mixing Leeds and Bath into a single stream. The groupby is what confines the accumulation to each group, which is almost always what "running total per city" means.
The wrong way people try
A common attempt is a manual loop with a dictionary of per-group totals. It works but is slow and easy to get wrong at the group boundaries. The groupby().cumsum() form is both faster and clearer, and it handles the reset for you.
Related cumulative functions
The same pattern works with cummax, cummin and cumprod for running maximum, minimum and product. For a running count of rows within each group (a per-group row number starting at zero), use groupby(key).cumcount() instead, which counts rather than sums.