Evaluating Survival Predictions › Example
Machine Learning from ScratchAll chapters Evaluating Survival Predictions › ExampleSection view · Sections in this chapter6 / 10 ↓
Reading tools Definitions, links & notes
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
We now work the same six units through the arithmetic by hand. The purpose is not to replace the code but to make every number in the code auditable. Once the formulas have been evaluated on a table this small, the vectorised implementation has a reference point we can trust.
ConsoleOut [15]Expand
unit time event risk pred_surv_at_8 A 3.0 1 0.9 0.55 B 5.0 1 0.6 0.62 C 6.0 0 0.7 0.70 D 8.0 1 0.3 0.75 E 9.0 0 0.4 0.80 F 12.0 0 0.1 0.85
Working through concordance by hand
List the units with their observed time, event indicator, and risk score: A, B, C, D, E, F. There are unordered pairs, and we classify each one by asking whether the earlier member is an observed failure.
Begin with A. A failed at month 3, so every pair in which A is the earlier member is usable: A with B, C, D, E, and F. That is five pairs, and in every one of them exceeds the other score. Five concordant pairs.
B fails at month 5, so the four pairs B vs C, B vs D, B vs E, B vs F are usable. Against C the model is wrong, since , giving one discordant pair. Against D, E, F the scores are , all below , giving three concordant pairs. Running total: eight concordant, one discordant.
C is censored, so the three pairs C vs D, C vs E, C vs F are excluded. D fails at month 8, so D vs E and D vs F are usable. Against E, is discordant. Against F, is concordant. Running total: nine concordant, two discordant. The remaining pair, E vs F, has both members censored and is excluded.
That gives eleven usable pairs, of which nine are concordant and two discordant, so
Thus : roughly four times in five, when the data tell us which of two units failed first, the model agrees. Note that the four excluded pairs are not a random sample of the fifteen: they all involve C or E, two of the three censored units, so any claim about the model's accuracy in the late-failure region rests on very little evidence.
For the teaching calculation we fix months, within positive censoring support. All eleven usable pairs have earlier failures before this cutoff. Their failure times do not tie censorings, so and the product weight reduces to . The censoring survival function comes from the same Kaplan–Meier machinery applied to the censoring indicator:
The step at month 6 comes from one censoring event among the four units still under observation, so the curve drops from 1 to . At month 9 one of the two remaining units is censored, so it drops further to . At month 12 the last unit is censored and the estimate reaches zero, so the strict Brier implementation used here rejects horizons at or beyond month 12. Censoring exactly at month 12 still establishes survival past that boundary; using that outcome would require the inclusive survivor selection and its matching left-limit censoring weight.
The pair weights are applied to the earlier member of each pair. A's pairs get weight each, contributing to the numerator and to the denominator. B's pairs get each, contributing to the numerator (three concordant) and to the denominator (four usable pairs). D's pairs get each, contributing and . Therefore
The IPCW version is lower because D's pairs, one of which is discordant, now carry extra weight relative to the others. The two numbers answer slightly different questions: Harrell targets the mix of pairs this study's censoring pattern allowed us to resolve, while truncated IPCW targets true failure pairs whose earlier failure precedes the specified cutoff, under the censoring assumptions.
Working through the IPCW Brier score by hand
Compute the Graf contributions in code so every displayed value comes from the data, then check them against the formulas.
PythonIn [16]Expand
tiny_g_at_t = step_value(tiny_times, tiny_g_times, tiny_g, side="left")
tiny_alive = tiny_times > tau_tiny
tiny_failed = (tiny_times <= tau_tiny) & (tiny_event == 1)
tiny_terms = np.zeros_like(tiny_times)
tiny_terms[tiny_failed] = tiny_pred[tiny_failed] ** 2 / tiny_g_at_t[tiny_failed]
tiny_terms[tiny_alive] = (1.0 - tiny_pred[tiny_alive]) ** 2 / g_tau
tiny_ipcw = float(tiny_terms.mean())ConsoleOut [17]Expand
unit A: IPCW contribution = 0.3025 unit B: IPCW contribution = 0.3844 unit C: IPCW contribution = 0.0000 unit D: IPCW contribution = 0.7500 unit E: IPCW contribution = 0.0533 unit F: IPCW contribution = 0.0300 IPCW Brier at tau = 8 = 0.2534
The table shows each unit's contribution, with censored units before the horizon contributing zero and the observed units carrying their weights.
Go unit by unit. A failed at month 3, before , so the outcome is 0 and the squared error is ; its weight is , giving . B failed at month 5, squared error , weight , giving . C was censored at month 6, before , so its outcome is unknown and it contributes . D failed exactly at , which counts as a failure by , so its outcome is 0 and the error is ; the weight is , giving . E and F both outlived , so their outcome is 1. E contributes , and F contributes .
Summing and dividing by :
PythonIn [18]Expand
tiny_naive = contrib_naive.mean()
tiny_drop = float(
contrib_naive[~((tiny_times <= tau_tiny) & (tiny_event == 0))].mean()
)ConsoleOut [19]Expand
IPCW Brier at tau = 8: 0.2534 Censored unit scored as survivor: 0.2337 Censored unit dropped: 0.2624
Scoring the censored unit as a survivor gives the model unearned credit for an outcome nobody observed, and dropping the censored unit conditions the sample on its horizon outcome being observable, which changes the question being asked. Under the stated censoring assumptions, IPCW targets the complete-outcome Brier error without imputing the unknown outcomes; finite-sample estimation error remains.
Verifying the arithmetic with code
The same two calculations, written as explicit loops over pairs, confirm the hand arithmetic and give us a reference to check the vectorised implementation against.
PythonIn [20]Expand
concordant = 0
discordant = 0
for i in range(len(tiny)):
for j in range(len(tiny)):
if tiny_event[i] == 1 and tiny_times[i] < tiny_times[j]:
if tiny_risk[i] > tiny_risk[j]:
concordant += 1
elif tiny_risk[i] < tiny_risk[j]:
discordant += 1
hand_c = concordant / (concordant + discordant)
tau_star_tiny = 10.0
pair_weights = []
pair_scores = []
for i in range(len(tiny)):
if tiny_event[i] == 1 and tiny_times[i] < tau_star_tiny:
assert g_before_t[i] > 0 and g_at_t[i] > 0
weight = 1.0 / (g_before_t[i] * g_at_t[i])
for j in range(len(tiny)):
if tiny_times[i] < tiny_times[j]:
pair_weights.append(weight)
pair_scores.append(weight * (tiny_risk[i] > tiny_risk[j]))
hand_uno = float(np.sum(pair_scores) / np.sum(pair_weights))
hand_brier = contrib_ipcw.mean()ConsoleOut [21]Expand
usable pairs : 11 (concordant 9, discordant 2) Harrell C : 0.8182 Uno C : 0.7788 IPCW Brier at tau=8 : 0.2534 naive Brier at tau=8: 0.2337
Three hand calculations, three confirmations. With the definitions validated on a cohort we can inspect, we can now scale up to a realistic sample where the models themselves must be fitted.
Comments
Add to the conversation
A question, a correction, or an example from your own work.
No comments yet. Be the first to share your thoughts!