Evidence

Every number, and what it rests on.

These charts are drawn from JSON the pipeline regenerates, not from images exported once and left to rot. Each figure carries the kind of evidence behind it, because “measured” and “simulated” are not the same claim, and a results page that blurs the two is doing something dishonest.

The score means what it says

#calibration

A collections agent reads the risk number and acts on its face value, so calibration is the gate, not AUC. A model can rank perfectly and still be badly calibrated. Points on the diagonal mean a predicted 30% happened about 30% of the time.

L1 Brier against a base rate of ; AUC . L2 AUC . Hover a point for its bin count. Method: time split at , never random. A random split leaks the future into the past on a panel. Metrics are unweighted on the delinquency-enriched sample, which is the frame the Risk Lens actually ranks in. Reproduce with python -m ganymede.risk --backtest.

The latency budget was measured, not chosen

#latency

Every inter-turn gap in a real ten-minute call. Drag the budget line: bars to its left are boundaries too narrow for a hint arriving at that latency, so the hint lands after the moment it addressed has passed.

300 ms of real boundaries are wide enough
gaps · median · p25 · p10 . The enforced budget is , the p25 rounded up. Method: energy VAD over 16 kHz mono audio; a silence shorter than 200 ms is treated as a within-speech micro-pause rather than a hand-off. Real 8 kHz telephony would shift this, and that gap is listed as open rather than corrected for. Reproduce with python -m ganymede.audio.vad.

The world moved, and the monitor saw it

#drift

Monthly self-cure rate among delinquent accounts. The training window sits left of the split, the test window right of it. No model calibrates to a regime shift it never saw, which is the point of having a drift monitor rather than an annual review.

Train → test , against a rate-change alert threshold of . PSI on the delinquency bucket: (alert at ). The L2 evaluation deliberately mirrors production: the booster trains once, then calibration is refit on the earlier half of the test window and scored on the later half, which is what the outcome loop does as new outcomes land. The booster never sees the evaluation rows.

How a book actually deteriorates

#roll

Where an account sits next month, given where it sits today. Read a row. Half of accounts one month behind cure on their own. One month later that has collapsed, and the dominant move is deeper delinquency. The month when a conversation is cheap and effective is the month before most systems are looking.

Every month-to-month transition across panel rows over loans, . Cells shade by rate; hover for the underlying count. Method: Freddie Mac single-family servicing records, joined to origination facts, one row per loan per month with real calendar dates. The panel gate fails the build if the reporting period ever runs backwards within a loan. Reproduce with python -m ganymede.panel --verify.

The allocator's edge, across every capacity

#frontier

Not one number, but the whole curve. The advantage over risk-ranking is largest when capacity is scarce and converges toward zero as capacity approaches full coverage, because with enough agents to call everybody the ordering stops mattering.

At the default 15% capacity the edge is , using contacts against . Move the capacity slider yourself → Honest boundary. The servicing data carries no treatment history, so uplift cannot be measured here. It is modelled from published practice: largest in the persuadable middle, near zero at both ends. This demonstrates the allocator's logic beating risk-ranking under a defensible shape. The magnitude is not a claim about real lift, which is why the badge says simulated. Reproduce with python -m ganymede.allocator --simulate.

Every headline figure

#table

Including the four this project deliberately refuses to estimate. Filter by the kind of evidence.

FigureValueEvidenceSource
Regenerate the whole table with python scripts/build_site_data.py. CI runs it in --check mode, so a figure that drifts from what the pipeline now produces fails the build rather than sitting stale on this page.