1 Análisis Exploratorio de Datos (EDA)

El EDA (Exploratory Data Analysis, o análisis exploratorio de datos) es la primera revisión de una base de datos: sirve para conocer qué variables tiene, cómo están organizadas, si hay datos faltantes o valores raros, y qué patrones se ven a simple vista en gráficos. No incluye pruebas estadísticas; esas se trabajarán en el Tema 2.

1.1 Contexto, fuente y preparación de los datos

1.1.1 Fuente de información

  • Notebook de referencia: EDA Medical Appointment System Dataset (Kaggle, autora Carolina González Galtier).
  • Base de datos: Medical Appointment Scheduling System (Kaggle). Contiene registros simulados de un consultorio: las citas agendadas, el estado de cada cita (atendida, cancelada, no asistió…), los tiempos de espera y la duración de la consulta.
  • Archivo usado: appointments.csv (una fila = una cita médica).

Para la gestión de recursos sanitarios, esta base sirve para mirar la demanda (cuántas citas se piden y cuándo), el aprovechamiento de la capacidad instalada (citas atendidas frente a citas perdidas) y la eficiencia operativa (tiempos de espera y de consulta).

1.1.2 Preparación del entorno

Paquetes solicitados en la actividad, más plotly (para que los gráficos sean interactivos) y knitr (para tablas).

# Si es la primera vez, instalar (se ejecuta una sola vez en la consola):
# install.packages(c("tidyverse", "ggplot2", "dplyr", "skimr", "janitor", "plotly", "knitr"))

library(tidyverse)   # conjunto de paquetes para manejar datos
library(ggplot2)     # gráficos
library(dplyr)       # transformar tablas (filtrar, agrupar, resumir)
library(skimr)       # resumen rápido de la base
library(janitor)     # limpieza de nombres y tablas de frecuencia
library(plotly)      # gráficos interactivos
library(knitr)       # tablas en el informe

1.1.3 Importación de los datos

El archivo CSV (valores separados por comas) debe estar en la misma carpeta que este archivo .Rmd.

citas <- read_csv("appointments.csv") %>%
  clean_names()          # deja los nombres de columnas en minúscula y sin espacios

# Variables derivadas útiles para el análisis
citas <- citas %>%
  mutate(
    across(where(hms::is_hms), as.character),   # horas como texto (evita errores en summary)
    status       = factor(status),
    sex          = factor(sex),
    age_group    = factor(age_group, levels = unique(age_group[order(age)])),
    dia_semana   = factor(weekdays(as.Date(appointment_date)),
                          levels = c("lunes", "martes", "miércoles", "jueves",
                                     "viernes", "sábado", "domingo",
                                     "Monday", "Tuesday", "Wednesday", "Thursday",
                                     "Friday", "Saturday", "Sunday")) %>% droplevels(),
    mes          = format(as.Date(appointment_date), "%Y-%m")
  )

dim(citas)   # número de filas (citas) y columnas (variables)
## [1] 111488     18

La base tiene 111.488 citas y 18 columnas, con fechas de cita entre 2015-01-01 y 2024-12-24.

1.2 Estructura y descripción de las variables

1.2.1 Estructura de la base: str() y glimpse()

str() y glimpse() muestran el nombre de cada variable, su tipo (número, texto, fecha) y los primeros valores.

glimpse(citas)
## Rows: 111,488
## Columns: 18
## $ appointment_id       <chr> "0000138", "0000146", "0000021", "0000233", "0000…
## $ slot_id              <chr> "0000001", "0000023", "0000024", "0000025", "0000…
## $ scheduling_date      <date> 2014-12-28, 2014-12-29, 2014-12-17, 2014-12-31, …
## $ appointment_date     <date> 2015-01-01, 2015-01-01, 2015-01-01, 2015-01-01, …
## $ appointment_time     <chr> "08:00:00", "13:30:00", "13:45:00", "14:00:00", "…
## $ scheduling_interval  <dbl> 4, 3, 15, 1, 6, 2, 2, 2, 4, 4, 4, 1, 1, 7, 3, 2, …
## $ status               <fct> did not attend, did not attend, attended, attende…
## $ check_in_time        <chr> NA, NA, "13:36:45", "13:59:32", NA, "14:08:53", "…
## $ appointment_duration <dbl> NA, NA, 5.2, 28.9, NA, 7.7, 4.2, 27.1, NA, 1.2, 7…
## $ start_time           <chr> NA, NA, "13:37:57", "14:00:40", NA, "14:30:38", "…
## $ end_time             <chr> NA, NA, "13:43:09", "14:29:34", NA, "14:38:20", "…
## $ waiting_time         <dbl> NA, NA, 1.2, 1.1, NA, 21.7, 16.2, 1.0, NA, 8.5, 2…
## $ patient_id           <chr> "08285", "05972", "06472", "05376", "08028", "043…
## $ sex                  <fct> Male, Male, Male, Female, Male, Female, Male, Mal…
## $ age                  <dbl> 37, 84, 77, 37, 72, 51, 28, 33, 29, 90, 66, 64, 3…
## $ age_group            <fct> 35-39, 80-84, 75-79, 35-39, 70-74, 50-54, 25-29, …
## $ dia_semana           <fct> jueves, jueves, jueves, jueves, jueves, jueves, j…
## $ mes                  <chr> "2015-01", "2015-01", "2015-01", "2015-01", "2015…

1.2.2 Resumen general: summary()

summary() entrega, para cada variable numérica, el mínimo, el máximo y los valores centrales; para las de texto, cuántos registros tiene.

summary(citas)
##    appointment_id        slot_id       scheduling_date     
##  Length   :111488   Length   :111488   Min.   :2014-12-09  
##  N.unique :111488   N.unique :103646   1st Qu.:2017-06-18  
##  N.blank  :     0   N.blank  :     0   Median :2019-12-14  
##  Min.nchar:     7   Min.nchar:     7   Mean   :2019-12-13  
##  Max.nchar:     7   Max.nchar:     7   3rd Qu.:2022-06-08  
##                                        Max.   :2024-12-01  
##                                                            
##  appointment_date      appointment_time  scheduling_interval
##  Min.   :2015-01-01   Length   :111488   Min.   : 1.00      
##  1st Qu.:2017-06-26   N.unique :    40   1st Qu.: 2.00      
##  Median :2019-12-20   N.blank  :     0   Median : 5.00      
##  Mean   :2019-12-20   Min.nchar:     8   Mean   : 7.19      
##  3rd Qu.:2022-06-15   Max.nchar:     8   3rd Qu.:10.00      
##  Max.   :2024-12-24                      Max.   :30.00      
##                                                             
##             status        check_in_time    appointment_duration
##  attended      :86032   Length   :111488   Min.   : 0.00       
##  cancelled     :18254   N.unique : 33251   1st Qu.: 8.60       
##  did not attend: 6615   N.blank  :     0   Median :15.80       
##  scheduled     :  141   Min.nchar:     8   Mean   :17.48       
##  unknown       :  446   Max.nchar:     8   3rd Qu.:24.70       
##                         NAs      : 25456   Max.   :58.70       
##                                            NAs    :25456       
##      start_time          end_time       waiting_time        patient_id    
##  Length   :111488   Length   :111488   Min.   :  0.60   Length   :111488  
##  N.unique : 35984   N.unique : 36852   1st Qu.: 12.60   N.unique : 36697  
##  N.blank  :     0   N.blank  :     0   Median : 33.50   N.blank  :     0  
##  Min.nchar:     8   Min.nchar:     8   Mean   : 44.09   Min.nchar:     5  
##  Max.nchar:     8   Max.nchar:     8   3rd Qu.: 64.60   Max.nchar:     5  
##  NAs      : 25456   NAs      : 25456   Max.   :297.30                     
##                                        NAs    :25456                      
##      sex             age           age_group         dia_semana   
##  Female:66086   Min.   : 15.00   75-79  : 9844   lunes    :22293  
##  Male  :45402   1st Qu.: 40.00   60-64  : 9649   martes   :22342  
##                 Median : 59.00   70-74  : 9485   miércoles:22190  
##                 Mean   : 57.21   65-69  : 9133   jueves   :22313  
##                 3rd Qu.: 74.00   55-59  : 8955   viernes  :22350  
##                 Max.   :100.00   80-84  : 8140                    
##                                  (Other):56282                    
##         mes        
##  Length   :111488  
##  N.unique :   120  
##  N.blank  :     0  
##  Min.nchar:     7  
##  Max.nchar:     7  
##                    
## 

1.2.3 Resumen completo con skimr::skim()

skim() agrega el número de datos faltantes (NA, celdas vacías) de cada variable y un mini histograma.

skim(citas)
Data summary
Name citas
Number of rows 111488
Number of columns 18
_______________________
Column type frequency:
character 8
Date 2
factor 4
numeric 4
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
appointment_id 0 1.00 7 7 0 111488 0
slot_id 0 1.00 7 7 0 103646 0
appointment_time 0 1.00 8 8 0 40 0
check_in_time 25456 0.77 8 8 0 33251 0
start_time 25456 0.77 8 8 0 35984 0
end_time 25456 0.77 8 8 0 36852 0
patient_id 0 1.00 5 5 0 36697 0
mes 0 1.00 7 7 0 120 0

Variable type: Date

skim_variable n_missing complete_rate min max median n_unique
scheduling_date 0 1 2014-12-09 2024-12-01 2019-12-14 3642
appointment_date 0 1 2015-01-01 2024-12-24 2019-12-20 2604

Variable type: factor

skim_variable n_missing complete_rate ordered n_unique top_counts
status 0 1 FALSE 5 att: 86032, can: 18254, did: 6615, unk: 446
sex 0 1 FALSE 2 Fem: 66086, Mal: 45402
age_group 0 1 FALSE 16 75-: 9844, 60-: 9649, 70-: 9485, 65-: 9133
dia_semana 0 1 FALSE 5 vie: 22350, mar: 22342, jue: 22313, lun: 22293

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
scheduling_interval 0 1.00 7.19 6.15 1.0 2.0 5.0 10.0 30.0 ▇▃▂▁▁
appointment_duration 25456 0.77 17.48 11.06 0.0 8.6 15.8 24.7 58.7 ▇▇▅▂▁
waiting_time 25456 0.77 44.09 40.79 0.6 12.6 33.5 64.6 297.3 ▇▂▁▁▁
age 0 1.00 57.21 20.16 15.0 40.0 59.0 74.0 100.0 ▅▆▇▇▂

1.2.4 Diccionario de variables

Variable Tipo Descripción
appointment_id Identificador Código único de la cita
slot_id Identificador Código del espacio (franja horaria) de agenda
scheduling_date Fecha Día en que el paciente pidió la cita
appointment_date Fecha Día programado de la cita
appointment_time Hora Hora programada de la cita
scheduling_interval Numérica Días entre la solicitud y la cita (anticipación)
status Categórica Estado de la cita: atendida, cancelada, no asistió, etc.
check_in_time Hora Hora de llegada del paciente
appointment_duration Numérica Duración de la consulta (minutos)
start_time / end_time Hora Hora real de inicio y fin de la consulta
waiting_time Numérica Minutos de espera del paciente antes de ser atendido
patient_id Identificador Código del paciente
sex Categórica Sexo del paciente
age Numérica Edad del paciente (años)
age_group Categórica Grupo de edad
dia_semana / mes Derivada Variables creadas en este informe a partir de la fecha de la cita

1.2.5 Calidad de los datos: valores faltantes

faltantes <- citas %>%
  summarise(across(everything(), ~ sum(is.na(.)))) %>%
  pivot_longer(everything(), names_to = "variable", values_to = "n_faltantes") %>%
  mutate(porcentaje = round(100 * n_faltantes / nrow(citas), 1)) %>%
  arrange(desc(n_faltantes))

kable(faltantes)
variable n_faltantes porcentaje
check_in_time 25456 22.8
appointment_duration 25456 22.8
start_time 25456 22.8
end_time 25456 22.8
waiting_time 25456 22.8
appointment_id 0 0.0
slot_id 0 0.0
scheduling_date 0 0.0
appointment_date 0 0.0
appointment_time 0 0.0
scheduling_interval 0 0.0
status 0 0.0
patient_id 0 0.0
sex 0 0.0
age 0 0.0
age_group 0 0.0
dia_semana 0 0.0
mes 0 0.0

Comentario: los faltantes se concentran en las variables de la atención (check_in_time, start_time, end_time, waiting_time, appointment_duration). Esto es lógico: si la cita fue cancelada o el paciente no asistió, no hay hora de llegada ni duración. No son errores de digitación, así que no se deben rellenar; los análisis de tiempos se hacen solo con las citas atendidas.

1.3 Visualizaciones interactivas y comentarios

Todos los gráficos son interactivos: al pasar el mouse se ven los valores, se puede hacer zoom arrastrando, y doble clic para volver a la vista original.

1.3.1 Gráfico 1. Frecuencia de citas según su estado (aprovechamiento de la capacidad)

estado <- citas %>%
  tabyl(status) %>%                         # tabla de frecuencias con janitor
  arrange(desc(n)) %>%
  mutate(porcentaje = round(100 * percent, 1))

kable(estado %>% select(status, n, porcentaje))
status n porcentaje
attended 86032 77.2
cancelled 18254 16.4
did not attend 6615 5.9
unknown 446 0.4
scheduled 141 0.1
g1 <- ggplot(estado, aes(x = reorder(status, n), y = n,
                         text = paste0("Estado: ", status,
                                       "<br>Citas: ", n,
                                       "<br>%: ", porcentaje))) +
  geom_col(fill = "#2C7FB8") +
  coord_flip() +
  labs(title = "Número de citas por estado", x = "Estado de la cita", y = "Número de citas") +
  theme_minimal()

ggplotly(g1, tooltip = "text")

Comentario: el 77.2% de las citas fueron atendidas; el 16.4% se cancelaron y el 5.9% corresponde a pacientes que no asistieron (inasistencia). Cada cita cancelada o perdida es una franja de agenda (tiempo de médico y consultorio) que no produce atención. Para la gestión, estos porcentajes muestran cuánta capacidad instalada se pierde y justifican acciones como recordatorios por mensaje de texto, listas de espera para reasignar cupos o sobreagendamiento controlado.

1.3.2 Gráfico 2. Demanda mensual de citas en el tiempo (uso de las instalaciones)

# Se excluye el último mes porque está incompleto (registros solo de los primeros días)
ultimo_mes <- max(citas$mes)

demanda_mes <- citas %>%
  filter(mes != ultimo_mes) %>%
  count(mes, status) %>%
  mutate(fecha = as.Date(paste0(mes, "-01")))

g2 <- ggplot(demanda_mes, aes(x = fecha, y = n, color = status, group = status,
                              text = paste0("Mes: ", mes, "<br>Estado: ", status,
                                            "<br>Citas: ", n))) +
  geom_line() +
  labs(title = "Citas por mes según su estado",
       x = "Mes", y = "Número de citas", color = "Estado") +
  scale_color_brewer(palette = "Set2") +
  theme_minimal()

ggplotly(g2, tooltip = "text")
# Complemento: total de citas por día de la semana
citas %>% count(dia_semana) %>% kable(col.names = c("Día", "Citas"))
Día Citas
lunes 22293
martes 22342
miércoles 22190
jueves 22313
viernes 22350

Comentario: la base cubre aproximadamente diez años de actividad. La demanda mensual se mantiene estable, entre unas 850 y 1.000 citas al mes, sin crecimientos ni caídas fuertes. Las citas atendidas son siempre la línea más alta y las canceladas y no asistidas mantienen niveles parecidos cada mes. Las citas se reparten casi por igual de lunes a viernes, y no hay atención los fines de semana. Para la gestión, una demanda estable y predecible permite planear con anticipación la capacidad instalada (consultorios y horarios) y los turnos del talento humano con una plantilla fija. El problema no es la falta de demanda, sino lo que se pierde por cancelaciones e inasistencias (Gráfico 1).

1.3.3 Gráfico 3. Distribución del tiempo de espera (variable numérica clave)

atendidas <- citas %>% filter(status == "attended", !is.na(waiting_time))

g3 <- ggplot(atendidas, aes(x = waiting_time)) +
  geom_histogram(binwidth = 5, fill = "#41AB5D", color = "white") +
  labs(title = "Distribución del tiempo de espera (citas atendidas)",
       x = "Tiempo de espera (minutos)", y = "Número de citas") +
  theme_minimal()

ggplotly(g3)

Comentario: el histograma (gráfico de barras que agrupa los valores en intervalos) muestra cuántos pacientes esperaron cada rango de minutos. Los tiempos van de 0.6 a 297.3 minutos. Observar dónde se acumulan las barras indica la espera “habitual”; las barras aisladas a la derecha son pacientes con esperas muy largas. El gráfico tiene una “cola” larga hacia la derecha: la mayoría espera menos de una hora, pero hay pacientes que esperan varias horas. Además, el 53.8% de los pacientes atendidos esperó más de 30 minutos. El tiempo de espera es un indicador de calidad percibida por el usuario, así que este es un hallazgo crítico: más de la mitad de los pacientes supera una meta razonable de 30 minutos. Para la gestión, esto indica revisar la puntualidad de la agenda, el flujo de recepción y si las franjas asignadas son suficientes.

1.3.4 Gráfico 4. Duración de la consulta por grupo de edad: identificación de valores atípicos (boxplot)

g4 <- ggplot(atendidas %>% filter(!is.na(appointment_duration)),
             aes(x = age_group, y = appointment_duration, fill = age_group)) +
  geom_boxplot(outlier.colour = "red", show.legend = FALSE) +
  labs(title = "Duración de la consulta según grupo de edad",
       x = "Grupo de edad", y = "Duración (minutos)") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

ggplotly(g4)

Comentario: el boxplot (diagrama de caja) resume cada grupo: la caja contiene la mitad central de las consultas, la línea interna es el valor del medio y los puntos rojos son valores atípicos (consultas mucho más largas o cortas que lo usual). Las cajas de todos los grupos de edad son prácticamente iguales: la consulta típica dura unos 15.8 minutos en todas las edades. Es decir, en esta base la edad no cambia el consumo de tiempo médico, y una misma duración de franja para todos los grupos es razonable. Los valores atípicos (consultas de hasta 58.7 minutos) aparecen en todos los grupos. Se deben revisar porque cada consulta muy larga retrasa las siguientes, y eso ayuda a explicar los tiempos de espera altos del Gráfico 3.

1.3.5 Gráfico 5. Relación preliminar: anticipación del agendamiento vs. estado de la cita

g5 <- ggplot(citas, aes(x = status, y = scheduling_interval, fill = status)) +
  geom_boxplot(show.legend = FALSE) +
  labs(title = "Días de anticipación con que se pidió la cita, según su estado",
       x = "Estado de la cita", y = "Días entre solicitud y cita") +
  scale_fill_brewer(palette = "Set2") +
  theme_minimal()

ggplotly(g5)

Comentario: este gráfico explora, de forma visual y preliminar, si las citas pedidas con mucha anticipación tienden a cancelarse o perderse más que las pedidas para pocos días después. Las cajas de “attended”, “cancelled” y “did not attend” son casi idénticas: la mayoría de las citas se pide con 2 a 10 días de anticipación, sin importar si después se cumplen o no. Visualmente, la anticipación no parece explicar las cancelaciones ni las inasistencias. Para la gestión, esto sugiere que limitar el horizonte de agendamiento no reduciría las pérdidas, y que se deben buscar otras causas (recordatorios, barreras de acceso, horario de la cita). La comprobación formal de esta relación corresponde al Tema 2.

1.4 Hallazgos generales e implicaciones para la toma de decisiones

  1. Calidad de la base: los datos están completos en las variables de agendamiento; los vacíos se explican por el estado de la cita (no hubo atención), así que la base es apta para análisis sin imputar (rellenar) valores.
  2. Capacidad perdida: cerca del 22.3% de las citas se cancelan o no se cumplen. Es el hallazgo con mayor impacto económico, porque son horas de personal y consultorio sin producción.
  3. Demanda estable: el volumen mensual y diario de citas es regular (lunes a viernes, sin picos marcados). Esto facilita planear la capacidad y los turnos del personal.
  4. Espera prolongada: es el principal problema de calidad, porque más de la mitad de los pacientes atendidos espera más de 30 minutos y hay casos de varias horas.
  5. Consumo de tiempo médico homogéneo: la duración de la consulta es similar en todos los grupos de edad. Hay consultas atípicamente largas que pueden retrasar la agenda.
  6. Anticipación de la cita: visualmente no se asocia con cancelar o no asistir. Se verificará en el Tema 2.

Decisiones de gestión sugeridas a partir del EDA: recordatorios y confirmación previa de citas, reasignación rápida de cupos liberados (lista de espera), revisión de la puntualidad y del flujo de recepción para bajar la espera, control de las consultas que se extienden y seguimiento mensual de estos indicadores (porcentaje de citas perdidas, espera promedio, porcentaje con espera mayor a 30 minutos) en un tablero.

2 Probabilidad y Estadística Aplicada

Tema pendiente: se desarrollará en la siguiente actividad.