Hide keyboard shortcuts

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 

3 

4Author: Chad Fulton 

5License: Simplified-BSD 

6""" 

7 

8import numpy as np 

9import pandas as pd 

10 

11from statsmodels.compat.pandas import Appender 

12 

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 

21 

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]) 

28 

29 

30class RecursiveLS(MLEModel): 

31 r""" 

32 Recursive least squares 

33 

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. 

48 

49 Notes 

50 ----- 

51 Recursive least squares (RLS) corresponds to expanding window ordinary 

52 least squares (OLS). 

53 

54 This model applies the Kalman filter to compute recursive estimates of the 

55 coefficients and recursive residuals. 

56 

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) 

68 

69 exog_is_using_pandas = _is_using_pandas(exog, None) 

70 if not exog_is_using_pandas: 

71 exog = np.asarray(exog) 

72 

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) 

79 

80 self.k_exog = exog.shape[1] 

81 

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] 

93 

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] 

102 

103 # Handle coefficient initialization 

104 kwargs.setdefault('initialization', 'diffuse') 

105 

106 # Initialize the state space representation 

107 super(RecursiveLS, self).__init__( 

108 endog, k_states=self.k_exog, exog=exog, **kwargs) 

109 

110 # Use univariate filtering by default 

111 self.ssm.filter_univariate = True 

112 

113 # Concentrate the scale out of the likelihood function 

114 self.ssm.filter_concentrated = True 

115 

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) 

122 

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) 

127 

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 

133 

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) 

138 

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.') 

144 

145 def fit(self): 

146 """ 

147 Fits the model by application of the Kalman filter 

148 

149 Returns 

150 ------- 

151 RecursiveLSResults 

152 """ 

153 smoother_results = self.smooth(return_ssm=True) 

154 

155 with self.ssm.fixed_scale(smoother_results.scale): 

156 res = self.smooth() 

157 

158 return res 

159 

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) 

165 

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 ) 

180 

181 return result 

182 

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) 

188 

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 ) 

203 

204 return result 

205 

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 

210 

211 @property 

212 def param_names(self): 

213 return self.exog_names 

214 

215 @property 

216 def start_params(self): 

217 # Only parameter is the measurement disturbance standard deviation 

218 return np.zeros(0) 

219 

220 def update(self, params, **kwargs): 

221 """ 

222 Update the parameters of the model 

223 

224 Updates the representation matrices to fill in the new parameter 

225 values. 

226 

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.. 

234 

235 Returns 

236 ------- 

237 params : array_like 

238 Array of parameters. 

239 """ 

240 pass 

241 

242 

243class RecursiveLSResults(MLEResults): 

244 """ 

245 Class to hold results from fitting a recursive least squares model. 

246 

247 Parameters 

248 ---------- 

249 model : RecursiveLS instance 

250 The fitted model instance 

251 

252 Attributes 

253 ---------- 

254 specification : dictionary 

255 Dictionary including all attributes from the recursive least squares 

256 model instance. 

257 

258 See Also 

259 -------- 

260 statsmodels.tsa.statespace.kalman_filter.FilterResults 

261 statsmodels.tsa.statespace.mlemodel.MLEResults 

262 """ 

263 

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) 

268 

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 

274 

275 # Save _init_kwds 

276 self._init_kwds = self.model._get_init_kwds() 

277 

278 # Save the model specification 

279 self.specification = Bunch(**{ 

280 'k_exog': self.model.k_exog, 

281 'k_constraints': self.model.k_constraints}) 

282 

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]) 

289 

290 @property 

291 def recursive_coefficients(self): 

292 """ 

293 Estimates of regression coefficients, recursively estimated 

294 

295 Returns 

296 ------- 

297 out: Bunch 

298 Has the following attributes: 

299 

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 

327 

328 @cache_readonly 

329 def resid_recursive(self): 

330 r""" 

331 Recursive residuals 

332 

333 Returns 

334 ------- 

335 resid_recursive : array_like 

336 An array of length `nobs` holding the recursive 

337 residuals. 

338 

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. 

347 

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) 

356 

357 @cache_readonly 

358 def cusum(self): 

359 r""" 

360 Cumulative sum of standardized recursive residuals statistics 

361 

362 Returns 

363 ------- 

364 cusum : array_like 

365 An array of length `nobs - k_exog` holding the 

366 CUSUM statistics. 

367 

368 Notes 

369 ----- 

370 The CUSUM statistic takes the form: 

371 

372 .. math:: 

373 

374 W_t = \frac{1}{\hat \sigma} \sum_{j=k+1}^t w_j 

375 

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. 

379 

380 Excludes the first `k_exog` datapoints. 

381 

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) 

387 

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)) 

399 

400 @cache_readonly 

401 def cusum_squares(self): 

402 r""" 

403 Cumulative sum of squares of standardized recursive residuals 

404 statistics 

405 

406 Returns 

407 ------- 

408 cusum_squares : array_like 

409 An array of length `nobs - k_exog` holding the 

410 CUSUM of squares statistics. 

411 

412 Notes 

413 ----- 

414 The CUSUM of squares statistic takes the form: 

415 

416 .. math:: 

417 

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 ) 

420 

421 where :math:`w_j` is the recursive residual at time :math:`j`. 

422 

423 Excludes the first `k_exog` datapoints. 

424 

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 

437 

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)) 

446 

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) 

453 

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] 

459 

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) 

465 

466 @cache_readonly 

467 def uncentered_tss(self): 

468 """uncentered tss""" 

469 return np.sum((self.filter_results.endog[0])**2) 

470 

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 

478 

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 

486 

487 @cache_readonly 

488 def mse_model(self): 

489 """mse_model""" 

490 return self.ess / self.df_model 

491 

492 @cache_readonly 

493 def mse_resid(self): 

494 """mse_resid""" 

495 return self.ssr / self.df_resid 

496 

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) 

504 

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] 

512 

513 # Handle start, end, dynamic 

514 start, end, out_of_sample, prediction_index = ( 

515 self.model._get_prediction_index(start, end, index)) 

516 

517 # Handle `dynamic` 

518 if isinstance(dynamic, (bytes, str)): 

519 dynamic, _, _ = self.model._get_index_loc(dynamic) 

520 

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.') 

525 

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) 

531 

532 # Return a new mlemodel.PredictionResults object 

533 res_obj = PredictionResults(self, prediction_results, 

534 row_labels=prediction_index) 

535 return PredictionResultsWrapper(res_obj) 

536 

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 

542 

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). 

560 

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) 

569 

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) 

576 

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) 

582 

583 for i in range(k_variables): 

584 variable = variables[i] 

585 ax = fig.add_subplot(k_variables, 1, i + 1) 

586 

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) 

593 

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]) 

598 

599 # Legend 

600 handles, labels = ax.get_legend_handles_labels() 

601 

602 # Get the critical value for confidence intervals 

603 if alpha is not None: 

604 critical_value = norm.ppf(1 - alpha / 2.) 

605 

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)) 

617 

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]) 

624 

625 handles.append(p) 

626 labels.append(ci_label) 

627 

628 ax.legend(handles, labels, loc=legend_loc) 

629 

630 # Remove xticks for all but the last plot 

631 if i < k_variables - 1: 

632 ax.xaxis.set_ticklabels([]) 

633 

634 fig.tight_layout() 

635 

636 return fig 

637 

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. 

651 

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. 

660 

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.') 

675 

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 

679 

680 def upper_line(x): 

681 return scalar * tmp + 2 * scalar * (x - d) / tmp 

682 

683 if points is None: 

684 points = np.array([d, self.nobs]) 

685 return -upper_line(points), upper_line(points) 

686 

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. 

691 

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). 

705 

706 Notes 

707 ----- 

708 Evidence of parameter instability may be found if the CUSUM statistic 

709 moves out of the significance bounds. 

710 

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) 

724 

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) 

731 

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) 

735 

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--') 

741 

742 ax.legend(loc=legend_loc) 

743 

744 return fig 

745 

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 

771 

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) 

776 

777 return line - crit, line + crit 

778 

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. 

783 

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). 

797 

798 Notes 

799 ----- 

800 Evidence of parameter instability may be found if the CUSUM of squares 

801 statistic moves out of the significance bounds. 

802 

803 Critical values used in creating the significance bounds are computed 

804 using the approximate formula of [1]_. 

805 

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) 

823 

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) 

830 

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) 

835 

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--') 

841 

842 ax.legend(loc=legend_loc) 

843 

844 return fig 

845 

846 

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)