chc.dlm¶
Stability tier: experimental.
Discount dynamic linear models: forward filtering with a learned observational variance, smoothing, backward sampling and the decomposition it gives, sampling with coefficients held to a sign, forecasting and monitoring. Experimental.
The model is West and Harrison's (1997): y_t = F_t' theta_t + nu_t with
theta_t = G theta_(t-1) + omega_t, where the evolution variance is not a parameter but a
discount of the information the last step carried. DynamicLinearModel is built from
blocks, each with its own discount delta in (0, 1]:
Polynomial: a level, or a level with a slope, and higher orders;Seasonal: Fourier harmonics of a period, which need not be an integer;Regression: coefficients on columns ofx, which follow random walks.
With P_t = G C_(t-1) G' the evolved posterior covariance, the prior covariance is
form="additive"(the default, West and Harrison's component discounting):R_t = P_t + blockdiag_i(P_t,ii (1 - delta_i) / delta_i), which inflates each block's own covariance and leaves the covariance between blocks as it was;form="multiplicative":R_t = D P_t D,D = diag(delta_i^(-1/2)), which also inflates the covariance between blocks.
With one discount for every block the multiplicative form is the single discount,
R_t = P_t / delta; the additive form is not, since it still leaves the covariance between
blocks uninflated. No third form is offered.
The observational variance is learned (Normal-Gamma): Prior.dof is n_0 and
Prior.scale is S_0, the prior point estimate of the variance; an infinite dof means
the variance is known and equal to scale. variance_discount (beta) lets it move:
n_t = beta n_(t-1) + 1. The one-step forecast is Student t with beta n_(t-1) degrees of
freedom, location f_t and squared scale Q_t, and DLMFit.log_likelihood sums its
log density over the observed steps: that is the criterion for choosing discounts. The update is
the Joseph form, so a covariance stays symmetric and positive semidefinite in floating point; a
step whose Q_t is not positive and finite raises rather than being zeroed.
Missing observations are NaN in y. A missing step evolves and does not update:
m_t = a_t, C_t = R_t, and the variance's degrees of freedom decay by beta.
What is exact and what is not:
- The filter and the forecasts are exact for the model. So is
backward_sample(forward filtering, backward sampling) whenbeta = 1; below 1 it draws the variance path by the beta-gamma evolution run backwards and each state given the next at that step's variance (McAlinn and West 2019, appendix A.2). constrained_sampledraws the posterior truncated to coefficients' signs by Gibbs sampling, which is exact only in the limit of its chains; its R-hat and effective sample size say whether a run can be read.smoothreturns the exact means, covariances and lag-one covariances of those draws. Each covariance is in the units ofE[V_t | D_T]: withbeta = 1that is the final estimaten_T S_T / (n_T - 2)at every step, not theS_tthe filter had att(a smoother that keeps eachC_tin its own units misstates it byS_T / S_t); below 1 it is one quadrature a step. Only the marginals' shape is approximate below 1, where they are not Studentt;SmoothedStates.exactsays which.forecastholds the evolution variance of the first step ahead for every later one by default (evolution="constant");"compounding"applies the discount at every step, which inflates thek-step state variance bydelta^(-k). The two differ by a factor 3.9 in the state variance of a level 52 steps ahead atdelta = 0.95.
What it does not do, and what to watch:
- Discounting is forgetting. With
G = Iand a single discount the posterior mean is weighted least squares with weightsdelta^(T - t)(Ameen and Harrison 1984), so the effective sample is(1 + delta) / (1 - delta)observations. - A regressor that is zero carries no information, and its coefficient's variance still grows
by
1 / deltaa step.Regression(hold_when_idle=True)discounts only the coordinates whose regressor is non-zero at the step, a coordinate-wise form of restricted forgetting (Kulhavy 1987); a long idle run without it is logged as a warning (chc_event="dlm_windup"). - A coefficient that moves is not identified from a saturation that bends. On observational marketing data a time-varying coefficient and a nonlinear response can explain the same series (Dew et al. 2024, arXiv:2408.07678); this module fits the first and cannot tell them apart.
- A discount picked by one-step likelihood can be too high for the coefficients' intervals.
On the random-walk world of
scripts/bench_dlm.py, 500 series, the likelihood picked the coefficients' discount at 0.95 or above in 450 and below 0.9 in none, and the 90% intervals of a channel's contribution over 13 weeks covered 0.78 and 0.77; fixed at 0.85 or 0.9 they covered 0.88-0.90. An interval reported from the likelihood's pick alone is too narrow; the union overconfidence_set's fits covered 0.94 there. - A monitor has until the filter absorbs a change. Once the discount has taken a shift into
the level, the one-step errors are white again. On
scripts/bench_dlm_monitor.py's local level atdelta = 0.9, 300 series,chc.gate.DriftAlarmat an average run length of 1000 caught a shift of two one-step deviations within 1000 steps in 74% of them, and one of three in all but one. West and Harrison's monitor attau = 0.135caught each within a median of one step, and on unchanged series alarmed within a median of 13; DriftAlarm alarmed by step 100 on 2.7% of them, under its bound of 10%. Those series are the model's own, so the bound holds there by construction. - A known variance set too low reads as a change. With every deviation 1.25 times what the known variance allowed for, the alarm sounded by step 100 on 57% of the series; with the variance learned from a prior of five degrees of freedom, on 4.3%.
monitor_evalues turns the one-step errors into e-values for West and Harrison's (1986)
alternatives -- a shift of the location, an inflation of the scale -- which
chc.gate.DriftAlarm turns into an alarm with a guaranteed average run length.
forward_filter(interventions=...) applies an extra discount at named steps, their
feed-back intervention, and logs each (chc_event="dlm_intervention").
Over geos. GeoDLM stacks national blocks every geo reads and regional blocks
each geo has its own copy of; forward_filter_geos filters a KPI a geo, y_(g,t) =
F_(g,t)' theta_t + nu_(g,t) with nu_(g,t) ~ N(0, V w_g), w_g the geo's known variance
relative to the others'. A geo's row of F reads the national blocks and its own regional ones,
and a national and a regional regression block read the geo's columns of x from the first, so a
geo's coefficient on a column both read is the national one plus its own deviation. The evolution
and the discounts are this module's for the stacked model; the update is West and Harrison's for a
vector with one variance scale (1997, section 16.4): with r_t geos observed,
Q_t = F R_t F' + S_(t-1) W, A_t = R_t F' Q_t^-1, n_t = beta n_(t-1) + r_t and
S_t = S_(t-1) (beta n_(t-1) + e' Q_t^-1 e) / n_t, and the one-step forecast of the observed
geos is multivariate Student t. That is the update one geo at a time with no evolution between
them, and its density the product of theirs (validation/geo_dlm.mac, steps 1 and 2).
- One geo is
forward_filter's model of the national blocks and then the regional ones, and with every discount 1 the filter is the conjugate regression on the first state over every observation at once; the tests hold the first to1e-12and the second to1e-10. - The filter is dense: with
pcoordinates, the national ones and every geo's, a step costsO(p^3)and the fit holds2 Tcovariances ofp^2entries. An observation of one geo adds nothing to the posterior precision between two others, and the multiplicative form, a diagonal scaling of the covariance, keeps that zero through the evolution while component discounting does not (step 3): a filter linear in the number of geos exists for the first form and is not built. smoothandbackward_sampletake a filter over geos and run back over its stacked state. They read the filter's moments, the evolution and the variance discount, and the variance's backward step readsn_tandS_t, not how many geos a step observed. Over three geos with missing observations the smoother is the joint Gaussian posterior of every state to1e-8, the variance known or learned. There is no forecast or decomposition over geos yet. One variance scale serves every geo, each geo's share of it fixed.fit_geo_spreadchooses the geos' spread, the regional prior's variance on named coordinates, by the marginal likelihood that the filter's log-likelihood is (type-II maximum likelihood), with each spread's likelihood-ratio interval. It draws the spread from its posterior under a prior flat on each standard deviation by importance sampling, so thatGeoSpread.mixturecarries a quantity's uncertainty about the spread as well as about the states. Where one spread is weakly identified the mixture is quadrature's within its Monte Carlo error, with the best inside the range, near its floor, and at its ceiling.
The implementation is NumPy in float64 and written from the published equations.
Polynomial
dataclass
¶
A polynomial trend: order=1 is a level, order=2 a level and a slope.
F = (1, 0, ..., 0) and G is the upper bidiagonal matrix of ones.
Raises:
| Type | Description |
|---|---|
ValueError
|
on an |
Seasonal
dataclass
¶
Fourier harmonics j of a period: a rotation by 2 pi j / period a step for each.
A harmonic takes two state coordinates, except j = period / 2, which takes one and flips
sign every step. period need not be an integer (52.18 weeks to a year).
Raises:
| Type | Description |
|---|---|
ValueError
|
on a |
Regression
dataclass
¶
Coefficients on width columns of x, each a random walk (G = I).
The columns are taken from x in block order: the first regression block reads the first
width columns, the next the columns after them.
hold_when_idle=True discounts, at each step, only the coordinates whose regressor is
non-zero, so a coefficient's variance does not grow while its column is zero.
Raises:
| Type | Description |
|---|---|
ValueError
|
on a |
Prior
dataclass
¶
The state and the observational variance before the first observation.
theta_0 ~ T_dof(mean, covariance) and 1 / V ~ Gamma(dof / 2, dof * scale / 2): scale
is the prior point estimate of the variance and covariance is stated at it. An infinite
dof means the variance is known, equal to scale, and the state's prior is normal.
covariance must be positive definite. Every block's G is invertible, so every later
prior and posterior covariance is positive definite too, which the smoother relies on.
Raises:
| Type | Description |
|---|---|
ValueError
|
on shapes that do not agree, a non-finite entry, a |
DynamicLinearModel
dataclass
¶
Blocks, their prior, how their discounts combine, and the variance discount.
The state is the blocks' coordinates in order. See the module docstring for form.
Raises:
| Type | Description |
|---|---|
ValueError
|
on no blocks, a prior whose size is not the blocks' total, an unknown
|
DLMFit
dataclass
¶
The forward filter's output, one row per step, in West and Harrison's notation.
At step t: the prior theta_t | D_(t-1) has mean a and covariance R; the one-step
forecast is Student t with one_step_dof degrees of freedom, location one_step_mean
(f) and squared scale one_step_scale (Q), so its variance is
Q dof / (dof - 2); the posterior has mean m and covariance C, the variance estimate
S with n degrees of freedom. log_scores is the one-step forecast's log density at
the observation, NaN on a missing step. C is stated at S_t, R at S_(t-1).
SmoothedStates
dataclass
¶
theta_t | D_T for every step: means, covariances, and Cov(theta_t, theta_(t+1)).
A covariance is the variance, not the scale. With a variance discount of 1 (exact) the
marginals are Student t with dof degrees of freedom, normal when infinite. Below 1 the
variance moves, the marginals are scale mixtures of normals that are not Student t, and
dof is that of a gamma matched to the smoothed precision's mean and variance, for an
approximate t; the means and covariances stay exact.
PosteriorDraws
dataclass
¶
Joint draws of the states and the observational variance given all the data.
ConstrainedDraws
dataclass
¶
Draws of theta_(1:T) and V given D_T and the signs, from Gibbs chains after
their warm-up. rhat is the largest rank-normalised split R-hat, bulk or tail, over every
state at every step and the variance (Vehtari et al. 2021); ess the smallest bulk
effective sample size over the same.
DLMForecast
dataclass
¶
k-step-ahead marginal forecasts from the end of the data, k = 1, ..., horizon.
mean and scale are the Student t location and squared scale of y_(T+k), with
dof degrees of freedom; state_mean and state_scale those of theta_(T+k).
GeoDLM
dataclass
¶
A discount DLM over geos geos: national blocks every geo reads and regional
blocks each geo has its own copy of, filtered by forward_filter_geos.
prior is on the stacked state: the national blocks' coordinates, then each geo's copy of
the regional blocks', in the geos' order; stacked_prior builds one whose geos' regional
priors are alike and independent. relative_variance is each geo's observational variance
relative to the others', 1 for every geo when omitted.
A national and a regional regression block read each geo's columns of x from the first,
so where both read a column, a geo's coefficient on it is the national coefficient plus the
geo's own deviation. The regional blocks' prior variance is then the spread of the geos about
the national coefficient, and the regional discount how long that prior is remembered.
Raises:
| Type | Description |
|---|---|
ValueError
|
on a |
GeoDLMFit
dataclass
¶
forward_filter_geos's output over the stacked state, one row per step, in
DLMFit's notation.
one_step_mean and one_step_scale are every geo's, observed or not: location F a and
squared scale F R F' + S_(t-1) W, W the relative variances on the diagonal.
log_scores is the observed geos' joint one-step log density, NaN on a step with none
observed. n rises by the number of geos observed at a step.
GeoSpread
dataclass
¶
How far the geos spread about the national coefficients, chosen by the marginal
likelihood: fit_geo_spread's output.
variance[i] is the prior variance of every geo's regional coordinate pooled[i], stated
at the prior's scale as Prior states a covariance: at a variance V the geos spread
about the national coefficient with variance variance[i] V / scale. fit is the filter
at variance, and lower and upper each spread's likelihood-ratio interval, the other
spreads held at their best.
draws are spreads drawn from their posterior, (draws, pooled), under a prior flat on
each standard deviation over the range searched, with self-normalised importance weights.
mixture averages a quantity's posterior over them, so that it carries the spread's
uncertainty as well as the states'.
at(variance)
¶
The model with every geo's pooled coordinates at prior variance variance.
mixture(quantity)
¶
A quantity's mean and variance over the spread's posterior.
quantity maps a filter to the quantity's posterior mean and variance at that filter's
spread; the result is their mixture over the draws, sum_j w_j m_j and
sum_j w_j (v_j + (m_j - mean)^2), one filter a draw of positive weight.
forward_filter(model, y, x=None, *, interventions=None)
¶
Filter y through model, with x the regression blocks' columns.
interventions maps a step to an extra discount applied to the whole prior covariance at
that step, R_t / delta: West and Harrison's feed-back intervention after a monitor's
alarm. Each is logged.
Raises:
| Type | Description |
|---|---|
ValueError
|
on a |
FloatingPointError
|
on a step whose one-step squared scale is not positive and finite, or whose update leaves a scale or a state that is not finite. |
smooth(fit)
¶
Retrospective moments of every state given all the data: Rauch-Tung-Striebel, with each
step's covariance in the units E[V_t | D_T] of the variance the whole series supports.
They are the moments of backward_sample's draws. A filter over geos is smoothed over
its stacked state.
Raises:
| Type | Description |
|---|---|
ValueError
|
when the final degrees of freedom |
backward_sample(fit, draws, seed)
¶
Forward filtering, backward sampling: joint draws of theta_(1:T) and V_(1:T) given
D_T, with the volatility path when variance_discount < 1 (McAlinn and West 2019,
appendix A.2). A filter over geos is sampled over its stacked state.
Raises:
| Type | Description |
|---|---|
ValueError
|
on |
decompose(fit, draws)
¶
The mean F_t' theta_t split into its parts, draw by draw: (draws, T, parts).
A Polynomial or Seasonal block is one part, its coordinates of F_t
times its coordinates of the state: the level, or the seasonal effect. A Regression
block is one part per column, the column times its coefficient. Parts follow the state's
order, and a draw's parts add up to its F_t' theta_t to rounding.
Each part's interval is a quantile over the draws. A window's total, a difference between two windows, or a return per unit of a column is taken draw by draw first, so its interval carries the parts' dependence across steps and on each other.
Raises:
| Type | Description |
|---|---|
ValueError
|
on draws of another model's or another series' states. |
constrained_sample(fit, signs, draws, seed, *, chains=4, warmup=None)
¶
Draws of the states and the variance given the data with the coefficients of some columns
of x held to a sign at every step: the model's posterior truncated to the signs, so the
point estimate is the truncated posterior's mean, not a clipped one, and a channel's pull
against its sign moves into the other states. signs maps a column of x to
"positive" (at least 0) or "negative" (at most 0).
The discounts define the evolution variances W_t through the unconstrained filter, which
depend on the design and not on y; with them the model is a Gaussian prior over paths,
and the signs condition it. Gibbs sampling over three blocks, each chain started from an
unconstrained backward draw moved into the signs (ADR 0042):
- the constrained coefficients' values with the other states integrated out, a Gaussian given
Vtruncated to the signs, by one exact Hamiltonian trajectory of lengthpi / 2(Pakman and Paninski 2014): the orbit is an ellipse, reflected where it meets a wall, with no step size and no rejection. The steps a zero evolution variance ties together (a discount of 1, or an idle held regressor) are one value; - the other states given those paths, by forward filtering and backward sampling;
Vfrom its inverse gamma given the constrained values alone, the other states integrated out as well. Given every state, each evolution innovation would add a degree of freedom, which pinsVto the path and slows its chain.
Truncating each backward-sampling step instead would not give this posterior, since the filter that feeds it never saw the signs. The values' covariance is held dense and factored once, so memory grows as the square of their number, and a trajectory meets more walls the harder the signs bind.
A constrained coefficient must be a Regression block of width one. rhat and
ess say whether the chains mixed; the draws are not to be read when they are not
ConstrainedDraws.mixed, and the run is then logged as a warning.
Raises:
| Type | Description |
|---|---|
ValueError
|
on no signs, a column that is not one of |
forecast(fit, horizon, x=None, *, evolution='constant')
¶
Forecast horizon steps past the data, with x the regression columns over them.
evolution="constant" holds the first step's evolution variance W_(T+1) for every later
step; "compounding" discounts again at every step. The variance's degrees of freedom decay
by variance_discount a step and its estimate stays at S_T.
Under form="multiplicative" with blocks whose discounts differ, W = D P D - P is not a
variance once the blocks are correlated enough, and holding it would drive the state variance
indefinite; "constant" refuses such a W.
Raises:
| Type | Description |
|---|---|
ValueError
|
on a |
confidence_set(fits, parameters, level=0.95)
¶
The fits a likelihood-ratio test at level does not reject, in the order given: those
whose DLMFit.log_likelihood is within chi2_parameters(level) / 2 of the best.
parameters is how many settings the fits vary over, two for a grid over two discounts.
An interval reported as the union of each member's interval, from its lowest lower end to its highest upper end, carries the uncertainty of the discounts as well as the states' (projection; Berger and Boos 1994). The best fit's interval alone does not, and a mixture weighted by the likelihood carries too little of it: the likelihood is flat where an evolution variance is weakly identified, and its best point there is a selection.
The union over-covers. On scripts/bench_dlm.py's 500 series a channel's 13-week
contribution was covered 0.94 at a nominal 0.90, both on a random walk, where the pick
covered 0.78, and on the discount model's own series, where it covered 0.85. The intervals
there were 1.2 times as wide as at the true discounts.
Raises:
| Type | Description |
|---|---|
ValueError
|
on no fits, fits of other observations than the first's, a |
monitor_evalues(fit, *, shifts=(2.0,), inflations=(2.0,))
¶
E-values for "the model's one-step forecasts hold", one row per observed step and one
column per alternative, for chc.gate.DriftAlarm.
The standardised error u_t = e_t / sqrt(Q_t) is Student t under the model. Each
alternative is a density for it -- the same t shifted by +h and by -h for each
h in shifts, and scaled by k for each k in inflations -- and its e-value is
the density ratio, whose expectation under the model is 1. West and Harrison's (1986)
cumulative Bayes-factor monitor is e-CUSUM over the same ratios, and its usual threshold
tau = 0.135 guarantees only that the average run length is at least 1 / tau, about
7.4 steps; chc.gate.DriftAlarm takes the average run length it guarantees as its
threshold.
Raises:
| Type | Description |
|---|---|
ValueError
|
on no alternative, a shift that is not positive and finite, or an inflation that is not above 1 and finite. |
stacked_prior(national, regional, geos)
¶
The stacked state's prior: national on the national blocks and regional on each
geo's copy of the regional blocks, every block independent of the others.
None stands for no blocks of that kind.
Raises:
| Type | Description |
|---|---|
ValueError
|
on two priors whose |
forward_filter_geos(model, y, x=None)
¶
Filter y, (T, geos), through model, with x, (T, geos, model.regressors),
each geo's columns for the regression blocks. A missing observation is NaN.
Raises:
| Type | Description |
|---|---|
ValueError
|
on a |
FloatingPointError
|
on a step whose observed geos' one-step squared scale is not positive definite and finite, or whose update leaves a scale or a state that is not finite. |
fit_geo_spread(model, y, x, pooled, draws, seed, *, level=0.9)
¶
How far the geos spread about the national coefficients, by the marginal likelihood of
y (type-II maximum likelihood, empirical Bayes), and draws of the spread from its
posterior.
pooled are coordinates of a geo's regional blocks, counted from 0: each one's prior
variance, the same in every geo and independent of every other coordinate, is the geos'
spread on it, and the model's own is the vaguest considered. Each spread's log is searched
from 1e-12 of the model's own up to it, from the model's own, by L-BFGS-B on central
differences of GeoDLMFit.log_likelihood, which is exact, so the search is the only
approximation. Each spread's interval at level is where the log-likelihood, the other
spreads at their best, is within chi2_1(level) / 2 of the best, or the range's end where
it stays so.
The posterior is under a prior flat on each standard deviation over the range (Gelman 2006),
which unlike a flat prior on the log does not pile its mass at a spread of 0. draws
spreads are drawn from it with seed by importance sampling in the range's logistic
coordinates, log_spread = low + (high - low) expit(u): a Student t about the
posterior's mode in u with its curvature there. Every draw is in the range, and the mode
is inside it even where the best is at an end, as when the data would spread the geos wider
than the model's own prior lets them. A quantity that carries the spread's uncertainty is the
mixture of its posteriors over the draws, GeoSpread.mixture.
Raises:
| Type | Description |
|---|---|
ValueError
|
on pooled coordinates that are not distinct coordinates of the regional
blocks, a pooled coordinate whose prior variance differs between geos or that has
prior covariance with another, |