La libreria Python per il calcolo numerico ad alte prestazioni: array N-dimensionali, indicizzazione, slicing, broadcasting, numeri casuali e universal functions. Con esempi pratici ed esercizi.
NumPy — Guida completa in italiano
NumPy (Numerical Python) è la libreria fondamentale per il calcolo scientifico in Python. Fornisce array multidimensionali ad alte prestazioni e una grande collezione di funzioni matematiche per operare su di essi.
È la base di tutto l'ecosistema scientifico Python: Pandas, Matplotlib, SciPy e Scikit-learn sono tutti costruiti sopra NumPy.
Caratteristica
Python list
NumPy array
Velocità
Lenta (oggetti Python)
Molto veloce (C sotto)
Memoria
Elevata
Compatta e contigua
Operazioni vettoriali
Richiede cicli
Broadcast nativo
Tipo dati
Misto
Omogeneo
Dimensioni
Solo 1D (di fatto)
nD (matrici, tensori)
💡 NumPy è il punto di partenza per Data Science, Machine Learning e analisi numerica con Python.
Introduzione a NumPy
Un array NumPy (chiamato ndarray) è una tabella di elementi dello stesso tipo, indicizzata da una tupla di interi non negativi. Il numero di dimensioni è detto rank; la forma (shape) è una tupla che indica la dimensione di ogni asse.
NumPy viene incluso automaticamente in distribuzioni come Anaconda. Se stai usando Python puro, installalo con pip:
Installazione
# Nel terminale
pip install numpy
# In un notebook Jupyter
!pip install numpy
Importazione standard
# Convenzione universale: importa numpy come np
import numpy as np
# Verifica versione
print(np.__version__) # es. 1.26.4
💡 L'alias np è una convenzione adottata da tutti. Usarla ti permette di leggere qualsiasi codice NumPy trovato online.
NumPy vs Python puro — velocità
Benchmark: somma di 1 milione di numeri
import numpy as np
import time
n = 1_000_000
# Python list
lst = list(range(n))
t0 = time.time()
s = sum(lst)
print(f"Python list: {(time.time()-t0)*1000:.2f} ms")
# NumPy array
arr = np.arange(n)
t0 = time.time()
s = arr.sum()
print(f"NumPy array: {(time.time()-t0)*1000:.2f} ms")
# NumPy è tipicamente 10-100x più veloce
Creare Array
NumPy offre molte funzioni per creare array senza digitare ogni elemento.
Funzioni di creazione principali
Array da lista / tuple
import numpy as np
# Da lista Python
a = np.array([1, 2, 3])
# Specificando il tipo
b = np.array([1.0, 2.0, 3.0], dtype=np.float32)
# Da tupla
c = np.array((10, 20, 30))
np.zeros, np.ones, np.full
import numpy as np
z = np.zeros((3, 4)) # matrice 3x4 di zeri float64
o = np.ones((2, 3)) # matrice 2x3 di uni float64
f = np.full((2, 2), 7) # matrice 2x2 riempita con 7
e = np.empty((3, 3)) # non inizializzato (valori casuali)
i = np.eye(3) # matrice identità 3x3
print(z)
print(f)
print(i)
np.arange e np.linspace
import numpy as np
# arange: come range() ma restituisce array
a = np.arange(0, 10, 2) # [0 2 4 6 8]
b = np.arange(1.0, 2.0, 0.1) # [1.0 1.1 1.2 ... 1.9]
# linspace: n punti equidistanti tra start e stop
c = np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1. ]
d = np.linspace(0, 10, 100) # 100 punti tra 0 e 10
print(a)
print(c)
import numpy as np
m = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
print(m[0, 0]) # 1 — riga 0, col 0
print(m[1, 2]) # 6 — riga 1, col 2
print(m[-1, 1]) # 8 — ultima riga, col 1
# Seleziona tutta la riga 1
print(m[1]) # [4 5 6]
# Seleziona tutta la colonna 0
print(m[:, 0]) # [1 4 7]
Indicizzazione fancy (array di indici)
import numpy as np
a = np.array([10, 20, 30, 40, 50])
# Seleziona elementi agli indici [0, 2, 4]
idx = [0, 2, 4]
print(a[idx]) # [10 30 50]
# Utile per riordinare
print(a[[4, 3, 2, 1, 0]]) # [50 40 30 20 10]
Indicizzazione booleana
import numpy as np
a = np.array([5, 10, 15, 20, 25])
mask = a > 12
print(mask) # [False False True True True]
print(a[mask]) # [15 20 25]
# In un passaggio solo
print(a[a % 10 == 0]) # [10 20]
🎯 Indicizzazione booleana: è il modo più idiomatico per filtrare dati in NumPy e Pandas. Usala spesso!
Slicing degli Array
La sintassi di slicing è [start:stop:step]. Come in Python, stop è escluso.
Slicing 1D
import numpy as np
a = np.array([0, 10, 20, 30, 40, 50, 60])
print(a[1:4]) # [10 20 30] — indici 1,2,3
print(a[:3]) # [ 0 10 20] — primi 3
print(a[4:]) # [40 50 60] — dal 4° in poi
print(a[::2]) # [ 0 20 40 60] — ogni 2
print(a[::-1]) # [60 50 40 30 20 10 0] — invertito
Slicing 2D
import numpy as np
m = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9,10,11,12]])
# Sottomatrice: righe 0:2, colonne 1:3
print(m[0:2, 1:3])
# [[2 3]
# [6 7]]
# Tutte le righe, colonne pari
print(m[:, ::2])
# [[ 1 3]
# [ 5 7]
# [ 9 11]]
Importante: lo slice è una VIEW
import numpy as np
a = np.array([1, 2, 3, 4, 5])
s = a[1:4] # s è una VIEW di a
s[0] = 99 # modifica anche a!
print(a) # [ 1 99 3 4 5]
# Per avere una copia indipendente usa .copy()
s2 = a[1:4].copy()
s2[0] = 0
print(a) # invariato
⚠️ Attenzione: lo slicing NumPy restituisce una view, non una copia! Modificare la slice modifica l'array originale. Usa .copy() se vuoi dati indipendenti.
Tipi di dato (dtype)
Ogni array NumPy ha un tipo di dato omogeneo (dtype) che determina quanta memoria occupa ogni elemento e come viene interpretato.
dtype
Descrizione
Byte
int8, int16, int32, int64
Interi con segno
1, 2, 4, 8
uint8, uint16, uint32, uint64
Interi senza segno
1, 2, 4, 8
float16, float32, float64
Virgola mobile
2, 4, 8
complex64, complex128
Numeri complessi
8, 16
bool
Booleano
1
object
Oggetto Python
variabile
str_
Stringa Unicode fissa
variabile
Specificare e controllare dtype
import numpy as np
a = np.array([1, 2, 3], dtype=np.float32)
print(a.dtype) # float32
print(a.itemsize) # 4
b = np.array([1.7, 2.9, 3.1], dtype=np.int32)
print(b) # [1 2 3] — troncato!
# Creare con tipo specifico
z = np.zeros((3, 3), dtype=np.uint8)
print(z.dtype) # uint8
Conversione con astype
import numpy as np
a = np.array([1.6, 2.7, 3.9])
print(a.dtype) # float64
b = a.astype(int) # tronca la parte decimale
print(b) # [1 2 3]
print(b.dtype) # int64 (o int32 su Windows)
c = b.astype(str)
print(c) # ['1' '2' '3']
print(c.dtype) # <U21
💡 Per il Machine Learning usa float32 invece di float64: dimezza la memoria e accelera i calcoli su GPU, con perdita trascurabile di precisione.
Copy vs View
Capire la differenza tra copia e vista è fondamentale per evitare bug silenziosi in NumPy.
View: le modifiche si propagano
Codice
import numpy as np
orig = np.array([1, 2, 3, 4, 5])
vista = orig.view()
print(vista.base is orig) # True — condivide i dati
vista[0] = 99
print(orig) # [99 2 3 4 5] ← modificato!
import numpy as np
a = np.array([1, 2, 3])
# slice → view
s = a[1:]
print(s.base is a) # True → è una view
# copy() → copia
c = a.copy()
print(c.base is None) # True → è una copia
# reshape può restituire view o copia
r = a.reshape(3, 1)
print(r.base is a) # True → view (reshape preferisce view)
🎯 Regola pratica: usa .copy() ogni volta che vuoi modificare un sottoarray senza toccare l'originale. Se non sei sicuro, verifica con array.base.
Shape e dimensioni
La shape di un array è una tupla che descrive il numero di elementi in ogni dimensione.
Leggere e modificare shape
import numpy as np
a = np.array([[1, 2, 3],
[4, 5, 6]])
print(a.shape) # (2, 3) — 2 righe, 3 colonne
print(a.ndim) # 2
print(a.size) # 6
# Modificare la shape (cambia la forma, non i dati)
a.shape = (3, 2)
print(a)
# [[1 2]
# [3 4]
# [5 6]]
np.squeeze e np.expand_dims
import numpy as np
# expand_dims: aggiunge un asse
a = np.array([1, 2, 3]) # shape (3,)
b = np.expand_dims(a, axis=0) # shape (1, 3)
c = np.expand_dims(a, axis=1) # shape (3, 1)
print(b.shape, c.shape) # (1, 3) (3, 1)
# squeeze: rimuove assi di dimensione 1
d = np.array([[[1, 2, 3]]]) # shape (1, 1, 3)
e = np.squeeze(d)
print(e.shape) # (3,)
reshape() cambia la forma di un array mantenendo gli stessi dati. Il numero totale di elementi deve rimanere invariato.
Reshape di base
import numpy as np
a = np.arange(12) # [0 1 2 ... 11]
print(a.shape) # (12,)
# Da 1D a 2D
b = a.reshape(3, 4)
print(b)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
# Da 1D a 3D
c = a.reshape(2, 2, 3)
print(c.shape) # (2, 2, 3)
Il valore -1 (calcolo automatico)
import numpy as np
a = np.arange(24)
# -1 lascia che NumPy calcoli quella dimensione
b = a.reshape(4, -1) # NumPy calcola 24/4 = 6
print(b.shape) # (4, 6)
c = a.reshape(-1, 6) # NumPy calcola 24/6 = 4
print(c.shape) # (4, 6)
# Appiattire un array multidimensionale
m = np.array([[1,2,3],[4,5,6]])
flat = m.reshape(-1) # stesso di m.flatten() ma è view
print(flat) # [1 2 3 4 5 6]
flatten vs ravel
import numpy as np
m = np.array([[1,2,3],[4,5,6]])
# flatten() → restituisce sempre una COPIA 1D
f = m.flatten()
f[0] = 99
print(m[0,0]) # 1 — invariato
# ravel() → restituisce VIEW se possibile
r = m.ravel()
r[0] = 99
print(m[0,0]) # 99 — modificato!
💡 Il Machine Learning usa reshape(-1, 1) o reshape(1, -1) continuamente per adattare i vettori alle API di Scikit-learn.
Iterazione sugli Array
Iterare su array NumPy con cicli Python è lento. Quando possibile, usa le operazioni vettorizzate. Quando devi iterare, conosci gli strumenti giusti.
Iterazione di base
import numpy as np
a = np.array([1, 2, 3, 4])
# 1D: itera sugli elementi
for x in a:
print(x)
# 2D: itera sulle righe
m = np.array([[1,2,3],[4,5,6]])
for row in m:
print(row)
np.nditer — iteratore universale
import numpy as np
m = np.array([[1,2,3],[4,5,6]])
# Itera su tutti gli elementi (ordine C, riga per riga)
for x in np.nditer(m):
print(x, end=' ')
# 1 2 3 4 5 6
# Con indice
for idx, x in np.ndenumerate(m):
print(idx, x)
# (0,0) 1 (0,1) 2 (0,2) 3 ...
Meglio: operazioni vettorizzate
import numpy as np
a = np.arange(1_000_000, dtype=float)
# Lento (ciclo Python)
# for i in range(len(a)): a[i] = a[i] ** 2
# Veloce (operazione vettorizzata)
b = a ** 2 # tutto in C, senza cicli
c = np.sqrt(a) # radice quadrata
d = np.where(a > 500_000, 1, 0) # condizionale
🎯 Regola d'oro NumPy: se ti ritrovi a scrivere un for su un array NumPy, fermati e chiediti se esiste un'operazione vettorizzata equivalente. Quasi sempre esiste.
💡 Queste tecniche di filtro sono identiche a come funzionano i filtri su Series e DataFrame in Pandas. Padroneggiare NumPy rende Pandas molto più intuitivo.
NumPy Random — Numeri casuali
Il modulo numpy.random (e la nuova API numpy.random.Generator) offre generatori di numeri pseudo-casuali molto più efficienti di quelli del modulo random di Python.
API moderna: np.random.default_rng()
import numpy as np
# Seed → risultati riproducibili
rng = np.random.default_rng(seed=42)
# Float in [0, 1)
r = rng.random(5)
print(r)
# Interi
i = rng.integers(0, 100, size=10)
print(i)
# Scelta casuale
scelte = rng.choice(['A', 'B', 'C'], size=5)
print(scelte)
# Permutazione
a = np.arange(10)
rng.shuffle(a)
print(a)
API classica (ancora usata)
import numpy as np
np.random.seed(42) # riproducibilità
r1 = np.random.rand(3, 3) # float uniformi [0,1)
r2 = np.random.randn(3, 3) # normale standard
r3 = np.random.randint(1, 7, size=10) # dado a 6 facce
print("Dado:", r3)
# Campionamento senza sostituzione
pop = np.arange(100)
campione = np.random.choice(pop, size=10, replace=False)
print("Campione:", campione)
Distribuzioni statistiche
NumPy permette di campionare da distribuzioni di probabilità fondamentali, utili per simulazioni, test e inizializzazione di modelli ML.
Le ufunc (Universal Functions) sono funzioni NumPy che operano elemento per elemento su array, usando codice C compilato. Sono la ragione principale per cui NumPy è così veloce.
Ogni operazione aritmetica come a + b è in realtà una chiamata a una ufunc (np.add(a, b)).
import numpy as np
# Funzione Python normale
def aggiungi_n(a, b):
return a + b
# Convertila in ufunc
ufunc_add = np.frompyfunc(aggiungi_n, 2, 1)
a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
print(ufunc_add(a, b)) # [11 22 33]
# Più utile per funzioni custom complesse
def clip_e_normalizza(x):
return max(0, min(1, x))
vfunc = np.vectorize(clip_e_normalizza)
print(vfunc(np.array([-0.5, 0.3, 1.2, 0.8])))
# [0. 0.3 1. 0.8]
💡 np.vectorize è comoda ma non vera ufunc (non è più veloce dei cicli Python). Per performance usa le ufunc native NumPy o scrivi estensioni C/Cython.
Aritmetica e Broadcasting
Le operazioni aritmetica sono applicate elemento per elemento. Il broadcasting permette operazioni tra array di forma diversa seguendo regole precise.
Operazioni elemento per elemento
import numpy as np
a = np.array([10, 20, 30])
b = np.array([1, 2, 3])
print(a + b) # [11 22 33]
print(a - b) # [ 9 18 27]
print(a * b) # [10 40 90]
print(a / b) # [10. 10. 10.]
print(a // b) # [10 10 10] — divisione intera
print(a % b) # [0 0 0] — modulo
print(a ** 2) # [100 400 900]
print(np.add(a, b)) # equivalente a a + b
print(np.multiply(a, b)) # equivalente a a * b
Broadcasting
import numpy as np
# Scalare x array
a = np.array([1, 2, 3, 4])
print(a * 10) # [10 20 30 40]
# (3,) + (3,1) → (3,3)
v = np.array([1, 2, 3])
col = np.array([[10], [20], [30]])
print(v + col)
# [[11 12 13]
# [21 22 23]
# [31 32 33]]
# Pratico: centrare i dati (zero-mean)
dati = np.array([[1.,2.,3.],[4.,5.,6.]])
media = dati.mean(axis=0) # shape (3,)
centrati = dati - media # broadcasting (2,3) - (3,)
print(centrati)
🎯 Regola broadcasting: due dimensioni sono compatibili se sono uguali, oppure una di esse è 1. NumPy "espande" il lato con 1 per far combaciare le forme.
Prodotto tra matrici
import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
# Prodotto elemento per elemento (NON prodotto matriciale)
print(A * B)
# [[ 5 12]
# [21 32]]
# Prodotto matriciale (dot product)
print(A @ B) # oppure np.dot(A, B)
# [[19 22]
# [43 50]]
# Trasposta
print(A.T)
# [[1 3]
# [2 4]]
Funzioni matematiche e statistiche
NumPy include decine di funzioni per somme, medie, varianze, round e molto altro.
Funzioni di aggregazione
import numpy as np
m = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]], dtype=float)
print(m.sum()) # 45.0 — somma totale
print(m.sum(axis=0)) # [ 12. 15. 18.] — somma colonne
print(m.sum(axis=1)) # [ 6. 15. 24.] — somma righe
print(m.mean()) # 5.0
print(m.mean(axis=0)) # [4. 5. 6.]
print(m.std()) # deviazione standard
print(m.var()) # varianza
print(m.min(), m.max()) # 1.0 9.0
print(m.cumsum()) # somma cumulativa
import numpy as np
A = np.array([[2., 1.],
[5., 3.]])
# Determinante
print(np.linalg.det(A)) # 1.0
# Matrice inversa
inv = np.linalg.inv(A)
print(inv)
print(A @ inv) # ≈ matrice identità
# Risolvi Ax = b
b = np.array([4., 7.])
x = np.linalg.solve(A, b)
print(x) # soluzione del sistema
# Autovalori e autovettori
vals, vecs = np.linalg.eig(A)
print("Autovalori:", vals)
Esercizi pratici
🏋️ Esercizio 1 — Statistiche su dati reali
Crea un array NumPy con 50 temperature simulate (distribuzione normale, media=20°C, std=5°C). Calcola: media, mediana, deviazione standard, percentuale di valori sotto i 15°C e sopra i 25°C.
Crea due matrici 3×3 con valori interi casuali tra 1 e 10. Calcola il loro prodotto matriciale, la trasposta del risultato e verifica che (AB)ᵀ = BᵀAᵀ.
Soluzione Esercizio 2
import numpy as np
rng = np.random.default_rng(7)
A = rng.integers(1, 10, size=(3,3))
B = rng.integers(1, 10, size=(3,3))
AB = A @ B
print("A @ B =")
print(AB)
ABt = AB.T
BtAt = B.T @ A.T
print("(AB)ᵀ == BᵀAᵀ:", np.array_equal(ABt, BtAt)) # True
🏋️ Esercizio 3 — Normalizzazione Min-Max
Dato un array 2D (5×4) con valori casuali, normalizza ogni colonna nell'intervallo [0, 1] usando la formula: x_norm = (x - min) / (max - min). Verifica che ogni colonna abbia min=0 e max=1.
Soluzione Esercizio 3
import numpy as np
rng = np.random.default_rng(99)
X = rng.uniform(10, 100, size=(5, 4))
col_min = X.min(axis=0) # shape (4,)
col_max = X.max(axis=0) # shape (4,)
X_norm = (X - col_min) / (col_max - col_min)
print("Min per colonna:", X_norm.min(axis=0).round(10))
print("Max per colonna:", X_norm.max(axis=0).round(10))
# Tutti 0.0 e 1.0
E ora?
Hai completato il corso NumPy! Ora conosci gli strumenti fondamentali per lavorare con dati numerici in Python ad alte prestazioni. Il passo naturale successivo è Pandas, che costruisce sopra NumPy per gestire dati tabulari e serie temporali.
Cosa hai imparato:
Creare e manipolare array N-dimensionali
Indicizzazione, slicing e filtro booleano
Tipi di dato, copy vs view, shape e reshape
Generatori di numeri casuali e distribuzioni statistiche
Universal functions, broadcasting e algebra lineare