La transición demográfica es el paso de un régimen de alta natalidad y alta mortalidad a uno de baja natalidad y baja mortalidad. Su huella más visible está en la estructura por edad de la población, que se lee con una pirámide poblacional:
El objetivo de este documento es mostrar ese proceso para México con las proyecciones de población de CONAPO, en dos pasos:
Fuente de datos: CONAPO, Conciliación Demográfica 1950-2019 y Proyecciones de la Población 2020-2070.
Se usa pacman para instalar (si hace falta) y cargar
todos los paquetes en un solo paso. Los paquetes se agrupan por
función:
tidyverse,
data.table, janitor.readxl, openxlsx, archive.showtext,
gganimate, gifski.knitr.if (!requireNamespace("pacman", quietly = TRUE)) {
install.packages("pacman")
}
pacman::p_load(
tidyverse,
data.table,
openxlsx,
readxl,
janitor,
archive,
showtext,
gganimate,
gifski,
knitr
)
CONAPO distribuye la base en un archivo ZIP. Esta función lo descarga
solo si no existe previamente (evita descargas
repetidas), valida que el archivo no esté vacío ni corrupto (revisando
que su encabezado sea el de un ZIP, bytes PK) y lo
descomprime en la carpeta indicada.
download_and_unzip <- function(url, dir) {
# Crear directorio de destino
dir.create(dir, recursive = TRUE, showWarnings = FALSE)
# Ruta del archivo ZIP
destfile <- file.path(dir, basename(url))
# Descargar solamente si no existe
if (!file.exists(destfile)) {
download.file(url, destfile, mode = "wb", method = "libcurl")
}
# Verificar existencia y tamaño
if (!file.exists(destfile) || file.info(destfile)$size == 0) {
stop("El archivo ZIP no se descargó correctamente.")
}
# Verificar encabezado ZIP (los dos primeros bytes deben ser "PK")
con <- file(destfile, "rb")
encabezado <- readBin(con, "raw", n = 2)
close(con)
if (!identical(encabezado, as.raw(c(0x50, 0x4B)))) {
stop("El archivo descargado no es un ZIP válido.")
}
# Extraer archivos
archive::archive_extract(archive = destfile, dir = dir)
message("Archivo descargado y descomprimido correctamente.")
invisible(destfile)
}
Se crea la carpeta donde se guardan los datos descargados y, más adelante, el GIF final.
data_dir <- file.path("data", "unidad_1")
dir.create(data_dir, recursive = TRUE, showWarnings = FALSE)
Primero se busca si el Excel 0_Pob_Mitad_1950_2070.xlsx
ya está en la carpeta. Si no está, se descarga y descomprime el ZIP
oficial. Esta base contiene la población a mitad de
año, por año, entidad, sexo y edad simple, de 1950 a 2070.
# URL oficial de CONAPO
url_conapo <- paste0(
"https://conapo.segob.gob.mx/work/models/CONAPO/pry23/DB/",
"ConDem50a19_ProyPob20a70.zip"
)
# Directorio de almacenamiento
conapo_dir <- file.path(data_dir, "conapo")
dir.create(conapo_dir, recursive = TRUE, showWarnings = FALSE)
# Buscar Excel previamente descargado
buscar_excel <- function() {
list.files(
conapo_dir,
pattern = "0_Pob_Mitad_1950_2070\\.xlsx$",
recursive = TRUE,
full.names = TRUE
)
}
archivos_excel <- buscar_excel()
# Descargar y extraer si no existe el Excel
if (length(archivos_excel) == 0) {
download_and_unzip(url = url_conapo, dir = conapo_dir)
archivos_excel <- buscar_excel()
}
# Verificar que se encontró el Excel
if (length(archivos_excel) == 0) {
stop("No se encontró 0_Pob_Mitad_1950_2070.xlsx dentro del ZIP.")
}
xlsx_path <- archivos_excel[1]
message("Archivo Excel localizado: ", xlsx_path)
# Leer base de datos
conapo <- readxl::read_excel(xlsx_path, sheet = 1)
message("Base CONAPO cargada correctamente.")
dim(conapo)
## [1] 737660 7
names(conapo)
## [1] "RENGLON" "AÑO" "ENTIDAD" "CVE_GEO" "EDAD" "SEXO" "POBLACION"
head(conapo)
## # A tibble: 6 × 7
## RENGLON AÑO ENTIDAD CVE_GEO EDAD SEXO POBLACION
## <dbl> <dbl> <chr> <dbl> <dbl> <chr> <dbl>
## 1 1 1950 República Mexicana 0 0 Hombres 580871
## 2 2 1950 República Mexicana 0 0 Mujeres 568159
## 3 3 1950 República Mexicana 0 1 Hombres 525478
## 4 4 1950 República Mexicana 0 1 Mujeres 516674
## 5 5 1950 República Mexicana 0 2 Hombres 492568
## 6 6 1950 República Mexicana 0 2 Mujeres 483825
Se preparan los datos para dibujar la pirámide:
janitor::clean_names().entidad_sel (por ejemplo,
"Jalisco").gedad):
de 00-04 a 80-84, y 85+ como grupo abierto. Los códigos con cero a la
izquierda mantienen el orden correcto al graficar.# Territorio a graficar ("República Mexicana" = total nacional)
entidad_sel <- "República Mexicana"
# Nombre para títulos y nombre del archivo
nombre_territorio <- if (entidad_sel == "República Mexicana") "México" else entidad_sel
pob <- conapo %>%
janitor::clean_names() %>%
select(-any_of("renglon")) %>%
filter(entidad == entidad_sel) %>%
mutate(
# Hombres en negativo para la forma de pirámide
poblacion = if_else(sexo == "Hombres", poblacion * -1, poblacion),
# Grupos quinquenales de edad
gedad = case_when(
edad >= 0 & edad < 5 ~ "00-04",
edad >= 5 & edad < 10 ~ "05-09",
edad >= 10 & edad < 15 ~ "10-14",
edad >= 15 & edad < 20 ~ "15-19",
edad >= 20 & edad < 25 ~ "20-24",
edad >= 25 & edad < 30 ~ "25-29",
edad >= 30 & edad < 35 ~ "30-34",
edad >= 35 & edad < 40 ~ "35-39",
edad >= 40 & edad < 45 ~ "40-44",
edad >= 45 & edad < 50 ~ "45-49",
edad >= 50 & edad < 55 ~ "50-54",
edad >= 55 & edad < 60 ~ "55-59",
edad >= 60 & edad < 65 ~ "60-64",
edad >= 65 & edad < 70 ~ "65-69",
edad >= 70 & edad < 75 ~ "70-74",
edad >= 75 & edad < 80 ~ "75-79",
edad >= 80 & edad < 85 ~ "80-84",
edad >= 85 ~ "85+",
TRUE ~ NA_character_
)
)
head(pob)
## # A tibble: 6 × 7
## ano entidad cve_geo edad sexo poblacion gedad
## <dbl> <chr> <dbl> <dbl> <chr> <dbl> <chr>
## 1 1950 República Mexicana 0 0 Hombres -580871 00-04
## 2 1950 República Mexicana 0 0 Mujeres 568159 00-04
## 3 1950 República Mexicana 0 1 Hombres -525478 00-04
## 4 1950 República Mexicana 0 1 Mujeres 516674 00-04
## 5 1950 República Mexicana 0 2 Hombres -492568 00-04
## 6 1950 República Mexicana 0 2 Mujeres 483825 00-04
Según cómo venga escrito el encabezado en el Excel,
clean_names() puede producir ano,
año o anio. Este chunk detecta cuál existe, la
convierte a numérica con el nombre estándar anio y elimina
registros sin año válido.
if ("ano" %in% names(pob)) {
pob <- pob %>% mutate(anio = as.numeric(ano))
} else if ("año" %in% names(pob)) {
pob <- pob %>% mutate(anio = as.numeric(año))
} else if ("anio" %in% names(pob)) {
pob <- pob %>% mutate(anio = as.numeric(anio))
} else {
stop("No se encontró una columna de año en el dataset.")
}
# Conservar solo valores de año válidos
pob <- pob %>% filter(!is.na(anio))
Se define una función que recibe un año y devuelve su pirámide:
barras horizontales por grupo de edad, hombres a la
izquierda y mujeres a la derecha, con la
población en millones. Con labels = abs el eje muestra
valores positivos en ambos lados.
graficar_piramide <- function(anio_sel) {
pob %>%
filter(anio == anio_sel) %>%
ggplot(aes(x = gedad, y = poblacion / 1e6, fill = sexo)) +
geom_bar(stat = "identity") +
coord_flip() +
scale_fill_manual(values = c("Hombres" = "cyan4", "Mujeres" = "firebrick4")) +
scale_y_continuous(labels = abs) +
labs(
title = paste(nombre_territorio, "- Pirámide poblacional", anio_sel),
subtitle = "Cortes por década, 1950-2070",
y = "Millones de personas",
x = "",
fill = "Sexo",
caption = "Fuente: CONAPO (Conciliación Demográfica 1950-2019 y Proyecciones 2020-2070)"
) +
theme_minimal() +
theme(
text = element_text(size = 12, face = "bold"),
plot.title = element_text(size = 18, hjust = 0.5),
plot.subtitle = element_text(size = 14, hjust = 0.5),
legend.position = "bottom"
)
}
Se grafican trece cortes, uno por década. Al compararlos en secuencia se observa la transición:
anios_objetivo <- seq(1950, 2070, by = 10)
graficos <- lapply(anios_objetivo, graficar_piramide)
for (g in graficos) {
print(g)
}
Las gráficas estáticas muestran cortes aislados. La animación recorre todos los años de la proyección y permite ver el cambio como un proceso continuo.
Se construye la pirámide con todos los años y la
escala del eje horizontal fija (de -10 a 10 millones), de modo que los
cuadros sean comparables. El marcador {closest_state} es el
que gganimate reemplaza con el año de cada cuadro.
p_base <- ggplot(pob, aes(x = gedad, y = poblacion / 1e6, fill = sexo)) +
geom_bar(stat = "identity") +
coord_flip() +
scale_fill_manual(values = c("Hombres" = "cyan4", "Mujeres" = "firebrick4")) +
scale_y_continuous(
labels = abs, # muestra valores positivos en ambos lados
breaks = seq(-10, 10, 2) # ajustar según la escala de población
) +
labs(
title = paste(nombre_territorio, "- Pirámide poblacional {closest_state}"),
subtitle = "Proyección de población 1950-2070",
y = "Millones de personas",
x = "",
fill = "Sexo",
caption = "Fuente: CONAPO (Conciliación Demográfica 1950-2019 y Proyecciones 2020-2070)"
) +
theme_minimal() +
theme(
text = element_text(size = 10, face = "bold"),
plot.title = element_text(size = 15, hjust = 0.5),
plot.subtitle = element_text(size = 11, hjust = 0.5),
legend.position = "bottom"
)
Se indica a gganimate que cada año es un
estado de la animación. Las transiciones suaves
(enter_fade(), exit_fade() y
ease_aes()) evitan cambios bruscos entre un año y el
siguiente.
p_anim <- p_base +
transition_states(
anio,
transition_length = 1,
state_length = 1
) +
enter_fade() +
exit_fade() +
ease_aes("cubic-in-out")
Renderizar la animación es lento. Para no repetirlo
en cada ejecución, el GIF solo se genera si todavía no existe en la
carpeta de datos. Para forzar una nueva versión (por ejemplo, tras
cambiar colores o título), cambie regenerar_gif a
TRUE o borre el archivo.
El GIF se incrusta solo en la salida HTML. En PDF no es posible mostrar animaciones, por lo que este chunk no genera contenido en ese formato (para PDF se conservan las pirámides estáticas).
entidad_sel en el chunk limpieza-datos (por
ejemplo, "Jalisco"). El título y el nombre del GIF se
ajustan automáticamente.animate(p_anim, fps = 10, width = 1000, height = 600, units = "px", res = 96)
rmarkdown::render(
"Transicion_demografica_II.Rmd",
output_format = "html_document"
)