Capítulo 38, Ecossistema
NumPy e pandas
O pandas é o NumPy com rótulos e com tipos por coluna. Saber onde os dois se tocam, e onde se comportam de forma diferente, evita os erros mais sutis de quem passa de um para o outro.
Código deste capítulo: ecossistema/cap38_numpy_pandas.py
Uma tabela é um conjunto de arrays com nomes
Um DataFrame é uma tabela em que cada coluna é, por baixo, um array do NumPy (cada coluna com o seu tipo), e as linhas e colunas têm rótulos. É exatamente o que o projeto de estudantes do capítulo 41 ensina a sentir: várias colunas do mesmo tamanho que descrevem as mesmas pessoas.
uv sync --group ecossistema
import numpy as np
import pandas as pd
df = pd.DataFrame(
{"nome": ["Ana", "Bia", "Caio", "Davi"], "nota": [7.5, 9.0, 6.0, 8.2], "faltas": [1, 0, 4, 2]}
)
print(df)
print(df.dtypes.astype(str).tolist())
nome nota faltas
0 Ana 7.5 1
1 Bia 9.0 0
2 Caio 6.0 4
3 Davi 8.2 2
['str', 'float64', 'int64']
Cada coluna tem o seu tipo. No pandas 3, as colunas de texto têm um tipo próprio, str, e as numéricas mantêm o dtype do NumPy.
Do pandas para o NumPy, e de volta
O to_numpy() extrai os valores como um array. Em uma coluna só, o resultado é um array 1D com o tipo da coluna. Em um conjunto de colunas numéricas, é uma matriz 2D (e os inteiros viram decimais, pelas regras de promoção do capítulo 7). Em um DataFrame com texto misturado a números, o NumPy só consegue representar tudo como object, e você perde a velocidade:
notas = df["nota"].to_numpy()
print(type(notas).__name__, notas.dtype, notas.mean().round(1))
print(df.to_numpy().dtype, df[["nota", "faltas"]].to_numpy().dtype, df[["nota", "faltas"]].to_numpy().shape)
ndarray float64 7.7
object float64 (4, 2)
A regra que eu sigo: selecione só as colunas numéricas antes de converter. Uma matriz object não tem nenhuma das vantagens do NumPy.
Para voltar, o DataFrame aceita um array e os nomes das colunas:
matriz = np.array([[1.0, 2.0], [3.0, 4.0]])
print(pd.DataFrame(matriz, columns=["a", "b"]))
a b
0 1.0 2.0
1 3.0 4.0
O array que volta é uma visão protegida
No pandas 3, o to_numpy() devolve, sempre que possível, uma visão dos dados da coluna, sem copiar (e somente leitura), para que escrever nele não corrompa o DataFrame sem você perceber:
print(notas.flags["WRITEABLE"], notas.flags["OWNDATA"])
copia_editavel = df["nota"].to_numpy().copy()
copia_editavel[0] = 0.0
print(df["nota"].iloc[0])
False False
7.5
O array não é editável (WRITEABLE falso) e não é dono dos dados (OWNDATA falso). Para modificar, faça uma cópia, como no capítulo 10, e o DataFrame fica intacto.
A diferença mais perigosa: alinhamento por rótulo
O NumPy soma por posição. O pandas soma por rótulo: ele alinha os índices antes de operar. Com os mesmos valores em outra ordem, o resultado muda:
s1 = pd.Series([1, 2, 3], index=[0, 1, 2])
s2 = pd.Series([10, 20, 30], index=[2, 1, 0])
print((s1 + s2).tolist(), (s1.to_numpy() + s2.to_numpy()).tolist())
[31, 22, 13] [11, 22, 33]
O pandas somou 1 + 30, 2 + 20 e 3 + 10, porque os rótulos 0, 1 e 2 se correspondem. O NumPy somou 1 + 10, 2 + 20 e 3 + 30, posição por posição. Nenhum dos dois está "errado", mas misturar os dois sem perceber é uma fonte clássica de resultados trocados.
As ideias do NumPy, com outro nome
Quase tudo o que você aprendeu tem uma versão no pandas, com a vantagem dos rótulos:
| No NumPy | No pandas |
|---|---|
a[a > 7] (máscara) | df[df["nota"] > 7] |
np.where(cond, x, y) | np.where funciona direto sobre as colunas |
a.mean(axis=0) | df.mean() |
np.bincount(grupos, weights=v) | df.groupby("grupo")["v"].sum() |
np.unique(a, return_counts=True) | df["col"].value_counts() |
np.isnan | df.isna() |
df["aprovado"] = np.where(df["nota"] >= 7, "sim", "não")
print(df["aprovado"].tolist(), df[df["nota"] > 7]["nome"].tolist())
vendas = pd.DataFrame(
{"regiao": ["sul", "norte", "sul", "norte", "sul"], "valor": [10, 5, 20, 15, 30]}
)
print(vendas.groupby("regiao")["valor"].sum().to_dict())
['sim', 'sim', 'não', 'sim'] ['Ana', 'Bia', 'Davi']
{'norte': 20, 'sul': 60}
O groupby é a versão com rótulos do que o capítulo 31 fez com argsort e reduceat, só que aceita qualquer tipo de chave (aqui, texto).
Quando ficar no NumPy, quando ir para o pandas
Eu fico no NumPy quando os dados são números do mesmo tipo e da mesma forma (imagens, sinais, matrizes de características) e a velocidade importa. Vou para o pandas quando há colunas de tipos diferentes, rótulos, datas, dados ausentes e junções entre tabelas. E converto entre os dois sem cerimônia, no ponto em que a natureza dos dados muda.
Exercício 1
Normalizar uma coluna e devolver à tabela
Escreva adicionar_zscore(df, coluna) que acrescente ao DataFrame uma coluna coluna + "_z" com o z-score da coluna, calculado em NumPy, sem alterar o DataFrame original.