Comienzo abordando en este taller, el concepto de la sepsis bacteriana, que se considera una urgencia tiempo-dependiente,por lo cual reconocerla temprano y administrar el antibiótico apropiado mejora la supervivencia de los pacientes, mientras que tratarla de más alimenta la resistencia antimicrobiana (Ljungström et al., 2017).

El problema clínico es que sus signos se solapan con condiciones no infecciosas, de modo que ningún biomarcador aislado basta para confirmarla o descartarla; entre esos signos engañosos figuran:

  • Fiebre, taquicardia y leucocitosis, que aparecen tanto en infecciones bacterianas como en cuadros inflamatorios no infecciosos, por lo que conviene apoyarse en marcadores de laboratorio.

En este taller evaluaré, con datos reales, qué tan bien dos biomarcadores de uso corriente, la procalcitonina y la proteína C reactiva, discriminan la sepsis bacteriana.

Trabajaré sobre la cohorte de Ljungström et al. (2017), que reúne 1.572 episodios de adultos atendidos en urgencias por sospecha de sepsis comunitaria, con una particularidad metodológica que fortalece la validez del dato, y es que todas las muestras se tomaron antes del antibiótico.

Mi variable desenlace es la sepsis bacteriana definida por criterios Sepsis-2, que construiré a partir de la clasificación clínica del estudio; además, conviene precisar que la exactitud de un biomarcador no es una propiedad fija, sino que depende de cómo se defina el evento.

Una aclaración sobre Sepsis-2 y Sepsis-3, que son dos definiciones de consenso para diagnosticar la sepsis y no resultan equivalentes:

  • Sepsis-2, la definición clásica, entiende la sepsis como una infección acompañada del síndrome de respuesta inflamatoria sistémica (SIRS), es decir, al menos dos signos como fiebre, taquicardia, taquipnea o alteración de los leucocitos; su forma grave añade disfunción orgánica, hipoperfusión o hipotensión.

  • Sepsis-3, el consenso de 2016, redefine la sepsis como una disfunción orgánica que amenaza la vida causada por una respuesta desregulada a la infección, y la operacionaliza mediante un aumento del puntaje SOFA igual o mayor a 2, dejando atrás el SIRS por considerarse poco específico (Ljungström et al., 2017).

Esta importa para mi análisis, porque al cambiar la definición cambia quién cuenta como enfermo; de hecho, en esta cohorte 667 episodios cumplen Sepsis-2 mientras que 560 cumplen Sepsis-3, y por eso un mismo biomarcador puede rendir distinto bajo una u otra definición (Ljungström et al., 2017).

Organizaré el análisis alrededor de los tres objetivos clásicos de la curva ROC (Roy-García et al., 2023):

  • Primero, identificaré el punto de corte óptimo que equilibra sensibilidad y especificidad mediante el índice de Youden.
  • Luego, estimaré el área bajo la curva como medida de discriminación independiente de la prevalencia.
  • Por último, compararé de manera formal ambos biomarcadores con la prueba de DeLong, apropiada cuando las dos curvas se miden en los mismos pacientes (DeLong et al., 1988).

Finalmente, a lo largo del trabajo tendré presente que el área bajo la curva cuantifica discriminación y no utilidad clínica, y que refleja el grado de separación entre las distribuciones de riesgo de enfermos y no enfermos (Janssens y Martens, 2020).

knitr::opts_chunk$set(
  echo      = TRUE,
  warning   = FALSE,
  message   = FALSE,
  fig.align = "center",
  fig.width = 8,
  fig.height = 5
)
options(digits = 7)
ggplot2::theme_set(ggplot2::theme_minimal(base_size = 12))

col_mono   <- "#9DBAD4"
pal_sepsis <- c("No" = "#A8D5BA", "S\u00ed" = "#E29CB0")

vars_modelos <- c("Sepsis2", "Age", "Gender", "SBP",
                  "Procalcitonin", "P_lactate", "CRP",
                  "FR", "SatO2", "HR", "Bodytemperature",
                  "Haemoglobin", "Leukocyteconcentration", "NL_ratio")

etiquetas_vars <- c(
  Age = "Edad", GenderMale = "Sexo masculino", SBP = "Tensi\u00f3n sist\u00f3lica",
  Procalcitonin = "Procalcitonina", P_lactate = "Lactato", CRP = "Prote\u00edna C reactiva",
  FR = "Frecuencia respiratoria", SatO2 = "Saturaci\u00f3n de ox\u00edgeno",
  HR = "Frecuencia card\u00edaca", Bodytemperature = "Temperatura corporal",
  Haemoglobin = "Hemoglobina", Leukocyteconcentration = "Leucocitos",
  NL_ratio = "\u00cdndice neutr\u00f3filos-linfocitos"
)
if (!requireNamespace("pacman", quietly = TRUE)) install.packages("pacman")

pacman::p_load(
  haven, dplyr, tidyr, tibble, forcats, labelled,
  janitor, skimr, dlookr, DataExplorer, visdat,
  ggplot2, GGally, corrplot, patchwork, scales,
  gtsummary, knitr, kableExtra, flextable,
  epiR, pROC, epitools, broom
)
sepsis_cruda <- read_dta("Ejercicio sepsis stata.dta")  
saveRDS(sepsis_cruda, "sepsis_cruda.rds")                

sepsis <- sepsis_cruda
dim(sepsis)
## [1] 1572   23
glimpse(sepsis)
## Rows: 1,572
## Columns: 23
## $ Patientno                      <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, …
## $ Ageyears                       <dbl> 79, 78, 54, 52, 72, 57, 76, 74, 61, 70,…
## $ Genderfemalemale               <chr> "Female", "Male", "Male", "Male", "Male…
## $ SystolicbloodpressuremmHg      <dbl> 136, NA, 148, 138, 130, 121, 117, 184, …
## $ Respiratoryratebreathsmin      <dbl> 16, 30, 18, 16, 20, 16, 18, 24, 22, 32,…
## $ Oxygensaturation               <dbl> 99, NA, 97, 96, 93, 100, 95, 93, 100, 9…
## $ Heartratebeatsmin              <dbl> 77, 95, 107, 120, 98, 94, 77, 78, 79, 1…
## $ BodytemperatureoC              <dbl> 37.4, 38.5, 38.5, 37.0, 36.9, 36.4, 39.…
## $ HaemoglobingL                  <dbl> 98, 130, 139, 153, 145, 109, 107, 136, …
## $ Leukocyteparticleconcentration <dbl> 10.3, 22.0, 9.5, 10.8, 12.8, 7.3, 3.2, …
## $ CreactiveproteinmgL            <dbl> 165, 217, 100, 59, 73, 183, 19, 162, 12…
## $ ProcalcitoninngmL              <dbl> 5.58, 0.54, 0.05, 0.05, 0.68, 4.11, 0.0…
## $ Neutrophillymphocytecountrati  <dbl> 10.4, 24.3, 31.6, 3.5, 7.3, 9.8, 3.4, 6…
## $ PlactatemmolL                  <dbl> 1.52, 1.99, 1.28, 2.46, NA, 1.07, 1.45,…
## $ Intensivecareunityesno         <chr> "No", "No", "No", "No", "No", "No", "No…
## $ dayssurvivalyesno              <chr> "Yes", "Yes", "Yes", "Yes", "Yes", "Yes…
## $ Positvebloodcultureyesno       <chr> "No", "No", "No", "No", "No", "No", "No…
## $ Provenbacterialinfectionyes    <chr> "Yes", "Yes", "Yes", "No", "Yes", "Yes"…
## $ Systemicinflammatoryresponses  <chr> "No", "Yes", "Yes", "No", "Yes", "No", …
## $ SOFAscore2yesno                <chr> "No", "Yes", "No", "No", "Yes", "Yes", …
## $ BacterialsepsisusingSepsis2    <chr> "No", "Yes", "Yes", "No", "Yes", "No", …
## $ Severebacterialsepsisseptics   <chr> "No", "No", "No", "No", "No", "No", "No…
## $ BacterialsepsisusingSepsis3    <chr> "No", "Yes", "No", "No", "Yes", "Yes", …
sapply(sepsis, class)
##                      Patientno                       Ageyears 
##                      "numeric"                      "numeric" 
##               Genderfemalemale      SystolicbloodpressuremmHg 
##                    "character"                      "numeric" 
##      Respiratoryratebreathsmin               Oxygensaturation 
##                      "numeric"                      "numeric" 
##              Heartratebeatsmin              BodytemperatureoC 
##                      "numeric"                      "numeric" 
##                  HaemoglobingL Leukocyteparticleconcentration 
##                      "numeric"                      "numeric" 
##            CreactiveproteinmgL              ProcalcitoninngmL 
##                      "numeric"                      "numeric" 
##  Neutrophillymphocytecountrati                  PlactatemmolL 
##                      "numeric"                      "numeric" 
##         Intensivecareunityesno              dayssurvivalyesno 
##                    "character"                    "character" 
##       Positvebloodcultureyesno    Provenbacterialinfectionyes 
##                    "character"                    "character" 
##  Systemicinflammatoryresponses                SOFAscore2yesno 
##                    "character"                    "character" 
##    BacterialsepsisusingSepsis2   Severebacterialsepsisseptics 
##                    "character"                    "character" 
##    BacterialsepsisusingSepsis3 
##                    "character"
sepsis <- sepsis %>%
  rename(
    Patient                = Patientno,
    Age                    = Ageyears,
    Gender                 = Genderfemalemale,
    SBP                    = SystolicbloodpressuremmHg,
    FR                     = Respiratoryratebreathsmin,
    SatO2                  = Oxygensaturation,
    HR                     = Heartratebeatsmin,
    Bodytemperature        = BodytemperatureoC,
    Haemoglobin            = HaemoglobingL,
    Leukocyteconcentration = Leukocyteparticleconcentration,
    CRP                    = CreactiveproteinmgL,
    Procalcitonin          = ProcalcitoninngmL,
    NL_ratio               = Neutrophillymphocytecountrati,
    P_lactate              = PlactatemmolL,
    ICU                    = Intensivecareunityesno,
    surv_28d               = dayssurvivalyesno,
    Cultive                = Positvebloodcultureyesno,
    BacterialInfection     = Provenbacterialinfectionyes,
    SIRS                   = Systemicinflammatoryresponses,
    SOFA                   = SOFAscore2yesno,
    BacterialSepsis        = BacterialsepsisusingSepsis2,
    SevereSepsis           = Severebacterialsepsisseptics,
    Sepsis3                = BacterialsepsisusingSepsis3
  )

names(sepsis)
##  [1] "Patient"                "Age"                    "Gender"                
##  [4] "SBP"                    "FR"                     "SatO2"                 
##  [7] "HR"                     "Bodytemperature"        "Haemoglobin"           
## [10] "Leukocyteconcentration" "CRP"                    "Procalcitonin"         
## [13] "NL_ratio"               "P_lactate"              "ICU"                   
## [16] "surv_28d"               "Cultive"                "BacterialInfection"    
## [19] "SIRS"                   "SOFA"                   "BacterialSepsis"       
## [22] "SevereSepsis"           "Sepsis3"

1 Renombro mis variables

Análisis: Asigno nombres cortos, legibles y válidos en R a las 23 variables originales, que me han llegado desde Stata con etiquetas largas y poco manejables; con ello el código lo veo con mayor claridad y así reduzco el riesgo de error cuando escriba cada variable.

  • Decisión de método: Empleo la función rename() de dplyr en lugar de reasignar los nombres por posición, porque nombrar cada variable como nuevo = original me deja un código autodocumentado y robusto; aunque cambiara el orden de las columnas, el renombrado seguiría siendo correcto.

  • Correcciones de sintaxis: Ajusto los nombres que obligaban a usar acentos graves al invocarlos; así, el cociente neutrófilos-linfocitos pasa a NL_ratio, el lactato a P_lactate, la supervivencia a surv_28d, pues un nombre no puede empezar por número, y la sepsis por Sepsis-3 a Sepsis3.

  • Compatibilidad preservada: Conservo idénticos los nombres para el resto del taller utilizando, como Procalcitonin, CRP, SatO2, SIRS, Age, Gender, SBP y Cultive, de modo que el flujo posterior me funcione sin fricciones.

2 Tipado de las variables categóricas

Antes de explorar la base debo declarar como factor las variables categóricas, porque R trata de manera distinta un texto y un factor; las funciones de diagnóstico, las tablas de frecuencia y las gráficas solo reconocen como categorías a los factores, de modo que sin este paso los conteos y las figuras me saldrían mal.

Fijo además el orden de los niveles con “No” como categoría de referencia y “Yes” como evento, que es la convención de un desenlace diagnóstico y la base sobre la que después se interpretan la sensibilidad y la especificidad (Westreich, 2020).

sepsis <- sepsis %>%
  mutate(
    Gender = factor(Gender),
    across(
      c(ICU, surv_28d, Cultive, BacterialInfection, SOFA, Sepsis3),
      ~ factor(., levels = c("No", "Yes"))
    ),
    across(
      c(SIRS, BacterialSepsis, SevereSepsis),
      ~ factor(., levels = c("No", "Yes", "Cannot be determined"))
    )
  )

sapply(sepsis, class)
##                Patient                    Age                 Gender 
##              "numeric"              "numeric"               "factor" 
##                    SBP                     FR                  SatO2 
##              "numeric"              "numeric"              "numeric" 
##                     HR        Bodytemperature            Haemoglobin 
##              "numeric"              "numeric"              "numeric" 
## Leukocyteconcentration                    CRP          Procalcitonin 
##              "numeric"              "numeric"              "numeric" 
##               NL_ratio              P_lactate                    ICU 
##              "numeric"              "numeric"               "factor" 
##               surv_28d                Cultive     BacterialInfection 
##               "factor"               "factor"               "factor" 
##                   SIRS                   SOFA        BacterialSepsis 
##               "factor"               "factor"               "factor" 
##           SevereSepsis                Sepsis3 
##               "factor"               "factor"

Análisis: Al revisar la salida del sapply confirmo que las variables categóricas quedaron declaradas como factor, de modo que ya tienen fijada de forma explícita su categoría de referencia y su evento; esta decisión no es irrelevante, pues de ella dependen el sentido que tendrá la tabla 2×2 y, más adelante, la lectura de la sensibilidad, la especificidad y los valores predictivos (Westreich, 2020).

  • Binarias Yes/No: observo que UCI, supervivencia, hemocultivo, infección bacteriana, SOFA y sepsis Sepsis-3 aparecen ahora como factor de dos niveles; al haber dejado “No” como referencia, el “Yes” queda como el evento de interés, tal como exige el marco de las pruebas diagnósticas (Fletcher, 2021).

  • Categóricas con indeterminado: compruebo que SIRS, sepsis Sepsis-2 y sepsis grave conservan su tercer nivel “Cannot be determined”, lo que me permite no borrar todavía esa información; corresponde a los episodios en que faltó al menos un criterio y, en el caso de Sepsis-2, son los 35 indeterminados que reporta el estudio original (Ljungström et al., 2017).

  • Sexo: verifico que Gender quedó como factor sin orden impuesto, lo cual es coherente con su papel puramente descriptivo, ya que ninguno de sus niveles representa un evento clínico.

  • Continuas intactas: noto que la procalcitonina, la proteína C reactiva, el lactato y los signos vitales permanecen como numéricas, condición indispensable para construir más adelante las curvas ROC, que requieren una escala continua (Roy-García et al., 2023).

3 Construcción de la variable objetivo (Sepsis2)

Para evaluar la capacidad diagnóstica de los biomarcadores necesito un desenlace binario, pues la curva ROC se construye contrastando enfermos frente a no enfermos en cada punto de corte (Roy-García et al., 2023). Por eso construiré la variable Sepsis2 a partir de la sepsis bacteriana por criterios Sepsis-2, que en la base trae tres categorías.

Reclasificaré “Cannot be determined” como dato faltante, porque corresponde a episodios en los que no es posible afirmar ni descartar la sepsis, y conservar un tercer nivel me rompería la lógica binaria del análisis; además, verificaré con una tabla cruzada que la variable derivada reproduce con exactitud la original, sin desplazar ningún caso.

sepsis <- sepsis %>%
  mutate(
    Sepsis2 = case_when(
      BacterialSepsis == "Yes" ~ "S\u00ed",
      BacterialSepsis == "No"  ~ "No",
      TRUE ~ NA_character_
    ),
    Sepsis2 = factor(Sepsis2, levels = c("No", "S\u00ed"))
  )

table(sepsis$BacterialSepsis, sepsis$Sepsis2, useNA = "always")
##                       
##                         No  Sí <NA>
##   No                   870   0    0
##   Yes                    0 667    0
##   Cannot be determined   0   0   35
##   <NA>                   0   0    0
janitor::tabyl(sepsis$Sepsis2)

Análisis: Al revisar la tabla cruzada confirmo que la variable que creé reproduce con exactitud la original:

  • los 870 episodios “No” se mantienen como “No”, los 667 “Yes” como “Yes”, y los 35 “Cannot be determined” quedan convertidos en faltantes, sin que se desplace ningún caso; esta correspondencia me da seguridad de que la recodificación fue correcta.

Distribución del desenlace: observo que Sepsis2 queda con 667 episodios con sepsis y 870 sin sepsis, sobre 1.537 episodios clasificables; el evento de interés representa así algo menos de la mitad de la muestra válida, una prevalencia alta que se explica porque todos los pacientes ingresaron por sospecha clínica de sepsis y no desde la población general (Ljungström et al., 2017).

Sentido de los 35 faltantes: compruebo que esos 35 valores ausentes coinciden con los episodios indeterminados que reporta el estudio original, aquellos en los que faltaba al menos uno de los criterios del síndrome de respuesta inflamatoria sistémica y que, por esa razón, no podían clasificarse bajo Sepsis-2 (Ljungström et al., 2017).

Implicación para las métricas: entiendo que esta prevalencia alta pesará más adelante sobre los valores predictivos, pues tanto el valor predictivo positivo como el negativo dependen de la frecuencia de la enfermedad en la población, a de la sensibilidad y la especificidad, que no lo hacen (Fletcher, 2021).

Codificación como factor: verifico que Sepsis2 quedó como factor con “No” de referencia y “Yes” como evento, la convención que da sentido a la tabla 2×2 y que me servirá de base a la regresión logística de la segunda parte del taller (Westreich, 2020).

4 Clasificación de variables y escala de medición

Antes de explorar la base necesito clasificar cada variable según su naturaleza estadística, porque la escala de medición determina qué resumen y qué gráfico son apropiados para cada una; no se describe igual una variable continua que una categórica, ni se grafican con las mismas herramientas (Bruce et al., 2022). Por eso construiré un diccionario que reúna, para las 24 variables, su nombre original, el asignado, la etiqueta clínica, el tipo de dato, la escala de medición y el papel que cumple en el estudio.

Separaré los datos numéricos, así: — continuos, como los biomarcadores, y discretos, como conteos

y de los datos categóricos así:

  • nominales sin orden, ordinales con orden y binarios de dos categorías

esta clasificación será mi brújula que justifique cada decisión gráfica y estadística del análisis exploratorio en adelante (Bruce et al., 2022).

diccionario <- tibble::tibble(
  num = 1:24,
  orig = c(
    "Patientno", "Ageyears", "Genderfemalemale", "SystolicbloodpressuremmHg",
    "Respiratoryratebreathsmin", "Oxygensaturation", "Heartratebeatsmin",
    "BodytemperatureoC", "HaemoglobingL", "Leukocyteparticleconcentration",
    "CreactiveproteinmgL", "ProcalcitoninngmL", "Neutrophillymphocytecountrati",
    "PlactatemmolL", "Intensivecareunityesno", "dayssurvivalyesno",
    "Positvebloodcultureyesno", "Provenbacterialinfectionyes",
    "Systemicinflammatoryresponses", "SOFAscore2yesno", "BacterialsepsisusingSepsis2",
    "Severebacterialsepsisseptics", "BacterialsepsisusingSepsis3", "(derivada)"
  ),
  asig = c(
    "Patient", "Age", "Gender", "SBP", "FR", "SatO2", "HR", "Bodytemperature",
    "Haemoglobin", "Leukocyteconcentration", "CRP", "Procalcitonin", "NL_ratio",
    "P_lactate", "ICU", "surv_28d", "Cultive", "BacterialInfection", "SIRS",
    "SOFA", "BacterialSepsis", "SevereSepsis", "Sepsis3", "Sepsis2"
  ),
  etiq = c(
    "Identificador del paciente", "Edad", "Sexo", "Presi\u00f3n arterial sist\u00f3lica",
    "Frecuencia respiratoria", "Saturaci\u00f3n de ox\u00edgeno", "Frecuencia card\u00edaca",
    "Temperatura corporal", "Hemoglobina", "Concentraci\u00f3n de leucocitos",
    "Prote\u00edna C reactiva", "Procalcitonina", "Cociente neutr\u00f3filos-linfocitos",
    "Lactato plasm\u00e1tico", "Ingreso a UCI", "Supervivencia a 28 d\u00edas",
    "Hemocultivo positivo", "Infecci\u00f3n bacteriana confirmada",
    "S\u00edndrome de respuesta inflamatoria sist\u00e9mica", "Puntaje SOFA >= 2",
    "Sepsis bacteriana (criterios Sepsis-2)", "Sepsis grave / shock s\u00e9ptico (Sepsis-2)",
    "Sepsis bacteriana (criterios Sepsis-3)", "Variable objetivo: sepsis Sepsis-2 (s\u00ed/no)"
  ),
  tipo = c(
    "Identificador", "Num\u00e9rica discreta", "Categ\u00f3rica", "Num\u00e9rica continua",
    "Num\u00e9rica continua", "Num\u00e9rica continua", "Num\u00e9rica continua", "Num\u00e9rica continua",
    "Num\u00e9rica continua", "Num\u00e9rica continua", "Num\u00e9rica continua", "Num\u00e9rica continua",
    "Num\u00e9rica continua", "Num\u00e9rica continua", "Categ\u00f3rica", "Categ\u00f3rica",
    "Categ\u00f3rica", "Categ\u00f3rica", "Categ\u00f3rica", "Categ\u00f3rica", "Categ\u00f3rica",
    "Categ\u00f3rica", "Categ\u00f3rica", "Categ\u00f3rica"
  ),
  escala = c(
    "-", "De raz\u00f3n", "Nominal binaria", "De raz\u00f3n", "De raz\u00f3n", "De raz\u00f3n",
    "De raz\u00f3n", "De intervalo", "De raz\u00f3n", "De raz\u00f3n", "De raz\u00f3n", "De raz\u00f3n",
    "De raz\u00f3n", "De raz\u00f3n", "Nominal binaria", "Nominal binaria", "Nominal binaria",
    "Nominal binaria", "Nominal (3 niveles)", "Nominal binaria", "Nominal (3 niveles)",
    "Nominal (3 niveles)", "Nominal binaria", "Nominal binaria"
  ),
  rol = c(
    "Identificador", "Descriptiva", "Descriptiva", "Descriptiva / predictora",
    "Descriptiva", "Descriptiva / predictora", "Descriptiva", "Descriptiva",
    "Descriptiva", "Descriptiva", "Prueba diagn\u00f3stica", "Prueba diagn\u00f3stica",
    "Predictora", "Predictora", "Desenlace secundario", "Desenlace secundario",
    "Descriptiva", "Descriptiva", "Componente diagn\u00f3stico", "Componente diagn\u00f3stico",
    "Origen del objetivo", "Desenlace alterno", "Desenlace alterno", "Variable objetivo"
  )
)

diccionario %>%
  kableExtra::kbl(
    col.names = c("#", "Nombre original (Stata)", "Nombre asignado",
                  "Etiqueta cl\u00ednica", "Tipo de dato", "Escala de medici\u00f3n",
                  "Rol en el estudio"),
    caption = "Diccionario de variables y escala de medici\u00f3n de la cohorte de sepsis (Ljungstr\u00f6m et al., 2017)") %>%
  kableExtra::kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE, position = "center", font_size = 13
  ) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::row_spec(c(11, 12), background = "#A8D5BA") %>%
  kableExtra::row_spec(24, background = "#C9A9E0", bold = TRUE) %>%
  kableExtra::column_spec(1, bold = TRUE) %>%
  kableExtra::column_spec(3, bold = TRUE)
Diccionario de variables y escala de medición de la cohorte de sepsis (Ljungström et al., 2017)
# Nombre original (Stata) Nombre asignado Etiqueta clínica Tipo de dato Escala de medición Rol en el estudio
1 Patientno Patient Identificador del paciente Identificador
Identificador
2 Ageyears Age Edad Numérica discreta De razón Descriptiva
3 Genderfemalemale Gender Sexo Categórica Nominal binaria Descriptiva
4 SystolicbloodpressuremmHg SBP Presión arterial sistólica Numérica continua De razón Descriptiva / predictora
5 Respiratoryratebreathsmin FR Frecuencia respiratoria Numérica continua De razón Descriptiva
6 Oxygensaturation SatO2 Saturación de oxígeno Numérica continua De razón Descriptiva / predictora
7 Heartratebeatsmin HR Frecuencia cardíaca Numérica continua De razón Descriptiva
8 BodytemperatureoC Bodytemperature Temperatura corporal Numérica continua De intervalo Descriptiva
9 HaemoglobingL Haemoglobin Hemoglobina Numérica continua De razón Descriptiva
10 Leukocyteparticleconcentration Leukocyteconcentration Concentración de leucocitos Numérica continua De razón Descriptiva
11 CreactiveproteinmgL CRP Proteína C reactiva Numérica continua De razón Prueba diagnóstica
12 ProcalcitoninngmL Procalcitonin Procalcitonina Numérica continua De razón Prueba diagnóstica
13 Neutrophillymphocytecountrati NL_ratio Cociente neutrófilos-linfocitos Numérica continua De razón Predictora
14 PlactatemmolL P_lactate Lactato plasmático Numérica continua De razón Predictora
15 Intensivecareunityesno ICU Ingreso a UCI Categórica Nominal binaria Desenlace secundario
16 dayssurvivalyesno surv_28d Supervivencia a 28 días Categórica Nominal binaria Desenlace secundario
17 Positvebloodcultureyesno Cultive Hemocultivo positivo Categórica Nominal binaria Descriptiva
18 Provenbacterialinfectionyes BacterialInfection Infección bacteriana confirmada Categórica Nominal binaria Descriptiva
19 Systemicinflammatoryresponses SIRS Síndrome de respuesta inflamatoria sistémica Categórica Nominal (3 niveles) Componente diagnóstico
20 SOFAscore2yesno SOFA Puntaje SOFA >= 2 Categórica Nominal binaria Componente diagnóstico
21 BacterialsepsisusingSepsis2 BacterialSepsis Sepsis bacteriana (criterios Sepsis-2) Categórica Nominal (3 niveles) Origen del objetivo
22 Severebacterialsepsisseptics SevereSepsis Sepsis grave / shock séptico (Sepsis-2) Categórica Nominal (3 niveles) Desenlace alterno
23 BacterialsepsisusingSepsis3 Sepsis3 Sepsis bacteriana (criterios Sepsis-3) Categórica Nominal binaria Desenlace alterno
24 (derivada) Sepsis2 Variable objetivo: sepsis Sepsis-2 (sí/no) Categórica Nominal binaria Variable objetivo

Análisis: Al ordenar todas las variables en el diccionario distingo con claridad su naturaleza estadística, lo que me permite anticipar qué resumen y qué gráfico corresponden a cada una; esta clasificación previa es la que da rigor al análisis exploratorio, porque asi evito describir una variable con una herramienta que no le conviene (Bruce et al., 2022).

  • Numéricas continuas de razón: identifico doce variables de este tipo, entre ellas los biomarcadores procalcitonina, proteína C reactiva, lactato y cociente neutrófilos-linfocitos, junto con los signos vitales; al tener un cero absoluto y unidades de razón, admiten medianas, rangos intercuartílicos, histogramas y cajas de bigotes, y son la materia prima de las curvas ROC.

  • Una numérica de intervalo: observo que la temperatura corporal en grados Celsius es la excepción, pues su cero no es absoluto sino convencional; aun así, para efectos descriptivos lo manejo como las demás continuas.

  • Categóricas nominales: reconozco siete variables binarias Yes/No (UCI, supervivencia, hemocultivo, infección bacteriana, SOFA, Sepsis-3 y sexo) que se resumen con frecuencias y porcentajes, y no con promedios, por carecer de orden numérico (Bruce et al., 2022).

  • Categóricas con tercer nivel: distingo tres variables (SIRS, sepsis Sepsis-2 y sepsis grave) que incluyen “Cannot be determined”, un nivel que me obliga a decisiones explícitas de manejo antes de cualquier cálculo.

  • Pruebas diagnósticas y desenlace: resalto la proteína C reactiva y la procalcitonina como las pruebas a evaluar, en verde, y la variable Sepsis2 como mi desenlace objetivo, en lila; sobre estas tres recaerá todo mi análisis ROC posterior.

5 Análisis exploratorio, mi panorama de datos faltantes

El primer paso de una exploración de calidad (como lo hemos aprendido en clases) es cuantificar la información ausente, pues los datos faltantes condicionan qué análisis son válidos y pueden sesgar los resultados si se manejan a la ligera (Bruce et al., 2022). Por eso, antes de describir o graficar las variables, mediré cuántos faltantes tiene cada una y en qué proporción, y examinaré si esos vacíos se concentran en las mismas observaciones o se dispersan entre variables distintas.

Para ello usaré tres herramientas complementarias del análisis de calidad de datos:

  • una tabla ordenada de faltantes por variable

  • un gráfico de Pareto que jerarquiza las variables según su proporción de vacíos, y

  • un mapa de faltantes que revela el patrón de combinaciones

con esa evidencia podré decidir, de forma argumentada, cómo manejar la información ausente sin amputar la muestra innecesariamente (Westreich, 2020).

faltantes <- sepsis %>%
  summarise(across(everything(), ~ sum(is.na(.)))) %>%
  pivot_longer(everything(), names_to = "Variable", values_to = "N_faltantes") %>%
  mutate(Porcentaje = N_faltantes / nrow(sepsis) * 100) %>%
  arrange(desc(N_faltantes))

faltantes %>%
  filter(N_faltantes > 0) %>%
  kableExtra::kbl(
    caption = "Datos faltantes por variable (solo variables con al menos un faltante)",
    digits = 2
  ) %>%
  kableExtra::kable_styling(
    bootstrap_options = c("striped", "hover", "condensed"),
    full_width = FALSE, position = "center"
  ) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE)
Datos faltantes por variable (solo variables con al menos un faltante)
Variable N_faltantes Porcentaje
FR 184 11.70
Bodytemperature 145 9.22
SatO2 92 5.85
HR 90 5.73
SBP 86 5.47
P_lactate 56 3.56
Sepsis2 35 2.23
NL_ratio 26 1.65
Haemoglobin 25 1.59
CRP 23 1.46
Leukocyteconcentration 21 1.34

Análisis: Esta tabla ordena las variables de mayor a menor cantidad de datos ausentes, y leo en ella tres columnas:

  • el nombre de la variable, el número de faltantes y su porcentaje sobre los 1.572 episodios; la disposición descendente me permite ver de un vistazo dónde se concentra el problema y dónde es despreciable.

  • Las cinco primeras filas son signos vitales: observo que la frecuencia respiratoria encabeza con 184 faltantes (11,70%), seguida de la temperatura corporal con 145 (9,22%), la saturación de oxígeno con 92 (5,85%), la frecuencia cardiaca con 90 (5,73%) y la presión sistólica con 86 (5,47%); clínicamente tiene sentido, porque en la urgencia estos parámetros no siempre se registran de forma completa en cada paciente.

  • Las filas intermedias y finales son de laboratorio: identifico al lactato con 56 faltantes (3,56%), al cociente neutrófilos-linfocitos con 26 (1,65%), a la hemoglobina con 25 (1,59%) y a los leucocitos con 21 (1,34%); lo que supongo que al provenir de muestras de sangre procesadas en laboratorio, su registro es más sistemático y por eso faltan menos.

  • Mis dos biomarcadores están casi completos: compruebo que la proteína C reactiva solo pierde 23 registros (1,46%) y que la procalcitonina no aparece en la tabla, es decir, no tiene ningún faltante; esto es decisivo, porque son las dos pruebas que evaluaré con curvas ROC y conservan prácticamente toda su información (Ljungström et al., 2017).

  • El desenlace y sus 35 vacíos: noto que Sepsis2 figura con 35 faltantes (2,23%), que no son un error sino los episodios indeterminados que yo misma reclasifiqué desde “Cannot be determined”, coincidentes con los que reporta el estudio original (Ljungström et al., 2017).

dlookr::plot_na_pareto(sepsis, only_na = TRUE, col = col_mono)

Análisis: Esta figura es un diagrama de Pareto, que elegí porque combina en un solo gráfico dos lecturas, consistente en unas barras que miden la frecuencia de faltantes de cada variable, ordenadas de mayor a menor, y una línea ascendente que acumula ese porcentaje hasta llegar al total; su utilidad es jerarquizar el problema y mostrar qué pocas variables concentran la mayor parte de los vacíos.

  • Las barras y su altura: observo que la altura de cada barra representa cuántos faltantes tiene la variable, por eso la frecuencia respiratoria y la temperatura, a la izquierda, son las más altas, y las barras decrecen hacia la derecha hasta los leucocitos; el orden descendente es la esencia del Pareto.

  • El color de las barras: interpreto que el color me codifica la gravedad de la ausencia según la escala de la función, donde los tonos naranja señalan las variables con más vacíos y los amarillos las de ausencia leve; así, el contraste cromático refuerza visualmente cuáles variables exigen más mi atención.

  • La línea acumulada: leo la curva azul como el porcentaje acumulado de faltantes; al avanzar de izquierda a derecha sube con rapidez al inicio, por el peso de los signos vitales, y luego se aplana, lo que indica que las primeras variables ya explican casi toda la información ausente y el resto aporta muy poco.

este gráfico me permite entonces confirmar visualmente lo que vi en la tabla, que el problema de faltantes es acotado y se concentra en pocos signos vitales, mientras mis biomarcadores quedan entre las barras más bajas; esto sostiene la decisión de no descartar la muestra completa (Bruce et al., 2022).

visdat::vis_miss(sepsis, sort_miss = TRUE)

Análisis: Esta figura es un mapa de datos faltantes, que generé para responder una pregunta que la tabla y el Pareto no pueden contestarme, sobre si los vacíos coinciden o no en los mismos pacientes; cada fila es un episodio, cada columna una variable, y el color marca la presencia o ausencia del dato.

Al leer este mapa, observo que las franjas oscuras señalan los valores ausentes y las claras los presentes; al recorrer las columnas, veo marcas dispersas en distintas alturas y no bloques continuos, lo que ya anticipa el patrón de los faltantes.

El patrón que observo es disperso, pues al no concentrarse las franjas oscuras en las mismas filas, los faltantes de una variable no coinciden sistemáticamente con los de otra; es decir, no existe un grupo de pacientes sin datos, sino ausencias parciales repartidas a lo largo de la cohorte.

Esa dispersión considero que es importante, ya que favorece un manejo por variable, pues eliminar un episodio entero por un único faltante descartaría datos válidos en todas sus demás variables; conviene entonces descartar solo donde cada análisis lo exija (Westreich, 2020).

Concluyo que la evidencia de las tres salidas apunta en la misma dirección, esto es, faltantes pocos, acotados a signos vitales y dispersos entre observaciones, lo que justifica el manejo cuidadoso que sustentaré en el siguiente paso, fiel al principio de preservar antes que amputar.

6 Mi decisión de manejo de faltantes y base de trabajo

Una vez que he conocido la magnitud y el patrón de los faltantes, debo decidir cómo manejarlos, y esta decisión no es menor, porque eliminar observaciones de forma indiscriminada puede introducir sesgo y restar potencia al análisis (Westreich, 2020).

Por eso optaré por un manejo cuidadoso, donde conservo la base íntegra y descarto faltantes únicamente en las variables que cada análisis concreto requiera, en lugar de aplicar una eliminación global que sacrifique episodios completos.

Para dimensionar lo que esa eliminación global costaría, calcularé cuántos episodios sobrevivirían a un análisis de casos completos sobre todas las variables; luego guardaré la base ya renombrada, tipada y con el desenlace construido como base de trabajo, conservando intacta mi versión cruda que resguardé al inicio.

n_total     <- nrow(sepsis)
n_completos <- sum(complete.cases(sepsis))
n_perdidos  <- n_total - n_completos
pct_perdido <- n_perdidos / n_total * 100

tabla_costo <- data.frame(
  Criterio = c("Episodios totales",
               "Episodios sin ningun faltante",
               "Episodios que se perderian",
               "Porcentaje que se perderia"),
  Valor = c(n_total, n_completos, n_perdidos, round(pct_perdido, 2))
)

tabla_costo %>%
  kableExtra::kbl(caption = "Costo de una eliminacion global de faltantes (casos completos)") %>%
  kableExtra::kable_styling(
    bootstrap_options = c("striped", "condensed"),
    full_width = FALSE,
    position = "center"
  ) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE)
Costo de una eliminacion global de faltantes (casos completos)
Criterio Valor
Episodios totales 1572.00
Episodios sin ningun faltante 1194.00
Episodios que se perderian 378.00
Porcentaje que se perderia 24.05
saveRDS(sepsis, "sepsis_trabajo.rds")

Análisis: Esta tabla cuantifica, cifra por cifra, lo que me costaría manejar los faltantes de la forma más drástica, es decir, conservando solo los episodios que tienen completas las 23 variables a la vez, y la lectura de sus cuatro filas me confirma que esa vía sería demasiado costosa.

La primera fila me recuerda el punto de partida, los 1.572 episodios totales de la cohorte; la segunda muestra que apenas 1.194 episodios están completos en todas y cada una de sus variables; la tercera revela la consecuencia, pues se perderían 378 episodios, que es la entre ambos; y la cuarta traduce esa pérdida a una proporción, el 24,05% de la muestra, es decir, prácticamente uno de cada cuatro pacientes.

Interpreto que sacrificar el 24,05% de la cohorte sería un precio injustificado, sobre todo porque esa pérdida no proviene de mis variables de interés sino, en su mayoría, de signos vitales ausentes que nada tienen que ver con los biomarcadores; descartar un episodio entero por una frecuencia respiratoria sin registrar eliminaría de paso una procalcitonina o una proteína C reactiva perfectamente medidas.

Por eso considero adoptar un “manejo quirúrgico y no global”, que elimina faltantes solo dentro de cada análisis concreto y así preservo el máximo de datos en cada cálculo, fiel al principio de preservar antes que amputar (Westreich, 2020).

Esta decisión es además coherente con el patrón disperso que constaté en el mapa de faltantes, donde las ausencias se repartían a lo largo de la cohorte en lugar de concentrarse en un grupo de pacientes; con ese panorama, lo razonable que haré es conservar la base íntegra.

Por ello guardo la base ya renombrada, tipada y con el desenlace Sepsis2 construido bajo el nombre sepsis_trabajo, conservando intacta la versión cruda que resguardé al comienzo, de modo que dispongo de un punto de retorno seguro y de una base depurada sobre la cual puedo continuar con tranquilidad y trazabilidad.

7 Comprobación de normalidad de las variables continuas

Antes de describir las variables continuas debo conocer su forma de distribución, porque de ella depende qué medidas de resumen voy a emplear; por ejemplo, una distribución simétrica admite media y desviación estándar, mientras que una asimétrica exige mediana y rango intercuartílico, más resistentes a los valores extremos (Bruce et al., 2022).

Por eso aplicaré la prueba de Shapiro-Wilk a cada variable continua y la acompañaré de gráficos de normalidad, para no decidir solo con un valor de probabilidad sino también con la evidencia visual.

La prueba de Shapiro-Wilk contrasta la hipótesis nula de que los datos provienen de una distribución normal; cuando su valor de probabilidad es menor que 0,05, rechazo esa hipótesis y concluyo que la variable no es normal (Bruce et al., 2022).

Es posible que los biomarcadores tenderán a ser asimétricos con cola hacia la derecha, pues unos pocos pacientes muy graves alcanzan valores extremos, lo que obligaría a resumirlos con la mediana, tal como procedieron en el estudio original (Ljungström et al., 2017).

sepsis %>%
  dplyr::select(where(is.numeric), -Patient) %>%
  dlookr::normality() %>%
  dplyr::arrange(p_value) %>%
  kableExtra::kbl(
    caption = "Prueba de normalidad de Shapiro-Wilk para las variables continuas",
    digits = 5
  ) %>%
  kableExtra::kable_styling(
    bootstrap_options = c("striped", "hover", "condensed"),
    full_width = FALSE, position = "center"
  ) %>%
  kableExtra::row_spec(0, background = "#9DBAD4", color = "#1c2b3a", bold = TRUE)
Prueba de normalidad de Shapiro-Wilk para las variables continuas
vars statistic p_value sample
Procalcitonin 0.25941 0.00000 1572
Leukocyteconcentration 0.49109 0.00000 1572
P_lactate 0.55733 0.00000 1572
NL_ratio 0.75938 0.00000 1572
SatO2 0.75066 0.00000 1572
CRP 0.91390 0.00000 1572
Age 0.92720 0.00000 1572
FR 0.93688 0.00000 1572
HR 0.96972 0.00000 1572
Haemoglobin 0.98133 0.00000 1572
Bodytemperature 0.99100 0.00000 1572
SBP 0.99600 0.00061 1572

Análisis: Esta tabla presenta, para cada variable continua, tres datos que leo en conjunto, donde el estadístico de Shapiro-Wilk, vale 1 cuando la distribución es perfectamente normal y disminuye a medida que se aleja de esa forma; luego el valor de probabilidad, que al ser menor que 0,05 me obliga a rechazar la normalidad; y el tamaño de muestra, que es de 1.572 episodios en todas, lo que me confirma que la prueba se aplicó sobre la cohorte completa.

La tabla se observa ordenada de menor a mayor estadístico, de modo que arriba quedan las variables más alejadas de la normalidad y abajo las que más se le aproximan. Al recorrer la columna del valor de probabilidad observo un resultado contundente, pues once de las doce variables tienen un valor de 0,00000 y la última, la presión arterial sistólica, alcanza apenas 0,00061, además, todas, sin excepción, quedan por debajo de 0,05, de manera que ninguna variable continua sigue una distribución normal.

Sin embargo, el verdadero matiz no está en la probabilidad, que es uniformemente diminuta por el gran tamaño muestral, sino en el estadístico, que sí gradúa la intensidad de la desviación. Observo en el extremo superior de la tabla, la procalcitonina que exhibe el estadístico más bajo de todos, 0,25941, seguida de los leucocitos con 0,49109 y del lactato con 0,55733; interpreto que estas son las variables más radicalmente asimétricas, y no me sorprende que la procalcitonina encabece la lista, porque es el biomarcador que en la cohorte va de 0,01 hasta valores extremadamente altos, concentrando a la mayoría de pacientes en cifras muy bajas.

El cociente neutrófilos-linfocitos (NL_ratio), con 0,75938, también se aparta con claridad, completando el cuadro de biomarcadores que considero fuertemente sesgados a la derecha.

Mi segundo biomarcador de interés, la proteína C reactiva (CRP), presenta un estadístico bastante más alto, 0,91390, lo que indica que, aun siendo no normal, su distribución es menos asimétrica que la de la procalcitonina; esta tiene sentido clínico, pues la proteína C reactiva se distribuye de forma más extendida entre los pacientes y por eso, en el artículo, los investigadores encontraron que discriminaba peor que otros marcadores (Ljungström et al., 2017).

En el extremo inferior, la presión sistólica, la temperatura y la hemoglobina, con estadísticos cercanos a 1, son las que más se aproximan a la normalidad sin alcanzarla, lo cual considero que concuerda con su naturaleza de variables fisiológicas más estables.

Considero que al no cumplirse la normalidad en ninguna variable debo resumirlas con la mediana y el rango intercuartílico, y compararlas entre grupos con pruebas no paramétricas; esta es exactamente la decisión que tomó el estudio original, donde reportaron medianas y rangos intercuartílicos tras haber verificado la no-normalidad de todas las variables continuas con la prueba de Kolmogorov-Smirnov (Ljungström et al., 2017).

sepsis %>%
  dplyr::select(CRP, Procalcitonin, P_lactate, NL_ratio) %>%
  dlookr::plot_normality(col = col_mono)

Análisis: Esta figura, llamada Normality Diagnosis Plot para el cociente neutrófilos-linfocitos (NL_ratio), reúne cuatro paneles que examinan la misma variable de cuatro maneras complementarias; la generé porque un solo gráfico no basta para entender una distribución, mientras que el conjunto me deja ver la forma original, contrastarla contra la normalidad teórica y comprobar si alguna transformación la corrige.

  • En el panel superior izquierdo, rotulado origin, observo el histograma de la variable tal como viene, donde el eje horizontal recorre los valores del cociente, desde 0 hasta más de 100, y el eje vertical cuenta cuántos pacientes caen en cada intervalo; veo que la barra más alta supera los 700 episodios y se ubica en los valores bajos, cercanos a 10, mientras que las barras se desploman a medida que avanzo hacia la derecha, dejando una cola larga y delgada que se estira hasta más allá de 100.

Esto significa, en términos clínicos, que la inmensa mayoría de los pacientes en este dataset, tienen un cociente bajo y solo un puñado, presumiblemente los más graves, alcanza valores extremos, pues este cociente se eleva con la intensidad de la respuesta inflamatoria (Ljungström et al., 2017).

  • En el panel superior derecho, el gráfico Q-Q, me compara los datos reales contra lo que se esperaría si fueran perfectamente normales; la línea recta negra representa esa normalidad ideal, y cada punto azul es un paciente ordenado según su valor.

Interpreto que mientras los puntos siguen la recta, los datos se comportarían como normales, pero aquí se despegan de ella y se curvan hacia arriba en el extremo derecho, formando una especie de palanca ascendente; esa desviación sistemática es la firma gráfica de una distribución sesgada a la derecha, y me confirma visualmente por qué la prueba de Shapiro-Wilk rechazó la normalidad.

  • En los dos paneles inferiores examino si una transformación corrige la asimetría, y el contraste es interesante, pues el panel log transformation muestra que, al aplicar el logaritmo, el histograma se vuelve mucho más simétrico y acampanado, con su centro hacia valores intermedios y colas equilibradas a ambos lados.

  • el panel sqrt transformation, con la raíz cuadrada, también mejora la simetría, aunque conserva algo de cola a la derecha. Considero que el logaritmo es el que mejor normaliza esta variable, lo que tiene una implicación práctica que usaré más adelante, pues graficar el cociente en escala logarítmica permitirá apreciar diferencias entre grupos que en la escala original quedan aplastadas por los valores extremos.

Considero que es importante recordar, que esa transformación reordena la escala pero no altera el área bajo la curva ROC, por conservar el orden de los pacientes (Janssens y Martens, 2020).

En conjunto, leo en estos cuatro paneles una misma conclusión vista desde cuatro ventanas, esto es, que el cociente neutrófilos-linfocitos no es normal sino marcadamente asimétrico hacia la derecha, y que su forma natural se acerca a una distribución logarítmica; por eso lo describiré con mediana y rango intercuartílico, y no con promedios, que un puñado de valores extremos distorsionaría (Bruce et al., 2022).

8 Análisis univariado de las variables continuas

Tras confirmar la no-normalidad, describo ahora cada variable continua por separado para conocer su tendencia central, su dispersión y la presencia de valores extremos, que es el propósito del análisis univariado (Bruce et al., 2022). Para cada biomarcador combinaré dos gráficos que se complementan:

  • un histograma, que muestra la forma de la distribución y dónde se acumulan los pacientes, con líneas que marcan la media y la mediana; y

  • una caja de bigotes, que resume los cuartiles y me deja ver con claridad los valores atípicos.

Mostrar juntas la media y la mediana tiene una intención analítica, pues en una distribución asimétrica la media se desplaza hacia la cola mientras la mediana permanece en el centro de los datos, de modo que la distancia entre ambas líneas es en sí misma una medida visual de la asimetría (Bruce et al., 2022).

Concentraré mi atención en los biomarcadores, que son los protagonistas del análisis ROC.

univariado <- function(df, var, etiqueta) {
  v <- df[[var]]
  med  <- mean(v, na.rm = TRUE)
  mediana <- median(v, na.rm = TRUE)

  hist <- ggplot(df, aes(x = .data[[var]])) +
    geom_histogram(bins = 30, fill = col_mono, color = "white") +
    geom_vline(xintercept = med, color = "#C0392B", linewidth = 0.8) +
    geom_vline(xintercept = mediana, color = "#1F618D", linewidth = 0.8, linetype = "dashed") +
    labs(title = etiqueta, subtitle = "Linea roja: media | Linea azul punteada: mediana",
         x = etiqueta, y = "Frecuencia")

  caja <- ggplot(df, aes(y = .data[[var]])) +
    geom_boxplot(fill = col_mono, width = 0.4, outlier.color = "#C0392B") +
    labs(x = NULL, y = etiqueta) +
    theme(axis.text.x = element_blank(), axis.ticks.x = element_blank())

  hist + caja + patchwork::plot_layout(widths = c(2.5, 1))
}

univariado(sepsis, "Procalcitonin", "Procalcitonina (ng/mL)")

Análisis: Esta figura me describe la procalcitonina con dos dibujos puestos lado a lado que prefiero leer juntos, porque cada uno me muestra algo que el otro esconde; a la izquierda está el histograma, que es como un conteo de pacientes apilados según su valor, y a la derecha la caja de bigotes, que resume ese mismo conteo en forma de cuartiles.

  • En el histograma, el eje horizontal de abajo representa la concentración de procalcitonina en nanogramos por mililitro, y avanza de izquierda a derecha desde 0 hasta cerca de 200; el eje vertical de la izquierda cuenta la frecuencia, es decir, cuántos pacientes tienen un valor dentro de cada franja, y su escala sube hasta más de 1.300, por lo cual, una barra alta significa que muchos pacientes comparten ese valor, y una barra baja, que casi ninguno lo tiene.

Lo que veo es un comportamiento extremo, pues casi toda la población se amontona en la primera barra pegada al cero, que reúne a más de 1.300 de los 1.572 pacientes, mientras que hacia la derecha las barras se desploman hasta volverse casi invisibles y dejan una cola larga y rasante que se arrastra hasta cerca de 200.

Esto quiere decir, en lenguaje clínico, que la enorme mayoría de los pacientes tiene la procalcitonina muy baja y solo unos poquísimos llegan a valores altos; es exactamente lo que la prueba de Shapiro-Wilk me había anticipado al darle a esta variable el estadístico más bajo de toda la base, 0,25941, el de mayor asimetría.

El detalle que más me habla son las dos líneas verticales del histograma, donde la línea azul punteada marca la mediana, que es el valor que parte a los pacientes en dos mitades iguales, la mitad por debajo y la mitad por encima; la veo pegada al cero, justo donde está el grueso de la gente.

La línea roja marca la media, que es el promedio aritmético, y la veo desplazada a la derecha, separada de la mediana. Esa distancia entre ambas es la mejor prueba de la asimetría, porque los pocos pacientes con valores altísimos jalan el promedio hacia arriba y lo vuelven engañoso, mientras la mediana se queda fiel a donde de verdad está la mayoría; por eso, en datos así de sesgados, la mediana describe mejor que la media (Bruce et al., 2022); ahora bien, si la distribución fuera simétrica, las dos líneas se montarían una sobre otra; aquí, su separación es la asimetría hecha imagen.

  • A la derecha está la caja de bigotes, que muestra los mismos datos pero ordenados por cuartiles; por tanto, si ordeno a todos los pacientes de menor a mayor procalcitonina y los parto en cuatro grupos iguales, los cortes que los separan son los cuartiles, así:

el primero deja por debajo al 25% más bajo, el segundo es la mediana que parte por la mitad, y el tercero deja por debajo al 75%. La caja del dibujo va del primer al tercer cuartil, así que contiene al 50% central de los pacientes, y la línea de adentro es la mediana.

Lo que veo es llamativo, porque la caja está tan comprimida contra el cero que parece una simple línea negra horizontal; eso me dice que el 50% central de los pacientes cabe en un rango estrechísimo de valores bajos, es decir, que la mayoría se parece mucho entre sí y tiene la procalcitonina baja.

Por encima de esa caja se levanta una columna densa de puntos rojos que sube desde unos 50 hasta cerca de 200, con casos que distingo alrededor de 75, 90, 100, 125, 145 y un máximo cercano a 200.

Esos puntos son los valores atípicos, que el gráfico dibuja aparte porque quedan muy lejos del grueso; pero atípico no significa erróneo, pues considero que son pacientes clínicamente reales con procalcitonina muy elevada, probablemente corresponde a los de sepsis bacteriana más grave, y por eso los conservo en lugar de borrarlos.

Esta lectura coincide con lo que reportó el estudio original, donde la procalcitonina alcanzaba su mediana más alta justamente en el grupo de sepsis grave o shock séptico, muy por encima del resto de los pacientes, lo que al parecer se confirma que el biomarcador se dispara cuando la infección es severa (Ljungström et al., 2017).

En conjunto, considero que esta doble imagen me consolida la decisión que vengo sosteniendo, esto es, describir la procalcitonina con mediana y rango intercuartílico en vez de media, y graficarla en escala logarítmica cuando la compare entre grupos; lo hago porque su escala original aplasta a casi todos los pacientes contra el eje y solo deja ver los extremos, de modo que el logaritmo me permitirá “estirar” la zona baja y apreciar diferencias que de otro modo quedarían ocultas.

Por consiguiente, conservaré esos valores altos por ser información clínica legítima, pues justamente en ellos reside la capacidad de la procalcitonina para señalar la sepsis grave, tal como lo mostró el artículo al ser el marcador con mayor concentración en los pacientes más críticos (Ljungström et al., 2017).

8.1 Proteína C reactiva, análisis con histograma y caja de bigotes

Tras describir la procalcitonina, aplicaré la misma exploración univariada a la proteína C reactiva, mi segundo biomarcador, porque para comparar de forma justa dos pruebas diagnósticas necesito conocer primero cómo se comporta cada una por separado (Bruce et al., 2022). Para ello generaré de nuevo el histograma con las líneas de media y mediana, junto a la caja de bigotes, que es la misma pareja de gráficos que usé antes y que me permite mantener una lectura comparable entre ambos biomarcadores.

Lo que pretendo ver es si la proteína C reactiva repite el patrón extremo de la procalcitonina o si su distribución es distinta, pues la prueba de Shapiro-Wilk ya me adelantó que su asimetría es más suave; quiero comprobar visualmente cuán repartidos están sus valores, dónde caen su media y su mediana, y cuántos valores extremos presenta.

Anticipo que esa forma de la distribución me dará una primera señal sobre su capacidad diagnóstica, pues una variable cuyos valores se solapan mucho entre pacientes tiende a discriminar peor, algo que el estudio original observó precisamente con este marcador (Ljungström et al., 2017).

univariado(sepsis, "CRP", "Proteina C reactiva (mg/L)")

Análisis: Esta figura describe la proteína C reactiva con los mismos dos dibujos que usé para la procalcitonina, el histograma a la izquierda y la caja de bigotes a la derecha, y los leo juntos para compararlos con lo que ya había visto.

  • El eje horizontal del histograma representa la concentración en miligramos por litro y avanza desde 0 hasta poco más de 500, mientras el eje vertical vuelve a contar la frecuencia, es decir, cuántos pacientes caen en cada franja de valores, llegando hasta unos 160.

Lo primero que noto es que la forma es muy distinta a la de la procalcitonina, y ese contraste es lo valioso; aquí los pacientes no se apilan todos en una sola barra pegada al cero, sino que se reparten a lo ancho del eje, con varias barras altas y parejas en la zona baja, entre 0 y 100, que rondan los 120 a 160 pacientes cada una, y desde ahí un descenso gradual y escalonado que forma una cola hacia la derecha.

Esto concuerda con lo que la prueba de Shapiro-Wilk me había mostrado, pues la proteína C reactiva tuvo un estadístico de 0,91390, mucho más cercano a 1 que el 0,25941 de la procalcitonina; sigue siendo no normal, pero su asimetría es claramente más suave y sus valores están más extendidos.

Las dos líneas verticales confirman ese carácter más moderado, pues la línea azul punteada de la mediana y la línea roja de la media aparecen muy próximas entre sí, casi juntas alrededor de 100; siguen separadas, porque la cola derecha todavía empuja un poco el promedio hacia arriba, pero su cercanía me dice que aquí la media engaña mucho menos que en la procalcitonina, donde ambas estaban muy distanciadas.

Aun así, mantengo el criterio de describir la variable con la mediana, porque no es simétrica y la mediana sigue siendo el resumen más fiel (Bruce et al., 2022).

  • En la caja de bigotes de la derecha vuelvo a leer los cuartiles, esos cortes que parten a los pacientes ordenados en cuatro grupos iguales, y encuentro el contraste más revelador con el biomarcador anterior; mientras la caja de la procalcitonina era una línea aplastada contra el cero, la de la proteína C reactiva es un rectángulo con altura clara, que va aproximadamente desde 45 en su borde inferior hasta 175 en el superior, con la mediana dibujada como una línea gruesa cerca de 100.

Esto significa que el 50% central de los pacientes ocupa un rango amplio, de unos 45 a 175 miligramos por litro, es decir, que los pacientes difieren bastante entre sí en su proteína C reactiva, y que la distancia entre el primer y el tercer cuartil, que es el rango intercuartílico, es grande.

Por encima del bigote superior, que llega hasta cerca de 360, aparece una columna de puntos rojos atípicos que asciende hasta unos 520, pacientes con valores muy altos que conservo por ser casos clínicos reales y no errores.

Se observa que, a de la procalcitonina, aquí la caja es ancha y los atípicos son la continuación natural de una cola, no un salto abrupto desde una caja aplastada. Esta lectura conecta directamente con un hallazgo central del estudio original, pues los investigadores encontraron que la proteína C reactiva era de las que peor discriminaba la sepsis bacteriana, con áreas bajo la curva bajas, y la razón está justamente en lo que veo, ya que al estar sus valores tan repartidos y solaparse tanto entre enfermos y no enfermos, le cuesta separar unos de otros (Ljungström et al., 2017).

En conjunto, considero que comparar las dos figuras me deja una enseñanza clave para el análisis ROC que viene, esto es, que la procalcitonina concentra a casi todos los pacientes en valores bajos y reserva los altos para los más graves, mientras que la proteína C reactiva los reparte a todos en un rango amplio donde enfermos y sanos se mezclan.

Esa de forma me habla de por qué un biomarcador podría discriminar mejor que el otro, y es precisamente lo que pondré a prueba al construir y comparar sus curvas (Ljungström et al., 2017).

9 Análisis bivariado, la correlación entre variables continuas

Hasta aquí describí cada variable por separado, pero ahora necesito estudiar cómo se relacionan entre sí, porque dos variables que miden información parecida tienden a moverse juntas, y eso importa tanto para entender la fisiología como para anticipar redundancias entre predictores (Bruce et al., 2022).

Para ello calcularé la correlación entre todas las variables continuas y la representaré como un mapa de calor, que traduce cada coeficiente en un color, de modo que las relaciones fuertes salten a la vista sin tener que leer una tabla de números.

  • El coeficiente de correlación resume en un solo valor, entre menos 1 y más 1, qué tan asociadas linealmente están dos variables, donde un valor cercano a más 1 indica que suben juntas, cercano a menos 1 que una sube cuando la otra baja, y cercano a 0 que no hay relación lineal (Bruce et al., 2022).

Lo que pretendo ver es si mis dos biomarcadores, la procalcitonina y la proteína C reactiva, están muy correlacionados entre sí, pues si lo estuvieran aportarían información diagnóstica parecida, mientras que si son poco redundantes podrían complementarse, tal como exploró el estudio original al combinar varios marcadores (Ljungström et al., 2017).

continuas <- sepsis %>%
  dplyr::select(Age, SBP, FR, SatO2, HR, Bodytemperature, Haemoglobin,
                Leukocyteconcentration, CRP, Procalcitonin, NL_ratio, P_lactate)

mat_cor <- cor(continuas, method = "spearman", use = "pairwise.complete.obs")

corrplot::corrplot(
  mat_cor,
  method = "color",
  type = "upper",
  addCoef.col = "black",
  number.cex = 0.7,
  tl.col = "black",
  tl.srt = 45,
  tl.cex = 0.8,
  col = colorRampPalette(c("#C0392B", "white", "#1F618D"))(200),
  diag = FALSE
)

Análisis: Esta figura es un mapa de calor de correlaciones, que elegí porque convierte una matriz de números en colores y me deja ver de inmediato qué variables se mueven juntas.

Cada celda cruza dos variables y muestra su coeficiente de correlación de Spearman, que usé en lugar del de Pearson precisamente porque mis variables no son normales; Spearman se basa en el orden de los datos y no en sus valores crudos, lo que lo hace resistente a los valores extremos que ya identifiqué (Bruce et al., 2022). - La escala de color azul representa correlaciones positivas, donde dos variables suben juntas; el rojo, las negativas, donde una sube mientras la otra baja; y el blanco, la ausencia de relación. Cuanto más intenso es el color, más fuerte es la asociación, y el número de cada celda da el valor exacto, entre −1 y +1. - Mis dos biomarcadores protagonistas: La procalcitonina y la proteína C reactiva correlacionan 0,40, un valor positivo y moderado dibujado en un azul medio; lo interpreto como una buena noticia para el análisis diagnóstico, porque 0,40 está lejos de 1 y significa que ambos marcadores comparten una parte de su información pero conservan cada uno una porción propia, es decir, no son redundantes. - Es importante esa complementariedad, porque es justamente la que permitió al estudio original construir biomarcadores compuestos que discriminaban mejor que cualquiera por separado, porque sumar señales parcialmente distintas aporta más que repetir la misma (Ljungström et al., 2017).

Reparo enseguida en las correlaciones más fuertes del mapa, que me sirven de control de coherencia fisiológica:

  • La más alta: Los leucocitos con el cociente neutrófilos-linfocitos, 0,49, en el azul más intenso, lo cual tiene todo el sentido porque ambos miden la respuesta celular inflamatoria y comparten los neutrófilos en su cálculo.
  • Otros marcadores de infección que suben juntos: La procalcitonina con el cociente neutrófilos-linfocitos en 0,44 y la procalcitonina con los leucocitos en 0,22. Me resulta tranquilizador que ni siquiera la correlación más alta, ese 0,49, se acerque a 1, pues cuando dos variables se correlacionan casi perfectamente aportan prácticamente la misma información, y al incluirlas juntas en un modelo se vuelve difícil distinguir el efecto propio de cada una, problema que se conoce como colinealidad (Bruce et al., 2022).

Como ninguna pareja llega a ese extremo, los biomarcadores pueden combinarse sin ese inconveniente, que es lo que hizo viable los marcadores compuestos del estudio original (Ljungström et al., 2017).

  • Las correlaciones negativas: La más marcada es la de la frecuencia respiratoria con la saturación de oxígeno, −0,38, pues un paciente que respira más rápido suele estar peor oxigenado, así que cuando una sube la otra baja.

En la misma línea, la edad con la saturación en −0,35 y la edad con la hemoglobina en −0,23, asociaciones débiles a moderadas coherentes con el deterioro fisiológico del paciente mayor; el resto de las celdas exhibe tonos muy pálidos, señal de correlaciones débiles o nulas.

En conjunto, leo en este mapa dos mensajes que me guían para lo que sigue: - el primero, que los biomarcadores son complementarios más que redundantes, con ese 0,40 que respalda la lógica del estudio de evaluarlos por separado y también combinados.

  • el segundo, que las correlaciones más fuertes son todas explicables por la fisiología y ninguna es tan extrema como para señalar información duplicada (Ljungström et al., 2017).

Esta observación no cambia el plan de la Parte 1 del taller, donde analizaré cada biomarcador de forma individual con curvas ROC, pero sí me ilumina por qué la combinación de marcadores resultó valiosa cuando los autores la pusieron a prueba.

10 Base de casos completos para el análisis diagnóstico (paso 2)

El segundo paso del taller me pide realizar un análisis de casos completos. De forma coherente con la decisión que ya argumenté —eliminar faltantes solo en las variables que cada análisis usa, y no de manera global (Westreich, 2020)—, construyo una única base de trabajo que conserve los episodios con información completa en las tres variables que intervienen en toda la Parte 1: el desenlace Sepsis2, la procalcitonina y la proteína C reactiva. Para un análisis de exactitud diagnóstica que solo emplea estas tres, “casos completos” significa precisamente completos en ellas, no en las 23 variables de la base, pues descartar un episodio por un signo vital ausente eliminaría una procalcitonina perfectamente medida.

Trabajar sobre una sola base me da dos ventajas: garantiza que todos los pasos siguientes compartan el mismo denominador, y hace posible la prueba de DeLong del paso 5, que exige comparar las dos curvas en los mismos pacientes (DeLong et al., 1988). Conservo intacto el objeto sepsis y creo la base analítica aparte, para no perder trazabilidad.

Espero conservar 1.514 episodios de los 1.537 clasificables, perdiendo únicamente los 23 en los que falta la proteína C reactiva, y espero que el desenlace se reparta en 657 con sepsis y 857 sin sepsis. Lo verificaré con el conteo y la tabla de frecuencias antes de avanzar.

sepsis_cc <- sepsis %>%
  tidyr::drop_na(Sepsis2, Procalcitonin, CRP)

janitor::tabyl(sepsis_cc$Sepsis2) %>%
  adorn_totals("row") %>%
  adorn_pct_formatting(digits = 1) %>%
  kbl(col.names = c("Sepsis-2", "n", "Porcentaje"), align = "lrr") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Sepsis-2 n Porcentaje
No 857 56.6%
657 43.4%
Total 1514 100.0%

Análisis: Mi base de trabajo quedó conformada por 1.514 episodios, de los cuales 657 corresponden a pacientes con sepsis bacteriana según el criterio Sepsis-2 y 857 a pacientes sin ella. Esta es la población única sobre la que sustentaré toda la Parte 1, y en su composición debo interpretar la exclusión que apliqué para llegar a ella y la prevalencia del evento.

  • 1. Apliqué el criterio de casos completos de manera selectiva, solo sobre las tres variables que intervienen en el análisis diagnóstico —el desenlace, la procalcitonina y la proteína C reactiva—, y no sobre la base entera.

Con ese criterio quedaron fuera 23 de los 1.537 episodios clasificables, todos por ausencia de proteína C reactiva, pues la procalcitonina estaba completa y los indeterminados del desenlace ya los había retirado antes.

Esa cifra de 23 excluidos equivale al 1,5 % de los episodios clasificables, proporción que resulta de dividir los 23 perdidos entre los 1.537 con desenlace definido; es una fracción lo bastante pequeña como para no comprometer la potencia del análisis.

  • 2. Opté por esta vía, y no por eliminar todo registro con algún dato ausente, porque esa alternativa habría sacrificado 378 episodios —los que quedaban al exigir información completa en las 23 variables—, es decir cerca de una cuarta parte de la cohorte, en su mayoría por signos vitales que ni siquiera participan en la curva ROC; restringir la exclusión a las variables que realmente empleo evita esa pérdida injustificada y es la estrategia recomendada cuando los faltantes no se concentran en las variables de interés (Westreich, 2020).

De todos los rasgos de esta base, el que mayor peso tendrá más adelante es la prevalencia del evento, que alcanza el 43,4 %, valor que surge de dividir los 657 pacientes con sepsis entre los 1.514 que conforman la muestra.

Es una prevalencia alta, y cobra sentido al recordar el diseño del estudio de referencia, pues todos estos pacientes llegaron al servicio de urgencias por sospecha clínica de sepsis, no provenían de un muestreo de la población general, de ahí que cerca de la mitad terminara confirmando el diagnóstico (Ljungström et al., 2017).

Esta magnitud es importante, porque fija el terreno sobre el que interpretaré el rendimiento de los biomarcadores, ya que en un escenario donde el evento es tan frecuente, un resultado negativo tiene más valor para descartar de lo que tendría en una población de bajo riesgo, mientras que la sensibilidad y la especificidad que estimaré describirán a la prueba con independencia de esta frecuencia, por tratarse de propiedades intrínsecas y no condicionadas por la prevalencia (Roy-García et al., 2023).

11 Curva ROC y punto de corte de la procalcitonina (paso 3)

Con mi base analítica ya consolidada en 1.514 episodios, entro a evaluar qué tan bien la procalcitonina discrimina la sepsis bacteriana. El taller me pide el área bajo la curva, el punto de corte de Youden y la sensibilidad y especificidad en ese punto; sin embargo, me propongo no quedarme en el valor que arroja la función, sino reconstruir todo el andamiaje que lo sostiene, porque cada número de una prueba diagnóstica nace de una tabla de 2×2 y de una cadena de razonamiento que conviene hacer explícita (Fletcher, 2021).

11.1 El punto de corte y la matriz de confusión 2×2

Toda medida de exactitud diagnóstica se calcula sobre una tabla de contingencia de 2×2 que enfrenta el resultado de la prueba con el verdadero estado del paciente (Fletcher, 2021). Como la procalcitonina es una variable continua, primero debo elegir un umbral que la convierta en una prueba binaria; para ello uso el punto que maximiza el índice de Youden, criterio que equilibra sensibilidad y especificidad y que realizaré más adelante.

Con ese umbral construyo la matriz, y de sus cuatro celdas nacerán todas las métricas siguientes, por tanto, las celdas son:

  • Verdaderos positivos (VP). Pacientes con sepsis a quienes la prueba clasifica correctamente como positivos.
  • Falsos positivos (FP). Pacientes sin sepsis que la prueba marca erróneamente como positivos, es decir, la falsa alarma.
  • Falsos negativos (FN). Pacientes con sepsis que la prueba deja pasar como negativos, el error más costoso en una urgencia tiempo-dependiente.
  • Verdaderos negativos (VN). Pacientes sin sepsis correctamente clasificados como negativos.
cat("Niveles de Sepsis2:", paste(levels(sepsis_cc$Sepsis2), collapse = " | "), "\n")
## Niveles de Sepsis2: No | Sí
print(table(sepsis_cc$Sepsis2, useNA = "ifany"))
## 
##  No  Sí 
## 857 657
nivel_evento <- if ("S\u00ed" %in% levels(sepsis_cc$Sepsis2)) "S\u00ed" else "Yes"
nivel_ref    <- "No"


curva_pct <- roc(
  response  = sepsis_cc$Sepsis2,
  predictor = sepsis_cc$Procalcitonin,
  levels    = c(nivel_ref, nivel_evento),
  direction = "<"
)

corte_youden <- coords(
  curva_pct, x = "best", best.method = "youden",
  ret = c("threshold", "sensitivity", "specificity"),
  transpose = FALSE
)
umbral_pct <- corte_youden$threshold[1]
corte_youden
sepsis_cc <- sepsis_cc %>%
  mutate(
    PCT_bin = factor(
      dplyr::if_else(Procalcitonin >= umbral_pct, "Alta", "Baja"),
      levels = c("Alta", "Baja")
    )
  )

Sepsis_diag <- factor(sepsis_cc$Sepsis2, levels = c(nivel_evento, nivel_ref))

tabla_2x2 <- table(Prueba = sepsis_cc$PCT_bin, Sepsis = Sepsis_diag)

VP <- as.integer(tabla_2x2["Alta", nivel_evento])
FP <- as.integer(tabla_2x2["Alta", nivel_ref])
FN <- as.integer(tabla_2x2["Baja", nivel_evento])
VN <- as.integer(tabla_2x2["Baja", nivel_ref])

tabla_2x2
##       Sepsis
## Prueba  Sí  No
##   Alta 429 376
##   Baja 228 481
tibble::tibble(
  Fuente        = c("Matriz 2x2 (c\u00e1lculo manual)", "Curva ROC (funci\u00f3n coords)"),
  Sensibilidad  = c(VP / (VP + FN), corte_youden$sensitivity[1]),
  Especificidad = c(VN / (VN + FP), corte_youden$specificity[1])
) %>%
  kableExtra::kbl(digits = 5, align = "lrr",
    caption = "Verificaci\u00f3n de consistencia entre la matriz 2\u00d72 y la curva ROC") %>%
  kableExtra::kable_styling(bootstrap_options = c("hover","condensed"),
    full_width = FALSE, position = "center") %>%
  kableExtra::row_spec(0, background = "#9DBAD4", color = "#1c2b3a", bold = TRUE)
Verificación de consistencia entre la matriz 2×2 y la curva ROC
Fuente Sensibilidad Especificidad
Matriz 2x2 (cálculo manual) 0.65297 0.56126
Curva ROC (función coords) 0.65297 0.56126
matriz_display <- data.frame(
  resultado = c(
    "Prueba positiva: procalcitonina \u2265 0,175 ng/mL",
    "Prueba negativa: procalcitonina < 0,175 ng/mL",
    "Total"),
  presente = c(
    paste0("Verdaderos positivos<br><b>n = ", VP, "</b>"),
    paste0("Falsos negativos<br><b>n = ", FN, "</b>"),
    paste0("<b>n = ", VP + FN, "</b>")),
  ausente = c(
    paste0("Falsos positivos<br><b>n = ", FP, "</b>"),
    paste0("Verdaderos negativos<br><b>n = ", VN, "</b>"),
    paste0("<b>n = ", FP + VN, "</b>")),
  total = c(
    paste0("<b>n = ", VP + FP, "</b>"),
    paste0("<b>n = ", FN + VN, "</b>"),
    paste0("<b>n = ", VP + FP + FN + VN, "</b>")),
  stringsAsFactors = FALSE
)

matriz_display %>%
  kableExtra::kbl(escape = FALSE, align = "lccc",
    col.names = c("Resultado de la prueba", "Sepsis presente", "Sepsis ausente", "Total"),
    caption = paste0("Matriz 2\u00d72 para procalcitonina en el punto de Youden ",
                     "(corte \u2265 0,175 ng/mL). Criterio de referencia: Sepsis-2.")) %>%
  kableExtra::kable_styling(bootstrap_options = c("hover","condensed"),
    full_width = FALSE, position = "center", font_size = 14) %>%
  kableExtra::add_header_above(
    c(" " = 1, "Sepsis bacteriana (criterio Sepsis-2)" = 2, " " = 1),
    bold = TRUE, background = "#C9A9E0", color = "#2d2440") %>%
  kableExtra::row_spec(0, background = "#EDE4F5", bold = TRUE) %>%
  kableExtra::row_spec(1, background = "#FBEEF3") %>%
  kableExtra::row_spec(2, background = "#EAF6EC") %>%
  kableExtra::row_spec(3, bold = TRUE) %>%
  kableExtra::column_spec(1, bold = TRUE, width = "6cm")
Matriz 2×2 para procalcitonina en el punto de Youden (corte ≥ 0,175 ng/mL). Criterio de referencia: Sepsis-2.
Sepsis bacteriana (criterio Sepsis-2)
Resultado de la prueba Sepsis presente Sepsis ausente Total
Prueba positiva: procalcitonina ≥ 0,175 ng/mL Verdaderos positivos
n = 429
Falsos positivos
n = 376
n = 805
Prueba negativa: procalcitonina < 0,175 ng/mL Falsos negativos
n = 228
Verdaderos negativos
n = 481
n = 709
Total n = 657 n = 857 n = 1514

Análisis: Esta matriz de 2×2 es el cimiento de todo el paso 3, pues sobre sus cuatro celdas calcularé enseguida la sensibilidad, la especificidad, los valores predictivos y las razones de verosimilitud (Fletcher, 2021).

La construí dicotomizando la procalcitonina en el umbral de Youden de 0,175 ng/mL, de modo que un valor igual o superior lo cuento como prueba positiva y uno inferior como prueba negativa; enfrenté ese resultado, en las filas, contra el verdadero estado del paciente según el criterio de referencia Sepsis-2, en las columnas. Los 1.514 episodios se reparten así:

  • 429 verdaderos positivos. Pacientes con sepsis a quienes la procalcitonina alta clasifica correctamente; de los 657 enfermos, la prueba detecta a 429, y de esta celda nacerá la sensibilidad.

  • 481 verdaderos negativos. Pacientes sin sepsis correctamente identificados por una procalcitonina baja; de los 857 sanos, la prueba descarta bien a 481, base de la especificidad.

  • 376 falsos positivos. Pacientes sin sepsis que la prueba marca como positivos; esta falsa alarma abarca casi la mitad de los sanos y ya anticipa una especificidad modesta.

  • 228 falsos negativos. Pacientes con sepsis que la prueba deja escapar como negativos; es el error más costoso en una urgencia tiempo-dependiente, porque implica no tratar a tiempo a quien sí tiene la infección (Ljungström et al., 2017).

De los 657 enfermos se reparten en 429 detectados y 228 perdidos, mientras que los 857 sanos se dividen en 481 bien descartados y 376 falsamente alarmados. Estas dos columnas corresponden a la sensibilidad y la especificidad, que no dependen de cuántos enfermos haya sino de qué tan bien la prueba reconoce a cada grupo (Fletcher, 2021).

La lectura por filas me da la mirada clínica frente al resultado, antes de conocer el diagnóstico: de las 805 pruebas positivas, 429 aciertan y 376 se equivocan; de las 709 pruebas negativas, 481 aciertan y 228 se equivocan. Estas dos filas corresponden a los valores predictivos, que sí dependen de la prevalencia y que calcularé enseguida (Fletcher, 2021).

En terminos generales, salta a la vista que la columna de errores es abultada, con 376 falsos positivos y 228 falsos negativos que juntos suman 604 clasificaciones equivocadas sobre 1.514, es decir, la prueba se equivoca en cerca de cuatro de cada diez pacientes.

Esto es reflejo fiel de un biomarcador que discrimina de forma modesta, pues el umbral de Youden, al ser tan bajo (0,175 ng/mL), prioriza no perder enfermos, y por eso atrapa a 429 de los 657, pero paga ese precio alarmando a 376 sanos.

Contraste con el estudio de referencia: este comportamiento concuerda con lo que reportaron Ljungström et al. (2017), quienes encontraron que la procalcitonina, pese a ser uno de los marcadores más estudiados en sepsis, alcanzaba una capacidad discriminativa apenas moderada para la sepsis bacteriana definida por Sepsis-2, claramente inferior a la de marcadores compuestos.

La abundancia de falsos positivos y negativos que veo en mi matriz es la traducción, celda a celda, de esa discriminación limitada, pues cuando las distribuciones de procalcitonina de enfermos y sanos se solapan de manera importante, ningún umbral logra separarlas de forma limpia, y la tabla lo evidencia (Ljungström et al., 2017).

Resulta importante que 0,175 ng/mL es el corte estadísticamente óptimo según Youden, no necesariamente el clínicamente adoptado. En la práctica se emplea con frecuencia el umbral de 0,5 ng/mL, que exige más para llamar positiva a la prueba y desplazaría el balance hacia menos falsas alarmas a costa de perder más enfermos; esa matriz alternativa la construiré en el paso 7, lo que me permitirá comparar de forma directa cómo cambia el rendimiento de la procalcitonina según el punto de corte que se elija (Fletcher, 2021).

11.2 Sensibilidad, especificidad y valores predictivos

De las cuatro celdas de la matriz derivo las cuatro medidas clásicas de una prueba diagnóstica, que se agrupan en dos parejas de naturaleza distinta (Fletcher, 2021):

  • La sensibilidad y la especificidad describen el comportamiento de la prueba dentro de cada grupo de pacientes y no dependen de la prevalencia; responden a la pregunta del investigador, que ya conoce el diagnóstico:

dado que el paciente está enfermo, o sano, ¿con qué probabilidad la prueba lo reconoce?

  • Los valores predictivos describen qué me dice un resultado concreto sobre el paciente y sí dependen de la prevalencia; responden a la pregunta del clínico, que aún no conoce el diagnóstico:

dado que la prueba salió positiva, o negativa, ¿con qué probabilidad el paciente está realmente enfermo, o sano?

\[\text{Sensibilidad} = \frac{\text{Verdaderos positivos}}{\text{Verdaderos positivos} + \text{Falsos negativos}} = \frac{VP}{VP + FN}\]

\[\text{Especificidad} = \frac{\text{Verdaderos negativos}}{\text{Verdaderos negativos} + \text{Falsos positivos}} = \frac{VN}{VN + FP}\]

\[\text{Valor predictivo positivo} = \frac{\text{Verdaderos positivos}}{\text{Verdaderos positivos} + \text{Falsos positivos}} = \frac{VP}{VP + FP}\]

\[\text{Valor predictivo negativo} = \frac{\text{Verdaderos negativos}}{\text{Verdaderos negativos} + \text{Falsos negativos}} = \frac{VN}{VN + FN}\]

Se  <- VP / (VP + FN)
Sp  <- VN / (VN + FP)
VPP <- VP / (VP + FP)
VPN <- VN / (VN + FN)

ic_wilson <- function(exitos, total) {
  pt <- prop.test(exitos, total, correct = FALSE)$conf.int
  sprintf("%.1f%% \u2013 %.1f%%", pt[1] * 100, pt[2] * 100)
}

tabla_metricas <- tibble::tibble(
  Medida = c("Sensibilidad", "Especificidad",
             "Valor predictivo positivo", "Valor predictivo negativo"),
  Sustitucion = c(
    paste0(VP, " / (", VP, " + ", FN, ")"),
    paste0(VN, " / (", VN, " + ", FP, ")"),
    paste0(VP, " / (", VP, " + ", FP, ")"),
    paste0(VN, " / (", VN, " + ", FN, ")")),
  Resultado = scales::percent(c(Se, Sp, VPP, VPN), accuracy = 0.1),
  IC95 = c(ic_wilson(VP, VP + FN), ic_wilson(VN, VN + FP),
           ic_wilson(VP, VP + FP), ic_wilson(VN, VN + FN))
)

tabla_metricas %>%
  kableExtra::kbl(align = "llcc",
    col.names = c("Medida", "Sustituci\u00f3n num\u00e9rica", "Resultado", "IC 95%"),
    caption = "Sensibilidad, especificidad y valores predictivos de la procalcitonina (corte de Youden, 0,175 ng/mL)") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
    full_width = FALSE, position = "center", font_size = 14) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::column_spec(1, bold = TRUE) %>%
  kableExtra::row_spec(1:2, background = "#EAF6EC") %>%
  kableExtra::row_spec(3:4, background = "#FBEEF3")
Sensibilidad, especificidad y valores predictivos de la procalcitonina (corte de Youden, 0,175 ng/mL)
Medida Sustitución numérica Resultado IC 95%
Sensibilidad 429 / (429 + 228) 65.3% 61.6% – 68.8%
Especificidad 481 / (481 + 376) 56.1% 52.8% – 59.4%
Valor predictivo positivo 429 / (429 + 376) 53.3% 49.8% – 56.7%
Valor predictivo negativo 481 / (481 + 228) 67.8% 64.3% – 71.2%

Análisis: Con esta tabla traduzco las cuatro celdas de la matriz en las cuatro medidas que caracterizan a la procalcitonina, mostrando en cada fila la sustitución numérica y el resultado con su intervalo de confianza del 95 %.

  • Sensibilidad de 65,3 %. Sale de dividir los 429 verdaderos positivos entre los 657 enfermos; significa que, de cada 100 pacientes que de verdad tienen sepsis, la procalcitonina alta detecta a unos 65 y deja escapar a 35. Es una capacidad de detección apenas moderada, insuficiente por sí sola para descartar la enfermedad con tranquilidad (Fletcher, 2021).

  • Especificidad de 56,1 %: Resulta de dividir los 481 verdaderos negativos entre los 857 sanos; quiere decir que, de cada 100 pacientes sin sepsis, la prueba solo descarta bien a unos 56 y alarma falsamente a los otros 44. Esta especificidad baja es el reflejo de los muchos falsos positivos de la matriz, y confirma que un umbral tan bajo como 0,175 ng/mL prioriza no perder enfermos a costa de alarmar a muchos sanos.

  • Valor predictivo positivo de 53,3 %: Proviene de dividir los 429 verdaderos positivos entre las 805 pruebas positivas; me dice que, cuando la procalcitonina sale alta, la probabilidad de que el paciente realmente tenga sepsis es de apenas un 53 %, poco más que una moneda al aire.

  • Valor predictivo negativo de 67,8 %: Sale de dividir los 481 verdaderos negativos entre las 709 pruebas negativas; indica que, cuando la prueba sale baja, hay un 68 % de probabilidad de que el paciente esté sano, y por tanto un 32 % de que la sepsis se haya escapado pese al resultado negativo.

La sensibilidad y la especificidad son propiedades de la prueba y se mantendrían aunque cambiara la frecuencia de la enfermedad; en cambio, los valores predictivos están moldeados por la prevalencia del 43,4 % de esta cohorte, tan alta porque todos los pacientes ingresaron por sospecha de sepsis(Ljungström et al., 2017).

En una población de menor riesgo, el valor predictivo positivo caería y el negativo subiría, aunque la sensibilidad y la especificidad no se movieran (Fletcher, 2021).

Estos valores concuerdan con el rendimiento modesto que Ljungström et al. (2017) reportaron para la procalcitonina en la discriminación de la sepsis bacteriana por criterios Sepsis-2.

Ninguna de las cuatro medidas se acerca a las cifras que yo esperaría de una prueba confirmatoria o de exclusión sólida; los valores predictivos, apenas por encima del 50-68 %, muestran que un resultado aislado de procalcitonina —en cualquiera de sus dos sentidos— deja aún un margen amplio de incertidumbre, lo que refuerza la conclusión del estudio de que este marcador rinde mejor combinado con otros que en solitario (Ljungström et al., 2017).

11.3 Razones de verosimilitud

La sensibilidad y la especificidad describen la prueba en abstracto, pero en la clínica necesito saber cuánto cambia su sospecha ante un resultado concreto; ese puente lo tienden las razones de verosimilitud, que comparan la probabilidad de un resultado en los enfermos frente a los sanos (Fletcher, 2021).

  • La razón de verosimilitud positiva indica cuántas veces es más probable obtener una prueba positiva en un paciente enfermo que en uno sano; cuanto más por encima de 1, más fuerza tiene un resultado positivo para acercar al diagnóstico.

  • La razón de verosimilitud negativa indica cuántas veces es más probable obtener una prueba negativa en un enfermo que en un sano; cuanto más por debajo de 1, más fuerza tiene un resultado negativo para alejar del diagnóstico.

Su gran ventaja es doble, pues combinan sensibilidad y especificidad en un solo número por cada tipo de resultado, no dependen de la prevalencia, como la sensibilidad y la especificidad, y son la pieza que permite pasar de la probabilidad pre-prueba a la post-prueba mediante el teorema de Bayes, que aplicaré enseguida (Fletcher, 2021). Escribo cada fórmula completa en su forma expandida desde las celdas:

\[\text{Razón de verosimilitud positiva} = \frac{\text{Sensibilidad}}{1 - \text{Especificidad}} = \frac{VP/(VP + FN)}{FP/(FP + VN)}\]

\[\text{Razón de verosimilitud negativa} = \frac{1 - \text{Sensibilidad}}{\text{Especificidad}} = \frac{FN/(VP + FN)}{VN/(VN + FP)}\]

LR_pos <- Se / (1 - Sp)
LR_neg <- (1 - Se) / Sp

tabla_lr <- tibble::tibble(
  Medida = c("Raz\u00f3n de verosimilitud positiva (LR+)",
             "Raz\u00f3n de verosimilitud negativa (LR\u2212)"),
  Formula = c("Sensibilidad / (1 \u2212 Especificidad)",
              "(1 \u2212 Sensibilidad) / Especificidad"),
  Sustitucion = c(
    paste0(sprintf("%.4f", Se), " / (1 \u2212 ", sprintf("%.4f", Sp), ")"),
    paste0("(1 \u2212 ", sprintf("%.4f", Se), ") / ", sprintf("%.4f", Sp))),
  Resultado = c(sprintf("%.3f", LR_pos), sprintf("%.3f", LR_neg))
)

tabla_lr %>%
  kableExtra::kbl(align = "lllc",
    col.names = c("Medida", "F\u00f3rmula", "Sustituci\u00f3n num\u00e9rica", "Resultado"),
    caption = "Razones de verosimilitud de la procalcitonina (corte de Youden, 0,175 ng/mL)") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
    full_width = FALSE, position = "center", font_size = 14) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::column_spec(1, bold = TRUE) %>%
  kableExtra::row_spec(1, background = "#FBEEF3") %>%
  kableExtra::row_spec(2, background = "#EAF6EC")
Razones de verosimilitud de la procalcitonina (corte de Youden, 0,175 ng/mL)
Medida Fórmula Sustitución numérica Resultado
Razón de verosimilitud positiva (LR+) Sensibilidad / (1 − Especificidad) 0.6530 / (1 − 0.5613) 1.488
Razón de verosimilitud negativa (LR−) (1 − Sensibilidad) / Especificidad (1 − 0.6530) / 0.5613 0.618

Análisis: Esta tabla presenta las dos razones de verosimilitud de la procalcitonina:

  • Razón de verosimilitud positiva de 1,49: Sale de dividir la sensibilidad (0,6530) entre el complemento de la especificidad (1 − 0,5613 = 0,4387). Significa que una procalcitonina alta es apenas 1,5 veces más frecuente en un paciente con sepsis que en uno sano; es un valor muy cercano a 1, y por convención una razón positiva solo empieza a ser clínicamente útil por encima de 5, y contundente por encima de 10 (Fletcher, 2021). Un 1,49 apenas mueve la aguja del diagnóstico.

  • Razón de verosimilitud negativa de 0,62: Resulta de dividir el complemento de la sensibilidad (1 − 0,6530 = 0,3470) entre la especificidad (0,5613). Indica que una procalcitonina baja es 0,62 veces tan frecuente en un enfermo como en un sano; para que un resultado negativo descarte con fuerza haría falta un valor por debajo de 0,1, de modo que este 0,62, tan cercano a 1, tampoco permite excluir la sepsis con tranquilidad (Fletcher, 2021).

Me pregunto ahora ¿Qué significan estos dos números juntos? Ambas razones se sitúan muy cerca de 1, que es el valor de una prueba inútil, aquella cuyo resultado no me cambia la probabilidad del diagnóstico.

Esto confirma, desde otra óptica, lo que ya mostraron la matriz y las métricas anteriores, pues en su punto de corte óptimo, la procalcitonina modifica poco la sospecha clínica, tanto si sale alta como si sale baja.

Es coherente con un área bajo la curva modesta, pues una prueba que discrimina poco necesariamente produce razones de verosimilitud tímidas.

Contraste con el estudio de referencia: este hallazgo se alinea con Ljungström et al. (2017), quienes situaron a la procalcitonina como un marcador de rendimiento intermedio para la sepsis bacteriana, por debajo de otros marcadores y de las combinaciones que ellos propusieron.

Unas razones de verosimilitud tan próximas a 1 explican, en el lenguaje bayesiano que desarrollaré enseguida, por qué un resultado aislado de procalcitonina desplaza tan poco la probabilidad pre-prueba del paciente, precisamente ese desplazamiento es lo que cuantificaré con el nomograma de Fagan (Fletcher, 2021).

11.4 Del resultado a la probabilidad, con el razonamiento bayesiano y nomograma de Fagan

Las razones de verosimilitud cobran su verdadero sentido cuando las uso para actualizar la probabilidad de enfermedad de un paciente concreto, que es la esencia del teorema de Bayes aplicado al diagnóstico (Fletcher, 2021).

La idea es que un resultado de laboratorio no me da una certeza, sino que transforma la probabilidad que tenía antes de la prueba (pre-prueba) en una probabilidad actualizada después de ella (post-prueba), y la razón de verosimilitud es el factor exacto de esa transformación.

El cálculo, sin embargo, no se hace directamente con probabilidades sino con chances (en inglés, odds), que son otra forma de expresar lo mismo. Mientras la probabilidad compara los casos favorables contra el total, las chances comparan los casos favorables contra los desfavorables; por eso la conversión es necesaria en tres pasos (Fletcher, 2021):

  1. Convierto la probabilidad pre-prueba en chances pre-prueba, con la fórmula:

\[\text{Chances} = \frac{\text{Probabilidad}}{1 - \text{Probabilidad}}\]

  1. Multiplico las chances pre-prueba por la razón de verosimilitud para obtener las chances post-prueba, que es el núcleo del teorema de Bayes en su forma de chances:

\[\text{Chances post-prueba} = \text{Chances pre-prueba} \times \text{Razón de verosimilitud}\]

  1. Reconvierto las chances post-prueba en probabilidad post-prueba, con la fórmula inversa:

\[\text{Probabilidad} = \frac{\text{Chances}}{1 + \text{Chances}}\]

Como probabilidad pre-prueba usaré la prevalencia de la sepsis en mi cohorte, 43,4 %, que es la mejor estimación de la sospecha basal antes de conocer la procalcitonina.

Aplicaré la cascada dos veces:

  • una con la razón de verosimilitud positiva, para ver a dónde lleva un resultado alto, y otra con la negativa, para ver a dónde lleva un resultado bajo.
prev <- (VP + FN) / (VP + FP + FN + VN)
chances_pre <- prev / (1 - prev)
chances_post_pos <- chances_pre * LR_pos
chances_post_neg <- chances_pre * LR_neg
prob_post_pos <- chances_post_pos / (1 + chances_post_pos)
prob_post_neg <- chances_post_neg / (1 + chances_post_neg)

tabla_bayes <- tibble::tibble(
  paso = c(
    "Probabilidad pre-prueba (prevalencia)",
    "Chances pre-prueba",
    "Chances post-prueba tras resultado positivo",
    "Probabilidad post-prueba tras resultado positivo",
    "Chances post-prueba tras resultado negativo",
    "Probabilidad post-prueba tras resultado negativo"),
  formula = c(
    "(VP + FN) / N",
    "P / (1 \u2212 P)",
    "Chances pre \u00d7 LR positivo",
    "Chances / (1 + Chances)",
    "Chances pre \u00d7 LR negativo",
    "Chances / (1 + Chances)"),
  sustitucion = c(
    sprintf("(%d + %d) / %d = %.4f", VP, FN, VP+FP+FN+VN, prev),
    sprintf("%.4f / (1 \u2212 %.4f) = %.4f", prev, prev, chances_pre),
    sprintf("%.4f \u00d7 %.3f = %.4f", chances_pre, LR_pos, chances_post_pos),
    sprintf("%.4f / (1 + %.4f) = %.1f%%", chances_post_pos, chances_post_pos, prob_post_pos*100),
    sprintf("%.4f \u00d7 %.3f = %.4f", chances_pre, LR_neg, chances_post_neg),
    sprintf("%.4f / (1 + %.4f) = %.1f%%", chances_post_neg, chances_post_neg, prob_post_neg*100))
)

tabla_bayes %>%
  kableExtra::kbl(align = "lll", escape = TRUE,
    col.names = c("Paso del c\u00e1lculo", "F\u00f3rmula general", "Sustituci\u00f3n y resultado"),
    caption = "Cascada bayesiana de la procalcitonina en el punto de Youden") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
    full_width = FALSE, position = "center", font_size = 13) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::column_spec(1, bold = TRUE, width = "6cm") %>%
  kableExtra::column_spec(2, width = "4.5cm") %>%
  kableExtra::pack_rows("Punto de partida (com\u00fan a ambos resultados)", 1, 2,
    background = "#EDE4F5", color = "#2d2440") %>%
  kableExtra::pack_rows("Si la prueba sale positiva", 3, 4,
    background = "#F4C2D7", color = "#5b2333") %>%
  kableExtra::pack_rows("Si la prueba sale negativa", 5, 6,
    background = "#BFE3C9", color = "#14361f") %>%
  kableExtra::row_spec(c(4, 6), bold = TRUE)
Cascada bayesiana de la procalcitonina en el punto de Youden
Paso del cálculo Fórmula general Sustitución y resultado
Punto de partida (común a ambos resultados)
Probabilidad pre-prueba (prevalencia) (VP + FN) / N (429 + 228) / 1514 = 0.4339
Chances pre-prueba P / (1 − P) 0.4339 / (1 − 0.4339) = 0.7666
Si la prueba sale positiva
Chances post-prueba tras resultado positivo Chances pre × LR positivo 0.7666 × 1.488 = 1.1410
Probabilidad post-prueba tras resultado positivo Chances / (1 + Chances) 1.1410 / (1 + 1.1410) = 53.3%
Si la prueba sale negativa
Chances post-prueba tras resultado negativo Chances pre × LR negativo 0.7666 × 0.618 = 0.4740
Probabilidad post-prueba tras resultado negativo Chances / (1 + Chances) 0.4740 / (1 + 0.4740) = 32.2%

Análisis: Esta tabla es importante porque no me dice solo cuánto rinde la procalcitonina, sino cómo cambia lo que yo creo sobre un paciente cuando llega el resultado de laboratorio.

La leo como un viaje en tres tramos, así:

  • El punto de partida, lo que sé antes de la prueba: antes de pedir la procalcitonina, lo único que sé de este paciente es que llegó a urgencias con un cuadro que hace sospechar sepsis, y que en esta población la prevalencia observada es del 43,4 %.

Esa es mi probabilidad pre-prueba, y la calculo como la prevalencia: los enfermos sobre el total, (429 + 228) / 1.514 = 0,4339, es decir un 43,4 %. Es mi apuesta de salida, la “corazonada clínica” informada antes de cualquier examen.

  • El cambio de probabilidad a chances: el teorema de Bayes no multiplica probabilidades directamente, sino que necesita que se las exprese en forma de chances.

La es simple, pues la probabilidad compara los enfermos contra el total, mientras que las chances comparan los enfermos contra los sanos; es la misma información contada como una relación entre lo que puede ocurrir y lo que no.

Convierto con la fórmula P / (1 − P): 0,4339 / (1 − 0,4339) = 0,7666. Ese 0,77 significa que, de partida, hay algo más de tres probabilidades a favor de la sepsis por cada cuatro en contra.

  • La primera rama, qué pasa si la procalcitonina sale alta: aplico el resultado multiplicando mis chances de partida por la razón de verosimilitud positiva, que es el factor exacto con que un resultado alto debe empujar mi sospecha: 0,7666 × 1,488 = 1,1410.

Mis chances subieron de 0,77 a 1,14, pero como quiero volver al idioma de siempre —probabilidad, no chances—, reconvierto con la fórmula inversa, chances / (1 + chances): 1,1410 / (1 + 1,1410) = 53,3 %. Significa que un paciente con procalcitonina alta pasa de una sospecha del 43,4 % a una del 53,3 %: subió, sí, pero apenas diez puntos, y sigo casi en la moneda al aire.

  • La segunda rama, qué pasa si la procalcitonina sale baja: repito el mismo mecanismo, pero con la razón de verosimilitud negativa, que empuja mi sospecha hacia abajo: 0,7666 × 0,618 = 0,4740.

Mis chances bajaron de 0,77 a 0,47. Reconvierto a probabilidad: 0,4740 / (1 + 0,4740) = 32,2 %. Es decir, un paciente con procalcitonina baja pasa del 43,4 % al 32,2 %. Bajó, pero también poco, pues todavía uno de cada tres pacientes con la prueba “tranquilizadora” seguiría teniendo sepsis.

Interpretación del comportamiento observado: concluyo que la procalcitonina, evaluada en su punto de corte óptimo, produce un desplazamiento diagnóstico de magnitud reducida. Partiendo de una probabilidad pre-prueba del 43,4 %, un resultado positivo la eleva solo hasta el 53,3 % y uno negativo la reduce hasta el 32,2 %, de modo que la probabilidad post-prueba permanece confinada en una franja de apenas 21 puntos porcentuales alrededor del valor basal.

Ninguno de los dos resultados traslada al paciente a una región de decisión clínica definida, pues no alcanza una probabilidad suficientemente alta para confirmar la sepsis ni suficientemente baja para descartarla con seguridad.

Este comportamiento es la consecuencia directa de unas razones de verosimilitud próximas a la unidad, ya que el LR positivo (1,49) y el LR negativo (0,62) se sitúan cerca de 1 —el valor que corresponde a una prueba sin capacidad discriminativa—, por lo que el factor que multiplica las chances pre-prueba modifica poco su magnitud.

La sensibilidad de la probabilidad post-prueba a la potencia del marcador es, sin embargo, considerable, pues si el LR positivo hubiese sido de 10, la probabilidad post-prueba positiva habría ascendido hasta cerca del 88 %, y con un LR negativo de 0,1 la post-prueba negativa habría descendido cerca del 7 %.

La estrechez de la franja observada es, por tanto, una expresión aritmética de la limitada capacidad discriminativa del biomarcador.

Contraste con el estudio de referencia: este hallazgo confirma, desde el razonamiento bayesiano, la conclusión que Ljungström et al. (2017) derivaron de su cohorte completa, donde ningún biomarcador aislado —la procalcitonina incluida— alcanzó a confirmar ni a descartar la sepsis bacteriana por sí solo, y la estrategia diagnósticamente rentable consistió en integrarlo con el resto del cuadro clínico y con marcadores adicionales.

La cascada bayesiana operacionaliza esa recomendación, pues cuando un resultado individual apenas modifica la probabilidad pre-prueba, la decisión clínica no puede sustentarse en él de forma aislada, sino en la integración del conjunto de la evidencia disponible sobre el paciente (Ljungström et al., 2017; Fletcher, 2021).

11.5 La misma prueba, distintos pacientes, la dependencia de la probabilidad pre-prueba

En la cascada anterior trabajé sobre un único punto de partida, la prevalencia del 43,4 % de mi cohorte. Sin embargo, un mismo resultado de procalcitonina no significa lo mismo en todos los pacientes, porque su efecto depende de la probabilidad que ya tenían antes de la prueba (Fletcher, 2021).

Para hacer visible esa dependencia, aplico la misma cascada bayesiana —con las mismas razones de verosimilitud de la procalcitonina— a una gama de probabilidades pre-prueba, desde un paciente de muy bajo riesgo hasta uno de riesgo muy alto, y observo a dónde lo lleva cada resultado.

escenarios <- c(0.05, 0.10, 0.25, prev, 0.60, 0.80)

post <- function(p, lr) {
  odds <- p / (1 - p)
  op <- odds * lr
  op / (1 + op)
}

tabla_escenarios <- tibble::tibble(
  pre = scales::percent(escenarios, accuracy = 0.1),
  pos = scales::percent(post(escenarios, LR_pos), accuracy = 0.1),
  neg = scales::percent(post(escenarios, LR_neg), accuracy = 0.1),
  perfil = c("Riesgo muy bajo", "Riesgo bajo", "Riesgo intermedio",
             "Mi cohorte (urgencias)", "Riesgo alto", "Riesgo muy alto")
)

tabla_escenarios %>%
  kableExtra::kbl(align = "cccl",
    col.names = c("Probabilidad pre-prueba", "Post-prueba si sale positiva",
                  "Post-prueba si sale negativa", "Perfil del paciente"),
    caption = "Efecto de la procalcitonina seg\u00fan la probabilidad pre-prueba del paciente") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
    full_width = FALSE, position = "center", font_size = 13) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::column_spec(4, italic = TRUE) %>%
  kableExtra::row_spec(4, background = "#EDE4F5", bold = TRUE)
Efecto de la procalcitonina según la probabilidad pre-prueba del paciente
Probabilidad pre-prueba Post-prueba si sale positiva Post-prueba si sale negativa Perfil del paciente
5.0% 7.3% 3.2% Riesgo muy bajo
10.0% 14.2% 6.4% Riesgo bajo
25.0% 33.2% 17.1% Riesgo intermedio
43.4% 53.3% 32.2% Mi cohorte (urgencias)
60.0% 69.1% 48.1% Riesgo alto
80.0% 85.6% 71.2% Riesgo muy alto

Análisis: Con esta tabla aplico la misma cascada bayesiana de la procalcitonina a seis pacientes distintos, que se diferencian únicamente en su probabilidad pre-prueba, y revela una lección central de las pruebas diagnósticas, pues el resultado no tiene un significado fijo, sino que se lee sobre el riesgo previo del paciente.

Recorro la columna del resultado positivo y observo su efecto desigual: en un paciente de riesgo muy bajo, con un 5 % pre-prueba, una procalcitonina alta lo eleva apenas al 7,3 %, un movimiento clínicamente irrelevante que no justificaría iniciar tratamiento.

En cambio, en un paciente de riesgo muy alto, con un 80 % pre-prueba, el mismo resultado positivo lo sube hasta cerca del 86 %, reforzando una sospecha que ya era fuerte. La prueba no cambió; cambió el paciente sobre el que interpreté.

Recorro la columna del resultado negativo y encuentro el fenómeno complementario: en el paciente de bajo riesgo, una procalcitonina baja lo deja en torno al 3 %, un valor lo bastante bajo para tranquilizar; pero en el de riesgo muy alto, el mismo resultado negativo apenas lo baja al 71 %, una probabilidad todavía demasiado elevada para descartar la sepsis con seguridad.

La fila resaltada corresponde a mi cohorte, con su prevalencia del 43,4 %, y confirma lo que ya obtuve en la cascada bayesiana: post-prueba del 53,3 % si sale positiva y del 32,2 % si sale negativa. Situarla dentro de esta gama me permite ver que mi población de urgencias ocupa una zona intermedia, precisamente aquella en la que un marcador de discriminación modesta resulta menos resolutivo, porque el paciente parte ya de una incertidumbre máxima.

Contraste con el estudio de referencia: esta dependencia del contexto refuerza la recomendación de Ljungström et al. (2017) de no interpretar la procalcitonina de forma aislada. La tabla muestra por qué un mismo valor de laboratorio puede ser irrelevante o decisivo según el paciente, es decir, sin conocer la probabilidad pre-prueba —es decir, sin el juicio clínico que la estima—, el resultado numérico por sí solo carece de significado accionable (Ljungström et al., 2017; Fletcher, 2021).

11.6 El área bajo la curva y la curva ROC de la procalcitonina

Hasta aquí he evaluado la procalcitonina en un único punto de corte, el de Youden. La curva ROC, en cambio, resume su rendimiento en todos los puntos de corte posibles a la vez, trazando la sensibilidad frente a la tasa de falsos positivos (1 − especificidad) a medida que el umbral se desliza de un extremo a otro (Roy-García et al., 2023).

El área bajo esa curva (AUC) condensa esa información en un solo número, que se interpreta como la probabilidad de que, tomados al azar un paciente enfermo y uno sano, el enfermo tenga una procalcitonina más alta que el sano; un AUC de 0,5 corresponde al azar y uno de 1 a la discriminación perfecta (Janssens y Martens, 2020).

auc_pct <- pROC::auc(curva_pct)
ic_auc  <- pROC::ci.auc(curva_pct, method = "delong")

tibble::tibble(
  Medida = "\u00c1rea bajo la curva (AUC)",
  Estimacion = sprintf("%.3f", as.numeric(auc_pct)),
  IC = sprintf("%.3f \u2013 %.3f", ic_auc[1], ic_auc[3])
) %>%
  kableExtra::kbl(align = "lcc",
    col.names = c("Medida", "Estimaci\u00f3n", "IC 95% (DeLong)"),
    caption = "\u00c1rea bajo la curva ROC de la procalcitonina para la sepsis bacteriana") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
    full_width = FALSE, position = "center", font_size = 14) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE)
Área bajo la curva ROC de la procalcitonina para la sepsis bacteriana
Medida Estimación IC 95% (DeLong)
Área bajo la curva (AUC) 0.641 0.613 – 0.669
library(ggplot2)

df_roc <- data.frame(
  esp = curva_pct$specificities,
  sen = curva_pct$sensitivities
)
df_roc$fpr <- 1 - df_roc$esp
df_roc <- df_roc[order(df_roc$fpr, df_roc$sen), ]

se_y  <- as.numeric(corte_youden$sensitivity[1])
sp_y  <- as.numeric(corte_youden$specificity[1])
fpr_y <- 1 - sp_y

etiqueta_auc <- sprintf("AUC = %.3f\nIC 95%%: %.3f \u2013 %.3f",
                        as.numeric(auc_pct), ic_auc[1], ic_auc[3])
etiqueta_youden <- sprintf("Punto de Youden\ncorte = 0,175 ng/mL\nSe = %.1f%%  |  Sp = %.1f%%",
                           se_y * 100, sp_y * 100)

ggplot(df_roc, aes(x = fpr, y = sen)) +
  geom_ribbon(aes(ymin = 0, ymax = sen), fill = "#E29CB0", alpha = 0.22) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "#9A9A9A") +
  geom_line(color = "#B5477E", linewidth = 1.1) +
  geom_point(x = fpr_y, y = se_y, color = "#5b3b6b", size = 3.5) +
  annotate("segment", x = fpr_y, xend = fpr_y, y = fpr_y, yend = se_y,
           linetype = "dotted", color = "#5b3b6b") +
  annotate("label", x = 0.55, y = 0.22, label = etiqueta_auc,
           fill = "#F7F3FA", color = "#5b3b6b", size = 4, fontface = "bold",
           label.size = 0, hjust = 0) +
  annotate("label", x = fpr_y + 0.03, y = se_y - 0.03, label = etiqueta_youden,
           fill = "#F7F3FA", color = "#5b3b6b", size = 3.2,
           label.size = 0, hjust = 0, vjust = 1) +
  scale_x_continuous("1 \u2212 Especificidad (tasa de falsos positivos)",
                     labels = scales::percent, limits = c(0, 1), expand = c(0, 0)) +
  scale_y_continuous("Sensibilidad (tasa de verdaderos positivos)",
                     labels = scales::percent, limits = c(0, 1), expand = c(0, 0)) +
  labs(title = "Curva ROC de la procalcitonina para la sepsis bacteriana",
       subtitle = "Criterio de referencia: Sepsis-2  |  n = 1.514 episodios") +
  coord_equal() +
  theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold"),
        panel.grid.minor = element_blank())

::: {.analisis} Análisis: Esta figura es la curva ROC de la procalcitonina, la síntesis visual de todo el paso 3, pues reúne en una sola línea el rendimiento del biomarcador en cada uno de sus puntos de corte posibles. La construí trazando la sensibilidad en el eje vertical frente a la tasa de falsos positivos en el horizontal, y sobre ella marqué el punto de Youden que he venido analizando.

En cuanto a la forma de la curva: parte del origen, asciende y se arquea hacia la esquina superior izquierda, que es el rincón de la prueba perfecta, aquel donde la sensibilidad es del 100 % sin ningún falso positivo. La curva de la procalcitonina se despega de la diagonal del azar, lo que confirma que el biomarcador discrimina, pero su arqueo es moderado y se mantiene lejos de esa esquina ideal, señal de una discriminación limitada.

El área bajo la curva cuantifica ese arqueo: 0,641, con un intervalo de confianza del 95 % de 0,613 a 0,669. Lo interpreto como que, si tomo al azar un paciente con sepsis y uno sin ella, hay un 64,1 % de probabilidad de que el enfermo tenga la procalcitonina más alta.

Está por encima del 50 % del azar, pero claramente por debajo del 80 % que suele considerarse el umbral de una discriminación clínicamente buena; el intervalo de confianza, además, no se acerca en ningún momento a ese valor, de modo que la modestia del marcador es un hallazgo firme y no un efecto del tamaño muestral (Janssens y Martens, 2020).

El punto de Youden, marcado sobre la curva, es el codo que más se aproxima a la esquina ideal, y corresponde al corte de 0,175 ng/mL con una sensibilidad del 65,3 % y una especificidad del 56,1 %. Verlo situado sobre la línea confirma visualmente lo que la cascada bayesiana ya me había dicho en números, pues incluso en su mejor equilibrio, el punto óptimo queda a media distancia entre el azar y la perfección.

En contraste con el estudio de referencia: este AUC de 0,641 es plenamente coherente con lo que reportaron Ljungström et al. (2017), quienes situaron a la procalcitonina como un marcador de capacidad discriminativa intermedia para la sepsis bacteriana por criterios Sepsis-2, por debajo de los marcadores compuestos que ellos propusieron.

La curva traduce a una imagen la conclusión que ha atravesado todo este análisis, donde la procalcitonina aporta información diagnóstica real, pero insuficiente por sí sola, y su verdadero valor emerge al integrarla con otros marcadores y con el juicio clínico (Ljungström et al., 2017).

12 Curva ROC y punto de corte de la proteína C reactiva (paso 4)

La proteína C reactiva se evalúa con la misma secuencia de análisis aplicada a la procalcitonina, consistente en la construcción de la matriz de 2×2 en el punto de corte óptimo, el cálculo de la sensibilidad, la especificidad y los valores predictivos, la estimación de las razones de verosimilitud y del área bajo la curva, y la representación de su curva ROC.

El propósito es doble, pues busco medir la capacidad diagnóstica de este biomarcador y, sobre todo, contrastarla con la de la procalcitonina, ya que la pregunta central no es cuánto rinde cada prueba por separado, sino cuál discrimina mejor la sepsis bacteriana (Ljungström et al., 2017).

El análisis se realiza sobre los mismos 1.514 episodios empleados con la procalcitonina. Evaluar ambos biomarcadores en los mismos pacientes es la condición que permite compararlos de forma válida, ya que así cualquier diferencia entre sus curvas se atribuye a su rendimiento y no a que se midieron en poblaciones distintas.

Esta es, además, la exigencia de la prueba de DeLong, con la que se contrastarán ambas curvas en el paso siguiente (DeLong et al., 1988).

12.1 La matriz de confusión de la proteína C reactiva

La construcción de la matriz de 2×2 parte de dicotomizar la proteína C reactiva en su punto de Youden, el umbral que maximiza la suma de sensibilidad y especificidad.

Este paso es necesario porque toda medida de exactitud diagnóstica se calcula sobre las cuatro celdas de la tabla de contingencia, que enfrentan el resultado de la prueba con el verdadero estado del paciente según el criterio de referencia Sepsis-2.

Espero que el umbral óptimo se sitúe en un valor alto de proteína C reactiva y que, dada la menor capacidad discriminativa de este marcador, la matriz presente más errores de clasificación que la de la procalcitonina. :::

curva_crp <- roc(
  response  = sepsis_cc$Sepsis2,
  predictor = sepsis_cc$CRP,
  levels    = c("No", nivel_evento),
  direction = "<"
)
corte_youden_crp <- coords(
  curva_crp, x = "best", best.method = "youden",
  ret = c("threshold", "sensitivity", "specificity"),
  transpose = FALSE
)
umbral_crp <- corte_youden_crp$threshold[1]
corte_youden_crp

Análisis: El punto de Youden de la proteína C reactiva se ubica en 130,5 mg/L, un umbral alto que resulta coherente con la naturaleza del marcador, pues la proteína C reactiva se eleva de forma inespecífica ante casi cualquier proceso inflamatorio y solo a partir de concentraciones elevadas comienza a orientar hacia una sepsis bacteriana.

En ese punto óptimo, la sensibilidad es de apenas 45,8 %, lo que significa que la prueba detecta a menos de la mitad de los pacientes que realmente tienen sepsis y deja escapar a los demás, una capacidad de detección insuficiente para descartar la enfermedad con un resultado bajo.

La especificidad, de 66,8 %, constituye su lado más favorable y expresa la probabilidad de que la prueba resulte negativa en un paciente que verdaderamente no tiene sepsis; es decir, de todos los individuos sanos, un 66,8 % obtiene una proteína C reactiva por debajo del umbral y queda correctamente clasificado como negativo, mientras que el 33,2 % restante supera el corte y produce un falso positivo.

Es una capacidad de descarte moderada, insuficiente para confiar en un resultado positivo como confirmación de la enfermedad.En conjunto, este resultado describe un marcador inclinado hacia la especificidad y débil en sensibilidad, un perfil opuesto al de la procalcitonina, cuyo punto de Youden ofrecía mayor sensibilidad que especificidad.

Ese contraste constituye la primera evidencia de que ambos biomarcadores capturan facetas distintas de la sepsis, lo que explica por qué en el estudio de referencia la combinación de marcadores rindió mejor que cualquiera de ellos por separado (Ljungström et al., 2017).

sepsis_cc <- sepsis_cc %>%
  mutate(
    CRP_bin = factor(
      dplyr::if_else(CRP >= umbral_crp, "Alta", "Baja"),
      levels = c("Alta", "Baja")
    )
  )
tabla_2x2_crp <- table(Prueba = sepsis_cc$CRP_bin, Sepsis = Sepsis_diag)
VP_c <- as.integer(tabla_2x2_crp["Alta", nivel_evento])
FP_c <- as.integer(tabla_2x2_crp["Alta", nivel_ref])
FN_c <- as.integer(tabla_2x2_crp["Baja", nivel_evento])
VN_c <- as.integer(tabla_2x2_crp["Baja", nivel_ref])
tabla_2x2_crp
##       Sepsis
## Prueba  Sí  No
##   Alta 301 284
##   Baja 356 573
matriz_display_crp <- data.frame(
  resultado = c(
    paste0("Prueba positiva: proteína C reactiva \u2265 ", round(umbral_crp,1), " mg/L"),
    paste0("Prueba negativa: proteína C reactiva < ", round(umbral_crp,1), " mg/L"),
    "Total"),
  presente = c(
    paste0("Verdaderos positivos<br><b>n = ", VP_c, "</b>"),
    paste0("Falsos negativos<br><b>n = ", FN_c, "</b>"),
    paste0("<b>n = ", VP_c + FN_c, "</b>")),
  ausente = c(
    paste0("Falsos positivos<br><b>n = ", FP_c, "</b>"),
    paste0("Verdaderos negativos<br><b>n = ", VN_c, "</b>"),
    paste0("<b>n = ", FP_c + VN_c, "</b>")),
  total = c(
    paste0("<b>n = ", VP_c + FP_c, "</b>"),
    paste0("<b>n = ", FN_c + VN_c, "</b>"),
    paste0("<b>n = ", VP_c + FP_c + FN_c + VN_c, "</b>")),
  stringsAsFactors = FALSE
)
matriz_display_crp %>%
  kableExtra::kbl(escape = FALSE, align = "lccc",
    col.names = c("Resultado de la prueba", "Sepsis presente", "Sepsis ausente", "Total"),
    caption = "Matriz 2\u00d72 de la prote\u00edna C reactiva en el punto de Youden. Criterio de referencia: Sepsis-2.") %>%
  kableExtra::kable_styling(bootstrap_options = c("hover","condensed"),
    full_width = FALSE, position = "center", font_size = 14) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::row_spec(1, background = "#FBEEF3") %>%
  kableExtra::row_spec(2, background = "#EAF6EC") %>%
  kableExtra::row_spec(3, bold = TRUE) %>%
  kableExtra::column_spec(1, bold = TRUE, width = "6cm")
Matriz 2×2 de la proteína C reactiva en el punto de Youden. Criterio de referencia: Sepsis-2.
Resultado de la prueba Sepsis presente Sepsis ausente Total
Prueba positiva: proteína C reactiva ≥ 130.5 mg/L Verdaderos positivos
n = 301
Falsos positivos
n = 284
n = 585
Prueba negativa: proteína C reactiva < 130.5 mg/L Falsos negativos
n = 356
Verdaderos negativos
n = 573
n = 929
Total n = 657 n = 857 n = 1514

Análisis: La matriz de la proteína C reactiva, construida en su punto de corte de 130,5 mg/L, revela un desempeño diagnóstico limitado que se hace visible al recorrer sus cuatro celdas.

Entre los 657 pacientes con sepsis, la prueba identifica correctamente a 301 (verdaderos positivos) pero deja pasar a 356 (falsos negativos), de modo que los enfermos no detectados superan a los detectados, un resultado que confirma la baja sensibilidad ya observada.

Entre los 857 pacientes sin sepsis, clasifica bien a 573 (verdaderos negativos) y se equivoca en 284 (falsos positivos), lo que refleja su especificidad moderada y explica por qué una proteína C reactiva elevada, por sí sola, no permite confirmar el diagnóstico.

El patrón de errores describe un marcador que falla más al detectar la enfermedad que al descartarla, es decir, comete más falsos negativos que falsos positivos, comportamiento inverso al de la procalcitonina, que en su punto óptimo dejaba escapar menos enfermos a cambio de más falsas alarmas.

Esta diferencia en la distribución de los errores tiene una raíz clara, pues la proteína C reactiva se eleva ante inflamaciones de origen muy diverso y sus valores se solapan ampliamente entre pacientes con y sin sepsis bacteriana, un solapamiento que el estudio de referencia también identificó al situarla entre los biomarcadores de menor capacidad discriminativa para este desenlace (Ljungström et al., 2017).

12.2 Exactitud diagnóstica y curva ROC de la proteína C reactiva

Sobre las cuatro celdas de la matriz calcularé la sensibilidad, la especificidad, los valores predictivos y las razones de verosimilitud, y considero estimar el área bajo la curva con su intervalo de confianza, con el fin de reunir en una sola tabla el rendimiento completo de la proteína C reactiva y disponerlo para el contraste posterior con la procalcitonina.

La curva ROC la presentaré además de forma gráfica, marcando sobre ella el punto de Youden, porque resume en una sola imagen el comportamiento del marcador en todos sus posibles puntos de corte.

Este conjunto de medidas es necesario porque el área bajo la curva me cuantifica la discriminación global del marcador con independencia del punto de corte, mientras que la sensibilidad, la especificidad y los valores predictivos describen su comportamiento en el umbral elegido, y las razones de verosimilitud traducen ese rendimiento al lenguaje de la decisión clínica.

Reunirlas permite juzgar a la proteína C reactiva no por un solo número, sino por el perfil completo que el estudio de referencia empleó para comparar biomarcadores entre sí (Ljungström et al., 2017).

Se_c  <- VP_c / (VP_c + FN_c)
Sp_c  <- VN_c / (VN_c + FP_c)
VPP_c <- VP_c / (VP_c + FP_c)
VPN_c <- VN_c / (VN_c + FN_c)
LRp_c <- Se_c / (1 - Sp_c)
LRn_c <- (1 - Se_c) / Sp_c
auc_crp <- pROC::auc(curva_crp)
ic_crp  <- pROC::ci.auc(curva_crp, method = "delong")

tibble::tibble(
  medida = c("Sensibilidad", "Especificidad", "Valor predictivo positivo",
             "Valor predictivo negativo", "Raz\u00f3n de verosimilitud positiva",
             "Raz\u00f3n de verosimilitud negativa", "\u00c1rea bajo la curva"),
  valor = c(scales::percent(Se_c, accuracy = 0.1), scales::percent(Sp_c, accuracy = 0.1),
            scales::percent(VPP_c, accuracy = 0.1), scales::percent(VPN_c, accuracy = 0.1),
            sprintf("%.3f", LRp_c), sprintf("%.3f", LRn_c),
            sprintf("%.3f (IC 95%%: %.3f\u2013%.3f)", as.numeric(auc_crp), ic_crp[1], ic_crp[3]))
) %>%
  kableExtra::kbl(align = "lc",
    col.names = c("Medida", "Valor"),
    caption = "Exactitud diagn\u00f3stica de la prote\u00edna C reactiva en el punto de Youden") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
    full_width = FALSE, position = "center", font_size = 14) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::column_spec(1, bold = TRUE)
Exactitud diagnóstica de la proteína C reactiva en el punto de Youden
Medida Valor
Sensibilidad 45.8%
Especificidad 66.9%
Valor predictivo positivo 51.5%
Valor predictivo negativo 61.7%
Razón de verosimilitud positiva 1.382
Razón de verosimilitud negativa 0.810
Área bajo la curva 0.573 (IC 95%: 0.544–0.603)

Análisis: Desde mi lectura, el rendimiento de la proteína C reactiva solo cobra sentido cuando lo sitúo en el problema real de la urgencia, que consiste en separar al paciente que tiene sepsis bacteriana de aquel cuya inflamación obedece a otra causa.

El área bajo la curva, de 0,573, es la medida que resume esa capacidad de separación, y representa la probabilidad de que, si tomo al azar un paciente con sepsis y otro sin ella, el enfermo tenga una concentración de proteína C reactiva más alta que el sano.

Un valor de 0,50 equivaldría a decidir lanzando una moneda, y uno de 1 a una separación perfecta; que la proteína C reactiva se quede en 0,573, tan cerca del azar, me indica que las concentraciones de enfermos y no enfermos se superponen casi por completo.

En el lenguaje de la clínica, esto quiere decir que un valor cualquiera de proteína C reactiva resulta casi tan probable en un paciente séptico como en uno con una inflamación no infecciosa, de modo que el marcador aporta poca información para distinguir entre ambos.

El intervalo de confianza del 95 %, que va de 0,544 a 0,603, refuerza esa conclusión, porque expresa el rango dentro del cual se encontraría el verdadero valor del área bajo la curva si el estudio se repitiera muchas veces en poblaciones semejantes.

En consecuencia, el extremo inferior del rango se mantiene por encima de 0,50, lo que permite descartar que la discriminación de la proteína C reactiva sea equivalente al puro azar.

Por otro, su extremo superior no supera 0,603 y queda muy lejos del valor de 0,80 que suele tomarse como el mínimo para hablar de una capacidad discriminativa clínicamente buena; como ni siquiera en el escenario más favorable de ese rango el marcador se aproximaría a ese estándar, interpreto que su rendimiento modesto no es un accidente del tamaño de la muestra, sino una característica estable del biomarcador en esta cohorte.

Interpreto que esa superposición de valores es la que explica una sensibilidad de apenas 45,8 % en el mejor punto de corte.

La sensibilidad es la probabilidad de que la prueba resulte positiva en un paciente que verdaderamente tiene sepsis, y que sea tan baja significa que, al confundirse las concentraciones de los enfermos con las de los sanos, cualquier umbral que se elija para no clasificar mal a los sanos termina dejando por debajo del corte a más de la mitad de los pacientes con sepsis, que quedan sin detectar.

La contrapartida es una especificidad del 66,9 %, es decir, la probabilidad de que la prueba resulte negativa en un paciente que realmente no tiene sepsis; es un desempeño algo mejor, aunque todavía insuficiente, y resulta coherente con el hecho de que la proteína C reactiva se eleva ante inflamaciones de origen muy diverso por su condición de reactante inespecífico, lo que hace que muchos pacientes sin infección bacteriana también alcancen valores altos.

En cuanto a las razones de verosimilitud, de 1,382 la positiva y 0,810 la negativa, me indica que la razón de verosimilitud positiva señala cuántas veces es más probable un resultado alto en un enfermo que en un sano, y la negativa cuántas veces lo es un resultado bajo; cuando ambas se acercan a 1, que es el valor de una prueba sin poder informativo, el resultado apenas modifica lo que ya se sabía del paciente.

Eso es justamente lo que ocurre aquí, pues ni un valor alto eleva de forma relevante la probabilidad de sepsis ni uno bajo la disminuye, de manera que un médico que dispusiera solo de este marcador terminaría con casi la misma incertidumbre con la que empezó.

Considero que este comportamiento confirma, desde la epidemiología clínica, lo que el estudio de referencia sostuvo con su cohorte completa, donde la proteína C reactiva quedó entre los biomarcadores de menor exactitud para la sepsis por criterios Sepsis-2, precisamente por el amplio solapamiento de sus concentraciones entre pacientes con y sin infección bacteriana.

Su valor, entonces, no reside en confirmar ni en descartar la sepsis por sí sola, sino en integrarse a una evaluación conjunta con otros marcadores y con el juicio clínico, que es la estrategia que el propio estudio propuso como la más rentable (Ljungström et al., 2017).

13 Comparación de las dos curvas ROC con la prueba de DeLong (paso 5)

Estimadas ya las áreas bajo la curva de la procalcitonina y de la proteína C reactiva, corresponde establecer si la diferencia observada entre ambas refleja una verdadera superioridad de un biomarcador sobre el otro o si podría surgir del azar propio del muestreo.

Para resolverlo aplicaré la prueba de DeLong, que compara las áreas bajo la curva de dos pruebas diagnósticas evaluadas en los mismos individuos y que, a diferencia de una comparación aislada, incorpora la correlación que existe entre ambas mediciones al haberse tomado sobre los mismos pacientes; ignorar esa correlación sobreestimaría el error y podría llevar a conclusiones equivocadas (DeLong et al., 1988).

La condición que la prueba exige se cumple, porque los dos marcadores se midieron sobre los mismos 1.514 episodios. Presentaré además ambas curvas superpuestas en una sola figura, ya que permite apreciar de forma directa cuál de las dos domina a la otra a lo largo de todo el rango de puntos de corte.

comparacion_delong <- pROC::roc.test(curva_pct, curva_crp, method = "delong")
comparacion_delong
## 
##  DeLong's test for two correlated ROC curves
## 
## data:  curva_pct and curva_crp
## Z = 4.1234, p-value = 3.733e-05
## alternative hypothesis: true difference in AUC is not equal to 0
## 95 percent confidence interval:
##  0.03537774 0.09947909
## sample estimates:
## AUC of roc1 AUC of roc2 
##   0.6408847   0.5734563

Análisis: La comparación directa de las dos áreas bajo la curva sitúa a la procalcitonina, con 0,641, por encima de la proteína C reactiva, con 0,573, una diferencia de 0,067 puntos a favor del primer marcador.

En esta comparación, cada área expresa la probabilidad de que, tomados al azar un paciente con sepsis y uno sin ella, el enfermo presente el valor más alto del biomarcador, de modo que un área mayor significa una mejor capacidad para ordenar correctamente a enfermos y sanos según su concentración.

Interpreto, por tanto, que la procalcitonina discrimina mejor la sepsis bacteriana que la proteína C reactiva, aunque ambas se mantienen en un terreno de rendimiento modesto, pues las dos quedan lejos del 0,80 que marcaría una discriminación clínicamente buena.

Considero que esta jerarquía entre los dos marcadores coincide con la que reportó el estudio de referencia sobre su cohorte completa, donde la procalcitonina superó a la proteína C reactiva en la discriminación de la sepsis por criterios Sepsis-2.

No obstante, la sola diferencia entre las áreas no basta para afirmar que esa superioridad sea estadísticamente sólida, ya que podría surgir del azar del muestreo; establecerlo con rigor me exige contrastar ambas áreas mediante la prueba de DeLong, cuyo resultado analizaré a continuación (Ljungström et al., 2017; DeLong et al., 1988).

df_pct <- data.frame(fpr = 1 - curva_pct$specificities,
                     sen = curva_pct$sensitivities, marcador = "Procalcitonina")
df_crp2 <- data.frame(fpr = 1 - curva_crp$specificities,
                      sen = curva_crp$sensitivities, marcador = "Prote\u00edna C reactiva")
df_ambas <- rbind(df_pct, df_crp2)
df_ambas <- df_ambas[order(df_ambas$marcador, df_ambas$fpr, df_ambas$sen), ]

etiqueta_comp <- sprintf("AUC procalcitonina = %.3f\nAUC prote\u00edna C reactiva = %.3f\nDeLong: p = %.4f",
                         as.numeric(pROC::auc(curva_pct)),
                         as.numeric(pROC::auc(curva_crp)),
                         comparacion_delong$p.value)

ggplot(df_ambas, aes(x = fpr, y = sen, color = marcador)) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "#9A9A9A") +
  geom_line(linewidth = 1.1) +
  annotate("label", x = 0.40, y = 0.14, label = etiqueta_comp,
           fill = "#FBFAFD", color = "#2d2440", size = 3.8, label.size = 0, hjust = 0) +
  scale_color_manual(values = c("Procalcitonina" = "#B5477E",
                                "Prote\u00edna C reactiva" = "#3d6fa3")) +
  scale_x_continuous("1 \u2212 Especificidad", labels = scales::percent, limits = c(0,1), expand = c(0,0)) +
  scale_y_continuous("Sensibilidad", labels = scales::percent, limits = c(0,1), expand = c(0,0)) +
  labs(title = "Curvas ROC comparadas para la sepsis bacteriana",
       subtitle = "Criterio de referencia: Sepsis-2  |  n = 1.514 episodios",
       color = NULL) +
  coord_equal() + theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold"),
        legend.position = "bottom", panel.grid.minor = element_blank())

::: {.analisis} Análisis: La figura representa las curvas ROC de los dos biomarcadores sobre un mismo plano. El eje horizontal corresponde a 1 menos la especificidad, es decir, la proporción de falsos positivos, y crece de izquierda a derecha desde 0 % hasta 100 %.

El eje vertical corresponde a la sensibilidad, esto es, la proporción de verdaderos positivos, y crece de abajo hacia arriba en el mismo rango. Cada punto de una curva refleja el rendimiento del marcador en un umbral distinto, de manera que el conjunto de la línea recorre todos los posibles puntos de corte a la vez.

La línea de color vino tinto corresponde a la procalcitonina y la de color azul a la proteína C reactiva, mientras que la diagonal discontinua gris representa la referencia del azar, aquella que obtendría una prueba sin ninguna capacidad para distinguir enfermos de sanos.

En cuanto a la distribución de las curvas, observo que ambas nacen en el vértice inferior izquierdo y terminan en el superior derecho, pero la de la procalcitonina discurre en todo momento por encima de la de la proteína C reactiva y se despega más de la diagonal, lo que indica que para un mismo nivel de falsos positivos alcanza una sensibilidad mayor.

La separación entre las dos líneas no es uniforme, pues se estrecha en los extremos y se ensancha en la zona central del gráfico, entre el 25 % y el 60 % del eje horizontal, que es el tramo donde suelen ubicarse los puntos de corte de uso clínico.

La curva azul de la proteína C reactiva, en cambio, se mantiene próxima a la diagonal en varios tramos, sobre todo en la mitad derecha, señal de una discriminación débil.

El recuadro incorporado al gráfico resume los tres datos que sustentan esta lectura:

  • un área bajo la curva de 0,641 para la procalcitonina, de 0,573 para la proteína C reactiva y un valor p de la prueba de DeLong menor que 0,0001.

Considero que la imagen confirma, de forma visual, la jerarquía que las cifras ya habían establecido, pues la curva superior pertenece al marcador con mayor área y esa superioridad resulta estadísticamente significativa; no obstante, la cercanía de ambas curvas a la diagonal y su distancia respecto de la esquina superior izquierda, que correspondería a una prueba perfecta, muestran que se trata de dos marcadores de rendimiento modesto, uno consistentemente mejor que el otro.

Esta observación gráfica coincide con lo que el estudio de referencia concluyó sobre su cohorte completa, donde la procalcitonina superó a la proteína C reactiva sin que ninguno alcanzara por sí solo una exactitud suficiente para la sepsis bacteriana (Ljungström et al., 2017).

14 Rendimiento en cortes clínicos y comparación con el punto de Youden (pasos 7, 8 y 9)

Los biomarcadores se dicotomizarán ahora en los cortes clínicos que establece el taller, la procalcitonina en 0,5 ng/mL y la proteína C reactiva en 20 mg/L, para calcular en cada uno la sensibilidad, la especificidad, los valores predictivos y el odds ratio diagnóstico.

El propósito es evaluar el desempeño de cada prueba en el umbral que se emplea en la práctica clínica, y no solo en el punto estadísticamente óptimo, porque un mismo biomarcador se comporta de manera muy distinta según dónde se sitúe el corte.

Los resultados se reunirán en una sola tabla que enfrenta, para cada marcador, su corte clínico y su punto de Youden, siguiendo el modelo de la Tabla 2 del artículo de referencia.

Esta disposición me permite observar de forma directa el intercambio entre sensibilidad y especificidad que introduce cada umbral, y aporta la evidencia numérica sobre la que se argumentará, en el paso final, cuál marcador y cuál corte me resultaran preferibles para confirmar la sepsis bacteriana.

El odds ratio diagnóstico se incluye porque condensa en un único valor la separación global que la prueba logra entre enfermos y sanos, con independencia de la prevalencia. :::

metricas_corte <- function(valor, corte, desenlace, evento) {
  prueba <- factor(ifelse(valor >= corte, "Positiva", "Negativa"),
                   levels = c("Positiva", "Negativa"))
  real   <- factor(ifelse(desenlace == evento, "Enfermo", "Sano"),
                   levels = c("Enfermo", "Sano"))
  tabla <- table(prueba, real)
  VP <- tabla["Positiva","Enfermo"]; FP <- tabla["Positiva","Sano"]
  FN <- tabla["Negativa","Enfermo"]; VN <- tabla["Negativa","Sano"]
  Se  <- VP/(VP+FN); Sp <- VN/(VN+FP)
  VPP <- VP/(VP+FP); VPN <- VN/(VN+FN)
  DOR <- (VP*VN)/(FP*FN)
  c(Se = Se, Sp = Sp, VPP = VPP, VPN = VPN, DOR = DOR)
}

pct <- sepsis_cc$Procalcitonin
crp <- sepsis_cc$CRP
des <- sepsis_cc$Sepsis2

esc <- rbind(
  metricas_corte(pct, 0.5,   des, nivel_evento),
  metricas_corte(pct, 0.175, des, nivel_evento),
  metricas_corte(crp, 20,    des, nivel_evento),
  metricas_corte(crp, 130.5, des, nivel_evento)
)

etiquetas <- c(
  "Procalcitonina \u2265 0,5 ng/mL (corte cl\u00ednico)",
  "Procalcitonina \u2265 0,175 ng/mL (Youden)",
  "Prote\u00edna C reactiva \u2265 20 mg/L (corte cl\u00ednico)",
  "Prote\u00edna C reactiva \u2265 130,5 mg/L (Youden)"
)

tabla_cortes <- data.frame(
  Escenario = etiquetas,
  Sensibilidad = scales::percent(esc[,"Se"], accuracy = 0.1),
  Especificidad = scales::percent(esc[,"Sp"], accuracy = 0.1),
  VPP = scales::percent(esc[,"VPP"], accuracy = 0.1),
  VPN = scales::percent(esc[,"VPN"], accuracy = 0.1),
  OR_diagnostico = sprintf("%.2f", esc[,"DOR"]),
  row.names = NULL
)

tabla_cortes %>%
  kableExtra::kbl(align = "lccccc",
    col.names = c("Escenario", "Sensibilidad", "Especificidad",
                  "Valor predictivo positivo", "Valor predictivo negativo",
                  "OR diagn\u00f3stico"),
    caption = "Rendimiento diagn\u00f3stico de la procalcitonina y la prote\u00edna C reactiva en el corte cl\u00ednico y en el punto de Youden, seg\u00fan el modelo de la Tabla 2 del art\u00edculo de referencia") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
    full_width = FALSE, position = "center", font_size = 13) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::row_spec(1:2, background = "#FBEEF3") %>%
  kableExtra::row_spec(3:4, background = "#EAF6EC") %>%
  kableExtra::column_spec(1, bold = TRUE, width = "6cm")
Rendimiento diagnóstico de la procalcitonina y la proteína C reactiva en el corte clínico y en el punto de Youden, según el modelo de la Tabla 2 del artículo de referencia
Escenario Sensibilidad Especificidad Valor predictivo positivo Valor predictivo negativo OR diagnóstico
Procalcitonina ≥ 0,5 ng/mL (corte clínico) 44.3% 73.7% 56.4% 63.3% 2.23
Procalcitonina ≥ 0,175 ng/mL (Youden) 65.3% 56.1% 53.3% 67.8% 2.41
Proteína C reactiva ≥ 20 mg/L (corte clínico) 88.1% 14.5% 44.1% 61.4% 1.26
Proteína C reactiva ≥ 130,5 mg/L (Youden) 45.8% 66.9% 51.5% 61.7% 1.71

Análisis: Esta tabla es, a mi juicio, la que mejor revela la naturaleza de cada biomarcador, porque muestra cómo una misma prueba se transforma por completo según el umbral en que se la fije, y permite enfrentar los dos marcadores en condiciones equivalentes.

En la procalcitonina, el paso del corte clínico de 0,5 ng/mL al punto de Youden de 0,175 ng/mL me ilustra el intercambio fundamental entre sensibilidad y especificidad.

En 0,5, la sensibilidad es del 44,3 %, lo que significa que la prueba detecta a menos de la mitad de los pacientes que verdaderamente tienen sepsis, mientras que su especificidad del 73,7 % indica que descarta correctamente a casi tres de cada cuatro pacientes sin la enfermedad; es un corte, por tanto, más orientado a confirmar que a descartar.

**Al bajar el umbral a 0,175, la sensibilidad asciende al 65,3 % porque un corte más permisivo captura a más enfermos, pero ese mismo valor, hace caer la especificidad al 56,1 %, ya que también deja pasar a más sanos como positivos.

El odds ratio diagnóstico, que resume en un solo número la separación global entre enfermos y sanos, apenas se mueve de 2,23 a 2,41, lo que me indica que, más allá de dónde ponga el corte, la capacidad de fondo de la procalcitonina para distinguir ambos grupos permanece modesta y estable.

En la proteína C reactiva, el contraste entre sus dos cortes es aún más llamativo, porque en el corte de 20 mg/L que fija el taller, la sensibilidad se dispara al 88,1 %, un valor que a primera vista parecería excelente, pero que al leerlo junto a su especificidad del 14,5 %, es tan baja que la prueba clasifica como positivos a la inmensa mayoría de los pacientes, tengan o no sepsis.

Interpreto que ese 88,1 % no refleja una verdadera capacidad de detección, sino que el umbral de 20 mg/L es tan bajo para la proteína C reactiva que casi todos lo superan, de modo que la prueba acierta en los enfermos simplemente porque marca positivo a casi todo el mundo.

El odds ratio diagnóstico de 1,26, apenas por encima de 1, confirma que en ese corte la prueba prácticamente no discrimina.

En su punto de Youden de 130,5 mg/L el equilibrio mejora, con una sensibilidad del 45,8 % y una especificidad del 66,9 %, y el odds ratio sube a 1,71, aunque sigue siendo el más bajo de los cuatro escenarios.

La lectura crítica que extraigo del conjunto es que los valores predictivos, que se mantienen en todos los escenarios en una banda estrecha entre el 44 % y el 68 %, revelan la consecuencia clínica de fondo, es decir, sea cual sea el marcador o el corte, ni un resultado positivo confirma la sepsis con solidez ni uno negativo la descarta con tranquilidad, porque todos los valores predictivos quedan próximos a la prevalencia del 43,4 % de la cohorte y se alejan poco de ella.

La comparación de los odds ratio diagnósticos ordena a los cuatro escenarios y sitúa a la procalcitonina en su punto de Youden (2,41) como el de mayor capacidad discriminativa, y a la proteína C reactiva en el corte de 20 mg/L (1,26) como el de menor, casi indistinguible del azar.

Considero que este panorama reproduce con fidelidad lo que el estudio de referencia mostró en su Tabla 2, donde la proteína C reactiva a cortes bajos alcanzaba sensibilidades altas a costa de especificidades ínfimas, y confirma que ninguno de estos biomarcadores, en ningún corte, reúne por sí solo la exactitud necesaria para decidir sobre la sepsis, y su utilidad real aparece cuando se integran entre sí y con el juicio clínico (Ljungström et al., 2017).

Para complementar la tabla, los cuatro escenarios se representan en un gráfico de barras agrupadas que enfrenta, en cada uno, la sensibilidad contra la especificidad.

La finalidad es hacer visible el intercambio entre ambas medidas que la tabla expresa en cifras, ya que al disponerlas lado a lado se aprecia de manera inmediata cómo el aumento de una se acompaña del descenso de la otra según el marcador y el corte elegidos.

datos_barras <- data.frame(
  escenario = rep(etiquetas, times = 2),
  medida = rep(c("Sensibilidad", "Especificidad"), each = 4),
  valor = c(esc[,"Se"], esc[,"Sp"])
)
datos_barras$escenario <- factor(datos_barras$escenario, levels = etiquetas)

ggplot(datos_barras, aes(x = escenario, y = valor, fill = medida)) +
  geom_col(position = position_dodge(width = 0.7), width = 0.62, alpha = 0.9) +
  geom_text(aes(label = scales::percent(valor, accuracy = 0.1)),
            position = position_dodge(width = 0.7), vjust = -0.4, size = 3.4) +
  scale_fill_manual(values = c("Sensibilidad" = "#B5477E", "Especificidad" = "#3d6fa3")) +
  scale_y_continuous(labels = scales::percent, limits = c(0, 1), expand = c(0, 0)) +
  scale_x_discrete(labels = function(x) stringr::str_wrap(x, width = 22)) +
  labs(title = "Sensibilidad frente a especificidad en cada corte diagn\u00f3stico",
       subtitle = "Procalcitonina y prote\u00edna C reactiva en su corte cl\u00ednico y en el punto de Youden",
       x = NULL, y = NULL, fill = NULL) +
  theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold"),
        legend.position = "bottom",
        panel.grid.major.x = element_blank(),
        axis.text.x = element_text(size = 9))

Análisis: Este gráfico traduce a imagen el intercambio que gobierna toda prueba diagnóstica basada en un umbral, y al disponer los cuatro escenarios uno junto a otro me permite leer de golpe lo que la tabla mostraba en cifras.

En el eje vertical se representa el valor de cada medida en porcentaje, de 0 a 100 %, mientras que cada par de barras corresponde a un corte, con la especificidad en azul y la sensibilidad en color vino.

Observo que cuando una barra sube la otra baja, porque considero que ese balancín es la manifestación visual del principio de que ganar sensibilidad casi siempre cuesta especificidad, y viceversa.

En la procalcitonina el balancín es nítido pero moderado, pues en su corte clínico de 0,5 ng/mL la barra azul de especificidad se eleva al 73,7 % mientras la rosa de sensibilidad se queda en 44,3 %, de modo que es un umbral pensado para no alarmar a los sanos, aunque a costa de perder enfermos.

Al pasar al punto de Youden de 0,175 ng/mL las dos barras casi se igualan, con la sensibilidad subiendo al 65,3 % y la especificidad cediendo hasta el 56,1 %, lo que refleja el propósito mismo del índice de Youden, que busca el punto donde ambas medidas quedan lo más equilibradas posible.

En la proteína C reactiva el gráfico expone que en el corte de 20 mg/L que fija el taller, la barra de sensibilidad se dispara al 88,1 %, la más alta de la figura, pero a su lado la barra de especificidad se desploma hasta un 14,5 % apenas perceptible, y ese contraste extremo delata que la aparente excelencia de la sensibilidad es engañosa, pues un umbral tan bajo clasifica como positivos a casi todos los pacientes y acierta en los enfermos solo porque marca positivo indiscriminadamente.

En su punto de Youden de 130,5 mg/L las barras se reequilibran, con especificidad del 66,9 % y sensibilidad del 45,8 %, un reparto más razonable pero de rendimiento discreto.

La lectura crítica que me deja la figura es que ninguna de las ocho barras se aproxima a la esquina ideal en que sensibilidad y especificidad fueran ambas altas al mismo tiempo, que es lo que caracterizaría a una prueba verdaderamente buena.

En todos los escenarios, elevar una medida hunde la otra, y ese comportamiento confirma visualmente lo que las áreas bajo la curva ya habían anticipado, esto es, que se trata de biomarcadores de capacidad discriminativa limitada.

Considero que esta imagen sostiene, desde la evidencia gráfica, la advertencia central del estudio de referencia sobre la interpretación de la proteína C reactiva a cortes bajos, donde sensibilidades altas conviven con especificidades ínfimas, y refuerza que la decisión clínica no puede apoyarse en un único marcador ni en un corte aislado, sino en la lectura conjunta de varios (Ljungström et al., 2017).

15 Elección del marcador y el punto de corte para confirmar la sepsis (paso 10)

La pregunta final del taller me exige decidir con qué biomarcador y en qué punto de corte convendría quedarme para confirmar la sepsis bacteriana, y la respuesta debe partir de una distinción conceptual, pues confirmar una enfermedad no es lo mismo que descartarla.

Confirmar exige una prueba cuyo resultado positivo deje pocas dudas, lo que reclama una especificidad y un valor predictivo positivo elevados, mientras que descartar reclamaría lo contrario, una sensibilidad alta que no deje escapar enfermos.

De la comparación global se desprende que la procalcitonina discrimina la sepsis mejor que la proteína C reactiva, con un área bajo la curva de 0,641 frente a 0,573, y la prueba de DeLong confirmó que esa diferencia es estadísticamente significativa y no atribuible al azar.

Este primer hallazgo ya inclina mi elección hacia la procalcitonina como el marcador de referencia entre los dos, aunque conviene recuerdo que ambos permanecen en un rango de capacidad discriminativa modesta y que ninguno alcanza, por sí solo, la exactitud de una prueba plenamente confiable.

Al descender al punto de corte, la tabla comparativa ofrece la clave para responder desde el objetivo de confirmar, pues el corte de la proteína C reactiva en 20 mg/L, pese a su llamativa sensibilidad del 88,1 %, resulta inservible para confirmar, porque su especificidad del 14,5 % implica que clasifica como positivos a casi todos los pacientes y su odds ratio diagnóstico de 1,26 lo sitúa al borde del azar.

Entre los cortes restantes, la procalcitonina en su umbral clínico de 0,5 ng/mL es la que ofrece la mayor especificidad, un 73,7 %, junto al valor predictivo positivo más alto de la tabla, un 56,4 %, lo que la convierte en la opción más coherente cuando el propósito es que un resultado positivo respalde el diagnóstico.

  • Por todo lo anterior, si hubiera de elegir un único marcador y un único corte para confirmar la sepsis bacteriana, me quedaría con la procalcitonina en el punto de corte de 0,5 ng/mL, por combinar la superioridad discriminativa global del marcador con la mayor especificidad y el mejor valor predictivo positivo entre las opciones evaluadas.

No obstante, esta elección debe entenderse dentro de los límites que el propio análisis ha puesto en evidencia, pues ni siquiera este corte alcanza cifras que permitan sustentar el diagnóstico en la prueba de manera aislada.

La conclusión coincide con la del estudio de referencia, que tras evaluar estos biomarcadores propuso que su mayor rendimiento se obtiene al combinarlos entre sí y con el juicio clínico, de modo que la procalcitonina en 0,5 ng/mL debe considerarse la mejor herramienta individual disponible para confirmar, pero siempre integrada en una valoración más amplia del paciente (Ljungström et al., 2017).

16 Parte 2. Modelos predictivos mediante regresión logística (Semana 7)

Hasta aquí evalué cada biomarcador de forma aislada mediante curvas ROC. Ahora doy un paso distinto, pues la sepsis rara vez se reconoce por un único dato, sino por la conjunción de varios signos y resultados que el clínico integra.

La regresión logística me permite formalizar esa integración, ya que combina varias variables en un solo modelo y estima el aporte propio de cada una a la probabilidad de sepsis, ajustando el efecto de una por el de las demás. En esta parte construiré modelos de complejidad creciente y los compararé en su capacidad de discriminación, ajuste y calibración.

Antes de construir ningún modelo debo fijar la base de datos sobre la que trabajarán todos ellos. Como los modelos que voy a comparar emplean conjuntos distintos de variables, y la comparación de sus áreas bajo la curva solo es válida si se ajustan sobre los mismos pacientes, defino una base común de casos completos que incluya todas las variables candidatas, por lo cual, generaré esa base y examinaré cuántos episodios sobreviven a esa exigencia.

vars_modelos <- c("Sepsis2", "Age", "Gender", "SBP",
                  "Procalcitonin", "P_lactate", "CRP",
                  "FR", "SatO2", "HR", "Bodytemperature",
                  "Haemoglobin", "Leukocyteconcentration", "NL_ratio")

sepsis_mod <- sepsis_cc %>%
  dplyr::filter(dplyr::if_all(dplyr::all_of(vars_modelos), ~ !is.na(.)))

n_antes <- nrow(sepsis_cc)
n_despues <- nrow(sepsis_mod)
n_perdidos <- n_antes - n_despues

data.frame(
  Concepto = c("Base de la Parte 1 (casos completos en 3 variables)",
               "Base de la Parte 2 (casos completos en 13 variables)",
               "Episodios excluidos"),
  n = c(n_antes, n_despues, n_perdidos)
) %>%
  kableExtra::kbl(align = "lc",
    col.names = c("Base anal\u00edtica", "N\u00famero de episodios"),
    caption = "Reducci\u00f3n de la muestra al exigir casos completos en las variables de los modelos") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
    full_width = FALSE, position = "center", font_size = 13) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::row_spec(3, background = "#FBEEF3", bold = TRUE) %>%
  kableExtra::column_spec(1, bold = TRUE)
Reducción de la muestra al exigir casos completos en las variables de los modelos
Base analítica Número de episodios
Base de la Parte 1 (casos completos en 3 variables) 1514
Base de la Parte 2 (casos completos en 13 variables) 1194
Episodios excluidos 320
faltantes_por_variable <- sepsis_cc %>%
  dplyr::summarise(dplyr::across(dplyr::all_of(vars_modelos[-1]),
                                 ~ sum(is.na(.)))) %>%
  tidyr::pivot_longer(everything(), names_to = "variable", values_to = "faltantes") %>%
  dplyr::arrange(dplyr::desc(faltantes)) %>%
  dplyr::filter(faltantes > 0)

faltantes_por_variable$variable <- dplyr::coalesce(
  etiquetas_vars[faltantes_por_variable$variable],
  faltantes_por_variable$variable
)

faltantes_por_variable %>%
  kableExtra::kbl(align = "lc",
    col.names = c("Variable", "Datos faltantes"),
    caption = "Datos faltantes por variable en la base de la Parte 1, ordenados de mayor a menor") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
    full_width = FALSE, position = "center", font_size = 13) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::column_spec(1, bold = TRUE)
Datos faltantes por variable en la base de la Parte 1, ordenados de mayor a menor
Variable Datos faltantes
Frecuencia respiratoria 150
Temperatura corporal 121
Saturación de oxígeno 74
Frecuencia cardíaca 71
Tensión sistólica 69
Lactato 42
Índice neutrófilos-linfocitos 26
Hemoglobina 10
Leucocitos 7

Análisis: Al construir la base común para los modelos me encuentro con una reducción de la muestra que no había anticipado y que requiero entender antes de seguir. La primera tabla muestra que, mientras la Parte 1 trabajaba con 1.514 episodios, la Parte 2 se queda con 1.194, de modo que 320 episodios, algo más de la quinta parte de la cohorte, quedan fuera al exigir casos completos.

La razón de esta pérdida no es una decisión arbitraria de descartar pacientes, sino una consecuencia directa del cambio de escenario, pues la Parte 1 solo necesitaba tres variables completas, el desenlace y los dos biomarcadores, mientras que estos modelos incorporan trece, y basta que un paciente tenga ausente cualquiera de ellas para quedar excluido del análisis de casos completos.

La segunda tabla me permite identificar de dónde proviene esa pérdida, y su lectura ordenada de mayor a menor resulta esclarecedora. Los faltantes no se reparten por igual, sino que se concentran en los signos vitales, encabezados por la frecuencia respiratoria con 150 ausencias, seguida de la temperatura corporal con 121, la saturación de oxígeno con 74, la frecuencia cardíaca con 71 y la tensión sistólica con 69.

Que sean precisamente los signos vitales los que más se pierden tiene sentido en el contexto de un servicio de urgencias, donde en la premura de la atención inicial no siempre se registra cada constante de forma completa, a diferencia de los parámetros de laboratorio como la hemoglobina o los leucocitos, que al proceder de una muestra de sangre procesada quedan casi siempre documentados y apenas aportan 10 y 7 faltantes respectivamente.

  • Considero por un lado, que la pérdida me obliga a asumir que los modelos se construyen sobre una submuestra, y aquí surge una cuestión metodológica de fondo, pues el análisis de casos completos solo produce resultados no sesgados si los datos faltan de manera aleatoria y no por un motivo relacionado con la propia sepsis, pues si los signos vitales faltaran con más frecuencia en los pacientes más graves, por ejemplo, la exclusión podría distorsionar las estimaciones.

  • Por otro lado, elijo de forma deliberada esta estrategia de casos completos, y no la imputación de los valores ausentes, porque garantiza que los cinco modelos se ajusten y se comparen sobre exactamente los mismos 1.194 pacientes, condición sin la cual la comparación de sus áreas bajo la curva mediante la prueba de DeLong no sería legítima.

Asumo, por tanto, la pérdida de esos 320 episodios como el precio de una comparación limpia, dejando planteada para el final de esta sección la comprobación de si ese precio altera o no las conclusiones.

Para apreciar mejor cómo se distribuyen los datos faltantes que motivaron la reducción de la muestra, represento las variables en un gráfico de barras horizontales ordenado de mayor a menor número de ausencias, y distingo con color los signos vitales de los parámetros de laboratorio.

La finalidad es hacer visible de un solo vistazo dónde se concentra la pérdida de información, algo que la tabla enumera pero que el color y la longitud de las barras me comunican de forma inmediata.

faltantes_grafico <- faltantes_por_variable %>%
  dplyr::mutate(
    tipo = dplyr::if_else(
      variable %in% c("Frecuencia respiratoria", "Temperatura corporal",
                      "Saturaci\u00f3n de ox\u00edgeno", "Frecuencia card\u00edaca",
                      "Tensi\u00f3n sist\u00f3lica"),
      "Signo vital", "Par\u00e1metro de laboratorio"),
    variable = factor(variable, levels = rev(variable))
  )

ggplot(faltantes_grafico, aes(x = variable, y = faltantes, fill = tipo)) +
  geom_col(width = 0.7, alpha = 0.9) +
  geom_text(aes(label = faltantes), hjust = -0.35, size = 3.6, color = "#2d2440") +
  scale_fill_manual(values = c("Signo vital" = "#B5477E",
                               "Par\u00e1metro de laboratorio" = "#3d6fa3")) +
  scale_y_continuous(limits = c(0, max(faltantes_grafico$faltantes) * 1.1),
                     expand = c(0, 0)) +
  coord_flip() +
  labs(title = "Datos faltantes por variable en la cohorte",
       subtitle = "Variables candidatas de los modelos, ordenadas de mayor a menor ausencia",
       x = NULL, y = "N\u00famero de datos faltantes", fill = NULL) +
  theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold"),
        legend.position = "bottom",
        panel.grid.major.y = element_blank())

Análisis: Esta figura es un diagrama de barras horizontales que ordena las variables candidatas según su número de datos faltantes y las distingue por color según su naturaleza, en color vino los signos vitales y en azul los parámetros de laboratorio.

La elegí porque convierte en una imagen lo que la tabla solo enumeraba, y sobre todo porque el color revela un patrón que en la tabla pasaba desapercibido: los faltantes no se distribuyen de forma errática entre las variables, sino que se agrupan por el tipo de dato.

Lo que la imagen deja ver de inmediato es que las cinco barras más largas, las de color vino, corresponden todas a signos vitales y se concentran en la parte superior, mientras que las barras azules de los parámetros de laboratorio se ubican todas en la mitad inferior, con longitudes muy cortas. Esa separación tan limpia entre los dos bloques de color es el verdadero hallazgo del gráfico, pues no solo hay más ausencias en los signos vitales, sino que la ausencia parece obedecer a la forma en que se recoge cada tipo de dato.

El bloque vino, encabezado por la frecuencia respiratoria y la temperatura, refleja mediciones que dependen del registro manual junto al paciente; el bloque azul, con la hemoglobina y los leucocitos casi sin faltantes, refleja resultados que llegan de forma sistemática desde el laboratorio una vez procesada la muestra de sangre.

Este patrón tiene una consecuencia que considero relevante para la validez de los modelos, porque si los datos faltaran de manera puramente aleatoria las ausencias se repartirían sin relación con el tipo de variable, y no es lo que observo.

Que se concentren en los signos vitales sugiere un mecanismo de pérdida ligado al proceso de atención y no al azar, lo que refuerza la necesidad de comprobar más adelante, considero mediante el análisis de sensibilidad con imputación, si trabajar solo con los casos completos introdujo o no algún sesgo en las estimaciones.

A continuación, El propósito será determinar qué combinación de variables predice mejor la sepsis y, sobre todo, comprobar si añadir los biomarcadores que estudié en la primera parte mejora de forma apreciable la predicción respecto de la información clínica elemental.**

16.1 Modelo 1. Predicción con variables clínicas básicas

Comienzo por el modelo más sencillo, que predice la sepsis a partir de tres variables disponibles en el primer contacto con el paciente en urgencias: la edad, el sexo y la tensión arterial sistólica.

Elijo este punto de partida porque son datos que se obtienen de inmediato, sin esperar ningún resultado de laboratorio, y porque me sirven de referencia para medir, más adelante, cuánto añade la incorporación de los biomarcadores.

La regresión logística estima para cada variable un odds ratio, que interpreto como el factor por el cual cambian las probabilidades de sepsis ante un aumento unitario de esa variable cuando las demás se mantienen constantes; un odds ratio mayor que 1 señala que la variable aumenta las probabilidades del evento, uno menor que 1 que las reduce, y su intervalo de confianza me dice si ese efecto es estadísticamente distinguible de la ausencia de efecto.

modelo1 <- glm(Sepsis2 ~ Age + Gender + SBP,
               data = sepsis_mod, family = binomial(link = "logit"))

modelo2 <- glm(Sepsis2 ~ Age + Gender + SBP + Procalcitonin,
               data = sepsis_mod, family = binomial(link = "logit"))

broom::tidy(modelo1, exponentiate = TRUE, conf.int = TRUE) %>%
  dplyr::transmute(
    Variable = term,
    OR = sprintf("%.3f", estimate),
    IC95 = sprintf("%.3f \u2013 %.3f", conf.low, conf.high),
    valor_p = format.pval(p.value, digits = 3, eps = 0.001)
  ) %>%
  kableExtra::kbl(align = "lccc",
    col.names = c("Variable", "OR", "IC 95%", "Valor p"),
    caption = "Modelo 1: regresi\u00f3n log\u00edstica de la sepsis seg\u00fan edad, sexo y tensi\u00f3n arterial sist\u00f3lica") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
    full_width = FALSE, position = "center", font_size = 13) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::column_spec(1, bold = TRUE)
Modelo 1: regresión logística de la sepsis según edad, sexo y tensión arterial sistólica
Variable OR IC 95% Valor p
(Intercept) 1.137 0.556 – 2.325 0.72520
Age 1.008 1.001 – 1.014 0.02181
GenderMale 0.912 0.724 – 1.148 0.43181
SBP 0.994 0.990 – 0.999 0.00924

Análisis: Este primer modelo me muestra qué aporta cada variable clínica básica a la predicción de la sepsis, y su lectura exige interpretar los odds ratios con cuidado, porque en variables continuas un valor cercano a 1 puede parecer irrelevante sin serlo.

La edad presenta un odds ratio de 1,008 y un valor p de 0,0218, por debajo de 0,05, de modo que su asociación con la sepsis es estadísticamente significativa. El odds ratio, mayor que 1, indica que la probabilidad de sepsis aumenta con la edad, un hallazgo coherente con la clínica, pues el paciente mayor tiene una respuesta inmunitaria más debilitada y una mayor vulnerabilidad a las infecciones graves.

El sexo masculino presenta un odds ratio de 0,912 y un valor p de 0,4318, muy por encima de 0,05, y su intervalo de confianza cruza el 1, lo que significa que su efecto es indistinguible de la ausencia de efecto. Se puede interrpretar, que el sexo no aporta capacidad predictiva en este modelo, ya que los datos son compatibles tanto con un ligero aumento como con una ligera reducción del riesgo.

La tensión arterial sistólica presenta un odds ratio de 0,994 y un valor p de 0,0092, el más bajo de los tres, claramente significativo. Al ser el odds ratio menor que 1, la tensión se comporta como un marcador predictivo inverso, es decir, a mayor tensión menor probabilidad de sepsis.

Me parece que se debe precisar la naturaleza de esta asociación, pues no significa que una tensión alta proteja frente a la infección, sino que en esta cohorte de pacientes con sospecha de sepsis la hipotensión acompaña con más frecuencia a quienes efectivamente la padecen.

La dirección es la que la fisiopatología predice, ya que la tensión baja no es causa sino consecuencia de la sepsis grave, en la que refleja la hipoperfusión y la respuesta inflamatoria sistémica.

En conjunto, la edad y la tensión se asocian de forma significativa con la sepsis y en la dirección esperable, mientras que el sexo no aporta capacidad predictiva.

Considero que son asociaciones de magnitud débil, lo que anticipa que un modelo basado solo en estos datos clínicos tendrá una discriminación limitada y justifica el paso siguiente, el de incorporar los biomarcadores para comprobar si mejoran la predicción.

16.2 Base común y modelo 2. Incorporación de la procalcitonina

Antes de ampliar los modelos fijo una base única de casos completos en todas las variables que intervendrán en cualquiera de ellos, porque comparar la capacidad predictiva de modelos distintos solo es válido cuando todos se ajustan sobre los mismos pacientes; si cada modelo empleara un número distinto de observaciones, sus áreas bajo la curva no serían comparables y la prueba estadística del punto 5 perdería sentido.

Sobre esa base construyo el modelo 2, que añade la procalcitonina a las tres variables clínicas del modelo 1. Escojo la procalcitonina entre los dos biomarcadores porque en la primera parte demostró la mayor capacidad de discriminación, de modo que este modelo pone a prueba si el mejor marcador individual, sumado a la información clínica básica, mejora de forma apreciable la predicción de la sepsis.

tabla_or <- function(modelo, etiqueta) {
  res <- broom::tidy(modelo, exponentiate = TRUE, conf.int = TRUE) %>%
    dplyr::filter(term != "(Intercept)")
  res$term <- dplyr::coalesce(etiquetas_vars[res$term], res$term)
  res %>%
    dplyr::transmute(
      Variable = term,
      !!paste0("OR (", etiqueta, ")") := sprintf("%.3f", estimate),
      !!paste0("p (", etiqueta, ")")  := format.pval(p.value, digits = 2, eps = 0.001)
    )
}

dplyr::full_join(
  tabla_or(modelo1, "Modelo 1"),
  tabla_or(modelo2, "Modelo 2"),
  by = "Variable"
) %>%
  kableExtra::kbl(align = "lcccc",
    caption = "Comparaci\u00f3n de los modelos 1 y 2: efecto de incorporar la procalcitonina") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
    full_width = FALSE, position = "center", font_size = 13) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::column_spec(1, bold = TRUE) %>%
  kableExtra::add_header_above(c(" " = 1, "Solo cl\u00ednicas" = 2, "Con procalcitonina" = 2))
Comparación de los modelos 1 y 2: efecto de incorporar la procalcitonina
Solo clínicas
Con procalcitonina
Variable OR (Modelo 1) p (Modelo 1) OR (Modelo 2) p (Modelo 2)
Edad 1.008 0.0218 1.006 0.084
Sexo masculino 0.912 0.4318 0.912 0.435
Tensión sistólica 0.994 0.0092 0.997 0.140
Procalcitonina NA NA 1.042 <0.001

Análisis: Esta tabla enfrenta los dos modelos y deja ver, en una sola imagen, cómo se reordena el peso de cada variable cuando la procalcitonina entra en la ecuación. La leo recorriendo qué le ocurre a cada predictor al pasar de la columna del modelo 1 a la del modelo 2.

La procalcitonina se incorpora como el único predictor fuertemente significativo, con un odds ratio de 1,042 y un valor p menor que 0,001, mientras que en el modelo 1 ni siquiera figuraba. Ese coeficiente por unidad debe interpretarse con prudencia, porque la distribución de la procalcitonina es muy asimétrica y un aumento de un ng/mL no representa lo mismo a lo largo de toda su escala; lo que sí puedo afirmar con solidez es que su asociación con la sepsis es robusta y no atribuible al azar.

El hallazgo más revelador está en lo que les sucede a las variables clínicas al añadir el biomarcador. La edad pasa de un valor p de 0,022 en el modelo 1 a 0,084 en el modelo 2, y la tensión sistólica de 0,009 a 0,140, de modo que ambas dejan de ser significativas en presencia de la procalcitonina; el sexo, por su parte, se mantiene sin significancia en los dos modelos, con valores p de 0,432 y 0,435.

Desde la epidemiología clínica no interpreto esta pérdida de significancia como que la edad y la tensión hayan dejado de importar, sino como un fenómeno de confusión, en el que su asociación con la sepsis estaba mediada en buena parte por la respuesta inflamatoria que la procalcitonina mide de forma directa.

El paciente mayor o hipotenso tiende a presentar la procalcitonina elevada precisamente porque cursa una infección más grave, de manera que al incluir el biomarcador, más próximo al mecanismo de la enfermedad, este absorbe el efecto que antes se repartía entre los marcadores clínicos más distales.

En conjunto, considero que el modelo 2 concentra su capacidad predictiva en un único predictor biológico de efecto marcado, en lugar de repartirla entre asociaciones clínicas débiles.

No obstante, que la procalcitonina sea un coeficiente significativo no garantiza que el modelo discrimine mejor entre pacientes con y sin sepsis, pues mejorar el ajuste interno del modelo no equivale necesariamente a clasificar mejor a los pacientes; esa comprobación solo podré hacerla al comparar las áreas bajo la curva de todos los modelos y contrastarlas estadísticamente en los pasos siguientes.

16.3 Modelo 3. Incorporación del lactato

Para el tercer modelo agrego al modelo clínico básico el lactato, en lugar de un segundo reactante inflamatorio, porque busco aportar una dimensión fisiológica distinta a la que ya cubre la procalcitonina.

Mientras los biomarcadores inflamatorios reflejan la respuesta del organismo a la infección, el lactato mide sus consecuencias sobre la perfusión de los tejidos, pues se acumula cuando el aporte de oxígeno a las células se vuelve insuficiente y el metabolismo se torna anaerobio.

En la sepsis grave esa hipoperfusión es un signo de deterioro, tanto que la hiperlactatemia forma parte de los criterios que definen las formas más severas de la enfermedad. Elijo el lactato, por tanto, con la hipótesis de que capta una faceta de la sepsis que las variables demográficas y hemodinámicas no alcanzan a representar, y evalúo si su incorporación aporta capacidad predictiva propia al modelo.

modelo3 <- glm(Sepsis2 ~ Age + Gender + SBP + P_lactate,
               data = sepsis_mod, family = binomial(link = "logit"))

tabla_or(modelo3, "Modelo 3") %>%
  kableExtra::kbl(align = "lcc",
    caption = "Modelo 3: sepsis seg\u00fan edad, sexo, tensi\u00f3n arterial sist\u00f3lica y lactato") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
    full_width = FALSE, position = "center", font_size = 13) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::column_spec(1, bold = TRUE)
Modelo 3: sepsis según edad, sexo, tensión arterial sistólica y lactato
Variable OR (Modelo 3) p (Modelo 3)
Edad 1.006 0.0931
Sexo masculino 0.888 0.3150
Tensión sistólica 0.995 0.0431
Lactato 1.162 0.0048

Análisis: El lactato entra al modelo con un odds ratio de 1,162 y un valor p de 0,0048. El valor p, por debajo de 0,05, indica que su asociación con la sepsis es estadísticamente significativa; el odds ratio de 1,162, mayor que 1, significa que por cada mmol/L de aumento del lactato las probabilidades de sepsis aumentan, en la dirección que corresponde a un marcador de hipoperfusión en la sepsis grave.

Su rango clínico estrecho hace que este odds ratio por unidad sea directamente interpretable, a diferencia del de la procalcitonina. Por otro lado, la tensión sistólica se mantiene significativa, con un valor p de 0,0431 y un odds ratio de 0,995, menor que 1, lo que indica que a mayor tensión menor probabilidad de sepsis.

Este es el punto que distingue a este modelo del anterior, pues cuando incorporé la procalcitonina, la tensión perdía significancia y su valor p subía a 0,140; con el lactato, ahora la tensión conserva su significancia con un valor p de 0,0431.

La razón es fisiopatológica, pues la procalcitonina y la hipotensión comparten el eje de la respuesta inflamatoria y una absorbía a la otra, mientras que el lactato y la tensión miden dimensiones distintas de la gravedad, la metabólica y la hemodinámica, y por eso aportan información independiente y coexisten en el modelo.

La edad presenta un odds ratio de 1,006 y un valor p de 0,0931, por encima de 0,05, de modo que no alcanza significancia en este modelo; su odds ratio, mayor que 1, apunta a un efecto en la dirección esperable, pero el valor p no permite afirmarlo con certeza estadística.

El sexo masculino, con un odds ratio de 0,888 y un valor p de 0,3150, no aporta capacidad predictiva, como en los modelos anteriores. En conjunto, el modelo 3 combina dos predictores significativos y complementarios de gravedad, el lactato y la tensión, en lugar de concentrar el peso en un único biomarcador.

Esta coexistencia indica que el lactato añade información que las variables clínicas no contienen. La significancia de los coeficientes, sin embargo, no implica que este modelo clasifique mejor a los pacientes, algo que estableceré al comparar las áreas bajo la curva de todos los modelos en los pasos siguientes.

16.4 Modelo 4. Selección de variables mediante estrategia hacia atrás

Los tres modelos anteriores nacieron de una elección guiada por el criterio clínico. Para el cuarto invierto la lógica y dejo que sean los datos los que propongan la mejor combinación de predictores, partiendo de un modelo que reúne todas las variables candidatas relevantes y aplicando una selección hacia atrás.

Recurro a esta estrategia porque me permite descubrir combinaciones que la intuición clínica podría pasar por alto, y porque me ofrece un modelo de referencia, construido con un criterio estadístico explícito, contra el cual contrastar los tres modelos teóricos.

El procedimiento se apoya en el criterio de información de Akaike, conocido por sus siglas en inglés como AIC, el cual resuelve un dilema central en la construcción de modelos, pues si se añaden variables sin límite el modelo se ajusta cada vez mejor a los datos con los que se construyó, pero se vuelve tan a la medida de esa muestra que pierde capacidad de funcionar en pacientes nuevos, un problema conocido como sobreajuste.

El criterio de Akaike pone esos dos elementos en una balanza, ya que premia que el modelo explique bien los datos y a la vez penaliza cada variable adicional, de modo que una variable solo compensa entrar si su aporte supera el castigo por complicar el modelo.

Su valor no significa nada por sí solo, pero permite comparar modelos entre sí, y entre dos el preferible es el de menor Akaike, el que mejor equilibra ajuste y simplicidad. La selección hacia atrás parte, por tanto, del modelo completo y retira las variables una a una, quedándose con la combinación que alcanza el menor valor de este criterio.

modelo_completo <- glm(
  Sepsis2 ~ Age + Gender + SBP + Procalcitonin + P_lactate + CRP +
            FR + SatO2 + HR + Bodytemperature + Haemoglobin +
            Leukocyteconcentration + NL_ratio,
  data = sepsis_mod, family = binomial(link = "logit"))

modelo4 <- MASS::stepAIC(modelo_completo, direction = "backward", trace = FALSE)

solo_or <- function(modelo, etiqueta) {
  res <- broom::tidy(modelo, exponentiate = TRUE) %>%
    dplyr::filter(term != "(Intercept)")
  res$term <- dplyr::coalesce(etiquetas_vars[res$term], res$term)
  res %>%
    dplyr::transmute(Variable = res$term,
                     !!etiqueta := sprintf("%.3f", estimate))
}

lista_modelos <- list(
  solo_or(modelo1, "Modelo 1"),
  solo_or(modelo2, "Modelo 2"),
  solo_or(modelo3, "Modelo 3"),
  solo_or(modelo4, "Modelo 4")
)

tabla_maestra <- Reduce(function(a, b) dplyr::full_join(a, b, by = "Variable"), lista_modelos)
tabla_maestra[is.na(tabla_maestra)] <- "\u2013"

tabla_maestra %>%
  kableExtra::kbl(align = "lcccc",
    caption = "S\u00edntesis comparativa de los odds ratios de los cuatro modelos. El gui\u00f3n indica que la variable no se incluy\u00f3 en ese modelo.") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
    full_width = FALSE, position = "center", font_size = 13) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::column_spec(1, bold = TRUE) %>%
  kableExtra::row_spec(c(1, 3), background = "#F3EEF9")
Síntesis comparativa de los odds ratios de los cuatro modelos. El guión indica que la variable no se incluyó en ese modelo.
Variable Modelo 1 Modelo 2 Modelo 3 Modelo 4
Edad 1.008 1.006 1.006 1.008
Sexo masculino 0.912 0.912 0.888
Tensión sistólica 0.994 0.997 0.995 0.996
Procalcitonina 1.042 1.023
Lactato 1.162
Proteína C reactiva 1.003
Frecuencia respiratoria 1.049
Saturación de oxígeno 1.022
Frecuencia cardíaca 1.018
Temperatura corporal 1.447
Leucocitos 1.020
Índice neutrófilos-linfocitos 1.025

Análisis: Esta tabla condensa la construcción de toda la sección y me permite leer el comportamiento de cada variable a lo largo de los cuatro modelos, por lo que encuentro tres patrones que considero reveladores:

  • La edad y la tensión sistólica son las únicas variables presentes en los cuatro modelos, y sus odds ratios se mantienen notablemente estables, la edad entre 1,006 y 1,008 y la tensión entre 0,994 y 0,997, sin importar qué otras variables las acompañen.

  • Esa estabilidad indica que su asociación con la sepsis es estructural y no depende del contexto del modelo, aunque, como vi en los análisis individuales, se trate de asociaciones de magnitud débil.

  • El sexo masculino aparece en los tres modelos teóricos con odds ratios cercanos a 1 y termina descartado por la selección automática, lo que confirma a lo largo de toda la tabla su falta de valor predictivo.

  • Los biomarcadores muestran un comportamiento más dependiente del modelo. La procalcitonina presenta un odds ratio de 1,042 cuando entra sola junto a las clínicas en el modelo 2, pero desciende a 1,023 en el modelo 4, donde compite con la proteína C reactiva y con otros marcadores inflamatorios que absorben parte de su peso.

  • El lactato, con un odds ratio de 1,162, es exclusivo del modelo 3 y no fue retenido por la selección automática, lo que sugiere que, aunque significativo por sí mismo, su información se solapa con la de otras variables cuando se dispone del conjunto completo.

  • La columna del modelo 4 concentra la mayor riqueza, pues reúne diez variables entre las que destacan, por su magnitud, la temperatura corporal con un odds ratio de 1,447 y la frecuencia respiratoria con 1,049.

  • Que la selección automática haya elegido predominantemente signos vitales y marcadores inflamatorios, y haya prescindido del sexo, el lactato y la hemoglobina, dibuja un retrato de la sepsis coherente con su fisiopatología, en el que la respuesta sistémica del organismo, medida a través de la temperatura, la frecuencia respiratoria, la frecuencia cardíaca y los reactantes inflamatorios, concentra la capacidad de predecir la enfermedad.

16.5 Curvas ROC de los modelos y comparación de su discriminación (puntos 4 y 5)

Construidos los cuatro modelos, corresponde ahora medir lo único que importa desde el punto de vista predictivo, y es cuán bien distingue cada uno a los pacientes con sepsis de los que no la tienen.

Para ello calculo la probabilidad de sepsis que cada modelo asigna a cada paciente y, a partir de ella, levanto su curva ROC y su área bajo la curva. Añado además el modelo 0, que no es un modelo múltiple sino la procalcitonina evaluada en solitario tal como la analicé en la primera parte, porque me sirve de línea de base para responder una pregunta central:

  • ¿construir modelos con varias variables mejora de verdad la discriminación que ya ofrecía el mejor biomarcador por sí solo?

Por lo cual, voy a superponer las cinco curvas en un mismo gráfico, pues disponerlas juntas me permitirá apreciar de inmediato cuáles se acercan más a la esquina de la clasificación perfecta y cuánto se separan entre sí.

modelo0 <- roc(response = sepsis_mod$Sepsis2, predictor = sepsis_mod$Procalcitonin,
               levels = c("No", nivel_evento), direction = "<")

roc_m1 <- roc(sepsis_mod$Sepsis2, predict(modelo1, type = "response"),
              levels = c("No", nivel_evento), direction = "<")
roc_m2 <- roc(sepsis_mod$Sepsis2, predict(modelo2, type = "response"),
              levels = c("No", nivel_evento), direction = "<")
roc_m3 <- roc(sepsis_mod$Sepsis2, predict(modelo3, type = "response"),
              levels = c("No", nivel_evento), direction = "<")
roc_m4 <- roc(sepsis_mod$Sepsis2, predict(modelo4, type = "response"),
              levels = c("No", nivel_evento), direction = "<")

lista_roc <- list(modelo0, roc_m1, roc_m2, roc_m3, roc_m4)
nombres_modelos <- c(
  "Modelo 0 (procalcitonina sola)",
  "Modelo 1 (cl\u00ednicas)",
  "Modelo 2 (+ procalcitonina)",
  "Modelo 3 (+ lactato)",
  "Modelo 4 (selecci\u00f3n autom\u00e1tica)"
)
names(lista_roc) <- nombres_modelos

tabla_auc_modelos <- purrr::imap_dfr(lista_roc, function(r, nombre) {
  ic <- pROC::ci.auc(r, method = "delong")
  data.frame(Modelo = nombre,
             AUC = sprintf("%.3f", as.numeric(pROC::auc(r))),
             IC95 = sprintf("%.3f \u2013 %.3f", ic[1], ic[3]))
})

tabla_auc_modelos %>%
  kableExtra::kbl(align = "lcc",
    col.names = c("Modelo", "\u00c1rea bajo la curva", "IC 95%"),
    caption = "\u00c1rea bajo la curva de los cinco modelos para la predicci\u00f3n de la sepsis") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
    full_width = FALSE, position = "center", font_size = 13) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::column_spec(1, bold = TRUE)
Área bajo la curva de los cinco modelos para la predicción de la sepsis
Modelo Área bajo la curva IC 95%
Modelo 0 (procalcitonina sola) 0.648 0.617 – 0.679
Modelo 1 (clínicas) 0.555 0.522 – 0.588
Modelo 2 (+ procalcitonina) 0.600 0.568 – 0.633
Modelo 3 (+ lactato) 0.576 0.543 – 0.609
Modelo 4 (selección automática) 0.751 0.724 – 0.778

Análisis: Esta tabla responde la pregunta que ha guiado toda mi sección, la de si construir modelos más complejos mejora realmente la capacidad de distinguir a los pacientes con sepsis, y su lectura ordenada de mayor a menor área bajo la curva deja dos hallazgos que considero centrales.

El modelo 4 se impone con claridad, con un área bajo la curva de 0,751 y un intervalo de confianza de 0,724 a 0,778 que no se solapa con el de ningún otro modelo, lo que indica que su superioridad es estadísticamente sólida y no fruto del azar.

Es el único de los cinco que supera el umbral de 0,70 y alcanza una discriminación que puede calificarse de aceptable, mientras que todos los demás se quedan por debajo.

Interpreto que la combinación amplia de signos vitales y marcadores inflamatorios que la selección automática reunió sí logra capturar el fenómeno de la sepsis mejor que cualquier modelo más reducido, de modo que aquí la complejidad añadida sí rindió un beneficio tangible.

  • El segundo hallazgo es el modelo 0, que no es más que la procalcitonina evaluada en solitario, pues alcanza un área de 0,648, y con ello supera no solo al modelo clínico básico, cuyo 0,555 con intervalo de 0,522 a 0,588 queda claramente por debajo sin solaparse, sino también a los modelos 2 y 3, que obtienen 0,600 y 0,576 pese a incluir a la propia procalcitonina junto a las variables clínicas.

Este resultado enseña que añadir predictores débiles a un buen biomarcador no mejora la discriminación, sino que puede diluirla, ya que las variables clínicas de escaso valor, al entrar en el modelo, introducen ruido que resta capacidad al conjunto en lugar de sumarla.

La procalcitonina sola discrimina mejor que la procalcitonina acompañada de la edad, el sexo y la tensión.En conjunto, la tabla confirma que la relación entre complejidad y discriminación no es lineal, pues un modelo con pocas variables mal elegidas puede rendir peor que un único biomarcador potente, mientras que un modelo amplio con variables bien seleccionadas por un criterio estadístico riguroso sí consigue una mejora real.

Considero que este contraste sostiene la enseñanza de fondo del trabajo, en la que lo determinante no es cuántas variables se incluyan, sino cuáles, y confirma desde la predicción multivariada lo que el estudio de referencia planteó sobre el valor de combinar marcadores de forma juiciosa y no indiscriminada (Ljungström et al., 2017).

df_roc_modelos <- purrr::imap_dfr(lista_roc, function(r, nombre) {
  data.frame(fpr = 1 - r$specificities, sen = r$sensitivities, modelo = nombre)
})
df_roc_modelos <- df_roc_modelos[order(df_roc_modelos$modelo, df_roc_modelos$fpr), ]

colores_modelos <- c(
  "Modelo 0 (procalcitonina sola)" = "#B5477E",
  "Modelo 1 (cl\u00ednicas)" = "#9A9A9A",
  "Modelo 2 (+ procalcitonina)" = "#3d6fa3",
  "Modelo 3 (+ lactato)" = "#4E9E6E",
  "Modelo 4 (selecci\u00f3n autom\u00e1tica)" = "#5b3b6b"
)

ggplot(df_roc_modelos, aes(x = fpr, y = sen, color = modelo)) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "#BBBBBB") +
  geom_line(linewidth = 1) +
  scale_color_manual(values = colores_modelos) +
  scale_x_continuous("1 \u2212 Especificidad", labels = scales::percent, limits = c(0,1), expand = c(0,0)) +
  scale_y_continuous("Sensibilidad", labels = scales::percent, limits = c(0,1), expand = c(0,0)) +
  labs(title = "Curvas ROC de los cinco modelos predictivos de sepsis",
       subtitle = "Criterio de referencia: Sepsis-2  |  base com\u00fan de casos completos",
       color = NULL) +
  coord_equal() + theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold"),
        legend.position = "bottom", panel.grid.minor = element_blank()) +
  guides(color = guide_legend(nrow = 2))

::: {.analisis} Análisis: Esta figura reúne las curvas ROC de los cinco modelos en un mismo plano, y me parece la imagen que mejor resume todo lo que he venido construyendo.

  • En el eje horizontal está uno menos (1-E) la especificidad, es decir, la proporción de falsos positivos que cada modelo genera, y en el eje vertical la sensibilidad, la proporción de enfermos que detecta.

  • La línea gris punteada de la diagonal marca lo que haría un modelo inútil, uno que acertara al azar, por cuanto más se aleja una curva de esa diagonal y se acerca a la esquina superior izquierda, mejor discrimina, así que voy leyendo cada color según su distancia a ese rincón ideal.

  • La curva morada oscura, que corresponde al modelo 4, es la que más me llama la atención, porque se despega netamente de todas las demás y sube casi vertical desde el origen antes de curvarse hacia la esquina superior. En la zona central del gráfico, cuando acepto alrededor de un 25 % de falsos positivos, esta curva ya alcanza cerca del 75 % de sensibilidad, mientras las otras apenas rondan la mitad.

Ese despliegue por encima del resto es la traducción visual de su área de 0,751, la más alta, y me confirma que el modelo con selección automática de variables es el que mejor separa a los enfermos de los sanos.

  • La curva de color vino rojizo, que es el modelo 0, la procalcitonina sola, ocupa el segundo lugar y me sorprende que lo haga, porque transcurre por encima de las curvas azul y verde durante casi todo su recorrido.

Ver que un único biomarcador se dibuje más cerca de la esquina ideal que modelos que incluyen varias variables es la imagen exacta del hallazgo que ya había notado en la tabla, esto es, que sumar predictores débiles no acercó las curvas al ideal sino que las dejó más pegadas a la diagonal.

  • Las curvas azul y verde, los modelos 2 y 3, van muy juntas y entrelazadas en la mitad inferior del gráfico, apenas separadas de la diagonal punteada. Que discurran casi superpuestas me dice que añadir la procalcitonina o el lactato al modelo clínico produjo mejoras parecidas y modestas, sin que ninguna de las dos lograra despegar la curva de forma apreciable.

  • Por debajo de todas, casi rozando la diagonal, corre la curva gris del modelo 1, el de las variables clínicas solas, cuya cercanía a la línea del azar refleja su área de 0,555, la más baja, y confirma que la edad, el sexo y la tensión por sí solos apenas distinguen mejor que el puro azar.

Cuando miro el conjunto, entiendo que la figura cuenta una historia clara sobre la distancia a la diagonal, pues las curvas se ordenan de arriba abajo igual que sus áreas, con el modelo 4 arriba, la procalcitonina sola en un segundo plano inesperado, los dos modelos mixtos entrelazados en el medio y el modelo clínico pegado al azar.

Lo que ahora comprendo de esta imagen es que la altura de una curva ROC no depende de cuántas variables tenga el modelo, sino de la calidad de la información que esas variables aportan, y que solo el modelo 4, con su combinación de signos vitales y marcadores inflamatorios bien seleccionados, consiguió elevarse hasta una zona de discriminación aceptable (Ljungström et al., 2017). :::

16.6 Comparación estadística de las áreas bajo la curva (punto 5)

La figura anterior mostró que las curvas se separan de forma desigual, con el modelo 4 destacado sobre el resto, pero la impresión visual necesita el respaldo de una prueba que determine si esas diferencias son estadísticamente reales o podrían atribuirse al azar del muestreo.

Para ello aplicaré la prueba de DeLong, que compara las áreas bajo la curva de modelos evaluados sobre los mismos pacientes teniendo en cuenta la correlación entre sus predicciones.

Contrasto el modelo 4, que obtuvo la mayor área, frente a cada uno de los demás, porque lo que me interesa establecer es si su superioridad aparente se sostiene con rigor estadístico y, en particular, si supera de forma significativa tanto al modelo clínico básico como a la procalcitonina evaluada en solitario.

comparaciones <- list(
  "Modelo 4 vs Modelo 0 (procalcitonina sola)" = pROC::roc.test(roc_m4, modelo0, method = "delong"),
  "Modelo 4 vs Modelo 1 (cl\u00ednicas)"          = pROC::roc.test(roc_m4, roc_m1, method = "delong"),
  "Modelo 4 vs Modelo 2 (+ procalcitonina)"    = pROC::roc.test(roc_m4, roc_m2, method = "delong"),
  "Modelo 4 vs Modelo 3 (+ lactato)"           = pROC::roc.test(roc_m4, roc_m3, method = "delong")
)

purrr::imap_dfr(comparaciones, function(t, nombre) {
  data.frame(Comparacion = nombre,
             valor_p = format.pval(t$p.value, digits = 3, eps = 0.001))
}) %>%
  kableExtra::kbl(align = "lc",
    col.names = c("Comparaci\u00f3n", "Valor p (prueba de DeLong)"),
    caption = "Comparaci\u00f3n estad\u00edstica del \u00e1rea bajo la curva del modelo 4 frente a los dem\u00e1s modelos") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
    full_width = FALSE, position = "center", font_size = 13) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::column_spec(1, bold = TRUE)
Comparación estadística del área bajo la curva del modelo 4 frente a los demás modelos
Comparación Valor p (prueba de DeLong)
Modelo 4 vs Modelo 0 (procalcitonina sola) <0.001
Modelo 4 vs Modelo 1 (clínicas) <0.001
Modelo 4 vs Modelo 2 (+ procalcitonina) <0.001
Modelo 4 vs Modelo 3 (+ lactato) <0.001

Análisis: La prueba de DeLong me confirma con contundencia lo que la figura y la tabla de áreas venían anticipando. En las cuatro comparaciones, el modelo 4 supera a sus rivales con un valor p menor que 0,001, muy por debajo del umbral de 0,05, de modo que rechazo en todos los casos la hipótesis de que las áreas sean iguales y concluyo que su superioridad no es una impresión del gráfico ni un capricho de esta muestra, sino una diferencia estadísticamente sólida.

El modelo construido mediante selección hacia atrás discriminó la sepsis mejor que el modelo clínico básico, mejor que los dos modelos que sumaban un biomarcador a las variables clínicas, y mejor incluso que la procalcitonina evaluada en solitario.

De estas cuatro comparaciones, la que enfrenta al modelo 4 con el modelo 0 es la que considero más iluminadora, porque contrapone el modelo más elaborado con el más sencillo de todos, la procalcitonina sola.

Que la diferencia entre ambos sea significativa demuestra que la ganancia del modelo 4 no proviene de haber incluido la procalcitonina, que por sí misma ya alcanzaba un área de 0,648, sino de haberla combinado con los signos vitales y los demás marcadores inflamatorios que la selección automática retuvo.

Es la confirmación estadística de una idea que ha recorrido toda esta parte, la de que el valor de un modelo predictivo no reside en la cantidad de variables sino en la sinergia de las que de verdad aportan.

Debo matizar, sin embargo, la lectura de estos valores p tan pequeños. La base sobre la que se ajustaron los modelos consta de 1.194 pacientes, aquellos con datos completos en las trece variables candidatas, y con un tamaño de esa magnitud la prueba dispone de una potencia elevada para detectar diferencias.

Por eso la significancia estadística confirma la dirección y la firmeza de la ventaja del modelo 4, pero no equivale por sí sola a una superioridad clínica rotunda.

Lo que sostiene el valor real de ese modelo no es únicamente el valor p, sino que su área de 0,751 es la única que alcanza una zona de discriminación aceptable, un argumento de magnitud que se suma al de significancia.

La combinación de ambos, un efecto significativo y a la vez de tamaño apreciable, es la que me permite afirmar que el modelo 4 es, de forma consistente, el de mejor capacidad predictiva entre los evaluados.

17 Sección 2. Ajuste, calibración y precisión de los modelos

17.1 Comparación de dos modelos en discriminación, ajuste y calibración (punto 6)

Para esta comparación escojo los dos modelos que representan los extremos del trabajo, el modelo 1, el más sencillo, construido solo con variables clínicas básicas, y el modelo 4, el más completo, surgido de la selección automática.

Los voy a enfrentar porque de esta manera puedo contrastar el de menor y el de mayor capacidad, lo que me permitirá ver con nitidez qué se gana al pasar de uno a otro, y porque un buen modelo predictivo debe evaluarse en más de una dimensión.

Voy a distinguir las tres propiedades que voy a examinar, porque miden cosas diferentes y un modelo puede cumplir una y fallar en otra.

  • La discriminación, que se mide con el área bajo la curva, indica si el modelo ordena correctamente a los pacientes según su riesgo, colocando a los enfermos por encima de los sanos.

  • La calibración indica si las probabilidades que el modelo predice se corresponden con las frecuencias realmente observadas, es decir, si de un grupo al que el modelo asigna un riesgo del 30 % efectivamente alrededor del 30 % desarrolla la sepsis.

  • y el ajuste global, que se resume con el criterio de información de Akaike,que equilibra lo bien que el modelo explica los datos con su complejidad.

Para valorar la calibración emplearé la prueba de Hosmer y Lemeshow, que es contraintuitiva, ya que a diferencia de la mayoría de las pruebas, aquí un valor de probabilidad alto es la señal favorable, pues me indicaría que no existe una discrepancia significativa entre lo que el modelo predice y lo que realmente ocurre. :::

hl_modelo1 <- ResourceSelection::hoslem.test(modelo1$y, fitted(modelo1), g = 10)
hl_modelo4 <- ResourceSelection::hoslem.test(modelo4$y, fitted(modelo4), g = 10)

ic_m1 <- pROC::ci.auc(roc_m1, method = "delong")
ic_m4 <- pROC::ci.auc(roc_m4, method = "delong")

data.frame(
  Medida = c("N\u00famero de variables predictoras",
             "\u00c1rea bajo la curva (discriminaci\u00f3n)",
             "IC 95% del \u00e1rea bajo la curva",
             "Criterio de Akaike (ajuste)",
             "Hosmer-Lemeshow, valor p (calibraci\u00f3n)"),
  `Modelo 1` = c(
    as.character(length(coef(modelo1)) - 1),
    sprintf("%.3f", as.numeric(pROC::auc(roc_m1))),
    sprintf("%.3f \u2013 %.3f", ic_m1[1], ic_m1[3]),
    sprintf("%.1f", AIC(modelo1)),
    sprintf("%.3f", hl_modelo1$p.value)),
  `Modelo 4` = c(
    as.character(length(coef(modelo4)) - 1),
    sprintf("%.3f", as.numeric(pROC::auc(roc_m4))),
    sprintf("%.3f \u2013 %.3f", ic_m4[1], ic_m4[3]),
    sprintf("%.1f", AIC(modelo4)),
    sprintf("%.3f", hl_modelo4$p.value)),
  check.names = FALSE
) %>%
  kableExtra::kbl(align = "lcc",
    caption = "Comparaci\u00f3n del modelo 1 y el modelo 4 en discriminaci\u00f3n, ajuste y calibraci\u00f3n") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
    full_width = FALSE, position = "center", font_size = 13) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::column_spec(1, bold = TRUE)
Comparación del modelo 1 y el modelo 4 en discriminación, ajuste y calibración
Medida Modelo 1 Modelo 4
Número de variables predictoras 3 10
Área bajo la curva (discriminación) 0.555 0.751
IC 95% del área bajo la curva 0.522 – 0.588 0.724 – 0.778
Criterio de Akaike (ajuste) 1638.3 1430.9
Hosmer-Lemeshow, valor p (calibración) 0.798 0.001

Análisis: Esta tabla enfrenta al modelo más sencillo y al más completo en las tres propiedades que definen la calidad de un modelo predictivo, y el resultado es más interesante de lo que esperaba, porque ninguno de los dos gana en todo, sino que cada uno sobresale en cosas distintas.

  • En discriminación la superioridad del modelo 4 es clara, pues su área bajo la curva de 0,751 se sitúa en una zona aceptable mientras que la del modelo 1, de 0,555, apenas se despega del azar, y sus intervalos de confianza ni siquiera se rozan, lo que confirma que la diferencia es sólida.

  • En el ajuste global ocurre lo mismo, ya que el criterio de Akaike del modelo 4 es de 1.430,9 frente a 1.638,3 del modelo 1, y como en este criterio el valor menor indica el mejor equilibrio entre explicación y complejidad, el modelo 4 resulta preferible pese a incluir diez variables en lugar de tres.

Esto tiene una lectura que considero relevante, pues significa que las siete variables adicionales del modelo 4 no son un exceso que penalice el ajuste, sino que aportan información suficiente para compensar con creces la mayor complejidad.

La sorpresa aparece en la calibración, donde los papeles se invierten, pues el modelo 1 obtiene un valor de probabilidad de Hosmer y Lemeshow de 0,798, muy por encima de 0,05, lo que en esta prueba de interpretación contraintuitiva significa que está bien calibrado, es decir, que las probabilidades que predice se corresponden con las frecuencias de sepsis realmente observadas.

El modelo 4, en cambio, obtiene un valor de 0,001, por debajo de 0,05, lo que revela una discrepancia significativa entre lo que predice y lo que ocurre, y por tanto una calibración deficiente. Encuentro aquí una paradoja aparente, pues el modelo que mejor discrimina es el que peor calibra.

La explicación de esa paradoja está en que discriminación y calibración miden cosas distintas y no tienen por qué ir de la mano; por lo cual, la discriminación evalúa si el modelo ordena bien a los pacientes, colocando a los enfermos por encima de los sanos en la escala de riesgo, mientras que la calibración evalúa si el valor absoluto de las probabilidades predichas es correcto.

Un modelo puede acertar en el orden de los pacientes y equivocarse en la magnitud de las probabilidades que les asigna, que es justo lo que le ocurre al modelo 4, capaz de distinguir bien quién tiene más riesgo pero con probabilidades que se desvían de la realidad observada.

Considero, que el modelo 1, al ser tan simple, predice probabilidades poco dispersas y cercanas a la prevalencia general, lo que le facilita parecer bien calibrado aunque discrimine mal, mientras que el modelo 4, al arriesgar predicciones más extremas para lograr su mejor discriminación, se expone a una mayor descalibración.

Esta distinción es la que justifica el punto siguiente, donde consideraré qué podría hacerse para mejorar la calibración del modelo más discriminante sin sacrificar su capacidad de ordenar a los pacientes.

Para hacer visible la diferencia de calibración que la prueba de Hosmer y Lemeshow expresó en un solo número, represento el gráfico de calibración de ambos modelos:

calibracion_modelo <- function(modelo, etiqueta, g = 10) {
  datos <- data.frame(predicho = fitted(modelo),
                      observado = modelo$y)
  datos$grupo <- dplyr::ntile(datos$predicho, g)
  datos %>%
    dplyr::group_by(grupo) %>%
    dplyr::summarise(prob_predicha = mean(predicho),
                     prob_observada = mean(observado),
                     .groups = "drop") %>%
    dplyr::mutate(modelo = etiqueta)
}

cal_ambos <- dplyr::bind_rows(
  calibracion_modelo(modelo1, "Modelo 1 (cl\u00ednicas)"),
  calibracion_modelo(modelo4, "Modelo 4 (selecci\u00f3n autom\u00e1tica)")
)

ggplot(cal_ambos, aes(x = prob_predicha, y = prob_observada, color = modelo)) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "#9A9A9A") +
  geom_line(linewidth = 0.8) +
  geom_point(size = 2.8) +
  scale_color_manual(values = c("Modelo 1 (cl\u00ednicas)" = "#9A9A9A",
                                "Modelo 4 (selecci\u00f3n autom\u00e1tica)" = "#5b3b6b")) +
  scale_x_continuous("Probabilidad predicha de sepsis", labels = scales::percent,
                     limits = c(0, 1), expand = c(0, 0)) +
  scale_y_continuous("Proporci\u00f3n observada de sepsis", labels = scales::percent,
                     limits = c(0, 1), expand = c(0, 0)) +
  labs(title = "Gr\u00e1fico de calibraci\u00f3n de los modelos 1 y 4",
       subtitle = "La diagonal representa la calibraci\u00f3n perfecta entre lo predicho y lo observado",
       color = NULL) +
  coord_equal() + theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold"),
        legend.position = "bottom", panel.grid.minor = element_blank())

::: {.analisis} Análisis: Esta figura enfrenta, en el eje horizontal, la probabilidad de sepsis que cada modelo predice, y en el vertical, la proporción de pacientes que realmente resultó tener sepsis dentro de cada grupo de riesgo.

La diagonal discontinua marca la calibración perfecta, aquella línea sobre la que caería un modelo cuyas predicciones coincidieran exactamente con lo observado. Agrupé a los pacientes en deciles de riesgo predicho, y cada punto representa uno de esos grupos, de modo que puedo ver de forma directa si las predicciones se ajustan a la realidad o se desvían de ella.

La curva gris del modelo 1 se mantiene muy próxima a la diagonal a lo largo de casi todo su recorrido, con sus puntos apenas separados de la línea ideal, lo que traduce a una imagen su buena calibración, la misma que la prueba de Hosmer y Lemeshow había expresado con un valor de probabilidad de 0,798; sin embargo, esa curva ocupa un rango horizontal muy estrecho, pues sus predicciones se concentran aproximadamente entre el 30 % y el 55 % de probabilidad, sin aventurarse hacia los extremos.

El modelo 1 acierta en la magnitud de sus predicciones, pero solo porque predice siempre valores cercanos a la prevalencia general y nunca se arriesga a señalar riesgos muy altos ni muy bajos.

La curva morada del modelo 4 cuenta una historia distinta, pues se extiende sobre un rango horizontal mucho más amplio, desde probabilidades cercanas al 10 % hasta más del 80 %, lo que refleja su capacidad de discriminar, ya que se atreve a asignar riesgos extremos a los pacientes según su perfil.

Pero al observar su trazo veo que sus puntos se apartan de la diagonal de forma sistemática, con tramos en los que la curva se aleja visiblemente de la línea ideal, y esa desviación es la representación gráfica de su mala calibración, la que la prueba de Hosmer y Lemeshow reveló con un valor de probabilidad de 0,001.

La comparación de ambas curvas hace visible la paradoja que ya había identificado en la tabla, pues el modelo que mejor discrimina es el que peor calibra.

  • El modelo 1 se pega a la diagonal a costa de no separarse de la prevalencia, mientras que el modelo 4 abarca todo el espectro de riesgo a costa de desviarse de lo observado.

Interpreto que esta imagen contiene una enseñanza fundamental para el uso clínico de un modelo predictivo, pues un modelo puede ordenar correctamente a los pacientes de menor a mayor riesgo y, al mismo tiempo, equivocarse en el valor absoluto de las probabilidades que les asigna.

Por eso la calibración merece evaluarse aparte de la discriminación, y por eso tiene sentido preguntarme, en el punto siguiente, cómo podría corregirse la descalibración del modelo más discriminante sin renunciar a su capacidad de separar a los enfermos de los sanos.

18 Análisis de sensibilidad, ¿cambian los resultados al recuperar los pacientes excluidos?

Este análisis lo sitúo fuera de lo que el taller me solicita, pero decido incluirlo porque toda la Parte 2 se construyó sobre una base de 1.194 pacientes, tras excluir a 320 por tener algún dato faltante, y considero necesario comprobar si esa decisión condicionó los resultados.

La pregunta que quiero responderme es concreta:

  • si en lugar de descartar a esos pacientes recupero la cohorte completa rellenando sus valores ausentes, ¿el modelo 4 conservaría su capacidad de discriminación o esta cambia de forma apreciable?

Para responderla aplico la imputación múltiple mediante el método de ecuaciones encadenadas, que genera varias versiones completas de la base reemplazando cada valor faltante por estimaciones que preservan la incertidumbre y las relaciones entre variables, un procedimiento más riguroso que rellenar con un único valor fijo.

Ajusto el modelo 4 en cada una de esas versiones y calculo su área bajo la curva en cada una, para luego promediarlas y contrastar ese resultado con el área obtenida en la base de casos completos.

Preciso que este ejercicio es una comprobación de robustez y no una comparación estadística formal, pues no existe un procedimiento estándar para combinar curvas ROC entre las bases imputadas, de modo que lo que busco no es un valor de significancia sino verificar si la magnitud de la discriminación se mantiene.

datos_imputar <- sepsis_cc %>%
  dplyr::select(dplyr::all_of(vars_modelos))

imputacion <- mice::mice(datos_imputar, m = 5, method = "pmm",
                         seed = 2026, printFlag = FALSE)

auc_por_imputacion <- sapply(1:5, function(i) {
  base_i <- mice::complete(imputacion, i)
  modelo_i <- glm(
    Sepsis2 ~ Age + SBP + Procalcitonin + CRP + FR + SatO2 +
              HR + Bodytemperature + Leukocyteconcentration + NL_ratio,
    data = base_i, family = binomial(link = "logit"))
  roc_i <- pROC::roc(base_i$Sepsis2, predict(modelo_i, type = "response"),
                     levels = c("No", nivel_evento), direction = "<")
  as.numeric(pROC::auc(roc_i))
})

auc_imputado <- mean(auc_por_imputacion)
auc_completo <- as.numeric(pROC::auc(roc_m4))

data.frame(
  Base = c("Casos completos", "Cohorte con imputaci\u00f3n m\u00faltiple"),
  n = c(nrow(sepsis_mod), nrow(sepsis_cc)),
  AUC = c(sprintf("%.3f", auc_completo), sprintf("%.3f", auc_imputado))
) %>%
  kableExtra::kbl(align = "lcc",
    col.names = c("Base analítica", "N\u00famero de pacientes", "\u00c1rea bajo la curva del modelo 4"),
    caption = "An\u00e1lisis de sensibilidad: discriminaci\u00f3n del modelo 4 en casos completos frente a la cohorte imputada") %>%
  kableExtra::kable_styling(bootstrap_options = c("striped","hover","condensed"),
    full_width = FALSE, position = "center", font_size = 13) %>%
  kableExtra::row_spec(0, background = "#C9A9E0", color = "#2d2440", bold = TRUE) %>%
  kableExtra::column_spec(1, bold = TRUE)
Análisis de sensibilidad: discriminación del modelo 4 en casos completos frente a la cohorte imputada
Base analítica Número de pacientes Área bajo la curva del modelo 4
Casos completos 1194 0.751
Cohorte con imputación múltiple 1514 0.755

Análisis: El resultado de este análisis de sensibilidad me deja tranquila respecto a la decisión que tomé al inicio de la Parte 2,pues el modelo 4 alcanza un área bajo la curva de 0,751 en la base de casos completos y de 0,755 en la cohorte recuperada mediante imputación múltiple, de modo que la diferencia entre ambas es de apenas 0,004, una distancia que aparece solo en la tercera cifra decimal y que puedo considerar despreciable. Recuperar a los 320 pacientes que había excluido no modifica la capacidad de discriminación del modelo.

Considero que esta coincidencia tiene una consecuencia importante para la validez de todo el trabajo, pues me confirma que trabajar únicamente con los casos completos no introdujo un sesgo que distorsionara las conclusiones.

Si la exclusión de esos pacientes hubiera estado sistemáticamente ligada a su probabilidad de sepsis, cabría esperar que el área bajo la curva cambiara al reincorporarlos, y no es lo que ocurrió; la discriminación se mantuvo casi intacta, lo que sugiere que, al menos en lo que respecta a esta medida, los datos faltantes no guardaban una relación determinante con el desenlace.

La decisión de casos completos, que adopté para garantizar la comparabilidad entre los modelos, resulta así respaldada por la evidencia.

Considero que este ejercicio, aunque queda fuera de lo que el taller pedía, me aporta un cierre valioso a la Parte 2, porque sometí a prueba una de las decisiones metodológicas de fondo en lugar de darla por buena sin cuestionarla.

La robustez del modelo 4 frente a dos formas distintas de tratar los datos faltantes refuerza la confianza en que su superioridad, establecida a lo largo de toda la sección, es un hallazgo estable y no un artefacto de la manera en que se construyó la muestra.

19 Conclusiones

Al finalizar este taller, reconozco que las dos partes que lo componen, donde se emplearon herramientas distintas, considero que han convergido en una misma enseñanza sobre el diagnóstico de la sepsis bacteriana.

  • La primera parte, centrada en la evaluación de pruebas diagnósticas mediante curvas ROC, y la segunda, dedicada a la construcción de modelos predictivos, terminaron por confirmarme, cada una desde su método, que ningún dato considerado de forma aislada resulta suficiente para decidir con seguridad sobre una enfermedad tan grave y de presentación tan inespecífica.

En la primera parte comprobé que la procalcitonina, pese a ser el mejor de los dos biomarcadores estudiados, alcanzó un área bajo la curva de apenas 0,641, y que la proteína C reactiva, con 0,573, se acercaba peligrosamente al azar.

La prueba de DeLong confirmó que la procalcitonina discrimina mejor, pero el análisis de los distintos puntos de corte me enseñó una lección que considero central, la de que la elección de un umbral no es una cuestión técnica sino clínica, pues depende de si el objetivo es confirmar la enfermedad, lo que exige especificidad, o descartarla, lo que exige sensibilidad.

Concluí entonces, que la procalcitonina en el corte de 0,5 ng/mL es la mejor opción individual para confirmar la sepsis, siempre entendida como una ayuda y nunca como una prueba definitiva.

En la segunda parte, la regresión logística me permitió ir más allá de los marcadores aislados y comprobar qué ocurre al combinarlos. El hallazgo que más me marcó fue contraintuitivo, pues descubrí que añadir variables débiles a un buen biomarcador no mejora la predicción sino que la diluye, hasta el punto de que la procalcitonina en solitario superó a los modelos que la incluían junto a variables clínicas de escaso valor.

Solo el modelo construido mediante selección automática, que reunió los signos vitales y los marcadores inflamatorios más informativos, logró una discriminación aceptable con un área de 0,751. Aprendí, así, que el valor de un modelo no reside en la cantidad de variables sino en la calidad y la sinergia de las que verdaderamente aportan.

Finalmente considero lo más valioso de todo el ejercicio, y es que la significancia estadística no equivale a la relevancia clínica, pues un valor de probabilidad muy pequeño puede reflejar solo el gran tamaño de la muestra y no una diferencia importante.

En cuanto a la discriminación y la calibración comprendo que son propiedades distintas e independientes, y que el modelo más capaz de ordenar a los pacientes según su riesgo puede ser, al mismo tiempo, el que peor estima el valor absoluto de ese riesgo.

Considero que toda decisión sobre el manejo de los datos, como la exclusión de casos incompletos, debe someterse a prueba, algo que el análisis de sensibilidad me confirmó al mostrar que las conclusiones se mantenían estables al recuperar los pacientes excluidos.

En conjunto, la epidemiología diagnóstica no busca la prueba perfecta, que rara vez existe, sino la interpretación juiciosa de pruebas imperfectas a la luz del contexto clínico de cada paciente.

Donde los biomarcadores y los modelos son instrumentos que orientan la sospecha y ordenan la incertidumbre, pero no sustituyen el juicio clínico, que integra la información cuantitativa con la historia y la evolución del enfermo.

20 Referencias

  1. Ljungström L, Pernestig AK, Jacobsson G, Andersson R, Usener B, Tilevik D. Diagnostic accuracy of procalcitonin, neutrophil-lymphocyte count ratio, C-reactive protein, and lactate in patients with suspected bacterial sepsis. PLoS One. 2017;12(7):e0181704. https://doi.org/10.1371/journal.pone.0181704

  2. DeLong ER, DeLong DM, Clarke-Pearson DL. Comparing the areas under two or more correlated receiver operating characteristic curves: a nonparametric approach. Biometrics. 1988;44(3):837-845. https://doi.org/10.2307/2531595

  3. Steyerberg EW, Vickers AJ, Cook NR, Gerds T, Gonen M, Obuchowski N, et al. Assessing the performance of prediction models: a framework for some traditional and novel measures. Epidemiology. 2010;21(1):128-138. https://doi.org/10.1097/EDE.0b013e3181c30fb2

  4. Van Calster B, McLernon DJ, van Smeden M, Wynants L, Steyerberg EW. Calibration: the Achilles heel of predictive analytics. BMC Med. 2019;17(1):230. https://doi.org/10.1186/s12916-019-1466-7

  5. Fletcher RH, Fletcher SW, Fletcher GS. Clinical epidemiology: the essentials. 6.ª ed. Filadelfia: Wolters Kluwer; 2021.