Compreendendo a velocidade e a eficiência do Polars

Saiba como o Polars alcança sua notável velocidade e eficiência de memória em comparação com o pandas, aproveitando mecanismos como execução otimizada de consultas, integração com Apache Arrow e processamento paralelo.

🕒 Tempo estimado de leitura: 9 minutos

Hoje abordaremos alguns tópicos avançados de uma crescente biblioteca em Python chamada Polars, que é usada principalmente para DataFrames e é altamente eficiente. À medida que exploramos opções inovadoras no Polars, você descobrirá como processar e realizar análises de volumes de dados consideráveis sem problemas.

Para se aprofundar no primeiro artigo, Introdução ao Python Polars: Uma rápida biblioteca de DataFrame, clique aqui.

Todos os exemplos também são explicados aqui👨‍🔬, um notebook correspondente do Google Colab para tornar seu aprendizado ainda mais interativo.

A crescente popularidade do Polars na ciência de dados Python

Apesar dessas vantagens, o pandas continua sendo a escolha preferida devido à sua profunda integração com o ecossistema mais amplo de ciência de dados Python. Sua interoperabilidade com vários pacotes no pipeline de aprendizado de máquina é incomparável. No entanto, o Polars está se atualizando rapidamente, com compatibilidade crescente com diversas bibliotecas de plotagem, como plotly, matplotlib, seaborn, altair e hvplot, tornando-o uma opção viável para análise exploratória de dados.

Polars agora também pode ser integrado em pipelines de aprendizado de máquina e deep learning. Por exemplo, a versão 1.4.0 do scikit-learn permite que os transformers gerem DataFrames Polars. Além disso, Polars DataFrames podem ser convertidos em tipos de dados PyTorch, permitindo uma integração mais fácil nos fluxos de trabalho PyTorch. Essa transformação pode ser feita usando o método to_torch em um Polars DataFrame.

Por que o Polars é tão rápido?

Em essência, o Polars foi projetado para velocidade. Ele foi construído desde o início para ser incrivelmente rápido e pode executar operações comuns até 5 a 10 vezes mais rápido que o pandas. Além disso, o Polars usa significativamente menos memória para suas operações em comparação com o pandas. Enquanto o Pandas requer cerca de 5 a 10 vezes mais RAM do que o tamanho do conjunto de dados para operações, o Polars precisa apenas de 2 a 4 vezes mais.

Para ter uma ideia de como o Polars se compara a outras bibliotecas de dataframe, você pode conferir essa comparação. Como mostram os resultados, o Polars é 10 a 100 vezes mais rápido que o pandas para operações comuns e é uma das bibliotecas DataFrame mais rápidas disponíveis. Além disso, ele pode lidar com conjuntos de dados maiores do que o pandas antes de encontrar erros de falta de memória.

Esses resultados são realmente notáveis, e você pode estar curioso para saber como o Polars atinge um desempenho tão alto enquanto ainda opera em uma única máquina. A biblioteca foi projetada pensando no desempenho desde o início, e isso é conseguido por meio de vários métodos.

O papel do Apache Arrow na melhoria do desempenho

O impressionante desempenho do Polars pode ser atribuído em parte ao uso do Apache Arrow, um formato de memória independente de linguagem co-criado por Wes McKinney para lidar com as limitações dos pandas em meio ao crescente tamanho dos dados. Este formato também sustenta o pandas 2.0, lançado em março de 2023 para melhorar seu desempenho. No entanto, Polars segue um caminho diferente ao implementar sua própria versão do Arrow em vez de depender do PyArrow como o pandas 2.0.

Um grande benefício de usar Arrow como uma biblioteca de dados é a interoperabilidade que oferece. Arrow padroniza formatos de dados na memória em várias bibliotecas e bancos de dados, o que elimina a necessidade de conversão de dados ao passá-los entre diferentes etapas em um pipeline de dados. Isso pode ser particularmente vantajoso ao trabalhar com pipelines de ciência de dados que utilizam diversas ferramentas.

Aqui está um exemplo simples para ilustrar isso:

import pyarrow as pa
import polars as pl

# Cria uma Arrow Table com duas colunas 'a' e 'b'
arrow_table = pa.table({
    'a': range(1000),
    'b': range(1000, 2000)
})

# Converta a tabela Arrow em um DataFrame Polars
polars_df = pl.from_arrow(arrow_table)

# Exibe o DataFrame Polars
print(polars_df)

Executar o código acima produzirá a seguinte saída.

shape: (1_000, 2)
┌─────┬──────┐
│ a   ┆ b    │
│ --- ┆ ---  │
│ i64 ┆ i64  │
╞═════╪══════╡
│ 0   ┆ 1000 │
│ 1   ┆ 1001 │
│ 2   ┆ 1002 │
│ 3   ┆ 1003 │
│ …   ┆ …    │
│ 996 ┆ 1996 │
│ 997 ┆ 1997 │
│ 998 ┆ 1998 │
│ 999 ┆ 1999 │
└─────┴──────┘

Isso não apenas aumenta o desempenho, evitando o caro processo de serialização e desserialização, mas também melhora a eficiência da memória. Na verdade, estima-se que a serialização e a desserialização representem cerca de 80–90% dos custos de computação em fluxos de trabalho de dados típicos, tornando a função da Arrow em Polars um impulsionador de desempenho significativo.

Arrow também suporta uma gama mais ampla de tipos de dados em comparação com pandas. Embora o pandas, construído em NumPy, seja excelente no tratamento de colunas inteiras e flutuantes, ele tem dificuldades com outros tipos. Arrow, por outro lado, gerencia com eficiência tipos de colunas de data e hora, booleanos, binários e até mesmo tipos de colunas complexas que contêm listas. Ele também pode lidar nativamente com dados ausentes, o que requer soluções alternativas no NumPy. Além disso, o uso de armazenamento de dados colunar pela biblioteca Arrow, onde todas as colunas são armazenadas em um bloco de memória contínuo, facilita o paralelismo e acelera a recuperação de dados.

Otimização de consulta

Um dos pontos fortes do Polars está na forma como ele processa o código. Ao contrário do pandas, que normalmente segue uma execução ansiosa (executando operações na ordem em que são escritas), o Polars pode executar tanto execução ansiosa quanto execução lenta. A execução lenta envolve um otimizador de consulta que avalia todas as operações necessárias e determina a ordem de execução mais eficiente. Por exemplo, considere esta expressão para calcular a média de uma coluna Quantidades para frutas específicas, Maçã e Banana:

import polars as pl

#Cria um DataFrame de exemplo
df = pl.DataFrame({
    "Frutas": ["Maçã", "Banana", "Maçã", "Cereja", "Banana"],
    "Quantidades": [10, 20, 15, 30, 25]
})

# Agrupar por "Frutas", calcular média de "Quantidades" e filtrar pelas frutas "Maçã" e "Banana"
resultado = (
    df.group_by("Frutas").agg(pl.col("Quantidades").mean())
    .filter(pl.col("Frutas").is_in(["Maçã", "Banana"]))
)

print(resultado)

Executar o código acima produzirá a seguinte saída.

shape: (2, 2)
┌────────┬─────────────┐
│ Frutas ┆ Quantidades │
│ ---    ┆ ---         │
│ str    ┆ f64         │
╞════════╪═════════════╡
│ Banana ┆ 22.5        │
│ Maçã   ┆ 12.5        │
└────────┴─────────────┘

Durante a execução ansiosa, a operação groupby é aplicada primeiro a todo o DataFrame, seguida por uma etapa de filtragem. No caso de execução lenta, entretanto, a filtragem acontece primeiro. Isso torna a operação groupby mais eficiente, pois processa apenas os dados relevantes.

API expressiva

Polars também possui uma API expressiva, permitindo que quase qualquer operação seja realizada usando métodos Polars. Por outro lado, o pandas geralmente requer o uso do método apply para operações complexas, que itera sequencialmente pelas linhas do DataFrame. Ao aproveitar métodos integrados, o Polars pode operar em nível colunar, permitindo o processamento paralelo por meio de Instrução Única, Dados Múltiplos (SIMD).

Polars fornece aos usuários uma API poderosa para manipulações de dados complexos, ao mesmo tempo que otimiza consultas em segundo plano para aumentar o desempenho.

Vamos dar uma olhada em um exemplo usando este recurso:

import polars as pl

#Começamos com um DataFrame simples
df = pl.DataFrame({
    "idade": [25, 32, 45, 22, 18],
    "renda": [50000, 150000, 70000, 30000, 20000]
})

# Podemos usar lógica condicional para criar uma nova coluna, 'income_level'
# Se a renda for maior que 100000, nós a rotularemos como 'Alta', caso contrário, 'Baixa'
df = df.with_columns([
    pl.when(pl.col("renda") > 100000)
      .then(pl.lit("Alta"))
      .otherwise(pl.lit("Baixa"))
      .alias("nivel_renda")
])

print(df)

Executar o código acima produzirá a seguinte saída.

shape: (5, 3)
┌───────┬────────┬─────────────┐
│ idade ┆ renda  ┆ nivel_renda │
│ ---   ┆ ---    ┆ ---         │
│ i64   ┆ i64    ┆ str         │
╞═══════╪════════╪═════════════╡
│ 25    ┆ 50000  ┆ Baixa       │
│ 32    ┆ 150000 ┆ Alta        │
│ 45    ┆ 70000  ┆ Baixa       │
│ 22    ┆ 30000  ┆ Baixa       │
│ 18    ┆ 20000  ┆ Baixa       │
└───────┴────────┴─────────────┘

Outro recurso que o Polars oferece é a capacidade de normalizar dados e criar novas colunas com base neles. Aqui está um exemplo:

# Vamos criar um DataFrame
df = pl.DataFrame({
    "nome": ["Alice", "Bruno", "Carlos", "David", "Evea", "Francisco"],
    "pontuacoes": [85, 95, 70, 65, 92, 88]
})

# Calculo da pontuação média
average_score = df["pontuacoes"].mean()

# Normalização das pontuações e criação de uma nova coluna, 'pontuacao_normalizada'
df = df.with_columns([
    ((pl.col("pontuacoes") / average_score) * 100).alias("pontuacao_normalizada")
])

# Uso da coluna 'pontuacao_normalizada' para criar uma coluna de 'nota'
resultado = df.with_columns([
    pl.when(pl.col("pontuacao_normalizada") > 90).then(pl.lit("A"))
    .when(pl.col("pontuacao_normalizada") > 70).then(pl.lit("B"))
    .when(pl.col("pontuacao_normalizada") > 50).then(pl.lit("C"))
    .otherwise(pl.lit("D")).alias("nota")
])

print(resultado)

Executar o código acima produzirá a seguinte saída.

shape: (6, 4)
┌───────────┬────────────┬───────────────────────┬──────┐
│ nome      ┆ pontuacoes ┆ pontuacao_normalizada ┆ nota │
│ ---       ┆ ---        ┆ ---                   ┆ ---  │
│ str       ┆ i64        ┆ f64                   ┆ str  │
╞═══════════╪════════════╪═══════════════════════╪══════╡
│ Alice     ┆ 85         ┆ 103.030303            ┆ A    │
│ Bruno     ┆ 95         ┆ 115.151515            ┆ A    │
│ Carlos    ┆ 70         ┆ 84.848485             ┆ B    │
│ David     ┆ 65         ┆ 78.787879             ┆ B    │
│ Evea      ┆ 92         ┆ 111.515152            ┆ A    │
│ Francisco ┆ 88         ┆ 106.666667            ┆ A    │
└───────────┴────────────┴───────────────────────┴──────┘

Polars também oferece suporte a uma variedade de operações de junção, garantindo que suas operações de mesclagem de dados sejam flexíveis e eficientes. Aqui está um exemplo:

# Importação da biblioteca polars
import polars as pl

#Criação do primeiro DataFrame
df1 = pl.DataFrame({
    "chave": [1, 2, 3],
    "valor": ["um", "dois", "tres"]
})

#Criação do segundo DataFrame
df2 = pl.DataFrame({
    "chave": [2, 3, 4],
    "valor": ["dois", "três", "quatro"]
})

# Realização de uma junção interna na coluna 'chave'
# O parâmetro 'how' é definido como 'inner' por padrão, portanto pode ser omitido
# O parâmetro 'suffix' é usado para adicionar um sufixo aos nomes de colunas sobrepostas do segundo DataFrame
resultado = df1.join(df2, on="chave", suffix="_df2")

# Imprima o resultado
print(resultado)

Executar o código acima produzirá a seguinte saída.

shape: (2, 3)
┌───────┬───────┬───────────┐
│ chave ┆ valor ┆ valor_df2 │
│ ---   ┆ ---   ┆ ---       │
│ i64   ┆ str   ┆ str       │
╞═══════╪═══════╪═══════════╡
│ 2     ┆ dois  ┆ dois      │
│ 3     ┆ tres  ┆ três      │
└───────┴───────┴───────────┘

Conclusão

Polars é uma biblioteca poderosa para manipulação de dados que mantém o desempenho em sua essência. Seus recursos avançados, como avaliação lenta, processamento paralelo, integração Arrow, APIs expressivas e operações específicas, fornecem um kit de ferramentas robusto para lidar com conjuntos de dados grandes e complexos. Ao aproveitar esses avanços, você pode aumentar consideravelmente seus recursos de processamento de dados em Python.

Portanto, na próxima vez que suas tarefas de processamento de dados começarem a diminuir o tempo e a eficiência, experimente os Polares para experimentar a melhoria por si mesmo. Continue experimentando e boa organização!