Python for Data course menu
Rolling Averages, Cumulative Sums and Percent Change in pandas
Smooth and compare time series in pandas: rolling means and sums, centred and time-based windows, expanding windows, cumulative sums, shift for previous values, and pct_change for growth rates.
On this page
Daily numbers jump around. To see the trend, analysts use moving averages, running totals and growth rates. pandas computes all of them in one line each.
The examples use two weeks of daily orders:
import pandas as pd
orders = pd.Series([12, 15, 9, 20, 18, 30, 28, 14, 16, 11, 22, 19, 33, 31],
index=pd.date_range("2026-05-04", periods=14, freq="D"), name="orders")
print(orders.head())2026-05-04 12
2026-05-05 15
2026-05-06 9
2026-05-07 20
2026-05-08 18
Freq: D, Name: orders, dtype: int64Rolling windows#
rolling(n) looks at the last n values for each position. A 7-day rolling mean smooths out the weekly pattern:
import pandas as pd
orders = pd.Series([12, 15, 9, 20, 18, 30, 28, 14, 16, 11, 22, 19, 33, 31],
index=pd.date_range("2026-05-04", periods=14, freq="D"), name="orders")
table = pd.DataFrame({
"orders": orders,
"avg_3d": orders.rolling(3).mean().round(1),
"avg_7d": orders.rolling(7).mean().round(1),
"sum_7d": orders.rolling(7).sum(),
})
print(table) orders avg_3d avg_7d sum_7d
2026-05-04 12 NaN NaN NaN
2026-05-05 15 NaN NaN NaN
2026-05-06 9 12.0 NaN NaN
2026-05-07 20 14.7 NaN NaN
2026-05-08 18 15.7 NaN NaN
2026-05-09 30 22.7 NaN NaN
2026-05-10 28 25.3 18.9 132.0
2026-05-11 14 24.0 19.1 134.0
2026-05-12 16 19.3 19.3 135.0
2026-05-13 11 13.7 19.6 137.0
2026-05-14 22 16.3 19.9 139.0
2026-05-15 19 17.3 20.0 140.0
2026-05-16 33 24.7 20.4 143.0
2026-05-17 31 27.7 20.9 146.0The first rows are NaN because there are not enough earlier values yet. Use min_periods to allow partial windows, for example rolling(7, min_periods=1).
Centred and time-based windows#
import pandas as pd
orders = pd.Series([12, 15, 9, 20, 18, 30, 28],
index=pd.date_range("2026-05-04", periods=7, freq="D"))
print(orders.rolling(3, center=True).mean().round(1).tolist()) # neighbours on both sides
print(orders.rolling("3D").sum().tolist()) # last 3 calendar days[nan, 12.0, 14.7, 15.7, 22.7, 25.3, nan]
[12.0, 27.0, 36.0, 44.0, 47.0, 68.0, 76.0]A centred window uses values before and after, which suits smoothing for charts but not forecasting (it uses future values). A time-based window like "3D" counts calendar days, so it handles missing dates correctly.
Cumulative values#
import pandas as pd
monthly = pd.Series([120, 95, 140, 160, 130, 175],
index=["Jan", "Feb", "Mar", "Apr", "May", "Jun"], name="revenue")
print(pd.DataFrame({
"revenue": monthly,
"year_to_date": monthly.cumsum(),
"best_so_far": monthly.cummax(),
"running_avg": monthly.expanding().mean().round(1),
})) revenue year_to_date best_so_far running_avg
Jan 120 120 120 120.0
Feb 95 215 120 107.5
Mar 140 355 140 118.3
Apr 160 515 160 128.8
May 130 645 160 129.0
Jun 175 820 175 136.7expanding() is a window that starts at the first row and grows, so expanding().mean() is the average of everything so far.
Previous values and growth#
shift(1) moves values down one row, which lines each value up with the one before it. pct_change computes the growth rate directly:
import pandas as pd
monthly = pd.Series([120, 95, 140, 160, 130, 175],
index=["Jan", "Feb", "Mar", "Apr", "May", "Jun"], name="revenue")
print(pd.DataFrame({
"revenue": monthly,
"previous": monthly.shift(1),
"change": monthly.diff(),
"growth_pct": (monthly.pct_change() * 100).round(1),
})) revenue previous change growth_pct
Jan 120 NaN NaN NaN
Feb 95 120.0 -25.0 -20.8
Mar 140 95.0 45.0 47.4
Apr 160 140.0 20.0 14.3
May 130 160.0 -30.0 -18.8
Jun 175 130.0 45.0 34.6March grew by 47.4% over February (from 95 to 140). These match SQL's LAG and window calculations.
Per group#
Combine with groupby to compute running values separately for each group:
import pandas as pd
sales = pd.DataFrame({
"store": ["A", "A", "A", "B", "B", "B"],
"month": ["Jan", "Feb", "Mar", "Jan", "Feb", "Mar"],
"revenue": [100, 120, 90, 200, 180, 240],
})
sales["ytd"] = sales.groupby("store")["revenue"].cumsum()
sales["growth_pct"] = (sales.groupby("store")["revenue"].pct_change() * 100).round(1)
print(sales) store month revenue ytd growth_pct
0 A Jan 100 100 NaN
1 A Feb 120 220 20.0
2 A Mar 90 310 -25.0
3 B Jan 200 200 NaN
4 B Feb 180 380 -10.0
5 B Mar 240 620 33.3Each store's running total restarts, and the first month of each store has no previous value to compare with.
Common mistakes#
Practice#
Practice exercise
Weekly sign-ups are [40, 52, 47, 60, 75, 68, 90, 84] for 8 weeks. Print a table with the sign-ups, the 4-week moving average (1 decimal), the running total, and the week-over-week growth in percent (1 decimal). Which week had the biggest percentage growth?
Show hint
rolling(4).mean(), cumsum() and pct_change() * 100. idxmax() on the growth column gives the week.
Show solution
import pandas as pd
signups = pd.Series([40, 52, 47, 60, 75, 68, 90, 84], index=range(1, 9), name="signups")
table = pd.DataFrame({
"signups": signups,
"avg_4w": signups.rolling(4).mean().round(1),
"total": signups.cumsum(),
"growth_pct": (signups.pct_change() * 100).round(1),
})
print(table)
print("Biggest growth in week", table["growth_pct"].idxmax()) signups avg_4w total growth_pct
1 40 NaN 40 NaN
2 52 NaN 92 30.0
3 47 NaN 139 -9.6
4 60 49.8 199 27.7
5 75 58.5 274 25.0
6 68 62.5 342 -9.3
7 90 73.2 432 32.4
8 84 79.2 516 -6.7
Biggest growth in week 7Key takeaway#
Next, the statistics behind the numbers: Descriptive Statistics.
Finished this lesson?