1from __future__ import annotations 2 3from statsmodels.compat.pandas import Appender, Substitution, call_cached_func 4from statsmodels.compat.python import Literal 5 6from collections import defaultdict 7import datetime as dt 8from itertools import combinations, product 9import textwrap 10from types import SimpleNamespace 11from typing import ( 12 TYPE_CHECKING, 13 Any, 14 Dict, 15 Hashable, 16 List, 17 Mapping, 18 NamedTuple, 19 Optional, 20 Sequence, 21 Tuple, 22 Union, 23) 24import warnings 25 26import numpy as np 27import pandas as pd 28from scipy import stats 29 30from statsmodels.base.data import PandasData 31import statsmodels.base.wrapper as wrap 32from statsmodels.iolib.summary import Summary, summary_params 33from statsmodels.regression.linear_model import OLS 34from statsmodels.tools.decorators import cache_readonly 35from statsmodels.tools.docstring import Docstring, Parameter, remove_parameters 36from statsmodels.tools.sm_exceptions import SpecificationWarning 37from statsmodels.tools.validation import ( 38 array_like, 39 bool_like, 40 float_like, 41 int_like, 42) 43from statsmodels.tsa.ar_model import ( 44 AROrderSelectionResults, 45 AutoReg, 46 AutoRegResults, 47 sumofsq, 48) 49from statsmodels.tsa.ardl import pss_critical_values 50from statsmodels.tsa.arima_process import arma2ma 51from statsmodels.tsa.base import tsa_model 52from statsmodels.tsa.base.prediction import PredictionResults 53from statsmodels.tsa.deterministic import DeterministicProcess 54from statsmodels.tsa.tsatools import lagmat 55 56if TYPE_CHECKING: 57 import matplotlib.figure 58 59__all__ = [ 60 "ARDL", 61 "ARDLResults", 62 "ardl_select_order", 63 "ARDLOrderSelectionResults", 64 "UECM", 65 "UECMResults", 66 "BoundsTestResult", 67] 68 69 70class BoundsTestResult(NamedTuple): 71 stat: float 72 crit_vals: pd.DataFrame 73 p_values: pd.Series 74 null: str 75 alternative: str 76 77 def __repr__(self): 78 return f"""\ 79{self.__class__.__name__} 80Stat: {self.stat:0.5f} 81Upper P-value: {self.p_values["upper"]:0.3g} 82Lower P-value: {self.p_values["lower"]:0.3g} 83Null: {self.null} 84Alternative: {self.alternative} 85""" 86 87 88_UECMOrder = Union[None, int, Dict[Hashable, Optional[int]]] 89 90_ARDLOrder = Union[ 91 _UECMOrder, 92 Sequence[int], 93 Dict[Hashable, Union[None, int, Sequence[int]]], 94] 95 96_ArrayLike1D = Union[Sequence[float], np.ndarray, pd.Series] 97_ArrayLike2D = Union[Sequence[Sequence[float]], np.ndarray, pd.DataFrame] 98_INT_TYPES = (int, np.integer) 99 100 101def _check_order(order: Union[int, Sequence[int]], causal: bool) -> bool: 102 if order is None: 103 return True 104 if isinstance(order, (int, np.integer)): 105 if int(order) < int(causal): 106 raise ValueError( 107 f"integer orders must be at least {int(causal)} when causal " 108 f"is {causal}." 109 ) 110 return True 111 for v in order: 112 if not isinstance(v, (int, np.integer)): 113 raise TypeError( 114 "sequence orders must contain non-negative integer values" 115 ) 116 order = [int(v) for v in order] 117 if len(set(order)) != len(order) or min(order) < 0: 118 raise ValueError( 119 "sequence orders must contain distinct non-negative values" 120 ) 121 if int(causal) and min(order) < 1: 122 raise ValueError( 123 "sequence orders must be strictly positive when causal is True" 124 ) 125 return True 126 127 128def _format_order( 129 exog: _ArrayLike2D, order: _ARDLOrder, causal: bool 130) -> Dict[Hashable, List[int]]: 131 if exog is None and order in (0, None): 132 return {} 133 if not isinstance(exog, pd.DataFrame): 134 exog = array_like(exog, "exog", ndim=2, maxdim=2) 135 keys = list(range(exog.shape[1])) 136 else: 137 keys = exog.columns 138 if order is None: 139 exog_order = {k: None for k in keys} 140 elif isinstance(order, Mapping): 141 exog_order = order 142 missing = set(keys).difference(order.keys()) 143 extra = set(order.keys()).difference(keys) 144 if extra: 145 msg = ( 146 "order dictionary contains keys for exogenous " 147 "variable(s) that are not contained in exog" 148 ) 149 msg += " Extra keys: " 150 msg += ", ".join([str(k) for k in sorted(extra)]) + "." 151 raise ValueError(msg) 152 if missing: 153 msg = ( 154 "exog contains variables that are missing from the order " 155 "dictionary. Missing keys: " 156 ) 157 msg += ", ".join([str(k) for k in sorted(missing)]) + "." 158 warnings.warn(msg, SpecificationWarning) 159 160 for key in exog_order: 161 _check_order(exog_order[key], causal) 162 elif isinstance(order, _INT_TYPES): 163 _check_order(order, causal) 164 exog_order = {k: int(order) for k in keys} 165 else: 166 _check_order(order, causal) 167 exog_order = {k: list(order) for k in keys} 168 final_order: Dict[Hashable, List[int]] = {} 169 for key in exog_order: 170 if exog_order[key] is None: 171 continue 172 if isinstance(exog_order[key], int): 173 final_order[key] = list(range(int(causal), exog_order[key] + 1)) 174 else: 175 final_order[key] = [int(lag) for lag in exog_order[key]] 176 177 return final_order 178 179 180class ARDL(AutoReg): 181 r""" 182 Autoregressive Distributed Lag (ARDL) Model 183 184 Parameters 185 ---------- 186 endog : array_like 187 A 1-d endogenous response variable. The dependent variable. 188 lags : {int, list[int]} 189 The number of lags to include in the model if an integer or the 190 list of lag indices to include. For example, [1, 4] will only 191 include lags 1 and 4 while lags=4 will include lags 1, 2, 3, and 4. 192 exog : array_like 193 Exogenous variables to include in the model. Either a DataFrame or 194 an 2-d array-like structure that can be converted to a NumPy array. 195 order : {int, sequence[int], dict} 196 If int, uses lags 0, 1, ..., order for all exog variables. If 197 sequence[int], uses the ``order`` for all variables. If a dict, 198 applies the lags series by series. If ``exog`` is anything other 199 than a DataFrame, the keys are the column index of exog (e.g., 0, 200 1, ...). If a DataFrame, keys are column names. 201 fixed : array_like 202 Additional fixed regressors that are not lagged. 203 causal : bool, optional 204 Whether to include lag 0 of exog variables. If True, only includes 205 lags 1, 2, ... 206 trend : {'n', 'c', 't', 'ct'}, optional 207 The trend to include in the model: 208 209 * 'n' - No trend. 210 * 'c' - Constant only. 211 * 't' - Time trend only. 212 * 'ct' - Constant and time trend. 213 214 The default is 'c'. 215 216 seasonal : bool, optional 217 Flag indicating whether to include seasonal dummies in the model. If 218 seasonal is True and trend includes 'c', then the first period 219 is excluded from the seasonal terms. 220 deterministic : DeterministicProcess, optional 221 A deterministic process. If provided, trend and seasonal are ignored. 222 A warning is raised if trend is not "n" and seasonal is not False. 223 hold_back : {None, int}, optional 224 Initial observations to exclude from the estimation sample. If None, 225 then hold_back is equal to the maximum lag in the model. Set to a 226 non-zero value to produce comparable models with different lag 227 length. For example, to compare the fit of a model with lags=3 and 228 lags=1, set hold_back=3 which ensures that both models are estimated 229 using observations 3,...,nobs. hold_back must be >= the maximum lag in 230 the model. 231 period : {None, int}, optional 232 The period of the data. Only used if seasonal is True. This parameter 233 can be omitted if using a pandas object for endog that contains a 234 recognized frequency. 235 missing : {"none", "drop", "raise"}, optional 236 Available options are 'none', 'drop', and 'raise'. If 'none', no nan 237 checking is done. If 'drop', any observations with nans are dropped. 238 If 'raise', an error is raised. Default is 'none'. 239 240 Notes 241 ----- 242 The full specification of an ARDL is 243 244 .. math :: 245 246 Y_t = \delta_0 + \delta_1 t + \delta_2 t^2 247 + \sum_{i=1}^{s-1} \gamma_i I_{[(\mod(t,s) + 1) = i]} 248 + \sum_{j=1}^p \phi_j Y_{t-j} 249 + \sum_{l=1}^k \sum_{m=0}^{o_l} \beta_{l,m} X_{l, t-m} 250 + Z_t \lambda 251 + \epsilon_t 252 253 where :math:`\delta_\bullet` capture trends, :math:`\gamma_\bullet` 254 capture seasonal shifts, s is the period of the seasonality, p is the 255 lag length of the endogenous variable, k is the number of exogenous 256 variables :math:`X_{l}`, :math:`o_l` is included the lag length of 257 :math:`X_{l}`, :math:`Z_t` are ``r`` included fixed regressors and 258 :math:`\epsilon_t` is a white noise shock. If ``causal`` is ``True``, 259 then the 0-th lag of the exogenous variables is not included and the 260 sum starts at ``m=1``. 261 262 See Also 263 -------- 264 statsmodels.tsa.ar_model.AutoReg 265 Autoregressive model estimation with optional exogenous regressors 266 statsmodels.tsa.ardl.UECM 267 Unconstrained Error Correction Model estimation 268 statsmodels.tsa.statespace.sarimax.SARIMAX 269 Seasonal ARIMA model estimation with optional exogenous regressors 270 statsmodels.tsa.arima.model.ARIMA 271 ARIMA model estimation 272 273 Examples 274 -------- 275 >>> from statsmodels.tsa.api import ARDL 276 >>> from statsmodels.datasets import danish_data 277 >>> data = danish_data.load_pandas().data 278 >>> lrm = data.lrm 279 >>> exog = data[["lry", "ibo", "ide"]] 280 281 A basic model where all variables have 3 lags included 282 283 >>> ARDL(data.lrm, 3, data[["lry", "ibo", "ide"]], 3) 284 285 A dictionary can be used to pass custom lag orders 286 287 >>> ARDL(data.lrm, [1, 3], exog, {"lry": 1, "ibo": 3, "ide": 2}) 288 289 Setting causal removes the 0-th lag from the exogenous variables 290 291 >>> exog_lags = {"lry": 1, "ibo": 3, "ide": 2} 292 >>> ARDL(data.lrm, [1, 3], exog, exog_lags, causal=True) 293 294 A dictionary can also be used to pass specific lags to include. 295 Sequences hold the specific lags to include, while integers are expanded 296 to include [0, 1, ..., lag]. If causal is False, then the 0-th lag is 297 excluded. 298 299 >>> ARDL(lrm, [1, 3], exog, {"lry": [0, 1], "ibo": [0, 1, 3], "ide": 2}) 300 301 When using NumPy arrays, the dictionary keys are the column index. 302 303 >>> import numpy as np 304 >>> lrma = np.asarray(lrm) 305 >>> exoga = np.asarray(exog) 306 >>> ARDL(lrma, 3, exoga, {0: [0, 1], 1: [0, 1, 3], 2: 2}) 307 """ 308 309 def __init__( 310 self, 311 endog: Union[Sequence[float], pd.Series, _ArrayLike2D], 312 lags: Union[None, int, Sequence[int]], 313 exog: Optional[_ArrayLike2D] = None, 314 order: _ARDLOrder = 0, 315 trend: Literal["n", "c", "ct", "ctt"] = "c", 316 *, 317 fixed: Optional[_ArrayLike2D] = None, 318 causal: bool = False, 319 seasonal: bool = False, 320 deterministic: Optional[DeterministicProcess] = None, 321 hold_back: Optional[int] = None, 322 period: Optional[int] = None, 323 missing: Literal["none", "drop", "raise"] = "none", 324 ) -> None: 325 self._x = np.empty((0, 0)) 326 self._y = np.empty((0,)) 327 328 super().__init__( 329 endog, 330 lags, 331 trend=trend, 332 seasonal=seasonal, 333 exog=exog, 334 hold_back=hold_back, 335 period=period, 336 missing=missing, 337 deterministic=deterministic, 338 old_names=False, 339 ) 340 # Reset hold back which was set in AutoReg.__init__ 341 self._causal = bool_like(causal, "causal", strict=True) 342 self.data.orig_fixed = fixed 343 if fixed is not None: 344 fixed_arr = array_like(fixed, "fixed", ndim=2, maxdim=2) 345 if fixed_arr.shape[0] != self.data.endog.shape[0] or not np.all( 346 np.isfinite(fixed_arr) 347 ): 348 raise ValueError( 349 "fixed must be an (nobs, m) array where nobs matches the " 350 "number of observations in the endog variable, and all" 351 "values must be finite" 352 ) 353 if isinstance(fixed, pd.DataFrame): 354 self._fixed_names = list(fixed.columns) 355 else: 356 self._fixed_names = [ 357 f"z.{i}" for i in range(fixed_arr.shape[1]) 358 ] 359 self._fixed = fixed_arr 360 else: 361 self._fixed = np.empty((self.data.endog.shape[0], 0)) 362 self._fixed_names = [] 363 364 self._blocks: Dict[str, np.ndarray] = {} 365 self._names: Dict[str, Sequence[str]] = {} 366 367 # 1. Check and update order 368 self._order = self._check_order(order) 369 # 2. Construct Regressors 370 self._y, self._x = self._construct_regressors(hold_back) 371 # 3. Construct variable names 372 self._endog_name, self._exog_names = self._construct_variable_names() 373 self.data.param_names = self.data.xnames = self._exog_names 374 self.data.ynames = self._endog_name 375 376 self._causal = True 377 if self._order: 378 min_lags = [min(val) for val in self._order.values()] 379 self._causal = min(min_lags) > 0 380 self._results_class = ARDLResults 381 self._results_wrapper = ARDLResultsWrapper 382 383 @property 384 def fixed(self) -> Union[None, np.ndarray, pd.DataFrame]: 385 """The fixed data used to construct the model""" 386 return self.data.orig_fixed 387 388 @property 389 def causal(self) -> bool: 390 """Flag indicating that the ARDL is causal""" 391 return self._causal 392 393 @property 394 def ar_lags(self) -> Optional[List[int]]: 395 """The autoregressive lags included in the model""" 396 return None if not self._lags else self._lags 397 398 @property 399 def dl_lags(self) -> Dict[Hashable, List[int]]: 400 """The lags of exogenous variables included in the model""" 401 return self._order 402 403 @property 404 def ardl_order(self) -> Tuple[int, ...]: 405 """The order of the ARDL(p,q)""" 406 ar_order = 0 if not self._lags else int(max(self._lags)) 407 ardl_order = [ar_order] 408 for lags in self._order.values(): 409 if lags is not None: 410 ardl_order.append(int(max(lags))) 411 return tuple(ardl_order) 412 413 def _setup_regressors(self) -> None: 414 """Place holder to let AutoReg init complete""" 415 self._y = np.empty((self.endog.shape[0] - self._hold_back, 0)) 416 417 @staticmethod 418 def _format_exog( 419 exog: _ArrayLike2D, order: Dict[Hashable, List[int]] 420 ) -> Dict[Hashable, np.ndarray]: 421 """Transform exogenous variables and orders to regressors""" 422 if not order: 423 return {} 424 max_order = 0 425 for val in order.values(): 426 if val is not None: 427 max_order = max(max(val), max_order) 428 if not isinstance(exog, pd.DataFrame): 429 exog = array_like(exog, "exog", ndim=2, maxdim=2) 430 exog_lags = {} 431 for key in order: 432 if order[key] is None: 433 continue 434 if isinstance(exog, np.ndarray): 435 col = exog[:, key] 436 else: 437 col = exog[key] 438 lagged_col = lagmat(col, max_order, original="in") 439 lags = order[key] 440 exog_lags[key] = lagged_col[:, lags] 441 return exog_lags 442 443 def _check_order(self, order: _ARDLOrder) -> Dict[Hashable, List[int]]: 444 """Validate and standardize the model order""" 445 return _format_order(self.data.orig_exog, order, self._causal) 446 447 def _fit( 448 self, 449 cov_type: str = "nonrobust", 450 cov_kwds: Dict[str, Any] = None, 451 use_t: bool = True, 452 ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: 453 if self._x.shape[1] == 0: 454 return np.empty((0,)), np.empty((0, 0)), np.empty((0, 0)) 455 ols_mod = OLS(self._y, self._x) 456 ols_res = ols_mod.fit( 457 cov_type=cov_type, cov_kwds=cov_kwds, use_t=use_t 458 ) 459 cov_params = ols_res.cov_params() 460 use_t = ols_res.use_t 461 if cov_type == "nonrobust" and not use_t: 462 nobs = self._y.shape[0] 463 k = self._x.shape[1] 464 scale = nobs / (nobs - k) 465 cov_params /= scale 466 467 return ols_res.params, cov_params, ols_res.normalized_cov_params 468 469 def fit( 470 self, 471 *, 472 cov_type: str = "nonrobust", 473 cov_kwds: Dict[str, Any] = None, 474 use_t: bool = True, 475 ) -> ARDLResults: 476 """ 477 Estimate the model parameters. 478 479 Parameters 480 ---------- 481 cov_type : str 482 The covariance estimator to use. The most common choices are listed 483 below. Supports all covariance estimators that are available 484 in ``OLS.fit``. 485 486 * 'nonrobust' - The class OLS covariance estimator that assumes 487 homoskedasticity. 488 * 'HC0', 'HC1', 'HC2', 'HC3' - Variants of White's 489 (or Eiker-Huber-White) covariance estimator. `HC0` is the 490 standard implementation. The other make corrections to improve 491 the finite sample performance of the heteroskedasticity robust 492 covariance estimator. 493 * 'HAC' - Heteroskedasticity-autocorrelation robust covariance 494 estimation. Supports cov_kwds. 495 496 - `maxlags` integer (required) : number of lags to use. 497 - `kernel` callable or str (optional) : kernel 498 currently available kernels are ['bartlett', 'uniform'], 499 default is Bartlett. 500 - `use_correction` bool (optional) : If true, use small sample 501 correction. 502 cov_kwds : dict, optional 503 A dictionary of keyword arguments to pass to the covariance 504 estimator. `nonrobust` and `HC#` do not support cov_kwds. 505 use_t : bool, optional 506 A flag indicating that inference should use the Student's t 507 distribution that accounts for model degree of freedom. If False, 508 uses the normal distribution. If None, defers the choice to 509 the cov_type. It also removes degree of freedom corrections from 510 the covariance estimator when cov_type is 'nonrobust'. 511 512 Returns 513 ------- 514 ARDLResults 515 Estimation results. 516 517 See Also 518 -------- 519 statsmodels.tsa.ar_model.AutoReg 520 Ordinary Least Squares estimation. 521 statsmodels.regression.linear_model.OLS 522 Ordinary Least Squares estimation. 523 statsmodels.regression.linear_model.RegressionResults 524 See ``get_robustcov_results`` for a detailed list of available 525 covariance estimators and options. 526 527 Notes 528 ----- 529 Use ``OLS`` to estimate model parameters and to estimate parameter 530 covariance. 531 """ 532 params, cov_params, norm_cov_params = self._fit( 533 cov_type=cov_type, cov_kwds=cov_kwds, use_t=use_t 534 ) 535 res = ARDLResults( 536 self, params, cov_params, norm_cov_params, use_t=use_t 537 ) 538 return ARDLResultsWrapper(res) 539 540 def _construct_regressors( 541 self, hold_back: Optional[int] 542 ) -> Tuple[np.ndarray, np.ndarray]: 543 """Construct and format model regressors""" 544 # TODO: Missing adjustment 545 self._maxlag = max(self._lags) if self._lags else 0 546 self._endog_reg, self._endog = lagmat( 547 self.data.endog, self._maxlag, original="sep" 548 ) 549 if self._endog_reg.shape[1] != len(self._lags): 550 lag_locs = [lag - 1 for lag in self._lags] 551 self._endog_reg = self._endog_reg[:, lag_locs] 552 553 orig_exog = self.data.orig_exog 554 self._exog = self._format_exog(orig_exog, self._order) 555 556 exog_maxlag = 0 557 for val in self._order.values(): 558 exog_maxlag = max(exog_maxlag, max(val) if val is not None else 0) 559 self._maxlag = max(self._maxlag, exog_maxlag) 560 561 self._deterministic_reg = self._deterministics.in_sample() 562 self._blocks = { 563 "endog": self._endog_reg, 564 "exog": self._exog, 565 "deterministic": self._deterministic_reg, 566 "fixed": self._fixed, 567 } 568 x = [self._deterministic_reg, self._endog_reg] 569 x += [ex for ex in self._exog.values()] + [self._fixed] 570 reg = np.column_stack(x) 571 if hold_back is None: 572 self._hold_back = int(self._maxlag) 573 if self._hold_back < self._maxlag: 574 raise ValueError( 575 "hold_back must be >= the maximum lag of the endog and exog " 576 "variables" 577 ) 578 reg = reg[self._hold_back :] 579 if reg.shape[1] > reg.shape[0]: 580 raise ValueError( 581 f"The number of regressors ({reg.shape[1]}) including " 582 "deterministics, lags of the endog, lags of the exogenous, " 583 "and fixed regressors is larer than the sample available " 584 f"for estimation ({reg.shape[0]})." 585 ) 586 return self.data.endog[self._hold_back :], reg 587 588 def _construct_variable_names(self): 589 """Construct model variables names""" 590 y_name = self.data.ynames 591 endog_lag_names = [f"{y_name}.L{i}" for i in self._lags] 592 593 exog = self.data.orig_exog 594 exog_names = {} 595 for key in self._order: 596 if isinstance(exog, np.ndarray): 597 base = f"x{key}" 598 else: 599 base = str(key) 600 lags = self._order[key] 601 exog_names[key] = [f"{base}.L{lag}" for lag in lags] 602 603 self._names = { 604 "endog": endog_lag_names, 605 "exog": exog_names, 606 "deterministic": self._deterministic_reg.columns, 607 "fixed": self._fixed_names, 608 } 609 x_names = list(self._deterministic_reg.columns) 610 x_names += endog_lag_names 611 for key in exog_names: 612 x_names += exog_names[key] 613 x_names += self._fixed_names 614 return y_name, x_names 615 616 def _forecasting_x( 617 self, 618 start: int, 619 end: int, 620 num_oos: int, 621 exog: Optional[_ArrayLike2D], 622 exog_oos: Optional[_ArrayLike2D], 623 fixed: Optional[_ArrayLike2D], 624 fixed_oos: Optional[_ArrayLike2D], 625 ) -> np.ndarray: 626 """Construct exog matrix for forecasts""" 627 628 def pad_x(x: np.ndarray, pad: int) -> np.ndarray: 629 if pad == 0: 630 return x 631 k = x.shape[1] 632 return np.vstack([np.full((pad, k), np.nan), x]) 633 634 pad = 0 if start >= self._hold_back else self._hold_back - start 635 # Shortcut if all in-sample and no new data 636 637 if (end + 1) < self.endog.shape[0] and exog is None and fixed is None: 638 adjusted_start = max(start - self._hold_back, 0) 639 return pad_x( 640 self._x[adjusted_start : end + 1 - self._hold_back], pad 641 ) 642 643 # If anything changed, rebuild x array 644 exog = self.data.exog if exog is None else np.asarray(exog) 645 if exog_oos is not None: 646 exog = np.vstack([exog, np.asarray(exog_oos)[:num_oos]]) 647 fixed = self._fixed if fixed is None else np.asarray(fixed) 648 if fixed_oos is not None: 649 fixed = np.vstack([fixed, np.asarray(fixed_oos)[:num_oos]]) 650 det = self._deterministics.in_sample() 651 if num_oos: 652 oos_det = self._deterministics.out_of_sample(num_oos) 653 det = pd.concat([det, oos_det], axis=0) 654 endog = self.data.endog 655 if num_oos: 656 endog = np.hstack([endog, np.full(num_oos, np.nan)]) 657 x = [det] 658 if self._lags: 659 endog_reg = lagmat(endog, max(self._lags), original="ex") 660 x.append(endog_reg[:, [lag - 1 for lag in self._lags]]) 661 if self.ardl_order[1:]: 662 if isinstance(self.data.orig_exog, pd.DataFrame): 663 exog = pd.DataFrame(exog, columns=self.data.orig_exog.columns) 664 exog = self._format_exog(exog, self._order) 665 x.extend([np.asarray(arr) for arr in exog.values()]) 666 if fixed.shape[1] > 0: 667 x.append(fixed) 668 _x = np.column_stack(x) 669 _x[: self._hold_back] = np.nan 670 return _x[start:] 671 672 def predict( 673 self, 674 params: _ArrayLike1D, 675 start: Union[None, int, str, dt.datetime, pd.Timestamp] = None, 676 end: Union[None, int, str, dt.datetime, pd.Timestamp] = None, 677 dynamic: bool = False, 678 exog: Union[None, np.ndarray, pd.DataFrame] = None, 679 exog_oos: Union[None, np.ndarray, pd.DataFrame] = None, 680 fixed: Union[None, np.ndarray, pd.DataFrame] = None, 681 fixed_oos: Union[None, np.ndarray, pd.DataFrame] = None, 682 ): 683 """ 684 In-sample prediction and out-of-sample forecasting. 685 686 Parameters 687 ---------- 688 params : array_like 689 The fitted model parameters. 690 start : int, str, or datetime, optional 691 Zero-indexed observation number at which to start forecasting, 692 i.e., the first forecast is start. Can also be a date string to 693 parse or a datetime type. Default is the the zeroth observation. 694 end : int, str, or datetime, optional 695 Zero-indexed observation number at which to end forecasting, i.e., 696 the last forecast is end. Can also be a date string to 697 parse or a datetime type. However, if the dates index does not 698 have a fixed frequency, end must be an integer index if you 699 want out-of-sample prediction. Default is the last observation in 700 the sample. Unlike standard python slices, end is inclusive so 701 that all the predictions [start, start+1, ..., end-1, end] are 702 returned. 703 dynamic : {bool, int, str, datetime, Timestamp}, optional 704 Integer offset relative to `start` at which to begin dynamic 705 prediction. Prior to this observation, true endogenous values 706 will be used for prediction; starting with this observation and 707 continuing through the end of prediction, forecasted endogenous 708 values will be used instead. Datetime-like objects are not 709 interpreted as offsets. They are instead used to find the index 710 location of `dynamic` which is then used to to compute the offset. 711 exog : array_like 712 A replacement exogenous array. Must have the same shape as the 713 exogenous data array used when the model was created. 714 exog_oos : array_like 715 An array containing out-of-sample values of the exogenous 716 variables. Must have the same number of columns as the exog 717 used when the model was created, and at least as many rows as 718 the number of out-of-sample forecasts. 719 fixed : array_like 720 A replacement fixed array. Must have the same shape as the 721 fixed data array used when the model was created. 722 fixed_oos : array_like 723 An array containing out-of-sample values of the fixed variables. 724 Must have the same number of columns as the fixed used when the 725 model was created, and at least as many rows as the number of 726 out-of-sample forecasts. 727 728 Returns 729 ------- 730 predictions : {ndarray, Series} 731 Array of out of in-sample predictions and / or out-of-sample 732 forecasts. 733 """ 734 params, exog, exog_oos, start, end, num_oos = self._prepare_prediction( 735 params, exog, exog_oos, start, end 736 ) 737 738 def check_exog(arr, name, orig, exact): 739 if isinstance(orig, pd.DataFrame): 740 if not isinstance(arr, pd.DataFrame): 741 raise TypeError( 742 f"{name} must be a DataFrame when the original exog " 743 f"was a DataFrame" 744 ) 745 if sorted(arr.columns) != sorted(self.data.orig_exog.columns): 746 raise ValueError( 747 f"{name} must have the same columns as the original " 748 f"exog" 749 ) 750 else: 751 arr = array_like(arr, name, ndim=2, optional=False) 752 if arr.ndim != 2 or arr.shape[1] != orig.shape[1]: 753 raise ValueError( 754 f"{name} must have the same number of columns as the " 755 f"original data, {orig.shape[1]}" 756 ) 757 if exact and arr.shape[0] != orig.shape[0]: 758 raise ValueError( 759 f"{name} must have the same number of rows as the " 760 f"original data ({n})." 761 ) 762 return arr 763 764 n = self.data.endog.shape[0] 765 if exog is not None: 766 exog = check_exog(exog, "exog", self.data.orig_exog, True) 767 if exog_oos is not None: 768 exog_oos = check_exog( 769 exog_oos, "exog_oos", self.data.orig_exog, False 770 ) 771 if fixed is not None: 772 fixed = check_exog(fixed, "fixed", self._fixed, True) 773 if fixed_oos is not None: 774 fixed_oos = check_exog( 775 np.asarray(fixed_oos), "fixed_oos", self._fixed, False 776 ) 777 # The maximum number of 1-step predictions that can be made, 778 # which depends on the model and lags 779 if self._fixed.shape[1] or not self._causal: 780 max_1step = 0 781 else: 782 max_1step = np.inf if not self._lags else min(self._lags) 783 if self._order: 784 min_exog = min([min(v) for v in self._order.values()]) 785 max_1step = min(max_1step, min_exog) 786 if num_oos > max_1step: 787 if self._order and exog_oos is None: 788 raise ValueError( 789 "exog_oos must be provided when out-of-sample " 790 "observations require values of the exog not in the " 791 "original sample" 792 ) 793 elif self._order and (exog_oos.shape[0] + max_1step) < num_oos: 794 raise ValueError( 795 f"exog_oos must have at least {num_oos - max_1step} " 796 f"observations to produce {num_oos} forecasts based on " 797 f"the model specification." 798 ) 799 800 if self._fixed.shape[1] and fixed_oos is None: 801 raise ValueError( 802 "fixed_oos must be provided when predicting " 803 "out-of-sample observations" 804 ) 805 elif self._fixed.shape[1] and fixed_oos.shape[0] < num_oos: 806 raise ValueError( 807 f"fixed_oos must have at least {num_oos} observations " 808 f"to produce {num_oos} forecasts." 809 ) 810 # Extend exog_oos if fcast is valid for horizon but no exog_oos given 811 if self.exog is not None and exog_oos is None and num_oos: 812 exog_oos = np.full((num_oos, self.exog.shape[1]), np.nan) 813 if isinstance(self.data.orig_exog, pd.DataFrame): 814 exog_oos = pd.DataFrame( 815 exog_oos, columns=self.data.orig_exog.columns 816 ) 817 x = self._forecasting_x( 818 start, end, num_oos, exog, exog_oos, fixed, fixed_oos 819 ) 820 if dynamic is False: 821 dynamic_start = end + 1 - start 822 else: 823 dynamic_step = self._parse_dynamic(dynamic, start) 824 dynamic_start = dynamic_step 825 if start < self._hold_back: 826 dynamic_start = max(dynamic_start, self._hold_back - start) 827 828 fcasts = np.full(x.shape[0], np.nan) 829 fcasts[:dynamic_start] = x[:dynamic_start] @ params 830 offset = self._deterministic_reg.shape[1] 831 for i in range(dynamic_start, fcasts.shape[0]): 832 for j, lag in enumerate(self._lags): 833 loc = i - lag 834 if loc >= dynamic_start: 835 val = fcasts[loc] 836 else: 837 # Actual data 838 val = self.endog[start + loc] 839 x[i, offset + j] = val 840 fcasts[i] = x[i] @ params 841 return self._wrap_prediction(fcasts, start, end + 1 + num_oos, 0) 842 843 @classmethod 844 def from_formula( 845 cls, 846 formula: str, 847 data: pd.DataFrame, 848 lags: Union[None, int, Sequence[int]] = 0, 849 order: _ARDLOrder = 0, 850 trend: Literal["n", "c", "ct", "ctt"] = "n", 851 *, 852 causal: bool = False, 853 seasonal: bool = False, 854 deterministic: Optional[DeterministicProcess] = None, 855 hold_back: Optional[int] = None, 856 period: Optional[int] = None, 857 missing: Literal["none", "raise"] = "none", 858 ) -> ARDL: 859 """ 860 Construct an ARDL from a formula 861 862 Parameters 863 ---------- 864 formula : str 865 Formula with form dependent ~ independent | fixed. See Examples 866 below. 867 data : DataFrame 868 DataFrame containing the variables in the formula. 869 lags : {int, list[int]} 870 The number of lags to include in the model if an integer or the 871 list of lag indices to include. For example, [1, 4] will only 872 include lags 1 and 4 while lags=4 will include lags 1, 2, 3, 873 and 4. 874 order : {int, sequence[int], dict} 875 If int, uses lags 0, 1, ..., order for all exog variables. If 876 sequence[int], uses the ``order`` for all variables. If a dict, 877 applies the lags series by series. If ``exog`` is anything other 878 than a DataFrame, the keys are the column index of exog (e.g., 0, 879 1, ...). If a DataFrame, keys are column names. 880 causal : bool, optional 881 Whether to include lag 0 of exog variables. If True, only 882 includes lags 1, 2, ... 883 trend : {'n', 'c', 't', 'ct'}, optional 884 The trend to include in the model: 885 886 * 'n' - No trend. 887 * 'c' - Constant only. 888 * 't' - Time trend only. 889 * 'ct' - Constant and time trend. 890 891 The default is 'c'. 892 893 seasonal : bool, optional 894 Flag indicating whether to include seasonal dummies in the model. 895 If seasonal is True and trend includes 'c', then the first period 896 is excluded from the seasonal terms. 897 deterministic : DeterministicProcess, optional 898 A deterministic process. If provided, trend and seasonal are 899 ignored. A warning is raised if trend is not "n" and seasonal 900 is not False. 901 hold_back : {None, int}, optional 902 Initial observations to exclude from the estimation sample. If 903 None, then hold_back is equal to the maximum lag in the model. 904 Set to a non-zero value to produce comparable models with 905 different lag length. For example, to compare the fit of a model 906 with lags=3 and lags=1, set hold_back=3 which ensures that both 907 models are estimated using observations 3,...,nobs. hold_back 908 must be >= the maximum lag in the model. 909 period : {None, int}, optional 910 The period of the data. Only used if seasonal is True. This 911 parameter can be omitted if using a pandas object for endog 912 that contains a recognized frequency. 913 missing : {"none", "drop", "raise"}, optional 914 Available options are 'none', 'drop', and 'raise'. If 'none', no 915 nan checking is done. If 'drop', any observations with nans are 916 dropped. If 'raise', an error is raised. Default is 'none'. 917 918 Returns 919 ------- 920 ARDL 921 The ARDL model instance 922 923 Examples 924 -------- 925 A simple ARDL using the Danish data 926 927 >>> from statsmodels.datasets.danish_data import load 928 >>> from statsmodels.tsa.api import ARDL 929 >>> data = load().data 930 >>> mod = ARDL.from_formula("lrm ~ ibo", data, 2, 2) 931 932 Fixed regressors can be specified using a | 933 934 >>> mod = ARDL.from_formula("lrm ~ ibo | ide", data, 2, 2) 935 """ 936 index = data.index 937 fixed_formula = None 938 if "|" in formula: 939 formula, fixed_formula = formula.split("|") 940 fixed_formula = fixed_formula.strip() 941 mod = OLS.from_formula(formula + " -1", data) 942 exog = mod.data.orig_exog 943 exog.index = index 944 endog = mod.data.orig_endog 945 endog.index = index 946 if fixed_formula is not None: 947 endog_name = formula.split("~")[0].strip() 948 fixed_formula = f"{endog_name} ~ {fixed_formula} - 1" 949 mod = OLS.from_formula(fixed_formula, data) 950 fixed: Optional[pd.DataFrame] = mod.data.orig_exog 951 fixed.index = index 952 else: 953 fixed = None 954 return cls( 955 endog, 956 lags, 957 exog, 958 order, 959 trend=trend, 960 fixed=fixed, 961 causal=causal, 962 seasonal=seasonal, 963 deterministic=deterministic, 964 hold_back=hold_back, 965 period=period, 966 missing=missing, 967 ) 968 969 970doc = Docstring(ARDL.predict.__doc__) 971_predict_params = doc.extract_parameters( 972 ["start", "end", "dynamic", "exog", "exog_oos", "fixed", "fixed_oos"], 8 973) 974 975 976class ARDLResults(AutoRegResults): 977 """ 978 Class to hold results from fitting an ARDL model. 979 980 Parameters 981 ---------- 982 model : ARDL 983 Reference to the model that is fit. 984 params : ndarray 985 The fitted parameters from the AR Model. 986 cov_params : ndarray 987 The estimated covariance matrix of the model parameters. 988 normalized_cov_params : ndarray 989 The array inv(dot(x.T,x)) where x contains the regressors in the 990 model. 991 scale : float, optional 992 An estimate of the scale of the model. 993 use_t : bool 994 Whether use_t was set in fit 995 """ 996 997 _cache = {} # for scale setter 998 999 def __init__( 1000 self, 1001 model: ARDL, 1002 params: np.ndarray, 1003 cov_params: np.ndarray, 1004 normalized_cov_params: Optional[np.ndarray] = None, 1005 scale: float = 1.0, 1006 use_t: bool = False, 1007 ): 1008 super().__init__( 1009 model, params, normalized_cov_params, scale, use_t=use_t 1010 ) 1011 self._cache = {} 1012 self._params = params 1013 self._nobs = model.nobs 1014 self._n_totobs = model.endog.shape[0] 1015 self._df_model = model.df_model 1016 self._ar_lags = model.ar_lags 1017 self._max_lag = 0 1018 if self._ar_lags: 1019 self._max_lag = max(self._ar_lags) 1020 self._hold_back = self.model.hold_back 1021 self.cov_params_default = cov_params 1022 1023 @Appender(remove_parameters(ARDL.predict.__doc__, "params")) 1024 def predict( 1025 self, 1026 start: Union[None, int, str, dt.datetime, pd.Timestamp] = None, 1027 end: Union[None, int, str, dt.datetime, pd.Timestamp] = None, 1028 dynamic: bool = False, 1029 exog: Union[None, np.ndarray, pd.DataFrame] = None, 1030 exog_oos: Union[None, np.ndarray, pd.DataFrame] = None, 1031 fixed: Union[None, np.ndarray, pd.DataFrame] = None, 1032 fixed_oos: Union[None, np.ndarray, pd.DataFrame] = None, 1033 ): 1034 return self.model.predict( 1035 self._params, 1036 start=start, 1037 end=end, 1038 dynamic=dynamic, 1039 exog=exog, 1040 exog_oos=exog_oos, 1041 fixed=fixed, 1042 fixed_oos=fixed_oos, 1043 ) 1044 1045 def forecast( 1046 self, 1047 steps: int = 1, 1048 exog: Union[None, np.ndarray, pd.DataFrame] = None, 1049 fixed: Union[None, np.ndarray, pd.DataFrame] = None, 1050 ) -> Union[np.ndarray, pd.Series]: 1051 """ 1052 Out-of-sample forecasts 1053 1054 Parameters 1055 ---------- 1056 steps : {int, str, datetime}, default 1 1057 If an integer, the number of steps to forecast from the end of the 1058 sample. Can also be a date string to parse or a datetime type. 1059 However, if the dates index does not have a fixed frequency, 1060 steps must be an integer. 1061 exog : array_like, optional 1062 Exogenous values to use out-of-sample. Must have same number of 1063 columns as original exog data and at least `steps` rows 1064 fixed : array_like, optional 1065 Fixed values to use out-of-sample. Must have same number of 1066 columns as original fixed data and at least `steps` rows 1067 1068 Returns 1069 ------- 1070 array_like 1071 Array of out of in-sample predictions and / or out-of-sample 1072 forecasts. 1073 1074 See Also 1075 -------- 1076 ARDLResults.predict 1077 In- and out-of-sample predictions 1078 ARDLResults.get_prediction 1079 In- and out-of-sample predictions and confidence intervals 1080 """ 1081 start = self.model.data.orig_endog.shape[0] 1082 if isinstance(steps, (int, np.integer)): 1083 end = start + steps - 1 1084 else: 1085 end = steps 1086 return self.predict( 1087 start=start, end=end, dynamic=False, exog_oos=exog, fixed_oos=fixed 1088 ) 1089 1090 def _lag_repr(self) -> np.ndarray: 1091 """Returns poly repr of an AR, (1 -phi1 L -phi2 L^2-...)""" 1092 ar_lags = self._ar_lags if self._ar_lags is not None else [] 1093 k_ar = len(ar_lags) 1094 ar_params = np.zeros(self._max_lag + 1) 1095 ar_params[0] = 1 1096 offset = self.model._deterministic_reg.shape[1] 1097 params = self._params[offset : offset + k_ar] 1098 for i, lag in enumerate(ar_lags): 1099 ar_params[lag] = -params[i] 1100 return ar_params 1101 1102 def get_prediction( 1103 self, 1104 start: Union[None, int, str, dt.datetime, pd.Timestamp] = None, 1105 end: Union[None, int, str, dt.datetime, pd.Timestamp] = None, 1106 dynamic: bool = False, 1107 exog: Union[None, np.ndarray, pd.DataFrame] = None, 1108 exog_oos: Union[None, np.ndarray, pd.DataFrame] = None, 1109 fixed: Union[None, np.ndarray, pd.DataFrame] = None, 1110 fixed_oos: Union[None, np.ndarray, pd.DataFrame] = None, 1111 ) -> Union[np.ndarray, pd.Series]: 1112 """ 1113 Predictions and prediction intervals 1114 1115 Parameters 1116 ---------- 1117 start : int, str, or datetime, optional 1118 Zero-indexed observation number at which to start forecasting, 1119 i.e., the first forecast is start. Can also be a date string to 1120 parse or a datetime type. Default is the the zeroth observation. 1121 end : int, str, or datetime, optional 1122 Zero-indexed observation number at which to end forecasting, i.e., 1123 the last forecast is end. Can also be a date string to 1124 parse or a datetime type. However, if the dates index does not 1125 have a fixed frequency, end must be an integer index if you 1126 want out-of-sample prediction. Default is the last observation in 1127 the sample. Unlike standard python slices, end is inclusive so 1128 that all the predictions [start, start+1, ..., end-1, end] are 1129 returned. 1130 dynamic : {bool, int, str, datetime, Timestamp}, optional 1131 Integer offset relative to `start` at which to begin dynamic 1132 prediction. Prior to this observation, true endogenous values 1133 will be used for prediction; starting with this observation and 1134 continuing through the end of prediction, forecasted endogenous 1135 values will be used instead. Datetime-like objects are not 1136 interpreted as offsets. They are instead used to find the index 1137 location of `dynamic` which is then used to to compute the offset. 1138 exog : array_like 1139 A replacement exogenous array. Must have the same shape as the 1140 exogenous data array used when the model was created. 1141 exog_oos : array_like 1142 An array containing out-of-sample values of the exogenous variable. 1143 Must has the same number of columns as the exog used when the 1144 model was created, and at least as many rows as the number of 1145 out-of-sample forecasts. 1146 fixed : array_like 1147 A replacement fixed array. Must have the same shape as the 1148 fixed data array used when the model was created. 1149 fixed_oos : array_like 1150 An array containing out-of-sample values of the fixed variables. 1151 Must have the same number of columns as the fixed used when the 1152 model was created, and at least as many rows as the number of 1153 out-of-sample forecasts. 1154 1155 Returns 1156 ------- 1157 PredictionResults 1158 Prediction results with mean and prediction intervals 1159 """ 1160 mean = self.predict( 1161 start=start, 1162 end=end, 1163 dynamic=dynamic, 1164 exog=exog, 1165 exog_oos=exog_oos, 1166 fixed=fixed, 1167 fixed_oos=fixed_oos, 1168 ) 1169 mean_var = np.full_like(mean, fill_value=self.sigma2) 1170 mean_var[np.isnan(mean)] = np.nan 1171 start = 0 if start is None else start 1172 end = self.model._index[-1] if end is None else end 1173 _, _, oos, _ = self.model._get_prediction_index(start, end) 1174 if oos > 0: 1175 ar_params = self._lag_repr() 1176 ma = arma2ma(ar_params, np.ones(1), lags=oos) 1177 mean_var[-oos:] = self.sigma2 * np.cumsum(ma ** 2) 1178 if isinstance(mean, pd.Series): 1179 mean_var = pd.Series(mean_var, index=mean.index) 1180 1181 return PredictionResults(mean, mean_var) 1182 1183 @Substitution(predict_params=_predict_params) 1184 def plot_predict( 1185 self, 1186 start: Union[None, int, str, dt.datetime, pd.Timestamp] = None, 1187 end: Union[None, int, str, dt.datetime, pd.Timestamp] = None, 1188 dynamic: bool = False, 1189 exog: Union[None, np.ndarray, pd.DataFrame] = None, 1190 exog_oos: Union[None, np.ndarray, pd.DataFrame] = None, 1191 fixed: Union[None, np.ndarray, pd.DataFrame] = None, 1192 fixed_oos: Union[None, np.ndarray, pd.DataFrame] = None, 1193 alpha: float = 0.05, 1194 in_sample: bool = True, 1195 fig: "matplotlib.figure.Figure" = None, 1196 figsize: Optional[Tuple[int, int]] = None, 1197 ) -> "matplotlib.figure.Figure": 1198 """ 1199 Plot in- and out-of-sample predictions 1200 1201 Parameters 1202 ----------\n%(predict_params)s 1203 alpha : {float, None} 1204 The tail probability not covered by the confidence interval. Must 1205 be in (0, 1). Confidence interval is constructed assuming normally 1206 distributed shocks. If None, figure will not show the confidence 1207 interval. 1208 in_sample : bool 1209 Flag indicating whether to include the in-sample period in the 1210 plot. 1211 fig : Figure 1212 An existing figure handle. If not provided, a new figure is 1213 created. 1214 figsize: tuple[float, float] 1215 Tuple containing the figure size values. 1216 1217 Returns 1218 ------- 1219 Figure 1220 Figure handle containing the plot. 1221 """ 1222 predictions = self.get_prediction( 1223 start=start, 1224 end=end, 1225 dynamic=dynamic, 1226 exog=exog, 1227 exog_oos=exog_oos, 1228 fixed=fixed, 1229 fixed_oos=fixed_oos, 1230 ) 1231 return self._plot_predictions( 1232 predictions, start, end, alpha, in_sample, fig, figsize 1233 ) 1234 1235 def summary(self, alpha: float = 0.05) -> Summary: 1236 """ 1237 Summarize the Model 1238 1239 Parameters 1240 ---------- 1241 alpha : float, optional 1242 Significance level for the confidence intervals. 1243 1244 Returns 1245 ------- 1246 smry : Summary instance 1247 This holds the summary table and text, which can be printed or 1248 converted to various output formats. 1249 1250 See Also 1251 -------- 1252 statsmodels.iolib.summary.Summary 1253 """ 1254 model = self.model 1255 1256 title = model.__class__.__name__ + " Model Results" 1257 method = "Conditional MLE" 1258 # get sample 1259 start = self._hold_back 1260 if self.data.dates is not None: 1261 dates = self.data.dates 1262 sample = [dates[start].strftime("%m-%d-%Y")] 1263 sample += ["- " + dates[-1].strftime("%m-%d-%Y")] 1264 else: 1265 sample = [str(start), str(len(self.data.orig_endog))] 1266 model = self.model.__class__.__name__ + str(self.model.ardl_order) 1267 if self.model.seasonal: 1268 model = "Seas. " + model 1269 1270 dep_name = str(self.model.endog_names) 1271 top_left = [ 1272 ("Dep. Variable:", [dep_name]), 1273 ("Model:", [model]), 1274 ("Method:", [method]), 1275 ("Date:", None), 1276 ("Time:", None), 1277 ("Sample:", [sample[0]]), 1278 ("", [sample[1]]), 1279 ] 1280 1281 top_right = [ 1282 ("No. Observations:", [str(len(self.model.endog))]), 1283 ("Log Likelihood", ["%#5.3f" % self.llf]), 1284 ("S.D. of innovations", ["%#5.3f" % self.sigma2 ** 0.5]), 1285 ("AIC", ["%#5.3f" % self.aic]), 1286 ("BIC", ["%#5.3f" % self.bic]), 1287 ("HQIC", ["%#5.3f" % self.hqic]), 1288 ] 1289 1290 smry = Summary() 1291 smry.add_table_2cols( 1292 self, gleft=top_left, gright=top_right, title=title 1293 ) 1294 smry.add_table_params(self, alpha=alpha, use_t=False) 1295 1296 return smry 1297 1298 1299class ARDLResultsWrapper(wrap.ResultsWrapper): 1300 _attrs = {} 1301 _wrap_attrs = wrap.union_dicts( 1302 tsa_model.TimeSeriesResultsWrapper._wrap_attrs, _attrs 1303 ) 1304 _methods = {} 1305 _wrap_methods = wrap.union_dicts( 1306 tsa_model.TimeSeriesResultsWrapper._wrap_methods, _methods 1307 ) 1308 1309 1310wrap.populate_wrapper(ARDLResultsWrapper, ARDLResults) 1311 1312 1313class ARDLOrderSelectionResults(AROrderSelectionResults): 1314 """ 1315 Results from an ARDL order selection 1316 1317 Contains the information criteria for all fitted model orders. 1318 """ 1319 1320 def __init__(self, model, ics, trend, seasonal, period): 1321 _ics = (((0,), (0, 0, 0)),) 1322 super().__init__(model, _ics, trend, seasonal, period) 1323 1324 def _to_dict(d): 1325 return d[0], dict(d[1:]) 1326 1327 self._aic = pd.Series( 1328 {v[0]: _to_dict(k) for k, v in ics.items()}, dtype=object 1329 ) 1330 self._aic.index.name = self._aic.name = "AIC" 1331 self._aic = self._aic.sort_index() 1332 1333 self._bic = pd.Series( 1334 {v[1]: _to_dict(k) for k, v in ics.items()}, dtype=object 1335 ) 1336 self._bic.index.name = self._bic.name = "BIC" 1337 self._bic = self._bic.sort_index() 1338 1339 self._hqic = pd.Series( 1340 {v[2]: _to_dict(k) for k, v in ics.items()}, dtype=object 1341 ) 1342 self._hqic.index.name = self._hqic.name = "HQIC" 1343 self._hqic = self._hqic.sort_index() 1344 1345 @property 1346 def dl_lags(self) -> Dict[Hashable, List[int]]: 1347 """The lags of exogenous variables in the selected model""" 1348 return self._model.dl_lags 1349 1350 1351def ardl_select_order( 1352 endog: Union[Sequence[float], pd.Series, _ArrayLike2D], 1353 maxlag: int, 1354 exog: _ArrayLike2D, 1355 maxorder: Union[int, Dict[Hashable, int]], 1356 trend: Literal["n", "c", "ct", "ctt"] = "c", 1357 *, 1358 fixed: Optional[_ArrayLike2D] = None, 1359 causal: bool = False, 1360 ic: Literal["aic", "bic"] = "bic", 1361 glob: bool = False, 1362 seasonal: bool = False, 1363 deterministic: Optional[DeterministicProcess] = None, 1364 hold_back: Optional[int] = None, 1365 period: Optional[int] = None, 1366 missing: Literal["none", "raise"] = "none", 1367) -> ARDLOrderSelectionResults: 1368 r""" 1369 ARDL order selection 1370 1371 Parameters 1372 ---------- 1373 endog : array_like 1374 A 1-d endogenous response variable. The dependent variable. 1375 maxlag : int 1376 The maximum lag to consider for the endogenous variable. 1377 exog : array_like 1378 Exogenous variables to include in the model. Either a DataFrame or 1379 an 2-d array-like structure that can be converted to a NumPy array. 1380 maxorder : {int, dict} 1381 If int, sets a common max lag length for all exog variables. If 1382 a dict, then sets individual lag length. They keys are column names 1383 if exog is a DataFrame or column indices otherwise. 1384 trend : {'n', 'c', 't', 'ct'}, optional 1385 The trend to include in the model: 1386 1387 * 'n' - No trend. 1388 * 'c' - Constant only. 1389 * 't' - Time trend only. 1390 * 'ct' - Constant and time trend. 1391 1392 The default is 'c'. 1393 fixed : array_like 1394 Additional fixed regressors that are not lagged. 1395 causal : bool, optional 1396 Whether to include lag 0 of exog variables. If True, only includes 1397 lags 1, 2, ... 1398 ic : {"aic", "bic", "hqic"} 1399 The information criterion to use in model selection. 1400 glob : bool 1401 Whether to consider all possible submodels of the largest model 1402 or only if smaller order lags must be included if larger order 1403 lags are. If ``True``, the number of model considered is of the 1404 order 2**(maxlag + k * maxorder) assuming maxorder is an int. This 1405 can be very large unless k and maxorder are bot relatively small. 1406 If False, the number of model considered is of the order 1407 maxlag*maxorder**k which may also be substantial when k and maxorder 1408 are large. 1409 seasonal : bool, optional 1410 Flag indicating whether to include seasonal dummies in the model. If 1411 seasonal is True and trend includes 'c', then the first period 1412 is excluded from the seasonal terms. 1413 deterministic : DeterministicProcess, optional 1414 A deterministic process. If provided, trend and seasonal are ignored. 1415 A warning is raised if trend is not "n" and seasonal is not False. 1416 hold_back : {None, int}, optional 1417 Initial observations to exclude from the estimation sample. If None, 1418 then hold_back is equal to the maximum lag in the model. Set to a 1419 non-zero value to produce comparable models with different lag 1420 length. For example, to compare the fit of a model with lags=3 and 1421 lags=1, set hold_back=3 which ensures that both models are estimated 1422 using observations 3,...,nobs. hold_back must be >= the maximum lag in 1423 the model. 1424 period : {None, int}, optional 1425 The period of the data. Only used if seasonal is True. This parameter 1426 can be omitted if using a pandas object for endog that contains a 1427 recognized frequency. 1428 missing : {"none", "drop", "raise"}, optional 1429 Available options are 'none', 'drop', and 'raise'. If 'none', no nan 1430 checking is done. If 'drop', any observations with nans are dropped. 1431 If 'raise', an error is raised. Default is 'none'. 1432 1433 Returns 1434 ------- 1435 ARDLSelectionResults 1436 A results holder containing the selected model and the complete set 1437 of information criteria for all models fit. 1438 """ 1439 orig_hold_back = int_like(hold_back, "hold_back", optional=True) 1440 1441 def compute_ics(y, x, df): 1442 if x.shape[1]: 1443 resid = y - x @ np.linalg.lstsq(x, y, rcond=None)[0] 1444 else: 1445 resid = y 1446 nobs = resid.shape[0] 1447 sigma2 = 1.0 / nobs * sumofsq(resid) 1448 llf = -nobs * (np.log(2 * np.pi * sigma2) + 1) / 2 1449 res = SimpleNamespace( 1450 nobs=nobs, df_model=df + x.shape[1], sigma2=sigma2, llf=llf 1451 ) 1452 1453 aic = call_cached_func(ARDLResults.aic, res) 1454 bic = call_cached_func(ARDLResults.bic, res) 1455 hqic = call_cached_func(ARDLResults.hqic, res) 1456 1457 return aic, bic, hqic 1458 1459 base = ARDL( 1460 endog, 1461 maxlag, 1462 exog, 1463 maxorder, 1464 trend, 1465 fixed=fixed, 1466 causal=causal, 1467 seasonal=seasonal, 1468 deterministic=deterministic, 1469 hold_back=hold_back, 1470 period=period, 1471 missing=missing, 1472 ) 1473 hold_back = base.hold_back 1474 blocks = base._blocks 1475 always = np.column_stack([blocks["deterministic"], blocks["fixed"]]) 1476 always = always[hold_back:] 1477 select = [] 1478 iter_orders = [] 1479 select.append(blocks["endog"][hold_back:]) 1480 iter_orders.append(list(range(blocks["endog"].shape[1] + 1))) 1481 var_names = [] 1482 for var in blocks["exog"]: 1483 block = blocks["exog"][var][hold_back:] 1484 select.append(block) 1485 iter_orders.append(list(range(block.shape[1] + 1))) 1486 var_names.append(var) 1487 y = base._y 1488 if always.shape[1]: 1489 pinv_always = np.linalg.pinv(always) 1490 for i in range(len(select)): 1491 x = select[i] 1492 select[i] = x - always @ (pinv_always @ x) 1493 y = y - always @ (pinv_always @ y) 1494 1495 def perm_to_tuple(keys, perm): 1496 if perm == (): 1497 d = {k: 0 for k, _ in keys if k is not None} 1498 return (0,) + tuple((k, v) for k, v in d.items()) 1499 d = defaultdict(list) 1500 y_lags = [] 1501 for v in perm: 1502 key = keys[v] 1503 if key[0] is None: 1504 y_lags.append(key[1]) 1505 else: 1506 d[key[0]].append(key[1]) 1507 d = dict(d) 1508 if not y_lags or y_lags == [0]: 1509 y_lags = 0 1510 else: 1511 y_lags = tuple(y_lags) 1512 for key in keys: 1513 if key[0] not in d and key[0] is not None: 1514 d[key[0]] = None 1515 for key in d: 1516 if d[key] is not None: 1517 d[key] = tuple(d[key]) 1518 return (y_lags,) + tuple((k, v) for k, v in d.items()) 1519 1520 always_df = always.shape[1] 1521 ics = {} 1522 if glob: 1523 ar_lags = base.ar_lags if base.ar_lags is not None else [] 1524 keys = [(None, i) for i in ar_lags] 1525 for k, v in base._order.items(): 1526 keys += [(k, i) for i in v] 1527 x = np.column_stack([a for a in select]) 1528 all_columns = list(range(x.shape[1])) 1529 for i in range(x.shape[1]): 1530 for perm in combinations(all_columns, i): 1531 key = perm_to_tuple(keys, perm) 1532 ics[key] = compute_ics(y, x[:, perm], always_df) 1533 else: 1534 for io in product(*iter_orders): 1535 x = np.column_stack([a[:, : io[i]] for i, a in enumerate(select)]) 1536 key = [io[0] if io[0] else None] 1537 for j, val in enumerate(io[1:]): 1538 var = var_names[j] 1539 if causal: 1540 key.append((var, None if val == 0 else val)) 1541 else: 1542 key.append((var, val - 1 if val - 1 >= 0 else None)) 1543 key = tuple(key) 1544 ics[key] = compute_ics(y, x, always_df) 1545 index = {"aic": 0, "bic": 1, "hqic": 2}[ic] 1546 lowest = np.inf 1547 for key in ics: 1548 val = ics[key][index] 1549 if val < lowest: 1550 lowest = val 1551 selected_order = key 1552 exog_order = {k: v for k, v in selected_order[1:]} 1553 model = ARDL( 1554 endog, 1555 selected_order[0], 1556 exog, 1557 exog_order, 1558 trend, 1559 fixed=fixed, 1560 causal=causal, 1561 seasonal=seasonal, 1562 deterministic=deterministic, 1563 hold_back=orig_hold_back, 1564 period=period, 1565 missing=missing, 1566 ) 1567 1568 return ARDLOrderSelectionResults(model, ics, trend, seasonal, period) 1569 1570 1571lags_descr = textwrap.wrap( 1572 "The number of lags of the endogenous variable to include in the model. " 1573 "Must be at least 1.", 1574 71, 1575) 1576lags_param = Parameter(name="lags", type="int", desc=lags_descr) 1577order_descr = textwrap.wrap( 1578 "If int, uses lags 0, 1, ..., order for all exog variables. If a dict, " 1579 "applies the lags series by series. If ``exog`` is anything other than a " 1580 "DataFrame, the keys are the column index of exog (e.g., 0, 1, ...). If " 1581 "a DataFrame, keys are column names.", 1582 71, 1583) 1584order_param = Parameter(name="order", type="int, dict", desc=order_descr) 1585 1586from_formula_doc = Docstring(ARDL.from_formula.__doc__) 1587from_formula_doc.replace_block("Summary", "Construct an UECM from a formula") 1588from_formula_doc.remove_parameters("lags") 1589from_formula_doc.remove_parameters("order") 1590from_formula_doc.insert_parameters("data", lags_param) 1591from_formula_doc.insert_parameters("lags", order_param) 1592 1593 1594fit_doc = Docstring(ARDL.fit.__doc__) 1595fit_doc.replace_block( 1596 "Returns", [Parameter("", "UECMResults", ["Estimation results."])] 1597) 1598 1599if fit_doc._ds is not None: 1600 see_also = fit_doc._ds["See Also"] 1601 see_also.insert( 1602 0, 1603 ( 1604 [("statsmodels.tsa.ardl.ARDL", None)], 1605 ["Autoregressive distributed lag model estimation"], 1606 ), 1607 ) 1608 fit_doc.replace_block("See Also", see_also) 1609 1610 1611class UECM(ARDL): 1612 r""" 1613 Unconstrained Error Correlation Model(UECM) 1614 1615 Parameters 1616 ---------- 1617 endog : array_like 1618 A 1-d endogenous response variable. The dependent variable. 1619 lags : {int, list[int]} 1620 The number of lags of the endogenous variable to include in the 1621 model. Must be at least 1. 1622 exog : array_like 1623 Exogenous variables to include in the model. Either a DataFrame or 1624 an 2-d array-like structure that can be converted to a NumPy array. 1625 order : {int, sequence[int], dict} 1626 If int, uses lags 0, 1, ..., order for all exog variables. If a 1627 dict, applies the lags series by series. If ``exog`` is anything 1628 other than a DataFrame, the keys are the column index of exog 1629 (e.g., 0, 1, ...). If a DataFrame, keys are column names. 1630 fixed : array_like 1631 Additional fixed regressors that are not lagged. 1632 causal : bool, optional 1633 Whether to include lag 0 of exog variables. If True, only includes 1634 lags 1, 2, ... 1635 trend : {'n', 'c', 't', 'ct'}, optional 1636 The trend to include in the model: 1637 1638 * 'n' - No trend. 1639 * 'c' - Constant only. 1640 * 't' - Time trend only. 1641 * 'ct' - Constant and time trend. 1642 1643 The default is 'c'. 1644 1645 seasonal : bool, optional 1646 Flag indicating whether to include seasonal dummies in the model. If 1647 seasonal is True and trend includes 'c', then the first period 1648 is excluded from the seasonal terms. 1649 deterministic : DeterministicProcess, optional 1650 A deterministic process. If provided, trend and seasonal are ignored. 1651 A warning is raised if trend is not "n" and seasonal is not False. 1652 hold_back : {None, int}, optional 1653 Initial observations to exclude from the estimation sample. If None, 1654 then hold_back is equal to the maximum lag in the model. Set to a 1655 non-zero value to produce comparable models with different lag 1656 length. For example, to compare the fit of a model with lags=3 and 1657 lags=1, set hold_back=3 which ensures that both models are estimated 1658 using observations 3,...,nobs. hold_back must be >= the maximum lag in 1659 the model. 1660 period : {None, int}, optional 1661 The period of the data. Only used if seasonal is True. This parameter 1662 can be omitted if using a pandas object for endog that contains a 1663 recognized frequency. 1664 missing : {"none", "drop", "raise"}, optional 1665 Available options are 'none', 'drop', and 'raise'. If 'none', no nan 1666 checking is done. If 'drop', any observations with nans are dropped. 1667 If 'raise', an error is raised. Default is 'none'. 1668 1669 Notes 1670 ----- 1671 The full specification of an UECM is 1672 1673 .. math :: 1674 1675 \Delta Y_t = \delta_0 + \delta_1 t + \delta_2 t^2 1676 + \sum_{i=1}^{s-1} \gamma_i I_{[(\mod(t,s) + 1) = i]} 1677 + \lambda_0 Y_{t-1} + \lambda_1 X_{1,t-1} + \ldots 1678 + \lambda_{k} X_{k,t-1} 1679 + \sum_{j=1}^{p-1} \phi_j \Delta Y_{t-j} 1680 + \sum_{l=1}^k \sum_{m=0}^{o_l-1} \beta_{l,m} \Delta X_{l, t-m} 1681 + Z_t \lambda 1682 + \epsilon_t 1683 1684 where :math:`\delta_\bullet` capture trends, :math:`\gamma_\bullet` 1685 capture seasonal shifts, s is the period of the seasonality, p is the 1686 lag length of the endogenous variable, k is the number of exogenous 1687 variables :math:`X_{l}`, :math:`o_l` is included the lag length of 1688 :math:`X_{l}`, :math:`Z_t` are ``r`` included fixed regressors and 1689 :math:`\epsilon_t` is a white noise shock. If ``causal`` is ``True``, 1690 then the 0-th lag of the exogenous variables is not included and the 1691 sum starts at ``m=1``. 1692 1693 See Also 1694 -------- 1695 statsmodels.tsa.ardl.ARDL 1696 Autoregressive distributed lag model estimation 1697 statsmodels.tsa.ar_model.AutoReg 1698 Autoregressive model estimation with optional exogenous regressors 1699 statsmodels.tsa.statespace.sarimax.SARIMAX 1700 Seasonal ARIMA model estimation with optional exogenous regressors 1701 statsmodels.tsa.arima.model.ARIMA 1702 ARIMA model estimation 1703 1704 Examples 1705 -------- 1706 >>> from statsmodels.tsa.api import UECM 1707 >>> from statsmodels.datasets import danish_data 1708 >>> data = danish_data.load_pandas().data 1709 >>> lrm = data.lrm 1710 >>> exog = data[["lry", "ibo", "ide"]] 1711 1712 A basic model where all variables have 3 lags included 1713 1714 >>> UECM(data.lrm, 3, data[["lry", "ibo", "ide"]], 3) 1715 1716 A dictionary can be used to pass custom lag orders 1717 1718 >>> UECM(data.lrm, [1, 3], exog, {"lry": 1, "ibo": 3, "ide": 2}) 1719 1720 Setting causal removes the 0-th lag from the exogenous variables 1721 1722 >>> exog_lags = {"lry": 1, "ibo": 3, "ide": 2} 1723 >>> UECM(data.lrm, 3, exog, exog_lags, causal=True) 1724 1725 When using NumPy arrays, the dictionary keys are the column index. 1726 1727 >>> import numpy as np 1728 >>> lrma = np.asarray(lrm) 1729 >>> exoga = np.asarray(exog) 1730 >>> UECM(lrma, 3, exoga, {0: 1, 1: 3, 2: 2}) 1731 """ 1732 1733 def __init__( 1734 self, 1735 endog: Union[Sequence[float], pd.Series, _ArrayLike2D], 1736 lags: Union[None, int], 1737 exog: Optional[_ArrayLike2D] = None, 1738 order: _UECMOrder = 0, 1739 trend: Literal["n", "c", "ct", "ctt"] = "c", 1740 *, 1741 fixed: Optional[_ArrayLike2D] = None, 1742 causal: bool = False, 1743 seasonal: bool = False, 1744 deterministic: Optional[DeterministicProcess] = None, 1745 hold_back: Optional[int] = None, 1746 period: Optional[int] = None, 1747 missing: Literal["none", "drop", "raise"] = "none", 1748 ) -> None: 1749 super().__init__( 1750 endog, 1751 lags, 1752 exog, 1753 order, 1754 trend=trend, 1755 fixed=fixed, 1756 seasonal=seasonal, 1757 causal=causal, 1758 hold_back=hold_back, 1759 period=period, 1760 missing=missing, 1761 deterministic=deterministic, 1762 ) 1763 self._results_class = UECMResults 1764 self._results_wrapper = UECMResultsWrapper 1765 1766 def _check_lags(self) -> Tuple[List[int], int]: 1767 """Check lags value conforms to requirement""" 1768 if not (isinstance(self._lags, _INT_TYPES) or self._lags is None): 1769 raise TypeError("lags must be an integer or None") 1770 return super()._check_lags() 1771 1772 def _check_order(self, order: _ARDLOrder): 1773 """Check order conforms to requirement""" 1774 if isinstance(order, Mapping): 1775 for k, v in order.items(): 1776 if not isinstance(v, _INT_TYPES) and v is not None: 1777 raise TypeError( 1778 "order values must be positive integers or None" 1779 ) 1780 elif not (isinstance(order, _INT_TYPES) or order is None): 1781 raise TypeError( 1782 "order must be None, a positive integer, or a dict " 1783 "containing positive integers or None" 1784 ) 1785 # TODO: Check order is >= 1 1786 order = super()._check_order(order) 1787 if not order: 1788 raise ValueError( 1789 "Model must contain at least one exogenous variable" 1790 ) 1791 for key, val in order.items(): 1792 if val == [0]: 1793 raise ValueError( 1794 "All included exog variables must have a lag length >= 1" 1795 ) 1796 return order 1797 1798 def _construct_variable_names(self): 1799 """Construct model variables names""" 1800 endog = self.data.orig_endog 1801 if isinstance(endog, pd.Series): 1802 y_base = endog.name or "y" 1803 elif isinstance(endog, pd.DataFrame): 1804 y_base = endog.squeeze().name or "y" 1805 else: 1806 y_base = "y" 1807 y_name = f"D.{y_base}" 1808 # 1. Deterministics 1809 x_names = list(self._deterministic_reg.columns) 1810 # 2. Levels 1811 x_names.append(f"{y_base}.L1") 1812 orig_exog = self.data.orig_exog 1813 exog_pandas = isinstance(orig_exog, pd.DataFrame) 1814 dexog_names = [] 1815 for key, val in self._order.items(): 1816 if val is not None: 1817 if exog_pandas: 1818 x_name = f"{key}.L1" 1819 else: 1820 x_name = f"x{key}.L1" 1821 x_names.append(x_name) 1822 lag_base = x_name[:-1] 1823 for lag in val[:-1]: 1824 dexog_names.append(f"D.{lag_base}{lag}") 1825 # 3. Lagged endog 1826 y_lags = max(self._lags) if self._lags else 0 1827 dendog_names = [f"{y_name}.L{lag}" for lag in range(1, y_lags)] 1828 x_names.extend(dendog_names) 1829 x_names.extend(dexog_names) 1830 x_names.extend(self._fixed_names) 1831 return y_name, x_names 1832 1833 def _construct_regressors( 1834 self, hold_back: Optional[int] 1835 ) -> Tuple[np.ndarray, np.ndarray]: 1836 """Construct and format model regressors""" 1837 # 1. Endogenous and endogenous lags 1838 self._maxlag = max(self._lags) if self._lags else 0 1839 dendog = np.full_like(self.data.endog, np.nan) 1840 dendog[1:] = np.diff(self.data.endog, axis=0) 1841 dlag = max(0, self._maxlag - 1) 1842 self._endog_reg, self._endog = lagmat(dendog, dlag, original="sep") 1843 # 2. Deterministics 1844 self._deterministic_reg = self._deterministics.in_sample() 1845 # 3. Levels 1846 orig_exog = self.data.orig_exog 1847 exog_pandas = isinstance(orig_exog, pd.DataFrame) 1848 lvl = np.full_like(self.data.endog, np.nan) 1849 lvl[1:] = self.data.endog[:-1] 1850 lvls = [lvl.copy()] 1851 for key, val in self._order.items(): 1852 if val is not None: 1853 if exog_pandas: 1854 loc = orig_exog.columns.get_loc(key) 1855 else: 1856 loc = key 1857 lvl[1:] = self.data.exog[:-1, loc] 1858 lvls.append(lvl.copy()) 1859 self._levels = np.column_stack(lvls) 1860 1861 # 4. exog Lags 1862 if exog_pandas: 1863 dexog = orig_exog.diff() 1864 else: 1865 dexog = np.full_like(self.data.exog, np.nan) 1866 dexog[1:] = np.diff(orig_exog, axis=0) 1867 adj_order = {} 1868 for key, val in self._order.items(): 1869 val = None if (val is None or val == [1]) else val[:-1] 1870 adj_order[key] = val 1871 self._exog = self._format_exog(dexog, adj_order) 1872 1873 self._blocks = { 1874 "deterministic": self._deterministic_reg, 1875 "levels": self._levels, 1876 "endog": self._endog_reg, 1877 "exog": self._exog, 1878 "fixed": self._fixed, 1879 } 1880 blocks = [self._endog] 1881 for key, val in self._blocks.items(): 1882 if key != "exog": 1883 blocks.append(np.asarray(val)) 1884 else: 1885 for subval in val.values(): 1886 blocks.append(np.asarray(subval)) 1887 y = blocks[0] 1888 reg = np.column_stack(blocks[1:]) 1889 exog_maxlag = 0 1890 for val in self._order.values(): 1891 exog_maxlag = max(exog_maxlag, max(val) if val is not None else 0) 1892 self._maxlag = max(self._maxlag, exog_maxlag) 1893 # Must be at least 1 since the endog is differenced 1894 self._maxlag = max(self._maxlag, 1) 1895 if hold_back is None: 1896 self._hold_back = int(self._maxlag) 1897 if self._hold_back < self._maxlag: 1898 raise ValueError( 1899 "hold_back must be >= the maximum lag of the endog and exog " 1900 "variables" 1901 ) 1902 reg = reg[self._hold_back :] 1903 if reg.shape[1] > reg.shape[0]: 1904 raise ValueError( 1905 f"The number of regressors ({reg.shape[1]}) including " 1906 "deterministics, lags of the endog, lags of the exogenous, " 1907 "and fixed regressors is larer than the sample available " 1908 f"for estimation ({reg.shape[0]})." 1909 ) 1910 return np.squeeze(y)[self._hold_back :], reg 1911 1912 @Appender(str(fit_doc)) 1913 def fit( 1914 self, 1915 *, 1916 cov_type: str = "nonrobust", 1917 cov_kwds: Dict[str, Any] = None, 1918 use_t: bool = True, 1919 ) -> UECMResults: 1920 params, cov_params, norm_cov_params = self._fit( 1921 cov_type=cov_type, cov_kwds=cov_kwds, use_t=use_t 1922 ) 1923 res = UECMResults( 1924 self, params, cov_params, norm_cov_params, use_t=use_t 1925 ) 1926 return UECMResultsWrapper(res) 1927 1928 @classmethod 1929 def from_ardl( 1930 cls, ardl: ARDL, missing: Literal["none", "drop", "raise"] = "none" 1931 ): 1932 """ 1933 Construct a UECM from an ARDL model 1934 1935 Parameters 1936 ---------- 1937 ardl : ARDL 1938 The ARDL model instance 1939 missing : {"none", "drop", "raise"}, default "none" 1940 How to treat missing observations. 1941 1942 Returns 1943 ------- 1944 UECM 1945 The UECM model instance 1946 1947 Notes 1948 ----- 1949 The lag requirements for a UECM are stricter than for an ARDL. 1950 Any variable that is included in the UECM must have a lag length 1951 of at least 1. Additionally, the included lags must be contiguous 1952 starting at 0 if non-causal or 1 if causal. 1953 """ 1954 err = ( 1955 "UECM can only be created from ARDL models that include all " 1956 "{var_typ} lags up to the maximum lag in the model." 1957 ) 1958 uecm_lags = {} 1959 dl_lags = ardl.dl_lags 1960 for key, val in dl_lags.items(): 1961 max_val = max(val) 1962 if len(dl_lags[key]) < (max_val + int(not ardl.causal)): 1963 raise ValueError(err.format(var_typ="exogenous")) 1964 uecm_lags[key] = max_val 1965 if ardl.ar_lags is None: 1966 ar_lags = None 1967 else: 1968 max_val = max(ardl.ar_lags) 1969 if len(ardl.ar_lags) != max_val: 1970 raise ValueError(err.format(var_typ="endogenous")) 1971 ar_lags = max_val 1972 1973 return cls( 1974 ardl.data.orig_endog, 1975 ar_lags, 1976 ardl.data.orig_exog, 1977 uecm_lags, 1978 trend=ardl.trend, 1979 fixed=ardl.fixed, 1980 seasonal=ardl.seasonal, 1981 hold_back=ardl.hold_back, 1982 period=ardl.period, 1983 causal=ardl.causal, 1984 missing=missing, 1985 deterministic=ardl.deterministic, 1986 ) 1987 1988 def predict( 1989 self, 1990 params: Union[np.ndarray, pd.DataFrame], 1991 start: Union[None, int, str, dt.datetime, pd.Timestamp] = None, 1992 end: Union[None, int, str, dt.datetime, pd.Timestamp] = None, 1993 dynamic: bool = False, 1994 exog: Union[None, np.ndarray, pd.DataFrame] = None, 1995 exog_oos: Union[None, np.ndarray, pd.DataFrame] = None, 1996 fixed: Union[None, np.ndarray, pd.DataFrame] = None, 1997 fixed_oos: Union[None, np.ndarray, pd.DataFrame] = None, 1998 ) -> np.ndarray: 1999 """ 2000 In-sample prediction and out-of-sample forecasting. 2001 2002 Parameters 2003 ---------- 2004 params : array_like 2005 The fitted model parameters. 2006 start : int, str, or datetime, optional 2007 Zero-indexed observation number at which to start forecasting, 2008 i.e., the first forecast is start. Can also be a date string to 2009 parse or a datetime type. Default is the the zeroth observation. 2010 end : int, str, or datetime, optional 2011 Zero-indexed observation number at which to end forecasting, i.e., 2012 the last forecast is end. Can also be a date string to 2013 parse or a datetime type. However, if the dates index does not 2014 have a fixed frequency, end must be an integer index if you 2015 want out-of-sample prediction. Default is the last observation in 2016 the sample. Unlike standard python slices, end is inclusive so 2017 that all the predictions [start, start+1, ..., end-1, end] are 2018 returned. 2019 dynamic : {bool, int, str, datetime, Timestamp}, optional 2020 Integer offset relative to `start` at which to begin dynamic 2021 prediction. Prior to this observation, true endogenous values 2022 will be used for prediction; starting with this observation and 2023 continuing through the end of prediction, forecasted endogenous 2024 values will be used instead. Datetime-like objects are not 2025 interpreted as offsets. They are instead used to find the index 2026 location of `dynamic` which is then used to to compute the offset. 2027 exog : array_like 2028 A replacement exogenous array. Must have the same shape as the 2029 exogenous data array used when the model was created. 2030 exog_oos : array_like 2031 An array containing out-of-sample values of the exogenous 2032 variables. Must have the same number of columns as the exog 2033 used when the model was created, and at least as many rows as 2034 the number of out-of-sample forecasts. 2035 fixed : array_like 2036 A replacement fixed array. Must have the same shape as the 2037 fixed data array used when the model was created. 2038 fixed_oos : array_like 2039 An array containing out-of-sample values of the fixed variables. 2040 Must have the same number of columns as the fixed used when the 2041 model was created, and at least as many rows as the number of 2042 out-of-sample forecasts. 2043 2044 Returns 2045 ------- 2046 predictions : {ndarray, Series} 2047 Array of out of in-sample predictions and / or out-of-sample 2048 forecasts. 2049 """ 2050 if dynamic is not False: 2051 raise NotImplementedError("dynamic forecasts are not supported") 2052 params, exog, exog_oos, start, end, num_oos = self._prepare_prediction( 2053 params, exog, exog_oos, start, end 2054 ) 2055 if num_oos != 0: 2056 raise NotImplementedError( 2057 "Out-of-sample forecasts are not supported" 2058 ) 2059 pred = np.full(self.endog.shape[0], np.nan) 2060 pred[-self._x.shape[0] :] = self._x @ params 2061 return pred[start : end + 1] 2062 2063 @classmethod 2064 @Appender(from_formula_doc.__str__().replace("ARDL", "UECM")) 2065 def from_formula( 2066 cls, 2067 formula: str, 2068 data: pd.DataFrame, 2069 lags: Union[None, int, Sequence[int]] = 0, 2070 order: _ARDLOrder = 0, 2071 trend: Literal["n", "c", "ct", "ctt"] = "n", 2072 *, 2073 causal: bool = False, 2074 seasonal: bool = False, 2075 deterministic: Optional[DeterministicProcess] = None, 2076 hold_back: Optional[int] = None, 2077 period: Optional[int] = None, 2078 missing: Literal["none", "raise"] = "none", 2079 ) -> UECM: 2080 return super().from_formula( 2081 formula, 2082 data, 2083 lags, 2084 order, 2085 trend, 2086 causal=causal, 2087 seasonal=seasonal, 2088 deterministic=deterministic, 2089 hold_back=hold_back, 2090 period=period, 2091 missing=missing, 2092 ) 2093 2094 2095class UECMResults(ARDLResults): 2096 """ 2097 Class to hold results from fitting an UECM model. 2098 2099 Parameters 2100 ---------- 2101 model : UECM 2102 Reference to the model that is fit. 2103 params : ndarray 2104 The fitted parameters from the AR Model. 2105 cov_params : ndarray 2106 The estimated covariance matrix of the model parameters. 2107 normalized_cov_params : ndarray 2108 The array inv(dot(x.T,x)) where x contains the regressors in the 2109 model. 2110 scale : float, optional 2111 An estimate of the scale of the model. 2112 """ 2113 2114 _cache = {} # for scale setter 2115 2116 def _ci_wrap( 2117 self, val: np.ndarray, name: str = "" 2118 ) -> Union[np.ndarray, pd.Series, pd.DataFrame]: 2119 if not isinstance(self.model.data, PandasData): 2120 return val 2121 ndet = self.model._blocks["deterministic"].shape[1] 2122 nlvl = self.model._blocks["levels"].shape[1] 2123 lbls = self.model.exog_names[: (ndet + nlvl)] 2124 for i in range(ndet, ndet + nlvl): 2125 lbl = lbls[i] 2126 if lbl.endswith(".L1"): 2127 lbls[i] = lbl[:-3] 2128 if val.ndim == 2: 2129 return pd.DataFrame(val, columns=lbls, index=lbls) 2130 return pd.Series(val, index=lbls, name=name) 2131 2132 @cache_readonly 2133 def ci_params(self) -> Union[np.ndarray, pd.Series]: 2134 """Parameters of normalized cointegrating relationship""" 2135 ndet = self.model._blocks["deterministic"].shape[1] 2136 nlvl = self.model._blocks["levels"].shape[1] 2137 base = np.asarray(self.params)[ndet] 2138 return self._ci_wrap(self.params[: ndet + nlvl] / base, "ci_params") 2139 2140 @cache_readonly 2141 def ci_bse(self) -> Union[np.ndarray, pd.Series]: 2142 """Standard Errors of normalized cointegrating relationship""" 2143 bse = np.sqrt(np.diag(self.ci_cov_params())) 2144 return self._ci_wrap(bse, "ci_bse") 2145 2146 @cache_readonly 2147 def ci_tvalues(self) -> Union[np.ndarray, pd.Series]: 2148 """T-values of normalized cointegrating relationship""" 2149 ndet = self.model._blocks["deterministic"].shape[1] 2150 with warnings.catch_warnings(): 2151 warnings.simplefilter("ignore") 2152 tvalues = np.asarray(self.ci_params) / np.asarray(self.ci_bse) 2153 tvalues[ndet] = np.nan 2154 return self._ci_wrap(tvalues, "ci_tvalues") 2155 2156 @cache_readonly 2157 def ci_pvalues(self) -> Union[np.ndarray, pd.Series]: 2158 """P-values of normalized cointegrating relationship""" 2159 with warnings.catch_warnings(): 2160 warnings.simplefilter("ignore") 2161 pvalues = 2 * (1 - stats.norm.cdf(np.abs(self.ci_tvalues))) 2162 return self._ci_wrap(pvalues, "ci_pvalues") 2163 2164 def ci_conf_int( 2165 self, alpha: float = 0.05 2166 ) -> Union[np.ndarray, pd.DataFrame]: 2167 alpha = float_like(alpha, "alpha") 2168 2169 if self.use_t: 2170 q = stats.t(self.df_resid).ppf(1 - alpha / 2) 2171 else: 2172 q = stats.norm().ppf(1 - alpha / 2) 2173 p = self.ci_params 2174 se = self.ci_bse 2175 out = [p - q * se, p + q * se] 2176 if not isinstance(p, pd.Series): 2177 return np.column_stack(out) 2178 2179 df = pd.concat(out, axis=1) 2180 df.columns = ["lower", "upper"] 2181 2182 return df 2183 2184 def ci_summary(self, alpha: float = 0.05) -> Summary: 2185 def _ci(alpha=alpha): 2186 return np.asarray(self.ci_conf_int(alpha)) 2187 2188 smry = Summary() 2189 ndet = self.model._blocks["deterministic"].shape[1] 2190 nlvl = self.model._blocks["levels"].shape[1] 2191 exog_names = list(self.model.exog_names)[: (ndet + nlvl)] 2192 2193 model = SimpleNamespace( 2194 endog_names=self.model.endog_names, exog_names=exog_names 2195 ) 2196 data = SimpleNamespace( 2197 params=self.ci_params, 2198 bse=self.ci_bse, 2199 tvalues=self.ci_tvalues, 2200 pvalues=self.ci_pvalues, 2201 conf_int=_ci, 2202 model=model, 2203 ) 2204 tab = summary_params(data) 2205 tab.title = "Cointegrating Vector" 2206 smry.tables.append(tab) 2207 2208 return smry 2209 2210 @cache_readonly 2211 def ci_resids(self) -> Union[np.ndarray, pd.Series]: 2212 d = self.model._blocks["deterministic"] 2213 exog = self.model.data.orig_exog 2214 is_pandas = isinstance(exog, pd.DataFrame) 2215 exog = exog if is_pandas else self.model.exog 2216 cols = [np.asarray(d), self.model.endog] 2217 for key, value in self.model.dl_lags.items(): 2218 if value is not None: 2219 if is_pandas: 2220 cols.append(np.asarray(exog[key])) 2221 else: 2222 cols.append(exog[:, key]) 2223 ci_x = np.column_stack(cols) 2224 resids = ci_x @ self.ci_params 2225 if not isinstance(self.model.data, PandasData): 2226 return resids 2227 index = self.model.data.orig_endog.index 2228 return pd.Series(resids, index=index, name="ci_resids") 2229 2230 def ci_cov_params(self) -> Union[np.ndarray, pd.DataFrame]: 2231 """Covariance of normalized of cointegrating relationship""" 2232 ndet = self.model._blocks["deterministic"].shape[1] 2233 nlvl = self.model._blocks["levels"].shape[1] 2234 loc = list(range(ndet + nlvl)) 2235 cov = self.cov_params() 2236 cov_a = np.asarray(cov) 2237 ci_cov = cov_a[np.ix_(loc, loc)] 2238 m = ci_cov.shape[0] 2239 params = np.asarray(self.params)[: ndet + nlvl] 2240 base = params[ndet] 2241 d = np.zeros((m, m)) 2242 for i in range(m): 2243 if i == ndet: 2244 continue 2245 d[i, i] = 1 / base 2246 d[i, ndet] = -params[i] / (base ** 2) 2247 ci_cov = d @ ci_cov @ d.T 2248 return self._ci_wrap(ci_cov) 2249 2250 def _lag_repr(self): 2251 """Returns poly repr of an AR, (1 -phi1 L -phi2 L^2-...)""" 2252 # TODO 2253 2254 def bounds_test( 2255 self, 2256 case: Literal[1, 2, 3, 4, 5], 2257 cov_type: str = "nonrobust", 2258 cov_kwds: Dict[str, Any] = None, 2259 use_t: bool = True, 2260 asymptotic: bool = True, 2261 nsim: int = 100_000, 2262 seed: Optional[ 2263 int, Sequence[int], np.random.RandomState, np.random.Generator 2264 ] = None, 2265 ): 2266 r""" 2267 Cointegration bounds test of Pesaran, Shin, and Smith 2268 2269 Parameters 2270 ---------- 2271 case : {1, 2, 3, 4, 5} 2272 One of the cases covered in the PSS test. 2273 cov_type : str 2274 The covariance estimator to use. The asymptotic distribution of 2275 the PSS test has only been established in the homoskedastic case, 2276 which is the default. 2277 2278 The most common choices are listed below. Supports all covariance 2279 estimators that are available in ``OLS.fit``. 2280 2281 * 'nonrobust' - The class OLS covariance estimator that assumes 2282 homoskedasticity. 2283 * 'HC0', 'HC1', 'HC2', 'HC3' - Variants of White's 2284 (or Eiker-Huber-White) covariance estimator. `HC0` is the 2285 standard implementation. The other make corrections to improve 2286 the finite sample performance of the heteroskedasticity robust 2287 covariance estimator. 2288 * 'HAC' - Heteroskedasticity-autocorrelation robust covariance 2289 estimation. Supports cov_kwds. 2290 2291 - `maxlags` integer (required) : number of lags to use. 2292 - `kernel` callable or str (optional) : kernel 2293 currently available kernels are ['bartlett', 'uniform'], 2294 default is Bartlett. 2295 - `use_correction` bool (optional) : If true, use small sample 2296 correction. 2297 cov_kwds : dict, optional 2298 A dictionary of keyword arguments to pass to the covariance 2299 estimator. `nonrobust` and `HC#` do not support cov_kwds. 2300 use_t : bool, optional 2301 A flag indicating that small-sample corrections should be applied 2302 to the covariance estimator. 2303 asymptotic : bool 2304 Flag indicating whether to use asymptotic critical values which 2305 were computed by simulation (True, default) or to simulate a 2306 sample-size specific set of critical values. Tables are only 2307 available for up to 10 components in the cointegrating 2308 relationship, so if more variables are included then simulation 2309 is always used. The simulation computed the test statistic under 2310 and assumption that the residuals are homoskedastic. 2311 nsim : int 2312 Number of simulations to run when computing exact critical values. 2313 Only used if ``asymptotic`` is ``True``. 2314 seed : {None, int, sequence[int], RandomState, Generator}, optional 2315 Seed to use when simulating critical values. Must be provided if 2316 reproducible critical value and p-values are required when 2317 ``asymptotic`` is ``False``. 2318 2319 Returns 2320 ------- 2321 BoundsTestResult 2322 Named tuple containing ``stat``, ``crit_vals``, ``p_values``, 2323 ``null` and ``alternative``. The statistic is the F-type 2324 test statistic favored in PSS. 2325 2326 Notes 2327 ----- 2328 The PSS bounds test has 5 cases which test the coefficients on the 2329 level terms in the model 2330 2331 .. math:: 2332 2333 \Delta Y_{t}=\delta_{0} + \delta_{1}t + Z_{t-1}\beta 2334 + \sum_{j=0}^{P}\Delta X_{t-j}\Gamma + \epsilon_{t} 2335 2336 where :math:`Z_{t-1}` contains both :math:`Y_{t-1}` and 2337 :math:`X_{t-1}`. 2338 2339 The cases determine which deterministic terms are included in the 2340 model and which are tested as part of the test. 2341 2342 Cases: 2343 2344 1. No deterministic terms 2345 2. Constant included in both the model and the test 2346 3. Constant included in the model but not in the test 2347 4. Constant and trend included in the model, only trend included in 2348 the test 2349 5. Constant and trend included in the model, neither included in the 2350 test 2351 2352 The test statistic is a Wald-type quadratic form test that all of the 2353 coefficients in :math:`\beta` are 0 along with any included 2354 deterministic terms, which depends on the case. The statistic returned 2355 is an F-type test statistic which is the standard quadratic form test 2356 statistic divided by the number of restrictions. 2357 2358 References 2359 ---------- 2360 .. [*] Pesaran, M. H., Shin, Y., & Smith, R. J. (2001). Bounds testing 2361 approaches to the analysis of level relationships. Journal of 2362 applied econometrics, 16(3), 289-326. 2363 """ 2364 model = self.model 2365 trend: Literal["n", "c", "ct"] 2366 if case == 1: 2367 trend = "n" 2368 elif case in (2, 3): 2369 trend = "c" 2370 else: 2371 trend = "ct" 2372 order = {key: max(val) for key, val in model._order.items()} 2373 uecm = UECM( 2374 model.data.endog, 2375 max(model.ar_lags), 2376 model.data.orig_exog, 2377 order=order, 2378 causal=model.causal, 2379 trend=trend, 2380 ) 2381 res = uecm.fit(cov_type=cov_type, cov_kwds=cov_kwds, use_t=use_t) 2382 cov = res.cov_params() 2383 nvar = len(res.model.ardl_order) 2384 if case == 1: 2385 rest = np.arange(nvar) 2386 elif case == 2: 2387 rest = np.arange(nvar + 1) 2388 elif case == 3: 2389 rest = np.arange(1, nvar + 1) 2390 elif case == 4: 2391 rest = np.arange(1, nvar + 2) 2392 elif case == 5: 2393 rest = np.arange(2, nvar + 2) 2394 r = np.zeros((rest.shape[0], cov.shape[1])) 2395 for i, loc in enumerate(rest): 2396 r[i, loc] = 1 2397 vcv = r @ cov @ r.T 2398 coef = r @ res.params 2399 stat = coef.T @ np.linalg.inv(vcv) @ coef / r.shape[0] 2400 k = nvar 2401 if asymptotic and k <= 10: 2402 cv = pss_critical_values.crit_vals 2403 key = (k, case) 2404 upper = cv[key + (True,)] 2405 lower = cv[key + (False,)] 2406 crit_vals = pd.DataFrame( 2407 {"lower": lower, "upper": upper}, 2408 index=pss_critical_values.crit_percentiles, 2409 ) 2410 crit_vals.index.name = "percentile" 2411 p_values = pd.Series( 2412 { 2413 "lower": _pss_pvalue(stat, k, case, False), 2414 "upper": _pss_pvalue(stat, k, case, True), 2415 } 2416 ) 2417 else: 2418 nobs = res.resid.shape[0] 2419 crit_vals, p_values = _pss_simulate( 2420 stat, k, case, nobs=nobs, nsim=nsim, seed=seed 2421 ) 2422 2423 return BoundsTestResult( 2424 stat, 2425 crit_vals, 2426 p_values, 2427 "No Cointegration", 2428 "Possible Cointegration", 2429 ) 2430 2431 2432def _pss_pvalue(stat: float, k: int, case: int, i1: bool) -> float: 2433 key = (k, case, i1) 2434 large_p = pss_critical_values.large_p[key] 2435 small_p = pss_critical_values.small_p[key] 2436 threshold = pss_critical_values.stat_star[key] 2437 log_stat = np.log(stat) 2438 p = small_p if stat > threshold else large_p 2439 x = [log_stat ** i for i in range(len(p))] 2440 return 1 - stats.norm.cdf(x @ np.array(p)) 2441 2442 2443def _pss_simulate( 2444 stat: float, 2445 k: int, 2446 case: Literal[1, 2, 3, 4, 5], 2447 nobs: int, 2448 nsim: int, 2449 seed: Union[ 2450 int, Sequence[int], np.random.RandomState, np.random.Generator 2451 ], 2452) -> Tuple[pd.DataFrame, pd.Series]: 2453 if not isinstance(seed, np.random.RandomState): 2454 rs: Union[ 2455 np.random.RandomState, np.random.Generator 2456 ] = np.random.default_rng(seed) 2457 else: 2458 assert isinstance(seed, np.random.RandomState) 2459 rs = seed 2460 2461 def _vectorized_ols_resid(rhs, lhs): 2462 rhs_t = np.transpose(rhs, [0, 2, 1]) 2463 xpx = np.matmul(rhs_t, rhs) 2464 xpy = np.matmul(rhs_t, lhs) 2465 b = np.linalg.solve(xpx, xpy) 2466 return np.squeeze(lhs - np.matmul(rhs, b)) 2467 2468 block_size = 100_000_000 // (8 * nobs * k) 2469 remaining = nsim 2470 loc = 0 2471 f_upper = np.empty(nsim) 2472 f_lower = np.empty(nsim) 2473 while remaining > 0: 2474 to_do = min(remaining, block_size) 2475 e = rs.standard_normal((to_do, nobs + 1, k)) 2476 2477 y = np.cumsum(e[:, :, :1], axis=1) 2478 x_upper = np.cumsum(e[:, :, 1:], axis=1) 2479 x_lower = e[:, :, 1:] 2480 lhs = np.diff(y, axis=1) 2481 if case in (2, 3): 2482 rhs = np.empty((to_do, nobs, k + 1)) 2483 rhs[:, :, -1] = 1 2484 elif case in (4, 5): 2485 rhs = np.empty((to_do, nobs, k + 2)) 2486 rhs[:, :, -2] = np.arange(nobs, dtype=float) 2487 rhs[:, :, -1] = 1 2488 else: 2489 rhs = np.empty((to_do, nobs, k)) 2490 rhs[:, :, :1] = y[:, :-1] 2491 rhs[:, :, 1:k] = x_upper[:, :-1] 2492 2493 u = _vectorized_ols_resid(rhs, lhs) 2494 df = rhs.shape[1] - rhs.shape[2] 2495 s2 = (u ** 2).sum(1) / df 2496 2497 if case in (3, 4): 2498 rhs_r = rhs[:, :, -1:] 2499 elif case == 5: # case 5 2500 rhs_r = rhs[:, :, -2:] 2501 if case in (3, 4, 5): 2502 ur = _vectorized_ols_resid(rhs_r, lhs) 2503 nrest = rhs.shape[-1] - rhs_r.shape[-1] 2504 else: 2505 ur = np.squeeze(lhs) 2506 nrest = rhs.shape[-1] 2507 2508 f = ((ur ** 2).sum(1) - (u ** 2).sum(1)) / nrest 2509 f /= s2 2510 f_upper[loc : loc + to_do] = f 2511 2512 # Lower 2513 rhs[:, :, 1:k] = x_lower[:, :-1] 2514 u = _vectorized_ols_resid(rhs, lhs) 2515 s2 = (u ** 2).sum(1) / df 2516 2517 if case in (3, 4): 2518 rhs_r = rhs[:, :, -1:] 2519 elif case == 5: # case 5 2520 rhs_r = rhs[:, :, -2:] 2521 if case in (3, 4, 5): 2522 ur = _vectorized_ols_resid(rhs_r, lhs) 2523 nrest = rhs.shape[-1] - rhs_r.shape[-1] 2524 else: 2525 ur = np.squeeze(lhs) 2526 nrest = rhs.shape[-1] 2527 2528 f = ((ur ** 2).sum(1) - (u ** 2).sum(1)) / nrest 2529 f /= s2 2530 f_lower[loc : loc + to_do] = f 2531 2532 loc += to_do 2533 remaining -= to_do 2534 2535 crit_percentiles = pss_critical_values.crit_percentiles 2536 crit_vals = pd.DataFrame( 2537 { 2538 "lower": np.percentile(f_lower, crit_percentiles), 2539 "upper": np.percentile(f_upper, crit_percentiles), 2540 }, 2541 index=crit_percentiles, 2542 ) 2543 crit_vals.index.name = "percentile" 2544 p_values = pd.Series( 2545 {"lower": (stat < f_lower).mean(), "upper": (stat < f_upper).mean()} 2546 ) 2547 return crit_vals, p_values 2548 2549 2550class UECMResultsWrapper(wrap.ResultsWrapper): 2551 _attrs = {} 2552 _wrap_attrs = wrap.union_dicts( 2553 tsa_model.TimeSeriesResultsWrapper._wrap_attrs, _attrs 2554 ) 2555 _methods = {} 2556 _wrap_methods = wrap.union_dicts( 2557 tsa_model.TimeSeriesResultsWrapper._wrap_methods, _methods 2558 ) 2559 2560 2561wrap.populate_wrapper(UECMResultsWrapper, UECMResults) 2562