Dataframes, preparação de dados


Preparação de dados

Programadores que lidam com análise de dados passam grande parte do tempo dedicado a um projeto preparando esses dados, antes mesmo de começar qualquer análise. Normalmente os dados são importados de uma fonte externa, tal como um arquivo em forma tabular em html, pdf, texto puro ou csv. Eles precisam ser convertidos para um formato legível e muitas vezes contém erros e valores ausentes. A vezes o próprio processo de conversão introduz perda de dados, tal como acontece em textos impressos transformados em texto digital por OCR (optical character recognition ). Seja qual for a origem dos dados algum trabalho de depuração deve ser feito. Em seguida eles devem passar por formatação adequada, a quebra de tabelas, o estabelecimento de vínculos entre elas, etc. Pandas oferece boas ferramentas para todas essas etapas.

Dados ausentes

Já vimos que dados não presentes em alguma tabela são representados por NaN (not a number). O objeto None do python também é tratado como um valor ausente ou NA (not available). O método dropna() descarta linhas (se axis=0, default) ou colunas (se axis=1) contendo campos nulos. dropna(how='all') descarta linhas ou colunas se todos os campos forem nulos. Também podemos determinar que apenas linhas ou colunas com um número mínimo de elementos não nulos sejam mantidas, com df.dropna(thresh=n).

» import pandas as pd
» import numpy as np

» dados = pd.Series([121.45, np.nan ,32.12,42.21,51.56])
» dados
↳ 0    121.45
  1       NaN
  2     32.12
  3     42.21
  4     51.56

» dados[3]= None
» dados.isnull()
↳ 0    False
  1     True
  2    False
  3     True
  4    False

» dados.dropna()                   # o mesmo que dados[dados.notnull()]
↳ 0    121.45
  2     32.12
  4     51.56

» from numpy import nan as NA     # para estabelecer um alias curto para np.nan
» data = pd.DataFrame([[1., 6.5, 3.9], [1.3, NA, NA], [NA, NA, NA], [NA, 5.8, 6.7]])
» data
↳        0      1      2
  0    1.0    6.5    3.9
  1    1.3    NaN    NaN
  2    NaN    NaN    NaN
  3    NaN    5.8    6.7

» data.dropna()
↳        0      1      2
  0    1.0    6.5    3.9

» data.dropna(how='all')
↳        0      1      2
  0    1.0    6.5    3.9
  1    1.3    NaN    NaN
  3    NaN    5.8    6.7

» data[4] = NA
» data
↳        0      1      2      4
  0    1.0    6.5    3.9    NaN
  1    1.3    NaN    NaN    NaN
  2    NaN    NaN    NaN    NaN
  3    NaN    5.8    6.7    NaN

» data.dropna(axis=1, how='all')
↳        0      1      2
  0    1.0    6.5    3.9
  1    1.3    NaN    NaN
  2    NaN    NaN    NaN
  3    NaN    5.8    6.7

Preenchendo valores ausentes

A invés de descartar linhas e colunas com campos ausentes podemos preencher estas lacunas. df.fillna(const) substitui campos NA com o valor único const. Um dicionário {coluna:valor} pode ser passado contendo constantes diferentes para cada coluna. Observando que df.mean() retorna uma Series com as médias de cada colunas, podemos usar df.fillna(df.mean()) para preencer NAs de cada coluna com essa média. Também podemos passar o parâmetro df.fillna(method='ffill') para preencher cada NA com o valor que o antecede na coluna. df.fillna(method='bfill') preenche NAs com o valor que o segue.

» # criando um df de teste com campos NA
» df = pd.DataFrame(np.random.randn(4, 3))
» df.iloc[0:3, 1] = NA
» df.iloc[1:3, 2] = NA

» df
↳             0           1            2
  0    0.615016         NaN    -0.860821
  1    1.195041         NaN          NaN
  2   -0.110482         NaN          NaN
  3    1.837690    1.569459     0.891858

» # preenche NAs com 0
» df.fillna(0)
↳             0           1           2
  0    0.615016    0.000000   -0.860821
  1    1.195041    0.000000    0.000000
  2   -0.110482    0.000000    0.000000
  3    1.837690    1.569459    0.891858

» # preenche coluna 1 com 10, coluna 2 com 20
» df.fillna({1:10, 2:20})
↳             0            1            2
  0    0.615016    10.000000    -0.860821
  1    1.195041    10.000000    20.000000
  2   -0.110482    10.000000    20.000000
  3    1.837690     1.569459     0.891858

» df.fillna(method='ffill')
↳             0          1            2
  0    0.615016        NaN    -0.860821
  1    1.195041        NaN    -0.860821
  2   -0.110482        NaN    -0.860821
  3    1.837690   1.569459     0.891858

» df.fillna(method='bfill')
↳             0           1           2
  0    0.615016    1.569459   -0.860821
  1    1.195041    1.569459    0.891858
  2   -0.110482    1.569459    0.891858
  3    1.837690    1.569459    0.891858

» df.fillna(method='bfill', limit=2)
» df.mean()
↳ 0    0.884316
  1    1.569459
  2    0.015519

» df.fillna(df.mean())
↳             0           1           2
  0    0.615016         NaN   -0.860821
  1    1.195041    1.569459    0.891858
  2   -0.110482    1.569459    0.891858
  3    1.837690    1.569459    0.891858

Vemos que df.fillna(method='ffill') não substituiu valores nas linhas 0, 1, 2 da coluna 1 pois nenhum valor os antecede. Nesse caso teríamos que usar method='bfill', ou outra forma de preencher o campo vazio.

Substituições com dataframe.replace()

O método df.replace() substitui valores específicos em uma Series ou dataframe. Por ex., suponha que temos uma Series de valores positivos e a inserção de negativos foi convencionada para indicar valores ausentes. Podemos alterar esses valores usando df.replace(), lembrando que nenhuma das formas abaixo altera a Serie original, a menos que inplace=True seja usado.

» serie = pd.Series([12,-2, 34, -1])
» serie
↳ 0    12
  1    -2
  2    34
  3    -1

» serie.replace(-2, -90)
↳ 0    12
  1   -90
  2    34
  3    -1

» serie.replace([-2,-1], [20,10])
↳ 0    12
  1    20
  2    34
  3    10

» serie.replace(-1, NA)
↳ 0    12.0
  1    -2.0
  2    34.0
  3     NaN

Claro que df.replace() pode ser usado para substituir um valor específico por valores calculados, usando métodos mais sofisticados de avaliação.

Em um dataframe df.replace(lista1, lista2) pode ser usado para substituir valores da lista1 pelos da lista2 (que deve ter o mesmo tamanho). df.replace(lista, escalar) substitui todos os valores em lista pelo escalar e df.replace(dicionario) substitui as chaves pelas valores no dicionário.

» df = pd.DataFrame({'a':[9,56,67], 'b':[33,55,66], 'c':[63,69,67], 'd':[2,3,9]})
» df
↳       a     b     c    d
  0     9    33    63    2
  1    56    55    69    3
  2    67    66    67    9

» df.replace(9, 100)
↳        a     b     c     d
  0    100    33    63     2
  1     56    55    69     3
  2     67    66    67   100

» df.replace([9, 55, 67], 0)
↳      a     b     c    d
  0    0    33    63    2
  1   56     0    69    3
  2    0    66     0    0

» df.replace([9, 55, 67], [1,2,3])
↳      a     b     c    d
  0    1    33    63    2
  1   56     2    69    3
  2    3    66     3    1

» df.replace({9:-9, 33:-33})
↳       a      b     c    d
  0    -9    -33    63    2
  1    56     55    69    3
  2    67     66    67   -9

Análise de outliers

Em qualquer processo de tomada de medidas ou coleta de dados existem restrições à precisão obtida. Mas, além da precisão restrita, é frequente existirem dados muito fora de qualquer curva esperada. Esses são os chamados pontos fora da curva ou outliers e geralmente são descartados. Os critérios de decisão sobre quais pontos são outliers dependem do modelo que se quer tratar.

No pandas podemos encontrar valores que estão acima ou abaixo de um certo limite.

Lembrando que np.random.randn(M, p) retorna um array de p colunas, cada uma com M valores, retirados aleatoriamente de uma distribuição normal com média 0 e variância 1, começamos por coletar um dataframe para testes.

Considerando os máximos e mínimos exibidos, vamos estabelecer arbitrariamente que valores afastados acima de 3 da média do conjunto são outliers. Isso quer dizer que consideraremos os pontos com |x| > 3 como outliers (onde |x| significa valor absoluto de x). Uma das possibilidades consiste em substituir valores não aceitáveis por np.nan e depois usar uma das formas de fill para preencher esses campos.

» dados = pd.DataFrame(np.random.randn(1000, 4))
» # são os valores mínimo e máximo desse dataframe
» dados.min().min(), dados.max().max()
↳ (-3.7113843289590496, 3.480659301328407)

» # substituimos |x| > 3 por np.nan
» dados[np.abs(dados) > 3] = np.nan
» dados.describe()    # (1) visualização do dataframe (alguns campos exibidos)
↳                   0             1             2            3
  count    996.000000    997.000000    998.000000   996.000000
  mean       0.086548      0.021479     -0.046291     0.019611
  min       -2.772219     -2.860741     -2.763174    -2.644022
  max        2.763864      2.849207      2.955914     2.905516

» dados = dados.fillna(method='bfill')
» dados.describe()    # (2) visualização do dataframe (alguns campos exibidos)
↳                    0              1              2              3
  count    1000.000000    1000.000000    1000.000000    1000.000000
  mean        0.089292       0.021845      -0.046568       0.020410
  min        -2.772219      -2.860741      -2.763174      -2.644022
  max         2.763864       2.849207       2.955914       2.905516

No primeiro uso de describe a contagem count mostra que existem linhas com campos nulos para cada coluna. Após a operação de fill todos os campos são numéricos.

Removendo linhas duplicadas

Para remover linhas duplicadas em um dataframe usamos df.drop_duplicates(). Valores duplicados em apenas uma coluna podem ser removidos com df.drop_duplicates('nomeColuna'), ou em várias colunas, passando-se uma lista df.drop_duplicates(['col1',..., 'coln']). Por default a primeira linhas, entre as duplicadas é mantida. Para manter a última usamos df.drop_duplicates('coluna', keep='last').

» # remoção de linhas duplicadas
» dic ={'col1': ['vaca', 'vaca', 'pato','pato'], 'col2': [1, 3, 4, 4]} 
» df = pd.dfFrame(dic)
» df
↳      col1    col2
  0    vaca    1
  1    vaca    3
  2    pato    4
  3    pato    4

» df.duplicated()           # retorna uma Series mostrando linhas duplicadas
↳ 0    False
  1    False
  2    False
  3     True

» df.drop_duplicates()
↳      col1   col2
  0    vaca      1
  1    vaca      3
  2    pato      4

» df.drop_duplicates('col1')
↳      col1  col2
  0    vaca     1
  2    pato     4

» df.drop_duplicates('col1', keep='last')
↳      col1   col2
  1    vaca     3
  3    pato     4

No atual estado de Pandas não é possível fazer a remoção de duplicadas sobre colunas. Para isso obtenha a transposta do dataframe, remova linhas duplicadas e o transponha novamente.

Transformações sobre elementos de um dataframe

Um restaurante faz uma lista de aquisição de produtos, descrevendo o ítem e quantas unidades devem se adquiridas.

» compra = {'produto':['leite', 'manteiga', 'laranja', 'arroz'],
            'quantos':[15, 40,50, 30]} 
» dfComprar = pd.DataFrame(compra)
» dfComprar
↳      produto  quantos
  0      leite       15
  1   manteiga       40
  2    laranja       50
  3      arroz       30

Mais tarde o gerente pede que os produtos sejam classificados como veganos ou não. Para isso podemos usar o método Series.map(dict) que transforma cada elemento usando-o como chave e retornando o valor no dicionário. Construímos um mapeamento entre produto e S/N, conforme o produto seja ou não vegano.

» veg = {'leite':'N', 'manteiga':'N', 'laranja':'S', 'arroz':'S'}
» # dfComprar['produto'] é uma Series e
» dfComprar['produto'].map(lambda x: vegano[x])
↳ 0    N
  1    N
  2    S
  3    S

» # inserindo esse serie em uma nova coluna do df
» dfComprar['vegano'] = dfComprar['produto'].map(veg)
» dfComprar
↳      produto   quantos   vegano
  0      leite        15        N
  1   manteiga        40        N
  2    laranja        50        S
  3      arroz        30        S

» # o mesmo resultado seria obtido com a função lambda 
» dfComprar['vegano']=dfComprar['produto'].map(lambda x: vegano[x])

Compartimentação e discretização

Compartimentação e discretização, (Binning e Discretization ) é o processo de particionamento de dados em faixas especificadas. Os compartimentos (faixas ou bins) são representados por variáveis categóricas, que são variáveis que podem assumir apenas um número discreto e limitado de valores, geralmente fixo. Elas estão associadas à propriedades qualitivas do sistema que se observa e podem satisfazer ou não algum critério de ordenamento.

Por ex., suponha que temos um estudo de qualquer natureza centrada sobre indivíduos onde o sexo e a faixa etária são relevantes para as conclusões que se procura obter. O sexo dos indivíduos (digamos que divididos em F = feminino, M = masculino, O = outros) não pode ser ordenado. Mas as faixas etárias são ordenáveis. Dividimos a população estudada em faixas ou bins. Sabendo que todos os participantes são maiores de idade e nenhum tem mais de 98 anos de idade usamos as faixas separadas pelas idades: 18, 34, 50, 66, 82, 98 anos.

» faixas = [18, 34, 50, 66, 82, 98]                            # definição dos intervalos de idade
» idades = [25, 18, 59, 39, 68, 26, 73, 63, 56, 84]            # idade dos indivíduos no estudo
» categorias = pd.cut(idades, faixas)
» categorias
↳ [(18.0, 34.0], NaN, (50.0, 66.0], (34.0, 50.0], (66.0, 82.0], (18.0, 34.0],
   (66.0, 82.0], (50.0, 66.0], (50.0, 66.0], (82.0, 98.0]]
  Categories (5, interval[int64]): [(18, 34] < (34, 50] < (50, 66] < (66, 82] < (82, 98]]

» # o objeto categorias é do tipo Categorical
» type(categorias)
↳ pandas.core.arrays.categorical.Categorical

» categorias.categories
↳ IntervalIndex([(18, 34], (34, 50], (50, 66], (66, 82], (82, 98]],
                closed='right', dtype='interval[int64]')

# O método pd.value_counts(categorias) fornece uma contagem para cada valor existente:
» pd.value_counts(categorias)
↳ (50, 66]    3
  (18, 34]    2
  (66, 82]    2
  (34, 50]    1
  (82, 98]    1

» # as colunas são formadas por
» pd.value_counts(categorias).index[0],  pd.value_counts(categorias)[0]
↳ (Interval(50, 66, closed='right'), 3)

» nomes_faixas = ['garoto','adulto','semi-novo','vô','matusa']
» categorias = pd.cut(idades, faixas, labels=nomes_faixas)
» categorias
↳  ['garoto', NaN, 'semi-novo', 'adulto', 'vô', 'garoto', 'vô', 'semi-novo', 'semi-novo', 'matusa']
   Categories (5, object): ['garoto' < 'adulto' < 'semi-novo' < 'vô' < 'matusa']
» pd.value_counts(categorias)
↳ semi-novo    3
  garoto       2
  vô           2
  adulto       1
  matusa       1

» # podemos transformar esse objeto em um dataframe
» dfCont = pd.DataFrame(pd.value_counts(categorias))

» # reordenar índices
» dfConf = dfCont.reindex(['garoto', 'adulto', 'semi-novo', 'vô', 'matusa'])
» dfConf
↳             0
  garoto      2
  adulto      1
  semi-novo   3
  vô          2
  matusa      1

As faixas numéricas são estabelecidas em intervalos do tipo (a, b] < (b, c] … representando intervalos abertos no limite inferior e fechados no superior. Isso significa que a não está no primeiro intervalo, mas b está. Para alterar esse comportamento usamos o parâmetro pandas.cut(...,right=False).

Podemos informar em quantas faixas queremos dividir os dados, ao invés de passar explicitamente essas faixas. Nesse caso o método pandas.cut(dados, n, precision=p) calculará n intervalos iguais baseados nos valores máximos e mínimos dos dados. precision=p determina a precisão decimal das faixas.

» # array com 20 numeros aleatórios    
» dados = np.random.rand(20)*10

» dados.min(), dados.max()         # valores mínimo e máximo
↳ (1.0012658194039414, 9.799331139583924)

» # 3 faixas (bins)
» picado = pd.cut(dados, 3, precision=2)
» pd.value_counts(picado)
↳ (6.87, 9.8]     10
  (0.99, 3.93]     7
  (3.93, 6.87]     3

Para distribuir dados em faixas baseadas em quantis usamos o método pandas.qcut(dados, n), onde n é o número de partes na partição. Intervalos de quantis customizados podem ser conseguidos passando-se uma lista em pandas.qcut(dados, lista).

» data = np.random.randn(1000)          # 1000 números aleatórios
» categorias = pd.qcut(data, 4)         # distribui em quartis
» pd.value_counts(categorias)
↳ (-3.0309999999999997, -0.683]    250
  (-0.683, 0.0106]                 250
  (0.0106, 0.702]                  250
  (0.702, 3.196]                   250

» # intervalos de quantis customizados
» pd.value_counts(pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.]))
↳ (-1.223, 0.0106]                 400
  (0.0106, 1.301]                  400
  (-3.0309999999999997, -1.223]    100
  (1.301, 3.196]                   100

Permutações aleatórias

Permutações entre as linhas (ou colunas) de um dataframe são obtidas com dataframe.take(arr), onde arr é um array com a ordem dos índices desejada. Se essa ordem for “sorteada” o dataframe fica com linhas em ordem “aleatoria”. Para reordenar colunas usamos axis=1. dataframe.sample(n) seleciona n linhas do dataframe, sem repetições (n < dataframe.shape[0]) e dataframe.sample(n, replace=True) retorna n linhas do dataframe que podem ser repetidas (como em um sorteio com reposição dos elementos sorteados).

» # dataframe de teste    
» df = pd.DataFrame(np.arange(16).reshape((4, 4)))
» df
↳      0    1    2    3
  0    0    1    2    3
  1    4    5    6    7
  2    8    9   10   11
  3   12   13   14   15

» sorteio = np.random.permutation(4)   # permutação aleatória de 0, 1, 2 e 3
» sorteio
↳ array([3, 2, 0, 1])

» df.take(sorteio)                     # dataframe na ordem de linhas sorteadas
↳       0     1     2     3
  3    12    13    14    15
  2     8     9    10    11
  0     0     1     2     3
  1     4     5     6     7

» df.take(sorteio, axis=1)             # dataframe na ordem de colunas sorteadas
↳       3     2     0     1
  0     3     2     0     1
  1     7     6     4     5
  2    11    10     8     9
  3    15    14    12    13

» df.sample(n=2)                       # 2 linhas selecionadas aleatoriamente
↳       0     1     2     3
  1     4     5     6     7
  3    12    13    14    15

» df.sample(n=2, axis=1)               # 2 colunas selecionadas aleatoriamente
↳      3    1
  0    3    1
  1    7    5
  2   11    9
  3   15   13

» df.sample(n=4, replace=True)        # 4 linhas selecionadas aleatoriamente, com reposição
↳      0    1    2    3
  0    0    1    2    3
  0    0    1    2    3
  1    4    5    6    7
  0    0    1    2    3

O mesmo dataframe obtido com df.take(sorteio) poderia ser conseguido com df.iloc[sorteio].

Indicador de computação, variáveis fictícias

Na estatística, econometria e aprendizado de máquina uma variável fictícia (variável dummy ) é uma representação de um efeito categórigo assumindo apenas os valores 0 ou 1 para indicar presença ou ausência de alguma forma de caracterização. Elas podem ser consideradas como representações numéricas de aspectos qualitativos. Um exemplo simples seria a representação da classificação de uma conta bancária como poupança (0) ou conta corrente (1).

Uma variável categórica pode ser transformada em uma matriz dummy ou de indicadores. Se uma series (uma coluna de um dataframe) possui p valores distintos podemos obter um dataframe com o mesmo número de colunas, cada uma contendo apenas 0 ou 1. Para isso usamos o método pandas.get_dummies(Series) que retorna um dataframe marcando as posições onde cada um dos p valores ocorrem. Um prefixo pode ser acrescentado aos nomes das colunas com pandas.get_dummies(Series, prefix='p').

Por ex., em uma pesquisa foi marcado, para cada indivíduo participante, o campo sexo = F (feminino), M (masculino), O (outros).

» df = pd.DataFrame({'individuo': ['Fulano', 'Beltrano', 'Cicrano', 'Deltrano', 'Cruciano', 'Marciano'],
                     'sexo': ['H','H','H','F','O','F']})
» df
↳      individuo   sexo
  0       Fulano      H
  1     Beltrano      H
  2      Cicrano      H
  3     Deltrano      F
  4     Cruciano      O
  5     Marciano      F

» # categorizando a coluna 'sexo'
» pd.get_dummies(df['sexo'])
↳      F    H    O
  0    0    1    0
  1    0    1    0
  2    0    1    0
  3    1    0    0
  4    0    0    1
  5    1    0    0

» # inserindo um prefixo (no nome das colunas)
» pd.get_dummies(df['sexo'], prefix='sexo').head(2)
↳    sexo_F  sexo_H  sexo_O
   0      0       1       0
   1      0       1       0

Muitas vezes os dados devem ser manipulados e preparados para uma devida categorização. Suponha que temos uma lista de autores, cada um associado a um ou mais gêneros literários separados por |. Queremos uma listagem de autores versus gêneros, marcando em qual gênero cada um escreve.

» # importamos de qualquer fonte o seguinte dataframe:
» dfAutores
↳       autor                genero
  0   Antonio          poesia|conto
  1      José               romance
  2     Marco      ficção|biografia
  3     Pedro          poesia|conto

» # cada autor está associado a um ou mais gêneros
» genero = dfAutores.genero
» autores = dfAutores.autor
» # as duas séries têm o mesmo comprimento (len(autores) = len(generos) = 4, 4

Criamos uma lista vazia e a preenchemos com todos os gêneros, quebrando os campos em |. Depois usamos pandas.unique(lista) para conseguir um array com os gêneros, sem repetições, como em um conjunto (set).

» lista = []
» for t in genero:
»     lista.extend(t.split('|'))
» unicos = pd.unique(lista)
» unicos
↳ array(['poesia', 'conto', 'romance', 'ficção', 'biografia'], dtype=object)

Em seguida criamos um dataframe de zeros com os autores nas colunas e gêneros nas linhas.

» dfZero = pd.DataFrame(np.zeros((len(unicos),len(autores))), index=unicos, columns=autores).astype(int)
» dfZero          # estado inicial de dfZero
↳ autor    Antonio    José    Marco    Pedro
  poesia         0       0        0        0
  conto          0       0        0        0
  romance        0       0        0        0
  ficção         0       0        0        0
  biografia      0       0        0        0

# preenchemos esse dataframe
» for i in range(len(unicos)):
»     for k in range(len(genero)):
»         if unicos[i] in genero[k]:
»             dfZero.iloc[i,k] = 1
            
» dfZero    # estado final de dfZero
↳ autor    Antonio    José    Marco    Pedro
  poesia         1       0        0        1
  conto          1       0        0        1
  romance        0       1        0        0
  ficção         0       0        1        0
  biografia      0       0        1        0

O duplo loop sobre a lista de gêneros únicos, unicos, e a lista original de gêneros genero faz a verificação se um dos generos está em genero1|genero2…. Por exemplo, na linha 3, coluna 2 temos:

» unicos[3], genero[2],  unicos[3] in genero[2]
↳ ('ficção', 'ficção|biografia', True)

Se o resultado é verdadeiro o dataframe terá o campo correspondente trocado para 1. Os demais permanecem com o valor 0. O dataframe final é o resultado desejado.

Tratamento de campos de texto

Operações com strings são também vetorializadas no pandas. No ex. usamos os códigos telefones dos países: 55-Brasil, 47-Noruega, 52-México. Construímos duas séries e as concatenamos em um dataframe, df = pd.concat([serie, srPais], axis=1).

» lista = ['055-11-12345678', '047-21-87654321', '055-11-13579135', '052-78-45665412']
» serie = pd.Series(lista)
» serie
↳ 0    055-11-12345678
  1    047-21-87654321
  2    055-11-13579135
  3    052-78-45665412

» # booleano, linhas que contém '-11-'
» serie.str.contains('-11-')
↳ 0     True
  1    False
  2     True
  3    False

» # linhas que contém '-11-'
» serie[serie.str.contains('-11-')]
↳ 0    055-11-12345678
  2    055-11-13579135

» # lista com os códigos dos países
» codigos = [x.split('-')[0] for x in lista]
» codigos
↳ ['055', '047', '055', '052']

» # dicionário para conversão código ⇒ país
» pais = {'055':'Brasil', '047':'Noruega', '052':'México'}
» srPais = pd.Series([pais[x] for x in codigos])      # veja comentário †
» srPais
↳ 0     Brasil
  1    Noruega
  2     Brasil
  3     México

» # juntamos as duas series em um dataframe
» df = pd.concat([serie, srPais], axis=1)
» df = df.rename(columns = {0:'telefone', 1:'pais'})

» df
↳             telefone       pais
  0    055-11-12345678     Brasil
  1    047-21-87654321    Noruega
  2    055-11-13579135     Brasil
  3    052-78-45665412     México

» # nome do país começado com 'No'
» df[df['pais'].str.startswith('No')]
↳             telefone       pais
  1    047-21-87654321    Noruega

» # acrescenta campo com 3 primeiras letras do nome
» df['abreviado'] = df['pais'].str[:3]
» df
↳             telefone      pais    abreviado
  0    055-11-12345678    Brasil          Bra
  1    047-21-87654321   Noruega          Nor
  2    055-11-13579135    Brasil          Bra
  3    052-78-45665412    México          Méx

(): A linha srPais = pd.Series([pais[x] for x in codigos]) (uma compreensão de lista) percorre os valores em codigos e os usa como chaves no dicionário pais, retornando seus valores.

🔺Início do artigo

Bibliografia

  • McKinney, Wes: Python for Data Analysis, Data Wrangling with Pandas, NumPy,and IPython
    O’Reilly Media, 2018.

Consulte bibliografia completa em Pandas, Introdução neste site.

Nesse site:

NumPy, Introdução


Numpy

NumPy é uma biblioteca do python especializada em computação científica e análise de dados. Ela é usada em diversos tipos de operações que envolvem operações matriciais. Além disso suas matrizes formam a base para outros pacotes, como o pandas e outras voltadas para o cálculo matemático e científico. Numpy foi primeiro lançado por Travis Oliphant em 2006 e tem sido mantido por um grande número de colabores desde então, sob licença BSD.

NumPy, com suas matrizes, apresenta algumas vantagens sobre o cálculo usual com objetos do python, como listas e tuplas. Essas operações são mais rápidas e flexíveis e foram construídas de forma a evitar a necessidade da realização de laços (loops ). Ela contém métodos voltados para operações da álgebra linear, geração de números aleatórios e transformadas de Fourier, além da interface voltada para a conexão com as linguagens C, C++ e FORTRAN.

Um exemplo rápido pode mostrar como as rotinas do numpy são mais eficientes que as de objetos list do python.

» # comparação de velocidades
» import numpy as np
» # um array do numpy
» array = np.arange(1_000_000)
» # uma lista usual do python
» lista = list(range(1_000_000))

» %time for _ in range(100): arr2 = array * 2
↳ CPU times: user 145 ms, sys: 7.93 ms, total: 152 ms
  Wall time: 151 ms

» %time for _ in range(100): lista2 = [x * 2 for x in lista]
↳ CPU times: user 5.35 s, sys: 778 ms, total: 6.13 s
  Wall time: 6.12 s

» # 5.35/.145 ≈ 37 × mais rápido

Wall time é o tempo total gasto pelo código para ser executado. CPU time é uma medida do tempo gasto pelo processador apenas quando esteve operando sobre a tarefa específica. Dependendo do cálculo feito o numpy pode ser mais de 100 vezes mais rápido que uma operação similar em puro python.

Instalação


Em geral o módulo está presente como pacote na maioria das distribuições de Python. Se necessária a sua instalação em separado pode ser feita. Numpy e pandas são instalados juntos com o Anaconda.

# No Linux (Ubuntu and Debian):
» sudo apt-get install python-numpy
# No Linux (Fedora)
» sudo yum install numpy scipy
# No Windows com Anaconda:
» conda install numpy
# após a instalação o módulo deve ser importado:
» import numpy as np
# O aliás np é opcional e de escolha do programador.

Ndarray

O objeto básico da biblioteca Numpy é o ndarray (N-dimensional array). Ndarrays são matrizes multidimensionais com número determinado de dimensões e elementos. Seus elementos pertencem todos a um único tipo, chamado de dtype (data-type ou tipo de dado). Cada dimensão é denominada por axis (eixo) e o número de eixos é o rank do objeto. Diferente das listas do python, ndarrays têm dimensões fixas, definidas em sua construção.

Um ndarray possui os seguintes atributos referentes ao seu tipo de dado, tamanho e ordem:

Atributo descrição
array.dtype tipo de dado armazenado. (Veja lista abaixo),
array.ndim número de dimensões (que são eixos ou axis ); o mesmo que rank
array.size número de elementos em cada eixo,
array.shape (ou forma), tupla de N inteiros positivos com o comprimento de cada eixo.

Um ndarray de 1 dimensão é um objeto similar a um vetor (rank = 1): arr1D = ([a0,a1,...,aN-1,]), arr1D.size = N, arr1D.ndim = 1, arr1D.shape = (N,).

Um ndarray de 2 dimensões é um objeto similar a uma matriz (rank = 2): se ela possui M linhas, cada uma com N elementos então arr2D.size = M × N , arr2D.ndim = 2, arr2D.shape = (M,N).

Um ndarray de 3 dimensões é uma coleção de matrizes (rank = 3): se ela possui K matrizes de M linhas, cada uma com N elementos então arr3D.size = K × M × N , arr3D.ndim = 3, arr3D.shape = (K, M, N).

Ndarrays de ordem superior são generalizações desse processo, acrescentados novos eixos.

Os eixos são numerados para diversas operações. Em 2 dimensões axis=0 são as linhas, axis=1 as colunas, e assim consecutivamente para ordens superiores.

Tipos, dtypes

Além dos tipos usuais do python, a importação de Numpy disponibiliza um conjunto extendido de tipos ou dtypes.

dtype descrição
bool booleano (true ou false) armazenado como um byte
intX inteiro com sinal, X-bit (X=8,16,32, 64)
uintX inteiro sem sinal, X-bit (X=8,16,32, 64)
intc idêntical ao int C (em geral int32 ou int64)
intp inteiro usado para indexação (como C size_t; em geral int32 ou int64)
float_ o mesmo que float64
float16 meia precisão float: sign bit, 5-bit exponente, 10-bit mantissa
float32 simple precisão float: sign bit, 8-bit exponente, 23-bit mantissa
float64 dupla precisão float: sign bit, 11-bit exponente, 52-bit mantissa
complex_ o mesmo que complex128
complex64 complexo, representado por dois 32-bit floats (parte real e imaginária)
complex128 complexo, representado por dois 64-bit floats (parte real e imaginária)

Construção de um array

Um array do NumPy pode ser criado passando-se uma lista para construtor np.array(lista).

» import numpy as np

» lista = [123,234,345]
» arr = np.array(lista)
» arr
↳ array([123, 234, 345])

# o objeto criado é um ndarray do numpy
» type(arr)
↳ numpy.ndarray

» arr.dtype
↳ dtype('int64')

» arr.ndim
↳ 1
» arr.shape
↳ (3,)
» arr.size
↳ 3

» # uma lista de listas
» lista2 = [[123,234,345],
            [456,567,678],
            [789,890,901]]
» arr2 = np.array(lista2)
» arr2
↳ array([[123, 234, 345],
         [456, 567, 678],
         [789, 890, 901]])

» arr2.ndim
↳ 2
» arr2.size
↳ 9
» arr2.shape
↳ (3, 3)

» # lista de listas de listas
» lista3 =[ [ [1,2],[2,3] ], [ [3,4],[4,5] ],  [ [1,2],[2,3] ], [ [3,4],[4,5] ] ]
» arr3 = np.array(lista3)
» # o resultado é: 4 matrizes de 2 x 2  elementos
» arr3
↳ array([[[1, 2],
         [2, 3]],

        [[3, 4],
         [4, 5]],

        [[1, 2],
         [2, 3]],

        [[3, 4],
         [4, 5]]])

» arr3.ndim
↳ 3
» arr3.shape
↳ (4, 2, 2)
» arr3.size
↳ 16

» # a 1ª matriz
» arr3[0]

↳  array([[1, 2],
         [2, 3]])

» # a 2ª linha da 1ª matriz
» arr3[0,1]
↳ array([2, 3])

» # o 1º elemento da 2ª linha da 1ª matriz
» arr3[0,1,0]
↳ 2

# todos os arrays criados tem o mesmo dtype
» arr3.dtype
↳ dtype('int64')

Arrays podem ser de outros tipos, como um array de strings. No entanto devem ser homogêneos (todos os elementos do mesmo tipo). Uma tentativa de criar um array como em stArr2 causa uma tentativa de ajuste (cast), transformando os inteiros em string. O método array(listas, dtype) aceita o parâmetro dtype onde se pode informar o tipo de elemento que se pretende armazenar.

» stArr = np.array([['a', 'b'],['c', 'd']])
» stArr
↳ array([['a', 'b'],
       ['c', 'd']], dtype='<U1')

» stArr[0,1]
↳ 'b'
» stArr.dtype
↳ dtype('<U1')
» stArr.dtype.name
↳ 'str32'

» stArr2 = np.array([[1.01, 2.02],['h', 'i']])
» stArr2
↳ array([['1.01', '2.02'],
        ['h', 'i']], dtype='<U21')

» stArr3 = np.array([[True, False],['h', 'i']])
» stArr3
↳ array([['True', 'False'],
        ['h', 'i']], dtype='<U5')

» # parâmetro dtype
» cplx = np.array([[1, 2, 3],[4, 5, 6]], dtype=complex)
» cplx
↳ array([[1.+0.j, 2.+0.j, 3.+0.j],
        [4.+0.j, 5.+0.j, 6.+0.j]])               

Arrays podem ser transformados de um tipo para outro (quando possível). Para isso usamos array.astype(). Na transformação de floats para inteiros a parte decimal será truncada. Arrays de strings, desde que devidamente formatados, podem ser convertidos em numéricos.

» # criando um array de integers
» arr = np.array([1, 2, 3, 4, 5])
» arr.dtype
↳ dtype('int64')

» # cast para array de ponto flutuante
» floatArr = arr.astype(np.float64)
» floatArr.dtype
↳ dtype('float64')

» # floats para integers
» # criando um array de floats
» arr = np.array([1.9, -8.2, -9.6, 0.9, 2.3, 10.7])
» arr
↳ array([ 1.9, -8.2, -9.6, 0.9, 2.3, 10.7])

» # converte para inteiros (trunca parte inteira)
» arr.astype(np.int32)
↳ array([ 1, -8, -9, 0, 2, 10], dtype=int32)

» # um array de strings
» arrNumStrings = np.array(['0.0', '7.75', '-6.6', '100'], dtype=np.string_)
» arrNumStrings.astype(float)
↳ array([ 0., 7.75, -6.6 , 100.]

Métodos predefinidos de construção

O método np.arange(m,n,[p]) retorna um array de inteiros no intervalo (m, n], i.e., começando em m e terminando em n-1. Se o primeiro argumento for omitido m=0. Um terceiro argumento informa o p=passo, intervalo entre os valores da sequência. Em np.arange(m,n,p) m, n devem ser inteiros mas p pode ser um float.

O método np.linspace(m,n,p) retorna um array no intervalo (m, n), ambos os extremos incluídos, com p números igualmente espaçados.

np.random.random(n) retorna um array com n elementos aleatórios e np.random(m, n) retorna um array com shape = (m,n) e elementos aleatórios.

» # np.range(n)
» np.arange(10)
↳ array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

» # np.range(m,n)
» np.arange(5, 15)
↳ array([ 5,  6,  7,  8,  9, 10, 11, 12, 13, 14])

» # np.range(m,n,p)
» np.arange(10, 100, 10)
↳ array([10, 20, 30, 40, 50, 60, 70, 80, 90])

» np.arange(10, 20, .5)
↳ array([10. , 10.5, 11. , 11.5, 12. , 12.5, 13. , 13.5, 14. , 14.5, 15. ,
         15.5, 16. , 16.5, 17. , 17.5, 18. , 18.5, 19. , 19.5])

» # np.linspace(m,n,p)
» np.linspace(10, 20, 5)
↳ array([10. , 12.5, 15. , 17.5, 20. ])

» # a sequência pode ser decrescente
» np.linspace(20, 10, 5)
↳ array([20. , 17.5, 15. , 12.5, 10. ])

» # np.random.random(n)
» np.random.random(10)
↳ array([0.35433322, 0.54555179, 0.48783323, 0.5785414 , 0.76837232,
         0.69888297, 0.62492788, 0.33289321, 0.75068313, 0.95667854])

» # np.random.random(m,n)
» np.random.random((2,3))
↳ array([[0.11351481, 0.76831577, 0.27597676],
         [0.73130126, 0.7225559 , 0.54040225]])

Os métodos np.zeros((m,n)) e np.ones((m,n)) criam, respectivamente, ndarrays de zeros e uns com as dimensões dadas pela tupla (ou lista) no argumento. np.eye(m,n) gera um ndarray m × n com elementos 1 na diagonal, 0 fora dela. np.eye(n,n) é o mesmo que np.eye(n) ou np.identity(n), que são a matriz identidade de n dimensões.

» np.zeros((2,4))
↳ array([[0., 0., 0., 0.],
         [0., 0., 0., 0.]])

» np.ones((2,3))
↳ array([[1., 1., 1.],
         [1., 1., 1.]])

» np.eye(2,4)
↳ array([[1., 0., 0., 0.],
         [0., 1., 0., 0.]])

» np.identity(3)
↳ array([[1., 0., 0.],
         [0., 1., 0.],
         [0., 0., 1.]])

» np.eye(2,6)
↳ array([[1., 0., 0., 0., 0., 0.],
         [0., 1., 0., 0., 0., 0.]])

» np.eye(4,4)
↳ array([[1., 0., 0., 0.],
         [0., 1., 0., 0.],
         [0., 0., 1., 0.],
         [0., 0., 0., 1.]])

O método np.empty(m,n) permite a criação de arrays vazios, em geral destinados a serem preenchidos depois de sua criação por meio de algum cálculo ou leitura de dados. Nos exemplos criamos um array numérico vazio, arrN. Observe que não há garantia de que as entradas serão nulas. Em seguida criamos um array vazio de strings, com espaço para 3 caracteres (dtype='<U3′), e o preenchemos em um loop.

» # array numérico "vazio"
» arrN = np.empty((1,3))
» arrN
↳ array([[4.66896202e-310, 0.00000000e+000, 1.58101007e-322]])    

» # array de strings
» arr = np.empty((3,3), dtype='<U3')
» arr
↳ array([['', '', ''],
         ['', '', ''],
         ['', '', '']], dtype='<U3')

» for linha in range(arr.shape[0]):
»     for coluna in range(arr.shape[1]):
»         arr[linha,coluna] = 'a' + str(linha) + str(coluna)
» arr
↳ array([['a00', 'a01', 'a02'],
         ['a10', 'a11', 'a12'],
         ['a20', 'a21', 'a22']], dtype='<U3')

Alterando dimensões

Dado um array de uma única linha com r elementos podemos tranformá-lo em um array com shape = (m,n), desde que as dimensões sejam compatíveis, i.e., r = m × n. De fato, qualquer array pode ser transformado em outro se eles possuem o mesmo número de elementos (mesmo size ).

» arr = np.random.random(12)
» arr
↳ array([0.04276829, 0.76468762, 0.24807651, 0.75531679, 0.60327475,
         0.81704922, 0.08233836, 0.64112484, 0.55276595, 0.30669723,
         0.43989324, 0.60031761])

» # reshape
» arr.reshape(3,4)
↳ array([[0.04276829, 0.76468762, 0.24807651, 0.75531679],
         [0.60327475, 0.81704922, 0.08233836, 0.64112484],
         [0.55276595, 0.30669723, 0.43989324, 0.60031761]])

» np.linspace(0,10, 6).reshape(2,3)
↳ array([[ 0.,  2.,  4.],
         [ 6.,  8., 10.]])    

Indexação e fatiamento


Quando um array é criado ele recebe automaticamente um conjunto de índices. Um elemento pode ser lido ou alterado por meio de seu índice. Índices negativos contam de trás para frente, sendo arr[-1] o último elemento do array. Para selecionar (ou editar) vários elementos passamos uma lista de índices.

» arr = np.linspace(1,12, 12)
» arr
↳ array([ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10., 11., 12.])

» # o 5º elemento
» arr[4]
↳ 5.

» # alterando o 5º elemento
» arr[4] = 100
» arr
↳ array([ 1.,  2.,  3.,  4.,  100.,  6.,  7.,  8.,  9., 10., 11., 12.])

» arr[-8]
↳ 100

» # lendo vários elementos
» arr[[0,2,5]]
↳ array([1., 3., 6.])

» # alterando vários elementos
» arr[[0,2,5]] = [10, 20, 30]
» arr
↳ array([ 10.,   2.,  20.,   4., 100.,  30.,   7.,   8.,   9.,  10.,  11.,  12.])

No caso de arrays bidimensionais os elementos do array são acessados pelos índices de suas linhas e colunas, sendo que arr[l,c] = al,c é o elemento da linha l e coluna c. Em objetos de ranks superiores cada índice se refere a um dos eixos.

Uma fatia ou slice do array é um subconjunto de elementos que pode ter o mesmo shape ou não. Para um vetor, digamos arr1D = [a0, a1, ..., aM] a fatia arr1D[m,n] = [am, ..., an-1], onde m ≥ 0, n ≤ M. Vale lembrar que o comprimento da fatia de um array unidimensional é arr1D[m,n].size = n-m.

» # outro teste para slices
» arr = np.arange(10)    # cria o array array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

» arr[6:9]
↳ array([6, 7, 8])

» arr[:3]
↳ array([0, 1, 2])

» arr[7:]
↳ array([7, 8, 9])

» # uma seção pode ser alterada
» arr[3:6] = -5
» arr
↳ array([ 0,  1,  2, -5, -5, -5,  6,  7,  8,  9])

» arr[7:] = -arr[7:]
» arr
↳ array([ 0,  1,  2, -5, -5, -5,  6, -7, -8, -9])

(†) Uma operação entre arrays de dimensões diferentes, como ocorre em arr[0:4] = -1 é chamado de propagação ou broadcasting. Para interagir com a 1ª parte da expressão a 2ª é transformada: -1 → arr[-1,-1,-1,-1]. Voltaremos a esse tópico.

Observe que uma fatia de um array é uma referência àquela parte do array e qualquer alteração feita na fatia se refletirá no array original. A notação arr[i:j] significa, claro, elementos de i-ésimo até (j-1)-ésimo. arr[:] significa todos os elementos do array.

» # criando um array de teste
» arr = (np.random.random(10)*10).round(1)
» arr
↳ array([3. , 1.5, 5.3, 1.3, 8.8, 9.8, 4.7, 0.1, 0.1, 0.6])
» fatia = arr[1:5]
» fatia
↳ array([1.5, 5.3, 1.3, 8.8])

# vamos alterar trecho da fatia
» fatia[1:3] = 0
» fatia
↳ array([1.5, 0. , 0. , 8.8])

» # o array original foi alterado
» arr
↳ array([3. , 1.5, 0. , 0. , 8.8, 9.8, 4.7, 0.1, 0.1, 0.6])

» # vamos alterar a fatia inteira
» fatia[:] = -10
» arr
↳ array([  3. , -10. , -10. , -10. , -10. ,   9.8,   4.7,   0.1,   0.1,  0.6])

» # valores específicos podem ser fornecidos (sem broadcast)
» fatia[:] = [-1,-2,-3,-4]
» arr
↳ array([ 3. , -1. , -2. , -3. , -4. ,  9.8,  4.7,  0.1,  0.1,  0.6])

Esse comportamento é útil quando se trabalha com array de dados muito grande e se deseja alterar apenas parte dele, lembrando que a biblioteca efetua suas operações mantendo os dados envolvidos na memória.

Em um array arr de 2 dimensões arr[m] é a m-ésima linha e arr[m,n] se refere ao elemento am,n, da m-ésima linha, n-ésima coluna. arr[m][n] é o mesmo am,n.

» # slices para dimensões mais altas
» arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
» arr
↳ array([[1, 2, 3],
         [4, 5, 6],
         [7, 8, 9]])

» # 2ª linha
» arr[1]
↳ array([4, 5, 6])

» arr[:1]
↳ array([[1, 2, 3]])

» # 1ª linha, 3º elemento
» arr[0,2]
↳ 3
» arr[0][2]
↳ 3

Uma cópia de um setor é uma referência para aquele setor, chamda de view ou visualização do segmento. Alterações feitas à view se refletam no array original, a menos que o método arr.copy() seja usado. Um array copiado dessa forma perde a referência com o array original e pode ser modificado independentemente.

» # slices em 3D:
» # criamos um array com shape (2,3,2)
» arr3D = np.arange(12).reshape(2,3,2)
» arr3D
↳ array([[[ 0,  1],
         [ 2,  3],
         [ 4,  5]],

        [[ 6,  7],
         [ 8,  9],
         [10, 11]]])

» # a 1ª matriz é
» arr3D[0]
↳ array([[0, 1],
        [2, 3],
        [4, 5]])
» # a 2ª linha da 1ª matriz é
» arr3D[0,1]
↳ array([2, 3])

» # seu 2º elemento
» arr3D[0,1,1] # o mesmo que arr3D[0,1][1]
↳ 1

» # copiamos um slice de 2 formas
» guardar = arr3D[0].copy()
» slice = arr3D[0]
» # ambos com os valores da  1ª matriz

» # alteramos toda a 1ª matriz
» arr3D[0] = 12
» # o array original foi alterado
» arr3D
↳ array([[[12, 12],
         [12, 12],
         [12, 12]],

        [[ 6,  7],
         [ 8,  9],
         [10, 11]]])
» # a cópia por referência foi alterada
» slice
↳ array([[12, 12],
         [12, 12],
         [12, 12]])

» # mas a cópia por valor não foi alterada
» guardar
↳ array([[0, 1],
         [2, 3],
         [4, 5]])

» # podemos restaurar o array aos seus valores originais
» arr3D[0] = guardar
» arr3D
↳ array([[[ 0,  1],
          [ 2,  3],
          [ 4,  5]],

         [[ 6,  7],
          [ 8,  9],
          [10, 11]]])


A notação de slice, idêntica à usada em listas do python, funciona em arrays. Para um array unidimensional arr1d[i:f] retorna do i-ésimo elemento até j-ésimo elemento (exclusive). Para um array de 2 dimensões arr2d[i:f] retorna i-ésima linha até j-ésima linha (exclusive). Se i é omitido o início é usado, se j é omitido o final é usado. Portanto arr2d[:2] significa as duas primeiras linhas do array (linha 0 e linha 1).

Slices ou segmentos múltiplos podem ser usados. Por exemplo, arr2d[m:n, r:s] são as linhas de m até n-1, colunas de r até s-1.

» # array 1d  (um vetor)  
» arr = np.array([1.2, 2.3, 3.4, 4.5, 5.6])
» arr[2:4]
↳ array([3.4, 4.5])

» # array 2d  (uma matriz)  
» arr2d = np.array([[1,2,3],[4,5,6],[7,8,9]])
» arr2d
↳ array([[1, 2, 3],
         [4, 5, 6],
         [7, 8, 9]])

» arr2d[0:2]
↳ array([[1, 2, 3],
         [4, 5, 6]])

» arr2d[:2]
↳ array([[1, 2, 3],
         [4, 5, 6]])
       
» # slices múltiplos
» arr2d[:2, 1:]
↳ array([[2, 3],
         [5, 6]])

arr2d[:2, 1:] são as linhas 0 e 1, colunas de 1 em diante.

O indexador pode ser um array booleano, e os valores serão filtrados apenas se o índice for True. Essa operação pode ser muito útil para a implementação de filtragens de tipos diversos.

» # o indexador pode ser booleano
» ar1 = np.array([-1,0, 3, 7, -2, 10])
» ar2 = np.array([-2,10, 2, 9, -1, 8])

» # extrair apenas 0º e 2º elemento
» ar1[[True, False, True, False, False, False]]
↳ array([-1,  3])

» # elementos de ar1 maiores que 2
» ar1[ar1>2]
↳ array([ 3,  7, 10])

» # elementos de ar1 maiores que os de ar2
» ar1[ar1>ar2]
↳ array([-1,  3, 10])

Suponha que temos dados sobre alguns países, armazenados em forma tabular. O primeiro array contém os nomes dos países, como se fosse um cabeçalho da tabela seguinte. O segundo array contém dados numéricos de qualquer natureza, com 5 linhas e 4 colunas, cada coluna contendo dados relativos ao país no cabeçalho. Para esse exemplo geramos esses dados aleatoriamente, apenas para exibir a operação.

» arrPais = np.array(['Brasil', 'Chile', 'Brasil', 'Peru'])
» arrDados = np.random.randn(5,4).round(2)  # 5 linhas e 4 colunas

» print(arrPais, '\n', arrDados)
↳ ['Brasil' 'Chile' 'Brasil' 'Peru'] 
   [[-0.71  0.42 -0.52  0.63]
   [-1.12 -0.29  0.03  1.43]
   [ 0.99  0.45  1.08  0.53]
   [-0.78  0.18 -0.07  0.28]
   [-2.03  0.44  0.07  1.28]]

» # podemos exibir todas as linhas das colunas 0 e 2
» arrDados[:,[0,2]]
↳ array([[-0.71, -0.52],
         [-1.12,  0.03],
         [ 0.99,  1.08],
         [-0.78, -0.07],
         [-2.03,  0.07]])

» # alternativamente, as colunas que correspondem ao Brasil
» arrBrasil = arrDados[:, arrPais=='Brasil']
» arrBrasil
↳ array([[-0.71, -0.52],
         [-1.12,  0.03],
         [ 0.99,  1.08],
         [-0.78, -0.07],
         [-2.03,  0.07]])

» # na tabela do Brasil, suponha que valores negativos sejam insignificantes
» # podemos eliminá-los com uma filtragem
» arrBrasil[arrBrasil < 0] = 0
» arrBrasil
↳ array([[0.  , 0.  ],
         [0.  , 0.03],
         [0.99, 1.08],
         [0.  , 0.  ],
         [0.  , 0.07]])

» # a soma desses dados é
» arrBrasil.sum()
↳ 2.17

» # para exibir os demais dados (não do Brasil)
» arrDados[:, arrPais!='Brasil'] # ou arrDados[:, ~(arrPais=='Brasil')]
↳ array([[ 0.42,  0.63],
         [-0.29,  1.43],
         [ 0.45,  0.53],
         [ 0.18,  0.28],
         [ 0.44,  1.28]])

Observe que arrPais!='Brasil' é a negação de arrPais=='Brasil'. O parênteses em ~(arrPais=='Brasil') é necessário pois a negação ~ tem precedência sobre o teste de igualdade. Sem o parênteses ~arrPais seria avaliado primeiro, o que resultaria em erro pois o array não é booleano.

As ferramentas do pandas facilitam operações como essas.

Operações matemáticas

Operações são realizadas elemento a elemento (que abreviaremos para “e/e” nesse texto). Operações usuais de um array por um escalar são são propagadas entre o escalar e cada elemento do array. Operações entre arrays são feitas e/e, ou seja, realizadas entre elementos na mesma posição. Os arrays devem ter as mesmas dimensões.

Operações de incremento, tais como array += 1 (que significa array = array + 1) ou array *= 2 são realizados inplace (alteram o próprio array). Diversas outras operações do NumPy (e do pandas) são realizadas inplace enquanto em várias delas existe o parâmetro inplace = True/False que permite a decisão de qual caso se deseja naquele momento.

Operações podem ser feitas entre arrays retornados por funções (que retornam arrays).

» # operações com escalares    
» ar1 =  np.linspace(10, 20, 5)
» ar1
↳ [10.  12.5 15.  17.5 20. ]

» ar1 + 10
↳ [20.  22.5 25.  27.5 30. ]

» ar1 * 10
↳ [100. 125. 150. 175. 200.]

» ar1 / 10
↳ [1.   1.25 1.5  1.75 2.  ]

» ar1**2
↳ [100.   156.25 225.   306.25 400.  ]

» 1/arr1
↳ array([0.1       , 0.08      , 0.06666667, 0.05714286, 0.05      ])

» # operações entre arrays
» ar2 =  np.linspace(10, 50, 5)
» print(ar1)
» print(ar2)
↳ [10.  12.5 15.  17.5 20. ]
↳ [10. 20. 30. 40. 50.]

» ar1 + ar2
↳ array([20. , 32.5, 45. , 57.5, 70. ])

» ar1 * ar2
↳ array([ 100.,  250.,  450.,  700., 1000.])

» ar2 / ar1
↳ array([1.        , 1.6       , 2.        , 2.28571429, 2.5       ])

» # operações com arrays (2 × 3)
» ar3 = np.linspace(0,5, 6).reshape(2,3)
» ar4 = np.linspace(0,10, 6).reshape(2,3)

» print(ar3)
↳ [[0. 1. 2.]
   [3. 4. 5.]]

» print(ar4)
↳ [[ 0.  2.  4.]
   [ 6.  8. 10.]]

» ar3 + ar4
↳ array([[ 0.,  3.,  6.],
         [ 9., 12., 15.]])

» ar3 - ar4
↳ array([[ 0., -1., -2.],
         [-3., -4., -5.]])

» ar3 * ar4
↳ array([[ 0.,  2.,  8.],
         [18., 32., 50.]])
       
» # operações de incremento são realizados inplace
» ar3 +=1
» ar3
↳ array([[1., 2., 3.],
         [4., 5., 6.]])

» # seno e cosseno em np retorna um array e/e
» ar1 * np.sin(ar2)
↳ array([ -5.44021111,  11.41181563, -14.82047436,  13.03948031,
          -5.24749707])

» ar3 * np.cos(ar4)
↳ array([[ 0.        , -0.41614684, -1.30728724],
         [ 2.88051086, -0.58200014, -4.19535765]])

Comparações entre arrays resultam em arrays booleanos.

» ar5 = np.linspace(0,5, 6).reshape(2,3)
» ar6 = np.random.random(6).reshape(2,3).round(2)*5

» ar5
↳ array([[0., 1., 2.],
         [3., 4., 5.]])
» ar6
↳ array([[3.  , 2.45, 1.2 ],
         [3.1 , 1.55, 1.75]])

» arrMaior= ar5 > ar6
» arrMaior
↳ array([[False, False,  True],
         [False,  True,  True]])

Broadcasting: A operações feitas acima, entre um array e um escalar, transformam o escalar em um array de dimensões apropriadas (de mesmo shape) antes de sua realização. Essa operação se chama broadcasting:. Por exemplo:

» ar1 = np.array([0,1,2,3])
» ar2 = np.array([4,4,4,4])

» # a soma com um escalar
» ar1 + 4
↳ array([4, 5, 6, 7])

» # é  mesmo que
» ar1 + ar2
↳ array([4, 5, 6, 7])

» # os elementos são iguais
» ar1 + ar2 == ar1 + 4
↳ array([ True,  True,  True,  True])

# o mesmo ocorre com comparações
» ar1 ≥ 2
↳ array([False, False,  True,  True])

Uma forma de seleção diferente consiste em passar listas de valores como índices. O array retornado depende de como essas listas são passadas. Essa é técnica é chamada de fancy indexing (indexação sofisticada). Em qualquer dos casos abaixo os índices podem aparecer em qualquer ordem.

O slice array[[i,j,...]] contém as linhas array[i], array[j], etc. (O mesmo que array[:,[i,j,...]]).
O slice array[:,[i,j,...]] contém as colunas array[:,i], array[:,j], etc.
O slice array[[i,j,...]:[r, s,...]] é uma linha contendo os elementos array[i,r], array[j,s], etc.

» # fancy indexing (passando arrays como indices)
» # vamos construir um array 6 × 5 e atribuir seus valores um a um
» arr = np.empty((6,5))

» for linha in range(6):
»     for coluna in range(5):
»         arr[linha,coluna] = linha * 10 + coluna
        
» # o array obtido é (uma forma de identificar facilmente de que elemento se trata)
» arr
↳ array([[ 0.,  1.,  2.,  3.,  4.],
         [10., 11., 12., 13., 14.],
         [20., 21., 22., 23., 24.],
         [30., 31., 32., 33., 34.],
         [40., 41., 42., 43., 44.],
         [50., 51., 52., 53., 54.]])

» # podemos selecionar linhas (em qualquer ordem)
» arr[[3,5,1]]       # o mesmo que arr[[3,5,1],:]
↳ array([[30., 31., 32., 33., 34.],
         [50., 51., 52., 53., 54.],
         [10., 11., 12., 13., 14.]]) 

» # ou colunas (em qualquer ordem)
» arr[:,[3,1]]
↳ array([[ 3.,  1.],
         [13., 11.],
         [23., 21.],
         [33., 31.],
         [43., 41.],
         [53., 51.]])

» # fornecer duas listas (que devem ter o mesmo tamanho) tem efeito diferente,
» # retornando array de i dimensão com os índices dados nas listas
» arr[[0,3,4,1],[0,3,4,1]]
↳ array([ 0., 33., 44., 11.])

» arr[[0,3,4,1],[1,0,4,2]]
↳ array([ 1., 30., 44., 12.])

Funções universais

Funções universais, ou ufunc, são funções que agem e/e, sobre todos os elementos de um array, retornando outra array de mesmas dimensões. Essas operações são também chamadas de operações vetorializadas. Embora envolvam laços (loops ) esses são realizados internamente e de forma eficiente, de modo a agilizar os processos.

Uma tabela das funções universais é encontrada abaixo.

Método retorna
abs, fabs valor absoluto inteiros, floats, ou complexos
sqrt raiz quadrada (equivale a arr**0.5)
square elementos elevados ao quadrado (equivale a arr**2)
exp exponencial de cada elemento (ex)
log, log10, logaritmos naturais (de base e e base 10var>
log2, log1p logaritmos de base 2 e log(1 + x)
sign sinal: (1, 0, -1) (positivo, zero, negativo)
ceil teto, menor inteiro maior ou igual
floor piso, maior inteiro menor ou igual
rint arredonda para o inteiro mais próximo, preservando dtype
modf partes inteiras e fracionárias do array, em e arrays
isnan array booleano, se o valor é NaN (Not a Number)
isfinite array booleano, se cada valor é finito (non-inf, non-NaN)
isinf array booleano, se cada valor é infinito
cos, sin, tan funções trigonométricas
cosh, sinh, tanh funções trigonométricas hiperbólicas
arccos, arcsin, arctan arcos de funções trigonométricas
arccosh, arcsinh, arctanh arcos de funções trigonométricas hiperbólicas
logical_not array booleano, negação do array (equivalent to ~arr).

Exemplos de uso:

» # Funções universais
» arrBool = np.array([True, False, False, True])
» arrBool
↳ array([ True, False, False,  True])
» np.logical_not(arrBool)
↳ array([False,  True,  True, False])

» arr = np.linspace(0, 10, 6)
» arr -=5
» arr
↳ array([-5., -3., -1.,  1.,  3.,  5.])

» np.abs(arr)
↳ array([5., 3., 1., 1., 3., 5.])

» # não altera arr
» np.sign(arr)
↳ array([-1., -1., -1.,  1.,  1.,  1.])

» arr = arr/10 +4
» arr = arr.reshape(2,3)
» arr
↳ array([[3.5, 3.7, 3.9],
         [4.1, 4.3, 4.5]])

» np.modf(arr)
↳ (array([[0.5, 0.7, 0.9],
          [0.1, 0.3, 0.5]]),
↳  array([[3., 3., 3.],
          [4., 4., 4.]]))

Funções de Agregação


Funções de agregação são funções que realizam operações em todos os elementos do array, retornando um escalar(um número). Em sua maioria elas retornam cálculos estatíscos sobre os dados.

» ag = np.array([3.3, 12.5, 11.2, 5.7, 0.3])
» ag
↳ array([ 3.3, 12.5, 11.2,  5.7,  0.3])

» # outputs nos comentários
» ag.sum()       # 33.0
» ag.min()       # 0.3
» ag.max()       # 12.5
» ag.mean()      # 6.6
» ag.std()       # 4.6337889464238655
» ag.var()       # 21.472
» ag.argmin()    # 4
» ag.argmax()    # 1

» # o mesmo vale para arrays com outros shapes
» ag23 =  np.random.random(6).reshape(2,3) -.5
» ag23
↳ array([[ 0.10425075, -0.29335437, -0.36814244],
         [ 0.32986805,  0.17289794, -0.4568041 ]])

» ag23.mean()
↳ -0.08521402850598175

Diversas das operações podem ser feitas sobre o array inteiro ou sobre linhas ou colunas. Para isso podemos especificar axis = 0 para operações sobre elementos das colunas, axis = 1 para operações sobre elementos das linhas.

» arr = np.random.randn(4,5).round(2)
» arr
↳ array([[ 0.45, -0.11, -0.54, -0.97, -0.23],
         [ 1.65,  0.76, -0.39, -1.83,  0.02],
         [ 0.45, -1.22,  1.93,  1.92, -0.43],
         [-0.39,  2.01,  0.04,  0.67, -1.1 ]])

» # a soma de todos os elementos
» arr.sum()
↳ 2.689999999999999

» # soma sobre elementos de cada coluna
» arr.sum(axis=0)       # ou arr.sum(0)
↳ array([ 2.17,  0.37,  0.17, -2.49, -3.13])

» # soma sobre elementos de cada linha
» arr.sum(axis=1)       # ou arr.sum(1)
↳ array([-0.42, -1.67,  0.6 , -1.42])

» # produtos dos elementos das linhas
» arr.prod(axis=1).round(2)
↳ array([0.01, 0.02, 0.87, 0.02])

» # produtos dos elementos das colunas
» arr.prod(axis=0).round(2)
↳ array([-0.13,  0.21,  0.02,  2.28, -0.  ])


Funções básicas de agregação:

Função retorna
np.all booleano, True se todos os elementos no array são não nulos
np.any booleano, True se algum dos elementos no array é não nulo
np.sum soma dos elementos do array ou sobre eixo especificado †.
np.mean Média aritmética; arrays de comprimento nulo têm média = NaN
np.std, np.var variância e desvio padrão
np.max, np.min valor máximo e mínimo no array
np.argmax, np.argmin índices do valor máximo e mínimo no array
np.cumsum soma cumulativa dos elementos, começando em 0
np.cumprod produto cumulativo dos elementos, começando em 1

(†) Para arrays de comprimento nulo tem soma np.sum = NaN.

🔺Início do artigo

Bibliografia

  • Blair, Steve: Python Data Science, edição do autor, 2019.
  • Harrison, Matt: Learning Pandas, Python Tools for Data Munging, Data Analysis, and Visualization,
    Treading on Python Series, Prentiss, 2016.
  • Johansson, Robert: Numerical Python, Scientific Computing and Data Science Applications with Numpy, SciPy and Matplotlib, 2nd., Chiba, Japan, 2019.
  • McKinney, Wes: Python for Data Analysis, O’Reilly Media, Sebastopol CA, 2018.
  • McKinney, Wes, Pandas Development Team: pandas: powerful Python data analysis toolkit Release 1.2.1,
  • Miller, Curtis: Hands-On Data Analysis with NumPy and pandas, Packt Publishing, Birmingham, 2018.
  • Nelli, Fabio: Python Data Analytics With Pandas, NumPy, and Matplotlib, 2nd., Springer, New York, 2018.
  • Site AI Ensina: Entendendo a biblioteca NumPy, acessado em julho de 2021.
  • Site GeeksforGeeks: Python NumPy, acessado em julho de 2021.
  • Site W3 Schools: NumPy Tutorial, acessado em julho de 2021.
  • NumPy, docs.
  • NumPy, Learn.

Nesse site: