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.
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:
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