""" SARIMAX parameters class. Author: Chad Fulton License: BSD-3 """ import numpy as np import pandas as pd from numpy.polynomial import Polynomial from statsmodels.tsa.statespace.tools import is_invertible from statsmodels.tsa.arima.tools import validate_basic class SARIMAXParams: """ SARIMAX parameters. Parameters ---------- spec : SARIMAXSpecification Specification of the SARIMAX model. Attributes ---------- spec : SARIMAXSpecification Specification of the SARIMAX model. exog_names : list of str Names associated with exogenous parameters. ar_names : list of str Names associated with (non-seasonal) autoregressive parameters. ma_names : list of str Names associated with (non-seasonal) moving average parameters. seasonal_ar_names : list of str Names associated with seasonal autoregressive parameters. seasonal_ma_names : list of str Names associated with seasonal moving average parameters. param_names :list of str Names of all model parameters. k_exog_params : int Number of parameters associated with exogenous variables. k_ar_params : int Number of parameters associated with (non-seasonal) autoregressive lags. k_ma_params : int Number of parameters associated with (non-seasonal) moving average lags. k_seasonal_ar_params : int Number of parameters associated with seasonal autoregressive lags. k_seasonal_ma_params : int Number of parameters associated with seasonal moving average lags. k_params : int Total number of model parameters. """ def __init__(self, spec): self.spec = spec # Local copies of relevant attributes self.exog_names = spec.exog_names self.ar_names = spec.ar_names self.ma_names = spec.ma_names self.seasonal_ar_names = spec.seasonal_ar_names self.seasonal_ma_names = spec.seasonal_ma_names self.param_names = spec.param_names self.k_exog_params = spec.k_exog_params self.k_ar_params = spec.k_ar_params self.k_ma_params = spec.k_ma_params self.k_seasonal_ar_params = spec.k_seasonal_ar_params self.k_seasonal_ma_params = spec.k_seasonal_ma_params self.k_params = spec.k_params # Cache for holding parameter values self._params_split = spec.split_params( np.zeros(self.k_params) * np.nan, allow_infnan=True) self._params = None @property def exog_params(self): """(array) Parameters associated with exogenous variables.""" return self._params_split['exog_params'] @exog_params.setter def exog_params(self, value): if np.isscalar(value): value = [value] * self.k_exog_params self._params_split['exog_params'] = validate_basic( value, self.k_exog_params, title='exogenous coefficients') self._params = None @property def ar_params(self): """(array) Autoregressive (non-seasonal) parameters.""" return self._params_split['ar_params'] @ar_params.setter def ar_params(self, value): if np.isscalar(value): value = [value] * self.k_ar_params self._params_split['ar_params'] = validate_basic( value, self.k_ar_params, title='AR coefficients') self._params = None @property def ar_poly(self): """(Polynomial) Autoregressive (non-seasonal) lag polynomial.""" coef = np.zeros(self.spec.max_ar_order + 1) coef[0] = 1 ix = self.spec.ar_lags coef[ix] = -self._params_split['ar_params'] return Polynomial(coef) @ar_poly.setter def ar_poly(self, value): # Convert from the polynomial to the parameters, and set that way if isinstance(value, Polynomial): value = value.coef value = validate_basic(value, self.spec.max_ar_order + 1, title='AR polynomial') if value[0] != 1: raise ValueError('AR polynomial constant must be equal to 1.') ar_params = [] for i in range(1, self.spec.max_ar_order + 1): if i in self.spec.ar_lags: ar_params.append(-value[i]) elif value[i] != 0: raise ValueError('AR polynomial includes non-zero values' ' for lags that are excluded in the' ' specification.') self.ar_params = ar_params @property def ma_params(self): """(array) Moving average (non-seasonal) parameters.""" return self._params_split['ma_params'] @ma_params.setter def ma_params(self, value): if np.isscalar(value): value = [value] * self.k_ma_params self._params_split['ma_params'] = validate_basic( value, self.k_ma_params, title='MA coefficients') self._params = None @property def ma_poly(self): """(Polynomial) Moving average (non-seasonal) lag polynomial.""" coef = np.zeros(self.spec.max_ma_order + 1) coef[0] = 1 ix = self.spec.ma_lags coef[ix] = self._params_split['ma_params'] return Polynomial(coef) @ma_poly.setter def ma_poly(self, value): # Convert from the polynomial to the parameters, and set that way if isinstance(value, Polynomial): value = value.coef value = validate_basic(value, self.spec.max_ma_order + 1, title='MA polynomial') if value[0] != 1: raise ValueError('MA polynomial constant must be equal to 1.') ma_params = [] for i in range(1, self.spec.max_ma_order + 1): if i in self.spec.ma_lags: ma_params.append(value[i]) elif value[i] != 0: raise ValueError('MA polynomial includes non-zero values' ' for lags that are excluded in the' ' specification.') self.ma_params = ma_params @property def seasonal_ar_params(self): """(array) Seasonal autoregressive parameters.""" return self._params_split['seasonal_ar_params'] @seasonal_ar_params.setter def seasonal_ar_params(self, value): if np.isscalar(value): value = [value] * self.k_seasonal_ar_params self._params_split['seasonal_ar_params'] = validate_basic( value, self.k_seasonal_ar_params, title='seasonal AR coefficients') self._params = None @property def seasonal_ar_poly(self): """(Polynomial) Seasonal autoregressive lag polynomial.""" # Need to expand the polynomial according to the season s = self.spec.seasonal_periods coef = [1] if s > 0: expanded = np.zeros(self.spec.max_seasonal_ar_order) ix = np.array(self.spec.seasonal_ar_lags, dtype=int) - 1 expanded[ix] = -self._params_split['seasonal_ar_params'] coef = np.r_[1, np.pad(np.reshape(expanded, (-1, 1)), [(0, 0), (s - 1, 0)], 'constant').flatten()] return Polynomial(coef) @seasonal_ar_poly.setter def seasonal_ar_poly(self, value): s = self.spec.seasonal_periods # Note: assume that we are given coefficients from the full polynomial # Convert from the polynomial to the parameters, and set that way if isinstance(value, Polynomial): value = value.coef value = validate_basic(value, 1 + s * self.spec.max_seasonal_ar_order, title='seasonal AR polynomial') if value[0] != 1: raise ValueError('Polynomial constant must be equal to 1.') seasonal_ar_params = [] for i in range(1, self.spec.max_seasonal_ar_order + 1): if i in self.spec.seasonal_ar_lags: seasonal_ar_params.append(-value[s * i]) elif value[s * i] != 0: raise ValueError('AR polynomial includes non-zero values' ' for lags that are excluded in the' ' specification.') self.seasonal_ar_params = seasonal_ar_params @property def seasonal_ma_params(self): """(array) Seasonal moving average parameters.""" return self._params_split['seasonal_ma_params'] @seasonal_ma_params.setter def seasonal_ma_params(self, value): if np.isscalar(value): value = [value] * self.k_seasonal_ma_params self._params_split['seasonal_ma_params'] = validate_basic( value, self.k_seasonal_ma_params, title='seasonal MA coefficients') self._params = None @property def seasonal_ma_poly(self): """(Polynomial) Seasonal moving average lag polynomial.""" # Need to expand the polynomial according to the season s = self.spec.seasonal_periods coef = np.array([1]) if s > 0: expanded = np.zeros(self.spec.max_seasonal_ma_order) ix = np.array(self.spec.seasonal_ma_lags, dtype=int) - 1 expanded[ix] = self._params_split['seasonal_ma_params'] coef = np.r_[1, np.pad(np.reshape(expanded, (-1, 1)), [(0, 0), (s - 1, 0)], 'constant').flatten()] return Polynomial(coef) @seasonal_ma_poly.setter def seasonal_ma_poly(self, value): s = self.spec.seasonal_periods # Note: assume that we are given coefficients from the full polynomial # Convert from the polynomial to the parameters, and set that way if isinstance(value, Polynomial): value = value.coef value = validate_basic(value, 1 + s * self.spec.max_seasonal_ma_order, title='seasonal MA polynomial',) if value[0] != 1: raise ValueError('Polynomial constant must be equal to 1.') seasonal_ma_params = [] for i in range(1, self.spec.max_seasonal_ma_order + 1): if i in self.spec.seasonal_ma_lags: seasonal_ma_params.append(value[s * i]) elif value[s * i] != 0: raise ValueError('MA polynomial includes non-zero values' ' for lags that are excluded in the' ' specification.') self.seasonal_ma_params = seasonal_ma_params @property def sigma2(self): """(float) Innovation variance.""" return self._params_split['sigma2'] @sigma2.setter def sigma2(self, params): length = int(not self.spec.concentrate_scale) self._params_split['sigma2'] = validate_basic( params, length, title='sigma2').item() self._params = None @property def reduced_ar_poly(self): """(Polynomial) Reduced form autoregressive lag polynomial.""" return self.ar_poly * self.seasonal_ar_poly @property def reduced_ma_poly(self): """(Polynomial) Reduced form moving average lag polynomial.""" return self.ma_poly * self.seasonal_ma_poly @property def params(self): """(array) Complete parameter vector.""" if self._params is None: self._params = self.spec.join_params(**self._params_split) return self._params.copy() @params.setter def params(self, value): self._params_split = self.spec.split_params(value) self._params = None @property def is_complete(self): """(bool) Are current parameter values all filled in (i.e. not NaN).""" return not np.any(np.isnan(self.params)) @property def is_valid(self): """(bool) Are current parameter values valid (e.g. variance > 0).""" valid = True try: self.spec.validate_params(self.params) except ValueError: valid = False return valid @property def is_stationary(self): """(bool) Is the reduced autoregressive lag poylnomial stationary.""" validate_basic(self.ar_params, self.k_ar_params, title='AR coefficients') validate_basic(self.seasonal_ar_params, self.k_seasonal_ar_params, title='seasonal AR coefficients') ar_stationary = True seasonal_ar_stationary = True if self.k_ar_params > 0: ar_stationary = is_invertible(self.ar_poly.coef) if self.k_seasonal_ar_params > 0: seasonal_ar_stationary = is_invertible(self.seasonal_ar_poly.coef) return ar_stationary and seasonal_ar_stationary @property def is_invertible(self): """(bool) Is the reduced moving average lag poylnomial invertible.""" # Short-circuit if there is no MA component validate_basic(self.ma_params, self.k_ma_params, title='MA coefficients') validate_basic(self.seasonal_ma_params, self.k_seasonal_ma_params, title='seasonal MA coefficients') ma_stationary = True seasonal_ma_stationary = True if self.k_ma_params > 0: ma_stationary = is_invertible(self.ma_poly.coef) if self.k_seasonal_ma_params > 0: seasonal_ma_stationary = is_invertible(self.seasonal_ma_poly.coef) return ma_stationary and seasonal_ma_stationary def to_dict(self): """ Return the parameters split by type into a dictionary. Returns ------- split_params : dict Dictionary with keys 'exog_params', 'ar_params', 'ma_params', 'seasonal_ar_params', 'seasonal_ma_params', and (unless `concentrate_scale=True`) 'sigma2'. Values are the parameters associated with the key, based on the `params` argument. """ return self._params_split.copy() def to_pandas(self): """ Return the parameters as a Pandas series. Returns ------- series : pd.Series Pandas series with index set to the parameter names. """ return pd.Series(self.params, index=self.param_names) def __repr__(self): """Represent SARIMAXParams object as a string.""" components = [] if self.k_exog_params: components.append('exog=%s' % str(self.exog_params)) if self.k_ar_params: components.append('ar=%s' % str(self.ar_params)) if self.k_ma_params: components.append('ma=%s' % str(self.ma_params)) if self.k_seasonal_ar_params: components.append('seasonal_ar=%s' % str(self.seasonal_ar_params)) if self.k_seasonal_ma_params: components.append('seasonal_ma=%s' % str(self.seasonal_ma_params)) if not self.spec.concentrate_scale: components.append('sigma2=%s' % self.sigma2) return 'SARIMAXParams(%s)' % ', '.join(components)