NumPy: indexing, reshape e operazioni vettoriali

Righe, colonne, sottomatrici, maschere booleane: impara a "ritagliare" un array NumPy come si deve, a cambiargli forma con reshape e a fare calcoli senza cicli grazie al broadcasting. Con lo schema visuale dello slicing che vorresti aver visto prima.

Hai una tabella di dati — cento studenti, cinque voti ciascuno — e ti serve solo la terza colonna. Oppure solo gli studenti con media sopra il 27. Con le liste Python ti tocca un ciclo, un if, una lista d’appoggio: tre righe di rumore per un’idea semplice. Con NumPy la stessa idea si scrive in una riga, e in questa lezione impari esattamente come: indexing e slicing per ritagliare, reshape per cambiare forma, operazioni vettoriali e broadcasting per calcolare senza cicli.

L’ndarray, indicizzazione e slicing, broadcasting e vettorizzazione sono trattati nel libro.

Leggi «NumPy: calcolo numerico vettorizzato» nel libro →

Prerequisito unico: la lezione precedente. Se non la ricordi, ti basta sapere che un array NumPy è un blocco di valori dello stesso tipo in memoria contigua, molto più veloce di una lista per la matematica. Ambiente: Python 3.12 nel tuo virtual environment (venv o uv venv), numpy installato, VS Code o PyCharm aperto.

Indexing 2D: battaglia navale

Un array a due dimensioni si legge come una griglia di battaglia navale: per colpire una casella dichiari prima la riga, poi la colonna. L’unica differenza è che qui si conta da zero.

import numpy as np

# 3 studenti x 4 voti: una matrice 3x4
voti = np.array([
    [27, 30, 24, 28],   # riga 0: Aluisi
    [22, 25, 30, 26],   # riga 1: Bianchi
    [30, 29, 28, 30],   # riga 2: Corradi
])

print(voti.shape)     # (3, 4): 3 righe, 4 colonne
print(voti[1, 2])     # 30 — riga 1, colonna 2: il terzo voto di Bianchi
print(voti[0])        # [27 30 24 28] — tutta la riga 0
print(voti[-1, -1])   # 30 — indici negativi: ultima riga, ultima colonna

Lo slicing estende l’idea: al posto di un indice metti un intervallo inizio:fine (fine esclusa, come nelle liste). E qui vale la pena fermarsi su uno schema, perché lo slicing 2D si capisce con gli occhi molto prima che con le parole:

a.shape = (4, 6) — tre ritagli, tre colori 0 1 2 3 4 5 0 1 2 3 a[1, :] tutta la riga 1 a[:, 4] tutta la colonna 4 a[2:4, 1:3] sottomatrice 2×2 Regola: a[righe, colonne] — i due punti da soli significano «tutto», inizio:fine prende da inizio a fine ESCLUSA (2:4 → righe 2 e 3). Attenzione: uno slice è una vista, non una copia.

Slicing visuale su un array 4×6: la coppia [righe, colonne] ritaglia righe intere, colonne intere o sottomatrici. La fine dell’intervallo è sempre esclusa.

L’avviso in terracotta nello schema merita una riga in più: uno slice NumPy è una vista sullo stesso blocco di memoria, non una fotocopia. Se modifichi la vista, modifichi l’originale — comodo e velocissimo, ma insidioso. Quando vuoi davvero una copia indipendente, chiedila esplicitamente con .copy().

Maschere booleane: il colino dei dati

Il ritaglio più potente non usa gli indici ma le condizioni. Funziona come un colino in cucina: la condizione è la trama del colino, e passano solo i valori che la soddisfano.

# Quali voti sono almeno 28?
maschera = voti >= 28
print(maschera)
# [[False  True False  True]
#  [False False  True False]
#  [ True  True  True  True]]

# La maschera "cola" i valori: restano solo i True
print(voti[maschera])        # [30 28 30 30 29 28 30]

# Tutto in una riga, come si scrive di solito:
print(voti[voti >= 28])

# Condizioni combinate: & per AND, | per OR, parentesi OBBLIGATORIE
print(voti[(voti >= 24) & (voti < 28)])   # [27 24 25 26]

# E il caso pratico dell'apertura: studenti con media sopra il 27
medie = voti.mean(axis=1)    # media per riga: [27.25 25.75 29.25]
print(voti[medie > 27])      # solo le righe di Aluisi e Corradi

Nota axis=1: è il modo di NumPy per dire «lungo le colonne, quindi una media per riga«. Con axis=0 otterresti la media di ogni colonna, cioè di ogni esame. Se l’asse ti confonde, memorizza così: axis è la dimensione che scompare nel risultato.

Reshape: stessa pasta, stampo diverso

A volte i dati arrivano nella forma sbagliata: un sensore ti dà 24 letture in fila, ma tu le vuoi come 4 fasce orarie da 6 valori. reshape è lo stampo per i biscotti: la pasta (i valori, nel loro ordine) resta identica, cambia solo la forma in cui la presenti. Proprio perché la pasta non cambia, il totale deve tornare: 24 valori possono diventare 4×6 o 2×12, mai 5×5.

letture = np.arange(24)          # [0, 1, 2, ..., 23]

giornata = letture.reshape(4, 6) # 4 fasce orarie x 6 letture
print(giornata.shape)            # (4, 6)

# Il -1 dice a NumPy: "questa dimensione calcolala tu"
print(letture.reshape(2, -1).shape)   # (2, 12)

# Il caso che incontrerai sempre in scikit-learn: da vettore a colonna.
# fit() vuole X di forma (n_campioni, n_feature), anche con 1 feature
x = np.array([1.0, 2.0, 3.0, 4.0])
X = x.reshape(-1, 1)             # da (4,) a (4, 1): 4 campioni, 1 feature
print(X.shape)

# L'operazione inversa: appiattire tutto
print(giornata.ravel().shape)    # (24,) — di nuovo una fila unica

Quel reshape(-1, 1) tienilo caro: è la riga che sblocca metà degli errori «Expected 2D array, got 1D array instead» che scikit-learn ti mostrerà nelle prossime lezioni.

Operazioni vettoriali e broadcasting

Il gran finale è il motivo per cui tutto questo esiste: fare matematica su interi array senza scrivere cicli. E il broadcasting è la regola che permette di combinare array di forme diverse. L’analogia giusta è l’IVA: quando il negoziante applica il 22% al listino, non calcola un’aliquota diversa per ogni prodotto — un solo numero si «spalma» su tutti i prezzi. Broadcasting è esattamente questo: NumPy allunga virtualmente l’operando più piccolo perché combaci con il più grande, senza copiare nulla in memoria.

prezzi = np.array([10.0, 25.0, 4.5, 99.0])

# Scalare su array: il 1.22 si spalma su tutti i prezzi
print(prezzi * 1.22)             # [12.2  30.5   5.49 120.78]

# Array su array (stessa forma): elemento per elemento
sconti = np.array([0.0, 0.10, 0.0, 0.30])
print(prezzi * (1 - sconti))     # [10.   22.5   4.5  69.3]

# Riga su matrice: la riga si spalma su OGNI riga della matrice.
# Caso reale: standardizzare le colonne di un dataset
X = np.array([[180.0, 75.0],
              [165.0, 60.0],
              [172.0, 68.0]])    # altezza (cm), peso (kg)

medie = X.mean(axis=0)           # forma (2,): media di ogni colonna
dev = X.std(axis=0)              # forma (2,): deviazione standard
X_std = (X - medie) / dev        # (3,2) con (2,): broadcasting!
print(X_std.round(2))

Ogni volta che stai per scrivere un for su un array, fermati e chiediti: esiste l’operazione vettoriale che lo fa per me? Nove volte su dieci la risposta è sì.

Un’avvertenza onesta: non tutto va vettorizzato a ogni costo. Se la logica per singolo elemento è davvero contorta — dipende da stati esterni, chiama API, ha dieci rami condizionali — un ciclo esplicito resta più leggibile, e su pochi dati la differenza di velocità non la noterai mai. La vettorizzazione è uno strumento, non una religione.

Prova tu

Genera una «classe» sintetica: rng = np.random.default_rng(7) e poi voti = rng.integers(18, 31, size=(20, 5)) — venti studenti, cinque esami. Ora, senza cicli: 1) estrai i voti del terzo esame di tutti gli studenti; 2) calcola la media di ogni studente con axis giusto; 3) usa una maschera booleana per contare quanti studenti hanno tutti i voti sopra il 24 (suggerimento: esplora il metodo .all(axis=...)); 4) «cura» la classe con il broadcasting aggiungendo [1, 0, 2, 0, 1] a tutti — un punto bonus diverso per esame — e verifica con np.clip che nessun voto superi 30.

Per approfondire: la pagina ufficiale Broadcasting — NumPy user guide con le sue figure è il riferimento definitivo sulle regole di compatibilità tra forme; per lo slicing avanzato, il capitolo su NumPy di Python Data Science Handbook di Jake VanderPlas (2ª edizione, liberamente leggibile online) è pieno di esempi visuali. Nella prossima lezione usiamo questi attrezzi per la statistica descrittiva: media, mediana, moda e varianza.

I commenti sono riservati agli iscritti.

Accedi per commentare