Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
- [Manipulation Testing](https://diff-diff.readthedocs.io/en/stable/api/regression_discontinuity.html) - Cattaneo, Jansson & Ma (2020) density-discontinuity test (`RDDensityTest`): rddensity 3.0 parity, robust bias-corrected inference, unrestricted/restricted models, mass-point adjustment
- [Parallel Trends Testing](https://diff-diff.readthedocs.io/en/stable/api/diagnostics.html) - simple and Wasserstein-robust parallel trends tests, equivalence testing (TOST)
- [Placebo Tests](https://diff-diff.readthedocs.io/en/stable/api/diagnostics.html) - placebo timing, group, permutation, leave-one-out
- [TWFE Weight Diagnostics](https://diff-diff.readthedocs.io/en/stable/api/twfe_weights.html) - Baker, Callaway, Cunningham, Goodman-Bacon & Sant'Anna (2025) implicit weights on ATT(g,t): `attgt_weights(cs_result, aggregation='twfe'|'overall'|'simple')` shows what a TWFE regression (vs ATT^O / ATT^simple) implicitly puts on each group-time effect, including negative weights; `decompose_twfe_weights(panel, method='fwl')` re-derives the estimate from its building blocks with the pre-trend-violation contribution and implicit covariate balance. Ported from Brantly Callaway's `twfeweights` R package (MIT)
- [Honest DiD](https://diff-diff.readthedocs.io/en/stable/api/honest_did.html) - Rambachan & Roth (2023) sensitivity analysis: robust CI under PT violations, breakdown values
- [Pre-Trends Power Analysis](https://diff-diff.readthedocs.io/en/stable/api/pretrends.html) - Roth (2022) minimum detectable violation and power curves
- [Power Analysis](https://diff-diff.readthedocs.io/en/stable/api/power.html) - analytical and simulation-based MDE, sample size, power curves for study design
Expand Down
406 changes: 406 additions & 0 deletions benchmarks/R/generate_twfeweights_golden.R

Large diffs are not rendered by default.

580 changes: 580 additions & 0 deletions benchmarks/data/twfeweights_golden.json

Large diffs are not rendered by default.

2,501 changes: 2,501 additions & 0 deletions benchmarks/data/twfeweights_mpdta_panel.csv

Large diffs are not rendered by default.

1,501 changes: 1,501 additions & 0 deletions benchmarks/data/twfeweights_sim_panel.csv

Large diffs are not rendered by default.

1,501 changes: 1,501 additions & 0 deletions benchmarks/data/twfeweights_unbalanced_panel.csv

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions changelog.d/20260831-twfe-weight-diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
### Added
- **TWFE weight diagnostics** (port of Brantly Callaway's `twfeweights` R
package, MIT): what a two-way fixed effects regression *implicitly* weights
on staggered-adoption data.
- `attgt_weights(results, aggregation="twfe"|"overall"|"simple")` reports the
weight a TWFE regression, ATT^O, or ATT^simple places on each ATT(g,t),
plus the negative-weight share. Takes a fitted `CallawaySantAnnaResults`
(reading cohort masses off its aggregation bookkeeping, so no raw panel is
needed); a `(gt_frame, data=, unit=, time=, first_treat=)` fallback
consumes `result.to_dataframe("group_time")` verbatim. Returns
`ATTGTWeightsResult`. `aggregation="twfe"` requires
`base_period="universal"` and `control_group="never_treated"`, matching
the restrictions R enforces.
- `decompose_twfe_weights(data, outcome=, unit=, time=, first_treat=,
method="fwl", covariates=, base_period="first_period"|"gmin1")` re-derives
the estimate from its ATT(g,t) building blocks and returns
`TWFEDecompositionResult` with `pretrend_bias` — the contribution of
pre-treatment cells, i.e. of parallel-trends violations rather than of
treatment. With `balance_covariates=`, `result.covariate_balance()`
reports whether the implicit weights actually balance those covariates.
- `plot_twfe_weights()` renders either view.

Names are deliberately separate from the existing `twowayfeweights` /
`TWFEWeightsResult` (de Chaisemartin & D'Haultfoeuille) surface, which
weights (unit, time) cells rather than ATT(g,t) parameters.

Validated against R `twfeweights` 0.9.0 output on three fixtures (`mpdta`
plus two simulated panels); goldens at
`benchmarks/data/twfeweights_golden.json`, regenerated by
`benchmarks/R/generate_twfeweights_golden.R`. R is never needed to run the
test suite. Methodology: Baker, Callaway, Cunningham, Goodman-Bacon &
Sant'Anna (2025); Callaway & Sant'Anna (2021) for the ATT^O / ATT^simple
weights.
17 changes: 17 additions & 0 deletions diff_diff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,14 @@
TROPResults,
trop,
)
from diff_diff.twfe_weights import (
attgt_weights,
decompose_twfe_weights,
)
from diff_diff.twfe_weights_results import (
ATTGTWeightsResult,
TWFEDecompositionResult,
)
from diff_diff.two_stage import (
TwoStageBootstrapResults,
TwoStageDiD,
Expand All @@ -321,6 +329,7 @@
plot_sensitivity,
plot_staircase,
plot_synth_weights,
plot_twfe_weights,
)
from diff_diff.wooldridge import WooldridgeDiD
from diff_diff.wooldridge_results import WooldridgeDiDResults
Expand Down Expand Up @@ -457,6 +466,13 @@ def __getattr__(name: str) -> _Any:
"TWFEWeightsResult",
"chaisemartin_dhaultfoeuille",
"twowayfeweights",
# TWFE weight diagnostics (Callaway `twfeweights` port) - distinct from
# the dCDH `twowayfeweights` surface above: these weight ATT(g,t)
# parameters, not (unit, time) cells.
"ATTGTWeightsResult",
"TWFEDecompositionResult",
"attgt_weights",
"decompose_twfe_weights",
# WooldridgeDiD (ETWFE)
"WooldridgeDiD",
"WooldridgeDiDResults",
Expand All @@ -473,6 +489,7 @@ def __getattr__(name: str) -> _Any:
"SieveLearner",
# Visualization
"plot_bacon",
"plot_twfe_weights",
"plot_event_study",
"plot_group_effects",
"plot_sensitivity",
Expand Down
65 changes: 65 additions & 0 deletions diff_diff/guides/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1503,6 +1503,44 @@ results.print_summary()
plot_bacon(results)
```

### TWFE Weight Diagnostics

What a TWFE regression implicitly weights on staggered data. Distinct from
`twowayfeweights` (dCDH), which weights (unit, time) cells: these weight
ATT(g,t) parameters. Ported from Brantly Callaway's `twfeweights` R package
(MIT); methodology Baker, Callaway, Cunningham, Goodman-Bacon & Sant'Anna
(2025).

```python
attgt_weights(
results, # CallawaySantAnnaResults, or a (g,t) frame
aggregation="twfe", # "twfe" | "overall" (ATT^O) | "simple"
data=None, unit=None, time=None, first_treat=None, # frame path only
weights=None, # unit-level sampling weights
) -> ATTGTWeightsResult

decompose_twfe_weights(
data, # balanced long panel (it re-estimates)
outcome=, unit=, time=, first_treat=,
method="fwl",
covariates=None,
base_period="first_period", # or "gmin1"
balance_covariates=None, # enables result.covariate_balance()
weights=None,
) -> TWFEDecompositionResult

plot_twfe_weights(result, kind="auto") # "weights" | "balance"
```

`aggregation="twfe"` requires a fit with `base_period="universal"` and
`control_group="never_treated"`; it raises otherwise. ATT^O and ATT^simple
weights are non-negative and sum to one, so comparing `implied_att` across
the three aggregations shows what the TWFE specification costs.

`decompose_twfe_weights` takes the raw panel rather than a fitted result
because it re-estimates. It is tied to `attgt_weights` by an identity:
`attgt_weights(cs, aggregation="twfe").implied_att == decompose_twfe_weights(panel, ...).estimate`.

### StaggeredTripleDifference

DEPRECATED in 3.9, removed in 4.0 (ledger row M-013). Use
Expand Down Expand Up @@ -1964,6 +2002,33 @@ Returned by `BaconDecomposition.fit()` (and the deprecated `bacon_decompose()` w

**Methods:** `summary()`, `print_summary()`, `to_dataframe()`

### ATTGTWeightsResult

Diagnostic result from `attgt_weights`. No inference quintet - the
decomposition is an algebraic identity.

- `weights`: DataFrame with `group`, `time`, `post`, `weight`, `att`
- `implied_att`: `sum(weight * att)` - the TWFE coefficient when
`aggregation="twfe"`
- `n_negative`, `negative_weight_share`: the staggered-TWFE pathology
- `aggregation`, `source`, `control_group`, `base_period`, `n_cells`
- `summary()`, `to_dataframe()`, `to_dict()`

### TWFEDecompositionResult

Diagnostic result from `decompose_twfe_weights`.

- `cells`: DataFrame with `group`, `time`, `post`, `att`, `weight`, `ess`,
`remainder`
- `estimate` == `decomposition` + `remainder`
- `pretrend_bias`: contribution of PRE-treatment cells, i.e. of
parallel-trends violations rather than of treatment
- `post_only`, `effective_sample_size`, `covariates`, `base_period`
- `covariate_balance(level="summary"|"cell", standardize=True,
post_only=True)`: implicit-weight covariate balance; raises when
`balance_covariates=` was not requested
- `summary()`, `to_dataframe()`, `to_dict()`

### Comparison2x2

Individual 2x2 DiD comparison (used in BaconDecompositionResults).
Expand Down
1 change: 1 addition & 0 deletions diff_diff/guides/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ The site is organized into 5 sections, each with a landing page:
- [Manipulation Testing](https://diff-diff.readthedocs.io/en/stable/api/regression_discontinuity.html): Cattaneo, Jansson & Ma (2020) density-discontinuity manipulation test (`RDDensityTest`), parity with R rddensity 3.0 - boundary-adaptive local polynomial density estimation at the cutoff, robust bias-corrected inference, unrestricted/restricted models, jackknife/plugin variances, data-driven bandwidths, mass-point adjustment
- [Parallel Trends Testing](https://diff-diff.readthedocs.io/en/stable/api/diagnostics.html): Simple and Wasserstein-robust parallel trends tests, equivalence testing (TOST)
- [Placebo Tests](https://diff-diff.readthedocs.io/en/stable/api/diagnostics.html): Placebo timing, group, permutation, and leave-one-out diagnostics
- [TWFE Weight Diagnostics](https://diff-diff.readthedocs.io/en/stable/api/twfe_weights.html): Baker et al. (2025) implicit weights on ATT(g,t) - `attgt_weights(results, aggregation='twfe'|'overall'|'simple')` takes a fitted `CallawaySantAnnaResults` (raw ATT(g,t) frame + panel as fallback) and returns the weight each estimand places on each group-time effect, with the negative-weight share; `decompose_twfe_weights(data, outcome=, unit=, time=, first_treat=, method='fwl', covariates=)` re-derives the TWFE estimate from its ATT(g,t) building blocks with `pretrend_bias`, and `result.covariate_balance()` reports implicit-weight covariate balance. Plot with `plot_twfe_weights`. R `twfeweights` 0.9.0 output parity
- [Honest DiD](https://diff-diff.readthedocs.io/en/stable/api/honest_did.html): Rambachan & Roth (2023) sensitivity analysis — robust CI under parallel trends violations, breakdown values
- [Pre-Trends Power Analysis](https://diff-diff.readthedocs.io/en/stable/api/pretrends.html): Roth (2022) Section II.A-B no-individually-significant (NIS) box-probability pretest power + minimum detectable violation; `pretest_form='nis'` (default) implements the paper's primary form, `pretest_form='wald'` retained as paper-supported alternative (Propositions 1+3+4 all apply); linear-violation MDV in Roth's γ units when relative-time labels are threaded through `fit()`; full Σ_22 routing on non-bootstrap CallawaySantAnna and SunAbraham adapters and on admitted CS-/StackedDiD-sourced `aggregate('event_study')` containers (StackedDiD persists its ES VCV in every inference mode)
- [Power Analysis](https://diff-diff.readthedocs.io/en/stable/api/power.html): Analytical and simulation-based power analysis — MDE, sample size, power curves for study design
Expand Down
Loading