¿Cómo hacemos filtrado y extracción de listas anidadas utilizando `purrr`?
Trabajar con datos JSON y listas anidadas es una tarea común en el análisis de datos moderno. Ya sea consumiendo APIs, procesando respuestas de servicios web, o manejando metadatos complejos, saber cómo filtrar y extraer información de estructuras anidadas es una habilidad esencial.
En este tutorial, exploraremos múltiples métodos para filtrar listas anidadas en R, centrándonos en el poderoso paquete purrr y su integración con el ecosistema tidyverse.
El Problema: Listas Dentro de Listas
Imagina que tienes metadatos de una tabla con información sobre sus campos, y necesitas identificar cuáles son de tipo "string". Esta es una situación típica cuando trabajas con:
- APIs REST
- Esquemas de bases de datos
- Metadatos de archivos
- Configuraciones JSON
Preparación: Creando Nuestro Ejemplo
Primero, creemos una estructura de lista anidada típica que podrías encontrar al parsear JSON:
# Instalar paquetes si es necesario
install.packages("purrr")
install.packages("dplyr")
install.packages("jsonlite")
# Cargar librerías
library(purrr)
library(dplyr)
library(jsonlite)
# Estructura de lista anidada
lista_anidada <- list(
fields = list(
list(
name = "id_distrito_local",
long_name = "id_distrito_local",
type = "integer",
format = "default"
),
list(
name = "cabecera_distrital_local",
type = "string",
format = "default"
),
list(
name = "id_municipio_local",
type = "integer",
format = "default"
)
)
)
# Visualizar la estructura
str(lista_anidada, max.level = 3)
Objetivo: Extraer solo los nombres de campos donde type == "string". En este caso, esperamos obtener "cabecera_distrital_local".
Método 1: purrr::keep() + purrr::map_chr() (Recomendado)
Este es el método más elegante y legible cuando trabajas con listas anidadas:
library(purrr)
# Filtrar y extraer en un pipeline
campos_string <- lista_anidada$fields %>%
purrr::keep(~ .x$type == "string") %>%
purrr::map_chr("name")
print(campos_string)
# [1] "cabecera_distrital_local"
¿Cómo Funciona?
purrr::keep(): Filtra la lista, manteniendo solo los elementos que cumplen la condición~ .x$type == "string": Función lambda que verifica si el tipo es "string"purrr::map_chr("name"): Extrae el campo "name" de cada elemento filtrado como character
Ventajas:
- ✅ Código limpio y legible
- ✅ Pipeline claro (se lee como una historia)
- ✅ Manejo automático de edge cases
- ✅ Performance eficiente
Método 2: purrr::map() con Filtro Explícito
Este método ofrece mayor control sobre el proceso de filtrado:
library(purrr)
# Mapear con condición, luego limpiar
campos_string <- lista_anidada$fields %>%
purrr::map(~ if(.x$type == "string") .x$name else NULL) %>%
purrr::compact() %>%
unlist()
print(campos_string)
# [1] "cabecera_distrital_local"
¿Cómo Funciona?
purrr::map(): Transforma cada elemento de la lista- Condicional: Si el tipo es "string", retorna el nombre; si no, retorna NULL
purrr::compact(): Elimina todos los elementos NULLunlist(): Convierte la lista en un vector
Ventajas:
- ✅ Lógica explícita y fácil de entender
- ✅ Fácil agregar condiciones complejas
- ✅ Útil cuando necesitas transformar antes de filtrar
Desventajas:
- ⚠️ Más verboso que
purrr::keep() - ⚠️ Requiere paso adicional con
purrr::compact()
Método 3: Convertir a Tibble Primero (Ideal para Análisis Complejos)
Cuando necesitas filtrar por múltiples criterios o realizar análisis más complejos, convertir a data frame primero es la mejor opción:
library(purrr)
library(dplyr)
# Paso 1: Convertir lista anidada a tibble
info_campos <- lista_anidada$fields %>%
purrr::map_df(~ data.frame(
name = .x$name,
type = .x$type,
long_name = ifelse(!is.null(.x$long_name), .x$long_name, NA),
format = ifelse(!is.null(.x$format), .x$format, NA),
stringsAsFactors = FALSE
))
# Visualizar el tibble
print(info_campos)
# name type long_name format
# 1 id_distrito_local integer id_distrito_local default
# 2 cabecera_distrital_local string <NA> default
# 3 id_municipio_local integer <NA> default
# Paso 2: Filtrar con dplyr
campos_string <- info_campos %>%
dplyr::filter(type == "string") %>%
dplyr::pull(name)
print(campos_string)
# [1] "cabecera_distrital_local"
Ventajas:
- ✅ Perfecto para análisis exploratorio: Puedes ver todos los datos en formato tabular
- ✅ Filtrado complejo: Fácil agregar múltiples condiciones
- ✅ Integración con dplyr: Todo el poder del tidyverse
- ✅ Exportable: El tibble se puede exportar fácilmente
Cuándo Usarlo:
- Necesitas filtrar por múltiples columnas
- Quieres explorar los datos visualmente primero
- Vas a realizar análisis adicionales
- Necesitas exportar el resultado
Ejemplo de Filtrado Complejo:
# Filtrado con múltiples condiciones
campos_filtrados <- info_campos %>%
dplyr::filter(
type == "string" | type == "integer",
format == "default",
!is.na(long_name)
) %>%
dplyr::pull(name)
print(campos_filtrados)
# [1] "id_distrito_local"
Método 4: Usando purrr::map_df() Directamente
Una variante más compacta del método anterior:
library(purrr)
library(dplyr)
# Todo en un pipeline
campos_string <- lista_anidada$fields %>%
purrr::map_df(~ tibble::tibble(
name = .x$name,
type = .x$type
)) %>%
dplyr::filter(type == "string") %>%
dplyr::pull(name)
print(campos_string)
# [1] "cabecera_distrital_local"
Integración con JSON: El Caso Real
En la práctica, probablemente recibirás estos datos como JSON. Aquí está cómo manejarlo:
library(jsonlite)
library(purrr)
# JSON de ejemplo (típico de una API)
json_data <- '{
"fields": [
{
"name": "id_distrito_local",
"long_name": "id_distrito_local",
"type": "integer",
"format": "default"
},
{
"name": "cabecera_distrital_local",
"type": "string",
"format": "default"
},
{
"name": "id_municipio_local",
"type": "integer",
"format": "default"
}
]
}'
# CRÍTICO: Parsear SIN simplificar
lista_anidada <- jsonlite::fromJSON(json_data, simplifyVector = FALSE)
# Ahora funciona perfecto con purrr::keep()
campos_string <- lista_anidada$fields %>%
purrr::keep(~ .x$type == "string") %>%
purrr::map_chr("name")
print(campos_string)
# [1] "cabecera_distrital_local"
⚠️ La Clave: simplifyVector = FALSE
Esta es la configuración más importante al trabajar con JSON anidado:
# ❌ MAL: Con simplificación (default)
lista_simplificada <- jsonlite::fromJSON(json_data)
# Resultado: data.frame, no lista anidada
# ✅ BIEN: Sin simplificación
lista_anidada <- jsonlite::fromJSON(json_data, simplifyVector = FALSE)
# Resultado: lista anidada que funciona con purrr::keep()
¿Por qué importa?
simplifyVector = TRUE(default) convierte arrays JSON en data.framessimplifyVector = FALSEmantiene la estructura de lista purapurrr::keep()funciona con listas, no con data.frames
Comparación de Métodos
| Método | Legibilidad | Performance | Flexibilidad | Mejor Para |
|---|---|---|---|---|
| purrr::keep() + map_chr() | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | Filtrado simple y rápido |
| map() + compact() | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Lógica condicional compleja |
| map_df() + dplyr | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Análisis exploratorio y filtrado complejo |
Casos de Uso Avanzados
Caso 1: Filtrado con Múltiples Condiciones
# Buscar campos numéricos con formato específico
campos_numericos_default <- lista_anidada$fields %>%
purrr::keep(~ .x$type %in% c("integer", "numeric") &&
!is.null(.x$format) &&
.x$format == "default") %>%
purrr::map_chr("name")
print(campos_numericos_default)
# [1] "id_distrito_local" "id_municipio_local"
Caso 2: Extraer Múltiples Propiedades
# Extraer nombre y tipo de campos string
info_strings <- lista_anidada$fields %>%
purrr::keep(~ .x$type == "string") %>%
purrr::map(~ list(
nombre = .x$name,
formato = .x$format
))
print(info_strings)
# [[1]]
# [[1]]$nombre
# [1] "cabecera_distrital_local"
#
# [[1]]$formato
# [1] "default"
Caso 3: Contar Tipos de Campos
# Análisis rápido de tipos
resumen_tipos <- lista_anidada$fields %>%
purrr::map_chr("type") %>%
table()
print(resumen_tipos)
# integer string
# 2 1
Caso 4: Validación de Esquema
# Verificar que todos los campos tengan 'name' y 'type'
campos_validos <- lista_anidada$fields %>%
purrr::map_lgl(~ !is.null(.x$name) && !is.null(.x$type)) %>%
all()
print(campos_validos)
# [1] TRUE
Manejo de Errores y Edge Cases
Caso 1: Campos Faltantes
# Lista con información incompleta
lista_incompleta <- list(
fields = list(
list(name = "campo1", type = "string"),
list(name = "campo2"), # Falta 'type'
list(type = "integer") # Falta 'name'
)
)
# Manejo seguro
campos_string_seguros <- lista_incompleta$fields %>%
purrr::keep(~ !is.null(.x$type) && .x$type == "string") %>%
purrr::map_chr(~ ifelse(!is.null(.x$name), .x$name, "sin_nombre"))
print(campos_string_seguros)
# [1] "campo1"
Caso 2: Lista Vacía
# Lista sin campos
lista_vacia <- list(fields = list())
# Manejo seguro que retorna vector vacío
campos_resultado <- lista_vacia$fields %>%
purrr::keep(~ .x$type == "string") %>%
purrr::map_chr("name")
print(campos_resultado)
# character(0)
Caso 3: Tipos NULL
# Filtrado con verificación NULL
campos_seguros <- lista_anidada$fields %>%
purrr::keep(~ !is.null(.x$type) && .x$type == "string") %>%
purrr::map_chr("name")
Patrones Comunes en el Mundo Real
Patrón 1: Procesar Respuesta de API
# Función reutilizable para procesar metadatos
procesar_metadatos <- function(json_response) {
datos <- jsonlite::fromJSON(json_response, simplifyVector = FALSE)
metadatos <- datos$fields %>%
purrr::map_df(~ tibble::tibble(
nombre = .x$name,
tipo = .x$type,
requerido = ifelse(!is.null(.x$required), .x$required, FALSE),
formato = ifelse(!is.null(.x$format), .x$format, NA)
))
return(metadatos)
}
Patrón 2: Validación de Esquema
# Validar que ciertos campos existan y sean del tipo correcto
validar_esquema <- function(lista_campos, campos_requeridos) {
campos_presentes <- lista_campos$fields %>%
purrr::map_chr("name")
todos_presentes <- all(campos_requeridos %in% campos_presentes)
if (!todos_presentes) {
faltantes <- setdiff(campos_requeridos, campos_presentes)
stop("Campos faltantes: ", paste(faltantes, collapse = ", "))
}
return(TRUE)
}
# Uso
validar_esquema(lista_anidada, c("id_distrito_local", "cabecera_distrital_local"))
Patrón 3: Transformación de Esquemas
# Convertir esquema de un formato a otro
transformar_esquema <- function(lista_campos) {
lista_campos$fields %>%
purrr::map(~ list(
field_name = .x$name,
data_type = switch(.x$type,
"string" = "TEXT",
"integer" = "INTEGER",
"numeric" = "REAL",
"UNKNOWN"
),
is_nullable = TRUE
))
}
Resumen de Mejores Prácticas
✅ Hacer
- Usar
simplifyVector = FALSEal parsear JSON conjsonlite::fromJSON() - Preferir
purrr::keep()para filtrado simple y legible - Convertir a tibble para análisis complejos con múltiples condiciones
- Manejar valores NULL explícitamente en tus condiciones
- Usar funciones lambda (
~) para mayor claridad
❌ Evitar
- No asumir que todos los campos existen
- No olvidar
simplifyVector = FALSEcon JSON anidado - No usar loops cuando
purrrpuede hacerlo más elegante - No ignorar edge cases (listas vacías, NULL values)
Conclusión
Dominar el filtrado de listas anidadas en R es esencial para trabajar eficientemente con datos modernos. Los métodos presentados te permiten elegir la herramienta adecuada según tu caso:
purrr::keep() + map_chr(): Tu herramienta principal para filtrado simplemap() + compact(): Para lógica condicional más complejamap_df() + dplyr: Para análisis exploratorio y filtrado complejo
La clave está en mantener la estructura de lista anidada (usando simplifyVector = FALSE con JSON) y aprovechar el poder del ecosistema purrr para manipulaciones elegantes y legibles.
Para profundizar más en purrr, consulta la documentación oficial: ?purrr::keep y ?purrr::map

Comentarios
Publicar un comentario