1. ¿De qué trata este documento?

En una frase: este documento identifica en qué municipios del Estado de México hay más nacimientos de madres adolescentes en relación con su población y revisa si esos municipios son también los que tienen mayores carencias (marginación), para señalar dónde sería más urgente concentrar la atención.

Pregunta de investigación. ¿Qué municipios del Estado de México tienen la mayor tasa de nacimientos de madres adolescentes por habitante y, al mismo tiempo, los mayores niveles de marginación (como aproximación a la vulnerabilidad de la niñez)?

Por qué tasas y no números absolutos. Un municipio muy poblado siempre tendrá más casos que uno pequeño, aunque en el pequeño el problema sea proporcionalmente mayor. Por eso todas las medidas de este documento se calculan contra la población del municipio (nacimientos por cada 1,000 habitantes), y no como conteos brutos.

¿Cómo está organizado?

  1. Sección 2: un panorama general del Estado de México, para conocer el escenario antes de hablar del tema.
  2. Sección 3: los resultados sobre embarazo adolescente, municipio por municipio.
  3. Sección 4: ampliaciones opcionales (tendencia, tasa de fecundidad exacta, pobreza y dashboard).
  4. Secciones 5 a 7: conclusiones, limitaciones y próximos pasos.
  5. Sección 8: anexo técnico con la preparación de los datos.

2. El Estado de México en pocas cifras

Antes de hablar de embarazo adolescente conviene conocer el territorio donde ocurre. Esta sección retrata, a grandes rasgos, a la entidad y a sus 125 municipios: cuánta gente vive, quién concentra qué y qué tan desiguales son las condiciones de vida. Es el escenario de la historia que sigue.

2.1 Un estado grande, con 125 municipios

  • 125 municipios y 16,992,418 habitantes (Censo 2020, según CONAPO): es la entidad federativa más poblada del país.
  • Solo los 10 municipios más poblados concentran el 47% de los habitantes del estado. Los otros 115 municipios se reparten el resto.
  • Se registran en promedio 172,114 nacimientos al año de madres que viven en el estado, es decir, 10.1 nacimientos por cada 1,000 habitantes.
  • 19% de la población vive en localidades de menos de 5,000 habitantes, mientras otros viven en zonas metropolitanas de millones de personas.

2.2 Los 10 municipios con “más de todo”

Aquí no hay una sola lista, sino siete rankings, uno por tema. Cada tema tiene su propia tabla, con una breve explicación de qué mide y cómo leerlo. Cada tarjeta indica arriba su familia: tamaño, natalidad o carencias.

Cómo leerla. Cada tabla es independiente: muestra los 10 municipios con el valor más alto de ese tema, de mayor a menor. No relaciones una tabla con otra por renglón; compáralas solo para ver qué municipios se repiten.

Tamaño

Más población

Habitantes según el Censo 2020.

# Municipio Habitantes
1 Ecatepec de Morelos 1,645,352
2 Nezahualcóyotl 1,077,208
3 Toluca 910,608
4 Naucalpan de Juárez 834,434
5 Chimalhuacán 705,193
6 Tlalnepantla de Baz 672,202
7 Cuautitlán Izcalli 555,163
8 Tecámac 547,503
9 Ixtapaluca 542,211
10 Atizapán de Zaragoza 523,674
Tamaño

Más nacimientos al año

Promedio anual de bebés cuya madre vive en el municipio (conteo, no tasa).

# Municipio Nacimientos
1 Ecatepec de Morelos 14,304
2 Toluca 9,588
3 Nezahualcóyotl 9,061
4 Chimalhuacán 7,908
5 Naucalpan de Juárez 7,616
6 Tlalnepantla de Baz 4,866
7 Ixtapaluca 4,612
8 Chalco 4,312
9 Tultitlán 4,284
10 Atizapán de Zaragoza 4,266
Natalidad

Mayor natalidad

Nacimientos al año por cada 1,000 habitantes. Los municipios chicos pueden subir mucho con pocos nacimientos.

# Municipio Por 1,000
1 Ixtapan del Oro 21.4
2 San José del Rincón 20.5
3 Villa Victoria 18.7
4 Donato Guerra 18.3
5 Coyotepec 16.9
6 Amanalco 16.9
7 San Felipe del Progreso 16.7
8 Zacazonapan 16.5
9 Villa del Carbón 16.3
10 Teoloyucan 16.2
Carencias

Mayor marginación

Índice de marginación de CONAPO. Un valor más bajo significa más marginación.

# Municipio IMN
1 Zacualpan 0.78
2 Villa Victoria 0.79
3 Donato Guerra 0.80
4 San José del Rincón 0.80
5 Luvianos 0.81
6 Sultepec 0.81
7 Morelos 0.81
8 Ixtapan del Oro 0.82
9 Tlatlaya 0.82
10 Zumpahuacán 0.82
Carencias

Más población rural

% de habitantes que viven en localidades de menos de 5,000 personas.

# Municipio % rural
1 Aculco 100%
2 Almoloya de Alquisiras 100%
3 Amanalco 100%
4 Amatepec 100%
5 Ayapango 100%
6 Isidro Fabela 100%
7 Ixtapan del Oro 100%
8 Jilotzingo 100%
9 Morelos 100%
10 Nopaltepec 100%
Carencias

Más ingresos bajos

% de personas ocupadas que ganan hasta 2 salarios mínimos.

# Municipio % ocupados
1 Zumpahuacán 96%
2 Ixtapan del Oro 93%
3 Zacualpan 89%
4 Joquicingo 88%
5 Morelos 88%
6 Malinalco 87%
7 Ozumba 87%
8 Atlautla 86%
9 Villa Guerrero 86%
10 Ocuilan 86%
Carencias

Mayor analfabetismo

% de personas de 15 años o más que no saben leer ni escribir.

# Municipio % analf.
1 Sultepec 14.9%
2 Otzoloapan 14.5%
3 Luvianos 14.2%
4 Tlatlaya 14.2%
5 San José del Rincón 13.5%
6 Amatepec 12.6%
7 Donato Guerra 12.2%
8 Villa Victoria 11.1%
9 San Felipe del Progreso 10.8%
10 Ixtapan del Oro 10.6%

Qué municipios se repiten entre temas

Lo que se ve: 8 municipios aparecen a la vez entre los que tienen más población y más nacimientos (Atizapán de Zaragoza, Chimalhuacán, Ecatepec de Morelos, Ixtapaluca, Naucalpan de Juárez, Nezahualcóyotl); es lógico, porque donde vive más gente nacen más bebés.

Los municipios con más carencias son otros: 11 aparecen en dos o más de los rankings de marginación, ruralidad, ingresos bajos y analfabetismo (Amatepec, Donato Guerra, Ixtapan del Oro, Luvianos, Morelos, San José del Rincón). Tamaño y carencias no coinciden, y esa es la razón de medir con tasas.

2.3 Dónde vive la gente

Cómo leerla. Cada barra es la población de un municipio y, junto a ella, aparece su porcentaje del total del estado. Son los 10 municipios más poblados.

tp <- top10_pob |>
  mutate(pct = 100 * pob_tot / pob_edo, municipio = reorder(municipio, pob_tot))
ggplot(tp, aes(pob_tot, municipio)) +
  geom_col(fill = pal$azul_rey, color = "white", linewidth = .8, width = .75) +
  geom_text(aes(label = paste0(fmt(pob_tot), "  (", sprintf("%.1f", pct), "%)")),
            hjust = -0.05, size = 3.4, color = pal$gris_osc) +
  scale_x_continuous(labels = comma, expand = expansion(mult = c(0, .28))) +
  labs(title = "Los 10 municipios más poblados del Estado de México",
       subtitle = paste0("Juntos concentran el ", round(pct_top10), "% de la población del estado"),
       x = "Habitantes (Censo 2020)", y = NULL, caption = fuente) +
  tema_h

2.4 Un estado desigual

Los municipios no viven igual. CONAPO resume las carencias de cada uno en el índice de marginación: educación, vivienda, ingresos y tamaño de las localidades. Aquí se agrupan en tres niveles.

Cómo leerla. Cada barra cuenta cuántos municipios hay en cada nivel de marginación (azul marino = bajo, beige = medio, guinda = alto). Debajo del número, el porcentaje de la población del estado que vive en ellos.

ggplot(dist_gm, aes(gm, municipios, fill = gm)) +
  geom_col(width = .68, color = pal$gris, linewidth = .4) +
  geom_text(aes(label = paste0(municipios, " municipios\n",
                               sprintf("%.0f", pct_pob), "% de la población")),
            vjust = -0.3, size = 3.6, color = pal$gris_osc, lineheight = .95) +
  scale_fill_manual(values = colores_gm, guide = "none") +
  scale_x_discrete(drop = FALSE) +
  scale_y_continuous(expand = expansion(mult = c(0, .22))) +
  labs(title = "Municipios del Estado de México según su nivel de marginación",
       subtitle = "Número de municipios y porcentaje de la población en cada nivel",
       x = "Nivel de marginación (CONAPO 2020)", y = "Número de municipios",
       caption = fuente) +
  tema

Lo que se ve: 12 municipios (4% de la población) tienen marginación alta, y 99 municipios (93% de la población) tienen marginación baja. Dentro de un mismo estado conviven realidades muy distintas, y de eso trata el estudio: de si el embarazo adolescente se concentra donde hay más carencias.

2.5 ¿A qué edad nacen los bebés?

Esta última gráfica es el puente hacia el tema del estudio: muestra la edad de las madres de todos los nacimientos del estado.

Cómo leerla. Cada barra es un grupo de edad de la madre y muestra qué porcentaje de los nacimientos le corresponde. Las barras en azul fuerte son las de madres menores de 20 años, las que nos interesan.

edades <- nac |>
  filter(cve_mun %in% pan$cve_mun) |>
  mutate(gr = cut(edad, c(9, 14, 19, 24, 29, 34, 39, Inf),
                  labels = c("10-14", "15-19", "20-24", "25-29", "30-34", "35-39", "40+"))) |>
  count(gr) |>
  mutate(pct = 100 * n / sum(n), adol = gr %in% c("10-14", "15-19"))
pct_adol_edo <- sum(edades$pct[edades$adol])

ggplot(edades, aes(gr, pct, fill = adol)) +
  geom_col(width = .75, color = "white", linewidth = .8) +
  geom_text(aes(label = paste0(sprintf("%.1f", pct), "%")), vjust = -0.5, size = 3.6,
            color = pal$gris_osc) +
  scale_fill_manual(values = c("TRUE" = pal$azul_rey, "FALSE" = pal$azul_cl), guide = "none") +
  scale_y_continuous(expand = expansion(mult = c(0, .12))) +
  labs(title = "Edad de la madre en los nacimientos del Estado de México",
       subtitle = paste0("De cada 100 nacimientos, ", round(pct_adol_edo, 1),
                         " son de madres menores de 20 años"),
       x = "Edad de la madre (años)", y = "% de los nacimientos", caption = fuente) +
  tema

Con este contexto, la pregunta del estudio es la siguiente: esos nacimientos de madres menores de 20 años, ¿se reparten parejo entre los municipios o se concentran donde hay más carencias? Para responderla se comparan los municipios con tasas por población, que es lo que sigue en la sección 3.

2.6 Conceptos clave

Término Qué significa en este documento
Madre adolescente Mujer que tenía menos de 20 años cuando nació su bebé.
Tasa por 1,000 habitantes Medida principal. Nacimientos de madres adolescentes en un año por cada 1,000 habitantes del municipio. Al dividir entre la población, permite comparar municipios de cualquier tamaño.
Tasa de fecundidad adolescente Versión más precisa (sección 4.2): nacimientos de madres de 15 a 19 años por cada 1,000 mujeres de 15 a 19 años. Requiere la población por edad y sexo.
Nacimientos 10–14 Nacimientos de madres de 10 a 14 años. Se muestran por separado por su especial gravedad.
Municipio de residencia Municipio donde vive la madre, que puede no ser donde se registró el nacimiento.
Índice de marginación (IMN) Medida de CONAPO (2020) que resume carencias: educación, vivienda, ingresos y localidades pequeñas. Ojo: en esta base, un valor bajo significa más marginación.
Grado de marginación CONAPO clasifica el índice en cinco grados. Aquí se agrupan en tres niveles: Bajo (muy bajo + bajo), Medio y Alto (alto + muy alto).
Índice de prioridad Medida propia que combina la tasa por habitante y la marginación. Sirve para ordenar municipios, no es un indicador oficial.

2.7 Fuentes de datos

Fuente Qué aporta
INEGI, Estadística de Nacimientos Registrados y Natalidad 2017-2025 (microdatos CSV) Edad de la madre, municipio de residencia de la madre y año de nacimiento.
CONAPO, Índice de marginación municipal 2020 Índice y grado de marginación, indicadores socioeconómicos y población total de cada municipio (Censo 2020), que es el denominador de las tasas.

3. Resultados

3.1 Panorama de la tasa

cat(sprintf(
"- Los municipios analizados suman **%s habitantes** (Censo 2020) y **%s nacimientos** de madres que viven en el Estado de México (años de nacimiento %s a %s).\n",
  fmt(sum(mun$pob_tot)), fmt(sum(mun$nacimientos)), min(nac$anio), max(nac$anio)))
  • Los municipios analizados suman 16,992,418 habitantes (Censo 2020) y 688,455 nacimientos de madres que viven en el Estado de México (años de nacimiento 2022 a 2025).
cat(sprintf(
"- La **tasa estatal** de nacimientos de madres adolescentes es de **%.2f por cada 1,000 habitantes** al año. De cada 100 nacimientos, %.1f son de madres menores de 20 años.\n",
  tasa_edo, pct_estatal))
  • La tasa estatal de nacimientos de madres adolescentes es de 1.42 por cada 1,000 habitantes al año. De cada 100 nacimientos, 14.0 son de madres menores de 20 años.
cat(sprintf(
"- Entre municipios, la tasa va de **%.2f** a **%.2f** por cada 1,000 habitantes (mediana: %.2f).\n",
  min(mun$tasa_hab), max(mun$tasa_hab), med_tasa))
  • Entre municipios, la tasa va de 0.60 a 5.37 por cada 1,000 habitantes (mediana: 1.94).

3.2 ¿Qué tan distintos son los municipios entre sí?

La gráfica muestra cuántos municipios caen en cada rango de tasa.

Cómo leerla. Cada barra agrupa municipios con tasas parecidas. La línea punteada negra es la mediana (la mitad de los municipios está por debajo y la otra mitad por encima). La línea negra continua es la tasa del Estado en conjunto. Si hay barras muy a la derecha, hay municipios claramente por encima del resto.

hj_med <- if (med_tasa <= tasa_edo) 1.1 else -0.1   # evita que las etiquetas se encimen
hj_est <- if (med_tasa <= tasa_edo) -0.1 else 1.1
ggplot(mun, aes(tasa_hab)) +
  geom_histogram(bins = 20, fill = pal$azul_rey, color = "white", linewidth = 1) +
  geom_vline(xintercept = med_tasa, linetype = "dashed", linewidth = .8,
             color = pal$texto) +
  geom_vline(xintercept = tasa_edo, color = pal$texto, linewidth = .9) +
  annotate("label", x = med_tasa, y = Inf, vjust = 1.4, hjust = hj_med, size = 3.4,
           label.size = 0, fill = pal$fondo, color = pal$gris_osc,
           label = paste0("Mediana: ", round(med_tasa, 2))) +
  annotate("label", x = tasa_edo, y = Inf, vjust = 1.4, hjust = hj_est, size = 3.4,
           label.size = 0, fill = pal$fondo, color = pal$texto, fontface = "bold",
           label = paste0("Estado: ", round(tasa_edo, 2))) +
  scale_y_continuous(expand = expansion(mult = c(0, .12))) +
  labs(title = "Distribución de la tasa de nacimientos de madres adolescentes",
       subtitle = "Cada barra agrupa municipios del Estado de México",
       x = "Nacimientos de madres menores de 20 años por cada 1,000 habitantes (anual)",
       y = "Número de municipios", caption = fuente) +
  tema

3.3 Municipios con mayor tasa de nacimientos adolescentes

Cómo leerla. Cada fila es un municipio y el punto marca su tasa: entre más a la derecha, más nacimientos de madres adolescentes hay por cada 1,000 habitantes. El color del punto indica su nivel de marginación (azul marino = bajo, beige = medio, guinda = alto). Si predominan los puntos guinda, los municipios con mayor tasa son también los más marginados. La línea punteada es la tasa estatal.

top <- mun |> slice_max(tasa_hab, n = 15) |>
  mutate(municipio = reorder(municipio, tasa_hab))
ggplot(top, aes(tasa_hab, municipio)) +
  geom_vline(xintercept = tasa_edo, linetype = "dashed", color = pal$gris) +
  geom_segment(aes(x = 0, xend = tasa_hab, yend = municipio),
               color = pal$gris_cl, linewidth = 1.2, lineend = "round") +
  geom_point(aes(fill = gm), shape = 21, size = 5.5, color = pal$gris, stroke = .7) +
  geom_text(aes(label = sprintf("%.1f", tasa_hab)),
            hjust = -0.6, size = 3.4, color = pal$gris_osc) +
  scale_fill_manual(values = colores_gm, drop = FALSE,
                    guide = guide_legend(override.aes = list(size = 4))) +
  scale_x_continuous(expand = expansion(mult = c(0, .12))) +
  labs(title = "15 municipios con mayor tasa de nacimientos adolescentes",
       subtitle = paste0("Línea punteada: tasa estatal (", round(tasa_edo, 2), " por 1,000 habitantes)"),
       x = "Nacimientos de madres < 20 años por cada 1,000 habitantes (anual)", y = NULL,
       fill = "Nivel de marginación", caption = fuente) +
  tema_h

3.4 ¿De qué edades son esos nacimientos?

La misma tasa de los 15 municipios anteriores, separada por edad de la madre. Ambas partes se calculan contra la población del municipio.

Cómo leerla. La longitud total de la barra es la tasa del municipio. El tramo azul fuerte corresponde a madres de 10 a 14 años y el azul claro a madres de 15 a 19 años.

orden <- mun |> slice_max(tasa_hab, n = 15) |> arrange(tasa_hab) |> pull(municipio)
top_e <- mun |>
  filter(municipio %in% orden) |>
  mutate(municipio = factor(municipio, levels = orden)) |>
  select(municipio, tasa_hab, `10-14 años` = tasa_hab_10_14, `15-19 años` = tasa_hab_15_19) |>
  pivot_longer(c(`10-14 años`, `15-19 años`), names_to = "grupo", values_to = "tasa")

ggplot(top_e, aes(tasa, municipio, fill = grupo)) +
  geom_col(width = .72, color = "white", linewidth = .8,
           position = position_stack(reverse = TRUE)) +
  geom_text(data = distinct(top_e, municipio, tasa_hab),
            aes(x = tasa_hab, y = municipio, label = sprintf("%.1f", tasa_hab)),
            inherit.aes = FALSE, hjust = -0.15, size = 3.4, color = pal$gris_osc) +
  scale_fill_manual(values = c("10-14 años" = pal$azul_rey, "15-19 años" = pal$azul_cl)) +
  scale_x_continuous(expand = expansion(mult = c(0, .10))) +
  labs(title = "Tasa de nacimientos adolescentes por edad de la madre",
       subtitle = "15 municipios con mayor tasa; nacimientos por 1,000 habitantes (anual)",
       x = "Nacimientos por cada 1,000 habitantes (anual)", y = NULL,
       fill = "Edad de la madre", caption = fuente) +
  tema_h

3.5 ¿Cambia la tasa según el nivel de marginación?

Aquí se agrupan los municipios según su nivel de marginación y se calcula la tasa de cada grupo completo: nacimientos de madres adolescentes de todos sus municipios ÷ población total de esos municipios.

Cómo leerla. Si las barras crecen de izquierda (marginación baja, en azul marino) a derecha (alta, en guinda), significa que a mayor marginación, mayor tasa de nacimientos de madres adolescentes. El número bajo cada valor indica cuántos municipios hay en el grupo.

por_gm <- mun |>
  group_by(gm) |>
  summarise(municipios = n(), poblacion = sum(pob_tot), nac_adol = sum(nac_adol),
            tasa = 1000 * (nac_adol / n_anios_comp) / poblacion, .groups = "drop")

ggplot(por_gm, aes(gm, tasa, fill = gm)) +
  geom_col(width = .68, color = pal$gris, linewidth = .4) +
  geom_text(aes(label = paste0(sprintf("%.2f", tasa), "\n(", municipios, " mun.)")),
            vjust = -0.3, size = 3.6, color = pal$gris_osc, lineheight = .95) +
  scale_fill_manual(values = colores_gm, guide = "none") +
  scale_x_discrete(drop = FALSE) +
  scale_y_continuous(expand = expansion(mult = c(0, .2))) +
  labs(title = "Tasa de nacimientos adolescentes según el nivel de marginación",
       subtitle = "Nacimientos por 1,000 habitantes; se suma la población de los municipios de cada nivel",
       x = "Nivel de marginación (CONAPO 2020)",
       y = "Nacimientos por 1,000 habitantes (anual)", caption = fuente) +
  tema

Para ver la dispersión dentro de cada grupo (no solo el valor conjunto), la siguiente gráfica muestra una caja por nivel.

Cómo leerla. La línea central de cada caja es la mediana de los municipios de ese nivel; la caja contiene a la mitad central de ellos; cada punto es un municipio.

ggplot(mun, aes(gm, tasa_hab, fill = gm)) +
  geom_boxplot(alpha = .85, width = .55, outlier.shape = NA, color = pal$gris_osc,
               linewidth = .5) +
  geom_jitter(width = .12, height = 0, size = 1.5, alpha = .45, color = pal$gris_osc) +
  scale_fill_manual(values = colores_gm, guide = "none") +
  scale_x_discrete(drop = FALSE) +
  labs(title = "Variación de la tasa dentro de cada nivel de marginación",
       x = "Nivel de marginación (CONAPO 2020)",
       y = "Nacimientos por 1,000 habitantes (anual)", caption = fuente) +
  tema

3.6 Relación entre marginación y tasa (cada punto es un municipio)

Cómo leerla. - Eje horizontal: marginación. Está invertido: entre más a la derecha, más marginación. - Eje vertical: tasa de nacimientos adolescentes por 1,000 habitantes. Entre más arriba, más alta. - Tamaño del punto: población del municipio. - Líneas punteadas: medianas. El cuadrante superior derecho (sombreado en gris) reúne a los municipios que combinan alta marginación y alta tasa: son los más críticos. - El color del punto es el nivel de marginación (azul marino = bajo, beige = medio, guinda = alto) y la línea oscura muestra la tendencia general.

mun <- mun |>
  mutate(cuadrante_critico = tasa_hab > med_tasa & imn < med_imn)

r_vuln <- cor(mun$tasa_hab, SIGNO_MARG * mun$imn, method = "spearman",
              use = "complete.obs")

p <- ggplot(mun, aes(imn, tasa_hab)) +
  annotate("rect", xmin = -Inf, xmax = med_imn, ymin = med_tasa, ymax = Inf,
           fill = pal$gris, alpha = .14) +
  geom_vline(xintercept = med_imn, linetype = "dashed", color = pal$gris) +
  geom_hline(yintercept = med_tasa, linetype = "dashed", color = pal$gris) +
  geom_smooth(method = "lm", se = FALSE, color = pal$texto, linewidth = .7) +
  geom_point(aes(size = pob_tot, color = gm,
                 text = paste0("<b>", municipio, "</b>",
                               "<br>Marginación: ", gm, " (IMN ", round(imn, 3), ")",
                               "<br>Tasa: ", round(tasa_hab, 2), " por 1,000 hab.",
                               "<br>Población: ", fmt(pob_tot))),
             alpha = .85) +
  scale_x_reverse() +
  scale_color_manual(values = colores_gm, drop = FALSE) +
  scale_size_continuous(range = c(1.5, 9), labels = comma) +
  labs(title = "Marginación y tasa de nacimientos adolescentes por municipio",
       subtitle = "Zona sombreada: alta marginación y alta tasa",
       x = "Marginación (IMN 2020, eje invertido): más a la derecha = MÁS marginación",
       y = "Nacimientos de madres < 20 años por 1,000 habitantes (anual)",
       color = "Nivel de marginación", size = "Población",
       caption = fuente) +
  tema
ggplotly(p, tooltip = "text") |> layout(legend = list(orientation = "h", y = -0.25))
intensidad <- cut(abs(r_vuln), c(-Inf, .2, .4, .6, Inf),
                  labels = c("muy débil", "débil", "moderada", "fuerte"))
sentido <- ifelse(r_vuln > 0,
  "**positiva**: a mayor marginación, mayor tasa de nacimientos adolescentes",
  "**negativa**: a mayor marginación, *menor* tasa de nacimientos adolescentes")
cat(sprintf(
"**Resultado.** La correlación de Spearman entre marginación y tasa de nacimientos adolescentes es **%.2f**, una relación %s y %s. Esto se refiere a los municipios en conjunto; no implica que la marginación *cause* el embarazo adolescente.\n\n",
  r_vuln, intensidad, sentido))

Resultado. La correlación de Spearman entre marginación y tasa de nacimientos adolescentes es 0.74, una relación fuerte y positiva: a mayor marginación, mayor tasa de nacimientos adolescentes. Esto se refiere a los municipios en conjunto; no implica que la marginación cause el embarazo adolescente.

cat(sprintf(
"**Cuadrante crítico:** %d de %d municipios (%.0f%%) tienen a la vez una tasa superior a la mediana (%.2f por 1,000 habitantes) y una marginación mayor a la mediana.\n",
  sum(mun$cuadrante_critico), nrow(mun),
  100 * mean(mun$cuadrante_critico), med_tasa))

Cuadrante crítico: 50 de 125 municipios (40%) tienen a la vez una tasa superior a la mediana (1.94 por 1,000 habitantes) y una marginación mayor a la mediana.

3.7 Municipios prioritarios

El índice de prioridad combina los dos indicadores en una sola cifra: entre más alto, más coinciden en ese municipio una tasa elevada de nacimientos adolescentes y una marginación alta.

Cómo leerla. Los municipios de arriba son los que mayor atención requerirían según este criterio. Cada punto marca el valor del índice de ese municipio.

top_prio <- mun |> slice_max(indice_prioridad, n = 15) |>
  mutate(municipio = reorder(municipio, indice_prioridad))
ggplot(top_prio, aes(indice_prioridad, municipio)) +
  geom_segment(aes(x = 0, xend = indice_prioridad, yend = municipio),
               color = pal$gris_cl, linewidth = 1.2, lineend = "round") +
  geom_point(shape = 21, size = 5.5, fill = pal$rojo, color = pal$fondo, stroke = 1.2) +
  geom_text(aes(label = sprintf("%.2f", indice_prioridad)),
            hjust = -0.6, size = 3.4, color = pal$gris_osc) +
  scale_x_continuous(expand = expansion(mult = c(0.02, .12))) +
  labs(title = "15 municipios con mayor índice de prioridad",
       subtitle = "Promedio de los puntajes z de la tasa por habitante y la marginación (más alto = más prioritario)",
       x = "Índice de prioridad", y = NULL, caption = fuente) +
  tema_h

Tabla completa (se puede ordenar, buscar y filtrar)

mun |>
  transmute(Municipio = municipio,
            `Nivel de marginación` = gm,
            `IMN 2020` = round(imn, 3),
            Población = pob_tot,
            `Tasa por 1,000 hab.` = round(tasa_hab, 2),
            `Tasa 10-14` = round(tasa_hab_10_14, 2),
            `Tasa 15-19` = round(tasa_hab_15_19, 2),
            `Índice de prioridad` = round(indice_prioridad, 2)) |>
  datatable(filter = "top", rownames = FALSE,
            options = list(pageLength = 15,
                           language = list(url = "//cdn.datatables.net/plug-ins/1.13.6/i18n/es-ES.json")))

4. Ampliaciones del estudio

Esta sección conecta cuatro mejoras al análisis base. Cada módulo se activa solo cuando encuentra su archivo; si todavía no lo tienes, simplemente no aparece y el resto del documento sigue funcionando.

Módulo Qué agrega Qué necesita Estado
4.1 Tendencia Cómo cambia la tasa por año CSV de natalidad de 3 o más años en data/natalidad/ ✅ activo
4.2 Fecundidad exacta Tasa por cada 1,000 mujeres de 15–19 años data/poblacion_municipal.csv ⏳ falta
4.3 Pobreza Índice de prioridad ampliado con pobreza municipal data/coneval_pobreza_municipal_2020.csv ⏳ falta
4.4 Dashboard App interactiva con todos los resultados app.R + resultados_municipios.rds ver abajo

4.1 Tendencia en el tiempo

Qué responde: ¿los nacimientos de madres adolescentes en el Estado están subiendo o bajando, en cifras reales y en tasa por habitantes?

Cómo activarla: descarga los CSV de natalidad de otros años en https://www.inegi.org.mx/programas/natalidad/#datos_abiertos y colócalos en data/natalidad/ (el documento los lee todos). Ajusta ANIO_HIST (primer año del histórico) para incluirlos. Se muestran todos los años disponibles; el último se marca si parece incompleto. La población usada como denominador es la del Censo 2020 en todos los años.

# Estado de México en conjunto (todos los municipios), solo dos series:
#   1) nacimientos reales de madres < 20 años   2) tasa por 1,000 habitantes
pob_total <- sum(mun_todos$pob_tot)
tend <- nac_hist |>
  filter(cve_mun %in% mun_todos$cve_mun) |>
  group_by(anio) |>
  summarise(n = n(), adol = sum(grupo != "20+"), .groups = "drop") |>
  mutate(tasa = 1000 * adol / pob_total,
         # el último año suele estar incompleto (registros tardíos): se marca con punto hueco
         parcial = anio == max(anio) & n < 0.9 * dplyr::lag(n, default = max(n)))

# Dos líneas en una sola gráfica: real (gris punteada, eje izquierdo) y tasa (azul, eje derecho).
# La tasa se dibuja con un factor de escala para que no se encime con la línea real; cada eje
# se lee con su propia serie.
k <- 0.7 * max(tend$adol) / max(tend$tasa)

ggplot(tend, aes(anio)) +
  geom_line(aes(y = adol), color = pal$gris, linetype = "dotted", linewidth = 1.5) +
  geom_point(aes(y = adol, fill = ifelse(parcial, "white", pal$gris)), shape = 21,
             color = pal$gris, stroke = 1.2, size = 3.6) +
  geom_text(aes(y = adol, label = fmt(adol)), vjust = -1.1, size = 3.3, color = pal$gris_osc) +
  geom_line(aes(y = tasa * k), color = pal$azul_rey, linewidth = 1.5) +
  geom_point(aes(y = tasa * k, fill = ifelse(parcial, "white", pal$azul_rey)), shape = 21,
             color = pal$azul_rey, stroke = 1.4, size = 4.2) +
  geom_text(aes(y = tasa * k, label = sprintf("%.2f", tasa)), vjust = 2.1, size = 3.4,
            color = pal$azul_rey, fontface = "bold") +
  scale_fill_identity() +
  scale_x_continuous(breaks = tend$anio, expand = expansion(mult = c(.04, .06))) +
  scale_y_continuous(name = "Nacimientos reales de madres < 20 años (gris punteada)",
                     labels = comma, expand = expansion(mult = c(.14, .15)),
                     breaks = seq(0, 200000, by = 20000),   # rayas horizontales cada 20,000
                     minor_breaks = seq(0, 200000, by = 10000),   # rayas punteadas cada 10,000
                     sec.axis = sec_axis(~ . / k, name = "Tasa por cada 1,000 habitantes (azul)")) +
  labs(title = "Tendencia de los nacimientos de madres adolescentes en el Estado de México",
       subtitle = "Azul: tasa por 1,000 habitantes (población base Censo 2020). Gris punteada: número real de nacimientos",
       x = "Año de nacimiento",
       caption = paste0(fuente, if (any(tend$parcial)) "\nPunto hueco: año probablemente incompleto (registros tardíos)." else "")) +
  tema + theme(axis.title.y.left = element_text(color = pal$gris),
               axis.title.y.right = element_text(color = pal$azul_rey),
               panel.grid.major.y = element_line(color = pal$gris_ml, linewidth = .4),
               panel.grid.minor.y = element_line(color = "#D6D1C7", linewidth = .5, linetype = "dotted"),
               panel.grid.major.x = element_blank(), panel.grid.minor.x = element_blank())

Cómo leerla. Hay dos líneas. La azul es la tasa por cada 1,000 habitantes y se lee en el eje de la derecha. La gris punteada es la cifra real (número de bebés de madres menores de 20 años) y se lee en el eje de la izquierda. Como ambas se dividen entre la misma población (Censo 2020), tienen la misma forma; la azul solo expresa la cifra real en relación con el tamaño del estado. Un punto hueco indica un año que probablemente aún no tiene todos sus registros.

Datos de la gráfica
Año de nacimiento Todos los nacimientos De madres < 20 años Por 1,000 hab.
2,017 276,203 49,699 2.92
2,018 258,720 46,235 2.72
2,019 240,043 40,799 2.40
2,020 219,844 34,050 2.00
2,021 194,601 29,255 1.72
2,022 198,009 28,219 1.66
2,023 185,388 26,242 1.54
2,024 171,171 24,301 1.43
2,025 133,887 17,964 1.06

4.2 Tasa de fecundidad adolescente (exacta)

Qué responde: la tasa por 1,000 habitantes usa a toda la población como denominador. La tasa de fecundidad adolescente es más precisa: compara los nacimientos de madres de 15 a 19 años con cuántas mujeres de esa edad viven en el municipio.

Fórmula: tasa = nacimientos de madres de 15–19 años en el año ÷ mujeres de 15–19 años en ese año × 1,000.

Cómo activarla: descarga las proyecciones de población municipal de CONAPO (https://www.gob.mx/conapo/documentos/reconstruccion-y-proyecciones-de-la-poblacion-de-los-municipios-de-mexico-1990-2040), guarda el CSV como data/poblacion_municipal.csv y revisa en el mensaje del documento qué columnas detectó (año, clave, sexo, edad, población). Si no coincide, mándame una captura de los encabezados y lo ajusto.

pob <- as.data.frame(data.table::fread(archivo_pob, encoding = "UTF-8"))
names(pob) <- tolower(names(pob))
bus <- function(p) grep(p, names(pob), value = TRUE)[1]
c_anio <- bus("^a.{1,2}o$|^anio$|^year$|^periodo$")
c_cve  <- bus("^cve_geo$|^cvegeo$|^cve_mun$|^clave$|^cve$")
c_sexo <- bus("sexo|^sex")
c_edad <- bus("edad|^age")
c_pob  <- bus("poblaci|^pob$|^pob_")
message("Población -> año: ", c_anio, " | clave: ", c_cve, " | sexo: ", c_sexo,
        " | edad: ", c_edad, " | población: ", c_pob)

edad_chr <- as.character(pob[[c_edad]])
es_15_19 <- grepl("^15\\s*(-|a)\\s*19", edad_chr) |
            (suppressWarnings(as.integer(edad_chr)) %in% 15:19)
es_mujer <- grepl("muj|fem|^2$", tolower(as.character(pob[[c_sexo]])))

pob_f <- pob |>
  mutate(cve_mun = suppressWarnings(as.integer(.data[[c_cve]])),
         anio_p  = suppressWarnings(as.integer(.data[[c_anio]])),
         p       = suppressWarnings(as.numeric(.data[[c_pob]]))) |>
  filter(es_15_19, es_mujer, anio_p == ANIO_TASA, cve_mun %/% 1000 == CVE_ENT) |>
  group_by(cve_mun) |>
  summarise(pob_f_15_19 = sum(p, na.rm = TRUE), .groups = "drop")

nac_t <- nac |> filter(anio == ANIO_TASA, grupo == "15-19") |>
  count(cve_mun, name = "nac_15_19_anio")

mun <- mun |>
  select(-any_of(c("pob_f_15_19", "nac_15_19_anio", "tasa_fec"))) |>
  left_join(pob_f, by = "cve_mun") |>
  left_join(nac_t, by = "cve_mun") |>
  mutate(nac_15_19_anio = coalesce(nac_15_19_anio, 0L),
         tasa_fec = 1000 * nac_15_19_anio / pob_f_15_19)

stopifnot(sum(!is.na(mun$tasa_fec)) > 0)
tasa_fec_edo <- 1000 * sum(mun$nac_15_19_anio, na.rm = TRUE) /
                       sum(mun$pob_f_15_19, na.rm = TRUE)

top_t <- mun |> filter(!is.na(tasa_fec)) |>
  slice_max(tasa_fec, n = 15) |>
  mutate(municipio = reorder(municipio, tasa_fec))

ggplot(top_t, aes(tasa_fec, municipio)) +
  geom_vline(xintercept = tasa_fec_edo, linetype = "dashed", color = pal$gris) +
  geom_segment(aes(x = 0, xend = tasa_fec, yend = municipio),
               color = pal$gris_cl, linewidth = 1.2, lineend = "round") +
  geom_point(aes(fill = gm), shape = 21, size = 5.5, color = pal$gris, stroke = .7) +
  geom_text(aes(label = round(tasa_fec, 1)), hjust = -0.6, size = 3.4,
            color = pal$gris_osc) +
  scale_fill_manual(values = colores_gm, drop = FALSE,
                    guide = guide_legend(override.aes = list(size = 4))) +
  scale_x_continuous(expand = expansion(mult = c(0, .12))) +
  labs(title = paste0("15 municipios con mayor tasa de fecundidad adolescente (", ANIO_TASA, ")"),
       subtitle = paste0("Nacimientos por cada 1,000 mujeres de 15 a 19 años. Línea punteada: Estado (",
                         round(tasa_fec_edo, 1), ")"),
       x = "Tasa por 1,000 mujeres de 15 a 19 años", y = NULL,
       fill = "Nivel de marginación", caption = fuente) +
  tema_h

Cómo leerla. Igual que el ranking de la sección 3.3, pero con la población de mujeres de 15 a 19 años como denominador. Si un municipio sube o baja mucho de posición respecto a ese ranking, es porque la proporción de mujeres adolescentes en su población es distinta a la de otros municipios.

4.3 Pobreza municipal (CONEVAL)

Qué responde: la marginación mide carencias de la población en general. La pobreza municipal de CONEVAL agrega una segunda mirada, basada en ingreso y carencias sociales, para refinar el índice de prioridad.

Cómo activarla: descarga la base de pobreza municipal 2020 en https://www.coneval.org.mx/Medicion/Paginas/Pobreza-municipio-2010-2020.aspx (o en https://www.coneval.org.mx/Medicion/Paginas/Programas_BD_municipal_2010_2020.aspx), deja una sola fila de encabezados, guárdala como CSV UTF-8 en data/coneval_pobreza_municipal_2020.csv y revisa el mensaje del documento. Necesita una columna con la clave del municipio y el porcentaje de población en pobreza.

cone <- tryCatch(
  readr::read_csv(archivo_cone, show_col_types = FALSE,
                  locale = readr::locale(encoding = "UTF-8")),
  error = function(e)
    readr::read_csv(archivo_cone, show_col_types = FALSE,
                    locale = readr::locale(encoding = "latin1")))
names(cone) <- tolower(names(cone))
bus <- function(p) grep(p, names(cone), value = TRUE)[1]
c_cve <- bus("^clave_municipio$|^cve_mun$|^cvegeo$|^cve_geo$|^clave$|^id_mun")
c_pob <- bus("^pobreza$|^pobreza_p$|^pobreza_pct$|^pobreza_porc")
c_ext <- bus("^pobreza_e$|^pobreza_ext")
message("CONEVAL -> clave: ", c_cve, " | pobreza: ", c_pob, " | pobreza extrema: ", c_ext)

cone2 <- data.frame(
  cve_mun = suppressWarnings(as.integer(cone[[c_cve]])),
  pobreza = suppressWarnings(as.numeric(cone[[c_pob]])),
  pobreza_ext = if (!is.na(c_ext)) suppressWarnings(as.numeric(cone[[c_ext]])) else NA_real_
)
if (max(cone2$pobreza, na.rm = TRUE) <= 1) {      # si viene como proporción, pasa a %
  cone2$pobreza <- 100 * cone2$pobreza
  cone2$pobreza_ext <- 100 * cone2$pobreza_ext
}

mun <- mun |>
  select(-any_of(c("pobreza", "pobreza_ext", "z_pob", "indice_prioridad2"))) |>
  left_join(cone2, by = "cve_mun") |>
  mutate(z_pob = as.numeric(scale(pobreza)),
         indice_prioridad2 = (z_tasa + z_pob + SIGNO_MARG * z_marg) / 3)

stopifnot(sum(!is.na(mun$pobreza)) > 0)
r_pob <- cor(mun$tasa_hab, mun$pobreza, method = "spearman", use = "complete.obs")

ggplot(mun, aes(pobreza, tasa_hab)) +
  geom_smooth(method = "lm", se = FALSE, color = pal$texto, linewidth = .7) +
  geom_point(aes(size = pob_tot, fill = gm), shape = 21, color = pal$gris,
             stroke = .6, alpha = .95) +
  scale_fill_manual(values = colores_gm, drop = FALSE,
                    guide = guide_legend(override.aes = list(size = 4))) +
  scale_size_continuous(range = c(1.5, 9), labels = comma) +
  labs(title = "Pobreza municipal y tasa de nacimientos adolescentes",
       subtitle = paste0("Correlación de Spearman: ", round(r_pob, 2)),
       x = "% de población en situación de pobreza (CONEVAL 2020)",
       y = "Nacimientos de madres < 20 años por 1,000 habitantes (anual)",
       fill = "Nivel de marginación", size = "Población",
       caption = "Fuente: INEGI, CONAPO y CONEVAL. Elaboración propia.") +
  tema

El índice de prioridad ampliado promedia tres puntajes z: tasa de nacimientos adolescentes por habitante, marginación (con signo invertido) y pobreza.

mun |>
  filter(!is.na(indice_prioridad2)) |>
  arrange(desc(indice_prioridad2)) |>
  mutate(`Posición ampliado` = row_number(),
         `Posición original` = rank(-indice_prioridad, ties.method = "min")) |>
  transmute(Municipio = municipio, `Nivel de marginación` = gm,
            `% pobreza` = round(pobreza, 1), `Tasa por 1,000 hab.` = round(tasa_hab, 2),
            `Índice ampliado` = round(indice_prioridad2, 2),
            `Posición ampliado`, `Posición original`) |>
  datatable(filter = "top", rownames = FALSE, options = list(pageLength = 10))

4.4 Dashboard interactivo (Shiny)

Los resultados se guardan en data/resultados_municipios.rds, incluyendo las columnas de las secciones 4.2 y 4.3 cuando están activas. El archivo app.R (se entrega junto a este documento) los lee y arma un tablero con ranking, dispersión, ficha por municipio y tabla descargable. Todos sus indicadores son tasas por población.

saveRDS(mun, "data/resultados_municipios.rds")
cat("Resultados guardados con", ncol(mun), "columnas y", nrow(mun), "municipios.\n")
## Resultados guardados con 24 columnas y 125 municipios.
cat("Columnas:", paste(names(mun), collapse = ", "))
## Columnas: cve_mun, nacimientos, nac_10_14, nac_15_19, nac_adol, pct_adol, cve_ent, municipio, pob_tot, imn, analf, po2sm, pl5000, vhac, gm_5, gm, tasa_hab, tasa_hab_10_14, tasa_hab_15_19, anios_base, z_tasa, z_marg, indice_prioridad, cuadrante_critico

Para usarlo: copia data/resultados_municipios.rds a una carpeta app/ junto con app.R, y ejecuta en R shiny::runApp("app"). Para publicarlo se usa shinyapps.io (RPubs no ejecuta Shiny). Cuando la app esté publicada, pega su dirección en URL_APP (anexo técnico, sección 8) y este documento mostrará un botón y la app incrustada.

(Aún no hay dirección de la app: define URL_APP para que aparezca aquí.)

5. Conclusiones

top3_tasa <- mun |> slice_max(tasa_hab, n = 3) |> pull(municipio)
top3_abs  <- mun |> slice_max(nac_adol, n = 3) |> pull(municipio)
top3_prio <- mun |> slice_max(indice_prioridad, n = 3) |> pull(municipio)
g <- por_gm |> filter(!is.na(gm))

cat(sprintf("1. **Nivel estatal.** Se registran %.2f nacimientos de madres menores de 20 años al año por cada 1,000 habitantes del Estado de México.\n",
            tasa_edo))
  1. Nivel estatal. Se registran 1.42 nacimientos de madres menores de 20 años al año por cada 1,000 habitantes del Estado de México.
cat(sprintf("2. **Mayor tasa por habitante.** Los municipios con mayor tasa son %s.\n",
            paste(top3_tasa, collapse = ", ")))
  1. Mayor tasa por habitante. Los municipios con mayor tasa son Ixtapan del Oro, San José del Rincón, Donato Guerra.
cat(sprintf("3. **Marginación.** Entre los municipios de marginación %s, la tasa conjunta es de %.2f por 1,000 habitantes; entre los de marginación %s, de %.2f.\n",
            tolower(as.character(g$gm[1])), g$tasa[1],
            tolower(as.character(g$gm[nrow(g)])), g$tasa[nrow(g)]))
  1. Marginación. Entre los municipios de marginación bajo, la tasa conjunta es de 1.33 por 1,000 habitantes; entre los de marginación alto, de 2.91.
cat(sprintf("4. **Prioridad.** Combinando tasa y marginación, los municipios que más requieren atención son %s.\n",
            paste(top3_prio, collapse = ", ")))
  1. Prioridad. Combinando tasa y marginación, los municipios que más requieren atención son Ixtapan del Oro, San José del Rincón, Villa Victoria.
cat(sprintf("5. **Tasa frente a número de casos.** Los municipios con más casos en números absolutos (%s) no necesariamente son los de mayor tasa: por eso el análisis se basa en tasas por población.\n",
            paste(top3_abs, collapse = ", ")))
  1. Tasa frente a número de casos. Los municipios con más casos en números absolutos (Ecatepec de Morelos, Chimalhuacán, Toluca) no necesariamente son los de mayor tasa: por eso el análisis se basa en tasas por población.

Recomendación de lectura. La tasa por habitante señala dónde el problema es proporcionalmente más grave. Los conteos absolutos pueden orientar recursos totales, pero no deben usarse para comparar municipios de distinto tamaño.

5.1 Balance neto: ¿hemos subido o bajado?

Cierre del estudio: comparar el primer y el último año completo disponible, siempre sobre la población base del Censo 2020, para ver, en neto, si la tasa de nacimientos de madres adolescentes del Estado subió o bajó, y si lo hizo igual en los municipios de marginación baja, media y alta.

  • Estado en conjunto: entre 2017 y 2024 la tasa de nacimientos de madres adolescentes bajó: pasó de 2.92 a 1.43 por cada 1,000 habitantes, un cambio neto de -1.49 puntos (-51.1%).
  • Municipios: la tasa bajó en 123 municipios y subió en 2 (de 125 analizados).
  • Marginación bajo: de 2.79 a 1.34 (-52.1%).
  • Marginación medio: de 4.49 a 2.54 (-43.5%).
  • Marginación alto: de 5.13 a 2.95 (-42.5%).

Cómo leerla. Para cada nivel de marginación hay dos barras: la clara es el primer año y la azul fuerte es el último. Si la barra azul fuerte es más baja, la tasa bajó. Compara si la baja (o el aumento) es parecida en los tres niveles o si algún nivel se queda atrás.

bg <- bal_gm |>
  tidyr::pivot_longer(c(t0, t1), names_to = "momento", values_to = "tasa") |>
  mutate(anio = factor(ifelse(momento == "t0", a0, a1), levels = c(a0, a1)))
ggplot(bg, aes(gm, tasa, fill = anio)) +
  geom_col(position = position_dodge(width = .75), width = .68, color = "white", linewidth = .8) +
  geom_text(aes(label = sprintf("%.2f", tasa)), position = position_dodge(width = .75),
            vjust = -0.5, size = 3.5, color = pal$gris_osc) +
  scale_fill_manual(values = setNames(c(pal$azul_cl, pal$azul_rey), c(a0, a1))) +
  scale_y_continuous(expand = expansion(mult = c(0, .15))) +
  labs(title = paste0("Balance neto de la tasa de nacimientos adolescentes, ", a0, " vs. ", a1),
       subtitle = "Nacimientos de madres < 20 años por 1,000 habitantes, según el nivel de marginación",
       x = "Nivel de marginación (CONAPO 2020)", y = "Nacimientos por 1,000 habitantes",
       fill = "Año de nacimiento", caption = fuente) +
  tema

Cambio neto, ajustado a la población base 2020. Ambos años se dividen entre la misma población (Censo 2020), así que la comparación es neta: el cambio refleja solo la variación de los nacimientos, no el crecimiento ni la migración de la población. Además, el último año puede estar incompleto por registros tardíos; por eso solo se comparan años con registros completos.

6. Limitaciones

  • Nacimientos, no embarazos. El indicador no incluye abortos ni pérdidas gestacionales, y depende del registro civil (subregistro y registro tardío).
  • Denominador aproximado. La tasa por 1,000 habitantes usa la población total de 2020 (Censo) y no la de mujeres de 15 a 19 años del año de los nacimientos. La tasa de fecundidad exacta se calcula en la sección 4.2 cuando se agrega la población por edad y sexo.
  • Municipios pequeños. Las tasas con pocos nacimientos son inestables; por eso se excluyen los municipios con menos de 30 nacimientos en el periodo.
  • Marginación como aproximación. El índice de CONAPO mide carencias de la población en general, no la vulnerabilidad de la niñez en específico. Se puede complementar con pobreza municipal (CONEVAL) y rezago educativo (Censo 2020).
  • Análisis ecológico. Las relaciones entre municipios no se pueden atribuir a personas individuales, y una correlación no demuestra causalidad.
  • Residencia vs. registro. Se usa el municipio donde vive la madre, no donde se registró el nacimiento.

7. Próximos pasos

Los cuatro pasos planteados ya están conectados en la sección 4. Lo que falta para completarlos:

Paso Qué falta
Tasa de fecundidad exacta Guardar la población de CONAPO por edad y sexo como data/poblacion_municipal.csv
Tendencia Agregar CSV de natalidad de otros años en data/natalidad/
Pobreza municipal Guardar la base de CONEVAL como data/coneval_pobreza_municipal_2020.csv
Dashboard Shiny Copiar resultados_municipios.rds y app.R a una carpeta app/

8. Anexo técnico: preparación de los datos

Esta sección explica cómo se prepararon los datos. El código se ejecuta al inicio del documento (para que el panorama de la sección 2 pueda usarlo) y se muestra aquí para que sea reproducible. Los parámetros se pueden modificar:

Parámetro Para qué sirve Valor
ANIO_HIST Primer año de nacimiento del histórico (tendencia y balance neto). 2017
ANIO_MIN Primer año de la ventana del análisis principal (tasas y rankings). 2022
ANIO_FIN Último año de nacimiento que se incluye (2025 aún puede recibir registros tardíos). 2025
MIN_NAC Mínimo de nacimientos por municipio. Con muy pocos nacimientos, la tasa es inestable. 30
SIGNO_MARG Corrige el sentido del índice de marginación (en CONAPO 2020, valor bajo = más marginación). -1
URL_APP Dirección de la app Shiny publicada, para enlazarla desde este documento. vacío

8.1 Parámetros y archivos

El documento busca los archivos en la carpeta data/ del proyecto. Si no están, los copia desde la carpeta de Descargas de Windows.

CVE_ENT    <- 15      # Estado de México
MIN_NAC    <- 30      # mínimo de nacimientos (periodo) por municipio
ANIO_HIST  <- 2017    # primer año de nacimiento del histórico (tendencia y balance neto)
ANIO_MIN   <- 2022    # primer año de la ventana del análisis principal (tasas y rankings)
ANIO_FIN   <- 2025    # último año de nacimiento a incluir (2025 aún puede recibir registros tardíos; usa 2024 si prefieres solo años casi completos)
SIGNO_MARG <- -1      # IMN alto = menos marginación -> se invierte el signo
ANIO_TASA  <- 2025    # año para calcular la tasa de fecundidad adolescente (sección 4.2)
URL_APP    <- ""      # pega aquí la dirección de tu app en shinyapps.io (sección 4.4)

archivo_marg <- "data/marginacion_municipal_2020.csv"
descargas <- file.path(Sys.getenv("USERPROFILE"), "Downloads")

# Archivos OPCIONALES de las ampliaciones (secciones 4.2 y 4.3)
archivo_pob  <- "data/poblacion_municipal.csv"                  # CONAPO, población por edad y sexo
archivo_cone <- "data/coneval_pobreza_municipal_2020.csv"       # CONEVAL, pobreza municipal
hay_pob  <- file.exists(archivo_pob)
hay_cone <- file.exists(archivo_cone)
hay_tend <- FALSE     # se activa solo si hay 3 o más años de nacimiento completos

# Copiar CONAPO desde Descargas si aún no está en el proyecto
if (!file.exists(archivo_marg)) {
  cand <- list.files(descargas, pattern = "^imm_2020.*\\.csv$",
                     full.names = TRUE, ignore.case = TRUE)
  if (length(cand)) file.copy(cand[1], archivo_marg)
}

# Copiar natalidad INEGI desde Descargas si aún no está en el proyecto
# (copia TODOS los años: conjunto_de_datos_enr2023..2025 y conjunto_de_datos_natalidad_2017..2022)
cand <- list.files(descargas, pattern = "^conjunto_de_datos_(enr|natalidad_?)20[0-9]{2}.*\\.csv$",
                   recursive = TRUE, full.names = TRUE, ignore.case = TRUE)
cand <- cand[!duplicated(basename(cand))]
cand <- cand[!basename(cand) %in% list.files("data/natalidad")]
if (length(cand)) file.copy(cand, "data/natalidad/")

archivos_nac <- list.files("data/natalidad", pattern = "\\.csv$",
                           full.names = TRUE, ignore.case = TRUE)
hay_nac  <- length(archivos_nac) > 0
hay_marg <- file.exists(archivo_marg)
listo    <- hay_nac && hay_marg
## Natalidad (INEGI):   conjunto_de_datos_enr2023.csv conjunto_de_datos_enr2024.csv conjunto_de_datos_enr2025.csv conjunto_de_datos_natalidad_2017.csv conjunto_de_datos_natalidad_2018.csv conjunto_de_datos_natalidad_2019.csv conjunto_de_datos_natalidad_2020.csv conjunto_de_datos_natalidad_2021.csv conjunto_de_datos_natalidad_2022.csv
## Marginación (CONAPO): data/marginacion_municipal_2020.csv

8.2 Marginación y población municipal (CONAPO)

Se leen los datos de CONAPO, se conservan solo los municipios del Estado de México (clave de entidad 15) y se verifica que sean 125. De este archivo salen el grado de marginación, la población total de cada municipio (pob_tot), que es el denominador de todas las tasas, y cuatro indicadores socioeconómicos que se usan en el panorama de la sección 2.

marg <- tryCatch(
  readr::read_csv(archivo_marg, show_col_types = FALSE,
                  locale = readr::locale(encoding = "UTF-8")),
  error = function(e)
    readr::read_csv(archivo_marg, show_col_types = FALSE,
                    locale = readr::locale(encoding = "latin1"))
)
names(marg) <- tolower(names(marg))
col <- function(patron) grep(patron, names(marg), value = TRUE)[1]
# Columna numérica opcional: si no existe en el archivo, devuelve NA
num <- function(patron) {
  c <- col(patron)
  if (is.na(c)) rep(NA_real_, nrow(marg)) else suppressWarnings(as.numeric(marg[[c]]))
}

marg <- marg |>
  transmute(
    cve_ent   = as.integer(.data[[col("^cve_ent$")]]),
    cve_mun   = as.integer(.data[[col("^cve_mun$")]]),
    municipio = .data[[col("^nom_mun$")]],
    pob_tot   = as.numeric(.data[[col("^pob_tot$")]]),
    imn       = as.numeric(.data[[col("^imn_2020$")]]),
    # Indicadores socioeconómicos de CONAPO usados en el panorama (sección 2)
    analf     = num("^analf$"),      # % población de 15 años o más analfabeta
    po2sm     = num("^po2sm$"),      # % población ocupada con ingresos de hasta 2 salarios mínimos
    pl5000    = num("^pl.5000$"),    # % población en localidades de menos de 5,000 habitantes
    vhac      = num("^vhac$"),       # % viviendas con hacinamiento
    gm_5      = .data[[col("^gm_2020$")]],
    gm        = factor(case_when(tolower(gm_5) %in% c("muy bajo", "bajo") ~ "Bajo",
                                 tolower(gm_5) == "medio"                  ~ "Medio",
                                 tolower(gm_5) %in% c("alto", "muy alto")  ~ "Alto"),
                       levels = niveles_gm)
  ) |>
  filter(cve_ent == CVE_ENT)

if (all(marg$cve_mun < 1000)) marg$cve_mun <- CVE_ENT * 1000L + marg$cve_mun
## Municipios en la base de marginación: 125 (el Estado de México tiene 125)
## Población total sumada: 16,992,418 habitantes

8.3 Nacimientos registrados (INEGI)

Se leen solo cuatro columnas del archivo de INEGI: edad de la madre (edad_madn), entidad y municipio de residencia de la madre (ent_resid, mun_resid) y año de nacimiento (ano_nac). Después se conservan los nacimientos de madres que viven en el Estado de México y se descartan registros con datos no especificados (edad 99, municipio 999).

leer_nac <- function(f) tryCatch(leer_nac0(f), error = function(e) {
  message("No se pudo leer ", basename(f), ": ", conditionMessage(e)); NULL })
leer_nac0 <- function(f) {
  # Los archivos de INEGI no son homogéneos: algunos traen tabuladores y el de 2017 trae
  # el encabezado separado por ";" y los datos por ",". Se detecta el separador de cada uno.
  l <- readLines(f, n = 2, warn = FALSE, encoding = "latin1")
  h <- sub("^(\u00ef\u00bb\u00bf|\ufeff)", "", l[1])
  cuenta <- function(x, sp) lengths(regmatches(x, gregexpr(sp, x, fixed = TRUE)))
  seps   <- c(";", "\t", ",")
  sep_h  <- seps[which.max(sapply(seps, function(z) cuenta(h, z)))]
  sep_d  <- seps[which.max(sapply(seps, function(z) cuenta(l[2], z)))]
  enc    <- trimws(gsub('"', "", strsplit(h, sep_h, fixed = TRUE)[[1]]))
  pick   <- function(patron) grep(patron, enc, ignore.case = TRUE)[1]
  i_edad <- pick("^edad_madn$"); i_ent <- pick("^ent_resid$")
  i_mun  <- pick("^mun_resid$"); i_anio <- pick("^ano_nac$|^anio_nac$")
  idx <- c(i_edad, i_ent, i_mun, i_anio)
  message(basename(f), " -> separador encabezado '", sep_h, "', datos '", sep_d,
          "' | columnas: ", paste(enc[idx], collapse = ", "))
  if (anyNA(idx)) stop("no se encontraron las columnas necesarias en el encabezado")
  d <- data.table::fread(f, skip = 1, header = FALSE, sep = sep_d,
                         select = sort(idx), showProgress = FALSE)
  names(d) <- c("edad", "ent", "mun", "anio")[order(idx)]
  d <- d[, c("edad", "ent", "mun", "anio"), with = FALSE]
  d <- d[, lapply(.SD, function(x) suppressWarnings(as.integer(x)))]
  message("   años de nacimiento en el archivo: ", paste(range(d$anio, na.rm = TRUE), collapse = " a "),
          " | renglones: ", format(nrow(d), big.mark = ","))
  as.data.frame(d[!is.na(ent) & ent == CVE_ENT])
}

# Histórico completo (todos los archivos): sirve para la tendencia y el balance neto
nac_hist <- bind_rows(lapply(archivos_nac, leer_nac)) |>
  filter(!is.na(edad), edad >= 10, edad <= 60,
         !is.na(mun), mun < 900,
         !is.na(anio), anio >= ANIO_HIST, anio <= ANIO_FIN) |>
  mutate(cve_mun = CVE_ENT * 1000L + mun,
         grupo = case_when(edad < 15 ~ "10-14",
                           edad < 20 ~ "15-19",
                           TRUE      ~ "20+")) |>
  select(-mun)   # evita que la columna `mun` tape al objeto `mun` (tabla de municipios) dentro de filter()
# Ventana del análisis principal (tasas, rankings, mapas de prioridad)
nac <- nac_hist |> filter(anio >= ANIO_MIN)
Nacimientos por año de nacimiento
Año de nacimiento Nacimientos (madres residentes en Edomex) De madres < 20 años
2,022 198,009 28,219
2,023 185,388 26,242
2,024 171,171 24,301
2,025 133,887 17,964

Nota. Los archivos de INEGI incluyen registros tardíos: bebés nacidos en años anteriores pero registrados en 2025. Por eso aparecen varios años en la tabla. Para calcular tasas anuales, el documento promedia solo los años con registros completos. El análisis principal usa los años de ANIO_MIN a ANIO_FIN; el histórico (desde ANIO_HIST) alimenta la tendencia y el balance.

8.4 Cálculo de indicadores por municipio

Para cada municipio se calcula:

  • Tasa por 1,000 habitantes (medida principal) = nacimientos de madres adolescentes en un año ÷ población del municipio × 1,000. Cuando hay varios años completos, se usa el promedio anual.
  • La misma tasa separada por edad de la madre (10–14 y 15–19 años).
  • Índice de prioridad: promedio de dos puntajes z (el de la tasa y el de la marginación, con el signo invertido para que alto = más vulnerable). Un puntaje z indica cuántas desviaciones estándar está un municipio por encima o por debajo del promedio estatal.

El porcentaje de nacimientos adolescentes (sobre el total de nacimientos) se conserva solo como dato de referencia. El panorama de la sección 2 usa los 125 municipios; las tasas de la sección 3 excluyen a los de muy pocos nacimientos.

# Años con registros completos (para anualizar los nacimientos)
n_anio       <- table(nac$anio)
n_anios_comp <- max(1, sum(n_anio >= 0.5 * max(n_anio)))

mun <- nac |>
  group_by(cve_mun) |>
  summarise(nacimientos = n(),
            nac_10_14   = sum(grupo == "10-14"),
            nac_15_19   = sum(grupo == "15-19"),
            .groups = "drop") |>
  mutate(nac_adol = nac_10_14 + nac_15_19,
         pct_adol = 100 * nac_adol / nacimientos) |>        # solo referencia
  inner_join(marg, by = "cve_mun") |>
  mutate(
    # Tasas por 1,000 habitantes del municipio (promedio anual)
    tasa_hab       = 1000 * (nac_adol  / n_anios_comp) / pob_tot,
    tasa_hab_10_14 = 1000 * (nac_10_14 / n_anios_comp) / pob_tot,
    tasa_hab_15_19 = 1000 * (nac_15_19 / n_anios_comp) / pob_tot,
    anios_base     = n_anios_comp)

mun_todos  <- mun          # los 125 municipios, sin filtro (para el panorama de la sección 2)
n_excluidos <- sum(mun$nacimientos < MIN_NAC)
mun <- mun |>
  filter(nacimientos >= MIN_NAC) |>
  mutate(z_tasa = as.numeric(scale(tasa_hab)),
         z_marg = as.numeric(scale(imn)),
         indice_prioridad = (z_tasa + SIGNO_MARG * z_marg) / 2) |>
  arrange(desc(indice_prioridad))

# Cifras de referencia para el texto y las gráficas
tasa_edo    <- 1000 * (sum(mun$nac_adol) / n_anios_comp) / sum(mun$pob_tot)
pct_estatal <- 100 * sum(mun$nac_adol) / sum(mun$nacimientos)
med_tasa    <- median(mun$tasa_hab)
med_imn     <- median(mun$imn)

# ¿Hay suficientes años completos para mostrar una tendencia? (sección 4.1)
n_anio_h <- table(nac_hist$anio)
hay_tend <- sum(n_anio_h >= 0.5 * max(n_anio_h)) >= 3

saveRDS(mun, "data/resultados_municipios.rds")   # insumo para la app Shiny
## Municipios analizados: 125 | excluidos por tener menos de 30 nacimientos: 0 | años completos promediados: 4

9. Reproducibilidad

sessionInfo()
## R version 4.5.1 (2025-06-13 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
## 
## Matrix products: default
##   LAPACK version 3.12.1
## 
## locale:
## [1] LC_COLLATE=Spanish_Mexico.utf8  LC_CTYPE=Spanish_Mexico.utf8   
## [3] LC_MONETARY=Spanish_Mexico.utf8 LC_NUMERIC=C                   
## [5] LC_TIME=Spanish_Mexico.utf8    
## 
## time zone: America/Mexico_City
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] knitr_1.50        scales_1.4.0      DT_0.34.0         plotly_4.12.1    
##  [5] ggplot2_3.5.2     readxl_1.4.5      readr_2.1.5       stringr_1.5.2    
##  [9] tidyr_1.3.1       dplyr_1.1.4       data.table_1.17.8
## 
## loaded via a namespace (and not attached):
##  [1] sass_0.4.10        generics_0.1.4     lattice_0.22-7     stringi_1.8.7     
##  [5] hms_1.1.3          digest_0.6.37      magrittr_2.0.3     evaluate_1.0.5    
##  [9] grid_4.5.1         RColorBrewer_1.1-3 fastmap_1.2.0      Matrix_1.7-3      
## [13] cellranger_1.1.0   jsonlite_2.0.0     httr_1.4.7         mgcv_1.9-3        
## [17] purrr_1.1.0        crosstalk_1.2.2    viridisLite_0.4.2  jquerylib_0.1.4   
## [21] cli_3.6.5          rlang_1.1.6        crayon_1.5.3       splines_4.5.1     
## [25] bit64_4.6.0-1      withr_3.0.2        cachem_1.1.0       yaml_2.3.10       
## [29] tools_4.5.1        parallel_4.5.1     tzdb_0.5.0         vctrs_0.6.5       
## [33] R6_2.6.1           lifecycle_1.0.4    htmlwidgets_1.6.4  bit_4.6.0         
## [37] vroom_1.6.5        pkgconfig_2.0.3    pillar_1.11.0      bslib_0.9.0       
## [41] gtable_0.3.6       glue_1.8.0         xfun_0.52          tibble_3.3.1      
## [45] tidyselect_1.2.1   rstudioapi_0.19.0  farver_2.1.2       nlme_3.1-168      
## [49] htmltools_0.5.8.1  rmarkdown_2.29     labeling_0.4.3     compiler_4.5.1