feat(detect): confirmatory DETECT dimensionality analysis (Zhang & Stout, 1999) - #222
Conversation
There was a problem hiding this comment.
Pull request overview
Adds confirmatory DETECT dimensionality analysis to the Rust core (mlsirm-core) with a thin PyO3/Python wrapper, exposing results as a DetectResult and pinning correctness against independent reference literals (plus guard/hostile-input tests).
Changes:
- Implement
mlsirm_core::detect::detect_analysis(conditional covariance estimation + DETECT/ASSI/RATIO/MADCOV100/MCOV100 indices) and export the new module. - Expose
detect_analysisvia PyO3 and a Python wrapper (fast_mlsirm.detect_analysis) returning a typedDetectResult. - Add Rust and Python tests that pin index outputs and per-pair ccovs to independent NumPy transcription literals, plus rejection/hostile-label coverage.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
crates/mlsirm-core/src/lib.rs |
Exposes the new detect module from the Rust core. |
crates/mlsirm-core/src/detect.rs |
Implements DETECT analysis logic and rustdoc describing scope/source parity. |
crates/fast-mlsirm-py/src/lib.rs |
Adds PyO3 binding for detect_analysis returning dict outputs to Python. |
python/fast_mlsirm/detect.py |
Adds Python wrapper + DetectResult dataclass with input validation/marshaling. |
python/fast_mlsirm/__init__.py |
Re-exports detect_analysis / DetectResult as part of the public API. |
tests/unit/detect_tests.rs |
Adds Rust-side fixture and behavioral tests (including ignored Monte Carlo). |
tests/test_paper_features.py |
Adds Python-side parity + trust-boundary rejection tests for DETECT. |
CHANGELOG.md |
Documents the new DETECT feature, scope, and test evidence. |
💡 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. |
All three review comments addressed in commits
|
|
@copilot resolve the merge conflicts on this branch. |
Merge conflicts resolved in commit |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
crates/mlsirm-core/src/detect.rs:157
ccov_sum_scoreallocates and sorts a fullordervector every call. Since it is invoked for the rest-score pass for every item pair, this makes DETECT scale as O(n_pairs * n_persons log n_persons) and can become a major bottleneck for realistic item counts. Consider replacing sort-based grouping with a bucketed aggregation over the integer score range (raw sums in 0..n_items, rest scores in 0..n_items-2), or at least reusing a scratchorderbuffer to avoid per-pair allocations.
fn ccov_sum_score(score: &[i64], xi: &[f64], xj: &[f64]) -> f64 {
let n = score.len() as f64;
let mut order: Vec<usize> = (0..score.len()).collect();
order.sort_unstable_by_key(|&p| score[p]);
ccov_sum_score_presorted(score, xi, xj, n, &order)
8dc78bc to
5425b1d
Compare
097dacd to
eba0445
Compare
5425b1d to
95c215e
Compare
eba0445 to
fb2cd6e
Compare
fb2cd6e to
95b856f
Compare
…out, 1999) Rebased onto main after #220 squash merge. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…Lee 2010 via cacIRT) Rebased onto main after #222 squash merge; keeps only classification feature files. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Summary
Iteration 5 of the autonomous paper-implementation loop. Adds confirmatory DETECT dimensionality analysis (Zhang & Stout, 1999) to
mlsirm-corewith a thin Python wrapper, stacked on #221 (Haberman subscores).DETECT estimates pairwise conditional covariances of binary items given the latent composite (proxied by sum scores) and scores a hypothesized item clustering: positive within-cluster / negative between-cluster conditional covariances indicate multidimensional simple structure.
What's implemented
mlsirm_core::detect::detect_analysis— sum-score-conditioned, bias-corrected conditional covariances and the DETECT / ASSI / RATIO / MADCOV100 / MCOV100 indices against a known clustering.ccov_ij = (cov | total + cov | rest)/2, rest scoreS − X_i − X_j; per-group ML covariance (divide byn); group weightsfreq/Nrecomputed per conditioning vector; singleton groups contribute 0.DETECT = 100·mean(ccov·δ),ASSI = mean(sign(ccov)·δ),RATIO = Σ(ccov·δ)/Σ|ccov|,MADCOV100 = 100·mean|ccov|,MCOV100 = 100·mean(ccov);δ = +1same-cluster else−1; labels opaque, equality-only.detect_analysis+ Python wrapperfast_mlsirm.detect_analysis→DetectResult(validation and marshaling only; all numerics in Rust).Source verification (paper-first discipline)
Zhang & Stout (1999a/b) are paywalled; the read oracle is the CRAN
sirtR source, transcribed line-by-line:detect.index.R,ccov.np.R,ccov_np_compute_ccov_sum_score.R,conf.detect.R(Robitzsch, 2024). Zhang & Stout (1999), Stout et al. (1996), Zhang (2007), Jang & Roussos (2007) are cited as cited in Robitzsch (2024). The adversarial spec-verify pass returned NO-GO on the first spec revision (it misread sirt's defaultscale_score=TRUEz-standardize+round path); the scope was reduced to the explicitscale_score=FALSEraw-sum-score path and all 8 required fixes were applied before implementation.Documented scope exclusions: kernel-smoothed default conditioning, missing data (sirt pairwise-deletes with a
rowSumsna.rm quirk),sqrt(N)-weighted variants (identical to unweighted under complete data), exploratory cluster search, polytomous DETECT.RATIO0/0 (NaN in R) is rejected with an error.Evidence
cargo test -p mlsirm-core)#[ignore]): 2D simple structure vs unidimensional separationpytest -k detect_analysis)Disclosed test limitations: the fixture cannot discriminate
scale_score(unique-value grouping is invariant to monotone transforms unless rounding merges groups) — the scope statement pins that contract;weighted ≡ unweightedunder complete data is an identity (variants cut rather than shipped untestable);DETECT == MCOV100for a single cluster is an identity anchored by the multi-cluster fixtures.Trust boundary
checked_muloverflow guards in both core and binding; responses restricted to exact 0/1; cluster labels never used as indices (hostilei64::MIN/i64::MAXlabels tested); Python wrapper rejects float labels outside i64 range before casting.Stacked on #221 → base
seonghobae-haberman-subscores.