31  How well does the forest predict?

31.1 When to use it

Model performance is not one question. For a binary outcome, an ROC curve asks whether predicted scores rank events above non-events across thresholds. For a right-censored survival outcome, a Brier curve asks how far predicted survival probabilities sit from observed survival status over follow-up, with censoring handled in the score.

Neither display validates transportability. And neither estimates a causal treatment effect. They assess predictions from a fitted model.

31.2 Discrimination for a binary outcome

We fit a classification forest to SynthDiabetes, the synthetic diabetes set that mlbench ships. It stands in for the Pima Indians data the package and the UCI repository both withdrew, once it emerged that the original records had been shared without the community’s consent. The replacement keeps the same 768 rows, the same nine columns and the same neg/pos outcome, so nothing below had to change. The factor levels are printed before we choose the positive class, because which_outcome = 2 means the second level, pos, in this fit.

data(SynthDiabetes, package = "mlbench")
levels(SynthDiabetes$diabetes)
[1] "neg" "pos"
set.seed(20260828)
rf_class <- rfsrc(
  diabetes ~ .,
  data = SynthDiabetes,
  ntree = 300
)
rf_class
                         Sample size: 768
           Frequency of class labels: neg=488, pos=280
                     Number of trees: 300
           Forest terminal node size: 1
       Average no. of terminal nodes: 133.08
No. of variables tried at each split: 3
              Total no. of variables: 8
       Resampling used to grow trees: swor
    Resample size used to grow trees: 485
                            Analysis: RF-C
                              Family: class
                      Splitting rule: gini *random*
       Number of random split points: 10
                    Imbalanced ratio: 1.7429
                   (OOB) Brier score: 0.18490121
        (OOB) Normalized Brier score: 0.73960482
                           (OOB) AUC: 0.77604289
                      (OOB) Log-loss: 0.55333008
                        (OOB) PR-AUC: 0.63672898
                        (OOB) G-mean: 0.6909735
   (OOB) Requested performance error: 0.27213542, 0.19467213, 0.40714286

Confusion matrix:

          predicted
  observed neg pos class.error class.freq
       neg 394  94      0.1926        488
       pos 114 166      0.4071        280

      (OOB) Misclassification rate: 0.2708333

Random-classifier baselines (uniform):
   Brier: 0.25   Normalized Brier: 1   Log-loss: 0.69314718

The 300-tree teaching fit keeps execution bounded. Before reporting a study AUC, we would verify the error trajectory with enough trees and evaluate the model on the intended validation population.

31.3 Build and inspect the ROC object

gg_roc() defaults to OOB class probabilities for an rfsrc training fit. Its returned columns are sensitivity, specificity, and the threshold used at each point.

roc_dta <- gg_roc(rf_class, which_outcome = 2, oob = TRUE)
roc_dta
<gg_roc>  from randomForestSRC  |  family: class  |  ntree: 300  |  n: 768
head(as.data.frame(roc_dta), 4)
        sens      spec         pct
1 0.00000000 1.0000000 0.000000000
2 0.02049180 1.0000000 0.000000000
3 0.02868852 1.0000000 0.008547009
4 0.03483607 0.9964286 0.008771930
roc_check <- data.frame(
  positive_class = levels(SynthDiabetes$diabetes)[2],
  auc = calc_auc(roc_dta),
  min_fpr = min(1 - roc_dta$spec),
  max_fpr = max(1 - roc_dta$spec),
  min_tpr = min(roc_dta$sens),
  max_tpr = max(roc_dta$sens)
)
roc_check[-1] <- lapply(roc_check[-1], round, digits = 3)
roc_check
  positive_class   auc min_fpr max_fpr min_tpr max_tpr
1            pos 0.776       0       1       0       1
plot(roc_dta) +
  theme_hv_manuscript(base_size = 12)
Figure 31.1: OOB ROC curve for positive diabetes, with false-positive rate on the x-axis and sensitivity on the y-axis. The solid black line is the ROC curve, the dashed red diagonal marks chance ranking, and the black text gives the AUC.

The curve runs from the lower-left to the upper-right. The red 45-degree line is chance ranking; a useful curve bows toward the upper-left. AUC is on the 0 to 1 scale, with 0.5 representing chance and 1 perfect ranking. It says nothing by itself about whether a predicted probability of 0.8 occurs 80% of the time.

For a multi-class forest, request one class index at a time and name the one-vs-rest comparison in the caption. Current rfsrc behavior does not build a multi-class object from per_class = TRUE, so explicit class-specific calls are the maintained, auditable route here.

31.4 Time-resolved survival prediction error

Now we change both the outcome and the metric. The veteran data provide a right-censored survival outcome. gg_brier() wraps the censoring-aware Brier calculation from randomForestSRC and returns one score per event time.

data(veteran, package = "randomForestSRC")

set.seed(20260828)
rf_surv <- rfsrc(
  Surv(time, status) ~ .,
  data = veteran,
  ntree = 300
)
rf_surv
                         Sample size: 137
                    Number of deaths: 128
                     Number of trees: 300
           Forest terminal node size: 15
       Average no. of terminal nodes: 6.2533
No. of variables tried at each split: 3
              Total no. of variables: 6
       Resampling used to grow trees: swor
    Resample size used to grow trees: 87
                            Analysis: RSF
                              Family: surv
                      Splitting rule: logrank *random*
       Number of random split points: 10
                          (OOB) CRPS: 62.92693288
             (OOB) standardized CRPS: 0.06298992
   (OOB) Requested performance error: 0.30309059
brier_dta <- gg_brier(rf_surv, cens.model = "km")
brier_dta
<gg_brier>  from randomForestSRC  |  family: surv  |  ntree: 300  |  n: 137  |  CRPS (time-normalized): 0.06299
head(as.data.frame(brier_dta)[c("time", "brier", "crps")], 5)
  time      brier       crps
1    1 0.01512371 0.01512371
2    2 0.02272134 0.01892252
3    3 0.02997425 0.02263516
4    4 0.03507451 0.02593156
5    7 0.05580175 0.03568485
brier_check <- data.frame(
  time_min = min(brier_dta$time),
  time_max = max(brier_dta$time),
  brier_min = min(brier_dta$brier),
  brier_max = max(brier_dta$brier),
  censoring_model = attr(brier_dta, "cens.model"),
  integrated_crps = attr(brier_dta, "crps_integrated")
)
brier_check
  time_min time_max    brier_min brier_max censoring_model integrated_crps
1        1      999 1.466476e-05  0.197275              km        62.92693
plot(brier_dta) +
  theme_hv_manuscript(base_size = 12) +
  labs(x = "Follow-up time (days)", y = "Brier score")
Figure 31.2: OOB time-resolved Brier prediction error for the veteran survival forest; lower is better. The solid black line is the Brier score at each follow-up time.

The Brier score compares predicted survival probability with whether each patient is event-free at that time. Because some outcomes are unknown after censoring, the calculation uses inverse-probability-of-censoring weighting; cens.model = "km" estimates that censoring distribution with Kaplan-Meier. Whenever randomForestSRC can do so, it uses OOB values.

Lower is better, but the curve is not a calibration curve and not a discrimination curve. It combines both properties of probabilistic prediction. The far-right tail rests on very few patients with long follow-up, so we do not read its drop toward zero as evidence of improving performance. To study calibration, compare predicted and observed risks by horizon; to study survival discrimination, use a censoring-aware concordance or time-dependent ROC analysis.

31.5 Pitfalls

  • Choosing the wrong positive class. Print the factor levels, then map the numeric which_outcome index to the clinical event in the caption.
  • Reversing the ROC axes. The plot uses false-positive rate (1 - specificity) on x and sensitivity on y. The chance line rises from (0, 0) to (1, 1).
  • Calling AUC calibration. AUC measures ranking discrimination.
  • Calling Brier score calibration alone. Brier score is overall probabilistic prediction error, affected by calibration and discrimination.
  • Ignoring censoring and time units. Report the IPCW censoring model and put the cohort’s follow-up units on the x-axis.
  • Calling OOB external validation. OOB metrics are internal estimates. They do not show that performance will carry to another registry or era.