feat: name leftover-map unexplained share on leftover pairs (v2.12.27) - #537
feat: name leftover-map unexplained share on leftover pairs (v2.12.27)#537seonghobae wants to merge 5 commits into
Conversation
Persist unexplained leftover share s = U_c² / R̃² of centered leftover on leftover post–criterion pairs (ADR 0183). Two-axis reconstruction R̂_c and centered leftover U_c stay internal. A rank-1 cell stores share 0 even when mean(R) ≠ 0. Fallback omits the share. Do not persist leftover-map unexplained leftover U or reconstruction R̂.
|
Merge contract for exact head
Requested reviewers stay empty: the only org collaborator is the author, so review cannot be requested from this account. Independent of leftover stacks already in review, including #535 leftover-map unexplained leftover. Devin’s grand-mean-in-U note on #535 is addressed here by centering leftover before naming share, not by patching that head. |
|
Warning Review limit reachedNext included review available in 40 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (27)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| filled_sq = float(filled * filled) | ||
| unexplained_sq = float(unexplained * unexplained) | ||
| if filled_sq > _LEFTOVER_SINGULAR_FLOOR: | ||
| return float(unexplained_sq / filled_sq) | ||
| if unexplained_sq <= _LEFTOVER_SINGULAR_FLOOR: | ||
| return 0.0 | ||
| return None |
There was a problem hiding this comment.
📝 Info: Per-cell share can exceed 1
_unexplained_leftover_share computes a per-cell ratio U_c²/R̃², not an aggregate. Two-axis reconstruction can overshoot one cell with opposite sign, so unexplained can exceed |filled| and s can exceed 1. The check constraint only rejects negatives and the frontend accepts any finite non-negative value, so a badge above 1.00 can render. Consistent with ADR 0183, which only forbids negative or non-finite shares.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Acknowledged — per-cell share can exceed 1 when two-axis reconstruction overshoots a cell. ADR 0183 only forbids a negative or non-finite share; the check constraint and badge follow that. Not clamping to 1.00 in this increment (that would hide honest overshoot). Author will not self-approve.
|
write-probe cycle 2026-08-24T08:04KST |
|
Cycle 2026-08-24T08:15KST write retry.
|
|
Independent next leftover gap is now open as #539 (explained share |
Drop the unused oidcReturnUrl import (pre-existing on main) and accept an optional AdminPanel accessToken so the login screen typechecks. Do not change leftover unexplained-share arithmetic.
|
Pushed tsc repair |
| let nextAction: string; | ||
| if (rankBadge !== null && observedExpected !== null) { | ||
| if (rankBadge !== null && observedExpected !== null && shareBadge !== null) { | ||
| nextAction = | ||
| pair.leftover_map_rank === 0 | ||
| ? tf( | ||
| "Leftover map rank 0 means no leftover structure after IRT main effects. Read observed Y {observed}, expected E {expected}, and unexplained share {value}, then open this post.", | ||
| { | ||
| observed: Number(pair.observed_response).toFixed(2), | ||
| expected: Number(pair.expected_response).toFixed(2), | ||
| value: Number(pair.leftover_map_unexplained_share).toFixed(2), | ||
| }, | ||
| ) | ||
| : tf( | ||
| "Read leftover map rank {rank}, observed Y {observed}, expected E {expected}, and unexplained share {value} after IRT main effects, then open this post.", | ||
| { | ||
| rank: String(pair.leftover_map_rank), | ||
| observed: Number(pair.observed_response).toFixed(2), | ||
| expected: Number(pair.expected_response).toFixed(2), | ||
| value: Number(pair.leftover_map_unexplained_share).toFixed(2), | ||
| }, | ||
| ); | ||
| } else if (rankBadge !== null && observedExpected !== null) { | ||
| nextAction = | ||
| pair.leftover_map_rank === 0 | ||
| ? tf( |
There was a problem hiding this comment.
📝 Info: Share badge shows but next-action text can omit it
In LeftoverPairList.tsx the share badge renders whenever shareBadge is non-null, but the next-action sentence names the share only when both rankBadge and observedExpected are also present. A pair carrying a share but lacking rank or observed/expected shows the badge while the sentence stays silent. Freshly computed pairs always carry an integer rank, so this only reaches legacy rows without rank.
Was this helpful? React with 👍 or 👎 to provide feedback.
| def _unexplained_leftover_share(filled: float, reconstruction: float) -> float | None: | ||
| """Return ``s = U_c² / R̃²`` when both terms are finite; otherwise omit. | ||
|
|
||
| ``filled`` is centered leftover ``R̃ = R − center``. Gabriel | ||
| reconstruction recovers that centered matrix, not raw residual | ||
| ``R``. Using raw ``R`` in the denominator (or subtracting | ||
| reconstruction from ``R``) leaves the grand mean inside the named | ||
| leftover and makes a fully reconstructed rank-1 cell look | ||
| unexplained whenever ``center ≠ 0``. | ||
| """ | ||
| if not np.isfinite(filled) or not np.isfinite(reconstruction): | ||
| return None | ||
| unexplained = filled - reconstruction | ||
| if not np.isfinite(unexplained): | ||
| return None | ||
| filled_sq = float(filled * filled) | ||
| unexplained_sq = float(unexplained * unexplained) | ||
| if filled_sq > _LEFTOVER_SINGULAR_FLOOR: | ||
| return float(unexplained_sq / filled_sq) | ||
| if unexplained_sq <= _LEFTOVER_SINGULAR_FLOOR: | ||
| return 0.0 | ||
| return None |
There was a problem hiding this comment.
📝 Info: Centering-first share matches two-axis reconstruction
In leftover_pairs.py, reconstruction is the inner product of the padded two-axis coordinates, the rank-2 approximation of the same centered matrix the SVD factorized, and _unexplained_leftover_share uses centered leftover in both numerator and denominator. Rank-1/rank-2 maps reconstruct exactly, so share is 0 even when mean(R) != 0; only rank>=3 yields nonzero share. The floor guard returns 0.0 only when the unexplained part is also negligible, otherwise omits, avoiding unstable ratios. The result is always non-negative, satisfying the DB check.
Was this helpful? React with 👍 or 👎 to provide feedback.
# Conflicts: # AGENTS.md # ARCHITECTURE.md # CHANGELOG.md # CLAUDE.md # backend/tests/test_api.py # docs/adr/0003-fast-mlsirm-report-integration.md # frontend/package.json # lineageweave/leftover_pairs.py # pyproject.toml # tests/test_leftover_pairs.py # tests/test_period_report.py # tests/test_schema.py # uv.lock
| cur.execute((migrations / "0169_report_leftover_map_axis.sql").read_text()) | ||
| cur.execute((migrations / "0183_report_leftover_map_unexplained_share.sql").read_text()) |
There was a problem hiding this comment.
🔍 Seed adds previously-missing axis migration
seed_demo_data.py adds 0169_report_leftover_map_axis.sql to the seed migration list. _persist_seed_period_report already inserts into report_leftover_map_axis, so before this change make seed would fail at that insert because the table was never created. This PR fixes that gap while adding 0183.
Was this helpful? React with 👍 or 👎 to provide feedback.
| person, | ||
| item, | ||
| max(distance, 0.0), | ||
| None, | ||
| ) | ||
| ) |
There was a problem hiding this comment.
📝 Info: Rank-0 origin map yields share 0.0, fallback yields None
For a constant (rank-0 after centering) complete-case rectangle, _complete_case_positions returns zeros arrays rather than None, so candidates are built with filled=0, reconstruction=0, giving share 0.0 with rank 0. The distinct fallback path (no complete-case rectangle at all) omits the share as None. Both rank-0 cases are handled distinctly per ADR 0183.
(Refers to this code)
Was this helpful? React with 👍 or 👎 to provide feedback.
Buyer next action
After
make seed, leftover post–criterion pairs sit above the member list with unexplained leftover shareU²/R̃²next to leftover-map distanced. Click the pair to open that post. The share badge is omitted when the pair has no complete-case leftover map (fallback rows do not invent a share). Never invent leftover score or theta.A leftover residual
Ris not the leftover the two leftover-map axes do not reconstruct. Unexplained leftover share namess = U_c² / R̃²of centered leftoverR̃ = R − centerafter two-axis reconstructionR̂_c = ξ_{1:2} · ζ_{1:2}(U_c = R̃ − R̂_c). Using rawRin the denominator (orU = R − recon) leaves the grand mean inside the named leftover, so a fully reconstructed rank-1 cell would look unexplained whenevercenter ≠ 0. This PR centers first.R̂_candU_cstay internal and are not persisted.Scope (this PR only)
leftover_map_unexplained_shareon leftover pair rowsleftover_map_unexplained_sharewhen complete-case ξ, ζ exist (s = U_c² / R̃²; unused axes pad with 0; hidden SVD axes after the second are dropped)U²/R̃²badge + named next action; i18n en/ko/zh/ja/vimean(R) ≠ 0; rank-3 synthetic residual provess ≠ R,≠ leftover_distance, and≠uncenteredU; leftover_distance stays full-rank Euclideans = 0; fallback omits shareleftover_map_unexplainedorleftover_map_reconstructionaccessToken(login screen has no token) and drop unusedoidcReturnUrlimport so leftover UI typechecksIndependent of leftover stacks already in review
Do not mix into:
Merge contract
9a6395546b03f6edc040492fef8f597bdca94212APPROVErequired (Copilot or human who is not the author)