3.3. Pandas

O pacote pandas é uma das ferramentas de Python mais importantes para cientistas e analistas de dados atualmente. Ele é a base da maior parte dos projetos que incluem leitura, manipulação, limpeza e escrita de dados. O nome pandas é derivado do termo panel data (dados em painel), que é um termo econométrico que descreve dados compostos de múltiplas observações através do tempo para os mesmos indivíduos.

Pandas foi desenvolvido como uma camada acima do NumPy, mas boa parte de suas funcionalidades de análise estatística são feitas pelo SciPy, além do uso do Matplotlib para funções de visualização. Dessa forma, pandas simplifica o uso de diversas bibliotecas úteis para estatísticos e cientistas de dados.

Os dois principais objetos de pandas são as séries (Series) e as tabelas (DataFrame). Uma Series é basicamente uma coluna, enquanto uma DataFrame é uma tabela multidimensional composta de uma coleção de Series. Exemplo:

import numpy as np
import pandas as pd
s = pd.Series([1, 3, 5, np.nan, 6, 8])

s
0    1.0
1    3.0
2    5.0
3    NaN
4    6.0
5    8.0
dtype: float64

A Series acima foi criada por meio de uma lista de valores, incluindo o valor NaN (not a number), que equivale a uma posição nula. A lista de valores foi passada para o construtor pd.Series e pandas criou um índice númerico para cada linha e determinou o tipo de que melhor se representaria os dados (float64). Note que todos os valores passados foram inteiros, menos o NaN, cujo valor é tratado como float64. A criação de DataFrames também é muito simples, porém muito versátil. No exemplo abaixo, criamos uma DataFrame composta por uma matriz de inteiros. Após a chamada ao construtor pd.DataFrame, pandas cria um índice númerico para cada linha e atribui um número como título de cada coluna.

pd.DataFrame(np.arange(12).reshape(4, 3))
0 1 2
0 0 1 2
1 3 4 5
2 6 7 8
3 9 10 11

Podemos passar mais informações ao construtor da DataFrame, como um nome para cada coluna:

pd.DataFrame(
    np.arange(12).reshape(4, 3),
    columns=['A', 'B', 'C']
)
A B C
0 0 1 2
1 3 4 5
2 6 7 8
3 9 10 11

Podemos também criar um índice customizado para cada linha. Por exemplo, vamos criar um índice com um intervalo de datas:

dates = pd.date_range(
    '20200101', periods=4
)  # experimente trocar a string da data por 'today'

pd.DataFrame(
    np.arange(12).reshape(4, 3),
    index=dates,
    columns=['A', 'B', 'C']
)
A B C
2020-01-01 0 1 2
2020-01-02 3 4 5
2020-01-03 6 7 8
2020-01-04 9 10 11

Uma outra forma de criar uma DataFrame é passar um dicionário com objetos que podem ser convertidos a objetos do tipo Series.

df = pd.DataFrame({
    'A': 1.,
    'B': pd.Timestamp('20130102'),
    'C': pd.Series(1, index=list(range(4)), dtype='float32'),
    'D': np.array([3] * 4, dtype='int32'),
    'E': pd.Categorical(["test", "train", "test", "train"]),
    'F': list('abcd')
})

df
A B C D E F
0 1.0 2013-01-02 1.0 3 test a
1 1.0 2013-01-02 1.0 3 train b
2 1.0 2013-01-02 1.0 3 test c
3 1.0 2013-01-02 1.0 3 train d

Note que as colunas resultantes tem tipos diferentes:

df.dtypes
A           float64
B    datetime64[ns]
C           float32
D             int32
E          category
F            object
dtype: object

3.3.1. Acessando os dados

O pacote oferece diversas formas de acessar os dados contidos em uma DataFrame. Por exemplo, para acessar as primeiras linhas, usa-se o método head:

df2 = pd.DataFrame(
    np.arange(200).reshape(40, 5),
    columns=list('ABCDE')
)

df2.head()
A B C D E
0 0 1 2 3 4
1 5 6 7 8 9
2 10 11 12 13 14
3 15 16 17 18 19
4 20 21 22 23 24

Se o método head for chamado sem parâmetro algum, ele irá retornar as cinco primeiras linhas da DataFrame. Caso receba um número inteiro, o método irá retornar a quantidade desejada.

df2.head(2)
A B C D E
0 0 1 2 3 4
1 5 6 7 8 9

De forma similar, podemos acessar as últimas linhas da DataFrame:

df2.tail()
A B C D E
35 175 176 177 178 179
36 180 181 182 183 184
37 185 186 187 188 189
38 190 191 192 193 194
39 195 196 197 198 199
df2.tail(3)
A B C D E
37 185 186 187 188 189
38 190 191 192 193 194
39 195 196 197 198 199

Para listar o índice e as colunas:

df2.index
RangeIndex(start=0, stop=40, step=1)
df2.columns
Index(['A', 'B', 'C', 'D', 'E'], dtype='object')

Para verificar o formato da tabela, usa-se o atributo shape:

df2.shape
(40, 5)

O método to_numpy fornece uma representação dos dados na forma de um array de NumPy. Isso pode ser uma operação simples, caso os dados da DataFrame sejam homogêneos:

df2.to_numpy()
array([[  0,   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,  29],
       [ 30,  31,  32,  33,  34],
       [ 35,  36,  37,  38,  39],
       [ 40,  41,  42,  43,  44],
       [ 45,  46,  47,  48,  49],
       [ 50,  51,  52,  53,  54],
       [ 55,  56,  57,  58,  59],
       [ 60,  61,  62,  63,  64],
       [ 65,  66,  67,  68,  69],
       [ 70,  71,  72,  73,  74],
       [ 75,  76,  77,  78,  79],
       [ 80,  81,  82,  83,  84],
       [ 85,  86,  87,  88,  89],
       [ 90,  91,  92,  93,  94],
       [ 95,  96,  97,  98,  99],
       [100, 101, 102, 103, 104],
       [105, 106, 107, 108, 109],
       [110, 111, 112, 113, 114],
       [115, 116, 117, 118, 119],
       [120, 121, 122, 123, 124],
       [125, 126, 127, 128, 129],
       [130, 131, 132, 133, 134],
       [135, 136, 137, 138, 139],
       [140, 141, 142, 143, 144],
       [145, 146, 147, 148, 149],
       [150, 151, 152, 153, 154],
       [155, 156, 157, 158, 159],
       [160, 161, 162, 163, 164],
       [165, 166, 167, 168, 169],
       [170, 171, 172, 173, 174],
       [175, 176, 177, 178, 179],
       [180, 181, 182, 183, 184],
       [185, 186, 187, 188, 189],
       [190, 191, 192, 193, 194],
       [195, 196, 197, 198, 199]])

No entanto, como arrays de NumPy devem ser homogêneos, no caso de DataFrames heterogêneas, a operação pode ser custosa, porque pandas vai precisar encontrar o tipo que melhorar representará todos os dados em um array. Por vezes, esse tipo pode terminar sendo object.

df.to_numpy()
array([[1.0, Timestamp('2013-01-02 00:00:00'), 1.0, 3, 'test', 'a'],
       [1.0, Timestamp('2013-01-02 00:00:00'), 1.0, 3, 'train', 'b'],
       [1.0, Timestamp('2013-01-02 00:00:00'), 1.0, 3, 'test', 'c'],
       [1.0, Timestamp('2013-01-02 00:00:00'), 1.0, 3, 'train', 'd']],
      dtype=object)

Note que o índice e os nomes das colunas não aparecem na saída de to_numpy. Para transpor a DataFrame, basta accessar o atributo T:

df2.T
0 1 2 3 4 5 6 7 8 9 ... 30 31 32 33 34 35 36 37 38 39
A 0 5 10 15 20 25 30 35 40 45 ... 150 155 160 165 170 175 180 185 190 195
B 1 6 11 16 21 26 31 36 41 46 ... 151 156 161 166 171 176 181 186 191 196
C 2 7 12 17 22 27 32 37 42 47 ... 152 157 162 167 172 177 182 187 192 197
D 3 8 13 18 23 28 33 38 43 48 ... 153 158 163 168 173 178 183 188 193 198
E 4 9 14 19 24 29 34 39 44 49 ... 154 159 164 169 174 179 184 189 194 199

5 rows × 40 columns

Para acessar uma única coluna, retornada como uma Series, podemos usar notação de colchetes ou de atributo:

df['E']
0     test
1    train
2     test
3    train
Name: E, dtype: category
Categories (2, object): [test, train]
df.E
0     test
1    train
2     test
3    train
Name: E, dtype: category
Categories (2, object): [test, train]

Para acessar linhas pelos seus índices, podemos usar o atributo loc:

dates = pd.date_range(
    '20200101', periods=6
)

df3 = pd.DataFrame(
    np.arange(18).reshape(6, 3),
    index=dates,
    columns=['A', 'B', 'C']
)

df3.loc[dates[0]]
A    0
B    1
C    2
Name: 2020-01-01 00:00:00, dtype: int64

O atributo loc também permite acessar listas de índices, bem como especificar quais colunas devem ser retornadas:

df3.loc[[dates[2], dates[4]]]
A B C
2020-01-03 6 7 8
2020-01-05 12 13 14
df3.loc[[dates[2], dates[4]], ['A', 'C']]
A C
2020-01-03 6 8
2020-01-05 12 14

Se apenas um índice e uma coluna forem especificados, pandas retornará o valor correspondente da tabela:

df3.loc[df3.index[0], 'B']
1

Note a diferença de comportamento se a única coluna for informada dentro de uma lista:

df3.loc[df3.index[0], ['B']]  # retorna uma Series com apenas um elemento
B    1
Name: 2020-01-01 00:00:00, dtype: int64

Para acessar apenas um valor de forma mais rápida, o atributo loc pode ser substituído por at:

df3.at[df3.index[0], 'B']
1

Similar ao loc, o atributo iloc também permite acessar os dados da DataFrame, porém ao invés de acessar através dos valores dos índices, ele permite acessar pelas posições das linhas. Exemplo:

df3.iloc[0]  # equivale a df3.loc[df3.index[0]]
A    0
B    1
C    2
Name: 2020-01-01 00:00:00, dtype: int64

As colunas também são tratadas numericamente por iloc. Inclusive, é possível usá-lo para acessar fatias da tabela, assim como em um array de NumPy:

df3.iloc[0, [0, 2]]
A    0
C    2
Name: 2020-01-01 00:00:00, dtype: int64
df3.iloc[0, 0]
0
df3.iloc[0:2, 0:2]  # exclui o final
A B
2020-01-01 0 1
2020-01-02 3 4

Continuando as similaridades com arrays, também é possível acessar os dados de uma DataFrame usando condições booleanas:

df2[df2.A % 2 == 0]  # apenas as linhas em que a coluna A é par
A B C D E
0 0 1 2 3 4
2 10 11 12 13 14
4 20 21 22 23 24
6 30 31 32 33 34
8 40 41 42 43 44
10 50 51 52 53 54
12 60 61 62 63 64
14 70 71 72 73 74
16 80 81 82 83 84
18 90 91 92 93 94
20 100 101 102 103 104
22 110 111 112 113 114
24 120 121 122 123 124
26 130 131 132 133 134
28 140 141 142 143 144
30 150 151 152 153 154
32 160 161 162 163 164
34 170 171 172 173 174
36 180 181 182 183 184
38 190 191 192 193 194
df2[df2 % 2 == 0]  # Apenas os valores pares
A B C D E
0 0.0 NaN 2.0 NaN 4.0
1 NaN 6.0 NaN 8.0 NaN
2 10.0 NaN 12.0 NaN 14.0
3 NaN 16.0 NaN 18.0 NaN
4 20.0 NaN 22.0 NaN 24.0
5 NaN 26.0 NaN 28.0 NaN
6 30.0 NaN 32.0 NaN 34.0
7 NaN 36.0 NaN 38.0 NaN
8 40.0 NaN 42.0 NaN 44.0
9 NaN 46.0 NaN 48.0 NaN
10 50.0 NaN 52.0 NaN 54.0
11 NaN 56.0 NaN 58.0 NaN
12 60.0 NaN 62.0 NaN 64.0
13 NaN 66.0 NaN 68.0 NaN
14 70.0 NaN 72.0 NaN 74.0
15 NaN 76.0 NaN 78.0 NaN
16 80.0 NaN 82.0 NaN 84.0
17 NaN 86.0 NaN 88.0 NaN
18 90.0 NaN 92.0 NaN 94.0
19 NaN 96.0 NaN 98.0 NaN
20 100.0 NaN 102.0 NaN 104.0
21 NaN 106.0 NaN 108.0 NaN
22 110.0 NaN 112.0 NaN 114.0
23 NaN 116.0 NaN 118.0 NaN
24 120.0 NaN 122.0 NaN 124.0
25 NaN 126.0 NaN 128.0 NaN
26 130.0 NaN 132.0 NaN 134.0
27 NaN 136.0 NaN 138.0 NaN
28 140.0 NaN 142.0 NaN 144.0
29 NaN 146.0 NaN 148.0 NaN
30 150.0 NaN 152.0 NaN 154.0
31 NaN 156.0 NaN 158.0 NaN
32 160.0 NaN 162.0 NaN 164.0
33 NaN 166.0 NaN 168.0 NaN
34 170.0 NaN 172.0 NaN 174.0
35 NaN 176.0 NaN 178.0 NaN
36 180.0 NaN 182.0 NaN 184.0
37 NaN 186.0 NaN 188.0 NaN
38 190.0 NaN 192.0 NaN 194.0
39 NaN 196.0 NaN 198.0 NaN

O método isin permite filtrar dados:

df[df['F'].isin(['a', 'd'])]
A B C D E F
0 1.0 2013-01-02 1.0 3 test a
3 1.0 2013-01-02 1.0 3 train d

Além de acessar dados, loc, iloc, at e iat permitem atribuir valores às posições indicadas:

df3.iloc[2] = 1

df3
A B C
2020-01-01 0 1 2
2020-01-02 3 4 5
2020-01-03 1 1 1
2020-01-04 9 10 11
2020-01-05 12 13 14
2020-01-06 15 16 17
df3.loc['20200101', 'C'] = 10

df3
A B C
2020-01-01 0 1 10
2020-01-02 3 4 5
2020-01-03 1 1 1
2020-01-04 9 10 11
2020-01-05 12 13 14
2020-01-06 15 16 17
df3.iat[2, 2] = -5

df3
A B C
2020-01-01 0 1 10
2020-01-02 3 4 5
2020-01-03 1 1 -5
2020-01-04 9 10 11
2020-01-05 12 13 14
2020-01-06 15 16 17

Também é possível usar máscaras booleanas para atribuir valores:

df2[df2 % 2 == 0] = -df2

df2
A B C D E
0 0 1 -2 3 -4
1 5 -6 7 -8 9
2 -10 11 -12 13 -14
3 15 -16 17 -18 19
4 -20 21 -22 23 -24
5 25 -26 27 -28 29
6 -30 31 -32 33 -34
7 35 -36 37 -38 39
8 -40 41 -42 43 -44
9 45 -46 47 -48 49
10 -50 51 -52 53 -54
11 55 -56 57 -58 59
12 -60 61 -62 63 -64
13 65 -66 67 -68 69
14 -70 71 -72 73 -74
15 75 -76 77 -78 79
16 -80 81 -82 83 -84
17 85 -86 87 -88 89
18 -90 91 -92 93 -94
19 95 -96 97 -98 99
20 -100 101 -102 103 -104
21 105 -106 107 -108 109
22 -110 111 -112 113 -114
23 115 -116 117 -118 119
24 -120 121 -122 123 -124
25 125 -126 127 -128 129
26 -130 131 -132 133 -134
27 135 -136 137 -138 139
28 -140 141 -142 143 -144
29 145 -146 147 -148 149
30 -150 151 -152 153 -154
31 155 -156 157 -158 159
32 -160 161 -162 163 -164
33 165 -166 167 -168 169
34 -170 171 -172 173 -174
35 175 -176 177 -178 179
36 -180 181 -182 183 -184
37 185 -186 187 -188 189
38 -190 191 -192 193 -194
39 195 -196 197 -198 199

Para adicionar uma nova coluna, basta usar a notação de colchetes com o nome da nova coluna:

df2['F'] = 1

df2
A B C D E F
0 0 1 -2 3 -4 1
1 5 -6 7 -8 9 1
2 -10 11 -12 13 -14 1
3 15 -16 17 -18 19 1
4 -20 21 -22 23 -24 1
5 25 -26 27 -28 29 1
6 -30 31 -32 33 -34 1
7 35 -36 37 -38 39 1
8 -40 41 -42 43 -44 1
9 45 -46 47 -48 49 1
10 -50 51 -52 53 -54 1
11 55 -56 57 -58 59 1
12 -60 61 -62 63 -64 1
13 65 -66 67 -68 69 1
14 -70 71 -72 73 -74 1
15 75 -76 77 -78 79 1
16 -80 81 -82 83 -84 1
17 85 -86 87 -88 89 1
18 -90 91 -92 93 -94 1
19 95 -96 97 -98 99 1
20 -100 101 -102 103 -104 1
21 105 -106 107 -108 109 1
22 -110 111 -112 113 -114 1
23 115 -116 117 -118 119 1
24 -120 121 -122 123 -124 1
25 125 -126 127 -128 129 1
26 -130 131 -132 133 -134 1
27 135 -136 137 -138 139 1
28 -140 141 -142 143 -144 1
29 145 -146 147 -148 149 1
30 -150 151 -152 153 -154 1
31 155 -156 157 -158 159 1
32 -160 161 -162 163 -164 1
33 165 -166 167 -168 169 1
34 -170 171 -172 173 -174 1
35 175 -176 177 -178 179 1
36 -180 181 -182 183 -184 1
37 185 -186 187 -188 189 1
38 -190 191 -192 193 -194 1
39 195 -196 197 -198 199 1

3.3.2. Estatística descritiva

O pacote pandas oferece diversas funções para análise de Estatística descritiva. A mais geral dessas funcionalidades é o método describe, que computa uma variedade de medidas:

df2.describe()
A B C D E F
count 40.000000 40.000000 40.000000 40.000000 40.000000 40.0
mean 2.500000 -2.500000 2.500000 -2.500000 2.500000 1.0
std 114.718161 115.591012 116.466128 117.343458 118.222953 0.0
min -190.000000 -196.000000 -192.000000 -198.000000 -194.000000 1.0
25% -92.500000 -98.500000 -94.500000 -100.500000 -96.500000 1.0
50% 2.500000 -2.500000 2.500000 -2.500000 2.500000 1.0
75% 97.500000 93.500000 99.500000 95.500000 101.500000 1.0
max 195.000000 191.000000 197.000000 193.000000 199.000000 1.0

É possivel selecionar os percentis que serão incluídos (a mediana é sempre retornada por padrão):

df2.describe(percentiles=[.05, .25, .75, .95])
A B C D E F
count 40.000000 40.000000 40.000000 40.000000 40.000000 40.0
mean 2.500000 -2.500000 2.500000 -2.500000 2.500000 1.0
std 114.718161 115.591012 116.466128 117.343458 118.222953 0.0
min -190.000000 -196.000000 -192.000000 -198.000000 -194.000000 1.0
5% -170.500000 -176.500000 -172.500000 -178.500000 -174.500000 1.0
25% -92.500000 -98.500000 -94.500000 -100.500000 -96.500000 1.0
50% 2.500000 -2.500000 2.500000 -2.500000 2.500000 1.0
75% 97.500000 93.500000 99.500000 95.500000 101.500000 1.0
95% 175.500000 171.500000 177.500000 173.500000 179.500000 1.0
max 195.000000 191.000000 197.000000 193.000000 199.000000 1.0

Para colunas não-númericas, describe retorna um sumário mais simples:

df['E'].describe()
count         4
unique        2
top       train
freq          2
Name: E, dtype: object

Numa DataFrame com tipos mistos, describe irá incluir apenas as colunas numéricas:

frame = pd.DataFrame({'a': ['Yes', 'Yes', 'No', 'No'], 'b': range(4)})

frame.describe()
b
count 4.000000
mean 1.500000
std 1.290994
min 0.000000
25% 0.750000
50% 1.500000
75% 2.250000
max 3.000000

Esse comportamento pode ser controlado pelos argumentos include e exclude:

frame.describe(include=['object'])
a
count 4
unique 2
top No
freq 2
frame.describe(include=['number'])
b
count 4.000000
mean 1.500000
std 1.290994
min 0.000000
25% 0.750000
50% 1.500000
75% 2.250000
max 3.000000
frame.describe(include=['object', 'number'])
a b
count 4 4.000000
unique 2 NaN
top No NaN
freq 2 NaN
mean NaN 1.500000
std NaN 1.290994
min NaN 0.000000
25% NaN 0.750000
50% NaN 1.500000
75% NaN 2.250000
max NaN 3.000000
frame.describe(include='all')
a b
count 4 4.000000
unique 2 NaN
top No NaN
freq 2 NaN
mean NaN 1.500000
std NaN 1.290994
min NaN 0.000000
25% NaN 0.750000
50% NaN 1.500000
75% NaN 2.250000
max NaN 3.000000

Cada uma das funções separadas de estatística descritiva pode ser calculada para um dado eixo (axis), assim como NumPy:

df = pd.DataFrame({
    'one': pd.Series(np.random.randn(3), index=['a', 'b', 'c']),
    'two': pd.Series(np.random.randn(4), index=['a', 'b', 'c', 'd']),
    'three': pd.Series(np.random.randn(3), index=['b', 'c', 'd'])
})

df
one two three
a 0.425667 0.190922 NaN
b -1.450517 -0.162678 -0.483092
c -0.448780 0.519226 -0.410795
d NaN 2.414328 0.823862
df.mean(0)
one     -0.491210
two      0.740449
three   -0.023342
dtype: float64
df.mean(1)
a    0.308294
b   -0.698762
c   -0.113450
d    1.619095
dtype: float64

Diferente de NumPy, se o eixo não for informado, o padrão é axis=0 (NumPy calcula a média para o array todo).

df.mean()
one     -0.491210
two      0.740449
three   -0.023342
dtype: float64

Note que os valores NaN são descartados. Isso pode ser controlado pelo argumento skipna, que é True por padrão:

df.mean(0, skipna=False)
one           NaN
two      0.740449
three         NaN
dtype: float64

Combinando comportameto aritmético e broadcasting, é possível realizar operações estatísticas, como a padronização (média 0 e desvio 1), de forma concisa:

df_stand = (df - df.mean()) / df.std()

df_stand.std()
one      1.0
two      1.0
three    1.0
dtype: float64
df_stand = df.sub(df.mean(1), axis=0).div(df.std(1), axis=0)

df_stand.std(1)
a    1.0
b    1.0
c    1.0
d    1.0
dtype: float64

Métodos como cumsum (soma acumulada) e cumprod (produto acumulado) preservam a posição de valores NaN:

df.cumsum()
one two three
a 0.425667 0.190922 NaN
b -1.024851 0.028244 -0.483092
c -1.473631 0.547470 -0.893887
d NaN 2.961798 -0.070026
df.cumprod(1)
one two three
a 0.425667 0.081269 NaN
b -1.450517 0.235967 -0.113994
c -0.448780 -0.233018 0.095723
d NaN 2.414328 1.989073

A tabela abaixo oferece um sumário de funções comumente usadas:

Função

Convenção

count

número de observações não-NaN

sum

soma dos valores

mean

média dos valores

mad

desvio absoluto médio

median

mediana dos valores

min

mínimo

max

máximo

mode

moda

abs

valores absolutos

prod

produto dos valores

std

desvio padrão amostral

var

variância amostral

skew

assimetria amostral

kurt

curtose amostral

quantile

quantis amostrais

cumsum

soma acumulada

cumprod

produto acumulado

cummax

máximo acumulado

cummin

mínimo acumulado

As funções idxmin e idxmax retornam os índices dos menores e maiores valores, respectivamente, através do eixo informado:

df.idxmin(0)
one      b
two      b
three    b
dtype: object
df.idxmax(1)
a    one
b    two
c    two
d    two
dtype: object

Os objetos do tipo Series disponibilizam um método chamado value_counts (que também pode ser usado como uma função) que computa um histograma de um array unidimensional:

data = np.random.randint(0, 7, size=50)

data
array([3, 4, 2, 1, 3, 3, 0, 1, 0, 0, 1, 1, 4, 0, 3, 4, 3, 6, 5, 0, 2, 2,
       0, 1, 2, 2, 2, 6, 0, 0, 5, 3, 6, 1, 6, 4, 4, 2, 3, 4, 3, 3, 4, 6,
       0, 1, 4, 2, 3, 5])
s = pd.Series(data)

s.value_counts()
3    10
0     9
4     8
2     8
1     7
6     5
5     3
dtype: int64
pd.value_counts(data)
3    10
0     9
4     8
2     8
1     7
6     5
5     3
dtype: int64

Valores contínuos podem ser discretizados usando as funções cut (intervalos baseados nos valores) e qcut (intervalos baseados nos quantis).

arr = np.random.randn(20)

factor = pd.cut(arr, 4)

factor
[(1.055, 2.297], (1.055, 2.297], (-1.429, -0.187], (1.055, 2.297], (-0.187, 1.055], ..., (-0.187, 1.055], (-0.187, 1.055], (-0.187, 1.055], (-2.676, -1.429], (-0.187, 1.055]]
Length: 20
Categories (4, interval[float64]): [(-2.676, -1.429] < (-1.429, -0.187] < (-0.187, 1.055] < (1.055, 2.297]]
factor = pd.cut(arr, [-5, -1, 0, 1, 5])

factor
[(1, 5], (1, 5], (-5, -1], (1, 5], (0, 1], ..., (0, 1], (0, 1], (0, 1], (-5, -1], (0, 1]]
Length: 20
Categories (4, interval[int64]): [(-5, -1] < (-1, 0] < (0, 1] < (1, 5]]
pd.value_counts(factor)
(0, 1]      7
(-1, 0]     7
(1, 5]      3
(-5, -1]    3
dtype: int64
factor = pd.qcut(arr, [0, .25, .5, .75, 1])

factor
[(0.584, 2.297], (0.584, 2.297], (-2.6719999999999997, -0.622], (0.584, 2.297], (-0.0254, 0.584], ..., (-0.0254, 0.584], (0.584, 2.297], (-0.0254, 0.584], (-2.6719999999999997, -0.622], (-0.0254, 0.584]]
Length: 20
Categories (4, interval[float64]): [(-2.6719999999999997, -0.622] < (-0.622, -0.0254] < (-0.0254, 0.584] < (0.584, 2.297]]
pd.value_counts(factor)
(0.584, 2.297]                   5
(-0.0254, 0.584]                 5
(-0.622, -0.0254]                5
(-2.6719999999999997, -0.622]    5
dtype: int64

3.3.3. Ordenando valores

Três formas de ordenação estão disponíveis em pandas: pelos índices, pelas colunas e pelas duas coisas. Os métodos Series.sort_index() e DataFrame.sort_index() são usados para ordenar objetos pandas pelos seus índices:

df = pd.DataFrame({
     'one': pd.Series(np.random.randn(3), index=['a', 'b', 'c']),
     'two': pd.Series(np.random.randn(4), index=['a', 'b', 'c', 'd']),
     'three': pd.Series(np.random.randn(3), index=['b', 'c', 'd'])
})

df
one two three
a 0.400364 -1.036728 NaN
b 1.345439 0.146045 0.476049
c 0.820887 -0.857148 -0.700924
d NaN 0.015896 -0.065780
df.sort_index(ascending=False)
one two three
d NaN 0.015896 -0.065780
c 0.820887 -0.857148 -0.700924
b 1.345439 0.146045 0.476049
a 0.400364 -1.036728 NaN
df.sort_index(axis=1)
one three two
a 0.400364 NaN -1.036728
b 1.345439 0.476049 0.146045
c 0.820887 -0.700924 -0.857148
d NaN -0.065780 0.015896
df['three'].sort_index(ascending=False)
d   -0.065780
c   -0.700924
b    0.476049
a         NaN
Name: three, dtype: float64

O método Series.sort_values() é usado para ordenar uma Series pelos seus valores. Já o método DataFrame.sort_values() pode ser usado para ordenar uma DataFrame pelos valores das linhas ou das colunas e tem um parâmetro opcional by= que serve para especificar uma ou mais colunas para determinar a ordem.

df1 = pd.DataFrame({'one': [2, 1, 1, 1],
                    'two': [1, 3, 2, 4],
                    'three': [5, 4, 3, 2]})

df1
one two three
0 2 1 5
1 1 3 4
2 1 2 3
3 1 4 2
df1.sort_values(by='two')
one two three
0 2 1 5
2 1 2 3
1 1 3 4
3 1 4 2
df1[['one', 'two', 'three']].sort_values(by=['one', 'two'])
one two three
2 1 2 3
1 1 3 4
3 1 4 2
0 2 1 5

Esses métodos tem um tratamento especial para valores faltantes, por meio do parâmetro na_position:

s = pd.Series(
    ['A', 'B', np.nan, 'Aaba', 'Baca', np.nan, 'CABA', 'dog', 'cat'],
    dtype="object"
)

s.sort_values()
0       A
3    Aaba
1       B
4    Baca
6    CABA
8     cat
7     dog
2     NaN
5     NaN
dtype: object
s.sort_values(na_position='first')
2     NaN
5     NaN
0       A
3    Aaba
1       B
4    Baca
6    CABA
8     cat
7     dog
dtype: object

Strings passadas para o argumento by= podem se referir a colunas ou nomes de índices. Aliás, esse é um bom momento para introduzir índices múltiplos:

idx = pd.MultiIndex.from_tuples(
    [('a', 1), ('a', 2), ('a', 2), ('b', 2), ('b', 1), ('b', 1)]
)

idx.names = ['first', 'second']

df_multi = pd.DataFrame(
    {'A': np.arange(6, 0, -1)},
    index=idx
)

df_multi
A
first second
a 1 6
2 5
2 4
b 2 3
1 2
1 1
df_multi.sort_values(by=['second', 'A'])
A
first second
b 1 1
1 2
a 1 6
b 2 3
a 2 4
2 5

Series também oferece os métodos nsmallest() e nlargest(), que retornam os menores ou maiores n valores, o que pode ser bem mais eficiente do que ordenar a Series toda só para chamar tail(n) ou head(n) no resultado. Esses métodos também são oferecidos por DataFrames e é preciso informar a(s) coluna(s) desejada(s).

s = pd.Series(np.random.permutation(10))

s
0    7
1    8
2    4
3    5
4    1
5    3
6    6
7    2
8    0
9    9
dtype: int64
s.sort_values()
8    0
4    1
7    2
5    3
2    4
3    5
6    6
0    7
1    8
9    9
dtype: int64
s.nsmallest(3)
8    0
4    1
7    2
dtype: int64
s.nlargest(2)
9    9
1    8
dtype: int64
df = pd.DataFrame({'a': [-2, -1, 1, 10, 8, 11, -1],
                   'b': list('abdceff'),
                   'c': [1.0, 2.0, 4.0, 3.2, np.nan, 3.0, 4.0]})
df
a b c
0 -2 a 1.0
1 -1 b 2.0
2 1 d 4.0
3 10 c 3.2
4 8 e NaN
5 11 f 3.0
6 -1 f 4.0
df.nlargest(3, 'a')
a b c
5 11 f 3.0
3 10 c 3.2
4 8 e NaN
df.nsmallest(5, ['a', 'c'])
a b c
0 -2 a 1.0
1 -1 b 2.0
6 -1 f 4.0
2 1 d 4.0
4 8 e NaN

3.3.4. Combinando Series e DataFrames

Pandas oferece diversas formas para combinar objetos Series e DataFrames, usando lógica de conjuntos para os índices e colunas e álgebra relacional para operações do tipo join (que remetem a operações de bancos de dados SQL).

A função concat faz o trabalho de concatenação ao longo de um eixo (axis), usando lógica de conjuntos (união ou interseção) dos índices ou colunas no outro eixo, se o outro eixo existir (Series tem apenas um eixo). Um exemplo:

df1 = pd.DataFrame({'A': ['A0', 'A1', 'A2', 'A3'],
                    'B': ['B0', 'B1', 'B2', 'B3'],
                    'C': ['C0', 'C1', 'C2', 'C3'],
                    'D': ['D0', 'D1', 'D2', 'D3']},
                    index=[0, 1, 2, 3])
 

df2 = pd.DataFrame({'A': ['A4', 'A5', 'A6', 'A7'],
                    'B': ['B4', 'B5', 'B6', 'B7'],
                    'C': ['C4', 'C5', 'C6', 'C7'],
                    'D': ['D4', 'D5', 'D6', 'D7']},
                    index=[4, 5, 6, 7])
 

df3 = pd.DataFrame({'A': ['A8', 'A9', 'A10', 'A11'],
                    'B': ['B8', 'B9', 'B10', 'B11'],
                    'C': ['C8', 'C9', 'C10', 'C11'],
                    'D': ['D8', 'D9', 'D10', 'D11']},
                    index=[8, 9, 10, 11])


frames = [df1, df2, df3]
result = pd.concat(frames)
result
A B C D
0 A0 B0 C0 D0
1 A1 B1 C1 D1
2 A2 B2 C2 D2
3 A3 B3 C3 D3
4 A4 B4 C4 D4
5 A5 B5 C5 D5
6 A6 B6 C6 D6
7 A7 B7 C7 D7
8 A8 B8 C8 D8
9 A9 B9 C9 D9
10 A10 B10 C10 D10
11 A11 B11 C11 D11

Suponha que você queira associar chaves específicas para os dados que pertenciam a cada DataFrame original. Para isso, pode-se usar o argumento keys:

result = pd.concat(frames, keys=['x', 'y', 'z'])
result
A B C D
x 0 A0 B0 C0 D0
1 A1 B1 C1 D1
2 A2 B2 C2 D2
3 A3 B3 C3 D3
y 4 A4 B4 C4 D4
5 A5 B5 C5 D5
6 A6 B6 C6 D6
7 A7 B7 C7 D7
z 8 A8 B8 C8 D8
9 A9 B9 C9 D9
10 A10 B10 C10 D10
11 A11 B11 C11 D11

O mesmo efeito pode ser obtido passando um dicionário com as DataFrames parciais:

pieces = {'x': df1, 'y': df2, 'z': df3}

result = pd.concat(pieces)

result
A B C D
x 0 A0 B0 C0 D0
1 A1 B1 C1 D1
2 A2 B2 C2 D2
3 A3 B3 C3 D3
y 4 A4 B4 C4 D4
5 A5 B5 C5 D5
6 A6 B6 C6 D6
7 A7 B7 C7 D7
z 8 A8 B8 C8 D8
9 A9 B9 C9 D9
10 A10 B10 C10 D10
11 A11 B11 C11 D11

O objeto resultante da concatenação passou a ter um índice múltiplo e hierárquico, o que permite selecionar cada bloco original usando o atributo loc. Note que no jupyter notebook, ao passar o ponteiro do mouse sobre um dos índices, as linhas que pertencem a ele são destacadas.

result.loc['y']
A B C D
4 A4 B4 C4 D4
5 A5 B5 C5 D5
6 A6 B6 C6 D6
7 A7 B7 C7 D7
result.index.levels
FrozenList([['x', 'y', 'z'], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]])

O uso da função concat realiza uma cópia dos dados, então ela não deve ser usada iterativamente, i.e. se seus dados são gerados por um processo repetido, o ideal é guardar as DataFrames parciais em uma lista e aplicar concat uma única vez.

Ao juntar múltiplas DataFrames, é possível escolher como lidar com os outros eixos de duas formas usando o argumento join:

  1. Tomando a união, i.e. join=’outer’. Essa é a opção padrão e nunca resulta em perda de informação.

  2. Tomando a interseção, i.e. join=’inner’.

Primeiro vejamos um exemplo de join=’outer’:

df1
A B C D
0 A0 B0 C0 D0
1 A1 B1 C1 D1
2 A2 B2 C2 D2
3 A3 B3 C3 D3
df4 = pd.DataFrame({'B': ['B2', 'B3', 'B6', 'B7'],
                    'D': ['D2', 'D3', 'D6', 'D7'],
                    'F': ['F2', 'F3', 'F6', 'F7']},
                    index=[2, 3, 6, 7])
df4
B D F
2 B2 D2 F2
3 B3 D3 F3
6 B6 D6 F6
7 B7 D7 F7
result = pd.concat([df1, df4], axis=1, sort=False)
result
A B C D B D F
0 A0 B0 C0 D0 NaN NaN NaN
1 A1 B1 C1 D1 NaN NaN NaN
2 A2 B2 C2 D2 B2 D2 F2
3 A3 B3 C3 D3 B3 D3 F3
6 NaN NaN NaN NaN B6 D6 F6
7 NaN NaN NaN NaN B7 D7 F7
result = pd.concat([df1, df4], axis=0, sort=False)
result
A B C D F
0 A0 B0 C0 D0 NaN
1 A1 B1 C1 D1 NaN
2 A2 B2 C2 D2 NaN
3 A3 B3 C3 D3 NaN
2 NaN B2 NaN D2 F2
3 NaN B3 NaN D3 F3
6 NaN B6 NaN D6 F6
7 NaN B7 NaN D7 F7
result = pd.concat([df1, df4], axis=1, join='inner')
result
A B C D B D F
2 A2 B2 C2 D2 B2 D2 F2
3 A3 B3 C3 D3 B3 D3 F3
result = pd.concat([df1, df4], axis=0, join='inner')
result
B D
0 B0 D0
1 B1 D1
2 B2 D2
3 B3 D3
2 B2 D2
3 B3 D3
6 B6 D6
7 B7 D7

Para DataFrames que não possem índices importantes, é possível concatená-las e ignorar os índices originais, o que pode ser útil quando existem índices repetidos, como no exemplo acima. Para isso, usa-se o argumento ignore_index.

result = pd.concat([df1, df4], axis=0, ignore_index=True, join='inner')
result
B D
0 B0 D0
1 B1 D1
2 B2 D2
3 B3 D3
4 B2 D2
5 B3 D3
6 B6 D6
7 B7 D7
result = pd.concat([df1, df4], axis=1, ignore_index=True, join='inner')
result
0 1 2 3 4 5 6
2 A2 B2 C2 D2 B2 D2 F2
3 A3 B3 C3 D3 B3 D3 F3

É possível concatenar uma mistura de objetos Series e DataFrame. Internamente, pandas transforma a(s) Series em DataFrame(s) com apenas um coluna com o(s) mesmo(s) nome(s) da(s) Series.

s1 = pd.Series(['X0', 'X1', 'X2', 'X3'], name='X')

pd.concat([df1, s1], axis=1)
A B C D X
0 A0 B0 C0 D0 X0
1 A1 B1 C1 D1 X1
2 A2 B2 C2 D2 X2
3 A3 B3 C3 D3 X3

Caso a(s) Series não tenha(m) nome, o(s) nome(s) será(ão) atribuído(s) numericamente e consecutivamente:

s2 = pd.Series(['_0', '_1', '_2', '_3'])

pd.concat([df1, s2, s2, s2], axis=1)
A B C D 0 1 2
0 A0 B0 C0 D0 _0 _0 _0
1 A1 B1 C1 D1 _1 _1 _1
2 A2 B2 C2 D2 _2 _2 _2
3 A3 B3 C3 D3 _3 _3 _3
pd.concat(
    [df1, s1], axis=1, ignore_index=True
)  # todas as colunas perdem seus nomes
0 1 2 3 4
0 A0 B0 C0 D0 X0
1 A1 B1 C1 D1 X1
2 A2 B2 C2 D2 X2
3 A3 B3 C3 D3 X3

O argumento keys tem um uso interessante que é sobrescrever os nomes das colunas quando uma nova DataFrame é criada por meio da concatenação de várias Series. Note que o comportamento padrão, como vimos acima, é que a DataFrame resultante use o nome de cada Series como nome para a coluna resultante (caso a Series tenha nome).

s3 = pd.Series([0, 1, 2, 3], name='foo')

s4 = pd.Series([0, 1, 2, 3])

s5 = pd.Series([0, 1, 4, 5])

pd.concat([s3, s4, s5], axis=1)
foo 0 1
0 0 0 0
1 1 1 1
2 2 2 4
3 3 3 5
pd.concat([s3, s4, s5], axis=1, keys=['red', 'blue', 'yellow'])
red blue yellow
0 0 0 0
1 1 1 1
2 2 2 4
3 3 3 5

Mesmo não sendo muito eficiente (porque um novo objeto precisa ser criado), é possível adicionar um única linha nova a uma DataFrame passando uma Series para o método append.

s2 = pd.Series(['X0', 'X1', 'X2', 'X3'], index=['A', 'B', 'C', 'D'], name='t')
df1.append(s2)
A B C D
0 A0 B0 C0 D0
1 A1 B1 C1 D1
2 A2 B2 C2 D2
3 A3 B3 C3 D3
t X0 X1 X2 X3

Caso a Series não tenha nome, é necessário usar o argumento ignore_index:

s2 = pd.Series(['X0', 'X1', 'X2', 'X3'], index=['A', 'B', 'C', 'D'])
df1.append(s2, ignore_index=True)
A B C D
0 A0 B0 C0 D0
1 A1 B1 C1 D1
2 A2 B2 C2 D2
3 A3 B3 C3 D3
4 X0 X1 X2 X3

3.3.5. Iterando sobre objetos

O comportamento de iteração em objetos pandas depende do tipo. Ao iterar sobre uma Series, o comportamento é igual a um array unidimensional, produzindo os seus valores.

for value in s2:
    print(value)
X0
X1
X2
X3

No caso de DataFrames, pandas itera sobre as colunas, como se estivesse iterando sobre as chaves de um dicionário.

for col in df1:
    print(col)
A
B
C
D

Para iterar sobre as linhas de uma DataFrame, pandas oferece os seguintes métodos: iterrows() e itertuples(). O método iterrows() retorna as linhas de uma DataFrame como pares (índice, Series). Como cada linha é retornada como uma Series, valores de colunas com tipos diferentes são convertidos para o tipo mais geral.

df = pd.DataFrame({'a': [1, 2, 3], 'b': ['a', 'b', 'c']})

for row_index, row in df.iterrows():
    print(row_index, row, sep='\n')
0
a    1
b    a
Name: 0, dtype: object
1
a    2
b    b
Name: 1, dtype: object
2
a    3
b    c
Name: 2, dtype: object

O método itertuples(), por outro lado, retorna uma tupla nomeada para cada linha da DataFrame. O primeiro elemento da tupla será o índice e cada elemento seguinte corresponderá ao valor em cada coluna na linha. Caso o nome da coluna não possa ser convertido para um identificador Python válido, ele será substituído para um nome posicional. Se o número de colunas for maior do 255, tuplas comuns são retornadas.

for row in df.itertuples():
    print(row)
Pandas(Index=0, a=1, b='a')
Pandas(Index=1, a=2, b='b')
Pandas(Index=2, a=3, b='c')
df = pd.DataFrame({'1': [1, 2, 3], '2': ['a', 'b', 'c']})

for row in df.itertuples():
    print(row)
Pandas(Index=0, _1=1, _2='a')
Pandas(Index=1, _1=2, _2='b')
Pandas(Index=2, _1=3, _2='c')

3.3.6. Dividindo um objeto em grupos

Objetos pandas podem ser divididos através de qualquer de seus eixos por meio de um mapeamento de valores para nomes de grupos. Essa operação cria um objeto do tipo GroupBy.

df = pd.DataFrame([('bird', 'Falconiformes', 389.0),
                   ('bird', 'Psittaciformes', 24.0),
                   ('mammal', 'Carnivora', 80.2),
                   ('mammal', 'Primates', np.nan),
                   ('mammal', 'Carnivora', 58)],
                  index=['falcon', 'parrot', 'lion', 'monkey', 'leopard'],
                  columns=('class', 'order', 'max_speed'))

df
class order max_speed
falcon bird Falconiformes 389.0
parrot bird Psittaciformes 24.0
lion mammal Carnivora 80.2
monkey mammal Primates NaN
leopard mammal Carnivora 58.0
grouped = df.groupby('class')
print(grouped)
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x7f5196c86710>

Note que objetos do tipo GroupBy não têm comportamento definido de impressão. Eles existem para facilitar operações agrupadas. No entanto, é possível iterar sobre os diferentes grupos:

for name_of_the_group, group in grouped:
    print(name_of_the_group)
    print(group)
    print()
bird
       class           order  max_speed
falcon  bird   Falconiformes      389.0
parrot  bird  Psittaciformes       24.0

mammal
          class      order  max_speed
lion     mammal  Carnivora       80.2
monkey   mammal   Primates        NaN
leopard  mammal  Carnivora       58.0
grouped = df.groupby('order', axis='columns')

for name_of_the_group, group in grouped:
    print(name_of_the_group)
    print(group)
    print()
grouped = df.groupby(['class', 'order'])

for name_of_the_group, group in grouped:
    print(name_of_the_group)
    print(group)
    print()
('bird', 'Falconiformes')
       class          order  max_speed
falcon  bird  Falconiformes      389.0

('bird', 'Psittaciformes')
       class           order  max_speed
parrot  bird  Psittaciformes       24.0

('mammal', 'Carnivora')
          class      order  max_speed
lion     mammal  Carnivora       80.2
leopard  mammal  Carnivora       58.0

('mammal', 'Primates')
         class     order  max_speed
monkey  mammal  Primates        NaN

Por padrão, as chaves dos grupos são ordenadas para realizar o agrupamento, mas é possível desativar esse comportamento usando sort=False para aumentar a performance da operação.

df2 = pd.DataFrame({'X': ['B', 'B', 'A', 'A'], 'Y': [1, 2, 3, 4]})

df2.groupby(['X']).sum()
Y
X
A 7
B 3
df2.groupby(['X'], sort=False).sum()
Y
X
B 3
A 7

Apesar de ordenar as chaves dos grupos por padrão, o agrupamento mantém a ordem original das observações em cada grupo.

df3 = pd.DataFrame({'X': ['A', 'B', 'A', 'B'], 'Y': [1, 4, 3, 2]})

df3.groupby(['X']).get_group('A')
X Y
0 A 1
2 A 3
df3.groupby(['X']).get_group('B')
X Y
1 B 4
3 B 2

Uma vez que um objeto GroupBy é criado, é possível usá-lo para realizar diversas operações de agregação de forma bastante eficiente, usando os métodos aggregate e agg.

arrays = [['bar', 'bar', 'baz', 'baz', 'foo', 'foo', 'qux', 'qux'],
          ['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two']]


index = pd.MultiIndex.from_arrays(arrays, names=['first', 'second'])
    
    
df = pd.DataFrame({'A': [1, 1, 1, 1, 2, 2, 3, 3],
                   'B': np.arange(8)},
                   index=index)

grouped = df.groupby('A')

grouped.aggregate(np.sum)
B
A
1 6
2 9
3 13
grouped.aggregate([np.sum, np.mean, np.std])
B
sum mean std
A
1 6 1.5 1.290994
2 9 4.5 0.707107
3 13 6.5 0.707107

Outra operação agregada simples pode ser feita usando o método size que retorna a quantidade de elementos em cada grupo:

grouped.size()
A
1    4
2    2
3    2
dtype: int64

É possível controlar o nome das colunas resultantes das operações de agregação por meio de um sintaxe especial no método agg(), chamada de agregação nomeada. Nesse caso, os argumentos serão os nomes das colunas resultantes e os valores dos argumentos serão tuplas, cujo primeiro elemento é a coluna a ser agregada e o segunda é a função de agregação a ser aplicada. A função de agregação pode ser passada como string ou pelo seu nome (e també pode ter sido criada pelo usuário).

animals = pd.DataFrame({'kind': ['cat', 'dog', 'cat', 'dog'],
                        'height': [9.1, 6.0, 9.5, 34.0],
                        'weight': [7.9, 7.5, 9.9, 198.0]})




animals.groupby("kind").agg(
    min_height=pd.NamedAgg(column='height', aggfunc='min'),
    max_height=pd.NamedAgg(column='height', aggfunc='max'),
    average_weight=pd.NamedAgg(column='weight', aggfunc=np.mean),
)
min_height max_height average_weight
kind
cat 9.1 9.5 8.90
dog 6.0 34.0 102.75

3.3.7. Lendo e escrevendo dados

Pandas consegue ler e escrever arquivos de diversos tipos, incluindo csv, excel, json, HDF5, SAS, SPSS e outros. Os tipos disponíveis podem ser visualizados na documentação de entrada e saída. De uma maneira geral, as funções de leitura tem assinatura read_tipo, e.g. read_csv, e podem ler arquivos locais ou remotos, enquanto os métodos de escrita tem assinatura to_tipo, e.g. to_csv. O código abaixo lê um arquivo csv hospedado em um repositório do Github e calcula as correlações entre as colunas. O parâmetro index_col indica qual coluna do arquivo csv deve ser usada como índice da tabela. Por padrão, supõe-se que cada coluna de um arquivo csv é separada por vírgulas. Isso pode ser modificado por meio do parâmetro sep.

url = 'https://tmfilho.github.io/pyestbook/data/google-trends-timeline.csv'
trends = pd.read_csv(url, index_col=0, parse_dates=True, sep=',')

trends
Python R Machine learning Data science
Week
2014-10-05 37 24 1 1
2014-10-12 37 24 1 1
2014-10-19 38 24 1 1
2014-10-26 37 24 1 1
2014-11-02 38 23 1 1
... ... ... ... ...
2019-08-25 88 22 8 6
2019-09-01 90 22 7 6
2019-09-08 97 24 8 6
2019-09-15 100 25 8 7
2019-09-22 100 25 8 6

260 rows × 4 columns

trends.corr(method='spearman')  # method é opcional
Python R Machine learning Data science
Python 1.000000 0.369220 0.964809 0.967090
R 0.369220 1.000000 0.287584 0.254663
Machine learning 0.964809 0.287584 1.000000 0.961119
Data science 0.967090 0.254663 0.961119 1.000000

Os dados lidos são compostos por séries temporais com 260 observações, que representam o interesse mundial por quatro tópicos de buscas no Google: Python, R, aprendizagem de máquina e ciência de dados. A quantidade de buscas de cada tópico foi medida semanalmente. O argumento parse_dates indica que as datas no índice da DataFrame devem ser tratadas como objetos datetime, não como strings.

trends.index
DatetimeIndex(['2014-10-05', '2014-10-12', '2014-10-19', '2014-10-26',
               '2014-11-02', '2014-11-09', '2014-11-16', '2014-11-23',
               '2014-11-30', '2014-12-07',
               ...
               '2019-07-21', '2019-07-28', '2019-08-04', '2019-08-11',
               '2019-08-18', '2019-08-25', '2019-09-01', '2019-09-08',
               '2019-09-15', '2019-09-22'],
              dtype='datetime64[ns]', name='Week', length=260, freq=None)
trends.index[0].weekday()
6

O argumento parse_dates pode receber diferentes valores, tendo comportamentos diferentes:

  1. boolean: Se True -> tentar tratar o índice;

  2. list de inteiros ou nomes: Se [1, 2, 3] -> tentar tratar colunas 1, 2 e 3 como colunas separadas de datas;

  3. list de lists: Se [[1, 3]] -> combinar colunas 1 e 3 e tratar como uma única coluna de datas;

  4. dict: Se {‘foo’ : [1, 3]} -> tratar colunas 1 e 3 como datas e chamar a coluna resultante de foo.

Se uma das colunas especificadas ou um índice possuir um valor não tratável ou uma mistura de fusos horários, seus valores serão retornados não-tratados com tipo object.

Para escrever uma DataFrame em um arquivo csv, pode-se usar o método to_csv:

trends.to_csv('google-trends-timeline.csv')

Se nenhum diretório for especificado, a DataFrame será escrita em um arquivo com o nome informado, localizado no mesmo diretório do script Python que chamou o método. Caso o método tenha sido chamado em um script interno de um projeto, o arquivo será salvo no mesmo diretório do script que contém o main.

3.3.8. Tabelas pivotadas

Pandas fornece a função pivot_table() para pivotar tabelas agregando dados numéricos. A ação de pivotar envolve um agrupamento (GroupBy) seguido da aplicação de função(ões) de agregação. A função pivot_table() recebe os seguintes argumentos:

  1. data: uma DataFrame.

  2. values: uma coluna ou lista de colunas cujos valores serão agregados.

  3. index: coluna(s), um objeto do tipo Grouper ou array que será usado como índice para as linhas da tabela resultante.

  4. columns: coluna(s), um objeto do tipo Grouper ou array que define as colunas da tabela resultante.

  5. aggfunc: função(ões) para realizar a agregação, com numpy.mean por padrão.

Exemplo:

import datetime

df = pd.DataFrame({'A': ['one', 'one', 'two', 'three'] * 6,
                   'B': ['A', 'B', 'C'] * 8,
                   'C': ['foo', 'foo', 'foo', 'bar', 'bar', 'bar'] * 4,
                   'D': np.random.randn(24),
                   'E': np.random.randn(24),
                   'F': [datetime.datetime(2013, i, 1) for i in range(1, 13)]
                   + [datetime.datetime(2013, i, 15) for i in range(1, 13)]})


df
A B C D E F
0 one A foo -0.326271 1.457352 2013-01-01
1 one B foo 0.231014 -1.417009 2013-02-01
2 two C foo 0.554309 2.091957 2013-03-01
3 three A bar 0.426194 -0.382060 2013-04-01
4 one B bar 1.257329 -1.241867 2013-05-01
5 one C bar -0.949541 1.438873 2013-06-01
6 two A foo -0.947266 -0.843566 2013-07-01
7 three B foo 0.657661 -1.577853 2013-08-01
8 one C foo -0.441156 -0.483276 2013-09-01
9 one A bar -1.676616 0.430094 2013-10-01
10 two B bar -0.763368 1.014398 2013-11-01
11 three C bar 1.238748 -0.596230 2013-12-01
12 one A foo 0.762141 -0.397035 2013-01-15
13 one B foo 1.357091 -0.023834 2013-02-15
14 two C foo -0.162096 -0.311615 2013-03-15
15 three A bar 0.375996 -0.428413 2013-04-15
16 one B bar -1.689584 2.386679 2013-05-15
17 one C bar 1.513322 -0.583729 2013-06-15
18 two A foo -0.320774 1.722535 2013-07-15
19 three B foo -0.225992 2.232583 2013-08-15
20 one C foo 1.767694 0.294606 2013-09-15
21 one A bar -1.904948 -1.136423 2013-10-15
22 two B bar -0.005716 -1.570005 2013-11-15
23 three C bar -0.397844 -0.207587 2013-12-15
pd.pivot_table(
    df, 
    values='D', 
    index=['A', 'B'], 
    columns=['C']
)
C bar foo
A B
one A -1.790782 0.217935
B -0.216128 0.794053
C 0.281891 0.663269
three A 0.401095 NaN
B NaN 0.215834
C 0.420452 NaN
two A NaN -0.634020
B -0.384542 NaN
C NaN 0.196107
pd.pivot_table(
    df, 
    values='D', 
    index=['B'], 
    columns=['A', 'C'], 
    aggfunc=np.sum
)
A one three two
C bar foo bar foo bar foo
B
A -3.581564 0.435870 0.802190 NaN NaN -1.268040
B -0.432255 1.588105 NaN 0.431668 -0.769084 NaN
C 0.563782 1.326537 0.840904 NaN NaN 0.392213
pd.pivot_table(
    df, 
    values=['D', 'E'], 
    index=['B'], 
    columns=['A', 'C'], 
    aggfunc=np.sum
)
D E
A one three two one three two
C bar foo bar foo bar foo bar foo bar foo bar foo
B
A -3.581564 0.435870 0.802190 NaN NaN -1.268040 -0.706328 1.060317 -0.810473 NaN NaN 0.878969
B -0.432255 1.588105 NaN 0.431668 -0.769084 NaN 1.144812 -1.440843 NaN 0.654729 -0.555607 NaN
C 0.563782 1.326537 0.840904 NaN NaN 0.392213 0.855144 -0.188670 -0.803816 NaN NaN 1.780342

O objeto resultante é uma DataFrame com índices e colunas potencialmente hieráquicos. Se o argumento values não for especificado, a tabela pivotada irá conter todas as colunas que podem ser agregadas em um nível adicional de hierarquia nas colunas:

pd.pivot_table(df, index=['A', 'B'], columns=['C'])
D E
C bar foo bar foo
A B
one A -1.790782 0.217935 -0.353164 0.530159
B -0.216128 0.794053 0.572406 -0.720421
C 0.281891 0.663269 0.427572 -0.094335
three A 0.401095 NaN -0.405236 NaN
B NaN 0.215834 NaN 0.327365
C 0.420452 NaN -0.401908 NaN
two A NaN -0.634020 NaN 0.439484
B -0.384542 NaN -0.277804 NaN
C NaN 0.196107 NaN 0.890171

Um objeto Grouper pode ser usado para os argumentos index e columns. No código abaixo, Grouper é usado para definir um agrupamento mensal das datas da coluna ‘F’. Para mais possibilidades de frequência e agrupamento, ver a documentação de Grouper.

pd.pivot_table(
    df, 
    values='D', 
    index=pd.Grouper(freq='M', key='F'),
    columns='C'
)
C bar foo
F
2013-01-31 NaN 0.217935
2013-02-28 NaN 0.794053
2013-03-31 NaN 0.196107
2013-04-30 0.401095 NaN
2013-05-31 -0.216128 NaN
2013-06-30 0.281891 NaN
2013-07-31 NaN -0.634020
2013-08-31 NaN 0.215834
2013-09-30 NaN 0.663269
2013-10-31 -1.790782 NaN
2013-11-30 -0.384542 NaN
2013-12-31 0.420452 NaN

Para imprimir a tabela resultante usando uma representação mais interessante para os dados faltantes, pode-se usar o método to_string:

table = pd.pivot_table(df, index=['A', 'B'], columns=['C'])

print(table.to_string(na_rep=''))
                D                   E          
C             bar       foo       bar       foo
A     B                                        
one   A -1.790782  0.217935 -0.353164  0.530159
      B -0.216128  0.794053  0.572406 -0.720421
      C  0.281891  0.663269  0.427572 -0.094335
three A  0.401095           -0.405236          
      B            0.215834            0.327365
      C  0.420452           -0.401908          
two   A           -0.634020            0.439484
      B -0.384542           -0.277804          
      C            0.196107            0.890171
print(table.to_string(na_rep='X'))
                D                   E          
C             bar       foo       bar       foo
A     B                                        
one   A -1.790782  0.217935 -0.353164  0.530159
      B -0.216128  0.794053  0.572406 -0.720421
      C  0.281891  0.663269  0.427572 -0.094335
three A  0.401095         X -0.405236         X
      B         X  0.215834         X  0.327365
      C  0.420452         X -0.401908         X
two   A         X -0.634020         X  0.439484
      B -0.384542         X -0.277804         X
      C         X  0.196107         X  0.890171

A função pivot_table() é frequentemente usada para agregar resultados de experimentos. Exemplo:

df = pd.DataFrame(
    {
        'accuracy': np.random.rand(40),
        'cross-entropy': np.random.rand(40) * 5,
        'method': ['LR'] * 10 + ['DT'] * 10 + ['SVM'] * 10 + ['NB'] * 10
    }
)


df
accuracy cross-entropy method
0 0.526352 3.662998 LR
1 0.147938 0.477259 LR
2 0.267650 1.063447 LR
3 0.527231 2.427523 LR
4 0.435128 4.425171 LR
5 0.444292 0.796132 LR
6 0.689321 0.315336 LR
7 0.095764 2.910655 LR
8 0.686488 3.477711 LR
9 0.935764 4.739121 LR
10 0.840590 3.122559 DT
11 0.914907 4.890066 DT
12 0.588449 2.890902 DT
13 0.529653 0.990773 DT
14 0.402867 2.422057 DT
15 0.764152 1.500235 DT
16 0.060231 1.506135 DT
17 0.877403 3.359640 DT
18 0.718945 1.649494 DT
19 0.199094 2.285115 DT
20 0.889822 1.216418 SVM
21 0.796074 4.528424 SVM
22 0.848134 0.380850 SVM
23 0.856210 2.388452 SVM
24 0.427196 1.755258 SVM
25 0.902870 3.935185 SVM
26 0.244625 0.552584 SVM
27 0.005992 4.112354 SVM
28 0.775665 2.401496 SVM
29 0.177399 4.838279 SVM
30 0.724646 0.305594 NB
31 0.877163 1.653998 NB
32 0.403407 2.919960 NB
33 0.943679 2.080387 NB
34 0.642419 0.772410 NB
35 0.217522 4.214960 NB
36 0.478655 3.336784 NB
37 0.281507 4.689718 NB
38 0.784900 3.149679 NB
39 0.747183 0.247242 NB
results = pd.pivot_table(
    df, 
    values=['accuracy', 'cross-entropy'], 
    index='method',
    aggfunc=[np.mean, np.std]
)

results
mean std
accuracy cross-entropy accuracy cross-entropy
method
DT 0.589629 2.461698 0.292507 1.155556
LR 0.475593 2.429535 0.259196 1.666125
NB 0.610108 2.337073 0.251263 1.582398
SVM 0.592399 2.610930 0.343067 1.654154

Após a geração da tabela pivotada com os resultados, pode-se gerar a tabela em formato de código LaTeX, para inclusão em artigos e relatórios:

results.to_latex()
'\\begin{tabular}{lrrrr}\n\\toprule\n{} & \\multicolumn{2}{l}{mean} & \\multicolumn{2}{l}{std} \\\\\n{} &  accuracy & cross-entropy &  accuracy & cross-entropy \\\\\nmethod &           &               &           &               \\\\\n\\midrule\nDT     &  0.589629 &      2.461698 &  0.292507 &      1.155556 \\\\\nLR     &  0.475593 &      2.429535 &  0.259196 &      1.666125 \\\\\nNB     &  0.610108 &      2.337073 &  0.251263 &      1.582398 \\\\\nSVM    &  0.592399 &      2.610930 &  0.343067 &      1.654154 \\\\\n\\bottomrule\n\\end{tabular}\n'

O retorno de to_latex() inclui barras duplas e quebras de linhas com \n pois elas são necessárias para retornar barras dentro de strings Python.

\begin{tabular}{lrrrr} \toprule {} & \multicolumn{2}{l}{mean} & \multicolumn{2}{l}{std} \ {} & accuracy & cross-entropy & accuracy & cross-entropy \ method & & & & \ \midrule DT & 0.365762 & 2.227704 & 0.326730 & 1.327534 \ LR & 0.314544 & 2.995433 & 0.250636 & 1.146052 \ NB & 0.439088 & 2.596552 & 0.286103 & 1.748327 \ SVM & 0.631967 & 2.153559 & 0.296118 & 1.059571 \ \bottomrule \end{tabular}

3.3.9. Reindexando e alterando rótulos

Suponha que você tem dados provenientes de fontes diferentes. As tabelas tem colunhas e linhas em comum e outras diferentes. Você quer trabalhar com os dados em comum, e preencher os valores faltantes de alguma forma. Além disso, você quer que os dados estejam dispostos em uma ordenação específica através dos índices e das colunas. Tudo isso pode ser realizado por meio do método reindex(), que é a forma principal de “realinhar” dados em pandas. Reindexar significa ajustar os dados a um dado conjuntode rótulos através de um eixo. Isso permite reordenar os dados existentes, inserir posições faltantes onde não existem dados para os rótulos passados e preencher esses dados faltantes. Exemplo:

s = pd.Series(
    np.random.randn(5), 
    index=['a', 'b', 'c', 'd', 'e']
)

s
a   -1.360823
b    1.587918
c    0.015884
d    2.056704
e   -2.511190
dtype: float64
s.reindex(['e', 'b', 'f', 'd'])
e   -2.511190
b    1.587918
f         NaN
d    2.056704
dtype: float64

Note que o rótulo ‘f’ não estava presente na Series, aparecendo como NaN no resultado. No caso de DataFrames, é possível reindexar índices e colunas simultaneamente:

df = pd.DataFrame({
    'one': pd.Series(np.random.randn(3), index=['a', 'b', 'c']),
    'two': pd.Series(np.random.randn(4), index=['a', 'b', 'c', 'd']),
    'three': pd.Series(np.random.randn(3), index=['b', 'c', 'd'])
})

df
one two three
a -0.704751 -1.085537 NaN
b 0.886696 -0.737286 0.251083
c -0.794707 -0.212274 0.545992
d NaN -0.077634 -0.127004
df.reindex(
    index=['c', 'f', 'b'], 
    columns=['three', 'two', 'one']
)
three two one
c 0.545992 -0.212274 -0.794707
f NaN NaN NaN
b 0.251083 -0.737286 0.886696

O método reindex() também pode ser usado com o argumento axis:

df.reindex(['c', 'f', 'b'], axis='index')
one two three
c -0.794707 -0.212274 0.545992
f NaN NaN NaN
b 0.886696 -0.737286 0.251083
df.reindex(['c', 'f', 'b'], axis='columns')
c f b
a NaN NaN NaN
b NaN NaN NaN
c NaN NaN NaN
d NaN NaN NaN
df.reindex(['three', 'two', 'one'], axis='columns')
three two one
a NaN -1.085537 -0.704751
b 0.251083 -0.737286 0.886696
c 0.545992 -0.212274 -0.794707
d -0.127004 -0.077634 NaN
df.reindex(['three', 'two', 'one'], axis='index')
one two three
three NaN NaN NaN
two NaN NaN NaN
one NaN NaN NaN

Objetos do tipo Index podem ser compartilhados entre objetos Series e DataFrame por meio do método reindex:

rs = s.reindex(df.index)

rs
a   -1.360823
b    1.587918
c    0.015884
d    2.056704
dtype: float64
rs.index is df.index
True

Para reindexar um objeto, de forma que ele fique perfeitamente “alinhado” com outro, pode-se usar o “atalho” reindex_like():

df
one two three
a -0.704751 -1.085537 NaN
b 0.886696 -0.737286 0.251083
c -0.794707 -0.212274 0.545992
d NaN -0.077634 -0.127004
df2 = pd.DataFrame({
    'two': pd.Series(np.random.randn(3), index=['a', 'b', 'c']),
    'one': pd.Series(np.random.randn(3), index=['a', 'b', 'c']),
})

df2
two one
a -0.608458 -0.450257
b -0.409285 0.714278
c 0.314374 0.386187
df.reindex_like(df2)
two one
a -1.085537 -0.704751
b -0.737286 0.886696
c -0.212274 -0.794707

A operação de reindexação torna-se mais importante quando é necessário escrever códigos cujo desempenho é fator essencial. Diversas operações são realizadas mais rapidamente sobre dados pré-alinhados, como as operações aritméticas, que precisam disparar um realinhamento interno, se os dados não estiverem alinhados.

Caso seja interessante alinhar dois objeto simultaneamente, pode-se usar o método align (ao invés de duas chamadas ao método reindex). O método align retorna uma tupla com os dois objetos realinhados e aceita o argumento join para indicar a forma como os objetos serão alinhados:

  1. join=’outer’: toma a união dos índices (default);

  2. join=’left’: usa o índice do objeto que chama o método;

  3. join=’right’: usa o índice do objeto passado como parâmetro;

  4. join=’inner’: toma a interseção do síndices.

Exemplos:

s = pd.Series(
    np.random.randn(5), 
    index=['a', 'b', 'c', 'd', 'e']
)

s1 = s[:4]
s2 = s[1:]
s1.align(s2)
(a    0.051467
 b    1.130306
 c    0.488664
 d   -0.795861
 e         NaN
 dtype: float64,
 a         NaN
 b    1.130306
 c    0.488664
 d   -0.795861
 e   -0.332046
 dtype: float64)
s1.align(s2, join='inner')
(b    1.130306
 c    0.488664
 d   -0.795861
 dtype: float64,
 b    1.130306
 c    0.488664
 d   -0.795861
 dtype: float64)
s1.align(s2, join='left')
(a    0.051467
 b    1.130306
 c    0.488664
 d   -0.795861
 dtype: float64,
 a         NaN
 b    1.130306
 c    0.488664
 d   -0.795861
 dtype: float64)

Para DataFrames, o join é aplicado aos índices e às colunas por padrão:

df
one two three
a -0.704751 -1.085537 NaN
b 0.886696 -0.737286 0.251083
c -0.794707 -0.212274 0.545992
d NaN -0.077634 -0.127004
df2
two one
a -0.608458 -0.450257
b -0.409285 0.714278
c 0.314374 0.386187
df.align(df2, join='inner')
(        one       two
 a -0.704751 -1.085537
 b  0.886696 -0.737286
 c -0.794707 -0.212274,
         one       two
 a -0.450257 -0.608458
 b  0.714278 -0.409285
 c  0.386187  0.314374)

Usando o argumento axis, é possível especificar a qual eixo o join deve ser aplicado:

df.align(df2, join='inner', axis=0)
(        one       two     three
 a -0.704751 -1.085537       NaN
 b  0.886696 -0.737286  0.251083
 c -0.794707 -0.212274  0.545992,
         two       one
 a -0.608458 -0.450257
 b -0.409285  0.714278
 c  0.314374  0.386187)

Ao passar uma Series ao método align de uma DataFrame,é possível escolher alinhas ambos os objetos de acordo com o índice ou com as colunas da DataFrame, usando o argumento axis:

df.align(df2.iloc[0], axis=1)
(        one     three       two
 a -0.704751       NaN -1.085537
 b  0.886696  0.251083 -0.737286
 c -0.794707  0.545992 -0.212274
 d       NaN -0.127004 -0.077634,
 one     -0.450257
 three         NaN
 two     -0.608458
 Name: a, dtype: float64)

3.3.9.1. Preenchendo valores faltantes

O método reindex pode receber um parâmetro opcional que determina uma forma de preencher dados faltantes. Os valores possíveis para esse argumento são:

  1. pad/ffill: preenche os valores para a frente;

  2. bfill/backfill: preenche os valores para trás;

  3. nearest: preenche com os valores do índice mais próximo.

Exemplos:

rng = pd.date_range('1/3/2000', periods=8)

ts = pd.Series(np.random.randn(8), index=rng)

ts2 = ts[[0, 3, 6]]

ts
2000-01-03   -1.721162
2000-01-04   -0.955864
2000-01-05   -1.223125
2000-01-06   -0.615997
2000-01-07   -1.603720
2000-01-08   -1.707942
2000-01-09   -0.231214
2000-01-10   -1.866278
Freq: D, dtype: float64
ts2
2000-01-03   -1.721162
2000-01-06   -0.615997
2000-01-09   -0.231214
dtype: float64
ts2.reindex(ts.index)
2000-01-03   -1.721162
2000-01-04         NaN
2000-01-05         NaN
2000-01-06   -0.615997
2000-01-07         NaN
2000-01-08         NaN
2000-01-09   -0.231214
2000-01-10         NaN
Freq: D, dtype: float64
ts2.reindex(ts.index, method='ffill')
2000-01-03   -1.721162
2000-01-04   -1.721162
2000-01-05   -1.721162
2000-01-06   -0.615997
2000-01-07   -0.615997
2000-01-08   -0.615997
2000-01-09   -0.231214
2000-01-10   -0.231214
Freq: D, dtype: float64
ts2.reindex(ts.index, method='bfill')
2000-01-03   -1.721162
2000-01-04   -0.615997
2000-01-05   -0.615997
2000-01-06   -0.615997
2000-01-07   -0.231214
2000-01-08   -0.231214
2000-01-09   -0.231214
2000-01-10         NaN
Freq: D, dtype: float64
ts2.reindex(ts.index, method='nearest')
2000-01-03   -1.721162
2000-01-04   -1.721162
2000-01-05   -0.615997
2000-01-06   -0.615997
2000-01-07   -0.615997
2000-01-08   -0.231214
2000-01-09   -0.231214
2000-01-10   -0.231214
Freq: D, dtype: float64

Note que esses métodos de preenchimento precisam que os índices estejam ordenados (em ordem ascendente ou decrescente). Caso seja desejável limitar o preenchimento de dados faltantes, para evitar que valores muito distantes sejam usados, pode-se usar os argumentos limit e tolerance. Limit especifica o número máximo de preenchimentos consecutivos:

ts2.reindex(ts.index, method='ffill', limit=1)
2000-01-03   -1.721162
2000-01-04   -1.721162
2000-01-05         NaN
2000-01-06   -0.615997
2000-01-07   -0.615997
2000-01-08         NaN
2000-01-09   -0.231214
2000-01-10   -0.231214
Freq: D, dtype: float64

Tolerance especifica o intervalo máximo entre o índice faltante e o índice que será usado para preenchê-lo.

ts2.reindex(ts.index, method='ffill', tolerance='1 day')
2000-01-03   -1.721162
2000-01-04   -1.721162
2000-01-05         NaN
2000-01-06   -0.615997
2000-01-07   -0.615997
2000-01-08         NaN
2000-01-09   -0.231214
2000-01-10   -0.231214
Freq: D, dtype: float64

Resultados parecidos de preenchimento podem ser obtidos pelo método fillna:

ts2.reindex(ts.index).fillna(method='ffill')
2000-01-03   -1.721162
2000-01-04   -1.721162
2000-01-05   -1.721162
2000-01-06   -0.615997
2000-01-07   -0.615997
2000-01-08   -0.615997
2000-01-09   -0.231214
2000-01-10   -0.231214
Freq: D, dtype: float64

O método fillna pode ser bastante útil por permitir o preenchimento usando um objeto pandas ou um dicionário que seja alinhável, ou seja, as chaves do dicionário ou índice da Series precisam se ajustar às colunas da DataFrame que será preenchida. Um caso de uso muito comum para isso é preencher dados faltantes com a média ou a mediana das colunas correspondentes:

dff = pd.DataFrame(np.random.randn(10, 3), columns=list('ABC'))

dff.iloc[3:5, 0] = np.nan

dff.iloc[4:6, 1] = np.nan

dff.iloc[5:8, 2] = np.nan

dff
A B C
0 -0.891958 -1.305684 1.724160
1 0.189915 -0.408208 -0.563463
2 -0.351267 -1.211801 0.359443
3 NaN 0.448340 -0.912656
4 NaN NaN -0.750471
5 0.161379 NaN NaN
6 -1.174634 -1.078165 NaN
7 -0.949707 -0.638253 NaN
8 0.672524 -0.077935 0.796112
9 1.522327 0.705898 -0.086618
dff.mean()
A   -0.102678
B   -0.445726
C    0.080930
dtype: float64
dff.fillna(dff.mean())
A B C
0 -0.891958 -1.305684 1.724160
1 0.189915 -0.408208 -0.563463
2 -0.351267 -1.211801 0.359443
3 -0.102678 0.448340 -0.912656
4 -0.102678 -0.445726 -0.750471
5 0.161379 -0.445726 0.080930
6 -1.174634 -1.078165 0.080930
7 -0.949707 -0.638253 0.080930
8 0.672524 -0.077935 0.796112
9 1.522327 0.705898 -0.086618
dff.fillna(dff.median())
A B C
0 -0.891958 -1.305684 1.724160
1 0.189915 -0.408208 -0.563463
2 -0.351267 -1.211801 0.359443
3 -0.094944 0.448340 -0.912656
4 -0.094944 -0.523230 -0.750471
5 0.161379 -0.523230 -0.086618
6 -1.174634 -1.078165 -0.086618
7 -0.949707 -0.638253 -0.086618
8 0.672524 -0.077935 0.796112
9 1.522327 0.705898 -0.086618
dff.fillna(dff.mean()['B':'C'])
A B C
0 -0.891958 -1.305684 1.724160
1 0.189915 -0.408208 -0.563463
2 -0.351267 -1.211801 0.359443
3 NaN 0.448340 -0.912656
4 NaN -0.445726 -0.750471
5 0.161379 -0.445726 0.080930
6 -1.174634 -1.078165 0.080930
7 -0.949707 -0.638253 0.080930
8 0.672524 -0.077935 0.796112
9 1.522327 0.705898 -0.086618

Dados faltantes também podem ser preenchidos usando interpolação, por meio do método interpolate:

rng = pd.date_range('1/3/2000', periods=100)

ts2 = pd.Series(np.random.randn(100), index=rng)

ts2[np.random.choice(100, size=34, replace=False)] = np.nan

ts2
2000-01-03   -0.501421
2000-01-04         NaN
2000-01-05         NaN
2000-01-06    0.764049
2000-01-07         NaN
                ...   
2000-04-07   -0.666162
2000-04-08   -0.592550
2000-04-09    0.690377
2000-04-10    0.146580
2000-04-11    0.634842
Freq: D, Length: 100, dtype: float64
ts2.count()
66
ts2.plot()
<matplotlib.axes._subplots.AxesSubplot at 0x7f51961a29b0>
../_images/04_pand_269_1.png
ts3 = ts2.interpolate()

ts3
2000-01-03   -0.501421
2000-01-04   -0.079598
2000-01-05    0.342226
2000-01-06    0.764049
2000-01-07    0.579077
                ...   
2000-04-07   -0.666162
2000-04-08   -0.592550
2000-04-09    0.690377
2000-04-10    0.146580
2000-04-11    0.634842
Freq: D, Length: 100, dtype: float64
ts3.count()
100
ts3.plot()
<matplotlib.axes._subplots.AxesSubplot at 0x7f51957ebc50>
../_images/04_pand_272_1.png

O método interpolate pode levar em consideração os valores dos índices (caso eles não sejam regularmente intervalados):

ser = pd.Series([0.0, np.nan, 10], index=[0., 1., 10.])

ser
0.0      0.0
1.0      NaN
10.0    10.0
dtype: float64
ser.interpolate()
0.0      0.0
1.0      5.0
10.0    10.0
dtype: float64
ser.interpolate(
    method='values'
)
0.0      0.0
1.0      1.0
10.0    10.0
dtype: float64

Também é possível interpolar DataFrames:

dfi = pd.DataFrame({'A': [1, 2.1, np.nan, 4.7, 5.6, 6.8],
                   'B': [.25, np.nan, np.nan, 4, 12.2, 14.4]})

dfi
A B
0 1.0 0.25
1 2.1 NaN
2 NaN NaN
3 4.7 4.00
4 5.6 12.20
5 6.8 14.40
dfi.interpolate()
A B
0 1.0 0.25
1 2.1 1.50
2 3.4 2.75
3 4.7 4.00
4 5.6 12.20
5 6.8 14.40

O argumento method flexibiliza interpolate para poder usar diferentes métodos de interpolação:

np.random.seed(2)

ser = pd.Series(np.arange(1, 10.1, .25) ** 2 + np.random.randn(37))

missing = np.array([4, 13, 14, 15, 16, 17, 18, 20, 29])

ser[missing] = np.nan

methods = ['linear', 'quadratic', 'cubic']

dfi = pd.DataFrame({m: ser.interpolate(method=m) for m in methods})

dfi.plot()
<matplotlib.axes._subplots.AxesSubplot at 0x7f517c4d3da0>
../_images/04_pand_281_1.png

3.3.10. Removendo rótulos

Caso a intenção ao usar reindex seja apenas remover certos rótulos, o ideal é usar o método drop:

df
one two three
a -0.704751 -1.085537 NaN
b 0.886696 -0.737286 0.251083
c -0.794707 -0.212274 0.545992
d NaN -0.077634 -0.127004
df.drop(['a', 'd'], axis=0)
one two three
b 0.886696 -0.737286 0.251083
c -0.794707 -0.212274 0.545992
df.reindex(
    df.index.difference(
        ['a', 'd']
    )
)  # o mesmo resultado, mas usando reindex
one two three
b 0.886696 -0.737286 0.251083
c -0.794707 -0.212274 0.545992
df.drop(['one'], axis=1)
two three
a -1.085537 NaN
b -0.737286 0.251083
c -0.212274 0.545992
d -0.077634 -0.127004

3.3.11. Renomeando rótulos

Para renomear índices e colunas, pandas fornece o método rename(), que pode receber como parâmetros um dicionário, uma Series ou uma função. Caso uma função seja usada, ela deve retornar um valor único e válido para cada rótulo renomeado.

s
a    0.051467
b    1.130306
c    0.488664
d   -0.795861
e   -0.332046
dtype: float64
s.rename(str.upper)
A    0.051467
B    1.130306
C    0.488664
D   -0.795861
E   -0.332046
dtype: float64

Ao renomear usando um mapeamento, os rótulos que não forem especificados não são renomeados. Além disso, se o mapeamento contiver rótulos inexistentes, eles são apenas ignorados sem erro.

df
one two three
a -0.704751 -1.085537 NaN
b 0.886696 -0.737286 0.251083
c -0.794707 -0.212274 0.545992
d NaN -0.077634 -0.127004
df.rename(
    columns={'one': 'foo', 'two': 'bar'},
    index={'a': 'apple', 'b': 'banana', 'd': 'durian'}
)
foo bar three
apple -0.704751 -1.085537 NaN
banana 0.886696 -0.737286 0.251083
c -0.794707 -0.212274 0.545992
durian NaN -0.077634 -0.127004

3.3.12. Substituindo valores

Frequentemente é necessário substituir certos valores em uma DataFrame ou Series. O método replace é uma forma simples e eficiente de realizar essa operação. Em uma Series, é possível substituir um único valor ou uma lista de valores por outros valores:

ser = pd.Series([0., 1., 2., 3., 4.])

ser.replace(0, 5)
0    5.0
1    1.0
2    2.0
3    3.0
4    4.0
dtype: float64
ser.replace([0, 1, 2, 3, 4], [4, 3, 2, 1, 0])
0    4.0
1    3.0
2    2.0
3    1.0
4    0.0
dtype: float64

Também é possível especificar um dicionário de substituições:

ser.replace({0: 10, 1: 100})
0     10.0
1    100.0
2      2.0
3      3.0
4      4.0
dtype: float64

Para DataFrames, é possível determinar substituições por coluna:

df = pd.DataFrame({'a': [0, 1, 2, 3, 4], 'b': [5, 6, 7, 8, 9]})

df.replace({'a': 0, 'b': 5}, 100)
a b
0 100 100
1 1 6
2 2 7
3 3 8
4 4 9
df.replace({'a': [0, 2], 'b': 5}, 100)
a b
0 100 100
1 1 6
2 100 7
3 3 8
4 4 9

Ao invés de substituir por valores específicos, é possível tratar todos os valores dados como faltantes e preencher ou interpolar:

ser
0    0.0
1    1.0
2    2.0
3    3.0
4    4.0
dtype: float64
ser.replace([2, 3], method='ffill')
0    0.0
1    1.0
2    1.0
3    1.0
4    4.0
dtype: float64

3.3.13. Aplicando funções

Funções arbitrárias podem ser aplicadas a um eixo de uma DataFrame usando o método apply(), que recebe um argumento opcional de eixo (axis).

df = pd.DataFrame({
    'one': pd.Series(np.random.randn(3), index=['a', 'b', 'c']),
    'two': pd.Series(np.random.randn(4), index=['a', 'b', 'c', 'd']),
    'three': pd.Series(np.random.randn(3), index=['b', 'c', 'd'])
})

df
one two three
a -0.844214 -0.313508 NaN
b 0.000010 0.771012 1.467678
c 0.542353 -1.868091 -0.335677
d NaN 1.731185 0.611341
df.apply(np.mean)
one     -0.100617
two      0.080149
three    0.581114
dtype: float64
df.mean()  # equivalente ao resultado acima
one     -0.100617
two      0.080149
three    0.581114
dtype: float64
df.apply(np.mean, axis=1)
a   -0.578861
b    0.746233
c   -0.553805
d    1.171263
dtype: float64
df.mean(axis=1)  # equivalente ao resultado acima
a   -0.578861
b    0.746233
c   -0.553805
d    1.171263
dtype: float64
df.apply(lambda x: x.max() - x.min())
one      1.386566
two      3.599275
three    1.803355
dtype: float64
df.apply(np.exp)
one two three
a 0.429895 0.730878 NaN
b 1.000010 2.161952 4.339148
c 1.720049 0.154418 0.714854
d NaN 5.647340 1.842901

O tipo de retorno da função usada no apply() influencia o tipo da saída. Se a função aplicada retornar uma Series, o resultado será uma DataFrame, caso contrário, o resultado será uma Series. O método apply pode ser usado de formas criativas para responder perguntas sobre um conjunto de dados. Por exemplo: suponha que queiramos saber a data onde o valor mínimo ocorreu para cada coluna:

tsdf = pd.DataFrame(
    np.random.randn(1000, 3), 
    columns=['A', 'B', 'C'],
    index=pd.date_range('1/1/2000', 
                        periods=1000)
)


tsdf.apply(lambda x: x.idxmax())
A   2000-10-26
B   2000-02-10
C   2001-02-25
dtype: datetime64[ns]

Também pode ser útil passar argumentos posicionais ou nomeados para o método apply. Exemplo:

def subtract_and_divide(x, sub, divide=1):
    return (x - sub) / divide
df.apply(subtract_and_divide, args=(5,), divide=3)
one two three
a -1.948071 -1.771169 NaN
b -1.666663 -1.409663 -1.177441
c -1.485882 -2.289364 -1.778559
d NaN -1.089605 -1.462886

Por último, apply pode receber o argumento raw, que é False por padrão. Caso ele seja passado como True, cada linha ou coluna é convertido para um objeto array de NumPy antes de realizar as operações o que pode trazer um impacto positivo de performance, caso as funcionalidades de indexação sejam desnecessárias.