Diferentes formas de representar registros

Intermediate

This tutorial is from open-source community. Access the source code

Introducción

En este laboratorio, aprenderás a explorar formas de almacenamiento de grandes conjuntos de datos en Python que sean eficientes en términos de memoria. También descubrirás diferentes formas de representar registros, como tuplas, diccionarios, clases y tuplas con nombre (named tuples).

Además, compararás el uso de memoria de diferentes estructuras de datos. Comprender las compensaciones entre estas estructuras es valioso para los usuarios de Python que realizan análisis de datos, ya que ayuda a optimizar el código.

Este es un Guided Lab, que proporciona instrucciones paso a paso para ayudarte a aprender y practicar. Sigue las instrucciones cuidadosamente para completar cada paso y obtener experiencia práctica. Los datos históricos muestran que este es un laboratorio de nivel principiante con una tasa de finalización del 86%. Ha recibido una tasa de reseñas positivas del 98% por parte de los estudiantes.

Explorando el Conjunto de Datos (Dataset)

Comencemos nuestro viaje echando un vistazo de cerca al conjunto de datos (dataset) con el que vamos a trabajar. El archivo ctabus.csv es un archivo CSV (Comma-Separated Values, valores separados por comas). Los archivos CSV son una forma común de almacenar datos tabulares, donde cada línea representa una fila, y los valores dentro de una fila están separados por comas. Este archivo en particular contiene datos diarios de pasajeros del sistema de autobuses de la Autoridad de Tránsito de Chicago (CTA, Chicago Transit Authority), que cubren el período del 1 de enero de 2001 al 31 de agosto de 2013.

Descomprime el archivo y elimina el archivo zip:

cd /home/labex/project
unzip ctabus.csv.zip
rm ctabus.csv.zip

Para comprender la estructura de este archivo, primero echaremos un vistazo dentro de él. Usaremos Python para leer el archivo e imprimir algunas líneas. Abre una terminal y ejecuta el siguiente código Python:

f = open('/home/labex/project/ctabus.csv')
print(next(f))  ## Read the header line
print(next(f))  ## Read the first data line
print(next(f))  ## Read the second data line
f.close()

En este código, primero abrimos el archivo usando la función open y lo asignamos a la variable f. La función next se utiliza para leer la siguiente línea del archivo. La usamos tres veces: la primera vez para leer la línea de encabezado (header), que generalmente contiene los nombres de las columnas en el conjunto de datos (dataset). La segunda y tercera vez, leemos la primera y segunda línea de datos respectivamente. Finalmente, cerramos el archivo usando el método close para liberar recursos del sistema.

Deberías ver una salida similar a esta:

route,date,daytype,rides

3,01/01/2001,U,7354

4,01/01/2001,U,9288

Esta salida muestra que el archivo tiene 4 columnas de datos. Desglosemos lo que representa cada columna:

  1. route: Este es el nombre o número de la ruta del autobús. Es la primera columna (Columna 0) en el conjunto de datos (dataset).
  2. date: Es una cadena de fecha (date string) en el formato MM/DD/AAAA. Esta es la segunda columna (Columna 1).
  3. daytype: Es un código de tipo de día, que es la tercera columna (Columna 2).
    • U = Domingo/Festivo (Sunday/Holiday)
    • A = Sábado (Saturday)
    • W = Día de la semana (Weekday)
  4. rides: Esta columna registra el número total de pasajeros como un entero. Es la cuarta columna (Columna 3).

La columna rides nos dice cuántas personas abordaron un autobús en una ruta específica en un día determinado. Por ejemplo, de la salida anterior, podemos ver que 7,354 personas viajaron en el autobús número 3 el 1 de enero de 2001.

Ahora, averigüemos cuántas líneas hay en el archivo. Conocer el número de líneas nos dará una idea del tamaño de nuestro conjunto de datos (dataset). Ejecuta el siguiente código Python:

with open('/home/labex/project/ctabus.csv') as f:
    line_count = sum(1 for line in f)
    print(f"Total lines in the file: {line_count}")

En este código, usamos la declaración with para abrir el archivo. La ventaja de usar with es que se encarga automáticamente de cerrar el archivo cuando terminamos con él. Luego usamos una expresión generadora (1 for line in f) para crear una secuencia de 1s, uno por cada línea en el archivo. La función sum suma todos estos 1s, dándonos el número total de líneas en el archivo. Finalmente, imprimimos el resultado.

Esto debería generar aproximadamente 577,564 líneas, lo que significa que estamos lidiando con un conjunto de datos (dataset) sustancial. Este gran conjunto de datos (dataset) nos proporcionará muchos datos para analizar y extraer información.

Medición del uso de memoria con diferentes métodos de almacenamiento

En este paso, vamos a ver cómo diferentes formas de almacenar datos pueden afectar el uso de memoria. El uso de memoria es un aspecto importante de la programación, especialmente cuando se trabaja con grandes conjuntos de datos. Para medir la memoria utilizada por nuestro código de Python, usaremos el módulo tracemalloc de Python. Este módulo es muy útil ya que nos permite realizar un seguimiento de las asignaciones de memoria realizadas por Python. Al usarlo, podemos ver cuánta memoria están consumiendo nuestros métodos de almacenamiento de datos.

Método 1: Almacenar todo el archivo como una sola cadena

Comencemos creando un nuevo archivo de Python. Navega al directorio /home/labex/project y crea un archivo llamado memory_test1.py. Puedes usar un editor de texto para abrir este archivo. Una vez abierto el archivo, agrega el siguiente código. Este código leerá todo el contenido de un archivo como una sola cadena y medirá el uso de memoria.

## memory_test1.py
import tracemalloc

def test_single_string():
    ## Start tracking memory
    tracemalloc.start()

    ## Read the entire file as a single string
    with open('/home/labex/project/ctabus.csv') as f:
        data = f.read()

    ## Get memory usage statistics
    current, peak = tracemalloc.get_traced_memory()

    print(f"File length: {len(data)} characters")
    print(f"Current memory usage: {current/1024/1024:.2f} MB")
    print(f"Peak memory usage: {peak/1024/1024:.2f} MB")

    ## Stop tracking memory
    tracemalloc.stop()

if __name__ == "__main__":
    test_single_string()

Después de agregar el código, guarda el archivo. Ahora, para ejecutar este script, abre tu terminal y ejecuta el siguiente comando:

python3 /home/labex/project/memory_test1.py

Cuando ejecutes el script, deberías ver una salida similar a esta:

File length: 12361039 characters
Current memory usage: 11.80 MB
Peak memory usage: 23.58 MB

Los números exactos pueden ser diferentes en tu sistema, pero generalmente, notarás que el uso de memoria actual es de alrededor de 12 MB y el uso de memoria máximo es de aproximadamente 24 MB.

Método 2: Almacenar como una lista de cadenas

A continuación, probaremos otra forma de almacenar los datos. Crea un nuevo archivo llamado memory_test2.py en el mismo directorio /home/labex/project. Abre este archivo en el editor y agrega el siguiente código. Este código lee el archivo y almacena cada línea como una cadena separada en una lista, y luego mide el uso de memoria.

## memory_test2.py
import tracemalloc

def test_list_of_strings():
    ## Start tracking memory
    tracemalloc.start()

    ## Read the file as a list of strings (one string per line)
    with open('/home/labex/project/ctabus.csv') as f:
        lines = f.readlines()

    ## Get memory usage statistics
    current, peak = tracemalloc.get_traced_memory()

    print(f"Number of lines: {len(lines)}")
    print(f"Current memory usage: {current/1024/1024:.2f} MB")
    print(f"Peak memory usage: {peak/1024/1024:.2f} MB")

    ## Stop tracking memory
    tracemalloc.stop()

if __name__ == "__main__":
    test_list_of_strings()

Guarda el archivo y luego ejecuta el script usando el siguiente comando en la terminal:

python3 /home/labex/project/memory_test2.py

Deberías ver una salida similar a esta:

Number of lines: 577564
Current memory usage: 43.70 MB
Peak memory usage: 43.74 MB

Observa que el uso de memoria ha aumentado significativamente en comparación con el método anterior de almacenar los datos como una sola cadena. Esto se debe a que cada línea en la lista es un objeto de cadena de Python separado, y cada objeto tiene su propio gasto de memoria.

Entendiendo la diferencia de memoria

La diferencia en el uso de memoria entre los dos enfoques muestra un concepto importante en la programación de Python llamado gasto de objeto. Cuando almacenas datos como una lista de cadenas, cada cadena es un objeto de Python separado. Cada objeto tiene algunos requisitos de memoria adicionales, que incluyen:

  1. El encabezado del objeto de Python (generalmente 16 - 24 bytes por objeto). Este encabezado contiene información sobre el objeto, como su tipo y el recuento de referencias.
  2. La representación real de la cadena en sí, que almacena los caracteres de la cadena.
  3. Relleno de alineación de memoria. Este es un espacio adicional agregado para garantizar que la dirección de memoria del objeto esté correctamente alineada para un acceso eficiente.

Por otro lado, cuando almacenas todo el contenido del archivo como una sola cadena, solo hay un objeto, y por lo tanto solo un conjunto de gastos. Esto lo hace más eficiente en términos de memoria cuando se considera el tamaño total de los datos.

Al diseñar programas que trabajen con grandes conjuntos de datos, debes considerar este compromiso entre la eficiencia de memoria y la accesibilidad de los datos. A veces, puede ser más conveniente acceder a los datos cuando se almacenan en una lista de cadenas, pero usará más memoria. En otras ocasiones, es posible que desees priorizar la eficiencia de memoria y optar por almacenar los datos como una sola cadena.

Trabajando con datos estructurados usando tuplas

Hasta ahora, hemos estado trabajando con el almacenamiento de datos de texto sin procesar. Pero cuando se trata de análisis de datos, generalmente necesitamos transformar los datos en formatos más organizados y estructurados. Esto facilita la realización de diversas operaciones y la obtención de información a partir de los datos. En este paso, aprenderemos cómo leer datos como una lista de tuplas utilizando el módulo csv. Las tuplas son una estructura de datos simple y útil en Python que puede contener múltiples valores.

Creando una función lectora con tuplas

Vamos a crear un nuevo archivo llamado readrides.py en el directorio /home/labex/project. Este archivo contendrá el código para leer los datos de un archivo CSV y almacenarlos como una lista de tuplas.

## readrides.py
import csv
import tracemalloc

def read_rides_as_tuples(filename):
    '''
    Read the bus ride data as a list of tuples
    '''
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        headings = next(rows)     ## Skip headers
        for row in rows:
            route = row[0]
            date = row[1]
            daytype = row[2]
            rides = int(row[3])
            record = (route, date, daytype, rides)
            records.append(record)
    return records

if __name__ == '__main__':
    tracemalloc.start()

    rows = read_rides_as_tuples('/home/labex/project/ctabus.csv')

    current, peak = tracemalloc.get_traced_memory()
    print(f'Number of records: {len(rows)}')
    print(f'First record: {rows[0]}')
    print(f'Second record: {rows[1]}')
    print(f'Memory Use: Current {current/1024/1024:.2f} MB, Peak {peak/1024/1024:.2f} MB')

Este script define una función llamada read_rides_as_tuples. Esto es lo que hace paso a paso:

  1. Abre el archivo CSV especificado por el parámetro filename. Esto nos permite acceder a los datos dentro del archivo.
  2. Utiliza el módulo csv para analizar cada línea del archivo. La función csv.reader nos ayuda a dividir las líneas en valores individuales.
  3. Extrae los cuatro campos (ruta, fecha, tipo de día y número de viajes) de cada fila. Estos campos son importantes para nuestro análisis de datos.
  4. Convierte el campo 'rides' a un entero. Esto es necesario porque los datos en el archivo CSV están inicialmente en formato de cadena, y necesitamos un valor numérico para realizar cálculos.
  5. Crea una tupla con estos cuatro valores. Las tuplas son inmutables, lo que significa que sus valores no se pueden cambiar una vez creados.
  6. Agrega la tupla a una lista llamada records. Esta lista contendrá todos los registros del archivo CSV.

Ahora, vamos a ejecutar el script. Abre tu terminal y escribe el siguiente comando:

python3 /home/labex/project/readrides.py

Deberías ver una salida similar a esta:

Number of records: 577563
First record: ('3', '01/01/2001', 'U', 7354)
Second record: ('4', '01/01/2001', 'U', 9288)
Memory Use: Current 89.12 MB, Peak 89.15 MB

Observa que el uso de memoria ha aumentado en comparación con nuestros ejemplos anteriores. Hay algunas razones para esto:

  1. Ahora estamos almacenando los datos en un formato estructurado (tuplas). Los datos estructurados generalmente requieren más memoria porque tienen una organización definida.
  2. Cada valor en la tupla es un objeto de Python separado. Los objetos de Python tienen cierto gasto, lo que contribuye al aumento del uso de memoria.
  3. Tenemos una estructura de lista adicional que contiene todas estas tuplas. Las listas también ocupan memoria para almacenar sus elementos.

La ventaja de utilizar este enfoque es que nuestros datos ahora están correctamente estructurados y listos para el análisis. Podemos acceder fácilmente a campos específicos de cada registro por índice. Por ejemplo:

## Example of accessing tuple elements (add this to readrides.py file to try it)
first_record = rows[0]
route = first_record[0]
date = first_record[1]
daytype = first_record[2]
rides = first_record[3]
print(f"Route: {route}, Date: {date}, Day type: {daytype}, Rides: {rides}")

Sin embargo, acceder a los datos por índice numérico no siempre es intuitivo. Puede ser difícil recordar a qué campo corresponde cada índice, especialmente cuando se trata de un gran número de campos. En el siguiente paso, exploraremos otras estructuras de datos que pueden hacer que nuestro código sea más legible y mantenible.

Comparación de diferentes estructuras de datos

En Python, las estructuras de datos se utilizan para organizar y almacenar datos relacionados. Son como contenedores que guardan diferentes tipos de información de manera estructurada. En este paso, compararemos diferentes estructuras de datos y veremos cuánta memoria utilizan.

Vamos a crear un nuevo archivo llamado compare_structures.py en el directorio /home/labex/project. Este archivo contendrá el código para leer datos de un archivo CSV y almacenarlos en diferentes estructuras de datos.

## compare_structures.py
import csv
import tracemalloc
from collections import namedtuple

## Define a named tuple for rides data
RideRecord = namedtuple('RideRecord', ['route', 'date', 'daytype', 'rides'])

## A named tuple is a lightweight class that allows you to access its fields by name.
## It's like a tuple, but with named attributes.

## Define a class with __slots__ for memory optimization
class SlottedRideRecord:
    __slots__ = ['route', 'date', 'daytype', 'rides']

    def __init__(self, route, date, daytype, rides):
        self.route = route
        self.date = date
        self.daytype = daytype
        self.rides = rides

## A class with __slots__ is a memory - optimized class.
## It avoids using an instance dictionary, which saves memory.

## Define a regular class for rides data
class RegularRideRecord:
    def __init__(self, route, date, daytype, rides):
        self.route = route
        self.date = date
        self.daytype = daytype
        self.rides = rides

## A regular class is an object - oriented way to represent data.
## It has named attributes and can have methods.

## Function to read data as tuples
def read_as_tuples(filename):
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        next(rows)  ## Skip headers
        for row in rows:
            record = (row[0], row[1], row[2], int(row[3]))
            records.append(record)
    return records

## This function reads data from a CSV file and stores it as tuples.
## Tuples are immutable sequences, and you access their elements by numeric index.

## Function to read data as dictionaries
def read_as_dicts(filename):
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        headers = next(rows)  ## Get headers
        for row in rows:
            record = {
                'route': row[0],
                'date': row[1],
                'daytype': row[2],
                'rides': int(row[3])
            }
            records.append(record)
    return records

## This function reads data from a CSV file and stores it as dictionaries.
## Dictionaries use key - value pairs, so you can access elements by their names.

## Function to read data as named tuples
def read_as_named_tuples(filename):
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        next(rows)  ## Skip headers
        for row in rows:
            record = RideRecord(row[0], row[1], row[2], int(row[3]))
            records.append(record)
    return records

## This function reads data from a CSV file and stores it as named tuples.
## Named tuples combine the efficiency of tuples with the readability of named access.

## Function to read data as regular class instances
def read_as_regular_classes(filename):
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        next(rows)  ## Skip headers
        for row in rows:
            record = RegularRideRecord(row[0], row[1], row[2], int(row[3]))
            records.append(record)
    return records

## This function reads data from a CSV file and stores it as instances of a regular class.
## Regular classes allow you to add methods to your data.

## Function to read data as slotted class instances
def read_as_slotted_classes(filename):
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        next(rows)  ## Skip headers
        for row in rows:
            record = SlottedRideRecord(row[0], row[1], row[2], int(row[3]))
            records.append(record)
    return records

## This function reads data from a CSV file and stores it as instances of a slotted class.
## Slotted classes are memory - optimized and still provide named access.

## Function to measure memory usage
def measure_memory(func, filename):
    tracemalloc.start()

    records = func(filename)

    current, peak = tracemalloc.get_traced_memory()

    ## Demonstrate how to use each data structure
    first_record = records[0]
    if func.__name__ == 'read_as_tuples':
        route, date, daytype, rides = first_record
    elif func.__name__ == 'read_as_dicts':
        route = first_record['route']
        date = first_record['date']
        daytype = first_record['daytype']
        rides = first_record['rides']
    else:  ## named tuples and classes
        route = first_record.route
        date = first_record.date
        daytype = first_record.daytype
        rides = first_record.rides

    print(f"Structure type: {func.__name__}")
    print(f"Record count: {len(records)}")
    print(f"Example access: Route={route}, Date={date}, Rides={rides}")
    print(f"Current memory: {current/1024/1024:.2f} MB")
    print(f"Peak memory: {peak/1024/1024:.2f} MB")
    print("-" * 50)

    tracemalloc.stop()

    return current

if __name__ == "__main__":
    filename = '/home/labex/project/ctabus.csv'

    ## Run all memory tests
    print("Memory usage comparison for different data structures:\n")

    results = []
    for reader_func in [
        read_as_tuples,
        read_as_dicts,
        read_as_named_tuples,
        read_as_regular_classes,
        read_as_slotted_classes
    ]:
        memory = measure_memory(reader_func, filename)
        results.append((reader_func.__name__, memory))

    ## Sort by memory usage (lowest first)
    results.sort(key=lambda x: x[1])

    print("\nRanking by memory efficiency (most efficient first):")
    for i, (name, memory) in enumerate(results, 1):
        print(f"{i}. {name}: {memory/1024/1024:.2f} MB")

Ejecuta el script para ver los resultados de la comparación:

python3 /home/labex/project/compare_structures.py

La salida mostrará el uso de memoria de cada estructura de datos, junto con un ranking de la más a la menos eficiente en términos de memoria.

Comprendiendo las diferentes estructuras de datos

  1. Tuplas:

    • Las tuplas son secuencias ligeras e inmutables. Esto significa que una vez que creas una tupla, no puedes cambiar sus elementos.
    • Accedes a los elementos de una tupla por su índice numérico, como record[0], record[1], etc.
    • Son muy eficientes en términos de memoria porque tienen una estructura simple.
    • Sin embargo, pueden ser menos legibles porque necesitas recordar el índice de cada elemento.
  2. Diccionarios:

    • Los diccionarios utilizan pares clave - valor, lo que te permite acceder a los elementos por sus nombres.
    • Son más legibles, por ejemplo, puedes usar record['route'], record['date'], etc.
    • Tienen un mayor uso de memoria debido al gasto de la tabla hash utilizada para almacenar los pares clave - valor.
    • Son flexibles porque puedes agregar o eliminar campos fácilmente.
  3. Tuplas con nombres (Named Tuples):

    • Las tuplas con nombres combinan la eficiencia de las tuplas con la capacidad de acceder a los elementos por nombre.
    • Puedes acceder a los elementos usando la notación de punto, como record.route, record.date, etc.
    • Son inmutables, al igual que las tuplas normales.
    • Son más eficientes en términos de memoria que los diccionarios.
  4. Clases regulares:

    • Las clases regulares siguen un enfoque orientado a objetos y tienen atributos con nombre.
    • Puedes acceder a los atributos usando la notación de punto, como record.route, record.date, etc.
    • Puedes agregar métodos a una clase regular para definir comportamiento.
    • Utilizan más memoria porque cada instancia tiene un diccionario de instancia para almacenar sus atributos.
  5. Clases con __slots__:

    • Las clases con __slots__ son clases optimizadas en términos de memoria. Evitan usar un diccionario de instancia, lo que ahorra memoria.
    • Todavía proporcionan acceso con nombre a los atributos, como record.route, record.date, etc.
    • Restringen la adición de nuevos atributos después de que se crea el objeto.
    • Son más eficientes en términos de memoria que las clases regulares.

Cuándo usar cada enfoque

  • Tuplas: Utiliza tuplas cuando la memoria sea un factor crítico y solo necesites un acceso indexado simple a tus datos.
  • Diccionarios: Utiliza diccionarios cuando necesites flexibilidad, como cuando los campos de tus datos pueden variar.
  • Tuplas con nombres (Named Tuples): Utiliza tuplas con nombres cuando necesites legibilidad y eficiencia en términos de memoria.
  • Clases regulares: Utiliza clases regulares cuando necesites agregar comportamiento (métodos) a tus datos.
  • Clases con __slots__: Utiliza clases con __slots__ cuando necesites comportamiento y máxima eficiencia en términos de memoria.

Al elegir la estructura de datos adecuada para tus necesidades, puedes mejorar significativamente el rendimiento y el uso de memoria de tus programas de Python, especialmente cuando trabajes con grandes conjuntos de datos.

Resumen

En este laboratorio (lab), has aprendido diferentes formas de representar registros en Python y has analizado su eficiencia en términos de memoria. Primero, has comprendido la estructura básica de un conjunto de datos CSV y has comparado métodos de almacenamiento de texto sin procesar. Luego, has trabajado con datos estructurados utilizando tuplas e implementado cinco estructuras de datos diferentes: tuplas, diccionarios, tuplas con nombres (named tuples), clases regulares y clases con __slots__.

Las principales lecciones aprendidas son que diferentes estructuras de datos ofrecen compensaciones entre eficiencia en memoria, legibilidad y funcionalidad. La sobrecarga de objetos en Python tiene un impacto significativo en el uso de memoria para grandes conjuntos de datos, y la elección de la estructura de datos puede afectar en gran medida el consumo de memoria. Las tuplas con nombres y las clases con __slots__ son buenos compromisos entre eficiencia en memoria y legibilidad del código. Estos conceptos son valiosos para los desarrolladores de Python en el procesamiento de datos, especialmente cuando se manejan grandes conjuntos de datos donde la eficiencia en memoria es crucial.