chc.switchback¶
Stability tier: experimental.
Which switchback to run, and how to read it, for a named effect of a lever on a plant with memory.
A switchback randomises a lever over time within a zone, on in some periods and off in others. On a plant with memory the periods are not independent units: what the lever did last period is still in the state. So the design has to say which effect it is for. This module plans for a first-order state per zone,
x_{t+1} = a x_t + b u_t + eps_t, u_t in {0, 1} with probability 1/2, y_t = x_t (+ eta_t),
and for the effect of holding the lever on for H periods, tau_H = b (1 - a^H) / (1 - a)
(Horizon): the channel b at H = 1, the steady state b / (1 - a) at
H = inf, and any horizon between. 0 < a < 1, and nothing here holds outside it.
The results it rests on (the lab's switchback thread, independently verified):
- The design enters only through one number,
A1 = sum_{s >= 1} a^(s-1) rho(s),rhothe assignment's autocorrelation, when the analysis regressesy_{t+1}on(1, y_t, u_t). That needs a fair, ergodic design and an intercept. - Orthogonality by design. That regression's plug-in
b_hat S_H(a_hat)has variance at least(4 sigma^2 / T) S_H(a)^2, the variance withaknown, with equality iffA1 = S_H'(a) / S_H(a): the design makes the persistence estimate orthogonal to the effect. Any other design losesq (A1 - A1*)^2 / D(A1)of it,q = b^2 / (4 sigma^2), and the optimum does not depend onq. So the channel wants an i.i.d. design, a horizon a Markov design that flips with probabilitya / (1 + 2a)atH = 2, and the steady state never to switch, which a floor on the number of switches stops. - Several effects at once lose
q (A1 - A1*_H)^2 / D(A1)each, withDcommon to all, so the design that minimises the worst loss sits between the extreme optima. - Measurement noise makes that regression inconsistent for every
H >= 2, and for the channel at any design but the i.i.d. one. IV with(u_t, u_{t-1})is consistent at every design, and is planned at its own minimax design. - Without a trusted state model only model-free readings are planned: the channel by the
regression at
p = 1/2;tau_Hby the difference in means over blocks of lengthHkeeping each block's last period,DIM(H, H-1), which is unbiased fortau_Hon any linear time-invariant plant, or by the local-projection sum atp = 1/2; the steady state by the block difference in means with the smallest worst mean squared error, whose bias is quoted and not removed, since no model-free design is unbiased for it.
design_switchback returns the arms, and for each effect the analysis, its standard error,
its minimum detectable effect and its loss against the effect's own best design, at the least
favourable a of the prior. Every variance is asymptotic in T. read_switchback reads
an effect off the data a design produced, with a standard error from the data, and
restate_mde reads it off the first periods of the run, an internal pilot, and restates the
minimum detectable effect the whole run can detect. randomisation_test and
randomisation_interval infer from the design's own randomisation instead of from T
being large: the test is exact at any number of blocks, and the interval is exact given a range
for a.
What is outside the model, and comes back as a warning rather than a refusal: a second state or a
longer carryover, drift, spillover between zones, a near 1 on a short run, and fewer than 100
switches. The plug-in's reading tests for a second state, and the block difference's standard error
keeps spillover; the plan's variances assume neither. On the zone market of chc.zones, whose
incentive also acts through a stock and draws half its recruits from the neighbouring zones, the
plug-in read its effects 3% to 14% off, and the block difference spread 1.6-1.7 times as far as
planned; the internal pilot reads that spread off the run. The prior's a must lie in
(0, 1): for a < 0 the bound is unattainable at even H and the best design depends on
q.
SwitchbackAnalysis = Literal['state_aware', 'iv', 'local_projection', 'block_dim']
module-attribute
¶
How an effect is read off the switchback:
"state_aware": OLS ofy_{t+1}on(1, y_t, u_t), andtau_H = b_hat S_H(a_hat);"iv": the same regression with instruments(1, u_{t-1}, u_t), and the same plug-in;"local_projection": OLS ofy_{t+1} + ... + y_{t+H}on(1, u_t, y_t), the coefficient onu_t;"block_dim": the difference in means ofy_{t+1}over the periods each block keeps.
Alternative = Literal['two-sided', 'greater', 'less']
module-attribute
¶
Which statistics are at least as extreme as the observed one: larger in absolute value, larger, or smaller.
Horizon
dataclass
¶
tau_H, the effect of holding the lever on for H periods. H = 1 is the channel
b and H = math.inf the steady state b / (1 - a).
Raises:
| Type | Description |
|---|---|
ValueError
|
when |
PersistencePrior
dataclass
¶
What a pilot or the history says before the design: a somewhere in [a_lo, a_hi], the
state's shock sd, a guess of the channel b, and the measurement noise's variance as a share
of the shock's (0 when the state is observed exactly).
Raises:
| Type | Description |
|---|---|
ValueError
|
unless |
signal_to_noise
property
¶
q = b^2 / (4 sigma^2): what one period of the lever adds, against the shock.
TargetMDE
dataclass
¶
Choose the number of zones: the smallest at which every effect's minimum detectable effect is
at most fraction of |tau_H|, at the least favourable a.
Raises:
| Type | Description |
|---|---|
ValueError
|
unless |
MarkovDesign
dataclass
¶
Each period the lever flips with probability flip; 1/2 is i.i.d.
BlockDesign
dataclass
¶
Blocks of length periods at one setting, each drawn fair and independently; the analysis
drops the first washout periods of each block.
SwitchbackArm
dataclass
¶
A design and the share of each zone's periods it runs for.
EstimandReport
dataclass
¶
One effect's reading: which arm and analysis, and what it resolves at the least favourable
a.
se is the standard error pooled over the zones; mde is
(z_{1 - alpha/2} + z_power) se + |bias|; bias is zero for every consistent analysis and
the working model's for the steady state read by a block difference in means. loss is the
variance over the effect's own best design's in the same mode, minus 1, at its worst a: 0
when the design is aligned to it.
SwitchbackPlan
dataclass
¶
The arms to run in every zone, the reading of each effect, and what the plan cannot see.
alpha is the two-sided test's level and power the power each effect's minimum
detectable effect is for.
SwitchbackReading
dataclass
¶
An effect read off a switchback's data.
interval is the 1 - alpha confidence interval: normal about estimate with se,
except for the steady state read through the plant, where it is Fieller's for b / (1 - a).
Fieller's is (-inf, inf) when the data cannot rule out a unit root, which the delta
method's se does not show.
RandomisationTest
dataclass
¶
The randomisation test of the sharp null that the lever moves no reading in any zone.
statistic is the reading's estimate / se, as read_switchback reads it, and
p_value the chance, over the design's schedules that the reading reads, of a statistic at
least as extreme against alternative. When enumerated it is read off all schedules
of the design, which are equally likely, less those the reading refuses; otherwise off
schedules drawn from it, as (1 + at least as extreme) / (1 + read). Either way it is
exact given that the data were read: under the null, P(p_value <= alpha) <= alpha, at any
number of blocks and whatever the plant's memory. A reading with neither an effect nor a noise,
a constant outcome, has statistic NaN and p_value 1.
design_switchback(estimands, prior, periods, zones=1, *, trust_state_model=True, alpha=0.05, power=0.8, min_switches=40)
¶
The switchback that reads estimands best over periods periods in each zone.
With trust_state_model the plan is a Markov design at the flip probability that minimises
the worst loss over the effects and over the prior's persistence interval, read by the
state-aware plug-in, or by IV when the state is measured with noise and a horizon is asked for.
Without it, only model-free readings are planned (see the module docstring). zones is the
number of zones, or a TargetMDE to choose it. min_switches floors how rarely the
lever may flip, since never switching is what the steady state asks for.
Raises:
| Type | Description |
|---|---|
ValueError
|
on no effects or a repeated one, fewer than |
read_switchback(lever, outcome, estimand, analysis, *, blocks=None, alpha=0.05, se='model')
¶
Read estimand off a switchback's data by analysis.
lever[z, t] is zone z's lever in period t, 0 or 1, and outcome[z, t] its
reading at the start of period t: outcome has one more period than lever, and
outcome[z, t + 1] is the first reading after lever[z, t]. One zone may be passed as 1-D
arrays. The regressions pool the zones as independent replications, each with an intercept.
"state_aware"and"iv"fitaandband readb S_H(a), by the delta method on a covariance robust to heteroskedasticity; IV's keeps the lag-one autocovariance of the MA(1) error a state measured with noise has. The plug-in also tests the first-order state it rests on, by whethery_(t-1)andu_(t-1)add to its regression, and warns when the data reject it at 1%; not for the channel at an i.i.d. lever, which it reads right on any linear time-invariant plant. A lever that alternates every period hides a stock it drives from the test, since the stock then alternates with it."local_projection"needs an i.i.d. lever, and its covariance keeps theH - 1lags of the overlapping sums."block_dim"needsblocks, the design's. Each block's kept periods are centred on the midpoint of the on and off blocks before it, so the difference in means is unbiased at any number of blocks; the blocks before both settings have been seen only centre. It readstau_HonBlockDesign(H, H - 1), and the steady state on any block, with the working-model biasdesign_switchbackquotes. Each zone's difference is its own and the zones are weighted alike, and the standard error sums the zones' scores block by block, so it keeps what a spillover or a shock common to the zones puts between them.
se is what the standard error is over. "model" reads it off the regression's errors as
above. "design" reads it over the lever's own randomisation, with the plant and its noise
held fixed (Liang and Recht, Biometrika 2025), for a local projection on a lever drawn by a
fair coin each period. The estimate is then the local projection with the design's moments in
place of the sample's, no intercept and no y_t: tau_H = sum_(s<H) g_s, with
g_s = 2 / (T - s) sum_t (2 u_t - 1) (y_(t+1+s) - y_0), each zone read from its first
reading, which precedes the lever, and the zones averaged. Over the coins it is unbiased for
tau_H on any linear time-invariant plant, whatever its noise does in time, as long as the
noise does not depend on the coins and no zone's lever moves another's outcome. Its variance
over the coins holds no product of the noise at two periods for the channel, and none more than
H - 1 apart for tau_H: the noise's autocorrelation does not enter it. The standard error
is their plug-in of that variance, over 3 log T lags of the impulse response, and is
asymptotic in T. It is wider than the model's where the model's holds, since it uses
neither the regression's intercept nor y_t.
Raises:
| Type | Description |
|---|---|
ValueError
|
when the shapes disagree, the lever is not 0 or 1 or never switches, the
outcome is not finite, |
restate_mde(plan, estimand, lever, outcome)
¶
estimand's report, with its standard error and minimum detectable effect re-read from
the first periods of its arm: an internal pilot.
The plan's variances are the working model's. On a plant with a second state or spillover the
model-free readings stay where they were, but those of a horizon or the steady state spread
further than planned: 1.04 to 1.73 times on chc.zones' market. lever and
outcome are the arm's first periods in every zone, as read_switchback takes them.
They are read by the plan's own analysis, and the standard error is carried to the arm's whole
length:
- a regression's by its rows,
sqrt((n_pilot - L) / (n_run - L)), withLthe lags its covariance keeps, which are also the rows it sheds; - the block difference's by its variance over the run's blocks against its variance over the pilot's. The blocks before a zone has shown both settings are lost, and the next are centred on few blocks before them: rescaled by its periods, a pilot of ten blocks read 1.2 times the run's standard error on the working model.
The restated MDE is c se + |bias|, with bias the plan's and c the power
quantile of the noncentral t with nu degrees of freedom and noncentrality
z_(1 - alpha/2). The run's power at that MDE, averaged over what the pilot could have read,
is then the plan's when the pilot's variance is chi-square on nu. For a regression
nu = n / (2 L + 1), with n its rows over every zone; for the block difference, whose
standard error squares one sum over the zones a block, it is Satterthwaite's over the blocks.
With c = z_(1 - alpha/2) + z_power the power at the restated MDE was 0.71 of 0.80 at a pilot
of ten blocks.
On chc.zones' market, over 4000 runs under each matching, the run's power at the
restated MDE about the reading's own mean, which leaves the reading's bias out, was 0.81 for
the channel from a tenth of its arm, 0.78 to 0.79 for tau_5 and 0.80 to 0.81 for tau_2,
and 0.75, 0.79 and 0.80 for the steady state from ten, fifteen and twenty of its forty blocks.
The steady state's shortfall at ten is the spillover between zones: it puts a covariance
between two zones' scores for the same block that the centring does not inflate, and the
block factor treats it as if it did, so a short pilot reads the standard error low.
The test at the end is unchanged, and so is its level: the MDE is restated, the run is not
resized. The report's loss is the plan's, since the design is.
Raises:
| Type | Description |
|---|---|
ValueError
|
when the plan does not read |
randomisation_test(lever, outcome, estimand, analysis, design, *, alternative='two-sided', alpha=0.05, draws=9999, seed=0)
¶
Test the sharp null that the lever moves no reading, in any zone and at any lag, by
drawing the design's schedule again: Fisher's randomisation test, with the reading's
estimate / se as the statistic.
Under the null the outcome does not depend on the schedule, so the observed statistic is one
draw from its distribution over the design's schedules, and the p-value is exact at any number
of blocks, whatever the plant's memory or the zones' spillover: there is no effect to carry or
to spill. The schedules the reading refuses, such as a block schedule with fewer than two
blocks after both settings, are left out of that distribution, since the observed one is a
schedule the reading read; counting them as never extreme instead would reject up to alpha
over the share read, 0.107 at 10% on six blocks. The statistic is studentised, which also keeps
the test's level, in large samples, under the weaker null of an effect that averages zero (Wu
and Ding 2021).
lever and outcome are as read_switchback takes them, and design is the one
that drew the lever: every zone's schedule independently, a Markov design's from a fair first
period and a block design's one fair coin a block. A block design with at most 2^14 schedules
over the zones is enumerated; otherwise draws schedules are drawn with seed.
On the working model with a = 0.8 and no effect (scripts/bench_switchback_randomisation.py
size, 1000 runs each, less those the reading refused), it rejected at 5% 3.6%, 5.2% and 4.6%
of the block differences of tau_5 over 6, 8 and 12 blocks, where the Wald interval
excluded 0 in 17.2%, 11.9% and 7.2%; 5.8% and 5.4% of the plug-in's tau_3 over 30 and 60
periods of MarkovDesign(0.3), against Wald's 8.7% and 6.1%; and 4.4% of the steady state's
over 60 periods at a = 0.9, against Fieller's 5.3%. Against pyfixest 0.60.0's ritest,
on 200 logs of an i.i.d. lever over 80 periods, the channel's statistic agreed with its HC1 t
to 1e-13. Its p-values, from 999 permutations of the lever where this redraws 4999 schedules,
differed by 0.008 on average and 0.054 at most, which the permutations' own noise allows.
Raises:
| Type | Description |
|---|---|
ValueError
|
when |
randomisation_interval(lever, outcome, estimand, analysis, design, persistence, *, alpha=0.05, draws=999, seed=0)
¶
estimand's 1 - alpha confidence interval, by inverting randomisation_test
under the working model's joint null.
A null that names tau_H alone is not sharp on a plant with memory: the schedule moves each
reading through the whole path of the lever, b sum_{s >= 1} a^(s - 1) u_(t - s), and two
memories with the same tau_H impute different readings. The joint null (a, b) is sharp:
it imputes the readings with the lever off, y - b sum a^(s - 1) u_(t - s), which the
schedule cannot move, and the test of no effect on them is exact
(validation/switchback_randomisation.mac). So the set of every tau_H = b S_H(a) that
some a in persistence leaves unrejected at alpha covers tau_H with probability
at least 1 - alpha whenever the plant is first-order with its a in that range: at any
number of blocks, where the Wald and Fieller intervals are asymptotic. The statistic is read
off the imputed readings rather than compared with the effect, so a reading that is biased for
its effect, as the block difference is for the steady state, keeps that. The range is the
price: it is an assumption, and a long memory read over few blocks is unbounded.
On the working model (scripts/bench_switchback_randomisation.py, 400 runs a case, 1000 for
the long memory; a run whose every effect the test rejected counts as a miss):
- the block difference of
tau_5ata = 0.8over 8, 12 and 20 blocks. Wald covered 0.886, 0.932 and 0.932. Over(0.6, 0.9)this covered 0.995, 0.980 and 0.973, unbounded in 78%, 10% and none of the runs; at the true memory alone 0.956, 0.935 and 0.948, unbounded in 28% of the runs over 8 blocks. At the truth the test rejected 4.0% and 4.9% of 4000 runs over 8 and 12 blocks. - the plug-in's
tau_3over 40 periods ofMarkovDesign(0.3): Wald 0.925, the range 0.990, the true memory 0.953, at 1.35 and 1.14 times Wald's median width. - the plug-in's steady state at
a = 0.9over 60 and 120 periods: Fieller 0.890 and 0.918, unbounded in 23% and 2% of the runs. Over(0.8, 0.95)this covered 0.993 and 1.000 at 1.37 and 1.57 times Fieller's median width, and at the true memory 0.938 and 0.940 at half of it. tau_2ata = 0.95over 12 and 24 blocks of 2: Wald 0.919 and 0.934, the true memory 0.949 and 0.947, but unbounded in 94% and 84% of the runs.
A null without the memory, persistence = (0, 0), is not sharp under carryover. For the
block difference it still covered 0.950 to 0.966 in every case above, and never unbounded; for
the plug-in it covered 0.53 of tau_3 and none of the steady states, where it rejected every
effect in 41% and 48% of the runs.
Every null is tested on the same draws schedules, or on all of a block design's when it has
at most 2^14, at 21 memories spread evenly across persistence; a memory between two of
them is covered by continuity, not exactly. The ends are found from the first effect accepted
near the estimate, by steps that double until the test rejects and then bisection to a
thousandth of a standard error, each end reported on its rejected side. An accepted set with
gaps may come back with a gap filled, or cut at the first rejection a step lands on. An end
past 1024 standard errors is infinite.
Raises:
| Type | Description |
|---|---|
ValueError
|
when |