Can tidymodels deal with the retransformation problem?

Viewed 31

I was getting acquainted with tidymodels by reading the book and this line in section 9.2 kept me thinking about retransformation.

It is best practice to analyze the predictions on the transformed scale (if one were used) even if the predictions are reported using the original units.

But I found it confusing that the examples in the book use a log transformation on the outcome, but they do not use a recipe for this (the recipe has not been introduced at this point, but later when they introduce recipe, still they do not use step_log for the outcome but just for the predictors). So I wanted to try that and found something puzzling, illustrated with the reprex below:

# So let's use most of the code from the examples in the book

library(tidymodels)
tidymodels_prefer()

set.seed(501)

# data budget
data(ames)    
ames_split <- initial_split(ames, prop = 0.80, strata = Sale_Price)
ames_train <- training(ames_split)
ames_test  <-  testing(ames_split)    
ames_folds <- vfold_cv(ames_train, v = 10, strata = Sale_Price)

# IJALM    
lm_model <- 
  linear_reg(penalty = 0) |> 
  set_engine("glmnet") 

# And use a recipe, 
# but Instead of manually transforming the outcome like this ...
# `ames <- ames %>% mutate(Sale_Price = log10(Sale_Price))`
# let's include the outcome transformation into the recipe
simple_ames <- 
  recipe(
    Sale_Price ~ Neighborhood + Gr_Liv_Area + Year_Built + Bldg_Type,
    data = ames_train
  ) |> 
  step_log(Gr_Liv_Area, base = 10) |> 
  step_dummy(all_nominal_predictors()) |> 
  step_log(Sale_Price, base = 10, skip = TRUE)

lm_wflow <- 
  workflow() |> 
  add_model(lm_model) |> 
  add_recipe(simple_ames)

lm_res <- fit_resamples(
  lm_wflow,
  resamples = ames_folds, 
  control = control_resamples(save_pred = TRUE, save_workflow = TRUE),
  metrics = metric_set(rmse)
)

collect_metrics(lm_res)
#> # A tibble: 1 x 6
#>   .metric .estimator    mean     n std_err .config             
#>   <chr>   <chr>        <dbl> <int>   <dbl> <chr>               
#> 1 rmse    standard   197264.    10   1362. Preprocessor1_Model1

# Now, I wanted to double-check how rmse was calculated
# It should be the mean of the rmse for each fold 
# (individual values stored in this list lm_res$.metrics, 
# with one element for each fold)
# and each rmse should have been calculated with the predictions of each fold
# (stored in lm_res$.predictions)

# So, this rmse corresponding to the first fold
lm_res$.metrics[[1]]
#> # A tibble: 1 x 4
#>   .metric .estimator .estimate .config             
#>   <chr>   <chr>          <dbl> <chr>               
#> 1 rmse    standard     196074. Preprocessor1_Model1

# Should have been calculated with this data
lm_res$.predictions[[1]]
#> # A tibble: 236 x 4
#>    .pred  .row Sale_Price .config             
#>    <dbl> <int>      <int> <chr>               
#>  1  5.09     2     126000 Preprocessor1_Model1
#>  2  4.92    33     105900 Preprocessor1_Model1
#>  3  5.06    34     125500 Preprocessor1_Model1
#>  4  5.18    44     121500 Preprocessor1_Model1
#>  5  5.14    51     127000 Preprocessor1_Model1
#>  6  5.13    53     114000 Preprocessor1_Model1
#>  7  4.90    57      84900 Preprocessor1_Model1
#>  8  5.13    62     113000 Preprocessor1_Model1
#>  9  5.02    74      83500 Preprocessor1_Model1
#> 10  5.02    76      88250 Preprocessor1_Model1
#> # ... with 226 more rows
#> # i Use `print(n = ...)` to see more rows

# But here's the issue!
# The predictions are in the log-scale while the observed values
# are in the original units.

# This is just a quick-check to make sure the rmse reported above 
# (calculated by yardstick) does in fact involve mixing-up the log-scale 
# (predictions) and the original units (observed values)
yhat <- lm_res$.predictions[[1]]$.pred
yobs <- lm_res$.predictions[[1]]$Sale_Price
sqrt(mean((yhat - yobs)^2))
#> [1] 196073.6

# So, apparently, for cross-validation tidymodels does not `bake` the folds
# with the recipe to calculate the metrics

And here’s where I got it (at least I think so), after spending half an hour writing this reprex. So, to not feel I wasted my time, I decided to post it anyway, and put what I think is going on as an answer. Perhaps someone finds it useful, because it was not evident to me at the first time. Or perhaps someone can explain if there is something else going on.

Created on 2022-08-07 by the reprex package (v2.0.1)

1 Answers

It was basically your fault. You explicitly told tidymodels not to bake() this step. The line below was the culprit, in particular, the skip = TRUE part.

step_log(Sale_Price, base = 10, skip = TRUE)

Hence, tidymodels will not include that step in baking the folds before making the predictions and you end up with the log-scale of the predictions mixed-up with the untransformed outcome variable. This is perhaps one of the examples they had in mind when they wrote in the documentation that:

Care should be taken when using skip = TRUE as it may affect the computations for subsequent operations.

You probably decided to skip that step, because the outcome variable may not be available on a new dataset for which you want predictions, and then the process would fail. But it basically messes up metrics for cross-validation. So, better not to skip the step and deal other way with that problem.

Related