CLASSE TERZA • MODULO 03

NumPy Essenziale

La libreria fondamentale per il calcolo scientifico in Python

🎯 Introduzione al Superpotere di Python

Hai mai provato a sommare 1000 numeri con un calcolatore normale? Ci vorrebbero ore! E se ti dicessi che con NumPy puoi fare operazioni su milioni di numeri in pochi millisecondi?

In questo modulo scoprirai NumPy, la libreria che trasforma Python in una macchina da calcolo super-veloce. È la base di tutto il Machine Learning e la Data Science!

💡 Analogia: Il Supermercato

Liste Python normali sono come fare la spesa con un carrello piccolo: puoi mettere qualsiasi cosa (mele, libri, scarpe), ma è lento e disorganizzato.

Array NumPy sono come i pallet di un supermercato: tutti gli articoli sono dello stesso tipo, perfettamente organizzati, e puoi muovere 1000 scatole tutte insieme con un solo carrello elevatore! ⚡

🚀 Cos'è NumPy?

NumPy (Numerical Python) è una libreria, cioè un "pacchetto aggiuntivo" che puoi installare in Python per ottenere nuove funzionalità. In particolare, NumPy ti dà un nuovo tipo di dato chiamato array, pensato apposta per lavorare con i numeri in modo velocissimo.

⚡ 100x Più Veloce

Le operazioni su array NumPy sono fino a 100 volte più veloci delle liste Python normali

🧮 Magia Vettoriale

Puoi eseguire operazioni su migliaia di elementi contemporaneamente, senza scrivere loop!

🔢 Base Universale

Pandas, TensorFlow, Scikit-Learn... tutte le librerie AI usano NumPy come fondamenta

💡 Perché è così veloce?

NumPy è scritto in C e Fortran (linguaggi super-ottimizzati), non in Python puro. È come avere un motore di Formula 1 invece di una bicicletta! 🏎️

📦 Installazione e Primo Import

✅ Se hai Anaconda: Niente da fare!

NumPy è già installato con Anaconda. Sei pronto a partire! 🎉

Se NON hai Anaconda

Apri il terminale e digita:

pip install numpy

Il Primo Import

Prima di usare NumPy, devi importarlo. Importare significa dire a Python: "Carica questa libreria, ne ho bisogno!"

import numpy as np

print("NumPy versione:", np.__version__)
print("✅ NumPy è pronto all'uso!")

🔍 Spieghiamo pezzo per pezzo: import numpy as np

import numpy → dice a Python: "carica la libreria NumPy nel mio programma".

as np → le assegna un soprannome breve. Invece di scrivere numpy.qualcosa() ogni volta, scriverai np.qualcosa(). Meno lettere, stesso risultato!

Tutti i programmatori del mondo usano il soprannome np per NumPy. È una convenzione universale.

Da questo momento, ogni volta che vedi np. nel codice, sappi che stiamo usando una funzione di NumPy.

🆚 Lista Python vs Array NumPy

Con le liste Python (il vecchio modo)

Per calcolare la media di alcuni voti con una lista normale, devi scrivere un ciclo for e fare i conti a mano:

voti = [7, 8, 6, 9, 7.5, 8.5]

somma = 0
for voto in voti:
    somma += voto
media = somma / len(voti)
print(f"Media: {media:.2f}")  # 7.67

Con NumPy (il nuovo modo) ⚡

Con NumPy, crei un array e poi usi la funzione .mean() che calcola la media al posto tuo, senza loop:

import numpy as np

voti_np = np.array([7, 8, 6, 9, 7.5, 8.5])

media = voti_np.mean()
print(f"Media: {media:.2f}")  # 7.67

🔍 Spieghiamo: np.array([7, 8, 6, 9, 7.5, 8.5])

np → è NumPy (il soprannome che gli abbiamo dato).

.array() → è una funzione di NumPy che crea un nuovo array.

[7, 8, 6, 9, 7.5, 8.5] → è la lista di numeri che vuoi mettere dentro l'array.

In pratica stai dicendo: "NumPy, prendi questi numeri e trasformali in un array super-veloce!"

🔍 Spieghiamo: voti_np.mean()

Quando hai un array NumPy, puoi usare il punto . seguito dal nome di una funzione per fare calcoli. Queste funzioni "appartengono" all'array e sanno lavorare sui suoi dati.

voti_np.mean() → calcola la media di tutti i numeri nell'array.

voti_np.min() → trova il valore più piccolo.

voti_np.max() → trova il valore più grande.

voti_np.sum() → calcola la somma di tutti i numeri.

voti_np.std() → calcola la deviazione standard (quanto i valori sono "sparsi").

È come se l'array fosse un oggetto "intelligente" che sa già fare i calcoli su sé stesso!

⚠️ Regola d'Oro di NumPy:

Gli array NumPy devono contenere tutti elementi dello stesso tipo (tutti numeri interi, oppure tutti numeri con la virgola, ecc.). Questo è il segreto della loro velocità!

Le liste Python possono avere tipi misti (numeri, stringhe, booleani...) ma sono più lente.

🏗️ Creare Array: Tanti Modi

1️⃣ Da una lista Python

Il modo più semplice: prendi una lista e la trasformi in array con np.array().

import numpy as np

temperature = [22, 25, 18, 30, 15, 28]
temp_array = np.array(temperature)

print("Temperature:", temp_array)
print("Tipo elementi:", temp_array.dtype)

🔍 Spieghiamo: .dtype

dtype sta per "data type" (tipo di dato). Ti dice che tipo di numeri ci sono dentro l'array.

Per esempio: int64 significa "numeri interi", float64 significa "numeri con la virgola".

A differenza di .mean(), qui non ci sono le parentesi perché dtype non è una funzione da eseguire, ma una proprietà (un'informazione) dell'array.

2️⃣ Array di zeri: np.zeros()

Crea un array pieno di zeri. Utile quando sai la dimensione ma lo riempirai dopo.

zeri = np.zeros(5)
print("5 zeri:", zeri)  # [0. 0. 0. 0. 0.]

# Per una tabella (2D): servono le doppie parentesi
tabella_zeri = np.zeros((3, 4))  # 3 righe, 4 colonne
print("Tabella 3x4 di zeri:")
print(tabella_zeri)

🔍 Perché np.zeros((3, 4)) ha le doppie parentesi?

Le parentesi esterne sono quelle normali della funzione zeros().

Le parentesi interne (3, 4) formano una tupla, cioè una coppia di numeri che indica la forma: 3 righe e 4 colonne.

Se vuoi un semplice array di 5 elementi, basta np.zeros(5) (una sola parentesi).

3️⃣ Array di uni: np.ones()

Come zeros() ma riempie tutto con il numero 1. Utile per creare array con un valore a scelta:

uni = np.ones(5)
print("5 uni:", uni)  # [1. 1. 1. 1. 1.]

# Trucco: moltiplica per avere un array con il valore che vuoi
array_di_10 = np.ones(5) * 10
print("5 dieci:", array_di_10)  # [10. 10. 10. 10. 10.]

4️⃣ Sequenze con arange() e linspace()

# arange(inizio, fine, passo) → numeri a salti regolari
# "Parti da 0, vai fino a 10 (escluso), a salti di 2"
numeri = np.arange(0, 10, 2)
print("arange(0, 10, 2):", numeri)  # [0 2 4 6 8]

# linspace(inizio, fine, quanti) → N punti equidistanti
# "Dammi 5 punti distribuiti uniformemente tra 0 e 1"
punti = np.linspace(0, 1, 5)
print("linspace(0, 1, 5):", punti)  # [0.   0.25 0.5  0.75 1.  ]

🔍 Differenza chiave tra arange e linspace

Con arange decidi il passo (ogni quanto saltare) e NumPy calcola quanti numeri escono.

Con linspace decidi quanti numeri vuoi e NumPy calcola il passo automaticamente.

5️⃣ Numeri casuali

# 5 numeri casuali decimali tra 0 e 1
casuali = np.random.random(5)
print("Casuali:", casuali)

# 10 numeri interi casuali tra 1 e 6 (come un dado)
dadi = np.random.randint(1, 7, size=10)
print("10 lanci di dado:", dadi)

# Distribuzione normale (campana di Gauss)
# loc = media desiderata, scale = deviazione standard, size = quanti
altezze = np.random.normal(loc=170, scale=10, size=5)
print("5 altezze casuali (media ~170cm):", altezze)

🔍 Spieghiamo: np.random.randint(1, 7, size=10)

np.random → il sotto-modulo di NumPy dedicato ai numeri casuali.

.randint(1, 7, ...) → genera numeri interi casuali. Il primo numero (1) è il minimo, il secondo (7) è il massimo escluso. Quindi i valori possibili sono: 1, 2, 3, 4, 5, 6.

size=10 → quanti numeri casuali generare.

🔍 Spieghiamo: np.random.normal(loc=170, scale=10, size=5)

Questa funzione genera numeri casuali che seguono la distribuzione normale (la famosa "campana di Gauss"). I numeri generati saranno concentrati attorno alla media, con pochi valori molto lontani.

loc=170 → la media (il centro della campana). I numeri generati saranno "attorno" a 170.

scale=10 → la deviazione standard (quanto i numeri si allontanano dalla media). Più è grande, più i numeri sono "sparsi".

size=5 → quanti numeri generare.

✨ Le Operazioni Vettoriali

Ecco la vera magia di NumPy: puoi fare operazioni matematiche su tutti gli elementi di un array contemporaneamente, senza scrivere nessun ciclo for!

Operazioni tra due array

Quando usi +, -, * o / tra due array della stessa lunghezza, NumPy applica l'operazione posizione per posizione:

import numpy as np

a = np.array([1, 2, 3, 4, 5])
b = np.array([10, 20, 30, 40, 50])

print("a + b =", a + b)    # [11 22 33 44 55]
print("a * b =", a * b)    # [10 40 90 160 250]
print("a ** 2 =", a ** 2)  # [1 4 9 16 25]
📝 Cosa succede dentro a + b, passo per passo: Posizione 0: 1 + 10 = 11 Posizione 1: 2 + 20 = 22 Posizione 2: 3 + 30 = 33 Posizione 3: 4 + 40 = 44 Posizione 4: 5 + 50 = 55 Risultato finale: [11, 22, 33, 44, 55]

Operazioni tra un array e un numero (Broadcasting)

Puoi anche usare un singolo numero con un intero array. NumPy applica quel numero a ogni elemento automaticamente. Questo meccanismo si chiama broadcasting:

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

# Aumento del 20% su TUTTI i prezzi
nuovi_prezzi = prezzi * 1.2
print("Prezzi +20%:", nuovi_prezzi)  # [12. 24. 36. 48. 60.]

# Sconto di 5€ su TUTTI
scontati = prezzi - 5
print("Prezzi -5€:", scontati)  # [5 15 25 35 45]
📝 Cosa succede dentro prezzi * 1.2: NumPy "espande" il numero 1.2 e lo applica a ogni elemento: 10 * 1.2 = 12.0 20 * 1.2 = 24.0 30 * 1.2 = 36.0 40 * 1.2 = 48.0 50 * 1.2 = 60.0 Risultato: [12.0, 24.0, 36.0, 48.0, 60.0]

Esempio pratico: Voti Finali

scritto = np.array([7, 8, 6, 9, 7.5])
orale   = np.array([8, 7.5, 7, 8.5, 8])

# Media ponderata: scritto vale 60%, orale vale 40%
voto_finale = scritto * 0.6 + orale * 0.4

print("Voti finali:", voto_finale)
📝 Come viene calcolato scritto * 0.6 + orale * 0.4: Prima: scritto * 0.6 7*0.6=4.2 8*0.6=4.8 6*0.6=3.6 9*0.6=5.4 7.5*0.6=4.5 Poi: orale * 0.4 8*0.4=3.2 7.5*0.4=3.0 7*0.4=2.8 8.5*0.4=3.4 8*0.4=3.2 Infine: somma posizione per posizione 4.2+3.2=7.4 4.8+3.0=7.8 3.6+2.8=6.4 5.4+3.4=8.8 4.5+3.2=7.7 Risultato: [7.4, 7.8, 6.4, 8.8, 7.7]

🧊 Le Matrici (Array 2D)

Fino ad ora abbiamo usato array 1D (una riga di numeri). Ma NumPy può anche gestire tabelle con righe e colonne, proprio come un foglio Excel. Queste tabelle si chiamano array 2D o matrici.

Come creare una matrice

Per creare un array 2D, passi a np.array() una lista di liste. Ogni lista interna diventa una riga:

import numpy as np

# 3 studenti, 4 materie ciascuno
voti_classe = np.array([
    [7, 8, 6, 9],      # Riga 0 = Studente 0
    [8, 7, 7.5, 8.5],  # Riga 1 = Studente 1
    [6, 6.5, 7, 7]     # Riga 2 = Studente 2
])

print("Voti:")
print(voti_classe)
print("Forma:", voti_classe.shape)  # (3, 4) = 3 righe, 4 colonne
print("Totale elementi:", voti_classe.size)   # 12
print("Numero dimensioni:", voti_classe.ndim) # 2

🔍 Spieghiamo: .shape, .size, .ndim

Sono tre proprietà che ti danno informazioni sulla struttura dell'array:

.shape → la forma dell'array. (3, 4) significa 3 righe e 4 colonne.

.size → il numero totale di elementi (3 × 4 = 12).

.ndim → il numero di dimensioni. Un array 1D (una riga) ha ndim=1, un array 2D (tabella) ha ndim=2.

🎨 Visualizzazione della matrice

Immagina una tabella: le righe sono studenti, le colonne sono materie

Col 0
Col 1
Col 2
Col 3
Riga 0
7
8
6
9
Riga 1
8
7
7.5
8.5
Riga 2
6
6.5
7
7

Leggere valori dalla matrice

Per accedere a un valore devi dire a NumPy quale riga e quale colonna vuoi, usando la sintassi matrice[riga, colonna]. Ricorda: si conta sempre da 0!

# Un singolo valore: [riga, colonna]
print(voti_classe[0, 2])  # Riga 0, Colonna 2 → 6

# Un'intera riga: basta l'indice della riga
print(voti_classe[1])  # Tutta la Riga 1 → [8, 7, 7.5, 8.5]

# Un'intera colonna: usa : per dire "tutte le righe"
print(voti_classe[:, 0])  # Tutte le righe, Colonna 0 → [7, 8, 6]

# Una sotto-tabella (slice): prime 2 righe, prime 3 colonne
print(voti_classe[:2, :3])

🔍 Spieghiamo: cosa significa : (i due punti)?

Il simbolo : dentro le parentesi quadre significa "tutti".

voti_classe[:, 0] → leggiamolo così: "prendi TUTTE le righe (ecco il :), ma solo la colonna 0".

Il risultato è un array con il primo voto di ogni studente: [7, 8, 6].

🔍 Spieghiamo: voti_classe[:2, :3]

:2 → "dalla riga 0 alla riga 2 esclusa", quindi righe 0 e 1.

:3 → "dalla colonna 0 alla colonna 3 esclusa", quindi colonne 0, 1 e 2.

Il risultato è una sotto-tabella 2×3 (il "pezzo" in alto a sinistra della matrice).

📝 Traccia visiva: voti_classe[:, 0] — prendo tutta la colonna 0 La matrice: Colonna 0 [7, 8, 6, 9 ] → 7 [8, 7, 7.5, 8.5] → 8 [6, 6.5, 7, 7 ] → 6 Risultato: [7, 8, 6]

Calcoli su righe e colonne: il parametro axis

Quando hai una matrice e vuoi calcolare la media, devi dire a NumPy in quale direzione calcolarla. Vuoi la media di ogni studente (ogni riga)? O la media di ogni materia (ogni colonna)? Per questo esiste il parametro axis.

# Media di ogni RIGA (= media di ogni studente)
media_studenti = voti_classe.mean(axis=1)
print("Media per studente:", media_studenti)

# Media di ogni COLONNA (= media di ogni materia)
media_materie = voti_classe.mean(axis=0)
print("Media per materia:", media_materie)

# Media di TUTTO (nessun axis)
media_totale = voti_classe.mean()
print(f"Media totale: {media_totale:.2f}")

🔍 Spieghiamo: come funziona axis?

Questo è un concetto che confonde molti, quindi andiamo piano.

axis=1"calcola lungo le colonne", cioè orizzontalmente. Per ogni riga, prende tutti i valori delle colonne e ne fa la media. Il risultato è un valore per ogni riga (= un valore per studente).

axis=0"calcola lungo le righe", cioè verticalmente. Per ogni colonna, prende tutti i valori delle righe e ne fa la media. Il risultato è un valore per ogni colonna (= un valore per materia).

Nessun axis → calcola su TUTTI gli elementi dell'intera matrice.

📝 Traccia: mean(axis=1) — media per studente (per ogni riga) Riga 0: [7, 8, 6, 9] → (7+8+6+9) / 4 = 7.50 Riga 1: [8, 7, 7.5, 8.5] → (8+7+7.5+8.5) / 4 = 7.75 Riga 2: [6, 6.5, 7, 7] → (6+6.5+7+7) / 4 = 6.63 Risultato: [7.50, 7.75, 6.63] ← un valore per ogni riga!
📝 Traccia: mean(axis=0) — media per materia (per ogni colonna) Col 0: [7, 8, 6] → (7+8+6) / 3 = 7.00 Col 1: [8, 7, 6.5] → (8+7+6.5) / 3 = 7.17 Col 2: [6, 7.5, 7] → (6+7.5+7) / 3 = 6.83 Col 3: [9, 8.5, 7] → (9+8.5+7) / 3 = 8.17 Risultato: [7.00, 7.17, 6.83, 8.17] ← un valore per ogni colonna!

💡 Trucco per ricordare axis:

axis=0 → il risultato ha meno righe (le righe vengono "compresse").

axis=1 → il risultato ha meno colonne (le colonne vengono "compresse").

📊 Funzioni Statistiche: Capire i Dati

Le statistiche servono a riassumere un gruppo di numeri con pochi valori significativi. Invece di guardare 1000 numeri uno per uno, puoi usare queste funzioni per capire subito "come vanno le cose". Vediamole una per una, partendo dalle più semplici.

I dati di esempio

Useremo questi punteggi di un videogioco per tutti gli esempi:

import numpy as np

punteggi = np.array([1200, 1500, 980, 1750, 1320, 1100, 1680, 1050, 1420, 1290])

Minimo, Massimo e Somma

Queste sono le più intuitive:

print("Minimo:", punteggi.min())    # 980 — il punteggio più basso
print("Massimo:", punteggi.max())   # 1750 — il punteggio più alto
print("Somma:", punteggi.sum())     # 13290 — tutti i punteggi sommati

La Media (average/mean)

La media è il valore che otterresti se distribuissi equamente il totale tra tutti. Si calcola sommando tutti i valori e dividendo per quanti sono.

print("Media:", punteggi.mean())  # 1329.0
📝 Come si calcola la media: Somma: 1200 + 1500 + 980 + 1750 + 1320 + 1100 + 1680 + 1050 + 1420 + 1290 = 13290 Quanti: 10 valori Media = 13290 / 10 = 1329.0

💡 Analogia: la Media è come dividere una pizza equamente

Se 10 amici hanno guadagnato in totale 13290 punti e decidessero di redistribuirli equamente, ognuno avrebbe 1329 punti. Questa è la media!

Attenzione: la media è sensibile ai valori estremi. Se un amico avesse 50000 punti, la media salirebbe tantissimo anche se gli altri hanno punteggi bassi. Per questo esiste la mediana.

La Mediana (il valore "di mezzo")

La mediana è il valore che sta esattamente nel mezzo quando ordini tutti i numeri dal più piccolo al più grande. Metà dei valori stanno sotto la mediana, metà stanno sopra.

print("Mediana:", np.median(punteggi))  # 1305.0
📝 Come si calcola la mediana: 1. Ordina i punteggi dal più piccolo al più grande: 980, 1050, 1100, 1200, 1290, 1320, 1420, 1500, 1680, 1750 2. Con 10 valori (numero pari), la mediana è la media dei due centrali: I due centrali sono: 1290 e 1320 Mediana = (1290 + 1320) / 2 = 1305.0

💡 Quando usare la mediana invece della media?

Immagina 5 persone: 4 guadagnano 1000€/mese e 1 guadagna 100.000€/mese.

Media: (4×1000 + 100000) / 5 = 20.800€ — sembra che tutti siano ricchi!

Mediana: 1000€ — molto più rappresentativa della realtà!

La mediana è più affidabile quando ci sono valori estremi che "distorcono" la media.

La Deviazione Standard (quanto i dati sono "sparsi")

La deviazione standard (abbreviata "std" o "dev. std.") ti dice quanto i valori si allontanano dalla media. In altre parole: i dati sono tutti simili tra loro, oppure sono molto diversi?

print("Deviazione standard:", punteggi.std())  # ~244.6

💡 Analogia: Deviazione Standard = quanto sono "sparsi" i dati

Immagina due classi che fanno lo stesso compito:

Classe A — voti: [6, 6, 7, 7, 7, 7, 8, 8]
Media = 7, Dev. Std = 0.7 (piccola) → tutti hanno voti simili, raggruppati attorno al 7.

Classe B — voti: [3, 4, 5, 7, 7, 9, 10, 10]
Media = 6.9, Dev. Std = 2.5 (grande) → i voti sono molto diversi tra loro, c'è chi va malissimo e chi benissimo.

La media è quasi uguale, ma la deviazione standard ti dice che la Classe B ha risultati molto più "sparsi"!

🔍 Come si calcola (semplificato)?

1. Calcoli la media dei dati.

2. Per ogni valore, calcoli quanto è lontano dalla media (la differenza).

3. Elevi al quadrato ogni differenza (per eliminare i segni negativi).

4. Calcoli la media di questi quadrati → questa è la varianza.

5. Fai la radice quadrata della varianza → questa è la deviazione standard.

Non devi calcolarla a mano: punteggi.std() fa tutto per te! L'importante è capire cosa significa il risultato.

La Varianza

La varianza è semplicemente la deviazione standard al quadrato. Misura la stessa cosa (quanto i dati sono sparsi), ma con una scala diversa.

print("Varianza:", punteggi.var())  # ~59781

# Relazione: varianza = (deviazione standard) al quadrato
# E deviazione standard = radice quadrata della varianza
print("Verifica:", punteggi.std() ** 2)  # Uguale alla varianza!

💡 Quando usare varianza vs deviazione standard?

Nella pratica si usa quasi sempre la deviazione standard perché ha la stessa unità di misura dei dati originali (es. se i dati sono in cm, la dev.std è in cm). La varianza è in cm² — meno intuitiva. Ma entrambe dicono la stessa cosa: più sono grandi, più i dati sono "sparsi".

I Percentili (dividere i dati in fasce)

Il percentile ti dice: "sotto quale valore cade una certa percentuale dei dati?". È come mettere tutti i dati in fila dal più piccolo al più grande e poi trovare il punto che separa una certa porzione.

print("25° percentile:", np.percentile(punteggi, 25))  # ~1087.5
print("50° percentile:", np.percentile(punteggi, 50))  # = mediana!
print("75° percentile:", np.percentile(punteggi, 75))  # ~1545.0

💡 Analogia: i Percentili sono come le posizioni in classifica

Immagina 100 studenti ordinati dal voto più basso al più alto:

25° percentile → il voto dello studente in posizione 25. Il 25% degli studenti ha un voto uguale o inferiore a questo.

50° percentile → il voto dello studente in posizione 50 (metà classe). Questo è uguale alla mediana!

75° percentile → il voto dello studente in posizione 75. Solo il 25% degli studenti ha un voto superiore a questo.

Se il tuo voto è al 90° percentile, significa che hai fatto meglio del 90% degli studenti! 🎉

📝 Esempio concreto con i nostri punteggi: Punteggi ordinati: 980, 1050, 1100, 1200, 1290, 1320, 1420, 1500, 1680, 1750 25° percentile ≈ 1087.5 → il 25% dei punteggi (i più bassi) sta sotto 1087.5 50° percentile ≈ 1305.0 → metà dei punteggi sta sotto 1305 (= la mediana!) 75° percentile ≈ 1545.0 → il 75% dei punteggi sta sotto 1545

Trovare la Posizione di Min e Max

print("Posizione del minimo:", punteggi.argmin())  # 2
print("Posizione del massimo:", punteggi.argmax()) # 3

🔍 Spieghiamo: .argmin() e .argmax()

Queste funzioni non restituiscono il valore minimo/massimo, ma la sua posizione (indice) nell'array.

Nell'esempio: punteggi.argmin() restituisce 2 perché il valore più piccolo (980) si trova in posizione 2 (ricorda: si conta da 0!).

Per ottenere il valore dalla posizione: punteggi[punteggi.argmin()]punteggi[2]980.

🔍 Nota: perché alcune funzioni si chiamano diversamente?

Alcune funzioni si chiamano sull'array (col punto): punteggi.min(), punteggi.mean(), punteggi.std()

Altre si chiamano su NumPy passando l'array tra parentesi: np.median(punteggi), np.percentile(punteggi, 25)

Non c'è una logica precisa: semplicemente sono state implementate in modo diverso. L'importante è sapere come usarle!

🔍 Filtri Logici: Selezionare Dati con Condizioni

Questa è una delle funzionalità più potenti (e inizialmente più confuse!) di NumPy. Permette di selezionare solo alcuni elementi di un array in base a una condizione. Andiamo per gradi.

Passo 1: Il confronto crea un array di True/False

Quando confronti un array con un numero (usando >, <, >=, <=, ==), NumPy confronta ogni singolo elemento e crea un nuovo array fatto di True e False:

import numpy as np

temperature = np.array([18, 22, 25, 15, 30, 28, 20, 35, 12, 27])

# Chiediamo: quali temperature sono maggiori di 25?
risultato = temperature > 25
print("temperature > 25 dà:", risultato)
📝 Cosa succede dentro temperature > 25: NumPy prende OGNI elemento e lo confronta con 25: 18 > 25? → False 22 > 25? → False 25 > 25? → False (25 NON è maggiore di 25, è solo uguale) 15 > 25? → False 30 > 25? → True ✓ 28 > 25? → True ✓ 20 > 25? → False 35 > 25? → True ✓ 12 > 25? → False 27 > 25? → True ✓ Risultato: [False, False, False, False, True, True, False, True, False, True]

Questo array di True/False si chiama maschera booleana. Da solo non serve a molto, ma il passo dopo è dove avviene la magia!

Passo 2: Usare la maschera per filtrare

Ora arriva il trucco fondamentale: puoi mettere la maschera dentro le parentesi quadre dell'array originale. NumPy terrà solo gli elementi dove la maschera vale True!

# Modo LUNGO (in due passaggi, per capire bene):
maschera = temperature > 25           # Passo 1: crea la maschera
temp_calde = temperature[maschera]    # Passo 2: filtra con la maschera
print("Temperature calde:", temp_calde)  # [30, 28, 35, 27]

# Modo COMPATTO (tutto in una riga):
temp_calde = temperature[temperature > 25]
print("Temperature calde:", temp_calde)  # [30, 28, 35, 27]

🔍 Spieghiamo pezzo per pezzo: temperature[temperature > 25]

Questa riga sembra strana perché "temperature" compare due volte, ma sono due operazioni distinte:

1. Prima Python esegue ciò che c'è DENTRO le parentesi quadre: temperature > 25. Questo produce la maschera: [False, False, False, False, True, True, False, True, False, True]

2. Poi NumPy usa quella maschera come filtro: temperature[maschera]. Prende dall'array originale SOLO i valori dove la maschera dice True.

Risultato: [30, 28, 35, 27] — solo i 4 valori che superano 25!

🎨 Visualizzazione del filtro

Array originale — i valori verdi passano il filtro (> 25), quelli sbiaditi vengono esclusi:

18
22
25
15
30
28
20
35
12
27

Dopo il filtro rimangono solo:

30
28
35
27

Passo 3: Contare quanti soddisfano la condizione

Ricorda: Python tratta True come 1 e False come 0. Quindi se fai la somma di una maschera, ottieni il conteggio dei True!

n_calde = np.sum(temperature > 25)
print(f"Giorni caldi (>25°): {n_calde}")  # 4

n_fredde = np.sum(temperature <= 20)
print(f"Giorni freddi (≤20°): {n_fredde}")  # 5

🔍 Spieghiamo: perché np.sum(temperature > 25) conta i True?

temperature > 25 produce: [False, False, False, False, True, True, False, True, False, True]

np.sum() fa la somma. Siccome True = 1 e False = 0:

0 + 0 + 0 + 0 + 1 + 1 + 0 + 1 + 0 + 1 = 4

Passo 4: Combinare più condizioni

Puoi combinare condizioni con & (AND = entrambe vere) e | (OR = almeno una vera). Ogni condizione deve stare tra parentesi tonde!

# Temperature tra 20 e 30 (ENTRAMBE le condizioni vere)
miti = temperature[(temperature >= 20) & (temperature <= 30)]
print("Miti (20-30):", miti)  # [22, 25, 30, 28, 20, 27]

# Temperature sotto 15 OPPURE sopra 30
estreme = temperature[(temperature < 15) | (temperature > 30)]
print("Estreme:", estreme)  # [12, 35]

⚠️ Attenzione alle parentesi!

Quando combini condizioni con & o |, ogni condizione deve avere le sue parentesi:

(temperature >= 20) & (temperature <= 30)

temperature >= 20 & temperature <= 30 → Errore!

Passo 5: Sostituire valori con np.where()

np.where() funziona come un "if-else" applicato a tutto l'array contemporaneamente:

# Sintassi: np.where(condizione, valore_se_true, valore_se_false)
# "Se > 30, metti 30. Altrimenti, tieni il valore originale."
temp_limitate = np.where(temperature > 30, 30, temperature)
print("Limitate:", temp_limitate)
📝 Traccia: np.where(temperature > 30, 30, temperature) Per ogni elemento chiede: "è maggiore di 30?" 18 > 30? No → tieni 18 22 > 30? No → tieni 22 25 > 30? No → tieni 25 15 > 30? No → tieni 15 30 > 30? No → tieni 30 (30 non è MAGGIORE di 30) 28 > 30? No → tieni 28 20 > 30? No → tieni 20 35 > 30? Sì → metti 30 (il 35 viene sostituito!) 12 > 30? No → tieni 12 27 > 30? No → tieni 27 Risultato: [18, 22, 25, 15, 30, 28, 20, 30, 12, 27]

Passo 6: Classificare con np.select()

np.select() è come un "if / elif / else" applicato a tutto l'array. Definisci una lista di condizioni e una lista di etichette corrispondenti:

# Classificare le temperature in categorie
condizioni = [
    temperature < 15,                          # Condizione 1
    (temperature >= 15) & (temperature < 25),  # Condizione 2
    temperature >= 25                           # Condizione 3
]
etichette = ["Freddo", "Mite", "Caldo"]       # Un'etichetta per ogni condizione

classificazione = np.select(condizioni, etichette)
print("Classificazione:", classificazione)

🔍 Spieghiamo: np.select(condizioni, etichette)

Per ogni elemento dell'array, NumPy controlla le condizioni in ordine:

1. È < 15? Se sì → "Freddo"

2. È tra 15 e 25? Se sì → "Mite"

3. È >= 25? Se sì → "Caldo"

Esempio: 18° → non è < 15, MA è tra 15 e 25 → "Mite"

Esempio: 35° → non è < 15, non è tra 15 e 25, MA è >= 25 → "Caldo"

✨ Best Practices

1️⃣ Evita i loop: usa le operazioni di NumPy

❌ Lento (con loop)
import numpy as np
array = np.arange(1000000)
result = []
for x in array:
    result.append(x * 2)
result = np.array(result)
✅ Veloce (senza loop)
import numpy as np
array = np.arange(1000000)
result = array * 2
# 100x più veloce! ⚡

2️⃣ Se devi usare un loop, prealloca l'array

❌ Append ripetuti
import numpy as np
result = []
for i in range(1000000):
    result.append(i ** 2)
result = np.array(result)
✅ Array pre-creato
import numpy as np
result = np.zeros(1000000)
for i in range(1000000):
    result[i] = i ** 2

💡 Ma la soluzione MIGLIORE è sempre evitare il loop!

In questo caso: result = np.arange(1000000) ** 2. Una riga, nessun loop, velocità massima!

3️⃣ Usa le funzioni già pronte di NumPy

❌ Ricalcolare a mano
import numpy as np
array = np.array([3, 7, 1, 9, 4])
somma = 0
for x in array:
    somma += x
media = somma / len(array)
✅ Funzione built-in
import numpy as np
array = np.array([3, 7, 1, 9, 4])
media = array.mean()
# Semplice e veloce!

💪 Esercizi Pratici

Esercizio 1: Statistiche Voti Facile

Crea un array con questi 10 voti: np.array([5, 7, 8, 4, 6, 9, 5.5, 7, 8.5, 6])

  • Calcola media, mediana e deviazione standard
  • Quanti voti sono sufficienti (≥ 6)? Usa np.sum()
  • Calcola la percentuale di promossi
  • Trova il voto più alto e più basso e le loro posizioni con argmin() e argmax()

Esercizio 2: Convertitore Temperature Facile

Crea un array di 15 temperature con np.linspace(-10, 40, 15):

  • Convertile in Fahrenheit con una sola operazione: fahrenheit = celsius * 9/5 + 32
  • Filtra le temperature Fahrenheit sopra i 100°F
  • Usa np.where() per sostituire le temperature Celsius negative con 0

Esercizio 3: Analisi Vendite Medio

Crea una matrice 4×3 (4 settimane, 3 prodotti) con vendite inventate:

  • Calcola vendite totali per ogni prodotto con .sum(axis=0)
  • Calcola vendite totali per ogni settimana con .sum(axis=1)
  • Trova la settimana con più vendite

Esercizio 4: Simulatore Dadi Medio

Simula 1000 lanci di due dadi:

  • Crea due array: dado1 = np.random.randint(1, 7, size=1000) e dado2 uguale
  • Calcola la somma: somme = dado1 + dado2
  • Quante volte esce 7? Usa np.sum(somme == 7)
  • Quante volte esce 12?

Esercizio 5: Matrix Challenge Avanzato

Crea due matrici 3×3 con np.random.randint(1, 10, size=(3,3)):

  • Calcola A + B, A * B (elemento per elemento)
  • Trova il massimo di ogni riga con .max(axis=1)
  • Crea una matrice C dove ogni elemento è il maggiore tra A e B: C = np.where(A > B, A, B)

Esercizio 6: Altezze Studenti Avanzato

Genera 100 altezze: altezze = np.random.normal(loc=170, scale=10, size=100)

  • Calcola media, mediana, min, max
  • Quanti studenti sono più alti di 180cm? np.sum(altezze > 180)
  • Quanti sono tra 160cm e 180cm?
  • Calcola i percentili 25, 50, 75
  • Normalizza i dati (z-score): z = (altezze - altezze.mean()) / altezze.std()
  • Identifica gli "outlier" (altezze fuori da ±2 deviazioni standard dalla media)

📝 Riepilogo

Congratulazioni! Hai imparato a usare NumPy! 🎉 Ecco cosa sai fare ora:

🎯 Perché NumPy è importante?

NumPy è la FONDAZIONE di tutto il mondo Data Science e Machine Learning in Python:

  • Pandas (prossimo modulo) è costruito su NumPy
  • Matplotlib usa array NumPy per i grafici
  • Scikit-Learn (ML) usa NumPy per tutto
  • TensorFlow/PyTorch (Deep Learning) sono ispirati a NumPy

🎯 Prossima Lezione: Matplotlib

Nel prossimo modulo imparerai a creare grafici professionali per visualizzare i tuoi dati! 📊✨