Limpiar planillas de Excel complejas en R con {unpivotr}

Convertir planillas con múltiples encabezados, tablas dinámicas o pivotadas a dataframes

1/10/2026

limpieza de datos procesamiento de datos tablas Excel

Muchas planillas Excel presentan sus datos de forma no-rectangular; por ejemplo, con múltiples encabezados encima de las columnas, celdas combinadas que describen las variables bajo ellas, encabezados de variables en las filas, etc.

Datos desordenados

Veamos un ejemplo de datos desordenados:

2026
Grupo A Grupo B
Variable Variable 1 Variable 2 Variable 1 Variable 2
Observación 1 10 20 15 25
Observación 2 12 22 17 27

La tabla anterior está desordenada porque tiene nombres de variables encima de nombres de variables, tienen valores como encabezados de columnas (años, grupos) los datos sobre las observaciones no están en sus propias columnas, y hay datos escondidos como nombres de columnas (los años).

Si bien este tipo de tablas pivotadas muchas veces facilita la lectura y mejora la densidad de información, da muchos problemas para manipular y transformar los datos:

¿Cómo filtrarías los datos de la Variable 1, si sale en 2 columnas distintas, que ni siquiera son consecutivas? Luego, ¿cómo seleccionarías los valores del Grupo B, si los datos están en 2 columnas, una de ellas sin siquiera tener etiqueta encima? O peor, imagina que la tabla abarca varios años, ¿cómo identificas en qué columnas están los valores de cierto año, si cada año abarca 4 columnas?

Un dolor de cabeza! 🫠

Así serían los mismos datos, pero en formato ordenado o tidy data, donde cada variable es una columna y cada observación es una fila:

Año Observación Grupo Variable 1 Variable 2
2026 1 A 10 20
2026 1 B 15 25
2026 2 A 12 22
2026 2 B 17 27

El paquete {unpivotr} fue diseñado para despivotar este tipo de tablas y volver a estructurar la información desestructurada en filas y columnas.

install.packages("unpivotr")
library(unpivotr)

En código, si importamos la tabla Excel desordenada anterior con {readxl}, se vería así:

library(dplyr)
Attaching package: 'dplyr'

The following objects are masked from 'package:stats':

    filter, lag

The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
desorden <- tibble(
  `...1` = c(NA, NA, "Variable", "Observación 1", "Observación 2"),
  `...2` = c(2026, "Grupo A", "Variable 1", 10, 12),
  `...3` = c(NA, "Grupo A", "Variable 2", 20, 22),
  `...4` = c(NA, "Grupo B", "Variable 1", 15, 17),
  `...5` = c(NA, "Grupo B", "Variable 2", 25, 27)
)

desorden
# A tibble: 5 × 5
  ...1          ...2       ...3       ...4       ...5      
  <chr>         <chr>      <chr>      <chr>      <chr>     
1 <NA>          2026       <NA>       <NA>       <NA>      
2 <NA>          Grupo A    Grupo A    Grupo B    Grupo B   
3 Variable      Variable 1 Variable 2 Variable 1 Variable 2
4 Observación 1 10         20         15         25        
5 Observación 2 12         22         17         27        

Como vemos, se trata de una abominación alejada de los ojos de Dios. Para enfrentarnos a estos horrores, lo primero es deconstruir la tabla a sus meras celdas con as_cells(). Esto transforma la tabla en una nueva tabla de una fila por celda, que describe las filas y columnas donde se ubica cada celda, que es muy poco legible, pero es lo que nos permitirá reconstruirla.

library(unpivotr)

celdas <- as_cells(desorden)

celdas
# A tibble: 25 × 4
     row   col data_type chr          
   <int> <int> <chr>     <chr>        
 1     1     1 chr       <NA>         
 2     2     1 chr       <NA>         
 3     3     1 chr       Variable     
 4     4     1 chr       Observación 1
 5     5     1 chr       Observación 2
 6     1     2 chr       2026         
 7     2     2 chr       Grupo A      
 8     3     2 chr       Variable 1   
 9     4     2 chr       10           
10     5     2 chr       12           
# ℹ 15 more rows

Si ignoramos las primeras 3 columnas, vemos que tenemos todas las celdas en una sola columna (chr).

Ahora que tenemos la tabla tokenizada, procedemos a iluminar a esta bestia del abismo con la función behead(), o decapitar 🔪

Mirando la tabla desordenada, vemos que la parte superior de la tabla tiene los años, así que decapitamos (behead()) la tabla por arriba ("up") para obtener la variable año:

celdas |> 
  behead("up", "año")
# A tibble: 20 × 5
     row   col data_type chr           año  
   <int> <int> <chr>     <chr>         <chr>
 1     2     1 chr       <NA>          <NA> 
 2     3     1 chr       Variable      <NA> 
 3     4     1 chr       Observación 1 <NA> 
 4     5     1 chr       Observación 2 <NA> 
 5     2     2 chr       Grupo A       2026 
 6     3     2 chr       Variable 1    2026 
 7     4     2 chr       10            2026 
 8     5     2 chr       12            2026 
 9     2     3 chr       Grupo A       <NA> 
10     3     3 chr       Variable 2    <NA> 
11     4     3 chr       20            <NA> 
12     5     3 chr       22            <NA> 
13     2     4 chr       Grupo B       <NA> 
14     3     4 chr       Variable 1    <NA> 
15     4     4 chr       15            <NA> 
16     5     4 chr       17            <NA> 
17     2     5 chr       Grupo B       <NA> 
18     3     5 chr       Variable 2    <NA> 
19     4     5 chr       25            <NA> 
20     5     5 chr       27            <NA> 

Vemos que aparece la columna con los años! Continuemos con el siguiente nivel de la tabla desde arriba hacia abajo, la variable grupo:

celdas |> 
  behead("up", "año") |> 
  behead("up", "grupo")
# A tibble: 15 × 6
     row   col data_type chr           año   grupo  
   <int> <int> <chr>     <chr>         <chr> <chr>  
 1     3     1 chr       Variable      <NA>  <NA>   
 2     4     1 chr       Observación 1 <NA>  <NA>   
 3     5     1 chr       Observación 2 <NA>  <NA>   
 4     3     2 chr       Variable 1    2026  Grupo A
 5     4     2 chr       10            2026  Grupo A
 6     5     2 chr       12            2026  Grupo A
 7     3     3 chr       Variable 2    <NA>  Grupo A
 8     4     3 chr       20            <NA>  Grupo A
 9     5     3 chr       22            <NA>  Grupo A
10     3     4 chr       Variable 1    <NA>  Grupo B
11     4     4 chr       15            <NA>  Grupo B
12     5     4 chr       17            <NA>  Grupo B
13     3     5 chr       Variable 2    <NA>  Grupo B
14     4     5 chr       25            <NA>  Grupo B
15     5     5 chr       27            <NA>  Grupo B

Ahora se va notando que, por cada nivel que extraemos, creamos una columna que empieza a reconstuir la tabla que merecemos. Seguimos con la variable:

celdas |> 
  behead("up", "año") |> 
  behead("up", "grupo") |> 
  behead("up", "variable")
# A tibble: 10 × 7
     row   col data_type chr           año   grupo   variable  
   <int> <int> <chr>     <chr>         <chr> <chr>   <chr>     
 1     4     1 chr       Observación 1 <NA>  <NA>    Variable  
 2     5     1 chr       Observación 2 <NA>  <NA>    Variable  
 3     4     2 chr       10            2026  Grupo A Variable 1
 4     5     2 chr       12            2026  Grupo A Variable 1
 5     4     3 chr       20            <NA>  Grupo A Variable 2
 6     5     3 chr       22            <NA>  Grupo A Variable 2
 7     4     4 chr       15            <NA>  Grupo B Variable 1
 8     5     4 chr       17            <NA>  Grupo B Variable 1
 9     4     5 chr       25            <NA>  Grupo B Variable 2
10     5     5 chr       27            <NA>  Grupo B Variable 2

Ahora podemos volver a mirar la tabla desordenada y recordamos que queda la columna con los nombres de las observaciones al lado izquierdo ("left"):

tabla <- celdas |> 
  behead("up", "año") |> 
  behead("up", "grupo") |> 
  behead("up", "variable") |> 
  behead("left", "observacion")

Ahora a la columna chr, que contenía los valores crudos de la tabla, solamente le quedan los valores de cada celda, así que guardamos el resultado en un objeto nuevo y procedemos a limpiar un poquito:

tabla <- tabla |> 
  select(año, observacion, grupo, variable,
         valor = chr)

tabla
# A tibble: 8 × 5
  año   observacion   grupo   variable   valor
  <chr> <chr>         <chr>   <chr>      <chr>
1 2026  Observación 1 Grupo A Variable 1 10   
2 2026  Observación 2 Grupo A Variable 1 12   
3 <NA>  Observación 1 Grupo A Variable 2 20   
4 <NA>  Observación 2 Grupo A Variable 2 22   
5 <NA>  Observación 1 Grupo B Variable 1 15   
6 <NA>  Observación 2 Grupo B Variable 1 17   
7 <NA>  Observación 1 Grupo B Variable 2 25   
8 <NA>  Observación 2 Grupo B Variable 2 27   

Vemos un último problema: el año no se propagó a todas las filas, porque la tabla tenía vacías esas columnas (dependía de que el/la usuario/a infiriera que el valor 2026 aplicaba a todas las columnas de la derecha). Podemos usar fill() de {tidyr} para rellenar hacia abajo, y luego limpiamos los valores de cada celda con str_remove() de {stringr}:

library(tidyr)
Attaching package: 'tidyr'

The following objects are masked from 'package:unpivotr':

    pack, unpack
library(stringr)

tabla <- tabla |> 
  fill(año, .direction = "down") |> 
  mutate(
    observacion = str_remove(observacion, "Observación "),
    grupo = str_remove(grupo, "Grupo "),
    variable = str_remove(variable, "Variable ")
  ) |> 
  arrange(observacion)

Ahora podemos pivotar la tabla para obtener datos ordenados (tidy): una columna por cada variable, una fila por cada observación:

library(tidyr)

tabla <- tabla |> 
  pivot_wider(
    names_from = variable, 
    values_from = valor, 
    names_prefix = "variable_")
año observacion grupo variable_1 variable_2
2026 1 A 10 20
2026 1 B 15 25
2026 2 A 12 22
2026 2 B 17 27

Obtuvimos una tabla limpia, idéntica a la del ejemplo de más arriba, a partir de la tabla sucia!

{unpivotr} es demasiado útil para importar datos desordenados y volverlos en datos útiles para análisis. Ahora veremos dos ejemplos más para aprender a usarlo con datos reales!


Ejemplos con datos reales

Ahora que entendimos la lógica de {unpivotr} con un ejemplo mínimo, veamos cómo se aplica a dos planillas con datos reales.

Datos municipales

El primer ejemplo es una planilla descargada desde el Sistema Nacional de Información Municipal (SINIM), que entrega información financiera de todas las municipalidades de Chile.

Descargamos 3 variables de ingresos municipales para los años 2023 a 2025:

  • Ingresos Municipales (Ingreso Total Percibido) (M$) IADM01
  • Ingresos por Fondo Común Municipal (M$) IADM40
  • Ingresos Propios Permanentes (IPP) (M$) IADM41

Primeras filas y columnas de una tabla descargada desde SINIM

Cargamos los datos usando {openxlsx2}, sin nombres de columnas porque sabemos que los datos no vienen en una estructura confiable:

# install.packages('openxlsx2')
library(openxlsx2)
library(dplyr)

datos <- read_xlsx("datos_municipales_20260903181833_Sin-Correccion-Monetaria.xlsx",
                   col_names = FALSE) |> 
  as_tibble()

Más o menos así se ve la planilla cargada en R:

A B C D E F G H
Valores en miles de pesos nominales (M$) de cada año. NA NA NA NA NA NA NA
NA NA IADM01 (M$) Ingresos Municipales (Ingreso Total Percibido) IADM01 (M$) Ingresos Municipales (Ingreso Total Percibido) IADM01 (M$) Ingresos Municipales (Ingreso Total Percibido) IADM40 (M$) Ingresos por Fondo Común Municipal IADM40 (M$) Ingresos por Fondo Común Municipal IADM40 (M$) Ingresos por Fondo Común Municipal
CODIGO MUNICIPIO 2025 2024 2023 2025 2024 2023
1101 IQUIQUE 108892278 104723522 98449509 8560862 7496937 6305296
1107 ALTO HOSPICIO 36876247 34424619 27951929 21540651 19217898 16489486
1401 POZO ALMONTE 20133051 22382992 17792283 5820144 4170737 3685890

Esta planilla es un asco! 🤮

Tiene varios problemas a la vez:

  • La primera fila es solamente una nota al pie (el texto “valores en miles de pesos…”) que ocupa únicamente la primera columna
  • La segunda fila contiene los códigos de las variables (IADM01, IADM40, IADM41)
  • La tercera fila mezcla los nombres de columna reales (CODIGO, MUNICIPIO) con los años de cada variable (2025, 2024, 2023)
  • Cada variable queda repartida en 3 columnas, una por año, en vez de existir una sola columna Año
  • No hay una fila de nombres de columna claros!

El problema es porque los datos vienen pivotados: no se respetan los principios de los datos ordenados (tidy data), así que tenemos variables en filas y columnas, y variables sobre otras variables en un encabezado.

Como ya vimos, el primer paso de la limpieza para despivotar tablas es tokenizar la planilla con as_cells() para convertirla a una fila por celda:

library(unpivotr)

celdas <- datos |> 
  as_cells()

celdas
# A tibble: 3,828 × 4
     row   col data_type chr                                                  
   <int> <int> <chr>     <chr>                                                
 1     1     1 chr       Valores en miles de pesos nominales (M$) de cada año.
 2     2     1 chr       <NA>                                                 
 3     3     1 chr       CODIGO                                               
 4     4     1 chr       1101                                                 
 5     5     1 chr       1107                                                 
 6     6     1 chr       1401                                                 
 7     7     1 chr       1402                                                 
 8     8     1 chr       1403                                                 
 9     9     1 chr       1404                                                 
10    10     1 chr       1405                                                 
# ℹ 3,818 more rows

La nota en la primera celda (una pésima práctica) queda convertida en una celda más, en la fila 1, columna 1. Podemos descartarla con un simple filtro antes de empezar a decapitar:

celdas <- celdas |> 
  filter(row > 1)

Ahora, y siempre mirando la tabla original, aplicamos la misma lógica del ejemplo anterior: extraemos dos variables de arriba de la tabla (variable y año) y dos variables a la izquierda (codigo y comuna). Como ya vimos el procedimiento paso a paso, esta vez encadenamos los cuatro niveles de una sola vez:

tabla <- celdas |> 
  behead(direction = "up", name = "variable") |> 
  behead(direction = "up", name = "año") |> 
  behead(direction = "left", name = "codigo") |> 
  behead(direction = "left", name = "comuna")

tabla
# A tibble: 3,105 × 8
     row   col data_type chr       variable                  año   codigo comuna
   <int> <int> <chr>     <chr>     <chr>                     <chr> <chr>  <chr> 
 1     4     3 chr       108892278 IADM01 (M$) Ingresos Mun… 2025  1101   IQUIQ…
 2     5     3 chr       36876247  IADM01 (M$) Ingresos Mun… 2025  1107   ALTO …
 3     6     3 chr       20133051  IADM01 (M$) Ingresos Mun… 2025  1401   POZO …
 4     7     3 chr       4606673   IADM01 (M$) Ingresos Mun… 2025  1402   CAMIÑA
 5     8     3 chr       4076487   IADM01 (M$) Ingresos Mun… 2025  1403   COLCH…
 6     9     3 chr       7521191   IADM01 (M$) Ingresos Mun… 2025  1404   HUARA 
 7    10     3 chr       11858610  IADM01 (M$) Ingresos Mun… 2025  1405   PICA  
 8    11     3 chr       192483545 IADM01 (M$) Ingresos Mun… 2025  2101   ANTOF…
 9    12     3 chr       15054777  IADM01 (M$) Ingresos Mun… 2025  2102   MEJIL…
10    13     3 chr       13643944  IADM01 (M$) Ingresos Mun… 2025  2103   SIERR…
# ℹ 3,095 more rows

Estamos casi! Convertimos las columnas a numéricas y sacamos las columnas innecesarias que describían las celdas:

tabla <- tabla |> 
  mutate(valor = as.integer(chr)) |> 
  select(-row, -col, -data_type, -chr) |> 
  relocate(variable, .after = valor)
Warning: There was 1 warning in `mutate()`.
ℹ In argument: `valor = as.integer(chr)`.
Caused by warning:
! NAs introduced by coercion

Al convertir la columna valor a número aparece una advertencia de valores transformados en NA, pero son los valores que antes decían "No Recepcionado", debido a que esa municipalidad no entregó la información.

año codigo comuna valor variable
2025 1101 IQUIQUE 108892278 IADM01 (M$) Ingresos Municipales (Ingreso Total Percibido)
2025 1107 ALTO HOSPICIO 36876247 IADM01 (M$) Ingresos Municipales (Ingreso Total Percibido)
2025 1401 POZO ALMONTE 20133051 IADM01 (M$) Ingresos Municipales (Ingreso Total Percibido)
2025 1402 CAMIÑA 4606673 IADM01 (M$) Ingresos Municipales (Ingreso Total Percibido)
2025 1403 COLCHANE 4076487 IADM01 (M$) Ingresos Municipales (Ingreso Total Percibido)
2025 1404 HUARA 7521191 IADM01 (M$) Ingresos Municipales (Ingreso Total Percibido)

Con esto obtuvimos una tabla ordenada, rectangular, con variables claras y una fila por municipio ✨


Datos de empresas

El segundo ejemplo es todavía más desordenado: una planilla del Servicio de Impuestos Internos (SII) con estadísticas de personas naturales y sus montos de impuesto global complementario y único de segunda categoría, desagregados por región.

La planilla viene en formato .xlsb (Excel binario), que {openxlsx2} también puede leer. Pero también ábrela en Excel para entenderla visualmente.

Primeras filas y columnas de la tabla de estadísticas del SII

Si nos fijamos, la tabla tiene muchas filas vacías arriba, así que cargamos saltándonos 7 filas definiendo start_row = 7:

# install.packages('openxlsx2')
library(openxlsx2)
library(unpivotr)

# cargar
datos <- wb_read(
  "PUB_Region.xlsb",
  col_names = FALSE,
  start_row = 7
  ) |> 
  as_tibble()
A B C D E F G H
NA NA NA Per. Naturales contribuyentes de GC NA NA Per. Naturales contribuyentes de 2a Cat. NA
Año Comercial Región Tramo de Rentas N° de Personas Renta Determinada (Millones de pesos) Impuesto Determinado (Millones de pesos) N° de Personas Renta Determinada (Millones de pesos)
2005 Región de Tarapacá Tramo 1 - 0 a 13,5 UTA (Exento) 19227 29106.8 0 54102 79348.60000000001
2005 Región de Tarapacá Tramo 2 - 13,5 a 30 UTA (Tasa 5%) 4817 36693.2 602.5 4422 31259.6
2005 Región de Tarapacá Tramo 3 - 30 a 50 UTA (Tasa 10%) 1969 28371.1 1213.7 594 8313.299999999999
2005 Región de Tarapacá Tramo 4 - 50 a 70 UTA (Tasa 15%) 817 18316.3 1300 161 3516.8

Esta planilla tiene 2 niveles de encabezado hacia arriba (que podríamos llamar categoria y variable) y 3 niveles hacia la izquierda (año, región y tramo).

Ahora convertimos a celdas:

celdas <- as_cells(datos)

Recomiendo ir haciendo línea por línea la decapitación de la tabla, así vamos viendo cómo aparecen las columnas nuevas:

Empezamos por la izquierda:

celdas |>
  behead("left", "año") 
# A tibble: 29,381 × 5
     row   col data_type chr                año          
   <int> <int> <chr>     <chr>              <chr>        
 1     1     2 chr       <NA>               <NA>         
 2     2     2 chr       Región             Año Comercial
 3     3     2 chr       Región de Tarapacá 2005         
 4     4     2 chr       Región de Tarapacá 2005         
 5     5     2 chr       Región de Tarapacá 2005         
 6     6     2 chr       Región de Tarapacá 2005         
 7     7     2 chr       Región de Tarapacá 2005         
 8     8     2 chr       Región de Tarapacá 2005         
 9     9     2 chr       Región de Tarapacá 2005         
10    10     2 chr       Región de Tarapacá 2005         
# ℹ 29,371 more rows

Seguimos extrayendo las variables de la izquierda…

celdas |>
  behead("left", "año") |>
  behead("left", "region") |>
  behead("left", "tramo") 
# A tibble: 24,039 × 7
     row   col data_type chr                                 año    region tramo
   <int> <int> <chr>     <chr>                               <chr>  <chr>  <chr>
 1     1     4 chr       Per. Naturales contribuyentes de GC <NA>   <NA>   <NA> 
 2     2     4 chr       N° de Personas                      Año C… Región Tram…
 3     3     4 chr       19227                               2005   Regió… Tram…
 4     4     4 chr       4817                                2005   Regió… Tram…
 5     5     4 chr       1969                                2005   Regió… Tram…
 6     6     4 chr       817                                 2005   Regió… Tram…
 7     7     4 chr       386                                 2005   Regió… Tram…
 8     8     4 chr       231                                 2005   Regió… Tram…
 9     9     4 chr       89                                  2005   Regió… Tram…
10    10     4 chr       59                                  2005   Regió… Tram…
# ℹ 24,029 more rows

Ya empieza a tomar forma! Ahora las de arriba:

tabla <- celdas |>
  behead("left", "año") |>
  behead("left", "region") |>
  behead("left", "tramo") |> 
  behead("up", "categoria") |>
  behead("up", "variable")

tabla
# A tibble: 24,021 × 9
     row   col data_type chr   año   region             tramo categoria variable
   <int> <int> <chr>     <chr> <chr> <chr>              <chr> <chr>     <chr>   
 1     3     4 chr       19227 2005  Región de Tarapacá Tram… Per. Nat… N° de P…
 2     4     4 chr       4817  2005  Región de Tarapacá Tram… Per. Nat… N° de P…
 3     5     4 chr       1969  2005  Región de Tarapacá Tram… Per. Nat… N° de P…
 4     6     4 chr       817   2005  Región de Tarapacá Tram… Per. Nat… N° de P…
 5     7     4 chr       386   2005  Región de Tarapacá Tram… Per. Nat… N° de P…
 6     8     4 chr       231   2005  Región de Tarapacá Tram… Per. Nat… N° de P…
 7     9     4 chr       89    2005  Región de Tarapacá Tram… Per. Nat… N° de P…
 8    10     4 chr       59    2005  Región de Tarapacá Tram… Per. Nat… N° de P…
 9    11     4 chr       33232 2005  Región de Antofag… Tram… Per. Nat… N° de P…
10    12     4 chr       10679 2005  Región de Antofag… Tram… Per. Nat… N° de P…
# ℹ 24,011 more rows

Con los 5 niveles extraídos, nos quedamos solamente con esas columnas y el valor de cada celda:

tabla <- tabla |>
  mutate(valor = as.integer(chr)) |> 
  select(-row, -col, -data_type, -chr)
Warning: There was 1 warning in `mutate()`.
ℹ In argument: `valor = as.integer(chr)`.
Caused by warning:
! NAs introduced by coercion
tabla
# A tibble: 24,021 × 6
   año   region                tramo                    categoria variable valor
   <chr> <chr>                 <chr>                    <chr>     <chr>    <int>
 1 2005  Región de Tarapacá    Tramo 1 - 0 a 13,5 UTA … Per. Nat… N° de P… 19227
 2 2005  Región de Tarapacá    Tramo 2 - 13,5 a 30 UTA… Per. Nat… N° de P…  4817
 3 2005  Región de Tarapacá    Tramo 3 - 30 a 50 UTA (… Per. Nat… N° de P…  1969
 4 2005  Región de Tarapacá    Tramo 4 - 50 a 70 UTA (… Per. Nat… N° de P…   817
 5 2005  Región de Tarapacá    Tramo 5 - 70 a 90 UTA (… Per. Nat… N° de P…   386
 6 2005  Región de Tarapacá    Tramo 6 - 90 a 120 UTA … Per. Nat… N° de P…   231
 7 2005  Región de Tarapacá    Tramo 7 - 120 a 150 UTA… Per. Nat… N° de P…    89
 8 2005  Región de Tarapacá    Tramo 8 - Más de 150 UT… Per. Nat… N° de P…    59
 9 2005  Región de Antofagasta Tramo 1 - 0 a 13,5 UTA … Per. Nat… N° de P… 33232
10 2005  Región de Antofagasta Tramo 2 - 13,5 a 30 UTA… Per. Nat… N° de P… 10679
# ℹ 24,011 more rows

Revisemos que la variable categoria, que es la más extraña en la tabla, haya quedado bien extraída:

tabla |>
  distinct(categoria)
# A tibble: 4 × 1
  categoria                               
  <chr>                                   
1 Per. Naturales contribuyentes de GC     
2 <NA>                                    
3 Per. Naturales contribuyentes de 2a Cat.
4 Consolidado                             

Aparece un valor NA! En cambio, la columna variable sí se extrajo completa para todas las columnas:

tabla |>
  distinct(categoria, variable)
# A tibble: 5 × 2
  categoria                                variable                             
  <chr>                                    <chr>                                
1 Per. Naturales contribuyentes de GC      N° de Personas                       
2 <NA>                                     Renta Determinada (Millones de pesos)
3 <NA>                                     Impuesto Determinado (Millones de pe…
4 Per. Naturales contribuyentes de 2a Cat. N° de Personas                       
5 Consolidado                              N° de Personas                       

Esto pasa porque, en la planilla original, el encabezado de cada categoria corresponde a una celda combinada que abarca 3 columnas (una por cada variable), o sea que, de las 3 columnas, el texto solo aparece en la primera, y las otras dos están vacías, así que salen como NA dado que se asume que la persona que mira la tabla se imagina que el primer valor se repite hacia el lado. Por eso behead("up", "categoria") no tiene nada que extraer para ellas y quedan en NA.

Veamos el problema en un caso puntual:

# ver el problema
tabla |>
  filter(
    region == "Región de Coquimbo",
    tramo == "Tramo 1 - 0 a 13,5 UTA (Exento)",
    año == 2015
  ) |> 
  select(-region)
# A tibble: 9 × 5
  año   tramo                           categoria                variable  valor
  <chr> <chr>                           <chr>                    <chr>     <int>
1 2015  Tramo 1 - 0 a 13,5 UTA (Exento) Per. Naturales contribu… N° de P…  55840
2 2015  Tramo 1 - 0 a 13,5 UTA (Exento) <NA>                     Renta D… 169887
3 2015  Tramo 1 - 0 a 13,5 UTA (Exento) <NA>                     Impuest…      0
4 2015  Tramo 1 - 0 a 13,5 UTA (Exento) Per. Naturales contribu… N° de P… 146253
5 2015  Tramo 1 - 0 a 13,5 UTA (Exento) <NA>                     Renta D… 381320
6 2015  Tramo 1 - 0 a 13,5 UTA (Exento) <NA>                     Impuest…    953
7 2015  Tramo 1 - 0 a 13,5 UTA (Exento) Consolidado              N° de P… 202093
8 2015  Tramo 1 - 0 a 13,5 UTA (Exento) <NA>                     Renta D… 551207
9 2015  Tramo 1 - 0 a 13,5 UTA (Exento) <NA>                     Impuest…    953

De las 9 filas, solamente las que corresponden a la primera variable de cada categoría (N° de Personas) quedaron con su categoria bien puesta; las otras 2 de cada grupo quedaron en NA.

Entonces la solución es repetir el valor de categoria con fill() hacia abajo, para rellenar esos NA, igual como hicimos con la variable año en el ejemplo anterior:

# rellenar
tabla <- tabla |>
  fill(categoria, .direction = "down")

Ahora sí! Y así se ve la tablita ordenada, filtrada:

tabla |>
  filter(
    region == "Región de Coquimbo",
    tramo == "Tramo 1 - 0 a 13,5 UTA (Exento)",
    año == 2015
  )
año region tramo categoria variable valor
2015 Región de Coquimbo Tramo 1 - 0 a 13,5 UTA (Exento) Per. Naturales contribuyentes de GC N° de Personas 55840
2015 Región de Coquimbo Tramo 1 - 0 a 13,5 UTA (Exento) Per. Naturales contribuyentes de GC Renta Determinada (Millones de pesos) 169887
2015 Región de Coquimbo Tramo 1 - 0 a 13,5 UTA (Exento) Per. Naturales contribuyentes de GC Impuesto Determinado (Millones de pesos) 0
2015 Región de Coquimbo Tramo 1 - 0 a 13,5 UTA (Exento) Per. Naturales contribuyentes de 2a Cat. N° de Personas 146253
2015 Región de Coquimbo Tramo 1 - 0 a 13,5 UTA (Exento) Per. Naturales contribuyentes de 2a Cat. Renta Determinada (Millones de pesos) 381320
2015 Región de Coquimbo Tramo 1 - 0 a 13,5 UTA (Exento) Per. Naturales contribuyentes de 2a Cat. Impuesto Determinado (Millones de pesos) 953

Mucho más claro!

Para terminar, en vez de dejar las 3 variables apiladas en una sola columna valor (formato largo), conviene transformarlas a formato ancho con pivot_wider(), para que cada variable (N° de Personas, Renta Determinada, Impuesto Determinado) quede en su propia columna:

library(tidyr)

tabla_wide <- tabla |>
  pivot_wider(
    names_from = variable,
    values_from = valor
  )
año region tramo categoria N° de Personas Renta Determinada (Millones de pesos) Impuesto Determinado (Millones de pesos)
2005 Región de Tarapacá Tramo 1 - 0 a 13,5 UTA (Exento) Per. Naturales contribuyentes de GC 19227 29106 0
2005 Región de Tarapacá Tramo 2 - 13,5 a 30 UTA (Tasa 5%) Per. Naturales contribuyentes de GC 4817 36693 602
2005 Región de Tarapacá Tramo 3 - 30 a 50 UTA (Tasa 10%) Per. Naturales contribuyentes de GC 1969 28371 1213
2005 Región de Tarapacá Tramo 4 - 50 a 70 UTA (Tasa 15%) Per. Naturales contribuyentes de GC 817 18316 1300
2005 Región de Tarapacá Tramo 5 - 70 a 90 UTA (Tasa 25%) Per. Naturales contribuyentes de GC 386 11509 1169
2005 Región de Tarapacá Tramo 6 - 90 a 120 UTA (Tasa 32%) Per. Naturales contribuyentes de GC 231 8933 1285

La tabla del terror quedó con una fila por región, tramo, año y categoría de contribuyente, con sus 3 variables como columnas.

Y así, decapitando planillas nivel por nivel, pasamos de dos tablas de Excel verdaderamente infernales a datos ordenados y listos para analizar 🎉

Fecha de publicación:
1/10/2026
Extensión:
21 minute read, 4424 words
Tags:
limpieza de datos procesamiento de datos tablas Excel
Ver también:
Creación de tablas atractivas y personalizables en R con `{gt}`
`{territorial}`: un paquete de R para trabajar con datos de comunas y regiones de Chile
Exporta tablas hechas con `{gt}` a formato Excel