feat(ksirt): kernel-smoothing nonparametric IRT (Ramsay 1991) - #220
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new nonparametric, kernel-smoothed IRT analysis path (ksIRT) to the Rust core and exposes it through a thin Python wrapper, enabling OCC/expected-score curves derived from rank-based ordinal ability estimates for item-quality diagnostics.
Changes:
- Implement
mlsirm_core::ksirtkernel-smoothing OCC estimation (Gaussian/quadratic/uniform) with rank→normal-quantile ability surrogates and Silverman-rule bandwidths. - Add PyO3 binding (
ksirt_occ) and Python API (fast_mlsirm.ksirt_analysis,KsirtResult) with validation and marshaling. - Add Rust + Python tests that pin fixture exactness and basic recovery/guard behavior; update changelog.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/ksirt_tests.rs | New Rust unit tests with hand fixtures and invariants for ksIRT outputs. |
| tests/test_paper_features.py | Python-level fixture/recovery tests and wrapper validation guards for ksirt_analysis. |
| python/fast_mlsirm/ksirt.py | New Python wrapper (ksirt_analysis) + KsirtResult dataclass. |
| python/fast_mlsirm/init.py | Exposes ksirt_analysis / KsirtResult from the package root. |
| crates/mlsirm-core/src/mokken.rs | Widens visibility of normal_upper_quantile for reuse by ksIRT. |
| crates/mlsirm-core/src/lib.rs | Registers the new ksirt Rust module. |
| crates/mlsirm-core/src/ksirt.rs | New Rust implementation of ksIRT OCC smoothing and expected-score curves. |
| crates/fast-mlsirm-py/src/lib.rs | Adds PyO3 ksirt_occ binding returning dict outputs to Python. |
| CHANGELOG.md | Documents the new kernel-smoothing nonparametric IRT feature and scope limits. |
💡 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. |
Addressed in 1589168. |
d0eb420 to
a7ef4db
Compare
8dc78bc to
5425b1d
Compare
Rebuild on top of updated main (post-audit) preserving poly.rs/fitstats.rs correctness from audit PR #240. KSIRT-unique changes only: - New: ksirt.rs, subscores.rs, ksirt.py, subscores.py, tests - Modified infra: lib.rs, CHANGELOG, __init__.py, py-bindings - mokken.rs: make normal_upper_quantile pub(crate) for KSIRT use Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
5425b1d to
95c215e
Compare
…out, 1999) Rebased onto main after #220 squash merge. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Summary
Iteration 3 of the autonomous paper-implementation loop (stacked on #219): kernel-smoothing nonparametric IRT (Ramsay, 1991) as
mlsirm_core::ksirt+fast_mlsirm.ksirt_analysis.Estimates option characteristic curves (OCCs) by Nadaraya–Watson kernel regression of option indicators on rank-based ordinal ability estimates, plus expected item score and expected total score curves. For LLM-as-a-Judge item-quality management this reveals non-monotone or poorly discriminating evaluation items without assuming a parametric response model.
Algorithm (all verified against read sources)
theta_i = qnorm(rank(total_i)/(n+1)), tiesfirst(stable sort, original order within ties)qequally spaced pointsqnorm(1/(n+1)) … qnorm(n/(n+1))1.06 · n^(−1/5)(σ=1 on the normal metric)K((θ_s−θ̂_i)/h) / Σ_i K(·); gaussian / quadratic / uniforme_j(θ_s) = Σ_l x_jl · p̂_jl(θ_s); total =Σ_j e_jSource verification: READ = Mazza, Punzo & McGuire (2014), JSS 58(6), doi:10.18637/jss.v058.i06 (Sections 2–2.3) + KernSmoothIRT R/C++ source (ksIRT.R, smoother3.cpp, CrossV.cpp, getweight.R) line-by-line. NOT READ (cited only as-cited-in) = Ramsay (1991), Silverman (1986), Nadaraya (1964), Watson (1964).
Adversarial spec review → REDUCED-SCOPE (applied)
Test evidence
Every assert reads crate outputs against independently derived constants; each test documents "crate value read" + "mutant killed".
#[ignore]MC)1.06·4^(−1/5).rev()theta iterationn+1 → n1.06 → 1.0Unkillable mutants (documented, not claimed): kernel-argument sign flip (symmetric kernels) and multiplicative kernel constants (cancelled by NW normalization) are identities; the test header names them and the discriminating anchors that would require asymmetric kernels.
Adversarial impl review → 2 confirmed defects, both fixed
nevalpointscould abort the interpreter via Rusthandle_alloc_error→ wrapper now caps at 100 000 (ValueError), with a guard test.Stacking
Base =
seonghobae-mokken-scale-analysis(#219). Merge order: #218 → #219 → this.