🌳 Dortmund real estate market analysis: tree-based methods

In pervious posts traditional regression models were fitted to real estate data. In this post tree-based models, namely random forests and gradient boosting, are trained to predict prices of the rent. These methods typically outperform traditional regression models yielding smaller errors. Furthermore, tree-based methods are much more robust to overfitting, which makes them superior in terms of prediction. However, the main disadvantage (and the reason why there is no love in insurance industry) is difficulties with interpretability.

Random forests

Originally random forests are implemented in randomForest package. There is also a faster implementation in ranger package, which is used further. As usual, we start with some preliminary code to clean out the memory, load packages and data.

packages <- c("ggplot2", "magrittr", "vtreat", "ranger", "xgboost", "caret")
sapply(packages, library, character.only = TRUE, logical.return = TRUE)

theme_set(theme_bw())
theme_update(text = element_text(size = 24))

rm(list = ls())

setwd("/Users/irudnyts/Documents/data/")
property <- read.csv("dortmund.csv")
set.seed(1)

For random forests there is a hyperparameter mtry determining the number of columns to split in each node. As long as we have only two columns, we set mtry = 2 (the model with default mtry = 1 yields higher RMSE). We also use default number of trees (500 trees). The main function to run the regression is ranger. The function predict(.ranger) behaves a bit differently from other methods. Instead of returning a vector, it returns a list of vectors of class ranger.prediction. Thus, we need to extract predictions form that class.

rf <- ranger(formula = price ~ area + rooms, data = property, mtry = 2)
predicted <- predict(rf, property)
(predicted$predictions - property$price) ^ 2 %>% mean() %>% sqrt()
# [1] 93.07542

Random forests model outperform all traditional regression models in previous posts in terms of in-sample RMSE. Let’s now cross-validate model by looking at out-of-sample RMSE:

property$pred_rf <- NULL

folds <- kWayCrossValidation(nRows = nrow(property), nSplits = 3)

for(fold in folds) {
    rf <- ranger(formula = price ~ area + rooms,
                 data = property[fold$train, ],
                 mtry = 2)
    property[fold$app, "pred_rf"] <-
        predict(rf, property[fold$app, ])$predictions
}

(property$price - property$pred_rf) ^ 2 %>% mean() %>% sqrt()
# [1] 152.3637

The out-of-sample RMSE is comparable with one returned by linear model, but larger than for GAM IG.

Gradient boosting

This methods iteratively optimize the RMSE (or other accuracy measure) on training data. Thus, gradient boosting is more exposed to overfit. In order to get the optimal ‘nrounds’ (the maximum number of iterations) we need to use cross-validation technique, implemented in xgb.cv, which also calculates out-of-sample RMSE. The number of folds we keep equals to $3$, to be consistent with pervious analysis. The learning rate parameter is set to be $0.1$, a little bit less than default ($0.3$), implying robustness to overfit, but also slower speed.

xg <- xgb.cv(data = as.matrix(property[, 2:3]),
             label = property[, 1],
             nfold = 3,
             nrounds = 500,
             metrics = "rmse",
             eta = 0.1)
which.min(xg$evaluation_log$test_rmse_mean)
# [1] 32
ggplot(data = xg$evaluation_log) + geom_line(aes(x = iter, y = test_rmse_mean))

The minimum out-of-sample RMSE is showed by the model with nrounds = 32. This value is approximate and depends on a seed. We use nrounds = 40. Let’s calculate RMSE for the optimal model and then out-of sample RMSE:

xg <- xgboost(data = as.matrix(property[, 2:3]),
              label = property[, 1],
              nfold = 3,
              nrounds = 34,
              eta = 0.1)

property$pred_xg <- predict(xg, as.matrix(property[, 2:3]))

(property$price - property$pred_xg) ^ 2 %>% mean() %>% sqrt()
# [1] 104.8407

property$pred_xg <- NULL
set.seed(1)
folds <- kWayCrossValidation(nRows = nrow(property), nSplits = 3)

for(fold in folds) {
    xg <- xgboost(data = as.matrix(property[fold$train, 2:3]),
                  label = property[fold$train, 1],
                  nfold = 3,
                  nrounds = 40,
                  eta = 0.1)
    property[fold$app, "pred_xg"] <-
        predict(xg, as.matrix(property[fold$app, 2:3]))
}

(property$price - property$pred_xg) ^ 2 %>% mean() %>% sqrt()
# [1] 145.9076

Unfortunately, XGboost model does not achieve smaller RMSE than previous models.

As summary, tree based methods are not always better solution for prediction. As we see, the out-of-sample RMSE is similar to one for linear model. At the same time, tree-based methods lose precious interpretability.

Note: Both packages have built-in cross-validation functions. However, for proper comparison one has to train models on the same training data set applying, then, to the same test set.