Los seguros de salud necesitan saber, con la mayor precisión posible, cuánto les va a costar atender a cada persona asegurada. Con esa información fijan el precio de las pólizas. Algunas características de las personas, como la edad, el peso o fumar, pueden hacer que una persona use más servicios médicos y, por lo tanto, genere costos más altos.
En este trabajo usamos una base de datos de 1.338 personas aseguradas en Estados Unidos para responder la siguiente pregunta:
Pregunta de investigación: ¿Qué características de una persona (edad, sexo, índice de masa corporal, número de hijos, si fuma y región donde vive) están relacionadas con los costos médicos que le factura el seguro de salud, y qué tan fuerte es esa relación?
Objetivo de modelación: construir un modelo de regresión lineal múltiple que explique los costos médicos anuales de una persona a partir de esas características. Con el modelo queremos saber cuáles de ellas pesan más y cuánto cambian los costos cuando cambia cada una.
Primero cargamos las librerías y la base de datos:
# Cargar librerias
library(tidyverse) # incluye dplyr, ggplot2, tidyr y readr
library(knitr)
library(kableExtra)
library(plotly)
library(ggcorrplot)
library(broom)
library(lmtest)
library(car)
Data <- read.csv("insurance.csv")
head(Data, 10) %>% kable(caption = "Tabla 1. Primeras 10 filas de la base original") %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)| age | sex | bmi | children | smoker | region | charges |
|---|---|---|---|---|---|---|
| 19 | female | 27.900 | 0 | yes | southwest | 16884.924 |
| 18 | male | 33.770 | 1 | no | southeast | 1725.552 |
| 28 | male | 33.000 | 3 | no | southeast | 4449.462 |
| 33 | male | 22.705 | 0 | no | northwest | 21984.471 |
| 32 | male | 28.880 | 0 | no | northwest | 3866.855 |
| 31 | female | 25.740 | 0 | no | southeast | 3756.622 |
| 46 | female | 33.440 | 1 | no | southeast | 8240.590 |
| 37 | female | 27.740 | 3 | no | northwest | 7281.506 |
| 37 | male | 29.830 | 2 | no | northeast | 6406.411 |
| 60 | female | 25.840 | 0 | no | northwest | 28923.137 |
Antes de analizar los datos revisamos si hay problemas de calidad: datos faltantes (celdas vacías) y filas repetidas.
# ¿Cuántos datos faltantes hay en cada columna?
faltantes <- data.frame(Variable = names(Data),
Faltantes = colSums(is.na(Data)),
row.names = NULL)
faltantes %>% kable(caption = "Tabla 2. Datos faltantes por variable") %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)| Variable | Faltantes |
|---|---|
| age | 0 |
| sex | 0 |
| bmi | 0 |
| children | 0 |
| smoker | 0 |
| region | 0 |
| charges | 0 |
# ¿Hay filas repetidas?
n_duplicados <- sum(duplicated(Data))
Data[duplicated(Data) | duplicated(Data, fromLast = TRUE), ] %>%
kable(caption = "Tabla 3. Filas repetidas encontradas") %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)| age | sex | bmi | children | smoker | region | charges | |
|---|---|---|---|---|---|---|---|
| 196 | 19 | male | 30.59 | 0 | no | northwest | 1639.563 |
| 582 | 19 | male | 30.59 | 0 | no | northwest | 1639.563 |
Qué encontramos:
Después traducimos los nombres y categorías al español y convertimos las variables de texto en factores, que es como R maneja las variables categóricas. En cada variable categórica elegimos una categoría de referencia, que es el grupo contra el que se comparan los demás en el modelo:
base_limpia <- Data %>%
distinct() %>% # quitar la fila repetida
rename(Edad = age, Sexo = sex, IMC = bmi, Hijos = children,
Fuma = smoker, Region = region, Costos = charges) %>%
mutate(
Sexo = factor(dplyr::recode(Sexo, female = "Mujer", male = "Hombre"),
levels = c("Mujer", "Hombre")),
Fuma = factor(dplyr::recode(Fuma, no = "No", yes = "Sí"),
levels = c("No", "Sí")),
Region = factor(dplyr::recode(Region, northeast = "Noreste", northwest = "Noroeste",
southeast = "Sureste", southwest = "Suroeste"),
levels = c("Noreste", "Noroeste", "Sureste", "Suroeste"))
)
str(base_limpia)## 'data.frame': 1337 obs. of 7 variables:
## $ Edad : int 19 18 28 33 32 31 46 37 37 60 ...
## $ Sexo : Factor w/ 2 levels "Mujer","Hombre": 1 2 2 2 2 1 1 1 2 1 ...
## $ IMC : num 27.9 33.8 33 22.7 28.9 ...
## $ Hijos : int 0 1 3 0 0 0 1 3 2 0 ...
## $ Fuma : Factor w/ 2 levels "No","Sí": 2 1 1 1 1 1 1 1 1 1 ...
## $ Region: Factor w/ 4 levels "Noreste","Noroeste",..: 4 3 3 2 2 3 3 2 1 2 ...
## $ Costos: num 16885 1726 4449 21984 3867 ...
La base final tiene 1337 personas y 7 variables. Este es el archivo que usamos en todo el análisis. Lo guardamos para entregarlo junto con el informe:
data.frame(
Variable = c("Costos", "Edad", "Sexo", "IMC", "Hijos", "Fuma", "Region"),
Papel = c("Dependiente (Y)", rep("Explicativa (X)", 6)),
Tipo = c("Cuantitativa continua", "Cuantitativa discreta", "Categórica (2 grupos)",
"Cuantitativa continua", "Cuantitativa discreta", "Categórica (2 grupos)",
"Categórica (4 grupos)"),
Unidad = c("Dólares (USD) al año", "Años", "Mujer / Hombre", "kg/m²",
"Número de hijos", "Sí / No", "Noreste / Noroeste / Sureste / Suroeste"),
Descripcion = c("Costos médicos individuales facturados por el seguro",
"Edad del beneficiario principal",
"Sexo del titular del seguro",
"Índice de masa corporal (peso / estatura²). Lo ideal está entre 18,5 y 24,9",
"Hijos o dependientes cubiertos por el seguro",
"Indica si la persona fuma",
"Zona de Estados Unidos donde vive")
) %>%
kable(caption = "Tabla 4. Variables utilizadas") %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = TRUE)| Variable | Papel | Tipo | Unidad | Descripcion |
|---|---|---|---|---|
| Costos | Dependiente (Y) | Cuantitativa continua | Dólares (USD) al año | Costos médicos individuales facturados por el seguro |
| Edad | Explicativa (X) | Cuantitativa discreta | Años | Edad del beneficiario principal |
| Sexo | Explicativa (X) | Categórica (2 grupos) | Mujer / Hombre | Sexo del titular del seguro |
| IMC | Explicativa (X) | Cuantitativa continua | kg/m² | Índice de masa corporal (peso / estatura²). Lo ideal está entre 18,5 y 24,9 |
| Hijos | Explicativa (X) | Cuantitativa discreta | Número de hijos | Hijos o dependientes cubiertos por el seguro |
| Fuma | Explicativa (X) | Categórica (2 grupos) | Sí / No | Indica si la persona fuma |
| Region | Explicativa (X) | Categórica (4 grupos) | Noreste / Noroeste / Sureste / Suroeste | Zona de Estados Unidos donde vive |
¿Por qué elegimos estas variables? Usamos todas las que trae la base, porque cada una tiene una razón lógica para relacionarse con los costos médicos:
No descartamos ninguna variable al principio. Dejamos que el modelo muestre cuáles son importantes y cuáles no.
Usamos un modelo de regresión lineal múltiple. En palabras simples, es una fórmula que intenta “predecir” los costos sumando el efecto de cada característica de la persona. La ecuación es:
\[ \text{Costos}_i = \beta_0 + \beta_1\,\text{Edad}_i + \beta_2\,\text{Hombre}_i + \beta_3\,\text{IMC}_i + \beta_4\,\text{Hijos}_i + \beta_5\,\text{Fuma}_i + \beta_6\,\text{Noroeste}_i + \beta_7\,\text{Sureste}_i + \beta_8\,\text{Suroeste}_i + \varepsilon_i \]
Donde:
base_limpia %>%
summarise(Media = round(mean(Costos), 2),
Mediana = round(median(Costos), 2),
D.Estandar = round(sd(Costos), 2),
Min = round(min(Costos), 2),
Max = round(max(Costos), 2)) %>%
kable(caption = "Tabla 5. Resumen de los costos médicos (USD)") %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)| Media | Mediana | D.Estandar | Min | Max |
|---|---|---|---|---|
| 13279.12 | 9386.16 | 12110.36 | 1121.87 | 63770.43 |
p1 <- ggplot(base_limpia, aes(x = Costos)) +
geom_histogram(bins = 40, fill = "#2c7fb8", color = "white") +
geom_vline(aes(xintercept = mean(Costos)), color = "red", linetype = "dashed") +
geom_vline(aes(xintercept = median(Costos)), color = "darkgreen", linetype = "dashed") +
labs(title = "Distribución de los costos médicos",
subtitle = "Línea roja = media, línea verde = mediana",
x = "Costos (USD)", y = "Número de personas") +
theme_minimal()
ggplotly(p1)¿Qué vemos? La mayoría de las personas tiene costos bajos, entre 1.000 y 15.000 dólares. Unas pocas tienen costos muy altos, de más de 40.000 dólares. Por eso el gráfico tiene una “cola larga” hacia la derecha. La media (13.279 USD) es mayor que la mediana (9.386 USD) porque esos pocos casos muy caros “jalan” el promedio hacia arriba.
q1 <- quantile(base_limpia$Costos, 0.25)
q3 <- quantile(base_limpia$Costos, 0.75)
lim <- q3 + 1.5 * (q3 - q1)
atipicos <- base_limpia %>% filter(Costos > lim)
atipicos %>% count(Fuma) %>%
mutate(Porcentaje = round(n / sum(n) * 100, 2)) %>%
kable(caption = "Tabla 6. Personas con costos atípicos según si fuman",
col.names = c("¿Fuma?", "Número de personas", "Porcentaje (%)")) %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)| ¿Fuma? | Número de personas | Porcentaje (%) |
|---|---|---|
| No | 3 | 2.16 |
| Sí | 136 | 97.84 |
Con la regla del boxplot (valores por encima de Q3 + 1,5 × rango intercuartílico) hay 139 personas con costos atípicos, es decir, por encima de 34.525 USD. La gran mayoría de ellas son fumadoras. Esto indica que no son errores en los datos sino casos reales: son personas con más riesgo que de verdad le cuestan más al seguro. Por eso decidimos no eliminarlas. Quitarlas haría que el modelo ignorara justamente a las personas más costosas.
base_limpia %>%
select(Edad, IMC, Hijos) %>%
pivot_longer(everything(), names_to = "Variable", values_to = "Valor") %>%
group_by(Variable) %>%
summarise(Media = round(mean(Valor), 2),
Mediana = round(median(Valor), 2),
D.Estandar = round(sd(Valor), 2),
Min = round(min(Valor), 2),
Max = round(max(Valor), 2)) %>%
kable(caption = "Tabla 7. Resumen de las variables cuantitativas") %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)| Variable | Media | Mediana | D.Estandar | Min | Max |
|---|---|---|---|---|---|
| Edad | 39.22 | 39.0 | 14.04 | 18.00 | 64.00 |
| Hijos | 1.10 | 1.0 | 1.21 | 0.00 | 5.00 |
| IMC | 30.66 | 30.4 | 6.10 | 15.96 | 53.13 |
base_limpia %>%
select(Edad, IMC, Hijos) %>%
pivot_longer(everything(), names_to = "Variable", values_to = "Valor") %>%
ggplot(aes(x = Valor)) +
geom_histogram(bins = 25, fill = "#41ab5d", color = "white") +
facet_wrap(~Variable, scales = "free") +
labs(title = "Distribución de Edad, Hijos e IMC", x = "", y = "Número de personas") +
theme_minimal()¿Qué vemos?
# TABLA DE FRECUENCIAS PARA LAS VARIABLES CATEGÓRICAS (DUMMIES)
bind_rows(
base_limpia %>% count(Categoria = Fuma) %>% mutate(Variable = "Fuma"),
base_limpia %>% count(Categoria = Sexo) %>% mutate(Variable = "Sexo"),
base_limpia %>% count(Categoria = Region) %>% mutate(Variable = "Region")
) %>%
group_by(Variable) %>%
mutate(Porcentaje = round(n / sum(n) * 100, 2)) %>%
ungroup() %>%
select(Variable, Categoria, n, Porcentaje) %>%
kable(caption = "Tabla 8. Frecuencias de las variables categóricas",
col.names = c("Variable", "Categoría", "Frecuencia (n)", "Participación (%)")) %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)| Variable | Categoría | Frecuencia (n) | Participación (%) |
|---|---|---|---|
| Fuma | No | 1063 | 79.51 |
| Fuma | Sí | 274 | 20.49 |
| Sexo | Mujer | 662 | 49.51 |
| Sexo | Hombre | 675 | 50.49 |
| Region | Noreste | 324 | 24.23 |
| Region | Noroeste | 324 | 24.23 |
| Region | Sureste | 364 | 27.23 |
| Region | Suroeste | 325 | 24.31 |
base_limpia %>%
select(Fuma, Sexo, Region) %>%
pivot_longer(everything(), names_to = "Variable", values_to = "Categoria") %>%
ggplot(aes(x = Categoria, fill = Variable)) +
geom_bar() +
geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.5) +
facet_wrap(~Variable, scales = "free_x") +
labs(title = "Distribución de personas por categoría", x = "", y = "Cantidad de personas") +
theme_minimal() +
theme(legend.position = "none")¿Qué vemos? Hombres y mujeres están casi en la misma proporción, y las cuatro regiones también tienen tamaños parecidos. En cambio, solo una de cada cinco personas fuma (20.5%).
tabla_fuma <- base_limpia %>%
group_by(Fuma) %>%
summarise(n = n(),
Media = round(mean(Costos), 2),
Mediana = round(median(Costos), 2),
D.Estandar = round(sd(Costos), 2))
tabla_fuma %>%
kable(caption = "Tabla 9. Costos médicos (USD) según si la persona fuma") %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)| Fuma | n | Media | Mediana | D.Estandar |
|---|---|---|---|---|
| No | 1063 | 8440.66 | 7345.73 | 5992.97 |
| Sí | 274 | 32050.23 | 34456.35 | 11541.55 |
p2 <- ggplot(base_limpia, aes(x = Fuma, y = Costos, fill = Fuma)) +
geom_boxplot() +
labs(title = "Costos médicos según si la persona fuma", x = "¿Fuma?", y = "Costos (USD)") +
theme_minimal() + theme(legend.position = "none")
ggplotly(p2)base_limpia %>%
select(Costos, Sexo, Region) %>%
pivot_longer(c(Sexo, Region), names_to = "Variable", values_to = "Categoria") %>%
ggplot(aes(x = Categoria, y = Costos, fill = Categoria)) +
geom_boxplot() +
facet_wrap(~Variable, scales = "free_x") +
labs(title = "Costos médicos según sexo y región", x = "", y = "Costos (USD)") +
theme_minimal() + theme(legend.position = "none")¿Qué vemos? La diferencia más grande, por mucho, es entre fumadores y no fumadores. Un fumador cuesta en promedio 32.050 USD al año, frente a 8.441 USD de un no fumador. Es casi 3.8 veces más. Entre hombres y mujeres, y entre regiones, las cajas se ven muy parecidas, así que esas diferencias parecen pequeñas.
p3 <- ggplot(base_limpia, aes(x = Edad, y = Costos, color = Fuma)) +
geom_point(size = 2, alpha = 0.6) +
geom_smooth(method = "lm", se = TRUE, color = "gray40") +
labs(title = "Edad vs Costos médicos", x = "Edad (años)", y = "Costos (USD)") +
theme_minimal()
ggplotly(p3)Edad: los costos suben a medida que la persona es mayor. Además se ven tres “franjas”: la de abajo son casi todos no fumadores, la de arriba casi todos fumadores y la del medio es mixta. En las tres franjas los costos crecen con la edad.
p4 <- ggplot(base_limpia, aes(x = IMC, y = Costos, color = Fuma)) +
geom_point(size = 2, alpha = 0.6) +
geom_smooth(method = "lm", se = TRUE, color = "gray40") +
geom_vline(xintercept = 30, linetype = "dashed") +
labs(title = "IMC vs Costos médicos (línea punteada = obesidad, IMC 30)",
x = "IMC (kg/m²)", y = "Costos (USD)") +
theme_minimal()
ggplotly(p4)IMC: en los no fumadores (puntos rojos) el IMC casi no cambia los costos. En los fumadores (puntos turquesa) se ve un “salto” muy claro: los fumadores con obesidad (IMC mayor a 30) tienen costos muchísimo más altos que los fumadores sin obesidad. Es decir, fumar y la obesidad juntos son una combinación especialmente cara.
p5 <- ggplot(base_limpia, aes(x = factor(Hijos), y = Costos, fill = factor(Hijos))) +
geom_boxplot() +
labs(title = "Costos médicos según número de hijos", x = "Número de hijos", y = "Costos (USD)") +
theme_minimal() + theme(legend.position = "none")
ggplotly(p5)Hijos: no se ve una tendencia clara. Los costos son parecidos sin importar el número de hijos.
La correlación mide qué tanto se mueven juntas dos variables. Va de -1 a 1: cerca de 1 es una relación positiva fuerte, cerca de 0 es que casi no hay relación. Para incluir “Fuma” la convertimos en 1 = fuma y 0 = no fuma.
# Matriz de correlaciones con cor() base de R
matriz_cor <- base_limpia %>%
mutate(Fuma = ifelse(Fuma == "Sí", 1, 0)) %>%
select(Costos, Edad, IMC, Hijos, Fuma) %>%
cor(use = "complete.obs")
round(matriz_cor, 3) %>%
kable(caption = "Tabla 10. Matriz de correlaciones") %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)| Costos | Edad | IMC | Hijos | Fuma | |
|---|---|---|---|---|---|
| Costos | 1.000 | 0.298 | 0.198 | 0.067 | 0.787 |
| Edad | 0.298 | 1.000 | 0.109 | 0.042 | -0.026 |
| IMC | 0.198 | 0.109 | 1.000 | 0.013 | 0.004 |
| Hijos | 0.067 | 0.042 | 0.013 | 1.000 | 0.007 |
| Fuma | 0.787 | -0.026 | 0.004 | 0.007 | 1.000 |
p6 <- ggcorrplot(matriz_cor,
type = "lower",
lab = TRUE,
lab_size = 3,
colors = c("#d73027", "white", "#1a9850"),
title = "Correlación entre las variables")
ggplotly(p6)¿Qué vemos?
modelo <- lm(Costos ~ Edad + Sexo + IMC + Hijos + Fuma + Region, data = base_limpia)
datasummary <- as.data.frame(summary(modelo)$coefficients)
colnames(datasummary) <- c("Estimado", "Error estándar", "t valor", "p-valor")
datasummary$Significativo <- ifelse(datasummary$`p-valor` < 0.05, "Sí", "No")
datasummary %>%
mutate(across(where(is.numeric), ~ round(.x, 4))) %>%
kable(caption = "Tabla 11. Coeficientes del modelo (variable dependiente: Costos en USD)") %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = TRUE)| Estimado | Error estándar | t valor | p-valor | Significativo | |
|---|---|---|---|---|---|
| (Intercept) | -11936.5575 | 988.2274 | -12.0788 | 0.0000 | Sí |
| Edad | 256.7646 | 11.9122 | 21.5547 | 0.0000 | Sí |
| SexoHombre | -129.4815 | 333.1952 | -0.3886 | 0.6976 | No |
| IMC | 339.2504 | 28.6113 | 11.8572 | 0.0000 | Sí |
| Hijos | 474.8205 | 137.8969 | 3.4433 | 0.0006 | Sí |
| FumaSí | 23847.3288 | 413.3479 | 57.6931 | 0.0000 | Sí |
| RegionNoroeste | -349.2265 | 476.8238 | -0.7324 | 0.4641 | No |
| RegionSureste | -1035.2656 | 478.8670 | -2.1619 | 0.0308 | Sí |
| RegionSuroeste | -960.0814 | 478.1059 | -2.0081 | 0.0448 | Sí |
La ecuación estimada queda así (valores redondeados):
\[ \widehat{\text{Costos}} = -11.937 + 256,8\,\text{Edad} -129,5\,\text{Hombre} + 339,3\,\text{IMC} + 474,8\,\text{Hijos} + 23.847,3\,\text{Fuma} -349,2\,\text{Noroeste} -1.035,3\,\text{Sureste} -960,1\,\text{Suroeste} \]
Decimos que una variable es significativa cuando su p-valor es menor a 0,05. Eso quiere decir que es muy poco probable que su efecto se deba a la casualidad. Todas las interpretaciones son “manteniendo todo lo demás igual”: comparamos dos personas que solo se diferencian en esa característica.
data.frame(
Medida = c("R²", "R² ajustado", "Estadístico F", "p-valor (prueba F)", "Error estándar residual (USD)"),
Valor = c(round(ajuste$r.squared, 4), round(ajuste$adj.r.squared, 4),
round(ajuste$statistic, 2), format.pval(ajuste$p.value, digits = 4),
round(ajuste$sigma, 2))
) %>%
kable(caption = "Tabla 12. Medidas de ajuste del modelo") %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)| Medida | Valor |
|---|---|
| R² | 0.7507 |
| R² ajustado | 0.7492 |
| Estadístico F | 499.99 |
| p-valor (prueba F) | < 0.00000000000000022 |
| Error estándar residual (USD) | 6064.3 |
Para que las conclusiones de una regresión sean confiables, el modelo debe cumplir algunos supuestos (condiciones). Los revisamos uno por uno.
Este supuesto pide que la relación entre los costos y cada variable sea más o menos una línea recta. Lo revisamos con el gráfico de residuos vs valores ajustados. Los residuos son la diferencia entre el costo real y el que predice el modelo. Si el supuesto se cumple, los puntos deberían verse como una nube sin forma, alrededor de la línea en 0.
# Gráficos de componentes + residuos para las variables cuantitativas
crPlots(modelo, terms = ~ Edad + IMC + Hijos, layout = c(1, 3))Resultado: los residuos no forman una nube sin forma. Se ven grupos separados y una curva. Esto indica que la relación no es del todo lineal. La razón principal la vimos en los gráficos de dispersión: el IMC afecta mucho más a los fumadores que a los no fumadores, y el modelo actual trata ese efecto como si fuera igual para todos. En los gráficos de componentes, la edad y el IMC sí muestran una tendencia razonablemente recta por sí solas.
Los errores del modelo deberían repartirse como una campana (distribución normal). Lo revisamos con la prueba de Shapiro-Wilk y con el gráfico Q-Q.
sw <- shapiro.test(residuals(modelo))
datashapiro <- data.frame(
`W` = round(sw$statistic, 4),
`p-valor` = format(sw$p.value, scientific = TRUE, digits = 4),
check.names = FALSE
)
datashapiro %>%
kable(caption = "Tabla 13. Prueba de normalidad de Shapiro-Wilk") %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)| W | p-valor | |
|---|---|---|
| W | 0.8991 | 8.936e-29 |
Resultado: el p-valor (8.94e-29) es mucho menor que 0,05, así que rechazamos que los residuos sean normales. En el gráfico Q-Q los puntos se salen de la línea en el extremo derecho: hay personas con costos mucho más altos de lo que el modelo predice, sobre todo fumadores con obesidad. Como la muestra es grande (1337 personas), los coeficientes siguen siendo una buena estimación. Pero los p-valores deben leerse con algo de precaución.
Este supuesto pide que el tamaño de los errores sea parecido para todas las personas, es decir, que el modelo no se equivoque mucho más con unos grupos que con otros. Usamos la prueba de Breusch-Pagan.
bp <- bptest(modelo)
databp <- data.frame(
`Estadístico BP` = round(bp$statistic, 4),
gl = bp$parameter,
`p-valor` = format(bp$p.value, scientific = TRUE, digits = 4),
check.names = FALSE
)
databp %>%
kable(caption = "Tabla 14. Prueba de homocedasticidad (Breusch-Pagan)") %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)| Estadístico BP | gl | p-valor | |
|---|---|---|---|
| BP | 121.5712 | 8 | 1.57e-22 |
Resultado: el p-valor (1.57e-22) es menor que 0,05, así que hay heterocedasticidad. El modelo se equivoca mucho más con las personas de costos altos (fumadores) que con las de costos bajos. Una forma de corregir los errores estándar es usar errores “robustos”, que se ajustan a este problema:
coeftest(modelo, vcov = hccm(modelo, type = "hc1")) %>%
tidy() %>%
mutate(across(where(is.numeric), ~ round(.x, 4))) %>%
kable(caption = "Tabla 15. Coeficientes con errores estándar robustos",
col.names = c("Término", "Estimado", "Error estándar robusto", "t valor", "p-valor")) %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = TRUE)| Término | Estimado | Error estándar robusto | t valor | p-valor |
|---|---|---|---|---|
| (Intercept) | -11936.5575 | 1045.7363 | -11.4145 | 0.0000 |
| Edad | 256.7646 | 11.9266 | 21.5287 | 0.0000 |
| SexoHombre | -129.4815 | 333.8105 | -0.3879 | 0.6982 |
| IMC | 339.2504 | 31.7028 | 10.7010 | 0.0000 |
| Hijos | 474.8205 | 130.3548 | 3.6425 | 0.0003 |
| FumaSí | 23847.3288 | 574.9920 | 41.4742 | 0.0000 |
| RegionNoroeste | -349.2265 | 485.3925 | -0.7195 | 0.4720 |
| RegionSureste | -1035.2656 | 501.2939 | -2.0652 | 0.0391 |
| RegionSuroeste | -960.0814 | 461.1243 | -2.0820 | 0.0375 |
Con los errores robustos, las variables importantes (fumar, edad, IMC e hijos) siguen siendo significativas, así que las conclusiones principales se mantienen.
La multicolinealidad ocurre cuando dos o más variables explicativas dicen casi lo mismo. Así el modelo no puede separar el efecto de cada una. El VIF mide este problema: valores menores a 5 se consideran aceptables, y mayores a 10, preocupantes.
vif_valores <- vif(modelo)
datavif <- data.frame(
Variable = rownames(vif_valores),
GVIF = round(vif_valores[, 1], 4),
Df = vif_valores[, 2],
`GVIF^(1/2Df)` = round(vif_valores[, 3], 4),
check.names = FALSE,
row.names = NULL
)
datavif %>%
kable(caption = "Tabla 16. Prueba de multicolinealidad (VIF)") %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)| Variable | GVIF | Df | GVIF^(1/2Df) |
|---|---|---|---|
| Edad | 1.0168 | 1 | 1.0084 |
| Sexo | 1.0089 | 1 | 1.0045 |
| IMC | 1.1067 | 1 | 1.0520 |
| Hijos | 1.0040 | 1 | 1.0020 |
| Fuma | 1.0121 | 1 | 1.0060 |
| Region | 1.0990 | 3 | 1.0159 |
Resultado: todos los valores están muy cerca de 1, así que no hay multicolinealidad. Cada variable aporta información diferente.
Cada fila es una persona distinta medida una sola vez. No hay datos en el tiempo (no es una serie de años o meses), así que no hay dependencia temporal. Aun así, aplicamos la prueba de Durbin-Watson como revisión:
dw <- dwtest(modelo)
data.frame(`Estadístico DW` = round(dw$statistic, 4),
`p-valor` = round(dw$p.value, 4), check.names = FALSE) %>%
kable(caption = "Tabla 17. Prueba de independencia (Durbin-Watson)") %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)| Estadístico DW | p-valor | |
|---|---|---|
| DW | 2.089 | 0.9482 |
Resultado: el estadístico está cerca de 2 (2.09), lo que indica que no hay dependencia entre observaciones. Una posible limitación es que no sabemos si algunas personas son de la misma familia, porque la base no trae esa información.
data.frame(
Supuesto = c("Linealidad", "Normalidad", "Homocedasticidad", "Multicolinealidad", "Independencia"),
Prueba = c("Gráfico residuos vs ajustados", "Shapiro-Wilk", "Breusch-Pagan", "VIF", "Durbin-Watson"),
`¿Se cumple?` = c("Parcialmente", "No", "No", "Sí", "Sí"),
check.names = FALSE
) %>%
kable(caption = "Tabla 18. Resumen de la evaluación de supuestos") %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)| Supuesto | Prueba | ¿Se cumple? |
|---|---|---|
| Linealidad | Gráfico residuos vs ajustados | Parcialmente |
| Normalidad | Shapiro-Wilk | No |
| Homocedasticidad | Breusch-Pagan | No |
| Multicolinealidad | VIF | Sí |
| Independencia | Durbin-Watson | Sí |
Los problemas de linealidad, normalidad y homocedasticidad tienen una
causa común: el efecto de la obesidad es mucho mayor en los
fumadores. Para comprobarlo, probamos un segundo modelo.
Agregamos una variable Obeso (1 si el IMC es 30 o más) y su
combinación con Fuma (interacción). Esto le permite al
modelo decir que “ser fumador y obeso” tiene un costo
extra.
base_limpia <- base_limpia %>% mutate(Obeso = ifelse(IMC >= 30, 1, 0))
modelo2 <- lm(Costos ~ Edad + Sexo + IMC + Hijos + Fuma * Obeso + Region, data = base_limpia)
tidy(modelo2) %>%
mutate(across(where(is.numeric), ~ round(.x, 4))) %>%
kable(caption = "Tabla 19. Coeficientes del modelo con interacción Fuma × Obeso",
col.names = c("Término", "Estimado", "Error estándar", "t valor", "p-valor")) %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = TRUE)| Término | Estimado | Error estándar | t valor | p-valor |
|---|---|---|---|---|
| (Intercept) | -4741.9577 | 960.5522 | -4.9367 | 0.0000 |
| Edad | 263.2030 | 8.8150 | 29.8585 | 0.0000 |
| SexoHombre | -490.3948 | 246.7497 | -1.9874 | 0.0471 |
| IMC | 114.9331 | 34.5841 | 3.3233 | 0.0009 |
| Hijos | 520.1092 | 102.0277 | 5.0977 | 0.0000 |
| FumaSí | 13402.3402 | 444.0748 | 30.1804 | 0.0000 |
| Obeso | -862.9655 | 426.3241 | -2.0242 | 0.0431 |
| RegionNoroeste | -265.2591 | 352.8096 | -0.7518 | 0.4523 |
| RegionSureste | -825.0073 | 354.9321 | -2.3244 | 0.0203 |
| RegionSuroeste | -1224.3081 | 353.8156 | -3.4603 | 0.0006 |
| FumaSí:Obeso | 19793.9119 | 610.3739 | 32.4292 | 0.0000 |
bind_rows(
glance(modelo) %>% mutate(Modelo = "1. Modelo original"),
glance(modelo2) %>% mutate(Modelo = "2. Con interacción Fuma × Obeso")
) %>%
transmute(Modelo,
`R²` = round(r.squared, 4),
`R² ajustado` = round(adj.r.squared, 4),
`Error estándar residual (USD)` = round(sigma, 2)) %>%
kable(caption = "Tabla 20. Comparación de los dos modelos") %>%
kable_styling(bootstrap_options = c("striped", "bordered"), full_width = FALSE)| Modelo | R² | R² ajustado | Error estándar residual (USD) |
|---|---|---|---|
|
0.7507 | 0.7492 | 6064.30 |
|
0.8638 | 0.8628 | 4486.47 |
Resultado: al incluir la interacción, el R² sube de 0.751 a 0.864 y el error promedio baja de 6.064 a 4.486 dólares. El coeficiente de la interacción indica que un fumador con obesidad tiene unos 19.794 dólares adicionales de costos, además de lo que ya suma por fumar. Este hallazgo es muy importante para entender los costos médicos.
Respuesta a la pregunta de investigación:
Importante: estos resultados muestran asociaciones, no causas. Los datos son una “foto” de personas en un solo momento, no un experimento. Por eso no podemos afirmar, por ejemplo, que “dejar de fumar bajará los costos exactamente en esa cantidad”. Lo que sí podemos decir es que los fumadores, en estos datos, cuestan mucho más.
¿El modelo logró responder al objetivo de la investigación?
Sí, en buena medida. El modelo identificó con claridad qué factores se relacionan con los costos y cuánto pesa cada uno. Además explica cerca del 75% de las diferencias entre personas, y la multicolinealidad no es un problema. Sin embargo, no cumple todos los supuestos: los residuos no son normales, su varianza no es constante y la relación no es del todo lineal. Esto significa que el modelo básico se equivoca bastante con las personas de costos altos y que sus p-valores deben tomarse con cuidado. Al agregar la interacción entre fumar y obesidad, el ajuste mejoró mucho (R² de 0.864). Esto muestra que el objetivo se cumple mejor con ese modelo ampliado.
Limitaciones:
Recomendaciones y extensiones: