Skip to content

pandas rolling

A window that slides down the frame one row at a time.

Is that spike a trend, or is it a Tuesday? Daily data rarely answers on its own, because the day-to-day jitter is bigger than the movement you care about. .rolling() replaces each value with a summary of the rows around it, so the shape survives and the noise flattens.

The call comes in two parts, and that surprises people. .rolling() computes nothing by itself: it returns a Rolling object describing the window, and you then call an aggregation on it. s.rolling(7).mean() is a seven-row moving average, s.rolling(7).max() is the seven-row high, and s.rolling(7).agg(['min', 'max']) gives you both in one pass.

Official documentation: DataFrame.rolling and Series.rolling.

The arguments that earn their keep

s.rolling(window=7,          # a number of rows, or a time offset like '7D'
          min_periods=None,  # how many real values before an answer appears
          center=False)      # label each window at its end, or at its middle

window as an integer counts rows. window as a string counts time, and needs a DatetimeIndex. Those are different questions on any series with gaps.

min_periods defaults to window for an integer window — hence the leading NaN values — and to 1 for a time-based one, which is why '7D' never returns NaN at all.

A worked example, on real data

The VIX is the market's estimate of how much the S&P 500 will move over the next month, and it is about as jumpy as public data gets. FRED serves it as a CSV with no key required:

import pandas as pd

url = 'https://fred.stlouisfed.org/graph/fredgraph.csv?id=VIXCLS'

vix = (
    pd.read_csv(url, parse_dates=['observation_date'], index_col='observation_date')
    .dropna()
    .loc['2026', 'VIXCLS']
)

vix.describe().round(2)
count    164.00
mean      18.70
std        3.49
min       14.25
25%       16.27
50%       17.80
75%       19.57
max       31.05
Name: VIXCLS, dtype: float64

A month is about 21 trading days, so that is the window:

vix.rolling(window=21).mean().round(2).loc['2026-01-28':'2026-02-04']
observation_date
2026-01-28      NaN
2026-01-29      NaN
2026-01-30    16.18
2026-02-02    16.27
2026-02-03    16.41
2026-02-04    16.60
Name: VIXCLS, dtype: float64

Note where the numbers start. Twenty rows of NaN came first, and nothing said so.

Three mistakes people make

Forgetting that the smoothed series is shorter. len() still reports 164, but only 144 of those rows hold a number:

len(vix), len(vix.rolling(21).mean().dropna())
(164, 144)

Twelve percent of the year vanished into the window. On a chart the line simply starts late; in an aggregation it is worse, because a mean or a corr() against another column quietly drops those rows and computes on a different denominator than you think. min_periods=1 fills them in from whatever is available:

vix.rolling(window=21, min_periods=1).mean().round(2).head(4)
observation_date
2026-01-02    14.51
2026-01-05    14.70
2026-01-06    14.72
2026-01-07    14.88
Name: VIXCLS, dtype: float64

That first value is a one-day "monthly" average, so it is honest only if you say what it is. Pick a floor you can defend — min_periods=10 on a 21-day window — rather than 1 or nothing.

Reading seven rows as seven days. They coincide only when every calendar day has a row. Good Friday fell on 3 April 2026, and the week after it looks like this:

pd.DataFrame({'seven_rows': vix.rolling(7).mean().round(2),
              'seven_days': vix.rolling('7D').mean().round(2),
              'rows_in_7D': vix.rolling('7D').count().astype(int)}
            ).loc['2026-04-01':'2026-04-09']
                  seven_rows  seven_days  rows_in_7D
observation_date
2026-04-01             27.31       27.78           5
2026-04-02             26.87       27.06           5
2026-04-06             26.70       24.46           4
2026-04-07             26.47       24.59           4
2026-04-08             25.04       23.72           4
2026-04-09             23.45       22.62           4

Two points apart on the same day. A seven-row window on this series spans eight to eleven calendar days, and it is still reaching back into March's spike; '7D' holds four or five rows, because a calendar week contains five trading days. Neither is wrong. Decide which one your sentence means.

Rolling a frame that is not in time order. This is the one that costs money. .rolling() never looks at your index to decide what "previous" means — it walks the rows in the order they sit in memory. Sort the same series descending and rolling(5).mean() still runs, still returns a full-looking column, and now reports the mean of the next five days at every date. On 14 August it says 15.24 rather than 14.83. No error, no warning, and a forecast where you wanted a trailing average. Call sort_index before you call rolling, every time.

center=True is the sanctioned way to make a window see the future, and it is worth knowing what it actually does. The trailing 21-row mean peaks at 25.99 on 6 April; the centered one peaks at 25.99 on 20 March. Same rows, same number, label moved ten rows back to sit in the middle of its own window. Use it for describing history, never for anything that has to be computable in real time.

Where it shows up in Bamboo Weekly

Bamboo Weekly #50: Red Sea shipping is the gentlest introduction, and free to read: .rolling(3).agg(['min', 'max']) over daily transits through the Bab el-Mandeb and Suez. The post stops to explain the two NaN rows at the top rather than stepping over them.

Bamboo Weekly #115: Sahm rule builds a real recession indicator out of rolling, and shows why rolling alone is not enough: the rule wants the three months before each month, so the answer is s.rolling(window=3).mean().shift(1), and the 12-month floor is s.rolling(window=12).min().shift(1).

Bamboo Weekly #98: Retail sales swaps .rolling(window=3).mean() in where an earlier query used resample('1QE') — the same three months of UK retail data, quarter buckets versus a sliding window, and the two answers differ.

Practice it

Work through a rolling exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/rolling/

Go deeper

expanding is the same machinery with a window that never lets go of the start, and shift is what you reach for when the window is aimed at the wrong months. For fixed calendar buckets rather than a sliding window, that is resample; for the change between two rows rather than a summary of many, diff and pct_change, which share this page's ordering trap exactly.

More Pandas videos on Python and Pandas with Reuven Lerner.

Bamboo Weekly is the practice. If you want the structured version — full courses with downloadable Jupyter notebooks, plus live Pandas office hours when you get stuck — that is LernerPython+Data. A paid Bamboo Weekly subscription is included with it.

See it on real data

Below are the 3 Bamboo Weekly exercises that use rolling on real-world data — try each one, then study the worked solution.

Part of the Pandas Methods Index. See also practice by skill.