Procesado de datos en Python con collections

Publicado el 09 septiembre 2024 por Daniel Rodríguez @analyticslane

Python cuenta con múltiples estructuras de datos nativas del lenguaje (listas, diccionarios, tuplas y conjuntos) que son fundamentales para los desarrolladores. Aunque, para ciertas tareas complejas, pueden no ser suficiente. En estos casos, se puede recurrir al módulo collections. Este módulo proporciona estructuras de datos avanzadas que se puede usar para simplificar el procesado de datos en Python. En esta entrada, se analizarán cinco de estas estructuras: Counter, defaultdict, OrderedDict, namedtuple y deque.

Las colecciones de collections

En esta entrada se verá cómo usar las siguientes colecciones del módulo collections:

  • Counter es una subclase de diccionario especializada en contar elementos hashables. Es útil para llevar un registro de la frecuencia de los elementos en una colección, cómo contar caracteres en una cadena o palabras en una lista.
  • defaultdict es una subclase de diccionario que proporciona un valor por defecto para claves inexistentes. Eliminando la necesidad de verificar la existencia de una clave antes de modificar su valor, lo que simplifica el código y mejora la legibilidad.
  • OrderedDict es una subclase de diccionario que mantiene el orden de inserción de los elementos. Dado que los diccionarios son elementos "desordenados", es necesario recurrir a esta colección cuando se necesita garantizar el orden en estos objetos. Algo crucial cuando el orden de los datos afecta el resultado, como en el procesamiento de secuencias o la generación de informes.
  • namedtuple es una función para crear tuplas con nombre, es decir, tuplas cuyos elementos pueden ser accedidos mediante nombres en lugar de índices. Esto mejora la legibilidad del código y facilita la gestión de datos estructurados.
  • deque (double-ended queue) es una estructura de datos que permite añadir y quitar elementos por ambos extremos de manera eficiente. Es ideal para implementar colas y pilas, donde las operaciones de inserción y eliminación son frecuentes en ambos extremos.

Una vez visto qué hace cada una de las colecciones, se puede comprender mejor su funcionamiento a través de diferentes ejemplos prácticos.

Counter es una herramienta del módulo collections que se utiliza para contar elementos hashables de manera eficiente. Siendo especialmente útil cuando se necesita llevar un registro de la frecuencia de elementos en una secuencia. Lo que se puede ver en el siguiente código:

from collections import Counter

# Contar caracteres en una cadena
cadena = "abracadabra"
contador = Counter(cadena)
print(contador)

# Contar palabras en una lista
palabras = ["apple", "banana", "apple", "orange", "banana", "banana"]
contador_palabras = Counter(palabras)
print(contador_palabras)

# Métodos útiles de Counter
print(contador.most_common(2))  # Los dos elementos más comunes
print(list(contador.elements()))  # Lista de elementos expandida
Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
Counter({'banana': 3, 'apple': 2, 'orange': 1})
[('a', 5), ('b', 2)]
['a', 'a', 'a', 'a', 'a', 'b', 'b', 'r', 'r', 'c', 'd']

En este código, Counter no solo cuenta elementos, sino que también proporciona métodos útiles como most_common(), que devuelve una lista de los elementos más comunes, y elements(), que devuelve un iterador sobre los elementos. Estos métodos permiten analizar y manipular los datos contados de manera eficiente.

2. defaultdict

defaultdict es una subclase de diccionario que devuelve un valor por defecto si la clave no existe. Este valor por defecto se define cuando se crea el defaultdict y puede ser cualquier tipo de objeto o función que se pueda llamar. Lo que se puede ver en el siguiente código:

from collections import defaultdict

# Definir un defaultdict con valor por defecto como lista
default_dict = defaultdict(list)

# Añadir elementos a listas dentro del defaultdict
default_dict["fruits"].append("apple")
default_dict["fruits"].append("banana")
default_dict["vegetables"].append("carrot")
print(default_dict)


# Definir un defaultdict con valor por defecto como entero
count_dict = defaultdict(int)
items = ["apple", "banana", "apple", "orange"]
for item in items:
    count_dict[item] += 1
print(count_dict)
defaultdict(<class 'list'>, {'fruits': ['apple', 'banana'], 'vegetables': ['carrot']})
defaultdict(<class 'int'>, {'apple': 2, 'banana': 1, 'orange': 1})

En este código, defaultdict simplifica el código al eliminar la necesidad de inicializar manualmente los valores de las claves. En el primer ejemplo, cada nueva clave tiene como valor por defecto una lista vacía, permitiendo agregar elementos directamente. En el segundo ejemplo, cada nueva clave tiene un valor por defecto de 0, facilitando el conteo de elementos.

3. OrderedDict

OrderedDict es una subclase de diccionario que mantiene el orden de inserción de los elementos. A diferencia de los diccionarios normales (en versiones de Python anteriores a 3.7), OrderedDict recuerda el orden en que se insertaron los elementos, lo cual es útil en muchas aplicaciones donde el orden de los datos es importante. Lo que se puede ver en el siguiente código:

from collections import OrderedDict

# Crear un OrderedDict
ordered_dict = OrderedDict()
ordered_dict["apple"] = 3
ordered_dict["banana"] = 2
ordered_dict["orange"] = 1
print(ordered_dict)

# Iterar sobre un OrderedDict
for key, value in ordered_dict.items():
    print(key, value)

# Mover un elemento al final o al principio
ordered_dict.move_to_end("banana")
print(ordered_dict)

ordered_dict.move_to_end("banana", last=False)
print(ordered_dict)
OrderedDict([('apple', 3), ('banana', 2), ('orange', 1)])
apple 3
banana 2
orange 1
OrderedDict([('apple', 3), ('orange', 1), ('banana', 2)])
OrderedDict([('banana', 2), ('apple', 3), ('orange', 1)])

En este ejemplo, OrderedDict se usa para preservar el orden de inserción de los elementos. Esto puede ser crucial en aplicaciones donde el orden de los datos afecta el resultado, como en el procesamiento de secuencias o en la generación de informes. Además, métodos como move_to_end() permiten reordenar elementos de manera eficiente.

4. namedtuple

namedtuple es una función del módulo collections que permite crear tuplas con nombre. Esto significa que los elementos de la tupla pueden ser accedidos mediante nombres en lugar de índices, mejorando la legibilidad y manejabilidad del código. Lo que se puede ver en el siguiente código:

from collections import namedtuple

# Definir un namedtuple
Persona = namedtuple("Persona", ["nombre", "edad", "ocupacion"])

# Crear una instancia de namedtuple
persona1 = Persona("Juan", 28, "Ingeniero")
print(persona1)

# Acceder a los elementos por nombre
print(persona1.nombre)
print(persona1.edad)
print(persona1.ocupacion)

# Convertir namedtuple a diccionario
print(persona1._asdict())
Persona(nombre='Juan', edad=28, ocupacion='Ingeniero')
Juan
28
Ingeniero
{'nombre': 'Juan', 'edad': 28, 'ocupacion': 'Ingeniero'}

Como se puede ver en el código anterior, namedtuple mejora la legibilidad al permitir el acceso a los elementos por nombre en lugar de por índice. Esto es particularmente útil en aplicaciones que manejan datos estructurados, como registros de base de datos o respuestas de API. Además, namedtuple incluye métodos adicionales como _asdict(), que convierte la tupla en un diccionario, facilitando su manipulación.

deque ( double-ended queue) es una estructura de datos que permite añadir y quitar elementos por ambos extremos de manera eficiente. A diferencia de las listas, donde las operaciones de inserción y eliminación en ambos extremos pueden ser costosas en términos de tiempo, deque está optimizado para estas operaciones. El uso de esta componente se puede ver en el siguiente ejemplo:

from collections import deque

# Crear una deque
dq = deque()

# Añadir elementos al final y al principio
dq.append("a")
dq.append("b")
dq.appendleft("c")
print(dq)

# Quitar elementos del final y del principio
dq.pop()
dq.popleft()
print(dq)

# Rotar elementos
dq.extend(["d", "e", "f"])
print(dq)
dq.rotate(1)  # Rotar a la derecha
print(dq)
dq.rotate(-1)  # Rotar a la izquierda
print(dq)
deque(['c', 'a', 'b'])
deque(['a'])
deque(['a', 'd', 'e', 'f'])
deque(['f', 'a', 'd', 'e'])
deque(['a', 'd', 'e', 'f'])

Como se puede ver en el ejemplo, deque es extremadamente eficiente para operaciones que añaden o quitan elementos de ambos extremos. Métodos como append(), appendleft(), pop() y popleft() permiten manipular la estructura de datos fácilmente. Además, deque proporciona operaciones avanzadas como rotate(), que permite rotar los elementos, facilitando la implementación de algoritmos más complejos.

Conclusiones

El módulo collections de Python es un módulo que se debe conocer cualquier desarrollador. Al proporcionar estructuras de datos avanzadas que simplifican la gestión y manipulación de datos, se puede conseguir un código mas eficiencia y legible. Ya sea contando elementos con Counter, manejando valores por defecto con defaultdict, preservando el orden de inserción con OrderedDict, creando estructuras de datos legibles con namedtuple o manipulando secuencias de manera eficiente con deque, collections ofrece soluciones elegantes, fáciles de implementar y eficaces para resolver muchos problemas comunes.

Imagen de StockSnap en Pixabay