Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/statsmodels/regression/recursive_ls.py : 19%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Recursive least squares model
4Author: Chad Fulton
5License: Simplified-BSD
6"""
8import numpy as np
9import pandas as pd
11from statsmodels.compat.pandas import Appender
13from statsmodels.tools.data import _is_using_pandas
14from statsmodels.tsa.statespace.mlemodel import (
15 MLEModel, MLEResults, MLEResultsWrapper, PredictionResults,
16 PredictionResultsWrapper)
17from statsmodels.tsa.statespace.tools import concat
18from statsmodels.tools.tools import Bunch
19from statsmodels.tools.decorators import cache_readonly
20import statsmodels.base.wrapper as wrap
22# Columns are alpha = 0.1, 0.05, 0.025, 0.01, 0.005
23_cusum_squares_scalars = np.array([
24 [1.0729830, 1.2238734, 1.3581015, 1.5174271, 1.6276236],
25 [-0.6698868, -0.6700069, -0.6701218, -0.6702672, -0.6703724],
26 [-0.5816458, -0.7351697, -0.8858694, -1.0847745, -1.2365861]
27])
30class RecursiveLS(MLEModel):
31 r"""
32 Recursive least squares
34 Parameters
35 ----------
36 endog : array_like
37 The observed time-series process :math:`y`
38 exog : array_like
39 Array of exogenous regressors, shaped nobs x k.
40 constraints : array_like, str, or tuple
41 - array : An r x k array where r is the number of restrictions to
42 test and k is the number of regressors. It is assumed that the
43 linear combination is equal to zero.
44 - str : The full hypotheses to test can be given as a string.
45 See the examples.
46 - tuple : A tuple of arrays in the form (R, q), ``q`` can be
47 either a scalar or a length p row vector.
49 Notes
50 -----
51 Recursive least squares (RLS) corresponds to expanding window ordinary
52 least squares (OLS).
54 This model applies the Kalman filter to compute recursive estimates of the
55 coefficients and recursive residuals.
57 References
58 ----------
59 .. [*] Durbin, James, and Siem Jan Koopman. 2012.
60 Time Series Analysis by State Space Methods: Second Edition.
61 Oxford University Press.
62 """
63 def __init__(self, endog, exog, constraints=None, **kwargs):
64 # Standardize data
65 endog_using_pandas = _is_using_pandas(endog, None)
66 if not endog_using_pandas:
67 endog = np.asanyarray(endog)
69 exog_is_using_pandas = _is_using_pandas(exog, None)
70 if not exog_is_using_pandas:
71 exog = np.asarray(exog)
73 # Make sure we have 2-dimensional array
74 if exog.ndim == 1:
75 if not exog_is_using_pandas:
76 exog = exog[:, None]
77 else:
78 exog = pd.DataFrame(exog)
80 self.k_exog = exog.shape[1]
82 # Handle constraints
83 self.k_constraints = 0
84 self._r_matrix = self._q_matrix = None
85 if constraints is not None:
86 from patsy import DesignInfo
87 from statsmodels.base.data import handle_data
88 data = handle_data(endog, exog, **kwargs)
89 names = data.param_names
90 LC = DesignInfo(names).linear_constraint(constraints)
91 self._r_matrix, self._q_matrix = LC.coefs, LC.constants
92 self.k_constraints = self._r_matrix.shape[0]
94 constraint_endog = np.zeros((len(endog), len(self._r_matrix)))
95 if endog_using_pandas:
96 constraint_endog = pd.DataFrame(constraint_endog,
97 index=endog.index)
98 endog = concat([endog, constraint_endog], axis=1)
99 endog.values[:, 1:] = self._q_matrix[:, 0]
100 else:
101 endog[:, 1:] = self._q_matrix[:, 0]
103 # Handle coefficient initialization
104 kwargs.setdefault('initialization', 'diffuse')
106 # Initialize the state space representation
107 super(RecursiveLS, self).__init__(
108 endog, k_states=self.k_exog, exog=exog, **kwargs)
110 # Use univariate filtering by default
111 self.ssm.filter_univariate = True
113 # Concentrate the scale out of the likelihood function
114 self.ssm.filter_concentrated = True
116 # Setup the state space representation
117 self['design'] = np.zeros((self.k_endog, self.k_states, self.nobs))
118 self['design', 0] = self.exog[:, :, None].T
119 if self._r_matrix is not None:
120 self['design', 1:, :] = self._r_matrix[:, :, None]
121 self['transition'] = np.eye(self.k_states)
123 # Notice that the filter output does not depend on the measurement
124 # variance, so we set it here to 1
125 self['obs_cov', 0, 0] = 1.
126 self['transition'] = np.eye(self.k_states)
128 # Linear constraints are technically imposed by adding "fake" endog
129 # variables that are used during filtering, but for all model- and
130 # results-based purposes we want k_endog = 1.
131 if self._r_matrix is not None:
132 self.k_endog = 1
134 @classmethod
135 def from_formula(cls, formula, data, subset=None, constraints=None):
136 return super(MLEModel, cls).from_formula(formula, data, subset,
137 constraints=constraints)
139 def _validate_can_fix_params(self, param_names):
140 raise ValueError('Linear constraints on coefficients should be given'
141 ' using the `constraints` argument in constructing.'
142 ' the model. Other parameter constraints are not'
143 ' available in the resursive least squares model.')
145 def fit(self):
146 """
147 Fits the model by application of the Kalman filter
149 Returns
150 -------
151 RecursiveLSResults
152 """
153 smoother_results = self.smooth(return_ssm=True)
155 with self.ssm.fixed_scale(smoother_results.scale):
156 res = self.smooth()
158 return res
160 def filter(self, return_ssm=False, **kwargs):
161 # Get the state space output
162 result = super(RecursiveLS, self).filter([], transformed=True,
163 cov_type='none',
164 return_ssm=True, **kwargs)
166 # Wrap in a results object
167 if not return_ssm:
168 params = result.filtered_state[:, -1]
169 cov_kwds = {
170 'custom_cov_type': 'nonrobust',
171 'custom_cov_params': result.filtered_state_cov[:, :, -1],
172 'custom_description': ('Parameters and covariance matrix'
173 ' estimates are RLS estimates'
174 ' conditional on the entire sample.')
175 }
176 result = RecursiveLSResultsWrapper(
177 RecursiveLSResults(self, params, result, cov_type='custom',
178 cov_kwds=cov_kwds)
179 )
181 return result
183 def smooth(self, return_ssm=False, **kwargs):
184 # Get the state space output
185 result = super(RecursiveLS, self).smooth([], transformed=True,
186 cov_type='none',
187 return_ssm=True, **kwargs)
189 # Wrap in a results object
190 if not return_ssm:
191 params = result.filtered_state[:, -1]
192 cov_kwds = {
193 'custom_cov_type': 'nonrobust',
194 'custom_cov_params': result.filtered_state_cov[:, :, -1],
195 'custom_description': ('Parameters and covariance matrix'
196 ' estimates are RLS estimates'
197 ' conditional on the entire sample.')
198 }
199 result = RecursiveLSResultsWrapper(
200 RecursiveLSResults(self, params, result, cov_type='custom',
201 cov_kwds=cov_kwds)
202 )
204 return result
206 @property
207 def endog_names(self):
208 endog_names = super(RecursiveLS, self).endog_names
209 return endog_names[0] if isinstance(endog_names, list) else endog_names
211 @property
212 def param_names(self):
213 return self.exog_names
215 @property
216 def start_params(self):
217 # Only parameter is the measurement disturbance standard deviation
218 return np.zeros(0)
220 def update(self, params, **kwargs):
221 """
222 Update the parameters of the model
224 Updates the representation matrices to fill in the new parameter
225 values.
227 Parameters
228 ----------
229 params : array_like
230 Array of new parameters.
231 transformed : bool, optional
232 Whether or not `params` is already transformed. If set to False,
233 `transform_params` is called. Default is True..
235 Returns
236 -------
237 params : array_like
238 Array of parameters.
239 """
240 pass
243class RecursiveLSResults(MLEResults):
244 """
245 Class to hold results from fitting a recursive least squares model.
247 Parameters
248 ----------
249 model : RecursiveLS instance
250 The fitted model instance
252 Attributes
253 ----------
254 specification : dictionary
255 Dictionary including all attributes from the recursive least squares
256 model instance.
258 See Also
259 --------
260 statsmodels.tsa.statespace.kalman_filter.FilterResults
261 statsmodels.tsa.statespace.mlemodel.MLEResults
262 """
264 def __init__(self, model, params, filter_results, cov_type='opg',
265 **kwargs):
266 super(RecursiveLSResults, self).__init__(
267 model, params, filter_results, cov_type, **kwargs)
269 # Since we are overriding params with things that are not MLE params,
270 # need to adjust df's
271 q = max(self.loglikelihood_burn, self.k_diffuse_states)
272 self.df_model = q - self.model.k_constraints
273 self.df_resid = self.nobs_effective - self.df_model
275 # Save _init_kwds
276 self._init_kwds = self.model._get_init_kwds()
278 # Save the model specification
279 self.specification = Bunch(**{
280 'k_exog': self.model.k_exog,
281 'k_constraints': self.model.k_constraints})
283 # Adjust results to remove "faux" endog from the constraints
284 if self.model._r_matrix is not None:
285 for name in ['forecasts', 'forecasts_error',
286 'forecasts_error_cov', 'standardized_forecasts_error',
287 'forecasts_error_diffuse_cov']:
288 setattr(self, name, getattr(self, name)[0:1])
290 @property
291 def recursive_coefficients(self):
292 """
293 Estimates of regression coefficients, recursively estimated
295 Returns
296 -------
297 out: Bunch
298 Has the following attributes:
300 - `filtered`: a time series array with the filtered estimate of
301 the component
302 - `filtered_cov`: a time series array with the filtered estimate of
303 the variance/covariance of the component
304 - `smoothed`: a time series array with the smoothed estimate of
305 the component
306 - `smoothed_cov`: a time series array with the smoothed estimate of
307 the variance/covariance of the component
308 - `offset`: an integer giving the offset in the state vector where
309 this component begins
310 """
311 out = None
312 spec = self.specification
313 start = offset = 0
314 end = offset + spec.k_exog
315 out = Bunch(
316 filtered=self.filtered_state[start:end],
317 filtered_cov=self.filtered_state_cov[start:end, start:end],
318 smoothed=None, smoothed_cov=None,
319 offset=offset
320 )
321 if self.smoothed_state is not None:
322 out.smoothed = self.smoothed_state[start:end]
323 if self.smoothed_state_cov is not None:
324 out.smoothed_cov = (
325 self.smoothed_state_cov[start:end, start:end])
326 return out
328 @cache_readonly
329 def resid_recursive(self):
330 r"""
331 Recursive residuals
333 Returns
334 -------
335 resid_recursive : array_like
336 An array of length `nobs` holding the recursive
337 residuals.
339 Notes
340 -----
341 These quantities are defined in, for example, Harvey (1989)
342 section 5.4. In fact, there he defines the standardized innovations in
343 equation 5.4.1, but in his version they have non-unit variance, whereas
344 the standardized forecast errors computed by the Kalman filter here
345 assume unit variance. To convert to Harvey's definition, we need to
346 multiply by the standard deviation.
348 Harvey notes that in smaller samples, "although the second moment
349 of the :math:`\tilde \sigma_*^{-1} \tilde v_t`'s is unity, the
350 variance is not necessarily equal to unity as the mean need not be
351 equal to zero", and he defines an alternative version (which are
352 not provided here).
353 """
354 return (self.filter_results.standardized_forecasts_error[0] *
355 self.scale**0.5)
357 @cache_readonly
358 def cusum(self):
359 r"""
360 Cumulative sum of standardized recursive residuals statistics
362 Returns
363 -------
364 cusum : array_like
365 An array of length `nobs - k_exog` holding the
366 CUSUM statistics.
368 Notes
369 -----
370 The CUSUM statistic takes the form:
372 .. math::
374 W_t = \frac{1}{\hat \sigma} \sum_{j=k+1}^t w_j
376 where :math:`w_j` is the recursive residual at time :math:`j` and
377 :math:`\hat \sigma` is the estimate of the standard deviation
378 from the full sample.
380 Excludes the first `k_exog` datapoints.
382 Due to differences in the way :math:`\hat \sigma` is calculated, the
383 output of this function differs slightly from the output in the
384 R package strucchange and the Stata contributed .ado file cusum6. The
385 calculation in this package is consistent with the description of
386 Brown et al. (1975)
388 References
389 ----------
390 .. [*] Brown, R. L., J. Durbin, and J. M. Evans. 1975.
391 "Techniques for Testing the Constancy of
392 Regression Relationships over Time."
393 Journal of the Royal Statistical Society.
394 Series B (Methodological) 37 (2): 149-92.
395 """
396 d = max(self.nobs_diffuse, self.loglikelihood_burn)
397 return (np.cumsum(self.resid_recursive[d:]) /
398 np.std(self.resid_recursive[d:], ddof=1))
400 @cache_readonly
401 def cusum_squares(self):
402 r"""
403 Cumulative sum of squares of standardized recursive residuals
404 statistics
406 Returns
407 -------
408 cusum_squares : array_like
409 An array of length `nobs - k_exog` holding the
410 CUSUM of squares statistics.
412 Notes
413 -----
414 The CUSUM of squares statistic takes the form:
416 .. math::
418 s_t = \left ( \sum_{j=k+1}^t w_j^2 \right ) \Bigg /
419 \left ( \sum_{j=k+1}^T w_j^2 \right )
421 where :math:`w_j` is the recursive residual at time :math:`j`.
423 Excludes the first `k_exog` datapoints.
425 References
426 ----------
427 .. [*] Brown, R. L., J. Durbin, and J. M. Evans. 1975.
428 "Techniques for Testing the Constancy of
429 Regression Relationships over Time."
430 Journal of the Royal Statistical Society.
431 Series B (Methodological) 37 (2): 149-92.
432 """
433 d = max(self.nobs_diffuse, self.loglikelihood_burn)
434 numer = np.cumsum(self.resid_recursive[d:]**2)
435 denom = numer[-1]
436 return numer / denom
438 @cache_readonly
439 def llf_recursive_obs(self):
440 """
441 (float) Loglikelihood at observation, computed from recursive residuals
442 """
443 from scipy.stats import norm
444 return np.log(norm.pdf(self.resid_recursive, loc=0,
445 scale=self.scale**0.5))
447 @cache_readonly
448 def llf_recursive(self):
449 """
450 (float) Loglikelihood defined by recursive residuals, equivalent to OLS
451 """
452 return np.sum(self.llf_recursive_obs)
454 @cache_readonly
455 def ssr(self):
456 """ssr"""
457 d = max(self.nobs_diffuse, self.loglikelihood_burn)
458 return (self.nobs - d) * self.filter_results.obs_cov[0, 0, 0]
460 @cache_readonly
461 def centered_tss(self):
462 """Centered tss"""
463 return np.sum((self.filter_results.endog[0] -
464 np.mean(self.filter_results.endog))**2)
466 @cache_readonly
467 def uncentered_tss(self):
468 """uncentered tss"""
469 return np.sum((self.filter_results.endog[0])**2)
471 @cache_readonly
472 def ess(self):
473 """ess"""
474 if self.k_constant:
475 return self.centered_tss - self.ssr
476 else:
477 return self.uncentered_tss - self.ssr
479 @cache_readonly
480 def rsquared(self):
481 """rsquared"""
482 if self.k_constant:
483 return 1 - self.ssr / self.centered_tss
484 else:
485 return 1 - self.ssr / self.uncentered_tss
487 @cache_readonly
488 def mse_model(self):
489 """mse_model"""
490 return self.ess / self.df_model
492 @cache_readonly
493 def mse_resid(self):
494 """mse_resid"""
495 return self.ssr / self.df_resid
497 @cache_readonly
498 def mse_total(self):
499 """mse_total"""
500 if self.k_constant:
501 return self.centered_tss / (self.df_resid + self.df_model)
502 else:
503 return self.uncentered_tss / (self.df_resid + self.df_model)
505 @Appender(MLEResults.get_prediction.__doc__)
506 def get_prediction(self, start=None, end=None, dynamic=False,
507 index=None, **kwargs):
508 # Note: need to override this, because we currently do not support
509 # dynamic prediction or forecasts when there are constraints.
510 if start is None:
511 start = self.model._index[0]
513 # Handle start, end, dynamic
514 start, end, out_of_sample, prediction_index = (
515 self.model._get_prediction_index(start, end, index))
517 # Handle `dynamic`
518 if isinstance(dynamic, (bytes, str)):
519 dynamic, _, _ = self.model._get_index_loc(dynamic)
521 if self.model._r_matrix is not None and (out_of_sample or dynamic):
522 raise NotImplementedError('Cannot yet perform out-of-sample or'
523 ' dynamic prediction in models with'
524 ' constraints.')
526 # Perform the prediction
527 # This is a (k_endog x npredictions) array; do not want to squeeze in
528 # case of npredictions = 1
529 prediction_results = self.filter_results.predict(
530 start, end + out_of_sample + 1, dynamic, **kwargs)
532 # Return a new mlemodel.PredictionResults object
533 res_obj = PredictionResults(self, prediction_results,
534 row_labels=prediction_index)
535 return PredictionResultsWrapper(res_obj)
537 def plot_recursive_coefficient(self, variables=0, alpha=0.05,
538 legend_loc='upper left', fig=None,
539 figsize=None):
540 r"""
541 Plot the recursively estimated coefficients on a given variable
543 Parameters
544 ----------
545 variables : {int, str, list[int], list[str]}, optional
546 Integer index or string name of the variable whose coefficient will
547 be plotted. Can also be an iterable of integers or strings. Default
548 is the first variable.
549 alpha : float, optional
550 The confidence intervals for the coefficient are (1 - alpha) %
551 legend_loc : str, optional
552 The location of the legend in the plot. Default is upper left.
553 fig : Figure, optional
554 If given, subplots are created in this figure instead of in a new
555 figure. Note that the grid will be created in the provided
556 figure using `fig.add_subplot()`.
557 figsize : tuple, optional
558 If a figure is created, this argument allows specifying a size.
559 The tuple is (width, height).
561 Notes
562 -----
563 All plots contain (1 - `alpha`) % confidence intervals.
564 """
565 # Get variables
566 if isinstance(variables, (int, str)):
567 variables = [variables]
568 k_variables = len(variables)
570 # If a string was given for `variable`, try to get it from exog names
571 exog_names = self.model.exog_names
572 for i in range(k_variables):
573 variable = variables[i]
574 if isinstance(variable, str):
575 variables[i] = exog_names.index(variable)
577 # Create the plot
578 from scipy.stats import norm
579 from statsmodels.graphics.utils import _import_mpl, create_mpl_fig
580 plt = _import_mpl()
581 fig = create_mpl_fig(fig, figsize)
583 for i in range(k_variables):
584 variable = variables[i]
585 ax = fig.add_subplot(k_variables, 1, i + 1)
587 # Get dates, if applicable
588 if hasattr(self.data, 'dates') and self.data.dates is not None:
589 dates = self.data.dates._mpl_repr()
590 else:
591 dates = np.arange(self.nobs)
592 d = max(self.nobs_diffuse, self.loglikelihood_burn)
594 # Plot the coefficient
595 coef = self.recursive_coefficients
596 ax.plot(dates[d:], coef.filtered[variable, d:],
597 label='Recursive estimates: %s' % exog_names[variable])
599 # Legend
600 handles, labels = ax.get_legend_handles_labels()
602 # Get the critical value for confidence intervals
603 if alpha is not None:
604 critical_value = norm.ppf(1 - alpha / 2.)
606 # Plot confidence intervals
607 std_errors = np.sqrt(coef.filtered_cov[variable, variable, :])
608 ci_lower = (
609 coef.filtered[variable] - critical_value * std_errors)
610 ci_upper = (
611 coef.filtered[variable] + critical_value * std_errors)
612 ci_poly = ax.fill_between(
613 dates[d:], ci_lower[d:], ci_upper[d:], alpha=0.2
614 )
615 ci_label = ('$%.3g \\%%$ confidence interval'
616 % ((1 - alpha)*100))
618 # Only add CI to legend for the first plot
619 if i == 0:
620 # Proxy artist for fill_between legend entry
621 # See https://matplotlib.org/1.3.1/users/legend_guide.html
622 p = plt.Rectangle((0, 0), 1, 1,
623 fc=ci_poly.get_facecolor()[0])
625 handles.append(p)
626 labels.append(ci_label)
628 ax.legend(handles, labels, loc=legend_loc)
630 # Remove xticks for all but the last plot
631 if i < k_variables - 1:
632 ax.xaxis.set_ticklabels([])
634 fig.tight_layout()
636 return fig
638 def _cusum_significance_bounds(self, alpha, ddof=0, points=None):
639 """
640 Parameters
641 ----------
642 alpha : float, optional
643 The significance bound is alpha %.
644 ddof : int, optional
645 The number of periods additional to `k_exog` to exclude in
646 constructing the bounds. Default is zero. This is usually used
647 only for testing purposes.
648 points : iterable, optional
649 The points at which to evaluate the significance bounds. Default is
650 two points, beginning and end of the sample.
652 Notes
653 -----
654 Comparing against the cusum6 package for Stata, this does not produce
655 exactly the same confidence bands (which are produced in cusum6 by
656 lw, uw) because they burn the first k_exog + 1 periods instead of the
657 first k_exog. If this change is performed
658 (so that `tmp = (self.nobs - d - 1)**0.5`), then the output here
659 matches cusum6.
661 The cusum6 behavior does not seem to be consistent with
662 Brown et al. (1975); it is likely they did that because they needed
663 three initial observations to get the initial OLS estimates, whereas
664 we do not need to do that.
665 """
666 # Get the constant associated with the significance level
667 if alpha == 0.01:
668 scalar = 1.143
669 elif alpha == 0.05:
670 scalar = 0.948
671 elif alpha == 0.10:
672 scalar = 0.950
673 else:
674 raise ValueError('Invalid significance level.')
676 # Get the points for the significance bound lines
677 d = max(self.nobs_diffuse, self.loglikelihood_burn)
678 tmp = (self.nobs - d - ddof)**0.5
680 def upper_line(x):
681 return scalar * tmp + 2 * scalar * (x - d) / tmp
683 if points is None:
684 points = np.array([d, self.nobs])
685 return -upper_line(points), upper_line(points)
687 def plot_cusum(self, alpha=0.05, legend_loc='upper left',
688 fig=None, figsize=None):
689 r"""
690 Plot the CUSUM statistic and significance bounds.
692 Parameters
693 ----------
694 alpha : float, optional
695 The plotted significance bounds are alpha %.
696 legend_loc : str, optional
697 The location of the legend in the plot. Default is upper left.
698 fig : Figure, optional
699 If given, subplots are created in this figure instead of in a new
700 figure. Note that the grid will be created in the provided
701 figure using `fig.add_subplot()`.
702 figsize : tuple, optional
703 If a figure is created, this argument allows specifying a size.
704 The tuple is (width, height).
706 Notes
707 -----
708 Evidence of parameter instability may be found if the CUSUM statistic
709 moves out of the significance bounds.
711 References
712 ----------
713 .. [*] Brown, R. L., J. Durbin, and J. M. Evans. 1975.
714 "Techniques for Testing the Constancy of
715 Regression Relationships over Time."
716 Journal of the Royal Statistical Society.
717 Series B (Methodological) 37 (2): 149-92.
718 """
719 # Create the plot
720 from statsmodels.graphics.utils import _import_mpl, create_mpl_fig
721 _import_mpl()
722 fig = create_mpl_fig(fig, figsize)
723 ax = fig.add_subplot(1, 1, 1)
725 # Get dates, if applicable
726 if hasattr(self.data, 'dates') and self.data.dates is not None:
727 dates = self.data.dates._mpl_repr()
728 else:
729 dates = np.arange(self.nobs)
730 d = max(self.nobs_diffuse, self.loglikelihood_burn)
732 # Plot cusum series and reference line
733 ax.plot(dates[d:], self.cusum, label='CUSUM')
734 ax.hlines(0, dates[d], dates[-1], color='k', alpha=0.3)
736 # Plot significance bounds
737 lower_line, upper_line = self._cusum_significance_bounds(alpha)
738 ax.plot([dates[d], dates[-1]], upper_line, 'k--',
739 label='%d%% significance' % (alpha * 100))
740 ax.plot([dates[d], dates[-1]], lower_line, 'k--')
742 ax.legend(loc=legend_loc)
744 return fig
746 def _cusum_squares_significance_bounds(self, alpha, points=None):
747 """
748 Notes
749 -----
750 Comparing against the cusum6 package for Stata, this does not produce
751 exactly the same confidence bands (which are produced in cusum6 by
752 lww, uww) because they use a different method for computing the
753 critical value; in particular, they use tabled values from
754 Table C, pp. 364-365 of "The Econometric Analysis of Time Series"
755 Harvey, (1990), and use the value given to 99 observations for any
756 larger number of observations. In contrast, we use the approximating
757 critical values suggested in Edgerton and Wells (1994) which allows
758 computing relatively good approximations for any number of
759 observations.
760 """
761 # Get the approximate critical value associated with the significance
762 # level
763 d = max(self.nobs_diffuse, self.loglikelihood_burn)
764 n = 0.5 * (self.nobs - d) - 1
765 try:
766 ix = [0.1, 0.05, 0.025, 0.01, 0.005].index(alpha / 2)
767 except ValueError:
768 raise ValueError('Invalid significance level.')
769 scalars = _cusum_squares_scalars[:, ix]
770 crit = scalars[0] / n**0.5 + scalars[1] / n + scalars[2] / n**1.5
772 # Get the points for the significance bound lines
773 if points is None:
774 points = np.array([d, self.nobs])
775 line = (points - d) / (self.nobs - d)
777 return line - crit, line + crit
779 def plot_cusum_squares(self, alpha=0.05, legend_loc='upper left',
780 fig=None, figsize=None):
781 r"""
782 Plot the CUSUM of squares statistic and significance bounds.
784 Parameters
785 ----------
786 alpha : float, optional
787 The plotted significance bounds are alpha %.
788 legend_loc : str, optional
789 The location of the legend in the plot. Default is upper left.
790 fig : Figure, optional
791 If given, subplots are created in this figure instead of in a new
792 figure. Note that the grid will be created in the provided
793 figure using `fig.add_subplot()`.
794 figsize : tuple, optional
795 If a figure is created, this argument allows specifying a size.
796 The tuple is (width, height).
798 Notes
799 -----
800 Evidence of parameter instability may be found if the CUSUM of squares
801 statistic moves out of the significance bounds.
803 Critical values used in creating the significance bounds are computed
804 using the approximate formula of [1]_.
806 References
807 ----------
808 .. [*] Brown, R. L., J. Durbin, and J. M. Evans. 1975.
809 "Techniques for Testing the Constancy of
810 Regression Relationships over Time."
811 Journal of the Royal Statistical Society.
812 Series B (Methodological) 37 (2): 149-92.
813 .. [1] Edgerton, David, and Curt Wells. 1994.
814 "Critical Values for the Cusumsq Statistic
815 in Medium and Large Sized Samples."
816 Oxford Bulletin of Economics and Statistics 56 (3): 355-65.
817 """
818 # Create the plot
819 from statsmodels.graphics.utils import _import_mpl, create_mpl_fig
820 _import_mpl()
821 fig = create_mpl_fig(fig, figsize)
822 ax = fig.add_subplot(1, 1, 1)
824 # Get dates, if applicable
825 if hasattr(self.data, 'dates') and self.data.dates is not None:
826 dates = self.data.dates._mpl_repr()
827 else:
828 dates = np.arange(self.nobs)
829 d = max(self.nobs_diffuse, self.loglikelihood_burn)
831 # Plot cusum series and reference line
832 ax.plot(dates[d:], self.cusum_squares, label='CUSUM of squares')
833 ref_line = (np.arange(d, self.nobs) - d) / (self.nobs - d)
834 ax.plot(dates[d:], ref_line, 'k', alpha=0.3)
836 # Plot significance bounds
837 lower_line, upper_line = self._cusum_squares_significance_bounds(alpha)
838 ax.plot([dates[d], dates[-1]], upper_line, 'k--',
839 label='%d%% significance' % (alpha * 100))
840 ax.plot([dates[d], dates[-1]], lower_line, 'k--')
842 ax.legend(loc=legend_loc)
844 return fig
847class RecursiveLSResultsWrapper(MLEResultsWrapper):
848 _attrs = {}
849 _wrap_attrs = wrap.union_dicts(MLEResultsWrapper._wrap_attrs,
850 _attrs)
851 _methods = {}
852 _wrap_methods = wrap.union_dicts(MLEResultsWrapper._wrap_methods,
853 _methods)
854wrap.populate_wrapper(RecursiveLSResultsWrapper, # noqa:E305
855 RecursiveLSResults)