Pregunta: ¿Qué grupos de diagnósticos concentran más egresos y días de estancia en los servicios internos del sector hospitalario privado de Guatemala, y cuáles ameritan una revisión prioritaria para la planificación de servicios?
Unidad de análisis: registro de egreso hospitalario, no paciente único. Los datos reflejan actividad registrada por establecimientos privados que reportan al INE. Las variables territoriales indican residencia y no localización del hospital. Los resultados no permiten medir ocupación de camas, eficiencia, costos reales, reingresos ni causalidad.
Fuentes: INE Guatemala, bases de servicios internos 2018–2024 y diccionario de variables/CIE-10. Repositorio oficial. Registrar fecha de consulta al entregar el proyecto.
Antes de comenzar: coloca este .Rmd y
los ocho Excel en la misma carpeta, o guarda los Excel
en una subcarpeta datos_originales/. Después selecciona
Knit → Knit to HTML. No necesitas las bases de servicios
externos. El resultado será solo el reporte HTML; las
tablas y figuras quedan dentro de él.
En el HTML puedes pulsar Code para mostrar u ocultar el código que produjo los resultados. No se exportarán gráficas ni hojas de cálculo.
# Identificamos los archivos de cada año que vamos a unir.
archivos <- c(
"servicios-internos-2018.xlsx", "servicios-internos-2019.xlsx",
"servicios-internos-2020.xlsx", "servicios-internos-2021.xlsx",
"servicios-internos-2022.xlsx", "bdinterna2023.xlsx",
"servicios-internos-2024.xlsx"
)
dic_archivo <- "diccionario-variables-interna.xlsx"
# Buscamos los archivos junto al Rmd o dentro de datos_originales.
buscar_archivo <- function(nombre) {
candidatos <- c(file.path("datos_originales", nombre), nombre)
existe <- candidatos[file.exists(candidatos)]
if (!length(existe)) stop("Falta el archivo: ", nombre, ". Colócalo junto al Rmd o en datos_originales/.")
existe[1]
}
rutas <- vapply(archivos, buscar_archivo, character(1))
ruta_dic <- buscar_archivo(dic_archivo)
knitr::kable(
tibble(
anio = 2018:2024,
archivo = basename(rutas)
),
col.names = c("Anio", "Archivo de origen")
)
| Anio | Archivo de origen |
|---|---|
| 2018 | servicios-internos-2018.xlsx |
| 2019 | servicios-internos-2019.xlsx |
| 2020 | servicios-internos-2020.xlsx |
| 2021 | servicios-internos-2021.xlsx |
| 2022 | servicios-internos-2022.xlsx |
| 2023 | bdinterna2023.xlsx |
| 2024 | servicios-internos-2024.xlsx |
Decisión técnica: leer todos los campos como texto preserva códigos municipales con ceros iniciales y evita la conversión accidental de códigos CIE-10.
# Leemos cada archivo como texto para conservar los códigos tal como vienen.
leer_anio <- function(ruta, anio_esperado) {
datos <- readxl::read_excel(ruta, sheet=1, col_types="text", .name_repair="minimal")
names(datos) <- toupper(trimws(names(datos)))
# La estancia se llama DIASESTAN solamente en 2024.
if ("DIASESTAN" %in% names(datos)) names(datos)[names(datos)=="DIASESTAN"] <- "DIASESTANCIA"
columnas <- c("AÑO", "MES", "DIASESTANCIA", "SEXO", "PPERTENENCIA", "EDAD", "PERIODOEDA", "DEPTORESIDEN", "MUNIRESIDEN", "CAUFIN", "CONDIEGRES", "TRATARECIB")
faltan <- setdiff(columnas, names(datos))
if (length(faltan)) stop("Columnas ausentes en ", basename(ruta), ": ", paste(faltan, collapse=", "))
datos <- datos[, columnas]
datos$ARCHIVO_ORIGEN <- basename(ruta)
datos$ANIO_ARCHIVO <- anio_esperado
datos
}
# Reunimos los siete años en una sola base sin eliminar filas.
base_cruda <- purrr::map2_dfr(rutas, 2018:2024, leer_anio)
cat("Registros consolidados:", format(nrow(base_cruda), big.mark=","), "\n")
## Registros consolidados: 1,902,855
Esta etapa es necesaria para que los códigos de la base tengan
significado y para que los valores como 9999 no
distorsionen los cálculos. Conservamos los registros originales para
contar egresos, incluso si les falta información de estancia.
# Hoja 2 tiene una fila de título encima de las cabeceras.
cie <- readxl::read_excel(ruta_dic, sheet="CIE-10", skip=1, col_types="text") |>
dplyr::transmute(codigo=toupper(trimws(CAUSA)), diagnostico=trimws(DESCRIP)) |>
dplyr::filter(!is.na(codigo), codigo!="", !is.na(diagnostico)) |>
dplyr::distinct(codigo, .keep_all=TRUE)
# Hoja 1: además de los códigos especiales, incluye los nombres de los departamentos.
diccionario_variables <- readxl::read_excel(ruta_dic, sheet="Interna", skip=1, col_types="text")
print(utils::head(diccionario_variables, 15))
## # A tibble: 15 × 3
## Valor Código Etiqueta
## <chr> <chr> <chr>
## 1 Año 2018 2018
## 2 <NA> 2022 2022
## 3 Mes 1 Enero
## 4 <NA> 2 Febrero
## 5 <NA> 3 Marzo
## 6 <NA> 4 Abril
## 7 <NA> 5 Mayo
## 8 <NA> 6 Junio
## 9 <NA> 7 Julio
## 10 <NA> 8 Agosto
## 11 <NA> 9 Septiembre
## 12 <NA> 10 Octubre
## 13 <NA> 11 Noviembre
## 14 <NA> 12 Diciembre
## 15 Días de estancia 1 1
# Extraemos los departamentos directamente del listado oficial del diccionario.
# El bloque empieza en "Departamento de residencia" y termina antes de "Municipio de residencia".
filas_dic <- readxl::read_excel(ruta_dic, sheet="Interna", col_names=FALSE, col_types="text")
inicio_depto <- which(trimws(filas_dic[[1]]) == "Departamento de residencia")[1]
fin_depto <- which(trimws(filas_dic[[1]]) == "Municipio de residencia")[1]
if (is.na(inicio_depto) || is.na(fin_depto) || fin_depto <= inicio_depto) {
stop("No se pudo localizar la lista de departamentos en el diccionario del INE.")
}
departamentos <- filas_dic[inicio_depto:(fin_depto - 1), 2:3] |>
setNames(c("codigo", "departamento")) |>
dplyr::mutate(codigo=stringr::str_pad(trimws(codigo), 2, pad="0"),
departamento=trimws(departamento)) |>
dplyr::filter(!is.na(codigo), !is.na(departamento), codigo!="", departamento!="") |>
dplyr::distinct(codigo, .keep_all=TRUE)
knitr::kable(departamentos, caption="Equivalencias de departamentos extraídas del diccionario del INE")
| codigo | departamento |
|---|---|
| 01 | Guatemala |
| 02 | El Progreso |
| 03 | Sacatepéquez |
| 04 | Chimaltenango |
| 05 | Escuintla |
| 06 | Santa Rosa |
| 07 | Sololá |
| 08 | Totonicapán |
| 09 | Quetzaltenango |
| 10 | Suchitepéquez |
| 11 | Retalhuleu |
| 12 | San Marcos |
| 13 | Huehuetenango |
| 14 | Quiché |
| 15 | Baja Verapaz |
| 16 | Alta Verapaz |
| 17 | Petén |
| 18 | Izabal |
| 19 | Zacapa |
| 20 | Chiquimula |
| 21 | Jalapa |
| 22 | Jutiapa |
| 23 | Extranjero |
| 99 | Ignorado |
cat("Códigos de diagnóstico en catálogo:", nrow(cie), "\n")
## Códigos de diagnóstico en catálogo: 14362
Criterio: 9999 en días de estancia
corresponde a “ignorado” según el diccionario. No es una hospitalización
de 9999 días. Se convierte en NA solamente para los
cálculos de estancia, pero se mantiene el egreso para los
conteos. La edad debe interpretarse según la unidad
PERIODOEDA, evitando asumir que todas las edades están
expresadas en años.
# Convertimos a número únicamente las columnas que se usarán para cálculos.
num <- function(x) suppressWarnings(as.numeric(trimws(x)))
# Creamos variables fáciles de analizar y unimos los nombres de diagnósticos.
base <- base_cruda |>
dplyr::mutate(
anio=num(`AÑO`), mes=num(MES), estancia_original=num(DIASESTANCIA),
estancia=dplyr::if_else(estancia_original==9999, NA_real_, estancia_original),
edad_valor=num(EDAD), unidad_edad=num(PERIODOEDA),
sexo_codigo=trimws(SEXO), departamento_codigo=stringr::str_pad(trimws(DEPTORESIDEN), 2, pad="0"),
municipio_codigo=stringr::str_pad(trimws(MUNIRESIDEN), 4, pad="0"),
codigo=toupper(stringr::str_replace_all(trimws(CAUFIN), "\\.", "")),
capitulo_inicial=substr(codigo, 1, 1),
# Codificación esperada: 1=días, 2=meses, 3=años. Validar en hoja Interna.
edad_anios=dplyr::case_when(
edad_valor==999 ~ NA_real_,
unidad_edad==1 ~ edad_valor/365.25,
unidad_edad==2 ~ edad_valor/12,
unidad_edad==3 ~ edad_valor,
TRUE ~ NA_real_
),
grupo_edad=cut(edad_anios, breaks=c(-Inf, 4, 14, 29, 44, 59, 74, Inf),
labels=c("0–4", "5–14", "15–29", "30–44", "45–59", "60–74", "75+")),
sexo=dplyr::case_when(sexo_codigo=="1" ~ "Hombre", sexo_codigo=="2" ~ "Mujer", TRUE ~ "Sin clasificar")
) |>
dplyr::left_join(cie, by="codigo") |>
dplyr::mutate(
diagnostico=dplyr::coalesce(diagnostico, paste0("Sin correspondencia CIE-10: ", codigo)),
anio=as.integer(anio), mes=as.integer(mes),
grupo_cie=dplyr::case_when(
capitulo_inicial %in% c("A","B") ~ "Infecciosas y parasitarias",
capitulo_inicial=="C" ~ "Tumores malignos",
capitulo_inicial=="D" ~ "Tumores benignos / sangre / inmunidad",
capitulo_inicial=="E" ~ "Endocrinas, nutricionales y metabólicas",
capitulo_inicial=="F" ~ "Trastornos mentales",
capitulo_inicial=="G" ~ "Sistema nervioso",
capitulo_inicial=="H" ~ "Ojo y oído",
capitulo_inicial=="I" ~ "Sistema circulatorio",
capitulo_inicial=="J" ~ "Sistema respiratorio",
capitulo_inicial=="K" ~ "Sistema digestivo",
capitulo_inicial=="L" ~ "Piel y tejido subcutáneo",
capitulo_inicial=="M" ~ "Sistema osteomuscular",
capitulo_inicial=="N" ~ "Sistema genitourinario",
capitulo_inicial=="O" ~ "Embarazo, parto y puerperio",
capitulo_inicial=="P" ~ "Afecciones perinatales",
capitulo_inicial=="Q" ~ "Malformaciones congénitas",
capitulo_inicial=="R" ~ "Síntomas y hallazgos",
capitulo_inicial %in% c("S","T") ~ "Lesiones e intoxicaciones",
capitulo_inicial %in% c("V","W","X","Y") ~ "Causas externas",
capitulo_inicial=="Z" ~ "Factores de contacto / servicios de salud",
TRUE ~ "Otros / revisar código"
)
)
# Los capítulos son un agrupamiento exploratorio por primer carácter del código.
# No mezclar grupos de enfermedad con embarazos/nacimientos o factores Z en una conclusión clínica.
¿Por qué revisamos la calidad? Para comprobar cuántos datos requieren precaución antes de sacar conclusiones. Los problemas identificados se documentan, no se eliminan sin justificación.
# Contamos registros y posibles problemas de calidad para cada año.
calidad <- base |>
dplyr::group_by(anio_archivo=ANIO_ARCHIVO) |>
dplyr::summarise(
registros=dplyr::n(),
anio_inconsistente=sum(is.na(anio) | anio!=anio_archivo),
mes_invalido=sum(is.na(mes) | !(mes %in% 1:12)),
estancia_ignorada=sum(is.na(estancia)),
estancia_negativa=sum(!is.na(estancia) & estancia<0),
estancia_mayor_365=sum(!is.na(estancia) & estancia>365),
diagnostico_sin_catalogo=sum(is.na(match(codigo, cie$codigo))),
edad_no_interpretable=sum(is.na(edad_anios)),
.groups="drop"
)
knitr::kable(calidad |> dplyr::rename(`Año`=anio_archivo, `Registros`=registros, `Año inconsistente`=anio_inconsistente, `Mes inválido`=mes_invalido, `Estancia ignorada`=estancia_ignorada, `Estancia negativa`=estancia_negativa, `Estancia mayor a 365`=estancia_mayor_365, `Diagnóstico sin catálogo`=diagnostico_sin_catalogo, `Edad no interpretable`=edad_no_interpretable), caption="Auditoría de calidad por año")
| Año | Registros | Año inconsistente | Mes inválido | Estancia ignorada | Estancia negativa | Estancia mayor a 365 | Diagnóstico sin catálogo | Edad no interpretable |
|---|---|---|---|---|---|---|---|---|
| 2018 | 238376 | 0 | 0 | 3655 | 0 | 0 | 0 | 433 |
| 2019 | 286085 | 0 | 0 | 2630 | 0 | 0 | 0 | 373 |
| 2020 | 261295 | 0 | 0 | 3521 | 0 | 0 | 0 | 640 |
| 2021 | 228471 | 0 | 0 | 2079 | 0 | 0 | 0 | 839 |
| 2022 | 194345 | 0 | 0 | 10670 | 0 | 0 | 0 | 593 |
| 2023 | 344262 | 0 | 0 | 1515 | 0 | 0 | 1 | 1069 |
| 2024 | 350021 | 0 | 0 | 3196 | 0 | 0 | 1 | 1328 |
cat("Aclaración: NO se eliminan duplicados idénticos automáticamente: pueden ser egresos distintos sin identificador de paciente.\n")
## Aclaración: NO se eliminan duplicados idénticos automáticamente: pueden ser egresos distintos sin identificador de paciente.
sin_cie <- base |> dplyr::filter(is.na(match(codigo, cie$codigo))) |>
dplyr::count(codigo, sort=TRUE)
knitr::kable(head(sin_cie, 20), caption="Códigos no encontrados en catálogo (revisar)")
| codigo | n |
|---|---|
| C63X | 1 |
| I849 | 1 |
estancias_extremas <- base |>
dplyr::filter(!is.na(estancia), estancia>365) |>
dplyr::count(anio, estancia, sort=TRUE)
knitr::kable(head(estancias_extremas, 20), caption="Estancias mayores a 365 días: revisar, no corregir automáticamente")
| anio | estancia | n |
|---|
Regla de prudencia: no se eliminan estancias
superiores a 365 días sin una razón documentada. Se presentan como
posible anomalía y se calcula además una sensibilidad restringida a
estancias de 0 a 365 días. Para el análisis principal, se excluye
únicamente 9999 y valores negativos en los cálculos de
días.
# Preparamos la estancia válida y una alternativa que excluye valores extremos.
base <- base |>
dplyr::mutate(estancia_valida=dplyr::if_else(!is.na(estancia) & estancia>=0, estancia, NA_real_),
estancia_sensibilidad=dplyr::if_else(!is.na(estancia) & estancia>=0 & estancia<=365, estancia, NA_real_))
resumen_anual <- base |>
dplyr::group_by(anio) |>
dplyr::summarise(egresos=dplyr::n(),
dias=sum(estancia_valida, na.rm=TRUE),
estancia_media=mean(estancia_valida, na.rm=TRUE),
media_sensibilidad=mean(estancia_sensibilidad, na.rm=TRUE),
registros_sin_estancia=sum(is.na(estancia_valida)), .groups="drop")
knitr::kable(resumen_anual |> dplyr::rename(`Año`=anio, `Egresos`=egresos, `Días de estancia`=dias, `Estancia promedio`=estancia_media, `Promedio de sensibilidad`=media_sensibilidad, `Sin estancia válida`=registros_sin_estancia), digits=2, caption="Resumen anual del período")
| Año | Egresos | Días de estancia | Estancia promedio | Promedio de sensibilidad | Sin estancia válida |
|---|---|---|---|---|---|
| 2018 | 238376 | 441147 | 1.88 | 1.88 | 3655 |
| 2019 | 286085 | 499144 | 1.76 | 1.76 | 2630 |
| 2020 | 261295 | 461734 | 1.79 | 1.79 | 3521 |
| 2021 | 228471 | 427226 | 1.89 | 1.89 | 2079 |
| 2022 | 194345 | 359081 | 1.95 | 1.95 | 10670 |
| 2023 | 344262 | 572636 | 1.67 | 1.67 | 1515 |
| 2024 | 350021 | 520212 | 1.50 | 1.50 | 3196 |
Para cada visualización explicamos por qué la hicimos, mostramos el gráfico y añadimos una interpretación automática basada en las cifras obtenidas. Los resultados se actualizarán si cambias los archivos de entrada. Las cifras describen egresos registrados, no casos únicos ni incidencia de enfermedad.
Justificación. Construimos una serie de tiempo para observar la tendencia de egresos registrados desde 2018 hasta 2024 e identificar cambios importantes entre años.
# Graficamos el total de egresos de cada año.
g1 <- ggplot(resumen_anual, aes(x=anio, y=egresos)) +
geom_line(linewidth=1, color="#176B8A") + geom_point(size=2.8, color="#176B8A") +
scale_x_continuous(breaks=2018:2024) +
scale_y_continuous(labels=scales::label_comma()) +
labs(title="Egresos hospitalarios registrados, 2018–2024", x="Año", y="Egresos", caption="INE · servicios internos privados · cambios pueden reflejar cobertura de reporte")
print(g1)
Interpretación. Se observa una caída sostenida de los egresos registrados desde 2019 hasta 2022. En 2019 hubo 286,085 egresos, frente a 194,345 en 2022, una variación de -32.1 %. Posteriormente se registra un aumento pronunciado en 2023 y 2024. Este comportamiento merece una revisión específica: la pandemia pudo afectar la atención hospitalaria durante parte del período, pero no podemos atribuirle toda la caída ni el repunte sin revisar además cambios en la cobertura de establecimientos, la notificación y las condiciones de prestación de servicios. El gráfico describe registros del INE, no una medida directa de incidencia de enfermedades.
Justificación. Comparamos los meses de los siete años para identificar posibles aumentos recurrentes y evaluar si existe un patrón estacional. No basta con identificar el mes con mayor acumulado: necesitamos comprobar si los picos se repiten entre años.
# Contamos los egresos por año y mes para comparar patrones.
# Si no hay registros en un mes, lo dejamos sin valor en vez de asumir cero.
mensual <- base |> dplyr::filter(mes %in% 1:12) |>
dplyr::count(anio, mes, name="egresos") |>
tidyr::complete(anio=2018:2024, mes=1:12)
g2 <- ggplot(mensual, aes(x=mes, y=egresos, group=factor(anio), color=factor(anio))) +
geom_line(linewidth=.85) + geom_point(size=1.3) +
scale_x_continuous(breaks=1:12, labels=c("Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic")) +
scale_y_continuous(labels=scales::label_comma()) +
labs(title="Distribución mensual de egresos", subtitle="Comparación descriptiva entre años", x="Mes", y="Egresos", color="Año")
print(g2)
# Identificamos el mes con más registros de cada año para comprobar si los máximos se repiten.
meses_nombre <- c("enero", "febrero", "marzo", "abril", "mayo", "junio",
"julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre")
picos_mensuales <- mensual |>
dplyr::group_by(anio) |>
dplyr::filter(!is.na(egresos)) |>
dplyr::slice_max(egresos, n=1, with_ties=FALSE) |>
dplyr::ungroup() |>
dplyr::mutate(mes_nombre=meses_nombre[mes])
faltantes_mensuales <- mensual |> dplyr::filter(is.na(egresos)) |> dplyr::count(anio, name="meses_sin_registros")
if (nrow(faltantes_mensuales)>0) {
knitr::kable(faltantes_mensuales, caption="Años con meses sin registros: revisar integridad antes de interpretar tendencias")
}
knitr::kable(picos_mensuales |>
dplyr::transmute(`Año`=anio, `Mes con más egresos`=mes_nombre, `Egresos del mes`=egresos),
caption="Mes con mayor cantidad de egresos en cada año")
| Año | Mes con más egresos | Egresos del mes |
|---|---|---|
| 2018 | noviembre | 22658 |
| 2019 | mayo | 25310 |
| 2020 | enero | 25762 |
| 2021 | mayo | 22773 |
| 2022 | enero | 20641 |
| 2023 | junio | 34196 |
| 2024 | enero | 36256 |
meses_distintos <- dplyr::n_distinct(picos_mensuales$mes)
mes_frecuente <- picos_mensuales |>
dplyr::count(mes_nombre, sort=TRUE) |>
dplyr::slice(1)
Interpretación. Al contrastar las siete curvas, observamos que el mes de mayor registro no es necesariamente el mismo cada año: los máximos se distribuyen entre 4 meses distintos. El mes que más veces aparece como máximo es enero, en 3 de los siete años. Por ello, aunque existen fluctuaciones mensuales, no podemos afirmar una estacionalidad constante únicamente a partir de estas curvas. También destaca que 2023 y 2024 presentan niveles generales superiores a 2022. Los doce meses aparecen representados en cada año, aunque esto por sí solo no confirma la integridad de la notificación. Para concluir que existe estacionalidad harían falta patrones repetidos y estabilidad en la cobertura del registro.
Justificación. Elaboramos un ranking de diagnósticos para identificar cuáles concentran la mayor cantidad de hospitalizaciones registradas. Es una primera forma de priorizar problemas por frecuencia.
# Resumimos cuántos egresos y días de estancia corresponde a cada diagnóstico.
por_diagnostico <- base |>
dplyr::group_by(codigo, diagnostico) |>
dplyr::summarise(egresos=dplyr::n(), dias=sum(estancia_valida, na.rm=TRUE),
registros_con_estancia=sum(!is.na(estancia_valida)),
estancia_media=ifelse(registros_con_estancia>0, dias/registros_con_estancia, NA_real_),
.groups="drop") |>
dplyr::mutate(pct_egresos=100*egresos/sum(egresos))
# Presentar el código junto con el nombre evita confusiones entre diagnósticos similares.
top_egresos <- por_diagnostico |> dplyr::slice_max(egresos, n=params$top_n, with_ties=FALSE) |>
dplyr::mutate(etiqueta=paste0(codigo, " · ", stringr::str_trunc(diagnostico, 65)))
knitr::kable(top_egresos[,c("codigo","diagnostico","egresos","pct_egresos","dias","estancia_media")], digits=2)
| codigo | diagnostico | egresos | pct_egresos | dias | estancia_media |
|---|---|---|---|---|---|
| N189 | Enfermedad renal crónica, no especificada | 197860 | 10.40 | 208504 | 1.05 |
| Z380 | Producto único, nacido en hospital | 134118 | 7.05 | 228275 | 1.71 |
| O829 | Parto por cesárea, sin otra especificación | 129917 | 6.83 | 238325 | 1.85 |
| N185 | Enfermedad renal crónica, etapa 5 | 57572 | 3.03 | 57579 | 1.00 |
| A099 | Gastroenteritis y colitis de origen no especificado | 46594 | 2.45 | 79784 | 1.75 |
| I120 | Enfermedad renal hipertensiva con insuficiencia renal | 43293 | 2.28 | 43317 | 1.00 |
| E142 | Diabetes mellitus no especificada, con complicaciones renales | 38786 | 2.04 | 39038 | 1.01 |
| N390 | Infección de vías urinarias, sitio no especificado | 25737 | 1.35 | 59816 | 2.36 |
| H269 | Catarata, no especificada | 25153 | 1.32 | 26449 | 1.07 |
| J189 | Neumonía, no especificada | 23909 | 1.26 | 67491 | 2.86 |
| K358 | Otras apendicitis agudas, y las no especificadas | 21808 | 1.15 | 37668 | 1.75 |
| O809 | Parto único espontáneo, sin otra especificación | 21084 | 1.11 | 29756 | 1.43 |
# Dibujamos los diagnósticos más frecuentes.
g3 <- ggplot(top_egresos, aes(x=reorder(etiqueta, egresos), y=egresos)) +
geom_col(fill="#176B8A") + coord_flip() +
scale_y_continuous(labels=scales::label_comma()) +
labs(title="Diagnósticos con más egresos", x=NULL, y="Egresos")
print(g3)
Interpretación. El código más frecuente fue N189 (Enfermedad renal crónica, no especificada), con 197,860 egresos (10.4 % del total). Esto señala volumen de atención, pero no necesariamente mayor gravedad clínica.
Justificación. Contar egresos no basta para comprender la utilización hospitalaria. También comparamos los días acumulados de estancia, ya que diagnósticos menos frecuentes podrían requerir hospitalizaciones más largas.
# Ordenamos diagnósticos por días totales de estancia válida.
top_dias <- por_diagnostico |> dplyr::slice_max(dias, n=params$top_n, with_ties=FALSE) |>
dplyr::mutate(etiqueta=paste0(codigo, " · ", stringr::str_trunc(diagnostico, 65)))
g4 <- ggplot(top_dias, aes(x=reorder(etiqueta, dias), y=dias)) +
geom_col(fill="#C07739") + coord_flip() +
scale_y_continuous(labels=scales::label_comma()) +
labs(title="Diagnósticos con más días acumulados de estancia", subtitle="Excluye 9999 = ignorado; revisar valores extremos", x=NULL, y="Días de estancia")
print(g4)
Interpretación. El diagnóstico con más días acumulados fue O829 (Parto por cesárea, sin otra especificación), con 238,325 días registrados. Conviene compararlo con la gráfica anterior: mayor duración no demuestra ineficiencia, pues depende de las necesidades clínicas.
Justificación. Calculamos el porcentaje acumulado de egresos para conocer si una cantidad relativamente pequeña de diagnósticos concentra gran parte de las hospitalizaciones.
# Ordenamos diagnósticos y acumulamos su porcentaje de egresos.
pareto <- por_diagnostico |> dplyr::arrange(dplyr::desc(egresos)) |>
dplyr::mutate(orden=dplyr::row_number(), acumulado=100*cumsum(egresos)/sum(egresos))
cantidad_80 <- min(which(pareto$acumulado >= 80))
cat("Diagnósticos necesarios para alcanzar al menos 80% de egresos:", cantidad_80, "de", nrow(pareto), "\n")
## Diagnósticos necesarios para alcanzar al menos 80% de egresos: 164 de 5061
pareto_plot <- head(pareto, max(params$top_n, 20))
g5 <- ggplot(pareto_plot, aes(x=orden, y=acumulado)) +
geom_line(color="#176B8A", linewidth=1) + geom_point(color="#176B8A", size=1.8) +
geom_hline(yintercept=80, linetype="dashed", color="#B4514D") +
scale_y_continuous(limits=c(0,100), breaks=seq(0,100,20), labels=function(x) paste0(x,"%")) +
labs(title="Pareto: concentración de egresos por diagnóstico", subtitle=paste0("Primeros ",nrow(pareto_plot)," diagnósticos ordenados por frecuencia"), x="Diagnóstico (posición en ranking)", y="Porcentaje acumulado de egresos")
print(g5)
Interpretación. Se necesitan 164 diagnósticos de 5,061 para alcanzar por lo menos el 80 % de los egresos. Este cálculo ayuda a dimensionar la concentración; no implica que el resto de diagnósticos no sea importante.
Justificación. Agrupamos los códigos en categorías amplias para simplificar la lectura y encontrar áreas relevantes para la planificación. Las categorías de embarazo, nacimiento y contacto con servicios no deben interpretarse como enfermedades.
# Contamos los egresos dentro de los grupos CIE-10.
por_grupo <- base |>
dplyr::group_by(grupo_cie) |>
dplyr::summarise(egresos=dplyr::n(), dias=sum(estancia_valida, na.rm=TRUE),
validos=sum(!is.na(estancia_valida)),
estancia_media=ifelse(validos>0, dias/validos, NA_real_), .groups="drop") |>
dplyr::arrange(dplyr::desc(egresos))
knitr::kable(por_grupo, digits=2)
| grupo_cie | egresos | dias | validos | estancia_media |
|---|---|---|---|---|
| Sistema genitourinario | 418289 | 543892 | 416239 | 1.31 |
| Factores de contacto / servicios de salud | 201459 | 330704 | 198860 | 1.66 |
| Embarazo, parto y puerperio | 190077 | 327400 | 188116 | 1.74 |
| Sistema digestivo | 188600 | 350231 | 185365 | 1.89 |
| Endocrinas, nutricionales y metabólicas | 121483 | 173369 | 120843 | 1.43 |
| Sistema circulatorio | 114508 | 192562 | 113615 | 1.69 |
| Lesiones e intoxicaciones | 107641 | 185793 | 105090 | 1.77 |
| Infecciosas y parasitarias | 99063 | 191145 | 97371 | 1.96 |
| Síntomas y hallazgos | 94985 | 166411 | 91172 | 1.83 |
| Sistema respiratorio | 86153 | 192094 | 84710 | 2.27 |
| Ojo y oído | 58933 | 65129 | 57879 | 1.13 |
| Tumores malignos | 53221 | 218404 | 51089 | 4.27 |
| Sistema osteomuscular | 42273 | 76189 | 41180 | 1.85 |
| Tumores benignos / sangre / inmunidad | 39402 | 84578 | 38714 | 2.18 |
| Malformaciones congénitas | 28021 | 36277 | 27885 | 1.30 |
| Otros / revisar código | 15309 | 46655 | 14770 | 3.16 |
| Piel y tejido subcutáneo | 15128 | 35133 | 14848 | 2.37 |
| Trastornos mentales | 12378 | 24783 | 12110 | 2.05 |
| Sistema nervioso | 9331 | 23501 | 9184 | 2.56 |
| Afecciones perinatales | 6601 | 16930 | 6549 | 2.59 |
g6 <- por_grupo |> dplyr::slice_max(egresos, n=12, with_ties=FALSE) |>
ggplot(aes(x=reorder(grupo_cie, egresos), y=egresos)) +
geom_col(fill="#176B8A") + coord_flip() +
scale_y_continuous(labels=scales::label_comma()) +
labs(title="Principales grupos CIE-10 por volumen de egresos", x=NULL, y="Egresos")
print(g6)
Interpretación. El grupo con más egresos fue Sistema genitourinario, con 418,289 registros (22.0 % del total). Se trata de un agrupamiento exploratorio por letra inicial del código, no de una clasificación clínica de necesidades o costos.
Nota clínica: los grupos O, P y Z incluyen partos, condiciones perinatales y motivos de contacto con servicios; no representan necesariamente “enfermedades”. Si se construye una matriz de intervención para enfermedades crónicas, debe definirse explícitamente qué grupos se incluyen y evitar duplicar diagnósticos con varias condiciones.
Justificación. Analizamos las edades y el sexo para identificar diferencias en el perfil de las hospitalizaciones y orientar futuras preguntas de investigación.
# Calculamos los egresos por sexo y grupo etario.
perfil_sexo <- base |> dplyr::count(sexo, name="egresos")
knitr::kable(perfil_sexo)
| sexo | egresos |
|---|---|
| Hombre | 860676 |
| Mujer | 1035485 |
| Sin clasificar | 6694 |
perfil_edad <- base |> dplyr::filter(!is.na(grupo_edad)) |>
dplyr::count(grupo_edad, sexo, name="egresos")
g7 <- ggplot(perfil_edad, aes(x=grupo_edad, y=egresos, fill=sexo)) +
geom_col(position="dodge") + scale_y_continuous(labels=scales::label_comma()) +
labs(title="Egresos según edad y sexo", subtitle="Edad armonizada según unidad registrada", x="Grupo de edad", y="Egresos", fill="Sexo")
print(g7)
Interpretación. El grupo etario con más egresos interpretables fue 30–44. Las categorías sin edad interpretable no se muestran; los totales corresponden a egresos y no a pacientes únicos.
Justificación. Comparamos la procedencia de los pacientes para reconocer concentraciones territoriales de los egresos registrados. La residencia no equivale a la ubicación del hospital.
# Contamos egresos por departamento y añadimos su nombre oficial desde el diccionario.
por_departamento <- base |>
dplyr::count(departamento_codigo, name="egresos", sort=TRUE) |>
dplyr::left_join(departamentos, by=c("departamento_codigo"="codigo")) |>
dplyr::mutate(departamento=dplyr::coalesce(departamento,
paste0("Código no identificado: ", departamento_codigo)))
# Mostramos los 15 departamentos con mayor cantidad de egresos y sus cifras al final de cada barra.
top_departamentos <- por_departamento |>
dplyr::slice_max(egresos, n=15, with_ties=FALSE)
g8 <- ggplot(top_departamentos, aes(x=reorder(departamento, egresos), y=egresos)) +
geom_col(fill="#278479", width=.72) +
geom_text(aes(label=scales::comma(egresos)), hjust=-.12, size=3.4) +
coord_flip(clip="off") +
scale_y_continuous(labels=scales::label_comma(), expand=expansion(mult=c(0,.22))) +
labs(title="Principales departamentos de residencia",
subtitle="Egresos hospitalarios registrados, 2018–2024",
x=NULL, y="Número de egresos",
caption="Fuente: INE. Departamento de residencia, no ubicación del hospital") +
theme(plot.margin=margin(6,25,6,6))
print(g8)
Interpretación. Guatemala concentra el mayor número de egresos registrados, con 976,017 (51.3 % del total). Esto refleja la procedencia de los pacientes que fueron atendidos en el sector privado informante, no la ubicación de los hospitales ni una tasa de enfermedad por departamento. El resultado no está ajustado por población, acceso económico ni cobertura de establecimientos.
Justificación. Reemplazamos la matriz de puntos porque era difícil identificar cuáles diagnósticos destacaban y qué decisión apoyaban. Esta visualización compara dos porcentajes para cada grupo CIE-10: su participación en egresos y su participación en días acumulados de estancia. Esto permite encontrar grupos que concentran una mayor proporción de días que de egresos, lo que resulta útil para seleccionar áreas que merecen análisis operativo adicional.
# Usamos los días de estancia válidos para comparar proporciones entre grupos.
# Elegimos los 10 grupos con más días, mostrando ambas medidas con la misma escala porcentual.
comparacion_grupos <- por_grupo |>
dplyr::mutate(
porcentaje_egresos=100*egresos/sum(egresos),
porcentaje_dias=100*dias/sum(dias),
diferencia_pp=porcentaje_dias-porcentaje_egresos
)
top_grupos_dias <- comparacion_grupos |>
dplyr::slice_max(dias, n=10, with_ties=FALSE) |>
dplyr::arrange(dias)
orden_grupos <- top_grupos_dias$grupo_cie
# Formato largo: por cada grupo mostramos una barra de egresos y otra de días de estancia.
g9_datos <- top_grupos_dias |>
dplyr::select(grupo_cie, porcentaje_egresos, porcentaje_dias) |>
tidyr::pivot_longer(cols=c(porcentaje_egresos, porcentaje_dias),
names_to="indicador", values_to="porcentaje") |>
dplyr::mutate(indicador=dplyr::recode(indicador,
porcentaje_egresos="Participación en egresos",
porcentaje_dias="Participación en días de estancia"),
grupo_cie=factor(grupo_cie, levels=orden_grupos))
g9 <- ggplot(g9_datos, aes(x=grupo_cie, y=porcentaje, fill=indicador)) +
geom_col(position=position_dodge(width=.75), width=.65) +
geom_text(aes(label=paste0(sprintf("%.1f",porcentaje)," %")),
position=position_dodge(width=.75), hjust=-.08, size=3) +
coord_flip(clip="off") +
scale_y_continuous(labels=function(x) paste0(x," %"), expand=expansion(mult=c(0,.25))) +
scale_fill_manual(values=c("Participación en egresos"="#176B8A",
"Participación en días de estancia"="#C07739")) +
labs(title="¿Qué grupos concentran más días de hospitalización?",
subtitle="Participación en egresos frente a participación en días de estancia · 2018–2024",
x=NULL, y="Porcentaje sobre el total del período", fill=NULL,
caption="Fuente: INE. Días según estancias válidas; incluye categorías no patológicas (O, P y Z)") +
theme(legend.position="top", plot.margin=margin(6,28,6,6))
print(g9)
Interpretación. Entre los diez grupos mostrados, Tumores malignos presenta la mayor diferencia positiva: representa 2.8 % de los egresos y 6.7 % de los días registrados, una diferencia de +3.9 puntos porcentuales. Esto indica una concentración relativa mayor de días de estancia, no que el tratamiento sea ineficiente. Para priorizar una intervención habría que revisar la mezcla de diagnósticos, complejidad clínica y calidad de los registros. Los porcentajes usan como denominador todos los egresos y todos los días válidos del período, respectivamente.
Este reporte muestra el proceso completo dentro del HTML. No se guardan archivos CSV, Excel, RDS ni PNG, y tampoco se crea una carpeta de resultados.
9999 se considera ignorado;
las estancias extremas requieren revisión. Comparar el promedio con y
sin estancias superiores a 365 días.Próximo paso de investigación: revisar los 2–3 grupos que destaquen en egresos, días y tendencia; revisar literatura clínica sobre intervenciones relacionadas y evaluar costos/beneficios como escenarios explícitamente hipotéticos.