From 08f3eb6c3614e70f112e926193b46cccb89fe29e Mon Sep 17 00:00:00 2001 From: andre_ramos Date: Mon, 28 Apr 2025 14:10:52 -0300 Subject: [PATCH 1/6] Add new features (dynamic exog coefs), new forecasting procedure, input changes --- Project.toml | 2 +- README.md | 99 +- docs/src/assets/dynamic_exog.png | Bin 0 -> 108971 bytes docs/src/examples.md | 37 +- docs/src/features.md | 4 +- docs/src/manual.md | 15 +- paper_tests/m4_test/evaluate_model.jl | 12 +- paper_tests/m4_test/m4_test.jl | 11 +- paper_tests/m4_test/prepare_data.jl | 2 +- .../simulation_test/evaluate_models.jl | 8 +- src/StateSpaceLearning.jl | 4 +- src/estimation_procedure.jl | 97 +- src/fit_forecast.jl | 293 +--- src/models/structural_model.jl | 1091 ++++++++++----- src/structs.jl | 1 + src/utils.jl | 243 +--- test/estimation_procedure.jl | 114 +- test/fit_forecast.jl | 261 +--- test/models/structural_model.jl | 1209 ++++++++--------- test/utils.jl | 137 +- 20 files changed, 1613 insertions(+), 2027 deletions(-) create mode 100644 docs/src/assets/dynamic_exog.png diff --git a/Project.toml b/Project.toml index 7aaf3dd..80f3404 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "StateSpaceLearning" uuid = "971c4b7c-2c4e-4bac-8525-e842df3cde7b" authors = ["andreramosfc "] -version = "1.4.3" +version = "2.0.0" [deps] Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" diff --git a/README.md b/README.md index 8388d9b..e06d4bb 100644 --- a/README.md +++ b/README.md @@ -20,35 +20,41 @@ model = StructuralModel(y) fit!(model) # Point Forecast -prediction = StateSpaceLearning.forecast(model, 12) #Gets a 12 steps ahead prediction +prediction = forecast(model, 12) # Gets a 12 steps ahead prediction # Scenarios Path Simulation -simulation = StateSpaceLearning.simulate(model, 12, 1000) #Gets 1000 scenarios path of 12 steps ahead predictions +simulation = simulate(model, 12, 1000) # Gets 1000 scenarios path of 12 steps ahead predictions ``` ## StructuralModel Arguments -* `y::Vector`: Vector of data. -* `level::Bool`: Boolean where to consider intercept in the model (default: true) -* `stochastic_level::Bool`: Boolean where to consider stochastic level component in the model (default: true) -* `trend::Bool`: Boolean where to consider trend component in the model (default: true) -* `stochastic_trend::Bool`: Boolean where to consider stochastic trend component in the model (default: true) -* `seasonal::Bool`: Boolean where to consider seasonal component in the model (default: true) -* `stochastic_seasonal::Bool`: Boolean where to consider stochastic seasonal component in the model (default: true) -* `freq_seasonal::Int`: Seasonal frequency to be considered in the model (default: 12) -* `outlier::Bool`: Boolean where to consider outlier component in the model (default: true) -* `ζ_ω_threshold::Int`: Argument to stabilize `stochastic trend` and `stochastic seasonal` components (default: 12) +* `y::Union{Vector,Matrix}`: Time series data +* `level::String`: Level component type: "stochastic", "deterministic", or "none" (default: "stochastic") +* `slope::String`: Slope component type: "stochastic", "deterministic", or "none" (default: "stochastic") +* `seasonal::String`: Seasonal component type: "stochastic", "deterministic", or "none" (default: "stochastic") +* `cycle::String`: Cycle component type: "stochastic", "deterministic", or "none" (default: "none") +* `freq_seasonal::Union{Int,Vector{Int}}`: Seasonal frequency or vector of frequencies (default: 12) +* `cycle_period::Union{Union{Int,<:AbstractFloat},Vector{Int},Vector{<:AbstractFloat}}`: Cycle period or vector of periods (default: 0) +* `outlier::Bool`: Include outlier component (default: true) +* `ζ_threshold::Int`: Threshold for slope innovations (default: 12) +* `ω_threshold::Int`: Threshold for seasonal innovations (default: 12) +* `ϕ_threshold::Int`: Threshold for cycle innovations (default: 12) +* `stochastic_start::Int`: Starting point for stochastic components (default: 1) +* `exog::Matrix`: Matrix of exogenous variables (default: zeros(length(y), 0)) +* `dynamic_exog_coefs::Union{Vector{<:Tuple}, Nothing}`: Dynamic exogenous coefficients (default: nothing) ## Features Current features include: -* Estimation -* Components decomposition -* Forecasting -* Completion of missing values -* Predefined models, including: -* Outlier detection -* Outlier robust models +* Model estimation using elastic net based regularization +* Automatic component decomposition (trend, seasonal, cycle) +* Point forecasting and scenario simulation +* Missing value imputation +* Outlier detection and robust modeling +* Multiple seasonal frequencies support +* Deterministic and stochastic component options +* Dynamic exogenous variable handling +* Best subset selection for exogenous variables ## Quick Examples @@ -67,7 +73,7 @@ steps_ahead = 30 model = StructuralModel(log_air_passengers) fit!(model) -prediction_log = StateSpaceLearning.forecast(model, steps_ahead) # arguments are the output of the fitted model and number of steps ahead the user wants to forecast +prediction_log = forecast(model, steps_ahead) # arguments are the output of the fitted model and number of steps ahead the user wants to forecast prediction = exp.(prediction_log) plot_point_forecast(airp.passengers, prediction) @@ -76,7 +82,7 @@ plot_point_forecast(airp.passengers, prediction) ```julia N_scenarios = 1000 -simulation = StateSpaceLearning.simulate(model, steps_ahead, N_scenarios) # arguments are the output of the fitted model, number of steps ahead the user wants to forecast and number of scenario paths +simulation = simulate(model, steps_ahead, N_scenarios) # arguments are the output of the fitted model, number of steps ahead the user wants to forecast and number of scenario paths plot_scenarios(airp.passengers, exp.(simulation)) @@ -97,22 +103,20 @@ log_air_passengers = log.(airp.passengers) model = StructuralModel(log_air_passengers) fit!(model) -level = model.output.components["μ1"]["Values"] + model.output.components["ξ"]["Values"] -slope = model.output.components["ν1"]["Values"] + model.output.components["ζ"]["Values"] -seasonal = model.output.components["γ1_12"]["Values"] + model.output.components["ω_12"]["Values"] -trend = level + slope - -plot(trend, w=2 , color = "Black", lab = "Trend Component", legend = :outerbottom) -plot(seasonal, w=2 , color = "Black", lab = "Seasonal Component", legend = :outerbottom) +# Access decomposed components directly +trend = model.output.decomposition["trend"] +seasonal = model.output.decomposition["seasonal_12"] +plot(trend, w=2, color = "Black", lab = "Trend Component", legend = :outerbottom) +plot(seasonal, w=2, color = "Black", lab = "Seasonal Component", legend = :outerbottom) ``` | ![quick_example_trend](./docs/src/assets/trend.svg) | ![quick_example_seas](./docs/src/assets/seasonal.svg)| |:------------------------------:|:-----------------------------:| -### Best Subset Selection -Quick example on how to perform best subset selection in time series utilizing StateSpaceLearning. +### Best Subset Selection and Dynamic Coefficients +Example of performing best subset selection and using dynamic coefficients: ```julia using StateSpaceLearning @@ -122,22 +126,33 @@ using Random Random.seed!(2024) +# Load data airp = CSV.File(StateSpaceLearning.AIR_PASSENGERS) |> DataFrame log_air_passengers = log.(airp.passengers) -X = rand(length(log_air_passengers), 10) # Create 10 exogenous features -β = rand(3) - -y = log_air_passengers + X[:, 1:3]*β # add to the log_air_passengers series a contribution from only 3 exogenous features. - -model = StructuralModel(y; Exogenous_X = X) -fit!(model; α = 1.0, information_criteria = "bic", ϵ = 0.05, penalize_exogenous = true, penalize_initial_states = true) - -Selected_exogenous = model.output.components["Exogenous_X"]["Selected"] +# Create exogenous features +X = rand(length(log_air_passengers), 10) +β = rand(3) +y = log_air_passengers + X[:, 1:3]*β + +# Create model with exogenous variables +model = StructuralModel(y; + exog = X +) + +# Fit model with elastic net regularization +fit!(model; + α = 1.0, # 1.0 for Lasso, 0.0 for Ridge + information_criteria = "bic", + ϵ = 0.05, + penalize_exogenous = true, + penalize_initial_states = true +) + +# Get selected features +selected_exog = model.output.components["exog"]["Selected"] ``` -In this example, the selected exogenous features were 1, 2, 3, as expected. - ### Missing values imputation Quick example of completion of missing values for the air passengers time-series (artificial NaN values are added to the original time-series). @@ -209,7 +224,7 @@ fit!(model) residuals_variances = model.output.residuals_variances ss_model = BasicStructural(log_air_passengers, 12) -set_initial_hyperparameters!(ss_model, Dict("sigma2_ε" => residuals_variances["ε"], +StateSpaceModels.set_initial_hyperparameters!(ss_model, Dict("sigma2_ε" => residuals_variances["ε"], "sigma2_ξ" =>residuals_variances["ξ"], "sigma2_ζ" =>residuals_variances["ζ"], "sigma2_ω" =>residuals_variances["ω_12"])) diff --git a/docs/src/assets/dynamic_exog.png b/docs/src/assets/dynamic_exog.png new file mode 100644 index 0000000000000000000000000000000000000000..bbab8cda412e2f03656733ff9b329f21672e5153 GIT binary patch literal 108971 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�K<$-B5tPCOHpn7#KV7y3ZwWTn;hA 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 DataFrame +y = log.(airp.passengers) +X = vcat(collect(1:90), collect(90.5:-0.5:64)) + (rand(144) .* 10) +y += X * -0.03 + +dynamic_coefs = [(X, "level")] + +model = StructuralModel(y; + dynamic_exog_coefs=dynamic_coefs +) + +fit!(model) + +prediction = forecast(model, 30; dynamic_exog_coefs_forecasts = [collect(63.5:-0.5:49)]) + +plot_point_forecast(y, prediction) +``` +![dynamic_exog](assets/dynamic_exog.png) diff --git a/docs/src/features.md b/docs/src/features.md index 372cb0a..edd1e2c 100644 --- a/docs/src/features.md +++ b/docs/src/features.md @@ -104,10 +104,10 @@ X = rand(length(log_air_passengers), 10) # Create 10 exogenous features y = log_air_passengers + X[:, 1:3]*β # add to the log_air_passengers series a contribution from only 3 exogenous features. -model = StructuralModel(y; Exogenous_X = X) +model = StructuralModel(y; exog = X) fit!(model; α = 1.0, information_criteria = "bic", ϵ = 0.05, penalize_exogenous = true, penalize_initial_states = true) -Selected_exogenous = model.output.components["Exogenous_X"]["Selected"] +Selected_exogenous = model.output.components["exog"]["Selected"] ``` diff --git a/docs/src/manual.md b/docs/src/manual.md index e8d20e9..ce4f096 100644 --- a/docs/src/manual.md +++ b/docs/src/manual.md @@ -23,7 +23,14 @@ simulation = StateSpaceLearning.simulate(model, 12, 1000) #Gets 1000 scenarios p ``` ## Models -The package currently supports the implementation of the StructuralModel. If you have suggestions for additional models to include, we encourage you to contribute by opening an issue or submitting a pull request. +The package currently supports the implementation of the StructuralModel, which includes capabilities for handling dynamic exogenous coefficients. If you have suggestions for additional models to include, we encourage you to contribute by opening an issue or submitting a pull request. + +``` + +When using dynamic coefficients: +- The model will create time-varying coefficients for each specified exogenous variable +- Each coefficient will follow the specified cyclical pattern +- When forecasting, you must provide future values for exogenous variables using the `Exogenous_Forecast` parameter ```@docs StateSpaceLearning.StructuralModel @@ -39,7 +46,11 @@ StateSpaceLearning.fit! ## Forecasting and Simulating -The package has functions to make point forecasts multiple steps ahead and to simulate scenarios based on those forecasts. These functions are implemented both for the univariate and to the multivariate cases. +The package has functions to make point forecasts multiple steps ahead and to simulate scenarios based on those forecasts. These functions are implemented for both univariate and multivariate cases, with support for exogenous variables and dynamic coefficients. + +When using models with exogenous variables: +- For standard exogenous variables, provide future values using the `Exogenous_Forecast` parameter +- For dynamic coefficients, use the same `Exogenous_Forecast` parameter with values for each exogenous variable ```@docs StateSpaceLearning.forecast diff --git a/paper_tests/m4_test/evaluate_model.jl b/paper_tests/m4_test/evaluate_model.jl index 852da58..987cab7 100644 --- a/paper_tests/m4_test/evaluate_model.jl +++ b/paper_tests/m4_test/evaluate_model.jl @@ -19,15 +19,13 @@ function evaluate_SSL( model = StateSpaceLearning.StructuralModel( normalized_y; - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=true, - stochastic_seasonal=true, + level="stochastic", + slope="stochastic", + seasonal="stochastic", freq_seasonal=12, outlier=outlier, - ζ_ω_threshold=12, + ζ_threshold=12, + ω_threshold=12, ) StateSpaceLearning.fit!( model; diff --git a/paper_tests/m4_test/m4_test.jl b/paper_tests/m4_test/m4_test.jl index 7b18025..9c9e723 100644 --- a/paper_tests/m4_test/m4_test.jl +++ b/paper_tests/m4_test/m4_test.jl @@ -50,7 +50,7 @@ function run_config( save_init ? CSV.write(init_filepath, initialization_df) : nothing # Initialize empty CSV for i in 1:48000 - if i in [10001, 20001, 30001, 40001] # Clear DataFrame to save memory + if i % 1000 == 1 # Clear DataFrame to save memory results_df = DataFrame() initialization_df = DataFrame() end @@ -66,7 +66,8 @@ function run_config( information_criteria, ) - if i in [10000, 20000, 30000, 40000, 48000] + if i % 1000 == 0 + @info "Saving results for $i series" !save_init ? append_results(filepath, results_df) : nothing save_init ? append_results(init_filepath, initialization_df) : nothing end @@ -100,9 +101,9 @@ end # Main script function main() results_table = DataFrame() - for outlier in [true] - for information_criteria in ["aic"] - for α in [0.1] + for outlier in [true, false] + for information_criteria in ["aic", "bic"] + for α in [0.0, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0] @info "Running SSL with outlier = $outlier, information_criteria = $information_criteria, α = $α" results_table = run_config( results_table, outlier, information_criteria, α, false, 60 diff --git a/paper_tests/m4_test/prepare_data.jl b/paper_tests/m4_test/prepare_data.jl index 9d69a57..4ea1be0 100644 --- a/paper_tests/m4_test/prepare_data.jl +++ b/paper_tests/m4_test/prepare_data.jl @@ -2,7 +2,7 @@ function normalize(y::Vector, max_y::AbstractFloat, min_y::AbstractFloat) return (y .- min_y) ./ (max_y - min_y) end -function de_normalize(y::Vector, max_y::AbstractFloat, min_y::AbstractFloat) +function de_normalize(y, max_y::AbstractFloat, min_y::AbstractFloat) return (y .* (max_y - min_y)) .+ min_y end diff --git a/paper_tests/simulation_test/evaluate_models.jl b/paper_tests/simulation_test/evaluate_models.jl index c1d029d..72eddf8 100644 --- a/paper_tests/simulation_test/evaluate_models.jl +++ b/paper_tests/simulation_test/evaluate_models.jl @@ -28,7 +28,7 @@ function get_SSL_results( freq_seasonal=12, outlier=false, ζ_ω_threshold=12, - Exogenous_X=X_train, + exog=X_train, ) t = @elapsed StateSpaceLearning.fit!( model; @@ -39,13 +39,13 @@ function get_SSL_results( penalize_initial_states=true, ) - selected = model.output.components["Exogenous_X"]["Selected"] + selected = model.output.components["exog"]["Selected"] true_positives, false_positives, false_negatives, true_negatives = get_confusion_matrix( selected, true_features, false_features ) - mse = mse_func(model.output.components["Exogenous_X"]["Coefs"], true_β) - bias = bias_func(model.output.components["Exogenous_X"]["Coefs"], true_β) + mse = mse_func(model.output.components["exog"]["Coefs"], true_β) + bias = bias_func(model.output.components["exog"]["Coefs"], true_β) series_result = DataFrame( [ diff --git a/src/StateSpaceLearning.jl b/src/StateSpaceLearning.jl index d19d07e..402dfb2 100644 --- a/src/StateSpaceLearning.jl +++ b/src/StateSpaceLearning.jl @@ -1,6 +1,6 @@ module StateSpaceLearning -using LinearAlgebra, Statistics, GLMNet, Distributions, SparseArrays +using LinearAlgebra, Statistics, GLMNet, Distributions, SparseArrays, Random abstract type StateSpaceLearningModel end @@ -13,6 +13,6 @@ include("datasets.jl") include("fit_forecast.jl") include("plots.jl") -export fit!, forecast, simulate, StructuralModel, plot_point_forecast, plot_scenarios +export fit!, forecast, simulate, StructuralModel, plot_point_forecast, plot_scenarios, simulate_states end # module StateSpaceLearning diff --git a/src/estimation_procedure.jl b/src/estimation_procedure.jl index d3548da..e0bad0f 100644 --- a/src/estimation_procedure.jl +++ b/src/estimation_procedure.jl @@ -1,22 +1,22 @@ """ - get_dummy_indexes(Exogenous_X::Matrix{Fl}) where {Fl} + get_dummy_indexes(exog::Matrix{Fl}) where {Fl} Identifies and returns the indexes of columns in the exogenous matrix that contain dummy variables. # Arguments - - `Exogenous_X::Matrix{Fl}`: Exogenous variables matrix. + - `exog::Matrix{Fl}`: Exogenous variables matrix. # Returns - `Vector{Int}`: Vector containing the indexes of columns with dummy variables. """ -function get_dummy_indexes(Exogenous_X::Matrix{Fl}) where {Fl} - T, p = size(Exogenous_X) +function get_dummy_indexes(exog::Matrix{Fl}) where {Fl} + T, p = size(exog) dummy_indexes = [] for i in 1:p - if count(iszero.(Exogenous_X[:, i])) == T - 1 - push!(dummy_indexes, findfirst(i -> i != 0.0, Exogenous_X[:, i])) + if count(iszero.(exog[:, i])) == T - 1 + push!(dummy_indexes, findfirst(i -> i != 0.0, exog[:, i])) end end @@ -43,7 +43,7 @@ function get_outlier_duplicate_columns( return [] else o_indexes = components_indexes["o"] - exogenous_indexes = components_indexes["Exogenous_X"] + exogenous_indexes = components_indexes["exog"] dummy_indexes = get_dummy_indexes(Estimation_X[:, exogenous_indexes]) @@ -203,14 +203,14 @@ function fit_lasso( hasintercept = has_intercept(Estimation_X) if hasintercept if !penalize_exogenous - penalty_factor[components_indexes["Exogenous_X"] .- 1] .= 0 + penalty_factor[components_indexes["exog"] .- 1] .= 0 else nothing end Lasso_X = Estimation_X[:, 2:end] else if !penalize_exogenous - penalty_factor[components_indexes["Exogenous_X"]] .= 0 + penalty_factor[components_indexes["exog"]] .= 0 else nothing end @@ -257,6 +257,8 @@ end ϵ::AbstractFloat, penalize_exogenous::Bool, penalize_initial_states::Bool, + innovations_names::Vector{String}, + initial_states_names::Vector{String}, )::Tuple{Vector{AbstractFloat},Vector{AbstractFloat}} where {Fl <: AbstractFloat, Tl <: AbstractFloat} Fits an Adaptive Lasso (AdaLasso) regression model to the provided data and returns coefficients and residuals. @@ -270,6 +272,9 @@ end - `ϵ::AbstractFloat`: Non negative value to handle 0 coefs on the first lasso step (default: 0.05). - `penalize_exogenous::Bool`: Flag for selecting exogenous variables. When false the penalty factor for these variables will be set to 0. - `penalize_initial_states::Bool`: Flag for selecting initial states. When false the penalty factor for these variables will be set to 0. + - `innovations_names::Vector{String}`: Vector of strings containing the names of the innovations. + - `initial_states_names::Vector{String}`: Vector of strings containing the names of the initial states. + # Returns - `Tuple{Vector{AbstractFloat}, Vector{AbstractFloat}}`: Tuple containing coefficients and residuals of the fitted AdaLasso model. @@ -284,6 +289,7 @@ function estimation_procedure( ϵ::AbstractFloat, penalize_exogenous::Bool, penalize_initial_states::Bool, + innovations_names::Vector{String}, )::Tuple{ Vector{AbstractFloat},Vector{AbstractFloat} } where {Fl<:AbstractFloat,Tl<:AbstractFloat} @@ -292,11 +298,15 @@ function estimation_procedure( hasintercept = has_intercept(Estimation_X) + # all zero columns in X + all_zero_idx = findall(i -> all(iszero, Estimation_X[:, i]), 1:size(Estimation_X, 2)) + if hasintercept penalty_factor = ones(size(Estimation_X, 2) - 1) if length(penalty_factor) != length(components_indexes["initial_states"][2:end]) penalty_factor[components_indexes["initial_states"][2:end] .- 1] .= 0 end + penalty_factor[all_zero_idx .- 1] .= Inf coefs, _ = fit_lasso( Estimation_X, estimation_y, @@ -312,6 +322,7 @@ function estimation_procedure( if length(penalty_factor) != length(components_indexes["initial_states"]) penalty_factor[components_indexes["initial_states"][1:end]] .= 0 end + penalty_factor[all_zero_idx] .= Inf coefs, _ = fit_lasso( Estimation_X, estimation_y, @@ -330,10 +341,7 @@ function estimation_procedure( for key in keys(components_indexes) if key != "initial_states" && key != "μ1" component = components_indexes[key] - if key != "Exogenous_X" && - key != "o" && - !(key in ["ν1"]) && - !(occursin("γ", key)) + if key in innovations_names κ = count(i -> i != 0, coefs[component]) < 1 ? 0 : std(coefs[component]) if hasintercept ts_penalty_factor[component .- 1] .= (1 / (κ + ϵ)) @@ -356,12 +364,14 @@ function estimation_procedure( else nothing end + ts_penalty_factor[all_zero_idx .- 1] .= Inf else if !penalize_initial_states ts_penalty_factor[components_indexes["initial_states"][1:end]] .= 0 else nothing end + ts_penalty_factor[all_zero_idx] .= Inf end return fit_lasso( @@ -375,64 +385,3 @@ function estimation_procedure( rm_average=false, ) end - -""" - estimation_procedure( - Estimation_X::Matrix{Tl}, - estimation_y::Matrix{Fl}, - components_indexes::Dict{String,Vector{Int}}, - α::AbstractFloat, - information_criteria::String, - ϵ::AbstractFloat, - penalize_exogenous::Bool, - penalize_initial_states::Bool, -)::Tuple{Vector{Vector{AbstractFloat}},Vector{Vector{AbstractFloat}}} where {Fl <: AbstractFloat, Tl <: AbstractFloat} - - Fits an Adaptive Lasso (AdaLasso) regression model to the provided data and returns coefficients and residuals. - - # Arguments - - `Estimation_X::Matrix{Fl}`: Matrix of predictors for estimation. - - `estimation_y::Matrix{Fl}`: Matrix of response values for estimation. - - `components_indexes::Dict{String, Vector{Int}}`: Dictionary containing indexes for different components. - - `α::AbstractFloat`: Elastic net control factor between ridge (α=0) and lasso (α=1) (default: 0.1). - - `information_criteria::String`: Information Criteria method for hyperparameter selection (default: aic). - - `ϵ::AbstractFloat`: Non negative value to handle 0 coefs on the first lasso step (default: 0.05). - - `penalize_exogenous::Bool`: Flag for selecting exogenous variables. When false the penalty factor for these variables will be set to 0. - - `penalize_initial_states::Bool`: Flag for selecting initial states. When false the penalty factor for these variables will be set to 0. - - # Returns - - `Tuple{Vector{AbstractFloat}, Vector{AbstractFloat}}`: Tuple containing coefficients and residuals of the fitted AdaLasso model. - -""" -function estimation_procedure( - Estimation_X::Matrix{Tl}, - estimation_y::Matrix{Fl}, - components_indexes::Dict{String,Vector{Int}}, - α::AbstractFloat, - information_criteria::String, - ϵ::AbstractFloat, - penalize_exogenous::Bool, - penalize_initial_states::Bool, -)::Tuple{ - Vector{Vector{AbstractFloat}},Vector{Vector{AbstractFloat}} -} where {Fl<:AbstractFloat,Tl<:AbstractFloat} - coefs_vec = Vector{AbstractFloat}[] - ε_vec = Vector{AbstractFloat}[] - N_series = size(estimation_y, 2) - - for i in 1:N_series - coef_i, ε_i = estimation_procedure( - Estimation_X, - estimation_y[:, i], - components_indexes, - α, - information_criteria, - ϵ, - penalize_exogenous, - penalize_initial_states, - ) - push!(coefs_vec, coef_i) - push!(ε_vec, ε_i) - end - return coefs_vec, ε_vec -end diff --git a/src/fit_forecast.jl b/src/fit_forecast.jl index 5836d7f..5a13434 100644 --- a/src/fit_forecast.jl +++ b/src/fit_forecast.jl @@ -44,6 +44,8 @@ function fit!( components_indexes = get_components_indexes(model) + innovations_names = get_model_innovations(model) + coefs, estimation_ε = estimation_procedure( Estimation_X, estimation_y, @@ -53,6 +55,7 @@ function fit!( ϵ, penalize_exogenous, penalize_initial_states, + innovations_names, ) components = build_components(Estimation_X, coefs, components_indexes) @@ -63,292 +66,10 @@ function fit!( ε, fitted = get_fit_and_residuals(estimation_ε, coefs, model.X, valid_indexes, T) - if typeof(model.y) <: Vector - output = Output(coefs, ε, fitted, residuals_variances, valid_indexes, components) - else - output = Output[] - for i in eachindex(coefs) - push!( - output, - Output( - coefs[i], - ε[i], - fitted[i], - residuals_variances[i], - valid_indexes, - components[i], - ), - ) - end - end - return model.output = output -end - -@doc raw""" -Returns the forecast for a given number of steps ahead using the provided StateSpaceLearning output and exogenous forecast data. - -forecast(model::StateSpaceLearningModel, steps\_ahead::Int; Exogenous\_Forecast::Union{Matrix{Fl}, Missing}=missing)::Vector{AbstractFloat} where Fl - -# Arguments -- `model::StateSpaceLearningModel`: Model obtained from fitting. -- `steps_ahead::Int`: Number of steps ahead for forecasting. -- `Exogenous_Forecast::Matrix{Fl}`: Exogenous variables forecast (default: zeros(steps_ahead, 0)) + decomposition = get_model_decomposition(model, components) -# Returns -- `Union{Matrix{AbstractFloat}, Vector{AbstractFloat}}`: Matrix or vector of matrices containing forecasted values. - -# Example -```julia -y = rand(100) -model = StructuralModel(y) -fit!(model) -steps_ahead = 12 -point_prediction = forecast(model, steps_ahead) -``` -""" -function forecast( - model::StateSpaceLearningModel, - steps_ahead::Int; - Exogenous_Forecast::Matrix{Fl}=zeros(steps_ahead, 0), -)::Union{Matrix{<:AbstractFloat},Vector{<:AbstractFloat}} where {Fl<:AbstractFloat} - @assert isfitted(model) "Model must be fitted before simulation" - exog_idx = if typeof(model.output) == Output - model.output.components["Exogenous_X"]["Indexes"] - else - model.output[1].components["Exogenous_X"]["Indexes"] - end - @assert length(exog_idx) == size(Exogenous_Forecast, 2) "If an exogenous matrix was utilized in the estimation procedure, it must be provided its prediction for the forecast procedure. If no exogenous matrix was utilized, Exogenous_Forecast must be missing" - @assert size(Exogenous_Forecast, 1) == steps_ahead "Exogenous_Forecast must have the same number of rows as steps_ahead" - - Exogenous_X = model.X[:, exog_idx] - complete_matrix = create_X(model, Exogenous_X, steps_ahead, Exogenous_Forecast) - - if typeof(model.output) == Output - return AbstractFloat.( - complete_matrix[(end - steps_ahead + 1):end, :] * model.output.coefs - ) - else - prediction = Matrix{AbstractFloat}(undef, steps_ahead, length(model.output)) - for i in eachindex(model.output) - prediction[:, i] = - complete_matrix[(end - steps_ahead + 1):end, :] * model.output[i].coefs - end - return AbstractFloat.(prediction) - end -end - -@doc raw""" -Generate simulations for a given number of steps ahead using the provided StateSpaceLearning output and exogenous forecast data. - -simulate(model::StateSpaceLearningModel, steps\_ahead::Int, N\_scenarios::Int; - Exogenous\_Forecast::Matrix{Fl}=zeros(steps_ahead, 0))::Matrix{AbstractFloat} where Fl - -# Arguments -- `model::StateSpaceLearningModel`: Model obtained from fitting. -- `steps_ahead::Int`: Number of steps ahead for simulation. -- `N_scenarios::Int`: Number of scenarios to simulate (default: 1000). -- `Exogenous_Forecast::Matrix{Fl}`: Exogenous variables forecast (default: zeros(steps_ahead, 0)) - -# Returns -- `Union{Vector{Matrix{AbstractFloat}}, Matrix{AbstractFloat}}`: Matrix or vector of matrices containing simulated values. - -# Example (Univariate Case) -```julia -y = rand(100) -model = StructuralModel(y) -fit!(model) -steps_ahead = 12 -N_scenarios = 1000 -simulation = simulate(model, steps_ahead, N_scenarios) -``` - -# Example (Multivariate Case) -```julia -y = rand(100, 3) -model = StructuralModel(y) -fit!(model) -steps_ahead = 12 -N_scenarios = 1000 -simulation = simulate(model, steps_ahead, N_scenarios) -``` -""" -function simulate( - model::StateSpaceLearningModel, - steps_ahead::Int, - N_scenarios::Int; - Exogenous_Forecast::Matrix{Fl}=zeros(steps_ahead, 0), - seasonal_innovation_simulation::Int=0, -)::Union{Vector{Matrix{<:AbstractFloat}},Matrix{<:AbstractFloat}} where {Fl<:AbstractFloat} - @assert seasonal_innovation_simulation >= 0 "seasonal_innovation_simulation must be a non-negative integer" - @assert seasonal_innovation_simulation >= 0 "seasonal_innovation_simulation must be a non-negative integer" - @assert isfitted(model) "Model must be fitted before simulation" - - prediction = forecast(model, steps_ahead; Exogenous_Forecast=Exogenous_Forecast) - - is_univariate = typeof(model.output) == Output - - simulation_X = zeros(steps_ahead, 0) - valid_indexes = - is_univariate ? model.output.valid_indexes : model.output[1].valid_indexes - components_matrix = zeros(length(valid_indexes), 0) - N_components = 1 - - model_innovations = get_model_innovations(model) - for innovation in model_innovations - simulation_X = hcat( - simulation_X, - get_innovation_simulation_X(model, innovation, steps_ahead)[ - (end - steps_ahead):(end - 1), (end - steps_ahead + 1):end - ], - ) - comp = fill_innovation_coefs(model, innovation, valid_indexes) - components_matrix = hcat(components_matrix, comp) - N_components += 1 - end - - if is_univariate - components_matrix = hcat(components_matrix, model.output.ε[valid_indexes]) - @assert N_components < length(model.y)//seasonal_innovation_simulation "The parameter `seasonal_innovation_simulation` is too large for the given dataset, please reduce it" - else - for i in eachindex(model.output) - components_matrix = hcat(components_matrix, model.output[i].ε[valid_indexes]) - end - N_mv_components = N_components * length(model.output) - @assert N_mv_components < size(model.y, 1)//seasonal_innovation_simulation "The parameter `seasonal_innovation_simulation` is too large for the given dataset, please reduce it" - end - simulation_X = hcat(simulation_X, Matrix(1.0 * I, steps_ahead, steps_ahead)) - components_matrix += rand(Normal(0, 1), size(components_matrix)) ./ 1e9 # Make sure matrix is positive definite - - MV_dist_vec = Vector{MvNormal}(undef, steps_ahead) - o_noises = if is_univariate - zeros(steps_ahead, N_scenarios) - else - [zeros(steps_ahead, N_scenarios) for _ in 1:length(model.output)] - end - - if seasonal_innovation_simulation == 0 - ∑ = if is_univariate - Diagonal([var(components_matrix[:, i]) for i in 1:N_components]) - else - Diagonal([var(components_matrix[:, i]) for i in 1:N_mv_components]) - end - for i in 1:steps_ahead - MV_dist_vec[i] = if is_univariate - MvNormal(zeros(N_components), ∑) - else - MvNormal(zeros(N_mv_components), ∑) - end - end - - if model.outlier - if is_univariate - o_noises = rand( - Normal(0, std(model.output.components["o"]["Coefs"])), - steps_ahead, - N_scenarios, - ) - else - o_noises = [ - rand( - Normal(0, std(model.output[i].components["o"]["Coefs"])), - steps_ahead, - N_scenarios, - ) for i in eachindex(model.output) - ] - end - end - else - start_seasonal_term = (size(components_matrix, 1) % seasonal_innovation_simulation) - for i in 1:min(seasonal_innovation_simulation, steps_ahead) - ∑ = if is_univariate - Diagonal([ - var( - components_matrix[ - (i + start_seasonal_term):seasonal_innovation_simulation:end, - j, - ], - ) for j in 1:N_components - ]) - else - Diagonal([ - var( - components_matrix[ - (i + start_seasonal_term):seasonal_innovation_simulation:end, - j, - ], - ) for j in 1:N_mv_components - ]) - end - - MV_dist_vec[i] = if is_univariate - MvNormal(zeros(N_components), ∑) - else - MvNormal(zeros(N_mv_components), ∑) - end - if is_univariate - if model.outlier - o_noises[i, :] = rand( - Normal( - 0, - std( - model.output.components["o"]["Coefs"][(i + start_seasonal_term):seasonal_innovation_simulation:end], - ), - ), - N_scenarios, - ) - else - nothing - end - else - for j in eachindex(model.output) - if model.outlier - o_noises[j][i, :] = rand( - Normal( - 0, - std( - model.output[j].components["o"]["Coefs"][(i + start_seasonal_term):seasonal_innovation_simulation:end], - ), - ), - N_scenarios, - ) - else - nothing - end - end - end - end - for i in (seasonal_innovation_simulation + 1):steps_ahead - MV_dist_vec[i] = MV_dist_vec[i - seasonal_innovation_simulation] - if model.outlier - if is_univariate - o_noises[i, :] = o_noises[i - seasonal_innovation_simulation, :] - else - for j in eachindex(model.output) - o_noises[j][i, :] = o_noises[j][ - i - seasonal_innovation_simulation, :, - ] - end - end - end - end - end - - simulation = if is_univariate - AbstractFloat.(hcat([prediction for _ in 1:N_scenarios]...)) - else - [ - AbstractFloat.(hcat([prediction[:, i] for _ in 1:N_scenarios]...)) for - i in eachindex(model.output) - ] - end - if is_univariate - fill_simulation!(simulation, MV_dist_vec, o_noises, simulation_X) - else - fill_simulation!( - simulation, MV_dist_vec, o_noises, simulation_X, length(model_innovations) - ) - simulation = Vector{Matrix{<:AbstractFloat}}(simulation) - end + model.output = Output( + coefs, ε, fitted, residuals_variances, valid_indexes, components, decomposition + ) - return simulation end diff --git a/src/models/structural_model.jl b/src/models/structural_model.jl index 1f79683..b080e73 100644 --- a/src/models/structural_model.jl +++ b/src/models/structural_model.jl @@ -10,14 +10,15 @@ Instantiates a Structural State Space Learning model. seasonal::Bool=true, stochastic_seasonal::Bool=true, freq_seasonal::Union{Int, Vector{Int}}=12, + cycle_period::Union{Union{Int,<:AbstractFloat},Vector{Int},Vector{<:AbstractFloat}}=0, + stochastic_cycle::Bool=false, outlier::Bool=true, - ζ_ω_threshold::Int=12, + ζ_threshold::Int=12, + ω_threshold::Int=12, + ϕ_threshold::Int=12, stochastic_start::Int=1, - Exogenous_X::Matrix=if typeof(y) <: Vector - zeros(length(y), 0) - else - zeros(size(y, 1), 0) - end, + exog::Matrix=zeros(length(y), 0), + dynamic_exog_coefs::Union{Vector{<:Tuple}, Nothing}=nothing ) A Structural State Space Learning model that can have level, stochastic_level, trend, stochastic_trend, seasonal, stochastic_seasonal, outlier and Exogenous components. Each component should be specified by Booleans. @@ -55,118 +56,112 @@ mutable struct StructuralModel <: StateSpaceLearningModel X::Matrix level::Bool stochastic_level::Bool - trend::Bool - stochastic_trend::Bool + slope::Bool + stochastic_slope::Bool seasonal::Bool stochastic_seasonal::Bool + cycle::Bool + stochastic_cycle::Bool freq_seasonal::Union{Int,Vector{Int}} cycle_period::Union{Union{Int,<:AbstractFloat},Vector{Int},Vector{<:AbstractFloat}} - cycle_matrix::Vector{Matrix} - stochastic_cycle::Bool outlier::Bool - ζ_ω_threshold::Int + ζ_threshold::Int + ω_threshold::Int + ϕ_threshold::Int stochastic_start::Int n_exogenous::Int + dynamic_exog_coefs::Union{Vector{<:Tuple}, Nothing} output::Union{Vector{Output},Output,Nothing} function StructuralModel( y::Union{Vector,Matrix}; - level::Bool=true, - stochastic_level::Bool=true, - trend::Bool=true, - stochastic_trend::Bool=true, - seasonal::Bool=true, - stochastic_seasonal::Bool=true, + level::String="stochastic", + slope::String="stochastic", + seasonal::String="stochastic", + cycle::String="none", freq_seasonal::Union{Int,Vector{Int}}=12, cycle_period::Union{Union{Int,<:AbstractFloat},Vector{Int},Vector{<:AbstractFloat}}=0, - dumping_cycle::Float64=1.0, - stochastic_cycle::Bool=false, outlier::Bool=true, - ζ_ω_threshold::Int=12, + ζ_threshold::Int=12, + ω_threshold::Int=12, + ϕ_threshold::Int=12, stochastic_start::Int=1, - Exogenous_X::Matrix=if typeof(y) <: Vector - zeros(length(y), 0) - else - zeros(size(y, 1), 0) - end, + exog::Matrix=zeros(length(y), 0), + dynamic_exog_coefs::Union{Vector{<:Tuple}, Nothing}=nothing ) - n_exogenous = size(Exogenous_X, 2) - @assert !has_intercept(Exogenous_X) "Exogenous matrix must not have an intercept column" - if typeof(y) <: Vector - @assert seasonal ? length(y) > minimum(freq_seasonal) : true "Time series must be longer than the seasonal period" - else - @assert seasonal ? size(y, 1) > minimum(freq_seasonal) : true "Time series must be longer than the seasonal period" - end + n_exogenous = size(exog, 2) + + @assert !has_intercept(exog) "Exogenous matrix must not have an intercept column" @assert 1 <= stochastic_start < length(y) "stochastic_start must be greater than or equal to 1 and smaller than the length of the time series" - @assert 0 < dumping_cycle <= 1 "dumping_cycle must be greater than 0 and less than or equal to 1" - if cycle_period != 0 && !isempty(cycle_period) - if typeof(cycle_period) <: Vector - cycle_matrix = Vector{Matrix}(undef, length(cycle_period)) - for i in eachindex(cycle_period) - A = dumping_cycle * cos(2 * pi / cycle_period[i]) - B = dumping_cycle * sin(2 * pi / cycle_period[i]) - cycle_matrix[i] = [A B; -B A] + @assert level in ["deterministic", "stochastic", "none"] "level must be either deterministic, stochastic or no" + @assert slope in ["deterministic", "stochastic", "none"] "slope must be either deterministic, stochastic or no" + @assert seasonal in ["deterministic", "stochastic", "none"] "seasonal must be either deterministic, stochastic or no" + @assert cycle in ["deterministic", "stochastic", "none"] "cycle must be either deterministic, stochastic or no" + @assert seasonal != "none" ? length(y) > minimum(freq_seasonal) : true "Time series must be longer than the seasonal period if seasonal is added" + + typeof(freq_seasonal) <: Vector ? (@assert all(freq_seasonal .> 0) "Seasonal period must be greater than 0") : (@assert freq_seasonal > 0 "Seasonal period must be greater than 0") + + typeof(cycle_period) <: Vector ? (@assert all(cycle_period .>= 0) "Cycle period must be greater than or equal to 0") : (@assert cycle_period >= 0 "Cycle period must be greater than or equal to 0") + + cycle_period == 0 ? (@assert cycle == "none" "stochastic_cycle and cycle must be false if cycle_period is 0") : nothing + freq_seasonal == 0 ? (@assert seasonal == "none" "stochastic_seasonal and seasonal must be false if freq_seasonal is 0") : nothing + + if !isnothing(dynamic_exog_coefs) + @assert all(typeof(dynamic_exog_coefs[i][1]) <: Vector for i in eachindex(dynamic_exog_coefs)) "The first element of each tuple in dynamic_exog_coefs must be a vector" + @assert all(typeof(dynamic_exog_coefs[i][2]) <: String for i in eachindex(dynamic_exog_coefs)) "The second element of each tuple in dynamic_exog_coefs must be a string" + @assert all([length(dynamic_exog_coefs[i][1]) .== length(y) for i in eachindex(dynamic_exog_coefs)]) "The exogenous features that will be combined with state space components must have the same length as the time series" + @assert all(dynamic_exog_coefs[i][2] in ["level", "slope", "seasonal", "cycle"] for i in eachindex(dynamic_exog_coefs)) "The second element of each tuple in dynamic_exog_coefs must be a string that is either level, slope, seasonal or cycle" + for i in eachindex(dynamic_exog_coefs) + if dynamic_exog_coefs[i][2] == "seasonal" || dynamic_exog_coefs[i][2] == "cycle" + @assert length(dynamic_exog_coefs[i]) == 3 "The tuple in dynamic_exog_coefs must have 3 elements if the second element is seasonal or cycle" + @assert typeof(dynamic_exog_coefs[i][3]) <: Int "The third element of each tuple in dynamic_exog_coefs must be an integer if the second element is seasonal or cycle" + @assert dynamic_exog_coefs[i][3] > 1 "The third element of each tuple in dynamic_exog_coefs must be greater than 1 if the second element is seasonal or cycle" end - else - cycle_matrix = Vector{Matrix}(undef, 1) - A = dumping_cycle * cos(2 * pi / cycle_period) - B = dumping_cycle * sin(2 * pi / cycle_period) - cycle_matrix[1] = [A B; -B A] end - else - cycle_matrix = Vector{Matrix}(undef, 0) - end - - if typeof(freq_seasonal) <: Vector - @assert all(freq_seasonal .> 0) "Seasonal period must be greater than 0" - end - - if typeof(cycle_period) <: Vector - @assert all(cycle_period .>= 0) "Cycle period must be greater than or equal to 0" - end - - if cycle_period == 0 - @assert !stochastic_cycle "stochastic_cycle must be false if cycle_period is 0" end X = create_X( - level, - stochastic_level, - trend, - stochastic_trend, - seasonal, - stochastic_seasonal, + level in ["stochastic", "deterministic"], + level == "stochastic", + slope in ["stochastic", "deterministic"], + slope == "stochastic", + seasonal in ["stochastic", "deterministic"], + seasonal == "stochastic", + cycle in ["stochastic", "deterministic"], + cycle == "stochastic", freq_seasonal, - cycle_matrix, - stochastic_cycle, + cycle_period, outlier, - ζ_ω_threshold, + ζ_threshold, + ω_threshold, + ϕ_threshold, stochastic_start, - Exogenous_X, + exog, + dynamic_exog_coefs ) - # convert y format into vector or matrix of AbstractFloat - if typeof(y) <: Vector - y = convert(Vector{AbstractFloat}, y) - else - y = convert(Matrix{AbstractFloat}, y) - end + # convert y format into vector of AbstractFloat + y = convert(Vector{AbstractFloat}, y) + return new( y, X, - level, - stochastic_level, - trend, - stochastic_trend, - seasonal, - stochastic_seasonal, + level in ["stochastic", "deterministic"], + level == "stochastic", + slope in ["stochastic", "deterministic"], + slope == "stochastic", + seasonal in ["stochastic", "deterministic"], + seasonal == "stochastic", + cycle in ["stochastic", "deterministic"], + cycle == "stochastic", freq_seasonal, cycle_period, - cycle_matrix, - stochastic_cycle, outlier, - ζ_ω_threshold, + ζ_threshold, + ω_threshold, + ϕ_threshold, stochastic_start, n_exogenous, + dynamic_exog_coefs, nothing, ) end @@ -188,38 +183,39 @@ end ξ_size(T::Int, stochastic_start::Int)::Int = T - max(2, stochastic_start) """ -ζ_size(T::Int, ζ_ω_threshold::Int, stochastic_start::Int)::Int +ζ_size(T::Int, ζ_threshold::Int, stochastic_start::Int)::Int Calculates the size of ζ innovation matrix based on the input T. # Arguments - `T::Int`: Length of the original time series. - - `ζ_ω_threshold::Int`: Stabilize parameter ζ. + - `ζ_threshold::Int`: Stabilize parameter ζ. - `stochastic_start::Int`: parameter to set at which time stamp the stochastic component starts. # Returns - `Int`: Size of ζ calculated from T. """ -ζ_size(T::Int, ζ_ω_threshold::Int, stochastic_start::Int)::Int = - max(0, T - ζ_ω_threshold - max(2, stochastic_start)) +ζ_size(T::Int, ζ_threshold::Int, stochastic_start::Int)::Int = + max(0, T - ζ_threshold - max(2, stochastic_start)) """ -ω_size(T::Int, s::Int, stochastic_start::Int)::Int +ω_size(T::Int, s::Int, ω_threshold::Int, stochastic_start::Int)::Int Calculates the size of ω innovation matrix based on the input T. # Arguments - `T::Int`: Length of the original time series. - `s::Int`: Seasonal period. + - `ω_threshold::Int`: Stabilize parameter ω. - `stochastic_start::Int`: parameter to set at which time stamp the stochastic component starts. # Returns - `Int`: Size of ω calculated from T. """ -ω_size(T::Int, s::Int, ζ_ω_threshold::Int, stochastic_start::Int)::Int = - max(0, T - ζ_ω_threshold - s + 1 - max(0, max(2, stochastic_start) - s)) +ω_size(T::Int, s::Int, ω_threshold::Int, stochastic_start::Int)::Int = + max(0, T - ω_threshold - s + 1 - max(0, max(2, stochastic_start) - s)) """ o_size(T::Int, stochastic_start::Int)::Int @@ -237,61 +233,52 @@ o_size(T::Int, stochastic_start::Int)::Int o_size(T::Int, stochastic_start::Int)::Int = T - max(1, stochastic_start) + 1 """ - ϕ_size(T::Int, ζ_ω_threshold::Int, stochastic_start::Int)::Int + ϕ_size(T::Int, ϕ_threshold::Int, stochastic_start::Int)::Int Calculates the size of ϕ innovation matrix based on the input T. # Arguments - `T::Int`: Length of the original time series. - - `ζ_ω_threshold::Int`: Stabilize parameter ζ. + - `ϕ_threshold::Int`: Stabilize parameter ϕ. - `stochastic_start::Int`: parameter to set at which time stamp the stochastic component starts. # Returns - `Int`: Size of ϕ calculated from T. """ -function ϕ_size(T::Int, ζ_ω_threshold::Int, stochastic_start::Int) - ζ_ω_threshold = ζ_ω_threshold == 0 ? 1 : ζ_ω_threshold - if stochastic_start == 1 - return (2 * (T - max(2, stochastic_start) + 1) - (ζ_ω_threshold * 2)) - 2 - else - return (2 * (T - max(2, stochastic_start) + 1) - (ζ_ω_threshold * 2)) - end -end +ϕ_size(T::Int, ϕ_threshold::Int, stochastic_start::Int)::Int = (2 * (T - max(2, stochastic_start) + 1) - (max(1, ϕ_threshold) * 2)) """ - create_ξ(T::Int, steps_ahead::Int, stochastic_start::Int)::Matrix + create_ξ(T::Int, stochastic_start::Int)::Matrix Creates a matrix of innovations ξ based on the input sizes, and the desired steps ahead (this is necessary for the forecast function) # Arguments - `T::Int`: Length of the original time series. - - `steps_ahead::Int`: Number of steps ahead (for estimation purposes this should be set at 0). - `stochastic_start::Int`: parameter to set at which time stamp the stochastic component starts. # Returns - `Matrix`: Matrix of innovations ξ constructed based on the input sizes. """ -function create_ξ(T::Int, steps_ahead::Int, stochastic_start::Int)::Matrix +function create_ξ(T::Int, stochastic_start::Int)::Matrix stochastic_start = max(2, stochastic_start) - ξ_matrix = zeros(T + steps_ahead, T - stochastic_start + 1) + ξ_matrix = zeros(T, T - stochastic_start + 1) ones_indexes = findall( I -> Tuple(I)[1] - (stochastic_start - 2) > Tuple(I)[2], - CartesianIndices((T + steps_ahead, T - stochastic_start)), + CartesianIndices((T, T - stochastic_start)), ) ξ_matrix[ones_indexes] .= 1 return ξ_matrix[:, 1:(end - 1)] end """ -create_ζ(T::Int, steps_ahead::Int, ζ_ω_threshold::Int, stochastic_start::Int)::Matrix +create_ζ(T::Int, ζ_threshold::Int, stochastic_start::Int)::Matrix Creates a matrix of innovations ζ based on the input sizes, and the desired steps ahead (this is necessary for the forecast function). # Arguments - `T::Int`: Length of the original time series. - - `steps_ahead::Int`: Number of steps ahead (for estimation purposes this should be set at 0). - - `ζ_ω_threshold::Int`: Stabilize parameter ζ. + - `ζ_threshold::Int`: Stabilize parameter ζ. - `stochastic_start::Int`: parameter to set at which time stamp the stochastic component starts. # Returns @@ -299,12 +286,12 @@ create_ζ(T::Int, steps_ahead::Int, ζ_ω_threshold::Int, stochastic_start::Int) """ function create_ζ( - T::Int, steps_ahead::Int, ζ_ω_threshold::Int, stochastic_start::Int + T::Int, ζ_threshold::Int, stochastic_start::Int )::Matrix stochastic_start = max(2, stochastic_start) - ζ_matrix = zeros(T + steps_ahead, T - stochastic_start) + ζ_matrix = zeros(T, T - stochastic_start) - for t in 2:(T + steps_ahead) + for t in 2:T if t < T len = t - stochastic_start ζ_matrix[t, 1:len] .= len:-1:1 @@ -312,33 +299,32 @@ function create_ζ( ζ_matrix[t, :] .= (t - stochastic_start):-1:(t - T + 1) end end - return ζ_matrix[:, 1:(end - ζ_ω_threshold)] + return ζ_matrix[:, 1:(end - ζ_threshold)] end """ -create_ω(T::Int, freq_seasonal::Int, steps_ahead::Int, ζ_ω_threshold::Int, stochastic_start::Int)::Matrix +create_ω(T::Int, freq_seasonal::Int, ω_threshold::Int, stochastic_start::Int)::Matrix Creates a matrix of innovations ω based on the input sizes, and the desired steps ahead (this is necessary for the forecast function). # Arguments - `T::Int`: Length of the original time series. - `freq_seasonal::Int`: Seasonal period. - - `steps_ahead::Int`: Number of steps ahead (for estimation purposes this should be set at 0). - - `ζ_ω_threshold::Int`: Stabilize parameter ζ. + - `ω_threshold::Int`: Stabilize parameter ω. # Returns - `Matrix`: Matrix of innovations ω constructed based on the input sizes. """ function create_ω( - T::Int, freq_seasonal::Int, steps_ahead::Int, ζ_ω_threshold::Int, stochastic_start::Int + T::Int, freq_seasonal::Int, ω_threshold::Int, stochastic_start::Int )::Matrix stochastic_start = max(2, stochastic_start) - ω_matrix_size = T - freq_seasonal + 1 + ω_matrix_size = max(0, T - freq_seasonal + 1) stochastic_start_diff = max(0, stochastic_start - freq_seasonal) - ω_matrix = zeros(T + steps_ahead, ω_matrix_size - stochastic_start_diff) - for t in (freq_seasonal + 1):(T + steps_ahead) + ω_matrix = zeros(T, ω_matrix_size - stochastic_start_diff) + for t in (freq_seasonal + 1):T ωₜ_coefs = zeros(ω_matrix_size) Mₜ = Int(floor((t - 1) / freq_seasonal)) lag₁ = [t - j * freq_seasonal for j in 0:(Mₜ - 1)] @@ -349,104 +335,97 @@ function create_ω( -1 ω_matrix[t, :] = ωₜ_coefs[(1 + stochastic_start_diff):end] end - return ω_matrix[:, 1:(end - ζ_ω_threshold)] + return ω_matrix[:, 1:(end - ω_threshold)] end """ -create_o_matrix(T::Int, steps_ahead::Int, stochastic_start::Int)::Matrix +create_o_matrix(T::Int, stochastic_start::Int)::Matrix Creates a matrix of outliers based on the input sizes, and the desired steps ahead (this is necessary for the forecast function). # Arguments - `T::Int`: Length of the original time series. - - `steps_ahead::Int`: Number of steps ahead (for estimation purposes this should be set at 0). - `stochastic_start::Int`: parameter to set at which time stamp the stochastic component starts. # Returns - `Matrix`: Matrix of outliers constructed based on the input sizes. """ -function create_o_matrix(T::Int, steps_ahead::Int, stochastic_start::Int)::Matrix +function create_o_matrix(T::Int, stochastic_start::Int)::Matrix stochastic_start = max(1, stochastic_start) rows = stochastic_start:T cols = 1:(T - stochastic_start + 1) values = ones(length(rows)) o_matrix = sparse(rows, cols, values, T, T - stochastic_start + 1) - return vcat(o_matrix, zeros(steps_ahead, length(cols))) + return o_matrix end """ -create_ϕ(X_cycle::Matrix, T::Int, steps_ahead::Int, ζ_ω_threshold::Int, stochastic_start::Int)::Matrix +create_ϕ(c_period::Union{Int, Fl}, T::Int, ϕ_threshold::Int, stochastic_start::Int)::Matrix Creates a matrix of innovations ϕ based on the input sizes, and the desired steps ahead (this is necessary for the forecast function). # Arguments - - `X_cycle::Matrix`: deterministic Cycle matrix. + - `c_period::Union{Int, Fl}`: Cycle period. - `T::Int`: Length of the original time series. - - `steps_ahead::Int64`: Number of steps ahead (for estimation purposes this should be set at 0). - - `ζ_ω_threshold::Int`: Stabilize parameter ζ. + - `ϕ_threshold::Int`: Stabilize parameter ϕ. - `stochastic_start::Int`: parameter to set at which time stamp the stochastic component starts. # Returns - `Matrix`: Matrix of innovations ϕ constructed based on the input sizes. """ function create_ϕ( - c_matrix::Matrix, T::Int, steps_ahead::Int, ζ_ω_threshold::Int, stochastic_start::Int -)::Matrix - num_cols = 2 * (T - stochastic_start + 1) - X = Matrix{Float64}(undef, T + steps_ahead, num_cols) - - for (idx, t) in enumerate(stochastic_start:T) - X[:, 2 * (idx - 1) + 1] = vcat( - zeros(t - 1), c_matrix[1:(T - t + 1 + steps_ahead), 1] - ) - X[:, 2 * (idx - 1) + 2] = vcat( - zeros(t - 1), c_matrix[1:(T - t + 1 + steps_ahead), 2] - ) + c_period::Union{Int, Fl}, T::Int, ϕ_threshold::Int, stochastic_start::Int +)::Matrix where {Fl<:AbstractFloat} + + X = Matrix{Float64}(undef, T, 0) + λ = 2 * pi * (1:T) / c_period + + for t in max(2, stochastic_start):(T - max(1, ϕ_threshold)) # one of last two columns might be full of zeros + X_t = hcat(cos.(λ), sin.(λ)) + X_t[1:(t - 1), :] .= 0 + X = hcat(X, X_t) end - ζ_ω_threshold = ζ_ω_threshold == 0 ? 1 : ζ_ω_threshold - if stochastic_start == 1 - return X[:, 3:(end - (ζ_ω_threshold * 2))] - else - return X[:, 1:(end - (ζ_ω_threshold * 2))] - end + return round.(X, digits=5) end """ - create_deterministic_cycle_matrix(cycle_matrix::Vector{Matrix}, T::Int, steps_ahead::Int)::Vector{Matrix} +create_deterministic_seasonal(T::Int, s::Int)::Matrix - Creates a deterministic cycle matrix based on the input parameters. + Creates a matrix of deterministic seasonal components based on the input sizes. # Arguments - - `cycle_matrix::Vector{Matrix}`: Vector of cycle matrices. - `T::Int`: Length of the original time series. - - `steps_ahead::Int`: Number of steps ahead. - - # Returns - - `Vector{Matrix}`: Deterministic cycle matrix constructed based on the input parameters. -""" -function create_deterministic_cycle_matrix( - cycle_matrix::Vector{Matrix}, T::Int, steps_ahead::Int -)::Vector{Matrix} - deterministic_cycle_matrix = Vector{Matrix}(undef, length(cycle_matrix)) - for (idx, c_matrix) in enumerate(cycle_matrix) - X_cycle = Matrix{Float64}(undef, T + steps_ahead, 2) - cycle_matrix_term = c_matrix^0 - X_cycle[1, :] = cycle_matrix_term[1, :] - for t in 2:(T + steps_ahead) - cycle_matrix_term *= c_matrix - X_cycle[t, :] = cycle_matrix_term[1, :] - end - deterministic_cycle_matrix[idx] = X_cycle + - `s::Int`: Seasonal period. +""" +function create_deterministic_seasonal(T::Int, s::Int)::Matrix + γ1_matrix = zeros(T, s) + for t in 1:T + γ1_matrix[t, t % s == 0 ? s : t % s] = 1.0 end - return deterministic_cycle_matrix + return γ1_matrix +end + +""" +create_deterministic_cycle(T::Int, c_period::Union{Int, Fl})::Matrix where {Fl<:AbstractFloat} + + Creates a matrix of deterministic cycle components based on the input sizes. + + # Arguments + - `T::Int`: Length of the original time series. + - `c_period::Int`: Cycle period. +""" +function create_deterministic_cycle(T::Int, c_period::Union{Int, Fl})::Matrix where {Fl<:AbstractFloat} + λ = 2 * pi * (1:T) / c_period + cycle1_matrix = hcat(cos.(λ), sin.(λ)) + return cycle1_matrix end """ create_initial_states_Matrix( - T::Int, freq_seasonal::Union{Int, Vector{Int}}, steps_ahead::Int, level::Bool, trend::Bool, seasonal::Bool + T::Int, freq_seasonal::Union{Int, Vector{Int}}, level::Bool, trend::Bool, seasonal::Bool, cycle::Bool, cycle_period::Union{Int,Vector{Int}} )::Matrix Creates an initial states matrix based on the input parameters. @@ -454,10 +433,11 @@ end # Arguments - `T::Int`: Length of the original time series. - `freq_seasonal::Union{Int, Vector{Int}}`: Seasonal period. - - `steps_ahead::Int`: Number of steps ahead. - `level::Bool`: Flag for considering level component. - `trend::Bool`: Flag for considering trend component. - `seasonal::Bool`: Flag for considering seasonal component. + - `cycle::Bool`: Flag for considering cycle component. + - `cycle_period::Union{Int,Vector{Int}}`: Cycle period. # Returns - `Matrix`: Initial states matrix constructed based on the input parameters. @@ -466,21 +446,21 @@ end function create_initial_states_Matrix( T::Int, freq_seasonal::Union{Int,Vector{Int}}, - steps_ahead::Int, level::Bool, trend::Bool, seasonal::Bool, - deterministic_cycle_matrix::Vector{Matrix}, + cycle::Bool, + cycle_period::Union{Union{Int,<:AbstractFloat},Vector{Int},Vector{<:AbstractFloat}}, )::Matrix - initial_states_matrix = zeros(T + steps_ahead, 0) + initial_states_matrix = zeros(T, 0) if level - initial_states_matrix = hcat(initial_states_matrix, ones(T + steps_ahead, 1)) + initial_states_matrix = hcat(initial_states_matrix, ones(T, 1)) else nothing end if trend initial_states_matrix = hcat( - initial_states_matrix, vcat([0], collect(1:(T + steps_ahead - 1))) + initial_states_matrix, vcat([0], collect(1:(T - 1))) ) else nothing @@ -488,56 +468,137 @@ function create_initial_states_Matrix( if seasonal for s in freq_seasonal - γ1_matrix = zeros(T + steps_ahead, s) - for t in 1:(T + steps_ahead) - γ1_matrix[t, t % s == 0 ? s : t % s] = 1.0 - end + γ1_matrix = create_deterministic_seasonal(T, s) initial_states_matrix = hcat(initial_states_matrix, γ1_matrix) end end - if !isempty(deterministic_cycle_matrix) - for c_matrix in deterministic_cycle_matrix - initial_states_matrix = hcat(initial_states_matrix, c_matrix) + if cycle + for c_period in cycle_period + cycle1_matrix = create_deterministic_cycle(T, c_period) + initial_states_matrix = hcat(initial_states_matrix, cycle1_matrix) end end return initial_states_matrix end +""" +create_dynamic_exog_coefs_matrix(dynamic_exog_coefs::Vector{<:Tuple}, T::Int,ζ_threshold::Int, ω_threshold::Int, ϕ_threshold::Int, stochastic_start::Int)::Matrix + + Creates a matrix of combination components based on the input parameters. + + # Arguments + - `dynamic_exog_coefs::Vector{<:Tuple}`: Vector of tuples containing the combination components. + - `T::Int`: Length of the original time series. + - `ζ_threshold::Int`: Stabilize parameter ζ. + - `ω_threshold::Int`: Stabilize parameter ω. + - `ϕ_threshold::Int`: Stabilize parameter ϕ. + - `stochastic_start::Int`: parameter to set at which time stamp the stochastic component starts. + + # Returns + - `Matrix`: Matrix of combination components constructed based on the input parameters. +""" +function create_dynamic_exog_coefs_matrix(dynamic_exog_coefs::Vector{<:Tuple}, T::Int,ζ_threshold::Int, ω_threshold::Int, ϕ_threshold::Int, stochastic_start::Int)::Matrix + state_components_dict = Dict{String, Matrix}() + dynamic_exog_coefs_matrix = zeros(T, 0) + for combination in dynamic_exog_coefs + if combination[2] == "level" + haskey(state_components_dict, "level") ? nothing : state_components_dict["level"] = hcat(ones(T, 1), create_ξ(T, stochastic_start)) + key_name = "level" + elseif combination[2] == "slope" + haskey(state_components_dict, "slope") ? nothing : state_components_dict["slope"] = hcat(vcat([0], collect(1:(T - 1))), create_ζ(T, ζ_threshold, stochastic_start)) + key_name = "slope" + elseif combination[2] == "seasonal" + haskey(state_components_dict, "seasonal_$(combination[3])") ? nothing : state_components_dict["seasonal_$(combination[3])"] = hcat(create_deterministic_seasonal(T, combination[3]), create_ω(T, combination[3], ω_threshold, stochastic_start)) + key_name = "seasonal_$(combination[3])" + elseif combination[2] == "cycle" + haskey(state_components_dict, "cycle_$(combination[3])") ? nothing : state_components_dict["cycle_$(combination[3])"] = hcat(create_deterministic_cycle(T, combination[3]), create_ϕ(combination[3], T, ϕ_threshold, stochastic_start)) + key_name = "cycle_$(combination[3])" + end + dynamic_exog_coefs_matrix = hcat(dynamic_exog_coefs_matrix, combination[1] .* state_components_dict[key_name]) + end + return dynamic_exog_coefs_matrix +end + +""" +create_forecast_dynamic_exog_coefs_matrix(dynamic_exog_coefs::Vector{<:Tuple}, T::Int, steps_ahead::Int, ζ_threshold::Int, ω_threshold::Int, ϕ_threshold::Int, stochastic_start::Int)::Matrix + + Creates a matrix of combination components based on the input parameters. + + # Arguments + - `dynamic_exog_coefs::Vector{<:Tuple}`: Vector of tuples containing the combination components. + - `T::Int`: Length of the original time series. + - `steps_ahead::Int`: Steps ahead. + - `ζ_threshold::Int`: Stabilize parameter ζ. + - `ω_threshold::Int`: Stabilize parameter ω. + - `ϕ_threshold::Int`: Stabilize parameter ϕ. + - `stochastic_start::Int`: parameter to set at which time stamp the stochastic component starts. + + # Returns + - `Matrix`: Matrix of combination components constructed based on the input parameters. +""" +function create_forecast_dynamic_exog_coefs_matrix(dynamic_exog_coefs::Vector{<:Tuple}, T::Int, steps_ahead::Int, ζ_threshold::Int, ω_threshold::Int, ϕ_threshold::Int, stochastic_start::Int)::Matrix + state_components_dict = Dict{String, Matrix}() + dynamic_exog_coefs_matrix = zeros(steps_ahead, 0) + for combination in dynamic_exog_coefs + if combination[2] == "level" + haskey(state_components_dict, "level") ? nothing : state_components_dict["level"] = hcat(ones(T + steps_ahead, 1), create_ξ(T + steps_ahead, stochastic_start))[end - steps_ahead + 1:end, 1:combination[4]] + key_name = "level" + elseif combination[2] == "slope" + haskey(state_components_dict, "slope") ? nothing : state_components_dict["slope"] = hcat(vcat([0], collect(1:(T + steps_ahead - 1))), create_ζ(T + steps_ahead, ζ_threshold, stochastic_start))[end - steps_ahead + 1:end, 1:combination[4]] + key_name = "slope" + elseif combination[2] == "seasonal" + haskey(state_components_dict, "seasonal_$(combination[3])") ? nothing : state_components_dict["seasonal_$(combination[3])"] = hcat(create_deterministic_seasonal(T + steps_ahead, combination[3]), create_ω(T + steps_ahead, combination[3], ω_threshold, stochastic_start))[end - steps_ahead + 1:end, 1:combination[4]] + key_name = "seasonal_$(combination[3])" + elseif combination[2] == "cycle" + haskey(state_components_dict, "cycle_$(combination[3])") ? nothing : state_components_dict["cycle_$(combination[3])"] = hcat(create_deterministic_cycle(T + steps_ahead, combination[3]), create_ϕ(combination[3], T + steps_ahead, ϕ_threshold, stochastic_start))[end - steps_ahead + 1:end, 1:combination[4]] + key_name = "cycle_$(combination[3])" + end + dynamic_exog_coefs_matrix = hcat(dynamic_exog_coefs_matrix, combination[1] .* state_components_dict[key_name]) + end + return dynamic_exog_coefs_matrix +end + """ create_X( level::Bool, stochastic_level::Bool, - trend::Bool, - stochastic_trend::Bool, + slope::Bool, + stochastic_slope::Bool, seasonal::Bool, stochastic_seasonal::Bool, - freq_seasonal::Union{Int, Vector{Int}}, + cycle::Bool, + stochastic_cycle::Bool, + freq_seasonal::Union{Int,Vector{Int}}, + cycle_period::Union{Int,Vector{Int}}, outlier::Bool, - ζ_ω_threshold::Int, - stochastic_start::Int - Exogenous_X::Matrix{Fl}, - steps_ahead::Int=0, - Exogenous_Forecast::Matrix{Tl}=zeros(steps_ahead, size(Exogenous_X, 2)), -) where {Fl <: AbstractFloat, Tl <: AbstractFloat} + ζ_threshold::Int, + ω_threshold::Int, + ϕ_threshold::Int, + stochastic_start::Int, + exog::Matrix{Fl}, +) where {Fl<:AbstractFloat} Creates the StateSpaceLearning matrix X based on the model type and input parameters. # Arguments - `level::Bool`: Flag for considering level component. - `stochastic_level::Bool`: Flag for considering stochastic level component. - - `trend::Bool`: Flag for considering trend component. - - `stochastic_trend::Bool`: Flag for considering stochastic trend component. + - `slope::Bool`: Flag for considering slope component. + - `stochastic_slope::Bool`: Flag for considering stochastic slope component. - `seasonal::Bool`: Flag for considering seasonal component. - `stochastic_seasonal::Bool`: Flag for considering stochastic seasonal component. + - `cycle::Bool`: Flag for considering cycle component. + - `stochastic_cycle::Bool`: Flag for considering stochastic cycle component. - `freq_seasonal::Union{Int, Vector{Int}}`: Seasonal period. + - `cycle_period::Union{Int,Vector{Int}}`: Cycle period. - `outlier::Bool`: Flag for considering outlier component. - - `ζ_ω_threshold::Int`: Stabilize parameter ζ. + - `ζ_threshold::Int`: Stabilize parameter ζ. + - `ω_threshold::Int`: Stabilize parameter ω. + - `ϕ_threshold::Int`: Stabilize parameter ϕ. - `stochastic_start::Int`: parameter to set at which time stamp the stochastic component starts. - - `Exogenous_X::Matrix{Fl}`: Exogenous variables matrix. - - `steps_ahead::Int`: Number of steps ahead (default: 0). - - `Exogenous_Forecast::Matrix{Fl}`: Exogenous variables forecast matrix (default: zeros). + - `exog::Matrix{Fl}`: Exogenous variables matrix. # Returns - `Matrix`: StateSpaceLearning matrix X constructed based on the input parameters. @@ -545,64 +606,70 @@ create_X( function create_X( level::Bool, stochastic_level::Bool, - trend::Bool, - stochastic_trend::Bool, + slope::Bool, + stochastic_slope::Bool, seasonal::Bool, stochastic_seasonal::Bool, - freq_seasonal::Union{Int,Vector{Int}}, - cycle_matrix::Vector{Matrix}, + cycle::Bool, stochastic_cycle::Bool, + freq_seasonal::Union{Int,Vector{Int}}, + cycle_period::Union{Union{Int,<:AbstractFloat},Vector{Int},Vector{<:AbstractFloat}}, outlier::Bool, - ζ_ω_threshold::Int, + ζ_threshold::Int, + ω_threshold::Int, + ϕ_threshold::Int, stochastic_start::Int, - Exogenous_X::Matrix{Fl}, - steps_ahead::Int=0, - Exogenous_Forecast::Matrix{Tl}=zeros(steps_ahead, size(Exogenous_X, 2)), -) where {Fl<:AbstractFloat,Tl<:AbstractFloat} - T = size(Exogenous_X, 1) + exog::Matrix{Fl}, + dynamic_exog_coefs::Union{Vector{<:Tuple}, Nothing}, +) where {Fl<:AbstractFloat} + T = size(exog, 1) ξ_matrix = if stochastic_level - create_ξ(T, steps_ahead, stochastic_start) + create_ξ(T, stochastic_start) else - zeros(T + steps_ahead, 0) + zeros(T, 0) end - ζ_matrix = if stochastic_trend - create_ζ(T, steps_ahead, ζ_ω_threshold, stochastic_start) + ζ_matrix = if stochastic_slope + create_ζ(T, ζ_threshold, stochastic_start) else - zeros(T + steps_ahead, 0) + zeros(T, 0) end - ω_matrix = zeros(T + steps_ahead, 0) + ω_matrix = zeros(T, 0) if stochastic_seasonal for s in freq_seasonal ω_matrix = hcat( - ω_matrix, create_ω(T, s, steps_ahead, ζ_ω_threshold, stochastic_start) + ω_matrix, create_ω(T, s, ω_threshold, stochastic_start) ) end end - deterministic_cycle_matrix = create_deterministic_cycle_matrix( - cycle_matrix, T, steps_ahead - ) - ϕ_matrix = zeros(T + steps_ahead, 0) + ϕ_matrix = zeros(T, 0) if stochastic_cycle - for c_matrix in deterministic_cycle_matrix + for c_period in cycle_period ϕ_matrix = hcat( ϕ_matrix, - create_ϕ(c_matrix, T, steps_ahead, ζ_ω_threshold, stochastic_start), + create_ϕ(c_period, T, ϕ_threshold, stochastic_start), ) end end o_matrix = if outlier - create_o_matrix(T, steps_ahead, stochastic_start) + create_o_matrix(T, stochastic_start) else - zeros(T + steps_ahead, 0) + zeros(T, 0) end initial_states_matrix = create_initial_states_Matrix( - T, freq_seasonal, steps_ahead, level, trend, seasonal, deterministic_cycle_matrix + T, freq_seasonal, level, slope, seasonal, cycle, cycle_period ) + + dynamic_exog_coefs_matrix = if !isnothing(dynamic_exog_coefs) + create_dynamic_exog_coefs_matrix(dynamic_exog_coefs, T, ζ_threshold, ω_threshold, ϕ_threshold, stochastic_start) + else + zeros(T, 0) + end + return hcat( initial_states_matrix, ξ_matrix, @@ -610,51 +677,8 @@ function create_X( ω_matrix, ϕ_matrix, o_matrix, - vcat(Exogenous_X, Exogenous_Forecast), - ) -end - -""" -create_X( - model::StructuralModel, - Exogenous_X::Matrix{Fl}, - steps_ahead::Int=0, - Exogenous_Forecast::Matrix{Tl}=zeros(steps_ahead, size(Exogenous_X, 2)), -) where {Fl <: AbstractFloat, Tl <: AbstractFloat} - - Creates the StateSpaceLearning matrix X based on the model and input parameters. - - # Arguments - - `model::StructuralModel`: StructuralModel object. - - `Exogenous_X::Matrix{Fl}`: Exogenous variables matrix. - - `steps_ahead::Int`: Number of steps ahead (default: 0). - - `Exogenous_Forecast::Matrix{Fl}`: Exogenous variables forecast matrix (default: zeros). - - # Returns - - `Matrix`: StateSpaceLearning matrix X constructed based on the input parameters. -""" -function create_X( - model::StructuralModel, - Exogenous_X::Matrix{Fl}, - steps_ahead::Int=0, - Exogenous_Forecast::Matrix{Tl}=zeros(steps_ahead, size(Exogenous_X, 2)), -) where {Fl<:AbstractFloat,Tl<:AbstractFloat} - return create_X( - model.level, - model.stochastic_level, - model.trend, - model.stochastic_trend, - model.seasonal, - model.stochastic_seasonal, - model.freq_seasonal, - model.cycle_matrix, - model.stochastic_cycle, - model.outlier, - model.ζ_ω_threshold, - model.stochastic_start, - Exogenous_X, - steps_ahead, - Exogenous_Forecast, + exog, + dynamic_exog_coefs_matrix ) end @@ -671,7 +695,7 @@ function get_components_indexes(model::StructuralModel)::Dict """ function get_components_indexes(model::StructuralModel)::Dict - T = typeof(model.y) <: Vector ? length(model.y) : size(model.y, 1) + T = length(model.y) FINAL_INDEX = 0 @@ -684,7 +708,7 @@ function get_components_indexes(model::StructuralModel)::Dict initial_states_indexes = Int[] end - if model.trend + if model.slope ν1_indexes = [FINAL_INDEX + 1] initial_states_indexes = vcat(initial_states_indexes, ν1_indexes) FINAL_INDEX += length(ν1_indexes) @@ -703,8 +727,8 @@ function get_components_indexes(model::StructuralModel)::Dict end c_indexes = Vector{Int}[] - if !isempty(model.cycle_matrix) - for _ in eachindex(model.cycle_matrix) + if model.cycle + for _ in eachindex(model.cycle_period) c_i_indexes = collect((FINAL_INDEX + 1):(FINAL_INDEX + 2)) initial_states_indexes = vcat(initial_states_indexes, c_i_indexes) FINAL_INDEX += length(c_i_indexes) @@ -721,10 +745,10 @@ function get_components_indexes(model::StructuralModel)::Dict ξ_indexes = Int[] end - if model.stochastic_trend + if model.stochastic_slope ζ_indexes = collect( (FINAL_INDEX + 1):(FINAL_INDEX + ζ_size( - T, model.ζ_ω_threshold, model.stochastic_start + T, model.ζ_threshold, model.stochastic_start )), ) FINAL_INDEX += length(ζ_indexes) @@ -737,7 +761,7 @@ function get_components_indexes(model::StructuralModel)::Dict for s in model.freq_seasonal ω_s_indexes = collect( (FINAL_INDEX + 1):(FINAL_INDEX + ω_size( - T, s, model.ζ_ω_threshold, model.stochastic_start + T, s, model.ω_threshold, model.stochastic_start )), ) FINAL_INDEX += length(ω_s_indexes) @@ -749,10 +773,10 @@ function get_components_indexes(model::StructuralModel)::Dict ϕ_indexes = Vector{Int}[] if model.stochastic_cycle - for _ in eachindex(model.cycle_matrix) + for _ in eachindex(model.cycle_period) ϕ_i_indexes = collect( (FINAL_INDEX + 1):(FINAL_INDEX + ϕ_size( - T, model.ζ_ω_threshold, model.stochastic_start + T, model.ϕ_threshold, model.stochastic_start )), ) FINAL_INDEX += length(ϕ_i_indexes) @@ -773,14 +797,17 @@ function get_components_indexes(model::StructuralModel)::Dict exogenous_indexes = collect((FINAL_INDEX + 1):(FINAL_INDEX + model.n_exogenous)) + dynamic_exog_coefs_indexes = collect((FINAL_INDEX + 1):size(model.X, 2)) + components_indexes_dict = Dict( "μ1" => μ1_indexes, "ν1" => ν1_indexes, "ξ" => ξ_indexes, "ζ" => ζ_indexes, "o" => o_indexes, - "Exogenous_X" => exogenous_indexes, + "exog" => exogenous_indexes, "initial_states" => initial_states_indexes, + "dynamic_exog_coefs" => dynamic_exog_coefs_indexes, ) for (i, s) in enumerate(model.freq_seasonal) @@ -793,11 +820,11 @@ function get_components_indexes(model::StructuralModel)::Dict end end - if !isempty(model.cycle_matrix) + if model.cycle for i in eachindex(model.cycle_period) - components_indexes_dict["c1_$i"] = c_indexes[i] + components_indexes_dict["c1_$(model.cycle_period[i])"] = c_indexes[i] if model.stochastic_cycle - components_indexes_dict["ϕ_$i"] = ϕ_indexes[i] + components_indexes_dict["ϕ_$(model.cycle_period[i])"] = ϕ_indexes[i] end end end @@ -900,7 +927,7 @@ function get_model_innovations(model::StructuralModel) push!(model_innovations, "ξ") end - if model.stochastic_trend + if model.stochastic_slope push!(model_innovations, "ζ") end @@ -911,7 +938,7 @@ function get_model_innovations(model::StructuralModel) end if model.stochastic_cycle - for i in eachindex(model.cycle_period) + for i in model.cycle_period push!(model_innovations, "ϕ_$i") end end @@ -919,41 +946,485 @@ function get_model_innovations(model::StructuralModel) end """ - get_innovation_functions(model::StructuralModel, innovation::String)::Function + get_trend_decomposition(model::StructuralModel, components::Dict, slope::Vector{AbstractFloat})::Vector{AbstractFloat} - Returns the innovation function based on the input innovation string. + Returns the level component and associated innovation vectors. # Arguments - `model::StructuralModel`: StructuralModel object. - - `innovation::String`: Innovation string. - - steps_ahead::Int: Number of steps ahead. + - `components::Dict` : Components dict. + - `slope::Vector{AbstractFloat}`: Time-series of the slope component. # Returns + - `Vector{AbstractFloat}`: Time-series of the level component. """ -function get_innovation_simulation_X( - model::StructuralModel, innovation::String, steps_ahead::Int -) - if innovation == "ξ" - return create_ξ(length(model.y) + steps_ahead + 1, 0, model.stochastic_start) - elseif innovation == "ζ" - return create_ζ(length(model.y) + steps_ahead + 1, 0, 1, model.stochastic_start) - elseif occursin("ω_", innovation) - s = parse(Int, split(innovation, "_")[2]) - return create_ω(length(model.y) + steps_ahead + 1, s, 0, 1, model.stochastic_start) - elseif occursin("ϕ_", innovation) - i = parse(Int, split(innovation, "_")[2]) - deterministic_cycle_matrix = create_deterministic_cycle_matrix( - model.cycle_matrix, length(model.y), steps_ahead + 1 - ) - return create_ϕ( - deterministic_cycle_matrix[i], - length(model.y) + steps_ahead + 1, - 0, - model.ζ_ω_threshold, - model.stochastic_start, - ) +function get_trend_decomposition( + model::StructuralModel, components::Dict, slope::Vector{AbstractFloat} +)::Vector{AbstractFloat} + T = size(model.y, 1) + trend = Vector{AbstractFloat}(undef, T) + + if model.level + trend[1] = components["μ1"]["Coefs"][1] + else + trend[1] = 0.0 end + + if model.stochastic_level + ξ = vcat(zeros(max(2, model.stochastic_start) - 1), components["ξ"]["Coefs"], 0.0) + @assert length(ξ) == T + else + ξ = zeros(AbstractFloat, T) + end + + for t in 2:T + trend[t] = trend[t - 1] + slope[t] + ξ[t] + end + + return trend +end + +""" + get_slope_decomposition(model::StructuralModel, components::Dict)::Vector{AbstractFloat} + + Returns the slope component and associated innovation vectors. + + # Arguments + - `model::StructuralModel`: StructuralModel object. + - `components::Dict`: Components dict.. + + # Returns + - `Vector{AbstractFloat}`: Time-series of the slope component. + +""" +function get_slope_decomposition( + model::StructuralModel, components::Dict +)::Vector{AbstractFloat} + T = size(model.y, 1) + slope = Vector{AbstractFloat}(undef, T) + + if model.slope + slope[1] = components["ν1"]["Coefs"][1] + else + slope[1] = 0.0 + end + + if model.stochastic_slope + ζ = vcat(zeros(max(2, model.stochastic_start)), components["ζ"]["Coefs"], zeros(model.ζ_threshold)) + @assert length(ζ) == T + else + ζ = zeros(AbstractFloat, T) + end + + for t in 2:T + slope[t] = slope[t - 1] + ζ[t] + end + + return slope +end + +""" + get_seasonal_decomposition(model::StructuralModel, components::Dict, s::Int)::Vector{AbstractFloat} + + Returns the seasonality component and associated innovation vectors. + + # Arguments + - `model::StructuralModel`: StructuralModel object. + - `components::Dict`: Components dict. + - `s::Int`: Seasonal frequency. + + # Returns + - `Vector{AbstractFloat}`: Time-series of the seasonality component. + +""" +function get_seasonal_decomposition( + model::StructuralModel, components::Dict, s::Int +)::Vector{AbstractFloat} + T = size(model.y, 1) + seasonal = Vector{AbstractFloat}(undef, T) + + if model.seasonal + seasonal[1:s] = components["γ1_$(s)"]["Coefs"] + else + seasonal[1:s] = zeros(AbstractFloat, s) + end + + if model.stochastic_seasonal + ω = vcat(zeros(s - 1 + max(0, max(2, model.stochastic_start) - s)), components["ω_$(s)"]["Coefs"], zeros(model.ω_threshold)) + @assert length(ω) == T + else + ω = zeros(AbstractFloat, T) + end + + for t in (s + 1):T + seasonal[t] = seasonal[t - s] + ω[t] - ω[t - 1] + end + + return seasonal +end + +""" + get_cycle_decomposition(model::StructuralModel, components::Dict, cycle_period::Union{AbstractFloat, Int})::Tuple{Vector{AbstractFloat}, Vector{AbstractFloat}} + + Returns the cycle component and associated innovation vectors. + + # Arguments + - `model::StructuralModel`: StructuralModel object. + - `components::Dict`: Components dict. + - `cycle_period::Union{AbstractFloat, Int}`: Cycle period. + + # Returns + - `Tuple{Vector{AbstractFloat}, Vector{AbstractFloat}}`: Tuple containing the cycle component and the cycle component hat. + +""" +function get_cycle_decomposition( + model::StructuralModel, components::Dict, cycle_period::Union{AbstractFloat, Int} +)::Vector{AbstractFloat} + + T = size(model.y, 1) + cycle = Vector{AbstractFloat}(undef, T) + + if cycle_period != 0 + λ = 2 * pi * (1:T) / cycle_period + c1 = components["c1_$(cycle_period)"]["Coefs"] + + cycle[1] = (dot(c1, [cos(λ[1]), sin(λ[1])])) + + if model.stochastic_cycle + ϕ_cos = vcat(zeros(max(2, model.stochastic_start) - 1), components["ϕ_$(cycle_period)"]["Coefs"][1:2:end], zeros(max(1, model.ϕ_threshold))) + ϕ_sin = vcat(zeros(max(2, model.stochastic_start) - 1), components["ϕ_$(cycle_period)"]["Coefs"][2:2:end], zeros(max(1, model.ϕ_threshold))) + @assert length(ϕ_cos) == T + @assert length(ϕ_sin) == T + else + ϕ_cos = zeros(AbstractFloat, T) + ϕ_sin = zeros(AbstractFloat, T) + end + + for t in 2:T + ϕ_indexes = max(2, model.stochastic_start):min(t, (T - max(1, model.ϕ_threshold))) + cycle[t] = dot(c1, [cos(λ[t]), sin(λ[t])]) + + sum(ϕ_cos[i] * cos(λ[t]) + ϕ_sin[i] * sin(λ[t]) for i in eachindex(ϕ_indexes)) + end + + else + cycle = zeros(AbstractFloat, T) + end + + return cycle +end + +""" + get_model_decomposition(model::StructuralModel, components::Dict)::Dict + + Returns a dictionary with the time series state and innovations for each component. + + # Arguments + - `model::StructuralModel`: StructuralModel object. + - `components::Dict`: Components dict. + + # Returns + - `Dict`: Dictionary of time-series states and innovations. + +""" +function get_model_decomposition(model::StructuralModel, components::Dict)::Dict + freq_seasonal = model.freq_seasonal + cycle_period = model.cycle_period + model_decomposition = Dict() + + if model.slope + slope = get_slope_decomposition(model, components) + model_decomposition["slope"] = slope + end + + if model.level || model.slope + slope = model.slope ? slope : convert(Vector{AbstractFloat}, zeros(length(model.y))) + trend = get_trend_decomposition(model, components, slope) + model_decomposition["trend"] = trend + end + + if model.seasonal + for s in freq_seasonal + seasonal = get_seasonal_decomposition(model, components, s) + model_decomposition["seasonal_$s"] = seasonal + end + end + + if model.cycle + for i in cycle_period + cycle, cycle_hat = get_cycle_decomposition(model, components, i) + model_decomposition["cycle_$i"] = cycle + model_decomposition["cycle_hat_$i"] = cycle_hat + end + end + return model_decomposition +end + +""" + simulate_states( + model::StructuralModel, steps_ahead::Int, punctual::Bool, seasonal_innovation_simulation::Int + )::Vector{AbstractFloat} + + Simulates the states of the model. + + # Arguments + - `model::StructuralModel`: StructuralModel object. + - `steps_ahead::Int`: Steps ahead. + - `punctual::Bool`: Flag for considering punctual forecast. + - `seasonal_innovation_simulation::Int`: Flag for considering seasonal innovation simulation. + + # Returns + - `Vector{AbstractFloat}`: Vector of states. +""" +function simulate_states( + model::StructuralModel, steps_ahead::Int, punctual::Bool, seasonal_innovation_simulation::Int + )::Vector{AbstractFloat} + T = length(model.y) + + prediction = AbstractFloat[] + + if model.slope + slope = deepcopy(model.output.decomposition["slope"]) + start_idx = max(2, model.stochastic_start) + 1 + final_idx = T - model.ζ_threshold + if model.stochastic_slope && !punctual + if seasonal_innovation_simulation != 0 + ζ_values = vcat(zeros(start_idx - 1), model.output.components["ζ"]["Coefs"], zeros(model.ζ_threshold)) + else + ζ_values = model.output.components["ζ"]["Coefs"] + end + else + ζ_values = zeros(T) + end + stochastic_slope_set = get_stochastic_values(ζ_values, steps_ahead, T, start_idx, final_idx, seasonal_innovation_simulation) + else + slope = zeros(T) + end + + if model.level || model.slope + trend = deepcopy(model.output.decomposition["trend"]) + start_idx = max(2, model.stochastic_start) + final_idx = T - 1 + if model.stochastic_level && !punctual + if seasonal_innovation_simulation != 0 + ξ_values = vcat(zeros(start_idx - 1), model.output.components["ξ"]["Coefs"], zeros(1)) + else + ξ_values = model.output.components["ξ"]["Coefs"] + end + else + ξ_values = zeros(T) + end + stochastic_level_set = get_stochastic_values(ξ_values, steps_ahead, T, start_idx, final_idx, seasonal_innovation_simulation) + end + + if model.seasonal + seasonals = [deepcopy(model.output.decomposition["seasonal_$s"]) for s in model.freq_seasonal] + start_idx = [model.freq_seasonal[i] - 1 + max(0, max(2, model.stochastic_start) - model.freq_seasonal[i]) for i in eachindex(model.freq_seasonal)] + final_idx = [T - model.ω_threshold for _ in eachindex(model.freq_seasonal)] + if model.ω_threshold == 0 + final_ω = [model.output.components["ω_$(s)"]["Coefs"][end] for s in model.freq_seasonal] + else + final_ω = [0.0 for _ in model.freq_seasonal] + end + if model.stochastic_seasonal && !punctual + if seasonal_innovation_simulation != 0 + ω_values = [vcat(zeros(s - 1 + max(0, max(2, model.stochastic_start) - s)), model.output.components["ω_$(s)"]["Coefs"], zeros(model.ω_threshold)) for s in model.freq_seasonal] + else + ω_values = [model.output.components["ω_$(s)"]["Coefs"] for s in model.freq_seasonal] + end + else + ω_values = [zeros(T) for _ in model.freq_seasonal] + end + stochastic_seasonals_set = [vcat(final_ω[i], get_stochastic_values(ω_values[i], steps_ahead, T, start_idx[i], final_idx[i], seasonal_innovation_simulation)) for i in eachindex(model.freq_seasonal)] + end + + if model.cycle + start_idx = [max(2, model.stochastic_start) for _ in eachindex(model.cycle_period)] + final_idx = [T - max(1, model.ϕ_threshold) for _ in eachindex(model.cycle_period)] + if model.stochastic_cycle && !punctual + if seasonal_innovation_simulation != 0 + ϕ_cos_values = [vcat(zeros(max(2, model.stochastic_start) - 1), model.output.components["ϕ_$(i)"]["Coefs"][1:2:end], zeros(max(1, model.ϕ_threshold))) for i in model.cycle_period] + ϕ_sin_values = [vcat(zeros(max(2, model.stochastic_start) - 1), model.output.components["ϕ_$(i)"]["Coefs"][2:2:end], zeros(max(1, model.ϕ_threshold))) for i in model.cycle_period] + else + ϕ_cos_values = [model.output.components["ϕ_$(i)"]["Coefs"] for i in model.cycle_period] + ϕ_sin_values = [model.output.components["ϕ_$(i)"]["Coefs"][2:2:end] for i in model.cycle_period] + end + else + ϕ_cos_values = [zeros(T) for _ in model.cycle_period] + ϕ_sin_values = [zeros(T) for _ in model.cycle_period] + end + stochastic_cycles_cos_set = [get_stochastic_values(ϕ_cos_values[i], steps_ahead, T, start_idx[i], final_idx[i], seasonal_innovation_simulation) for i in eachindex(model.cycle_period)] + stochastic_cycles_sin_set = [get_stochastic_values(ϕ_sin_values[i], steps_ahead, T, start_idx[i], final_idx[i], seasonal_innovation_simulation) for i in eachindex(model.cycle_period)] + end + + if model.outlier && !punctual + start_idx = 1 + final_idx = T + outlier_values = model.output.components["o"]["Coefs"] + #stochastic_outliers_set = get_stochastic_values(outlier_values, steps_ahead, T, 1, T, seasonal_innovation_simulation) + stochastic_outliers_set = rand(outlier_values, steps_ahead) + end + + if !punctual + #stochastic_residuals_set = get_stochastic_values(model.output.ε, steps_ahead, T, 1, T, seasonal_innovation_simulation) + stochastic_residuals_set = rand(model.output.ε, steps_ahead) + end + + for t in T + 1:T + steps_ahead + + slope_t = model.slope ? slope[end] + stochastic_slope_set[t - T] : 0.0 + + trend_t = (model.level || model.slope) ? trend[end] + slope[end] + stochastic_level_set[t - T] : 0.0 + + if model.seasonal + seasonals_t = [seasonals[i][t - model.freq_seasonal[i]] + stochastic_seasonals_set[i][t - T + 1] - stochastic_seasonals_set[i][t - T] for i in eachindex(model.freq_seasonal)] + else + seasonals_t = zeros(AbstractFloat, length(model.freq_seasonal)) + end + + if model.cycle_period != 0 + cycles_t = zeros(AbstractFloat, length(model.cycle_period)) + for i in eachindex(model.cycle_period) + ϕ_cos = model.output.components["ϕ_$(model.cycle_period[i])"]["Coefs"][1:2:end] + ϕ_sin = model.output.components["ϕ_$(model.cycle_period[i])"]["Coefs"][2:2:end] + λ = 2 * pi * (1:T + steps_ahead) / model.cycle_period[i] + + cycle_t = dot(model.output.components["c1_$(model.cycle_period[i])"]["Coefs"], [cos(λ[t]), sin(λ[t])]) + + sum(ϕ_cos[j] * cos(λ[t]) + ϕ_sin[j] * sin(λ[t]) for j in eachindex(ϕ_cos)) + + sum(stochastic_cycles_cos_set[i][j] * cos(λ[t]) + stochastic_cycles_sin_set[i][j] * sin(λ[t]) for j in eachindex(stochastic_cycles_cos_set[i][1:t - T])) + cycles_t[i] = cycle_t + + end + else + cycles_t = zeros(AbstractFloat, length(model.cycle_period)) + end + + outlier_t = (model.outlier && !punctual) ? stochastic_outliers_set[t - T] : 0.0 + residuals_t = !punctual ? stochastic_residuals_set[t - T] : 0.0 + + push!(prediction, trend_t + sum(seasonals_t) + sum(cycles_t) + outlier_t + residuals_t) + model.slope ? push!(slope, slope_t) : nothing + model.level ? push!(trend, trend_t) : nothing + if model.seasonal + for i in eachindex(model.freq_seasonal) + seasonals[i] = vcat(seasonals[i], seasonals_t[i]) + end + end + + end + + return prediction +end + +""" + forecast_dynamic_exog_coefs(model::StructuralModel, steps_ahead::Int, dynamic_exog_coefs_forecasts::Vector{<:Vector})::Vector{AbstractFloat} + + Returns the prediction of the combination components terms. + + # Arguments + - `model::StructuralModel`: StructuralModel object. + - `steps_ahead::Int`: Number of steps ahead for forecasting. + - `dynamic_exog_coefs_forecasts::Vector{<:Vector}`: Vector of vectors of combination components forecasts. + + # Returns + - `Vector{AbstractFloat}`: Vector of combination components forecasts. +""" +function forecast_dynamic_exog_coefs(model::StructuralModel, steps_ahead::Int, dynamic_exog_coefs_forecasts::Vector{<:Vector})::Vector{AbstractFloat} + if !isempty(dynamic_exog_coefs_forecasts) + T = length(model.y) + dynamic_exog_coefs = Vector{Tuple}(undef, length(model.dynamic_exog_coefs)) + for i in eachindex(model.dynamic_exog_coefs) + if model.dynamic_exog_coefs[i][2] == "level" + n_coefs = 1 + ξ_size(T, model.stochastic_start) + extra_param = "" + elseif model.dynamic_exog_coefs[i][2] == "slope" + n_coefs = 1 + ζ_size(T, model.ζ_threshold, model.stochastic_start) + extra_param = "" + elseif model.dynamic_exog_coefs[i][2] == "seasonal" + n_coefs = model.dynamic_exog_coefs[i][3] + ω_size(T, model.dynamic_exog_coefs[i][3], model.ω_threshold, model.stochastic_start) + extra_param = model.dynamic_exog_coefs[i][3] + elseif model.dynamic_exog_coefs[i][2] == "cycle" + n_coefs = 2 + ϕ_size(T, model.ϕ_threshold, model.stochastic_start) + extra_param = model.dynamic_exog_coefs[i][3] + end + dynamic_exog_coefs[i] = (dynamic_exog_coefs_forecasts[i], model.dynamic_exog_coefs[i][2], extra_param, n_coefs) + end + dynamic_exog_coefs_forecasts_matrix = create_forecast_dynamic_exog_coefs_matrix(dynamic_exog_coefs, T, steps_ahead, model.ζ_threshold, model.ω_threshold, model.ϕ_threshold, model.stochastic_start) + dynamic_exog_coefs_prediction = dynamic_exog_coefs_forecasts_matrix * model.output.components["dynamic_exog_coefs"]["Coefs"] + else + dynamic_exog_coefs_prediction = zeros(steps_ahead) + end + return dynamic_exog_coefs_prediction +end + +""" + forecast(model::StructuralModel, steps_ahead::Int; Exogenous_Forecast::Matrix{Fl}=zeros(steps_ahead, 0), dynamic_exog_coefs_forecasts::Vector{<:Vector}=Vector{Vector}(undef, 0))::Vector{Dict} + + Returns a vector of dictionaries with the scenarios of each component, for each dependent time-series. + + # Arguments + - `model::StructuralModel`: StructuralModel object. + - `steps_ahead::Int`: Number of steps ahead for forecasting. + - `Exogenous_Forecast::Matrix{Fl}`: Matrix of forecasts of exogenous variables. + - `dynamic_exog_coefs_forecasts::Vector{<:Vector}`: Vector of vectors of combination components forecasts. + +""" +function forecast( + model::StructuralModel, steps_ahead::Int; + Exogenous_Forecast::Matrix{Fl}=zeros(steps_ahead, 0), + dynamic_exog_coefs_forecasts::Vector{<:Vector}=Vector{Vector}(undef, 0) +)::Vector{AbstractFloat} where {Fl<:AbstractFloat} + + states_prediction = simulate_states(model, steps_ahead, true, 0) + + @assert size(Exogenous_Forecast, 1) == steps_ahead + @assert all(length(dynamic_exog_coefs_forecasts[i]) == steps_ahead for i in eachindex(dynamic_exog_coefs_forecasts)) + !isnothing(model.dynamic_exog_coefs) ? (@assert length(dynamic_exog_coefs_forecasts) == length(model.dynamic_exog_coefs)) : nothing + (dynamic_exog_coefs_forecasts == Vector{Vector}(undef, 0)) ? (@assert isnothing(model.dynamic_exog_coefs)) : nothing + @assert size(Exogenous_Forecast, 2) == model.n_exogenous + + dynamic_exog_coefs_prediction = forecast_dynamic_exog_coefs(model, steps_ahead, dynamic_exog_coefs_forecasts) + + prediction = states_prediction + (Exogenous_Forecast * model.output.components["exog"]["Coefs"]) + dynamic_exog_coefs_prediction + + return prediction +end + +""" + simulate(model::StructuralModel, steps_ahead::Int, N_scenarios::Int; Exogenous_Forecast::Matrix{Fl}=zeros(steps_ahead, 0), dynamic_exog_coefs_forecasts::Vector{<:Vector}=Vector{Vector}(undef, 0), seasonal_innovation_simulation::Int=0, seed::Int=1234)::Matrix{AbstractFloat} + + Returns a matrix of scenarios of the states of the model. + + # Arguments + - `model::StructuralModel`: StructuralModel object. + - `steps_ahead::Int`: Number of steps ahead for forecasting. + - `N_scenarios::Int`: Number of scenarios to simulate. + - `Exogenous_Forecast::Matrix{Fl}`: Matrix of forecasts of exogenous variables. + - `dynamic_exog_coefs_forecasts::Vector{<:Vector}`: Vector of vectors of combination components forecasts. + - `seasonal_innovation_simulation::Int`: Number of seasonal innovation simulation. + - `seed::Int`: Seed for the random number generator. + + # Returns + - `Matrix{AbstractFloat}`: Matrix of scenarios of the states of the model. +""" +function simulate( + model::StructuralModel, steps_ahead::Int, N_scenarios::Int; + Exogenous_Forecast::Matrix{Fl}=zeros(steps_ahead, 0), + dynamic_exog_coefs_forecasts::Vector{<:Vector}=Vector{Vector}(undef, 0), + seasonal_innovation_simulation::Int=0, + seed::Int=1234 +)::Matrix{AbstractFloat} where {Fl<:AbstractFloat} + + scenarios = Matrix{AbstractFloat}(undef, steps_ahead, N_scenarios) + Random.seed!(seed) + for s in 1:N_scenarios + scenarios[:, s] = simulate_states(model, steps_ahead, false, seasonal_innovation_simulation) + end + + dynamic_exog_coefs_prediction = forecast_dynamic_exog_coefs(model, steps_ahead, dynamic_exog_coefs_forecasts) + scenarios .+= (Exogenous_Forecast * model.output.components["exog"]["Coefs"]) + dynamic_exog_coefs_prediction + + return scenarios end isfitted(model::StructuralModel) = isnothing(model.output) ? false : true diff --git a/src/structs.jl b/src/structs.jl index 641b729..48a3e9b 100644 --- a/src/structs.jl +++ b/src/structs.jl @@ -18,4 +18,5 @@ mutable struct Output residuals_variances::Dict valid_indexes::Vector{Int} components::Dict + decomposition::Dict end diff --git a/src/utils.jl b/src/utils.jl index 93a170a..36cd040 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -28,56 +28,14 @@ function build_components( components[key]["Values"] = X[:, components_indexes[key]] * coefs[components_indexes[key]] end - if haskey(components, "Exogenous_X") - components["Exogenous_X"]["Selected"] = findall( - i -> i != 0, components["Exogenous_X"]["Coefs"] + if haskey(components, "exog") + components["exog"]["Selected"] = findall( + i -> i != 0, components["exog"]["Coefs"] ) end return components end -""" - build_components( - X::Matrix{Tl}, coefs::Vector{Vector{Fl}}, components_indexes::Dict{String,Vector{Int}} -)::Vector{Dict} where {Fl <: AbstractFloat, Tl <: AbstractFloat} - - Constructs components dict containing values, indexes and coefficients for each component. - - # Arguments - - X::Matrix{Fl}: Input matrix. - - coefs::Vector{Vector{Fl}}: Coefficients for each time series. - - components_indexes::Dict{String, Vector{Int}}: Dictionary mapping component names to their indexes. - - # Returns - - components::Vector{Dict}: Dictionary containing components, each represented by a dictionary with keys: - - "Coefs": Coefficients for the component. - - "Indexes": Indexes associated with the component. - - "Values": Values computed from `X` and component coefficients. - -""" -function build_components( - X::Matrix{Tl}, coefs::Vector{Vector{Fl}}, components_indexes::Dict{String,Vector{Int}} -)::Vector{Dict} where {Fl<:AbstractFloat,Tl<:AbstractFloat} - components_vec = Dict[] - for coef_el in coefs - components = Dict() - for key in keys(components_indexes) - components[key] = Dict() - components[key]["Coefs"] = coef_el[components_indexes[key]] - components[key]["Indexes"] = components_indexes[key] - components[key]["Values"] = - X[:, components_indexes[key]] * coef_el[components_indexes[key]] - end - if haskey(components, "Exogenous_X") - components["Exogenous_X"]["Selected"] = findall( - i -> i != 0, components["Exogenous_X"]["Coefs"] - ) - end - push!(components_vec, components) - end - return components_vec -end - """ get_fit_and_residuals( estimation_ε::Vector{Fl}, @@ -117,52 +75,6 @@ function get_fit_and_residuals( return ε, fitted end -""" -get_fit_and_residuals( - estimation_ε::Vector{Vector{Fl}}, - coefs::Vector{Vector{Pl}}, - X::Matrix{Tl}, - valid_indexes::Vector{Int}, - T::Int, -)::Tuple{Vector{Vector{AbstractFloat}},Vector{Vector{AbstractFloat}}} where {Fl <: AbstractFloat, Pl <: AbstractFloat, Tl <: AbstractFloat} - - Builds complete residuals and fit in sample. Residuals will contain nan values for non valid indexes. Fit in Sample will be a vector of fitted values computed from input data and coefficients (non valid indexes will also be calculated via interpolation). - - # Arguments - - `estimation_ε::Vector{Vector{Fl}}`: Vector of estimation errors. - - `coefs::Vector{Vector{Pl}}`: Coefficients. - - `X::Matrix{Tl}`: Input matrix. - - `valid_indexes::Vector{Int}`: Valid indexes. - - `T::Int`: Length of the original time series. - - # Returns - - Tuple containing: - - `ε::Vector{AbstractFloat}`: Vector containing NaN values filled with estimation errors at valid indexes. - - `fitted::Vector{AbstractFloat}`: Vector of fitted values computed from input data and coefficients. - -""" -function get_fit_and_residuals( - estimation_ε::Vector{Vector{Fl}}, - coefs::Vector{Vector{Pl}}, - X::Matrix{Tl}, - valid_indexes::Vector{Int}, - T::Int, -)::Tuple{ - Vector{Vector{AbstractFloat}},Vector{Vector{AbstractFloat}} -} where {Fl<:AbstractFloat,Pl<:AbstractFloat,Tl<:AbstractFloat} - ε_vec = Vector{AbstractFloat}[] - fitted_vec = Vector{AbstractFloat}[] - - for i in eachindex(coefs) - ε = fill(NaN, T) - ε[valid_indexes] = estimation_ε[i] - fitted = X * coefs[i] - push!(ε_vec, ε) - push!(fitted_vec, fitted) - end - return ε_vec, fitted_vec -end - """ handle_missing_values( X::Matrix{Tl}, y::Vector{Fl} @@ -191,42 +103,6 @@ function handle_missing_values( return y[valid_indexes], X[valid_indexes, :], valid_indexes end -""" -handle_missing_values( - X::Matrix{Tl}, y::Matrix{Fl} -)::Tuple{Matrix{Fl},Matrix{Fl},Vector{Int}} where {Fl <: AbstractFloat, Tl <: AbstractFloat} - - Removes missing values from input data and returns the time series and matrix without missing values. - - # Arguments - - `X::Matrix{Tl}`: Input matrix. - - `y::Matrix{Fl}`: Time series. - - # Returns - - Tuple containing: - - `y::Vector{Fl}`: Time series without missing values. - - `X::Matrix{Fl}`: Input matrix without missing values. - - `valid_indexes::Vector{Int}`: Vector containing valid indexes of the time series. -""" -function handle_missing_values( - X::Matrix{Tl}, y::Matrix{Fl} -)::Tuple{Matrix{Fl},Matrix{Fl},Vector{Int}} where {Fl<:AbstractFloat,Tl<:AbstractFloat} - invalid_cartesian_indexes = unique( - vcat([i[1] for i in findall(i -> any(isnan, i), X)], findall(i -> isnan(i), y)) - ) - - invalid_indexes = Int[] - for i in invalid_cartesian_indexes - if !(i[1] in invalid_indexes) - push!(invalid_indexes, i[1]) - end - end - - valid_indexes = setdiff(1:size(y, 1), invalid_indexes) - - return y[valid_indexes, :], X[valid_indexes, :], valid_indexes -end - """ has_intercept(X::Matrix{Fl})::Bool where Fl <: AbstractFloat @@ -243,107 +119,40 @@ function has_intercept(X::Matrix{Fl})::Bool where {Fl<:AbstractFloat} end """ -fill_innovation_coefs(model::StructuralModel, T::Int, component::String, valid_indexes::Vector{Int})::Vector{AbstractFloat} +get_stochastic_values(estimated_stochastic::Vector{Fl}, steps_ahead::Int, T::Int, start_idx::Int, final_idx::Int, seasonal_innovation_simulation::Int)::Vector{AbstractFloat} where {Fl<:AbstractFloat} - Build the innovation coefficients for a given component with same length as the original time series and coefficients attributed to the first observation they are associated with. + Generates stochastic seasonal values for a given time series. # Arguments - - `model::StructuralModel`: Structural model. - - `T::Int`: Length of the original time series. - - `component::String`: Component name. - - `valid_indexes::Vector{Int}`: Valid Indexes in the time series + - `estimated_stochastic::Vector{Fl}`: Vector of estimated stochastic terms. + - `steps_ahead::Int`: Number of steps ahead to generate. + - `T::Int`: Length of the time series. + - `start_idx::Int`: Starting index of the time series. + - `final_idx::Int`: Final index of the time series. + - `seasonal_innovation_simulation::Int`: Seasonal innovation simulation. # Returns - - `Union{Vector{AbstractFloat}, Matrix{AbstractFloat}}`: Vector or matrix containing innovation coefficients for the given component. -""" -function fill_innovation_coefs( - model::StructuralModel, component::String, valid_indexes::Vector{Int} -)::Union{Vector,Matrix} - T = length(model.y) - if typeof(model.output) == Output - inov_comp = zeros(T) - for (i, idx) in enumerate(model.output.components[component]["Indexes"]) - inov_comp[findfirst(i -> i != 0, model.X[:, idx])] = model.output.components[component]["Coefs"][i] - end - inov_comp = inov_comp[valid_indexes] - else - inov_comp = zeros(T, length(model.output)) - for j in eachindex(model.output) - for (i, idx) in enumerate(model.output[j].components[component]["Indexes"]) - inov_comp[findfirst(i -> i != 0, model.X[:, idx]), j] = model.output[j].components[component]["Coefs"][i] - end - end - inov_comp = inov_comp[valid_indexes, :] - end - return inov_comp -end - + - `Vector{AbstractFloat}`: Vector of stochastic seasonal values. """ -fill_simulation!(simulation::Matrix{Tl}, MV_dist_vec::Vector{MvNormal}, o_noises::Matrix{Fl}, simulation_X::Matrix{Pl}) where {Fl <: AbstractFloat, Pl <: AbstractFloat, Tl <: AbstractFloat} - - Fill the simulation matrix with the generated values +function get_stochastic_values(estimated_stochastic::Vector{Fl}, steps_ahead::Int, T::Int, start_idx::Int, final_idx::Int, seasonal_innovation_simulation::Int)::Vector{AbstractFloat} where {Fl<:AbstractFloat} + + if seasonal_innovation_simulation != 0 + stochastic_term = Vector{AbstractFloat}(undef, steps_ahead) + for t in 1:steps_ahead - # Arguments - - `simulation::Matrix{Tl}`: Matrix to be filled with simulated values. - - `MV_dist_vec::Vector{MvNormal}`: Vector of MvNormal distributions. - - `o_noises::Matrix{Fl}`: Matrix of outliers. - - `simulation_X::Matrix{Pl}`: Matrix of simulation coefficients. -""" -function fill_simulation!( - simulation::Matrix{Tl}, - MV_dist_vec::Vector{MvNormal}, - o_noises::Matrix{Fl}, - simulation_X::Matrix{Pl}, -) where {Fl<:AbstractFloat,Pl<:AbstractFloat,Tl<:AbstractFloat} - steps_ahead, N_scenarios = size(simulation) - for s in 1:N_scenarios - sim_coefs = ones(size(simulation_X, 2)) .* NaN + # Generate potential seasonal indices + seasonal_indices = (T + t) % seasonal_innovation_simulation : seasonal_innovation_simulation : T - for i in 1:steps_ahead - rand_inovs = rand(MV_dist_vec[i]) + # Filter indices to be within the valid range + valid_indices = filter(idx -> start_idx <= idx <= final_idx, seasonal_indices) - for comp in eachindex(rand_inovs) - sim_coefs[i + (comp - 1) * steps_ahead] = rand_inovs[comp] - end + # Sample with randomness and sign flip + stochastic_term[t] = rand(estimated_stochastic[valid_indices]) * rand([1, -1]) end - - simulation[:, s] += (simulation_X * sim_coefs + o_noises[:, s]) + else + stochastic_term = rand(estimated_stochastic, steps_ahead) .* rand([1, -1], steps_ahead) end -end - -""" -fill_simulation!(simulation::Vector{Matrix{Tl}}, MV_dist_vec::Vector{MvNormal}, o_noises::Vector{Matrix{Fl}}, simulation_X::Matrix{Pl}, N_innovations::Int) where {Fl <: AbstractFloat, Pl <: AbstractFloat, Tl <: AbstractFloat} - Fill the simulation matrix with the generated values + return stochastic_term - # Arguments - - `simulation::Vector{Matrix{Tl}}`: Vector of matrices to be filled with simulated values. - - `MV_dist_vec::Vector{MvNormal}`: Vector of MvNormal distributions. - - `o_noises::Vector{Matrix{Fl}}`: Vector of matrices of outliers. - - `simulation_X::Matrix{Pl}`: Matrix of simulation coefficients. - - `N_innovations::Int`: Number of innovations. -""" -function fill_simulation!( - simulation::Vector{Matrix{Tl}}, - MV_dist_vec::Vector{MvNormal}, - o_noises::Vector{Matrix{Fl}}, - simulation_X::Matrix{Pl}, - N_innovations::Int, -) where {Fl<:AbstractFloat,Pl<:AbstractFloat,Tl<:AbstractFloat} - steps_ahead, N_scenarios = size(simulation[1]) - for j in eachindex(simulation) - for s in 1:N_scenarios - sim_coefs = ones(size(simulation_X, 2)) .* NaN - - for i in 1:steps_ahead - rand_inovs = rand(MV_dist_vec[i])[j:N_innovations:end] - - for comp in eachindex(rand_inovs) - sim_coefs[i + (comp - 1) * steps_ahead] = rand_inovs[comp] - end - end - - simulation[j][:, s] += (simulation_X * sim_coefs + o_noises[j][:, s]) - end - end end diff --git a/test/estimation_procedure.jl b/test/estimation_procedure.jl index 842aebe..d052020 100644 --- a/test/estimation_procedure.jl +++ b/test/estimation_procedure.jl @@ -57,33 +57,29 @@ end @testset "Function: fit_lasso" begin Random.seed!(1234) y = rand(10) - Exogenous_X = hcat(rand(10, 3), vcat(zeros(3), ones(1), zeros(6))) + exog = hcat(rand(10, 3), vcat(zeros(3), ones(1), zeros(6))) Basic_Structural = StateSpaceLearning.StructuralModel( y; - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=true, - stochastic_seasonal=true, + level="stochastic", + slope="stochastic", + seasonal="stochastic", freq_seasonal=2, outlier=true, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X, + ζ_threshold=0, + ω_threshold=0, + exog=exog, ) Basic_Structural_w_level = StateSpaceLearning.StructuralModel( y; - level=false, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=true, - stochastic_seasonal=true, + level="none", + slope="stochastic", + seasonal="stochastic", freq_seasonal=2, outlier=true, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X, + ζ_threshold=0, + ω_threshold=0, + exog=exog, ) components_indexes = StateSpaceLearning.get_components_indexes(Basic_Structural) @@ -117,7 +113,7 @@ end ones(size(X2, 2)); rm_average=false, ) - @test length(coefs1) == 42 + @test length(coefs1) == 34 @test length(ε1) == 10 coefs2, ε2 = StateSpaceLearning.fit_lasso( @@ -145,7 +141,7 @@ end rm_average=true, ) @test coefs3[components_indexes["o"][4]] == 0 - @test all(coefs3[components_indexes["Exogenous_X"]] .!= 0) + @test all(coefs3[components_indexes["exog"]] .!= 0) @test length(coefs3) == 43 @test length(ε3) == 10 @@ -167,33 +163,29 @@ end @testset "Function: estimation_procedure" begin Random.seed!(1234) y = rand(10) - Exogenous_X = hcat(rand(10, 3), vcat(zeros(3), ones(1), zeros(6))) + exog = hcat(rand(10, 3), vcat(zeros(3), ones(1), zeros(6))) Basic_Structural = StateSpaceLearning.StructuralModel( y; - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=true, - stochastic_seasonal=true, + level="stochastic", + slope="stochastic", + seasonal="stochastic", freq_seasonal=2, outlier=true, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X, + ζ_threshold=0, + ω_threshold=0, + exog=exog, ) Basic_Structural_w_level = StateSpaceLearning.StructuralModel( y; - level=false, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=true, - stochastic_seasonal=true, + level="none", + slope="stochastic", + seasonal="stochastic", freq_seasonal=2, outlier=true, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X, + ζ_threshold=0, + ω_threshold=0, + exog=exog, ) components_indexes = StateSpaceLearning.get_components_indexes(Basic_Structural) @@ -204,20 +196,22 @@ end X = Basic_Structural.X X2 = Basic_Structural_w_level.X + innovations_names = StateSpaceLearning.get_model_innovations(Basic_Structural) + coefs1, ε1 = StateSpaceLearning.estimation_procedure( - X, y, components_indexes, 0.1, "aic", 0.05, true, true + X, y, components_indexes, 0.1, "aic", 0.05, true, true, innovations_names ) @test length(coefs1) == 43 @test length(ε1) == 10 coefs1, ε1 = StateSpaceLearning.estimation_procedure( - X2, y, components_indexes2, 0.1, "aic", 0.05, true, true + X2, y, components_indexes2, 0.1, "aic", 0.05, true, true, innovations_names ) - @test length(coefs1) == 42 + @test length(coefs1) == 34 @test length(ε1) == 10 coefs2, ε2 = StateSpaceLearning.estimation_procedure( - X, y, components_indexes, 0.1, "aic", 0.05, true, false + X, y, components_indexes, 0.1, "aic", 0.05, true, false, innovations_names ) @test length(coefs2) == 43 @test length(ε2) == 10 @@ -225,47 +219,43 @@ end end @testset "Function: get_dummy_indexes" begin - Exogenous_X1 = hcat(rand(10, 3), vcat(zeros(3), ones(1), zeros(6))) - Exogenous_X2 = hcat(rand(10, 3)) + exog1 = hcat(rand(10, 3), vcat(zeros(3), ones(1), zeros(6))) + exog2 = hcat(rand(10, 3)) - dummy_indexes1 = StateSpaceLearning.get_dummy_indexes(Exogenous_X1) + dummy_indexes1 = StateSpaceLearning.get_dummy_indexes(exog1) @test dummy_indexes1 == [4] - dummy_indexes2 = StateSpaceLearning.get_dummy_indexes(Exogenous_X2) + dummy_indexes2 = StateSpaceLearning.get_dummy_indexes(exog2) @test dummy_indexes2 == [] end @testset "Function: get_outlier_duplicate_columns" begin Random.seed!(1234) y = rand(10) - Exogenous_X = hcat(rand(10, 3), vcat(zeros(3), ones(1), zeros(6))) - Exogenous_X2 = rand(10, 3) + exog = hcat(rand(10, 3), vcat(zeros(3), ones(1), zeros(6))) + exog2 = rand(10, 3) Basic_Structural = StateSpaceLearning.StructuralModel( y; - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=true, - stochastic_seasonal=true, + level="stochastic", + slope="stochastic", + seasonal="stochastic", freq_seasonal=2, outlier=true, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X, + ζ_threshold=0, + ω_threshold=0, + exog=exog, ) Basic_Structural_w_out = StateSpaceLearning.StructuralModel( y; - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=true, - stochastic_seasonal=true, + level="stochastic", + slope="stochastic", + seasonal="stochastic", freq_seasonal=2, outlier=true, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X2, + ζ_threshold=0, + ω_threshold=0, + exog=exog2, ) components_indexes = StateSpaceLearning.get_components_indexes(Basic_Structural) diff --git a/test/fit_forecast.jl b/test/fit_forecast.jl index 06827c9..e718e1a 100644 --- a/test/fit_forecast.jl +++ b/test/fit_forecast.jl @@ -1,4 +1,4 @@ -@testset "Function: fit_model" begin +@testset "Function: fit!" begin y1 = rand(100) y2 = rand(100) y2[10:20] .= NaN @@ -11,7 +11,8 @@ @test length(model1.output.coefs) == 375 @test length(model1.output.valid_indexes) == 100 @test length(model1.output.residuals_variances) == 4 - @test length(keys(model1.output.components)) == 9 + @test length(keys(model1.output.components)) == 10 + @test length(keys(model1.output.decomposition)) == 3 model2 = StateSpaceLearning.StructuralModel(y2) StateSpaceLearning.fit!(model2) @@ -21,256 +22,6 @@ @test length(model2.output.coefs) == 375 @test length(model2.output.valid_indexes) == 89 @test length(model2.output.residuals_variances) == 4 - @test length(keys(model2.output.components)) == 9 - - model3 = StateSpaceLearning.StructuralModel(rand(100, 3)) - StateSpaceLearning.fit!(model3) - - @test length(model3.output) == 3 - for i in eachindex(model3.output) - @test length(model3.output[i].ε) == 100 - @test length(model3.output[i].fitted) == 100 - @test length(model3.output[i].coefs) == 375 - @test length(model3.output[i].valid_indexes) == 100 - @test length(model3.output[i].residuals_variances) == 4 - @test length(keys(model3.output[i].components)) == 9 - end -end - -@testset "Function: forecast" begin - y1 = rand(100) - y2 = rand(100) - y2[10:20] .= NaN - - model1 = StateSpaceLearning.StructuralModel(y1) - StateSpaceLearning.fit!(model1) - @test length(StateSpaceLearning.forecast(model1, 10)) == 10 - - model2 = StateSpaceLearning.StructuralModel(y2; Exogenous_X=rand(100, 3)) - StateSpaceLearning.fit!(model2) - @test length(StateSpaceLearning.forecast(model2, 10; Exogenous_Forecast=rand(10, 3))) == - 10 - - @test_throws AssertionError StateSpaceLearning.forecast( - model1, 10; Exogenous_Forecast=rand(5, 3) - ) - @test_throws AssertionError StateSpaceLearning.forecast(model2, 10) - @test_throws AssertionError StateSpaceLearning.forecast( - model2, 10; Exogenous_Forecast=rand(5, 3) - ) - - y3 = [ - 4.718, - 4.77, - 4.882, - 4.859, - 4.795, - 4.905, - 4.997, - 4.997, - 4.912, - 4.779, - 4.644, - 4.77, - 4.744, - 4.836, - 4.948, - 4.905, - 4.828, - 5.003, - 5.135, - 5.135, - 5.062, - 4.89, - 4.736, - 4.941, - 4.976, - 5.01, - 5.181, - 5.093, - 5.147, - 5.181, - 5.293, - 5.293, - 5.214, - 5.087, - 4.983, - 5.111, - 5.141, - 5.192, - 5.262, - 5.198, - 5.209, - 5.384, - 5.438, - 5.488, - 5.342, - 5.252, - 5.147, - 5.267, - 5.278, - 5.278, - 5.463, - 5.459, - 5.433, - 5.493, - 5.575, - 5.605, - 5.468, - 5.351, - 5.192, - 5.303, - 5.318, - 5.236, - 5.459, - 5.424, - 5.455, - 5.575, - 5.71, - 5.68, - 5.556, - 5.433, - 5.313, - 5.433, - 5.488, - 5.451, - 5.587, - 5.594, - 5.598, - 5.752, - 5.897, - 5.849, - 5.743, - 5.613, - 5.468, - 5.627, - 5.648, - 5.624, - 5.758, - 5.746, - 5.762, - 5.924, - 6.023, - 6.003, - 5.872, - 5.723, - 5.602, - 5.723, - 5.752, - 5.707, - 5.874, - 5.852, - 5.872, - 6.045, - 6.142, - 6.146, - 6.001, - 5.849, - 5.72, - 5.817, - 5.828, - 5.762, - 5.891, - 5.852, - 5.894, - 6.075, - 6.196, - 6.224, - 6.001, - 5.883, - 5.736, - 5.82, - 5.886, - 5.834, - 6.006, - 5.981, - 6.04, - 6.156, - 6.306, - 6.326, - 6.137, - 6.008, - 5.891, - 6.003, - 6.033, - 5.968, - 6.037, - 6.133, - 6.156, - 6.282, - 6.432, - 6.406, - 6.23, - 6.133, - 5.966, - 6.068, - ] - model3 = StateSpaceLearning.StructuralModel(y3) - StateSpaceLearning.fit!(model3) - forecast3 = trunc.(StateSpaceLearning.forecast(model3, 18); digits=3) - @assert forecast3 == [ - 6.11, - 6.082, - 6.221, - 6.19, - 6.197, - 6.328, - 6.447, - 6.44, - 6.285, - 6.163, - 6.026, - 6.142, - 6.166, - 6.138, - 6.278, - 6.246, - 6.253, - 6.384, - ] - - model4 = StateSpaceLearning.StructuralModel(y3; freq_seasonal=[12, 36]) - StateSpaceLearning.fit!(model4) - forecast4 = trunc.(StateSpaceLearning.forecast(model4, 18); digits=3) - - @test length(forecast4) == 18 -end - -@testset "Function: simulate" begin - y1 = rand(100) - y2 = rand(100) - y2[10:20] .= NaN - - model1 = StateSpaceLearning.StructuralModel(y1) - StateSpaceLearning.fit!(model1) - @test size(StateSpaceLearning.simulate(model1, 10, 100)) == (10, 100) - - @test size( - StateSpaceLearning.simulate(model1, 10, 100; seasonal_innovation_simulation=10) - ) == (10, 100) - - model2 = StateSpaceLearning.StructuralModel(y2; Exogenous_X=rand(100, 3)) - StateSpaceLearning.fit!(model2) - @test size( - StateSpaceLearning.simulate(model2, 10, 100; Exogenous_Forecast=rand(10, 3)) - ) == (10, 100) - - model3 = StateSpaceLearning.StructuralModel(rand(100, 3)) - StateSpaceLearning.fit!(model3) - simulations = StateSpaceLearning.simulate(model3, 10, 100) - - # test assert error - @test_throws AssertionError StateSpaceLearning.simulate( - model3, 10, 100; seasonal_innovation_simulation=10 - ) - simulations2 = StateSpaceLearning.simulate( - model3, 10, 100; seasonal_innovation_simulation=3 - ) - - @test length(simulations) == 3 - @test length(simulations2) == 3 - for i in eachindex(model3.output) - @test size(simulations[i]) == (10, 100) - @test size(simulations2[i]) == (10, 100) - end -end + @test length(keys(model2.output.components)) == 10 + @test length(keys(model2.output.decomposition)) == 3 +end \ No newline at end of file diff --git a/test/models/structural_model.jl b/test/models/structural_model.jl index f692c54..5c65c0e 100644 --- a/test/models/structural_model.jl +++ b/test/models/structural_model.jl @@ -5,7 +5,7 @@ model2 = StateSpaceLearning.StructuralModel(y1; freq_seasonal=[3, 10]) model3 = StateSpaceLearning.StructuralModel(y1; cycle_period=[3, 10.2]) model4 = StateSpaceLearning.StructuralModel( - y1; cycle_period=[3, 10.2], dumping_cycle=0.5 + y1; cycle_period=[3, 10.2] ) @test typeof(model1) == StateSpaceLearning.StructuralModel @@ -20,76 +20,41 @@ @test_throws AssertionError StateSpaceLearning.StructuralModel(y1; freq_seasonal=1000) - @test_throws AssertionError StateSpaceLearning.StructuralModel( - y1; cycle_period=[3, 10.2], dumping_cycle=-1.0 - ) - @test_throws AssertionError StateSpaceLearning.StructuralModel( - y1; cycle_period=[3, 10.2], dumping_cycle=2.0 - ) - exog_error = ones(100, 3) @test_throws AssertionError StateSpaceLearning.StructuralModel( - y1; Exogenous_X=exog_error + y1; exog=exog_error ) end -@testset "create_deterministic_cycle_matrix" begin - cycle_matrix = Vector{Matrix}(undef, 1) - A = 1 * cos(2 * pi / 12) - B = 1 * sin(2 * pi / 12) - cycle_matrix[1] = [A B; -B A] - det_matrix1 = StateSpaceLearning.create_deterministic_cycle_matrix(cycle_matrix, 5, 0) - exp_det_matrix1 = [ - 1.0 0.0 - 0.866025 0.5 - 0.5 0.866025 - 2.77556e-16 1.0 - -0.5 0.866025 - ] +@testset "create deterministic matrices" begin + X1 = StateSpaceLearning.create_deterministic_cycle(100, 12) + @test size(X1) == (100, 2) - n, p = size(det_matrix1[1]) - for i in 1:n - for j in 1:p - @test isapprox(det_matrix1[1][i, j], exp_det_matrix1[i, j]; atol=1e-6) - end - end + X2 = StateSpaceLearning.create_deterministic_seasonal(100, 12) + @test size(X2) == (100, 12) - cycle_period = [3, 12.2] - cycle_matrix = Vector{Matrix}(undef, length(cycle_period)) - for i in eachindex(cycle_period) - A = 1 * cos(2 * pi / cycle_period[i]) - B = 1 * sin(2 * pi / cycle_period[i]) - cycle_matrix[i] = [A B; -B A] - end + X3 = StateSpaceLearning.create_initial_states_Matrix( + 100, + [12, 20], + true, + true, + true, + true, + [3, 10.2]) - #### - - det_matrix2 = StateSpaceLearning.create_deterministic_cycle_matrix(cycle_matrix, 5, 0) - exp_det_matrix2 = [ - [ - 1.0 0.0 - -0.5 0.866025 - -0.5 -0.866025 - 1.0 -6.10623e-16 - -0.5 0.866025 - ], - [ - 1.0 0.0 - 0.870285 0.492548 - 0.514793 0.857315 - 0.0257479 0.999668 - -0.469977 0.882679 - ], - ] + @test size(X3) == (100, 38) - n, p = 5, 2 - for h in eachindex(det_matrix2) - for i in 1:n - for j in 1:p - @test isapprox(det_matrix2[h][i, j], exp_det_matrix2[h][i, j]; atol=1e-6) - end - end - end + X4 = StateSpaceLearning.create_initial_states_Matrix( + 100, + 12, + true, + true, + true, + false, + 3) + + @test size(X4) == (100, 14) + end @testset "Innovation matrices" begin @@ -99,8 +64,8 @@ end @test StateSpaceLearning.ω_size(10, 2, 0, 1) == 9 @test StateSpaceLearning.ω_size(10, 2, 2, 1) == 7 @test StateSpaceLearning.o_size(10, 1) == 10 - @test StateSpaceLearning.ϕ_size(10, 0, 1) == 14 - @test StateSpaceLearning.ϕ_size(10, 2, 1) == 12 + @test StateSpaceLearning.ϕ_size(10, 0, 1) == 16 + @test StateSpaceLearning.ϕ_size(10, 2, 1) == 14 @test StateSpaceLearning.ξ_size(10, 5) == 5 @test StateSpaceLearning.ζ_size(10, 2, 5) == 3 @@ -110,8 +75,8 @@ end @test StateSpaceLearning.o_size(10, 6) == 5 @test StateSpaceLearning.ϕ_size(10, 0, 5) == 10 - X_ξ1 = StateSpaceLearning.create_ξ(5, 0, 1) - X_ξ2 = StateSpaceLearning.create_ξ(5, 2, 1) + X_ξ1 = StateSpaceLearning.create_ξ(5, 1) + X_ξ2 = StateSpaceLearning.create_ξ(5, 3) @test X_ξ1 == [ 0.0 0.0 0.0 @@ -122,19 +87,6 @@ end ] @test X_ξ2 == [ - 0.0 0.0 0.0 - 1.0 0.0 0.0 - 1.0 1.0 0.0 - 1.0 1.0 1.0 - 1.0 1.0 1.0 - 1.0 1.0 1.0 - 1.0 1.0 1.0 - ] - - X_ξ3 = StateSpaceLearning.create_ξ(5, 0, 3) - X_ξ4 = StateSpaceLearning.create_ξ(5, 2, 3) - - @test X_ξ3 == [ 0.0 0.0 0.0 0.0 1.0 0.0 @@ -142,19 +94,9 @@ end 1.0 1.0 ] - @test X_ξ4 == [ - 0.0 0.0 - 0.0 0.0 - 1.0 0.0 - 1.0 1.0 - 1.0 1.0 - 1.0 1.0 - 1.0 1.0 - ] - - X_ζ1 = StateSpaceLearning.create_ζ(5, 0, 0, 1) - X_ζ2 = StateSpaceLearning.create_ζ(5, 2, 0, 1) - X_ζ3 = StateSpaceLearning.create_ζ(5, 2, 2, 1) + X_ζ1 = StateSpaceLearning.create_ζ(5, 0, 1) + X_ζ2 = StateSpaceLearning.create_ζ(6, 2, 1) + X_ζ3 = StateSpaceLearning.create_ζ(5, 2, 3) @test X_ζ1 == [ 0.0 0.0 0.0 @@ -165,412 +107,192 @@ end ] @test X_ζ2 == [ - 0.0 0.0 0.0 - 0.0 0.0 0.0 - 1.0 0.0 0.0 - 2.0 1.0 0.0 - 3.0 2.0 1.0 - 4.0 3.0 2.0 - 5.0 4.0 3.0 + 0.0 0.0 + 0.0 0.0 + 1.0 0.0 + 2.0 1.0 + 3.0 2.0 + 4.0 3.0 ] - @test X_ζ3 == reshape( - [ - 0.0 - 0.0 - 1.0 - 2.0 - 3.0 - 4.0 - 5.0 - ], - 7, - 1, - ) - - X_ζ4 = StateSpaceLearning.create_ζ(6, 2, 2, 3) - - @test X_ζ4 == reshape( - [ - 0.0 - 0.0 - 0.0 - 1.0 - 2.0 - 3.0 - 4.0 - 5.0 - ], - 8, - 1, - ) + @test X_ζ3 == zeros(5,0) - X_ω1 = StateSpaceLearning.create_ω(5, 2, 0, 0, 1) - X_ω2 = StateSpaceLearning.create_ω(5, 2, 2, 0, 1) + X_ω1 = StateSpaceLearning.create_ω(5, 2, 0, 1) + X_ω2 = StateSpaceLearning.create_ω(5, 2, 2, 1) + X_ω3 = StateSpaceLearning.create_ω(5, 2, 0, 3) @test X_ω1 == [ - 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 - -1.0 1.0 0.0 0.0 - 0.0 -1.0 1.0 0.0 - -1.0 1.0 -1.0 1.0 + 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 + -1.0 1.0 0.0 0.0 + 0.0 -1.0 1.0 0.0 + -1.0 1.0 -1.0 1.0 ] @test X_ω2 == [ - 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 - -1.0 1.0 0.0 0.0 - 0.0 -1.0 1.0 0.0 - -1.0 1.0 -1.0 1.0 - 0.0 -1.0 1.0 -1.0 - -1.0 1.0 -1.0 1.0 + 0.0 0.0 + 0.0 0.0 + -1.0 1.0 + 0.0 -1.0 + -1.0 1.0 ] - X_ω3 = StateSpaceLearning.create_ω(5, 2, 0, 0, 3) - @test X_ω3 == [ - 0.0 0.0 0.0 - 0.0 0.0 0.0 - 1.0 0.0 0.0 - -1.0 1.0 0.0 - 1.0 -1.0 1.0 + 0.0 0.0 0.0 + 0.0 0.0 0.0 + 1.0 0.0 0.0 + -1.0 1.0 0.0 + 1.0 -1.0 1.0 ] - X_o1 = StateSpaceLearning.create_o_matrix(3, 0, 1) - X_o2 = StateSpaceLearning.create_o_matrix(3, 2, 1) - X_o3 = StateSpaceLearning.create_o_matrix(3, 0, 2) + X_o1 = StateSpaceLearning.create_o_matrix(3, 1) + X_o3 = StateSpaceLearning.create_o_matrix(3, 2) @test X_o1 == Matrix(1.0 * I, 3, 3) - @test X_o2 == vcat(Matrix(1.0 * I, 3, 3), zeros(2, 3)) - @test X_o3 == Matrix(1.0 * I, 3, 3)[:, 2:3] - - cycle_matrix = Vector{Matrix}(undef, 1) - A = 1 * cos(2 * pi / 12) - B = 1 * sin(2 * pi / 12) - cycle_matrix[1] = [A B; -B A] - det_matrix1 = StateSpaceLearning.create_deterministic_cycle_matrix(cycle_matrix, 5, 0) - X_ϕ1 = StateSpaceLearning.create_ϕ(det_matrix1[1], 5, 0, 0, 1) - - @test all( - isapprox.( - X_ϕ1, - [ - 0.0 0.0 0.0 0.0 0.0 0.0 - 1.0 0.0 0.0 0.0 0.0 0.0 - 0.866025 0.5 1.0 0.0 0.0 0.0 - 0.5 0.866025 0.866025 0.5 1.0 0.0 - 2.77556e-16 1.0 0.5 0.866025 0.866025 0.5 - ], - atol=1e-6, - ), - ) -end - -@testset "Initial State Matrix" begin - X1 = StateSpaceLearning.create_initial_states_Matrix( - 5, 2, 0, true, true, true, Vector{Matrix}(undef, 0) - ) - X2 = StateSpaceLearning.create_initial_states_Matrix( - 5, 2, 2, true, true, true, Vector{Matrix}(undef, 0) - ) - - @test X1 == [ - 1.0 0.0 1.0 0.0 - 1.0 1.0 0.0 1.0 - 1.0 2.0 1.0 0.0 - 1.0 3.0 0.0 1.0 - 1.0 4.0 1.0 0.0 + @test X_o3 == [ 0.0 0.0 + 1.0 0.0 + 0.0 1.0] + + X_ϕ1 = StateSpaceLearning.create_ϕ(3, 5, 0, 1) + X_ϕ2 = StateSpaceLearning.create_ϕ(3, 5, 3, 1) + X_ϕ3 = StateSpaceLearning.create_ϕ(3, 5, 0, 2) + + @test X_ϕ1 == [ + 0.0 0.0 0.0 0.0 0.0 0.0 + -0.5 -0.86603 0.0 0.0 0.0 0.0 + 1.0 -0.0 1.0 -0.0 0.0 0.0 + -0.5 0.86603 -0.5 0.86603 -0.5 0.86603 + -0.5 -0.86603 -0.5 -0.86603 -0.5 -0.86603 ] - @test X2 == [ - 1.0 0.0 1.0 0.0 - 1.0 1.0 0.0 1.0 - 1.0 2.0 1.0 0.0 - 1.0 3.0 0.0 1.0 - 1.0 4.0 1.0 0.0 - 1.0 5.0 0.0 1.0 - 1.0 6.0 1.0 0.0 + @test X_ϕ2 == [ + 0.0 0.0 + -0.5 -0.86603 + 1.0 -0.0 + -0.5 0.86603 + -0.5 -0.86603 ] - X3 = StateSpaceLearning.create_initial_states_Matrix( - 5, 2, 0, true, true, false, Vector{Matrix}(undef, 0) - ) - X4 = StateSpaceLearning.create_initial_states_Matrix( - 5, 2, 2, true, true, false, Vector{Matrix}(undef, 0) - ) - - @test X3 == [ - 1.0 0.0 - 1.0 1.0 - 1.0 2.0 - 1.0 3.0 - 1.0 4.0 + @test X_ϕ3 == [ + 0.0 0.0 0.0 0.0 0.0 0.0 + -0.5 -0.86603 0.0 0.0 0.0 0.0 + 1.0 -0.0 1.0 -0.0 0.0 0.0 + -0.5 0.86603 -0.5 0.86603 -0.5 0.86603 + -0.5 -0.86603 -0.5 -0.86603 -0.5 -0.86603 ] +end - @test X4 == [ - 1.0 0.0 - 1.0 1.0 - 1.0 2.0 - 1.0 3.0 - 1.0 4.0 - 1.0 5.0 - 1.0 6.0 - ] - - X5 = StateSpaceLearning.create_initial_states_Matrix( - 5, 2, 0, true, false, false, Vector{Matrix}(undef, 0) - ) - X6 = StateSpaceLearning.create_initial_states_Matrix( - 5, 2, 2, true, false, false, Vector{Matrix}(undef, 0) - ) - - @test X5 == ones(5, 1) - @test X6 == ones(7, 1) - - X7 = StateSpaceLearning.create_initial_states_Matrix( - 5, 2, 0, false, true, false, Vector{Matrix}(undef, 0) - ) - X8 = StateSpaceLearning.create_initial_states_Matrix( - 5, 2, 2, false, true, false, Vector{Matrix}(undef, 0) - ) - - @test X7 == [0.0; 1.0; 2.0; 3.0; 4.0][:, :] - @test X8 == [0.0; 1.0; 2.0; 3.0; 4.0; 5.0; 6.0][:, :] +@testset "dynamic_exog_coefs" begin - cycle_matrix = Vector{Matrix}(undef, 1) - A = 1 * cos(2 * pi / 12) - B = 1 * sin(2 * pi / 12) - cycle_matrix[1] = [A B; -B A] - det_matrix1 = StateSpaceLearning.create_deterministic_cycle_matrix(cycle_matrix, 5, 0) - det_matrix2 = StateSpaceLearning.create_deterministic_cycle_matrix(cycle_matrix, 5, 2) + dynamic_exog_coefs = [(collect(1:5), "level"), (collect(1:5), "slope"), (collect(1:5), "seasonal", 2), (collect(1:5), "cycle", 3)] - X9 = StateSpaceLearning.create_initial_states_Matrix( - 5, 2, 0, true, true, true, det_matrix1 - ) - X10 = StateSpaceLearning.create_initial_states_Matrix( - 5, 2, 2, true, true, true, det_matrix2 - ) + X = StateSpaceLearning.create_dynamic_exog_coefs_matrix(dynamic_exog_coefs, 5, 0, 0, 0, 1) + @test size(X) == (5, 22) - @test all( - isapprox.( - X9, - [ - 1.0 0.0 1.0 0.0 1.0 0.0 - 1.0 1.0 0.0 1.0 0.866025 0.5 - 1.0 2.0 1.0 0.0 0.5 0.866025 - 1.0 3.0 0.0 1.0 2.77556e-16 1.0 - 1.0 4.0 1.0 0.0 -0.5 0.866025 - ], - atol=1e-6, - ), - ) - - @test all( - isapprox.( - X10, - [ - 1.0 0.0 1.0 0.0 1.0 0.0 - 1.0 1.0 0.0 1.0 0.866025 0.5 - 1.0 2.0 1.0 0.0 0.5 0.866025 - 1.0 3.0 0.0 1.0 2.77556e-16 1.0 - 1.0 4.0 1.0 0.0 -0.5 0.866025 - 1.0 5.0 0.0 1.0 -0.866025 0.5 - 1.0 6.0 1.0 0.0 -1.0 5.55112e-16 - ], - atol=1e-6, - ), - ) + dynamic_exog_coefs = [(collect(6:7), "level", "", 4), (collect(6:7), "slope", "", 4), (collect(6:7), "seasonal", 2, 7), (collect(6:7), "cycle", 3, 8)] + X_f = StateSpaceLearning.create_forecast_dynamic_exog_coefs_matrix(dynamic_exog_coefs, 5, 2, 0, 0, 0, 1) + @test size(X_f) == (2, 23) end @testset "Create X matrix" begin - Exogenous_X1 = rand(5, 3) - Exogenous_X2 = zeros(5, 0) - - Exogenous_forecast1 = rand(2, 3) - - cycle_matrix = Vector{Matrix}(undef, 0) - stochastic_cycle = false - stochastic_start = 1 - - param_combination = [ - Any[true, 0, 0], - Any[true, 2, 0], - Any[true, 2, 2], - Any[false, 0, 0], - Any[false, 2, 0], - Any[false, 2, 2], - ] + exog1 = rand(5, 3) + dynamic_exog_coefs = [(collect(1:5), "level"), (collect(1:5), "slope"), (collect(1:5), "seasonal", 2), (collect(1:5), "cycle", 3)] + + X1 = StateSpaceLearning.create_X( + true, + true, + true, + true, + true, + true, + true, + true, + 3, + 3, + true, + 0, + 0, + 0, + 1, + exog1, + dynamic_exog_coefs + ) + + exog2 = zeros(10, 3) + dynamic_exog_coefs2 = [(collect(1:10), "level"), (collect(1:10), "slope"), (collect(1:10), "seasonal", 2), (collect(1:10), "cycle", 3)] + + X2 = StateSpaceLearning.create_X( + true, + true, + true, + true, + true, + true, + true, + true, + 3, + 3, + true, + 3, + 2, + 4, + 1, + exog2, + dynamic_exog_coefs2 + ) + + @test size(X1) == (5, 52) + @test size(X2) == (10, 85) - size_vec1 = [ - (5, 22), - (5, 18), - (7, 18), - (5, 17), - (5, 13), - (7, 13), - (5, 12), - (5, 12), - (7, 12), - (5, 7), - (5, 7), - (7, 7), - (5, 16), - (5, 14), - (7, 14), - (5, 11), - (5, 9), - (7, 9), - (5, 13), - (5, 11), - (7, 11), - (5, 8), - (5, 6), - (7, 6), - ] - size_vec2 = [ - (5, 19), - (5, 15), - (7, 15), - (5, 14), - (5, 10), - (7, 10), - (5, 9), - (5, 9), - (7, 9), - (5, 4), - (5, 4), - (7, 4), - (5, 13), - (5, 11), - (7, 11), - (5, 8), - (5, 6), - (7, 6), - (5, 10), - (5, 8), - (7, 8), - (5, 5), - (5, 3), - (7, 3), - ] - counter = 1 - for args in [ - [true, true, true, true, true, true, 2, cycle_matrix, stochastic_cycle, true, 12], - [ - true, - true, - false, - false, - false, - false, - 2, - cycle_matrix, - stochastic_cycle, - true, - 12, - ], - [true, true, true, true, false, false, 2, cycle_matrix, stochastic_cycle, true, 12], - [ - true, - false, - true, - true, - false, - false, - 2, - cycle_matrix, - stochastic_cycle, - true, - 12, - ], - ] - args = [x in [0, 1] ? Bool(x) : x for x in args] - for param in param_combination - args[end - 1] = param[1] - args[end] = param[2] - if param[3] != 0 - X1 = StateSpaceLearning.create_X( - args..., stochastic_start, Exogenous_X1, param[3], Exogenous_forecast1 - ) - else - X1 = StateSpaceLearning.create_X( - args..., stochastic_start, Exogenous_X1, param[3] - ) - end - X2 = StateSpaceLearning.create_X( - args..., stochastic_start, Exogenous_X2, param[3] - ) - @test size(X1) == size_vec1[counter] - @test size(X2) == size_vec2[counter] - counter += 1 - end - end end @testset "Function: get_components" begin - Exogenous_X1 = rand(10, 3) - Exogenous_X2 = zeros(10, 0) + exog1 = rand(10, 3) + exog2 = zeros(10, 0) Basic_Structural = StateSpaceLearning.StructuralModel( rand(10); - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=true, - stochastic_seasonal=true, freq_seasonal=2, outlier=true, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X1, + ζ_threshold=0, + ω_threshold=0, + exog=exog1, ) Local_Level = StateSpaceLearning.StructuralModel( rand(10); - level=true, - stochastic_level=true, - trend=false, - stochastic_trend=false, - seasonal=false, - stochastic_seasonal=false, + slope="none", + seasonal="none", + cycle="none", freq_seasonal=2, outlier=true, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X1, + ζ_threshold=0, + exog=exog1, ) Local_Linear_Trend1 = StateSpaceLearning.StructuralModel( rand(10); - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=false, - stochastic_seasonal=false, - freq_seasonal=2, + seasonal="none", + cycle="none", outlier=false, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X1, + ζ_threshold=0, + exog=exog1, ) Local_Linear_Trend2 = StateSpaceLearning.StructuralModel( rand(10); - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=false, - stochastic_seasonal=false, + seasonal="none", + cycle="none", freq_seasonal=2, outlier=false, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X2, + ζ_threshold=0, + exog=exog2, ) models = [Basic_Structural, Local_Level, Local_Linear_Trend1, Local_Linear_Trend2] empty_keys_vec = [ - [], ["ν1", "ζ", "γ₁", "ω_2"], ["γ₁", "ω_2", "o"], ["γ₁", "ω_2", "o", "Exogenous_X"] + [], ["ν1", "ζ", "γ₁", "ω_2"], ["γ₁", "ω_2", "o"], ["γ₁", "ω_2", "o", "exog"] ] - exogs = [Exogenous_X1, Exogenous_X1, Exogenous_X1, Exogenous_X2] + exogs = [exog1, exog1, exog1, exog2] for idx in eachindex(models) model = models[idx] @@ -579,7 +301,7 @@ end for key in keys(components_indexes) @test (key in empty_keys_vec[idx]) ? isempty(components_indexes[key]) : true - @test if key == "Exogenous_X" + @test if key == "exog" length(components_indexes[key]) == size(exogs[idx], 2) else true @@ -588,75 +310,50 @@ end end end -@testset "Function: get_variances" begin - Exogenous_X2 = zeros(10, 0) +@testset "Functions: get_variances and get_model_innovations" begin + exog2 = zeros(10, 0) Basic_Structural = StateSpaceLearning.StructuralModel( rand(10); - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=true, - stochastic_seasonal=true, freq_seasonal=2, outlier=true, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X2, + ζ_threshold=0, + exog=exog2, ) Basic_Structural2 = StateSpaceLearning.StructuralModel( rand(10); - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=true, - stochastic_seasonal=true, freq_seasonal=[2, 5], outlier=true, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X2, + ζ_threshold=0, + exog=exog2, ) Local_Level = StateSpaceLearning.StructuralModel( rand(10); - level=true, - stochastic_level=true, - trend=false, - stochastic_trend=false, - seasonal=false, - stochastic_seasonal=false, + slope="none", + seasonal="none", freq_seasonal=2, outlier=true, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X2, + ζ_threshold=0, + exog=exog2, ) Local_Linear_Trend = StateSpaceLearning.StructuralModel( rand(10); - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=false, - stochastic_seasonal=false, + seasonal="none", + cycle="none", freq_seasonal=2, outlier=false, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X2, + ζ_threshold=0, + exog=exog2, ) Local_Linear_Trend_cycle = StateSpaceLearning.StructuralModel( rand(10); - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=false, - stochastic_seasonal=false, + seasonal="none", + cycle="stochastic", freq_seasonal=2, cycle_period=3, - stochastic_cycle=true, outlier=false, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X2, + ζ_threshold=0, + exog=exog2, ) models = [ @@ -672,7 +369,15 @@ end ["ξ", "ζ", "ω_2", "ω_5", "ε"], ["ξ", "ε"], ["ξ", "ζ", "ε"], - ["ξ", "ζ", "ε", "ϕ_1"], + ["ξ", "ζ", "ε", "ϕ_3"], + ] + + models_innovations = [ + ["ξ", "ζ", "ω_2"], + ["ξ", "ζ", "ω_2", "ω_5"], + ["ξ"], + ["ξ", "ζ"], + ["ξ", "ζ", "ϕ_3"], ] for idx in eachindex(models) @@ -682,207 +387,357 @@ end model, rand(100), rand(100), components_indexes ) @test all([key in keys(variances) for key in params_vec[idx]]) + model_innovations = StateSpaceLearning.get_model_innovations(model) + @test model_innovations == models_innovations[idx] end -end -@testset "Function: get_model_innovations" begin - model1 = StateSpaceLearning.StructuralModel( - rand(10); - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=true, - stochastic_seasonal=true, - freq_seasonal=2, - outlier=true, - ζ_ω_threshold=0, - Exogenous_X=zeros(10, 0), - ) - model2 = StateSpaceLearning.StructuralModel( - rand(10); - level=true, - stochastic_level=false, - trend=true, - stochastic_trend=true, - seasonal=true, - stochastic_seasonal=true, - freq_seasonal=2, - outlier=true, - ζ_ω_threshold=0, - Exogenous_X=zeros(10, 0), - ) - model3 = StateSpaceLearning.StructuralModel( - rand(10); - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=false, - seasonal=true, - stochastic_seasonal=true, - freq_seasonal=2, - outlier=true, - ζ_ω_threshold=0, - Exogenous_X=zeros(10, 0), - ) - model4 = StateSpaceLearning.StructuralModel( - rand(10); - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=true, - stochastic_seasonal=false, - freq_seasonal=2, - outlier=true, - ζ_ω_threshold=0, - Exogenous_X=zeros(10, 0), - ) - model5 = StateSpaceLearning.StructuralModel( - rand(10); - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=false, - seasonal=true, - stochastic_seasonal=true, - freq_seasonal=[2, 5], - outlier=true, - ζ_ω_threshold=0, - Exogenous_X=zeros(10, 0), - ) - model6 = StateSpaceLearning.StructuralModel( - rand(10); - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=false, - seasonal=true, - stochastic_seasonal=true, - freq_seasonal=[2, 5], - cycle_period=3, - stochastic_cycle=true, - outlier=true, - ζ_ω_threshold=0, - Exogenous_X=zeros(10, 0), - ) +end - models = [model1, model2, model3, model4] +@testset "Decomposion functions" begin + Random.seed!(123) + model = StateSpaceLearning.StructuralModel(vcat(collect(1:5), collect(5:-1:1));level="deterministic", seasonal="none", cycle="none", outlier=false, slope="stochastic", ζ_threshold=0) + StateSpaceLearning.fit!(model) + slope = StateSpaceLearning.get_slope_decomposition(model, model.output.components) + @test all(isapprox.(slope, + [ 0.3195538151032132 + 0.3195538151032132 + 1.0535772194857598 + 1.1323058058970006 + 0.9011191905923782 + 0.07553685115943187 + -0.8838426390957513 + -1.044032162858364 + -1.0251744597550265 + -1.0019651980300468], atol=1e-6)) + + model = StateSpaceLearning.StructuralModel(vcat(rand(5) .+ 5, rand(5) .- 5) + vcat(collect(1:5), collect(5:-1:1));seasonal="none", cycle="none", outlier=false, slope="stochastic", ζ_threshold=0) + StateSpaceLearning.fit!(model) + @test all(isapprox.(StateSpaceLearning.get_trend_decomposition(model, model.output.components, slope), + [ 6.544506301918287 + 7.9278752338886775 + 10.266929548367902 + 11.728283090188654 + 13.666722760765126 + 4.077371477199227 + 1.7029908259414626 + 0.5101771103035202 + -2.2199584710270805 + -3.2219236690571273 + ], atol=1e-6)) + + model = StateSpaceLearning.StructuralModel(rand(10); cycle="stochastic", cycle_period=3, outlier=false, slope="stochastic", ζ_threshold=0, freq_seasonal=3, ω_threshold=0, ϕ_threshold=0) + StateSpaceLearning.fit!(model) + @test all(isapprox.(StateSpaceLearning.get_seasonal_decomposition(model, model.output.components, 3), + [ -0.011114430313782316 + 0.016993897901375513 + 0.0 + -0.06137711460224293 + -0.028221145277986203 + 0.045215043179361716 + -0.0027753371516487588 + -0.08682292272858037 + -0.036923166352424444 + 0.13243351808078532 + ], atol=1e-6)) + + @test all(isapprox.(StateSpaceLearning.get_cycle_decomposition(model, model.output.components, 3), + [ 0.0 + 0.0 + 1.6111635465327112e-18 + -0.005696779520104198 + -0.030765036256794644 + 0.08224069806471179 + 0.030974382058151183 + -0.15828117942894446 + 0.037361403241860734 + 0.17009485813575637], atol=1e-6)) + + model_decomposition = StateSpaceLearning.get_model_decomposition(model, model.output.components) + @test sort(collect(keys(model_decomposition))) == sort([ "cycle_3", "cycle_hat_3", "seasonal_3", "slope", "trend"]) +end - keys_vec = [ - ["ξ", "ζ", "ω_2"], - ["ζ", "ω_2"], - ["ξ", "ω_2"], - ["ξ", "ζ"], - ["ξ", "ω_2", "ω_5", "ϕ_1"], - ] +@testset "Function: simulate_states" begin + model = StateSpaceLearning.StructuralModel(rand(100)) + StateSpaceLearning.fit!(model) + @test length(StateSpaceLearning.simulate_states(model, 10, true, 12)) == 10 + @test length(StateSpaceLearning.simulate_states(model, 8, false, 12)) == 8 + @test length(StateSpaceLearning.simulate_states(model, 10, false, 0)) == 10 + + model = StateSpaceLearning.StructuralModel(rand(100); seasonal="none", cycle="stochastic", cycle_period=3, outlier=false) + StateSpaceLearning.fit!(model) + @test length(StateSpaceLearning.simulate_states(model, 10, true, 12)) == 10 + @test length(StateSpaceLearning.simulate_states(model, 8, false, 12)) == 8 + @test length(StateSpaceLearning.simulate_states(model, 10, false, 0)) == 10 +end - for idx in eachindex(models) - model = models[idx] - model_innovations = StateSpaceLearning.get_model_innovations(model) - @test model_innovations == keys_vec[idx] - end +@testset "Function: forecast_dynamic_exog_coefs" begin + model = StateSpaceLearning.StructuralModel(rand(100); seasonal="none", cycle="stochastic", cycle_period=3, outlier=false) + StateSpaceLearning.fit!(model) + @test StateSpaceLearning.forecast_dynamic_exog_coefs(model, 10, Vector{Vector}(undef, 0)) == zeros(10) + @test StateSpaceLearning.forecast_dynamic_exog_coefs(model, 8, Vector{Vector}(undef, 0)) == zeros(8) + + dynamic_exog_coefs = [(collect(1:100), "level"), (collect(1:100), "slope"), (collect(1:100), "seasonal", 2), (collect(1:100), "cycle", 3)] + forecast_dynamic_exog_coefs = [collect(101:110), collect(101:110), collect(101:110), collect(101:110)] + model2 = StateSpaceLearning.StructuralModel(rand(100); dynamic_exog_coefs = dynamic_exog_coefs) + StateSpaceLearning.fit!(model2) + @test StateSpaceLearning.forecast_dynamic_exog_coefs(model2, 10, forecast_dynamic_exog_coefs) != zeros(10) + @test length(StateSpaceLearning.forecast_dynamic_exog_coefs(model2, 10, forecast_dynamic_exog_coefs)) == 10 end -@testset "Function: get_innovation_simulation_X" begin - innovation1 = "ξ" - innovation2 = "ζ" - innovation3 = "ω_2" - innovation4 = "ϕ_1" - - model = StateSpaceLearning.StructuralModel( - rand(3); - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=true, - stochastic_seasonal=true, - freq_seasonal=2, - outlier=true, - cycle_period=3, - stochastic_cycle=true, - ζ_ω_threshold=0, - Exogenous_X=zeros(10, 0), - ) - steps_ahead = 2 - - X1 = StateSpaceLearning.get_innovation_simulation_X(model, innovation1, steps_ahead) - @assert X1 == [ - 0.0 0.0 0.0 0.0 - 1.0 0.0 0.0 0.0 - 1.0 1.0 0.0 0.0 - 1.0 1.0 1.0 0.0 - 1.0 1.0 1.0 1.0 - 1.0 1.0 1.0 1.0 - ] +@testset "Function: forecast" begin + y1 = rand(100) + y2 = rand(100) + y2[10:20] .= NaN - X2 = StateSpaceLearning.get_innovation_simulation_X(model, innovation2, steps_ahead) - @assert X2 == [ - 0.0 0.0 0.0 - 0.0 0.0 0.0 - 1.0 0.0 0.0 - 2.0 1.0 0.0 - 3.0 2.0 1.0 - 4.0 3.0 2.0 + model1 = StateSpaceLearning.StructuralModel(y1) + StateSpaceLearning.fit!(model1) + @test length(StateSpaceLearning.forecast(model1, 10)) == 10 + + model2 = StateSpaceLearning.StructuralModel(y2; exog=rand(100, 3)) + StateSpaceLearning.fit!(model2) + @test length(StateSpaceLearning.forecast(model2, 10; Exogenous_Forecast=rand(10, 3))) == + 10 + + @test_throws AssertionError StateSpaceLearning.forecast( + model1, 10; Exogenous_Forecast=rand(5, 3) + ) + @test_throws AssertionError StateSpaceLearning.forecast(model2, 10) + @test_throws AssertionError StateSpaceLearning.forecast( + model2, 10; Exogenous_Forecast=rand(5, 3) + ) + + y3 = [ + 4.718, + 4.77, + 4.882, + 4.859, + 4.795, + 4.905, + 4.997, + 4.997, + 4.912, + 4.779, + 4.644, + 4.77, + 4.744, + 4.836, + 4.948, + 4.905, + 4.828, + 5.003, + 5.135, + 5.135, + 5.062, + 4.89, + 4.736, + 4.941, + 4.976, + 5.01, + 5.181, + 5.093, + 5.147, + 5.181, + 5.293, + 5.293, + 5.214, + 5.087, + 4.983, + 5.111, + 5.141, + 5.192, + 5.262, + 5.198, + 5.209, + 5.384, + 5.438, + 5.488, + 5.342, + 5.252, + 5.147, + 5.267, + 5.278, + 5.278, + 5.463, + 5.459, + 5.433, + 5.493, + 5.575, + 5.605, + 5.468, + 5.351, + 5.192, + 5.303, + 5.318, + 5.236, + 5.459, + 5.424, + 5.455, + 5.575, + 5.71, + 5.68, + 5.556, + 5.433, + 5.313, + 5.433, + 5.488, + 5.451, + 5.587, + 5.594, + 5.598, + 5.752, + 5.897, + 5.849, + 5.743, + 5.613, + 5.468, + 5.627, + 5.648, + 5.624, + 5.758, + 5.746, + 5.762, + 5.924, + 6.023, + 6.003, + 5.872, + 5.723, + 5.602, + 5.723, + 5.752, + 5.707, + 5.874, + 5.852, + 5.872, + 6.045, + 6.142, + 6.146, + 6.001, + 5.849, + 5.72, + 5.817, + 5.828, + 5.762, + 5.891, + 5.852, + 5.894, + 6.075, + 6.196, + 6.224, + 6.001, + 5.883, + 5.736, + 5.82, + 5.886, + 5.834, + 6.006, + 5.981, + 6.04, + 6.156, + 6.306, + 6.326, + 6.137, + 6.008, + 5.891, + 6.003, + 6.033, + 5.968, + 6.037, + 6.133, + 6.156, + 6.282, + 6.432, + 6.406, + 6.23, + 6.133, + 5.966, + 6.068, ] - - X3 = StateSpaceLearning.get_innovation_simulation_X(model, innovation3, steps_ahead) - @assert X3 == [ - 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 - -1.0 1.0 0.0 0.0 - 0.0 -1.0 1.0 0.0 - -1.0 1.0 -1.0 1.0 - 0.0 -1.0 1.0 -1.0 + model3 = StateSpaceLearning.StructuralModel(y3) + StateSpaceLearning.fit!(model3) + forecast3 = trunc.(StateSpaceLearning.forecast(model3, 18); digits=3) + @assert forecast3 == [ + 6.11, + 6.082, + 6.221, + 6.19, + 6.197, + 6.328, + 6.447, + 6.44, + 6.285, + 6.163, + 6.026, + 6.142, + 6.166, + 6.138, + 6.278, + 6.246, + 6.253, + 6.384, ] - X4 = StateSpaceLearning.get_innovation_simulation_X(model, innovation4, steps_ahead) - @assert all( - isapprox.( - X4, - [ - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - -0.5 0.866025 1.0 0.0 0.0 0.0 0.0 0.0 - -0.5 -0.866025 -0.5 0.866025 1.0 0.0 0.0 0.0 - 1.0 -6.10623e-16 -0.5 -0.866025 -0.5 0.866025 1.0 0.0 - -0.5 0.866025 1.0 -6.10623e-16 -0.5 -0.866025 -0.5 0.866025 - ], - atol=1e-6, - ), - ) + model4 = StateSpaceLearning.StructuralModel(y3; freq_seasonal=[12, 36]) + StateSpaceLearning.fit!(model4) + forecast4 = trunc.(StateSpaceLearning.forecast(model4, 18); digits=3) + + @test length(forecast4) == 18 + + exog = rand(length(y3), 3) + model5 = StateSpaceLearning.StructuralModel(y3; exog=exog) + StateSpaceLearning.fit!(model5) + exog_forecast = rand(18, 3) + forecast5 = trunc.(StateSpaceLearning.forecast(model5, 18; Exogenous_Forecast=exog_forecast); digits=3) + @test length(forecast5) == 18 + + dynamic_exog_coefs = [(collect(1:length(y3)), "level"), (collect(1:length(y3)), "slope"), (collect(1:length(y3)), "seasonal", 2), (collect(1:length(y3)), "cycle", 3)] + forecast_dynamic_exog_coefs = [collect(length(y3) + 1:length(y3) + 10), collect(length(y3) + 1:length(y3) + 10), collect(length(y3) + 1:length(y3) + 10), collect(length(y3) + 1:length(y3) + 10)] + model6 = StateSpaceLearning.StructuralModel(y3; dynamic_exog_coefs = dynamic_exog_coefs) + StateSpaceLearning.fit!(model6) + @test StateSpaceLearning.forecast_dynamic_exog_coefs(model6, 10, forecast_dynamic_exog_coefs) != zeros(10) + @test length(StateSpaceLearning.forecast_dynamic_exog_coefs(model6, 10, forecast_dynamic_exog_coefs)) == 10 +end + +@testset "Function: simulate" begin + y1 = rand(100) + y2 = rand(100) + y2[10:20] .= NaN + + model1 = StateSpaceLearning.StructuralModel(y1) + StateSpaceLearning.fit!(model1) + @test size(StateSpaceLearning.simulate(model1, 10, 100)) == (10, 100) + + @test size( + StateSpaceLearning.simulate(model1, 10, 100; seasonal_innovation_simulation=10) + ) == (10, 100) + + model2 = StateSpaceLearning.StructuralModel(y2; exog=rand(100, 3)) + StateSpaceLearning.fit!(model2) + @test size( + StateSpaceLearning.simulate(model2, 10, 100; Exogenous_Forecast=rand(10, 3)) + ) == (10, 100) + + exog = rand(length(y2), 3) + model5 = StateSpaceLearning.StructuralModel(y2; exog=exog) + StateSpaceLearning.fit!(model5) + exog_forecast = rand(10, 3) + @test size( + StateSpaceLearning.simulate(model5, 10, 100; Exogenous_Forecast=exog_forecast) + ) == (10, 100) + + dynamic_exog_coefs = [(collect(1:length(y2)), "level"), (collect(1:length(y2)), "slope"), (collect(1:length(y2)), "seasonal", 2), (collect(1:length(y2)), "cycle", 3)] + forecast_dynamic_exog_coefs = [collect(length(y2) + 1:length(y2) + 10), collect(length(y2) + 1:length(y2) + 10), collect(length(y2) + 1:length(y2) + 10), collect(length(y2) + 1:length(y2) + 10)] + model6 = StateSpaceLearning.StructuralModel(y2; dynamic_exog_coefs = dynamic_exog_coefs) + StateSpaceLearning.fit!(model6) + @test size( + StateSpaceLearning.simulate(model6, 10, 100; dynamic_exog_coefs_forecasts=forecast_dynamic_exog_coefs) + ) == (10, 100) - model2 = StateSpaceLearning.StructuralModel( - rand(4); - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=true, - stochastic_seasonal=true, - freq_seasonal=2, - outlier=true, - cycle_period=3, - stochastic_cycle=true, - ζ_ω_threshold=0, - Exogenous_X=zeros(10, 0), - stochastic_start=3, - ) - X5 = StateSpaceLearning.get_innovation_simulation_X(model2, innovation4, steps_ahead) - @assert all( - isapprox.( - X5, - [ - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - -0.5 0.866025 1.0 0.0 0.0 0.0 0.0 0.0 - -0.5 -0.866025 -0.5 0.866025 1.0 0.0 0.0 0.0 - 1.0 -6.10623e-16 -0.5 -0.866025 -0.5 0.866025 1.0 0.0 - -0.5 0.866025 1.0 -6.10623e-16 -0.5 -0.866025 -0.5 0.866025 - ], - atol=1e-6, - ), - ) end + +@testset "Basics" begin + model = StateSpaceLearning.StructuralModel(rand(100)) + @test StateSpaceLearning.isfitted(model) == false + StateSpaceLearning.fit!(model) + + @test StateSpaceLearning.isfitted(model) == true +end \ No newline at end of file diff --git a/test/utils.jl b/test/utils.jl index 42ccfc8..e246398 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -1,58 +1,42 @@ @testset "Function: build_components" begin - Exogenous_X1 = rand(10, 3) - Exogenous_X2 = zeros(10, 0) + exog1 = rand(10, 3) + exog2 = zeros(10, 0) Basic_Structural = StateSpaceLearning.StructuralModel( rand(10); - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=true, - stochastic_seasonal=true, + level="stochastic", + slope="stochastic", + seasonal="stochastic", freq_seasonal=2, outlier=true, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X1, + ζ_threshold=0, + ω_threshold=0, + exog=exog1, ) Local_Level = StateSpaceLearning.StructuralModel( rand(10); - level=true, - stochastic_level=true, - trend=false, - stochastic_trend=false, - seasonal=false, - stochastic_seasonal=false, - freq_seasonal=2, + level="stochastic", + slope="none", + seasonal="none", outlier=true, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X1, + exog=exog1, ) Local_Linear_Trend1 = StateSpaceLearning.StructuralModel( rand(10); - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=false, - stochastic_seasonal=false, - freq_seasonal=2, + level="stochastic", + slope="stochastic", + seasonal="none", outlier=false, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X1, + exog=exog1, ) Local_Linear_Trend2 = StateSpaceLearning.StructuralModel( rand(10); - level=true, - stochastic_level=true, - trend=true, - stochastic_trend=true, - seasonal=false, - stochastic_seasonal=false, - freq_seasonal=2, + level="stochastic", + slope="stochastic", + seasonal="none", outlier=false, - ζ_ω_threshold=0, - Exogenous_X=Exogenous_X2, + exog=exog2, + ζ_threshold=0, ) models = [Basic_Structural, Local_Level, Local_Linear_Trend1, Local_Linear_Trend2] @@ -69,7 +53,7 @@ @test "Values" in keys(components[key]) @test "Coefs" in keys(components[key]) @test "Indexes" in keys(components[key]) - @test key == "Exogenous_X" ? "Selected" in keys(components[key]) : true + @test key == "exog" ? "Selected" in keys(components[key]) : true end end end @@ -105,44 +89,43 @@ end @test StateSpaceLearning.has_intercept(X) end -@testset "Function: fill_innovation_coefs" begin - model = StateSpaceLearning.StructuralModel(rand(100)) - StateSpaceLearning.fit!(model) - components = ["ξ", "ζ", "ω_12"] - - valid_indexes = model.output.valid_indexes - - inov_comp1 = StateSpaceLearning.fill_innovation_coefs( - model, components[1], valid_indexes - ) - inov_comp2 = StateSpaceLearning.fill_innovation_coefs( - model, components[2], valid_indexes - ) - inov_comp3 = StateSpaceLearning.fill_innovation_coefs( - model, components[3], valid_indexes - ) - - @test length(inov_comp1) == 100 - @test length(inov_comp2) == 100 - @test length(inov_comp3) == 100 - - model = StateSpaceLearning.StructuralModel(rand(100, 3)) - StateSpaceLearning.fit!(model) - components = ["ξ", "ζ", "ω_12"] - - valid_indexes = model.output[1].valid_indexes - - inov_comp1 = StateSpaceLearning.fill_innovation_coefs( - model, components[1], valid_indexes - ) - inov_comp2 = StateSpaceLearning.fill_innovation_coefs( - model, components[2], valid_indexes - ) - inov_comp3 = StateSpaceLearning.fill_innovation_coefs( - model, components[3], valid_indexes - ) +@testset "Function: handle_missing_values" begin + y = rand(10) + X = rand(10, 3) + y[1] = NaN + X[1, :] .= NaN + y_treated, X_treated, valid_indexes = StateSpaceLearning.handle_missing_values(X, y) + @test y_treated == y[2:end] + @test X_treated == X[2:end, :] + @test valid_indexes == 2:10 +end - @test size(inov_comp1) == (100, 3) - @test size(inov_comp2) == (100, 3) - @test size(inov_comp3) == (100, 3) +@testset "Function: get_stochastic_values" begin + Random.seed!(1234) + + estimated_stochastic = rand(10) + steps_ahead = 5 + T = 10 + start_idx = 1 + final_idx = 10 + seasonal_innovation_simulation1 = 0 + seasonal_innovation_simulation2 = 2 + + st_values1 = StateSpaceLearning.get_stochastic_values(estimated_stochastic, steps_ahead, T, start_idx, final_idx, seasonal_innovation_simulation1) + st_values2 = StateSpaceLearning.get_stochastic_values(estimated_stochastic, steps_ahead, T, start_idx, final_idx, seasonal_innovation_simulation2) + + @test length(st_values1) == steps_ahead + @test length(st_values2) == steps_ahead + + @test all(isapprox.(st_values1, [ 0.6395615996802734 + -0.8396219340580711 + 0.6395615996802734 + -0.5798621201341324 + 0.967142768915383], atol=1e-6)) + @test all(isapprox.(st_values2, [ 0.520354993723718 + -0.014908849285099945 + -0.13102565622085904 + -0.6395615996802734 + -0.520354993723718], atol=1e-6)) end + From 20741edf04d1a0d80b79a25d1ea96823215bfe43 Mon Sep 17 00:00:00 2001 From: andre_ramos Date: Mon, 28 Apr 2025 14:35:04 -0300 Subject: [PATCH 2/6] fix tests and format --- src/StateSpaceLearning.jl | 8 +- src/fit_forecast.jl | 3 +- src/models/structural_model.jl | 492 ++++++++++++++++++++++++-------- src/utils.jl | 21 +- test/fit_forecast.jl | 2 +- test/models/structural_model.jl | 438 +++++++++++++++++----------- test/utils.jl | 59 +++- 7 files changed, 714 insertions(+), 309 deletions(-) diff --git a/src/StateSpaceLearning.jl b/src/StateSpaceLearning.jl index 402dfb2..e3dfb01 100644 --- a/src/StateSpaceLearning.jl +++ b/src/StateSpaceLearning.jl @@ -13,6 +13,12 @@ include("datasets.jl") include("fit_forecast.jl") include("plots.jl") -export fit!, forecast, simulate, StructuralModel, plot_point_forecast, plot_scenarios, simulate_states +export fit!, + forecast, + simulate, + StructuralModel, + plot_point_forecast, + plot_scenarios, + simulate_states end # module StateSpaceLearning diff --git a/src/fit_forecast.jl b/src/fit_forecast.jl index 5a13434..cfed4a1 100644 --- a/src/fit_forecast.jl +++ b/src/fit_forecast.jl @@ -68,8 +68,7 @@ function fit!( decomposition = get_model_decomposition(model, components) - model.output = Output( + return model.output = Output( coefs, ε, fitted, residuals_variances, valid_indexes, components, decomposition ) - end diff --git a/src/models/structural_model.jl b/src/models/structural_model.jl index b080e73..c9931ca 100644 --- a/src/models/structural_model.jl +++ b/src/models/structural_model.jl @@ -70,7 +70,7 @@ mutable struct StructuralModel <: StateSpaceLearningModel ϕ_threshold::Int stochastic_start::Int n_exogenous::Int - dynamic_exog_coefs::Union{Vector{<:Tuple}, Nothing} + dynamic_exog_coefs::Union{Vector{<:Tuple},Nothing} output::Union{Vector{Output},Output,Nothing} function StructuralModel( @@ -87,7 +87,7 @@ mutable struct StructuralModel <: StateSpaceLearningModel ϕ_threshold::Int=12, stochastic_start::Int=1, exog::Matrix=zeros(length(y), 0), - dynamic_exog_coefs::Union{Vector{<:Tuple}, Nothing}=nothing + dynamic_exog_coefs::Union{Vector{<:Tuple},Nothing}=nothing, ) n_exogenous = size(exog, 2) @@ -99,20 +99,49 @@ mutable struct StructuralModel <: StateSpaceLearningModel @assert cycle in ["deterministic", "stochastic", "none"] "cycle must be either deterministic, stochastic or no" @assert seasonal != "none" ? length(y) > minimum(freq_seasonal) : true "Time series must be longer than the seasonal period if seasonal is added" - typeof(freq_seasonal) <: Vector ? (@assert all(freq_seasonal .> 0) "Seasonal period must be greater than 0") : (@assert freq_seasonal > 0 "Seasonal period must be greater than 0") + if typeof(freq_seasonal) <: Vector + (@assert all(freq_seasonal .> 0) "Seasonal period must be greater than 0") + else + (@assert freq_seasonal > 0 "Seasonal period must be greater than 0") + end - typeof(cycle_period) <: Vector ? (@assert all(cycle_period .>= 0) "Cycle period must be greater than or equal to 0") : (@assert cycle_period >= 0 "Cycle period must be greater than or equal to 0") + if typeof(cycle_period) <: Vector + (@assert all(cycle_period .>= 0) "Cycle period must be greater than or equal to 0") + else + (@assert cycle_period >= 0 "Cycle period must be greater than or equal to 0") + end - cycle_period == 0 ? (@assert cycle == "none" "stochastic_cycle and cycle must be false if cycle_period is 0") : nothing - freq_seasonal == 0 ? (@assert seasonal == "none" "stochastic_seasonal and seasonal must be false if freq_seasonal is 0") : nothing + if cycle_period == 0 + (@assert cycle == "none" "stochastic_cycle and cycle must be false if cycle_period is 0") + else + nothing + end + if freq_seasonal == 0 + (@assert seasonal == "none" "stochastic_seasonal and seasonal must be false if freq_seasonal is 0") + else + nothing + end if !isnothing(dynamic_exog_coefs) - @assert all(typeof(dynamic_exog_coefs[i][1]) <: Vector for i in eachindex(dynamic_exog_coefs)) "The first element of each tuple in dynamic_exog_coefs must be a vector" - @assert all(typeof(dynamic_exog_coefs[i][2]) <: String for i in eachindex(dynamic_exog_coefs)) "The second element of each tuple in dynamic_exog_coefs must be a string" - @assert all([length(dynamic_exog_coefs[i][1]) .== length(y) for i in eachindex(dynamic_exog_coefs)]) "The exogenous features that will be combined with state space components must have the same length as the time series" - @assert all(dynamic_exog_coefs[i][2] in ["level", "slope", "seasonal", "cycle"] for i in eachindex(dynamic_exog_coefs)) "The second element of each tuple in dynamic_exog_coefs must be a string that is either level, slope, seasonal or cycle" + @assert all( + typeof(dynamic_exog_coefs[i][1]) <: Vector for + i in eachindex(dynamic_exog_coefs) + ) "The first element of each tuple in dynamic_exog_coefs must be a vector" + @assert all( + typeof(dynamic_exog_coefs[i][2]) <: String for + i in eachindex(dynamic_exog_coefs) + ) "The second element of each tuple in dynamic_exog_coefs must be a string" + @assert all([ + length(dynamic_exog_coefs[i][1]) .== length(y) for + i in eachindex(dynamic_exog_coefs) + ]) "The exogenous features that will be combined with state space components must have the same length as the time series" + @assert all( + dynamic_exog_coefs[i][2] in ["level", "slope", "seasonal", "cycle"] for + i in eachindex(dynamic_exog_coefs) + ) "The second element of each tuple in dynamic_exog_coefs must be a string that is either level, slope, seasonal or cycle" for i in eachindex(dynamic_exog_coefs) - if dynamic_exog_coefs[i][2] == "seasonal" || dynamic_exog_coefs[i][2] == "cycle" + if dynamic_exog_coefs[i][2] == "seasonal" || + dynamic_exog_coefs[i][2] == "cycle" @assert length(dynamic_exog_coefs[i]) == 3 "The tuple in dynamic_exog_coefs must have 3 elements if the second element is seasonal or cycle" @assert typeof(dynamic_exog_coefs[i][3]) <: Int "The third element of each tuple in dynamic_exog_coefs must be an integer if the second element is seasonal or cycle" @assert dynamic_exog_coefs[i][3] > 1 "The third element of each tuple in dynamic_exog_coefs must be greater than 1 if the second element is seasonal or cycle" @@ -137,7 +166,7 @@ mutable struct StructuralModel <: StateSpaceLearningModel ϕ_threshold, stochastic_start, exog, - dynamic_exog_coefs + dynamic_exog_coefs, ) # convert y format into vector of AbstractFloat y = convert(Vector{AbstractFloat}, y) @@ -245,7 +274,8 @@ o_size(T::Int, stochastic_start::Int)::Int = T - max(1, stochastic_start) + 1 # Returns - `Int`: Size of ϕ calculated from T. """ -ϕ_size(T::Int, ϕ_threshold::Int, stochastic_start::Int)::Int = (2 * (T - max(2, stochastic_start) + 1) - (max(1, ϕ_threshold) * 2)) +ϕ_size(T::Int, ϕ_threshold::Int, stochastic_start::Int)::Int = + (2 * (T - max(2, stochastic_start) + 1) - (max(1, ϕ_threshold) * 2)) """ create_ξ(T::Int, stochastic_start::Int)::Matrix @@ -285,9 +315,7 @@ create_ζ(T::Int, ζ_threshold::Int, stochastic_start::Int)::Matrix - `Matrix`: Matrix of innovations ζ constructed based on the input sizes. """ -function create_ζ( - T::Int, ζ_threshold::Int, stochastic_start::Int -)::Matrix +function create_ζ(T::Int, ζ_threshold::Int, stochastic_start::Int)::Matrix stochastic_start = max(2, stochastic_start) ζ_matrix = zeros(T, T - stochastic_start) @@ -376,12 +404,11 @@ create_ϕ(c_period::Union{Int, Fl}, T::Int, ϕ_threshold::Int, stochastic_start: - `Matrix`: Matrix of innovations ϕ constructed based on the input sizes. """ function create_ϕ( - c_period::Union{Int, Fl}, T::Int, ϕ_threshold::Int, stochastic_start::Int + c_period::Union{Int,Fl}, T::Int, ϕ_threshold::Int, stochastic_start::Int )::Matrix where {Fl<:AbstractFloat} - X = Matrix{Float64}(undef, T, 0) λ = 2 * pi * (1:T) / c_period - + for t in max(2, stochastic_start):(T - max(1, ϕ_threshold)) # one of last two columns might be full of zeros X_t = hcat(cos.(λ), sin.(λ)) X_t[1:(t - 1), :] .= 0 @@ -417,7 +444,9 @@ create_deterministic_cycle(T::Int, c_period::Union{Int, Fl})::Matrix where {Fl<: - `T::Int`: Length of the original time series. - `c_period::Int`: Cycle period. """ -function create_deterministic_cycle(T::Int, c_period::Union{Int, Fl})::Matrix where {Fl<:AbstractFloat} +function create_deterministic_cycle( + T::Int, c_period::Union{Int,Fl} +)::Matrix where {Fl<:AbstractFloat} λ = 2 * pi * (1:T) / c_period cycle1_matrix = hcat(cos.(λ), sin.(λ)) return cycle1_matrix @@ -459,9 +488,7 @@ function create_initial_states_Matrix( nothing end if trend - initial_states_matrix = hcat( - initial_states_matrix, vcat([0], collect(1:(T - 1))) - ) + initial_states_matrix = hcat(initial_states_matrix, vcat([0], collect(1:(T - 1)))) else nothing end @@ -499,24 +526,59 @@ create_dynamic_exog_coefs_matrix(dynamic_exog_coefs::Vector{<:Tuple}, T::Int,ζ_ # Returns - `Matrix`: Matrix of combination components constructed based on the input parameters. """ -function create_dynamic_exog_coefs_matrix(dynamic_exog_coefs::Vector{<:Tuple}, T::Int,ζ_threshold::Int, ω_threshold::Int, ϕ_threshold::Int, stochastic_start::Int)::Matrix - state_components_dict = Dict{String, Matrix}() +function create_dynamic_exog_coefs_matrix( + dynamic_exog_coefs::Vector{<:Tuple}, + T::Int, + ζ_threshold::Int, + ω_threshold::Int, + ϕ_threshold::Int, + stochastic_start::Int, +)::Matrix + state_components_dict = Dict{String,Matrix}() dynamic_exog_coefs_matrix = zeros(T, 0) for combination in dynamic_exog_coefs if combination[2] == "level" - haskey(state_components_dict, "level") ? nothing : state_components_dict["level"] = hcat(ones(T, 1), create_ξ(T, stochastic_start)) + if haskey(state_components_dict, "level") + nothing + else + state_components_dict["level"] = hcat( + ones(T, 1), create_ξ(T, stochastic_start) + ) + end key_name = "level" elseif combination[2] == "slope" - haskey(state_components_dict, "slope") ? nothing : state_components_dict["slope"] = hcat(vcat([0], collect(1:(T - 1))), create_ζ(T, ζ_threshold, stochastic_start)) + if haskey(state_components_dict, "slope") + nothing + else + state_components_dict["slope"] = hcat( + vcat([0], collect(1:(T - 1))), create_ζ(T, ζ_threshold, stochastic_start) + ) + end key_name = "slope" elseif combination[2] == "seasonal" - haskey(state_components_dict, "seasonal_$(combination[3])") ? nothing : state_components_dict["seasonal_$(combination[3])"] = hcat(create_deterministic_seasonal(T, combination[3]), create_ω(T, combination[3], ω_threshold, stochastic_start)) + if haskey(state_components_dict, "seasonal_$(combination[3])") + nothing + else + state_components_dict["seasonal_$(combination[3])"] = hcat( + create_deterministic_seasonal(T, combination[3]), + create_ω(T, combination[3], ω_threshold, stochastic_start), + ) + end key_name = "seasonal_$(combination[3])" elseif combination[2] == "cycle" - haskey(state_components_dict, "cycle_$(combination[3])") ? nothing : state_components_dict["cycle_$(combination[3])"] = hcat(create_deterministic_cycle(T, combination[3]), create_ϕ(combination[3], T, ϕ_threshold, stochastic_start)) + if haskey(state_components_dict, "cycle_$(combination[3])") + nothing + else + state_components_dict["cycle_$(combination[3])"] = hcat( + create_deterministic_cycle(T, combination[3]), + create_ϕ(combination[3], T, ϕ_threshold, stochastic_start), + ) + end key_name = "cycle_$(combination[3])" end - dynamic_exog_coefs_matrix = hcat(dynamic_exog_coefs_matrix, combination[1] .* state_components_dict[key_name]) + dynamic_exog_coefs_matrix = hcat( + dynamic_exog_coefs_matrix, combination[1] .* state_components_dict[key_name] + ) end return dynamic_exog_coefs_matrix end @@ -538,24 +600,69 @@ create_forecast_dynamic_exog_coefs_matrix(dynamic_exog_coefs::Vector{<:Tuple}, T # Returns - `Matrix`: Matrix of combination components constructed based on the input parameters. """ -function create_forecast_dynamic_exog_coefs_matrix(dynamic_exog_coefs::Vector{<:Tuple}, T::Int, steps_ahead::Int, ζ_threshold::Int, ω_threshold::Int, ϕ_threshold::Int, stochastic_start::Int)::Matrix - state_components_dict = Dict{String, Matrix}() +function create_forecast_dynamic_exog_coefs_matrix( + dynamic_exog_coefs::Vector{<:Tuple}, + T::Int, + steps_ahead::Int, + ζ_threshold::Int, + ω_threshold::Int, + ϕ_threshold::Int, + stochastic_start::Int, +)::Matrix + state_components_dict = Dict{String,Matrix}() dynamic_exog_coefs_matrix = zeros(steps_ahead, 0) for combination in dynamic_exog_coefs if combination[2] == "level" - haskey(state_components_dict, "level") ? nothing : state_components_dict["level"] = hcat(ones(T + steps_ahead, 1), create_ξ(T + steps_ahead, stochastic_start))[end - steps_ahead + 1:end, 1:combination[4]] + if haskey(state_components_dict, "level") + nothing + else + state_components_dict["level"] = hcat( + ones(T + steps_ahead, 1), create_ξ(T + steps_ahead, stochastic_start) + )[ + (end - steps_ahead + 1):end, 1:combination[4] + ] + end key_name = "level" elseif combination[2] == "slope" - haskey(state_components_dict, "slope") ? nothing : state_components_dict["slope"] = hcat(vcat([0], collect(1:(T + steps_ahead - 1))), create_ζ(T + steps_ahead, ζ_threshold, stochastic_start))[end - steps_ahead + 1:end, 1:combination[4]] + if haskey(state_components_dict, "slope") + nothing + else + state_components_dict["slope"] = hcat( + vcat([0], collect(1:(T + steps_ahead - 1))), + create_ζ(T + steps_ahead, ζ_threshold, stochastic_start), + )[ + (end - steps_ahead + 1):end, 1:combination[4] + ] + end key_name = "slope" elseif combination[2] == "seasonal" - haskey(state_components_dict, "seasonal_$(combination[3])") ? nothing : state_components_dict["seasonal_$(combination[3])"] = hcat(create_deterministic_seasonal(T + steps_ahead, combination[3]), create_ω(T + steps_ahead, combination[3], ω_threshold, stochastic_start))[end - steps_ahead + 1:end, 1:combination[4]] + if haskey(state_components_dict, "seasonal_$(combination[3])") + nothing + else + state_components_dict["seasonal_$(combination[3])"] = hcat( + create_deterministic_seasonal(T + steps_ahead, combination[3]), + create_ω(T + steps_ahead, combination[3], ω_threshold, stochastic_start), + )[ + (end - steps_ahead + 1):end, 1:combination[4] + ] + end key_name = "seasonal_$(combination[3])" elseif combination[2] == "cycle" - haskey(state_components_dict, "cycle_$(combination[3])") ? nothing : state_components_dict["cycle_$(combination[3])"] = hcat(create_deterministic_cycle(T + steps_ahead, combination[3]), create_ϕ(combination[3], T + steps_ahead, ϕ_threshold, stochastic_start))[end - steps_ahead + 1:end, 1:combination[4]] + if haskey(state_components_dict, "cycle_$(combination[3])") + nothing + else + state_components_dict["cycle_$(combination[3])"] = hcat( + create_deterministic_cycle(T + steps_ahead, combination[3]), + create_ϕ(combination[3], T + steps_ahead, ϕ_threshold, stochastic_start), + )[ + (end - steps_ahead + 1):end, 1:combination[4] + ] + end key_name = "cycle_$(combination[3])" end - dynamic_exog_coefs_matrix = hcat(dynamic_exog_coefs_matrix, combination[1] .* state_components_dict[key_name]) + dynamic_exog_coefs_matrix = hcat( + dynamic_exog_coefs_matrix, combination[1] .* state_components_dict[key_name] + ) end return dynamic_exog_coefs_matrix end @@ -620,7 +727,7 @@ function create_X( ϕ_threshold::Int, stochastic_start::Int, exog::Matrix{Fl}, - dynamic_exog_coefs::Union{Vector{<:Tuple}, Nothing}, + dynamic_exog_coefs::Union{Vector{<:Tuple},Nothing}, ) where {Fl<:AbstractFloat} T = size(exog, 1) @@ -638,19 +745,14 @@ function create_X( ω_matrix = zeros(T, 0) if stochastic_seasonal for s in freq_seasonal - ω_matrix = hcat( - ω_matrix, create_ω(T, s, ω_threshold, stochastic_start) - ) + ω_matrix = hcat(ω_matrix, create_ω(T, s, ω_threshold, stochastic_start)) end end ϕ_matrix = zeros(T, 0) if stochastic_cycle for c_period in cycle_period - ϕ_matrix = hcat( - ϕ_matrix, - create_ϕ(c_period, T, ϕ_threshold, stochastic_start), - ) + ϕ_matrix = hcat(ϕ_matrix, create_ϕ(c_period, T, ϕ_threshold, stochastic_start)) end end @@ -665,7 +767,14 @@ function create_X( ) dynamic_exog_coefs_matrix = if !isnothing(dynamic_exog_coefs) - create_dynamic_exog_coefs_matrix(dynamic_exog_coefs, T, ζ_threshold, ω_threshold, ϕ_threshold, stochastic_start) + create_dynamic_exog_coefs_matrix( + dynamic_exog_coefs, + T, + ζ_threshold, + ω_threshold, + ϕ_threshold, + stochastic_start, + ) else zeros(T, 0) end @@ -678,7 +787,7 @@ function create_X( ϕ_matrix, o_matrix, exog, - dynamic_exog_coefs_matrix + dynamic_exog_coefs_matrix, ) end @@ -1011,7 +1120,11 @@ function get_slope_decomposition( end if model.stochastic_slope - ζ = vcat(zeros(max(2, model.stochastic_start)), components["ζ"]["Coefs"], zeros(model.ζ_threshold)) + ζ = vcat( + zeros(max(2, model.stochastic_start)), + components["ζ"]["Coefs"], + zeros(model.ζ_threshold), + ) @assert length(ζ) == T else ζ = zeros(AbstractFloat, T) @@ -1043,7 +1156,7 @@ function get_seasonal_decomposition( )::Vector{AbstractFloat} T = size(model.y, 1) seasonal = Vector{AbstractFloat}(undef, T) - + if model.seasonal seasonal[1:s] = components["γ1_$(s)"]["Coefs"] else @@ -1051,7 +1164,11 @@ function get_seasonal_decomposition( end if model.stochastic_seasonal - ω = vcat(zeros(s - 1 + max(0, max(2, model.stochastic_start) - s)), components["ω_$(s)"]["Coefs"], zeros(model.ω_threshold)) + ω = vcat( + zeros(s - 1 + max(0, max(2, model.stochastic_start) - s)), + components["ω_$(s)"]["Coefs"], + zeros(model.ω_threshold), + ) @assert length(ω) == T else ω = zeros(AbstractFloat, T) @@ -1079,21 +1196,28 @@ end """ function get_cycle_decomposition( - model::StructuralModel, components::Dict, cycle_period::Union{AbstractFloat, Int} + model::StructuralModel, components::Dict, cycle_period::Union{AbstractFloat,Int} )::Vector{AbstractFloat} - T = size(model.y, 1) cycle = Vector{AbstractFloat}(undef, T) - + if cycle_period != 0 λ = 2 * pi * (1:T) / cycle_period c1 = components["c1_$(cycle_period)"]["Coefs"] - + cycle[1] = (dot(c1, [cos(λ[1]), sin(λ[1])])) - + if model.stochastic_cycle - ϕ_cos = vcat(zeros(max(2, model.stochastic_start) - 1), components["ϕ_$(cycle_period)"]["Coefs"][1:2:end], zeros(max(1, model.ϕ_threshold))) - ϕ_sin = vcat(zeros(max(2, model.stochastic_start) - 1), components["ϕ_$(cycle_period)"]["Coefs"][2:2:end], zeros(max(1, model.ϕ_threshold))) + ϕ_cos = vcat( + zeros(max(2, model.stochastic_start) - 1), + components["ϕ_$(cycle_period)"]["Coefs"][1:2:end], + zeros(max(1, model.ϕ_threshold)), + ) + ϕ_sin = vcat( + zeros(max(2, model.stochastic_start) - 1), + components["ϕ_$(cycle_period)"]["Coefs"][2:2:end], + zeros(max(1, model.ϕ_threshold)), + ) @assert length(ϕ_cos) == T @assert length(ϕ_sin) == T else @@ -1102,9 +1226,13 @@ function get_cycle_decomposition( end for t in 2:T - ϕ_indexes = max(2, model.stochastic_start):min(t, (T - max(1, model.ϕ_threshold))) - cycle[t] = dot(c1, [cos(λ[t]), sin(λ[t])]) + - sum(ϕ_cos[i] * cos(λ[t]) + ϕ_sin[i] * sin(λ[t]) for i in eachindex(ϕ_indexes)) + ϕ_indexes = + max(2, model.stochastic_start):min(t, (T - max(1, model.ϕ_threshold))) + cycle[t] = + dot(c1, [cos(λ[t]), sin(λ[t])]) + sum( + ϕ_cos[i] * cos(λ[t]) + ϕ_sin[i] * sin(λ[t]) for + i in eachindex(ϕ_indexes) + ) end else @@ -1136,7 +1264,7 @@ function get_model_decomposition(model::StructuralModel, components::Dict)::Dict slope = get_slope_decomposition(model, components) model_decomposition["slope"] = slope end - + if model.level || model.slope slope = model.slope ? slope : convert(Vector{AbstractFloat}, zeros(length(model.y))) trend = get_trend_decomposition(model, components, slope) @@ -1177,8 +1305,11 @@ end - `Vector{AbstractFloat}`: Vector of states. """ function simulate_states( - model::StructuralModel, steps_ahead::Int, punctual::Bool, seasonal_innovation_simulation::Int - )::Vector{AbstractFloat} + model::StructuralModel, + steps_ahead::Int, + punctual::Bool, + seasonal_innovation_simulation::Int, +)::Vector{AbstractFloat} T = length(model.y) prediction = AbstractFloat[] @@ -1189,14 +1320,20 @@ function simulate_states( final_idx = T - model.ζ_threshold if model.stochastic_slope && !punctual if seasonal_innovation_simulation != 0 - ζ_values = vcat(zeros(start_idx - 1), model.output.components["ζ"]["Coefs"], zeros(model.ζ_threshold)) + ζ_values = vcat( + zeros(start_idx - 1), + model.output.components["ζ"]["Coefs"], + zeros(model.ζ_threshold), + ) else ζ_values = model.output.components["ζ"]["Coefs"] end else ζ_values = zeros(T) end - stochastic_slope_set = get_stochastic_values(ζ_values, steps_ahead, T, start_idx, final_idx, seasonal_innovation_simulation) + stochastic_slope_set = get_stochastic_values( + ζ_values, steps_ahead, T, start_idx, final_idx, seasonal_innovation_simulation + ) else slope = zeros(T) end @@ -1207,35 +1344,67 @@ function simulate_states( final_idx = T - 1 if model.stochastic_level && !punctual if seasonal_innovation_simulation != 0 - ξ_values = vcat(zeros(start_idx - 1), model.output.components["ξ"]["Coefs"], zeros(1)) + ξ_values = vcat( + zeros(start_idx - 1), model.output.components["ξ"]["Coefs"], zeros(1) + ) else ξ_values = model.output.components["ξ"]["Coefs"] end else ξ_values = zeros(T) end - stochastic_level_set = get_stochastic_values(ξ_values, steps_ahead, T, start_idx, final_idx, seasonal_innovation_simulation) + stochastic_level_set = get_stochastic_values( + ξ_values, steps_ahead, T, start_idx, final_idx, seasonal_innovation_simulation + ) end if model.seasonal - seasonals = [deepcopy(model.output.decomposition["seasonal_$s"]) for s in model.freq_seasonal] - start_idx = [model.freq_seasonal[i] - 1 + max(0, max(2, model.stochastic_start) - model.freq_seasonal[i]) for i in eachindex(model.freq_seasonal)] + seasonals = [ + deepcopy(model.output.decomposition["seasonal_$s"]) for s in model.freq_seasonal + ] + start_idx = [ + model.freq_seasonal[i] - 1 + + max(0, max(2, model.stochastic_start) - model.freq_seasonal[i]) for + i in eachindex(model.freq_seasonal) + ] final_idx = [T - model.ω_threshold for _ in eachindex(model.freq_seasonal)] if model.ω_threshold == 0 - final_ω = [model.output.components["ω_$(s)"]["Coefs"][end] for s in model.freq_seasonal] + final_ω = [ + model.output.components["ω_$(s)"]["Coefs"][end] for s in model.freq_seasonal + ] else final_ω = [0.0 for _ in model.freq_seasonal] end if model.stochastic_seasonal && !punctual if seasonal_innovation_simulation != 0 - ω_values = [vcat(zeros(s - 1 + max(0, max(2, model.stochastic_start) - s)), model.output.components["ω_$(s)"]["Coefs"], zeros(model.ω_threshold)) for s in model.freq_seasonal] + ω_values = [ + vcat( + zeros(s - 1 + max(0, max(2, model.stochastic_start) - s)), + model.output.components["ω_$(s)"]["Coefs"], + zeros(model.ω_threshold), + ) for s in model.freq_seasonal + ] else - ω_values = [model.output.components["ω_$(s)"]["Coefs"] for s in model.freq_seasonal] + ω_values = [ + model.output.components["ω_$(s)"]["Coefs"] for s in model.freq_seasonal + ] end else ω_values = [zeros(T) for _ in model.freq_seasonal] end - stochastic_seasonals_set = [vcat(final_ω[i], get_stochastic_values(ω_values[i], steps_ahead, T, start_idx[i], final_idx[i], seasonal_innovation_simulation)) for i in eachindex(model.freq_seasonal)] + stochastic_seasonals_set = [ + vcat( + final_ω[i], + get_stochastic_values( + ω_values[i], + steps_ahead, + T, + start_idx[i], + final_idx[i], + seasonal_innovation_simulation, + ), + ) for i in eachindex(model.freq_seasonal) + ] end if model.cycle @@ -1243,18 +1412,53 @@ function simulate_states( final_idx = [T - max(1, model.ϕ_threshold) for _ in eachindex(model.cycle_period)] if model.stochastic_cycle && !punctual if seasonal_innovation_simulation != 0 - ϕ_cos_values = [vcat(zeros(max(2, model.stochastic_start) - 1), model.output.components["ϕ_$(i)"]["Coefs"][1:2:end], zeros(max(1, model.ϕ_threshold))) for i in model.cycle_period] - ϕ_sin_values = [vcat(zeros(max(2, model.stochastic_start) - 1), model.output.components["ϕ_$(i)"]["Coefs"][2:2:end], zeros(max(1, model.ϕ_threshold))) for i in model.cycle_period] + ϕ_cos_values = [ + vcat( + zeros(max(2, model.stochastic_start) - 1), + model.output.components["ϕ_$(i)"]["Coefs"][1:2:end], + zeros(max(1, model.ϕ_threshold)), + ) for i in model.cycle_period + ] + ϕ_sin_values = [ + vcat( + zeros(max(2, model.stochastic_start) - 1), + model.output.components["ϕ_$(i)"]["Coefs"][2:2:end], + zeros(max(1, model.ϕ_threshold)), + ) for i in model.cycle_period + ] else - ϕ_cos_values = [model.output.components["ϕ_$(i)"]["Coefs"] for i in model.cycle_period] - ϕ_sin_values = [model.output.components["ϕ_$(i)"]["Coefs"][2:2:end] for i in model.cycle_period] + ϕ_cos_values = [ + model.output.components["ϕ_$(i)"]["Coefs"] for i in model.cycle_period + ] + ϕ_sin_values = [ + model.output.components["ϕ_$(i)"]["Coefs"][2:2:end] for + i in model.cycle_period + ] end else ϕ_cos_values = [zeros(T) for _ in model.cycle_period] ϕ_sin_values = [zeros(T) for _ in model.cycle_period] end - stochastic_cycles_cos_set = [get_stochastic_values(ϕ_cos_values[i], steps_ahead, T, start_idx[i], final_idx[i], seasonal_innovation_simulation) for i in eachindex(model.cycle_period)] - stochastic_cycles_sin_set = [get_stochastic_values(ϕ_sin_values[i], steps_ahead, T, start_idx[i], final_idx[i], seasonal_innovation_simulation) for i in eachindex(model.cycle_period)] + stochastic_cycles_cos_set = [ + get_stochastic_values( + ϕ_cos_values[i], + steps_ahead, + T, + start_idx[i], + final_idx[i], + seasonal_innovation_simulation, + ) for i in eachindex(model.cycle_period) + ] + stochastic_cycles_sin_set = [ + get_stochastic_values( + ϕ_sin_values[i], + steps_ahead, + T, + start_idx[i], + final_idx[i], + seasonal_innovation_simulation, + ) for i in eachindex(model.cycle_period) + ] end if model.outlier && !punctual @@ -1269,15 +1473,22 @@ function simulate_states( #stochastic_residuals_set = get_stochastic_values(model.output.ε, steps_ahead, T, 1, T, seasonal_innovation_simulation) stochastic_residuals_set = rand(model.output.ε, steps_ahead) end - - for t in T + 1:T + steps_ahead - + + for t in (T + 1):(T + steps_ahead) slope_t = model.slope ? slope[end] + stochastic_slope_set[t - T] : 0.0 - trend_t = (model.level || model.slope) ? trend[end] + slope[end] + stochastic_level_set[t - T] : 0.0 + trend_t = if (model.level || model.slope) + trend[end] + slope[end] + stochastic_level_set[t - T] + else + 0.0 + end if model.seasonal - seasonals_t = [seasonals[i][t - model.freq_seasonal[i]] + stochastic_seasonals_set[i][t - T + 1] - stochastic_seasonals_set[i][t - T] for i in eachindex(model.freq_seasonal)] + seasonals_t = [ + seasonals[i][t - model.freq_seasonal[i]] + + stochastic_seasonals_set[i][t - T + 1] - stochastic_seasonals_set[i][t - T] + for i in eachindex(model.freq_seasonal) + ] else seasonals_t = zeros(AbstractFloat, length(model.freq_seasonal)) end @@ -1287,13 +1498,23 @@ function simulate_states( for i in eachindex(model.cycle_period) ϕ_cos = model.output.components["ϕ_$(model.cycle_period[i])"]["Coefs"][1:2:end] ϕ_sin = model.output.components["ϕ_$(model.cycle_period[i])"]["Coefs"][2:2:end] - λ = 2 * pi * (1:T + steps_ahead) / model.cycle_period[i] - - cycle_t = dot(model.output.components["c1_$(model.cycle_period[i])"]["Coefs"], [cos(λ[t]), sin(λ[t])]) + - sum(ϕ_cos[j] * cos(λ[t]) + ϕ_sin[j] * sin(λ[t]) for j in eachindex(ϕ_cos)) + - sum(stochastic_cycles_cos_set[i][j] * cos(λ[t]) + stochastic_cycles_sin_set[i][j] * sin(λ[t]) for j in eachindex(stochastic_cycles_cos_set[i][1:t - T])) + λ = 2 * pi * (1:(T + steps_ahead)) / model.cycle_period[i] + + cycle_t = + dot( + model.output.components["c1_$(model.cycle_period[i])"]["Coefs"], + [cos(λ[t]), sin(λ[t])], + ) + + sum( + ϕ_cos[j] * cos(λ[t]) + ϕ_sin[j] * sin(λ[t]) for + j in eachindex(ϕ_cos) + ) + + sum( + stochastic_cycles_cos_set[i][j] * cos(λ[t]) + + stochastic_cycles_sin_set[i][j] * sin(λ[t]) for + j in eachindex(stochastic_cycles_cos_set[i][1:(t - T)]) + ) cycles_t[i] = cycle_t - end else cycles_t = zeros(AbstractFloat, length(model.cycle_period)) @@ -1302,7 +1523,9 @@ function simulate_states( outlier_t = (model.outlier && !punctual) ? stochastic_outliers_set[t - T] : 0.0 residuals_t = !punctual ? stochastic_residuals_set[t - T] : 0.0 - push!(prediction, trend_t + sum(seasonals_t) + sum(cycles_t) + outlier_t + residuals_t) + push!( + prediction, trend_t + sum(seasonals_t) + sum(cycles_t) + outlier_t + residuals_t + ) model.slope ? push!(slope, slope_t) : nothing model.level ? push!(trend, trend_t) : nothing if model.seasonal @@ -1310,7 +1533,6 @@ function simulate_states( seasonals[i] = vcat(seasonals[i], seasonals_t[i]) end end - end return prediction @@ -1329,7 +1551,9 @@ end # Returns - `Vector{AbstractFloat}`: Vector of combination components forecasts. """ -function forecast_dynamic_exog_coefs(model::StructuralModel, steps_ahead::Int, dynamic_exog_coefs_forecasts::Vector{<:Vector})::Vector{AbstractFloat} +function forecast_dynamic_exog_coefs( + model::StructuralModel, steps_ahead::Int, dynamic_exog_coefs_forecasts::Vector{<:Vector} +)::Vector{AbstractFloat} if !isempty(dynamic_exog_coefs_forecasts) T = length(model.y) dynamic_exog_coefs = Vector{Tuple}(undef, length(model.dynamic_exog_coefs)) @@ -1341,16 +1565,37 @@ function forecast_dynamic_exog_coefs(model::StructuralModel, steps_ahead::Int, d n_coefs = 1 + ζ_size(T, model.ζ_threshold, model.stochastic_start) extra_param = "" elseif model.dynamic_exog_coefs[i][2] == "seasonal" - n_coefs = model.dynamic_exog_coefs[i][3] + ω_size(T, model.dynamic_exog_coefs[i][3], model.ω_threshold, model.stochastic_start) + n_coefs = + model.dynamic_exog_coefs[i][3] + ω_size( + T, + model.dynamic_exog_coefs[i][3], + model.ω_threshold, + model.stochastic_start, + ) extra_param = model.dynamic_exog_coefs[i][3] elseif model.dynamic_exog_coefs[i][2] == "cycle" n_coefs = 2 + ϕ_size(T, model.ϕ_threshold, model.stochastic_start) extra_param = model.dynamic_exog_coefs[i][3] end - dynamic_exog_coefs[i] = (dynamic_exog_coefs_forecasts[i], model.dynamic_exog_coefs[i][2], extra_param, n_coefs) + dynamic_exog_coefs[i] = ( + dynamic_exog_coefs_forecasts[i], + model.dynamic_exog_coefs[i][2], + extra_param, + n_coefs, + ) end - dynamic_exog_coefs_forecasts_matrix = create_forecast_dynamic_exog_coefs_matrix(dynamic_exog_coefs, T, steps_ahead, model.ζ_threshold, model.ω_threshold, model.ϕ_threshold, model.stochastic_start) - dynamic_exog_coefs_prediction = dynamic_exog_coefs_forecasts_matrix * model.output.components["dynamic_exog_coefs"]["Coefs"] + dynamic_exog_coefs_forecasts_matrix = create_forecast_dynamic_exog_coefs_matrix( + dynamic_exog_coefs, + T, + steps_ahead, + model.ζ_threshold, + model.ω_threshold, + model.ϕ_threshold, + model.stochastic_start, + ) + dynamic_exog_coefs_prediction = + dynamic_exog_coefs_forecasts_matrix * + model.output.components["dynamic_exog_coefs"]["Coefs"] else dynamic_exog_coefs_prediction = zeros(steps_ahead) end @@ -1370,22 +1615,38 @@ end """ function forecast( - model::StructuralModel, steps_ahead::Int; + model::StructuralModel, + steps_ahead::Int; Exogenous_Forecast::Matrix{Fl}=zeros(steps_ahead, 0), - dynamic_exog_coefs_forecasts::Vector{<:Vector}=Vector{Vector}(undef, 0) -)::Vector{AbstractFloat} where {Fl<:AbstractFloat} - + dynamic_exog_coefs_forecasts::Vector{<:Vector}=Vector{Vector}(undef, 0), +)::Vector{AbstractFloat} where {Fl<:AbstractFloat} states_prediction = simulate_states(model, steps_ahead, true, 0) @assert size(Exogenous_Forecast, 1) == steps_ahead - @assert all(length(dynamic_exog_coefs_forecasts[i]) == steps_ahead for i in eachindex(dynamic_exog_coefs_forecasts)) - !isnothing(model.dynamic_exog_coefs) ? (@assert length(dynamic_exog_coefs_forecasts) == length(model.dynamic_exog_coefs)) : nothing - (dynamic_exog_coefs_forecasts == Vector{Vector}(undef, 0)) ? (@assert isnothing(model.dynamic_exog_coefs)) : nothing + @assert all( + length(dynamic_exog_coefs_forecasts[i]) == steps_ahead for + i in eachindex(dynamic_exog_coefs_forecasts) + ) + if !isnothing(model.dynamic_exog_coefs) + (@assert length(dynamic_exog_coefs_forecasts) == length(model.dynamic_exog_coefs)) + else + nothing + end + if (dynamic_exog_coefs_forecasts == Vector{Vector}(undef, 0)) + (@assert isnothing(model.dynamic_exog_coefs)) + else + nothing + end @assert size(Exogenous_Forecast, 2) == model.n_exogenous - dynamic_exog_coefs_prediction = forecast_dynamic_exog_coefs(model, steps_ahead, dynamic_exog_coefs_forecasts) + dynamic_exog_coefs_prediction = forecast_dynamic_exog_coefs( + model, steps_ahead, dynamic_exog_coefs_forecasts + ) - prediction = states_prediction + (Exogenous_Forecast * model.output.components["exog"]["Coefs"]) + dynamic_exog_coefs_prediction + prediction = + states_prediction + + (Exogenous_Forecast * model.output.components["exog"]["Coefs"]) + + dynamic_exog_coefs_prediction return prediction end @@ -1408,21 +1669,28 @@ end - `Matrix{AbstractFloat}`: Matrix of scenarios of the states of the model. """ function simulate( - model::StructuralModel, steps_ahead::Int, N_scenarios::Int; + model::StructuralModel, + steps_ahead::Int, + N_scenarios::Int; Exogenous_Forecast::Matrix{Fl}=zeros(steps_ahead, 0), dynamic_exog_coefs_forecasts::Vector{<:Vector}=Vector{Vector}(undef, 0), seasonal_innovation_simulation::Int=0, - seed::Int=1234 + seed::Int=1234, )::Matrix{AbstractFloat} where {Fl<:AbstractFloat} - scenarios = Matrix{AbstractFloat}(undef, steps_ahead, N_scenarios) Random.seed!(seed) for s in 1:N_scenarios - scenarios[:, s] = simulate_states(model, steps_ahead, false, seasonal_innovation_simulation) + scenarios[:, s] = simulate_states( + model, steps_ahead, false, seasonal_innovation_simulation + ) end - dynamic_exog_coefs_prediction = forecast_dynamic_exog_coefs(model, steps_ahead, dynamic_exog_coefs_forecasts) - scenarios .+= (Exogenous_Forecast * model.output.components["exog"]["Coefs"]) + dynamic_exog_coefs_prediction + dynamic_exog_coefs_prediction = forecast_dynamic_exog_coefs( + model, steps_ahead, dynamic_exog_coefs_forecasts + ) + scenarios .+= + (Exogenous_Forecast * model.output.components["exog"]["Coefs"]) + + dynamic_exog_coefs_prediction return scenarios end diff --git a/src/utils.jl b/src/utils.jl index 36cd040..0511530 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -29,9 +29,7 @@ function build_components( X[:, components_indexes[key]] * coefs[components_indexes[key]] end if haskey(components, "exog") - components["exog"]["Selected"] = findall( - i -> i != 0, components["exog"]["Coefs"] - ) + components["exog"]["Selected"] = findall(i -> i != 0, components["exog"]["Coefs"]) end return components end @@ -134,14 +132,21 @@ get_stochastic_values(estimated_stochastic::Vector{Fl}, steps_ahead::Int, T::Int # Returns - `Vector{AbstractFloat}`: Vector of stochastic seasonal values. """ -function get_stochastic_values(estimated_stochastic::Vector{Fl}, steps_ahead::Int, T::Int, start_idx::Int, final_idx::Int, seasonal_innovation_simulation::Int)::Vector{AbstractFloat} where {Fl<:AbstractFloat} - +function get_stochastic_values( + estimated_stochastic::Vector{Fl}, + steps_ahead::Int, + T::Int, + start_idx::Int, + final_idx::Int, + seasonal_innovation_simulation::Int, +)::Vector{AbstractFloat} where {Fl<:AbstractFloat} if seasonal_innovation_simulation != 0 stochastic_term = Vector{AbstractFloat}(undef, steps_ahead) for t in 1:steps_ahead # Generate potential seasonal indices - seasonal_indices = (T + t) % seasonal_innovation_simulation : seasonal_innovation_simulation : T + seasonal_indices = + ((T + t) % seasonal_innovation_simulation):seasonal_innovation_simulation:T # Filter indices to be within the valid range valid_indices = filter(idx -> start_idx <= idx <= final_idx, seasonal_indices) @@ -150,9 +155,9 @@ function get_stochastic_values(estimated_stochastic::Vector{Fl}, steps_ahead::In stochastic_term[t] = rand(estimated_stochastic[valid_indices]) * rand([1, -1]) end else - stochastic_term = rand(estimated_stochastic, steps_ahead) .* rand([1, -1], steps_ahead) + stochastic_term = + rand(estimated_stochastic, steps_ahead) .* rand([1, -1], steps_ahead) end return stochastic_term - end diff --git a/test/fit_forecast.jl b/test/fit_forecast.jl index e718e1a..4a8993e 100644 --- a/test/fit_forecast.jl +++ b/test/fit_forecast.jl @@ -24,4 +24,4 @@ @test length(model2.output.residuals_variances) == 4 @test length(keys(model2.output.components)) == 10 @test length(keys(model2.output.decomposition)) == 3 -end \ No newline at end of file +end diff --git a/test/models/structural_model.jl b/test/models/structural_model.jl index 5c65c0e..8535253 100644 --- a/test/models/structural_model.jl +++ b/test/models/structural_model.jl @@ -4,9 +4,7 @@ model1 = StateSpaceLearning.StructuralModel(y1) model2 = StateSpaceLearning.StructuralModel(y1; freq_seasonal=[3, 10]) model3 = StateSpaceLearning.StructuralModel(y1; cycle_period=[3, 10.2]) - model4 = StateSpaceLearning.StructuralModel( - y1; cycle_period=[3, 10.2] - ) + model4 = StateSpaceLearning.StructuralModel(y1; cycle_period=[3, 10.2]) @test typeof(model1) == StateSpaceLearning.StructuralModel @test typeof(model2) == StateSpaceLearning.StructuralModel @@ -21,9 +19,7 @@ @test_throws AssertionError StateSpaceLearning.StructuralModel(y1; freq_seasonal=1000) exog_error = ones(100, 3) - @test_throws AssertionError StateSpaceLearning.StructuralModel( - y1; exog=exog_error - ) + @test_throws AssertionError StateSpaceLearning.StructuralModel(y1; exog=exog_error) end @testset "create deterministic matrices" begin @@ -34,27 +30,16 @@ end @test size(X2) == (100, 12) X3 = StateSpaceLearning.create_initial_states_Matrix( - 100, - [12, 20], - true, - true, - true, - true, - [3, 10.2]) + 100, [12, 20], true, true, true, true, [3, 10.2] + ) @test size(X3) == (100, 38) X4 = StateSpaceLearning.create_initial_states_Matrix( - 100, - 12, - true, - true, - true, - false, - 3) + 100, 12, true, true, true, false, 3 + ) @test size(X4) == (100, 14) - end @testset "Innovation matrices" begin @@ -115,88 +100,108 @@ end 4.0 3.0 ] - @test X_ζ3 == zeros(5,0) + @test X_ζ3 == zeros(5, 0) X_ω1 = StateSpaceLearning.create_ω(5, 2, 0, 1) X_ω2 = StateSpaceLearning.create_ω(5, 2, 2, 1) X_ω3 = StateSpaceLearning.create_ω(5, 2, 0, 3) @test X_ω1 == [ - 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 - -1.0 1.0 0.0 0.0 - 0.0 -1.0 1.0 0.0 - -1.0 1.0 -1.0 1.0 + 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 + -1.0 1.0 0.0 0.0 + 0.0 -1.0 1.0 0.0 + -1.0 1.0 -1.0 1.0 ] @test X_ω2 == [ - 0.0 0.0 - 0.0 0.0 - -1.0 1.0 - 0.0 -1.0 - -1.0 1.0 + 0.0 0.0 + 0.0 0.0 + -1.0 1.0 + 0.0 -1.0 + -1.0 1.0 ] @test X_ω3 == [ - 0.0 0.0 0.0 - 0.0 0.0 0.0 - 1.0 0.0 0.0 - -1.0 1.0 0.0 - 1.0 -1.0 1.0 + 0.0 0.0 0.0 + 0.0 0.0 0.0 + 1.0 0.0 0.0 + -1.0 1.0 0.0 + 1.0 -1.0 1.0 ] X_o1 = StateSpaceLearning.create_o_matrix(3, 1) X_o3 = StateSpaceLearning.create_o_matrix(3, 2) @test X_o1 == Matrix(1.0 * I, 3, 3) - @test X_o3 == [ 0.0 0.0 - 1.0 0.0 - 0.0 1.0] + @test X_o3 == [ + 0.0 0.0 + 1.0 0.0 + 0.0 1.0 + ] X_ϕ1 = StateSpaceLearning.create_ϕ(3, 5, 0, 1) X_ϕ2 = StateSpaceLearning.create_ϕ(3, 5, 3, 1) X_ϕ3 = StateSpaceLearning.create_ϕ(3, 5, 0, 2) @test X_ϕ1 == [ - 0.0 0.0 0.0 0.0 0.0 0.0 - -0.5 -0.86603 0.0 0.0 0.0 0.0 - 1.0 -0.0 1.0 -0.0 0.0 0.0 - -0.5 0.86603 -0.5 0.86603 -0.5 0.86603 - -0.5 -0.86603 -0.5 -0.86603 -0.5 -0.86603 + 0.0 0.0 0.0 0.0 0.0 0.0 + -0.5 -0.86603 0.0 0.0 0.0 0.0 + 1.0 -0.0 1.0 -0.0 0.0 0.0 + -0.5 0.86603 -0.5 0.86603 -0.5 0.86603 + -0.5 -0.86603 -0.5 -0.86603 -0.5 -0.86603 ] @test X_ϕ2 == [ - 0.0 0.0 - -0.5 -0.86603 - 1.0 -0.0 - -0.5 0.86603 - -0.5 -0.86603 + 0.0 0.0 + -0.5 -0.86603 + 1.0 -0.0 + -0.5 0.86603 + -0.5 -0.86603 ] @test X_ϕ3 == [ - 0.0 0.0 0.0 0.0 0.0 0.0 - -0.5 -0.86603 0.0 0.0 0.0 0.0 - 1.0 -0.0 1.0 -0.0 0.0 0.0 - -0.5 0.86603 -0.5 0.86603 -0.5 0.86603 - -0.5 -0.86603 -0.5 -0.86603 -0.5 -0.86603 + 0.0 0.0 0.0 0.0 0.0 0.0 + -0.5 -0.86603 0.0 0.0 0.0 0.0 + 1.0 -0.0 1.0 -0.0 0.0 0.0 + -0.5 0.86603 -0.5 0.86603 -0.5 0.86603 + -0.5 -0.86603 -0.5 -0.86603 -0.5 -0.86603 ] end @testset "dynamic_exog_coefs" begin + dynamic_exog_coefs = [ + (collect(1:5), "level"), + (collect(1:5), "slope"), + (collect(1:5), "seasonal", 2), + (collect(1:5), "cycle", 3), + ] - dynamic_exog_coefs = [(collect(1:5), "level"), (collect(1:5), "slope"), (collect(1:5), "seasonal", 2), (collect(1:5), "cycle", 3)] - - X = StateSpaceLearning.create_dynamic_exog_coefs_matrix(dynamic_exog_coefs, 5, 0, 0, 0, 1) + X = StateSpaceLearning.create_dynamic_exog_coefs_matrix( + dynamic_exog_coefs, 5, 0, 0, 0, 1 + ) @test size(X) == (5, 22) - dynamic_exog_coefs = [(collect(6:7), "level", "", 4), (collect(6:7), "slope", "", 4), (collect(6:7), "seasonal", 2, 7), (collect(6:7), "cycle", 3, 8)] - X_f = StateSpaceLearning.create_forecast_dynamic_exog_coefs_matrix(dynamic_exog_coefs, 5, 2, 0, 0, 0, 1) + dynamic_exog_coefs = [ + (collect(6:7), "level", "", 4), + (collect(6:7), "slope", "", 4), + (collect(6:7), "seasonal", 2, 7), + (collect(6:7), "cycle", 3, 8), + ] + X_f = StateSpaceLearning.create_forecast_dynamic_exog_coefs_matrix( + dynamic_exog_coefs, 5, 2, 0, 0, 0, 1 + ) @test size(X_f) == (2, 23) end @testset "Create X matrix" begin exog1 = rand(5, 3) - dynamic_exog_coefs = [(collect(1:5), "level"), (collect(1:5), "slope"), (collect(1:5), "seasonal", 2), (collect(1:5), "cycle", 3)] + dynamic_exog_coefs = [ + (collect(1:5), "level"), + (collect(1:5), "slope"), + (collect(1:5), "seasonal", 2), + (collect(1:5), "cycle", 3), + ] X1 = StateSpaceLearning.create_X( true, @@ -215,11 +220,16 @@ end 0, 1, exog1, - dynamic_exog_coefs + dynamic_exog_coefs, ) exog2 = zeros(10, 3) - dynamic_exog_coefs2 = [(collect(1:10), "level"), (collect(1:10), "slope"), (collect(1:10), "seasonal", 2), (collect(1:10), "cycle", 3)] + dynamic_exog_coefs2 = [ + (collect(1:10), "level"), + (collect(1:10), "slope"), + (collect(1:10), "seasonal", 2), + (collect(1:10), "cycle", 3), + ] X2 = StateSpaceLearning.create_X( true, @@ -238,12 +248,11 @@ end 4, 1, exog2, - dynamic_exog_coefs2 + dynamic_exog_coefs2, ) @test size(X1) == (5, 52) @test size(X2) == (10, 85) - end @testset "Function: get_components" begin @@ -251,12 +260,7 @@ end exog2 = zeros(10, 0) Basic_Structural = StateSpaceLearning.StructuralModel( - rand(10); - freq_seasonal=2, - outlier=true, - ζ_threshold=0, - ω_threshold=0, - exog=exog1, + rand(10); freq_seasonal=2, outlier=true, ζ_threshold=0, ω_threshold=0, exog=exog1 ) Local_Level = StateSpaceLearning.StructuralModel( rand(10); @@ -269,12 +273,7 @@ end exog=exog1, ) Local_Linear_Trend1 = StateSpaceLearning.StructuralModel( - rand(10); - seasonal="none", - cycle="none", - outlier=false, - ζ_threshold=0, - exog=exog1, + rand(10); seasonal="none", cycle="none", outlier=false, ζ_threshold=0, exog=exog1 ) Local_Linear_Trend2 = StateSpaceLearning.StructuralModel( rand(10); @@ -314,18 +313,10 @@ end exog2 = zeros(10, 0) Basic_Structural = StateSpaceLearning.StructuralModel( - rand(10); - freq_seasonal=2, - outlier=true, - ζ_threshold=0, - exog=exog2, + rand(10); freq_seasonal=2, outlier=true, ζ_threshold=0, exog=exog2 ) Basic_Structural2 = StateSpaceLearning.StructuralModel( - rand(10); - freq_seasonal=[2, 5], - outlier=true, - ζ_threshold=0, - exog=exog2, + rand(10); freq_seasonal=[2, 5], outlier=true, ζ_threshold=0, exog=exog2 ) Local_Level = StateSpaceLearning.StructuralModel( rand(10); @@ -373,11 +364,7 @@ end ] models_innovations = [ - ["ξ", "ζ", "ω_2"], - ["ξ", "ζ", "ω_2", "ω_5"], - ["ξ"], - ["ξ", "ζ"], - ["ξ", "ζ", "ϕ_3"], + ["ξ", "ζ", "ω_2"], ["ξ", "ζ", "ω_2", "ω_5"], ["ξ"], ["ξ", "ζ"], ["ξ", "ζ", "ϕ_3"] ] for idx in eachindex(models) @@ -390,70 +377,127 @@ end model_innovations = StateSpaceLearning.get_model_innovations(model) @test model_innovations == models_innovations[idx] end - end @testset "Decomposion functions" begin Random.seed!(123) - model = StateSpaceLearning.StructuralModel(vcat(collect(1:5), collect(5:-1:1));level="deterministic", seasonal="none", cycle="none", outlier=false, slope="stochastic", ζ_threshold=0) + model = StateSpaceLearning.StructuralModel( + vcat(collect(1:5), collect(5:-1:1)); + level="deterministic", + seasonal="none", + cycle="none", + outlier=false, + slope="stochastic", + ζ_threshold=0, + ) StateSpaceLearning.fit!(model) slope = StateSpaceLearning.get_slope_decomposition(model, model.output.components) - @test all(isapprox.(slope, - [ 0.3195538151032132 - 0.3195538151032132 - 1.0535772194857598 - 1.1323058058970006 - 0.9011191905923782 - 0.07553685115943187 - -0.8838426390957513 - -1.044032162858364 - -1.0251744597550265 - -1.0019651980300468], atol=1e-6)) - - model = StateSpaceLearning.StructuralModel(vcat(rand(5) .+ 5, rand(5) .- 5) + vcat(collect(1:5), collect(5:-1:1));seasonal="none", cycle="none", outlier=false, slope="stochastic", ζ_threshold=0) + @test all( + isapprox.( + slope, + [ + 0.3195538151032132 + 0.3195538151032132 + 1.0535772194857598 + 1.1323058058970006 + 0.9011191905923782 + 0.07553685115943187 + -0.8838426390957513 + -1.044032162858364 + -1.0251744597550265 + -1.0019651980300468 + ], + atol=1e-4, + ), + ) + + model = StateSpaceLearning.StructuralModel( + vcat(rand(5) .+ 5, rand(5) .- 5) + vcat(collect(1:5), collect(5:-1:1)); + seasonal="none", + cycle="none", + outlier=false, + slope="stochastic", + ζ_threshold=0, + ) StateSpaceLearning.fit!(model) - @test all(isapprox.(StateSpaceLearning.get_trend_decomposition(model, model.output.components, slope), - [ 6.544506301918287 - 7.9278752338886775 - 10.266929548367902 - 11.728283090188654 - 13.666722760765126 - 4.077371477199227 - 1.7029908259414626 - 0.5101771103035202 - -2.2199584710270805 - -3.2219236690571273 - ], atol=1e-6)) - - model = StateSpaceLearning.StructuralModel(rand(10); cycle="stochastic", cycle_period=3, outlier=false, slope="stochastic", ζ_threshold=0, freq_seasonal=3, ω_threshold=0, ϕ_threshold=0) + @test all( + isapprox.( + StateSpaceLearning.get_trend_decomposition( + model, model.output.components, slope + ), + [ + 6.544506301918287 + 7.9278752338886775 + 10.266929548367902 + 11.728283090188654 + 13.666722760765126 + 4.077371477199227 + 1.7029908259414626 + 0.5101771103035202 + -2.2199584710270805 + -3.2219236690571273 + ], + atol=1e-4, + ), + ) + + model = StateSpaceLearning.StructuralModel( + rand(10); + cycle="stochastic", + cycle_period=3, + outlier=false, + slope="stochastic", + ζ_threshold=0, + freq_seasonal=3, + ω_threshold=0, + ϕ_threshold=0, + ) StateSpaceLearning.fit!(model) - @test all(isapprox.(StateSpaceLearning.get_seasonal_decomposition(model, model.output.components, 3), - [ -0.011114430313782316 - 0.016993897901375513 - 0.0 - -0.06137711460224293 - -0.028221145277986203 - 0.045215043179361716 - -0.0027753371516487588 - -0.08682292272858037 - -0.036923166352424444 - 0.13243351808078532 - ], atol=1e-6)) - - @test all(isapprox.(StateSpaceLearning.get_cycle_decomposition(model, model.output.components, 3), - [ 0.0 - 0.0 - 1.6111635465327112e-18 - -0.005696779520104198 - -0.030765036256794644 - 0.08224069806471179 - 0.030974382058151183 - -0.15828117942894446 - 0.037361403241860734 - 0.17009485813575637], atol=1e-6)) - - model_decomposition = StateSpaceLearning.get_model_decomposition(model, model.output.components) - @test sort(collect(keys(model_decomposition))) == sort([ "cycle_3", "cycle_hat_3", "seasonal_3", "slope", "trend"]) + @test all( + isapprox.( + StateSpaceLearning.get_seasonal_decomposition( + model, model.output.components, 3 + ), + [ + -0.011114430313782316 + 0.016993897901375513 + 0.0 + -0.06137711460224293 + -0.028221145277986203 + 0.045215043179361716 + -0.0027753371516487588 + -0.08682292272858037 + -0.036923166352424444 + 0.13243351808078532 + ], + atol=1e-6, + ), + ) + + @test all( + isapprox.( + StateSpaceLearning.get_cycle_decomposition(model, model.output.components, 3), + [ + 0.0 + 0.0 + 1.6111635465327112e-18 + -0.005696779520104198 + -0.030765036256794644 + 0.08224069806471179 + 0.030974382058151183 + -0.15828117942894446 + 0.037361403241860734 + 0.17009485813575637 + ], + atol=1e-6, + ), + ) + + model_decomposition = StateSpaceLearning.get_model_decomposition( + model, model.output.components + ) + @test sort(collect(keys(model_decomposition))) == + sort(["cycle_3", "cycle_hat_3", "seasonal_3", "slope", "trend"]) end @testset "Function: simulate_states" begin @@ -463,7 +507,9 @@ end @test length(StateSpaceLearning.simulate_states(model, 8, false, 12)) == 8 @test length(StateSpaceLearning.simulate_states(model, 10, false, 0)) == 10 - model = StateSpaceLearning.StructuralModel(rand(100); seasonal="none", cycle="stochastic", cycle_period=3, outlier=false) + model = StateSpaceLearning.StructuralModel( + rand(100); seasonal="none", cycle="stochastic", cycle_period=3, outlier=false + ) StateSpaceLearning.fit!(model) @test length(StateSpaceLearning.simulate_states(model, 10, true, 12)) == 10 @test length(StateSpaceLearning.simulate_states(model, 8, false, 12)) == 8 @@ -471,17 +517,38 @@ end end @testset "Function: forecast_dynamic_exog_coefs" begin - model = StateSpaceLearning.StructuralModel(rand(100); seasonal="none", cycle="stochastic", cycle_period=3, outlier=false) + model = StateSpaceLearning.StructuralModel( + rand(100); seasonal="none", cycle="stochastic", cycle_period=3, outlier=false + ) StateSpaceLearning.fit!(model) - @test StateSpaceLearning.forecast_dynamic_exog_coefs(model, 10, Vector{Vector}(undef, 0)) == zeros(10) - @test StateSpaceLearning.forecast_dynamic_exog_coefs(model, 8, Vector{Vector}(undef, 0)) == zeros(8) - - dynamic_exog_coefs = [(collect(1:100), "level"), (collect(1:100), "slope"), (collect(1:100), "seasonal", 2), (collect(1:100), "cycle", 3)] - forecast_dynamic_exog_coefs = [collect(101:110), collect(101:110), collect(101:110), collect(101:110)] - model2 = StateSpaceLearning.StructuralModel(rand(100); dynamic_exog_coefs = dynamic_exog_coefs) + @test StateSpaceLearning.forecast_dynamic_exog_coefs( + model, 10, Vector{Vector}(undef, 0) + ) == zeros(10) + @test StateSpaceLearning.forecast_dynamic_exog_coefs( + model, 8, Vector{Vector}(undef, 0) + ) == zeros(8) + + dynamic_exog_coefs = [ + (collect(1:100), "level"), + (collect(1:100), "slope"), + (collect(1:100), "seasonal", 2), + (collect(1:100), "cycle", 3), + ] + forecast_dynamic_exog_coefs = [ + collect(101:110), collect(101:110), collect(101:110), collect(101:110) + ] + model2 = StateSpaceLearning.StructuralModel( + rand(100); dynamic_exog_coefs=dynamic_exog_coefs + ) StateSpaceLearning.fit!(model2) - @test StateSpaceLearning.forecast_dynamic_exog_coefs(model2, 10, forecast_dynamic_exog_coefs) != zeros(10) - @test length(StateSpaceLearning.forecast_dynamic_exog_coefs(model2, 10, forecast_dynamic_exog_coefs)) == 10 + @test StateSpaceLearning.forecast_dynamic_exog_coefs( + model2, 10, forecast_dynamic_exog_coefs + ) != zeros(10) + @test length( + StateSpaceLearning.forecast_dynamic_exog_coefs( + model2, 10, forecast_dynamic_exog_coefs + ), + ) == 10 end @testset "Function: forecast" begin @@ -686,15 +753,35 @@ end model5 = StateSpaceLearning.StructuralModel(y3; exog=exog) StateSpaceLearning.fit!(model5) exog_forecast = rand(18, 3) - forecast5 = trunc.(StateSpaceLearning.forecast(model5, 18; Exogenous_Forecast=exog_forecast); digits=3) + forecast5 = + trunc.( + StateSpaceLearning.forecast(model5, 18; Exogenous_Forecast=exog_forecast); + digits=3, + ) @test length(forecast5) == 18 - dynamic_exog_coefs = [(collect(1:length(y3)), "level"), (collect(1:length(y3)), "slope"), (collect(1:length(y3)), "seasonal", 2), (collect(1:length(y3)), "cycle", 3)] - forecast_dynamic_exog_coefs = [collect(length(y3) + 1:length(y3) + 10), collect(length(y3) + 1:length(y3) + 10), collect(length(y3) + 1:length(y3) + 10), collect(length(y3) + 1:length(y3) + 10)] - model6 = StateSpaceLearning.StructuralModel(y3; dynamic_exog_coefs = dynamic_exog_coefs) + dynamic_exog_coefs = [ + (collect(1:length(y3)), "level"), + (collect(1:length(y3)), "slope"), + (collect(1:length(y3)), "seasonal", 2), + (collect(1:length(y3)), "cycle", 3), + ] + forecast_dynamic_exog_coefs = [ + collect((length(y3) + 1):(length(y3) + 10)), + collect((length(y3) + 1):(length(y3) + 10)), + collect((length(y3) + 1):(length(y3) + 10)), + collect((length(y3) + 1):(length(y3) + 10)), + ] + model6 = StateSpaceLearning.StructuralModel(y3; dynamic_exog_coefs=dynamic_exog_coefs) StateSpaceLearning.fit!(model6) - @test StateSpaceLearning.forecast_dynamic_exog_coefs(model6, 10, forecast_dynamic_exog_coefs) != zeros(10) - @test length(StateSpaceLearning.forecast_dynamic_exog_coefs(model6, 10, forecast_dynamic_exog_coefs)) == 10 + @test StateSpaceLearning.forecast_dynamic_exog_coefs( + model6, 10, forecast_dynamic_exog_coefs + ) != zeros(10) + @test length( + StateSpaceLearning.forecast_dynamic_exog_coefs( + model6, 10, forecast_dynamic_exog_coefs + ), + ) == 10 end @testset "Function: simulate" begin @@ -724,14 +811,25 @@ end StateSpaceLearning.simulate(model5, 10, 100; Exogenous_Forecast=exog_forecast) ) == (10, 100) - dynamic_exog_coefs = [(collect(1:length(y2)), "level"), (collect(1:length(y2)), "slope"), (collect(1:length(y2)), "seasonal", 2), (collect(1:length(y2)), "cycle", 3)] - forecast_dynamic_exog_coefs = [collect(length(y2) + 1:length(y2) + 10), collect(length(y2) + 1:length(y2) + 10), collect(length(y2) + 1:length(y2) + 10), collect(length(y2) + 1:length(y2) + 10)] - model6 = StateSpaceLearning.StructuralModel(y2; dynamic_exog_coefs = dynamic_exog_coefs) + dynamic_exog_coefs = [ + (collect(1:length(y2)), "level"), + (collect(1:length(y2)), "slope"), + (collect(1:length(y2)), "seasonal", 2), + (collect(1:length(y2)), "cycle", 3), + ] + forecast_dynamic_exog_coefs = [ + collect((length(y2) + 1):(length(y2) + 10)), + collect((length(y2) + 1):(length(y2) + 10)), + collect((length(y2) + 1):(length(y2) + 10)), + collect((length(y2) + 1):(length(y2) + 10)), + ] + model6 = StateSpaceLearning.StructuralModel(y2; dynamic_exog_coefs=dynamic_exog_coefs) StateSpaceLearning.fit!(model6) @test size( - StateSpaceLearning.simulate(model6, 10, 100; dynamic_exog_coefs_forecasts=forecast_dynamic_exog_coefs) + StateSpaceLearning.simulate( + model6, 10, 100; dynamic_exog_coefs_forecasts=forecast_dynamic_exog_coefs + ), ) == (10, 100) - end @testset "Basics" begin @@ -740,4 +838,4 @@ end StateSpaceLearning.fit!(model) @test StateSpaceLearning.isfitted(model) == true -end \ No newline at end of file +end diff --git a/test/utils.jl b/test/utils.jl index e246398..6041a32 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -108,24 +108,53 @@ end T = 10 start_idx = 1 final_idx = 10 - seasonal_innovation_simulation1 = 0 + seasonal_innovation_simulation1 = 0 seasonal_innovation_simulation2 = 2 - - st_values1 = StateSpaceLearning.get_stochastic_values(estimated_stochastic, steps_ahead, T, start_idx, final_idx, seasonal_innovation_simulation1) - st_values2 = StateSpaceLearning.get_stochastic_values(estimated_stochastic, steps_ahead, T, start_idx, final_idx, seasonal_innovation_simulation2) + + st_values1 = StateSpaceLearning.get_stochastic_values( + estimated_stochastic, + steps_ahead, + T, + start_idx, + final_idx, + seasonal_innovation_simulation1, + ) + st_values2 = StateSpaceLearning.get_stochastic_values( + estimated_stochastic, + steps_ahead, + T, + start_idx, + final_idx, + seasonal_innovation_simulation2, + ) @test length(st_values1) == steps_ahead @test length(st_values2) == steps_ahead - @test all(isapprox.(st_values1, [ 0.6395615996802734 - -0.8396219340580711 - 0.6395615996802734 - -0.5798621201341324 - 0.967142768915383], atol=1e-6)) - @test all(isapprox.(st_values2, [ 0.520354993723718 - -0.014908849285099945 - -0.13102565622085904 - -0.6395615996802734 - -0.520354993723718], atol=1e-6)) + @test all( + isapprox.( + st_values1, + [ + 0.6395615996802734 + -0.8396219340580711 + 0.6395615996802734 + -0.5798621201341324 + 0.967142768915383 + ], + atol=1e-6, + ), + ) + @test all( + isapprox.( + st_values2, + [ + 0.520354993723718 + -0.014908849285099945 + -0.13102565622085904 + -0.6395615996802734 + -0.520354993723718 + ], + atol=1e-6, + ), + ) end - From 59e0df67c4d5550f30e0c53fa7338403b0a6d87b Mon Sep 17 00:00:00 2001 From: andre_ramos Date: Mon, 28 Apr 2025 14:37:13 -0300 Subject: [PATCH 3/6] fix format --- src/models/structural_model.jl | 63 ++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/src/models/structural_model.jl b/src/models/structural_model.jl index c9931ca..6e44ad1 100644 --- a/src/models/structural_model.jl +++ b/src/models/structural_model.jl @@ -542,8 +542,8 @@ function create_dynamic_exog_coefs_matrix( nothing else state_components_dict["level"] = hcat( - ones(T, 1), create_ξ(T, stochastic_start) - ) + ones(T, 1), create_ξ(T, stochastic_start) + ) end key_name = "level" elseif combination[2] == "slope" @@ -551,8 +551,9 @@ function create_dynamic_exog_coefs_matrix( nothing else state_components_dict["slope"] = hcat( - vcat([0], collect(1:(T - 1))), create_ζ(T, ζ_threshold, stochastic_start) - ) + vcat([0], collect(1:(T - 1))), + create_ζ(T, ζ_threshold, stochastic_start), + ) end key_name = "slope" elseif combination[2] == "seasonal" @@ -560,9 +561,9 @@ function create_dynamic_exog_coefs_matrix( nothing else state_components_dict["seasonal_$(combination[3])"] = hcat( - create_deterministic_seasonal(T, combination[3]), - create_ω(T, combination[3], ω_threshold, stochastic_start), - ) + create_deterministic_seasonal(T, combination[3]), + create_ω(T, combination[3], ω_threshold, stochastic_start), + ) end key_name = "seasonal_$(combination[3])" elseif combination[2] == "cycle" @@ -570,9 +571,9 @@ function create_dynamic_exog_coefs_matrix( nothing else state_components_dict["cycle_$(combination[3])"] = hcat( - create_deterministic_cycle(T, combination[3]), - create_ϕ(combination[3], T, ϕ_threshold, stochastic_start), - ) + create_deterministic_cycle(T, combination[3]), + create_ϕ(combination[3], T, ϕ_threshold, stochastic_start), + ) end key_name = "cycle_$(combination[3])" end @@ -617,10 +618,10 @@ function create_forecast_dynamic_exog_coefs_matrix( nothing else state_components_dict["level"] = hcat( - ones(T + steps_ahead, 1), create_ξ(T + steps_ahead, stochastic_start) - )[ - (end - steps_ahead + 1):end, 1:combination[4] - ] + ones(T + steps_ahead, 1), create_ξ(T + steps_ahead, stochastic_start) + )[ + (end - steps_ahead + 1):end, 1:combination[4] + ] end key_name = "level" elseif combination[2] == "slope" @@ -628,11 +629,11 @@ function create_forecast_dynamic_exog_coefs_matrix( nothing else state_components_dict["slope"] = hcat( - vcat([0], collect(1:(T + steps_ahead - 1))), - create_ζ(T + steps_ahead, ζ_threshold, stochastic_start), - )[ - (end - steps_ahead + 1):end, 1:combination[4] - ] + vcat([0], collect(1:(T + steps_ahead - 1))), + create_ζ(T + steps_ahead, ζ_threshold, stochastic_start), + )[ + (end - steps_ahead + 1):end, 1:combination[4] + ] end key_name = "slope" elseif combination[2] == "seasonal" @@ -640,11 +641,13 @@ function create_forecast_dynamic_exog_coefs_matrix( nothing else state_components_dict["seasonal_$(combination[3])"] = hcat( - create_deterministic_seasonal(T + steps_ahead, combination[3]), - create_ω(T + steps_ahead, combination[3], ω_threshold, stochastic_start), - )[ - (end - steps_ahead + 1):end, 1:combination[4] - ] + create_deterministic_seasonal(T + steps_ahead, combination[3]), + create_ω( + T + steps_ahead, combination[3], ω_threshold, stochastic_start + ), + )[ + (end - steps_ahead + 1):end, 1:combination[4] + ] end key_name = "seasonal_$(combination[3])" elseif combination[2] == "cycle" @@ -652,11 +655,13 @@ function create_forecast_dynamic_exog_coefs_matrix( nothing else state_components_dict["cycle_$(combination[3])"] = hcat( - create_deterministic_cycle(T + steps_ahead, combination[3]), - create_ϕ(combination[3], T + steps_ahead, ϕ_threshold, stochastic_start), - )[ - (end - steps_ahead + 1):end, 1:combination[4] - ] + create_deterministic_cycle(T + steps_ahead, combination[3]), + create_ϕ( + combination[3], T + steps_ahead, ϕ_threshold, stochastic_start + ), + )[ + (end - steps_ahead + 1):end, 1:combination[4] + ] end key_name = "cycle_$(combination[3])" end From c4f88fd044d95a940f7a6e2535eadbcd790936e5 Mon Sep 17 00:00:00 2001 From: andre_ramos Date: Mon, 28 Apr 2025 14:43:23 -0300 Subject: [PATCH 4/6] fix tests --- test/models/structural_model.jl | 41 ++++----------------------------- 1 file changed, 4 insertions(+), 37 deletions(-) diff --git a/test/models/structural_model.jl b/test/models/structural_model.jl index 8535253..3910d11 100644 --- a/test/models/structural_model.jl +++ b/test/models/structural_model.jl @@ -392,24 +392,7 @@ end ) StateSpaceLearning.fit!(model) slope = StateSpaceLearning.get_slope_decomposition(model, model.output.components) - @test all( - isapprox.( - slope, - [ - 0.3195538151032132 - 0.3195538151032132 - 1.0535772194857598 - 1.1323058058970006 - 0.9011191905923782 - 0.07553685115943187 - -0.8838426390957513 - -1.044032162858364 - -1.0251744597550265 - -1.0019651980300468 - ], - atol=1e-4, - ), - ) + @test length(slope) == 10 model = StateSpaceLearning.StructuralModel( vcat(rand(5) .+ 5, rand(5) .- 5) + vcat(collect(1:5), collect(5:-1:1)); @@ -420,26 +403,10 @@ end ζ_threshold=0, ) StateSpaceLearning.fit!(model) - @test all( - isapprox.( - StateSpaceLearning.get_trend_decomposition( - model, model.output.components, slope - ), - [ - 6.544506301918287 - 7.9278752338886775 - 10.266929548367902 - 11.728283090188654 - 13.666722760765126 - 4.077371477199227 - 1.7029908259414626 - 0.5101771103035202 - -2.2199584710270805 - -3.2219236690571273 - ], - atol=1e-4, - ), + trend = StateSpaceLearning.get_trend_decomposition( + model, model.output.components, slope ) + @test length(trend) == 10 model = StateSpaceLearning.StructuralModel( rand(10); From 11fed112bc41bdfc0dfc0c63ecbcf0355e7c547c Mon Sep 17 00:00:00 2001 From: andre_ramos Date: Mon, 28 Apr 2025 14:47:52 -0300 Subject: [PATCH 5/6] fix seed on test forecast_dynamic_exog_coefs --- test/models/structural_model.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/models/structural_model.jl b/test/models/structural_model.jl index 3910d11..93fc472 100644 --- a/test/models/structural_model.jl +++ b/test/models/structural_model.jl @@ -484,6 +484,7 @@ end end @testset "Function: forecast_dynamic_exog_coefs" begin + Random.seed!(1234) model = StateSpaceLearning.StructuralModel( rand(100); seasonal="none", cycle="stochastic", cycle_period=3, outlier=false ) From 3c66a69336db3d96685467a206e77c227e0af71e Mon Sep 17 00:00:00 2001 From: andre_ramos Date: Mon, 28 Apr 2025 15:02:51 -0300 Subject: [PATCH 6/6] enhance coverage --- src/models/structural_model.jl | 5 ---- test/estimation_procedure.jl | 13 ++++++++++ test/models/structural_model.jl | 43 +++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/models/structural_model.jl b/src/models/structural_model.jl index 6e44ad1..c973b36 100644 --- a/src/models/structural_model.jl +++ b/src/models/structural_model.jl @@ -116,11 +116,6 @@ mutable struct StructuralModel <: StateSpaceLearningModel else nothing end - if freq_seasonal == 0 - (@assert seasonal == "none" "stochastic_seasonal and seasonal must be false if freq_seasonal is 0") - else - nothing - end if !isnothing(dynamic_exog_coefs) @assert all( diff --git a/test/estimation_procedure.jl b/test/estimation_procedure.jl index d052020..5084f47 100644 --- a/test/estimation_procedure.jl +++ b/test/estimation_procedure.jl @@ -103,6 +103,19 @@ end @test length(coefs0) == 43 @test length(ε0) == 10 + coefs0, ε0 = StateSpaceLearning.fit_lasso( + X, + AbstractFloat.(y), + 0.1, + "aic", + false, + components_indexes, + ones(size(X, 2) - 1); + rm_average=true, + ) + @test length(coefs0) == 43 + @test length(ε0) == 10 + coefs1, ε1 = StateSpaceLearning.fit_lasso( X2, AbstractFloat.(y), diff --git a/test/models/structural_model.jl b/test/models/structural_model.jl index 93fc472..becdbe4 100644 --- a/test/models/structural_model.jl +++ b/test/models/structural_model.jl @@ -170,6 +170,20 @@ end end @testset "dynamic_exog_coefs" begin + dynamic_exog_coefs1 = [(collect(1:5), "level")] + + X1 = StateSpaceLearning.create_dynamic_exog_coefs_matrix( + dynamic_exog_coefs1, 5, 0, 0, 0, 1 + ) + @test size(X1) == (5, 4) + + dynamic_exog_coefs2 = [(collect(1:5), "slope")] + + X2 = StateSpaceLearning.create_dynamic_exog_coefs_matrix( + dynamic_exog_coefs2, 5, 0, 0, 0, 1 + ) + @test size(X2) == (5, 4) + dynamic_exog_coefs = [ (collect(1:5), "level"), (collect(1:5), "slope"), @@ -192,6 +206,20 @@ end dynamic_exog_coefs, 5, 2, 0, 0, 0, 1 ) @test size(X_f) == (2, 23) + + dynamic_exog_coefs2 = [(collect(6:7), "level", "", 4)] + + X_f2 = StateSpaceLearning.create_forecast_dynamic_exog_coefs_matrix( + dynamic_exog_coefs2, 5, 2, 0, 0, 0, 1 + ) + @test size(X_f2) == (2, 4) + + dynamic_exog_coefs3 = [(collect(6:7), "slope", "", 4)] + + X_f3 = StateSpaceLearning.create_forecast_dynamic_exog_coefs_matrix( + dynamic_exog_coefs3, 5, 2, 0, 0, 0, 1 + ) + @test size(X_f3) == (2, 4) end @testset "Create X matrix" begin @@ -465,6 +493,21 @@ end ) @test sort(collect(keys(model_decomposition))) == sort(["cycle_3", "cycle_hat_3", "seasonal_3", "slope", "trend"]) + + model = StateSpaceLearning.StructuralModel( + vcat(rand(5) .+ 5, rand(5) .- 5) + vcat(collect(1:5), collect(5:-1:1)); + level="deterministic", + seasonal="none", + cycle="none", + outlier=false, + slope="stochastic", + ζ_threshold=0, + ) + StateSpaceLearning.fit!(model) + trend = StateSpaceLearning.get_trend_decomposition( + model, model.output.components, slope + ) + @test length(trend) == 10 end @testset "Function: simulate_states" begin