Python port of EquiTrends pre-trend equivalence testing tools.
Project description
equitrends
Equivalence Tests for Pre-Trends in DiD Estimation
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
- Requirements
- Installation
- Data requirements
- Quick start
- API reference
- Test selection guide
- Methodology
- RMS test alpha restriction
- Result objects
- Visualization
- CLI entry points
- Examples
- Citation
- Authors
- License
Overview
Standard pre-trend tests in DiD designs test the null hypothesis of exact parallel trends (H₀: β = 0). This approach suffers from fundamental limitations:
- 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.
- Conditional bias amplification: Roth (2022) demonstrates that conditioning on passing traditional pre-tests can amplify DiD bias when violations exist.
- 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)
==================================================
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
-
Maximum test (
type="max"): Start here as the default, conservative choice.- Detects any single large violation
- Use
method="IU"for analytical inference ormethod="wild"for non-spherical errors - Recommended when you want to rule out any substantial pre-trend violation
-
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
-
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):
- 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| $$
- 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 $$
- 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:
- Xuanyu Cai, City University of Macau Email: xuanyuCAI@outlook.com
- Wenli Xu, City University of Macau Email: wlxu@cityu.edu.mo
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
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 equitrends-0.1.0.tar.gz.
File metadata
- Download URL: equitrends-0.1.0.tar.gz
- Upload date:
- Size: 181.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d3f92dd0572dcb692ac7d904c8a67dcc8e6e10fc0a1b2812e3dd781499c6aba6
|
|
| MD5 |
1043428a98903cc714c9730ac2e54a0f
|
|
| BLAKE2b-256 |
dc93f697d0c1f3fe3009b8f2c3fa3018a208d7d443bcc80084dfe207b042f21d
|
File details
Details for the file equitrends-0.1.0-py3-none-any.whl.
File metadata
- Download URL: equitrends-0.1.0-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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8f3250b91e7a2eceae555a3b2abd608b7530b03bda137936067762a7e09b2f60
|
|
| MD5 |
6c9daa116696a33049cf53b39838b485
|
|
| BLAKE2b-256 |
e28f0342f8b2a92294c6bc617b50a2e51f44c564dadfcaebfa2c3382a0172bb6
|