Skip to content
Merged
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
25 changes: 15 additions & 10 deletions .claude/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,23 @@ everywhere; `to_dict()` / `to_dataframe()` on most classes. See
Target contract: `docs/v4-design.md` section 5; per-surface lifecycle:
`docs/v4-deprecations.yaml` (CI-enforced by `tests/test_v4_matrix.py`).

### Column Naming — CURRENT (3.x)
- `unit` unit id (`unit_col` on HAD; `group` on dCDH — both slated for 4.0)
- `time` calendar period, EXCEPT DifferenceInDifferences / TripleDifference /
static TwoWayFixedEffects, where `time` is the 0/1 post dummy (TWFE warns
on >2 unique values; 4.0 renames these to `post`, and TWFE's `time`
becomes the event-study calendar column)
### Column Naming — CURRENT (4.0)
- `unit` unit id everywhere (the 3.x `unit_col` on HAD and `group` on dCDH
were removed at 4.0)
- `time` calendar period everywhere; DifferenceInDifferences /
TripleDifference take the 0/1 post dummy as `post` (the 3.x `time=`
spelling on DiD was removed at 4.0, M-030; on TripleDifference it is
still a warning shim, M-031, because `time` persists as the merged
class's staggered calendar column - its 4.0 enforcement is M-085);
static TwoWayFixedEffects takes `post` and its `time` is the event-study
calendar column (the static `time=` alias on TWFE is the same kind of
surviving shim, M-082, enforced by M-083)
- `treatment` 0/1 treated-group indicator; `first_treat` cohort column
(`cohort` on WooldridgeDiD — slated for 4.0)
- `covariates` covariate list (`controls` on dCDH — slated for 4.0)
everywhere (the 3.x `cohort` on WooldridgeDiD was removed at 4.0)
- `covariates` covariate list everywhere (the 3.x `controls` on dCDH was
removed at 4.0)

### Column Naming — 4.0 TARGET (do not write against this pre-4.0)
Target vocabulary and rules: `docs/v4-design.md` section 8.
Vocabulary and rules: `docs/v4-design.md` section 8.

## Session Notes

Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,8 @@ A new parameter is only complete when it is:
forwards via `super().__init__(*args, **kwargs)` with an import-time
`__signature__` mirror - so both inherit a new
`DifferenceInDifferences` constructor parameter automatically;
`SyntheticDiD` defines its OWN signature (it forwards only
`robust`/`cluster`/`alpha` to `super().__init__`), so a new parent
`SyntheticDiD` defines its OWN signature (it forwards only `alpha` to
`super().__init__`, pinning `cluster=None`), so a new parent
parameter must be explicitly mirrored — or explicitly rejected, like its
`vcov_type`/`conley_*` guards — there, and `BaseEstimator` introspects the
concrete signature, so an unmirrored parent param simply won't appear in
Expand Down
4 changes: 2 additions & 2 deletions benchmarks/python/benchmark_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,12 @@ def main():

if args.warmup:
print("Warm-up fit (untimed)...")
DifferenceInDifferences(robust=True, cluster=args.cluster).fit(
DifferenceInDifferences(cluster=args.cluster).fit(
data, formula="outcome ~ treated * post"
)

# Use DifferenceInDifferences with formula to match R's fixest::feols
did = DifferenceInDifferences(robust=True, cluster=args.cluster)
did = DifferenceInDifferences(cluster=args.cluster)

with Timer() as timer:
results = did.fit(
Expand Down
4 changes: 2 additions & 2 deletions benchmarks/python/benchmark_multiperiod.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def main():

if args.warmup:
print("Warm-up fit (untimed)...")
MultiPeriodDiD(robust=True, cluster=args.cluster).fit(
MultiPeriodDiD(cluster=args.cluster).fit(
data,
outcome="outcome",
treatment="treated",
Expand All @@ -112,7 +112,7 @@ def main():
absorb=["unit"],
)

did = MultiPeriodDiD(robust=True, cluster=args.cluster)
did = MultiPeriodDiD(cluster=args.cluster)

with Timer() as timer:
results = did.fit(
Expand Down
4 changes: 2 additions & 2 deletions benchmarks/python/benchmark_twfe.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ def main():

if args.warmup:
print("Warm-up fit (untimed)...")
TwoWayFixedEffects(robust=True).fit(
TwoWayFixedEffects().fit(
data, outcome="outcome", treatment="treated", post="post", unit="unit"
)

twfe = TwoWayFixedEffects(robust=True) # auto-clusters at unit level
twfe = TwoWayFixedEffects() # auto-clusters at unit level

with Timer() as timer:
results = twfe.fit(
Expand Down
4 changes: 2 additions & 2 deletions benchmarks/python/benchmark_wooldridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,13 @@ def main():
outcome="lemp",
unit="countyreal",
time="year",
cohort="first_treat",
first_treat="first_treat",
)

estimation_time = estimation_timer.elapsed

# Compute event study aggregation
results.aggregate("event")
results.aggregate("event_study")
total_time = estimation_timer.elapsed

# Store data info
Expand Down
12 changes: 6 additions & 6 deletions benchmarks/speed_review/bench_brand_awareness_survey.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ def naive_fit():
# accounted for the sampling structure. The SE-inflation story
# only shows up if this step is as untreated-for-design as
# practitioners actually start.
did = DifferenceInDifferences(robust=True)
did = DifferenceInDifferences()
results["naive"] = did.fit(
data, outcome="outcome", treatment="treat_unit", post="post",
)

def tsl_fit():
did = DifferenceInDifferences(robust=True)
did = DifferenceInDifferences()
results["tsl"] = did.fit(
data, outcome="outcome", treatment="treat_unit", post="post",
survey_design=sd_tsl,
Expand All @@ -92,7 +92,7 @@ def replicate_fit():
weights="weight", replicate_weights=rw_cols,
replicate_method="JK1",
)
did = DifferenceInDifferences(robust=True)
did = DifferenceInDifferences()
results["replicate"] = did.fit(
data, outcome="outcome", treatment="treat_unit", post="post",
survey_design=sd,
Expand All @@ -101,7 +101,7 @@ def replicate_fit():
def multi_outcome_loop():
out = {}
for y in ("outcome", "consideration", "purchase_intent"):
did = DifferenceInDifferences(robust=True)
did = DifferenceInDifferences()
out[y] = did.fit(
data, outcome=y, treatment="treat_unit", post="post",
survey_design=sd_tsl,
Expand All @@ -118,10 +118,10 @@ def pretrends():
def placebo_refit():
pre = data[data["period"] < 7].copy()
pre["placebo_post"] = (pre["period"] >= 4).astype(int)
did = DifferenceInDifferences(robust=True)
did = DifferenceInDifferences()
results["placebo"] = did.fit(
pre, outcome="outcome", treatment="treat_unit",
time="placebo_post", survey_design=sd_tsl,
post="placebo_post", survey_design=sd_tsl,
)

def honest_did_grid():
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/speed_review/bench_dose_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def binarized_comparison():
data_bin = data.copy()
data_bin["treated_any"] = (data_bin["dose"] > 0).astype(int)
data_bin["post"] = (data_bin["period"] >= treated_cohort).astype(int)
did = DifferenceInDifferences(robust=True)
did = DifferenceInDifferences()
results["binarized"] = did.fit(
data_bin, outcome="outcome", treatment="treated_any", post="post",
)
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/speed_review/bench_memory_scaling.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def _run_one(cfg):
from diff_diff.twfe import TwoWayFixedEffects

ncov = cfg.get("ncov", 6)
TwoWayFixedEffects(robust=True).fit(
TwoWayFixedEffects().fit(
_twfe_panel(nu, ncov=ncov),
outcome="outcome",
treatment="treated",
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/speed_review/bench_reversible_dcdh.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def main():

results = {}
fit_kwargs = dict(
data=data, outcome="outcome", group="group", time="period",
data=data, outcome="outcome", unit="group", time="period",
treatment="treatment",
)
sd = SurveyDesign(
Expand Down
59 changes: 59 additions & 0 deletions changelog.d/20261018-v4-pr2-shim-removals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
### Removed
- **The 3.9 rename shims are gone (ledger rows flipped to `removed`).** Old keyword spellings
now raise `TypeError`: `DifferenceInDifferences.fit(time=)` -> `post=` (`M-030`),
`permutation_test(time=)` / `leave_one_out_test(time=)` -> `post=` (`M-137`, `M-138`),
`WooldridgeDiD.fit(cohort=)` -> `first_treat=` (`M-032`), `ChaisemartinDHaultfoeuille.fit(group=,
controls=)` -> `unit=`, `covariates=` (`M-033`, `M-034`), `twowayfeweights(group=)` -> `unit=`
(`M-097`), the `*_col` spellings on `HeterogeneousAdoptionDiD.fit`, `RegressionDiscontinuity.fit`,
`RDPlot.fit`, `joint_pretrends_test`, `joint_homogeneity_test`, `did_had_pretest_workflow`
(`M-035`..`M-042`, `M-088`, `M-089`, `M-098`..`M-112`) and `trim_weights(weight_col=)` ->
`weights=` (`M-113`), `StackedDiD(clean_control=)` -> `control_group=` (`M-043`),
`WooldridgeDiDResults.to_dataframe(aggregation=)` -> `level=` (`M-044`) and
`summary(aggregation)` (`M-087`; `summary(alpha=None)` is now the positional signature),
`ContinuousDiD(covariates=)` -> `fit(covariates=)` (`M-084`). The `"event"` value spelling is
rejected with `ValueError` on `WooldridgeDiDResults.aggregate(type=)`, `.to_dataframe(level=)` and
`LPDiDResults.to_dataframe(level=)` - use `"event_study"` (`M-086`, `M-150`, `M-136`).
- **`robust=` removed** from `DifferenceInDifferences` (and so `TwoWayFixedEffects`,
`MultiPeriodDiD`), `TripleDifference`, `HeterogeneousAdoptionDiD` and `LinearRegression`
(`M-045`..`M-047`, `M-115`); `vcov_type=` is the single selector. The legacy-CR1 remap that
turned `robust=False` + clustering into `hc1` went with it. Positional constructor slots after
the removed parameter shift left by one (DiD slot 1, LinearRegression/TripleDifference slot 2,
HeterogeneousAdoptionDiD slot 6); `SyntheticDiD`'s inert `lambda_reg`/`zeta` (`M-001`, `M-002`,
constructor slots 7-8) are removed too, shifting `vcov_type` and the `conley_*` parameters two
slots left. Pass these by keyword.
- **Results-field aliases removed**: `SyntheticDiDResults.placebo_effects` (`M-003`; use
`variance_effects`), `RegressionDiscontinuityResults.treatment_col` (`M-094`; `takeup`),
`StackedDiDResults.clean_control` and `StackedDiD.clean_control` (`M-095`, `M-043`;
`control_group`), `ChaisemartinDHaultfoeuilleResults.groups` (`M-114`; `units`),
`ChangesInChangesResults.estimator` (`M-143`; `method`). Their serialized dual keys are gone from
`to_dict()` (`treatment_col`, `clean_control`, `estimator`, Wooldridge's
`aggregation_weights["event"]`); 3.x pickles still load (`__setstate__` key migrations kept).
- **The eight module-level wrapper functions are removed** (`M-070`..`M-077`): `imputation_did`,
`two_stage_did`, `stacked_did`, `trop`, `synthetic_control`, `triple_difference`,
`bacon_decompose`, `chaisemartin_dhaultfoeuille`. Call the estimator classes. `diff_diff.trop`,
`diff_diff.synthetic_control`, `diff_diff.stacked_did` and `diff_diff.chaisemartin_dhaultfoeuille`
now name the submodules (as `diff_diff.bacon` always did).
- **The dieted aliases are removed** (`M-132`..`M-134`): `CDiD` -> `ContinuousDiD`, `Stacked` ->
`StackedDiD`, `Gardner` -> `TwoStageDiD`; the module `__getattr__` warning shim (`M-135`) is gone.

### Changed
- **`WooldridgeDiDResults.summary(alpha=None)`** renders the headline (simple) row only; the
group / calendar / event-study tables it used to render on request - and their `[pre]`/`[antic]`
relative-period labels - are no longer part of `summary()`. Use `aggregate(...)` /
`to_dataframe(level=...)`; `anticipation` is still reported on the results object.
`to_dataframe(level=)` now rejects an unknown level with `ValueError` instead of returning an
empty frame.
- `practitioner_next_steps` no longer recognises duck-typed results that carry only the legacy
`estimator` attribute; `ChangesInChangesResults.method` is the routing field.

### Breaking Changes
- **`BusinessReport.to_dict()` schema `2.0` -> `3.0`**: the StackedDiD `assumption` block no longer
carries the legacy `clean_control` key (read `control_group`). `DiagnosticReport`'s schema stays
`2.0`.

### Internal
- `tests/test_v4_matrix.py` removed-status probes: a function row that resolves to a submodule
counts as absent; a param row absent from the explicit signature counts as removed despite
`**kwargs`. Naming-guard allowlist extended for the kept body-local aliases, `__setstate__` keys
and shared vocabulary. The `diff_diff._deprecation` helpers survive for the remaining
M-031/M-082, PR-3 and 5.0-window shims.
Loading
Loading