Introduction to sysbreak

Cheolju Kim, Pierre Perron, Zhongjun Qu

2026-10-20

Overview

The sysbreak package provides methods for detecting, estimating, and conducting inference on multiple structural breaks in a linear system of equations, such as multivariate linear regressions, seemingly unrelated regressions, vector autoregressions, and network vector autoregressions. Breaks can occur in the regression coefficients, the covariance matrix of the errors, or both, and can be restricted to a subset of the coefficients.

For observation \(t\) in regime \(j\),

\[ y_t = Z_t \beta_j + u_t, \quad t = T_{j-1}+1,\ldots,T_j, \quad j = 1,\ldots,m+1. \]

The unknown break dates are \(T_1,\ldots,T_m\). The package searches over admissible partitions subject to a trimming rule. It determines the number of breaks, gives the estimated break dates, the regime-specific parameters, and confidence intervals.

The main reference is Qu and Perron (2007), which develops estimation and testing for structural changes in multivariate regressions.

Main Functions

The main user-facing functions are:

Basic Example

This example uses simulated data with two breaks.

library(sysbreak)

set.seed(3)
bigt <- 60
x <- rnorm(bigt)

beta <- rbind(
  c(1.0,  0.5),
  c(2.0, -0.2),
  c(0.5,  0.8)
)

y <- numeric(bigt)
y[1:20]  <- beta[1, 1] + beta[1, 2] * x[1:20]  + rnorm(20, sd = 0.4)
y[21:40] <- beta[2, 1] + beta[2, 2] * x[21:40] + rnorm(20, sd = 0.4)
y[41:60] <- beta[3, 1] + beta[3, 2] * x[41:60] + rnorm(20, sd = 0.4)

df <- data.frame(y = y, x = x)

The model is given as a formula through model, with the data supplied in data. The intercept is included automatically. Which coefficients are allowed to change is controlled by coef_breaks; the default "all" lets every coefficient change across regimes. The model is estimated by:

fit <- sysbreak(y ~ x, data = df, m = 2)

The results are summarized by:

summary(fit)
#> sysbreak: 1 equation, T = 60, m = 2, trim = 0.15, robust = none
#> Covariance matrix: constant across regimes
#> 
#> Tests for the number of breaks (5% level)
#>            Statistic Critical value  
#>   WDmax       77.595         12.594 *
#>   SEQ(2|1)    63.080         13.472 *
#> Breaks selected: 2
#> 
#> Break dates (rows of the data) with 95% confidence intervals
#>           Estimate Lower Upper
#>   Break 1       20    19    21
#>   Break 2       40    39    41
#> 
#> Regime 1 (rows 1 to 20)
#>               Estimate Std. Error z value Pr(>|z|)    
#> y:(Intercept)  1.10076    0.06468  17.018  < 2e-16 ***
#> y:x            0.45801    0.08286   5.527 3.25e-08 ***
#> 
#> Regime 2 (rows 21 to 40)
#>               Estimate Std. Error z value Pr(>|z|)    
#> y:(Intercept)  2.04581    0.06330   32.32  < 2e-16 ***
#> y:x           -0.35734    0.07308   -4.89 1.01e-06 ***
#> 
#> Regime 3 (rows 41 to 60)
#>               Estimate Std. Error z value Pr(>|z|)    
#> y:(Intercept)  0.51638    0.06426   8.036 9.29e-16 ***
#> y:x            0.75479    0.06037  12.504  < 2e-16 ***
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

The output reports the tests used to select the number of breaks (WDmax and the sequential test of one against two breaks), the selected number of breaks, the break dates with their confidence intervals, and the coefficient estimates in each regime. Both tests reject, and two breaks are selected. The break dates and their confidence intervals are reported as rows of df. They are returned by confint(), and the regime coefficients by coef(); their covariance matrix is returned by vcov():

confint(fit)
#>        2.5 % break 97.5 %
#> break1    19    20     21
#> break2    39    40     41
coef(fit)
#> regime1:y:(Intercept)           regime1:y:x regime2:y:(Intercept) 
#>             1.1007640             0.4580116             2.0458059 
#>           regime2:y:x regime3:y:(Intercept)           regime3:y:x 
#>            -0.3573438             0.5163846             0.7547884

The fit can be plotted. Each series is plotted with the estimated break dates, their confidence intervals and the fitted values:

plot(fit)

The SupLR statistics for one and two breaks, together with the other test results, are stored in fit$testing; see the next section.

Output Structure

The methods shown above extract the main results; residuals(), fitted() and nobs() are also available. The components returned by sysbreak(), varbreak(), and netbreak() are:

varbreak() and netbreak() also return spillover when h is supplied and at least one break is selected.

Restricted Estimation

Instead of letting every coefficient change, the breaks can be restricted to a subset of the coefficients. The argument coef_breaks then takes a list of formulas, one per equation: the left-hand side identifies the equation and the right-hand side the coefficients that are allowed to change. All other coefficients are held constant across regimes.

To illustrate, the data below keep the intercept at 1 in all regimes and let only the slope on x change, at the same dates as before:

set.seed(13)
y_r <- 1 + rep(c(0.5, -0.2, 0.8), each = 20) * x + rnorm(bigt, sd = 0.4)
df_r <- data.frame(y = y_r, x = x)

The model with only the slope allowed to change is estimated by:

restricted_fit <- sysbreak(y ~ x, data = df_r, m = 2, coef_breaks = list(y ~ x))
summary(restricted_fit)
#> sysbreak: 1 equation, T = 60, m = 2, trim = 0.15, robust = none
#> Covariance matrix: constant across regimes
#> 
#> Tests for the number of breaks (5% level)
#>            Statistic Critical value  
#>   WDmax       33.034          9.567 *
#>   SEQ(2|1)    24.074         10.187 *
#> Breaks selected: 2
#> 
#> Break dates (rows of the data) with 95% confidence intervals
#>           Estimate Lower Upper
#>   Break 1       20    17    23
#>   Break 2       40    38    42
#> 
#> Coefficients allowed to change: y:x
#> 
#> Regime 1 (rows 1 to 20)
#>               Estimate Std. Error z value Pr(>|z|)    
#> y:(Intercept)  1.01180    0.04807  21.048  < 2e-16 ***
#> y:x            0.55702    0.10600   5.255 1.48e-07 ***
#> 
#> Regime 2 (rows 21 to 40)
#>               Estimate Std. Error z value Pr(>|z|)    
#> y:(Intercept)  1.01180    0.04807  21.048  < 2e-16 ***
#> y:x           -0.30795    0.09485  -3.247  0.00117 ** 
#> 
#> Regime 3 (rows 41 to 60)
#>               Estimate Std. Error z value Pr(>|z|)    
#> y:(Intercept)  1.01180    0.04807   21.05   <2e-16 ***
#> y:x            0.80879    0.07757   10.43   <2e-16 ***
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Two breaks are selected, at rows 20 and 40. The critical values are smaller than in the basic example, because fewer coefficients change. The line Coefficients allowed to change lists the coefficients that change; the intercept is estimated from the whole sample and is therefore the same in the three regimes. The returned components describe this restricted model; an unrestricted fit is not returned.

A pure covariance-break model (no coefficient changes) is obtained with coef_breaks = "none" together with brv = TRUE. With robust = "hc" or "hac", a covariance-only robust test is not implemented, so the test statistic and its type are returned as missing.

Further Options

The examples above use the default settings. The following arguments, common to sysbreak(), varbreak() and netbreak(), can be used to alter how the model is estimated and how inference is carried out.

For example, the basic model with inference robust to serial correlation and 90% confidence intervals is estimated by:

fit_hac <- sysbreak(y ~ x, data = df, m = 2, robust = "hac", ci_level = 0.90)

VAR Example and Types of Breaks

varbreak() estimates structural breaks in a VAR. Its first argument is the matrix of the series, and the lag order is set by lags. The data below follow a bivariate VAR(1) whose coefficients are the same throughout, while at row 100 the volatility of y1 doubles and the correlation of the errors rises from 0.2 to 0.7:

set.seed(14)
bigt_var <- 200
A <- matrix(c(0.5, 0.1, 0.2, 0.4), 2, 2)
chol1 <- t(chol(matrix(c(1, 0.2, 0.2, 1), 2, 2)))
chol2 <- t(chol(matrix(c(4, 1.4, 1.4, 1), 2, 2)))
y_var <- matrix(0, bigt_var, 2, dimnames = list(NULL, c("y1", "y2")))
for (tt in 2:bigt_var) {
  chol_t <- if (tt <= 100) chol1 else chol2
  y_var[tt, ] <- A %*% y_var[tt - 1, ] + chol_t %*% rnorm(2)
}

The model lets both the coefficients and the covariance matrix change (brv = TRUE). hetq = TRUE is used because the distribution of the lagged series changes with the volatility:

var_fit <- varbreak(y_var, lags = 1, m = 2, brv = TRUE, hetq = TRUE)
confint(var_fit)
#>        2.5 % break 97.5 %
#> break1    99   100    115

One break is selected, at row 100. brtype() then tests, at each break date, whether the volatilities, the correlations and the coefficients differ between the two adjacent regimes:

round(brtype(var_fit), 4)
#>   breaks volatility volatility_p correlation correlation_p coefficients
#> 1    100    55.5739            0     20.7622             0       4.9234
#>   coefficients_p
#> 1         0.5537

The volatility and correlation tests reject, with p-values below 0.0001, while the coefficient test does not (p-value 0.55). The break is in the covariance matrix only, as in the simulated data.

Network VAR Example

netbreak() estimates structural breaks in a Network VAR (Zhu et al., 2017) whose transition matrices are restricted by a fixed network matrix:

\[ y_t = \mu_j + \sum_{\ell=1}^p (\alpha_{\ell,j} W + \beta_{\ell,j} I)y_{t-\ell} + u_t . \]

The argument coef_breaks controls which low-dimensional Network VAR parameters are allowed to change across regimes.

set.seed(5)
bigt_nvar <- 80
W <- matrix(c(0, 1, 1, 0), 2, 2, byrow = TRUE)
y_nvar <- matrix(0, bigt_nvar, 2)
colnames(y_nvar) <- c("node1", "node2")

for (tt in 2:bigt_nvar) {
  alpha <- if (tt <= 40) 0.05 else 0.75
  beta <- 0.1
  y_nvar[tt, ] <- alpha * as.numeric(W %*% y_nvar[tt - 1, ]) +
    beta * y_nvar[tt - 1, ] + rnorm(2, sd = 0.2)
}

nvar_fit <- netbreak(
  y = y_nvar,
  W = W,
  lags = 1,
  m = 1,
  coef_breaks = c("network", "own"),
  trim = 0.2,
  h = 10
)

nvar_fit$estimation$breaks
#> [1] 45
nvar_fit$spillover$regimes[[1]]$total
#> [1] 2.193206

The same spillover engine can be called after estimation:

spill <- dyspill(nvar_fit, h = 10)
spill$regimes[[1]]$fevd
spill$regimes[[1]]$from
spill$regimes[[1]]$to
spill$regimes[[1]]$net

Ordered Two-Break Example

ordbreak() estimates an ordered two-break model in which the equations in set1 break at the first date and those in set2 at the second. The test is that of Qu and Perron (2007, Section 6.4) for locally ordered breaks. The test for globally ordered breaks in their Section 6.5, whose limiting distribution differs, is not implemented.

set.seed(13)
bigt2 <- 60
x1 <- rnorm(bigt2)
x2 <- rnorm(bigt2)
y1 <- 1 + 0.5 * x1 + rnorm(bigt2, sd = 0.3)
y2 <- 1 + 0.5 * x2 + rnorm(bigt2, sd = 0.3)
y1[21:bigt2] <- y1[21:bigt2] + 1.5 * x1[21:bigt2]
y2[36:bigt2] <- y2[36:bigt2] - 1.5 * x2[36:bigt2]
df2 <- data.frame(y1 = y1, y2 = y2, x1 = x1, x2 = x2)

ordered_fit <- ordbreak(y1 ~ x1, y2 ~ x2, data = df2, trim = 0.2)

ordered_fit$estimation$breaks
#> [1] 20 35

Data Sets

The package includes five data sets, each documented on its help page (for example, ?housing):

Critical Values

Critical values are computed internally, following Bai and Perron (2003), and reported at the significance level test_level (default 0.05). In the basic example above, fit$testing$critical_values has one entry for each candidate number of breaks \(1,\ldots,m\); the critical value of the WDmax test is fit$testing$wdmax_critical_values, and that of each sequential test is stored in the corresponding fit$testing$sequential[[l]] entry. Another level is obtained by refitting with a different test_level (0.10, 0.05, 0.025, or 0.01).

References

Qu, Z., and Perron, P. (2007). Estimating and testing structural changes in multivariate regressions. Econometrica, 75(2), 459-502.

Bataa, E., Osborn, D. R., Sensier, M., and van Dijk, D. (2013). Structural breaks in the international dynamics of inflation. Review of Economics and Statistics, 95(2), 646-659.

Bai, J., and Perron, P. (2003). Critical values for multiple structural change tests. The Econometrics Journal, 6(1), 72-78.

Zhu, X., Pan, R., Li, G., Liu, Y., and Wang, H. (2017). Network vector autoregression. The Annals of Statistics, 45(3), 1096-1123.

Diebold, F. X., and Yilmaz, K. (2012). Better to give than to receive: Predictive directional measurement of volatility spillovers. International Journal of Forecasting, 28(1), 57-66.

Ahamada, I., and Diaz Sanchez, J. L. (2013). A retrospective analysis of the house price macro-relationship in the United States. International Journal of Central Banking, 9(4), 153-174.

Han, Y., and Lee, T. C. M. (2024). Structural break detection in non-stationary network vector autoregression models. IEEE Transactions on Network Science and Engineering, 11(5), 4134-4145.

Andrews, D. W. K. (1991). Heteroskedasticity and autocorrelation consistent covariance matrix estimation. Econometrica, 59(3), 817-858.

Session Info

sessionInfo()
#> R version 4.4.3 (2025-02-28 ucrt)
#> Platform: x86_64-w64-mingw32/x64
#> Running under: Windows 11 x64 (build 26200)
#> 
#> Matrix products: default
#> 
#> 
#> locale:
#> [1] LC_COLLATE=C                          
#> [2] LC_CTYPE=English_United States.utf8   
#> [3] LC_MONETARY=English_United States.utf8
#> [4] LC_NUMERIC=C                          
#> [5] LC_TIME=English_United States.utf8    
#> 
#> time zone: America/New_York
#> tzcode source: internal
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] sysbreak_1.1.0
#> 
#> loaded via a namespace (and not attached):
#>  [1] digest_0.6.37     R6_2.6.1          fastmap_1.2.0     xfun_0.53        
#>  [5] cachem_1.1.0      knitr_1.50        htmltools_0.5.8.1 rmarkdown_2.29   
#>  [9] lifecycle_1.0.4   cli_3.6.4         sass_0.4.9        jquerylib_0.1.4  
#> [13] compiler_4.4.3    rstudioapi_0.17.1 tools_4.4.3       evaluate_1.0.3   
#> [17] bslib_0.9.0       yaml_2.3.10       rlang_1.1.5       jsonlite_1.9.1   
#> [21] MASS_7.3-64