Tidymodels: el paquete recipes

Aprenda a preprocesar sus datos de una manera simple y consistente.

¿Qué es tidymodels?


tidymodels es un nuevo ecosistema que consiste en una serie de paquetes que facilitan el proceso de modelado en proyectos de ciencia de datos. Permite de manera unificada realizar remuestreo, preprocesamiento de datos, aporta una interfaz de modelado unificado y validación de resultados. El ciclo completo sería el siguiente:

En esta publicación nos centraremos en el paso del preprocesamiento de datos con el paquete recipes.


Introducción a recipes


Este paquete nace del esfuerzo de reunir todos los pasos de preparación de datos antes de aplicar un modelo de manera simple, eficiente y consistente. recipes nace de la analogía entre preparar una receta de cocina y preprocesar sus datos … ¿cuál es la similitud? Ambos siguen algunos pasos antes de cocinar (modelar).

Cada receta consta de cuatro pasos fundamentales:

  • recipe(): Se especifica la fórmula (variables predictoras y variables de respuesta).

  • step_xxx(): Se definen los pasos a seguir: imputación de missing values, variables dummy, centrado, escalado, etc.

  • prep(): Preparación de la receta. Esto significa que se utiliza un conjunto de datos para analizar cada paso en él.

  • bake(): Aplique los pasos de preprocesamiento a sus conjuntos de datos.

En esta publicación cubriremos estas cuatro partes y veremos ejemplos de buenas prácticas. Espero poder convencerte de que en adelante, tidymodels en general y recipes en particular, son una buena opción.

Un breve ejemplo: Airquality dataset

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
library(rsample)
library(recipes)

data("airquality")

head(airquality) %>% 
  knitr::kable('html') %>% 
  kableExtra::kable_styling(position = 'left', 
                            font_size = 10, 
                            fixed_thead = list(enabled = T, background = "blue")
                            ) 
Ozone Solar.R Wind Temp Month Day
41 190 7.4 67 5 1
36 118 8.0 72 5 2
12 149 12.6 74 5 3
18 313 11.5 62 5 4
NA NA 14.3 56 5 5
28 NA 14.9 66 5 6

Primero, vamos a separar nuestro conjunto de datos en un conjunto de datos de entrenamiento y uno de test. Utilizaremos el 80% de nuestros datos para entrenar y el 20% restante para testear.

1
(.df<-airquality %>% initial_split(prop = 0.8))
## <Analysis/Assess/Total>
## <122/31/153>
1
2
3
.df_train<-.df %>% training()

.df_test<-.df %>% testing()

Se ver cómo rsample nos arroja un objeto dividido, donde se nos dice cuántos registros se utilizan en cada conjunto de datos y el total. A partir de las funciones training () y testing () podemos extraer los datos correspondientes.

Creando un recipiente

Ahora es el momento de crear nuestra primera receta.

1
2
3
.recipe<-recipe(Ozone ~ ., data = .df_train)

summary(.recipe)
## # A tibble: 6 x 4
##   variable type    role      source  
##   <chr>    <chr>   <chr>     <chr>   
## 1 Solar.R  numeric predictor original
## 2 Wind     numeric predictor original
## 3 Temp     numeric predictor original
## 4 Month    numeric predictor original
## 5 Day      numeric predictor original
## 6 Ozone    numeric outcome   original

Se puede ver cómo ‘recipes’ asigna a cada variable un tipo y un rol. Esto nos permitirá seleccionar posteriormente a qué variables aplicar un paso de preprocesamiento en función de su tipología o rol. Una opción muy interesante que permite recipes es actualizar los roles de las variables. Por ejemplo, en este conjunto de datos tenemos dos columnas que tienen valores faltantes: ‘Ozone’ y ‘Solar.R’. Podemos asignar a estas variables un rol específico que luego nos permitirá identificarlas. Crearemos el nuevo rol ‘NA_Variable’ con la función udpate_role():

1
2
3
.recipe<-.recipe %>% update_role(Ozone, Solar.R, new_role = 'NA_Variable')

summary(.recipe)
## # A tibble: 6 x 4
##   variable type    role        source  
##   <chr>    <chr>   <chr>       <chr>   
## 1 Solar.R  numeric NA_Variable original
## 2 Wind     numeric predictor   original
## 3 Temp     numeric predictor   original
## 4 Month    numeric predictor   original
## 5 Day      numeric predictor   original
## 6 Ozone    numeric NA_Variable original

Seleccionando los pasos de preprocesamiento

Una vez que se crea la receta, es el turno de agregar los pasos necesarios para llevar a cabo el preprocesamiento de los datos. Tenemos muchos pasos para elegir:

1
ls('package:recipes', pattern = '^step_')
##  [1] "step_arrange"       "step_bagimpute"     "step_bin2factor"   
##  [4] "step_BoxCox"        "step_bs"            "step_center"       
##  [7] "step_classdist"     "step_corr"          "step_count"        
## [10] "step_cut"           "step_date"          "step_depth"        
## [13] "step_discretize"    "step_downsample"    "step_dummy"        
## [16] "step_factor2string" "step_filter"        "step_geodist"      
## [19] "step_holiday"       "step_hyperbolic"    "step_ica"          
## [22] "step_impute_bag"    "step_impute_knn"    "step_impute_linear"
## [25] "step_impute_lower"  "step_impute_mean"   "step_impute_median"
## [28] "step_impute_mode"   "step_impute_roll"   "step_indicate_na"  
## [31] "step_integer"       "step_interact"      "step_intercept"    
## [34] "step_inverse"       "step_invlogit"      "step_isomap"       
## [37] "step_knnimpute"     "step_kpca"          "step_kpca_poly"    
## [40] "step_kpca_rbf"      "step_lag"           "step_lincomb"      
## [43] "step_log"           "step_logit"         "step_lowerimpute"  
## [46] "step_meanimpute"    "step_medianimpute"  "step_modeimpute"   
## [49] "step_mutate"        "step_mutate_at"     "step_naomit"       
## [52] "step_nnmf"          "step_normalize"     "step_novel"        
## [55] "step_ns"            "step_num2factor"    "step_nzv"          
## [58] "step_ordinalscore"  "step_other"         "step_pca"          
## [61] "step_pls"           "step_poly"          "step_profile"      
## [64] "step_range"         "step_ratio"         "step_regex"        
## [67] "step_relevel"       "step_relu"          "step_rename"       
## [70] "step_rename_at"     "step_rm"            "step_rollimpute"   
## [73] "step_sample"        "step_scale"         "step_select"       
## [76] "step_shuffle"       "step_slice"         "step_spatialsign"  
## [79] "step_sqrt"          "step_string2factor" "step_unknown"      
## [82] "step_unorder"       "step_upsample"      "step_window"       
## [85] "step_YeoJohnson"    "step_zv"

Algunos de los más comunes son los siguientes:

  • step_XXXimpute(): Métodos para imputar missing values como meanimpute, medianimpute, knnimpute …

  • step_range(): Normaliza los datos numéricos para estar dentro de un rango de valores predefinido.

  • step_center(): Normaliza los datos numéricos para tener una media de cero.

  • step_scale(): Normaliza los datos numéricos para tener una desviación estándar de uno.

  • step_dummy(): Convierte datos nominales (por ejemplo, caracteres o factores) en uno o más términos numéricos de modelo binario para los niveles de los datos originales.

  • step_other(): Paso que potencialmente agrupará valores que ocurren con poca frecuencia en una categoría de “otros”.

El orden en que se ejecutan los pasos es importante, como se puede leer en la página oficial del paquete:

  1. Imputar
  2. Transformaciones individuales para la asimetría y otros problemas.
  3. Discretice (si es necesario y si no tiene otra opción)
  4. Crea variables dummy
  5. Crea interacciones
  6. Pasos de normalización (centrado, escalado, rango, etc.)
  7. Transformación multivariada (por ejemplo, PCA)

Además, en cada paso debemos especificar a qué columnas afecta ese paso. Hay varias formas de hacerlo, mencionaremos las más comunes:

  1. Pasando el nombre de la variable en el primer argumento

  2. Selección por el papel de las variables con las funciones all_predictors() y all_outcomes(). Como en nuestro caso hemos cambiado los roles ‘predeterminados’ a ‘NA_Variable’, podemos usar la función has_role() para seleccionarlos.

  3. Seleccionando por el tipo de las variables con las funciones all_nominal() y all_numerical().

  4. Usando los selectores dplyr como contains(), starts_with() o ends_with() funciones.

Vamos a aplicar algunos de estos pasos a nuestro ejemplo:

1
2
3
4
5
.recipe<-.recipe %>% 
          step_meanimpute(has_role('NA_Variable'), -Solar.R) %>%
          step_knnimpute(contains('.R'), neighbors = 3) %>%
          step_center(all_numeric(), -has_role('NA_Variable')) %>%
          step_scale(all_numeric(), -has_role('NA_Variable'))
## Warning: `step_knnimpute()` was deprecated in recipes 0.1.16.
## Please use `step_impute_knn()` instead.
## Warning: `step_meanimpute()` was deprecated in recipes 0.1.16.
## Please use `step_impute_mean()` instead.
1
.recipe
## Data Recipe
## 
## Inputs:
## 
##         role #variables
##  NA_Variable          2
##    predictor          4
## 
## Operations:
## 
## Mean Imputation for has_role("NA_Variable"), -Solar.R
## K-nearest neighbor imputation for contains(".R")
## Centering for all_numeric(), -has_role("NA_Variable")
## Scaling for all_numeric(), -has_role("NA_Variable")

Se puede ver cómo combinando todas las técnicas de selección de variables obtenemos una gran versatilidad. También comentar que cuando se usa el signo menos significa que las columnas que cumplen con esta condición están excluidas del paso de preprocesamiento. También se puede ver cómo el objeto de recetas ahora especifica todos los pasos que se llevarán a cabo y en qué variables.

Preparando la receta

Ha llegado el momento de preparar la receta en un conjunto de datos. Una vez preparado, podemos aplicar esta receta en múltiples conjuntos de datos:

1
(.recipe<-.recipe %>% prep(.df_train))
## Data Recipe
## 
## Inputs:
## 
##         role #variables
##  NA_Variable          2
##    predictor          4
## 
## Training data contained 122 data points and 36 incomplete rows. 
## 
## Operations:
## 
## Mean Imputation for Ozone [trained]
## K-nearest neighbor imputation for Wind, Temp, Month, Day [trained]
## Centering for Wind, Temp, Month, Day [trained]
## Scaling for Wind, Temp, Month, Day [trained]

Se observa cómo la receta ahora está ‘entrenada’.

Hornear la receta

Ahora podemos aplicar esta receta a otro conjunto de datos, por ejemplo a los datos de prueba:

1
2
3
4
5
6
7
8
.df_test<-.recipe %>% bake(.df_test)

head(.df_test) %>% 
  knitr::kable('html') %>% 
  kableExtra::kable_styling(position = 'left', 
                            font_size = 10, 
                            fixed_thead = list(enabled = T, background = "blue")
                            ) 
Solar.R Wind Temp Month Day Ozone
334 0.3277434 -1.4315911 -1.3028525 0.0018752 14
307 0.4670629 -1.2120385 -1.3028525 0.1162603 34
42 -0.6474931 -2.2000253 -1.3028525 1.2601115 38
286 -0.4803097 0.1052773 -0.6206722 -1.7139017 38
120 0.3277434 -0.4436043 -0.6206722 0.3450305 12
137 -0.0066234 -0.1142753 -0.6206722 0.4594157 13

Finalmente lo ponemos todo junto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.df<-recipe(Ozone ~ ., data = .df_train) %>%
   
       update_role(Ozone, Solar.R, new_role = 'NA_Variable') %>%
   
       step_meanimpute(has_role('NA_Variable'), -Solar.R) %>%
       step_knnimpute(contains('.R'), neighbors = 3) %>%
       step_center(all_numeric(), -has_role('NA_Variable')) %>%
       step_scale(all_numeric(), -has_role('NA_Variable')) %>%
   
       prep(.df_train) %>%
       bake(.df_test)
## Warning: `step_knnimpute()` was deprecated in recipes 0.1.16.
## Please use `step_impute_knn()` instead.
## Warning: `step_meanimpute()` was deprecated in recipes 0.1.16.
## Please use `step_impute_mean()` instead.
1
2
3
4
5
6
head(.df) %>% 
  knitr::kable('html') %>% 
  kableExtra::kable_styling(position = 'left', 
                            font_size = 10, 
                            fixed_thead = list(enabled = T, background = "blue")
                            ) 
Solar.R Wind Temp Month Day Ozone
334 -2.785283 -8.614430 -5.602534 -1.828072 14
307 -2.746463 -8.590328 -5.602534 -1.814988 34
42 -3.057022 -8.698786 -5.602534 -1.684149 38
286 -3.010438 -8.445718 -5.137164 -2.024332 38
120 -2.785283 -8.505972 -5.137164 -1.788820 12
137 -2.878451 -8.469820 -5.137164 -1.775737 13

Como se ha visto, este paquete ofrece una amplia gama de posibilidades y facilidades para llevar a cabo la tarea de preprocesamiento. Muchos otros temas interesantes sobre este paquete se han dejado de lado en esta publicación: crear su propio paso de preprocesamiento o combinar este paquete con rsamples para aplicar múltiples recetas a particiones utilizadas por técnicas de bootstrapping o cv-Folds. Tal vez para una próxima publicación.

updatedupdated2021-08-192021-08-19
Load Comments?