Trees, Forests and kNN in R (rpart, randomForest, kknn)

Context: FIT2086_MOC · Studio 9 · drills the W9 tree, Random Forest and kNN material on diabetes.*.csv · benchmark ➔ lasso · wrappers from Studio 8’s wrappers.R · extends R Toolkit (Cheatsheet) Problem it solves: fit a non-linear model to a numeric (or binary) target, size it by CV, read which predictors matter, and decide by test RMSE whether it beats a linear benchmark.

Quick Revision

  • 🎯 Trigger: “flexible model”, “which variables matter”, “beat the linear model?” ➔ rpart ➔ learn.tree.cv ➔ randomForest(importance = TRUE) ➔ train.kknn ➔ one RMSE per model vs cv.glmnet.f lasso.
  • ⚡ Key Constraint: the three packages predict differently — tree/forest predict(fit, test); kNN has no fitted model: fitted(kknn(formula, train, test, k =, kernel =)) every time.

🔧 Minimal Working Example

library(rpart); library(randomForest); library(kknn); library(glmnet)
source("wrappers.R")                                   # learn.tree.cv, plot.tree.cv, cv.glmnet.f, ...
diabetes.train = read.csv("diabetes.train.csv")         # n = 354: AGE SEX BMI BP S1..S6, target Y
diabetes.test  = read.csv("diabetes.test.csv")
rmse = function(yhat) sqrt(mean((yhat - diabetes.test$Y)^2))   # same scale as Y
 
# 1. tree: rpart's stopping heuristics, then size by 10-fold CV repeated m = 1000 times
tree.diabetes = rpart(Y ~ ., diabetes.train)
tree.diabetes$variable.importance / max(tree.diabetes$variable.importance)
cv = learn.tree.cv(Y ~ ., data = diabetes.train, nfolds = 10, m = 1000)
plot.tree.cv(cv)                                       # CV error vs leaves; minimum in red
rmse(predict(tree.diabetes, diabetes.test)); rmse(predict(cv$best.tree, diabetes.test))
 
# 2. linear benchmark
lasso.fit = cv.glmnet.f(Y ~ ., data = diabetes.train); glmnet.tidy.coef(lasso.fit)
rmse(predict.glmnet.f(lasso.fit, diabetes.test))
 
# 3. forest
rf.diabetes = randomForest(Y ~ ., data = diabetes.train, importance = TRUE, ntree = 5000)
rf.diabetes                                            # OOB MSE + % Var explained
rmse(predict(rf.diabetes, diabetes.test)); round(importance(rf.diabetes), 2)
 
# 4. kNN: CV over k = 1..25 and six kernels
kernels = c("rectangular","triangular","epanechnikov","gaussian","rank","optimal")
knn = train.kknn(Y ~ ., data = diabetes.train, kmax = 25, kernel = kernels)
ytest.hat = fitted(kknn(Y ~ ., diabetes.train, diabetes.test,
                        kernel = knn$best.parameters$kernel, k = knn$best.parameters$k))
rmse(ytest.hat)

(rmse() is a shorthand for the studio’s repeated sqrt(mean((… - diabetes.test$Y)^2)).)

Expected output:

ModelTest RMSEPredictors it uses / ranks top
rpart default, leavesBMI, S5, S1, S3, BP, S6, S2
CV-pruned tree, leavesBMI, S5, BP, S6 — S1, S2, S3 dropped
kNN, default settingsall predictors, none selected
lasso cv.glmnet.fBMI, BP, S3, S5
forest, trees—
forest, trees%IncMSE: BMI · S5 · BP
kNN, CV-chosen gaussian, about the lasso’s, a little worse than the forest—
  • Verdict ➔ forest lasso single tree ⟹ the relationship is at least somewhat linear; the forest’s small gain costs all interpretability ➔ always fit the linear benchmark.
  • Pruning ➔ half the leaves, RMSE ⟹ CV pruning rarely does much worse, can do much better when the full tree overfits, and always reads more easily.
  • Importance agrees ➔ rpart normalised: BMI , S5 , BP ⟹ the same top three as the forest’s %IncMSE and the lasso’s selection.

🔀 Variations

Reading print(tree)

node), split, n, deviance, yval      * = leaf
 1) root 354 2069865.000 148.30790
   2) BMI< 27.75 238  871378.700 120.11760
   3) BMI>=27.75 116  621294.500 206.14660
     6) BP< 101.5 63  376184.900 175.22220
  • yval ➔ mean of in that node (root = mean(Y)); check any node by hand: mean(diabetes.train$Y[diabetes.train$BMI >= 27.75 & diabetes.train$BP < 101.5]) .
  • Classification tree ➔ yval is the most likely class, followed by the class probabilities.
  • Repeat splits ➔ a numeric predictor may split more than once (S5 at , , ) — binary splits lose no generality; a categorical split may send several levels down one branch.

Three importance measures

MeasureCallDefined asScale
tree importancetree$variable.importancecontribution to improving the tree’s fitarbitrary ➔ divide by max()
%IncMSEimportance(rf)[, 1] (needs importance = TRUE)rise in OOB MSE when the predictor is randomly permuted ➔ the permutation idea% increase
IncNodePurityimportance(rf)[, 2]total purity gain from splits on the predictorarbitrary

Printed forest diagnostics

  • No. of variables tried at each split: 3 ➔ the random candidate subset per split (heuristic from ).
  • Mean of squared residuals: 3222.09 ➔ out-of-bag MSE: each row predicted only by trees whose bootstrap sample excluded it ⟹ , close to the test RMSE .
  • % Var explained: 44.89 ➔ OOB analogue of ⟹ .

Binary target (Pima / gene data)

tree = rpart(DIABETES ~ ., pima.train)
my.pred.stats(predict(tree, pima.test)[, 2], pima.test$DIABETES)          # col 2 = P(second level)
rf.pima = randomForest(DIABETES ~ ., data = pima.train)
my.pred.stats(predict(rf.pima, pima.test, type = "prob")[, 2], pima.test$DIABETES)
yhat.test = fitted(kknn(DIABETES ~ ., pima.train, pima.test))             # labels only
mean(yhat.test == pima.test$DIABETES) * 100                               # accuracy %: no AUC / log-loss

(The Studio 9 sheet writes my.prediction.stats() — the file name; the function that file defines is my.pred.stats ➔ Logistic Regression in R (glm, pROC, step).)

✍️ Practice

⚠️ Common Mistakes

  • 💡 predict(rf.pima, test) for AUC ➔ returns class labels; add type = "prob" and take column 2.
  • 💡 Treating kNN like a fitted model ➔ there is nothing to predict(); pass train and test to kknn() each time — prediction slows as the training set grows.
  • 💡 Over-reading the CV tree size ➔ CV error is nearly flat over – leaves and CV is random; large m stabilises the choice but “about 7” is the honest reading.
  • 💡 Skipping the linear benchmark ➔ here lasso RMSE beat the best single tree’s ; a flexible model is only worth its opacity if it beats the linear one.