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.
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(`Año`=2018:2024, `Archivo de origen`=basename(rutas)))
| Año | 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 se explica por qué se elaboró, se muestra el gráfico y se realiza una interpretación automática basada en las cifras obtenidas. 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. 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.
¿Qué hicimos? Integramos los archivos anuales de
servicios internos privados de 2018 a 2024 en una sola
base. Leímos los códigos como texto y unificamos diferencias en los
nombres de columnas; en particular, la variable de estancia se
denominaba DIASESTAN en 2024 y DIASESTANCIA en
los años anteriores.
¿Por qué? Una base única permite comparar los siete años y evita que un código cambie de significado por una conversión automática.
Después utilizamos el diccionario oficial del INE para traducir
códigos CIE-10 y departamentos. Para los cálculos de
estancia tratamos 9999 como ignorado, sin
borrar el egreso completo; también identificamos valores de edad no
interpretables y códigos que no coincidían con el catálogo.
# Calculamos cifras generales que aparecerán en el resumen del HTML.
# Usamos las tablas ya procesadas: no volvemos a leer ni a modificar la base.
total_egresos <- sum(resumen_anual$egresos)
total_dias <- sum(resumen_anual$dias)
total_estancias_ignoradas <- sum(calidad$estancia_ignorada)
total_sin_cie <- sum(calidad$diagnostico_sin_catalogo)
porcentaje_ignoradas <- 100 * total_estancias_ignoradas / total_egresos
# Esta tabla permite verificar de forma rápida el alcance del análisis.
resumen_proceso <- tibble::tibble(
Indicador = c("Años analizados", "Egresos registrados",
"Días de estancia válidos acumulados",
"Registros sin estancia válida",
"Códigos de diagnóstico sin equivalencia"),
Resultado = c("2018–2024", scales::comma(total_egresos),
scales::comma(total_dias),
paste0(scales::comma(total_estancias_ignoradas),
" (", sprintf("%.2f", porcentaje_ignoradas), " %)"),
scales::comma(total_sin_cie))
)
knitr::kable(resumen_proceso, caption="Resumen de datos utilizados y controles de calidad")
| Indicador | Resultado |
|---|---|
| Años analizados | 2018–2024 |
| Egresos registrados | 1,902,855 |
| Días de estancia válidos acumulados | 3,281,180 |
| Registros sin estancia válida | 27,266 (1.43 %) |
| Códigos de diagnóstico sin equivalencia | 2 |
Resultado. Trabajamos con 1,902,855 egresos. Se conservaron los registros para analizar frecuencias, pero 27,266 (1.43 %) no aportan días de estancia válidos. Encontramos 2 registros cuyos códigos requieren revisar su equivalencia CIE-10. No eliminamos automáticamente registros aparentemente duplicados, porque carecemos de un identificador que permita saber si corresponden al mismo episodio.
¿Qué hicimos? Construimos nueve visualizaciones para responder preguntas diferentes y complementarias:
¿Por qué? Usar varias perspectivas evita depender de un solo indicador. Por ejemplo, el diagnóstico más frecuente no tiene por qué ser el que acumula más días de estancia.
# Calculamos comparaciones con las mismas cifras de las gráficas.
valor_anual <- function(anio_objetivo, variable) {
resumen_anual[[variable]][match(anio_objetivo, resumen_anual$anio)]
}
caida_2019_2022 <- 100 * (valor_anual(2022, "egresos") /
valor_anual(2019, "egresos") - 1)
repunte_2022_2023 <- 100 * (valor_anual(2023, "egresos") /
valor_anual(2022, "egresos") - 1)
variacion_2023_2024 <- 100 * (valor_anual(2024, "egresos") /
valor_anual(2023, "egresos") - 1)
# Tomamos el diagnóstico de mayor frecuencia y el de más días acumulados.
principal_egresos <- por_diagnostico |>
dplyr::slice_max(egresos, n=1, with_ties=FALSE)
principal_dias <- por_diagnostico |>
dplyr::slice_max(dias, n=1, with_ties=FALSE)
# Identificamos el grupo con más días de estancia relativos a sus egresos.
grupo_desproporcion <- comparacion_grupos |>
dplyr::slice_max(diferencia_pp, n=1, with_ties=FALSE)
# Registramos cuál fue el año con más estancias ignoradas.
anio_calidad <- calidad |>
dplyr::slice_max(estancia_ignorada, n=1, with_ties=FALSE)
Los egresos registrados pasaron de 286,085 en 2019 a 194,345 en 2022, una variación de -32.1 %. Luego aumentaron +77.1 % entre 2022 y 2023, y +1.7 % entre 2023 y 2024.
¿Qué significa? El volumen registrado cambió considerablemente. La pandemia es un posible factor contextual para parte de la caída, pero los datos disponibles no permiten identificar su causa; también debemos revisar cambios de cobertura y notificación de los establecimientos privados.
Los meses con más egresos varían entre años: aparecen 4 meses distintos como máximos anuales. El mes que más se repite es enero, presente en 3 de los siete años.
¿Qué significa? Hay fluctuaciones mensuales, pero no se observa un pico idéntico y sostenido cada año. No recomendamos planificar personal o suministros usando un supuesto patrón estacional sin contrastarlo antes con registros del propio hospital.
El diagnóstico con más egresos fue Enfermedad renal crónica, no especificada (N189), con 197,860 egresos. En cambio, el código con más días acumulados de estancia fue Parto por cesárea, sin otra especificación (O829), con 238,325 días.
¿Qué significa? No conviene priorizar servicios exclusivamente por el número de hospitalizaciones: los días de estancia ofrecen una perspectiva complementaria. Además, hay que separar los motivos obstétricos y de nacimiento de los diagnósticos de enfermedad antes de proponer programas clínicos específicos.
En la comparación de grupos CIE-10, Tumores malignos representa 2.8 % de los egresos, pero 6.7 % de los días válidos: una diferencia de +3.9 puntos porcentuales.
¿Qué significa? Este grupo tiene mayor peso relativo en la estancia acumulada que en los egresos. Es una señal útil para investigar más, pero no demuestra estancias innecesarias, saturación ni ineficiencia.
El departamento de residencia con más egresos fue Guatemala, con 976,017 registros. Además, 2022 fue el año con más estancias ignoradas: 10,670 registros.
¿Qué significa? La concentración territorial describe la procedencia de quienes recibieron atención en establecimientos privados informantes; no mide por sí sola necesidades no atendidas. Los valores ignorados también deben tenerse en cuenta al interpretar promedios de estancia.
En conjunto, los hallazgos sirven para seleccionar qué diagnósticos o grupos conviene examinar primero en una red de hospitales privados. Una posible aplicación sería revisar, con datos internos y criterio médico, los servicios que combinan muchos egresos, mayor acumulación de días y cambios sostenidos en su demanda; a partir de eso se podría diseñar un piloto de seguimiento de pacientes o planificación operativa.
No podemos concluir que un hospital necesita un número específico de camas, que exista desperdicio de recursos o que una intervención ahorrará dinero. Para esas decisiones se necesitan datos internos de capacidad, costos, personal, características clínicas y resultados posteriores al alta.
Limitaciones principales: las series pueden reflejar cambios en la cobertura de reporte; cada registro es un egreso, no una persona única; no existe identificación de hospital o paciente para calcular ocupación o reingresos; los días ignorados se excluyen de métricas de estancia y la clasificación CIE-10 mezcla enfermedades con otros motivos de atención.
Conclusión del análisis: los datos del INE permiten identificar qué diagnósticos concentran la utilización hospitalaria registrada, pero la selección de una mejora específica debe completarse con validación clínica y datos operativos de los hospitales. Este estudio proporciona una base cuantitativa para decidir dónde profundizar, no una demostración de eficiencia o impacto financiero.