Skip to content

Commit a884867

Browse files
committed
wooldridge: CI R2 P1 fix — plot_event_study always re-aggregates + cohort_share→cell regression
1 parent 379e065 commit a884867

2 files changed

Lines changed: 69 additions & 13 deletions

File tree

diff_diff/wooldridge_results.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,7 @@ def to_dataframe(self, aggregation: str = "event") -> pd.DataFrame:
694694
return pd.DataFrame(rows)
695695

696696
def plot_event_study(self, weights: str = "cell", **kwargs) -> None:
697-
"""Event study plot. Calls ``aggregate('event', weights=weights)`` if needed.
697+
"""Event study plot. Always calls ``aggregate('event', weights=weights)``.
698698
699699
Parameters
700700
----------
@@ -707,19 +707,24 @@ def plot_event_study(self, weights: str = "cell", **kwargs) -> None:
707707
contract documented in REGISTRY.
708708
**kwargs
709709
Forwarded to ``diff_diff.visualization.plot_event_study``.
710+
711+
Notes
712+
-----
713+
The wrapper unconditionally re-aggregates the event study under
714+
the requested ``weights`` scheme. This avoids the stale-cache
715+
hazard where a prior ``plot_event_study(weights="cohort_share")``
716+
call would leave the cached ``event_study_effects`` restricted
717+
to ``k >= 0`` (per the Eq. 7.6 scope), and a subsequent
718+
``plot_event_study()`` (default ``weights="cell"``) call would
719+
silently reuse the cohort-share-keyed cache instead of restoring
720+
the full event range including pre-period placebo leads.
710721
"""
711-
# Recompute under the active weighting scheme if the cached
712-
# event_study_effects was built under a different scheme — or
713-
# has not been built yet. Aggregating under "cell" then under
714-
# "cohort_share" (or vice versa) replaces ``event_study_effects``
715-
# in place per the existing aggregate() contract.
716-
if self.event_study_effects is None:
717-
self.aggregate("event", weights=weights)
718-
elif weights == "cohort_share":
719-
# Force re-aggregation so the cohort-share contract is
720-
# honored from a wrapper call that may have been preceded
721-
# by an aggregate("event", weights="cell") at fit time.
722-
self.aggregate("event", weights=weights)
722+
# Always re-aggregate under the requested weighting scheme. The
723+
# aggregate() method replaces ``event_study_effects`` in place
724+
# per the existing contract, so this is cheap and avoids
725+
# cohort_share→cell (or any cross-scheme) stale-cache bugs.
726+
self.aggregate("event", weights=weights)
727+
723728
from diff_diff.visualization import plot_event_study # type: ignore
724729

725730
effects = {k: v["att"] for k, v in (self.event_study_effects or {}).items()}

tests/test_methodology_wooldridge.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1469,6 +1469,57 @@ def test_plot_event_study_propagates_weights_kwarg(self) -> None:
14691469
"leads."
14701470
)
14711471

1472+
def test_plot_event_study_cohort_share_to_cell_round_trip_restores_placebo_leads(
1473+
self,
1474+
) -> None:
1475+
"""CI R2 P1 fix: ``plot_event_study()`` reverse direction (cohort_share → cell).
1476+
1477+
Codex caught a stale-cache hazard: my earlier fix re-aggregated
1478+
on ``weights="cohort_share"`` but skipped re-aggregation on the
1479+
default ``weights="cell"`` path when the cached ``event_study_effects``
1480+
was already populated. A user calling ``plot_event_study(weights=
1481+
"cohort_share")`` (which restricts to k>=0) and then
1482+
``plot_event_study()`` (default cell weights) would silently
1483+
plot the stale cohort-share-keyed data. The fix unconditionally
1484+
re-aggregates on every call.
1485+
1486+
This test exercises the reverse direction by:
1487+
1. First call: ``plot_event_study(weights="cohort_share")`` —
1488+
caches cohort-share keys (k >= 0 only)
1489+
2. Second call: ``plot_event_study()`` (default cell weights) —
1490+
must restore the full event range including k < 0 placebo leads
1491+
"""
1492+
from unittest.mock import patch
1493+
1494+
rng = np.random.default_rng(_BASE_SEED_SECTION8 + 16)
1495+
panel = _make_three_cohort_four_period_panel(rng, n_per_cohort=80, sigma=0.05)
1496+
with warnings.catch_warnings():
1497+
warnings.filterwarnings("ignore", category=UserWarning)
1498+
res = WooldridgeDiD(method="ols", control_group="never_treated").fit(
1499+
panel, outcome="y", unit="unit", time="time", cohort="cohort"
1500+
)
1501+
# Step 1: plot under cohort_share — caches k>=0 keys
1502+
with warnings.catch_warnings():
1503+
warnings.filterwarnings("ignore", category=UserWarning)
1504+
with patch("diff_diff.visualization.plot_event_study"):
1505+
res.plot_event_study(weights="cohort_share")
1506+
assert res.event_study_effects is not None
1507+
cohort_share_keys = sorted(res.event_study_effects.keys())
1508+
assert all(k >= 0 for k in cohort_share_keys), (
1509+
f"DGP precondition: cohort_share path must restrict to k>=0; "
1510+
f"got {cohort_share_keys}"
1511+
)
1512+
# Step 2: plot under default weights="cell" — must restore k<0 leads
1513+
with patch("diff_diff.visualization.plot_event_study"):
1514+
res.plot_event_study() # default weights="cell"
1515+
assert res.event_study_effects is not None
1516+
cell_keys = sorted(res.event_study_effects.keys())
1517+
assert any(k < 0 for k in cell_keys), (
1518+
f"plot_event_study() (cell weights) after a cohort_share "
1519+
f"call must restore k<0 placebo leads; got {cell_keys}. "
1520+
f"Stale cohort_share cache was reused — CI R2 P1 fix regressed."
1521+
)
1522+
14721523
def test_cohort_trends_true_plus_bootstrap_preserves_bootstrap_se(self) -> None:
14731524
"""R5 P1 fix: ``cohort_trends=True`` + ``n_bootstrap > 0`` runs cleanly.
14741525

0 commit comments

Comments
 (0)