A Diary Full of Numbers, and a Question Nobody Could Answer
Ravi's mother runs a small stall outside their housing society gate that sells umbrellas and raincoats every monsoon. She keeps a little diary: one line per day, one number — how many umbrellas she sold. Two weeks into August 2026, she flips back through the pages and asks Ravi a question that sounds simple but isn't: "Business feels like it's growing. But is it really growing, or was last Sunday just a lucky day because everyone was out shopping in the rain?" She has fourteen numbers. She needs to turn them into an answer.
This is the exact problem that time series analysis exists to solve. A spreadsheet of exam marks for forty students doesn't care what order the rows are in — Aditi's mark and Rohan's mark are independent of each other, and you could shuffle the whole sheet without losing any information. But Ravi's mother's diary is different. Each day's sales number is connected to the days around it: today depends a little on yesterday, and this Saturday depends a lot on the fact that it's a Saturday. Shuffle her diary and you destroy the very thing she is trying to understand. Any dataset where the row order encodes real information — because it's ordered by time — is a time series, and it needs its own set of tools. This chapter builds those tools in Python, from arithmetic first principles up to a working next-day forecast, using exactly the kind of data Ravi's mother could have collected herself.
What Exactly Is a Time Series?
Formally, a time series is a sequence of data points, each one tagged with a timestamp, recorded in chronological order, usually at regular intervals — every day, every hour, every quarter. The two things that make it a time series rather than just "a list of numbers" are: (1) the timestamps matter and are stored alongside the values, and (2) the order is not arbitrary — it is literally the order in which events happened in the real world. Stock prices recorded every minute, a weather station's daily rainfall, a school's monthly attendance percentage, the number of UPI transactions processed per day by a payments app — all of these are time series. What makes them interesting to analyse is that nearby points tend to resemble each other (today's temperature is close to yesterday's), and there are often repeating patterns tied to the calendar (colder in December, hotter in May) layered on top of a longer-term drift (average temperatures inching up over decades). Time series analysis is the set of techniques for separating those layers and using them to understand the past and estimate the future.
Meet the Data: Fourteen Days at the Umbrella Stall
Here is Ravi's mother's diary for the first fortnight of August 2026 — the number of umbrellas sold each day. 3 August 2026 happens to fall on a Monday, so this fortnight covers exactly two full Monday-to-Sunday weeks.
Date Day Umbrellas sold
2026-08-03 Monday 140
2026-08-04 Tuesday 150
2026-08-05 Wednesday 145
2026-08-06 Thursday 160
2026-08-07 Friday 170
2026-08-08 Saturday 210
2026-08-09 Sunday 230
2026-08-10 Monday 155
2026-08-11 Tuesday 165
2026-08-12 Wednesday 160
2026-08-13 Thursday 175
2026-08-14 Friday 185
2026-08-15 Saturday 235
2026-08-16 Sunday 255
Add these up and you get 2535 umbrellas over 14 days, for a mean of 2535 ÷ 14 ≈ 181.07 umbrellas per day. But that single average flattens away everything interesting: it can't tell Ravi's mother whether Sundays are special, whether business is trending upward, or whether the jump from Friday 170 to Saturday 210 in week one is a pattern that will repeat or a one-off. To answer those questions we need to look at the sequence itself, not just its average — and that means loading it into Python in a way that remembers the order and the dates.
Building the Time Series in pandas
The pandas library has a data type built specifically for this: a Series indexed by dates instead of plain integer positions. You generate the dates with pd.date_range() and attach the sales numbers to them.
import pandas as pd
dates = pd.date_range(start="2026-08-03", periods=14, freq="D")
sales = pd.Series(
[140, 150, 145, 160, 170, 210, 230,
155, 165, 160, 175, 185, 235, 255],
index=dates
)
print(sales["2026-08-09"]) # 230
print(sales.mean()) # 181.07142857142858
Tracing this: pd.date_range(start="2026-08-03", periods=14, freq="D") generates exactly 14 consecutive calendar dates starting 3 August 2026 and ending 16 August 2026 (freq="D" means "step forward one day each time"). That list of dates becomes the index of the Series — not just decoration, but the actual key you use to look values up. sales["2026-08-09"] pulls out Sunday's value, 230, by date rather than by position number. This is the first real advantage over a plain Python list: you can query "what happened on this date" directly, and pandas keeps every later calculation lined up against the correct day automatically.
Trend, Seasonality, and Noise: Three Layers Inside One Wiggly Line
Before computing anything, it helps to name what we're looking for. Classical time series analysis says almost any real-world series is a mixture of three components layered on top of each other:
- Trend — the slow, long-run direction. As the monsoon intensifies through August, more people carry umbrellas by habit and the stall gets more regular customers; sales drift upward across the whole fortnight even if you ignore day-to-day bumps.
- Seasonality — a pattern that repeats on a fixed calendar cycle. Here it's weekly: people go out shopping on weekends, so Saturdays and Sundays are consistently higher than weekdays, and this repeats every seven days regardless of the overall trend.
- Noise (or residual) — the leftover randomness that trend and seasonality don't explain: a sudden downpour that keeps everyone indoors, a competing stall opening nearby for one day, a WhatsApp forward that sends a burst of customers.
Ravi's mother's real question — "is business growing, or was Sunday just lucky?" — is really asking: how much of what I'm seeing is trend, how much is the normal weekend seasonality I should expect every week, and how much is one-off noise I shouldn't read too much into? The rest of this chapter builds, one tool at a time, the ability to answer exactly that.
Smoothing Away the Noise: The Moving Average, By Hand
The simplest tool for separating "the pattern" from "the noise" is the moving average: instead of looking at each day in isolation, you replace it with the average of that day and the couple of days before it. This smooths out one-day wobbles while still tracking slower changes.
A 3-day moving average at any given day is the mean of that day's value and the two days immediately before it. Let's compute it by hand for the first day it's possible to compute — 5 August, the third day in the diary, since you need at least three days of history:
MA(5 Aug) = (140 + 150 + 145) ÷ 3 = 435 ÷ 3 = 145.0
Slide the three-day window forward by one day and repeat, for 6 August:
MA(6 Aug) = (150 + 145 + 160) ÷ 3 = 455 ÷ 3 = 151.67
Notice what happened: 140 dropped out of the window, 160 entered, and the average shifted accordingly. That's the entire mechanism — a "window" of fixed size 3 that slides one day forward at a time, each step dropping the oldest value and admitting the newest one. Because it takes three days of data to produce the first average, the moving average simply does not exist for 3 August or 4 August — there's no way to compute a 3-day average with only one or two numbers, so those two days are left . This detail matters enormously once we do it in code, because Python won't silently skip those two days — it will mark them explicitly as missing.
Moving Averages in pandas: .rolling().mean()
Doing this arithmetic by hand for 14 days would be tedious and error-prone at 30 or 365 days it would be unworkable — which is exactly why pandas has a built-in method for it: .rolling(window=3).mean().
sales_ma3 = sales.rolling(window=3).mean()
print(sales_ma3)
Output:
2026-08-03 NaN
2026-08-04 NaN
2026-08-05 145.000000
2026-08-06 151.666667
2026-08-07 158.333333
2026-08-08 180.000000
2026-08-09 203.333333
2026-08-10 198.333333
2026-08-11 183.333333
2026-08-12 160.000000
2026-08-13 166.666667
2026-08-14 173.333333
2026-08-15 198.333333
2026-08-16 225.000000
Freq: D, dtype: float64
NaN stands for "Not a Number" — pandas's way of marking a value that genuinely cannot be computed, exactly matching our by-hand reasoning: the first two days have no three-day history behind them. From 5 August onward, every number matches what we would get doing the arithmetic by hand — 145.0 on the 5th, 151.666667 on the 6th, and so on, ending at 225.0 on 16 August. This kind of rolling window, which only ever looks at the current day and days before it, is called a trailing window — the default behaviour of .rolling() in pandas.
Seeing It: Raw Data Versus the Smoothed Trend
Numbers in a column are useful, but a picture makes the relationship between the jagged raw line and the smoother moving-average line immediate. The chart below plots both series side by side, with the two weekends shaded.
Read the first shaded weekend, Saturday 8 and Sunday 9 August. The raw (red) line jumps sharply there — 210, then 230, up from Friday's 170 — while the blue moving-average line lags behind, reaching only 180.0 on the Saturday and 203.33 on the Sunday, because it is still averaging in Thursday's and Wednesday's smaller numbers. Then watch what happens the very next day, Monday 10 August: raw sales crash back down to 155, a one-day drop of 75 units. The moving average barely reacts, easing only from 203.33 to 198.33, because Saturday's and Sunday's big numbers are still sitting inside its three-day window. This is the moving average's defining trade-off: it trades responsiveness for stability. It never overreacts to a single unusual day, but it always trails a step or two behind sudden jumps or crashes.
Now look at the second shaded weekend, Saturday 15 and Sunday 16 August — the far right of the chart. Both lines climb together here: raw sales hit 235 then 255 (the two highest raw numbers in the entire fortnight), and the moving average climbs too, from 198.33 to 225.0, its highest point in the whole chart. Comparing this to the first weekend's peak moving-average value of 203.33, the second weekend's 225.0 is clearly higher — a real sign that, weekend-for-weekend, business grew across the fortnight, not just that one particular Sunday got lucky. One more detail worth noticing: on 12 August the moving average (160.0) lands exactly on that day's raw value (160) — a coincidence of these particular numbers, and a useful reminder that a moving average isn't always below or above the raw line; it depends entirely on whether the recent days were higher or lower than today.
Removing the Trend: Differencing with .diff()
Moving averages smooth a series; differencing does something almost the opposite — it strips out the level of the series entirely and keeps only the day-to-day change. Series.diff() subtracts each value from the value that came immediately before it.
print(sales.diff())
2026-08-03 NaN
2026-08-04 10.0
2026-08-05 -5.0
2026-08-06 15.0
2026-08-07 10.0
2026-08-08 40.0
2026-08-09 20.0
2026-08-10 -75.0
2026-08-11 10.0
2026-08-12 -5.0
2026-08-13 15.0
2026-08-14 10.0
2026-08-15 50.0
2026-08-16 20.0
Freq: D, dtype: float64
3 August has no earlier day to subtract from, so it's NaN, exactly like the first entries of the rolling average. Every other value is simply today minus yesterday: 4 August is 150 − 140 = 10.0, 8 August (the Friday-to-Saturday jump) is 210 − 170 = 40.0, and 10 August (the weekend-to-Monday crash) is 155 − 230 = −75.0. Differencing is useful precisely because it throws away the trend and the absolute level and leaves only the pattern of ups and downs — which makes the weekly rhythm jump out visually: big positive jumps around Friday-to-Saturday and Saturday-to-Sunday, then one large negative drop every Sunday-to-Monday. That rhythm is the seasonality hiding inside the raw numbers, and differencing is one way to expose it.
Isolating the Weekly Pattern: .shift() and Week-over-Week Comparison
Differencing by one day mixes trend and weekly seasonality together — a Friday-to-Saturday jump reflects both "the weekend effect" and "the overall upward drift." To separate them, compare each day only to the same weekday exactly one week earlier. Pandas does this with .shift(7), which moves every value forward by 7 positions in the index without changing the dates — so on any given date, sales.shift(7) reports what sales were exactly 7 days earlier.
week_over_week = sales.shift(7)
weekly_change = (sales - week_over_week).dropna()
print(weekly_change)
2026-08-10 15.0
2026-08-11 15.0
2026-08-12 15.0
2026-08-13 15.0
2026-08-14 15.0
2026-08-15 25.0
2026-08-16 25.0
Freq: D, dtype: float64
The first seven days (3-9 August) have no "same weekday, one week earlier" to compare against, so .dropna() removes those NaN rows and leaves only the seven genuine weekday-to-weekday comparisons. Trace a couple by hand to check the code: Monday 10 August (155) minus Monday 3 August (140) is 15; Saturday 15 August (235) minus Saturday 8 August (210) is 25. Every weekday from Monday through Friday improved by exactly 15 units week-over-week, while both weekend days improved by 25 — meaning the underlying weekly trend is real and roughly steady, and the weekend days happen to be growing a little faster than the weekdays. Averaging all seven weekday-matched comparisons gives a single trend estimate:
avg_weekly_trend = weekly_change.mean()
print(avg_weekly_trend) # 17.857142857142858
(15 + 15 + 15 + 15 + 15 + 25 + 25) ÷ 7 = 125 ÷ 7 ≈ 17.86. That single number, 17.86 umbrellas, is Ravi's mother's answer, stated precisely for the first time: on average, each day of the week is selling about 18 more umbrellas than the same day sold exactly one week before. That is the trend, cleanly separated from the weekly seasonality that was confusing her raw diary.
A Naive but Principled Forecast
With a weekly seasonal pattern and an average weekly trend in hand, a reasonable first forecast for the next Monday, 17 August 2026, is: take the value from the most recent matching weekday (Monday 10 August, 155) and add the average weekly trend.
last_monday = sales["2026-08-10"]
forecast_aug17 = last_monday + avg_weekly_trend
print(forecast_aug17) # 172.85714285714286
155 + 17.86 ≈ 172.86, so the forecast is roughly 173 umbrellas for Monday 17 August. This method is called seasonal naive forecasting with a trend adjustment — "naive" because it doesn't use any sophisticated statistics, just "repeat the same day last week, then nudge it by the trend we measured." It's a legitimate starting point precisely because it respects both components we identified: it anchors to the correct point in the weekly cycle (a Monday, not a Saturday) rather than just extrapolating the raw sequence, and it adjusts for the drift we measured with .shift(7) rather than assuming next week looks identical to this week.
A Common Misconception: "The Moving Average Predicts the Future"
Because the blue moving-average line in the chart above looks smooth and confident, many students assume it must be doing some kind of forecasting — after all, it looks like it "knows" where the data is heading. It does not. .rolling(window=3).mean() only ever looks backward: today's three-day average is built strictly out of today, yesterday, and the day before. It has no access to tomorrow's number and cannot anticipate it. This is exactly why the moving-average line consistently lags one or two days behind sudden jumps and crashes in the raw data — it needs those days to actually happen and enter the window before it can react to them. A moving average is a smoothing tool for understanding what already happened, not a forecasting tool for what hasn't happened yet; producing an actual forecast, as in the previous section, requires a separate, explicit step.
A second, related trap: because a time series looks like an ordinary pandas table with rows and columns, it's tempting to treat it exactly like one — for instance, sorting it by sales value instead of by date "to see the best days first." Do this and every one of the tools in this chapter breaks silently: .rolling() would average together days that were never actually adjacent in time, .diff() would compute the difference between two unrelated days, and the weekly pattern would vanish completely, because "one week apart" no longer means anything once the rows are reordered by value. In a time series, the row order is not incidental bookkeeping — it is data. Keep the date index sorted chronologically, always.
Practice: Test Yourself
- Suppose 17 August sells 165 umbrellas. Using the original 14-day series plus this new value, compute the 3-day moving average for 17 August by hand.
- Using that same new value, compute
diff()for 17 August: what is 17 August minus 16 August? Does the size of this number look more like ordinary daily noise, or like a repeat of a "weekend crash" pattern (even though 17 August is a Monday)? - If
sales.shift(7)is evaluated at the date 12 August, which date's original value does it return? Explain why using the definition of.shift(), not by re-reading the table. - A classmate claims: "Since the moving average was 225.0 on 16 August and the raw value was 255.0, the moving average is 'wrong' by 30 units." Explain, using what you now know about trailing windows, why this framing misunderstands what a moving average is for.
- Explain in your own words the difference between trend, seasonality, and noise, using one day from the umbrella-stall data as an example of each.
- Why would sorting the 14-day sales Series by value (highest sales first) instead of by date make
.rolling(),.diff(), and.shift(7)all produce meaningless results?
Summary
- A time series is data indexed by time, where the order of rows carries real information — unlike an ordinary table of independent rows.
- Any time series can be thought of as three layers: trend (long-run direction), seasonality (a pattern repeating on a fixed calendar cycle), and noise (unexplained randomness).
pd.date_range()pluspd.Series(..., index=dates)builds a pandas time series with a searchableDatetimeIndex..rolling(window=n).mean()computes a trailing moving average — it smooths noise but always lags behind sudden real changes, and the firstn-1entries areNaNbecause there isn't enough history yet..diff()computes day-over-day change, stripping out the series' absolute level and exposing short-term ups and downs..shift(7)lets you compare each day to the same weekday exactly one week earlier, isolating the weekly trend from weekly seasonality.- A simple seasonal forecast combines both: take the value from the same point in the last cycle, and adjust it by the average trend measured across that cycle.
- A moving average only looks backward — it smooths the past; it does not predict the future.
Think About It
Think about this: How would you explain time series analysis with python to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.