3.1. NumPy

NumPy é um pacote fundamental para computação científica em Python, sendo usado como base para praticamente todos os outros. Suas capacidades incluem uma poderosa classe array, que pode representar vetores e matrizes, sofisticadas funções para manipulação de arrays, ferramentas para integração de códigos em C/C++ e Fortran, além de funções de álgebra linear e de geração de números aleatórios. Os arrays de NumPy também podem ser usados para armazenar dados de tipos genéricos, permitindo fácil integração com diferentes tipos de bases e bancos de dados. A documentação completa para todas as versões de NumPy pode ser encontrada em numpy.org/doc.

3.1.1. A classe array

A classe array é o principal componente de NumPy e representa uma lista homogênea multidimensional, na forma de uma tabela de elementos (normalmente números), todos do mesmo tipo, indexada por tuplas de inteiros positivos. Um dimensão de um array é chamada de eixo (axis). Abaixo, temos dois exemplos de arrays (note sua criação por meio de listas):

import numpy as np

a = np.array([1, 2, 1])  # Uma dimensão, 3 elementos, tipo inteiro
b = np.array([
    [1., 0., 1.], 
    [0., 1., 2.]
])  # Duas dimensões, a primeira com tamanho 2 e a segunda com tamanho 3, tipo ponto flutuante

print(len(a), a.dtype)
print(b.shape, len(b), b.dtype)
print(b)
3 int64
(2, 3) 2 float64
[[1. 0. 1.]
 [0. 1. 2.]]

A biblioteca padrão de Python também tem uma classe array.array, mas ela serve apenas pra casos unidimensionais e oferece menos funcionalidades. Os atributos mais importantes do array de numpy incluem:

  • array.ndim: o número de eixos do array

  • array.shape: as dimensões do array na forma de uma tupla indicando o tamanho de cada dimensão; matriz \(n \times m\) possui shape \((n, m)\)

  • array.size: o número total de elementos no array

  • array.dtype: o tipo dos elementos do array

3.1.1.1. Criando arrays

A criação de um array pode ser feita usando listas ou tuplas, com o tipo dos dados sendo deduzido através dos tpos dos elementos da sequência. Uma lista de listas resultará em um array bidimensional, uma lista de listas de listas dará um array tridimensional e assim por diante. É possível especificar o tipo do array na hora da criação:

b = np.array(
    [
        [1., 0., 1.], 
        [0., 1., 2.]
    ],
    dtype=complex
)
print(b)
[[1.+0.j 0.+0.j 1.+0.j]
 [0.+0.j 1.+0.j 2.+0.j]]

Frequentemente é necessario criar arrays com um tamanho definido e valores iniciais que serão posteriormente modificados. Para isso, NumPy fornece diversas funções, incluindo a função zeros, que cria um array preenchido com zeros; ones, que cria um array preenchido com uns; empty, que cria um array cujo conteúdo inicial é aleatório e depende do estado da memória; e eye, que cria uma matriz identidade.

print(np.zeros((3, 4)))  # Note que as dimensões são passadas como uma tupla
print()
print(np.ones((3, 4)))
print()
print(np.empty((3, 4)))  # Saída pode variar
print()
print(np.eye(3, dtype=np.int32))  # Basta passar uma dimensão, pois a matriz será quadrada
[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]

[[1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]]

[[1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]]

[[1 0 0]
 [0 1 0]
 [0 0 1]]

Para criar sequências de números, pode-se usar a função arange, que é análoga à função range, mas retorna arrays e aceita criar intervalos de pontos flutuantes.

print(np.arange(5, 20, 5))
print()
print(np.arange(0, 2, 0.4))
[ 5 10 15]

[0.  0.4 0.8 1.2 1.6]

Devido a possíveis problemas de arredondamento, não é sempre possível saber quantos elementos serão gerados pela função arange ao receber pontos flutuantes como parâmetros. Nesses casos, é melhor usar a função linspace, que recebe como argumentos o início, o fim (inclusivo) e o número de valores desejados no intervalo.

print(np.linspace(0, 1, 9))  # 9 números começando em 0 e terminando em 1
[0.    0.125 0.25  0.375 0.5   0.625 0.75  0.875 1.   ]

A função linspace é útil para avaliar funções em muitos pontos.

import matplotlib.pyplot as plt

x = np.linspace(0, 2 * np.pi, 100)
f = np.sin(x)

plt.plot(x, f)
plt.show()
../_images/02_nump_11_0.png

3.1.1.2. Operações básicas

Operações aritméticas com arrays sempre são aplicadas elemento-a-elemento, criando um novo array como resultado:

a = np.array([10, 20, 30, 40])
print(a)
b = np.arange(4)
print(b)
print()

c = a - b
print(c)
print()

print(b ** 2)
print()

print(a < 25)
print()

print(a * b)
[10 20 30 40]
[0 1 2 3]

[10 19 28 37]

[0 1 4 9]

[ True  True False False]

[  0  20  60 120]

O operador ‘*’ multiplica os arrays através dos elementos. Para realizar a multiplicação de matrizes, pode-se usar o operador ‘@’, a função dot do NumPy, ou o método dot do array:

A = np.array(
    [
        [1, 1],
        [0, 1]
    ]
)

B = np.array(
    [
        [2, 0],
        [0, 2]
    ]
)

print(A @ B)
print(np.dot(A, B))
print(A.dot(B))
[[2 2]
 [0 2]]
[[2 2]
 [0 2]]
[[2 2]
 [0 2]]

Algumas operações, como as de atribuição aritmética “+=” e “*=”, modificam o array diretamente, ao invés de criar outro com o resultado.

A = np.array(
    [
        [1, 1],
        [0, 1]
    ]
)

A += 1

print(A)
[[2 2]
 [1 2]]

Operações com arrays de tipos diferentes resultam em um array do tipo mais geral ou preciso (upcasting). Exemplo:

a = np.ones(3, dtype=int)
b = np.ones(3, dtype=float)

print((a + b).dtype)
float64

Muitas operações são computadas como métodos da classe array, por exemplo a soma de todos os elementos, a média ou o desvio-padrão:

a = np.random.random((5, 3))
print(a)
print(
    'Soma: {}, Média: {}, Desvio-padrão: {}'.format(
        a.sum(), a.mean(), a.std()
    )
)
[[0.81296599 0.09935408 0.65092664]
 [0.76496675 0.85090531 0.69975859]
 [0.45251882 0.85895655 0.30661229]
 [0.59373761 0.66245283 0.16172554]
 [0.65839727 0.0174765  0.77209149]]
Soma: 8.36284626413465, Média: 0.5575230842756433, Desvio-padrão: 0.27263141074030073

Por padrão, essas operações são computadas sobre todos os elementos do array, independente das suas dimensões. No entanto, é possível especificar a dimensão desejada, usando o parâmetro axis, como mostra o código abaixo:

print(a.sum(axis=0)) # soma de cada coluna
print(a.mean(axis=1))  # média de cada linha  
print(a.cumsum(axis=0))  # soma acumulada de cada coluna
[3.28258644 2.48914528 2.59111455]
[0.52108224 0.77187688 0.53936256 0.47263866 0.48265508]
[[0.81296599 0.09935408 0.65092664]
 [1.57793274 0.9502594  1.35068522]
 [2.03045156 1.80921595 1.65729752]
 [2.62418918 2.47166878 1.81902306]
 [3.28258644 2.48914528 2.59111455]]

3.1.1.3. Indexando e iterando sobre elementos

Arrays unidimensionais podem ser indexados, fatiados e iterados exatamente como listas e outras coleções.

a = np.arange(8) ** 2
print(a)
print()

print(a[2])
print()

print(a[2:5])
print()

a[:4:2] = -1000
print(a)
print()

print(a[ : :-1])
print()

for i in a:
    print(i * 2)
[ 0  1  4  9 16 25 36 49]

4

[ 4  9 16]

[-1000     1 -1000     9    16    25    36    49]

[   49    36    25    16     9 -1000     1 -1000]

-2000
2
-2000
18
32
50
72
98

Arrays multidimensionais podem receber um índice ou fatia por eixo, informados em uma tupla. Por exemplo:

b = np.random.random((5, 4))
print(b)
print()

print(b[2, 3])  # Elemento na terceira linha e quarta coluna
print()

print(b[:, 1])  # A segunda coluna inteira
print()

print(b[:4, 1])  # Do primeiro ao quarto elemento da segunda coluna      
print()

print(b[1:3, :])  # Todas as colunas da segunda à terceira linha              
[[0.56065199 0.07415559 0.8151899  0.83594075]
 [0.99652337 0.56821437 0.96664614 0.39391584]
 [0.23798191 0.62870993 0.58003876 0.05682516]
 [0.41417606 0.8139878  0.77483601 0.68228058]
 [0.38807113 0.06093388 0.90599298 0.3480452 ]]

0.056825155331797994

[0.07415559 0.56821437 0.62870993 0.8139878  0.06093388]

[0.07415559 0.56821437 0.62870993 0.8139878 ]

[[0.99652337 0.56821437 0.96664614 0.39391584]
 [0.23798191 0.62870993 0.58003876 0.05682516]]

Quando os índices forem fornecidos em uma tupla menor do que o número de eixos, os índices que não forem fornecidos são considerados fatias completas, i.e. “:”. Os índices faltantes também podem ser representados por reticências. Exemplo:

print(b[-1])
print(b[-1, :])
print(b[-1, ...])
[0.38807113 0.06093388 0.90599298 0.3480452 ]
[0.38807113 0.06093388 0.90599298 0.3480452 ]
[0.38807113 0.06093388 0.90599298 0.3480452 ]

NumPy oferece mais formas de indexar os seus arrays, usando listas ou arrays de inteiros ou booleanos. Quando o array b é multidimensional, a indexação usando uma única lista de índices se refere à primeira dimensão de b.

print(b)
print()

print(b[[1, 4]])  # Segunda e quinta linhas da matriz b, equivale a b[[1, 4], :]
print()

print(b[:, [1, 2]])  # Segunda e terceira colunas da matriz b
print()

print(b[[1, 4], [1, 2]])  # Indexação pareada, equivale a [b[1,1], b[4, 2]]
print()

i = [
    [1, 1],
    [4, 4]
]
j = [
    [1, 2],
    [1, 2]
]

print(b[i, j])  # Indexação pareada, segunda e terceira colunas da segunda e quinta linhas
print()

print(b[
    [[1], [4]],  # Equivale a b[i, j], note que o array
    [1, 2]       # de índices do primeiro eixo é bidimensional
])
print()

b[i, j] = 0  # As posições indexadas podem receber valores
print(b)
[[0.56065199 0.07415559 0.8151899  0.83594075]
 [0.99652337 0.56821437 0.96664614 0.39391584]
 [0.23798191 0.62870993 0.58003876 0.05682516]
 [0.41417606 0.8139878  0.77483601 0.68228058]
 [0.38807113 0.06093388 0.90599298 0.3480452 ]]

[[0.99652337 0.56821437 0.96664614 0.39391584]
 [0.38807113 0.06093388 0.90599298 0.3480452 ]]

[[0.07415559 0.8151899 ]
 [0.56821437 0.96664614]
 [0.62870993 0.58003876]
 [0.8139878  0.77483601]
 [0.06093388 0.90599298]]

[0.56821437 0.90599298]

[[0.56821437 0.96664614]
 [0.06093388 0.90599298]]

[[0.56821437 0.96664614]
 [0.06093388 0.90599298]]

[[0.56065199 0.07415559 0.8151899  0.83594075]
 [0.99652337 0.         0.         0.39391584]
 [0.23798191 0.62870993 0.58003876 0.05682516]
 [0.41417606 0.8139878  0.77483601 0.68228058]
 [0.38807113 0.         0.         0.3480452 ]]

Para indexar arrays usando arrays ou listas de booleanos, pode-se usar um array com exatamente a mesma quantidade de elementos que o array indexado. Isso pode ser muito útil para fazer atribuições. Exemplo:

a = b < 0.5

print(a)
print()

print(b[a])  # array unidimensional com os elementos selecionados
print()

b[a] = 0
print(b)
[[False  True False False]
 [False  True  True  True]
 [ True False False  True]
 [ True False False False]
 [ True  True  True  True]]

[0.07415559 0.         0.         0.39391584 0.23798191 0.05682516
 0.41417606 0.38807113 0.         0.         0.3480452 ]

[[0.56065199 0.         0.8151899  0.83594075]
 [0.99652337 0.         0.         0.        ]
 [0.         0.62870993 0.58003876 0.        ]
 [0.         0.8139878  0.77483601 0.68228058]
 [0.         0.         0.         0.        ]]

Também é possível indexar cada dimensão usando arrays unidimensionais de booleanos com o mesmo tamanho da dimensão indexada:

b1 = np.array([True, False, True, False, False])
b2 = np.array([True, True, False, False])

print(b[b1])  # Equivale a b[b1, :]
print()

print(b[:, b2])
print()

print(b[b1,b2])
print()
[[0.56065199 0.         0.8151899  0.83594075]
 [0.         0.62870993 0.58003876 0.        ]]

[[0.56065199 0.        ]
 [0.99652337 0.        ]
 [0.         0.62870993]
 [0.         0.8139878 ]
 [0.         0.        ]]

[0.56065199 0.62870993]

Iterações sobre arrays multidimensionais são feitas ao longo do primeiro eixo:

for row in b:
    print(row)
[0.56065199 0.         0.8151899  0.83594075]
[0.99652337 0.         0.         0.        ]
[0.         0.62870993 0.58003876 0.        ]
[0.         0.8139878  0.77483601 0.68228058]
[0. 0. 0. 0.]

3.1.1.4. Redimensionando arrays

Como vimos acima, o tamanho das dimensões do array pode ser obtido por meio do atributo shape:

a = np.arange(12)
print(a)
print(a.shape)
[ 0  1  2  3  4  5  6  7  8  9 10 11]
(12,)

No entanto, a forma do array não é fixa e pode ser modificada de diversas formas, retornando um novo array com os mesmos elementos do array original, mas reposicionados para se adequar à nova forma.

c = a.reshape(4, 3)
print(c)
print()

print(c.ravel())  # retorna o array "achatado" 
print()

print(c.T)  # retorna o array transposto
print()
[[ 0  1  2]
 [ 3  4  5]
 [ 6  7  8]
 [ 9 10 11]]
[ 0  1  2  3  4  5  6  7  8  9 10 11]

[[ 0  3  6  9]
 [ 1  4  7 10]
 [ 2  5  8 11]]

A função reshape retorna um novo array. Para modificar o próprio array, pode-se usar a função resize:

a.resize(4, 3)
print(a)
[[ 0  1  2]
 [ 3  4  5]
 [ 6  7  8]
 [ 9 10 11]]

Se alguma dimensão receber o valor -1 para uma operação de redimensionamento, o seu novo tamanho será automaticamente calculado. Por exemplo, se quisermos que a tenha 2 linhas e o número necessário de colunas, podemos fazer:

a.reshape(2, -1)
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11]])

Se tentarmos tranpor um array unidimensional usando o atributo T, ele continuará sendo unidimensional. Portanto, para realizar a transposição, podemos usar o método reshape, com -1 no número de linhas, gerando um vetor coluna com o número necessário de linhas:

a = np.arange(12)
print(a)
print(a.T)
print(a.reshape(-1, 1))
[ 0  1  2  3  4  5  6  7  8  9 10 11]
[ 0  1  2  3  4  5  6  7  8  9 10 11]
[[ 0]
 [ 1]
 [ 2]
 [ 3]
 [ 4]
 [ 5]
 [ 6]
 [ 7]
 [ 8]
 [ 9]
 [10]
 [11]]

Outra forma de transpor um array unidimensional para obter um vetor coluna é usando o atalho de NumPy para criar novos eixos:

a[:, np.newaxis]
array([[ 0],
       [ 1],
       [ 2],
       [ 3],
       [ 4],
       [ 5],
       [ 6],
       [ 7],
       [ 8],
       [ 9],
       [10],
       [11]])

3.1.1.5. Concatenando arrays

As funções vstack e hstack permitem concatenar dois ou mais arrays verticalmente e horizontalmente, respectivamente:

a = np.arange(6).reshape(3, 2)
b = np.random.random((3, 2))

print(np.vstack((a, b)))
print()

print(np.hstack((a, b)))
[[0.         1.        ]
 [2.         3.        ]
 [4.         5.        ]
 [0.32312324 0.90765726]
 [0.14160601 0.45340747]
 [0.96750857 0.57539334]]

[[0.         1.         0.32312324 0.90765726]
 [2.         3.         0.14160601 0.45340747]
 [4.         5.         0.96750857 0.57539334]]

A função column_stack concatena dois ou mais arrays unidimensionais na forma de colunas em um array 2D resultante:

a = np.arange(3)
b = np.random.random(3)

print(np.column_stack((a, b)))
print()

print(np.hstack((a, b)))  # resultado diferente
[[0.         0.91994108]
 [1.         0.63254906]
 [2.         0.68542513]]

[0.         1.         2.         0.91994108 0.63254906 0.68542513]

Essas funções de concatenação são todas casos especiais de uso mais comum da função concatenate, que permite definir o eixo sobre o qual ocorrerá a concatenação.

3.1.1.6. Cópias e vistas

Ao operar com arrays, os dados são às vezes copiados em um novo array e às vezes não, o que pode confundir iniciantes. Por exemplo, atribuições simples não criam cópias do array, i.e. elas apenas criam ponteiros para o mesmo objeto:

a = np.arange(12)
b = a
print(b is a)

b.shape = 3,4  # outra forma de mudar a forma de um array
print(a.shape)  # o comando acima muda a forma de a também
True
(3, 4)

Ao chamar funções, Python passa objetos mutáveis como referências, portanto quando um array é passado como argumento para uma função, o mesmo objeto estará disponível no escopo da função chamada:

def f(x):
    print(id(x))

print(id(a))
f(a)
140376454380480
140376454380480

O método view cria um novo objeto, mas esse novo objeto “olha” para os mesmos dados, ou seja, ele é uma “vista”. Vejamos:

c = a.view()
print(c is a)
print(c.base is a)

c.shape = 2,6
print(a.shape)  # a não muda de forma

c[0,4] = 1234  # muda em a também
print(a)
False
True
(3, 4)
[[   0    1    2    3]
 [1234    5    6    7]
 [   8    9   10   11]]

Uma fatia de um array retorna uma vista:

s = a[:,1:3]
s[:] = 10

print(a)
[[   0   10   10    3]
 [1234   10   10    7]
 [   8   10   10   11]]

Para obter uma cópia independente de um array, usa-se o método copy:

d = a.copy()
print(d is a)
print(d.base is a)  # d não compartilha dados com a

d[0,0] = 9999
print(a)
False
False
[[   0   10   10    3]
 [1234   10   10    7]
 [   8   10   10   11]]

3.1.2. Álgebra linear

A maior parte das funções de álgebra linear de NumPy estão disponíveis no módulo np.linalg. Algumas funções importantes incluem calcular a inversa de uma matriz:

a = np.array([[1.0, 2.0], [3.0, 4.0]])
print(np.linalg.inv(a))
[[-2.   1. ]
 [ 1.5 -0.5]]

Calcular a pseudoinversa de uma matriz (se a matriz for invertível, a inversa será calculada):

print(np.linalg.pinv(a))
[[-2.   1. ]
 [ 1.5 -0.5]]

Calcular o determinante e o traço de uma matriz:

print(np.linalg.det(a))
print(np.trace(a))  # o traço está disponível no próprio np
-2.0000000000000004
5.0

Obter a diagonal de uma matriz como um vetor:

np.diag(a)
array([1., 4.])

Obter uma matriz diagonal a partir de um vetor:

np.diag(np.diag(a))
array([[1., 0.],
       [0., 4.]])

Resolve rum sistema de equações lineares:

y = np.array([[5.0], [7.0]])
np.linalg.solve(a, y)
array([[-3.],
       [ 4.]])

Encontrar os autovalores e os autovetores de uma matriz:

np.linalg.eig(a)  # retorna uma tupla (autovalores, autovetores)
(array([-0.37228132,  5.37228132]),
 array([[-0.82456484, -0.41597356],
        [ 0.56576746, -0.90937671]]))

3.1.3. Ordenando arrays

NumPy oferece algumas funções para ordenar arrays. A principal delas é a função sort, que retorna uma cópia ordenada do array passado como parâmetro:

a = np.array([12, 9, 13, -1, 0, -4, 2, 7, 1])
print(np.sort(a))
[-4 -1  0  1  2  7  9 12 13]

Caso a seja um array multidimensional, pode-se usar o parâmetro axis para indicar através de qual eixo a ordenação deve ser feita.

b = np.array([[12, 9, 13], [-1, 0, -4], [2, 7, 1]])
print(np.sort(b, axis=None))  # se axis for None, o array é aplainado antes de ordenar
print(np.sort(b, axis=0))  # axis=0 irá ordenar através das linhas
print(np.sort(b, axis=1))  # axis=1 irá ordenar através das colunas
[-4 -1  0  1  2  7  9 12 13]
[[-1  0 -4]
 [ 2  7  1]
 [12  9 13]]
[[ 9 12 13]
 [-4 -1  0]
 [ 1  2  7]]

Também é possível escolher o algoritmo de ordenação usando o parâmetro kind (cujo valor padrão é ‘quicksort’):

print(np.sort(a, kind='mergesort'))
[-4 -1  0  1  2  7  9 12 13]

Outra função importante é argsort, que ao invés de retornar o array ordenado, retorna os índices do array nas posições corretas para ordená-lo. A função argsort aceita os mesmos parâmetros da função sort.

print(a)
print(np.argsort(a))
print(a[np.argsort(a)])
print()

b = np.array([
    [12, 0, 1], 
    [-1, 9, -4], 
    [2, 7, 13]
])
print(np.argsort(b, axis=0))
[12  9 13 -1  0 -4  2  7  1]
[5 3 4 8 6 7 1 0 2]
[-4 -1  0  1  2  7  9 12 13]

[[1 0 1]
 [2 2 0]
 [0 1 2]]

3.1.4. Buscando valores

As funções amax e amin retornam o menor e o maior valor do array, respectivamente. Naturalmente, é possível definir o eixo desejado usando o parâmetro axis.

b = np.array([
    [12, 0, 1], 
    [-1, 9, -4], 
    [2, 7, 13]
])

print(np.amax(b))
print(np.amin(b))
print()

print(np.amax(b, axis=0))  # maior elemento de cada coluna
print(np.amin(b, axis=1))  # menor elemento de cada linha
13
-4

[12  9 13]
[ 0 -4  2]

De maneira similar à função argsort, as funções argmax e argmin retornam os índices do maior ou do menor elemeno do array respectivamente, sendo possível definir o eixo desejado.

print(np.argmax(b))  # considera o array aplainado (b.ravel())
print(np.argmin(b))
print()

print(np.argmax(b, axis=0))  # índice do maior elemento de cada coluna
print(np.argmin(b, axis=1))  # índice do menor elemento de cada linha
8
5

[0 1 2]
[1 2 0]

Uma outra função importante para encontrar valores em um array é a função where, que retorna uma tupla com os índices em que uma condição é verdadeira.

print(b)
print(b < 5)
print(np.where(b < 5))
[[12  0  1]
 [-1  9 -4]
 [ 2  7 13]]
[[False  True  True]
 [ True False  True]
 [ True False False]]
(array([0, 0, 1, 1, 2]), array([1, 2, 0, 2, 0]))

3.1.5. Geração de números aleatórios

NumPy fornece diversas funções de geração de números aleatórios por meio do pacote random. Por exemplo, para gerar números no intervalo [0.0, 1.0) uniformemente, pode-se usar a função random:

print(np.random.random())  # apenas um número
print(np.random.random(3))  # vetor com 3 números aleatórios
print(np.random.random((3, 2)))  # matriz aleatória 3x2
0.3176521107850725
[0.71620635 0.27393953 0.2189988 ]
[[0.28931034 0.75446745]
 [0.99294893 0.38465496]
 [0.39369781 0.42724032]]

Para gerar inteiros uniformemente no intervalo [a, b), pode-se usar a função randint:

print(np.random.randint(2, 5))  # inteiro em [2, 5)
print(np.random.randint(5))  # inteiro em [0, 5)
print(np.random.randint(2, 5, (3, 2)))  # matriz aleatória 3x2 em [2, 5)
4
3
[[2 2]
 [3 2]
 [4 4]]

Para obter uma amostra aleatória de um array, usa-se a função choice:

a = np.arange(8)
print(a)
print()
print(np.random.choice(a))  # um elemento aleatório de a
print()
print(np.random.choice(a, 3))  # três elementos aleatórios de a
print()
print(np.random.choice(a, (3, 2)))  # seis elementos aleatórios de a na forma 3x2
print()
print(np.random.choice(a, 3, replace=False))  # três elementos aleatórios de a sem reposição
print()
print(
    np.random.choice(
        a, 
        3, 
        replace=False,
        p=a/np.sum(a)
    )
)  # três elementos aleatórios de a sem reposição e com diferentes probabilidades
print()
print(np.random.choice(5))  # um elemento aleatório no range(5)
[0 1 2 3 4 5 6 7]

4

[5 5 3]

[[7 3]
 [2 7]
 [5 7]]

[0 3 4]

[6 7 4]

2

Para embaralhar os elementos de um array, pode-se usar a função shuffle, que modifica o próprio array, i.e. não retorna um novo array como resultado. Exemplo:

a = np.arange(12)

np.random.shuffle(a)
print(a)
[ 3  5  4  9  7  6  2  1 11 10  8  0]

Arrays multidimensionais são embaralhados apenas no primeiro eixo:

a = np.arange(12).reshape(3, 4)
print(a)
np.random.shuffle(a)
print(a)
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
[[ 4  5  6  7]
 [ 8  9 10 11]
 [ 0  1  2  3]]

Por curiosidade, para embaralhar também as colunas em um array 2D, pode-se usar a função de programação funcional apply_along_axis, que aplica uma função a um eixo do array.

np.apply_along_axis(np.random.shuffle, 0, a)
print(a)
print()
np.apply_along_axis(np.random.shuffle, 1, a)
print(a)
[[ 8  9  2  7]
 [ 4  5  6  3]
 [ 0  1 10 11]]

[[ 2  9  7  8]
 [ 3  4  6  5]
 [11 10  0  1]]

De forma similar à função shuffle, a função permutation também embaralha os conteúdos de um array, no entanto ela retorna um novo array como resultado. Caso o array seja multidimensional, ela também só embaralha o primeiro eixo. Uma diferença em relação à função shuffle é que ela pode receber como parâmetro um inteiro x, ao invés de um array. Nesse caso, ela retornará uma permutação do range(x).

a = np.arange(12)
print(a)
print(np.random.permutation(a))
print(a)
print()
print(np.random.permutation(10))
[ 0  1  2  3  4  5  6  7  8  9 10 11]
[10  6  7  2  0 11  4  3  9  5  1  8]
[ 0  1  2  3  4  5  6  7  8  9 10 11]

[3 0 5 8 9 4 6 2 1 7]

Em simulações estatísticas e experimentos, é fundamental que os resultados sejam reproduzíveis. Para isso, é preciso que os valores aleatórios gerados durante a execução sejam os mesmos sempre que script for executado. Para isso, pode-se determinar a “semente” do gerador de números pseudoaleatórios do NumPy, usando a função random.seed. Por exemplo: número gerado pelo código abaixo será sempre o mesmo.

np.random.seed(42)
print(np.random.random())
0.3745401188473625

3.1.5.1. Gerando amostras de diversas distribuições

Os valores que geramos até agora foram todos uniformemente distribuídos, mas NumPy oferece funções para gerar amostras de mais de 30 distribuições, incluindo normal, beta, log-normal, qui-quadrado, Dirichlet, exponencial, etc. De uma maneira geral, essas funções recebem como parâmetros os parâmetros da distribuição desejada e o tamanho da amostra:

print(np.random.normal(0, 2, (3,2)))  # Matriz 3x2 seguindo N(0, 4)
print()
print(np.random.beta(1, 2, (3,2)))  # Matriz 3x2 seguindo Beta(1, 2)
print()
print(np.random.dirichlet((1, 2, 1), 3))  # Matriz com 3 vetores que seguem Dirichlet(1, 2, 1)
print()
[[-2.22376024  0.63780437]
 [ 0.55808258  2.02103057]
 [-1.16175627 -1.05033961]]

[[0.09203035 0.61453255]
 [0.16729818 0.78251414]
 [0.40562146 0.09343537]]

[[0.65225853 0.22050243 0.12723905]
 [0.08776947 0.88863216 0.02359837]
 [0.39321582 0.4865287  0.12025548]]