¿Cómo puedo expandir columnas anidadas en R? Guía completa de `tidyr::unnest_wider()`

 


Cuando trabajas con datos complejos en R, es común encontrar columnas anidadas que contienen tibbles o listas dentro de cada celda. La función tidyr::unnest_wider() es tu herramienta ideal cuando necesitas expandir estos datos horizontalmente, convirtiendo cada elemento de la lista en una columna separada.

¿Qué es tidyr::unnest_wider()?

tidyr::unnest_wider() es una función del paquete tidyr que expande columnas anidadas en múltiples columnas del tibble principal. A diferencia de tidyr::unnest() que expande en filas, unnest_wider() expande horizontalmente.

Sintaxis Básica

tibble_desanidado <- tibble_original %>% 
  tidyr::unnest_wider(columna_anidada)

Caso de Uso 1: Lista de Vectores Nombrados

Este es el caso más común: tienes una columna que contiene listas con elementos nombrados.


# Crear datos de ejemplo
datos_ejemplo <- tibble::tibble(
  id = 1:3,
  info = list(
    c(nombre = "Ana", edad = "25", ciudad = "Madrid"),
    c(nombre = "Luis", edad = "30", ciudad = "Barcelona"),
    c(nombre = "María", edad = "28", ciudad = "Valencia")
  )
)

# Ver estructura original
print(datos_ejemplo)
# # A tibble: 3 × 2
#   id info     
#   <int> <list>   
# 1     1 <chr [3]>
# 2     2 <chr [3]>
# 3     3 <chr [3]>

# Expandir horizontalmente
datos_expandidos <- datos_ejemplo %>% 
  tidyr::unnest_wider(info)

print(datos_expandidos)
# # A tibble: 3 × 4
#      id nombre edad  ciudad   
#   <int> <chr>  <chr> <chr>    
# 1     1 Ana    25    Madrid   
# 2     2 Luis   30    Barcelona
# 3     3 María  28    Valencia

¿Qué Sucedió?

Cada elemento de la lista info (nombre, edad, ciudad) se convirtió en su propia columna. Las tres columnas anidadas ahora son columnas independientes del tibble.

Caso de Uso 2: Columnas con Tibbles Anidados

Cuando tu columna anidada contiene tibbles completos:

# Datos con tibbles anidados
datos_complejos <- tibble::tibble(
  producto = c("Laptop", "Mouse", "Teclado"),
  especificaciones = list(
    tibble::tibble(marca = "Dell", precio = 1200, stock = 15),
    tibble::tibble(marca = "Logitech", precio = 25, stock = 100),
    tibble::tibble(marca = "Corsair", precio = 80, stock = 45)
  )
)

# Ver estructura
print(datos_complejos)
# # A tibble: 3 × 2
#   producto especificaciones
#   <chr>    <list>          
# 1 Laptop   <tibble [1 × 3]>
# 2 Mouse    <tibble [1 × 3]>
# 3 Teclado  <tibble [1 × 3]>

# Expandir
productos_expandidos <- datos_complejos %>% 
  tidyr::unnest_wider(especificaciones)

print(productos_expandidos)
# # A tibble: 3 × 4
#   producto marca    precio stock
#   <chr>    <chr>     <dbl> <dbl>
# 1 Laptop   Dell       1200    15
# 2 Mouse    Logitech     25   100
# 3 Teclado  Corsair      80    45

Caso de Uso 3: Listas con Diferentes Longitudes

¿Qué pasa cuando no todos los elementos tienen los mismos campos?

# Datos con estructura inconsistente
datos_inconsistentes <- tibble::tibble(
  usuario = c("user1", "user2", "user3"),
  datos = list(
    list(edad = 25, ciudad = "Madrid", profesion = "Ingeniero"),
    list(edad = 30, ciudad = "Barcelona"),  # Falta 'profesion'
    list(edad = 28, profesion = "Doctora")  # Falta 'ciudad'
  )
)

# Expandir con valores NA para elementos faltantes
usuarios_expandidos <- datos_inconsistentes %>% 
  tidyr::unnest_wider(datos)

print(usuarios_expandidos)
# # A tibble: 3 × 4
#   usuario  edad ciudad    profesion
#   <chr>   <dbl> <chr>     <chr>    
# 1 user1      25 Madrid    Ingeniero
# 2 user2      30 Barcelona NA       
# 3 user3      28 NA        Doctora

Nota importante: tidyr::unnest_wider() automáticamente rellena con NA los valores faltantes.

Parámetros Útiles de tidyr::unnest_wider()

1. names_sep: Añadir Prefijos a Columnas

# Útil cuando tienes múltiples columnas anidadas
datos_multiples <- tibble::tibble(
  id = 1:2,
  medicion_A = list(
    list(valor = 10, unidad = "kg"),
    list(valor = 15, unidad = "kg")
  ),
  medicion_B = list(
    list(valor = 20, unidad = "m"),
    list(valor = 25, unidad = "m")
  )
)

# Expandir con separador para evitar conflictos de nombres
datos_con_prefijo <- datos_multiples %>% 
  tidyr::unnest_wider(medicion_A, names_sep = "_") %>% 
  tidyr::unnest_wider(medicion_B, names_sep = "_")

print(datos_con_prefijo)
# # A tibble: 2 × 5
#      id medicion_A_valor medicion_A_unidad medicion_B_valor medicion_B_unidad
#   <int>            <dbl> <chr>                        <dbl> <chr>            
# 1     1               10 kg                              20 m                
# 2     2               15 kg                              25 m

2. names_repair: Manejo de Nombres Duplicados

# Datos con posibles nombres duplicados
datos_duplicados <- tibble::tibble(
  id = 1:2,
  datos = list(
    list(x = 1, y = 2, x = 3),  # 'x' está duplicado
    list(x = 4, y = 5, x = 6)
  )
)

# Usar names_repair para resolver conflictos
datos_reparados <- datos_duplicados %>% 
  tidyr::unnest_wider(datos, names_repair = "unique")

print(datos_reparados)
# Los nombres duplicados se renombran automáticamente (x, x...3)

3. simplify: Control de Simplificación

# Control sobre cómo se simplifican las listas
datos_lista <- tibble::tibble(
  grupo = c("A", "B"),
  valores = list(
    list(a = 1:3, b = 4:6),
    list(a = 7:9, b = 10:12)
  )
)

# Sin simplificar (mantiene como listas)
sin_simplificar <- datos_lista %>% 
  tidyr::unnest_wider(valores, simplify = FALSE)

# Con simplificación (intenta convertir a vectores)
con_simplificar <- datos_lista %>% 
  tidyr::unnest_wider(valores, simplify = TRUE)

Comparación: unnest_wider() vs unnest_longer()

Para entender mejor cuándo usar tidyr::unnest_wider(), veamos la diferencia con tidyr::unnest_longer():

# Datos de ejemplo
datos_comparacion <- tibble::tibble(
  id = c(1, 2),
  medidas = list(
    list(altura = 170, peso = 70),
    list(altura = 180, peso = 85)
  )
)

# Con unnest_wider (expandir en COLUMNAS)
wider_resultado <- datos_comparacion %>% 
  tidyr::unnest_wider(medidas)

print(wider_resultado)
# # A tibble: 2 × 3
#      id altura  peso
#   <dbl>  <dbl> <dbl>
# 1     1    170    70
# 2     2    180    85

# Con unnest_longer (expandir en FILAS)
longer_resultado <- datos_comparacion %>% 
  tidyr::unnest_longer(medidas)

print(longer_resultado)
# # A tibble: 4 × 3
#      id medidas medidas_id
#   <dbl>   <dbl> <chr>     
# 1     1     170 altura    
# 2     1      70 peso      
# 3     2     180 altura    
# 4     2      85 peso

Regla de oro:

  • Usa tidyr::unnest_wider() cuando quieras cada elemento como columna separada
  • Usa tidyr::unnest_longer() cuando quieras cada elemento como fila separada

Caso Práctico: Datos de APIs JSON

Un uso común de tidyr::unnest_wider() es procesar respuestas de APIs que vienen en formato JSON:

# Simular respuesta de API
respuesta_api <- tibble::tibble(
  endpoint = c("/users/1", "/users/2", "/users/3"),
  respuesta = list(
    list(nombre = "Ana García", email = "ana@email.com", activo = TRUE, edad = 28),
    list(nombre = "Luis Pérez", email = "luis@email.com", activo = TRUE, edad = 35),
    list(nombre = "María López", email = "maria@email.com", activo = FALSE, edad = 42)
  )
)

# Procesar respuesta
usuarios_procesados <- respuesta_api %>% 
  tidyr::unnest_wider(respuesta) %>% 
  dplyr::mutate(
    edad_grupo = dplyr::case_when(
      edad < 30 ~ "Joven",
      edad < 40 ~ "Adulto",
      TRUE ~ "Senior"
    )
  ) %>% 
  dplyr::select(nombre, email, edad, edad_grupo, activo)

print(usuarios_procesados)
# # A tibble: 3 × 5
#   nombre       email            edad edad_grupo activo
#   <chr>        <chr>           <dbl> <chr>      <lgl> 
# 1 Ana García   ana@email.com      28 Joven      TRUE  
# 2 Luis Pérez   luis@email.com     35 Adulto     TRUE  
# 3 María López  maria@email.com    42 Senior     FALSE

Combinando tidyr::unnest_wider() con dplyr

La verdadera potencia viene al combinar con otras funciones del tidyverse:

# Ejemplo completo: análisis de ventas por región
ventas_anidadas <- tibble::tibble(
  region = c("Norte", "Sur", "Este"),
  metricas = list(
    list(ventas = 50000, clientes = 120, ticket_promedio = 416.67),
    list(ventas = 35000, clientes = 85, ticket_promedio = 411.76),
    list(ventas = 62000, clientes = 145, ticket_promedio = 427.59)
  )
)

# Pipeline completo de análisis
analisis_ventas <- ventas_anidadas %>% 
  tidyr::unnest_wider(metricas) %>% 
  dplyr::mutate(
    categoria = dplyr::case_when(
      ventas > 60000 ~ "Alta",
      ventas > 40000 ~ "Media",
      TRUE ~ "Baja"
    ),
    eficiencia = ventas / clientes
  ) %>% 
  dplyr::arrange(dplyr::desc(ventas)) %>% 
  dplyr::select(region, categoria, ventas, clientes, eficiencia)

print(analisis_ventas)
# # A tibble: 3 × 5
#   region categoria ventas clientes eficiencia
#   <chr>  <chr>      <dbl>    <dbl>      <dbl>
# 1 Este   Alta       62000      145       428.
# 2 Norte  Media      50000      120       417.
# 3 Sur    Baja       35000       85       412.

Errores Comunes y Soluciones

Error 1: Columna No Es una Lista

# ❌ ERROR
datos_mal <- tibble::tibble(
  id = 1:3,
  valor = c(10, 20, 30)  # No es una lista
)

# Esto dará error
# datos_mal %>% tidyr::unnest_wider(valor)

# ✅ SOLUCIÓN: Verificar estructura primero
print(class(datos_mal$valor))  # "numeric", no "list"

Error 2: Nombres de Columnas Conflictivos

# ❌ PROBLEMA
datos_conflicto <- tibble::tibble(
  id = 1:2,
  nombre = c("A", "B"),
  datos = list(
    list(nombre = "Ana", edad = 25),  # 'nombre' ya existe
    list(nombre = "Luis", edad = 30)
  )
)

# ✅ SOLUCIÓN 1: Usar names_sep
solucion1 <- datos_conflicto %>% 
  tidyr::unnest_wider(datos, names_sep = "_")

# ✅ SOLUCIÓN 2: Renombrar antes
solucion2 <- datos_conflicto %>% 
  dplyr::rename(grupo = nombre) %>% 
  tidyr::unnest_wider(datos)

Error 3: Elementos No Nombrados

# ❌ PROBLEMA: Lista sin nombres
datos_sin_nombres <- tibble::tibble(
  id = 1:2,
  valores = list(
    c(10, 20, 30),  # Sin nombres
    c(15, 25, 35)
  )
)

# ✅ SOLUCIÓN: Asignar nombres primero
datos_corregidos <- datos_sin_nombres %>% 
  dplyr::mutate(
    valores = purrr::map(valores, ~{
      names(.x) <- c("valor1", "valor2", "valor3")
      .x
    })
  ) %>% 
  tidyr::unnest_wider(valores)

Cuándo Usar tidyr::unnest_wider()

Usa tidyr::unnest_wider() cuando:

  • ✅ Tienes una columna con listas de elementos nombrados
  • ✅ Quieres que cada elemento sea una columna separada
  • ✅ Estás procesando respuestas de APIs JSON
  • ✅ Necesitas "aplanar" estructuras de datos complejas
  • ✅ Los elementos dentro de cada lista representan diferentes atributos del mismo objeto

NO uses tidyr::unnest_wider() cuando:

  • ❌ Quieres expandir en filas (usa tidyr::unnest_longer())
  • ❌ Tu columna no contiene listas (no es necesario)
  • ❌ Cada lista tiene longitud variable de elementos sin nombres consistentes

Resumen de Funciones Relacionadas

Función Propósito Dirección de Expansión
tidyr::unnest_wider() Expandir listas en columnas Horizontal (→)
tidyr::unnest_longer() Expandir listas en filas Vertical (↓)
tidyr::unnest() Expandir tibbles anidados Vertical (↓)
tidyr::hoist() Extraer elementos específicos Selectiva

Conclusión

tidyr::unnest_wider() es una herramienta esencial para trabajar con datos complejos y anidados en R. Su capacidad para transformar estructuras de listas en columnas separadas la hace indispensable cuando trabajas con:

  • Respuestas de APIs
  • Datos JSON
  • Resultados de análisis complejos
  • Estructuras de datos anidadas

Puntos clave para recordar:

  1. Expande horizontalmente (en columnas)
  2. Requiere que la columna sea una lista
  3. Los elementos deben tener nombres para crear columnas
  4. Automáticamente maneja valores faltantes con NA
  5. Se combina perfectamente con dplyr para análisis completos

Dominar tidyr::unnest_wider() te permitirá trabajar con datos complejos de manera eficiente y mantener tu código limpio y legible dentro del ecosistema tidyverse.


Para más información sobre manipulación de datos anidados, consulta la documentación oficial de tidyr: ?tidyr::unnest_wider

Comentarios

Entradas más populares de este blog

¿Cómo comparar nombres de equipos de distintas fuentes?

¿Cómo hacemos filtrado y extracción de listas anidadas utilizando `purrr`?