Skip to content

Add Luce Spectral Ranking (LSR / I-LSR) paired-comparison estimators - #292

Merged
seonghobae merged 3 commits into
seonghobae-bradley-terryfrom
seonghobae-lsr
Jul 31, 2026
Merged

Add Luce Spectral Ranking (LSR / I-LSR) paired-comparison estimators#292
seonghobae merged 3 commits into
seonghobae-bradley-terryfrom
seonghobae-lsr

Conversation

@seonghobae

@seonghobae seonghobae commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Implements Luce Spectral Ranking (LSR) and iterative LSR (I-LSR) for pairwise comparison data, following choix 0.4.1's lsr.py dense path (source READ; Maystre & Grossglauser, 2015, Fast and Accurate Inference of Plackett-Luce Models, NIPS — NOT READ, cited as described by choix).

Stacked on #291 (seonghobae-bradley-terry) — review only the last commit.

What

  • Rust core mlsirm_core::scaling::{lsr_pairwise, ilsr_pairwise} + LsrResult {params, weights, iterations}: LSR Markov chain (rate c/(w_i+w_j) on each loser->winner edge, alpha everywhere as regularizer), stationary distribution via Gaussian elimination with partial pivoting (sum-constraint row replaces the redundant stationarity equation), scaled to sum n; params = centered logs.
  • Post-solve guards (spec-review mandate): positivity/finiteness, sum ~ n, and pi . chain ~ 0 residual check — a custom elimination must prove stationarity, not just return something positive. Disconnected graphs at alpha=0 and overflow from huge counts/alpha raise errors instead of NaN (choix can emit NaN there).
  • I-LSR: choix NormOfDifferenceTest semantics (L1 <= tol*n, first check never fires, default max_iter=100). At alpha=0 the fixed point IS the Bradley-Terry MLE — verified against bradley_terry_mm to 4e-19 on the oracle fixtures (cross-algorithm anchor). For alpha>0 the two regularization paths deliberately differ (chain-rate vs Dirichlet-MAP; both follow their sources, documented).
  • PyO3 bindings (plain names) + thin Python wrappers fast_mlsirm.lsr_pairwise / ilsr_pairwise with full input validation before casts.

Spec-verify (BEFORE implementation)

Adversarial review of hand-derived spec + EXECUTED oracle: APPROVED-WITH-CHANGES; all 4 mandatory changes implemented (overflow tests for huge counts/alpha, statdist residual guard, alpha-diagonal equivalence documented, no-initial_params exposure documented).

Oracle (EXECUTED)

Exact-Fraction one-shot statdist + mpmath 50-digit I-LSR fixed points, cross-checked against pip choix 0.4.1 (<= 2.2e-13) and the shipped BT oracle (<= 4e-19). Exact pins e.g. fixture A statdist = [12/17, 45/34, 33/34]; iteration pins A=15 / C=17 at tol=1e-8.

Mutation kills (all EXECUTED, restore-verified)

Mutant Killed by
MU1 chain transpose one-shot A pins (dev 0.685)
MU2 drop diagonal subtraction positivity guard errs (exact statdist [9/2, 3/4, -9/4])
MU3 drop centering A params (dev 0.033)
MU4 statdist sum n -> 1 weights pins (params PROVEN invariant — documented limitation)
MU5 denominator (w_i+w_j) -> w_i I-LSR pins (one-shot UNOBSERVABLE at uniform weights — documented limitation; dev 0.331 from pass 2)
MU6 tol*n -> tol fixture C iteration pin 17 vs 18 (A does not separate: 15/15 — measured)

Tests

  • 5 Rust tests + MC-500 #[ignore] (all asserts read crate outputs); cargo 745 pass / 92 ignored.
  • 3 pytest tests (exact pins, BT cross-anchor, error contract incl. overflow); pytest 273 pass.

Adversarial impl-review outcome

  • Round 1 (files/lsr_impl_review.md): 1 HIGH finding — valid connected win matrices with globally huge counts (base*1e20, asymmetric 1e150) were falsely rejected: the Gaussian-elimination pivot threshold compared an O(1) sum-constraint pivot against a global scale dominated by O(count) stationarity rows.
  • Fix (commit 343fc2f): normalize the generator to unit max magnitude after the overflow guard (stationary distribution is invariant under global rescaling of transition rates). Regression asserts: base vs base*1e20 params/weights equal to 1e-12; asymmetric 1e150 ILSR finite. Overflow fixture replaced with a genuinely overflowing n=4 all-1.7e308 matrix (the old n=3 all-1e308 fixture never overflowed and is correctly accepted post-fix).
  • Targeted adversarial re-review (files/lsr_rereview.md): VERDICT CLEAN — invariance re-derived, guard ordering verified, regression tests confirmed to fail without the fix (revert probe), exact-fraction pins unaffected, pip choix cross-check on scaled inputs matches.

Implements choix 0.4.1's lsr.py dense pairwise path (source READ;
Maystre & Grossglauser 2015 NOT READ, cited as described by choix):
one-shot spectral estimate and iterative MLE from an n x n win-count
matrix. Rust core (mlsirm_core::scaling::{lsr_pairwise, ilsr_pairwise})
with Gaussian-elimination statdist guarded by positivity, sum, and
residual checks; overflow from huge counts/alpha raises instead of
returning NaN. I-LSR at alpha=0 reproduces the Bradley-Terry MLE
(cross-algorithm anchor vs bradley_terry_mm); alpha>0 regularization
semantics deliberately differ (chain-rate vs Dirichlet-MAP, per source).

Pins from an EXECUTED exact-Fraction/mpmath oracle cross-checked with
pip choix 0.4.1 (<= 2.2e-13). Six mutation kills EXECUTED (chain
transpose, dropped diagonal subtraction, dropped centering,
sum-n normalization via weights pins, denominator collapse via I-LSR
pins with the one-shot-unobservable limitation documented, tol*n vs tol
via a separating iteration-count fixture). MC-500 recovery test
(#[ignore]) passing. cargo 745 pass; pytest 273 pass.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ce1e2286-f8b9-463c-a7e3-2cf05bf8cccc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch seonghobae-lsr

Comment @coderabbitai help to get the list of available commands.

seonghobae and others added 2 commits July 26, 2026 23:14
… rejection

The relative pivot threshold in the stationary-distribution solve
compared an O(1) sum-constraint pivot against an O(max count) global
scale, falsely rejecting validly connected win matrices with globally
huge counts (impl-review finding). Normalize the generator to unit max
magnitude after the overflow guard; the stationary distribution is
invariant under global rescaling of transition rates.

Regression asserts: base vs base*1e20 params/weights equal to 1e-12;
asymmetric 1e150 ILSR finite. The overflow fixture is now a genuinely
overflowing n=4 matrix (row sums -> inf); the previous n=3 all-1e308
fixture never overflowed and is correctly accepted post-fix.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…atrix

Mirror of the Rust-side fixture change in 343fc2f: the n=3 all-1e308
matrix never overflowed and is correctly accepted after the
scale-invariance fix; use n=4 at 1.7e308 (row sums -> inf) and add a
Python-side scale-invariance regression assert.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@seonghobae
seonghobae merged commit 6909778 into seonghobae-bradley-terry Jul 31, 2026
6 checks passed
@seonghobae
seonghobae deleted the seonghobae-lsr branch July 31, 2026 12:37
@seonghobae

Copy link
Copy Markdown
Contributor Author

Note: this PR was squash-merged into a non-main stacked base, so its commits did not land on main via this PR. The feature set reached main via #374 (stack tip integration) after #290.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant