Resumo do curso 'Pandas' do Kaggle (2) - Lições 4–6
Resumo do uso do Pandas para limpar e transformar dados. Síntese do curso 'Pandas' do Kaggle, com complementos. Parte 2: Lições 4–6 — groupby, ordenação, dtypes/NaN e joins.
Aqui organizo o que estudei por meio do curso Pandas do Kaggle.
Como o conteúdo é extenso, dividi em 2 partes.
- Parte 1: Lições 1–3
- Parte 2: Lições 4–6 (este post)
Lesson 4. Grouping and Sorting
Às vezes, é preciso agrupar dados e aplicar operações por grupo ou ordená-los por algum critério.
Análise por grupo
Com o método groupby(), podemos agrupar registros cujos valores em determinada coluna sejam iguais e, em seguida, obter visões gerais ou aplicar transformações por grupo.
Vimos antes o método value_counts(); o mesmo comportamento pode ser implementado com groupby() assim:
1
reviews.groupby('taster_name').size()
- Agrupa o DataFrame
reviewspor valores iguais na colunataster_name - Retorna uma Series com o tamanho (número de registros) de cada grupo
Ou então:
1
reviews.groupby('taster_name').taster_name.count()
- Agrupa o DataFrame
reviewspor valores iguais na colunataster_name - Em cada grupo, seleciona a coluna
taster_name - Retorna uma Series com a contagem de valores não nulos
Ou seja, o método value_counts() é um atalho para a lógica acima. Além de count(), podemos usar qualquer função de resumo. Por exemplo, para obter o menor preço por nota:
1
reviews.groupby('points').price.min()
1
2
3
4
5
6
7
points
80 5.0
81 5.0
...
99 44.0
100 80.0
Name: price, Length: 21, dtype: float64
- Agrupa o DataFrame
reviewspor valores iguais na colunapoints - Em cada grupo, seleciona a coluna
price - Retorna uma Series com o valor mínimo em cada grupo
Também é possível agrupar por mais de uma coluna. Para selecionar, por país e estado/província, apenas o vinho com maior nota:
1
reviews.groupby(['country', 'province']).apply(lambda df: df.loc[df.points.idxmax()])
Outro método útil do objeto DataFrameGroupBy é agg(), que permite aplicar várias funções ao mesmo tempo em cada grupo.
Como argumento, podem ser passados:
- uma função
- uma string com o nome da função
- uma lista de funções ou de nomes de função
- um dicionário cujo rótulo do eixo é a chave e a(s) função(ões) a aplicar nesse eixo é o valor
A função deve:
- aceitar um DataFrame como entrada ou
- ser utilizável como argumento de
DataFrame.apply(), visto anteriormenteEsta explicação não consta no curso original do Kaggle; foi complementada a partir da documentação oficial do pandas.
Exemplo: estatísticas de preço por país.
1
reviews.groupby(['country']).price.agg([len, min, max])
Aqui,
lené a função nativa do Pythonlen(). No exemplo, foi usada para retornar a contagem de registros de preço (price) por agrupamento (country), incluindo valores ausentes. Comolenaceita DataFrames/Series, seu uso é válido nesse contexto.Já o método
count()do pandas retorna a contagem de valores não nulos, o que difere do comportamento acima.Esta nota também não está no curso original; foi complementada com base na documentação oficial de Python e pandas.
Índice múltiplo
Ao usar groupby() em transformações/análises, muitas vezes obtemos DataFrames com índice em múltiplos níveis (MultiIndex), em vez de rótulos simples.
1
2
countries_reviewed = reviews.groupby(['country', 'province']).description.agg([len])
countries_reviewed
| len | ||
|---|---|---|
| Country | province | |
| Argentina | Mendoza Province | 3264 |
| Other | 536 | |
| ... | ... | ... |
| Uruguay | San Jose | 3 |
| Uruguay | 24 |
1
2
mi = countries_reviewed.index
type(mi)
1
pandas.core.indexes.multi.MultiIndex
Um MultiIndex tem métodos próprios para lidar com hierarquia, inexistentes em índices simples. Veja a seção MultiIndex / advanced indexing no User Guide do pandas para exemplos e diretrizes detalhadas.
O método mais usado no dia a dia é reset_index(), para voltar a um índice “plano”.
1
countries_reviewed.reset_index()
| country | province | len | |
|---|---|---|---|
| 0 | Argentina | Mendoza Province | 3264 |
| 1 | Argentina | Other | 536 |
| … | … | … | … |
| 423 | Uruguay | San Jose | 3 |
| 424 | Uruguay | Uruguay | 24 |
Ordenação
Observando countries_reviewed, percebemos que o resultado do agrupamento vem ordenado pelo índice. Ou seja, a ordem das linhas do groupby é determinada pelos valores do índice, não pelo conteúdo dos dados.
Se necessário, podemos ordenar manualmente de outra forma. O método sort_values() é conveniente. Por exemplo, para ordenar país e estado pelo número de registros (“len”) em ordem crescente:
1
2
countries_reviewed = countries_reviewed.reset_index()
countries_reviewed.sort_values(by='len')
| country | province | len | |
|---|---|---|---|
| 179 | Greece | Muscat of Kefallonian | 1 |
| 192 | Greece | Sterea Ellada | 1 |
| … | … | … | … |
| 415 | US | Washington | 8639 |
| 392 | US | California | 36247 |
sort_values() usa ordem crescente por padrão, mas podemos inverter com ascending=False:
1
countries_reviewed.sort_values(by='len', ascending=False)
| country | province | len | |
|---|---|---|---|
| 392 | US | California | 36247 |
| 415 | US | Washington | 8639 |
| … | … | … | … |
| 63 | Chile | Coelemu | 1 |
| 149 | Greece | Beotia | 1 |
Para ordenar pelo índice, use sort_index(). Ele tem os mesmos parâmetros e ordem padrão de classificação que sort_values().
1
countries_reviewed.sort_index()
| country | province | len | |
|---|---|---|---|
| 0 | Argentina | Mendoza Province | 3264 |
| 1 | Argentina | Other | 536 |
| … | … | … | … |
| 423 | Uruguay | San Jose | 3 |
| 424 | Uruguay | Uruguay | 24 |
Por fim, é possível ordenar de uma só vez por mais de uma coluna:
1
countries_reviewed.sort_values(by=['country', 'len'])
Lesson 5. Data Types and Missing Values
Na prática, os dados raramente chegam perfeitamente limpos. Muitas vezes é preciso converter tipos, e lidar com valores ausentes no meio do caminho é a parte mais trabalhosa do pipeline de análise.
Tipos de dados
O tipo de uma coluna (ou de uma Series) em um DataFrame é o dtype. A propriedade dtype permite inspecionar o tipo de uma coluna. Exemplo: verificar o dtype da coluna price do DataFrame reviews.
1
reviews.price.dtype
1
dtype('float64')
Também podemos inspecionar os tipos de todas as colunas de uma vez, via dtypes:
1
reviews.dtypes
1
2
3
4
5
6
country object
description object
...
variety object
winery object
Length: 13, dtype: object
O dtype indica como o pandas armazena internamente os dados. float64 é ponto flutuante de 64 bits; int64 é inteiro de 64 bits.
Uma particularidade: colunas de strings não têm um tipo próprio, sendo tratadas como objetos (object).
Com astype(), podemos converter o tipo de uma coluna. Por exemplo, converter a coluna points (antes int64) para float64:
1
reviews.points.astype('float64')
1
2
3
4
5
6
0 87.0
1 87.0
...
129969 90.0
129970 90.0
Name: points, Length: 129971, dtype: float64
O índice de um DataFrame/Series também tem tipo:
1
reviews.index.dtype
1
dtype('int64')
O pandas também oferece suporte a tipos externos, como dados categóricos e séries temporais.
Valores ausentes
Entradas vazias são representadas por NaN (de “Not a Number”). Por razões técnicas, NaN tem sempre tipo float64.
O pandas fornece funções específicas para ausência de dados. Já vimos algo semelhante: existem as funções independentes pd.isna e pd.notna. Elas retornam um booleano (ou array booleano) indicando se a entrada é ausente (ou não) e podem ser usadas assim:
1
reviews[pd.isna(reviews.country)]
Geralmente, verificamos se há ausências e, caso haja, definimos uma estratégia para preenchê-las. Com fillna(), substituímos NaN por um valor conveniente. Exemplo: trocar todos os NaN de reviews.region_2 por "Unknown":
1
reviews.region_2.fillna("Unknown")
Outra estratégia é copiar o valor válido mais próximo antes (forward fill) ou depois (backward fill). Use ffill() e bfill().
Antigamente, podia-se usar
fillna()com o parâmetromethod='ffill'/'bfill'. Desde o pandas 2.1.0, essa forma foi depreciada; prefiraffill()/bfill()conforme o caso.
Em outras situações, mesmo não sendo ausências, pode ser necessário substituir valores em massa. No curso do Kaggle, o exemplo é a troca do handle do Twitter de um revisor. Um exemplo mais próximo da realidade brasileira: imagine que o norte da província de Gyeonggi, na Coreia do Sul, foi separado para formar a nova unidade administrativa Gyeonggibuk-do; existe um dataset com esse nome, mas alguém decide renomeá-la para Pyeonghwanuri Special Self-Governing Province e consegue emplacar essa ideia. É um cenário hipotético, mas assusta pensar que algo parecido quase aconteceu. Para refletir essa mudança no dataset, teríamos que substituir "Gyeonggibuk-do" por "Pyeonghwanuri State" ou "Pyeonghwanuri Special Self-Governing Province". Uma forma de fazer isso no pandas é com replace():
1
rok_2030_census.province.replace("Gyeonggibuk-do", "Pyeonghwanuri Special Self-Governing Province")
Com o código acima, trocamos eficientemente todas as ocorrências de "Gyeonggibuk-do" na coluna province do dataset rok_2030_census por “aquele nome comprido”. Reconforta saber que ninguém precisou realmente rodar um código desses.
Substituições de strings também são úteis na limpeza de ausências quando elas aparecem como textos como "Unknown", "Undisclosed", "Invalid", em vez de NaN. Em fluxos que digitalizam documentos antigos via OCR, isso é até mais comum.
Lesson 6. Renaming and Combining
Às vezes, precisamos renomear colunas ou o índice de um dataset. Também é comum combinar DataFrames e Series.
Renomeando
Com rename(), renomeamos colunas ou o índice. Embora aceite formatos variados, o mais prático costuma ser um dicionário Python. Exemplos: renomear a coluna points para score e, no índice, trocar 0, 1 por firstEntry, secondEntry:
1
reviews.rename(columns={'points': 'score'})
1
reviews.rename(index={0: 'firstEntry', 1: 'secondEntry'})
Renomear colunas é comum; renomear valores do índice, nem tanto. Para esse fim, geralmente é mais conveniente usar set_index(), como vimos antes.
As dimensões de linhas e colunas também têm a propriedade name. Com rename_axis(), podemos nomear esses eixos. Ex.: chamar o eixo do índice de wines e o das colunas de fields:
1
reviews.rename_axis("wines", axis='index').rename_axis("fields", axis='columns')
Combinando datasets
Com frequência, é preciso combinar DataFrames ou Series. O pandas oferece três funções principais, em ordem crescente de complexidade: concat(), join() e merge(). O curso do Kaggle observa que a maior parte do que se faz com merge() pode ser feita de modo mais simples com join(), então foca nos dois primeiros.
concat() é a mais simples: concatena DataFrames/Series ao longo de um eixo. É útil quando todos têm as mesmas colunas. Por padrão, concatena ao longo do eixo do índice; com axis=1 ou axis='columns', concatena por colunas.
1
2
3
4
5
6
7
8
>>> s1 = pd.Series(['a', 'b'])
>>> s2 = pd.Series(['c', 'd'])
>>> pd.concat([s1, s2])
0 a
1 b
0 c
1 d
dtype: object
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
>>> df1 = pd.DataFrame([['a', 1], ['b', 2]],
... columns=['letter', 'number'])
>>> df1
letter number
0 a 1
1 b 2
>>> df2 = pd.DataFrame([['c', 3], ['d', 4]],
... columns=['letter', 'number'])
>>> df2
letter number
0 c 3
1 d 4
>>> pd.concat([df1, df2])
letter number
0 a 1
1 b 2
0 c 3
1 d 4
>>> df4 = pd.DataFrame([['bird', 'polly'], ['monkey', 'george']],
... columns=['animal', 'name'])
>>> df4
animal name
0 bird polly
1 monkey george
>>> pd.concat([df1, df4], axis=1)
letter number animal name
0 a 1 bird polly
1 b 2 monkey george
De acordo com a documentação oficial do pandas, ao construir um DataFrame a partir de várias linhas, evite adicionar uma a uma dentro de um loop. Em vez disso, junte as linhas em uma lista e faça uma única chamada a
concat().
join() é um pouco mais complexo: ele “anexa” um DataFrame a outro, alinhando pelo índice. Se houver nomes de colunas em conflito, use os parâmetros lsuffix e rsuffix para definir sufixos distintos e evitar colisões.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> df = pd.DataFrame({'key': ['K0', 'K1', 'K2', 'K3', 'K4', 'K5'],
... 'A': ['A0', 'A1', 'A2', 'A3', 'A4', 'A5']})
>>> df
key A
0 K0 A0
1 K1 A1
2 K2 A2
3 K3 A3
4 K4 A4
5 K5 A5
>>> other = pd.DataFrame({'key': ['K0', 'K1', 'K2'],
... 'B': ['B0', 'B1', 'B2']})
>>> other
key B
0 K0 B0
1 K1 B1
2 K2 B2
>>> df.join(other, lsuffix='_caller', rsuffix='_other')
key_caller A key_other B
0 K0 A0 K0 B0
1 K1 A1 K1 B1
2 K2 A2 K2 B2
3 K3 A3 NaN NaN
4 K4 A4 NaN NaN
5 K5 A5 NaN NaN

