Introdução ao Python Polars: Uma rápida biblioteca de DataFrame

Polars lida com eficiência com milhões de linhas, tornando os códigos Python mais simples e limpos. Em termos de velocidade, o Polars não é apenas rápido; é incrivelmente rápido.

Este artigo é destinado àqueles que já estão familiarizados com o uso de pandas 🐼 e estão curiosos para saber se o Polars 🐻‍❄️ pode ser um bom complemento para seu fluxo de trabalho. Se você ainda não está familiarizado com pandas, é altamente recomendável começar com o artigo Trabalhando com dados em Python: do básico às técnicas avançadas para obter uma compreensão básica.

Hoje, existem muitas bibliotecas em Python para lidar com os dados, e pandas 🐼 é a mais comumente usada.

Ao longo dos anos, pandas se estabeleceu como a ferramenta ideal para análise de dados em Python. O projeto, iniciado por Wes McKinney em 2008, atingiu seu marco principal com o lançamento 1.0 em janeiro de 2020. Desde então, ele permaneceu um elemento básico na comunidade de análise de dados e não mostra sinais de enfraquecimento.

Apesar de sua popularidade, pandas tem suas falhas. Wes McKinney (criador do pandas) destacou vários desses desafios, e um número significativo de críticas online geralmente se concentra em duas questões principais:

  1. limitações de desempenho e

  2. uma API às vezes estranha ou complexa.

Em um esforço para resolver essas deficiências, Richie Vink desenvolveu Polares 🐻‍❄️. Em uma postagem no blog detalhada de 2021, Vink apresentou métricas que fundamentam suas afirmações sobre o melhor desempenho do Polars e seu design mais eficiente.

Neste artigo, falaremos sobre o que é o Polars, algumas de suas funcionalidades e um caso de uso prático onde o Polars tem um desempenho excelente.

Por que Polares 🐻‍❄️?

À medida que o tamanho dos dados aumenta e a velocidade se torna um fator importante, novas bibliotecas como Polars parecem substituir as anteriores pela nova velocidade aprimorada. Polars é uma biblioteca DataFrame excepcionalmente rápida projetada para lidar com dados estruturados. Seu núcleo é desenvolvido em Rust e pode ser acessado por usuários de Python, R e NodeJS.

O Polars oferece vários benefícios que o tornam uma opção atraente para manipulação e análise de dados:

  • Velocidade: Construído desde o início em Rust, o Polars é livre de dependências externas, garantindo alto desempenho.

  • E/S versátil: oferece suporte a vários sistemas de armazenamento de dados, incluindo armazenamento local, serviços em nuvem e bancos de dados.

  • API amigável: permite que você escreva consultas naturalmente, com o otimizador de consultas interno do Polars para descobrir o método de execução mais eficiente.

  • Uso eficiente de memória: a API de streaming processa os resultados sem carregar todos os dados na memória simultaneamente.

  • Processamento paralelo: aproveita todos os núcleos de CPU disponíveis para distribuição de carga de trabalho sem precisar de configuração extra.

  • Mecanismo de consulta otimizado: usa Apache Arrow para processamento de dados colunares e SIMD para eficiência máxima da CPU.

Apache Arrow estabelece um formato de memória colunar que é independente de plataforma, atendendo a estruturas de dados planas e hierárquicas. Este formato é otimizado para processamento analítico eficiente em hardware contemporâneo, incluindo CPUs e GPUs. Além disso, o formato de memória Arrow permite leituras de cópia zero, possibilitando acesso extremamente rápido aos dados sem a sobrecarga de serialização.

Single Instruction Multiple Data (SIMD) é um método avançado de microarquitetura usado em processadores. Esta técnica permite que uma instrução execute simultaneamente uma operação em vários pontos de dados. Por exemplo, ele pode multiplicar vários números em apenas um ciclo de clock do processador.

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

Uso Básico

Para começar a usar o Polars, você precisará instalá-lo. Isso pode ser feito facilmente com pip:

# Executar a linha a seguir instalará a biblioteca 'polars'
!pip install polars

Depois de instalado, você pode começar a usar Polars como qualquer outra biblioteca DataFrame.

# Importe a biblioteca polars como pl para lidar com frames de dados de forma eficiente
import polars as pl

Aqui está um exemplo simples para demonstrar a funcionalidade básica do Polars

# Crie um DataFrame usando polars (semelhante ao pandas, mas otimizado para desempenho)
# O DataFrame contém três colunas: 'nome', 'idade' e 'salário'
df = pl.DataFrame({
    "nome": ["Alice", "João", "Carlos"],
    "idade": [25, 30, 35],
    "salario": [10000, 7500, 5000]
})

# Imprime o DataFrame inicial no console para visualização
print("DataFrame inicial:")
print(df)

# Filtra o DataFrame para incluir apenas linhas onde a coluna 'idade' é maior que 28
# Isso é conseguido usando o método filter e a função col from polars para selecionar a coluna 'age'
filtrado_df = df.filter(pl.col("idade") > 28)

# Imprima o DataFrame filtrado para mostrar apenas as entradas com idade> 28
print("\nDataFrame filtrado (idade > 28):")
print(filtrado_df)

# Cálculo da soma do 'salário' para cada idade única
# O método agg é usado para agregação e o alias é usado para renomear a coluna resultante para 'salario_total'
grouped_df = df.groupby("idade").agg([pl.sum("salario").alias("salario_total")])

# Imprimaeo DataFrame agrupado para exibir o salário total de cada faixa etária
print("\nDataFrame agrupado (salário total por idade):")
print(grouped_df)

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

DataFrame inicial:
shape: (3, 3)
┌────────┬───────┬─────────┐
│ nome   ┆ idade ┆ salario │
│ ---    ┆ ---   ┆ ---     │
│ str    ┆ i64   ┆ i64     │
╞════════╪═══════╪═════════╡
│ Alice  ┆ 25    ┆ 10000   │
│ João   ┆ 30    ┆ 7500    │
│ Carlos ┆ 35    ┆ 5000    │
└────────┴───────┴─────────┘

DataFrame filtrado (idade > 28):
shape: (2, 3)
┌────────┬───────┬─────────┐
│ nome   ┆ idade ┆ salario │
│ ---    ┆ ---   ┆ ---     │
│ str    ┆ i64   ┆ i64     │
╞════════╪═══════╪═════════╡
│ João   ┆ 30    ┆ 7500    │
│ Carlos ┆ 35    ┆ 5000    │
└────────┴───────┴─────────┘

DataFrame agrupado (salário total por idade):
shape: (3, 2)
┌───────┬───────────────┐
│ idade ┆ salario_total │
│ ---   ┆ ---           │
│ i64   ┆ i64           │
╞═══════╪═══════════════╡
│ 35    ┆ 5000          │
│ 25    ┆ 10000         │
│ 30    ┆ 7500          │
└───────┴───────────────┘
<ipython-input-5-b03a1849c764>:23: DeprecationWarning: `groupby` is deprecated. It has been renamed to `group_by`.
  grouped_df = df.groupby("idade").agg([pl.sum("salario").alias("salario_total")])

Aprofundando-se nas funções

Vamos explorar algumas das funcionalidades avançadas do Polars através de exemplos:

1. Lazy Evaluation (Avaliação preguiçosa)

Lazy Evaluation permite declarar uma série de transformações e executá-las todas de uma vez. Isso pode melhorar significativamente o desempenho de fluxos de trabalho complexos.

# Converte o DataFrame em LazyFrame. LazyFrames permite que você construa
# uma consulta (série de transformações) sem executá-las imediatamente.
# Isso pode otimizar o desempenho combinando operações e reduzindo
# múltiplas verificações de seus dados.
lf = df.lazy()

# Declare transformações no LazyFrame.
# Transformação 1: Filtragem das linhas onde a coluna 'idade' é maior que 28.
# Transformação 2: Agrupamento dos dados filtrados pela coluna 'idade'.
# Transformação 3: Agregação do grupo somando a coluna ‘salário’ e renomeando o resultado para ‘salario_total’.
resultado_preguicoso = lf.filter(pl.col("idade") > 28).groupby("idade").agg([ pl.sum("salario").alias("salario_total")])

# Executa transformações.
# O método collect() aciona a execução da consulta construída até o momento no LazyFrame.
# Isso lê os dados, aplica o filtro, agrupamento e agregação e retorna um DataFrame convencional.
resultado = resultado_preguicoso.collect()

# Imprime o resultado.
print("Resultado da execução lenta:")
print(resultado)

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

Resultado da execução lenta:
shape: (2, 2)
┌───────┬───────────────┐
│ idade ┆ salario_total │
│ ---   ┆ ---           │
│ i64   ┆ i64           │
╞═══════╪═══════════════╡
│ 30    ┆ 7500          │
│ 35    ┆ 5000          │
└───────┴───────────────┘
<ipython-input-6-28feaec3ede3>:11: DeprecationWarning: `groupby` is deprecated. It has been renamed to `group_by`.
  resultado_preguicoso = lf.filter(pl.col("idade") > 28).groupby("idade").agg([ pl.sum("salario").alias("salario_total")])

2. Execução Paralela

O Polars pode paralelizar automaticamente as operações para aproveitar ao máximo os processadores multicore.

# Criação de um DataFrame grande ('df_large') com uma única coluna chamada 'num'.
# A coluna 'num' é preenchida com números inteiros que variam de 1 a 1.000.000.
# O 'list(range(1, 1000001))' gera uma lista começando de 1 até 1.000.000 inclusive.
df_large = pl.DataFrame({"num": list(range(1, 1000001))})

# Aplique uma transformação ao DataFrame usando o método select do Polars.
# O pl.col("num") faz referência à coluna 'num' do DataFrame.
# O operador '*' duplica cada valor na coluna 'num', criando efetivamente uma nova coluna com esses valores transformados.
# Polars usa execução lenta e multithreading nos bastidores, o que pode ajudar a acelerar as operações em grandes conjuntos de dados.
resultado_paralelo = df_large.select( pl.col("num") * 2)

# Imprime o DataFrame transformado 'parallel_result', que contém os valores duplicados da coluna 'num'.
print("Execução Paralela:")
print(resultado_paralelo)

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

Execução Paralela:
shape: (1_000_000, 1)
┌─────────┐
│ num     │
│ ---     │
│ i64     │
╞═════════╡
│ 2       │
│ 4       │
│ 6       │
│ 8       │
│ …       │
│ 1999994 │
│ 1999996 │
│ 1999998 │
│ 2000000 │
└─────────┘

Caso de uso do mundo real: análise de dados financeiros

Vamos simular um caso de uso real onde analisamos um grande conjunto de dados de preços de ações para encontrar tendências e calcular médias móveis.

  • Carregamento de dados: Carga do arquivo CSV com preços históricos de ações.

  • Limpeza de dados: Remoção de todas as linhas com dados ausentes.

  • Cálculos: Calculo das médias móveis de 7 dias e 30 dias.

  • Análise: Identificação das datas em que a média móvel de 7 dias ultrapassa a média móvel de 30 dias.

# Etapa 1: Carregamento de dados

# Carga dos dados do preço das ações usando um DataFrame Polars
df = pl.read_csv("https://infinitepy.s3.amazonaws.com/samples/stock_price.csv")

# Impressão dos dados iniciais para ver rapidamente as primeiras linhas
print("Dados iniciais:")
print(df.head()) # o método head() mostra as primeiras 5 linhas por padrão


# Etapa 2: Limpeza de dados

# Remoção das linhas com quaisquer valores nulos (ausentes) e armazena o DataFrame limpo
df_clean = df.drop_nulls()

# Impressão dos dados limpos para inspecionar as primeiras linhas após remover os valores nulos
print("\nDados limpos:")
print(df_clean.head())


# Etapa 3: Cálculo das médias móveis

# Cálculo das médias móveis de 7 e 30 dias para a coluna 'Preço'

# O método with_columns é usado para adicionar novas colunas ao DataFrame
df_clean = df_clean.with_columns([
    # Cálculo da média móvel de 7 dias da coluna 'Price' e nomeação da coluna resultante como '7_day_ma'
    pl.col("Price").rolling_mean(window_size=7).alias("7_day_ma"),

    # Cálculo da média móvel de 30 dias da coluna 'Price' e nomeação da coluna resultante como '30_day_ma'
    pl.col("Price").rolling_mean(window_size=30).alias("30_day_ma")
])

# Impressão das primeiras 40 linhas dos dados para verificação das médias móveis
print("\nDados com médias móveis:")
print(df_clean.head(40)) # head(40) mostra as primeiras 40 linhas


# Etapa 4: Identificação dos cruzamentos

# Definir a condição para cruzamentos: quando a média móvel de 7 dias ultrapassa a média móvel de 30 dias
# Use o método filter para aplicar esta condição
cruzamentos = df_clean.filter(
    (pl.col("7_day_ma") > pl.col("30_day_ma")) & # Condição atual em que o MA de 7 dias é maior que o MA de 30 dias
    (pl.col("7_day_ma").shift(1) <= pl.col("30_day_ma").shift(1)) # Condição anterior em que o MA de 7 dias era menor ou igual ao MA de 30 dias
    # shift(1) olha para a linha anterior; isso ajuda a detectar o ponto de cruzamento
)

# Imprime as linhas onde os cruzamentos são detectados
print("\nCruzamentos:")
print(cruzamentos)

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

Dados iniciais:
shape: (5, 2)
┌────────────┬───────────┐
│ Date       ┆ Price     │
│ ---        ┆ ---       │
│ str        ┆ f64       │
╞════════════╪═══════════╡
│ 14-08-2018 ┆ 23.02     │
│ 15-08-2018 ┆ 23.15     │
│ 16-08-2018 ┆ 23.5      │
│ 17-08-2018 ┆ 23.4      │
│ 20-08-2018 ┆ 23.549999 │
└────────────┴───────────┘

Dados limpos:
shape: (5, 2)
┌────────────┬───────────┐
│ Date       ┆ Price     │
│ ---        ┆ ---       │
│ str        ┆ f64       │
╞════════════╪═══════════╡
│ 14-08-2018 ┆ 23.02     │
│ 15-08-2018 ┆ 23.15     │
│ 16-08-2018 ┆ 23.5      │
│ 17-08-2018 ┆ 23.4      │
│ 20-08-2018 ┆ 23.549999 │
└────────────┴───────────┘

Dados com médias móveis:
shape: (40, 4)
┌────────────┬───────────┬───────────┬───────────┐
│ Date       ┆ Price     ┆ 7_day_ma  ┆ 30_day_ma │
│ ---        ┆ ---       ┆ ---       ┆ ---       │
│ str        ┆ f64       ┆ f64       ┆ f64       │
╞════════════╪═══════════╪═══════════╪═══════════╡
│ 14-08-2018 ┆ 23.02     ┆ null      ┆ null      │
│ 15-08-2018 ┆ 23.15     ┆ null      ┆ null      │
│ 16-08-2018 ┆ 23.5      ┆ null      ┆ null      │
│ 17-08-2018 ┆ 23.4      ┆ null      ┆ null      │
│ …          ┆ …         ┆ …         ┆ …         │
│ 04-10-2018 ┆ 20.780001 ┆ 21.427143 ┆ 22.306667 │
│ 05-10-2018 ┆ 20.76     ┆ 21.28     ┆ 22.224    │
│ 08-10-2018 ┆ 20.809999 ┆ 21.177142 ┆ 22.143667 │
│ 09-10-2018 ┆ 20.76     ┆ 21.064285 ┆ 22.047    │
└────────────┴───────────┴───────────┴───────────┘

Cruzamentos:
shape: (4, 4)
┌────────────┬───────────┬───────────┬───────────┐
│ Date       ┆ Price     ┆ 7_day_ma  ┆ 30_day_ma │
│ ---        ┆ ---       ┆ ---       ┆ ---       │
│ str        ┆ f64       ┆ f64       ┆ f64       │
╞════════════╪═══════════╪═══════════╪═══════════╡
│ 09-01-2019 ┆ 16.58     ┆ 15.945714 ┆ 15.837    │
│ 13-05-2019 ┆ 19.43     ┆ 18.917143 ┆ 18.896    │
│ 21-06-2019 ┆ 19.110001 ┆ 19.341429 ┆ 19.310333 │
│ 30-07-2019 ┆ 19.459999 ┆ 18.941429 ┆ 18.784333 │
└────────────┴───────────┴───────────┴───────────┘

Conclusão

Polars é uma biblioteca DataFrame poderosa que oferece vantagens significativas de desempenho em relação a bibliotecas tradicionais como pandas. Sua capacidade de lidar com grandes conjuntos de dados com eficiência e sua ênfase na velocidade e no uso de memória tornam-no uma excelente escolha para aplicações com uso intensivo de dados.

Quer você esteja lidando com dados financeiros, séries temporais ou análises de dados em grande escala, o Polars pode ajudá-lo a obter resultados mais rápidos e eficientes.

Ao incorporar o Polars em seus fluxos de trabalho de análise de dados, você pode aproveitar ao máximo os recursos modernos de hardware e obter melhor desempenho, proporcionando mais tempo para se concentrar na obtenção de insights de seus dados, em vez de se preocupar com a velocidade de execução.

🔔 Assine o InfinitePy Newsletter para mais recursos e uma abordagem passo a passo para aprender Python, e fique atualizado com as últimas tendências e dicas práticas.

InfinitePy Newsletter - Sua fonte de aprendizado e inspiração em Python.