The useful outcome
Seasonal forecasting is not just adding lag s. Multiplying lag polynomials creates cross-terms, seasonal differencing changes the forecast scale, and exogenous forecasts require a declared future driver path. The practical goal is a path that another person can reproduce from a declared cutoff—not a line whose origin, state, or future assumptions are hidden.
The question this tutorial answers is: How are non-seasonal, seasonal, and exogenous effects combined without double-counting lags or using unknown future drivers?
What you will build
You will calculate the model by hand, implement it in Python and TypeScript, inspect eight long synthetic scenarios, and learn where correct recursion ends and empirical forecast validation begins.
ARIMA provides ordinary differencing and innovation recursion. Seasonal diagnostics help choose s, P, D, and Q but do not establish them automatically.
Freeze the variant before fitting anything
Regression plus multiplicative SARIMA errors with fixed coefficients, d and D in {0,1}, known future exogenous rows, zero future innovations, and explicit polynomial cross-terms.
| Variant | Definition | Best use | Main limitation |
|---|---|---|---|
| Selected regression + SARIMA errors | fixed beta and multiplicative seasonal errors | auditable driver scenario | future x required |
| Pure SARIMA | no exogenous regressors | seasonal univariate series | cannot condition on known drivers |
| State-space SARIMAX | likelihood/Kalman representation | estimation and missing data | initialization and trend representation vary |
| Dynamic regression | distributed or lagged x effects | delayed driver response | different coefficient contract |
This matters because forecast libraries often share a model-family name while differing in constants, trend representations, signs, initialization, missing data, or optimizer behavior.
Intuition before notation
First remove the known regression effect. Then difference and forecast the remaining seasonal error process. Finally undo differencing and add the known future driver effect. The hardest part is multiplicative seasonal cross-lags and conditional future exogenous information. The forecast origin is therefore a visible data boundary, not just the point where a chart changes color.
Formula and symbols
| Symbol | Meaning | Unit or range |
|---|---|---|
| (p,d,q) | non-seasonal order | integers |
| (P,D,Q)_s | seasonal order and period | integers |
| x_t, beta | exogenous row and fixed coefficient vector | aligned units |
| Phi, Theta | seasonal AR and MA polynomials | dimensionless |
| s | observations per seasonal cycle | integer >= 2 |
The formula is a conditional point forecast. Future random innovations use zero because zero is their conditional expectation—not because the future is known to contain no shocks.
Data that the formula is allowed to see
A finalized level series, aligned exogenous rows, a complete future exogenous path, beta coefficients, non-seasonal and seasonal orders, period s, intercept, and horizon. Observations must be equally spaced, finite, ordered, and on one consistent scale. The model version and cutoff are frozen before the holdout. Revisions require a new forecast version rather than silent history replacement.
Algorithm
- Validate one-to-one alignment of y and exogenous rows plus exactly H future exogenous rows.
- Subtract x_t beta to form the regression-residual series.
- Apply ordinary and seasonal differences under d,D in {0,1}.
- Multiply AR and seasonal-AR polynomials and MA and seasonal-MA polynomials, retaining cross-lags.
- Forecast the transformed error process, invert differences recursively, and add each future x beta.
Hand calculation
The following is author-derived arithmetic over synthetic values:
{
"values": [
12,
15,
18,
21,
24,
27,
30,
33
],
"config": {
"exog": [
[
1
],
[
2
],
[
3
],
[
4
],
[
5
],
[
6
],
[
7
],
[
8
]
],
"future_exog": [
[
9
],
[
10
]
],
"beta": [
2
],
"ar": [
0.5
],
"ma": [],
"seasonal_ar": [
0.25
],
"seasonal_ma": [],
"intercept": 0,
"difference_order": 1,
"seasonal_difference_order": 0,
"period": 2,
"horizon": 2
}
}
- Residualized levels y-2x are [10,11,12,13,14,15,16,17]; first differences are all 1.
- Combined AR recursion is 0.5w[t-1]+0.25w[t-2]-0.125w[t-3].
- h=1 transformed forecast=.625; residualized level=17.625; add 2(9) => 35.625.
- h=2 transformed forecast=.4375; residualized level=18.0625; add 2(10) => 38.0625.
The expected path is [35.625, 38.0625]. Independent tests pin
these values before the larger fixtures are generated.
Calculation flow
The order matters. Revealed actuals may score the path, but they may not alter the version that was issued at the cutoff.
Guided experiment
The standalone lab contains eight scenarios and 1,440 calendar-month-end observations. It trains on 144 values and forecasts a 36-value holdout. Start at horizon one, predict what state should persist, then Step through twelve model-specific checkpoints. Change the principal parameter and watch the complete path recompute from the frozen training data.
The scenarios isolate persistence, mean reversion, linear trend, seasonal structure, a late level shift, one gross outlier, a variance-regime change, and seasonal phase/amplitude drift. They are not selected trades or provider histories.
The state an expert audits
For this model, keep the transformed seasonal-error forecast, residualized level, explicit future-driver contribution, and multiplicative cross-lags visible.
Use SARIMAX when seasonality and a genuinely available future driver path are both part of the forecasting question. Freeze the driver scenario at the origin and evaluate alternative driver scenarios separately.
Decompose every forecast into regression contribution and SARIMA-error contribution. A perfect clean seasonal path is a unit test, not evidence that phase, amplitude, or exogenous relationships remain stable.
When implementations disagree, use this order: Audit exogenous timing and column order, regression-before-differencing versus another convention, polynomial cross-lags, seasonal period, future-driver rows, and state-space initialization.
The workbench is deliberately quantitative. It connects the model's unique state to the declared benchmark rather than asking the learner to judge whether the purple line “looks right.”
Failure diagnosis
- An unknown future exogenous value cannot be silently copied forward.
- The cross-term at lag s+1 is required in an AR(1)xSAR(1) model.
- Seasonal period means observations per cycle, not a calendar label.
- Ordinary and seasonal differencing reduce usable history and change deterministic-term semantics.
- A driver scenario is conditional evidence, not an unconditional prediction.
Feeding realized future exogenous values into a historical backtest or omitting multiplicative cross-terms. Correct arithmetic answers “did we implement the selected model?” It does not answer “is this a good model for this series?”
Implementation contract
The Python reference favors readable state transitions. TypeScript implements
the same contract independently. Shared fixtures and focused tests require
matching numeric paths within 1e-9 and exact agreement on lengths, nulls,
states, and invalid-input behavior.
Estimation is intentionally outside the public function. In a real workflow, fit or select parameters using only training data, record the method and version, pass the frozen result into this recursion engine, and evaluate using rolling origins against simple benchmarks.
Practical evaluation
Use a loss function that matches the decision, report by horizon, and include baseline forecasts. Preserve the refit schedule and origin timestamps. If prediction intervals are needed, choose and validate an uncertainty model; do not manufacture intervals from point forecasts alone.
The lab's declared baseline is seasonal naïve for seasonal cases; last-value naïve otherwise. At active horizon k,
it calculates errors only for the first k revealed holdout values:
MAE = mean(abs(actual - forecast));RMSE = sqrt(mean((actual - forecast)^2));bias = mean(actual - forecast);MAE skill = 1 - model_MAE / benchmark_MAE.
Positive skill means the model's revealed-prefix MAE is lower than the frozen benchmark's. Negative skill is useful evidence: the sophisticated recursion is losing to a simple rule on this synthetic path. When benchmark MAE is zero, skill is undefined rather than infinite. One origin still cannot establish general performance; repeat the complete procedure over rolling origins before making an empirical claim.
Related methods
AutoReg introduces lag memory. ARMA adds innovation memory. ARIMA changes the modeled scale through differencing. SARIMA/SARIMAX adds seasonal polynomials and future driver scenarios. Holt-Winters maintains level, trend, and seasonal states directly. Theta combines a smoothed level with constrained drift.
Next: Holt-Winters.
Historical and evidence boundary
A historical example is not useful for this mechanism lesson. A defensible case requires licensed point-in-time observations, identity, frequency, adjustment and revision basis, cutoff, parameters frozen before inspection, benchmark, holdout, and publication rights.
This tutorial establishes definition fidelity. It does not establish stationarity, model adequacy, forecast calibration, historical association, causation, profitability, or investment suitability.
Primary references
- statsmodels.tsa.statespace.sarimax.SARIMAX — Multiplicative seasonal ARIMA orders, exogenous regression, state-space estimation, and alternative differencing representations.
- Forecasting: Principles and Practice — Seasonal ARIMA — Multiplicative non-seasonal and seasonal lag polynomials and seasonal order notation.
- statsmodels.tsa.arima.model.ARIMA — AR, MA, ARMA, ARIMA, seasonal, trend, and exogenous special cases plus stationarity and invertibility options.
- Forecasting: Principles and Practice — Evaluating point forecast accuracy — Forecast accuracy must be measured on genuine forecasts outside the training sample; MAE, RMSE, scaled errors, and benchmarks answer different evaluation questions.
- Forecasting: Principles and Practice — Time series cross-validation — Rolling forecasting origins preserve temporal order and can evaluate horizon-specific errors without using future observations.
- Forecasting: Principles and Practice — Evaluating regression and residual behavior — Residual plots and residual autocorrelation diagnose information left in the fitted model, but residual size is not a substitute for genuine forecast error.
Rendered from the canonical Mermaid sources linked by this article.
SARIMA/SARIMAX Calculation Flow
Purpose: separate validated historical state, fixed model parameters, recursive forecast state, and holdout evaluation.
Takeaway: evaluation data follows publication; it never repairs the already issued forecast.
ReferencesPrimary sources and evidence notesExpand the source trail, evidence role, and limitations behind the engineering choices.
Expand the source trail, evidence role, and limitations behind the engineering choices.
STATSMODELS_SARIMAX — statsmodels.tsa.statespace.sarimax.SARIMAX
- Organization or authors: statsmodels developers
- Source type: Maintained official library documentation
- Publication or effective date: 2025-12-05 documentation build
- Version: statsmodels 0.14.6
- URL or DOI: https://www.statsmodels.org/stable/generated/statsmodels.tsa.statespace.sarimax.SARIMAX.html
- Accessed: 2026-07-27
- Jurisdiction: Technical method; no legal jurisdiction.
- Supports: Multiplicative seasonal ARIMA orders, exogenous regression, state-space estimation, and alternative differencing representations.
- Limitations: Trend treatment and state initialization differ across APIs; this package freezes regression-plus-SARIMA-errors recursion.
- Evidence role: Definition/variant comparison only; not provider data or an empirical result for the synthetic fixtures.
- Publication boundary: Link and paraphrase are public; no source dataset or copyrighted table is reproduced.
FPP_SEASONAL — Forecasting: Principles and Practice — Seasonal ARIMA
- Organization or authors: Rob J. Hyndman and George Athanasopoulos
- Source type: Authoritative open forecasting textbook
- Publication or effective date: 2021 third edition; online version updated 2026-03-09
- Version: 3rd edition
- URL or DOI: https://otexts.com/fpp3/seasonal-arima.html
- Accessed: 2026-07-27
- Jurisdiction: Technical method; no legal jurisdiction.
- Supports: Multiplicative non-seasonal and seasonal lag polynomials and seasonal order notation.
- Limitations: The package supports d and D only in {0,1} to keep inversion auditable.
- Evidence role: Definition/variant comparison only; not provider data or an empirical result for the synthetic fixtures.
- Publication boundary: Link and paraphrase are public; no source dataset or copyrighted table is reproduced.
STATSMODELS_ARIMA — statsmodels.tsa.arima.model.ARIMA
- Organization or authors: statsmodels developers
- Source type: Maintained official library documentation
- Publication or effective date: 2025-12-05 documentation build
- Version: statsmodels 0.14.6
- URL or DOI: https://www.statsmodels.org/stable/generated/statsmodels.tsa.arima.model.ARIMA.html
- Accessed: 2026-07-27
- Jurisdiction: Technical method; no legal jurisdiction.
- Supports: AR, MA, ARMA, ARIMA, seasonal, trend, and exogenous special cases plus stationarity and invertibility options.
- Limitations: Its state-space likelihood, initialization, and optimizer are outside this deterministic teaching engine.
- Evidence role: Definition/variant comparison only; not provider data or an empirical result for the synthetic fixtures.
- Publication boundary: Link and paraphrase are public; no source dataset or copyrighted table is reproduced.
FPP_ACCURACY — Forecasting: Principles and Practice — Evaluating point forecast accuracy
- Organization or authors: Rob J. Hyndman and George Athanasopoulos
- Source type: Authoritative open forecasting textbook
- Publication or effective date: 2021 third edition; online version updated 2026-03-09
- Version: 3rd edition
- URL or DOI: https://otexts.com/fpp3/accuracy.html
- Accessed: 2026-07-27
- Jurisdiction: Technical method; no legal jurisdiction.
- Supports: Forecast accuracy must be measured on genuine forecasts outside the training sample; MAE, RMSE, scaled errors, and benchmarks answer different evaluation questions.
- Limitations: A single synthetic holdout is a teaching comparison, not an empirical model-selection study.
- Evidence role: Definition/variant comparison only; not provider data or an empirical result for the synthetic fixtures.
- Publication boundary: Link and paraphrase are public; no source dataset or copyrighted table is reproduced.
FPP_TSCV — Forecasting: Principles and Practice — Time series cross-validation
- Organization or authors: Rob J. Hyndman and George Athanasopoulos
- Source type: Authoritative open forecasting textbook
- Publication or effective date: 2021 third edition; online version updated 2026-03-09
- Version: 3rd edition
- URL or DOI: https://otexts.com/fpp3/tscv.html
- Accessed: 2026-07-27
- Jurisdiction: Technical method; no legal jurisdiction.
- Supports: Rolling forecasting origins preserve temporal order and can evaluate horizon-specific errors without using future observations.
- Limitations: This family demonstrates one frozen origin; production claims require many origins and a declared refit schedule.
- Evidence role: Definition/variant comparison only; not provider data or an empirical result for the synthetic fixtures.
- Publication boundary: Link and paraphrase are public; no source dataset or copyrighted table is reproduced.
FPP_RESIDUALS — Forecasting: Principles and Practice — Evaluating regression and residual behavior
- Organization or authors: Rob J. Hyndman and George Athanasopoulos
- Source type: Authoritative open forecasting textbook
- Publication or effective date: 2021 third edition; online version updated 2026-03-09
- Version: 3rd edition
- URL or DOI: https://otexts.com/fpp3/regression-evaluation.html
- Accessed: 2026-07-27
- Jurisdiction: Technical method; no legal jurisdiction.
- Supports: Residual plots and residual autocorrelation diagnose information left in the fitted model, but residual size is not a substitute for genuine forecast error.
- Limitations: Residual diagnostics do not prove unbiasedness, calibrated intervals, or out-of-sample usefulness for these fixed teaching configurations.
- Evidence role: Definition/variant comparison only; not provider data or an empirical result for the synthetic fixtures.
- Publication boundary: Link and paraphrase are public; no source dataset or copyrighted table is reproduced.
Full dependency-light reference implementations in both supported languages.
type ForecastResult = {
forecast:number[];
fitted:(number|null)[];
residuals?:number[];
state:Record<string,unknown>;
[key:string]:unknown;
};
const finite=(value:number,name:string)=>{
if(typeof value!=="number"||!Number.isFinite(value))throw new Error(`${name} must be finite`);
return value;
};
const positiveInt=(value:number,name:string,min=1)=>{
if(!Number.isInteger(value)||value<min)throw new Error(`${name} must be an integer >= ${min}`);
return value;
};
const series=(values:number[],minimum=2)=>{
if(!Array.isArray(values)||values.length<minimum)throw new Error(`values must contain at least ${minimum} observations`);
return values.map((value,index)=>finite(value,`values[${index}]`));
};
const coefficients=(values:number[],name:string,allowEmpty=true)=>{
if(!Array.isArray(values)||(!allowEmpty&&values.length===0))throw new Error(`${name} must be a coefficient list`);
return values.map((value,index)=>finite(value,`${name}[${index}]`));
};
function armaCore(values:number[],ar:number[],ma:number[],intercept:number,horizon:number):ForecastResult{
const a=coefficients(ar,"ar"),m=coefficients(ma,"ma");
if(a.length===0&&m.length===0)throw new Error("at least one AR or MA coefficient is required");
const p=a.length,q=m.length,y=series(values,Math.max(p+2,q+2,3)),c=finite(intercept,"intercept"),H=positiveInt(horizon,"horizon");
const fitted:(number|null)[]=Array(y.length).fill(null),residuals:number[]=Array(y.length).fill(0);
for(let t=p;t<y.length;t++){
let prediction=c;
for(let lag=1;lag<=p;lag++)prediction+=a[lag-1]*y[t-lag];
for(let lag=1;lag<=q;lag++)if(t-lag>=0)prediction+=m[lag-1]*residuals[t-lag];
fitted[t]=prediction;residuals[t]=y[t]-prediction;
}
const extended=[...y],errors=[...residuals],forecast:number[]=[];
for(let step=0;step<H;step++){
const t=extended.length;let prediction=c;
for(let lag=1;lag<=p;lag++)prediction+=a[lag-1]*extended[t-lag];
for(let lag=1;lag<=q;lag++)if(t-lag>=0)prediction+=m[lag-1]*errors[t-lag];
extended.push(prediction);errors.push(0);forecast.push(prediction);
}
return {forecast,fitted,residuals,state:{order:[p,q],last_values:y.slice(-Math.max(1,p)),last_residuals:residuals.slice(-Math.max(1,q)),presample_innovations:"zero",future_innovations:"zero conditional mean"}};
}
const poly=(left:number[],right:number[])=>{
const result=Array(left.length+right.length-1).fill(0);
left.forEach((a,i)=>right.forEach((b,j)=>result[i+j]+=a*b));return result;
};
const seasonalPoly=(values:number[],period:number,sign:number)=>{
const result=Array(period*values.length+1).fill(0);result[0]=1;
values.forEach((value,index)=>result[(index+1)*period]=sign*value);return result;
};
function combined(ar:number[],ma:number[],sar:number[],sma:number[],period:number){
positiveInt(period,"period",2);
const ap=poly([1,...coefficients(ar,"ar").map(v=>-v)],seasonalPoly(coefficients(sar,"seasonal_ar"),period,-1));
const mp=poly([1,...coefficients(ma,"ma")],seasonalPoly(coefficients(sma,"seasonal_ma"),period,1));
return {ar:ap.slice(1).map(v=>-v),ma:mp.slice(1)};
}
const dot=(row:number[],beta:number[])=>row.reduce((sum,value,index)=>sum+value*beta[index],0);
function validateRows(rows:number[][],width:number,name:string){
if(!Array.isArray(rows))throw new Error(`${name} must be a list`);
return rows.map((row,index)=>{
if(!Array.isArray(row)||row.length!==width)throw new Error(`${name}[${index}] must contain ${width} values`);
return row.map((value,column)=>finite(value,`${name}[${index}][${column}]`));
});
}
function seasonalDifference(values:number[],d:number,D:number,period:number){
if(![0,1].includes(d)||![0,1].includes(D))throw new Error("d and D must be 0 or 1");
const result:number[]=[],start=d+D*period;
for(let t=start;t<values.length;t++){
if(d===0&&D===0)result.push(values[t]);
else if(d===1&&D===0)result.push(values[t]-values[t-1]);
else if(d===0&&D===1)result.push(values[t]-values[t-period]);
else result.push(values[t]-values[t-1]-values[t-period]+values[t-period-1]);
}
return result;
}
function integrate(history:number[],forecast:number,d:number,D:number,period:number){
if(d===0&&D===0)return forecast;
if(d===1&&D===0)return forecast+history.at(-1)!;
if(d===0&&D===1)return forecast+history.at(-period)!;
return forecast+history.at(-1)!+history.at(-period)!-history.at(-period-1)!;
}
export function forecastSARIMAX(values:number[],exog:number[][],futureExog:number[][],beta:number[],ar:number[],ma:number[],seasonalAr:number[],seasonalMa:number[],intercept:number,differenceOrder:number,seasonalDifferenceOrder:number,period:number,horizon:number):ForecastResult{
const y=series(values,period*2+4),b=coefficients(beta,"beta"),x=validateRows(exog,b.length,"exog"),xf=validateRows(futureExog,b.length,"future_exog"),H=positiveInt(horizon,"horizon");
if(x.length!==y.length)throw new Error("exog must align one-for-one with values");
if(xf.length!==H)throw new Error("future_exog must contain exactly horizon rows");
const residualized=y.map((value,index)=>value-dot(x[index],b));
const transformed=seasonalDifference(residualized,differenceOrder,seasonalDifferenceOrder,period);
const c=combined(ar,ma,seasonalAr,seasonalMa,period),base=armaCore(transformed,c.ar,c.ma,intercept,H);
const history=[...residualized],residualizedForecast:number[]=[],forecast:number[]=[];
base.forecast.forEach((value,index)=>{const next=integrate(history,value,differenceOrder,seasonalDifferenceOrder,period);history.push(next);residualizedForecast.push(next);forecast.push(next+dot(xf[index],b));});
return {...base,forecast,transformed_forecast:base.forecast,residualized_forecast:residualizedForecast,state:{...base.state,orders:{nonseasonal:[ar.length,differenceOrder,ma.length],seasonal:[seasonalAr.length,seasonalDifferenceOrder,seasonalMa.length,period]},combined_ar:c.ar,combined_ma:c.ma,beta:b}};
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.