Objetivo y referente metodológico (punto 1).
Identificar patrones de asistencia y espera para orientar la gestión de
agendas y recursos. Martínez Bautista y Delgadillo Hurtado (2024), en
Explorando medicaldata en R, examinan una base clínica mediante
inspección de variables (names, head),
faltantes (is.na), resúmenes (summary) y
gráficos de distribución y asociación (ggplot2). Aquí
replicamos esa secuencia analítica, pero con las
tres bases entregadas por el profesor, no con la base
de diabetes. A diferencia de la exclusión global de casos incompletos
mostrada en el artículo, empleamos datos disponibles por indicador para
evitar pérdidas innecesarias.
111.488 citas 36.697 pacientes 104.360 franjas de agenda
Integración y calidad. appointment_id
identifica citas; patient_id y slot_id
relacionan seguro y cupos. Se detectaron 25.456 esperas y 25.456
duraciones sin registro; corresponden principalmente a citas no
atendidas, por lo que no equivalen automáticamente a errores de
captura. No se usan nombres ni datos de identificación
personal. Registros del 01/01/2015 al 24/12/2024.
# table() cuenta casos; prop.table() calcula probabilidades empíricas.
tabla_estado <- table(citas$status)
prob_estado <- prop.table(tabla_estado)
knitr::kable(data.frame(Estado=names(tabla_estado),
Citas=as.integer(tabla_estado), Probabilidad=round(as.numeric(prob_estado),3)),
caption="Estados registrados y proporción empírica")
| Estado | Citas | Probabilidad |
|---|---|---|
| attended | 86032 | 0.772 |
| cancelled | 18254 | 0.164 |
| did not attend | 6615 | 0.059 |
| scheduled | 141 | 0.001 |
| unknown | 446 | 0.004 |
Interpretación. 77,2% de las citas figuran como attended y 5,9% como did not attend; otras están canceladas o pendientes. Estos porcentajes describen registros, no tasas ajustadas por complejidad de pacientes ni por diferencias de periodos. Del inventario de franjas, 10,7% registra disponibilidad: se interpreta como el estado consignado y no como capacidad futura garantizada.
Lectura gerencial. Un tablero debe separar atención, cancelación y no asistencia; las franjas disponibles son una fotografía de la base y no garantizan cupos futuros. Esto permite focalizar recordatorios y revisar capacidad sin confundir categorías.
Experimento aleatorio (puntos 2–4). Espacio
muestral: \(\Omega=\{\text{asistió, canceló,
no asistió}\}\). Evento \(A=\{\text{no
asistió}\}\). Mediante sample() se generan 500 citas
ficticias de los tres estados concluidos, con probabilidades relativas
observadas. table() y prop.table() verifican
la frecuencia de cada resultado. Estas 500 citas no se agregan a
los datos reales.
# sample(): selección aleatoria. table(): frecuencias y probabilidades simuladas.
clases <- c("attended", "cancelled", "did not attend")
prob_clases <- as.numeric(prop.table(table(factor(
citas$status[citas$status %in% clases], levels=clases))))
seleccion <- sample(clases, size=500, replace=TRUE, prob=prob_clases)
frec_sim <- table(factor(seleccion, levels=clases))
knitr::kable(data.frame(Eventos=clases,
Frecuencia=as.integer(frec_sim), Probabilidad=round(as.numeric(frec_sim)/500,3)),
caption="500 citas simuladas mediante sample() y table()")
| Eventos | Frecuencia | Probabilidad |
|---|---|---|
| attended | 401 | 0.802 |
| cancelled | 77 | 0.154 |
| did not attend | 22 | 0.044 |
# runif() genera umbrales uniformes para un evento binario hipotético.
u <- runif(1000, min=0, max=1)
p_no_asiste <- mean(citas$status == "did not attend")
no_asiste_uniforme <- as.integer(u < p_no_asiste)
# rbinom(): número de no asistencias entre 20 citas por día hipotético.
no_asistencias <- rbinom(1000, size=20, prob=p_no_asiste)
# rpois(): demanda diaria ilustrativa; dpois(): probabilidad puntual teórica.
citas_por_dia <- table(citas$appointment_date)
lambda <- mean(as.numeric(citas_por_dia)) # Días con al menos una cita registrada.
demanda <- rpois(1000, lambda=lambda)
k <- round(lambda)
prob_puntual <- dpois(k, lambda=lambda)
Resultados e interpretación. El evento de no
asistencia tiene probabilidad empírica 5,9%; entre 1.000 umbrales
uniformes, 54 activaron ese evento. rbinom() produce en
promedio 1.21 inasistencias por bloque hipotético de 20 citas. Si la
demanda se modela como Poisson, con parámetro \(\lambda=42.81\) citas por día con actividad
registrada, dpois(43, lambda) = 0.0607;
dpois() no genera valores aleatorios, sino
que calcula la probabilidad de exactamente ese número de citas.
rpois() es la función que simula conteos.
Interpretación gerencial. Si las ausencias esperadas
aumentan, es razonable ensayar recordatorios, pero no sobreagendar
automáticamente. runif() modela incertidumbre individual;
rbinom() requiere ensayos con probabilidad estable, y
Poisson supone tasa constante e independencia: esas hipótesis deben
validarse con las series reales.
Se examinan waiting_time (espera) y
appointment_duration (duración) en
minutos, presentes solo en citas atendidas. En lugar de
eliminar todos los registros incompletos, se usan los casos con valor
disponible para cada indicador.
# Estadísticos base obligatorios aplicados al tiempo de espera observado.
indicadores <- c(Media=mean(real_espera), Mediana=median(real_espera),
Desviacion=sd(real_espera), Varianza=var(real_espera),
Minimo=range(real_espera)[1], Maximo=range(real_espera)[2])
knitr::kable(data.frame(Estadistico=names(indicadores),
Valor=round(as.numeric(indicadores),2)),
caption="Tiempo de espera en citas atendidas (minutos; varianza en min²)")
| Estadistico | Valor |
|---|---|
| Media | 44.09 |
| Mediana | 33.50 |
| Desviacion | 40.79 |
| Varianza | 1663.73 |
| Minimo | 0.60 |
| Maximo | 297.30 |
# summary() ofrece mínimo, cuartiles, media, máximo y valores ausentes.
summary(citas[,c("waiting_time","appointment_duration","scheduling_interval")])
## waiting_time appointment_duration scheduling_interval
## Min. : 0.60 Min. : 0.00 Min. : 1.00
## 1st Qu.: 12.60 1st Qu.: 8.60 1st Qu.: 2.00
## Median : 33.50 Median :15.80 Median : 5.00
## Mean : 44.09 Mean :17.48 Mean : 7.19
## 3rd Qu.: 64.60 3rd Qu.:24.70 3rd Qu.:10.00
## Max. :297.30 Max. :58.70 Max. :30.00
## NAs :25456 NAs :25456
# Comparación de 3 modelos para 1000 esperas hipotéticas cada uno.
n <- 1000
sim_normal <- rnorm(n, mean=media_espera, sd=sd_espera)
sim_exponencial <- rexp(n, rate=1/media_espera)
sim_uniforme <- runif(n, min=0, max=2*media_espera)
# Densidades y probabilidades acumuladas en el umbral de 30 minutos.
dens_norm_30 <- dnorm(30, mean=media_espera, sd=sd_espera)
prob_norm_30 <- pnorm(30, mean=media_espera, sd=sd_espera)
dens_exp_30 <- dexp(30, rate=1/media_espera)
prob_exp_30 <- pexp(30, rate=1/media_espera)
resumen_modelos <- data.frame(Modelo=c("Normal", "Exponencial", "Uniforme"),
Espera_media_simulada=round(c(mean(sim_normal),mean(sim_exponencial),mean(sim_uniforme)),1))
knitr::kable(resumen_modelos, caption="Promedios simulados (minutos)")
| Modelo | Espera_media_simulada |
|---|---|
| Normal | 41.5 |
| Exponencial | 43.1 |
| Uniforme | 44.3 |
Interpretación. La espera observada promedia 44.1
minutos (mediana 33.5, desviación estándar 40.8). Bajo un modelo normal,
pnorm(30) estima 36,5% de esperas hasta 30 minutos; bajo
exponencial, pexp(30) estima 49,4%. Las densidades
dnorm(30) = 0.00921 y dexp(30) = 0.0115
no son probabilidades puntuales. La normal puede
simular esperas negativas, lo que evidencia su limitación para esta
variable.
Decisión gerencial. Para establecer metas de oportunidad conviene medir mediana y percentil 90 por periodo y servicio; los modelos probabilísticos son escenarios, no predicciones hasta validar sus supuestos.
Se adopta un nivel de significancia de 5%. Las pruebas describen asociaciones, no causalidad: existen citas repetidas por paciente y posibles diferencias de año, agenda, edad o aseguradora.
# 8. t.test(): medias de espera entre mujeres y hombres atendidos.
prueba_t <- t.test(waiting_time ~ sex, data=atendidas)
# 9. aov(): medias de espera por tres grupos de edad.
anova <- aov(waiting_time ~ edad_tramo, data=atendidas)
p_anova <- summary(anova)[[1]][["Pr(>F)"]][1]
# 10. chisq.test(): aseguradora vs no asistencia, cuatro aseguradoras más frecuentes.
seguimiento <- subset(citas, status %in% c("attended", "did not attend") & !is.na(insurance))
aseguradoras <- names(sort(table(seguimiento$insurance), decreasing=TRUE))[1:4]
tabla_seg <- table(seguimiento$insurance[seguimiento$insurance %in% aseguradoras],
seguimiento$status[seguimiento$insurance %in% aseguradoras])
prueba_chi <- chisq.test(tabla_seg)
# 11. prop.test(): asistencia entre mujeres y hombres (excluidas cancelaciones).
tabla_sexo <- table(factor(asistencia$sex, levels=c("Female","Male")),
factor(asistencia$status, levels=c("attended","did not attend")))
prueba_prop <- prop.test(x=tabla_sexo[,"attended"], n=rowSums(tabla_sexo))
resultados <- data.frame(
Prueba=c("t: espera por sexo", "ANOVA: espera por edad",
"Chi cuadrado: seguro y asistencia", "Proporciones: asistencia por sexo"),
Valor_p=c(pval(prueba_t$p.value),pval(p_anova),pval(prueba_chi$p.value),
pval(prueba_prop$p.value)))
knitr::kable(resultados, caption="Pruebas con nivel de referencia α = 0,05")
| Prueba | Valor_p |
|---|---|
| t: espera por sexo | 0,992 |
| ANOVA: espera por edad | 0,535 |
| Chi cuadrado: seguro y asistencia | 0,922 |
| Proporciones: asistencia por sexo | 0,496 |
# 12. DOS figuras ggpubr: dispersión de espera y tasas de asistencia por sexo.
# El muestreo para visualización evita una figura sobrecargada; las pruebas usan
# TODOS los registros elegibles (no solo esta submuestra).
set.seed(2026)
vis <- atendidas[sample(nrow(atendidas), min(2400, nrow(atendidas))), ]
vis$edad_tramo <- droplevels(vis$edad_tramo)
# Gráfico 1: cajas de espera por grupo de edad.
g_cajas <- ggpubr::ggboxplot(vis, x="edad_tramo", y="waiting_time",
color="edad_tramo", outlier.shape=NA) +
ggplot2::coord_cartesian(ylim=c(0, unname(quantile(vis$waiting_time, .90)))) +
ggplot2::labs(title="Espera por edad", x="Edad", y="Espera (min)") +
ggplot2::theme_minimal(base_size=8) + ggplot2::theme(legend.position="none")
# Gráfico 2: barras de porcentaje de asistencia entre citas concluidas.
tasas_sexo <- data.frame(
Sexo=c("Mujeres", "Hombres"),
Asistencia=as.numeric(tabla_sexo[, "attended"] / rowSums(tabla_sexo)) * 100)
g_barras <- ggpubr::ggbarplot(tasas_sexo, x="Sexo", y="Asistencia", fill="Sexo") +
ggplot2::labs(title="Asistencia por sexo", x="", y="Porcentaje (%)") +
ggplot2::coord_cartesian(ylim=c(0,100)) +
ggplot2::theme_minimal(base_size=8) + ggplot2::theme(legend.position="none")
# Dos comparaciones reproducibles con el paquete ggpubr.
ggpubr::ggarrange(g_cajas, g_barras, ncol=2, nrow=1)
Interpretación de las pruebas y gráficos. Las cajas
muestran dispersión y posibles diferencias de espera entre edades; las
barras muestran las tasas brutas de asistencia por sexo. La prueba t
produce p 0,992 e intervalo de confianza del 95% para la diferencia de
medias de [-0.56, 0.55] minutos. El ANOVA por edades obtiene p 0,535; si
es menor a 0,05, al menos dos medias difieren, sin
indicar cuáles (haría falta una prueba poshoc). Chi-cuadrado arroja p
0,922 para la asociación entre aseguradora y asistencia entre las cuatro
más frecuentes. prop.test() obtiene p 0,496 en la
comparación de proporciones de asistencia: mujeres 92,9% y hombres
92,8%. Conclusión inferencial: en los cuatro contrastes
no hay evidencia estadística suficiente de diferencias o asociaciones al
5 %. Un valor p alto no demuestra igualdad entre grupos. La
significación estadística tampoco implica relevancia operativa: deben
revisarse tamaños de efecto, periodos y posibles factores de
confusión.
Recomendaciones. (1) Monitorear mediana y percentil 90 de espera; (2) pilotear recordatorios y medir resultados; (3) examinar brechas de acceso ajustando por factores de confusión; (4) validar escenarios de demanda antes de modificar la oferta. Limitaciones: no se ajusta por citas repetidas, estacionalidad ni diferencias de complejidad.
Fuentes: bases del docente appointments.csv,
patients.csv y slots.csv, incorporadas en el ZIP
entregado; Martínez Bautista, H. y Delgadillo Hurtado, M. L. (2024),
Explorando
medicaldata en R (referencia metodológica, no fuente de datos de
este ejercicio). Resultados de simulación fijados con
set.seed(2026). Los gráficos de simulaciones en HTML
permiten interacción al pasar el cursor; los dos comparativos de ggpubr
son figuras estáticas que facilitan la impresión. El botón Code
despliega las instrucciones R comentadas.