Agrupar Datos con Tuplas de Python
Aprende a agrupar datos con tuplas de Python: claves de diccionario, agrupación multi-campo, itertools.groupby, namedtuple, Counter y zip.
Las tuplas son ideales para agrupar datos en Python. Como una tupla es inmutable y hashable, puede servir como clave de diccionario — algo que una lista nunca puede hacer. Esto convierte a las tuplas en la opción natural cuando necesitas agrupar registros por una combinación de campos, rastrear coordenadas multidimensionales o contar eventos compuestos.
Este capítulo cubre cuatro patrones prácticos de agrupación:
- Tupla como clave de diccionario — agrupación por un solo campo y por múltiples campos
itertools.groupbycon tuplas — agrupación en flujo sobre secuencias ordenadascollections.namedtuple— añadir nombres a los registros agrupadoscollections.Countercon tuplas — contar eventos compuestos
Capítulos relacionados: Python Tuples · Access Tuples · Loop Tuples · Python Dictionaries · Python Lists Group
Por qué las Tuplas Pueden Ser Claves de Diccionario
Python requiere que las claves de diccionario sean hashables — su valor nunca debe cambiar después de que la clave sea almacenada. Las tuplas satisfacen esto porque son inmutables. Las listas no lo satisfacen y generan un TypeError cuando intentas usarlas como claves.
# A tuple can be a dictionary key
coordinates = {}
coordinates[(10, 20)] = "warehouse A"
coordinates[(30, 40)] = "warehouse B"
print(coordinates[(10, 20)]) # warehouse A
# A list cannot be a dictionary key
try:
d = {[10, 20]: "warehouse A"}
except TypeError as e:
print(f"TypeError: {e}")
# TypeError: unhashable type: 'list'Un punto importante a tener en cuenta: una tupla que contiene un elemento mutable (como una lista) también es no hashable y no puede usarse como clave:
try:
d = {(1, [2, 3]): "value"}
except TypeError as e:
print(f"TypeError: {e}")
# TypeError: unhashable type: 'list'Mantén las claves de tupla compuestas únicamente de valores inmutables — strings, números, booleanos u otras tuplas.
Agrupar por un Solo Campo de Tupla
El caso de uso más simple es desempaquetar una secuencia de tuplas y agrupar por un elemento. Usa collections.defaultdict(list) para evitar el código repetitivo de verificar si una clave ya existe.
from collections import defaultdict
employees = [
("Alice", "Engineering"),
("Bob", "Marketing"),
("Carol", "Engineering"),
("Dave", "Marketing"),
("Eve", "Engineering"),
]
by_dept = defaultdict(list)
for name, dept in employees:
by_dept[dept].append(name)
for dept, members in sorted(by_dept.items()):
print(f"{dept}: {members}")
# Engineering: ['Alice', 'Carol', 'Eve']
# Marketing: ['Bob', 'Dave']defaultdict(list) crea automáticamente una lista vacía la primera vez que se encuentra una nueva clave dept, por lo que no se necesita ninguna guarda if dept not in by_dept.
Agrupación Multi-Clave con una Clave de Tupla
El verdadero poder de las claves de tupla surge cuando necesitas agrupar por más de un campo a la vez. Combina los campos en una tupla y usa esa tupla como clave del diccionario.
from collections import defaultdict
records = [
("Alice", "Engineering", "Senior"),
("Bob", "Marketing", "Junior"),
("Carol", "Engineering", "Junior"),
("Dave", "Marketing", "Senior"),
("Eve", "Engineering", "Senior"),
]
# Group by (department, level) — a two-field composite key
grouped = defaultdict(list)
for name, dept, level in records:
grouped[(dept, level)].append(name)
for (dept, level), names in sorted(grouped.items()):
print(f"{dept} / {level}: {names}")
# Engineering / Junior: ['Carol']
# Engineering / Senior: ['Alice', 'Eve']
# Marketing / Junior: ['Bob']
# Marketing / Senior: ['Dave']Como (dept, level) es en sí mismo una tupla, es hashable y puede servir como clave de diccionario sin importar cuántos campos contenga. Desestructurar la clave con for (dept, level), names in ... mantiene el código legible.
Agrupación de grillas y coordenadas
La agrupación de tuplas multi-clave también maneja datos espaciales de forma natural:
points = [(0, 0), (1, 2), (0, 1), (1, 3), (2, 4)]
from collections import defaultdict
by_x = defaultdict(list)
for x, y in points:
by_x[x].append(y)
for x, ys in sorted(by_x.items()):
print(f"x={x}: y-values={ys}")
# x=0: y-values=[0, 1]
# x=1: y-values=[2, 3]
# x=2: y-values=[4]Agrupar con itertools.groupby
itertools.groupby agrupa elementos consecutivos que comparten la misma clave. Es eficiente en memoria porque es perezoso — no carga todos los grupos en memoria a la vez. La contrapartida es que la entrada debe estar ordenada por la misma clave antes de pasarla a groupby, de lo contrario obtendrás múltiples grupos parciales para la misma clave en lugar de uno solo.
from itertools import groupby
sales = [
("East", "Q1", 1200),
("East", "Q2", 1500),
("West", "Q1", 900),
("West", "Q2", 1100),
("East", "Q3", 1800),
]
# Sort by region (index 0) before grouping
sales_sorted = sorted(sales, key=lambda t: t[0])
for region, group in groupby(sales_sorted, key=lambda t: t[0]):
items = list(group)
total = sum(q[2] for q in items)
print(f"{region}: total={total}, quarters={[q[1] for q in items]}")
# East: total=4500, quarters=['Q1', 'Q2', 'Q3']
# West: total=2000, quarters=['Q1', 'Q2']Dos cosas a recordar al usar groupby con tuplas:
- Ordenar primero. Sin ordenar, cada nueva secuencia consecutiva del mismo valor de clave crea un grupo separado.
- Consumir el iterador del grupo de inmediato. El iterador interno
groupse agota cuando el bucle externo avanza a la siguiente clave. Llama siempre alist(group)dentro del cuerpo del bucle antes de usarlo en otro lugar.
Cuándo groupby es preferible a defaultdict
Usa groupby cuando proceses una secuencia grande ya ordenada donde no quieres cargar el resultado completo agrupado en memoria. Para agrupación de propósito general sin garantía de ordenamiento, defaultdict(list) es más simple y confiable.
Agrupar con collections.namedtuple
namedtuple te permite dar nombres a los campos de las tuplas, haciendo que los datos agrupados se documenten a sí mismos. Una vez que defines el tipo namedtuple, las instancias se comportan exactamente como tuplas regulares — son inmutables, hashables e iterables — pero los campos son accesibles por nombre además de por índice.
from collections import namedtuple, defaultdict
Employee = namedtuple("Employee", ["name", "department", "salary"])
employees = [
Employee("Alice", "Engineering", 95000),
Employee("Bob", "Marketing", 72000),
Employee("Carol", "Engineering", 88000),
Employee("Dave", "Marketing", 68000),
Employee("Eve", "Engineering", 102000),
]
by_dept = defaultdict(list)
for emp in employees:
by_dept[emp.department].append(emp)
for dept, members in sorted(by_dept.items()):
avg_salary = sum(e.salary for e in members) / len(members)
print(f"{dept}: {[e.name for e in members]}, avg salary={avg_salary:.0f}")
# Engineering: ['Alice', 'Carol', 'Eve'], avg salary=95000
# Marketing: ['Bob', 'Dave'], avg salary=70000Observa que emp.department y emp.salary se leen con más claridad que emp[1] y emp[2]. El enfoque con namedtuple es especialmente útil cuando la tupla tiene muchos campos y la indexación posicional se vuelve difícil de seguir.
Contar Eventos Compuestos con Counter
collections.Counter cuenta objetos hashables. Cuando el "objeto" que deseas contar es una combinación de valores, envuelve esos valores en una tupla y pasa la secuencia de tuplas a Counter.
from collections import Counter
log = [
("GET", 200),
("POST", 201),
("GET", 200),
("GET", 404),
("POST", 500),
("GET", 200),
("DELETE", 204),
]
counts = Counter(log)
for entry, n in counts.most_common():
method, status = entry
print(f"{method} {status}: {n} times")
# GET 200: 3 times
# POST 201: 1 times
# GET 404: 1 times
# POST 500: 1 times
# DELETE 204: 1 timesCounter usa la tupla como clave hash internamente, por lo que cada combinación única de (method, status) se rastrea de forma separada sin necesidad de código de agrupación manual.
Construir Grupos de Tuplas con zip
zip empareja elementos de dos o más secuencias en tuplas. Esta es una forma natural de ensamblar registros agrupados a partir de listas paralelas antes de aplicar una operación de agrupación.
from collections import defaultdict
names = ["Alice", "Bob", "Carol"]
scores = [95, 87, 92]
departments = ["Engineering", "Marketing", "Engineering"]
# Pair the three sequences into tuples
records = list(zip(names, scores, departments))
print(records)
# [('Alice', 95, 'Engineering'), ('Bob', 87, 'Marketing'), ('Carol', 92, 'Engineering')]
# Now group by department
by_dept = defaultdict(list)
for name, score, dept in records:
by_dept[dept].append((name, score))
for dept, members in sorted(by_dept.items()):
print(f"{dept}: {members}")
# Engineering: [('Alice', 95), ('Carol', 92)]
# Marketing: [('Bob', 87)]Elegir la Herramienta de Agrupación Correcta
| Objetivo | Mejor herramienta |
|---|---|
| Agrupar por un campo de una lista de tuplas | defaultdict(list) |
| Agrupar por dos o más campos simultáneamente | defaultdict(list) con clave de tupla |
| Agrupar en flujo una secuencia grande ya ordenada | itertools.groupby |
| Añadir nombres de campo a los registros agrupados | collections.namedtuple |
| Contar ocurrencias de eventos compuestos | collections.Counter |
| Ensamblar listas paralelas en tuplas agrupadas | zip |
Errores Comunes
Olvidar ordenar antes de groupby. itertools.groupby solo fusiona claves consecutivas idénticas. Si la misma clave aparece en múltiples posiciones no consecutivas, cada secuencia se convierte en un grupo separado. Ordena siempre por la misma función de clave antes de llamar a groupby.
Usar un valor mutable dentro de una clave de tupla. Una tupla que contiene una lista no es hashable y genera un TypeError cuando se usa como clave de diccionario. Mantén las claves de tupla compuestas de strings, números, booleanos o tuplas anidadas.
Consumir el iterador de groupby más de una vez. El sub-iterador del grupo de groupby se agota una vez que el bucle externo avanza. Llama a list(group) dentro del cuerpo del bucle si necesitas iterar el grupo más de una vez.
Tratar la salida de defaultdict como un dict normal. Un defaultdict crea automáticamente nuevas claves cuando lees una clave faltante, lo que puede poblar silenciosamente el diccionario con listas vacías. Si necesitas verificar la existencia de una clave sin crear nuevas entradas, conviértelo primero a un dict normal: dict(grouped).
Temas Relacionados
- Python Tuples — crear, indexar y cortar tuplas
- Access Tuples — indexación, corte y prueba de pertenencia
- Loop Tuples — iterar sobre una tupla con
forywhile - Unpack Tuples — asignar elementos de tupla a variables en una línea
- Update Tuples — métodos alternativos para modificar tuplas inmutables
- Join Tuples — concatenación y el constructor
tuple() - Tuple Methods —
count()eindex()en profundidad - Python Dictionaries — el almacén clave-valor que hace posible la agrupación con claves de tupla
- Python Lists Group — agrupar listas con
defaultdict,groupbyy comprensiones de diccionario - Python Collections Module —
defaultdict,Counter,namedtupley más