Tidymodels es en realidad un conjunto de paquetes de R que, al igual que Tidyverse, busca unificar la sintaxis y modo de uso al alrededor del modelado estadístico de datos. A diferencia de Tidyverse, Tidymodels no implementa nuevas maneras de, por ejemplo, aplicar el modelo lineal, si no que genera una interfaz única a partir de la cual utilizar las funciones que ya existen. De la misma manera, la salida que genera es consistente a lo largo de distintos modelos e implementaciones, y mejor aún en formato tidy.
Los principales paquete de Tidymodels son:
rsample |
Para dividir y hacer remuestreo de los datos | |
recipes |
Preprocesamiento de los datos de manera ordenada y reproducible | |
parsnip |
Interfaz unificada para modelar datos | |
workflows |
Organiza el preprocesamiento, el modelado y postprocesa | |
yardstick |
Mide la efectividad de los modelos | |
broom |
Resume la salida de modelos en formato tidy |
pero hay muchos otros con objetivos específicos que completan el universo de tidymodels.
Este diagrama muestra el proceso de modelado de datos teórico. Por supuesto, en la realidad vamos y venimos entre los distintos pasos hasta ajustar cada detalle. Pero es importante tener en cuenta algunos detalles:
Cómo aprovechar los datos: es importante hacer
un “presupuesto” para planificar cómo vamos a usar los datos. Si usamos
el 100% de nuestra muestra en el entrenamiento de un modelo, luego no
podremos hacer ningún tipo de verificación (el modelo ya conoce los
datos, cualquier resultado no será válido). Por eso necesitamos dividir
los datos en (al menos) entrenamiento y testeo. Usaremos
rsample
para esta tarea.
Qué modelo utilizar: la mayoría de las veces no sabremos cual es el mejor modelo a aplicar, dependerá en parte de nuestro objetivo: queremos hacer describir, hacer inferencias o predecir a partir de los datos? Lo interensante es probar distintos modelos para quedarnos con el mejor o utilizar una combinación. Para esto es necesario hacer un remuestreo de los datos de entrenamiento y así hacer comparaciones válidas.
La verificación: es importante hacer una verificación tanto cualitativa (visual) como cuantitativa con los el subset de testeo, pero nunca con los datos de entrenamiento!
Vamos a trabajar con datos de rendimiento de cultivos que incluyen información sobre la floración, la altura del cultivo, la campaña, el tipo de ensayo, la distacia entre surcos entre otras cosas. Con esta información intentaremos generar un modelo que nos permita predecir la variable aceite_porcentaje.
cultivos <- read_csv("https://raw.githubusercontent.com/paocorrales/intro-tidymodels-agro/main/datos/cultivos.csv")
## Rows: 6856 Columns: 13
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (7): campania, tipo_ensayo, epoca, tipo_siembra, testigo, visible, cultivar
## dbl (6): floracion_dias, altura_cm, densidad_sem, rendimiento, aceite_porcen...
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
La variable aceite_porcentaje
tiene la siguiente
distribucuón:
cultivos %>%
ggplot(aes(aceite_porcentaje)) +
geom_histogram(color = "white") +
geom_rug(alpha = 0.1)
Esta base de datos tiene en total 6856 registros de distintos ensayos
de cultivos. Vamos a dividir los datos en 2 subsets de entrenamiento
(cultivos_train
) y testeo (cultivos_test
). En
primer lugar la función initial_split()
divide los datos en
2 utilizando el argumento prop
para definir cuantos datos
iran a cada subset. En ese caso usamos por defecto 3/4
, es
decir, el 75% de los datos iran al entrenamiento y el 25% quedará para
el testeo. Decidir este porcentaje es un problea en si mismo!
set.seed(91)
cultivos_split <- initial_split(cultivos,
prop = 3/4)
cultivos_split
## <Training/Testing/Total>
## <5142/1714/6856>
En segundo lugar, con training()
y
testing()
extraemos los 2 subsets de datos a partir del
objeto que generamos recién.
cultivos_train <- training(cultivos_split)
cultivos_test <- testing(cultivos_split)
Veamos ahora que pinta tiene nuestra variable de interes en relación
con otras, por ejemplo densidad_sem
. Inicialmente no parece
haber una relación muy clara.
cultivos_train %>%
ggplot(aes(densidad_sem, aceite_porcentaje)) +
geom_smooth()
Sin embargo si incluimos tipo_siempra
como tercera
variable, vemos que esta nueva variable podría llegar a explicar, en
parte el comportamiento de aceite_porcentaje
.
cultivos_train %>%
ggplot(aes(densidad_sem, aceite_porcentaje)) +
geom_smooth(aes(color = tipo_siembra))
Comencemos ahora con el preprocesamiento de los datos. Para esto crearemos una receta, es decir una serie de pasos que realicen las transformaciones necesaria para dejar los datos listos para el modelado. A diferencia de otras implementaciones con tidymodel separamos completamente el preprocesamiento del modelado. De esta manera modularizamos el proceso y podemos utilizar la misma receta para aplicar modelos distintos.
Crearemos la receta con los datos de entrenamiento pero luego
podremos aplicar estos pasos también a los datos de testeo. Iniciamos la
receta con la función recipe()
indicando la formula, en
este caso queremos evaluar la relación de aceite_porcentaje
en función de todas las otras variables.
receta_aceite <-
recipe(aceite_porcentaje ~ ., data = cultivos_train)
El siguiente paso es trabajar con predictores categóricos, para esto
sumamos un paso a la receta step_dummy()
que convierte las
variables nominales (caracteres o factores) en variables numéricas
binarias. En este caso aplicamos el paso a todos os predictores
nominales. Aquí vemos además que la receta es compatible con el uso de
la %>%
por lo que podemos ir sumando los paso uno detrás
del otro.
receta_aceite <-
recipe(aceite_porcentaje ~ ., data = cultivos_train) %>%
step_dummy(all_nominal_predictors())
##
## ── Recipe ──────────────────────────────────────────────────────────────────────
##
## ── Inputs
## Number of variables by role
## outcome: 1
## predictor: 12
##
## ── Operations
## • Dummy variables from: all_nominal_predictors()
En el siguiente paso step_zv()
remueve las variables que
contienen solo 1 valor único (si las hubiera), es decir con varianza
cero.
receta_aceite <-
recipe(aceite_porcentaje ~ ., data = cultivos_train) %>%
step_dummy(all_factor_predictors()) %>%
step_zv(all_predictors())
Cómo vimos que hay alguna relación entre densidad_sem
y
tipo_siembra
vamos a sumar un nuevo paso que tenga en
cuenta esa interacción.
receta_aceite <-
recipe(aceite_porcentaje ~ ., data = cultivos_train) %>%
step_dummy(all_factor_predictors()) %>%
step_zv(all_predictors()) %>%
step_interact(~ starts_with("densidad_sem"):starts_with("tipo_siembra"))
Finalmente aplicamos step_normalize()
para normalizar
todos los predictores.
receta_aceite <-
recipe(aceite_porcentaje ~ ., data = cultivos_train) %>%
step_dummy(all_factor_predictors()) %>%
step_zv(all_predictors()) %>%
step_interact(~ starts_with("densidad_sem"):starts_with("tipo_siembra")) %>%
step_normalize(all_numeric_predictors())
Y ahora si, tenemos nuestra receta.
receta_aceite
##
## ── Recipe ──────────────────────────────────────────────────────────────────────
##
## ── Inputs
## Number of variables by role
## outcome: 1
## predictor: 12
##
## ── Operations
## • Dummy variables from: all_factor_predictors()
## • Zero variance filter on: all_predictors()
## • Interactions with: starts_with("densidad_sem"):starts_with("tipo_siembra")
## • Centering and scaling for: all_numeric_predictors()
Esta receta además, define claramente las transformaciones que hacemos en los datos por lo que es reproducible.
Podríamos encadenar la función prep()
para
preparar los datos usando la receta pero en nuestro caso
ejecutaremos todos los pasos necesarios en un workflow.
El siguiente paso es definir el modelo a aplicar. Nuevamente usamos
tidymodels que tienen funciones que funcionan como interfaz con
implementaciones en distintos paquetes. Podés revisar la lista de
modelos disponibles acá.
En nuestro caso vamos a arrancar con el modelo linea usando la función
linear_reg()
.
modelo_lineal <-
linear_reg()
Por defecto esta función usa lm
del paquete
stats
de R base pedro podríamos usar muchos otros
engines, es decir, otras implementaciones al modelo lineal como
glm
y glment
. Para setear un engine usamos
set_engine()
:
modelo_lineal <-
linear_reg() %>%
set_engine("lm")
En algunos casos necesitaremos tener paquetes específicos instalados,
por ejemplo lme
requiere un extension del paquete
parsnip
.
modelo_lineal
## Linear Regression Model Specification (regression)
##
## Computational engine: lm
Ya tenemos el segundo paso, el modelo. Pero como habrás notado, en
ningún momento modelo_lineal
se cruzó con nuestros datos.
Vamos a unir todo en un workflow.
Un workflow es un obejto que incorpora toda la información
necesaria para ajustar y hacer predicciones en base a un modelo.
Iniciamos nuestro workflow con la función workflow()
:
wflow_lm <-
workflow()
Y luego con las funciones add_model()
y
add_recipe()
agregamos el modelo y la receta que creamos
recién.
wflow_lm <-
workflow() %>%
add_model(modelo_lineal) %>%
add_recipe(receta_aceite)
Como vemos el obejto wflow_lm
tiene la información del
modelo (linear_reg()
) y el preprocesamiento con nuestra
receta.
wflow_lm
## ══ Workflow ════════════════════════════════════════════════════════════════════
## Preprocessor: Recipe
## Model: linear_reg()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## 4 Recipe Steps
##
## • step_dummy()
## • step_zv()
## • step_interact()
## • step_normalize()
##
## ── Model ───────────────────────────────────────────────────────────────────────
## Linear Regression Model Specification (regression)
##
## Computational engine: lm
Con el workflow completo ahora si podemos ajustar el modelo usando los datos de entrenamiento:
modelo_fit <- fit(wflow_lm, data = cultivos_train)
El objeto modelo_fit
contiene muchísima información
incluyendo el modelo, la receta y entre otras cosas nos permitirá
extraer predicciones. Esto lo hacemos con la función
predict
predict(modelo_fit, new_data = cultivos_test) %>%
head()
## # A tibble: 6 × 1
## .pred
## <dbl>
## 1 43.5
## 2 44.1
## 3 47.2
## 4 44.7
## 5 NA
## 6 50.9
Además de predecir el valor medio, también podemos estimar intervalos de confianza con la misma función:
mean_predict <- predict(modelo_fit, cultivos_test)
conf_int_pred <- predict(modelo_fit,
new_data = cultivos_test,
type = "conf_int")
Esto genera un par de tibbles con columnas .pred_lower
y
.pred_upper
que son los límites inferiores y superiores de
ese intervalo de confianza.
Veamos como le fue a nuestro modelo (spoiler alert: bastante mal).
Primero vamos a graficar los valores predichos de
aceite_porcentaje
en función de los observados y que
tenemos en nuestro subset de testeo.
aceite_predic <-
cultivos_test %>%
bind_cols(mean_predict)
ggplot(aceite_predic, aes(aceite_porcentaje, .pred)) +
geom_abline(slope = 1, intercept = 0, color = "darkorange") +
geom_point(alpha = 0.1) +
geom_smooth(method = "lm") +
labs(x = "Observado", y = "Predicho") +
coord_equal(xlim = c(29,60), ylim = c(29, 60))
Si nuestro modelo fuera “perfecto” todos los puntos deberían caer
alrededor de la linea naranja. Sin embargo tenemos puntos que se alejan
bastante. Desde este punto de vista más cualitativo vemos que el modelo
no es muy bueno. Calculemos ahora cual seria el error que cometeríamos
si usamos el modelo para predecir un valor de
aceite_porcentaje
en el futuro.
rmse(aceite_predic, aceite_porcentaje, .pred)
## # A tibble: 1 × 3
## .metric .estimator .estimate
## <chr> <chr> <dbl>
## 1 rmse standard 3.08
En este caso calculamos la raiz del error cuadrático medio o rmse con
la función rmse()
del paquee yardsick
. El
resultado es 3.0757701, esto significa que al estimar
aceite_porcentaje
con el modelo cometemos en promedio un
error de 3.0757701.
Una herramienta muy poderosa que implementa Tidymodels es el remuestreo. Esto es generar subsets de datos tomando datos de manera aleatoria a partir de un data set original. En este caso podemos aplicar el remuestreo para hacer un cálculo más robusto del error, generando muestras y calculando el error para cada una de ellas.
aceite_pred_remuestreo <- bind_rows(
replicate(
n = 10,
expr = sample_n(aceite_predic, 500, replace = TRUE),
simplify = FALSE
),
.id = "resample"
)
errores <- aceite_pred_remuestreo %>%
group_by(resample) %>%
rmse(aceite_porcentaje, .pred)
errores
## # A tibble: 10 × 4
## resample .metric .estimator .estimate
## <chr> <chr> <chr> <dbl>
## 1 1 rmse standard 3.10
## 2 10 rmse standard 2.99
## 3 2 rmse standard 2.99
## 4 3 rmse standard 3.07
## 5 4 rmse standard 3.27
## 6 5 rmse standard 3.09
## 7 6 rmse standard 3.03
## 8 7 rmse standard 3.05
## 9 8 rmse standard 3.07
## 10 9 rmse standard 2.99
Tenemos ahora 10 estimaciones del error calculadas a partir de cada submuestra. Podemos calcular el promedio de esa estimación y compararla con el error que obtuvimos usando el subset de testeo completo.
promedio <- errores %>%
summarise(rmse_promedio = mean(.estimate))
En este caso la diferencia es pequeña, ahora da 3.0657874, pero aún así la estimación es más robusta.
Aquí dejamos una lista de los materiales que inspiraron este y que puede servir para continuar aprendiendo sobre Tidymodels.
En inglés:
En español: