Pandas · Lesson 10 of 12

Rolling, Expanding and Window Calculations

Compute moving averages, running totals and group windows in pandas with rolling, expanding, ewm, shift, diff and time-based windows like rolling('7D').

  • Intermediate
  • 17 min read
  • 4 objectives

Before this lessonLesson 9: Dates, Plots and Exporting

What you will learn

  • Use rolling windows with min_periods, center and time-based sizes
  • Compute running totals and records with expanding and cumulative methods
  • Smooth noisy data with exponentially weighted averages
  • Apply window calculations per group without leaking across groups

Your Progress

0 of 12 lessons 0%

  • Lessons0 / 12
  • Completed0
  • Est. time left~ 3 hours

Create a free account to keep your progress on every device.

Tip: pressing Next marks this lesson complete automatically.

The dates lesson introduced rolling(7).mean() and shift in a few lines. Window calculations deserve a closer look, because they answer many everyday business questions: What is the 7-day average of signups? What is our running revenue for the year? How does each day compare to the same day last week? Is this value unusually high compared to recent history?

A window is a set of neighbouring rows that pandas looks at together. The three main kinds are rolling (a fixed-size window that slides along), expanding (everything from the start up to now) and exponentially weighted (all history, with recent rows counting more). All of them assume your rows are in time order, so sort first.

Rolling windows

rolling(n) groups each row with the n - 1 rows before it, then you choose an aggregation such as mean, sum, max or std. The first n - 1 results are missing because the window is not full yet. min_periods lets pandas compute a result from a partial window.

import pandas as pd

signups = pd.Series(
    [12, 15, 9, 20, 18, 25, 30],
    index=pd.date_range("2026-06-01", periods=7, freq="D"),
    name="signups",
)

df = signups.to_frame()
df["avg_3"] = signups.rolling(3).mean().round(1)
df["avg_3_partial"] = signups.rolling(3, min_periods=1).mean().round(1)
df["max_3"] = signups.rolling(3).max()
print(df)
Output
            signups  avg_3  avg_3_partial  max_3
2026-06-01       12    NaN           12.0    NaN
2026-06-02       15    NaN           13.5    NaN
2026-06-03        9   12.0           12.0   15.0
2026-06-04       20   14.7           14.7   20.0
2026-06-05       18   15.7           15.7   20.0
2026-06-06       25   21.0           21.0   25.0
2026-06-07       30   24.3           24.3   30.0

By default the window is trailing: each value summarizes the current row and the ones before it, which is what you want for anything reported "as of" a day. center=True puts the current row in the middle of the window instead. That is nicer for smoothing a chart after the fact, but it peeks into the future, so never use it for features in a forecasting model.

Time-based windows

Real data has gaps: no orders on a holiday, a sensor that went offline. rolling(3) always means "3 rows", no matter how far apart they are. With a DatetimeIndex you can pass a duration like "3D" instead, and pandas includes whichever rows fall inside that span of time.

import pandas as pd

orders = pd.Series(
    [100, 80, 120, 90],
    index=pd.to_datetime(["2026-06-01", "2026-06-02", "2026-06-05", "2026-06-06"]),
)

out = pd.DataFrame({
    "amount": orders,
    "last_3_rows": orders.rolling(3).sum(),
    "last_3_days": orders.rolling("3D").sum(),
})
print(out)
Output
            amount  last_3_rows  last_3_days
2026-06-01     100          NaN        100.0
2026-06-02      80          NaN        180.0
2026-06-05     120        300.0        120.0
2026-06-06      90        290.0        210.0

On June 5 the row-based window adds up June 1, 2 and 5 (spanning five days), while the time-based window correctly only includes June 5 itself, because June 3 and 4 had no orders. Time-based windows also compute a value from the first row onward, since they use min_periods=1 by default.

Expanding windows and running totals

An expanding window starts at the first row and grows by one each step, so it answers "from the beginning until now" questions. For sums and extremes there are also shortcut methods: cumsum, cummax, cummin and cumprod.

import pandas as pd

revenue = pd.Series([120, 90, 150, 60, 200], name="revenue",
                    index=["mon", "tue", "wed", "thu", "fri"])

df = revenue.to_frame()
df["running_total"] = revenue.cumsum()
df["running_avg"] = revenue.expanding().mean().round(1)
df["best_so_far"] = revenue.cummax()
df["is_record"] = revenue == revenue.cummax()
print(df)
Output
     revenue  running_total  running_avg  best_so_far  is_record
mon      120            120        120.0          120       True
tue       90            210        105.0          120      False
wed      150            360        120.0          150       True
thu       60            420        105.0          150      False
fri      200            620        124.0          200       True

Exponentially weighted averages

A simple rolling mean treats every day in the window equally and forgets everything outside it, which makes it jump when a big value enters or leaves. ewm (exponentially weighted moving) gives every past value a weight that shrinks the older it is. The span parameter is roughly comparable to a rolling window size. It reacts faster to recent changes while staying smooth.

import pandas as pd

latency = pd.Series([100, 102, 98, 101, 180, 99, 100], name="ms")

df = latency.to_frame()
df["rolling_3"] = latency.rolling(3).mean().round(1)
df["ewm_3"] = latency.ewm(span=3, adjust=False).mean().round(1)
print(df)
Output
    ms  rolling_3  ewm_3
0  100        NaN  100.0
1  102        NaN  101.0
2   98      100.0   99.5
3  101      100.3  100.2
4  180      126.3  140.1
5   99      126.7  119.6
6  100      126.3  109.8

Comparing to earlier rows: shift, diff, pct_change

Many metrics compare a value with an earlier one. shift(n) moves values down by n rows, so you can line up "today" with "n rows ago". diff(n) and pct_change(n) are shortcuts for the difference and percentage change. With daily data, n=7 gives week-over-week comparisons that cancel out weekday effects.

import pandas as pd

visits = pd.Series([200, 220, 210, 260, 250],
                   index=pd.date_range("2026-06-01", periods=5, freq="D"))

df = pd.DataFrame({
    "visits": visits,
    "prev": visits.shift(1),
    "change": visits.diff(),
    "pct": (visits.pct_change() * 100).round(1),
})
print(df)
Output
            visits   prev  change   pct
2026-06-01     200    NaN     NaN   NaN
2026-06-02     220  200.0    20.0  10.0
2026-06-03     210  220.0   -10.0  -4.5
2026-06-04     260  210.0    50.0  23.8
2026-06-05     250  260.0   -10.0  -3.8

Windows per group

With several series in one long table (one row per store per day, say), a plain rolling mean would slide straight from the end of one store into the start of the next. Group first, and the window restarts for each group. groupby(...).rolling(...) returns a result with the group added to the index; transform keeps the original row order so you can assign it back as a column.

import pandas as pd

df = pd.DataFrame({
    "store": ["A", "A", "A", "B", "B", "B"],
    "day": [1, 2, 3, 1, 2, 3],
    "sales": [10, 20, 30, 100, 200, 300],
}).sort_values(["store", "day"])

df["avg_2"] = df.groupby("store")["sales"].transform(lambda s: s.rolling(2).mean())
df["cum_sales"] = df.groupby("store")["sales"].cumsum()
df["prev_day"] = df.groupby("store")["sales"].shift(1)
print(df)
Output
  store  day  sales  avg_2  cum_sales  prev_day
0     A    1     10    NaN         10       NaN
1     A    2     20   15.0         30      10.0
2     A    3     30   25.0         60      20.0
3     B    1    100    NaN        100       NaN
4     B    2    200  150.0        300     100.0
5     B    3    300  250.0        600     200.0

Look at the first row of store B: its avg_2 and prev_day are missing instead of borrowing store A's last value. That is exactly what you want.

Recap

  • Sort by time first; windows follow row order.
  • rolling(n) uses the last n rows; rolling("7D") uses the last 7 days and handles gaps correctly.
  • expanding() and cumsum/cummax answer "from the start until now" questions.
  • ewm(span=n) smooths while reacting faster to recent changes; shift, diff and pct_change compare to earlier rows.
  • Use groupby with transform, cumsum or shift so windows never cross group boundaries.
# Write your solution here

Finished reading? Mark this lesson complete to track your progress.

Up next · Lesson 11Performance and Large DatasetsSpeed up pandas on large datasets: measure memory, downcast dtypes, use categories and PyArrow, read CSVs in chunks, and know when to use Polars or DuckDB.