Causal models and Bayesian structural time series v0.13¶
v0.13 adds four Bayesian models. One model forecasts a series from its parts. Three models measure the effect of a change, such as a campaign, a launch or a price change:
| Model | Package | In plain words | Your data |
|---|---|---|---|
ts.bsts |
mapi-plugin-ts |
Bayesian trend, season and drivers | One series over time, optional drivers |
causal.impact |
mapi-plugin-causal |
Effect of a campaign on one series (CausalImpact) | One series, optional control series, the start date of the change |
causal.synthetic_control |
mapi-plugin-causal |
Geo-lift test (Bayesian synthetic control) | The same number for many markets, the test markets and the test dates |
causal.did |
mapi-plugin-causal |
Before and after, with a comparison group (difference-in-differences and event study) | The same number for many units, and a 0/1 treatment column |
All four models sample with NUTS. They run in the time-series runner image
(mapi/base-forecast). They log tracking
metrics and params on each fit. Each model has an About card, so Help me choose finds them
for the goal "Measure the effect of a campaign, a launch or a test".
Measure the effect of a change is the procedure. This page is the reference.
The effect, in one bundle group¶
The three causal models write the effect to the impact group of the bundle. Plots,
workflow nodes and the web app read only this group. The values are in the units of the
target.
| Variable | Dims | What it is |
|---|---|---|
actual |
(unit,) time |
The observed value. It can be NaN. |
counterfactual |
sample, (unit,) time |
Draws of the value without the change. The draws include the observation noise. |
effect |
sample, (unit,) time |
actual - counterfactual |
cumulative_effect |
sample, (unit,) time |
The running (cumulative) sum of effect along time over the cells of the effect window. It is NaN outside them. |
post |
(unit,) time |
True in the effect window (from the start to the end, inclusive) |
The time dim is always time. causal.synthetic_control and causal.did add the unit dim
(the treated units only). causal.impact has no unit dim. The group attributes are
intervention_start, intervention_end (empty when the change did not stop), model and
units. The sample dim has a maximum of 500 draws.
Before the start, counterfactual is the fit of the model. Thus the causal.pre_fit chart
shows how well the model follows the data before the change.
Common random numbers
The noise of the counterfactual draws comes from the seed of the fit. Two fits with the same seed and the same data give the same noise. A comparison of two scenarios thus shows only the difference between the scenarios.
ts.bsts: Bayesian structural time series¶
ts.bsts divides one series into a level, a trend, seasonal patterns and the effect of
drivers. It uses the state-space models of
pymc-extras (Apache-2.0). The sampler uses the
exact Kalman likelihood.
Inputs. t (time, policy extend) and the target y(t). Missing periods are
permitted. The filter continues over them. The drivers that you name in regressors are
variables along t. A forecast needs their future values (MISSING_FUTURE_COVARIATE if
they are not in the payload). The model standardises y and the drivers. It changes the
outputs back to your units.
| Setting | Default | Meaning |
|---|---|---|
trend |
local_linear |
level (a random walk), local_linear (a level and a slope that both change) or smooth (only the slope changes) |
seasonality |
[] |
A maximum of three seasonal patterns: {period, harmonics, stochastic, sigma} |
regressors |
[] |
The drivers. Each driver has one effect for all periods. |
level_sigma, slope_sigma |
0.1, 0.02 | Half-normal scales of the trend innovations (standardised scale) |
obs_sigma |
0.5 | Half-normal scale of the observation noise |
initial_sd |
1.0 | The prior sd of the first state |
regression_prior, regression_sigma |
normal, 1.0 |
normal or horseshoe (when you have many drivers) |
A seasonal pattern has a period in periods of t (52.18 for weekly data with a yearly
pattern, 7 for daily data with a weekly pattern). Without harmonics, the pattern has one
effect for each season. This needs an integer period of 24 or less. With harmonics, the
pattern is a Fourier series of that order (a maximum of half the period). With
stochastic: true, the pattern can change over time. The model has a maximum of 40 states.
Score modes.
predict/evaluate: in the fitted range, the one-step-ahead prediction (the value that the model expected before it saw the period). After the fitted range, a forecast.forecast: a forecast from the state at the end of the fit (theterminal_stategroup).filter: runs the Kalman filter over new periods that continue the fit. It returns the one-step-ahead predictions and an updatedterminal_state. Thus you can add new periods without a new fit (see Forecasting with actuals).
Outputs. y_hat(t) (the expected value or a predictive draw) and level(t) (the level
and the trend, expected), in the units of the target. The bundle also has the groups
components (level, season_<i> and regression over sample and t; the
core.decomposition chart shows them), one_step and terminal_state.
The manifest declares pointwise leave-one-out as not valid: the Kalman likelihood is a sum
over the whole series, and the periods depend on each other. Validation rejects elpd_loo.
data:
bind: {dims: {t: date}, vars: {y: sales}}
split: {kind: time_holdout, dim: t, last: 12}
model:
use: ts.bsts@^1
settings:
trend: local_linear
seasonality: [{period: 12}]
regressors: [price]
fit: {mode: evaluate_then_refit, tune: 500, draws: 500, chains: 4, seed: 7}
causal.impact: the effect of a change on one series¶
causal.impact is CausalImpact (Brodersen et al. 2015) on the ts.bsts core. The model
fits a Bayesian structural time series to the periods before the change. The control
series are its drivers. Then it predicts the series after the start from the control series.
The difference between the actual values and the prediction is the effect. Without control
series, this is an interrupted time series.
Inputs. time (time, policy extend), the target y(time) (missing values are
permitted) and the control series that you name in controls, along time. The control
series must have values for all periods.
| Setting | Default | Meaning |
|---|---|---|
intervention_start |
(necessary) | The first period of the change: an ISO date (2024-03-04) or a period number. A date between two periods selects the next period. |
intervention_end |
none | The last period of the change. Empty: the change did not stop. |
controls |
[] |
Series that the change did not touch (sales in other countries, searches for the category) |
trend, seasonality, ... |
as ts.bsts |
The structure of the model |
The model needs a minimum of 3 periods before the start. It gives the diagnostic
SHORT_PRE_PERIOD when fewer than two seasons of the longest seasonal pattern come before
the start.
The periods must be evenly spaced (SCHEMA_INVALID): the model counts periods by position.
Add the missing periods with an empty target.
Score modes. predict gives y_hat(time), the value without the change, and
effect(time), the payload y minus y_hat. Before the start, y_hat is the one-step-ahead
prediction. From the start, it is a forecast with the control series of the payload.
forecast does the same after the data. With output.units: original, effect is the
difference in the units of the data, also when the pipeline transforms the target (for
example with core.log1p).
causal.synthetic_control: a geo-lift test¶
causal.synthetic_control is a Bayesian synthetic control (Abadie, Diamond and Hainmueller
2010). For each treated market, the model finds weights of the other markets (the donors).
Before the test, the weighted mix of the donors follows the treated market. During the test,
the mix gives the value without the campaign.
Inputs. unit (categorical, fixed), time (time, policy extend) and the target
y(unit, time). The donors must have values for all periods before the test and for all
periods that you score.
| Setting | Default | Meaning |
|---|---|---|
treated |
(necessary) | The test markets (labels of unit). A text such as "Leeds, Bristol" is also correct. |
donors |
all other units | The markets for the mix (a minimum of two) |
intervention_start, intervention_end |
(necessary), none | The dates of the test |
weights |
simplex |
simplex: positive weights with a sum of 1 (the classic method). horseshoe: weights that can be negative and have no fixed sum. |
concentration |
1.0 | Dirichlet concentration of the simplex weights (less than 1: fewer donors) |
intercept |
true | A level difference between a market and its mix |
normalize |
mean |
mean divides each market by its mean before the test. none keeps the units. |
noise_sigma, likelihood |
0.5, normal |
The noise prior, and normal or student_t |
Outputs. y_hat(unit, time) for the treated units only, and the groups impact and
donor_weights (weight(sample, unit, donor) and the error before the test, pre_rmse(unit)).
The charts are causal.impact, causal.donor_weights and causal.pre_fit.
Do not use the horseshoe weights without a look at the pre-period chart
The horseshoe weights can extrapolate outside the range of the donors. A treated market
that is larger than all donors then gets a mix that can be wrong. Look at
causal.pre_fit before you use the effect.
From a geo-lift test to a lift test for the MMM¶
The impact group of a synthetic control fit gives a lift test for the
marketing mix model. The total effect over the test is the kpi_delta.
Its sd is the se. These are the three ways to get the lift test:
- The Guided report of the
geo_lifttemplate shows a "Lift test for your marketing mix" card. Type the channel and the spend change. Copy the YAML entry intocalibration.lift_testsof the MMM spec. - The workflow node
model.lift_testmakes aLiftTestfrom any causal model version. - The workflow template
geo_lift_mmmfits the test, makes the lift test and fitsmmm.bayesianwith it. See Feed the result to the MMM.
A lift test from one treated market has the geo of that market. A lift test from two or more
markets is national (geo: null).
causal.did: difference-in-differences and event study¶
causal.did compares units that got a change with units that never got it. It is a Bayesian
two-way fixed effects model: an effect for each unit, an effect for each period and the
effect of the change.
Inputs. unit (categorical, fixed), time (time, fixed), the target
y(unit, time) (missing values are permitted), treatment(unit, time) (0 before the change,
1 from the period of the change) and the covariates that you name in covariates. A minimum
of one unit must never get the change. Each unit must keep the change after it starts.
| Setting | Default | Meaning |
|---|---|---|
design |
event_study |
event_study: one effect for each period relative to the change. did: one average effect. |
leads, lags |
4, 8 | The periods before and after the change with their own effect |
bin_endpoints |
true | The periods further away share the effect of the first lead or the last lag |
time_effects |
iid |
iid or random_walk |
covariates |
[] |
Variables along (unit, time) |
likelihood, effect_sigma, noise_sigma |
normal, 1.0, 0.5 |
The likelihood and the priors (standardised scale) |
The reference period is the period before the change (event time -1). Its coefficient is 0.
Outputs. y_hat(unit, time), and the groups impact (the treated units) and
event_study (coef(sample, event_time), n_cells(event_time) and the average effect after
the change, att(sample)). The counterfactual score mode sets treatment to 0. The
causal.event_study chart shows the coefficients. If the coefficients before the change are
near 0, the treated and the other units moved together before the change.
A score payload can have only some periods. The periods that it does not have take the
treatment of the fit, thus the period of the change of each unit stays the same. If the payload
sets the treatment to 0 (the counterfactual mode), the earlier periods also get 0.
Do not trust the estimate when the units got the change at different times and the effect is different for each group
The two-way fixed effects estimate can then be wrong (Goodman-Bacon 2021, Sun and Abraham
2021). The model gives the warning STAGGERED_ADOPTION when the units got the change at
different times.
The time effects are only for the periods of the data. Thus causal.did does not forecast.
Known gaps¶
ts.bststakes one series. Each driver has one effect for all periods. A forecast does not use actual values that you know (use thefiltermode). A seasonal pattern withoutharmonicshas a maximum period of 24 (useharmonicsfor longer periods).causal.impactfits the periods before the change only.causal.synthetic_controlhas no placebo tests and does not design a test (choice of the markets, power).- A lift test from two or more markets is national.
- The time effects of
causal.didare fixed: the model does not forecast. Staggered adoption can make the estimates wrong. - The lift-test card of the Guided report does not write into the MMM spec. Copy the entry.
References¶
- Brodersen et al. (2015), Inferring causal impact using Bayesian structural time-series models, Annals of Applied Statistics.
- Abadie, Diamond and Hainmueller (2010), Synthetic control methods for comparative case studies, JASA.
- Goodman-Bacon (2021), Difference-in-differences with variation in treatment timing.
- Sun and Abraham (2021), Estimating dynamic treatment effects in event studies with heterogeneous treatment effects.
- Harvey (1989), Forecasting, Structural Time Series Models and the Kalman Filter. Durbin and Koopman (2012), Time Series Analysis by State Space Methods.