El EDA (revisión inicial de los datos antes de cualquier análisis formal) permite conocer qué contiene la base, en qué estado está y qué patrones visuales aparecen.
library(tidyverse) # manipulación de datos y gráficos (incluye ggplot2 y lubridate)
library(plotly) # vuelve interactivos los gráficos
library(DT) # tablas interactivas
Los identificadores se leen como texto para no perder los ceros a la izquierda.
citas <- read_csv("Appointments.csv",
col_types = cols(appointment_id = col_character(),
slot_id = col_character(),
patient_id = col_character()))
dim(citas) # filas y columnas
## [1] 111488 16
glimpse(citas) # nombre, tipo y primeros valores de cada variable
## Rows: 111,488
## Columns: 16
## $ appointment_id <chr> "0000138", "0000146", "0000021", "0000233", "0000…
## $ slot_id <chr> "0000001", "0000023", "0000024", "0000025", "0000…
## $ scheduling_date <date> 2014-12-28, 2014-12-29, 2014-12-17, 2014-12-31, …
## $ appointment_date <date> 2015-01-01, 2015-01-01, 2015-01-01, 2015-01-01, …
## $ appointment_time <time> 08:00:00, 13:30:00, 13:45:00, 14:00:00, 14:15:00…
## $ scheduling_interval <dbl> 4, 3, 15, 1, 6, 2, 2, 2, 4, 4, 4, 1, 1, 7, 3, 2, …
## $ status <chr> "did not attend", "did not attend", "attended", "…
## $ check_in_time <time> NA, NA, 13:36:45, 13:59:32, NA…
## $ appointment_duration <dbl> NA, NA, 5.2, 28.9, NA, 7.7, 4.2, 27.1, NA, 1.2, 7…
## $ start_time <time> NA, NA, 13:37:57, 14:00:40, NA…
## $ end_time <time> NA, NA, 13:43:09, 14:29:34, NA…
## $ waiting_time <dbl> NA, NA, 1.2, 1.1, NA, 21.7, 16.2, 1.0, NA, 8.5, 2…
## $ patient_id <chr> "08285", "05972", "06472", "05376", "08028", "043…
## $ sex <chr> "Male", "Male", "Male", "Female", "Male", "Female…
## $ age <dbl> 37, 84, 77, 37, 72, 51, 28, 33, 29, 90, 66, 64, 3…
## $ age_group <chr> "35-39", "80-84", "75-79", "35-39", "70-74", "50-…
datatable(head(citas, 100), options = list(scrollX = TRUE, pageLength = 5))
La base contiene 111.488 citas y 16 variables. Las citas van del 1 de enero de 2015 al 24 de diciembre de 2024 (los agendamientos inician en diciembre de 2014) y corresponden a 36.697 pacientes distintos; es decir, un mismo paciente tiene varias citas.
diccionario <- tibble(
Variable = names(citas),
Descripcion = c(
"Identificador único de la cita",
"Identificador del espacio (turno) en la agenda",
"Fecha en que se agendó la cita",
"Fecha programada de la cita",
"Hora programada de la cita",
"Días entre el agendamiento y la cita",
"Estado final de la cita",
"Hora de llegada/registro del paciente",
"Duración de la atención (minutos)",
"Hora real de inicio de la atención",
"Hora real de finalización de la atención",
"Tiempo de espera antes de ser atendido (minutos)",
"Identificador del paciente",
"Sexo del paciente",
"Edad del paciente (años)",
"Grupo de edad (rangos de 5 años)"
),
Tipo = sapply(citas, function(x) class(x)[1])
)
datatable(diccionario, options = list(pageLength = 16))
Las variables se agrupan en: identificadores
(appointment_id, slot_id,
patient_id), fechas y horas de agenda y de atención real,
tiempos en minutos (appointment_duration,
waiting_time) y características del paciente
(sex, age, age_group).
Se traducen las categorías al español y se convierten a
factor (variable con categorías fijas).
citas <- citas %>%
mutate(
status = recode(status,
"attended" = "Asistió",
"cancelled" = "Cancelada",
"did not attend" = "No asistió",
"scheduled" = "Programada",
"unknown" = "Desconocido"),
sex = recode(sex, "Female" = "Femenino", "Male" = "Masculino"),
status = as.factor(status),
sex = as.factor(sex),
age_group = factor(age_group, levels = c("15-19","20-24","25-29","30-34","35-39",
"40-44","45-49","50-54","55-59","60-64",
"65-69","70-74","75-79","80-84","85-89","90+"))
)
glimpse(citas)
## Rows: 111,488
## Columns: 16
## $ appointment_id <chr> "0000138", "0000146", "0000021", "0000233", "0000…
## $ slot_id <chr> "0000001", "0000023", "0000024", "0000025", "0000…
## $ scheduling_date <date> 2014-12-28, 2014-12-29, 2014-12-17, 2014-12-31, …
## $ appointment_date <date> 2015-01-01, 2015-01-01, 2015-01-01, 2015-01-01, …
## $ appointment_time <time> 08:00:00, 13:30:00, 13:45:00, 14:00:00, 14:15:00…
## $ scheduling_interval <dbl> 4, 3, 15, 1, 6, 2, 2, 2, 4, 4, 4, 1, 1, 7, 3, 2, …
## $ status <fct> No asistió, No asistió, Asistió, Asistió, Cancela…
## $ check_in_time <time> NA, NA, 13:36:45, 13:59:32, NA…
## $ appointment_duration <dbl> NA, NA, 5.2, 28.9, NA, 7.7, 4.2, 27.1, NA, 1.2, 7…
## $ start_time <time> NA, NA, 13:37:57, 14:00:40, NA…
## $ end_time <time> NA, NA, 13:43:09, 14:29:34, NA…
## $ waiting_time <dbl> NA, NA, 1.2, 1.1, NA, 21.7, 16.2, 1.0, NA, 8.5, 2…
## $ patient_id <chr> "08285", "05972", "06472", "05376", "08028", "043…
## $ sex <fct> Masculino, Masculino, Masculino, Femenino, Mascul…
## $ age <dbl> 37, 84, 77, 37, 72, 51, 28, 33, 29, 90, 66, 64, 3…
## $ age_group <fct> 35-39, 80-84, 75-79, 35-39, 70-74, 50-54, 25-29, …
faltantes <- tibble(
Variable = names(citas),
Faltantes = colSums(is.na(citas)),
Porcentaje = round(colSums(is.na(citas)) / nrow(citas) * 100, 2)
)
datatable(faltantes)
# ¿En qué estados están las celdas vacías de la atención?
citas %>%
filter(is.na(check_in_time)) %>%
count(status)
## # A tibble: 4 × 2
## status n
## <fct> <int>
## 1 Cancelada 18254
## 2 Desconocido 446
## 3 No asistió 6615
## 4 Programada 141
sum(duplicated(citas$appointment_id)) # citas repetidas
## [1] 0
sum(duplicated(citas)) # filas completas repetidas
## [1] 0
Las cinco variables de atención (check_in_time,
appointment_duration, start_time,
end_time, waiting_time) tienen los mismos
25.456 faltantes (NA, celdas vacías), y todos pertenecen a citas sin
atención: canceladas, no asistidas, desconocidas y programadas. Las
citas atendidas no tienen faltantes. Por eso no son errores de captura y
no deben eliminarse ni rellenarse. No hay citas ni filas duplicadas.
g1 <- citas %>%
count(status) %>%
ggplot(aes(x = reorder(status, -n), y = n, fill = status)) +
geom_col() +
labs(title = "Citas según estado", x = "Estado", y = "Número de citas") +
theme_minimal() +
theme(legend.position = "none")
ggplotly(g1)
La mayoría de citas fueron atendidas (86.032). Las canceladas (18.254) y las inasistencias (6.615) suman 24.869 espacios de agenda no usados como se planeó, lo que representa capacidad instalada perdida (horas de profesional disponibles que no se convierten en atención). Las 141 citas “Programadas” son de diciembre de 2024: aún no habían ocurrido al corte de la base. Los 446 casos “Desconocido” son un problema de registro a reportar.
g2 <- citas %>%
count(age_group, sex) %>%
ggplot(aes(x = age_group, y = n, fill = sex)) +
geom_col(position = "dodge") +
labs(title = "Citas por grupo de edad y sexo",
x = "Grupo de edad", y = "Número de citas", fill = "Sexo") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
ggplotly(g2)
La demanda se concentra en mayores de 55 años; el grupo con más citas es 75-79 (9.844). Las mujeres tienen más citas en todos los grupos (66.086 frente a 45.402 de hombres), con la brecha más amplia entre 25 y 39 años (en 30-34: 5.807 mujeres frente a 1.720 hombres), posiblemente asociada a atención en edad reproductiva. Desde los 60 años la demanda de ambos sexos casi se iguala.
Se excluye diciembre de 2024 porque solo tiene 166 citas (corte a mitad de mes) y mostraría una caída falsa.
g3 <- citas %>%
filter(appointment_date < as.Date("2024-12-01")) %>%
mutate(mes = floor_date(appointment_date, "month")) %>%
count(mes, status) %>%
ggplot(aes(x = mes, y = n, color = status)) +
geom_line() +
labs(title = "Citas por mes según estado (ene-2015 a nov-2024)",
x = "Mes", y = "Número de citas", color = "Estado") +
theme_minimal()
ggplotly(g3)
El volumen es estable durante los diez años (alrededor de 8.600 a 8.700 citas atendidas por año), sin tendencia de crecimiento ni caída. 2020 no muestra la caída esperable por la pandemia, lo que sugiere que la base podría ser simulada (generada artificialmente); conviene confirmarlo con la fuente. Febrero y diciembre son los meses con menos citas. Cancelaciones e inasistencias se mueven en paralelo a las atendidas, sin picos atípicos.
En este tema se usa la base Sleep Health and Lifestyle. Con ella se responden preguntas de salud y seguridad en el trabajo sobre el sueño de los trabajadores, entre ellos personal de enfermería y medicina. Las herramientas son la probabilidad (qué tan posible es que algo ocurra), la simulación (datos artificiales que imitan la realidad) y la estadística descriptiva e inferencial (sacar conclusiones confiables a partir de los datos).
El archivo Sleep_health_and_lifestyle_dataset.xlsx debe
estar en la misma carpeta de este .Rmd.
library(readxl)
sueno <- read_excel("Sleep_health_and_lifestyle_dataset.xlsx") %>%
rename(id = `Person ID`, sexo = Gender, edad = Age, ocupacion = Occupation,
horas_sueno = `Sleep Duration`, calidad_sueno = `Quality of Sleep`,
actividad_fisica = `Physical Activity Level`, estres = `Stress Level`,
imc = `BMI Category`, presion = `Blood Pressure`,
frec_cardiaca = `Heart Rate`, pasos = `Daily Steps`,
trastorno = `Sleep Disorder`) %>%
mutate(
horas_sueno = as.numeric(horas_sueno), # venía como texto
pas = as.numeric(sub("/.*", "", presion)), # presión sistólica (número antes de "/")
pad = as.numeric(sub(".*/", "", presion)), # presión diastólica (número después de "/")
sexo = recode(sexo, "Female" = "Mujer", "Male" = "Hombre"),
imc = recode(imc, "Normal Weight" = "Normal", "Overweight" = "Sobrepeso", "Obese" = "Obesidad"),
imc = factor(imc, levels = c("Normal", "Sobrepeso", "Obesidad")),
trastorno = factor(ifelse(trastorno == 1, "Con trastorno", "Sin trastorno"),
levels = c("Sin trastorno", "Con trastorno")),
ocupacion = recode(ocupacion,
"Nurse" = "Enfermería", "Doctor" = "Medicina", "Engineer" = "Ingeniería",
"Lawyer" = "Derecho", "Teacher" = "Docencia", "Accountant" = "Contaduría",
"Salesperson" = "Ventas", "Sales Representative" = "Representante de ventas",
"Scientist" = "Ciencias", "Software Engineer" = "Ingeniería de software",
"Manager" = "Gerencia"),
estres_nivel = factor(ifelse(estres >= 7, "Alto (7-8)", "Bajo o medio (3-6)")),
sueno_corto = factor(ifelse(horas_sueno < 7, "Menos de 7 h", "7 h o más"))
)
dim(sueno)
## [1] 374 17
glimpse(sueno)
## Rows: 374
## Columns: 17
## $ id <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16…
## $ sexo <chr> "Hombre", "Hombre", "Hombre", "Hombre", "Hombre", "Ho…
## $ edad <dbl> 27, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 2…
## $ ocupacion <chr> "Ingeniería de software", "Medicina", "Medicina", "Re…
## $ horas_sueno <dbl> 6.1, 6.2, 6.2, 5.9, 5.9, 5.9, 6.3, 7.8, 7.8, 7.8, 6.1…
## $ calidad_sueno <dbl> 6, 6, 6, 4, 4, 4, 6, 7, 7, 7, 6, 7, 6, 6, 6, 6, 5, 6,…
## $ actividad_fisica <dbl> 42, 60, 60, 30, 30, 30, 40, 75, 75, 75, 30, 75, 30, 3…
## $ estres <dbl> 6, 8, 8, 8, 8, 8, 7, 6, 6, 6, 8, 6, 8, 8, 8, 8, 7, 8,…
## $ imc <fct> Sobrepeso, Normal, Normal, Obesidad, Obesidad, Obesid…
## $ presion <chr> "126/83", "125/80", "125/80", "140/90", "140/90", "14…
## $ frec_cardiaca <dbl> 77, 75, 75, 85, 85, 85, 82, 70, 70, 70, 70, 70, 70, 7…
## $ pasos <dbl> 4200, 10000, 10000, 3000, 3000, 3000, 3500, 8000, 800…
## $ trastorno <fct> Sin trastorno, Sin trastorno, Sin trastorno, Con tras…
## $ pas <dbl> 126, 125, 125, 140, 140, 140, 140, 120, 120, 120, 120…
## $ pad <dbl> 83, 80, 80, 90, 90, 90, 90, 80, 80, 80, 80, 80, 80, 8…
## $ estres_nivel <fct> Bajo o medio (3-6), Alto (7-8), Alto (7-8), Alto (7-8…
## $ sueno_corto <fct> Menos de 7 h, Menos de 7 h, Menos de 7 h, Menos de 7 …
Interpretación y advertencias sobre la base: el archivo recibido tiene diferencias con la descripción del enunciado:
pas
(sistólica) y pad (diastólica).No hay datos faltantes. Sin embargo, 248 filas repiten exactamente el mismo perfil de otra persona, lo que sugiere que la base fue generada artificialmente para fines académicos. Las conclusiones sirven para aprender el método, no para describir una población real.
dicc_sueno <- tibble(
Variable = c("sexo", "edad", "ocupacion", "horas_sueno", "calidad_sueno",
"actividad_fisica", "estres", "imc", "pas / pad", "frec_cardiaca",
"pasos", "trastorno"),
Descripcion = c("Sexo del trabajador", "Edad (años)", "Ocupación",
"Horas de sueño por día", "Calidad del sueño (escala 1 a 10)",
"Minutos diarios de actividad física", "Nivel de estrés (escala 1 a 10)",
"Categoría de índice de masa corporal", "Presión arterial sistólica / diastólica (mmHg)",
"Frecuencia cardíaca en reposo (latidos por minuto)", "Pasos diarios",
"Presencia de trastorno del sueño (insomnio o apnea)")
)
datatable(dicc_sueno, options = list(pageLength = 12))
El espacio muestral (todos los resultados posibles) de un trabajador tamizado es: “Sin trastorno” o “Con trastorno”. El evento (resultado que interesa) es que haya más trabajadores positivos que cupos disponibles con el especialista.
Caso práctico: tamizaje de trastornos del sueño. El área de Seguridad y Salud en el Trabajo (SST) escoge al azar 20 trabajadores para un tamizaje (prueba rápida para detectar posibles casos). Solo hay 10 cupos de remisión a polisomnografía (estudio del sueño en laboratorio).
set.seed(2026) # fija la semilla (punto de partida del azar) para poder repetir el resultado
espacio_muestral <- c("Sin trastorno", "Con trastorno")
# 1. Selección real: 20 trabajadores al azar de la base, sin repetir
seleccionados <- sample(1:nrow(sueno), size = 20)
tamizaje <- sueno[seleccionados, ]
table(tamizaje$trastorno)
##
## Sin trastorno Con trastorno
## 12 8
# 2. Simulación: repetir el tamizaje 1.000 veces con la probabilidad observada
p_trast <- mean(sueno$trastorno == "Con trastorno")
p_trast
## [1] 0.4144385
supera_cupos <- replicate(1000, {
grupo <- sample(espacio_muestral, size = 20, replace = TRUE,
prob = c(1 - p_trast, p_trast))
sum(grupo == "Con trastorno") > 10
})
mean(supera_cupos) # probabilidad del evento "más de 10 positivos"
## [1] 0.151
Interpretación: en la selección real, 8 de los 20 trabajadores tienen trastorno, así que los 10 cupos alcanzan. Cuando se repite el tamizaje 1.000 veces (con probabilidad de trastorno de 41,4 %), en el 15,1 % de las veces hay más de 10 positivos. Para SST esto significa que, en más o menos 1 de cada 7 jornadas de tamizaje, faltarían cupos de polisomnografía. Con esa cifra se puede justificar ante la gerencia o la ARL la necesidad de cupos de reserva.
La probabilidad empírica (la que sale de contar lo observado) es casos del evento / total de casos.
# Probabilidad de tener trastorno del sueño
round(prop.table(table(sueno$trastorno)), 3)
##
## Sin trastorno Con trastorno
## 0.586 0.414
# Probabilidad de trastorno según la categoría de IMC
round(prop.table(table(sueno$imc, sueno$trastorno), margin = 1), 3)
##
## Sin trastorno Con trastorno
## Normal 0.926 0.074
## Sobrepeso 0.128 0.872
## Obesidad 0.000 1.000
# Probabilidad de trastorno y de dormir menos de 7 h según la ocupación
ocupaciones_principales <- c("Enfermería", "Medicina", "Ingeniería", "Derecho",
"Docencia", "Contaduría", "Ventas")
sueno_ocup <- sueno %>% filter(ocupacion %in% ocupaciones_principales)
round(prop.table(table(sueno_ocup$ocupacion, sueno_ocup$trastorno), margin = 1), 3)
##
## Sin trastorno Con trastorno
## Contaduría 0.811 0.189
## Derecho 0.894 0.106
## Docencia 0.225 0.775
## Enfermería 0.123 0.877
## Ingeniería 0.905 0.095
## Medicina 0.901 0.099
## Ventas 0.062 0.938
round(prop.table(table(sueno_ocup$ocupacion, sueno_ocup$sueno_corto), margin = 1), 3)
##
## 7 h o más Menos de 7 h
## Contaduría 0.838 0.162
## Derecho 0.979 0.021
## Docencia 0.150 0.850
## Enfermería 0.493 0.507
## Ingeniería 0.952 0.048
## Medicina 0.535 0.465
## Ventas 0.000 1.000
Interpretación:
Una distribución de probabilidad describe qué valores puede tomar una variable y qué tan frecuente es cada uno. Las discretas cuentan (número de trabajadores con trastorno) y las continuas miden (horas de sueño).
runif(): un servicio de 30 enfermeras.
runif() genera números al azar entre 0 y 1. Si el número es
menor que la probabilidad de trastorno en enfermería, esa enfermera
“tiene trastorno”.
set.seed(2026)
p_enf <- mean(sueno$trastorno[sueno$ocupacion == "Enfermería"] == "Con trastorno")
p_med <- mean(sueno$trastorno[sueno$ocupacion == "Medicina"] == "Con trastorno")
p_enf
## [1] 0.8767123
p_med
## [1] 0.09859155
servicio <- ifelse(runif(30) < p_enf, "Con trastorno", "Sin trastorno")
table(servicio)
## servicio
## Con trastorno
## 30
rbinom(): 1.000 servicios simulados. La distribución binomial cuenta cuántos “casos” aparecen en un número fijo de personas con la misma probabilidad. Se comparan servicios de 30 enfermeras con servicios de 30 médicos.
set.seed(2026)
sim_enf <- rbinom(1000, size = 30, prob = p_enf)
sim_med <- rbinom(1000, size = 30, prob = p_med)
table(sim_enf)
## sim_enf
## 20 21 22 23 24 25 26 27 28 29 30
## 1 5 17 37 101 152 210 205 160 92 20
mean(sim_enf >= 25) # P(25 o más enfermeras con trastorno en un servicio de 30)
## [1] 0.839
table(sim_med)
## sim_med
## 0 1 2 3 4 5 6 7 8 10
## 27 150 250 244 165 95 39 23 6 1
mean(sim_med >= 5) # P(5 o más médicos con trastorno en un grupo de 30)
## [1] 0.164
dpois(): positivos por día en el tamizaje. La distribución de Poisson describe cuántas veces ocurre algo en un periodo cuando se conoce el promedio (lambda). Si se tamizan 10 trabajadores al día, el promedio de positivos es 10 × p. La unidad de SST tiene 6 cupos diarios de valoración con medicina laboral.
lambda_pos <- 10 * p_trast
lambda_pos
## [1] 4.144385
positivos <- tibble(
positivos_dia = 0:10,
Poisson = dpois(0:10, lambda = lambda_pos),
Binomial = dbinom(0:10, size = 10, prob = p_trast)
)
round(positivos, 3)
## # A tibble: 11 × 3
## positivos_dia Poisson Binomial
## <dbl> <dbl> <dbl>
## 1 0 0.016 0.005
## 2 1 0.066 0.034
## 3 2 0.136 0.107
## 4 3 0.188 0.202
## 5 4 0.195 0.25
## 6 5 0.162 0.212
## 7 6 0.112 0.125
## 8 7 0.066 0.051
## 9 8 0.034 0.013
## 10 9 0.016 0.002
## 11 10 0.007 0
1 - sum(dpois(0:6, lambda = lambda_pos)) # P(más de 6 positivos) según Poisson
## [1] 0.1262543
1 - sum(dbinom(0:6, size = 10, prob = p_trast)) # mismo cálculo con binomial
## [1] 0.0662868
Interpretación:
rnorm(): horas de sueño. La distribución normal tiene forma de campana, simétrica alrededor del promedio. La referencia de salud pública para adultos es dormir 7 horas o más.
set.seed(2026)
m_sueno <- mean(sueno$horas_sueno)
s_sueno <- sd(sueno$horas_sueno)
sim_sueno <- rnorm(1000, mean = m_sueno, sd = s_sueno)
pnorm(7, mean = m_sueno, sd = s_sueno) # P(dormir menos de 7 h) según la normal
## [1] 0.4340752
mean(sueno$horas_sueno < 7) # realidad
## [1] 0.4144385
pnorm(6, mean = m_sueno, sd = s_sueno) # P(dormir menos de 6 h) según la normal
## [1] 0.07739249
mean(sueno$horas_sueno < 6) # realidad
## [1] 0.01604278
runif(): modelo uniforme de las horas de sueño. La distribución uniforme continua da la misma probabilidad a cualquier valor entre un mínimo y un máximo. Sirve para comparar: ¿qué pasa si se supone que cualquier duración entre 5,8 y 8,5 h es igual de probable?
set.seed(2026)
sim_unif <- runif(1000, min = min(sueno$horas_sueno), max = max(sueno$horas_sueno))
punif(7, min = min(sueno$horas_sueno), max = max(sueno$horas_sueno)) # P(< 7 h) según uniforme
## [1] 0.4444444
mean(sim_unif < 7) # en la simulación
## [1] 0.447
rexp(): espera para la polisomnografía (escenario supuesto). La distribución exponencial describe tiempos de espera: muchos cortos y pocos muy largos. La base no trae tiempos de espera, así que se supone un promedio de 30 días entre la remisión y el estudio. Cambia ese valor por el de la EPS o el prestador con que trabajes.
set.seed(2026)
espera_media <- 30 # días (supuesto)
sim_espera <- rexp(1000, rate = 1 / espera_media)
pexp(15, rate = 1 / espera_media) # P(esperar 15 días o menos)
## [1] 0.3934693
pexp(60, rate = 1 / espera_media, lower.tail = FALSE) # P(esperar más de 60 días)
## [1] 0.1353353
mean(sim_espera > 60) # en la simulación
## [1] 0.152
Interpretación:
g_binom <- tibble(Enfermería = sim_enf, Medicina = sim_med) %>%
pivot_longer(everything(), names_to = "Grupo", values_to = "casos") %>%
ggplot(aes(x = casos, fill = Grupo)) +
geom_bar(position = "identity", alpha = 0.7) +
labs(title = "Trabajadores con trastorno en grupos de 30 (1.000 simulaciones)",
x = "Número de trabajadores con trastorno", y = "Simulaciones") +
theme_minimal()
ggplotly(g_binom)
Lectura del gráfico: las dos distribuciones no se cruzan. La de enfermería se concentra entre 24 y 29 casos por cada 30 enfermeras, y la de medicina entre 1 y 4. Al pasar el mouse se ve cuántas simulaciones tuvo cada valor.
g_pois <- positivos %>%
pivot_longer(c(Poisson, Binomial), names_to = "Modelo", values_to = "Probabilidad") %>%
ggplot(aes(x = positivos_dia, y = Probabilidad, fill = Modelo)) +
geom_col(position = "dodge") +
geom_vline(xintercept = 6.5, linetype = "dashed") +
labs(title = "Positivos por día al tamizar 10 trabajadores (línea: 6 cupos)",
x = "Positivos en el día", y = "Probabilidad") +
theme_minimal()
ggplotly(g_pois)
Lectura del gráfico: la binomial se concentra entre 3 y 5 positivos y cae rápido. La Poisson es más ancha y asigna probabilidad a valores que en la práctica son raros. La línea punteada marca el límite de 6 cupos: lo que queda a la derecha de la línea son los días en que faltarían cupos.
g_normal <- ggplot(sueno, aes(x = horas_sueno)) +
geom_histogram(aes(y = after_stat(density)), binwidth = 0.2, fill = "grey70") +
stat_function(fun = dnorm, args = list(mean = m_sueno, sd = s_sueno),
color = "red", linewidth = 1) +
stat_function(fun = dunif, args = list(min = min(sueno$horas_sueno), max = max(sueno$horas_sueno)),
color = "blue", linewidth = 1, linetype = "dashed") +
geom_vline(xintercept = 7, linetype = "dotted") +
labs(title = "Horas de sueño reales vs. normal (rojo) y uniforme (azul)",
x = "Horas de sueño", y = "Densidad") +
theme_minimal()
ggplotly(g_normal)
Lectura del gráfico: las horas de sueño reales no forman una campana perfecta. Tienen varios picos (los valores más frecuentes son 7,2 h, 6,0 h y 7,8 h), porque hay grupos de trabajadores con hábitos parecidos. La curva normal (roja) sigue la forma general y la uniforme (azul) es plana. La línea punteada en 7 h separa a quienes no cumplen la recomendación.
g_exp <- tibble(dias = sim_espera) %>%
ggplot(aes(x = dias)) +
geom_histogram(aes(y = after_stat(density)), binwidth = 5, fill = "grey70") +
stat_function(fun = dexp, args = list(rate = 1 / espera_media),
color = "red", linewidth = 1) +
labs(title = "Espera simulada para polisomnografía (promedio supuesto: 30 días)",
x = "Días de espera", y = "Densidad") +
theme_minimal()
ggplotly(g_exp)
Lectura del gráfico: la mayoría de las esperas simuladas son cortas, pero la cola se extiende más allá de 100 días. Por eso un promedio de 30 días puede esconder casos de varios meses de espera.
La media es el promedio. La mediana es el valor del medio (no se deja afectar por extremos). La desviación estándar mide qué tanto se alejan los datos del promedio, en las mismas unidades. La varianza es la desviación estándar al cuadrado. El rango muestra el mínimo y el máximo.
medidas <- function(x) {
c(Media = mean(x), Mediana = median(x), Desv_estandar = sd(x),
Varianza = var(x), Minimo = range(x)[1], Maximo = range(x)[2])
}
indicadores_sueno <- rbind(
"Horas de sueño" = medidas(sueno$horas_sueno),
"Calidad del sueño (1-10)" = medidas(sueno$calidad_sueno),
"Estrés (1-10)" = medidas(sueno$estres),
"Actividad física (min/día)" = medidas(sueno$actividad_fisica),
"Pasos diarios" = medidas(sueno$pasos),
"Frecuencia cardíaca (lpm)" = medidas(sueno$frec_cardiaca),
"Presión sistólica (mmHg)" = medidas(sueno$pas),
"Presión diastólica (mmHg)" = medidas(sueno$pad)
)
round(indicadores_sueno, 2)
## Media Mediana Desv_estandar Varianza Minimo
## Horas de sueño 7.13 7.2 0.80 0.63 5.8
## Calidad del sueño (1-10) 7.31 7.0 1.20 1.43 4.0
## Estrés (1-10) 5.39 5.0 1.77 3.15 3.0
## Actividad física (min/día) 59.17 60.0 20.83 433.92 30.0
## Pasos diarios 6816.84 7000.0 1617.92 2617651.14 3000.0
## Frecuencia cardíaca (lpm) 70.17 70.0 4.14 17.10 65.0
## Presión sistólica (mmHg) 128.55 130.0 7.75 60.03 115.0
## Presión diastólica (mmHg) 84.65 85.0 6.16 37.97 75.0
## Maximo
## Horas de sueño 8.5
## Calidad del sueño (1-10) 9.0
## Estrés (1-10) 8.0
## Actividad física (min/día) 90.0
## Pasos diarios 10000.0
## Frecuencia cardíaca (lpm) 86.0
## Presión sistólica (mmHg) 142.0
## Presión diastólica (mmHg) 95.0
# Indicadores por ocupación
sueno_ocup %>%
group_by(ocupacion) %>%
summarise(n = n(),
Horas_sueno = mean(horas_sueno),
Calidad = mean(calidad_sueno),
Estres = mean(estres),
Pct_trastorno = mean(trastorno == "Con trastorno") * 100) %>%
arrange(Horas_sueno) %>%
mutate(across(where(is.numeric), ~ round(.x, 1)))
## # A tibble: 7 × 6
## ocupacion n Horas_sueno Calidad Estres Pct_trastorno
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Ventas 32 6.4 6 7 93.8
## 2 Docencia 40 6.7 7 4.5 77.5
## 3 Medicina 71 7 6.6 6.7 9.9
## 4 Enfermería 73 7.1 7.4 5.5 87.7
## 5 Contaduría 37 7.1 7.9 4.6 18.9
## 6 Derecho 47 7.4 7.9 5.1 10.6
## 7 Ingeniería 63 8 8.4 3.9 9.5
Interpretación:
summary(sueno %>% select(edad, horas_sueno, calidad_sueno, estres, actividad_fisica,
pasos, frec_cardiaca, pas, pad, imc, trastorno))
## edad horas_sueno calidad_sueno estres
## Min. :27.00 Min. :5.800 Min. :4.000 Min. :3.000
## 1st Qu.:35.25 1st Qu.:6.400 1st Qu.:6.000 1st Qu.:4.000
## Median :43.00 Median :7.200 Median :7.000 Median :5.000
## Mean :42.18 Mean :7.132 Mean :7.313 Mean :5.385
## 3rd Qu.:50.00 3rd Qu.:7.800 3rd Qu.:8.000 3rd Qu.:7.000
## Max. :59.00 Max. :8.500 Max. :9.000 Max. :8.000
## actividad_fisica pasos frec_cardiaca pas
## Min. :30.00 Min. : 3000 Min. :65.00 Min. :115.0
## 1st Qu.:45.00 1st Qu.: 5600 1st Qu.:68.00 1st Qu.:125.0
## Median :60.00 Median : 7000 Median :70.00 Median :130.0
## Mean :59.17 Mean : 6817 Mean :70.17 Mean :128.6
## 3rd Qu.:75.00 3rd Qu.: 8000 3rd Qu.:72.00 3rd Qu.:135.0
## Max. :90.00 Max. :10000 Max. :86.00 Max. :142.0
## pad imc trastorno
## Min. :75.00 Normal :216 Sin trastorno:219
## 1st Qu.:80.00 Sobrepeso:148 Con trastorno:155
## Median :85.00 Obesidad : 10
## Mean :84.65
## 3rd Qu.:90.00
## Max. :95.00
Interpretación: summary() muestra en
una sola tabla el mínimo, los cuartiles (valores que dividen los datos
ordenados en cuatro partes iguales), la mediana, la media y el máximo de
cada variable. Para las variables categóricas muestra el conteo de cada
categoría. Algunos datos para destacar:
La estadística inferencial verifica si una diferencia observada es real o pudo deberse al azar. Se plantea una hipótesis nula (H0: “no hay diferencia”) y se calcula el valor p (probabilidad de ver una diferencia así si H0 fuera cierta). Si p < 0,05, se rechaza H0 y la diferencia se considera estadísticamente significativa.
Un intervalo de confianza del 95 % es el rango donde, con 95 % de confianza, está el verdadero promedio de la población.
# 1. ¿Los trabajadores duermen, en promedio, las 7 h recomendadas?
t.test(sueno$horas_sueno, mu = 7)
##
## One Sample t-test
##
## data: sueno$horas_sueno
## t = 3.2104, df = 373, p-value = 0.00144
## alternative hypothesis: true mean is not equal to 7
## 95 percent confidence interval:
## 7.051185 7.212986
## sample estimates:
## mean of x
## 7.132086
# 2. ¿Duermen lo mismo mujeres y hombres?
t.test(horas_sueno ~ sexo, data = sueno)
##
## Welch Two Sample t-test
##
## data: horas_sueno by sexo
## t = -2.3565, df = 349.38, p-value = 0.019
## alternative hypothesis: true difference in means between group Hombre and group Mujer is not equal to 0
## 95 percent confidence interval:
## -0.35448564 -0.03195795
## sample estimates:
## mean in group Hombre mean in group Mujer
## 7.036508 7.229730
# 3. ¿La calidad del sueño es menor en quienes tienen trastorno?
t.test(calidad_sueno ~ trastorno, data = sueno)
##
## Welch Two Sample t-test
##
## data: calidad_sueno by trastorno
## t = 5.9881, df = 265.18, p-value = 6.878e-09
## alternative hypothesis: true difference in means between group Sin trastorno and group Con trastorno is not equal to 0
## 95 percent confidence interval:
## 0.5064815 1.0027245
## sample estimates:
## mean in group Sin trastorno mean in group Con trastorno
## 7.625571 6.870968
Interpretación:
El ANOVA compara los promedios de tres o más grupos al mismo tiempo. Si sale significativo, la prueba de Tukey (comparación de los grupos de dos en dos) indica cuáles grupos son distintos.
# 1. ¿Las horas de sueño cambian según la ocupación?
anova_ocup <- aov(horas_sueno ~ ocupacion, data = sueno_ocup)
summary(anova_ocup)
## Df Sum Sq Mean Sq F value Pr(>F)
## ocupacion 6 76.55 12.759 30.66 <2e-16 ***
## Residuals 356 148.14 0.416
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
tukey <- TukeyHSD(anova_ocup)$ocupacion
round(tukey[grepl("Enfermería|Medicina", rownames(tukey)), ], 3)
## diff lwr upr p adj
## Enfermería-Contaduría -0.050 -0.437 0.336 1.000
## Medicina-Contaduría -0.143 -0.531 0.245 0.930
## Enfermería-Derecho -0.348 -0.705 0.010 0.063
## Medicina-Derecho -0.440 -0.800 -0.081 0.006
## Enfermería-Docencia 0.373 -0.003 0.749 0.054
## Medicina-Docencia 0.280 -0.098 0.659 0.299
## Ingeniería-Enfermería 0.924 0.595 1.253 0.000
## Medicina-Enfermería -0.093 -0.411 0.226 0.978
## Ventas-Enfermería -0.660 -1.065 -0.254 0.000
## Medicina-Ingeniería -1.017 -1.348 -0.686 0.000
## Ventas-Medicina -0.567 -0.975 -0.160 0.001
# 2. ¿La presión sistólica cambia según el IMC?
anova_imc <- aov(pas ~ imc, data = sueno)
summary(anova_imc)
## Df Sum Sq Mean Sq F value Pr(>F)
## imc 2 12674 6337 241.9 <2e-16 ***
## Residuals 371 9719 26
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
tapply(sueno$pas, sueno$imc, mean)
## Normal Sobrepeso Obesidad
## 123.6065 135.0541 139.2000
Interpretación:
La prueba chi-cuadrado revisa si dos variables categóricas están relacionadas (H0: son independientes).
# Obesidad tiene solo 10 personas: se agrupa con sobrepeso para que la prueba sea válida
sueno <- sueno %>%
mutate(peso = factor(ifelse(imc == "Normal", "Peso normal", "Exceso de peso"),
levels = c("Peso normal", "Exceso de peso")))
tabla_peso <- table(sueno$peso, sueno$trastorno)
tabla_peso
##
## Sin trastorno Con trastorno
## Peso normal 200 16
## Exceso de peso 19 139
chisq.test(tabla_peso)
##
## Pearson's Chi-squared test with Yates' continuity correction
##
## data: tabla_peso
## X-squared = 240.77, df = 1, p-value < 2.2e-16
Interpretación: X² = 240,8 y p < 0,001. El exceso de peso y el trastorno del sueño están fuertemente asociados: 139 de 158 trabajadores con exceso de peso (88 %) tienen trastorno, frente a 16 de 216 con peso normal (7 %). Se agruparon sobrepeso y obesidad porque, con solo 10 personas en obesidad, la prueba chi-cuadrado no es confiable (R muestra una advertencia). Para la gestión, el IMC es un criterio sencillo y barato para priorizar a quién remitir a estudio del sueño.
# 1. ¿La proporción de trastorno es diferente entre mujeres y hombres?
tab_sexo <- table(sueno$sexo, sueno$trastorno)
tab_sexo
##
## Sin trastorno Con trastorno
## Hombre 137 52
## Mujer 82 103
prop.test(x = tab_sexo[, "Con trastorno"], n = rowSums(tab_sexo))
##
## 2-sample test for equality of proportions with continuity correction
##
## data: tab_sexo[, "Con trastorno"] out of rowSums(tab_sexo)
## X-squared = 29.405, df = 1, p-value = 5.874e-08
## alternative hypothesis: two.sided
## 95 percent confidence interval:
## -0.3827736 -0.1804754
## sample estimates:
## prop 1 prop 2
## 0.2751323 0.5567568
# 2. ¿Quienes tienen estrés alto presentan más trastornos del sueño?
tab_estres <- table(sueno$estres_nivel, sueno$trastorno)
tab_estres
##
## Sin trastorno Con trastorno
## Alto (7-8) 36 84
## Bajo o medio (3-6) 183 71
prop.test(x = tab_estres[, "Con trastorno"], n = rowSums(tab_estres))
##
## 2-sample test for equality of proportions with continuity correction
##
## data: tab_estres[, "Con trastorno"] out of rowSums(tab_estres)
## X-squared = 57.653, df = 1, p-value = 3.128e-14
## alternative hypothesis: two.sided
## 95 percent confidence interval:
## 0.3155022 0.5254427
## sample estimates:
## prop 1 prop 2
## 0.7000000 0.2795276
Interpretación:
ggpubr hace gráficos comparativos que incluyen en la
misma imagen el resultado de la prueba estadística.
library(ggpubr)
ggboxplot(sueno_ocup, x = "ocupacion", y = "horas_sueno", fill = "ocupacion",
palette = "Set2", legend = "none",
xlab = "Ocupación", ylab = "Horas de sueño",
title = "Horas de sueño por ocupación",
order = c("Ventas", "Docencia", "Medicina", "Enfermería",
"Contaduría", "Derecho", "Ingeniería")) +
geom_hline(yintercept = 7, linetype = "dashed", color = "red") +
stat_compare_means(method = "anova", label.y = 8.8)
Lectura del gráfico: la línea roja marca 7 h. Las cajas de ventas y docencia quedan casi completas por debajo de la línea. Enfermería tiene la mediana en 6,5 h: la mitad de las enfermeras duerme menos de 7 h. Medicina tiene la mediana en 7,6 h, pero su caja es muy amplia porque hay un grupo de médicos que duerme alrededor de 6 h. Las dos profesiones de salud son las que muestran mayor diferencia entre su propio personal. Ingeniería queda totalmente por encima. El valor p del ANOVA aparece en el gráfico.
ggboxplot(sueno, x = "trastorno", y = "calidad_sueno", fill = "trastorno",
palette = c("#4E79A7", "#E15759"), legend = "none",
xlab = "", ylab = "Calidad del sueño (1-10)",
title = "Calidad del sueño según presencia de trastorno") +
stat_compare_means(method = "t.test", label.y = 9.8)
Lectura del gráfico: la caja de quienes tienen trastorno está más abajo y es más ancha. Tienen peor calidad de sueño y más variable. El valor p de la prueba t aparece en el gráfico.
prop_grupos <- bind_rows(
sueno %>% group_by(Grupo = sexo) %>% summarise(Pct = mean(trastorno == "Con trastorno") * 100),
sueno %>% group_by(Grupo = estres_nivel) %>% summarise(Pct = mean(trastorno == "Con trastorno") * 100),
sueno %>% group_by(Grupo = peso) %>% summarise(Pct = mean(trastorno == "Con trastorno") * 100)
) %>%
mutate(Factor = rep(c("Sexo", "Estrés", "Peso"), each = 2),
Pct = round(Pct, 1))
ggbarplot(prop_grupos, x = "Grupo", y = "Pct", fill = "Factor",
palette = c("#59A14F", "#F28E2B", "#4E79A7"),
label = TRUE, facet.by = "Factor", scales = "free_x", legend = "none",
xlab = "", ylab = "% con trastorno del sueño", x.text.angle = 20,
title = "Porcentaje de trabajadores con trastorno del sueño por grupo") +
coord_cartesian(ylim = c(0, 100))
Lectura del gráfico: de los tres factores, el peso muestra la diferencia más grande (7 % frente a 88 %), seguido por el estrés (28 % frente a 70 %) y el sexo (28 % frente a 56 %). Es el gráfico más útil para presentar a la gerencia: con el peso y el estrés se puede priorizar el programa de sueño.