El EDA (revisión inicial de los datos antes de cualquier análisis formal) permite conocer qué contiene la base, en qué estado está y qué patrones visuales aparecen.
library(tidyverse) # manipulación de datos y gráficos (incluye ggplot2 y lubridate)
library(plotly) # vuelve interactivos los gráficos
library(DT) # tablas interactivas
Los identificadores se leen como texto para no perder los ceros a la izquierda.
citas <- read_csv("Appointments.csv",
col_types = cols(appointment_id = col_character(),
slot_id = col_character(),
patient_id = col_character()))
dim(citas) # filas y columnas
## [1] 111488 16
glimpse(citas) # nombre, tipo y primeros valores de cada variable
## Rows: 111,488
## Columns: 16
## $ 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 <time> 08:00:00, 13:30:00, 13:45:00, 14:00:00, 14:15:00…
## $ scheduling_interval <dbl> 4, 3, 15, 1, 6, 2, 2, 2, 4, 4, 4, 1, 1, 7, 3, 2, …
## $ status <chr> "did not attend", "did not attend", "attended", "…
## $ check_in_time <time> NA, NA, 13:36:45, 13:59:32, NA…
## $ appointment_duration <dbl> NA, NA, 5.2, 28.9, NA, 7.7, 4.2, 27.1, NA, 1.2, 7…
## $ start_time <time> NA, NA, 13:37:57, 14:00:40, NA…
## $ end_time <time> NA, NA, 13:43:09, 14:29:34, NA…
## $ 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 <chr> "Male", "Male", "Male", "Female", "Male", "Female…
## $ age <dbl> 37, 84, 77, 37, 72, 51, 28, 33, 29, 90, 66, 64, 3…
## $ age_group <chr> "35-39", "80-84", "75-79", "35-39", "70-74", "50-…
datatable(head(citas, 100), options = list(scrollX = TRUE, pageLength = 5))
La base contiene 111.488 citas y 16 variables. Las citas van del 1 de enero de 2015 al 24 de diciembre de 2024 (los agendamientos inician en diciembre de 2014) y corresponden a 36.697 pacientes distintos; es decir, un mismo paciente tiene varias citas.
diccionario <- tibble(
Variable = names(citas),
Descripcion = c(
"Identificador único de la cita",
"Identificador del espacio (turno) en la agenda",
"Fecha en que se agendó la cita",
"Fecha programada de la cita",
"Hora programada de la cita",
"Días entre el agendamiento y la cita",
"Estado final de la cita",
"Hora de llegada/registro del paciente",
"Duración de la atención (minutos)",
"Hora real de inicio de la atención",
"Hora real de finalización de la atención",
"Tiempo de espera antes de ser atendido (minutos)",
"Identificador del paciente",
"Sexo del paciente",
"Edad del paciente (años)",
"Grupo de edad (rangos de 5 años)"
),
Tipo = sapply(citas, function(x) class(x)[1])
)
datatable(diccionario, options = list(pageLength = 16))
Las variables se agrupan en: identificadores
(appointment_id, slot_id,
patient_id), fechas y horas de agenda y de atención real,
tiempos en minutos (appointment_duration,
waiting_time) y características del paciente
(sex, age, age_group).
Se traducen las categorías al español y se convierten a
factor (variable con categorías fijas).
citas <- citas %>%
mutate(
status = recode(status,
"attended" = "Asistió",
"cancelled" = "Cancelada",
"did not attend" = "No asistió",
"scheduled" = "Programada",
"unknown" = "Desconocido"),
sex = recode(sex, "Female" = "Femenino", "Male" = "Masculino"),
status = as.factor(status),
sex = as.factor(sex),
age_group = factor(age_group, levels = c("15-19","20-24","25-29","30-34","35-39",
"40-44","45-49","50-54","55-59","60-64",
"65-69","70-74","75-79","80-84","85-89","90+"))
)
glimpse(citas)
## Rows: 111,488
## Columns: 16
## $ 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 <time> 08:00:00, 13:30:00, 13:45:00, 14:00:00, 14:15:00…
## $ scheduling_interval <dbl> 4, 3, 15, 1, 6, 2, 2, 2, 4, 4, 4, 1, 1, 7, 3, 2, …
## $ status <fct> No asistió, No asistió, Asistió, Asistió, Cancela…
## $ check_in_time <time> NA, NA, 13:36:45, 13:59:32, NA…
## $ appointment_duration <dbl> NA, NA, 5.2, 28.9, NA, 7.7, 4.2, 27.1, NA, 1.2, 7…
## $ start_time <time> NA, NA, 13:37:57, 14:00:40, NA…
## $ end_time <time> NA, NA, 13:43:09, 14:29:34, NA…
## $ 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> Masculino, Masculino, Masculino, Femenino, Mascul…
## $ 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, …
faltantes <- tibble(
Variable = names(citas),
Faltantes = colSums(is.na(citas)),
Porcentaje = round(colSums(is.na(citas)) / nrow(citas) * 100, 2)
)
datatable(faltantes)
# ¿En qué estados están las celdas vacías de la atención?
citas %>%
filter(is.na(check_in_time)) %>%
count(status)
## # A tibble: 4 × 2
## status n
## <fct> <int>
## 1 Cancelada 18254
## 2 Desconocido 446
## 3 No asistió 6615
## 4 Programada 141
sum(duplicated(citas$appointment_id)) # citas repetidas
## [1] 0
sum(duplicated(citas)) # filas completas repetidas
## [1] 0
Las cinco variables de atención (check_in_time,
appointment_duration, start_time,
end_time, waiting_time) tienen los mismos
25.456 faltantes (NA, celdas vacías), y todos pertenecen a citas sin
atención: canceladas, no asistidas, desconocidas y programadas. Las
citas atendidas no tienen faltantes. Por eso no son errores de captura y
no deben eliminarse ni rellenarse. No hay citas ni filas duplicadas.
g1 <- citas %>%
count(status) %>%
ggplot(aes(x = reorder(status, -n), y = n, fill = status)) +
geom_col() +
labs(title = "Citas según estado", x = "Estado", y = "Número de citas") +
theme_minimal() +
theme(legend.position = "none")
ggplotly(g1)
La mayoría de citas fueron atendidas (86.032). Las canceladas (18.254) y las inasistencias (6.615) suman 24.869 espacios de agenda no usados como se planeó, lo que representa capacidad instalada perdida (horas de profesional disponibles que no se convierten en atención). Las 141 citas “Programadas” son de diciembre de 2024: aún no habían ocurrido al corte de la base. Los 446 casos “Desconocido” son un problema de registro a reportar.
g2 <- citas %>%
count(age_group, sex) %>%
ggplot(aes(x = age_group, y = n, fill = sex)) +
geom_col(position = "dodge") +
labs(title = "Citas por grupo de edad y sexo",
x = "Grupo de edad", y = "Número de citas", fill = "Sexo") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
ggplotly(g2)
La demanda se concentra en mayores de 55 años; el grupo con más citas es 75-79 (9.844). Las mujeres tienen más citas en todos los grupos (66.086 frente a 45.402 de hombres), con la brecha más amplia entre 25 y 39 años (en 30-34: 5.807 mujeres frente a 1.720 hombres), posiblemente asociada a atención en edad reproductiva. Desde los 60 años la demanda de ambos sexos casi se iguala.
Se excluye diciembre de 2024 porque solo tiene 166 citas (corte a mitad de mes) y mostraría una caída falsa.
g3 <- citas %>%
filter(appointment_date < as.Date("2024-12-01")) %>%
mutate(mes = floor_date(appointment_date, "month")) %>%
count(mes, status) %>%
ggplot(aes(x = mes, y = n, color = status)) +
geom_line() +
labs(title = "Citas por mes según estado (ene-2015 a nov-2024)",
x = "Mes", y = "Número de citas", color = "Estado") +
theme_minimal()
ggplotly(g3)
El volumen es estable durante los diez años (alrededor de 8.600 a 8.700 citas atendidas por año), sin tendencia de crecimiento ni caída. 2020 no muestra la caída esperable por la pandemia, lo que sugiere que la base podría ser simulada (generada artificialmente); conviene confirmarlo con la fuente. Febrero y diciembre son los meses con menos citas. Cancelaciones e inasistencias se mueven en paralelo a las atendidas, sin picos atípicos.