When the interval can be trusted¶
Reference parity says StatsPAI reproduces Stata or R. It does not say the
95% interval covers 95% of the time on your data. This page collects what
the simulation studies under tests/reliability/ found about that, as
rules you can act on. Each study fixed its design before the first run,
stores every cell, and has a test that recomputes part of it. The numbers
are in docs/evidence_inventory.md; the tables and their reading are in
tests/reliability/README.md.
A rule here is what one set of designs showed. Treat it as a reason to
look, and check the result's own warnings and model_info for the
diagnostic it names.
Clusters¶
The count of clusters is not what matters. Two other numbers are, and StatsPAI records both.
n_clusters_effective discounts unequal sizes. With 40 clusters of which
one holds half the sample, a cluster-robust t-test rejected a true null
36% of the time in a pooled regression and 22% with unit fixed effects.
In the pooled designs vce='cr3' stayed at 3% to 5%.
n_clusters_effective_weights discounts unequal weights. With unit-level
weights, the interval of a clustered fixed-effects regression covered 94%
with 82 clusters in effect, 91% with 23, 87% with 17 and 78% with 7. The
panels had 50 or 200 equal-sized clusters throughout.
import statspai as sp
fit = sp.panel(df, "y ~ x", entity="id", time="t", method="fe",
weights="w", cluster="id")
fit.model_info["n_clusters_effective"] # by size
fit.model_info["n_clusters_effective_weights"] # by weight
A wild cluster bootstrap helps when clusters are few and similar in size. It does not help when only one or two clusters are treated, where it almost never rejects.
Few treated clusters¶
What can be few is the number of clusters that are ever treated, however
many clusters there are. With 40 units and a true effect of zero, a
unit-clustered two-way fixed-effects test rejected 75% of the time with
one treated unit, 31% with two, 10% with five and 6% with ten. StatsPAI
records such a regressor in few_treated_clusters and warns.
sp.did_few_treated is the remedy for one or two treated units, where
it rejected 3% and 7%. It is not a remedy beyond that. With five treated
units it rejected 10% and with ten 14%. Between three and nine treated
units no test here is at its nominal size, and the honest report says
so.
Weights¶
Passing weights= and nothing else gives the classical weighted variance.
That is right when the weights are precisions (linear models) or
frequencies (Poisson). It is wrong for sampling or inverse-probability
weights. Under dispersed sampling weights the default interval covered
34% to 78% for OLS, 45% to 80% with fixed effects and 14% to 68% for
Poisson. The fast entry points, sp.nbreg, sp.logit, sp.probit,
sp.glm and sp.iv behave the same way (15% to 79%).
Say what the weights are, and the variance follows.
sp.regress("y ~ x", df, weights="w", weights_as="sampling") # Stata [pw=w]
sp.poisson("y ~ x", df, weights="w", weights_as="sampling")
sp.regress("y ~ x", df, weights="w", weights_as="analytic") # Stata [aw=w]
weights_as='sampling' gives the robust variance, the same as adding
robust='hc1' by hand. The model-based reading ('analytic' for linear
models, 'frequency' for likelihood models) keeps the default variance
and tells StatsPAI you meant it, so the warning about dispersed weights
goes away.
A robust variance is itself short when a few observations carry most of
the weight. n_effective_weights is the Kish effective sample size.
Below about 100, HC1 covered 84% to 88% for OLS and Poisson alike. For
OLS vce='hc3' held 93% to 94% in the same designs.
Unbalanced panels in staggered DiD¶
Estimators that compare a unit with itself are robust to cells that go
missing at random and to units that leave according to their level. The
default of sp.callaway_santanna, sp.did_imputation and two-way fixed
effects all covered 94% to 97% in both cases.
sp.callaway_santanna(allow_unbalanced_panel=True) keeps every observed
row by comparing group means. That needs each group's composition to be
stable over time. When treated units with a high level left the panel,
the estimate was biased by 40% of the effect and the interval covered
50% with 100 units and 3% with 400. Use it when cells are missing for
reasons unrelated to the unit, and keep the default otherwise.
Nothing repairs attrition on the outcome itself. When treated cells with a low outcome went missing, every estimator was biased by 38% to 50% of the effect.
A discrete running variable in RD¶
sp.rdrobust held its coverage down to 20 support points on each side of
the cutoff. At 10 and 5 it covered 89% with 1,000 observations and 69%
with 4,000, because more data narrows the interval around a biased
value. sp.rd_discrete and sp.rdrandinf are built for that case.
Do not cluster on the support points of the running variable. That made coverage worse at every level, 86% at 50 points a side and 22% at 5.
The learner in DML¶
No first-stage learner was right under both kinds of confounding.
| confounding | OLS, Lasso | random forest | gradient boosting (default) | model averaging |
|---|---|---|---|---|
| linear | 94% to 95% | 91% to 93% | 90% to 92% | 94% to 95% |
| nonlinear | 0% | 43% to 67% | 87% to 90% | 73% to 87% |
The reported standard error matched the spread of the estimates in every design. The intervals failed because of bias in the nuisance fits, which a standard error does not carry. The available check is to refit with a different learner and see whether the estimate moves by more than its standard error.
a = sp.dml(df, y="y", treat="d", covariates=xs, model="plr")
b = sp.dml_model_averaging(df, y="y", treat="d", covariates=xs)
abs(a.estimate - b.estimate) / a.se
Weak instruments and poor overlap¶
These two come from the Track B stress designs rather than the studies
above. 2SLS with a first-stage F near 3 covered 88%; use
sp.anderson_rubin_ci there. A causal forest with propensity scores
near 0 or 1 covered 90% on 300 replications; look at sp.overlap_plot
and trim before fitting.