Skip to content

Commit

Permalink
MAINT: Protect against future pandas changes
Browse files Browse the repository at this point in the history
CachedProperty is not property-like and uses fget rather than func
  • Loading branch information
bashtage committed Nov 5, 2021
1 parent 222dd53 commit 6f0731a
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 100 deletions.
20 changes: 20 additions & 0 deletions statsmodels/compat/pandas.py
@@ -1,4 +1,5 @@
from distutils.version import LooseVersion
from typing import Optional

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -26,6 +27,9 @@
"make_dataframe",
"to_numpy",
"pandas_lt_1_0_0",
"get_cached_func",
"get_cached_doc",
"call_cached_func",
]

version = LooseVersion(pd.__version__)
Expand Down Expand Up @@ -181,3 +185,19 @@ def to_numpy(po: pd.DataFrame) -> np.ndarray:
return po.to_numpy()
except AttributeError:
return po.values


def get_cached_func(cached_prop):
try:
return cached_prop.fget
except AttributeError:
return cached_prop.func


def call_cached_func(cached_prop, *args, **kwargs):
f = get_cached_func(cached_prop)
return f(*args, **kwargs)


def get_cached_doc(cached_prop) -> Optional[str]:
return get_cached_func(cached_prop).__doc__
78 changes: 46 additions & 32 deletions statsmodels/regression/rolling.py
Expand Up @@ -8,7 +8,13 @@
License: 3-clause BSD
"""
from statsmodels.compat.numpy import lstsq
from statsmodels.compat.pandas import Appender, Substitution, cache_readonly
from statsmodels.compat.pandas import (
Appender,
Substitution,
cache_readonly,
call_cached_func,
get_cached_doc,
)

from collections import namedtuple

Expand Down Expand Up @@ -474,6 +480,7 @@ class RollingRegressionResults(object):
cov_type : str
Name of covariance estimator
"""

_data_in_cache = tuple()

def __init__(
Expand Down Expand Up @@ -515,32 +522,33 @@ def _wrap(self, val):
return DataFrame(val, columns=col_names, index=mi)

@cache_readonly
@Appender(RegressionResults.aic.func.__doc__)
@Appender(get_cached_doc(RegressionResults.aic))
def aic(self):
return self._wrap(RegressionResults.aic.func(self))
return self._wrap(call_cached_func(RegressionResults.aic, self))

@cache_readonly
@Appender(RegressionResults.bic.func.__doc__)
@Appender(get_cached_doc(RegressionResults.bic))
def bic(self):
with np.errstate(divide="ignore"):
return self._wrap(RegressionResults.bic.func(self))
return self._wrap(call_cached_func(RegressionResults.bic, self))

def info_criteria(self, crit, dk_params=0):
return self._wrap(RegressionResults.info_criteria(
self, crit, dk_params=dk_params))
return self._wrap(
RegressionResults.info_criteria(self, crit, dk_params=dk_params)
)

@cache_readonly
def params(self):
"""Estimated model parameters"""
return self._wrap(self._params)

@cache_readonly
@Appender(RegressionResults.ssr.func.__doc__)
@Appender(get_cached_doc(RegressionResults.ssr))
def ssr(self):
return self._wrap(self._ssr)

@cache_readonly
@Appender(RegressionResults.llf.func.__doc__)
@Appender(get_cached_doc(RegressionResults.llf))
def llf(self):
return self._wrap(self._llf)

Expand All @@ -555,27 +563,29 @@ def k_constant(self):
return self._k_constant

@cache_readonly
@Appender(RegressionResults.centered_tss.func.__doc__)
@Appender(get_cached_doc(RegressionResults.centered_tss))
def centered_tss(self):
return self._centered_tss

@cache_readonly
@Appender(RegressionResults.uncentered_tss.func.__doc__)
@Appender(get_cached_doc(RegressionResults.uncentered_tss))
def uncentered_tss(self):
return self._uncentered_tss

@cache_readonly
@Appender(RegressionResults.rsquared.func.__doc__)
@Appender(get_cached_doc(RegressionResults.rsquared))
def rsquared(self):
return self._wrap(RegressionResults.rsquared.func(self))
return self._wrap(call_cached_func(RegressionResults.rsquared, self))

@cache_readonly
@Appender(RegressionResults.rsquared_adj.func.__doc__)
@Appender(get_cached_doc(RegressionResults.rsquared_adj))
def rsquared_adj(self):
return self._wrap(RegressionResults.rsquared_adj.func(self))
return self._wrap(
call_cached_func(RegressionResults.rsquared_adj, self)
)

@cache_readonly
@Appender(RegressionResults.nobs.func.__doc__)
@Appender(get_cached_doc(RegressionResults.nobs))
def nobs(self):
return self._wrap(self._nobs)

Expand All @@ -590,24 +600,24 @@ def use_t(self):
return self._use_t

@cache_readonly
@Appender(RegressionResults.ess.func.__doc__)
@Appender(get_cached_doc(RegressionResults.ess))
def ess(self):
return self._wrap(RegressionResults.ess.func(self))
return self._wrap(call_cached_func(RegressionResults.ess, self))

@cache_readonly
@Appender(RegressionResults.mse_model.func.__doc__)
@Appender(get_cached_doc(RegressionResults.mse_model))
def mse_model(self):
return self._wrap(RegressionResults.mse_model.func(self))
return self._wrap(call_cached_func(RegressionResults.mse_model, self))

@cache_readonly
@Appender(RegressionResults.mse_resid.func.__doc__)
@Appender(get_cached_doc(RegressionResults.mse_resid))
def mse_resid(self):
return self._wrap(RegressionResults.mse_resid.func(self))
return self._wrap(call_cached_func(RegressionResults.mse_resid, self))

@cache_readonly
@Appender(RegressionResults.mse_total.func.__doc__)
@Appender(get_cached_doc(RegressionResults.mse_total))
def mse_total(self):
return self._wrap(RegressionResults.mse_total.func(self))
return self._wrap(call_cached_func(RegressionResults.mse_total, self))

@cache_readonly
def _cov_params(self):
Expand All @@ -629,17 +639,19 @@ def cov_params(self):
the returned covariance is a DataFrame with a MultiIndex with
key (observation, variable), so that the covariance for
observation with index i is cov.loc[i].
"""
"""
return self._wrap(self._cov_params)

@cache_readonly
@Appender(RegressionResults.f_pvalue.func.__doc__)
@Appender(get_cached_doc(RegressionResults.f_pvalue))
def f_pvalue(self):
with np.errstate(invalid="ignore"):
return self._wrap(RegressionResults.f_pvalue.func(self))
return self._wrap(
call_cached_func(RegressionResults.f_pvalue, self)
)

@cache_readonly
@Appender(RegressionResults.fvalue.func.__doc__)
@Appender(get_cached_doc(RegressionResults.fvalue))
def fvalue(self):
if self._cov_type == "nonrobust":
return self.mse_model / self.mse_resid
Expand All @@ -665,19 +677,21 @@ def fvalue(self):
return stat

@cache_readonly
@Appender(RegressionResults.bse.func.__doc__)
@Appender(get_cached_doc(RegressionResults.bse))
def bse(self):
with np.errstate(invalid="ignore"):
return self._wrap(np.sqrt(np.diagonal(self._cov_params, 0, 2)))

@cache_readonly
@Appender(LikelihoodModelResults.tvalues.func.__doc__)
@Appender(get_cached_doc(LikelihoodModelResults.tvalues))
def tvalues(self):
with np.errstate(invalid="ignore"):
return self._wrap(LikelihoodModelResults.tvalues.func(self))
return self._wrap(
call_cached_func(LikelihoodModelResults.tvalues, self)
)

@cache_readonly
@Appender(LikelihoodModelResults.pvalues.func.__doc__)
@Appender(get_cached_doc(LikelihoodModelResults.pvalues))
def pvalues(self):
if self.use_t:
df_resid = getattr(self, "df_resid_inference", self.df_resid)
Expand Down
133 changes: 69 additions & 64 deletions statsmodels/tsa/ar_model.py
@@ -1,7 +1,12 @@
# -*- coding: utf-8 -*-
from __future__ import annotations

from statsmodels.compat.pandas import Appender, Substitution, to_numpy
from statsmodels.compat.pandas import (
Appender,
Substitution,
call_cached_func,
to_numpy,
)

from collections.abc import Iterable
import datetime as dt
Expand Down Expand Up @@ -1440,29 +1445,29 @@ def plot_predict(
figsize=None,
):
"""
Plot in- and out-of-sample predictions
Parameters
----------
%(predict_params)s
alpha : {float, None}
The tail probability not covered by the confidence interval. Must
be in (0, 1). Confidence interval is constructed assuming normally
distributed shocks. If None, figure will not show the confidence
interval.
in_sample : bool
Flag indicating whether to include the in-sample period in the
plot.
fig : Figure
An existing figure handle. If not provided, a new figure is
created.
figsize: tuple[float, float]
Tuple containing the figure size values.
Returns
-------
Figure
Figure handle containing the plot.
Plot in- and out-of-sample predictions
Parameters
----------
%(predict_params)s
alpha : {float, None}
The tail probability not covered by the confidence interval. Must
be in (0, 1). Confidence interval is constructed assuming normally
distributed shocks. If None, figure will not show the confidence
interval.
in_sample : bool
Flag indicating whether to include the in-sample period in the
plot.
fig : Figure
An existing figure handle. If not provided, a new figure is
created.
figsize: tuple[float, float]
Tuple containing the figure size values.
Returns
-------
Figure
Figure handle containing the plot.
"""
predictions = self.get_prediction(
start=start, end=end, dynamic=dynamic, exog=exog, exog_oos=exog_oos
Expand Down Expand Up @@ -1702,51 +1707,51 @@ def ar_select_order(
old_names=False,
):
"""
Autoregressive AR-X(p) model order selection.
Autoregressive AR-X(p) model order selection.
Parameters
----------
endog : array_like
A 1-d endogenous response variable. The independent variable.
maxlag : int
The maximum lag to consider.
ic : {'aic', 'hqic', 'bic'}
The information criterion to use in the selection.
glob : bool
Flag indicating where to use a global search across all combinations
of lags. In practice, this option is not computational feasible when
maxlag is larger than 15 (or perhaps 20) since the global search
requires fitting 2**maxlag models.
%(auto_reg_params)s
Returns
-------
AROrderSelectionResults
A results holder containing the model and the complete set of
information criteria for all models fit.
Parameters
----------
endog : array_like
A 1-d endogenous response variable. The independent variable.
maxlag : int
The maximum lag to consider.
ic : {'aic', 'hqic', 'bic'}
The information criterion to use in the selection.
glob : bool
Flag indicating where to use a global search across all combinations
of lags. In practice, this option is not computational feasible when
maxlag is larger than 15 (or perhaps 20) since the global search
requires fitting 2**maxlag models.
%(auto_reg_params)s
Examples
--------
>>> from statsmodels.tsa.ar_model import ar_select_order
>>> data = sm.datasets.sunspots.load_pandas().data['SUNACTIVITY']
Returns
-------
AROrderSelectionResults
A results holder containing the model and the complete set of
information criteria for all models fit.
Examples
--------
>>> from statsmodels.tsa.ar_model import ar_select_order
>>> data = sm.datasets.sunspots.load_pandas().data['SUNACTIVITY']
Determine the optimal lag structure
Determine the optimal lag structure
>>> mod = ar_select_order(data, maxlag=13)
>>> mod.ar_lags
array([1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> mod = ar_select_order(data, maxlag=13)
>>> mod.ar_lags
array([1, 2, 3, 4, 5, 6, 7, 8, 9])
Determine the optimal lag structure with seasonal terms
Determine the optimal lag structure with seasonal terms
>>> mod = ar_select_order(data, maxlag=13, seasonal=True, period=12)
>>> mod.ar_lags
array([1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> mod = ar_select_order(data, maxlag=13, seasonal=True, period=12)
>>> mod.ar_lags
array([1, 2, 3, 4, 5, 6, 7, 8, 9])
Globally determine the optimal lag structure
Globally determine the optimal lag structure
>>> mod = ar_select_order(data, maxlag=13, glob=True)
>>> mod.ar_lags
array([1, 2, 9])
>>> mod = ar_select_order(data, maxlag=13, glob=True)
>>> mod.ar_lags
array([1, 2, 9])
"""
full_mod = AutoReg(
endog,
Expand Down Expand Up @@ -1774,9 +1779,9 @@ def compute_ics(res):
nobs=nobs, df_model=df_model, sigma2=sigma2, llf=llf
)

aic = AutoRegResults.aic.func(res)
bic = AutoRegResults.bic.func(res)
hqic = AutoRegResults.hqic.func(res)
aic = call_cached_func(AutoRegResults.aic, res)
bic = call_cached_func(AutoRegResults.bic, res)
hqic = call_cached_func(AutoRegResults.hqic, res)

return aic, bic, hqic

Expand Down

0 comments on commit 6f0731a

Please sign in to comment.