Censo 2024 de Población y Viviendas en Chile con Códigos Territoriales Corregidos

[This article was first published on https://pacha.dev/blog, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
pacha.dev/blog

Censo 2024 de Población y Viviendas en Chile con Códigos Territoriales Corregidos

He liberado los datos censales en formato DuckDB para proporcionar los datos censales con los códigos de región, provincia y comuna corregidos y además con llaves foráneas para ayudar en los cruces de tablas.
Author

Mauricio “Pachá” Vargas S.

Published

December 5, 2025

Si esta publicación te resulta útil, te agradecería que hicieras una pequeña donación en Buy Me a Coffee. La utilizaré para continuar con mis iniciativas de código abierto.

Puedes enviarme preguntas para el blog utilizando este formulario y suscribirse para recibir un correo electrónico cuando haya una nueva publicación.

Luego de analizar los datos censales del día 4 de diciembre en este post, el mismo día que se liberaron los microdatos del censo, decidí utilizar dichos datos para dar un ejemplo de flujos migratorios en el contexto del modelo de gravedad estructural.

Tuve que reemplazar valores en “p27_nacionalidad_nac” (lugar de nacimiento), “p27_nacionalidad_esp” (nacionalidad) y “p44_lug_trab_esp” (lugar de trabajo) por -999 ya que “10” no es un valor en los códigos territoriales específicos.La necesidad de utilizar -999 (no aplica) en las llaves foráneas es para distinguir “NA”/“NULL” del caso -99 (no responde), pues estas no admiten valores nulos.

Dicho trabajo adicional no debería ser realizado si quienes publican estos datos revisaran dos veces y publicaran una, pero dado que no es así, he decidido compartir mis correcciones para que otros puedan beneficiarse de ellas y no perder tiempo corrigiendo los mismos errores.

Paso 1: Instalación y carga de paquetes

Utilicé los siguientes paquetes de R para manejar la descarga, lectura, limpieza y organización de los datos:

  • archive: Extraer archivos comprimidos (ZIP, 7z, etc.)
  • arrow: Leer archivos Parquet
  • duckdb: Leer/escribir de datos SQL portable
  • readxl: Leer archivos Excel
  • dplyr: Manipulación de datos (filtrar, agrupar, resumir)
  • dbplyr: Uso sintaxis de dplyr con SQL
  • janitor: Limpiar nombres de columnas
  • stringr: Manipulación de texto
  • purrr: Iteraciones (e.g., copiar una región a la vez)
# 1: packages ----

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(duckdb)) install.packages("duckdb", 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(dbplyr)) install.packages("dbplyr", 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")
if (!require(purrr)) install.packages("purrr", repos = "http://cran.r-project.org")

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

Paso 2: Descarga y lectura de datos brutos

Descargué los microdatos del Censo 2024 desde el servidor del INE. El archivo ZIP contiene tres archivos Parquet (personas, hogares, viviendas) y un diccionario en Excel que explica qué significa cada variable y código.

# 2: raw data ----

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

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

archive_extract(zip, dir = dirname(zip))

Creé una función auxiliar para leer las distintas hojas del diccionario Excel. La función clean_names() convierte los nombres de las columnas a un formato consistente (minúsculas, sin espacios).

read_dic <- function(file = "diccionario_variables_censo2024.xlsx", sheet) {
  read_excel(file, sheet = sheet) |>
    clean_names()
}

codigos_personas <- read_dic(sheet = "tabla_personas")

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

codigos_hogares <- read_dic(sheet = "tabla_hogares")

codigos_viviendas <- read_dic(sheet = "tabla_viviendas")

codigos_territoriales <- read_dic(sheet = "codigos_territoriales")

codigos_territoriales_especificos <- read_dic(sheet = "cod_territoriales_especificos")

Leí los archivos y debo reconocer que el INE tomó unabuena decisión al proporcionar estos archivos además de CSV. El formato Parquet es muy eficiente para datos grandes porque usa compresión, utlizando alrededor de 300 MB para el censo completo.

personas <- read_parquet("personas_censo2024.parquet")

hogares <- read_parquet("hogares_censo2024.parquet")

viviendas <- read_parquet("viviendas_censo2024.parquet")

La siguiente función limpia todas las columnas de texto: elimina espacios múltiples, saltos de línea y espacios al inicio o final. Esto es importante porque los datos brutos tienen inconsistencias de formato.

# for all chr-type columns, replace multiple spaces / new lines with single space, trim leading/trailing spaces
clean_chr_cols <- function(df) {
  chr_cols <- names(df)[sapply(df, is.character)]
  df <- df |> mutate(across(all_of(chr_cols), ~ str_squish(str_trim(.))))
  return(df)
}

codigos_personas <- clean_chr_cols(codigos_personas) |>
  select(-entidad)

codigos_hogares <- clean_chr_cols(codigos_hogares) |>
  select(-entidad)

codigos_viviendas <- clean_chr_cols(codigos_viviendas) |>
  select(-entidad)

personas <- clean_chr_cols(personas)
hogares <- clean_chr_cols(hogares)
viviendas <- clean_chr_cols(viviendas)

El problema con los códigos de región

Este es el problema que detecté en el post del día 4 de diciembre. Los códigos de región en los datos no son únicos. Esto rompe los cruces directos con la tabla de códigos territoriales.

# 3: problem with "region" ----

glimpse(personas)

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

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

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

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

Como los códigos de comuna son únicos (5 dígitos), podemos usarlos para hacer los cruces y luego derivar la región a partir de los primeros 2 dígitos del código de comuna.

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

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

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

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 == "15" ~ "Región de Arica y Parinacota",
      region == "01" ~ "Región de Tarapacá",
      region == "02" ~ "Región de Antofagasta",
      region == "03" ~ "Región de Atacama",
      region == "04" ~ "Región de Coquimbo",
      region == "05" ~ "Región de Valparaíso",
      region == "13" ~ "Región Metropolitana de Santiago",
      region == "06" ~ "Región del Libertador General Bernardo O'Higgins",
      region == "07" ~ "Región del Maule",
      region == "16" ~ "Región de Ñuble",
      region == "08" ~ "Región del Bío Bío",
      region == "09" ~ "Región de La Araucanía",
      region == "14" ~ "Región de Los Ríos",
      region == "10" ~ "Región de Los Lagos",
      region == "11" ~ "Región de Aysén del General Carlos Ibáñez del Campo",
      region == "12" ~ "Región de Magallanes y de la Antártica Chilena",
      TRUE ~ "Desconocida"
    )
  )

Corrección definitiva de los códigos

Esta es la corrección principal. El problema es que los códigos de comuna de 4 dígitos (como 1101 para Iquique) deben tener un cero adelante para quedar como 5 dígitos (01101). Además, las columnas de códigos territoriales específicos (lugar de residencia hace 5 años, lugar de nacimiento, nacionalidad, lugar de trabajo) también necesitan esta corrección.

Usamos -999 para marcar valores que no aplican (por ejemplo, lugar de trabajo para menores de edad), distinguiéndolo de -99 que significa “no responde”.

# 4: proper fix ----

personas <- personas |>
  mutate(
    comuna = str_pad(comuna, width = 5, side = "left", pad = "0"),
    # NOTE: region and provincia removed - derive with substr(comuna, 1, 2) and substr(comuna, 1, 3)
    # Convert to VARCHAR and pad to 5 digits for FK to codigos_territoriales_especificos
    # First clean, then only pad codes of length 4 (comunas that need leading zero)
    # Replace NA with "-999" for consistency with census data and FK constraints
    p24_lug_resid5_esp = case_when(
      is.na(p24_lug_resid5_esp) ~ "-999",
      TRUE ~ str_squish(str_trim(as.character(p24_lug_resid5_esp)))
    ),

    p24_lug_resid5_esp = case_when(
      p24_lug_resid5_esp == "-999" ~ "-999",
      str_detect(p24_lug_resid5_esp, "^[0-9]+$") & str_length(p24_lug_resid5_esp) == 4 ~ str_pad(p24_lug_resid5_esp, width = 5, side = "left", pad = "0"),
      TRUE ~ p24_lug_resid5_esp
    ),
    
    p25_lug_nacimiento_esp = case_when(
      is.na(p25_lug_nacimiento_esp) ~ "-999",
      TRUE ~ str_squish(str_trim(as.character(p25_lug_nacimiento_esp)))
    ),
    
    p25_lug_nacimiento_esp = case_when(
      p25_lug_nacimiento_esp == "-999" ~ "-999",
      str_detect(p25_lug_nacimiento_esp, "^[0-9]+$") & str_length(p25_lug_nacimiento_esp) == 4 ~ str_pad(p25_lug_nacimiento_esp, width = 5, side = "left", pad = "0"),
      TRUE ~ p25_lug_nacimiento_esp
    ),
    
    p27_nacionalidad_esp = case_when(
      is.na(p27_nacionalidad_esp) ~ "-999",
      TRUE ~ str_squish(str_trim(as.character(p27_nacionalidad_esp)))
    ),
    
    p27_nacionalidad_esp = case_when(
      p27_nacionalidad_esp == "-999" ~ "-999",
      str_detect(p27_nacionalidad_esp, "^[0-9]+$") & str_length(p27_nacionalidad_esp) == 4 ~ str_pad(p27_nacionalidad_esp, width = 5, side = "left", pad = "0"),
      TRUE ~ p27_nacionalidad_esp
    ),
    
    p44_lug_trab_esp = case_when(
      is.na(p44_lug_trab_esp) ~ "-999",
      TRUE ~ str_squish(str_trim(as.character(p44_lug_trab_esp)))
    ),
    
    p44_lug_trab_esp = case_when(
      p44_lug_trab_esp == "-999" ~ "-999",
      str_detect(p44_lug_trab_esp, "^[0-9]+$") & str_length(p44_lug_trab_esp) == 4 ~ str_pad(p44_lug_trab_esp, width = 5, side = "left", pad = "0"),
      TRUE ~ p44_lug_trab_esp
    )
  ) |>
  select(-any_of(c("region", "provincia")))

Los códigos de región también necesitan estandarización. Por ejemplo, la Región de Tarapacá debería ser “01” (no “1”), y las comunas deben tener 5 dígitos. Además, hay que corregir manualmente algunos casos donde el mismo código se usaba para entidades diferentes.

codigos_territoriales <- codigos_territoriales |>
  mutate(
    codigo_territorial = case_when(
      codigo_territorial <= 16 ~ str_pad(codigo_territorial, width = 2, side = "left", pad = "0"),
      codigo_territorial > 16 & codigo_territorial <= 99 ~ str_pad(codigo_territorial, width = 3, side = "left", pad = "0"),
      str_length(codigo_territorial) == 4 ~ str_pad(codigo_territorial, width = 5, side = "left", pad = "0"),
      TRUE ~ as.character(codigo_territorial)
    ),
    codigo_territorial = case_when(
      territorio == "Arica y Parinacota" & str_length(codigo_territorial) == 2 ~ "15",
      territorio == "Tarapacá" & str_length(codigo_territorial) == 2 ~ "01",
      territorio == "Iquique" & str_length(codigo_territorial) == 2 ~ "011",
      territorio == "Del Tamarugal" & str_length(codigo_territorial) == 2 ~ "014",
      territorio == "Antofagasta" & str_length(codigo_territorial) == 2 ~ "02",
      territorio == "Atacama" & str_length(codigo_territorial) == 2 ~ "03",
      territorio == "Coquimbo" & str_length(codigo_territorial) == 2 ~ "04",
      territorio == "Valparaíso" & str_length(codigo_territorial) == 2 ~ "05",
      territorio == "Metropolitana de Santiago" & str_length(codigo_territorial) == 2 ~ "13",
      territorio == "Libertador General Bernardo O'Higgins" & str_length(codigo_territorial) == 2 ~ "06",
      territorio == "Maule" & str_length(codigo_territorial) == 2 ~ "07",
      territorio == "Ñuble" & str_length(codigo_territorial) == 2 ~ "16",
      territorio == "Biobío" & str_length(codigo_territorial) == 2 ~ "08",
      territorio == "La Araucanía" & str_length(codigo_territorial) == 2 ~ "09",
      territorio == "Los Ríos" & str_length(codigo_territorial) == 2 ~ "14",
      territorio == "Los Lagos" & str_length(codigo_territorial) == 2 ~ "10",
      territorio == "Aysén del General Carlos Ibáñez del Campo" & str_length(codigo_territorial) == 2 ~ "11",
      territorio == "Magallanes y de la Antártica Chilena" & str_length(codigo_territorial) == 2 ~ "12",
      TRUE ~ codigo_territorial
    )
  )

codigos_territoriales |>
  filter(str_length(codigo_territorial) == 2) |>
  arrange(codigo_territorial) |>
  print(n = 30)

codigos_territoriales |>
  distinct()

Para los códigos específicos (países, comunas para variables de migración/trabajo) es similar:

# similar for codigos_territoriales_especificos
# len 4 -> add 0, and ensure character type

codigos_territoriales_especificos <- codigos_territoriales_especificos |>
  mutate(
    codigo_especifico = as.character(codigo_especifico),
    codigo_especifico = case_when(
      str_length(codigo_especifico) == 4 ~ str_pad(codigo_especifico, width = 5, side = "left", pad = "0"),
      TRUE ~ codigo_especifico
    )
  )

Las columnas de lugar de residencia, nacimiento, nacionalidad y trabajo pueden contener tanto códigos de comuna chilenas como códigos de países. Para que las llaves foráneas funcionen, necesitamos una tabla unificada que contenga todos los códigos posibles.

# The p24, p25, p27, p44 columns contain BOTH comuna codes (from codigos_territoriales)
# AND country/special codes (from codigos_territoriales_especificos)
# We need to add all comuna codes to codigos_territoriales_especificos for FK to work
# Also add special codes: -66 (anonimizado), -99 (no respuesta)
codigos_territoriales_especificos <- codigos_territoriales_especificos |>
  bind_rows(
    codigos_territoriales |>
      filter(str_length(codigo_territorial) == 5) |>
      select(codigo_especifico = codigo_territorial, territorio_especifico = territorio)
  ) |>
  bind_rows(
    tibble(
      codigo_especifico = c("-66", "-99", "-999"),
      territorio_especifico = c("Anonimizado", "No respuesta", "No aplica")
    )
  ) |>
  distinct(codigo_especifico, .keep_all = TRUE)

Organización en base de datos SQL (DuckDB)

Exporté los datos a una base de datos DuckDB. Esto tiene varias ventajas:

  1. Integridad referencial: Las llaves foráneas garantizan que no haya códigos inválidos (esto me permitió detectar errores y actualizar el código)
  2. Eficiencia: DuckDB está optimizado para consultas analíticas
  3. Portabilidad: Un solo archivo que contiene toda la base de datos
# 5: organize in SQL ---

# same fix as for personas

hogares <- hogares |>
  mutate(
    comuna = str_pad(comuna, width = 5, side = "left", pad = "0")
  ) |>
  select(-any_of(c("region", "provincia")))

viviendas <- viviendas |>
  mutate(
    comuna = str_pad(comuna, width = 5, side = "left", pad = "0")
  ) |>
  select(-any_of(c("region", "provincia")))

# persona: id_hogar -> hogar: id_hogar
# persona: id_vivienda -> vivienda: id_vivienda

length(unique(personas$id_hogar)) # -> add comuna to deambiguate

Escribí las tablas corregidas en una nueva base de datos DuckDB:

unlink("censo2024_corregido.duckdb")
unlink("censo2024_corregido.duckdb.wal")

con <- dbConnect(duckdb::duckdb(), "censo2024_corregido.duckdb", read_only = FALSE)

dbWriteTable(con, "personas", personas, overwrite = TRUE)
dbWriteTable(con, "hogares", hogares, overwrite = TRUE)
dbWriteTable(con, "viviendas", viviendas, overwrite = TRUE)

dbWriteTable(con, "codigos_personas", codigos_personas, overwrite = TRUE)
dbWriteTable(con, "codigos_hogares", codigos_hogares, overwrite = TRUE)
dbWriteTable(con, "codigos_viviendas", codigos_viviendas, overwrite = TRUE)

dbWriteTable(con, "codigos_territoriales", codigos_territoriales, overwrite = TRUE)
dbWriteTable(con, "codigos_territoriales_especificos", codigos_territoriales_especificos, overwrite = TRUE)

gc()

A diferencia de PostgreSQL, DuckDB no permite agregar restricciones a tablas existentes con ALTER TABLE, así que debí recrear las tablas con las restricciones definidas desde el inicio.

# DuckDB doesn't support ALTER TABLE for adding constraints
# We need to recreate tables with constraints using CREATE TABLE ... AS

# First, add primary keys by recreating tables
dbExecute(con, "
  CREATE OR REPLACE TABLE codigos_territoriales_new (
    codigo_territorial VARCHAR PRIMARY KEY,
    territorio VARCHAR
  )
")

# Note: codigo_territorial uses VARCHAR because it has variable length (2, 3, or 5 chars)
# comuna columns in main tables will be converted to CHAR(5) for efficiency
dbExecute(con, "INSERT INTO codigos_territoriales_new SELECT * FROM codigos_territoriales")
dbExecute(con, "DROP TABLE codigos_territoriales")
dbExecute(con, "ALTER TABLE codigos_territoriales_new RENAME TO codigos_territoriales")

dbExecute(con, "
  CREATE OR REPLACE TABLE codigos_territoriales_especificos_new (
    codigo_especifico VARCHAR PRIMARY KEY,
    territorio_especifico VARCHAR
  )
")
dbExecute(con, "INSERT INTO codigos_territoriales_especificos_new SELECT * FROM codigos_territoriales_especificos")
dbExecute(con, "DROP TABLE codigos_territoriales_especificos")
dbExecute(con, "ALTER TABLE codigos_territoriales_especificos_new RENAME TO codigos_territoriales_especificos")

Luego creé las tablas principales (viviendas, hogares, personas) con sus llaves primarias y foráneas. Esto garantiza la integridad de los datos (e.g. que no existan valores en la tabla de personas u otra tabla que no están no descritos en las tablas de códigos territoriales).

# Get all column names and types from hogares
viviendas_schema <- dbGetQuery(con, "DESCRIBE viviendas")
hogares_schema <- dbGetQuery(con, "DESCRIBE hogares")
personas_schema <- dbGetQuery(con, "DESCRIBE personas")

# Build the column definitions dynamically
# Convert comuna from VARCHAR to CHAR(5) for fixed-width efficiency
viviendas_cols <- paste(
  sapply(1:nrow(viviendas_schema), function(i) {
    col_name <- viviendas_schema$column_name[i]
    col_type <- viviendas_schema$column_type[i]
    # Use CHAR(5) for comuna - fixed width is more efficient
    if (col_name == "comuna") col_type <- "CHAR(5)"
    paste(col_name, col_type)
  }),
  collapse = ",\n    "
)

hogares_cols <- paste(
  sapply(1:nrow(hogares_schema), function(i) {
    col_name <- hogares_schema$column_name[i]
    col_type <- hogares_schema$column_type[i]
    if (col_name == "comuna") col_type <- "CHAR(5)"
    paste(col_name, col_type)
  }),
  collapse = ",\n    "
)

personas_cols <- paste(
  sapply(1:nrow(personas_schema), function(i) {
    col_name <- personas_schema$column_name[i]
    col_type <- personas_schema$column_type[i]
    if (col_name == "comuna") col_type <- "CHAR(5)"
    paste(col_name, col_type)
  }),
  collapse = ",\n    "
)

# Create new table with PK and FK
dbExecute(con, paste0("
  CREATE OR REPLACE TABLE viviendas_new (
    ", viviendas_cols, ",
    FOREIGN KEY (comuna) REFERENCES codigos_territoriales (codigo_territorial),
    PRIMARY KEY (id_vivienda, comuna)
  )
"))

dbExecute(con, paste0("
  CREATE OR REPLACE TABLE hogares_new (
    ", hogares_cols, ",
    FOREIGN KEY (comuna) REFERENCES codigos_territoriales (codigo_territorial),
    PRIMARY KEY (id_hogar, id_vivienda, comuna)
  )
"))

# NULLs replaced with "-999" to maintain FK constraints
dbExecute(con, paste0("
  CREATE OR REPLACE TABLE personas_new (
    ", personas_cols, ",
    FOREIGN KEY (comuna) REFERENCES codigos_territoriales (codigo_territorial),
    FOREIGN KEY (p24_lug_resid5_esp) REFERENCES codigos_territoriales_especificos (codigo_especifico),
    FOREIGN KEY (p25_lug_nacimiento_esp) REFERENCES codigos_territoriales_especificos (codigo_especifico),
    FOREIGN KEY (p27_nacionalidad_esp) REFERENCES codigos_territoriales_especificos (codigo_especifico),
    FOREIGN KEY (p44_lug_trab_esp) REFERENCES codigos_territoriales_especificos (codigo_especifico),
    PRIMARY KEY (id_persona, id_hogar, id_vivienda, comuna)
  )
"))

Inserté los datos en las tablas con restricciones y eliminé las tablas originales.

dbExecute(con, "INSERT INTO viviendas_new SELECT * FROM viviendas")
dbExecute(con, "DROP TABLE viviendas")
dbExecute(con, "ALTER TABLE viviendas_new RENAME TO viviendas")

tbl(con, "codigos_territoriales_especificos") |>
  filter(codigo_especifico == "13104")

tbl(con, "codigos_territoriales_especificos") |>
  filter(codigo_especifico == "07101")

dbExecute(con, "INSERT INTO hogares_new SELECT * FROM hogares")
dbExecute(con, "DROP TABLE hogares")
dbExecute(con, "ALTER TABLE hogares_new RENAME TO hogares")

Durante la migración descubrimos que el código “10” aparece en algunas columnas pero no es un código territorial escpecífico válido. Lo reemplacé por “-999” (no aplica).

# Find all unique codes in p24/p25/p27/p44 that are NOT in codigos_territoriales_especificos
missing_codes <- dbGetQuery(con, "
  SELECT DISTINCT code FROM (
    SELECT DISTINCT p24_lug_resid5_esp AS code FROM personas
    UNION
    SELECT DISTINCT p25_lug_nacimiento_esp FROM personas
    UNION
    SELECT DISTINCT p27_nacionalidad_esp FROM personas
    UNION
    SELECT DISTINCT p44_lug_trab_esp FROM personas
  ) t
  WHERE code NOT IN (SELECT codigo_especifico FROM codigos_territoriales_especificos)
  ORDER BY code
")

# code 10 is not valid, replace with -999 in personas
dbSendQuery(con, "
  UPDATE personas
  SET p24_lug_resid5_esp = '-999'
  WHERE p24_lug_resid5_esp = '10'
")
dbSendQuery(con, "
  UPDATE personas
  SET p25_lug_nacimiento_esp = '-999'
  WHERE p25_lug_nacimiento_esp = '10'
")
dbSendQuery(con, "
  UPDATE personas
  SET p27_nacionalidad_esp = '-999'
  WHERE p27_nacionalidad_esp = '10'
")
dbSendQuery(con, "
  UPDATE personas
  SET p44_lug_trab_esp = '-999'
  WHERE p44_lug_trab_esp = '10'
")

Para evitar problemas de memoria y facilitar la depuración, inserté los datos de personas región por región.

regiones <- dbGetQuery(con, "SELECT DISTINCT SUBSTR(comuna, 1, 2) AS region FROM personas ORDER BY region")$region

walk(regiones, function(reg) {
  tryCatch({
    message(sprintf("Processing region %s", reg))
    dbExecute(con, sprintf("INSERT INTO personas_new SELECT * FROM personas WHERE SUBSTR(comuna, 1, 2) = '%s'", reg))
  }, error = function(e) {
    message(sprintf("Error in region %s: %s", reg, e$message))
    stop(sprintf("Stopping at region %s", reg))
  })
})

dbExecute(con, "DROP TABLE personas")
dbExecute(con, "ALTER TABLE personas_new RENAME TO personas")

tbl(con, "personas")
tbl(con, "hogares")
tbl(con, "viviendas")

Compactación final de la base de datos

Finalmente, compacté la base de datos exportándola a Parquet y reimportándola. Esto elimina el espacio asignado a las operaciones de eliminación y actualización.

# Compact the database by exporting and reimporting
dbDisconnect(con, shutdown = TRUE)

# Export to parquet, then reimport to a fresh database
con <- dbConnect(duckdb::duckdb(), "censo2024_corregido.duckdb", read_only = FALSE)

# Create export directory
export_dir <- "censo_export_temp"
unlink(export_dir, recursive = TRUE)
dir.create(export_dir)

dbExecute(con, sprintf("EXPORT DATABASE '%s' (FORMAT PARQUET, COMPRESSION ZSTD)", export_dir))
dbDisconnect(con, shutdown = TRUE)

# Remove old database and create fresh one
unlink("censo2024_corregido.duckdb")
unlink("censo2024_corregido.duckdb.wal")

con <- dbConnect(duckdb::duckdb(), "censo2024_corregido.duckdb", read_only = FALSE)
dbExecute(con, sprintf("IMPORT DATABASE '%s'", export_dir))
dbDisconnect(con, shutdown = TRUE)

# Clean up export directory
unlink(export_dir, recursive = TRUE)

# Show final database size
db_size <- file.info("censo2024_corregido.duckdb")$size / 1024^2
message(sprintf("Database size: %.1f MB", db_size))

Ejemplo: Porcentaje de personas provenientes de Venezuela y Perú por región

La base resultante ocupa 3.3 GB en disco pero tiene la ventaja de que el proceso de leer y filtrar el censo completo para generar la siguiente table toma menos de un segundo gracias a las optimizaciones de DuckDB.

¿Cuántos Venezolanos y Peruanos hay en cada región y qué porcentaje representan de la población total?

con <- dbConnect(duckdb::duckdb(), "censo2024_corregido.duckdb", read_only = TRUE)

cod_venezuela <- tbl(con, "codigos_territoriales_especificos") |>
  filter(territorio_especifico == "Venezuela (República Bolivariana de)") |>
  select(codigo_especifico) |>
  collect() |>
  pull()

cod_peru <- tbl(con, "codigos_territoriales_especificos") |>
  filter(territorio_especifico == "Perú") |>
  select(codigo_especifico) |>
  collect() |>
  pull()

venezolanos_region <- tbl(con, "personas") |>
  filter(p27_nacionalidad_esp == cod_venezuela) |>
  group_by(comuna) |>
  summarise(n_venezolanos = n(), .groups = "drop") |>  
  inner_join(
    tbl(con, "codigos_territoriales") |>
      select(comuna = codigo_territorial, nombre_comuna = territorio)
  ) |>
  mutate(
    region = substr(comuna, 1, 2)
  ) |>
  group_by(region) |>
  summarise(n_venezolanos = sum(n_venezolanos), .groups = "drop") |>

  inner_join(
    tbl(con, "personas") |>
      filter(p27_nacionalidad_esp == cod_peru) |>
      group_by(comuna) |>
      summarise(n_peruanos = n(), .groups = "drop") |>  
      inner_join(
        tbl(con, "codigos_territoriales") |>
          select(comuna = codigo_territorial, nombre_comuna = territorio)
      ) |>
      mutate(
        region = substr(comuna, 1, 2)
      ) |>
      group_by(region) |>
      summarise(n_peruanos = sum(n_peruanos), .groups = "drop") |>
      inner_join(
        tbl(con, "codigos_territoriales") |>
          select(region = codigo_territorial, nombre_region = territorio) |>
          filter(str_length(region) == 2)
      )
  ) |>

  inner_join(
    tbl(con, "personas") |>
      mutate(region = substr(comuna, 1, 2)) |>
      group_by(region) |>
      summarise(total_personas = n(), .groups = "drop")
  ) |>

  inner_join(
    tbl(con, "codigos_territoriales") |>
      select(region = codigo_territorial, nombre_region = territorio) |>
      filter(str_length(region) == 2)
  ) |>

  mutate(
    pct_venezolanos = n_venezolanos / total_personas * 100,
    pct_peruanos = n_peruanos / total_personas * 100
  ) |>
  
  select(
    region,
    nombre_region,
    n_venezolanos,
    pct_venezolanos,
    n_peruanos,
    pct_peruanos,
    total_personas
  ) |>
  
  collect()

dbDisconnect(con, shutdown = TRUE)

El resultado es el siguiente:

# A tibble: 16 × 7
   region nombre_region    n_venezolanos pct_venezolanos n_peruanos pct_peruanos
   <chr>  <chr>                    <dbl>           <dbl>      <dbl>        <dbl>
 1 01     Tarapacá                 11140           3.01       14534       3.93  
 2 02     Antofagasta              15882           2.50       13341       2.10  
 3 03     Atacama                   6411           2.14        2112       0.706 
 4 04     Coquimbo                 19557           2.35        2613       0.314 
 5 05     Valparaíso               47486           2.50        4859       0.256 
 6 06     Libertador Gene…         23897           2.42        1914       0.194 
 7 07     Maule                    23505           2.09        1516       0.135 
 8 08     Biobío                   29074           1.80        1694       0.105 
 9 09     La Araucanía              7429           0.735        691       0.0684
10 10     Los Lagos                18914           2.12         934       0.105 
11 11     Aysén del Gener…           921           0.914        107       0.106 
12 12     Magallanes y de…          3213           1.93         213       0.128 
13 13     Metropolitana d…        431283           5.83      165278       2.23  
14 14     Los Ríos                  2568           0.645        181       0.0455
15 15     Arica y Parinac…          6544           2.68        9547       3.90  
16 16     Ñuble                     5643           1.10         437       0.0853

Acceso a los datos

En el siguiente repositorio: https://github.com/pachadotdev/censo2024-duckdb

Loading...

To leave a comment for the author, please follow the link and comment on their blog: https://pacha.dev/blog.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)