Por Que Migrar do Pandas para o Polars em 2026?
Se você trabalha com dados em Python, é bem provável que o Pandas faça parte da sua rotina há um bom tempo. Eu mesmo usei Pandas por anos sem questionar — até começar a lidar com datasets que faziam minha máquina travar. Em 2026, o cenário mudou bastante. O Polars, uma biblioteca DataFrame construída em Rust, entrega ganhos de desempenho de 4x a 14x em operações comuns e até 100x em datasets grandes, consumindo uma fração da memória.
Com metade das equipes de dados já testando ou adotando o Polars, a pergunta deixou de ser "devo considerar o Polars?" e passou a ser "como faço pra migrar?".
Neste guia, vamos ver passo a passo como converter suas operações mais comuns do Pandas para o Polars, com exemplos lado a lado, benchmarks reais e uma estratégia de migração que funciona na prática.
O Que É o Polars e Como Ele Se Diferencia
O Polars é uma biblioteca open-source de processamento de dados escrita em Rust, com bindings para Python. Enquanto o Pandas opera em thread única sobre arrays NumPy, o Polars foi projetado desde o início pra aproveitar todos os núcleos do processador, usando Apache Arrow como base de memória colunar.
As vantagens técnicas principais são:
- Multi-threading automático — todas as operações são paralelizadas sem configuração extra
- Lazy execution — o Polars monta um plano de consulta otimizado antes de executar, parecido com um motor SQL
- Eficiência de memória — armazenamento colunar Apache Arrow consome 30% a 70% menos RAM que o Pandas
- Streaming — processa datasets que não cabem na memória (algo que o Pandas simplesmente não faz nativamente)
O Polars requer Python 3.10+ e está sob licença MIT. A versão mais recente saiu em março de 2026, com atualizações a cada duas semanas mais ou menos.
Instalação e Configuração Inicial
Instalar o Polars é direto ao ponto:
pip install polars
Se você precisa de funcionalidades extras como leitura de Excel, conexão com bancos de dados ou operações com fuso horário, instale com os extras correspondentes:
# Instalar com todos os extras
pip install "polars[all]"
# Ou selecionar extras específicos
pip install "polars[xlsx2csv,sqlalchemy,timezone]"
Pra verificar se deu tudo certo:
import polars as pl
print(pl.__version__)
pl.show_versions()
Comparação de Sintaxe: Pandas vs Polars
Aqui vai a boa notícia: a sintaxe do Polars é bem intuitiva pra quem já conhece Pandas. Vamos ver as operações mais comuns lado a lado.
Criação de DataFrames
# Pandas
import pandas as pd
df_pd = pd.DataFrame({
"nome": ["Ana", "Bruno", "Carlos"],
"idade": [28, 35, 42],
"salario": [5500.0, 7800.0, 9200.0]
})
# Polars
import polars as pl
df_pl = pl.DataFrame({
"nome": ["Ana", "Bruno", "Carlos"],
"idade": [28, 35, 42],
"salario": [5500.0, 7800.0, 9200.0]
})
Praticamente idêntico, certo? A diferença principal é que o Polars não tem índice — e, honestamente, isso simplifica muita coisa.
Leitura de Arquivos CSV e Parquet
# Pandas
df_pd = pd.read_csv("dados.csv")
df_pd = pd.read_parquet("dados.parquet")
# Polars (eager - execução imediata)
df_pl = pl.read_csv("dados.csv")
df_pl = pl.read_parquet("dados.parquet")
# Polars (lazy - execução otimizada, recomendado)
df_lazy = pl.scan_csv("dados.csv")
df_lazy = pl.scan_parquet("dados.parquet")
# Executa só quando necessário:
resultado = df_lazy.collect()
O scan_* é onde a mágica acontece. Ele cria um LazyFrame que permite ao otimizador reorganizar e eliminar operações desnecessárias antes de executar qualquer coisa. Na prática, isso faz uma diferença enorme em pipelines com vários passos.
Seleção de Colunas e Filtragem
# Pandas
df_pd[["nome", "salario"]]
df_pd[df_pd["idade"] > 30]
df_pd.loc[df_pd["salario"] > 6000, "nome"]
# Polars
df_pl.select(["nome", "salario"])
df_pl.filter(pl.col("idade") > 30)
df_pl.filter(pl.col("salario") > 6000).select("nome")
No Polars, pl.col() é o bloco fundamental de tudo. Toda operação sobre colunas usa expressões baseadas nessa função, o que torna o código mais explícito e — mais importante — otimizável pelo motor interno.
GroupBy e Agregações
# Pandas
df_pd.groupby("departamento").agg(
media_salario=("salario", "mean"),
total_funcionarios=("nome", "count")
)
# Polars
df_pl.group_by("departamento").agg(
pl.col("salario").mean().alias("media_salario"),
pl.col("nome").count().alias("total_funcionarios")
)
Detalhe importante: no Polars o método é group_by (com underscore), enquanto no Pandas é groupby. Parece bobagem, mas isso pega muita gente no início. As agregações usam expressões encadeadas — mais verboso, sim, mas também mais poderoso e legível quando as coisas ficam complexas.
Joins (Junções)
# Pandas
resultado = pd.merge(df_vendas, df_clientes, on="cliente_id", how="left")
# Polars
resultado = df_vendas.join(df_clientes, on="cliente_id", how="left")
Os joins no Polars são entre 3x e 13x mais rápidos que no Pandas. Sério. Isso graças à implementação paralela de hash join.
Adição e Transformação de Colunas
# Pandas
df_pd["salario_anual"] = df_pd["salario"] * 12
df_pd["faixa_etaria"] = df_pd["idade"].apply(
lambda x: "jovem" if x < 30 else "senior"
)
# Polars
df_pl = df_pl.with_columns(
(pl.col("salario") * 12).alias("salario_anual"),
pl.when(pl.col("idade") < 30)
.then(pl.lit("jovem"))
.otherwise(pl.lit("senior"))
.alias("faixa_etaria")
)
No Polars, with_columns() substitui o assign() ou a atribuição direta do Pandas. E aqui vai um ponto crucial: pl.when().then().otherwise() substitui o temido apply() com lambda. O desempenho é muito superior porque é vetorizado — nada de Python puro rodando linha a linha.
Lazy Execution: O Grande Diferencial do Polars
Se eu tivesse que escolher uma única razão pra adotar o Polars, seria a lazy execution. Esse recurso não tem equivalente direto no Pandas, e ele muda completamente a forma como você pensa sobre pipelines de dados.
A ideia é simples: você descreve toda a sequência de operações que quer fazer, e o Polars otimiza o plano de execução antes de processar qualquer dado.
# Pipeline completo com lazy execution
resultado = (
pl.scan_parquet("vendas_2026.parquet")
.filter(pl.col("valor") > 100)
.filter(pl.col("regiao") == "sudeste")
.group_by("categoria")
.agg([
pl.col("valor").sum().alias("total_vendas"),
pl.col("valor").mean().alias("ticket_medio"),
pl.col("id_pedido").n_unique().alias("num_pedidos")
])
.sort("total_vendas", descending=True)
.collect() # Executa tudo de uma vez, otimizado
)
O otimizador aplica automaticamente técnicas como:
- Predicate pushdown — filtros são empurrados pra o mais cedo possível na leitura do arquivo
- Projection pushdown — só as colunas que você realmente usa são carregadas do disco
- Eliminação de operações redundantes — passos desnecessários são removidos automaticamente
- Paralelização automática — operações independentes rodam em paralelo sem você fazer nada
Dá pra visualizar o plano de execução e entender exatamente o que o Polars vai fazer:
# Ver o plano de execução otimizado
lazy_query = (
pl.scan_parquet("vendas_2026.parquet")
.filter(pl.col("valor") > 100)
.select(["categoria", "valor"])
)
print(lazy_query.explain())
Quando vi isso pela primeira vez, confesso que fiquei impressionado. É como ter um DBA otimizando suas queries automaticamente.
Benchmarks Reais: Pandas vs Polars em 2026
Números falam mais alto que promessas. Testes independentes com datasets de aproximadamente 1 GB mostram diferenças bem expressivas:
- Leitura de CSV: Polars é 5x mais rápido e usa 87% menos memória (179 MB vs 1,4 GB no Pandas)
- Filtragem de linhas: Polars é 4,6x mais rápido graças ao multi-threading
- GroupBy e agregações: Polars é 2,6x a 10x mais rápido, dependendo da complexidade da operação
- Joins: Polars é 9x a 13x mais rápido com hash joins paralelos
- Ordenação (sort): Polars é 11,7x mais rápido — a maior diferença entre todas as operações testadas
Vale ressaltar: pra datasets pequenos (menos de 100 MB), a diferença é bem menos expressiva. E pra operações pesadas de strings e regex, o Pandas ainda pode ganhar em certos cenários. Não é tudo preto e branco.
Guia de Migração Passo a Passo
Migrar um projeto inteiro do Pandas pro Polars de uma tacada só? Péssima ideia. Siga esta estratégia gradual — funciona muito melhor na prática.
Passo 1: Identifique os Gargalos
Antes de sair migrando tudo, meça onde o Pandas é mais lento no seu pipeline. Use profiling simples pra descobrir as operações que mais consomem tempo e memória:
import time
inicio = time.perf_counter()
# Sua operação Pandas aqui
df_resultado = df.groupby("coluna").agg({"valor": "sum"})
tempo = time.perf_counter() - inicio
print(f"Tempo: {tempo:.2f}s")
Não precisa ser sofisticado. Às vezes um time.perf_counter() simples já mostra onde estão os problemas.
Passo 2: Converta as Operações Críticas Primeiro
Comece migrando as operações mais lentas. Leitura de arquivos grandes, joins e agregações costumam ser os candidatos ideais — é onde o Polars brilha mais.
Passo 3: Use Interoperabilidade Pandas ↔ Polars
O Polars facilita bastante a convivência entre as duas bibliotecas:
# Pandas → Polars
df_polars = pl.from_pandas(df_pandas)
# Polars → Pandas
df_pandas = df_polars.to_pandas()
# Conversão eficiente via Arrow (sem cópia de memória)
import pyarrow as pa
arrow_table = df_polars.to_arrow()
df_pandas = arrow_table.to_pandas()
Essa interoperabilidade é o que torna a migração gradual viável. Você não precisa reescrever tudo de uma vez — vai convertendo aos poucos.
Passo 4: Adapte o Código de Machine Learning
A maioria das bibliotecas de ML (scikit-learn, XGBoost, LightGBM) ainda espera Pandas ou NumPy como entrada. A dica aqui é: faça o processamento pesado com Polars e converta só na fronteira com essas bibliotecas.
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
# Processamento pesado com Polars (rápido)
df_processado = (
pl.scan_parquet("features.parquet")
.filter(pl.col("valido") == True)
.select(["feature_1", "feature_2", "feature_3", "target"])
.collect()
)
# Converte para NumPy apenas para o ML
X = df_processado.select(["feature_1", "feature_2", "feature_3"]).to_numpy()
y = df_processado.select("target").to_numpy().ravel()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
modelo = RandomForestClassifier()
modelo.fit(X_train, y_train)
Passo 5: Substitua apply() por Expressões
O apply() é uma das maiores fontes de lentidão no Pandas. Ele executa Python puro linha a linha — basicamente anula qualquer vantagem de vetorização. No Polars, substitua por expressões nativas:
# Pandas (lento)
df_pd["desconto"] = df_pd.apply(
lambda row: row["preco"] * 0.1 if row["vip"] else 0, axis=1
)
# Polars (rápido)
df_pl = df_pl.with_columns(
pl.when(pl.col("vip"))
.then(pl.col("preco") * 0.1)
.otherwise(0)
.alias("desconto")
)
Essa mudança sozinha já pode acelerar partes do seu pipeline em 10x ou mais.
Quando NÃO Migrar para o Polars
Nem tudo é sobre velocidade. Existem cenários onde manter o Pandas faz total sentido:
- Datasets pequenos (menos de 100 MB) — o ganho é mínimo e não justifica reescrever código que já funciona
- Manipulação intensiva de strings — regex complexo, parsing de texto e codificação categórica ainda são mais rápidos no Pandas em vários casos
- Integração forte com ecossistema ML — se seu pipeline depende pesadamente de scikit-learn, a conversão constante entre formatos pode anular os ganhos
- Equipe sem familiaridade — se todo mundo só conhece Pandas e o prazo tá apertado, migrar agora pode não ser a melhor escolha
- Análise exploratória rápida — pra EDA em Jupyter Notebooks com datasets moderados, o Pandas continua excelente
O importante é usar a ferramenta certa pro problema certo. Não é porque o Polars é mais rápido que você precisa migrar tudo cegamente.
Pipeline Híbrido: O Melhor dos Dois Mundos
Na minha experiência, a abordagem mais prática em 2026 é o pipeline híbrido. Use Polars pros gargalos de ETL e processamento pesado, e converta pra Pandas nas fronteiras com bibliotecas de visualização e ML.
import polars as pl
import pandas as pd
import matplotlib.pyplot as plt
# ETL pesado com Polars (rápido)
df_resultado = (
pl.scan_parquet("vendas_grande.parquet")
.filter(pl.col("ano") == 2026)
.group_by("mes")
.agg(pl.col("receita").sum().alias("receita_total"))
.sort("mes")
.collect()
)
# Visualização com Matplotlib via Pandas
df_plot = df_resultado.to_pandas()
plt.figure(figsize=(10, 6))
plt.bar(df_plot["mes"], df_plot["receita_total"])
plt.title("Receita Mensal 2026")
plt.xlabel("Mês")
plt.ylabel("Receita (R$)")
plt.show()
Esse padrão permite que você aproveite a velocidade do Polars onde realmente importa, sem abrir mão do ecossistema gigante do Pandas. É pragmático, funciona, e evita aquela dor de cabeça de reescrever tudo de uma vez.
Perguntas Frequentes
O Polars vai substituir o Pandas completamente?
Não no curto prazo — e talvez nunca completamente. O Pandas tem mais de uma década de desenvolvimento, um ecossistema massivo e milhões de usuários. O Polars é uma alternativa poderosa pra cenários onde desempenho e escala são críticos, mas o Pandas continua sendo a escolha padrão pra análise exploratória e projetos menores. A tendência em 2026 é dominar ambos e usar cada um no contexto certo.
Preciso reescrever todo o meu código Pandas para usar Polars?
De jeito nenhum. A melhor estratégia é a migração gradual. Identifique os gargalos de desempenho no seu pipeline e converta apenas essas partes. O Polars oferece pl.from_pandas() e .to_pandas() justamente pra facilitar a convivência entre as duas bibliotecas no mesmo projeto.
O Polars funciona com Jupyter Notebooks?
Sim, funciona perfeitamente. O Polars exibe DataFrames de forma organizada no Jupyter, e a experiência é bem parecida com a do Pandas. A diferença é que operações sobre datasets grandes vão ser significativamente mais rápidas.
Qual a diferença entre Polars e PySpark?
O PySpark é feito pra computação distribuída em clusters — ideal pra datasets de dezenas ou centenas de gigabytes que não cabem numa máquina só. O Polars é otimizado pra processamento local, aproveitando ao máximo os recursos de uma única máquina. Pra datasets de até 10-50 GB, o Polars geralmente é mais rápido e muito mais simples de configurar.
Como o Polars lida com valores nulos?
O Polars usa null nativo do Apache Arrow pra representar valores ausentes, que é mais eficiente que o NaN do NumPy usado pelo Pandas. Pra tratar nulos, use fill_null(), drop_nulls() ou interpolate() — os nomes são parecidos com os do Pandas, então a adaptação é rápida.