Regresión logística condicional
Introducción
En algunos estudios de casos y controles, se utiliza un diseño apareado para controlar la influencia de factores de confusión como la edad, el sexo o el lugar de residencia. La regresión logística condicional (RLC) es una extensión de la regresión logística clásica que incorpora el apareamiento de los datos, estratificando el análisis por grupos definidos por las variables de emparejamiento (estratos).
Este modelo permite estimar el efecto de las covariables sobre una variable dependiente binaria mientras controla de manera efectiva los efectos de las variables de emparejamiento o agrupamiento. Es especialmente útil en estudios de casos y controles apareados, investigaciones multicéntricas y estudios observacionales que abarcan múltiples ubicaciones geográficas, proporcionando un enfoque robusto para manejar la heterogeneidad dentro de los datos.
Modelo logístico condicional
El modelo logístico condicional utiliza una función de verosimilitud condicional que elimina la necesidad de estimar los efectos de los factores de emparejamiento como interceptos adicionales. Este enfoque resulta especialmente útil cuando hay un gran número de grupos o estratos, ya que permite concentrarse exclusivamente en los coeficientes de las covariables de interés.
Se modela la probabilidad de que un evento ocurra en función de las covariables dentro de cada estrato, definido por las variables de emparejamiento. La función de verosimilitud condicional se puede expresar matemáticamente como:
\[ logit(p) = \alpha_1 + \alpha_2 z_2 + ... + \alpha_s z_s + \beta_1 x_1 + ... + \beta_p x_p \]
donde:
\(\alpha_1 + \alpha_2 z_2 + ... + \alpha_s z_s\): Coeficientes asociados a las variables de estratificación \(z_s\), que identifican los estratos.
\(z_1, z_2, ..., z_s\): Variables de emparejamiento o estratos.
\(\beta_1, ..., \beta_p\): Efectos de las variables independientes ajustadas por los estratos.
\(x_1, ... x_p\): Variables independientes incluídas en el modelo.
El uso de la verosimilitud condicional simplifica el modelo al evitar la estimación de un gran número de interceptos y reduce el sesgo en situaciones con pocos datos por estrato.
Supuestos de la regresión logística condicional
Independencia dentro de los estratos: Las observaciones son independientes entre los diferentes estratos o grupos definidos por el emparejamiento, pero pueden estar correlacionadas dentro de los mismos.
Relación logit-lineal: Existe una relación lineal entre el logit de la probabilidad del evento y las covariables explicativas.
No existencia de confusión residual: Las variables de emparejamiento deben ser suficientes para controlar por los factores de confusión.
Tamaño adecuado del estrato: Cada estrato debe tener al menos un caso y un control.
Ejemplo práctico en R
En R podemos ajustar modelos de regresión logística condicional usando la función clogit() del paquete survival (Therneau 2024).
Cargamos los paquetes necesarios:
Usaremos la base “higado_graso.txt” que contiene datos de un estudio de casos y controles apareados por sexo y edad para muertes por hígado graso no alcohólico:
# Carga datos
datos <- read_delim("datos/higado_graso.txt")|>
# Cambia nivel de referencia de fib4
mutate(fib4_cat = fct_rev(fib4_cat))
# Explora datos
glimpse(datos)Rows: 2,467
Columns: 11
$ id <dbl> 19, 25, 26, 32, 56, 57, 68, 77, 78, 99, 100, 120, 126, 13…
$ tipo <dbl> 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, …
$ edad <dbl> 35, 39, 55, 45, 51, 81, 51, 55, 32, 33, 50, 82, 56, 69, 7…
$ sexo <dbl> 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, …
$ bmi <dbl> 30.49939, 24.26164, 30.59225, 22.11900, 36.42498, 26.3815…
$ hdl <dbl> 50.09091, 52.50000, 49.13043, 70.55556, 45.10526, 67.7500…
$ p_sistolica <dbl> NA, NA, 134.2500, NA, 164.0000, 148.0833, 124.5000, NA, N…
$ p_diastolica <dbl> NA, NA, 134.2500, NA, 164.0000, 148.0833, 124.5000, NA, N…
$ fib4_cat <fct> bajo riesgo, bajo riesgo, bajo riesgo, bajo riesgo, bajo …
$ fuma <dbl> 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, …
$ status <dbl> 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, …
El dataset tiene 2467 observaciones y 11 columnas. Las variables de interés son las siguientes:
id: identificador único del paciente.tipo: Indica si el paciente tiene hígado graso no alcohólico (caso: 1) o no presenta la condición (control: 0).edad: Edad del paciente al momento de su ingreso al hospital.sexo: Sexo biológico del paciente (1: masculino, 0: femenino).bmi: Índice de masa corporal del paciente al momento de la hospitalización.hdl: Nivel de lipoproteínas de alta densidad (HDL) en sangre.p_sistolica: Presión sistólica en milímetros de mercurio (mmHg).p_diastolica: Presión diastólica en milímetros de mercurio (mmHg).fib4_cat: Índice de fibrosis hepática categorizado (bajo riesgo, alto riesgo).fuma: Tabaquismo (0: no, 1: sí).status: Variable dependiente que toma el valor de 0 si la persona sobrevivió y 1 si falleció.
Exploremos más en profundidad los datos usando la función tabyl() del paquete janitor y la función skim() del paquete skimr:
# Número de casos y controles
tabyl(datos$tipo) |>
adorn_pct_formatting() datos$tipo n percent
0 1685 68.3%
1 782 31.7%
# Niveles fib4
tabyl(datos$fib4_cat) datos$fib4_cat n percent
bajo riesgo 2109 0.8548845
alto riesgo 358 0.1451155
# Explora valores ausentes
skim(datos)| Name | datos |
| Number of rows | 2467 |
| Number of columns | 11 |
| _______________________ | |
| Column type frequency: | |
| factor | 1 |
| numeric | 10 |
| ________________________ | |
| Group variables | None |
Variable type: factor
| skim_variable | n_missing | complete_rate | ordered | n_unique | top_counts |
|---|---|---|---|---|---|
| fib4_cat | 0 | 1 | FALSE | 2 | baj: 2109, alt: 358 |
Variable type: numeric
| skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
|---|---|---|---|---|---|---|---|---|---|---|
| id | 0 | 1.00 | 8644.27 | 4998.09 | 19.00 | 4486.00 | 8507.00 | 12864.00 | 17563.0 | ▇▇▇▇▇ |
| tipo | 0 | 1.00 | 0.32 | 0.47 | 0.00 | 0.00 | 0.00 | 1.00 | 1.0 | ▇▁▁▁▃ |
| edad | 0 | 1.00 | 53.60 | 14.73 | 18.00 | 44.00 | 53.00 | 64.00 | 98.0 | ▂▆▇▃▁ |
| sexo | 0 | 1.00 | 0.50 | 0.50 | 0.00 | 0.00 | 0.00 | 1.00 | 1.0 | ▇▁▁▁▇ |
| bmi | 0 | 1.00 | 31.22 | 7.51 | 9.21 | 26.02 | 30.14 | 35.04 | 84.4 | ▂▇▁▁▁ |
| hdl | 0 | 1.00 | 50.92 | 14.94 | 19.83 | 40.00 | 48.53 | 58.50 | 134.0 | ▅▇▂▁▁ |
| p_sistolica | 928 | 0.62 | 131.41 | 17.11 | 40.00 | 121.53 | 132.50 | 142.67 | 211.0 | ▁▁▇▃▁ |
| p_diastolica | 928 | 0.62 | 131.41 | 17.11 | 40.00 | 121.53 | 132.50 | 142.67 | 211.0 | ▁▁▇▃▁ |
| fuma | 0 | 1.00 | 0.64 | 0.48 | 0.00 | 0.00 | 1.00 | 1.00 | 1.0 | ▅▁▁▁▇ |
| status | 0 | 1.00 | 0.12 | 0.32 | 0.00 | 0.00 | 0.00 | 0.00 | 1.0 | ▇▁▁▁▁ |
Las variables p_sistolica y p_diastolica tienen gran cantidad de valores ausentes, por lo que no serán consideradas para el análisis.
Como no tenemos definido a que caso corresponde cada control, vamos a aparear casos y controles usando la función matchit() del paquete MatchIt (Ho et al. 2011), especificando que queremos dos controles por caso con el argumento ratio = 2:
Call:
matchit(formula = tipo ~ edad + sexo, data = datos, exact = "sexo",
ratio = 2)
Summary of Balance for All Data:
Means Treated Means Control Std. Mean Diff. Var. Ratio eCDF Mean
distance 0.3268 0.3124 0.2552 1.0589 0.0507
edad 51.0243 54.7923 -0.2550 1.0302 0.0486
sexo 0.4962 0.5003 -0.0083 . 0.0041
eCDF Max
distance 0.1284
edad 0.1274
sexo 0.0041
Summary of Balance for Matched Data:
Means Treated Means Control Std. Mean Diff. Var. Ratio eCDF Mean
distance 0.3268 0.3181 0.1549 1.1495 0.0301
edad 51.0243 53.2270 -0.1490 1.1373 0.0288
sexo 0.4962 0.4962 0.0000 . 0.0000
eCDF Max Std. Pair Dist.
distance 0.0882 0.1578
edad 0.0838 0.1528
sexo 0.0000 0.0000
Sample Sizes:
Control Treated
All 1685 782
Matched 1564 782
Unmatched 121 0
Discarded 0 0
Obtenemos los datos apareados:
datos <- match.data(match, drop.unmatched = T)
names(datos) [1] "id" "tipo" "edad" "sexo" "bmi"
[6] "hdl" "p_sistolica" "p_diastolica" "fib4_cat" "fuma"
[11] "status" "distance" "weights" "subclass"
Ajustamos el modelo de regresión logística condicional, usando la variable subclass como variable de estratificación:
Revisemos la salida del modelo:
summary(mod_cond)Call:
coxph(formula = Surv(rep(1, 2346L), status) ~ bmi + hdl + fuma +
fib4_cat + strata(subclass), data = datos, method = "exact")
n= 2346, number of events= 251
coef exp(coef) se(coef) z Pr(>|z|)
bmi 0.005507 1.005522 0.012814 0.430 0.667
hdl -0.006502 0.993519 0.006980 -0.931 0.352
fuma -0.237017 0.788978 0.186629 -1.270 0.204
fib4_catalto riesgo 1.072163 2.921692 0.224099 4.784 1.72e-06 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
exp(coef) exp(-coef) lower .95 upper .95
bmi 1.0055 0.9945 0.9806 1.031
hdl 0.9935 1.0065 0.9800 1.007
fuma 0.7890 1.2675 0.5473 1.137
fib4_catalto riesgo 2.9217 0.3423 1.8831 4.533
Concordance= 0.625 (se = 0.039 )
Likelihood ratio test= 27.94 on 4 df, p=1e-05
Wald test = 25.92 on 4 df, p=3e-05
Score (logrank) test = 28.23 on 4 df, p=1e-05
Probemos eliminar una de las variables explicativas no significativas a la vez:
Comparamos los modelos
# Compara modelos
compare_performance(mod_cond, mod_cond1, mod_cond2, mod_cond3,
metrics = "common")# Comparison of Model Performance Indices
Name | Model | AIC (weights) | BIC (weights) | Nagelkerke's R2 | RMSE
----------------------------------------------------------------------------
mod_cond | clogit | 393.1 (0.452) | 416.2 (0.044) | 0.073 | 0.230
mod_cond1 | clogit | 392.8 (0.548) | 410.0 (0.956) | 0.069 | 0.230
mod_cond2 | clogit | 921.1 (<.001) | 938.3 (<.001) | 0.055 | 0.276
mod_cond3 | clogit | 923.5 (<.001) | 940.8 (<.001) | 0.052 | 0.276
# Compara modelos y ordena según performance
compare_performance(mod_cond, mod_cond1, mod_cond2, mod_cond3,
metrics = "common", rank = T)# Comparison of Model Performance Indices
Name | Model | Nagelkerke's R2 | RMSE | AIC weights | BIC weights | Performance-Score
--------------------------------------------------------------------------------------------
mod_cond1 | clogit | 0.069 | 0.230 | 0.548 | 0.956 | 95.02%
mod_cond | clogit | 0.073 | 0.230 | 0.452 | 0.044 | 71.65%
mod_cond2 | clogit | 0.055 | 0.276 | 1.04e-115 | 1.81e-115 | 3.55%
mod_cond3 | clogit | 0.052 | 0.276 | 3.12e-116 | 5.45e-116 | 0.40%
El modelo sin fuma tiene mejor performance que el modelo saturado:
# Salida modelo condicional 1
summary(mod_cond1)Call:
coxph(formula = Surv(rep(1, 2346L), status) ~ bmi + hdl + fib4_cat +
strata(subclass), data = datos, method = "exact")
n= 2346, number of events= 251
coef exp(coef) se(coef) z Pr(>|z|)
bmi 0.004445 1.004455 0.012740 0.349 0.727
hdl -0.006922 0.993102 0.006941 -0.997 0.319
fib4_catalto riesgo 1.064462 2.899278 0.223595 4.761 1.93e-06 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
exp(coef) exp(-coef) lower .95 upper .95
bmi 1.0045 0.9956 0.9797 1.030
hdl 0.9931 1.0069 0.9797 1.007
fib4_catalto riesgo 2.8993 0.3449 1.8705 4.494
Concordance= 0.641 (se = 0.038 )
Likelihood ratio test= 26.33 on 3 df, p=8e-06
Wald test = 24.56 on 3 df, p=2e-05
Score (logrank) test = 26.63 on 3 df, p=7e-06
# (-) BMI
mod_cond1a <- clogit(status ~ hdl + fib4_cat +
strata(subclass),
data = datos)
# (-) HDL
mod_cond1b <- clogit(status ~ bmi + fib4_cat +
strata(subclass),
data = datos)
# Compara modelos y ordena según performance
compare_performance(mod_cond1, mod_cond1a, mod_cond1b,
metrics = "common", rank = T)# Comparison of Model Performance Indices
Name | Model | Nagelkerke's R2 | RMSE | AIC weights | BIC weights | Performance-Score
---------------------------------------------------------------------------------------------
mod_cond1a | clogit | 0.069 | 0.230 | 0.492 | 0.601 | 72.00%
mod_cond1 | clogit | 0.069 | 0.230 | 0.192 | 0.013 | 50.00%
mod_cond1b | clogit | 0.066 | 0.230 | 0.316 | 0.386 | 32.90%
# Salida modelo condicional 1b
summary(mod_cond1b)Call:
coxph(formula = Surv(rep(1, 2346L), status) ~ bmi + fib4_cat +
strata(subclass), data = datos, method = "exact")
n= 2346, number of events= 251
coef exp(coef) se(coef) z Pr(>|z|)
bmi 0.007979 1.008011 0.012127 0.658 0.511
fib4_catalto riesgo 1.067510 2.908129 0.223684 4.772 1.82e-06 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
exp(coef) exp(-coef) lower .95 upper .95
bmi 1.008 0.9921 0.9843 1.032
fib4_catalto riesgo 2.908 0.3439 1.8759 4.508
Concordance= 0.601 (se = 0.04 )
Likelihood ratio test= 25.32 on 2 df, p=3e-06
Wald test = 23.74 on 2 df, p=7e-06
Score (logrank) test = 25.68 on 2 df, p=3e-06
# Comparison of Model Performance Indices
Name | Model | AIC (weights) | BIC (weights) | Nagelkerke's R2 | RMSE
-----------------------------------------------------------------------------
mod_cond1b | clogit | 391.8 (0.313) | 403.3 (0.025) | 0.066 | 0.230
mod_cond1c | clogit | 390.2 (0.687) | 396.0 (0.975) | 0.065 | 0.230
Nos quedamos con el modelo que tiene solo fib4_cat como variable explicativa:
tbl_regression(mod_cond1c, exponentiate = T)| Characteristic | OR | 95% CI | p-value |
|---|---|---|---|
| fib4_cat | |||
| bajo riesgo | — | — | |
| alto riesgo | 2.94 | 1.90, 4.55 | <0.001 |
| Abbreviations: CI = Confidence Interval, OR = Odds Ratio | |||
En el modelo final, las personas con alto riesgo de fibrosis hepática presentan 3,7 veces más probabilidades de morir (\(95% IC: 2,45-5,57\)) que aquellas sin la condición.