Skip to content

fix(estimation): estimation-state contracts for refits, storage, and unsupported paths - #1501

Closed
s3alfisc wants to merge 10 commits into
refactor/weight-within-domainsfrom
fix/estimation-state-contracts
Closed

fix(estimation): estimation-state contracts for refits, storage, and unsupported paths#1501
s3alfisc wants to merge 10 commits into
refactor/weight-within-domainsfrom
fix/estimation-state-contracts

Conversation

@s3alfisc

@s3alfisc s3alfisc commented Sep 3, 2026

Copy link
Copy Markdown
Member

Completes the shared estimator-state lifecycle: covariance updates are computed on detached candidates and published only after covariance and inference succeed. Single models and both multi-model containers retain their existing identities and previous results on failure. Gaussian get_performance() now uses the appropriate shared within-response accessor rather than assuming linear-only state.

Preserves the existing raw-scale weighted FE reconstruction, storage cleanup, unsupported-path guards, and faithful CRV3/randomization refits. update(inplace=True) remains explicitly rejected. Gaussian offsets remain unsupported; explicit covariance data must already match the filtered sample and order.

Integrates reviewed #1500 through an additive merge. No reviewed history was rewritten, and #1504 or its descendants were not adjusted.

Local verification: release contract 1,355 passed; lifecycle 65 passed; focused checks 9 passed; Gaussian/R-reference plus offset rejection 2 passed; covariance/HAC primitives 16 passed; live-R HAC 353 passed with 184 non-applicable combinations skipped; Ruff, mypy, and whitespace checks passed. Fresh exact-head CI supplies remaining Python/R/docs evidence. #1500's Ubuntu R preflight hit an unchanged auxiliary quantile-solver convergence failure; same-runtime old/new comparisons pass locally, but that failed CI check remains unresolved. Not merge-ready pending reference/platform evidence and human review.

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.50549% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pyfixest/estimation/FixestMulti_.py 91.30% 2 Missing ⚠️
...fixest/estimation/models/_result_accessor_mixin.py 75.00% 1 Missing ⚠️
pyfixest/estimation/models/feols_.py 97.05% 1 Missing ⚠️
pyfixest/estimation/quantreg/QuantregMulti.py 93.75% 1 Missing ⚠️
Flag Coverage Δ
core-tests 80.98% <94.50%> (+1.95%) ⬆️
test-r-core-other 48.44% <54.94%> (+<0.01%) ⬆️
test-r-extended 22.73% <18.68%> (+0.01%) ⬆️
test-r-fixest 39.86% <40.65%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
pyfixest/estimation/models/feglm_.py 96.52% <100.00%> (+0.02%) ⬆️
pyfixest/estimation/models/feiv_.py 98.49% <ø> (-0.02%) ⬇️
pyfixest/estimation/models/fepois_.py 100.00% <100.00%> (ø)
pyfixest/estimation/post_estimation/ritest.py 52.02% <100.00%> (+9.16%) ⬆️
pyfixest/estimation/quantreg/quantreg_.py 98.81% <100.00%> (+<0.01%) ⬆️
...fixest/estimation/models/_result_accessor_mixin.py 98.03% <75.00%> (+0.02%) ⬆️
pyfixest/estimation/models/feols_.py 91.92% <97.05%> (+1.83%) ⬆️
pyfixest/estimation/quantreg/QuantregMulti.py 82.14% <93.75%> (+2.14%) ⬆️
pyfixest/estimation/FixestMulti_.py 72.72% <91.30%> (+7.29%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

A CRV3 request on a gaussian, logit, or probit result was accepted and
then refit each leave-one-cluster-out sample through the Poisson API,
so the jackknife covariance described a different model than the one
reported. Feglm now disables CRV3 support; Fepois keeps the
longstanding Poisson jackknife path, which is unchanged numerically and
exercised through both feglm(family="poisson") and fepois().
The CRV3 slow jackknife and the slow randomization-inference loop rebuilt
each leave-one-cluster-out or resampled sample by calling feols()/fepois()
with only `weights` and `weights_type`. Everything else the user chose was
silently reset to the API defaults: the small-sample correction, singleton
removal, the solver, the demeaner and its tolerances, the collinearity
tolerance, `drop_intercept`, the formula evaluation context, and for Poisson
the offset, the IRLS tolerance and iteration cap, and the separation checks.
The resulting jackknife coefficients therefore came from a different
estimator than the one whose covariance was being reported.

`_estimation_refit_kwargs()` now carries that contract and both refit paths
replay it. A `Preconditioner` handed to `LsmrDemeaner` is the one option that
must not be forwarded verbatim: a prebuilt factorization is tied to the
fixed-effect design of the full sample, and every leave-one-cluster-out
sample has a different one. Its algorithmic variant is kept and the
factorization is rebuilt per refit.
For a weighted fit, `fixef()` solved the least-squares problem on the
square-root-weighted design and then stored `sqrt(W) * D @ alpha` in
`_sumFE`, so the retained fixed-effect contribution -- and therefore
`predict()` whenever fixed effects were present -- was on the solver
scale rather than in response units. The `lsqr` solve still uses the
weighted design; only the retained product switches to the unweighted
one.

R fixest reports `sumFE` on the response scale, and the new
`against_r_core` test compares `_sumFE`, `fixef()`, fitted values, and
`predict(newdata=...)` against `fixest` for both analytic and frequency
weights.
Computing the effective F statistic refits the first stage with
heteroskedasticity-robust errors. As a side effect it also overwrote the
outer model's `_vcov_type_detail` with "hetero", so after `IV_Diag()` a
later `vcov()` call or a report read a covariance label the main model
had never been estimated with, while its stored standard errors still
came from the requested type.

Only the label is removed. The effective F was and remains computed from
the first stage's heteroskedasticity-robust covariance, so the statistic
is unchanged and matches the value obtained from a `vcov="hetero"` fit.
`wildboottest()`, `ritest()`, and `decompose()` reached their OLS solver
paths for every estimator that inherits them. On a `Feglm` result they read
`_X`, `_Y`, and `_u_hat`, which hold the IRLS working design, working
response, and working residuals of the final iteration, and treated them as
the OLS design, dependent variable, and residuals. On a `Quantreg` result
the same attributes hold the quantile solver's inputs and check-function
residuals, which carry no least-squares interpretation at all. Both cases
returned bootstrap distributions, randomization p-values, and Gelbach
decompositions that look like valid output but describe a linear model
nobody estimated.

Each path now rejects the estimators it cannot represent: the wild bootstrap
via the `_supports_wildboottest` flag, which `Feglm` now sets to `False`
alongside `Quantreg`; randomization inference via an explicit `_method`
check against `{"feols", "fepois"}`; and `decompose()` via
`_support_decomposition`, which `Quantreg` now sets to `False` next to the
existing `False` on `Feglm`, `Fepois`, and `Feiv`.
`FixestMulti` stored the full input frame, the estimation config, and the
captured evaluation context on the container unconditionally, so
`store_data=False` and `lean=True` stripped every child result while the
container kept the very frame those options were asked to release. The
container now retains `_data`, `_config`, and `_context` only when the
options actually permit it; `_parsed` stays unconditional because the
container's formula accessors read it.

`FixestMulti.vcov()` gains the `data=` argument that child results already
accept, which is the only way to update a data-dependent covariance after
`store_data=False`. A forwarded frame must describe one estimation sample
shared by every child, and that has to be checked once, before any child is
touched: children are updated in a loop, so a mismatch discovered halfway
through would leave some results on the new covariance and the rest on the
old one. The preflight therefore rejects a row count that does not match
every child, and rejects children whose split sample or NA mask differ even
when their row counts agree, before the loop starts.

`QuantregMulti._clear_attributes` deleted `_within_data` from every child
after every fit, not only under the storage options, so a default
multi-quantile result had no design or response left and could neither
predict nor recompute its covariance. Fit state is now published through
`_publish_fit_state`, which also sets `_Y_hat_link` and `_Y_hat_response`
alongside `_beta_hat`, `_u_hat`, and `_hessian`, and the unconditional
deletion loop is gone; each child's own `_clear_attributes` still honors
`store_data=False` and `lean=True`.
Records in the architecture guide which post-estimation paths reject
non-OLS results, that CRV3 and slow randomization refits replay the
estimation contract, and how the storage options now reach
multiple-estimation containers. Files the container storage note under
bug fixes next to the related first-stage note instead of under the
removals list.
The lookup has held array-native DemeanedData entries since the shared
demean cache stopped converting to DataFrames; the annotation still
said pd.DataFrame. Annotation only, no runtime change.
# Conflicts:
#	docs/developer/architecture.md
#	pyfixest/estimation/models/feols_.py
@s3alfisc

s3alfisc commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

Superseded by a nine-layer replacement stack above unchanged #1499, rebuilt from this PR's reviewed final code (head 03f284b). Each layer answers one review question and passes its checks against its immediate parent:

  1. fix(estimation): reject unsupported operations #1516 explicit unsupported operations (fix/unsupported-estimation-operations)
  2. refactor(estimation): add weighted fit primitives #1518 weighted fit primitives (refactor/weighted-fit-primitives)
  3. refactor(estimation): add named demeaning cache #1519 array-native demeaning cache (refactor/named-demeaning-cache)
  4. refactor(estimation): separate linear state domains #1520 linear-model state and consumers (refactor/linear-estimation-state)
  5. refactor(glm): separate observation and working state #1521 GLM working state and frequency weights (refactor/glm-working-state)
  6. refactor(estimation): expose estimator state through read-only aliases #1522 getter-only compatibility aliases (refactor/estimator-state-aliases)
  7. fix(estimation): enforce retained estimation storage contracts #1523 storage and stripped-state contracts (fix/retained-estimation-storage)
  8. fix(estimation): replay estimation options in data-changing refits #1524 faithful estimation refits (fix/faithful-estimation-refits)
  9. fix(inference): publish covariance updates atomically #1525 transactional covariance updates (fix/atomic-covariance-state)

The final layer's tracked-file contents are identical to 03f284b. Closing as superseded; this branch and the review discussion stay in place for reference.

@s3alfisc s3alfisc closed this Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant