Manipulação de dados - I¶
Introdução¶
Vimos no início do curso que a ciência de dados pode ser mapeada em algumas etapas gerais, tais como: i) definição do problema; ii) aquisição de dados; iii) processamento de dados; iv) análise de dados; v) descoberta de dados e vi) solução.
Neste capítulo, abordaremos tópicos compreendidos entre as etapas ii) e iii) utilizando a biblioteca pandas.
A biblioteca pandas¶
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 |
|
João |
80 |
178 |
19 |
|
Maria |
62 |
164 |
21 |
|
Pedro |
67 |
165 |
22 |
|
Túlio |
73 |
171 |
20 |
|
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 argumentofreq
).
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)