NumPy

O NumPy é uma das principais bibliotecas para computação científica em Python. Com ela é possível criar arrays multidimensionais de alta performance, além de possuir ferramentas para manipular estes arrays. O NumPy serve de base para outras bibliotecas e frameworks na área de ciência de dados e inteligência artificial.

O NumPy é frequentemente importado da seguinte forma:

In [1]:
import numpy as np

Arrays

Os arrays do NumPy podem armazenar dados multidimensionais, porém todos os dados devem ser do mesmo tipo.

Vamos criar um array a partir de uma lista.

In [2]:
a = np.array([1, 2, 3])

Podemos acessar os valores do array e alterá-los.

In [3]:
print(a[0], a[1], a[2])

a[0] = 9

print(a)
1 2 3
[9 2 3]

A propriedade shape retorna o tamanho do array.

In [4]:
a.shape
Out[4]:
(3,)

Vamos agora criar um array com duas dimensões.

In [5]:
b = np.array([[1, 2, 3], [4, 5, 6]])

print(b)
print(b.shape)
[[1 2 3]
 [4 5 6]]
(2, 3)

A propriedade shape agora traz o tamanho do array em cada uma das dimensões. Nesse caso, interpretamos o tamanho como: 2 linhas e 3 colunas.

A indexação para arrays com 2 ou mais dimensões é feita através de uma tupla.

In [6]:
print(b[0, 0], b[0, 1], b[0, 2])
print(b[1, 0], b[1, 1], b[1, 2])
1 2 3
4 5 6
In [7]:
b[0, 0] = 9
print(b[0, 0], b[0, 1], b[0, 2])
print(b[1, 0], b[1, 1], b[1, 2])
9 2 3
4 5 6

O NumPy traz algumas funções para facilitar a criação de arrays.

np.zeros cria um array com todos os elementos igual a zero.

In [8]:
a = np.zeros((2, 2))   # (2, 2) é o tamanho do array
print(a)
[[0. 0.]
 [0. 0.]]

np.ones cria um array com todos os elementos igual a um.

In [9]:
b = np.ones((2, 2))    # (2, 2) é o tamanho do array
print(b)
[[1. 1.]
 [1. 1.]]

np.full cria um array com todos os elementos iguais a uma constante passada como parâmetro.

In [10]:
c = np.full((2, 2), 7)  # (2, 2) é o tamanho do array e 7 é a constante
print(c)
[[7 7]
 [7 7]]

np.arange cria um array com um intervalo de números.

In [11]:
d1 = np.arange(10)  # intervalo de 0 até antes de 10
print(d1)

d2 = np.arange(1, 9)  # intervalo de 1 até antes de 9
print(d2)

d3 = np.arange(1, 9, 2)  # intervalo de 1 até antes de 9 com um passo de tamanho 2
print(d3)
[0 1 2 3 4 5 6 7 8 9]
[1 2 3 4 5 6 7 8]
[1 3 5 7]

np.eye Cria uma matriz identidade.

In [12]:
e = np.eye(2)  # 2 é o tamanho da matriz identidade
print(e)
[[1. 0.]
 [0. 1.]]

np.random.random Cria um array com valores aleatórios.

In [13]:
f = np.random.random((2, 2))  # (2, 2) é o tamanho do array
print(f)
[[0.11682408 0.89582127]
 [0.8772504  0.33403101]]

Tipos de dados

Ao criar um array, o NumPy tenta inferir o tipo de dado dos elementos.

In [14]:
a = np.array([1, 2, 3])
print(a.dtype)

b = np.array([1.0, 2.0, 3.0])
print(b.dtype)
int64
float64

Entretanto, é possível explicitar o tipo que deve ser usado.

In [15]:
a = np.array([1, 2, 3], dtype=float)
print(a.dtype)
float64

O NumPy suporta diversos tipos de dados que podem ser usados para armazenar de forma eficiente os diferentes valores suportados pela linguagem. Para mais detalhes: https://docs.scipy.org/doc/numpy/reference/arrays.dtypes.html

Indexação

Além da indexação vista anteriormente, o NumPy fornece outras maneiras de acessar os elementos de um array.

Slicing

Assim como as listas do Python, os arrays do NumPy também podem "fatiados". A sintaxe do slice é: meu_array[início:fim]. Onde início é a posição de início e fim é o limite final do slice.

In [16]:
a = np.array([1, 2, 3, 4, 5])
a[1:4]  # começa na posição 1 e termina antes da posição 4
Out[16]:
array([2, 3, 4])

Omitindo o início o slice é feito a partir do primeiro elemento. Omitindo o fim o slice é feito até o último elemento.

In [17]:
print(a[:4])
print(a[2:])
[1 2 3 4]
[3 4 5]

Arrays multidimensionais também suportam slicing.

In [18]:
b = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
print(b)
[[ 1  2  3  4]
 [ 5  6  7  8]
 [ 9 10 11 12]]
In [19]:
b[0:2, 1:3]
Out[19]:
array([[2, 3],
       [6, 7]])

A figura abaixo ilustra o resultado do slicing anterior. Na primeira dimensão, foi especificado o slice 0:2, que é representado pelo retângulo verde na figura. Esse slice é composto da primeira e da segunda linha. Na segunda dimensão, foi especificado o slice 1:3, representado pelo retângulo vermelho. Esse slice é composto da segunda e da terceira coluna. O resultado do slicing é a interseção dos retângulos verde e vermelho.

Podemos combinar slicing e atribuição.

In [20]:
a = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
a[5:] = 10
print(a)

b = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
b[2:5] = np.array([20, 30, 40])  # esse array precisa ser do mesmo tamanho do slice
print(b)
[ 0  1  2  3  4 10 10 10 10 10]
[ 0  1 20 30 40  5  6  7  8  9]

O resultado de um slice é uma espécie de janela para o array original. Assim, modificando o slice você modificará o array original.

In [21]:
a = np.array([1, 2, 3, 4, 5])
a_slice = a[1:3]

a_slice[0] = 9
print(a_slice)
print(a)
[9 3]
[1 9 3 4 5]

Indexação Avançada

A indexação avançada é utilizada para criar novos arrays a partir de um array original através de máscaras definidas por números inteiros ou valores booleanos.

Máscaras Booleanas

Para máscaras booleanas, definimos um array com a mesma quantidade de elementos do array original, onde True indica que o elemento naquela posição vai fazer parte do novo array e False indica que o elemento naquela posição não vai fazer parte do array.

In [22]:
a = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
mask = [True, True, True, True, True, True, False, False, False, False]

a[mask]
Out[22]:
array([0, 1, 2, 3, 4, 5])

Podemos criar máscaras booleanas aplicando operadores lógicos aos arrays.

In [23]:
a <= 5
Out[23]:
array([ True,  True,  True,  True,  True,  True, False, False, False,
       False])

Com isso, conseguimos usar a máscara booleana de forma direta.

In [24]:
a[a <= 5]
Out[24]:
array([0, 1, 2, 3, 4, 5])

Usando máscaras booleanas, podemos filtrar o array de acordo com os seus elementos. Por exemplo, para criar um array apenas com números pares, temos:

In [25]:
a[a%2 == 0]
Out[25]:
array([0, 2, 4, 6, 8])

As máscaras booleanas também podem auxiliar na atribuição de novos valores. No exemplo abaixo, vamos substituir todas as entradas nulas por zero.

In [26]:
b = np.array([20, 50, None, 35, 90, None, 110])
b[b == None] = 0

print(b)
[20 50 0 35 90 0 110]

As máscaras precisam ter as mesmas dimensões do array original.

In [27]:
a = np.array([[1, 2], [3, 4], [5, 6]])
mask = a > 2
print(mask)
[[False False]
 [ True  True]
 [ True  True]]

Entretanto, a indexação usando máscaras multidimensionais retornam arrays unidimensionais.

In [28]:
a[mask]
Out[28]:
array([3, 4, 5, 6])

Indexação com arrays de inteiros

Com a indexação com arrays de inteiros, novos arrays são criados escolhendo os elementos do array original de acordo com suas posições.

In [29]:
a = np.array([0, 10, 20, 30, 40, 50, 60, 70 , 80, 90])
a[[1, 5, 9]]
Out[29]:
array([10, 50, 90])

Na indexação, os índices não precisam aparecer em ordem.

In [30]:
a[[5, 1, 9]]
Out[30]:
array([50, 10, 90])

Os índices também podem ser repetidos.

In [31]:
a[[5, 1, 5, 9, 9, 5, 5]]
Out[31]:
array([50, 10, 50, 90, 90, 50, 50])

Assim como nas máscaras booleanas, podemos usar o array de inteiros para atribuições de novos valores.

In [32]:
a[[5, 1, 9]] = 100
a
Out[32]:
array([  0, 100,  20,  30,  40, 100,  60,  70,  80, 100])

Para arrays multidimensionais, precisamos passar arrays multidimensionais na indexação. No exemplo abaixo, são selecionados os elementos a[0,0] , a[1,1] e a[2,0].

In [33]:
a = np.array([[1 ,2], [3, 4], [5, 6]])
a[[0, 1, 2], [0, 1, 0]]
Out[33]:
array([1, 4, 5])

Operações elemento por elemento

Operações matemáticas básicas são realizadas elemento por elemento nos arrays.

In [34]:
a = np.array([1, 2, 3, 4, 5])

print(a + 1)  # 1 é somado a cada elemento do array
print(2 * a)  # 2 é multiplicado a cada elemento do array
[2 3 4 5 6]
[ 2  4  6  8 10]

Também conseguimos fazer operações matemáticas onde os dois operandos são arrays.

In [35]:
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])

print(a+b)  # cada elemento do array a é somado com o elemento do array b da posição correspondente
print(a*b)  # cada elemento do array a é mutiplicado pelo o elemento do array b da posição correspondente
[[ 6  8]
 [10 12]]
[[ 5 12]
 [21 32]]

No NumPy, usar o operador de multiplicação entre arrays não significa que estamos fazendo multiplicação de matrizes. Para multiplicar duas matrizes usamos a função np.dot.

In [36]:
print(a*b)
print(np.dot(a, b))
[[ 5 12]
 [21 32]]
[[19 22]
 [43 50]]

Além de operações aritméticas, também é possível fazer operações lógicas entre arrays.

In [37]:
a = np.array([1, 2, 3, 4])
b = np.array([4, 2, 2, 4])
a == b
Out[37]:
array([False,  True, False,  True])

Usando o operador == foi realizada uma comparação elemento por elemento e o resultado foi mostrado elemento por elemento. Para comparar se todos os elementos de dois arrays são iguais usamos a função np.array_equal.

In [38]:
c = np.array([1, 2, 3, 4])

print(np.array_equal(a, b))
print(np.array_equal(a, c))
False
True

Para as operações OR e AND, temos:

In [39]:
a = np.array([1, 1, 0, 0])
b = np.array([1, 0, 1, 0])

print(a | b)  # OR
print(a & b)  # AND
[1 1 1 0]
[1 0 0 0]

Agregações

Funções de agregação são úteis para sumarizar informações contidas nos arrays.

np.sum soma todos os valores de um array.

In [40]:
a = np.array([1, 2, 3, 4])
np.sum(a)
Out[40]:
10
In [41]:
b = np.array([[1, 2, 3], [4, 5, 6]])
np.sum(b)
Out[41]:
21

Utilizando o parâmetro axis especificamos a agregação numa determinada dimensão.

In [42]:
np.sum(b, axis=0)  # 0 é a primeira dimensão, ou seja, as colunas
Out[42]:
array([5, 7, 9])
In [43]:
np.sum(b, axis=1)  # 1 é a primeira dimensão, ou seja, as linhas
Out[43]:
array([ 6, 15])

np.min encontra o valor mínimo de um array e np.max encontra o valor máximo.

In [44]:
print(np.min(a))
print(np.min(b))

print(np.max(a))
print(np.max(b))
1
1
4
6

Utilizando o parâmetro axis encontramos o valor máximo ou mínimo numa determinada dimensão.

In [45]:
print(np.min(b, axis=0))  # encontra o valor mínimo por coluna
print(np.min(b, axis=1))  # encontra o valor mínimo por linha

print(np.max(b, axis=0))  # encontra o valor máximo por coluna
print(np.max(b, axis=1))  # encontra o valor máximo por linha
[1 2 3]
[1 4]
[4 5 6]
[3 6]

np.mean calcula a média dos elementos de um array, np.median calcula a mediana e np.std calcula o desvio padrão.

In [46]:
print(np.mean(a))
print(np.mean(b))

print(np.median(a))
print(np.median(b))

print(np.std(a))
print(np.std(b))
2.5
3.5
2.5
3.5
1.118033988749895
1.707825127659933
In [47]:
print(np.mean(b, axis=0))
print(np.mean(b, axis=1))

print(np.median(b, axis=0))
print(np.median(b, axis=1))

print(np.std(b, axis=0))
print(np.std(b, axis=1))
[2.5 3.5 4.5]
[2. 5.]
[2.5 3.5 4.5]
[2. 5.]
[1.5 1.5 1.5]
[0.81649658 0.81649658]

np.any avalia se algum elemento satisfaz uma expressão booleana, np.all avalia se todos os elementos satisfazem uma expressão booleana.

In [48]:
c = np.array([1, 2, 3, 4])
d = np.array([2, 4, 6, 8])

print(np.any(c == 2))  # True, pois um dos elementos de c é igual a 2
print(np.all(c == 2))  # False, pois existem elementos de c que não são iguais a 2
print(np.all(c < 5))  # True, pois todos os elementos de c são menores que 5
True
False
True
In [49]:
print(np.any(c % 2 == 0))  # True, pois o elemento 2 e o elemento 4 são divisíveis por 2
print(np.all(c % 2 == 0))  # False, pois o elemento 1 e o element 3 não são divisíveis por 2
print(np.any(d % 2 == 0))  # True, pois todos os elementos de d são divisíveis por 2
True
False
True

Broadcast

Para arrays de mesmo tamanho, as operações são realizadas elemento por elemento. Entretanto, através do broadcast, o NumPy permite que essas operações sejam feitas usando arrays com diferentes dimensões.

In [50]:
a = np.array([[1, 2, 3],
              [4, 5, 6]])

b = np.array([10, 20, 30])

a+b
Out[50]:
array([[11, 22, 33],
       [14, 25, 36]])

Percebam que o NumPy somou a linha [1, 2, 3] com o array [10, 20, 30] e somou a linha [4, 5, 6] com o mesmo array [10, 20, 30]. Através do broadcast, o NumPy "estica" o array com menos dimensões repetindo os seus dados. A soma anterior é a mesma que:

In [51]:
a = np.array([[1, 2, 3],
              [4, 5, 6]])

b = np.array([[10, 20, 30],
              [10, 20, 30]])

a+b
Out[51]:
array([[11, 22, 33],
       [14, 25, 36]])

O broadcast segue três regras para determinar a interação entre os arrays:

  1. Se dois arrays possuirem dimensões diferentes, o formato do array com menos dimensões é acrescido de 1 do lado esquerdo.

  2. Se os tamanhos dos arrays não forem iguais em alguma dimensão, o array com tamanho igual a 1 numa determinada dimensão é esticado, repetindo seus elementos, para ficar igual ao outro tamanho.

  3. Se os tamanhos dos array não forem iguais e nenhum dos dois for igual a 1, então é retornado um erro.

In [52]:
a = np.array([[1, 2, 3],
              [4, 5, 6]])

b = np.array([10, 20, 30])

print(a.shape)
print(b.shape)
(2, 3)
(3,)

Nesse caso, b tem menos dimensões que a. Assim, de acordo com a regra número 1, seu formato vai ter 1 adicionado do lado esquerdo, resultando em (1, 3).

Agora, o formato de a é (2, 3) e o de b é (1, 3). Seguindo a regra 2, b vai ser esticado para ficar com tamanho (2, 3).

Utilizando as regras do broadcast, percebemos que é possível esticar dois arrays ao mesmo tempo.

In [53]:
a = np.array([1, 2, 3])
b = np.array([[10],
              [20],
              [30]])

a+b
Out[53]:
array([[11, 12, 13],
       [21, 22, 23],
       [31, 32, 33]])
In [54]:
print(a.shape)
print(b.shape)
(3,)
(3, 1)

Nesse caso, a tem menos dimensões que b. Assim, seu formato vai ter 1 adicionado do lado esquerdo, resultando em (1, 3).

Agora, a tem o formato (1, 3) e b tem o formato (3, 1). Seguindo a regra 2, a vai ser esticado para ficar com tamanho (3, 3) e b vai ser esticado para ficar com o tamanho (3, 3).

Alterando dimensões

O NumPy permite que um array tenha o seu formato alterado para distribuir seus valores usando diferentes dimensões.

In [55]:
a = np.array([1, 2, 3, 4])

a.reshape((2, 2))  # transforma o array a num array 2x2
Out[55]:
array([[1, 2],
       [3, 4]])

Note que a quantidade de elementos do array inicial precisa ser a mesma do novo array.

Podemos usar o reshape para transformar um array multidimensional num array de uma dimensão.

In [56]:
b = np.array([[1, 2, 3],
              [4, 5, 6]])

b.reshape((6,))
Out[56]:
array([1, 2, 3, 4, 5, 6])

Também podemos converter um array de uma dimensão em uma matriz coluna ou matriz linha.

In [57]:
print(a.reshape((1, 4)))  # matriz linha
print(a.reshape((4, 1)))  # matriz coluna
[[1 2 3 4]]
[[1]
 [2]
 [3]
 [4]]

Trabalhando com matrizes, uma operação comum é obtenção de uma nova matriz através da troca de suas dimensões, também conhecida como a matriz transposta.

In [58]:
b.T
Out[58]:
array([[1, 4],
       [2, 5],
       [3, 6]])

Ordenação

Para ordenar um array usamos a função np.sort() que implementa o algoritmo quicksort.

In [59]:
a = np.array([4, 1, 3, 5, 2])
np.sort(a)
Out[59]:
array([1, 2, 3, 4, 5])

A função anterior retorna o array ordenado. Se você desejar ordenar o próprio array, basta usar o método sort().

In [60]:
print(a)
a.sort()
print(a)
[4 1 3 5 2]
[1 2 3 4 5]

Ao invés de ordenar o array, é possível obter um array com os índices dos elementos ordenados utilizando a função np.argsort().

In [61]:
a = np.array([4, 1, 3, 5, 2])
np.argsort(a)
Out[61]:
array([1, 4, 2, 0, 3])

O resultado anterior significa que o menor elemento está na posição 1, o segundo menor na posição 4, o terceiro menor na posição 2, o quarto menor na posição 0 e o quinto menor (ou o maior) na posição 3.

Utilizando a indexação avançada, podemos usar o resultado do np.argsort() para obter o array ordenado.

In [62]:
a = np.array([4, 1, 3, 5, 2])
sort_pos = np.argsort(a)

a[sort_pos]
Out[62]:
array([1, 2, 3, 4, 5])

Por fim, o parâmetro axis está disponível para as funções de ordenação. Com ele, especificamos que a ordenação deve ser feita através de uma determinada dimensão.

In [63]:
b = np.array([[4, 1, 3, 5, 2],
              [2, 5, 2, 3, 1],
              [3, 2, 5, 1, 4]])

np.sort(b, axis=0)  # ordena cada coluna separadamente
Out[63]:
array([[2, 1, 2, 1, 1],
       [3, 2, 3, 3, 2],
       [4, 5, 5, 5, 4]])
In [64]:
np.sort(b, axis=1)  # ordena cada linha separadamente
Out[64]:
array([[1, 2, 3, 4, 5],
       [1, 2, 2, 3, 5],
       [1, 2, 3, 4, 5]])
In [65]:
np.sort(b)  # omitindo axis, a última dimensão é usada, nesse caso, as linhas
Out[65]:
array([[1, 2, 3, 4, 5],
       [1, 2, 2, 3, 5],
       [1, 2, 3, 4, 5]])
In [66]:
np.sort(b, axis=None)  # usando None, o array é transformado em unidimensional e depois ordenado
Out[66]:
array([1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5])