Pular para o conteúdo

    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.

    Terminal
    uv sync --group ecossistema
    
    ecossistema/cap38_numpy_pandas.pylinhas 10 a 17
    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())
    
    Saída
       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:

    ecossistema/cap38_numpy_pandas.pylinhas 22 a 24
    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)
    
    Saída
    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:

    ecossistema/cap38_numpy_pandas.pylinhas 26 a 27
    matriz = np.array([[1.0, 2.0], [3.0, 4.0]])
    print(pd.DataFrame(matriz, columns=["a", "b"]))
    
    Saída
         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:

    ecossistema/cap38_numpy_pandas.pylinhas 32 a 35
    print(notas.flags["WRITEABLE"], notas.flags["OWNDATA"])
    copia_editavel = df["nota"].to_numpy().copy()
    copia_editavel[0] = 0.0
    print(df["nota"].iloc[0])
    
    Saída
    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:

    ecossistema/cap38_numpy_pandas.pylinhas 40 a 42
    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())
    
    Saída
    [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 NumPyNo 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.isnandf.isna()
    ecossistema/cap38_numpy_pandas.pylinhas 47 a 53
    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())
    
    Saída
    ['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.