1. Exploración Analítica de Datos

El análisis exploratorio de datos (EDA) constituye la primera etapa del proceso analítico y tiene como propósito conocer la estructura, calidad y comportamiento general de la información antes de realizar análisis estadísticos de mayor complejidad. En este informe se utiliza una base de datos obtenida por encuesta de vigilancia de factores de riesgo en población adulta de EE.UU, que contiene aproximadamente 401.958 registros y 279 variables en la versión oficial de 2020, y pertenece al Sistema de Vigilancia de Factores de Riesgo Conductuales (BRFSS), de donde se conservan solo un subconjunto de 19 variables como propósito de análisis del informe.

1.1 Pregunta central y objetivo

Pregunta central: ¿Cómo se acumulan los factores de riesgo modificables (tabaquismo, sedentarismo, obesidad y sueño insuficiente) y las barreras económicas de acceso según el ingreso y la edad, y cómo se relacionan con la mala salud autopercibida y la enfermedad coronaria en adultos de EE. UU.?

Objetivo del EDA: Describir la estructura y la calidad de la base, identificar los grupos con mayor carga de riesgo y traducir los hallazgos en prioridades de gestión del riesgo y prevención.


1.2 Preparación del entorno y de los datos

1.2.1 Paquetes y paleta de colores

Se instalan solo los paquetes que falten y se cargan.

paquetes <- c("tidyverse", "skimr", "janitor", "plotly", "knitr")
faltantes_pkg <- paquetes[!paquetes %in% installed.packages()[, "Package"]]
if (length(faltantes_pkg) > 0) install.packages(faltantes_pkg)

library(tidyverse)
library(skimr)
library(janitor)
library(plotly)
library(knitr)

# Paleta institucional del informe
paleta <- c(azul = "#1F4E79", naranja = "#E67E22", verde = "#2E8B57", gris = "#7F8C8D")

1.2.2 Importación y selección de variables

# Importación de la base de datos
ruta_archivo <- "brfss2020.csv"
base_completa <- read_csv(ruta_archivo, col_types = cols(.default = col_double()))

# Datos generales de la base completa 
n_registros  <- nrow(base_completa)
n_variables  <- ncol(base_completa)
tipos_var    <- table(sapply(base_completa, class))
n_estados    <- n_distinct(base_completa$`_STATE`)
faltantes_por_variable <- base_completa |>
  summarise(across(everything(), ~ mean(is.na(.x)) * 100)) |>
  pivot_longer(everything(), names_to = "variable", values_to = "pct_faltante")

# Visualización inicial
head(base_completa)
# Nombres originales -> nombres en español
nombres_es <- c(
  estado_eeuu            = "_STATE",   sexo                   = "SEXVAR",
  salud_general          = "GENHLTH",  dias_mala_salud_fisica = "PHYSHLTH",
  dias_mala_salud_mental = "MENTHLTH", tiene_seguro           = "HLTHPLN1",
  barrera_costo          = "MEDCOST",  ultimo_chequeo         = "CHECKUP1",
  actividad_fisica       = "EXERANY2", horas_sueno            = "SLEPTIM1",
  tabaquismo             = "_SMOKER3", enf_coronaria          = "_MICHD",
  acv                    = "CVDSTRK3", diabetes               = "DIABETE4",
  imc                    = "_BMI5",    edad                   = "_AGE80",
  ingreso                = "_INCOMG",  educacion              = "_EDUCAG",
  peso_muestral          = "_LLCPWT"
)

base_sel <- base_completa |>
  select(all_of(nombres_es)) |>   
  clean_names()

rm(base_completa); invisible(gc())  

1.2.3 Normalización al español (recodificación)

Se convierten en NA (dato faltante) o en su valor real, cada categoría se traduce al español y se crean los indicadores de riesgo (1 = presente, 0 = ausente)

dias_validos <- function(x) case_when(x == 88 ~ 0, x >= 1 & x <= 30 ~ x, TRUE ~ NA_real_)

base_eda <- base_sel |>
  mutate(
    sexo             = factor(sexo, levels = 1:2, labels = c("Hombre", "Mujer")),
    salud_general    = factor(salud_general, levels = 1:5,
                              labels = c("Excelente", "Muy buena", "Buena", "Regular", "Mala")),
    dias_mala_salud_fisica = dias_validos(dias_mala_salud_fisica),
    dias_mala_salud_mental = dias_validos(dias_mala_salud_mental),
    tiene_seguro     = factor(tiene_seguro,  levels = 1:2, labels = c("Sí", "No")),
    barrera_costo    = factor(barrera_costo, levels = 1:2, labels = c("Sí", "No")),
    ultimo_chequeo   = factor(ultimo_chequeo, levels = c(1, 2, 3, 4, 8),
                              labels = c("< 1 año", "1-2 años", "2-5 años", "5+ años", "Nunca")),
    actividad_fisica = factor(actividad_fisica, levels = 1:2, labels = c("Sí", "No")),
    horas_sueno      = if_else(horas_sueno <= 24, horas_sueno, NA_real_),
    tabaquismo       = factor(tabaquismo, levels = 1:4,
                              labels = c("Fuma diario", "Fuma algunos días", "Exfumador", "Nunca fumó")),
    enf_coronaria    = factor(enf_coronaria, levels = 1:2, labels = c("Sí", "No")),
    acv              = factor(acv, levels = 1:2, labels = c("Sí", "No")),
    diabetes         = factor(diabetes, levels = 1:4,
                              labels = c("Sí", "Solo en embarazo", "No", "Prediabetes")),
    imc              = imc / 100,   # la base trae el IMC con 2 decimales implícitos
    ingreso          = factor(ingreso, levels = 1:5, ordered = TRUE,
                              labels = c("< 15 mil", "15-25 mil", "25-35 mil", "35-50 mil", "50 mil o más")),
    educacion        = factor(educacion, levels = 1:4,
                              labels = c("Sin bachillerato", "Bachiller", "Superior incompleta", "Universitario")),
    grupo_edad       = cut(edad, breaks = c(17, 34, 49, 64, 80),
                           labels = c("18-34", "35-49", "50-64", "65+")),
    # Indicadores de riesgo (1 = presente)
    fuma_actual      = case_when(tabaquismo %in% c("Fuma diario", "Fuma algunos días") ~ 1,
                                 tabaquismo %in% c("Exfumador", "Nunca fumó") ~ 0),
    sedentarismo     = case_when(actividad_fisica == "No" ~ 1, actividad_fisica == "Sí" ~ 0),
    obesidad         = if_else(imc >= 30, 1, 0),
    sueno_corto      = if_else(horas_sueno < 7, 1, 0),
    n_factores_riesgo = fuma_actual + sedentarismo + obesidad + sueno_corto,
    mala_salud       = case_when(salud_general %in% c("Regular", "Mala") ~ 1,
                                 !is.na(salud_general) ~ 0)
  )

1.2.4 Diccionario de variables

diccionario <- tibble(
  variable_original = unname(nombres_es),
  variable_espanol  = names(nombres_es),
  descripcion = c(
    "Estado o territorio de residencia", "Sexo del encuestado",
    "Salud general autopercibida", "Días con mala salud física (últimos 30)",
    "Días con mala salud mental (últimos 30)", "Tiene algún seguro de salud",
    "No consultó al médico por costo (último año)", "Tiempo desde el último chequeo",
    "Actividad física fuera del trabajo (último mes)", "Horas de sueño en 24 h",
    "Condición de fumador", "Enfermedad coronaria o infarto (calculada)",
    "Antecedente de ACV", "Diagnóstico de diabetes", "Índice de masa corporal (kg/m²)",
    "Edad en años (tope 80)", "Ingreso anual del hogar (USD)", "Nivel educativo",
    "Peso muestral (factor de expansión)"),
  tipo = c("Categórica", "Categórica", "Ordinal", "Numérica", "Numérica", "Categórica",
           "Categórica", "Ordinal", "Categórica", "Numérica", "Categórica", "Categórica",
           "Categórica", "Categórica", "Numérica", "Numérica", "Ordinal", "Ordinal", "Numérica"),
  dimension = c("Identificación", "Demografía", "Resultado en salud", "Resultado en salud",
                "Resultado en salud", "Acceso / financiero", "Acceso / financiero", "Uso de servicios",
                "Riesgo modificable", "Riesgo modificable", "Riesgo modificable", "Enfermedad crónica",
                "Enfermedad crónica", "Enfermedad crónica", "Riesgo modificable", "Demografía",
                "Socioeconómica / financiero", "Socioeconómica", "Diseño muestral")
)
diccionario

1.3 Comprensión de la base

1.3.1 Registros, variables y unidad de análisis

tibble(
  Indicador = c("Número de registros", "Número de variables", "Estados/territorios",
                "Variables seleccionadas para el EDA"),
  Valor = c(format(n_registros, big.mark = "."), n_variables, n_estados, ncol(base_sel))
)
  • Unidad de análisis: cada fila es un adulto (18 años o más) encuestado por teléfono en 2020. No son pacientes ni atenciones: es población general, lo que la hace útil para prevención y no para medir producción de servicios.
  • La base tiene 401.958 registros, 279 variables y cubre 53 estados y territorios (50 estados, Distrito de Columbia, Guam y Puerto Rico).
  • Datos financieros: la base no contiene costos ni facturación. Se usan tres variables como aproximación (proxy, variable indirecta) de la dimensión financiera: ingreso del hogar, haber dejado de consultar por costo y tener seguro de salud.
  • Ponderación: el BRFSS es una encuesta compleja; para cifras oficiales debe usarse peso_muestral. Este EDA reporta valores sin ponderar; la diferencia es pequeña (mala salud: 15,4 % sin ponderar vs. 14,7 % ponderado; obesidad: 32,0 % vs. 31,9 %).

1.3.2 Tipos de variables

tipos_var                       # tipo en el archivo original
## 
## numeric 
##     279
table(sapply(base_eda, function(x) class(x)[1]))   # tipo después de normalizar
## 
##  factor numeric ordered 
##      12      13       1

1.3.3 Datos faltantes

faltantes_por_variable |>
  mutate(rango = cut(pct_faltante, breaks = c(-Inf, 0, 10, 50, Inf),
                     labels = c("Sin faltantes", "Hasta 10 %", "10 % a 50 %", "Más de 50 %"))) |>
  count(rango, name = "n_variables")
# Faltantes en las variables del EDA, después de convertir códigos 7/9/77/99 a NA
base_eda |>
  summarise(across(c(salud_general, dias_mala_salud_fisica, dias_mala_salud_mental, barrera_costo,
                     horas_sueno, tabaquismo, enf_coronaria, imc, ingreso, n_factores_riesgo),
                   ~ round(mean(is.na(.x)) * 100, 1))) |>
  pivot_longer(everything(), names_to = "variable", values_to = "pct_faltante") |>
  arrange(desc(pct_faltante))

Interpretación:

  • En la base completa, el 45,7 % de las celdas está vacío y 142 de las 279 variables tienen más de 50 % de faltantes; ningún registro está completo. Esto no es un error: el BRFSS usa módulos opcionales (cada estado decide cuáles aplica) y preguntas condicionadas (por ejemplo, la tamización de próstata solo se hace a hombres).
  • Implicación: no se debe eliminar filas con cualquier faltante, porque se perdería toda la base.
  • Alerta: el ingreso tiene cerca de 20 % sin dato; es la variable financiera clave y su faltante no es aleatorio (las personas suelen negarse a declarar ingresos). El IMC falta en ~10 % y el número de factores de riesgo en ~13 %.

1.4 Calidad y distribución

1.4.1 Estructura de los datos

str(base_eda)
## tibble [401,958 × 26] (S3: tbl_df/tbl/data.frame)
##  $ estado_eeuu           : num [1:401958] 1 1 1 1 1 1 1 1 1 1 ...
##  $ sexo                  : Factor w/ 2 levels "Hombre","Mujer": 2 2 2 2 2 1 2 2 2 2 ...
##  $ salud_general         : Factor w/ 5 levels "Excelente","Muy buena",..: 2 3 3 1 2 4 3 4 2 4 ...
##  $ dias_mala_salud_fisica: num [1:401958] 3 0 0 0 0 20 0 15 28 6 ...
##  $ dias_mala_salud_mental: num [1:401958] 30 0 0 0 0 30 0 10 0 0 ...
##  $ tiene_seguro          : Factor w/ 2 levels "Sí","No": 2 1 1 1 1 1 1 1 1 1 ...
##  $ barrera_costo         : Factor w/ 2 levels "Sí","No": 1 1 2 2 2 2 2 2 2 2 ...
##  $ ultimo_chequeo        : Factor w/ 5 levels "< 1 año","1-2 años",..: 4 1 1 2 1 2 1 1 1 1 ...
##  $ actividad_fisica      : Factor w/ 2 levels "Sí","No": 1 1 1 2 1 1 2 1 1 2 ...
##  $ horas_sueno           : num [1:401958] 5 7 7 6 7 8 6 6 8 12 ...
##  $ tabaquismo            : Factor w/ 4 levels "Fuma diario",..: 1 NA 4 4 4 3 4 1 4 3 ...
##  $ enf_coronaria         : Factor w/ 2 levels "Sí","No": 2 2 2 2 2 2 2 2 2 1 ...
##  $ acv                   : Factor w/ 2 levels "Sí","No": 2 2 2 2 1 2 2 2 2 2 ...
##  $ diabetes              : Factor w/ 4 levels "Sí","Solo en embarazo",..: 1 3 3 3 3 1 3 1 3 3 ...
##  $ imc                   : num [1:401958] 16.6 29.2 NA NA 20.3 ...
##  $ edad                  : num [1:401958] 56 65 65 80 80 66 75 69 41 78 ...
##  $ ingreso               : Ord.factor w/ 5 levels "< 15 mil"<"15-25 mil"<..: 1 NA 5 NA NA 3 4 3 4 NA ...
##  $ educacion             : Factor w/ 4 levels "Sin bachillerato",..: 4 4 3 2 4 2 2 2 4 1 ...
##  $ peso_muestral         : num [1:401958] 284 171 1334 1297 455 ...
##  $ grupo_edad            : Factor w/ 4 levels "18-34","35-49",..: 3 4 4 4 4 4 4 4 2 4 ...
##  $ fuma_actual           : num [1:401958] 1 NA 0 0 0 0 0 1 0 0 ...
##  $ sedentarismo          : num [1:401958] 0 0 0 1 0 0 1 0 0 1 ...
##  $ obesidad              : num [1:401958] 0 0 NA NA 0 0 0 0 0 0 ...
##  $ sueno_corto           : num [1:401958] 1 0 0 1 0 0 1 1 0 0 ...
##  $ n_factores_riesgo     : num [1:401958] 2 NA NA NA 0 0 2 2 0 1 ...
##  $ mala_salud            : num [1:401958] 0 0 0 0 0 1 0 1 0 1 ...
summary(select(base_eda, edad, imc, horas_sueno, dias_mala_salud_fisica, dias_mala_salud_mental))
##       edad            imc         horas_sueno     dias_mala_salud_fisica
##  Min.   :18.00   Min.   :12.02   Min.   : 1.000   Min.   : 0.000        
##  1st Qu.:40.00   1st Qu.:23.99   1st Qu.: 6.000   1st Qu.: 0.000        
##  Median :57.00   Median :27.32   Median : 7.000   Median : 0.000        
##  Mean   :54.43   Mean   :28.31   Mean   : 7.097   Mean   : 3.458        
##  3rd Qu.:69.00   3rd Qu.:31.38   3rd Qu.: 8.000   3rd Qu.: 2.000        
##  Max.   :80.00   Max.   :98.43   Max.   :24.000   Max.   :30.000        
##                  NA's   :41357   NA's   :4694     NA's   :8691          
##  dias_mala_salud_mental
##  Min.   : 0.000        
##  1st Qu.: 0.000        
##  Median : 0.000        
##  Mean   : 3.916        
##  3rd Qu.: 3.000        
##  Max.   :30.000        
##  NA's   :7929

1.4.2 Variables numéricas

Resume en una tabla: faltantes, media, desviación estándar, percentiles y un mini histograma.

base_eda |>
  select(edad, imc, horas_sueno, dias_mala_salud_fisica, dias_mala_salud_mental, n_factores_riesgo) |>
  skim()
Data summary
Name select(…)
Number of rows 401958
Number of columns 6
_______________________
Column type frequency:
numeric 6
________________________
Group variables None

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
edad 0 1.00 54.43 17.67 18.00 40.00 57.00 69.00 80.00 ▃▅▆▇▇
imc 41357 0.90 28.31 6.38 12.02 23.99 27.32 31.38 98.43 ▇▅▁▁▁
horas_sueno 4694 0.99 7.10 1.47 1.00 6.00 7.00 8.00 24.00 ▁▇▁▁▁
dias_mala_salud_fisica 8691 0.98 3.46 8.08 0.00 0.00 0.00 2.00 30.00 ▇▁▁▁▁
dias_mala_salud_mental 7929 0.98 3.92 8.05 0.00 0.00 0.00 3.00 30.00 ▇▁▁▁▁
n_factores_riesgo 53108 0.87 1.00 0.95 0.00 0.00 1.00 2.00 4.00 ▇▇▅▂▁

Interpretación:

  • Edad: mediana 57 años; población adulta mayor que el promedio, consistente con encuestas telefónicas.
  • IMC: mediana 27,3 kg/m² (rango de sobrepeso); la mitad central de la población está entre 24,0 y 31,4.
  • Horas de sueño: mediana 7 h, pero el 30,6 % duerme menos de 7 h (sueño insuficiente).
  • Días de mala salud física y mental: la mediana es 0, pero el promedio es mayor. Son variables asimétricas (la mayoría reporta 0 días y un grupo pequeño reporta muchos), por eso la mediana describe mejor al grupo típico que la media.

1.4.3 Variables categóricas

variables_cat <- c("sexo", "salud_general", "tiene_seguro", "barrera_costo", "ultimo_chequeo",
                   "actividad_fisica", "tabaquismo", "diabetes", "enf_coronaria", "ingreso")

base_eda |>
  select(all_of(variables_cat)) |>
  mutate(across(everything(), as.character)) |>
  pivot_longer(everything(), names_to = "variable", values_to = "categoria") |>
  mutate(categoria = replace_na(categoria, "Sin dato")) |>
  count(variable, categoria) |>
  group_by(variable) |>
  mutate(porcentaje = round(100 * n / sum(n), 1)) |>
  ungroup()

Se genera Tabla cruzada de la dimensión financiera: porcentaje que no consultó por costo según ingreso (con janitor::tabyl).

base_eda |>
  tabyl(ingreso, barrera_costo, show_na = FALSE) |>
  adorn_percentages("row") |>
  adorn_pct_formatting(digits = 1) |>
  adorn_ns()

Interpretación:

  • Salud general: 15,4 % se califica en salud regular o mala; este es el indicador resultado más completo de la base.
  • Acceso: 8,5 % no tiene seguro y 8,6 % dejó de consultar por costo; 77 % tuvo chequeo en el último año.
  • Riesgo: 14 % fuma actualmente, 24 % es sedentario y 13 % reporta diabetes.
  • Gradiente financiero: en hogares con ingreso < 15 mil USD, 18,2 % no consultó por costo, frente a 4,8 % en hogares con 50 mil o más (casi 4 veces más). El costo es una barrera concentrada, no general.

1.4.4 Valores extremos (boxplot)

Un boxplot (diagrama de caja) muestra la mediana (línea central), el 50 % central de los datos (la caja) y los valores atípicos (puntos fuera de 1,5 veces el rango intercuartílico, es decir, la distancia entre el percentil 25 y el 75).

datos_box <- base_eda |>
  select(`IMC (kg/m²)` = imc, `Horas de sueño` = horas_sueno,
         `Días mala salud física` = dias_mala_salud_fisica,
         `Días mala salud mental` = dias_mala_salud_mental) |>
  pivot_longer(everything(), names_to = "variable", values_to = "valor") |>
  drop_na()

ggplot(datos_box, aes(x = variable, y = valor, fill = variable)) +
  geom_boxplot(outlier.alpha = 0.05, outlier.colour = paleta["gris"], width = 0.5) +
  facet_wrap(~ variable, scales = "free") +
  scale_fill_manual(values = unname(paleta)) +
  labs(title = "Valores extremos en variables numéricas clave", x = NULL, y = NULL) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "none", axis.text.x = element_blank())

# Cuantificación de atípicos
datos_box |>
  group_by(variable) |>
  summarise(mediana = median(valor), q1 = quantile(valor, 0.25), q3 = quantile(valor, 0.75),
            limite_superior = q3 + 1.5 * (q3 - q1),
            pct_atipicos = round(100 * mean(valor < q1 - 1.5 * (q3 - q1) | valor > limite_superior), 1),
            maximo = max(valor))

Interpretación:

  • IMC: 3,3 % de atípicos con máximo de 98 kg/m²; valores > 70 son biológicamente posibles pero raros y pueden ser errores de digitación de peso o talla. Decisión: se conservan para el EDA y se marcan para revisión antes de cualquier modelamiento.
  • Sueño: 1,5 % de atípicos (personas que reportan 1-3 h o más de 12 h). Pueden reflejar trabajo nocturno o enfermedad.
  • Días de mala salud física (15,1 %) y mental (16,2 %): aquí los “atípicos” no son errores: son las personas con mayor carga de enfermedad. Desde la gestión del riesgo, estos extremos son la población objetivo, no ruido que se deba eliminar.

1.4.5 Distribución del IMC

ggplot(filter(base_eda, imc <= 60), aes(x = imc)) +
  geom_histogram(binwidth = 1, fill = paleta["azul"], colour = "white") +
  geom_vline(xintercept = c(25, 30), linetype = "dashed", colour = paleta["naranja"], linewidth = 0.8) +
  annotate("text", x = c(25, 30), y = Inf, vjust = 1.5, hjust = -0.1,
           label = c("Sobrepeso", "Obesidad"), colour = paleta["naranja"]) +
  labs(title = "Distribución del índice de masa corporal",
       subtitle = "Se muestran valores hasta 60 kg/m² para facilitar la lectura",
       x = "IMC (kg/m²)", y = "Número de personas") +
  theme_minimal(base_size = 12)

Interpretación: la distribución tiene sesgo a la derecha (cola larga hacia valores altos). El pico está en sobrepeso y el 32 % de los adultos con dato tiene obesidad (IMC ≥ 30). La obesidad es el factor de riesgo modificable más frecuente de los cuatro analizados.


1.5 Relaciones entre variables

Los siguientes gráficos son interactivos: al pasar el cursor se ve el valor exacto

1.5.1 Gradiente socioeconómico del riesgo y del acceso

  • Variables resultado: mala salud autopercibida, no consultar por costo, no tener seguro, sedentarismo (%).
  • Variable de comparación: ingreso anual del hogar.
gradiente_ingreso <- base_eda |>
  filter(!is.na(ingreso)) |>
  group_by(ingreso) |>
  summarise(`Mala salud autopercibida` = mean(mala_salud, na.rm = TRUE) * 100,
            `No consultó por costo`    = mean(barrera_costo == "Sí", na.rm = TRUE) * 100,
            `Sin seguro de salud`      = mean(tiene_seguro == "No", na.rm = TRUE) * 100,
            `Sedentarismo`             = mean(sedentarismo, na.rm = TRUE) * 100) |>
  pivot_longer(-ingreso, names_to = "indicador", values_to = "porcentaje")

plot_ly(gradiente_ingreso, x = ~ingreso, y = ~porcentaje, color = ~indicador,
        colors = unname(paleta), type = "bar",
        text = ~paste0(round(porcentaje, 1), "%"), textposition = "outside",
        hovertemplate = "%{x}<br>%{y:.1f}%<extra></extra>") |>
  layout(title = "A menor ingreso, mayor riesgo y menor acceso",
         barmode = "group",
         xaxis = list(title = "Ingreso anual del hogar (USD)"),
         yaxis = list(title = "% de adultos", range = c(0, 50)),
         legend = list(orientation = "h", y = -0.2))

Interpretación: los cuatro indicadores descienden de forma escalonada a medida que sube el ingreso (gradiente). En el grupo de menor ingreso, 38,9 % reporta mala salud, frente a 7,1 % en el de mayor ingreso (5,5 veces más). El sedentarismo pasa de 41,7 % a 14,5 %, la falta de seguro de 16,2 % a 4,1 % y la barrera de costo de 18,2 % a 4,8 %.

Para la gestión: el ingreso funciona como un criterio de focalización: un mismo recurso preventivo tiene más impacto potencial en los quintiles bajos, donde coinciden más riesgo y menos acceso.

1.5.2 Superficie 3D: edad × factores de riesgo × enfermedad coronaria

  • Variable resultado: prevalencia (porcentaje de personas que tienen la condición) de enfermedad coronaria o infarto.
  • Variables de comparación: grupo de edad y número de factores de riesgo modificables (0 a 4).
matriz_coronaria <- base_eda |>
  filter(!is.na(grupo_edad), !is.na(n_factores_riesgo), !is.na(enf_coronaria)) |>
  group_by(grupo_edad, n_factores_riesgo) |>
  summarise(prevalencia = mean(enf_coronaria == "Sí") * 100, .groups = "drop") |>
  pivot_wider(names_from = n_factores_riesgo, values_from = prevalencia)

z_prevalencia <- as.matrix(matriz_coronaria[, -1])   # filas = edad, columnas = n.º factores

plot_ly(x = 0:4, y = 1:4, z = z_prevalencia, type = "surface",
        colorscale = list(c(0, paleta[["verde"]]), c(0.5, paleta[["gris"]]), c(1, paleta[["naranja"]])),
        colorbar = list(title = "% coronaria"),
        hovertemplate = "Factores de riesgo: %{x}<br>Grupo de edad: %{y}<br>Prevalencia: %{z:.1f}%<extra></extra>") |>
  layout(title = "Riesgo acumulado: la edad no modificable y los factores modificables se suman",
         scene = list(
           xaxis = list(title = "N.º factores de riesgo"),
           yaxis = list(title = "Edad", tickvals = 1:4, ticktext = levels(base_eda$grupo_edad)),
           zaxis = list(title = "% enfermedad coronaria", color = paleta[["azul"]]),
           camera = list(eye = list(x = 1.6, y = -1.6, z = 0.9))))

Interpretación: la superficie sube en las dos direcciones. Con la edad (factor no modificable) y con el número de factores de riesgo (factores modificables).

  • En 50-64 años, la prevalencia pasa de 4,1 % sin factores de riesgo a 21,7 % con los cuatro (más de 5 veces).
  • En 65+, va de 13,3 % a 29,8 %.
  • Una persona de 50-64 años con 3 factores (15,5 %) tiene una prevalencia mayor que una de 65+ sin factores (13,3 %).
  • El 27,9 % de los adultos con dato completo acumula 2 o más factores.

Para la gestión: el número de factores de riesgo es una regla simple de estratificación (clasificar a la población por nivel de riesgo) que se puede aplicar en el primer nivel de atención. La ventana de mayor rendimiento preventivo está en 35-64 años, donde los factores modificables todavía cambian mucho el resultado.

Nota: es una asociación transversal (medida en un solo momento), no causal; las celdas de 4 factores en jóvenes tienen pocos casos.

1.5.3 Burbujas 3D: ingreso × edad × mala salud, con barrera de costo

  • Variable resultado: % de mala salud autopercibida (altura).
  • Variables de comparación: ingreso, grupo de edad y % que no consultó por costo (color). El tamaño de la burbuja indica cuántas personas hay en el grupo.
burbujas <- base_eda |>
  filter(!is.na(ingreso), !is.na(grupo_edad)) |>
  group_by(ingreso, grupo_edad) |>
  summarise(pct_mala_salud = mean(mala_salud, na.rm = TRUE) * 100,
            pct_costo      = mean(barrera_costo == "Sí", na.rm = TRUE) * 100,
            n = n(), .groups = "drop") |>
  mutate(tamano = scales::rescale(sqrt(n), to = c(8, 30)))

plot_ly(burbujas, x = ~as.numeric(ingreso), y = ~as.numeric(grupo_edad), z = ~pct_mala_salud,
        type = "scatter3d", mode = "markers",
        marker = list(size = ~tamano, color = ~pct_costo, opacity = 0.85,
                      colorscale = list(c(0, paleta[["azul"]]), c(0.5, paleta[["gris"]]), c(1, paleta[["naranja"]])),
                      colorbar = list(title = "% no consultó<br>por costo"), line = list(width = 0)),
        text = ~paste0("Ingreso: ", ingreso, "<br>Edad: ", grupo_edad,
                       "<br>Mala salud: ", round(pct_mala_salud, 1), "%",
                       "<br>Barrera de costo: ", round(pct_costo, 1), "%",
                       "<br>Personas: ", format(n, big.mark = ".")),
        hoverinfo = "text") |>
  layout(title = "Dónde se concentran la mala salud y la barrera económica",
         scene = list(
           xaxis = list(title = "Ingreso (USD)", tickvals = 1:5, ticktext = levels(base_eda$ingreso)),
           yaxis = list(title = "Edad", tickvals = 1:4, ticktext = levels(base_eda$grupo_edad)),
           zaxis = list(title = "% mala salud")))

Interpretación:

  • El punto más alto está en 50-64 años con ingreso < 15 mil USD: casi la mitad (49,7 %) reporta mala salud, frente a 7,1 % en la misma edad con ingreso alto (7 veces más).
  • La barrera de costo (color naranja) se concentra en menores de 65 años de bajo ingreso (21-24 %). En 65+ cae a 2-10 %, lo que es coherente con la cobertura pública universal desde los 65 años en EE. UU. (Medicare).
  • Las burbujas grandes (más población) están en ingreso alto, mientras que la mayor carga está en grupos pequeños. Por eso los promedios generales esconden el problema.

Para la gestión: el segmento adulto 35-64 de bajo ingreso combina la peor salud y la mayor barrera económica. Es el grupo prioritario para programas de prevención con subsidio o con atención sin copago.

1.5.4 Correlación preliminar entre variables

Se usa la correlación de Spearman (mide si dos variables aumentan o disminuyen juntas, basada en el orden de los valores; va de -1 a +1). Es adecuada porque varias variables son asimétricas u ordinales.

matriz_cor <- base_eda |>
  transmute(Edad = edad, IMC = imc, `Horas sueño` = horas_sueno,
            `Días salud física` = dias_mala_salud_fisica, `Días salud mental` = dias_mala_salud_mental,
            Ingreso = as.numeric(ingreso), `Salud general (1=excelente)` = as.numeric(salud_general),
            `N.º factores riesgo` = n_factores_riesgo) |>
  cor(method = "spearman", use = "pairwise.complete.obs")

plot_ly(x = colnames(matriz_cor), y = rownames(matriz_cor), z = matriz_cor, type = "heatmap",
        zmin = -1, zmax = 1,
        colorscale = list(c(0, paleta[["naranja"]]), c(0.5, "#F2F2F2"), c(1, paleta[["azul"]])),
        text = round(matriz_cor, 2), texttemplate = "%{text}",
        hovertemplate = "%{x} vs %{y}: %{z:.2f}<extra></extra>") |>
  layout(title = "Matriz de correlación (Spearman)",
         xaxis = list(tickangle = -35), yaxis = list(autorange = "reversed"))

Interpretación (en salud general, valores altos significan peor salud):

  • La correlación más fuerte con la mala salud general es con los días de mala salud física (0,41), seguida del número de factores de riesgo (0,34) y del ingreso (-0,30): a más ingreso, mejor salud.
  • Salud física y mental se mueven juntas (0,29), lo que apoya un abordaje integral y no por separado.
  • La edad se asocia negativamente con los días de mala salud mental (-0,22): los adultos jóvenes reportan más carga mental.
  • Las correlaciones del número de factores con IMC (0,49) y sueño (-0,46) son altas por construcción (son parte del indicador), no son hallazgos.
  • En general las correlaciones son débiles a moderadas (< 0,5): ninguna variable explica por sí sola la salud. Esto justifica pasar, en una fase posterior, a un modelo que combine varias variables.

1.6 Hallazgos principales y su influencia en la toma de decisiones

# Hallazgo Implicación para la gestión del riesgo
1 Base grande (401.958 adultos), pero con 45,7 % de celdas vacías por diseño modular; ingreso sin dato en ~20 %. Analizar por módulo y documentar faltantes; no eliminar registros en bloque. Mejorar la captura de variables socioeconómicas en los propios sistemas de información.
2 Gradiente socioeconómico claro: la mala salud es 5,5 veces mayor en ingreso bajo que en alto; la barrera de costo, casi 4 veces mayor. Focalizar recursos de prevención por nivel socioeconómico; el costo de bolsillo es una barrera de acceso medible y modificable.
3 Los factores de riesgo modificables se acumulan (28 % tiene 2 o más) y la prevalencia coronaria crece con cada factor adicional en todos los grupos de edad. Usar el conteo de factores como herramienta sencilla de estratificación del riesgo en atención primaria.
4 El segmento de 35-64 años de bajo ingreso concentra la peor salud y la mayor barrera económica, mientras que en 65+ la barrera cae. Priorizar este segmento en programas de detección temprana y control de riesgo; la cobertura financiera reduce la barrera de acceso.
5 Los valores extremos en días de mala salud (~15 %) representan población de alta carga, no errores. Identificar y gestionar a los usuarios de alta carga (gestión de casos); revisar solo los extremos de IMC biológicamente improbables.
6 Las correlaciones son débiles a moderadas y multifactoriales. Justifica la siguiente fase: modelamiento con varias variables y uso del peso muestral.