Rolling & Expanding Windows

Reviewed & published by Brayan K

A window function computes a statistic over a moving slice of nearby rows rather than the whole column at once — the engine behind moving averages, running totals, and smoothed trend lines in time-series data.

Part of the free Pandas course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

You will master .rolling() for fixed-width windows, .expanding() for running totals, min_periods for the warm-up rows, and .ewm() for recency-weighted smoothing.

What You'll Learn in This Lesson

1 .rolling(window) — The Moving Average

.rolling(window=3) creates a sliding window 3 rows wide. As it moves down the Series, it computes a statistic over each group of 3 consecutive rows. Pairing it with .mean() gives the classic 3-period moving average used to smooth noisy data.

import pandas as pd

s = pd.Series([10, 20, 30, 40, 50])

s.rolling(window=3).mean()
# 0     NaN   <- not enough rows yet
# 1     NaN
# 2    20.0   <- mean of 10,20,30
# 3    30.0   <- mean of 20,30,40
# 4    40.0   <- mean of 30,40,50
import pandas as pd

sales = pd.Series([4, 6, 8, 10, 12], name="sales")

# 3-day moving average
ma = sales.rolling(window=3).mean()
print(ma)

# ✅ Expected output:
# 0     NaN
# 1     NaN
# 2     6.0
# 3     8.0
# 4    10.0
# Name: sales, dtype: float64

2 min_periods — Filling the Warm-Up Rows

Those leading NaN values appear because, by default, a window of 3 refuses to return a number until it has seen 3 values. The min_periods argument lowers that requirement: min_periods=1 says "give me a result as soon as there is at least one value", filling the early rows with partial-window answers.

s = pd.Series([10, 20, 30, 40])

s.rolling(window=3, min_periods=1).mean()
# 0    10.0   <- mean of [10]
# 1    15.0   <- mean of [10,20]
# 2    20.0   <- mean of [10,20,30]
# 3    30.0   <- mean of [20,30,40]
import pandas as pd

s = pd.Series([10, 20, 30, 40], name="x")

# Start computing immediately with partial windows
print(s.rolling(window=3, min_periods=1).mean())

# ✅ Expected output:
# 0    10.0
# 1    15.0
# 2    20.0
# 3    30.0
# Name: x, dtype: float64

3 .expanding() and .ewm() — Running & Weighted

.expanding() is a window that never forgets: row n uses every value from the start through n. .expanding().mean() is a running average, .expanding().sum() a running total. .ewm(span=...) is the exponentially weighted mean, where recent rows count for more and old rows fade out smoothly.

s = pd.Series([10, 20, 30, 40])

s.expanding().mean()      # running average
# 10.0, 15.0, 20.0, 25.0

s.ewm(span=2).mean()      # recency-weighted smoothing
# newer values pull the average harder
import pandas as pd

s = pd.Series([10, 20, 30, 40], name="v")

# Running (cumulative) average
print(s.expanding().mean())

# Running total for comparison
print(s.expanding().sum().tolist())

# ✅ Expected output:
# 0    10.0
# 1    15.0
# 2    20.0
# 3    25.0
# Name: v, dtype: float64
# [10.0, 30.0, 60.0, 100.0]
import pandas as pd

temps = pd.Series([15, 17, 16, 20, 22, 21], name="temp")

# 🎯 YOUR TURN:
# Build a 2-day moving average of the temperature.
# Replace ___ with the window width.

ma = temps.rolling(window=___).mean()
print(ma)

# Expected output:
# 0     NaN
# 1    16.0
# 2    16.5
# 3    18.0
# 4    21.0
# 5    21.5
# Name: temp, dtype: float64

! Common Errors (And How to Fix Them)

A rolling object alone is not data — it needs an aggregation:

s.rolling(3)            # ❌ a Rolling object, prints nothing useful

✅ Fix: call .mean(), .sum(), .max(), etc.

s.rolling(3).mean()    # ✅ actual numbers

❌ Window leaks across groups

df["ma"] = df["sales"].rolling(3).mean()   # ❌ mixes customers together

✅ Fix: roll inside each group:

df.groupby("customer")["sales"].rolling(3).mean()   # ✅

🎯 Mini Challenge: Smooth a Sales Series

Add three derived columns to a daily sales table.

import pandas as pd

df = pd.DataFrame({"day": [1, 2, 3, 4, 5],
                   "sales": [10, 14, 8, 16, 12]})

df["ma3"] = df["sales"].rolling(3).mean().round(2)
df["running"] = df["sales"].expanding().sum()
df["ewm"] = df["sales"].ewm(span=2).mean().round(2)

print(df)

# ✅ Expected output:
#    day  sales    ma3  running    ewm
# 0    1     10    NaN     10.0  10.00
# 1    2     14    NaN     24.0  13.00
# 2    3      8  10.67     32.0   9.54
# 3    4     16  12.67     48.0  13.90
# 4    5     12  12.00     60.0  12.63

❓ Frequently Asked Questions

Lesson complete — you can smooth and accumulate!

You can slide a .rolling() window, tame the warm-up rows with min_periods, accumulate with .expanding(), and weight the recent past with .ewm().

Practice quiz

What does s.rolling(window=3).mean() compute?

  • A 3-period moving average
  • The overall mean
  • The cumulative sum
  • The standard deviation of all rows

Answer: A 3-period moving average. It averages each sliding window of 3 consecutive rows.

Why are the first rows of a rolling(3) mean NaN?

  • The data was unsorted
  • A full window of 3 values is not yet available
  • rolling drops them
  • The mean is undefined

Answer: A full window of 3 values is not yet available. By default a full window is required before producing a value.

Which argument fills those warm-up rows with partial-window results?

  • fillna=0
  • warmup=True
  • min_periods=1
  • partial=True

Answer: min_periods=1. min_periods=1 computes as soon as one value is available.

What does .expanding() do compared with .rolling()?

  • It uses a fixed 3-row window
  • It weights recent rows more
  • It only looks at one row
  • It grows to include every row from the start to now

Answer: It grows to include every row from the start to now. An expanding window accumulates all prior rows (cumulative).

What does s.expanding().sum() give you?

  • A running cumulative total
  • The single grand total
  • A 3-row moving sum
  • The maximum so far

Answer: A running cumulative total. expanding().sum() produces a running total down the column.

What is special about .ewm()?

  • It ignores recent values
  • It weights recent observations more, older ones decay smoothly
  • It requires a DatetimeIndex
  • It returns integers

Answer: It weights recent observations more, older ones decay smoothly. Exponentially weighted means give recent data more influence.

Why is a bare s.rolling(3) not useful on its own?

  • It raises an error
  • It deletes rows
  • It is a lazy Rolling object until you call an aggregator
  • It returns the original Series

Answer: It is a lazy Rolling object until you call an aggregator. You must call .mean(), .sum(), etc. to get numbers.

How do you keep a rolling window from leaking across groups?

  • Use min_periods=0
  • Sort the index
  • Drop NaN first
  • Use df.groupby('key')['col'].rolling(3).mean()

Answer: Use df.groupby('key')['col'].rolling(3).mean(). Grouping restarts the window within each group.

Which aggregators can you call on a rolling object?

  • Only .mean()
  • .mean(), .sum(), .max(), .std() and more
  • Only .count()
  • None — rolling has no aggregators

Answer: .mean(), .sum(), .max(), .std() and more. Rolling shares the usual aggregator methods like mean, sum, max, std.

What is the trade-off of using min_periods=1?

  • It is always slower
  • It removes the column
  • Early results use tiny windows and are noisier
  • It changes the dtype to object

Answer: Early results use tiny windows and are noisier. Filling early rows means those values come from small windows.

Continue this course

Frequently asked questions

What is the difference between rolling and expanding?

A rolling window has a fixed width — .rolling(3) always looks at the last 3 rows, so it slides along the data. An expanding window grows: .expanding() includes every row from the start up to the current one, so each result is a running (cumulative) statistic over all data seen so far.

Why are the first few rolling values NaN?

By default a rolling window needs a full window of data before it can produce a number, so a window of 3 leaves the first 2 rows as NaN. Set min_periods=1 to start computing as soon as a single value is available, which fills those early rows with partial-window results.

What does .ewm() do differently from .rolling()?

Exponentially weighted (.ewm) gives more weight to recent observations and less to older ones, with the weights decaying smoothly rather than dropping off a cliff. A rolling mean treats every value inside the window equally and ignores everything outside it. Use ewm when recent data should matter more.

Can I use window functions per group?

Yes — call df.groupby('key')['col'].rolling(3).mean() and the window restarts within each group, so values never leak across group boundaries. This is the standard way to compute per-customer or per-stock moving averages.