import { Fragment } from 'preact'; import { useEffect, useMemo, useState } from 'preact/hooks'; import { explainPanelQuestion, getModelCoefficients, getVariableDictionary, getMacroGlossary, getLgd, getExhibitsList, } from '../api.js'; import { runDate } from '../format.js'; import SearchableTable from '../components/SearchableTable.jsx'; import ExhibitImage from '../components/ExhibitImage.jsx'; import Panel from '../components/Panel.jsx'; import HowToReadCoefficients from '../components/HowToReadCoefficients.jsx'; import ModelAtAGlance from '../components/ModelAtAGlance.jsx'; import EadEirMethod, { buildEadEirExplainQuestion } from '../components/EadEirMethod.jsx'; import { ExpandToggle, InterpretationRow, useExpandableRows, } from '../components/CoefficientInterpretation.jsx'; const FAMILY_LABEL = { baseline: 'Baseline (seasoning)', borrower: 'Borrower quality', collateral: 'Collateral / equity', macro: 'Macro-economic', incentive: 'Incentive / behavioural', }; const COEF_COLS = 6; // toggle + variable + HR + per-unit HR + CI + p function CoefficientsTable({ model, modelKey }) { const { isOpen, toggle } = useExpandableRows(); if (!model) return null; const families = []; const seen = new Set(); for (const c of model.coefficients) { if (!seen.has(c.family)) { seen.add(c.family); families.push(c.family); } } return (
{families.map((fam) => ( {model.coefficients .filter((c) => c.family === fam) .map((c) => { const key = `${modelKey}:${c.variable}`; const open = isOpen(key); return ( toggle(key)}> {open && } ); })} ))}
Variable Hazard ratio Per-unit HR 95% CI p
{FAMILY_LABEL[fam] ?? fam}
toggle(key)} label={c.variable} /> {c.variable} {c.fred_series && FRED} 1 ? 'hr-up' : 'hr-down'}`}> {c.hazard_ratio.toFixed(4)} {c.hazard_ratio_per_unit != null ? c.hazard_ratio_per_unit.toFixed(4) : '—'} [{c.ci[0].toFixed(3)}, {c.ci[1].toFixed(3)}] {c.p_display}
{model.coefficients.find((c) => c.family === fam)?.story}
); } function FitStats({ fitStats }) { if (!fitStats) return null; const rows = [ { id: 'default', label: 'Default hazard', ...fitStats.default }, { id: 'prepay', label: 'Prepayment hazard', ...fitStats.prepay }, ]; return ( <>
{rows.map((r) => ( ))}
Model n fit Events Train AUC OOT AUC McFadden R²
{r.label} {r.n_fit.toLocaleString()} {r.events.toLocaleString()} {r.train_auc.toFixed(4)} {r.oot_auc.toFixed(4)} {r.mcfadden_r2.toFixed(4)}

Honest caveat — OOT is the stress window. Out-of-time (t=41–60, 2010Q2–2015Q1) is the GFC stress aftermath, not a random holdout: the AUC drop from train to OOT above is expected and does not by itself indicate overfitting.

Net UER effect. {fitStats.net_uer_effect_note}

Double trigger (LTV × UER). {fitStats.double_trigger_note}

{fitStats.seasoning_peak && (

Seasoning peak. Fitted hazard peaks at quarter{' '} {fitStats.seasoning_peak.fitted_q} vs an empirical peak at{' '} {fitStats.seasoning_peak.empirical_q} (plausible window{' '} {fitStats.seasoning_peak.plausible_window_q.join('–')}).

)}
); } function VariableDictionary({ dict }) { if (!dict) return null; const columns = [ { key: 'variable', label: 'Variable' }, { key: 'source_transformation', label: 'Source / transformation' }, { key: 'lag_window', label: 'Lag / window' }, { key: 'economic_rationale', label: 'Economic rationale' }, { key: 'expected_sign', label: 'Expected sign' }, { key: 'fitted_verified', label: 'Fitted / verified' }, { key: 'consumed_by', label: 'Consumed by' }, { key: 'fred_series', label: 'FRED', render: (r) => (r.fred_series ? {r.fred_series} : '—'), }, ]; return ( <>

{dict.preamble}

{dict.notes}

); } function MacroGlossary({ glossary }) { if (!glossary) return null; const columns = [ { key: 'label', label: 'Series' }, { key: 'fred_series', label: 'FRED ID', render: (r) => (r.fred_series ? {r.fred_series} : '—'), }, { key: 'geography', label: 'Geography' }, { key: 'frequency', label: 'Frequency' }, { key: 'transformation', label: 'Transformation' }, { key: 'lag', label: 'Lag' }, { key: 'lag_rationale', label: 'Why this lag' }, { key: 'which_models', label: 'Used by', render: (r) => r.which_models.join('; '), }, ]; return (
Macro data glossary ({glossary.series.length} series)
); } function LgdSection({ lgd, exhibits }) { if (!lgd) return null; const calRows = Object.entries(lgd.oot_calibration).map(([key, v]) => ({ metric: key.replace(/_/g, ' '), train: v.train, oot: v.oot, })); const coefCols = [ { key: 'variable', label: 'Variable' }, { key: 'coef', label: 'Coef', align: 'right', render: (r) => r.coef.toFixed(4) }, { key: 'se', label: 'SE', align: 'right', render: (r) => (r.se ?? r.se_hc1)?.toFixed(4) }, { key: 'z', label: 'z', align: 'right', render: (r) => r.z.toFixed(3) }, { key: 'p', label: 'p', align: 'right', render: (r) => r.p.toFixed(4) }, ]; return ( <>
Cure rate
{(lgd.cure_rate * 100).toFixed(1)}%
Cure AUC (train / OOT)
{lgd.cure_auc.train.toFixed(3)} / {lgd.cure_auc.oot.toFixed(3)}
Excess-loss loading
{(lgd.excess_loss_loading * 100).toFixed(2)}%

OOT calibration

{calRows.map((r) => ( ))}
MetricTrainOOT
{r.metric} {r.train.toFixed(4)} {r.oot.toFixed(4)}

Cure-stage coefficients (logit)

Severity-stage coefficients (OLS, HC1)

(c.key === 'se' ? { ...c, label: 'SE (HC1)' } : c))} rows={lgd.severity_stage_coefficients} placeholder="Search…" />

LGD exhibits

{exhibits .filter((e) => e.id.startsWith('lgd_')) .map((e) => ( ))}
); } export default function ModelTab() { const [coeffs, setCoeffs] = useState(null); const [dict, setDict] = useState(null); const [macroGlossary, setMacroGlossary] = useState(null); const [lgd, setLgd] = useState(null); const [exhibits, setExhibits] = useState([]); const [selected, setSelected] = useState('default'); const [error, setError] = useState(null); useEffect(() => { let alive = true; Promise.all([ getModelCoefficients(), getVariableDictionary(), getMacroGlossary(), getLgd(), getExhibitsList(), ]) .then(([c, d, mg, l, ex]) => { if (!alive) return; setCoeffs(c); setDict(d); setMacroGlossary(mg); setLgd(l); setExhibits(ex.exhibits); }) .catch((e) => alive && setError(e.message)); return () => { alive = false; }; }, []); const seasoningExhibits = useMemo( () => exhibits.filter((e) => e.id.startsWith('hazard_')), [exhibits], ); const model = coeffs?.models?.[selected]; return (

The Model

Coefficients, fit statistics, and the variable dictionary — with the honest caveats.

{error && (
Engine API offline ({error}).
)}
} buildExplainQuestion={() => explainPanelQuestion({ panelId: 'hazard_coefficients', params: { model: selected }, exhibitLabel: 'Exhibit 1', title: 'Hazard-ratio coefficients', recap: model ? `${selected} hazard model: n=${model.n_fit.toLocaleString()}, ${model.coefficients.length} coefficients, McFadden R² ${model.mcfadden_r2.toFixed(4)}. Largest hazard ratio: ${model.coefficients.reduce((a, b) => (Math.abs(Math.log(b.hazard_ratio)) > Math.abs(Math.log(a.hazard_ratio)) ? b : a)).variable}.` : 'no data rendered yet', }) } > explainPanelQuestion({ panelId: 'fit_stats', exhibitLabel: 'Exhibit 2', title: 'Fit statistics', recap: coeffs?.fit_stats ? `Default hazard: train AUC ${coeffs.fit_stats.default.train_auc.toFixed(4)}, OOT AUC ${coeffs.fit_stats.default.oot_auc.toFixed(4)}. Prepayment hazard: train AUC ${coeffs.fit_stats.prepay.train_auc.toFixed(4)}, OOT AUC ${coeffs.fit_stats.prepay.oot_auc.toFixed(4)}.` : 'no data rendered yet', }) } > explainPanelQuestion({ panelId: 'seasoning_exhibits', exhibitLabel: 'Exhibit 3', title: 'Seasoning & term-structure exhibits', recap: `${seasoningExhibits.length} seasoning/term-structure exhibits rendered: ${seasoningExhibits.map((e) => e.title).join(', ')}.`, }) } >
{seasoningExhibits.map((e) => ( ))}
explainPanelQuestion({ panelId: 'variable_dictionary', exhibitLabel: 'Exhibit 4', title: 'Variable dictionary', recap: dict ? `${dict.rows.length} model variables documented, spanning baseline, borrower, collateral, macro and incentive families.` : 'no data rendered yet', }) } > explainPanelQuestion({ panelId: 'macro_glossary', exhibitLabel: 'Exhibit 5', title: 'Macro data glossary', recap: macroGlossary ? `${macroGlossary.series.length} macro series documented across DCR, SFLLD and the satellite: ${macroGlossary.series.map((s) => s.label).join('; ')}.` : 'no data rendered yet', }) } > explainPanelQuestion({ panelId: 'lgd', exhibitLabel: 'Exhibit 6', title: 'LGD — two-stage workout model', recap: lgd ? `Cure rate ${(lgd.cure_rate * 100).toFixed(1)}%, cure AUC (train/OOT) ${lgd.cure_auc.train.toFixed(3)}/${lgd.cure_auc.oot.toFixed(3)}, excess-loss loading ${(lgd.excess_loss_loading * 100).toFixed(2)}%.` : 'no data rendered yet', }) } > ); }