Skip to content

feat(3.9): tail-df consolidation - three-value df_convention knob + t(residual) defect fix (M-127) - #738

Merged
igerber merged 2 commits into
mainfrom
feat/tail-df-convention
Jul 31, 2026
Merged

feat(3.9): tail-df consolidation - three-value df_convention knob + t(residual) defect fix (M-127)#738
igerber merged 2 commits into
mainfrom
feat/tail-df-convention

Conversation

@igerber

@igerber igerber commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

  • PR C of the 3.9 variance-consolidation program (after feat(3.9): variance-conventions inventory + component-aware absorbed-FE rank (D3) #736 inventory/D3 and feat(3.9): converge clustered CR1 on K_reference - D1+D2 fix (M-126) #737 K_reference/M-126): converge the tail degrees of freedom used by the t-distribution across the analytical clustered surfaces, ledgered as M-127.
  • Defect fix (z -> t(residual); point estimates, SEs, and t-stats unchanged) on the four normal-theory surfaces: WooldridgeDiD default-hc1 OLS arms, SunAbraham aggregates (cells and aggregates now share ONE df source per fit - the D4 two-conventions-in-one-fit defect is gone), StackedDiD pooled inference, and ImputationDiD pretrends per-lead inference (the joint pretrend Wald F keeps its cluster-robust F(q, G-1) denominator, documented as a pairing).
  • Three-value df_convention knob {"residual", "cluster", "normal"}: widened on DiD/TWFE/MultiPeriodDiD/LinearRegression ("normal" is ungated at the fallback level - clustered, unclustered, and conley) and NEW on SunAbraham, WooldridgeDiD (OLS arms), StackedDiD, ImputationDiD (pretrends), and LPDiD. "normal" reproduces the pre-3.9 numbers on the fixed surfaces; "cluster" gives the fixest/Stata t(G-1) reference.
  • LPDiD defaults to "cluster" (its existing Stata t(G-1) convention - zero movement, bit-identity locked; degenerate lanes keep literal df=None under every value).
  • Survey/replicate df and hc2_bm Bell-McCaffrey DOF keep precedence under every knob value (test-pinned per estimator). set_params is atomic under unknown keys on all five standalone estimators.
  • Provenance: df_convention on five results containers; inference_df on SunAbraham/StackedDiD (None under bootstrap overrides); Wooldridge _df_one_way renamed _df_analytic_fallback with a __setstate__ pickle migration.
  • Ledger/docs: M-127 behavior row + M-128..M-131 default-flip rows (phase 5, 'residual' -> 'cluster'), matrix snapshot 105 -> 110, v4-design/DEFERRED/TODO/CHANGELOG sync, REGISTRY tail-df notes for TWFE/SA/StackedDiD/Wooldridge/ImputationDiD/LPDiD, regenerated variance-conventions inventory (defect rows converged), llms-full updates.

Methodology references (required if estimator / math changes)

  • Method name(s): tail degrees of freedom for cluster-robust analytical inference (CR1 sandwich t-reference) across SunAbraham, Wooldridge ETWFE (OLS arms), StackedDiD, ImputationDiD (BJS pretrends leads), LPDiD, and the DiD/TWFE/MPD/LinearRegression core.
  • Paper / source link(s): Sun & Abraham (2021, J. Econometrics); Wooldridge (2021/2025 ETWFE); Wing, Freedman & Hollingsworth (2024, NBER w32054); Borusyak, Jaravel & Spiess (2024, REStud); Dube, Girardi, Jorda & Taylor (2023/2025 LP-DiD); R fixest and Stata (reghdfe/jwdid/lpdid) as t(G-1) reference implementations.
  • Any intentional deviations from the source (and why): the 3.9 default uses t(residual df) where fixest/Stata use t(G-1) at all levels - documented in docs/methodology/REGISTRY.md (TWFE deviation note + per-estimator tail-df notes) with df_convention="cluster" available for reference parity now and the default flip version-gated to 4.0 (M-004..M-006 + M-128..M-131). The LPDiD RA-lane "residual" df (n_total - k0_kept - 1) is a library convention with no external anchor (Stata teffects ra reports z), stated in the LPDiD REGISTRY note.

Validation

  • Tests added/updated: tests/test_utils.py (resolver branch table), tests/test_variance_conventions.py (audit-matrix re-pins with measured df multisets + SA-cells spy arm; regenerated byte-gated inventory), tests/test_sun_abraham.py, tests/test_wooldridge.py, tests/test_stacked_did.py, tests/test_imputation.py, tests/test_lpdid.py (per-estimator df-convention suites incl. survey-df precedence under every knob value and atomic set_params), tests/test_estimators_vcov_type.py ("normal" on clustered/unclustered/conley), tests/test_fixest_did_twfe_parity.py (golden p/CI now gated: iid arms under the residual default, cluster arms under df_convention="cluster"), tests/test_event_study_surface.py (provenance flips), tests/test_v4_matrix.py (snapshot 110).
  • Backtest / simulation / notebook evidence (if applicable): fixest committed-golden parity - relative p residuals ~1e-15..1e-12 (iid arms, residual default) and ~1e-12..5e-10 (cluster arms, "cluster"); LPDiD audit rows and all existing goldens bit-identical under its "cluster" default.

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

…(residual) defect fix (M-127)

Converge the tail degrees of freedom the t-distribution uses across the
analytical clustered surfaces (PR C of the variance-consolidation
program, after #736 inventory/D3 and #737 K_reference/M-126).

Defect fix (z -> t(residual), point estimates/SEs/t-stats unchanged):
- Wooldridge default-hc1 OLS arms: one knob-resolved df_fallback feeds
  every non-BM arm (survey df first; classical/hc2 reproduce the
  historical df_one_way values bit-for-bit under the default).
- SunAbraham aggregates: cells and aggregates now share ONE df source
  per fit (the saturated regression's df_) - the D4 two-conventions-in-
  one-fit defect is gone.
- StackedDiD pooled inference: positive-weight n_eff - k_kept.
- ImputationDiD pretrends per-lead inference: n - k_kept - absorbed
  rank on the untreated subsample (the joint pretrend Wald F keeps its
  cluster-robust F(q, G-1) denominator, documented as a pairing).

Three-value df_convention knob {"residual","cluster","normal"} on
DiD/TWFE/MPD/LinearRegression (widened from two values; "normal" is
ungated at the fallback level - clustered, unclustered, and conley) and
NEW on SunAbraham, WooldridgeDiD (OLS arms), StackedDiD, ImputationDiD
(pretrends), and LPDiD. LPDiD defaults to "cluster" (the existing Stata
t(G-1) reference - zero movement, degenerate lanes keep literal
df=None). "normal" reproduces the pre-3.9 numbers on the fixed
surfaces. Survey/replicate df and hc2_bm BM DOF keep precedence under
every value. set_params is atomic under unknown keys on all five
standalone estimators. Shared resolver: utils.resolve_tail_df +
validate_df_convention.

Provenance: df_convention on five results containers; inference_df on
SunAbraham/StackedDiD (None under bootstrap overrides); Wooldridge
_df_one_way renamed _df_analytic_fallback with a __setstate__ pickle
migration (stays live on bootstrap fits so post-fit analytical
aggregate() reproduces fit-time inference).

Ledger/docs: M-127 behavior row + M-128..M-131 default-flip rows
(phase 5, 'residual' -> 'cluster'), matrix snapshot 105 -> 110,
v4-design/DEFERRED/TODO/CHANGELOG sync; REGISTRY tail-df notes for
TWFE/SA/StackedDiD/Wooldridge/ImputationDiD/LPDiD; regenerated
variance-conventions inventory (defect rows converged); llms-full
constructor/results updates.

Tests: resolver unit branch table; per-estimator df-convention suites
(cluster == hand-reconstructed t(G-1), normal == z, default != old z,
survey-df precedence under every knob value, bit-identity locks);
audit-matrix re-pins with measured df multisets; fixest golden p/CI now
gated on both lanes (iid arms under the residual default, cluster arms
under df_convention="cluster"); event-study-surface provenance flips;
Wooldridge pickle-migration and bootstrap-aggregate parity tests.
@github-actions

Copy link
Copy Markdown

Overall assessment

Looks good — no unmitigated P0/P1 findings.

Executive summary

  • Tail-df behavior matches the Methodology Registry and documented source/reference conventions.
  • Survey, replicate, and Bell–McCaffrey df correctly retain precedence.
  • New parameters propagate through inference, aggregation, results, and parameter APIs.
  • NaN-sensitive inference consistently uses safe_inference().
  • One minor API-documentation inconsistency remains.

Methodology

  • P3 — Documented deviations, no action required
    • Impact: The residual-df default differs from fixest/Stata’s G−1; ImputationDiD’s per-lead t versus joint-F pairing and LPDiD’s RA residual df are library conventions.
    • Concrete fix: None. These are explicitly documented in docs/methodology/REGISTRY.md:L467, docs/methodology/REGISTRY.md:L1798, and docs/methodology/REGISTRY.md:L2349, with v4 flips tracked as M-128–M-131.

No undocumented methodology mismatch, missing assumption check, or incorrect variance/SE was identified.

Code Quality

No findings. The shared resolver and safe_inference() usage avoid duplicated inline inference logic.

Performance

No findings. Added rank/count computations are linear and occur once per relevant fit.

Maintainability

No findings. Parameter validation, propagation, and result provenance are consistent across the affected estimators.

Tech Debt

  • P3 — Provenance completion is tracked
    • Impact: Wooldridge and ImputationDiD lack some public per-row df provenance.
    • Concrete fix: None for this PR; tracked in TODO.md:L65.

Security

No findings. No secrets, unsafe execution, or new external-input risks were identified.

Documentation/Tests

  • P2 — TWFE/API documentation is stale
    • Impact: TwoWayFixedEffects still documents only "residual" and "cluster" and incorrectly says the knob is inert on unclustered/Conley fits, although "normal" deliberately selects z-inference there. Generated result API pages also omit the new df_convention/inference_df attributes.
    • Concrete fix: Update diff_diff/twfe.py:L63-L68 for the three-value semantics and regenerate the affected files under docs/api/_autosummary/.

The added tests otherwise cover resolver branches, df precedence, estimator interactions, transactional parameter updates, and NaN-safe inference.

…ribute entries

The TwoWayFixedEffects constructor docstring still enumerated two knob
values and claimed blanket unclustered/Conley inertness, which "normal"
falsifies (it is deliberate z at the fallback level on every fit) -
rewritten with the per-value scoping used by DifferenceInDifferences and
LinearRegression. The committed docs/api/_autosummary attribute lists
gain the new result fields: df_convention on the five containers,
inference_df on SunAbrahamResults/StackedDiDResults (alphabetical
insertion, the #715/#726 convention).
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: ed5d822031ace237b0c9637734535d9c64ff70fc


Overall assessment

Looks good — no unmitigated P0/P1 findings.

Executive summary

  • The previous documentation finding is resolved.
  • Tail-df behavior matches the Methodology Registry.
  • Survey, replicate, and Bell–McCaffrey df retain precedence.
  • Parameter propagation and NaN-safe inference are complete.
  • Remaining provenance work is explicitly tracked.

Methodology

  • P3 — Documented deviations; no action required
    • Impact: Residual df remains the 3.9 default rather than fixest/Stata’s G−1; ImputationDiD’s joint-F pairing and LPDiD’s RA residual df are library conventions.
    • Concrete fix: None. These are documented in docs/methodology/REGISTRY.md:L467-L500, docs/methodology/REGISTRY.md:L1803-L1819, and docs/methodology/REGISTRY.md:L2349.

No incorrect variance, SE, identification logic, or undocumented methodological deviation was found.

Code Quality

No findings. Inference consistently routes through safe_inference().

Performance

No findings. Added rank and cluster-count calculations occur once per relevant fit.

Maintainability

No findings. Constructor, parameter API, wrapper, result, aggregation, and inference propagation are consistent.

Tech Debt

  • P3 — Provenance completion is tracked
    • Impact: Some Wooldridge and Imputation per-row df provenance remains unavailable; numerical inference is unaffected.
    • Concrete fix: None for this PR; tracked in TODO.md:L67.

Security

No findings.

Documentation/Tests

No findings. The previous P2 is resolved: TWFE now documents all three conventions at diff_diff/twfe.py:L63-L71, and the generated API stubs include the new result fields.

Runtime tests could not be executed because the reviewer environment lacks pytest and numpy; CI remains the execution gate.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Jul 31, 2026
@igerber
igerber merged commit a50873e into main Jul 31, 2026
39 of 40 checks passed
@igerber
igerber deleted the feat/tail-df-convention branch July 31, 2026 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-ci Triggers CI test workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant