Skip to content

feat(3.9): variance-conventions inventory + component-aware absorbed-FE rank (D3) - #736

Merged
igerber merged 1 commit into
mainfrom
feat/variance-conventions-inventory
Jul 30, 2026
Merged

feat(3.9): variance-conventions inventory + component-aware absorbed-FE rank (D3)#736
igerber merged 1 commit into
mainfrom
feat/variance-conventions-inventory

Conversation

@igerber

@igerber igerber commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds docs/methodology/variance-conventions.md (repo-internal, Sphinx-excluded): the measured inventory of every surface reaching the shared clustered-CR1 denominator and every tail-df convention, with a defect/legitimate classification (D1/D2/D4 scheduled for the 3.9 consolidation PRs; L1-L4 declared exceptions with reasons and external anchors). The table is generated from expected literals in tests/test_variance_conventions.py; a byte-equality test keeps the committed doc in sync.
  • Adds tests/test_variance_conventions.py: the audit matrix. Programmatically discovered instrumentation (every module binding safe_inference/safe_inference_batch; Rust backend disabled per module) captures each row's CR1 k multiset (vcov_type="hc1" only - a clustered call in any other family fails the row) and tail-df multiset against pinned literals, fast subset by default and the full sweep under -m slow.
  • Fixes D3: new diff_diff.utils.absorbed_fe_rank computes the two-way absorbed-FE df from the connected components of the bipartite level graph (sum(levels) - C, minus 1 when the visible design carries an intercept column), replacing the sum(levels - 1) count that over-counted on disconnected and hierarchical panels (absorb=["state", "state_year"]: true rank 29, old count 34). Consumed by DifferenceInDifferences(absorb=), MultiPeriodDiD(absorb=), within-transform TwoWayFixedEffects (intercept form), and SunAbraham (no-intercept form). Connected independent panels are bit-identical; levels and graph edges use positive-weight rows only per the REGISTRY zero-weight-padding guarantee; NaN group keys raise an actionable ValueError; N >= 3 keeps the legacy count with the limitation documented and tracked.
  • Documents the demean_by_groups/demean_by_group second return value as a raw level count that is NOT a valid df adjustment (no library caller consumes it as df any more), and updates REGISTRY.md: the component-aware TwoWayFixedEffects absorbed-FE df note with cross-references from the DiD/MPD/SunAbraham sections, both sides of the D1 split, the labeled deviation from the fixest default, and the Wooldridge hc1 mechanism status.

Methodology references (required if estimator / math changes)

  • Method name(s): absorbed fixed-effects degrees-of-freedom rank (two-way FE dummy-space rank via connected components); clustered-variance convention inventory (CR1 finite-sample factor, tail-df conventions)
  • Paper / source link(s): Abowd, J.M., Creecy, R.H., & Kramarz, F. (2002), "Computing Person and Firm Effects Using Linked Longitudinal Employer-Employee Data" (Census TP-2002-06) - two-factor identification via connected components; fixest::ssc(K.exact = TRUE) documentation (exact irregular-FE parameter count)
  • Any intentional deviations from the source (and why): The exact rank matches fixest::ssc(K.exact = TRUE) rather than fixest's approximate DEFAULT (K.exact = FALSE, which reproduces the old over-count); recorded as a labeled "Note (deviation from R default - irregular FE designs)" in REGISTRY.md with measured numbers, and anchored by a committed R golden (benchmarks/data/fixest_kexact_golden.json, fixest 0.14.2) at <= 1e-12. Pre-existing D1/D2/D4 conventions are inventoried and documented, not changed here (scheduled follow-up PRs).

Validation

  • Tests added/updated: tests/test_variance_conventions.py (new: audit matrix, absorbed_fe_rank unit + end-to-end arms incl. disconnected/hierarchical/zero-weight/NaN-key/fail-closed-boundary cases, doc byte-equality sync, skip-guarded fixest K.exact parity), tests/test_utils.py (pins the demean count as the raw level count vs the df-valid rank)
  • Backtest / simulation / notebook evidence (if applicable): committed R golden generated by benchmarks/R/generate_fixest_kexact_golden.R (fixest 0.14.2, deterministic embedded data); classical SE matches the K.exact = TRUE side to all printed digits and discriminates from the R default (>2% apart)

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

@github-actions

Copy link
Copy Markdown

Overall Assessment

Looks good — no unmitigated P0/P1 findings.

Executive Summary

  • Exact two-way absorbed-FE rank matches Abowd–Creecy–Kramarz and fixest::ssc(K.exact=TRUE).
  • Intercept accounting is correct across all four affected estimator paths.
  • Zero-weight rows and disconnected/hierarchical panels are handled consistently.
  • Existing CR1/tail-df deviations are documented and not changed here.
  • The three-way limitation and performance cost are explicitly tracked.

Methodology

  • P3 — Documented limitation: Three-or-more absorbed dimensions retain the approximate sum(levels−1) count, which can overstate rank for nested designs.
    Impact: Potentially conservative residual df/SEs for irregular N-way FE.
    Concrete fix: No action required for this PR; tracked in TODO.md. See diff_diff/utils.py:L2953-L2957.

  • P3 — Documented deviation from R default: Two-way irregular FE uses exact rank rather than fixest’s approximate default.
    Impact: Intentional agreement with K.exact=TRUE; methodologically valid.
    Concrete fix: None. Documented in docs/methodology/REGISTRY.md:L401-L415.

The affected methods are DifferenceInDifferences(absorb=), MultiPeriodDiD(absorb=), within-transform TwoWayFixedEffects, and SunAbraham; propagation is complete at diff_diff/estimators.py:L500, diff_diff/estimators.py:L1767, diff_diff/twfe.py:L422, and diff_diff/sun_abraham.py:L1661.

Code Quality

No findings. Rank is computed before in-place demeaning, and intercept/no-intercept contracts are explicit.

Performance

  • P3 — Tracked overhead: Refactorization adds approximately 1.9 ms on the documented benchmark.
    Impact: About 7.7% on the fastest configuration.
    Concrete fix: No current action required; reuse of demeaner codes is tracked in TODO.md.

Maintainability

No findings. Centralizing rank computation avoids divergent estimator-specific formulas.

Tech Debt

No untracked debt. The N-way exact-rank extension and factorization reuse are both recorded in TODO.md.

Security

No findings. No secrets, unsafe execution, or sensitive data were introduced.

Documentation/Tests

No findings. Tests cover connected, disconnected, hierarchical, zero-weight, NaN-key, saturated, estimator-level, and external fixest parity cases.

Runtime tests could not be executed in the review environment because pytest and numerical dependencies are unavailable; all changed Python files passed syntax parsing.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Jul 30, 2026
…FE rank (D3)

The measured map behind the 3.9 variance-consolidation program, plus the
first of its fixes.

Inventory (docs/methodology/variance-conventions.md, repo-internal):
- Measured matrix of every surface reaching the shared clustered-CR1
  denominator (linalg._compute_robust_vcov_numpy, vcov_type="hc1" only -
  a clustered call in any other family fails its row) and every tail-df
  convention passed to safe_inference/safe_inference_batch. The table is
  generated from expected literals in tests/test_variance_conventions.py;
  the parametrized tests assert those literals against live
  instrumentation (fast subset by default, full sweep under -m slow) and
  a byte-equality test keeps the committed doc in sync.
- Classifies the defects scheduled for the consolidation PRs - D1
  (absorb= vs fixed_effects=: 10.35% SE split on the same model, ratio
  exactly sqrt((360-2)/(360-66))), D2 (clustered CR1 never counts
  absorbed FE not nested in the cluster; anti-conservative, up to 5.51%
  at G=60), D4 (SunAbraham reports residual df per cohort-period cell but
  normal theory on aggregates) - and the legitimate exceptions L1-L4
  (StackedDiD CR1S, LPDiD G-1, influence-function paths, hc2/hc2_bm +
  survey TSL) with reasons and external anchors.

D3 fix (this PR's behavior change):
- New diff_diff.utils.absorbed_fe_rank: two-way absorbed-FE df from the
  connected components of the bipartite level graph (sum(levels) - C,
  minus 1 when the visible design carries an intercept column), exact per
  Abowd-Creecy-Kramarz. Levels and edges use positive-weight rows only,
  per the REGISTRY zero-weight-padding guarantee; NaN group keys raise an
  actionable ValueError. N>=3 keeps sum(levels-1) with the over-count
  limitation documented and tracked in TODO.md.
- Consumers: DiD(absorb=) and MultiPeriodDiD(absorb=) (intercept form,
  computed on the pre-transform frame), within-transform
  TwoWayFixedEffects (intercept form), SunAbraham (no-intercept form -
  its saturated design has coef_offset=0). Connected independent panels
  are bit-identical to the old count; disconnected and hierarchical
  designs (absorb=["state","state_year"]: true rank 29, old count 34) get
  corrected residual df and non-clustered classical/HC1 SE scaling, with
  the fail-closed NaN boundary moving consistently in both directions.
- External anchor: matches fixest::ssc(K.exact = TRUE) at <=1e-12 on a
  committed hierarchical R golden (benchmarks/data/fixest_kexact_golden.json,
  fixest 0.14.2, generator script included); fixest's DEFAULT
  K.exact=FALSE reproduces the old approximate count, recorded as a
  labeled deviation from the R default in REGISTRY.md.
- demean_by_groups/demean_by_group's second return value is now
  documented as the raw level count, NOT a valid df adjustment, pointing
  to absorbed_fe_rank (no callers consume it as df any more; returning
  the rank from the demeaner instead was rejected because within_transform
  delegates there and would compute the graph twice per TWFE/SA fit).
- REGISTRY.md: the TwoWayFixedEffects absorbed-FE df note is now
  component-aware, with cross-references from the DiD, MultiPeriodDiD,
  and SunAbraham sections; both sides of the D1 split and the Wooldridge
  hc1 mechanism status are documented. doc-deps.yaml registers the
  inventory doc against its 11 source modules; performance-plan.md
  records the helper's measured 1.9 ms / 186k-row cost with the
  codes-reuse optimization tracked as its own TODO row.
@igerber
igerber force-pushed the feat/variance-conventions-inventory branch from 3401c81 to 993b2d0 Compare July 30, 2026 16:40
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 993b2d0a3e19db61a0ed48c465dbef6769d6b928


Overall Assessment

Looks good — no unmitigated P0/P1 findings.

Executive Summary

  • Two-way FE rank correctly uses sum(levels) − components.
  • Intercept accounting is correct across all four estimator paths.
  • Positive-weight support, NaN keys, disconnected panels, and saturated inference are covered.
  • Existing CR1/tail-df discrepancies are documented and unchanged.
  • Prior P3 limitations remain properly tracked.

Methodology

  • P3 — Tracked limitation: Three-or-more absorbed dimensions retain the approximate sum(levels−1) count.
    Impact: Irregular N-way FE designs may overcount rank.
    Concrete fix: No action required; tracked in TODO.md. See diff_diff/utils.py:L2953-L2957 and TODO.md:L24.

  • P3 — Documented deviation from R default: Exact two-way rank matches Abowd–Creecy–Kramarz and fixest::ssc(K.exact=TRUE), rather than fixest’s approximate default.
    Impact: Intentional, methodologically valid df/SE difference on irregular panels.
    Concrete fix: None. See docs/methodology/REGISTRY.md:L400-L415.

Propagation and intercept handling are correct in diff_diff/estimators.py:L500-L529, diff_diff/estimators.py:L1767-L1800, diff_diff/twfe.py:L417-L427, and diff_diff/sun_abraham.py:L1657-L1668.

Code Quality

No findings.

Performance

  • P3 — Tracked overhead: Rank calculation adds approximately 1.9 ms on the documented benchmark.
    Impact: About 7.7% on the fastest Rust-backed TWFE configuration.
    Concrete fix: No action required; factorization reuse is tracked in TODO.md:L52.

Maintainability

No findings. Centralizing rank calculation removes estimator-specific df formulas.

Tech Debt

No untracked debt. N-way exact rank and factorization reuse are recorded in TODO.md.

Security

No findings. No secrets, unsafe execution, or sensitive data were introduced.

Documentation/Tests

No findings. Tests cover connected, disconnected, hierarchical, zero-weight, NaN-key, saturated, estimator-level, and fixest-parity cases.

Runtime tests could not be executed because pytest, NumPy, pandas, and SciPy are unavailable in the review environment. All changed Python files passed AST parsing.

@igerber
igerber merged commit 1e693a8 into main Jul 30, 2026
31 checks passed
@igerber
igerber deleted the feat/variance-conventions-inventory branch July 30, 2026 18:05
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