datarekha
Pandas & Data Wrangling Medium Asked at Two SigmaAsked at Goldman SachsAsked at Spotify

How do rolling and expanding windows work in pandas, and when do you use each?

The short answer

rolling() computes statistics over a fixed-size sliding window, discarding data outside the window; expanding() grows the window from the first row to the current row, equivalent to an ever-increasing cumulative calculation. Both return objects you chain .mean(), .sum(), .std(), or a custom .apply() onto.

How to think about it

What the interviewer is really probing is whether you understand what each window forgets. A rolling window is a sliding spotlight — it sees only the last N rows and deliberately drops everything older. An expanding window is a running total — it never forgets, widening with every new row. Get that distinction and the use cases fall out on their own: rolling for short-term trend (“is this week above the 4-week average?”), expanding for all-time metrics (“what is the running mean up to today?”).

Both work the same way in code. You call .rolling(window=N) or .expanding() to get a window object, then chain the statistic you want — .mean(), .sum(), .std(), or a custom .apply(). The only subtlety worth memorizing is the warm-up: a 3-row rolling window has nothing to average for the first two rows, so it returns NaN there unless you tell it otherwise with min_periods.

Four windows on one series

Put a rolling mean, a rolling mean with min_periods=1, an expanding mean, and an expanding max side by side on the same price series, and the behaviors separate cleanly:

import pandas as pd

prices = pd.Series([10, 12, 11, 14, 13, 15, 16, 14, 17, 18], name="price")
df = pd.DataFrame({"price": prices})

df["sma_3"]       = df["price"].rolling(window=3).mean()
df["sma_3_full"]  = df["price"].rolling(window=3, min_periods=1).mean()
df["cum_mean"]    = df["price"].expanding().mean()
df["running_max"] = df["price"].expanding().max()

print(df.round(2).to_string())
   price  sma_3  sma_3_full  cum_mean  running_max
0     10    NaN       10.00     10.00         10.0
1     12    NaN       11.00     11.00         12.0
2     11  11.00       11.00     11.00         12.0
3     14  12.33       12.33     11.75         14.0
4     13  12.67       12.67     12.00         14.0
5     15  14.00       14.00     12.50         15.0
6     16  14.67       14.67     13.00         16.0
7     14  15.00       15.00     13.12         16.0
8     17  15.67       15.67     13.56         17.0
9     18  16.33       16.33     14.00         18.0

Read the columns against each other. sma_3 is NaN for the first two rows — not enough history yet — then tracks only the latest three prices, so it dips and rises sharply with recent moves. sma_3_full fills those early gaps because min_periods=1 lets it average whatever it has. cum_mean climbs slowly and smoothly: each value folds in all prices so far, so one new point barely moves it. And running_max is a ratchet — it only ever goes up, recording the all-time high at every row. Rolling reacts; expanding remembers.

The same machinery extends naturally. On a DatetimeIndex you can pass a time offset instead of a row count — ts.rolling("3D").mean() covers 3 calendar days regardless of how many rows fall inside, which is the correct way to handle irregular timestamps. For per-group features, pair it with groupby and transform so the result stays index-aligned and assigns straight back as a column:

df["rolling_avg"] = (
    df.groupby("ticker")["close"]
      .transform(lambda s: s.rolling(window=5).mean())
)

And for custom logic, .apply() takes any function over the window — prices.rolling(3).apply(lambda x: x[-1] - x[0], raw=True) computes 3-period momentum; raw=True hands the lambda a NumPy array instead of a Series, skipping pandas overhead and running noticeably faster on large windows.

Learn it properly Time series

Keep practising

All Pandas & Data Wrangling questions

Explore further

Skip to content