Guia de funcionalidades avançadas do Pandas

Artigo traduzido para o português do original escrito por Fabian Bosler em 20/set/2019.
Você pode acessar o artigo original aqui.

Na parte I desse série sobre Pandas, exploramos o básico do Pandas, incluindo:

  • Como carregar dados
  • Como inspecionar, ordenar e filtrar dados
  • Como analisar dados usando groupby/transform

Caso esses conceitos sejam novos para você, volte à parte I e dê uma olhada lá.

Nesse artigo, iremos cobrir os seguintes tópicos:

  • Tipos de dados e conversões
  • Métodos úteis de acesso a certos tipos de dados
  • Combinando DataFrames
  • Reformatando DataFrames

Prérequisitos

Ambiente Python, sugiro o Jupyter Notebook. Caso você ainda não tenha, não se preocupe. O esforço é mínimo e leva menos de 10 minutos.

Setup

Antes de qualquer manipulação de dados, vamos pegar alguns. Iremos usar dados fictícios de vendas. Este repo do GitHub contém os dados e o código deste artigo.

Crie um novo notebook e importe o Pandas (import pandas as pd). Em geral eu costumo ajustar as configurações do meu notebook dessa forma:

from IPython.core.display import display, HTML
display(HTML("<style>.container {width:90% !important;}</style>"))

Esses comandos tornam o notebook mais largo e com isso utilizam mais espaço da tela (normalmente o notebook tem uma largura fixa, o que fica ruim com telas mais largas).

Carregando os dados

invoices =
pd.read_csv('https://raw.githubusercontent.com/FBosler/you-
datascientist/master/invoices.csv')

Tipos de dados e conversões

Tipos de dados disponíveis

  • object: utilizado para ‘strings’ (ie, sequências de caracteres)
  • int64: usado para números inteiros
  • float64: usado para números de ‘ponto-flutuante’ (ie, decimais e frações)
  • bool: usado somente para valores True ou False (Verdadeiro ou Falso)
  • datetime64: usado para valores relativos a datas
  • timedelta: usado para representar a diferença entre datas
  • category: usado para valores que usam um de um número limitado de opções disponíveis (não é mandatório, mas categorias podem ter ordenamento explícito)

Retornando tipos de dados

Após carregar os dados e rodar uma consulta rápida invoices.sample(5), observamos que a base de dados parece ser grande, mas bem estruturada.

Para começar a entender a base de dados, eu geralmente rodo info e describe depois do sample, mas como estamos aqui para aprender sobre tipos de dados iremos pular a parte de exploração de dados. Não surpreende então que exista um comando para exibir os tipos de dados de um DataFrame.

Nós carregamos os dados sem nenhum tipo de conversão, então o Pandas fez o seu melhor em termos de designar os tipos. Vamos ver que todas as colunas, à exceção de ‘Meal Price’ e de “Super Hero Present’, são do tipo object (ie, são strings). A partir da inspeção rápida que fizemos antes, parece que algumas colunas poderiam ter algum outro tipo de dado. Vamos alterar isso.

Convertendo tipos de dados

Há duas formas padrão de converter tipos de dados no pandas:

  • <coluna>.astype(<tipo desejado>)
  • Funções de conversão, como pd.to_numeric ou pd.to_datetime

(A) astype

astype é rápido e trabalha bem com dados sanitizados e quando a conversão é direta, por exemplo, de int64 para float64 (ou vice-versa). astype precisa ser chamado diretamente na coluna que você quer converter. Dessa forma:

invoices['Type of Meal'] = invoices['Type of Meal'].astype('category')
invoices['Date'] = invoices['Date'].astype('datetime64') invoices['Meal Price'] = invoices['Meal Price'].astype('int')

Para validar o resultado da conversão de tipos, rodamos invoices.dtypes novamente:

Nota: Há algumas diferenças entre versões do Pandas. Na versão 0.23.x é possível converter a coluna ‘Date of Meal usando .astype(‘datetime64’) e o Pandas automaticamente converte para UTC. O formato UTC é útil pois é um formato padrão de data e nos permite subtrair ou adicionar datas a outras datas.

Contudo, isto já não funciona mais a partir da versão 0.25.x onde teremos como retorno um ValueError que nos diz que datas Tz-aware (sensível a fuso horário) não podem ser convertidas sem ajustes adicionais.

(B) funções de conversão

Existem 3 funções pd.to_<algum_tipo>, mas para mim apenas 2 são usadas com frequência:

  • pd.to_numeric()
  • pd.to_datetime()
  • pd.to_timedelta() (não me recordo se já cheguei a usar essa função, pra dizer a verdade)

A principal vantagem dessas funções sobre a astype é que torna possível especificar o comportamento no caso de encontrar um valor que não pode ser convertido. Ambas funções aceitam um parâmetro adicional ‘errors’, que define como os erros devem ser tratados. Poderíamos escolher por ignorar erros através errors=’ignore’, ou transformar os valores ofensores em valores np.nan através de errors=’coerce’. O comportamento padrão é alertar os erros.

Eu acho que não há uma solução perfeita, e eu em geral investigo antes de tomar uma decisão. Quanto menos valores ofensores em relação ao número total de observações, mais provável fica de que eu use np.nan.

pd.to_numeric()

Para melhor exemplificar, vamos mexer um pouco com nossos dados:

invoices.loc[45612,'Meal Price'] = 'I am causing trouble'
invoices.loc[35612,'Meal Price'] = 'Me too'

invoices[‘Meal Price’].astype(int) agora irá dar o erro ValueError: invalid literal for int() with base 10: ‘Me too.’ Porque não há uma solução óbvia para converter uma string num inteiro. Sempre que eu encontro erros inesperados de conversão, eu em geral checo explicitamente os valores da coluna para melhor entender a magnitude dos valores estranhos.

Você poderia então encontrar as linhas ofensoras dessa forma:

invoices['Meal Price'][invoices['Meal Price'].apply( lambda x: isinstance(x,str)
)]

A partir daí fica rápido consertar os valores ou tomar uma decisão embasada sobre como você quer lidar com a queda de conversão.

O exemplo acima é um onde seria razoável apenas converter os valores em np.nan ao passar errors=’coerce’ para pd.to_numeric(), dessa forma:

pd.to_numeric(invoices['Meal Price'], errors='coerce')

Agora deveria ser notado que você terá dois valores np.nan nos seus dados, então deve ser uma boa ideia lidar com eles direto. O problema com valores np.nan é que colunas de inteiros não sabem como lidar com eles. Por isso a coluna será de valor flutuante.

#converte os valores ofensores em np.nan
invoices['Meal Price'] = pd.to_numeric(invoices['Meal Price'],errors='coerce')

#preenche valores np.nan com a mediana dos dados
invoices['Meal Price'] = invoices['Meal Price'].fillna(invoices['Meal Price'].median())

#converte a coluna em inteiro
invoices['Meal Price'].astype(int)

pd.to_datetime()

Faz o que o nome sugere, o método converte uma string em um formato datetime (date e horário). Para chamar t0_datetime numa coluna você deveria: pd.to_datetime(invoices[‘Date of Meal’]. Pandas irá então ver o formato e parsear a data. E costuma fazer isso muito bem:

Contudo, às vezes você poderá encontrar alguma formatação atípica, como no último exemplo acima. Nesse caso, você pode especificar um formato próprio, dessa forma: pd.to_datetime(‘20190108, format=’%Y%d%m’). Pense nesse formato de string como uma máscara para checar contra a string de data. Caso a máscara se encaixe, a string será convertida. Cheque esse link para ver uma lista de todos componentes de formatos de datas. Um parâmetro adicional quando trabalhando com formatos customizados é exact=false. print(pd.to_datetime(‘yolo 20190108′, format=’%Y%d%m’, exact=False)) irá funcionar, ao passo que falharia sem o parâmetro exact. Com exact=False, o Pandas tenta achar o padrão em qualquer ponto da string.

Antes de seguir, vamos converter nossa coluna Date of Meal, dessa forma: invoices[‘Date of Meal’] = pd.to_datetime(invoices[‘Date of Meal’],
utc=True)

Métodos úteis de acesso a certos tipos de dados

Pense no ‘accessor‘ do Pandas como uma propriedade que age como uma interface aos métodos específicos dos tipos que você quer acessar. Esses métodos são altamente especializados. Eles executam um e apenas um trabalho. Contudo, são excelentes e muito concisos em respeito ao seu trabalho.

Existem três tipos diferentes de accessors:

  • dt
  • str
  • cat

Todos os métodos são acessados através de .<accessor>.method na coluna selecionada, dessa forma: invoices[‘Data of Meal’].dt.date.

Accessor – dt

Esse é um dos mais úteis e mais diretos métodos accessor:

  • date (retorna a data do valor datetime), ou
  • weekday_name (retorna o nome do dia), month_name (este é implementado de modo inconsistente, pois weekday_name é uma propriedade, mas month_name é um método e precisa ser chamdo com os parentesis)
  • days_in_month
  • nanosecond, microsecond, second, minute, hour, day, week, month, quarter, year traz o inteiro da frequência correspondente.
  • is_leap_year, is_month_start, is_month_end, is_quarter_start, is_quarter_end, is_year_start, is_year_end retorna True ou False para cada valor respectivamente. Veja o exemplo de is_month_end

Podemos usar os resultados para filtrar nossos dados para as linhas onde Date of Meal seja final do mês.

  • to_pydatetime(), converte o datetime Pandas em um formato padrão de datetime Python
  • to_period(<PERÍODO>), disponível para W, M, Q e Y, converte as datas em períodos

Accessor – str

O accessor str é muito útil, não porque permite adicionar funcionalidades, mas porque facilita muito a leitura do código.

  • lower() / upper() para gerenciar maiúsculas e minúsculas
  • ljust(largura), rjust(largura), center(largura), zfill(largura) para controlar a posição dos caracteres. Todas tomam a largura total da string resultante como input. ljust, rjust e center preenchem a diferença para a largura desejada com espaços em branco. zfil adiciona zeros. ljust ajusta à esquerda, rjust ajusta à direita.
  • startswith(substring), endswith(substring), contains(substring) checa a presença de substrings
  • swapcase(), repeat(vezes) para diversão

Accessor – cat

Na minha opinião é o menos poderoso dos três accessors, ou pelo menos é o que eu menos utilizo. cat provém acesso a algumas operações categóricas, tais como:

  • ordered deixa você saber se a coluna está ordenada
  • categories retorna as categorias
  • codes converte a categoria para a sua representação numérica
  • reorder_categories muda o ordenamento existente das categorias

Combinando DataFrames

Concatenando

Concatenar é útil quando você tem dados similares (estruturalmente e em termos de conteúdo) espalhados em múltiplos arquivos. Você pode concatenar dados de forma vertical (ie., empilhar os dados um em cima do outro) ou de forma horizontal (ie., um ao lado do outro).

Concatenando – empilhamento vertical

Por exemplo, imagine que um cliente fornece um arquivo de dados por mês (porque ele tira os relatórios mensalmente). Esses arquivos são conceitualmente idênticos e portanto um exemplo perfeito de empilhamento vertical.

Vejamos como podemos fazer isso com o Pandas. Vamos artificalmente dividir as invoices por anos.

Nós usamos .copy() para garantir que os DataFrames resultantes serão uma cópia dos dados e não apenas uma referência ao DataFrame original.

Vamos validar se a divisão funcionou.

Agora para concatenar num único DataFrame, vamos chamar pd.concat(<LISTA DE DATAFRAMES>), dessa forma:

pd.concat leva ainda alguns parâmetros adicionais ao lado da lista de DataFrames para as quais você chamou concat:

  • axis: 0 para vertical, 1 para horizontal. O padrão é 0
  • join: ‘inner’ para a interseção, ‘outer’ para a união dos índices do eixo não-concatenado. Quando usamos axis=0 e join=’inner’ nós consideramos apenas as colunas em interseção. Quando usando axis=1 e join=’inner’ consideramos a interseção de índices. No caso de outer, colunas e índices fora de interseção serão preenchidos com valores nan. Para join, o padrão é outer
  • ignore_index: True para ignorar índices pre-existentes e ao invés usar rótulos de 0 a n-1 para o DataFrame resultante. ignore_index tem padrão False
  • keys: Se fornecermos uma lista (quantidade deve ser igual ao número de DataFrames) um índice hierárquico será construído. keys tem padrão None. Use keys para adicionar a fonte dos dados. Melhor usado em combinação com names.
  • names: Caso você tenha fornecido keys, os nomes serão usados para rotular o índice hierárquico resultante. names tem padrão None.

Concatenando – agrupamento horizontal

Um caso de uso para o agrupamento horizontal é quando você tem múltiplas séries temporais com índices que se sobrepõe porém que não são idênticos. Nesse caso, você não iria querer acabar com um DataFrame com milhares de colunas, mas sim com um DataFrame com milhares de linhas.

Rodar o script Python abaixo vai produzir o seguinte resultado:

Nota sobre Append:

Você deve ter visto o uso de append com os mesmo fim de usar concat. Não aconselho usar append, pois append é apenas um caso especial de concat e não dá nenhum benefício a mais sobre a concatenação.

União – “merging”

União ou “Merging”, ao contrário de concatenar DataFrames, nos permite combinar dois DatFrames numa forma mais tradicional baseada em query de SQL. Quando unindo DataFrames, na maioria das vezes você quer alguma informação de uma fonte e um pedaço de informação de outra fonte. Quando concatenando DataFrames, eles são similares em termos de estrutura e de conteúdo. E você pode combiná-los num único DataFrame.

Um exemplo simples que gosto de usar em processos seletivos é o seguinte:

Suponha que você tem duas tabelas. Uma tabela contem os nomes dos seus funcionários e um id de localização, a outra tabela contém ids de localização e o nome de uma cidade. Como você pode obter a lista de todos os funcionários e cidade onde eles trabalham?

Unir dois DataFrames no Pandas é feito com pd.merge. Vamos olhar a assinatura dessa função (assinatura é a lista de todos os possíveis parâmetros da função e também seu resultado).

Vamos analisar os parâmetros um por um:

  • left/right: o lado esquerdo e direito, respectivamente, do DataFrame que você quer fundir
  • how: ‘left’, ‘right’, ‘outer’, ‘inner’. how tem como padrão o ‘inner‘. Veja abaixo uma visão esquemática de como cada um funciona. Iremos discutir exemplos específicos mais a frente.
  • left_index/right_index: Se True, use o índice do DataFrame esquerdo ou direito para fazer a fusão. O padrão é False
  • on: Nomes das colunas para fazer a fusão. As colunas precisam existem em ambos lados do DataFrame. Caso on não seja passado e tanto left_index quanto right_index sejam False, a interseção das colunas em ambos os DataFrames serão considerados como on.
  • left_on/right_on: Nomes das colunas dos lados esquerdo e direito do DataFrame para fazer a fusão. Típico caso de uso: chaves que você está unindo tem rótulos diferentes no seu DataFrame. E.g., o que é location_id no DataFrame esquerdo pode ser _id no DataFrame direito. Nesse caso, você faria left_on=’location_id’, right_on=’_id’.
  • suffixes: Uma tupla de sufixos de strings para aplicar nas colunas em interseção. suffixes tem como padrão (‘_x’, ‘_y’). Eu gosto de usar (‘_base’, ‘_joined’).

União (merging) – Carregar e inspecionar novos dados

Já vimos a teoria, vamos agora ver alguns exemplos. Para fazer isso, precisamos de alguns dados adicionais para fazer a união.

Carregar dados:

Inspecionar dados:

Vemos logo que Order ID, Company ID a Date aparecem em múltiplos DataFrames e são portanto bons candidatos para a união.

União (Merging) – Como

Vamos unir ‘invoices’ com ‘order_data’ e testar os parâmetros um pouco.

  • Nenhum parâmetro: todos os parâmetros usam os seus padrões. A união será inner (equivalent a how=’inner’). A união será feita com todas as colunas em comum, ou seja, ‘Date’, ‘Order Id’, e ‘Company Id’ (equivalente a on=[‘Date’, ‘Order Id’, ‘Company Id’]. Sufixos não são relevantes pois as colunas em comum serão usadas na união, então não haverão colunas duplicadas remanescentes. Rode pd.merge(order_data, invoices):
  • how=’left’: novamente, a união é baseada nas colunas em comum. Sufixos não são relevantes pois as colunas em comum serão usadas na união, então não haverão colunas duplicadas remanescentes. Contudo, dessa vez, vamos unir how=’left’ o que significa que pegamos todas as linhas do frame esquerdo e somente adicionamos dados do frame direito onde os encontramos. Rode pd.merge(order_data, invoices, how=’left’):
  • how=’right’: novamente, a união é baseada nas colunas em comum. Sufixos não são relevantes pois as colunas em comum serão usadas na união, então não haverão colunas duplicadas remanescentes. Contudo, dessa vez, vamos unir how=’right’ o que significa que pegamos todas as linhas do frame direito e somente adicionamos dados do frame esquerdo onde os encontramos. Essa caso é equivalente a ‘inner’ no nosso exemplo pois toda linha no DataFrame esquerdo tem uma linha correspondente no DataFrame direito. Rode pd.merge(order_data, invoices, how=’right’):
  • how=’outer’: novamente, a união é baseada nas colunas em comum. Sufixos não são relevantes pois as colunas em comum serão usadas na união, então não haverão colunas duplicadas remanescentes. Contudo, dessa vez, vamos unir com how=’outer’. Pegamos todas as linhas tanto da esquerda quanto da direita do DataFrame e adicionamos os dados correspondentes encontrados no outro DataFrame. Esse caso é equivalente a ‘left’ no nosso exemplo pois todas as linhas no DataFrame esquerdo tem uma linha correspondente no DataFrame direito. Rode pd.merg(order_data, invoices, how=’outer’)

União – sufixos/on

Se fornecermos explicitamente um parâmetro on, isso irá sobrepor o comportamento padrão e tentará encontrar a coluna dada em ambos DataFrames. As colunas duplicadas remanescentes que não estiverem sendo usadas na união ganharão sufixo.

No exemplo a seguir, apenas unimos em ‘Order Id’. contudo, já que as colunas ‘Date’ e ‘Company Id’ também estão presentes em ambos DataFrames, elas terão sufixo para indicar qual o seu DataFrame de origem, como indicado no exemplo abaixo.

Rode: pd.merge(order_data, invoices, on=’Order Id’)

Também podemos especificar sufixos customizados, dessa forma:
pd.merge(order_data,invoices,on=’Order Id’,suffixes=(‘_base’,’_join’))

União – left_on/right_on

Você geralmente usaria os parâmetros left_on e right_on quando as colunas são nomeadas diferentemente em dois DataFrames.

Nota sobre join:
Você deve ter visto o uso de join ter o mesmo propósito de merge. join por padrão une os índices de ambos os DataFrames. Eu não aconselho o uso de join pois se trata apenas de um caso especial de merge e não traz nenhum benefício adicional.

Map

Para finalizar o capítulo sobre combinação de DataFrames, podemos falar brevemente sobre map. map pode ser chamado numa coluna de Dataframe ou no índice, assim:

O argumento (no nosso caso lookup) para map sempre tem que ser uma série ou um dicionário. Embora não sejam similares, uma série Pandas e um dicionário tem muitas funcionalidades em comum e podem ser usadas de modo intercambiável. Assim como num dicionário, você pode passar por uma série chamando for k, v in series.item():

Sumário da combinação de DataFrames

  • Use pd.concat para empilhar múltiplos DataFrames em cima ou ao lado de cada um. O padrão é vertical. Para sobrescrever o padrão use axis=1. Por padrão, o empilhamento se dar por sobre todas as colunas e índices. Para limitar às colunas e índices comuns, use join=’inner’.
  • Não use pd.DataFrame.append pois é apenas caso um especial e limitado de pd.concat, e apenas deixará seu código menos padronizados e coerente. Use pd.concat ao invés.
  • Use pd.merge para combinar informações de dois DataFrames. Merge tem como padrão um inner join e irá inferir quais colunas combinar a partir do subconjunto de colunas comuns nos DataFrames.
  • Não use pd.DataFrame.join pois é apenas caso um especial e limitado de pd.merge, e apenas deixará seu código menos padronizados e coerente. Use pd.merge ao invés.
  • Use pd.series.map como uma espécie de funcionalidade de busca para encontrar o valor de um índice ou chave específico de uma série ou dicionário.

Reformatando DataFrames (melt, pivot, transpose, stack, unstack)

Transpose

Transpor um DataFrame significa trocar o índice pela coluna. Em outras palavras, você rotaciona o DataFrame ao redor da origem. Transpor não muda o conteúdo do DataFrame. Apenas muda a orientação do DataFrame. Vamos visualizar isso através de um exemplo:

Um DataFrame é transposto de forma simples, basta chamar o DataFrame por .T (eg, invoices.T).

Melt

Melt transforma um DataFrame de um formato ‘wide’ para um formato ‘long’. Melt dá flexibilidade em como a transformação deveria ocorrer. Em outras palavras, melt permite pegar colunas e transformá-las em linhas enquanto deixa outras colunas intactas. Vou explicar melhor o melt com um exemplo. Vamos criar alguns dados amostrais:

Sem dúvida este é um exemplo restrito porém que ilustra bem a questão. Nós tornamos ‘Type of Meal’ em colunas e designamos os preços nas linhas correspondentes. Agora para reverter isso para uma versão onde ‘Type of Meal’ é uma coluna e o valor é o preço, podemos usar pd.melt dessa forma:

resultando em:

Melt é útil para transforma um DataFrame num format onde uma ou mais colunas são variáveis de identificação (id_vars), enquanto outras colunas, consideradas variáveis de medição (value_vars), são movidas para o eixo das linhas, deixando apenas 2 colunas não-identificadoras. Para cada coluna que usamos melt (value_vars), a linha correspondente é duplicada para acomodar a união dos dados numa única coluna e extender nosso DataFrame. Depois do melt, ficamos com um número três vezes maior de linhas do que antes (porque usamos 3 value_vars e portanto triplicamos cada linha).

Groupby

Discutimos isso em outro artigo, mas uma revisão rápida faz sentido aqui, especialmente à luz do stacking e unstacking, sobre o qual falaremos mais tarde. Se transpose e melt deixam o conteúdo do DataFrame intacto e “somente” rearranjam a aparência, groupby e os métodos seguintes irão agregar dados de uma forma ou de outra. Groupby irá resultar num DataFrame agregado com um novo índice (o valor das colunas pelas quais você está agrupando). Se você estiver agrupando por mais de um valor, o DataFrame resultante terá mais de um índice (multi-índices).

Começando com a versão 0.25.1 do Pandas, há também a agregação, que faz o groupby mais legível.

O resultado é o mesmo que o anterior. Contudo, a coluna é renomeada durante o processo.

Pivot

Pandas também incorpora a funcionalidade de pivot table,

O resultado

Podemos também especificar

Stack/Unstack

Stack e unstack são úteis quando rearranjando suas colunas e índices. Unstack por padrão será chamado no nível mais extremo do índice, como pode ser visto no exemplo que segue, onde chamar unstack() transforma o índice Heroes Adjustment em duas colunas.

Podemos usar unstack num nível específico do índice, como no exemplo a seguir, onde usamos unstack na coluna Type of Meal.

Stack, por outro lado, faz o oposto. Stack transforma colunas em linhas, mas também estende o índice no processo (ao contrário de melt). Vamos ver um exemplo.

Precisamos primeiro arrumar alguns dados onde stack seria útil. Executar o seguinte código resulta num DataFrame multi-índice e multi-nível.

Se quisermos usar stack nesse DataFrame, chamamos stack_test.stack() e obtemos:

Aqui stack usou o nível mais extremo das nossas colunas multi-nível e as transformou no índice. Agora temos apenas colunas uni-nível. Alternativamente, podemos chamar também stack com level=0 e obter o seguinte resultado.

Resumo

Nesse artigo, você aprendeu como se tornar um ninja em Pandas. Você aprendeu como converter dados em certos tipos e entre tipos. Você aprendeu como usar métodos únicos para esses tipos para acessar funcionalidades que de outro modo precisariam de muitas linhas de código. Você aprendeu como combinar diferentes DataFrames empilhando uns em cima dos outros, ou extraindo informação dos DataFrames e os combinando em algo mais útil. Você aprendeu como botar o seu DataFrame de ponta cabeça. Você aprendeu a rotacionar no seu ponto de origem, mover colunas em linhas, agregar dados através de pivot ou groupby e então usar stack e unstack nos resultados.

Bom trabalho, e obrigado pela leitura!

Veja também: