Note on the synthetic sample. This rendered version of the report runs against
data/sample.csv, a 540-row synthetic sample (60 merchants × 9 weeks). The original 245,000-row commercial dataset is not redistributed for licensing reasons — seedata/README.md. The synthetic sample preserves the marginal distributions ofgenero,rubroandcuarentena1and approximately reproduces the headline gap, so the qualitative findings hold, but coefficient magnitudes and any model that needs many degrees of freedom (large fixed-effect blocks, ML hyperparameter grids) will be noisier than on the full dataset.
Carga de librerías y lectura del dataset (muestra sintética de 540 filas).
#Preliminares
rm(list=ls()) # Limpia la lista de objetos
graphics.off() # Limpia la lista de gráficos
options(digits = 5) # Número de dígitos a utilizar#cargamos las librerias necesarias
library(readr) #Para leer el .csv
library(glmnet) #Ajusta modelo lineal
library(ggplot2) #Para realización de gráficos
library(ggcorrplot) #Para realizar grafico correlacion
library(fixest) #Para correr modelos con efectos fijos
library(tidyverse) #Funcionalidades para manipular y desplegar datos
library(kableExtra) # Para formatear tablas
library(modelsummary) # Para resumir resultados del modelo
library(caret) # for most ML models
library(earth) # for MARS
library(randomForest)# for random Forest.Lectura de la base de datos:
Verificación missing values
## idm zona semana año
## FALSE FALSE FALSE FALSE
## semana_año edad genero ingreso
## FALSE FALSE FALSE FALSE
## dias_trabajados antiguedad rubro transacciones
## FALSE FALSE FALSE FALSE
## hrs_trabajadas cuarentena1 hrs_diarias transacciones_hr
## FALSE FALSE FALSE FALSE
## ingreso_hr
## FALSE
Sin valores faltantes — podemos continuar.
Preprocesamiento y limpieza:
#Reemplazamos comas por puntos y se convierten en carácter númerico
df$ingreso_hr <- as.numeric(gsub(",", ".", df$ingreso_hr))
df$transacciones_hr <- as.numeric(gsub(",", ".", df$transacciones_hr))
df$hrs_diarias <- as.numeric(gsub(",", ".", df$hrs_diarias))
df$semana <- as.factor(df$semana)
#Eliminamos columnas que no serán de utilidad para el análisis, ya que no aportan información importante en el caso de idm, y en el caso de semana_año por que esa info está en otras variables
df <- subset(df, select = -idm)
df <- subset(df, select = -semana_año)
#Sacamos del df las filas con el error de tener 8 días a la semana trabajados
df <- df[df$dias_trabajados != 8, ]
# Crear intervalos de edades, que nos servirá para graficar y en distintos análisis
df$edades_intervalos <- cut(df$edad, breaks = c(17, 30, 40, 50, 60, 69, Inf),
labels = c("18-30", "31-40", "41-50", "51-60", "61-70", "71+"))
#Creamos Log Ingreso para facilitar el análisis de la variable ingreso como variable dependiente de los modelos
df$logingreso <- log(df$ingreso)
#Para crear un análisis con menor ruido, eliminaremos de la base las filas con ingresos muy extremos, definiendo una ventana entre los 1.800 y 7.000.000 semanales.
df <- subset(df, ingreso >= 1800 &ingreso <= 7000000)
# Creamos variables dummy a partir de la columna rubro, que nos serán de utilidad para utlizar en los modelos
dummies <- model.matrix(~ rubro - 1, data = df)
# Cambiamos los nombres de las columnas de las variables dummy
colnames(dummies) <- c("r_alim", "r_co", "r_ofi", "r_ret", "r_vest") # Reemplaza con los nombres deseados
# Agregamos las variables dummy al data frame original
df <- cbind(df, dummies)
#Se borra una de las dummies para que no exista colinealidad y tenagamos el caso base
df <- subset(df, select = -r_ofi)Exploramos cómo se comportan los ingresos de los comerciantes según género e identificamos las variables observables que más plausiblemente median la brecha (horas trabajadas, rubro, zona, edad).
Exploramos las variables que plausiblemente explican la variación de ingresos entre comerciantes de distinto género.
#Se realiza un summary para obtener estadísticas generales de las variables en la base de datos, teniendo así una exploración inicial del comportamiento de estas.
summary(df)## zona semana año edad genero
## Length:540 29 : 18 Min. :2020 Min. :22.0 Min. :0.000
## Class :character 27 : 16 1st Qu.:2020 1st Qu.:31.8 1st Qu.:0.000
## Mode :character 34 : 15 Median :2021 Median :41.5 Median :1.000
## 6 : 14 Mean :2021 Mean :42.0 Mean :0.583
## 7 : 14 3rd Qu.:2021 3rd Qu.:52.0 3rd Qu.:1.000
## 11 : 14 Max. :2021 Max. :62.0 Max. :1.000
## (Other):449
## ingreso dias_trabajados antiguedad rubro
## Min. : 20548 Min. :3.00 Min. :0.00 Length:540
## 1st Qu.: 86228 1st Qu.:4.00 1st Qu.:0.75 Class :character
## Median :125606 Median :5.00 Median :1.00 Mode :character
## Mean :152601 Mean :4.52 Mean :1.15
## 3rd Qu.:194801 3rd Qu.:5.00 3rd Qu.:2.00
## Max. :546343 Max. :6.00 Max. :3.00
##
## transacciones hrs_trabajadas cuarentena1 hrs_diarias transacciones_hr
## Min. : 2.0 Min. : 6.1 Min. :0.0000 Min. :2.01 Min. :0.180
## 1st Qu.: 8.0 1st Qu.:13.8 1st Qu.:0.0000 1st Qu.:3.25 1st Qu.:0.510
## Median :12.0 Median :18.4 Median :0.0000 Median :4.28 Median :0.687
## Mean :15.2 Mean :19.4 Mean :0.0889 Mean :4.29 Mean :0.775
## 3rd Qu.:19.0 3rd Qu.:24.7 3rd Qu.:0.0000 3rd Qu.:5.39 3rd Qu.:0.961
## Max. :61.0 Max. :38.9 Max. :1.0000 Max. :6.49 Max. :2.652
##
## ingreso_hr edades_intervalos logingreso r_alim
## Min. : 2408 18-30:126 Min. : 9.93 Min. :0.000
## 1st Qu.: 5409 31-40:126 1st Qu.:11.36 1st Qu.:0.000
## Median : 7140 41-50:117 Median :11.74 Median :0.000
## Mean : 7814 51-60:144 Mean :11.77 Mean :0.417
## 3rd Qu.: 9501 61-70: 27 3rd Qu.:12.18 3rd Qu.:1.000
## Max. :25142 71+ : 0 Max. :13.21 Max. :1.000
##
## r_co r_ret r_vest
## Min. :0.000 Min. :0.000 Min. :0.000
## 1st Qu.:0.000 1st Qu.:0.000 1st Qu.:0.000
## Median :0.000 Median :0.000 Median :0.000
## Mean :0.167 Mean :0.167 Mean :0.117
## 3rd Qu.:0.000 3rd Qu.:0.000 3rd Qu.:0.000
## Max. :1.000 Max. :1.000 Max. :1.000
##
La muestra abarca comerciantes de 18 a 80 años (promedio 44.9), con ingresos semanales promedio de $278.733.
Distribución demográfica de los comerciantes:
# contamos la cantidad de cada género y luego los metemos a un dataframe
conteo_genero <- table(df$genero)
datos_genero <- data.frame(Genero = factor(names(conteo_genero), labels = c("Hombre", "Mujer")), Cantidad = as.vector(conteo_genero))
# Creamos gráfico de torta
ggplot(datos_genero, aes(x = "", y = Cantidad, fill = Genero, label = Cantidad)) +
geom_bar(stat = "identity", width = 1) +
geom_text(aes(label = Cantidad), position = position_stack(vjust = 0.5), color = "white", size = 5) +
coord_polar(theta = "y") + # Convertir el gráfico en uno de torta
labs(title = "Distribución de género") +
scale_fill_manual(values = c("Hombre" = "blue", "Mujer" = "pink")) +
theme_void() # Repetimos procedimiento para visualizar la cantidad por cada intervalo de edad
tabla_genero_edad <- table(df$edades_intervalos, df$genero)
df_tabla <- as.data.frame(tabla_genero_edad)
colnames(df_tabla) <- c("edades_intervalos", "genero", "cantidad")
# creamos el gráfico
ggplot(df_tabla, aes(x = edades_intervalos, y = cantidad, fill = factor(genero))) +
geom_bar(stat = "identity", position = "dodge") +
geom_text(aes(label = cantidad), position = position_dodge(width = 0.9), vjust = -0.5) +
labs(title = "Distribución de género por intervalo de edad",
x = "Intervalo de Edad",
y = "Cantidad de Personas") +
scale_fill_manual(values = c("0" = "blue", "1" = "pink"),
breaks = c("0", "1"),
labels = c("Hombre", "Mujer")) +
theme_minimal()#Se hace lo mismo para verlo por rubro
tabla_rubro_genero <- table(df$rubro, df$genero)
df_tabla2 <- as.data.frame(tabla_rubro_genero)
colnames(df_tabla2) <- c("rubro", "genero", "cantidad")
#se crea el gráfico
ggplot(df_tabla2, aes(x = rubro, y = cantidad, fill = factor(genero))) +
geom_bar(stat = "identity", position = "dodge") +
geom_text(aes(label = cantidad), position = position_dodge(width = 0.9), vjust = -0.5) +
labs(title = "Distribución de género por rubro",
x = "Rubro",
y = "Cantidad de Personas") +
scale_fill_manual(values = c("0" = "blue", "1" = "pink"),
breaks = c("0", "1"),
labels = c("Hombre", "Mujer")) +
theme_minimal()En primer lugar, vale la pena notar que de las 245.857 personas, el 58.7% corresponde a mujeres. En cuanto a la composición etaria, a excepción del intervalo de 71 años o más, las mujeres predominan en cantidad en cada uno de ellos. El grueso de la data se encuentra entre los 30 y 60 años, siendo el intervalo entre los 31 y 40 años el que mayor cantidad de personas tiene.
Por último, la cantidad de datos que se tiene en cada rubro está desbalanceado. El comercio de Alimentos y Abarrotes predomina por mucho,luego lo sigue el del Retail y se destaca que para Vestuario, la cantidad de datos que se tienen es muy pequeña en relación a los demás rubros, además sucede algo particular como es que la cantidad de mujeres en este triplica a la cantidad de hombres.
Matriz de correlación
Se realiza esta matriz para observar como correlacionan las variables
# creamos gráfico de matriz de correlación
columnas_numericas <- df[, sapply(df, is.numeric)]
matriz_correlacion <- cor(columnas_numericas)
ggcorrplot(matriz_correlacion, type = "lower", lab = TRUE, lab_size = 2)Se observa que hay variables de las que se esperaba que tuviesen una gran correlación positiva entre sí, como por ejemplo transacciones semanales con transacciones por hora y horas trabajadas a la semana con horas trabajadas por día. Por otro lado, se destacan las variables que tienen gran correlación con el ingreso, ya que pueden servir como un proxy para explicar por qué varían y su aporte a la brecha de género. Estas variables son el número de transacciones (semanales y por hora), y las horas trabajadas (por semana y por día).
Se realiza un gráfico de densidad de los ingresos tanto para hombres como para mujeres.
# creamos media para incluirla en la gráfica
media_hombres <- mean(df$logingreso[df$genero == 0], na.rm = TRUE)
media_mujeres <- mean(df$logingreso[df$genero == 1], na.rm = TRUE)
#se crea la gráfica
ggplot(df, aes(x = logingreso, fill = factor(genero))) +
geom_density(alpha = 0.5) +
labs(title = "Densidad de distribución de los ingresos(log) por género",x = "Log(Ingresos)", y = "Densidad") +
scale_fill_discrete(name = "Género", labels = c("Hombre" , "Mujer" )) +
theme_minimal() +
theme(legend.position = "top") +
geom_vline(xintercept = media_hombres, color = "red", linetype = "dashed", size = 1) +
geom_vline(xintercept = media_mujeres, color = "blue", linetype = "dashed", size = 1)Este gráfico permite observar la brecha de género sobre los ingresos a grandes rasgos. Se observa que la curva de distribución de ingresos de los hombres está corrida a la derecha en relación a la de las mujeres, al igual que la media de la distribución representada por la línea punteada en color rojo.
Se verán los ingresos promedio por cada rango de edad y por ambos géneros, para verificar su comportamiento y estudiar el aporte que puede significar esta variable a la brecha de género.
# Se crea el gráfico señalado
ggplot(df, aes(x = edades_intervalos, y = ingreso, fill = factor(genero))) +
geom_bar(stat = "summary", fun = "mean", position = "dodge") +
labs(title = "Ingreso promedio por intervalo de edad por género",x = "Rango de Edad", y = "Ingresos promedio") +
scale_fill_manual(values = c("0" = "blue", "1" = "pink"),
breaks = c("0", "1"),
labels = c("Hombre", "Mujer")) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
scale_y_continuous(labels = scales::comma_format(scale = 1))Esta grafica es interesante para notar que la edad podría ser una variable interesante para explicar la diferencia en los ingresos entre géneros. Por un lado, es claro que la edad parece importar más para los hombres a la hora de explicar los ingresos, esto porque las variaciones entre intrevalos de edad es considerable. Los hombres presentan una alza en su ingreso promedio hasta el rango de los 41-50 en donde supera los 300.000 semanal, para luego bajar consecutivamente hasta los 71 o más años. Por otro lado, el rango de ingreso promedio de las mujeres se mantiene relativamente constante a lo largo de los intervalos de edad. Con esto, se puede observar que entre los intervalos, la brecha de género en los ingresos no es la misma, por lo que controlar por esta variable puede ser interesante para explicar el por qué.
Ingreso promedio por rubro y género — para evaluar si el tipo de negocio media la brecha:
#Se crea el gráfico
ggplot(df, aes(x = rubro, y = ingreso, fill = factor(genero))) +
geom_bar(stat = "summary", fun = "mean", position = "dodge") +
labs(title = "Ingreso promedio por rubro para cada género",x = "Rubro", y = "Ingresos promedio") +
scale_fill_manual(values = c("0" = "blue", "1" = "pink"),
breaks = c("0", "1"),
labels = c("Hombre", "Mujer")) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
scale_y_continuous(labels = scales::comma_format(scale = 1))En todos los rubros el ingreso promedio masculino supera al femenino. La brecha es más pronunciada en vestuario y más moderada en oficios y otros servicios, lo que sugiere que el rubro media parcialmente la diferencia de ingresos.
Ingresos por intervalo de horas trabajadas:
#Se crean los intervalos de horas trabajadas para visualizar lo señalado
df$hrs_intervalo <- cut(df$hrs_trabajadas, breaks = c(0, 20, 40, 60, Inf), labels = c("0-20", "21-40", "41-60", "61+"))
#se crea este gráfico
ggplot(df, aes(x = hrs_intervalo, y = logingreso, fill = factor(genero))) + geom_boxplot() +
scale_fill_manual(values = c("0" = "blue", "1" = "pink"),
breaks = c("0", "1"),
labels = c("Hombre", "Mujer"))+
labs(title = "Ingreso promedio (log) por intervalo de horas trabajadas por género",x = "Horas Trabajadas", y = "Ingreso Promedio(log)", fill = "Género") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))Los ingresos crecen con las horas trabajadas, y la brecha hombre-mujer se mantiene relativamente constante a lo largo de los intervalos. Esto indica que las horas por sí solas no explican la diferencia — el gap persiste a igual nivel de dedicación.
Relación entre horas trabajadas e ingreso por hora — proxy de productividad por género:
# Se colapsa la base para tener un promedio de ingreso por horas trabajadas y género
ingreso_hr_promedio_por_horas <- df %>%
group_by(hrs_trabajadas, genero) %>%
summarize(ingreso_hr_promedio = mean(ingreso_hr))
# Se crea el gráfico
ggplot(ingreso_hr_promedio_por_horas, aes(x = hrs_trabajadas, y = ingreso_hr_promedio, color = factor(genero))) +
geom_point(size = 1, alpha = 0.7) +
geom_smooth(method = "lm", se = FALSE) +
labs(title = "Ingreso/hora por horas trabajadas",
x = "Horas Trabajadas",
y = "Ingreso/Hora Promedio") +
scale_color_manual(values = c("0" = "blue", "1" = "pink"),
breaks = c("0", "1"),
labels = c("Hombre", "Mujer")) +
theme_minimal()A igual número de horas, las mujeres generan menos ingreso por hora. La pendiente de su recta de regresión es menor, lo que apunta a una brecha de productividad-hora que persiste incluso controlando por dedicación.
La zona geográfica puede mediar la brecha tanto por diferencias de nivel de ingreso entre zonas como por variación en la magnitud del gap. Mostramos una selección de zonas para ilustrar:
# Se saca la muestra de zonas a visualizar y se agrupan en un data frama
zonas_interes <- c("zona 1", "zona 8", "zona 15", "zona 20", "zona 33", "zona 40", "zona 45", "zona 49", "zona 52")
df_zona <- df[df$zona %in% zonas_interes, ]
# Se colapsa la base para sacar los promedios de ingreso por zona y genero
ingresos_promedio_zona <- df_zona %>%
group_by(zona, genero) %>%
summarize(ingreso_promedio = mean(ingreso))
# Se crea el gráfico
ggplot(ingresos_promedio_zona, aes(x = factor(genero), y = ingreso_promedio, fill = factor(genero))) +
geom_bar(stat = "identity") +
labs(title = "Ingresos promedio por género en distintas zonas",
x = "Género",
y = "Ingreso Promedio") +
scale_fill_manual(values = c("0" = "blue", "1" = "pink"),
breaks = c("0", "1"),
labels = c("Hombre", "Mujer")) +
facet_wrap(~zona) +
scale_y_continuous(labels = scales::comma_format(scale = 1))+
theme_minimal()La zona importa en dos sentidos: hay zonas con ingresos promedio mucho mayores que otras, y la magnitud de la brecha varía entre zonas (por ejemplo, zonas 1 y 52 muestran brechas marcadas). Capturar el efecto fijo de zona permite aislar mejor el efecto puro del género.
Evolución temporal de los ingresos por semana del año:
#Se colapsa la base originial para sacar promedio de ingreso por cada semana y genero
promedios_semana_genero <- df %>%
group_by(semana, genero) %>%
summarize(ingreso_promedio = mean(ingreso))
# Se proceso la variable semana a numerica para graficarla
promedios_semana_genero$semana <- as.numeric(promedios_semana_genero$semana)
# Se crea el gráfico
ggplot(promedios_semana_genero, aes(x = semana, y = ingreso_promedio, color = factor(genero))) +
geom_line() +
labs(title = "Ingreso promedio por semana",
x = "Semana del Año",
y = "Ingreso Promedio") +
scale_x_continuous(breaks = seq(1,52, by = 3)) +
scale_color_manual(values = c("0" = "blue", "1" = "pink"),
breaks = c("0","1"),
labels = c("Hombre", "Mujer")) +
theme_minimal()Hay estacionalidad clara (pico en fin de año), pero ambas curvas se mueven en paralelo — la brecha de género no varía con la semana. La variable temporal explica nivel de ingresos pero no la brecha.
Efecto de la cuarentena (2020-2021) sobre los ingresos por género:
#Se saca la data de los años donde hubo cuarentena
df_2020_2021 <- df[(df$año ==2020 | df$año==2021), ]
# Se crea el gráfico
ggplot(df_2020_2021, aes(x = factor(cuarentena1), y = ingreso, fill = factor(genero))) +
geom_bar(stat = "summary", fun = "mean", position = "dodge") +
labs(title = "Ingresos promedio con/sin cuarentena 2020-2021",
x = "Cuarentena",
y = "Ingreso Promedio") +
scale_fill_manual(values = c("0" = "blue", "1" = "red"),
breaks = c("0", "1"),
labels = c("Hombre", "Mujer")) +
scale_y_continuous(labels = scales::comma_format(scale = 1))+
theme_minimal()Dato contra-intuitivo: en 2020-2021 los ingresos promedio fueron mayores en semanas con cuarentena que sin ella.
Sin embargo, el comportamiento es simétrico entre géneros: la cuarentena no amplió ni redujo la brecha.
Ingreso promedio por número de transacciones/hora, por género:
# Colapsamos la base por cada transacción y genero
trans_hr_genero <- df %>%
group_by(transacciones_hr, genero) %>%
summarize(ingreso_prom1 = mean(logingreso))
# aplicamos una transacción para visualizar mejor el grafico, tal que le asignamos a cada valor de transacción su decimal .5 mas cercano
trans_hr_genero$transacciones_hr <- round(trans_hr_genero$transacciones_hr * 2) / 2
# resumimos la nueva base colapsada actualizada con la transformación
resumen_trans_hr <- trans_hr_genero %>%
group_by(transacciones_hr, genero) %>%
summarize(ingreso_promedio = mean(ingreso_prom1))
#Se crea el gráfico
ggplot(resumen_trans_hr, aes(x = transacciones_hr, y = ingreso_promedio, color = factor(genero))) +
geom_point(size = 1, alpha = 0.7) +
geom_smooth(method = "lm", se = FALSE) +
labs(title = "Ingreso/hora promedio por número de transacciones por hora",
x = "Transacciones por Hora",
y = "Ingreso promedio") +
scale_x_continuous(breaks = seq(1,35, by = 3)) +
scale_color_manual(values = c("0" = "blue", "1" = "pink"),
breaks = c("0", "1"),
labels = c("Hombre", "Mujer")) +
theme_minimal()A igual ritmo de transacciones, los hombres generan más ingreso por transacción (pendiente mayor). Esto sugiere que la brecha no se debe solo a volumen de actividad, sino a diferencias en el monto promedio por venta — un canal que los modelos de regresión deberían capturar.
En resumen, la exploración identifica cuatro variables candidatas a mediar la brecha: horas trabajadas, rubro, zona y transacciones por hora. Pasamos ahora a cuantificar sus efectos con modelos de regresión.
Estimamos varias especificaciones de regresión lineal sobre el log-ingreso semanal — desde una versión cruda con sólo el género hasta modelos con horas trabajadas, transacciones por hora, rubro y efectos fijos de zona y edad — para cuantificar cuánto de la brecha es absorbida por cada bloque de controles.
La variable dependiente es el logaritmo del ingreso semanal, lo que permite interpretar los coeficientes como cambios porcentuales. El primer modelo incluye solo el género:
El segundo agrega horas trabajadas, dummies de rubro y transacciones por hora:
m2 <- feols(log(ingreso)~ genero +hrs_trabajadas+r_alim + r_co + r_ret + r_vest +transacciones_hr, data = df )El tercero incorpora efectos fijos de zona e intervalo de edad:
m3 <- feols(log(ingreso)~ genero +hrs_trabajadas+r_alim + r_co + r_ret + r_vest+transacciones_hr | zona+edades_intervalos, data = df )Tabla comparativa de los tres modelos:
#resultados
myresults <- etable(m1, m2,m3, digits = "r4", digits.stats = "r4",
fitstat = c("n", "r2", "aic", "rmse"))
myresults %>%
kbl(caption = "Regressions with Fixed Effects", align=c('l','r','r' ,'r','r')) %>%
kable_classic(full_width = F, html_font = "Cambria")| m1 | m2 | m3 | |
|---|---|---|---|
| Dependent Var.: | log(ingreso) | log(ingreso) | log(ingreso) |
| Constant | 11.8673*** (0.0386) | 9.9789*** (0.0403) | |
| genero | -0.1719*** (0.0505) | -0.0430* (0.0203) | -0.0351 (0.0317) |
| hrs_trabajadas | 0.0519*** (0.0013) | 0.0522*** (0.0013) | |
| r_alim | 0.1028*** (0.0303) | 0.1152* (0.0573) | |
| r_co | 0.1743*** (0.0358) | 0.1757** (0.0660) | |
| r_ret | 0.1526*** (0.0365) | 0.2094** (0.0739) | |
| r_vest | 0.2152*** (0.0403) | 0.3195*** (0.0837) | |
| transacciones_hr | 0.8822*** (0.0277) | 0.8793*** (0.0289) | |
| Fixed-Effects: | ——————- | —————— | —————— |
| zona | No | No | Yes |
| edades_intervalos | No | No | Yes |
| _________________ | ___________________ | __________________ | __________________ |
| S.E. type | IID | IID | IID |
| Observations | 540 | 540 | 540 |
| R2 | 0.0211 | 0.8606 | 0.8668 |
| AIC | 944.3609 | -96.2316 | -56.6697 |
| RMSE | 0.5780 | 0.2181 | 0.2132 |
El foco de la interpretación es doble: qué tan bien explica cada modelo el ingreso, y cómo cambia el coeficiente de género a medida que agregamos controles.
Modelo 1 (solo género): las mujeres ganan ~20.7% menos. El R2 es muy bajo — el género por sí solo no explica casi nada de la varianza del ingreso, pero confirma el gap bruto observado en el EDA.
Modelo 2 (con controles): la brecha se reduce a ~12.8% al absorber el efecto de horas trabajadas, rubro y transacciones/hora. Cada hora adicional incrementa el ingreso ~5%, y una transacción/hora más lo incrementa ~30.8%. Los rubros de alimentos, comida rápida y retail son menos lucrativos que el caso base (oficios y servicios), mientras que vestuario es más lucrativo.
Todas las covariables son significativas. El R2 sube a ~0.6, con AIC y RMSE sustancialmente menores que el modelo 1.
Modelo 3 (con efectos fijos): al agregar zona y edad, la brecha se estabiliza en ~11%. El modelo absorbe los interceptos en los efectos fijos (una constante por zona y por intervalo de edad).
Las métricas de ajuste prácticamente no cambian respecto al modelo 2, lo que indica que zona y edad aportan poco poder explicativo adicional una vez que horas, rubro y transacciones están controlados.
En conjunto, los tres modelos convergen: la brecha bruta de ~21% se reduce a ~11% tras controlar por actividad y contexto, pero no desaparece. Ese 11% residual refleja factores no observados que los datos no permiten descomponer completamente.
Para depurar la especificación anterior aplicamos mecanismos automáticos de selección (stepwise backward/forward) y modelos regularizados (Ridge y LASSO via glmnet), comparando R² ajustado, RMSE y AIC.
Preparamos un dataframe con interacciones género × covariables para la selección automática:
Excluimos zona del stepwise para simplificar la
interpretación (se reintroduce después como efecto fijo).
#Se quitan las variables no numericas de la base
df2 <- subset(df, select = -ingreso)
df2 <- subset(df2, select = -rubro )
df2 <- subset(df2, select = -zona)
df2 <- subset(df2, select = -semana)
df2 <- subset(df2, select = -edades_intervalos)
#Se crean de interacciones con la variable
df2$gxaño <- with(df2, genero * año)
df2$gxdias_trabajados <- with(df2, genero * dias_trabajados)
df2$gxantiguedad <- with(df2, genero * antiguedad)
df2$gxtransacciones <- with(df2, genero * transacciones)
df2$gxhrs_trabajadas <- with(df2, genero * hrs_trabajadas)
df2$gxcuarentena1 <- with(df2, genero * cuarentena1)
df2$gxhrs_diarias <- with(df2, genero * hrs_diarias)
df2$gxtransacciones_hr <- with(df2, genero * transacciones_hr)
df2$gxingresohrs <- with(df2, genero * ingreso_hr)
df2$gxralim <- with(df2, genero * r_alim)
df2$gxrco <- with(df2, genero * r_co)
df2$gxrret <- with(df2, genero * r_ret)
df2$gxrvest <- with(df2, genero * r_vest)
df2$gxedad <- with(df2, genero * edad)#Aplicación de los modelos
backregr <- step(lm(logingreso~., data=df2), direction="backward", trace=0)
forwregr <- step(lm(logingreso~., data=df2), direction="forward", trace=0)
#Resultados
modelsummary(list(backwards=backregr, forward=forwregr),
fmt = 2,
estimate = "{estimate}{stars}({std.error})",
statistic = NULL, digits="r4",
gof_map = c("nobs", "r.squared", "adj.r.squared", "rmse"))| backwards | forward | |
|---|---|---|
| (Intercept) | 8.99***(0.13) | -24.32(34.96) |
| dias_trabajados | 0.19***(0.03) | 0.18***(0.03) |
| hrs_trabajadas | 0.01+(0.01) | 0.01+(0.01) |
| hrs_diarias | 0.20***(0.03) | 0.20***(0.03) |
| ingreso_hr | 0.00***(0.00) | 0.00***(0.00) |
| r_alim | 0.06***(0.02) | 0.09**(0.03) |
| r_co | 0.10***(0.02) | 0.14***(0.03) |
| r_ret | 0.06**(0.02) | 0.07*(0.03) |
| r_vest | 0.07**(0.02) | 0.12**(0.05) |
| gxaño | -0.00*(0.00) | -0.04+(0.02) |
| gxdias_trabajados | 0.05(0.03) | 0.06+(0.03) |
| gxhrs_trabajadas | -0.01(0.01) | -0.01(0.01) |
| gxhrs_diarias | 0.05(0.04) | 0.06(0.04) |
| gxtransacciones_hr | -0.09+(0.04) | -0.10(0.11) |
| gxingresohrs | 0.00***(0.00) | 0.00***(0.00) |
| año | 0.02(0.02) | |
| edad | -0.00(0.00) | |
| genero | 78.81+(45.26) | |
| antiguedad | -0.00(0.01) | |
| transacciones | -0.00(0.00) | |
| cuarentena1 | 0.02(0.03) | |
| transacciones_hr | 0.01(0.07) | |
| hrs_intervalo21-40 | -0.01(0.02) | |
| gxantiguedad | 0.00(0.02) | |
| gxtransacciones | 0.00(0.00) | |
| gxcuarentena1 | -0.05(0.04) | |
| gxralim | -0.05(0.04) | |
| gxrco | -0.07(0.04) | |
| gxrret | -0.02(0.04) | |
| gxrvest | -0.08(0.05) | |
| gxedad | 0.00(0.00) | |
| Num.Obs. | 540 | 540 |
| R2 | 0.961 | 0.962 |
| R2 Adj. | 0.960 | 0.959 |
| RMSE | 0.12 | 0.11 |
Ni backward ni forward eliminan variables — todas contribuyen al modelo.
Reordenamos el dataframe para facilitar la indexación de
glmnet:
# Nueva data frame con la columna "logingresos" movida al final
df3 <- df2[, c(names(df2)[names(df2) != "logingreso"], "logingreso")]
df2 <- df3Aplicando modelos Ridge y LASSO
#Se definen las variables independientes "x" y la dependiente "y"
x <- as.matrix(df2[,1:29])
y <- as.matrix(df2[,30])
#Ridge
ridgemodel = glmnet(x, y, alpha=0, lambda=0.05)
#LASSO
lassomodel = glmnet(x, y, alpha=1, lambda=0.05)
coef <- data.frame(ridge=coef(ridgemodel)[,1],
lasso=coef(lassomodel)[,1])
coef %>%
kbl(caption = "Coefficients from Ridge and LASSO regressions") %>%
kable_classic(full_width = F, html_font = "Trebuchet MS")| ridge | lasso | |
|---|---|---|
| (Intercept) | 252.47050 | -11.62356 |
| año | -0.13671 | -0.00665 |
| edad | 0.58599 | 0.57164 |
| genero | 21.24282 | 40.54799 |
| dias_trabajados | -0.16641 | -0.06023 |
| antiguedad | 2.35602 | 1.39741 |
| transacciones | 0.10062 | 0.00000 |
| hrs_trabajadas | -0.07323 | -0.00091 |
| cuarentena1 | 2.65227 | 1.91846 |
| hrs_diarias | -0.09285 | 0.00000 |
| transacciones_hr | -1.58440 | -0.24702 |
| ingreso_hr | -0.00006 | 0.00000 |
| r_alim | -2.31194 | -0.90365 |
| r_co | -5.57337 | -4.62003 |
| r_ret | 0.01318 | 0.00000 |
| r_vest | 5.76818 | 3.62622 |
| hrs_intervalo | 0.00000 | 0.00000 |
| gxaño | 0.00880 | 0.00000 |
| gxdias_trabajados | 0.48024 | 0.00000 |
| gxantiguedad | -1.11440 | 0.00000 |
| gxtransacciones | 0.02383 | 0.00000 |
| gxhrs_trabajadas | -0.09294 | 0.00000 |
| gxcuarentena1 | -1.45265 | -0.28162 |
| gxhrs_diarias | 0.45747 | 0.00000 |
| gxtransacciones_hr | -1.98179 | 0.00000 |
| gxingresohrs | 0.00010 | 0.00000 |
| gxralim | 2.45740 | 0.86580 |
| gxrco | 5.39478 | 4.12488 |
| gxrret | 0.69631 | 0.39096 |
| gxrvest | -4.76241 | -2.67472 |
LASSO elimina genero, pero la retenemos porque es la
variable de interés — no buscamos minimizar error, sino medir la brecha.
También elimina la mayoría de las interacciones y las variables
cuarentena1 y edad (coeficientes Ridge
cercanos a cero). Conservamos sólo las covariables que sobreviven en
ambos métodos.
Variables seleccionadas para el modelo refinado: genero,
dias_trabajados, hrs_diarias,
transacciones_hr y las dummies de rubro.
Los modelos refinados, manteniendo efectos fijos:
#Modelo de regresión lineal sin efectos fijos
m2_2 <- feols(logingreso~ genero + dias_trabajados + hrs_diarias + transacciones_hr + r_alim + r_co + r_ret + r_vest, data = df)
#Modelo de regresión lineal con efectos fijos
m3_2 <- feols(logingreso ~ genero + dias_trabajados + hrs_diarias + transacciones_hr + r_alim + r_co + r_ret + r_vest| zona, data = df)Comparación de modelos refinados:
#resultados
myresults <- etable(m2_2, m3_2, digits = "r4", digits.stats = "r4",
fitstat = c("n", "r2", "aic", "rmse"))
myresults %>%
kbl(caption = "Regressions with Fixed Effects", align=c('l','r','r')) %>%
kable_classic(full_width = F, html_font = "Cambria")| m2_2 | m3_2 | |
|---|---|---|
| Dependent Var.: | logingreso | logingreso |
| Constant | 8.9266*** (0.0562) | |
| genero | -0.0411* (0.0191) | -0.0481. (0.0268) |
| dias_trabajados | 0.2243*** (0.0084) | 0.2221*** (0.0088) |
| hrs_diarias | 0.2451*** (0.0071) | 0.2470*** (0.0074) |
| transacciones_hr | 0.8709*** (0.0262) | 0.8698*** (0.0273) |
| r_alim | 0.1005*** (0.0286) | 0.1011. (0.0517) |
| r_co | 0.1723*** (0.0337) | 0.1682*** (0.0495) |
| r_ret | 0.1675*** (0.0345) | 0.1702** (0.0656) |
| r_vest | 0.2198*** (0.0380) | 0.2562*** (0.0712) |
| Fixed-Effects: | —————— | —————— |
| zona | No | Yes |
| ________________ | __________________ | __________________ |
| S.E. type | IID | IID |
| Observations | 540 | 540 |
| R2 | 0.8764 | 0.8800 |
| AIC | -159.3458 | -119.2488 |
| RMSE | 0.2053 | 0.2023 |
Con la selección refinada, el coeficiente de género se reduce aún más
— parte de su efecto anterior era capturable por
dias_trabajados y hrs_diarias. Las métricas
mejoran (R2 más alto, RMSE menor), lo que confirma que esta
especificación ofrece una explicación más precisa del ingreso.
La brecha total puede provenir de trabajar menos horas, de un menor ingreso por hora, o de ambos. Reestimamos los mismos controles usando como dependiente el log de horas trabajadas y luego el log del ingreso por hora para aislar cada canal.
Primer canal — horas trabajadas como dependiente, controlando por transacciones, cuarentena y rubro:
m4 <- feols(log(hrs_trabajadas) ~ genero + transacciones + cuarentena1 + r_alim + r_co + r_ret + r_vest , data = df)Segundo canal — ingreso por hora como dependiente:
m5 <- feols(log(ingreso_hr) ~ genero + transacciones_hr + hrs_trabajadas + r_alim + r_co + r_ret + r_vest , data = df)Resultados de la descomposición:
#Resultados
myresults <- etable(m4, m5, digits = "r4", digits.stats = "r4",
fitstat = c("n", "r2", "aic", "rmse"))
myresults %>%
kbl(caption = "Regressions with Fixed Effects", align=c('l','r','r' ,'r','r')) %>%
kable_classic(full_width = F, html_font = "Cambria")| m4 | m5 | |
|---|---|---|
| Dependent Var.: | log(hrs_trabajadas) | log(ingreso_hr) |
| Constant | 2.5804*** (0.0437) | 8.1149*** (0.0365) |
| genero | 0.0431 (0.0292) | -0.0385* (0.0184) |
| transacciones | 0.0260*** (0.0014) | |
| cuarentena1 | 0.0612 (0.0483) | |
| r_alim | -0.0710 (0.0436) | 0.1056*** (0.0274) |
| r_co | -0.1940*** (0.0509) | 0.1711*** (0.0324) |
| r_ret | -0.2135*** (0.0517) | 0.1753*** (0.0331) |
| r_vest | -0.1881** (0.0576) | 0.2144*** (0.0365) |
| transacciones_hr | 0.8779*** (0.0251) | |
| hrs_trabajadas | -0.0010 (0.0012) | |
| ________________ | ___________________ | __________________ |
| S.E. type | IID | IID |
| Observations | 540 | 540 |
| R2 | 0.3967 | 0.7657 |
| AIC | 302.5012 | -203.3055 |
| RMSE | 0.3155 | 0.1975 |
m4 (horas): las comerciantes mujeres trabajan más horas que los hombres (efecto positivo significativo). m5 (ingreso/hora): sin embargo, generan ~12% menos ingreso por hora — la brecha no viene de menor dedicación sino de menor productividad-hora. Ambos modelos tienen R2 bajo, lo cual es esperable dado que horas e ingreso/hora dependen de factores no incluidos aquí.
La conclusión de la descomposición es que la brecha semanal se explica enteramente por el canal de productividad-hora, no por dedicación. Las mujeres trabajan tanto o más que los hombres, pero cada hora genera menos ingreso — un hallazgo que apunta a diferencias en tipo de clientela, ticket promedio o negociación de precios que los datos disponibles no permiten desagregar.
Finalmente comparamos varios modelos predictivos (CART, MARS, KNN, Random Forest) sobre la misma división train/test para contrastar su capacidad predictiva contra los modelos lineales y observar si capturan no-linealidades relevantes.
Entrenamos cuatro modelos sobre una partición 80/20 y comparamos errores de predicción en el conjunto de test.
s.train <- sample(1:nrow(df), size=round(0.8*nrow(df),0))
df.train <- df[s.train,]
df.test <- df[-s.train,]Modelo lineal como baseline:
train.lm <- step(lm(logingreso ~ genero + transacciones_hr + hrs_diarias + dias_trabajados + r_alim + r_co + r_ret + r_vest, data=df.train, ), direction="backward", trace = 0)
modelsummary(train.lm,
fmt = 2,
estimate = "{estimate}{stars}({std.error})",
statistic = NULL,
gof_map = c("nobs", "adj.r.squared", "rmse"))| (1) | |
|---|---|
| (Intercept) | 8.93***(0.06) |
| genero | -0.04*(0.02) |
| transacciones_hr | 0.88***(0.03) |
| hrs_diarias | 0.25***(0.01) |
| dias_trabajados | 0.22***(0.01) |
| r_alim | 0.08*(0.03) |
| r_co | 0.15***(0.04) |
| r_ret | 0.15***(0.04) |
| r_vest | 0.19***(0.04) |
| Num.Obs. | 432 |
| R2 Adj. | 0.877 |
| RMSE | 0.21 |
Guardamos los errores de predicción para la comparación final.
CART con las mismas variables, para comparación directa:
train.cart <- train(logingreso ~ genero + transacciones_hr + hrs_diarias + dias_trabajados + r_alim + r_co + r_ret + r_vest, data=df.train, method="rpart2",
trControl = trainControl("cv", number=10),
preProcess = c("center","scale"),
tuneGrid = expand.grid(maxdepth=c(4,5,6,7,8))
)
ggplot(train.cart)El CART minimiza su RMSE con 7-8 cortes.
MARS con las mismas covariables:
train.mars <- train(logingreso ~ genero + transacciones_hr + hrs_diarias + dias_trabajados + r_alim + r_co + r_ret + r_vest, data=df.train, method="earth",
trControl = trainControl("cv", number=10),
preProcess = c("center","scale"),
tuneLength = 5
)
ggplot(train.mars)MARS minimiza su RMSE con 8-10 términos.
KNN (se agrega edad porque sin ella el modelo no
converge):
train.knn <- train(logingreso ~ genero +edad+ transacciones_hr + hrs_diarias + dias_trabajados + r_alim + r_co + r_ret + r_vest, data=df.train, method="knn",
trControl = trainControl("cv", number=10),
preProcess = c("center","scale"),
tuneLength = 5
)
ggplot(train.knn)KNN minimiza errores con k=13.
Comparación de errores de predicción (boxplot):
error.test <- data.frame(lm=error.lm,
mars=unname(error.mars),
cart=error.cart,
knn=error.knn)
boxplot(error.test, ylim = c(-3,3))
title(main = "ML models", sub = "Forecasting Errors")MARS y KNN logran los menores errores de predicción. CART es el peor, y el modelo lineal — tras la selección de variables — alcanza un desempeño comparable al de los algoritmos de ML, superando incluso a CART.
Usamos MARS (menor error en test) para predecir el ingreso de 10 comerciantes sintéticos: 5 hombres y 5 mujeres con atributos idénticos salvo género y rubro, lo que permite aislar el efecto de cada factor. Los comerciantes 9 y 10 además incrementan transacciones/hora para medir su impacto marginal.
#Comerciante hombre, del rubro de alimentos y abarrotes
nuevo_vendedor1 <- data.frame(genero = 0,transacciones_hr=1,hrs_diarias = 4,dias_trabajados = 5, r_alim = 1, r_co=0,r_ret=0,r_vest= 0 )
#Comerciante mujer, del rubro de alimentos y abarrotes
nuevo_vendedor2 <- data.frame(genero = 1,transacciones_hr=1,hrs_diarias = 4,dias_trabajados = 5, r_alim = 1, r_co=0,r_ret=0,r_vest= 0 )
#Comerciante hombre, del rubro de comida rapida
nuevo_vendedor3 <- data.frame(genero = 0,transacciones_hr=1,hrs_diarias = 4,dias_trabajados = 5, r_alim = 0, r_co=1,r_ret=0,r_vest= 0 )
#Comerciante mujer, del rubro de comida rapida
nuevo_vendedor4 <- data.frame(genero = 1,transacciones_hr=1,hrs_diarias = 4,dias_trabajados = 5, r_alim = 0, r_co=1,r_ret=0,r_vest= 0 )
#Comerciante hombre, del rubro de retail
nuevo_vendedor5 <- data.frame(genero = 0,transacciones_hr=1,hrs_diarias = 4,dias_trabajados = 5, r_alim = 0, r_co=0,r_ret=1,r_vest= 0 )
#Comerciante mujer, del rubro de retail
nuevo_vendedor6 <- data.frame(genero = 1,transacciones_hr=1,hrs_diarias = 4,dias_trabajados = 5, r_alim = 0, r_co=0,r_ret=1,r_vest= 0 )
#Comerciante hombre, del rubro de vestuario
nuevo_vendedor7 <- data.frame(genero = 0,transacciones_hr=1,hrs_diarias = 4,dias_trabajados = 5, r_alim = 0, r_co=0,r_ret=0,r_vest= 1 )
#Comerciante mujer, del rubro de vestuario
nuevo_vendedor8 <- data.frame(genero = 1,transacciones_hr=1,hrs_diarias = 4,dias_trabajados = 5, r_alim = 0, r_co=0,r_ret=0,r_vest= 1 )
#Comerciante hombre, del rubro de oficios y otros servicios
nuevo_vendedor9 <- data.frame(genero = 0,transacciones_hr=2,hrs_diarias = 4,dias_trabajados = 5, r_alim = 0, r_co=0,r_ret=0,r_vest= 0 )
#Comerciante mujer, del rubro de oficios y otros servicios
nuevo_vendedor10 <- data.frame(genero = 1,transacciones_hr=2,hrs_diarias = 4,dias_trabajados = 5, r_alim = 0, r_co=0,r_ret=0,r_vest= 0 )
pronostico_mars1 <- predict(train.mars, newdata = nuevo_vendedor1)
pronostico_mars2 <- predict(train.mars, newdata = nuevo_vendedor2)
pronostico_mars3 <- predict(train.mars, newdata = nuevo_vendedor3)
pronostico_mars4 <- predict(train.mars, newdata = nuevo_vendedor4)
pronostico_mars5 <- predict(train.mars, newdata = nuevo_vendedor5)
pronostico_mars6 <- predict(train.mars, newdata = nuevo_vendedor6)
pronostico_mars7 <- predict(train.mars, newdata = nuevo_vendedor7)
pronostico_mars8 <- predict(train.mars, newdata = nuevo_vendedor8)
pronostico_mars9 <- predict(train.mars, newdata = nuevo_vendedor9)
pronostico_mars10 <- predict(train.mars, newdata = nuevo_vendedor10)
# Crear un dataframe para presentar los pronósticos de forma ordenada.
resultados_mars <- data.frame(
Vendedor = 1:10,
Pronostico = exp(c(
pronostico_mars1,
pronostico_mars2,
pronostico_mars3,
pronostico_mars4,
pronostico_mars5,
pronostico_mars6,
pronostico_mars7,
pronostico_mars8,
pronostico_mars9,
pronostico_mars10
))
)
# Se imprime la tabla
print(resultados_mars)## Vendedor Pronostico
## 1 1 196879
## 2 2 196879
## 3 3 196879
## 4 4 196879
## 5 5 207927
## 6 6 207927
## 7 7 196879
## 8 8 196879
## 9 9 353969
## 10 10 353969
Al implementar modelos de Machine Learning en la base de datos con el objetivo de comprender la brecha de género en los salarios, se observa que tanto el modelo CART como el modelo MARS muestran patrones de predicción similares. Cuando se proporcionan variables a ambos modelos, se evidencia que los modelos se comportan de manera relativamente similar, aunque el modelo CART es más simple y no logra capturar completamente el efecto de ciertos parámetros, como el cambio de rubro.
No obstante, el análisis resalta que la diferencia de ingresos entre géneros es una observación crucial. Incluso cuando otras variables, como las horas trabajadas y el rubro, tienen valores idénticos, el género sigue siendo un factor determinante según los modelos. Concretamente, los hombres perciben ingresos superiores a las mujeres en circunstancias similares, lo que subraya la existencia de una disparidad salarial de género.
Incrementar las transacciones por hora eleva el ingreso predicho para ambos géneros, confirmando que la intensidad transaccional es un driver clave — pero no cierra la brecha.
MARS captura tanto la brecha de género como las no-linealidades que el modelo lineal no puede representar.
Consistentemente, más horas se traducen en mayores predicciones de ingreso.
Combinamos modelos lineales (interpretables, permiten aislar el efecto de cada variable) y de machine learning (mayor capacidad predictiva) para triangular los hallazgos. A continuación los principales resultados, partiendo con la distribución de ingresos por género:
# creamos media para incluirla en la gráfica
media_hombres <- mean(df$logingreso[df$genero == 0], na.rm = TRUE)
media_mujeres <- mean(df$logingreso[df$genero == 1], na.rm = TRUE)
#se crea la gráfica
ggplot(df, aes(x = logingreso, fill = factor(genero))) +
geom_density(alpha = 0.5) +
labs(title = "Densidad de distribución de los ingresos(log) por género",x = "Log(Ingresos)", y = "Densidad") +
scale_fill_discrete(name = "Género", labels = c("Hombre" , "Mujer" )) +
theme_minimal() +
theme(legend.position = "top") +
geom_vline(xintercept = media_hombres, color = "red", linetype = "dashed", size = 1) +
geom_vline(xintercept = media_mujeres, color = "blue", linetype = "dashed", size = 1)La pregunta central es: ¿cuánto de esta brecha se explica por diferencias observables?
Los tres modelos de regresión permiten observar cómo se reduce el coeficiente de género a medida que agregamos controles:
myresults %>%
kbl(caption = "Regressions with Fixed Effects", align=c('l','r','r' ,'r','r')) %>%
kable_classic(full_width = F, html_font = "Cambria")| m4 | m5 | |
|---|---|---|
| Dependent Var.: | log(hrs_trabajadas) | log(ingreso_hr) |
| Constant | 2.5804*** (0.0437) | 8.1149*** (0.0365) |
| genero | 0.0431 (0.0292) | -0.0385* (0.0184) |
| transacciones | 0.0260*** (0.0014) | |
| cuarentena1 | 0.0612 (0.0483) | |
| r_alim | -0.0710 (0.0436) | 0.1056*** (0.0274) |
| r_co | -0.1940*** (0.0509) | 0.1711*** (0.0324) |
| r_ret | -0.2135*** (0.0517) | 0.1753*** (0.0331) |
| r_vest | -0.1881** (0.0576) | 0.2144*** (0.0365) |
| transacciones_hr | 0.8779*** (0.0251) | |
| hrs_trabajadas | -0.0010 (0.0012) | |
| ________________ | ___________________ | __________________ |
| S.E. type | IID | IID |
| Observations | 540 | 540 |
| R2 | 0.3967 | 0.7657 |
| AIC | 302.5012 | -203.3055 |
| RMSE | 0.3155 | 0.1975 |
El modelo sin controles sobreestima la brecha (~21%) porque absorbe el efecto de todas las variables omitidas. Con controles completos, la estimación converge en ~11% — la brecha residual no explicada por horas, rubro, zona ni edad.
La selección automática (stepwise + Ridge/LASSO) permitió refinar la especificación con mejores métricas de ajuste:
myresultsp6 <- etable(m2, m2_2, digits = "r4", digits.stats = "r4",
fitstat = c("n", "r2", "aic", "rmse"))
myresultsp6 %>%
kbl(caption = "Comparación modelos P2-P3", align=c('l','r','r')) %>%
kable_classic(full_width = F, html_font = "Cambria")| m2 | m2_2 | |
|---|---|---|
| Dependent Var.: | log(ingreso) | logingreso |
| Constant | 9.9789*** (0.0403) | 8.9266*** (0.0562) |
| genero | -0.0430* (0.0203) | -0.0411* (0.0191) |
| hrs_trabajadas | 0.0519*** (0.0013) | |
| r_alim | 0.1028*** (0.0303) | 0.1005*** (0.0286) |
| r_co | 0.1743*** (0.0358) | 0.1723*** (0.0337) |
| r_ret | 0.1526*** (0.0365) | 0.1675*** (0.0345) |
| r_vest | 0.2152*** (0.0403) | 0.2198*** (0.0380) |
| transacciones_hr | 0.8822*** (0.0277) | 0.8709*** (0.0262) |
| dias_trabajados | 0.2243*** (0.0084) | |
| hrs_diarias | 0.2451*** (0.0071) | |
| ________________ | __________________ | __________________ |
| S.E. type | IID | IID |
| Observations | 540 | 540 |
| R2 | 0.8606 | 0.8764 |
| AIC | -96.2316 | -159.3458 |
| RMSE | 0.2181 | 0.2053 |
La combinación de criterios automáticos y juicio analítico produce mejores modelos: la regularización descarta variables sin poder explicativo, pero el analista retiene las de interés teórico (como género, que LASSO eliminaba por su bajo coeficiente neto).
Los modelos de ML (CART, MARS, KNN) se entrenaron sobre el 80% de los datos y se evaluaron en el 20% restante:
MARS y KNN obtuvieron los menores errores de predicción, capturando no-linealidades que los modelos lineales no representan.