Skip to content

Commit 8b0f65d

Browse files
author
andre_ramos
committed
allow for the removal of level componentt
1 parent 1223c5c commit 8b0f65d

File tree

8 files changed

+172
-80
lines changed

8 files changed

+172
-80
lines changed

src/StateSpaceLearning.jl

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,21 @@ include("estimation_procedure/default_estimation_procedure.jl")
99
include("utils.jl")
1010
include("datasets.jl")
1111

12-
const DEFAULT_COMPONENTS_PARAMETERS = ["stochastic_level", "trend", "stochastic_trend", "seasonal", "stochastic_seasonal", "freq_seasonal"]
12+
const DEFAULT_COMPONENTS_PARAMETERS = ["level", "stochastic_level", "trend", "stochastic_trend", "seasonal", "stochastic_seasonal", "freq_seasonal"]
1313

1414
export fit_model, forecast
1515

1616
"""
1717
fit_model(y::Vector{Fl};
18-
model_input::Dict = Dict("stochastic_level" => true, "trend" => true, "stochastic_trend" => true,
19-
"seasonal" => true, "stochastic_seasonal" => true, "freq_seasonal" => 12,
20-
"outlier" => true, "ζ_ω_threshold" => 12),
21-
model_functions::Dict = Dict("create_X" => create_X, "get_components_indexes" => get_components_indexes,
22-
"get_variances" => get_variances),
23-
estimation_input::Dict = Dict("α" => 0.1, "information_criteria" => "aic", "ϵ" => 0.05,
24-
"penalize_exogenous" => true, "penalize_initial_states" => true),
25-
estimation_function::Function = default_estimation_procedure,
26-
Exogenous_X::Matrix{Fl} = zeros(length(y), 0))::Output where Fl
18+
model_input::Dict = Dict("level" => true, "stochastic_level" => true, "trend" => true, "stochastic_trend" => true,
19+
"seasonal" => true, "stochastic_seasonal" => true, "freq_seasonal" => 12,
20+
"outlier" => true, "ζ_ω_threshold" => 12),
21+
model_functions::Dict = Dict("create_X" => create_X, "get_components_indexes" => get_components_indexes,
22+
"get_variances" => get_variances),
23+
estimation_input::Dict = Dict("α" => 0.1, "information_criteria" => "aic", "ϵ" => 0.05,
24+
"penalize_exogenous" => true, "penalize_initial_states" => true),
25+
estimation_function::Function = default_estimation_procedure,
26+
Exogenous_X::Matrix{Fl} = zeros(length(y), 0))::Output where Fl
2727
2828
Fits the StateSpaceLearning model using specified parameters and estimation procedures.
2929
@@ -40,7 +40,7 @@ fit_model(y::Vector{Fl};
4040
4141
"""
4242
function fit_model(y::Vector{Fl};
43-
model_input::Dict = Dict("stochastic_level" => true, "trend" => true, "stochastic_trend" => true,
43+
model_input::Dict = Dict("level" => true, "stochastic_level" => true, "trend" => true, "stochastic_trend" => true,
4444
"seasonal" => true, "stochastic_seasonal" => true, "freq_seasonal" => 12,
4545
"outlier" => true, "ζ_ω_threshold" => 12),
4646
model_functions::Dict = Dict("create_X" => create_X, "get_components_indexes" => get_components_indexes,
@@ -57,8 +57,15 @@ function fit_model(y::Vector{Fl};
5757
@assert all([key in keys(model_input) for key in DEFAULT_COMPONENTS_PARAMETERS]) "The default components model must have all the necessary parameters $(DEFAULT_COMPONENTS_PARAMETERS)"
5858
end
5959

60+
@assert !has_intercept(Exogenous_X) "Exogenous matrix must not have an intercept column"
61+
6062
X = model_functions["create_X"](model_input, Exogenous_X)
6163

64+
if has_intercept(X)
65+
@assert allequal(X[:, 1]) "Intercept column must be the first column"
66+
@assert !has_intercept(X[:, 2:end]) "Matrix must not have more than one intercept column"
67+
end
68+
6269
estimation_y, Estimation_X, valid_indexes = handle_missing_values(X, y)
6370

6471
components_indexes = model_functions["get_components_indexes"](Exogenous_X, model_input)

src/estimation_procedure/default_estimation_procedure.jl

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -52,71 +52,71 @@ function get_outlier_duplicate_columns(Estimation_X::Matrix{Tl}, components_inde
5252
end
5353

5454
"""
55-
get_path_information_criteria(model::GLMNetPath, Estimation_X::Matrix{Tl}, estimation_y::Vector{Fl},
55+
get_path_information_criteria(model::GLMNetPath, Lasso_X::Matrix{Tl}, Lasso_y::Vector{Fl},
5656
information_criteria::String; intercept::Bool = true)::Tuple{Vector{Float64}, Vector{Float64}} where {Tl, Fl}
5757
5858
Calculates the information criteria along the regularization path of a GLMNet model and returns coefficients and residuals of the best model based on the selected information criteria.
5959
6060
# Arguments
6161
- `model::GLMNetPath`: Fitted GLMNetPath model object.
62-
- `Estimation_X::Matrix{Tl}`: Matrix of predictors for estimation.
63-
- `estimation_y::Vector{Fl}`: Vector of response values for estimation.
62+
- `Lasso_X::Matrix{Tl}`: Matrix of predictors for estimation.
63+
- `Lasso_y::Vector{Fl}`: Vector of response values for estimation.
6464
- `information_criteria::String`: Information Criteria method for hyperparameter selection.
6565
- `intercept::Bool`: Flag for intercept inclusion in the model (default: true).
6666
6767
# Returns
6868
- `Tuple{Vector{Float64}, Vector{Float64}}`: Tuple containing coefficients and residuals of the best model.
6969
7070
"""
71-
function get_path_information_criteria(model::GLMNetPath, Estimation_X::Matrix{Tl}, estimation_y::Vector{Fl}, information_criteria::String; intercept::Bool = true)::Tuple{Vector{Float64}, Vector{Float64}} where {Tl, Fl}
71+
function get_path_information_criteria(model::GLMNetPath, Lasso_X::Matrix{Tl}, Lasso_y::Vector{Fl}, information_criteria::String; intercept::Bool = true)::Tuple{Vector{Float64}, Vector{Float64}} where {Tl, Fl}
7272
path_size = length(model.lambda)
73-
T = size(Estimation_X, 1)
73+
T = size(Lasso_X, 1)
7474
K = count(i->i != 0, model.betas; dims = 1)'
7575

7676
method_vec = Vector{Float64}(undef, path_size)
7777
for i in 1:path_size
78-
fit = Estimation_X*model.betas[:, i] .+ model.a0[i]
79-
ϵ = estimation_y - fit
78+
fit = Lasso_X*model.betas[:, i] .+ model.a0[i]
79+
ϵ = Lasso_y - fit
8080

8181
method_vec[i] = get_information(T, K[i], ϵ; information_criteria = information_criteria)
8282
end
8383

8484
best_model_idx = argmin(method_vec)
8585
coefs = intercept ? vcat(model.a0[best_model_idx], model.betas[:, best_model_idx]) : model.betas[:, best_model_idx]
86-
fit = intercept ? hcat(ones(T), Estimation_X)*coefs : Estimation_X*coefs
87-
ϵ = estimation_y - fit
86+
fit = intercept ? hcat(ones(T), Lasso_X)*coefs : Lasso_X*coefs
87+
ϵ = Lasso_y - fit
8888
return coefs, ϵ
8989
end
9090

9191
"""
92-
fit_glmnet(Estimation_X::Matrix{Tl}, estimation_y::Vector{Fl}, α::Float64;
92+
fit_glmnet(Lasso_X::Matrix{Tl}, Lasso_y::Vector{Fl}, α::Float64;
9393
information_criteria::String = "aic",
94-
penalty_factor::Vector{Float64}=ones(size(Estimation_X,2) - 1),
94+
penalty_factor::Vector{Float64}=ones(size(Lasso_X,2) - 1),
9595
intercept::Bool = intercept)::Tuple{Vector{Float64}, Vector{Float64}} where {Tl, Fl}
9696
9797
Fits a GLMNet model to the provided data and returns coefficients and residuals based on selected criteria.
9898
9999
# Arguments
100-
- `Estimation_X::Matrix{Tl}`: Matrix of predictors for estimation.
101-
- `estimation_y::Vector{Fl}`: Vector of response values for estimation.
100+
- `Lasso_X::Matrix{Tl}`: Matrix of predictors for estimation.
101+
- `Lasso_y::Vector{Fl}`: Vector of response values for estimation.
102102
- `α::Float64`: Elastic net control factor between ridge (α=0) and lasso (α=1) (default: 0.1).
103103
- `information_criteria::String`: Information Criteria method for hyperparameter selection (default: aic).
104-
- `penalty_factor::Vector{Float64}`: Penalty factors for each predictor (default: ones(size(Estimation_X, 2) - 1)).
104+
- `penalty_factor::Vector{Float64}`: Penalty factors for each predictor (default: ones(size(Lasso_X, 2) - 1)).
105105
- `intercept::Bool`: Flag for intercept inclusion in the model (default: true).
106106
107107
# Returns
108108
- `Tuple{Vector{Float64}, Vector{Float64}}`: Tuple containing coefficients and residuals of the best model.
109109
110110
"""
111-
function fit_glmnet(Estimation_X::Matrix{Tl}, estimation_y::Vector{Fl}, α::Float64; information_criteria::String = "aic", penalty_factor::Vector{Float64}=ones(size(Estimation_X,2) - 1), intercept::Bool = intercept)::Tuple{Vector{Float64}, Vector{Float64}} where {Tl, Fl}
112-
model = glmnet(Estimation_X, estimation_y, alpha = α, penalty_factor = penalty_factor, intercept = intercept, dfmax=size(Estimation_X, 2), lambda_min_ratio=0.001)
113-
return get_path_information_criteria(model, Estimation_X, estimation_y, information_criteria; intercept = intercept)
111+
function fit_glmnet(Lasso_X::Matrix{Tl}, Lasso_y::Vector{Fl}, α::Float64; information_criteria::String = "aic", penalty_factor::Vector{Float64}=ones(size(Lasso_X,2) - 1), intercept::Bool = intercept)::Tuple{Vector{Float64}, Vector{Float64}} where {Tl, Fl}
112+
model = glmnet(Lasso_X, Lasso_y, alpha = α, penalty_factor = penalty_factor, intercept = intercept, dfmax=size(Lasso_X, 2), lambda_min_ratio=0.001)
113+
return get_path_information_criteria(model, Lasso_X, Lasso_y, information_criteria; intercept = intercept)
114114
end
115115

116116
"""
117117
fit_lasso(Estimation_X::Matrix{Tl}, estimation_y::Vector{Fl}, α::Float64, information_criteria::String,
118-
penalize_exogenous::Bool, components_indexes::Dict{String, Vector{Int64}};
119-
penalty_factor::Vector{Float64}=ones(size(Estimation_X,2) - 1), intercept::Bool = true)::Tuple{Vector{Float64}, Vector{Float64}} where {Tl, Fl}
118+
penalize_exogenous::Bool, components_indexes::Dict{String, Vector{Int64}}, penalty_factor::Vector{Float64};
119+
rm_average::Bool = false)::Tuple{Vector{Float64}, Vector{Float64}} where {Tl, Fl}
120120
121121
Fits a Lasso regression model to the provided data and returns coefficients and residuals based on selected criteria.
122122
@@ -127,23 +127,41 @@ end
127127
- `information_criteria::String`: Information Criteria method for hyperparameter selection (default: aic).
128128
- `penalize_exogenous::Bool`: Flag for selecting exogenous variables. When false the penalty factor for these variables will be set to 0.
129129
- `components_indexes::Dict{String, Vector{Int64}}`: Dictionary containing indexes for different components.
130-
- `penalty_factor::Vector{Float64}`: Penalty factors for each predictor (default: ones(size(Estimation_X, 2) - 1)).
131-
- `intercept::Bool`: Flag for intercept inclusion in the model (default: true).
130+
- `penalty_factor::Vector{Float64}`: Penalty factors for each predictor.
131+
- `rm_average::Bool`: Flag to consider if the intercept will be calculated is the average of the time series (default: false).
132132
133133
# Returns
134134
- `Tuple{Vector{Float64}, Vector{Float64}}`: Tuple containing coefficients and residuals of the fitted Lasso model.
135135
136136
"""
137-
function fit_lasso(Estimation_X::Matrix{Tl}, estimation_y::Vector{Fl}, α::Float64, information_criteria::String, penalize_exogenous::Bool, components_indexes::Dict{String, Vector{Int64}}; penalty_factor::Vector{Float64}=ones(size(Estimation_X,2) - 1), intercept::Bool = true)::Tuple{Vector{Float64}, Vector{Float64}} where {Tl, Fl}
137+
function fit_lasso(Estimation_X::Matrix{Tl}, estimation_y::Vector{Fl}, α::Float64, information_criteria::String, penalize_exogenous::Bool, components_indexes::Dict{String, Vector{Int64}}, penalty_factor::Vector{Float64}; rm_average::Bool = false)::Tuple{Vector{Float64}, Vector{Float64}} where {Tl, Fl}
138138

139139
outlier_duplicate_columns = get_outlier_duplicate_columns(Estimation_X, components_indexes)
140140
penalty_factor[outlier_duplicate_columns] .= Inf
141141

142-
!penalize_exogenous ? penalty_factor[components_indexes["Exogenous_X"] .- 1] .= 0 : nothing
143-
mean_y = mean(estimation_y); Lasso_y = intercept ? estimation_y : estimation_y .- mean_y
142+
hasintercept = has_intercept(Estimation_X)
143+
if hasintercept
144+
!penalize_exogenous ? penalty_factor[components_indexes["Exogenous_X"] .- 1] .= 0 : nothing
145+
Lasso_X = Estimation_X[:, 2:end]
146+
else
147+
!penalize_exogenous ? penalty_factor[components_indexes["Exogenous_X"]] .= 0 : nothing
148+
Lasso_X = Estimation_X
149+
@assert !rm_average "Intercept must be included in the model if rm_average is set to true"
150+
end
151+
152+
if rm_average
153+
mean_y = mean(estimation_y)
154+
Lasso_y = estimation_y .- mean_y
155+
else
156+
Lasso_y = estimation_y
157+
end
144158

145-
coefs, ϵ = fit_glmnet(Estimation_X[:, 2:end], Lasso_y, α; information_criteria=information_criteria, penalty_factor=penalty_factor, intercept = intercept)
146-
return !intercept ? (vcat(mean_y, coefs), ϵ) : (coefs, ϵ)
159+
if hasintercept
160+
coefs, ϵ = fit_glmnet(Lasso_X, Lasso_y, α; information_criteria=information_criteria, penalty_factor=penalty_factor, intercept = !rm_average)
161+
else
162+
coefs, ϵ = fit_glmnet(Lasso_X, Lasso_y, α; information_criteria=information_criteria, penalty_factor=penalty_factor, intercept = false)
163+
end
164+
return rm_average ? (vcat(mean_y, coefs), ϵ) : (coefs, ϵ)
147165

148166
end
149167

@@ -175,22 +193,37 @@ function default_estimation_procedure(Estimation_X::Matrix{Tl}, estimation_y::Ve
175193

176194
@assert 0 <= α <= 1 "α must be in [0, 1]"
177195

178-
penalty_factor = ones(size(Estimation_X, 2) - 1); penalty_factor[components_indexes["initial_states"][2:end] .- 1] .= 0
179-
coefs, _ = fit_lasso(Estimation_X, estimation_y, α, information_criteria, penalize_exogenous, components_indexes; penalty_factor = penalty_factor, intercept = false)
196+
hasintercept = has_intercept(Estimation_X)
197+
198+
if hasintercept
199+
penalty_factor = ones(size(Estimation_X, 2) - 1)
200+
penalty_factor[components_indexes["initial_states"][2:end] .- 1] .= 0
201+
coefs, _ = fit_lasso(Estimation_X, estimation_y, α, information_criteria, penalize_exogenous, components_indexes, penalty_factor; rm_average = true)
202+
else
203+
penalty_factor = ones(size(Estimation_X, 2))
204+
penalty_factor[components_indexes["initial_states"][2:end]] .= 0
205+
coefs, _ = fit_lasso(Estimation_X, estimation_y, α, information_criteria, penalize_exogenous, components_indexes, penalty_factor; rm_average = false)
206+
end
180207

181208
#AdaLasso per component
182-
penalty_factor = zeros(size(Estimation_X, 2) - 1)
209+
ts_penalty_factor = hasintercept ? zeros(size(Estimation_X, 2) - 1) : zeros(size(Estimation_X, 2))
183210
for key in keys(components_indexes)
184211
if key != "initial_states" && key != "μ1"
185212
component = components_indexes[key]
186213
if key != "Exogenous_X" && key != "o" && !(key in ["ν1", "γ1"])
187214
κ = count(i -> i != 0, coefs[component]) < 1 ? 0 : std(coefs[component])
188-
penalty_factor[component .- 1] .= (1 /+ ϵ))
215+
hasintercept ? ts_penalty_factor[component .- 1] .= (1 /+ ϵ)) : ts_penalty_factor[component] .= (1 /+ ϵ))
189216
else
190-
penalty_factor[component .- 1] = (1 ./ (abs.(coefs[component]) .+ ϵ))
217+
hasintercept ? ts_penalty_factor[component .- 1] = (1 ./ (abs.(coefs[component]) .+ ϵ)) : ts_penalty_factor[component] = (1 ./ (abs.(coefs[component]) .+ ϵ))
191218
end
192219
end
193220
end
194-
!penalize_initial_states ? penalty_factor[components_indexes["initial_states"][2:end] .- 1] .= 0 : nothing
195-
return fit_lasso(Estimation_X, estimation_y, α, information_criteria, penalize_exogenous, components_indexes; penalty_factor=penalty_factor)
221+
222+
if hasintercept
223+
!penalize_initial_states ? ts_penalty_factor[components_indexes["initial_states"][2:end] .- 1] .= 0 : nothing
224+
else
225+
!penalize_initial_states ? ts_penalty_factor[components_indexes["initial_states"][2:end]] .= 0 : nothing
226+
end
227+
228+
return fit_lasso(Estimation_X, estimation_y, α, information_criteria, penalize_exogenous, components_indexes, penalty_factor; rm_average = false)
196229
end

src/models/default_model.jl

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,24 +116,26 @@ function create_ω(T::Int64, freq_seasonal::Int64, steps_ahead::Int64, ζ_ω_thr
116116
end
117117

118118
"""
119-
create_initial_states_Matrix(T::Int64, s::Int64, steps_ahead::Int64, trend::Bool, seasonal::Bool)::Matrix
119+
create_initial_states_Matrix(T::Int64, s::Int64, steps_ahead::Int64, level::Bool, trend::Bool, seasonal::Bool)::Matrix
120120
121121
Creates an initial states matrix based on the input parameters.
122122
123123
# Arguments
124124
- `T::Int64`: Length of the original time series.
125125
- `freq_seasonal::Int64`: Seasonal period.
126126
- `steps_ahead::Int64`: Number of steps ahead.
127+
- `level::Bool`: Flag for considering level component.
127128
- `trend::Bool`: Flag for considering trend component.
128129
- `seasonal::Bool`: Flag for considering seasonal component.
129130
130131
# Returns
131132
- `Matrix`: Initial states matrix constructed based on the input parameters.
132133
133134
"""
134-
function create_initial_states_Matrix(T::Int64, freq_seasonal::Int64, steps_ahead::Int64, trend::Bool, seasonal::Bool)::Matrix
135+
function create_initial_states_Matrix(T::Int64, freq_seasonal::Int64, steps_ahead::Int64, level::Bool, trend::Bool, seasonal::Bool)::Matrix
135136

136-
initial_states_matrix = ones(T+steps_ahead, 1)
137+
initial_states_matrix = zeros(T+steps_ahead, 0)
138+
level ? initial_states_matrix = hcat(initial_states_matrix, ones(T+steps_ahead, 1)) : nothing
137139
trend ? initial_states_matrix = hcat(initial_states_matrix, vcat([0], collect(1:T+steps_ahead-1))) : nothing
138140

139141
if seasonal
@@ -172,7 +174,7 @@ function create_X(model_input::Dict, Exogenous_X::Matrix{Fl},
172174
ω_matrix = model_input["stochastic_seasonal"] ? create_ω(T, model_input["freq_seasonal"], steps_ahead, ζ_ω_threshold) : zeros(T+steps_ahead, 0)
173175
o_matrix = outlier ? create_o_matrix(T, steps_ahead) : zeros(T+steps_ahead, 0)
174176

175-
initial_states_matrix = create_initial_states_Matrix(T, model_input["freq_seasonal"], steps_ahead, model_input["trend"], model_input["seasonal"])
177+
initial_states_matrix = create_initial_states_Matrix(T, model_input["freq_seasonal"], steps_ahead, model_input["level"], model_input["trend"], model_input["seasonal"])
176178
return hcat(initial_states_matrix, ξ_matrix, ζ_matrix, ω_matrix, o_matrix, vcat(Exogenous_X, Exogenous_Forecast))
177179

178180
end
@@ -194,9 +196,16 @@ function get_components_indexes(Exogenous_X::Matrix{Fl}, model_input::Dict)::Dic
194196

195197
outlier = model_input["outlier"]; ζ_ω_threshold = model_input["ζ_ω_threshold"]; T = size(Exogenous_X, 1)
196198

197-
μ1_indexes = [1]
198-
initial_states_indexes = [1]
199-
FINAL_INDEX = 1
199+
FINAL_INDEX = 0
200+
201+
if model_input["level"]
202+
μ1_indexes = [1]
203+
initial_states_indexes = [1]
204+
FINAL_INDEX += length(μ1_indexes)
205+
else
206+
μ1_indexes = Int64[]
207+
initial_states_indexes = Int64[]
208+
end
200209

201210
if model_input["trend"]
202211
ν1_indexes = [2]
@@ -214,7 +223,6 @@ function get_components_indexes(Exogenous_X::Matrix{Fl}, model_input::Dict)::Dic
214223
γ1_indexes = Int64[]
215224
end
216225

217-
218226
if model_input["stochastic_level"]
219227
ξ_indexes = collect(FINAL_INDEX+1:FINAL_INDEX+ξ_size(T))
220228
FINAL_INDEX += length(ξ_indexes)

src/utils.jl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,19 @@ function handle_missing_values(X::Matrix{Tl}, y::Vector{Fl})::Tuple{Vector{Fl},
105105
valid_indexes = setdiff(1:length(y), invalid_indexes)
106106

107107
return y[valid_indexes], X[valid_indexes, :], valid_indexes
108+
end
109+
110+
"""
111+
has_intercept(X::Matrix{Tl})::Bool where Tl
112+
113+
Checks if the input matrix has a constant column (intercept).
114+
115+
# Arguments
116+
- `X::Matrix{Tl}`: Input matrix.
117+
118+
# Returns
119+
- `Bool`: True if the input matrix has a constant column, false otherwise.
120+
"""
121+
function has_intercept(X::Matrix{Tl})::Bool where Tl
122+
return any([all(X[:, i] .== 1) for i in 1:size(X, 2)])
108123
end

0 commit comments

Comments
 (0)