Limited-MC-statistics de-biasing for binned template fits (continuous-M + cross-fit)#141
Draft
bendavid wants to merge 15 commits into
Draft
Limited-MC-statistics de-biasing for binned template fits (continuous-M + cross-fit)#141bendavid wants to merge 15 commits into
bendavid wants to merge 15 commits into
Conversation
- TensorWriter.add_mc_stat_moment(M, param_names): frozen nparams^2 noise-floor matrix stored as an external-likelihood term (hess=-M -> objective -1/2 th^T M th). - Fitter: --mcStatDebias/--mcStatDebiasCov/--mcStatKfold/--covMode options; _build_mcstat_M (reconstruct M from the mcstat external term); cov_mcstat_sandwich (A^-1 + A^-1 M A^-1, analytic, no 2nd Hessian pass); linearity warning (the -1/2 th^T M th penalty only de-biases LINEAR params; rabbit's default x^2 POI transform cancels it -- use --allowNegativeParam). - rabbit_fit.py reports the sandwich covariance when continuousM+sandwich. - tests/toy_mcstat.py, tests/verify_mcstat.py: 200-bin degenerate toy; rabbit matches an independent numpy reference to 4 d.p. (de-biased point, curvature, sandwich). See RESULTS.md S9. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… + sandwich) - TensorWriter.add_process(..., fold_axis=<name>): slice a named MC-stat fold axis into k folds, derive the full by projecting it out, store hnorm_folds [k,nbinsfull,nproc]. Non-folded procs stored as norm_full/k per fold (MC-stat-exempt). Common k required. - inputdata: load norm_folds / mcstat_fold_k. - Fitter: precompute half templates norm_A/norm_B (=2*sum of fold halves, shared logk); _compute_yields_noBBB(templates='A'|'B'); jackknife objective L_cf = 2 L_full - 0.5 L_A - 0.5 L_B in _compute_nll_components; meat objective loss_val_grad_hess_meat + cov_twohalf_sandwich (A^-1 H A^-1). - rabbit_fit.py reports the two-half sandwich when twoHalf/kfold + sandwich. - tests/toy_twohalf.py, tests/verify_twohalf.py: rabbit matches numpy reference to 4 d.p.; two-half also de-biases the NONLINEAR (x^2) POI where continuous-M is cancelled. See RESULTS.md S9d. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- fisher_curvature / fisher_curvature_full / _fisher_core: Gauss-Newton expected information F = H_obj - H_ln_obs + sum_i coeff_i J_i^T D J_i (D=1/V chisq, data_cov_inv covFit, 1/nexp Poisson). For the jackknife objective the GN data part is exactly the cross-half Fisher F_ch. Drops the residual*d2nexp term, leaves constraint/external/BB-lite curvature intact. - rabbit_fit.py: --covMode fisher uses the GN curvature as the bread for the standard cov AND the sandwich (continuous-M and two-half); cov_twohalf_sandwich meat follows covMode too. - Validated: linear -> fisher==observed (5.8e-11); nonlinear -> differs (drops residual). CLI runs both modes end-to-end. - Refined continuous-M linearity warning: _mcstat_M_params_linear() fires only when M actually touches nonlinear params (squared POI / log_normal syst / non- chisq); silent for the linear-Gaussian case where continuous-M is exact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- TensorWriter.add_systematic(h, ..., fold_axis=<name>): split a systematic's logk per fold (per-fold logk vs per-fold nominal), register the full syst by projecting the fold axis out. Assemble/write hlogk_folds [k,nbinsfull,nproc, nsyst] (folded systs split; others replicated so logk_A==logk_B). Minimal scope: dense, symmetric tensor, single-hist (mirror=True). - inputdata: load logk_folds. - Fitter: build logk_A/logk_B (k=2: per-fold logk = per-half logk, same rnorm_init scaling as shared logk); _compute_yields_noBBB(templates='A'|'B') uses them. - Validated (tests/toy_splitlogk.py): a systematic reusing the nominal MC sample has its template noise de-biased by split logk (tilt sigma 0.0705 -> 0.0907) where shared logk does not (0.0705). See RESULTS.md S9f. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Examined composing the de-biasing with Barlow-Beeston-lite; both diverged with BB-lite ON. Root causes + fixes: - two-half + BB-lite was UNBOUNDED: beta-profiling flattens L_full's POI curvature while -1/2 L_A -1/2 L_B keep full curvature -> A=2H_full,bb-1/2H_A -1/2H_B indefinite. Fix: apply the full-sample profiled beta (per-bin factor nexp/nexp_full_raw) to the half predictions too, so all three terms are consistently profiled and A ~ H_full,bb > 0 (bounded/PD). Composes sensibly: BB-lite inflates the baseline, two-half de-biases on top. - continuous-M + BB-lite: M must use the BB-inflated variance (mu+sumw2) in its denominator (else H-M non-PD, divergence). Fitter warns; add_mc_stat_moment documents it. Works with the correct M. - Both --covMode modes work with BB-lite (fisher ~ observed). tests/verify_bblite.py asserts PD convergence + de-bias for both methods with BB-lite on. No-BB paths unchanged (verify_mcstat / verify_twohalf exact). See RESULTS.md S9g. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- fitter.cov holds the reported de-biased cov (sandwich/curvature), so parameter errors, expected-hist bin errors (--saveHists), and postfit variations already propagate it (verified: sandwich bin errors > curvature). - Traditional impacts now decompose the de-biased CURVATURE A^-1 with the matching covMode-aware bread for total/syst/stat (was: sandwich for syst, raw observed Hessian for stat; --covMode fisher unpropagated). impacts_parms gains cov= and extra_group_vars=. The sandwich's extra coverage term diag(sandwich-curvature) is appended as a new 'mcStatDebias' grouped-impact column (workspace axis label added). Verified columns/labels/values consistent. - global_impacts_parms gains a cov override (decompose the de-biased curvature). - All --mcStatDebias x --covMode x --mcStatDebiasCov combos run --doImpacts / --globalImpacts / --saveHists end-to-end; standard impacts + no-BB/BB-lite validation suites unchanged. See RESULTS.md S9h. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Generalize the fisher (GN) cross-half curvature to the complete k-fold
U-statistic A_k = k/(k-1)[F_full - sum_i F_i_raw] = k/(k-1) sum_{i!=j} cross,
via the full-minus-self identity: O(k), streamable, no partition enumeration.
Reduces EXACTLY to the 2-half cross-half Fisher at k=2.
- _fisher_data_terms returns the raw-per-fold U-statistic terms (residual-free GN);
_fisher_core splits H_ln (objective's rescaled halves -> non-data extraction)
from F (U-statistic data Fisher). _compute_yields_noBBB gains templates='fold'
+ fold_index; __init__ stores n_folds and per-fold scaled logk.
- Realized in --covMode fisher (auto for k>=2; benefit for k>=3). Observed mode
k>=3 uses the single 2-half split; fitter logs an INFO -> use fisher. The point
(objective) keeps the rescaled 2-half jackknife (unbiased). --mcStatKfold
deprecated (k inferred from folds; U-statistic is parameter-free).
- Validated (tests/verify_kfold.py): rabbit == numpy U-statistic (k=4) to 4 d.p.;
ensemble median ~ sigma_inf (unbiased), RMS shrinks with k toward the
continuous-M floor. No regression. See RESULTS.md S9i.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Lift the k=2-only split-logk restriction. Build logk_folds_scaled [k,...] (per-
fold scaled logk) for any k; k=2 sets the half logk for the objective (full
treatment), k>2 objective falls back to shared logk (point de-biases nominal
noise; systematic-template noise de-biased in the curvature) + INFO.
- _fisher_data_terms uses n_sumfold = sum_i n_i as the U-statistic 'full' term and
the meat, so full-minus-self = sum_{i!=j} cross holds EXACTLY for shared logk
(sum_i n_i == nexp_full, common case unchanged) AND split logk. BB-lite beta
applied to every per-fold prediction.
- toy_splitlogk.py generalized to K folds (env K, default 2). Validated: shared-
logk paths unchanged (verify_kfold/twohalf/bblite pass; k=2 tilt sandwich 0.0907
unchanged); k=4 split-logk loads per-fold logk [4,...], converges, de-biases the
tilt systematic more than shared (0.130 vs 0.086). See RESULTS.md S9j.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- add_systematic([up,down], ..., fold_axis=, symmetrize=): _add_systematic_folded handles the up/down PAIR; per fold computes logkavg (+ logkhalfdiff for the asymmetric symmetrize mode), stored in dict_logkavg_folds / dict_logkhalfdiff_ folds. write() assembles the fold logk in the full tensor's shape ([k,..,nsyst] symmetric or [k,..,2,nsyst] asymmetric; folded systs split, others replicated). - Fitter: shape-aware rnorm_init scaling of logk_folds_scaled (sym/asym); removed the symmetric-tensor guard; per-fold asym logk flows through the existing asymmetric interpolation in _compute_yields_noBBB(templates='fold'). - Folded process + asymmetric SHARED syst already worked (verified). 'linear'/ 'quadratic' symmetrize not supported with folds. Validated: k=4 folded asym syst converges PD, logk_folds [4,40,1,2,1]. Symmetric/split paths unchanged. See RESULTS.md S9k. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fold norm is stored dense [k,nbinsfull,nproc] even in sparse mode (the full template stays sparse CSR); removed the writer's sparse fold_axis raise (folds densified). - Fitter sparse per-fold yield: scatter the shared systematic factor from the sparse logk (exp(logsnorm) log_normal / additive logsnorm normal) into a dense [nbinsfull,nproc] grid and contract with the dense fold/half norm. The fisher U-statistic curvature (dense per-bin yield + dense Jacobian) then works for sparse-input + dense-cov. split-logk in sparse not supported (raises). - Validated (tests/verify_sparsefold.py): sparse k=4 fold de-bias == dense to 1e-6 (point, fisher curvature, sandwich). Sparse non-fold path unchanged; verify_* pass. See RESULTS.md S9l. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The sparse fold-yield uses the shared systematic factor; split-logk needs per- fold logk. Instead of densifying the full [k,nbins,nproc,nsyst] logk, store a per-fold DELTA over the FOLDED systs only: delta=logk_fold-logk_full, dense [k,nbinsfull,nproc,n_folded], + folded global syst indices. - Writer: _add_systematic_folded computes logk_full densely and stores the delta; write() assembles hlogk_folds_delta + hmcstat_folded_syst_idx (sparse only). inputdata loads them. - Fitter: sparse fold-yield multiplies the shared log_normal factor by exp(delta.theta) for the active fold (curvature path); shared_logk+delta=fold_logk exactly. Objective/halves use shared logk (point de-biases nominal noise). log_normal symmetric only (asymmetric sparse split-logk raises). - Validated (tests/verify_sparse_splitlogk.py): sparse k=4 split-logk == dense to 1e-6 (point, fisher, sandwich). All other fold paths unchanged. See RESULTS.md S9m. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rabbit's built-in toys fluctuate DATA only; MC-stat coverage needs the TEMPLATES fluctuated. tests/mcstat_coverage.py fluctuates both per pseudo-experiment, builds a rabbit tensor, fits, and measures POI coverage (configurable debias x covMode x debiasCov x BB-lite x chisq/Poisson). Near-degenerate slides toy with NONZERO degenerate-direction truth (rnorm_true=[1.3,0.7]) so the attenuation bias is real. Findings: de-bias REMOVES the central-value bias (naive med_bias ~ -0.29 on a true 0.42 -> ~0) and inflates sigma toward sigma_inf (validates the first two original criteria); the raw curvature/sandwich sigma still underestimates the de-biased point's ENSEMBLE scatter by ~20-25% (MC-noise variance in the score not in the single-toy meat) -> coverage ~0.4-0.55 not 0.683 -> the Bartlett/calibration factor is needed for exact coverage (CONFIRMS the known limitation, now measured natively). tests/verify_coverage.py asserts the robust facts (bias removed, sigma inflated, coverage improved). See RESULTS.md S9n. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…concile proper sandwich - run_coverage: require positive variance in the MEASURED directions (not full PD), so BB-lite's rank-deficient-other-direction covariance on near-degenerate toys is not spuriously rejected; return nan instead of crashing when n=0. - Reconciliation (RESULTS S9o): S9n measured the BARE sandwich (--noBinByBinStat, meat omits sigma_MC) -> undercovers (cov 0.51), calibration gap. The PROPER sandwich with BB-lite (meat = A+M = H_bb includes sigma_MC) COVERS 0.67 ~ 0.683 with NO calibration (med_sig 0.041 matches the ensemble scatter 0.040) -- matches the numpy result. rabbit's two-half sandwich uses the Hessian meat, not the cross-fold score variance, so it is not yet self-calibrating without BB-lite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sandwich) - Fitter.cov_dataprop_sandwich: delta-method sandwich (RESULTS S7d/demo23) propagating the per-bin DATA variance and per-(bin,proc) TEMPLATE variance (sumw2) through the de-biased estimator by implicit diff (Sigma = dx/dnobs diag(Var_nobs) dx/dnobs^T + dx/dnorm diag(sumw2) dx/dnorm^T). Continuous-M, dense (watches indata.norm). - --mcStatDebiasCov gains 'dataPropagated'; rabbit_fit wires it. - Validated: conservative / OVER-covers (slides toy: cov 0.79 vs analytic 0.50, none 0.06) -- matches S7d (~0.81). Corrects S9o: the score-variance meat is NOT self-calibrating, it over-covers (needs a downward k); the calibration-free meat is the analytic/BB-lite one. CLI runs end-to-end. See RESULTS.md S9p. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…CI lint) - isort (--profile black --line-length 88) + black across the mcstat source and test files; remove an unused 'import tensorflow as tf' (F401) and a dead 'bias_dif' list. No functional change (verify_twohalf still matches numpy). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Limited-MC-statistics de-biasing for binned template fits
Adds opt-in de-biasing of the bias and under-coverage that finite MC template
statistics induce in chi-squared / profile-likelihood template fits (the
errors-in-variables / Neyman–Scott "incidental parameters" effect). Two
complementary methods, both injected at the scalar-NLL level so they flow through
rabbit's existing TF-autodiff grad/Hessian/HVP machinery and SciPy minimizers.
Draft — opening for design review and discussion of the interfaces before polish
(the test scripts are standalone validation drivers, not yet pytest/CI-wired).
Methods
M(supplied to theTensorWriter)enters the objective as
-½ θᵀ M θ, giving the de-biased point(H−M)⁻¹gandcurvature
H−M. Lowest variance; requires the de-biased parameters to enter theprediction linearly (e.g.
--allowNegativeParamPOIs); a warning fires otherwise.fold_axis; the writer slices it (full derived by projecting it out). The fitterforms the jackknife
2L_full − ½L_A − ½L_B; the--covMode fishercurvature usesthe complete pairwise U-statistic over all k folds (k-fold averaging, O(k), no
partition enumeration, → continuous-M as k→N). General / nonlinearity-robust.
Interfaces
TensorWriter.add_mc_stat_moment(M, param_names);add_process(..., fold_axis=),add_systematic(..., fold_axis=)(split-logk, symmetric/asymmetric, dense/sparse).--mcStatDebias {none,continuousM,twoHalf,kfold},--mcStatDebiasCov {curvature,sandwich,dataPropagated},--covMode {observed,fisher}(applies to the standard cov too).through to impacts (a new
mcStatDebiasimpact group) and output histograms.Validation
curvature, sandwich) for continuous-M and two-half; k-fold U-statistic == numpy and
RMS↓ with k; sparse == dense and split-logk sparse == dense to 1e-6.
tests/mcstat_coverage.py):the de-bias removes the central-value attenuation bias and inflates σ→σ∞; the
proper sandwich (BB-lite σ_MC in the meat) covers ~0.68 with no calibration,
while the bare-Hessian meat under-covers and the data-propagated meat over-covers.
Known limitations (documented)
dataPropagated/sparse-split-logk are continuous-M/dense and log_normal-symmetricrespectively; large-
nparams--noHessian(matrix-free) de-biased covariance isnot yet implemented (the HVPs are automatic; only a sandwich-aware Hessian-free row
extractor is missing).
🤖 Generated with Claude Code