Los tiempos de espera en la atención ambulatoria constituyen un problema relevante en los servicios de salud, ya que afectan la oportunidad de la atención, la satisfacción del paciente y la eficiencia operativa. Demoras prolongadas pueden generar congestión, percepción negativa de la calidad y dificultades en la gestión de agendas médicas.
En una población de pacientes que asisten a citas médicas ambulatorias, se observa variabilidad en el tiempo de espera una vez el paciente llega al servicio. Sin embargo, no se tiene claridad sobre el papel que desempeñan factores operativos de la programación de la cita, como el periodo del día y el día de la semana, en dicha variabilidad.
Pregunta de investigación:
¿Qué factores asociados a la programación de la cita (periodo del día y
día de la semana) se relacionan con el tiempo de espera de los pacientes
que asisten a su cita médica?
library(readr)
library(dplyr)
library(ggplot2)
library(lubridate)
library(broom)
library(tidyr)
library(plotly)appointments <- readRDS(“appointments.rds”) ## 4. Carga de datos y preparación
appointments <- readRDS("appointments.rds")
# appointments <- read_csv("appointments.csv")
# Exploración inicial (estructura general)
dim(appointments)## [1] 111488 20
## [1] "appointment_id" "slot_id" "scheduling_date"
## [4] "appointment_date" "appointment_time" "scheduling_interval"
## [7] "status" "check_in_time" "appointment_duration"
## [10] "start_time" "end_time" "waiting_time"
## [13] "patient_id" "sex" "age"
## [16] "age_group" "age_decade" "weekday"
## [19] "hour" "time_period"
## 'data.frame': 111488 obs. of 20 variables:
## $ appointment_id : int 138 146 21 233 90 180 197 191 135 130 ...
## $ slot_id : int 1 23 24 25 26 27 28 29 30 22 ...
## $ scheduling_date : Date, format: "2014-12-28" "2014-12-29" ...
## $ appointment_date : Date, format: "2015-01-01" "2015-01-01" ...
## $ appointment_time : chr "8:00:00" "13:30:00" "13:45:00" "14:00:00" ...
## $ scheduling_interval : int 4 3 15 1 6 2 2 2 4 4 ...
## $ status : Factor w/ 5 levels "attended","cancelled",..: 3 3 1 1 2 1 1 1 2 1 ...
## $ check_in_time : chr "" "" "13:36:45" "13:59:32" ...
## $ appointment_duration: num NA NA 5.2 28.9 NA 7.7 4.2 27.1 NA 1.2 ...
## $ start_time : chr "" "" "13:37:57" "14:00:40" ...
## $ end_time : chr "" "" "13:43:09" "14:29:34" ...
## $ waiting_time : num NA NA 1.2 1.1 NA 21.7 16.2 1 NA 8.5 ...
## $ patient_id : int 8285 5972 6472 5376 8028 4317 7638 7061 2475 4217 ...
## $ sex : Factor w/ 2 levels "Female","Male": 2 2 2 1 2 1 2 2 1 1 ...
## $ age : int 37 84 77 37 72 51 28 33 29 90 ...
## $ age_group : Factor w/ 16 levels "15-19","20-24",..: 5 14 13 5 12 8 3 4 3 16 ...
## $ age_decade : num 30 80 70 30 70 50 20 30 20 90 ...
## $ weekday : Factor w/ 5 levels "jueves","lunes",..: 1 1 1 1 1 1 1 1 1 1 ...
## $ hour : num NA 13 13 14 14 14 14 15 15 13 ...
## $ time_period : Factor w/ 3 levels "morning","afternoon",..: 3 2 2 2 2 2 2 2 2 2 ...
# Limpieza de tipos y creación de variables derivadas para modelado
appointments <- appointments %>%
mutate(
appointment_date = as.Date(appointment_date),
scheduling_date = as.Date(scheduling_date),
status = factor(status),
sex = factor(sex),
age_group = factor(age_group),
weekday = factor(weekdays(appointment_date)),
hour = as.numeric(substr(as.character(appointment_time), 1, 2)),
time_period = case_when(
hour >= 6 & hour < 12 ~ "morning",
hour >= 12 & hour < 18 ~ "afternoon",
TRUE ~ "other"
),
time_period = factor(time_period, levels = c("morning", "afternoon", "other"))
)
# Subconjunto de análisis: asistieron + waiting_time válido
waiting_data <- appointments %>%
filter(
status == "attended",
!is.na(waiting_time),
waiting_time >= 0
)
dim(waiting_data)## [1] 86032 20
##
## morning afternoon other
## 17184 51644 17204
##
## jueves lunes martes miércoles viernes
## 17179 17231 17155 17193 17274
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.60 12.60 33.50 44.09 64.60 297.30
Se describe el comportamiento del tiempo de espera mediante medidas de tendencia central y dispersión robusta, complementadas con visualizaciones.
tendencia_wait <- waiting_data %>%
summarise(
n = n(),
media = mean(waiting_time, na.rm = TRUE),
mediana = median(waiting_time, na.rm = TRUE),
IQR = IQR(waiting_time, na.rm = TRUE)
)
tendencia_waitSe identifican valores extremos usando la regla 1.5 × IQR. Los outliers no se eliminan del modelado; se excluyen únicamente en una visualización para facilitar la lectura del comportamiento central.
Q1 <- quantile(waiting_data$waiting_time, 0.25, na.rm = TRUE)
Q3 <- quantile(waiting_data$waiting_time, 0.75, na.rm = TRUE)
IQRv <- Q3 - Q1
lim_inf <- Q1 - 1.5 * IQRv
lim_sup <- Q3 + 1.5 * IQRv
waiting_data <- waiting_data %>%
mutate(outlier_wait = waiting_time < lim_inf | waiting_time > lim_sup)
outliers_resumen <- waiting_data %>%
summarise(
outliers_n = sum(outlier_wait, na.rm = TRUE),
outliers_pct = mean(outlier_wait, na.rm = TRUE) * 100,
limite_inferior = lim_inf,
limite_superior = lim_sup
)
outliers_resumenwaiting_no_outliers <- waiting_data %>% filter(outlier_wait == FALSE)
ggplot(waiting_no_outliers, aes(y = waiting_time)) +
geom_boxplot() +
labs(
title = "Boxplot del tiempo de espera sin outliers",
subtitle = "Outliers excluidos únicamente para mejorar la lectura del comportamiento central",
y = "Tiempo de espera (minutos)"
) +
theme_minimal()Para responder la pregunta de investigación se ajustan dos modelos de regresión lineal:
waiting_time ~ time_periodwaiting_time ~ time_period + weekdayLa comparación se realiza con R², ANOVA entre modelos anidados, y validación predictiva sencilla (train/test) con RMSE y MAE.
medianas_modelo1 <- waiting_data %>%
group_by(time_period) %>%
summarise(mediana = median(waiting_time, na.rm = TRUE))
mean_morning <- coef(modelo1)["(Intercept)"]
mean_afternoon <- coef(modelo1)["(Intercept)"] + coef(modelo1)["time_periodafternoon"]
diff_afternoon <- mean_afternoon - mean_morning
ggplot(waiting_data, aes(x = time_period, y = waiting_time)) +
geom_boxplot() +
geom_text(
data = medianas_modelo1,
aes(x = time_period, y = mediana, label = round(mediana, 1)),
vjust = -0.5, size = 4
) +
annotate(
"segment",
x = 1, xend = 2,
y = mean_morning, yend = mean_afternoon,
linewidth = 1
) +
annotate(
"text",
x = 1.5,
y = (mean_morning + mean_afternoon) / 2,
label = paste0("Δ ≈ ", round(diff_afternoon, 1), " min"),
size = 4,
fontface = "bold"
) +
labs(
title = "Tiempo de espera por periodo del día (Modelo 1)",
subtitle = "Diferencia promedio estimada entre afternoon y morning",
x = "Periodo del día",
y = "Tiempo de espera (minutos)"
) +
theme_minimal()waiting_no_outliers_m2 <- waiting_data %>% filter(outlier_wait == FALSE)
medianas_m2 <- waiting_no_outliers_m2 %>%
group_by(weekday) %>%
summarise(mediana = median(waiting_time, na.rm = TRUE))
ggplot(waiting_no_outliers_m2, aes(x = weekday, y = waiting_time)) +
geom_boxplot() +
geom_text(
data = medianas_m2,
aes(x = weekday, y = mediana, label = round(mediana, 1)),
vjust = -0.6, size = 4
) +
labs(
title = "Tiempo de espera por día de la semana (Modelo 2)",
subtitle = "Outliers excluidos únicamente para resaltar el comportamiento central",
x = "Día de la semana",
y = "Tiempo de espera (minutos)"
) +
theme_minimal()Interpretación:
En bases de datos grandes, la incorporación de una variable adicional
puede producir aumentos pequeños en R². Aun así, la prueba ANOVA evalúa
si la variable adicional mejora significativamente el ajuste. Un p-valor
pequeño respalda que el Modelo 2 es estadísticamente
superior al Modelo 1.
set.seed(123)
n <- nrow(waiting_data)
idx_train <- sample(seq_len(n), size = round(0.8 * n))
train <- waiting_data[idx_train, ]
test <- waiting_data[-idx_train, ]
m1_train <- lm(waiting_time ~ time_period, data = train)
m2_train <- lm(waiting_time ~ time_period + weekday, data = train)
pred1 <- predict(m1_train, newdata = test)
pred2 <- predict(m2_train, newdata = test)
rmse <- function(y, yhat) sqrt(mean((y - yhat)^2, na.rm = TRUE))
mae <- function(y, yhat) mean(abs(y - yhat), na.rm = TRUE)
eval_tbl <- tibble(
modelo = c("Modelo 1", "Modelo 2"),
RMSE = c(rmse(test$waiting_time, pred1), rmse(test$waiting_time, pred2)),
MAE = c(mae(test$waiting_time, pred1), mae(test$waiting_time, pred2))
)
eval_tblEl gráfico muestra valores predichos (estimaciones
promedio del modelo) para cada combinación de time_period y
weekday, lo cual permite explorar el efecto conjunto de
ambas variables de forma interactiva.
mf <- model.frame(modelo2)
mf$time_period <- as.factor(mf$time_period)
mf$weekday <- as.factor(mf$weekday)
pred_data <- expand.grid(
time_period = levels(mf$time_period),
weekday = levels(mf$weekday)
)
pred_data$tiempo_predicho <- predict(modelo2, newdata = pred_data)
pred_peak <- pred_data %>%
slice_max(order_by = tiempo_predicho, n = 1, with_ties = FALSE)
p <- ggplot(pred_data, aes(x = time_period, y = tiempo_predicho, color = weekday, group = weekday)) +
geom_line() +
geom_point() +
geom_text(
data = pred_peak,
aes(label = paste0("Pico: ", round(tiempo_predicho, 1), " min (", weekday, "-", time_period, ")")),
hjust = -0.15, vjust = -0.6, color = "black", size = 4
) +
labs(
title = "Efectos predichos por time_period y weekday (Modelo 2)",
x = "Periodo del día (time_period)",
y = "Tiempo de espera predicho (minutos)",
color = "Día de la semana (weekday)"
) +
theme_minimal()
ggplotly(p)El análisis evidenció que el periodo del día se asocia de forma relevante con el tiempo de espera: la tarde concentra mayores tiempos de espera promedio frente a la mañana. La inclusión del día de la semana en el Modelo 2 refina el análisis e incorpora variación adicional (moderada pero consistente).
Aunque la mejora en R² puede ser pequeña —algo esperable en bases de datos grandes y con muchos determinantes no observados—, la prueba ANOVA demuestra que el Modelo 2 mejora significativamente el ajuste respecto al Modelo 1. Adicionalmente, la validación predictiva simple (train/test) permite contrastar desempeño mediante RMSE/MAE, apoyando la evaluación de la capacidad predictiva.
En conjunto, estos resultados aportan evidencia cuantitativa para comprender la problemática de tiempos de espera y respaldan la toma de decisiones operativas para mejorar la organización de agendas médicas.