From c1e453d96275377ff815e64fb5d45d3f99d5a3e4 Mon Sep 17 00:00:00 2001 From: kevinBell Date: Tue, 21 Jul 2026 16:43:22 -0600 Subject: [PATCH] various edits --- dashboard/README.md | 6 + dashboard/index.html | 6 +- dashboard/js/app.js | 9 +- dashboard/js/charts.js | 39 ++-- dashboard/public/data/cohort_scorecard.json | 2 +- dashboard/public/data/data_manifest.json | 2 +- dashboard/public/data/filter_catalog.json | 2 +- dashboard/public/data/sha256_manifest.json | 2 +- dashboard/styles.css | 29 +++ dashboard/tests/contract.test.mjs | 47 +++++ docs/dashboard_spec.md | 7 + docs/demo_script.md | 4 +- docs/final_report.md | 4 +- docs/model_card.md | 3 +- scripts/export_dashboard_data.py | 188 +++++++++++++++++- tests/test_dashboard_export.py | 207 ++++++++++++++++++++ 16 files changed, 526 insertions(+), 31 deletions(-) diff --git a/dashboard/README.md b/dashboard/README.md index 4bc52bd..62ee7f7 100644 --- a/dashboard/README.md +++ b/dashboard/README.md @@ -57,6 +57,12 @@ contains a denied identifier-like field, the dashboard shows an unavailable state and no estimates. Regenerate assets with the repository's private local pipeline; never hand-edit public JSON to bypass suppression. +Previously published cohort definitions are frozen against cross-release +differencing. New canonical model families may be added only when the combined +family clears every publication threshold; aliases or trims must not be folded +into an existing public cell. Increment `PUBLICATION_POLICY_VERSION` whenever +an approved aggregation policy changes. + The site intentionally contains no vehicle lookup, individual estimator, operational connection, or row-level prediction output. VINs, plates, ZIPs, stations, technician identifiers, raw source JSON, credentials, and operational diff --git a/dashboard/index.html b/dashboard/index.html index 186223a..cd09872 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -126,7 +126,7 @@
-

Availability only—this is not a county outcome ranking. Teal feeds are represented in the sample; gray counties are unavailable, not zero.

+

Availability only—this is not a county outcome ranking. Teal markers show counties represented in the sample; unmarked counties are unavailable, not zero.

@@ -177,7 +177,7 @@ Make or model -

Published grain: prior make and model only. County, age, fuel, program, and period slices are not available for these scorecards.

+

Published grain: prior make and model. Combined model families: Chevrolet Silverado 1500; Dodge Ram 1500; Hyundai Elantra; Nissan Altima; Subaru Outback; and Toyota 4Runner, Corolla, and Tacoma. Exact source-label cohorts: Ford F150; Honda Accord and Civic; and Toyota Camry. County, age, fuel, program, and period slices are not available for these scorecards.

@@ -200,7 +200,7 @@

Descriptive sample comparison

Observed cohort associations

-

Ordering is a viewing aid within this sample, not a reliability ranking or population comparison.

+

The filter note distinguishes combined model families from exact source-label cohorts. Ordering is a viewing aid within this sample, not a reliability ranking or population comparison.

diff --git a/dashboard/js/app.js b/dashboard/js/app.js index 210069c..d43d45f 100644 --- a/dashboard/js/app.js +++ b/dashboard/js/app.js @@ -230,9 +230,16 @@ function updateExplorer() { scorecardLabel(row).toLowerCase().includes(query), ); visibleScorecards = sortedScorecards(matches, sortMode); + const cohortCount = visibleScorecards.length; + let summary = "0 supported sample cohorts."; + if (cohortCount > 0 && cohortCount <= 12) { + summary = `${cohortCount} supported sample cohort${cohortCount === 1 ? "" : "s"}; all shown.`; + } else if (cohortCount > 12) { + summary = `${cohortCount} supported sample cohorts; showing ${Math.min(cohortCount, 18)} cards and 12 chart rows.`; + } setText( "result-summary", - `${visibleScorecards.length} supported sample cohort${visibleScorecards.length === 1 ? "" : "s"}; showing up to 18 cards and 12 chart rows.`, + summary, ); byId("empty-results").hidden = visibleScorecards.length > 0; renderCohortDotPlot(byId("cohort-dot-plot"), visibleScorecards); diff --git a/dashboard/js/charts.js b/dashboard/js/charts.js index 17fe53e..8a180eb 100644 --- a/dashboard/js/charts.js +++ b/dashboard/js/charts.js @@ -205,37 +205,50 @@ export function renderCohortDotPlot(container, rows) { ); } +// Simplified from the Census 2025 1:20m cartographic boundary for Utah +// (GEOID 49). Utah's defining step is on the northeast edge, not both sides. +const UTAH_OUTLINE_PATH = "M44 20H153V67H226V255H44Z"; + +// Census county centers projected into the same Utah map frame. const COUNTY_POINTS = { - cache: [154, 40], - weber: [137, 86], - davis: [126, 108], - salt_lake: [133, 133], - "salt lake": [133, 133], - utah: [140, 170], + cache: [128, 33], + weber: [123, 54], + davis: [111, 65], + "salt lake": [121, 83], + utah: [131, 108], }; +function normalizeCountyName(county) { + return String(county).trim().toLowerCase().replaceAll("_", " ").replace(/\s+/g, " "); +} + +function displayCountyName(county) { + return county.replace(/\b\w/g, (letter) => letter.toUpperCase()); +} + export function renderUtahCoverage(container, coveredCounties) { - const normalized = new Set(coveredCounties.map((county) => county.toLowerCase())); + const normalized = new Set(coveredCounties.map(normalizeCountyName)); const points = [...normalized] .map((county) => ({ county, coordinates: COUNTY_POINTS[county] })) .filter((item) => item.coordinates); + const countyNames = [...normalized].map(displayCountyName); container.innerHTML = ` `; container.setAttribute( "aria-label", - coveredCounties.length - ? `Development-sample feed availability includes ${coveredCounties.join(", ")}. This is not an outcome ranking; other counties are unavailable.` - : "No county feed coverage is available.", + countyNames.length + ? `Outline of Utah showing development-sample feed availability for ${countyNames.join(", ")} County feeds. This is not an outcome ranking; other counties are unavailable.` + : "Outline of Utah. No county feed coverage is available.", ); } diff --git a/dashboard/public/data/cohort_scorecard.json b/dashboard/public/data/cohort_scorecard.json index a09c894..4a5ac0b 100644 --- a/dashboard/public/data/cohort_scorecard.json +++ b/dashboard/public/data/cohort_scorecard.json @@ -1 +1 @@ -{"development_preview":true,"population_estimate_allowed":false,"rows":[{"nonpass_rate":0.116,"prior_make":"FORD","prior_model":"F150","support_rounded":1100},{"nonpass_rate":0.09,"prior_make":"HONDA","prior_model":"ACCORD","support_rounded":400},{"nonpass_rate":0.073,"prior_make":"HONDA","prior_model":"CIVIC","support_rounded":300},{"nonpass_rate":0.086,"prior_make":"TOYOTA","prior_model":"CAMRY","support_rounded":500}],"schema_version":"dashboard_data_v1"} +{"development_preview":true,"population_estimate_allowed":false,"rows":[{"nonpass_rate":0.123,"prior_make":"CHEVROLET","prior_model":"SILVERADO 1500","support_rounded":500},{"nonpass_rate":0.117,"prior_make":"DODGE","prior_model":"RAM 1500","support_rounded":600},{"nonpass_rate":0.116,"prior_make":"FORD","prior_model":"F150","support_rounded":1100},{"nonpass_rate":0.09,"prior_make":"HONDA","prior_model":"ACCORD","support_rounded":400},{"nonpass_rate":0.073,"prior_make":"HONDA","prior_model":"CIVIC","support_rounded":300},{"nonpass_rate":0.094,"prior_make":"HYUNDAI","prior_model":"ELANTRA","support_rounded":500},{"nonpass_rate":0.128,"prior_make":"NISSAN","prior_model":"ALTIMA","support_rounded":500},{"nonpass_rate":0.084,"prior_make":"SUBARU","prior_model":"OUTBACK","support_rounded":600},{"nonpass_rate":0.08,"prior_make":"TOYOTA","prior_model":"4RUNNER","support_rounded":600},{"nonpass_rate":0.086,"prior_make":"TOYOTA","prior_model":"CAMRY","support_rounded":500},{"nonpass_rate":0.082,"prior_make":"TOYOTA","prior_model":"COROLLA","support_rounded":800},{"nonpass_rate":0.08,"prior_make":"TOYOTA","prior_model":"TACOMA","support_rounded":600}],"schema_version":"dashboard_data_v1"} diff --git a/dashboard/public/data/data_manifest.json b/dashboard/public/data/data_manifest.json index 44bf720..7473eb9 100644 --- a/dashboard/public/data/data_manifest.json +++ b/dashboard/public/data/data_manifest.json @@ -1 +1 @@ -{"assets":["age_risk_curve.json","cohort_scorecard.json","coverage_quality.json","filter_catalog.json","model_diagnostics.json","overview_period_county.json"],"data_scope":{"first_year":2016,"last_year":2024,"model_names":["hist_gradient_boosting_platt","hist_gradient_boosting_raw","logistic_platt","logistic_raw","previous_episode_literal","training_prevalence"],"partitions":["train","tune","calibrate"]},"definitions":{"episode_gap_days":30,"locked_test_metrics_published":false,"support_rounding":100,"suppression_min_distinct_nonpass_vehicles":10,"suppression_min_distinct_pass_vehicles":10,"suppression_min_distinct_vehicles":100,"suppression_min_nonpass":10,"suppression_min_pass":10,"suppression_min_support":100,"target":"first-attempt next-episode binary non-pass rate"},"development_preview":true,"model_versions":["baseline_v1","hist_gradient_boosting_v1"],"population_estimate_allowed":false,"release_id":"7c1af8d22d3841b84d2cf4cd5ba7bce6a43578c78bc3d32a25d200b4b1986704","schema_version":"dashboard_data_v1"} +{"assets":["age_risk_curve.json","cohort_scorecard.json","coverage_quality.json","filter_catalog.json","model_diagnostics.json","overview_period_county.json"],"data_scope":{"first_year":2016,"last_year":2024,"model_names":["hist_gradient_boosting_platt","hist_gradient_boosting_raw","logistic_platt","logistic_raw","previous_episode_literal","training_prevalence"],"partitions":["train","tune","calibrate"]},"definitions":{"episode_gap_days":30,"locked_test_metrics_published":false,"support_rounding":100,"suppression_min_distinct_nonpass_vehicles":10,"suppression_min_distinct_pass_vehicles":10,"suppression_min_distinct_vehicles":100,"suppression_min_nonpass":10,"suppression_min_pass":10,"suppression_min_support":100,"target":"first-attempt next-episode binary non-pass rate"},"development_preview":true,"model_versions":["baseline_v1","hist_gradient_boosting_v1"],"population_estimate_allowed":false,"release_id":"7d207baa6389ff26d6d9fa39da8fd917500bca5f7bdc0ecd090e9ef1a7ca105e","schema_version":"dashboard_data_v1"} diff --git a/dashboard/public/data/filter_catalog.json b/dashboard/public/data/filter_catalog.json index 83721a2..4f2cd2c 100644 --- a/dashboard/public/data/filter_catalog.json +++ b/dashboard/public/data/filter_catalog.json @@ -1 +1 @@ -{"age_bands":["0-3","4-7","8-11","12-15","16-20","21+"],"development_preview":true,"models":["hist_gradient_boosting_platt","hist_gradient_boosting_raw","logistic_platt","logistic_raw","previous_episode_literal","training_prevalence"],"partitions":["train","tune","calibrate"],"periods":[{"quarters":[1,2,3,4],"year":2016},{"quarters":[1,2,3,4],"year":2017},{"quarters":[1,2,3,4],"year":2018},{"quarters":[1,2,3,4],"year":2019},{"quarters":[1,2,3,4],"year":2020},{"quarters":[1,2,3,4],"year":2021},{"quarters":[1,2,3,4],"year":2022},{"quarters":[1,2,3,4],"year":2023},{"quarters":[1,2,3,4],"year":2024}],"population_estimate_allowed":false,"prior_make_models":[{"prior_make":"FORD","prior_model":"F150"},{"prior_make":"HONDA","prior_model":"ACCORD"},{"prior_make":"HONDA","prior_model":"CIVIC"},{"prior_make":"TOYOTA","prior_model":"CAMRY"}],"public_counties":["salt_lake","utah","weber"],"schema_version":"dashboard_data_v1"} +{"age_bands":["0-3","4-7","8-11","12-15","16-20","21+"],"development_preview":true,"models":["hist_gradient_boosting_platt","hist_gradient_boosting_raw","logistic_platt","logistic_raw","previous_episode_literal","training_prevalence"],"partitions":["train","tune","calibrate"],"periods":[{"quarters":[1,2,3,4],"year":2016},{"quarters":[1,2,3,4],"year":2017},{"quarters":[1,2,3,4],"year":2018},{"quarters":[1,2,3,4],"year":2019},{"quarters":[1,2,3,4],"year":2020},{"quarters":[1,2,3,4],"year":2021},{"quarters":[1,2,3,4],"year":2022},{"quarters":[1,2,3,4],"year":2023},{"quarters":[1,2,3,4],"year":2024}],"population_estimate_allowed":false,"prior_make_models":[{"prior_make":"CHEVROLET","prior_model":"SILVERADO 1500"},{"prior_make":"DODGE","prior_model":"RAM 1500"},{"prior_make":"FORD","prior_model":"F150"},{"prior_make":"HONDA","prior_model":"ACCORD"},{"prior_make":"HONDA","prior_model":"CIVIC"},{"prior_make":"HYUNDAI","prior_model":"ELANTRA"},{"prior_make":"NISSAN","prior_model":"ALTIMA"},{"prior_make":"SUBARU","prior_model":"OUTBACK"},{"prior_make":"TOYOTA","prior_model":"4RUNNER"},{"prior_make":"TOYOTA","prior_model":"CAMRY"},{"prior_make":"TOYOTA","prior_model":"COROLLA"},{"prior_make":"TOYOTA","prior_model":"TACOMA"}],"public_counties":["salt_lake","utah","weber"],"schema_version":"dashboard_data_v1"} diff --git a/dashboard/public/data/sha256_manifest.json b/dashboard/public/data/sha256_manifest.json index 86b1e1e..72b2e5d 100644 --- a/dashboard/public/data/sha256_manifest.json +++ b/dashboard/public/data/sha256_manifest.json @@ -1 +1 @@ -{"development_preview":true,"files":[{"name":"age_risk_curve.json","sha256":"5445d52365ad3494ba05f097e7f4f5e41fffeecfe50fbb2952d72044f496c925"},{"name":"cohort_scorecard.json","sha256":"2ae83d9d55aa1f2ccae6420c66350fb491405eedfa90374aa3df2782a4aa0bf1"},{"name":"coverage_quality.json","sha256":"35b7dbed50e1b8260aac18c269f5045010ea23649a649f984a4693f3f00a1e83"},{"name":"data_manifest.json","sha256":"b7196b04eb223683c928cfb5375ed618e1230fa03382bd8a889de24c589557a0"},{"name":"filter_catalog.json","sha256":"9e45d428cd38853002ebd0bda089eb46d0832cd1b7b6b05ac953bc8472b01eb5"},{"name":"model_diagnostics.json","sha256":"ebf99eb32436a5eabb7d754cd88109f2cc0981575dd28966757031ac7faafa57"},{"name":"overview_period_county.json","sha256":"fe2a4233563b47fa31fc87859b100983d3bfb574c5a7fbc7241b9cfb4661b038"}],"population_estimate_allowed":false,"schema_version":"dashboard_data_v1"} +{"development_preview":true,"files":[{"name":"age_risk_curve.json","sha256":"5445d52365ad3494ba05f097e7f4f5e41fffeecfe50fbb2952d72044f496c925"},{"name":"cohort_scorecard.json","sha256":"3acd3aa5a32f53e8f0a2baaad6fec8bb0610efaac8151d8ec4a88285faca23fd"},{"name":"coverage_quality.json","sha256":"35b7dbed50e1b8260aac18c269f5045010ea23649a649f984a4693f3f00a1e83"},{"name":"data_manifest.json","sha256":"8ed1593dd223ddd2fbdcd808040ee6a9f37f078011e821a058a1c7a6d94694d2"},{"name":"filter_catalog.json","sha256":"e78e65ae463edf4ea1b5c757e1a002b5fd953beb75c31381f8264e267295f48e"},{"name":"model_diagnostics.json","sha256":"ebf99eb32436a5eabb7d754cd88109f2cc0981575dd28966757031ac7faafa57"},{"name":"overview_period_county.json","sha256":"fe2a4233563b47fa31fc87859b100983d3bfb574c5a7fbc7241b9cfb4661b038"}],"population_estimate_allowed":false,"schema_version":"dashboard_data_v1"} diff --git a/dashboard/styles.css b/dashboard/styles.css index ed7ec2c..b2f1b76 100644 --- a/dashboard/styles.css +++ b/dashboard/styles.css @@ -658,6 +658,22 @@ main { margin: 0 auto 1rem; } +.utah-map .axis-label, +.utah-map .state-label { + fill: var(--ink-650); + font-family: var(--font-sans); +} + +.utah-map .axis-label { + font-size: 11px; +} + +.utah-map .state-label { + font-size: 12px; + font-weight: 700; + letter-spacing: 0.16em; +} + .county-list { display: flex; flex-wrap: wrap; @@ -716,6 +732,10 @@ main { align-items: start; } +.explorer-results { + min-width: 0; +} + .filter-panel { position: sticky; top: 155px; @@ -1522,6 +1542,15 @@ fieldset:disabled .field select { justify-content: flex-start; } + .chart--scorecard { + overflow-x: auto; + overscroll-behavior-inline: contain; + } + + .chart--scorecard svg { + min-width: 720px; + } + .locked-panel { display: block; } diff --git a/dashboard/tests/contract.test.mjs b/dashboard/tests/contract.test.mjs index c36e2d4..5088be5 100644 --- a/dashboard/tests/contract.test.mjs +++ b/dashboard/tests/contract.test.mjs @@ -12,6 +12,7 @@ import { loadDashboardData, validateAssetSet, } from "../js/data.js"; +import { renderUtahCoverage } from "../js/charts.js"; const DASHBOARD_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const PUBLIC_DATA = path.join(DASHBOARD_ROOT, "public", "data"); @@ -84,13 +85,41 @@ test("narrow layouts constrain body content while preserving local nav scrolling assert.match(css, /html\s*{[^}]*max-width:\s*100%;[^}]*overflow-x:\s*clip;/s); assert.match(css, /body\s*{[^}]*max-width:\s*100%;[^}]*overflow-x:\s*clip;/s); assert.match(css, /\.primary-nav__inner\s*{[^}]*overflow-x:\s*auto;/s); + assert.match(css, /\.explorer-results\s*{[^}]*min-width:\s*0;/s); const mobile = css.match(/@media \(max-width: 660px\)\s*{[\s\S]*?(?=@media|$)/)?.[0]; assert.ok(mobile, "mobile breakpoint is present"); assert.match(mobile, /\.page-shell\s*{[^}]*width:\s*auto;[^}]*max-width:\s*calc\(100% - 2rem\);/s); assert.match(mobile, /\.preview-banner\s*{[^}]*justify-content:\s*flex-start;/s); + assert.match(mobile, /\.chart--scorecard\s*{[^}]*overflow-x:\s*auto;/s); + assert.match(mobile, /\.chart--scorecard svg\s*{[^}]*min-width:\s*720px;/s); assert.match(mobile, /overflow-wrap:\s*anywhere;/); }); +test("county coverage renders Utah's northeast step and geographically aligned feeds", () => { + const attributes = new Map(); + const container = { + innerHTML: "", + setAttribute(name, value) { + attributes.set(name, value); + }, + }; + + renderUtahCoverage(container, ["weber", "salt_lake", "utah"]); + + assert.match( + container.innerHTML, + /data-map-feature="utah-outline" d="M44 20H153V67H226V255H44Z"/, + ); + assert.doesNotMatch(container.innerHTML, /stroke-dasharray/); + assert.equal(container.innerHTML.match(/]*>Weber County/); + assert.match(container.innerHTML, /cx="121" cy="83"[^>]*><title>Salt Lake County/); + assert.match(container.innerHTML, /cx="131" cy="108"[^>]*><title>Utah County/); + assert.match(container.innerHTML, />UTAH<\/text>/); + assert.match(attributes.get("aria-label"), /^Outline of Utah showing/); + assert.doesNotMatch(attributes.get("aria-label"), /_/); +}); + test("model view declares the final model, benchmark role, and publication boundary", () => { const html = readFileSync(path.join(DASHBOARD_ROOT, "index.html"), "utf8"); const modelView = viewSource(html, "model"); @@ -120,6 +149,16 @@ test("every result-facing view visibly labels the sample and non-population scop assert.match(html, /not a reliability ranking or population comparison/i); }); +test("cohort view discloses which rows are combined families", () => { + const html = readFileSync(path.join(DASHBOARD_ROOT, "index.html"), "utf8"); + const cohortView = viewSource(html, "cohorts"); + assert.match(cohortView, /Combined model families:/); + assert.match(cohortView, /Silverado 1500; Dodge Ram 1500; Hyundai Elantra; Nissan Altima; Subaru Outback;/); + assert.match(cohortView, /Toyota 4Runner, Corolla, and Tacoma/); + assert.match(cohortView, /Exact source-label cohorts:/); + assert.match(cohortView, /Ford F150; Honda Accord and Civic; and Toyota Camry/); +}); + test("all required generated assets satisfy the browser contract", () => { const validated = validateAssetSet(assetSet()); assert.equal(validated.manifest.schema_version, SCHEMA_VERSION); @@ -139,6 +178,14 @@ test("all required generated assets satisfy the browser contract", () => { ["train", "tune", "calibrate"], ); assert.ok(validated.overview.rows.length > 0); + assert.equal(validated.scorecards.rows.length, 12); + assert.deepEqual( + validated.filters.prior_make_models, + validated.scorecards.rows.map(({ prior_make, prior_model }) => ({ + prior_make, + prior_model, + })), + ); }); test("browser loader verifies every raw asset digest before rendering", async () => { diff --git a/docs/dashboard_spec.md b/docs/dashboard_spec.md index f2c59ec..069494c 100644 --- a/docs/dashboard_spec.md +++ b/docs/dashboard_spec.md @@ -31,6 +31,13 @@ headlined, or described as population county performance. ### Sample cohorts - Search and compare only supported, suppression-cleared make/model cohorts +- Combine documented source abbreviations and trim labels for newly published + model families before applying the unchanged publication thresholds +- Keep earlier public cohort definitions stable across releases so differences + cannot expose a formerly suppressed slice +- Identify Silverado 1500, Ram 1500, Elantra, Altima, Outback, 4Runner, + Corolla, and Tacoma as combined families; identify F150, Accord, Civic, and + Camry as exact source-label cohorts - Show observed sample non-pass rates and rounded support - Default sorting by support rather than risk - Disable filters or adjusted views that the aggregate bundle cannot support diff --git a/docs/demo_script.md b/docs/demo_script.md index 8230a78..02b3e06 100644 --- a/docs/demo_script.md +++ b/docs/demo_script.md @@ -53,7 +53,9 @@ the “observed sample non-pass” label. **Say:** > This view contains only prior make-and-model cohorts that cleared the -> publication thresholds. I can search supported cohorts, but these are observed +> publication thresholds. Selected source abbreviations and trim labels are +> combined for new model families, while earlier published cohorts keep stable +> definitions. I can search supported cohorts, but these are observed > development-sample associations—not reliability grades, rankings, or > recommendations. Unsupported slices are not inferred in the browser, and > suppressed rows are absent rather than hidden. diff --git a/docs/final_report.md b/docs/final_report.md index 1045c89..ccd1359 100644 --- a/docs/final_report.md +++ b/docs/final_report.md @@ -140,7 +140,9 @@ views: 1. **Overview** shows rounded support, sample outcome patterns, vehicle-age patterns, and county feed coverage. -2. **Sample cohorts** shows only supported make/model sample aggregates; it does +2. **Sample cohorts** shows only supported make/model sample aggregates; + selected source abbreviations and trim labels are combined for new model + families while earlier published cohorts keep stable definitions. It does not issue reliability ratings or recommendations. 3. **Model & benchmark** identifies calibrated logistic regression as final and the boosted tree as benchmark-only. It shows pre-2025, 2024 calibration-fit diff --git a/docs/model_card.md b/docs/model_card.md index 1544c9e..6191554 100644 --- a/docs/model_card.md +++ b/docs/model_card.md @@ -132,7 +132,8 @@ population or external-validation results. - Page sampling is not population-representative. - Source, program, time, and geography are entangled. - Reject and abort are heterogeneous non-pass outcomes. -- Make/model aliases and incomplete feed coverage can distort cohorts. +- Residual make/model aliases outside the documented scorecard mappings and + incomplete feed coverage can distort cohorts. - The one-time 2025 holdout is not an external validation dataset. - No protected attributes are modeled, but that does not establish fairness. - Sample calibration does not establish production calibration. diff --git a/scripts/export_dashboard_data.py b/scripts/export_dashboard_data.py index 234ff71..8d1649f 100644 --- a/scripts/export_dashboard_data.py +++ b/scripts/export_dashboard_data.py @@ -26,6 +26,7 @@ import duckdb PROJECT_ROOT = Path(__file__).resolve().parents[1] PUBLIC_DATA_ROOT = (PROJECT_ROOT / "dashboard/public/data").resolve() SCHEMA_VERSION = "dashboard_data_v1" +PUBLICATION_POLICY_VERSION = "dashboard_publication_policy_v1" ALLOWED_PARTITIONS = ("train", "tune", "calibrate") PARTITION_ORDER = {name: index for index, name in enumerate(ALLOWED_PARTITIONS)} LOCKED_PARTITION_ALIASES = ("test", "locked_test", "locked-test") @@ -641,7 +642,183 @@ def _age_risk_rows(connection: duckdb.DuckDBPyConnection) -> List[Dict[str, obje def _scorecard_rows(connection: duckdb.DuckDBPyConnection) -> List[Dict[str, object]]: raw = connection.execute( """ - SELECT last_observed_make, last_observed_model, + WITH legacy_cells(prior_make, prior_model) AS ( + VALUES + ('FORD', 'F150'), + ('HONDA', 'ACCORD'), + ('HONDA', 'CIVIC'), + ('TOYOTA', 'CAMRY') + ), + make_aliases(raw_make, canonical_make) AS ( + VALUES + ('CHEVR', 'CHEVROLET'), + ('HYUND', 'HYUNDAI'), + ('NISSA', 'NISSAN'), + ('SUBAR', 'SUBARU'), + ('TOYOT', 'TOYOTA') + ), + normalized AS ( + SELECT + safe_mart.last_observed_make AS raw_make, + safe_mart.last_observed_model AS raw_model, + coalesce( + make_aliases.canonical_make, + upper(trim(safe_mart.last_observed_make)) + ) AS normalized_make, + upper(trim(safe_mart.last_observed_model)) AS normalized_model, + exact_legacy.prior_make IS NOT NULL AS is_exact_legacy, + display_legacy.prior_make IS NOT NULL AS collides_with_legacy, + safe_mart.vehicle_token, + safe_mart.target_nonpass + FROM safe_mart + LEFT JOIN make_aliases + ON upper(trim(safe_mart.last_observed_make)) = make_aliases.raw_make + LEFT JOIN legacy_cells AS exact_legacy + ON safe_mart.last_observed_make = exact_legacy.prior_make + AND safe_mart.last_observed_model = exact_legacy.prior_model + LEFT JOIN legacy_cells AS display_legacy + ON trim(safe_mart.last_observed_make) = display_legacy.prior_make + AND trim(safe_mart.last_observed_model) = display_legacy.prior_model + WHERE safe_mart.eligible_returning_target + AND safe_mart.target_nonpass IN (0, 1) + AND safe_mart.last_observed_make IS NOT NULL + AND safe_mart.last_observed_model IS NOT NULL + ), + classified AS ( + SELECT + CASE + -- Only the eight families introduced in this release use + -- alias folding. The four legacy cells never enter this + -- branch, so their published counts retain raw-key meaning. + WHEN normalized_make = 'TOYOTA' + AND ( + normalized_model = 'COROLLA' + OR normalized_model LIKE 'COROLLA %' + ) + AND normalized_model != 'COROLLA IM' + AND normalized_model NOT LIKE 'COROLLA IM %' + THEN 'toyota_corolla' + WHEN normalized_make = 'TOYOTA' + AND ( + normalized_model = '4RUNNER' + OR normalized_model LIKE '4RUNNER %' + ) + THEN 'toyota_4runner' + WHEN normalized_make = 'TOYOTA' + AND ( + normalized_model = 'TACOMA' + OR normalized_model LIKE 'TACOMA %' + ) + THEN 'toyota_tacoma' + WHEN normalized_make = 'DODGE' + AND ( + normalized_model = 'RAM 1500' + OR normalized_model LIKE 'RAM 1500 %' + OR normalized_model = 'RAM PICKUP 1500' + OR normalized_model LIKE 'RAM PICKUP 1500 %' + OR normalized_model = 'RAM1500' + OR normalized_model LIKE 'RAM1500 %' + ) + AND normalized_model NOT LIKE '% VAN' + AND normalized_model NOT LIKE '% VAN %' + THEN 'dodge_ram_1500' + WHEN normalized_make = 'CHEVROLET' + AND ( + normalized_model = 'SILVERADO 1500' + OR normalized_model LIKE 'SILVERADO 1500 %' + OR normalized_model = 'SILVERADO 1500HD' + OR normalized_model LIKE 'SILVERADO 1500HD %' + OR normalized_model = 'SILVERADO C1500' + OR normalized_model LIKE 'SILVERADO C1500 %' + OR normalized_model = 'SILVERADO K1500' + OR normalized_model LIKE 'SILVERADO K1500 %' + OR normalized_model = 'C1500 SILVERADO' + OR normalized_model LIKE 'C1500 SILVERADO %' + OR normalized_model = 'K15 SILVERADO' + OR normalized_model LIKE 'K15 SILVERADO %' + OR normalized_model = 'K1500 SILVERADO' + OR normalized_model LIKE 'K1500 SILVERADO %' + ) + THEN 'chevrolet_silverado_1500' + WHEN normalized_make = 'NISSAN' + AND ( + normalized_model = 'ALTIMA' + OR normalized_model LIKE 'ALTIMA %' + ) + THEN 'nissan_altima' + WHEN normalized_make = 'HYUNDAI' + AND ( + normalized_model = 'ELANTRA' + OR normalized_model LIKE 'ELANTRA %' + ) + THEN 'hyundai_elantra' + WHEN normalized_make = 'SUBARU' + AND ( + normalized_model = 'OUTBACK' + OR normalized_model LIKE 'OUTBACK %' + OR normalized_model = 'LEGACY OUTBACK' + OR normalized_model LIKE 'LEGACY OUTBACK %' + ) + THEN 'subaru_outback' + ELSE NULL + END AS new_family, + * + FROM normalized + ), + publication_input AS ( + -- Exact legacy keys are deliberately copied from the raw columns. + SELECT raw_make AS prior_make, + raw_model AS prior_model, + vehicle_token, + target_nonpass + FROM classified + WHERE is_exact_legacy + + UNION ALL + + -- Preserve the original raw-key behavior for unrelated cohorts. + -- A whitespace variant that would render as a legacy key is + -- withheld rather than creating a duplicate public identity. + SELECT raw_make AS prior_make, + raw_model AS prior_model, + vehicle_token, + target_nonpass + FROM classified + WHERE NOT is_exact_legacy + AND NOT collides_with_legacy + AND new_family IS NULL + + UNION ALL + + SELECT + CASE new_family + WHEN 'chevrolet_silverado_1500' THEN 'CHEVROLET' + WHEN 'dodge_ram_1500' THEN 'DODGE' + WHEN 'hyundai_elantra' THEN 'HYUNDAI' + WHEN 'nissan_altima' THEN 'NISSAN' + WHEN 'subaru_outback' THEN 'SUBARU' + WHEN 'toyota_4runner' THEN 'TOYOTA' + WHEN 'toyota_corolla' THEN 'TOYOTA' + WHEN 'toyota_tacoma' THEN 'TOYOTA' + ELSE NULL + END AS prior_make, + CASE new_family + WHEN 'chevrolet_silverado_1500' THEN 'SILVERADO 1500' + WHEN 'dodge_ram_1500' THEN 'RAM 1500' + WHEN 'hyundai_elantra' THEN 'ELANTRA' + WHEN 'nissan_altima' THEN 'ALTIMA' + WHEN 'subaru_outback' THEN 'OUTBACK' + WHEN 'toyota_4runner' THEN '4RUNNER' + WHEN 'toyota_corolla' THEN 'COROLLA' + WHEN 'toyota_tacoma' THEN 'TACOMA' + ELSE NULL + END AS prior_model, + vehicle_token, + target_nonpass + FROM classified + WHERE new_family IS NOT NULL + ) + SELECT prior_make, prior_model, count(*)::BIGINT AS n, sum(target_nonpass)::BIGINT AS nonpass, count(DISTINCT vehicle_token)::BIGINT AS vehicles, @@ -651,11 +828,7 @@ def _scorecard_rows(connection: duckdb.DuckDBPyConnection) -> List[Dict[str, obj count(DISTINCT vehicle_token) FILTER ( WHERE target_nonpass = 1 )::BIGINT AS nonpass_vehicles - FROM safe_mart - WHERE eligible_returning_target - AND target_nonpass IN (0, 1) - AND last_observed_make IS NOT NULL - AND last_observed_model IS NOT NULL + FROM publication_input GROUP BY 1, 2 HAVING count(*) >= 100 AND sum(target_nonpass) >= 10 @@ -667,7 +840,7 @@ def _scorecard_rows(connection: duckdb.DuckDBPyConnection) -> List[Dict[str, obj AND count(DISTINCT vehicle_token) FILTER ( WHERE target_nonpass = 1 ) >= 10 - ORDER BY n DESC, last_observed_make, last_observed_model + ORDER BY n DESC, prior_make, prior_model LIMIT 200 """ ).fetchall() @@ -784,6 +957,7 @@ def _release_id( ) -> str: material = { "schema_version": SCHEMA_VERSION, + "publication_policy_version": PUBLICATION_POLICY_VERSION, "mart_sha256": mart_digest, "models": [ {"model_version": version, "metrics_sha256": metrics_digest} diff --git a/tests/test_dashboard_export.py b/tests/test_dashboard_export.py index 35099d1..027fc16 100644 --- a/tests/test_dashboard_export.py +++ b/tests/test_dashboard_export.py @@ -117,6 +117,213 @@ class DashboardExportTests(unittest.TestCase): } self.assertEqual(first_bytes, second_bytes) + def test_release_id_includes_publication_policy_version(self) -> None: + mart_digest = "a" * 64 + provenance = [("model_v1", "b" * 64)] + release_id = dashboard_export._release_id(mart_digest, provenance) + + with mock.patch.object( + dashboard_export, + "PUBLICATION_POLICY_VERSION", + dashboard_export.PUBLICATION_POLICY_VERSION + "_changed", + ): + changed_release_id = dashboard_export._release_id( + mart_digest, provenance + ) + + self.assertNotEqual(release_id, changed_release_id) + + def test_scorecards_fold_only_new_family_aliases_before_suppression(self) -> None: + connection = duckdb.connect(":memory:") + try: + connection.execute( + """ + CREATE TABLE safe_mart ( + vehicle_token VARCHAR, + eligible_returning_target BOOLEAN, + target_nonpass INTEGER, + last_observed_make VARCHAR, + last_observed_model VARCHAR + ) + """ + ) + rows = [] + + def add_group(make: str, model: str, nonpass: int = 10) -> None: + group_number = len(rows) + for index in range(60): + rows.append( + ( + "group-{}-vehicle-{}".format(group_number, index), + True, + 1 if index < nonpass else 0, + make, + model, + ) + ) + + # Every raw-string group is below the 100-vehicle threshold. Each + # approved pair reaches 120 only after its explicit aliases merge. + canonical_groups = { + ("CHEVROLET", "SILVERADO 1500"): ( + ("CHEVROLET", "SILVERADO 1500 LT"), + ("CHEVR", "K15 SILVERADO"), + ), + ("DODGE", "RAM 1500"): ( + ("DODGE", "RAM 1500 QUAD"), + ("DODGE", "RAM PICKUP 1500"), + ), + ("HYUNDAI", "ELANTRA"): ( + ("HYUNDAI", "ELANTRA GLS"), + ("HYUND", "ELANTRA"), + ), + ("NISSAN", "ALTIMA"): ( + ("NISSAN", "ALTIMA 2.5 S"), + ("NISSA", "ALTIMA"), + ), + ("SUBARU", "OUTBACK"): ( + ("SUBARU", "LEGACY OUTBACK 2.5I AWD"), + ("SUBAR", "OUTBACK"), + ), + ("TOYOTA", "4RUNNER"): ( + ("TOYOTA", "4RUNNER SR5"), + ("TOYOT", "4RUNNER 4WD"), + ), + ("TOYOTA", "COROLLA"): ( + ("TOYOTA", "COROLLA CE LE S"), + ("TOYOT", "COROLLA"), + ), + ("TOYOTA", "TACOMA"): ( + ("TOYOTA", "TACOMA V6"), + ("TOYOT", "TACOMA 4WD"), + ), + } + for variants in canonical_groups.values(): + for make, model in variants: + add_group(make, model) + + # These separately marketed lines share a prefix with a canonical + # family but must remain distinct and suppressed at this support. + for make, model in ( + ("HONDA", "ACCORD CROSS TOUR EXL"), + ("HONDA", "ACCORD CROSSTOUR"), + ("HONDA", "CIVIC CRX SI"), + ("HONDA", "CIVIC DEL SOL SI"), + ("TOYOT", "CAMRY SOLARA"), + ("TOYOTA", "COROLLA IM"), + ("DODGE", "RAM 1500 VAN"), + ("FORD", "F1500"), + ("NISSAN", "ALTIMAX"), + ("TOYOTA", "CAMRYX"), + ): + add_group(make, model, nonpass=30) + + connection.executemany( + "INSERT INTO safe_mart VALUES (?, ?, ?, ?, ?)", rows + ) + scorecards = dashboard_export._scorecard_rows(connection) + finally: + connection.close() + + self.assertEqual( + [(row["prior_make"], row["prior_model"]) for row in scorecards], + sorted(canonical_groups), + ) + self.assertTrue( + all(row["support_rounded"] == 100 for row in scorecards) + ) + self.assertTrue(all(row["nonpass_rate"] == 0.167 for row in scorecards)) + + def test_legacy_scorecard_cells_are_not_broadened_by_aliases(self) -> None: + connection = duckdb.connect(":memory:") + try: + connection.execute( + """ + CREATE TABLE safe_mart ( + vehicle_token VARCHAR, + eligible_returning_target BOOLEAN, + target_nonpass INTEGER, + last_observed_make VARCHAR, + last_observed_model VARCHAR + ) + """ + ) + rows = [] + + def add_group( + make: str, + model: str, + n: int, + nonpass: int, + ) -> None: + group_number = len(rows) + for index in range(n): + rows.append( + ( + "group-{}-vehicle-{}".format(group_number, index), + True, + 1 if index < nonpass else 0, + make, + model, + ) + ) + + legacy_cells = { + ("FORD", "F150"): 12, + ("HONDA", "ACCORD"): 18, + ("HONDA", "CIVIC"): 24, + ("TOYOTA", "CAMRY"): 30, + } + aliases = { + ("FORD", "F150"): ( + ("FORD", "F150 4WD"), + ("ford", "f150"), + ), + ("HONDA", "ACCORD"): ( + ("HONDA", "ACCORD LX"), + ("honda", "accord"), + ), + ("HONDA", "CIVIC"): ( + ("HONDA", "CIVIC EX"), + ("honda", "civic"), + ), + ("TOYOTA", "CAMRY"): ( + ("TOYOTA", "CAMRY LE"), + ("TOYOT", "CAMRY"), + ), + } + for (make, model), nonpass in legacy_cells.items(): + add_group(make, model, 120, nonpass) + + # This raw key renders as the legacy key after safe-category + # trimming. It must not become a second public identity. + add_group(" " + make + " ", " " + model + " ", 120, 60) + + # Each alias is suppressed alone but would pass support if the + # two were folded together, making accidental broadening clear. + for alias_make, alias_model in aliases[(make, model)]: + add_group(alias_make, alias_model, 60, 30) + + connection.executemany( + "INSERT INTO safe_mart VALUES (?, ?, ?, ?, ?)", rows + ) + scorecards = dashboard_export._scorecard_rows(connection) + finally: + connection.close() + + self.assertEqual(len(scorecards), len(legacy_cells)) + by_cell = { + (row["prior_make"], row["prior_model"]): row + for row in scorecards + } + self.assertEqual(set(by_cell), set(legacy_cells)) + for cell, nonpass in legacy_cells.items(): + with self.subTest(cell=cell): + self.assertEqual(by_cell[cell]["support_rounded"], 100) + self.assertEqual( + by_cell[cell]["nonpass_rate"], round(nonpass / 120, 3) + ) + def test_unlocked_model_manifest_is_refused(self) -> None: manifest = self._read_json(self.model_manifest) manifest["locked_test_evaluated"] = True