Skip to content
Laviyx Learn

Try

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.

Intermediate9 min readUpdated
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:

Python
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())
Output
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: int64

Rolling windows#

rolling(n) looks at the last n values for each position. A 7-day rolling mean smooths out the weekly pattern:

Python
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)
Output
            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.0

The 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#

Python
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
Output
[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#

Python
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),
}))
Output
     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.7

expanding() 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:

Python
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),
}))
Output
     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.6

March 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:

Python
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)
Output
  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.3

Each 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
Python
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())
Output
   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 7

Key takeaway#

Next, the statistics behind the numbers: Descriptive Statistics.

Finished this lesson?