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_drawssuch 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,), orNone.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 least500; unused by the CUSUM.seed (
int|Generator|None) – Seed or generator for that simulation.
- Return type:
- Returns:
The
BreakTestwith 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_drawsis 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
qobservations do not identify the regression.
Note
break_indexis 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. Usesup_wald()orbai_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, whosepathandboundsdraw 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)