Repository navigation
Breaking Changes
-
LWDiDResults.pvalueandLWDiDResults.ciare removed (ledger rowsM-151,M-152): the
3.10-born alias properties forp_value/conf_intwere first shimmed with aFutureWarning
and then removed outright at 4.0 (item 9b of the cut). Read.p_value/.conf_int. -
LWDiDResults.to_dataframe()is reshaped onto the shared results schema (ledger row
M-153): eachlevelreturnsaggregate(level).to_dataframe()- columnslevel, label, target, att, se, t_stat, p_value, conf_int_lower, conf_int_upper, n, weight, df(the
event-study schema forlevel="event_study") - plus LWDiD'srolling,estimation_method
andvcov_typecolumns. The default"simple"is ONE headline row; the former per-cohort
rows arelevel="group"(without the old trailing"Overall"row). Theterm/cohort
column is nowlabel,n_treatedis the schema'sn,n_obsandn_controlare no longer
in the frame (both stay into_dict()), andci_lower/ci_upperare
conf_int_lower/conf_int_upper.to_csv()follows and takes the samelevel. -
BusinessReport.to_dict()schema2.0->3.0: the StackedDiDassumptionblock no longer
carries the legacyclean_controlkey (readcontrol_group).DiagnosticReport's schema stays
2.0at this step. (Intermediate: later bullets in this release move the final 4.0 versions to
BUSINESS_REPORT_SCHEMA_VERSION4.0 andDIAGNOSTIC_REPORT_SCHEMA_VERSION3.0.) -
Passing
aggregate=orbalance_e=toCallawaySantAnna.fit,EfficientDiD.fit,
StackedDiD.fit,ChaisemartinDHaultfoeuille.fitorHeterogeneousAdoptionDiD.fit(or
aggregate=todid_had_pretest_workflow) now raises Python's native
TypeError: ... unexpected keyword argument.TripleDifference's staggered-mode
aggregate=/balance_e=are untouched at this step (M-140/M-141; they are REMOVED later in
this release by the DDD container unification bullet below). -
Positional slots after the removed parameters shift left.
CallawaySantAnna:
survey_design9 -> 7;EfficientDiD:survey_design9 -> 7,store_eif10 -> 8;
StackedDiD(aggregatesat before them):population,survey_design,covariates
7/8/9 -> 6/7/8;ChaisemartinDHaultfoeuille:L_max,covariates,trends_linear,
trends_nonparam,honest_did,heterogeneity,design2,survey_designeach move one
slot left (7..14 -> 6..13).HeterogeneousAdoptionDiD.fitshifts nothing (everything after
the removed slot is keyword-only; a seventh positional now raises). Pass these by keyword. -
CallawaySantAnnaResults.to_dataframe(level="event_study"|"group")and the
EfficientDiDResultstwins raise on every fresh fit (the legacy fields are alwaysNone);
useresults.aggregate(...).to_dataframe(). (Intermediate: theM-093schema bullet below
supersedes this - since item 8bto_dataframe(level="event_study")recomputes the surface on
these classes instead of raising;level="group"still raises with the post-fit pointer.) -
Passing
aggregate=orbalance_e=toImputationDiD.fit,TwoStageDiD.fitor
ContinuousDiD.fitnow raises Python's nativeTypeError: ... unexpected keyword argument. -
Positional slots after the removed parameters shift left.
ImputationDiDand
TwoStageDiD:survey_design9 -> 7;ContinuousDiD:survey_design,covariates
8/9 -> 7/8. Pass these by keyword. -
aggregate('event_study', balance_e=k)on a BOOTSTRAPPEDImputationDiD/TwoStageDiD
fit raisesNotImplementedError(the relayed surface was computed unrestricted; re-fit with
n_bootstrap=0for a balanced window). Analytic fits recompute withbalance_eas before. -
A bootstrapped
ImputationDiD/TwoStageDiD/ContinuousDiDfit WITH bootstrap inference
but WITHOUT a stored surface - a 3.9-3.13 bootstrapped plain-fit pickle, or a fit whose
fail-soft materialization dropped it - raisesValueErrorfromaggregate('event_study')/
aggregate('group')(previouslyNotImplementedErrorfrom the bootstrap gate); the message
names the refit. A 3.9-3.13 bootstrapped pickle fitted withbalance_e=kstores a restricted
surface and the relay returns it as stored. -
TripleDifference.fit(time=)in the 2x2x2 design now raisesValueErrornamingpost=
(M-085). Through 3.xtime=was the deprecated alias for the 0/1 post indicator and emitted
aFutureWarning(M-031); since 4.0timemeans the staggered calendar column only.
Supplying bothpost=andtime=keeps raising the "pass only post=" error. -
Keyword mapping for callers of the removed class:
eligibility=ispartition=on
TripleDifference; the staggered fit parameters are keyword-only;cluster=raises in
staggered mode (usen_bootstrap > 0for unit-level clustered inference). Users of
balance[did]stay on 3.x through that extra'sdiff-diff<4pin, where the old name still
resolves. -
TwoWayFixedEffects.fit(time=)in static mode now raisesValueErrornamingpost=
(M-083). Through 3.xtime=was the deprecated alias for the 0/1 post indicator and
emitted aFutureWarning(M-082); since 4.0timemeans the event-study calendar column
only. Supplying bothpost=andtime=keeps raising the "pass only post=" error. -
EventStudyResults.period_effectsis removed (M-016; the compatibility view
this item first shipped was retired at 4.0 by item 9b - no release carried it). Through
the pre-release window, on a calendar surface it returned the former calendar-keyed dict of
per-period records (period, effect, se, t_stat, p_value, conf_int, is_significant, significance_stars, reference period omitted;is_significantis evaluated at the fit's
alpha, where the removed record hard-coded 0.05; duplicate labels raise). Relative-time
aggregate("event_study")surfaces do not carry it (AttributeError, no warning). The
successor isto_dataframe()and the columnar arrays. -
The 3.x "default reference_period has changed"
FutureWarningis no longer emitted
(M-007): the event-study mode defaults to the last pre-period silently. -
simulate_powerand friends read the flatatt/se/p_value/conf_intnames from
unregistered custom estimators' results and raiseTypeErrornaming_EstimatorProfile
otherwise (theavg_*fallback left with the removed container; passresult_extractor=
for other shapes). -
4.0 cut, storage flip of the headline quintet (
M-050..M-058).CallawaySantAnnaResults(andDMLDiDResults),SunAbrahamResults,ImputationDiDResults,TwoStageDiDResults,StackedDiDResults,EfficientDiDResults,WooldridgeDiDResults,ChaisemartinDHaultfoeuilleResultsandContinuousDiDResultsnow store the canonicalatt/se/t_stat/p_value/conf_intas their native dataclass fields (renamed in slot, so the positional constructor layout is unchanged) and take those names as constructor keywords (overall_att=raisesTypeError). The 3.xoverall_att/overall_se/overall_t_stat/overall_p_value/overall_conf_intnames - and onContinuousDiDResultsall nine legacy spellings, theoverall_att_*fields and the former silentoverall_se-style aliases - are removed at 4.0 (item 9b retired theFutureWarningproperty shims this item first shipped - reading one raisesAttributeError); they are absent fromdataclasses.fields(),asdict()andto_dict(). 3.x pickles load unchanged (each container's__setstate__rewrites the legacy keys on load). The bootstrap draw containers (bootstrap_results.overall_att_seand siblings) are unchanged;StaggeredTripleDiffResultskept itsoverall_*storage at this step and is removed by the DDD container unification bullet below. -
Report schemas.
DiagnosticReport.to_dict()reportsheadline_metric.nameandtarget_parameter.headline_attributeas"att"for the nine containers above ("overall_att"under 3.x);DIAGNOSTIC_REPORT_SCHEMA_VERSION2.0 -> 3.0.BusinessReport.to_dict()reportstarget_parameter.headline_attributelikewise;BUSINESS_REPORT_SCHEMA_VERSION3.0 -> 4.0 (itsheadlineblock carries no name key).StaggeredTripleDiffResultskeptheadline_attribute: "overall_att"at this step (removed by the DDD unification below). -
4.0 cut, DDD container unification (
M-014,M-140,M-141).TripleDifference.fit()returns ONETripleDifferenceResultsfor both designs:results.designis"2x2x2"or"staggered", the canonicalatt/se/t_stat/p_value/conf_intare native in both, the 2x2x2 positional layout is unchanged, and the staggered structure (group_time_effects,groups,time_periods, unit counts,control_group,base_period,anticipation,bootstrap_results, ...) is appended with defaults (group_time_effectsis{}in the 2x2x2 design). The 3.xStaggeredTripleDiffResultsclass, its module, itsoverall_*fields andto_dict()keys, and its fit-timeoverall_att_es/overall_se_es/overall_t_stat_es/overall_p_value_es/overall_conf_int_esfields are REMOVED (3.x staggered pickles cannot load; 3.x 2x2x2 pickles load unchanged). The fit-timeaggregate=/balance_e=keywords ofTripleDifference.fit()are REMOVED (TypeError): aggregation is post-fit -results.aggregate('event_study')(EventStudyResults,balance_e=),results.aggregate('group')(AggregationResult) andresults.aggregate('simple')(a one-row relay of the stored headline, both designs); bootstrapped fits REPLAY the fit-time multiplier bootstrap so sup-t bands and percentile inference match a 3.x fit-time aggregation; the 2x2x2 design fails the staggered levels closed. The paper's Eq. 4.14 overall isresults.aggregate('simple', weights='event_time')(acceptsbalance_e=; both 3.x warnings preserved).summary()no longer prints the event-study / cohort / Eq. 4.14 blocks;to_dataframe('event_study' | 'group')raise with the post-fit pointer;to_dataframe(level=None)defaults to'group_time'in the staggered design and'overall'in 2x2x2 (whose one-row frame gainsdesign,pscore_trim,alpha,df_inference). New fielddf_inferencerecords the 2x2x2 engine's t df.plot_group_effectsand its siblings raiseValueErroron a 2x2x2 fit (no cells). -
Report schemas. For staggered DDD fits
DiagnosticReport.to_dict()['estimator']andBusinessReport.to_dict()['estimator']['class_name']readTripleDifferenceResults, andtarget_parameter.headline_attributereads"att"(the last"overall_att"). Both schemas gain an additive key - DR a top-leveldesign, BRestimator.design-"2x2x2"/"staggered"forTripleDifferenceResults,Noneotherwise. No version bump:DIAGNOSTIC_REPORT_SCHEMA_VERSION3.0 andBUSINESS_REPORT_SCHEMA_VERSION4.0 had not shipped (folded into the PR 5b bump).power.py's staggered-boundary guard no longer keys onaggregate/balance_e(anestimator_kwargscarrying them surfaces asfit()'sTypeError). -
4.0 cut,
df_conventiondefault flip (M-004,M-005,M-006,M-128,M-129,M-130,M-131). The analytical tail-df convention for CLUSTERED fits defaults to"cluster"(the Stata/fixestt(G - 1)reference) instead of"residual"(the fitted residual dft(n - K_full)) onDifferenceInDifferences,TwoWayFixedEffects,LinearRegression,SunAbraham,WooldridgeDiD(OLS arms),StackedDiDandImputationDiD(pretrends leads);LPDiDalready defaulted to"cluster"at 3.9. What moves: p-values and confidence intervals on clustered analytical fits -TwoWayFixedEffects(static, and event-study withunit=),SunAbraham,WooldridgeDiDOLS andStackedDiDcluster atunitby default, so their default fits move;ImputationDiDmoves on thepretrends=Truelead rows;DifferenceInDifferences/LinearRegressionmove only with an explicitcluster=/cluster_ids=; the analytical lanes inside bootstrapped fits (WooldridgeDiDper-cell inference and post-fitaggregate(),ImputationDiDbootstrapped leads) move too, as do the fake-timing / fake-group placebo diagnostics and the power simulators that relay those p-values. What does not: point estimates, standard errors and t-statistics; survey-design fits (survey df keeps precedence);vcov_type="hc2_bm"(Bell-McCaffrey DOF keep precedence); bootstrap percentile p-values / CIs; unclustered and Conley fits; the BJS overall/post inference ofImputationDiD;WooldridgeDiDlogit/poisson arms; permutation and leave-one-out diagnostics.df_convention="residual"restores the 3.x inference bit-for-bit; on the surfaces where the knob is a documented no-op (non-pretrendsImputationDiD,WooldridgeDiDlogit/poisson) the inert-configUserWarningnow fires for an explicit"residual"rather than for an explicit"cluster".SyntheticDiDinherits the flipped attribute fromDifferenceInDifferenceswithout reading it (no behaviour change). -
SpilloverDiDauto-clusters atunitwhencluster=is omitted ([M-080], item 7b of the 4.0 cut), mirroringTwoWayFixedEffects' carve-outs (explicitcluster=wins; the auto-cluster is dropped on the Conley path and never injected as a survey PSU). Its default standard errors, p-values and CIs move;cluster=Falsereproduces the 3.x observation-level HC1 numbers. Results reportcluster_name="unit"/n_clusters. Inference stayed on the residual df at this step; thedf_conventionknob (next bullet, [M-154]) moves it tot(G - 1)in the same release. -
cluster=Falseis the uniform explicit off-switch ([M-080]): the plain heteroskedasticity-robust sandwich with residual-df t inference onTwoWayFixedEffects(static + event-study; one-way CR2 Bell-McCaffrey underhc2_bm),SunAbraham,WooldridgeDiD(OLS within-transform lane with the absorbed-FE full-K rescale; logit/poisson plain QMLE sandwich) andLPDiD(per-horizon HC1;if_robustunclustered IF variance on the regression-adjustment path; implicit per-observation PSUs under a survey design); an accepted bit-identical no-op on the unit-level-by-construction estimators (CallawaySantAnnaincl. its RC auto-PSU path,EfficientDiDincl.hausman_pretest,DMLDiD,ChaisemartinDHaultfoeuille, staggeredTripleDifference,LWDiDincl. the sensitivity helpers,HeterogeneousAdoptionDiD,ImputationDiD,TwoStageDiD) and on the 2x2DifferenceInDifferences/ 2x2x2TripleDifferencesurfaces; still rejected onStackedDiD. Rejected withinference="wild_bootstrap"(DiD/TWFE),WooldridgeDiD(n_bootstrap > 0)andvcov_type="hc2_bm"onSunAbraham/WooldridgeDiD(cluster-only Bell-McCaffrey DOF; tracked in TODO.md). -
clusteracceptsstr | None | Falseonly ([M-155],diff_diff.utils.validate_cluster):Trueand non-strcolumn labels raise one sharedValueErrorat construction, throughset_paramsand atfit()on every estimator exposing the parameter (DMLDiD's existing rule generalized; integer-labelled cluster columns must be renamed). -
Cluster metadata:
DifferenceInDifferences/TwoWayFixedEffectsreportn_clustersfrom the regression's effective cluster count on non-survey analytical fits (previouslyNoneunless the wild bootstrap ran);SunAbrahamResultsgains an appendedn_clustersfield and reportscluster_nameon clustered analytical fits (previouslyNoneoff the Conley path), and itssummary()renders the variance-family label on analytical non-survey fits;TwoWayFixedEffectsunder a survey design with no explicitcluster=reportscluster_name=None(the auto-cluster is never injected as a PSU). -
SpilloverDiDgains the three-valuedf_conventionknob with the 4.0 default"cluster"([M-154], item 7c of the 4.0 cut): p-values and confidence intervals on its clustered lanes - the unit auto-cluster and an explicitcluster=- now use the Stata/fixestt(G - 1)reference like every other auto-clustered default, withGthe effective cluster count reported asn_clusters. Point estimates, standard errors and t-statistics never move.df_convention="residual"restores the 3.x residual-df inference bit-for-bit;"normal"is deliberate z."cluster"falls back to the residual df oncluster=Falseand on everyvcov_type="conley"fit, an explicit product-kernelcluster=included ("normal"still selects z there); asurvey_designdf keeps precedence under every value; a saturated stage-2 sample stays all-NaN under every value.SpilloverDiDResultsreportsdf_conventionandinference_df(also into_dict()). -
Event-study reference rows are marked explicitly ([M-093], item 8a of the 4.0 cut): every native
event_study_effectsdict that materializes its normalization row (StackedDiD,ImputationDiD,TwoStageDiD,SpilloverDiD, theCallawaySantAnna/DMLDiDuniversal-base aggregation carrier) now carries"is_reference": Trueon that row and NO count key there - the 3.xn_obs == 0/n_groups == 0sentinel is retired, and every remaining zero count is a genuine count (a non-estimable horizon).SpilloverDiD's event-studyatt_dynamic/spillover_effectsframes gain a booleanis_referencecolumn withn_obs = NaNon the reference rows (the column is float;to_dict()records follow). The nativeto_dataframe(level="event_study")frames ofStackedDiD/ bootstrappedImputationDiD/TwoStageDiDrendered the reference row'sn_obsas NaN (float column) at this step; the 8b schema rewire (next bullet) replaces those frames.plot_event_study,plot_honest_event_study, HonestDiD, PreTrendsPower andDiagnosticReportlocate the reference by the key on their raw-dict routes - dispatch, admission, rendering and exception types are unchanged.TwoStageDiD.summary()now prints its genuine all-filtered horizons as NaN rows instead of a mislabeled[ref: h]line. 3.x pickles are migrated on load.SunAbraham(omitted row,reference_period) andEfficientDiD(PT-Post anchor, an estimated cell named byreference_period) are unchanged. -
Every results class's
to_dataframe(level="event_study")now emits the unifiedEVENT_STUDY_SCHEMA([M-093], item 8b of the 4.0 cut):event_time, att, se, t_stat, p_value, conf_int_lower, conf_int_upper, cband_lower, cband_upper, n, df, is_reference, estimand- the frameresults.aggregate("event_study").to_dataframe()returns (CallawaySantAnna/DMLDiD,EfficientDiD,ImputationDiD,TwoStageDiD,StackedDiD,ContinuousDiD,TripleDifferencestaggered,ChaisemartinDHaultfoeuille, HAD event-study results,DurationDiD,LWDiD) or the builder frame (SunAbraham,LPDiD,WooldridgeDiD,SpilloverDiD). The 3.x bespoke frames are gone:relative_period->event_time;effect/att_glob/ LPDiD'scoefficient->att;conf_int_lo/hi,conf_low/high, HAD'sconf_int_low/high->conf_int_lower/conf_int_upper;n_obs->n(NaN on theis_referencerow); dCDH'shorizon->event_timeon the paper'sl1_first_switchconvention, its per-horizonestimandlabels (DID^pl_l/ref/DID_l) are not carried (estimandis"att") and its reference row is markedis_referenceinstead ofn_obs 0.WooldridgeDiDandLWDiDappend their fit-configuration columns after the schema (cohort_trends,aggregation_weights;rolling,estimation_method,vcov_type), andWooldridgeDiD.to_dataframe(level="event_study")(its default level) now RAISES beforeaggregate("event_study", weights=)instead of returning an empty frame.CallawaySantAnna,EfficientDiD,TripleDifferenceand analyticalImputationDiD/TwoStageDiD/ContinuousDiDno longer raise "not stored on 4.0 fits" - the level recomputes the surface from the retained kit (bootstrappedCallawaySantAnna/EfficientDiD/TripleDifferencefits replay the fit-time bootstrap on every call; anImputationDiDreplicate-weight survey fit withpretrends=TrueraisesNotImplementedErrorasaggregate()does); a kit-less pre-3.9 pickle, or a bootstrapped pickle predating the replay state, that still carries a stored dict now raises where it returned a frame.level="group"on those three classes still raises with the post-fit pointer. New levels:SpilloverDiDResults.to_dataframe(level="simple" | "event_study")(default"simple", the former one-row frame; aggregate-mode fits raise on"event_study"),HeterogeneousAdoptionDiDEventStudyResults.to_dataframe(level="event_study"),DurationDiDResults.to_dataframe(level="event_study"). AnLPDiDfit whose stored event-study frame is empty raises instead of returning the empty frame. -
4.0 cut, item 9b: the compatibility shims scheduled for a 5.0 removal are removed at 4.0 (maintainer
decision - no 4.x debt; none of theseFutureWarningshims ever reached a release, so the released
3.x names break without a warned release -docs/migration-4.0.mdis the notice). The 3.x
overall_att/overall_se/overall_t_stat/overall_p_value/overall_conf_intnames on
CallawaySantAnnaResults(andDMLDiDResults),SunAbrahamResults,ImputationDiDResults,
TwoStageDiDResults,StackedDiDResults,EfficientDiDResults,WooldridgeDiDResults,
ChaisemartinDHaultfoeuilleResultsand - all nine spellings,overall_att_*included -
ContinuousDiDResultsraiseAttributeError; read the canonicalatt/se/t_stat/
p_value/conf_int(M-050..M-058). 3.x pickles still load (the__setstate__key migration
stays).EventStudyResults.period_effectsis gone; readto_dataframe()or the columnar arrays
(M-016).LWDiDResults.pvalue/.ciare gone; read.p_value/.conf_int(M-151,M-152). -
SyntheticControl(inner_max_iter=, inner_min_decrease=)are removed (M-148,M-149; the
DeprecationWarningshipped in 3.13.0): passing either is aTypeError, and the positional
parameters after them (standardize,alpha,seed,v_cv_t0) move two slots left - pass
them by keyword. -
run_placebo_test/run_all_placebo_tests:timeis the calendar column only and
post_periodsis required for everytest_type(M-156,M-157; spec section 8 rule 1). The
0/1 post dummy the permutation and leave-one-out tests need is derived as
data[time].isin(post_periods); a missingtimecolumn, missing/emptypost_periods, missing
values intime,post_periodsvalues absent from the column, or a pre-existing_placebo_post
column raiseValueError(on the suite wrapper before any test runs). Calls that passed the 0/1
indicator astime=withpost_periods=[1]return identical numbers; calls that omitted
post_periodsmust add it.
Added
- Python 3.15 support:
requires-pythonnow allows 3.15 (<3.16) and
the 3.15 classifier is declared; release wheels are built for CPython 3.15
on Linux (x86_64, aarch64), macOS arm64 and Windows, and CI runs the
Rust-backend matrix and the pure-Python fallback on 3.15. The Rust
extension builds against PyO3 0.30, which supports 3.15.0 final. LWDiDResults.summary(alpha=None)/print_summary(alpha=None)join the family-wide
summary-alpha guard (ledger rowM-146): a value other than the fit-timealpharaises
ValueErrorinstead of being ignored; the methods previously took noalphaat all.LWDiDResults.to_dataframe(level=...)/to_csv(path, level=...)gain the uniform
levelselector ("simple"default,"group","event_study") - see Breaking Changes.DurationDiDResults.aggregate("simple"): a verbatim relay of the stored headline
quintet (n = n_units,dfNaN - inference is bootstrap-only), joining the spec section 6
simplecontract every aggregation adopter honours.RDDensityTestResult.to_dict(): JSON-serializable scalars with native casts (NaN
inference passes through), the*_conventionalblockNoneunlessreport_all=True, and
the bandwidth-selection table nested underbandwidthsonly when it was estimated.
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 ofsummary(). Useaggregate(...)/
to_dataframe(level=...);anticipationis still reported on the results object.
to_dataframe(level=)now rejects an unknown level withValueErrorinstead of returning an
empty frame.practitioner_next_stepsno longer recognises duck-typed results that carry only the legacy
estimatorattribute;ChangesInChangesResults.methodis the routing field.- Runtime messages, the four
llms*.txtguides, the API docs, the migration guide and the
benchmarks no longer steer users to the fit-time route on these five estimators;
CallawaySantAnna's class docstring,plot_event_study,honest_did/pretrendsand the
results-class "not stored" messages point atresults.aggregate('event_study'). - Bootstrapped
ImputationDiD/TwoStageDiD/ContinuousDiDfits relay their event-study
(and, on Imputation/TwoStage, group) surface. The deprecated keyword was the only route to
bootstrap event-study inference there (no per-draw state is retained for a replay), so a fit
withn_bootstrap > 0now ALWAYS computes those surfaces at fit - exactly what
fit(aggregate="all")computed,balance_e=None- stores them in the legacy
event_study_effects/group_effectsfields, andaggregate('event_study')/
aggregate('group')RELAYS them (kit-snapshot alpha / anticipation / survey metadata; NaN
dfcolumn;vcovNone) instead of raisingNotImplementedError. Analytic fits are
unchanged (legacy fieldsNone, kit recompute on demand). Storage rule: the legacy fields
hold a surface IFF the fit carries bootstrap inference. On Imputation/TwoStage a bootstrapped
fit that ends analytical (zero valid draws, total bootstrap failure) clears them and
aggregate()recomputes analytically. ContinuousDiD's bootstrapped kit is scalars-only, so
no analytical fallback exists there: a bootstrapped fit with no post-treatment cells never
runs its bootstrap (overall ATT already NaN with aUserWarning), stores no surface, and
aggregate('event_study')raises the absent-surfaceValueError.- Materialization is FAIL-SOFT with a one-shot overall-only retry: an exception while
building the surfaces, or a bootstrap attempt that raises / returnsNonewith surfaces
present, leaves themNone, emits oneUserWarning("... could not be materialized ...")
and re-runs the bootstrap overall-only (a seeded fit reproduces the first attempt's weight
draws), so a plain bootstrapped fit's headline quintet and its success/abort outcome are
unchanged by the removal. ContinuousDiD bootstrap failures still propagate as before. - Consequences on bootstrapped plain fits: the aggregators' own
UserWarnings (ImputationDiD
Prop-5 unidentified horizons, empty horizons, LOO single-unit auxiliaries; TwoStageDiD
Stage-1/Stage-2 singular or rank-deficient fallbacks) and the per-target bootstrap-stats
RuntimeWarnings now fire where a plain fit never emitted them;summary()prints the
event-study/group tables andto_dataframe(level="event_study"|"group")returns frames
instead of raising;DiagnosticReport/BusinessReportrun heterogeneity (Imputation /
TwoStage on the relayed group dict, ContinuousDiD on the event study) and parallel trends
(Imputation / TwoStage) on the relayed surface instead of skipping with
NotImplementedError;plot_event_study(results)plots them natively. Measured cost (199
draws, 300 units x 12 periods): Imputation 0.01s -> 0.08s, TwoStage 0.01s -> 0.04s,
Continuous unchanged at 0.05s; all three default ton_bootstrap=0.
- Materialization is FAIL-SOFT with a one-shot overall-only retry: an exception while
ImputationDiD's inert-df_conventionwarning predicate is now exact: the per-lead inference
is reachable iffpretrends=Trueand the design is not replicate-weighted (M-127); the
unreachable pretrends+replicate fit-time gate is deleted (the combination is still rejected
post-fit byaggregate('event_study')).- Runtime messages (
plot_event_study, the results-class "not stored" hints,
_reporting_helpers,DiagnosticReportdocstrings),practitioner_next_stepsadvice for
bootstrapped ImputationDiD / TwoStageDiD / ContinuousDiD fits, thellms*.txtguides, the
ContinuousDiD API page and tutorial, the migration guide and the performance docs describe
the relay instead of steering users to the removed fit-time route.
Removed
- The 3.9 rename shims are gone (ledger rows flipped to
removed). Old keyword spellings
now raiseTypeError: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*_colspellings onHeterogeneousAdoptionDiD.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) andtrim_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 withValueErroronWooldridgeDiDResults.aggregate(type=),.to_dataframe(level=)and
LPDiDResults.to_dataframe(level=)- use"event_study"(M-086,M-150,M-136). robust=removed fromDifferenceInDifferences(and soTwoWayFixedEffects,
MultiPeriodDiD),TripleDifference,HeterogeneousAdoptionDiDandLinearRegression
(M-045..M-047,M-115);vcov_type=is the single selector. The legacy-CR1 remap that
turnedrobust=False+ clustering intohc1went 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 inertlambda_reg/zeta(M-001,M-002,
constructor slots 7-8) are removed too, shiftingvcov_typeand theconley_*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_controlandStackedDiD.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_didanddiff_diff.chaisemartin_dhaultfoeuille
now name the submodules (asdiff_diff.baconalways 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. - The fit-time
aggregate=/balance_e=keywords are gone on the estimators whose post-fit
route already existed (ledger rows flipped toremoved).fit(aggregate=)is removed from
CallawaySantAnna(M-020),EfficientDiD(M-023),StackedDiD(M-024),
ChaisemartinDHaultfoeuille(M-026) andHeterogeneousAdoptionDiD(M-027),
fit(balance_e=)fromCallawaySantAnna(M-117) andEfficientDiD(M-120), and
aggregate=fromdid_had_pretest_workflow(M-139; theHADPretestReport.aggregate
output field survives). The successor is post-fitresults.aggregate(type, balance_e=); HAD
selects its mode from the panel shape. With the keywords go StackedDiD's fit-time
group/all/unknown-valueValueErrors (aggregate('group')already fails closed in the
mixin) and the private_VALID_AGGREGATESsets inhad.py/had_pretests.py
(_infer_aggregate_modeis the sole selector).ImputationDiD,TwoStageDiDand
ContinuousDiDlost their keywords in the same release once the bootstrapped-fit relay
landed (the next bullet but one). - CallawaySantAnna and EfficientDiD compute no event-study/group surface at fit. Their
legacyevent_study_effects/group_effectsfields - and CS'scband_crit_value,
event_study_vcov,event_study_vcov_index,event_study_df- areNoneafter EVERY fit;
the post-fit replay (aggregate('event_study'|'group', balance_e=)) is the only bootstrap
route and is unchanged. - The last fit-time
aggregate=/balance_e=keywords are gone (ledger rows flipped to
removed).fit(aggregate=)is removed fromImputationDiD(M-021),TwoStageDiD
(M-022) andContinuousDiD(M-025),fit(balance_e=)fromImputationDiD(M-118)
andTwoStageDiD(M-119). With PR 3b's removals on CallawaySantAnna / EfficientDiD /
StackedDiD / dCDH / HAD this completes the aggregate-postfit group: every adopter aggregates
through post-fitresults.aggregate(type, balance_e=)only;TripleDifference's
staggered-mode keywords (M-140/M-141) were the last fit-timeaggregate=at this step
and are removed in the same release by the DDD container unification (Breaking Changes
above). With the keywords go ContinuousDiD's
no-underscore"eventstudy"spelling and its fit-time value validation, the dead analytic
fit-time event-study inference arm incontinuous_did.py, and ImputationDiD's unreachable
pretrends+replicate fit-time gate. - The
StaggeredTripleDifferenceclass and itsSDDDalias are gone (M-013,M-064; 4.0
cut, PR 4a).from diff_diff import StaggeredTripleDifferenceandSDDDraiseImportError,
and thediff_diff.staggered_triple_diffmodule no longer exists. The staggered DDD estimator
isTripleDifference().fit(..., unit=, time=, first_treat=, partition=)- the same engine
(diff_diff/_staggered_triple_diff_engine.py), pinned against the committed oracle in
tests/test_v4_merge_ddd.py. R's compactcontrol_groupspellings
("notyettreated"/"nevertreated") died with the class; the merged surface takes
"not_yet_treated"/"never_treated". - The
QDiDclass and theQDiDResultsalias are gone (M-015,M-061). The quantile-DiD
comparison estimator isChangesInChanges(method="qdid")(same engine, identical numbers);
the results container isChangesInChangesResultsfor both methods. - The two
pyproject.tomlpytestfilterwarningsentries that silenced the removed classes'
constructionFutureWarnings. - The
MultiPeriodDiDclass, itsMultiPeriodDiDResultscontainer, thePeriodEffect
record and theEventStudyalias are gone (M-010,M-011,M-012,M-060; 4.0 cut,
PR 4c-ii).from diff_diff import MultiPeriodDiD(and the other three names) raises
ImportError. The estimator isTwoWayFixedEffects().fit(..., event_study=True, spec=, post_periods=, reference_period=), which returns the unifiedEventStudyResultssurface
natively:spec="pooled"is the removed class's design (bit-exact withoutunit=; with
unit=the pooled spec auto-clusters at unit),spec="within"is itsabsorb=["unit"]
design;post_periods=is required (the midpoint default died with the class) and the
post-period averageavg_atthas no successor field. Pinned field for field against the
pre-refactor capturetests/fixtures/v4_twfe_event_study_oracles.json. - The
pyproject.tomlpytestfilterwarningsentry that silenced the removed class's
constructionFutureWarning, theMultiPeriodDiDpower-simulation profile
(simulate_powerhas no event-study profile yet; TODO row), and theMultiPeriodDiDResults
branches of DiagnosticReport / BusinessReport /practitioner_next_steps(both reports still
rejectEventStudyResultsby design; the admission TODO row records where the logic lived).
Fixed
-
DiagnosticReportkept every event-study horizon on fractional calendars. The parallel-trends and heterogeneity collectors keyed the derivedaggregate('event_study')surface (and compared rawevent_study_effectskeys) throughint(k), so on a half-step calendar (e = t - gin-1.5, -1.0, -0.5, ...)-1.5merged into-1and-0.5into the treatment horizon0: pre-periods silently left the PT family and a violated horizon could read as a clean verdict. Integer-valued horizons keep theirintkeys; fractional horizons stayfloat, compare at their real value against the anticipation-aware boundary, and align with the covariance index for the joint Wald. Affected every post-fit event-study producer routed through the report (CallawaySantAnna included), not only the staggered DDD route this release enables. -
run_all_placebo_testsandrun_placebo_test(test_type="permutation" | "leave_one_out")no
longer fail on a multi-period calendar column: through 3.x the wrappers forwardedtimeitself as
the 0/1 post dummy, soDifferenceInDifferences.fitrejected it and the suite silently reported
the two tests as error dicts (tutorial 04's placebo cell printedERRORlines). The dummy is now
derived frompost_periods.
Security
- CI smoke-test lockfile refresh: bumped the transitive
undici(7.29.0 -> 7.30.1)
andsource-map-js(1.2.1 -> 1.2.2) pins in.github/scripts/package-lock.json,
clearing all seven open Dependabot alerts. CI-only (docs search-excerpt smoke test);
nothing in the published wheel is affected.
Documentation
-
4.0-cut framing (the first PR of the 4.0 cut, at v3.13.0):
docs/v4-design.md
re-synced with the library as shipped since 3.9 - the estimator roster now counts
LWDiD/DMLDiD/DurationDiD (27 -> 24 at 4.0, 23 estimators + Bacon in the diagnostic
family), the diagnostic family names the TWFE weights containers and
DurationDiDPretestResults, and section 9 carries the ten-PR cut sequence.M-080
(auto-cluster policy) re-scoped to the measured roster: staticTwoWayFixedEffects
already auto-clusters at unit,SpilloverDiDis the one estimator whose default SE
moves at 4.0,cluster=Falsebecomes the uniform explicit off-switch, and the
by-construction estimators keep no auto-cluster metadata by decision (new "Default
clustering roster" section indocs/methodology/variance-conventions.md; REGISTRY
Notes on SpilloverDiD, StackedDiD, ImputationDiD, TwoStageDiD, DMLDiD, DurationDiD).
M-008(DIFF_DIFF_SOLVE_OLS_FASTPATHdefault) decided: evaluated, kept off. New
ledger rowM-150givesWooldridgeDiDResults.to_dataframe(level="event")its own
locator (twin ofM-136). The migration guide's auto-cluster section, appendix and
orientation table follow the ledger (111 appendix rows). -
Tutorials 02/04/05/07/23 are rebuilt on
TwoWayFixedEffects().fit(..., event_study=True)
(spec="within": unit + period fixed effects, unit-clustered SEs) ahead of the
MultiPeriodDiD removal (M-010, 4.0 cut PR 4c). Point estimates match the former
MultiPeriodDiD fits on these balanced panels; the SEs change because the within spec
clusters by unit. Tutorial 23's naive headline is the equal-weight average of the two
post-period coefficients computed in the cell (ATT unchanged at -4.29; the SE is now the
unit-clustered one), and tutorial 02's period-effects loop readsto_dataframe(). -
The static
TwoWayFixedEffects().fit(..., time=...)calls in tutorials 01/02/09 (the
M-082alias, misused there as the post dummy) are replaced: 01 passespost="post";
02 and 09 reportTwoWayFixedEffects().decompose(...).twfe_estimate, the unit + period
fixed-effects coefficient their Goodman-Bacon sections decompose (the old cells printed
treated x periodartefacts: 0.6309 and 0.0001). Tutorial 02 cell 36's covariate draw is
seeded. Tutorials 02 and 09 are re-executed on the Rust backend, which produced the
committed outputs, so the seeded bootstrap cells are unchanged. -
Tutorials 08 and 16 describe
StaggeredTripleDifferenceas the removed class (4.0 cut,
PR 4a).diff_diff/guides/llms-full.txt's event-study snippet callsprint(es.summary())
(EventStudyResultshas noprint_summary). -
BaconDecompositionis re-homed into the diagnostics family ([M-090], item 9 of the 4.0 cut): it leaves the estimator catalogues - README## Estimators,llms.txt## Estimators, thellms-full.txtEstimators section and its alias line, the API-reference Estimators autosummary and module toctree, and the docs homepage Supported Estimators table - for their Diagnostics groupings.RDPlotmoves from the API-reference Visualization autosummary into Diagnostics;RDPlot,RDDensityTestand the TWFE weight diagnostics leave thellms-full.txtEstimators section. Docs-only: the import path, theBaconalias, the results class and every number are unchanged. -
The docs homepage gains a "Diagnostics & Sensitivity" table (BaconDecomposition, RDPlot, RDDensityTest, HonestDiD, PreTrendsPower, PowerAnalysis, DiagnosticReport, the parallel-trends / placebo / TWFE-weight functions and the HAD pretest workflow) next to the estimator table; the API-reference Estimators and Diagnostics intros describe the split.
-
docs/methodology/REGISTRY.mdarchive framing for the classes removed at 4.0: the MultiPeriodDiD and StaggeredTripleDifference sections and the QDiD class-spelling subsection open with an archive Note, the table of contents marks them (archived) and states the archived-sections convention, the cross-reference tables nameChangesInChanges(method="qdid")instead of a standaloneQDiDestimator, and the "20-estimator count" wording is gone. The migration guide gains the [M-090] bullet.
Testing
tests/test_t23_spillover_tva_drift.py'snaive_fitfixture mirrors the notebook's TWFE
event-study headline cell, runs the fit with warnings escalated to errors, and pins the
post-period-average identity and the unit-clustered vcov-block SE alongside the existing
-4.29 endpoint.- The MultiPeriodDiD-fixture suites (honest_did, pretrends, methodology pretrends/honest,
visualization, Conley, survey, replicate weights, vcov_type, df_convention, the estimators
event-study behaviour pins, the event-study surface roster, the DiagnosticReport /
BusinessReport generic fixtures) now run onTwoWayFixedEffects().fit(..., event_study=True)
(or a static TWFE fit for the report classes); MultiPeriodDiD's own tests, the MPD-only
designs (multi-absorb,fixed_effects=, wild bootstrap, container-field assertions, the
MPD-branch consumer tests) and the MPD-vs-TWFE parity oracles are unchanged until PR 4c-ii.
Newtests/test_twfe_event_study_decoupling.py, shared helpers
tests/helpers/twfe_es_oracle_lanes.py,event_study_average.pyand
event_study_surface_builders.py; the variance-conventions inventory gains the two TWFE
event-study rows; a TODO row tracks the one-way HC2-BM absorbed-FE R-parity lane.
Internal
-
Codex reviewer gpt-6 campaign pre-registration (PR-1 of 2):
tools/reviewer-eval
pre-registers thegpt-5.6-sol->gpt-6-astraevaluation (DECISION_RULE.md:
symmetric regression gate over every ground-truth bug, FP gate, downgrade rule,
verifier-confirmed leaks void a cell and contest the gate, stop conditions; the
2026-07 rule is archived undercampaigns/) and splits the grader-facing text into
GRADING_RUBRIC.md(marker-basedLEAK?flag whose list renders from
reviewer_audit.LEAK_RULES). New engines:reviewer_verdict.py(gates, precedence,
binding to the sealed artifacts, protocol guards, PROVISIONAL/DEFINITIVE) and
reviewer_audit.py(post-unblinding session-log audit, informational and
over-matching by design).run_eval.pygainsauditandverdict, runs every
codex call under an isolated CI-faithfulCODEX_HOME, records seven-key git
provenance per artifact and in the manifest, seals artifacts at run time
(artifacts_sha), binds the blinded bundle (bundle_id,run_ids_sha,
rubric/prompt sha) and refuses post-run stages on a dirty or moved tree;run
holds an exclusive per-subdir lock for the whole attempt, records the OK cells of
aborted attempts too and refuses a rerun that would re-dispatch any cell an
earlier attempt recorded OK; review worktree leaves are salted opaque hashes. Harness CLI re-pinned to
codex-cli 0.162.1 (armE= gpt-6-astra @ xhigh added; CIcodex-versionpinned
to the same, lockstep-tested withDEFAULT_MODEL, the plan-review pin, the
control arm and the prose pins). -
openai_review.py:call_codexparses codex's stderr session events into
session_commands(+ start/exec/other-event counts, truncation flag) on every
exit path and prints a one-line summary;--timeoutnow also caps the codex
backend's wall clock when passed explicitly (previously ignored there),0
means no timeout on both backends and negatives are rejected;PRICING
corrected for gpt-5.6-* and extended with gpt-6-; gpt-6 counts as a
reasoning family; blinding scrubs gpt-6/astra and Unicode-dash spellings. -
Codex reviewer swap gpt-5.6-sol -> gpt-6.1-sol (reviewer-eval 2026-10 campaigns, PR-2):
two blinded, pre-registered 44-review campaigns on the frozentools/reviewer-eval
harness. Campaign 1 (B = gpt-5.6-sol vs E = gpt-6-astra, both @ xhigh, k=2): mechanical
DEFINITIVE NO-GO on one regression (s4-cs-unbalanced-nan-outcome), accepted by the
user; no astra swap (campaigns/2026-10-gpt6-VERDICT.md). Campaign 2 (B vs F =
gpt-6.1-sol @ xhigh, k=2, fresh control sample, same rule re-issued for F):
mechanical DEFINITIVE PARITY with zero rule-defined regressions, zero downgrades,
0 vs 0 false positives, two favourable catch-status changes against one
UNSTABLE-to-MISSED judgment flag, ~15% lower latency and half the input price; the
user accepted PARITY as GO
(campaigns/2026-10-gpt61sol-VERDICT.md). Both verdicts ship with their redacted
grading records (campaigns/2026-10-gpt6*-grading/). Production pins move in lockstep
togpt-6.1-sol: the CI workflowmodel,openai_review.DEFAULT_MODELand the
plan-reviewCODEX_MODEL(carried over from the code-review campaign, not re-run
through plan-review-eval; TODO.md row), with the plan-reviewCODEX_TIMEOUT_Sraised
2400 -> 3600 to equal the harness ceiling. Harness: armFregistered with the contrast
(B, F), campaign 1's rule archived ascampaigns/2026-10-gpt6-DECISION_RULE.md,
run --configsdefaults to the last registered shape,PRICINGgainsgpt-6.1-sol,
arm F takesrole=control(B retained as previous production), the candidate-exposure
gate exempts the control arm so baresmokekeeps working after a swap, and the
DEFERREDgpt-6-solcost-fallback row is retired (codex-cli 0.162.1's ChatGPT-account
login accepts the gpt-6 Sol tier; campaign 2 was that evaluation). -
Dependabot npm coverage: added an
npmecosystem entry for/.github/scripts
so the smoke-test toolchain gets weekly version-update PRs (grouped minor/patch)
instead of drifting into advisories between manual lockfile refreshes. -
Ledger cut window (
docs/v4-deprecations.yamlheadercut_window: "4.0",
cut_base: "3.13.0"; spec section 11): rows scheduled at 4.0 may flip terminal
ahead of the version bump so the removals land over many PRs while__version__
stayed 3.13.0;tests/test_v4_matrix.pyrelaxes only the early-removal guard for the
window version, refuses any other version bump while early-terminal rows exist, and
keeps due enforcement unchanged. The id snapshot andROW_COUNT_FLOORnow include
M-148/M-149(shipped by #838 without extending them) andM-150(132 rows).
M-091's roster records the four later-bornDiagnosticcontainers;M-050's
code_refsrecord thatdiff_diff/_deprecation.pysurvives 4.0 as pickle-migration
infrastructure;M-045/M-115name the legacy-CR1 remap branches their removal
deletes. Nomaint/3.13branch was cut (maintainer decision 2026-10-18); 3.13.x
patches, if ever needed, come from thev3.13.0tag. -
tests/test_v4_matrix.pyremoved-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. Thediff_diff._deprecationhelpers survived this PR for the remaining
M-031/M-082 and PR-3 shims (item 9b later reduced the module to its pickle-migration and
required-arg helpers). -
4.0 cut, PR 3a - tests-first migration to the post-fit aggregation containers. The
test and benchmark suites now read event-study / group surfaces from
results.aggregate("event_study")/results.aggregate("group")(the container API that
has existed since 3.9) instead of the deprecated fit-timefit(aggregate=...)keyword and
the legacyevent_study_effects/group_effectsfields, so the keyword removals in PRs 3b
and 3c land against already-migrated suites. No library source changes and no ledger row
flips in this PR; the fit-time keywords still work and still warn. Bootstrapped
ImputationDiD / TwoStageDiD / ContinuousDiD sites stay on the fit-time route until PR 3c
adds the post-fit relay for them. -
Committed per-backend aggregation oracles.
tests/test_aggregate_contract.pyno longer
uses the fit-time route as its numerical reference: every analytic surface is pinned at
rtol=1e-12(confidence-interval bounds at1e-9: scipy'st.ppfmoved by up to 3.4e-11
relative between 1.13 and 1.14+) and every bootstrapped CS / EfficientDiD replay at the
replay-parity tolerances againsttests/fixtures/v4_aggregate_oracles.json, captured on the unmodified
tree at8724fecdbytests/_capture_v4_aggregate_oracles.pyunder both weight backends
(seetests/fixtures/README.md). Newtests/helpers/legacy_surface.py(es_dict,
group_dict,legacy_standin) gives consumer tests a faithful stand-in for the 3.x
fit-time surface where their subject is the native (legacy-pickle) route. -
Benchmarks (
benchmarks/python/benchmark_callaway.py,benchmark_imputation.py, the
speed_reviewscripts) callaggregate()post-fit inside the timed region and emit the
same JSON row shape the R refresh joins on. -
docs/v4-design.mdsection 9 records the three-way split of cut-sequence item 3 (3a
tests-first, 3b mechanical removals, 3c the Imputation/TwoStage/ContinuousDiD removals +
relay). -
tests/test_aggregate_contract.py: theTest*Shimclasses, fit-time fixtures and
inertness comparisons for the five estimators are retired in favour ofTest*Removal
pins (TypeError, signature, positional-slot binding); the committed oracle fixture is the
numerical reference.tests/_capture_v4_aggregate_oracles.pyis marked historical (it
drives the removed route; runnable only at the recordedcaptured_at_head). Naming-guard
allowlist entries for thellms.txt/llms-full.txtcanonical post-fitaggregate
vocabulary. Ledger rowM-093records that CS/EfficientDiD populate the legacy fields on
no fit.warn_deprecated_kwargloses its last library caller and is retained (spec section
8 rule 11) with a direct unit pin. -
tests/test_aggregate_contract.py: the remainingTest*Shimclasses, fit-time fixtures and
fail-closed bootstrap pins are retired forTest*Removalpins (TypeError, signature,
positional-slot binding) and relay pins - the relayed surfaces equal the stored legacy dicts
row-for-row and match the per-backend bootstrap oracles - plus failure-injection pins for the
fail-soft materialization and the storage rule. The estimator suites,DiagnosticReport,
event_study_surface(bootstrapped Imputation / TwoStage / Continuous enrolled as genuine
native producers) and practitioner tests take their relay pins; the module-level
FutureWarning filters for the fit-time route are gone. Naming-guard allowlist entries for the
guides' post-fitaggregatevocabulary are finalized. Ledger rowM-093gains the fit-side
table of which producers populate the legacy fields;M-127records the exact predicate.
docs/v4-design.mdsection 6 carries the relay amendment and the HYBRIDS paragraph; section 9
marks sub-item 3c shipped. -
The three StaggeredTripleDifference suites (
tests/test_staggered_triple_diff.py,
tests/test_methodology_staggered_triple_diff.py,tests/test_survey_staggered_ddd.py) are
ported toTripleDifference's staggered mode (keyword fits; the R golden's compact
control_groupvalues are mapped), so the engine keeps its behavior, methodology and survey
coverage.tests/test_v4_merge_ddd.py/tests/test_v4_merge_cic.pydrop their old-vs-new
parity lanes forTest*Removalpins; the two capture scripts are marked historical and
make_2x2lives in the CiC merge test. -
Ledger:
M-013/M-015->removed,M-061/M-064/M-085->done;M-031keeps
shimmedwithwarning: null;M-080drops the deleted module from itscode_refs.
Spec: cut-sequence item 4 split into 4a/4b/4c, the cross-row rule gains its alias clause.
docs/doc-deps.yamlre-primes thestaggered_triple_diffgroup on the results module. -
tests/test_v4_matrix.pyno longer requiresStaggeredTripleDifference/QDiDworked-example
owners in the migration guide; the two autosummary stubs are deleted; REGISTRY keeps the
## StaggeredTripleDifferenceand### QDiDsections as the engine's methodology record
(archive framing lands withM-090). -
docs/v4-design.mdcut-sequence item 4b marked shipped; item 4c lists the two remaining
markdown-only MultiPeriodDiD mentions (06 cell 34, 16 cell 40).docs/doc-deps.yamlmaps
the TWFE module to tutorials 01/02/04/05/07/09/23. No ledger row flips. -
The TWFE event-study surface is built directly from the shared core's payload
(4.0 cut, PR 4c-i; ledger rowsM-010/M-011):DifferenceInDifferences._fit_event_study_core
returns a private_EventStudyCorePayload,TwoWayFixedEffects.fit(..., event_study=True)
builds itsEventStudyResultsfrom it viaresults_base._from_event_study_payload, and only
MultiPeriodDiD.fitstill wraps the payload intoMultiPeriodDiDResults._from_mpdand the
payload route share one calendar-surface builder, so they cannot drift; a committed oracle
captured on the pre-refactor tree (tests/fixtures/v4_twfe_event_study_oracles.json, capture
scripttests/_capture_v4_merge_mpd_oracles.py) pins every field of the TWFE surface and of
its MultiPeriodDiD twin across eleven variance lanes. No user-visible behavior changes. -
Spec item 4c is split into 4c-i (this PR) and 4c-ii (the removals); the 4c-ii bullet records
thespec="within"benchmark-arm relabel and theM-016view's record type (user decisions
2026-10-24). LedgerM-010/M-011notes updated; no status flips. -
PR 4c-ii-b (docs/benchmarks half of the MultiPeriodDiD removal): REGISTRY / REPORTING /
survey-theory / METHODOLOGY_REVIEW and the remaining user-doc prose stop describing the
removed class as live; TODO rows 33/35/38/95/99 and three DEFERRED rows that governed the
class are reworded or retired. The oracle fixturetests/fixtures/v4_twfe_event_study_oracles. jsondrops itsmpd_twinrecords and the lane metadata loses its MPD twin fields (the TWFE
records are byte-identical); the capture script documents the per-lane capture SHAs. -
Benchmarks: the live event-study arm is the new
benchmarks/python/benchmark_twfe_event_study.py
(spec="within",cluster="unit"; parity against Rfixest::feols(... | unit)verified
locally - ATT to 2e-12, SE to 0.0%, per-period effects correlation 1.000);
benchmark_multiperiod.pyandbenchmarks/refresh_2026_07/stay frozen because that harness
runs inside pinned 3.5.3/3.7.0 venvs that predateevent_study=True. -
DifferenceInDifferences._fit_event_study_coreserves only the TWFE event-study mode
(post_periodsrequired by the core;include_treatment_main/cluster_override
keyword-required); the private payload is pruned to the fields the surface builder reads.
The clusteredhc2_bmcontrast batch keeps the former post-period-average column as the
batch-relative Bell-McCaffrey DOF guard's anchor so per-period NaN masks stay bit-identical
(REGISTRY Note; TODO row for the guard redesign).build_event_study_surfaceopens with an
isinstance(EventStudyResults)passthrough. -
Ledger:
M-010/M-011/M-012->removed,M-060/M-007/M-083->done,M-016
->shimmedondiff_diff:EventStudyResults.period_effects,M-082keepsshimmedwith
warning: null. Spec item 4c-ii carries the SHIPPED note incl. the three recorded
deviations (anchor column, new live benchmark script, DR/BR branch deletion). -
Tests: the MPD-only tests are deleted or re-fixtured onto
cs_fit/ the TWFE surface
(tests/test_v4_merge_mpd.pycarries the removal,M-016andM-007pins and the anchor
pin); the oracle fixture gains two clusteredhc2_bmlanes captured atd45d5316before
the core pruning (itsmpd_twinrecords are pruned in PR 4c-ii-b); the clustered
per-period CR2 DOF R/clubSandwich pin and the event-study covariate-collision guard are
migrated to TWFE; variance-conventionstwfe_es_*rows re-pinned (tail_df5) and the table
regenerated. The methodology/benchmark prose half and the oracle fixture's
mpd_twinprune follow in PR 4c-ii-b. -
4.0 cut, PR 5a-ii (the documentation half of cut-sequence item 5's reader migration,
split from PR 5a because the combined diff exceeded the AI review input budget): every
documented read of the 3.xoverall_att/overall_se/overall_t_stat/
overall_p_value/overall_conf_intnames (and ContinuousDiD'soverall_att_*
spellings) on the nineM-050..M-058containers - RTD pages, the bundled
llms*.txtguides, tutorial code cells, the tutorial drift tests that mirror them
(tutorial 32's code-cell hash pins re-pinned), benchmark scripts and the carousel
generator - now reads the canonicalatt/se/t_stat/p_value/conf_int
quintet. No user-visible behaviour change (the canonical names already resolve);
committed notebook outputs are unchanged except tutorial 18's echoed guidance string.
Bootstrap draw containers (bootstrap_results.overall_att_se) and R / golden-JSON
interface keys are untouched. -
4.0 cut, PR 5a (the reader half of cut-sequence item 5, the
M-050..M-058storage
flip): every in-repo reader of the 3.xoverall_att/overall_se/overall_t_stat/
overall_p_value/overall_conf_intfield names (and ContinuousDiD'soverall_att_*
spellings) on the nine flipping containers in SOURCE and TESTS - consumers, the
containers' ownsummary()/to_dict()/__repr__/ aggregation bodies, and every
test suite - now reads the canonicalatt/se/
t_stat/p_value/conf_intquintet. No user-visible change: the canonical names
already resolve as properties, numbers are bit-identical and nothing warns yet; the
storage flip itself, theFutureWarningshims and the report-schema changes follow in
PR 5b; the documentation / guides / tutorial / benchmark half of the reader migration
is PR 5a-ii (split off because the combined diff exceeded the AI review input budget).power.py's_extract_staggered(a silentgetattrfallback chain over the old
names) is deleted - the staggered power profiles use_extract_simple; the
practitioner_next_stepsoverall_attfallback is gone. Bootstrap draw containers
(CSBootstrapResults.overall_att_seand siblings) andStaggeredTripleDiffResults
(unified at cut item 6) are untouched. -
Ledger rows
M-050..M-058flip toshimmedwithtests/test_v4_storage_flip.pyas theirtest_ref;diff_diff._deprecationgainsLEGACY_OVERALL_QUINTETandmigrate_renamed_state_keys(the pickle-migration infrastructure);DMLDiDResultsre-declares the five shims so the warning names the subclass (naming-guard allowlist reason reworded);_extract_scalar_headline's legacy alias row is deleted;tests/test_result_aliases.pypins the post-flip direction (SDDD kept in the 3.x direction); the API autosummary stubs are regenerated; the bootstrap draw containers' payload names are recorded as a REGISTRYNote(user decision 2026-10-26); spec item 5 carries its SHIPPED note. -
Ledger rows
M-014,M-140,M-141flip toremovedwithtests/test_v4_unify_ddd.pyas theirtest_ref;diff_diff/staggered_triple_diff_results.pyis renamed todiff_diff/triple_diff_results.py(hosting the unified container, re-exported fromtriple_diff.py); the staggered engine attaches the CallawaySantAnna aggregation kit (staggered.py's_build_aggregation_kiton the eligibility-zeroed precomputed copy, plus theddd_df_survey_originalbookkeeping key for the Eq. 4.14 df rule); report consumers dispatch on the design through_reporting_helpers.results_design/is_staggered_ddd(DR's_DESIGN_APPLICABILITY/_DESIGN_PT_METHOD); the practitioner handler gains a staggered branch;docs/api/_autosummaryregenerated; the spec's section 4.2 / section 6 / item-6 SHIPPED notes and the REGISTRY## TripleDifferencepost-fit Note record the mechanics and theweights='event_time'decision. -
Ledger rows
M-004..M-006andM-128..M-131flip todonewithtests/test_v4_inference_defaults.pyas theirtest_ref; the spec's section 7 / item 7 SHIPPED notes and item 10 now record the deferred bulk re-execution of the nine tutorials whose clustered-default outputs moved (24 re-executed here if its outputs moved); the REGISTRY TwoWayFixedEffects clustered-CR1 inference-df note is relabelled a plain Note (the default now matches fixest) and the SunAbraham / ImputationDiD / StackedDiD / WooldridgeDiD / LPDiD tail-df notes read the 4.0 default;TestDfConvention,test_moderate_t_pins_cluster_df_conventionand the per-estimator knob suites pin the G-1 default against explicit"residual"baselines (every default-vs-"cluster"comparison became default-vs-"residual"so none passes vacuously);test_ci_matches_r_twfetightens to machine precision; the variance-conventions audit matrix keeps its residual-df rows as explicit"residual"arms (the K_full accounting pins) and gains eight G-1_default_dftwins (three in the fast set), withdocs/methodology/variance-conventions.mdregenerated; the committed TWFE event-study oracle lanespooled_cluster_unit/within_cluster_unitand the ImputationDiDpretrends_allcontract lanes name the"residual"convention they were captured under (metadata-only, no oracle number touched); theDEFERRED.mdVersion-gated (v4) row is retired. -
Ledger: M-080 and the new M-155 (shared cluster contract) ->
done; new M-154 (SpilloverDiDdf_convention, item 7c) ->planned; pinned bytests/test_v4_cluster_policy.py. Tutorial 23 section 6 shows the four-row SE table (CR1 at unit, HC1 viacluster=False, Conley lag 0 / lag 1) and was re-executed;tests/test_t23_spillover_tva_drift.pypins the new default and the table's source. -
Ledger: M-154 ->
done, pinned bytests/test_v4_spillover_df_convention.py; the df ladder is the pure helperspillover._resolve_inference_df(survey df -> saturation ->utils.resolve_tail_df). The variance-conventions matrix gains thespillover_cluster_unit/spillover_cluster_unit_default_dfrows; the REGISTRYdf_conventionknob roster,docs/methodology/variance-conventions.md, the migration guide,llms-full.txtanddocs/api/spillover.rstdescribe the knob; tutorial 23 re-validated (it prints no p-values or CIs). -
results_base._from_relative_dictreads only theis_referencekey and the namedreference_period(the zero-count heuristic is gone; a keyed row with a finite nonzero effect raises);_deprecation.migrate_event_study_reference_sentinelis the single place the retired sentinel is still recognized, run by the carriers'__setstate__(SpilloverDiDResultsgained one; aggregate-mode frames untouched). Ledger: M-093 gainstest_ref(tests/test_v4_sentinel_retirement.py) and staysplanneduntil 8b (theto_dataframe(level="event_study")schema rewire on every results class, incl. the dCDH native frame'sestimand 'ref'row). REGISTRY (SpilloverDiD, StackedDiD/ImputationDiD/TwoStageDiD, the plotting Notes), the migration guide,llms-full.txtanddocs/api/spillover.rstdescribe the marking. -
The routing rule is the M-153 LWDiD precedent (
aggregate(level).to_dataframe()on theAggregationMixinclasses,build_event_study_surface(self).to_dataframe()elsewhere);results_base._ABSENT_SURFACE_HINTSgains aSunAbrahamResultsentry. Ledger: M-093 flips todonewithtest_reftests/test_v4_event_study_schema.py(the cross-producer schema-prefix / route-equality / error-contract pins;tests/test_v4_sentinel_retirement.pykeeps the 8a pins) and a tenth amendment note;docs/v4-design.mdsection 5 now states the PREFIX rule and item 8 is complete. REGISTRY (CS, EfficientDiD, Imputation/TwoStage/Stacked relay and reference-row Notes, DDD, HAD, plus new to_dataframe-schema Notes for dCDH, SunAbraham, WooldridgeDiD, LPDiD, DurationDiD and SpilloverDiD), the migration guide,llms-full.txt, the practitioner guide,docs/api/wooldridge_etwfe.rstanddocs/api/spillover.rstdescribe the frame; notebooks 16 (re-executed, outputs refreshed and prose reconciled - it leaves the TODO.md tutorial re-execution row), 19 (readsevent_time/att/is_reference) and 14 (export prose) updated. TODO.md gains thelevel='group'relay row. -
Ledger row M-090 flips to
donewithtests/test_v4_diagnostic_family_docs.pyas itstest_ref; the test pins the estimators/diagnostics split on README,llms.txt,llms-full.txt,docs/index.rst,docs/api/index.rstand the REGISTRY archive framing, and runs in the docs-tests workflow (README-only diffs now trigger that lane).docs/doc-deps.yamlpoints the diagnostic modules at the new homepage table; the/docs-checkclassification table files Bacon as a diagnostics-class entry. -
Ledger rows
M-016,M-050..M-058,M-151,M-152flip toremovedat 4.0 with
deprecated_in: null(no release warned);M-148/M-149flip toremovedwith
deprecated_in: 3.13.0; new behavior rowsM-156/M-157(tests/test_diagnostics.py); the
ledger header, spec ladder, section 5/8/9/11 notes and the phase-5 roster carry dated amendments.
diff_diff._deprecationis reduced to the pickle-migration (LEGACY_OVERALL_QUINTET,
migrate_renamed_state_keys,migrate_event_study_reference_sentinel) and required-arg
(NOT_SUPPLIED,require_arg) helpers -deprecated_field_property,resolve_renamed_kwarg,
warn_deprecated_kwarganddeprecated_kwarg_messageare deleted with their last consumers.
With the descriptors gone, assigning a legacy name to a results instance creates a shadow
attribute instead of raising (the non-slotted dataclasses) - the naming guard's consumer sweep
is the in-repo defense. Naming guard: theDMLDiDResults.overall_attsurface entry and the
_deprecation.pyplacebo_effectsconsumer entry are deleted, the two placebo wrappers join the
rule-1 calendartimeinventory, and the retired token families' remaining in-repo hits
(body-localoverall_attnames, the kept__setstate__key maps, methodology history) are
allowlisted. API autosummary stubs regenerated; TODO.md's placebo-wrapper redesign row closed;
tutorial 17 re-executed (its stored practitioner snippet echoed the oldtime='post'advice).