E.S.E. Hospital César Uribe Piedrahita · Caucasia, Antioquia · 2.272 nacimientos analizados
El nacimiento es uno de los eventos más importantes de la atención en salud, y la forma en que ocurre, por vía vaginal o por cesárea, influye en la recuperación de la madre, en la salud del recién nacido y en los recursos que el hospital debe tener disponibles. Por esa razón, el tipo de parto es un indicador que los sistemas de salud monitorean de manera sistemática a través de las estadísticas vitales. Desde la ingeniería industrial, la atención del parto puede verse como un proceso con recursos limitados (quirófanos, personal, camas e insumos) que se deben planificar con anticipación.
El aprendizaje supervisado es una herramienta ampliamente usada para analizar datos clínicos y administrativos. A partir de casos ya observados, en los que se conoce el resultado, se construyen modelos que aprenden la relación entre unas variables de entrada y una variable respuesta, y que luego se emplean para clasificar casos nuevos (James et al., 2021). Cuando la respuesta tiene dos categorías, como cesárea o parto vaginal, se habla de un problema de clasificación binaria.
Este informe aplica dos técnicas de clasificación, K vecinos más cercanos (KNN) y regresión logística (Logit), a una muestra de la base de datos del E.S.E. Hospital César Uribe Piedrahita de Caucasia, Antioquia. Primero se presenta el contexto y el problema; luego la metodología y la descripción de los datos; después los resultados de los modelos y su comparación; y finalmente las conclusiones sobre qué tan útiles son estos modelos para anticipar el tipo de parto.
La cesárea es una intervención quirúrgica que salva vidas maternas y perinatales cuando existe una indicación médica. Sin embargo, la Organización Mundial de la Salud (OMS) advierte que, a nivel poblacional, las tasas de cesárea superiores al 10 % no se asocian con reducciones de la mortalidad materna y neonatal, y que su aumento injustificado expone a madres y recién nacidos a complicaciones a corto y largo plazo frente al parto vaginal (OMS, 2015). Colombia no es ajena a esta tendencia: según el DANE, en 2023 los nacimientos por cesárea superaron por primera vez a los partos espontáneos en la serie 2014-2023, y el aumento se observa tanto en zonas urbanas como rurales (DANE, 2024).
La base analizada refleja esta situación: en la muestra de la E.S.E. Hospital César Uribe Piedrahita de Caucasia, Antioquia (2.272 nacimientos), el 48,4 % terminó en cesárea. Para un hospital de este tipo, anticipar qué gestantes tienen mayor probabilidad de cesárea es relevante para programar quirófanos, personal de anestesiología, camas y suministros, y para identificar factores asociados que orienten la vigilancia prenatal. La clasificación estadística, en todo caso, apoya la planeación y no reemplaza la indicación clínica.
¿Es posible clasificar el tipo de parto (cesárea vs. parto vaginal) a partir de cinco variables maternas y neonatales, con un desempeño suficiente para apoyar la planeación hospitalaria? ¿Qué método, KNN o regresión logística, lo hace mejor?
Construir y comparar dos modelos de aprendizaje supervisado (KNN y Logit) para clasificar el tipo de parto en una muestra de nacimientos del hospital de Caucasia.
La información proviene de la base de nacimientos del hospital
(archivo nacidos.xlsx). Para construir la base de análisis
se siguieron estos pasos:
if (file.exists("nacidos.xlsx")) {
nacidos <- read_excel("nacidos.xlsx")
} else if (file.exists("nacidos (1).xlsx")) {
nacidos <- read_excel("nacidos (1).xlsx")
} else {
stop("No se encuentra el Excel. Guarda nacidos.xlsx o nacidos (1).xlsx en la misma carpeta del Rmd.")
}
niveles <- c("Vaginal", "Cesarea") # la clase positiva es "Cesarea"
variables <- c("EdadMadre", "TiempoGestacion", "ConsultasPrenatales", "Peso", "Talla")
tipo_original <- toupper(trimws(
iconv(as.character(nacidos[["Tipo Parto"]]), to = "ASCII//TRANSLIT")
))
tipo_binario <- case_when(
str_detect(tipo_original, "CESAR") ~ "Cesarea",
str_detect(tipo_original, "ESPONTANEO|INSTRUMENTADO") ~ "Vaginal",
TRUE ~ NA_character_
)
datos <- tibble(
TipoParto = factor(tipo_binario, levels = niveles),
EdadMadre = suppressWarnings(as.numeric(as.character(nacidos[["Edad Madre"]]))),
TiempoGestacion = suppressWarnings(as.numeric(as.character(nacidos[["Tiempo Gestación"]]))),
ConsultasPrenatales = suppressWarnings(as.numeric(as.character(nacidos[["Número Consultas Prenatales"]]))),
Peso = suppressWarnings(as.numeric(as.character(nacidos[["Peso"]]))),
Talla = suppressWarnings(as.numeric(as.character(nacidos[["Talla"]])))
) %>% drop_na()
En total se utilizaron 2272 observaciones y se excluyeron 0 por valores faltantes o categorías no reconocidas.
Para evaluar los modelos con datos que no se usaron al entrenarlos, la base se dividió en dos conjuntos mediante una partición estratificada por tipo de parto (se conserva la proporción de cesáreas en ambos): un 70 % para entrenamiento y el resto para prueba. La semilla es 123, lo que permite reproducir exactamente la misma partición.
Como KNN calcula distancias euclidianas, las variables con escalas grandes (por ejemplo, el peso en gramos) dominarían a las de escalas pequeñas (por ejemplo, las consultas prenatales). Por eso las cinco predictoras se estandarizaron (media 0 y desviación estándar 1). Los parámetros (media y desviación) se calcularon únicamente con el conjunto de entrenamiento y se aplicaron luego al de prueba, para evitar fuga de información. El modelo Logit se ajustó con las mismas variables.
Finalmente, la capacidad predictiva de ambos enfoques se contrastó sobre el conjunto de prueba inédito. Para determinar la superioridad técnica, se construyeron matrices de confusión y Curvas ROC, comparando métricas como la Exactitud (rendimiento global), la Sensibilidad (capacidad de detectar la clase Cesárea), la Especificidad y el Área Bajo la Curva (AUC).
| Variable | Tipo | Unidad | Disponible antes del parto |
|---|---|---|---|
| EdadMadre | Cuantitativa continua | años | Sí |
| TiempoGestacion | Cuantitativa continua | semanas | Sí |
| ConsultasPrenatales | Cuantitativa discreta | número de consultas | Sí |
| Peso | Cuantitativa continua | gramos | No |
| Talla | Cuantitativa continua | centímetros | No |
Edad de la madre: los extremos de la vida reproductiva representan un mayor riesgo obstétrico. El embarazo adolescente (menores de 19 años) suele asociarse con inmadurez pélvica, mientras que la maternidad tardía (mayores de 35 años) incrementa la probabilidad de comorbilidades como preeclampsia o diabetes gestacional.
Tiempo de gestación: modela la madurez fetal al nacer. Las gestaciones a término (37 a 40 semanas) favorecen el parto vaginal, mientras que la prematuridad extrema o los embarazos prolongados (más de 41 semanas) pueden requerir una extracción quirúrgica.
Número de consultas prenatales: indicador indirecto del seguimiento médico y del acceso al sistema de salud. Muy pocos controles sugieren embarazos sin monitoreo; muchos controles pueden reflejar un embarazo de alto riesgo que se vigila para programar una cesárea.
Peso del recién nacido: uno de los factores más determinantes. Valores muy altos (macrosomía, por encima de unos 4000 g) pueden generar desproporción cefalopélvica. Como referencia clínica para clasificar el peso al nacer (bajo peso, peso adecuado, macrosomía) se consultaron el Ministerio de Salud y FECOLSOG (2014) y Barranquero Gómez y Azaña Gutiérrez (2023).
Talla del recién nacido: variable antropométrica que complementa al peso y dimensiona el desarrollo físico del feto.
Peso y talla solo se conocen cuando el bebé ya nació, por lo que no servirían para anticipar el tipo de parto. Este punto se retoma en el análisis de sensibilidad.
El modelo Logit estima la probabilidad \(p\) de que el parto sea cesárea como función de las cinco variables:
\[\ln\left(\frac{p}{1-p}\right)=\beta_0+\beta_1 X_1+\beta_2 X_2+\beta_3 X_3+\beta_4 X_4+\beta_5 X_5\]
Un parto se clasifica como cesárea cuando \(p\) supera un umbral. Se probaron dos umbrales: el usual de 0.5 y el que maximiza el índice de Youden (sensibilidad + especificidad − 1), calculado solo con datos de entrenamiento. Sus coeficientes se interpretan mediante odds ratios (Hosmer et al., 2013), lo que lo hace un modelo explicable.
KNN clasifica un parto nuevo según la clase que predomina entre los \(k\) partos más parecidos del conjunto de entrenamiento, donde “parecido” se mide con la distancia euclidiana entre las variables estandarizadas. No supone una forma funcional entre las variables y la respuesta, pero depende de la elección de \(k\). Se evaluó con \(k = 5\) y con un \(k\) óptimo elegido por validación cruzada de 10 pliegues, usando el AUC como criterio (James et al., 2021; Kuhn y Johnson, 2013).
Los modelos se compararon sobre el conjunto de prueba con una matriz de confusión, donde la cesárea es la clase positiva, y con las siguientes métricas:
| Métrica | Fórmula | Lectura en el hospital |
|---|---|---|
| Exactitud | (VP + VN) / total | Proporción de partos bien clasificados |
| Sensibilidad | VP / (VP + FN) | De las cesáreas reales, cuántas se anticipan |
| Especificidad | VN / (VN + FP) | De los partos vaginales, cuántos se reconocen como tales |
| Precisión | VP / (VP + FP) | De las alertas de cesárea, cuántas se confirman |
| F1 | 2 · Precisión · Sensibilidad / (Precisión + Sensibilidad) | Balance entre precisión y sensibilidad |
| AUC | Área bajo la curva ROC | Capacidad de separar las dos clases sin fijar un umbral |
En términos de operación hospitalaria, un falso negativo es una cesárea que no se previó (el quirófano y el equipo no estaban reservados) y un falso positivo es un recurso reservado sin necesidad. Por eso, además de la exactitud, se analizó el efecto del umbral de clasificación y un ejercicio ilustrativo de costos de error. El AUC se calculó con la curva ROC (Robin et al., 2011).
En la comparación principal se enfrenta el Logit con umbral 0.5 al KNN con el \(k\) óptimo elegido por validación cruzada, que es el KNN ya ajustado; el KNN con \(k = 5\) se conserva únicamente como referencia.
El análisis se realizó en R version 4.6.1 (2026-06-24 ucrt) dentro de RStudio, con R Markdown para el reporte y los siguientes paquetes:
| Paquete | Uso | Versión | Referencia |
|---|---|---|---|
| tidyverse | Manipulación de datos y gráficos (ggplot2) | 2.0.0 | Wickham et al. (2019) |
| readxl | Lectura del archivo Excel | 1.5.0 | - |
| caret | Partición estratificada, validación cruzada y matriz de confusión | 7.0.1 | Kuhn (2008) |
| class | Algoritmo KNN | 7.3.23 | - |
| pROC | Curvas ROC y AUC | 1.19.1 | Robin et al. (2011) |
| plotly | Gráficos interactivos | 4.12.1 | Sievert (2020) |
| broom | Tabla ordenada de coeficientes del Logit | 1.0.13 | - |
| car | Factor de inflación de la varianza (VIF) | 3.1.5 | Fox y Weisberg (2019) |
| knitr | Tablas con kable() | 1.51 | - |
Para dejar documentado el entorno exacto de ejecución:
R version 4.6.1 (2026-06-24 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 26200)
Matrix products: default
LAPACK version 3.12.1
locale:
[1] LC_COLLATE=Spanish_Mexico.utf8 LC_CTYPE=Spanish_Mexico.utf8
[3] LC_MONETARY=Spanish_Mexico.utf8 LC_NUMERIC=C
[5] LC_TIME=Spanish_Mexico.utf8
time zone: America/Bogota
tzcode source: internal
attached base packages:
[1] stats graphics grDevices utils datasets methods base
other attached packages:
[1] plotly_4.12.1 pROC_1.19.1 class_7.3-23 caret_7.0-1
[5] lattice_0.22-9 knitr_1.51 lubridate_1.9.5 forcats_1.0.1
[9] stringr_1.6.0 dplyr_1.2.1 purrr_1.2.2 readr_2.2.0
[13] tidyr_1.3.2 tibble_3.3.1 ggplot2_4.0.3 tidyverse_2.0.0
[17] readxl_1.5.0
loaded via a namespace (and not attached):
[1] tidyselect_1.2.1 viridisLite_0.4.3 timeDate_4052.112
[4] farver_2.1.2 S7_0.2.2 fastmap_1.2.0
[7] digest_0.6.39 rpart_4.1.27 timechange_0.4.0
[10] lifecycle_1.0.5 survival_3.8-6 magrittr_2.0.5
[13] compiler_4.6.1 rlang_1.2.0 sass_0.4.10
[16] tools_4.6.1 yaml_2.3.12 data.table_1.18.6.1
[19] htmlwidgets_1.6.4 plyr_1.8.9 RColorBrewer_1.1-3
[22] withr_3.0.3 nnet_7.3-20 grid_4.6.1
[25] stats4_4.6.1 future_1.76.0 globals_0.19.1
[28] scales_1.4.0 iterators_1.0.14 MASS_7.3-65
[31] cli_3.6.6 rmarkdown_2.31 generics_0.1.4
[34] otel_0.2.0 rstudioapi_0.19.0 future.apply_1.20.2
[37] httr_1.4.9 reshape2_1.4.5 tzdb_0.5.0
[40] cachem_1.1.0 splines_4.6.1 parallel_4.6.1
[43] cellranger_1.1.0 vctrs_0.7.3 hardhat_1.4.3
[46] Matrix_1.7-5 jsonlite_2.0.0 hms_1.1.4
[49] listenv_1.1.0 foreach_1.5.2 gower_1.0.2
[52] jquerylib_0.1.4 recipes_1.4.0 glue_1.8.1
[55] parallelly_1.48.0 codetools_0.2-20 stringi_1.8.7
[58] gtable_0.3.6 pillar_1.11.1 htmltools_0.5.9
[61] ipred_0.9-16 lava_1.9.3 R6_2.6.1
[64] evaluate_1.0.5 bslib_0.11.0 Rcpp_1.1.2
[67] nlme_3.1-169 prodlim_2026.03.11 xfun_0.57
[70] pkgconfig_2.0.3 ModelMetrics_1.2.2.2
Además se definieron dos funciones propias, que se reutilizan en el modelo completo y en el análisis de sensibilidad:
#' Calcula las métricas de clasificación sobre el conjunto de prueba.
#'
#' @param real Factor con el tipo de parto observado.
#' @param predicho Factor con la clase predicha.
#' @param probabilidades Probabilidad predicha de la clase "Cesarea" (para el AUC).
#' @return Vector con exactitud, sensibilidad, especificidad, precisión, F1 y AUC.
#' La clase positiva es "Cesarea".
evaluar_modelo <- function(real, predicho, probabilidades) {
matriz <- table(
Real = factor(real, levels = niveles),
Predicho = factor(predicho, levels = niveles)
)
TN <- matriz["Vaginal", "Vaginal"]
FP <- matriz["Vaginal", "Cesarea"]
FN <- matriz["Cesarea", "Vaginal"]
TP <- matriz["Cesarea", "Cesarea"]
exactitud <- (TP + TN) / sum(matriz)
sensibilidad <- if ((TP + FN) > 0) TP / (TP + FN) else NA_real_
especificidad <- if ((TN + FP) > 0) TN / (TN + FP) else NA_real_
precision <- if ((TP + FP) > 0) TP / (TP + FP) else NA_real_
F1 <- if (!is.na(precision) && !is.na(sensibilidad) && (precision + sensibilidad) > 0) {
2 * precision * sensibilidad / (precision + sensibilidad)
} else {
NA_real_
}
curva <- pROC::roc(response = real, predictor = as.numeric(probabilidades),
levels = niveles, direction = "<", quiet = TRUE)
c(Exactitud = exactitud, Sensibilidad = sensibilidad,
Especificidad = especificidad, Precision = precision,
F1 = F1, AUC = as.numeric(pROC::auc(curva)))
}
#' Ajusta KNN y Logit para un conjunto de variables y los evalúa en prueba.
#'
#' @param vars Vector con los nombres de las variables predictoras.
#' @param k_fijo Valor de k para el KNN "base" (las etiquetas de la tabla suponen k = 5).
#' @param semilla Semilla para el KNN (desempates) y la validación cruzada.
#' @return Lista con la tabla de métricas, el modelo Logit, el objeto de validación
#' cruzada, k y umbral óptimos, las matrices estandarizadas y las predicciones
#' sobre el conjunto de prueba.
ajustar_modelos <- function(vars, k_fijo = 5, semilla = params$semilla) {
tr <- entrenamiento
te <- prueba
# Estandarización con parámetros del entrenamiento
centros <- sapply(tr[, vars], mean)
escalas <- sapply(tr[, vars], sd)
escalas[is.na(escalas) | escalas == 0] <- 1
tr_s <- scale(tr[, vars], center = centros, scale = escalas)
te_s <- scale(te[, vars], center = centros, scale = escalas)
# KNN para un k dado: devuelve la clase predicha y la probabilidad de cesárea
knn_pred <- function(k) {
set.seed(semilla)
p <- class::knn(train = tr_s, test = te_s, cl = tr$TipoParto, k = k, prob = TRUE)
v <- attr(p, "prob")
list(pred = factor(p, levels = niveles),
prob = ifelse(p == "Cesarea", v, 1 - v))
}
# k óptimo por validación cruzada de 10 pliegues (criterio: AUC)
set.seed(semilla)
cv <- caret::train(
x = as.data.frame(tr_s), y = tr$TipoParto, method = "knn",
tuneGrid = data.frame(k = seq(3, 51, by = 2)),
trControl = trainControl(method = "cv", number = 10,
classProbs = TRUE, summaryFunction = twoClassSummary),
metric = "ROC"
)
k_opt <- cv$bestTune$k
# Logit
f <- as.formula(paste("TipoParto ~", paste(vars, collapse = " + ")))
logit <- glm(f, data = tr, family = binomial(link = "logit"))
p_tr <- predict(logit, type = "response")
p_te <- predict(logit, newdata = te, type = "response")
# Umbral de Youden calculado SOLO con datos de entrenamiento
roc_tr <- pROC::roc(tr$TipoParto, p_tr, levels = niveles, direction = "<", quiet = TRUE)
u_opt <- as.numeric(pROC::coords(roc_tr, "best", best.method = "youden",
ret = "threshold", transpose = FALSE)[1, "threshold"])
clasif <- function(p, u) factor(ifelse(p >= u, "Cesarea", "Vaginal"), levels = niveles)
k5 <- knn_pred(k_fijo)
kopt <- knn_pred(k_opt)
pred_l05 <- clasif(p_te, 0.5)
pred_lopt <- clasif(p_te, u_opt)
resultados <- rbind(
"KNN (k=5)" = evaluar_modelo(te$TipoParto, k5$pred, k5$prob),
"KNN (k óptimo)" = evaluar_modelo(te$TipoParto, kopt$pred, kopt$prob),
"Logit (u=0.5)" = evaluar_modelo(te$TipoParto, pred_l05, p_te),
"Logit (u óptimo)" = evaluar_modelo(te$TipoParto, pred_lopt, p_te)
)
list(resultados = resultados, logit = logit, cv = cv, k_opt = k_opt, u_opt = u_opt,
k5 = k5, kopt = kopt, p_te = p_te, pred_l05 = pred_l05, pred_lopt = pred_lopt,
tr_s = tr_s, te_s = te_s)
}
n_total <- nrow(datos)
pct_ces <- round(100 * mean(datos$TipoParto == "Cesarea"), 1)
# Formato largo para graficar y resumir las cinco variables cuantitativas
datos_largos <- datos %>%
select(TipoParto, all_of(variables)) %>%
pivot_longer(cols = all_of(variables), names_to = "Variable", values_to = "Valor") %>%
mutate(Variable = factor(Variable, levels = variables))
# Tabla 1: distribución del tipo de parto
frecuencias <- as.data.frame(table(datos$TipoParto))
names(frecuencias) <- c("Tipo de parto", "Frecuencia")
frecuencias$Porcentaje <- round(100 * frecuencias$Frecuencia / sum(frecuencias$Frecuencia), 1)
# Tabla 2: resumen de variables cuantitativas
resumen <- datos_largos %>%
group_by(Variable) %>%
summarise(Observaciones = n(), Media = mean(Valor), Desviacion = sd(Valor),
Minimo = min(Valor), Q1 = quantile(Valor, 0.25), Mediana = median(Valor),
Q3 = quantile(Valor, 0.75), Maximo = max(Valor), .groups = "drop") %>%
mutate(across(where(is.numeric), ~ round(.x, 2)))
# Tabla 3: variables cuantitativas según tipo de parto y prueba de Wilcoxon
por_grupo <- datos_largos %>%
group_by(Variable, TipoParto) %>%
summarise(Media = mean(Valor), Desviacion = sd(Valor), Mediana = median(Valor),
.groups = "drop") %>%
mutate(across(where(is.numeric), ~ round(.x, 2)))
pvals <- datos_largos %>%
group_by(Variable) %>%
summarise(Valor_p_Wilcoxon = signif(wilcox.test(Valor ~ TipoParto)$p.value, 3),
.groups = "drop")
tabla_grupo <- left_join(por_grupo, pvals, by = "Variable")
vars_sig <- as.character(pvals$Variable[pvals$Valor_p_Wilcoxon < 0.05])
# Correlaciones entre predictoras
cor_mat <- cor(datos[, variables])
cor_pt <- cor(datos$Peso, datos$Talla)
# Variables categóricas adicionales (no entran al modelo)
categoricas <- tibble(
TipoParto = factor(tipo_binario, levels = niveles),
Area = as.character(nacidos[["Area Nacimiento"]]),
Multiplicidad = as.character(nacidos[["Multiplicidad Embarazo"]])
) %>%
filter(!is.na(TipoParto)) %>%
mutate(across(c(Area, Multiplicidad), ~ replace_na(.x, "Sin dato")))
tabla_cat_una <- function(var, etiqueta) {
categoricas %>%
group_by(Categoria = .data[[var]]) %>%
summarise(Observaciones = n(),
Cesareas = sum(TipoParto == "Cesarea"),
Pct_cesarea = round(100 * mean(TipoParto == "Cesarea"), 1),
.groups = "drop") %>%
mutate(Variable = etiqueta, .before = 1)
}
tabla_cat <- bind_rows(tabla_cat_una("Area", "Área de nacimiento"),
tabla_cat_una("Multiplicidad", "Multiplicidad del embarazo"))
set.seed(params$semilla)
p_area <- chisq.test(table(categoricas$Area, categoricas$TipoParto),
simulate.p.value = TRUE, B = 5000)$p.value
p_mult <- chisq.test(table(categoricas$Multiplicidad, categoricas$TipoParto),
simulate.p.value = TRUE, B = 5000)$p.value
La base de análisis contiene 2272 nacimientos, de los cuales el 48.4 % terminó en cesárea, es decir, una distribución casi balanceada entre las dos clases. Esto es favorable porque la exactitud es una métrica informativa y no se requieren técnicas de balanceo.
Al comparar las cinco variables entre tipos de parto con la prueba de Wilcoxon (Tabla 3), las variables con diferencias significativas al 5 % son: EdadMadre, TiempoGestacion, Talla. Estas diferencias son descriptivas y no demuestran causalidad.
Entre las predictoras, el peso y la talla del recién nacido tienen una correlación de 0.62, lo que anticipa un posible problema de multicolinealidad en el modelo Logit que se revisa más adelante. Entre las variables categóricas adicionales, la asociación con el tipo de parto tiene un valor p de 0.727 para el área de nacimiento y de 0.196 para la multiplicidad del embarazo.
| Tipo de parto | Frecuencia | Porcentaje |
|---|---|---|
| Vaginal | 1173 | 51.6 |
| Cesarea | 1099 | 48.4 |
Análisis. La Tabla 1 indica que, de los 2272 nacimientos, 1099 (48.4 %) fueron cesáreas y 1173 (51.6 %) partos vaginales. La diferencia entre las dos clases es de 3.2 puntos porcentuales, por lo que la variable respuesta está prácticamente balanceada. Esto tiene una consecuencia directa para el modelado: un clasificador que siempre predijera la clase más frecuente acertaría alrededor del 51.6 % de las veces, y ese es el punto de referencia que los modelos deben superar para aportar información.
| Variable | Observaciones | Media | Desviacion | Minimo | Q1 | Mediana | Q3 | Maximo |
|---|---|---|---|---|---|---|---|---|
| EdadMadre | 2272 | 24.08 | 6.31 | 12 | 19.00 | 23 | 28 | 51 |
| TiempoGestacion | 2272 | 38.51 | 1.46 | 21 | 38.00 | 39 | 39 | 42 |
| ConsultasPrenatales | 2272 | 6.21 | 2.37 | 0 | 5.00 | 7 | 8 | 16 |
| Peso | 2272 | 3188.64 | 428.42 | 730 | 2923.75 | 3200 | 3460 | 4690 |
| Talla | 2272 | 50.04 | 2.30 | 22 | 49.00 | 50 | 51 | 59 |
Análisis. La Tabla 2 resume las cinco variables predictoras:
Como las variables tienen escalas muy distintas (años, semanas, número de consultas, gramos y centímetros), se estandarizaron antes de aplicar KNN.
| Variable | TipoParto | Media | Desviacion | Mediana | Valor_p_Wilcoxon |
|---|---|---|---|---|---|
| EdadMadre | Vaginal | 23.86 | 6.50 | 22 | 1.52e-02 |
| EdadMadre | Cesarea | 24.32 | 6.09 | 23 | 1.52e-02 |
| TiempoGestacion | Vaginal | 38.60 | 1.56 | 39 | 2.00e-07 |
| TiempoGestacion | Cesarea | 38.41 | 1.34 | 38 | 2.00e-07 |
| ConsultasPrenatales | Vaginal | 6.14 | 2.50 | 7 | 3.20e-01 |
| ConsultasPrenatales | Cesarea | 6.28 | 2.23 | 7 | 3.20e-01 |
| Peso | Vaginal | 3172.93 | 434.43 | 3185 | 1.70e-01 |
| Peso | Cesarea | 3205.40 | 421.46 | 3200 | 1.70e-01 |
| Talla | Vaginal | 50.35 | 2.40 | 50 | 0.00e+00 |
| Talla | Cesarea | 49.70 | 2.15 | 50 | 0.00e+00 |
Análisis. Al comparar cada variable entre cesáreas y partos vaginales (Tabla 3) se observa lo siguiente:
En total, 3 de las 5 variables presentan diferencias significativas al 5 % (EdadMadre, TiempoGestacion, Talla). Con una muestra de 2272 nacimientos, incluso diferencias pequeñas pueden resultar significativas, por lo que el valor p debe leerse junto con la magnitud de las diferencias (ver Figura 3).
| Variable | Categoria | Observaciones | Cesareas | Pct_cesarea |
|---|---|---|---|---|
| Área de nacimiento | CABECERA MUNICIPAL | 2264 | 1096 | 48.4 |
| Área de nacimiento | RURAL DISPERSO | 8 | 3 | 37.5 |
| Multiplicidad del embarazo | CUÁDRUPLE O MÁS | 1 | 1 | 100.0 |
| Multiplicidad del embarazo | DOBLE | 21 | 14 | 66.7 |
| Multiplicidad del embarazo | SIMPLE | 2245 | 1081 | 48.2 |
| Multiplicidad del embarazo | TRIPLE | 5 | 3 | 60.0 |
Análisis. En el área de nacimiento, la categoría con mayor porcentaje de cesáreas es «CABECERA MUNICIPAL» (48.4 %, 2264 observaciones) y la de menor porcentaje es «RURAL DISPERSO» (37.5 %, 8 observaciones); la prueba chi-cuadrado (p = 0.727) indica que no hay evidencia de asociación entre esta variable y el tipo de parto. En la multiplicidad del embarazo, la categoría con mayor porcentaje de cesáreas es «CUÁDRUPLE O MÁS» (100 %, 1 observaciones) y la de menor porcentaje es «SIMPLE» (48.2 %, 2245 observaciones); con p = 0.196, no hay evidencia de asociación. Las categorías con pocas observaciones (la más pequeña tiene 1) producen porcentajes poco estables, por lo que deben leerse con cautela. Como estas variables no entran a los modelos, su papel es contextual.
p1 <- datos %>%
count(TipoParto) %>%
mutate(Porcentaje = round(100 * n / sum(n), 1)) %>%
ggplot(aes(x = TipoParto, y = n, fill = TipoParto,
text = paste0(n, " registros (", Porcentaje, " %)"))) +
geom_col(width = 0.6) +
scale_fill_manual(values = colores_parto) +
labs(x = "Tipo de parto", y = "Número de registros") +
theme(legend.position = "none")
ggplotly(p1, tooltip = "text")
Análisis. La Figura 1 muestra dos barras de alturas similares: 1099 cesáreas frente a 1173 partos vaginales, es decir, una diferencia de 74 registros. Visualmente se confirma el balance de la variable respuesta que indicaba la Tabla 1. Para el hospital, esto significa que aproximadamente 48 de cada 100 partos requiere quirófano, una carga alta de este recurso que justifica el interés en anticiparla.
p2 <- ggplot(datos_largos, aes(x = Valor)) +
geom_histogram(bins = 25, fill = "#4CAF73", color = "white") +
facet_wrap(~Variable, scales = "free", ncol = 2) +
labs(x = "Valor", y = "Frecuencia")
ggplotly(p2)
Análisis. Los histogramas permiten ver la forma de cada variable:
Las colas largas de algunas variables sugieren valores atípicos, que se revisan en la nota al final de esta sección.
p3 <- ggplot(datos_largos, aes(x = TipoParto, y = Valor, fill = TipoParto)) +
geom_boxplot(show.legend = FALSE) +
facet_wrap(~Variable, scales = "free", ncol = 2) +
scale_fill_manual(values = colores_parto) +
labs(x = "Tipo de parto", y = "Valor")
ggplotly(p3)
Análisis. Los diagramas de caja comparan la distribución de cada variable según el tipo de parto. Para medir cuánto se separan los dos grupos se calculó el tamaño del efecto (d de Cohen: diferencia de medias dividida por la desviación estándar combinada; un valor positivo indica que el promedio es mayor en las cesáreas):
La variable que más separa a los dos grupos es Talla (|d| = 0.29). En ningún caso la separación es grande, lo que anticipa que el poder de clasificación de los modelos será moderado.
cat_largo <- categoricas %>%
pivot_longer(c(Area, Multiplicidad), names_to = "Variable", values_to = "Categoria") %>%
mutate(Variable = recode(Variable, Area = "Área de nacimiento",
Multiplicidad = "Multiplicidad del embarazo"))
p4 <- ggplot(cat_largo, aes(x = Categoria, fill = TipoParto)) +
geom_bar(position = "fill") +
facet_wrap(~Variable, scales = "free_x") +
scale_fill_manual(values = colores_parto) +
scale_y_continuous(labels = scales::percent) +
labs(x = NULL, y = "Porcentaje de observaciones", fill = "Tipo de parto") +
theme(axis.text.x = element_text(angle = 20, hjust = 1))
ggplotly(p4)
Análisis. La Figura 4 muestra, dentro de cada categoría, qué proporción de los partos fue cesárea; el porcentaje global es de 48.4 %. Ninguna categoría con al menos 30 observaciones se aleja más de 5 puntos porcentuales de ese valor, lo que concuerda con una asociación débil entre estas variables y el tipo de parto. Las barras de categorías con pocas observaciones deben interpretarse con cautela, porque un solo caso cambia mucho su porcentaje.
cor_long <- as.data.frame(as.table(cor_mat)) %>%
rename(Variable_1 = Var1, Variable_2 = Var2, Correlacion = Freq) %>%
mutate(Correlacion = round(Correlacion, 2))
ggplot(cor_long, aes(x = Variable_1, y = Variable_2, fill = Correlacion)) +
geom_tile(color = "white") +
geom_text(aes(label = Correlacion), size = 3.5) +
scale_fill_gradient2(low = "#8D6E63", mid = "white", high = "#1B6B3A",
midpoint = 0, limits = c(-1, 1)) +
labs(x = NULL, y = NULL) +
theme(axis.text.x = element_text(angle = 30, hjust = 1))
Análisis. La correlación más fuerte entre las predictoras es la de Talla con Peso (r = 0.62), de sentido positivo: una relación de intensidad moderada o débil. La correlación entre peso y talla del recién nacido es de 0.62. Respecto al tipo de parto, la variable con mayor correlación (punto-biserial) es Talla (r = -0.14). Como esa correlación es débil (menor que 0.3 en valor absoluto), ninguna variable por sí sola explica el tipo de parto, lo que concuerda con un desempeño moderado de los modelos.
atipicos <- datos %>%
filter(Talla < 35 | Peso < 1000 | TiempoGestacion < 28 | EdadMadre > 45)
cat("Registros con valores extremos (talla < 35 cm, peso < 1000 g, gestación < 28 semanas o edad > 45):",
nrow(atipicos), "\n")
## Registros con valores extremos (talla < 35 cm, peso < 1000 g, gestación < 28 semanas o edad > 45): 11
kable(head(atipicos, 15), caption = "Registros con valores extremos (se muestran hasta 15)")
| TipoParto | EdadMadre | TiempoGestacion | ConsultasPrenatales | Peso | Talla |
|---|---|---|---|---|---|
| Vaginal | 24 | 25 | 3 | 730 | 35 |
| Cesarea | 19 | 27 | 5 | 1100 | 35 |
| Cesarea | 36 | 38 | 9 | 2780 | 22 |
| Vaginal | 22 | 37 | 5 | 3215 | 33 |
| Vaginal | 30 | 37 | 8 | 3080 | 32 |
| Vaginal | 22 | 27 | 6 | 985 | 33 |
| Vaginal | 22 | 27 | 6 | 1160 | 42 |
| Vaginal | 21 | 21 | 10 | 2630 | 49 |
| Vaginal | 29 | 25 | 0 | 760 | 29 |
| Cesarea | 51 | 39 | 6 | 3405 | 51 |
| Cesarea | 21 | 21 | 4 | 3020 | 49 |
Algunos valores extremos son clínicamente posibles (prematuridad extrema o madres de edad avanzada), pero otros, como tallas muy bajas, podrían ser errores de digitación. Se decidió conservarlos, por ser pocos y no poder verificarse contra la historia clínica, y se reconoce como limitación.
set.seed(params$semilla)
indices <- createDataPartition(datos$TipoParto, p = params$prop_train, list = FALSE)
entrenamiento <- datos[indices, ]
prueba <- datos[-indices, ]
tabla_particion <- tibble(
Conjunto = c("Entrenamiento", "Prueba"),
Observaciones = c(nrow(entrenamiento), nrow(prueba)),
Porcentaje = round(100 * c(nrow(entrenamiento), nrow(prueba)) / nrow(datos), 2),
Pct_cesarea = round(100 * c(mean(entrenamiento$TipoParto == "Cesarea"),
mean(prueba$TipoParto == "Cesarea")), 1)
)
kable(tabla_particion, caption = "Distribución de la muestra en entrenamiento y prueba")
| Conjunto | Observaciones | Porcentaje | Pct_cesarea |
|---|---|---|---|
| Entrenamiento | 1592 | 70.07 | 48.4 |
| Prueba | 680 | 29.93 | 48.4 |
La partición estratificada mantiene prácticamente la misma proporción de cesáreas en los dos conjuntos, por lo que la comparación posterior es justa.
En este paso se ajustan todos los modelos con la función
ajustar_modelos(). Aquí se presenta el Logit; el KNN se
muestra en la sección “Modelo KNN”.
mod_completo <- ajustar_modelos(variables)
res_completo <- mod_completo$resultados
modelo_logit <- mod_completo$logit
summary(modelo_logit)
##
## Call:
## glm(formula = f, family = binomial(link = "logit"), data = tr)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 14.5718851 2.0087392 7.254 4.04e-13 ***
## EdadMadre 0.0057303 0.0083588 0.686 0.4930
## TiempoGestacion -0.0969699 0.0416266 -2.330 0.0198 *
## ConsultasPrenatales 0.0416841 0.0221679 1.880 0.0601 .
## Peso 0.0012989 0.0001762 7.370 1.71e-13 ***
## Talla -0.3085378 0.0370064 -8.337 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 2205.3 on 1591 degrees of freedom
## Residual deviance: 2103.9 on 1586 degrees of freedom
## AIC: 2115.9
##
## Number of Fisher Scoring iterations: 4
# Ajuste global: ¿cuánto baja la devianza al incluir las variables?
dev_nula <- modelo_logit$null.deviance # modelo sin variables
dev_mod <- modelo_logit$deviance # modelo con las cinco variables
reduccion_dev <- 100 * (1 - dev_mod / dev_nula)
gl <- modelo_logit$df.null - modelo_logit$df.residual
# ¿La reducción es mayor de lo que se esperaría por azar? (chi-cuadrado, 5 % de significancia)
mejora_real <- (dev_nula - dev_mod) > qchisq(0.95, df = gl)
tam_mejora <- if (reduccion_dev < 10) "pequeña" else if (reduccion_dev < 20) "moderada" else "grande"
# Funciones auxiliares para redactar la interpretación a partir de los resultados
tabla_coef <- summary(modelo_logit)$coefficients
p_val <- setNames(tabla_coef[, "Pr(>|z|)"], rownames(tabla_coef))
# Valor p en formato legible: "p < 0.001" o "p = 0.034"
p_txt <- function(v) {
p <- p_val[[v]]
if (p < 0.001) "p < 0.001" else sprintf("p = %.3f", p)
}
# Frase de interpretación de un odds ratio (cambio porcentual en los odds de cesárea)
efecto <- function(v, or, frase) {
p <- p_val[[v]]
cambio <- round(abs(100 * (or - 1)), 1)
verbo <- if (or > 1) "aumenta" else "reduce"
signo <- if (or > 1) "+" else "-"
if (p < 0.05) {
sprintf("%s %s los odds de cesárea en promedio un %s %% (OR = %.2f; %s), manteniendo constantes las demás variables. La asociación es estadísticamente significativa.",
frase, verbo, cambio, or, p_txt(v))
} else if (p < 0.10) {
sprintf("%s, el cambio estimado en los odds de cesárea es de %s%s %% (OR = %.2f; %s). Es solo marginalmente significativo: no lo es al 5 %%, por lo que la evidencia de asociación es débil.",
frase, signo, cambio, or, p_txt(v))
} else {
sprintf("%s, el cambio estimado en los odds de cesárea es de %s%s %% (OR = %.2f; %s), pero no es estadísticamente significativo: no hay evidencia suficiente de asociación con el tipo de parto una vez controladas las demás variables.",
frase, signo, cambio, or, p_txt(v))
}
}
or_peso100 <- exp(100 * coef(modelo_logit)[["Peso"]])
or_talla <- exp(coef(modelo_logit)[["Talla"]])
or_gest <- exp(coef(modelo_logit)[["TiempoGestacion"]])
or_consultas <- exp(coef(modelo_logit)[["ConsultasPrenatales"]])
or_edad <- exp(coef(modelo_logit)[["EdadMadre"]])
signos_opuestos <- sign(coef(modelo_logit)[["Peso"]]) != sign(coef(modelo_logit)[["Talla"]])
texto_signos <- if (signos_opuestos) {
paste0(" Peso y talla tienen efectos de signo contrario, algo que suele deberse a su correlación (r = ",
round(cor_pt, 2), "); por eso se revisa el VIF más abajo.")
} else {
paste0(" Peso y talla están correlacionados (r = ", round(cor_pt, 2), "); por eso se revisa el VIF más abajo.")
}
Ajuste global. En el resumen del modelo, la devianza mide qué tan lejos queda el modelo de los datos observados (mientras menor, mejor). Sin ninguna variable, la devianza es 2205.3 (null deviance); al incluir las cinco variables baja a 2103.9 (residual deviance), una reducción de 4.6 %. Esa reducción es mayor de lo que se esperaría por azar, así que las variables sí aportan información sobre el tipo de parto, pero la mejora es pequeña: el desempeño de clasificación debe juzgarse con las métricas calculadas en la muestra de prueba.
Cómo leer los coeficientes. Los coeficientes del Logit están en escala de log-odds: un coeficiente positivo indica que la variable aumenta la probabilidad de cesárea y uno negativo, que la reduce, manteniendo constantes las demás variables. Como esa escala es difícil de leer, se transforman en razones de momios (odds ratios, OR = e elevado al coeficiente). El odds es la razón entre la probabilidad de cesárea y la de parto vaginal. Un OR mayor que 1 indica que la variable aumenta ese odds (asociación positiva con la cesárea), uno menor que 1 lo reduce (asociación negativa) y uno cercano a 1 indica que no hay efecto. Si el intervalo de confianza al 95 % de un OR incluye el 1, el resultado es compatible con ausencia de asociación.
coeficientes <- broom::tidy(modelo_logit, conf.int = TRUE, exponentiate = TRUE) %>%
mutate(Coeficiente = as.numeric(coef(modelo_logit)[term])) %>%
transmute(Variable = term, Coeficiente, Odds_Ratio = estimate,
IC_inferior = conf.low, IC_superior = conf.high, Valor_p = p.value) %>%
mutate(across(where(is.numeric), ~ round(.x, 4)))
kable(coeficientes, caption = "Coeficientes (log-odds), odds ratios e intervalos de confianza al 95 % del modelo Logit")
| Variable | Coeficiente | Odds_Ratio | IC_inferior | IC_superior | Valor_p |
|---|---|---|---|---|---|
| (Intercept) | 14.5719 | 2130538.0456 | 45369.8784 | 1.192896e+08 | 0.0000 |
| EdadMadre | 0.0057 | 1.0057 | 0.9894 | 1.022400e+00 | 0.4930 |
| TiempoGestacion | -0.0970 | 0.9076 | 0.8346 | 9.827000e-01 | 0.0198 |
| ConsultasPrenatales | 0.0417 | 1.0426 | 0.9983 | 1.089000e+00 | 0.0601 |
| Peso | 0.0013 | 1.0013 | 1.0010 | 1.001600e+00 | 0.0000 |
| Talla | -0.3085 | 0.7345 | 0.6824 | 7.890000e-01 | 0.0000 |
El OR de Peso en la tabla está expresado por gramo (por
eso queda muy cerca de 1); en los comentarios siguientes se reescala a
100 g para que sea más fácil de leer.
Peso del recién nacido: Cada 100 g adicionales de peso aumenta los odds de cesárea en promedio un 13.9 % (OR = 1.14; p < 0.001), manteniendo constantes las demás variables. La asociación es estadísticamente significativa.
Talla del recién nacido: Cada centímetro adicional de talla reduce los odds de cesárea en promedio un 26.5 % (OR = 0.73; p < 0.001), manteniendo constantes las demás variables. La asociación es estadísticamente significativa. Peso y talla tienen efectos de signo contrario, algo que suele deberse a su correlación (r = 0.62); por eso se revisa el VIF más abajo.
Tiempo de gestación: Cada semana adicional de gestación reduce los odds de cesárea en promedio un 9.2 % (OR = 0.91; p = 0.020), manteniendo constantes las demás variables. La asociación es estadísticamente significativa.
Consultas prenatales: Cada consulta prenatal adicional, el cambio estimado en los odds de cesárea es de +4.3 % (OR = 1.04; p = 0.060). Es solo marginalmente significativo: no lo es al 5 %, por lo que la evidencia de asociación es débil.
Edad de la madre: Cada año adicional de edad de la madre, el cambio estimado en los odds de cesárea es de +0.6 % (OR = 1.01; p = 0.493), pero no es estadísticamente significativo: no hay evidencia suficiente de asociación con el tipo de parto una vez controladas las demás variables.
El intercepto no tiene una interpretación práctica (corresponde a valores de todas las variables iguales a cero).
Diagnóstico de multicolinealidad. Valores del factor de inflación de la varianza (VIF) cercanos a 1 indican baja colinealidad; valores superiores a 5 o 10 indicarían un problema serio.
v <- car::vif(modelo_logit)
vif_max <- max(v)
var_vif <- names(v)[which.max(v)]
kable(data.frame(Variable = names(v), VIF = round(as.numeric(v), 2)),
caption = "Factor de inflación de la varianza (VIF)")
| Variable | VIF |
|---|---|
| EdadMadre | 1.05 |
| TiempoGestacion | 1.15 |
| ConsultasPrenatales | 1.04 |
| Peso | 1.94 |
| Talla | 1.88 |
El VIF más alto corresponde a Peso (1.94). Ningún valor supera 5, por lo que no hay evidencia de multicolinealidad que distorsione la interpretación de los coeficientes (la correlación entre peso y talla es r = 0.62).
Para KNN se compara el valor fijo \(k = 5\) con el valor que maximiza el AUC en validación cruzada dentro del conjunto de entrenamiento (sin usar la muestra de prueba).
res_cv <- mod_completo$cv$results
auc_cv_opt <- max(res_cv$ROC)
auc_cv_5 <- res_cv$ROC[res_cv$k == 5]
rango_cv <- max(res_cv$ROC) - min(res_cv$ROC)
txt_k <- paste0(
if (rango_cv < 0.03) {
paste0("El AUC cambia poco entre los valores de k probados (variación máxima de ",
sprintf("%.3f", rango_cv), "), así que el desempeño no es muy sensible a esa elección.")
} else {
paste0("El AUC varía de forma apreciable con k (variación máxima de ",
sprintf("%.3f", rango_cv), "), por lo que elegirlo por validación cruzada es importante.")
},
if (mod_completo$k_opt >= max(res_cv$k)) {
" El óptimo está en el extremo superior de los valores evaluados, por lo que valores mayores de k podrían rendir igual o mejor."
} else ""
)
p_k <- ggplot(mod_completo$cv$results, aes(x = k, y = ROC)) +
geom_line(color = verde_oscuro) +
geom_point(color = verde_medio) +
geom_vline(xintercept = mod_completo$k_opt, linetype = "dashed", color = "#F2A900") +
labs(x = "Número de vecinos (k)", y = "AUC (validación cruzada)")
ggplotly(p_k)
El valor óptimo fue k = 51, que corresponde a la línea punteada de la figura. Con ese valor, cada parto nuevo se clasifica por votación entre sus 51 vecinos más parecidos dentro de los 1592 partos de entrenamiento. En validación cruzada el AUC fue 0.651 con ese k, frente a 0.581 con k = 5. El AUC varía de forma apreciable con k (variación máxima de 0.095), por lo que elegirlo por validación cruzada es importante. El óptimo está en el extremo superior de los valores evaluados, por lo que valores mayores de k podrían rendir igual o mejor.
A diferencia del Logit, KNN no produce coeficientes ni odds ratios: no tiene una fórmula que se pueda leer, sino que clasifica comparando cada parto con los más parecidos del conjunto de entrenamiento. Por eso su interpretación se hace con dos herramientas: la importancia por permutación, que indica cuánto depende el modelo de cada variable, y un perfil de las predicciones, que indica en qué dirección se mueven las variables entre los partos que el modelo clasifica como cesárea y como vaginal.
Se desordenan al azar los valores de una variable en el conjunto de prueba (10 repeticiones) y se mide cuánto cae el AUC. Si la caída es grande, el modelo depende de esa variable; si es cercana a cero, la variable casi no influye en sus predicciones. Como criterio práctico, se considera relevante una variable cuya caída promedio supera 0.01 de AUC.
# AUC del KNN para unas matrices estandarizadas dadas
auc_knn <- function(tr_s, te_s, k) {
p <- class::knn(train = tr_s, test = te_s, cl = entrenamiento$TipoParto, k = k, prob = TRUE)
v <- attr(p, "prob")
prob <- ifelse(p == "Cesarea", v, 1 - v)
as.numeric(pROC::auc(pROC::roc(prueba$TipoParto, prob, levels = niveles,
direction = "<", quiet = TRUE)))
}
set.seed(params$semilla)
auc_base <- auc_knn(mod_completo$tr_s, mod_completo$te_s, mod_completo$k_opt)
caidas <- sapply(variables, function(v) {
replicate(10, {
te_perm <- mod_completo$te_s
te_perm[, v] <- sample(te_perm[, v]) # se desordena solo una variable
auc_base - auc_knn(mod_completo$tr_s, te_perm, mod_completo$k_opt)
})
})
colnames(caidas) <- variables
importancia <- data.frame(Variable = variables,
Caida_AUC = colMeans(caidas),
Desviacion = apply(caidas, 2, sd)) %>%
mutate(Relevante = ifelse(Caida_AUC > 0.01, "Sí", "No")) %>%
arrange(desc(Caida_AUC))
kable(importancia %>% mutate(across(where(is.numeric), ~ round(.x, 4))),
caption = "Importancia por permutación en KNN (caída promedio del AUC al desordenar cada variable)")
| Variable | Caida_AUC | Desviacion | Relevante | |
|---|---|---|---|---|
| Talla | Talla | 0.0890 | 0.0140 | Sí |
| TiempoGestacion | TiempoGestacion | 0.0335 | 0.0074 | Sí |
| Peso | Peso | 0.0284 | 0.0092 | Sí |
| ConsultasPrenatales | ConsultasPrenatales | 0.0207 | 0.0090 | Sí |
| EdadMadre | EdadMadre | 0.0145 | 0.0094 | Sí |
var_top <- importancia$Variable[1]
vars_rel_knn <- importancia$Variable[importancia$Relevante == "Sí"]
vars_sig_logit <- setdiff(names(p_val)[p_val < 0.05], "(Intercept)")
lista_txt <- function(x) if (length(x) == 0) "ninguna" else paste(x, collapse = ", ")
p_imp <- ggplot(importancia, aes(x = reorder(Variable, Caida_AUC), y = Caida_AUC,
text = paste0(Variable, ": ", round(Caida_AUC, 4)))) +
geom_col(fill = verde_medio) +
coord_flip() +
labs(x = NULL, y = "Caída del AUC al desordenar la variable")
ggplotly(p_imp, tooltip = "text")
El AUC base del KNN en prueba es 0.643. La variable de la que más depende el modelo es Talla (caída de 0.089). Las variables relevantes según el criterio anterior son: Talla, TiempoGestacion, Peso, ConsultasPrenatales, EdadMadre. Como la principal es una variable antropométrica del recién nacido, que no está disponible antes del parto, el modelo se apoya sobre todo en información posparto; esto se contrasta más adelante en el análisis de sensibilidad.
Esta medida debe leerse con cautela. Peso y talla están correlacionadas (r = 0.62), y al desordenar solo una de ellas la otra conserva parte de la información, por lo que la importancia individual de ambas puede estar subestimada.
La importancia no dice si una variable empuja hacia cesárea o hacia parto vaginal. Para verlo, se comparan los promedios de cada variable (en sus unidades originales) entre los partos de prueba que el KNN clasifica como cesárea y los que clasifica como vaginal.
perfil <- prueba %>%
select(all_of(variables)) %>%
mutate(Prediccion = mod_completo$kopt$pred) %>%
pivot_longer(-Prediccion, names_to = "Variable", values_to = "Valor") %>%
group_by(Variable, Prediccion) %>%
summarise(Media = mean(Valor), .groups = "drop") %>%
pivot_wider(names_from = Prediccion, values_from = Media) %>%
mutate(Diferencia = Cesarea - Vaginal,
Variable = factor(Variable, levels = variables)) %>%
arrange(Variable) %>%
mutate(across(where(is.numeric), ~ round(.x, 2)))
kable(perfil, caption = "Promedio de cada variable según la clase que predice el KNN (k óptimo), conjunto de prueba")
| Variable | Vaginal | Cesarea | Diferencia |
|---|---|---|---|
| EdadMadre | 23.59 | 24.42 | 0.83 |
| TiempoGestacion | 38.96 | 38.17 | -0.78 |
| ConsultasPrenatales | 6.20 | 6.38 | 0.18 |
| Peso | 3168.40 | 3180.48 | 12.08 |
| Talla | 50.77 | 49.42 | -1.35 |
unidades <- c(EdadMadre = "años", TiempoGestacion = "semanas", ConsultasPrenatales = "consultas",
Peso = "g", Talla = "cm")
fila_top <- perfil[perfil$Variable == var_top, ]
Por ejemplo, en los partos que el KNN clasifica como cesárea, Talla promedia 49.42 cm, frente a 50.77 cm en los clasificados como parto vaginal (diferencia de -1.35 cm). Estas diferencias describen lo que el modelo predice y no implican causalidad.
en_ambos <- intersect(vars_rel_knn, vars_sig_logit)
solo_knn <- setdiff(vars_rel_knn, vars_sig_logit)
solo_logit <- setdiff(vars_sig_logit, vars_rel_knn)
txt_coinc <- paste0(
if (length(en_ambos) > 0) paste0("Ambos modelos coinciden en: ", lista_txt(en_ambos), ". ") else "Los modelos no coinciden en ninguna variable. ",
if (length(solo_knn) > 0) paste0("Solo el KNN usa de forma relevante: ", lista_txt(solo_knn), ". ") else "",
if (length(solo_logit) > 0) paste0("Solo el Logit las marca como significativas: ", lista_txt(solo_logit), ". ") else ""
)
Variables relevantes para el KNN: Talla, TiempoGestacion, Peso, ConsultasPrenatales, EdadMadre. Variables significativas al 5 % en el Logit: TiempoGestacion, Peso, Talla. Ambos modelos coinciden en: Talla, TiempoGestacion, Peso. Solo el KNN usa de forma relevante: ConsultasPrenatales, EdadMadre. Las diferencias pueden deberse a que el Logit mide un efecto lineal promedio, mientras que el KNN captura relaciones locales y no lineales, y a que los criterios de relevancia de ambos métodos no son equivalentes.
La cesárea se considera la clase positiva. Se presentan los
resultados completos de caret para los dos modelos que se
comparan (KNN con k óptimo = 51 y Logit con umbral 0.5) y los conteos de
los cuatro modelos evaluados.
matriz_knn <- confusionMatrix(data = mod_completo$kopt$pred, reference = prueba$TipoParto,
positive = "Cesarea")
matriz_logit <- confusionMatrix(data = mod_completo$pred_l05, reference = prueba$TipoParto,
positive = "Cesarea")
cat("=== KNN (k óptimo =", mod_completo$k_opt, ") ===\n")
## === KNN (k óptimo = 51 ) ===
print(matriz_knn)
## Confusion Matrix and Statistics
##
## Reference
## Prediction Vaginal Cesarea
## Vaginal 194 106
## Cesarea 157 223
##
## Accuracy : 0.6132
## 95% CI : (0.5755, 0.65)
## No Information Rate : 0.5162
## P-Value [Acc > NIR] : 2.189e-07
##
## Kappa : 0.2294
##
## Mcnemar's Test P-Value : 0.002048
##
## Sensitivity : 0.6778
## Specificity : 0.5527
## Pos Pred Value : 0.5868
## Neg Pred Value : 0.6467
## Prevalence : 0.4838
## Detection Rate : 0.3279
## Detection Prevalence : 0.5588
## Balanced Accuracy : 0.6153
##
## 'Positive' Class : Cesarea
##
cat("\n=== Logit (umbral 0.5) ===\n")
##
## === Logit (umbral 0.5) ===
print(matriz_logit)
## Confusion Matrix and Statistics
##
## Reference
## Prediction Vaginal Cesarea
## Vaginal 242 146
## Cesarea 109 183
##
## Accuracy : 0.625
## 95% CI : (0.5874, 0.6615)
## No Information Rate : 0.5162
## P-Value [Acc > NIR] : 6.936e-09
##
## Kappa : 0.2466
##
## Mcnemar's Test P-Value : 0.02417
##
## Sensitivity : 0.5562
## Specificity : 0.6895
## Pos Pred Value : 0.6267
## Neg Pred Value : 0.6237
## Prevalence : 0.4838
## Detection Rate : 0.2691
## Detection Prevalence : 0.4294
## Balanced Accuracy : 0.6228
##
## 'Positive' Class : Cesarea
##
conteos <- function(pred) {
m <- table(factor(prueba$TipoParto, levels = niveles), factor(pred, levels = niveles))
c(VN = m[1, 1], FP = m[1, 2], FN = m[2, 1], VP = m[2, 2])
}
tabla_conteos <- rbind(
"KNN (k=5)" = conteos(mod_completo$k5$pred),
"KNN (k óptimo)" = conteos(mod_completo$kopt$pred),
"Logit (u=0.5)" = conteos(mod_completo$pred_l05),
"Logit (u óptimo)" = conteos(mod_completo$pred_lopt)
)
kable(tabla_conteos, caption = "Conteos de la matriz de confusión en prueba (VN, FP, FN, VP)")
| VN | FP | FN | VP | |
|---|---|---|---|---|
| KNN (k=5) | 190 | 161 | 124 | 205 |
| KNN (k óptimo) | 194 | 157 | 106 | 223 |
| Logit (u=0.5) | 242 | 109 | 146 | 183 |
| Logit (u óptimo) | 155 | 196 | 84 | 245 |
kable(round(res_completo, 4), caption = "Métricas de clasificación en el conjunto de prueba")
| Exactitud | Sensibilidad | Especificidad | Precision | F1 | AUC | |
|---|---|---|---|---|---|---|
| KNN (k=5) | 0.5809 | 0.6231 | 0.5413 | 0.5601 | 0.5899 | 0.6092 |
| KNN (k óptimo) | 0.6132 | 0.6778 | 0.5527 | 0.5868 | 0.6291 | 0.6425 |
| Logit (u=0.5) | 0.6250 | 0.5562 | 0.6895 | 0.6267 | 0.5894 | 0.6540 |
| Logit (u óptimo) | 0.5882 | 0.7447 | 0.4416 | 0.5556 | 0.6364 | 0.6540 |
metricas_largo <- as.data.frame(res_completo) %>%
rownames_to_column("Modelo") %>%
pivot_longer(-Modelo, names_to = "Metrica", values_to = "Valor") %>%
mutate(Modelo = factor(Modelo, levels = rownames(res_completo)),
Metrica = factor(Metrica, levels = colnames(res_completo)))
p_met <- ggplot(metricas_largo, aes(x = Metrica, y = Valor, fill = Modelo,
text = paste0(Modelo, ": ", round(Valor, 3)))) +
geom_col(position = "dodge") +
scale_fill_manual(values = colores_modelos) +
coord_cartesian(ylim = c(0, 1)) +
labs(x = NULL, y = "Valor de la métrica")
ggplotly(p_met, tooltip = "text")
Entre todas las configuraciones evaluadas, la de mayor exactitud fue Logit (u=0.5), la de mayor sensibilidad Logit (u óptimo), la de mayor especificidad Logit (u=0.5), la de mayor F1 Logit (u óptimo) y la de mayor AUC Logit (u=0.5).
Entre los dos modelos que se comparan (Logit con umbral 0.5 y KNN con \(k\) = 51), la regresión logística fue superior en: exactitud (0.625 frente a 0.613), especificidad (0.689 frente a 0.553), precisión (0.627 frente a 0.587), AUC (0.654 frente a 0.643). El KNN fue superior en: sensibilidad (0.556 frente a 0.678), F1 (0.589 frente a 0.629). En cuanto a los errores, el KNN produjo 157 falsos positivos y 106 falsos negativos, frente a 109 y 146 del Logit. Como referencia, el KNN con \(k = 5\) obtuvo una exactitud de 0.581 y un AUC de 0.609. Ambos modelos superan la tasa de no información (0.516); el kappa del Logit es 0.25, lo que corresponde a un acuerdo débil.
Efecto de la afinación. Al elegir k por validación cruzada (k = 51), el AUC de KNN fue 0.643 (frente a 0.609 con k = 5). Al desplazar el umbral del Logit a 0.43, la sensibilidad pasó de 0.556 a 0.745 y la especificidad de 0.689 a 0.442.
Lectura operativa. Las siguientes densidades muestran la probabilidad de cesárea que el Logit asigna a los partos de prueba, separada por el tipo de parto que realmente ocurrió. Si ambas curvas estuvieran bien separadas, el modelo discriminaría bien; el traslape entre ellas es lo que limita el AUC y el kappa.
dens <- tibble(Real = prueba$TipoParto, Probabilidad = as.numeric(mod_completo$p_te))
p_dens <- ggplot(dens, aes(x = Probabilidad, fill = Real)) +
geom_density(alpha = 0.6) +
geom_vline(xintercept = 0.5, linetype = "dashed") +
scale_fill_manual(values = colores_parto) +
labs(x = "Probabilidad predicha de cesárea (Logit)", y = "Densidad",
fill = "Tipo de parto real")
ggplotly(p_dens)
En términos del hospital, con el umbral usual de 0.5 para el Logit: de cada 100 cesáreas reales, el Logit logra anticipar 56 y el KNN (k = 51) 68. Y de cada 100 partos que el modelo señala como cesárea, realmente lo fueron 63 con el Logit y 59 con KNN.
rocs <- list(
"KNN (k=5)" = pROC::roc(prueba$TipoParto, as.numeric(mod_completo$k5$prob),
levels = niveles, direction = "<", quiet = TRUE),
"KNN (k óptimo)" = pROC::roc(prueba$TipoParto, as.numeric(mod_completo$kopt$prob),
levels = niveles, direction = "<", quiet = TRUE),
"Logit" = pROC::roc(prueba$TipoParto, as.numeric(mod_completo$p_te),
levels = niveles, direction = "<", quiet = TRUE)
)
names(rocs) <- paste0(names(rocs), " (AUC = ",
sapply(rocs, function(r) round(as.numeric(pROC::auc(r)), 3)), ")")
ggroc(rocs, legacy.axes = TRUE, linewidth = 1) +
geom_abline(intercept = 0, slope = 1, linetype = "dashed", color = "gray50") +
scale_color_manual(values = colores_modelos[1:3]) +
labs(x = "1 - Especificidad", y = "Sensibilidad", color = "Modelo")
Las probabilidades de KNN con k = 5 solo toman seis valores posibles (0, 0.2, …, 1), por lo que su curva ROC es muy escalonada. Con el \(k\) óptimo las probabilidades son múltiplos de \(1/k\) y la curva es más fina, aunque sigue siendo discreta frente al Logit, que produce probabilidades continuas.
El umbral de 0.5 no es obligatorio: moverlo cambia el equilibrio entre detectar más cesáreas (sensibilidad) y generar menos falsas alarmas (especificidad). La figura muestra ese intercambio para el modelo Logit en el conjunto de prueba.
real_ces <- prueba$TipoParto == "Cesarea"
umbrales <- seq(0.05, 0.95, by = 0.01)
curva_umbral <- purrr::map_dfr(umbrales, function(u) {
pred <- mod_completo$p_te >= u
tibble(Umbral = u,
Sensibilidad = sum(pred & real_ces) / sum(real_ces),
Especificidad = sum(!pred & !real_ces) / sum(!real_ces),
Exactitud = mean(pred == real_ces),
FP = sum(pred & !real_ces),
FN = sum(!pred & real_ces))
})
umbral_largo <- curva_umbral %>%
select(Umbral, Sensibilidad, Especificidad, Exactitud) %>%
pivot_longer(-Umbral, names_to = "Metrica", values_to = "Valor")
p_umb <- ggplot(umbral_largo, aes(x = Umbral, y = Valor, color = Metrica)) +
geom_line(linewidth = 1) +
geom_vline(xintercept = c(0.5, mod_completo$u_opt), linetype = "dashed", color = "gray50") +
scale_color_manual(values = colores_modelos[1:3]) +
labs(x = "Umbral de clasificación", y = "Valor", color = "Métrica")
ggplotly(p_umb)
Las líneas punteadas marcan el umbral usual (0.5) y el umbral de Youden (0.43).
Ejercicio ilustrativo de costos de error. Desde la gestión de operaciones, no todos los errores cuestan lo mismo. Como ejemplo, se supone que no prever una cesárea (falso negativo) cuesta el doble que reservar recursos sin necesidad (falso positivo). Estos costos son supuestos ilustrativos, en unidades relativas: el hospital debería reemplazarlos por sus costos reales (por ejemplo, el de reservar un quirófano que no se usa frente al de una cesárea de urgencia sin programar).
costo_FN <- 2 # supuesto: costo relativo de una cesárea no prevista
costo_FP <- 1 # supuesto: costo relativo de un recurso reservado sin necesidad
curva_umbral <- curva_umbral %>% mutate(Costo_total = costo_FN * FN + costo_FP * FP)
u_costo <- curva_umbral$Umbral[which.min(curva_umbral$Costo_total)]
p_cost <- ggplot(curva_umbral, aes(x = Umbral, y = Costo_total)) +
geom_line(color = verde_oscuro, linewidth = 1) +
geom_vline(xintercept = u_costo, linetype = "dashed", color = "#F2A900") +
labs(x = "Umbral de clasificación", y = "Costo total relativo de los errores")
ggplotly(p_cost)
Con esos supuestos, el costo total se minimiza con un umbral cercano a 0.39. Como este cálculo usa el conjunto de prueba, es solo una ilustración del efecto del umbral; en la práctica el umbral debería definirse con datos de entrenamiento o validación y con costos reales.
El peso y la talla del recién nacido solo se conocen una vez que ocurre el nacimiento, por lo que no podrían usarse para anticipar el tipo de parto. Para evaluar la utilidad real del modelo con fines de planeación, se repite todo el procedimiento usando únicamente edad de la madre, tiempo de gestación y consultas prenatales.
vars_ant <- c("EdadMadre", "TiempoGestacion", "ConsultasPrenatales")
mod_ant <- ajustar_modelos(vars_ant)
res_ant <- mod_ant$resultados
kable(round(res_ant, 4), caption = "Métricas con variables disponibles antes del parto (conjunto de prueba)")
| Exactitud | Sensibilidad | Especificidad | Precision | F1 | AUC | |
|---|---|---|---|---|---|---|
| KNN (k=5) | 0.5544 | 0.5562 | 0.5527 | 0.5382 | 0.5471 | 0.5674 |
| KNN (k óptimo) | 0.5794 | 0.5653 | 0.5926 | 0.5653 | 0.5653 | 0.5950 |
| Logit (u=0.5) | 0.5515 | 0.3739 | 0.7179 | 0.5541 | 0.4465 | 0.5610 |
| Logit (u óptimo) | 0.5353 | 0.6657 | 0.4131 | 0.5153 | 0.5809 | 0.5610 |
comparacion_auc <- data.frame(
Modelo = rownames(res_completo),
AUC_completo = round(res_completo[, "AUC"], 4),
AUC_antes_del_parto = round(res_ant[, "AUC"], 4)
)
kable(comparacion_auc, row.names = FALSE, caption = "AUC con y sin peso y talla del recién nacido")
| Modelo | AUC_completo | AUC_antes_del_parto |
|---|---|---|
| KNN (k=5) | 0.6092 | 0.5674 |
| KNN (k óptimo) | 0.6425 | 0.5950 |
| Logit (u=0.5) | 0.6540 | 0.5610 |
| Logit (u óptimo) | 0.6540 | 0.5610 |
delta_auc <- res_completo["Logit (u=0.5)", "AUC"] - res_ant["Logit (u=0.5)", "AUC"]
Al excluir peso y talla, el AUC del Logit pasó de 0.654 a 0.561. En el KNN con k óptimo (que en este caso es k = 47), el AUC pasó de 0.643 a 0.595. Esto muestra que una parte importante de la capacidad del modelo completo proviene de variables que no están disponibles antes del parto, por lo que su utilidad para planeación anticipada es limitada.
| Criterio | Logit (u=0.5) | KNN (k óptimo) | Mejor |
|---|---|---|---|
| Exactitud | 0.625 | 0.613 | Logit |
| Sensibilidad (detectar cesáreas) | 0.556 | 0.678 | KNN |
| Especificidad (evitar falsas alarmas) | 0.689 | 0.553 | Logit |
| AUC (discriminación global) | 0.654 | 0.643 | Logit |
| Interpretabilidad | Alta (odds ratios) | Baja (sin coeficientes) | Logit |
| Dependencia de hiperparámetros | Ninguna | Requiere elegir k (aquí por validación cruzada) | Logit |
| Control del umbral de decisión | Directo (probabilidades continuas) | Limitado (probabilidades discretas, múltiplos de 1/k) | Logit |
Recomendación: Se recomienda la regresión logística. Su AUC (0.654) es igual o superior al del KNN con k óptimo (0.643), se puede explicar a los profesionales de la salud mediante odds ratios, no depende de elegir un valor de k y permite ajustar el umbral de forma explícita. La diferencia de AUC es pequeña (0.011), por lo que, con una sola partición, no puede considerarse concluyente.
Si el hospital priorizara no dejar pasar ninguna cesárea aun aceptando más falsas alarmas, la recomendación no sería cambiar a KNN sino reducir el umbral del Logit (por ejemplo, al umbral de Youden), con lo que se controla el balance entre sensibilidad y especificidad usando el costo real de cada error.
Alcance Predictivo vs. Realidad Operativa. Aunque el modelo matemático demuestra que es factible encontrar patrones claros que relacionan las características materno-fetales con la vía del parto, el análisis de sensibilidad revela una limitación fundamental en la base de datos. Al excluir variables de resultado como el Peso y la Talla (información que clínicamente solo se confirma después del nacimiento), las variables estrictamente prenatales disponibles en el dataset (edad, consultas, semanas de gestación) resultan insuficientes para predecir con alta precisión si una paciente requerirá cesárea desde su ingreso.
Importancia de las Variables y Comportamiento Clínico. El análisis estadístico y la validación de supuestos (como la ausencia de multicolinealidad severa mediante el factor de inflación de la varianza, VIF) confirman que factores como la Edad de la Madre y el Peso del recién nacido son determinantes críticos en la vía de finalización del embarazo. Se evidenció estadísticamente que edades maternas más avanzadas y pesos fetales atípicamente altos elevan sustancialmente el riesgo de intervención quirúrgica. Esto concuerda con la literatura médica actual y valida la calidad de la base de datos utilizada para el entrenamiento.
El Valor del Análisis de Sensibilidad (Pre vs. Posparto). Uno de los hallazgos metodológicos más relevantes de este ejercicio fue la identificación de la naturaleza temporal de las variables. Si bien el modelo completo logra un rendimiento predictivo destacable, incluir el peso y la talla exactos del bebé representa un sesgo de información, ya que estos datos solo se conocen con exactitud tras el nacimiento. El análisis de sensibilidad demostró que un modelo predictivo verdaderamente útil para el triaje hospitalario debe calibrarse utilizando únicamente estimaciones ecográficas y variables prenatales (edad de la madre, número de consultas, semanas de gestación).
| Objetivo específico | Resultado principal | ¿Cumplido? |
|---|---|---|
| 1. Describir las variables | Se caracterizaron las cinco predictoras, sus correlaciones y variables categóricas adicionales; la muestra está balanceada (48 % cesáreas) | Sí |
| 2. Ajustar KNN y Logit | KNN (k = 5 y k = 51) y Logit (umbral 0.5 y 0.43) ajustados con datos de entrenamiento | Sí |
| 3. Comparar los modelos | Logit (u=0.5): exactitud 0.625, AUC 0.654, sensibilidad 0.556; KNN (k = 51): exactitud 0.613, AUC 0.643, sensibilidad 0.678 | Sí |
| 4. Usar solo variables previas al parto | AUC del Logit sin peso ni talla: 0.561 (con ellas: 0.654) | Sí |
| 5. Apoyar la planeación hospitalaria | Desempeño del Logit (u=0.5): exactitud 0.625, sensibilidad 0.556, AUC 0.654 | Parcial |
Respuesta a la pregunta de investigación. La respuesta a la viabilidad de anticipar la vía del parto con este conjunto de datos específico es mixta: estadísticamente las relaciones existen y son coherentes con la literatura médica, pero operativamente las variables prenatales capturadas se quedan cortas. Para que un modelo de esta naturaleza responda integralmente al problema de gestión de quirófanos y triage, es imperativo que los sistemas de información hospitalaria incluyan variables predictoras más robustas previas al parto, tales como estimaciones de peso por ecografía, antecedentes de cesáreas previas, dilatación al ingreso y comorbilidades maternas. Solo así el algoritmo dejará de ser un ejercicio estadístico para convertirse en una herramienta de apoyo a la decisión clínica.
Sobre el objetivo de anticipar. El peso y la talla del recién nacido solo se conocen tras el nacimiento. Al restringir el modelo a variables previas al parto, el AUC del Logit pasó de 0.654 a 0.561. Esto muestra que una parte importante de la capacidad del modelo completo proviene de variables posparto, por lo que su utilidad para planeación anticipada es limitada. Finalmente, la decisión de una cesárea depende de factores clínicos ausentes en la base (cesárea previa, presentación fetal, complicaciones obstétricas, decisión médica), lo que limita el desempeño de cualquier modelo construido con estas variables.
Barranquero Gómez, M., & Azaña Gutiérrez, S. (2023, 11 de octubre). ¿Cuál es el peso adecuado del bebé en el momento de nacer? Reproducción Asistida ORG. https://www.reproduccionasistida.org/pesos-en-el-nacimiento/#bebes-de-bajo-peso-al-nacer
Departamento Administrativo Nacional de Estadística (DANE). (2024). Estadísticas Vitales (nacimientos y defunciones): principales resultados IV trimestre 2023 [Comunicado de prensa]. https://www.dane.gov.co/files/operaciones/EEVV/cp-EEVV-IVtrim2023.pdf
Fox, J., & Weisberg, S. (2019). An R Companion to Applied Regression (3.ª ed.). Sage.
Hosmer, D. W., Lemeshow, S., & Sturdivant, R. X. (2013). Applied Logistic Regression (3.ª ed.). Wiley.
James, G., Witten, D., Hastie, T., & Tibshirani, R. (2021). An Introduction to Statistical Learning (2.ª ed.). Springer. https://www.statlearning.com/
Kuhn, M. (2008). Building predictive models in R using the caret package. Journal of Statistical Software, 28(5), 1-26. https://doi.org/10.18637/jss.v028.i05
Kuhn, M., & Johnson, K. (2013). Applied Predictive Modeling. Springer.
Ministerio de Salud, & Federación Colombiana de Sociedades de Obstetricia y Ginecología. (2014). Guía de atención del bajo peso al nacer (Guía No. 13). Ministerio de Salud y Protección Social de Colombia. https://www.minsalud.gov.co/sites/rid/Lists/BibliotecaDigital/RIDE/VS/PP/13Atencion%20del%20Bajo%20peso.pdf
Organización Mundial de la Salud (OMS). (2015). Declaración de la OMS sobre las tasas de cesárea. Ginebra: Organización Mundial de la Salud. https://www.who.int/es/publications/i/item/WHO-RHR-15.02
Robin, X., Turck, N., Hainard, A., et al. (2011). pROC: an open-source package for R and S+ to analyze and compare ROC curves. BMC Bioinformatics, 12, 77. https://doi.org/10.1186/1471-2105-12-77
Sievert, C. (2020). Interactive Web-Based Data Visualization with R, plotly, and shiny. Chapman and Hall/CRC.
Wickham, H., et al. (2019). Welcome to the tidyverse. Journal of Open Source Software, 4(43), 1686. https://doi.org/10.21105/joss.01686