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

## [Unreleased]

### Deprecated
- **Mechanical rename wave** (v4 program 2(c)-ii, ledger rows [M-032]
[M-033] [M-034] [M-035]..[M-042] [M-088] [M-089] [M-094] [M-097]..[M-113]
[M-114]): the following parameters/fields gain their canonical names now,
with the old spellings kept as deprecated aliases that emit
`FutureWarning` and will be REMOVED in 4.0. The new name takes the old
name's signature position, so positional calls are unaffected, and the
deprecated path routes to bit-identical results. Passing both spellings
raises `ValueError`.
- `WooldridgeDiD.fit`: `cohort=` -> `first_treat=` [M-032].
- `ChaisemartinDHaultfoeuille.fit`: `group=` -> `unit=` [M-033],
`controls=` -> `covariates=` [M-034]; `twowayfeweights()`:
`group=` -> `unit=` [M-097].
- `ChaisemartinDHaultfoeuilleResults.groups` -> `.units` [M-114]
(deprecated read-only alias property; `repr` reports `n_units=`,
`summary()` prints "Units (post-filter):"; old pickles migrate via
`__setstate__`; the `n_groups_dropped_*` counters are unchanged, and
CS-family `groups` cohort vocabulary is NOT affected).
- `HeterogeneousAdoptionDiD.fit` and the `joint_pretrends_test` /
`joint_homogeneity_test` / `did_had_pretest_workflow` functions:
`outcome_col`/`dose_col`/`time_col`/`unit_col`/`first_treat_col` ->
bare `outcome`/`dose`/`time`/`unit`/`first_treat`
[M-035]..[M-039], [M-098]..[M-112].
- `RegressionDiscontinuity.fit`: `outcome_col`/`running_col`/
`treatment_col` -> `outcome`/`running`/`takeup` [M-040]..[M-042];
`RegressionDiscontinuityResults.treatment_col` -> `.takeup` [M-094]
(alias property + `__setstate__`; `to_dict()` emits BOTH `"takeup"`
and the deprecated `"treatment_col"` key through 3.9, the old key is
dropped in 4.0). `RDPlot.fit`: `outcome_col`/`running_col` ->
`outcome`/`running` [M-088] [M-089].
- `trim_weights()`: `weight_col=` -> `weights=` [M-113].
- Note: sentinel-defaulted shim parameters are transitionally annotated
`Any`; the 4.0 removal restores the narrow annotations.

### Changed
- **Shared `BaseEstimator` mixin** (`diff_diff/_base.py`, v4 program 2(c)-i):
the 25 hand-rolled `get_params`/`set_params` pairs (6 divergent
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
- [TripleDifference](https://diff-diff.readthedocs.io/en/stable/api/triple_diff.html) - triple difference (DDD) estimator for designs requiring two criteria for treatment eligibility
- [ContinuousDiD](https://diff-diff.readthedocs.io/en/stable/api/continuous_did.html) - Callaway, Goodman-Bacon & Sant'Anna (2024) continuous treatment DiD with dose-response curves
- [HeterogeneousAdoptionDiD](https://diff-diff.readthedocs.io/en/stable/api/had.html) - de Chaisemartin, Ciccia, D'Haultfœuille & Knau (2026) for designs where **no unit remains untreated**; local-linear estimator at the dose support boundary returning Weighted Average Slope (WAS) on Design 1' (`d̲ = 0` / QUG) or `WAS_{d̲}` on Design 1 (`d̲ > 0`, continuous-near-d̲ or mass-point), with a multi-period event-study extension (last-treatment cohort, pointwise CIs). **Panel-only** in this release - repeated cross-sections rejected by the validator. Alias `HAD`.
- [RegressionDiscontinuity](https://diff-diff.readthedocs.io/en/stable/api/regression_discontinuity.html) - Calonico, Cattaneo & Titiunik (2014) sharp, fuzzy, AND covariate-adjusted regression discontinuity with robust bias-corrected inference and rdrobust-parity bandwidth selection (all 10 selectors, mass-point handling; fuzzy via `treatment_col=` with a first-stage block and weak-identification warning; covariates via `covariates=` - CCFT 2019, same estimand, covariate-aware bandwidths). Canonical `att` is the bias-corrected estimate with a coherent robust CI (rdrobust's printed headline is `att_conventional`). Alias `RDD`.
- [RegressionDiscontinuity](https://diff-diff.readthedocs.io/en/stable/api/regression_discontinuity.html) - Calonico, Cattaneo & Titiunik (2014) sharp, fuzzy, AND covariate-adjusted regression discontinuity with robust bias-corrected inference and rdrobust-parity bandwidth selection (all 10 selectors, mass-point handling; fuzzy via `takeup=` with a first-stage block and weak-identification warning; covariates via `covariates=` - CCFT 2019, same estimand, covariate-aware bandwidths). Canonical `att` is the bias-corrected estimate with a coherent robust CI (rdrobust's printed headline is `att_conventional`). Alias `RDD`.
- [StackedDiD](https://diff-diff.readthedocs.io/en/stable/api/stacked_did.html) - Wing, Freedman & Hollingsworth (2024) stacked DiD with Q-weights and sub-experiments; optional covariate balancing (Ustyuzhanin 2026)
- [EfficientDiD](https://diff-diff.readthedocs.io/en/stable/api/efficient_did.html) - Chen, Sant'Anna & Xie (2025) efficient DiD with optimal weighting for tighter SEs
- [TROP](https://diff-diff.readthedocs.io/en/stable/api/trop.html) - Triply Robust Panel estimator (Athey et al. 2025) with nuclear norm factor adjustment
Expand Down
164 changes: 164 additions & 0 deletions diff_diff/_deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
"""Shared deprecation-shim machinery for the 3.9 rename sweep (4.0 program, Phase 2(c)-ii).

This module is a deliberate LEAF: it imports only the standard library, never
other ``diff_diff`` modules, so every estimator module can import it without
cycles (the ``_base.py`` precedent). It is private and unexported; the 4.0
removal PR deletes the shims that use it.

The helpers implement the ledger's shim contract (``docs/v4-deprecations.yaml``,
groups ``renames-*``; rules in ``docs/v4-design.md`` section 8):

- Renamed parameter: the NEW name occupies the old name's signature position,
the OLD name moves to the signature tail with the ``NOT_SUPPLIED`` sentinel
default. ``resolve_renamed_kwarg`` maps old to new with a single
``FutureWarning``, rejects both-supplied calls loudly, and returns the
caller-declared default when neither is passed.
- Dropped parameter (no successor name, e.g. ``robust``): the parameter keeps
its position with a ``None`` sentinel default; the constructor calls
``warn_deprecated_kwarg`` when a value was actually supplied.
- Renamed results field: the dataclass field takes the new name and
``deprecated_field_property`` builds the read-only warning alias under the
old name (a plain class attribute, so it is a descriptor but NOT a
``__dataclass_fields__`` entry - the shape ``tests/test_v4_matrix.py``
asserts for ``shimmed`` field rows).

Stacklevel contract: ``resolve_renamed_kwarg`` / ``warn_deprecated_kwarg``
must be called DIRECTLY from the public method that owns the parameter
(user frame -> public method -> helper -> ``warnings.warn`` = stacklevel 3).
``deprecated_field_property``'s getter warns at stacklevel 2 (user frame ->
property getter).

All warnings are ``FutureWarning`` (visible to end users by default) per the
section 2 category rule: new shims warn with ``FutureWarning``; only the
pre-existing M-001..M-003 sites keep ``DeprecationWarning``.
"""

import warnings
from typing import Any

__all__: "list[str]" = []


class _NotSupplied:
"""Sentinel for renamed/deprecated parameters (M-020 precedent).

A plain ``None`` default cannot distinguish "not passed" from "passed
None", so a bare ``None`` default would fire the FutureWarning on EVERY
call (and break the warnings-as-errors ``cls(**est.get_params())``
round-trip in ``tests/test_base_estimator.py``). The warning must fire
only when the caller actually supplies the argument.
"""

def __repr__(self) -> str: # pragma: no cover - debugging aid
return "<not supplied>"


NOT_SUPPLIED = _NotSupplied()


def deprecated_kwarg_message(qualname: str, name: str, instruction: str) -> str:
"""The single message template every shim warning and test pins."""
return f"{qualname}({name}=) is deprecated and will be removed in 4.0; {instruction}."


def warn_deprecated_kwarg(
qualname: str, name: str, instruction: str, *, stacklevel: int = 3
) -> None:
"""Emit the deprecation warning for a supplied deprecated parameter.

The caller performs the was-it-supplied check (sentinel or ``None``
depending on the parameter's legal value space) so that default and
round-trip constructions stay silent.
"""
warnings.warn(
deprecated_kwarg_message(qualname, name, instruction),
FutureWarning,
stacklevel=stacklevel,
)


def resolve_renamed_kwarg(
qualname: str,
old_name: str,
old_value: Any,
new_name: str,
new_value: Any,
*,
default: Any = None,
extra: str = "",
) -> Any:
"""Resolve a renamed keyword pair during the 3.9 shim window.

Parameters
----------
qualname : str
The public surface the parameters belong to (``"Class.method"`` or
a module-level function name) - used verbatim in messages.
old_name, old_value : str, Any
The deprecated parameter and what the caller passed (``NOT_SUPPLIED``
when absent).
new_name, new_value : str, Any
The canonical parameter and what the caller passed (``NOT_SUPPLIED``
when absent).
default : Any
Returned when neither name was supplied. Required parameters pass
``NOT_SUPPLIED`` here and re-validate with :func:`require_arg`.
extra : str
Appended to the warning message (e.g. M-031's note that ``time=``
survives as the calendar column).

Returns
-------
Any
The effective value for the canonical parameter.
"""
old_passed = not isinstance(old_value, _NotSupplied)
new_passed = not isinstance(new_value, _NotSupplied)
if old_passed and new_passed:
raise ValueError(
f"{qualname}() got both {old_name}= (deprecated) and {new_name}=; "
f"pass only {new_name}=."
)
if old_passed:
message = deprecated_kwarg_message(qualname, old_name, f"use {new_name}= instead")
if extra:
message = f"{message} {extra}"
warnings.warn(message, FutureWarning, stacklevel=3)
return old_value
if new_passed:
return new_value
return default


def require_arg(qualname: str, name: str, value: Any) -> None:
"""Re-validate required-ness after the rename mapping.

Sentinel-defaulted required parameters lose Python's built-in
missing-argument ``TypeError``; this restores it, naming the NEW
parameter.
"""
if isinstance(value, _NotSupplied):
raise TypeError(f"{qualname}() missing required argument: '{name}'")


def deprecated_field_property(cls_name: str, old: str, new: str) -> property:
"""Build the read-only warning alias for a renamed results field.

Assigned WITHOUT an annotation in the dataclass body, so it stays a
plain descriptor (never a ``__dataclass_fields__`` entry). Wording
follows the ``SyntheticDiDResults`` renamed-field alias precedent
(M-003, ``diff_diff/results.py``), with the FutureWarning category the
ledger declares for the M-094/M-095/M-114 rows.
"""

def _get(self: Any) -> Any:
warnings.warn(
f"{cls_name}.{old} is deprecated; use {new} instead. " "Will be removed in 4.0.",
FutureWarning,
stacklevel=2,
)
return getattr(self, new)

_get.__name__ = old
_get.__doc__ = f"Deprecated alias for :attr:`{new}` (removed in 4.0)."
return property(_get)
5 changes: 3 additions & 2 deletions diff_diff/agent_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def agent_workflow(
# `first_treat` does not by itself identify which estimator to use:
# CallawaySantAnna (binary staggered), ContinuousDiD (continuous-
# dose with first_treat), and HeterogeneousAdoptionDiD event-study
# (heterogeneous intensity with first_treat_col) all accept it.
# (heterogeneous intensity with first_treat) all accept it.
# Show CallawaySantAnna as the binary-staggered canonical example
# and list the alternatives for continuous / heterogeneous designs
# so an agent isn't steered to the wrong estimator.
Expand All @@ -185,7 +185,8 @@ def agent_workflow(
" - Continuous dose : ContinuousDiD (also takes first_treat=)",
" - Heterogeneous adoption intensity:",
" HeterogeneousAdoptionDiD (event study,",
" takes first_treat_col=, NOT first_treat=)",
" takes first_treat=; first_treat_col= is a",
" deprecated alias slated for 4.0 removal)",
]
else:
fit_example_kwargs = _join_kwargs(
Expand Down
Loading