✦ Classe Terza · Modulo 3.4 · Grand Finale

NumPy per l'Intelligenza Artificiale

Preparare dati, normalizzare valori, calcolare distanze, lavorare con le immagini

Il tuo percorso NumPy

4
Fondamenta Statistiche Filtri & 2D Per l'AI ← sei qui
🎯

Cosa farai in questo modulo

Negli ultimi tre moduli hai imparato NumPy in modo teorico. Ora vediamo a cosa serve davvero quando si costruisce un programma di Intelligenza Artificiale.

Le aziende che fanno AI (Netflix, Spotify, Google…) usano esattamente queste tecniche ogni giorno. Imparerai:

1
Pulire i dati

Come eliminare i valori mancanti e i dati "strani" che farebbero sbagliare l'AI

2
Normalizzare i dati

Come mettere tutto sulla stessa scala, così l'AI tratta tutti i dati in modo equo

3
Calcolare distanze

Il concetto centrale di tantissimi algoritmi AI: "quanto sono simili due cose?"

4
Lavorare con le immagini

Come una fotografia è in realtà solo una grande tabella di numeri

💡 Analogia — Pensa a uno chef

Prima di cucinare un piatto, uno chef deve preparare gli ingredienti: lavarli, tagliarli alla stessa dimensione, pesarli. Solo dopo li mette in pentola.

Per l'AI è uguale: prima prepari i dati con NumPy, poi li "cucini" con un modello. Senza la preparazione, il piatto viene male!

1

Preparare i Dati per l'AI

🤔 Perché i dati vanno "preparati"?

Nel mondo reale i dati sono sempre disordinati. Ci sono valori mancanti, errori di inserimento, dati fuori scala.

Se dai dati disordinati a un modello AI, le sue previsioni saranno sbagliate — un po' come se ti allenassi per una gara usando informazioni false. L'AI impara dai dati, quindi i dati devono essere corretti!

Problema 1 — Valori mancanti (NaN)

In NumPy un valore mancante si chiama NaN (Not a Number = "non è un numero"). Succede quando qualcuno non ha risposto a una domanda di un questionario, o quando un sensore non ha registrato nulla.

Perché è un problema? Prova a fare una media con un NaN dentro:

Cosa succede con i NaN
1.5
2.3
NaN
4.1
NaN
5.7

I valori in rosso sono NaN. Se calcoli la media di questo array, NumPy restituisce nan — un risultato inutilizzabile!

La soluzione: trovare dove si trovano i NaN e rimuoverli prima di usare i dati.

pythonrimozione_nan.py
import numpy as np

# Dati con alcuni valori mancanti
dati = np.array([1.5, 2.3, np.nan, 4.1, np.nan, 5.7])

# np.isnan() restituisce True dove c'è un NaN, False altrove
print(np.isnan(dati))
# [False, False, True, False, True, False]

# Il simbolo ~ significa "al contrario" → True diventa False e viceversa
maschera_validi = ~np.isnan(dati)
print(maschera_validi)
# [True, True, False, True, False, True]

# Usiamo la maschera per tenere solo i valori validi
dati_puliti = dati[maschera_validi]
print("Dati puliti:", dati_puliti)
# Dati puliti: [1.5, 2.3, 4.1, 5.7]

# Ora la media funziona!
print("Media:", dati_puliti.mean())  # 3.4
📝 Traccia passo-passo: come lavora il codice dati = [1.5, 2.3, nan, 4.1, nan, 5.7] np.isnan(dati) → [F, F, T, F, T, F] ~ rovescia tutto: [T, T, F, T, F, T] ↑ tiene ↑ tiene ↑ scarta ↑ tiene ↑ scarta ↑ tiene Risultato: [1.5, 2.3, 4.1, 5.7] ✓

Problema 2 — Valori estremi (outlier)

Un outlier è un valore così fuori scala da essere probabilmente un errore — o comunque da rovinare le analisi. Esempio classico: stipendi in un'azienda dove tutti guadagnano tra 25.000 e 40.000 €, tranne il CEO che guadagna 500.000 €.

⚠️ Perché gli outlier sono pericolosi per l'AI?

Se vuoi prevedere lo stipendio medio dei dipendenti, un valore di 500.000 € "trascina" la media verso l'alto in modo falso. Il modello AI crederà che gli stipendi siano enormi e farà previsioni sbagliate per tutti.

pythonoutlier.py
import numpy as np

# Stipendi di 10 dipendenti (in migliaia di euro)
stipendi = np.array([25, 28, 30, 32, 35, 28, 31, 29, 500, 27])
#                                                         ^^^
#                                                 questo è il CEO = outlier!

# Confrontiamo media e mediana per capire il problema
print(f"Media:   {stipendi.mean():.1f}k")   # 76.5k — distorta dall'outlier!
print(f"Mediana: {np.median(stipendi):.1f}k")  # 29.5k — molto più realistica

# Come identificare gli outlier con la "regola dei 3 sigma"
# Un valore è sospetto se è più di 3 deviazioni standard dalla media
media = stipendi.mean()
deviazione = stipendi.std()

limite_alto = media + 3 * deviazione
limite_basso = media - 3 * deviazione

print(f"\nLimite superiore accettabile: {limite_alto:.1f}k")
print(f"Limiti inferiore accettabile:  {limite_basso:.1f}k")

# Teniamo solo i valori DENTRO i limiti
dati_sani = stipendi[(stipendi >= limite_basso) & (stipendi <= limite_alto)]
print(f"\nDati sani: {dati_sani}")
print(f"Media corretta: {dati_sani.mean():.1f}k")  # 28.9k — ha senso ora!
💡 Regola pratica da ricordare

La mediana (che hai visto nel Modulo 3.2) è molto più robusta degli outlier rispetto alla media. Quando i dati hanno outlier, usa sempre la mediana per descriverli!

2

Normalizzazione: la Stessa Scala per Tutti

🤔 Perché normalizzare? Il problema delle scale diverse

Immagina di voler descrivere una persona con tre numeri:

Età: 17 anni

Stipendio: 25.000 €

Voto di maturità: 85/100

Se dai questi numeri direttamente a un modello AI, lui "pensa" che lo stipendio (25.000) sia molto più importante dell'età (17) solo perché è un numero più grande. Non è un errore di logica — è solo che i numeri hanno scale completamente diverse.

La normalizzazione risolve questo: trasforma tutto in numeri confrontabili, così l'AI tratta ogni informazione in modo equo.

Tecnica 1 — Normalizzazione Min-Max (porta tutto tra 0 e 1)

Questa tecnica trasforma ogni valore in un numero tra 0 e 1, dove 0 è il valore più piccolo dell'array e 1 è il più grande.

La formula:

Formula Min-Max
valore_normalizzato = (valore - minimo) / (massimo - minimo)

Sottraiamo il minimo per far iniziare da 0, poi dividiamo per l'intervallo totale

pythonmin_max.py
import numpy as np

altezze = np.array([150, 165, 180, 175, 160])  # in centimetri

# La formula Min-Max in NumPy:
altezze_norm = (altezze - altezze.min()) / (altezze.max() - altezze.min())

print("Altezze originali:", altezze)
print("Altezze normalizzate:", altezze_norm.round(2))
#                               [0.  , 0.5 , 1.  , 0.83, 0.33]
#                                 ↑                   ↑
#                           il più basso          il più alto
📝 Traccia passo-passo: altezze [150, 165, 180, 175, 160] min = 150, max = 180, intervallo = 180 - 150 = 30 150 → (150 - 150) / 30 = 0 / 30 = 0.00 ← il più basso 165 → (165 - 150) / 30 = 15 / 30 = 0.50 ← a metà 180 → (180 - 150) / 30 = 30 / 30 = 1.00 ← il più alto 175 → (175 - 150) / 30 = 25 / 30 = 0.83 160 → (160 - 150) / 30 = 10 / 30 = 0.33 Risultato: [0.00, 0.50, 1.00, 0.83, 0.33] ← tutti tra 0 e 1 ✓

Tecnica 2 — Standardizzazione (Z-score)

Questa tecnica trasforma i dati in modo che abbiano media = 0 e deviazione standard = 1. I valori possono essere sia positivi che negativi. Un valore positivo significa "sopra la media", uno negativo "sotto la media".

La formula:

Formula Standardizzazione (Z-score)
valore_standardizzato = (valore - media) / deviazione_standard

Sottraiamo la media per centrare in zero, poi dividiamo per "quanto variano i dati"

pythonstandardizzazione.py
import numpy as np

voti = np.array([5, 7, 8, 4, 6, 9, 5.5, 7, 8.5, 6])

# La formula Z-score in NumPy:
voti_std = (voti - voti.mean()) / voti.std()

print(f"Media originale: {voti.mean():.2f}")       # 6.60
print(f"Voti standardizzati: {voti_std.round(2)}")

print(f"\nVerifica — dopo la standardizzazione:")
print(f"  Media:           {voti_std.mean():.2f}")  # ~0.00 ← quasi zero!
print(f"  Dev. standard:   {voti_std.std():.2f}")   # ~1.00 ← quasi uno!
💡 Cosa significano i valori standardizzati?

Il valore standardizzato ti dice di quante "deviazioni standard" sei lontano dalla media del gruppo.

z = 0 → sei esattamente nella media

z = +1.5 → sei 1.5 deviazioni sopra la media (abbastanza bravo)

z = -2.0 → sei 2 deviazioni sotto la media (molto sotto)

Questo concetto lo usi anche nelle valutazioni scolastiche e nei test standardizzati come il QI!

Quale tecnica usare?

Min-Max (0 → 1)

Usala quando conosci i limiti naturali del dato: voti da 0 a 10, percentuali da 0 a 100, pixel da 0 a 255.

Standardizzazione

Usala quando non conosci i limiti o quando i dati hanno outlier. È la scelta più comune nell'AI. Se sei in dubbio, scegli questa.

3

Calcolare Distanze: "Quanto Siamo Simili?"

🤔 Perché le distanze sono fondamentali per l'AI?

Molti algoritmi AI funzionano su un'idea semplicissima: cose simili sono vicine nello spazio.

Netflix ti consiglia film che "gente simile a te" ha amato. Spotify ti suggerisce canzoni "vicine" al tuo gusto. Gmail manda le email nella cartella giusta perché le email spam si "assomigliano" tra loro.

In tutti questi casi, il programma deve calcolare "quanto sono distanti questi due punti?". E in NumPy, è facilissimo.

La distanza euclidea

È la distanza in linea retta tra due punti — la stessa che misuri con un righello. In 2D è il teorema di Pitagora che forse conosci già:

Distanza euclidea in 2D
A(2,3) B(5,7) Δx = 3 Δy = 4 d = 5

d = √(3² + 4²) = √(9 + 16) = √25 = 5

pythondistanza.py
import numpy as np

punto_A = np.array([2, 3])
punto_B = np.array([5, 7])

# Metodo 1: calcolo manuale (formula di Pitagora estesa)
differenza = punto_B - punto_A       # [3, 4]
quadrati   = differenza ** 2         # [9, 16]
somma      = np.sum(quadrati)        # 25
distanza   = np.sqrt(somma)          # 5.0

print(f"Distanza: {distanza}")       # 5.0

# Metodo 2: formula compatta con np.linalg.norm()
# (fa la stessa cosa in una riga sola!)
distanza2 = np.linalg.norm(punto_B - punto_A)
print(f"Distanza: {distanza2}")      # 5.0 — stesso risultato!
💡 Cosa significa np.linalg.norm()?

linalg sta per "linear algebra" (algebra lineare). norm è il termine tecnico per "lunghezza di un vettore" — che è esattamente la distanza dal punto di origine. Usalo sempre nelle tue soluzioni: è più breve e più leggibile.

Trovare la distanza tra più punti

Nella pratica si lavora sempre con molti punti. Ecco come calcolare le distanze tra tutti gli studenti di una classe, usando le loro ore di studio e le ore di sonno come coordinate:

pythonmatrice_distanze.py
import numpy as np

# Ogni riga = uno studente
# Colonne = [ore_di_studio, ore_di_sonno]
studenti = np.array([
    [5, 8],   # Studente 0
    [7, 6],   # Studente 1
    [3, 9],   # Studente 2
    [6, 7],   # Studente 3
])

n = len(studenti)

# Creiamo una tabella (matrice) 4x4 per tutte le distanze
distanze = np.zeros((n, n))

for i in range(n):
    for j in range(n):
        distanze[i, j] = np.linalg.norm(studenti[i] - studenti[j])

print("Tabella distanze tra studenti:")
print(distanze.round(2))

# Esempio: chi è più simile allo studente 0?
distanze_da_0 = distanze[0]  # Riga 0 = distanze dallo studente 0
print(f"\nDistanze dallo studente 0: {distanze_da_0.round(2)}")
# Il valore minimo (escludendo 0.00 = sé stesso) è il più simile!
📝 Come leggere la matrice delle distanze Std.0 Std.1 Std.2 Std.3 Std.0 [ 0.00, 2.83, 2.83, 1.41 ] Std.1 [ 2.83, 0.00, 4.47, 1.41 ] Std.2 [ 2.83, 4.47, 0.00, 3.16 ] Std.3 [ 1.41, 1.41, 3.16, 0.00 ] Diagonale = 0: ogni studente ha distanza 0 da sé stesso ✓ Riga 0: lo studente 0 è più vicino allo studente 3 (dist = 1.41) → Studente 0 e Studente 3 hanno stili di vita simili!
4

Le Immagini sono Matrici di Numeri

💡 La rivelazione: cos'è davvero un'immagine?

Una foto digitale non è "disegnata" — è una grandissima tabella di numeri. Ogni numero rappresenta la luminosità di un puntino (pixel) della foto.

• Una foto in bianco e nero: ogni cella ha un numero da 0 (nero totale) a 255 (bianco totale)

• Una foto a colori: ogni pixel ha tre numeri — uno per il rosso, uno per il verde, uno per il blu (RGB)

Quando un'AI "guarda" una foto, non vede immagini — vede solo una matrice NumPy piena di numeri!

Un'immagine 5×5 in bianco e nero

Ecco un'immagine piccola (5×5 pixel) e la sua rappresentazione come matrice NumPy:

pythonimmagine.py
import numpy as np

# Ogni numero = luminosità di un pixel (0 = nero, 255 = bianco)
immagine = np.array([
    [  0,  64, 128, 192, 255],  # Riga 0: da nero a bianco
    [255, 192, 128,  64,   0],  # Riga 1: da bianco a nero
    [  0,   0, 255,   0,   0],  # Riga 2: punto bianco al centro
    [128, 128, 128, 128, 128],  # Riga 3: grigio uniforme
    [255, 255, 255, 255, 255],  # Riga 4: bianco puro
])

print(f"Dimensioni immagine: {immagine.shape}")  # (5, 5)
print(f"Pixel in totale: {immagine.size}")       # 25
print(f"Tipo dati: {immagine.dtype}")            # int64
Come appare visivamente questa matrice
0
64
128
192
255
255
192
128
64
0
0
0
255
0
0
128
128
128
128
128
255
255
255
255
255

Passa il mouse sui pixel per vedere i loro valori numerici

Modificare un'immagine = modificare una matrice

pythonmodifica_immagine.py
import numpy as np

immagine = np.array([
    [  0,  64, 128, 192, 255],
    [255, 192, 128,  64,   0],
    [  0,   0, 255,   0,   0],
    [128, 128, 128, 128, 128],
    [255, 255, 255, 255, 255],
])

# --- Operazione 1: scurire l'immagine ---
# Dividiamo ogni pixel per 2 → tutti i valori si dimezzano → più scuro
immagine_scura = immagine // 2  # // = divisione intera (senza decimali)
print("Pixel scuriti (prima riga):", immagine_scura[0])
# [  0,  32,  64,  96, 127] ← valori dimezzati

# --- Operazione 2: ritagliare una parte dell'immagine ---
# Prendiamo solo le prime 3 righe e le prime 3 colonne
ritaglio = immagine[:3, :3]
print("\nRitaglio 3x3:")
print(ritaglio)

# --- Operazione 3: ruotare l'immagine di 90° ---
ruotata = np.rot90(immagine)
print("\nImmagine ruotata 90°:")
print(ruotata)
💡 Tutto ciò che fa Instagram, fa anche NumPy

I filtri di Instagram sono solo operazioni matematiche su matrici NumPy:

Filtro luminosità: aggiungi un numero fisso a tutti i pixel

Filtro contrasto: moltiplica tutti i pixel per un coefficiente

Bianco e nero: fai la media dei 3 canali RGB

Ritaglio: usa lo slicing che hai già imparato nel Modulo 3.3!

🚀 Progetto Finale

Costruisci un Sistema di Raccomandazione

Questo è esattamente come funzionano Netflix, Spotify e Amazon Prime.

📊 Il Dataset

Hai 5 utenti che hanno votato 4 film da 1 a 5 stelle:

python
import numpy as np

# Colonne: [Avengers, Titanic, Harry Potter, Star Wars]
utenti = np.array([
    [5, 1, 4, 2],  # Utente 0: ama azione, odia romantico
    [1, 5, 2, 1],  # Utente 1: ama i film romantici
    [4, 2, 5, 3],  # Utente 2: ama il fantasy
    [2, 4, 3, 5],  # Utente 3: ama la fantascienza
    [3, 3, 3, 3],  # Utente 4: vota tutto uguale
])

La tua missione in 4 passi:

  1. Normalizza i voti con la standardizzazione
  2. Calcola la distanza tra l'Utente 0 e tutti gli altri
  3. Trova i 2 utenti più simili all'Utente 0
  4. Identifica quale film quei due utenti amano (≥4 stelle) ma che l'Utente 0 ha votato poco
pythonsuggerimento.py — soluzione guidata
import numpy as np

utenti = np.array([
    [5, 1, 4, 2],
    [1, 5, 2, 1],
    [4, 2, 5, 3],
    [2, 4, 3, 5],
    [3, 3, 3, 3],
])

# Passo 1: normalizza con standardizzazione (asse 0 = per colonna)
utenti_norm = (utenti - utenti.mean(axis=0)) / utenti.std(axis=0)

# Passo 2: calcola le distanze dall'Utente 0
distanze = np.array([
    np.linalg.norm(utenti_norm[0] - utenti_norm[i])
    for i in range(len(utenti))
])

print("Distanze dall'Utente 0:", distanze.round(2))

# Passo 3: ordina per distanza crescente, salta l'indice 0 (sé stesso)
indici_simili = np.argsort(distanze)[1:3]  # I 2 più vicini
print("Utenti più simili:", indici_simili)

# Passo 4: cosa amano questi utenti?
print("\nVoti degli utenti simili:")
print(utenti[indici_simili])

# Suggerisci il film che loro amano (>= 4) ma l'utente 0 vota poco (< 3)
film = ["Avengers", "Titanic", "Harry Potter", "Star Wars"]
for idx in indici_simili:
    for f_idx, f_nome in enumerate(film):
        if utenti[idx, f_idx] >= 4 and utenti[0, f_idx] < 3:
            print(f"\n🎬 Suggerimento: '{f_nome}'")

🎁 Bonus Challenge

Trasforma il codice sopra in una funzione suggerisci_film(utente_id) che funziona per qualsiasi utente, non solo lo 0. Provala con tutti e 5 gli utenti e vedi che suggerimenti produce!

  • L'utente 1 cosa dovrebbe vedere?
  • E l'utente 4 (quello neutrale)?
  • Qualcuno non riceve suggerimenti? Perché?
🏋️ Esercizi Pratici

Allenati con contesti reali

Tre livelli di difficoltà — inizia dall'inizio e sali gradualmente. Ogni esercizio ha un suggerimento nascosto se sei bloccato, e la soluzione completa da confrontare con la tua.

● Livello 1 — Warm-up ● Livello 2 — Intermedio ● Livello 3 — Sfida
Livello 1 — Warm-up
Es. 1.1
🏃 Sport & Fitness Pulizia dati · NaN

App di corsa con dati mancanti

Una app di running registra la velocità media (km/h) di un atleta in 8 allenamenti. In 2 sessioni il GPS ha avuto problemi e non ha registrato nulla → NaN.

python— il tuo punto di partenza
import numpy as np

velocita = np.array([12.3, 11.8, np.nan, 13.1, 12.7, np.nan, 11.5, 14.0])

# DOMANDE:
# 1. Quante sessioni hanno dati mancanti?
# 2. Rimuovi i NaN e salva i dati validi in "velocita_pulita"
# 3. Calcola la velocità media dell'atleta (solo sui dati validi)
# 4. Quante sessioni aveva velocità sopra la media?
💡 Suggerimento (clicca per vedere)

• Per contare i NaN: np.sum(np.isnan(velocita))

• Per filtrare: usa ~np.isnan() come maschera booleana

• Per contare sopra la media: usa un filtro con velocita_pulita > media

✅ Soluzione completa (clicca dopo aver provato!)
pythonsoluzione
import numpy as np

velocita = np.array([12.3, 11.8, np.nan, 13.1, 12.7, np.nan, 11.5, 14.0])

# 1. Quante sessioni mancanti?
n_mancanti = np.sum(np.isnan(velocita))
print(f"Sessioni con dati mancanti: {n_mancanti}")  # 2

# 2. Rimozione NaN
velocita_pulita = velocita[~np.isnan(velocita)]
print(f"Velocità valide: {velocita_pulita}")
# [12.3, 11.8, 13.1, 12.7, 11.5, 14.0]

# 3. Velocità media
media = velocita_pulita.mean()
print(f"Velocità media: {media:.2f} km/h")  # 12.57 km/h

# 4. Sessioni sopra la media
sopra_media = velocita_pulita[velocita_pulita > media]
print(f"Sessioni sopra la media: {len(sopra_media)}")  # 3
print(f"Velocità di quelle sessioni: {sopra_media}")
Es. 1.2
🎵 Musica & Streaming Normalizzazione Min-Max

Normalizzare le caratteristiche di una canzone

Spotify descrive ogni canzone con delle caratteristiche numeriche. Il problema è che sono su scale completamente diverse: prima di passarle a un algoritmo AI devi normalizzarle tutte tra 0 e 1.

python— il tuo punto di partenza
import numpy as np

# Caratteristiche di 5 canzoni
# Colonne: [BPM (battiti/min), durata (secondi), loudness (dB), popolarita (0-100)]
canzoni = np.array([
    [128, 210, -5,  87],   # canzone 0
    [ 95, 195, -8,  62],   # canzone 1
    [140, 240, -3,  95],   # canzone 2
    [ 75, 180, -12, 41],   # canzone 3
    [110, 225, -6,  78],   # canzone 4
])

# DOMANDE:
# 1. Normalizza OGNI COLONNA con Min-Max (axis=0 lavora colonna per colonna)
#    Formula: (valore - min_colonna) / (max_colonna - min_colonna)
# 2. Dopo la normalizzazione, qual è il valore massimo e minimo in ogni colonna?
#    (dovrebbero essere tutti 1.0 e 0.0)
# 3. Quale canzone ha il BPM più alto in assoluto? (indice nella colonna 0)
💡 Suggerimento

• Quando scrivi canzoni.min(axis=0), NumPy calcola il minimo di ogni colonna separatamente → restituisce un array di 4 valori (uno per colonna)

• Poi canzoni - canzoni.min(axis=0) sottrae automaticamente il minimo giusto a ogni colonna (broadcasting!)

• Per trovare l'indice del massimo in una colonna: np.argmax(canzoni_norm[:, 0])

✅ Soluzione completa
pythonsoluzione
import numpy as np

canzoni = np.array([
    [128, 210, -5,  87],
    [ 95, 195, -8,  62],
    [140, 240, -3,  95],
    [ 75, 180, -12, 41],
    [110, 225, -6,  78],
])

# 1. Normalizzazione Min-Max colonna per colonna
minimi = canzoni.min(axis=0)   # [75, 180, -12, 41]
massimi = canzoni.max(axis=0)  # [140, 240, -3, 95]
canzoni_norm = (canzoni - minimi) / (massimi - minimi)

print("Canzoni normalizzate:")
print(canzoni_norm.round(3))

# 2. Verifica: ogni colonna va da 0.0 a 1.0
print("\nMinimi per colonna:", canzoni_norm.min(axis=0))  # [0. 0. 0. 0.]
print("Massimi per colonna:", canzoni_norm.max(axis=0))  # [1. 1. 1. 1.]

# 3. Canzone con BPM più alto
idx_bpm_max = np.argmax(canzoni_norm[:, 0])
print(f"\nCanzone con BPM più alto: canzone {idx_bpm_max}")  # canzone 2 (140 BPM)
Livello 2 — Intermedio
Es. 2.1
🏥 Salute & Medicina Outlier · Standardizzazione

Analisi pressione sanguigna in un reparto ospedaliero

Un ospedale registra la pressione sistolica (la "prima" delle due misure, es. 120 in "120/80") di 12 pazienti. Alcuni valori potrebbero essere errori di inserimento dati. Prima di usarli per un'analisi AI, devi pulire e standardizzare i dati.

python— il tuo punto di partenza
import numpy as np

# Pressione sistolica in mmHg di 12 pazienti
pressione = np.array([118, 125, 132, 119, 450, 128, 121,
                       135, 127, 122, 12, 130])
#                            ^^^              ^^
#                    450 = errore inserimento  12 = impossibile (troppo basso)

# DOMANDE:
# 1. Calcola media e mediana — perché sono molto diverse?
# 2. Identifica i valori "impossibili": una pressione normale è tra 80 e 200 mmHg.
#    Filtra i dati tenendo solo i valori nel range normale.
# 3. Standardizza (Z-score) i dati puliti
# 4. Quale paziente ha la pressione più alta rispetto al gruppo?
#    (cerca il Z-score massimo nei dati standardizzati)
💡 Suggerimento

• Per filtrare nel range usa due condizioni unite con &: pressione[(pressione >= 80) & (pressione <= 200)]

• La standardizzazione è: (dati - dati.mean()) / dati.std()

np.argmax(array) restituisce l'indice del valore più grande

✅ Soluzione completa
pythonsoluzione
import numpy as np

pressione = np.array([118, 125, 132, 119, 450, 128, 121,
                       135, 127, 122, 12, 130])

# 1. Media vs mediana
print(f"Media:   {pressione.mean():.1f} mmHg")    # 139.1 — distorta dagli outlier
print(f"Mediana: {np.median(pressione):.1f} mmHg") # 126.5 — molto più realistica
print("→ Sono diverse perché 450 e 12 distorcono la media!\n")

# 2. Filtrare nel range fisiologico
pressione_pulita = pressione[(pressione >= 80) & (pressione <= 200)]
print(f"Dati rimossi: {len(pressione) - len(pressione_pulita)}")  # 2
print(f"Pressioni valide: {pressione_pulita}")

# 3. Standardizzazione
media = pressione_pulita.mean()
std = pressione_pulita.std()
pressione_std = (pressione_pulita - media) / std
print(f"\nPressioni standardizzate: {pressione_std.round(2)}")
print(f"Verifica media: {pressione_std.mean():.4f}")  # ~0.0000
print(f"Verifica std:   {pressione_std.std():.4f}")   # ~1.0000

# 4. Paziente con pressione più alta nel gruppo
idx_max = np.argmax(pressione_std)
print(f"\nPaziente {idx_max} ha la pressione più alta nel gruppo")
print(f"Z-score: {pressione_std[idx_max]:.2f} → {pressione_std[idx_max]:.1f} dev.std sopra la media")
Es. 2.2
🛒 E-commerce Distanze · Prodotti simili

Trovare prodotti simili su uno shop online

Un sito e-commerce descrive ogni smartphone con 3 caratteristiche: prezzo (€), RAM (GB) e punteggio fotocamera (da 1 a 10). Vuoi costruire una funzione "prodotti simili" — la sezione che appare in fondo alle pagine prodotto.

python— il tuo punto di partenza
import numpy as np

# Ogni riga = uno smartphone
# Colonne: [prezzo €, RAM GB, voto_fotocamera]
smartphone = np.array([
    [799,  8, 8.5],   # iPhone SE
    [1299, 12, 9.2],  # iPhone 15 Pro
    [749,  8, 7.8],   # Samsung A55
    [1199, 12, 8.9],  # Samsung S24
    [699,  6, 7.2],   # Pixel 7a
    [1099, 12, 9.0],  # Pixel 8 Pro
])
nomi = ["iPhone SE", "iPhone 15 Pro", "Samsung A55",
        "Samsung S24", "Pixel 7a", "Pixel 8 Pro"]

# DOMANDE:
# 1. Prima di calcolare le distanze, normalizza con Min-Max (le scale sono molto diverse:
#    il prezzo va fino a 1299, la RAM fino a 12, il voto fino a 10!)
# 2. Calcola la distanza tra TUTTI i prodotti (matrice 6x6)
# 3. Per ogni smartphone, stampa quale altro smartphone gli è più simile
#    (escludi sé stesso cercando il secondo minimo, non il primo!)
💡 Suggerimento

• È fondamentale normalizzare prima di calcolare le distanze: senza normalizzare, il prezzo (fino a 1299) dominerebbe completamente RAM (max 12) e voto (max 10)

np.argsort(riga)[1] → ordina gli indici per distanza crescente e prende il secondo (indice 1), cioè il più vicino escludendo sé stesso (indice 0)

✅ Soluzione completa
pythonsoluzione
import numpy as np

smartphone = np.array([
    [799,  8, 8.5],
    [1299, 12, 9.2],
    [749,  8, 7.8],
    [1199, 12, 8.9],
    [699,  6, 7.2],
    [1099, 12, 9.0],
])
nomi = ["iPhone SE", "iPhone 15 Pro", "Samsung A55",
        "Samsung S24", "Pixel 7a", "Pixel 8 Pro"]

# 1. Normalizzazione Min-Max (essenziale prima delle distanze!)
minimi = smartphone.min(axis=0)
massimi = smartphone.max(axis=0)
smart_norm = (smartphone - minimi) / (massimi - minimi)

# 2. Matrice distanze 6x6
n = len(smartphone)
distanze = np.zeros((n, n))
for i in range(n):
    for j in range(n):
        distanze[i, j] = np.linalg.norm(smart_norm[i] - smart_norm[j])

print("Matrice distanze (arrotondata):")
print(distanze.round(2))

# 3. Per ogni prodotto, trova il più simile
print("\n--- Prodotti simili ---")
for i in range(n):
    indici_ordinati = np.argsort(distanze[i])
    # indici_ordinati[0] = sé stesso (dist 0), indici_ordinati[1] = più vicino
    piu_simile = indici_ordinati[1]
    dist = distanze[i, piu_simile]
    print(f"{nomi[i]:15s} → più simile a: {nomi[piu_simile]:15s} (dist={dist:.2f})")
Livello 3 — Sfida
Es. 3.1
📱 Social Media Pipeline completa · Tutto insieme

Analisi profili Instagram: pulisci, normalizza, confronta

Stai costruendo un tool per analizzare profili Instagram e trovare creator simili tra loro. Hai dati grezzi con valori mancanti, outlier e scale diverse. Devi costruire una pipeline completa: pulizia → normalizzazione → calcolo somiglianza.

python— il tuo punto di partenza
import numpy as np

# Dati di 8 creator (colonne: follower, post_per_settimana, engagement_rate %)
# Alcuni valori sono NaN (profili privati o non tracciabili)
dati_grezzi = np.array([
    [15000,    4,   3.2],
    [82000,    7,   5.1],
    [  np.nan, 3,   2.8],   # follower non tracciabili
    [210000,   2,   4.7],
    [9500000,  1,   0.3],   # influencer con 9.5M follower = possibile outlier
    [44000,    5,   np.nan], # engagement non tracciabile
    [67000,    6,   4.2],
    [31000,    3,   3.8],
])
nomi = ["Alice", "Bob", "Carla", "Diego",
        "Elena", "Fabio", "Giulia", "Hana"]

# MISSIONE — costruisci una pipeline in 4 fasi:
#
# FASE 1 — Identifica le righe con almeno un NaN e rimuovile.
#   Usa np.any(np.isnan(dati_grezzi), axis=1) per trovare le righe con NaN
#   (axis=1 = controlla lungo le colonne, per ogni riga)
#
# FASE 2 — Identifica gli outlier nella colonna dei follower.
#   Un valore è outlier se supera media + 2 * deviazione_standard
#   Rimuovi anche quelle righe.
#
# FASE 3 — Normalizza i dati puliti con Min-Max.
#
# FASE 4 — Calcola la matrice delle distanze e stampa,
#   per ogni creator rimasto, chi gli è più simile.
💡 Suggerimento

Fase 1: np.any(np.isnan(dati), axis=1) restituisce True per le righe che contengono almeno un NaN. Con ~ puoi invertire e tenere solo le righe senza NaN.

Fase 2: dopo aver rimosso i NaN, lavora sulla colonna 0 (follower): calcola media e std, poi filtra le righe dove la colonna 0 è entro i limiti.

Ricorda: quando filtri le righe dei dati, filtra anche la lista nomi con lo stesso array booleano, così rimangono allineati!

✅ Soluzione completa
pythonsoluzione
import numpy as np

dati_grezzi = np.array([
    [15000,    4,   3.2],
    [82000,    7,   5.1],
    [  np.nan, 3,   2.8],
    [210000,   2,   4.7],
    [9500000,  1,   0.3],
    [44000,    5,   np.nan],
    [67000,    6,   4.2],
    [31000,    3,   3.8],
])
nomi = np.array(["Alice", "Bob", "Carla", "Diego",
                  "Elena", "Fabio", "Giulia", "Hana"])

# FASE 1 — Rimuovi righe con NaN
# np.any(..., axis=1) → True se almeno un valore nella riga è NaN
mask_nan = ~np.any(np.isnan(dati_grezzi), axis=1)
dati = dati_grezzi[mask_nan]
nomi = nomi[mask_nan]
print(f"Fase 1 — Rimossi {np.sum(~mask_nan)} profili con dati mancanti")
print(f"Rimasti: {list(nomi)}\n")

# FASE 2 — Rimuovi outlier nei follower (colonna 0)
follower = dati[:, 0]
media_f = follower.mean()
std_f   = follower.std()
limite  = media_f + 2 * std_f
print(f"Fase 2 — Limite follower: {limite:,.0f}")
mask_out = follower <= limite
dati = dati[mask_out]
nomi = nomi[mask_out]
print(f"Rimossi outlier: {np.sum(~mask_out)}")
print(f"Rimasti: {list(nomi)}\n")

# FASE 3 — Normalizzazione Min-Max
minimi = dati.min(axis=0)
massimi = dati.max(axis=0)
dati_norm = (dati - minimi) / (massimi - minimi)
print("Fase 3 — Dati normalizzati:")
print(dati_norm.round(3))

# FASE 4 — Matrice distanze + creator simili
n = len(dati_norm)
distanze = np.zeros((n, n))
for i in range(n):
    for j in range(n):
        distanze[i, j] = np.linalg.norm(dati_norm[i] - dati_norm[j])

print("\nFase 4 — Creator più simili:")
for i in range(n):
    simile_idx = np.argsort(distanze[i])[1]
    print(f"  {nomi[i]:8s} → simile a {nomi[simile_idx]} (dist={distanze[i,simile_idx]:.2f})")
Es. 3.2
🎮 Gaming Immagini · Elaborazione pixel

Effetti grafici su uno sprite di gioco

Lavori come programmatore in un piccolo studio di videogiochi. Hai uno "sprite" (immagine del personaggio) rappresentato come una matrice 6×6 di pixel in scala di grigi. Devi applicare tre effetti grafici diversi per creare varianti del personaggio.

python— il tuo punto di partenza
import numpy as np

# Sprite del personaggio (6x6 pixel, valori 0-255)
sprite = np.array([
    [  0,   0, 200, 200,   0,   0],
    [  0, 200, 255, 255, 200,   0],
    [200, 255, 180, 180, 255, 200],
    [200, 255, 180, 180, 255, 200],
    [  0, 200, 255, 255, 200,   0],
    [  0,   0, 200, 200,   0,   0],
])

# EFFETTI DA CREARE:
#
# EFFETTO 1 — "Versione notturna" (sprite scuro)
#   Riduci la luminosità al 35% moltiplicando per 0.35
#   Usa .astype(int) per riportare i valori a interi
#
# EFFETTO 2 — "Versione fantasma" (sprite semitrasparente)
#   Clip i valori tra 80 e 200 con np.clip(sprite, 80, 200)
#   (np.clip taglia i valori fuori dal range: sotto 80 → 80, sopra 200 → 200)
#
# EFFETTO 3 — "Flip orizzontale" (sprite specchiato)
#   Usa np.fliplr() che capovolge la matrice da sinistra a destra
#   (fliplr = flip left-right)
#
# BONUS — "Negativo fotografico"
#   In fotografia il negativo inverte i colori: bianco diventa nero e viceversa
#   Formula: negativo = 255 - sprite
💡 Suggerimento

• La moltiplicazione sprite * 0.35 produce float (numeri con virgola). Aggiungi .astype(int) per riconvertire in interi

np.clip(array, min, max): qualsiasi valore sotto min viene portato a min, qualsiasi sopra max viene portato a max

• Il negativo è semplicissimo: 255 - sprite. I pixel bianchi (255) diventano neri (0) e viceversa!

✅ Soluzione completa
pythonsoluzione
import numpy as np

sprite = np.array([
    [  0,   0, 200, 200,   0,   0],
    [  0, 200, 255, 255, 200,   0],
    [200, 255, 180, 180, 255, 200],
    [200, 255, 180, 180, 255, 200],
    [  0, 200, 255, 255, 200,   0],
    [  0,   0, 200, 200,   0,   0],
])

print("Sprite originale:")
print(sprite)

# EFFETTO 1 — Versione notturna
notturno = (sprite * 0.35).astype(int)
print("\nEffetto 1 — Notturno (35% luminosità):")
print(notturno)

# EFFETTO 2 — Versione fantasma
fantasma = np.clip(sprite, 80, 200)
print("\nEffetto 2 — Fantasma (clip 80-200):")
print(fantasma)

# EFFETTO 3 — Flip orizzontale
specchio = np.fliplr(sprite)
print("\nEffetto 3 — Specchio orizzontale:")
print(specchio)

# BONUS — Negativo fotografico
negativo = 255 - sprite
print("\nBonus — Negativo fotografico:")
print(negativo)

# Verifica del negativo:
# Il pixel in alto a sinistra era 0 (nero) → diventa 255 (bianco) ✓
print(f"\nVerifica: 0 → {255-0}, 255 → {255-255}, 200 → {255-200}")

✅ Checklist: Sei pronto per il Modulo 4?