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