Manipulação de dados com pandas

Introdução

Pandas é uma biblioteca para leitura, tratamento e manipulação de dados em Python que possui funções muito similares a softwares empregados em planilhamento, tais como Microsoft Excel, LibreOffice Calc e Apple Numbers. Além de ser uma ferramenta de uso gratuito, ela possui inúmeras vantagens. Para saber mais sobre suas capacidades, veja página oficial da biblioteca.

Nesta parte de nosso curso, aprenderemos duas novas estruturas de dados que pandas introduz:

  • Series e

  • DataFrame.

Um DataFrame é uma estrutura de dados tabular com linhas e colunas rotuladas.

Peso

Altura

Idade

Gênero

Ana

55

162

20

feminino

João

80

178

19

masculino

Maria

62

164

21

feminino

Pedro

67

165

22

masculino

Túlio

73

171

20

masculino

As colunas do DataFrame são vetores unidimensionais do tipo Series, ao passo que as linhas são rotuladas por uma estrutura de dados especial chamada index. Os index no Pandas são listas personalizadas de rótulos que nos permitem realizar pesquisas rápidas e algumas operações importantes.

Para utilizarmos estas estruturas de dados, importaremos as bibliotecas numpy utilizando o placeholder usual np e pandas utilizando o placeholder usual pd.

import numpy as np
import pandas as pd

Series

As Series:

  • são vetores, ou seja, são arrays unidimensionais;

  • possuem um index para cada entrada (e são muito eficientes para operar com base neles);

  • podem conter qualquer um dos tipos de dados (int, str, float etc.).

Criando um objeto do tipo Series

O método padrão é utilizar a função Series da biblioteca pandas:

serie_exemplo = pd.Series(dados_de_interesse, index=indice_de_interesse)

No exemplo acima, dados_de_interesse pode ser:

  • um dicionário (objeto do tipo dict);

  • uma lista (objeto do tipo list);

  • um objeto array do numpy;

  • um escalar, tal como o número inteiro 1.

Criando Series a partir de dicionários

dicionario_exemplo = {'Ana':20, 'João': 19, 'Maria': 21, 'Pedro': 22, 'Túlio': 20}
pd.Series(dicionario_exemplo)
Ana      20
João     19
Maria    21
Pedro    22
Túlio    20
dtype: int64

Note que o index foi obtido a partir das “chaves” dos dicionários. Assim, no caso do exemplo, o index foi dado por “Ana”, “João”, “Maria”, “Pedro” e “Túlio”. A ordem do index foi dada pela ordem de entrada no dicionário.

Podemos fornecer um novo index ao dicionário já criado

pd.Series(dicionario_exemplo, index=['Maria', 'Maria', 'ana', 'Paula', 'Túlio', 'Pedro'])
Maria    21.0
Maria    21.0
ana       NaN
Paula     NaN
Túlio    20.0
Pedro    22.0
dtype: float64

Dados não encontrados são assinalados por um valor especial. O marcador padrão do pandas para dados faltantes é o NaN (not a number).

Criando Series a partir de listas

lista_exemplo = [1,2,3,4,5]
pd.Series(lista_exemplo)
0    1
1    2
2    3
3    4
4    5
dtype: int64

Se os index não forem fornecidos, o pandas atribuirá automaticamente os valores 0, 1, ..., N-1, onde N é o número de elementos da lista.

Criando Series a partir de arrays do numpy

array_exemplo = np.array([1,2,3,4,5])
pd.Series(array_exemplo)
0    1
1    2
2    3
3    4
4    5
dtype: int64

Fornecendo um index na criação da Series

O total de elementos do index deve ser igual ao tamanho do array. Caso contrário, um erro será retornado.

pd.Series(array_exemplo, index=['a','b','c','d','e','f'])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-9-f8e840b4247a> in <module>
----> 1 pd.Series(array_exemplo, index=['a','b','c','d','e','f'])

~/anaconda3/lib/python3.7/site-packages/pandas/core/series.py in __init__(self, data, index, dtype, name, copy, fastpath)
    290                     if len(index) != len(data):
    291                         raise ValueError(
--> 292                             f"Length of passed values is {len(data)}, "
    293                             f"index implies {len(index)}."
    294                         )

ValueError: Length of passed values is 5, index implies 6.
pd.Series(array_exemplo, index=['a','b','c','d','e'])
a    1
b    2
c    3
d    4
e    5
dtype: int64

Além disso, não é necessário que que os elementos no index sejam únicos.

pd.Series(array_exemplo, index=['a','a','b','b','c'])
a    1
a    2
b    3
b    4
c    5
dtype: int64

Um erro ocorrerá se uma operação que dependa da unicidade dos elementos no index for realizada, a exemplo do método reindex.

series_exemplo = pd.Series(array_exemplo, index=['a','a','b','b','c'])
series_exemplo.reindex(['b','a','c','d','e']) # 'a' e 'b' duplicados na origem
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-13-ee01c968fae1> in <module>
----> 1 series_exemplo.reindex(['b','a','c','d','e']) # 'a' e 'b' duplicados na origem

~/anaconda3/lib/python3.7/site-packages/pandas/core/series.py in reindex(self, index, **kwargs)
   4028     @Appender(generic.NDFrame.reindex.__doc__)
   4029     def reindex(self, index=None, **kwargs):
-> 4030         return super().reindex(index=index, **kwargs)
   4031 
   4032     def drop(

~/anaconda3/lib/python3.7/site-packages/pandas/core/generic.py in reindex(self, *args, **kwargs)
   4542         # perform the reindex on the axes
   4543         return self._reindex_axes(
-> 4544             axes, level, limit, tolerance, method, fill_value, copy
   4545         ).__finalize__(self)
   4546 

~/anaconda3/lib/python3.7/site-packages/pandas/core/generic.py in _reindex_axes(self, axes, level, limit, tolerance, method, fill_value, copy)
   4565                 fill_value=fill_value,
   4566                 copy=copy,
-> 4567                 allow_dups=False,
   4568             )
   4569 

~/anaconda3/lib/python3.7/site-packages/pandas/core/generic.py in _reindex_with_indexers(self, reindexers, fill_value, copy, allow_dups)
   4611                 fill_value=fill_value,
   4612                 allow_dups=allow_dups,
-> 4613                 copy=copy,
   4614             )
   4615 

~/anaconda3/lib/python3.7/site-packages/pandas/core/internals/managers.py in reindex_indexer(self, new_axis, indexer, axis, fill_value, allow_dups, copy)
   1249         # some axes don't allow reindexing with dups
   1250         if not allow_dups:
-> 1251             self.axes[axis]._can_reindex(indexer)
   1252 
   1253         if axis >= self.ndim:

~/anaconda3/lib/python3.7/site-packages/pandas/core/indexes/base.py in _can_reindex(self, indexer)
   3096         # trying to reindex on an axis with duplicates
   3097         if not self.is_unique and len(indexer):
-> 3098             raise ValueError("cannot reindex from a duplicate axis")
   3099 
   3100     def reindex(self, target, method=None, level=None, limit=None, tolerance=None):

ValueError: cannot reindex from a duplicate axis

Criando Series a partir de escalares

pd.Series(1, index=['a', 'b', 'c', 'd'])
a    1
b    1
c    1
d    1
dtype: int64

Neste caso, um índice deve ser fornecido!

Series comportam-se como arrays do numpy

Uma Series do pandas comporta-se como um array unidimensional do numpy. Pode ser utilizada como argumento para a maioria das funções do numpy. A diferença é que o index aparece.

Exemplo:

series_exemplo = pd.Series(array_exemplo, index=['a','b','c','d','e'])
series_exemplo[2]
3
series_exemplo[:2]
a    1
b    2
dtype: int64
np.log(series_exemplo)
a    0.000000
b    0.693147
c    1.098612
d    1.386294
e    1.609438
dtype: float64

Mais exemplos:

serie_1 = pd.Series([1,2,3,4,5])
serie_2 = pd.Series([4,5,6,7,8])
serie_1 + serie_2
0     5
1     7
2     9
3    11
4    13
dtype: int64
serie_1 * 2 - serie_2 * 3
0   -10
1   -11
2   -12
3   -13
4   -14
dtype: int64

Assim como arrays do numpy, as Series do pandas também possuem atributos dtype (data type).

series_exemplo.dtype
dtype('int64')

Se o interesse for utilizar os dados de uma Series do pandas como um array do numpy, basta utilizar o método to_numpy para convertê-la.

series_exemplo.to_numpy()
array([1, 2, 3, 4, 5])

Series comportam-se como dicionários

Podemos acessar os elementos de uma Series através das chaves fornecidas no index.

series_exemplo
a    1
b    2
c    3
d    4
e    5
dtype: int64
series_exemplo['a']
1

Podemos adicionar novos elementos associados a chaves novas.

series_exemplo['f'] = 6
series_exemplo
a    1
b    2
c    3
d    4
e    5
f    6
dtype: int64
'f' in series_exemplo
True
'g' in series_exemplo
False

Neste examplo, tentamos acessar uma chave inexistente. Logo, um erro ocorre.

series_exemplo['g']
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
~/anaconda3/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_value(self, series, key)
   4409             try:
-> 4410                 return libindex.get_value_at(s, key)
   4411             except IndexError:

pandas/_libs/index.pyx in pandas._libs.index.get_value_at()

pandas/_libs/index.pyx in pandas._libs.index.get_value_at()

pandas/_libs/util.pxd in pandas._libs.util.get_value_at()

pandas/_libs/util.pxd in pandas._libs.util.validate_indexer()

TypeError: 'str' object cannot be interpreted as an integer

During handling of the above exception, another exception occurred:

KeyError                                  Traceback (most recent call last)
<ipython-input-31-12f1880611a9> in <module>
----> 1 series_exemplo['g']

~/anaconda3/lib/python3.7/site-packages/pandas/core/series.py in __getitem__(self, key)
    869         key = com.apply_if_callable(key, self)
    870         try:
--> 871             result = self.index.get_value(self, key)
    872 
    873             if not is_scalar(result):

~/anaconda3/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_value(self, series, key)
   4416                     raise InvalidIndexError(key)
   4417                 else:
-> 4418                     raise e1
   4419             except Exception:
   4420                 raise e1

~/anaconda3/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_value(self, series, key)
   4402         k = self._convert_scalar_indexer(k, kind="getitem")
   4403         try:
-> 4404             return self._engine.get_value(s, k, tz=getattr(series.dtype, "tz", None))
   4405         except KeyError as e1:
   4406             if len(self) > 0 and (self.holds_integer() or self.is_boolean()):

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_value()

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_value()

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'g'
series_exemplo.get('g')

Entretanto, podemos utilizar o método get para lidar com chaves que possivelmente inexistam e adicionar um NaN do numpy como valor alternativo se, de fato, não exista valor atribuído.

series_exemplo.get('g',np.nan)
nan

O atributo name

Uma Series do pandas possui um atributo opcional name que nos permite identificar o objeto. Ele é bastante útil em operações envolvendo DataFrames.

serie_com_nome = pd.Series(dicionario_exemplo, name = "Idade")
serie_com_nome
Ana      20
João     19
Maria    21
Pedro    22
Túlio    20
Name: Idade, dtype: int64

A função date_range

Em muitas situações, os índices podem ser organizados como datas. A função data_range cria índices a partir de datas. Alguns argumentos desta função são:

  • start: str contendo a data que serve como limite à esquerda das datas. Padrão: None

  • end: str contendo a data que serve como limite à direita das datas. Padrão: None

  • freq: frequência a ser considerada. Por exemplo, dias (D), horas (H), semanas (W), fins de meses (M), inícios de meses (MS), fins de anos (Y), inícios de anos (YS) etc. Pode-se também utilizar múltiplos (p.ex. 5H, 2Y etc.). Padrão: None.

  • periods: número de períodos a serem considerados (o período é determinado pelo argumento freq).

Abaixo damos exemplos do uso de date_range com diferente formatos de data.

pd.date_range(start='1/1/2020', freq='W', periods=10)
DatetimeIndex(['2020-01-05', '2020-01-12', '2020-01-19', '2020-01-26',
               '2020-02-02', '2020-02-09', '2020-02-16', '2020-02-23',
               '2020-03-01', '2020-03-08'],
              dtype='datetime64[ns]', freq='W-SUN')
pd.date_range(start='2010-01-01', freq='2Y', periods=10)
DatetimeIndex(['2010-12-31', '2012-12-31', '2014-12-31', '2016-12-31',
               '2018-12-31', '2020-12-31', '2022-12-31', '2024-12-31',
               '2026-12-31', '2028-12-31'],
              dtype='datetime64[ns]', freq='2A-DEC')
pd.date_range('1/1/2020', freq='5H', periods=10)
DatetimeIndex(['2020-01-01 00:00:00', '2020-01-01 05:00:00',
               '2020-01-01 10:00:00', '2020-01-01 15:00:00',
               '2020-01-01 20:00:00', '2020-01-02 01:00:00',
               '2020-01-02 06:00:00', '2020-01-02 11:00:00',
               '2020-01-02 16:00:00', '2020-01-02 21:00:00'],
              dtype='datetime64[ns]', freq='5H')
pd.date_range(start='2010-01-01', freq='3YS', periods=3)
DatetimeIndex(['2010-01-01', '2013-01-01', '2016-01-01'], dtype='datetime64[ns]', freq='3AS-JAN')

O exemplo a seguir cria duas Series com valores aleatórios associados a um interstício de 10 dias.

indice_exemplo = pd.date_range('2020-01-01', periods=10, freq='D')
serie_1 = pd.Series(np.random.randn(10),index=indice_exemplo)
serie_2 = pd.Series(np.random.randn(10),index=indice_exemplo)