6  Creando Modelos Con parsnip

El paquete parsnip, un de los paquetes de R que es parte del metaapaquete tidymodels, proporciona una interfaz fluida y estandarizada para una variedad de modelos diferentes. En este capítulo, damos algunas motivaciones sobre por qué una interfaz común es beneficiosa para comprender y construir modelos en la práctica y mostramos cómo usar el paquete parsnip.

Específicamente, nos centraremos en cómo fit() y predict() directamente con un objeto parsnip, que puede ser una buena opción para algunos problemas de modelado sencillos. El próximo capítulo ilustra un mejor enfoque para muchas tareas de modelado al combinar modelos y preprocesadores en algo llamado un objeto de “flujo de trabajo”.

6.1 Crear Un Modelo

Una vez que los datos se han codificado en un formato listo para un algoritmo de modelado, como una matriz numérica, se pueden utilizar en el proceso de construcción del modelo.

Supongamos que nuestra elección inicial fue un modelo de regresión lineal. Esto equivale a especificar que los datos del resultado son numéricos y que los predictores están relacionados con el resultado en términos de pendientes e intersecciones simples:

\[y_i = \beta_0 + \beta_1 x_{1i} + \ldots + \beta_p x_{pi}\]

Se pueden utilizar diversos métodos para estimar los parámetros del modelo:

  • La regresión lineal ordinaria utiliza el método tradicional de mínimos cuadrados para resolver los parámetros del modelo.

  • Regresión lineal regularizada añade una penalización al método de mínimos cuadrados para fomentar la simplicidad eliminando predictores y/o reduciendo sus coeficientes hacia cero. Esto se puede ejecutar utilizando técnicas bayesianas o no bayesianas.

En R, el paquete stats se puede utilizar para el primer caso. La sintaxis para la regresión lineal usando la función lm() es:

model <- lm(formula, data, ...)

donde ... simboliza otras opciones para pasar a lm(). La función not tiene una interfaz x/y, donde podríamos pasar nuestro resultado como y y nuestros predictores como x.

Para estimar con regularización, el segundo caso, se puede ajustar un modelo bayesiano usando el paquete rstanarm:

model <- stan_glm(formula, data, family = "gaussian", ...)

En este caso, las otras opciones pasadas a través de ... incluirían argumentos para las distribuciones anteriores de los parámetros, así como detalles sobre los aspectos numéricos del modelo. Al igual que con lm(), solo está disponible la interfaz de fórmula.

Un enfoque popular no bayesiano para la regresión regularizada es el modelo glmnet (Friedman, Hastie, y Tibshirani 2010). Su sintaxis es:

model <- glmnet(x = matrix, y = vector, family = "gaussian", ...)

En este caso, los datos del predictor ya deben estar formateados en una matriz numérica; solo hay un método x/y y ningún método de fórmula.

Tenga en cuenta que estas interfaces son heterogéneas en la forma en que se pasan los datos a la función del modelo o en términos de sus argumentos. El primer problema es que, para ajustar los modelos a diferentes paquetes, los datos deben formatearse de diferentes maneras. lm() y stan_glm() solo tienen interfaces de fórmula mientras que glmnet() no. Para otro tipo de modelos, las interfaces pueden ser aún más dispares. Para una persona que intenta realizar un análisis de datos, estas diferencias requieren la memorización de la sintaxis de cada paquete y pueden resultar muy frustrantes.

Para tidymodels, el enfoque para especificar un modelo pretende ser más unificado:

  1. Especifique el tipo de modelo según su estructura matemática (por ejemplo, regresión lineal, random forest, KNN, etc.).

  2. Especificar motor para montar el modelo. La mayoría de las veces esto refleja el paquete de software que se debe utilizar, como Stan o glmnet. Estos son modelos por derecho propio, y parsnip proporciona interfaces consistentes al usarlos como motores para el modelado.

  3. Cuando sea necesario, declarar el moda del modelo. El modo refleja el tipo de resultado de la predicción. Para resultados numéricos, el modo es la regresión; para resultados cualitativos, es clasificación.1 Si un algoritmo modelo solo puede abordar un tipo de resultado de predicción, como la regresión lineal, el modo ya está establecido.

Estas especificaciones se construyen sin hacer referencia a los datos. Por ejemplo, para los tres casos que describimos:

library(tidymodels)
tidymodels_prefer()

linear_reg() %>% set_engine("lm")
## Linear Regression Model Specification (regression)
## 
## Computational engine: lm

linear_reg() %>% set_engine("glmnet") 
## Linear Regression Model Specification (regression)
## 
## Computational engine: glmnet

linear_reg() %>% set_engine("stan")
## Linear Regression Model Specification (regression)
## 
## Computational engine: stan

Una vez que se han especificado los detalles del modelo, la estimación del modelo se puede realizar con la función fit() (para usar una fórmula) o con la función fit_xy() (cuando sus datos ya están preprocesados). El paquete parsnip permite al usuario ser indiferente a la interfaz del modelo subyacente; siempre puedes usar una fórmula incluso si la función del paquete de modelado solo tiene la interfaz x/y.

La función translate() puede proporcionar detalles sobre cómo parsnip convierte el código del usuario a la sintaxis del paquete:

linear_reg() %>% set_engine("lm") %>% translate()
## Linear Regression Model Specification (regression)
## 
## Computational engine: lm 
## 
## Model fit template:
## stats::lm(formula = missing_arg(), data = missing_arg(), weights = missing_arg())

linear_reg(penalty = 1) %>% set_engine("glmnet") %>% translate()
## Linear Regression Model Specification (regression)
## 
## Main Arguments:
##   penalty = 1
## 
## Computational engine: glmnet 
## 
## Model fit template:
## glmnet::glmnet(x = missing_arg(), y = missing_arg(), weights = missing_arg(), 
##     family = "gaussian")

linear_reg() %>% set_engine("stan") %>% translate()
## Linear Regression Model Specification (regression)
## 
## Computational engine: stan 
## 
## Model fit template:
## rstanarm::stan_glm(formula = missing_arg(), data = missing_arg(), 
##     weights = missing_arg(), family = stats::gaussian, refresh = 0)

Tenga en cuenta que missing_arg() es solo un marcador de posición para los datos que aún no se han proporcionado.

Proporcionamos un argumento de penalty (penalización) requerido para el motor glmnet. Además, para los motores Stan y glmnet, el argumento famiy(familia) se agregó automáticamente como valor predeterminado. Como se mostrará más adelante en esta sección, esta opción se puede cambiar.

Veamos cómo predecir el precio de venta de las casas en los datos de Ames en función únicamente de la longitud y la latitud:2

lm_model <- 
  linear_reg() %>% 
  set_engine("lm")

lm_form_fit <- 
  lm_model %>% 
  # Recuerde que a Sale_Price se le ha aplicado una transformación logarítmica previamente
  fit(Sale_Price ~ Longitude + Latitude, data = ames_train)

lm_xy_fit <- 
  lm_model %>% 
  fit_xy(
    x = ames_train %>% select(Longitude, Latitude),
    y = ames_train %>% pull(Sale_Price)
  )

lm_form_fit
## parsnip model object
## 
## 
## Call:
## stats::lm(formula = Sale_Price ~ Longitude + Latitude, data = data)
## 
## Coefficients:
## (Intercept)    Longitude     Latitude  
##     -302.97        -2.07         2.71
lm_xy_fit
## parsnip model object
## 
## 
## Call:
## stats::lm(formula = ..y ~ ., data = data)
## 
## Coefficients:
## (Intercept)    Longitude     Latitude  
##     -302.97        -2.07         2.71

parsnip no solo permite una interfaz de modelo coherente para diferentes paquetes, sino que también proporciona coherencia en los argumentos del modelo. Es común que diferentes funciones que se ajustan al mismo modelo tengan diferentes nombres de argumentos. Las funciones del modelo de random forest son un buen ejemplo. Tres argumentos comúnmente utilizados son la cantidad de árboles en el conjunto, la cantidad de predictores para muestrear aleatoriamente con cada división dentro de un árbol y la cantidad de puntos de datos necesarios para realizar una división. Para tres paquetes R diferentes que implementan este algoritmo, esos argumentos se muestran en Tabla 6.1.

Tabla 6.1: Ejemplos de nombres de argumentos para diferentes funciones de random forest.
Tipo de argumento ranger randomForest sparklyr
# predictores muestreo mtry mtry feature_subset_strategy
# árboles num.trees ntree num_trees
# puntos de datos por división min.node.size nodesize min_instances_per_node

En un esfuerzo por hacer que la especificación de argumentos sea menos complicada, parsnip utiliza nombres de argumentos comunes dentro y entre paquetes. Tabla 6.2 muestra, para radom forest, qué utilizan los modelos parsnip.

Tabla 6.2: Nombres de argumentos de random forest utilizados por parsnip.
Tipo de argumento parsnip
# predictores muestreo mtry
# árboles trees
# puntos de datos por división min_n

Es cierto que este es un conjunto más de argumentos para memorizar. Sin embargo, cuando otros tipos de modelos tienen los mismos tipos de argumentos, estos nombres aún se aplican. Por ejemplo, los conjuntos de árboles potenciados también crean una gran cantidad de modelos basados ​​en árboles, por lo que allí también se utilizan trees, al igual que min_n, etc.

Algunos de los nombres de los argumentos originales pueden ser bastante jerga. Por ejemplo, para especificar la cantidad de regularización que se utilizará en un modelo glmnet, se utiliza la letra griega lambda. Si bien esta notación matemática se usa comúnmente en la literatura estadística, para muchas personas no es obvio qué representa “lambda” (especialmente aquellos que consumen los resultados del modelo). Dado que esta es la penalización utilizada en la regularización, parsnip estandariza el nombre del argumento penalty. De manera similar, el número de vecinos en un modelo KNN se denomina neighbors en lugar de k. Nuestra regla general al estandarizar los nombres de los argumentos es:

Si un profesional incluyera estos nombres en un gráfico o tabla, ¿entenderían el nombre las personas que vieran esos resultados?

Para comprender cómo los nombres de los argumentos parsnip se asignan a los nombres originales, use el archivo de ayuda para el modelo (disponible a través de ?rand_forest), así como la función translate():

rand_forest(trees = 1000, min_n = 5) %>% 
  set_engine("ranger") %>% 
  set_mode("regression") %>% 
  translate()
## Random Forest Model Specification (regression)
## 
## Main Arguments:
##   trees = 1000
##   min_n = 5
## 
## Computational engine: ranger 
## 
## Model fit template:
## ranger::ranger(x = missing_arg(), y = missing_arg(), weights = missing_arg(), 
##     num.trees = 1000, min.node.size = min_rows(~5, x), num.threads = 1, 
##     verbose = FALSE, seed = sample.int(10^5, 1))

Las funciones de modelado en parsnip separan los argumentos del modelo en dos categorías:

  • Los argumentos principales se utilizan con más frecuencia y tienden a estar disponibles en todos los motores.

  • Los argumentos del motor son específicos de un motor en particular o se usan con menos frecuencia.

Por ejemplo, en la traducción del código de random forest anterior, los argumentos num.threads, verbose y seed se agregaron de forma predeterminada. Estos argumentos son específicos de la implementación ranger de modelos random forest y no tendrían sentido como argumentos principales. Los argumentos específicos del motor se pueden especificar en set_engine(). Por ejemplo, para que la función ranger::ranger() imprima más información sobre el ajuste:

rand_forest(trees = 1000, min_n = 5) %>% 
  set_engine("ranger", verbose = TRUE) %>% 
  set_mode("regression") 
## Random Forest Model Specification (regression)
## 
## Main Arguments:
##   trees = 1000
##   min_n = 5
## 
## Engine-Specific Arguments:
##   verbose = TRUE
## 
## Computational engine: ranger

6.2 Utilizar Los Resultados Del Modelo

Una vez creado y ajustado el modelo, podemos utilizar los resultados de diversas formas; es posible que queramos trazar, imprimir o examinar de otro modo el resultado del modelo. Varias cantidades se almacenan en un objeto modelo parsnip, incluido el modelo ajustado. Esto se puede encontrar en un elemento llamado fit, que se puede devolver usando la función extract_fit_engine():

lm_form_fit %>% extract_fit_engine()
## 
## Call:
## stats::lm(formula = Sale_Price ~ Longitude + Latitude, data = data)
## 
## Coefficients:
## (Intercept)    Longitude     Latitude  
##     -302.97        -2.07         2.71

Se pueden aplicar métodos normales a este objeto, como imprimir y trazar:

lm_form_fit %>% extract_fit_engine() %>% vcov()
##             (Intercept) Longitude Latitude
## (Intercept)     207.311   1.57466 -1.42397
## Longitude         1.575   0.01655 -0.00060
## Latitude         -1.424  -0.00060  0.03254

Nunca pase el elemento fit de un modelo parsnip a una función de predicción del modelo, es decir, use predict(lm_form_fit) pero no use predict(lm_form_fit$fit). Si los datos fueron preprocesados ​​de alguna manera, se generarán predicciones incorrectas (a veces, sin errores). La función de predicción del modelo subyacente no tiene idea de si se ha realizado alguna transformación en los datos antes de ejecutar el modelo. Consulte Sección 6.3 para obtener más información sobre cómo hacer predicciones.

Un problema con algunos métodos existentes en base R es que los resultados se almacenan de una manera que puede no ser la más útil. Por ejemplo, el método summary() para objetos lm se puede utilizar para imprimir los resultados del ajuste del modelo, incluida una tabla con los valores de los parámetros, sus estimaciones de incertidumbre y los valores p. Estos resultados particulares también se pueden guardar:

model_res <- 
  lm_form_fit %>% 
  extract_fit_engine() %>% 
  summary()

# Se puede acceder a la tabla de coeficientes del modelo mediante el método "coef".
param_est <- coef(model_res)
class(param_est)
## [1] "matrix" "array"
param_est
##             Estimate Std. Error t value  Pr(>|t|)
## (Intercept) -302.974    14.3983  -21.04 3.640e-90
## Longitude     -2.075     0.1286  -16.13 1.395e-55
## Latitude       2.710     0.1804   15.02 9.289e-49

Hay algunas cosas a tener en cuenta sobre este resultado. Primero, el objeto es una matriz numérica. Lo más probable es que se haya elegido esta estructura de datos, ya que todos los resultados calculados son numéricos y un objeto de matriz se almacena de manera más eficiente que un marco de datos. Esta elección probablemente se hizo a finales de la década de 1970, cuando la eficiencia computacional era extremadamente crítica. En segundo lugar, los datos no numéricos (las etiquetas de los coeficientes) están contenidos en los nombres de las filas. Mantener las etiquetas de los parámetros como nombres de filas es muy coherente con las convenciones del lenguaje S original.

Un siguiente paso razonable podría ser crear una visualización de los valores de los parámetros. Para ello, sería sensato convertir la matriz de parámetros en un marco de datos. Podríamos agregar los nombres de las filas como una columna para que puedan usarse en un gráfico. Sin embargo, observe que varios de los nombres de columnas de matriz existentes no serían nombres de columnas R válidos para marcos de datos ordinarios (por ejemplo, "Pr(>|t|)"). Otra complicación es la coherencia de los nombres de las columnas. Para objetos lm, la columna para el valor p es "Pr(>|t|)", pero para otros modelos, se podría usar una prueba diferente y, como resultado, el nombre de la columna sería diferente ( por ejemplo, "Pr(>|z|)") y el tipo de prueba se codificaría en el nombre de la columna.

Si bien estos pasos adicionales de formato de datos no son imposibles de superar, son un obstáculo, especialmente porque pueden ser diferentes para distintos tipos de modelos. La matriz no es una estructura de datos altamente reutilizable, principalmente porque limita los datos a ser de un solo tipo (por ejemplo, numéricos). Además, mantener algunos datos en los nombres de las dimensiones también es problemático, ya que esos datos deben extraerse para que sean de uso general.

Como solución, el paquete broom puede convertir muchos tipos de objetos modelo en una estructura ordenada. Por ejemplo, usar el método tidy() en el modelo lineal produce:

tidy(lm_form_fit)
## # A tibble: 3 × 5
##   term        estimate std.error statistic  p.value
##   <chr>          <dbl>     <dbl>     <dbl>    <dbl>
## 1 (Intercept)  -303.      14.4       -21.0 3.64e-90
## 2 Longitude      -2.07     0.129     -16.1 1.40e-55
## 3 Latitude        2.71     0.180      15.0 9.29e-49

Los nombres de las columnas están estandarizados en todos los modelos y no contienen ningún dato adicional (como el tipo de prueba estadística). Los datos que antes estaban contenidos en los nombres de las filas ahora están en una columna llamada term (término). Un principio importante en el ecosistema de tidymodels es que una función debe devolver valores que sean predecibles, consistentes y no sorprendentes.

6.3 Hacer Predicciones

Otra área donde parsnip difiere de las funciones de modelado R convencionales es el formato de los valores devueltos por predict(). Para las predicciones, parsnip siempre se ajusta a las siguientes reglas:

  1. Los resultados son siempre un tibble.
  2. Los nombres de las columnas del tibble siempre son predecibles.
  3. Siempre hay tantas filas en el tibble como en el conjunto de datos de entrada.

Por ejemplo, cuando se predicen datos numéricos:

ames_test_small <- ames_test %>% slice(1:5)
predict(lm_form_fit, new_data = ames_test_small)
## # A tibble: 5 × 1
##   .pred
##   <dbl>
## 1  5.22
## 2  5.21
## 3  5.28
## 4  5.27
## 5  5.28

El orden de las filas de las predicciones es siempre el mismo que el de los datos originales.

¿Por qué el punto inicial en algunos de los nombres de las columnas? Algunos argumentos y valores de retorno de tidyverse y tidymodels contienen puntos. Esto es para proteger contra la fusión de datos con nombres duplicados. ¡Hay algunos conjuntos de datos que contienen predictores llamados “pred”!

Estas tres reglas facilitan la combinación de predicciones con los datos originales:

ames_test_small %>% 
  select(Sale_Price) %>% 
  bind_cols(predict(lm_form_fit, ames_test_small)) %>% 
  # Agregue intervalos de predicción del 95% a los resultados:
  bind_cols(predict(lm_form_fit, ames_test_small, type = "pred_int")) 
## # A tibble: 5 × 4
##   Sale_Price .pred .pred_lower .pred_upper
##        <dbl> <dbl>       <dbl>       <dbl>
## 1       5.02  5.22        4.91        5.54
## 2       5.39  5.21        4.90        5.53
## 3       5.28  5.28        4.97        5.60
## 4       5.28  5.27        4.96        5.59
## 5       5.28  5.28        4.97        5.60

La motivación para la primera regla proviene de algunos paquetes de R que producen tipos de datos diferentes a partir de funciones de predicción. Por ejemplo, el paquete ranger es una excelente herramienta para calcular modelos forestales aleatorios. Sin embargo, en lugar de devolver un marco de datos o un vector como salida, devuelve un objeto especializado que tiene múltiples valores incrustados (incluidos los valores predichos). Este es solo un paso más que el analista de datos debe solucionar en sus scripts. Como otro ejemplo, el modelo nativo glmnet puede devolver al menos cuatro tipos de salida diferentes para predicciones, dependiendo de los detalles del modelo y las características de los datos. Estos se muestran en Tabla 6.3.

Tabla 6.3: Diferentes valores de retorno para tipos de predicción glmnet.
Tipo de Predicción Devuelve:
numérica matriz numérica
clase matriz de texto
probabilidad (2 classes) matriz numérica (solo 2do nivel)
probabilidad (3+ classes) arreglo numérico 3D (todos los niveles)

Además, los nombres de las columnas de los resultados contienen valores codificados que se asignan a un vector llamado lambda dentro del objeto del modelo glmnet. Este excelente método estadístico puede resultar desalentador en la práctica debido a todos los casos especiales que un analista puede encontrar y que requieren código adicional para ser útil.

Para la segunda regla de predicción de tidymodels, los nombres de columnas predecibles para diferentes tipos de predicciones se muestran en Tabla 6.4.

Tabla 6.4: El mapeo de tidymodels de tipos de predicción y nombres de columnas.
tipo de valor nombre(s) de columna(s)
numeric .pred
class .pred_class
prob .pred_{class levels}
conf_int .pred_lower, .pred_upper
pred_int .pred_lower, .pred_upper

La tercera regla con respecto al número de filas en la salida es crítica. Por ejemplo, si alguna fila de los datos nuevos contiene valores faltantes, la salida se completará con los resultados faltantes para esas filas. Una ventaja principal de estandarizar la interfaz del modelo y los tipos de predicción en parsnip es que, cuando se utilizan diferentes modelos, la sintaxis es idéntica. Supongamos que utilizamos un árbol de decisión para modelar los datos de Ames. Fuera de la especificación del modelo, no hay diferencias significativas en la canalización del código:

tree_model <- 
  decision_tree(min_n = 2) %>% 
  set_engine("rpart") %>% 
  set_mode("regression")

tree_fit <- 
  tree_model %>% 
  fit(Sale_Price ~ Longitude + Latitude, data = ames_train)

ames_test_small %>% 
  select(Sale_Price) %>% 
  bind_cols(predict(tree_fit, ames_test_small))
## # A tibble: 5 × 2
##   Sale_Price .pred
##        <dbl> <dbl>
## 1       5.02  5.15
## 2       5.39  5.15
## 3       5.28  5.32
## 4       5.28  5.32
## 5       5.28  5.32

Esto demuestra el beneficio de homogeneizar el proceso de análisis de datos y la sintaxis en diferentes modelos. Permite a los usuarios dedicar su tiempo a los resultados y la interpretación en lugar de tener que centrarse en las diferencias sintácticas entre los paquetes de R.

6.4 Paquetes De Extensión De parsnip

El paquete parsnip en sí contiene interfaces para varios modelos. Sin embargo, para facilitar la instalación y el mantenimiento del paquete, existen otros paquetes tidymodels que tienen definiciones de modelo parsnip para otros conjuntos de modelos. El paquete discrim tiene definiciones de modelos para el conjunto de técnicas de clasificación llamadas métodos de análisis discriminante (como análisis discriminante lineal o cuadrático). De esta manera, se reducen las dependencias de paquetes necesarias para instalar parsnip. Puede encontrar una lista de todos los modelos que se pueden usar con parsnip (en diferentes paquetes que están en CRAN) en https://www.tidymodels.org/find/.

6.5 Crear Especificaciones De Modelo

Puede resultar tedioso escribir muchas especificaciones de modelos o recordar cómo escribir el código para generarlas. El paquete parsnip incluye un complemento RStudio3 que puede ayudar. Ya sea eligiendo este complemento en el menú de la barra de herramientas Addins o ejecutando el código:

parsnip_addin()

abrirá una ventana en el panel Viewer de RStudio IDE con una lista de posibles modelos para cada modo de modelo. Estos se pueden escribir en el panel de código fuente.

La lista de modelos incluye modelos de los paquetes de extensión parsnip y parsnip que están en CRAN.

6.6 Resumen Del Capítulo

Este capítulo presentó el paquete parsnip, que proporciona una interfaz común para modelos en todos los paquetes R utilizando una sintaxis estándar. La interfaz y los objetos resultantes tienen una estructura predecible.

El código para modelar los datos de Ames que usaremos en el futuro es:

library(tidymodels)
data(ames)
ames <- mutate(ames, Sale_Price = log10(Sale_Price))

set.seed(502)
ames_split <- initial_split(ames, prop = 0.80, strata = Sale_Price)
ames_train <- training(ames_split)
ames_test  <-  testing(ames_split)

lm_model <- linear_reg() %>% set_engine("lm")

  1. Tenga en cuenta que parsnip restringe la columna de resultados de un modelo de clasificación para que se codifique como un factor; el uso de valores numéricos binarios generará un error.↩︎

  2. ¿Cuáles son las diferencias entre fit() y fit_xy()? La función fit_xy() siempre pasa los datos tal cual a la función del modelo subyacente. No creará variables ficticias/indicadoras antes de hacerlo. Cuando se usa fit() con una especificación de modelo, esto casi siempre significa que se crearán variables ficticias a partir de predictores cualitativos. Si la función subyacente requiere una matriz (como glmnet), creará la matriz. Sin embargo, si la función subyacente usa una fórmula, fit() simplemente pasa la fórmula a esa función. Estimamos que el 99% de las funciones de modelado que utilizan fórmulas generan variables ficticias. El otro 1% incluye métodos basados ​​en árboles que no requieren predictores puramente numéricos. Consulte Sección 7.4 para obtener más información sobre el uso de fórmulas en tidymodels.↩︎

  3. https://rstudio.github.io/rstudioaddins/↩︎