Explorando los datos del Censo 2024 de Población y Viviendas en Chile

Analizo el porcentaje de población por nivel educacional y región a modo de explicar como realizar cruces de tables con datos del Censo 2024 utilizando R y Apache Arrow.
Author

Mauricio “Pachá” Vargas S.

Published

December 4, 2025

If this post is useful to you I kindly ask a minimal donation on Buy Me a Coffee. It shall be used to continue my Open Source efforts.

You can send me questions for the blog using this form and subscribe to receive an email when there is a new post.

Un querido amigo me envío el siguiente mensaje: “https://censo2024.ine.gob.cl/resultados/ Salieron por fin para descargar”

Asumiré que quería que actualizara mi post del Censo 2017 en R utilizando estos nuevos datos.

Parto por los puntos que rescato del trabajo de los equipos del INE en la divulgación de los datos censales:

  1. Cuentan con documentación detallada
  2. Utilizan formatos abiertos (CSV y parquet)
  3. No usan el formato REDATAM

Dudo haber influido de manera estadísticamente significativa al cambio en la presentación de los datos. Espero creer que tras años de correspondencia con el INE y mi artículo de crítica constructiva al formato REDATAM, hayan decidido cambiar a formatos más amigables.

Para quienes deseen leer mi crítica, esta corresponde al siguiente artículo:

Vargas Sepúlveda, Mauricio and Barkai, Lital. 2025. “The REDATAM format and its challenges for data access and information creation in public policy.” Data & Policy 7 (January): e18. https://dx.doi.org/10.1017/dap.2025.4.

Los puntos anteriores se ven parcialmente eclipsados por algunos errores en la documentación:

  1. Menciona archivos tar.gz a la vez que los datos se han liberado comprimidos en zip.
  2. En distintas partes confunde KB con MB, lo que puede llevar a confusión sobre el tamaño real de los archivos.
  3. La documentación podría haber sido explicita al momento de proporcionar ejemplos de lectura de los datos y cruce de tablas.

Se podría haber realizado un mejor trabajo proporcionando los datos en una base de datos relacional, como lo es el caso de DuckDB, que proporciona una base de datos SQL embebida, de código abierto, rápida y ligera. Esto habría facilitado el trabajo de análisis de los datos y las asociaciones entre las variables en distintas tablas.

A modo de intentar realizar un aporte, a continuación muestro cómo obtener el porcentaje de población por nivel educacional y región. Para realizar esto debo unir tablas a modo de situar a cada persona en la región donde vive.

Parto por extraer los datos en R usando el paquete “archive” que ahorra bastantes dolores de cabeza con archivos zip codificados para un sistema operativo en específico:

if (!require(archive)) install.packages("archive", repos = "http://cran.r-project.org")
if (!require(arrow)) install.packages("arrow", repos = "http://cran.r-project.org")
if (!require(readxl)) install.packages("readxl", repos = "http://cran.r-project.org")
if (!require(dplyr)) install.packages("dplyr", repos = "http://cran.r-project.org")
if (!require(janitor)) install.packages("janitor", repos = "http://cran.r-project.org")
if (!require(stringr)) install.packages("stringr", repos = "http://cran.r-project.org")

library(archive)
library(arrow)
library(readxl)
library(dplyr)
library(janitor)
library(stringr)

url <- "https://storage.googleapis.com/bktdescargascenso2024/viv_hog_per_censo2024.zip"
zip <- paste0("2025/12/04/", basename(url))

if (!file.exists(zip)) {
  download.file(url, zip, mode = "wb")
}

archive_extract(zip, dir = dirname(zip))

Procedo a leer el diccionario de variables:

dic_personas <- read_excel("2025/12/04/diccionario_variables_censo2024.xlsx", sheet = "tabla_personas") |>
    clean_names()

dic_regiones <- read_excel("2025/12/04/diccionario_variables_censo2024.xlsx", sheet = "codigos_territoriales") |>
    clean_names()

La variable de educación que mide “Logro educativo de acuerdo con la Clasificación Internacional Normalizada de la Educación” es “cine11” en la tabla de personas.

dic_personas |>
  filter(nombre_variable == "cine11") |>

> dic_personas |>
+   filter(nombre_variable == "cine11")
# A tibble: 13 × 8
   entidad nombre_variable descripcion_de_la_varia…¹ valor etiqueta_de_categoria
   <chr>   <chr>           <chr>                     <chr> <chr>                
 1 Persona cine11          Logro educativo de acuer… 1     01: Nunca cursó un p…
 2 Persona cine11          Logro educativo de acuer… 2     02: Educación de la …
 3 Persona cine11          Logro educativo de acuer… 3     03: Primaria en form…
 4 Persona cine11          Logro educativo de acuer… 4     10: Educación primar…
 5 Persona cine11          Logro educativo de acuer… 5     14: Educación primar…
 6 Persona cine11          Logro educativo de acuer… 6     24: Educación secund…
 7 Persona cine11          Logro educativo de acuer… 7     25: Educación secund…
 8 Persona cine11          Logro educativo de acuer… 8     35: Educación tercia…
 9 Persona cine11          Logro educativo de acuer… 9     46: Grado de educaci…
10 Persona cine11          Logro educativo de acuer… 10    56: Nivel de maestrí…
11 Persona cine11          Logro educativo de acuer… 11    64: Nivel de doctora…
12 Persona cine11          Logro educativo de acuer… 12    98: Educación especi…
13 Persona cine11          Logro educativo de acuer… -99   No respuesta         
# ℹ abbreviated name: ¹​descripcion_de_la_variable
# ℹ 3 more variables: rango <chr>, universo <chr>, conteo <dbl>

La siguiente tabla muestra que, a diferencia del Censo 2017, no necesito asociar cada persona a un hogar y luego el hogar a una región geográfica, ya que la tabla de personas cuenta con la variable “region” que indica la región donde vive cada persona.

personas <- read_parquet("2025/12/04/personas_censo2024.parquet")

glimpse(personas)

> glimpse(personas)
Rows: 18,480,432
Columns: 63
$ id_vivienda              <int> 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 6, …
$ id_hogar                 <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
$ id_persona               <int> 1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 1, …
$ region                   <int> 5, 5, 5, 5, 4, 4, 4, 11, 11, 11, 1, 1, 1, 8, …
$ provincia                <int> 58, 58, 58, 58, 43, 43, 43, 112, 112, 112, 11
$ comuna                   <int> 5802, 5802, 5802, 5802, 4303, 4303, 4303, 112
$ comuna_bajo_umbral       <int> 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, …
$ area                     <int> 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, …
$ tipo_operativo           <int> 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, …
$ sexo                     <int> 2, 1, 2, 2, 1, 2, 1, 2, 1, 1, 1, 2, 1, 2, 1, …
$ edad                     <int> 80, 52, 45, 8, 69, 65, 58, -66, -66, -66, 73,…
$ edad_quinquenal          <int> 80, 50, 45, 5, 65, 65, 55, 30, 55, 5, 70, 70,…
$ parentesco               <int> 1, 11, 5, 12, 9, 7, 1, 1, 4, 5, 1, 2, 5, 1, 5
$ p23_est_civil            <int> 6, 8, 8, NA, 1, 1, 8, 2, 2, NA, 1, 1, 8, 8, 8
$ p24_lug_resid5           <int> 3, 2, 2, 2, 3, 3, 2, 3, 2, 3, 2, 2, 2, 2, 2, …
$ p24_lug_resid5_esp       <int> 13117, 5802, 5802, 5802, 4301, 4301, 4303, 10
$ p25_lug_nacimiento       <int> 2, 2, 2, 1, 2, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, …
$ p25_lug_nacimiento_rec   <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
$ p25_lug_nacimiento_esp   <int> 12101, 5101, 13120, 5802, 5109, 4303, 4303, -
$ p26_llegada_periodo      <int> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
$ p27_nacionalidad         <int> 1, 1, 1, 1, 1, 1, 1, -66, -66, -66, 1, 1, 1, …
$ p27_nacionalidad_esp     <int> 152, 152, 152, 152, 152, 152, 152, -66, -66, …
$ p27_nacionalidad_rec     <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
$ p28_autoid_pueblo        <int> 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 2, 2, …
$ p28_pueblo_pert          <int> NA, NA, NA, NA, NA, NA, NA, 1, NA, 1, NA, NA,…
$ p29_afrodescendencia_rec <int> 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, …
$ p29_afrodescendencia     <int> 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, …
$ p30_lengua_indigena      <int> 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, …
$ p30_lengua_indigena_rec  <int> 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, …
$ p31_religion             <int> 12, 12, 12, NA, 1, 1, 12, 2, 1, NA, 1, 1, 1, …
$ p31_religion_rec         <int> 2, 2, 2, NA, 1, 1, 2, 1, 1, NA, 1, 1, 1, 1, 1
$ p32a_dificultad_ver      <int> 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, …
$ p32b_dificultad_oir      <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
$ p32c_dificultad_mover    <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, …
$ p32d_dificultad_cogni    <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, …
$ p32e_dificultad_cuidado  <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, …
$ p32f_dificultad_comunic  <int> 1, 1, 1, 1, 1, 1, 1, 3, 1, 2, 1, 1, 1, 1, 1, …
$ discapacidad             <int> 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, …
$ p33_edu_asiste           <int> 2, 2, 2, 1, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 1, …
$ asistencia_parv          <int> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
$ asistencia_basica        <int> NA, NA, NA, 1, NA, NA, NA, NA, NA, -66, NA, N…
$ asistencia_media         <int> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
$ asistencia_superior      <int> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
$ p37_alfabet              <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, …
$ escolaridad              <int> 17, 14, 12, 2, 12, 12, 15, 8, 5, 3, 8, 8, 16,…
$ cine11                   <int> 9, 6, 6, 3, 6, 6, 6, 5, 3, 3, 5, 5, 6, 5, 5, …
$ sit_fuerza_trabajo       <int> 3, 1, 1, NA, 3, 3, 1, 1, 1, NA, 1, 3, 1, 3, 3
$ p40_cise_rec             <int> NA, 1, 2, NA, NA, NA, 1, 2, 1, NA, 1, NA, 2, …
$ depend_econ_deficit_hab  <int> 1, 1, 1, 2, 1, 2, 1, 1, 1, 2, 1, 1, 1, 1, 2, …
$ cod_ciuo                 <int> NA, 7, 2, NA, NA, NA, 7, 5, 7, NA, 1, NA, 3, …
$ cod_caenes               <chr> NA, "F", "P", NA, NA, NA, "F", "I", "F", NA, …
$ p44_lug_trab             <int> NA, 5, 2, NA, NA, NA, 2, 1, 1, NA, 2, NA, 2, …
$ p44_lug_trab_esp         <int> NA, 998, 5802, NA, NA, NA, 4303, 11202, 11202
$ p45_medio_transporte     <int> NA, 2, 3, NA, NA, NA, 2, NA, NA, NA, 1, NA, 2
$ p46a_tot_hijs_nac        <int> 3, NA, 1, NA, NA, 3, NA, 2, NA, NA, NA, 3, NA…
$ p46b_hijas_nac           <int> 2, NA, 1, NA, NA, 0, NA, 1, NA, NA, NA, 0, NA…
$ p46c_hijos_nac           <int> 1, NA, 0, NA, NA, 3, NA, 1, NA, NA, NA, 3, NA…
$ p47a_tot_hijs_sobrev     <int> 3, NA, 1, NA, NA, 2, NA, 2, NA, NA, NA, 3, NA…
$ p47b_hijas_sobrev        <int> 2, NA, 1, NA, NA, -99, NA, 1, NA, NA, NA, 0, …
$ p47c_hijos_sobrev        <int> 1, NA, 0, NA, NA, -99, NA, 1, NA, NA, NA, 3, …
$ p48_anio_nac_uh          <int> 1978, NA, 2015, NA, NA, 1984, NA, 2014, NA, N…
$ p48_mes_nac_uh           <int> 7, NA, 9, NA, NA, 6, NA, 12, NA, NA, NA, 10, …
$ div_genero               <int> 2, 2, 2, NA, -66, -66, -66, -66, -66, NA, 2, …

A partir de estos debo asociar códigos de región a nombres de región. Esto es un punto favorable, podría haber sido mucho peor. Para esto uso el diccionario de variables pero el la unión con la tabla región arroja una advertencia de algunas regiones no tienen una correspondencia uno a uno:

personas_educacion <- personas |>
  group_by(region, cine11) |>
  summarise(total = n(), .groups = "drop") |>
  collect() |>

  inner_join(
    dic_personas |>
        filter(nombre_variable == "cine11") |>
        select(cine11 = valor, nivel_educacional = etiqueta_de_categoria) |>
        mutate(cine11 = as.integer(cine11))
  ) |>

  inner_join(
    dic_regiones |>
        select(region = codigo_territorial, nombre_region = territorio) |>
        mutate(region = as.integer(region))
  )

Explorando el diccionario, el problema es el siguiente:

> dic_regiones |>
+     filter(codigo_territorial %in% 1:16) |>
+     group_by(codigo_territorial) |>
+     filter(n() > 1)

# A tibble: 4 × 2
# Groups:   codigo_territorial [2]
  codigo_territorial territorio                               
               <dbl> <chr>                                    
1                 11 Aysén del General Carlos Ibáñez del Campo
2                 11 Iquique                                  
3                 14 Los Ríos                                 
4                 14 Del Tamarugal 

Dado que Iquique y Aisén se encuentran a 3.600 kilómetros de distancia, asumo que el error está en el diccionario e intento con el código de comuna:

personas_educacion <- personas |>
  group_by(comuna, cine11) |>
  summarise(total = n(), .groups = "drop") |>
  collect() |>

  inner_join(
    dic_personas |>
        filter(nombre_variable == "cine11") |>
        select(cine11 = valor, nivel_educacional = etiqueta_de_categoria) |>
        mutate(cine11 = as.integer(cine11))
  ) |>

  inner_join(
    dic_regiones |>
        select(comuna = codigo_territorial, nombre_comuna = territorio) |>
        mutate(comuna = as.integer(comuna))
  )

A partir del código de comuna, asumiendo que este respeta el estándar oficial, puedo asociar cada comuna a una región utilizando mi conocimiento previo del país:

personas_educacion <- personas_educacion |>
  mutate(
    comuna = str_pad(comuna, width = 5, side = "left", pad = "0"),
    region = str_sub(comuna, 1, 2),
    nombre_region = case_when(
      region == "01" ~ "Región de Arica y Parinacota",
      region == "02" ~ "Región de Tarapacá",
      region == "03" ~ "Región de Antofagasta",
      region == "04" ~ "Región de Atacama",
      region == "05" ~ "Región de Coquimbo",
      region == "06" ~ "Región de Valparaíso",
      region == "07" ~ "Región Metropolitana de Santiago",
      region == "08" ~ "Región del Libertador General Bernardo O'Higgins",
      region == "09" ~ "Región del Maule",
      region == "10" ~ "Región de Ñuble",
      region == "11" ~ "Región del Biobío",
      region == "12" ~ "Región de La Araucanía",
      region == "13" ~ "Región de Los Ríos",
      region == "14" ~ "Región de Los Lagos",
      region == "15" ~ "Región de Aysén del General Carlos Ibáñez del Campo",
      region == "16" ~ "Región de Magallanes y de la Antártica Chilena",
      TRUE ~ "Desconocida"
    )
  )

A modo de verificación, tomemos una comuna de altos ingresos y una de bajos ingresos, como lo es el caso de Vitacura y La Pintana:

personas_educacion |> 
    filter(nombre_comuna == "Vitacura") |>
    group_by(nivel_educacional) |>
    summarise(n_vitacura = sum(total)) |>
    inner_join(
      personas_educacion |> 
        filter(nombre_comuna == "La Pintana") |>
        group_by(nivel_educacional) |>
        summarise(n_la_pintana = sum(total))
    )

> personas_educacion |> 
+     filter(nombre_comuna == "Vitacura") |>
+     group_by(nivel_educacional) |>
+     summarise(n_vitacura = sum(total)) |>
+     inner_join(
+       personas_educacion |> 
+         filter(nombre_comuna == "La Pintana") |>
+         group_by(nivel_educacional) |>
+         summarise(n_la_pintana = sum(total))
+     )
Joining with `by = join_by(nivel_educacional)`
# A tibble: 13 × 3
   nivel_educacional                                     n_vitacura n_la_pintana
   <chr>                                                      <int>        <int>
 1 01: Nunca cursó un programa educativo                       1916         6463
 2 02: Educación de la primera infancia (incluye la for…       4955         8938
 3 03: Primaria en forma parcial (sin conclusión del ni…       5738        25842
 4 10: Educación primaria (nivel 1)                            2443        14269
 5 14: Educación primaria (nivel 2), con orientación ge…       6202        43583
 6 24: Educación secundaria, con orientación general          14926        33280
 7 25: Educación secundaria, con orientación vocacional         842        26427
 8 35: Educación terciaria de ciclo corto, con orientac…       4020         7375
 9 46: Grado de educación terciaria o nivel equivalente…      32349         6242
10 56: Nivel de maestría, especialización o equivalente…      10567          123
11 64: Nivel de doctorado o equivalente, con orientació…       1077           11
12 98: Educación especial o diferencial                         181         1541
13 No respuesta                                                1204         1327

Comparando las proporciones y la cantidad de personas en cada nivel educacional entre ambas comunas, se observa que los códigos de comuna y región han sido correctamente asociados. Vitacura presenta una mayor proporción de personas con niveles educacionales más altos en comparación con La Pintana, lo cual es consistente con las diferencias socioeconómicas entre ambas comunas.

En lo personal me gustaría observar que el próximo presidente de Chile implemente reformas educacionales que lleven a que las personas de La Pintana y otras comunas de bajos puedan acceder a grados como maestría y doctorado si es su interés y hay una necesidad social (o nacional) de becar a personas para cubrir vacíos en áreas donde el país requiere de conocimiento especializado y no en lo que sea que las personas quieran estudiar sin considerar las necesidades del país y la empleabilidad futura de tales becados. De manera similar tengo el deseo de que quienes no acceden a la educación terciaria puedan igualmente acceder a educación que les permita descubrir sus intereses y oficios que les permitan llevar al máximo sus capacidades y talentos.

Espero que este post tan modesto llegue a José Antonio Kast o alguien de su equipo. No busco consultorías pagadas, para eso ya tengo mi nicho en el sector privado. Busco poder apotar a mi país y que este me ayude de vuelta de ser menos gruñón respecto de los errores evidentes que encontré en el censo.