import { useState } from 'react' /** * Chart 6: Probability the district exceeds the national rate for each student group. * Uses posterior draws (approximated via interval bounds + median) to compute * P(district_rate > national_rate) per race×sex group. */ export default function ExceedanceProbability({ rateByGroup, nationalRates }) { const [selectedModel, setSelectedModel] = useState('unified_m2_mod') // Compute exceedance probability for each group using normal approximation: // P(district_rate > national) ≈ Φ((modeled_median - national) / modeled_sd) // where sd is derived from the interval width (upper - lower) / (2 * z_{0.95}) const computed = rateByGroup.map(d => { const natKey = `${d.race}_${d.sex}` const nationalRate = nationalRates[natKey] || 0 // Use modeled median and interval from the estimate data if available // (rate_median is per student; multiply by 1000 for per-1k) const modeledMedianPerK = ((d.modeledRateMedian || d.observedRate * (d.enroll || 1) / 1000)) // Actually: rate_median is already a probability (per-student). Convert to per-1k. const modeledRatePerK = ((d.rate_median || 0) * 1000) || d.observedRate // Derive SD from interval width if available, else use observed as proxy const lowerPerK = ((d.rate_lower || 0) * 1000) || (modeledRatePerK * 0.7) const upperPerK = ((d.rate_upper || 0) * 1000) || (modeledRatePerK * 1.3) // sd ≈ (upper - lower) / (2 * 1.645) for a 90% interval under normality const modeledSd = Math.max((upperPerK - lowerPerK) / 3.29, 0.05) // P(district > national) using normal CDF approximation const z = (modeledRatePerK - nationalRate) / modeledSd const probExceeds = normalCDF(z) return { ...d, natKey, nationalRate, modeledRate: modeledRatePerK, modeledSd, probExceeds: Math.max(0.01, Math.min(0.99, probExceeds)), // clamp to avoid extremes } }).sort((a, b) => b.probExceeds - a.probExceeds) const width = 400 const height = 320 const margin = { top: 50, right: 100, bottom: 90, left: 70 } const innerWidth = width - margin.left - margin.right return (
{/* Chart title */}

Probability district exceeds national rate — by student group

{/* Model selector (advanced toggle) */} {/* Background */} {/* Y-axis gridlines (probability scale 0–1) */} {[0, 0.25, 0.5, 0.75, 1].map((frac) => { const y = margin.top + ((height - margin.top - margin.bottom) - frac * (height - margin.top - margin.bottom)) return ( {frac.toFixed(2)} ) })} {/* Y-axis label */} P(district rate > national) {/* Bars */} {computed.map((d, i) => { const barHeight = Math.min(22, (innerWidth * 0.65) / computed.length) const y = margin.top + i * (barHeight + 4) const probWidth = d.probExceeds * innerWidth // Color by threshold: green if <50%, amber if ~50-75%, red if >75% let barColor if (d.probExceeds < 0.5) barColor = 'var(--moss-600)' // "not likely" → green else if (d.probExceeds < 0.8) barColor = 'var(--brass-600)' // "possible" → amber else barColor = 'var(--cv-danger)' // "likely" → red return ( {/* Bar */} {/* Probability label at end of bar */} {d.probExceeds > 0.05 && ( {Math.round(d.probExceeds * 100)}% )} {/* Group label */} {d.label} {/* National rate reference line at 50% */} {i === Math.floor(computed.length / 2) && ( )} {/* National rate annotation */} national = {d.nationalRate.toFixed(2)}/1k ) })} {/* X-axis label */} Probability (higher = district more likely to exceed national rate) {/* Legend */} < 50% (not likely) 50–80% (possible) > 80% (likely) {/* Summary note */}

Based on the {selectedModel === 'unified_m2_mod' ? 'one-year + covariate model (recommended)' : 'three-year + covariate model'}, which balances precision with current-year data. Green bars indicate groups where this district's rate is unlikely to exceed the national average; red indicates likely exceedance.

) } // Standard normal CDF approximation (Abramowitz & Stegun) function normalCDF(z) { const t = 1 / (1 + 0.2316419 * Math.abs(z)) const d = 0.3989423 * Math.exp(-z * z / 2) let prob = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274)))) return z > 0 ? 1 - prob : prob } // Inject a CSS fix for the broken fill attribute in ExceedanceProbability and DistrictVsNational const style = document.createElement('style')