The useful outcome
Holt-Winters represents forecast structure directly as evolving level, trend, and seasonal states. It is often easier to explain operationally than lag-polynomial models. 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 do level, trend, and seasonal states update after every observation and produce a multi-season forecast?
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.
Simple exponential smoothing explains a weighted level. Seasonal period and additive versus multiplicative scale must be understood before choosing this variant.
Freeze the variant before fitting anything
Additive trend and additive seasonality, fixed alpha/beta/gamma-star in [0,1], a supplied known initial state with published provenance, no damping, and deterministic recursive updates.
| Variant | Definition | Best use | Main limitation |
|---|---|---|---|
| Selected additive-additive | constant-magnitude seasonality and additive trend | stable seasonal amplitude | can forecast negative values |
| Multiplicative seasonality | seasonal factors scale the level | amplitude grows with level | requires positive compatible data |
| Damped trend | future trend contribution decays by phi | long-horizon restraint | adds another state convention |
| ETS state space | explicit error/trend/seasonal model | likelihood and intervals | broader than classical recursions |
This matters because forecast libraries often share a model-family name while differing in constants, trend representations, signs, initialization, missing data, or optimizer behavior.
Initialization and gamma-star audit
The lab publishes the two-cycle initialization information set separately from
the supplied state. Its gamma input is gamma-star in the updated-level
seasonal equation. If another system uses the prior-level-and-trend seasonal
equation, reconcile the parameterization before comparing values.
Intuition before notation
Each observation is split into a seasonally adjusted level update, a trend update, and an update to the seasonal position that just occurred. The hardest part is state timing: use the previous seasonal state for prediction before overwriting it. 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 |
|---|---|---|
| ell_t | level state | series units |
| b_t | trend state per observation | series units/step |
| s_t | additive seasonal state | series units |
| alpha,beta,gamma-star | level, trend, and selected updated-level seasonal weights | [0,1] |
| m | seasonal period | observations/cycle |
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 equally spaced series with at least two seasons, alpha/beta/gamma-star, period m, known initial level/trend/m seasonal states, initialization information set, 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 smoothing weights, period, two seasons of data, and all known initial states.
- Before updating, calculate the one-step fitted value from previous level, trend, and matching seasonal state.
- Update level using the seasonally adjusted observation.
- Update trend from the level change and seasonal state from observation minus new level.
- Forecast h steps as final level plus h times trend plus the aligned seasonal state.
Hand calculation
The following is author-derived arithmetic over synthetic values:
{
"values": [
10,
12,
11,
13
],
"config": {
"alpha": 0.5,
"beta": 0.5,
"gamma": 0.5,
"period": 2,
"horizon": 2,
"initial_level": 9,
"initial_trend": 1,
"initial_seasonals": [
-1,
1
]
}
}
- After four updates: level=12.5859375, trend=.69140625, seasonal states=[-.921875,.61328125].
- h=1: 12.5859375+.69140625-.921875=12.35546875.
- h=2: 12.5859375+2(.69140625)+.61328125=14.58203125.
The expected path is [12.35546875, 14.58203125]. 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 final level, accumulated undamped trend, aligned seasonal index, initialization provenance, and gamma-star convention visible.
Use additive Holt-Winters when seasonal amplitude is roughly constant in series units and explicit evolving states aid operations. Compare multiplicative seasonality and damped trend when amplitude scales with level or long-horizon trend is implausible.
Decompose the path into level, h-times-trend, and seasonal contribution. A phase error points to period or state alignment; explosive long-horizon movement points to undamped trend; early instability points to initialization.
When implementations disagree, use this order: Verify whether gamma multiplies y minus the updated level or y minus the prior level-and-trend forecast, then check initial-state timestamp, seasonal ordering, damping, and additive versus multiplicative form.
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
- Initialization can materially change the first several seasons.
- A wrong period rotates seasonal states onto the wrong timestamps.
- Additive seasonality is inappropriate when amplitude scales with level.
- Undamped trends can become implausible at long horizons.
- A level shift can be absorbed slowly depending on alpha and beta.
Calling smoothing weights probabilities or assuming automatic initialization is universal. 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 forecast. 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: Theta Forecast.
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
- Forecasting: Principles and Practice — Holt-Winters — Level, trend, and seasonal recursions plus additive versus multiplicative interpretation.
- Exponential smoothing examples — Holt and Holt-Winters combinations, fixed smoothing parameters, damping, and additive versus multiplicative components.
- 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.
Holt-Winters 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.
FPP_HW — Forecasting: Principles and Practice — Holt-Winters
- 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/holt-winters.html
- Accessed: 2026-07-27
- Jurisdiction: Technical method; no legal jurisdiction.
- Supports: Level, trend, and seasonal recursions plus additive versus multiplicative interpretation.
- Limitations: State-space error models and automatic initialization are not reproduced.
- 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_HW — Exponential smoothing examples
- 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/examples/notebooks/generated/exponential_smoothing.html
- Accessed: 2026-07-27
- Jurisdiction: Technical method; no legal jurisdiction.
- Supports: Holt and Holt-Winters combinations, fixed smoothing parameters, damping, and additive versus multiplicative components.
- Limitations: The package selects additive trend and additive seasonality with known initialization and no damping.
- 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}]`));
};
export function forecastHoltWintersAdditive(values:number[],alpha:number,beta:number,gamma:number,period:number,horizon:number,initialLevel:number,initialTrend:number,initialSeasonals:number[]):ForecastResult{
const m=positiveInt(period,"period",2),y=series(values,m*2),H=positiveInt(horizon,"horizon"),a=finite(alpha,"alpha"),b=finite(beta,"beta"),g=finite(gamma,"gamma");
for(const [name,value] of [["alpha",a],["beta",b],["gamma",g]] as const)if(value<0||value>1)throw new Error(`${name} must be in [0, 1]`);
let level=finite(initialLevel,"initial_level"),trend=finite(initialTrend,"initial_trend");
const seasonals=coefficients(initialSeasonals,"initial_seasonals",false);if(seasonals.length!==m)throw new Error("initial_seasonals must contain exactly period values");
const fitted:number[]=[],residuals:number[]=[],trace:Record<string,number>[]=[];
y.forEach((observation,t)=>{
const position=t%m,oldLevel=level,oldTrend=trend,oldSeason=seasonals[position],prediction=oldLevel+oldTrend+oldSeason;
level=a*(observation-oldSeason)+(1-a)*(oldLevel+oldTrend);
trend=b*(level-oldLevel)+(1-b)*oldTrend;
seasonals[position]=g*(observation-level)+(1-g)*oldSeason;
fitted.push(prediction);residuals.push(observation-prediction);trace.push({index:t,level,trend,seasonal:seasonals[position]});
});
const forecast=Array.from({length:H},(_,index)=>level+(index+1)*trend+seasonals[(y.length+index)%m]);
return {forecast,fitted,residuals,trace,state:{level,trend,seasonals}};
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.