Random Survival Forests › Example

Michael BrenndoerferOctober 29, 20267 min read
Machine Learning from ScratchAll chapters
Part 8 of 15Tree-Based Models
Chapter 60Random Survival Forests › Example
Reading section 6 of 10 · Example · 7 minSection 6 of 10 · 7 min

Section 6 of 10 · 7 min read

Random Survival Forests › 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

The formulas are easier to trust once every number has been computed by hand. We use eight patients and a single binary covariate, so that the split is given and the arithmetic is the only thing left to do. Every quantity we compute below maps to one of the equations above: the risk sets and counts feed the split criterion, and the same counts feed the terminal-node estimate.

Group A is the low-risk group: four patients, of whom one has an event at time 8 and three are censored at times 1, 2 and 3. Group B is the high-risk group: four patients with events at times 1, 2, 3 and 4. The table below is the entire dataset.

PythonIn [18]
CollapseExpand
example = pd.DataFrame(
    {
        "patient": np.arange(1, 9),
        "group": ["A"] * 4 + ["B"] * 4,
        "time": [1, 2, 3, 8, 1, 2, 3, 4],
        "event": [0, 0, 0, 1, 1, 1, 1, 1],
    }
)
example["role"] = np.where(example["event"] == 1, "event", "censored")
Out[19]:
patient group time event role
0 1 A 1 0 censored
1 2 A 2 0 censored
2 3 A 3 0 censored
3 4 A 8 1 event
4 5 B 1 1 event
5 6 B 2 1 event
6 7 B 3 1 event
7 8 B 4 1 event

The split ss puts group A on the left and group B on the right. We compute the log-rank statistic at each distinct event time. There are five of them: 1, 2, 3, 4 and 8. At time tt, the risk set is everybody with a time at least tt, whether censored or not, because a censoring does not remove a patient from the risk set before their own observed time. Every number in the table below can be reconstructed from the patient table above by applying the definitions from the notation subsection one event time at a time.

Log-rank contributions at each distinct event time for the two-group split.
ttY(t)Y(t)d(t)d(t)YL(t)Y_L(t)dL(t)d_L(t)EL(t)E_L(t)V(t)V(t)
181400.500.250
261300.500.250
341200.500.250
421100.500.250
811111.000.000

At t=1t=1 the risk set is all eight patients, one of whom dies, and four of the eight are in group A, so the expected number of deaths in A is 1×4/8=0.51 \times 4/8 = 0.5. The variance is 1×7×4×4/(82×7)=0.251 \times 7 \times 4 \times 4 / (8^2 \times 7) = 0.25. The censoring at times 1, 2, and 3 does not remove those patients from the risk set until their own observed time: a patient censored at time 3 is still at risk for the events at times 1 and 2, and remains at risk for an event tied at time 3, leaving the risk set after that time. That is why the risk set shrinks gradually as the table progresses rather than dropping by the number of censored patients at each step, and it is why the arithmetic is easier to read from the table than from the change.

At t=8t=8 the risk set has a single patient, and that patient is the only one in group A with an event. The observed number of deaths in A is 1 and the expected number is 1, so the contribution to the numerator is zero. The displayed variance fraction would divide by zero; the comparison variance is assigned zero because the sole patient must have that event: no comparison between groups is possible when only one patient remains. The time contributes nothing, which is the correct behaviour.

Summing the columns gives the statistic:

∑j(dL(tj)−EL(tj))=0−0.5−0.5−0.5−0.5+0=−2.0,\sum_j \bigl(d_L(t_j) - E_L(t_j)\bigr) = 0 - 0.5 - 0.5 - 0.5 - 0.5 + 0 = -2.0, ∑jV(tj)=0.25+0.25+0.25+0.25=1.0,\sum_j V(t_j) = 0.25 + 0.25 + 0.25 + 0.25 = 1.0, L(s)=−2.01.0=−2.0.L(s) = \frac{-2.0}{\sqrt{1.0}} = -2.0.

The negative sign indicates fewer group-A events than expected in the log-rank comparison. The squared statistic is 4.04.0. For a prespecified grouping, its asymptotic one-degree-of-freedom chi-square tail probability is about 0.0460.046. With only eight patients and five events, that approximation should not be treated as reliable small-sample significance; it would also be inappropriate after searching many candidate splits.

Now we estimate the two terminal nodes. For group A, the only event time is 8, with one patient at risk and one death, so

H^A(8)=11=1.0,S^A(8)=e−1=0.368.\hat H_A(8) = \frac{1}{1} = 1.0, \qquad \hat S_A(8) = e^{-1} = 0.368.

For group B, the event times are 1, 2, 3 and 4, with risk sets of 4, 3, 2 and 1, so the increments of the cumulative hazard are the reciprocals of the risk sets: 1/41/4, 1/31/3, 1/21/2 and 1/11/1 at times 11, 22, 33 and 44. The cumulative hazard at each event time is the running sum of those increments:

H^B(1)=14=0.25H^B(2)=14+13=0.25+0.3333=0.5833H^B(3)=14+13+12=1.0833H^B(4)=14+13+12+11=2.0833\begin{aligned} \hat H_B(1) &= \frac{1}{4} = 0.25 \\ \hat H_B(2) &= \frac{1}{4} + \frac{1}{3} = 0.25 + 0.3333 = 0.5833 \\ \hat H_B(3) &= \frac{1}{4} + \frac{1}{3} + \frac{1}{2} = 1.0833 \\ \hat H_B(4) &= \frac{1}{4} + \frac{1}{3} + \frac{1}{2} + \frac{1}{1} = 2.0833 \end{aligned}

so the cumulative values at the four event times are 0.250.25, 0.58330.5833, 1.08331.0833 and 2.08332.0833, and the cumulative hazard at t=4t = 4 is 2.08332.0833. The survival function is the exponential of the negative of each cumulative value: e−0.25≈0.779e^{-0.25} \approx 0.779, e−0.5833≈0.558e^{-0.5833} \approx 0.558, e−1.0833≈0.338e^{-1.0833} \approx 0.338 and e−2.0833≈0.125e^{-2.0833} \approx 0.125.

The risk scores are the sums of the cumulative hazard over the shared grid {1,2,3,4,8}\{1, 2, 3, 4, 8\}. Group A has zero everywhere except at 8, where its cumulative hazard is 1.01.0, and is flat afterwards, so on the shared grid {1,2,3,4,8}\{1, 2, 3, 4, 8\} its risk score is τA=0+0+0+0+1.0=1.0\tau_A = 0 + 0 + 0 + 0 + 1.0 = 1.0. Group B has H^B\hat H_B equal to 0.250.25, 0.58330.5833, 1.08331.0833 and 2.08332.0833 at the event times 11, 22, 33 and 44, and is flat afterwards, so on the shared grid {1,2,3,4,8}\{1, 2, 3, 4, 8\} its risk score is τB=1/4+7/12+13/12+25/12+25/12=73/12≈6.0833\tau_B = 1/4 + 7/12 + 13/12 + 25/12 + 25/12 = 73/12 \approx 6.0833. Group A is ranked as the lower risk, which is what we expect.

PythonIn [20]
CollapseExpand
def step_function(times, values, grid):
    """Evaluate a right-continuous step function on an arbitrary grid."""
    if times.size == 0:
        return np.zeros(grid.size)
    position = np.searchsorted(times, grid, side="right") - 1
    return np.where(position >= 0, values[np.clip(position, 0, None)], 0.0)


hand_time = example["time"].to_numpy(dtype=float)
hand_event = example["event"].to_numpy(dtype=bool)
hand_left = (example["group"] == "A").to_numpy()

hand_stat = logrank_statistic(hand_time, hand_event, hand_left)
hand_curves = {}
for name, mask in [("A", hand_left), ("B", ~hand_left)]:
    hand_curves[name] = nelson_aalen(hand_time[mask], hand_event[mask])

grid_hand = np.array([1.0, 2.0, 3.0, 4.0, 8.0])
hand_risk = {
    name: step_function(t_h, h_h, grid_hand).sum()
    for name, (t_h, h_h) in hand_curves.items()
}
ConsoleOut [21]
CollapseExpand
log-rank statistic for the split: -2.0000
group A: event times [8.]
group A: cumulative hazard [1.]
group A: risk score 1.0000
group B: event times [1. 2. 3. 4.]
group B: cumulative hazard [0.25   0.5833 1.0833 2.0833]
group B: risk score 6.0833

The printed values reproduce the hand calculation. The left child (group A) is the lower-risk child, so the sign of the log-rank statistic is negative. If any printed value disagrees with the arithmetic above, the implementation has a bug, and the worked example is what makes that comparison possible. The split criterion and the terminal estimate are built from the same three numbers per event time: the risk-set size, the number of deaths, and the group membership. Those counts determine the two survival calculations once a grouping is fixed. The tree also needs the covariates and candidate thresholds to decide that grouping.

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