-
Notifications
You must be signed in to change notification settings - Fork 1
feat: name leftover-map inner product on leftover pairs (v2.12.23) #531
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
c979331
e359dcd
5169434
0214a40
096d618
ecbc486
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,7 @@ | ||
| ## 2.12.23 — Leftover-map inner product | ||
|
|
||
| - Persist leftover-map inner product `ξ·ζ` on leftover post–criterion | ||
| pairs (ADR 0179). After `make seed`, closest and farthest leftover | ||
| pairs sit above the member list with `ξ·ζ` next to leftover-map | ||
| distance `d`; click opens that post. Omit the badge when the inner | ||
| product is missing. Never invent a leftover score. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # ADR 0179 — Name leftover-map inner product 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 reconstructs the leftover cell as | ||
| the inner product `ξ_p · ζ_i`. Hiding that inner product lets a buyer | ||
| read a distant map pair as a large leftover cell, or a close pair as a | ||
| reconstructed leftover, without the reconstructed value. | ||
|
|
||
| This increment does not persist leftover-map coordinates, does not name | ||
| observed `Y` / expected `E`, 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 0162–0178. This protected-main increment uses **0179** so it does | ||
| not collide with 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_inner_product` — the Gabriel inner | ||
| product `ξ·ζ` of the leftover-map person and item coordinates that | ||
| produced leftover-map distance `d`. Migration `0179` is the single | ||
| source of the column on every install path, fresh or existing -- | ||
| shipped migrations (`0001` / `0012`) are never edited after the fact. | ||
| The column is nullable so older leftover rows keep distance and residual | ||
| without fabricating an inner product. Fallback pairs that have no | ||
| complete-case leftover map omit the value rather than inventing one. | ||
|
|
||
| The pair button shows `ξ·ζ {signed}` next to leftover-map distance `d` | ||
| when the value is finite. Next action: leftover-map inner product `ξ·ζ` | ||
| reconstructs leftover residual after IRT main effects; open this post | ||
| to read the named criterion. A missing or non-finite inner product | ||
| omits the badge 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_inner_product`. After `make seed`, closest and farthest | ||
| leftover pairs sit above the member list with named `ξ·ζ` 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, and leftover-map rank. | ||
|
|
||
| ## 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,19 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { formatLeftoverInnerProduct, formatSignedLeftoverValue } from "./leftoverInnerProduct"; | ||
|
|
||
| describe("formatLeftoverInnerProduct", () => { | ||
| it("names leftover-map inner product without inventing a leftover score", () => { | ||
| expect(formatLeftoverInnerProduct(0.4)).toBe("ξ·ζ +0.40"); | ||
| expect(formatLeftoverInnerProduct(-1.1)).toBe("ξ·ζ \u22121.10"); | ||
| expect(formatLeftoverInnerProduct(0)).toBe("ξ·ζ 0.00"); | ||
| expect(formatSignedLeftoverValue(0.4)).toBe("+0.40"); | ||
| expect(formatSignedLeftoverValue(-1.1)).toBe("\u22121.10"); | ||
| }); | ||
|
|
||
| it("omits the badge when the inner product is missing or non-finite", () => { | ||
| expect(formatLeftoverInnerProduct(null)).toBeNull(); | ||
| expect(formatLeftoverInnerProduct(undefined)).toBeNull(); | ||
| expect(formatLeftoverInnerProduct(Number.NaN)).toBeNull(); | ||
| expect(formatLeftoverInnerProduct(Number.POSITIVE_INFINITY)).toBeNull(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| /** Gabriel leftover-map inner product ``ξ·ζ`` after IRT main effects. */ | ||
|
|
||
| export const LEFTOVER_INNER_PRODUCT_ACTION = | ||
| "Leftover-map inner product ξ·ζ {value} reconstructs leftover residual after IRT main effects. Open this post to read {criterion}."; | ||
|
|
||
| export function formatSignedLeftoverValue(value: number): string | null { | ||
| if (!Number.isFinite(value)) { | ||
| return null; | ||
| } | ||
| const magnitude = Math.abs(value).toFixed(2); | ||
| if (value > 0) { | ||
| return `+${magnitude}`; | ||
| } | ||
| if (value < 0) { | ||
| return `\u2212${magnitude}`; | ||
| } | ||
| return magnitude; | ||
| } | ||
|
|
||
| export function formatLeftoverInnerProduct(value: number | null | undefined): string | null { | ||
| if (value == null) { | ||
| return null; | ||
| } | ||
| const signed = formatSignedLeftoverValue(value); | ||
| return signed === null ? null : `ξ·ζ ${signed}`; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -87,11 +87,12 @@ def leftover_pairs_from_residual( | |
| for person, item in observed: | ||
| if person not in local_person or item not in local_item: | ||
| continue | ||
| distance = float( | ||
| np.linalg.norm(person_pos[local_person[person]] - item_pos[local_item[item]]) | ||
| ) | ||
| person_coord = person_pos[local_person[person]] | ||
| item_coord = item_pos[local_item[item]] | ||
| distance = float(np.linalg.norm(person_coord - item_coord)) | ||
| if not np.isfinite(distance): | ||
| continue | ||
| inner_product = float(np.dot(person_coord, item_coord)) | ||
| candidates.append( | ||
| _candidate_row(post_ids, item_codes, matrix, expected, residual, person, item, distance) | ||
| ) | ||
|
Comment on lines
+90
to
98
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. 📝 Info: Inner product reconstructs centered residual, not raw residual
Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| -- ADR 0179: persist leftover-map inner product ξ·ζ on leftover | ||
| -- post–criterion pairs. Distance stays Euclidean leftover-map d. | ||
| -- Upgrade column is nullable so older leftover rows keep distance and | ||
| -- residual without fabricating an inner product. This migration is the | ||
| -- single source of the column on fresh and existing installations. | ||
|
|
||
| alter table report_leftover_pair | ||
| add column if not exists leftover_inner_product numeric; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| -- Reverse 0179. Leftover distance and residual stay on the pair row. | ||
|
|
||
| alter table report_leftover_pair | ||
| drop column if exists leftover_inner_product; |
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.
🔴 Inner product computed then discarded
The inner product is computed (
np.dotat leftover_pairs.py) but never added to the candidate tuple orLeftoverPair, soreport_ingestionpersists nothing and the newleftover_inner_productcolumn stays null. Theξ·ζbadge never gets a value, making the feature a no-op end to end.Prompt for agents
Was this helpful? React with 👍 or 👎 to provide feedback.