Python · medium
Given a pandas DataFrame, compute the 30-day moving average of a column.
Asked in Data Scientist interviews, in the Python round.
Short answer
df['ma'] = df['x'].rolling(30).mean().
Sample data
These tables are loaded into an in-browser engine on this page, so you can run your own answer against them. No account needed.
prices
| price_date | price |
|---|---|
| 2026-06-01 | 101 |
| 2026-06-02 | 103 |
| 2026-06-03 | 99 |
| 2026-06-04 | 104 |
| 2026-06-05 | 108 |
| 2026-06-06 | 105 |
| 2026-06-07 | 110 |
| 2026-06-08 | 107 |
| 2026-06-09 | 112 |
| 2026-06-10 | 109 |
| 2026-06-11 | 115 |
| 2026-06-12 | 111 |
| 2026-06-13 | 118 |
| 2026-06-14 | 114 |
How to answer it
Definition
The average of a column over the current row and the 29 before it. In pandas that is .rolling(), and the question is really about what "30" counts: rows, or days.
Syntax
df["ma_30"] = df["value"].rolling(window=30).mean() # 30 ROWS
df["ma_30"] = df["value"].rolling("30D").mean() # 30 DAYS
Example
import pandas as pd
df = df.sort_values("date").set_index("date")
# One row per day first, so a row and a day mean the same thing
daily = df["revenue"].resample("D").sum()
ma = daily.rolling("30D").mean()
print(ma.tail(3))
Output
date
2025-03-29 41832.4
2025-03-30 41905.1
2025-03-31 42017.8
Freq: D, Name: revenue, dtype: float64
Rows or days
rolling(30) counts 30 rows. If the frame has gaps, or several rows per day, that is not 30 days. rolling("30D") counts a time offset and needs a DatetimeIndex, which is why the example sets one. Resampling to a daily grain first makes the two equivalent and removes the ambiguity entirely.
Key points
- Sort by the date first.
rollingtrusts the current order and will happily average nonsense. rolling(30)is rows,rolling("30D")is days. Say which the question wants.- The first 29 values are NaN by design.
min_periods=1fills them with partial averages, which is a decision rather than a default. .rolling()includes the current row. For a trailing average that excludes today,.shift(1)first.- Grouped version:
df.groupby("user")["value"].rolling(30).mean(), then reset the index.
FAQ
- Why are my results all NaN? Fewer than 30 rows, or NaNs inside the window. Check
min_periods. - Rolling or expanding? Rolling has a fixed window;
.expanding()grows from the start. - Is this leakage in a model? Yes, if the window includes the current row and the target derives from it. Shift before using it as a feature.
The 30-second answer
Sort by date, then rolling thirty and take the mean.
The thing I would clarify before writing it is whether thirty means thirty rows or thirty days, because rolling with a number counts rows. If the data has gaps, or more than one row per day, those are different answers, so I would either resample to a daily grain first or use the offset form with "30D", which needs a DatetimeIndex.
I would also mention that the first twenty-nine values come back NaN by design, and that filling them through min_periods is a choice to make deliberately rather than to default into.
In the interview
- What they are checking: rows versus days, and the sort.
- Common mistake: not sorting first. A rolling window over unsorted dates produces numbers that look plausible and mean nothing.