Evaluating Survival Predictions › Example

Michael BrenndoerferOctober 27, 20267 min read
Machine Learning from ScratchAll chapters
Part 6 of 15Survival Analysis
Chapter 52Evaluating Survival Predictions › Example
Reading section 6 of 10 · Example · 7 minSection 6 of 10 · 7 min

Section 6 of 10 · 7 min read

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

Notes & comments

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]
CollapseExpand
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(3,1,0.90)(3, 1, 0.90), B(5,1,0.60)(5, 1, 0.60), C(6,0,0.70)(6, 0, 0.70), D(8,1,0.30)(8, 1, 0.30), E(9,0,0.40)(9, 0, 0.40), F(12,0,0.10)(12, 0, 0.10). There are (62)=15\binom{6}{2} = 15 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 rA=0.90r_A = 0.90 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 0.60<0.700.60 < 0.70, giving one discordant pair. Against D, E, F the scores are 0.30,0.40,0.100.30, 0.40, 0.10, all below 0.600.60, 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, 0.30<0.400.30 < 0.40 is discordant. Against F, 0.30>0.100.30 > 0.10 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

C^=9+12⋅011=911≈0.818.\hat{C} = \frac{9 + \tfrac{1}{2}\cdot 0}{11} = \frac{9}{11} \approx 0.818.

Thus C^≈0.818\hat{C} \approx 0.818: 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 τ∗=10\tau^*=10 months, within positive censoring support. All eleven usable pairs have earlier failures before this cutoff. Their failure times do not tie censorings, so G^(ti−)=G^(ti)\hat G(t_i-)=\hat G(t_i) and the product weight reduces to G^(ti)−2\hat G(t_i)^{-2}. The censoring survival function comes from the same Kaplan–Meier machinery applied to the censoring indicator:

G^(3)=1,G^(5)=1,G^(6)=34,G^(8)=34,G^(9)=38,G^(12)=0.\hat{G}(3) = 1, \quad \hat{G}(5) = 1, \quad \hat{G}(6) = \tfrac{3}{4}, \quad \hat{G}(8) = \tfrac{3}{4}, \quad \hat{G}(9) = \tfrac{3}{8}, \quad \hat{G}(12) = 0.

The step at month 6 comes from one censoring event among the four units still under observation, so the curve drops from 1 to 1⋅(1−1/4)=0.751\cdot(1 - 1/4) = 0.75. At month 9 one of the two remaining units is censored, so it drops further to 0.75⋅(1−1/2)=0.3750.75\cdot(1 - 1/2) = 0.375. 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 G^(ti)−2\hat{G}(t_i)^{-2} applied to the earlier member of each pair. A's pairs get weight 1/12=11/1^2 = 1 each, contributing 55 to the numerator and 55 to the denominator. B's pairs get 1/12=11/1^2 = 1 each, contributing 33 to the numerator (three concordant) and 44 to the denominator (four usable pairs). D's pairs get 1/0.752=1.77781/0.75^2 = 1.7778 each, contributing 1.77781.7778 and 3.55563.5556. Therefore

C^Uno=5+3+1.77785+4+3.5556=9.777812.5556≈0.779.\hat{C}_{\text{Uno}} = \frac{5 + 3 + 1.7778}{5 + 4 + 3.5556} = \frac{9.7778}{12.5556} \approx 0.779.

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]
CollapseExpand
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]
CollapseExpand
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 1/G^1/\hat{G} weights.

Go unit by unit. A failed at month 3, before τ\tau, so the outcome is 0 and the squared error is 0.552=0.30250.55^2 = 0.3025; its weight is 1/G^(3)=11/\hat{G}(3) = 1, giving 0.30250.3025. B failed at month 5, squared error 0.622=0.38440.62^2 = 0.3844, weight 1/G^(5)=11/\hat{G}(5) = 1, giving 0.38440.3844. C was censored at month 6, before τ\tau, so its outcome is unknown and it contributes 00. D failed exactly at τ=8\tau = 8, which counts as a failure by τ\tau, so its outcome is 0 and the error is 0.752=0.56250.75^2 = 0.5625; the weight is 1/G^(8)=1.33331/\hat{G}(8) = 1.3333, giving 0.750.75. E and F both outlived τ\tau, so their outcome is 1. E contributes (1−0.80)2/G^(8)=0.04/0.75=0.0533(1 - 0.80)^2/\hat{G}(8) = 0.04/0.75 = 0.0533, and F contributes (1−0.85)2/0.75=0.0225/0.75=0.03(1 - 0.85)^2/0.75 = 0.0225/0.75 = 0.03.

Summing and dividing by n=6n = 6:

BS^(8)=0.3025+0.3844+0+0.75+0.0533+0.036=1.52026≈0.2534.\widehat{\text{BS}}(8) = \frac{0.3025 + 0.3844 + 0 + 0.75 + 0.0533 + 0.03}{6} = \frac{1.5202}{6} \approx 0.2534.
PythonIn [18]
CollapseExpand
tiny_naive = contrib_naive.mean()
tiny_drop = float(
    contrib_naive[~((tiny_times <= tau_tiny) & (tiny_event == 0))].mean()
)
ConsoleOut [19]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
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!

Newsletter

Stay up to date

Get new articles, book updates, and notes on AI and technology.

Unsubscribe at any time. Privacy policy