1 1. Introducción y problemática

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.

2 2. Planteamiento del problema y pregunta de investigación

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?

3 3. Librerías

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
names(appointments)
##  [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"
str(appointments)
## '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
table(waiting_data$time_period, useNA = "ifany")
## 
##   morning afternoon     other 
##     17184     51644     17204
table(waiting_data$weekday, useNA = "ifany")
## 
##    jueves     lunes    martes miércoles   viernes 
##     17179     17231     17155     17193     17274
summary(waiting_data$waiting_time)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    0.60   12.60   33.50   44.09   64.60  297.30

4 5. Exploración analítica y estadísticas descriptivas

Se describe el comportamiento del tiempo de espera mediante medidas de tendencia central y dispersión robusta, complementadas con visualizaciones.

4.1 5.1 Medidas de tendencia central y dispersión (robustas)

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_wait

4.2 5.2 Histograma del tiempo de espera

ggplot(waiting_data, aes(x = waiting_time)) +
  geom_histogram(binwidth = 5) +
  labs(
    title = "Histograma del tiempo de espera (datos completos)",
    x = "Tiempo de espera (minutos)",
    y = "Frecuencia"
  ) +
  theme_minimal()

4.3 5.3 Boxplot del tiempo de espera (incluye outliers)

ggplot(waiting_data, aes(y = waiting_time)) +
  geom_boxplot(outlier.alpha = 0.6) +
  labs(
    title = "Boxplot del tiempo de espera (incluye outliers)",
    y = "Tiempo de espera (minutos)"
  ) +
  theme_minimal()

5 6. Detección de outliers (criterio de Tukey)

Se 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_resumen

5.1 6.1 Boxplot sin outliers (solo visualización)

waiting_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()

6 7. Modelamiento estadístico

Para responder la pregunta de investigación se ajustan dos modelos de regresión lineal:

La comparación se realiza con , ANOVA entre modelos anidados, y validación predictiva sencilla (train/test) con RMSE y MAE.

7 8. Modelo 1: efecto del periodo del día

modelo1 <- lm(waiting_time ~ time_period, data = waiting_data)
glance(modelo1)
tidy(modelo1)

7.1 8.1 Gráfica soporte Modelo 1 (diferencia afternoon vs morning)

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()

8 9. Modelo 2: efecto del periodo del día y día de la semana

modelo2 <- lm(waiting_time ~ time_period + weekday, data = waiting_data)
glance(modelo2)
tidy(modelo2)

8.1 9.1 Gráfica soporte Modelo 2 (weekday) sin outliers para lectura

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()

9 10. Validación, testeo y evaluación del desempeño

9.1 10.1 Comparación entre modelos (ANOVA)

anova(modelo1, modelo2)

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.

9.2 10.2 Evaluación predictiva (train/test) con RMSE y MAE

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_tbl

10 11. Gráfico dinámico: efectos predichos (Modelo 2)

El 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)

11 12. Conclusiones y coherencia con la problemática

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.