HTML CSS JAVASCRIPT SQL MYSQL PYTHON JAVA PHP C C++ C# PACKET TRACER CYBERSECURITY REACT TYPESCRIPT NODE.JS GIT & GITHUB DATA SCIENCE BOOTSTRAP GO KOTLIN JQUERY DJANGO VUE ANGULAR MONGODB POSTGRESQL SASS NUMPY PANDAS SCIPY DSA RUST SWIFT BASHXMLW3.CSSANGULARJSAIAWSPROGRAMMAZIONEEXCEL WORD POWERPOINT
Corso gratuito

Corso NumPy in italiano

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.

CaratteristicaPython listNumPy array
VelocitàLenta (oggetti Python)Molto veloce (C sotto)
MemoriaElevataCompatta e contigua
Operazioni vettorialiRichiede cicliBroadcast nativo
Tipo datiMistoOmogeneo
DimensioniSolo 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.

Il tuo primo array NumPy
import numpy as np

# Array 1D (vettore)
a = np.array([1, 2, 3, 4, 5])
print(a)          # [1 2 3 4 5]
print(type(a))    # <class 'numpy.ndarray'>
print(a.dtype)    # int64
print(a.shape)    # (5,)
print(a.ndim)     # 1

# Array 2D (matrice)
m = np.array([[1, 2, 3],
              [4, 5, 6]])
print(m.shape)    # (2, 3)
print(m.ndim)     # 2

Attributi principali di ndarray

AttributoDescrizioneEsempio
ndimNumero di dimensionia.ndim → 2
shapeForma (tupla)a.shape → (3, 4)
sizeNumero totale di elementia.size → 12
dtypeTipo di dato degli elementia.dtype → float64
itemsizeByte per elementoa.itemsize → 8
Array 0D, 1D, 2D, 3D
import numpy as np

a0 = np.array(42)              # 0D (scalare)
a1 = np.array([1, 2, 3])       # 1D (vettore)
a2 = np.array([[1,2],[3,4]])   # 2D (matrice)
a3 = np.array([[[1,2],[3,4]],  # 3D (tensore)
               [[5,6],[7,8]]])

print(a0.ndim, a1.ndim, a2.ndim, a3.ndim)  # 0 1 2 3

Installazione e importazione

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)
Array casuali
import numpy as np

# Float uniformi in [0, 1)
r1 = np.random.rand(3, 4)

# Interi casuali
r2 = np.random.randint(0, 100, size=(3, 3))

# Distribuzione normale (media=0, std=1)
r3 = np.random.randn(5)

print(r2)
FunzioneDescrizione
np.zeros(shape)Array di zeri
np.ones(shape)Array di uni
np.full(shape, val)Array riempito con val
np.eye(n)Matrice identità n×n
np.arange(start, stop, step)Sequenza con passo
np.linspace(start, stop, n)n punti equidistanti
np.random.rand(shape)Float casuali [0,1)
np.random.randint(low, high, size)Interi casuali

Indicizzazione degli Array

L'indicizzazione NumPy parte da 0. Puoi usare indici negativi per accedere agli elementi dalla fine.

Indicizzazione 1D
import numpy as np

a = np.array([10, 20, 30, 40, 50])

print(a[0])   # 10  — primo elemento
print(a[-1])  # 50  — ultimo elemento
print(a[-2])  # 40  — penultimo

# Modifica
a[2] = 99
print(a)      # [10 20 99 40 50]
Indicizzazione 2D
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.

dtypeDescrizioneByte
int8, int16, int32, int64Interi con segno1, 2, 4, 8
uint8, uint16, uint32, uint64Interi senza segno1, 2, 4, 8
float16, float32, float64Virgola mobile2, 4, 8
complex64, complex128Numeri complessi8, 16
boolBooleano1
objectOggetto Pythonvariabile
str_Stringa Unicode fissavariabile
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!
Copia: dati indipendenti
import numpy as np

orig = np.array([1, 2, 3, 4, 5])
copia = orig.copy()

print(copia.base is None)  # True — copia indipendente

copia[0] = 99
print(orig)   # [1 2 3 4 5]  ← invariato
print(copia)  # [99  2  3  4  5]
Come verificare
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,)
Confronto tra forme diverse
import numpy as np

# shape (3,) — 1D
v = np.array([1, 2, 3])

# shape (1, 3) — riga
row = v[np.newaxis, :]
print(row.shape)   # (1, 3)

# shape (3, 1) — colonna
col = v[:, np.newaxis]
print(col.shape)   # (3, 1)

# Utile per il broadcasting
print(col + row)
# [[2 3 4]
#  [3 4 5]
#  [4 5 6]]

Reshape — rimodellare gli Array

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.

Join e Split degli Array

Concatenare array

np.concatenate
import numpy as np

a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

# 1D
c = np.concatenate([a, b])
print(c)  # [1 2 3 4 5 6]

# 2D: axis=0 (lungo le righe)
m1 = np.array([[1,2],[3,4]])
m2 = np.array([[5,6],[7,8]])
print(np.concatenate([m1, m2], axis=0))
# [[1 2]
#  [3 4]
#  [5 6]
#  [7 8]]

# axis=1 (lungo le colonne)
print(np.concatenate([m1, m2], axis=1))
# [[1 2 5 6]
#  [3 4 7 8]]
np.stack, np.vstack, np.hstack
import numpy as np

a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

# stack: crea un nuovo asse
print(np.stack([a, b]))         # shape (2, 3)
print(np.stack([a, b], axis=1)) # shape (3, 2)

# vstack: impila verticalmente
print(np.vstack([a, b]))        # shape (2, 3)

# hstack: impila orizzontalmente
print(np.hstack([a, b]))        # shape (6,)

Dividere array

np.split, np.hsplit, np.vsplit
import numpy as np

a = np.arange(12)

# split in 3 parti uguali
parti = np.split(a, 3)
print(parti)  # [array([0,1,2,3]), array([4,5,6,7]), ...]

# split in posizioni specifiche
parti2 = np.split(a, [3, 7])  # split a indice 3 e 7
print(parti2)

# Matrice
m = np.arange(16).reshape(4, 4)
righe = np.vsplit(m, 2)    # 2 metà orizzontali
cols  = np.hsplit(m, 2)    # 2 metà verticali

Filtro degli Array

Filtrare significa selezionare elementi che soddisfano una condizione. NumPy offre due approcci: indicizzazione booleana e np.where.

Filtro con maschera booleana
import numpy as np

a = np.array([10, 15, 20, 25, 30, 35, 40])

# Crea maschera
maschera = a > 22
print(maschera)   # [False False False  True  True  True  True]

# Applica maschera
filtrato = a[maschera]
print(filtrato)   # [25 30 35 40]

# In un passaggio solo
print(a[a % 10 == 0])   # [10 20 30 40]
Filtro con np.where
import numpy as np

a = np.array([10, -5, 20, -3, 15, -8, 30])

# Dove negativo, metti 0
b = np.where(a < 0, 0, a)
print(b)   # [10  0 20  0 15  0 30]

# Sostituisci con valore alternativo
c = np.where(a > 0, a, -a)   # valore assoluto!
print(c)   # [10  5 20  3 15  8 30]
Broadcasting e filtro combinato
import numpy as np

prezzi = np.array([9.99, 24.99, 5.50, 49.99, 12.00, 3.99])
vendite = np.array([100, 45, 200, 20, 80, 300])

# Revenue per prodotto
revenue = prezzi * vendite

# Prodotti con revenue > 1000
print(prezzi[revenue > 1000])
# [ 9.99  5.5   3.99]

# Statistiche su sottoinsieme
alto = vendite[vendite > 100]
print(f"Media vendite alte: {alto.mean():.1f}")
💡 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.

DistribuzioneFunzioneParametriUso tipico
Normale (Gaussiana)rng.normal()loc, scale, sizeRumori, pesi ML
Uniformerng.uniform()low, high, sizeInizializzazione
Binomialerng.binomial()n, p, sizeEsperimenti sì/no
Poissonrng.poisson()lam, sizeConteggi rari
Esponenzialerng.exponential()scale, sizeTempi di attesa
Chi-quadratorng.chisquare()df, sizeTest statistici
Distribuzione normale
import numpy as np

rng = np.random.default_rng(0)

# media=170, dev.std=10 — altezze simulate
altezze = rng.normal(loc=170, scale=10, size=1000)

print(f"Media:  {altezze.mean():.2f} cm")
print(f"Std:    {altezze.std():.2f} cm")
print(f"Min:    {altezze.min():.2f} cm")
print(f"Max:    {altezze.max():.2f} cm")

# ~68% tra 160 e 180
in_range = ((altezze >= 160) & (altezze <= 180)).sum()
print(f"Tra 160-180: {in_range/10:.1f}%")
Binomiale, Poisson, Esponenziale
import numpy as np

rng = np.random.default_rng(1)

# Binomiale: 10 lanci di moneta, p=0.5 — quante teste?
moneta = rng.binomial(n=10, p=0.5, size=100)
print("Teste medie:", moneta.mean())   # ≈ 5

# Poisson: media 3 email/ora
email = rng.poisson(lam=3, size=1000)
print("Email medie:", email.mean())    # ≈ 3

# Esponenziale: tempo tra chiamate (media 5 min)
tempi = rng.exponential(scale=5, size=500)
print("Attesa media:", tempi.mean(), "min")  # ≈ 5

Universal Functions (ufunc)

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

ufunc di base
import numpy as np

a = np.array([1, 4, 9, 16, 25])

print(np.sqrt(a))    # [1. 2. 3. 4. 5.]
print(np.square(a))  # [  1  16  81 256 625]
print(np.abs(np.array([-1, -2, 3])))  # [1 2 3]
print(np.log(a))     # logaritmo naturale
print(np.log10(a))   # logaritmo base 10
print(np.exp(np.array([0, 1, 2])))  # [1. 2.718 7.389]
Creare una ufunc personalizzata
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
Arrotondamento e trigonometria
import numpy as np

a = np.array([1.234, 2.567, 3.891])

print(np.round(a, 1))    # [1.2 2.6 3.9]
print(np.floor(a))       # [1. 2. 3.]
print(np.ceil(a))        # [2. 3. 4.]
print(np.trunc(a))       # [1. 2. 3.]

# Trigonometria (angoli in radianti)
angoli = np.array([0, np.pi/6, np.pi/4, np.pi/3, np.pi/2])
print(np.sin(angoli).round(4))
print(np.cos(angoli).round(4))

# Conversione gradi ↔ radianti
gradi = np.array([0, 30, 45, 90, 180])
print(np.radians(gradi))
print(np.degrees(np.pi))  # 180.0
Algebra lineare con np.linalg
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.

Soluzione Esercizio 1
import numpy as np

rng = np.random.default_rng(42)
temp = rng.normal(loc=20, scale=5, size=50)

print(f"Media:    {temp.mean():.2f} °C")
print(f"Mediana:  {np.median(temp):.2f} °C")
print(f"Std:      {temp.std():.2f} °C")
print(f"< 15°C:   {(temp < 15).sum()} ({(temp < 15).mean()*100:.1f}%)")
print(f"> 25°C:   {(temp > 25).sum()} ({(temp > 25).mean()*100:.1f}%)")
🏋️ Esercizio 2 — Moltiplicazione di matrici

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