Evidencia: la espera crece hora a hora y supera 30 min desde las 10:00. Relevancia: la tarde concentra las quejas. Acción: cupos de amortiguación a media mañana y al inicio de la tarde.
Evidencia: ~22 % de la agenda se pierde cada mes desde 2015. Relevancia: capacidad pagada sin producción. Acción: recordatorios universales y 2 cupos de sobreagendamiento diarios.
Evidencia: en los días con más de 600 min de consulta la espera media se multiplica. Acción: ocupación objetivo ≤ 90 % (≈ 540 min por día).
Evidencia: la espera sube con la hora y es igual todos los días. Acción: aplicar el rediseño de agenda en todos los días.
Atención: el problema. Más de la mitad de los pacientes espera más de 30 minutos y se pierde ~1 de cada 5 citas. Audiencia: Gerencia, Dirección Científica, Coordinación de Consulta Externa y Oficina de Calidad.
Análisis: la causa. La consulta promedio (~17,5 min) no cabe en la franja de 15 min. La agenda opera cerca del 97 % de su capacidad, así que cada retraso se arrastra hasta el final del día. La espera no depende del paciente (edad, sexo) ni del día de la semana, sino de la hora y de la carga acumulada.
Acción: las decisiones.
Informe completo con el análisis estadístico y el modelo: Análisis de datos para la gestión de citas médicas.
---
title: "Tablero de gestión: espera y capacidad en consulta externa"
author: "Camilo Aponte y Andrea Camacho"
output:
flexdashboard::flex_dashboard:
orientation: rows
vertical_layout: fill
theme: flatly
source_code: embed
---
```{r setup, include=FALSE}
# Problema (modelo A³ - Atención): reducir la espera de los pacientes y recuperar la capacidad
# perdida en la consulta externa.
# Audiencia: Gerencia, Dirección Científica, Coordinación de Consulta Externa y Oficina de Calidad.
knitr::opts_chunk$set(echo = FALSE, message = FALSE, warning = FALSE)
library(tidyverse)
library(flexdashboard)
library(plotly)
citas <- read_csv("appointments.csv") %>%
mutate(across(where(hms::is_hms), as.character),
status = fct_recode(factor(status), "Atendida" = "attended", "Cancelada" = "cancelled",
"No asistió" = "did not attend", "Programada" = "scheduled",
"Desconocido" = "unknown"),
hora = as.integer(substr(appointment_time, 1, 2)),
mes = format(as.Date(appointment_date), "%Y-%m"),
dia_semana = factor(weekdays(as.Date(appointment_date))))
atendidas <- citas %>% filter(status == "Atendida")
# Indicadores
p_perdida <- 100 * mean(citas$status %in% c("Cancelada", "No asistió"))
esp_media <- mean(atendidas$waiting_time)
p_30 <- 100 * mean(atendidas$waiting_time > 30)
dia <- atendidas %>% group_by(appointment_date) %>%
summarise(min_consulta = sum(appointment_duration), espera = mean(waiting_time), citas = n()) %>%
mutate(estado = factor(ifelse(min_consulta > 600, "Sobrecarga (> 600 min)", "Dentro de la capacidad"),
levels = c("Sobrecarga (> 600 min)", "Dentro de la capacidad")))
ocupacion <- 100 * mean(dia$min_consulta) / 600
```
Panel de control
=====================================
Row {data-height=160}
-------------------------------------
### Citas perdidas (meta < 15 %)
```{r}
valueBox(paste0(round(p_perdida, 1), " %"), icon = "fa-calendar-times-o",
color = ifelse(p_perdida < 15, "success", "danger"))
```
### Espera media (meta < 30 min)
```{r}
valueBox(paste0(round(esp_media, 1), " min"), icon = "fa-clock-o",
color = ifelse(esp_media < 30, "success", "danger"))
```
### Pacientes con espera > 30 min (meta < 25 %)
```{r}
valueBox(paste0(round(p_30, 1), " %"), icon = "fa-hourglass-half",
color = ifelse(p_30 < 25, "success", "danger"))
```
### Ocupación de la agenda (meta ≤ 90 %)
```{r}
gauge(round(ocupacion, 1), min = 0, max = 100, symbol = "%",
sectors = gaugeSectors(success = c(0, 85), warning = c(85, 90), danger = c(90, 100)))
```
Row
-------------------------------------
### La espera supera la meta desde media mañana
```{r}
esp_h <- atendidas %>% group_by(hora) %>% summarise(espera = mean(waiting_time)) %>%
mutate(meta = factor(ifelse(espera > 30, "Supera la meta", "Dentro de la meta"),
levels = c("Supera la meta", "Dentro de la meta")))
g1 <- ggplot(esp_h, aes(factor(hora), espera, fill = meta,
text = paste0("Hora: ", hora, ":00<br>Espera media: ", round(espera, 1), " min"))) +
geom_col(width = 0.7) +
geom_hline(yintercept = 30, linetype = "dashed") +
scale_fill_brewer(palette = "RdBu") +
labs(x = "Hora de la cita", y = "Espera media (min)", fill = NULL) + theme_minimal()
ggplotly(g1, tooltip = "text")
```
> **Evidencia:** la espera crece hora a hora y supera 30 min desde las 10:00. **Relevancia:** la tarde concentra las quejas. **Acción:** cupos de amortiguación a media mañana y al inicio de la tarde.
### La pérdida de capacidad no mejora con el tiempo
```{r}
perd_mes <- citas %>% filter(mes != max(mes)) %>% group_by(mes) %>%
summarise(pct = 100 * mean(status %in% c("Cancelada", "No asistió"))) %>%
mutate(fecha = as.Date(paste0(mes, "-01")))
g2 <- ggplot(perd_mes, aes(fecha, pct, group = 1,
text = paste0("Mes: ", mes, "<br>Citas perdidas: ", round(pct, 1), " %"))) +
geom_line(color = "grey55") +
geom_hline(yintercept = mean(perd_mes$pct), linetype = "dashed", color = "#2C7FB8") +
labs(x = NULL, y = "% de citas perdidas") + theme_minimal()
ggplotly(g2, tooltip = "text")
```
> **Evidencia:** ~22 % de la agenda se pierde cada mes desde 2015. **Relevancia:** capacidad pagada sin producción. **Acción:** recordatorios universales y 2 cupos de sobreagendamiento diarios.
Días críticos
=====================================
Column
-------------------------------------
### Cuando la carga del día supera la jornada (600 min), la espera se dispara
```{r}
g3 <- ggplot(dia, aes(min_consulta, espera, color = estado,
text = paste0("Fecha: ", appointment_date, "<br>Minutos de consulta: ", round(min_consulta),
"<br>Espera media: ", round(espera, 1), " min<br>Citas: ", citas))) +
geom_point(alpha = 0.4, size = 1.2) +
geom_vline(xintercept = 600, linetype = "dashed") +
scale_color_brewer(palette = "Set1") +
labs(x = "Minutos totales de consulta en el día", y = "Espera media del día (min)", color = NULL) +
theme_minimal()
ggplotly(g3, tooltip = "text")
```
> **Evidencia:** en los días con más de 600 min de consulta la espera media se multiplica. **Acción:** ocupación objetivo ≤ 90 % (≈ 540 min por día).
Column
-------------------------------------
### Espera media por hora y día de la semana (3D, se puede girar)
```{r}
sup <- atendidas %>% group_by(dia_semana, hora) %>%
summarise(espera = mean(waiting_time), .groups = "drop") %>%
pivot_wider(names_from = hora, values_from = espera)
z <- as.matrix(sup[, -1])
plot_ly(x = as.numeric(colnames(z)), y = seq_len(nrow(z)), z = z, type = "surface", colorscale = "YlOrRd") %>%
layout(scene = list(xaxis = list(title = "Hora"),
yaxis = list(title = "Día", tickvals = seq_len(nrow(z)), ticktext = as.character(sup$dia_semana)),
zaxis = list(title = "Espera (min)")))
```
> **Evidencia:** la espera sube con la hora y es igual todos los días. **Acción:** aplicar el rediseño de agenda en todos los días.
Historia y decisiones
=====================================
Column
-------------------------------------
### Historia en tres actos (modelo A³)
**Atención: el problema.** Más de la mitad de los pacientes espera más de 30 minutos y se pierde ~1 de cada 5 citas. **Audiencia:** Gerencia, Dirección Científica, Coordinación de Consulta Externa y Oficina de Calidad.
**Análisis: la causa.** La consulta promedio (~17,5 min) no cabe en la franja de 15 min. La agenda opera cerca del 97 % de su capacidad, así que cada retraso se arrastra hasta el final del día. La espera no depende del paciente (edad, sexo) ni del día de la semana, sino de la hora y de la carga acumulada.
**Acción: las decisiones.**
1. **Rediseñar la agenda para bajar la ocupación al 90 %:** franjas de 20 min en las primeras horas o dos cupos de amortiguación diarios, compensados con 2 cupos de sobreagendamiento que aprovechan las inasistencias predecibles. Meta: espera media < 30 min en seis meses.
2. **Gobernar con datos:** revisar este tablero cada mes en el comité de calidad, alertar durante la jornada cuando la espera prevista supere 30 min y enviar recordatorios a todos los pacientes. Meta: citas perdidas < 15 %.
Informe completo con el análisis estadístico y el modelo: [Análisis de datos para la gestión de citas médicas](https://rpubs.com/Camilobedo123/eda-citas-medicas-semana1).