Capítulo 35, Avançado
O que mudou no Pandas 3
Muito do que se encontra na internet foi escrito para o Pandas 1 ou 2. Este capítulo reúne, em um lugar, o que mudou, **cada item provado por código**, e como encontrar o que precisa ser migrado em um projeto antigo.
O que foi removido
Estes nomes, muito comuns em material antigo, não existem mais no Pandas 3:
import inspect
import pandas as pd
removidos = {
"DataFrame.append": hasattr(pd.DataFrame, "append"),
"DataFrame.applymap": hasattr(pd.DataFrame, "applymap"),
"DataFrame.iteritems": hasattr(pd.DataFrame, "iteritems"),
"DataFrame.mad": hasattr(pd.DataFrame, "mad"),
"DataFrame.ix": hasattr(pd.DataFrame, "ix"),
"DataFrame.swapaxes": hasattr(pd.DataFrame, "swapaxes"),
"DataFrame.first": hasattr(pd.DataFrame, "first"),
"Series.view": hasattr(pd.Series, "view"),
}
print(removidos)
print("o substituto do applymap:", hasattr(pd.DataFrame, "map"))
{'DataFrame.append': False, 'DataFrame.applymap': False, 'DataFrame.iteritems': False, 'DataFrame.mad': False, 'DataFrame.ix': False, 'DataFrame.swapaxes': False, 'DataFrame.first': False, 'Series.view': False}
o substituto do applymap: True
| Antes | Agora |
|---|---|
df.append(outra) | pd.concat([df, outra]) |
df.applymap(f) | df.map(f) |
df.iteritems() | df.items() |
df.ix[...] | df.loc[...] ou df.iloc[...] |
df.first("5D") | Fatiar pelo índice de datas |
s.view(...) | s.astype(...) ou o NumPy |
O que mudou de comportamento
As mudanças mais perigosas não dão erro: o mesmo código produz outro resultado. Cada uma foi vista em um capítulo deste curso, e aqui estão juntas, com a prova:
import warnings
import numpy as np
texto = pd.Series(["a", "b"])
print("1. texto é str:", texto.dtype)
s = pd.Series([1, 2, 3], index=["x", "y", "z"])
try:
s[0]
except KeyError:
print("2. s[0] em índice de texto: KeyError (use .iloc[0])")
grupos = inspect.signature(pd.DataFrame.groupby).parameters
print("3. groupby observed:", grupos["observed"].default, "| tem axis:", "axis" in grupos)
print("4. resolução de datas:", pd.to_datetime(pd.Series(["2025-01-01"])).dtype)
print("5. str.contains com ausente:", pd.Series(["a", None]).str.contains("a").tolist())
1. texto é str: str
2. s[0] em índice de texto: KeyError (use .iloc[0])
3. groupby observed: True | tem axis: False
4. resolução de datas: datetime64[us]
5. str.contains com ausente: [True, False]
try:
pd.Series([1.0, None]).fillna(method="ffill")
except TypeError:
print("6. fillna(method=...) removido: use ffill() e bfill()")
try:
pd.Series([1, 2]).resample("M")
except (TypeError, ValueError):
print("7. resample('M') não vale mais: use 'ME' ou 'MS'")
df = pd.DataFrame({"a": [1, 2, 3], "b": [10.0, 20.0, 30.0]})
with warnings.catch_warnings(record=True) as avisos:
warnings.simplefilter("always")
df[df["a"] > 1]["b"] = 0
print("8. atribuição encadeada:", [a.category.__name__ for a in avisos], df["b"].tolist())
6. fillna(method=...) removido: use ffill() e bfill()
7. resample('M') não vale mais: use 'ME' ou 'MS'
8. atribuição encadeada: ['ChainedAssignmentError'] [10.0, 20.0, 30.0]
| # | Antes (Pandas 1 e 2) | Pandas 3 | Capítulo |
|---|---|---|---|
| 1 | Texto era object | Texto é str | 3 |
| 2 | s[0] caía para a posição em índice de texto | KeyError | 4 |
| 3 | groupby(observed=False) por padrão, com axis | observed=True, sem axis | 19 e 23 |
| 4 | Datas em nanossegundos | Datas em microssegundos (datetime64[us]) | 18 |
| 5 | str.contains devolvia NaN para ausentes (em object) | Devolve False no tipo str | 8 |
| 6 | fillna(method="ffill") | Removido: ffill() | 9 |
| 7 | resample("M") | "ME" ou "MS" | 18 |
| 8 | Atribuição encadeada às vezes funcionava | Nunca altera o original | 27 |
| 9 | groupby().apply recebia a coluna do grupo | Não recebe mais | 23 |
| 10 | to_numpy() devolvia um array gravável (às vezes) | Visão somente leitura | 27 |
Como achar o que migrar
Um projeto antigo pode ter centenas de usos de padrões que mudaram. O ruff, com as regras do conjunto PD (pandas-vet), aponta alguns dos mais comuns. Considere este arquivo antigo:
import pandas as pd
df = pd.read_csv("x.csv")
df.dropna(inplace=True)
valores = df.values
df = df.append({"a": 1}, ignore_index=True)
antigo = df.iteritems()
ruff check --select PD velho.py
velho.py:4:11: PD002 `inplace=True` should be avoided; it has inconsistent behavior
velho.py:5:11: PD011 Use `.to_numpy()` or `.array` instead of `.values`
O ruff apontou dois problemas, e eu conferi que ele não aponta o append nem o iteritems: esses já falham sozinhos, na primeira execução, com um AttributeError. A ferramenta ajuda, mas não substitui os testes: a melhor rede de segurança em uma migração é uma boa suíte, rodada com o Pandas 3.
Dá para fazer uma varredura simples também, com uma função que procura os padrões antigos no texto do código:
import re
PADROES = {
r"\.append\(": "df.append foi removido (use pd.concat)",
r"\.applymap\(": "applymap foi substituído por map",
r"inplace\s*=\s*True": "evite inplace",
r"fillna\(\s*method\s*=": "fillna(method=) foi removido (use ffill/bfill)",
r"\.ix\[": ".ix foi removido (use loc ou iloc)",
r"resample\(\s*[\"']M[\"']": "'M' virou 'ME' ou 'MS'",
r"\]\[[^\]]+\]\s*=": "possível atribuição encadeada (use .loc)",
}
def auditar(codigo: str) -> list[str]:
achados = []
for numero, linha in enumerate(codigo.splitlines(), start=1):
for padrao, aviso in PADROES.items():
if re.search(padrao, linha):
achados.append(f"linha {numero}: {aviso}")
return achados
amostra = 'df = df.append(x)\ndf["a"].fillna(0, inplace=True)\nr = s.resample("M").sum()\n'
for achado in auditar(amostra):
print(achado)
linha 1: df.append foi removido (use pd.concat)
linha 2: evite inplace
linha 3: 'M' virou 'ME' ou 'MS'
Fixar a versão enquanto você migra
Se um projeto ainda não pode migrar, fixe a versão, e migre quando houver tempo e testes:
uv add "pandas<3"
A regra de ouro: migre com testes. As mudanças de comportamento (itens 1 a 10 da tabela) não levantam erro, e só uma boa suíte, ou uma conferência cuidadosa das saídas, mostra que algo mudou.
Exercício 1
O auditor de código
Use a função auditar para conferir que ela acha os três problemas da amostra, em linhas diferentes, e que não acusa um código já migrado (pd.concat, ffill, resample("ME")).