Conditional extrapolation pre-testing for difference-in-differences in Python
Project description
pretest
Conditional Extrapolation Pre-Testing for Difference-in-Differences
Overview
pretest implements the conditional extrapolation pre-test and interval-reporting
rule of Mikhaeil and Harshaw (2026) as a typed Python API. Given pre-treatment
violations, a covariance structure, and an analyst threshold M, it returns a
PretestResultSnapshot containing the pass/fail decision, conditional confidence
interval, conventional comparison interval, and all diagnostic scalars.
Under the conditional extrapolation assumption, if pre-treatment violations do not exceed M, the conditional interval becomes reportable:
Assumption 3 (Conditional Extrapolation): If Spre ≤ M, then Spost ≤ Spre.
First Run:
import pretest
df, dgp = pretest.generate_did_data_from_preset("small_violation", n_units=200, seed=42)
snapshot = pretest.pretest_from_dataframe(
df, outcome="outcome", treatment="treatment", time="time",
threshold=1.0, treat_time=6, mode="iterative", seed=42,
)
print(snapshot.reporting_summary()["decision"]) # "PASS" or "FAIL"
Requirements
| Requirement | Description |
|---|---|
| Python | 3.11 or higher |
| Minimum 3 time periods | Tpre ≥ 2. Iterative violations ν̂t are defined for t ≥ 2. |
| Block adoption design | All treated units share a single treatment time t0. Staggered adoption is not supported. |
| Binary treatment | Coded as 0 (control) or 1 (treated). |
| Complete time-group cells | Each period must have observations in both groups. |
Data Completeness
When some time periods lack observations for either group, the covariance matrix
cannot be computed. The API returns an invalid PretestResultSnapshot:
snapshot.reporting_summary()["decision"]→"INVALID"snapshot.reporting_summary()["data_valid"]→0snapshot.conditional_interval()→None
Two-Period Designs
This package cannot be used for canonical 2×2 DID designs with only two time periods (Tpre < 2).
Installation
pip install pretest-did
From GitHub:
pip install "git+https://github.com/gorgeousfish/pretest-py.git#subdirectory=pretest-py"
Verify:
python -m pretest --version
Quick Start
import pretest
# Generate a DID dataset with a small pre-treatment violation
df, dgp = pretest.generate_did_data_from_preset("small_violation", n_units=200, seed=42)
# Run the full pre-test pipeline
snapshot = pretest.pretest_from_dataframe(
df,
outcome="outcome",
treatment="treatment",
time="time",
threshold=1.0,
treat_time=6,
p=2.0,
alpha=0.05,
mode="iterative",
simulations=5000,
seed=42,
)
# Inspect the result
summary = snapshot.reporting_summary()
summary["decision"] # "PASS", "FAIL", "INVALID", or "UNKNOWN"
summary["S_pre"] # pre-treatment severity
summary["threshold"] # analyst threshold M
summary["conditional_interval"] # conditional CI bounds (None if FAIL)
summary["conventional_interval"] # comparison interval bounds
API Reference
Core Functions
| Function | Description | Reference |
|---|---|---|
pretest_from_dataframe(...) |
End-to-end pre-test from a DataFrame | Theorems 1–2 |
compute_pretest_snapshot(...) |
Snapshot from pre-computed kernel inputs | Theorems 1–2 |
compute_severity(...) |
Compute Ŝpre from violations | §2.1 |
classify_pretest(...) |
Decision φ = 𝟙{Ŝpre > M} | Theorem 1 |
compute_kappa(...) |
Bias-bound constant κ | Theorem 2 |
compute_ci_half_width(...) |
Conditional CI half-width | Theorem 2 |
compute_critical_value(...) |
Monte Carlo critical value f(α, Σ̂) | §3 |
Pipeline
| Function | Description |
|---|---|
compute_pretest_snapshot_from_records(...) |
Snapshot from group-time CSV records |
compute_pretest_kernel_inputs_from_records(...) |
Extract kernel inputs from records |
load_prop99_window_iter_records_from_csv(...) |
Load Proposition 99 example records |
Simulation
| Function | Description |
|---|---|
simulate_coverage(...) |
Coverage from draw-level sequences |
simulate_coverage_from_covariance(...) |
Coverage from covariance specification |
compute_section6_violation_path(...) |
Section 6 violation path generator |
run_monte_carlo_coverage(...) |
Full Monte Carlo coverage experiment |
Sensitivity
| Function | Description |
|---|---|
compute_m_sensitivity(...) |
M-sensitivity analysis over a threshold grid |
Data Generating Processes
| Function | Description |
|---|---|
generate_did_data(...) |
Generate DID data from a DGPConfig |
generate_did_data_from_preset(...) |
Generate data from named presets |
compute_true_covariance(...) |
Population covariance for a DGP |
Options
| Option | Default | Description |
|---|---|---|
threshold |
— | Acceptable violation threshold M > 0 (required) |
p |
2 | Severity norm p ≥ 1 |
alpha |
0.05 | Significance level |
mode |
"iterative" |
"iterative" or "overall" |
simulations |
5000 | Monte Carlo draws for critical value |
seed |
12345 | Random seed |
cluster |
None |
Column name for cluster-robust SE |
Key Formulas
Pre-test (Theorem 1)
φ = 𝟙{Ŝpre > M}
φ = 0 → PASS (conditional interval reportable); φ = 1 → FAIL.
Average DID Estimand
Important: δ̄̂ is not the traditional ATT.
δ̂t = (Ȳt,D=1 − Ȳt₀,D=1) − (Ȳt,D=0 − Ȳt₀,D=0)
δ̄̂ = (1/Tpost) × Σt=t₀T δ̂t
| Aspect | Paper's δ̄̂ | Traditional ATT |
|---|---|---|
| Reference point | Treatment time t0 | Pre-treatment average |
| δ̂t₀ | Always 0 (by construction) | N/A |
| Interpretation | Incremental change from t0 | Total treatment effect |
Conditional Confidence Interval (Theorem 2)
Iterative mode (default):
I = δ̄̂ ± {κ · Ŝpre + f(α, Σ̂) / √n}
Overall mode:
IΔ = δ̄̂ ± {ŜΔpre + fΔ(α, Σ̂Δ) / √n}
κ Constant (Iterative Mode Only)
κ = ((1/Tpost) · Σt=1Tpost tq)1/q
where q is the Hölder conjugate of p. In overall mode κ = 1 (no multiplier).
Result Object
PretestResultSnapshot fields accessible via reporting_summary():
| Field | Description |
|---|---|
decision |
"PASS", "FAIL", "INVALID", or "UNKNOWN" |
data_valid |
1 if input data passed validation |
S_pre |
Pre-treatment severity |
threshold |
Analyst threshold M |
phi |
Pre-test indicator (0 = pass, 1 = fail) |
kappa |
Bias-bound constant κ |
delta_bar |
Average DID estimate δ̄̂ |
se_delta_bar |
Standard error of δ̄̂ |
f_alpha |
Simulated critical value |
simulations |
Number of Monte Carlo draws |
seed |
Random seed used |
conditional_interval |
(lower, upper) or None if test fails |
conventional_interval |
(lower, upper) comparison interval |
mode |
"iterative" or "overall" |
Additional methods:
snapshot.conditional_interval() # -> tuple[float, float] | None
snapshot.conventional_interval() # -> tuple[float, float] | None
snapshot.reporting_summary() # -> dict with all fields above
Mode Selection: Iterative vs. Overall
| Feature | Iterative Mode (Default) | Overall Mode (overall) |
|---|---|---|
| Assumption | Violations accumulate period-to-period | Violations bounded by cumulative total |
| Sensitivity | Sensitive to volatility/noise | Sensitive to drift/trend |
| Blind Spot | May pass smooth linear trends | May fail even if period-to-period changes are small |
| Bias Bound | Scaled by κ (∝ √Tpost) | No multiplier (κ = 1) |
| CI Width | Includes iterative κ multiplier | Appendix C kappa-free half-width |
Reporting guidance:
- Choose the mode before reading the conditional interval.
- Use iterative mode when bounding period-to-period violations; overall mode when bounding cumulative divergence.
- If the two modes disagree, treat the result as a diagnostic contrast.
Example
import numpy as np
import pretest
# Upstream event-study coefficients (e.g., from PyFixest or statsmodels)
relative_terms = (-2, -1, 0, 1, 2)
event_coefficients = np.array([0.125, -0.076, -0.271, -0.311, -0.240])
event_covariance = np.diag([0.0016] * 5) + 0.0008
# Extract pre/post indices
pre_idx = [i for i, t in enumerate(relative_terms) if t < 0]
post_idx = [i for i, t in enumerate(relative_terms) if t >= 0]
ordered = pre_idx + post_idx
# Compute kernel inputs
nu_vector = tuple(float(event_coefficients[i]) for i in pre_idx)
cov_matrix = tuple(
tuple(float(event_covariance[r, c]) for c in ordered)
for r in ordered
)
post_weights = np.ones(len(post_idx)) / len(post_idx)
delta_bar = float(post_weights @ event_coefficients[post_idx])
se_delta_bar = float(np.sqrt(
post_weights @ event_covariance[np.ix_(post_idx, post_idx)] @ post_weights
))
# Run pretest
snapshot = pretest.compute_pretest_snapshot(
"pretest outcome, treatment(treated) time(year) treat_time(2019) threshold(0.3)",
pretest.DatasetProfile(
time_periods=(2016, 2017, 2018, 2019, 2020, 2021),
treatment_values=(0, 1),
),
nu_vector=nu_vector,
covariance_matrix=cov_matrix,
delta_bar=delta_bar,
se_delta_bar=se_delta_bar,
sample_size=240,
simulations=5000,
seed=42,
)
summary = snapshot.reporting_summary()
print(f"Decision: {summary['decision']}")
print(f"S_pre: {summary['S_pre']:.4f}")
print(f"Conditional CI: {summary['conditional_interval']}")
print(f"Conventional CI: {summary['conventional_interval']}")
For a minimal snapshot from already-computed scalars:
import pretest
snapshot = pretest.compute_pretest_snapshot(
"pretest cigsale, treatment(treated) time(year) treat_time(1989) threshold(5)",
pretest.DatasetProfile(
time_periods=(1985, 1986, 1987, 1988, 1989, 1990, 1991),
treatment_values=(0, 1),
),
nu_vector=(1.0, 2.0, 3.0),
f_alpha=2.5,
delta_bar=1.5,
sample_size=100,
)
snapshot.reporting_summary()["decision"]
snapshot.conditional_interval()
M-Sensitivity Analysis
compute_m_sensitivity(...) evaluates how the pre-test decision and conditional
interval respond to a grid of threshold values:
import pretest
result = pretest.compute_m_sensitivity(
snapshot,
m_grid=[0.5, 1.0, 2.0, 5.0, 10.0],
)
result.pass_threshold # smallest M where the test passes
result.results # list of per-M snapshots
Not Implemented in 0.1.0
- Triple difference-in-differences designs
- Staggered treatment adoption
- Covariate-adjusted estimation
- Threshold-sensitivity plots over a continuous range of M values
References
Mikhaeil, J. M., & Harshaw, C. (2026). Valid Inference when Testing Violations of Parallel Trends for Difference-in-Differences. arXiv preprint arXiv:2510.26470v3. Available at: https://arxiv.org/abs/2510.26470
Rambachan, A., & Roth, J. (2023). A More Credible Approach to Parallel Trends. Review of Economic Studies, 90(5), 2555–2591. https://doi.org/10.1093/restud/rdad018
Roth, J. (2022). Pretest with Caution: Event-Study Estimates after Testing for Parallel Trends. American Economic Review: Insights, 4(3), 305–322. https://doi.org/10.1257/aeri.20210236
Authors
Python Implementation:
- Xuanyu Cai, City University of Macau Email: xuanyuCAI@outlook.com
- Wenli Xu, City University of Macau Email: wlxu@cityu.edu.mo
Methodology:
- Jonas M. Mikhaeil, Department of Statistics, Columbia University
- Christopher Harshaw, Department of Statistics, Columbia University
License
AGPL-3.0. See LICENSE for details.
Citation
If you use this package in your research, please cite:
APA Format:
Cai, X., & Xu, W. (2026). pretest: Conditional Extrapolation Pre-Testing for Difference-in-Differences (Version 0.1.0) [Computer software]. https://github.com/gorgeousfish/pretest-py
Mikhaeil, J. M., & Harshaw, C. (2026). Valid Inference when Testing Violations of Parallel Trends for Difference-in-Differences. arXiv preprint arXiv:2510.26470v3. https://arxiv.org/abs/2510.26470
BibTeX:
@software{cai2026pretest,
author = {Cai, Xuanyu and Xu, Wenli},
title = {pretest: Conditional Extrapolation Pre-Testing for Difference-in-Differences},
year = {2026},
version = {0.1.0},
url = {https://github.com/gorgeousfish/pretest-py}
}
@misc{mikhaeil2026validinferencetestingviolations,
title={Valid Inference when Testing Violations of Parallel Trends for Difference-in-Differences},
author={Jonas M. Mikhaeil and Christopher Harshaw},
year={2026},
eprint={2510.26470},
archivePrefix={arXiv},
primaryClass={stat.ME},
url={https://arxiv.org/abs/2510.26470},
}
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pretest_did-0.1.1.tar.gz.
File metadata
- Download URL: pretest_did-0.1.1.tar.gz
- Upload date:
- Size: 8.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5626a617f5d4e6eecff206db024889da9ad925104113a2512fd78255c19b042c
|
|
| MD5 |
cc488320dd0e2461549b08a35eca1d77
|
|
| BLAKE2b-256 |
0c0dcc6a552de146d8667a76de0afb920029532352172fd3e55ad05052953b44
|
File details
Details for the file pretest_did-0.1.1-py3-none-any.whl.
File metadata
- Download URL: pretest_did-0.1.1-py3-none-any.whl
- Upload date:
- Size: 160.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14145a46301a3689068a27c59db8161fafb933e6594d560e69fc03c33e9abfb2
|
|
| MD5 |
188033cd51bd83903960f198409d9d77
|
|
| BLAKE2b-256 |
be44e49cfa92e46f1408264b2ce734ae7b059dae1fd3ebde716efb0021a45bec
|