Contexto del estudio: Este análisis explora las brechas en estado de salud, acceso a servicios y gasto de bolsillo entre el régimen contributivo y subsidiado en el Caribe colombiano, usando datos de la ECV 2024 (DANE). Es el análisis descriptivo y exploratorio previo a la estimación econométrica del costo esperado en cáncer de mama.

Modelo econométrico objetivo: \(Costos_i = \beta_0 + \beta_1 Regimen_i + \beta_2 Estadio_i + \beta_3 Ruralidad_i + \beta_4 Edad_i + \varepsilon_i\)


1 Carga de datos

ruta_base <- file.path(
  Sys.getenv("OneDrive"),
  "OCSA 2026",
  "ANALISIS ECO. SALUD",
  "salud2024"
)

ruta_salud <- file.path(ruta_base, "salud.CSV")
ruta_compo <- file.path(ruta_base, "caracteristicas_composicion_hogar.CSV")

cat("¿Existe módulo Salud?      ", file.exists(ruta_salud), "\n")
## ¿Existe módulo Salud?       TRUE
cat("¿Existe módulo Composición?", file.exists(ruta_compo), "\n")
## ¿Existe módulo Composición? TRUE
salud <- read_delim(
  ruta_salud,
  delim  = ";",
  locale = locale(encoding = "latin1"),
  show_col_types = FALSE
)

composicion <- read_delim(
  ruta_compo,
  delim  = ";",
  locale = locale(encoding = "latin1"),
  show_col_types = FALSE
)

cat("Módulo Salud:       ", nrow(salud),      "filas x", ncol(salud),      "columnas\n")
## Módulo Salud:        228688 filas x 157 columnas
cat("Módulo Composicion: ", nrow(composicion), "filas x", ncol(composicion), "columnas\n")
## Módulo Composicion:  228688 filas x 78 columnas

2 Merge y construcción de la base

keys <- c("DIRECTORIO", "SECUENCIA_ENCUESTA", "SECUENCIA_P", "ORDEN")

vars_compo <- c(
  keys,
  "P6020",  # sexo al nacer
  "P6040",  # edad en años
  "P6080",  # autorreconocimiento etnico
  "P1897",  # satisfaccion con la salud (0-10)
  "P754",   # ruralidad
  "FEX_C"   # factor de expansion
)

df <- salud %>%
  left_join(
    composicion %>% dplyr::select(all_of(vars_compo)),
    by = keys
  )

cat("Base combinada: ", nrow(df), "filas x", ncol(df), "columnas\n")
## Base combinada:  228688 filas x 163 columnas

3 Limpieza y variables

df_clean <- df %>%
  filter(
    P6020 == 2,
    P6100 %in% c(1, 3),
    P6040 >= 18,
    P6040 <= 90
  ) %>%
  mutate(
    salud_ord = factor(P6127,
                       levels = 1:4,
                       labels = c("Muy bueno", "Bueno", "Regular", "Malo"),
                       ordered = TRUE),

    satisfaccion = ifelse(P1897 %in% c(99, NA), NA_real_, P1897),

    regimen = factor(ifelse(P6100 == 1, "Contributivo", "Subsidiado"),
                     levels = c("Subsidiado", "Contributivo")),
    regimen_bin = ifelse(P6100 == 1, 1, 0),

    edad  = P6040,
    edad2 = (P6040 - mean(P6040, na.rm = TRUE))^2,

    rural = factor(ifelse(P754 == 2, "Rural", "Urbano"),
                   levels = c("Urbano", "Rural")),
    rural_bin = ifelse(P754 == 2, 1, 0),

    afro = factor(ifelse(P6080 == 5, "Afrodescendiente", "No afrodescendiente"),
                  levels = c("No afrodescendiente", "Afrodescendiente")),
    afro_bin = ifelse(P6080 == 5, 1, 0),

    cronica = factor(ifelse(P1930 == 1, "Con cronica", "Sin cronica"),
                     levels = c("Sin cronica", "Con cronica")),
    cronica_bin = ifelse(P1930 == 1, 1, 0),

    barrera = ifelse(!is.na(P6153), 1, 0),

    razon_barrera = case_when(
      P6153 == 1  ~ "Caso leve",
      P6153 == 3  ~ "Centro queda lejos",
      P6153 == 4  ~ "Falta de dinero",
      P6153 == 5  ~ "Mal servicio / cita lejana",
      P6153 == 10 ~ "No autorizaron atencion",
      P6153 == 11 ~ "Espera excesiva",
      !is.na(P6153) ~ "Otra razon",
      TRUE ~ NA_character_
    ),

    gasto_medicamentos = ifelse(is.na(P3182S1), 0, P3182S1),
    gasto_transporte   = ifelse(is.na(P3186S1), 0, P3186S1),
    gasto_total        = gasto_medicamentos + gasto_transporte,

    peso = FEX_C.x
  )

cat("Muestra final:", nrow(df_clean), "\n")
## Muestra final: 83983
cat("  Contributivo:", sum(df_clean$regimen == "Contributivo"), "\n")
##   Contributivo: 24287
cat("  Subsidiado:  ", sum(df_clean$regimen == "Subsidiado"),   "\n")
##   Subsidiado:   59696

4 Estadísticas descriptivas

resumen <- df_clean %>%
  group_by(regimen) %>%
  summarise(
    N                        = n(),
    `Edad media`             = round(mean(edad, na.rm = TRUE), 1),
    `% Rural`                = round(mean(rural_bin, na.rm = TRUE) * 100, 1),
    `% Afrodescendiente`     = round(mean(afro_bin, na.rm = TRUE) * 100, 1),
    `% Con cronica`          = round(mean(cronica_bin, na.rm = TRUE) * 100, 1),
    `% Barrera acceso`       = round(mean(barrera, na.rm = TRUE) * 100, 1),
    `Satisfaccion media`     = round(mean(satisfaccion, na.rm = TRUE), 2),
    `Gasto bolsillo mediana` = round(median(gasto_total[gasto_total > 0],
                                            na.rm = TRUE), 0)
  )

resumen %>%
  kable(caption = "Tabla 1. Estadisticas descriptivas por regimen — ECV 2024") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE) %>%
  column_spec(1, bold = TRUE)
Tabla 1. Estadisticas descriptivas por regimen — ECV 2024
regimen N Edad media % Rural % Afrodescendiente % Con cronica % Barrera acceso Satisfaccion media Gasto bolsillo mediana
Subsidiado 59696 44.8 39.1 9.5 17.6 1.3 7.77 30000
Contributivo 24287 46.5 19.9 4.5 18.2 1.1 8.13 25000

5 Por que Ordered Logit

5.1 Justificacion

La variable de resultado es el estado de salud autopercibido con cuatro categorias ordenadas:

\[\text{Muy bueno} < \text{Bueno} < \text{Regular} < \text{Malo}\]

El Ordered Logit modela la probabilidad acumulada de estar en cada categoria o peor:

\[\log\left(\frac{P(Y \leq j)}{P(Y > j)}\right) = \alpha_j - \mathbf{X}\boldsymbol{\beta}\]

  • OR > 1: mayor probabilidad de peor salud
  • OR < 1: mayor probabilidad de mejor salud

5.2 Comparacion de opciones

Modelo Variable dependiente Correcto aqui?
MCO lineal Continua No — salud es ordinal
Logit binario 0/1 No — pierde informacion
Ordered Logit Ordinal k categorias Si
MCO Escala 0-10 continua Si — para satisfaccion

6 Estimacion de modelos

# Modelo 0: solo regimen
m0 <- polr(salud_ord ~ regimen,
           data = df_clean %>% filter(!is.na(salud_ord)),
           Hess = TRUE, method = "logistic")

# Modelo 1: con controles
m1 <- polr(salud_ord ~ regimen + edad + edad2 + rural + afro + cronica,
           data = df_clean %>% filter(!is.na(salud_ord), !is.na(rural)),
           Hess = TRUE, method = "logistic")

# Modelo 2: con barrera de acceso
m2 <- polr(salud_ord ~ regimen + edad + edad2 + rural + afro + cronica + barrera,
           data = df_clean %>% filter(!is.na(salud_ord), !is.na(rural)),
           Hess = TRUE, method = "logistic")

# Modelo 3: MCO satisfaccion
m3 <- lm(satisfaccion ~ regimen + edad + edad2 + rural + afro + cronica,
         data = df_clean, weights = peso)

cat("Modelos estimados correctamente.\n")
## Modelos estimados correctamente.
modelsummary(
  list("M0 Solo regimen" = m0,
       "M1 Con controles" = m1,
       "M2 Barrera acceso" = m2),
  exponentiate = TRUE,
  stars        = TRUE,
  title        = "Tabla 2. Ordered Logit — Estado de salud autopercibido (OR)",
  notes        = "OR > 1: mayor probabilidad de peor salud. Fuente: DANE ECV 2024.",
  coef_rename  = c(
    "regimenContributivo"     = "Regimen contributivo",
    "edad"                    = "Edad (anos)",
    "edad2"                   = "Edad2 (centrada)",
    "ruralRural"              = "Zona rural",
    "afroAfrodescendiente"    = "Afrodescendiente",
    "cronicaCon cronica"      = "Enfermedad cronica",
    "barrera"                 = "Barrera de acceso"
  )
)
Tabla 2. Ordered Logit — Estado de salud autopercibido (OR)
M0 Solo regimen M1 Con controles M2 Barrera acceso
+ p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001
OR > 1: mayor probabilidad de peor salud. Fuente: DANE ECV 2024.
Muy bueno|Bueno 0.128*** 0.529*** 0.540***
(0.002) (0.026) (0.026)
Bueno|Regular 3.941*** 21.814*** 22.632***
(0.038) (1.220) (1.272)
Regular|Malo 72.729*** 574.287*** 609.935***
(2.272) (49.182) (52.613)
Regimen contributivo 0.742*** 0.668*** 0.674***
(0.012) (0.023) (0.023)
Edad (anos) 1.032*** 1.032***
(0.001) (0.001)
Edad2 (centrada) 1.000 1.000
(0.000) (0.000)
Zona rural 1.109** 1.113**
(0.039) (0.039)
Afrodescendiente 0.832* 0.843*
(0.064) (0.065)
Enfermedad cronica 5.296*** 5.275***
(0.232) (0.232)
Barrera de acceso 3.516***
(0.404)
Num.Obs. 83983 17786 17786
AIC 147069.8 29379.6 29266.0
BIC 147107.1 29449.7 29343.9
RMSE 1.94 1.98 1.98
coefs <- coef(m1)
zetas <- m1$zeta
betas <- coefs[!names(coefs) %in% names(zetas)]
vcov_betas <- vcov(m1)[names(betas), names(betas)]

OR_tabla <- data.frame(
  Variable = names(betas),
  OR       = round(exp(betas), 3),
  IC_inf   = round(exp(betas - 1.96 * sqrt(diag(vcov_betas))), 3),
  IC_sup   = round(exp(betas + 1.96 * sqrt(diag(vcov_betas))), 3),
  row.names = NULL
)

OR_tabla %>%
  kable(caption = "Tabla 3. Odds Ratios — Modelo M1 con IC 95%",
        col.names = c("Variable", "OR", "IC 2.5%", "IC 97.5%")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  row_spec(which(OR_tabla$IC_inf > 1 | OR_tabla$IC_sup < 1),
           bold = TRUE, color = "#D85A30")
Tabla 3. Odds Ratios — Modelo M1 con IC 95%
Variable OR IC 2.5% IC 97.5%
regimenContributivo 0.668 0.624 0.715
edad 1.032 1.030 1.034
edad2 1.000 1.000 1.000
ruralRural 1.109 1.036 1.187
afroAfrodescendiente 0.832 0.715 0.968
cronicaCon cronica 5.296 4.860 5.770
modelsummary(
  list("M3 MCO Satisfaccion" = m3),
  stars = TRUE,
  title = "Tabla 4. MCO — Satisfaccion con la salud (escala 0-10)",
  notes = "Coeficientes: cambio en puntos de satisfaccion. Fuente: DANE ECV 2024.",
  coef_rename = c(
    "regimenContributivo"     = "Regimen contributivo",
    "edad"                    = "Edad (anos)",
    "edad2"                   = "Edad2 (centrada)",
    "ruralRural"              = "Zona rural",
    "afroAfrodescendiente"    = "Afrodescendiente",
    "cronicaCon cronica"      = "Enfermedad cronica"
  )
)
Tabla 4. MCO — Satisfaccion con la salud (escala 0-10)
M3 MCO Satisfaccion
+ p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001
Coeficientes: cambio en puntos de satisfaccion. Fuente: DANE ECV 2024.
(Intercept) 8.950***
(0.039)
Regimen contributivo 0.513***
(0.027)
Edad (anos) -0.023***
(0.001)
Edad2 (centrada) -0.000
(0.000)
Zona rural 0.056
(0.038)
Afrodescendiente -0.389***
(0.063)
Enfermedad cronica -1.280***
(0.037)
Num.Obs. 17786
R2 0.155
R2 Adj. 0.154
AIC 85165.4
BIC 85227.7
Log.Lik. -42574.688
F 542.549
RMSE 1.78

7 Probabilidades predichas

perfil_base <- data.frame(
  regimen = factor(c("Subsidiado", "Contributivo"),
                   levels = c("Subsidiado", "Contributivo")),
  edad    = mean(df_clean$edad, na.rm = TRUE),
  edad2   = mean(df_clean$edad2, na.rm = TRUE),
  rural   = factor("Urbano", levels = c("Urbano", "Rural")),
  afro    = factor("No afrodescendiente",
                   levels = c("No afrodescendiente", "Afrodescendiente")),
  cronica = factor("Sin cronica", levels = c("Sin cronica", "Con cronica"))
)

probs <- MASS:::predict.polr(m1, newdata = perfil_base, type = "probs")

data.frame(
  Regimen    = c("Subsidiado", "Contributivo"),
  Muy_bueno  = round(probs[, 1] * 100, 1),
  Bueno      = round(probs[, 2] * 100, 1),
  Regular    = round(probs[, 3] * 100, 1),
  Malo       = round(probs[, 4] * 100, 1)
) %>%
  kable(caption = "Tabla 5. Probabilidades predichas (%) por regimen",
        col.names = c("Regimen", "Muy bueno", "Bueno", "Regular", "Malo")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  column_spec(1, bold = TRUE) %>%
  add_header_above(c(" " = 1, "Estado de salud autopercibido (%)" = 4))
Tabla 5. Probabilidades predichas (%) por regimen
Estado de salud autopercibido (%)
Regimen Muy bueno Bueno Regular Malo
Subsidiado 11.3 72.7 15.3 0.7
Contributivo 16.0 72.7 10.8 0.5

8 Visualizaciones

8.1 Grafico 1 — Estado de salud

df_clean %>%
  filter(!is.na(salud_ord)) %>%
  count(regimen, salud_ord) %>%
  group_by(regimen) %>%
  mutate(pct = n / sum(n) * 100) %>%
  ggplot(aes(x = salud_ord, y = pct, fill = regimen)) +
  geom_col(position = "dodge", width = 0.7) +
  geom_text(aes(label = paste0(round(pct, 1), "%")),
            position = position_dodge(width = 0.7),
            vjust = -0.4, size = 3.2, fontface = "bold") +
  scale_fill_manual(values = colores) +
  scale_y_continuous(labels = label_percent(scale = 1), limits = c(0, 82)) +
  labs(title    = "Estado de salud autopercibido por regimen",
       subtitle = "Mujeres mayores de 18 anos — ECV 2024",
       x = NULL, y = "Porcentaje (%)", fill = "Regimen") +
  tema_obs

8.2 Grafico 2 — Satisfaccion

df_clean %>%
  filter(!is.na(satisfaccion)) %>%
  ggplot(aes(x = regimen, y = satisfaccion, fill = regimen)) +
  geom_violin(alpha = 0.35, color = NA) +
  geom_boxplot(width = 0.2, outlier.shape = NA, color = "#333333") +
  stat_summary(fun = mean, geom = "point",
               shape = 23, size = 3.5, fill = "white", color = "#333333") +
  scale_fill_manual(values = colores) +
  scale_y_continuous(breaks = 0:10) +
  labs(title    = "Satisfaccion con la salud por regimen",
       subtitle = "Escala 0-10 · Punto blanco = media",
       x = NULL, y = "Satisfaccion (0-10)", fill = "Regimen") +
  tema_obs + theme(legend.position = "none")

8.3 Grafico 3 — Brecha rural

df_clean %>%
  filter(!is.na(satisfaccion), !is.na(rural)) %>%
  group_by(regimen, rural) %>%
  summarise(media = mean(satisfaccion, na.rm = TRUE), .groups = "drop") %>%
  ggplot(aes(x = rural, y = media, fill = regimen)) +
  geom_col(position = "dodge", width = 0.6) +
  geom_text(aes(label = round(media, 2)),
            position = position_dodge(width = 0.6),
            vjust = -0.5, size = 4, fontface = "bold") +
  scale_fill_manual(values = colores) +
  scale_y_continuous(limits = c(0, 10.5)) +
  labs(title    = "Doble brecha: regimen x ruralidad",
       subtitle = "Ser subsidiado Y rural es peor que cualquiera de los dos por separado",
       x = NULL, y = "Satisfaccion media (0-10)", fill = "Regimen") +
  tema_obs

8.4 Grafico 4 — Barreras

df_clean %>%
  filter(!is.na(razon_barrera)) %>%
  count(regimen, razon_barrera) %>%
  group_by(regimen) %>%
  mutate(pct = n / sum(n) * 100) %>%
  ggplot(aes(x = reorder(razon_barrera, pct), y = pct, fill = regimen)) +
  geom_col(position = "dodge", width = 0.7) +
  geom_text(aes(label = paste0(round(pct, 1), "%")),
            position = position_dodge(width = 0.7),
            hjust = -0.1, size = 3) +
  coord_flip() +
  scale_fill_manual(values = colores) +
  scale_y_continuous(labels = label_percent(scale = 1), limits = c(0, 55)) +
  labs(title    = "Razones de no atencion medica por regimen",
       subtitle = "Solo personas que reportaron no recibir atencion",
       x = NULL, y = "Porcentaje (%)", fill = "Regimen") +
  tema_obs

8.5 Grafico 5 — Forest plot

or_df <- data.frame(
  variable  = names(betas),
  OR        = exp(betas),
  lower     = exp(betas - 1.96 * sqrt(diag(vcov_betas))),
  upper     = exp(betas + 1.96 * sqrt(diag(vcov_betas))),
  row.names = NULL
) %>%
  mutate(
    etiqueta      = recode(variable,
      "regimenContributivo"  = "Regimen contributivo",
      "edad"                 = "Edad (anos)",
      "edad2"                = "Edad2",
      "ruralRural"           = "Zona rural",
      "afroAfrodescendiente" = "Afrodescendiente",
      "cronicaCon cronica"   = "Enfermedad cronica"),
    significativo = ifelse(lower > 1 | upper < 1,
                           "Significativo", "No significativo")
  )

ggplot(or_df, aes(x = OR, y = reorder(etiqueta, OR), color = significativo)) +
  geom_vline(xintercept = 1, linetype = "dashed",
             color = "#888888", linewidth = 0.8) +
  geom_errorbarh(aes(xmin = lower, xmax = upper), height = 0.3, linewidth = 1) +
  geom_point(size = 4) +
  scale_color_manual(values = c("Significativo" = "#D85A30",
                                "No significativo" = "#AAAAAA"),
                     name = NULL) +
  scale_x_log10(breaks = c(0.5, 0.75, 1, 1.5, 2, 3, 5)) +
  labs(title    = "Forest plot — Ordered Logit (Modelo M1)",
       subtitle = "OR > 1: mayor probabilidad de peor salud · Linea = sin efecto",
       x = "Odds Ratio (escala log)", y = NULL) +
  tema_obs + theme(legend.position = "bottom")

8.6 Grafico 6 — Gasto de bolsillo

df_clean %>%
  filter(gasto_total > 0) %>%
  ggplot(aes(x = gasto_total / 1000, fill = regimen)) +
  geom_histogram(bins = 35, alpha = 0.75, position = "identity", color = NA) +
  scale_x_continuous(
    labels = label_dollar(prefix = "$", suffix = "K", big.mark = "."),
    limits = c(0, 300)) +
  scale_fill_manual(values = colores) +
  facet_wrap(~regimen, ncol = 1, scales = "free_y") +
  labs(title    = "Gasto de bolsillo en salud (ultimos 30 dias)",
       subtitle = "Medicamentos + transporte · Excluye ceros",
       x = "Gasto de bolsillo (miles COP)", y = "Frecuencia") +
  tema_obs + theme(legend.position = "none")


9 Sintesis de hallazgos

Modelo OR Regimen Contributivo Interpretacion
M0 Sin controles 0.742 Brecha cruda
M1 Con controles 0.668 Brecha neta
M2 Con barrera 0.674 Brecha controlando mecanismo

10 Conexion con el modelo de costos clinicos

\[\underbrace{\text{Regimen}}_{\beta_1} \rightarrow \underbrace{\text{Acceso}}_{\text{barrera}} \rightarrow \underbrace{\text{Estadio}}_{\beta_2} \rightarrow \underbrace{\text{Costo}}_{\text{Y}}\]

El siguiente paso es obtener los datos clinicos (RIPS + ADRES) para estimar:

\[Costos_i = \beta_0 + \beta_1 Regimen_i + \beta_2 Estadio_i + \beta_3 Ruralidad_i + \beta_4 Edad_i + \varepsilon_i\]


## Analisis: R 4 . 5.3
## Fecha: 24/07/2026
## Observatorio de Economia de la Salud — Uninorte, Barranquilla