Random Survival Forests › Example
Machine Learning from ScratchAll chapters 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
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]Expand
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")| 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 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 , the risk set is everybody with a time at least , 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.
| 1 | 8 | 1 | 4 | 0 | 0.50 | 0.250 |
| 2 | 6 | 1 | 3 | 0 | 0.50 | 0.250 |
| 3 | 4 | 1 | 2 | 0 | 0.50 | 0.250 |
| 4 | 2 | 1 | 1 | 0 | 0.50 | 0.250 |
| 8 | 1 | 1 | 1 | 1 | 1.00 | 0.000 |
At 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 . The variance is . 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 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:
The negative sign indicates fewer group-A events than expected in the log-rank comparison. The squared statistic is . For a prespecified grouping, its asymptotic one-degree-of-freedom chi-square tail probability is about . 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
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: , , and at times , , and . The cumulative hazard at each event time is the running sum of those increments:
so the cumulative values at the four event times are , , and , and the cumulative hazard at is . The survival function is the exponential of the negative of each cumulative value: , , and .
The risk scores are the sums of the cumulative hazard over the shared grid . Group A has zero everywhere except at 8, where its cumulative hazard is , and is flat afterwards, so on the shared grid its risk score is . Group B has equal to , , and at the event times , , and , and is flat afterwards, so on the shared grid its risk score is . Group A is ranked as the lower risk, which is what we expect.
PythonIn [20]Expand
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]Expand
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!