Diferentes Formas de Representar Registros

Intermediate

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

Introdução

Neste laboratório, você aprenderá a explorar maneiras eficientes em termos de memória para armazenar grandes conjuntos de dados em Python. Você também descobrirá diferentes formas de representar registros, como tuplas, dicionários, classes e tuplas nomeadas (named tuples).

Além disso, você comparará o uso de memória de diferentes estruturas de dados. Compreender as compensações (trade-offs) entre essas estruturas é valioso para usuários de Python que realizam análise de dados, pois ajuda a otimizar o código.

Este é um Lab Guiado, que fornece instruções passo a passo para ajudá-lo a aprender e praticar. Siga as instruções cuidadosamente para completar cada etapa e ganhar experiência prática. Dados históricos mostram que este é um laboratório de nível iniciante com uma taxa de conclusão de 86%. Recebeu uma taxa de avaliações positivas de 98% dos estudantes.

Explorando o Conjunto de Dados

Vamos começar nossa jornada dando uma olhada de perto no conjunto de dados com o qual vamos trabalhar. O arquivo ctabus.csv é um arquivo CSV (Comma-Separated Values - Valores Separados por Vírgula). Arquivos CSV são uma forma comum de armazenar dados tabulares, onde cada linha representa uma linha e os valores dentro de uma linha são separados por vírgulas. Este arquivo em particular contém dados diários de passageiros para o sistema de ônibus da Chicago Transit Authority (CTA), cobrindo o período de 1º de janeiro de 2001 a 31 de agosto de 2013.

Descompacte o arquivo e remova o arquivo zip:

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

Para entender a estrutura deste arquivo, primeiro vamos dar uma olhada nele. Usaremos Python para ler o arquivo e imprimir algumas linhas. Abra um terminal e execute o seguinte 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()

Neste código, primeiro abrimos o arquivo usando a função open e o atribuímos à variável f. A função next é usada para ler a próxima linha do arquivo. Usamos isso três vezes: a primeira vez para ler a linha do cabeçalho, que geralmente contém os nomes das colunas no conjunto de dados. A segunda e terceira vezes, lemos a primeira e a segunda linhas de dados, respectivamente. Finalmente, fechamos o arquivo usando o método close para liberar recursos do sistema.

Você deve ver uma saída semelhante a esta:

route,date,daytype,rides

3,01/01/2001,U,7354

4,01/01/2001,U,9288

Esta saída mostra que o arquivo tem 4 colunas de dados. Vamos detalhar o que cada coluna representa:

  1. route: Este é o nome ou número da rota do ônibus. É a primeira coluna (Coluna 0) no conjunto de dados.
  2. date: É uma string de data no formato MM/DD/YYYY. Esta é a segunda coluna (Coluna 1).
  3. daytype: É um código de tipo de dia, que é a terceira coluna (Coluna 2).
    • U = Domingo/Feriado
    • A = Sábado
    • W = Dia de semana
  4. rides: Esta coluna registra o número total de passageiros como um inteiro. É a quarta coluna (Coluna 3).

A coluna rides nos diz quantas pessoas embarcaram em um ônibus em uma rota específica em um determinado dia. Por exemplo, na saída acima, podemos ver que 7.354 pessoas andaram no ônibus número 3 em 1º de janeiro de 2001.

Agora, vamos descobrir quantas linhas existem no arquivo. Saber o número de linhas nos dará uma ideia do tamanho do nosso conjunto de dados. Execute o seguinte 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}")

Neste código, usamos a instrução with para abrir o arquivo. A vantagem de usar with é que ele cuida automaticamente de fechar o arquivo quando terminamos com ele. Em seguida, usamos uma expressão geradora (1 for line in f) para criar uma sequência de 1s, um para cada linha no arquivo. A função sum soma todos esses 1s, dando-nos o número total de linhas no arquivo. Finalmente, imprimimos o resultado.

Isso deve gerar aproximadamente 577.564 linhas, o que significa que estamos lidando com um conjunto de dados substancial. Este grande conjunto de dados nos fornecerá muitos dados para analisar e obter insights.

Medindo o Uso de Memória com Diferentes Métodos de Armazenamento

Nesta etapa, vamos analisar como diferentes formas de armazenar dados podem impactar o uso de memória. O uso de memória é um aspecto importante da programação, especialmente ao lidar com grandes conjuntos de dados. Para medir a memória usada pelo nosso código Python, usaremos o módulo tracemalloc do Python. Este módulo é muito útil, pois nos permite rastrear as alocações de memória feitas pelo Python. Ao usá-lo, podemos ver quanta memória nossos métodos de armazenamento de dados estão consumindo.

Método 1: Armazenando o Arquivo Inteiro como uma Única String

Vamos começar criando um novo arquivo Python. Navegue até o diretório /home/labex/project e crie um arquivo chamado memory_test1.py. Você pode usar um editor de texto para abrir este arquivo. Depois que o arquivo estiver aberto, adicione o seguinte código a ele. Este código lerá todo o conteúdo de um arquivo como uma única string e medirá o uso de memória.

## 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()

Depois de adicionar o código, salve o arquivo. Agora, para executar este script, abra seu terminal e execute o seguinte comando:

python3 /home/labex/project/memory_test1.py

Ao executar o script, você deve ver uma saída semelhante a esta:

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

Os números exatos podem ser diferentes em seu sistema, mas, em geral, você notará que o uso atual de memória é de cerca de 12 MB e o uso máximo de memória é de cerca de 24 MB.

Método 2: Armazenando como uma Lista de Strings

Em seguida, testaremos outra forma de armazenar os dados. Crie um novo arquivo chamado memory_test2.py no mesmo diretório /home/labex/project. Abra este arquivo no editor e adicione o seguinte código. Este código lê o arquivo e armazena cada linha como uma string separada em uma lista e, em seguida, mede o uso de memória.

## 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()

Salve o arquivo e, em seguida, execute o script usando o seguinte comando no terminal:

python3 /home/labex/project/memory_test2.py

Você deve ver uma saída semelhante a esta:

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

Observe que o uso de memória aumentou significativamente em comparação com o método anterior de armazenar os dados como uma única string. Isso ocorre porque cada linha na lista é um objeto string Python separado, e cada objeto tem sua própria sobrecarga de memória.

Compreendendo a Diferença de Memória

A diferença no uso de memória entre as duas abordagens mostra um conceito importante na programação Python chamado sobrecarga de objeto (object overhead). Quando você armazena dados como uma lista de strings, cada string é um objeto Python separado. Cada objeto tem alguns requisitos de memória adicionais, que incluem:

  1. O cabeçalho do objeto Python (geralmente 16 a 24 bytes por objeto). Este cabeçalho contém informações sobre o objeto, como seu tipo e contagem de referência.
  2. A própria representação real da string, que armazena os caracteres da string.
  3. Preenchimento de alinhamento de memória. Este é um espaço extra adicionado para garantir que o endereço de memória do objeto seja devidamente alinhado para acesso eficiente.

Por outro lado, quando você armazena todo o conteúdo do arquivo como uma única string, há apenas um objeto e, portanto, apenas um conjunto de sobrecarga. Isso o torna mais eficiente em termos de memória ao considerar o tamanho total dos dados.

Ao projetar programas que funcionam com grandes conjuntos de dados, você precisa considerar essa compensação (trade-off) entre eficiência de memória e acessibilidade de dados. Às vezes, pode ser mais conveniente acessar dados quando eles são armazenados em uma lista de strings, mas isso usará mais memória. Em outros momentos, você pode priorizar a eficiência da memória e optar por armazenar os dados como uma única string.

Trabalhando com Dados Estruturados usando Tuplas

Até agora, temos lidado com o armazenamento de dados de texto bruto. Mas quando se trata de análise de dados, geralmente precisamos transformar os dados em formatos mais organizados e estruturados. Isso facilita a execução de várias operações e a obtenção de insights dos dados. Nesta etapa, aprenderemos como ler dados como uma lista de tuplas usando o módulo csv. Tuplas são uma estrutura de dados simples e útil em Python que pode conter vários valores.

Criando uma Função de Leitura com Tuplas

Vamos criar um novo arquivo chamado readrides.py no diretório /home/labex/project. Este arquivo conterá o código para ler os dados de um arquivo CSV e armazená-los como uma 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 uma função chamada read_rides_as_tuples. Veja o que ela faz passo a passo:

  1. Ele abre o arquivo CSV especificado pelo parâmetro filename. Isso nos permite acessar os dados dentro do arquivo.
  2. Ele usa o módulo csv para analisar cada linha do arquivo. A função csv.reader nos ajuda a dividir as linhas em valores individuais.
  3. Ele extrai os quatro campos (rota, data, tipo de dia e número de viagens) de cada linha. Esses campos são importantes para nossa análise de dados.
  4. Ele converte o campo 'rides' em um inteiro. Isso é necessário porque os dados no arquivo CSV estão inicialmente em formato de string, e precisamos de um valor numérico para cálculos.
  5. Ele cria uma tupla com esses quatro valores. Tuplas são imutáveis, o que significa que seus valores não podem ser alterados depois de criados.
  6. Ele adiciona a tupla a uma lista chamada records. Esta lista conterá todos os registros do arquivo CSV.

Agora, vamos executar o script. Abra seu terminal e digite o seguinte comando:

python3 /home/labex/project/readrides.py

Você deve ver uma saída semelhante 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

Observe que o uso de memória aumentou em comparação com nossos exemplos anteriores. Existem algumas razões para isso:

  1. Agora estamos armazenando os dados em um formato estruturado (tuplas). Dados estruturados geralmente exigem mais memória porque têm uma organização definida.
  2. Cada valor na tupla é um objeto Python separado. Objetos Python têm alguma sobrecarga, o que contribui para o aumento do uso de memória.
  3. Temos uma estrutura de lista adicional que contém todas essas tuplas. Listas também ocupam memória para armazenar seus elementos.

A vantagem de usar essa abordagem é que nossos dados agora estão devidamente estruturados e prontos para análise. Podemos acessar facilmente campos específicos de cada registro por índice. Por exemplo:

## 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}")

No entanto, acessar dados por índice numérico nem sempre é intuitivo. Pode ser difícil lembrar qual índice corresponde a qual campo, especialmente ao lidar com um grande número de campos. Na próxima etapa, exploraremos outras estruturas de dados que podem tornar nosso código mais legível e sustentável.

Comparando Diferentes Estruturas de Dados

Em Python, as estruturas de dados são usadas para organizar e armazenar dados relacionados. Elas são como contêineres que contêm diferentes tipos de informações de forma estruturada. Nesta etapa, compararemos diferentes estruturas de dados e veremos quanta memória elas usam.

Vamos criar um novo arquivo chamado compare_structures.py no diretório /home/labex/project. Este arquivo conterá o código para ler dados de um arquivo CSV e armazená-los em diferentes estruturas de dados.

## 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")

Execute o script para ver os resultados da comparação:

python3 /home/labex/project/compare_structures.py

A saída mostrará o uso de memória para cada estrutura de dados, juntamente com uma classificação da mais para a menos eficiente em termos de memória.

Compreendendo as Diferentes Estruturas de Dados

  1. Tuplas:

    • Tuplas são sequências leves e imutáveis. Isso significa que, uma vez que você cria uma tupla, não pode alterar seus elementos.
    • Você acessa elementos em uma tupla por seu índice numérico, como record[0], record[1], etc.
    • Elas são muito eficientes em termos de memória porque têm uma estrutura simples.
    • No entanto, elas podem ser menos legíveis porque você precisa lembrar o índice de cada elemento.
  2. Dicionários:

    • Dicionários usam pares chave-valor, o que permite que você acesse elementos por seus nomes.
    • Eles são mais legíveis, por exemplo, você pode usar record['route'], record['date'], etc.
    • Eles têm maior uso de memória devido à sobrecarga da tabela hash usada para armazenar os pares chave-valor.
    • Eles são flexíveis porque você pode adicionar ou remover campos facilmente.
  3. Named Tuples:

    • Named tuples combinam a eficiência das tuplas com a capacidade de acessar elementos por nome.
    • Você pode acessar elementos usando a notação de ponto, como record.route, record.date, etc.
    • Elas são imutáveis, assim como as tuplas regulares.
    • Elas são mais eficientes em termos de memória do que os dicionários.
  4. Classes Regulares:

    • Classes regulares seguem uma abordagem orientada a objetos e têm atributos nomeados.
    • Você pode acessar atributos usando a notação de ponto, como record.route, record.date, etc.
    • Você pode adicionar métodos a uma classe regular para definir o comportamento.
    • Elas usam mais memória porque cada instância tem um dicionário de instância para armazenar seus atributos.
  5. Classes com __slots__:

    • Classes com __slots__ são classes otimizadas para memória. Elas evitam o uso de um dicionário de instância, o que economiza memória.
    • Elas ainda fornecem acesso nomeado aos atributos, como record.route, record.date, etc.
    • Elas restringem a adição de novos atributos após a criação do objeto.
    • Elas são mais eficientes em termos de memória do que as classes regulares.

Quando Usar Cada Abordagem

  • Tuplas: Use tuplas quando a memória for um fator crítico e você só precisar de acesso indexado simples aos seus dados.
  • Dicionários: Use dicionários quando você precisar de flexibilidade, como quando os campos em seus dados podem variar.
  • Named Tuples: Use named tuples quando você precisar de legibilidade e eficiência de memória.
  • Classes Regulares: Use classes regulares quando você precisar adicionar comportamento (métodos) aos seus dados.
  • Classes com __slots__: Use classes com __slots__ quando você precisar de comportamento e máxima eficiência de memória.

Ao escolher a estrutura de dados certa para suas necessidades, você pode melhorar significativamente o desempenho e o uso de memória de seus programas Python, especialmente ao trabalhar com grandes conjuntos de dados.

Resumo

Neste laboratório, você aprendeu diferentes maneiras de representar registros em Python e analisou sua eficiência de memória. Primeiro, você entendeu a estrutura básica do conjunto de dados CSV e comparou os métodos de armazenamento de texto bruto. Em seguida, você trabalhou com dados estruturados usando tuplas e implementou cinco estruturas de dados diferentes: tuplas, dicionários, named tuples, classes regulares e classes com slots.

As principais conclusões incluem que diferentes estruturas de dados oferecem trade-offs (compensações) entre eficiência de memória, legibilidade e funcionalidade. A sobrecarga de objetos do Python tem um impacto significativo no uso de memória para grandes conjuntos de dados, e a escolha da estrutura de dados pode afetar muito o consumo de memória. Named tuples e classes com slots são bons compromissos entre eficiência de memória e legibilidade do código. Esses conceitos são valiosos para desenvolvedores Python em processamento de dados, especialmente ao lidar com grandes conjuntos de dados onde a eficiência de memória é crucial.