feat(mokken): Mokken scale analysis with Loevinger H and AISP - #219
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds two new psychometric item-quality / calibration capabilities to fast-mlsirm: (1) nonparametric Mokken scale analysis (Loevinger H, Mokken Z, AISP “search normal”) and (2) Many-Facet Rasch Model (MFRM) rater-severity calibration, both implemented in the Rust core with thin Python wrappers and backed by Rust + Python test coverage.
Changes:
- Add
mlsirm_core::mokken(coefficients + Z + AISP) and expose it via PyO3 +fast_mlsirm.mokken_analysis. - Add
mlsirm_core::facets(MFRM via marginal-ML EM + connectedness flag) and expose it via PyO3 +fast_mlsirm.fit_facets. - Add extensive Rust unit tests and Python “paper features” tests for both features; update public exports and changelog.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
crates/mlsirm-core/src/mokken.rs |
New Rust Mokken implementation (H, Z, AISP) and internal tests hook. |
crates/mlsirm-core/src/facets.rs |
New Rust MFRM EM implementation with connectedness diagnostic and tests hook. |
crates/mlsirm-core/src/lib.rs |
Exposes new core modules. |
crates/fast-mlsirm-py/src/lib.rs |
Adds PyO3 bindings for fit_facets, mokken_coef_h, mokken_aisp. |
python/fast_mlsirm/mokken.py |
Python validation + marshaling wrapper returning MokkenResult. |
python/fast_mlsirm/facets.py |
Python validation + marshaling wrapper returning FacetsFit. |
python/fast_mlsirm/__init__.py |
Exports new public API symbols. |
tests/unit/mokken_tests.rs |
Rust unit tests for Mokken (oracle, anchors, AISP fixtures). |
tests/unit/facets_tests.rs |
Rust unit tests for facets (FD anchors, recovery, sparse/disconnected). |
tests/test_paper_features.py |
Python integration/regression tests for facets + mokken wrappers. |
CHANGELOG.md |
Documents both new features and their scope/verification summary. |
Comments suppressed due to low confidence (2)
crates/mlsirm-core/src/mokken.rs:214
normal_upper_quantileonly has adebug_assert!(q > 0 && q < 1)after computingq = 1 - p. For very smallp,qcan round to exactly1.0(even whenp > 0), tripping the debug assert and/or sending later code down an invalid path. Add explicit runtime bounds handling forpand avoid asserting onq.
fn normal_upper_quantile(p: f64) -> f64 {
// invert the CDF at 1 - p using Peter Acklam's approximation
let q = 1.0 - p;
debug_assert!(q > 0.0 && q < 1.0);
crates/mlsirm-core/src/mokken.rs:260
- In the upper-tail branch,
ris computed from(1.0 - q).ln()whereq = 1 - p. Whenpis small enough thatqrounds to1.0, this becomesln(0)and can yieldinf/NaNquantiles. Since(1 - q)is mathematicallyphere, computerfromp.ln()directly to avoid the cancellation/rounding problem.
} else {
let r = (-2.0 * (1.0 - q).ln()).sqrt();
-((((((C[0] * r + C[1]) * r + C[2]) * r + C[3]) * r + C[4]) * r + C[5])
/ ((((D[0] * r + D[1]) * r + D[2]) * r + D[3]) * r + 1.0))
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@copilot Fix the code for all comments in this review thread. When a review comment includes a suggested change, apply the suggestion exactly. Do not make changes beyond what is described in the linked review thread. |
Fixed in commit
All 11 mokken tests pass; CodeQL scan is clean. |
|
@copilot resolve the merge conflicts on this branch. |
Merge conflicts resolved in commit
|
Implements sample Loevinger scalability coefficients H_ij, H_i, H, their Mokken Z statistics, and the automated item selection procedure (AISP, 'search normal') in Rust (mlsirm_core::mokken) and Python (fast_mlsirm.mokken_analysis, MokkenResult). Formula verified line-by-line against the mokken R package (van der Ark, 2007, doi:10.18637/jss.v020.i11): H_ij = S_ij / Smax_ij where Smax uses the comonotone (sorted-column) coupling. References: - Mokken, R. J. (1971). A Theory and Procedure of Scale Analysis. - van der Ark, L. A. (2007). doi:10.18637/jss.v020.i11 - Straat, J. H. et al. (2013). doi:10.1007/s00357-013-9133-6 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
d0eb420 to
a7ef4db
Compare
Mokken scale analysis: Loevinger H, Mokken Z, and AISP
Adds nonparametric Mokken scale analysis (Mokken, 1971, as cited in van der Ark, 2007) to the Rust core with a thin Python wrapper. Stacked on #218.
What
mlsirm_core::mokken(new): Loevinger scalability coefficientsHij,Hi,H; Mokken Z statistics; and the automated item selection procedure (AISP, "search normal"). Dichotomous and polytomous complete integer data.mokken_coef_h,mokken_aisp.fast_mlsirm.mokken_analysis→MokkenResult(hij/hi/h, zij/zi/z, per-item AISP scale labels; 0 = unscalable).Source verification (spec-verify workflow ran before implementation)
internalFunctions.R::coefHTiny,coefZ.R,search.normal.R):Smax= sorted-column (comonotone) covariance;Hi/H= ratios of pairwise sums; per-scale Bonferroni-adjusted Z gatesalpha/(K1(K1-1)/2 + sum of step candidate counts); Criterion-1 negative-Hijexclusion; eps tie-break on the larger pair index.Hidenominator and a fixed-restscore-marginal reading of van der Ark Eq. 2 (counterexample: X1=X2=[0,0,1,1], X3=[0,1,0,1] → 1/3 vs 2/3); the module doc records the counterexample and implements the R-package form.Test evidence (every assert reads crate outputs)
pairwise/h_subsetH = 1#[ignore]500-rep MC (normal + skewed traits): mean-H band + ≥95% one-scale recoveryKnown limitation (stated, not hidden): the MC test cannot pin exact constants; algebra anchors are the oracle and hand fixtures.
Adversarial impl-review ran post-implementation: confirmed all math OK by independent computation; its two findings (tie-break fidelity vs R's eps rule; a mismatched unread-source citation) are fixed in this PR, with the tie-break now pinned by its own mutation-verified test.
cargo test -p mlsirm-core: 437 passed. Python suite targeted run: 2 passed.