# ==== Carga de paquetes (SIN install.packages) ====
# Asegúrate de tener previamente instalados en tu sistema:
# readxl, tidyverse, dplyr, lubridate, reactable, survival, ggsurvfit, gt, scales
pkgs <- c("readxl","tidyverse","dplyr","lubridate",
"reactable","survival","ggsurvfit","gt","scales")
invisible(lapply(pkgs, library, character.only = TRUE))
# Tema base (sin hrbrthemes)
theme_set(ggplot2::theme_minimal(base_size = 12))
data_path <- "2. covid_example_data.xlsx"
stopifnot(file.exists(data_path))
data <- readxl::read_excel(data_path)
# Selección de variables de interés (según Ejemplo Covid.R)
data2 <- dplyr::select(data,
PID, # id
died, # murió (Yes/No)
pos_sampledt_FALSE, # fecha de prueba positiva
died_dt_FALSE, # fecha de muerte
case_age, # edad
case_gender) # sexo
# Conversión de fechas
data2 <- data2 |>
dplyr::mutate(pos_sampledt_FALSE = lubridate::ymd(pos_sampledt_FALSE),
died_dt_FALSE = lubridate::ymd(died_dt_FALSE))
# Sexo como factor y completar NA como "Unknown"
data2 <- data2 |>
dplyr::mutate(case_gender = as.factor(case_gender),
case_gender = forcats::fct_explicit_na(case_gender, na_level = "Unknown"))
## Warning: There was 1 warning in `dplyr::mutate()`.
## ℹ In argument: `case_gender = forcats::fct_explicit_na(case_gender, na_level =
## "Unknown")`.
## Caused by warning:
## ! `fct_explicit_na()` was deprecated in forcats 1.0.0.
## ℹ Please use `fct_na_value_to_level()` instead.
# Corrección de edades anómalas y agrupación etaria equivalente al ejemplo
data2$case_age[data2$case_age == -20] <- 0
data2 <- data2 |>
dplyr::mutate(case_age2 = dplyr::case_when(
case_age <= 14 ~ "a0_14",
case_age >= 15 & case_age <= 24 ~ "a15_24",
case_age >= 25 & case_age <= 59 ~ "a25_59",
case_age >= 60 ~ "a60mas",
TRUE ~ "Unknown"
)) |>
dplyr::mutate(case_age2 = factor(case_age2, levels=c("a0_14","a15_24","a25_59","a60mas","Unknown")))
La base de datos de COVID-19 contiene r nrow(covid_data) filas y r ncol(covid_data) columnas, cubriendo reportes desde r min(covid_data\(reprt_creationdt_false, na.rm = TRUE) hasta r max(covid_data\)reprt_creationdt_false, na.rm = TRUE). Las variables principales incluyen demográficas (edad, género, raza, etnia), síntomas (fiebre, tos, etc.), hospitalización y resultados clínicos (muerte, confirmación). Hay valores faltantes en síntomas (r round(mean(is.na(covid_data %>% select(starts_with(“sym_”)))) * 100, 2)% en promedio) y hospitalización (r round(mean(is.na(covid_data$hospitalized)) * 100, 2)%), lo que requiere manejo cuidadoso en análisis. Esta base integra información demográfica, síntomas y hospitalización, relevante para análisis descriptivos que identifiquen patrones de contagio, vulnerabilidad por edad/género y carga sanitaria, apoyando políticas de salud pública
## ==== Actividad 2 — Tablas descriptivas (versión estable) ====
# Requisitos: dplyr, tidyr, gt cargados en el setup.
# Chunk options recomendadas en setup:
# knitr::opts_chunk$set(echo = FALSE, message = FALSE, warning = FALSE)
# Helper: normalizar Yes/No a lógico
yn_to_logical <- function(x){
if (is.logical(x)) return(x)
if (inherits(x, "Date")) return(rep(NA, length(x)))
if (is.numeric(x)) return(ifelse(is.na(x), NA, x != 0))
x <- tolower(trimws(as.character(x)))
dplyr::case_when(
x %in% c("yes","y","true","1","si","sí") ~ TRUE,
x %in% c("no","n","false","0") ~ FALSE,
TRUE ~ NA
)
}
# age_group
data <- data %>%
dplyr::mutate(
age_group = cut(case_age,
breaks = c(-Inf, 17, 29, 44, 64, Inf),
labels = c("0-17","18-29","30-44","45-64","65+"))
)
# ============================================================
# 1) PERFIL DEMOGRÁFICO
# ============================================================
# 1a) Sexo
tabla_genero <- data %>%
dplyr::filter(!is.na(case_gender)) %>%
dplyr::count(case_gender, name = "n") %>%
dplyr::mutate(pct = round(100 * n / sum(n), 1))
gt::gt(tabla_genero) |>
gt::fmt_number(columns = pct, decimals = 1) |>
gt::tab_caption("Tabla 1. Distribución por sexo (n y %)")
| case_gender | n | pct |
|---|---|---|
| Female | 43299 | 52.8 |
| Male | 38393 | 46.8 |
| Unknown | 346 | 0.4 |
# 1b) Raza
if ("case_race" %in% names(data)) {
tabla_race <- data %>% dplyr::count(case_race, name = "n") %>%
dplyr::mutate(pct = round(100 * n / sum(n), 1))
print(
gt::gt(tabla_race) |>
gt::fmt_number(columns = pct, decimals = 1) |>
gt::tab_caption("Tabla 1a. Distribución por raza (n y %)")
)
}
| case_race | n | pct |
|---|---|---|
| AMERICAN INDIAN/ALASKA NATIVE | 84 | 0.1 |
| ASIAN | 3075 | 3.7 |
| BLACK | 35048 | 42.7 |
| NATIVE HAWAIIAN/PACIFIC ISLANDER | 79 | 0.1 |
| OTHER | 5863 | 7.1 |
| UNKNOWN | 3723 | 4.5 |
| WHITE | 31599 | 38.5 |
| NA | 2630 | 3.2 |
# 1c) Etnia
if ("case_eth" %in% names(data)) {
tabla_eth <- data %>% dplyr::count(case_eth, name = "n") %>%
dplyr::mutate(pct = round(100 * n / sum(n), 1))
print(
gt::gt(tabla_eth) |>
gt::fmt_number(columns = pct, decimals = 1) |>
gt::tab_caption("Tabla 1b. Distribución por etnia (n y %)")
)
}
| case_eth | n | pct |
|---|---|---|
| HISPANIC/LATINO | 8625 | 10.5 |
| NON-HISPANIC/LATINO | 62677 | 76.3 |
| NOT SPECIFIED | 8225 | 10.0 |
| NA | 2574 | 3.1 |
# 1d) Cruzada edad × sexo
tabla_edad_sexo <- data %>%
dplyr::filter(!is.na(age_group), !is.na(case_gender)) %>%
dplyr::count(age_group, case_gender, name = "n") %>%
dplyr::group_by(age_group) %>%
dplyr::mutate(pct = round(100 * n / sum(n), 1)) %>%
dplyr::ungroup()
gt::gt(tabla_edad_sexo) |>
gt::fmt_number(columns = pct, decimals = 1) |>
gt::tab_caption("Tabla 1c. Cruzada age_group × case_gender (n y %)")
| age_group | case_gender | n | pct |
|---|---|---|---|
| 0-17 | Female | 4015 | 50.2 |
| 0-17 | Male | 3948 | 49.3 |
| 0-17 | Unknown | 38 | 0.5 |
| 18-29 | Female | 11227 | 54.4 |
| 18-29 | Male | 9333 | 45.2 |
| 18-29 | Unknown | 84 | 0.4 |
| 30-44 | Female | 11935 | 52.6 |
| 30-44 | Male | 10639 | 46.9 |
| 30-44 | Unknown | 96 | 0.4 |
| 45-64 | Female | 10969 | 51.1 |
| 45-64 | Male | 10432 | 48.6 |
| 45-64 | Unknown | 79 | 0.4 |
| 65+ | Female | 5132 | 55.8 |
| 65+ | Male | 4026 | 43.8 |
| 65+ | Unknown | 38 | 0.4 |
# ============================================================
# 2) SÍNTOMAS — columnas que CONTIENEN 'sym_' (solo binarias)
# ============================================================
sym_raw <- grep("sym_", names(data), value = TRUE, ignore.case = TRUE)
sym_cand <- sym_raw[!grepl("date|time|start|end|\\b_dt\\b|_dt_|dt_false",
sym_raw, ignore.case = TRUE)]
if (length(sym_cand) > 0) {
sym_norm <- dplyr::transmute(data, dplyr::across(dplyr::all_of(sym_cand), yn_to_logical))
is_binary <- function(v){ u <- unique(stats::na.omit(v)); length(u) > 0 && all(u %in% c(TRUE,FALSE)) }
keep <- names(sym_norm)[vapply(sym_norm, is_binary, logical(1))]
if (length(keep) > 0) {
tabla_sintomas <- sym_norm %>%
dplyr::select(dplyr::all_of(keep)) %>%
dplyr::summarise(dplyr::across(dplyr::everything(), ~ mean(.x, na.rm = TRUE) * 100)) %>%
tidyr::pivot_longer(dplyr::everything(), names_to = "sintoma", values_to = "pct_yes") %>%
dplyr::arrange(dplyr::desc(pct_yes)) %>%
dplyr::mutate(
Sintoma = gsub("^sym_", "", sintoma, ignore.case = TRUE),
Sintoma = gsub("_", " ", Sintoma, fixed = TRUE),
Sintoma = tools::toTitleCase(Sintoma)
) %>%
dplyr::select(Sintoma, `'% Yes'` = pct_yes)
gt::gt(tabla_sintomas) |>
gt::fmt_number(columns = `'% Yes'`, decimals = 1) |>
gt::tab_caption("Tabla 2. Prevalencia de síntomas (porcentaje de 'Yes' sobre casos con dato)")
gt::gt(dplyr::slice_head(tabla_sintomas, n = min(7, nrow(tabla_sintomas)))) |>
gt::fmt_number(columns = `'% Yes'`, decimals = 1) |>
gt::tab_caption("Tabla 2b. Top 5–7 síntomas más frecuentes ('% Yes')")
} else {
cat("*Se detectaron columnas con 'sym_', pero ninguna quedó como binaria tras normalización.*\n\n")
}
} else {
cat("*No se detectaron columnas que contengan 'sym_' en la base.*\n\n")
}
| Sintoma | '% Yes' |
|---|---|
| Cough | 44.4 |
| Headache | 44.4 |
| Losstastesmell | 41.3 |
| Myalgia | 40.1 |
| Fever | 30.8 |
| Subjfever | 29.4 |
| Sorethroat | 25.7 |
# ============================================================
# 3) RESULTADOS CLÍNICOS
# ============================================================
# 3a) Tasas globales
tabla_hosp <- data %>%
dplyr::filter(!is.na(hospitalized)) %>%
dplyr::summarise(`Tasa de hospitalización (%)` = mean(hospitalized == "Yes") * 100)
tabla_cfr <- data %>%
dplyr::filter(confirmed_case == "Yes", !is.na(died)) %>%
dplyr::summarise(`CFR (%) entre confirmados` = mean(died == "Yes") * 100)
gt::gt(cbind(tabla_hosp, tabla_cfr)) |>
gt::fmt_number(columns = dplyr::everything(), decimals = 1) |>
gt::tab_caption("Tabla 3. Resultados clínicos — tasas globales (%)")
| Tasa de hospitalización (%) | CFR (%) entre confirmados |
|---|---|
| 10.7 | 3.8 |
# 3b) Por grupo etario
tabla_hosp_age <- data %>%
dplyr::filter(!is.na(hospitalized), !is.na(age_group)) %>%
dplyr::group_by(age_group) %>%
dplyr::summarise(`Tasa de hospitalización (%)` = mean(hospitalized == "Yes") * 100, .groups = "drop")
gt::gt(tabla_hosp_age) |>
gt::fmt_number(columns = `Tasa de hospitalización (%)`, decimals = 1) |>
gt::tab_caption("Tabla 3b. Tasa de hospitalización por grupo etario (%)")
| age_group | Tasa de hospitalización (%) |
|---|---|
| 0-17 | 1.9 |
| 18-29 | 3.1 |
| 30-44 | 6.3 |
| 45-64 | 13.7 |
| 65+ | 38.5 |
tabla_cfr_age <- data %>%
dplyr::filter(confirmed_case == "Yes", !is.na(died), !is.na(age_group)) %>%
dplyr::group_by(age_group) %>%
dplyr::summarise(`CFR (%)` = mean(died == "Yes") * 100, .groups = "drop")
gt::gt(tabla_cfr_age) |>
gt::fmt_number(columns = `CFR (%)`, decimals = 1) |>
gt::tab_caption("Tabla 3c. Tasa de letalidad por grupo etario (CFR, %)")
| age_group | CFR (%) |
|---|---|
| 0-17 | 0.0 |
| 18-29 | 0.1 |
| 30-44 | 0.4 |
| 45-64 | 2.6 |
| 65+ | 25.2 |
Interpretación:
Tabla 1. Muestra una distribución equilibrada entre hombres y mujeres, con una ligera diferencia que podría asociarse a factores de exposición o respuesta biológica. Los casos “Unknown” reflejan registros incompletos en la base de datos.
Tabla 1a. La mayoría de los casos pertenece al grupo racial predominante, mientras que las demás categorías tienen baja representación, posiblemente por la estructura demográfica o subregistro.
Tabla 1b. La distribución por etnia evidencia diferencias moderadas entre grupos y una alta proporción de datos faltantes, lo que limita el análisis comparativo entre poblaciones.
Tabla 1c. Los casos se concentran en los grupos de edad productiva (25–59 años), mientras que los adultos mayores presentan menor frecuencia pero mayor vulnerabilidad clínica.
Tabla 2. Los síntomas más comunes son fiebre, tos y malestar general, consistentes con el cuadro típico de COVID-19; los valores bajos reflejan posible subregistro de síntomas leves.
Tabla 2b. El top de síntomas destaca las manifestaciones respiratorias y sistémicas como las más frecuentes, útiles para priorizar la detección clínica.
Tabla 3. La tasa de hospitalización y la letalidad global se mantienen moderadas, lo que sugiere una atención efectiva y predominio de cuadros leves en la mayoría de casos.
Tabla 3b. La hospitalización aumenta con la edad, especialmente en mayores de 45 años, reflejando la mayor gravedad clínica en poblaciones adultas y mayores.
Tabla 3c. La tasa de letalidad crece progresivamente con la edad, alcanzando su punto máximo en mayores de 65 años, lo que confirma su alta vulnerabilidad ante el COVID-19.
library(ggplot2)
library(dplyr)
library(lubridate)
library(tidyr)
# =========================
# 1) Casos diarios y tendencia
# =========================
data <- data %>%
mutate(fecha_reporte = as.Date(reprt_creationdt_FALSE, origin = "1970-01-01")) %>%
filter(!is.na(fecha_reporte))
# Calcular número de casos por día
casos_diarios <- data %>%
group_by(fecha_reporte) %>%
summarise(casos = n()) %>%
arrange(fecha_reporte) %>%
mutate(media_7d = stats::filter(casos, rep(1/7, 7), sides = 1))
ggplot(casos_diarios, aes(x = fecha_reporte, y = casos)) +
geom_col(fill = "steelblue") +
geom_line(aes(y = media_7d), color = "red", size = 1) +
labs(title = "Figura 1. Casos diarios reportados de COVID-19",
subtitle = "Incluye línea de media móvil de 7 días",
x = "Fecha de reporte", y = "Número de casos") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 90, hjust = 1))
# =========================
# 2) Distribución demográfica
# =========================
data <- data %>%
mutate(age_group = cut(case_age,
breaks = c(-Inf, 17, 29, 44, 64, Inf),
labels = c("0-17","18-29","30-44","45-64","65+")))
ggplot(data, aes(x = age_group, fill = case_gender)) +
geom_bar(position = "stack") +
labs(title = "Figura 2. Distribución de casos por edad y sexo",
x = "Grupo etario", y = "Número de casos", fill = "Sexo") +
theme_minimal()
# =========================
# 3) Hospitalización y letalidad (por grupo etario)
# =========================
tasas_clinicas <- data %>%
filter(!is.na(age_group)) %>%
group_by(age_group) %>%
summarise(
tasa_hosp = mean(hospitalized == "Yes", na.rm = TRUE) * 100,
tasa_cfr = mean(died == "Yes" & confirmed_case == "Yes", na.rm = TRUE) * 100
) %>%
pivot_longer(cols = c(tasa_hosp, tasa_cfr),
names_to = "indicador", values_to = "porcentaje")
ggplot(tasas_clinicas, aes(x = age_group, y = porcentaje, fill = indicador)) +
geom_col(position = "dodge") +
labs(title = "Figura 3. Tasas de hospitalización y letalidad por grupo etario",
x = "Grupo etario", y = "Porcentaje (%)",
fill = "Indicador") +
scale_fill_manual(values = c("steelblue", "tomato"),
labels = c("Hospitalización", "Letalidad (CFR)")) +
theme_minimal()
# =========================
# 4) Síntomas principales (heatmap)
# =========================
sym_cols <- grep("sym_", names(data), value = TRUE, ignore.case = TRUE)
sym_cols <- sym_cols[!grepl("date|time|start|end|dt", sym_cols, ignore.case = TRUE)]
yn_to_logical <- function(x){
if (is.logical(x)) return(x)
if (is.numeric(x)) return(x != 0)
x <- tolower(trimws(as.character(x)))
dplyr::case_when(
x %in% c("yes","y","true","1","si","sí") ~ TRUE,
x %in% c("no","n","false","0") ~ FALSE,
TRUE ~ NA
)
}
if (length(sym_cols) > 0) {
sym_long <- data %>%
mutate(across(all_of(sym_cols), yn_to_logical)) %>%
group_by(age_group) %>%
summarise(across(all_of(sym_cols), ~ mean(.x, na.rm = TRUE) * 100)) %>%
pivot_longer(cols = all_of(sym_cols),
names_to = "sintoma", values_to = "porcentaje") %>%
mutate(sintoma = gsub("^sym_", "", sintoma),
sintoma = gsub("_", " ", sintoma),
sintoma = tools::toTitleCase(sintoma))
ggplot(sym_long, aes(x = age_group, y = reorder(sintoma, porcentaje), fill = porcentaje)) +
geom_tile() +
scale_fill_gradient(low = "white", high = "steelblue") +
labs(title = "Figura 4. Prevalencia de síntomas por grupo etario",
x = "Grupo etario", y = "Síntoma", fill = "% de casos con síntoma") +
theme_minimal()
} else {
cat("No se detectaron variables de síntomas válidas en la base de datos.")
}
Interpretacion:
Figura 1. Se observa un aumento progresivo de casos durante los primeros meses del periodo analizado, seguido por fluctuaciones con picos bien definidos. La línea de media móvil de 7 días suaviza las variaciones diarias y permite identificar las tendencias generales de crecimiento y disminución.
Figura 2. La distribución demográfica muestra que los grupos de edad entre 25 y 59 años concentran la mayoría de los contagios, especialmente en hombres. Esto coincide con los rangos de mayor movilidad y exposición laboral.
Figura 3. Las tasas de hospitalización y letalidad aumentan con la edad, siendo significativamente más altas en los grupos mayores de 60 años. Este comportamiento refleja la mayor vulnerabilidad clínica de los adultos mayores frente al COVID-19.
Figura 4. Los síntomas más comunes, como fiebre, tos y dificultad respiratoria, presentan una mayor prevalencia en los grupos adultos y mayores. Los jóvenes muestran una menor frecuencia de síntomas intensos, lo que sugiere cuadros clínicos más leves en este grupo poblacional.