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
48 changes: 44 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Deprecated
- **The 8 estimator convenience wrappers + the CDiD/Gardner/Stacked alias
diet; new `SCM` alias** (v4 program 2(d) PR-A; ledger rows
[M-070]..[M-077] shimmed, [M-062] + [M-135] done, notes amendments to
[M-021]/[M-022] and [M-132]..[M-134]). The module-level wrapper
functions `imputation_did`, `two_stage_did`, `stacked_did`, `trop`,
`synthetic_control`, `triple_difference`, `bacon_decompose`, and
`chaisemartin_dhaultfoeuille` are deprecated (3.9, removed in 4.0):
every call now emits a `FutureWarning` naming the class successor -
classes are the single canonical construction surface (only 8 of 24
estimators ever had wrappers, so the surface retires rather than
completes; `twowayfeweights` stays - a diagnostic function, not a
class duplicate). The wrappers remain pure construct+fit relays -
results are bit-identical to the class path (pinned across the full
inference quintet in `tests/test_v4_wrapper_shims.py`), and the
`aggregate`/`balance_e` sentinel forwarding is unchanged (a plain
wrapper call fires exactly ONE warning - the wrapper deprecation;
four `test_plain_wrapper_call_does_not_warn`-style pins flipped BY
DESIGN to expect it). The `DiagnosticReport` Bacon runner migrated to
`BaconDecomposition` internally (numbers identical). ALIAS DIET: the
`CDiD`/`Gardner`/`Stacked` aliases are deprecated (3.9, removed 4.0;
rows M-132..M-134) - they leave module globals (gone from `dir()`
and static autocomplete) but stay importable and in `__all__`,
served by a new PEP 562 module `__getattr__` ([M-135]) that warns and
returns `ContinuousDiD`/`TwoStageDiD`/`StackedDiD`; a star-import
records the three alias warnings twice each (the package import
protocol resolves each missing `__all__` name twice - the tests
assert the message set, not the count). NEW: `SCM =
SyntheticControl` ([M-062], introduce-only). Emitted-guidance
register: `practitioner_next_steps()` recommendation strings now use
full class names (e.g. "CallawaySantAnna, SunAbraham, or
TwoStageDiD" instead of "CS, SA, or Gardner") and its
`output["estimator"]` display label for TwoStageDiD results is now
"TwoStageDiD" (previously "TwoStageDiD (Gardner)"); the
BusinessReport/DiagnosticReport Bacon caveats name classes likewise.

### Added
- **HeterogeneousAdoptionDiD post-fit `aggregate()` + panel-shape mode
inference, and the per-level bootstrap-gate convergence** (v4 program 2(b)
Expand Down Expand Up @@ -122,7 +158,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`fit()` never warns; supplying ANY value (None included) warns once;
the deprecated path still returns exactly the numbers it always did.
The `imputation_did`/`two_stage_did` wrappers forward the sentinel,
so plain wrapper calls never warn. Unknown `aggregate` strings keep
so plain wrapper calls never fire the aggregate warning (since 3.9
every wrapper call fires its own M-070/M-071 wrapper-deprecation
warning instead). Unknown `aggregate` strings keep
silently acting like None on the deprecated path; the post-fit
successor fails closed on unknown types - a behavior improvement.
- MEMORY CONTRACT: ImputationDiD's kit references the SAME per-fit
Expand Down Expand Up @@ -245,7 +283,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
inert (CS-style warn-and-still-work - results are identical to a
plain fit's), and the `group`/`all`/unknown-value `ValueError`s
persist after the warning. The `stacked_did()` convenience wrapper
forwards the shim. `StackedDiDResults.aggregate()` ships as a pure
forwards the shim (a plain wrapper call fires only the M-072
wrapper-deprecation warning since 3.9). `StackedDiDResults.aggregate()` ships as a pure
VIEW: `'event_study'` returns the unified container (with
`base_period='universal'` and singleton `reference_event_times`
provenance - one omitted reference per fit by construction);
Expand Down Expand Up @@ -519,7 +558,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`guides/*.txt`, and `docs/methodology/`. The guard's first run completed 26
rows' `code_refs` with newly confirmed readers (e.g. `diagnostics.py`'s
`fit(time=...)` call sites on [M-030], `diagnostic_report.py`'s bare
`bacon_decompose(...)` call on [M-076], the packaged-guide readers of
`bacon_decompose(...)` call on [M-076] (that internal call migrated to
the class form in the 2(d) PR-A), the packaged-guide readers of
`cohort`/`aggregation`/the wrapper functions), rule 11 gained the
token-family `code_refs` union clause, and the `plot_group_effects` cohort
selector joined the section-8 domain-vocabulary carve-outs. Wired into the
Expand Down Expand Up @@ -741,7 +781,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
ImputationDiD↔TwoStageDiD merge, and a moderate **alias diet** — `CDiD`,
`Stacked` and `Gardner` are deprecated at 3.9 and removed at 4.0
(ledger rows M-132..M-134), with the 3.9 `FutureWarning` carried by a
module-level `__getattr__` (M-135, ships in phase 2d) because the
module-level `__getattr__` (M-135, shipped by the 2(d) PR-A) because the
target classes survive; `EDiD` was initially slated but retained (review
evidence: it is the Chen-Sant'Anna-Xie paper's own estimator label); the
never-shipped `Spillover` alias introduction is
Expand Down
4 changes: 2 additions & 2 deletions benchmarks/speed_review/bench_dose_response.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Scenario 6: Pricing dose-response with ContinuousDiD cubic spline.

Chains: CDiD fit (overall ATT + ACRT + dose-response curves are always
Chains: ContinuousDiD fit (overall ATT + ACRT + dose-response curves are always
computed; bootstrap 199) -> dataframe extraction -> post-fit event-study
pre-trend (results.aggregate('event_study'), row M-025) -> binarized-DiD
comparison -> spline sensitivity (degree=1, num_knots=2).
Expand Down Expand Up @@ -76,7 +76,7 @@ def cdid_event_study():

def binarized_comparison():
# Derive post from the actual first_treat cohort in the data so
# this phase is aligned with the CDiD fits above. A hardcoded
# this phase is aligned with the ContinuousDiD fits above. A hardcoded
# period cutoff would silently desync if the DGP cohort moves.
treated_cohort = int(
sorted(v for v in data["first_treat"].unique() if v > 0)[0]
Expand Down
39 changes: 35 additions & 4 deletions diff_diff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
the difference-in-differences methodology.
"""

import warnings as _warnings
from typing import Any as _Any

# Import backend detection from dedicated module (avoids circular imports)
from diff_diff._backend import (
HAS_RUST_BACKEND,
Expand Down Expand Up @@ -318,20 +321,45 @@
EventStudy = MultiPeriodDiD
SDiD = SyntheticDiD
CS = CallawaySantAnna
CDiD = ContinuousDiD
SA = SunAbraham
BJS = ImputationDiD
Gardner = TwoStageDiD
DDD = TripleDifference
SDDD = StaggeredTripleDifference
Stacked = StackedDiD
Bacon = BaconDecomposition
EDiD = EfficientDiD
ETWFE = WooldridgeDiD
DCDH = ChaisemartinDHaultfoeuille
HAD = HeterogeneousAdoptionDiD
CiC = ChangesInChanges
RDD = RegressionDiscontinuity
SCM = SyntheticControl

# Alias diet (rows M-132..M-134, mechanism M-135): CDiD / Gardner /
# Stacked are deprecated in 3.9 and removed in 4.0. They deliberately
# do NOT live in module globals — dir()/vars() no longer list them —
# but stay importable (and in __all__) through 3.9, served by the PEP
# 562 module __getattr__ below, which emits the FutureWarning naming
# the surviving class.
_DEPRECATED_ALIASES = {
"CDiD": "ContinuousDiD",
"Gardner": "TwoStageDiD",
"Stacked": "StackedDiD",
}


def __getattr__(name: str) -> _Any:
"""PEP 562 warning shim for the dieted aliases (row M-135)."""
target = _DEPRECATED_ALIASES.get(name)
if target is not None:
_warnings.warn(
f"diff_diff.{name} is deprecated and will be removed in 4.0; "
f"use diff_diff.{target}.",
FutureWarning,
stacklevel=2,
)
return globals()[target]
raise AttributeError(f"module 'diff_diff' has no attribute {name!r}")


__version__ = "3.8.0"
__all__ = [
Expand All @@ -353,7 +381,9 @@
"TROP",
"SyntheticControl",
"StackedDiD",
# Estimator aliases (short names)
# Estimator aliases (short names). CDiD / Gardner / Stacked are
# deprecated (M-132..M-134): still importable through 3.9 via the
# module __getattr__ (M-135), gone from module globals/dir().
"DiD",
"TWFE",
"EventStudy",
Expand All @@ -367,6 +397,7 @@
"Gardner",
"DDD",
"SDDD",
"SCM",
"Stacked",
"Bacon",
# Bacon Decomposition
Expand Down
11 changes: 11 additions & 0 deletions diff_diff/bacon.py
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,11 @@ def bacon_decompose(
"""
Convenience function for Goodman-Bacon decomposition.

.. deprecated:: 3.9
``bacon_decompose()`` is deprecated and will be removed in 4.0
(row M-076). Construct the diagnostic instead:
``BaconDecomposition(...).fit(data, ...)``.

Decomposes a TWFE estimate into weighted 2x2 DiD comparisons,
showing which comparisons drive the estimate and whether
problematic "forbidden comparisons" are involved.
Expand Down Expand Up @@ -1392,5 +1397,11 @@ def bacon_decompose(
plot_bacon : Visualize the decomposition
CallawaySantAnna : Robust estimator that avoids forbidden comparisons
"""
warnings.warn(
"bacon_decompose() is deprecated and will be removed in 4.0; "
"construct the diagnostic instead: BaconDecomposition(...).fit(data, ...).",
FutureWarning,
stacklevel=2,
)
decomp = BaconDecomposition(weights=weights)
return decomp.fit(data, outcome, unit, time, first_treat, survey_design=survey_design)
7 changes: 4 additions & 3 deletions diff_diff/business_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -1652,8 +1652,8 @@ def _build_caveats(
# Round-45 P1 CI review on PR #318: Goodman-Bacon is a
# decomposition of TWFE weights (see ``bacon.py`` header and
# Goodman-Bacon 2021). On fits already produced by a
# heterogeneity-robust estimator (CS / SA / BJS / Gardner /
# Wooldridge / EfficientDiD / Stacked / dCDH / TripleDifference /
# heterogeneity-robust estimator (CS / SA / BJS / TwoStageDiD /
# Wooldridge / EfficientDiD / StackedDiD / dCDH / TripleDifference /
# StaggeredTripleDiff / SDiD / TROP), a high forbidden-weight share
# says "TWFE would have been materially biased on this rollout",
# not "the displayed estimator needs to be replaced" — the
Expand All @@ -1680,7 +1680,8 @@ def _build_caveats(
"later-vs-earlier comparisons. TWFE may be "
"materially biased under heterogeneous effects. "
"Re-estimate with a heterogeneity-robust "
"estimator (CS / SA / BJS / Gardner)."
"estimator (CallawaySantAnna, SunAbraham, "
"ImputationDiD, or TwoStageDiD)."
)
else:
bacon_message = (
Expand Down
12 changes: 12 additions & 0 deletions diff_diff/chaisemartin_dhaultfoeuille.py
Original file line number Diff line number Diff line change
Expand Up @@ -8737,6 +8737,11 @@ def chaisemartin_dhaultfoeuille(
One-shot convenience wrapper around
:class:`ChaisemartinDHaultfoeuille`.

.. deprecated:: 3.9
``chaisemartin_dhaultfoeuille()`` is deprecated and will be
removed in 4.0 (row M-077). Construct the estimator instead:
``ChaisemartinDHaultfoeuille(...).fit(data, ...)``.

Equivalent to::

ChaisemartinDHaultfoeuille(**init_kwargs).fit(
Expand All @@ -8761,6 +8766,13 @@ def chaisemartin_dhaultfoeuille(
"""
import inspect

warnings.warn(
"chaisemartin_dhaultfoeuille() is deprecated and will be removed in 4.0; "
"construct the estimator instead: "
"ChaisemartinDHaultfoeuille(...).fit(data, ...).",
FutureWarning,
stacklevel=2,
)
init_keys = {
name
for name, p in inspect.signature(ChaisemartinDHaultfoeuille.__init__).parameters.items()
Expand Down
38 changes: 21 additions & 17 deletions diff_diff/diagnostic_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
- No estimator fitting and no variance re-derivation from raw data. Every
effect, SE, p-value, CI, and sensitivity bound is either read from
``results`` or produced by an existing diff-diff utility. May call
``check_parallel_trends`` / ``bacon_decompose`` /
``check_parallel_trends`` / ``BaconDecomposition`` /
``EfficientDiD.hausman_pretest`` when the caller supplies the panel +
column kwargs. Report-layer cross-period aggregations (joint-Wald /
Bonferroni pre-trends p-value, heterogeneity dispersion over
Expand Down Expand Up @@ -138,7 +138,7 @@
# ``bacon``. ``bacon`` is excluded because SpilloverDiD identifies
# the direct effect off FAR-AWAY units (Butts Assumption 5), not
# off the TWFE 2x2 comparisons a Goodman-Bacon decomposition
# enumerates: ``bacon_decompose`` on the raw binary treatment
# enumerates: a Bacon decomposition on the raw binary treatment
# ignores the ring/distance structure and would pool spillover-
# contaminated in-ring units into the control group — the exact
# SUTVA violation the estimator exists to handle (same rationale
Expand Down Expand Up @@ -344,7 +344,7 @@ class DiagnosticReport:
survey_design : SurveyDesign, optional
The ``SurveyDesign`` object used to fit a survey-weighted
estimator. Required for fit-faithful replay of Goodman-Bacon on a
survey-backed fit; threaded to ``bacon_decompose(survey_design=...)``.
survey-backed fit; threaded to ``BaconDecomposition.fit(survey_design=...)``.
When the fit carries ``survey_metadata`` but ``survey_design`` is
not supplied, Bacon is skipped with an explicit reason rather than
replaying an unweighted decomposition for a design that does not
Expand Down Expand Up @@ -445,7 +445,7 @@ def __init__(
self._sensitivity_method = sensitivity_method
self._alpha = float(alpha)
# Round-40 P1 CI review on PR #318: survey-backed fits need the
# ``SurveyDesign`` object threaded through to ``bacon_decompose``
# ``SurveyDesign`` object threaded through to ``BaconDecomposition.fit``
# for a fit-faithful Goodman-Bacon replay, and the unweighted
# 2x2 parallel-trends helper (``utils.check_parallel_trends``)
# cannot be called on a survey-weighted DiDResults without
Expand Down Expand Up @@ -944,7 +944,7 @@ def _instance_skip_reason(self, check: str) -> Optional[str]:
if name == "BaconDecompositionResults":
return None
# Otherwise mirror the full argument contract of
# ``_check_bacon`` / ``bacon_decompose``: the runner needs
# ``_check_bacon`` / ``BaconDecomposition.fit``: the runner needs
# ``data``, ``first_treat``, and the ``outcome`` / ``time`` /
# ``unit`` column names. Gating on only ``data`` +
# ``first_treat`` (as before) left ``applicable_checks``
Expand All @@ -966,7 +966,7 @@ def _instance_skip_reason(self, check: str) -> Optional[str]:
"Bacon decomposition needs panel data + outcome / time "
"/ unit / first_treat column names. Missing: " + ", ".join(bacon_missing) + "."
)
# Round-40 P1 CI review on PR #318: ``bacon_decompose``
# Round-40 P1 CI review on PR #318: ``BaconDecomposition.fit``
# supports a ``survey_design`` kwarg for survey-weighted
# decomposition. When the fitted result carries
# ``survey_metadata`` but the caller did not supply a
Expand Down Expand Up @@ -2004,7 +2004,7 @@ def _check_bacon(self) -> Dict[str, Any]:
"""Surface Bacon decomposition: read-out when applicable, else skip.

If ``results`` is itself a ``BaconDecompositionResults``, read fields.
If ``data`` + ``first_treat`` are supplied, call ``bacon_decompose``.
If ``data`` + ``first_treat`` are supplied, run ``BaconDecomposition``.
Otherwise, skip with a helpful reason.
"""
if "bacon" in self._precomputed:
Expand Down Expand Up @@ -2046,15 +2046,16 @@ def _check_bacon(self) -> Dict[str, Any]:
}

try:
from diff_diff.bacon import bacon_decompose
from diff_diff.bacon import BaconDecomposition

bacon = bacon_decompose(
bacon = BaconDecomposition(
weights="exact", # paper-faithful Eqs. 7-9 / 10e-g
).fit(
data,
outcome=outcome,
unit=unit,
time=time,
first_treat=first_treat,
weights="exact", # paper-faithful Eqs. 7-9 / 10e-g
survey_design=self._survey_design,
)
except ValueError as exc:
Expand All @@ -2063,7 +2064,8 @@ def _check_bacon(self) -> Dict[str, Any]:
# ``_validate_unit_constant_survey``. Survey-backed reports on
# panels with time-varying within-unit weights / strata / PSU
# / FPC fail that check. The library still supports those
# panels via explicit ``bacon_decompose(weights="approximate", ...)``,
# panels via explicit
# ``BaconDecomposition(weights="approximate").fit(...)``,
# so emit a structured skip pointing users at the
# ``precomputed={'bacon': ...}`` escape hatch rather than an
# opaque ``error`` block.
Expand All @@ -2074,16 +2076,17 @@ def _check_bacon(self) -> Dict[str, Any]:
"Survey design has within-unit-varying columns "
"(weights / strata / PSU / FPC), which the "
'paper-faithful ``weights="exact"`` Bacon path '
"rejects. Run ``bacon_decompose(data, ..., "
'weights="approximate", survey_design=design)`` '
"rejects. Run "
'``BaconDecomposition(weights="approximate")'
".fit(data, ..., survey_design=design)`` "
"yourself and pass via "
"``DiagnosticReport(..., precomputed={'bacon': result})`` "
f"to populate this section. Validator detail: {exc}"
),
}
return {
"status": "error",
"reason": f"bacon_decompose raised {type(exc).__name__}: {exc}",
"reason": f"the Bacon decomposition raised {type(exc).__name__}: {exc}",
}
except NotImplementedError as exc:
# PR #454 R4 P3: ``BaconDecomposition.fit()`` raises
Expand All @@ -2102,7 +2105,7 @@ def _check_bacon(self) -> Dict[str, Any]:
"Bacon decomposition does not support (bacon is a "
"diagnostic and does not compute replicate-based "
"variance). To populate this section, run "
"``bacon_decompose(data, ..., "
"``BaconDecomposition().fit(data, ..., "
"survey_design=SurveyDesign(weights=..., strata=..., "
"psu=..., fpc=...))`` with a TSL-based design and "
"pass via "
Expand All @@ -2113,7 +2116,7 @@ def _check_bacon(self) -> Dict[str, Any]:
except Exception as exc: # noqa: BLE001
return {
"status": "error",
"reason": f"bacon_decompose raised {type(exc).__name__}: {exc}",
"reason": f"the Bacon decomposition raised {type(exc).__name__}: {exc}",
}
return self._format_bacon(bacon)

Expand Down Expand Up @@ -4148,7 +4151,8 @@ def _render_overall_interpretation(schema: Dict[str, Any], labels: Dict[str, str
sentences.append(
f"Goodman-Bacon decomposition flags {fw:.0%} of TWFE weight on "
f"'forbidden' later-vs-earlier comparisons — consider a "
f"heterogeneity-robust estimator (CS / SA / BJS / Gardner) if "
f"heterogeneity-robust estimator (CallawaySantAnna, SunAbraham, "
f"ImputationDiD, or TwoStageDiD) if "
f"not already in use."
)
deff = schema.get("design_effect") or {}
Expand Down
Loading
Loading