Quantile & distributional treatment effects — the full family¶
"The ATE is misleading when treatment redistributes outcomes." A policy can leave the mean unchanged while compressing or spreading the distribution — only QTE and distributional-TE catch that. StatsPAI v1.4 ships the full 2025-2026 frontier.
The Average Treatment Effect answers: "how much did the mean outcome move?" But for policy, clinical, and fairness work you usually care about who got moved. Did the bottom decile benefit? Did treatment widen inequality? Those questions need quantile or distributional effects.
Every function below is at top level: sp.qdid, sp.qte,
sp.distributional_te, sp.dist_iv, sp.kan_dlate,
sp.beyond_average_late, sp.qte_hd_panel, sp.cic.
The three levels of granularity¶
| Level | What it answers | Estimator(s) |
|---|---|---|
| Average | "Did the mean go up?" | sp.did, sp.iv, sp.dml |
| Quantile (τ-th) | "Did the bottom 10%? The median? The top 10%?" | sp.qte, sp.qdid, sp.cic |
| Whole distribution | "How did the shape change?" | sp.distributional_te, sp.dist_iv |
Conditional QTE under unconfoundedness — sp.qte¶
The default when you have observational data with X rich enough for
selection-on-observables:
r = sp.qte(
data=df, y="wage", treatment="job_program",
controls=["age", "education", "experience"],
quantiles=[0.1, 0.25, 0.5, 0.75, 0.9],
method="quantile_regression", # or "ipw"
n_boot=500,
)
r.plot() # QTE curve with bootstrap uniform band
Reference: Firpo (2007), Econometrica 75(1). IPW variant: Firpo (2007) §4 with the propensity score.
Quantile DiD — sp.qdid¶
Two-period, two-group DiD at each quantile. Parallel-trends is replaced by the quantile parallel trends assumption: "in the absence of treatment, the τ-th quantile of the treated group would have moved the same way as the τ-th quantile of the control group."
r = sp.qdid(
data=df, y="y", group="treated", time="post",
quantiles=[0.1, 0.25, 0.5, 0.75, 0.9],
n_boot=500,
)
r.summary() # QTE(τ) for each τ with placebo-based CI
Reference: Athey & Imbens (2006), Econometrica 74(2).
Changes-in-changes (CiC) — sp.cic¶
Weaker than QDiD: CiC requires only that the rank of each unit within its group's outcome distribution is stable over time in the absence of treatment. No parallel-trends. Works when outcome distributions have different shapes across groups.
r = sp.cic(
data=df, y="earnings", group="treated", time="post",
quantiles=np.linspace(0.05, 0.95, 19),
n_boot=500,
)
Reference: Athey & Imbens (2006), Econometrica. The same paper proves CiC nests QDiD when the distributions happen to share scale.
Distributional TE (whole density/CDF) — sp.distributional_te¶
Instead of a QTE curve at a handful of quantiles, distributional_te
estimates the entire counterfactual CDF:
r = sp.distributional_te(
data=df, y="income", treatment="program",
x=["age", "education"],
method="ipw", # or "regression"
n_grid=200,
n_boot=500,
)
r.plot_cdf() # control CDF vs counterfactual-under-treatment CDF
r.plot_pdf() # with bootstrap uniform band
When to use it: when you care about stochastic-dominance claims ("treatment first-order dominates control") or Lorenz-curve-style inequality summaries.
Distributional IV — sp.dist_iv (Sharma-Xue 2025)¶
IV at every quantile, with a uniform band over τ:
r = sp.dist_iv(
data=df, y="earnings", treat="schooling",
instrument="compulsory_reform",
covariates=["age", "region"],
quantiles=np.linspace(0.1, 0.9, 17),
n_boot=200,
)
Identification: extends Abadie (2002) QTE-under-compliance to a continuous grid of τ with uniform-over-τ confidence coverage, which is what you need to report "schooling raises wages at every quantile" or "the top-decile return is twice the bottom-decile return."
Citation: Holovchak, Saengkyongam, Meinshausen & Shen (2025), arXiv:2502.07641.
KAN D-LATE — sp.kan_dlate (Kennedy et al. 2025)¶
A Kolmogorov-Arnold-Network-powered variant of dist_iv that replaces
the linear bridge function with a flexible KAN surface. Same call shape
as dist_iv, swap in when you suspect the return-to-schooling (say) is
strongly non-linear in D.
r = sp.kan_dlate(
data=df, y="y", treat="d", instrument="z",
covariates=["x1", "x2"],
quantiles=np.linspace(0.1, 0.9, 9),
)
Citation: Shaw (2025), arXiv:2506.12765.
Beyond-average LATE — sp.beyond_average_late (Xie-Wu 2025)¶
The Angrist-Imbens LATE gives the treatment effect among compliers (units whose treatment would flip if the instrument flipped). But that's an average over the complier subgroup. Xie-Wu identify the whole distribution of treatment effects among compliers:
r = sp.beyond_average_late(
data=df, y="y", treat="d", instrument="z",
quantiles=np.linspace(0.1, 0.9, 9),
n_boot=200,
)
When it matters: if the complier class has a heavy-tailed treatment- effect distribution, the mean LATE hides heterogeneity that a decile or quantile summary reveals. Classic example: schooling returns vary dramatically across compliers by ability.
Citation: Byambadalai, Hirata, Oka & Yasui (2025), arXiv:2509.15594.
High-dimensional panel QTE — sp.qte_hd_panel (Fan et al. 2025)¶
Panel data with many covariates? qte_hd_panel fits an ℓ1-penalised
quantile regression per-τ with a Lasso nuisance step:
r = sp.qte_hd_panel(
data=df, y="y", treat="d",
unit="county", time="year",
covariates=[f"x{j}" for j in range(50)],
quantiles=np.linspace(0.1, 0.9, 9),
lasso_alpha=0.01,
)
When to use: many controls (p ≥ 20 or so), panel DiD-flavoured
identification, and you care about the τ curve. Honest post-Lasso
inference follows Belloni-Chernozhukov-Fernández-Val (2017).
Citation: Xu & Zheng (2025), arXiv:2504.00785.
Decision guide¶
Cross-section + unconfoundedness:
ATE only → sp.dml / sp.aipw
QTE at some τ → sp.qte
Full distribution → sp.distributional_te
DiD design:
Mean ATT → sp.did
QTE(τ) at each τ → sp.qdid
Rank-preserving → sp.cic
IV design:
LATE (mean) → sp.iv
Distributional LATE → sp.dist_iv (linear) / sp.kan_dlate (KAN)
Beyond-average LATE → sp.beyond_average_late (distribution among compliers)
Panel with many controls:
→ sp.qte_hd_panel
Sanity checks to report¶
- Monotonicity in τ — the QTE curve should move smoothly with τ; wild oscillations signal overfitting or instability.
- Uniform band coverage — report the Kolmogorov-Smirnov-style uniform band, not just pointwise CIs at each τ.
- Stochastic dominance test —
sp.stochastic_dominance(r)tells you if the treated-CDF first-order dominates control. - Common-support diagnostic — for IV/IPW-based QTE, verify the propensity overlap at every τ you report.
Current for StatsPAI ≥ 1.5.0. All functions are registered; inspect
with sp.describe_function("beyond_average_late"), etc.
For Agents¶
Pre-conditions - binary or continuous treatment (method='quantile_regression' supports both; 'ipw' needs binary) - continuous outcome - controls cover the confounding set (for 'quantile_regression') - overlap when method='ipw'
Identifying assumptions - For 'quantile_regression': unconfoundedness conditional on controls - For 'ipw': unconfoundedness + overlap 0 < e(x) < 1 - Correct parametric quantile model (sensitivity tested via multiple quantiles)
Failure modes → recovery
| Symptom | Exception | Remedy | Try next |
|---|---|---|---|
| Large IPW weights (method='ipw') | statspai.AssumptionViolation |
Extreme propensities — trim (sp.trimming) or switch to doubly-robust DR-QTE. | sp.trimming |
| Quantile crossing | statspai.AssumptionWarning |
Use rearrangement (Chernozhukov-Fernandez-Val-Galichon) or monotone constraints. |
Alternatives (ranked)
- sp.qdid
- sp.rifreg
- sp.cic
- sp.metalearner
Typical minimum N: 500
For Agents¶
Pre-conditions - panel or repeated cross-section - group is binary 0/1 - time is binary 0/1 (pre / post) - outcome is continuous
Identifying assumptions - CIC rank invariance: the quantile rank in the untreated distribution is stable across groups - Continuous outcome support covering both groups in both periods - SUTVA (no cross-group spillovers)
Failure modes → recovery
| Symptom | Exception | Remedy | Try next |
|---|---|---|---|
| Outcome heavily discrete / zero-inflated | statspai.AssumptionViolation |
CIC rank-matching is unstable on discrete supports — use QTE regression (sp.qte) or Firpo-RIF. | sp.qte |
| Bootstrap CI across quantiles varies wildly | statspai.DataInsufficient |
Thin tails at extreme quantiles — restrict to [0.2, 0.8] or raise n_boot to 2000. |
Alternatives (ranked)
- sp.qte
- sp.did
- sp.rifreg
Typical minimum N: 500