Sesión 1.2: Fundamentos de Machine Learning
Primera parte
Segunda parte
Fuente: AWS in Plain English
En la práctica, el proceso es iterativo: volvemos a pasos anteriores cuando algo falla
Recolección de datos
Preprocesamiento
Ejemplo: datos de países
Datos crudos:
país | pib | edu | internet
Uruguay | 17020 | 4.9 | 87.7
Bolivia | NA | 6.5 | 48.2
Chile | 15346 | 5.4 | NA
Después de preprocesar:
país | pib | edu | internet
Uruguay | 17020 | 4.9 | 87.7
Bolivia | 10500 | 6.5 | 48.2 ← imputado
Chile | 15346 | 5.4 | 72.3 ← imputado
El preprocesamiento es la mayor parte del trabajo en un proyecto real de ML
Problemas comunes en datos reales:
Ejemplo de data leakage:
Quieren predecir si un paciente será hospitalizado
Variable: "días_en_hospital"
Si incluyen esta variable,
el modelo "aprende" que
días_en_hospital > 0 → hospitalizado
¡Pero esta información no existe
al momento de la predicción!
El data leakage es uno de los errores más difíciles de detectar y puede inflar artificialmente las métricas
Ejemplo: predicción de abandono
Variables originales:
fecha_registro, ultima_compra,
monto_total, cantidad_compras
Variables derivadas:
dias_desde_registro
dias_sin_comprar ← predictor fuerte
promedio_por_compra
frecuencia_compras
tendencia_reciente ← ¿aumenta o baja?
Un buen feature engineering puede valer más que un modelo complejo
Con más datos, el rendimiento mejora, pero con rendimientos decrecientes.
A veces más datos ayudan más que un modelo más complejo
Fuente: X.com
Subajuste
Buen ajuste
Sobreajuste
Escenario: están construyendo un modelo para predecir si los pacientes tienen una enfermedad rara que afecta a 1 de cada 1.000 personas
Un colega les muestra un modelo y les dice con orgullo: “Logra un 99,9% de accuracy (exactitud)”
Pregunta: ¿es bueno este modelo?
Piénsenlo 30 segundos…
La verdad incómoda: ese modelo “99,9% preciso” no detecta ni un solo caso real. Simplemente predice “sano” para todos. Cada paciente enfermo es ignorado
La accuracy (exactitud) no basta, especialmente con clases desbalanceadas
Precisión = VP / (VP + FP)
Recall = VP / (VP + FN)
Fuente: Analytics Vidhya
Un ejemplo: de 1.000 pacientes, 10 están enfermos. El modelo marca a 8 personas y acierta en 6
Si bajamos el umbral, el modelo marca a más gente: sube el recall y baja la precisión. No se pueden maximizar ambos
¿Qué modelo usar?
Principio de parsimonia:
Guía práctica
| Situación | Modelo sugerido |
|---|---|
| Pocos datos, interpretabilidad alta | Regresión logística/lineal |
| Datos moderados, interpretabilidad media | Árboles, Random Forest |
| Muchos datos, rendimiento máximo | Gradient Boosting, Redes |
| Relaciones muy no lineales | Random Forest, XGBoost |
| Texto | Transformers, BERT |
En la práctica, probar varios modelos y comparar con validación cruzada
Problemas comunes:
En R:
En general:
Ejemplo: estructura de proyecto reproducible
mi_proyecto/
├── README.md ← descripción
├── renv.lock ← versiones de paquetes
├── data/
│ ├── raw/ ← datos originales
│ └── processed/ ← datos procesados
├── scripts/
│ ├── 01-preprocess.R
│ ├── 02-train.R
│ └── 03-evaluate.R
├── models/ ← modelos guardados
└── reports/ ← resultados
Invertir en reproducibilidad ahorra tiempo a futuro
tidymodels
├── rsample → dividir datos
├── recipes → preprocesar
├── parsnip → especificar modelo
├── workflows → combinar todo
├── tune → ajustar hiperparámetros
└── yardstick → evaluar
initial_split() separa los datos en entrenamiento y prueba. Con prop = 0.75 dejamos 75% para entrenarstrata = mantiene la misma proporción de clases en ambos conjuntos, algo importante cuando una categoría es poco frecuentetraining() y testing() extraen cada parte del objeto que devuelve initial_split()vfold_cv() arma los K folds de validación cruzada, siempre a partir del conjunto de entrenamientoset.seed() antes de dividir: sin eso, cada ejecución da una división distinta y los resultados no son reproducibleslibrary(tidymodels)
set.seed(2026)
# 75% entrenamiento, 25% prueba
datos_split <- initial_split(
datos,
prop = 0.75,
strata = crecimiento_alto
)
datos_train <- training(datos_split)
datos_test <- testing(datos_split)
# 5 folds para validación cruzada
folds <- vfold_cv(
datos_train,
v = 5,
strata = crecimiento_alto
)Todo modelo se define con tres verbos:
logistic_reg(), decision_tree(), rand_forest(), linear_reg(), etc.set_engine(): qué paquete de R hace el cálculo por detrás (glm, rpart, ranger)set_mode(): "classification" o "regression"Después:
fit() ajusta el modelo con los datos de entrenamientoy ~ . significa “predecir y con todas las demás variables”# Regresión logística
modelo_log <- logistic_reg() |>
set_engine("glm") |>
set_mode("classification")
# Árbol de decisión: mismo patrón,
# solo cambian el tipo y el motor
modelo_arbol <- decision_tree() |>
set_engine("rpart") |>
set_mode("classification")
# Ajustar con los datos de entrenamiento
ajuste <- modelo_log |>
fit(crecimiento_alto ~ .,
data = datos_train)Especificar el modelo y ajustarlo son pasos separados: podemos definir varios modelos y recién después decidir cuáles ajustar. La lista de modelos está en https://www.tidymodels.org/find/parsnip/
augment() agrega las predicciones (clase y probabilidades) al conjunto de prueba en una sola líneaconf_mat() arma la matriz de confusión que vimos antesmetric_set() junta varias métricas. En los labs usamos precision, recall y roc_auc en lugar de accuracyroc_auc es el área bajo la curva ROC: mide qué tan bien separa el modelo las dos clases (0,5 = azar, 1 = perfecto)event_level = "second" le indica cuál es la clase positiva, porque R ordena los niveles alfabéticamente y deja "si" en segundo lugarfit_resamples() entrena sobre los folds y collect_metrics() promedia los resultados# Predicciones sobre el test set
pred_test <- ajuste |> augment(datos_test)
conf_mat(pred_test,
truth = crecimiento_alto,
estimate = .pred_class)
# La misma evaluación, pero con
# validación cruzada sobre los folds
fit_resamples(modelo_log,
crecimiento_alto ~ .,
resamples = folds,
metrics = metric_set(precision, recall, roc_auc),
control = control_resamples(
event_level = "second")
) |>
collect_metrics()Hoy pasamos la fórmula directamente a fit(). Mañana la vamos a reemplazar por recipe() + workflow(), pero el esqueleto sigue siendo este
Conceptos fundamentales:
Problemas y soluciones: