Window functions: rolling, expanding, ewm

pandas · Advanced · 15 min

Smoothing, trend detection, running KPIs and feature engineering all rely on windowed calculations.

Window functions compute a statistic over a window of neighbouring rows for every row, much like SQL's OVER (...) clauses.

`rolling(window)` uses a fixed-size moving window: s.rolling(7).mean() is a 7-row moving average. The first six results are NaN because the window isn't full; min_periods=1 relaxes that. center=True centres the window instead of looking back.

With a DatetimeIndex (sorted), the window can be a time span: s.rolling("7D").sum() includes all observations in the last 7 days, however many there are. That's correct for irregular data, where a 7-row window would mean different things on different days. Use on="ts" to window on a column instead of the index.

`expanding()` grows from the start: s.expanding().max() is the running maximum, expanding().mean() the cumulative average. (For sums, cumsum() is the simpler equivalent.)

`ewm(span=10)` gives exponentially weighted statistics, which react faster than simple moving averages because recent points weigh more. span, halflife, com and alpha are alternative ways to set the decay.

Windows combine with groupby for per-entity calculations:

df.groupby("store")["sales"].rolling(3).mean()

The result carries a MultiIndex (store, original index); reset_index(level=0, drop=True) aligns it back to df. Alternatively, transform(lambda s: s.rolling(3).mean()) returns a row-aligned result directly.

Related shift-based tools: shift(1) (previous value), diff(), pct_change(). Remember that a rolling window includes the current row; for features in predictive models use shift(1) first to avoid leaking today's value into today's prediction. Custom functions via rolling(...).apply(f, raw=True) work but are slow; engine="numba" can speed them up.

Example

import pandas as pd

ts = pd.to_datetime(["2024-01-01", "2024-01-02", "2024-01-03", "2024-01-07", "2024-01-08"])
s = pd.Series([10, 20, 30, 40, 50], index=ts)

print(s.rolling(2).mean().tolist())          # [nan, 15.0, 25.0, 35.0, 45.0]
print(s.rolling("3D").sum().tolist())        # [10.0, 30.0, 60.0, 40.0, 90.0]  by time
print(s.expanding().max().tolist())          # [10.0, 20.0, 30.0, 40.0, 50.0]
print(s.ewm(span=3).mean().round(1).tolist())

df = pd.DataFrame({"store": ["a", "a", "a", "b", "b"], "sales": [1, 2, 3, 10, 20]})
df["ma2"] = df.groupby("store")["sales"].transform(lambda x: x.rolling(2, min_periods=1).mean())
df["prev"] = df.groupby("store")["sales"].shift(1)    # no look-ahead leakage
print(df)
#   store  sales   ma2  prev
# 0     a      1   1.0   NaN
# 1     a      2   1.5   1.0
# 2     a      3   2.5   2.0
# 3     b     10  10.0   NaN
# 4     b     20  15.0  10.0

Remember

  • rolling(n) is a fixed row window; rolling('7D') a time window on a sorted DatetimeIndex
  • expanding() accumulates from the start; ewm() weights recent values more
  • Combine with groupby for per-entity windows
  • Shift before rolling when building predictive features

Watch out for

  • Using a row-count window on irregular timestamps, so '7 rows' covers very different time spans.

Check yourself

  1. Daily data has missing days. You want the sum over the last 7 calendar days. Which call?
  2. Why is `s.rolling(3).mean()` NaN for the first two rows by default?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pandas.pydata.org/docs/user_guide/window.html

Builds on

Unlocks

Nothing builds on this yet.