Engee documentation
Notebook

Predicting multidimensional time series using a hybrid method

Introduction

In time series analysis and forecasting, the classical problem remains the separation of the observed signal into deterministic and stochastic components. If the deterministic part allows for an accurate parametric description, then the remainder, as a rule, contains a correlated structure that is not reduced to simple noise, but is also not described by linear methods. Hybrid schemes combining classical decomposition with neural network modeling of the remainder make it possible to use the strengths of both methods.

In this paper, we consider a hybrid method for predicting multidimensional time series, which includes three stages:

  1. Least Squares Linear trend Removal (OLS);
  2. Selection of two dominant sinusoidal components with frequency optimization by the Nelder-Mead method;
  3. Prediction of the remainder by the recurrent neural network LSTM.

The quality of the forecast is assessed using RMSE and MAE metrics, and is also compared with a model without a neural network block.

Let's consider some definitions used in this work.:

  • A trend is a long–term, monotonous component of a time series that reflects the general direction of its change: growth, decline, or stagnation.
  • Seasonality is a periodic component of a series that repeats at approximately equal time intervals with a constant amplitude and phase.
  • LSTM (Long Short-Term Memory) is a type of recurrent neural network capable of storing information about previous states of a sequence at long intervals. Unlike the classical recurrent neural network, LSTM uses a mechanism that controls the writing, reading and erasing of information in a hidden state, which allows it to effectively model long-term dependencies in time series.
  • Residual energy is the sum of the squares of the discrepancies between the signal and its model.: . This is a standard OLS objective function; minimizing it means finding the model parameters at which it best describes the signal. In this paper, the residual energy is used as a criterion for optimizing the frequencies of sinusoids.
  • RMS (Root Mean Square, RMS value) – the root of the average square of the value: ​​. For the remainder of the series, RMS shows the typical amplitude of the discrepancy in the same units as the signal. The RMS of the remainder in the training area characterizes the "unexplained" part of the series, which is then transmitted to the LSTM input.
  • RMSE (Root Mean Square Error, root of the mean square error) is a metric of forecast quality: ​. It is measured in signal units and penalizes large misses more strongly than MAE.
  • MAE (Mean Absolute Error) – metric: . It shows the average "typical" forecast error in signal units and is less sensitive to outliers than RMSE.
  • MNC (least squares method) is a method for estimating the parameters of a linear model that minimizes the sum of squared residuals. It is used to estimate the linear trend and the amplitudes of sinusoids.
  • The Nelder-Mead method is an unconditional optimization algorithm that does not require derivatives; it is used to refine a pair of sinusoid frequencies by the criterion of residual energy.

Joining libraries

Connecting the necessary libraries:

Flux – neural network training and application;

Optim – optimization of sinusoid frequencies;

Statistics– calculation of remainder statistics for normalization;

LinearAlgebra – solution of MNC systems for calculating linear trend and sinusoid amplitudes;

Random – for reproducibility of learning.

In [ ]:
for pkg in ["Flux", "Optim", "Statistics", "LinearAlgebra", "Random"]
    try
        eval(Meta.parse("using $pkg"))
    catch
        Pkg.add(pkg)
        eval(Meta.parse("using $pkg"))
    end
end
Random.seed!(42)
Out[0]:
TaskLocalRNG()

Data generation

We will generate three time series , , 5000 points long.

Each row contains a general trend and the seasonal component with phase shift and constant . For and The amplitude of the overall trend is scaling, and the phase of the seasonal component is shifting.

Let's plot all three rows.

In [ ]:
t = 0:1:4999
B = 8 .* sin.(0.006 .* t) .+ 0.006 .* t
c = 30
X = B .+ 5 .* sin.(0.016 .* t)               .+ c
Y = 0.8 .* B .+ 5 .* sin.(0.016 .* t .+ π/3) .+ c
Z = 1.2 .* B .+ 5 .* sin.(0.016 .* t .+ 2π/3) .+ c
plot(t, [X Y Z], title="Time series", lw=1.8, label=["X" "Y" "Z"])
Out[0]:

Splitting into training and test samples

Let's split the data into a training sample (the first 4000 points) and a test sample (the last 1000 points), and form a common array. данные_все (5000×3) and the time vector tt. Let's highlight the time axis separately. t_обуч for the training area.

In [ ]:
N_обуч = 4000
N_тест  = 1000
N = N_обуч + N_тест
данные_все   = hcat(X, Y, Z)                 
данные_обуч  = данные_все[1:N_обуч, :]
данные_тест  = данные_все[N_обуч+1:end, :]
tt = collect(Float64, 0:N-1)
t_обуч = tt[1:N_обуч]
Out[0]:
4000-element Vector{Float64}:
    0.0
    1.0
    2.0
    3.0
    4.0
    5.0
    6.0
    7.0
    8.0
    9.0
   10.0
   11.0
   12.0
    ⋮
 3988.0
 3989.0
 3990.0
 3991.0
 3992.0
 3993.0
 3994.0
 3995.0
 3996.0
 3997.0
 3998.0
 3999.0

Removing a linear trend

Let's estimate the linear trend using the least squares (OLS) training sample for each row, plot the trend along its entire length and subtract it from the source data. As a result, we get the detrended data. детренд_все and детренд_обуч.

In [ ]:
A = hcat(ones(N_обуч), t_обуч)
коэф = A \ данные_обуч                         
тренд_все = hcat(ones(N), tt) * коэф   
детренд_все   = данные_все   .- тренд_все
детренд_обуч  = детренд_все[1:N_обуч, :]
Out[0]:
4000×3 Matrix{Float64}:
 -2.05727    2.91277    2.60508
 -1.92836    2.9912     2.62286
 -1.79948    3.06851    2.63953
 -1.67064    3.14469    2.65511
 -1.54187    3.21973    2.66961
 -1.41318    3.29362    2.68304
 -1.28461    3.36634    2.6954
 -1.15617    3.43789    2.70671
 -1.02789    3.50826    2.71697
 -0.89978    3.57744    2.7262
 -0.771873   3.64541    2.73441
 -0.644186   3.71218    2.7416
 -0.516743   3.77772    2.74779
  ⋮                    
 -1.76054   -0.549052  -7.43301
 -1.69803   -0.570166  -7.49135
 -1.63633   -0.592207  -7.54943
 -1.57544   -0.615167  -7.60724
 -1.51537   -0.639036  -7.66476
 -1.45614   -0.663805  -7.72197
 -1.39776   -0.689464  -7.77885
 -1.34024   -0.716004  -7.83538
 -1.28359   -0.743413  -7.89154
 -1.22782   -0.771681  -7.94732
 -1.17294   -0.800798  -8.00268
 -1.11896   -0.830753  -8.05761

Residual energy function

Define the function остаточная_энергия(частоты, сигнал, t), which, based on a given pair of frequencies, builds a basis of constants, cosines and sines, selects the coefficients of the OLS and returns the sum of the squares of the residuals. This function will be used as a target function for frequency optimization.

In [ ]:
function остаточная_энергия(частоты, сигнал, t)
    f1, f2 = частоты
    (f1 <= 0 || f2 <= 0) && return 1e30
    M = hcat(ones(length(t)),
             cos.(2π*f1*t), sin.(2π*f1*t),
             cos.(2π*f2*t), sin.(2π*f2*t))
    β = M \ сигнал
    r = сигнал .- M*β
    return sum(abs2, r)
end
Out[0]:
остаточная_энергия (generic function with 1 method)

Two sinusoids search function

Define the function синусоиды(сигнал, t), which searches for two dominant frequencies in the signal: first, it finds the first frequency by brute force searching through the grid, then, by subtracting its contribution, the second, and then clarifies both frequencies using the NelderMead method. Returns the found pair (f1, f2).

In [ ]:
function синусоиды(сигнал, t)
    сетка_частот = 1e-5 : 1e-5 : 1.2e-2
    лучшая_f1 = 0.0; лучший_r = Inf
    for f in сетка_частот
        M = hcat(ones(length(t)), cos.(2π*f*t), sin.(2π*f*t))
        β = M \ сигнал
        r = sum(abs2, сигнал .- M*β)
        if r < лучший_r
            лучший_r = r; лучшая_f1 = f
        end
    end

    M1 = hcat(ones(length(t)), cos.(2π*лучшая_f1*t), sin.(2π*лучшая_f1*t))
    сигнал2 = сигнал .- M1 * (M1 \ сигнал)

    лучшая_f2 = 0.0; лучший_r2 = Inf
    for f in сетка_частот
        abs(f - лучшая_f1) < 5e-4 && continue
        M = hcat(ones(length(t)), cos.(2π*f*t), sin.(2π*f*t))
        β = M \ сигнал2
        r = sum(abs2, сигнал2 .- M*β)
        if r < лучший_r2
            лучший_r2 = r; лучшая_f2 = f
        end
    end

    целевая = частоты -> остаточная_энергия(частоты, сигнал, t)
    res = Optim.optimize(целевая, [лучшая_f1, лучшая_f2],
                         NelderMead(),
                         Optim.Options(x_tol=1e-12, f_tol=1e-15,
                                       iterations=N))
    f1, f2 = Optim.minimizer(res)
    return f1, f2
end
Out[0]:
синусоиды (generic function with 1 method)

Selection of sinusoids by series

Apply the function синусоиды for each of the three determined series, compare the found frequencies with the true values. and . we will perform the final MNC selection of amplitudes in the training area and construct sinusoidal components along the entire length of the row. We will output RMS for each row.

In [ ]:
_, n_сигн = size(данные_все)
синус_все = zeros(N, n_сигн)
частоты_итог = zeros(n_сигн, 2)

for k in 1:n_сигн
    сигнал = детренд_обуч[:, k]
    f1, f2 = синусоиды(сигнал, t_обуч)
    частоты_итог[k, :] = [f1, f2]

    f1_истина = 0.006 / (2π)
    f2_истина = 0.016 / (2π)
    println("Ряд $k: f1 = $(round(f1, digits=10))  (истина $(round(f1_истина, digits=10)), Δ = $(abs(f1-f1_истина)))")
    println("       f2 = $(round(f2, digits=10))  (истина $(round(f2_истина, digits=10)), Δ = $(abs(f2-f2_истина)))")

    M_обуч = hcat(ones(N_обуч),
                   cos.(2π*f1*t_обуч), sin.(2π*f1*t_обуч),
                   cos.(2π*f2*t_обуч), sin.(2π*f2*t_обуч))
    β = M_обуч \ сигнал

    M_все = hcat(ones(N),
                 cos.(2π*f1*tt), sin.(2π*f1*tt),
                 cos.(2π*f2*tt), sin.(2π*f2*tt))
    синус_все[:, k] = M_все * β
end

остаток_все   = детренд_все   .- синус_все
остаток_обуч  = остаток_все[1:N_обуч, :]

println("\nRMS:")
for k in 1:n_сигн
    println("  Ряд $k: $(round(sqrt(mean(abs2, остаток_обуч[:,k])), sigdigits=3))")
end
┌ Warning: x_tol is deprecated. Use x_abstol or x_reltol instead. The provided value (1.0e-12) will be used as x_abstol.
└ @ Optim /opt/depot/demos/packages/Optim/z6105/src/types.jl:128
┌ Warning: f_tol is deprecated. Use f_abstol or f_reltol instead. The provided value (1.0e-15) will be used as f_reltol.
└ @ Optim /opt/depot/demos/packages/Optim/z6105/src/types.jl:138
Ряд 1: f1 = 0.0009511555  (истина 0.0009549297, Δ = 3.7741976086258684e-6)
       f2 = 0.0025495928  (истина 0.0025464791, Δ = 3.113760232510091e-6)
Ряд 2: f1 = 0.000951996  (истина 0.0009549297, Δ = 2.933630766334557e-6)
       f2 = 0.0025460388  (истина 0.0025464791, Δ = 4.402527278529571e-7)
┌ Warning: x_tol is deprecated. Use x_abstol or x_reltol instead. The provided value (1.0e-12) will be used as x_abstol.
└ @ Optim /opt/depot/demos/packages/Optim/z6105/src/types.jl:128
┌ Warning: f_tol is deprecated. Use f_abstol or f_reltol instead. The provided value (1.0e-15) will be used as f_reltol.
└ @ Optim /opt/depot/demos/packages/Optim/z6105/src/types.jl:138
Ряд 3: f1 = 0.0009523338  (истина 0.0009549297, Δ = 2.5958343298196265e-6)
       f2 = 0.0025436879  (истина 0.0025464791, Δ = 2.791216266237375e-6)

RMS:
┌ Warning: x_tol is deprecated. Use x_abstol or x_reltol instead. The provided value (1.0e-12) will be used as x_abstol.
└ @ Optim /opt/depot/demos/packages/Optim/z6105/src/types.jl:128
┌ Warning: f_tol is deprecated. Use f_abstol or f_reltol instead. The provided value (1.0e-15) will be used as f_reltol.
└ @ Optim /opt/depot/demos/packages/Optim/z6105/src/types.jl:138
  Ряд 1: 1.02
  Ряд 2: 0.662
  Ряд 3: 0.815

Normalization of the balance

Calculate the average and the standard deviation the remainder of the training sample, let's replace the ones that are too small by 1.0 , we get the normalized remainder . остаток_норм with zero mean and unit variance for each row.

In [ ]:
μ = mean(остаток_обуч, dims=1)
σ = std(остаток_обуч,  dims=1)
σ[σ .< 1e-10] .= 1.0
остаток_норм = (остаток_обуч .- μ) ./ σ     
Out[0]:
4000×3 Matrix{Float64}:
 -1.84503  -2.04697  -1.95095
 -1.84156  -2.04278  -1.94441
 -1.83812  -2.03856  -1.93787
 -1.83471  -2.03432  -1.93134
 -1.83132  -2.03005  -1.92483
 -1.82797  -2.02577  -1.91834
 -1.82465  -2.02146  -1.91185
 -1.82135  -2.01712  -1.90539
 -1.81809  -2.01277  -1.89894
 -1.81485  -2.0084   -1.89252
 -1.81165  -2.00401  -1.88611
 -1.80848  -1.99959  -1.87972
 -1.80534  -1.99516  -1.87336
  ⋮                  
  1.69907   1.76467   1.58288
  1.70712   1.76956   1.58819
  1.71519   1.77445   1.59355
  1.72328   1.77934   1.59896
  1.73139   1.78422   1.60442
  1.73952   1.7891    1.60992
  1.74766   1.79398   1.61547
  1.75582   1.79885   1.62107
  1.76399   1.80372   1.62671
  1.77218   1.80859   1.6324
  1.78038   1.81345   1.63814
  1.78859   1.8183    1.64392

Creating windows for LSTM

Let's create training sequences for LSTM: the window length is L = 100, for each of the three channels. Getting arrays X_посл forms (3, 100, 3900) – entrance windows, and Y_посл The forms (3, 3900) are the target values.

In [ ]:
L = 100
N = N_обуч - L
K = 3

X_посл = zeros(Float32, K, L, N)
Y_посл = zeros(Float32, K, N)
for i in 1:N
    X_посл[:, :, i] = Float32.(остаток_норм[i:i+L-1, :]')
    Y_посл[:, i]    = Float32.(остаток_норм[i+L, :])
end

LSTM Training

Let's define an LSTM model (input 3 → hidden state 32 → output 3) with a fully connected layer at the output. Let's set up the Adam optimizer and train the model with 10 epochs in mini-batches of 64 examples each, minimizing the MSE between the prediction at the last step of the window and the true value. We will output the value of the loss function for each epoch.

In [ ]:
модель = Chain(LSTM(K => 32), Dense(32 => K))

function потери(модель, x, y)
    Flux.reset!(модель)
    ŷ = модель(x)                    
    return Flux.mse(ŷ[:, end, :], y)
end

сост_опт = Flux.setup(Flux.Adam(1e-3), модель)
размер_батча = 64
эпох = 10
n_батчей = div(N, размер_батча)

for эпоха in 1:эпох
    перест = randperm(N)
    потери_эпохи = 0.0
    for b in 1:n_батчей
        idx = перест[(b-1)*размер_батча+1 : b*размер_батча]
        xb = X_посл[:, :, idx]
        yb = Y_посл[:, idx]
        Flux.reset!(модель)
        l, grads = Flux.withgradient(модель) do m
            Flux.mse(m(xb)[:, end, :], yb)
        end
        Flux.update!(сост_опт, модель, grads[1])
        потери_эпохи += l
    end
    println("эпоха $эпоха  потери = $(round(потери_эпохи/n_батчей, sigdigits=4))")
end
эпоха 1  потери = 0.5232
эпоха 2  потери = 0.01699
эпоха 3  потери = 0.004644
эпоха 4  потери = 0.003566
эпоха 5  потери = 0.003047
эпоха 6  потери = 0.002708
эпоха 7  потери = 0.002429
эпоха 8  потери = 0.002257
эпоха 9  потери = 0.002112
эпоха 10  потери = 0.002007

Balance forecast

Define the function прогноз_остатка, which recursively predicts the remainder by N_тест steps forward: at each step, the last window of length is taken L, is fed to the LSTM, the received prediction is added to the buffer and used to form the next window. Then we denormalize the forecast back to the original scale using and . As a result, we get остаток_полн the dimension is 5000×3.

In [ ]:
function прогноз_остатка(модель, остаток_норм, μ, σ, N_обуч, L, горизонт)
    K = 3
    всего = N_обуч + горизонт
    буфер = zeros(Float32, всего, K)
    буфер[1:N_обуч, :] = Float32.(остаток_норм)

    Flux.reset!(модель)
    for i in 1:горизонт
        idx = N_обуч + i
        окно = буфер[idx-L:idx-1, :]'
        x_вх   = reshape(окно, K, L, 1)
        ŷ = модель(x_вх)
        буфер[idx, :] = vec(ŷ[:, end, :])
    end

    return Float64.(буфер) .* Float64.(σ) .+ Float64.(μ)
end

остаток_полн = прогноз_остатка(модель, остаток_норм, μ, σ, N_обуч, L, N_тест)
Out[0]:
5000×3 Matrix{Float64}:
 -1.88057  -1.35504  -1.58977
 -1.87703  -1.35226  -1.58444
 -1.87352  -1.34947  -1.57911
 -1.87004  -1.34666  -1.57379
 -1.86659  -1.34384  -1.56849
 -1.86317  -1.341    -1.56319
 -1.85979  -1.33815  -1.55791
 -1.85643  -1.33528  -1.55265
 -1.8531   -1.3324   -1.54739
 -1.84981  -1.3295   -1.54216
 -1.84654  -1.3266   -1.53693
 -1.84331  -1.32367  -1.53173
 -1.84011  -1.32074  -1.52655
  ⋮                  
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256
  1.92881   1.13253   1.40256

Final forecast

Let's add up the three components: a linear trend тренд_все, the sinusoidal componentсинус_все and the forecast of the balance остаток_полн. We will get the full forecast y_полн and let's highlight the test part y_прогноз. We will save the true values for comparison in y_истина.

In [ ]:
y_полн = тренд_все .+ синус_все .+ остаток_полн
y_прогноз = y_полн[N_обуч+1:end, :]
y_истина = данные_тест
Out[0]:
1000×3 Matrix{Float64}:
 51.3555  47.4012  49.5032
 51.4128  47.3738  49.4556
 51.4691  47.3456  49.4086
 51.5245  47.3166  49.362
 51.5789  47.2869  49.316
 51.6324  47.2563  49.2705
 51.685   47.2251  49.2256
 51.7365  47.193   49.1813
 51.7871  47.1603  49.1375
 51.8368  47.1269  49.0944
 51.8854  47.0927  49.0519
 51.9331  47.0579  49.0101
 51.9797  47.0224  48.969
  ⋮                
 47.1826  43.8875  57.4425
 47.1695  43.9503  57.5326
 47.1579  44.0142  57.6226
 47.1479  44.0793  57.7128
 47.1393  44.1455  57.8029
 47.1323  44.2128  57.893
 47.1268  44.2812  57.9831
 47.1228  44.3507  58.0731
 47.1204  44.4213  58.163
 47.1196  44.4929  58.2529
 47.1203  44.5655  58.3426
 47.1225  44.6392  58.4322

Quality metrics

Let's calculate the RMSE and MAE metrics for each of the three rows in the test area. Additionally, we will calculate the forecast without taking into account the LSTM for comparison. It will show how much LSTM improves the quality of the forecast.

In [ ]:
rmse(a, p) = sqrt(mean((a .- p).^2))
mae(a, p)  = mean(abs.(a .- p))
имена = ["X", "Y", "Z"]
y_прогноз_безLSTM = (тренд_все .+ синус_все .+ vcat(остаток_обуч, zeros(N_тест,3)))[N_обуч+1:end, :]
println("\nКачество прогноза (t = $N_обуч..$(N_обуч+N_тест-1)):\n")
println(rpad("Row", 5), rpad("RMSE (with LSTM)", 16), rpad("RMSE / размах", 16),
        rpad("MAE (с LSTM)", 16), rpad("RMSE без LSTM", 16), "Улучшение по RMSE")
for k in 1:3
    истина_k  = y_истина[:, k]
    прогноз_k = y_прогноз[:, k]
    безLSTM_k = y_прогноз_безLSTM[:, k]

    r_с   = rmse(истина_k, прогноз_k)
    r_без = rmse(истина_k, безLSTM_k)
    m_с   = mae(истина_k, прогноз_k)
    размах = maximum(истина_k) - minimum(истина_k)
    доля   = 100 * r_с / размах
    улучш  = r_без / r_с

    println(rpad(имена[k], 5),
            rpad(round(r_с, digits=3), 16),
            rpad("≈ $(round(доля, digits=1)) %", 16),
            rpad(round(m_с, digits=3), 16),
            rpad(round(r_без, digits=3), 16),
            "в $(round(улучш, digits=1)) раза")
end
Качество прогноза (t = 4000..4999):

Ряд  RMSE (с LSTM)   RMSE / размах   MAE (с LSTM)    RMSE без LSTM   Улучшение по RMSE
X    0.517           ≈ 2.3 %         0.472           2.327           в 4.5 раза
Y    0.38            ≈ 2.0 %         0.333           1.477           в 3.9 раза
Z    0.516           ≈ 2.1 %         0.45            1.845           в 3.6 раза

Visualization of the forecast

Let's build three subgraphs (, , ), on each training area in blue, the true values on the test are dotted in orange and the forecast is red.

In [ ]:
p1 = plot(tt[1:N_обуч], X[1:N_обуч], label="", color=:blue, linewidth=2,
          title="X", legend=false)
plot!(p1, tt[N_обуч+1:end], X[N_обуч+1:end], label="",
      color=:orange, linestyle=:dash, linewidth=3)
plot!(p1, tt[N_обуч+1:end], y_прогноз[:,1], label="",
      color=:red, linewidth=3)
vline!(p1, [N_обуч+0.5], color=:black, linestyle=:dash, label="", linewidth=1)

p2 = plot(tt[1:N_обуч], Y[1:N_обуч], label="", color=:blue, linewidth=2,
          title="Y", legend=false)
plot!(p2, tt[N_обуч+1:end], Y[N_обуч+1:end], label="",
      color=:orange, linestyle=:dash, linewidth=3)
plot!(p2, tt[N_обуч+1:end], y_прогноз[:,2], label="",
      color=:red, linewidth=3)
vline!(p2, [N_обуч+0.5], color=:black, linestyle=:dash, label="", linewidth=1)

p3 = plot(tt[1:N_обуч], Z[1:N_обуч], label="", color=:blue, linewidth=2,
          title="Z", legend=false)
plot!(p3, tt[N_обуч+1:end], Z[N_обуч+1:end], label="",
      color=:orange, linestyle=:dash, linewidth=3)
plot!(p3, tt[N_обуч+1:end], y_прогноз[:,3], label="",
      color=:red, linewidth=3)
vline!(p3, [N_обуч+0.5], color=:black, linestyle=:dash, label="", linewidth=1)

display(plot(p1, p2, p3, layout=(3,1), size=(950, 950), legend=false))

Conclusion

In this paper, a hybrid method for predicting multidimensional time series is constructed and investigated, combining OLS decomposition with LSTM modeling of the remainder. The proposed method showed high quality: the prediction error in the test section was ≈ 2% of the signal range (RMSE ≈ 0.38–0.52 with a range of ≈ 25 units), and compared with the model without a neural network unit, the accuracy improved 3.5–4.5 times.

Hybrid schemes are in demand in a wide range of applications: forecasting energy consumption and load of power grids, demand and sales, financial and product ranges, meteorological and climatic indicators, parameters of technological processes and telemetry, traffic and logistics. Classical decomposition provides interpretability and stability to small samples, while LSTM takes into account non–linear and long-term dependencies in the remainder.