W3docs

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.groupby con tuplas — agrupación en flujo sobre secuencias ordenadas
  • collections.namedtuple — añadir nombres a los registros agrupados
  • collections.Counter con 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:

  1. Ordenar primero. Sin ordenar, cada nueva secuencia consecutiva del mismo valor de clave crea un grupo separado.
  2. Consumir el iterador del grupo de inmediato. El iterador interno group se agota cuando el bucle externo avanza a la siguiente clave. Llama siempre a list(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=70000

Observa 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 times

Counter 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

ObjetivoMejor herramienta
Agrupar por un campo de una lista de tuplasdefaultdict(list)
Agrupar por dos o más campos simultáneamentedefaultdict(list) con clave de tupla
Agrupar en flujo una secuencia grande ya ordenadaitertools.groupby
Añadir nombres de campo a los registros agrupadoscollections.namedtuple
Contar ocurrencias de eventos compuestoscollections.Counter
Ensamblar listas paralelas en tuplas agrupadaszip

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

Práctica

Práctica
Which of the following can be used as a Python dictionary key?
Which of the following can be used as a Python dictionary key?
Was this page helpful?