Post

Resumo do curso 'Pandas' do Kaggle (1) - Lições 1–3

Resumo prático do uso do Pandas para limpeza e manipulação de dados. Sintetiza o curso aberto 'Pandas' do Kaggle, com complementos quando necessário. Este post cobre as Lições 1–3.

Resumo do curso 'Pandas' do Kaggle (1) - Lições 1–3

Aqui organizo o que estudei por meio do curso Pandas do Kaggle.
Como o conteúdo é extenso, dividi em 2 partes.

Certificado de Conclusão

Lesson 1. Creating, Reading and Writing

Importando o pandas

1
import pandas as pd

No pandas há dois objetos centrais: DataFrame e Series.

DataFrame

DataFrame pode ser visto como uma tabela ou uma matriz. É composto por uma grade de entradas independentes; cada entrada possui um valor, correspondendo a uma linha (ou registro) e a uma coluna.

1
pd.DataFrame({'Yes': [50, 21], 'No': [131, 2]})
 YesNo
050131
1212

As entradas de um DataFrame não precisam ser numéricas; abaixo um exemplo com strings (avaliações de usuários):

1
pd.DataFrame({'Bob': ['I liked it.', 'It was awful.'], 'Sue': ['Pretty good.', 'Bland.']})
 BobSue
0I liked it.Pretty good.
1It was awful.Bland.

Para criar um DataFrame, usa-se o construtor pd.DataFrame() e a sintaxe de dicionário em Python: chaves são os nomes das colunas, valores são listas com os itens a registrar. Esse é o método padrão para declarar um novo DataFrame.

Ao declarar um DataFrame, nomeamos as colunas; já as linhas, se não forem especificadas, recebem rótulos inteiros 0, 1, 2, … . Se necessário, podemos rotular as linhas manualmente. A lista de rótulos de linha é o índice, e pode ser definido pelo parâmetro index do construtor.

1
2
3
pd.DataFrame({'Bob': ['I liked it.', 'It was awful.'], 
              'Sue': ['Pretty good.', 'Bland.']},
             index=['Product A', 'Product B'])
 BobSue
Product AI liked it.Pretty good.
Product BIt was awful.Bland.

Series

Series é uma sequência de valores, ou um vetor.

1
pd.Series([1, 2, 3, 4, 5])

Uma Series é essencialmente igual a uma única coluna de um DataFrame. Assim, também pode ter índice e, em vez de “nome de coluna”, tem apenas um “nome” (name).

1
pd.Series([30, 35, 40], index=['12015 Sales', '12016 Sales', '12017 Sales'], name='Product A')
1
2
3
4
12015 Sales    30
12016 Sales    35
12017 Sales    40
Name: Product A, dtype: int64

Series e DataFrame estão intimamente relacionados. É útil pensar um DataFrame como um conjunto de várias Series.

Lendo arquivos de dados

Na maioria das vezes, em vez de digitar tudo do zero, importamos dados já existentes. Dados podem estar em vários formatos; o mais básico é o CSV. Um arquivo CSV costuma ser assim:

Product A,Product B,Product C,
30,21,9,
35,34,1,
41,11,11

Ou seja, um CSV é uma tabela cujos valores são separados por vírgulas. Daí o nome “Comma-Separated Values (CSV)”.

Para carregar um CSV em um DataFrame, usa-se pd.read_csv().

1
product_reviews = pd.read_csv("../input/product-reviews/example-data.csv")

A propriedade shape mostra o formato do DataFrame.

1
product_reviews.shape
1
(129971, 14)

A saída acima indica 129971 registros e 14 colunas.

Com o método head(), vemos as cinco primeiras linhas.

1
product_reviews.head()

A função pd.read_csv() tem mais de 30 parâmetros. Por exemplo, se o CSV já tiver uma coluna de índice, podemos definir index_col para usá-la em vez do índice automático do pandas.

1
product_reviews = pd.read_csv("../input/product-reviews/example-data.csv", index_col=0)

Escrevendo arquivos de dados

Com to_csv() podemos exportar um DataFrame para CSV. Exemplo:

1
product_reviews.to_csv("../output/product-reviews/example-data.csv")

Lesson 2. Indexing, Selecting & Assigning

Selecionar valores específicos em um DataFrame ou Series é etapa comum em praticamente toda tarefa de processamento de dados; portanto, é essencial aprender a fazer seleções rápidas e eficientes.

Acessores nativos do Python

Objetos nativos do Python oferecem bons mecanismos de indexação, e o pandas os suporta da mesma forma.

Atributos de objeto

Em Python, acessamos uma propriedade de um objeto pelo nome do atributo. Por exemplo, se example_obj tem o atributo title, chamamos example_obj.title. Com colunas de um DataFrame, o acesso funciona de modo análogo.

1
reviews.country
1
2
3
4
5
6
0            Italy
1         Portugal
            ...   
129969      France
129970      France
Name: country, Length: 129971, dtype: object

Indexação de dicionário

Para dicionários, usamos o operador de indexação ([]) para acessar valores. Em DataFrames, também funciona para colunas.

1
reviews['country']
1
2
3
4
5
6
0            Italy
1         Portugal
            ...   
129969      France
129970      France
Name: country, Length: 129971, dtype: object

Ambas as formas são válidas; porém, a indexação estilo dicionário tem a vantagem de lidar com nomes de coluna que incluem caracteres reservados, como espaços (por exemplo, reviews['country providence'] funciona, enquanto reviews.country providence não).

Dentro da Series selecionada, podemos novamente usar [] para ler um valor específico.

1
reviews['country'][0]
1
'Italy'

Acessores próprios do pandas

Além dos modos acima, o pandas oferece acessores próprios: loc e iloc.

Seleção baseada em índice

Com iloc, fazemos seleção baseada em índice (index-based selection): escolhemos pela posição numérica.

Por exemplo, para pegar a primeira linha do DataFrame:

1
reviews.iloc[0]
1
2
3
4
5
6
country                                                    Italy
description    Aromas include tropical fruit, broom, brimston...
                                     ...                        
variety                                              White Blend
winery                                                   Nicosia
Name: 0, Length: 13, dtype: object

Diferente do acesso nativo (coluna depois linha), iloc seleciona primeiro linhas e depois colunas. Para pegar a primeira coluna:

1
reviews.iloc[:, 0]
1
2
3
4
5
6
0            Italy
1         Portugal
            ...   
129969      France
129970      France
Name: country, Length: 129971, dtype: object

Acima, : seleciona todas as linhas, e depois escolhemos a primeira coluna. Se quisermos as linhas 2ª (1) e 3ª (2) da primeira coluna:

1
reviews.iloc[1:3, 0]
1
2
3
1    Portugal
2          US
Name: country, dtype: object

Ou passando uma lista:

1
reviews.iloc[[1, 2], 0]
1
2
3
1    Portugal
2          US
Name: country, dtype: object

Também é possível usar índices negativos para selecionar a partir do fim. Exemplo: as últimas 5 linhas.

1
reviews.iloc[-5:]

Seleção baseada em rótulos

Outra opção é loc, para seleção baseada em rótulos (label-based selection). Aqui escolhemos pelos valores do índice.

Por exemplo, o elemento na linha de índice 0 e coluna ‘country’:

1
reviews.loc[0, 'country']
1
'Italy'

iloc ignora os valores do índice e trata o conjunto como uma grande matriz, acessando por posição. Já loc utiliza a informação do índice; como geralmente há significado nos rótulos, loc costuma ser mais intuitivo.

Diferenças de fatiamento entre iloc e loc

iloc segue o padrão do Python: 0:10 significa intervalo semiaberto, 0 até 10 não-inclusivo, isto é, 0,...,9.

loc, por sua vez, trata intervalos como fechados: 0:10 significa 0 até 10 inclusivo, ou 0,...,10.

O motivo: loc aceita, além de inteiros, rótulos de qualquer tipo padrão. Suponha um índice em ordem alfabética com valores Apples, ..., Potatoes, .... Se quisermos os itens de ‘Apples’ até ‘Potatoes’, é mais natural escrever df.loc['Apples':'Potatoes'] do que ajustar para “‘Apples’ até antes de ‘Potatoet’” (df.loc['Apples':'Potatoet']). Para rótulos não inteiros, a forma inclusiva costuma ser mais intuitiva; por isso loc a adota.

Fora isso, o funcionamento é análogo.

Pessoalmente, em datasets com índice inteiro crescente e quando preciso fatias com :, prefiro iloc para evitar confusões com a diferença de intervalos; nos demais casos, uso o mais intuitivo loc.

Manipulando o índice

Também podemos ajustar o índice conforme necessário. Com set_index(), por exemplo, definimos alguma coluna como novo índice:

1
reviews.set_index("title")

Seleção condicional

Até aqui usamos propriedades estruturais do DataFrame para selecionar e transformar dados. Podemos ir além e filtrar por condições arbitrárias.

Exemplo: num DataFrame com informações de vinhos, selecionar somente os italianos com nota a partir de 90.

1
reviews.country == 'Italy'

A expressão acima retorna uma Series booleana, com True/False.

1
2
3
4
5
6
0          True
1         False
          ...  
129969    False
129970    False
Name: country, Length: 129971, dtype: bool

loc é baseado em rótulos, mas também aceita arrays booleanos ou Series booleanas alinhadas. Assim, podemos selecionar só os vinhos italianos:

1
reviews.loc[reviews.country == 'Italy']

Podemos combinar múltiplas condições com & e |. Para italianos e com nota ≥ 90:

1
reviews.loc[(reviews.country == 'Italy') & (reviews.points >= 90)]

Para italianos ou com nota ≥ 90:

1
reviews.loc[(reviews.country == 'Italy') | (reviews.points >= 90)]

O pandas também fornece seletores condicionais úteis como isin e isnull/notnull.

isin verifica se o valor “está em” uma lista e retorna uma máscara booleana. Exemplo: vinhos da Itália ou da França.

1
reviews.loc[reviews.country.isin(['Italy', 'France'])]

isna/notna filtram valores ausentes (NaN). Exemplo: selecionar somente vinhos com preço informado.

1
reviews.loc[reviews.price.notna()]

Nota: embora não conste no curso do Kaggle, iloc também aceita arrays booleanos. Diferentemente de loc, porém, suporta apenas arrays (não Series), o que limita usos como os acima.

Atribuição de dados

Podemos criar ou sobrescrever dados em um DataFrame.

1
2
reviews['critic'] = 'everyone'
reviews['critic']
1
2
3
4
5
6
0         everyone
1         everyone
            ...   
129969    everyone
129970    everyone
Name: critic, Length: 129971, dtype: object
1
2
reviews['index_backwards'] = range(len(reviews), 0, -1)
reviews['index_backwards']
1
2
3
4
5
6
0         129971
1         129970
           ...  
129969         2
129970         1
Name: index_backwards, Length: 129971, dtype: int64

Lesson 3. Summary Functions and Maps

Visão geral dos dados

O método describe() fornece um resumo de alto nível de uma coluna.

1
reviews.points.describe()
1
2
3
4
5
6
count    129971.000000
mean         88.447138
             ...      
75%          91.000000
max         100.000000
Name: points, Length: 8, dtype: float64

A saída depende do tipo dos dados. Para strings, por exemplo:

1
reviews.taster_name.describe()
1
2
3
4
5
count         103727
unique            19
top       Roger Voss
freq           25514
Name: taster_name, dtype: object

Também podemos obter estatísticas específicas.

1
reviews.points.mean()
1
88.44713820775404
1
reviews.taster_name.unique()
1
2
3
4
5
6
7
array(['Kerin O’Keefe', 'Roger Voss', 'Paul Gregutt',
       'Alexander Peartree', 'Michael Schachner', 'Anna Lee C. Iijima',
       'Virginie Boone', 'Matt Kettmann', nan, 'Sean P. Sullivan',
       'Jim Gordon', 'Joe Czerwinski', 'Anne Krebiehl\xa0MW',
       'Lauren Buzzeo', 'Mike DeSimone', 'Jeff Jenssen',
       'Susan Kostrzewa', 'Carrie Dykes', 'Fiona Adams',
       'Christina Pickard'], dtype=object)

Se quisermos as contagens de cada valor distinto, usamos value_counts().

1
reviews.taster_name.value_counts()
1
2
3
4
5
6
Roger Voss           25514
Michael Schachner    15134
                     ...  
Fiona Adams             27
Christina Pickard        6
Name: taster_name, Length: 19, dtype: int64

Mapeamentos (Maps)

Em matemática, um mapeamento (map) é uma função que envia um conjunto em outro. Em ciência de dados, frequentemente transformamos dados para outra representação; usamos mapeamentos para isso, portanto são muito importantes.

Dois métodos são especialmente comuns.

Series.map() recebe uma função que transforma um único valor em outro e a aplica a todos os valores da Series, retornando uma nova Series. Por exemplo, para subtrair a média das notas e obter desvios:

1
2
review_points_mean = reviews.points.mean()
reviews.points.map(lambda p: p - review_points_mean)
1
2
3
4
5
6
0        -1.447138
1        -1.447138
            ...   
129969    1.552862
129970    1.552862
Name: points, Length: 129971, dtype: float64

DataFrame.apply() aplica uma função customizada a cada linha, transformando o DataFrame inteiro.

1
2
3
4
5
def remean_points(row):
    row.points = row.points - review_points_mean
    return row

reviews.apply(remean_points, axis='columns')

Com axis='index', aplicamos por colunas em vez de por linhas.

Series.map() e DataFrame.apply() retornam novos objetos transformados; os dados originais permanecem inalterados.

MétodoSeries.map()DataFrame.apply()
AlvoSeriesDataFrame
Unidade de aplicaçãoAplica por valor
(se virmos a Series como um vetor coluna, aplica por linha)
Por padrão, aplica por linha
com opção para aplicar por coluna

Observação: também existem Series.apply() e DataFrame.map().

  • Series.apply():
    • by_row='compat' (padrão): funciona como Series.map()
    • by_row=False: passa a Series inteira de uma vez para a função (análogo a DataFrame.apply() com axis='index')
  • DataFrame.map(): aplica a função a cada valor do DataFrame (semelhante a Series.map(), mas no nível do DataFrame)

Na verdade, o pandas já otimiza muitos mapeamentos comuns. O exemplo anterior pode ser escrito de forma bem mais simples, e o pandas entende a intenção:

1
2
review_points_mean = reviews.points.mean()
reviews.points - review_points_mean
1
2
3
4
5
6
0        -1.447138
1        -1.447138
            ...   
129969    1.552862
129970    1.552862
Name: points, Length: 129971, dtype: float64

Além disso, o pandas suporta operações entre Series de mesmo tamanho. No exemplo dos vinhos, podemos concatenar país e região diretamente como strings:

1
reviews.country + " - " + reviews.region_1
1
2
3
4
5
6
0            Italy - Etna
1                     NaN
               ...       
129969    France - Alsace
129970    France - Alsace
Length: 129971, dtype: object

Essas operações são aceleradas internamente e costumam ser mais rápidas que map()/apply(). O pandas oferece esse comportamento para todos os operadores padrão do Python (>, <, ==, etc.). Ainda assim, map() e apply() são mais flexíveis e permitem tarefas mais complexas, por isso vale a pena conhecê-los.

Esta postagem está licenciada sob CC BY-NC 4.0 pelo autor.