Agrupación de listas en Python
Aprende tres formas de agrupar listas en Python: defaultdict, itertools.groupby y comprensiones de diccionario, con ejemplos ejecutables y errores comunes.
Agrupar una lista significa dividir sus elementos en subcolecciones que comparten una clave común — por ejemplo, agrupar palabras por su primera letra, o agrupar registros por un campo de categoría. Python ofrece tres enfoques principales: un bucle manual con collections.defaultdict, itertools.groupby de la biblioteca estándar, y comprensiones de diccionario. Este capítulo explica cada técnica, cuándo elegir una sobre otra y los errores que se deben evitar.
Capítulos relacionados: Listas de Python · Métodos de lista · Comprensión de listas · Recorrer listas · Módulo collections
Qué significa "agrupar"
Dada una lista plana y una función clave que asigna cada elemento a una etiqueta de grupo, el objetivo es producir un mapeo de cada etiqueta a la lista de elementos que le pertenecen:
['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']
key = first letter
→ {'a': ['apple', 'avocado', 'apricot'],
'b': ['banana', 'blueberry'],
'c': ['cherry']}Las tres técnicas descritas a continuación producen este tipo de resultado. Difieren en verbosidad, rendimiento y las restricciones que imponen sobre la entrada.
Técnica 1: Bucle manual con defaultdict
collections.defaultdict es el enfoque más común y flexible. Cuando se accede a una clave que aún no existe, un defaultdict(list) crea automáticamente una lista vacía para esa clave, por lo que nunca se necesita una comprobación del tipo if key in d.
from collections import defaultdict
words = ['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']
by_letter = defaultdict(list)
for word in words:
by_letter[word[0]].append(word)
for letter, group in sorted(by_letter.items()):
print(f'{letter}: {group}')
# a: ['apple', 'avocado', 'apricot']
# b: ['banana', 'blueberry']
# c: ['cherry']¿Por qué usar defaultdict en lugar de un dict normal?
Con un dict normal se necesita una comprobación explícita antes del primer append:
# Plain dict — more boilerplate, same result
by_letter = {}
for word in words:
if word[0] not in by_letter:
by_letter[word[0]] = []
by_letter[word[0]].append(word)Una alternativa más corta con un dict normal es dict.setdefault:
by_letter = {}
for word in words:
by_letter.setdefault(word[0], []).append(word)setdefault está bien para scripts cortos, pero defaultdict es más rápido (sin búsquedas de clave repetidas) y más explícito en cuanto a la intención.
Agrupación por una clave calculada
La clave puede ser cualquier expresión, no solo un atributo. Aquí, una lista de enteros se divide en grupos pares e impares:
from collections import defaultdict
numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
by_parity = defaultdict(list)
for n in numbers:
by_parity['even' if n % 2 == 0 else 'odd'].append(n)
print('even:', sorted(by_parity['even'])) # even: [2, 4, 6]
print('odd:', sorted(by_parity['odd'])) # odd: [1, 1, 3, 3, 5, 5, 5, 9]Agrupación de una lista de dicts
Este es el escenario más común en el mundo real: agrupar filas de datos por el valor de un campo:
from collections import defaultdict
data = [
{'category': 'fruit', 'name': 'apple'},
{'category': 'vegetable', 'name': 'carrot'},
{'category': 'fruit', 'name': 'banana'},
{'category': 'vegetable', 'name': 'broccoli'},
]
grouped = defaultdict(list)
for item in data:
grouped[item['category']].append(item['name'])
for category, names in grouped.items():
print(f'{category}: {names}')
# fruit: ['apple', 'banana']
# vegetable: ['carrot', 'broccoli']Técnica 2: itertools.groupby
itertools.groupby agrupa elementos consecutivos que comparten la misma clave. Es útil cuando se necesita preservar la estructura de secuencias repetidas o cuando los datos ya están ordenados y se desea evitar construir todo el diccionario de una vez (es perezoso/en streaming).
from itertools import groupby
words = ['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']
# groupby only groups consecutive elements, so sort first
words_sorted = sorted(words, key=lambda w: w[0])
for letter, group in groupby(words_sorted, key=lambda w: w[0]):
print(f'{letter}: {list(group)}')
# a: ['apple', 'avocado', 'apricot']
# b: ['banana', 'blueberry']
# c: ['cherry']El error crítico: ordenar antes de groupby
groupby solo agrupa elementos consecutivos que comparten la misma clave. Si la entrada no está ordenada por la clave, se obtienen varios grupos pequeños en lugar de un grupo por clave:
from itertools import groupby
# Unsorted input — groupby produces WRONG results
numbers = [1, 1, 2, 3, 3, 1, 2, 2]
for key, group in groupby(numbers):
print(f'{key}: {list(group)}')
# 1: [1, 1] ← first run of 1s
# 2: [2]
# 3: [3, 3]
# 1: [1] ← second run of 1s — NOT merged with the first!
# 2: [2, 2]Siempre ordena con la misma función clave antes de llamar a groupby:
numbers_sorted = sorted(numbers)
for key, group in groupby(numbers_sorted):
print(f'{key}: {list(group)}')
# 1: [1, 1, 1]
# 2: [2, 2, 2]
# 3: [3, 3]Cuándo brilla groupby: procesamiento de datos grandes en streaming
Como groupby devuelve un iterador, no carga todos los grupos en memoria a la vez. Esto lo hace útil para procesar archivos grandes ordenados línea a línea sin construir un diccionario completo.
from itertools import groupby
# Grouping namedtuple records
from collections import namedtuple
Product = namedtuple('Product', ['category', 'name', 'price'])
products = [
Product('dairy', 'milk', 1.10),
Product('fruit', 'apple', 1.20),
Product('fruit', 'banana', 0.50),
Product('vegetable', 'broccoli', 1.50),
Product('vegetable', 'carrot', 0.80),
]
# products is already sorted by category here
for category, group in groupby(products, key=lambda p: p.category):
items = list(group)
print(f'{category}: {[p.name for p in items]}')
# dairy: ['milk']
# fruit: ['apple', 'banana']
# vegetable: ['broccoli', 'carrot']Técnica 3: Comprensión de diccionario
Una comprensión de diccionario construye el dict agrupado en una sola expresión. Es concisa pero tiene una desventaja: la comprensión de lista interna reescanea toda la entrada por cada clave única, lo que la hace O(n × k) donde k es el número de claves únicas. Para listas pequeñas esto está bien; para listas grandes, es preferible usar defaultdict.
words = ['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']
# Collect unique keys first, then build each group
letters = sorted(set(w[0] for w in words))
grouped = {letter: [w for w in words if w[0] == letter] for letter in letters}
for letter, group in grouped.items():
print(f'{letter}: {group}')
# a: ['apple', 'avocado', 'apricot']
# b: ['banana', 'blueberry']
# c: ['cherry']Esta técnica es más legible cuando el conjunto de claves es pequeño y ya se conoce — por ejemplo, agrupando resultados True/False o un conjunto fijo de categorías.
Agregación de grupos tras la agrupación
Un paso habitual después de agrupar es la agregación: calcular una suma, promedio, mínimo o conteo por grupo. Combina defaultdict(list) con aritmética estándar de Python:
from collections import defaultdict
scores = [
('Alice', 90), ('Bob', 75), ('Alice', 85),
('Bob', 88), ('Carol', 92),
]
by_student = defaultdict(list)
for name, score in scores:
by_student[name].append(score)
for student, student_scores in sorted(by_student.items()):
avg = sum(student_scores) / len(student_scores)
print(f'{student}: scores={student_scores}, avg={avg:.1f}')
# Alice: scores=[90, 85], avg=87.5
# Bob: scores=[75, 88], avg=81.5
# Carol: scores=[92], avg=92.0Cómo elegir la técnica adecuada
| Situación | Mejor opción |
|---|---|
| Agrupación general, cualquier orden | defaultdict(list) |
| Necesidad de procesar en streaming datos grandes y ordenados | itertools.groupby |
| Lista pequeña, expresión concisa en una línea | Comprensión de diccionario |
| La entrada ya está ordenada | defaultdict o groupby indistintamente |
| Necesidad de agregar (suma, promedio, etc.) | defaultdict(list) + aritmética |
Errores comunes
Olvidar ordenar antes de groupby. groupby solo fusiona claves idénticas consecutivas. Siempre ordena la entrada con sorted() usando la misma función clave antes de pasarla a groupby.
Asignar list(group) inmediatamente. El iterador de grupo de groupby se agota en cuanto el bucle for externo avanza a la siguiente clave. Conviértelo a una lista dentro del cuerpo del bucle si necesitas usarlo más de una vez.
Modificar la lista de entrada durante la agrupación. Añadir o eliminar elementos de la lista durante un bucle de agrupación produce resultados impredecibles. Construye primero el dict agrupado y luego modifica los elementos.
defaultdict aparece en repr. defaultdict(list, {...}) se ve diferente de un dict normal en repr. Envuélvelo con dict(grouped) cuando necesites una salida de dict normal.