Migrating to 4.0#
Note
This describes the upcoming 4.0 release. Most of the renames below can be adopted from 3.9 onward, well before 4.0 lands — the new spellings and the old ones both work during the deprecation window, so you can migrate incrementally and silence the warnings as you go.
diff-diff 4.0 removes the deprecated surface that 3.9 shipped warnings for, merges three pairs
of estimators, and flips two families of inference defaults. Every change is tracked as a row in
docs/v4-deprecations.yaml, and the appendix below is checked against that ledger by
tests/test_v4_matrix.py — if a row is added, moved, or rescheduled and this page is not
updated, CI fails.
What changes#
Area |
What changes |
Rows |
Where |
|---|---|---|---|
aggregate-postfit |
Aggregation moves off |
13 |
§4 |
alias-table |
Six export aliases are removed in favour of their canonical class names |
6 |
§7 |
constructor-hygiene |
|
1 |
§7b |
df-convention-flip |
The |
7 |
§6 |
diagnostic-family |
Bacon is re-homed out of the estimator roster into the diagnostics family |
1 |
§7b |
field-flip |
Nine results containers rename |
9 |
§5 |
function-wrappers |
Eight module-level wrapper functions are removed; call the classes |
8 |
§7 |
merge-ddd |
|
5 |
§2 |
merge-mpd |
|
5 |
§2 |
merge-qdid |
|
2 |
§2 |
obligation-sdid-params |
Two SyntheticDiD constructor params (inert since 3.0.0) are removed |
3 |
§7b |
obligation-warning-retirements |
A transition |
1 |
§7b |
policy-auto-cluster |
TwoWayFixedEffects auto-clusters on |
1 |
§6 |
renames-cohort |
|
1 |
§3 |
renames-col-suffix |
The |
27 |
§3 |
renames-control-group |
|
2 |
§3 |
renames-dcdh |
dCDH’s |
4 |
§3 |
renames-level |
Aggregation-level params and their accepted values unify on |
4 |
§3 |
renames-post |
The post-dummy |
3 |
§3 |
renames-robust-drop |
|
4 |
§3 |
results-contract |
A legacy sentinel field is retired in favour of the unified event-study surface |
1 |
§5 |
The three merges#
Each merge keeps one class and retires the other. The retired class still works in 3.9 and warns.
MultiPeriodDiD → TwoWayFixedEffects(event_study=True)#
# 3.x
from diff_diff import MultiPeriodDiD
results = MultiPeriodDiD().fit(data, outcome="y", unit="id", time="period", treatment="treat")
# 4.0
from diff_diff import TwoWayFixedEffects
results = TwoWayFixedEffects().fit(
data, outcome="y", unit="id", time="period", treatment="treat",
event_study=True, spec="pooled", post_periods=[3, 4, 5],
)
Two things are easy to get wrong here, and both change numbers:
spec="pooled"reproduces the MultiPeriodDiD design. The new default isspec="within", which adds unit fixed effects. That moves point estimates as well as standard errors on unbalanced panels or with covariates — the two specs coincide only in the restricted equivalence case (balanced panel, no covariates, simultaneous adoption). “Only SEs move” is not the migration message.post_periods=is required in event-study mode. MultiPeriodDiD defaulted to a midpoint split of the calendar, which is a silent guess; the merged mode makes you state the treatment boundary.spec="pooled"is also the only spec valid for repeated cross-sections.
StaggeredTripleDifference → TripleDifference#
# 3.x
from diff_diff import StaggeredTripleDifference
results = StaggeredTripleDifference().fit(data, outcome="y", unit="id", time="period",
first_treat="g", eligibility="p")
# 4.0
from diff_diff import TripleDifference
results = TripleDifference().fit(data, outcome="y", unit="id", time="period",
first_treat="g", partition="p")
The staggered fit parameters are keyword-only on the merged class. The results container is the
unified TripleDifference shape; the 2x2x2 design reads as a degenerate single-ATT view of it.
QDiD → ChangesInChanges(method=”qdid”)#
# 3.x
from diff_diff import QDiD
results = QDiD(n_bootstrap=200, seed=42).fit(data, outcome="y", treatment="treat", time="post")
# 4.0
from diff_diff import ChangesInChanges
results = ChangesInChanges(method="qdid", n_bootstrap=200, seed=42).fit(
data, outcome="y", treatment="treat", time="post")
Only the class spelling is deprecated, not the estimator. method="qdid" is a fully
supported comparison mode and emits no warning of its own — the numbers are unchanged, because
it is the same engine. method="cic" is the default and encodes Athey–Imbens’ recommendation.
Renamed parameters#
Most renames are mechanical: the new spelling already exists, so you can adopt it today and the old one keeps working until 4.0. See the appendix for the full list, and the R Comparison page for how these names map onto the R packages.
Three families need more than a search-and-replace:
robust= → vcov_type= is per-estimator, not a single rule. Verified against the current
release:
estimator |
|
|
|---|---|---|
|
already the |
|
|
drop it |
drop it — only |
|
|
drop it — non-robust is its legacy default |
time= → post= carries a semantic change, and the old name survives. On
DifferenceInDifferences, TripleDifference and TwoWayFixedEffects, time= used to mean a
0/1 post dummy. The name is not removed — it is repurposed to mean the calendar column. From
4.0, passing time= in static mode (TwoWayFixedEffects) or 2x2x2 mode (TripleDifference)
raises ValueError rather than being silently reinterpreted. If you pass a post dummy, rename
it to post=; if you pass a calendar column to an event-study or staggered fit, leave it alone.
Dropped parameters are not always deletions. A parameter with no successor in the appendix
may still need a replacement call — see its Fix cell.
Post-fit aggregation#
Aggregation moves off fit() onto the results object:
# 3.x
results = CallawaySantAnna().fit(data, ..., aggregate="event_study")
# 4.0
results = CallawaySantAnna().fit(data, ...)
event_study = results.aggregate("event_study")
DiagnosticReport / BusinessReport need no migration step of their own:
where applicable, their event-study-gated checks derive the surface
internally via the result’s post-fit aggregate('event_study') when the
raw event_study_effects field is absent, so moving a fit off the
fit-time keyword no longer silently disables those checks. The routing is
estimator-specific: CallawaySantAnna derives for parallel trends,
pre-trends power, and sensitivity; ImputationDiD and TwoStageDiD for
parallel trends (pretrends=True fits) and heterogeneity; ContinuousDiD
for heterogeneity. StackedDiD and SunAbraham always populate the raw
surface (nothing to derive), and ChaisemartinDHaultfoeuille’s pre-period
checks read placebo_event_study directly.
Warning
Bootstrapped fits: CallawaySantAnna, DMLDiD, and EfficientDiD are covered; the remaining recompute adopters are not yet.
On CallawaySantAnna, DMLDiD (which inherits the CS replay channel), and EfficientDiD, the post-fit recompute levels now REPLAY the
fit-time multiplier bootstrap from the fit-retained RNG state (percentile inference
matching a fit-time aggregation to floating-point reassociation) — no fit-time keyword
needed; only pre-replay legacy pickles and artifacts moved across the Rust/NumPy weight
backend fail closed with a refit message. On ImputationDiD, TwoStageDiD and
ContinuousDiD, the recompute levels still raise NotImplementedError when the fit used
n_bootstrap > 0 — keep the fit-time call there for now and track their open TODO.md
rows.
aggregate("simple") — and, on its five adopters, aggregate("total") (3.10; DMLDiD joined in 3.11 — panel non-survey fits only: RCS fits, declared-survey_design= fits, and bare-cluster= fits with incomplete treated support fail total closed) — does relay,
and StackedDiD, ChaisemartinDHaultfoeuille and HeterogeneousAdoptionDiD are unaffected —
their aggregate() is a pure view over stored fields.
Results fields#
Nine containers rename overall_att to the canonical att. The new accessor already works
today, so this migration can be done immediately; the old name becomes a warning-emitting
property at 4.0 and is removed at 5.0.
att = results.att # canonical, works now
att = results.overall_att # warns from 4.0, removed at 5.0
The whole inference quintet moves, not just the point estimate. Wherever a container
carried overall_* inference fields alongside overall_att, they flip to the canonical
names in the same step:
old |
new |
|---|---|
|
|
|
|
|
|
|
|
|
|
Note
ContinuousDiDResults is the outlier: its sibling fields are spelled
overall_att_se, overall_att_t_stat, overall_att_p_value and
overall_att_conf_int — with the att infix — and they flip to the same canonical
names. If you grep for overall_se you will miss them.
Inference defaults that move numbers#
Two changes alter results without changing any call:
df_conventionflips to"cluster"on seven estimators. Passdf_convention="residual"to reproduce 3.x numbers exactly.Panel estimators auto-cluster at
unitwhencluster=is omitted. Passcluster=Falseto disable it —Noneand omission both mean “auto-cluster”, socluster=Nonedoes not reproduce unclustered 3.x standard errors. Cross-sectional 2x2 estimators stay HC-robust unless you passcluster=. Note the TWFE event-study mode has auto-clustered since 3.9 (it shipped that way with the merge), so this flip changes the static and other panel paths, not that one.
Removed functions and aliases#
Eight module-level wrapper functions and six export aliases are removed. Both were thin indirections: call the estimator class directly, or import the canonical name. The appendix lists each one with its target.
Remaining 4.0 changes#
Smaller items that do not fit the families above — two inert SyntheticDiD constructor
parameters, the covariates= constructor-to-fit() move, a retired transition warning, the
Bacon roster re-homing, and the family-wide anticipation validation below. The appendix lists
the ledger-derived removals/flips; [M-144], [M-145], and [M-146] are behavior tightenings,
and [M-147] is an additive policy. These have no removal/deprecation fields and appear here only.
WooldridgeDiD(unsupported_period_action="error")([M-147]) refuses periods lacking comparison support before removing them. The default"drop"preserves filtering and warnings, so existing callers need no migration. The policy applies to all three methods independently ofrank_deficient_action; other identification checks still apply. Survey validation runs first, then"error"raisesValueErrorfor unsupported periods;"drop"retains the survey-domainNotImplementedErrorwhen removal would be required.anticipationis validated across the family ([M-144], landing at 4.0): whole-valued floats that previously fit identically to their integer now raise — pass theint; bool and negative/non-integer values raise too. Accepted numpy integers are normalized, so the publicanticipationattribute andget_params()["anticipation"]are now always built-inint(previously a numpy scalar survived). WooldridgeDiD’s error message text changed to the shared wording, and its constructor now reports a badbootstrap_weights/vcov_type/df_conventionbefore a badanticipation(the ordering flipped).pscore_trimis validated via a shared helper ([M-145], landing at 4.0):ContinuousDiDtightens from[0, 0.5)to(0, 0.5)—pscore_trim=0(which disabled the overlap clip keeping IPW/DR weights finite) now raises, its message wording changed, and non-real-scalar inputs (None, strings,Decimal/Fraction, 1-element arrays) raiseValueErrorinstead ofTypeError(or being silently accepted, for the 1-element array).CallawaySantAnnagains the same type guard;TripleDifference,CallawaySantAnna, andContinuousDiDnow store the value coerced to built-infloat;LWDiD’s message wording changed. The deprecatedStaggeredTripleDifferencekeeps its permissive construction shape.summary(alpha=...)/print_summary(alpha=...)on the results classes family-wide ([M-146], landing at 4.0; the staggered family first, then the non-staggered summaries - DiDResults incl. SpilloverDiDResults, MultiPeriodDiDResults, SyntheticDiDResults, TripleDifferenceResults, TROPResults, ContinuousDiDResults): a value different from the fit-timealphanow raisesValueErrorinstead of silently relabeling the confidence-interval header over fit-time stored intervals;alpha=0.0, previously swallowed by a falsy-ordefault, raises too. Re-fit at the desired alpha instead.SyntheticControlResults.summaryjoins the contract as a carve-out: itsalphawas a dead no-op (it prints no alpha-based interval; the displayed confidence set is keyed on its own storedgamma), and a non-fit value now raises like the rest of the family.
One pending decision: the DIFF_DIFF_SOLVE_OLS_FASTPATH environment default has a go/no-go due
at 4.0 that has not been made. If it lands on, it is a numerics change and will be documented
then; “evaluated, kept off” is an equally valid outcome, so it carries no appendix row today.
Codemod#
The mechanical keyword renames can be applied with a regex sweep. This table covers only
identifier renames — it deliberately excludes dropped parameters (no successor to write),
results-field renames (.groups → .units would rewrite every pandas groupby(...).groups in
your code), and accepted-value renames, which change strings rather than names.
find |
replace |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Note treatment_col maps to takeup, not treatment — it is the one *_col parameter
whose replacement is not just the suffix dropped.
Three renames are deliberately NOT in the table, because a global replace would corrupt working code. Each is a rename only in a specific call:
rename |
where it applies |
why it cannot be global |
|---|---|---|
|
|
|
|
|
|
|
the aggregation family |
not a rename at all; it moves off |
Review every hit even in the safe table: these are word-boundary matches on common words.
Already shipped in 3.9#
These landed in 3.9 and need no 4.0 action, but they moved behaviour and are easy to mistake for 4.0 changes:
Numbers moved. The ETWFE reference-period fixes (unidentified-cohort exclusion, and a
fail-closed guard on designs with no estimable post-treatment cell), the clustered-CR1
K_reference convergence, and the tail-df consolidation.
Fail-closed validation. n_bootstrap semantics unified; inference="wild_bootstrap" without
cluster= now raises; TripleDifference(pscore_trim=0) is rejected; an all-NaN Wooldridge
overall ATT raises rather than returning NaN. These are refusals, not default changes — no
numeric default moved.
Additive API and container changes. The unified EventStudyResults surface, the
AggregationResult container, and the Diagnostic marker base on the diagnostic result roster.
The alias-diet __getattr__ shim also landed here: CDiD, Stacked and Gardner still import
but now emit a FutureWarning, and they no longer appear in dir()/vars().
Appendix: every 4.0 change#
One row per ledger row that requires action at 4.0 — 108 in total. Old and New are the
ledger’s own locators. An em dash in New means the ledger names no successor locator; it
does not mean no action is required, so read the Fix cell.
Row |
Group |
Old |
New |
Fix |
|---|---|---|---|---|
M-020 |
aggregate-postfit |
|
|
Move |
M-021 |
aggregate-postfit |
|
|
Move |
M-022 |
aggregate-postfit |
|
|
Move |
M-023 |
aggregate-postfit |
|
|
Move |
M-024 |
aggregate-postfit |
|
|
Move |
M-025 |
aggregate-postfit |
|
|
Move |
M-026 |
aggregate-postfit |
|
|
Move |
M-027 |
aggregate-postfit |
|
|
Move |
M-117 |
aggregate-postfit |
|
|
Move |
M-118 |
aggregate-postfit |
|
|
Move |
M-119 |
aggregate-postfit |
|
|
Move |
M-120 |
aggregate-postfit |
|
|
Move |
M-139 |
aggregate-postfit |
|
— |
Remove |
M-060 |
alias-table |
|
— |
Import |
M-061 |
alias-table |
|
— |
Import |
M-064 |
alias-table |
|
— |
Import |
M-132 |
alias-table |
|
— |
Import |
M-133 |
alias-table |
|
— |
Import |
M-134 |
alias-table |
|
— |
Import |
M-084 |
constructor-hygiene |
|
|
|
M-004 |
df-convention-flip |
|
— |
The |
M-005 |
df-convention-flip |
|
— |
The |
M-006 |
df-convention-flip |
|
— |
The |
M-128 |
df-convention-flip |
|
— |
The |
M-129 |
df-convention-flip |
|
— |
The |
M-130 |
df-convention-flip |
|
— |
The |
M-131 |
df-convention-flip |
|
— |
The |
M-090 |
diagnostic-family |
|
— |
Bacon moves out of the estimator roster into the diagnostics family - update docs references and imports of the roster, not call sites. |
M-050 |
field-flip |
|
|
Read |
M-051 |
field-flip |
|
|
Read |
M-052 |
field-flip |
|
|
Read |
M-053 |
field-flip |
|
|
Read |
M-054 |
field-flip |
|
|
Read |
M-055 |
field-flip |
|
|
Read |
M-056 |
field-flip |
|
|
Read |
M-057 |
field-flip |
|
|
Read |
M-058 |
field-flip |
|
|
Read |
M-070 |
function-wrappers |
|
— |
Call the estimator class directly instead of |
M-071 |
function-wrappers |
|
— |
Call the estimator class directly instead of |
M-072 |
function-wrappers |
|
— |
Call the estimator class directly instead of |
M-073 |
function-wrappers |
|
— |
Call the estimator class directly instead of |
M-074 |
function-wrappers |
|
— |
Call the estimator class directly instead of |
M-075 |
function-wrappers |
|
— |
Call the estimator class directly instead of |
M-076 |
function-wrappers |
|
— |
Call the estimator class directly instead of |
M-077 |
function-wrappers |
|
— |
Call the estimator class directly instead of |
M-013 |
merge-ddd |
|
|
See the merges section for the worked before/after. |
M-014 |
merge-ddd |
|
— |
Read the unified |
M-085 |
merge-ddd |
|
— |
In 2x2x2 mode, |
M-140 |
merge-ddd |
|
|
No successor yet - post-fit |
M-141 |
merge-ddd |
|
|
No successor yet - post-fit |
M-010 |
merge-mpd |
|
|
See the merges section for the worked before/after. |
M-011 |
merge-mpd |
|
— |
Read the unified event-study surface on the merged |
M-012 |
merge-mpd |
|
— |
Superseded by the unified event-study representation - read the event-study surface rather than per-period |
M-016 |
merge-mpd |
|
— |
Not removed at 4.0: it becomes a |
M-083 |
merge-mpd |
|
— |
In static mode ( |
M-015 |
merge-qdid |
|
|
See the merges section for the worked before/after. |
M-143 |
merge-qdid |
|
|
See the merges section for the worked before/after. |
M-001 |
obligation-sdid-params |
|
— |
Drop it. It has been IGNORED since 3.0.0, so 3.x already auto-computes regularization - do NOT copy its value into |
M-002 |
obligation-sdid-params |
|
— |
Drop it. It has been IGNORED since 3.0.0, so 3.x already auto-computes regularization - do NOT copy its value into |
M-003 |
obligation-sdid-params |
|
|
Read |
M-007 |
obligation-warning-retirements |
|
— |
The MultiPeriodDiD |
M-080 |
policy-auto-cluster |
|
— |
Panel estimators auto-cluster at |
M-032 |
renames-cohort |
|
|
Rename the keyword: |
M-035 |
renames-col-suffix |
|
|
Rename the keyword: |
M-036 |
renames-col-suffix |
|
|
Rename the keyword: |
M-037 |
renames-col-suffix |
|
|
Rename the keyword: |
M-038 |
renames-col-suffix |
|
|
Rename the keyword: |
M-039 |
renames-col-suffix |
|
|
Rename the keyword: |
M-040 |
renames-col-suffix |
|
|
Rename the keyword: |
M-041 |
renames-col-suffix |
|
|
Rename the keyword: |
M-042 |
renames-col-suffix |
|
|
Rename the keyword: |
M-088 |
renames-col-suffix |
|
|
Rename the keyword: |
M-089 |
renames-col-suffix |
|
|
Rename the keyword: |
M-094 |
renames-col-suffix |
|
|
Read |
M-098 |
renames-col-suffix |
|
|
Rename the keyword: |
M-099 |
renames-col-suffix |
|
|
Rename the keyword: |
M-100 |
renames-col-suffix |
|
|
Rename the keyword: |
M-101 |
renames-col-suffix |
|
|
Rename the keyword: |
M-102 |
renames-col-suffix |
|
|
Rename the keyword: |
M-103 |
renames-col-suffix |
|
|
Rename the keyword: |
M-104 |
renames-col-suffix |
|
|
Rename the keyword: |
M-105 |
renames-col-suffix |
|
|
Rename the keyword: |
M-106 |
renames-col-suffix |
|
|
Rename the keyword: |
M-107 |
renames-col-suffix |
|
|
Rename the keyword: |
M-108 |
renames-col-suffix |
|
|
Rename the keyword: |
M-109 |
renames-col-suffix |
|
|
Rename the keyword: |
M-110 |
renames-col-suffix |
|
|
Rename the keyword: |
M-111 |
renames-col-suffix |
|
|
Rename the keyword: |
M-112 |
renames-col-suffix |
|
|
Rename the keyword: |
M-113 |
renames-col-suffix |
|
|
Rename the keyword: |
M-043 |
renames-control-group |
|
|
Rename the keyword: |
M-095 |
renames-control-group |
|
|
Read |
M-033 |
renames-dcdh |
|
|
Rename the keyword: |
M-034 |
renames-dcdh |
|
|
Rename the keyword: |
M-097 |
renames-dcdh |
|
|
Rename the keyword: |
M-114 |
renames-dcdh |
|
|
Read |
M-044 |
renames-level |
|
|
Rename the keyword: |
M-086 |
renames-level |
|
|
Change the accepted value: |
M-087 |
renames-level |
|
— |
|
M-136 |
renames-level |
|
|
Change the accepted value: |
M-030 |
renames-post |
|
|
Rename the keyword: |
M-137 |
renames-post |
|
|
Rename the keyword: |
M-138 |
renames-post |
|
|
Rename the keyword: |
M-045 |
renames-robust-drop |
|
— |
Drop |
M-046 |
renames-robust-drop |
|
— |
Drop it. |
M-047 |
renames-robust-drop |
|
— |
HAD’s legacy default is non-robust: |
M-115 |
renames-robust-drop |
|
— |
Drop |
M-093 |
results-contract |
|
|
The sentinel is retired - read the unified |