Panel de control

Row

Citas perdidas (meta < 15 %)

22.3 %

Espera media (meta < 30 min)

44.1 min

Pacientes con espera > 30 min (meta < 25 %)

53.8 %

Ocupación de la agenda (meta ≤ 90 %)

Row

La espera supera la meta desde media mañana

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

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

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)

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.

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