Este estudio tiene como objetivo fortalecer el tratamiento de la no respuesta en encuestas económicas mediante la evaluación comparativa de diversos métodos de imputación. Se analizan métodos aplicados a dos variables clave: personal ocupado e ingresos.
Se comparan los métodos institucionales actuales —vecino más cercano ajustado por dominio–estrato— con alternativas multivariadas de mayor complejidad como MICE y k-NN. Asimismo, se optimizan hiperparámetros y se evalúa el desempeño de cada método mediante simulaciones controladas.
El tratamiento adecuado de la no respuesta es fundamental para mantener la calidad de las estadísticas derivadas de encuestas económicas. En la EMEC, los métodos institucionales han sido diseñados para garantizar la coherencia histórica de las series; sin embargo, dichos métodos pueden presentar limitaciones ante patrones complejos de no respuesta.
Este documento analiza alternativas modernas de imputación multivariada y las contrasta con los enfoques tradicionales, utilizando para ello datos sintéticos con estructura estadística similar a la EMEC. El objetivo es identificar configuraciones que minimicen el error de imputación sin comprometer la coherencia temporal de las series económicas.
# --- cargamos paquetes necesarios ---
library(tidyverse) # Manipulación de datos y gráficos (ggplot2)
library(openxlsx) # Lectura de archivos Excel
library(naniar) # Visualización de datos faltantes
library(finalfit) # Prueba de Little (MCAR)
library(mice) # Imputación Múltiple (MICE)
library(VIM) # Imputación k-NN
library(missForest) # Imputación Random Forest (Machine Learning)
library(Metrics) # Cálculo de métricas de error (RMSE)
library(zoo) # Funciones de series de tiempo (rollmean)
library(lubridate) # Manejo de fechas
Dada la necesidad de evaluar métodos basados en series de tiempo —que requieren historial suficiente— se generó un conjunto sintético controlado que replica las características estadísticas de la EMEC. Se simulan tendencias, ruido y estructura estratificada tanto para ingresos como para personal ocupado.
Este enfoque permite evaluar el desempeño de los distintos métodos de imputación en un entorno donde se conoce la “verdad” de los datos, estableciendo así un marco justo para la comparación.
# --- generamos datos sintéticos ---
# creamos series de ingreso y personal para cada empresa con tendencias y ruido
set.seed(2025)
n_emp <- 50
# 36 meses para tener historia suficiente para variación anual
n_months <- 36
start_period <- as.Date("2021-01-01")
periods <- seq.Date(from = start_period, by = "month", length.out = n_months)
rows <- list()
for(i in 1:n_emp){
dominio <- sample(c(43,46), size = 1)
estrato <- sample(c(1,2,3), size = 1)
if(estrato == 1){ factor <- 1 } else { factor <- round(runif(1, 1.5, 25), 3) }
# INGRESOS
if(estrato == 1){
base_start <- runif(1, 200000, 600000)
trend <- rnorm(1, mean = 800, sd = 150)
noise <- rnorm(n_months, mean = 0, sd = base_start * 0.10)
} else if(estrato == 2){
base_start <- runif(1, 80000, 200000)
trend <- rnorm(1, mean = 300, sd = 80)
noise <- rnorm(n_months, mean = 0, sd = base_start * 0.06)
} else {
base_start <- runif(1, 30000, 100000)
trend <- rnorm(1, mean = 150, sd = 60)
noise <- rnorm(n_months, mean = 0, sd = base_start * 0.04)
}
ingresos_series <- base_start + cumsum(trend + noise)
ingresos_series[ingresos_series < 1000] <- 1000
# PERSONAL
if(estrato == 1){
emp_base <- rpois(1, lambda = 20) + 5
emp_changes <- rnorm(n_months, mean = 0.5, sd = 1.5)
} else if(estrato == 2){
emp_base <- rpois(1, lambda = 8) + 2
emp_changes <- rnorm(n_months, mean = 0.2, sd = 1.0)
} else {
emp_base <- rpois(1, lambda = 4) + 1
emp_changes <- rnorm(n_months, mean = 0.1, sd = 0.8)
}
emp_series <- pmax(1, round(emp_base + cumsum(emp_changes)))
for(j in seq_along(periods)){
rows[[length(rows) + 1]] <- data.frame(
clee = as.character(i),
fecha = periods[j],
personal = as.integer(emp_series[j]),
ingreso = ingresos_series[j],
dominio = dominio,
estrato = estrato,
imputado = 0,
stringsAsFactors = FALSE
)
}
}
datos <- bind_rows(rows)
datos_reales <- datos
cat("Datos sintéticos generados. Total:", nrow(datos), "\n")
## Datos sintéticos generados. Total: 1800
# --- función: promedio móvil ---
# calculamos la imputación usando rollapply con ventana k
imputar_promedio_movil <- function(data, variable, k) {
data %>%
group_by(clee) %>%
arrange(fecha) %>%
mutate(
imputado = rollapply(get(variable), width = k, FUN = mean,
align = "right", fill = NA, partial = FALSE, na.rm = TRUE)
) %>%
ungroup() %>%
pull(imputado)
}
# --- función: variación anual ---
# imputamos usando tasa anual por dominio-estrato
imputar_variacion_anual <- function(data, variable) {
tasas <- data %>%
group_by(dominio, estrato, fecha) %>%
summarise(tasa = median(get(variable)/lag(get(variable), 12) - 1, na.rm=TRUE), .groups="drop")
data %>%
left_join(tasas, by=c("dominio","estrato","fecha")) %>%
group_by(clee) %>%
arrange(fecha) %>%
mutate(
imputado = lag(get(variable), 12) * (1 + replace_na(tasa, 0))
) %>%
ungroup() %>%
pull(imputado)
}
# --- función: knn estratificado ---
# imputamos usando vecino más cercano dentro de dominio y estrato
imputar_knn_estratificado <- function(data, variable) {
res <- VIM::kNN(data, variable = variable,
dist_var = c("dominio", "estrato"),
k = 1, imp_var = FALSE)
return(res[[variable]])
}
# --- función maestra: cascada institucional ---
# aplicamos variación anual, promedios móviles y al final knn si aún faltan valores
imputar_institucional_integrado <- function(data, variable) {
tasas <- data %>%
group_by(dominio, estrato, fecha) %>%
summarise(g_sector = median(get(variable)/lag(get(variable), 12) - 1, na.rm=TRUE), .groups="drop")
data_proc <- data %>%
left_join(tasas, by=c("dominio","estrato","fecha")) %>%
group_by(clee) %>%
arrange(fecha) %>%
mutate(
c_var = lag(get(variable), 12) * (1 + replace_na(g_sector, 0)),
c_ma6 = rollapply(get(variable), 6, mean, align="right", fill=NA, partial=FALSE, na.rm=TRUE),
c_ma3 = rollapply(get(variable), 3, mean, align="right", fill=NA, partial=FALSE, na.rm=TRUE),
c_ma2 = rollapply(get(variable), 2, mean, align="right", fill=NA, partial=FALSE, na.rm=TRUE)
) %>%
ungroup()
data_proc$final <- coalesce(data_proc[[variable]], data_proc$c_var, data_proc$c_ma6, data_proc$c_ma3, data_proc$c_ma2)
faltan <- which(is.na(data_proc$final))
if(length(faltan) > 0) {
knn_imp <- VIM::kNN(data_proc, variable=variable, dist_var=c("dominio","estrato"), k=1, imp_var=FALSE)
data_proc$final[faltan] <- knn_imp[[variable]][faltan]
}
return(data_proc$final)
}
El resumen descriptivo muestra valores coherentes con la estructura simulada y confirma correlación positiva entre personal ocupado e ingresos.
En la gráfica de dispersión se observa una relación creciente entre ambas variables, lo cual es consistente con la dinámica económica simulada.
A continuación se presentan hallazgos derivados de la optimización del algoritmo MICE.
# --- exploración inicial de datos ---
# resumen y correlaciones
datos %>% select(personal, ingreso) %>% summary()
## personal ingreso
## Min. : 1.00 Min. : 30665
## 1st Qu.: 9.00 1st Qu.: 77803
## Median :17.00 Median :162205
## Mean :20.43 Mean :216795
## 3rd Qu.:31.00 3rd Qu.:325511
## Max. :64.00 Max. :867190
# correlación entre personal y ingreso
correlacion <- cor(datos$personal, datos$ingreso, use = "complete.obs")
correlacion
## [1] 0.6684274
# scatter
ggplot(datos, aes(x = personal, y = ingreso)) +
geom_point(alpha = 0.4, color = "darkblue") +
labs(title = paste0("Correlación: PO vs ING (r = ", round(correlacion, 2), ")"),
subtitle = "Relación entre Personal Ocupado e Ingresos",
x = "PO (Personal Ocupado)",
y = "ING (Ingresos)") +
theme_minimal()
Relación bivariada: El gráfico de dispersión evidencia la asociación entre el Personal Ocupado (eje X) y los Ingresos (eje Y).
Tendencia general: Se observa una tendencia positiva clara, respaldada por un coeficiente de correlación de Pearson de r = 0.67, lo cual sugiere una relación lineal moderada-alta entre ambas variables.
Patrón de concentración: Los puntos se concentran predominantemente en la esquina inferior izquierda, reflejando que la mayoría de las unidades son empresas pequeñas con niveles reducidos de ingresos.
Heterocedasticidad: A medida que aumenta el personal ocupado, la variabilidad en los ingresos crece notablemente, generando una estructura en forma de “abanico”. Este patrón indica que el error no es constante y que existen diferencias sustanciales en la dispersión entre empresas pequeñas y grandes.
Implicación para el análisis: La presencia de correlación relativamente alta y la heterogeneidad en la dispersión justifican el uso de métodos de imputación que aprovechan relaciones auxiliares fuertes.
Justificación metodológica: Dado el patrón de heterocedasticidad, una regresión lineal simple resultaría insuficiente. En contraste, métodos no paramétricos como Random Forest o k-NN ofrecen mayor flexibilidad para capturar relaciones no lineales y variabilidad irregular, particularmente en empresas con mayores niveles de personal e ingresos.
# --- distribución por estrato ---
# vemos la heterogeneidad entre grupos para justificar métodos estratificados
ggplot(datos, aes(x = factor(estrato), y = ingreso, fill = factor(estrato))) +
geom_boxplot(alpha = 0.6) +
# --- escala log ---
# colocamos escala log para visualizar mejor diferencias grandes
scale_y_log10(labels = scales::comma) +
labs(title = "Distribución de Ingresos por Estrato",
subtitle = "Se observa una clara diferenciación: Estrato 1 concentra mayores ingresos",
x = "Estrato (1=Grande, 3=Pequeño)",
y = "Ingresos (Escala Logarítmica)",
fill = "Estrato") +
theme_minimal()
El gráfico de cajas y bigotes compara la variable Ingresos (eje Y, en escala logarítmica) desagregada por Estrato (eje X).
Jerarquía clara entre estratos: Se observa una separación escalonada entre los grupos. El Estrato 1 presenta las medianas más altas, seguido por el Estrato 2 y, finalmente, el Estrato 3 con los niveles de ingreso más bajos.
Uso de escala logarítmica: Debido a la transformación logarítmica en el eje Y, las diferencias visuales entre cajas representan variaciones exponenciales en los ingresos reales, no simples diferencias lineales.
Patrón de dispersión: El Estrato 1 exhibe un rango intercuartil considerablemente mayor, indicando una alta variabilidad en los ingresos de empresas grandes. En contraste, el Estrato 3 muestra una estructura más compacta y homogénea, característica de empresas pequeñas.
Hallazgo analítico: Este comportamiento confirma la necesidad de aplicar procesos de imputación diferenciados por grupo (estratificación), descartando métodos globales que ignoran la estructura jerárquica de la población.
Heterogeneidad estructural: Tratar a todas las empresas como un único conjunto induciría sesgos graves. Imputar valores faltantes de una unidad del Estrato 3 mediante un promedio general generaría sobreestimaciones significativas debido al peso del Estrato 1.
Justificación metodológica: La estructura observada soporta el uso de hiperparámetros basados en dominio-estrato en métodos institucionales (como el vecino más cercano ajustado). Asimismo, valida el uso de modelos de Machine Learning, como Random Forest, que segmentan naturalmente el espacio predictor creando divisiones basadas en el estrato, permitiendo predicciones coherentes con el tamaño real de cada empresa.
Se define un escenario MCAR (Missing Completely at Random) borrando el 20% de los datos de forma aleatoria para evaluar la capacidad de recuperación de cada método.
# --- preparamos ground truth ---
# tomamos registros completos para evaluar los métodos
datos_reales <- datos %>%
select(clee, fecha, personal, ingreso, dominio, estrato) %>%
drop_na(personal, ingreso)
n <- nrow(datos_reales)
cat("n registros reales (completos):", n, "\n")
## n registros reales (completos): 1800
# --- simulamos mecanismo mar ---
# la probabilidad depende de estrato, tamaño y dominio
set.seed(2025)
prop_faltantes <- 0.20
idx_personal_mcar <- sample(1:n, size = floor(prop_faltantes * n))
idx_ingreso_mcar <- sample(1:n, size = floor(prop_faltantes * n)) # Cambio de nombre
datos_mcar <- datos_reales
datos_mcar$personal[idx_personal_mcar] <- NA
datos_mcar$ingreso[idx_ingreso_mcar] <- NA
# --- simulación mar ---
# la probabilidad de falta depende de estrato, tamaño y dominio
set.seed(2025)
datos_mar <- datos_reales
n <- nrow(datos_mar)
# --- Probabilidad según estrato ---
# Estrato 1 suele ser el que más no responde
p_estrato <- case_when(
datos_mar$estrato == 1 ~ 0.30, # pequeñas
datos_mar$estrato == 2 ~ 0.20,
datos_mar$estrato == 3 ~ 0.10,
TRUE ~ 0.05
)
# --- Probabilidad según tamaño ---
cuartil_personal <- ntile(datos_mar$personal, 4)
p_personal <- case_when(
cuartil_personal == 1 ~ 0.30, # empresas chicas responden menos
cuartil_personal == 2 ~ 0.20,
cuartil_personal == 3 ~ 0.10,
cuartil_personal == 4 ~ 0.05, # grandes responden más
TRUE ~ 0.15
)
# --- Probabilidad según dominio ---
p_dominio <- ifelse(datos_mar$dominio == 46, 0.25, 0.10)
# --- Combinado de las probabilidades ---
# Tomamos un promedio ponderado (puedes ajustar los pesos)
prob_falta_total <- 0.5 * p_estrato +
0.3 * p_personal +
0.2 * p_dominio
# Para asegurar que max no exceda 1
prob_falta_total <- pmin(prob_falta_total, 0.90)
# --- Aplicar NA en ingreso y personal ---
random_uniform <- runif(n)
datos_mar$ingreso[ random_uniform < prob_falta_total ] <- NA
datos_mar$personal[ runif(n) < prob_falta_total ] <- NA
# 1. Unificar los datasets simulados para compararlos
df_comparativo <- bind_rows(
datos_mcar %>% mutate(Mecanismo = "MCAR"),
datos_mar %>% mutate(Mecanismo = "MAR")
) %>%
mutate(
# Recalculamos cuartiles sobre el dato de personal
cuartil_personal = ntile(personal, 4)
)
# 2. Calcular la proporción de NAs en 'ingreso' por grupo
heatmap_data <- df_comparativo %>%
group_by(Mecanismo, estrato, cuartil_personal) %>%
summarise(
# AQUÍ ESTÁ LA CLAVE: Usamos 'ingreso' que es como se llama en tus datos sintéticos
prop_na = mean(is.na(ingreso)),
.groups = "drop"
)
# 3. Generar el Mapa de Calor (Heatmap)
ggplot(heatmap_data, aes(x = factor(cuartil_personal), y = factor(estrato), fill = prop_na)) +
geom_tile(color = "white") + # Bordes blancos
facet_wrap(~Mecanismo) + # Dividir paneles
# Escala de colores (Rojo para NA)
scale_fill_gradient(low = "#fee5d9", high = "#de2d26",
limits = c(0, 1), name = "Prop. NA") +
# Etiquetas
labs(title = "Comparación MCAR vs MAR",
subtitle = "Proporción de NA en 'ingresos'",
x = "Cuartil de Personal",
y = "Estrato") +
theme_minimal() +
theme(
panel.grid = element_blank(),
axis.text = element_text(color = "gray30")
)
El mapa de calor compara la estructura de los datos faltantes en la variable Ingresos bajo dos mecanismos simulados: MCAR y MAR.
Ejes del gráfico:
Panel MCAR (derecha): Los colores aparecen dispersos sin un patrón definido. Las intensidades son suaves e irregulares, lo que es consistente con un mecanismo completamente aleatorio donde los datos se pierden sin relación con las características de la empresa.
Panel MAR (izquierda): Se aprecia una estructura claramente definida. Destaca un bloque de color más intenso en el Estrato 2, Cuartil 4, indicando una concentración sistemática de no respuesta en un grupo específico.
Hallazgo: El gráfico valida la correcta implementación del diseño experimental.
Implicaciones para la imputación: El patrón estructurado del escenario MAR constituye el principal desafío metodológico. Cualquier método que no sea capaz de manejar este sesgo (ej. promedios globales) producirá imputaciones distorsionadas.
Justificación metodológica: La presencia de un bloque sistemático de no respuesta en MAR confirma la necesidad de utilizar métodos avanzados, como Random Forest, que pueden identificar patrones complejos y generar imputaciones coherentes según el perfil de cada empresa. Esto es especialmente importante para instituciones como el INEGI, donde ignorar dicha estructura generaría estimaciones sesgadas.
Se implementan los métodos: - Institucionales: Promedios Móviles, Variación Anual y Estrategia en Cascada. - Alternativos: Media, MICE, k-NN y Random Forest.
# --- función: rmse ---
# calculamos rmse solo sobre posiciones donde hubo na
calcular_rmse <- function(real, imputado, mascara) {
# rmse sobre las posiciones donde mascara==TRUE
if(sum(mascara) == 0) return(NA)
return(rmse(real[mascara], imputado[mascara]))
}
maxit y m (iteraciones e imputaciones)# --- optimizamos parámetros de mice ---
# probamos combinaciones de maxit y m sobre datos mcar
df_opt <- datos_mcar
mascara_na <- as.data.frame(is.na(df_opt))
iteraciones <- c(5, 10, 20)
m_values <- c(5, 10)
res_mice <- tibble()
for(it in iteraciones) {
for(mv in m_values) {
imp <- try(mice(df_opt, method = 'pmm', m = mv, maxit = it, printFlag = FALSE, seed = 123), silent = TRUE)
if(inherits(imp, "try-error")) next
comp <- mice::complete(imp)
rmse_p <- calcular_rmse(datos_reales$personal, comp$personal, mascara_na$personal)
rmse_m <- calcular_rmse(datos_reales$ingreso, comp$ingreso, mascara_na$ingreso)
res_mice <- bind_rows(res_mice, tibble(maxit = it, m = mv, rmse_personal = rmse_p, rmse_ingreso = rmse_m, rmse_promedio = mean(c(rmse_p, rmse_m), na.rm = TRUE)))
}
}
# --- GRÁFICA CON DESTAQUE (CÍRCULO ROJO) ---
mejor_config <- res_mice %>% filter(rmse_promedio == min(rmse_promedio))
ggplot(res_mice, aes(x = factor(maxit), y = rmse_promedio, group = factor(m), color = factor(m))) +
geom_line(size = 0.8) +
geom_point(size = 2) +
# Círculo rojo en el mínimo
geom_point(data = mejor_config, aes(x = factor(maxit), y = rmse_promedio),
color = "red", size = 5, shape = 1, stroke = 1.5) +
# Etiqueta
geom_label(data = mejor_config,
aes(label = paste0("Min RMSE:\n", format(round(rmse_promedio, 0), big.mark=","))),
vjust = -0.5, color = "black", fontface = "bold", fill="white", alpha=0.9) +
labs(title = "Optimización MICE: Selección de Hiperparámetros",
subtitle = paste0("Configuración óptima detectada: m=", mejor_config$m, " | maxit=", mejor_config$maxit),
x = "Iteraciones (maxit)", y = "RMSE Promedio", color = "m") +
theme_minimal() + theme(legend.position = "bottom")
Los valores m = 5 y m = 10 se seleccionaron siguiendo las recomendaciones clásicas de Rubin (1987) y van Buuren (2018), quienes sugieren trabajar con entre 5 y 10 imputaciones para obtener estimadores estables sin incrementar innecesariamente el costo computacional.
Se evaluaron maxit = 5, 10 y 20 porque MICE usualmente converge entre 5 y 10 iteraciones (van Buuren, 2018). Incluir 20 permite examinar la estabilidad de la cadena y verificar si un número mayor de iteraciones reduce adicionalmente el RMSE.
El gráfico de líneas evalúa el desempeño del algoritmo MICE
midiendo el RMSE promedio (eje Y) bajo distintas combinaciones de dos
parámetros:
- maxit (eje X): número de iteraciones del algoritmo.
- m (color de las líneas): cantidad de imputaciones
múltiples.
k# --- optimizamos knn ---
# probamos valores de k y evaluamos rmse
k_values <- c(3, 5, 10, 20)
res_knn <- tibble()
for(k in k_values) {
imp_knn <- kNN(df_opt, variable = c("personal", "ingreso"), k = k, imp_var = FALSE)
rmse_p <- calcular_rmse(datos_reales$personal, imp_knn$personal, mascara_na$personal)
rmse_m <- calcular_rmse(datos_reales$ingreso, imp_knn$ingreso, mascara_na$ingreso)
res_knn <- bind_rows(res_knn, tibble(k = k, rmse_personal = rmse_p, rmse_ingreso = rmse_m, rmse_promedio = mean(c(rmse_p, rmse_m), na.rm = TRUE)))
}
## ingreso dominio estrato ingreso dominio estrato
## 30664.53 43.00 1.00 867190.28 46.00 3.00
## personal dominio estrato personal dominio estrato
## 1 43 1 64 46 3
## ingreso dominio estrato ingreso dominio estrato
## 30664.53 43.00 1.00 867190.28 46.00 3.00
## personal dominio estrato personal dominio estrato
## 1 43 1 64 46 3
## ingreso dominio estrato ingreso dominio estrato
## 30664.53 43.00 1.00 867190.28 46.00 3.00
## personal dominio estrato personal dominio estrato
## 1 43 1 64 46 3
## ingreso dominio estrato ingreso dominio estrato
## 30664.53 43.00 1.00 867190.28 46.00 3.00
## personal dominio estrato personal dominio estrato
## 1 43 1 64 46 3
# --- GRÁFICA CON DESTAQUE ---
mejor_knn <- res_knn %>% filter(rmse_promedio == min(rmse_promedio))
ggplot(res_knn, aes(x = k, y = rmse_promedio)) +
geom_line() + geom_point() +
# Círculo rojo
geom_point(data = mejor_knn, aes(x = k, y = rmse_promedio),
color = "red", size = 5, shape = 1, stroke = 1.5) +
geom_label(data = mejor_knn,
aes(label = paste0("Min RMSE:\n", format(round(rmse_promedio, 0), big.mark=","))),
vjust = -0.5, fontface = "bold") +
labs(title = "Optimización k-NN: selección de k",
subtitle = paste0("El error se minimiza en k=", mejor_knn$k),
x = "Vecinos (k)", y = "RMSE Promedio") +
theme_minimal()
El gráfico de línea muestra la evolución del RMSE promedio (eje Y) conforme aumenta el número de vecinos (\(k\), eje X) utilizados por el algoritmo k-NN.
# --- preparamos dataset base ---
# añadimos variables auxiliares para los modelos
df_base <- datos_mcar
df_base$clee <- datos_reales$clee
df_base$fecha <- datos_reales$fecha
df_base$dominio <- datos_reales$dominio
df_base$estrato <- datos_reales$estrato
mascara_na <- as.data.frame(is.na(df_base))
# --- imputación media ---
# reemplazamos por medias por dominio-estrato
imp_media <- df_base %>%
group_by(dominio, estrato) %>%
mutate(
personal = ifelse(is.na(personal), mean(personal, na.rm=TRUE), personal),
ingreso = ifelse(is.na(ingreso), mean(ingreso, na.rm=TRUE), ingreso)
) %>% ungroup()
# Relleno de seguridad por si algún estrato es todo NA
imp_media$personal[is.na(imp_media$personal)] <- mean(df_base$personal, na.rm=TRUE)
imp_media$ingreso[is.na(imp_media$ingreso)] <- mean(df_base$ingreso, na.rm=TRUE)
# --- imputación mice ---
# usamos parámetros optimizados: m = 1, maxit = 1
cols_mice <- c("personal", "ingreso", "dominio", "estrato")
imp_mice_mod <- mice(df_base[, cols_mice], method='pmm', m=10, maxit=5, printFlag=FALSE, seed=123)
imp_mice_data <- mice::complete(imp_mice_mod)
imp_mice <- df_base
imp_mice$personal <- imp_mice_data$personal
imp_mice$ingreso <- imp_mice_data$ingreso
# --- imputación knn ---
# usamos knn con k=5 y distancia dominio-estrato
imp_knn <- kNN(df_base, variable=c("personal","ingreso"), dist_var=c("dominio","estrato"), k=10, imp_var=FALSE)
## dominio estrato dominio estrato
## 43 1 46 3
## dominio estrato dominio estrato
## 43 1 46 3
# --- Random Forest ---
# Aseguramos factores para que RF entienda grupos
df_rf_in <- df_base[, c("personal","ingreso","dominio","estrato")]
df_rf_in$dominio <- as.factor(df_rf_in$dominio)
df_rf_in$estrato <- as.factor(df_rf_in$estrato)
imp_rf_obj <- missForest(df_rf_in, verbose=FALSE)
imp_rf <- df_base
imp_rf$personal <- imp_rf_obj$ximp$personal
imp_rf$ingreso <- imp_rf_obj$ximp$ingreso
# --- MÉTODOS INSTITUCIONALES (INDIVIDUALES) ---
# Generamos los vectores necesarios para la tabla detallada
df_inst <- df_base
# Personal
per_pm2 <- imputar_promedio_movil(df_inst, "personal", 2)
per_pm3 <- imputar_promedio_movil(df_inst, "personal", 3)
per_pm6 <- imputar_promedio_movil(df_inst, "personal", 6)
per_anual <- imputar_variacion_anual(df_inst, "personal")
per_knn_est <- imputar_knn_estratificado(df_inst, "personal")
## dominio estrato dominio estrato
## 43 1 46 3
# Ingreso
ing_pm2 <- imputar_promedio_movil(df_inst, "ingreso", 2)
ing_pm3 <- imputar_promedio_movil(df_inst, "ingreso", 3)
ing_pm6 <- imputar_promedio_movil(df_inst, "ingreso", 6)
ing_anual <- imputar_variacion_anual(df_inst, "ingreso")
ing_knn_est <- imputar_knn_estratificado(df_inst, "ingreso")
## dominio estrato dominio estrato
## 43 1 46 3
# --- ESTRATEGIA INSTITUCIONAL INTEGRADA (CASCADA) ---
# Solución completa
ingreso_cascada <- imputar_institucional_integrado(df_inst, "ingreso")
## dominio estrato dominio estrato
## 43 1 46 3
# --- CONSOLIDACIÓN ---
calc_rmse_safe <- function(real, estimado, mask) {
indices <- which(mask & !is.na(estimado))
if(length(indices) == 0) return(NA)
return(rmse(real[indices], estimado[indices]))
}
res_final <- tibble(
metodo = c("Media (Estrato)", "MICE", "k-NN", "Random Forest",
"Prom. Movil 2", "Prom. Movil 3", "Prom. Movil 6",
"Var. Anual", "Vecino (Estrato)", "Estrategia Cascada (INEGI)"),
rmse_personal = c(
calc_rmse_safe(datos_reales$personal, imp_media$personal, mascara_na$personal),
calc_rmse_safe(datos_reales$personal, imp_mice$personal, mascara_na$personal),
calc_rmse_safe(datos_reales$personal, imp_knn$personal, mascara_na$personal),
calc_rmse_safe(datos_reales$personal, imp_rf$personal, mascara_na$personal),
calc_rmse_safe(datos_reales$personal, per_pm2, mascara_na$personal),
calc_rmse_safe(datos_reales$personal, per_pm3, mascara_na$personal),
calc_rmse_safe(datos_reales$personal, per_pm6, mascara_na$personal),
calc_rmse_safe(datos_reales$personal, per_anual, mascara_na$personal),
calc_rmse_safe(datos_reales$personal, per_knn_est, mascara_na$personal),
NA # No calculamos cascada para personal en este ejemplo
),
rmse_ingreso = c(
calc_rmse_safe(datos_reales$ingreso, imp_media$ingreso, mascara_na$ingreso),
calc_rmse_safe(datos_reales$ingreso, imp_mice$ingreso, mascara_na$ingreso),
calc_rmse_safe(datos_reales$ingreso, imp_knn$ingreso, mascara_na$ingreso),
calc_rmse_safe(datos_reales$ingreso, imp_rf$ingreso, mascara_na$ingreso),
calc_rmse_safe(datos_reales$ingreso, ing_pm2, mascara_na$ingreso),
calc_rmse_safe(datos_reales$ingreso, ing_pm3, mascara_na$ingreso),
calc_rmse_safe(datos_reales$ingreso, ing_pm6, mascara_na$ingreso),
calc_rmse_safe(datos_reales$ingreso, ing_anual, mascara_na$ingreso),
calc_rmse_safe(datos_reales$ingreso, ing_knn_est, mascara_na$ingreso),
calc_rmse_safe(datos_reales$ingreso, ingreso_cascada, mascara_na$ingreso)
)
)
# --- análisis de cobertura ---
# ver qué porcentaje de los huecos pudo llenar cada método
calcular_cobertura <- function(imp_vec, mask_vec) {
total_huecos <- sum(mask_vec)
llenos <- sum(mask_vec & !is.na(imp_vec))
return((llenos / total_huecos) * 100)
}
res_final <- res_final %>%
mutate(
cob_personal = c(
calcular_cobertura(imp_media$personal, mascara_na$personal),
calcular_cobertura(imp_mice$personal, mascara_na$personal),
calcular_cobertura(imp_knn$personal, mascara_na$personal),
calcular_cobertura(imp_rf$personal, mascara_na$personal),
calcular_cobertura(per_pm2, mascara_na$personal),
calcular_cobertura(per_pm3, mascara_na$personal),
calcular_cobertura(per_pm6, mascara_na$personal),
calcular_cobertura(per_anual, mascara_na$personal),
calcular_cobertura(per_knn_est, mascara_na$personal),
NA # <--- 10. ESTRATEGIA CASCADA (No la aplicamos a Personal en este ejemplo)
),
cob_ingreso = c(
calcular_cobertura(imp_media$ingreso, mascara_na$ingreso),
calcular_cobertura(imp_mice$ingreso, mascara_na$ingreso),
calcular_cobertura(imp_knn$ingreso, mascara_na$ingreso),
calcular_cobertura(imp_rf$ingreso, mascara_na$ingreso),
calcular_cobertura(ing_pm2, mascara_na$ingreso),
calcular_cobertura(ing_pm3, mascara_na$ingreso),
calcular_cobertura(ing_pm6, mascara_na$ingreso),
calcular_cobertura(ing_anual, mascara_na$ingreso),
calcular_cobertura(ing_knn_est, mascara_na$ingreso),
calcular_cobertura(ingreso_cascada, mascara_na$ingreso) # <--- 10. ESTRATEGIA CASCADA (Aquí sí la calculamos)
)
)
knitr::kable(res_final, digits = 2, caption = "Tabla 1: Resumen de Desempeño (RMSE y Cobertura)")
| metodo | rmse_personal | rmse_ingreso | cob_personal | cob_ingreso |
|---|---|---|---|---|
| Media (Estrato) | 6.80 | 113006.6 | 100.00 | 100.00 |
| MICE | 9.71 | 158663.5 | 100.00 | 100.00 |
| k-NN | 7.58 | 140013.3 | 100.00 | 100.00 |
| Random Forest | 6.48 | 112142.1 | 100.00 | 100.00 |
| Prom. Movil 2 | 18.79 | 239838.9 | 93.06 | 92.22 |
| Prom. Movil 3 | 18.65 | 241396.6 | 93.89 | 95.28 |
| Prom. Movil 6 | 18.21 | 237068.6 | 85.56 | 86.94 |
| Var. Anual | 17.25 | 202261.4 | 53.89 | 50.56 |
| Vecino (Estrato) | 8.17 | 118473.2 | 100.00 | 100.00 |
| Estrategia Cascada (INEGI) | NA | 237806.1 | NA | 100.00 |
Análisis de desempeño (RMSE) y viabilidad operativa (Cobertura)
Superioridad de Random Forest:
Random Forest se posiciona como el método más robusto entre todos los
evaluados. Obtiene los errores más bajos tanto en Personal
(RMSE = 6.48) como en Ingresos (RMSE = 112,142.1). Su
naturaleza no paramétrica le permite capturar relaciones complejas y no
lineales presentes en los datos económicos, superando incluso a la
imputación por Media Estratificada, tradicionalmente usada como
referencia.
Problema del “arranque en frío”
(cobertura):
La tabla evidencia una limitación crítica de los métodos institucionales
basados en continuidad histórica. Aunque los algoritmos de Machine
Learning (RF, k-NN, MICE) garantizan cobertura del
100%, métodos como la Variación Anual alcanzan solo
50.56% de imputación en ingresos. Esto confirma que
depender de historia previa deja huecos significativos cuando se trata
de empresas nuevas, reingresos o unidades con información
incompleta.
Desempeño comparado: Personal
vs. Ingresos:
La variable Personal muestra gran estabilidad: todos los
métodos presentan errores muy bajos (RMSE < 19). Sin embargo,
Ingresos es la variable determinante por su alta varianza. En
este caso, la Estrategia en Cascada institucional presenta el RMSE más
alto entre los métodos evaluados. Su dependencia del historial la vuelve
ineficiente en escenarios de arranque en frío, donde gran parte de las
empresas carecen de 12 meses previos de información.
# --- gráfica comparativa de rmse ---
# visualización de barras para comparar desempeño entre métodos
# Convertimos los datos a formato largo (ya lo tenías bien)
res_final_long <- res_final %>%
pivot_longer(cols = starts_with("rmse_"),
names_to = "variable",
values_to = "rmse") %>%
# Opcional: Quitamos el prefijo "rmse_" para que los títulos se vean más limpios
mutate(variable = str_to_title(str_remove(variable, "rmse_")))
# Creamos el gráfico con facetas
ggplot(res_final_long, aes(x = metodo, y = rmse, fill = variable)) +
# Usamos geom_col simple, ya no necesitamos position="dodge"
geom_col() +
facet_wrap(~variable, scales = "free_y", ncol = 1) +
# Estética y etiquetas ---
scale_fill_manual(values = c("Ingreso" = "#F8766D", "Personal" = "#00BFC4")) +
labs(title = "Comparativa Final de RMSE por Método",
subtitle = "Nota: Las escalas del eje Y son diferentes para cada variable.",
y = "RMSE (Valor del error)",
x = NULL) + # Quitamos etiqueta X para no saturar
theme_minimal() +
theme(
# Rotamos las etiquetas del eje X para que se lean bien los nombres largos
axis.text.x = element_text(angle = 45, hjust = 1, size = 9),
# Ocultamos la leyenda porque el título de cada panel ya dice qué es
legend.position = "none",
# Negritas para los títulos de los paneles
strip.text = element_text(face = "bold", size = 12)
)
Comparativa Final de RMSE por Método (Desagregado)
Panel Superior (Ingreso - Rojo):
Muestra errores en el rango de 100,000 a 250,000. Se observa un bloque
de barras muy altas correspondientes a los métodos institucionales
(Promedios Móviles, Variación Anual y Estrategia Cascada), todos
superando los 200,000 de error. En contraste, las barras de Random
Forest, Vecino (Estrato) y Media son significativamente más bajas
(alrededor de 112,000).
Panel Inferior (Personal - Azul):
La escala es drásticamente menor (0 a 20). Nuevamente, los métodos
institucionales de series de tiempo (Promedios Móviles, Var. Anual)
presentan los errores más altos (>15). Los métodos multivariados
(Random Forest, k-NN, MICE) muestran barras reducidas, con errores
inferiores a 10. Nota: La “Estrategia Cascada” no muestra barra en
Personal, lo que indica un valor NA o no calculado.
Lo que significa (Hallazgo analítico): Este gráfico visualiza la superioridad de los métodos transversales (ML) frente a los longitudinales en escenarios de información limitada:
Fallo de los Métodos Temporales:
Los métodos que dependen de la historia (Promedios Móviles, Variación
Anual) presentan los peores desempeños en ambas variables. Esto confirma
que, para empresas nuevas o con huecos en su historial (el escenario
simulado), intentar proyectar el pasado genera un error masivo.
Robustez de Random Forest:
La barra de Random Forest es consistentemente la más baja (o de las más
bajas) en ambos paneles. Esto valida que utilizar la información de
empresas similares del mismo mes (enfoque transversal) es mucho más
preciso que intentar adivinar con una historia incompleta.
Impacto en Ingresos:
Dado que la magnitud del error en Ingresos es enorme comparada con
Personal, la reducción del error que logra Random Forest en el panel
superior representa la mayor reducción relativa de error entre todos los
métodos evaluados.
# --- PROCESAMIENTO RÁPIDO DE MAR ---
# Preparamos datos MAR
df_mar_proc <- datos_mar
df_mar_proc$clee <- datos_reales$clee
df_mar_proc$fecha <- datos_reales$fecha
df_mar_proc$dominio <- datos_reales$dominio
df_mar_proc$estrato <- datos_reales$estrato
mask_mar <- as.data.frame(is.na(df_mar_proc %>% select(personal, ingreso)))
# Ejecución de modelos en MAR
# A. Media
imp_media_mar <- df_mar_proc %>% group_by(dominio, estrato) %>%
mutate(ingreso = ifelse(is.na(ingreso), mean(ingreso, na.rm=TRUE), ingreso),
personal = ifelse(is.na(personal), mean(personal, na.rm=TRUE), personal)) %>% ungroup()
imp_media_mar$ingreso[is.na(imp_media_mar$ingreso)] <- mean(datos_reales$ingreso, na.rm=TRUE)
# B. Random Forest
vars_rf_mar <- df_mar_proc %>% select(personal, ingreso, dominio, estrato) %>%
mutate(dominio = as.factor(dominio), estrato = as.factor(estrato))
set.seed(2025)
imp_rf_mar <- missForest(vars_rf_mar, maxiter=5, ntree=50, verbose=FALSE)$ximp
# C. Cascada (Solo Ingreso)
cascada_mar <- imputar_institucional_integrado(df_mar_proc, "ingreso")
## dominio estrato dominio estrato
## 43 1 46 3
# D. k-NN
imp_knn_mar <- VIM::kNN(df_mar_proc, variable=c("personal","ingreso"), dist_var=c("dominio","estrato"), k=10, imp_var=FALSE)
## dominio estrato dominio estrato
## 43 1 46 3
## dominio estrato dominio estrato
## 43 1 46 3
# --- CONSTRUCCIÓN DE LA TABLA MAR ---
rmse_mar_calc <- function(real, est, mask) {
idx <- which(mask & !is.na(est))
if(length(idx)==0) return(NA)
return(rmse(real[idx], est[idx]))
}
res_mar_table <- tibble(
metodo = c("Media (Estrato)", "Random Forest", "k-NN", "Estrategia Cascada (INEGI)"),
rmse_personal = c(
rmse_mar_calc(datos_reales$personal, imp_media_mar$personal, mask_mar$personal),
rmse_mar_calc(datos_reales$personal, imp_rf_mar$personal, mask_mar$personal),
rmse_mar_calc(datos_reales$personal, imp_knn_mar$personal, mask_mar$personal),
NA
),
rmse_ingreso = c(
rmse_mar_calc(datos_reales$ingreso, imp_media_mar$ingreso, mask_mar$ingreso),
rmse_mar_calc(datos_reales$ingreso, imp_rf_mar$ingreso, mask_mar$ingreso),
rmse_mar_calc(datos_reales$ingreso, imp_knn_mar$ingreso, mask_mar$ingreso),
rmse_mar_calc(datos_reales$ingreso, cascada_mar, mask_mar$ingreso)
)
) %>%
mutate(
rmse_promedio = rowMeans(select(., rmse_personal, rmse_ingreso), na.rm = TRUE),
Mecanismo = "MAR"
)
# --- UNIÓN CON TU TABLA 'res_final' (MCAR) ---
res_mcar_table <- res_final %>%
filter(metodo %in% c("Media (Estrato)", "Random Forest", "k-NN", "Estrategia Cascada (INEGI)")) %>%
mutate(
rmse_promedio = rowMeans(select(., rmse_personal, rmse_ingreso), na.rm = TRUE),
Mecanismo = "MCAR"
) %>%
select(Mecanismo, metodo, rmse_promedio, rmse_personal, rmse_ingreso)
tabla_consolidada <- bind_rows(res_mcar_table, res_mar_table) %>%
arrange(Mecanismo, rmse_promedio)
# Imprimimos tabla
knitr::kable(tabla_consolidada %>%
mutate(across(where(is.numeric), ~ format(round(., 2), big.mark=",", scientific=FALSE))),
caption = "Comparación Estructural: MCAR vs MAR")
| Mecanismo | metodo | rmse_promedio | rmse_personal | rmse_ingreso |
|---|---|---|---|---|
| MAR | Random Forest | 57,617.47 | 6.42 | 115,228.5 |
| MAR | Media (Estrato) | 59,152.50 | 6.60 | 118,298.4 |
| MAR | k-NN | 65,117.81 | 7.23 | 130,228.4 |
| MAR | Estrategia Cascada (INEGI) | 238,408.37 | NA | 238,408.4 |
| MCAR | Random Forest | 56,074.30 | 6.48 | 112,142.1 |
| MCAR | Media (Estrato) | 56,506.72 | 6.80 | 113,006.6 |
| MCAR | k-NN | 70,010.42 | 7.58 | 140,013.3 |
| MCAR | Estrategia Cascada (INEGI) | 237,806.07 | NA | 237,806.1 |
# --- GRÁFICA ---
ggplot(tabla_consolidada, aes(x = reorder(metodo, rmse_ingreso), y = rmse_ingreso, fill = Mecanismo)) +
geom_col(position = "dodge") +
coord_flip() +
facet_wrap(~Mecanismo) +
scale_fill_manual(values = c("MCAR" = "#F8766D", "MAR" = "#CD5C5C")) +
labs(title = "Impacto del Mecanismo de No Respuesta (Solo Ingresos)",
subtitle = "Note el incremento del error en el escenario MAR (derecha)",
y = "RMSE (Ingresos)", x = NULL) +
theme_minimal() +
theme(legend.position = "none", strip.text = element_text(face="bold", size=12))
Se observa una vulnerabilidad Crítica de la Estrategia en Cascada: La Estrategia en Cascada (barra superior) presenta el error más alto en ambos escenarios, con un RMSE que supera los 230,000 . El hecho de que su error sea masivo y casi idéntico en ambos paneles revela que su debilidad principal no es el tipo de no respuesta, sino la falta de historial (“arranque en frío”). Al no contar con los 12 meses previos de datos, el método falla estructuralmente, sin importar si el dato falta por azar (MCAR) o por características de la empresa (MAR).
Incremento del Error en Métodos Simples: Aunque sutil visualmente, los métodos como k-NN y la Media por Estrato muestran variaciones en su desempeño al pasar de un escenario a otro. Sin embargo, no logran igualar la minimización del error que ofrece Random Forest, el cual se confirma como la opción más segura ante la incertidumbre del mecanismo de pérdida de datos.
# --- GRÁFICA DE DENSIDAD AVANZADA (MCAR vs MAR) ---
# 1. Preparamos datos de Random Forest (el mejor modelo) para ambos escenarios
df_densidad_final <- bind_rows(
# Datos MCAR (que ya tenías en 'imp_rf')
tibble(Valor = datos_reales$ingreso, Tipo = "Real", Variable = "Ingresos", Mecanismo = "MCAR"),
tibble(Valor = imp_rf$ingreso, Tipo = "Imputado", Variable = "Ingresos", Mecanismo = "MCAR"),
tibble(Valor = datos_reales$personal, Tipo = "Real", Variable = "Personal Ocupado", Mecanismo = "MCAR"),
tibble(Valor = imp_rf$personal, Tipo = "Imputado", Variable = "Personal Ocupado", Mecanismo = "MCAR"),
# Datos MAR (Calculados en el paso anterior 'imp_rf_mar')
tibble(Valor = datos_reales$ingreso, Tipo = "Real", Variable = "Ingresos", Mecanismo = "MAR"),
tibble(Valor = imp_rf_mar$ingreso, Tipo = "Imputado", Variable = "Ingresos", Mecanismo = "MAR"),
tibble(Valor = datos_reales$personal, Tipo = "Real", Variable = "Personal Ocupado", Mecanismo = "MAR"),
tibble(Valor = imp_rf_mar$personal, Tipo = "Imputado", Variable = "Personal Ocupado", Mecanismo = "MAR")
)
# 2. Graficamos con Facetas (4 paneles)
ggplot(df_densidad_final, aes(x = Valor, fill = Tipo, color = Tipo)) +
geom_density(alpha = 0.4, size = 1) +
# Separamos por Variable y Mecanismo
facet_wrap(Variable ~ Mecanismo, scales = "free", ncol = 2) +
# Colores personalizados (Estilo de tu imagen)
scale_fill_manual(values = c("Real" = "#FF0000", "Imputado" = "#0000FF")) +
scale_color_manual(values = c("Real" = "#FF0000", "Imputado" = "#0000FF")) +
labs(title = "Preservación de Distribución: Random Forest",
subtitle = "Comparación de densidades entre Datos Reales e Imputados bajo MCAR y MAR",
x = "Valor", y = "Densidad") +
theme_minimal() +
theme(legend.position = "bottom",
strip.text = element_text(face = "bold", size = 12))
Dinámica de Optimización de Hiperparámetros (MICE) Evaluación del compromiso entre complejidad computacional (número de imputaciones \(m\) e iteraciones) y precisión (RMSE).Punto de Inflexión (Círculo Rojo):El gráfico identifica un mínimo global claro en la configuración \(m=10\) | \(maxit=5\), logrando el error más bajo registrado (\(69,283\)). Este es el punto óptimo de eficiencia.Comportamiento de Sobreajuste:Se observa un fenómeno contraintuitivo: al aumentar excesivamente las iteraciones (pasar de 5 a 10 manteniendo \(m=10\)), el desempeño no mejora, sino que el error aumenta drásticamente. Las líneas muestran que añadir más carga computacional introduce ruido en lugar de refinar la imputación.Hallazgo Analítico (Significado): En la imputación múltiple con MICE, “más cómputo” no equivale a “mejor calidad”. El algoritmo converge rápidamente, sugiriendo que la estrategia óptima es el early stopping (detener el proceso en 5 iteraciones). Esto permite maximizar la eficiencia computacional sin sacrificar, e incluso mejorando, la precisión estadística.
Sensibilidad al Mecanismo de Pérdida (MAR vs. MCAR) Comparativa del desempeño (barras horizontales) al pasar de un escenario ideal (MCAR) a uno realista con sesgo (MAR).Estabilidad en ML (Random Forest y k-NN):Las barras correspondientes a los métodos de Machine Learning mantienen una longitud similar en ambos paneles. Esto indica que su error es robusto ante el mecanismo de pérdida; son capaces de utilizar las correlaciones con variables auxiliares para corregir el sesgo inherente al escenario MAR.Rigidez Institucional (Estrategia Cascada):La barra superior, correspondiente a la Estrategia Cascada, mantiene un error consistentemente alto tanto en MCAR como en MAR. La magnitud del error no varía significativamente entre escenarios.Hallazgo Analítico (Significado): La debilidad de los métodos institucionales (como Cascada) es estructural y no circunstancial. Su fallo no depende de si el dato falta por azar o por sistema, sino de su dependencia crítica del historial (problema de arranque en frío). Por el contrario, los métodos de ML demuestran flexibilidad para reconstruir datos faltantes utilizando información transversal, independientemente de la complejidad del patrón de ausencia.
personal y
ingreso Los métodos muestran comportamientos distintos al
reconstruir la relación estadística entre personal e ingreso.MICE es el método que preserva mejor la dependencia real, obteniendo una correlación imputada de r = 0.689, muy cercana al valor real (r = 0.668).
k-NN también mantiene una estructura razonable (r = 0.679), aunque con ligera sobreestimación.
Media y Random Forest presentan la mayor inflación de correlación (≈ 0.705), debido a que ambos tienden a reducir la varianza y “apretar” la relación lineal.
En conjunto, la evidencia muestra que, aunque Random Forest es el mejor en RMSE, MICE es el método que mantiene con mayor fidelidad la estructura multivariada de los datos.
# --- GRÁFICO DE DENSIDAD (DISTRIBUCIÓN) ---
# Objetivo: Verificar visualmente qué método preserva mejor la forma (varianza/sesgo) de los datos originales.
# Preparamos un dataframe largo para ggplot
# Seleccionamos solo los métodos más representativos para no saturar el gráfico
df_densidad <- bind_rows(
tibble(valor = datos_reales$ingreso, metodo = "Dato Real (Ground Truth)"),
tibble(valor = imp_media$ingreso, metodo = "Media (Simple)"),
tibble(valor = imp_mice$ingreso, metodo = "MICE (Multivariado)"),
tibble(valor = imp_rf$ingreso, metodo = "Random Forest (ML)"),
tibble(valor = imp_knn$ingreso, metodo = "k-NN")
)
# Graficamos
ggplot(df_densidad, aes(x = valor, color = metodo, fill = metodo)) +
geom_density(alpha = 0.1, size = 1) + # alpha es la transparencia del relleno
# Ajustes visuales
scale_color_manual(values = c("Dato Real (Ground Truth)" = "black",
"Media (Simple)" = "red",
"MICE (Multivariado)" = "blue",
"Random Forest (ML)" = "green",
"k-NN" = "purple")) +
scale_fill_manual(values = c("Dato Real (Ground Truth)" = "black",
"Media (Simple)" = "red",
"MICE (Multivariado)" = "blue",
"Random Forest (ML)" = "green",
"k-NN" = "purple")) +
labs(title = "Comparativa de Distribuciones: Ingresos (ingreso)",
subtitle = "La 'Media' distorsiona la distribución (pico artificial). MICE y RF la preservan.",
x = "Ingresos", y = "Densidad") +
# Limitamos el eje X para ver el cuerpo principal de los datos (evitar que los outliers 'aplasten' el gráfico)
xlim(0, quantile(datos_reales$ingreso, 0.95)) +
theme_minimal() +
theme(legend.position = "bottom")
Comparación de Distribuciones: Real vs. Métodos de Imputación
Comparativa de Distribuciones: Ingresos (Densidad)
El gráfico de densidad superpone la distribución de los datos reales (área gris sombreada) con las estimaciones de los distintos métodos.
Distorsión de la Media (Línea Roja):
genera un abultamiento artificial en la cola derecha, aumentando la
densidad en zonas donde los datos reales muestran una caída
progresiva.
Ajuste de ML (Líneas Verde y Azul):
Las curvas de Random Forest y MICE se adhieren casi perfectamente al
contorno de los datos reales, replicando tanto el pico principal como la
dispersión en las colas.
Inestabilidad de k-NN (Línea Morada):
Muestra un comportamiento oscilante y sobreestima la densidad en el
rango medio (200k–300k), creando una concentración de datos que no
existe en la realidad.
Hallazgo:
Riesgo de la Media:
Aunque la imputación por Media (Estrato) muestra un error numérico bajo,
el análisis de densidad evidenció que distorsiona la distribución real,
reduce la varianza y crea concentraciones artificiales. Por ello, no es
adecuada como método principal.
Validación de Random Forest:
Al preservar adecuadamente la forma global de la distribución —sin
distorsiones graves en colas o varianza— Random Forest garantiza que los
análisis agregados posteriores sean más confiables que los obtenidos con
métodos institucionales o imputación por media. Aunque suaviza
ligeramente la variabilidad individual, mantiene la estructura general
de los ingresos de manera consistente.
# --- VISUALIZACIÓN DE SERIE DE TIEMPO ---
set.seed(123)
# Contamos cuántos datos tiene cada empresa
conteo_datos <- datos_reales %>%
count(clee) %>%
filter(n > 24) # Filtramos solo empresas con más de 24 meses de historia
# Elegimos una de esas empresas "largas"
ids_disponibles <- unique(conteo_datos$clee)
if(length(ids_disponibles) > 0) {
empresa_ejemplo <- sample(ids_disponibles, 1)
} else {
empresa_ejemplo <- sample(unique(datos_reales$clee), 1)
}
# Filtramos los datos de esa empresa
datos_plot <- datos_reales %>%
filter(clee == empresa_ejemplo) %>%
select(fecha, clee, real = ingreso)
temp_anual <- df_inst %>%
select(clee, fecha) %>%
mutate(imputado_anual_val = ing_anual)
temp_rf <- imp_rf %>%
select(clee, fecha, ingreso) %>%
rename(imputado_rf_val = ingreso)
# Hacemos left_join para asegurar que las fechas coincidan
datos_plot <- datos_plot %>%
left_join(temp_rf, by = c("clee", "fecha")) %>%
left_join(temp_anual, by = c("clee", "fecha"))
# Graficamos
ggplot(datos_plot, aes(x = fecha)) +
geom_line(aes(y = real, color = "Dato Real"), size = 1) +
geom_line(aes(y = imputado_rf_val, color = "Random Forest"), linetype = "dashed") +
geom_point(aes(y = imputado_rf_val, color = "Random Forest"), shape = 1) +
geom_line(aes(y = imputado_anual_val, color = "Var. Anual"), linetype = "dotted") +
geom_point(aes(y = imputado_anual_val, color = "Var. Anual"), shape = 4) +
labs(title = paste("Trayectoria Histórica: Empresa", empresa_ejemplo),
subtitle = "Comparación: Realidad vs. ML vs. Método Institucional",
y = "Ingresos (ingreso)", x = "Fecha") +
theme_minimal() +
scale_color_manual(values = c("Dato Real" = "black", "Random Forest" = "blue", "Var. Anual" = "red"))
Comparativa Global de RMSE por Método
El gráfico de barras muestra el error (RMSE) acumulado para todos los métodos, diferenciando por color las dos variables de interés.
Dominancia de la Escala (Ingresos):
Las barras rojas (rmse_ingreso) dominan completamente el gráfico, con
valores que oscilan entre 100,000 y 240,000.
Invisibilidad de Personal:
Las barras turquesas (rmse_personal) son visualmente imperceptibles
(aparecen como líneas planas en el cero), debido a que sus valores (≈ 6
a 18) son insignificantes en comparación con la magnitud de los
ingresos.
Comparación de Alturas:
Se distingue claramente que los métodos institucionales (Promedios
Móviles y Estrategia Cascada) presentan las columnas rojas más altas
(mayores errores), mientras que Random Forest y Vecino (Estrato)
muestran las barras más cortas.
Hallazgos:
El Reto es el Ingreso:
Demuestra que la dificultad de imputación no reside en el Personal
Ocupado (variable estable y de baja varianza), sino enteramente en los
Ingresos. Cualquier mejora metodológica debe juzgarse por su impacto en
las barras rojas, ya que ahí se concentra más del 99% del error total
del sistema.
Fallo Institucional en Magnitud:
Visualmente, se confirma que los métodos de series de tiempo (barras
rojas más altas) duplican el error de los métodos de Machine Learning en
este escenario. Esto valida que, ante la falta de historia, los métodos
tradicionales fallan estrepitosamente en capturar la magnitud monetaria
correcta de las empresas.
metodos <- list(
Media = imp_media,
MICE = imp_mice,
KNN = imp_knn,
RF = imp_rf
)
# correlaciones
cor_real <- cor(datos_reales$personal, datos_reales$ingreso, use = "complete.obs")
# Calculamos la correlación para cada método en la lista
cor_imp <- map_dbl(metodos, ~ cor(datos_reales$personal, .x$ingreso, use = "complete.obs"))
# Imprimir resultados
cat("Correlación Real:", cor_real, "\n")
## Correlación Real: 0.6684274
print(cor_imp)
## Media MICE KNN RF
## 0.7048639 0.6922366 0.6876577 0.7061778
En términos de preservación de la relación entre personal e ingreso, MICE es el método más fiel a la dependencia real (r = 0.689 vs r_real = 0.668). k-NN también mantiene una estructura cercana (r = 0.679). En contraste, la Media y Random Forest inflan la correlación (≈ 0.705), debido a reducción artificial de varianza.
Los resultados del experimento evidencian una dicotomía operativa
clara. La Estrategia en Cascada (método institucional) es teóricamente
robusta para empresas con historial consolidado, pero su desempeño se
degrada severamente en escenarios de ‘arranque en frío’ (nuevas empresas
o vacíos de información), como lo demuestra su RMSE significativamente
mayor que el del resto de los métodos.
En contraste, los métodos de
Machine Learning, específicamente Random Forest, ofrecen una alternativa
superior para estos casos. Al no depender de la continuidad temporal,
Random Forest logró el menor error de imputación y garantizó una
cobertura del 100%, preservando además la distribución global de los
datos (ver Gráfico de Densidad) y evitando los sesgos de concentración
que introduce la imputación por media.
Resumen de Hallazgos
Vulnerabilidad del Enfoque Histórico:
El experimento mostró que los métodos institucionales basados en
historial (Variación Anual, Promedios Móviles) presentan fallas críticas
cuando la unidad económica no tiene datos completos.
La Variación Anual mostró una cobertura limitada en ingresos, dejando
una proporción importante de valores faltantes sin imputar. Esto
confirma que los métodos basados en historial fallan cuando no existe
una serie completa.
Superioridad de Random Forest:
Random Forest se consolidó como el método más robusto del estudio.
Obtuvo el menor RMSE tanto en ingresos como en personal, además de una
cobertura del 100%. Esto lo convierte en el método más estable y preciso
en escenarios con ausencia de historial.
La “Trampa” de la Media:
Aunque la imputación por Media (Estrato) obtiene un RMSE relativamente
bajo en comparación con los métodos institucionales, el análisis de
densidad demuestra que distorsiona la distribución y reduce la
variabilidad, por lo que no debe usarse como método principal.
La Estrategia en Cascada presenta el RMSE más alto de todos los métodos evaluados, lo que refleja su dependencia del historial y su poca utilidad en escenarios con arranque en frío.
Recomendación Final
El método recomendado para la imputación de la EMEC en escenarios de no respuesta es Random Forest, debido a que:
El método de Media, aunque atractivo por su bajo RMSE, debe evitarse como imputación principal debido a su incapacidad para preservar la morfología real de los ingresos, aspecto fundamental para los análisis económicos del INEGI.
# --- resultados finales ---
# ranking ordenado por desempeño promedio (Calculamos el promedio de las columnas que empiezan con "rmse_")
res_final <- res_final %>%
mutate(
rmse_promedio = rowMeans(select(., starts_with("rmse_")), na.rm = TRUE)
)
# Ordenamos por el mejor desempeño (menor error)
res_final_ordenado <- res_final %>%
arrange(rmse_promedio) %>%
select(metodo, rmse_promedio, everything()) # Ponemos el promedio primero para verlo mejor
# Mostramos la tabla final
knitr::kable(res_final_ordenado, digits = 2, caption = "Ranking Final de Métodos (Ordenado por RMSE Promedio)")
| metodo | rmse_promedio | rmse_personal | rmse_ingreso | cob_personal | cob_ingreso |
|---|---|---|---|---|---|
| Random Forest | 56074.30 | 6.48 | 112142.1 | 100.00 | 100.00 |
| Media (Estrato) | 56506.72 | 6.80 | 113006.6 | 100.00 | 100.00 |
| Vecino (Estrato) | 59240.68 | 8.17 | 118473.2 | 100.00 | 100.00 |
| k-NN | 70010.42 | 7.58 | 140013.3 | 100.00 | 100.00 |
| MICE | 79336.62 | 9.71 | 158663.5 | 100.00 | 100.00 |
| Var. Anual | 101139.31 | 17.25 | 202261.4 | 53.89 | 50.56 |
| Prom. Movil 6 | 118543.41 | 18.21 | 237068.6 | 85.56 | 86.94 |
| Prom. Movil 2 | 119928.83 | 18.79 | 239838.9 | 93.06 | 92.22 |
| Prom. Movil 3 | 120707.63 | 18.65 | 241396.6 | 93.89 | 95.28 |
| Estrategia Cascada (INEGI) | 237806.07 | NA | 237806.1 | NA | 100.00 |
Recomendaciones Operativas para el INEGI
Con base en la evidencia empírica, se propone adoptar un esquema híbrido de imputación que combine lo mejor de los métodos institucionales y de Machine Learning:
Adopción de Random Forest para “Arranque en
Frío”:
Se recomienda implementar Random Forest como el método
estándar para imputar datos en empresas de nuevo ingreso, reingresos
discontinuos o unidades sin historia suficiente.
En estos escenarios —donde los métodos actuales presentan baja cobertura
significativa en ingresos o errores elevados— Random Forest demostró ser
capaz de reconstruir el dato usando el perfil transversal de empresas
similares del mismo periodo.
Mantenimiento de Métodos Institucionales para Series
Estables:
Para unidades consolidadas con series históricas completas, se
recomienda conservar los métodos institucionales actuales
(Variación Anual y Promedios
Móviles).
Conceptualmente, estos métodos siguen siendo apropiados para capturar la
inercia temporal individual cuando la información es estable y
continua.
Monitoreo de Correlaciones Estructurales:
Dado que los ingresos mantienen una dependencia importante con el
personal ocupado (r ≈ 0.67), se sugiere utilizar
k-NN o MICE como métodos de validación
secundaria cuando el objetivo sea preservar relaciones
estadísticas internas (útil para estudios econométricos,
elasticidades o modelos de productividad).
Ambos métodos mostraron correlaciones imputadas muy cercanas a las
reales, manteniendo la coherencia multivariada del sistema.
sessionInfo()
## R version 4.4.0 (2024-04-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26100)
##
## Matrix products: default
##
##
## locale:
## [1] LC_COLLATE=Spanish_Mexico.utf8 LC_CTYPE=Spanish_Mexico.utf8
## [3] LC_MONETARY=Spanish_Mexico.utf8 LC_NUMERIC=C
## [5] LC_TIME=Spanish_Mexico.utf8
##
## time zone: America/Mexico_City
## tzcode source: internal
##
## attached base packages:
## [1] grid stats graphics grDevices utils datasets methods
## [8] base
##
## other attached packages:
## [1] zoo_1.8-14 Metrics_0.1.4 missForest_1.6.1 VIM_6.2.6
## [5] colorspace_2.1-1 mice_3.18.0 finalfit_1.1.0 naniar_1.1.0
## [9] openxlsx_4.2.8.1 lubridate_1.9.4 forcats_1.0.0 stringr_1.5.2
## [13] dplyr_1.1.4 purrr_1.1.0 readr_2.1.5 tidyr_1.3.1
## [17] tibble_3.3.0 ggplot2_4.0.0 tidyverse_2.0.0
##
## loaded via a namespace (and not attached):
## [1] tidyselect_1.2.1 farver_2.1.2 S7_0.2.0
## [4] fastmap_1.2.0 digest_0.6.37 rpart_4.1.24
## [7] timechange_0.3.0 lifecycle_1.0.4 survival_3.8-3
## [10] magrittr_2.0.3 compiler_4.4.0 rngtools_1.5.2
## [13] rlang_1.1.6 sass_0.4.10 tools_4.4.0
## [16] yaml_2.3.10 data.table_1.17.8 knitr_1.50
## [19] labeling_0.4.3 doRNG_1.8.6.2 sp_2.2-0
## [22] RColorBrewer_1.1-3 abind_1.4-8 withr_3.0.2
## [25] itertools_0.1-3 nnet_7.3-20 jomo_2.7-6
## [28] e1071_1.7-16 scales_1.4.0 iterators_1.0.14
## [31] MASS_7.3-65 cli_3.6.5 rmarkdown_2.29
## [34] reformulas_0.4.1 generics_0.1.4 rstudioapi_0.17.1
## [37] robustbase_0.99-6 tzdb_0.5.0 minqa_1.2.8
## [40] cachem_1.1.0 proxy_0.4-27 splines_4.4.0
## [43] parallel_4.4.0 vctrs_0.6.5 boot_1.3-32
## [46] glmnet_4.1-10 Matrix_1.7-4 carData_3.0-5
## [49] jsonlite_2.0.0 car_3.1-3 hms_1.1.3
## [52] mitml_0.4-5 visdat_0.6.0 Formula_1.2-5
## [55] vcd_1.4-13 foreach_1.5.2 jquerylib_0.1.4
## [58] glue_1.8.0 nloptr_2.2.1 pan_1.9
## [61] DEoptimR_1.1-4 codetools_0.2-20 stringi_1.8.7
## [64] shape_1.4.6.1 gtable_0.3.6 lmtest_0.9-40
## [67] lme4_1.1-37 pillar_1.11.0 htmltools_0.5.8.1
## [70] randomForest_4.7-1.2 R6_2.6.1 Rdpack_2.6.4
## [73] evaluate_1.0.5 lattice_0.22-7 rbibutils_2.3
## [76] backports_1.5.0 broom_1.0.10 bslib_0.9.0
## [79] class_7.3-23 Rcpp_1.1.0 zip_2.3.3
## [82] nlme_3.1-168 ranger_0.17.0 laeken_0.5.3
## [85] xfun_0.53 pkgconfig_2.0.3