@@ -67,52 +67,65 @@ class BayesianETS(PyMCStateSpace):
6767
6868 \begin{align}
6969 y_t &= l_{t-1} + b_{t-1} + \epsilon_t \\
70- l_t &= l_{t-1} + \alpha \epsilon_t \\
71- b_t &= b_{t-1} + \beta \epsilon_t
70+ l_t &= l_{t-1} + b_{t-1} + \alpha \epsilon_t \\
71+ b_t &= b_{t-1} + \alpha \ beta^\star \epsilon_t
7272 \end{align}
7373
74+ [1]_ also consider an alternative parameterization with :math:`\beta = \alpha \beta^\star`.
75+
7476 * `ETS(A,N,A)`: Additive seasonal method
7577
7678 .. math::
7779
7880 \begin{align}
7981 y_t &= l_{t-1} + s_{t-m} + \epsilon_t \\
8082 l_t &= l_{t-1} + \alpha \epsilon_t \\
81- s_t &= s_{t-m} + \ gamma \epsilon_t
83+ s_t &= s_{t-m} + (1 - \alpha)\ gamma^\star \epsilon_t
8284 \end{align}
8385
86+ [1]_ also consider an alternative parameterization with :math:`\gamma = (1 - \alpha) \gamma^\star`.
87+
8488 * `ETS(A,A,A)`: Additive Holt-Winters method
8589
8690 .. math::
8791
8892 \begin{align}
8993 y_t &= l_{t-1} + b_{t-1} + s_{t-m} + \epsilon_t \\
9094 l_t &= l_{t-1} + \alpha \epsilon_t \\
91- b_t &= b_{t-1} + \beta \epsilon_t \\
92- s_t &= s_{t-m} + \ gamma \epsilon_t
95+ b_t &= b_{t-1} + \alpha \ beta^\star \epsilon_t \\
96+ s_t &= s_{t-m} + (1 - \alpha) \ gamma^\star \epsilon_t
9397 \end{align}
9498
99+ [1]_ also consider an alternative parameterization with :math:`\beta = \alpha \beta^star` and
100+ :math:`\gamma = (1 - \alpha) \gamma^\star`.
101+
95102 * `ETS(A, Ad, N)`: Dampened trend method
96103
97104 .. math::
98105
99106 \begin{align}
100107 y_t &= l_{t-1} + b_{t-1} + \epsilon_t \\
101108 l_t &= l_{t-1} + \alpha \epsilon_t \\
102- b_t &= \phi b_{t-1} + \beta \epsilon_t
109+ b_t &= \phi b_{t-1} + \alpha \ beta^\star \epsilon_t
103110 \end{align}
104111
112+ [1]_ also consider an alternative parameterization with :math:`\beta = \alpha \beta^\star`.
113+
105114 * `ETS(A, Ad, A)`: Dampened trend with seasonal method
106115
107116 .. math::
108117
109118 \begin{align}
110119 y_t &= l_{t-1} + b_{t-1} + s_{t-m} + \epsilon_t \\
111120 l_t &= l_{t-1} + \alpha \epsilon_t \\
112- b_t &= \phi b_{t-1} + \beta \epsilon_t \\
113- s_t &= s_{t-m} + \ gamma \epsilon_t
121+ b_t &= \phi b_{t-1} + \alpha \ beta^\star \epsilon_t \\
122+ s_t &= s_{t-m} + (1 - \alpha) \ gamma^\star \epsilon_t
114123 \end{align}
115124
125+ [1]_ also consider an alternative parameterization with :math:`\beta = \alpha \beta^star` and
126+ :math:`\gamma = (1 - \alpha) \gamma^\star`.
127+
128+
116129 Parameters
117130 ----------
118131 endog: pd.DataFrame
@@ -138,6 +151,17 @@ class BayesianETS(PyMCStateSpace):
138151 The number of periods in a complete seasonal cycle. Ignored if `seasonal` is `False`.
139152 measurement_error: bool
140153 Whether to include a measurement error term in the model. Default is `False`.
154+ use_transformed_parameterization: bool, default False
155+ If true, use the :math:`\alpha, \beta, \gamma` parameterization, otherwise use the :math:`\alpha, \beta^\star,
156+ \gamma^\star` parameterization. This will change the admissible region for the priors.
157+
158+ - Under the **non-transformed** parameterization, all of :math:`\alpha, \beta^\star, \gamma^\star` should be
159+ between 0 and 1.
160+ - Under the **transformed** parameterization, :math:`\alpha \in (0, 1)`, :math:`\beta \in (0, \alpha)`, and
161+ :math:`\gamma \in (0, 1 - \alpha)`
162+
163+ The :meth:`param_info` method will change to reflect the suggested intervals based on the value of this
164+ argument.
141165 filter_type: str, default "standard"
142166 The type of Kalman Filter to use. Options are "standard", "single", "univariate", "steady_state",
143167 and "cholesky". See the docs for kalman filters for more details.
@@ -157,6 +181,7 @@ def __init__(
157181 seasonal : bool = False ,
158182 seasonal_periods : int | None = None ,
159183 measurement_error : bool = False ,
184+ use_transformed_parameterization : bool = False ,
160185 filter_type : str = "standard" ,
161186 verbose : bool = True ,
162187 ):
@@ -184,6 +209,7 @@ def __init__(
184209 self .damped_trend = damped_trend
185210 self .seasonal = seasonal
186211 self .seasonal_periods = seasonal_periods
212+ self .use_transformed_parameterization = use_transformed_parameterization
187213
188214 if self .seasonal and self .seasonal_periods is None :
189215 raise ValueError ("If seasonal is True, seasonal_periods must be provided." )
@@ -258,15 +284,19 @@ def param_info(self) -> dict[str, dict[str, Any]]:
258284 },
259285 "alpha" : {
260286 "shape" : None ,
261- "constraints" : "0 < Sum( alpha, beta, gamma) < 1" ,
287+ "constraints" : "0 < alpha < 1" ,
262288 },
263289 "beta" : {
264290 "shape" : None ,
265- "constraints" : "0 < Sum(alpha, beta, gamma) < 1" ,
291+ "constraints" : "0 < beta < 1"
292+ if not self .use_transformed_parameterization
293+ else "0 < beta < alpha" ,
266294 },
267295 "gamma" : {
268296 "shape" : None ,
269- "constraints" : "0 < Sum(alpha, beta, gamma) < 1" ,
297+ "constraints" : "0 < gamma< 1"
298+ if not self .use_transformed_parameterization
299+ else "0 < gamma < (1 - alpha)" ,
270300 },
271301 "phi" : {
272302 "shape" : None ,
@@ -342,11 +372,18 @@ def make_symbolic_graph(self) -> None:
342372
343373 # The shape of R can be pre-allocated, then filled with the required parameters
344374 R = pt .zeros ((self .k_states , self .k_posdef ))
345- R = pt .set_subtensor (R [0 , :], 1.0 ) # We will always have y_t = ... + e_t
346375
347376 alpha = self .make_and_register_variable ("alpha" , shape = (), dtype = floatX )
348377 R = pt .set_subtensor (R [1 , 0 ], alpha ) # and l_t = ... + alpha * e_t
349378
379+ # The R[0, 0] entry needs to be adjusted for a shift in the time indices. Consider the (A, N, N) model:
380+ # y_t = l_{t-1} + e_t
381+ # l_t = l_{t-1} + alpha * e_t
382+ # We want the first equation to be in terms of time t on the RHS, because our observation equation is always
383+ # y_t = Z @ x_t. Re-arranging equation 2, we get l_{t-1} = l_t - alpha * e_t --> y_t = l_t + e_t - alpha * e_t
384+ # --> y_t = l_t + (1 - alpha) * e_t
385+ R = pt .set_subtensor (R [0 , :], (1 - alpha ))
386+
350387 # Shock and level component always exists, the base case is e_t = e_t and l_t = l_{t-1}
351388 T_base = pt .as_tensor_variable (np .array ([[0.0 , 0.0 ], [0.0 , 1.0 ]]))
352389
@@ -357,10 +394,12 @@ def make_symbolic_graph(self) -> None:
357394 self .ssm ["initial_state" , 2 ] = initial_trend
358395
359396 beta = self .make_and_register_variable ("beta" , shape = (), dtype = floatX )
360- R = pt .set_subtensor (R [2 , 0 ], beta )
397+ if self .use_transformed_parameterization :
398+ R = pt .set_subtensor (R [2 , 0 ], beta )
399+ else :
400+ R = pt .set_subtensor (R [2 , 0 ], alpha * beta )
361401
362402 # If a trend is requested, we have the following transition equations (omitting the shocks):
363- # y_t = l_{t-1} + b_{t-1}
364403 # l_t = l_{t-1} + b_{t-1}
365404 # b_t = b_{t-1}
366405 T_base = pt .as_tensor_variable (([0.0 , 0.0 , 0.0 ], [0.0 , 1.0 , 1.0 ], [0.0 , 0.0 , 1.0 ]))
@@ -369,7 +408,6 @@ def make_symbolic_graph(self) -> None:
369408 phi = self .make_and_register_variable ("phi" , shape = (), dtype = floatX )
370409 # We are always in the case where we have a trend, so we can add the dampening parameter to T_base defined
371410 # in that branch. Transition equations become:
372- # y_t = l_{t-1} + phi * b_{t-1}
373411 # l_t = l_{t-1} + phi * b_{t-1}
374412 # b_t = phi * b_{t-1}
375413 T_base = pt .set_subtensor (T_base [1 :, 2 ], phi )
@@ -384,7 +422,21 @@ def make_symbolic_graph(self) -> None:
384422 self .ssm ["initial_state" , 2 + int (self .trend ) :] = initial_seasonal
385423
386424 gamma = self .make_and_register_variable ("gamma" , shape = (), dtype = floatX )
387- R = pt .set_subtensor (R [2 + int (self .trend ), 0 ], gamma )
425+
426+ if self .use_transformed_parameterization :
427+ param = gamma
428+ else :
429+ param = (1 - alpha ) * gamma
430+
431+ R = pt .set_subtensor (R [2 + int (self .trend ), 0 ], param )
432+
433+ # Additional adjustment to the R[0, 0] position is required. Start from:
434+ # y_t = l_{t-1} + s_{t-m} + e_t
435+ # l_t = l_{t-1} + alpha * e_t
436+ # s_t = s_{t-m} + gamma * e_t
437+ # Solve for l_{t-1} and s_{t-m} in terms of l_t and s_t, then substitute into the observation equation:
438+ # y_t = l_t + s_t - alpha * e_t - gamma * e_t + e_t --> y_t = l_t + s_t + (1 - alpha - gamma) * e_t
439+ R = pt .set_subtensor (R [0 , 0 ], R [0 , 0 ] - param )
388440
389441 # The seasonal component is always going to look like a TimeFrequency structural component, see that
390442 # docstring for more details
0 commit comments