En este taller aprenderemos a comparar la mortalidad entre poblaciones con estructuras de edad diferentes (Blancos, Negros e Hispanos). Para evitar sesgos, utilizaremos el Método Directo de Estandarización usando la población estándar de EE.UU. año 2000.
Primero, cargamos las librerías y preparamos los datos desde el Excel.
# --- LIBRERÍAS ---
library(readxl)
library(dplyr)
library(tidyr)
library(ggplot2)
library(writexl)
archivo <- "direct_indirect_student_v2.xlsx"
library(): Es como abrir una caja de herramientas. R
base tiene funciones básicas, pero para análisis complejos necesitamos
“paquetes” especializados.
readxl y writexl: Son el puente entre R
y Excel. Nos permiten leer (read) y guardar
(write) archivos .xlsx.
dplyr y tidyr: Son el estándar de oro
en ciencia de datos (parte del “Tidyverse”). dplyr funciona
con verbos (seleccionar, filtrar) y tidyr ayuda a cambiar
la forma de los datos.
Leemos la hoja de muertes seleccionando exactamente el área donde están los datos numéricos.
# --- PASO 1: LEER MUERTES ---
# Leemos un rango específico (A4:J14) para evitar títulos innecesarios
df_muertes <- read_excel(archivo, sheet = "Maryland Deaths", range = "A4:J14", col_names = FALSE)
# Asignamos nombres manuales para evitar espacios y caracteres extraños
colnames(df_muertes) <- c("Edad",
"Blanco_Total", "Blanco_Hombres", "Blanco_Mujeres",
"Negro_Total", "Negro_Hombres", "Negro_Mujeres",
"Hispano_Total", "Hispano_Hombres", "Hispano_Mujeres")
# Vemos las primeras filas
head(df_muertes, 3)
## # A tibble: 3 × 10
## Edad Blanco_Total Blanco_Hombres Blanco_Mujeres Negro_Total Negro_Hombres
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 <1 150 78 72 250 141
## 2 1-4 26 17 9 34 18
## 3 5-14 29 14 15 36 22
## # ℹ 4 more variables: Negro_Mujeres <dbl>, Hispano_Total <dbl>,
## # Hispano_Hombres <dbl>, Hispano_Mujeres <dbl>
range = "A4:J14": Esta función actúa como un
“cortador de galletas”. Le decimos a R que ignore todo el archivo y solo
recorte el rectángulo exacto donde están nuestros datos.
col_names = FALSE: Le decimos a R que la primera
fila que lee (la 4) es un dato, no el título de la columna.
colnames(...) <- c(...): Renombramos las columnas
manualmente. En programación, es vital evitar espacios en los nombres
(ej: usar Blanco_Total en vez de Blanco Total)
para no tener errores después.
Leemos la población y seleccionamos solo las columnas que coinciden con las muertes.
# --- PASO 2: LEER POBLACIÓN ---
df_pob_raw <- read_excel(archivo, sheet = "Maryland Population", range = "A5:R23", col_names = FALSE)
# Seleccionamos columnas útiles usando su posición numérica
df_pob_raw <- df_pob_raw %>% select(1, 2, 4, 6, 8, 10, 12, 14, 16, 18)
# Usamos los mismos nombres que en la tabla de muertes
colnames(df_pob_raw) <- colnames(df_muertes)
%>% (Pipe): Este símbolo significa “y luego…”.
Toma el resultado de la izquierda y lo pasa a la función de la
derecha.
select(1, 2, 4...): El Excel original tenía columnas
vacías intercaladas. Aquí le decimos a R: “Quédate con la columna 1, la
2, salta la 3, quédate con la 4…”. Así limpiamos la “basura” del
Excel.
El archivo de Población tiene 19 grupos de edad (ej: 5-9 y 10-14), pero el de Muertes tiene 11 (ej: 5-14). Debemos sumar la población para que coincidan.
# --- PASO 3: HOMOLOGAR POBLACIÓN (19 filas -> 11 filas) ---
# Creamos un "mapa" o índice de agrupación
# 1 y 2 se quedan solos. Del 3 al 10 se repiten dos veces (para sumar pares).
grupos_id <- c(1, 2, rep(3:10, each = 2), 11)
# Aplicamos la agrupación ("Split-Apply-Combine")
df_pob_agrupada <- df_pob_raw %>%
mutate(Grupo_ID = grupos_id) %>% # 1. Agregamos el ID
group_by(Grupo_ID) %>% # 2. Agrupamos por ese ID
summarise(across(where(is.numeric), sum)) %>% # 3. Sumamos los números
select(-Grupo_ID) # 4. Quitamos el ID
# Verificamos dimensiones (debe tener 11 filas)
nrow(df_pob_agrupada)
## [1] 11
Esta es la parte lógica más compleja:
rep(3:10, each = 2): Genera una secuencia 3, 3, 4,
4, 5, 5…. Esto le dice a R que las filas 3 y 4 del Excel original deben
sumarse juntas en un solo grupo nuevo.
group_by + summarise: Es la combinación más poderosa
de dplyr.
group_by: “Junta las filas que tengan el mismo
ID”.
summarise(sum): “Una vez juntas, suma sus
valores”.
El resultado es una tabla comprimida de 11 filas, lista para dividirse con la tabla de muertes.
Aplicamos los ponderadores de la población estándar (US 2000) a cada grupo.
# --- PASO 4: DEFINIR PONDERADORES ---
pesos_us_2000 <- c(0.013818, 0.055317, 0.145565, 0.138646, 0.135573,
0.162613, 0.134834, 0.087247, 0.066037, 0.044842, 0.015508)
# --- PASO 5: CÁLCULO ITERATIVO ---
# Creamos una tabla vacía para guardar resultados
resumen_ajuste <- data.frame()
# Obtenemos los nombres de las categorías (ej: Blanco_Hombres, etc.)
categorias <- colnames(df_muertes)[2:10]
for (cat in categorias) {
# Extraemos las columnas actuales
muertes <- df_muertes[[cat]]
poblacion <- df_pob_agrupada[[cat]]
# 1. Tasa Cruda (Real)
tasa_cruda <- (sum(muertes) / sum(poblacion)) * 1000
# 2. Tasa Ajustada (Hipotética)
# Fórmula: Sumatoria de (Tasa Específica x Peso Estándar)
tasas_especificas <- (muertes / poblacion) * 1000
muertes_esperadas <- tasas_especificas * pesos_us_2000
tasa_ajustada <- sum(muertes_esperadas)
# Guardamos en la tabla resumen
resumen_ajuste <- rbind(resumen_ajuste, data.frame(
Categoria = cat,
Tasa_Cruda = tasa_cruda,
Tasa_Ajustada = tasa_ajustada
))
}
# Mostramos la tabla final
knitr::kable(resumen_ajuste, digits = 2, caption = "Tasas Crudas vs Ajustadas")
| Categoria | Tasa_Cruda | Tasa_Ajustada |
|---|---|---|
| Blanco_Total | 8.46 | 6.71 |
| Blanco_Hombres | 8.51 | 7.96 |
| Blanco_Mujeres | 8.42 | 5.74 |
| Negro_Total | 6.70 | 8.00 |
| Negro_Hombres | 7.21 | 9.88 |
| Negro_Mujeres | 6.25 | 6.71 |
| Hispano_Total | 1.39 | 3.09 |
| Hispano_Hombres | 1.44 | 3.45 |
| Hispano_Mujeres | 1.34 | 2.75 |
pesos_us_2000: Es la estructura de edad “ideal” que
usamos para comparar.
for (cat in categorias): Es un Bucle. Le dice a R:
“Repite las instrucciones que están entre corchetes { } para cada
columna de datos”.
La Matemática:
Tasa Cruda: Es el promedio simple. (Total Muertes /
Total Pob).
Tasa Ajustada: Es un promedio ponderado.
Multiplicamos la tasa de cada edad por el “peso” de esa edad en la
población estándar.
Comparamos visualmente las tasas para entender el impacto del ajuste en la población masculina y femenina.
Observa la diferencia entre las barras blancas (Cruda) y negras (Ajustada).
# --- 1. PREPARAR DATOS (TIDY) ---
datos_males <- resumen_ajuste %>%
filter(grepl("_Hombres", Categoria)) %>% # Filtramos solo hombres
mutate(Raza = case_when( # Creamos columna Raza limpia
grepl("Blanco", Categoria) ~ "White",
grepl("Negro", Categoria) ~ "Black",
grepl("Hispano", Categoria) ~ "Hispanic"
)) %>%
pivot_longer(cols = c("Tasa_Cruda", "Tasa_Ajustada"), # De Ancho a Largo
names_to = "Tipo", values_to = "Valor")
# --- 2. GRAFICAR Y GUARDAR EN OBJETO ---
g_males <- ggplot(datos_males, aes(x = Tipo, y = Valor, fill = Raza)) +
geom_bar(stat = "identity", position = "dodge", color = "black") +
geom_text(aes(label = round(Valor, 1)), position = position_dodge(0.9), vjust = -0.5) +
scale_fill_manual(values = c("White"="white", "Black"="black", "Hispanic"="grey70")) +
labs(title = "Mortalidad en Hombres (Males)", y = "Tasa por 1,000 hab.", x = NULL) +
theme_classic()
# Ahora sí lo imprimimos para que se vea en el tutorial
print(g_males)
filter(grepl(...)): Busca un patrón de texto. Aquí
decimos “Quédate solo con las filas que digan ’_Hombres’“.
pivot_longer: Convierte la tabla de formato “Ancho”
(columnas separadas para Cruda y Ajustada) a formato “Largo”.
ggplot2 prefiere el formato largo para poder usar una sola
columna de valores para la altura de las barras.
geom_bar(position = "dodge"): Por defecto, las
barras se apilan. dodge le dice a R que las ponga una al lado de la
otra.
# --- 1. PREPARAR DATOS ---
datos_fem <- resumen_ajuste %>%
filter(grepl("_Mujeres", Categoria)) %>%
mutate(Raza = case_when(
grepl("Blanco", Categoria) ~ "White",
grepl("Negro", Categoria) ~ "Black",
grepl("Hispano", Categoria) ~ "Hispanic"
)) %>%
pivot_longer(cols = c("Tasa_Cruda", "Tasa_Ajustada"),
names_to = "Tipo", values_to = "Valor")
# --- 2. GRAFICAR ---
g_fem <- ggplot(datos_fem, aes(x = Tipo, y = Valor, fill = Raza)) +
geom_bar(stat = "identity", position = "dodge", color = "black") +
geom_text(aes(label = round(Valor, 1)), position = position_dodge(0.9), vjust = -0.5) +
scale_fill_manual(values = c("White"="white", "Black"="black", "Hispanic"="grey70")) +
labs(title = "Mortalidad en Mujeres (Females)", y = "Tasa por 1,000 hab.", x = NULL) +
theme_classic()
# Imprimimos
print(g_fem)
El código es casi idéntico al de hombres, solo cambiamos el filtro
inicial a _Mujeres. Esto demuestra la ventaja de programar:
el código es reutilizable. Una vez resuelves el
problema para un grupo, es muy fácil aplicarlo a otros.
Por último, guardamos las tablas en Excel y las imágenes en alta calidad en la carpeta del proyecto.
# Guardar Excel
write_xlsx(list("Resumen" = resumen_ajuste), "Reporte_Maryland.xlsx")
# Guardar Imágenes PNG
ggsave("Grafico_Hombres.png", plot = g_males, width = 7, height = 5, dpi = 300)
ggsave("Grafico_Mujeres.png", plot = g_fem, width = 7, height = 5, dpi = 300)
list(...): Al exportar a Excel, podemos crear un
archivo con varias hojas (pestañas). Cada elemento dentro de la lista
(list("Nombre Pestaña" = datos)) se convierte en una hoja
del archivo Excel. Explicación para el Estudiante (Sobre este error)
Este es un excelente momento de enseñanza:
“En R, si escribes 2 + 2, la consola te muestra
4 pero lo olvida inmediatamente. Si quieres usar ese
4 después, tienes que decirle:
resultado <- 2 + 2. Con los gráficos pasa lo mismo:
ggplot(...) solo muestra el dibujo.
g_males <- ggplot(...) guarda el dibujo en una variable
para poder exportarlo a un archivo después.”
En esta sección final, calcularemos 6 Razones de Mortalidad Estandarizada (SMR). Compararemos a la población Hispana y Negra (General, Hombres y Mujeres) contra el estándar de la población Blanca.
El objetivo es responder: “¿Tienen estos grupos mayor o menor riesgo de morir que los blancos de su mismo sexo y edad?”
Este bloque define la hoja de Excel y crea la “máquina” (función) que hará los cálculos repetitivos.
# --- 1. CONFIGURACIÓN INICIAL ---
# Nombre de la hoja en el Excel donde están los datos indirectos
hoja_ind <- "Indirect MRs"
# --- 2. DEFINICIÓN DE LA FUNCIÓN ---
# Creamos una función para no repetir el mismo código 6 veces.
# Inputs: Rangos de celdas (Texto) y el número total de muertes observadas (Número).
calcular_un_smr <- function(rango_tasa_blancos, rango_pob_estudio, obs_reales, nombre_grupo) {
# A. Leer Tasa de Referencia (Blancos)
# Usamos col_names = FALSE porque tu diagnóstico demostró que así SÍ lee los datos
df_tasa <- read_excel(archivo, sheet = hoja_ind, range = rango_tasa_blancos, col_names = FALSE)
colnames(df_tasa) <- "Tasa_Ref" # Le ponemos nombre manualmente
# B. Leer Población de Estudio
df_pob <- read_excel(archivo, sheet = hoja_ind, range = rango_pob_estudio, col_names = FALSE)
colnames(df_pob) <- "Pob_Estudio" # Le ponemos nombre manualmente
# C. Calcular Muertes Esperadas
# Convertimos a numérico explícitamente para evitar cualquier error de formato
tasa_num <- as.numeric(df_tasa$Tasa_Ref)
pob_num <- as.numeric(df_pob$Pob_Estudio)
# Cálculo matemático
esperadas <- sum((tasa_num / 1000) * pob_num, na.rm = TRUE)
# D. Calcular SMR e Intervalos
if(esperadas == 0) {
# Si da 0, devolvemos NA para no romper el código
return(data.frame(Grupo = nombre_grupo, Observadas = obs_reales, Esperadas = 0, SMR = NA, Lower_CI = NA, Upper_CI = NA))
}
smr <- obs_reales / esperadas
se <- smr / sqrt(obs_reales)
return(data.frame(
Grupo = nombre_grupo,
Observadas = obs_reales,
Esperadas = round(esperadas, 1),
SMR = round(smr, 3),
Lower_CI = round(smr - (1.96 * se), 3),
Upper_CI = round(smr + (1.96 * se), 3)
))
}
¿Qué es una function()? En programación, cuando tienes
que repetir la misma tarea muchas veces (en este caso, 6 veces), es mala
práctica copiar y pegar el código. En su lugar, creamos una “máquina
personalizada”.
Inputs (Entradas): Le decimos a la máquina: “Toma las tasas de la celda X a la Y, toma la población de Z a W, y usa este número de muertes reales”.
Output (Salida): La máquina nos devuelve una fila limpia con el SMR calculado y sus intervalos de confianza listos.
Automatización: Esto reduce errores humanos. Si te equivocaste en la fórmula del SMR, solo la corriges en un lugar (dentro de la función) y se arregla automáticamente para todos los grupos.
Este gráfico resume todo el análisis de estandarización indirecta.
# --- 3. EJECUCIÓN DE CÁLCULOS ---
# Inicializamos una tabla vacía
tabla_final_smr <- data.frame()
# --- A) POBLACIÓN HISPANA (Columna C vs Tasa Blancos Col B) ---
# 1. Total (Filas 8-18)
tabla_final_smr <- rbind(tabla_final_smr, calcular_un_smr(
"B8:B18", "C8:C18", sum(df_muertes$Hispano_Total), "Hispano - Total"))
# 2. Hombres (Filas 24-34 -> Tomamos 11 filas exactas)
tabla_final_smr <- rbind(tabla_final_smr, calcular_un_smr(
"B24:B34", "C24:C34", sum(df_muertes$Hispano_Hombres), "Hispano - Hombres"))
# 3. Mujeres (Filas 40-50)
tabla_final_smr <- rbind(tabla_final_smr, calcular_un_smr(
"B40:B50", "C40:C50", sum(df_muertes$Hispano_Mujeres), "Hispano - Mujeres"))
# --- B) POBLACIÓN NEGRA (Columna N vs Tasa Blancos Col B) ---
# Nota: La tasa de referencia (Blancos) sigue siendo la Columna B.
# 4. Total (Filas 8-18)
tabla_final_smr <- rbind(tabla_final_smr, calcular_un_smr(
"B8:B18", "N8:N18", sum(df_muertes$Negro_Total), "Negro - Total"))
# 5. Hombres (Filas 24-34)
tabla_final_smr <- rbind(tabla_final_smr, calcular_un_smr(
"B24:B34", "N24:N34", sum(df_muertes$Negro_Hombres), "Negro - Hombres"))
# 6. Mujeres (Filas 40-50)
tabla_final_smr <- rbind(tabla_final_smr, calcular_un_smr(
"B40:B50", "N40:N50", sum(df_muertes$Negro_Mujeres), "Negro - Mujeres"))
# Mostrar tabla final
knitr::kable(tabla_final_smr, caption = "Resumen SMR: Comparación vs. Población Blanca")
| Grupo | Observadas | Esperadas | SMR | Lower_CI | Upper_CI |
|---|---|---|---|---|---|
| Hispano - Total | 741 | 1700.2 | 0.436 | 0.404 | 0.467 |
| Hispano - Hombres | 402 | 763.9 | 0.526 | 0.475 | 0.578 |
| Hispano - Mujeres | 339 | 936.3 | 0.362 | 0.324 | 0.401 |
| Negro - Total | 12355 | 10754.6 | 1.149 | 1.129 | 1.169 |
| Negro - Hombres | 6219 | 4202.0 | 1.480 | 1.443 | 1.517 |
| Negro - Mujeres | 6136 | 6552.6 | 0.936 | 0.913 | 0.960 |
La Lógica de Comparación:
Para los Hispanos, usamos las Tasas de Blancos (Columna B) multiplicadas por la Población Hispana (Columna C).
Para los Negros, usamos las mismas Tasas de Blancos (Columna B) multiplicadas por la Población Negra (Columna N).
¿Por qué siempre la Columna B? Porque la Población Blanca es nuestro Estándar de Referencia (“The Gold Standard”) contra el cual medimos a los demás.
rbind (Row Bind): Significa “Unir Filas”. Imaginen que
estamos apilando ladrillos. Calculamos una fila, la ponemos en la tabla
tabla_final_smr, calculamos la siguiente y la ponemos
encima, y así sucesivamente.
Este bloque genera el gráfico de barras con intervalos de confianza y colorea según el riesgo.
# --- 4. VISUALIZACIÓN FINAL ---
# Añadimos una columna para colorear automáticamente (Rojo = Malo, Verde = Bueno)
tabla_final_smr <- tabla_final_smr %>%
mutate(Condicion = ifelse(SMR > 1, "Mayor Riesgo", "Menor Riesgo"))
# Crear Gráfico
g_final <- ggplot(tabla_final_smr, aes(x = Grupo, y = SMR, fill = Condicion)) +
geom_bar(stat = "identity", color = "black", width = 0.7) +
# Barras de error (Intervalo de Confianza)
geom_errorbar(aes(ymin = Lower_CI, ymax = Upper_CI), width = 0.2) +
# Línea de Referencia (SMR = 1)
geom_hline(yintercept = 1, color = "red", linetype = "dashed", size = 1.2) +
# Etiquetas de texto sobre las barras
geom_text(aes(label = SMR), vjust = -2.5, fontface = "bold") +
# Colores personalizados
scale_fill_manual(values = c("Mayor Riesgo" = "#ff6666", "Menor Riesgo" = "#66cc66")) +
# Títulos y Ejes
labs(title = "SMR: Comparación con Población Blanca",
subtitle = "SMR = 1 indica riesgo idéntico a los Blancos (Línea Roja)",
y = "Razón de Mortalidad Estandarizada",
x = NULL) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) # Rotar etiquetas del eje X
# Imprimir y Guardar
print(g_final)
ggsave("SMR_Completo.png", plot = g_final, width = 10, height = 6)
Interpretación Visual:
Línea Roja (y = 1): Es la frontera del riesgo normal (el riesgo de los Blancos).
Barras Verdes (SMR < 1): Significan “Protección” o menor mortalidad. Por ejemplo, si la barra de Hispanos está en 0.5, significa que mueren la mitad de lo esperado comparado con los blancos. Esto se conoce en epidemiología como la “Paradoja Hispana”.
Barras Rojas (SMR > 1): Significan “Exceso de Riesgo”. Si la barra de Negros está en 1.2, tienen un 20% más riesgo de muerte que los blancos de la misma edad.
Barras de Error (las “antenas”): Representan el intervalo de confianza. Si la línea negra cruza la línea roja punteada, la diferencia no es estadísticamente significativa (podría ser casualidad).
# --- BLOQUE DE DIAGNÓSTICO DE EXCEL ---
library(readxl)
# 1. Verificar si el archivo existe
print(paste("¿Archivo encontrado?:", file.exists(archivo)))
## [1] "¿Archivo encontrado?: TRUE"
# 2. Listar las pestañas que R ve en el archivo
# (Fíjate si "Indirect MRs" aparece EXACTAMENTE así en la lista)
print("Pestañas disponibles en el Excel:")
## [1] "Pestañas disponibles en el Excel:"
print(excel_sheets(archivo))
## [1] "Maryland Deaths" "Maryland Population" "Direct MRs"
## [4] "Indirect MRs"
# 3. VER LA REALIDAD: ¿Qué hay exactamente en B8:B18?
# Leemos sin encabezados para ver el dato crudo
print("--- CONTENIDO DE TASAS (B8:B18) ---")
## [1] "--- CONTENIDO DE TASAS (B8:B18) ---"
test_rango <- read_excel(archivo, sheet = "Indirect MRs", range = "B8:B18", col_names = FALSE)
print(test_rango)
## # A tibble: 11 × 1
## ...1
## <dbl>
## 1 3.58
## 2 0.156
## 3 0.0678
## 4 0.574
## 5 0.947
## 6 1.77
## 7 4.43
## 8 9.15
## 9 16.0
## 10 46.1
## 11 134.
# 4. VER LA REALIDAD: ¿Qué hay exactamente en C8:C18?
print("--- CONTENIDO DE POBLACIÓN (C8:C18) ---")
## [1] "--- CONTENIDO DE POBLACIÓN (C8:C18) ---"
test_pob <- read_excel(archivo, sheet = "Indirect MRs", range = "C8:C18", col_names = FALSE)
print(test_pob)
## # A tibble: 11 × 1
## ...1
## <dbl>
## 1 11887
## 2 45679
## 3 90447
## 4 81986
## 5 103347
## 6 87939
## 7 58276
## 8 30953
## 9 13504
## 10 6073
## 11 2283