Mais sobre arrays
==================
Nessa aula vamos passar a usar o ndarray do módulo NumPy e esconder um pouco sua implementação e representação interna que foi o foco das aulas anteriores quando desenvolvemos a classe Array2D.
Objetivos de aprendizagem
-------------------------
Realize seus estudos para que, ao final dessa aula, você seja capaz de:
* acessar elementos e fatias de ``ndarrays``;
* realizar operações elemento-a-elemento com ``ndarrays``;
* criar e usar objetos do tipo ``ndarray`` em seus programas.
Introdução
----------
Na aula anterior, interrompemos a nossa descrição do tipo ``ndarray`` para reforçar os conceitos de ``vista`` e ``cópia`` de arrays.
Como as fatias de arrays geram ``vistas`` (ou seja, não geram ``cópias`` como as fatias de listas), o entendimento desses conceitos é fundamental para entender o comportamento das operações com arrays e prever os resultados das operações para que você possa utilizá-las de forma correta em seus programas.
Vistas e cópias de Array2D
...........................
Os conceitos de ``vista`` e ``cópia`` foram treinados nos exercícios e atividade da aula anterior ao desenvolver os seguintes métodos da classe Array2D:
* ``def copy(self)``: retorna um Array2D, cópia de self.
* ``def reshape(self, tupla)``: retorna um Array2D, vista de self, com novo formato (shape) definido pelo parâmetro ``tupla``.
* ``def carregue_vista(self, lista)``:
* ``def carregue_copia(self, lista)``:
O Trinket abaixo ilustra uma implementação desses métodos.
.. trinket
.. raw:: html
Observe que na definição da classe Array2D que a criação de vistas nos métodos ``reshape()`` e ``carregue_vista`` implica em modificar o atributo ``data`` para **compartilhar** os dados. Dessa forma, uma alteração de um elemento da lista ``data`` é "vista" em todas as demais ``vistas``.
O método ``copy(self)`` cria uma nova lista, incialmente com conteúdo idêntico a ``self``. Apesar de serem inicialmente 'iguais', os dados são armazenados **separadamente**. Ao mudar o valor de um elemento de ``self`` essa mudança não será "vista" na cópia e, portanto, deixarão de ser cópias uma da outra.
Finalmente, observe que o método ``carregue_copia(self, lista)`` não cria uma nova lista mas varre ``lista`` e copia, elemento-a-elemento, os valores para o atributo ``self.data``.
E por que não fazer, por exemplo, ``self.data = lista[:]``, que cria uma cópia de ``lista`` e associa a ``self.data``? Essa forma não é recomendada pois, nesse caso, perderíamos as vistas já existentes de ``self`` pois essas vistas continuariam a fazer referência à lista antiga.
Escreva testes para ``carregue_copia()`` e ``carregue_vista()`` na função ``main()`` do Trinket acima.
Mais sobre o uso de tuplas para acessar elementos de um array
-------------------------------------------------------------
Observe como a classe Array2D definida acima implementa os métodos especiais ``__getitem__()`` e ``__setitem__()``. Esses métodos recebem tuplas para selecionar um elemento do array. Assim, dado uma Array2D `ar`, devemos usar a notação ``ar[3,2]`` para acessar o elemento na linha de índice 3 e coluna de índice 2, ao invés de usar pares de colchetes como usados em uma matriz aninhada `ma`, como em ``ma[3][2]``.
Para acessar um elemento apenas, essa notação usando tuplas não traz nenhum grande benefício pois o comportamento é exatamente o mesmo. No entanto, o uso de tuplas dá muito mais flexibilidade e poder para selecionar uma região retangular de arrays usando fatias. A começar, por exemplo, com a possibilidade de indicar colunas, o que não é possível usando apenas fatias de matrizes representadas usando listas aninhadas.
Para ilustrar essa características, considere as funções ``inverta_linhas()`` e ``inverta_colunas()`` abaixo, para inverter as linhas e as colunas de um array.
Essas funções mostram como um vista de uma linha e coluna podem ser criadas e copiadas. Observe que a vista pode inclusive ser usada do lado esquerdo de uma atribuição. Nesse caso a fatia do array é tratada como uma região que pode receber os valores de outro array de mesmo tamanho elemento-a-elemento.
.. raw:: html
.. .. code:: python
import numpy as np
def main():
ar = np.array(range(12)).reshape(3,4)
print(f"Array:\n{ar}\n")
inverta_linhas(ar)
print(f"Array após inverta_linhas:\n{ar}\n")
inverta_colunas(ar)
print(f"Array após inverta_colunas:\n{ar}\n")
def inverta_linhas( ar ):
''' (ndarray) -> None '''
nlin, ncol = ar.shape
for lin in range(nlin//2):
cp = ar[lin, :].copy()
ar[lin, :] = ar[nlin-lin-1, :]
ar[nlin-lin-1, :] = cp
def inverta_colunas(ar):
''' (ndarray) -> None '''
nlin, ncol = ar.shape
for col in range(ncol//2):
cp = ar[:, col].copy()
ar[ :, col] = ar[ :, ncol-col-1]
ar[ :, ncol-col-1] = cp
if __name__ == '__main__':
main()
Essa forma de atribuição múltipla para fatias de arrays é bastante útil, como ilustrado no trecho de código abaixo.
.. raw:: html
Impressão de listas aninhadas e arrays
--------------------------------------
Observe o resultado dos prints abaixo:
.. code:: python
>>> import numpy as np
>>> A = [ [1,2,3,4], [5,6,7,8] ]
>>> print( A )
[[1, 2, 3, 4], [5, 6, 7, 8]]
>>>
>>> B = np.array(A)
>>> print(B)
[[1 2 3 4]
[5 6 7 8]]
>>>
Note que a lista A é impressa em uma única linha, enquanto ``print(B)`` mostra a estrutura do array, imprimindo cada linha separadamente.
É possível criar ``ndarray``s com 2, 3 ou mais dimensões.
Ao imprimir arrays de ``n`` dimensões, a impressão obedece a mesma ordem dos índices. Assim, um array 3D seria impresso da seguinte forma:
.. code:: python
>>> tri = np.array( range(24) ).reshape(2,3,4)
>>> print(tri)
[[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]]
>>> bi = tri.reshape((4,6))
>>> print(bi)
[[ 0 1 2 3 4 5]
[ 6 7 8 9 10 11]
[12 13 14 15 16 17]
[18 19 20 21 22 23]]
Nesse exemplo um array de uma dimensão é criado com os inteiros de 0 a 23 (resultado do ``range(24)``). Uma forma alternativa seria usar diretamente o método ``np.arrange(24)``.
O método ``reshape()`` permite alterar as dimensões do array, sem alterar os dados, de forma similar ao comportamento da classe Array2D. Para isso o produto dos argumentos usados no reshape, nesse caso a tupla (2,3,4), deve ser igual ao número de elementos do array (2x3x4 = 24). Como resultado são impressas 2 matrizes de 3 linhas e 4 colunas cada, que representa o conteúdo do array de 3 dimensões.
Observe que podemos usar ``reshape()`` para criar uma vista do array com outro formato, como no exemplo, uma array $4\times6$ de nome ``bi``.
Uma outra conveniência fornecida para arrays muito grandes é a impressão apenas dos cantos, como no exemplo abaixo. Você pode modificar esse comportamento pelo método `set_printoptions `__.
.. code:: python
>>> grande = np.arange(10000).reshape(100,100)
>>> print(grande)
[[ 0 1 2 ... 97 98 99]
[ 100 101 102 ... 197 198 199]
[ 200 201 202 ... 297 298 299]
...
[9700 9701 9702 ... 9797 9798 9799]
[9800 9801 9802 ... 9897 9898 9899]
[9900 9901 9902 ... 9997 9998 9999]]
>>>
Manipulando as dimensões de um array
------------------------------------
As dimensões de um array podem ser manipuladas criando vistas diferentes usando, por exemplo, os métodos ``ravel()`` e ``reshape()`` e o atributo ``T``, como ilustrado no trecho de código a seguir.
.. code:: python
>>> bi = np.arange(6).reshape(2,3)
>>> bi
array([[0, 1, 2],
[3, 4, 5]])
>>> bi.ravel()
array([0, 1, 2, 3, 4, 5])
>>> bi
array([[0, 1, 2],
[3, 4, 5]])
>>> bi.T
array([[0, 3],
[1, 4],
[2, 5]])
>>> bi
array([[0, 1, 2],
[3, 4, 5]])
>>> bi.T.shape
(3, 2)
>>> bi.shape
(2, 3)
>>> bi.resize(3,2)
>>> bi
array([[0, 1],
[2, 3],
[4, 5]])
>>>
Esse exemplo mostra a criação de um array 2D referenciado por ``bi``.
O método ``ravel()`` retorna uma **vista** (view) 1D do array, mas sem modificar o seu conteúdo.
O atributo ``T`` retorna a transposta do array e também não modifica o array ``bi``.
As dimensões do array podem ser alteradas **definitivamente** pelo método ``resize()``.
Ao usar métodos que retornam vistas do array lembre-se que, apesar de possuírem dimensões (``shape``) diferentes, seu conteúdo no atributo ``data`` é o mesmo, ou seja, todos compartilham os mesmos dados. Apesar desse compartilhamento tornar o processamento mais eficiente pois se evita a cópia dos dados, isso pode ser indesejável em certas situações. Utilize o método ``copy()`` sempre que precisar criar arrays com dados separados. As cópias, ao serem modificadas, deixam de ser cópias pois os arrays originais não são alterados.
.. , como mostra o trecho de código a seguir.
.. code:: python
>>> ci = bi.reshape(3,2)
>>> ci
array([[0, 1],
[2, 3],
[4, 5]])
>>> bi
array([[0, 1, 2],
[3, 4, 5]])
>>> bi[1,1] = -1
>>> bi
array([[ 0, 1, 2],
[ 3, -1, 5]])
>>> ci
array([[ 0, 1],
[ 2, 3],
[-1, 5]])
>>>
A variável ``ci`` compartilha o mesmo conteúdo (dados) que a variável ``bi``, apesar de terem ``shapes`` diferentes (``bi`` é (2,3) enquanto ``ci`` é (3,2)). Quando o elemento ``bi[1,1]`` é alterado, é possível ver o resultado em ``ci``.
Caso esse efeito, devemos criar uma cópia do array usando o método ``copy()`` como abaixo.
.. code:: python
>>> bi = np.arange(6).reshape(2,3)
>>> ci = bi.reshape(3,2)
>>> di = ci.copy()
>>> bi[1,1] = 99
>>> bi
array([[ 0, 1, 2],
[ 3, 99, 5]])
>>> ci
array([[ 0, 1],
[ 2, 3],
[99, 5]])
>>> di
array([[0, 1],
[2, 3],
[4, 5]])
>>>
O ``copy()`` cria um clone do array, ou seja, replica os dados em outro bloco de memória. Como ``di`` é uma cópia de ``ci``, quando ``bi[1,1]`` é alterado, o conteúdo de ``ci`` se altera pois compartilha o mesmo bloco de dados (dizemos que tanto ``bi`` quanto ``ci`` fazem referência ao mesmo bloco de dados), mas o conteúdo de ``di`` permanece o mesmo. Vale dizer que criar uma cópia de uma estrutura muito grande é computacionalmente caro e deve ser evitado se possível. Por criar apenas vistas diferentes usando referências para o mesmo bloco de dados, essas manipulações de dimensão são operações bastante eficientes em NumPy mesmo para estruturas grandes.
Operações básicas
-----------------
Apesar da semelhança do tipo ``numpy.ndarray`` com as listas de Python, o NumPy oferece muito mais recursos para operação e manipulação de arrays que facilitam a computação científica.
Em ciência da computação, **vetorização** (`vectorization `__)
é a generalização das operações em escalares a vetores e matrizes de várias dimensões.
Linguagens de programação que aplicam de maneira transparente e concisa operação sobre arrays são
chamadas de `array programming languages `__.
Exemplos de linguagens que admitem vetorização são
`GNU Octave `__,
`Julia `__,
`R `__ e
`Python NumPy `__.
Os arrays do NumPy permitem usar os símbolos das operações básicas como `*`, `+`, etc, para calcular um novo array "por elemento", como ilustrado no trecho de código abaixo.
.. code:: python
>>> import numpy as np
>>> a = np.array([[1,2],[3,4]])
>>> a
array([[1, 2],
[3, 4]])
>>> b = np.array([[0,2],[3,1]])
>>> b
array([[0, 2],
[3, 1]])
>>> a+b
array([[1, 4],
[6, 5]])
>>> a*b
array([[0, 4],
[9, 4]])
>>> b/a
array([[ 0. , 1. ],
[ 1. , 0.25]])
Observe que não basta ter o mesmo número de elementos, mas a
forma (``shape``) do array também é importante, como mostra o
exemplo a seguir.
.. code:: python
>>> import numpy as np
>>> a = np.array([[1,2],[3,4]])
>>> a.shape
(2, 2)
>>> c = np.array([5,6,7,8])
>>> c
array([5, 6, 7, 8])
>>> c.shape
(4, )
>>> a + c
Traceback (most recent call last):s
File "", line 1, in
ValueError: operands could not be broadcast together with shapes (2,2) (4,)
>>>
Operações entre arrays de tamanhos distintos
............................................
Em muitos casos, é necessário trabalhar com arrays com um número distinto de
elementos, ou de formas diferentes. Nesse caso, é necessário entender as regras
do NumPy que difundem as informações do array menor
para o array maior.
O termo **difusão** (`broadcasting `__)
descreve como NumPy trata arrays de dimensões diferentes em
operações aritméticas. O array menor é
"difundido" a uma array maior para que tenham
as mesmas dimensões.
No caso de um escalar, o valor do escalar é difundido para todos os elementos
do array maior, como no exemplo:
.. code:: python
>>> import numpy as np
>>> a = np.array([[1,2],[3,4]])
>>> a
array([[1, 2],
[3, 4]])
>>> a + 1
array([[2, 3],
[4, 5]])
>>> a * 2
array([[2, 4],
[6, 8]])
>>> a / 2
array([[ 0.5, 1. ],
[ 1.5, 2. ]])
>>>
No caso de arrays com uma das dimensões unitárias (um array linha ou coluna por exemplo), mas onde as demais dimensões sejam compatíveis com a do array maior, o array menor é difundido para a outra dimensão do array maior. Veja os exemplos abaixo.
.. code:: python
>>> import numpy as np
>>> a = np.array(range(1,13,2)).reshape(2,3)
>>> a
array([[ 1, 3, 5],
[ 7, 9, 11]])
>>> b = ([4,5,6])
>>> a - b
array([[-3, -2, -1],
[ 3, 4, 5]])
>>> c = np.array([[0],[1]])
>>> c
array([[0],
[1]])
>>> c.shape
(2, 1)
>>> a * c
array([[ 0, 0, 0],
[ 7, 9, 11]])
>>> d = np.array([1,2])
>>> a * d
Traceback (most recent call last):
File "", line 1, in
ValueError: operands could not be broadcast together with shapes (2,3) (2,)
>>> b * c
array([[0, 0, 0],
[4, 5, 6]])
>>> c + b
array([[4, 5, 6],
[5, 6, 7]])
>>>
Observe que nos dois últimos exemplos (``b*c`` e ``b+c``), quando a operação envolve
dois arrays com uma dimensão unitária, por exemplo, ``(1,3)`` e ``(2,1)``, o resultado
é um array de dimensão ``(2,3)``.
Outras funções do NumPy
-----------------------
Talvez você se sinta incomodado que o operador `*` não calcula o produto de duas matrizes, mas o produto elemento a elemento de dois arrays de mesma dimensão. Felizmente, o NumPy oferece vários outros recursos como para:
* **Álgebra Linear**
O produto de matrizes pode ser calculado usando o método ``dot``
.. code:: python
>>> import numpy as np
>>> a = np.array(range(1,13,2)).reshape(2,3)
>>> a
array([[ 1, 3, 5],
[ 7, 9, 11]])
>>> b = np.array(range(11,-1,-1)).reshape(3,4)
>>> b
array([[11, 10, 9, 8],
[ 7, 6, 5, 4],
[ 3, 2, 1, 0]])
>>> a.dot(b)
array([[ 47, 38, 29, 20],
[173, 146, 119, 92]])
>>> b.dot(a)
Traceback (most recent call last):
File "", line 1, in
ValueError: shapes (3,4) and (2,3) not aligned: 4 (dim 1) != 2 (dim 0)
>>>
Além de calcular o produto de vetores e matrizes, o NumPy oferece também
vários outros recursos para inversão de matrizes e solução de sistemas
lineares, cálculo de normas, de auto-valores e auto-vetores, etc.
Para saber mais, consulte a documentação sobre os recursos de
`Álgebra Linear `__.
* **Estatística**
`Rotinas estatísticas `__ para cálculo de média, variância e desvio padrão,
entre outras, além de funções para vários tipos de `amostragem `__, inclusive um gerador de números pseudo-aleatórios.
**Matemática**
Diversas `funções matemáticas `__ como `sqrt`, `log`, `exp`, `min`, `max`, `sum`, `tan`, `sin`, `cos`, etc.
* **e muitas outras**
Como funções financeiras, Fast Fourier Transform, polinômios, ordenação, etc.
Dê uma olhada no conjunto de rotinas do NumPy em `http://docs.scipy.org/doc/numpy-dev/reference/routines.html `__.
Exercícios
----------
O **Jogo da Vida** (`The Game of Life `__) é um autômato celular (`cellular automaton `__) introduzido por John Horton Conway em 1970. Um automato celular consiste de uma rede de células. Cada célula pode estar em um número finito de estados, como *morta* ou *viva*. O "jogo" é na verdade uma simulação que permite observar a evolução de um processo a partir de uma certa condição inicial.
O jogo se desenvolve sobre uma matriz bi-dimensional que pode ser tão grande quanto se queira.
Vamos chamar essa matriz de **mapa**. Cada posição ou célula do mapa pode estar vazia (= célula morta) ou ocupada por um agente (= célula viva). Cada posição possui também até 8 posições vizinhas: imediatamente acima, abaixo, aos lados e nas diagonais. Em um determinado instante, o mapa contém uma geração de agentes. A geração no instante seguinte é determinada segundo as regras abaixo:
- se uma célula [i,j] está vazia então:
- um novo agente nasce em [i,j] se essa célula possui exatamente 3 agentes vizinhos;
- se uma célula [i,j] possui um agente então:
- o agente em [i,j] sobrevive se possui 2 ou 3 agentes vizinhos;
- o agente em [i.j] morre se possuir menos de 2 agentes vizinhos, por falta de recursos;
- o agente em [i.j] morre se possuir mais de 3 agentes vizinhos, por excesso de competição.
Supondo que um mapa M é representado por um ``ndarray`` onde células vazias
tem valor ``0`` (zero) e agentes tem valor ``1`` (um), escreva a seguinte função:
.. code:: python
def iteracao(M):
''' (mapa) -> None
Recebe uma mapa (ndarray) com a geração de agentes em um determinado
instante e atualiza o mapa de tal forma que represente geração de
agentes no instante seguinte.
'''
A cada iteração, o mapa deve ser varrido e, para cada célula,
a função deve calcular o seu novo valor baseado nas regras do jogo.
Observe que o próximo estado do mapa (no instante seguinte) depende **exclusivamente**
do estado (no instante) atual.
Veja alguns padrões produzidos pelo jogo da vida em `Glider `__.
Variantes do jogo da vida são
`Day and Night `__,
`Highlife `__,
`Life without Death `__ e
`Seeds `__.
**Dicas**:
#. o método ``sum()`` calcula a soma dos elementos de um array e pode ser aplicado
em fatias de arrays. Por exemplo, execute o seguinte trecho de código para ver o resultado.
.. code:: python
import numpy as np
x = np.array( range(12) ).reshape(3,4)
print(x)
print(x[1:3,1:3])
print(x[1:3, 1:3].sum())
#. Teste usando ndarrays bem pequenos.
Onde estamos e para onde vamos?
-------------------------------
Arrays são estruturas muito utilizadas em computação científica. Nessa aula começamos a abstrair a implementação para usar essas estruturas como mapas ou matrizes e manipulando partes dessas estruturas usando fatias e operações com arrays.
Nas próximas aulas vamos continuar esse processo de abstração para fortalecer seu domínio sobre arrays e operações com arrays.
Para saber mais
---------------
* `Matrizes usando listas aninhadas `__.
* `NumPy quickstart `__.