Core Module (core)

Main entry point for difference-in-differences estimation with panel data.

This module provides the main user-facing function lwdid() for estimating average treatment effects on the treated (ATT) using the Lee and Wooldridge transformation-based approach. The function supports both common timing designs (where all treated units receive treatment simultaneously) and staggered adoption designs (where units are treated at different times).

The method transforms panel data via unit-specific time-series operations (demeaning or detrending), then applies cross-sectional regression to the transformed outcomes. Under classical linear model assumptions, exact t-based inference is available for small samples. For large samples, asymptotic inference with robust standard errors or doubly robust estimators (IPW, IPWRA, PSM) is supported.

Main Function

lwdid.lwdid(data, y, d=None, ivar=None, tvar=None, post=None, rolling='demean', *, gvar=None, control_group='not_yet_treated', estimator='ra', aggregate='cohort', balanced_panel='warn', ps_controls=None, trim_threshold=0.01, return_diagnostics=False, n_neighbors=1, caliper=None, with_replacement=True, match_order='data', vce=None, controls=None, cluster_var=None, alpha=0.05, ri=False, rireps=1000, seed=None, ri_method='bootstrap', graph=False, gid=None, graph_options=None, season_var=None, Q=4, auto_detect_frequency=False, include_pretreatment=False, pretreatment_test=True, pretreatment_alpha=0.05, exclude_pre_periods=0, **kwargs)[source]

Difference-in-differences estimator with unit-specific transformations.

Implements the rolling transformation approach for DiD estimation, supporting three methodological scenarios:

  1. Small-sample common timing: Exact t-based inference under classical linear model assumptions.

  2. Large-sample common timing: Asymptotic inference with heteroskedasticity-robust standard errors.

  3. Staggered adoption: Cohort-time specific effect estimation with flexible control group strategies.

The transformation removes unit-specific pre-treatment patterns, converting panel DiD into a cross-sectional treatment effects problem.

Parameters:
  • data (pd.DataFrame) – Panel data in long format with one row per unit-time observation. Each (unit, time) combination must be unique. Requires at least 3 units.

  • y (str) – Column name of the outcome variable.

  • d (str, optional) – Column name of the unit-level treatment indicator (required for common timing mode). Must be time-invariant: non-zero for treated units, zero for control units. Ignored in staggered mode.

  • ivar (str) – Column name of the unit identifier.

  • tvar (str or list of str) – Time variable specification. For annual data, a single column name. For quarterly data, a list of two column names [year_var, quarter_var] where quarter_var contains values in {1, 2, 3, 4}.

  • post (str, optional) – Column name of the post-treatment indicator (required for common timing mode). Internally binarized: non-zero values indicate post-treatment periods. Must be monotone non-decreasing in time (no treatment reversals).

  • rolling ({'demean', 'detrend', 'demeanq', 'detrendq'}, default='demean') –

    Transformation method (case-insensitive):

    • ’demean’: Remove unit-specific pre-treatment mean.

    • ’detrend’: Remove unit-specific linear time trend.

    • ’demeanq’: Demeaning with seasonal fixed effects. Requires season_var and Q parameters. Supports quarterly (Q=4), monthly (Q=12), or weekly (Q=52) data.

    • ’detrendq’: Detrending with seasonal fixed effects. Requires season_var and Q parameters. Supports quarterly (Q=4), monthly (Q=12), or weekly (Q=52) data.

    All four transformation methods are supported for both common timing and staggered adoption designs.

  • gvar (str, optional) – Column name indicating first treatment period for staggered adoption. If specified, activates staggered mode and ignores d and post. Valid values: positive integers (treatment cohort), 0/inf/NaN (never-treated).

  • control_group ({'not_yet_treated', 'never_treated', 'all_others'}, default='not_yet_treated') –

    Control group composition for staggered adoption:

    • ’not_yet_treated’: Never-treated plus not-yet-treated units.

    • ’never_treated’: Only never-treated units.

    • ’all_others’: All units not in the treated cohort (including already-treated units). This option is mainly intended for replication/diagnostics and may introduce forbidden comparisons under no-anticipation.

    Auto-switched to ‘never_treated’ for cohort/overall aggregation.

  • estimator ({'ra', 'ipw', 'ipwra', 'psm'}, default='ra') –

    Estimation method (case-insensitive):

    • ’ra’: Regression adjustment via OLS on transformed outcomes.

    • ’ipw’: Inverse probability weighting. Requires controls.

    • ’ipwra’: Doubly robust combining IPW with RA. Requires controls.

    • ’psm’: Propensity score matching. Requires controls.

  • aggregate ({'none', 'cohort', 'overall'}, default='cohort') –

    Aggregation level for staggered adoption:

    • ’none’: Return cohort-time specific effects only.

    • ’cohort’: Aggregate to cohort-specific effects.

    • ’overall’: Aggregate to a single weighted overall effect.

  • balanced_panel ({'warn', 'error', 'ignore'}, default='warn') –

    How to handle unbalanced panels (units with different observation counts):

    • ’warn’: Issue a warning with selection mechanism diagnostics (default).

    • ’error’: Raise UnbalancedPanelError if panel is unbalanced.

    • ’ignore’: Silently proceed without warnings.

    Selection may depend on time-invariant heterogeneity but not on shocks to the untreated potential outcome. Use diagnose_selection_mechanism() for detailed diagnostics.

  • ps_controls (list of str, optional) – Control variables for propensity score model. If None, uses controls.

  • trim_threshold (float, default=0.01) – Propensity score trimming threshold. Observations with propensity scores outside [trim_threshold, 1 - trim_threshold] are excluded.

  • return_diagnostics (bool, default=False) – Whether to include propensity score diagnostics in results.

  • n_neighbors (int, default=1) – Number of nearest neighbors for PSM matching.

  • caliper (float, optional) – Maximum propensity score distance for PSM, in units of PS standard deviation. Treated units without valid matches are dropped.

  • with_replacement (bool, default=True) – Whether PSM allows control units to be matched multiple times.

  • match_order ({'data', 'random', 'largest', 'smallest'}, default='data') –

    Order for processing treated units in without-replacement PSM:

    • ’data’: Original data order.

    • ’random’: Randomized order (use seed for reproducibility).

    • ’largest’: Prioritize units with extreme propensity scores.

    • ’smallest’: Prioritize units with propensity scores near 0.5.

  • vce ({None, 'robust', 'hc0', 'hc1', 'hc2', 'hc3', 'hc4', 'cluster'}, optional) –

    Variance estimator (case-insensitive):

    • None: Homoskedastic OLS standard errors.

    • ’hc0’: White heteroskedasticity-robust.

    • ’robust’/’hc1’: HC1 with degrees-of-freedom correction N/(N-K).

    • ’hc2’: Leverage-adjusted using (1 - h_ii)^{-1}.

    • ’hc3’: Small-sample adjusted using (1 - h_ii)^{-2}.

    • ’hc4’: Adaptive leverage correction.

    • ’cluster’: Cluster-robust (requires cluster_var).

  • controls (list of str, optional) – Time-invariant control variables for outcome regression.

  • cluster_var (str, optional) – Column name for clustering (required when vce=’cluster’).

  • alpha (float, default=0.05) – Significance level for confidence intervals.

  • ri (bool, default=False) – Whether to perform randomization inference for the null H0: ATT=0.

  • rireps (int, default=1000) – Number of randomization inference replications.

  • seed (int, optional) – Random seed for reproducibility in randomization inference.

  • ri_method ({'bootstrap', 'permutation'}, default='bootstrap') –

    Resampling method for randomization inference:

    • ’bootstrap’: With-replacement resampling.

    • ’permutation’: Fisher’s exact permutation test.

  • graph (bool, default=False) – Whether to generate a plot of transformed outcomes over time.

  • gid (str or int, optional) – Specific unit identifier to highlight in the plot.

  • graph_options (dict, optional) – Additional plotting options passed to the visualization function.

  • season_var (str, optional) – Column name of seasonal indicator variable for seasonal transformations (demeanq, detrendq). Values should be integers from 1 to Q representing seasonal periods (e.g., quarters 1-4, months 1-12, or weeks 1-52). This parameter is preferred over the legacy quarter parameter in tvar for non-quarterly seasonal data.

  • Q (int, default=4) –

    Number of seasonal periods per cycle. Used with seasonal transformations (demeanq, detrendq). Common values:

    • 4: Quarterly data (default)

    • 12: Monthly data

    • 52: Weekly data

    Must match the range of values in season_var (1 to Q).

  • auto_detect_frequency (bool, default=False) –

    Whether to automatically detect data frequency and set Q accordingly. When True, the function analyzes the time variable to infer whether data is quarterly (Q=4), monthly (Q=12), or weekly (Q=52).

    • If detection succeeds with high confidence, Q is set automatically.

    • If detection fails or has low confidence, a warning is issued and the explicit Q value is used.

    • An explicit Q value always overrides auto-detection when both are specified (Q != 4 and auto_detect_frequency=True).

    This parameter is useful when working with datasets of unknown frequency or when building generic analysis pipelines.

  • include_pretreatment (bool, default=False) –

    Whether to compute pre-treatment transformed outcomes and ATT estimates for parallel trends assessment. Only applicable in staggered mode.

    When True:

    • Applies rolling transformations to pre-treatment periods using future pre-treatment periods {t+1, …, g-1} as reference.

    • Estimates pre-treatment ATT for each (cohort, period) pair.

    • Stores results in att_pre_treatment attribute of LWDIDResults.

    • Enables extended event study visualization with pre-treatment effects.

    Under the parallel trends assumption, pre-treatment ATT estimates should be statistically indistinguishable from zero.

  • pretreatment_test (bool, default=True) –

    Whether to perform parallel trends statistical test when include_pretreatment=True. The test includes:

    • Individual t-tests for each pre-treatment period ATT.

    • Joint F-test for H0: all pre-treatment ATT = 0.

    Results are stored in parallel_trends_test attribute.

  • pretreatment_alpha (float, default=0.05) – Significance level for parallel trends test. Used for determining reject_null in the test results.

  • exclude_pre_periods (int, default=0) –

    Number of pre-treatment periods to exclude immediately before treatment. Used to address potential anticipation effects when the no-anticipation assumption may be violated.

    When exclude_pre_periods > 0:

    • The specified number of periods immediately before treatment are excluded from the pre-treatment sample used for transformation.

    • For common timing: excludes the last k pre-treatment periods.

    • For staggered adoption: excludes k periods before each cohort’s treatment date.

    This implements a robustness check for testing sensitivity to anticipation effects.

    Example: If treatment occurs at t=6 and exclude_pre_periods=2, periods t=4 and t=5 are excluded from the pre-treatment sample.

Returns:

Results object with the following key attributes:

  • att : Average treatment effect on the treated.

  • se_att : Standard error of ATT.

  • t_stat : t-statistic for H0: ATT=0.

  • pvalue : Two-sided p-value.

  • ci_lower, ci_upper : Confidence interval bounds.

  • df_inference : Degrees of freedom for inference.

  • nobs : Number of observations in estimation sample.

  • n_treated, n_control : Unit counts by treatment status.

  • att_by_period : Period-specific ATT estimates (DataFrame).

  • ri_pvalue : Randomization inference p-value (if ri=True).

  • att_pre_treatment : Pre-treatment ATT estimates (if include_pretreatment=True).

  • parallel_trends_test : Parallel trends test results (if include_pretreatment=True).

  • include_pretreatment : Whether pre-treatment dynamics were computed.

Key methods: summary(), plot(), to_excel(), to_csv(), to_latex(), get_diagnostics(), plot_event_study().

Return type:

LWDIDResults

Raises:

Notes

Mode selection:

  • Common timing (gvar=None): Requires d, post, rolling.

  • Staggered adoption (gvar specified): Requires gvar, rolling.

Confidence intervals use t-distribution critical values with degrees of freedom N-k (homoskedastic) or G-1 (cluster-robust).

See also

LWDIDResults

Detailed documentation of the results container.

Examples

Basic Usage

Simplest DiD estimation with default settings:

from lwdid import lwdid
import pandas as pd

# Load data
data = pd.read_csv('smoking.csv')

# Run estimation
results = lwdid(
    data,
    y='lcigsale',      # Outcome variable
    d='d',             # Treatment indicator
    ivar='state',      # Unit ID
    tvar='year',       # Time variable
    post='post',       # Post-treatment indicator
    rolling='demean'   # Transformation method
)

# View results
print(results.summary())
print(f"ATT: {results.att:.4f}")
print(f"SE: {results.se_att:.4f}")
print(f"p-value: {results.pvalue:.4f}")

With Robust Standard Errors

Using HC3 heteroskedasticity-robust standard errors:

results = lwdid(
    data, 'lcigsale', 'd', 'state', 'year', 'post', 'detrend',
    vce='hc3'  # HC3 robust standard errors
)

With Randomization Inference

Adding randomization inference for non-parametric testing:

results = lwdid(
    data, 'lcigsale', 'd', 'state', 'year', 'post', 'demean',
    ri=True,           # Enable randomization inference
    rireps=1000,       # Number of permutations
    ri_method='permutation',  # Use permutation (recommended)
    seed=42            # For reproducibility
)

print(f"t-based p-value: {results.pvalue:.4f}")
print(f"RI p-value: {results.ri_pvalue:.4f}")

With Control Variables

Including time-invariant control variables:

# Prepare time-invariant controls
# Use pre-treatment mean for time-varying variables
data_prep = data.copy()
for var in ['retprice', 'beer']:
    pre_mean = data[data['post']==0].groupby('state')[var].mean()
    data_prep[f'{var}_pre'] = data_prep['state'].map(pre_mean)

results = lwdid(
    data_prep, 'lcigsale', 'd', 'state', 'year', 'post', 'detrend',
    controls=['retprice_pre', 'beer_pre'],  # Time-invariant controls
    vce='hc3'
)

Warning

Control variables must be time-invariant (constant within each unit). Time-varying variables must first be aggregated to create unit-level constants. Common approaches:

  • Pre-treatment mean: data[data['post']==0].groupby('unit')[var].mean()

  • First period value: data.groupby('unit')[var].first()

  • Overall mean: data.groupby('unit')[var].mean()

Cluster-Robust Standard Errors

When errors are correlated within clusters:

results = lwdid(
    data, 'outcome', 'd', 'unit', 'year', 'post', 'demean',
    vce='cluster',
    cluster_var='state'  # Cluster by state
)

Quarterly Data

Handling quarterly data with seasonal patterns:

# Data has columns: unit, year, quarter, outcome, d, post
results = lwdid(
    data_q,
    y='sales',
    d='d',
    ivar='store',
    tvar=['year', 'quarter'],  # Composite time variable
    post='post',
    rolling='detrendq'  # Quarterly detrending with seasonality
)

Complete Example with All Options

results = lwdid(
    data,
    y='outcome',
    d='d',
    ivar='unit',
    tvar='year',
    post='post_treatment',
    rolling='detrend',
    controls=['baseline_x1', 'baseline_x2'],
    vce='hc3',
    ri=True,
    rireps=2000,
    ri_method='permutation',
    seed=12345
)

# Access results
print(f"ATT: {results.att:.3f} ({results.ci_lower:.3f}, {results.ci_upper:.3f})")
print(f"t-stat: {results.t_stat:.3f}, p-value: {results.pvalue:.4f}")
print(f"RI p-value: {results.ri_pvalue:.4f}")
print(f"N = {results.nobs}, df = {results.df_inference}")

# Export results
results.to_excel('results.xlsx')
results.plot()

Doubly Robust Estimation (Large-Sample Common Timing)

For large cross-sectional samples in common timing designs, multiple estimators are available beyond regression adjustment (RA). Lee and Wooldridge (2025) shows that the rolling transformation approach enables application of doubly robust estimators.

# IPWRA (doubly robust) estimator
results = lwdid(
    data, 'outcome', 'd', 'unit', 'year', 'post', 'demean',
    estimator='ipwra',           # Doubly robust estimator
    controls=['x1', 'x2'],       # Controls for outcome and propensity score
    vce='hc3'
)

# IPW (inverse probability weighting) estimator
results_ipw = lwdid(
    data, 'outcome', 'd', 'unit', 'year', 'post', 'demean',
    estimator='ipw',
    controls=['x1', 'x2'],
    vce='hc3'
)

# PSM (propensity score matching) estimator
results_psm = lwdid(
    data, 'outcome', 'd', 'unit', 'year', 'post', 'demean',
    estimator='psm',
    controls=['x1', 'x2'],
    n_neighbors=3,               # 3 nearest neighbors
    caliper=0.1                  # Caliper in PS standard deviations
)

Note

Estimator Selection Guidelines:

  • RA (default): Efficient under correct outcome model specification.

  • IPWRA: Doubly robust; consistent if either outcome or propensity score model is correctly specified. Recommended when functional form is uncertain.

  • IPW/PSM: Use when propensity score methods are preferred for substantive reasons.

For detailed guidance on estimator selection, see Estimation Module (estimation) and User Guide.

Staggered DiD

For staggered treatment adoption (different units treated at different times):

# Castle Law example - states adopted Castle Doctrine at different years
data = pd.read_csv('castle.csv')
data['gvar'] = data['effyear'].fillna(0).astype(int)

# Overall effect across all cohorts
results = lwdid(
    data=data,
    y='lhomicide',         # Log homicide rate
    ivar='sid',            # State ID (must be integer)
    tvar='year',           # Year
    gvar='gvar',           # First treatment year (0 = never treated)
    rolling='demean',      # Transformation method
    control_group='never_treated',
    aggregate='overall',   # Get weighted average effect
    vce='hc3'
)

print(f"Overall ATT: {results.att_overall:.4f}")
print(f"95% CI: [{results.ci_overall_lower:.4f}, {results.ci_overall_upper:.4f}]")
# Cohort-specific effects
results = lwdid(
    data=data,
    y='lhomicide',
    ivar='sid',
    tvar='year',
    gvar='gvar',
    aggregate='cohort',    # Average within each cohort
)
print(results.att_by_cohort)  # DataFrame with cohort-level estimates
# All (cohort, time) specific effects for event study
results = lwdid(
    data=data,
    y='lhomicide',
    ivar='sid',
    tvar='year',
    gvar='gvar',
    aggregate='none',      # No aggregation
)

# Plot event study
results.plot_event_study(title='Castle Doctrine Effect')

Note

When using aggregate='cohort' or aggregate='overall', the control group is automatically switched to never_treated if not_yet_treated was specified. This is required by the theoretical framework (see Lee and Wooldridge, 2025).

Handling Unbalanced Panels

For panels with missing observations:

from lwdid import lwdid, diagnose_selection_mechanism

# Run selection diagnostics first
diagnostics = diagnose_selection_mechanism(
    data=data, ivar='unit', tvar='year', gvar='gvar'
)
print(f"Selection risk: {diagnostics.risk_level}")

# Estimation with unbalanced panel handling
results = lwdid(
    data=data,
    y='outcome',
    ivar='unit',
    tvar='year',
    gvar='gvar',
    rolling='detrend',     # Detrending is more robust to selection
    balanced_panel='warn'  # Issue warning with diagnostics (default)
)

Excluding Pre-treatment Periods for Anticipation

When no-anticipation assumption may be violated:

# Exclude 2 periods before treatment to test for anticipation effects
results = lwdid(
    data=data,
    y='outcome',
    d='treated',
    ivar='unit',
    tvar='year',
    post='post',
    rolling='demean',
    exclude_pre_periods=2  # Exclude t-1 and t-2 from transformation
)

See Also

  • lwdid.LWDIDResults - Results object returned by lwdid()

  • lwdid.staggered - Low-level staggered estimation functions

  • User Guide - Comprehensive usage guide

  • Quick Start - Quick start tutorial