cusum#

cultivars.diagnostics.breaks.cusum(endog, exog=None, *, trend='c', squares=False, n_draws=5000, seed=None)[source]#

CUSUM and CUSUM-of-squares tests of coefficient constancy.

The recursive residuals of Brown, Durbin and Evans (1975) are the one-step forecast errors of the regression re-estimated as each observation arrives, standardized by their forecast variance,

\[w_t = \frac{y_t - x_t^\top \hat\beta_{t-1}} {\sqrt{1 + x_t^\top (X_{t-1}^\top X_{t-1})^{-1} x_t}}, \qquad t = q + 1, \ldots, T,\]

so that under constant coefficients and Gaussian errors they are i.i.d. \(\mathcal{N}(0, \sigma^2)\). The CUSUM cumulates them, \(W_r = \sum_{t \le r} w_t / \hat\sigma\) with \(\hat\sigma\) the residuals’ standard deviation about their mean, and compares the path with the linear boundary \(\pm a \sqrt{n}\,(1 + 2r / n)\) over the \(n = T - q\) steps; the statistic is the largest ratio \(|W_r|\) to the boundary shape, and it reads directly against the tabulated multipliers \(a = 1.143, 0.948, 0.850\) at 1%, 5%, and 10%. A drift in the coefficients makes the forecast errors one-signed and the path leave the band. The CUSUM of squares cumulates the share \(s_r = \sum_{t \le r} w_t^2 / \sum_t w_t^2\) against its expectation \(r / n\) and takes the largest deviation; it is the sharper test for a change in variance, and since under the null the path is a function of i.i.d. Gaussian draws alone its finite-sample boundary is simulated exactly from n_draws such paths, which gives it a p-value the CUSUM proper does not have.

Parameters:
  • endog (ArrayLike) – The target, (T,).

  • exog (ArrayLike | None) – Regressors (T, k) or (T,), or None.

  • trend (str) – Deterministic terms, "n", "c" or "ct".

  • squares (bool) – Whether to run the CUSUM-of-squares test instead of the CUSUM.

  • n_draws (int) – Simulated paths for the CUSUM-of-squares boundary, at least 500; unused by the CUSUM.

  • seed (int | Generator | None) – Seed or generator for that simulation.

Return type:

BreakTest

Returns:

The BreakTest with the (T - q,) path and the (3, T - q) boundary at the tabulated levels; the CUSUM test carries no p-value and reads against its critical values, the squared one carries both.

Raises:
  • SpecificationError – If the trend is unknown or n_draws is too small.

  • DimensionError – If the regressors do not align with the target.

  • NumericalError – If a value is not finite, the design is rank deficient, or the first q observations do not identify the regression.

Note

break_index is the observation at which the path first leaves the 5% band, not an estimate of the break date: the CUSUM crossing lags a break, since the cumulated errors take time to drift out, and the CUSUM-of-squares crossing can precede one, since the share is normalized by the whole-sample sum and a late rise in variance depresses the early path. Use sup_wald() or bai_perron() to date a break the CUSUM has flagged. The CUSUM has low power against a break in a coefficient whose regressor averages zero over the sample, which is why the two tests are offered together.

See also

  • BreakTest – the record, whose path and bounds draw the familiar CUSUM plot.

  • sup_wald() – the Andrews test, which dates the break and carries a p-value.

  • bai_perron() – when more than one break is possible.

References

Brown, R. L., Durbin, J., & Evans, J. M. (1975). Techniques for testing the constancy of regression relationships over time. Journal of the Royal Statistical Society B, 37(2), 149-192.

Ploberger, W., & Krämer, W. (1992). The CUSUM test with OLS residuals. Econometrica, 60(2), 271-285.

Example

A mean shift at 100 is caught by the CUSUM; the crossing comes after the break:

>>> import numpy as np
>>> rng = np.random.default_rng(0)
>>> y = np.concatenate([rng.standard_normal(100), 2.0 + rng.standard_normal(100)])
>>> test = cusum(y)
>>> test
BreakTest(name='CUSUM', statistic=2.7283, pvalue=None, break_index=116, nobs=200)
>>> test.critical_values
{'1%': 1.143, '5%': 0.948, '10%': 0.85}
>>> test.reject(alpha=0.01)
True

A tripling of the error standard deviation at 150 leaves the CUSUM inside its band and is caught by the CUSUM of squares:

>>> y = np.concatenate([rng.standard_normal(150), 3.0 * rng.standard_normal(150)])
>>> cusum(y).reject(alpha=0.05), cusum(y, squares=True, seed=0).reject()
(False, True)