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?
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.
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.
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 |
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 |
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 |
Í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 |
% 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% |
% 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% |
% 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% |
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.
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
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.
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.
| 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. |
| 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. |
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)))
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))
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))
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
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
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
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
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.
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
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")))
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 |
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.
| 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 |
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.
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))
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í.)
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))
cat(sprintf("2. **Mayor tasa por habitante.** Los municipios con mayor tasa son %s.\n",
paste(top3_tasa, collapse = ", ")))
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)]))
cat(sprintf("4. **Prioridad.** Combinando tasa y marginación, los municipios que más requieren atención son %s.\n",
paste(top3_prio, collapse = ", ")))
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 = ", ")))
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.
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.
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.
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/ |
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 |
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
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
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)
| 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_MINaANIO_FIN; el histórico (desdeANIO_HIST) alimenta la tendencia y el balance.
Para cada municipio se calcula:
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
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