Time Series Foundations
By the end: For one airline series you name its trend, every seasonal period, the events and breaks you can see, its data type and the stationarity checks you would run, and you write the six workflow steps with one concrete entry per step.
At a glance
Before modelling, name every part of the series, check whether it behaves the same throughout, and say what kind of data it is.
Order matters
Neighbouring values resemble each other, so a series is never shuffled.
Parts
Trend, one or more seasons, events, breaks and noise; daily sales or load data have two periods.
Stationarity
Constant mean, variance and autocorrelation: sniff with rolling statistics, then test.
Breaks
A dated change of process; averaging across it blends two worlds.
Data type first
Gaps, panels, hierarchies, zeros and counts change the model, metric and interval.
Recall
Without looking back: on the Day 2 framing card, what are the cutoff and the horizon of the fare-class decision, and why can a missing date on a flight series not be read as zero bookings?
Concept
A time series is a sequence in which order carries information: neighbouring values resemble each other, so shuffling the rows destroys what you want to learn. Read every series as parts that add (or multiply): trend, one or more seasons, events, breaks and noise. In a short window a break, a trend and a long cycle look alike.
Predict first
With a level break of -30 switched on and a 12-step rolling window, what does the rolling mean do at the break?
Text description of the figure
A synthetic series built from trend, seasonal amplitude, period, noise and an optional level break, with the dashed underlying level. Below it, a trailing rolling mean and a trailing rolling variance over an adjustable window, and a readout that flags the mean or variance as not constant.
Trend
A long-run direction, not necessarily straight.
Seasonality
A fixed, known period tied to the calendar: 7 and about 365.25 days for daily data.
Cycle
Rises and falls with no fixed period, such as business cycles.
Calendar events
Holidays and fairs. Moving holidays are known-in-advance regressors, not a period.
Noise
What is left once the other parts are removed.
Break
A change in the process itself: a competitor's move, a price-rule change, a system migration.
The step at month 54 is a single jump in the data, but the moving average spreads it over the 12 months around it (shaded). The remainder there has a standard deviation of 5.0, against 2.1 elsewhere: the break leaks into both layers.
Synthetic monthly series, seeded: trend 0.6 per month, a 12-month season, a level drop of 18 at month 54 and noise (sd 2.2). Decomposed with the course's classical-decomposition code; the first and last six months have no centred average.
Trend, season and remainder of a seeded monthly series. The step cannot live in a smooth trend, so it is smeared over one period and leaks into the remainder.
Keeps falling: model a trend, difference or detrend.
Flat, one step, flat: date it, step regressor or post-break window.
Turns back up: no fixed period, so neither trend nor season.
Three synthetic monthly series (seeded, noise sd 2.5, no season). Top: a 36-month window with its 12-month trailing mean. Bottom: the full 144 months.
In a three-year window the rolling mean falls by a similar amount for a trend, a break and a long cycle. The longer context, or a dated comparison, tells them apart.
Plot the raw series
Components, gaps and dated changes, by eye.
Rolling mean and variance
Window = the seasonal period. A drifting level or spread is the sniff, not the verdict.
Remove the season
Seasonal difference or STL, so the tests see the remainder.
KPSS and ADF
Opposite null hypotheses: read them as a pair (matrix below).
Backtest the choice
The final judge of any transformation (Day 25).
Both tests assume no deterministic season and have little power on short series, so they come after the plot and the season removal, never instead of them.
Both say non-stationary
Difference or detrend, then check again.
Conflict
Often a break, a trend or a season the tests were not built for. Plot and date it.
Too little evidence
Short or noisy series: low power. Rely on plots and the backtest.
Both say stationary
Proceed with this transformation; the backtest still decides.
The two tests ask opposite questions. Agreement is informative; disagreement is a prompt to look for what neither test models.
Output of today's query on the synthetic database. Same months remove the season; the comparison group removes the network trend; the gap moves by about 12 points.
- Parts: trend, seasonality with a fixed period (daily sales, load or traffic data carry 7 and about 365.25 days), cycles, moving calendar events, noise, breaks.
- Stationarity: mean, variance and autocorrelation do not depend on when you look. Sniff with rolling statistics, then test with KPSS and ADF.
- Structural break: a dated change of process. Date it, find the cause, model it explicitly.
- Data type and the forecasting workflow decide the next six steps. Details are in the pictures and the optional depth below.
Assumptions
The timestamps are regular, or the missing ones are made explicit; lags and seasonal periods are meaningless on an uneven or gapped index.
A fixed period is a calendar decision; the weekly period is exact, the annual one is about 365.25 days, and moving holidays need their own variables.
The process in the forecast window is related to the one in the history; a break you did not date violates this silently.
Application and workplace decision
Run the query on the airline sample database. Before you run it, predict the sign of the March-December change on the three stepped routes.
- Groups: the three routes where competitors added 25% seats on 2024-03-01 (IST-ADB, IST-AYT, IST-FRA) and all other routes.
- Measure: average final bookings per departure in the same calendar months of 2023 and 2024.
- Equal months remove the season; the comparison group removes the network-wide trend.
SQL (airline sample database)
WITH f AS (
SELECT CASE WHEN route IN ('IST-ADB', 'IST-AYT', 'IST-FRA')
THEN 'competitor +25% seats' ELSE 'other routes' END AS route_group,
CASE WHEN substr(date, 6, 2) IN ('01', '02') THEN 'Jan-Feb' ELSE 'Mar-Dec' END AS span,
substr(date, 1, 4) AS yr,
bookings_final
FROM flight_day
WHERE substr(date, 1, 4) IN ('2023', '2024')
)
SELECT route_group, span,
ROUND(AVG(CASE WHEN yr = '2023' THEN bookings_final END), 1) AS avg_2023,
ROUND(AVG(CASE WHEN yr = '2024' THEN bookings_final END), 1) AS avg_2024,
ROUND(100.0 * (AVG(CASE WHEN yr = '2024' THEN bookings_final END)
/ AVG(CASE WHEN yr = '2023' THEN bookings_final END) - 1), 1) AS yoy_pct
FROM f
GROUP BY route_group, span
ORDER BY route_group, span;
Mini-assessment
Technical depth
The probability model: white noise and the random walkWhat object a forecast estimates, stationarity defined precisely, and the random walk's variance derived and then measured.
- Process {Y_t}one random variable per time t
- Observed y_1..y_Tthe one realization we see
- Stationary?stable moments
- yes: to Forecast of Y_T+h
no - Transform or model the changedifference, detrend, date the break (Day 22)
- Forecast of Y_T+hE[Y_T+h | y_1..y_T] or a quantile
One path teaches you about T+h only if what you estimate from it (mean, variance, autocorrelation) does not depend on when you look. Strict stationarity, a shift-invariant joint distribution, is stronger; the linear models of Days 22-23 and the ADF and KPSS tests need only the weak form.
- the same mean at every time t
- the same variance at every time t
- autocovariance depends only on the lag k
Trend breaks the first condition, growing swings the second, and a fixed seasonal pattern makes the mean depend on the date. Strict stationarity (a shift-invariant joint distribution) is stronger; models use this weak form.
- white noise: mean 0, the same variance sigma squared, zero correlation across time
- unrolling the recursion telescopes every shock into the level
- variances of uncorrelated shocks add, so t shocks give t sigma squared
- the standard deviation: uncertainty grows with the square root of time
The variance depends on t, so no random walk is stationary; its first difference is the white noise itself. Every shock stays in the level for ever, which is why naive is the right baseline for it.
| Step t | Shock | Level y_t | Var(y_t) = t sigma^2 | sd = sigma sqrt(t) |
|---|---|---|---|---|
| 1 | +2 | 2 | 4 | 2.0 |
| 2 | -1 | 1 | 8 | 2.83 |
| 3 | +3 | 4 | 12 | 3.46 |
| 4 | -2 | 2 | 16 | 4.0 |
One illustrative path (y_0 = 0); the variance column is over all possible paths. 200,000 simulated walks with sigma = 2 (numpy default_rng(0)) give variances 4.00, 8.01, 12.05, 16.02 at steps 1-4: the formula, measured.
24 of 400 simulated paths. Each step adds an uncorrelated shock, so spread accumulates: the dashed envelope is ±2σ√t, growing with the square root of time, never settling.
Variance of y_t across the 400 paths at each step (dots) against the derived line t·σ². The level is unforecastable far ahead, and no fixed mean or variance exists: the walk is not stationary.
Computed: 400 seeded random walks y_t = y_t-1 + ε_t with ε ~ N(0, 1); the left panel draws 24 of them, the right panel the variance over all 400 at each step against the derived t·σ² line. Synthetic data.
Simulated random-walk paths widen like the square root of t (left); the variance across paths at each step lies on the derived straight line (right).
| Criterion | White noisey_t = epsilon_t | Random walky_t = y_t-1 + epsilon_t |
|---|---|---|
| Stationary? | Yes: same mean, variance and (zero) autocorrelation | No: Var(y_t) = t sigma^2 grows with t |
| Best point forecast | The mean, at every horizon: no skill beyond it | The last value (naive), at every horizon |
| Interval at horizon h | Constant width | Widens like sigma sqrt(h) |
| First difference gives | Over-differencing: lag-1 correlation of -0.5 appears (Day 22) | The white noise back: the logic of the d in ARIMA (Days 22-23) |
Opposite corners. White noise is stationary and unforecastable beyond its mean; the random walk is non-stationary, yet its best forecast is trivial. Stock prices and exchange rates are the classic approximate random walks. The -0.5 is exact: the difference of white noise has covariance -sigma^2 over variance 2 sigma^2.
Components, periods and data typesWhich periods a series can carry, how additive and multiplicative structures differ, and the six data types that change the model.
| Criterion | Additive | Multiplicative |
|---|---|---|
| Seasonal swing | Constant size at every level | Grows with the level |
| Structure | y = trend + season + remainder | y = trend x season x remainder |
| Rolling variance | Roughly flat | Grows with the trend |
| Treatment | Decompose as it is | Take logs: a product becomes a sum (Day 22) |
| Data frequency | Period(s) m | Typical series |
|---|---|---|
| Quarterly | 4 | Quarterly revenue |
| Monthly | 12 | Monthly bookings |
| Weekly | about 52.18 (365.25 / 7) | Weekly store sales |
| Daily | 7 and about 365.25 | Daily bookings, sales, web traffic |
| Hourly | 24, 168 and about 8,766 | Electricity load, call-centre arrivals |
Daily and hourly data carry several periods at once. Moving holidays (religious holidays that shift against the Gregorian calendar, Easter retail peaks) are not a period at all: they are regressors known in advance.
MSTL
One STL seasonal layer per period (7 and 365), then the trend. statsforecast ships it.
Fourier terms + ARIMA errors
Sine and cosine pairs per period as regressors; ARIMA models the rest (dynamic harmonic regression).
TBATS
Exponential smoothing with trigonometric seasonal terms. statsforecast ships it.
Calendar features
ML models read weekday, day of year and holiday windows as columns (Day 24).
- Which data type?decide before the model
- The time indexis it a regular grid?
- irregularGapped timestampsreindex; decide zero vs missing (Day 6)
- Many serieshow are they related?
- relatedPanellags per entity, shared fit (Day 24)
- nestedHierarchicallevels must add up (Day 26)
- The valueszeros, integers, bounds
- mostly 0IntermittentCroston; no MAPE (Days 16, 26)
- small integersCountscount intervals: Poisson, neg. binomial
- boundedPositive or boundedlog or logit keeps it in range (Day 22)
- The time indexis it a regular grid?
A day with no sales is a measured zero; a day the logger was down is missing. Examples: SKUs per store or meters per feeder (panel), store, region and chain totals (hierarchical), spare-parts demand (intermittent), daily insurance claims or defects per shift (counts).
Decomposition: classical, STL and MSTLWhat each method can represent, its library defaults, how strong the season is, and why a break ends up in the wrong layer.
| Criterion | Classical | STL | MSTL |
|---|---|---|---|
| Trend | Centred moving average of one period: smears steps over m points | Loess curve; default trend window 23 for monthly data | STL trend after removing every season |
| Seasonal shape | Fixed: the same pattern every year | May evolve; the seasonal window (default 7) sets how fast | One seasonal layer per period, such as 7 and 365 |
| Outliers and breaks | Leak into trend, season and remainder | robust=True down-weights them; the default is False | Robust fitting passed through stl_kwargs |
| Ends of the series | No trend for the first and last m/2 points | Estimated to the ends, least stable there | As STL |
| statsmodels call (0.15) | seasonal_decompose(y, period=m) | STL(y, period=m, seasonal=7, robust=False) | MSTL(y, periods=(7, 365)) |
Defaults read from the installed statsmodels 0.15.0. Classical over-smooths fast changes and repeats one seasonal pattern every year (FPP3, 3.4); STL lets the season evolve and resists outliers when robust (FPP3, 3.6). All three are additive: take logs first when the season scales with the level.
- the seasonal layer of the decomposition
- the remainder: what trend and season leave over
- near 0: no usable season; near 1: the season dominates the remainder
Day 22's lab computes it. Then read the remainder, not the trend: structure left in R_t (runs of one sign, a spike pair at a date) is where breaks and events show up.
Stationarity, defined and testedWhat ADF and KPSS returned on five constructed series, read against the definition in the first chapter.
| Series | ADF p | KPSS p | Reading |
|---|---|---|---|
| Constant level + weekly season | 0.000 | 0.100 or more | Stationary by both tests |
| Linear trend + weekly season | 0.908 | 0.010 or less | Non-stationary by both |
| Weekly + annual season | 0.511 | 0.100 or more | Neither rejects, so the verdicts disagree: the annual season fools ADF |
| Level break at day 700 | 0.837 | 0.010 or less | Non-stationary, but the cause is a break, not a unit root |
| Random walk | 0.998 | 0.010 or less | Unit root: difference it |
Synthetic, three years of daily data. ADF tests the null of a unit root, KPSS the null of stationarity: opposite questions. Neither models a deterministic season, which the annual-season row proves, so remove the season first. statsmodels reports KPSS p-values only inside its table range: 0.100 means at least 0.10, 0.010 at most 0.01.
Inside the two tests, and the knobs that move themadvancedThe regressions behind ADF and KPSS, their statsmodels defaults, and why a break pushes them apart.
- pull back toward the mean; 0 means a unit root (shocks never fade)
- deterministic terms: regression='c' keeps alpha, 'ct' adds the trend
- lagged differences that whiten the errors; autolag='AIC' picks p up to ceil(12 (T/100)^(1/4))
- assumed white noise; a season or break left in it distorts the test
The statistic is the t-ratio of gamma, compared with Dickey-Fuller critical values, not the normal table. A level shift inside the sample looks like a shock that never fades, so ADF loses power to reject.
- a random-walk level; under the null its variance is zero, so the level is constant
- a deterministic trend, included with regression='ct' (default 'c': none)
- stationary noise; its long-run variance uses a data-dependent lag (nlags='auto')
A level shift looks like a random-walk step to KPSS, which is why a break makes it reject.
| Knob | Default | What it trades |
|---|---|---|
| adfuller regression | 'c' (constant) | Use 'ct' for a trending series; the wrong choice costs power or size |
| adfuller autolag / maxlag | 'AIC', ceil(12 (T/100)^(1/4)): 14 for T = 144 | More lags whiten the errors and cost power |
| kpss regression | 'c' (level stationary) | 'ct' tests stationarity around a trend |
| kpss nlags | 'auto' (data-dependent) | Too few lags over-reject on persistent but stationary series |
| kpss p-value | Interpolated, bounded to 0.01-0.10 | Outside the table you get the bound plus a warning |
| AutoARIMA d (statsforecast 2.0) | test='kpss' | Repeated KPSS tests choose d: every caveat above applies inside the search |
Break-aware unit-root tests exist (statsmodels ships zivot_andrews), but in business data a dated change log plus a backtest is usually the cheaper evidence (practitioner heuristic).
Breaks: what they change and how to treat themFour kinds of break, a decision flow, the window trade-off, three treatments compared and the loop that catches the next one.
Level
A step up or down. A step regressor handles exactly this.
Slope
Growth speeds up or stalls. Needs a trend change term or a post-break window.
Seasonal shape
Peaks move, for example a new timetable or shift plan. Needs post-break seasons or interactions.
Variance
Swings get larger. Point forecasts survive; intervals must be refitted.
- Dated cause found?log, control group
- no: to Check the data first
yes - Permanent?
- no: to Intervention dummy
yes - Full season since?12 months after
- no: to Step regressor
yes - Step vs post-break windowboth are real candidates
- to Rolling-origin backtest
- Check the data firstmigration, definition change
- Intervention dummyspike or temporary step
- Step regressorborrows the old seasonal shapeagainst naive
- Rolling-origin backtestpicks the treatment (Day 25)
Without a full post-break season, a step regressor is the only option that keeps a seasonal shape; with one, a post-break window becomes a real candidate. Either way the backtest decides, not the flow.
Best window: 30 days, exactly the post-break history. Every older day adds bias faster than it removes noise.
Best window: 60 days, reaching back across the break: the step is small next to the noise, so extra history buys precision.
Simulation, seeded: 360 days of history with noise sd 6 and a level drop 30 days before the forecast origin. Vertical axis: mean absolute error of the mean of the last L days against the true new level, averaged over 200 draws per point (separate scales).
Short windows are noisy, long ones blend two levels. The size of the break against the noise decides where the best window lies.
| Criterion | Ignore it | Step regressor | Post-break window |
|---|---|---|---|
| Assumes | One regime before and after | The break changed only the level | Only recent data describe the future |
| Keeps seasonal history | Yes, but blended | Yes | No: less than one annual cycle at first |
| Main risk | Every month after a drop is over-forecast | Misses a changed slope or season | Season poorly estimated; noisy with little data |
Compare the options on rolling origins after the break (Day 25); none is right by default.
- Change logprices, competitor entries, migrationsinform ↓
- Forecastfrom a fixed originmonitor ↓
- Error monitora run of same-sign errors after a datesuspect a break ↓
- Date and explaincontrol group, same months, data checktreat ↓
- Choose a treatmentstep, dummy or window, by backtestrecord it ↓
- ↺ back to 1 · Change log
A run of same-sign forecast errors after a date is the cheapest detector (practitioner heuristic). Every confirmed break goes back into the change log, so the next model starts with it dated.
Forecastability and common misreadingsFour conditions that set a ceiling no model can lift, and five misreadings that cost credibility in a review.
Drivers understood?
How well do you know what moves the series?
Data available?
How much relevant history exists?
Future like the past?
Will the process in the horizon resemble the history?
Does the forecast feed back?
Publishing it can change the outcome; financial returns are the classic case.
The four conditions from the textbook (FPP3, 1.1). A ceiling set by noise and weak drivers cannot be lifted by a better algorithm. In one comprehensive study, equity-premium predictors proposed in the literature performed poorly out of sample over about 30 years (Welch and Goyal, 2008).
Stationary means predictable
Stable moments say nothing about skill; white noise is stationary and unforecastable.
Difference the break away
A difference turns a step into one spike; the level change is still unmodelled.
One p-value decides
Both tests have little power on short series and are distorted by seasons and breaks.
The period is estimated
A seasonal period is a calendar fact (7, 12, about 365.25), not a fitted number.
A cycle is a season
Cycles have no fixed period; a seasonal model will repeat them at the wrong time.
The workflow for the rest of Part ISix steps, each with the day that covers it.
Define the problem
Decision, horizon, cutoff and metric (Day 2).
Gather information
Data, and which drivers are known in advance for the whole horizon.
Explore
Plot, components, breaks, gaps and data type (today).
Fit candidates
Baselines first (Day 23), then classical and ML models (Day 24).
Forecast
From a fixed origin, at the horizon the decision uses.
Evaluate
Rolling origins against the baseline (Day 25).
The textbook lists five steps and merges the last two; splitting them keeps producing a forecast and judging it apart.
Memory anchor
Name every part before you model: periods, dates, breaks - and ask first whether the series can be forecast at all.
Useful for
Triage of any new series in ten minutes - components, data type, breaks and forecastability before a model is chosen.
Writing the exploration step of a forecasting project, and deciding which features to build (calendar, event, step).
Explaining to stakeholders why history before a structural change may not be a valid basis for a forecast.
Not suitable for
Choosing between models - this lesson names what is in the series; baselines and ETS/ARIMA come on Day 23 and validation on Day 25.
Causal statements about a break - a dated level shift is descriptive; its cause needs a design (Days 5 and 28).
Traps
Reading a break as a trend
A rolling mean or a fitted line over a window that contains a level shift shows a slope. The model extrapolates the slope, and each new month confirms the error.
How to avoid it
Plot the raw series and compare the same calendar months across years and against a control group before fitting any trend term.
Letting one stationarity test decide
ADF and KPSS ask opposite questions, are not designed for seasonal data, and are weak on short series. A single p-value is read as a verdict and the series is differenced or left alone on that basis.
How to avoid it
Use plots, a rolling-statistics check, a test of each type after removing the season, and finally the backtest result of the choice (Day 22).
Senior reasoning
A senior reads the series before modelling it: components with periods and dates, gaps and events, the data type, and whether the process in the forecast window is the process in the history. They keep a list of dated changes next to the data (pricing rules, competitor entries, system migrations), treat each as a modelling decision with a backtest, and set expectations on forecastability early: a ceiling set by noise and weak drivers cannot be lifted by a better algorithm.
Interview question
"Sales fell 15% in April. How do you tell whether it is seasonality, trend, or a structural break?" Structure the answer.
Recall cards scheduled after completion
- Name the parts of a daily airline series, and say which of them history can and cannot extrapolate.
- Which checks would you run before differencing, and what are the six workflow steps?
Prerequisites and related concepts
- Temporal gaps and irregular timestamps · day 6
- Horizon · day 2
- Point-in-time correctness · day 2
- Trend
- Stationarity
- Structural breaks and regimes
- Time series data types
- Forecasting workflow
- Autocorrelation · day 22
- Differencing · day 22
- Forecast baselines · day 23
- Rolling-origin backtesting · day 25
- Capacity-censored demand · day 28
References
- Forecasting: Principles and Practice (3rd edition)core
Trend, seasonal and cyclic patterns (cyclic means no fixed frequency); the basic steps of a forecasting task; what makes a quantity forecastable and the warning against claiming more than is possible for exchange rates; daily data with weekly and annual patterns handled by STL with several periods or dynamic harmonic regression. (Sections 1.1, 1.6, 2.3 and 12.1)
- A Comprehensive Look at the Empirical Performance of Equity Premium Predictionoptional
Proposed equity-premium predictors performed poorly out of sample over about 30 years and looked unstable; supports the forecastability caution for financial returns. (Abstract)
- statsmodels.tsa.stattools.kpss (statsmodels documentation)optional
KPSS null hypothesis (stationarity around a constant or trend), the regression and nlags arguments, and the warning that p-values outside the interpolation table are boundary values.
- statsmodels.tsa.seasonal.STL (statsmodels documentation)optional
STL defaults used in the decomposition comparison (seasonal=7, trend window chosen from the period, robust=False).
- STL: A Seasonal-Trend Decomposition Procedure Based on Loessoptional
The STL procedure - iterated loess into trend, seasonal and remainder, with robustness weights.
- MSTL: A Seasonal-Trend Decomposition Algorithm for Time Series with Multiple Seasonal Patternsoptional
MSTL, an STL extension for several seasonal periods, as used for daily data with weekly and annual patterns.
- statsmodels.tsa.stattools.adfuller (statsmodels documentation)optional
ADF null hypothesis of a unit root, the regression options (constant, trend) and autolag used in the lab.
Optional sections
Deep dive
Additive vs multiplicative. In an additive series the seasonal swing has constant size; in a multiplicative one it scales with the level, so the rolling variance grows with the trend (the widget's multiplicative toggle). A log transform turns a product into a sum, which is why Day 22 transforms before it decomposes.
Multiple seasonality. Most textbook methods take one period. For daily data with weekly and annual patterns the textbook suggests STL with several seasonal periods, or dynamic harmonic regression with Fourier terms for each period and ARIMA errors; MSTL (iterated STL over several periods) and TBATS (exponential smoothing with trigonometric seasonal terms) are other options, and statsforecast ships both. ML models (Day 24) get the same information through calendar features: weekday, day of year, holiday windows.
Data-type table.
| Type | What changes | Where it is handled |
|---|---|---|
| Irregular or gapped timestamps | Lags and seasonal periods lose meaning; reindex to a grid, decide zero vs missing | Day 6, insight on time grids |
| Panel (many series) | Share information across series; entity-safe lags | Day 24 |
| Hierarchical | Forecasts must add up across levels | Day 26 |
| Intermittent | Many zeros; MAPE undefined; Croston-type methods | Days 16 and 26 |
| Count | Small integers; Poisson or negative-binomial intervals | Day 26 |
| Positive or bounded | Log or logit transform so forecasts stay in range | Day 22 |
Stationarity tests, read carefully. ADF tests the null of a unit root; KPSS tests the null of stationarity around a constant or a trend. Both assume no deterministic seasonality, and the lab shows ADF failing to reject on a stationary-plus-annual-season series. KPSS p-values outside the interpolation table are reported as boundary values (the library says so), so read "0.10" as "at least 0.10".
Forecastability and finance. The textbook gives four conditions: how well the drivers are understood, how much data exist, how similar the future is to the past, and whether forecasts affect the outcome. Equity-premium predictors proposed in the literature performed poorly out of sample over about 30 years in one comprehensive study. Volatility and volume are often more predictable than returns - a common practitioner view, not shown by a source in this course's registry.
Role lenses. An analyst checks the grid and compares the same months across years; a data scientist writes the component list and the break treatment; an ML engineer makes sure the calendar and break variables exist at serving time for the whole horizon.
Hands-on lab: Stationarity checks on five constructed series
Run the code, then answer: (1) Which series do ADF and KPSS classify the same way, and which do they disagree on? (2) Why does the seasonal series get a high ADF p-value although it has no trend? (3) Replace the break series by one with the same break but no weekly season and compare the rolling-mean statistic. (4) Change the "last 200 days" window to 50 and to 400 and describe the trade-off in the final two lines.
import warnings
import numpy as np
from statsmodels.tsa.stattools import adfuller, kpss
warnings.filterwarnings("ignore")
rng = np.random.default_rng(21)
n = 3 * 364 # three years of daily data (a multiple of the weekly period)
t = np.arange(n)
weekly = 8 * np.sin(2 * np.pi * t / 7) + 4 * np.cos(4 * np.pi * t / 7)
annual = 20 * np.sin(2 * np.pi * (t - 80) / 365.25)
noise = rng.normal(0, 5, n)
cases = {
"constant level + weekly season": 100 + weekly + noise,
"linear trend + weekly season": 100 + 0.05 * t + weekly + noise,
"weekly + annual season": 100 + weekly + annual + noise,
"level break at day 700": 100 + weekly + noise - 25 * (t >= 700),
"random walk": 100 + np.cumsum(rng.normal(0, 3, n)),
}
print(f"{'series':32s} {'ADF p':>6s} {'KPSS p':>7s} {'roll-mean range / sd':>21s}")
for name, y in cases.items():
adf_p = adfuller(y, result_object=True).pvalue # H0: unit root (result_object: statsmodels 0.15+)
kpss_p = kpss(y, regression="c", result_object=True).pvalue # H0: stationary around a constant
roll = np.convolve(y, np.ones(28) / 28, mode="valid") # 28 days = four weekly cycles
print(f"{name:32s} {adf_p:6.3f} {kpss_p:7.3f} {np.ptp(roll) / y.std():21.2f}")
# Which period does the series remember? Autocorrelation at weekly and annual lags
y = cases["weekly + annual season"]
z = y - y.mean()
for lag in (7, 14, 182, 364):
print(f"lag {lag:3d} autocorrelation: {np.corrcoef(z[:-lag], z[lag:])[0, 1]:+.2f}")
# What a break does to a level forecast: all history vs the post-break window only
y = cases["level break at day 700"]
train, future = y[:900], y[900:]
print(f"MAE, mean of all 900 days: {np.abs(future - train.mean()).mean():.1f}")
print(f"MAE, mean of last 200 days: {np.abs(future - train[-200:].mean()).mean():.1f}")
Interview practice
A telecom analyst shows a plot of daily support tickets for three years and says "it is stationary, so we can use an ARIMA model directly". What do you ask and check?