HomeSharpStack
pandas5 min

Otimização Avançada de Memória e Tipos de Dados em Pandas

Otimização Avançada de Memória e Tipos de Dados em Pandas

Ao trabalhar com datasets de grande escala, a gestão eficiente de memória torna-se crítica. Pandas oferece mecanismos sofisticados para reduzir o footprint de memória sem comprometer a integridade dos dados. Este guia explora as estratégias internas e trade-offs envolvidos.

Profiling de Memória: Além do .info()

O método .info() fornece uma visão superficial. Para análise profunda, precisamos entender como Pandas aloca memória internamente:

import pandas as pd
import numpy as np
from memory_profiler import profile

df = pd.read_csv('large_dataset.csv')

# Análise detalhada de memória por coluna
memory_usage = df.memory_usage(deep=True)
print(memory_usage)
print(f'Total: {memory_usage.sum() / 1024**2:.2f} MB')

# Identificar colunas problemáticas
top_consumers = memory_usage.nlargest(5)
print(top_consumers)

O parâmetro deep=True força a inspeção de objetos string, revelando o consumo real. Sem ele, Pandas reporta apenas o tamanho do array de ponteiros, não dos dados subjacentes.

Seleção Estratégica de Dtypes

A escolha de dtype é uma decisão arquitetural com implicações profundas. Considere os trade-offs:

import pandas as pd

# Problema: int64 padrão desperdiça memória
df = pd.DataFrame({
    'user_id': np.arange(1000000),
    'age': np.random.randint(18, 100, 1000000),
    'score': np.random.randint(0, 1000, 1000000)
})

print(f'Antes: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB')

# Otimização: downcast inteligente
df['user_id'] = df['user_id'].astype('uint32')  # 0 a 4 bilhões
df['age'] = df['age'].astype('uint8')           # 0 a 255
df['score'] = df['score'].astype('uint16')      # 0 a 65535

print(f'Depois: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB')

# Redução típica: 60-70% para dados numéricos

Trade-off crítico: Downcast reduz memória mas aumenta risco de overflow. Um uint8 que recebe valor 256 silenciosamente wraps para 0. Valide ranges antes de converter.

Categoricals: O Arma Secreta para Strings

Colunas categóricas com alta repetição (ex: países, status) podem economizar 90%+ de memória:

df = pd.DataFrame({
    'country': np.random.choice(['Brasil', 'EUA', 'China', 'Japão'], 1000000),
    'status': np.random.choice(['ativo', 'inativo', 'pendente'], 1000000)
})

print(f'String padrão: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB')

# Conversão para categorical
df['country'] = df['country'].astype('category')
df['status'] = df['status'].astype('category')

print(f'Com categorical: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB')

# Internamente: Pandas armazena inteiros (0, 1, 2...) + mapa de categorias
print(df['country'].cat.categories)
print(df['country'].cat.codes.dtype)  # int8 ou int16

Caveat importante: Operações em categoricals são mais lentas (ex: .str accessor não funciona diretamente). Use apenas quando memória é constraint crítico.

Estratégia Híbrida: Sparse Arrays

Para dados com muitos zeros ou valores repetidos, sparse arrays reduzem memória dramaticamente:

# Dataset com 95% de zeros (comum em recomendação)
df = pd.DataFrame({
    'feature_1': pd.arrays.SparseArray(np.random.choice([0, 1], 1000000, p=[0.95, 0.05])),
    'feature_2': pd.arrays.SparseArray(np.random.choice([0, 1], 1000000, p=[0.95, 0.05]))
})

print(f'Sparse: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB')
# Comparação: dense teria ~8 MB, sparse ~0.4 MB

Profiling em Produção: Monitoramento Contínuo

Para pipelines críticos, implemente monitoramento:

def optimize_dtypes(df, target_memory_mb=None):
    """Downcast automático com validação de ranges"""
    initial_memory = df.memory_usage(deep=True).sum() / 1024**2
    
    for col in df.select_dtypes(include=['int64']).columns:
        col_min, col_max = df[col].min(), df[col].max()
        
        if col_max < 128 and col_min >= -128:
            df[col] = df[col].astype('int8')
        elif col_max < 32768 and col_min >= -32768:
            df[col] = df[col].astype('int16')
    
    final_memory = df.memory_usage(deep=True).sum() / 1024**2
    reduction = ((initial_memory - final_memory) / initial_memory) * 100
    print(f'Redução: {reduction:.1f}%')
    
    return df

Edge Cases e Armadilhas

1. NaN em inteiros: Pandas converte int para float ao encontrar NaN. Use Int64 (nullable) em vez de int64.

2. Categorical ordering: Categoricals ordenados consomem mais memória mas permitem comparações lógicas.

3. String interning: Pandas não interna strings automaticamente. Colunas com muitas strings únicas desperdiçam memória mesmo com categorical.

Key Takeaways

  • Use memory_usage(deep=True) para profiling preciso; downcast inteligente de tipos numéricos reduz footprint em 60-70%, mas exige validação rigorosa de ranges para evitar overflow silencioso
  • Categorical dtype economiza 90%+ de memória para colunas com alta repetição, mas sacrifica velocidade em operações string e requer conversão reversa para manipulação textual
  • Sparse arrays são ideais para dados esparsos (>90% zeros), mas monitoramento contínuo em produção é essencial pois otimizações podem criar gargalos inesperados em operações downstream

Enjoyed this reading?

SharpStack delivers personalized tech readings every day, calibrated to your skill level. 5 minutes a day to stay sharp.

“Stay sharp. At your pace. Everyday.”