Skip to main content

Python port of EquiTrends pre-trend equivalence testing tools.

Project description

equitrends

Equivalence Tests for Pre-Trends in DiD Estimation

Python Version License

Reversing the Burden of Proof

equitrends is a Python package implementing equivalence tests for pre-trends in Difference-in-Differences (DiD) designs, based on Dette & Schumann (2024), published in the Journal of Business & Economic Statistics.

Contents

Overview

Standard pre-trend tests in DiD designs test the null hypothesis of exact parallel trends (H₀: β = 0). This approach suffers from fundamental limitations:

  1. Failure to reject ≠ Evidence in favor: Low statistical power may prevent detection of actual violations. Failing to reject does not support the parallel trends assumption.
  2. Conditional bias amplification: Roth (2022) demonstrates that conditioning on passing traditional pre-tests can amplify DiD bias when violations exist.
  3. No explicit threshold: Traditional tests provide no framework for determining what constitutes a "negligible" deviation from parallel trends.

equitrends implements equivalence tests that reverse the burden of proof: the null hypothesis is that deviations are large (H₀: ‖β‖ ≥ threshold), and rejection provides statistical evidence that deviations are small. This approach:

  • Requires explicit justification of the equivalence threshold
  • Controls Type I error (falsely concluding equivalence)
  • Increases statistical power with sample size
  • Allows researchers to quantify the smallest threshold at which equivalence holds

Key features

  • Three equivalence hypotheses: maximum, mean, and RMS (Dette & Schumann, 2024, Section 3.1)
  • Minimum equivalence threshold: compute the smallest threshold at which equivalence can be concluded
  • Multiple inference methods for the maximum test: IU (analytical), spherical bootstrap, and wild bootstrap
  • Visualization: coefficient plots with equivalence bounds (plot_equivtest)
  • CLI tools: JSON-emitting command-line entry points for scripted workflows
  • Bundled empirical dataset: Di Tella & Schargrodsky (2004) crime panel

Requirements

  • Python 3.10 or higher
  • numpy ≥ 2.0
  • pandas ≥ 2.0
  • scipy ≥ 1.12
  • matplotlib ≥ 3.8

Installation

pip install equitrends

From a local checkout (editable install):

cd equitrends-py
pip install -e .

Data requirements

Before using this package, ensure your data meets the following requirements:

Requirement Description
Panel structure Panel data with individual (id) and time (period) identifiers. Both balanced and unbalanced panels are supported.
Minimum pre-treatment periods At least one pre-treatment period (T ≥ 1). More periods increase statistical power.
Treatment group indicator Binary variable coded as 0 (control) or 1 (treated).
Block adoption design All treated units must receive treatment at the same time. Staggered adoption requires cohort-specific analysis (see Dette & Schumann, 2024, Section 5).
Complete time-group cells Each time period must contain observations in both treatment and control groups.

Unbalanced panels

The package automatically detects and handles unbalanced panels (where individuals have different numbers of observed time periods). No special syntax is required—simply pass the data as usual.

Data completeness

When some time periods lack observations for either group, the placebo regression cannot be estimated correctly. Ensure at least one observation per time-treatment cell, or restrict analysis to time periods with complete coverage.

Quick start

Empirical example: Di Tella & Schargrodsky (2004)

This example replicates the empirical application in Dette & Schumann (2024, Section 7) using the Buenos Aires crime data from Di Tella & Schargrodsky (2004). The dataset contains monthly car theft counts for 876 Buenos Aires city blocks (April–December 1994), of which 37 blocks received police protection after a July terrorist attack.

from equitrends import equiv_test, load_dataset, plot_equivtest

# Load and prepare data
panel = load_dataset("MonthlyPanel")
panel = panel.rename(columns={"observ": "ID", "totrob": "Y", "mes": "period"}).copy()
panel["G"] = (panel["distanci"] == 0).astype(int)

# Maximum test (IU method, cluster-robust SE)
result = equiv_test(
    type="max",
    method="IU",
    data=panel,
    y="Y",
    id="ID",
    g="G",
    period="period",
    pretreatment_period=[4, 5, 6, 7],
    base_period=7,
    vcov="cluster",
    cluster="ID",
)
print(result)

Outpu:

==================================================
Equivalence Tests for Pre-trends in DiD Estimation
==================================================
Type: Intersection Union
Significance level: 0.05
Alternative hypothesis: the maximum placebo effect does not exceed the equivalence threshold.
---
Abs. Estimate  Std. Error  Min. Equiv. Threshold
        0.032     0.06167                 0.1313
        0.009     0.03652                 0.0605
        0.073     0.04515                 0.1472
---
No. placebo coefficients estimated: 3
Base period: 7

Balanced Panel:
 + No. pre-treatment periods: 4
 + No. individuals: 876
 + Total no. observations: 3504
# Mean test
result_mean = equiv_test(
    type="mean",
    data=panel,
    y="Y",
    id="ID",
    g="G",
    period="period",
    pretreatment_period=[4, 5, 6, 7],
    base_period=7,
)

# RMS test
result_rms = equiv_test(
    type="rms",
    data=panel,
    y="Y",
    id="ID",
    g="G",
    period="period",
    pretreatment_period=[4, 5, 6, 7],
    base_period=7,
    seed=2024,
)

# Visualize results
ax = plot_equivtest(result)

Bootstrap methods (maximum test only)

# Spherical bootstrap (assumes spherical errors; Theorem 1)
result_boot = equiv_test(
    type="max",
    method="boot",
    data=panel,
    y="Y",
    id="ID",
    g="G",
    period="period",
    pretreatment_period=[4, 5, 6, 7],
    base_period=7,
    B=1000,
    seed=12345,
)

# Wild bootstrap (recommended for non-spherical errors; Remark 1(c))
result_wild = equiv_test(
    type="max",
    method="wild",
    data=panel,
    y="Y",
    id="ID",
    g="G",
    period="period",
    pretreatment_period=[4, 5, 6, 7],
    base_period=7,
    B=1000,
    seed=12345,
)

Control variables (conditional parallel trends)

The x argument includes additional control variables in the TWFE placebo regression (Dette & Schumann, 2024, Section 5). Time-invariant covariates are absorbed by the individual fixed effects during double demeaning.

result = equiv_test(
    type="max",
    method="IU",
    data=panel,
    y="Y",
    id="ID",
    g="G",
    period="period",
    pretreatment_period=[4, 5, 6, 7],
    base_period=7,
    x=["edpub", "estserv", "banco"],
    vcov="cluster",
    cluster="ID",
)

API reference

Function Description
equiv_test Unified interface for all three tests (recommended)
max_equiv_test Maximum absolute coefficient test (IU/Boot/Wild)
mean_equiv_test Mean coefficient test
rms_equiv_test Root mean square (RMS) test
plot_equivtest Coefficient plot with equivalence bounds
prepare_equivtest_plot_data Extract plot-ready data without rendering
sim_panel_data Generate R-compatible simulated panel
load_dataset Load bundled example datasets
list_datasets Inspect available bundled datasets

Unified syntax

from equitrends import equiv_test

result = equiv_test(
    type="max|mean|rms",
    data=panel,
    y="Y",
    id="ID",
    g="G",
    period="period",
    # ... options
)

Core options

Parameter Description
type Test type:"max", "mean", or "rms" (required)
data pandas DataFrame with panel data (required)
y Outcome variable name (required)
id Panel identifier column (required)
g Treatment group indicator 0/1 (required)
period Time period variable (required)
equiv_threshold Equivalence threshold; omit to compute minimum threshold
alpha Significance level; default 0.05
pretreatment_period List of pre-treatment periods to include
base_period Base period for placebo construction
x List of control variable names

Options specific to type="max":

Parameter Description
method Inference method:"IU", "boot", or "wild"
B Bootstrap replications; default 1000
seed Random seed for bootstrap

Options specific to type="rms":

Parameter Description
no_lambda Number of subsamples for self-normalization; default 5
seed Random seed for subsampling

Variance estimator options (available for IU/mean; not for bootstrap or RMS):

Parameter Description
vcov Variance estimator type; see table below
cluster Cluster variable name (required for cluster-robust types)

Variance estimator types (vcov):

Type Description
"ols" Homoskedastic OLS variance (default)
"robust" / "hc1" HC1 heteroskedasticity-robust (White, 1980)
"hc2" HC2 leverage-adjusted (MacKinnon & White, 1985)
"hc3" HC3 more conservative leverage adjustment (Davidson & MacKinnon, 1993)
"hac" Arellano (1987) HAC estimator for panel data
"cluster" / "cr0" CR0 cluster-robust without small-sample adjustment; requirescluster
"cr1" CR1 cluster-robust with G/(G-1) adjustment; requirescluster
"hc1_cluster" HC1 cluster-robust with small-sample adjustment; requirescluster

Test selection guide

equitrends offers three equivalence tests with different properties. Choose based on your research context:

Feature Maximum test Mean test RMS test
Hypothesis max|βₜ| < δ |β̄| < τ β_RMS < ζ
Measures Largest single violation Average violation Root mean square
Cancellation No Yes (opposing signs cancel) No
Sensitivity Any single large deviation Systematic directional bias Balanced across deviations
Conservativeness Most conservative Least conservative Moderate

Recommendations

  1. Maximum test (type="max"): Start here as the default, conservative choice.

    • Detects any single large violation
    • Use method="IU" for analytical inference or method="wild" for non-spherical errors
    • Recommended when you want to rule out any substantial pre-trend violation
  2. Mean test (type="mean"): Use when violations are expected to be monotone (same sign).

    • More powerful when deviations are directionally consistent
    • Caution: Opposing violations may cancel out, leading to false equivalence
  3. RMS test (type="rms"): Use as a general-purpose alternative.

    • Balances sensitivity across all placebo coefficients
    • No cancellation problem
    • Self-normalized (no variance estimation required)

Interpreting minimum thresholds

When equiv_threshold is omitted, equitrends reports the smallest equivalence threshold (δ*, τ*, or ζ*) at which equivalence can be concluded at the specified significance level. Compare this to your estimated treatment effect:

  • δ* << estimated ATT: Strong evidence for negligible pre-trends
  • δ* ≈ estimated ATT: Pre-trend violations may explain the treatment effect
  • δ* >> estimated ATT: Insufficient evidence for parallel trends; consider alternative designs

Methodology

Let $\beta = (\beta_1,\ldots,\beta_T)'$ denote the vector of placebo (pre-treatment) coefficients from the TWFE placebo regression (Dette & Schumann, 2024, Eq. (2.5)). equitrends implements three equivalence hypotheses (Section 3.1):

  1. Maximum deviation (Eq. (3.1)):

$$ H_0: |\beta|{\infty} \ge \delta \quad \text{vs.} \quad H_1: |\beta|{\infty} < \delta, \qquad |\beta|{\infty}=\max{l\in{1,\ldots,T}}|\beta_l| $$

  1. Mean deviation (Eq. (3.2)):

$$ \bar{\beta}=\frac{1}{T}\sum_{l=1}^{T}\beta_l, \qquad H_0: |\bar{\beta}| \ge \tau \quad \text{vs.} \quad H_1: |\bar{\beta}| < \tau $$

  1. RMS deviation (Eq. (3.3)):

$$ \beta_{\mathrm{RMS}}=\sqrt{\frac{1}{T}\sum_{l=1}^{T}\beta_l^2}, \qquad H_0: \beta_{\mathrm{RMS}} \ge \zeta \quad \text{vs.} \quad H_1: \beta_{\mathrm{RMS}} < \zeta $$

Inference methods for the maximum test

  • IU (Intersection-Union, analytical): For each placebo coefficient t = 1, ..., T, the test rejects H0 iff all |beta_t| < Q(alpha), where Q denotes the alpha-quantile of the folded normal distribution with mean delta and variance sigma_tt/n (Dette & Schumann, 2024, Eq. (4.4)). Computationally attractive but conservative for large T.
  • Bootstrap (method="boot"): Generates bootstrap samples under the constraint on beta using constrained OLS, then computes the empirical alpha-quantile as the critical value (Dette & Schumann, 2024, Theorem 1). Assumes spherical errors. More powerful than IU for T > 1.
  • Wild bootstrap (method="wild"): Replaces i.i.d. bootstrap errors with Rademacher-weighted residuals, making the test robust to heteroskedasticity and serial correlation (Dette & Schumann, 2024, Remark 1(c)). Recommended for non-spherical errors.

Inference for the mean test

The mean test rejects H0 whenever the absolute sample mean of placebo coefficients falls below the alpha-quantile of the folded normal with mean tau and variance 1'Sigma1/(nT^2) (Dette & Schumann, 2024, Eq. (4.12)).

Inference for the RMS test

The RMS test uses a self-normalized statistic based on subsampling (Dette & Schumann, 2024, Theorem 2). It rejects H0 whenever beta_RMS^2 < zeta^2 + Q_W(alpha) * V_n, where Q_W(alpha) is the alpha-quantile of the limiting distribution (a functional of Brownian motion) and V_n is computed from subsample estimates (Eq. (4.18)). This test is pivotal and does not require variance estimation.

RMS test alpha restriction

The RMS test supports only:

$$ \alpha \in {0.01, 0.025, 0.05, 0.1, 0.2} $$

This reflects the implementation based on critical values for the limiting distribution in Dette & Schumann (2024, Theorems 2-3).

Result objects

Every test call returns a typed result object. The object keeps the fitted panel available for interactive inspection, while to_dict() returns a machine-readable payload for scripts, CLI JSON, and manuscript tables.

result = equiv_test(
    type="max", method="IU", data=panel,
    y="Y", id="ID", g="G", period="period",
    pretreatment_period=[4, 5, 6, 7], base_period=7,
)

payload = result.to_dict()

Common keys

Key Description
test_type Test type:"max", "mean", or "rms"
method Inference method (max test only)
significance_level Significance level used
equiv_threshold Specified threshold (orNone if omitted)
equiv_threshold_specified True if threshold was specified
base_period Base period for placebo construction
num_individuals Number of individuals (panels)
num_periods Number of pre-treatment periods
num_observations Total number of observations
is_panel_balanced True if balanced panel
placebo_names Names of placebo coefficient periods
minimum_equiv_threshold Smallest threshold at which equivalence holds (if not specified)
reject_null_hypothesis True if H₀ rejected (if threshold specified)

Type-specific keys for type="max":

Key Description
placebo_coefficients Placebo coefficient vector
abs_placebo_coefficients Absolute values of placebo coefficients
max_abs_coefficient Maximum absolute placebo coefficient
placebo_coefficients_se Standard errors (IU method)
minimum_equiv_thresholds Per-coefficient minimum thresholds (IU method)
iu_critical_values Critical values per coefficient (threshold specified)
bootstrap_critical_value Bootstrap critical value (boot/wild methods)
B Number of bootstrap replications

Visualization

plot_equivtest creates coefficient plots of placebo coefficients with equivalence bounds. Pass a result object directly:

from equitrends import equiv_test, load_dataset, plot_equivtest

panel = load_dataset("MonthlyPanel")
panel = panel.rename(columns={"observ": "ID", "totrob": "Y", "mes": "period"}).copy()
panel["G"] = (panel["distanci"] == 0).astype(int)

result = equiv_test(
    type="max", method="IU", data=panel,
    y="Y", id="ID", g="G", period="period",
    pretreatment_period=[4, 5, 6, 7], base_period=7,
    vcov="cluster", cluster="ID",
)

# Basic plot
ax = plot_equivtest(result)

# Plot with confidence intervals
ax = plot_equivtest(result, ci=True)

# Publication-quality plot
ax = plot_equivtest(
    result,
    ci=True,
    connect=True,
    figsize=(7.2, 4.4),
    title="Pre-trend Equivalence Analysis",
    subtitle="Minimum max-IU equivalence bound: ±0.1472",
)

Plot-ready data extraction

To inspect the plotting payload without rendering a Matplotlib figure:

from equitrends import prepare_equivtest_plot_data, EquivTestPlotData

plot_data = prepare_equivtest_plot_data(result)
assert isinstance(plot_data, EquivTestPlotData)
plot_frame = plot_data.to_frame()

Output:

   period  relative_time  coefficient  is_base_period
0       4             -3     0.032221           False
1       5             -2     0.008867           False
2       6             -1     0.072947           False
3       7              0     0.000000            True

CLI entry points

The installed console scripts emit JSON to stdout:

Command Description
equivtest Unified interface (specify--type)
maxequivtest Maximum test
meanequivtest Mean test
rmsequivtest RMS test
equivsim Generate simulated panel data (CSV output)
# Run maximum test from CSV input
equivtest --type max --data panel.csv --y Y --id ID --g G \
    --period period --pretreatment-period 1 2 3 4 --base-period 4

# Save result to file
equivtest --type max --data panel.csv --y Y --id ID --g G \
    --period period --pretreatment-period 1 2 3 4 --base-period 4 \
    --output result.json

# Check version
equivtest --version

# Generate simulated panel
equivsim --preperiods 4 --rcompat --beta 0 0 0 0 --seed 123 --output sim.csv

Examples

Simulated panel data

This example demonstrates the package using simulated data with parallel trends satisfied.

from equitrends import equiv_test, sim_panel_data

# Generate simulated panel data (N=100 individuals, T=5 pre-treatment periods)
sim = sim_panel_data(
    N=100,
    tt=5,
    beta=[0.0, 0.0, 0.0, 0.0, 0.0],
    p=1,
    gamma=[1.0],
    het=0,
    phi=0.0,
    sd=1.0,
    burnins=10,
    seed=12345,
)

# Run maximum test with IU method
result = equiv_test(
    type="max",
    method="IU",
    data=sim,
    y="Y",
    id="ID",
    g="G",
    period="period",
    pretreatment_period=[1, 2, 3, 4, 5],
    base_period=5,
)
print(result)

Output:

==================================================
Equivalence Tests for Pre-trends in DiD Estimation
==================================================
Type: Intersection Union
Significance level: 0.05
Alternative hypothesis: the maximum placebo effect does not exceed the equivalence threshold.
---
Abs. Estimate  Std. Error  Min. Equiv. Threshold
        0.054     0.41158                 0.5022
        0.139     0.41158                 0.7663
        0.180     0.41158                 0.8309
        0.043     0.41158                 0.4117
---
No. placebo coefficients estimated: 4
Base period: 5

Balanced Panel:
 + No. pre-treatment periods: 5
 + No. individuals: 100
 + Total no. observations: 500

Testing with a pre-specified threshold

from equitrends import equiv_test, load_dataset

panel = load_dataset("MonthlyPanel")
panel = panel.rename(columns={"observ": "ID", "totrob": "Y", "mes": "period"}).copy()
panel["G"] = (panel["distanci"] == 0).astype(int)

# Test whether max violation < 0.2
result = equiv_test(
    type="max",
    method="IU",
    data=panel,
    y="Y",
    id="ID",
    g="G",
    period="period",
    pretreatment_period=[4, 5, 6, 7],
    base_period=7,
    equiv_threshold=0.2,
)
print(result)

Output:

==================================================
Equivalence Tests for Pre-trends in DiD Estimation
==================================================
Type: Intersection Union
Alternative hypothesis: the maximum placebo effect does not exceed the equivalence threshold of 0.2.
---
Abs. Estimate  Std. Error  Critical Value
        0.032     0.05021         0.11740
        0.009     0.05021         0.11740
        0.073     0.05021         0.11740
Reject H0: TRUE
---
No. placebo coefficients estimated: 3
Base period: 7

Balanced Panel:
 + No. pre-treatment periods: 4
 + No. individuals: 876
 + Total no. observations: 3504

Interpretation: At α = 0.05, all absolute placebo coefficients fall below the critical value of 0.117, so H₀ (non-equivalence) is rejected. We conclude that the maximum absolute placebo coefficient is less than 0.2, supporting negligible pre-trend violations.

Bundled dataset

from equitrends import list_datasets, load_dataset

# Inspect available datasets
datasets = list_datasets()
# {'MonthlyPanel': 'Di Tella and Schargrodsky (2004) crime data for empirical examples'}

# Load the bundled panel
panel = load_dataset("MonthlyPanel")
# Shape: (9636, 12)
# Columns: ['observ', 'barrio', 'calle', 'altura', 'institu1', 'institu3',
#            'distanci', 'edpub', 'estserv', 'banco', 'totrob', 'mes']

Citation

If you use this package in your research, please cite both the Python implementation and the methodology paper:

APA Format:

Cai, X., & Xu, W. (2026). equitrends: Python package for equivalence tests for pre-trends in DiD (Version 0.1.0) [Computer software]. GitHub. https://github.com/gorgeousfish/equitrends

Dette, H., & Schumann, M. (2024). Testing for Equivalence of Pre-Trends in Difference-in-Differences Estimation. Journal of Business & Economic Statistics, 42(4), 1289–1301. https://doi.org/10.1080/07350015.2024.2308121

BibTeX:

@software{equitrends2026python,
  title={equitrends: Python package for equivalence tests for pre-trends in DiD},
  author={Cai, Xuanyu and Xu, Wenli},
  year={2026},
  version={0.1.0},
  url={https://github.com/gorgeousfish/equitrends}
}

@article{dette2024testing,
  title={Testing for Equivalence of Pre-Trends in Difference-in-Differences Estimation},
  author={Dette, Holger and Schumann, Martin},
  journal={Journal of Business \& Economic Statistics},
  volume={42},
  number={4},
  pages={1289--1301},
  year={2024},
  publisher={Taylor \& Francis},
  doi={10.1080/07350015.2024.2308121}
}

Authors

Python Implementation:

Methodology:

  • Holger Dette, Department of Mathematics, Ruhr University Bochum
  • Martin Schumann, School of Business and Economics, Maastricht University

License

AGPL-3.0 License. See LICENSE.

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

equitrends-0.1.1.tar.gz (181.7 kB view details)

Uploaded Source

Built Distribution

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

equitrends-0.1.1-py3-none-any.whl (175.0 kB view details)

Uploaded Python 3

File details

Details for the file equitrends-0.1.1.tar.gz.

File metadata

  • Download URL: equitrends-0.1.1.tar.gz
  • Upload date:
  • Size: 181.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.6

File hashes

Hashes for equitrends-0.1.1.tar.gz
Algorithm Hash digest
SHA256 632b9090f7b6c82f1ca202affbf8b46728e245c1e7d6b98830a8f38ec99023fc
MD5 345a86cf4cc98f25da6c2e715cbe9ec5
BLAKE2b-256 d36772b288a8a914ce202ba7efeeef4bc8d69b2b91dce48d03379b0244cb6baf

See more details on using hashes here.

File details

Details for the file equitrends-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: equitrends-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 175.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.6

File hashes

Hashes for equitrends-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 971bd0c86f827aed6afd5f1ea202bf7de4a4ac8fdbab3ef9e8d0e052bdbabe31
MD5 fc92a02389e1e01658302caad1c86a1e
BLAKE2b-256 f3b353efd93e42a43b38fcf7c6f8ac32cf18448c5518f581631065381cf3bddc

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