-
Notifications
You must be signed in to change notification settings - Fork 1
feat: name leftover-map length on leftover pairs (v2.12.25) #533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e01e911
ef38a84
d6e6fa1
8798445
6c8e378
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| ## 2.12.25 — Leftover-map length | ||
|
|
||
| - Persist leftover-map lengths `‖ξ‖` and `‖ζ‖` on leftover | ||
| post–criterion pairs (ADR 0181). After `make seed`, closest and | ||
| farthest leftover pairs sit above the member list with `‖ξ‖` and | ||
| `‖ζ‖` next to leftover-map distance `d`; click opens that post. | ||
| Omit the badges when lengths are missing. Never invent a leftover | ||
| score. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| # ADR 0181 — Name leftover-map length on period-report pair rows | ||
|
|
||
| **Decision status:** Accepted | ||
| **Date:** 2026-08-24 | ||
|
|
||
| Amends [ADR 0048](0048-persist-lsirm-leftover-pairs.md) and | ||
| [ADR 0049](0049-leftover-pair-report-ui.md). | ||
|
|
||
| ## Context | ||
|
|
||
| ADR 0048 already persists leftover-map distance `d = ‖ξ_p − ζ_i‖` and | ||
| leftover residual `R = Y − E[Y|θ, item]` on `report_leftover_pair`. | ||
| ADR 0049 already renders closest and farthest pairs above the member | ||
| list and opens the named post. Distance is the Jeon et al. (2021, | ||
| eq. 3) map gap. Gabriel (1971) also names leftover-map vector lengths | ||
| `‖ξ‖` and `‖ζ‖`. Hiding those lengths lets a buyer read a close | ||
| leftover-map pair as leftover-map aligned magnitude, or a distant pair | ||
| as large leftover-map displacement, without the polar magnitude. | ||
|
|
||
| A close leftover-map pair can sit at the origin (`‖ξ‖ = ‖ζ‖ = 0`) or | ||
| share a non-origin position (`d = 0` with positive length). Those are | ||
| different leftover-map facts. | ||
|
|
||
| This increment does not persist leftover-map coordinates, does not name | ||
| observed `Y` / expected `E`, does not name leftover-map cosine, does | ||
| not name leftover-map inner product, does not name leftover-map rank, | ||
| does not split leftover-map distance onto two axes, and does not land | ||
| Post quality on the leftover criterion. | ||
|
|
||
| The unprotected-stack reconstructions for neighbouring leftover facts | ||
| use 0121–0180. This protected-main increment uses **0181** so it does | ||
| not collide with leftover-map cosine (0180), leftover-map inner product | ||
| (0179), leftover residual disclosure (0178), leftover observed `Y` / | ||
| expected `E` (0177), leftover-map rank (0172), two-axis leftover-map | ||
| distance (0166), leftover coverage (0168), leftover-map axis share | ||
| (0148), or leftover interaction-map persistence (0121). | ||
|
|
||
| ## Decision | ||
|
|
||
| Each leftover pair names `leftover_map_person_length` and | ||
| `leftover_map_item_length` — the Euclidean lengths `‖ξ‖` and `‖ζ‖` of | ||
| the leftover-map person and item coordinates that produced leftover-map | ||
| distance `d`. Migration `0181` is the single source of the columns on | ||
| every install path, fresh or existing -- shipped migrations (`0001` / | ||
| `0012`) are never edited after the fact. The columns are nullable so | ||
| older leftover rows keep distance and residual without fabricating | ||
| lengths. Fallback pairs that have no complete-case leftover map omit | ||
| the values rather than inventing them. Origin coordinates persist | ||
| length `0` because that is the measured leftover-map magnitude. | ||
|
|
||
| The pair button shows `‖ξ‖ {person}` and `‖ζ‖ {item}` next to | ||
| leftover-map distance `d` when both values are finite. Next action: | ||
| leftover-map length names leftover-map magnitude independently of | ||
| leftover-map distance; open this post to read the named criterion. A | ||
| missing or non-finite length omits the badges and keeps the existing | ||
| closest/farthest next action. Do not invent a leftover score. Do not | ||
| invent a theta. | ||
|
|
||
| ## Consequences | ||
|
|
||
| `GET /api/reports/{grouping}/{period}` returns | ||
| `leftover_map_person_length` and `leftover_map_item_length`. After | ||
| `make seed`, closest and farthest leftover pairs sit above the member | ||
| list with named `‖ξ‖` and `‖ζ‖` next to `d`; click opens that post. | ||
| Hidden posts stay hidden. | ||
|
|
||
| ## Related | ||
|
|
||
| Independent of leftover interaction-map persistence, leftover-criterion | ||
| evaluation landing, leftover residual disclosure, leftover observed | ||
| `Y` / expected `E`, leftover-map complete-case coverage, leftover-map | ||
| axis share, leftover pairs on the grouping comparison strip, two-axis | ||
| leftover-map distance, leftover-map rank, leftover-map inner product, | ||
| and leftover-map cosine. | ||
|
|
||
| ## References | ||
|
|
||
| Gabriel, K. R. (1971). The biplot graphic display of matrices with | ||
| application to principal component analysis. *Biometrika, 58*(3), | ||
| 453–467. https://doi.org/10.1093/biomet/58.3.453 | ||
|
|
||
| Jeon, M., Jin, I. H., Schweinberger, M., & Baugh, S. (2021). Mapping | ||
| unobserved item–respondent interactions: A latent space item response | ||
| model with interaction map. *Psychometrika, 86*(2), 378–403. | ||
| https://doi.org/10.1007/s11336-021-09762-5 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { | ||
| formatLeftoverMapItemLength, | ||
| formatLeftoverMapLength, | ||
| formatLeftoverMapPersonLength, | ||
| } from "./leftoverMapLength"; | ||
|
|
||
| describe("formatLeftoverMapLength", () => { | ||
| it("names leftover-map length without inventing a leftover score", () => { | ||
| expect(formatLeftoverMapPersonLength(0)).toBe("‖ξ‖ 0.00"); | ||
| expect(formatLeftoverMapItemLength(1.25)).toBe("‖ζ‖ 1.25"); | ||
| expect(formatLeftoverMapLength(0.4)).toBe("0.40"); | ||
| expect(formatLeftoverMapPersonLength(0.4)).toBe("‖ξ‖ 0.40"); | ||
| expect(formatLeftoverMapItemLength(0.9)).toBe("‖ζ‖ 0.90"); | ||
| }); | ||
|
|
||
| it("omits the badge when length is missing, negative, or non-finite", () => { | ||
| expect(formatLeftoverMapPersonLength(null)).toBeNull(); | ||
| expect(formatLeftoverMapItemLength(undefined)).toBeNull(); | ||
| expect(formatLeftoverMapLength(Number.NaN)).toBeNull(); | ||
| expect(formatLeftoverMapLength(Number.POSITIVE_INFINITY)).toBeNull(); | ||
| expect(formatLeftoverMapLength(-0.01)).toBeNull(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| /** Gabriel leftover-map lengths ``‖ξ‖`` and ``‖ζ‖`` after IRT main effects. */ | ||
|
|
||
| export const LEFTOVER_MAP_LENGTH_ACTION = | ||
| "Leftover-map length ‖ξ‖ {person} and ‖ζ‖ {item} names leftover-map magnitude independently of leftover-map distance. Open this post to read {criterion}."; | ||
|
|
||
| export function formatLeftoverMapLength(value: number | null | undefined): string | null { | ||
| if (value == null || !Number.isFinite(value) || value < 0) { | ||
| return null; | ||
| } | ||
| return value.toFixed(2); | ||
| } | ||
|
|
||
| export function formatLeftoverMapPersonLength(value: number | null | undefined): string | null { | ||
| const formatted = formatLeftoverMapLength(value); | ||
| return formatted === null ? null : `‖ξ‖ ${formatted}`; | ||
| } | ||
|
|
||
| export function formatLeftoverMapItemLength(value: number | null | undefined): string | null { | ||
| const formatted = formatLeftoverMapLength(value); | ||
| return formatted === null ? null : `‖ζ‖ ${formatted}`; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| -- ADR 0181: persist leftover-map lengths ‖ξ‖ and ‖ζ‖ on leftover | ||
| -- post–criterion pairs. Distance stays Euclidean leftover-map d. | ||
| -- Upgrade columns are nullable so older leftover rows keep distance | ||
| -- and residual without fabricating lengths. This migration is the | ||
| -- single source of the columns on fresh and existing installations. | ||
|
|
||
| alter table report_leftover_pair | ||
| add column if not exists leftover_map_person_length numeric, | ||
| add column if not exists leftover_map_item_length numeric; | ||
|
|
||
| do $$ | ||
| begin | ||
| if not exists ( | ||
| select 1 | ||
| from pg_constraint | ||
| where conname = 'report_leftover_pair_map_length_nonnegative' | ||
| ) then | ||
| alter table report_leftover_pair | ||
| add constraint report_leftover_pair_map_length_nonnegative | ||
| check ( | ||
| (leftover_map_person_length is null or leftover_map_person_length >= 0) | ||
| and (leftover_map_item_length is null or leftover_map_item_length >= 0) | ||
| ); | ||
| end if; | ||
| end $$; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| -- Reverse 0181. Leftover distance and residual stay on the pair row. | ||
|
|
||
| alter table report_leftover_pair | ||
| drop constraint if exists report_leftover_pair_map_length_nonnegative; | ||
|
|
||
| alter table report_leftover_pair | ||
| drop column if exists leftover_map_person_length, | ||
| drop column if exists leftover_map_item_length; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -225,6 +225,10 @@ def test_leftover_residual_biplot_separates_aligned_and_opposed_cells() -> None: | |
| } | ||
| assert farthest.leftover_residual == pytest.approx(-2.0) | ||
| assert farthest.leftover_distance == pytest.approx(2.0 * np.sqrt(2.0), rel=1e-6) | ||
| assert closest.leftover_map_person_length == pytest.approx(0.0, abs=1e-9) | ||
| assert closest.leftover_map_item_length == pytest.approx(0.0, abs=1e-9) | ||
| assert farthest.leftover_map_person_length == pytest.approx(np.sqrt(2.0), rel=1e-6) | ||
| assert farthest.leftover_map_item_length == pytest.approx(np.sqrt(2.0), rel=1e-6) | ||
|
Comment on lines
+228
to
+231
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Leftover-length test reads attributes that never exist The new assertions read Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
|
|
||
| def test_leftover_missing_cells_are_not_treated_as_zero() -> None: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 Frontend formatter not wired into UI
formatLeftoverMapLengthand its person/item variants are exported and unit-tested, but no component imports them. The badges and i18n the PR describes are absent, so the formatter renders nothing to a buyer.Was this helpful? React with 👍 or 👎 to provide feedback.