Sesión 2.1: Métodos de clasificación
\[P(y = 1 | x) = \frac{1}{1 + e^{-(w_0 + w_1 x_1 + \ldots + w_n x_n)}}\]
Fuente: Wikipedia
Ventajas
Limitaciones
Usos comunes en ciencias sociales
La interpretabilidad es clave para informar políticas públicas, aunque a veces sacrifiquemos capacidad predictiva
Pregunta de investigación: ¿qué factores predicen si un uruguayo apoya la democracia? Datos del Latinobarómetro para Uruguay (simulados)
Variables: edad, educación, ingreso, confianza en instituciones, percepción económica, zona, género
Variable de respuesta: apoya_democracia (sí/no)
La regresión logística nos dice qué factores aumentan o disminuyen la probabilidad de apoyo, y en qué magnitud
Resultados (datos simulados, n = 1125):
R> broom::tidy(ajuste_log) |>
+ dplyr::mutate(
+ odds_ratio = exp(estimate),
+ sig = dplyr::case_when(
+ p.value < 0.001 ~ "***",
+ p.value < 0.01 ~ "**",
+ p.value < 0.05 ~ "*",
+ TRUE ~ ""
+ ),
+ term = dplyr::recode(term,
+ `(Intercept)` = "Intercept",
+ `zonaurbano` = "zona_urbano"
+ )
+ ) |>
+ dplyr::select(Variable = term,
+ Coef = estimate,
+ `Odds Ratio` = odds_ratio,
+ ` ` = sig) |>
+ knitr::kable(digits = 2, align = c("l", "r", "r", "l"))| Variable | Coef | Odds Ratio | |
|---|---|---|---|
| Intercept | -4.40 | 0.01 | *** |
| edad | 0.01 | 1.01 | ** |
| educacion_anos | 0.09 | 1.09 | *** |
| ingreso_log | 0.21 | 1.23 | ** |
| confianza_gobierno | 0.23 | 1.25 | *** |
| percepcion_economia | 0.31 | 1.36 | *** |
| zona_urbano | -0.11 | 0.90 | |
| generomasculino | -0.08 | 0.92 |
*** p < 0.001, ** p < 0.01, * p < 0.05
Pregunta: ¿qué significa un OR de 1.25 para confianza en el gobierno?
Interpretación del odds ratio:
Ejemplo: OR = 1.25 para confianza en el gobierno
Cálculo en R:
| term | OR | IC_inf | IC_sup |
|---|---|---|---|
| (Intercept) | 0.01 | 0.00 | 0.05 |
| edad | 1.01 | 1.00 | 1.02 |
| educacion_anos | 1.09 | 1.05 | 1.13 |
| ingreso_log | 1.23 | 1.07 | 1.42 |
| confianza_gobierno | 1.25 | 1.19 | 1.32 |
| percepcion_economia | 1.36 | 1.23 | 1.51 |
| zonaurbano | 0.90 | 0.61 | 1.31 |
| generomasculino | 0.92 | 0.70 | 1.22 |
Siempre reportar intervalos de confianza para los odds ratios
Fuente: Medium
Ejemplo paso a paso
Datos: 500 personas, 265 votaron
Paso 1: ¿edad > 35?
├── Sí (300 personas, 200 votaron)
│ Paso 2: ¿educación > 12?
│ ├── Sí → Predecir: VOTÓ ✅
│ └── No → Predecir: NO VOTÓ ❌
└── No (200 personas, 65 votaron)
Paso 2: ¿interés_política > 3?
├── Sí → Predecir: VOTÓ ✅
└── No → Predecir: NO VOTÓ ❌
Cada pregunta busca la "mejor"
separación posible
Ventajas
Problemas
Con tidymodels, solo necesitamos cambiar la especificación del modelo:
R> # Ajustar un árbol de decisión (spec + fit en una sola pipeline)
R> ajuste_arbol <- decision_tree(tree_depth = 5, min_n = 10) |>
+ set_engine("rpart") |>
+ set_mode("classification") |>
+ fit(apoya_democracia ~ ., data = datos_train)
R>
R> # Evaluar: accuracy y ROC-AUC en test
R> pred_arbol <- augment(ajuste_arbol, datos_test)
R> pred_arbol |> accuracy(truth = apoya_democracia, estimate = .pred_class)# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 accuracy binary 0.709
# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 roc_auc binary 0.613
logistic_reg() por decision_tree(), ¡la misma sintaxis para todos los modelos!tree_depth y min_n son hiperparámetros: controlan la complejidad del modeloaugment() agrega las predicciones al test set; una sola llamada da todo lo necesario para cualquier métrica de yardstickrpart.plot:R> library(rpart.plot)
R>
R> # Extraer el modelo rpart del ajuste tidymodels
R> arbol_rpart <- extract_fit_engine(ajuste_arbol)
R>
R> # type=4: muestra reglas en todos los nodos
R> # extra=104: clase predicha + % de observaciones
R> # roundint=FALSE: evita warning con predictores
R> # no enteros
R> rpart.plot(arbol_rpart,
+ type = 4,
+ extra = 104,
+ roundint = FALSE)Árbol ajustado a nuestros datos:
¡Cualquier persona puede entender este modelo, incluso sin formación técnica!
tree_depth (profundidad máxima), min_n (mínimo por nodo) y cost_complexity (penalización por complejidad)El trade-off (con nuestros datos):
R> profs <- c(1, 2, 3, 5, 7, 10, 15, 20)
R> err <- purrr::map_dfr(profs, function(d) {
+ fit <- rpart(apoya_democracia ~ .,
+ data = datos_train,
+ control = rpart.control(maxdepth = d,
+ minsplit = 2,
+ minbucket = 1,
+ cp = 0))
+ tibble::tibble(
+ profundidad = d,
+ train = mean(predict(fit, datos_train, type = "class") !=
+ datos_train$apoya_democracia),
+ test = mean(predict(fit, datos_test, type = "class") !=
+ datos_test$apoya_democracia)
+ )
+ })
R>
R> err |>
+ tidyr::pivot_longer(c(train, test),
+ names_to = "conjunto", values_to = "error") |>
+ ggplot(aes(profundidad, error, colour = conjunto)) +
+ geom_line(linewidth = 1) +
+ geom_point(size = 2) +
+ scale_colour_manual(values = c(train = "#1f77b4", test = "#d62728"),
+ labels = c(train = "Entrenamiento", test = "Test")) +
+ labs(x = "Profundidad del árbol", y = "Error de clasificación",
+ colour = NULL) +
+ theme_minimal(base_size = 13) +
+ theme(legend.position = "bottom")A partir de cierta profundidad, el error en test empeora mientras el error en train sigue bajando: la poda previene el sobreajuste
Random Forest con 500 árboles:
Árbol 1: muestra 1, variables {A,C,F}
→ Predicción: VOTÓ
Árbol 2: muestra 2, variables {B,D,E}
→ Predicción: NO VOTÓ
Árbol 3: muestra 3, variables {A,D,G}
→ Predicción: VOTÓ
...
Árbol 500: muestra 500, variables {C,E,F}
→ Predicción: VOTÓ
Predicción final: VOTÓ
(mayoría de votos entre los 500 árboles)
La sabiduría de la multitud: ¡muchos modelos “mediocres” juntos hacen un modelo excelente!
De nuevo, con tidymodels solo cambiamos la especificación:
R> # Ajustar Random Forest (importance = "permutation" lo usamos después en VIP)
R> ajuste_rf <- rand_forest(trees = 2000, mtry = 2, min_n = 5) |>
+ set_engine("ranger", seed = 1, importance = "permutation") |>
+ set_mode("classification") |>
+ fit(apoya_democracia ~ ., data = datos_train)
R>
R> # Evaluar: accuracy y ROC-AUC en test
R> pred_rf <- augment(ajuste_rf, datos_test)
R> pred_rf |> accuracy(truth = apoya_democracia, estimate = .pred_class)# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 accuracy binary 0.715
# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 roc_auc binary 0.676
ranger es una implementación rápida y eficiente de Random Forest en RHiperparámetros principales
| Parámetro | Descripción |
|---|---|
trees |
Número de árboles (más = mejor, pero más lento) |
mtry |
Variables a considerar en cada división |
min_n |
Mínimo de observaciones por nodo |
mtry \(= \sqrt{p}\) (clasificación) o \(p/3\) (regresión); restringir las variables por división fuerza a los árboles a ser diferentes. En la práctica, los valores por defecto funcionan bienedad hace que el modelo sea mucho peor, entonces edad es importantezona no cambia nada, entonces zona no aporta muchoPDP: confianza en el gobierno → P(apoya democracia)
mtry, min_n) se fijan antes de entrenar; los valores por defecto suelen servir, pero no siempre son óptimostune()tune_grid() sobre los folds (resamples = folds, grid = grilla)select_best() + finalize_model() para el ajuste finalR> # 1. Hiperparámetros a ajustar con tune()
R> modelo_rf_tune <- rand_forest(mtry = tune(), min_n = tune()) |>
+ set_engine("ranger", seed = 1) |>
+ set_mode("classification")
R>
R> # 2. Grilla de valores + folds de validación cruzada
R> grilla <- grid_regular(mtry(range = c(2, 10)),
+ min_n(range = c(5, 20)), levels = 4)
R> folds <- vfold_cv(datos_train, v = 5,
+ strata = apoya_democracia)
R>
R> # 3. Probar cada combinación con CV
R> resultados <- tune_grid(modelo_rf_tune,
+ apoya_democracia ~ ., resamples = folds,
+ grid = grilla, metrics = metric_set(roc_auc))
R>
R> # 4. Elegir la mejor y entrenar el modelo final
R> mejor <- select_best(resultados, metric = "roc_auc")
R> ajuste_final <- finalize_model(modelo_rf_tune, mejor) |>
+ fit(apoya_democracia ~ ., data = datos_train)El tuning mejora el rendimiento pero toma tiempo. Lo ejecutan paso a paso en el laboratorio
Random Forest vs. Gradient Boosting:
| RF | Boosting | |
|---|---|---|
| Entrenamiento | Paralelo | Secuencial |
| Hiperparámetros | Pocos | Muchos |
| Riesgo de sobreajuste | Bajo | Más alto |
| Velocidad | Rápido | Variable |
| Rendimiento típico | Muy bueno | Excelente |
En tidymodels, la misma interfaz de siempre:
Como los cuatro ajustes comparten la misma estructura, podemos compararlos en una métrica común (ROC-AUC en test):
R> # Los cuatro ajustes de esta sesión (misma estructura, distinta spec)
R> ajuste_log <- logistic_reg() |> set_engine("glm") |> set_mode("classification") |>
+ fit(apoya_democracia ~ ., data = datos_train)
R> ajuste_arbol <- decision_tree(tree_depth = 5, min_n = 10) |> set_engine("rpart") |> set_mode("classification") |>
+ fit(apoya_democracia ~ ., data = datos_train)
R> ajuste_rf <- rand_forest(trees = 2000, mtry = 2, min_n = 5) |> set_engine("ranger", seed = 1, importance = "permutation") |> set_mode("classification") |>
+ fit(apoya_democracia ~ ., data = datos_train)
R> ajuste_xgb <- boost_tree(trees = 500, tree_depth = 3, learn_rate = 0.05, min_n = 10) |> set_engine("xgboost", seed = 1) |> set_mode("classification") |>
+ fit(apoya_democracia ~ ., data = datos_train)
R>
R> # Métrica: ROC-AUC en test
R> tibble(
+ Modelo = c("Logística", "Árbol", "Random Forest", "XGBoost"),
+ `ROC-AUC` = c(
+ augment(ajuste_log, datos_test) |> roc_auc(apoya_democracia, .pred_si, event_level = "second") |> pull(.estimate),
+ augment(ajuste_arbol, datos_test) |> roc_auc(apoya_democracia, .pred_si, event_level = "second") |> pull(.estimate),
+ augment(ajuste_rf, datos_test) |> roc_auc(apoya_democracia, .pred_si, event_level = "second") |> pull(.estimate),
+ augment(ajuste_xgb, datos_test) |> roc_auc(apoya_democracia, .pred_si, event_level = "second") |> pull(.estimate)
+ )
+ ) |> knitr::kable(digits = 3)| Modelo | ROC-AUC |
|---|---|
| Logística | 0.707 |
| Árbol | 0.613 |
| Random Forest | 0.676 |
| XGBoost | 0.687 |
| Criterio | Reg. logística | Árbol | Random Forest | XGBoost |
|---|---|---|---|---|
| Interpretabilidad | Alta | Alta (visual) | Baja | Baja |
| Capacidad predictiva | Moderada | Moderada-baja | Alta | Alta |
| Riesgo de sobreajuste | Bajo | Alto | Bajo | Medio-alto |
| Velocidad | Muy rápida | Rápida | Moderada | Moderada |
| Relaciones no lineales | No | Sí | Sí | Sí |
| Hiperparámetros | Ninguno | Pocos | Pocos | Muchos |
Lo que vimos hoy:
Para explorar:
La elección del modelo depende del objetivo: ¿explicar o predecir?
¡Nos vemos después del descanso!
\[\text{Gini}(t) = 1 - \sum_{k=1}^{K} p_k^2\]
donde \(p_k\) es la proporción de la clase \(k\) en el nodo \(t\)
\[\text{Gini}_{\text{split}} = \frac{n_L}{n} \text{Gini}(t_L) + \frac{n_R}{n} \text{Gini}(t_R)\]
Ejemplo con nuestros datos (apoyo a la democracia)
| Nodo | Sí | No | Gini |
|---|---|---|---|
| Raíz | 804 | 321 | 0.408 |
| confianza_gobierno < 3.5 | 186 | 152 | 0.495 |
| confianza_gobierno ≥ 3.5 | 618 | 169 | 0.337 |
\(\text{Gini}_{\text{split}} = \tfrac{338}{1125}(0.495) + \tfrac{787}{1125}(0.337) = 0.385\)
Como \(0.385 < 0.408\), esta división mejora la separación de las clases.
Muchlinski et al. (2016): Predicción de guerras civiles
Jean et al. (2016): Pobreza desde el espacio
Otros ejemplos en ciencias sociales:
En todos estos casos, RF es un punto de partida común