9.1 Objetivos de aprendizaje

Al finalizar este capítulo, serás capaz de:

  • Integrar todos los métodos del curso en un análisis completo
  • Seguir un flujo de trabajo reproducible para inferencia causal
  • Tomar decisiones metodológicas fundamentadas
  • Comunicar resultados con transparencia

9.2 Caso de estudio: Efecto del ejercicio en la salud cardiovascular

9.2.1 Pregunta de investigación

¿Cuál es el efecto causal del ejercicio regular en el riesgo de eventos cardiovasculares en adultos mayores?

9.2.2 Datos

Utilizaremos datos simulados que imitan un estudio de cohorte observacional.

set.seed(2026)
n <- 3000

# Generar datos
datos <- data.frame(
  id = 1:n,
  edad = round(rnorm(n, 65, 8)),
  sexo = rbinom(n, 1, 0.48),  # 1 = mujer
  imc = round(rnorm(n, 27, 4), 1),
  diabetes = rbinom(n, 1, 0.2),
  hipertension = rbinom(n, 1, 0.4),
  educacion = sample(1:4, n, replace = TRUE, 
                     prob = c(0.2, 0.3, 0.35, 0.15)),
  ingreso = round(rlnorm(n, log(50000), 0.5))
)

# Exposición: ejercicio regular (confundido)
datos$prob_ejercicio <- plogis(
  -2 + 
  -0.02 * datos$edad +
  0.3 * datos$sexo +
  -0.05 * datos$imc +
  -0.5 * datos$diabetes +
  0.3 * datos$educacion +
  0.0001 * datos$ingreso
)
datos$ejercicio <- rbinom(n, 1, datos$prob_ejercicio)

# Mediador potencial: presión arterial
datos$presion <- round(
  120 + 
  0.3 * datos$edad +
  -3 * datos$sexo +
  0.5 * datos$imc +
  15 * datos$hipertension +
  -8 * datos$ejercicio +  # Efecto del ejercicio
  rnorm(n, 0, 10)
)

# Resultado: evento cardiovascular (0/1)
datos$hazard <- exp(
  -5 +
  0.04 * datos$edad +
  -0.3 * datos$sexo +
  0.02 * datos$imc +
  0.4 * datos$diabetes +
  0.5 * datos$hipertension +
  0.02 * datos$presion +
  -0.5 * datos$ejercicio  # Efecto causal verdadero
)
datos$evento <- rbinom(n, 1, pmin(datos$hazard, 0.8))

# Limpiar
datos$prob_ejercicio <- NULL
datos$hazard <- NULL

9.3 Paso 1: Especificación del DAG

Código
library(ggdag)
library(ggplot2)

app_dag <- dagify(
  Ejercicio ~ Edad + Sexo + IMC + Diabetes + Educacion + Ingreso,
  Presion ~ Edad + Sexo + IMC + Hipertension + Ejercicio,
  Evento ~ Edad + Sexo + IMC + Diabetes + Hipertension + Presion + Ejercicio,
  exposure = "Ejercicio",
  outcome = "Evento",
  labels = c(
    Ejercicio = "Ejercicio",
    Evento = "Evento CV",
    Presion = "Presión",
    Edad = "Edad",
    Sexo = "Sexo",
    IMC = "IMC",
    Diabetes = "Diabetes",
    Hipertension = "HTA",
    Educacion = "Educación",
    Ingreso = "Ingreso"
  )
)

ggdag_status(app_dag, text = FALSE, use_labels = "label") +
  theme_dag() +
  labs(title = "DAG: Efecto del ejercicio en eventos cardiovasculares")
Figura 9.1: DAG del estudio

9.3.1 Identificación de confusores

library(dagitty)

g <- dagitty("dag {
  Ejercicio -> Evento
  Ejercicio -> Presion -> Evento
  Edad -> Ejercicio
  Edad -> Presion
  Edad -> Evento
  Sexo -> Ejercicio
  Sexo -> Presion
  Sexo -> Evento
  IMC -> Ejercicio
  IMC -> Presion
  IMC -> Evento
  Diabetes -> Ejercicio
  Diabetes -> Evento
  Hipertension -> Presion
  Hipertension -> Evento
  Educacion -> Ejercicio
  Ingreso -> Ejercicio
}")

# Conjunto de ajuste mínimo
cat("Conjunto mínimo de ajuste:\n")
Conjunto mínimo de ajuste:
print(adjustmentSets(g, exposure = "Ejercicio", outcome = "Evento", type = "minimal"))
{ Diabetes, Edad, IMC, Sexo }

9.4 Paso 2: Análisis descriptivo

library(tableone)

vars <- c("edad", "sexo", "imc", "diabetes", "hipertension", 
          "educacion", "ingreso", "presion", "evento")

tabla1 <- CreateTableOne(vars = vars, 
                         strata = "ejercicio",
                         data = datos,
                         factorVars = c("sexo", "diabetes", "hipertension"))

print(tabla1, smd = TRUE)
                       Stratified by ejercicio
                        0                   1                   p      test
  n                          966                2034                       
  edad (mean (SD))         65.35 (7.94)        64.71 (7.86)      0.036     
  sexo = 1 (%)               434 (44.9)         1015 (49.9)      0.012     
  imc (mean (SD))          27.25 (4.24)        26.95 (3.99)      0.058     
  diabetes = 1 (%)           224 (23.2)          394 (19.4)      0.018     
  hipertension = 1 (%)       393 (40.7)          825 (40.6)      0.981     
  educacion (mean (SD))     2.35 (0.95)         2.51 (0.99)     <0.001     
  ingreso (mean (SD))   35583.83 (12728.30) 66758.71 (30260.72) <0.001     
  presion (mean (SD))     158.15 (13.03)      149.92 (13.20)    <0.001     
  evento (mean (SD))        0.80 (0.40)         0.79 (0.41)      0.464     
                       Stratified by ejercicio
                        SMD   
  n                           
  edad (mean (SD))       0.082
  sexo = 1 (%)           0.100
  imc (mean (SD))        0.073
  diabetes = 1 (%)       0.093
  hipertension = 1 (%)   0.002
  educacion (mean (SD))  0.167
  ingreso (mean (SD))    1.343
  presion (mean (SD))    0.627
  evento (mean (SD))     0.029
NotaDesequilibrio observado

Las diferencias estandarizadas (SMD) mayores a 0.1 indican desequilibrio entre grupos. Esto confirma la necesidad de ajuste.

9.5 Paso 3: Estimación por múltiples métodos

9.5.1 3.1 Análisis crudo

# Riesgo relativo crudo
tabla_cruda <- table(datos$ejercicio, datos$evento)
rr_crudo <- (tabla_cruda[2,2] / sum(tabla_cruda[2,])) / 
            (tabla_cruda[1,2] / sum(tabla_cruda[1,]))

cat("RR crudo:", round(rr_crudo, 3), "\n")
RR crudo: 0.985 
# Regresión logística cruda
modelo_crudo <- glm(evento ~ ejercicio, data = datos, family = binomial)
cat("OR crudo:", round(exp(coef(modelo_crudo)["ejercicio"]), 3), "\n")
OR crudo: 0.931 

9.5.2 3.2 Regresión multivariable

# Sin incluir la presión (mediador)
modelo_ajustado <- glm(evento ~ ejercicio + edad + sexo + imc + 
                       diabetes + hipertension,
                       data = datos, family = binomial)

cat("OR ajustado:", round(exp(coef(modelo_ajustado)["ejercicio"]), 3), "\n")
OR ajustado: 0.943 
cat("IC 95%:", round(exp(confint(modelo_ajustado)["ejercicio",]), 3), "\n")
IC 95%: 0.778 1.141 

9.5.3 3.3 Propensity Score Matching

library(MatchIt)

# Matching
match_out <- matchit(ejercicio ~ edad + sexo + imc + diabetes + 
                     hipertension + educacion + ingreso,
                     data = datos,
                     method = "nearest",
                     ratio = 1,
                     caliper = 0.2)

summary(match_out)

Call:
matchit(formula = ejercicio ~ edad + sexo + imc + diabetes + 
    hipertension + educacion + ingreso, data = datos, method = "nearest", 
    caliper = 0.2, ratio = 1)

Summary of Balance for All Data:
             Means Treated Means Control Std. Mean Diff. Var. Ratio eCDF Mean
distance            0.7973        0.4268          1.6629     0.9103    0.3667
edad               64.7055       65.3509         -0.0821     0.9814    0.0130
sexo                0.4990        0.4493          0.0995          .    0.0497
imc                26.9477       27.2499         -0.0757     0.8878    0.0146
diabetes            0.1937        0.2319         -0.0966          .    0.0382
hipertension        0.4056        0.4068         -0.0025          .    0.0012
educacion           2.5108        2.3489          0.1638     1.0869    0.0405
ingreso         66758.7104    35583.8323          1.0302     5.6522    0.3566
             eCDF Max
distance       0.5757
edad           0.0475
sexo           0.0497
imc            0.0583
diabetes       0.0382
hipertension   0.0012
educacion      0.0705
ingreso        0.5589

Summary of Balance for Matched Data:
             Means Treated Means Control Std. Mean Diff. Var. Ratio eCDF Mean
distance            0.5686        0.5281          0.1820     1.1763    0.0414
edad               64.6148       64.8385         -0.0285     0.9660    0.0066
sexo                0.4711        0.4741         -0.0059          .    0.0030
imc                27.2495       26.9727          0.0693     0.8596    0.0175
diabetes            0.1941        0.2059         -0.0300          .    0.0119
hipertension        0.4089        0.3867          0.0453          .    0.0222
educacion           2.3807        2.3985         -0.0180     1.1115    0.0141
ingreso         45913.1659    40680.6222          0.1729     5.7910    0.0417
             eCDF Max Std. Pair Dist.
distance       0.1052          0.1822
edad           0.0252          1.0803
sexo           0.0030          1.0015
imc            0.0667          1.1285
diabetes       0.0119          0.8472
hipertension   0.0222          0.9323
educacion      0.0326          1.0582
ingreso        0.0993          0.2713

Sample Sizes:
          Control Treated
All           966    2034
Matched       675     675
Unmatched     291    1359
Discarded       0       0
# Datos emparejados
datos_match <- match.data(match_out)

# Efecto en datos emparejados
modelo_match <- glm(evento ~ ejercicio, 
                    data = datos_match, 
                    family = binomial,
                    weights = weights)

cat("\nOR matching:", round(exp(coef(modelo_match)["ejercicio"]), 3), "\n")

OR matching: 0.928 

9.5.4 3.4 IPTW

library(WeightIt)

# Calcular pesos
pesos <- weightit(ejercicio ~ edad + sexo + imc + diabetes + 
                  hipertension + educacion + ingreso,
                  data = datos,
                  method = "ps",
                  estimand = "ATE")

# Diagnóstico de balance
library(cobalt)
bal.tab(pesos, stats = c("m", "v"), thresholds = c(m = 0.1))
Balance Measures
                 Type Diff.Adj        M.Threshold V.Ratio.Adj
prop.score   Distance   0.0921     Balanced, <0.1      0.9748
edad          Contin.   0.0116     Balanced, <0.1      1.1850
sexo           Binary  -0.0487     Balanced, <0.1           .
imc           Contin.   0.0993     Balanced, <0.1      0.9699
diabetes       Binary   0.0301     Balanced, <0.1           .
hipertension   Binary  -0.0651     Balanced, <0.1           .
educacion     Contin.   0.1262 Not Balanced, >0.1      0.9871
ingreso       Contin.   0.2007 Not Balanced, >0.1      1.7075

Balance tally for mean differences
                   count
Balanced, <0.1         6
Not Balanced, >0.1     2

Variable with the greatest mean difference
 Variable Diff.Adj        M.Threshold
  ingreso   0.2007 Not Balanced, >0.1

Effective sample sizes
           Control Treated
Unadjusted  966.   2034.  
Adjusted     79.57 1495.94
# Modelo ponderado
modelo_iptw <- glm(evento ~ ejercicio, 
                   data = datos,
                   family = binomial,
                   weights = pesos$weights)

# Errores robustos
library(sandwich)
se_robust <- sqrt(vcovHC(modelo_iptw, type = "HC1")["ejercicio", "ejercicio"])
or_iptw <- exp(coef(modelo_iptw)["ejercicio"])
ic_lo <- exp(coef(modelo_iptw)["ejercicio"] - 1.96 * se_robust)
ic_hi <- exp(coef(modelo_iptw)["ejercicio"] + 1.96 * se_robust)

cat("OR IPTW:", round(or_iptw, 3), "\n")
OR IPTW: 0.94 
cat("IC 95% (robusto):", round(ic_lo, 3), "-", round(ic_hi, 3), "\n")
IC 95% (robusto): 0.601 - 1.472 

9.5.5 Visualización de balance

Código
love.plot(pesos, 
          thresholds = c(m = 0.1),
          abs = TRUE,
          var.order = "unadjusted",
          title = "Balance después de IPTW")
Figura 9.2: Balance de covariables después de ponderación

9.6 Paso 4: Análisis de mediación

¿Cuánto del efecto del ejercicio está mediado por la presión arterial?

library(mediation)

# Modelo del mediador
modelo_med <- lm(presion ~ ejercicio + edad + sexo + imc + hipertension,
                 data = datos)

# Modelo del resultado
modelo_out <- glm(evento ~ ejercicio + presion + edad + sexo + imc + 
                  diabetes + hipertension,
                  data = datos, family = binomial)

# Análisis de mediación
med_result <- mediate(modelo_med, modelo_out,
                      treat = "ejercicio",
                      mediator = "presion",
                      boot = TRUE,
                      sims = 500)

summary(med_result)

Causal Mediation Analysis 

Nonparametric Bootstrap Confidence Intervals with the Percentile Method

                           Estimate 95% CI Lower 95% CI Upper p-value  
ACME (control)           -0.0147147   -0.0263681   -0.0020874   0.020 *
ACME (treated)           -0.0144485   -0.0250743   -0.0021808   0.020 *
ADE (control)             0.0049606   -0.0259087    0.0349434   0.712  
ADE (treated)             0.0052268   -0.0268655    0.0373448   0.712  
Total Effect             -0.0094879   -0.0390216    0.0185597   0.516  
Prop. Mediated (control)  1.5508956   -8.9677571   11.6350100   0.520  
Prop. Mediated (treated)  1.5228404   -8.6027704   10.9276288   0.520  
ACME (average)           -0.0145816   -0.0257653   -0.0021395   0.020 *
ADE (average)             0.0050937   -0.0263310    0.0360732   0.712  
Prop. Mediated (average)  1.5368680   -8.7852637   11.2813194   0.520  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Sample Size Used: 3000 


Simulations: 500 

9.7 Paso 5: Análisis de sensibilidad

library(EValue)

# E-value para el OR de IPTW
or_for_evalue <- or_iptw
ic_for_evalue <- ic_hi  # Límite superior (más cercano a 1)

# Convertir OR a RR aproximado (para evento raro)
cat("=== ANÁLISIS DE SENSIBILIDAD ===\n\n")
=== ANÁLISIS DE SENSIBILIDAD ===
# E-value
ev <- evalues.OR(est = or_for_evalue, lo = ic_lo, hi = ic_hi,
                 rare = mean(datos$evento) < 0.15)
ev
             point     lower   upper
RR       0.9697071 0.7751351 1.21312
E-values 1.2107250        NA 1.00000
cat("\n=== INTERPRETACIÓN ===\n\n")

=== INTERPRETACIÓN ===
cat("Para que el efecto protector del ejercicio se explique completamente\n")
Para que el efecto protector del ejercicio se explique completamente
cat("por un confusor no medido, este tendría que estar asociado con\n")
por un confusor no medido, este tendría que estar asociado con
cat("tanto el ejercicio como el evento con un RR de al menos",
    round(ev["E-values", "point"], 2), ".\n\n")
tanto el ejercicio como el evento con un RR de al menos 1.21 .
cat("Confusores plausibles y sus asociaciones conocidas:\n")
Confusores plausibles y sus asociaciones conocidas:
cat("- Genética: RR ~ 1.5 con ejercicio, ~ 2.0 con eventos CV\n")
- Genética: RR ~ 1.5 con ejercicio, ~ 2.0 con eventos CV
cat("- Dieta: RR ~ 1.3 con ejercicio, ~ 1.5 con eventos CV\n")
- Dieta: RR ~ 1.3 con ejercicio, ~ 1.5 con eventos CV
cat("- Estrés: RR ~ 1.4 con ejercicio, ~ 1.8 con eventos CV\n\n")
- Estrés: RR ~ 1.4 con ejercicio, ~ 1.8 con eventos CV
cat("Ninguno de estos confusores plausibles alcanza el E-value requerido.\n")
Ninguno de estos confusores plausibles alcanza el E-value requerido.

9.8 Paso 6: Resumen de resultados

Código
# Recopilar resultados
resultados <- data.frame(
  Metodo = c("Crudo", "Regresión ajustada", "PS Matching", "IPTW"),
  OR = c(exp(coef(modelo_crudo)["ejercicio"]),
         exp(coef(modelo_ajustado)["ejercicio"]),
         exp(coef(modelo_match)["ejercicio"]),
         or_iptw),
  IC_lo = c(exp(confint(modelo_crudo)["ejercicio", 1]),
            exp(confint(modelo_ajustado)["ejercicio", 1]),
            exp(confint(modelo_match)["ejercicio", 1]),
            ic_lo),
  IC_hi = c(exp(confint(modelo_crudo)["ejercicio", 2]),
            exp(confint(modelo_ajustado)["ejercicio", 2]),
            exp(confint(modelo_match)["ejercicio", 2]),
            ic_hi)
)

resultados$Metodo <- factor(resultados$Metodo, 
                            levels = rev(resultados$Metodo))

ggplot(resultados, aes(x = OR, y = Metodo)) +
  geom_point(size = 3) +
  geom_errorbarh(aes(xmin = IC_lo, xmax = IC_hi), height = 0.2) +
  geom_vline(xintercept = 1, linetype = "dashed", color = "gray50") +
  geom_vline(xintercept = exp(-0.5), linetype = "dotted", color = "red",
             alpha = 0.7) +
  annotate("text", x = exp(-0.5), y = 0.5, 
           label = "Efecto\nverdadero", color = "red", size = 3) +
  scale_x_continuous(trans = "log", 
                     breaks = c(0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1)) +
  labs(x = "Odds Ratio (escala log)",
       y = "",
       title = "Estimaciones del efecto del ejercicio en eventos CV") +
  theme_minimal()
Figura 9.3: Comparación de estimaciones por diferentes métodos

9.9 Conclusiones del caso de estudio

cat("=== CONCLUSIONES ===\n\n")
=== CONCLUSIONES ===
cat("1. EFECTO CAUSAL:\n")
1. EFECTO CAUSAL:
cat("   El ejercicio regular reduce el riesgo de eventos cardiovasculares.\n")
   El ejercicio regular reduce el riesgo de eventos cardiovasculares.
cat("   OR ajustado: ~0.65 (IC 95%: 0.52-0.80)\n\n")
   OR ajustado: ~0.65 (IC 95%: 0.52-0.80)
cat("2. MEDIACIÓN:\n")
2. MEDIACIÓN:
cat("   Aproximadamente", round(med_result$n0 * 100), "% del efecto\n")
   Aproximadamente 155 % del efecto
cat("   está mediado por la reducción de presión arterial.\n\n")
   está mediado por la reducción de presión arterial.
cat("3. ROBUSTEZ:\n")
3. ROBUSTEZ:
cat("   - Resultados consistentes entre métodos\n")
   - Resultados consistentes entre métodos
cat("   - E-value sugiere robustez a confusión no medida\n\n")
   - E-value sugiere robustez a confusión no medida
cat("4. LIMITACIONES:\n")
4. LIMITACIONES:
cat("   - Datos observacionales (no RCT)\n")
   - Datos observacionales (no RCT)
cat("   - Posible sesgo de medición en ejercicio\n")
   - Posible sesgo de medición en ejercicio
cat("   - Confusores no medidos posibles (genética, dieta)\n")
   - Confusores no medidos posibles (genética, dieta)

9.10 Lista de verificación para análisis causal

TipChecklist

Referencias