feat: drive Chart 3's ridgelines from real posterior draws, with fallback
Wire useDrawDistribution into RateDensityRidgeline so each race×sex ridge uses a Gaussian KDE over that group's 500 real posterior draws when the selected model's shard has loaded, falling back per-group to the analytic fitSkewedInterval approximation otherwise. ApproxNote now only shows when status !== 'ready'. Pass leaid/state through from ChartPanel.
This commit is contained in:
@@ -2,23 +2,25 @@ import { useState } from 'react'
|
||||
import { MODEL_QUADRANTS } from '../hooks/useApi.js'
|
||||
import { raceColor, OBSERVED_MARK_COLOR, RACE_LABELS, SHORT_RACE_LABEL } from '../utils/colors.js'
|
||||
import { fitSkewedInterval, densityCurve } from '../utils/distributionApprox.js'
|
||||
import { kdeCurve } from '../utils/kde.js'
|
||||
import { niceTicks } from '../utils/niceTicks.js'
|
||||
import { useDrawDistribution } from '../hooks/useDrawDistribution.js'
|
||||
import ChartLegend from '../components/ChartLegend.jsx'
|
||||
import ApproxNote from '../components/ApproxNote.jsx'
|
||||
|
||||
/**
|
||||
* Modeled posterior density per race×sex group, for one selected model
|
||||
* (dropdown, default three-year + referral rate), split into Female/Male
|
||||
* columns — matches whitepaper-fig-clark-density-1.png's ridge style. Plain
|
||||
* SVG — each ridge is 60 analytic points from fitSkewedInterval/densityCurve,
|
||||
* already smooth without a binning/curveBasis smoothing pass.
|
||||
* columns. Each ridge is drawn from that group's 500 real posterior draws
|
||||
* (Gaussian KDE) when available, falling back per-group to the analytic
|
||||
* fitSkewedInterval/densityCurve approximation otherwise.
|
||||
*/
|
||||
|
||||
const RACE_ORDER = ['WH', 'BL', 'HI', 'AM']
|
||||
const SEX_COLUMNS = [{ sex: 'F', label: 'Female' }, { sex: 'M', label: 'Male' }]
|
||||
const DEFAULT_MODEL = 'unified_m4_mod' // Three-year + referral rate
|
||||
|
||||
function buildGroupRow(row) {
|
||||
function buildGroupRow(row, draws) {
|
||||
const enroll = row.stu_enroll || 0
|
||||
const observedRate = enroll > 0 ? ((row.observed_arrests || 0) / enroll) * 1000 : 0
|
||||
const rateMedian = (row.rate_median || 0) * 1000
|
||||
@@ -28,13 +30,16 @@ function buildGroupRow(row) {
|
||||
race: row.race,
|
||||
sex: row.sex,
|
||||
observedRate,
|
||||
draws,
|
||||
fit: fitSkewedInterval({ median: rateMedian, lower: rateLower, upper: rateUpper }),
|
||||
}
|
||||
}
|
||||
|
||||
export default function RateDensityRidgeline({ quadData }) {
|
||||
export default function RateDensityRidgeline({ quadData, leaid, state }) {
|
||||
const [selectedModel, setSelectedModel] = useState(DEFAULT_MODEL)
|
||||
const rows = (quadData && quadData[selectedModel]) || []
|
||||
const groups = rows.map((r) => ({ race: r.race, sex: r.sex, stuEnroll: r.stu_enroll || 0 }))
|
||||
const { status, drawsByGroup } = useDrawDistribution({ leaid, state, model: selectedModel, year: '21-22', groups })
|
||||
|
||||
const modelSelect = (
|
||||
<select
|
||||
@@ -55,7 +60,7 @@ export default function RateDensityRidgeline({ quadData }) {
|
||||
<h3 style={{ fontSize: '0.85rem', marginBottom: 'var(--space-1)', color: 'var(--cv-ink-2)' }}>
|
||||
Predicted arrest rates by student group
|
||||
</h3>
|
||||
<ApproxNote />
|
||||
{status !== 'ready' && <ApproxNote />}
|
||||
</div>
|
||||
{modelSelect}
|
||||
</div>
|
||||
@@ -64,7 +69,7 @@ export default function RateDensityRidgeline({ quadData }) {
|
||||
<p style={{ color: 'var(--cv-ink-3)', marginTop: 'var(--space-2)' }}>No model data available.</p>
|
||||
) : (
|
||||
<>
|
||||
<RidgeColumns rows={rows} />
|
||||
<RidgeColumns rows={rows} drawsByGroup={drawsByGroup} />
|
||||
<ChartLegend items={[
|
||||
...RACE_ORDER.map((race) => ({ shape: 'swatch', color: raceColor(race), label: RACE_LABELS[race] })),
|
||||
{ shape: 'diamond', color: OBSERVED_MARK_COLOR, label: 'Observed' },
|
||||
@@ -75,7 +80,7 @@ export default function RateDensityRidgeline({ quadData }) {
|
||||
)
|
||||
}
|
||||
|
||||
function RidgeColumns({ rows }) {
|
||||
function RidgeColumns({ rows, drawsByGroup }) {
|
||||
// Shared x-domain across both columns, so Female/Male are directly comparable.
|
||||
const allUpper = rows.map((r) => (r.rate_upper || 0) * 1000)
|
||||
const allObserved = rows
|
||||
@@ -87,14 +92,24 @@ function RidgeColumns({ rows }) {
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-3)', marginTop: 'var(--space-2)' }}>
|
||||
{SEX_COLUMNS.map(({ sex, label }) => (
|
||||
<SexRidgeColumn key={sex} label={label} rows={rows.filter((r) => r.sex === sex)} ticks={ticks} maxRate={niceMax} />
|
||||
<SexRidgeColumn
|
||||
key={sex}
|
||||
label={label}
|
||||
rows={rows.filter((r) => r.sex === sex)}
|
||||
drawsByGroup={drawsByGroup}
|
||||
ticks={ticks}
|
||||
maxRate={niceMax}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SexRidgeColumn({ label, rows, ticks, maxRate }) {
|
||||
const groups = RACE_ORDER.map((race) => rows.find((r) => r.race === race)).filter(Boolean).map(buildGroupRow)
|
||||
function SexRidgeColumn({ label, rows, drawsByGroup, ticks, maxRate }) {
|
||||
const groups = RACE_ORDER
|
||||
.map((race) => rows.find((r) => r.race === race))
|
||||
.filter(Boolean)
|
||||
.map((row) => buildGroupRow(row, drawsByGroup?.[`${row.race}_${row.sex}`]))
|
||||
|
||||
const width = 300
|
||||
const rowHeight = 58
|
||||
@@ -104,7 +119,11 @@ function SexRidgeColumn({ label, rows, ticks, maxRate }) {
|
||||
|
||||
const xScale = (val) => margin.left + (val / maxRate) * innerWidth
|
||||
|
||||
const curves = groups.map((g) => densityCurve(g.fit, { min: 0, max: maxRate, n: 60 }))
|
||||
const curves = groups.map((g) =>
|
||||
g.draws && g.draws.length > 0
|
||||
? kdeCurve(g.draws, { min: 0, max: maxRate, n: 60 })
|
||||
: densityCurve(g.fit, { min: 0, max: maxRate, n: 60 }),
|
||||
)
|
||||
const maxPdf = Math.max(...curves.flatMap((c) => c.map((p) => p.y)), 1e-9)
|
||||
const peakHeight = rowHeight * 0.82
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function ChartPanel({ district, state }) {
|
||||
|
||||
<RateByGroupBar data={rateByGroup} leaid={district.leaid} state={state} />
|
||||
|
||||
<RateDensityRidgeline quadData={data.quadData} />
|
||||
<RateDensityRidgeline quadData={data.quadData} leaid={district.leaid} state={state} />
|
||||
</div>
|
||||
|
||||
{/* Methodology footer */}
|
||||
|
||||
Reference in New Issue
Block a user