Introducción y objetivo

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:

  • Base ancha y cima angosta (mediados del siglo XX): población joven, con alta fecundidad.
  • Base que se estrecha y centro que se ensancha (finales del siglo XX e inicios del XXI): descenso de la fecundidad y mayor peso de la población en edad de trabajar.
  • Cima que se ensancha (proyecciones hacia 2070): envejecimiento poblacional.

El objetivo de este documento es mostrar ese proceso para México con las proyecciones de población de CONAPO, en dos pasos:

  1. Gráficas estáticas de la pirámide en años seleccionados (cortes por década).
  2. Animación (GIF) que recorre todos los años de 1950 a 2070.

Fuente de datos: CONAPO, Conciliación Demográfica 1950-2019 y Proyecciones de la Población 2020-2070.

1 Preparación del entorno

1.1 Paquetes

Se usa pacman para instalar (si hace falta) y cargar todos los paquetes en un solo paso. Los paquetes se agrupan por función:

  • Manejo de datos: tidyverse, data.table, janitor.
  • Lectura y escritura de archivos: readxl, openxlsx, archive.
  • Gráficos y animación: showtext, gganimate, gifski.
  • Tablas y reporte: knitr.
if (!requireNamespace("pacman", quietly = TRUE)) {
  install.packages("pacman")
}

pacman::p_load(
  tidyverse,
  data.table,
  openxlsx,
  readxl,
  janitor,
  archive,
  showtext,
  gganimate,
  gifski,
  knitr
)

1.2 Función de descarga y descompresión

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)
}

1.3 Carpeta de trabajo

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)

2 Datos de población

2.1 Descarga y lectura de la base CONAPO

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

2.2 Limpieza y construcción de grupos de edad

Se preparan los datos para dibujar la pirámide:

  1. Nombres de columnas homogéneos con janitor::clean_names().
  2. Territorio: se filtra el nivel nacional. Para ver una entidad, cambie entidad_sel (por ejemplo, "Jalisco").
  3. Hombres en negativo: así sus barras se dibujan hacia la izquierda y las de las mujeres hacia la derecha, formando la pirámide.
  4. Grupos quinquenales de edad (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

2.3 Verificación de la columna de año

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

3 Pirámides poblacionales estáticas

3.1 Función para graficar un año

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

3.2 Pirámides por década, 1950-2070

Se grafican trece cortes, uno por década. Al compararlos en secuencia se observa la transición:

  • 1950-1970: base ancha; predominan niñas y niños.
  • 1980-2010: la base se estrecha por el descenso de la fecundidad y los grupos en edad de trabajar ganan peso (periodo del bono demográfico).
  • 2020-2070: el centro y la cima se ensanchan; aumenta la población adulta mayor (envejecimiento).
anios_objetivo <- seq(1950, 2070, by = 10)

graficos <- lapply(anios_objetivo, graficar_piramide)

for (g in graficos) {
  print(g)
}

4 Animación de la transición demográfica

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.

4.1 Gráfico base

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

4.2 Definición de la transición

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

4.3 Exportación del GIF

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.

4.4 Visualización del GIF

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

Notas de uso

  • Otra entidad federativa: cambie entidad_sel en el chunk limpieza-datos (por ejemplo, "Jalisco"). El título y el nombre del GIF se ajustan automáticamente.
  • Vista previa rápida de la animación (sin guardar el archivo):
animate(p_anim, fps = 10, width = 1000, height = 600, units = "px", res = 96)
  • Generar el documento desde la consola:
rmarkdown::render(
  "Transicion_demografica_II.Rmd",
  output_format = "html_document"
)