Tema 1. Exploración Analítica de Datos

El presente apartado tiene como propósito realizar una exploración inicial de la base de datos de citas médicas. La exploración se centra en reconocer la estructura de los datos, identificar las variables disponibles, revisar los tipos de información, verificar la presencia de datos faltantes y realizar visualizaciones exploratorias.

1.1. Importación y reconocimiento de la base de datos

# Cargar paquetes
library(readxl)
library(tidyverse)
library(plotly)

# Importar el archivo Excel
appointment <- read_excel(
  "Appointment . Proyecto.xlsx",
  sheet = "Appointment (1)"
)

# Mostrar las primeras filas
head(appointment)
# Mostrar los archivos disponibles en la carpeta de trabajo
list.files()
##  [1] "Analitica de datos.Rproj"              
##  [2] "Appointment . Proyecto.xlsx"           
##  [3] "appointments final.xlsx"               
##  [4] "EncuestaBrfss2020-selected-columns.csv"
##  [5] "Practica de R mack.Rmd"                
##  [6] "R-Marckdown---Proyecto-1.html"         
##  [7] "R-Marckdown---Proyecto-1.Rmd"          
##  [8] "R-Marckdown--clase.html"               
##  [9] "R Marckdown-  Proyecto 1.Rmd"          
## [10] "R Marckdown- clase.Rmd"                
## [11] "RD202401.csv.crdownload"               
## [12] "rsconnect"

Appointment . Proyecto.xlsx

1.2. Identificación del número de filas y columnas

# Número de filas
nrow(appointment)
## [1] 1000
# Número de columnas
ncol(appointment)
## [1] 5
# Dimensiones completas
dim(appointment)
## [1] 1000    5

1.3. Identificación de las variables

# Mostrar los nombres de las variables
names(appointment)
## [1] "AppointmentID" "Date"          "Time"          "PatientID"    
## [5] "DoctorID"

Las variables disponibles son:

  • AppointmentID: identificador de cada cita.
  • Date: fecha asociada al registro de la cita.
  • Time: información temporal registrada en la base.
  • PatientID: identificador del paciente.
  • DoctorID: identificador del médico.

1.4. Reconocimiento de los tipos de variables

# Revisar la estructura de la base
str(appointment)
## tibble [1,000 × 5] (S3: tbl_df/tbl/data.frame)
##  $ AppointmentID: num [1:1000] 639 404 281 628 889 551 769 385 813 986 ...
##  $ Date         : POSIXct[1:1000], format: "2022-04-08" "2023-04-10" ...
##  $ Time         : chr [1:1000] "2023-12-23T14:33:46.406Z" "2023-12-23T14:33:46.407Z" "2023-12-23T14:33:46.407Z" "2023-12-23T14:33:46.407Z" ...
##  $ PatientID    : num [1:1000] 109 823 465 443 852 160 886 480 634 898 ...
##  $ DoctorID     : num [1:1000] 462 774 226 656 262 748 580 438 662 653 ...
# Tipo de cada variable
sapply(appointment, class)
## $AppointmentID
## [1] "numeric"
## 
## $Date
## [1] "POSIXct" "POSIXt" 
## 
## $Time
## [1] "character"
## 
## $PatientID
## [1] "numeric"
## 
## $DoctorID
## [1] "numeric"

1.5. Exploración inicial de los registros

# Primeros 10 registros
head(appointment, 10)
# Últimos 10 registros
tail(appointment, 10)

1.6. Reconocimiento de valores diferentes

# Cantidad de valores diferentes por variable
sapply(appointment, n_distinct)
## AppointmentID          Date          Time     PatientID      DoctorID 
##           602           721            12           610           611

1.7. Revisión de datos faltantes

# Cantidad de datos faltantes por variable
colSums(is.na(appointment))
## AppointmentID          Date          Time     PatientID      DoctorID 
##             0             0             0             0             0
# Comprobar si existe algún dato faltante
any(is.na(appointment))
## [1] FALSE

1.8. Preparación básica de las fechas

# Convertir Date a formato de fecha
appointment$Date <- as.Date(appointment$Date)

# Revisar la variable
class(appointment$Date)
## [1] "Date"
# Fecha más antigua registrada
min(appointment$Date)
## [1] "2020-01-01"
# Fecha más reciente registrada
max(appointment$Date)
## [1] "2023-12-31"

1.9. Visualización exploratoria mediante gráficos interactivos

Se utilizará el paquete plotly, que permite generar gráficos interactivos en el documento HTML de R Markdown.

1.9.1. Registros de citas según fecha

# Contar registros por fecha
citas_fecha <- appointment %>%
  count(Date)

# Crear gráfico interactivo
plot_ly(
  data = citas_fecha,
  x = ~Date,
  y = ~n,
  type = "scatter",
  mode = "lines+markers"
) %>%
  layout(
    title = "Registros de citas según fecha",
    xaxis = list(title = "Fecha"),
    yaxis = list(title = "Número de registros")
  )

Esta visualización permite reconocer cómo se distribuyen los registros de citas a lo largo del periodo cubierto por la base de datos. Al representar las fechas en el eje horizontal y el número de registros en el eje vertical, es posible identificar visualmente las fechas con mayor o menor presencia de registros y observar variaciones aparentes a lo largo del tiempo. Desde la gestión sanitaria, esta exploración puede servir como punto de partida para conocer la organización temporal de los registros de atención. Sin embargo, por sí sola no permite determinar la demanda real de servicios, la disponibilidad de citas ni las causas de las variaciones observadas.

1.9.2. Registros de citas por DoctorID

# Contar registros por médico
citas_doctor <- appointment %>%
  count(DoctorID, sort = TRUE)

# Seleccionar los 10 DoctorID con mayor cantidad de registros
citas_doctor_10 <- citas_doctor %>%
  slice_head(n = 10)

# Crear gráfico interactivo
plot_ly(
  data = citas_doctor_10,
  x = ~DoctorID,
  y = ~n,
  type = "bar",
  text = ~n,
  textposition = "auto"
) %>%
  layout(
    title = "Registros de citas por DoctorID",
    xaxis = list(title = "DoctorID"),
    yaxis = list(title = "Número de registros")
  )

El gráfico de barras presenta los diez identificadores de médicos con mayor cantidad de registros en la base. La altura de cada barra representa el número de registros asociados a cada DoctorID. Esto permite comparar visualmente la presencia de los distintos identificadores en los datos y reconocer si algunos concentran más registros que otros. En el contexto de la administración de recursos sanitarios, esta información constituye una aproximación inicial a la distribución de los registros por médico. No obstante, no debe interpretarse directamente como productividad, desempeño laboral o carga asistencial, ya que la base explorada no aporta por sí sola información suficiente sobre jornadas, horas trabajadas, complejidad de los pacientes o tipo de atención.

1.9.3. Registros asociados a PatientID

# Contar registros por paciente
citas_paciente <- appointment %>%
  count(PatientID, sort = TRUE)

# Seleccionar los 10 PatientID con mayor cantidad de registros
citas_paciente_10 <- citas_paciente %>%
  slice_head(n = 10)

# Crear gráfico interactivo
plot_ly(
  data = citas_paciente_10,
  x = ~PatientID,
  y = ~n,
  type = "bar",
  text = ~n,
  textposition = "auto"
) %>%
  layout(
    title = "Registros asociados a PatientID",
    xaxis = list(title = "PatientID"),
    yaxis = list(title = "Número de registros")
  )

El gráfico permite identificar visualmente los PatientID que presentan una mayor cantidad de registros dentro de la base de datos.

1.9.4. Registros según Time

# Contar los registros de cada valor de Time
citas_time <- appointment %>%
  count(Time)

# Crear gráfico interactivo
plot_ly(
  data = citas_time,
  x = ~Time,
  y = ~n,
  type = "bar",
  text = ~n,
  textposition = "auto"
) %>%
  layout(
    title = "Registros según Time",
    xaxis = list(title = "Time"),
    yaxis = list(title = "Número de registros")
  )

El gráfico permite observar cómo están distribuidos los registros entre los diferentes valores presentes en la variable Time.

Interpretación de la exploración

La exploración inicial permitió reconocer una base de datos de 1.000 registros y 5 variables: AppointmentID, Date, Time, PatientID y DoctorID. Se identificó la función de cada variable, se revisó la estructura de la información y se verificó la presencia de datos faltantes.

Las visualizaciones permiten explorar la distribución de los registros según fecha, médico, paciente y tiempo. Esta primera aproximación se limita al reconocimiento y visualización de los datos y no incorpora procedimientos de estadística descriptiva o inferencial.

En conjunto, el ejercicio permitió conocer la estructura de la base de datos, identificar sus variables, revisar sus formatos, comprobar la presencia de datos faltantes y elaborar visualizaciones interactivas para explorar los registros de citas desde diferentes perspectivas: temporal, por médico y por paciente. Los gráficos facilitan la identificación visual de categorías, frecuencias de registro y patrones aparentes que pueden resultar relevantes para la administración de servicios de salud. Sin embargo, los resultados deben entenderse como una exploración inicial de los datos disponibles, no como una evaluación del funcionamiento de la institución ni de la calidad de la atención

Tema 2. Probabilidad y Estadística Aplicada”

datos <- read_excel(
  "Appointment . Proyecto.xlsx",
  sheet = 1
)

dim(datos)
## [1] 1000    5
names(datos)
## [1] "AppointmentID" "Date"          "Time"          "PatientID"    
## [5] "DoctorID"
head(datos)
# Asegurar que Date tenga formato de fecha
if (inherits(datos$Date, "POSIXt")) {
  datos$Date <- as.Date(datos$Date)
} else if (is.numeric(datos$Date)) {
  datos$Date <- as.Date(
    datos$Date,
    origin = "1899-12-30"
  )
} else {
  datos$Date <- as.Date(as.character(datos$Date))
}

# Tratar los identificadores como códigos
datos$AppointmentID <- as.character(datos$AppointmentID)
datos$PatientID <- as.character(datos$PatientID)
datos$DoctorID <- as.character(datos$DoctorID)

# Crear variables de día y mes en español
dias <- c(
  "domingo", "lunes", "martes", "miércoles",
  "jueves", "viernes", "sábado"
)

orden_dias <- c(
  "lunes", "martes", "miércoles", "jueves",
  "viernes", "sábado", "domingo"
)

meses <- c(
  "enero", "febrero", "marzo", "abril",
  "mayo", "junio", "julio", "agosto",
  "septiembre", "octubre", "noviembre", "diciembre"
)

datos$DiaSemana <- factor(
  dias[as.POSIXlt(datos$Date)$wday + 1],
  levels = orden_dias
)

datos$Mes <- factor(
  meses[as.POSIXlt(datos$Date)$mon + 1],
  levels = meses
)

head(datos)
set.seed(123)

# Seleccionar hasta 30 citas sin repetir registros
numero_muestra <- min(30, nrow(datos))

indices_muestra <- sample(
  seq_len(nrow(datos)),
  size = numero_muestra,
  replace = FALSE
)

muestra_citas <- datos[indices_muestra, ]

# Mostrar los primeros registros seleccionados
knitr::kable(
  head(muestra_citas, 10),
  caption = "Registros seleccionados aleatoriamente"
)
Registros seleccionados aleatoriamente
AppointmentID Date Time PatientID DoctorID DiaSemana Mes
114 2023-10-08 2023-12-23T14:33:46.412Z 214 714 domingo octubre
488 2023-11-10 2023-12-23T14:33:46.412Z 590 370 viernes noviembre
311 2023-07-06 2023-12-23T14:33:46.410Z 468 617 jueves julio
807 2022-04-04 2023-12-23T14:33:46.413Z 372 630 lunes abril
974 2021-02-03 2023-12-23T14:33:46.410Z 802 546 miércoles febrero
237 2020-10-30 2023-12-23T14:33:46.417Z 281 522 viernes octubre
863 2022-11-10 2023-12-23T14:33:46.415Z 448 112 jueves noviembre
315 2022-03-26 2023-12-23T14:33:46.408Z 616 185 sábado marzo
398 2023-07-08 2023-12-23T14:33:46.411Z 336 406 sábado julio
423 2021-09-29 2023-12-23T14:33:46.410Z 168 919 miércoles septiembre
# Excluir fechas que no pudieron interpretarse
dias_validos <- datos$DiaSemana[!is.na(datos$DiaSemana)]

# Frecuencias observadas
frecuencia_dias <- table(dias_validos)
frecuencia_dias
## dias_validos
##     lunes    martes miércoles    jueves   viernes    sábado   domingo 
##       146       146       145       137       157       134       135
# Probabilidades empíricas
probabilidad_dias <- prop.table(frecuencia_dias)
round(probabilidad_dias, 3)
## dias_validos
##     lunes    martes miércoles    jueves   viernes    sábado   domingo 
##     0.146     0.146     0.145     0.137     0.157     0.134     0.135
# Probabilidad empírica de una cita registrada el viernes
prob_viernes <- mean(
  datos$DiaSemana == "viernes",
  na.rm = TRUE
)

prob_viernes
## [1] 0.157
prob_viernes_base <- mean(
  datos$DiaSemana == "viernes",
  na.rm = TRUE
)

prob_viernes_muestra <- mean(
  muestra_citas$DiaSemana == "viernes",
  na.rm = TRUE
)

prob_viernes_base
## [1] 0.157
prob_viernes_muestra
## [1] 0.1666667

Interpretación: la primera cifra corresponde a toda la base y la segunda a la muestra seleccionada. Si difieren, esa diferencia ilustra el efecto de la selección aleatoria. Una muestra pequeña puede representar de manera imperfecta la distribución completa.

2.1.2. Simulación de variables aleatorias discretas y continuas

set.seed(123)

conteos_simulados <- floor(
  runif(100, min = 0, max = 11)
)

table(conteos_simulados)
## conteos_simulados
##  0  1  2  3  4  5  6  7  8  9 10 
##  5 13  9  8 13  8  6 11 12  9  6

Interpretación: la tabla presenta cuántas veces aparece cada conteo generado. Se trata de datos simulados, no de citas efectivamente registradas. El ejemplo permite comprender la variación que introduce un proceso aleatorio.

set.seed(123)

confirmacion_simulada <- rbinom(
  n = 100,
  size = 1,
  prob = 0.90
)

table(confirmacion_simulada)
## confirmacion_simulada
##  0  1 
##  7 93
# Proporción simulada de resultados favorables
mean(confirmacion_simulada)
## [1] 0.93

Interpretación: 1 representa el resultado favorable supuesto y 0 representa el otro resultado. La media de la variable indica la proporción de resultados favorables en la simulación. No debe reportarse como la tasa real de confirmación o asistencia de los pacientes.

fecha_minima <- min(datos$Date, na.rm = TRUE)
fecha_maxima <- max(datos$Date, na.rm = TRUE)

calendario <- data.frame(
  Date = seq(fecha_minima, fecha_maxima, by = "day")
)

resumen_diario <- calendario %>%
  left_join(
    datos %>%
      filter(!is.na(Date)) %>%
      count(Date, name = "citas"),
    by = "Date"
  ) %>%
  mutate(citas = tidyr::replace_na(citas, 0L))

# Calcular el promedio de citas por día calendario
lambda <- mean(resumen_diario$citas)

# Probabilidad teórica de distintos conteos diarios
max_citas <- max(resumen_diario$citas)

prob_poisson <- data.frame(
  citas = 0:max_citas,
  probabilidad = dpois(
    0:max_citas,
    lambda = lambda
  )
)

lambda
## [1] 0.6844627
prob_poisson

Interpretación: lambda es el promedio observado de citas por día calendario. dpois() calcula la probabilidad teórica de registrar exactamente cero, una, dos o más citas durante un día, según una distribución de Poisson (modelo probabilístico para contar eventos en un intervalo). Esta distribución es una aproximación para practicar probabilidad; no se puede afirmar que describa perfectamente la demanda real sin evaluar sus supuestos.

set.seed(123)

citas_simuladas_poisson <- rpois(
  n = 100,
  lambda = lambda
)

table(citas_simuladas_poisson)
## citas_simuladas_poisson
##  0  1  2  3 
## 53 32 13  2

Interpretación: la tabla muestra los conteos que produjo la simulación. Sirve para visualizar la variabilidad que podría aparecer bajo el supuesto de Poisson, pero no equivale a una predicción validada de citas futuras.

set.seed(123)

# Simulación normal: media de 30 minutos, desviación de 5
tiempo_normal <- rnorm(
  1000,
  mean = 30,
  sd = 5
)

# Simulación exponencial: media teórica de 15 minutos
tiempo_exponencial <- rexp(
  1000,
  rate = 1 / 15
)

# Simulación uniforme: tiempos entre 10 y 30 minutos
tiempo_uniforme <- runif(
  1000,
  min = 10,
  max = 30
)

simulaciones <- data.frame(
  Normal = tiempo_normal,
  Exponencial = tiempo_exponencial,
  Uniforme = tiempo_uniforme
)

simulaciones_largas <- tidyr::pivot_longer(
  simulaciones,
  cols = everything(),
  names_to = "Distribucion",
  values_to = "Minutos"
)

head(simulaciones_largas)

Interpretación: los datos simulados presentan tres comportamientos diferentes. La normal concentra valores cerca de una media, la exponencial genera tiempos positivos con frecuencia alta de valores pequeños y la uniforme distribuye los valores dentro de un intervalo definido. Todos los resultados dependen de los parámetros elegidos para el ejercicio.

# Distribución normal
dnorm(35, mean = 30, sd = 5)
## [1] 0.04839414
pnorm(35, mean = 30, sd = 5)
## [1] 0.8413447
# Distribución exponencial
dexp(10, rate = 1 / 15)
## [1] 0.03422781
pexp(10, rate = 1 / 15)
## [1] 0.4865829
# Probabilidad acumulada para una uniforme
punif(20, min = 10, max = 30)
## [1] 0.5

Interpretación: pnorm() y pexp() calculan probabilidades acumuladas hasta un valor específico. Por ejemplo, pnorm(35, …) calcula la probabilidad de obtener 35 minutos o menos según el escenario normal propuesto. dnorm() y dexp() devuelven densidades, que no son directamente probabilidades de obtener exactamente un valor continuo.

grafico_distribuciones <- ggplot(
  simulaciones_largas,
  aes(x = Minutos)
) +
  geom_histogram(
    bins = 30,
    fill = "steelblue",
    color = "white"
  ) +
  facet_wrap(
    ~ Distribucion,
    scales = "free_y"
  ) +
  labs(
    title = "Distribuciones de tiempos hipotéticos",
    x = "Tiempo simulado en minutos",
    y = "Frecuencia"
  ) +
  theme_minimal()

ggplotly(grafico_distribuciones)

Comentario analítico: cada histograma representa la frecuencia de los tiempos generados bajo un supuesto diferente. La comparación ayuda a entender cómo cambia la variabilidad entre distribuciones. No permite concluir que los tiempos reales del servicio sanitario sigan alguno de esos patrones.

2.2 Estadística descriptiva y visualización de datos

2.2.1. Medidas descriptivas de la actividad de citas

# Medidas del número de citas por día
media_citas <- mean(resumen_diario$citas)
mediana_citas <- median(resumen_diario$citas)
desviacion_citas <- sd(resumen_diario$citas)
varianza_citas <- var(resumen_diario$citas)
rango_citas <- range(resumen_diario$citas)

media_citas
## [1] 0.6844627
mediana_citas
## [1] 0
desviacion_citas
## [1] 0.8199304
varianza_citas
## [1] 0.6722858
rango_citas
## [1] 0 4
summary(datos)
##    AppointmentID       Date                   Time          PatientID   
##  Length   :1000   Min.   :2020-01-01   Length   :1000   Length   :1000  
##  N.unique : 602   1st Qu.:2020-12-19   N.unique :  12   N.unique : 610  
##  N.blank  :   0   Median :2021-12-20   N.blank  :   0   N.blank  :   0  
##  Min.nchar:   3   Mean   :2021-12-22   Min.nchar:  24   Min.nchar:   3  
##  Max.nchar:   3   3rd Qu.:2023-01-05   Max.nchar:  24   Max.nchar:   4  
##                   Max.   :2023-12-31                                    
##                                                                         
##       DoctorID        DiaSemana           Mes     
##  Length   :1000   lunes    :146   enero     : 97  
##  N.unique : 611   martes   :146   abril     : 96  
##  N.blank  :   0   miércoles:145   junio     : 87  
##  Min.nchar:   3   jueves   :137   diciembre : 87  
##  Max.nchar:   4   viernes  :157   noviembre : 82  
##                   sábado   :134   septiembre: 81  
##                   domingo  :135   (Other)   :470
summary(resumen_diario$citas)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##  0.0000  0.0000  0.0000  0.6845  1.0000  4.0000

2.2.2. Gráficos interactivos y comparativos para la gestión sanitaria

citas_dia <- datos %>%
  count(DiaSemana, name = "total_citas", .drop = FALSE)

grafico_dias <- ggplot(
  citas_dia,
  aes(
    x = DiaSemana,
    y = total_citas,
    text = paste(
      "Día:", DiaSemana,
      "<br>Citas:", total_citas
    )
  )
) +
  geom_col(fill = "steelblue") +
  labs(
    title = "Distribución de citas por día de la semana",
    x = "Día de la semana",
    y = "Número de citas"
  ) +
  theme_minimal()

ggplotly(grafico_dias, tooltip = "text")

Comentario analítico: el gráfico permite identificar los días con mayor y menor cantidad de citas registradas. Una concentración de registros en un día puede motivar una revisión de las agendas, la disponibilidad de consultorios y el personal asignado. Sin embargo, el gráfico por sí solo no demuestra que exista sobrecarga o falta de capacidad.

citas_mes <- datos %>%
  count(Mes, name = "total_citas", .drop = FALSE)

grafico_meses <- ggplot(
  citas_mes,
  aes(
    x = Mes,
    y = total_citas,
    text = paste(
      "Mes:", Mes,
      "<br>Citas:", total_citas
    )
  )
) +
  geom_col(fill = "darkcyan") +
  labs(
    title = "Distribución de las citas por mes",
    x = "Mes",
    y = "Número de citas"
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(
      angle = 45,
      hjust = 1
    )
  )

ggplotly(grafico_meses, tooltip = "text")

Comentario analítico: el gráfico muestra cuántas citas corresponden a cada mes, sumando los años presentes en la base. Permite explorar patrones estacionales que podrían orientar una revisión de la planificación. Como acumula varios años, no demuestra por sí mismo que un mes específico tenga la misma demanda en todos los años.

resumen_diario$DiaSemana <- factor(
  dias[as.POSIXlt(resumen_diario$Date)$wday + 1],
  levels = orden_dias
)
grafico_comparativo <- ggpubr::ggboxplot(
  resumen_diario,
  x = "DiaSemana",
  y = "citas",
  color = "DiaSemana",
  add = "jitter"
) +
  labs(
    title = "Variabilidad de las citas diarias por día semanal",
    x = "Día de la semana",
    y = "Citas por día calendario"
  ) +
  theme_minimal()

ggplotly(grafico_comparativo)

Comentario analítico: este gráfico compara no solo los conteos de cada día semanal, sino también su variación entre distintas fechas. La caja representa la distribución central de los valores y los puntos muestran observaciones diarias. Un día con una distribución más alta puede concentrar más citas; una distribución más dispersa indica que la cantidad de citas varía más entre fechas.

resumen_diario$TipoDia <- ifelse(
  resumen_diario$DiaSemana %in% c("sábado", "domingo"),
  "Fin de semana",
  "Entre semana"
)

resumen_diario$TipoDia <- factor(
  resumen_diario$TipoDia,
  levels = c("Entre semana", "Fin de semana")
)

prueba_t <- t.test(
  citas ~ TipoDia,
  data = resumen_diario
)

prueba_t
## 
##  Welch Two Sample t-test
## 
## data:  citas by TipoDia
## t = 1.2162, df = 780.07, p-value = 0.2243
## alternative hypothesis: true difference in means between group Entre semana and group Fin de semana is not equal to 0
## 95 percent confidence interval:
##  -0.0352005  0.1498450
## sample estimates:
##  mean in group Entre semana mean in group Fin de semana 
##                   0.7008629                   0.6435407

Interpretación: el resultado compara los promedios de citas por día calendario en los dos grupos y presenta un intervalo de confianza para la diferencia de medias. Si p < 0.05, existe evidencia estadística de una diferencia entre los promedios, bajo los supuestos de la prueba. Si p ≥ 0.05, no hay evidencia suficiente para concluir que los promedios sean diferentes. Esto no demuestra que sean idénticos. Desde la gestión sanitaria, la comparación puede ayudar a describir cómo cambia la actividad registrada entre días laborales y fines de semana. Como las fechas consecutivas pueden compartir condiciones, el resultado debe considerarse exploratorio y no una prueba definitiva de una diferencia estructural.

modelo_anova <- aov(
  citas ~ DiaSemana,
  data = resumen_diario
)

summary(modelo_anova)
##               Df Sum Sq Mean Sq F value Pr(>F)
## DiaSemana      6    2.0  0.3256   0.483  0.821
## Residuals   1454  979.6  0.6737

Interpretación: el ANOVA evalúa si las medias del número diario de citas pueden considerarse iguales entre los días de la semana. Si el valor p < 0.05, hay evidencia de que al menos una media difiere, aunque el resultado general no señala por sí solo cuáles días son diferentes.

TukeyHSD(modelo_anova)
##   Tukey multiple comparisons of means
##     95% family-wise confidence level
## 
## Fit: aov(formula = citas ~ DiaSemana, data = resumen_diario)
## 
## $DiaSemana
##                            diff        lwr       upr     p adj
## martes-lunes      -1.110223e-16 -0.2376330 0.2376330 1.0000000
## miércoles-lunes   -8.143173e-03 -0.2454917 0.2292054 0.9999999
## jueves-lunes      -4.642068e-02 -0.2837692 0.1909279 0.9974324
## viernes-lunes      4.927310e-02 -0.1880755 0.2866216 0.9964242
## sábado-lunes      -6.077475e-02 -0.2981233 0.1765738 0.9888592
## domingo-lunes     -5.599006e-02 -0.2933386 0.1813585 0.9928120
## miércoles-martes  -8.143173e-03 -0.2454917 0.2292054 0.9999999
## jueves-martes     -4.642068e-02 -0.2837692 0.1909279 0.9974324
## viernes-martes     4.927310e-02 -0.1880755 0.2866216 0.9964242
## sábado-martes     -6.077475e-02 -0.2981233 0.1765738 0.9888592
## domingo-martes    -5.599006e-02 -0.2933386 0.1813585 0.9928120
## jueves-miércoles  -3.827751e-02 -0.2753413 0.1987863 0.9991305
## viernes-miércoles  5.741627e-02 -0.1796475 0.2944801 0.9917174
## sábado-miércoles  -5.263158e-02 -0.2896954 0.1844322 0.9948287
## domingo-miércoles -4.784689e-02 -0.2849107 0.1892169 0.9969409
## viernes-jueves     9.569378e-02 -0.1413700 0.3327576 0.8974050
## sábado-jueves     -1.435407e-02 -0.2514179 0.2227097 0.9999973
## domingo-jueves    -9.569378e-03 -0.2466332 0.2274944 0.9999998
## sábado-viernes    -1.100478e-01 -0.3471116 0.1270159 0.8175309
## domingo-viernes   -1.052632e-01 -0.3423269 0.1318006 0.8469878
## domingo-sábado     4.784689e-03 -0.2322791 0.2418485 1.0000000
datos$TipoDia <- factor(
  ifelse(
    datos$DiaSemana %in% c("sábado", "domingo"),
    "Fin de semana",
    "Entre semana"
  ),
  levels = c("Entre semana", "Fin de semana")
)

tabla_mes_dia <- table(
  datos$Mes,
  datos$TipoDia
)

tabla_mes_dia
##             
##              Entre semana Fin de semana
##   enero                73            24
##   febrero              55            23
##   marzo                60            17
##   abril                74            22
##   mayo                 52            25
##   junio                67            20
##   julio                57            23
##   agosto               56            24
##   septiembre           61            20
##   octubre              56            22
##   noviembre            56            26
##   diciembre            64            23
prueba_chi <- chisq.test(tabla_mes_dia)

prueba_chi
## 
##  Pearson's Chi-squared test
## 
## data:  tabla_mes_dia
## X-squared = 5.843, df = 11, p-value = 0.8836
# Verificar las frecuencias esperadas
min(prueba_chi$expected)
## [1] 20.713

Interpretación: la prueba examina si el mes y el tipo de día aparecen asociados en los registros. Si p < 0.05, existe evidencia estadística de una asociación, pero eso no significa que el mes cause el patrón observado.

corte <- fecha_minima +
  floor(as.numeric(fecha_maxima - fecha_minima) / 2)

datos_periodo <- datos %>%
  filter(!is.na(Date), !is.na(DiaSemana)) %>%
  mutate(
    Periodo = factor(
      ifelse(
        Date <= corte,
        "Primera mitad",
        "Segunda mitad"
      ),
      levels = c("Primera mitad", "Segunda mitad")
    ),
    FinSemana = DiaSemana %in% c("sábado", "domingo")
  )

resumen_proporciones <- datos_periodo %>%
  group_by(Periodo) %>%
  summarise(
    citas_fin_semana = sum(FinSemana),
    total_citas = n(),
    proporcion = citas_fin_semana / total_citas,
    .groups = "drop"
  )

resumen_proporciones
prueba_prop <- prop.test(
  x = resumen_proporciones$citas_fin_semana,
  n = resumen_proporciones$total_citas
)

prueba_prop
## 
##  2-sample test for equality of proportions with continuity correction
## 
## data:  resumen_proporciones$citas_fin_semana out of resumen_proporciones$total_citas
## X-squared = 0.069487, df = 1, p-value = 0.7921
## alternative hypothesis: two.sided
## 95 percent confidence interval:
##  -0.04756873  0.06635754
## sample estimates:
##    prop 1    prop 2 
## 0.2736220 0.2642276

Interpretación: la prueba compara la proporción de registros correspondientes a sábados y domingos entre los dos periodos. El valor p y el intervalo de confianza ayudan a evaluar si existe evidencia de una diferencia en esas proporciones.

install.packages(c(“ggpubr”, “plotly”, “dplyr”, “tidyr”))

library(ggpubr)
library(plotly)
library(dplyr)
library(tidyr)
# Asegurar que Date tenga formato de fecha
if (!inherits(datos$Date, "Date")) {
  if (inherits(datos$Date, "POSIXt")) {
    datos$Date <- as.Date(datos$Date)
  } else if (is.numeric(datos$Date)) {
    datos$Date <- as.Date(
      datos$Date,
      origin = "1899-12-30"
    )
  } else {
    datos$Date <- as.Date(as.character(datos$Date))
  }
}

# Crear un calendario diario dentro del periodo observado
fecha_inicial <- min(datos$Date, na.rm = TRUE)
fecha_final <- max(datos$Date, na.rm = TRUE)

calendario <- data.frame(
  Date = seq(fecha_inicial, fecha_final, by = "day")
)

# Contar las citas registradas por fecha
conteo_diario <- datos %>%
  filter(!is.na(Date)) %>%
  count(Date, name = "citas")

# Incorporar fechas sin registros en la base
resumen_diario <- calendario %>%
  left_join(conteo_diario, by = "Date") %>%
  mutate(citas = tidyr::replace_na(citas, 0L))

# Identificar el día de la semana
dias <- c(
  "domingo", "lunes", "martes", "miércoles",
  "jueves", "viernes", "sábado"
)

orden_dias <- c(
  "lunes", "martes", "miércoles", "jueves",
  "viernes", "sábado", "domingo"
)

resumen_diario$DiaSemana <- factor(
  dias[as.POSIXlt(resumen_diario$Date)$wday + 1],
  levels = orden_dias
)

head(resumen_diario)
grafico_dias <- ggpubr::ggboxplot(
  resumen_diario,
  x = "DiaSemana",
  y = "citas",
  color = "DiaSemana",
  palette = "jco",
  add = "jitter"
) +
  labs(
    title = "Variabilidad de las citas por día de la semana",
    x = "Día de la semana",
    y = "Citas registradas por día"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

ggplotly(grafico_dias)

El gráfico permite comparar la distribución y la variabilidad del número de citas entre los días de la semana. La línea central de cada caja representa la mediana, mientras que la caja resume la parte central de los datos y los puntos muestran observaciones individuales.

# Construir el primer día de cada mes
conteo_mensual <- datos %>%
  filter(!is.na(Date)) %>%
  mutate(
    MesInicio = as.Date(format(Date, "%Y-%m-01"))
  ) %>%
  count(MesInicio, name = "total_citas")

# Completar los meses del periodo observado
calendario_meses <- data.frame(
  MesInicio = seq(
    as.Date(format(fecha_inicial, "%Y-%m-01")),
    as.Date(format(fecha_final, "%Y-%m-01")),
    by = "month"
  )
)

citas_mes <- calendario_meses %>%
  left_join(conteo_mensual, by = "MesInicio") %>%
  mutate(
    total_citas = tidyr::replace_na(total_citas, 0L),
    Periodo = format(MesInicio, "%Y-%m")
  )

# Mantener el orden cronológico
citas_mes$Periodo <- factor(
  citas_mes$Periodo,
  levels = citas_mes$Periodo
)

# Crear gráfico con ggpubr
grafico_meses <- ggpubr::ggline(
  citas_mes,
  x = "Periodo",
  y = "total_citas",
  color = "steelblue",
  size = 0.8
) +
  geom_point(size = 2) +
  labs(
    title = "Evolución mensual de las citas registradas",
    x = "Mes y año",
    y = "Número de citas"
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(
      angle = 60,
      hjust = 1
    )
  )
## Warning in .resolve_linewidth_args(size = size, linewidth = linewidth,
## default_linewidth = 0.5): The 'size' parameter for lines is deprecated in
## ggplot2 3.4.0+. Please use 'linewidth' instead to avoid this warning in future
## versions.
ggplotly(grafico_meses)

La gráfica permite observar cómo cambia el número de citas registradas de un mes a otro. Los incrementos y descensos pueden señalar periodos que requieren una revisión de la planificación operativa, de la disponibilidad de profesionales o de las agendas.

top_medicos <- datos %>%
  filter(!is.na(DoctorID)) %>%
  count(DoctorID, name = "total_citas", sort = TRUE) %>%
  slice_head(n = 10)

grafico_medicos <- ggpubr::ggbarplot(
  top_medicos,
  x = "DoctorID",
  y = "total_citas",
  fill = "DoctorID",
  palette = "jco",
  sort.val = "desc",
  rotate = TRUE
) +
  labs(
    title = "Diez identificadores con más citas registradas",
    x = "Identificador del médico",
    y = "Número de citas"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

ggplotly(grafico_medicos)

La gráfica facilita identificar qué DoctorID presentan más registros de citas. Desde la gestión empresarial, esta información puede utilizarse como punto de partida para revisar la distribución de la programación, las agendas y la asignación de recursos.

resumen_diario$TipoDia <- factor(
  ifelse(
    resumen_diario$DiaSemana %in% c("sábado", "domingo"),
    "Fin de semana",
    "Entre semana"
  ),
  levels = c("Entre semana", "Fin de semana")
)

grafico_tipo_dia <- ggpubr::ggboxplot(
  resumen_diario,
  x = "TipoDia",
  y = "citas",
  color = "TipoDia",
  palette = "jco",
  add = "jitter"
) +
  labs(
    title = "Comparación de citas entre semana y fin de semana",
    x = "Tipo de día",
    y = "Citas registradas por día"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

ggplotly(grafico_tipo_dia)

Este gráfico permite explorar si la distribución diaria de citas es diferente entre los días laborales y los fines de semana. La posición de las cajas y la dispersión de los puntos ayudan a comparar el comportamiento de los registros en ambos grupos.

Interpretación de datos Desde la perspectiva de la administración de recursos sanitarios, estos gráficos constituyen herramientas de apoyo para identificar patrones de programación y formular preguntas sobre la distribución de agendas, la planificación del personal y la organización temporal de los servicios. No obstante, las visualizaciones describen los registros disponibles y no permiten, por sí solas, concluir que exista sobrecarga, baja productividad o deficiencias en la atención.

Interpretación del informe:

El presente informe tiene como propósito analizar la base de datos Appointment, correspondiente al registro de citas de un servicio de salud, mediante herramientas de probabilidad, estadística descriptiva e inferencial desarrolladas en RStudio a través de R Markdown. El análisis se orienta a comprender la distribución de la actividad registrada y a identificar oportunidades de mejora en la planificación administrativa de los servicios sanitarios.

La base de datos contiene cinco variables principales: AppointmentID, que identifica cada registro de cita; Date, que representa la fecha programada; Time, que corresponde al campo de hora y requiere validación de su formato; PatientID, que identifica al paciente, y DoctorID, que identifica al médico asociado con el registro. Estas variables permiten explorar aspectos relacionados con la distribución temporal de las citas y su asignación entre médicos, aunque no proporcionan información directa sobre la duración de la consulta, los tiempos de espera, las inasistencias, los costos ni los resultados clínicos.