Pular para o conteúdo

    Capítulo 34, Avançado

    Pandas e NumPy

    O NumPy está por baixo de cada coluna. Saber ir e voltar entre os dois, e onde as convenções diferem, evita os resultados "quase certos" que mais custam a ser achados.

    De Pandas para NumPy e de volta

    O to_numpy() extrai o array de uma Series ou de um DataFrame. Em uma tabela, o resultado tem um só tipo: o NumPy não aceita um tipo por coluna, então o Pandas escolhe o que comporta todos. Para voltar, o DataFrame recebe o array, e você devolve os rótulos que o array não carrega:

    avancado/cap34_pandas_numpy.pylinhas 10 a 26
    import numpy as np
    import pandas as pd
    
    pd.set_option("display.width", 170)
    pd.set_option("display.max_columns", 20)
    
    CANONICO = {"engenharia": "Engenharia", "marketing": "Marketing", "finanças": "Finanças",
                "ciência de dados": "Ciência de Dados", "rh": "RH"}
    df = pd.read_csv("dados/funcionarios_100.csv").drop_duplicates("id_funcionario", keep="last")
    df["salario"] = pd.to_numeric(df["salario"].str.replace("R$ ", "", regex=False))
    df["departamento"] = df["departamento"].str.strip().str.lower().map(CANONICO)
    df = df.dropna(subset=["salario", "departamento"]).reset_index(drop=True)
    
    X = df[["experiencia", "projetos_concluidos"]].to_numpy(dtype=float)
    print(X.shape, X.dtype)
    de_volta = pd.DataFrame(X, index=df.index, columns=["experiencia", "projetos_concluidos"])
    print(bool(de_volta.equals(df[["experiencia", "projetos_concluidos"]].astype(float))))
    
    Saída
    (90, 2) float64
    True
    

    Ausentes ao atravessar a fronteira

    Um tipo anulável (Int64) com ausentes não cabe em um array de inteiros do NumPy. O to_numpy() devolve um array de objetos, a não ser que você diga o tipo e o valor que representa o ausente:

    avancado/cap34_pandas_numpy.pylinhas 31 a 33
    idade = pd.Series([30, None, 28], dtype="Int64")
    print(idade.to_numpy().dtype)
    print(idade.to_numpy(dtype="float64", na_value=np.nan))
    
    Saída
    float64
    [30. nan 28.]
    

    Funções do NumPy mantêm os rótulos

    Passar uma Series a uma função do NumPy devolve uma Series com o mesmo índice: os rótulos viajam. Já o np.where devolve um array simples, e você reenvelopa (ou usa o where do próprio Pandas):

    avancado/cap34_pandas_numpy.pylinhas 38 a 45
    salario = df["salario"]
    log_salario = np.log(salario)
    print(type(log_salario).__name__, bool(log_salario.index.equals(salario.index)))
    
    faixa = np.where(salario > salario.median(), "acima", "abaixo")
    print(type(faixa).__name__)
    faixa = pd.Series(faixa, index=salario.index)
    print(faixa.value_counts().to_dict())
    
    Saída
    Series True
    ndarray
    {'abaixo': 45, 'acima': 45}
    

    A armadilha do desvio padrão

    O std do Pandas divide por n - 1 (a variância amostral, ddof=1), e o np.std divide por n (ddof=0). Um z-score calculado de cada jeito dá números diferentes, e quem mistura os dois (um na análise, outro no modelo) tem um erro pequeno que ninguém acha:

    avancado/cap34_pandas_numpy.pylinhas 50 a 52
    valores = df["salario"]
    print(round(float(valores.std()), 2), round(float(np.std(valores)), 2), round(float(np.std(valores.to_numpy())), 2))
    print(round(float(np.std(valores.to_numpy(), ddof=1)), 2) == round(float(valores.std()), 2))
    
    Saída
    2433.44 2419.88 2419.88
    True
    

    Repare no terceiro caso: o np.std(valores), aplicado à Series, deu o mesmo valor do array (ddof=0, 2419,88) e não o do .std() da própria Series (2433,44). O NumPy repassa o ddof=0 ao método da Series, e o Pandas respeita o que recebe. Ou seja, o mesmo dado e duas formas de pedir o desvio padrão no Pandas dão resultados diferentes. A regra: escreva o ddof explicitamente, em qualquer das duas bibliotecas, e confira que a análise e o modelo usam o mesmo.

    Padronizar com broadcasting

    Com o NumPy, padronizar todas as colunas de uma vez é uma linha (o broadcasting do capítulo 13 do curso de NumPy), e dá o mesmo resultado do Pandas quando o ddof coincide:

    avancado/cap34_pandas_numpy.pylinhas 57 a 60
    Z = (X - X.mean(axis=0)) / X.std(axis=0, ddof=1)
    z_pandas = (df[["experiencia", "projetos_concluidos"]] - df[["experiencia", "projetos_concluidos"]].mean()) / \
        df[["experiencia", "projetos_concluidos"]].std()
    print(bool(np.allclose(Z, z_pandas.to_numpy())))
    
    Saída
    True
    

    Uma regressão com o NumPy, conferida com o Pandas

    O salário cresce com a experiência? A reta de mínimos quadrados responde, e a inclinação tem uma relação exata com a correlação do Pandas (inclinação = correlação × desvio_y / desvio_x):

    avancado/cap34_pandas_numpy.pylinhas 65 a 72
    exp = df["experiencia"].to_numpy(dtype=float)
    sal = df["salario"].to_numpy(dtype=float)
    A = np.column_stack([np.ones(len(exp)), exp])
    (intercepto, inclinacao), *_ = np.linalg.lstsq(A, sal, rcond=None)
    print(round(float(inclinacao)), round(float(intercepto)))
    
    pela_correlacao = df["salario"].corr(df["experiencia"]) * df["salario"].std() / df["experiencia"].std()
    print(bool(np.isclose(inclinacao, pela_correlacao)))
    
    Saída
    379 5044
    True
    

    Cada ano de experiência soma cerca de 380 reais ao salário neste conjunto (que foi gerado assim, mais ruído). A segunda linha mostra que o NumPy e o Pandas contam a mesma história por caminhos diferentes.

    Variáveis de texto como números

    O get_dummies transforma uma coluna de categorias em colunas de 0 e 1, a matriz que um modelo espera:

    avancado/cap34_pandas_numpy.pylinhas 77 a 79
    dummies = pd.get_dummies(df["departamento"], dtype=float)
    print(dummies.shape, dummies.sum(axis=1).unique().tolist(), dummies.columns.tolist())
    print(type(dummies.to_numpy()).__name__)
    
    Saída
    (90, 5) [1.0] ['Ciência de Dados', 'Engenharia', 'Finanças', 'Marketing', 'RH']
    ndarray
    

    Cada linha soma 1, porque cada pessoa está em um departamento.

    Exercício 1

    Padronizar e devolver com os rótulos

    Escreva padronizar(tabela, colunas), que devolva uma DataFrame com as colunas padronizadas pelo NumPy (com ddof=1), mantendo o índice e os nomes, e confira que o resultado é igual ao z-score do Pandas.