Aprofundando em arranjos
Atribuição de um arranjo não copia os elementos!
x = np.linspace(0, 10, 11)
a = x
a[-1] = 1000
x[-1]
também se modifica para 1000?
Sim, porque arranjos são mutáveis! Da mesma forma que listas.
Para evitar isso, pode-se criar uma cópia:
a = x.copy()
O mesmo ocorre para os cortes:
a = x[4:] ## a é uma visualização de parte de x
a[-1] = 1000 ## muda x[-1]!
a = x[4:].copy()
a[-1] = 1000 ## não muda x[-1]
Operações elemento-a-elemento em arranjos
As duas seguintes declarações são matematicamente equivalentes:
a = a + b ## a e b são arranjos (arrays)
a += b
No entanto:
a = a + b
é calculado como:r1 = a + b
a = r1
a += b
é calculado comoa[i] += b[i]
para todos os índices;a += b
é uma adição elemento-a-elemento, pois modifica todo elemento dea
, além de resultar em um novo arranjo, o resultado dea + b
Operações elemento-a-elemento podem reduzir o consumo de memória
Considere:
a = (3*x**4 + 2*x + 4)/(x + 1)
Essas são os cálculos efetuados pelo computador:
r1 = x**4; r2 = 3*r1; r3 = 2*x; r4 = r1 + r3
r5 = r4 + 4; r6 = x + 1; r7 = r5/r6; a = r7
Com as operações elemento-a-elemento pode-se eliminar 4 arranjos extras:
a = x.copy()
a **= 4
a *= 3
a += 2*x
a += 4
a /= x + 1
Economiza memória mas não acelera tanto o código
Vamos medir o tempo computacional entre os dois:
In [1]: def expression(x):
...: return (3*x**4 + 2*x + 4)/(x + 1)
...:
In [2]: def expression_inplace(x):
...: a = x.copy()
...: a **= 4
...: a *= 3
...: a += 2*x
...: a += 4
...: a /= x + 1
...: return a
...:
In [3]: import numpy as np
In [4]: x = np.linspace(0, 1, 10000000)
In [5]: %timeit expression(x)
1 loops, best of 3: 771 ms per loop
In [6]: %timeit expression_inplace(x)
1 loops, best of 3: 728 ms per loop
Apenas 5% de ganho de velocidade. Quando os arranjos são grandes, você precisa economizar memória!
Operações úteis com arranjos
Faça um novo arranjo do mesmo tamanho que outro:
import numpy as np
x = np.linspace(0, 1, 100)
## x is numpy array
a = x.copy()
## ou
a = np.zeros(x.shape, x.dtype)
## ou
a = np.zeros_like(x) ## zeros e o mesmo tamanho de x
Convertendo para arranjos:
a = asarray(a)
b = asarray(algumArranjo, dtype=float) ## especificando o tipo dos dados
Testando se um objeto é um arranjo:
>>> type(a)
<type 'numpy.ndarray'>
>>> isinstance(a, ndarray)
True
Exemplo: vetorizando uma função constante
def f(x):
return 2
A função vetorizada deve retornar 2.
def fv(x):
return zeros(x.shape, x.dtype) + 2
Versão mais completa, válida para escalares e arranjos:
def f(x):
if isinstance(x, (float, int)):
return 2
elif isinstance(x, ndarray):
return zeros(x.shape, x.dtype) + 2
else:
raise TypeError(
'x must be int/float/ndarray, not %s' % type(x))
Indexação generalizada de arranjos
Lembra-se da forma de corte: a[f:t:i]
onde f, t e i são os índices início, fim e incremento?
Qualquer lista ou arranjo de inteiros pode ser utilizado para indicar um conjunto de índices:
>>> a = linspace(1, 8, 8)
>>> a
array([ 1., 2., 3., 4., 5., 6., 7., 8.])
>>> a[[1,6,7]] = 10
>>> a
array([ 1., 10., 3., 4., 5., 6., 10., 10.])
>>> a[range(2,8,3)] = -2 ## mesmo que a[2:8:3] = -2
>>> a
array([ 1., 10., -2., 4., 5., -2., 10., 10.])
Indexação generalizada de arranjos com expressões boleanas
>>> a < 0
[False, False, True, False, False, True, False, False]
>>> a[a < 0] ## todos os elementos negativos
array([-2., -2.])
>>> a[a < 0] = a.max() ## se a[i]<10, faça a[i]=10
>>> a
array([ 1., 10., 10., 4., 5., 10., 10., 10.])
>>> a[a % 2 == 0] = -1
Mais alguns exemplos:
Arranjos bidimensionais
Fazendo e preenchendo um arranjo bidimensional do numpy:
A = zeros((3,4)) ## tabela de números 3x4
A[0,0] = -1
A[1,0] = 1
A[2,0] = 10
A[0,1] = -5
...
A[2,3] = -100
De uma lista para um arranjo
Vamos fazer uma tabela de números em uma lista de listas:
>>> Cgraus = [-30 + i*10 for i in range(3)]
>>> Fgraus = [9./5*C + 32 for C in Cgrauss]
>>> tabela = [[C, F] for C, F in zip(Cgrauss, Fgraus)]
>>> print tabela
[[-30, -22.0], [-20, -4.0], [-10, 14.0]]
Convertendo em arranjo (NumPy array):
>>> tabela2 = array(tabela)
>>> print tabela2
[[-30. -22.]
[-20. -4.]
[-10. 14.]]
Como corre um laço sobre um arranjo bidimensional
>>> tabela2.shape ## o numero de elementos por direção
(3, 2) ## 3 linhas, 2 colunas
Um laço sobre todos os elementos do arranjo:
>>> for i in range(tabela2.shape[0]):
... for j in range(tabela2.shape[1]):
... print 'tabela2[%d,%d] = %g' % (i, j, tabela2[i,j])
...
tabela2[0,0] = -30
tabela2[0,1] = -22
...
tabela2[2,1] = 14
Uma forma alternativa com único laço:
>> for tupla_indice, valor in np.ndenumerate(tabela2):
... print 'indice %s tem valor %g' % \
... (tupla_indice, tabela2[tupla_indice])
...
indice (0,0) tem valor -30
indice (0,1) tem valor -22
...
indice (2,1) tem valor 14
>>> type(tupla_indice)
<type 'tuple'>
Como fazer cortes em arranjos bidimensionais
Regra: podemos usar cortes no formato inicio:fim:incremento
tabela2[0:tabela2.shape[0], 1] ## 2 coluna (indice 1)
array([-22., -4., 14.])
>>> tabela2[0:, 1] ## mesmo
array([-22., -4., 14.])
>>> tabela2[:, 1] ## mesmo
array([-22., -4., 14.])
>>> t = linspace(1, 30, 30).reshape(5, 6)
>>> t[1:-1:2, 2:]
array([[ 9., 10., 11., 12.],
[ 21., 22., 23., 24.]])
>>> t
array([[ 1., 2., 3., 4., 5., 6.],
[ 7., 8., 9., 10., 11., 12.],
[ 13., 14., 15., 16., 17., 18.],
[ 19., 20., 21., 22., 23., 24.],
[ 25., 26., 27., 28., 29., 30.]])
Sendo o t
acima, o que t[1:-1:2, 2:]
será?
Mais alguns exemplos:
Reduções básicas
Calculando somas:
x = np.array([1, 2, 3, 4])
np.sum(x)
x.sum()
Soma por linha e por coluna:
x = np.array([[1, 1], [2, 2]])
x
x.sum(axis=0) ## colunas (primeira dimensão)
x[:, 0].sum(), x[:, 1].sum()
x.sum(axis=1) ## linhas (segunda dimensão)
x[0, :].sum(), x[1, :].sum()
A ideia é a mesma em maiores dimensões:
x = np.random.rand(2, 2, 2)
x.sum(axis=2)[0, 1]
x[0, 1, :].sum()
Extremos:
x = np.array([1, 3, 2])
x.min()
x.max()
x.argmin() ## índice do mínimo
x.argmax() ## índice do máximo
Comparações de arranjos:
a = np.zeros((100, 100))
np.any(a != 0)
np.all(a == a)
a = np.array([1, 2, 3, 2])
b = np.array([2, 2, 3, 2])
c = np.array([6, 4, 4, 5])
((a <= b) & (b <= c)).all()
Transmissão
- As operações básicas nos arranjos do NumPy são elemento a elemento;
- Essas operações só funcionam em arranjos de mesmo tamanho, no entanto, é possível fazer operações em arranjos de diferentes tamanhos se o NumPy puder transformá-las para que todas tenham o mesmo tamanho: essa convenção é chamada de transmissão (Broadcasting).
A imagem abaixo dá um exemplo de broadcasting:
Verificando:
a = np.tile(np.arange(0, 40, 10), (3, 1)).T
a
b = np.array([0, 1, 2])
a + b
Um truque útil:
a = np.arange(0, 40, 10)
a.shape
a = a[:, np.newaxis] ## adiciona um novo eixo -> array 2D
a.shape
a
a + b
Ou seja, acabamos de utilizar a transmissão sem saber:
a = np.ones((4, 5))
a[0] = 2 ## atribuimos uma array de dimensão 0 a uma array de dimensão 1
a
Transmissão parece ser um pouco mágico, mas na verdade é bem natural utilizá-lo quando se deseja resolver um problema no qual os dados de saída são um arranjo com mais dimensões que os dados de entrada.
Vários problemas do tipo grid-based ou netword-bases podem também utilizar transmissão. Por exemplo, se desejarmos calcular a distância da origem aos pontos em um grid de 10x10, podemos fazer:
x, y = np.arange(5), np.arange(5)
distance = np.sqrt(x ** 2 + y[:, np.newaxis] ** 2)
distance
Ou, em cores:
plt.pcolor(distance)
plt.colorbar()
Detalhe: a função np.pgrid
permite criar diretamento vetores x e y do exemplo anterior com duas "dimensões significativas".
x, y = np.ogrid[0:5, 0:5]
x, y
x.shape, y.shape
distance = np.sqrt(x ** 2 + y ** 2)
Então, o np.ogrid
é muito útil quando precisarmos lidar com computação de dados em grids. Por outro lado, np.mgrid
oferece matrizes cheias de índices para os casos os não quisermos ou não pudermos nos beneficiar da transmissão:
x, y = np.mgrid[0:4, 0:4]
x
y
Na prática é muito pouco utilizado.
Manipulação de forma do arranjo
Achatamento (flattening):
a = np.array([[1, 2, 3], [4, 5, 6]])
a.ravel()
a.T
a.T.ravel()
Maiores dimensões se desfazem primeiro.
Remodelagem (reshaping) operação inversa ao achatamento:
a.shape
b = a.ravel()
b
b.reshape((2, 3))
Criando um array de forma diferente com outro array:
a = np.arange(36)
b = a.reshape((6, 6))
b
ou:
b = a.reshape((6, -1))
Embaralhamento da dimensão:
a = np.arange(4*3*2).reshape(4, 3, 2)
a.shape
a[0, 2, 1]
b = a.transpose(1, 2, 0)
b.shape
b[2, 1, 0]
Atenção: essa operação cria uma visualização!
Redimensionamento:
a = np.arange(4)
a.resize((8,))
a
Mas para modificar redimensionar o arranjo ele não pode ter nenhuma visualização:
b = a
a.resize((4,))
Formato do NumPy
O NumPy possui seu próprio formato de arquivo, não portável mas eficiente:
dados = np.ones((3, 3))
np.save('pop.npy', dados)
dados3 = np.load('pop.npy')
As vantagens desse formato de arquivo são:
- Pode-se escrever arranjos diretamente em um arquivo;
- O formato é binário (rápido)./li>
A desvantagem é que não é portável, ou seja, você somente consegue abrir com um código em Python usando o NumPy.
Tipos de dados estruturados
É possível estruturar os dados de um arranjo definindo o seu tipo:
- codigo_sensor: cadeia de caracteres com 4 caracteres;
- posicao: ponto flutuante;
- valor: ponto flutuante.
amostra = np.zeros((6,), dtype=[('codigo_sensor', 'S4'),
('posicao', float), ('valor', float)])
print amostra.ndim
print amostra.shape
print amostra.dtype.names
amostra[:] = [('ALFA', 1, 0.37), ('BETA', 1, 0.11), ('TAU', 1, 0.13),
('ALFA', 1.5, 0.37), ('ALFA', 3, 0.11), ('TAU', 1.2, 0.13)]
print amostra
Acesso aos campos funciona pela indexação com nomes do campo:
amostra = np.zeros((6,), dtype=[('codigo_sensor', 'S4'),
('posicao', float), ('valor', float)])
amostra['codigo_sensor']
amostra['valor']
amostra[0]
amostra[0]['codigo_sensor'] = 'TAU'
amostra[0]
Múltiplos campos ao mesmo tempo:
samples[['posicao', 'valor']]
A indexação sofisticada funciona, como usual:
samples[samples['sensor_code'] == 'ALFA']
Observação: existem um monte de outras sintaxes para a construção de arranjos estruturados, veja aqui e aqui.
Tarefa
Desafio: Criar uma aplicação que contenha pelo menos uma ramificação e um laço sem utilizar declarações com if
, for
e while
.
O que é uma aplicação?
É a utilização de um conceito matemático ou físico para a resolução de um determinado problema.
Requisitos à cumprir na tarefa:
- A aplicação deve ser composta de um programa principal e seus módulos (pelo menos 1);
- Tanto no programa principal quanto nos módulos devem ser utilizadas funções e funções teste (pelo menos uma teste em cada);
- Levante seus possíveis erros com
try
eexcept
; - Todas as funções devem estar documentadas;
- Os resultados da aplicação devem ser ilustrados com um gráfico;
- O gráfico de resultados deve ser personalizado.