Forecasting with Exponential Smoothing

This page documents the exponential smoothing methods and functions provided by the package, including detailed explanations, docstrings, and usage examples.

Method Functions

Simple Exponential Smoothing

Simple exponential smoothing model. Fields:

  • alpha::Float64: Smoothing constant.

Example:

model = MCHammer.SimpleES(0.2)
SimpleES(0.2)

Double Exponential Smoothing

Double exponential smoothing model with trend. Fields:

  • alpha::Float64: Level smoothing constant.
  • beta::Float64: Trend smoothing factor.

Example:

model = DoubleES(0.3, 0.1)
DoubleES(0.3, 0.1)

Triple Exponential Smoothing

Triple exponential smoothing model (Holt–Winters method) with trend and seasonality.

Fields:

  • season_length::Int: Number of periods in a season (e.g., 12 for monthly).
  • alpha::Float64: Level smoothing constant.
  • beta::Float64: Trend smoothing factor.
  • gamma::Float64: Seasonal smoothing factor.

Example:

model = TripleES(12, 0.3, 0.2, 0.1)
TripleES(12, 0.3, 0.2, 0.1)

Smoothing Functions

Perform smoothing on historical data.

MCHammer.es_smoothFunction

Perform simple exponential smoothing on the historical series.

es_smooth(m::SimpleES, HistoricalSeries::Vector{<:Real}; forecast_only::Bool=false)

Arguments

  • m::SimpleES: A SimpleES model containing the smoothing constant alpha.
  • HistoricalSeries: A vector of historical data.
  • forecast_only (optional): If true, only the smoothed forecast is returned.

Returns

A dataframe of smoothed values or the original and smoothed values.

source

Perform double exponential smoothing on the historical series.

es_smooth(m::DoubleES, HistoricalSeries::Vector{<:Real})

Arguments

  • m::DoubleES: A DoubleES model with alpha and beta.
  • HistoricalSeries: A vector of historical data.

Returns

A DataFrame where:

  • level: The smoothed level values.
  • trend: The estimated trend values.
  • smoothed: The sum of level and trend.
source

Smoothing SimpleES:

data = [100, 102, 104, 108, 110]
simple_model = SimpleES(0.2)
smoothed_simple = es_smooth(simple_model, data; forecast_only=true)
5×1 DataFrame
Rowforecast
Float64
1100.0
2100.4
3101.12
4102.496
5103.997

Smoothing DoubleES:

# DoubleES example
double_model = DoubleES(0.2, 0.1)
smoothed_double = es_smooth(double_model, data)
5×3 DataFrame
Rowleveltrendsmoothed
Float64Float64Float64
1100.00.0100.0
2100.40.04100.44
3101.120.108101.228
4102.4960.2348102.731
5103.9970.3614104.358

Forecasting out

Generate forecasts based on smoothing models.

MCHammer.es_forecastFunction

Produce a forecast using double exponential smoothing.

es_forecast(m::DoubleES, HistoricalSeries::Vector{<:Real}, periods::Int)

Arguments

  • m::DoubleES: A DoubleES model with alpha and beta.
  • HistoricalSeries: A vector of historical data.
  • periods: Number of periods to forecast beyond the historical data.

Returns

A DataFrame with columns: Historical, Level, and Forecast.

source

Produce a forecast using triple exponential smoothing (Holt–Winters method) with seasonal adjustment.

es_forecast(m::TripleES, HistoricalSeries::Vector{<:Real}, periods_out::Int; forecast_only::Bool=false)

Arguments

  • m::TripleES: A TripleES model with season_length, alpha, beta, and gamma.
  • HistoricalSeries: A vector of historical data.
  • periods_out: Number of periods to forecast beyond the historical data.
  • forecast_only (optional): If true, returns only the forecasted values.

Returns

Either a vector of forecasted values (if forecast_only=true) or the complete in-sample forecast.

source

Forecasting DoubleES:

data = [100, 102, 104, 108, 110]

# DoubleES forecast
double_model = DoubleES(0.2, 0.1)
df_forecast = es_forecast(double_model, data, 3)
8×3 DataFrame
RowHistoricalLevelForecast
Int64Float64Float64
1100100.0100.0
2102100.4100.4
3104101.12101.12
4108102.496102.496
5110103.997103.997
600.0104.358
700.0104.72
800.0105.081

<br> Let's visualize this as a line plot comparing historical data and the forecast <br>

using Plots

# Create an index vector corresponding to the rows.
x_all = 1:nrow(df_forecast)

# Identify the indices where Historical is nonzero.
nonzero_idx = findall(x -> x != 0, df_forecast.Historical)

# Plot forecast over all rows.
plot(x_all, df_forecast.Forecast, label="Forecast", xlabel="Time", ylabel="Value", lw=2)

# Plot Historical and Level only on nonzero indices.
plot!(nonzero_idx, df_forecast.Historical[nonzero_idx], label="Historical", marker=:circle, lw=2)
plot!(nonzero_idx, df_forecast.Level[nonzero_idx], label="Level", marker=:square, lw=2)

Forecasting TripleES:

# TripleES forecast

seasonal_data = [120,130,140,130,125,135,145,150,160,155,165,170]
triple_model = MCHammer.TripleES(12, 0.3, 0.2, 0.1)
df_forecast = es_forecast(triple_model, seasonal_data, 6; forecast_only=false)
18×2 DataFrame
RowHistoricalForecast
Int64Float64
1120120.0
2130123.599
3140141.283
4130136.585
5125134.901
6135146.628
7145157.139
8150161.769
9160170.804
10155164.48
11165172.984
12170176.458
130127.067
140139.386
150149.558
160139.938
170135.499
180146.212
Let's visualize this as a line plot comparing historical and forecasted data
using Plots
theme(:ggplot2)

# Create an index vector corresponding to the rows.
x_all = 1:nrow(df_forecast)

# Identify the indices where Historical is nonzero.
nonzero_idx = findall(x -> x != 0, df_forecast.Historical)

# Plot forecast over all rows.
plot(x_all, df_forecast.Forecast, label="Forecast", xlabel="Time", ylabel="Value", lw=2)

# Plot Historical and Level only on nonzero indices.
plot!(nonzero_idx, df_forecast.Historical[nonzero_idx], label="Historical", marker=:circle, lw=2)

Forecast Standard Error

Calculate standard error between historical and forecasted data.

MCHammer.FrctStdErrorFunction

Calculate the fractional standard error between the historical series and forecast series.

FrctStdError(HistoricalSeries::Vector{<:Real}, ForecastSeries::Vector{<:Real})

Arguments

  • HistoricalSeries: A vector of historical data.
  • ForecastSeries: A vector of forecasted data.

Returns

The standard error as a Float64.

source

Example:

data = [100, 102, 104, 108, 110]
double_model = DoubleES(0.3, 0.2)

# Generate in-sample forecast; since periods = 0, the forecast equals the smoothed level

df_forecast = es_forecast(double_model, data, 0)
forecasted = df_forecast.Forecast  # Access the forecast column from the returned DataFrame
se = FrctStdError(data, forecasted)
println("Forecast Standard Error: ", se)
Forecast Standard Error: 4.951604689391114

Fitting historical data

Automatically optimize parameters for TripleES.

MCHammer.es_fitFunction

Automatically fit optimal parameters for triple exponential smoothing by random trials.

es_fit(::Type{TripleES}, HistoricalSeries::Vector{<:Real}, season_length::Int, trials::Int)

Arguments

  • HistoricalSeries: A vector of historical data.
  • season_length: Number of periods in a season.
  • trials: Number of random trials to perform.

Returns

A TripleES instance with the best parameters (lowest forecast error).

source
data = [120,130,140,130,125,135,145,150,160,155,165,170]
best_model = es_fit(TripleES, data, 12, 1000)
TripleES(12, 0.03592609905071564, 0.22023858456162726, 0.8885711298535152)

Simulating forecasts (Monte-Carlo)

Simulate forecast uncertainty using historical returns.

MCHammer.forecast_uncertaintyFunction
forecast_uncertainty(HistoricalData::Vector{<:Real}, PeriodsToForecast::Int) -> Vector{Float64}

Calculate forecast uncertainty multipliers based on historical data volatility.

This function computes the percentage returns from the historical data, estimates the standard deviation (σ) of those returns, and then generates a vector of forecast multipliers. Each multiplier is calculated as:

$u = 1 + \sigma \cdot \epsilon$

where ($\epsilon$) is a random sample drawn from a standard normal distribution $\epsilon \sim \mathcal{N}(0,1)$ and $\sigma$ is the standard deviation of the historical percentage returns.

Arguments

  • HistoricalData::Vector{<:Real}: A vector of historical observations (e.g., prices, values).
  • PeriodsToForecast::Int: The number of forecast periods for which to generate uncertainty multipliers.

Returns

  • A vector of length PeriodsToForecast containing the forecast uncertainty multipliers. These multipliers can be used to perturb a base forecast to simulate forecast variability.
source

Practical Example:

#The historical data is used to assess the volatility of the series automatically.
data = [100, 105, 102, 108, 110]

uncertainty = forecast_uncertainty(data, 4)
4-element Vector{Float64}:
 1.011876340185373
 1.012254908712964
 0.9587948500115532
 1.0354953933329445

Now let's assume we want to run 1000 Monte-Carlo Trials, this is the approach we would take.

function simulate_ESTS()

#Simulation Model Inputs and Parameters
n_trials = 1000
n_periods = 5
historical_data = [100, 105, 102, 108, 110, 115, 120, 118, 122, 125, 130, 128]
seasonality = 12

    # 1. Fit optimal TripleES parameters using 1,000 optimization trials. Seasonality is the number of periods to test for seasonal patterns.
    optimal_triple = es_fit(TripleES, historical_data, seasonality, 1000)

    # 2. Generate the base forecast (for n_periods = 5) with the fitted model
    base_forecast = es_forecast(optimal_triple, historical_data, n_periods; forecast_only=true)

    # 3. Run simulation: for 1,000 trials, apply forecast uncertainty to the base forecast


    # Prepare a DataFrame to store simulation results in long format.
    # Columns: Trial (simulation number), Period (forecast period), Forecast (simulated forecast value)
    sim_results = DataFrame(Trial=Int[], Period=Int[], Forecast=Float64[])

        for trial in 1:n_trials
        # Compute forecast uncertainty multipliers for the next n_periods
        uncertainty = forecast_uncertainty(historical_data, n_periods)

        # Apply the uncertainty multipliers to the base forecast to simulate forecast variability.
        # This returns a DataFrame with columns "Historical", "Level", and "Forecast".
        simulated_forecast = base_forecast .* uncertainty

        # Now iterate over the rows using eachrow() and enumerate to get an index.
            for (i, row) in enumerate(eachrow(simulated_forecast))
                push!(sim_results, (Trial = trial, Period = i, Forecast = row.Forecast))
            end

        end


        return sim_results = unstack(sim_results, :Period, :Forecast)

end

#Genreate simulation trials
sim_results_to_chart = simulate_ESTS()

#Here are the first 10 trials
first(sim_results_to_chart,10)
10×6 DataFrame
RowTrial12345
Int64Float64?Float64?Float64?Float64?Float64?
11103.21111.323105.878110.127120.755
22100.894104.252105.863119.184112.218
3396.0384112.571108.383109.232110.863
4499.8376105.885108.98111.684110.869
55104.315115.385107.618111.613117.21
66102.666105.887100.43113.962116.073
77101.027108.202107.008110.316119.041
8897.7293105.462101.295112.037120.382
99102.1103.141100.514115.858112.529
1010104.209109.836101.908111.903113.437

When plotted, we can see the uncertainty is applied using historical volatility. Remember to remove the trials column for better charting.

trend_chrt(sim_results_to_chart[:,2:6])

Sources & References

  • Eric Torkia, Decision Superhero Vol. 3, chapter 5 : Predicting 1000 futures, Technics Publishing, 2025
  • Available on Amazon : https://a.co/d/4YlJFzY . Volumes 2 and 3 to be released in Spring and Fall 2025.