The jumps package implements the method described in
Maranzano and Pelagatti (2025), “A Hodrick-Prescott filter with
automatically selected breaks”, Economic Modelling 150, 107132,
doi:10.1016/j.econmod.2025.107132. The terms
jumps and breaks are used interchangeably in the
package documentation. Indeed, in the initial version of the article, we
used the term jumps, and one of the referees suggested that this term is
generally devoted to the jump processes used in mathematical finance,
while breaks is generally used in econometrics. We agreed with
the referee and decided to use the term breaks in the article’s final
version. However, we decided to keep the term jumps in the
package name and in the function names because we had already written
most of it.
We introduced an innovation to the well-known Hodrick-Prescott filter
(HPF) that allows for a small number of discontinuities in the otherwise
smooth filter. The number, positions and magnitudes of these
discontinuities, which we name breaks or jumps, are
automatically estimated from the data. The method is based on the
minimization of the sum of squared residuals of the HPF subject to a
penalty on the number of breaks. The penalty is chosen by the user and
can be set to zero, in which case the method reduces to the standard
HPF. The technique is implemented in the hpj function,
which is the main function of the package. The function hpj
is a wrapper for other functions that implement various variations of
the technique.
For efficiency, the package’s computational engine is written in C++ through the Rcpp package. The engine implements the Kalman filter and smoother for the state-space form underlying the HPF. All formulae can be found in the article and in the vignette titled Formulae.
Users needing higher control over the estimation process can use the
functions with names hpfj* and auto_hpfj*.
However, the wrapper function hpj makes the process simpler
and more stable. Indeed, good starting values are essential since the
technique is based on a complex numerical optimisation. Inside the
hpj function, we linearly transform the time series so that
the starting values in the hpfj* functions are expected to
work fine, and then we apply the anti-transform to all the results so
that they refer to the original time series. The functions
hpfj* and auto_hpfj* allow full control over
the starting values and penalties.
While hpfj* and auto_hpfj* accept only time
series stored in a numerical vector (any type of time series object is
cast to numerical vector), the hpj function accepts all the
main time series objects used in R (ts, zoo, xts, and timeSeries). All
the time series generated by the function receive the same class and
dates as the original time series.
The function is called as
hpj(y, maxsum = NULL, lambda = NULL, xreg = NULL, ic = c("bic", "hq", "aic", "aicc"), scl = 1000)
where y is the time series to be filtered,
maxsum is inverse penalty, when zero no jumps are allowed,
the larger maxsum the more and larger breaks are allowed,
lambda is the smoothing parameter of the HPF,
xreg is a matrix of regressors (we are still woring on
this, so, at the moment it is not yet implemented), and ic
is the information criterion used to select the penalty when
maxsum = NULL. When lambda = NULL, the
smoothing parameter is estimated by quasi maximum likelihood. The
parameter lambda can be a positive number or one of the
following strings (in parenthesis the value of the smoothing parameter
corresponding to the string): daily (110,930,628,906), weekly
(45,697,600), monthly (129,600), quarterly (1,600), annual (6.25).
The function returns an object of class hpj, which is a
list with the following slots:
y the input time series;maxsum the maximum sum of additional standard
deviations;lambda the smoothing constant of the HP filter;pars vector of estimated parameters (sigma_slope,
sigma_noise, gamma);hpj: the time series of the HP filter with jumps;hpj_std the time series of the HP filter with jumps
standard deviations;breaks vector of indices of the detected breaks;sigmas vector of additional standard deviations of the
level disturbance;xreg matrix of regressors;df model’s degrees of freedom;loglik value of the log-likelihood at maximum;ic vector of information criteria (aic, aicc, bic,
hq);opt the output of the optimization function
(nloptr);call the call to the function.The methods print and plot are available
for the hpj class. The plot method can be
customised:
plot(x, prob = NULL, show_breaks = TRUE, main = "original + filter", use_ggplot = TRUE, ...)
where x is the object of class hpj,
prob is the coverage of the confidence interval for the
filter, which is not plotted when prob = NULL,
show_breaks is a logical indicating whether to show the
breaks in the plot, main is the title of the plot,
use_ggplot is a logical indicating whether to use ggplot2,
and ... are additional arguments passed to the
plot function when the standard plot is used (that is when
use_ggplot = FALSE).
library(jumps)
set.seed(2025)
n <- 100
# simulated smooth trend
mu <- 100*cos(3*pi/n*(1:n)) - ((1:n) > 50)*n - c(rep(0, 50), 1:50)*10
# simulated time series
y <- mu + rnorm(n, sd = 20)
# HP filter with jumps with estimated lambda and fixed penalty (maxsum = 50)
hpj_sim <- hpj(y, maxsum = 50)
print(hpj_sim)
#> Hodrick-Prescott filter with jumps
#> Call:
#> hpj(y, maxsum = 50)
#> Parameters:
#> sd(slope) = 1.848931
#> sd(noise) = 20.67477
#> gamma = 0.03659672
#> lambda = 125.0375
#> maxsum = 50
#> Model's degrees of freedom: 12.44784
#> Log-likelihood: -492.3048
#> Information criteria:
#> AIC = 1009.505
#> AICc = 1013.373
#> BIC = 1041.934
#> HQ = 1022.63
#> Break dates:
#> 1 51
plot(hpj_sim)# HP filter with jumps with estimated lambda and automatically selected penalty
hpj_nile <- hpj(Nile)
print(hpj_nile)
#> Hodrick-Prescott filter with jumps
#> Call:
#> hpj(Nile)
#> Parameters:
#> sd(slope) = 1.692275e-07
#> sd(noise) = 127.634
#> gamma = 3.428068e-14
#> lambda = 5.688412e+17
#> maxsum = 270.764
#> Model's degrees of freedom: 2.949825
#> Log-likelihood: -644.0627
#> Information criteria:
#> AIC = 1294.025
#> AICc = 1294.268
#> BIC = 1301.71
#> HQ = 1297.135
#> Break dates:
#> 1 1899
plot(hpj_nile, main = "Nile river flow")
#> Don't know how to automatically pick scale for object of type <ts>. Defaulting
#> to continuous.plot(hpj_nile, prob = 0.95, main = "Nile river flow")
#> Don't know how to automatically pick scale for object of type <ts>. Defaulting
#> to continuous.data("employed_IT")
y <- window(employed_IT[, "Y25.29"], start = c(2009, 1))
hpj_emp <- hpj(y, scl = "original")
print(hpj_emp)
#> Hodrick-Prescott filter with jumps
#> Call:
#> hpj(y, scl = "original")
#> Parameters:
#> sd(slope) = 3.347636
#> sd(noise) = 21.13837
#> gamma = 17.3066
#> lambda = 39.87185
#> maxsum = 76.59153
#> Model's degrees of freedom: 12.26254
#> Log-likelihood: -301.9286
#> Information criteria:
#> AIC = 628.3823
#> AICc = 635.1959
#> BIC = 654.267
#> HQ = 638.5267
#> Break dates:
#> 1 2010.25
#> 2 2012
#> 3 2013.75
#> 4 2019.75
#> 5 2020.25
plot(hpj_emp, main = "Millions of employed in Italy: age 25-29")Besides the HP filter, the package fits cubic smoothing splines that allow a small number of discontinuities at unknown locations, following Pelagatti, Maranzano and Gatto Ronchieri (2026).
The idea is to leave the classical roughness penalty untouched and instead deform the domain over which it is computed. Every pair of consecutive observations is granted a non-negative domain warp \(\gamma_i \ge 0\), an amount of fictitious domain length inserted between them, so that the latent spacing becomes \(\tau_i = \delta_i + \gamma_i\) with \(\delta_i = x_{i+1} - x_i\). The spline is fitted, and its roughness penalised, on this warped domain. Where the signal is smooth, warping buys nothing and the optimal \(\gamma_i\) is zero, so the fit coincides with an ordinary smoothing spline; where the signal jumps, the same vertical change can be spread over a longer warped interval, which lowers its curvature cost dramatically. A budget \(\sum_i \gamma_i \le M\) keeps the warping concentrated on the few locations where a jump is actually present.
Four estimators are provided. ssj_mle() casts the model
in state-space form and estimates \((\sigma,
\lambda, \boldsymbol{\gamma})\) by maximum likelihood through the
Kalman filter; ssj_kfs() does the same with \(\lambda\) held fixed;
ssj_atw() is the fast alternating domain-warping algorithm,
of complexity \(O(n \log n)\) per
iteration; and ssj_fix() implements the earlier variant in
which the additional disturbance variances, rather than the spacings,
are the free parameters. The auto_* wrappers select the
budget \(M\) over a grid by information
criteria, EBIC by default. Two further wrappers,
auto_ssj_atw_gcv() and auto_ssj_atw_cv(), also
choose \(\lambda\), so that the
alternating algorithm does not need a pilot fit from
ssj_mle().
The strength of the EBIC search penalty, ebic_xi, has a
calibrated default that differs by estimator – 0.3, 0.15 and 0.25 for
ssj_mle(), ssj_kfs() and
ssj_atw(). They differ because the three are different
procedures, not because they fit different models; see the “Choosing
ebic_xi” section of each help page.
We use the test signal of Section 3 of the paper: a Bessel function of the first kind carrying three discontinuities, at \(x = 0.3\) (of size \(+0.3\)), \(x = 0.4\) (\(-0.4\)) and \(x = 0.6\) (\(-0.6\)), observed at 100 random locations with Gaussian noise.
g1 <- function(x) {
besselJ(20 * x, nu = 1) + x * (x >= 0.3 & x <= 0.4) - x * (x >= 0.6)
}
true_disc <- c(0.3, 0.4, 0.6)
set.seed(2026)
n <- 100
x <- sort(runif(n))
y <- g1(x) + rnorm(n, sd = 0.05)Two of the estimators are shown here, because they are the two a user
would actually choose between, and they reach the same answer by
different routes. auto_ssj_mle() estimates \((\sigma, \lambda)\) by maximum likelihood
through the Kalman filter and lets EBIC pick the warping budget;
auto_ssj_atw_cv() runs the fast alternating algorithm,
taking \(\lambda\) from a
cross-validation of its own and again leaving the budget to EBIC. The
second is self-contained: unlike the two-step recipe used in the paper’s
simulation study it needs no pilot fit from the likelihood-based
estimator. The ebic_xi defaults are calibrated and differ
by estimator; ?ssj_atw explains why, and nothing needs
overriding here.
fit_mle <- auto_ssj_mle(y = y, x = x, ic = "ebic")
fit_mle
#> Smoothing spline with jumps
#> Method: SSJ-MLE: domain warping, state-space maximum likelihood
#> Call:
#> auto_ssj_mle(y = y, x = x, ic = "ebic")
#>
#> Parameters:
#> sigma = 27.27
#> lambda = 3.534e-06
#> budget = 0.09764 (maximum sum of the domain warps)
#>
#> Observations: 100
#> Effective degrees of freedom: 26.17
#> Log-likelihood: 93.58
#> Information criteria:
#> AIC = -134.8
#> AICC = -115.3
#> BIC = -66.66
#> HQ = -107.2
#> EBIC = -39.09
#>
#> Declared discontinuities: 5
#> x jump warp
#> 32 0.2984 0.07775 0.008003
#> 33 0.3178 0.23885 0.006113
#> 44 0.4107 -0.25526 0.030401
#> 45 0.4124 -0.03780 0.006045
#> 64 0.6017 -0.42911 0.039998fit_atw <- auto_ssj_atw_cv(y = y, x = x, ic = "ebic")
fit_atw
#> Smoothing spline with jumps
#> Method: SSJ-ATW: alternating domain-warping algorithm
#> Call:
#> auto_ssj_atw_cv(y = y, x = x, ic = "ebic")
#>
#> Parameters:
#> sigma = 0.05091
#> lambda = 2.943e-06
#> budget = 0.09764 (maximum sum of the domain warps)
#>
#> Observations: 100
#> Effective degrees of freedom: 27.3
#> Log-likelihood: 169.5
#> Information criteria:
#> AIC = -572.8
#> AICC = -551.3
#> BIC = -501.7
#> HQ = -544.1
#> EBIC = -478.8
#>
#> Declared discontinuities: 5
#> x jump warp
#> 32 0.2984 0.08228 0.008554
#> 33 0.3178 0.23757 0.005578
#> 44 0.4107 -0.25368 0.027743
#> 45 0.4124 -0.04819 0.007752
#> 64 0.6017 -0.43060 0.037672The two smoothing constants agree closely, which is what makes the cross-validated route a usable replacement for the pilot:
c(lambda_mle = fit_mle$pars[["lambda"]],
lambda_cv = fit_atw$pars[["lambda"]])
#> lambda_mle lambda_cv
#> 3.533619e-06 2.943219e-06Both find all three discontinuities: SSJ-MLE at 0.298, 0.318, 0.411, 0.412, 0.602 and SSJ-ATW-CV at 0.298, 0.318, 0.411, 0.412, 0.602, against true locations 0.3, 0.4, 0.6. Recall that a break is dated at the first observation belonging to the new regime, so the reported location is the first sampled point after the discontinuity rather than the discontinuity itself.
There are more declarations than discontinuities, and it is worth
seeing why. The warping budget is spread over the two intervals either
side of a transition rather than concentrated on one, so a single jump
can clear the declaration threshold twice at adjacent locations; the two
reported sizes then add up to roughly the true one. This is a property
of the reporting rule, which treats each interval separately, not of the
fit – the curve itself is unaffected, and ssj_breaks()
accepts stricter frac_total and frac_delta if
you would rather see one declaration per transition. Judge the fit by
the curve and the warp profile; treat the table as a guide to where the
transitions are.
The plot method draws the fitted curve over the data and
the estimated domain warps below, which is where the jumps become
visible: spikes standing out of an otherwise flat sequence. Since
version 2.0.0 it builds the figure with ggplot2 and
returns it, so the two panels are facets of one object and the result
can be themed or saved like any other plot; pass
use_ggplot = FALSE for base graphics instead.
The two are hard to tell apart, which is the point: the alternating algorithm reproduces the maximum-likelihood fit at a fraction of the cost, and it no longer needs that fit in order to run.
Superimposing the true signal confirms that the fit is smooth between the breaks and sharp at them.
plot(x, y, pch = 16, cex = 0.6, col = "grey55", xlab = "x", ylab = "y")
lines(x, g1(x), col = "grey20", lty = 2)
lines(fit_atw$x, fit_atw$smoothed_level, col = "firebrick", lwd = 2)
abline(v = ssj_breaks(fit_atw)$x, lty = 3, col = "firebrick")
legend("bottomleft", bty = "n", cex = 0.85,
lty = c(2, 1), lwd = c(1, 2), col = c("grey20", "firebrick"),
legend = c("true signal", "smoothing spline with jumps"))The package ships the data used in the electricity demand versus
temperature application of inst/scripts/load_temperature.R.
load_temp holds the full hourly series (Northern-Italy load
from the GME day-ahead market, Milan temperature from the Open-Meteo
archive, 2023-01-01 to 2025-09-30), while demand_temp is
the homogeneous analysis sample obtained from it: Tuesdays to Thursdays
only, non-vacation months, the central hours 10-15, and no extreme loads
or temperatures. Those filters strip out the weekday, holiday and
time-of-day effects that would otherwise confound the relationship of
interest.
data(demand_temp)
str(demand_temp)
#> 'data.frame': 1691 obs. of 2 variables:
#> $ Temperature: num 8.8 10.8 12.2 12.9 12.9 12.1 10.9 12.6 12.6 13.6 ...
#> $ Load : num 25278 24460 23997 22126 22212 ...Demand is a U-shaped function of temperature – heating on the left, cooling on the right – but the interesting question is whether the curve is smooth. Thermostat settings, air-conditioning switch-on thresholds and similar behavioural rules suggest that it need not be.
Here we use the alternating algorithm auto_ssj_atw()
rather than auto_ssj_mle(), for two reasons. It is far
faster: ssj_mle() estimates one warping parameter per
observation, which on these 1691 points takes minutes, whereas the
alternating algorithm takes a fraction of a second. And it collapses
tied values of the covariate, which matters here because the sample has
only 295 distinct temperatures: with ties left in place the budget can
be spent warping the interval between two observations recorded at
the same temperature, which is not identified. Since
ssj_atw() needs \(\lambda\) to be supplied, we pass the value
obtained from a preliminary auto_ssj_mle() fit – the pilot
strategy used in the paper. That pilot must itself be run on the
tie-averaged data, for the reason just given: fitted on the raw
observations it returns \(\lambda =
154.5\), on the averaged ones \(\lambda
= 254.3\).
fit <- auto_ssj_atw(y = demand_temp$Load, x = demand_temp$Temperature,
lambda = 254.3, ic = "ebic")
fit
#> Smoothing spline with jumps
#> Method: SSJ-ATW: alternating domain-warping algorithm
#> Call:
#> auto_ssj_atw(y = demand_temp$Load, x = demand_temp$Temperature,
#> lambda = 254.3, ic = "ebic")
#>
#> Parameters:
#> sigma = 1001
#> lambda = 254.3
#> budget = 32.3 (maximum sum of the domain warps)
#>
#> Observations: 295
#> Effective degrees of freedom: 12.55
#> Log-likelihood: -12569
#> Information criteria:
#> AIC = 20364
#> AICC = 20364
#> BIC = 20432
#> HQ = 20389
#> EBIC = 20492
#>
#> Declared discontinuities: 8
#> x jump warp
#> 10 4.0 -550.6 2.350
#> 46 8.7 -297.2 2.029
#> 77 11.9 -485.8 3.282
#> 109 15.1 -212.7 1.725
#> 179 22.3 134.1 2.075
#> 224 27.0 298.9 1.655
#> 225 27.1 668.6 3.654
#> 288 34.0 880.2 5.041All the ssj_* functions return an object of class
ssj, with print and plot methods.
The declared discontinuities reported above follow the rule of the
paper: an interval counts as a discontinuity when its estimated domain
warp \(\hat\gamma_t\) clears both 5% of
the total warping and 30% of the mean sampling interval. The table
itself is available through ssj_breaks(), which also
accepts a single absolute threshold through its thresh
argument.
ssj_breaks(fit)
#> index x warp jump
#> 1 10 4.0 2.349993 -550.5671
#> 2 46 8.7 2.028604 -297.1779
#> 3 77 11.9 3.281853 -485.7816
#> 4 109 15.1 1.725359 -212.6548
#> 5 179 22.3 2.074768 134.1489
#> 6 224 27.0 1.654665 298.8849
#> 7 225 27.1 3.653521 668.6330
#> 8 288 34.0 5.040918 880.2336The plot method reproduces Figure 4 of the paper: the
fitted curve over the data, with vertical lines at the declared breaks,
and a lower panel with the estimated warps, which measure the strength
of each jump. The points shown are the 295 distinct temperatures
actually seen by the estimator rather than all 1691 observations. Pass
prob to add a confidence band, whenever the estimator
returns pointwise variances (ssj_mle(),
ssj_kfs() and ssj_fix() do;
ssj_atw() does not).
The declared breaks fall into two groups, and their signs separate them cleanly. Above about 22 degrees they are upward and grow with temperature, peaking either side of 27 degrees, which is consistent with air conditioning being switched on once a comfort threshold is crossed; the paper’s Section 4.1 identifies the jump at 27 degrees as the most relevant one, and it appears here as the largest of the warps. Below 16 degrees they are downward and smaller, tracing the mild role of electric heating in Italy: demand falls as the weather warms towards the comfort zone. A classical smoothing spline would round all of these transitions off into a gentle curve.
Two of the declarations, at 27.0 and 27.1 degrees, are the same
transition seen twice, for the reason discussed in the simulated example
above. Raising ebic_xi, or the thresholds of
ssj_breaks(), returns a shorter list concentrated on the
cooling region alone.
The same data can be analysed with the CSSD algorithm of Storath and
Weinmann (2024), which penalises the number of discontinuities directly
instead of a warping budget. It is a different proposal, not one of
ours, and the print method says so.
Its delta argument is the pointwise noise standard
deviation, and supplying it is what makes the data-fit and roughness
terms commensurate. Leaving delta = 1 on a series measured
in MWh would make the data term dominate by several orders of magnitude
and return an essentially interpolating fit.
# robust noise scale, immune to the jumps themselves
sig <- mad(diff(demand_temp$Load[order(demand_temp$Temperature)])) / sqrt(2)
fit_cssd <- ssj_cssd_dp(demand_temp$Load, demand_temp$Temperature,
p = 0.9, gamma = 10, delta = sig)
fit_cssd
#> Smoothing spline with jumps
#> Method: CSSD: cubic smoothing spline with discontinuities (Storath and Weinmann, 2024)
#> Call:
#> ssj_cssd_dp(y = demand_temp$Load, x = demand_temp$Temperature,
#> p = 0.9, gamma = 10, delta = sig)
#>
#> Parameters:
#> p = 0.9
#> gamma = 10
#>
#> Observations: 295
#>
#> Declared discontinuities: 2
#> x jump
#> 142 18.4 453.1
#> 224 27.0 1119.9ssj_cssd_dp() returns an ssj object too, so
the same print and plot methods apply. There
is no lower panel here: CSSD has no domain warps to show, because its
discontinuity set is part of the solution of the underlying
combinatorial problem rather than a by-product of a continuous
penalty.
The fitted piecewise cubic spline can be evaluated at arbitrary
temperatures with predict_cssd_dp(), which is what makes
the size of a discontinuity directly readable:
The two methods are quite different – one warps the domain under a budget constraint, the other penalises the number of breaks directly – yet both put a discontinuity at about 27 degrees, which is reassuring.