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.0Remember
rolling(n)is a fixed row window;rolling('7D')a time window on a sorted DatetimeIndexexpanding()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.