Crea mapas bivariados en R para visualizar la relación entre dos variables

Visualización de datos geoespaciales por polígonos bivariados

17/7/2026

mapas visualización de datos Chile

Normalmente, cuando creamos un mapa que visualice datos mediante color sólo podemos mostrar un fenómeno a la vez: la población de cada territorio, el ingreso promedio, el porcentaje de algo, etc. Pero, ¿qué pasa si queremos observar cómo se relacionan dos variables en un mismo mapa? Para eso existen los mapas bivariados! En vez de usar una escala de colores en una sola dimensión (por ejemplo, un color que va desde claro a oscuro), un mapa bivariado usa una paleta de colores en dos dimensiones: una cuadrícula de colores donde cada eje representa una variable distinta. De este modo, el color de cada territorio nos indica al mismo tiempo el valor de las dos variables, y podemos detectar visualmente en qué lugares ambas coinciden en ser altas, bajas, o una alta y la otra baja.

En este tutorial crearemos un mapa bivariado de nivel comunal que cruza el promedio de escolaridad, obtenido del Censo de Población y Vivienda 2024, con la pobreza por ingresos, estimada por el Ministerio de Desarrollo Social de Chile.

Obtención de datos

Nuestro objetivo es obtener una tabla con dos indicadores por comuna:

  1. Promedio de años de escolaridad de los habitantes de cada comuna de Chile, según el Censo 2024.
  2. Porcentaje de personas en situación de pobreza por ingresos, según las estimaciones comunales realizadas por el Ministerio de Desarrollo Social en 2024.

Como los datos vienen de dos fuentes distintas, los prepararemos por separado y luego los uniremos por comuna.

Escolaridad según el Censo 2024

Los datos de escolaridad los obtenemos del Censo, que ya hemos usado en tutoriales anteriores, donde también vimos cómo descargar los microdatos y cargarlos como base de datos. Aquí repasaremos el proceso de forma más resumida, así que si no tienes experiencia con este conjunto de datos te recomiendo revisar ese tutorial primero.

Lo primero es descargar los microdatos del Censo, disponibles en su sitio oficial, en el archivo Base de microdatos - Viviendas, hogares, personas Censo 2024 (parquet).

Cargamos la base de personas con la función open_dataset() de {arrow}, que abre los datos como una base de datos: esto significa que podemos filtrar y consultar millones de observaciones de forma eficiente, incluso cuando los datos son más grandes que la memoria de nuestras computadoras, y solamente cuando terminamos de manipular los datos copiamos los resultados a la memoria con la función collect().

library(arrow)

personas <- open_dataset("personas_censo2024.parquet")

La base de personas incluye la variable escolaridad, que corresponde a los años de escolaridad de cada persona. Para calcular el promedio comunal, primero filtramos los valores válidos (escolaridad != 99) y consideramos solamente a personas de 25 años o más ([^1] >= 25)1, edad a la que la mayoría ya completó su trayectoria educativa.

Luego agrupamos por comuna y calculamos el promedio con summarize(), y recién ahí traemos el resultado (ya resumido) a la memoria con collect().

library(dplyr)

escolaridad_comuna <- personas |>
  filter(escolaridad != 99) |>
  filter(edad_quinquenal >= 25) |> 
  rename(codigo_comuna = comuna) |> 
  group_by(codigo_comuna) |>
  summarize(escolaridad = mean(escolaridad)) |>
  collect() # traer los resultados a la memoria

escolaridad_comuna
# A tibble: 346 × 2
   codigo_comuna escolaridad
           <int>       <dbl>
 1          5802       10.9 
 2          4303        9.07
 3         11202       10.5 
 4          1101       11.6 
 5          8301       11.2 
 6         13124       10.9 
 7          8111       11.2 
 8         14108        9.44
 9         13101       13.1 
10         13603       10.6 
# ℹ 336 more rows

Obtenemos una tabla con el promedio de años de escolaridad por cada código de comuna.

Pobreza por ingresos

El segundo indicador proviene de las estimaciones de pobreza comunal por ingresos (2024) calculadas por el Ministerio de Desarrollo Social de Chile a partir de la encuesta Casen.

Cargamos el archivo sae_ingresos_2024.xlsx con read_xlsx() y como no viene muy limpio para su trabajo, lo limpiamos primero:

  • Usamos row_to_names(2) para usar la segunda fila como nombres de columna (porque viene con una fila en blanco)
  • Con clean_names() de {janitor} limpiarmos los nombres de columnas esos nombres
  • Renombramos la columna del porcentaje de pobreza para que sea más corta, y la de codigo_comuna para que coincida con el Censo,
  • Convertimos a números las columnas del código de comuna y del porcentaje
  • Nos quedamos solo con las columnas que necesitamos: el nombre y código de las comunas, y el porcentaje de pobreza.
  • Finalmente, filtramos también las filas sin código de comuna válido (como totales o encabezados) con filter(!is.na(comuna)).
library(readxl)

pobreza <- read_xlsx("sae_ingresos_2024.xlsx")
library(janitor)

pobreza_comuna <- pobreza |>
  row_to_names(2) |>
  clean_names() |>
  rename(codigo_comuna = codigo,
         pobreza = 6) |>
  mutate(codigo_comuna = as.numeric(codigo_comuna),
         pobreza = as.numeric(pobreza)) |> 
  select(codigo_comuna,
         nombre_comuna,
         pobreza) |>
  filter(!is.na(codigo_comuna))

Unir ambos indicadores

Ahora que tenemos nuestras dos tablas, ambas con una columna codigo_comuna, las unimos con left_join(), tomando la tabla de escolaridad como base y agregándole la tabla de pobreza. Como la tabla de pobreza trae los nombres de las comunas, los reubicamos al principio con relocate().

datos <- escolaridad_comuna |>
  left_join(pobreza_comuna, 
            by = join_by(codigo_comuna)) |>
  relocate(nombre_comuna, .before = codigo_comuna)

datos
# A tibble: 346 × 4
   nombre_comuna codigo_comuna escolaridad pobreza
   <chr>                 <dbl>       <dbl>   <dbl>
 1 Limache                5802       10.9    0.203
 2 Monte Patria           4303        9.07   0.247
 3 Cisnes                11202       10.5    0.165
 4 Iquique                1101       11.6    0.162
 5 Los Ángeles            8301       11.2    0.193
 6 Pudahuel              13124       10.9    0.154
 7 Tomé                   8111       11.2    0.212
 8 Panguipulli           14108        9.44   0.323
 9 Santiago              13101       13.1    0.102
10 Isla De Maipo         13603       10.6    0.162
# ℹ 336 more rows

Obtenemos una tabla con una fila por comuna y dos columnas de interés: escolaridad_prom (promedio de años de escolaridad) y pobreza_p (proporción de personas en pobreza). Estas serán las dos variables que cruzaremos en el mapa bivariado.


Unir los datos con el mapa

Ahora necesitamos obtener los polígonos de las comunas de Chile para poder dibujar el mapa. Obtendremos los mapas de Chile desde el paquete {chilemapas}, como hemos visto en otros tutoriales de mapas de este blog.

Si no tienes {chilemapas}, instálalo:

pak::pak("pachadotdev/chilemapas")

Cargamos el mapa comunal, convertimos los códigos de comuna y región a números (para que coincidan con nuestros datos), y renombramos las columnas para que tengan los mismos nombres que usamos en datos:

library(chilemapas)

mapa_comunas <- chilemapas::mapa_comunas |>
  mutate(codigo_comuna = as.numeric(codigo_comuna),
         codigo_region = as.numeric(codigo_region)) |>
  select(-codigo_provincia) |> 
  st_as_sf()

Con las columnas alineadas, cruzamos nuestros datos con el mapa usando left_join() por la variable comuna. Así, cada comuna del mapa queda asociada a sus dos indicadores.

mapa_datos <- datos |>
  left_join(mapa_comunas,
            by = join_by(codigo_comuna)) |> 
  relocate(codigo_region, .after = codigo_comuna)

mapa_datos
# A tibble: 346 × 6
   nombre_comuna codigo_comuna codigo_region escolaridad pobreza
   <chr>                 <dbl>         <dbl>       <dbl>   <dbl>
 1 Limache                5802             5       10.9    0.203
 2 Monte Patria           4303             4        9.07   0.247
 3 Cisnes                11202            11       10.5    0.165
 4 Iquique                1101             1       11.6    0.162
 5 Los Ángeles            8301             8       11.2    0.193
 6 Pudahuel              13124            13       10.9    0.154
 7 Tomé                   8111             8       11.2    0.212
 8 Panguipulli           14108            14        9.44   0.323
 9 Santiago              13101            13       13.1    0.102
10 Isla De Maipo         13603            13       10.6    0.162
# ℹ 336 more rows
# ℹ 1 more variable: geometry <MULTIPOLYGON [°]>

Ahora tenemos una tabla con las dos variables que queremos, junto a los polígonos comunales, así que estamos listxs para visualizar los mapas!

Clasificar datos bivariados

Para facilitar la creación de un mapa bivariado usaremos el paquete {biscale}, que se encarga de clasificar las dos variables en categorías y de asignarles una paleta de colores en dos dimensiones.

Si no lo tienes instalado, hazlo con:

install.packages("biscale")

Antes de clasificar, definiremos dos parámetros que nos servirán para ajustar el mapa fácilmente: la región que queremos visualizar y el número de dimensiones de la paleta, es decir, en cuántos niveles se divide cada variable.

region_mapa <- 13
dimensiones <- 3

Filtramos la región elegida y aplicamos bi_class() para clasificar cada comuna del mapa con una clase bivariada, según los valores que tenga cada territorio en cada una de las variables de interés. Le indicamos las dos variables (x e y), el método de clasificación, y el número de dimensiones:

library(biscale)

mapa_datos_bi <- mapa_datos |>
  filter(codigo_region == region_mapa) |>
  bi_class(x = escolaridad,
           y = pobreza,
           style = "quantile",
           dim = dimensiones)

mapa_datos_bi |> 
  select(nombre_comuna, codigo_comuna, bi_class)
# A tibble: 52 × 3
   nombre_comuna    codigo_comuna bi_class
   <chr>                    <dbl> <chr>   
 1 Pudahuel                 13124 2-2     
 2 Santiago                 13101 3-1     
 3 Isla De Maipo            13603 1-2     
 4 Melipilla                13501 1-3     
 5 Huechuraba               13107 3-1     
 6 Maipú                    13119 3-1     
 7 Quinta Normal            13126 2-1     
 8 Providencia              13123 3-1     
 9 Estación Central         13106 2-2     
10 San Bernardo             13401 2-3     
# ℹ 42 more rows

El resultado incluye una columna nueva, bi_class, con valores como "1-1", "2-3", etc. Cada número indica en qué tercio (o categoría) cae la comuna para cada variable: el primer número corresponde al eje x (escolaridad) y el segundo al eje y (pobreza).

Visualizar el mapa bivariado

Ver código para el tema de los gráficos
library(ggplot2)

theme_set(
  theme_void(
    base_family = "Atkinson Hyperlegible",
    paper = "#EAD1FA",
    ink = "#543A73",
    accent = "#9069C0"
  ) +
    # margen de mapas
    theme(plot.margin = unit(c(2, 2, 2, 2), "mm")) +
    # fondo transparente (paper de theme_void deja fondo opaco)
    theme(plot.background = element_rect(fill = "transparent", color = "transparent"))
)

Construir un mapa bivariado requiere de dos pasos: hacer el mapa en sí, y hace la leyenda en forma de cuadrícula que explica los colores. Luego uniremos ambos pasos con {patchwork}.

Pero primero, elegimos una de las paletas bivariadas que ofrece {biscale}. Hay varias disponibles, como "DkBlue", "BlueOr" o "DkViolet2".

paleta <- "DkBlue2"

Leyenda bivariada

Como ya dijimos, la leyenda de un mapa bivariado es una cuadrícula que muestra todas las combinaciones posibles de las dos variables; en nuestro caso, una cuadrícula de 3×3. Podemos crear la leyenda bivariada con bi_legend(), usando la misma paleta y dimensiones que el mapa, y etiquetando cada eje:

leyenda <- bi_legend(pal = paleta,
                     dim = dimensiones,
                     xlab = "Escolaridad",
                     ylab = "Pobreza",
                     size = 8) +
  # fondo transparente para insertarla sobre el mapa
  theme(plot.background = element_blank(),
        panel.background = element_blank(),
        panel.grid.major = element_blank(),
        axis.title = element_text(color = "#543A73"))

En concreto, la leyenda bivariada es un gráfico por sí mismo, así que podemos previsualizarla:

leyenda

Mapa bivariado

Creamos el mapa con {ggplot2} y geom_sf() (como vimos en el tutorial de mapas con {sf}), definiendo la escala de colores con bi_scale_fill() de {biscale} para expresar la columna bi_class en la paleta de colores en dos dimensiones. Ocultamos la leyenda automática (show.legend = FALSE) porque usaremos la leyenda personalizada que creamos recién.

mapa <- mapa_datos_bi |>
  st_as_sf() |>
  ggplot() +
  aes(fill = bi_class) +
  geom_sf(show.legend = FALSE,
          linewidth = 0.1, color = "white") +
  # escala de colores bivariada
  bi_scale_fill(pal = paleta, dim = dimensiones)

mapa

Tenemos un mapa bivariado! Pero aún tenemos que agregarle la leyenda para que pueda ser interpretable.

Combinar mapa y leyenda

Finalmente, usamos el paquete {patchwork} para insertar la leyenda dentro del mapa con inset_element(), ubicándola en una esquina. Los argumentos left, bottom, right y top definen la posición y el tamaño de la leyenda, en una escala de 0 a 1 relativa al gráfico.

library(patchwork)

mapa_bivariado <- mapa +
  labs(title = "Región Metropolitana de Santiago",
       subtitle = "Relación entre porcentaje de pobreza y escolaridad promedio",
       caption = "Fuente: Censo 2024, Casen 2024\nHecho por Bastián Olea H.") +
  inset_element(leyenda,
                left = -0.05, bottom = 0.65,
                right = 0.25, top = 1)

mapa_bivariado

Obtenemos un mapa bivariado donde cada comuna se colorea según la combinación de sus dos variables.

Siguiendo la leyenda, las comunas del color más oscuro (arriba a la derecha) son aquellas donde coinciden una alta escolaridad promedio y una alta pobreza por ingresos, mientras que los colores más tenues (abajo a la izquierda) corresponden a comunas con baja escolaridad y baja pobreza. La visualización nos permite distinguit comunas con alta escolaridad y baja pobreza (en celeste/calipso), y comunas con baja escolaridad y alta pobreza (en rosado/fucsia), que reflejan la relación inversa que solemos esperar entre educación y pobreza. Sin embargo, para afirmar que existe una asociación estadística entre ambas variables habría que aplicar las pruebas estadísticas apropiadas.

Lo bueno de haber parametrizado la región y las dimensiones al comienzo es que puedes reutilizar todo este código cambiando solo un par de valores: prueba cambiando la región (region_mapa), otra cantidad de dimensiones, o incluso otras variables para construir tus propios mapas bivariados!2

Para probar la parametrización, repitamos el proceso para generar otro mapa de una región distinta, repitiendo el código anterior, con leves ajustes para posicionar la leyenda correctamente:

library(ggplot2)
library(biscale)
library(sf)

# parámetros para la visualización
region_mapa <- 6
dimensiones <- 3
paleta <- "DkViolet2"

# preparar datos bivariados
mapa_datos_bi <- mapa_datos |>
  filter(codigo_region == region_mapa) |>
  bi_class(x = escolaridad,
           y = pobreza,
           style = "quantile",
           dim = dimensiones)

# generar la leyenda
leyenda <- bi_legend(pal = paleta,
                     dim = dimensiones,
                     xlab = "Escolaridad",
                     ylab = "Pobreza",
                     size = 8) +
  # fondo transparente para insertarla sobre el mapa
  theme(plot.background = element_blank(),
        panel.background = element_blank(),
        panel.grid.major = element_blank(),
        axis.title = element_text(color = "#543A73"))

# mapa bivariado
mapa <- mapa_datos_bi |>
  st_as_sf() |>
  ggplot() +
  aes(fill = bi_class) +
  geom_sf(show.legend = FALSE,
          linewidth = 0.1, color = "white") +
  # escala de colores bivariada
  bi_scale_fill(pal = paleta, dim = dimensiones)

# mapa + leyenda y textos
mapa_bivariado <- mapa +
  labs(title = "Región de O'Higgins",
       subtitle = "Relación entre porcentaje de pobreza y escolaridad promedio",
       caption = "Fuente: Censo 2024, Casen 2024\nHecho por Bastián Olea H.") +
  # agregar espacios abajo para ajustar la leyenda
  theme(plot.margin = unit(c(2, 2, 12, 2), "mm"),
        plot.caption = element_text(margin = margin(t = 20))) +
  # leyenda
  inset_element(leyenda,
                left = -0.05, bottom = -0.2,
                right = 0.2, top = 0.15)

mapa_bivariado

Dos mapas por el precio de uno! Pero en este segundo mapa pusimos la leyenda un poco distinto: hicimos espacio debajo del mapa en la capa theme() del gráfico, dado que la forma del mapa no dejaba muchos espacios para poner la leyenda.

Recursos


  1. Usamos edad_quinquenal en lugar de edad porque la edad viene con un proceso de anonimización de datos, mientras que la edad en quinquenios no. ↩︎

  2. Disclaimer: usé un LLM para escribir el boceto de este post, porque tenía el código escrito hace mucho tiempo, pero no encontraba el tiempo para terminarlo. ↩︎

Fecha de publicación:
July 17, 2026
Extensión:
13 minute read, 2756 words
Categorías:
Tutoriales
Tags:
mapas visualización de datos Chile
Ver también:
Mapas de las regiones de Chile según macrozonas
Colores y paletas de colores en R
`{territorial}`: un paquete de R para trabajar con datos de comunas y regiones de Chile