Take the mean of the last n values at every row with .rolling(n).mean(), and put a 5-day and a 10-day average on a price column.
A moving average is the mean of the last n values, worked out again at every row. On day 20 a 3-day average is the mean of days 18, 19 and 20. On day 21 it is the mean of days 19, 20 and 21. The window slides forward one row at a time.
.rolling(n).mean() does it in one call.
In this lesson I run a 3-day average on six closes and check one window by hand, then put a 5-day and a 10-day average on a price column from a CSV and compare them on the last row.
0 NaN
1 NaN
2 101.000000
3 102.666667
4 103.333333
5 105.666667
dtype: float64
Six values in, six values out. .rolling(3) does not shorten the Series, it lines the average up with the last day of each window.
The first two are NaN. Row 0 has one observation behind it and row 1 has two, and a 3-day window needs three. Row 2 is the first row with a full window, so it is the first row with a number.
101.0 is what row 2 printed and 102.666667 is what row 3 printed. Row 3 dropped 100 off the back and took 105 on the front. That dropping and taking is the whole of .rolling().
Step 3. A 5-day and a 10-day average on a price column
prices.csv sits next to this lesson: Date, Ticker, Close and Volume for AAPL, MSFT and NVDA over 40 business days. The prices are simulated, not downloaded.
(40, 6)
Date Close MA5 MA10
0 2026-01-02 186.66 NaN NaN
3 2026-01-05 186.38 NaN NaN
6 2026-01-06 188.51 NaN NaN
9 2026-01-07 187.24 NaN NaN
12 2026-01-08 189.27 187.612 NaN
15 2026-01-09 185.32 187.344 NaN
18 2026-01-12 185.46 187.160 NaN
21 2026-01-13 187.01 186.860 NaN
24 2026-01-14 185.68 186.548 NaN
27 2026-01-15 187.83 186.260 186.936
30 2026-01-16 188.11 186.818 187.081
The row labels run 0, 3, 6, 9 because they are the labels these rows carried in the full table, where every date holds three tickers. .rolling() works down the rows in the order they sit, so the labels do not matter here.
MA5 starts on the fifth AAPL row and MA10 on the tenth. A longer window costs more NaN at the front:
Date Close MA5 MA10
111 2026-02-24 172.64 172.954 175.886
114 2026-02-25 171.42 172.502 174.767
117 2026-02-26 169.42 171.880 173.449
On 2026-02-26 the close is 169.42, the 5-day average is 171.88 and the 10-day average is 173.45. The close sits below both, and the 5-day sits below the 10-day, because the price has been falling and the short window holds only recent, lower closes while the long window still carries older, higher ones.
Both numbers are plain means of the last few closes:
The standard deviation of the last five closes is 1.7394 and the highest of them is 174.17. .min(), .sum() and .median() follow .rolling() the same way.
Your turn
Take the MSFT rows out of prices.csv, add a 5-day moving average, and print the last three rows. How many rows come out NaN?
Row 0 is now the mean of one close and row 1 the mean of two, so nothing is NaN. Those two rows are not 3-day averages, though, and if you compare them with the rest you are comparing a mean of one against a mean of three. The default, min_periods=n, keeps them empty instead.