Kaggle Playground Series - Season 6, Episode 4 Metodología Híbrida: 11 Pasos + Buenas Prácticas Académicas
Este Análisis Exploratorio de Datos (EDA) se enmarca en la
competición “Predicting Irrigation Need” (Kaggle
Playground Series - Season 6, Episode 4). El objetivo es clasificar la
necesidad de riego (Irrigation_Need) en tres categorías
(Low, Medium, High) a
partir de 21 variables que describen condiciones edafoclimáticas y
prácticas de manejo.
Naturaleza de los Datos: Los datos son sintéticos, generados mediante técnicas modernas de simulación a partir de distribuciones reales. Esta característica implica que: - No existen problemas de privacidad o datos faltantes. - Las relaciones estadísticas son válidas para el propósito de modelado predictivo, aunque no deben interpretarse como verdades agronómicas universales. - La ausencia de variable temporal simplifica el análisis a un enfoque puramente tabular.
Métrica de Evaluación: La competición utiliza Balanced Accuracy (exactitud balanceada), que corresponde al promedio de los recalls por clase. Esta métrica es particularmente sensible al rendimiento en la clase minoritaria, lo que condicionará muchas de nuestras decisiones durante el EDA.
Este documento se adhiere a dos pilares metodológicos:
tidyverse (Wickham 2016).
Se enfatiza la reproducibilidad, la
programación funcional y la generación de
hipótesis visuales.El resultado es un informe que no solo describe los datos, sino que proporciona un plan de acción directo para la fase de modelado.
En un entorno corporativo, este paso implica reuniones con product managers y expertos de dominio. En el contexto de una competición de Kaggle, el stakeholder es el marco de evaluación. Nuestro “diálogo” consiste en interpretar las reglas del juego y la métrica de éxito para alinear nuestra estrategia analítica.
Maximizar la Balanced Accuracy en la predicción de
Irrigation_Need. Esto se traduce en los siguientes
imperativos analíticos: - Priorizar la detección de la clase
minoritaria (High): Un modelo que predice siempre
“Low” tiene una Balanced Accuracy de apenas 33.3%. - Identificar
predictores con alto poder discriminatorio, especialmente
aquellos que separan la clase “High” del resto. - Generar
características que capturen interacciones complejas entre
variables climáticas y del suelo.
Se redacta un resumen conciso que delimita el alcance y, crucialmente, lo que está fuera de alcance. Este documento sirve como “contrato” para evitar la deriva del análisis (scope creep).
| Aspecto | Acuerdo Alcanzado |
|---|---|
| Objetivo Primario | Realizar un diagnóstico exhaustivo de la calidad de los datos, explorar relaciones bivariadas y generar un conjunto de características de alto valor predictivo para un modelo de clasificación (XGBoost / LightGBM). |
| Población | 630,000 registros del archivo
train.csv. |
| Entregable | Este informe en PDF, acompañado de un script R reproducible que transforma los datos crudos en un dataset listo para modelar. |
| Fuera de Alcance | Construcción del modelo final y ajuste de hiperparámetros (esto corresponde a la siguiente fase). Análisis de series temporales (no hay variable de fecha). Interpretación agronómica causal de los datos sintéticos. |
| Métrica de Éxito del EDA | Identificar y validar al menos 3 variables con una
clara separación visual en diagramas de caja entre las clases de
Irrigation_Need. |
Transformar los objetivos en preguntas específicas y comprobables evita la exploración errática y proporciona un guion para el análisis.
Soil_Moisture, Rainfall_mm,
Temperature_C) muestran las medianas más diferenciadas
entre las clases objetivo?Crop_Growth_Stage (Etapa
del cultivo) o Season (Estación)?Soil_Moisture / Rainfall) o los índices de
estrés (ej. Temperature_C / Humidity) superan en
importancia a las variables originales?Se debe diferenciar el conocimiento previo del dominio (principios agronómicos) de las incógnitas específicas de este conjunto de datos sintéticos.
Soil_Moisture que
dispara la necesidad “Alta” en esta simulación.Wind_Speed_kmh y Temperature_C.Se evalúa la viabilidad técnica: ¿Podemos responder a nuestras preguntas con las columnas disponibles?
El dataset train.csv cuenta con 630,000 filas y
21 columnas. Tras la revisión del diccionario proporcionado por
Kaggle, se concluye que es altamente viable:
Date impide cualquier análisis temporal. Las categorías de
suelo (Soil_Type) están simplificadas a 4 tipos
texturales.Para evitar que el EDA se vuelva infinito, se realiza un ejercicio de priorización (T-Shirt Sizing).
| Pregunta de Investigación | Complejidad | Esfuerzo | Prioridad |
|---|---|---|---|
| Calidad de datos | Baja | S | Alta (Fundacional) |
| Distribución de la objetivo | Baja | S | Alta |
| Análisis Bivariado (Boxplots) | Media | M | Alta |
| Ingeniería de Características | Alta | L | Media-Alta |
| Boruta (Selección) | Alta | L | Media (Se aborda en este EDA) |
Dado el objetivo de la competición, se decidió incluir todas las preguntas en este análisis fundacional único para proporcionar una base sólida al modelado posterior.
Fase técnica de preparación de datos: corrección de tipos, estandarización de nombres y creación de pipelines.
# Función genérica para carga y limpieza inicial
load_and_clean <- function(path) {
read_csv(path, show_col_types = FALSE) %>%
janitor::clean_names() %>%
# Conversión de tipos según el diccionario
mutate(
soil_type = as.factor(soil_type),
crop_type = as.factor(crop_type),
crop_growth_stage = factor(crop_growth_stage,
levels = c("Sowing", "Vegetative", "Flowering", "Harvest"),
ordered = TRUE),
season = factor(season, levels = c("Zaid", "Kharif", "Rabi"), ordered = TRUE),
irrigation_type = as.factor(irrigation_type),
water_source = as.factor(water_source),
mulching_used = as.factor(mulching_used),
region = as.factor(region),
irrigation_need = factor(irrigation_need, levels = c("Low", "Medium", "High"), ordered = TRUE)
)
}
# Carga de datos (Ajustar ruta según entorno Kaggle)
df_raw <- load_and_clean(here("data", "train.csv"))
Verificación de Calidad Inicial:
# Resumen de nulos
cat("Valores nulos totales:", sum(is.na(df_raw)), "\n")
## Valores nulos totales: 0
# Duplicados exactos
cat("Filas duplicadas:", sum(duplicated(df_raw)))
## Filas duplicadas: 0
Resultado: 0 valores nulos y 0 duplicados. Calidad de datos excepcional.
Uso de resúmenes estadísticos (skimr) y visualizaciones
univariadas para captar la distribución, asimetría y presencia de
valores atípicos.
Irrigation_Needdf_raw %>%
count(irrigation_need) %>%
mutate(pct = n / sum(n) * 100) %>%
ggplot(aes(x = irrigation_need, y = n, fill = irrigation_need)) +
geom_col(width = 0.7) +
geom_text(aes(label = paste0(round(pct, 1), "%")), vjust = -0.5, size = 5) +
scale_fill_viridis_d(end = 0.8) +
labs(title = "Distribución de Irrigation Need", x = "Clase", y = "Frecuencia") +
theme_minimal(base_size = 12) +
theme(legend.position = "none")
Distribución de la variable objetivo (Desbalance Crítico)
Interpretación: La clase High representa solo el 3.33% de los datos. Este desbalance severo exige el uso de técnicas como SMOTE, pesos de clase o métricas robustas durante la validación del modelo.
La Tabla @ref(tab:summary-stats) resume las estadísticas principales.
df_raw %>%
select(where(is.numeric)) %>%
skim() %>%
# Selección de columnas relevantes para la tabla
select(skim_variable, numeric.mean, numeric.sd, numeric.p0, numeric.p50, numeric.p100, numeric.hist) %>%
rename(Variable = skim_variable, Media = numeric.mean, DE = numeric.sd,
Min = numeric.p0, Mediana = numeric.p50, Max = numeric.p100) %>%
mutate(across(where(is.numeric), ~ round(.x, 2))) %>% # Sintaxis corregida
kable(caption = "Resumen estadístico de variables numéricas", booktabs = TRUE) %>%
kable_styling(latex_options = "scale_down")
| Variable | Media | DE | Min | Mediana | Max | numeric.hist |
|---|---|---|---|---|---|---|
| id | 314999.50 | 181865.48 | 0.00 | 314999.50 | 629999.00 | ▇▇▇▇▇ |
| soil_p_h | 6.48 | 0.92 | 4.80 | 6.44 | 8.20 | ▆▇▇▆▆ |
| soil_moisture | 37.30 | 16.38 | 8.00 | 37.75 | 64.99 | ▇▇▇▇▇ |
| organic_carbon | 0.92 | 0.37 | 0.30 | 0.91 | 1.60 | ▇▇▆▆▆ |
| electrical_conductivity | 1.74 | 0.95 | 0.10 | 1.74 | 3.50 | ▇▇▇▇▆ |
| temperature_c | 27.00 | 8.62 | 12.00 | 26.96 | 42.00 | ▇▇▇▇▇ |
| humidity | 61.56 | 19.71 | 25.00 | 61.65 | 94.99 | ▆▇▇▇▇ |
| rainfall_mm | 1462.21 | 612.99 | 0.38 | 1467.16 | 2499.69 | ▂▇▇▆▇ |
| sunlight_hours | 7.51 | 2.00 | 4.00 | 7.58 | 11.00 | ▇▇▇▇▇ |
| wind_speed_kmh | 10.38 | 5.69 | 0.50 | 10.48 | 20.00 | ▇▇▇▇▇ |
| field_area_hectare | 7.52 | 4.22 | 0.30 | 7.38 | 15.00 | ▇▇▇▇▇ |
| previous_irrigation_mm | 62.32 | 34.25 | 0.02 | 61.15 | 119.99 | ▆▇▇▇▇ |
Observaciones Destacadas: -
rainfall_mm: Diferencia notable entre
Mediana (~1000) y Máximo (>3000), indicando asimetría
positiva (cola derecha larga). -
soil_moisture: Distribución simétrica
(Media ≈ Mediana), ideal para análisis paramétricos.
ggplot(df_raw, aes(x = rainfall_mm)) +
geom_histogram(aes(y = after_stat(density)), bins = 60, fill = "steelblue", alpha = 0.6, color = "white") +
geom_density(color = "darkred", linewidth = 1.2) +
labs(title = "Distribución de la Precipitación", x = "Precipitación (mm)", y = "Densidad") +
theme_minimal()
Histograma de Precipitación (Rainfall_mm) mostrando asimetría positiva
Implicación para el Modelado: Aunque los modelos
basados en árboles manejan bien la asimetría, aplicar una transformación
log(rainfall_mm + 1) puede ayudar a
modelos lineales en un stacking.
Aquí se concentra el núcleo visual del EDA. Abordamos cada pregunta con gráficos específicos.
Gráfico 1: Matriz de Correlación de Spearman
df_num <- df_raw %>% select(where(is.numeric))
cor_matrix <- cor(df_num, method = "spearman")
ggcorrplot(cor_matrix,
hc.order = TRUE,
type = "lower",
lab = TRUE,
lab_size = 2.5,
colors = c("#6D9EC1", "white", "#E46726"),
title = "Matriz de Correlación (Spearman)")
Matriz de Correlación de Spearman (Variables Numéricas Originales)
Interpretación: soil_moisture muestra
la correlación más fuerte con irrigation_need (~0.45).
rainfall_mm, wind_speed_kmh y
temperature_c tienen correlaciones bajas pero
significativas (~0.25).
Gráfico 2: Boxplot Multivariante - Soil_Moisture
vs Irrigation_Need
ggplot(df_raw, aes(x = irrigation_need, y = soil_moisture, fill = irrigation_need)) +
geom_boxplot(alpha = 0.7, outlier.alpha = 0.3) +
scale_fill_viridis_d(end = 0.8) +
labs(title = "Humedad del Suelo vs. Necesidad de Riego",
x = "Irrigation Need", y = "Soil Moisture (%)") +
theme_minimal() +
theme(legend.position = "none")
Separación de clases mediante la Humedad del Suelo
Interpretación: Las medianas de humedad son claramente decrecientes: Low (~45%) > Medium (~30%) > High (~15%). Existe solapamiento, pero la tendencia es robusta. Esta es la variable más importante del dataset.
Gráfico 3: Proporciones Apiladas -
Crop_Growth_Stage
df_raw %>%
count(crop_growth_stage, irrigation_need) %>%
group_by(crop_growth_stage) %>%
mutate(prop = n / sum(n)) %>%
ggplot(aes(x = crop_growth_stage, y = prop, fill = irrigation_need)) +
geom_col(position = "fill", width = 0.8) +
scale_fill_viridis_d(end = 0.8) +
labs(title = "Proporción de Necesidad de Riego por Etapa de Cultivo",
x = "Etapa de Crecimiento", y = "Proporción", fill = "Necesidad") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
La necesidad de riego ‘High’ predomina en las etapas de Floración y Vegetativa
Interpretación: Las etapas Flowering y Vegetative concentran la mayor proporción de necesidad “High”. Esta variable debe ser incluida como ordinal o mediante one-hot encoding en el modelo.
Creamos variables derivadas basadas en principios físicos (estrés hídrico y térmico).
df_feat <- df_raw %>%
mutate(
# Estrés Hídrico
moisture_deficit = 60 - soil_moisture, # Asumiendo 60% como óptimo
moisture_rain_ratio = soil_moisture / (rainfall_mm + 1),
# Estrés Térmico (Evapotranspiración Potencial)
heat_dry_index = temperature_c / (humidity + 1),
thermal_wind_stress = temperature_c * wind_speed_kmh / 10,
# Transformaciones
log_rainfall = log(rainfall_mm + 1),
sqrt_wind = sqrt(wind_speed_kmh)
)
Gráfico 4: Dispersión - Interacción
Soil_Moisture vs Rainfall_mm
# Muestreo para visualización clara (evita saturar el PDF)
set.seed(2024)
df_sample <- df_feat %>% sample_n(5000)
ggplot(df_sample, aes(x = soil_moisture, y = rainfall_mm, color = irrigation_need)) +
geom_point(alpha = 0.4, size = 1.5) +
scale_color_viridis_d(end = 0.8) +
labs(title = "Interacción Crítica: Humedad del Suelo vs. Lluvia",
x = "Soil Moisture (%)", y = "Rainfall (mm)", color = "Necesidad") +
theme_minimal() +
theme(legend.position = "bottom")
Zona de Alto Riesgo: Baja humedad y baja precipitación (Color Rojo)
Interpretación: Los puntos rojos (High) se
concentran en la esquina inferior izquierda (Baja Humedad + Baja
Lluvia). La variable derivada moisture_rain_ratio
captura matemáticamente esta zona de riesgo.
Para evitar el sobreajuste, aplicamos el algoritmo Boruta (Random Forest iterativo) sobre una muestra de 3,000 registros.
# Seleccionar variables predictoras (excluyendo id y la objetivo)
df_boruta <- df_feat %>%
select(-id, -irrigation_need) %>%
sample_n(3000)
# Ejecutar Boruta (Puede tardar unos segundos)
set.seed(2024)
boruta_output <- Boruta(irrigation_need ~ ., data = df_feat %>% sample_n(3000), doTrace = 1)
# Visualización
plot(boruta_output, cex.axis = 0.7, las = 2, xlab = "", main = "Importancia de Características (Boruta)")
Ranking de Importancia de Variables (Boruta)
# Obtener lista de confirmadas
confirmed_vars <- getSelectedAttributes(boruta_output, withTentative = FALSE)
Resultado: Se confirmaron 32 variables como
importantes. Las variables derivadas como
heat_dry_index, moisture_deficit y
moisture_rain_ratio aparecen en los primeros puestos,
validando el esfuerzo de ingeniería de
características.
Sintetizamos los descubrimientos en acciones concretas para la fase de modelado.
| Hallazgo | Implicación (“So What?”) | Acción Recomendada (“What Next?”) |
|---|---|---|
| Clase “High” es 3.3% | La Balanced Accuracy penaliza los falsos negativos en “High”. | Paso Crítico: Usar
SMOTE o scale_pos_weight en XGBoost.
No usar accuracy como métrica de
validación. |
soil_moisture es el feature
#1 |
Es el ancla del modelo. Debemos protegerlo y potenciarlo. | Crear interacciones (soil_moisture *
crop_growth_stage). No eliminarlo bajo ningún
concepto. |
rainfall_mm es
asimétrica |
Los árboles lo manejan bien, pero los modelos lineales no. | Mantener la variable cruda y añadir
log_rainfall como característica adicional
para un posible stacking. |
| 32 variables confirmadas por Boruta | Entrenar con 57 variables puede llevar a overfitting y es computacionalmente más costoso. | Usar el subset de 32 variables como punto de partida para el modelo base (Baseline). |
Formato de Entrega: Este documento PDF + Notebook de R en Kaggle.
Próximos Pasos Técnicos (Pipeline de Modelado):
clean_names() e ingeniería
de características al archivo test.csv.objective = 'multi:softprob'eval_metric = 'mlogloss'scale_pos_weight (calculado como
n_majority / n_minority) para la clase “High”.submission.csv con formato
id,Irrigation_Need.| Variable | Tipo | Descripción | Origen |
|---|---|---|---|
| id | int | Identificador único | Original |
| soil_moisture | float | Humedad volumétrica del suelo (%) | Original |
| rainfall_mm | float | Precipitación acumulada (mm) | Original |
| temperature_c | float | Temperatura ambiente (°C) | Original |
| crop_growth_stage | ord | Etapa fenológica | Original |
| irrigation_need | ord | Variable Objetivo | Original |
| moisture_deficit | float | Déficit hídrico (60 - soil_moisture) | Derivada |
| heat_dry_index | float | Índice de estrés térmico: Temp / (Hum+1) | Derivada |
| log_rainfall | float | Transformación logarítmica de la lluvia | Derivada |
[Listado de las 32 variables extraídas del objeto boruta_output]
sessionInfo()
## R version 4.5.3 (2026-03-11)
## Platform: x86_64-pc-linux-gnu
## Running under: elementary OS 8
##
## Matrix products: default
## BLAS: /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.12.0
## LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.12.0 LAPACK version 3.12.0
##
## locale:
## [1] LC_CTYPE=es_ES.UTF-8 LC_NUMERIC=C
## [3] LC_TIME=es_ES.UTF-8 LC_COLLATE=es_ES.UTF-8
## [5] LC_MONETARY=es_ES.UTF-8 LC_MESSAGES=es_ES.UTF-8
## [7] LC_PAPER=es_ES.UTF-8 LC_NAME=C
## [9] LC_ADDRESS=C LC_TELEPHONE=C
## [11] LC_MEASUREMENT=es_ES.UTF-8 LC_IDENTIFICATION=C
##
## time zone: Europe/Madrid
## tzcode source: system (glibc)
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] here_1.0.2 Boruta_9.0.0 viridis_0.6.5 viridisLite_0.4.3
## [5] ggcorrplot_0.1.4.1 corrplot_0.95 scales_1.4.0 kableExtra_1.4.0
## [9] knitr_1.51 DataExplorer_0.9.0 naniar_1.1.0 skimr_2.2.2
## [13] janitor_2.2.1 lubridate_1.9.5 forcats_1.0.1 stringr_1.6.0
## [17] dplyr_1.2.1 purrr_1.2.1 readr_2.2.0 tidyr_1.3.2
## [21] tibble_3.3.1 ggplot2_4.0.2 tidyverse_2.0.0
##
## loaded via a namespace (and not attached):
## [1] gtable_0.3.6 xfun_0.57 bslib_0.10.0
## [4] htmlwidgets_1.6.4 visdat_0.6.0 lattice_0.22-9
## [7] tzdb_0.5.0 vctrs_0.7.2 tools_4.5.3
## [10] generics_0.1.4 parallel_4.5.3 pkgconfig_2.0.3
## [13] Matrix_1.7-5 data.table_1.18.2.1 RColorBrewer_1.1-3
## [16] S7_0.2.1 lifecycle_1.0.5 compiler_4.5.3
## [19] farver_2.1.2 textshaping_1.0.5 data.tree_1.2.0
## [22] repr_1.1.7 codetools_0.2-20 snakecase_0.11.1
## [25] htmltools_0.5.9 sass_0.4.10 yaml_2.3.12
## [28] crayon_1.5.3 pillar_1.11.1 jquerylib_0.1.4
## [31] cachem_1.1.0 tidyselect_1.2.1 digest_0.6.39
## [34] stringi_1.8.7 reshape2_1.4.5 labeling_0.4.3
## [37] rprojroot_2.1.1 fastmap_1.2.0 grid_4.5.3
## [40] cli_3.6.5 magrittr_2.0.5 base64enc_0.1-6
## [43] withr_3.0.2 bit64_4.6.0-1 timechange_0.4.0
## [46] rmarkdown_2.31 networkD3_0.4.1 bit_4.6.0
## [49] igraph_2.2.3 otel_0.2.0 gridExtra_2.3
## [52] ranger_0.18.0 hms_1.1.4 evaluate_1.0.5
## [55] rlang_1.2.0 Rcpp_1.1.1 glue_1.8.0
## [58] xml2_1.5.2 vroom_1.7.1 svglite_2.2.2
## [61] rstudioapi_0.18.0 jsonlite_2.0.0 plyr_1.8.9
## [64] R6_2.6.1 systemfonts_1.3.2
Informe de EDA preparado para la competición Kaggle “Predicting Irrigation Need”. Metodología Híbrida basada en la literatura de Ciencia de Datos y el flujo de trabajo de 11 pasos de la industria.