# Arrays e cálculo com NumPy

Arrays 1D e 2D, shape, dtype, eixos, broadcasting, cópias e vistas.

Página: https://resumos.rgo.pt/cadeiras/ct-iadp/numpy/

Uma lista pode guardar objetos diferentes. Um array NumPy guarda elementos de um tipo comum e suporta operações sobre todos eles. Para calcular o total de cada venda, podemos multiplicar um array de quantidades por outro de preços.

## Criar e inspecionar

```
import numpy as np

quantidades = np.array([2, 3, 4])
precos = np.array([1.5, 1.2, 2.0])
```

`quantidades.ndim` é 1, `quantidades.shape` é `(3,)` e `quantidades.size` é 3. `dtype` indica o tipo guardado. Ao criar `np.array([1, 2.5])`, os elementos passam a reais, para partilhar o mesmo tipo.

Um array de inteiros não passa automaticamente a reais quando lhe atribuis 2.5. Escolhe `dtype=float` quando o cálculo exige frações. A mistura com strings pode converter todos os elementos para texto e impedir a aritmética esperada.

`np.zeros((2, 3))` cria duas linhas e três colunas preenchidas com zero. `np.ones` preenche com um. `np.arange(1, 6, 2)` produz 1, 3 e 5. `np.linspace(0, 1, 5)` produz cinco pontos igualmente espaçados, incluindo normalmente ambos os extremos. `np.empty` reserva espaço sem inicializar valores úteis; preenche-o antes de ler.

## Tipo e memória

O número no nome `int8`, `uint8`, `float32` ou `float64` indica bits por elemento. `int8` guarda inteiros de −128 a 127; `uint8`, sem sinal, guarda 0 a 255. `float32` ocupa quatro bytes e `float64` oito. `complex64` ocupa oito bytes no total, com duas componentes reais de quatro bytes.

`a.itemsize` dá bytes por elemento e `a.nbytes` o total de bytes dos dados. Um array `(2, 3)` de `float64` tem seis elementos e ocupa 48 bytes de dados. Isto não inclui todo o custo do objeto Python. `.data` expõe o buffer; usa índices para aceder aos valores.

`a.astype(float)` devolve normalmente outro array convertido. Escolhe um tipo que represente os valores e a precisão necessários: inteiros de tamanho fixo podem transbordar e converter reais para inteiros elimina a parte fracionária. `np.iinfo(np.int8)` mostra limites de um tipo inteiro. O tipo de `np.array` é inferido dos dados quando omites `dtype`; não é sempre `float64`.

## Identidade e leitura de dados numéricos

`np.eye(3)` cria uma matriz identidade 3 × 3, com uns na diagonal e zeros fora dela. Para dados tabulares numéricos, `genfromtxt` lê um ficheiro ou objeto semelhante a ficheiro, permitindo indicar separador e linhas de cabeçalho.

```
from io import StringIO
import numpy as np

texto = StringIO("a;b\n2;3\n4;5\n")
a = np.genfromtxt(texto, delimiter=";", skip_header=1)
print(a.shape)       # (2, 2)
print(a.sum(axis=0)) # [6. 8.]
```

Aqui as colunas são numéricas. Para colunas de tipos diferentes, datas ou categorias, um DataFrame pandas costuma conservar melhor o significado da tabela. Confere a forma e os valores em falta após ler dados externos.

## Operar elemento a elemento

```python
import numpy as np
quantidades = np.array([2, 3, 4])
precos = np.array([1.5, 1.2, 2.0])
totais = quantidades * precos
print(totais.round(2))
print(round(totais.sum(), 2))
print(quantidades[quantidades >= 3])
```

Os totais são 3,00, 3,60 e 8,00 euros; a soma é 14,60 euros. A comparação cria uma máscara booleana com `False, True, True`. Aplicar a máscara seleciona as quantidades 3 e 4.

Com listas, `[2, 3] * 2` repete a sequência. Com NumPy, `np.array([2, 3]) * 2` multiplica os valores e dá 4 e 6. `*` não calcula um produto matricial; `@` calcula esse produto quando as dimensões são compatíveis.

## Duas dimensões e eixos

Vamos guardar duas lojas em linhas e três produtos em colunas:

```python
import numpy as np
vendas = np.array([[2, 3, 4], [5, 1, 2]])
print(vendas.shape)
print(vendas[1, 0])
print(vendas[:, 1])
print(vendas.sum(axis=0))
print(vendas.sum(axis=1))
```

A forma é `(2, 3)`. A célula de linha 1, coluna 0 contém 5. A coluna 1 contém 3 e 1. A soma com `axis=0` elimina o eixo das linhas e dá os totais por coluna: 7, 4 e 6. Com `axis=1`, elimina o eixo das colunas e dá os totais por linha: 9 e 8.

O nome do eixo é o que percorres e reduzes. Para evitar trocar os dois, confere o tamanho do resultado: três produtos exigem três totais, duas lojas exigem dois.

`reshape(3, 2)` reorganiza seis elementos numa forma compatível. Não cria nem elimina elementos. `reshape(-1, 2)` pede ao NumPy que deduza o número de linhas. `.T` troca os eixos de um array 2D.

## Broadcasting

Ao multiplicar a matriz `(2, 3)` por preços com forma `(3,)`, NumPy usa o mesmo vetor de preços em cada linha. Este alargamento chama-se **broadcasting**. Compara as dimensões a partir da direita: devem ser iguais ou uma delas deve ser 1.

`(2, 3)` e `(3,)` são compatíveis. `(2, 3)` e `(2,)` não são. Para aplicar um desconto diferente por loja, usa um array com forma `(2, 1)`.

## Vistas e cópias

Um slice básico pode ser uma **vista**, que partilha os dados com o array original. Alterar `parte = dados[:2]` pode alterar `dados`. `dados[:2].copy()` cria dados independentes. Seleção por máscara booleana e índices avançados produz uma cópia; não uses essa distinção como substituto para tornar a alteração explícita.

Quando queres alterar o original, escreve `dados[mascara] = novo_valor`. Quando queres trabalhar à parte, usa `.copy()`.

## Estatísticas e acumulados de arrays

`sum`, `mean`, `min`, `max`, `std` e `var` reduzem valores. NumPy usa `ddof=0` por defeito em `std` e `var`; indica `ddof=1` para a convenção amostral. `argmin` e `argmax` devolvem posições, enquanto `min` e `max` devolvem valores. Sem eixo, a posição refere-se ao array achatado.

`np.cumsum([2, 3, 1])` devolve 2, 5 e 6; `np.cumprod` devolve 2, 6 e 6. Num array 2D, indica `axis` para conservar a organização pretendida. Sem eixo, os acumulados usam uma sequência achatada. A presença de NaN pode propagar ausência; funções como `np.nansum` exprimem a decisão de ignorar NaN.

## Gerar dados aleatórios reproduzíveis

Cria um gerador com `rng = np.random.default_rng(12)`. A semente fixa o estado inicial. Na mesma versão e com a mesma sequência de chamadas, podes repetir a experiência; mudar a ordem das chamadas muda os valores seguintes.

| Método do gerador | Resultado |
| --- | --- |
| `rng.random(4)` | Quatro reais uniformes em `[0, 1)` |
| `rng.integers(1, 7, size=4)` | Quatro inteiros de 1 a 6, extremo superior excluído |
| `rng.standard_normal(4)` | Quatro observações normais, média 0 e desvio padrão 1 |
| `rng.normal(10, 2, size=4)` | Normal com média 10 e desvio padrão 2 |
| `rng.uniform(2, 5, size=4)` | Uniforme entre 2 e 5 |
| `rng.binomial(8, 0.25, size=4)` | Número de sucessos em oito ensaios, probabilidade 0,25 |
| `rng.permutation(valores)` | Nova sequência com os elementos baralhados |
| `rng.shuffle(valores)` | Baralha o objeto fornecido, sem devolver uma nova sequência |

Slides e programas antigos usam `np.random.seed`, `rand`, `randint` e `randn`. Correspondem à interface anterior: `rand` gera uniformes, `randint` inteiros e `randn` normais padrão. Para código novo, o gerador explícito evita partilhar estado aleatório global entre exemplos.

```python
import numpy as np

def lancamentos(semente):
    rng = np.random.default_rng(semente)
    return rng.integers(1, 7, size=12)

a = lancamentos(12)
b = lancamentos(12)
print("repetiu:", np.array_equal(a, b))
print("entre 1 e 6:", bool(((a >= 1) & (a <= 6)).all()))
print("contagens:", np.bincount(a, minlength=7)[1:].tolist())
```

As contagens somam 12. Não devem ser iguais por face numa amostra pequena. Um gerador reproduz uma simulação; não transforma resultados aleatórios numa garantia de frequências exatas.

## Exercícios

Prever o tamanho da redução

Um array tem forma (2, 3). Qual é a forma de a.sum(axis=0)?

Primeira pista

O eixo 0 representa as linhas neste array.

Mais uma pista

O resultado precisa de um total para cada uma das três colunas.

Ver solução

A forma é (3,). Em \[\[2, 3, 4\], \[5, 1, 2\]\], os valores seriam \[7, 4, 6\].

*   **(3,).** O eixo das duas linhas é reduzido; sobra um valor por coluna.
*   **(2,).** Seria o resultado de reduzir axis=1.
*   **(2, 3).** A soma por um eixo reduz uma dimensão, salvo opções como keepdims.

#### Erros frequentes

Pensar que axis=0 significa devolver um valor por linha.

[Voltar à explicação](https://resumos.rgo.pt/cadeiras/ct-iadp/numpy/#duas-dimens%C3%B5es-e-eixos)

Escolher o desconto por linha

Para uma matriz (2, 3), queres multiplicar cada linha por um desconto próprio. Qual deve ser a forma do array de fatores?

Primeira pista

Compara as formas a partir da direita.

Mais uma pista

Precisas de conservar dois fatores e permitir que cada um se repita em três colunas.

Ver solução

Usa fatores com forma (2, 1). Por exemplo, np.array(\[0.9, 0.8\])\[:, None\].

*   **(2, 1).** O eixo das colunas tem tamanho 1 e alarga para as três colunas.
*   **(2,).** A última dimensão 2 não é compatível com 3.
*   **(3,).** Aplica um fator por coluna, não um por linha.

#### Erros frequentes

Supor que o número de fatores basta sem conferir o alinhamento das dimensões.

[Voltar à explicação](https://resumos.rgo.pt/cadeiras/ct-iadp/numpy/#broadcasting)

Reconhecer dados partilhados

a = np.array(\[1, 2, 3\]); b = a\[

\]; b\[0\] = 8. Quanto vale a\[0\]?

Primeira pista

Um slice básico pode devolver uma vista.

Mais uma pista

A vista refere a primeira parte dos mesmos dados.

Ver solução

a\[0\] passa a 8. Para separar os dados, usa b = a\[

\].copy().

#### Erros frequentes

Transferir a regra dos slices de listas para arrays NumPy.

[Voltar à explicação](https://resumos.rgo.pt/cadeiras/ct-iadp/numpy/#vistas-e-c%C3%B3pias)

Calcular bytes dos dados

Um array tem forma (2, 3) e dtype float64. Quantos bytes ocupam os seus dados, sem contar a estrutura do objeto?

Primeira pista

Calcula quantos elementos existem e quantos bytes tem cada um.

Mais uma pista

Há seis elementos. Cada float64 usa oito bytes.

Ver solução

2 × 3 × 8 = 48 bytes. a.size é 6, a.itemsize é 8 e a.nbytes é 48.

#### Erros frequentes

Tratar 64 bits como 64 bytes ou somar as dimensões em vez de as multiplicar.

[Voltar à explicação](https://resumos.rgo.pt/cadeiras/ct-iadp/numpy/#tipo-e-mem%C3%B3ria)
