Skip to main content

Conditional extrapolation pre-testing for difference-in-differences in Python

Project description

pretest

Conditional Extrapolation Pre-Testing for Difference-in-Differences

Python 3.11+ License: AGPL-3.0 Version: 0.1.0

pretest

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 SpreM, then SpostSpre.

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"]0
  • snapshot.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

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:

  1. Choose the mode before reading the conditional interval.
  2. Use iterative mode when bounding period-to-period violations; overall mode when bounding cumulative divergence.
  3. 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:

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pretest_did-0.1.0.tar.gz (8.5 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pretest_did-0.1.0-py3-none-any.whl (160.3 kB view details)

Uploaded Python 3

File details

Details for the file pretest_did-0.1.0.tar.gz.

File metadata

  • Download URL: pretest_did-0.1.0.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

Hashes for pretest_did-0.1.0.tar.gz
Algorithm Hash digest
SHA256 dcfb2d1d2549ac9cb5a4155d219a190adeaa6d696738a22cc51b093befcc85d3
MD5 608e0e1351b4fc61541083a84992eeb1
BLAKE2b-256 6cd24af92a55fcf15fd45c4655ad5de4e58072399aa67892f77d4f5cc3699325

See more details on using hashes here.

File details

Details for the file pretest_did-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pretest_did-0.1.0-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

Hashes for pretest_did-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c2a021e4227f45ecc5136d3d7f9f1aa6dcc3e2ef0cd4ebc40c6d619bfaa22bec
MD5 27163da624f8fe2312974573c2c160ef
BLAKE2b-256 ba4223b966a585503f19c7a1cd420acb1795eca318d5b27374d014ac0ae50a2a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page