A time series is a sequence of measurements taken over time: daily sales, hourly temperature, number of connections per minute. What sets it apart from an ordinary table is that the order of the rows carries information. You never shuffle the rows, and you always ask yourself: what is the frequency? Are there gaps?
Build a DatetimeIndex
The first step is to convert the date column and put it in the index:
import numpy as np
import pandas as pd
rng = np.random.default_rng(0)
df = pd.DataFrame({
"date": pd.date_range("2024-01-01", periods=90, freq="D").astype(str),
"ventes": rng.integers(50, 150, size=90),
})
df["date"] = pd.to_datetime(df["date"]) # text -> datetime64
df = df.set_index("date").sort_index() # sorted time index
print(df.loc["2024-02"].head()) # the whole month of February
print(df.loc["2024-01-10":"2024-01-15"]) # a date range (bounds included)
With a DatetimeIndex, you also get useful attributes: df.index.day_of_week (0 = Monday; the old name dayofweek is deprecated since pandas 3.1), df.index.month, df.index.year.
Change frequency with resample
resample groups observations by period, a bit like a groupby on time. You must always specify the aggregation:
mensuel = df["ventes"].resample("ME").sum() # total per month end
hebdo_moy = df["ventes"].resample("W").mean() # mean per week
Choose the aggregation according to the nature of the measure: you sum volumes (sales, visits) and average levels (temperature, average stock). Since pandas 2.2, the month-end alias is "ME" ("MS" for month start); the old "M" is no longer accepted in pandas 3.
In the other direction, moving to a finer frequency (upsampling) creates gaps that must be filled, for example with .ffill() or .interpolate().
Rolling windows and shifts
A moving average smooths out noise to reveal the trend:
df["moy_7j"] = df["ventes"].rolling(window=7).mean() # NaN on the first 6 rows
df["lag_1"] = df["ventes"].shift(1) # previous day's value
df["var_j"] = df["ventes"].diff() # ventes(t) - ventes(t-1)
df["var_pct"] = df["ventes"].pct_change() # relative change
rolling(7)looks at the last 7 values, the current value included. The first rows areNaNuntil the window is complete (themin_periodsparameter relaxes this).shift(1)shifts downward: the January 2 row receives the January 1 value. This is the basic building block of the lags we will use for forecasting.diff()is equivalent toseries - series.shift(1).
Spot the gaps
A “clean” series has a regular frequency. To check:
complet = pd.date_range(df.index.min(), df.index.max(), freq="D")
manquants = complet.difference(df.index)
df = df.reindex(complet) # missing days appear as NaN
Making gaps explicit is better than ignoring them: a day with no sales (0) is not the same as a day with no data (NaN).
Key takeaways
A sorted time index, a known frequency, a suitable aggregation, and caution with windows: these four reflexes avoid most errors in the rest of the course.
Exercises
Solutions and tests stay on the server. In the student space, your code is run and checked, and the tutor gives hints before the solution.
Monthly sales
Write the function
ventes_mensuelles(df)that receives a DataFrame indexed by date (daily frequency) with aventescolumn, and returns a Series of total sales per month (month-end alias"ME").import numpy as np import pandas as pd rng = np.random.default_rng(42) idx = pd.date_range("2024-01-01", periods=90, freq="D") df = pd.DataFrame({"ventes": rng.integers(50, 150, size=90)}, index=idx) def ventes_mensuelles(df): # TODO : rééchantillonner par mois et sommer passMoving average, lag, and change
Write
ajouter_indicateurs(df)that returns a copy of the DataFrame with three new columns:moy_7j: 7-day moving average of sales (full window required);lag_1: the previous day’s sales;var_j: the difference ventes(t) - ventes(t-1).
import numpy as np import pandas as pd rng = np.random.default_rng(7) idx = pd.date_range("2024-03-01", periods=30, freq="D") df = pd.DataFrame({"ventes": rng.normal(100, 10, 30).round(1)}, index=idx) def ajouter_indicateurs(df): out = df.copy() # TODO return outResample or rolling?
You want a table with one row per week containing total sales. Which statement do you use?
- df['ventes'].rolling(7).sum()
- df['ventes'].resample('W').sum()
- df['ventes'].shift(7)
- df['ventes'].diff(7)
End-of-lesson quiz
What does the March 10 row contain after df[‘ventes’].shift(1)?
- March 11's sales
- March 9's sales
- The mean of sales up to March 10
- NaN
Why are the first 6 values of rolling(7).mean() NaN?
- Because pandas ignores the start of series
- Because the window of 7 observations is not yet complete
- Because the index is not sorted
- Because you must use resample first
To go from hourly temperatures to one value per day, which aggregation is the most appropriate?
- sum
- mean
- count
- first
The rest of the course
- Trend, Seasonality, and Stationarity18 min
- Baselines and Forecast Evaluation15 min
- Classical Models: Exponential Smoothing and ARIMA20 min
- Forecast with Machine Learning and Run a Backtest20 min