# Librerías
library(readxl) # lectura de Excel
library(dplyr) # manipulación de datos
library(tidyr) # formato largo para gráficos
library(ggplot2) # gráficos
library(scales) # formato de ejes en pesos
library(knitr) # tablas
library(car) # VIF, Durbin-Watson
library(lmtest) # Breusch-Pagan, RESET
# Paleta sobria
azul <- "#1F3A5F"
gris <- "#8C8C8C"
tema <- theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold", color = azul),
panel.grid.minor = element_blank())
# Formato de pesos en millones
millones <- label_number(scale = 1e-6, suffix = " M", accuracy = 0.1)
Una empresa necesita estimar el ingreso mensual de sus empleados para apoyar decisiones de Recursos Humanos: definir bandas salariales por cargo, preparar ofertas para nuevos ingresos y detectar salarios que se alejan de lo esperado para un perfil determinado.
Se utiliza la Base 3, con información de 1.470 empleados. La variable dependiente es el ingreso mensual en pesos colombianos y se mantiene durante todo el caso. La pregunta que guía el análisis es si un modelo de regresión lineal construido con el cargo y la trayectoria laboral del empleado predice el ingreso con la precisión suficiente para que Recursos Humanos lo use en esas decisiones.
Los resultados se leen como asociaciones estadísticas. La base es un corte transversal de una sola empresa y no permite afirmar que una variable cause un mayor ingreso.
# Carga de la base
base <- read_excel("base3_ingreso.xlsx", sheet = "Base")
# Nombres sin tildes ni eñes para evitar problemas de codificación
base <- base %>%
rename(Ingreso = Ingreso_Mensual,
Experiencia = `Años_Experiencia`)
# Variables categóricas como factor, con su categoría de referencia
base <- base %>%
mutate(Cargo = relevel(factor(Cargo), ref = "Tecnico_Laboratorio"),
Genero = relevel(factor(Genero), ref = "F"))
dim(base)
## [1] 1470 24
Ingreso,
ingreso mensual en pesos colombianos.| Variable | Tipo | Uso en el caso | Justificación |
|---|---|---|---|
Experiencia |
Cuantitativa (años) | Regresión simple y múltiple | Mayor correlación con el ingreso (r = 0,77). La experiencia total acumulada es el criterio habitual para ubicar a una persona dentro de la escala salarial. |
Antiguedad |
Cuantitativa (años en la empresa) | Regresión simple y múltiple | Correlación de 0,51 con el ingreso. Recoge la trayectoria dentro de la empresa, distinta de la experiencia total (r = 0,63 entre ambas, una correlación moderada que permite usarlas juntas). |
Genero |
Categórica binaria (referencia: F) | Regresión simple y control en la múltiple | Es la binaria pertinente para el problema: permite evaluar si existe una diferencia salarial entre hombres y mujeres con el mismo cargo y la misma trayectoria, que es la interpretación responsable que pide el caso. Se usa como control, no como criterio para fijar salarios. |
Cargo |
Categórica, 9 niveles (referencia: Técnico de Laboratorio) | Regresión múltiple | Define la escala salarial: las medianas van de 2,6 M (Representante de Ventas) a 17,5 M (Gerente). Se elige Técnico de Laboratorio como referencia por ser uno de los cargos más numerosos y de menor salario, lo que facilita leer cada coeficiente como la prima del cargo. |
Antes de definir el modelo se revisaron las demás variables de la base. Se descartan por una de tres razones: no tienen relación con el ingreso, su información ya está contenida en otra variable o no son pertinentes para predecir.
| Variable | Evidencia | Razón para descartarla |
|---|---|---|
Edad |
r = 0,50 con el ingreso, pero r = 0,68 con la experiencia | Redundante con la experiencia (ver prueba F). |
Educacion |
r = 0,10 con el ingreso | Relación débil y no significativa (ver prueba F). |
Horas_Extra |
R² = 0,000; p = 0,82 | No se relaciona con el ingreso. |
Rendimiento_Laboral |
Solo toma los valores 3 y 4; p = 0,11 con cargo y experiencia | No se relaciona con el nivel del salario, sino con el aumento (r =
0,77 con Porcentaje_aumento_salarial). Al no haber casos
con calificación 1 o 2, el modelo no podría usarse para bajo
rendimiento. |
Porcentaje_aumento_salarial |
r = −0,03 con el ingreso | Es una tasa de cambio, no un nivel. Sin el salario anterior no aporta información sobre el ingreso actual. |
Rotación |
Diferencia significativa, R² = 0,03 | Es una consecuencia, no un determinante: probablemente quienes ganan menos se van más. Además, no se conoce de antemano para un nuevo empleado. |
| Satisfacción, equilibrio vida-trabajo, distancia a casa, capacitaciones | |r| < 0,04 | Sin relación con el ingreso. |
Departamento, Campo_Educación,
Estado_Civil |
– | El área de trabajo ya está contenida en el cargo, y el estado civil no es un criterio pertinente para fijar salarios. |
Para justificar la exclusión de Edad y
Educacion se compara un modelo completo con un modelo
restringido que no las incluye. La hipótesis nula es que ambos
coeficientes son cero de forma simultánea:
\[H_0: \beta_{Edad} = \beta_{Educacion} = 0 \qquad H_1: \text{al menos uno es distinto de cero}\]
# Modelo completo: con edad y educación
m_completo <- lm(Ingreso ~ Experiencia + Antiguedad + Edad + Educacion +
Cargo + Genero, data = base)
# Modelo restringido: sin edad ni educación
m_restringido <- lm(Ingreso ~ Experiencia + Antiguedad + Cargo + Genero,
data = base)
# Prueba F conjunta
prueba_f <- anova(m_restringido, m_completo)
prueba_f
| Res.Df | RSS | Df | Sum of Sq | F | Pr(>F) |
|---|---|---|---|---|---|
| 1458 | 3.994840e+15 | NA | NA | NA | NA |
| 1456 | 3.988984e+15 | 2 | 5.855454e+12 | 1.068636 | 0.3437461 |
# Comparación de ajuste
data.frame(
Modelo = c("Completo", "Sin edad ni educación"),
R2_ajustado = round(c(summary(m_completo)$adj.r.squared,
summary(m_restringido)$adj.r.squared), 4),
AIC = round(c(AIC(m_completo), AIC(m_restringido)), 0)
)
| Modelo | R2_ajustado | AIC |
|---|---|---|
| Completo | 0.8764 | 46287 |
| Sin edad ni educación | 0.8764 | 46285 |
Interpretación. La prueba da F = 1,07 con p = 0,344, por lo que no se rechaza la hipótesis nula: edad y educación no aportan información, ni por separado ni en conjunto, una vez se conocen la experiencia, la antigüedad, el cargo y el género. Al quitarlas, el R² ajustado se mantiene en 0,876 y el AIC baja de 46.287 a 46.285, lo que indica que el modelo restringido es preferible.
La edad sí tiene relación con el ingreso por sí sola (r = 0,50), pero esa relación pasa por la experiencia: las personas mayores tienen más años trabajados. Cuando la experiencia está en el modelo, la edad ya no agrega nada. Al retirarla, además, la antigüedad pasa de p = 0,05 a p = 0,026, porque deja de repartir su efecto con una variable correlacionada.
Nota metodológica. Esta selección se hace con toda la base como paso exploratorio. En la validación cruzada del punto 8 y del Punto 2, la selección de variables se repite dentro de cada partición de entrenamiento.
# Variables del análisis
vars <- c("Ingreso", "Experiencia", "Antiguedad", "Cargo", "Genero")
datos <- base %>% select(all_of(vars))
# Valores faltantes por variable
colSums(is.na(datos))
## Ingreso Experiencia Antiguedad Cargo Genero
## 0 0 0 0 0
# Filas duplicadas en la base completa
sum(duplicated(base))
## [1] 0
# Consistencia: la antigüedad en la empresa no puede superar la experiencia total
sum(base$Antiguedad > base$Experiencia)
## [1] 0
La base no tiene valores faltantes ni filas duplicadas. Ningún empleado tiene más años en la empresa que años de experiencia total, de modo que no hay inconsistencias entre ambas variables.
# Estadísticas descriptivas
resumen <- datos %>%
select(Ingreso, Experiencia, Antiguedad) %>%
pivot_longer(everything(), names_to = "Variable", values_to = "Valor") %>%
group_by(Variable) %>%
summarise(Media = mean(Valor),
Mediana = median(Valor),
Desv_Est = sd(Valor),
Minimo = min(Valor),
Maximo = max(Valor),
CV = sd(Valor) / mean(Valor))
kable(resumen, digits = 2, format.args = list(big.mark = "."))
| Variable | Media | Mediana | Desv_Est | Minimo | Maximo | CV |
|---|---|---|---|---|---|---|
| Antiguedad | 7.01 | 5 | 6.13 | 0 | 40 | 0.87 |
| Experiencia | 11.28 | 10 | 7.78 | 0 | 40 | 0.69 |
| Ingreso | 6.502.931.29 | 4.919.000 | 4.707.956.78 | 1.009.000 | 19.999.000 | 0.72 |
# Distribución del ingreso
ggplot(datos, aes(x = Ingreso)) +
geom_histogram(bins = 40, fill = azul, color = "white") +
geom_vline(aes(xintercept = mean(Ingreso)), color = gris, linetype = "dashed") +
geom_vline(aes(xintercept = median(Ingreso)), color = gris) +
scale_x_continuous(labels = millones) +
labs(title = "Distribución del ingreso mensual",
subtitle = "Línea continua: mediana · Línea punteada: media",
x = "Ingreso mensual", y = "Empleados") +
tema
Ingreso. La mediana es 4,9 M y la media 6,5 M. La media queda por encima porque la distribución tiene una cola larga a la derecha (asimetría de 1,37): la mayoría gana entre 2,9 M y 8,4 M, y un grupo más pequeño supera los 15 M. Esta forma anticipa que una transformación logarítmica puede ser útil en el Punto 2.
# Distribución de experiencia y antigüedad
datos %>%
select(Experiencia, Antiguedad) %>%
pivot_longer(everything(), names_to = "Variable", values_to = "Años") %>%
ggplot(aes(x = Años)) +
geom_histogram(binwidth = 2, fill = azul, color = "white") +
facet_wrap(~ Variable, scales = "free_y") +
labs(title = "Distribución de la experiencia y la antigüedad",
x = "Años", y = "Empleados") +
tema
Experiencia y antigüedad. La experiencia tiene una mediana de 10 años (rango de 0 a 40) y la antigüedad de 5 años (rango de 0 a 40). Ambas son asimétricas a la derecha: la mayoría de los empleados lleva pocos años en la empresa y unos pocos superan los 20.
# Criterio del rango intercuartílico: valores por encima de Q3 + 1,5·RIC
limite_sup <- function(x) quantile(x, 0.75) + 1.5 * IQR(x)
data.frame(
Variable = c("Ingreso", "Experiencia", "Antiguedad"),
Limite = c(limite_sup(datos$Ingreso),
limite_sup(datos$Experiencia),
limite_sup(datos$Antiguedad)),
Atipicos = c(sum(datos$Ingreso > limite_sup(datos$Ingreso)),
sum(datos$Experiencia > limite_sup(datos$Experiencia)),
sum(datos$Antiguedad > limite_sup(datos$Antiguedad)))
)
| Variable | Limite | Atipicos |
|---|---|---|
| Ingreso | 16581000.0 | 114 |
| Experiencia | 28.5 | 63 |
| Antiguedad | 18.0 | 104 |
# ¿A qué cargos pertenecen los ingresos atípicos?
datos %>%
filter(Ingreso > limite_sup(Ingreso)) %>%
count(Cargo)
| Cargo | n |
|---|---|
| Director_Investigación | 40 |
| Gerente | 74 |
Con el criterio del rango intercuartílico se identifican 114 ingresos atípicos (por encima de 16,6 M), 63 en experiencia (más de 28,5 años) y 104 en antigüedad (más de 18 años).
Tratamiento: se conservan todos. Los 114 ingresos atípicos corresponden en su totalidad a Gerentes (74) y Directores de Investigación (40), los dos cargos mejor pagados de la empresa. No son errores de registro, sino la parte alta de la escala salarial. Eliminarlos haría que el modelo no pudiera predecir el salario de esos cargos. Lo mismo ocurre con la experiencia y la antigüedad: valores de 30 o 40 años son plausibles para empleados de mayor edad. Su posible influencia sobre los coeficientes se evalúa en el Punto 2 con la distancia de Cook.
# Distribución del cargo
datos %>%
count(Cargo) %>%
mutate(Porcentaje = round(100 * n / sum(n), 1)) %>%
arrange(desc(n))
| Cargo | n | Porcentaje |
|---|---|---|
| Ejecutivo_Ventas | 326 | 22.2 |
| Investigador_Cientifico | 292 | 19.9 |
| Tecnico_Laboratorio | 259 | 17.6 |
| Director_Manofactura | 145 | 9.9 |
| Representante_Salud | 131 | 8.9 |
| Gerente | 102 | 6.9 |
| Representante_Ventas | 83 | 5.6 |
| Director_Investigación | 80 | 5.4 |
| Recursos_Humanos | 52 | 3.5 |
# Distribución del género
datos %>%
count(Genero) %>%
mutate(Porcentaje = round(100 * n / sum(n), 1))
| Genero | n | Porcentaje |
|---|---|---|
| F | 588 | 40 |
| M | 882 | 60 |
Los cargos más frecuentes son Ejecutivo de Ventas (22 %), Investigador Científico (20 %) y Técnico de Laboratorio (18 %). Recursos Humanos es el menos frecuente (52 empleados, 3,5 %), por lo que sus estimaciones serán menos precisas. En género, el 60 % son hombres y el 40 % mujeres; ambos grupos tienen tamaño suficiente para compararlos.
# Correlación de Pearson entre el ingreso y las variables cuantitativas
# (se incluyen edad y educación como referencia de la exclusión)
round(cor(base[, c("Ingreso", "Experiencia", "Antiguedad", "Edad", "Educacion")]), 3)
## Ingreso Experiencia Antiguedad Edad Educacion
## Ingreso 1.000 0.773 0.514 0.498 0.095
## Experiencia 0.773 1.000 0.628 0.680 0.148
## Antiguedad 0.514 0.628 1.000 0.311 0.069
## Edad 0.498 0.680 0.311 1.000 0.208
## Educacion 0.095 0.148 0.069 0.208 1.000
La experiencia tiene la correlación más alta con el ingreso (0,77), seguida de la antigüedad (0,51). Entre los predictores, la correlación experiencia–antigüedad es moderada (0,63), por lo que ambas pueden convivir en un mismo modelo. La edad, en cambio, está más relacionada con la experiencia (0,68) que con el ingreso (0,50), lo que confirma lo encontrado en la prueba F.
# Diagramas de dispersión con recta de regresión
datos %>%
select(Ingreso, Experiencia, Antiguedad) %>%
pivot_longer(-Ingreso, names_to = "Variable", values_to = "Años") %>%
ggplot(aes(x = Años, y = Ingreso)) +
geom_point(alpha = 0.3, color = azul) +
geom_smooth(method = "lm", se = FALSE, color = gris, linewidth = 1) +
facet_wrap(~ Variable, scales = "free_x") +
scale_y_continuous(labels = millones) +
labs(title = "Ingreso mensual frente a experiencia y antigüedad",
x = "Años", y = "Ingreso mensual") +
tema
Experiencia. La relación es positiva y clara: a más años de experiencia, mayor ingreso. La nube se abre a medida que aumenta la experiencia. Con pocos años los salarios están concentrados, y con más de 15 años hay personas que ganan 5 M y otras que ganan 19 M. Esa diferencia se debe al cargo, y es una señal de que la varianza no será constante (heterocedasticidad), lo que se evalúa en el Punto 2.
Antigüedad. La relación también es positiva pero más débil y dispersa. Muchos empleados con pocos años en la empresa ya tienen ingresos altos, lo que sugiere que el salario depende más de la experiencia que traen que del tiempo que llevan en la empresa.
# Ingreso por cargo, ordenado por la mediana
ggplot(datos, aes(x = reorder(Cargo, Ingreso, FUN = median), y = Ingreso)) +
geom_boxplot(fill = azul, alpha = 0.7, color = "grey30", outlier.color = gris) +
coord_flip() +
scale_y_continuous(labels = millones) +
labs(title = "Ingreso mensual por cargo",
x = NULL, y = "Ingreso mensual") +
tema
# Medianas por cargo
datos %>%
group_by(Cargo) %>%
summarise(n = n(), Mediana = median(Ingreso), Minimo = min(Ingreso),
Maximo = max(Ingreso)) %>%
arrange(Mediana)
| Cargo | n | Mediana | Minimo | Maximo |
|---|---|---|---|---|
| Representante_Ventas | 83 | 2579000 | 1052000 | 6632000 |
| Tecnico_Laboratorio | 259 | 2886000 | 1102000 | 7403000 |
| Investigador_Cientifico | 292 | 2887500 | 1009000 | 9724000 |
| Recursos_Humanos | 52 | 3093000 | 1555000 | 10725000 |
| Ejecutivo_Ventas | 326 | 6231000 | 4001000 | 13872000 |
| Director_Manofactura | 145 | 6447000 | 4011000 | 13973000 |
| Representante_Salud | 131 | 6811000 | 4000000 | 13966000 |
| Director_Investigación | 80 | 16510000 | 11031000 | 19973000 |
| Gerente | 102 | 17454500 | 11244000 | 19999000 |
El cargo separa a los empleados en tres niveles salariales:
Las cajas casi no se superponen entre niveles, lo que indica que el cargo es el principal determinante del ingreso. Dentro de cada cargo sigue habiendo variación (un Gerente gana entre 11,2 y 20 M), que es la que se espera explicar con la experiencia y la antigüedad.
# Ingreso por género
ggplot(datos, aes(x = Genero, y = Ingreso)) +
geom_boxplot(fill = c(gris, azul), alpha = 0.7, color = "grey30") +
scale_y_continuous(labels = millones) +
labs(title = "Ingreso mensual por género", x = NULL, y = "Ingreso mensual") +
tema
datos %>%
group_by(Genero) %>%
summarise(n = n(), Media = mean(Ingreso), Mediana = median(Ingreso))
| Genero | n | Media | Mediana |
|---|---|---|---|
| F | 588 | 6686566 | 5081500 |
| M | 882 | 6380508 | 4837500 |
Sin controlar por otras variables, las mujeres tienen un ingreso mediano ligeramente mayor (5,08 M frente a 4,84 M). Las distribuciones son muy parecidas, por lo que se espera que el género explique poco por sí solo. La pregunta relevante, que se aborda en el Punto 2, es si existe alguna diferencia entre hombres y mujeres que ocupan el mismo cargo y tienen la misma trayectoria.
Se proponen tres modelos simples, cada uno con un predictor diferente:
Ingreso ~ Experiencia. Es la
variable con mayor correlación con el ingreso.Ingreso ~ Antiguedad. Recoge
la trayectoria dentro de la empresa.Ingreso ~ Genero. Es la
variable binaria pertinente, con categoría de referencia F.El cargo no se usa como regresión simple porque tiene nueve categorías. Se incorpora en los modelos múltiples del Punto 2.
# Modelo 1: experiencia
m1 <- lm(Ingreso ~ Experiencia, data = base)
summary(m1)
##
## Call:
## lm(formula = Ingreso ~ Experiencia, data = base)
##
## Residuals:
## Min 1Q Median 3Q Max
## -11271297 -1750781 -87495 1398604 11539481
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 1227935 137299 8.944 <2e-16 ***
## Experiencia 467658 10021 46.669 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 2988000 on 1468 degrees of freedom
## Multiple R-squared: 0.5974, Adjusted R-squared: 0.5971
## F-statistic: 2178 on 1 and 1468 DF, p-value: < 2.2e-16
# Modelo 2: antigüedad
m2 <- lm(Ingreso ~ Antiguedad, data = base)
summary(m2)
##
## Call:
## lm(formula = Ingreso ~ Antiguedad, data = base)
##
## Residuals:
## Min 1Q Median 3Q Max
## -9504365 -2498983 -1188091 1392960 15484522
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 3733273 160091 23.32 <2e-16 ***
## Antiguedad 395205 17201 22.98 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 4039000 on 1468 degrees of freedom
## Multiple R-squared: 0.2645, Adjusted R-squared: 0.264
## F-statistic: 527.9 on 1 and 1468 DF, p-value: < 2.2e-16
# Modelo 3: género (referencia: F)
m3 <- lm(Ingreso ~ Genero, data = base)
summary(m3)
##
## Call:
## lm(formula = Ingreso ~ Genero, data = base)
##
## Residuals:
## Min 1Q Median 3Q Max
## -5557566 -3562258 -1562537 1872198 13618492
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 6686566 194120 34.445 <2e-16 ***
## GeneroM -306058 250608 -1.221 0.222
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 4707000 on 1468 degrees of freedom
## Multiple R-squared: 0.001015, Adjusted R-squared: 0.0003345
## F-statistic: 1.491 on 1 and 1468 DF, p-value: 0.2222
# Tabla resumen de los tres modelos simples
resumen_modelo <- function(modelo, nombre) {
s <- summary(modelo)
data.frame(Modelo = nombre,
Intercepto = coef(modelo)[1],
Pendiente = coef(modelo)[2],
p_valor = s$coefficients[2, 4],
R2 = s$r.squared,
Error_est = s$sigma)
}
tabla_simples <- rbind(resumen_modelo(m1, "M1: Experiencia"),
resumen_modelo(m2, "M2: Antigüedad"),
resumen_modelo(m3, "M3: Género (M)"))
rownames(tabla_simples) <- NULL
kable(tabla_simples, digits = c(0, 0, 0, 4, 3, 0),
format.args = list(big.mark = "."),
caption = "Resumen de los modelos de regresión simple")
| Modelo | Intercepto | Pendiente | p_valor | R2 | Error_est |
|---|---|---|---|---|---|
| M1: Experiencia | 1.227.935 | 467.658 | 0.0000 | 0.597 | 2.988.386 |
| M2: Antigüedad | 3.733.273 | 395.205 | 0.0000 | 0.264 | 4.039.010 |
| M3: Género (M) | 6.686.566 | -306.058 | 0.2222 | 0.001 | 4.707.169 |
La experiencia es, por amplio margen, el predictor simple con mayor capacidad explicativa. La antigüedad es significativa pero explica bastante menos, y el género no muestra una diferencia significativa por sí solo. La interpretación detallada se desarrolla en el punto 5 y la comparación predictiva en el punto 8.
# Rectas ajustadas de los modelos cuantitativos
datos %>%
select(Ingreso, Experiencia, Antiguedad) %>%
pivot_longer(-Ingreso, names_to = "Variable", values_to = "Años") %>%
mutate(Variable = if_else(Variable == "Experiencia",
"M1: Experiencia", "M2: Antigüedad")) %>%
ggplot(aes(x = Años, y = Ingreso)) +
geom_point(alpha = 0.25, color = gris) +
geom_smooth(method = "lm", se = TRUE, color = azul, fill = azul, alpha = 0.2) +
facet_wrap(~ Variable, scales = "free_x") +
scale_y_continuous(labels = millones) +
labs(title = "Rectas ajustadas de los modelos simples",
x = "Años", y = "Ingreso mensual") +
tema
# Intervalos de confianza del 95 % para los coeficientes
confint(m1)
## 2.5 % 97.5 %
## (Intercept) 958611.9 1497259
## Experiencia 448001.8 487315
confint(m2)
## 2.5 % 97.5 %
## (Intercept) 3419241.7 4047304.6
## Antiguedad 361463.7 428945.4
confint(m3)
## 2.5 % 97.5 %
## (Intercept) 6305783 7067349.3
## GeneroM -797647 185530.3
En los modelos 1 y 2 el p-valor de la pendiente es menor que 0,001, por lo que se rechaza la hipótesis de que la pendiente es cero: experiencia y antigüedad tienen una relación estadísticamente significativa con el ingreso. En el modelo 3 el p-valor es 0,222, por lo que no hay evidencia de una diferencia de ingreso entre hombres y mujeres cuando no se considera ninguna otra variable.
Los coeficientes miden asociaciones, no efectos causales. Que cada año de experiencia se asocie con 468 mil pesos más no significa que sumar un año de experiencia a una persona le suba el salario en esa cantidad. Hay tres razones:
Además, la base es un corte transversal de una sola empresa: observa a distintas personas en un mismo momento, no a la misma persona a lo largo del tiempo.
Los supuestos se evalúan sobre el Modelo 1 (experiencia), que es el mejor modelo simple. El diagnóstico de los modelos múltiples se hace en el Punto 2.
# Gráficos de diagnóstico del Modelo 1
par(mfrow = c(2, 2))
plot(m1, col = adjustcolor(azul, 0.4), pch = 16)
par(mfrow = c(1, 1))
# Linealidad: prueba RESET de Ramsey (H0: la forma funcional lineal es adecuada)
resettest(m1)
##
## RESET test
##
## data: m1
## RESET = 19.966, df1 = 2, df2 = 1466, p-value = 2.786e-09
# Varianza constante: prueba de Breusch-Pagan (H0: homocedasticidad)
bptest(m1)
##
## studentized Breusch-Pagan test
##
## data: m1
## BP = 286.05, df = 1, p-value < 2.2e-16
# Normalidad de los errores: prueba de Shapiro-Wilk (H0: los errores son normales)
shapiro.test(residuals(m1))
##
## Shapiro-Wilk normality test
##
## data: residuals(m1)
## W = 0.9819, p-value = 1.227e-12
# Independencia: prueba de Durbin-Watson (H0: no hay autocorrelación)
# (el p-valor se obtiene por remuestreo y puede variar ligeramente entre ejecuciones)
durbinWatsonTest(m1)
## lag Autocorrelation D-W Statistic p-value
## 1 0.003275237 1.993359 0.904
## Alternative hypothesis: rho != 0
# Observaciones influyentes: distancia de Cook
cook <- cooks.distance(m1)
c(Mayores_4_n = sum(cook > 4 / nrow(base)), Maxima = round(max(cook), 4))
## Mayores_4_n Maxima
## 139.0000 0.0587
| Supuesto | Prueba | Resultado | Conclusión |
|---|---|---|---|
| Linealidad | RESET | p < 0,001 | No se cumple del todo: hay curvatura. |
| Varianza constante | Breusch-Pagan | BP = 286, p < 0,001 | No se cumple: heterocedasticidad. |
| Normalidad | Shapiro-Wilk | W = 0,982, p < 0,001 | No se cumple estrictamente. |
| Independencia | Durbin-Watson | DW = 1,99, p > 0,05 | Se cumple. |
| Influyentes | Distancia de Cook | Máxima = 0,059 | Ninguna observación es influyente por sí sola. |
Linealidad. La prueba RESET rechaza la forma lineal. En el gráfico de residuos frente a valores ajustados, la línea de tendencia no es completamente plana: queda por debajo de cero en los valores ajustados bajos y sube por encima alrededor de 10 M, donde empiezan a aparecer los salarios de directores y gerentes. La relación no es exactamente una recta porque los saltos de cargo (de técnico a director, por ejemplo) no ocurren de forma gradual con la experiencia.
Varianza constante. Es el problema más importante. Los residuos forman un embudo: con poca experiencia los errores son pequeños y con mucha experiencia son grandes. Es lo que ya se veía en el diagrama de dispersión: con más de 15 años de experiencia conviven personas que ganan 5 M y otras que ganan 19 M. Consecuencia: los coeficientes siguen siendo insesgados, pero los errores estándar y los intervalos de predicción no son confiables. El intervalo es demasiado ancho para perfiles con poca experiencia y demasiado estrecho para perfiles con mucha.
Normalidad. Con 1.470 observaciones, Shapiro-Wilk detecta desviaciones pequeñas. El gráfico Q-Q muestra que los residuos se alejan de la diagonal en las colas. Por el tamaño de la muestra, las pruebas t y F siguen siendo aproximadamente válidas (teorema del límite central), pero los intervalos de predicción, que sí dependen de la normalidad, pueden ser imprecisos.
Independencia. El estadístico de Durbin-Watson es cercano a 2 y no hay autocorrelación. Es lo esperado en un corte transversal en el que cada fila es un empleado distinto.
Observaciones influyentes. Hay 139 observaciones por encima del umbral 4/n, pero la distancia de Cook máxima es 0,059, muy lejos del valor de 1 que indicaría una observación capaz de cambiar los resultados por sí sola. Esto confirma la decisión de conservar los atípicos.
La heterocedasticidad y la asimetría del ingreso justifican probar una transformación logarítmica de la respuesta. Con el logaritmo, la experiencia pasa a tener un efecto porcentual en lugar de un efecto fijo en pesos, lo que es coherente con la forma de embudo: un año adicional suma más pesos a quien ya gana más.
# Modelo 1 con logaritmo del ingreso
m1_log <- lm(log(Ingreso) ~ Experiencia, data = base)
summary(m1_log)
##
## Call:
## lm(formula = log(Ingreso) ~ Experiencia, data = base)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1.50265 -0.31612 0.01573 0.32857 1.29804
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 14.746880 0.020519 718.68 <2e-16 ***
## Experiencia 0.063246 0.001498 42.23 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.4466 on 1468 degrees of freedom
## Multiple R-squared: 0.5485, Adjusted R-squared: 0.5482
## F-statistic: 1784 on 1 and 1468 DF, p-value: < 2.2e-16
# Supuestos del modelo transformado
bptest(m1_log)
##
## studentized Breusch-Pagan test
##
## data: m1_log
## BP = 21.548, df = 1, p-value = 3.451e-06
shapiro.test(residuals(m1_log))
##
## Shapiro-Wilk normality test
##
## data: residuals(m1_log)
## W = 0.99594, p-value = 0.0005788
# Residuos frente a ajustados: modelo original y transformado
diag <- rbind(
data.frame(Modelo = "Original", Ajustado = fitted(m1),
Residuo = rstandard(m1)),
data.frame(Modelo = "Logaritmo", Ajustado = fitted(m1_log),
Residuo = rstandard(m1_log)))
diag$Modelo <- factor(diag$Modelo, levels = c("Original", "Logaritmo"))
ggplot(diag, aes(x = Ajustado, y = Residuo)) +
geom_point(alpha = 0.3, color = azul) +
geom_hline(yintercept = 0, color = gris, linetype = "dashed") +
facet_wrap(~ Modelo, scales = "free_x") +
labs(title = "Residuos estandarizados frente a valores ajustados",
x = "Valor ajustado", y = "Residuo estandarizado") +
tema
Interpretación del modelo transformado. Cada año adicional de experiencia se asocia con un ingreso 6,5 % mayor en promedio (exp(0,0632) − 1 = 0,065). El intercepto corresponde a un ingreso de exp(14,75) ≈ 2,5 M para alguien sin experiencia.
Efecto sobre los supuestos. El estadístico de Breusch-Pagan baja de 286 a 21,5 y el de Shapiro-Wilk sube de 0,982 a 0,996. Ambas pruebas siguen rechazando, pero los problemas son mucho menores y el embudo de residuos prácticamente desaparece.
El R² del modelo transformado (0,549) no se compara con el del original (0,597), porque miden la variabilidad de respuestas distintas. La comparación correcta se hace con el MAE en pesos, en el punto 8.
Los cuatro modelos y la referencia se evalúan con las mismas cinco particiones. En cada partición se ajusta el modelo con el 80 % de los datos y se mide el error absoluto medio (MAE) sobre el 20 % restante. Para el modelo con logaritmo, la predicción se devuelve a pesos con la función exponencial antes de calcular el error.
# Particiones fijas para todos los modelos
set.seed(123)
k <- 5
particion <- sample(rep(1:k, length.out = nrow(base)))
# Función de validación cruzada: devuelve el MAE de cada partición en pesos
mae_cv <- function(formula, logaritmo = FALSE) {
sapply(1:k, function(i) {
entrenamiento <- base[particion != i, ]
prueba <- base[particion == i, ]
modelo <- lm(formula, data = entrenamiento)
prediccion <- predict(modelo, newdata = prueba)
if (logaritmo) prediccion <- exp(prediccion) # volver a pesos
mean(abs(prueba$Ingreso - prediccion))
})
}
# Referencia: predecir la mediana del entrenamiento
mae_mediana <- sapply(1:k, function(i) {
mediana <- median(base$Ingreso[particion != i])
mean(abs(base$Ingreso[particion == i] - mediana))
})
# MAE por partición y promedio
resultados <- rbind(
"Referencia: mediana" = mae_mediana,
"M1: Experiencia" = mae_cv(Ingreso ~ Experiencia),
"M2: Antigüedad" = mae_cv(Ingreso ~ Antiguedad),
"M3: Género" = mae_cv(Ingreso ~ Genero),
"M1 log: Experiencia" = mae_cv(log(Ingreso) ~ Experiencia, logaritmo = TRUE)
)
tabla_cv <- data.frame(Modelo = rownames(resultados),
MAE = rowMeans(resultados),
Desv_Est = apply(resultados, 1, sd),
Mejora_vs_mediana = 1 - rowMeans(resultados) / mean(mae_mediana))
rownames(tabla_cv) <- NULL
kable(tabla_cv, digits = c(0, 0, 0, 3), format.args = list(big.mark = "."),
caption = "MAE promedio en validación cruzada de 5 particiones (pesos)")
| Modelo | MAE | Desv_Est | Mejora_vs_mediana |
|---|---|---|---|
| Referencia: mediana | 3.347.858 | 207.527 | 0.000 |
| M1: Experiencia | 2.218.163 | 140.867 | 0.337 |
| M2: Antigüedad | 2.963.646 | 135.804 | 0.115 |
| M3: Género | 3.644.874 | 173.204 | -0.089 |
| M1 log: Experiencia | 2.300.671 | 192.244 | 0.313 |
# Comparación visual del MAE
ggplot(tabla_cv, aes(x = reorder(Modelo, -MAE), y = MAE)) +
geom_col(fill = ifelse(tabla_cv$Modelo == "M1: Experiencia", azul, gris)) +
geom_text(aes(label = millones(MAE)), hjust = -0.1, size = 3.5) +
coord_flip() +
scale_y_continuous(labels = millones, expand = expansion(mult = c(0, 0.15))) +
labs(title = "Error absoluto medio en validación cruzada",
x = NULL, y = "MAE") +
tema
Resultados.
Elección del mejor modelo simple: Modelo 1 (experiencia), en su versión original.
La versión logarítmica predice un poco peor en promedio, pero cumple mejor los supuestos. Por eso se usa para construir los intervalos de predicción.
Se plantean dos casos que Recursos Humanos podría enfrentar:
# Perfiles hipotéticos
escenarios <- data.frame(Experiencia = c(5, 20))
# Modelo original: predicción e intervalo de predicción del 95 %
pred_lineal <- predict(m1, newdata = escenarios, interval = "prediction", level = 0.95)
# Modelo logarítmico: predicción e intervalo devueltos a pesos
pred_log <- exp(predict(m1_log, newdata = escenarios, interval = "prediction", level = 0.95))
data.frame(
Escenario = c("A: 5 años", "B: 20 años"),
Modelo = rep(c("Original", "Logaritmo"), each = 2),
Prediccion = c(pred_lineal[, "fit"], pred_log[, "fit"]),
Limite_inf = c(pred_lineal[, "lwr"], pred_log[, "lwr"]),
Limite_sup = c(pred_lineal[, "upr"], pred_log[, "upr"])
) %>%
kable(digits = 0, format.args = list(big.mark = "."),
caption = "Predicciones e intervalos de predicción del 95 % (pesos)")
| Escenario | Modelo | Prediccion | Limite_inf | Limite_sup |
|---|---|---|---|---|
| A: 5 años | Original | 3.566.227 | -2.299.027 | 9.431.481 |
| B: 20 años | Original | 10.581.104 | 4.714.644 | 16.447.563 |
| A: 5 años | Logaritmo | 3.481.976 | 1.449.241 | 8.365.867 |
| B: 20 años | Logaritmo | 8.991.612 | 3.741.744 | 21.607.325 |
Interpretación.
Qué significa el error en la práctica. Un MAE de 2,2 M equivale al 45 % del ingreso mediano de la empresa (4,9 M). Para fijar una oferta salarial concreta, este error es demasiado grande: los intervalos van de 1,5 a 8,4 M para el mismo perfil. El modelo simple sirve como una primera aproximación del rango salarial según la experiencia, pero no es suficiente para decidir un salario. La amplitud de los intervalos se debe a que no considera el cargo, que es el principal determinante del ingreso. Por eso, en el Punto 2 se incorpora el cargo en un modelo múltiple.
Se proponen dos modelos que responden a dos momentos distintos de una decisión de Recursos Humanos:
| Modelo | Predictores | Uso | Categórica y referencia |
|---|---|---|---|
| MM1: perfil sin cargo | Experiencia + Antigüedad + Género | Estimar el ingreso de una persona antes de definir el cargo, solo con su trayectoria. | Género (referencia: F) |
| MM2: perfil con cargo | Experiencia + Antigüedad + Cargo + Género | Estimar el ingreso de una persona para un cargo determinado. | Cargo (referencia: Técnico de Laboratorio) y Género (referencia: F) |
Justificación de los predictores.
Comparar MM1 con MM2 muestra cuánto vale conocer el cargo para predecir el salario.
# MM1: perfil sin cargo
mm1 <- lm(Ingreso ~ Experiencia + Antiguedad + Genero, data = base)
summary(mm1)
##
## Call:
## lm(formula = Ingreso ~ Experiencia + Antiguedad + Genero, data = base)
##
## Residuals:
## Min 1Q Median 3Q Max
## -10789449 -1760924 -60161 1363240 11390209
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 1148907 171792 6.688 3.21e-11 ***
## Experiencia 449700 12873 34.932 < 2e-16 ***
## Antiguedad 36562 16339 2.238 0.0254 *
## GeneroM 42264 159109 0.266 0.7906
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 2985000 on 1466 degrees of freedom
## Multiple R-squared: 0.5988, Adjusted R-squared: 0.5979
## F-statistic: 729.2 on 3 and 1466 DF, p-value: < 2.2e-16
# MM2: perfil con cargo (referencias: Técnico de Laboratorio y F)
mm2 <- lm(Ingreso ~ Experiencia + Antiguedad + Cargo + Genero, data = base)
summary(mm2)
##
## Call:
## lm(formula = Ingreso ~ Experiencia + Antiguedad + Cargo + Genero,
## data = base)
##
## Residuals:
## Min 1Q Median 3Q Max
## -5206138 -1093729 -82388 992650 5487063
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 1.577e+06 1.328e+05 11.872 < 2e-16 ***
## Experiencia 1.888e+05 8.561e+03 22.058 < 2e-16 ***
## Antiguedad 2.031e+04 9.111e+03 2.229 0.025937 *
## CargoDirector_Investigación 1.010e+07 2.349e+05 42.968 < 2e-16 ***
## CargoDirector_Manofactura 3.065e+06 1.764e+05 17.377 < 2e-16 ***
## CargoEjecutivo_Ventas 2.999e+06 1.405e+05 21.352 < 2e-16 ***
## CargoGerente 1.059e+07 2.308e+05 45.867 < 2e-16 ***
## CargoInvestigador_Cientifico 1.413e+02 1.414e+05 0.001 0.999203
## CargoRecursos_Humanos 8.913e+05 2.516e+05 3.543 0.000408 ***
## CargoRepresentante_Salud 3.023e+06 1.838e+05 16.450 < 2e-16 ***
## CargoRepresentante_Ventas 1.640e+04 2.104e+05 0.078 0.937877
## GeneroM 1.680e+05 8.863e+04 1.896 0.058184 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 1655000 on 1458 degrees of freedom
## Multiple R-squared: 0.8773, Adjusted R-squared: 0.8764
## F-statistic: 947.8 on 11 and 1458 DF, p-value: < 2.2e-16
# Coeficientes de ambos modelos en una sola tabla
coeficientes <- function(modelo, nombre) {
s <- summary(modelo)$coefficients
data.frame(Modelo = nombre, Variable = rownames(s),
Coeficiente = s[, 1], p_valor = s[, 4], row.names = NULL)
}
rbind(coeficientes(mm1, "MM1"), coeficientes(mm2, "MM2")) %>%
kable(digits = c(0, 0, 0, 4), format.args = list(big.mark = "."),
caption = "Coeficientes de los modelos múltiples (pesos)")
| Modelo | Variable | Coeficiente | p_valor |
|---|---|---|---|
| MM1 | (Intercept) | 1.148.907 | 0.0000 |
| MM1 | Experiencia | 449.700 | 0.0000 |
| MM1 | Antiguedad | 36.562 | 0.0254 |
| MM1 | GeneroM | 42.264 | 0.7906 |
| MM2 | (Intercept) | 1.576.512 | 0.0000 |
| MM2 | Experiencia | 188.839 | 0.0000 |
| MM2 | Antiguedad | 20.312 | 0.0259 |
| MM2 | CargoDirector_Investigación | 10.095.004 | 0.0000 |
| MM2 | CargoDirector_Manofactura | 3.065.127 | 0.0000 |
| MM2 | CargoEjecutivo_Ventas | 2.999.153 | 0.0000 |
| MM2 | CargoGerente | 10.585.619 | 0.0000 |
| MM2 | CargoInvestigador_Cientifico | 141 | 0.9992 |
| MM2 | CargoRecursos_Humanos | 891.314 | 0.0004 |
| MM2 | CargoRepresentante_Salud | 3.022.825 | 0.0000 |
| MM2 | CargoRepresentante_Ventas | 16.399 | 0.9379 |
| MM2 | GeneroM | 168.030 | 0.0582 |
# Ajuste global
data.frame(Modelo = c("MM1: sin cargo", "MM2: con cargo"),
R2 = c(summary(mm1)$r.squared, summary(mm2)$r.squared),
R2_ajustado = c(summary(mm1)$adj.r.squared, summary(mm2)$adj.r.squared),
AIC = c(AIC(mm1), AIC(mm2))) %>%
kable(digits = c(0, 3, 3, 0), format.args = list(big.mark = "."))
| Modelo | R2 | R2_ajustado | AIC |
|---|---|---|---|
| MM1: sin cargo | 0.599 | 0.598 | 48.011 |
| MM2: con cargo | 0.877 | 0.876 | 46.285 |
MM1 (sin cargo). El R² ajustado es 0,598, prácticamente el mismo que el de la regresión simple con experiencia (0,597).
Sin el cargo, agregar antigüedad y género no mejora lo que ya explicaba la experiencia.
MM2 (con cargo). El R² ajustado sube a 0,876: el modelo explica el 88 % de la variabilidad del ingreso. El AIC baja de 48.011 a 46.285.
# Significancia conjunta del cargo: MM2 frente a MM1
anova(mm1, mm2)
| Res.Df | RSS | Df | Sum of Sq | F | Pr(>F) |
|---|---|---|---|---|---|
| 1466 | 1.306465e+16 | NA | NA | NA | NA |
| 1458 | 3.994840e+15 | 8 | 9.06981e+15 | 413.777 | 0 |
Algunos niveles del cargo no son significativos por separado, pero el cargo como variable sí lo es (F = 414, p < 0,001). Con una variable de varias categorías, la significancia se evalúa en conjunto.
La selección se aplica a los dos modelos y, además, a un modelo ampliado que incluye edad y educación, para verificar si el AIC confirma la exclusión decidida con la prueba F.
# Selección paso a paso en ambas direcciones
mm1_step <- step(mm1, direction = "both", trace = 0)
mm2_step <- step(mm2, direction = "both", trace = 0)
# Selección desde el modelo ampliado (con edad y educación)
ampliado <- lm(Ingreso ~ Experiencia + Antiguedad + Edad + Educacion + Cargo + Genero,
data = base)
ampliado_step <- step(ampliado, direction = "both", trace = 1)
## Start: AIC=42113.06
## Ingreso ~ Experiencia + Antiguedad + Edad + Educacion + Cargo +
## Genero
##
## Df Sum of Sq RSS AIC
## - Edad 1 2.0814e+12 3.9911e+15 42112
## - Educacion 1 2.9639e+12 3.9919e+15 42112
## <none> 3.9890e+15 42113
## - Genero 1 9.6308e+12 3.9986e+15 42115
## - Antiguedad 1 1.0558e+13 3.9995e+15 42115
## - Experiencia 1 9.2038e+14 4.9094e+15 42416
## - Cargo 8 9.0371e+15 1.3026e+16 43837
##
## Step: AIC=42111.83
## Ingreso ~ Experiencia + Antiguedad + Educacion + Cargo + Genero
##
## Df Sum of Sq RSS AIC
## - Educacion 1 3.7741e+12 3.9948e+15 42111
## <none> 3.9911e+15 42112
## + Edad 1 2.0814e+12 3.9890e+15 42113
## - Genero 1 9.7006e+12 4.0008e+15 42113
## - Antiguedad 1 1.3108e+13 4.0042e+15 42115
## - Experiencia 1 1.3306e+15 5.3217e+15 42533
## - Cargo 8 9.0622e+15 1.3053e+16 43838
##
## Step: AIC=42111.22
## Ingreso ~ Experiencia + Antiguedad + Cargo + Genero
##
## Df Sum of Sq RSS AIC
## <none> 3.9948e+15 42111
## + Educacion 1 3.7741e+12 3.9911e+15 42112
## + Edad 1 2.8916e+12 3.9919e+15 42112
## - Genero 1 9.8475e+12 4.0047e+15 42113
## - Antiguedad 1 1.3619e+13 4.0085e+15 42114
## - Experiencia 1 1.3332e+15 5.3280e+15 42533
## - Cargo 8 9.0698e+15 1.3065e+16 43837
# Fórmulas finales
formula(mm1_step)
## Ingreso ~ Experiencia + Antiguedad
formula(mm2_step)
## Ingreso ~ Experiencia + Antiguedad + Cargo + Genero
formula(ampliado_step)
## Ingreso ~ Experiencia + Antiguedad + Cargo + Genero
Qué cambia.
Ingreso ~ Experiencia + Antiguedad.El stepwise también se repite dentro de cada partición de la validación cruzada (punto 5), como recomienda el enunciado. Desde el modelo ampliado llegó al MM2 en cuatro de las cinco particiones. En la restante cambió el género por la educación, lo que indica que ambas variables están en el límite.
El diagnóstico se hace sobre el MM2, que es el modelo con mejor desempeño.
# Gráficos de diagnóstico del MM2
par(mfrow = c(2, 2))
plot(mm2, col = adjustcolor(azul, 0.4), pch = 16)
par(mfrow = c(1, 1))
# Linealidad
resettest(mm2)
##
## RESET test
##
## data: mm2
## RESET = 34.966, df1 = 2, df2 = 1456, p-value = 1.472e-15
# Varianza constante
bptest(mm2)
##
## studentized Breusch-Pagan test
##
## data: mm2
## BP = 279.57, df = 11, p-value < 2.2e-16
# Normalidad de los errores
shapiro.test(residuals(mm2))
##
## Shapiro-Wilk normality test
##
## data: residuals(mm2)
## W = 0.99416, p-value = 1.577e-05
# Multicolinealidad (GVIF para variables con varias categorías)
vif(mm2)
## GVIF Df GVIF^(1/(2*Df))
## Experiencia 2.378815 1 1.542341
## Antiguedad 1.670365 1 1.292426
## Cargo 1.808657 8 1.037731
## Genero 1.011522 1 1.005744
# Independencia
durbinWatsonTest(mm2)
## lag Autocorrelation D-W Statistic p-value
## 1 -0.01098858 2.021443 0.716
## Alternative hypothesis: rho != 0
# Observaciones influyentes
cook_mm2 <- cooks.distance(mm2)
c(Mayores_4_n = sum(cook_mm2 > 4 / nrow(base)), Maxima = round(max(cook_mm2), 4))
## Mayores_4_n Maxima
## 113.0000 0.0197
# Dispersión de los residuos por cargo: origen de la heterocedasticidad
data.frame(Cargo = base$Cargo, Residuo = residuals(mm2)) %>%
ggplot(aes(x = reorder(Cargo, Residuo, FUN = sd), y = Residuo)) +
geom_boxplot(fill = azul, alpha = 0.7, color = "grey30", outlier.color = gris) +
geom_hline(yintercept = 0, color = gris, linetype = "dashed") +
coord_flip() +
scale_y_continuous(labels = millones) +
labs(title = "Residuos del MM2 por cargo",
subtitle = "Ordenados de menor a mayor dispersión",
x = NULL, y = "Residuo (ingreso observado − estimado)") +
tema
| Supuesto | Prueba | Resultado | Conclusión |
|---|---|---|---|
| Linealidad | RESET | p < 0,001 | No se cumple del todo. |
| Varianza constante | Breusch-Pagan | BP = 279,6, p < 0,001 | No se cumple: heterocedasticidad por cargo. |
| Normalidad | Shapiro-Wilk | W = 0,994, p < 0,001 | Desviación leve. |
| Multicolinealidad | GVIF | Máximo 2,38 (experiencia) | Sin problema. |
| Independencia | Durbin-Watson | DW = 2,02, p > 0,05 | Se cumple. |
| Influyentes | Distancia de Cook | Máxima = 0,020 | Ninguna observación influyente. |
Linealidad. La prueba RESET sigue rechazando, aunque la curvatura del gráfico de residuos es mucho menor que en el modelo simple: el cargo absorbió los saltos de nivel salarial. Lo que queda indica que el efecto de la experiencia no es exactamente igual en todos los años ni en todos los cargos.
Varianza constante. Es el problema principal, y el gráfico por cargo muestra de dónde viene. En los cargos de menor salario (Representante de Ventas, Investigador Científico, Técnico de Laboratorio) la desviación de los residuos es de 1,0 a 1,2 M. En los demás cargos es de 1,9 a 2,2 M. Consecuencia: los coeficientes siguen siendo insesgados, pero el intervalo de predicción tiene el mismo ancho para todos (unos 6,5 M). Es demasiado amplio para un técnico y demasiado estrecho para un director (ver punto 5).
Normalidad. Con el cargo en el modelo, los residuos son casi normales (W = 0,994). El gráfico Q-Q solo se separa de la diagonal en los extremos. Con 1.470 observaciones, las pruebas t y F son válidas.
Multicolinealidad. Todos los GVIF son menores que 2,4, lejos del umbral de 5. Los coeficientes de experiencia y antigüedad son estables y se pueden interpretar por separado.
Independencia. Durbin-Watson es cercano a 2: cada fila es un empleado distinto y no hay dependencia entre observaciones.
Observaciones influyentes. La distancia de Cook máxima es 0,020. Ningún empleado cambia los resultados por sí solo, lo que respalda la decisión de conservar los salarios atípicos.
La heterocedasticidad justifica probar el logaritmo del ingreso. Con el logaritmo, cada coeficiente pasa a ser un efecto porcentual, y el error crece en proporción al salario en lugar de ser un monto fijo.
# MM2 con logaritmo del ingreso
mm2_log <- lm(log(Ingreso) ~ Experiencia + Antiguedad + Cargo + Genero, data = base)
summary(mm2_log)
##
## Call:
## lm(formula = log(Ingreso) ~ Experiencia + Antiguedad + Cargo +
## Genero, data = base)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.91136 -0.19016 -0.00042 0.19026 0.95805
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 14.698050 0.022542 652.039 < 2e-16 ***
## Experiencia 0.026863 0.001453 18.486 < 2e-16 ***
## Antiguedad 0.004199 0.001546 2.715 0.00670 **
## CargoDirector_Investigación 1.249595 0.039880 31.334 < 2e-16 ***
## CargoDirector_Manofactura 0.661772 0.029940 22.103 < 2e-16 ***
## CargoEjecutivo_Ventas 0.663894 0.023843 27.845 < 2e-16 ***
## CargoGerente 1.225932 0.039175 31.294 < 2e-16 ***
## CargoInvestigador_Cientifico -0.002849 0.024001 -0.119 0.90554
## CargoRecursos_Humanos 0.174528 0.042703 4.087 4.61e-05 ***
## CargoRepresentante_Salud 0.662313 0.031192 21.234 < 2e-16 ***
## CargoRepresentante_Ventas -0.109106 0.035710 -3.055 0.00229 **
## GeneroM 0.009571 0.015045 0.636 0.52477
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.281 on 1458 degrees of freedom
## Multiple R-squared: 0.8225, Adjusted R-squared: 0.8212
## F-statistic: 614.3 on 11 and 1458 DF, p-value: < 2.2e-16
# Efectos en porcentaje: (exp(b) - 1) * 100
round((exp(coef(mm2_log)[-1]) - 1) * 100, 1)
## Experiencia Antiguedad
## 2.7 0.4
## CargoDirector_Investigación CargoDirector_Manofactura
## 248.9 93.8
## CargoEjecutivo_Ventas CargoGerente
## 94.2 240.7
## CargoInvestigador_Cientifico CargoRecursos_Humanos
## -0.3 19.1
## CargoRepresentante_Salud CargoRepresentante_Ventas
## 93.9 -10.3
## GeneroM
## 1.0
# Supuestos del modelo transformado
bptest(mm2_log)
##
## studentized Breusch-Pagan test
##
## data: mm2_log
## BP = 125.78, df = 11, p-value < 2.2e-16
shapiro.test(residuals(mm2_log))
##
## Shapiro-Wilk normality test
##
## data: residuals(mm2_log)
## W = 0.99861, p-value = 0.2911
resettest(mm2_log)
##
## RESET test
##
## data: mm2_log
## RESET = 22.171, df1 = 2, df2 = 1456, p-value = 3.274e-10
Interpretación del modelo transformado.
Efecto sobre los supuestos.
El R² y el AIC del modelo en logaritmo no se comparan con los del modelo original, porque la respuesta está en otra escala. La comparación se hace con el MAE en pesos.
Todos los modelos se evalúan con las mismas cinco particiones del Punto 1. En los modelos con selección paso a paso, el stepwise se aplica dentro de cada partición de entrenamiento. En los modelos en logaritmo, la predicción se devuelve a pesos antes de calcular el error. Además del MAE, se mide la cobertura del intervalo de predicción del 95 %: el porcentaje de empleados de prueba cuyo salario real queda dentro del intervalo.
# Validación cruzada general: ajusta en entrenamiento y evalúa en prueba
# ajuste: función que recibe los datos de entrenamiento y devuelve un modelo lm
cv_modelo <- function(ajuste, logaritmo = FALSE) {
sapply(1:k, function(i) {
entrenamiento <- base[particion != i, ]
prueba <- base[particion == i, ]
modelo <- ajuste(entrenamiento)
p <- predict(modelo, newdata = prueba, interval = "prediction", level = 0.95)
if (logaritmo) p <- exp(p) # volver a pesos
c(MAE = mean(abs(prueba$Ingreso - p[, "fit"])),
Cobertura = mean(prueba$Ingreso >= p[, "lwr"] & prueba$Ingreso <= p[, "upr"]))
})
}
modelos <- list(
"S1: Experiencia" = function(d) lm(Ingreso ~ Experiencia, data = d),
"S2: Antigüedad" = function(d) lm(Ingreso ~ Antiguedad, data = d),
"S3: Género" = function(d) lm(Ingreso ~ Genero, data = d),
"MM1: sin cargo" = function(d) lm(Ingreso ~ Experiencia + Antiguedad + Genero, data = d),
"MM1 stepwise" = function(d) step(lm(Ingreso ~ Experiencia + Antiguedad + Genero,
data = d), trace = 0),
"MM2: con cargo" = function(d) lm(Ingreso ~ Experiencia + Antiguedad + Cargo + Genero,
data = d),
"MM2 stepwise (ampliado)" = function(d) step(lm(Ingreso ~ Experiencia + Antiguedad + Edad +
Educacion + Cargo + Genero, data = d),
trace = 0)
)
# Modelos en pesos
cv_pesos <- lapply(modelos, cv_modelo)
# Modelos en logaritmo
cv_log <- list(
"S1 log" = cv_modelo(function(d) lm(log(Ingreso) ~ Experiencia, data = d), logaritmo = TRUE),
"MM2 log" = cv_modelo(function(d) lm(log(Ingreso) ~ Experiencia + Antiguedad + Cargo + Genero,
data = d), logaritmo = TRUE)
)
todos <- c(cv_pesos, cv_log)
comparacion <- data.frame(
Modelo = c("Referencia: mediana", names(todos)),
MAE = c(mean(mae_mediana), sapply(todos, function(x) mean(x["MAE", ]))),
Desv_Est = c(sd(mae_mediana), sapply(todos, function(x) sd(x["MAE", ]))),
Cobertura = c(NA, sapply(todos, function(x) mean(x["Cobertura", ])))
) %>%
mutate(Mejora_vs_mediana = 1 - MAE / mean(mae_mediana)) %>%
arrange(MAE)
rownames(comparacion) <- NULL
kable(comparacion, digits = c(0, 0, 0, 3, 3), format.args = list(big.mark = "."),
caption = "Comparación de todos los modelos en validación cruzada (MAE en pesos)")
| Modelo | MAE | Desv_Est | Cobertura | Mejora_vs_mediana |
|---|---|---|---|---|
| MM2: con cargo | 1.304.404 | 37.467 | 0.950 | 0.610 |
| MM2 stepwise (ampliado) | 1.307.409 | 41.285 | 0.948 | 0.609 |
| MM2 log | 1.331.006 | 61.774 | 0.958 | 0.602 |
| S1: Experiencia | 2.218.163 | 140.867 | 0.931 | 0.337 |
| MM1: sin cargo | 2.220.597 | 133.745 | 0.929 | 0.337 |
| MM1 stepwise | 2.221.129 | 137.586 | 0.927 | 0.337 |
| S1 log | 2.300.671 | 192.244 | 0.958 | 0.313 |
| S2: Antigüedad | 2.963.646 | 135.804 | 0.938 | 0.115 |
| Referencia: mediana | 3.347.858 | 207.527 | NA | 0.000 |
| S3: Género | 3.644.874 | 173.204 | 0.913 | -0.089 |
# MAE de todos los modelos
ggplot(comparacion, aes(x = reorder(Modelo, -MAE), y = MAE)) +
geom_col(fill = ifelse(comparacion$Modelo == "MM2: con cargo", azul, gris)) +
geom_text(aes(label = millones(MAE)), hjust = -0.1, size = 3.5) +
coord_flip() +
scale_y_continuous(labels = millones, expand = expansion(mult = c(0, 0.15))) +
labs(title = "Error absoluto medio en validación cruzada",
subtitle = "Mismas cinco particiones para todos los modelos",
x = NULL, y = "MAE") +
tema
Resultados.
# Cobertura y ancho del intervalo del 95 % por cargo, en validación cruzada
detalle <- do.call(rbind, lapply(1:k, function(i) {
entrenamiento <- base[particion != i, ]
prueba <- base[particion == i, ]
p_lin <- predict(lm(Ingreso ~ Experiencia + Antiguedad + Cargo + Genero, data = entrenamiento),
newdata = prueba, interval = "prediction")
p_log <- exp(predict(lm(log(Ingreso) ~ Experiencia + Antiguedad + Cargo + Genero,
data = entrenamiento), newdata = prueba, interval = "prediction"))
data.frame(Cargo = prueba$Cargo,
Real = prueba$Ingreso,
Pred = p_lin[, "fit"],
Cub_pesos = prueba$Ingreso >= p_lin[, "lwr"] & prueba$Ingreso <= p_lin[, "upr"],
Cub_log = prueba$Ingreso >= p_log[, "lwr"] & prueba$Ingreso <= p_log[, "upr"],
Limite_negativo = p_lin[, "lwr"] < 0)
}))
detalle %>%
group_by(Cargo) %>%
summarise(n = n(),
Cobertura_pesos = mean(Cub_pesos),
Cobertura_log = mean(Cub_log),
Limite_inferior_negativo = mean(Limite_negativo)) %>%
arrange(Cobertura_pesos) %>%
kable(digits = 3, caption = "Cobertura del intervalo de predicción del 95 % por cargo")
| Cargo | n | Cobertura_pesos | Cobertura_log | Limite_inferior_negativo |
|---|---|---|---|---|
| Director_Manofactura | 145 | 0.890 | 0.979 | 0.000 |
| Director_Investigación | 80 | 0.900 | 1.000 | 0.000 |
| Recursos_Humanos | 52 | 0.904 | 0.865 | 0.135 |
| Representante_Salud | 131 | 0.916 | 0.977 | 0.000 |
| Ejecutivo_Ventas | 326 | 0.923 | 0.985 | 0.000 |
| Gerente | 102 | 0.951 | 1.000 | 0.000 |
| Investigador_Cientifico | 292 | 0.990 | 0.942 | 0.589 |
| Tecnico_Laboratorio | 259 | 0.996 | 0.923 | 0.556 |
| Representante_Ventas | 83 | 1.000 | 0.916 | 0.819 |
Qué muestra la cobertura por cargo.
Elección del mejor modelo: MM2 en pesos.
Para los intervalos, conviene usar la versión en logaritmo en los cargos de menor salario, donde el modelo en pesos da límites imposibles.
Se plantean dos casos realistas para Recursos Humanos:
# Perfiles hipotéticos
escenarios_mm2 <- data.frame(
Experiencia = c(3, 22),
Antiguedad = c(1, 10),
Cargo = factor(c("Tecnico_Laboratorio", "Gerente"), levels = levels(base$Cargo)),
Genero = factor(c("F", "M"), levels = levels(base$Genero))
)
# Predicción e intervalo del 95 %: MM2 en pesos y en logaritmo
p_pesos <- predict(mm2, newdata = escenarios_mm2, interval = "prediction", level = 0.95)
p_log <- exp(predict(mm2_log, newdata = escenarios_mm2, interval = "prediction", level = 0.95))
data.frame(
Escenario = rep(c("A: Técnica junior", "B: Gerente senior"), 2),
Modelo = rep(c("MM2 pesos", "MM2 log"), each = 2),
Prediccion = c(p_pesos[, "fit"], p_log[, "fit"]),
Limite_inf = c(p_pesos[, "lwr"], p_log[, "lwr"]),
Limite_sup = c(p_pesos[, "upr"], p_log[, "upr"])
) %>%
kable(digits = 0, format.args = list(big.mark = "."),
caption = "Predicciones e intervalos de predicción del 95 % (pesos)")
| Escenario | Modelo | Prediccion | Limite_inf | Limite_sup |
|---|---|---|---|---|
| A: Técnica junior | MM2 pesos | 2.163.341 | -1.092.912 | 5.419.594 |
| B: Gerente senior | MM2 pesos | 16.687.739 | 13.423.200 | 19.952.278 |
| A: Técnica junior | MM2 log | 2.630.903 | 1.513.759 | 4.572.491 |
| B: Gerente senior | MM2 log | 15.658.524 | 8.996.880 | 27.252.711 |
# Referencia real: salarios observados en perfiles parecidos
base %>%
filter((Cargo == "Tecnico_Laboratorio" & Experiencia <= 5) |
(Cargo == "Gerente" & Experiencia >= 18 & Experiencia <= 26)) %>%
group_by(Cargo) %>%
summarise(n = n(), Mediana = median(Ingreso), Minimo = min(Ingreso), Maximo = max(Ingreso))
| Cargo | n | Mediana | Minimo | Maximo |
|---|---|---|---|---|
| Tecnico_Laboratorio | 88 | 2519000 | 1102000 | 4841000 |
| Gerente | 54 | 18051000 | 14756000 | 19926000 |
Escenario A (Técnica junior).
Escenario B (Gerente senior).
# Error del MM2 por cargo en validación cruzada, absoluto y relativo a la mediana del cargo
error_cargo <- detalle %>%
group_by(Cargo) %>%
summarise(n = n(),
MAE = mean(abs(Real - Pred)),
Mediana_ingreso = median(Real),
Error_relativo = MAE / Mediana_ingreso,
Sesgo_medio = mean(Pred - Real)) %>%
arrange(Error_relativo)
kable(error_cargo, digits = c(0, 0, 0, 0, 3, 0), format.args = list(big.mark = "."),
caption = "Error de predicción del MM2 por cargo (validación cruzada)")
| Cargo | n | MAE | Mediana_ingreso | Error_relativo | Sesgo_medio |
|---|---|---|---|---|---|
| Gerente | 102 | 1.601.940 | 17.454.500 | 0.092 | 11.240 |
| Director_Investigación | 80 | 1.951.305 | 16.510.000 | 0.118 | 8.782 |
| Representante_Salud | 131 | 1.609.033 | 6.811.000 | 0.236 | 27.124 |
| Ejecutivo_Ventas | 326 | 1.604.476 | 6.231.000 | 0.257 | 909 |
| Representante_Ventas | 83 | 666.557 | 2.579.000 | 0.258 | 6.344 |
| Director_Manofactura | 145 | 1.695.786 | 6.447.000 | 0.263 | 4.642 |
| Investigador_Cientifico | 292 | 842.882 | 2.887.500 | 0.292 | 882 |
| Tecnico_Laboratorio | 259 | 912.448 | 2.886.000 | 0.316 | -2.392 |
| Recursos_Humanos | 52 | 1.547.504 | 3.093.000 | 0.500 | 2.937 |
# Proporción de predicciones que se equivocan en más de 2 millones
mean(abs(detalle$Real - detalle$Pred) > 2e6)
## [1] 0.229932
# Empleados actuales cuyo salario queda fuera del intervalo del 95 %
fuera <- predict(mm2, interval = "prediction")
fuera_rango <- base$Ingreso < fuera[, "lwr"] | base$Ingreso > fuera[, "upr"]
sum(fuera_rango)
## [1] 72
# Error relativo por cargo
ggplot(error_cargo, aes(x = reorder(Cargo, -Error_relativo), y = Error_relativo)) +
geom_col(fill = azul) +
geom_text(aes(label = percent(Error_relativo, accuracy = 1)), hjust = -0.1, size = 3.5) +
coord_flip() +
scale_y_continuous(labels = percent, expand = expansion(mult = c(0, 0.15))) +
labs(title = "Error del MM2 como porcentaje del salario mediano del cargo",
x = NULL, y = "MAE / mediana del cargo") +
tema
El área de Recursos Humanos, para tres decisiones:
Limitaciones.
No debería usarse para:
Sí, como herramienta de referencia y con condiciones:
Información adicional que lo mejoraría:
Recomendación: aplicar el modelo como apoyo a Recursos Humanos para definir rangos salariales y priorizar revisiones, siempre con revisión humana, sin usar el género como criterio y reestimándolo con información de mercado y de desempeño.