1 Objetivo y alcance

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.

2 ETAPA 1. Carga y revisión de las bases de datos

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

3 ETAPA 2. Limpieza y homologación de datos

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")
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.

4 ETAPA 3. Revisión de calidad de los datos

¿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")
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)")
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")
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")
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

5 ETAPA 4. Construcción e interpretación de visualizaciones

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.

5.1 Gráfica 1. Evolución anual

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.

5.2 Gráfica 2. Comportamiento mensual

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")
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.

5.3 Gráfica 3. Diagnósticos con más egresos

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.

5.4 Gráfica 4. Diagnósticos con más días de estancia

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.

5.5 Gráfica 5. Pareto de diagnósticos

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.

5.6 Gráfica 6. Grupos de diagnósticos CIE-10

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.

5.7 Gráfica 7. Distribución por edad y sexo

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.

5.8 Gráfica 8. Distribución por departamento de residencia

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.

5.9 Gráfica 9. ¿Qué grupos concentran más días de hospitalización en relación con sus egresos?

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.

6 ETAPA 5. Conclusiones y límites del análisis

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.

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.