Skip to content

Commit e3db644

Browse files
Merge pull request #59 from LAMPSPUC/major_update
Add new features (dynamic exog coefs), new forecasting procedure, structural model input changes
2 parents 2c2bad0 + 3c66a69 commit e3db644

20 files changed

+2018
-2003
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "StateSpaceLearning"
22
uuid = "971c4b7c-2c4e-4bac-8525-e842df3cde7b"
33
authors = ["andreramosfc <[email protected]>"]
4-
version = "1.4.3"
4+
version = "2.0.0"
55

66
[deps]
77
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"

README.md

Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,35 +20,41 @@ model = StructuralModel(y)
2020
fit!(model)
2121

2222
# Point Forecast
23-
prediction = StateSpaceLearning.forecast(model, 12) #Gets a 12 steps ahead prediction
23+
prediction = forecast(model, 12) # Gets a 12 steps ahead prediction
2424

2525
# Scenarios Path Simulation
26-
simulation = StateSpaceLearning.simulate(model, 12, 1000) #Gets 1000 scenarios path of 12 steps ahead predictions
26+
simulation = simulate(model, 12, 1000) # Gets 1000 scenarios path of 12 steps ahead predictions
2727
```
2828

2929
## StructuralModel Arguments
3030

31-
* `y::Vector`: Vector of data.
32-
* `level::Bool`: Boolean where to consider intercept in the model (default: true)
33-
* `stochastic_level::Bool`: Boolean where to consider stochastic level component in the model (default: true)
34-
* `trend::Bool`: Boolean where to consider trend component in the model (default: true)
35-
* `stochastic_trend::Bool`: Boolean where to consider stochastic trend component in the model (default: true)
36-
* `seasonal::Bool`: Boolean where to consider seasonal component in the model (default: true)
37-
* `stochastic_seasonal::Bool`: Boolean where to consider stochastic seasonal component in the model (default: true)
38-
* `freq_seasonal::Int`: Seasonal frequency to be considered in the model (default: 12)
39-
* `outlier::Bool`: Boolean where to consider outlier component in the model (default: true)
40-
* `ζ_ω_threshold::Int`: Argument to stabilize `stochastic trend` and `stochastic seasonal` components (default: 12)
31+
* `y::Union{Vector,Matrix}`: Time series data
32+
* `level::String`: Level component type: "stochastic", "deterministic", or "none" (default: "stochastic")
33+
* `slope::String`: Slope component type: "stochastic", "deterministic", or "none" (default: "stochastic")
34+
* `seasonal::String`: Seasonal component type: "stochastic", "deterministic", or "none" (default: "stochastic")
35+
* `cycle::String`: Cycle component type: "stochastic", "deterministic", or "none" (default: "none")
36+
* `freq_seasonal::Union{Int,Vector{Int}}`: Seasonal frequency or vector of frequencies (default: 12)
37+
* `cycle_period::Union{Union{Int,<:AbstractFloat},Vector{Int},Vector{<:AbstractFloat}}`: Cycle period or vector of periods (default: 0)
38+
* `outlier::Bool`: Include outlier component (default: true)
39+
* `ζ_threshold::Int`: Threshold for slope innovations (default: 12)
40+
* `ω_threshold::Int`: Threshold for seasonal innovations (default: 12)
41+
* `ϕ_threshold::Int`: Threshold for cycle innovations (default: 12)
42+
* `stochastic_start::Int`: Starting point for stochastic components (default: 1)
43+
* `exog::Matrix`: Matrix of exogenous variables (default: zeros(length(y), 0))
44+
* `dynamic_exog_coefs::Union{Vector{<:Tuple}, Nothing}`: Dynamic exogenous coefficients (default: nothing)
4145

4246
## Features
4347

4448
Current features include:
45-
* Estimation
46-
* Components decomposition
47-
* Forecasting
48-
* Completion of missing values
49-
* Predefined models, including:
50-
* Outlier detection
51-
* Outlier robust models
49+
* Model estimation using elastic net based regularization
50+
* Automatic component decomposition (trend, seasonal, cycle)
51+
* Point forecasting and scenario simulation
52+
* Missing value imputation
53+
* Outlier detection and robust modeling
54+
* Multiple seasonal frequencies support
55+
* Deterministic and stochastic component options
56+
* Dynamic exogenous variable handling
57+
* Best subset selection for exogenous variables
5258

5359
## Quick Examples
5460

@@ -67,7 +73,7 @@ steps_ahead = 30
6773

6874
model = StructuralModel(log_air_passengers)
6975
fit!(model)
70-
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
76+
prediction_log = forecast(model, steps_ahead) # arguments are the output of the fitted model and number of steps ahead the user wants to forecast
7177
prediction = exp.(prediction_log)
7278

7379
plot_point_forecast(airp.passengers, prediction)
@@ -76,7 +82,7 @@ plot_point_forecast(airp.passengers, prediction)
7682

7783
```julia
7884
N_scenarios = 1000
79-
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
85+
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
8086

8187
plot_scenarios(airp.passengers, exp.(simulation))
8288

@@ -97,22 +103,20 @@ log_air_passengers = log.(airp.passengers)
97103
model = StructuralModel(log_air_passengers)
98104
fit!(model)
99105

100-
level = model.output.components["μ1"]["Values"] + model.output.components["ξ"]["Values"]
101-
slope = model.output.components["ν1"]["Values"] + model.output.components["ζ"]["Values"]
102-
seasonal = model.output.components["γ1_12"]["Values"] + model.output.components["ω_12"]["Values"]
103-
trend = level + slope
104-
105-
plot(trend, w=2 , color = "Black", lab = "Trend Component", legend = :outerbottom)
106-
plot(seasonal, w=2 , color = "Black", lab = "Seasonal Component", legend = :outerbottom)
106+
# Access decomposed components directly
107+
trend = model.output.decomposition["trend"]
108+
seasonal = model.output.decomposition["seasonal_12"]
107109

110+
plot(trend, w=2, color = "Black", lab = "Trend Component", legend = :outerbottom)
111+
plot(seasonal, w=2, color = "Black", lab = "Seasonal Component", legend = :outerbottom)
108112
```
109113

110114
| ![quick_example_trend](./docs/src/assets/trend.svg) | ![quick_example_seas](./docs/src/assets/seasonal.svg)|
111115
|:------------------------------:|:-----------------------------:|
112116

113117

114-
### Best Subset Selection
115-
Quick example on how to perform best subset selection in time series utilizing StateSpaceLearning.
118+
### Best Subset Selection and Dynamic Coefficients
119+
Example of performing best subset selection and using dynamic coefficients:
116120

117121
```julia
118122
using StateSpaceLearning
@@ -122,22 +126,33 @@ using Random
122126

123127
Random.seed!(2024)
124128

129+
# Load data
125130
airp = CSV.File(StateSpaceLearning.AIR_PASSENGERS) |> DataFrame
126131
log_air_passengers = log.(airp.passengers)
127-
X = rand(length(log_air_passengers), 10) # Create 10 exogenous features
128-
β = rand(3)
129-
130-
y = log_air_passengers + X[:, 1:3]*β # add to the log_air_passengers series a contribution from only 3 exogenous features.
131-
132-
model = StructuralModel(y; Exogenous_X = X)
133-
fit!(model; α = 1.0, information_criteria = "bic", ϵ = 0.05, penalize_exogenous = true, penalize_initial_states = true)
134-
135-
Selected_exogenous = model.output.components["Exogenous_X"]["Selected"]
136132

133+
# Create exogenous features
134+
X = rand(length(log_air_passengers), 10)
135+
β = rand(3)
136+
y = log_air_passengers + X[:, 1:3]*β
137+
138+
# Create model with exogenous variables
139+
model = StructuralModel(y;
140+
exog = X
141+
)
142+
143+
# Fit model with elastic net regularization
144+
fit!(model;
145+
α = 1.0, # 1.0 for Lasso, 0.0 for Ridge
146+
information_criteria = "bic",
147+
ϵ = 0.05,
148+
penalize_exogenous = true,
149+
penalize_initial_states = true
150+
)
151+
152+
# Get selected features
153+
selected_exog = model.output.components["exog"]["Selected"]
137154
```
138155

139-
In this example, the selected exogenous features were 1, 2, 3, as expected.
140-
141156
### Missing values imputation
142157
Quick example of completion of missing values for the air passengers time-series (artificial NaN values are added to the original time-series).
143158

@@ -209,7 +224,7 @@ fit!(model)
209224
residuals_variances = model.output.residuals_variances
210225

211226
ss_model = BasicStructural(log_air_passengers, 12)
212-
set_initial_hyperparameters!(ss_model, Dict("sigma2_ε" => residuals_variances["ε"],
227+
StateSpaceModels.set_initial_hyperparameters!(ss_model, Dict("sigma2_ε" => residuals_variances["ε"],
213228
"sigma2_ξ" =>residuals_variances["ξ"],
214229
"sigma2_ζ" =>residuals_variances["ζ"],
215230
"sigma2_ω" =>residuals_variances["ω_12"]))

docs/src/assets/dynamic_exog.png

106 KB
Loading

docs/src/examples.md

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ y = solars[!, "y1"]
4646
T = length(y)
4747
steps_ahead = 48
4848

49-
model = StructuralModel(y; freq_seasonal=24, trend=false, level=false)
49+
model = StructuralModel(y; freq_seasonal=24, slope="none", level="none", outlier=false)
5050
fit!(model; penalize_initial_states=false)
5151
simulation = StateSpaceLearning.simulate(model, steps_ahead, 100) #Gets a 12 steps ahead prediction
5252
plot_scenarios(y, simulation)
@@ -63,7 +63,7 @@ y = solars[!, "y1"]
6363
T = length(y)
6464
steps_ahead = 48
6565

66-
model = StructuralModel(y; freq_seasonal=24, trend=false, level=false)
66+
model = StructuralModel(y; freq_seasonal=24, slope="none", level="none", outlier = false)
6767
fit!(model; penalize_initial_states=false)
6868
simulation = StateSpaceLearning.simulate(model, steps_ahead, 100; seasonal_innovation_simulation=24) #Gets a 12 steps ahead prediction
6969
plot_scenarios(y, simulation)
@@ -114,4 +114,35 @@ plot_point_forecast(y, prediction)
114114
```
115115
![two_seas](assets/two_seas.png)
116116

117-
Note that the model was able to capture both seasonalities in this case.
117+
Note that the model was able to capture both seasonalities in this case.
118+
119+
## Dynamic Exogenous Coefficients
120+
121+
Dynamic exogenous coefficients allow the effect of exogenous variables to vary over time with specific patterns (e.g., level, slope, seasonal or cyclical). This is configured through the `dynamic_exog_coefs` parameter in the StructuralModel constructor.
122+
123+
The `dynamic_exog_coefs` parameter accepts a vector of tuples, where each tuple contains:
124+
- First element: A vector of an exogenous variable
125+
- Second element: The name of the component that the exogenous variable will be associated with
126+
- Third element (optional): For the seasonal component, the freq_seasonal parameter and for cycle component, the cycle_period parameter.
127+
128+
For example:
129+
```julia
130+
# Make X1's effect vary annually and X2's effect vary semi-annually
131+
airp = CSV.File(StateSpaceLearning.AIR_PASSENGERS) |> DataFrame
132+
y = log.(airp.passengers)
133+
X = vcat(collect(1:90), collect(90.5:-0.5:64)) + (rand(144) .* 10)
134+
y += X * -0.03
135+
136+
dynamic_coefs = [(X, "level")]
137+
138+
model = StructuralModel(y;
139+
dynamic_exog_coefs=dynamic_coefs
140+
)
141+
142+
fit!(model)
143+
144+
prediction = forecast(model, 30; dynamic_exog_coefs_forecasts = [collect(63.5:-0.5:49)])
145+
146+
plot_point_forecast(y, prediction)
147+
```
148+
![dynamic_exog](assets/dynamic_exog.png)

docs/src/features.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,10 @@ X = rand(length(log_air_passengers), 10) # Create 10 exogenous features
104104

105105
y = log_air_passengers + X[:, 1:3]*β # add to the log_air_passengers series a contribution from only 3 exogenous features.
106106

107-
model = StructuralModel(y; Exogenous_X = X)
107+
model = StructuralModel(y; exog = X)
108108
fit!(model; α = 1.0, information_criteria = "bic", ϵ = 0.05, penalize_exogenous = true, penalize_initial_states = true)
109109

110-
Selected_exogenous = model.output.components["Exogenous_X"]["Selected"]
110+
Selected_exogenous = model.output.components["exog"]["Selected"]
111111

112112
```
113113

docs/src/manual.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,14 @@ simulation = StateSpaceLearning.simulate(model, 12, 1000) #Gets 1000 scenarios p
2323
```
2424
## Models
2525

26-
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.
26+
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.
27+
28+
```
29+
30+
When using dynamic coefficients:
31+
- The model will create time-varying coefficients for each specified exogenous variable
32+
- Each coefficient will follow the specified cyclical pattern
33+
- When forecasting, you must provide future values for exogenous variables using the `Exogenous_Forecast` parameter
2734
2835
```@docs
2936
StateSpaceLearning.StructuralModel
@@ -39,7 +46,11 @@ StateSpaceLearning.fit!
3946

4047
## Forecasting and Simulating
4148

42-
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.
49+
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.
50+
51+
When using models with exogenous variables:
52+
- For standard exogenous variables, provide future values using the `Exogenous_Forecast` parameter
53+
- For dynamic coefficients, use the same `Exogenous_Forecast` parameter with values for each exogenous variable
4354

4455
```@docs
4556
StateSpaceLearning.forecast

paper_tests/m4_test/evaluate_model.jl

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,13 @@ function evaluate_SSL(
1919

2020
model = StateSpaceLearning.StructuralModel(
2121
normalized_y;
22-
level=true,
23-
stochastic_level=true,
24-
trend=true,
25-
stochastic_trend=true,
26-
seasonal=true,
27-
stochastic_seasonal=true,
22+
level="stochastic",
23+
slope="stochastic",
24+
seasonal="stochastic",
2825
freq_seasonal=12,
2926
outlier=outlier,
30-
ζ_ω_threshold=12,
27+
ζ_threshold=12,
28+
ω_threshold=12,
3129
)
3230
StateSpaceLearning.fit!(
3331
model;

paper_tests/m4_test/m4_test.jl

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ function run_config(
5050
save_init ? CSV.write(init_filepath, initialization_df) : nothing # Initialize empty CSV
5151

5252
for i in 1:48000
53-
if i in [10001, 20001, 30001, 40001] # Clear DataFrame to save memory
53+
if i % 1000 == 1 # Clear DataFrame to save memory
5454
results_df = DataFrame()
5555
initialization_df = DataFrame()
5656
end
@@ -66,7 +66,8 @@ function run_config(
6666
information_criteria,
6767
)
6868

69-
if i in [10000, 20000, 30000, 40000, 48000]
69+
if i % 1000 == 0
70+
@info "Saving results for $i series"
7071
!save_init ? append_results(filepath, results_df) : nothing
7172
save_init ? append_results(init_filepath, initialization_df) : nothing
7273
end
@@ -100,9 +101,9 @@ end
100101
# Main script
101102
function main()
102103
results_table = DataFrame()
103-
for outlier in [true]
104-
for information_criteria in ["aic"]
105-
for α in [0.1]
104+
for outlier in [true, false]
105+
for information_criteria in ["aic", "bic"]
106+
for α in [0.0, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0]
106107
@info "Running SSL with outlier = $outlier, information_criteria = $information_criteria, α = "
107108
results_table = run_config(
108109
results_table, outlier, information_criteria, α, false, 60

paper_tests/m4_test/prepare_data.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ function normalize(y::Vector, max_y::AbstractFloat, min_y::AbstractFloat)
22
return (y .- min_y) ./ (max_y - min_y)
33
end
44

5-
function de_normalize(y::Vector, max_y::AbstractFloat, min_y::AbstractFloat)
5+
function de_normalize(y, max_y::AbstractFloat, min_y::AbstractFloat)
66
return (y .* (max_y - min_y)) .+ min_y
77
end
88

paper_tests/simulation_test/evaluate_models.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function get_SSL_results(
2828
freq_seasonal=12,
2929
outlier=false,
3030
ζ_ω_threshold=12,
31-
Exogenous_X=X_train,
31+
exog=X_train,
3232
)
3333
t = @elapsed StateSpaceLearning.fit!(
3434
model;
@@ -39,13 +39,13 @@ function get_SSL_results(
3939
penalize_initial_states=true,
4040
)
4141

42-
selected = model.output.components["Exogenous_X"]["Selected"]
42+
selected = model.output.components["exog"]["Selected"]
4343
true_positives, false_positives, false_negatives, true_negatives = get_confusion_matrix(
4444
selected, true_features, false_features
4545
)
4646

47-
mse = mse_func(model.output.components["Exogenous_X"]["Coefs"], true_β)
48-
bias = bias_func(model.output.components["Exogenous_X"]["Coefs"], true_β)
47+
mse = mse_func(model.output.components["exog"]["Coefs"], true_β)
48+
bias = bias_func(model.output.components["exog"]["Coefs"], true_β)
4949

5050
series_result = DataFrame(
5151
[

0 commit comments

Comments
 (0)