feat: Haberman subscore added-value analysis (PRMSE) - #221
Merged
seonghobae merged 1 commit intoJul 24, 2026
Merged
Conversation
Add `mlsirm_core::subscores` implementing the Haberman (2008, as cited in Sinharay, 2010) subscore added-value methodology: Cronbach alphas, the true-score covariance construction (observed off-diagonals, alpha-adjusted diagonal, row sum over subscore columns only), PRMSE_s / PRMSE_x / PRMSE_sx, the augmented-regression weights tau/beta/gamma, three per-person true- subscore estimators, disattenuated subscore correlations, and added-value decisions (Haberman's PRMSE_s > PRMSE_x; Sinharay's 2010 +0.01 margin for augmentation, labeled - CRAN CTTsub's relative rule documented but not implemented). Formulas verified against the Appendix of Sinharay (2010, ETS RR-10-16) and the CRAN `subscore` package R source read line-by-line; Haberman (2008) and Wainer et al. (2001) are cited only as cited in Sinharay (2010). Divergences (validated partition, hard rejection of degenerate samples, CRAN's nonnegative-root s_hat_x convention) are documented in the module docs. Rust-only numerics; PyO3 binding `subscore_analysis` with checked-mul dimension validation before any allocation; thin Python wrapper `fast_mlsirm.subscore_analysis` returning a `SubscoreResult` dataclass with trust-boundary shape/index guards. Tests pin every reported statistic against literals from an independent NumPy transcription of the R semantics on an asymmetric 10x4 fixture with mixed added-value outcomes, cover the structural and degeneracy guards (negative alpha, collinear subscore, sparse-index DoS, bad partitions, incomplete data), assert conditional PRMSE dominance on guard-passing random data, and include a 500-rep #[ignore] Monte Carlo MSE comparison. Three mutation spot-checks (dropped m/(m-1), rowsum including the total column, tau numerator sign flip) were run and killed. An adversarial implementation review independently replicated the formulas from the R oracle and its four confirmed findings (PyO3 capacity-overflow panic, zero-column wrapper path, positive-root doc overclaim, CHANGELOG coverage overclaim) are fixed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds Haberman (2008, as cited in Sinharay 2010) subscore added-value analysis to the Rust core and exposes it through a Python API, with pinned reference-based tests to ensure numerical and guard-behavior parity with the documented CRAN subscore semantics.
Changes:
- Implemented
mlsirm_core::subscoresto compute PRMSE-based subscore estimators, augmented-regression weights, correlations, and added-value decisions with explicit degeneracy/partition guards. - Added Python wrapper
fast_mlsirm.subscore_analysisreturning a typedSubscoreResult, including validation to keep hostile/degenerate shapes from crossing the Rust boundary. - Added Rust + Python tests that pin outputs to independently-derived NumPy literals and exercise mandated rejection behavior; documented the feature in the changelog.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/subscores_tests.rs | New Rust unit tests pinning subscores outputs to independent reference literals and exercising guard behavior (plus an ignored Monte Carlo check). |
| tests/test_paper_features.py | New Python integration tests validating wrapper output against reference literals and verifying degenerate-input rejection. |
| python/fast_mlsirm/subscores.py | New Python API (SubscoreResult, subscore_analysis) with input validation and marshaling into the Rust core. |
| python/fast_mlsirm/init.py | Exports subscore_analysis and SubscoreResult at the top-level package API. |
| crates/mlsirm-core/src/subscores.rs | New Rust implementation of Haberman/Sinharay subscore added-value analysis with documented sources/guards and per-person estimators. |
| crates/mlsirm-core/src/lib.rs | Wires the new subscores module into the core crate’s public module tree. |
| crates/fast-mlsirm-py/src/lib.rs | Adds the PyO3 binding function subscore_analysis and exposes it on the compiled core module. |
| CHANGELOG.md | Documents the new subscore added-value feature, outputs, and the testing/guarding approach. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
seonghobae
merged commit Jul 24, 2026
f099cef
into
seonghobae-ksirt-kernel-smoothing
6 checks passed
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.
Haberman subscore added-value analysis (PRMSE)
Iteration 4 of the autonomous paper-implementation loop (stacked on #220).
Implements the Haberman (2008) subscore added-value methodology as
mlsirm_core::subscores+fast_mlsirm.subscore_analysis: for each subscaleof a disjoint, exhaustive item partition, the PRMSEs of the three CTT
true-subscore estimators (observed subscore / observed total / augmented),
the augmented-regression weights, per-person estimator matrices,
disattenuated subscore correlations, and added-value decisions.
Sources (paper-first discipline)
subscorepackage R source (subscore.s.r,subscore.x.R,subscore.sx.R,data.prep.R,CTTsub.R)Formulas (all source-verified)
C_T: observed covariances off-diagonal,alpha_k * V(s_k)on the diagonal;cov_k= row sum over the K subscore columns only (total column excluded).rho^2(s_t, x_t) = cov_k^2 / (V(s_kt) V(x_t));PRMSE_x = rho^2 * alpha_x;tau = (sqrt(alpha_x) sqrt(rho^2) − r sqrt(alpha_k)) / (1 − r^2);PRMSE_sx = alpha_k + tau^2 (1 − r^2);beta = sqrt(alpha_k)(sqrt(alpha_k) − r tau);gamma = sqrt(alpha_k) tau sigma_s / sigma_x.PRMSE_s > PRMSE_x(Haberman);PRMSE_sx > max + 0.01(Sinharay 2010 convention, labeled; CRAN
CTTsub's0.1(1−max)ruledocumented but not implemented).
Documented divergences from CRAN
data.prepsilently allows non-union totals).collinear with total, non-finite moments, PRMSE out of [0,1]) instead of
propagating NaN.
s_hat_xuses CRAN's nonnegative-root convention even when the signedappendix correlation would be negative (doc-noted; guards do not force
cov(s_t, x_t) > 0).Adversarial process evidence
fixes, all applied before any code was written (e.g. duplicate-subscale
"parallel forms anchor" was proven invalid — corr(s,x)=1 makes tau divide by
zero — and became a rejection test; dominance downgraded to conditional).
the R oracle matched the crate to all pinned literals; four confirmed
findings, all fixed:
2^63 * 2wrap →checked_mul+dimension validation before any allocation.
n_persons < 3 or n_items < 4pre-boundary.doc corrected.
defensive PRMSE-range guard is not separately exercised).
Test evidence (mutation-resistant discipline)
Every assert reads crate output; fixture literals come from an independent
NumPy transcription of the R semantics (never calling the crate). The 10×4
fixture is deliberately asymmetric:
added_value_sx = [false, true], andsubscale 0's margin (0.00754 < 0.01) kills any mutant dropping the +0.01 rule.
cargo test -p mlsirm-core subscores#[ignore])#[ignore]500-rep Monte Carlo (augmented MSE ≤ observed MSE vs true subscore)pytest -k subscorem/(m−1)in alpha2^63persons, sparse10^9group index, zero-column array)ValueErrors, no panic/allocKnown unkillable identity, disclosed in-test:
PRMSE_sx ≥ PRMSE_sisalpha + tau²(1−r²) ≥ alpha, true for any tau — the discriminating anchor isthe
PRMSE_sx ≥ PRMSE_xhalf plus the pinned PRMSE_x literals.LLM-as-a-Judge relevance
Decides whether per-domain judge subscores carry diagnostic information beyond
the overall score, or whether reporting them would be statistically misleading
— directly applicable to evaluation-item quality management.