Este texto puede incorporarse directamente en tu R Markdown.
La oportunidad de atención es un indicador relevante para la gestión
de los servicios sanitarios. Los tiempos de espera elevados pueden
afectar la experiencia del paciente y la organización de los recursos
disponibles. Por ello, se propone construir un modelo de regresión
lineal que estime el tiempo de espera de las citas atendidas a partir de
variables disponibles en la base Appointments.
Estimar el tiempo de espera registrado de las citas atendidas e identificar variables asociadas con su variación, con el propósito de generar información que apoye la planificación de agendas y la revisión de los procesos de atención.
Código 1 — Cargar paquetes
Este bloque puede ir al inicio del R Markdown.
# Instalar los paquetes solamente si no están instalados
# install.packages("tidyverse")
# install.packages("plotly")
# install.packages("DT")
library(tidyverse)
library(plotly)
library(DT)Código 2 — Importar el archivo CSV
Como el archivo tiene las filas entre comillas, utilizamos una lectura sencilla que permite conservar correctamente las 16 columnas.
# Leer el archivo línea por línea
datos <- readLines("Appointments.csv", encoding = "UTF-8")
# Eliminar las comillas que rodean cada fila
datos <- gsub('^"|"$', '', datos)
# Convertir las líneas en una tabla
appointments <- read.csv(
text = datos,
header = TRUE,
stringsAsFactors = FALSE,
na.strings = c("", "NA")
)
# Revisar las dimensiones
dim(appointments)## [1] 111488 16
Código 3 — Visualizar las primeras observaciones, estructura, dimension, nombre de variables.
## 'data.frame': 111488 obs. of 16 variables:
## $ appointment_id : int 138 146 21 233 90 180 197 191 135 130 ...
## $ slot_id : int 1 23 24 25 26 27 28 29 30 22 ...
## $ scheduling_date : chr "2014-12-28" "2014-12-29" "2014-12-17" "2014-12-31" ...
## $ appointment_date : chr "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 : int 4 3 15 1 6 2 2 2 4 4 ...
## $ status : chr "did not attend" "did not attend" "attended" "attended" ...
## $ check_in_time : chr NA NA "13:36:45" "13:59:32" ...
## $ appointment_duration: num NA NA 5.2 28.9 NA 7.7 4.2 27.1 NA 1.2 ...
## $ start_time : chr NA NA "13:37:57" "14:00:40" ...
## $ end_time : chr NA NA "13:43:09" "14:29:34" ...
## $ waiting_time : num NA NA 1.2 1.1 NA 21.7 16.2 1 NA 8.5 ...
## $ patient_id : int 8285 5972 6472 5376 8028 4317 7638 7061 2475 4217 ...
## $ sex : chr "Male" "Male" "Male" "Female" ...
## $ age : int 37 84 77 37 72 51 28 33 29 90 ...
## $ age_group : chr "35-39" "80-84" "75-79" "35-39" ...
## [1] 111488 16
## [1] "appointment_id" "slot_id" "scheduling_date"
## [4] "appointment_date" "appointment_time" "scheduling_interval"
## [7] "status" "check_in_time" "appointment_duration"
## [10] "start_time" "end_time" "waiting_time"
## [13] "patient_id" "sex" "age"
## [16] "age_group"
Código 7 — Tabla básica de descripción de variables
Para un primer R Markdown es útil construir una tabla que explique qué representa cada campo.
descripcion_variables <- data.frame(
Variable = names(appointments),
Descripcion = c(
"Identificador de la cita",
"Identificador del espacio disponible",
"Fecha en que se programó la cita",
"Fecha asignada para la cita",
"Hora asignada para la cita",
"Intervalo entre la programación y la cita",
"Estado final de la cita",
"Hora de llegada del paciente",
"Duración de la atención",
"Hora de inicio de la atención",
"Hora de finalización de la atención",
"Tiempo de espera del paciente",
"Identificador del paciente",
"Sexo registrado",
"Edad del paciente",
"Grupo de edad"
)
)
knitr::kable(
descripcion_variables,
caption = "Descripción de las variables de la base Appointments"
)| Variable | Descripcion |
|---|---|
| appointment_id | Identificador de la cita |
| slot_id | Identificador del espacio disponible |
| scheduling_date | Fecha en que se programó la cita |
| appointment_date | Fecha asignada para la cita |
| appointment_time | Hora asignada para la cita |
| scheduling_interval | Intervalo entre la programación y la cita |
| status | Estado final de la cita |
| check_in_time | Hora de llegada del paciente |
| appointment_duration | Duración de la atención |
| start_time | Hora de inicio de la atención |
| end_time | Hora de finalización de la atención |
| waiting_time | Tiempo de espera del paciente |
| patient_id | Identificador del paciente |
| sex | Sexo registrado |
| age | Edad del paciente |
| age_group | Grupo de edad |
La base de datos contiene información relacionada con la programación, atención y estado de las citas. Las variables incluyen identificadores, fechas y horas, características de los pacientes y variables relacionadas con el proceso de atención. Para el EDA se revisará inicialmente la estructura y organización de estas variables antes de realizar la exploración gráfica.
Aquí todavía estamos haciendo EDA. La finalidad es conocer cómo están organizados los datos y detectar problemas básicos de calidad, sin entrar todavía en análisis estadístico.
Código 8 — Convertir fechas
appointments$scheduling_date <- as.Date(
appointments$scheduling_date
)
appointments$appointment_date <- as.Date(
appointments$appointment_date
)Código 9 — Convertir variables numéricas
appointments$scheduling_interval <- as.numeric(
appointments$scheduling_interval
)
appointments$appointment_duration <- as.numeric(
appointments$appointment_duration
)
appointments$waiting_time <- as.numeric(
appointments$waiting_time
)
appointments$age <- as.numeric(
appointments$age
)Código 10 — Convertir variables categóricas
appointments$status <- as.factor(appointments$status)
appointments$sex <- as.factor(appointments$sex)
appointments$age_group <- as.factor(appointments$age_group)Código 11 — Revisar datos faltantes
Los datos faltantes (valores que no están registrados) son importantes en una exploración inicial.
## appointment_id slot_id scheduling_date
## 0 0 0
## appointment_date appointment_time scheduling_interval
## 0 0 0
## status check_in_time appointment_duration
## 0 25456 25456
## start_time end_time waiting_time
## 25456 25456 25456
## patient_id sex age
## 0 0 0
## age_group
## 0
La revisión inicial permite identificar que algunas variables relacionadas con la atención, como la duración de la cita y el tiempo de espera, pueden presentar valores faltantes. Esto debe tenerse en cuenta durante la exploración de la información, ya que no todas las citas necesariamente tienen registrada información correspondiente a una atención efectiva.
##Exploración gráfica de los datos
Aquí podemos incorporar los tres gráficos interactivos básicos solicitados.
Gráfico 1 — Estado de las citas
Primero exploramos la variable status, que permite observar cómo se distribuyen las citas según su estado registrado.
## [1] 2
grafico_status <- appointments %>%
count(status)
plot_ly(
grafico_status,
x = ~status,
y = ~n,
type = "bar"
) %>%
layout(
title = "Estado de las citas",
xaxis = list(title = "Estado"),
yaxis = list(title = "Número de citas")
)datos <- readLines("Appointments.csv", encoding = "UTF-8")
datos <- gsub('^"|"$', '', datos)
appointments <- read.csv(
text = datos,
header = TRUE,
stringsAsFactors = FALSE,
na.strings = c("", "NA")
)## [1] 111488 16
Gráfico 2 — Distribución de pacientes por grupo de edad
Utilizamos age_group, evitando por ahora realizar medidas estadísticas sobre la edad.
Código 13
grafico_edad <- appointments %>%
count(age_group)
grafico <- plotly::plot_ly(
grafico_edad,
x = ~age_group,
y = ~n,
type = "bar"
)
grafico <- plotly::layout(
grafico,
title = "Citas según grupo de edad",
xaxis = list(title = "Grupo de edad"),
yaxis = list(title = "Número de citas")
)
graficoEl gráfico muestra que las citas se concentran principalmente en pacientes de 55 a 79 años, con el valor más alto en el grupo de 75 a 79 años, cercano a 9.800 citas. En contraste, los grupos de 15 a 19 años y mayores de 90 años presentan la menor cantidad de citas. Para la gestión sanitaria, esto sugiere la importancia de planificar personal, agendas y recursos teniendo en cuenta la mayor demanda observada en los grupos de edad avanzada.
## [1] "Date"
codigo 14 evolución de las citas en el tiempo.
citas_tiempo <- appointments %>%
mutate(
mes = format(appointment_date, "%Y-%m")
) %>%
count(mes)
grafico <- plotly::plot_ly(
citas_tiempo,
x = ~mes,
y = ~n,
type = "scatter",
mode = "lines+markers"
)
grafico <- plotly::layout(
grafico,
title = "Evolución temporal de las citas",
xaxis = list(
title = "Mes de la cita",
tickangle = 45
),
yaxis = list(
title = "Número de citas"
)
)
graficoEl gráfico muestra que el número mensual de citas se mantiene relativamente estable entre 2015 y 2024, con aproximadamente 850 a 1.000 citas por mes, sin una tendencia sostenida de crecimiento o disminución. Sin embargo, al final de 2024 se observa una caída abrupta hasta cerca de 170 citas, que podría corresponder a un periodo incompleto o a datos faltantes. Para la gestión sanitaria, es importante verificar ese último registro antes de utilizarlo para proyectar la demanda y planificar los recursos.
Aunque el requisito mínimo son tres gráficos, este cuarto gráfico puede ser interesante para un estudiante de Gerencia de Servicios de Salud, porque permite explorar conjuntamente dos variables de la base sin entrar todavía en inferencia estadística.
Código 15 — Estado de la cita según sexo
Este será un gráfico adicional de exploración, todavía dentro del EDA. Permite observar conjuntamente sex y status.
grafico_sexo <- appointments %>%
count(sex, status)
grafico <- plotly::plot_ly(
grafico_sexo,
x = ~status,
y = ~n,
color = ~sex,
type = "bar"
)
grafico <- plotly::layout(
grafico,
title = "Estado de las citas según sexo",
barmode = "group",
xaxis = list(
title = "Estado de la cita"
),
yaxis = list(
title = "Número de citas"
)
)
graficoEl gráfico muestra que las mujeres (Female) registran un mayor volumen de citas en todas las categorías principales en comparación con los hombres (Male), siendo la asistencia el estado predominante para ambos sexo
El objetivo es analizar la base de datos Appointments desde la perspectiva de la administración y gestión de recursos sanitarios, utilizando RStudio y R Markdown. Los ejemplos se centrarán en situaciones reales: inasistencia a citas, tiempos de espera, características de los pacientes y diferencias entre grupos. La estructura propuesta conserva tres niveles para tu tabla de contenido y mantiene el enfoque en probabilidad y estadística descriptiva e inferencial, sin incluir modelamiento cuantitativo.
Espacio muestral, eventos, frecuencias, probabilidades empíricas y distribuciones de probabilidad.
Media, mediana, desviación estándar, varianza, rango, resumen de variables y gráficos comparativos.
Pruebas t, ANOVA, chi-cuadrado y comparación de proporciones para apoyar decisiones de gestión.
La base contiene 111.488 registros y 16 variables. Como el archivo adjunto está en formato CSV y sus filas están entrecomilladas de una forma que puede provocar una lectura incorrecta, utiliza el siguiente bloque para importarlo. Este código también convierte las variables numéricas y las fechas al formato adecuado.
Código 1. Importar y preparar la base
# Importar la base de datos
lineas <- readLines("Appointments.csv", warn = FALSE)
lineas <- sub('^"', '', lineas)
lineas <- sub('"$', '', lineas)
appointments <- read.csv(
text = paste(lineas, collapse = "\n"),
stringsAsFactors = FALSE
)
# Convertir variables numéricas
appointments$age <- as.numeric(appointments$age)
appointments$waiting_time <- as.numeric(
appointments$waiting_time
)
appointments$appointment_duration <- as.numeric(
appointments$appointment_duration
)
# Convertir fechas
appointments$scheduling_date <- as.Date(
appointments$scheduling_date
)
appointments$appointment_date <- as.Date(
appointments$appointment_date
)
# Revisar la estructura
dim(appointments)## [1] 111488 16
## 'data.frame': 111488 obs. of 16 variables:
## $ appointment_id : int 138 146 21 233 90 180 197 191 135 130 ...
## $ slot_id : int 1 23 24 25 26 27 28 29 30 22 ...
## $ scheduling_date : Date, format: "2014-12-28" "2014-12-29" ...
## $ appointment_date : Date, format: "2015-01-01" "2015-01-01" ...
## $ appointment_time : chr "08:00:00" "13:30:00" "13:45:00" "14:00:00" ...
## $ scheduling_interval : int 4 3 15 1 6 2 2 2 4 4 ...
## $ status : chr "did not attend" "did not attend" "attended" "attended" ...
## $ check_in_time : chr "" "" "13:36:45" "13:59:32" ...
## $ appointment_duration: num NA NA 5.2 28.9 NA 7.7 4.2 27.1 NA 1.2 ...
## $ start_time : chr "" "" "13:37:57" "14:00:40" ...
## $ end_time : chr "" "" "13:43:09" "14:29:34" ...
## $ waiting_time : num NA NA 1.2 1.1 NA 21.7 16.2 1 NA 8.5 ...
## $ patient_id : int 8285 5972 6472 5376 8028 4317 7638 7061 2475 4217 ...
## $ sex : chr "Male" "Male" "Male" "Female" ...
## $ age : num 37 84 77 37 72 51 28 33 29 90 ...
## $ age_group : chr "35-39" "80-84" "75-79" "35-39" ...
Interpretación: dim() permite comprobar las filas y columnas importadas, mientras que str() muestra los tipos de datos. Es importante verificar que existan las 16 variables antes de continuar.
Variable Descripción Utilidad en la gestión sanitaria appointment_id Identificador de la cita Control de registros slot_id Identificador del turno Gestión de agendas scheduling_date Fecha de programación Planeación de citas appointment_date Fecha de atención programada Análisis de demanda appointment_time Hora programada Distribución de turnos scheduling_interval Intervalo entre programación y cita, según la definición de la base Anticipación de la agenda status Estado de la cita Asistencia e inasistencia check_in_time Hora de registro del paciente Flujo de ingreso appointment_duration Duración de la atención registrada Uso del tiempo asistencial start_time Hora de inicio de atención Oportunidad del servicio end_time Hora de finalización Organización de turnos waiting_time Tiempo de espera registrado Oportunidad de atención patient_id Identificador del paciente Identificación de registros por paciente sex Sexo registrado Descripción de la población age Edad en años Caracterización de pacientes age_group Grupo de edad Comparación entre grupos poblacionales
Un espacio muestral es el conjunto de resultados posibles de un experimento. En este caso, podemos utilizar los estados de las citas registrados en la base.
Código 2. Seleccionar citas al azar con sample()
set.seed(123)
# Seleccionar 10 citas al azar
muestra <- sample(
appointments$appointment_id,
size = 10,
replace = FALSE
)
muestra## [1] 51365 57967 2780 29886 95230 102890 68511 62687 45400 65157
Interpretación: se seleccionan diez citas diferentes al azar. Este procedimiento permite revisar una muestra de registros sin inspeccionar toda la base.
Código 3. Identificar eventos posibles
## [1] "did not attend" "attended" "cancelled" "unknown"
## [5] "scheduled"
# Evento: cita con asistencia registrada
evento_asistencia <- appointments$status == "attended"
sum(evento_asistencia, na.rm = TRUE)## [1] 86032
Interpretación: el espacio muestral observado corresponde a las categorías de estado presentes en la base. El evento de asistencia identifica cuántas citas tienen el estado attended.
Una probabilidad empírica es la proporción de veces que se observa un evento en los datos.
Código 4. Aplicar table() y calcular probabilidades
##
## attended cancelled did not attend scheduled unknown
## 86032 18254 6615 141 446
##
## attended cancelled did not attend scheduled unknown
## 0.7717 0.1637 0.0593 0.0013 0.0040
# Probabilidad empírica de inasistencia
p_inasistencia <- mean(
appointments$status == "did not attend",
na.rm = TRUE
)
p_inasistencia## [1] 0.05933374
Interpretación: las frecuencias muestran cuántas citas pertenecen a cada estado y las proporciones indican qué fracción representan del total. La probabilidad empírica de inasistencia permite dimensionar el problema para planificar recordatorios y administrar la disponibilidad de los turnos. Importante: el estado cancelled es diferente de did not attend; no deben sumarse automáticamente como si fueran el mismo evento.
Una variable discreta representa resultados contables, por ejemplo, el número de pacientes que no asisten.
Código 5. Simular inasistencias con runif(), rbinom() y dpois()
# Probabilidad de inasistencia observada
p <- mean(
appointments$status == "did not attend",
na.rm = TRUE
)
# Simular 100 citas usando números aleatorios uniformes
simulacion <- runif(100)
inasistencia <- simulacion < p
mean(inasistencia)## [1] 0.03
## [1] 1 4 0 0 0 2 1 3 2 2
# Probabilidad de observar exactamente 3 inasistencias
# en 20 citas, bajo una aproximación de Poisson
dpois(x = 3, lambda = 20 * p)## [1] 0.08501148
Interpretación: runif() genera números aleatorios que se utilizan para simular si ocurre una inasistencia; rbinom() simula el número de inasistencias en grupos de 20 citas; y dpois() calcula la probabilidad de un número específico de eventos según una distribución de Poisson (modelo de conteo de eventos).
La aproximación de Poisson es ilustrativa: para un número fijo de citas con probabilidad individual de inasistencia, la distribución binomial es el modelo más directo. Estas simulaciones no son pronósticos garantizados de la demanda real.
Las distribuciones continuas permiten estudiar tiempos y variabilidad. Para interpretar estos ejemplos se usan distribuciones teóricas, que deben contrastarse con los datos reales.
Código 6. Simular tiempos con rnorm(), rexp() y runif()
set.seed(123)
# Simulación normal: tiempo de espera ilustrativo
tiempo_normal <- rnorm(
1000,
mean = 15,
sd = 5
)
# Simulación exponencial: tiempo entre eventos
tiempo_exponencial <- rexp(
1000,
rate = 1 / 10
)
# Simulación uniforme: duración entre 5 y 15 minutos
tiempo_uniforme <- runif(
1000,
min = 5,
max = 15
)
summary(tiempo_normal)## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.9511 11.8584 15.0460 15.0806 18.3230 31.2052
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.006097 2.880114 6.684024 9.836951 14.043615 58.488172
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 5.001 7.429 9.962 9.930 12.402 14.999
Interpretación: la normal genera valores alrededor de una media; la exponencial simula tiempos positivos entre eventos; y la uniforme asigna igual densidad a los valores comprendidos entre dos límites. Los parámetros son ejemplos didácticos y no estimaciones verificadas de la institución.
Código 7. Calcular densidades y probabilidades acumuladas
## [1] 0.07978846
## [1] 0.8413447
## [1] 0.03678794
## [1] 0.6321206
Interpretación: dnorm() y dexp() calculan densidades en puntos específicos; no son probabilidades de observar exactamente un valor en una distribución continua. pnorm() calcula la probabilidad acumulada hasta 20 en el ejemplo normal, mientras que pexp() calcula la probabilidad acumulada hasta 10 en el ejemplo exponencial.
Para que los gráficos sean interactivos, se utilizarán ggplot2 para construirlos y plotly para habilitar funciones como acercamiento, desplazamiento y consulta de valores.
Código 8. Histograma interactivo de los tiempos simulados
grafico_normal <- ggplot(
data.frame(tiempo = tiempo_normal),
aes(x = tiempo)
) +
geom_histogram(
bins = 30,
fill = "steelblue",
color = "white"
) +
labs(
title = "Distribución normal simulada",
x = "Tiempo simulado",
y = "Frecuencia"
) +
theme_minimal()
ggplotly(grafico_normal)Interpretación: El análisis del histograma de la Distribución Normal Simulada permite validar el comportamiento del sistema de atención, destacando una alta concentración de los tiempos de espera de los pacientes alrededor de la media, la cual se ubica de forma referencial en los 15 minutos. Esta acumulación central de las frecuencias evidencia la existencia de un estándar operativo predominante en el agendamiento y recepción de las citas médicas.
Por otra parte, la simetría y el descenso gradual de las barras hacia ambos extremos reflejan la dispersión natural del proceso generada por la desviación estándar. Esta variabilidad sitúa el grueso de los tiempos de espera en un intervalo predecible de entre 5 y 25 minutos. El control visual de esta dispersión confirma que las desviaciones extremas —como esperas mayores a 25 minutos— poseen una probabilidad de ocurrencia significativamente baja, lo que demuestra que el modelo simulado cuenta con una estabilidad estadística óptima para la toma de decisiones y la optimización de los flujos de trabajo en la clínica.
Código 9. Gráfico interactivo de la distribución exponencial
grafico_exponencial <- ggplot(
data.frame(tiempo = tiempo_exponencial),
aes(x = tiempo)
) +
geom_histogram(
bins = 30,
fill = "darkorange",
color = "white"
) +
labs(
title = "Tiempos entre eventos simulados",
x = "Tiempo",
y = "Frecuencia"
) +
theme_minimal()
ggplotly(grafico_exponencial)Interpretación: El gráfico interactivo de la distribución exponencial demuestra que el sistema está diseñado para ofrecer una atención ágil, ya que la inmensa mayoría de los pacientes experimenta tiempos de espera mínimos. Esto se evidencia en la alta concentración de datos al inicio de la gráfica, donde el mayor volumen de personas es atendido en un rango rápido de 2 a 4 minutos.
Al tratarse de un comportamiento exponencial, el número de personas en espera disminuye de forma drástica y acelerada a medida que pasan los minutos. En la práctica, esto confirma que las demoras prolongadas son eventos muy poco comunes y aislados, siendo extremadamente raro que un paciente supere los 40 minutos de espera, lo que consolida un flujo de servicio altamente eficiente.
La estadística descriptiva resume los datos observados para comprender el funcionamiento del servicio, identificar variabilidad y detectar posibles problemas de oportunidad en la atención.
Antes de calcular indicadores, se deben excluir los valores faltantes. No se deben reemplazar automáticamente por cero, porque eso reduciría artificialmente los tiempos promedio.
Código 10. Calcular media, mediana, desviación estándar, varianza y rango
# Seleccionar tiempos de espera disponibles
espera <- appointments$waiting_time
espera <- espera[!is.na(espera)]
# Medidas descriptivas
mean(espera)## [1] 44.08651
## [1] 33.5
## [1] 40.78879
## [1] 1663.725
## [1] 0.6 297.3
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.60 12.60 33.50 44.09 64.60 297.30
Interpretación: El análisis descriptivo revela que el sistema de citas médicas presenta una preocupante falta de uniformidad e impredecibilidad. Aunque la mitad de los pacientes espera un tiempo moderado de 33.50 minutos o menos (mediana), el promedio general se eleva a 44.09 (media) minutos debido a retrasos extremos. Esta alta dispersión, reflejada en una desviación estándar de 40.79 minutos y un rango que alcanza un máximo crítico de casi 5 horas (297.30 minutos), demuestra la existencia de cuellos de botella severos. Se concluye la necesidad prioritaria de intervenir los procesos operativos para mitigar estas desviaciones y estandarizar el servicio.
Aplicación gerencial: si la media es considerablemente mayor que la mediana, algunos tiempos de espera elevados podrían estar aumentando el promedio. Esto justifica revisar las citas con mayores demoras y las condiciones operativas asociadas.
Para estudiar los tiempos de espera asociados con las atenciones efectivamente realizadas, conviene seleccionar las citas con estado attended y excluir los tiempos faltantes.
Código 11. Resumen de las citas atendidas
atendidas <- subset(
appointments,
status == "attended" &
!is.na(waiting_time)
)
summary(atendidas$waiting_time)## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.60 12.60 33.50 44.09 64.60 297.30
## [1] 44.08651
## [1] 33.5
## [1] 40.78879
Interpretación: El análisis descriptivo de las citas con estado “atendidas” concluye que el proceso de atención médica presenta una preocupante falta de estandarización. Aunque el 50% de los pacientes experimenta esperas tolerables de 33.50 minutos o menos (mediana), el promedio general se eleva de forma atípica a 44.09 minutos debido al impacto del 25% de los usuarios que sufre retrasos severos superiores a los 64.60 minutos. La alta dispersión general (desviación estándar de 40.79 minutos) y un tiempo máximo crítico de casi 5 horas evidencian que el sistema genera cuellos de botella severos antes de la consulta, señalando la necesidad urgente de reestructurar los flujos operativos para garantizar un servicio predecible y oportuno.
Código 12. Distribución de los tiempos de espera
grafico_espera <- ggplot(
atendidas,
aes(x = waiting_time)
) +
geom_histogram(
bins = 30,
fill = "steelblue",
color = "white"
) +
labs(
title = "Distribución de los tiempos de espera",
x = "Tiempo de espera registrado",
y = "Número de citas"
) +
theme_minimal()
ggplotly(grafico_espera)Interpretación: El análisis del histograma de tiempos de espera reales concluye que el proceso de atención médica exhibe un comportamiento probabilístico con asimetría positiva de tipo exponencial. El sistema demuestra una alta eficiencia operativa inicial, logrando una concentración máxima que supera las 14,000 citas con una atención inmediata (cercana a los 0 minutos).
La rápida y progresiva disminución en el volumen de citas a medida que aumenta el tiempo confirma que la probabilidad de experimentar demoras prolongadas decae drásticamente. Al situarse la gran mayoría de los flujos de atención muy por debajo de los 100 minutos, y siendo los registros cercanos a los 200 minutos eventos marginales y atípicos, se valida que el modelo general funciona de manera ágil y oportuna, requiriendo solo intervenciones puntuales en la cola extrema de la distribución
Código 13. Gráfico de cajas interactivo
grafico_sexo <- ggplot(
atendidas,
aes(x = sex, y = waiting_time, fill = sex)
) +
geom_boxplot() +
labs(
title = "Tiempos de espera según sexo",
x = "Sexo registrado",
y = "Tiempo de espera"
) +
theme_minimal() +
theme(legend.position = "none")
ggplotly(grafico_sexo)Interpretación: Se concluye que el tiempo de espera en las citas médicas es estadísticamente idéntico e independiente del sexo registrado del paciente. Ambos grupos presentan exactamente la misma estructura de distribución: una mediana de 33.50 minutos, un primer cuartil de 12.70 minutos y un tercer cuartil de 64.70 minutos.
Asimismo, la dispersión interna y el comportamiento de los valores atípicos severos se replican de forma exacta en mujeres y hombres, alcanzando en ambos casos un límite superior de atraso crítico de 296.00 minutos. Estos hallazgos demuestran que las ineficiencias del sistema y los cuellos de botella identificados operan de manera homogénea en toda la población, descartando cualquier sesgo o disparidad en el servicio por razones de género.
El paquete ggpubr facilita la creación de gráficos comparativos con una presentación clara.
Código 14. Comparar tiempos de espera por sexo
ggboxplot(
atendidas,
x = "sex",
y = "waiting_time",
color = "sex",
add = "jitter",
title = "Comparación de tiempos de espera por sexo",
xlab = "Sexo",
ylab = "Tiempo de espera"
)Interpretación: El gráfico de dispersión combinado con diagramas de caja confirma de manera contundente la homogeneidad en los tiempos de espera entre ambos sexos. Tanto en hombres (Male) como en mujeres (Female), se observa un comportamiento idéntico en la distribución de los datos, manteniendo la misma mediana en torno a los 33.5 minutos y cuartiles equivalentes representados por las cajas de fondo.
La densidad de los puntos distribuidos revela una altísima concentración de atenciones en la base (tiempos menores a 100 minutos) y una dispersión que decrece progresivamente. Asimismo, la presencia de puntos aislados en la parte superior demuestra que la incidencia de retrasos críticos extremos (cercanos a los 300 minutos) afecta con la misma frecuencia e intensidad a ambos grupos, consolidando que las ineficiencias del flujo operativo son de carácter sistémico y ajenas al género del paciente.
La estadística inferencial utiliza una muestra de datos para evaluar hipótesis sobre diferencias o asociaciones. En estos ejemplos se emplea la base disponible, pero los resultados no deben interpretarse automáticamente como evidencia causal.
Una prueba t permite evaluar si la diferencia entre dos medias es compatible con la hipótesis de igualdad. Un intervalo de confianza del 95 % proporciona un rango de valores plausibles para la diferencia entre medias bajo los supuestos del procedimiento.
Código 15. Comparar la edad promedio según sexo
datos_edad <- subset(
appointments,
!is.na(age) & !is.na(sex)
)
t.test(
age ~ sex,
data = datos_edad,
conf.level = 0.95
)##
## Welch Two Sample t-test
##
## data: age by sex
## t = -56.758, df = 101597, p-value < 2.2e-16
## alternative hypothesis: true difference in means between group Female and group Male is not equal to 0
## 95 percent confidence interval:
## -7.028949 -6.559701
## sample estimates:
## mean in group Female mean in group Male
## 54.43831 61.23263
Interpretación: La prueba t realizada para evaluar la relación entre la edad y el sexo de los pacientes revela una diferencia estadísticamente significativa entre ambos grupos. Los estimadores muestrales indican que la media de edad en el grupo de mujeres (Female) es de 54.44 años, mientras que la media de edad en el grupo de hombres (Male) es notablemente mayor, situándose en 61.23 años.
La diferencia estimada entre ambas medias es de aproximadamente 6.79 años a favor del grupo masculino. El intervalo de confianza del 95% para esta diferencia se extiende desde -7.03 hasta -6.56 años; al no incluir el valor cero, se confirma la solidez de este hallazgo. Dado que el valor p calculado es extremadamente bajo (p-value < 2.2e-16), siendo muy inferior al umbral crítico de 0.05, existe evidencia estadística concluyente para rechazar la hipótesis nula de igualdad. Por lo tanto, se concluye al nivel de significación del 5% que las medias de edad poblacionales no son iguales, lo cual aporta un perfil demográfico diferenciado para la gestión de las citas médicas, independientemente de la relevancia clínica que dicha variación pueda representar.
La prueba supone observaciones independientes. Si un mismo paciente tiene varias citas, la dependencia entre registros debe tenerse en cuenta al interpretar el resultado.
El análisis de varianza (ANOVA) permite evaluar si hay evidencia de diferencias entre las medias de tres o más grupos. En esta base puede utilizarse la edad como variable numérica y el estado de la cita como variable de agrupación.
Código 16. Comparar la edad según el estado de la cita
datos_anova <- subset(
appointments,
!is.na(age) & !is.na(status)
)
modelo_anova <- aov(
age ~ status,
data = datos_anova
)
summary(modelo_anova)## Df Sum Sq Mean Sq F value Pr(>F)
## status 4 775 193.8 0.477 0.753
## Residuals 111483 45301942 406.4
Interpretación: El análisis de varianza (ANOVA) de un factor realizado para evaluar si la edad promedio de los pacientes difiere según el estado de la cita médica (status) indica que no existen diferencias estadísticamente significativas entre los grupos. El modelo registra un valor del estadístico F de 0.477 y un valor p resultante de 0.753 (Pr(>F) = 0.753), el cual es sustancialmente mayor que el umbral de significación estándar de 0.05.
Dado que el valor p supera el límite crítico, no se dispone de evidencia estadística suficiente para rechazar la hipótesis nula de igualdad de medias. En consecuencia, se concluye al nivel de significación del 5% que las edades promedio de los pacientes se mantienen homogéneas a través de los diferentes estados de las citas analizados (como asistidas, canceladas o ausentes). Al no detectarse variaciones significativas globales, se descarta la necesidad de realizar pruebas post-hoc de comparación múltiple. No obstante, para la validación definitiva de este modelo formal, se recomienda realizar de manera complementaria la revisión de los supuestos estadísticos subyacentes de independencia de las observaciones, normalidad en la distribución de los residuos y homogeneidad de varianzas (homocedasticidad).
En términos de gestión, este análisis permite explorar si los perfiles de edad varían entre las categorías de asistencia y cancelación, sin asumir que la edad explica por sí sola el estado de la cita.
La prueba chi-cuadrado evalúa si existe asociación entre dos variables categóricas. Se utilizarán sex y status.
Código 17. Examinar la asociación entre sexo y estado de la cita
##
## attended cancelled did not attend scheduled unknown
## Female 51096 10721 3900 83 286
## Male 34936 7533 2715 58 160
##
## Pearson's Chi-squared test
##
## data: tabla_sexo_estado
## X-squared = 7.3475, df = 4, p-value = 0.1186
Interpretación: No existe una asociación estadísticamente significativa entre el sexo registrado y el estado de la cita, ya que el valor p obtenido en la prueba de Chi-cuadrado es 0.1186, el cual es mayor que el nivel de significancia estándar de 0.05
Aplicación gerencial: si existe asociación, conviene investigar si las diferencias se relacionan con la programación, el acceso al servicio u otras condiciones operativas, sin atribuirlas automáticamente al sexo.
Una proporción es la fracción de observaciones que cumplen una condición. Para evitar mezclar categorías, se comparará la proporción de asistencia (attended) entre dos grupos de sexo, seleccionados de forma explícita.
Código 18. Comparar proporciones de asistencia
# Seleccionar dos grupos existentes
grupos <- unique(
na.omit(appointments$sex)
)
grupos <- grupos[1:2]
# Número total de citas por grupo
totales <- sapply(grupos, function(g) {
sum(appointments$sex == g, na.rm = TRUE)
})
# Número de citas atendidas por grupo
asistencias <- sapply(grupos, function(g) {
sum(
appointments$sex == g &
appointments$status == "attended",
na.rm = TRUE
)
})
# Comparar las proporciones
prop.test(
x = asistencias,
n = totales,
conf.level = 0.95
)##
## 2-sample test for equality of proportions with continuity correction
##
## data: asistencias out of totales
## X-squared = 2.0621, df = 1, p-value = 0.151
## alternative hypothesis: two.sided
## 95 percent confidence interval:
## -0.008731601 0.001345945
## sample estimates:
## prop 1 prop 2
## 0.7694815 0.7731743
Interpretación: No existe una diferencia estadísticamente significativa en la proporción de citas atendidas entre hombres y mujeres, ya que el valor p obtenido en la prueba de dos proporciones es 0.151, el cual es mayor que el nivel de significancia de 0.05.
Esta comparación es exploratoria: si un paciente aparece varias veces, las observaciones pueden no ser independientes. Además, una proporción de citas atendidas no equivale necesariamente a la proporción de pacientes únicos que asistieron.
Este último gráfico complementa las pruebas estadísticas con una visualización directamente útil para la gestión de agendas.
Código 19. Gráfico interactivo de estados
frecuencias_estado <- as.data.frame(
table(appointments$status)
)
names(frecuencias_estado) <- c(
"Estado", "Frecuencia"
)
grafico_estado <- ggplot(
frecuencias_estado,
aes(
x = Estado,
y = Frecuencia,
text = paste(
"Estado:", Estado,
"<br>Citas:", Frecuencia
)
)
) +
geom_col(fill = "steelblue") +
labs(
title = "Distribución de estados de las citas",
x = "Estado de la cita",
y = "Número de citas"
) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 30, hjust = 1))
ggplotly(grafico_estado, tooltip = "text")Interpretación: La categoría de citas atendidas (attended) es el evento modal y dominante, concentrando más del 75% del volumen total de los datos. El fenómeno de la pérdida de citas se divide de forma desigual; la tasa de cancelación previa (cancelled) duplica ampliamente a la inasistencia neta (did not attend), lo que sugiere que los pacientes tienden más a dar aviso de su baja que a ausentarse sin notificar.
El propósito de este capítulo es construir un modelo analítico básico en RStudio que ayude a anticipar necesidades operativas en la gestión de citas. Se utilizará la base Appointments, con sus 111.488 registros y 16 variables. Para mantener la continuidad con los temas anteriores, la estructura tendrá tres niveles: 3.1. Definición del problema y preparación de datos;
Modelo propuesto
Se recomienda comenzar con un modelo de regresión lineal (lm()), porque es sencillo de explicar, permite estudiar relaciones entre variables y constituye una base para comparar modelos más avanzados. - Problema de gestión: estimar el tiempo de espera de las citas atendidas para identificar factores asociados con mayores demoras. - Variable objetivo: waiting_time (tiempo de espera registrado). - Variables predictoras: edad, sexo, duración de la cita, intervalo de programación y hora de la cita, siempre que sus registros sean válidos y estén disponibles al momento de realizar la predicción. - Uso esperado: apoyar la identificación de condiciones operativas asociadas con esperas prolongadas y orientar la revisión de agendas.
Hay una limitación importante: el tiempo de espera puede depender de la hora real de llegada, del inicio de la atención y de otras variables que no están incluidas. Por tanto, el modelo será exploratorio y no debe utilizarse como sistema operativo de asignación de citas sin validación adicional.
El desempeño se evaluará mediante el RMSE (raíz del error cuadrático
medio, que mide el tamaño típico del error de predicción y penaliza más
los errores grandes). Como meta inicial, se propone un RMSE en los datos
de prueba inferior o igual a 10 unidades de tiempo, siempre que la
unidad de waiting_time se confirme y ese margen sea
aceptable para la institución. Este umbral es provisional y deberá
ajustarse a los estándares del servicio.
También se comparará el error en los datos de entrenamiento y de prueba para identificar posibles señales de sobreajuste (cuando el modelo funciona bien con los datos conocidos, pero pierde precisión con datos nuevos).
Las variables seleccionadas para el primer modelo son: Variable Función Justificación waiting_time Objetivo Es el tiempo que se busca estimar. age Predictora Permite explorar si el tiempo varía según la edad. sex Predictora Permite explorar diferencias entre categorías registradas. appointment_time Predictora transformada La hora programada permite analizar variaciones durante la jornada. scheduling_interval Predictora candidata Puede representar la anticipación con que se programa una cita, si su definición y tipo se confirman.
Para el primer ejercicio usaremos edad, sexo y hora programada. Así evitamos depender de la definición aún no verificada de scheduling_interval. Estas variables no necesariamente explican causalmente las demoras; el modelo examina asociaciones y capacidad predictiva.
Código 1. Instalar y cargar los paquetes
La instalación se hace una sola vez. Si ya tienes los paquetes instalados, ejecuta solamente library().
library(dplyr)
library(tidyr)
library(skimr)
library(rsample)
library(broom)
library(yardstick)
library(ggplot2)
library(plotly)Código 2. Explorar la estructura de la base
Este bloque parte del objeto appointments que creaste en los temas anteriores.
## [1] 111488 16
## Rows: 111,488
## Columns: 16
## $ appointment_id <int> 138, 146, 21, 233, 90, 180, 197, 191, 135, 130, 1…
## $ slot_id <int> 1, 23, 24, 25, 26, 27, 28, 29, 30, 22, 31, 33, 34…
## $ 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 <int> 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 <chr> "", "", "13:36:45", "13:59:32", "", "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> "", "", "13:37:57", "14:00:40", "", "14:30:38", "…
## $ end_time <chr> "", "", "13:43:09", "14:29:34", "", "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 <int> 8285, 5972, 6472, 5376, 8028, 4317, 7638, 7061, 2…
## $ 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-…
| Name | appointments |
| Number of rows | 111488 |
| Number of columns | 16 |
| _______________________ | |
| Column type frequency: | |
| character | 7 |
| Date | 2 |
| numeric | 7 |
| ________________________ | |
| Group variables | None |
Variable type: character
| skim_variable | n_missing | complete_rate | min | max | empty | n_unique | whitespace |
|---|---|---|---|---|---|---|---|
| appointment_time | 0 | 1 | 8 | 8 | 0 | 40 | 0 |
| status | 0 | 1 | 7 | 14 | 0 | 5 | 0 |
| check_in_time | 0 | 1 | 0 | 8 | 25456 | 33252 | 0 |
| start_time | 0 | 1 | 0 | 8 | 25456 | 35985 | 0 |
| end_time | 0 | 1 | 0 | 8 | 25456 | 36853 | 0 |
| sex | 0 | 1 | 4 | 6 | 0 | 2 | 0 |
| age_group | 0 | 1 | 3 | 5 | 0 | 16 | 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: numeric
| skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
|---|---|---|---|---|---|---|---|---|---|---|
| appointment_id | 0 | 1.00 | 55744.50 | 32183.96 | 1.0 | 27872.75 | 55744.5 | 83616.25 | 111488.0 | ▇▇▇▇▇ |
| slot_id | 0 | 1.00 | 51854.32 | 29945.83 | 1.0 | 25909.75 | 51852.0 | 77796.25 | 104159.0 | ▇▇▇▇▇ |
| scheduling_interval | 0 | 1.00 | 7.19 | 6.15 | 1.0 | 2.00 | 5.0 | 10.00 | 30.0 | ▇▃▂▁▁ |
| appointment_duration | 25456 | 0.77 | 17.48 | 11.06 | 0.0 | 8.60 | 15.8 | 24.70 | 58.7 | ▇▇▅▂▁ |
| waiting_time | 25456 | 0.77 | 44.09 | 40.79 | 0.6 | 12.60 | 33.5 | 64.60 | 297.3 | ▇▂▁▁▁ |
| patient_id | 0 | 1.00 | 13783.23 | 9722.94 | 1.0 | 5702.00 | 11816.0 | 20736.25 | 36697.0 | ▇▇▅▃▂ |
| age | 0 | 1.00 | 57.21 | 20.16 | 15.0 | 40.00 | 59.0 | 74.00 | 100.0 | ▅▆▇▇▂ |
Interpretación: estos resultados permiten verificar que las variables estén correctamente importadas, identificar valores faltantes y revisar los tipos de datos antes de construir el modelo.
Código 3. Preparar las variables
La hora se transformará en una variable numérica que representa la hora del día. Por ejemplo, 09:30 se convierte en 9 y 14:45 en 14. Es una aproximación por hora, no una medida exacta de minutos.
datos_modelo <- appointments %>%
mutate(
age = as.numeric(as.character(age)),
waiting_time = as.numeric(as.character(waiting_time)),
hora = as.numeric(
sub(
"^\\s*([0-9]{1,2}).*",
"\\1",
as.character(appointment_time)
)
),
sex = as.factor(sex)
) %>%
filter(
status == "attended",
!is.na(waiting_time),
!is.na(age),
!is.na(sex),
!is.na(hora),
hora >= 0,
hora <= 23,
waiting_time >= 0
) %>%
select(
waiting_time,
age,
sex,
hora
) %>%
tidyr::drop_na()Interpretación: se conservan las citas atendidas con información disponible para el análisis. Se excluyen tiempos de espera negativos y registros incompletos en las variables seleccionadas. Antes de continuar, verifica que appointment_time tenga el formato esperado y que waiting_time esté expresado en una unidad coherente.
Código 4. Comprobar los datos preparados
## [1] 86032 4
## waiting_time age sex hora
## Min. : 0.60 Min. : 15.00 Female:51096 Min. : 8.0
## 1st Qu.: 12.60 1st Qu.: 40.00 Male :34936 1st Qu.:10.0
## Median : 33.50 Median : 59.00 Median :13.0
## Mean : 44.09 Mean : 57.23 Mean :12.5
## 3rd Qu.: 64.60 3rd Qu.: 74.00 3rd Qu.:15.0
## Max. :297.30 Max. :100.00 Max. :17.0
##
## Female Male
## 51096 34936
Interpretación: dim() indica cuántas observaciones quedaron después de la limpieza; summary() resume las variables y table() muestra la cantidad de registros por categoría de sexo. La cantidad final puede ser menor que las 111.488 filas originales por los filtros aplicados.
El conjunto de entrenamiento (datos usados para ajustar el modelo) contendrá el 75 % de las observaciones; el conjunto de prueba (datos reservados para evaluar el desempeño) contendrá el 25 % restante.
Código 5. Crear la división 75/25
set.seed(123)
division <- rsample::initial_split(
datos_modelo,
prop = 0.75
)
entrenamiento <- rsample::training(division)
prueba <- rsample::testing(division)
# Verificar tamaños
nrow(entrenamiento)## [1] 64524
## [1] 21508
Interpretación: el modelo se ajustará exclusivamente con entrenamiento. La base prueba se reservará para comprobar qué tan bien predice observaciones que no utilizó al ajustarse.
Advertencia: si una misma persona tiene múltiples citas, una división aleatoria por fila puede colocar citas del mismo paciente en ambos conjuntos. Por eso, la evaluación es preliminar; una validación posterior debería separar los datos por paciente o por periodo temporal.
El flujo de trabajo será el siguiente: Base Appointments, Selección y limpiezaDivisión 75% / 25%EntrenamientoRegresión linealPrediccionesDatos de pruebaCalcular RMSEInterpretar y proponer mejoras
El modelo relaciona el tiempo de espera con la edad, el sexo registrado y la hora de la cita. El objetivo es estimar valores, no establecer que estas variables sean causas de las demoras.
Código 6. Entrenar el modelo
##
## Call:
## lm(formula = waiting_time ~ age + sex + hora, data = entrenamiento)
##
## Residuals:
## Min 1Q Median 3Q Max
## -68.249 -24.376 -6.245 18.853 228.499
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -24.672279 0.785143 -31.424 <2e-16 ***
## age 0.003609 0.007445 0.485 0.628
## sexMale 0.160333 0.305467 0.525 0.600
## hora 5.479451 0.051569 106.254 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 37.6 on 64520 degrees of freedom
## Multiple R-squared: 0.1489, Adjusted R-squared: 0.1489
## F-statistic: 3764 on 3 and 64520 DF, p-value: < 2.2e-16
Interpretación: el resultado muestra los coeficientes del modelo, que describen cómo se relaciona cada variable con el tiempo de espera estimado, manteniendo las demás constantes. Los valores p ayudan a evaluar la evidencia estadística de cada asociación, pero no demuestran causalidad.
El \(R^2\) indica qué proporción de la variabilidad de los tiempos de espera se explica por las variables incluidas en el modelo dentro de los datos analizados. Un \(R^2\) alto no garantiza que las predicciones sean precisas en datos nuevos.
Código 7. Predecir los tiempos de espera
# Predicciones para entrenamiento
pred_entrenamiento <- predict(
modelo,
newdata = entrenamiento
)
# Predicciones para prueba
pred_prueba <- predict(
modelo,
newdata = prueba
)
# Revisar algunos resultados
head(data.frame(
real = prueba$waiting_time,
predicho = pred_prueba
))Interpretación: la columna real contiene el tiempo de espera registrado y predicho el valor estimado por el modelo. La diferencia entre ambos indica el error de cada predicción.
Un residuo es la diferencia entre el valor observado y el valor estimado por el modelo. La homocedasticidad significa que la dispersión de los errores es aproximadamente constante en distintos niveles de predicción.
Código 8. Extraer los residuos y graficarlos
diagnostico <- broom::augment(modelo)
ggplot(
diagnostico,
aes(x = .fitted, y = .resid)
) +
geom_point(alpha = 0.3) +
geom_hline(
yintercept = 0,
linetype = "dashed"
) +
labs(
title = "Diagnóstico de residuos",
x = "Tiempo de espera estimado",
y = "Residuo"
) +
theme_minimal()Interpretación: Este gráfico de Diagnóstico de residuos muestra que el modelo estadístico utilizado presenta un problema de heterocedasticidad, lo que significa que los errores de predicción no son constantes y aumentan a medida que el tiempo de espera es mayor.
Código 9. Revisar la normalidad aproximada de los residuos
qqnorm(
diagnostico$.resid,
main = "Gráfico Q-Q de los residuos"
)
qqline(
diagnostico$.resid,
col = "red"
)Interpretación: Este Gráfico Q-Q de los residuos demuestra que los errores del modelo no siguen una distribución normal, presentando una fuerte desviación o asimetría hacia la derecha (sesgo positivo).
Conectando esto con el gráfico anterior de heterocedasticidad, se confirma que el modelo está fallando al predecir los valores más altos. Los residuos están fuertemente sesgados debido a que el modelo subestima drásticamente los tiempos de espera más largos.
El RMSE resume el tamaño del error de predicción en las mismas unidades que la variable objetivo. Cuanto menor sea, mejor será la precisión promedio según esta métrica.
Código 10. Comparar entrenamiento y prueba
rmse_entrenamiento <- yardstick::rmse_vec(
truth = entrenamiento$waiting_time,
estimate = as.numeric(pred_entrenamiento)
)
rmse_prueba <- yardstick::rmse_vec(
truth = prueba$waiting_time,
estimate = as.numeric(pred_prueba)
)
metricas <- data.frame(
Conjunto = c("Entrenamiento", "Prueba"),
RMSE = c(rmse_entrenamiento, rmse_prueba)
)
metricasInterpretación: compara ambos valores de RMSE. Si el error de prueba es mucho mayor que el de entrenamiento, puede haber sobreajuste o diferencias entre los datos. Si ambos son altos, las variables incluidas pueden ser insuficientes para estimar adecuadamente los tiempos de espera.
No se debe afirmar que el modelo cumple la meta de error máximo hasta observar el resultado y confirmar la unidad de medida. El umbral de 10 unidades es una meta provisional, no un resultado garantizado.
Código 11. Comparar las predicciones en los datos de prueba
resultados <- data.frame(
Real = prueba$waiting_time,
Predicho = as.numeric(pred_prueba),
Edad = prueba$age,
Sexo = prueba$sex,
Hora = prueba$hora
)
grafico_predicciones <- ggplot(
resultados,
aes(
x = Real,
y = Predicho,
color = Sexo
)
) +
geom_point(alpha = 0.5) +
geom_abline(
intercept = 0,
slope = 1,
linetype = "dashed"
) +
labs(
title = "Tiempos de espera reales y predichos",
x = "Tiempo real",
y = "Tiempo predicho"
) +
theme_minimal()
plotly::ggplotly(grafico_predicciones)Interpretación: los puntos cercanos a la línea diagonal representan predicciones más próximas a los valores reales. Una separación amplia indica errores importantes. Al pasar el cursor sobre los puntos se pueden explorar las observaciones.
Este gráfico relaciona el tiempo real, el tiempo estimado y la edad de cada paciente.
Código 12. Comparación tridimensional
plotly::plot_ly(
data = resultados,
x = ~Real,
y = ~Predicho,
z = ~Edad,
color = ~Sexo,
type = "scatter3d",
mode = "markers",
marker = list(size = 3),
text = ~paste(
"Real:", round(Real, 2),
"<br>Predicho:", round(Predicho, 2),
"<br>Edad:", Edad,
"<br>Sexo:", Sexo
),
hoverinfo = "text"
) %>%
plotly::layout(
title = "Gráfico 3D: espera real, predicha y edad",
scene = list(
xaxis = list(title = "Tiempo real"),
yaxis = list(title = "Tiempo predicho"),
zaxis = list(title = "Edad")
)
)Interpretación: El gráfico confirma que tienes una base de datos densa y bien distribuida por edad y género, pero reafirma que los casos con Tiempos reales excesivamente largos (mayores a 100) actúan como valores atípicos que están desestabilizando las predicciones de tu modelo actual.
Este gráfico examina la relación entre la hora programada, la edad y el tiempo de espera predicho.
Código 13. Explorar los tiempos estimados por hora y edad
plotly::plot_ly(
data = resultados,
x = ~Hora,
y = ~Edad,
z = ~Predicho,
color = ~Sexo,
type = "scatter3d",
mode = "markers",
marker = list(size = 3),
text = ~paste(
"Hora:", Hora,
"<br>Edad:", Edad,
"<br>Espera predicha:", round(Predicho, 2),
"<br>Sexo:", Sexo
),
hoverinfo = "text"
) %>%
plotly::layout(
title = "Gráfico 3D: hora, edad y espera predicha",
scene = list(
xaxis = list(title = "Hora programada"),
yaxis = list(title = "Edad"),
zaxis = list(title = "Tiempo predicho")
)
)Interpretación: Este gráfico refleja la rigidez matemática del modelo actual. Al usar una regresión lineal simple o directa, el modelo elimina toda la complejidad de la realidad y asume una relación completamente rígida y proporcional entre la edad de la persona, la hora de su cita y el tiempo que va a esperar.
Una vez obtenidos los resultados, se proponen las siguientes acciones para mejorar la utilidad del análisis en la gestión sanitaria. Acción 1. Incorporar variables operativas adicionales. Evaluar la disponibilidad de datos sobre servicio o especialidad, día de la semana, anticipación de la cita, carga de pacientes por franja horaria y retrasos en el inicio de la atención. Estas variables podrían aportar información que actualmente no tiene el modelo.
Acción 2. Comparar algoritmos. Comparar la regresión lineal con otro método, como los bosques aleatorios (algoritmo que combina varios árboles de decisión), utilizando la misma separación de entrenamiento y prueba. La comparación debe basarse en el RMSE de prueba, no solo en el ajuste sobre los datos de entrenamiento.
Acción 3. Mejorar la validación. Separar los datos por paciente o por periodos temporales para comprobar si el modelo mantiene su desempeño en pacientes nuevos o en periodos posteriores. Esto es especialmente importante si se pretende utilizar el modelo para apoyar decisiones operativas.
Puedes incluir el siguiente texto al final del capítulo y completar los espacios con los resultados obtenidos en RStudio.
Se construyó un modelo de regresión lineal para estimar el tiempo de espera de las citas atendidas, utilizando como variables predictoras la edad, el sexo registrado y la hora programada. El desempeño se evaluó mediante el RMSE en los conjuntos de entrenamiento y prueba.
El RMSE de entrenamiento fue de [completar resultado], mientras que el RMSE de prueba fue de [completar resultado]. Estos valores permiten evaluar la precisión de las estimaciones y determinar si existe una diferencia importante entre el desempeño con datos conocidos y datos no utilizados durante el ajuste.
Desde la perspectiva de la gestión de recursos sanitarios, el modelo constituye un ejercicio inicial para identificar oportunidades de mejora en la organización de las agendas y en la oportunidad de atención. Sin embargo, antes de utilizarlo en decisiones reales, será necesario validar su precisión, revisar los supuestos estadísticos e incorporar otras variables operativas relevantes.