Machine Learning con Python Lezione #01

Corso di Machine Learning con Python: dalle basi agli algoritmi avanzati, deep learning, NLP e deployment. Teoria, pratica e casi reali.

Per approfondire queste conoscenze segui il corso in aula di Machine Learning con Python

Introduzione al Machine learning

Cos'è il machine learning?

Torna all’indice

Definizione e panoramica

Torna all’indice

Il machine learning (ML) è un sottoinsieme dell'intelligenza artificiale (IA) incentrato sullo sviluppo di algoritmi e modelli statistici che consentono ai computer di apprendere dai dati e di prendere decisioni o formulare previsioni basandosi su di essi. Invece di essere programmati esplicitamente per eseguire un compito specifico, gli algoritmi di ML individuano schemi, apprendono dalle osservazioni e migliorano le proprie prestazioni nel tempo. In termini più semplici, il machine learning permette ai sistemi di evolversi e adattarsi a nuovi scenari senza intervento umano, apprendendo dai dati passati. Questa capacità di auto-miglioramento ha reso il machine learning uno strumento potente per risolvere problemi complessi in svariati ambiti.

Esistono tre tipi principali di apprendimento automatico:

  1. Apprendimento supervisionato: prevede l'addestramento di un modello su dati etichettati, in cui le coppie input-output sono fornite esplicitamente. Il modello apprende una funzione di mappatura e la applica a dati mai visti in precedenza. Tra gli esempi figurano la regressione, la classificazione e la previsione di serie temporali.
  2. Apprendimento non supervisionato: opera su dati non etichettati per individuare pattern o strutture nascosti. Tecniche come il clustering e la riduzione della dimensionalità rientrano in questa categoria.
  3. Apprendimento per rinforzo (Reinforcement Learning): prevede l'addestramento di agenti a prendere decisioni interagendo con un ambiente e ricevendo feedback sotto forma di ricompense o penalità. Questo metodo è comunemente utilizzato nella robotica e negli algoritmi per i giochi.

Il machine learning sfrutta concetti provenienti dalla matematica, dalla statistica e dall'informatica, configurandosi come una disciplina multidisciplinare. I recenti progressi nella potenza di calcolo, nella disponibilità dei dati e nella progettazione degli algoritmi ne hanno notevolmente accelerato l'adozione e le potenzialità.

Contesto storico ed evoluzione

Torna all’indice

Le origini del machine learning risalgono alla metà del XX secolo, ma le sue radici affondano in concetti fondamentali della matematica, della statistica e dell'informatica. Di seguito è riportata una cronologia delle tappe fondamentali nell'evoluzione del machine learning:

Applicazioni chiave in diversi ambiti

Torna all’indice

Il machine learning ha trovato applicazioni in quasi tutti i settori industriali. La sua capacità di analizzare grandi quantità di dati, individuare schemi ed effettuare previsioni ha trasformato il modo in cui le aziende operano e risolvono i problemi. Di seguito sono riportate alcune applicazioni significative in diversi ambiti:

  1. Assistenza sanitaria
    • Diagnostica e diagnostica per immagini: I modelli di machine learning analizzano immagini mediche (ad esempio radiografie e risonanze magnetiche) per rilevare patologie quali cancro, retinopatia diabetica e malattie cardiovascolari.
    • Scoperta di farmaci: La modellazione predittiva accelera l'identificazione di potenziali candidati farmaci, riducendo i costi e i tempi di sviluppo.
    • Medicina personalizzata: Il machine learning personalizza i piani di trattamento sulla base di dati specifici del paziente, come le informazioni genetiche e la storia clinica.
    • Analisi predittiva: Gli ospedali utilizzano il machine learning per prevedere i ricoveri dei pazienti, ottimizzare l'allocazione delle risorse e identificare i pazienti a rischio.
  2. Finanza
    • Rilevamento delle frodi: Gli algoritmi di machine learning individuano anomalie nelle transazioni finanziarie per identificare potenziali frodi.
    • Algorithmic Trading: Le società finanziarie utilizzano il machine learning per analizzare le tendenze di mercato, prevedere i movimenti dei prezzi ed eseguire operazioni nei momenti ottimali.
    • Credit scoring: I modelli di machine learning valutano l'affidabilità creditizia analizzando un'ampia gamma di dati relativi al mutuatario.
    • Gestione del rischio: Le istituzioni finanziarie sfruttano il machine learning per identificare e mitigare i rischi associati a investimenti e prestiti.
  3. Vendita al dettaglio ed E-commerce
    • Sistemi di raccomandazione: Piattaforme come Amazon e Netflix utilizzano il machine learning per fornire consigli personalizzati su prodotti e contenuti.
    • Previsione della domanda: I rivenditori utilizzano modelli predittivi per ottimizzare la gestione delle scorte e ridurre le rotture di stock.
    • Analisi del sentiment dei clienti: Le aziende analizzano il feedback e le recensioni dei clienti per migliorare prodotti e servizi.
    • Prezzi dinamici: Il machine learning consente di adeguare i prezzi in tempo reale in base alla domanda, alla concorrenza e ad altri fattori.
  4. Trasporti e sistemi autonomi
    • Auto a guida autonoma: Il machine learning è un pilastro della tecnologia dei veicoli autonomi, poiché consente alle auto di riconoscere oggetti, navigare e prendere decisioni in tempo reale.
    • Gestione del traffico: Le città utilizzano il machine learning per ottimizzare il flusso del traffico e ridurre la congestione.
    • Manutenzione predittiva: Le aziende di trasporto sfruttano il machine learning per prevedere quando i veicoli o le infrastrutture necessitano di manutenzione, riducendo al minimo i tempi di inattività.
  5. Produzione
    • Controllo qualità: I modelli di machine learning rilevano difetti nei prodotti tramite l'analisi delle immagini e il rilevamento di anomalie.
    • Ottimizzazione della catena di approvvigionamento: L'analisi predittiva ottimizza la pianificazione della produzione, la gestione delle scorte e la logistica.
    • Robotica: Il machine learning potenzia le capacità dei robot industriali, rendendoli più adattabili ed efficienti.
  6. Energia e Ambiente
    • Previsione della produzione di energia rinnovabile: Il machine learning prevede la produzione di energia da fonti rinnovabili come il solare e l'eolico.
    • Reti intelligenti: Il machine learning ottimizza la distribuzione dell'energia, riduce gli sprechi e migliora l'affidabilità della rete.
    • Monitoraggio ambientale: I modelli analizzano i dati provenienti da sensori e satelliti per monitorare i cambiamenti climatici, prevedere disastri naturali e tenere sotto controllo la fauna selvatica.
  7. Intrattenimento e media
    • Creazione di contenuti: Il machine learning contribuisce alla generazione di musica, arte e video. Strumenti basati sull'IA, come DALL-E e ChatGPT, ne dimostrano le applicazioni creative.
    • Approfondimenti sul pubblico: Le aziende dei media utilizzano il machine learning per analizzare le preferenze degli spettatori e personalizzare i contenuti.
    • Sviluppo di videogiochi: Il machine learning potenzia i comportamenti dei personaggi non giocanti (NPC), la generazione procedurale di contenuti e le analisi di gioco. Gli NPC analizzano le tue mosse e imparano dai tuoi schemi d'attacco.
  8. Istruzione
    • Apprendimento personalizzato: Il machine learning adatta i contenuti didattici alle esigenze dei singoli studenti, aiutandoli ad apprendere secondo i propri ritmi.
    • Valutazione automatizzata: Il machine learning automatizza la valutazione di compiti e test, facendo risparmiare tempo agli insegnanti.
    • Traduzione linguistica: Strumenti come Google Translate consentono una comunicazione fluida tra lingue diverse.
  9. Sicurezza e sorveglianza
    • Rilevamento delle minacce: Il machine learning potenzia la sicurezza informatica identificando potenziali minacce, come malware e attacchi di phishing.
    • Riconoscimento facciale: I sistemi di sicurezza utilizzano il machine learning per la verifica dell'identità e il controllo degli accessi.
    • Il machine learning automatizza l'analisi dei filmati di sorveglianza, rilevando attività o comportamenti insoliti.
  10. Agricoltura
    • Agricoltura di precisione: Il machine learning aiuta gli agricoltori a ottimizzare l'irrigazione, la fertilizzazione e la lotta ai parassiti analizzando i dati provenienti da sensori e droni.
    • Previsione della resa: I modelli predittivi stimano le rese delle colture, agevolando la pianificazione e l'allocazione delle risorse.
    • Monitoraggio del suolo e delle condizioni meteorologiche: Il machine learning analizza la qualità del suolo e le condizioni meteorologiche per migliorare le pratiche agricole.

La versatilità e l'efficacia del machine learning ne fanno una tecnologia indispensabile nel mondo moderno. Dalla promozione di progressi medici salvavita allo stimolo di innovazioni nell'intrattenimento e in altri settori, il suo potenziale continua a crescere man mano che emergono nuove tecniche e applicazioni. Con l'evolversi del settore, affrontare sfide quali la privacy dei dati, i pregiudizi algoritmici e le questioni etiche sarà fondamentale per realizzarne appieno le potenzialità.

Le basi di Python per il machine learning

Torna all’indice

Introduzione a Python

Torna all’indice

Python è un linguaggio di programmazione versatile e ampiamente utilizzato, divenuto un pilastro del machine learning e della data science. La sua semplicità, il vasto ecosistema di librerie e il forte supporto della comunità lo rendono una scelta ideale sia per i principianti che per i professionisti. Il machine learning consiste nella creazione di modelli capaci di apprendere dai dati per effettuare previsioni o prendere decisioni. Python semplifica questo processo grazie alla sua sintassi intuitiva e a una vasta gamma di librerie specializzate. Che si tratti di analizzare dati, visualizzare tendenze o sviluppare algoritmi predittivi, Python offre gli strumenti necessari per raggiungere i propri obiettivi.

Tra i motivi principali per cui Python è preferito per il machine learning figurano:

  1. Facilità d'uso La sintassi semplice di Python consente agli sviluppatori di concentrarsi sulla risoluzione dei problemi anziché sugli errori di sintassi.
  2. Supporto alla comunità Una comunità ampia e attiva significa che puoi trovare risposte a quasi tutti i problemi che incontri.
  3. Librerie vaste Le librerie di Python, come NumPy, pandas, Matplotlib e scikit-learn, forniscono tutte le funzionalità necessarie per l'analisi dei dati e il machine learning.
  4. Interoperabilità Python può integrarsi facilmente con altri linguaggi e strumenti, rendendolo estremamente adattabile a diversi flussi di lavoro.
  5. Scalabilità: Python è in grado di gestire qualsiasi cosa, dai prototipi su piccola scala ai sistemi di machine learning su larga scala.

Questa lezione ti introdurrà alle basi di Python, metterà in evidenza le librerie chiave per il machine learning e fornirà le istruzioni per configurare il tuo ambiente.

Librerie chiave per il machine learning

Torna all’indice

L'ecosistema di Python comprende diverse librerie potenti che costituiscono le fondamenta del machine learning. Qui presentiamo quattro librerie essenziali: NumPy, pandas, Matplotlib e scikit-learn.

  1. NumPy

    NumPy (Numerical Python) è una libreria per il calcolo numerico. Offre supporto per array multidimensionali e una raccolta di funzioni matematiche per operare su tali array. Le caratteristiche principali di NumPy includono:

    • Manipolazione efficiente di array: Gli array NumPy (ndarray) sono più veloci e più efficienti nell'uso della memoria rispetto alle liste Python.
    • Operazioni matematiche: Include operazioni quali la moltiplicazione di matrici, i calcoli statistici e l'algebra lineare.
    • Generazione di numeri casuali: Utile per creare dataset sintetici e inizializzare i pesi del modello.
    001 base
    Primi passi con NumPy e array
    Creazione, indicizzazione e operazioni vettoriali di base su array numerici.
    import numpy as np
    
    # Crea un array
    arr = np.array([1, 2, 3, 4])
    print(arr)
    # Esegue le operazioni
    print(arr + 2) # Aggiunge 2 a ciascun elemento
    print(np.mean(arr)) # Calcola la media
    [1 2 3 4]
    [3 4 5 6]
    2.5
  2. pandas

    pandas è una libreria per la manipolazione e l'analisi dei dati. Offre strutture dati come DataFrame e Series che rendono la gestione dei dati strutturati semplice e intuitiva.

    • DataFrame: Strutture dati tabellari con assi etichettati (righe e colonne).
    • Pulizia dei dati: Funzioni per la gestione dei dati mancanti, il filtraggio e la ristrutturazione dei dataset.
    • Integrazione Funziona perfettamente con NumPy, Matplotlib e altre librerie.
    002 base
    Primi passi con pandas e DataFrame
    Creazione, indicizzazione e operazioni di base su DataFrame.
    import pandas as pd
    
    # Crea un DataFrame
    data = {
    'Name': ['Alice', 'Bob', 'Charlie'],
    'Age': [25, 30, 35],
    'Salary': [50000, 60000, 70000]
    }
    df = pd.DataFrame(data)
    
    print(df)
    # Analizza data
    print(df.describe()) # Statistiche di sintesi
    print(df[df['Age'] > 28]) # Filtra righe
          Name  Age  Salary
    0    Alice   25   50000
    1      Bob   30   60000
    2  Charlie   35   70000
            Age   Salary
    count   3.0      3.0
    mean   30.0  60000.0
    std     5.0  10000.0
    min    25.0  50000.0
    25%    27.5  55000.0
    50%    30.0  60000.0
    75%    32.5  65000.0
    max    35.0  70000.0
          Name  Age  Salary
    1      Bob   30   60000
    2  Charlie   35   70000
  3. Matplotlib

    Matplotlib è una libreria di plotting utilizzata per creare visualizzazioni statiche, interattive e animate. Viene spesso impiegata per esplorare i dati e comunicare i risultati in modo efficace.

    • Grafici 2D: Grafici a linee, grafici a barre, istogrammi, grafici a dispersione, ecc..
    • Personalizzazione: Controllo su ogni aspetto di un grafico (titoli, etichette, legende, ecc.).
    • Integrazione: Funziona bene con pandas e NumPy.
    003 base
    Primi passi con matplotlib e visualizzazione dei dati
    Creazione di grafici e visualizzazioni per analizzare i dati.
    import matplotlib.pyplot as plt
    
    # Crea data
    x = [1, 2, 3, 4]
    y = [10, 20, 25, 30]
    
    # Crea un Grafico
    plt.plot(x, y, label= 'Trend', color= 'blue', marker='o')
    plt.title('Esempio di Grafico con Matplotlib')
    plt.xlabel('X-asse')
    plt.ylabel('Y-asse')
    plt.legend()
    plt.show()
  4. scikit-learn

    scikit-learn è una libreria di machine learning che offre strumenti semplici ed efficienti per il data mining e l'analisi dei dati. Supporta sia l'apprendimento supervisionato che quello non supervisionato e si integra bene con altre librerie Python.

    • Algoritmi: Implementa algoritmi quali regressione lineare, alberi di decisione, clustering e macchine a vettori di supporto.
    • Pre-elaborazione: Strumenti per il ridimensionamento delle caratteristiche, la codifica e la suddivisione dei dati.
    • Valutazione del modello: Funzioni per la convalida incrociata e le metriche di prestazione.
    004 base
    Apprendimento supervisionato con scikit-learn
    Machine learning supervisionato: training, test ed errore MSE.
    # Importazione delle librerie necessarie da scikit-learn
    from sklearn.model_selection import train_test_split
    from sklearn.linear_model import LinearRegression
    from sklearn.metrics import mean_squared_error
    
    # Definizione dei dati
    # X sono le caratteristiche in ingresso (le "domande")
    X = [[1], [2], [3], [4]] # Features
    # y sono i valori target corretti (le "risposte")
    y = [10, 20, 30, 40] # Target
    
    # Suddivisione dei dati in training set e test set
    # l'80% dei dati viene usato per l'addestramento, il 20% per il test
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42
    )
    
    # Creazione e addestramento del modello di regressione lineare
    model = LinearRegression()
    model.fit(X_train, y_train)
    
    # Step 4: Predizione sui dati di test e valutazione dell'errore
    predictions = model.predict(X_test)
    print(f'Mean Squared Error: {mean_squared_error(y_test, predictions)}')
    Mean Squared Error: 1.262177448353619e-29

    004 Apprendimento supervisionato con scikit-learn

    Lezione 01

    Primo esempio di apprendimento supervisionato con scikit-learn: alleniamo un modello di regressione lineare su pochi dati etichettati e ne valutiamo l'errore di previsione su dati mai visti.

    1. Importazione delle librerie
    from sklearn.model_selection import train_test_split
    from sklearn.linear_model import LinearRegression
    from sklearn.metrics import mean_squared_error

    Il primo passo consiste nell'importare gli strumenti necessari da scikit-learn: la funzione per dividere i dati, il modello di regressione lineare e la metrica per valutarne gli errori.

    • train_test_split e LinearRegression gestiscono rispettivamente la suddivisione dei dati e l'algoritmo di apprendimento
    • mean_squared_error calcola l'errore quadratico medio tra previsioni e valori reali

    Queste tre funzioni provengono da moduli diversi di scikit-learn, la libreria di riferimento per il machine learning classico in Python.

    2. Definizione dei dati (X e y)
    X = [[1], [2], [3], [4]] # Features
    y = [10, 20, 30, 40] # Target

    X rappresenta le caratteristiche in ingresso, mentre y contiene i valori target che il modello deve imparare a prevedere. In questo esempio la relazione è lineare e perfetta: ogni valore di y è dieci volte il corrispondente valore di X.

    • X sono i dati in ingresso, organizzati come una lista di liste.
    • y sono i risultati corretti associati a ciascun valore di X.

    La doppia parentesi quadra in X è necessaria perché scikit-learn si aspetta sempre una tabella di dati, anche con una sola caratteristica per riga.

    3. Suddivisione dei dati in training e test
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42
    )

    I dati vengono divisi in due gruppi: uno usato per addestrare il modello e uno tenuto da parte per verificarne le prestazioni su dati mai visti.

    • test_size=0.2 riserva il 20% dei dati al test
    • random_state=42 rende la suddivisione riproducibile ad ogni esecuzione.

    Con soli 4 esempi il 20% corrisponde a un solo dato di test: sufficiente per capire il meccanismo, ma non rappresentativo di un caso reale.

    4. Addestramento del modello
    model = LinearRegression()
    model.fit(X_train, y_train)

    Viene creata un'istanza del modello di regressione lineare e la si addestra sui dati di training tramite il metodo fit, che cerca la retta che meglio approssima i punti forniti.

    • LinearRegression() crea il modello, ancora privo di conoscenza.
    • fit() avvia il processo di apprendimento vero e proprio.

    Dato che la relazione tra X e y è perfettamente lineare (y = 10x), il modello troverà una retta praticamente esatta.

    5. Predizione e valutazione dell'errore
    predictions = model.predict(X_test)
    print(f'Mean Squared Error: {mean_squared_error(y_test, predictions)}')

    Il modello addestrato viene usato per prevedere il valore del dato di test, e la previsione viene confrontata con il valore reale tramite il Mean Squared Error, che misura l'errore medio al quadrato.

    • predict() restituisce la stima del modello sui dati di test.
    • mean_squared_error() calcola quanto la previsione si discosta dal valore reale.

    Trattandosi di una relazione perfetta e senza rumore, ci si aspetta un MSE prossimo allo zero.

    Punto chiave da sottolineare

    In questo esempio i dati non contengono rumore: la relazione tra X e y è esatta (y = 10x). Questo rende il modello quasi perfetto, ma non è realistico: nei problemi reali i dati contengono sempre variazioni casuali, e l'MSE sarà un valore significativo, non prossimo a zero.

    In poche parole...

    Il codice mostra un flusso completo di apprendimento supervisionato: si definiscono i dati etichettati, si dividono in training e test, si addestra un modello di regressione lineare e se ne valuta l'errore di previsione. Questo schema (dati, split, fit, predict, evaluate) è alla base della maggior parte degli algoritmi di ML supervisionato.

Configurazione dell'ambiente

Torna all’indice

Per iniziare a usare Python per il machine learning, dovrai configurare il tuo ambiente. Segui questi passaggi per iniziare:

  1. Installare Python

    Scarica e installa Python dal sito ufficiale python.org. Si consiglia di utilizzare Python 3.10 o versioni successive per garantire la compatibilità con le librerie più recenti.

  2. Scegli un IDE o un editor di testo

    Alcune opzioni popolari includono:

    • Visual Studio Code VS Code è leggero ma completo: con l'estensione Python ufficiale offre autocompletamento, debug integrato, supporto per ambienti virtuali e Jupyter Notebook. Gratuito, multipiattaforma ed estendibile, è ideale per imparare Python senza la complessità di un IDE pesante.
    • PyCharm IDE ricco di funzionalità per lo sviluppo in Python.
    • Jupyter Notebook Ambiente interattivo basato sul web, ideale per l'esplorazione e la visualizzazione dei dati.
  3. Crea un ambiente virtuale

    Gli ambienti virtuali aiutano a gestire le dipendenze e a evitare conflitti tra i progetti.

    # Crea un ambiente virtuale
    python -m venv .venv
    
    # attiva manualmente l'ambiente
    .venv\Scripts\activate
    
    # macOS/Linux
    source .venv/bin/activate
    
    # Seleziona l'interprete Python corretto in VS Code:
    Ctrl+Shift+P
    Python: Select Interpreter
    seleziona l'interprete dentro .venv
    
  4. Installa le librerie essenziali

    Usa pip (il gestore di pacchetti di Python) per installare le librerie. Ad esempio:

    #Installa le librerie all'interno dell'ambiente
    python -m pip install numpy pandas scikit-learn matplotlib
    

    In alternativa, prendi in considerazione l'utilizzo di Anaconda, una distribuzione che include numerose librerie preinstallate e un gestore di pacchetti.

  5. Verifica la tua configurazione

    Crea uno script di test per verificare che tutto funzioni:

    005 base
    Librerie essenziali per il machine learning
    NumPy, pandas, matplotlib e scikit-learn.
    import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    from sklearn.linear_model import LinearRegression
    
    print("All libraries imported successfully!")
    All libraries imported successfully!

    Esegui lo script per verificare che non si verifichino errori. Queste nozioni fondamentali ti prepareranno ad approfondire le potenzialità di Python per il machine learning. Una volta predisposto l'ambiente e acquisita familiarità con queste librerie, potrai iniziare a esplorare argomenti avanzati come il deep learning, l'elaborazione del linguaggio naturale e la computer vision.

Tipi di machine learning

Torna all’indice

Il machine learning (ML) è un sottoinsieme dell'intelligenza artificiale (AI) che consente ai sistemi di apprendere e prendere decisioni basandosi sui dati. In linea generale, il machine learning può essere suddiviso in quattro tipologie principali: apprendimento supervisionato, apprendimento non supervisionato, apprendimento per rinforzo e apprendimento semi-supervisionato. Ciascuna tipologia assolve a scopi specifici e trova impiego in diverse applicazioni, a seconda della natura del problema e dei dati disponibili.

Apprendimento supervisionato

Torna all’indice

L'apprendimento supervisionato è una delle tipologie più comuni di apprendimento automatico. In questo approccio, il modello viene addestrato utilizzando dati etichettati, per i quali vengono forniti sia le caratteristiche di input che i corrispondenti output (target). L'obiettivo è apprendere una funzione di mappatura che metta in relazione input e output, consentendo al modello di effettuare previsioni accurate su dati nuovi e mai visti in precedenza.

Concetti chiave

Algoritmi comuni

  1. Regressione lineare: Utilizzato per prevedere valori continui (ad es. i prezzi delle case).
  2. Regressione logistica: Utilizzato per problemi di classificazione binaria (ad es. rilevamento di spam).
  3. Alberi di decisione: Modelli non lineari che suddividono i dati in base ai valori delle caratteristiche.
  4. Macchine a vettori di supporto (SVM): Trova un iperpiano che separa le classi con il massimo margine.
  5. Reti neurali: Replica il funzionamento del cervello umano; utile per schemi complessi.

Applicazioni

006 base
Apprendimento Supervisionato - Regressione Lineare
Flusso completo di un problema di regressione supervisionata con preparazione di un dato nuovo su cui fare previsioni.
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

# Sample data
X = [[1], [2], [3], [4], [5]] # Input features
y = [2, 4, 6, 8, 10] # Target values

# Split data into training and testing sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2,
                                                    random_state=42)

# Show the split
print(f'Dati di training: {X_train} -> {y_train}')
print(f'Dati di test: {X_test} -> {y_test}')

# Train a linear regression model
model = LinearRegression()
model.fit(X_train, y_train)

# Show the relationship the model has learned (y = coef * x + intercept)
print(f'Relazione trovata: y = {model.coef_[0]:.2f} * x + '
+ f'{model.intercept_:.2f}')

# Make predictions on the test set
predictions = model.predict(X_test)
print(f'Mean Squared Error: {mean_squared_error(y_test, predictions)}')

# Use the model on a brand-new, never-seen value
nuovo_valore = [[10]]
previsione = model.predict(nuovo_valore)
print(f'Previsione per x=10: {previsione[0]:.2f}')
Dati di training: [[5], [3], [1], [4]] -> [10, 6, 2, 8]
Dati di test: [[2]] -> [4]
Relazione trovata: y = 2.00 * x + 0.00
Mean Squared Error: 0.0
Previsione per x=10: 20.00

006 Apprendimento Supervisionato - Regressione Lineare

Lezione 01

Lo scopo dell'apprendimento supervisionato è quello di insegnare al modello a riconoscere una relazione matematica tra input e output, utilizzando esempi etichettati. Questo processo permette al modello di fare previsioni su dati nuovi e mai visti in precedenza. Questo codice amplia l'esempio base di apprendimento supervisionato aggiungendo la visualizzazione della suddivisione dei dati, dei parametri appresi dal modello e la preparazione di un dato nuovo su cui fare previsioni.

1. Importazione delle librerie
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

Il primo passo consiste nell'importare gli strumenti necessari da scikit-learn: la funzione per dividere i dati, il modello di regressione lineare e la metrica per valutarne gli errori.

  • train_test_split e LinearRegression gestiscono la suddivisione dei dati e l'algoritmo di apprendimento.
  • mean_squared_error calcola l'errore quadratico medio tra previsioni e valori reali.

Queste tre funzioni provengono da moduli diversi di scikit-learn, la libreria di riferimento per il machine learning classico in Python.

2. Definizione dei dati (X e y)
X = [[1], [2], [3], [4], [5]] # Input features
y = [2, 4, 6, 8, 10] # Target values

X rappresenta le caratteristiche in ingresso, mentre y contiene i valori target che il modello deve imparare a prevedere. In questo esempio la relazione è lineare e perfetta: ogni valore di y è il doppio del corrispondente valore di X.

  • X sono i dati in ingresso, organizzati come una lista di liste.
  • y sono i risultati corretti associati a ciascun valore di X

Qui la relazione è semplicissima e voluta apposta: ogni valore di y è il doppio del corrispondente valore di X (1→2, 2→4, 3→6...). È come dire: "guarda questi esempi e capisci la regola.

Perché X ha le doppie parentesi [[1], [2]...] e non [1, 2...]?
Questo è un punto che spesso confonde chi inizia: scikit-learn si aspetta sempre una tabella di dati (righe = esempi, colonne = caratteristiche), anche quando c'è una sola caratteristica. [[1], [2], [3]] significa "5 esempi, ciascuno con 1 sola caratteristica.

3. Suddivisione dei dati in training e test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

I dati vengono divisi in due gruppi: uno usato per addestrare il modello e uno tenuto da parte per verificarne le prestazioni su dati mai visti.

  • test_size=0.2 riserva il 20% dei dati al test.
  • random_state=42 rende la suddivisione riproducibile ad ogni esecuzione.

È lo stesso principio di un esame: non si può interrogare uno studente sugli stessi identici esercizi fatti a lezione, altrimenti non si capisce se ha davvero capito o solo memorizzato.

Nota: con solo 5 esempi, il 20% corrisponde praticamente a 1 solo dato di test - utile per capire il meccanismo, ma in un caso reale servirebbero molti più dati per avere una divisione sensata.

4. Visualizzazione della suddivisione
print(f'Dati di training: {X_train} -> {y_train}')
print(f'Dati di test: {X_test} -> {y_test}')

Queste due righe non sono strettamente necessarie al funzionamento del modello, ma aiutano a capire concretamente quali punti sono finiti nel training set e quali nel test set.

Vedere esplicitamente i dati aiuta a smontare l'idea che il train test split sia una "scatola nera": in realtà seleziona semplicemente un sottoinsieme di righe.

5. Addestramento del modello
model = LinearRegression()
model.fit(X_train, y_train)

Viene creata un'istanza del modello di regressione lineare e la si addestra sui dati di training tramite il metodo fit, che cerca la retta che meglio approssima i punti forniti.

  • LinearRegression() crea il modello, ancora privo di conoscenza.
  • fit() avvia il processo di apprendimento vero e proprio.

Dato che la relazione tra X e y è perfettamente lineare (y = 2x), il modello troverà una retta praticamente esatta.

6. Visualizzazione della relazione appresa
print(f'Relazione trovata: y = {model.coef_[0]:.2f} * x + {model.intercept_:.2f}')

Dopo l'addestramento, il modello memorizza i parametri della retta trovata. Stamparli permette di verificare concretamente cosa ha davvero imparato il modello.

  • coef_ rappresenta la pendenza della retta, cioè di quanto cresce y per ogni unità di X.
  • intercept_ è il valore di y quando X è zero.

Essendo i dati privi di rumore, ci si aspetta un risultato molto vicino a "y = 2.00 * x + 0.00".

7. Previsione sui dati di test e valutazione dell'errore
predictions = model.predict(X_test)
print(f'Mean Squared Error: {mean_squared_error(y_test, predictions)}')

Il modello addestrato viene usato per prevedere il valore del dato di test, e la previsione viene confrontata con il valore reale tramite il Mean Squared Error, che misura l'errore medio al quadrato.

  • predict() restituisce la stima del modello sui dati di test.
  • mean_squared_error() calcola quanto la previsione si discosta dal valore reale.

Trattandosi di una relazione perfetta e senza rumore, l'MSE risulterà un numero estremamente vicino allo zero.

8. Preparazione di un dato nuovo
nuovo_valore = [[10]]
previsione = model.predict(nuovo_valore)
print(f'Previsione per x=10: {previsione[0]:.2f}')

Prepara un dato completamente nuovo, mai visto né in training né in test, da usare per una futura previsione. Anche qui il valore è racchiuso in una doppia parentesi quadra per rispettare il formato tabellare richiesto da scikit-learn.

Effettua la previsione (model.predict) e la stampa a schermo.

Punto chiave da sottolineare

Anche in questa versione più completa, la relazione tra X e y resta perfetta (y = 2x): per questo l'MSE sarà praticamente zero e il coefficiente trovato sarà molto vicino a 2.00. Con dati reali, invece, ci si aspettano sia rumore che un MSE più significativo.

In poche parole...

Il codice mostra il flusso completo di un problema di regressione supervisionata: si preparano i dati, si dividono in training e test, si addestra il modello, se ne osservano i parametri e l'errore, infine si prepara un dato nuovo su cui applicare le previsioni. È lo schema di base che ricorre in quasi tutti i problemi di apprendimento supervisionato.

Un caso reale

007 base
Apprendimento supervisionato con scikit-learn, un caso reale
Prevedi un prezzo da una feature: training, test ed errore MSE.
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

# ---------------------------------------------------------
# 1. CREIAMO I DATI
# ---------------------------------------------------------
# Simuliamo, ad esempio, i mq di un locale (X) e il suo prezzo di affitto
# mensile in euro (y). Aggiungiamo un po' di "rumore" casuale, perché
# nella realtà i dati non stanno mai perfettamente su una linea retta.

np.random.seed(42)  # fissiamo il seed per risultati riproducibili

n_campioni = 30
X = np.array([[i] for i in range(20, 20 + n_campioni)])  # mq da 20 a 49
rumore = np.random.normal(loc=0, scale=15, size=n_campioni)
y = 25 * X.flatten() + 100 + rumore  # relazione lineare + rumore

# ---------------------------------------------------------
# 2. DIVIDIAMO I DATI IN TRAINING E TEST
# ---------------------------------------------------------
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# ---------------------------------------------------------
# 3. ADDESTRIAMO IL MODELLO
# ---------------------------------------------------------
modello = LinearRegression()
modello.fit(X_train, y_train)

print(f"Relazione trovata: prezzo = {modello.coef_[0]:.2f} * mq "
      f"+ {modello.intercept_:.2f}")

# ---------------------------------------------------------
# 4. FACCIAMO PREVISIONI E VALUTIAMO L'ERRORE
# ---------------------------------------------------------
previsioni = modello.predict(X_test)
mse = mean_squared_error(y_test, previsioni)
print(f"Mean Squared Error: {mse:.2f}")

# ---------------------------------------------------------
# 5. USIAMO IL MODELLO SU UN DATO NUOVO
# ---------------------------------------------------------
nuovo_mq = [[35]]
prezzo_stimato = modello.predict(nuovo_mq)
print(f"Stima per un locale di 35 mq: {prezzo_stimato[0]:.2f} euro/mese")
Relazione trovata: prezzo = 24.42 * mq + 116.84
Mean Squared Error: 111.27
Stima per un locale di 35 mq: 971.66 euro/mese

007 Apprendimento supervisionato con scikit-learn, un caso reale

Lezione 01

Cosa stiamo cercando di fare? Immagina di voler prevedere il prezzo di affitto di un locale commerciale conoscendo solo i suoi metri quadri. Se avessi tanti esempi reali (mq e prezzo di locali già affittati), potresti "far imparare" a un computer la relazione tra le due cose, per poi usarla su un locale nuovo di cui conosci solo i mq.

1. Creiamo i dati
n_campioni = 30
X = np.array([[i] for i in range(20, 20 + n_campioni)])  # mq da 20 a 49
y = 25 * X.flatten() + 100 + rumore  # relazione lineare + rumore
  • X sono i dati in ingresso (le "domande")'.
  • y sono i risultati corretti associati (le "risposte")'.

Creiamo 30 esempi finti ma realistici: locali da 20 a 49 mq, con un prezzo che segue circa la regola "25€ per ogni mq, più 100€ fissi" — ma con delle piccole variazioni casuali (il "rumore"), perché nella vita reale due locali della stessa metratura non costano mai esattamente uguale (dipende dalla zona, dallo stato dell'immobile, ecc.).

2. Dividiamo i dati in Training e Test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

Non possiamo verificare quanto il modello ha imparato bene usando gli stessi dati con cui l'abbiamo istruito — sarebbe come dare a uno studente le risposte prima dell'esame. Per questo dividiamo i dati: l'80% (24 locali) serve per l'addestramento, il 20% (6 locali) resta "nascosto" e serve solo per il test finale.

  • Una parte (80%) per far studiare il modello.
  • Una parte (20%) tenuta "nascosta", per verificare dopo se ha imparato bene.

È lo stesso principio di un esame: non si può interrogare uno studente sugli stessi identici esercizi fatti a lezione, altrimenti non si capisce se ha davvero capito o solo memorizzato.

3. Addestriamo il modello
modello = LinearRegression()
modello.fit(X_train, y_train)
print(f"Relazione trovata: prezzo = {modello.coef_[0]:.2f} * mq + {modello.intercept_:.2f}")

La LinearRegression cerca la retta che meglio approssima tutti i punti di training. Nel nostro caso ha trovato circa prezzo = 24.42 × mq + 116.84 — molto vicino alla regola reale (25 × mq + 100) che avevamo usato per generare i dati: è il segno che il modello ha "capito" bene la relazione.

4. Facciamo previsioni e valutiamo l'errore
previsioni = modello.predict(X_test)
mse = mean_squared_error(y_test, previsioni)
print(f"Mean Squared Error: {mse:.2f}")

Il Mean Squared Error misura quanto, in media, le previsioni si discostano dai valori reali (elevando al quadrato le differenze, così gli errori grandi pesano di più). Un MSE vicino a zero indica un ottimo modello; un MSE alto indica previsioni poco affidabili. Nel nostro caso è utile guardare anche la sua radice quadrata (RMSE), perché è espressa nella stessa unità di misura del prezzo — quindi "di quanti euro, in media, sbagliamo".

5. Usiamo il modello su un dato nuovo
nuovo_mq = [[35]]
prezzo_stimato = modello.predict(nuovo_mq)
print(f"Stima per un locale di 35 mq: {prezzo_stimato[0]:.2f} euro/mese")

Una volta addestrato e validato, il modello può fare previsioni su dati completamente nuovi: qui stimiamo il prezzo per un locale di 35 mq che non è mai stato visto dal modello.

Apprendimento non supervisionato

Torna all’indice

Nell'apprendimento non supervisionato, il modello viene addestrato su dati privi di output etichettati. L'obiettivo è individuare pattern, strutture o relazioni nascosti all'interno dei dati. Questo tipo di apprendimento è utile quando il dataset è sprovvisto di etichette o categorie predefinite.

Concetti chiave

Algoritmi comuni

  1. Clustering K-Means: Suddivide i dati in k cluster in base alla somiglianza.
  2. Clustering gerarchico: Costruisce una struttura a albero di cluster.
  3. Analisi delle componenti principali (PCA): Riduce la dimensionalità preservando la varianza.
  4. t-SNE: Visualizza dati ad alta dimensionalità in uno spazio 2D o 3D.

Applicazioni

008 base
Apprendimento NON supervisionato - K-Means clustering
K-Means è un algoritmo di clustering che raggruppa dati simili senza bisogno di etichette.
from sklearn.cluster import KMeans
import numpy as np

# Dati di esempio: 6 punti nello spazio 2D (coordinate x, y)
# Nota: NON forniamo etichette/categorie ai dati -> apprendimento NON supervisionato
X = np.array([[1, 2], [1, 4], [1, 0], [10, 2], [10, 4], [10, 0]])

# Creiamo il modello K-Means chiedendo di trovare 2 gruppi (cluster)
# random_state=42 fissa il seed casuale per ottenere risultati riproducibili
kmeans = KMeans(n_clusters=2, random_state=42)

# Addestriamo il modello: l'algoritmo analizza i dati e trova da solo
# la struttura sottostante, senza che gli venga detto "questo è giusto/sbagliato"
kmeans.fit(X)

# Stampiamo l'etichetta di cluster assegnata a ciascun punto (0 oppure 1)
# L'ordine corrisponde all'ordine dei punti in X
print(kmeans.labels_)
[0 0 0 1 1 1]

008 Apprendimento NON supervisionato - K-Means clustering

Lezione 01

Nell'apprendimento non supervisionato non forniamo al modello le "risposte giuste", ma solo i dati grezzi: sarà l'algoritmo a scoprire da solo pattern e raggruppamenti nascosti. Un esempio classico è il clustering, dove l'obiettivo è dividere i dati in gruppi di elementi simili tra loro. Vediamo come farlo in pratica con K-Means, uno degli algoritmi di clustering più usati.

1. Importazione delle librerie
from sklearn.cluster import KMeans
import numpy as np

Il primo passo consiste nell'importare gli strumenti necessari: la classe KMeans dalla libreria scikit-learn, che contiene l'implementazione dell'algoritmo di clustering, e NumPy, la libreria fondamentale per la gestione di array e calcoli numerici in Python.

  • scikit-learn: libreria di machine learning con algoritmi pronti all'uso.
  • NumPy: Gestisce array multidimensionali in modo efficiente.

Queste due librerie sono lo standard de facto per il machine learning classico in Python e si trovano quasi sempre insieme in questo tipo di script.

2. Definizione dei dati di esempio
X = np.array([[1, 2], [1, 4], [1, 0], [10, 2], [10, 4], [10, 0]])

X è una matrice con 6 punti, ognuno con 2 coordinate (x, y). Guardando i numeri si nota subito che ci sono due gruppi: i primi 3 punti hanno la prima coordinata vicina a 1, gli ultimi 3 vicina a 10 — sono quindi "spazialmente" separati.

  • I primi tre punti hanno coordinata x uguale a 1.
  • Gli ultimi tre punti hanno coordinata x uguale a 10.

Notando la distanza tra i due gruppi di punti, è facile intuire visivamente che i dati formano naturalmente due cluster distinti, uno vicino a x=1 e uno vicino a x=10.

Nell'apprendimento non supervisionato non forniamo etichette: è l'algoritmo stesso a dover scoprire questa struttura nascosta nei dati.

3. Creazione e addestramento del modello K-Means
kmeans = KMeans(n_clusters=2, random_state=42)
kmeans.fit(X)

Qui viene creato il modello K-Means specificando il numero di cluster desiderati (n_clusters=2) e un valore di random_state per rendere i risultati riproducibili. Il metodo fit() avvia l'addestramento vero e proprio sui dati.

  • n_clusters=2: indichiamo esplicitamente in quanti gruppi dividere i dati.
  • random_state=42: fissa il seed casuale per risultati costanti ad ogni esecuzione.

Durante il fit, l'algoritmo posiziona due centroidi e li sposta iterativamente fino a minimizzare la distanza tra ogni punto e il centroide del proprio cluster.

4. Visualizzazione dei risultati
print(kmeans.labels_)

Una volta completato l'addestramento, l'attributo labels_ contiene l'etichetta del cluster assegnata a ciascun punto del dataset originale, nello stesso ordine in cui i punti erano stati inseriti in X.

  • Ogni valore in labels_ sarà 0 oppure 1, corrispondente ai due cluster.
  • L'ordine delle etichette rispecchia l'ordine dei punti in X.

Ci aspettiamo un output simile a [0 0 0 1 1 1], dove i primi tre punti finiscono in un cluster e gli ultimi tre nell'altro, confermando l'intuizione visiva.

Punto chiave da sottolineare

A differenza dell'apprendimento supervisionato, qui non abbiamo mai detto al modello quali fossero le "risposte giuste". Non esiste una colonna y con le etichette corrette. L'algoritmo scopre da solo i pattern nei dati, basandosi solo sulla vicinanza geometrica dei punti. Questo è esattamente ciò che distingue il clustering (non supervisionato) dalla classificazione (supervisionata).

In poche parole...

K-Means è un algoritmo di clustering che raggruppa dati simili senza bisogno di etichette pre-assegnate, cercando di minimizzare la distanza tra i punti e il centro (centroide) del proprio gruppo. È utile ogni volta che vogliamo scoprire strutture o segmenti nascosti nei dati (es. segmentazione clienti, raggruppamento di documenti, compressione di immagini).

Un caso reale

009 base
Apprendimento NON supervisionato - K-Means clustering
K-Means per raggruppare automaticamente i clienti in 3 gruppi.
from sklearn.cluster import KMeans  # Importiamo l'algoritmo di clustering
import numpy as np  # Importiamo numpy per gestire gli array

# Definiamo i dati dei clienti (spesa media mensile, numero di visite al mese)
# Nessuna etichetta "tipo cliente" viene fornita al modello
X = np.array([
    [15, 2],   # cliente occasionale
    [18, 3],
    [12, 1],
    [90, 15],  # cliente affezionato
    [95, 18],
    [88, 14],
    [50, 8],   # cliente intermedio
    [55, 9],
])

# Creiamo il modello KMeans chiedendo di trovare 3 gruppi di clienti
# n_init=10 fa ripartire l'algoritmo 10 volte da centri diversi e
# tiene il risultato migliore
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)

# Addestriamo il modello sui dati, assegnando ogni cliente a un cluster
kmeans.fit(X)

# Visualizziamo i risultati ottenuti
print("Cluster assegnati a ciascun cliente:", kmeans.labels_)
print("Centro di ciascun gruppo (spesa media, visite medie):\n",
      kmeans.cluster_centers_)
Cluster assegnati a ciascun cliente: [0 0 0 1 1 1 2 2]
Centro di ciascun gruppo (spesa media, visite medie):
 [[15.          2.        ]
 [91.         15.66666667]
 [52.5         8.5       ]]

009 Apprendimento NON supervisionato - K-Means clustering

Lezione 01

Questo esempio mostra il clustering, una tecnica di apprendimento non supervisionato che raggruppa i dati senza etichette predefinite. L'algoritmo K-Means individua automaticamente pattern nascosti nei dati dei clienti, basandosi solo sulle caratteristiche numeriche.

1. Importazione delle librerie
from sklearn.cluster import KMeans
import numpy as np

Importiamo la classe KMeans da scikit-learn, l'algoritmo che useremo per raggruppare i clienti, e numpy per gestire i dati numerici in formato array.

KMeans fa parte del modulo cluster di scikit-learn, dedicato interamente alle tecniche di apprendimento non supervisionato basate sul raggruppamento.

2. Definizione dei dati
X = np.array([
    [15, 2],   # cliente occasionale
    [18, 3],
    [12, 1],
    [90, 15],  # cliente affezionato
    [95, 18],
    [88, 14],
    [50, 8],   # cliente intermedio
    [55, 9],
])

Creiamo la matrice X, dove ogni riga rappresenta un cliente e le due colonne indicano rispettivamente la spesa media mensile e il numero di visite al mese.

  • Nessuna etichetta "tipo cliente" viene fornita al modello.
  • Ogni riga contiene solo le caratteristiche osservate, senza indicazioni sul gruppo di appartenenza.

Questa è la differenza chiave rispetto all'apprendimento supervisionato: il modello non sa in anticipo quali siano i gruppi, ma dovrà scoprirli da solo.

3. Creazione del modello KMeans
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)

Istanziamo l'algoritmo K-Means specificando quanti gruppi (cluster) vogliamo ottenere e alcuni parametri per rendere il risultato riproducibile e affidabile.

  • n_clusters=3 Indica che vogliamo suddividere i clienti in 3 gruppi.
  • n_init=10 Fa ripartire l'algoritmo 10 volte da centri diversi, tenendo il risultato migliore.

Il parametro random_state=42 fissa il seed casuale, così l'esecuzione del codice produce sempre lo stesso risultato, utile a scopo didattico.

Scegliere il numero corretto di cluster è spesso la parte più delicata del clustering, e in un caso reale andrebbe stimato con tecniche apposite come il metodo del gomito. Il metodo del gomito (elbow method) è una tecnica pratica per scegliere il numero ottimale di cluster (k) da usare in K-Means, invece di sceglierlo "a occhio" o per tentativi.

4. Addestramento del modello
kmeans.fit(X)

Il metodo fit avvia l'algoritmo, che assegna ogni cliente a un cluster e aggiorna iterativamente la posizione dei centri finché non si stabilizzano.

A ogni iterazione K-Means ricalcola i centri come media dei punti assegnati a ciascun gruppo, ripetendo il processo fino alla convergenza.

5. Visualizzazione dei risultati
print("Cluster assegnati a ciascun cliente:", kmeans.labels_)
print("Centro di ciascun gruppo (spesa media, visite medie):\n", 
      kmeans.cluster_centers_)

Stampiamo l'etichetta di cluster assegnata a ciascun cliente e le coordinate dei centri, che rappresentano il profilo medio di ogni gruppo individuato.

  • labels_ Contiene un numero di cluster (0, 1 o 2) per ciascun cliente.
  • cluster_centers_ Contiene le coordinate medie di spesa e visite per ogni gruppo.

Osservando i centri possiamo interpretare i gruppi a posteriori, ad esempio riconoscendo i clienti occasionali, intermedi e affezionati.

Punto chiave da sottolineare

K-Means scopre da solo la struttura nei dati, senza che nessuno gli dica in anticipo quali siano i gruppi "corretti". L'interpretazione dei cluster (occasionale, intermedio, affezionato) avviene solo dopo, osservando i centri ottenuti.

In poche parole...

Abbiamo usato K-Means per raggruppare automaticamente 8 clienti in 3 gruppi, basandoci solo su spesa media e numero di visite. L'algoritmo non riceve etichette, ma trova da solo pattern e somiglianze nei dati. Il risultato finale sono le assegnazioni di cluster e i centri di ciascun gruppo.

Apprendimento per rinforzo

Torna all’indice

L'apprendimento per rinforzo (RL) è una tipologia di apprendimento automatico in cui un agente interagisce con un ambiente per apprendere una sequenza di azioni che massimizzano una ricompensa cumulativa. A differenza dell'apprendimento supervisionato e non supervisionato, l'RL non si basa su dataset predefiniti, ma apprende attraverso tentativi ed errori.

Concetti chiave

Algoritmi comuni

  1. Q-Learning: Un metodo basato sul valore che apprende la qualità delle azioni.
  2. Deep Q-Network (DQN): Combinano il Q-learning con le reti neurali profonde.
  3. Metodi di gradiente della policy: Ottimizzano direttamente la policy.

Applicazioni

010 base
Apprendimento per rinforzo
Il problema del bandito a più braccia (multi-armed bandit): è considerato la forma più elementare di apprendimento per rinforzo, perché mantiene esplorazione/sfruttamento e aggiornamento della stima.
import numpy as np

# STEP 1: Definisce il problema: 3 leve con probabilità di successo nascoste
n_arms = 3
q_values = np.zeros(n_arms)     # Stima corrente del valore di ogni leva
counts = np.zeros(n_arms)       # Numero di volte che ogni leva è stata scelta
true_rewards = [0.2, 0.5, 0.8]  # Probabilità reali di successo
                                # (sconosciute all'agente)

epsilon = 0.1
episodes = 1000

for i in range(episodes):
    # STEP 2: Sceglie la leva con strategia epsilon-greedy
    if np.random.uniform(0, 1) < epsilon:
        action = np.random.randint(n_arms)   # Esplorazione: leva casuale
    else:
        action = np.argmax(q_values)         # Sfruttamento: leva migliore nota

    # STEP 3: Osserva la ricompensa ottenuta tirando la leva scelta
    reward = 1 if np.random.uniform(0, 1) < true_rewards[action] else 0

    # STEP 4: Aggiorna la stima della leva
    # con la media incrementale delle ricompense
    counts[action] += 1
    q_values[action] += (reward - q_values[action]) / counts[action]

print("Stime finali:", q_values)
print("Leva ritenuta migliore:", np.argmax(q_values))
Stime finali: [0.22033898 0.52941176 0.79823594]
Leva ritenuta migliore: 2

010 Apprendimento per rinforzo - multi-armed bandit

Lezione 01

Il problema del bandito a più braccia è la forma più semplice di apprendimento per rinforzo: l'agente deve scoprire, provando ripetutamente, quale tra alcune opzioni dà in media la ricompensa migliore.

1. Definizione del problema
import numpy as np
# STEP 1: Definisce il problema: 3 leve con probabilità di successo nascoste
n_arms = 3
q_values = np.zeros(n_arms)     # Stima corrente del valore di ogni leva
counts = np.zeros(n_arms)       # Numero di volte che ogni leva è stata scelta
true_rewards = [0.2, 0.5, 0.8]  # Probabilità reali di successo
                                # (sconosciute all'agente)
epsilon = 0.1
episodes = 1000

Immaginiamo 3 'leve' (arms) da tirare, ciascuna con una probabilità nascosta di dare una ricompensa. L'agente non conosce true_rewards: deve scoprirlo tirando le leve e osservando i risultati.

  • q_values conserva la stima corrente di quanto sia 'buona' ogni leva.
  • counts conta quante volte ogni leva è stata scelta, utile per calcolare una media.

A differenza del Q-learning con Q-table, qui non esistono stati diversi: c'è un solo 'stato' e l'unica decisione è quale leva tirare.

2. Scelta della leva: esplorazione vs sfruttamento
if np.random.uniform(0, 1) < epsilon:
    action = np.random.randint(n_arms)   # Esplorazione: leva casuale
else:
    action = np.argmax(q_values)         # Sfruttamento: leva migliore nota

Ad ogni tentativo l'agente deve bilanciare esplorazione e sfruttamento: a volte prova una leva a caso, altre volte sceglie quella che finora sembra la migliore.

  • np.random.randint(n_arms) sceglie una leva a caso tra tutte quelle disponibili.
  • np.argmax(q_values) sceglie la leva con la stima di ricompensa più alta finora.

Con epsilon basso l'agente esplora raramente e tende a ripetere presto la scelta che sembra migliore, anche se potrebbe non esserlo davvero.

3. Osservazione della ricompensa
reward = 1 if np.random.uniform(0, 1) < true_rewards[action] else 0

L'ambiente risponde all'azione scelta: la leva tirata restituisce una ricompensa di 1 (successo) con una probabilità pari al suo valore reale, oppure 0 altrimenti.

Questa riga simula il comportamento imprevedibile del mondo reale: anche la leva migliore non dà sempre una ricompensa, dà solo una ricompensa più probabile rispetto alle altre.

4. Aggiornamento della stima
counts[action] += 1
q_values[action] += (reward - q_values[action]) / counts[action]

La stima della leva scelta viene corretta avvicinandola al risultato appena osservato. Questa formula calcola in modo incrementale la media di tutte le ricompense ottenute finora da quella leva.

  • (reward - q_values[action]) è l'errore tra ciò che ci si aspettava e ciò che si è ottenuto.
  • / counts[action] riduce progressivamente l'ampiezza della correzione man mano che si accumula esperienza.

Questo meccanismo è una versione ridotta dell'equazione di Bellman usata nel Q-learning completo: qui non c'è uno stato successivo da considerare, solo la media delle ricompense passate.

Punto chiave da sottolineare

Anche nella sua forma più semplice, l'apprendimento per rinforzo si basa sempre sullo stesso schema: scegliere un'azione, osservare una ricompensa, correggere le proprie stime. Il bandito a più braccia isola questo meccanismo togliendo la complessità degli stati multipli.

In poche parole...

Il problema del bandito a più braccia mostra in forma minima i due ingredienti chiave del reinforcement learning: la scelta tra esplorare e sfruttare, e l'aggiornamento graduale delle stime in base ai risultati osservati. È il punto di partenza naturale prima di affrontare il Q-learning.

Apprendimento semi-supervisionato

Torna all’indice

L'apprendimento semi-supervisionato è un approccio ibrido che sfrutta una piccola quantità di dati etichettati insieme a una grande quantità di dati non etichettati. Questo approccio è utile quando l'etichettatura dei dati è costosa o richiede molto tempo, ma i dati non etichettati sono abbondanti.

Concetti chiave

Algoritmi comuni

  1. Auto-formazione: Il modello viene addestrato su dati etichettati e successivamente utilizzato per etichettare dati non etichettati in modo iterativo.
  2. Formazione congiunta: Due modelli vengono addestrati su set di caratteristiche diversi e contribuiscono a etichettare i dati l'uno dell'altro.
  3. Metodi basati su grafi: Sfrutta le strutture a grafo per propagare le etichette tra i punti dati.

Applicazioni

011 base
Apprendimento Semi-Supervisionato - Label Propagation
L'apprendimento semi-supervisionato sfrutta pochi dati etichettati insieme a molti dati non etichettati.
from sklearn.semi_supervised import LabelPropagation
import numpy as np

# STEP 2: dataset misto - tre punti etichettati (classe 0) e tre
# non etichettati (-1)
X = np.array([[1, 2], [1, 4], [1, 0], [10, 2], [10, 4], [10, 0]])
y = np.array([0, 0, 0, -1, -1, -1])  # -1 = dato non etichettato

# STEP 3: creazione e addestramento del modello di label propagation
model = LabelPropagation()
model.fit(X, y)

# STEP 4: etichette finali dedotte per tutti i punti
# (compresi quelli non etichettati)
print("Etichette propagate:", model.transduction_)

# Bonus: livello di confidenza del modello per ogni etichetta assegnata
print("Distribuzione di probabilita\':")
print(model.label_distributions_)
Etichette propagate: [0 0 0 0 0 0]
Distribuzione di probabilita':
[[1.]
 [1.]
 [1.]
 [0.]
 [0.]
 [0.]]

011 Apprendimento Semi-Supervisionato: propagare le etichette con LabelPropagation

Lezione 01

L'apprendimento semi-supervisionato sfrutta pochi dati etichettati insieme a molti dati non etichettati per addestrare un modello. In questo esempio usiamo LabelPropagation, un algoritmo che "propaga" le etichette conosciute ai punti vicini nello spazio delle feature.

1. Importazione delle librerie
from sklearn.semi_supervised import LabelPropagation
import numpy as np

Importiamo LabelPropagation, l'algoritmo di apprendimento semi-supervisionato che useremo, e NumPy per gestire i dati in forma di array numerici.

  • LabelPropagation: costruisce un grafo di similarità tra i punti e diffonde le etichette note verso quelli sconosciuti.
  • NumPy: permette di convertire liste Python in array efficienti, il formato che scikit-learn si aspetta in ingresso.

A differenza dell'apprendimento supervisionato classico, qui non serve dividere i dati in train e test: il modello lavora su un unico insieme misto di dati etichettati e non.

2. Creazione del dataset misto
X = np.array([[1, 2], [1, 4], [1, 0], [10, 2], [10, 4], [10, 0]])
y = np.array([0, 0, 0, -1, -1, -1])  # -1 = dato non etichettato

Definiamo sei punti in uno spazio a due dimensioni, raggruppati in due nuvole ben separate: una vicino all'origine e una intorno al valore 10.

  • X: le coordinate dei sei punti, tre per ciascuna delle due nuvole.
  • y: le etichette note (classe 0 per il primo gruppo) e il valore convenzionale -1 per i punti non etichettati.

Il valore -1 è una convenzione di scikit-learn per segnalare "etichetta mancante": l'algoritmo la userà come indizio, non come una vera classe.

3. Addestramento del modello
model = LabelPropagation()
model.fit(X, y)

Il modello costruisce un grafo in cui ogni punto è collegato ai più vicini, e fa "circolare" l'etichetta 0 lungo i collegamenti verso i punti non etichettati più simili.

  • fit(X, y): a differenza della classificazione classica, riceve anche i punti senza etichetta, che partecipano alla costruzione del grafo.
  • Similarità: punti vicini nello spazio delle feature tendono a ricevere la stessa etichetta propagata.

Poiché il secondo gruppo di punti (intorno a x=10) è nettamente separato dal primo, ci aspettiamo che riceva una nuova etichetta diversa da 0, ad esempio 1.

4. Lettura delle etichette propagate
print("Etichette propagate:", model.transduction_)

L'attributo transduction_ contiene le etichette finali assegnate a tutti i punti del dataset, comprese quelle inizialmente segnate come -1.

  • transduction_: array delle etichette dedotte, allineato punto per punto con l'array X originale.
  • Output atteso: qualcosa di simile a [0 0 0 1 1 1], dove il secondo gruppo riceve automaticamente l'etichetta 1.

Il modello non "inventa" un'etichetta qualsiasi: la assegna in base alla distanza dai punti già noti, seguendo la struttura geometrica dei dati.

5. Livello di confidenza del modello
print("Distribuzione di probabilita\':")
print(model.label_distributions_)

Oltre all'etichetta finale, il modello calcola per ogni punto una distribuzione di probabilità tra le classi possibili, che indica quanto è "sicuro" della propagazione.

  • label_distributions_: matrice con una riga per punto e una colonna per classe, con i valori di probabilità stimati.
  • Utilità didattica: permette di mostrare che il modello ragiona per probabilità e non per assegnazione arbitraria.

Valori di probabilità vicini a 1 indicano alta confidenza; valori più bassi segnalano punti "di confine" tra le due nuvole, utili per una discussione in aula sui casi ambigui.

Punto chiave da sottolineare

La forza dell'apprendimento semi-supervisionato sta nello sfruttare la struttura geometrica dei dati non etichettati per estendere poche etichette note. Funziona bene quando i gruppi sono ben separati, come in questo esempio, ma richiede cautela quando le classi si sovrappongono.

In poche parole...

Abbiamo usato LabelPropagation per assegnare automaticamente etichette a punti non classificati, basandoci sulla loro vicinanza a punti già etichettati, osservando anche il livello di confidenza di ogni assegnazione. Questo approccio è utile quando etichettare manualmente tutti i dati è costoso o impossibile.

Un caso reale

012 base
Apprendimento Semi-Supervisionato con LabelSpreading sul Dataset Digits
Apprendimento semi-supervisionato applicato al riconoscimento di cifre scritte a mano.
from sklearn.datasets import load_digits
from sklearn.semi_supervised import LabelSpreading
import numpy as np

# Caricamento del dataset digits (immagini 8x8 di cifre manoscritte)
digits = load_digits()
X = digits.data                # Feature: pixel delle immagini appiattiti
y = np.copy(digits.target)     # Copia delle etichette reali,
                               # per poterle modificare

# Simulazione di uno scenario con pochi dati etichettati
# Generatore casuale riproducibile
rng = np.random.RandomState(42)
# Selezioniamo il 90% dei campioni da "nascondere"
mask = rng.rand(len(y)) < 0.9
# -1 indica etichetta sconosciuta per scikit-learn
y[mask] = -1

n_labeled = np.sum(~mask)
print("Campioni etichettati disponibili:", n_labeled, "su", len(y))

# STEP 3: Creazione e addestramento del modello di propagazione delle etichette
model = LabelSpreading(kernel='knn', n_neighbors=7)
model.fit(X, y)

# STEP 4: Valutazione dell'accuratezza sui campioni originariamente nascosti
accuracy = (model.transduction_[mask] == digits.target[mask]).mean()
print("Accuratezza sui dati originariamente nascosti:", accuracy)
Campioni etichettati disponibili: 181 su 1797
Accuratezza sui dati originariamente nascosti: 0.9665841584158416

012 Apprendimento Semi-Supervisionato con LabelSpreading sul Dataset Digits

Lezione 01

In questo esempio esploriamo l'apprendimento semi-supervisionato applicato al riconoscimento di cifre scritte a mano, simulando uno scenario realistico in cui solo una piccola parte dei dati possiede un'etichetta. Utilizziamo l'algoritmo LabelSpreading per propagare le etichette conosciute verso i campioni non etichettati.

1. Caricamento del dataset e importazione degli strumenti
from sklearn.datasets import load_digits
from sklearn.semi_supervised import LabelSpreading
import numpy as np

# STEP 1: Caricamento del dataset digits (immagini 8x8 di cifre manoscritte)
digits = load_digits()
X = digits.data                # Feature: pixel delle immagini appiattiti
y = np.copy(digits.target)     # Copia delle etichette reali, per poterle modificare

Importiamo il dataset digits, composto da immagini 8x8 di cifre manoscritte, e creiamo una copia dell'array delle etichette originali per poterle modificare senza alterare i dati di riferimento.

  • X contiene le feature numeriche di ogni immagine, ovvero i valori dei pixel appiattiti in un vettore.
  • y è la copia delle etichette reali, che verrà parzialmente "nascosta" per simulare dati poco etichettati.

Usare np.copy() invece di assegnare direttamente y = digits.target è fondamentale: senza la copia, mascherare le etichette modificherebbe anche l'array originale del dataset.

2. Simulazione di dati parzialmente etichettati
# STEP 2: Simulazione di uno scenario con pochi dati etichettati
rng = np.random.RandomState(42)        # Generatore casuale riproducibile
mask = rng.rand(len(y)) < 0.9          # Selezioniamo il 90% dei campioni da "nascondere"
y[mask] = -1                           # -1 indica etichetta sconosciuta per scikit-learn

n_labeled = np.sum(~mask)
print("Campioni etichettati disponibili:", n_labeled, "su", len(y))

Generiamo una maschera booleana casuale che nasconde il 90% delle etichette, sostituendole con il valore -1, convenzione utilizzata da scikit-learn per indicare "etichetta sconosciuta".

  • RandomState(42) garantisce che la selezione casuale sia riproducibile ad ogni esecuzione.
  • mask è un array di booleani che indica quali campioni perderanno la propria etichetta originale.

Abbiamo aggiunto una stampa del numero di campioni ancora etichettati: con solo il 10% di dati etichettati su quasi 1800 immagini, il modello dispone di circa 180 esempi certi da cui partire.

3. Creazione e addestramento del modello LabelSpreading
# Creazione e addestramento del modello di propagazione delle etichette
model = LabelSpreading(kernel='knn', n_neighbors=7)
model.fit(X, y)

Istanziamo LabelSpreading, un algoritmo che costruisce un grafo di similarità tra tutti i campioni e propaga le etichette note lungo le connessioni più forti, aggiornandole iterativamente anche per i punti già etichettati.

  • kernel='knn' costruisce il grafo usando i k vicini più prossimi, spesso più stabile del kernel rbf su dataset ad alta dimensionalità come le immagini.
  • n_neighbors=7 controlla quanti vicini considerare per ogni campione durante la propagazione.

A differenza di LabelPropagation, LabelSpreading introduce un parametro di regolarizzazione che rende l'algoritmo più robusto al rumore, permettendo alle etichette originali di essere leggermente "corrette" durante la diffusione.

La scelta esplicita del kernel knn è una modifica didattica rispetto al kernel rbf di default, che su dati con 64 feature può risultare instabile e meno intuitivo da spiegare.

4. Valutazione dell'accuratezza sui dati nascosti
# Valutazione dell'accuratezza sui campioni originariamente nascosti
accuracy = (model.transduction_[mask] == digits.target[mask]).mean()
print("Accuratezza sui dati originariamente nascosti:", accuracy)

Confrontiamo le etichette assegnate dal modello (transduction_) con le etichette reali originali, ma solo per i campioni che erano stati mascherati, così da misurare quanto bene il modello ha "indovinato" le etichette mancanti.

  • transduction_ è l'attributo che contiene le etichette finali stimate per tutti i campioni, incluso quelli inizialmente nascosti.
  • mask viene riutilizzata per isolare esclusivamente i campioni su cui il modello ha dovuto "indovinare".

Ottenere un'accuratezza elevata con solo il 10% di dati etichettati dimostra il valore pratico del semi-supervisionato in contesti reali, dove etichettare manualmente ogni esempio è costoso o richiede tempo.

Punto chiave da sottolineare

LabelSpreading sfrutta la struttura geometrica dei dati non etichettati per propagare l'informazione da pochi esempi noti a molti sconosciuti. Il parametro kernel e il numero di vicini influenzano fortemente la qualità della propagazione. Anche con solo il 10% di etichette, il modello può raggiungere un'accuratezza sorprendentemente alta.

In poche parole...

Questo esempio mostra come l'apprendimento semi-supervisionato permetta di sfruttare grandi quantità di dati non etichettati insieme a pochi esempi noti. Attraverso LabelSpreading, il dataset digits viene classificato propagando le etichette lungo un grafo di similarità. Il risultato finale è un'accuratezza misurata solo sui dati originariamente nascosti.

Pre-elaborazione dei dati

Torna all’indice

La pre-elaborazione dei dati è una fase cruciale nella pipeline di machine learning. Essa garantisce che i dati utilizzati per addestrare un modello siano puliti, coerenti e strutturati in modo adeguato, migliorando così le prestazioni e l'accuratezza del modello stesso. Approfondiamo quattro aspetti fondamentali della pre-elaborazione dei dati:

  1. Comprendere i tipi di dati
  2. Gestione dei valori mancanti
  3. Normalizzazione e standardizzazione dei dati
  4. Codifica dei dati categorici

Comprendere i tipi di dati

Torna all’indice

I dati si presentano in varie forme e comprendere le tipologie di dati con cui si ha a che fare è fondamentale per scegliere le tecniche di pre-elaborazione e gli algoritmi di machine learning più adeguati.

Tipi di dati

  1. Dati numerici:

    Rappresentano grandezze misurabili.

    • Dati continui: i valori possono assumere qualsiasi numero all'interno di un intervallo (ad es. altezza, temperatura).
    • Dati discreti: I valori sono numerabili e finiti (ad esempio, il numero di studenti in una classe).
  2. Dati categorici:
    • Rappresentano categorie o gruppi.
    • Esempi includono il genere (maschio/femmina) e il colore (rosso/verde/blu/nero).
  3. Dati ordinali:
    • Un tipo di dati categorici in cui le categorie presentano un ordine o una classificazione significativa.
    • Tra gli esempi figurano il livello di istruzione (scuola superiore, laurea di primo livello, laurea magistrale)
  4. Dati testuali:
    • Dati non strutturati sotto forma di testo (ad es. recensioni dei clienti, post sui social media).
  5. Dati di serie storiche:
    • Punti dati raccolti o registrati a intervalli di tempo specifici.
    • Tra gli esempi figurano i prezzi delle azioni e i dati meteorologici.
  6. Dati booleani:
    • Rappresentano risultati binari (Vero/Falso, 0/1).

Importanza dei tipi di dati nella pre-elaborazione

013 base
L'importanza dei tipi di dati nella pre-elaborazione con pandas
pandas inferisce automaticamente i tipi di dati da un dataset
import pandas as pd

# Dataset di esempio con valori mancanti e tipi eterogenei
data = {
    'Age': [25, 30, 35, None],
    'Gender': ['Male', 'Female', 'Male', 'Female'],
    'Salary': [50000, 60000, 70000, None],
    'Married': [True, False, True, None]
}

# Creazione del DataFrame
df = pd.DataFrame(data)

# Ispezione dei tipi di dati inferiti automaticamente da pandas
print("Tipi di dati originali:")
print(df.dtypes)
print()

# Conversione ai tipi di dati "nullable"
# Intero che supporta NaN (pd.NA)
df['Age'] = df['Age'].astype('Int64')
# Categoria per valori discreti ripetuti
df['Gender'] = df['Gender'].astype('category')
# Float nullable esplicito
df['Salary'] = df['Salary'].astype('Float64')
# Booleano che supporta NaN (pd.NA)
df['Married'] = df['Married'].astype('boolean')

# Verifica dei tipi dopo la conversione
print("Tipi di dati dopo la conversione:")
print(df.dtypes)
print()
print(df)
Tipi di dati originali:
Age        float64
Gender      object
Salary     float64
Married     object
dtype: object

Tipi di dati dopo la conversione:
Age           Int64
Gender     category
Salary      Float64
Married     boolean
dtype: object

    Age  Gender   Salary  Married
0    25    Male  50000.0     True
1    30  Female  60000.0    False
2    35    Male  70000.0     True
3    Female          

013 L'importanza dei tipi di dati nella pre-elaborazione con pandas

Lezione 01

In questo esempio vediamo come pandas inferisce automaticamente i tipi di dati da un dataset e perché i valori mancanti possono alterare silenziosamente questi tipi, con conseguenze dirette sulla qualità della pre-elaborazione.

1. Creazione del dataset con valori eterogenei e mancanti
# Dataset di esempio con valori mancanti e tipi eterogenei
data = {
    'Age': [25, 30, 35, None],
    'Gender': ['Male', 'Female', 'Male', 'Female'],
    'Salary': [50000, 60000, 70000, None],
    'Married': [True, False, True, None]
}

Il dizionario data raccoglie quattro colonne con nature diverse: numeriche intere, testuali, numeriche continue e booleane, e in tre di esse compare un valore None per simulare dati reali incompleti.

  • None rappresenta l'assenza di un dato ed è l'elemento che, come vedremo, forza pandas a cambiare il tipo di una colonna.
  • Eterogeneità del dataset: mescolare interi, stringhe, float e booleani nello stesso oggetto è tipico dei dataset reali provenienti da fonti diverse.

Costruire il dataset direttamente in Python, prima di passarlo a pandas, permette di controllare esattamente quali tipi ci si aspetta in ingresso.

Questo confronto tra tipo atteso e tipo effettivamente assegnato da pandas è il cuore didattico dell'esempio.

2. Creazione del DataFrame
# Creazione del DataFrame
df = pd.DataFrame(data)

Il costruttore pd.DataFrame() converte il dizionario in una struttura tabellare, dove ogni chiave diventa una colonna e pandas assegna automaticamente un tipo a ciascuna di esse.

  • DataFrame: struttura dati bidimensionale, simile a un foglio di calcolo, indicizzata per righe e colonne.
  • Inferenza automatica: pandas analizza i valori di ogni colonna per decidere il tipo più adatto, senza alcuna indicazione esplicita da parte nostra.

Questa comodità ha un costo: l'inferenza automatica non sempre produce il tipo più efficiente o più corretto dal punto di vista semantico.

3. Ispezione dei tipi di dati inferiti
# Ispezione dei tipi di dati inferiti automaticamente da pandas
print("Tipi di dati originali:")
print(df.dtypes)

df.dtypes mostra il tipo assegnato a ciascuna colonna: si nota che Age e Salary diventano float64 invece di interi, mentre Married diventa object invece di booleano.

  • float64: NumPy non ha un modo nativo di rappresentare un intero mancante, quindi l'intera colonna viene promossa a virgola mobile per ospitare NaN.
  • object: quando una colonna booleana contiene None, pandas la tratta come una sequenza di oggetti Python generici, perdendo i vantaggi del tipo booleano.

Questo comportamento è silenzioso: nessun errore viene sollevato, ma il significato originale dei dati viene alterato senza che il programmatore se ne accorga.

Per un algoritmo di machine learning, ricevere una colonna object al posto di una booleana o numerica può causare errori o interpretazioni scorrette a valle.

4. Conversione ai tipi di dati nullable
# Conversione ai tipi di dati "nullable"
df['Age'] = df['Age'].astype('Int64')            # Intero che supporta NaN (pd.NA)
df['Gender'] = df['Gender'].astype('category')   # Categoria per valori discreti ripetuti
df['Salary'] = df['Salary'].astype('Float64')    # Float nullable esplicito
df['Married'] = df['Married'].astype('boolean')  # Booleano che supporta NaN (pd.NA)

Pandas mette a disposizione tipi di dati "nullable" (con la lettera maiuscola, come Int64 o boolean) che mantengono il tipo semantico corretto anche in presenza di valori mancanti.

  • Int64/boolean nullable: usano internamente pd.NA al posto di NaN, evitando la promozione forzata a float64 o object.
  • category: per colonne con valori ripetuti come Gender, riduce l'uso di memoria e rende esplicita la natura discreta della variabile.

Questa conversione esplicita restituisce al dataset il significato originale dei dati, che l'inferenza automatica di pandas aveva alterato.

È una pratica fondamentale in fase di pre-elaborazione, prima di passare i dati a un encoder o a un modello di scikit-learn.

5. Verifica dei tipi dopo la conversione
# Verifica dei tipi dopo la conversione
print("Tipi di dati dopo la conversione:")
print(df.dtypes)
print()
print(df)

Ripetendo l'ispezione con df.dtypes si osserva che ogni colonna ora riporta il tipo semanticamente corretto, mentre i valori mancanti restano visibili come senza forzare conversioni indesiderate.

  • <NA>: rappresentazione uniforme del valore mancante, indipendente dal tipo di colonna in cui compare.
  • Coerenza tra tipo dichiarato e tipo effettivo: condizione necessaria per pipeline di pre-elaborazione affidabili.

Questo passaggio finale conferma che la scelta consapevole dei tipi di dati è parte integrante, e non accessoria, della pre-elaborazione.

Punto chiave da sottolineare

I tipi di dati non sono un dettaglio tecnico marginale: pandas li inferisce automaticamente, ma i valori mancanti possono alterarli silenziosamente (interi che diventano float, booleani che diventano oggetti). Controllare e correggere esplicitamente i tipi è un passo essenziale della pre-elaborazione.

In poche parole...

Questo esempio mostra come un dataset misto con valori mancanti venga caricato in un DataFrame, come i tipi originali vengano ispezionati con dtypes, e come i tipi "nullable" di pandas permettano di correggere le distorsioni introdotte dall'inferenza automatica, restituendo ai dati il loro significato corretto.

Gestione dei valori mancanti

Torna all’indice

I valori mancanti rappresentano un problema comune nei dataset e possono influire in modo significativo sulle prestazioni di un modello di machine learning. Una gestione adeguata dei valori mancanti è fondamentale per costruire modelli robusti.

Cause dei valori mancanti

  1. Errori di inserimento dati
  2. Malfunzionamento dell'apparecchiatura
  3. Preoccupazioni relative alla privacy che portano a risposte incomplete
  4. Problemi di unione dei dati

Identificazione dei valori mancanti

014 base
Identificazione e Gestione dei Valori Mancanti in un DataFrame Pandas
Impariamo a individuarli, contarli e gestirli con le tecniche più diffuse offerte da pandas.
import pandas as pd

# Dataset di esempio con valori mancanti e tipi eterogenei
data = {
    'Age': [25, 30, 35, None],
    'Gender': ['Male', 'Female', 'Male', 'Female'],
    'Salary': [50000, 60000, 70000, None],
    'Married': [True, False, True, None]
}

# Creazione del DataFrame a partire dal dizionario
df = pd.DataFrame(data)
print("DataFrame originale:")
print(df)

# Identificazione dei valori mancanti: matrice booleana True/False
print("\nMatrice dei valori mancanti:")
print(df.isnull())

# Conteggio dei valori mancanti per ciascuna colonna
print("\nConteggio valori mancanti per colonna:")
print(df.isnull().sum())

# Rimozione delle righe con almeno un valore mancante
df_dropped = df.dropna()
print("\nDataFrame dopo dropna():")
print(df_dropped)

# Sostituzione dei valori mancanti con strategie differenziate per colonna
# Sostituzione dei valori mancanti con valori di default
df_filled = df.copy()
df_filled['Age'] = df_filled['Age'].fillna(df_filled['Age'].mean())
df_filled['Salary'] = df_filled['Salary'].fillna(df_filled['Salary'].median())
# Conversione esplicita al tipo nullable 'boolean' prima del fillna
# per evitare il FutureWarning sul downcasting implicito di pandas
df_filled['Married'] = df_filled['Married'].astype('boolean').fillna(False)
print(df_filled)
DataFrame originale:
    Age  Gender   Salary Married
0  25.0    Male  50000.0    True
1  30.0  Female  60000.0   False
2  35.0    Male  70000.0    True
3   NaN  Female      NaN    None

Matrice dei valori mancanti:
     Age  Gender  Salary  Married
0  False   False   False    False
1  False   False   False    False
2  False   False   False    False
3   True   False    True     True

Conteggio valori mancanti per colonna:
Age        1
Gender     0
Salary     1
Married    1
dtype: int64

DataFrame dopo dropna():
    Age  Gender   Salary Married
0  25.0    Male  50000.0    True
1  30.0  Female  60000.0   False
2  35.0    Male  70000.0    True
    Age  Gender   Salary  Married
0  25.0    Male  50000.0     True
1  30.0  Female  60000.0    False
2  35.0    Male  70000.0     True
3  30.0  Female  60000.0    False

014 Identificazione e Gestione dei Valori Mancanti in un DataFrame Pandas

Lezione 01

I valori mancanti sono uno dei problemi più comuni nei dataset reali e possono compromettere l'affidabilità di un modello di Machine Learning. In questo esempio impariamo a individuarli, contarli e gestirli con le tecniche più diffuse offerte da pandas.

1. Creazione del dataset con valori mancanti
import pandas as pd

# Dataset di esempio con valori mancanti e tipi eterogenei
data = {
    'Age': [25, 30, 35, None],
    'Gender': ['Male', 'Female', 'Male', 'Female'],
    'Salary': [50000, 60000, 70000, None],
    'Married': [True, False, True, None]
}

Il dizionario data rappresenta un piccolo dataset eterogeneo, con colonne numeriche, testuali e booleane. I valori mancanti sono indicati con None, il valore nullo standard di Python.

  • Eterogeneità dei tipi: il dataset combina numeri, stringhe e booleani per mostrare che i dati mancanti riguardano ogni tipo di colonna.
  • None come segnaposto: pandas convertirà automaticamente None nel valore NaN (o equivalente) più adatto al tipo di dato della colonna.

Nella pratica i valori mancanti derivano da errori di inserimento, sensori guasti o campi facoltativi non compilati. Riconoscerli è il primo passo per un'analisi corretta.

2. Creazione e visualizzazione del DataFrame
# Creazione del DataFrame a partire dal dizionario
df = pd.DataFrame(data)
print("DataFrame originale:")
print(df)

Il dizionario viene trasformato in un DataFrame, la struttura dati tabellare di pandas. Stampandolo si osserva che i valori mancanti compaiono come NaN o None a seconda della colonna.

  • Colonna Age: essendo numerica, il None viene convertito in NaN (float).
  • Colonna Married: essendo booleana con un valore mancante, pandas converte l'intera colonna in tipo object.

Questo comportamento è importante da conoscere: la presenza di un solo valore mancante può cambiare il tipo di dato di un'intera colonna, con effetti su calcoli e confronti successivi.

3. Identificazione dei valori mancanti
# Identificazione dei valori mancanti: matrice booleana True/False
print("\nMatrice dei valori mancanti:")
print(df.isnull())

Il metodo isnull() restituisce un DataFrame delle stesse dimensioni dell'originale, con valori booleani: True dove il dato è mancante, False altrimenti.

  • Metodo equivalente: isna() produce esattamente lo stesso risultato ed è spesso usato in modo intercambiabile.
  • Uso combinato: questa maschera booleana può essere usata per filtrare o selezionare le righe con dati mancanti.

Visualizzare l'intera matrice booleana è utile su dataset piccoli, ma diventa poco pratico su dataset di grandi dimensioni, dove conviene passare a un riepilogo numerico.

4. Conteggio dei valori mancanti per colonna
# Conteggio dei valori mancanti per ciascuna colonna
print("\nConteggio valori mancanti per colonna:")
print(df.isnull().sum())

Concatenando sum() a isnull() si ottiene il numero totale di valori mancanti per ciascuna colonna, poiché True viene interpretato come 1 e False come 0.

  • Vista d'insieme: in una sola riga di codice si individuano subito le colonne più problematiche.
  • Percentuale: dividendo per len(df) si può ottenere la percentuale di dati mancanti per colonna.

Questo è tipicamente il primo controllo da eseguire su un nuovo dataset, prima di decidere come trattare i valori mancanti.

5. Rimozione delle righe con valori mancanti
# Rimozione delle righe con almeno un valore mancante
df_dropped = df.dropna()
print("\nDataFrame dopo dropna():")
print(df_dropped)

Il metodo dropna() elimina tutte le righe che contengono almeno un valore mancante, restituendo un nuovo DataFrame senza modificare l'originale.

  • Parametro axis: con axis=1 si eliminano le colonne invece delle righe.
  • Parametro thresh: permette di mantenere righe che hanno almeno un certo numero di valori non nulli.

Questo approccio è semplice ma può causare una significativa perdita di informazione, specialmente su dataset piccoli o con molti valori mancanti sparsi.

6. Sostituzione dei valori mancanti
# Sostituzione dei valori mancanti con strategie differenziate per colonna
# Sostituzione dei valori mancanti con valori di default
df_filled = df.copy()
df_filled['Age'] = df_filled['Age'].fillna(df_filled['Age'].mean())
df_filled['Salary'] = df_filled['Salary'].fillna(df_filled['Salary'].median())
# Conversione esplicita al tipo nullable 'boolean' prima del fillna
# per evitare il FutureWarning sul downcasting implicito di pandas
df_filled['Married'] = df_filled['Married'].astype('boolean').fillna(False)
print(df_filled)

Il metodo fillna() sostituisce i valori mancanti con un valore scelto: qui si usano media, mediana e, per la colonna booleana, una conversione esplicita al tipo nullable prima della sostituzione.

  • Media vs mediana: la mediana è preferibile in presenza di valori estremi (outlier) che distorcerebbero la media.
  • Tipo nullable boolean: convertire esplicitamente con astype('boolean') evita conversioni implicite ambigue e rende il codice compatibile con le versioni future di pandas.

Le versioni recenti di pandas stanno progressivamente eliminando le conversioni di tipo "silenziose": dichiarare esplicitamente i tipi è oggi una buona pratica.

Punto chiave da sottolineare

Individuare i valori mancanti con isnull() e sum() è solo il primo passo: la vera decisione riguarda come gestirli, scegliendo tra rimozione (dropna()) e sostituzione (fillna()) in base all'impatto sulla qualità dei dati.

In poche parole...

In questo esempio abbiamo creato un dataset eterogeneo con valori mancanti, li abbiamo individuati con isnull(), contati con sum() e infine gestiti tramite dropna() e fillna(), confrontando i due approcci.

Strategie per gestire i valori mancanti

  1. Rimuovere valori mancanti:
    • Eliminare righe o colonne con dati mancanti.
    • Indicato quando la percentuale di dati mancanti è ridotta.
    # Elimina le righe con valori mancanti
    df_cleaned = df.dropna()
  2. Imputazione:
    • Sostituire i valori mancanti con sostituti appropriati.
    • Tecniche:
      • Imputazione basata su media, mediana o moda per dati numerici.
      • Valore costante o categoria più frequente per i dati categorici.
      # Riempire i valori numerici mancanti con la media
      df['Age'] = df['Age'].fillna(df['Age'].mean())

      Il metodo mean() calcola la media aritmetica dei valori non nulli della colonna 'Age', e fillna() la usa per sostituire ogni valore mancante.

      # Riempire i valori categorici mancanti con la moda
      df['Gender'] = df['Gender'].fillna(df['Gender'].mode()[0])

      mode() restituisce tutti i valori più frequenti della colonna sotto forma di Series; per questo si usa [0] per prendere il primo valore modale e passarlo a fillna()

  3. Tecniche avanzate di imputazione:
    • Utilizzare algoritmi di apprendimento automatico per prevedere i valori mancanti.
    • Librerie come sklearn.impute offrono strumenti quali KNNImputer.

Normalizzazione e standardizzazione dei dati

Torna all’indice

La normalizzazione e la standardizzazione sono tecniche utilizzate per scalare le caratteristiche numeriche, in modo che contribuiscano in misura uguale a un modello di machine learning. Ciò è particolarmente importante per gli algoritmi sensibili all'ordine di grandezza delle caratteristiche.

Normalizzazione

La normalizzazione scala i dati in un intervallo specifico, solitamente [0, 1]. È utile quando si desidera che tutte le caratteristiche abbiano la stessa scala, senza distorcere le differenze all'interno dell'intervallo.

015 base
Normalizzazione dei dati con MinMaxScaler in scikit-learn
La normalizzazione riporta i valori numerici di un dataset entro un intervallo comune.
import numpy as np
from sklearn.preprocessing import MinMaxScaler

# Dati di esempio: due feature con scale molto diverse tra loro
# Colonna 1: valori piccoli (1-5), Colonna 2: valori grandi (900-1800)
dati = np.array([
    [1, 1000],
    [2, 1500],
    [3, 1200],
    [4, 1800],
    [5, 900]
])

# Creazione dello scaler con range di normalizzazione esplicito [0, 1]
scaler = MinMaxScaler(feature_range=(0, 1))

# fit: calcola min e max per colonna
# transform: applica la formula (x - min) / (max - min)
dati_normalizzati = scaler.fit_transform(dati)

print("Dati originali:\n", dati)
print("\nDati normalizzati:\n", dati_normalizzati)

# Verifica: è possibile tornare ai valori originali grazie ai parametri
# appresi durante il fit (min e max di ogni colonna)
dati_originali_ricostruiti = scaler.inverse_transform(dati_normalizzati)
print("\nDati ricostruiti (inverse_transform):\n", dati_originali_ricostruiti)
Dati originali:
 [[   1 1000]
 [   2 1500]
 [   3 1200]
 [   4 1800]
 [   5  900]]

Dati normalizzati:
 [[0.         0.11111111]
 [0.25       0.66666667]
 [0.5        0.33333333]
 [0.75       1.        ]
 [1.         0.        ]]

Dati ricostruiti (inverse_transform):
 [[1.0e+00 1.0e+03]
 [2.0e+00 1.5e+03]
 [3.0e+00 1.2e+03]
 [4.0e+00 1.8e+03]
 [5.0e+00 9.0e+02]]

015 Normalizzazione dei dati con MinMaxScaler in scikit-learn

Lezione 01

La normalizzazione riporta i valori numerici di un dataset entro un intervallo comune, tipicamente [0, 1]. Questo passaggio è fondamentale quando le feature hanno scale molto diverse tra loro, per evitare che alcune dominino l'apprendimento del modello.

1. Importazione delle librerie necessarie
import numpy as np
from sklearn.preprocessing import MinMaxScaler

Importiamo NumPy per gestire i dati in forma di array e MinMaxScaler da scikit-learn, la classe che si occupa della normalizzazione.

  • numpy: consente di rappresentare i dati come array multidimensionali, formato richiesto da scikit-learn
  • MinMaxScaler: implementa la normalizzazione min-max, riportando i dati in un intervallo definito
2. Creazione di un dataset con scale diverse
# Dati di esempio: due feature con scale molto diverse tra loro
# Colonna 1: valori piccoli (1-5), Colonna 2: valori grandi (900-1800)
dati = np.array([
    [1, 1000],
    [2, 1500],
    [3, 1200],
    [4, 1800],
    [5, 900]
])

Utilizziamo un dataset con due feature su scale molto diverse, per mostrare in modo realistico perché la normalizzazione sia necessaria.

  • Prima colonna: valori compresi tra 1 e 5.
  • Seconda colonna: valori compresi tra 900 e 1800.

Senza normalizzazione, un algoritmo basato sulla distanza (come KNN) darebbe più peso alla seconda colonna solo perché i suoi valori sono numericamente più grandi.

3. Creazione dello scaler
# Creazione dello scaler con range di normalizzazione esplicito [0, 1]
scaler = MinMaxScaler(feature_range=(0, 1))

Istanziamo MinMaxScaler specificando esplicitamente il parametro feature_range, ovvero l'intervallo di destinazione dei dati normalizzati.

  • feature_range=(0, 1): è il valore di default, ma renderlo esplicito aiuta a capire cosa si sta configurando.
  • scaler: a questo punto non ha ancora "visto" i dati, serve solo a definire la strategia di trasformazione.
4. Calcolo e applicazione della normalizzazione
# fit: calcola min e max per colonna
# transform: applica la formula (x - min) / (max - min)
dati_normalizzati = scaler.fit_transform(dati)

print("Dati originali:\n", dati)
print("\nDati normalizzati:\n", dati_normalizzati)

fit_transform esegue due operazioni in sequenza: calcola il minimo e il massimo di ogni colonna, poi applica la formula di normalizzazione a ciascun valore.

  • fit: apprende min e max da ogni colonna del dataset.
  • transform: applica la formula (x - min) / (max - min) a ogni valore.

Ogni colonna viene normalizzata in modo indipendente: il minimo e il massimo della prima colonna non influenzano la seconda, e viceversa.

5. Verifica con inverse_transform
# Verifica: è possibile tornare ai valori originali grazie ai parametri
# appresi durante il fit (min e max di ogni colonna)
dati_originali_ricostruiti = scaler.inverse_transform(dati_normalizzati)
print("\nDati ricostruiti (inverse_transform):\n", dati_originali_ricostruiti)

Lo scaler memorizza i parametri appresi (min e max), quindi è in grado di invertire la trasformazione e restituire i valori nella scala originale.

  • inverse_transform: utile per interpretare le predizioni di un modello riportandole alla scala reale.
  • Reversibilità: conferma che la normalizzazione non fa perdere informazione, ma solo riscala i valori.
Punto chiave da sottolineare

MinMaxScaler riscala ogni feature in modo indipendente secondo la formula (x - min) / (max - min), portando i valori nell'intervallo [0, 1]. È sensibile agli outlier, perché un valore estremo comprime tutti gli altri valori verso un piccolo sotto-intervallo.

In poche parole...

Abbiamo normalizzato un dataset con feature su scale diverse usando MinMaxScaler, osservando come fit calcoli min e max, transform applichi la formula, e inverse_transform permetta di tornare ai dati originali. Questo passaggio prepara i dati per algoritmi sensibili alla scala delle feature.

Standardizzazione

La standardizzazione scala i dati in modo che abbiano una media pari a 0 e una deviazione standard pari a 1. È utile quando i dati seguono una distribuzione gaussiana o quando gli algoritmi presuppongono una distribuzione normale standard.

016 base
Standardizzazione dei dati con StandardScaler di scikit-learn
from sklearn.preprocessing import StandardScaler
import numpy as np

# Dati di esempio: punteggi ottenuti da 5 studenti in un test (scala 0-100)
data = [[50], [60], [70], [80], [90]]

# Convertiamo in array numpy per calcolare statistiche descrittive
data_array = np.array(data)

# Calcoliamo media e deviazione standard originali,
# utili per il confronto successivo
print(f"Media originale: {data_array.mean():.2f}")
print(f"Deviazione standard originale: {data_array.std():.2f}")

# Creiamo l'oggetto StandardScaler, che si occuperà di standardizzare i dati
scaler = StandardScaler()

# fit_transform calcola media e deviazione standard dai dati (fit)
# e applica subito la trasformazione (transform)
standardized_data = scaler.fit_transform(data)

print("Dati standardizzati:")
print(standardized_data)

# Verifichiamo che i dati standardizzati abbiano media 0 e
# deviazione standard 1
print(f"Media dopo standardizzazione: {standardized_data.mean():.2f}")
print(f"Deviazione standard dopo standardizzazione:" +
  f"{standardized_data.std():.2f}")
Media originale: 70.00
Deviazione standard originale: 14.14
Dati standardizzati:
[[-1.41421356]
 [-0.70710678]
 [ 0.        ]
 [ 0.70710678]
 [ 1.41421356]]
Media dopo standardizzazione: 0.00
Deviazione standard dopo standardizzazione:1.00

016 Standardizzazione dei dati con StandardScaler di scikit-learn

Lezione 01

La standardizzazione trasforma i dati in modo che abbiano media 0 e deviazione standard 1. Questo passaggio è fondamentale per molti algoritmi di machine learning sensibili alla scala delle variabili. In questo esempio vediamo come applicarla con StandardScaler di scikit-learn.

1. Importazione delle librerie necessarie
from sklearn.preprocessing import StandardScaler
import numpy as np

Importiamo StandardScaler dal modulo preprocessing di scikit-learn, la classe che si occupa della standardizzazione, insieme a NumPy per calcolare statistiche descrittive sui dati.

  • StandardScaler è la classe di scikit-learn dedicata alla standardizzazione dei dati.
  • NumPy ci permette di calcolare media e deviazione standard per verificare i risultati.

Separare l'importazione delle librerie dal resto del codice è una buona pratica che rende lo script più leggibile e organizzato.

2. Preparazione dei dati e statistiche originali
# Convertiamo in array numpy per calcolare facilmente statistiche descrittive
data_array = np.array(data)

# Calcoliamo media e deviazione standard originali, utili per il confronto successivo
print(f"Media originale: {data_array.mean():.2f}")
print(f"Deviazione standard originale: {data_array.std():.2f}")

Definiamo i dati di esempio come una lista di liste, formato richiesto da scikit-learn, e calcoliamo media e deviazione standard originali per avere un termine di confronto.

  • I dati rappresentano punteggi di un test compresi tra 50 e 90, con media 70.
  • Ogni valore è racchiuso in una lista perché scikit-learn si aspetta un array bidimensionale (righe = campioni, colonne = feature).

Conoscere le statistiche originali ci permette di verificare concretamente l'effetto della standardizzazione una volta applicata.

3. Creazione dell'oggetto StandardScaler
# Creiamo l'oggetto StandardScaler, che si occuperà di standardizzare i dati
scaler = StandardScaler()

Istanziamo l'oggetto StandardScaler, che a questo punto non ha ancora elaborato alcun dato: memorizzerà media e deviazione standard solo al momento del fit.

  • Lo scaler segue lo stesso pattern fit / transform comune a molti strumenti di scikit-learn.
  • Una volta addestrato, lo stesso scaler può essere riutilizzato per standardizzare nuovi dati con gli stessi parametri.

Questa caratteristica è particolarmente utile in un flusso di lavoro reale, dove i dati di test devono essere standardizzati con i parametri calcolati sul training set, non con parametri propri.

4. Applicazione della standardizzazione con fit_transform
# fit_transform calcola media e deviazione standard dai dati (fit) e applica subito la trasformazione (transform)
standardized_data = scaler.fit_transform(data)

print("Dati standardizzati:")
print(standardized_data)

Il metodo fit_transform esegue in un solo passaggio due operazioni: calcola media e deviazione standard dai dati (fit) e applica subito la formula di standardizzazione (transform).

  • fit calcola i parametri statistici necessari (media e deviazione standard).
  • transform applica la formula z = (x - media) / deviazione standard a ciascun valore.

Se in futuro si dispone già di uno scaler addestrato, è possibile usare solo transform per applicare la stessa trasformazione a nuovi dati, senza ricalcolare i parametri.

5. Verifica del risultato ottenuto
# Verifichiamo che i dati standardizzati abbiano media 0 e deviazione standard 1
print(f"Media dopo standardizzazione: {standardized_data.mean():.2f}")
print(f"Deviazione standard dopo standardizzazione: {standardized_data.std():.2f}")

Verifichiamo che la trasformazione abbia funzionato correttamente, controllando che i dati standardizzati abbiano effettivamente media 0 e deviazione standard 1.

  • Una media pari a 0 indica che i valori sono centrati attorno all'origine.
  • Una deviazione standard pari a 1 indica che i valori sono espressi in unità di deviazione standard rispetto alla media.

Questo controllo finale è una buona abitudine: verificare numericamente che una trasformazione teorica produca il risultato atteso.

Punto chiave da sottolineare

La standardizzazione trasforma i dati secondo la formula z = (x - media) / deviazione standard, portando ogni feature ad avere media 0 e deviazione standard 1. È indispensabile per algoritmi come SVM, regressione logistica o reti neurali, sensibili alla scala delle variabili in input.

In poche parole...

Abbiamo standardizzato una serie di punteggi usando StandardScaler di scikit-learn. Il metodo fit_transform calcola i parametri statistici e applica la trasformazione in un solo passaggio. La verifica finale ha confermato media 0 e deviazione standard 1 sui dati trasformati.

Quando utilizzare la Normalizzazione e la Standardizzazione

Codifica dei dati categorici

Torna all’indice

I modelli di machine learning operano con dati numerici; pertanto, i dati categorici devono essere codificati in formato numerico. Esistono diverse tecniche di codifica, ciascuna adatta a specifiche tipologie di dati.

  1. Codifica delle etichette
    • Assegna un valore numerico univoco a ciascuna categoria.
    • Adatto per dati ordinali.
    017 base
    Codifica delle Etichette con LabelEncoder: Trasformare Categorie in Numeri
    from sklearn.preprocessing import LabelEncoder
    
    # Preparazione dei dati
    # Definiamo una lista di dati categorici testuali (genere), tipico esempio
    # di variabile nominale che deve essere convertita in formato numerico
    # prima di essere utilizzata da un algoritmo di Machine Learning
    generi = ['Male', 'Female', 'Female', 'Male']
    
    # Creazione dell'encoder
    # Istanziamo l'oggetto LabelEncoder. In questa fase l'oggetto non ha
    # ancora appreso nulla: non conosce ancora le categorie presenti nei dati
    label_encoder = LabelEncoder()
    
    # Addestramento e trasformazione
    # fit_transform esegue due operazioni in un unico passaggio:
    # 1) fit: individua le categorie uniche e le ordina alfabeticamente
    # 2) transform: assegna a ciascuna categoria un numero intero progressivo
    etichette_codificate = label_encoder.fit_transform(generi)
    print("Etichette codificate:", etichette_codificate)
    
    # L'attributo classes_ mostra le categorie originali nell'ordine
    # utilizzato per la codifica (ordine alfabetico crescente)
    print("Classi individuate:", label_encoder.classes_)
    
    # Mappatura leggibile e decodifica inversa
    # Costruiamo un dizionario esplicito per rendere subito chiara
    # la corrispondenza tra ciascuna categoria testuale e il relativo codice numerico
    mappatura = dict(zip(label_encoder.classes_, label_encoder.transform(label_encoder.classes_)))
    print("Mappatura classe -> numero:", mappatura)
    
    # inverse_transform riporta i valori numerici alle etichette testuali originali
    # operazione fondamentale per interpretare le predizioni di un modello
    decodificato = label_encoder.inverse_transform(etichette_codificate)
    print("Valori decodificati:", decodificato)
    
    # Nota didattica: con solo due categorie l'ordine numerico non crea ambiguità,
    # ma con tre o più categorie nominali (es. 'Rosso', 'Verde', 'Blu') l'ordine
    # alfabetico implicito potrebbe essere erroneamente interpretato come una
    # relazione d'ordine dall'algoritmo: in quei casi è preferibile OneHotEncoder
    Etichette codificate: [1 0 0 1]
    Classi individuate: ['Female' 'Male']
    Mappatura classe -> numero: {np.str_('Female'): np.int64(0), np.str_('Male'): np.int64(1)}
    Valori decodificati: ['Male' 'Female' 'Female' 'Male']

    017 Codifica delle Etichette con LabelEncoder: Trasformare Categorie in Numeri

    Lezione 01

    Molti algoritmi di Machine Learning richiedono input numerici e non sono in grado di lavorare direttamente con valori testuali. LabelEncoder di scikit-learn risolve questo problema trasformando le categorie testuali in numeri interi. Vedremo come applicarlo e come interpretarne correttamente i risultati.

    1. Importazione e preparazione dei dati
    from sklearn.preprocessing import LabelEncoder
    
    generi = ['Male', 'Female', 'Female', 'Male']

    Importiamo la classe LabelEncoder dal modulo di preprocessing di scikit-learn e definiamo una lista di dati categorici testuali, in questo caso valori relativi al genere.

    • LabelEncoder è una classe pensata per trasformare etichette categoriche in valori numerici interi.
    • generi è una lista Python contenente stringhe ripetute, che rappresentano le categorie da codificare.

    I dati testuali sono comuni nei dataset reali, ma la maggior parte degli algoritmi di Machine Learning richiede input numerici per poter effettuare calcoli.

    Prima di applicare qualsiasi codifica è buona norma osservare quali e quante categorie distinte sono presenti nei dati.

    2. Creazione dell'encoder
    # Creazione dell'oggetto LabelEncoder
    label_encoder = LabelEncoder()

    Creiamo un'istanza di LabelEncoder. In questa fase l'oggetto non ha ancora appreso nulla dai dati: si tratta semplicemente dell'inizializzazione dello strumento che utilizzeremo per la codifica.

    • label_encoder è l'oggetto che memorizzerà la corrispondenza tra categorie e numeri una volta addestrato.
    • LabelEncoder() non richiede parametri: la logica di codifica è sempre la stessa, basata sull'ordine alfabetico delle categorie.

    Come per molti trasformatori di scikit-learn, la creazione dell'oggetto e il suo addestramento sono due fasi distinte e separate.

    3. Addestramento e trasformazione con fit_transform
    # Addestramento e trasformazione in un unico passaggio
    etichette_codificate = label_encoder.fit_transform(generi)
    print("Etichette codificate:", etichette_codificate)
    
    # Visualizzazione delle classi individuate, in ordine alfabetico
    print("Classi individuate:", label_encoder.classes_)

    Il metodo fit_transform esegue due operazioni in sequenza: apprende le categorie uniche presenti nei dati (fit) e le converte immediatamente in numeri interi (transform).

    • fit_transform assegna un numero intero crescente a ciascuna categoria unica, seguendo l'ordine alfabetico.
    • classes_ è un attributo che, dopo l'addestramento, contiene l'elenco delle categorie originali nell'ordine usato per la codifica.

    Nel nostro esempio 'Female' viene mappato a 0 e 'Male' a 1, semplicemente perché F precede M nell'ordine alfabetico.

    È importante ricordare che l'ordine numerico assegnato non riflette alcuna gerarchia reale tra le categorie: è puramente convenzionale.

    4. Mappatura leggibile e decodifica inversa
    # Creazione di un dizionario di mappatura per maggiore chiarezza
    mappatura = dict(zip(label_encoder.classes_, label_encoder.transform(label_encoder.classes_)))
    print("Mappatura classe -> numero:", mappatura)
    
    # Decodifica: trasformazione inversa da numero a categoria originale
    decodificato = label_encoder.inverse_transform(etichette_codificate)
    print("Valori decodificati:", decodificato)

    Costruiamo un dizionario esplicito per rendere subito visibile la corrispondenza tra ciascuna categoria e il numero assegnato, e utilizziamo inverse_transform per tornare dai numeri alle etichette originali.

    • mappatura è un dizionario che associa ogni categoria testuale al proprio codice numerico, utile a scopo didattico e di debug.
    • inverse_transform permette di recuperare i valori testuali originali a partire dai numeri codificati, operazione utile quando si interpretano le predizioni di un modello.

    Questa operazione è particolarmente importante quando LabelEncoder viene usato sulla variabile target: dopo la predizione occorre spesso ridecodificare il risultato in un'etichetta leggibile.

    Con più di due categorie, l'ordine numerico implicito da LabelEncoder può indurre un algoritmo a interpretare una relazione d'ordine inesistente tra le classi.

    Punto chiave da sottolineare

    LabelEncoder assegna numeri interi seguendo l'ordine alfabetico delle categorie, senza alcun significato ordinale reale. È ideale per variabili binarie o per la codifica della variabile target, ma va usato con cautela sulle feature con più di due categorie nominali, dove è preferibile OneHotEncoder.

    In poche parole...

    Abbiamo importato e istanziato LabelEncoder, applicato fit_transform per codificare dati testuali in numeri, ispezionato le classi individuate tramite classes_ e infine utilizzato inverse_transform per tornare ai valori originali, un passaggio chiave per interpretare correttamente i risultati di un modello.

  2. Codifica One-Hot
    • Converte ciascuna categoria in un vettore binario.
    • Adatto per dati nominali (non ordinali).
    018 base
    Codifica delle Variabili Categoriche con OneHotEncoder
    OneHotEncoder gestisce valori testuali trasformando ogni categoria in un vettore binario.
    from sklearn.preprocessing import OneHotEncoder
    import numpy as np
    
    # Dati di esempio
    # Colore di alcuni prodotti: variabile categorica nominale (nessun ordine)
    dati = np.array(['Rosso', 'Blu', 'Verde', 'Blu'])[:, None]  # reshape a colonna 2D
    
    # Creazione e addestramento dell'encoder
    one_hot_encoder = OneHotEncoder(sparse_output=False)
    one_hot_encoder.fit(dati)  # apprende le categorie uniche presenti nei dati
    
    # Trasformazione e ispezione del risultato
    dati_codificati = one_hot_encoder.transform(dati)
    nomi_colonne = one_hot_encoder.get_feature_names_out(['colore'])
    
    print("Colonne generate:", nomi_colonne)
    print("Dati codificati:\n", dati_codificati)
    
    # Gestione di categorie sconosciute in nuovi dati
    encoder_robusto = OneHotEncoder(sparse_output=False, handle_unknown='ignore')
    encoder_robusto.fit(dati)
    
    nuovi_dati = np.array(['Giallo'])[:, None]  # categoria mai vista in training
    print("Categoria sconosciuta codificata:\n", encoder_robusto.transform(nuovi_dati))
    Colonne generate: ['colore_Blu' 'colore_Rosso' 'colore_Verde']
    Dati codificati:
     [[0. 1. 0.]
     [1. 0. 0.]
     [0. 0. 1.]
     [1. 0. 0.]]
    Categoria sconosciuta codificata:
     [[0. 0. 0.]]

    018 Codifica delle Variabili Categoriche con OneHotEncoder

    Lezione 01

    Molti algoritmi di machine learning richiedono input numerici e non sanno gestire direttamente valori testuali come "Rosso" o "Blu". OneHotEncoder risolve il problema trasformando ogni categoria in un vettore binario, senza introdurre relazioni d'ordine inesistenti tra i valori.

    1. Preparazione dei dati di esempio
    from sklearn.preprocessing import OneHotEncoder
    import numpy as np
    
    # Dati di esempio: il colore di alcuni prodotti in magazzino
    dati = np.array(['Rosso', 'Blu', 'Verde', 'Blu'])[:, None]

    Partiamo da una variabile categorica nominale, cioè priva di un ordine naturale tra i valori. L'operazione [:, None] trasforma l'array monodimensionale in una colonna a due dimensioni, formato richiesto da scikit-learn per le trasformazioni.

    • Variabile nominale: i colori non hanno una gerarchia, "Verde" non è maggiore o minore di "Rosso".
    • Reshape a due dimensioni: [:, None] converte l'array da forma (4,) a (4, 1), come richiesto dall'encoder.

    Abbiamo inserito il valore "Blu" due volte per rendere l'esempio più realistico: nei dati reali le categorie si ripetono spesso.

    2. Creazione e addestramento dell'encoder
    one_hot_encoder = OneHotEncoder(sparse_output=False)
    one_hot_encoder.fit(dati)

    Il metodo fit analizza i dati e memorizza l'elenco delle categorie uniche presenti, senza ancora produrre alcuna trasformazione. Questo passaggio separato rende esplicito cosa l'encoder ha effettivamente imparato.

    • sparse_output=False: richiede in output un array denso, più semplice da leggere e ispezionare rispetto a una matrice sparsa.
    • fit: apprende le categorie senza trasformare i dati, a differenza di fit_transform che fa entrambe le cose insieme.

    Dopo il fit, l'attributo one_hot_encoder.categories_ contiene l'elenco delle categorie apprese, ordinate alfabeticamente.

    3. Trasformazione dei dati e nomi delle colonne
    dati_codificati = one_hot_encoder.transform(dati)
    nomi_colonne = one_hot_encoder.get_feature_names_out(['colore'])
    
    print(nomi_colonne)
    print(dati_codificati)

    Il metodo transform converte ogni valore categorico in un vettore binario, dove un solo elemento vale 1 (la categoria presente) e tutti gli altri valgono 0.

    • get_feature_names_out: restituisce i nomi leggibili delle nuove colonne generate, ad esempio colore_Rosso.
    • Vettore binario: ogni riga della matrice risultante rappresenta una categoria con un unico 1 nella colonna corrispondente.

    A differenza di una semplice codifica numerica (0, 1, 2), il one-hot non suggerisce alcun ordine o distanza tra le categorie.

    Questo è essenziale per modelli come la regressione lineare, che interpreterebbero erroneamente numeri consecutivi come valori ordinabili.

    4. Gestione di categorie sconosciute
    encoder_robusto = OneHotEncoder(sparse_output=False, handle_unknown='ignore')
    encoder_robusto.fit(dati)
    
    nuovi_dati = np.array(['Giallo'])[:, None]
    print(encoder_robusto.transform(nuovi_dati))

    In produzione può capitare di incontrare categorie mai viste durante l'addestramento. Il parametro handle_unknown='ignore' evita che l'encoder generi un errore in questi casi.

    • handle_unknown='ignore': le categorie sconosciute vengono codificate con un vettore di soli zeri, invece di bloccare l'esecuzione.
    • Robustezza in produzione: fondamentale quando i nuovi dati potrebbero contenere valori non presenti nel training set.

    Per impostazione predefinita OneHotEncoder solleva un errore su categorie sconosciute: una scelta di sicurezza che va disattivata consapevolmente.

    Punto chiave da sottolineare

    OneHotEncoder trasforma variabili categoriche nominali in vettori binari indipendenti, evitando di introdurre relazioni d'ordine inesistenti. È indispensabile per algoritmi che richiedono input numerici e va sempre gestito con attenzione per categorie non previste in fase di addestramento.

    In poche parole...

    Abbiamo visto come preparare dati categorici per OneHotEncoder, addestrare l'encoder, trasformare i dati in vettori binari leggibili con get_feature_names_out, e gestire in modo robusto categorie sconosciute con handle_unknown='ignore'.

  3. Codifica di destinazione
    • Sostituisce le categorie con la media della variabile target per ciascuna categoria.
    • Utilizzato in situazioni in cui è fondamentale preservare la relazione con il destinatario.
  4. Codifica binaria
    • Combina i vantaggi della codifica one-hot e della codifica delle etichette (label encoding).
    • Converte le categorie in rappresentazioni binarie.
    019 base
    Codifica binaria con BinaryEncoder: ridurre la dimensionalità delle variabili categoriali
    Quando una variabile categoriale ha molte categorie BinaryEncoder codifica le categorie in forma binaria, riducendo drasticamente la dimensionalità
    # pip install category_encoders
    import pandas as pd
    from category_encoders import BinaryEncoder
    
    # Creiamo un dataset di esempio con una variabile categoriale nominale
    data = pd.DataFrame({'Colore': ['Rosso', 'Blu', 'Verde', 'Giallo', 'Viola']})
    print("Dataset originale:")
    print(data)
    print()
    
    # Inizializziamo il BinaryEncoder specificando esplicitamente la colonna da trasformare
    # Questo evita trasformazioni indesiderate su altre colonne numeriche eventualmente presenti
    binary_encoder = BinaryEncoder(cols=['Colore'])
    
    # Addestriamo l'encoder sui dati e trasformiamo il dataset in un'unica operazione
    # fit: assegna un codice intero progressivo a ogni categoria unica
    # transform: converte il codice intero nella sua rappresentazione binaria distribuita su più colonne
    encoded_data = binary_encoder.fit_transform(data)
    print("Dataset codificato con BinaryEncoder:")
    print(encoded_data)
    print()
    
    # Confrontiamo il numero di colonne generate dal BinaryEncoder
    # con quelle che avrebbe generato un One-Hot Encoding sullo stesso dataset
    numero_categorie = data['Colore'].nunique()
    colonne_generate = len(encoded_data.columns)
    
    print(f"Numero di categorie uniche: {numero_categorie}")
    print(f"Colonne generate dal BinaryEncoder: {colonne_generate}")
    print(f"Colonne che genererebbe il One-Hot Encoding: {numero_categorie}")
    Dataset originale:
       Colore
    0   Rosso
    1     Blu
    2   Verde
    3  Giallo
    4   Viola
    
    Dataset codificato con BinaryEncoder:
       Colore_0  Colore_1  Colore_2
    0         0         0         1
    1         0         1         0
    2         0         1         1
    3         1         0         0
    4         1         0         1
    
    Numero di categorie uniche: 5
    Colonne generate dal BinaryEncoder: 3
    Colonne che genererebbe il One-Hot Encoding: 5

    019 Codifica binaria con BinaryEncoder: ridurre la dimensionalità delle variabili categoriali

    Lezione 01

    Quando una variabile categoriale ha molte categorie, il One-Hot Encoding genera troppe colonne. Il BinaryEncoder risolve il problema codificando le categorie in forma binaria, riducendo drasticamente la dimensionalità.

    1. Creazione del dataset di esempio
    import pandas as pd
    from category_encoders import BinaryEncoder
    
    # Creiamo un dataset di esempio con una variabile categoriale nominale
    data = pd.DataFrame({'Colore': ['Rosso', 'Blu', 'Verde', 'Giallo', 'Viola']})
    print(data)

    Costruiamo un semplice DataFrame con una colonna categoriale nominale composta da cinque valori distinti, così da avere abbastanza categorie da rendere evidente il funzionamento del BinaryEncoder.

    • Variabile nominale: i colori non hanno un ordine naturale tra loro.
    • Cinque categorie: numero sufficiente per apprezzare il vantaggio della codifica binaria.

    Con poche categorie il vantaggio del BinaryEncoder rispetto al One-Hot Encoding è marginale: la differenza diventa significativa all'aumentare del numero di valori distinti.

    2. Inizializzazione del BinaryEncoder
    # Inizializziamo il BinaryEncoder specificando la colonna da trasformare
    binary_encoder = BinaryEncoder(cols=['Colore'])

    Creiamo un'istanza di BinaryEncoder indicando esplicitamente su quale colonna operare tramite il parametro cols. Questo è utile quando il dataset contiene più colonne e si vuole codificarne solo alcune.

    • cols: elenco delle colonne categoriali da trasformare.
    • Comportamento di default: senza specificare cols, l'encoder trasforma automaticamente tutte le colonne di tipo object o category.

    Specificare sempre esplicitamente le colonne da codificare è una buona pratica: evita trasformazioni indesiderate su colonne numeriche presenti nello stesso DataFrame.

    3. Addestramento e trasformazione dei dati
    # Addestriamo l'encoder sui dati e trasformiamo il dataset
    encoded_data = binary_encoder.fit_transform(data)
    print("Dataset codificato con BinaryEncoder:")
    print(encoded_data)

    Il metodo fit_transform apprende la mappatura tra ciascuna categoria e un valore intero progressivo, poi converte quell'intero nella sua rappresentazione binaria distribuita su più colonne.

    • fit: assegna un codice intero a ogni categoria unica.
    • transform: converte il codice intero in cifre binarie, una per colonna.

    A differenza del One-Hot Encoding, qui ogni riga avrà più valori diversi da zero contemporaneamente, poiché la codifica binaria combina più bit per rappresentare una categoria.

    4. Confronto della dimensionalità generata
    # Confrontiamo il numero di colonne generate dal BinaryEncoder
    # con quelle che avrebbe generato un One-Hot Encoding sullo stesso dataset
    numero_categorie = data['Colore'].nunique()
    colonne_generate = len(encoded_data.columns)
    
    print(f"Numero di categorie uniche: {numero_categorie}")
    print(f"Colonne generate dal BinaryEncoder: {colonne_generate}")
    print(f"Colonne che genererebbe il One-Hot Encoding: {numero_categorie}")

    Verifichiamo concretamente il vantaggio del BinaryEncoder confrontando il numero di colonne prodotte con quelle che avrebbe generato un One-Hot Encoding sullo stesso dataset.

    • One-Hot Encoding: genererebbe 5 colonne, una per categoria.
    • BinaryEncoder: ne genera solo 3, sufficienti a rappresentare 5 valori distinti in binario.

    Con dataset che contengono decine o centinaia di categorie, questa riduzione diventa fondamentale per contenere il numero di feature e la complessità computazionale del modello.

    Punto chiave da sottolineare

    Il BinaryEncoder converte ogni categoria in un codice intero e poi in cifre binarie distribuite su più colonne, riducendo drasticamente la dimensionalità rispetto al One-Hot Encoding. È particolarmente utile con variabili nominali ad alta cardinalità.

    In poche parole...

    Abbiamo creato un dataset con una variabile categoriale a cinque valori, applicato il BinaryEncoder e confrontato il numero di colonne generate rispetto al One-Hot Encoding. La codifica binaria offre un buon compromesso tra compattezza ed espressività.

Scegliere la tecnica di codifica corretta

La pre-elaborazione dei dati è alla base del successo dei progetti di machine learning. Comprendendo i tipi di dati, gestendo i valori mancanti, normalizzando i dati numerici e codificando i dati categorici, è possibile preparare efficacemente i dataset per l'addestramento di modelli robusti e accurati.

Ingegneria delle caratteristiche

Torna all’indice

Selezione delle caratteristiche

Torna all’indice

Il feature engineering è un processo fondamentale nel flusso di lavoro del machine learning, incentrato sulla selezione, trasformazione e creazione di feature a partire dai dati grezzi per migliorare le prestazioni del modello. Feature di alta qualità potenziano la capacità del modello di effettuare previsioni accurate e riducono la complessità dell'addestramento. Il presente documento esamina i principali aspetti del feature engineering:

Tipologie di selezione delle caratteristiche

Passaggi per la selezione delle funzionalità

  1. Comprendere il dominio: conosci i dati e il problema che stai risolvendo.
  2. Visualizzare le relazioni: utilizza diagrammi a dispersione, mappe di calore e confronti a coppie.
  3. Applicare tecniche di selezione: scegli un metodo adatto ai tuoi dati e al tuo modello.
020 base
Ingegneria delle caratteristiche – Selezione delle feature con SelectKBest e chi2
Selezione delle feature più rilevanti per un modello di classificazione tramite il test chi-quadro (chi2) e la classe SelectKBest di scikit-learn.
# =============================================================
# Ingegneria delle caratteristiche:
# Selezione delle feature più informative con SelectKBest e chi2
# =============================================================

import pandas as pd
from sklearn.feature_selection import SelectKBest, chi2

# ---------------------------------------------------------------
# Creazione del dataset di esempio
# ---------------------------------------------------------------
# Simuliamo un piccolo dataset che descrive dieci studenti tramite
# tre caratteristiche numeriche non negative (ore di studio, numero
# di esercizi svolti, numero di assenze) e l'esito finale (promosso
# o non promosso). Il test chi-quadro richiede valori non negativi,
# quindi tutte le feature sono state scelte rispettando questo vincolo.
data = pd.DataFrame({
    'Ore_Studio':      [2, 4, 6, 8, 3, 7, 5, 9, 1, 6],
    'Numero_Esercizi': [1, 1, 2, 3, 2, 4, 3, 5, 0, 3],
    'Assenze':         [8, 6, 4, 1, 7, 2, 5, 0, 9, 3],
    'Promosso':        [0, 0, 1, 1, 0, 1, 1, 1, 0, 1]
})

print("Dataset originale:")
print(data)

# ---------------------------------------------------------------
# Separazione tra feature (X) e target (y)
# ---------------------------------------------------------------
# X contiene le variabili indipendenti, y la variabile da prevedere.
# Questo passo è comune a qualunque tecnica di selezione delle feature.
X = data[['Ore_Studio', 'Numero_Esercizi', 'Assenze']]
y = data['Promosso']

# ---------------------------------------------------------------
# Selezione delle feature migliori con SelectKBest e chi2
# ---------------------------------------------------------------
# SelectKBest mantiene solo le k feature con il punteggio più alto
# secondo la funzione di scoring indicata. Qui usiamo il test
# chi-quadro (chi2), adatto a feature numeriche non negative.
selector = SelectKBest(score_func=chi2, k=2)
X_new = selector.fit_transform(X, y)

# ---------------------------------------------------------------
# Analisi dei punteggi e delle feature selezionate
# ---------------------------------------------------------------
# Recuperiamo i punteggi chi-quadro, i p-value e la maschera booleana
# che indica quali feature sono state effettivamente mantenute.
punteggi = selector.scores_
p_values = selector.pvalues_
feature_selezionate = X.columns[selector.get_support()]

riepilogo_punteggi = pd.DataFrame({
    'Feature': X.columns,
    'Punteggio_Chi2': punteggi,
    'P_value': p_values,
    'Selezionata': selector.get_support()
}).sort_values(by='Punteggio_Chi2', ascending=False)

print("\nPunteggi Chi-quadro per ciascuna feature:")
print(riepilogo_punteggi)

# ---------------------------------------------------------------
# Costruzione del dataset ridotto
# ---------------------------------------------------------------
# Ricostruiamo un DataFrame con i soli nomi delle feature selezionate,
# pronto per essere utilizzato nell'addestramento di un modello.
X_new_df = pd.DataFrame(X_new, columns=feature_selezionate)

print("\nFeature selezionate:", list(feature_selezionate))
print("\nDataset ridotto:")
print(X_new_df)
Dataset originale:
   Ore_Studio  Numero_Esercizi  Assenze  Promosso
0           2                1        8         0
1           4                1        6         0
2           6                2        4         1
3           8                3        1         1
4           3                2        7         0
5           7                4        2         1
6           5                3        5         1
7           9                5        0         1
8           1                0        9         0
9           6                3        3         1

Punteggi Chi-quadro per ciascuna feature:
           Feature  Punteggio_Chi2   P_value  Selezionata
2          Assenze       13.333333  0.000261         True
0       Ore_Studio        8.836601  0.002952         True
1  Numero_Esercizi        5.444444  0.019631        False

Feature selezionate: ['Ore_Studio', 'Assenze']

Dataset ridotto:
   Ore_Studio  Assenze
0           2        8
1           4        6
2           6        4
3           8        1
4           3        7
5           7        2
6           5        5
7           9        0
8           1        9
9           6        3

020 Ingegneria delle caratteristiche – Selezione delle feature con SelectKBest e chi2

Lezione 01

L'ingegneria delle caratteristiche comprende anche la selezione delle feature più informative. In questo esempio usiamo SelectKBest con il test chi-quadro per individuare, tra più variabili numeriche non negative, quelle che meglio spiegano il risultato finale (promozione o meno di uno studente).

1. Creazione del dataset di esempio
data = pd.DataFrame({
    'Ore_Studio':      [2, 4, 6, 8, 3, 7, 5, 9, 1, 6],
    'Numero_Esercizi': [1, 1, 2, 3, 2, 4, 3, 5, 0, 3],
    'Assenze':         [8, 6, 4, 1, 7, 2, 5, 0, 9, 3],
    'Promosso':        [0, 0, 1, 1, 0, 1, 1, 1, 0, 1]
})

Costruiamo un dataset che descrive dieci studenti attraverso tre caratteristiche numeriche non negative e l'esito finale (promosso o non promosso).

  • Ore_Studio, Numero_Esercizi, Assenze: le feature candidate, tutte con valori non negativi come richiesto dal test chi-quadro.
  • Promosso: la variabile target binaria (0 = non promosso, 1 = promosso).

Un dataset ampio permette al test statistico di calcolare punteggi più significativi e affidabili.

2. Separazione tra feature e target
X = data[['Ore_Studio', 'Numero_Esercizi', 'Assenze']]
y = data['Promosso']

Isoliamo le variabili indipendenti (X) dalla variabile da prevedere (y), passo indispensabile prima di qualunque tecnica di selezione delle feature.

Questa separazione è comune a tutte le tecniche di feature selection e feature engineering che incontreremo nel corso.

3. Applicazione di SelectKBest con test chi-quadro
selector = SelectKBest(score_func=chi2, k=2)
X_new = selector.fit_transform(X, y)

SelectKBest classifica le feature in base a un punteggio statistico e mantiene solo le k migliori; qui usiamo il test chi-quadro (chi2), adatto a variabili non negative.

  • score_func=chi2: misura la dipendenza statistica tra ogni feature e il target.
  • k=2: numero di feature da mantenere dopo la selezione.

Il test chi-quadro non funziona con valori negativi: in presenza di feature negative occorre prima normalizzare con MinMaxScaler.

Aumentando k si mantengono più feature, riducendo la capacità del test di scartare quelle meno informative.

4. Analisi dei punteggi e delle feature selezionate
punteggi = selector.scores_
p_values = selector.pvalues_
feature_selezionate = X.columns[selector.get_support()]

riepilogo_punteggi = pd.DataFrame({
    'Feature': X.columns,
    'Punteggio_Chi2': punteggi,
    'P_value': p_values,
    'Selezionata': selector.get_support()
}).sort_values(by='Punteggio_Chi2', ascending=False)

Esaminiamo i punteggi calcolati per ciascuna feature, individuando quali sono state effettivamente selezionate.

  • scores_: punteggio di dipendenza statistica assegnato a ciascuna feature.
  • get_support(): maschera booleana che indica quali feature sono state mantenute.

Un punteggio chi-quadro più alto indica una relazione statistica più forte tra la feature e la variabile target.

5. Costruzione del dataset ridotto
X_new_df = pd.DataFrame(X_new, columns=feature_selezionate)

Ricostruiamo un DataFrame contenente solo le feature selezionate, pronto per essere utilizzato nell'addestramento del modello.

Ridurre il numero di feature può migliorare le prestazioni del modello e diminuire il rischio di overfitting.

La selezione delle feature è un passo di ingegneria delle caratteristiche complementare alla creazione di nuove variabili.

Punto chiave da sottolineare

SelectKBest con il test chi-quadro individua automaticamente le feature numeriche non negative più correlate al target, semplificando il modello.

In poche parole...

Abbiamo selezionato le feature più informative tramite SelectKBest e chi2, analizzando punteggi e p-value per costruire un dataset ridotto ed efficace.

Estrazione delle caratteristiche

Torna all’indice

L'estrazione di caratteristiche trasforma i dati grezzi in caratteristiche significative che catturano i modelli sottostanti. È particolarmente utile per i dati non strutturati come testo, immagini e audio.

Tecniche per l'estrazione di caratteristiche

  1. Dati testuali:
    • TF-IDF (Frequenza del termine - Frequenza inversa del documento): evidenzia i termini importanti in un documento.
    • Incorporamento di parole: converte le parole in vettori utilizzando modelli come Word2Vec, GloVe o BERT.
  2. Dati di immagine:
    • Caratteristiche dei pixel: estrarre i valori di intensità dei pixel.
    • Caratteristiche convoluzionali: utilizzare reti neurali convoluzionali (CNN) per estrarre caratteristiche spaziali.
  3. Dati di serie temporali:
    • Trasformata di Fourier: estrae le componenti di frequenza dalle serie temporali.
    • Autocorrelazione: cattura le relazioni temporali
  4. 021 base
    Estrazione delle Caratteristiche da Testo con TF-IDF
    Trasformazione di un corpus testuale in una matrice numerica di pesi TF-IDF, utile per confrontare l'importanza delle parole nei documenti in attività di machine learning.
    # Estrazione delle caratteristiche da testo con TF-IDF
    # ---------------------------------------------------------------------
    # Obiettivo: trasformare un insieme di frasi (corpus) in una matrice
    # numerica che rappresenti l'importanza di ogni parola in ciascun
    # documento, utilizzando la tecnica TF-IDF (Term Frequency - Inverse
    # Document Frequency).
    
    import pandas as pd
    from sklearn.feature_extraction.text import TfidfVectorizer
    
    # Preparazione del corpus di testo
    # Il corpus e' l'insieme dei documenti (frasi) che vogliamo analizzare.
    # Sono stati aggiunti termini ripetuti ("machine", "learning", "is") e
    # termini unici ("fascinating", "crucial", "deep", "subset") per
    # mostrare come TF-IDF distingua le parole comuni da quelle rare.
    corpus = [
        "Machine learning is fascinating and powerful.",
        "Feature engineering is crucial for machine learning.",
        "Deep learning is a subset of machine learning."
    ]
    
    # Creazione del vettorizzatore TF-IDF e trasformazione del corpus
    # TfidfVectorizer calcola automaticamente sia la frequenza dei termini
    # (TF) sia la frequenza inversa di documento (IDF), combinandole in
    # un unico punteggio per ogni parola in ogni documento.
    vectorizer = TfidfVectorizer()
    X = vectorizer.fit_transform(corpus)
    
    # Esplorazione del vocabolario
    # Il vocabolario contiene tutte le parole uniche individuate nel corpus,
    # ciascuna associata a un indice di colonna nella matrice risultante.
    feature_names = vectorizer.get_feature_names_out()
    print("Vocabolario individuato:")
    print(feature_names)
    
    # Rappresentazione tabellare con pandas
    # Convertiamo la matrice sparsa in un DataFrame per rendere i valori
    # TF-IDF piu' leggibili, con le parole come colonne e i documenti come righe.
    df_tfidf = pd.DataFrame(X.toarray(), columns=feature_names)
    df_tfidf.index = [f"Documento {i + 1}" for i in range(len(corpus))]
    print("\nMatrice TF-IDF:")
    print(df_tfidf.round(3))
    
    # Interpretazione dei valori TF-IDF
    # Confrontiamo il peso medio delle parole che compaiono in piu' documenti
    # rispetto a quelle rare, per verificare che TF-IDF penalizzi i termini comuni.
    parole_comuni = ["learning", "machine", "is"]
    parole_rare = ["fascinating", "crucial", "subset"]
    
    peso_medio_comuni = df_tfidf[parole_comuni].mean().mean()
    peso_medio_rare = df_tfidf[parole_rare].mean().mean()
    
    print(f"\nPeso medio parole comuni: {peso_medio_comuni:.3f}")
    print(f"Peso medio parole rare: {peso_medio_rare:.3f}")
    Vocabolario individuato:
    ['and' 'crucial' 'deep' 'engineering' 'fascinating' 'feature' 'for' 'is'
     'learning' 'machine' 'of' 'powerful' 'subset']
    
    Matrice TF-IDF:
                   and  crucial   deep  engineering  fascinating  feature    for     is  learning  machine     of  powerful  subset
    Documento 1  0.497    0.000  0.000        0.000        0.497    0.000  0.000  0.294     0.294    0.294  0.000     0.497   0.000
    Documento 2  0.000    0.445  0.000        0.445        0.000    0.445  0.445  0.263     0.263    0.263  0.000     0.000   0.000
    Documento 3  0.000    0.000  0.443        0.000        0.000    0.000  0.000  0.262     0.523    0.262  0.443     0.000   0.443
    
    Peso medio parole comuni: 0.302
    Peso medio parole rare: 0.154

    021 Estrazione delle Caratteristiche da Testo con TF-IDF

    Lezione 01

    Il machine learning non lavora direttamente con il testo, ma con numeri. La tecnica TF-IDF converte un insieme di frasi in una matrice numerica, assegnando a ogni parola un peso che riflette quanto essa sia rilevante e distintiva all'interno dei singoli documenti.

    1. Preparazione del corpus di testo
    corpus = [
        "Machine learning is fascinating and powerful.",
        "Feature engineering is crucial for machine learning.",
        "Deep learning is a subset of machine learning."
    ]

    Il corpus è l'insieme dei documenti testuali da analizzare. Sono state usate tre frasi, alcune contenenti le stesse parole chiave e altre con termini unici, per rendere più evidente il comportamento di TF-IDF.

    • Parole ripetute: "machine", "learning" e "is" compaiono in più frasi e avranno quindi un peso IDF più basso.
    • Parole uniche: "fascinating", "crucial" e "subset" compaiono una sola volta e otterranno un peso più alto.

    Un corpus con almeno tre documenti permette di osservare in modo più realistico l'effetto della frequenza inversa di un documento rispetto a un corpus con sole due frasi.

    2. Creazione del vettorizzatore TF-IDF
    vectorizer = TfidfVectorizer()
    X = vectorizer.fit_transform(corpus)

    TfidfVectorizer calcola automaticamente due componenti: la frequenza del termine nel documento (TF) e la frequenza inversa di documento (IDF), combinandole in un unico punteggio.

    • fit: analizza il corpus e costruisce il vocabolario delle parole uniche.
    • transform: converte ogni documento in un vettore numerico di pesi TF-IDF.

    Il risultato X e una matrice sparsa, formato efficiente per rappresentare dati testuali dove la maggior parte delle parole del vocabolario non compare in ogni singolo documento.

    3. Esplorazione del vocabolario
    feature_names = vectorizer.get_feature_names_out()
    print("Vocabolario individuato:")
    print(feature_names)

    Il metodo get_feature_names_out restituisce l'elenco ordinato di tutte le parole uniche individuate nel corpus, ciascuna corrispondente a una colonna della matrice TF-IDF.

    Conoscere il vocabolario è fondamentale per interpretare correttamente i valori numerici: ogni colonna della matrice rappresenta infatti una parola specifica.

    4. Rappresentazione tabellare con pandas
    df_tfidf = pd.DataFrame(X.toarray(), columns=feature_names)
    df_tfidf.index = [f"Documento {i + 1}" for i in range(len(corpus))]
    print("\nMatrice TF-IDF:")
    print(df_tfidf.round(3))

    Convertire la matrice sparsa in un DataFrame pandas rende i valori TF-IDF molto più leggibili, associando ogni parola alla propria colonna e ogni documento alla propria riga.

    • toarray(): trasforma la matrice sparsa in un array denso leggibile.
    • round(3): arrotonda i pesi per facilitarne il confronto visivo.

    Questa tabella permette di vedere a colpo d'occhio quali parole pesano di più in ciascun documento, rendendo concreto un concetto altrimenti astratto.

    5. Interpretazione dei valori TF-IDF
    parole_comuni = ["learning", "machine", "is"]
    parole_rare = ["fascinating", "crucial", "subset"]
    
    peso_medio_comuni = df_tfidf[parole_comuni].mean().mean()
    peso_medio_rare = df_tfidf[parole_rare].mean().mean()
    
    print(f"\nPeso medio parole comuni: {peso_medio_comuni:.3f}")
    print(f"Peso medio parole rare: {peso_medio_rare:.3f}")

    Confrontando il peso medio delle parole comuni con quello delle parole rare si verifica empiricamente il comportamento atteso di TF-IDF: le parole diffuse in più documenti ricevono un peso inferiore.

    • Parole comuni: peso medio più basso, poiché presenti in più documenti (IDF ridotto).
    • Parole rare: peso medio più alto, poiché distintive di un singolo documento (IDF elevato).

    Questo meccanismo è ciò che rende TF-IDF particolarmente utile per individuare le parole realmente caratterizzanti di un testo, escludendo termini troppo generici.

    Punto chiave da sottolineare

    TF-IDF assegna un peso a ogni parola combinando quanto essa sia frequente in un documento (TF) con quanto essa sia rara nell'intero corpus (IDF). Le parole comuni a molti documenti ricevono un peso basso, mentre le parole distintive di un solo documento ricevono un peso alto, rendendo questa tecnica utile per identificare i termini realmente significativi.

    In poche parole...

    In questo esempio abbiamo trasformato un corpus di frasi in una matrice numerica TF-IDF con TfidfVectorizer, esplorato il vocabolario generato, rappresentato i risultati in un DataFrame pandas per maggiore leggibilità e confrontato il peso di parole comuni rispetto a parole rare, verificando il comportamento atteso della tecnica.

    Analisi delle componenti principali (PCA)

    Torna all’indice

    L'analisi delle componenti principali (PCA) è una popolare tecnica di riduzione della dimensionalità utilizzata per proiettare dati ad alta dimensionalità in uno spazio a dimensionalità inferiore, preservando al contempo la maggior parte possibile della varianza.

    • Come funziona la PCA
      1. Calcola la matrice di covarianza: calcola la matrice di covarianza del dataset per comprendere le relazioni tra le caratteristiche.
      2. Calcola autovalori e autovettori: gli autovettori determinano la direzione del nuovo spazio delle caratteristiche, e gli autovalori rappresentano l'entità della varianza in quella direzione.
      3. Componenti della classifica: seleziona le componenti principali che spiegano la maggior parte della varianza.
    • Passaggi per eseguire la PCA
      1. Normalizzare i dati.
      2. Calcolare la matrice di covarianza.
      3. Eseguire la propria decomposizione.
      4. Proiettare i dati sui primi k componenti.
    • Vantaggi della PCA
      • Riduce la dimensionalità, velocizzando l'addestramento.
      • Aiuta a visualizzare dati ad alta dimensionalità.
      • Elimina il rumore concentrandosi sugli schemi principali.
    022 base
    Riduzione della dimensionalità con la PCA (Principal Component Analysis)
    Applica la PCA a un dataset di due feature correlate, riducendolo a una sola componente principale dopo averlo standardizzato con StandardScaler.
    # Riduzione della dimensionalità con la PCA (Principal Component Analysis)
    # ============================================================
    # Importazione delle librerie e creazione del dataset
    # ============================================================
    import numpy as np
    import pandas as pd
    from sklearn.decomposition import PCA
    from sklearn.preprocessing import StandardScaler
    
    # Creiamo un dataset di esempio con 2 feature correlate tra loro,
    # ad esempio "ore di studio" e "punteggio esame" ottenuto
    data = np.array([
        [2.5, 2.4],
        [0.5, 0.7],
        [2.2, 2.9],
        [1.9, 2.2],
        [3.1, 3.0],
        [2.3, 2.7],
        [2.0, 1.6],
        [1.0, 1.1],
        [1.5, 1.6],
        [1.1, 0.9]
    ])
    
    # Convertiamo in DataFrame per una lettura più chiara delle feature
    df = pd.DataFrame(data, columns=["Ore di studio", "Punteggio esame"])
    print("Dataset originale:")
    print(df)
    
    # ============================================================
    # Standardizzazione dei dati
    # ============================================================
    # La PCA è sensibile alla scala delle feature: variabili con range
    # più ampi dominerebbero artificialmente le componenti principali.
    # Standardizziamo quindi i dati a media 0 e deviazione standard 1.
    scaler = StandardScaler()
    data_scaled = scaler.fit_transform(df)
    
    df_scaled = pd.DataFrame(data_scaled, columns=df.columns)
    print("\nDataset standardizzato:")
    print(df_scaled)
    
    # ============================================================
    # Applicazione della PCA
    # ============================================================
    # Riduciamo le 2 feature originali a 1 sola componente principale,
    # che cattura la maggior parte della varianza presente nei dati.
    pca = PCA(n_components=1)
    transformed_data = pca.fit_transform(data_scaled)
    
    print("\nDati trasformati (1 componente principale):")
    print(transformed_data)
    
    # ============================================================
    # Analisi della varianza spiegata
    # ============================================================
    # explained_variance_ratio_ indica quanta informazione (varianza)
    # del dataset originale è stata conservata dalla componente principale.
    varianza_spiegata = pca.explained_variance_ratio_
    print(f"\nVarianza spiegata dalla prima componente: {varianza_spiegata[0]:.2%}")
    
    # ============================================================
    # Ricostruzione approssimata dei dati originali
    # ============================================================
    # Con inverse_transform torniamo allo spazio originale a 2 dimensioni,
    # osservando quanta informazione è andata persa nella riduzione.
    data_reconstructed = pca.inverse_transform(transformed_data)
    
    df_reconstructed = pd.DataFrame(data_reconstructed, columns=df.columns)
    print("\nDati ricostruiti dopo la riduzione dimensionale:")
    print(df_reconstructed)
    
    # Calcoliamo l'errore quadratico medio di ricostruzione rispetto
    # ai dati standardizzati originali
    errore_ricostruzione = np.mean((data_scaled - data_reconstructed) ** 2)
    print(f"\nErrore quadratico medio di ricostruzione: {errore_ricostruzione:.4f}")
    Dataset originale:
       Ore di studio  Punteggio esame
    0            2.5              2.4
    1            0.5              0.7
    2            2.2              2.9
    3            1.9              2.2
    4            3.1              3.0
    5            2.3              2.7
    6            2.0              1.6
    7            1.0              1.1
    8            1.5              1.6
    9            1.1              0.9
    
    Dataset standardizzato:
       Ore di studio  Punteggio esame
    0       0.926279         0.610169
    1      -1.758587        -1.506743
    2       0.523549         1.232790
    3       0.120819         0.361120
    4       1.731739         1.357314
    5       0.657792         0.983741
    6       0.255062        -0.386025
    7      -1.087371        -1.008646
    8      -0.416154        -0.386025
    9      -0.953127        -1.257695
    
    Dati trasformati (1 componente principale):
    [[ 1.08643242]
     [-2.3089372 ]
     [ 1.24191895]
     [ 0.34078247]
     [ 2.18429003]
     [ 1.16073946]
     [-0.09260467]
     [-1.48210777]
     [-0.56722643]
     [-1.56328726]]
    
    Varianza spiegata dalla prima componente: 96.30%
    
    Dati ricostruiti dopo la riduzione dimensionale:
       Ore di studio  Punteggio esame
    0       0.768224         0.768224
    1      -1.632665        -1.632665
    2       0.878169         0.878169
    3       0.240970         0.240970
    4       1.544526         1.544526
    5       0.820767         0.820767
    6      -0.065481        -0.065481
    7      -1.048008        -1.048008
    8      -0.401090        -0.401090
    9      -1.105411        -1.105411
    
    Errore quadratico medio di ricostruzione: 0.0370

    022 Riduzione della dimensionalità con la PCA (Principal Component Analysis)

    Lezione 01

    La PCA (Principal Component Analysis) è una tecnica che riduce il numero di feature di un dataset conservando la maggior parte dell'informazione. In questo esempio vedremo come applicarla correttamente, passando anche per la standardizzazione dei dati.

    1. Creazione del dataset di esempio
    data = np.array([
        [2.5, 2.4],
        [0.5, 0.7],
        [2.2, 2.9],
        [1.9, 2.2],
        [3.1, 3.0],
        [2.3, 2.7],
        [2.0, 1.6],
        [1.0, 1.1],
        [1.5, 1.6],
        [1.1, 0.9]
    ])
    df = pd.DataFrame(data, columns=["Ore di studio", "Punteggio esame"])

    Costruiamo un dataset con due feature correlate tra loro: ore di studio e punteggio esame. Abbiamo ampliato l'esempio originale da 4 a 10 righe per rendere più stabile e realistica l'analisi della varianza.

    • Correlazione tra feature: le due colonne tendono a crescere insieme, condizione ideale per mostrare l'utilità della PCA.
    • DataFrame con nomi di colonna: rende il dataset leggibile e più vicino a un caso d'uso reale.

    Con solo 4 osservazioni, come nel codice originale, la varianza spiegata sarebbe poco affidabile e didatticamente poco significativa.

    2. Standardizzazione dei dati
    scaler = StandardScaler()
    data_scaled = scaler.fit_transform(df)
    
    df_scaled = pd.DataFrame(data_scaled, columns=df.columns)

    Prima di applicare la PCA, standardizziamo i dati a media 0 e deviazione standard 1. Questo passaggio, assente nel codice originale, è fondamentale per un'analisi corretta.

    • Sensibilità alla scala: la PCA calcola le componenti principali in base alla varianza, quindi feature con range più ampi la dominerebbero senza motivo.
    • StandardScaler: riporta ogni feature sulla stessa scala, rendendo il confronto tra le variabili equo.

    Saltare questa fase è uno degli errori più comuni quando si applica la PCA a dataset con feature di scale diverse.

    3. Applicazione della PCA
    pca = PCA(n_components=1)
    transformed_data = pca.fit_transform(data_scaled)

    Riduciamo le due feature originali a un'unica componente principale, che riassume la direzione di massima varianza nei dati standardizzati.

    • n_components=1: specifica quante dimensioni finali vogliamo ottenere.
    • fit_transform: calcola le componenti principali e proietta i dati su di esse in un unico passaggio.

    Il risultato è un array monodimensionale: ogni osservazione originale è ora rappresentata da un solo valore numerico.

    4. Analisi della varianza spiegata
    varianza_spiegata = pca.explained_variance_ratio_
    print(f"\nVarianza spiegata dalla prima componente: {varianza_spiegata[0]:.2%}")

    La proprietà explained_variance_ratio_ indica quale percentuale dell'informazione originale è stata conservata dalla componente principale scelta.

    • Valore vicino a 1: significa che la componente cattura quasi tutta la variabilità del dataset originale.
    • Valore basso: segnala che una sola componente non è sufficiente a rappresentare bene i dati.

    Questo indicatore è cruciale per decidere quante componenti mantenere in dataset reali con molte più feature.

    5. Ricostruzione approssimata dei dati originali
    data_reconstructed = pca.inverse_transform(transformed_data)
    errore_ricostruzione = np.mean((data_scaled - data_reconstructed) ** 2)

    Con inverse_transform torniamo allo spazio a due dimensioni originario, per visualizzare concretamente quanta informazione è stata persa nella riduzione.

    • inverse_transform: ricostruisce un'approssimazione dei dati standardizzati a partire dalla componente principale.
    • Errore quadratico medio: quantifica numericamente la differenza tra dati originali e dati ricostruiti.

    Un errore basso conferma che la componente principale scelta rappresenta fedelmente il dataset originale, giustificando la riduzione dimensionale operata.

    Punto chiave da sottolineare

    La PCA richiede dati standardizzati per funzionare correttamente, altrimenti le feature con scale maggiori dominano ingiustamente le componenti principali. La varianza spiegata quantifica quanta informazione viene conservata, mentre l'inverse_transform rende visibile quanta se ne perde nella riduzione dimensionale.

    In poche parole...

    In questo esempio abbiamo applicato la PCA a un dataset di due feature correlate, standardizzandole preventivamente con StandardScaler. Abbiamo ridotto i dati a una sola componente principale, analizzato la varianza spiegata e ricostruito i dati originali con inverse_transform, misurando l'errore di ricostruzione introdotto dalla riduzione.

    Tecniche di riduzione della dimensionalità

    Torna all’indice

    La riduzione della dimensionalità diminuisce il numero di caratteristiche in un dataset mantenendo le informazioni importanti. Può migliorare l'efficienza computazionale, ridurre l'overfitting e migliorare l'interpretabilità del modello.

    • Tecniche
      1. Selezione delle funzionalità (rivista sopra)
        • Mantiene un sottoinsieme delle caratteristiche originali.
      2. Estrazione delle caratteristiche (rivista sopra)
        • Crea nuove funzionalità basate su trasformazioni.
      3. Tecniche lineari
        • Analisi delle componenti principali (PCA): i dati dei progetti sono lineari.
        • Analisi discriminante lineare (LDA): massimizza la separabilità delle classi.
      4. Tecniche non lineari
        • t-SNE (t-Distributed Stochastic Neighbor Embedding): proietta i dati in 2D/3D per la visualizzazione.
        • UMAP (Uniform Manifold Approximation and Projection): mantiene la struttura globale e locale ai fini della visualizzazione e dell'analisi.
        • Autoencoder: reti neurali progettate per la riduzione non supervisionata della dimensionalità.
    • Scegliere la tecnica giusta
      • Utilizza l'analisi delle componenti principali (PCA) per dati numerici ad alta dimensionalità.
      • Utilizzare t-SNE o UMAP per la visualizzazione.
      • Utilizza gli Autoencoder per l'estrazione di caratteristiche basata sul deep learning.
    023 base
    Riduzione della dimensionalità con t-SNE per la visualizzazione dei dati
    Applica l'algoritmo t-SNE per ridurre un dataset ad alta dimensionalità a due componenti, permettendo di visualizzare cluster e pattern nascosti nei dati in modo intuitivo.
    # =============================================================
    # Riduzione della dimensionalità con t-SNE
    # =============================================================
    
    # Importazione delle librerie e caricamento del dataset
    # Importiamo le librerie necessarie per il preprocessing, il modello
    # t-SNE e la visualizzazione dei risultati
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.manifold import TSNE
    from sklearn.datasets import load_digits
    from sklearn.preprocessing import StandardScaler
    
    # Carichiamo il dataset digits: 1797 immagini 8x8 di cifre scritte
    # a mano (da 0 a 9), gia trasformate in vettori di 64 feature (pixel)
    digits = load_digits()
    X = digits.data      # matrice delle feature: 1797 campioni x 64 feature
    y = digits.target    # etichette delle cifre (0-9), usate solo per colorare il grafico
    
    
    # Esplorazione della struttura del dataset
    # Verifichiamo le dimensioni del dataset originale, per capire
    # perche una visualizzazione diretta non e possibile
    print("Forma dei dati (campioni, feature):", X.shape)
    print("Numero di classi (cifre 0-9):", len(np.unique(y)))
    print("Esempio di vettore feature (prima immagine, 64 valori):")
    print(X[0])
    
    
    # Standardizzazione delle feature
    # Portiamo tutte le feature sulla stessa scala, poiche t-SNE si basa
    # su distanze tra punti ed e sensibile a differenze di scala tra le feature
    scaler = StandardScaler()
    X_scaled = scaler.fit_transform(X)
    
    
    # Configurazione dell'algoritmo t-SNE
    # Impostiamo il modello t-SNE per ridurre le 64 dimensioni originali
    # a sole 2 dimensioni, adatte alla visualizzazione su un grafico 2D
    tsne = TSNE(
        n_components=2,     # numero di dimensioni di output desiderate
        perplexity=30,      # bilancia l'attenzione tra struttura locale e globale
        max_iter=1000,      # numero massimo di iterazioni di ottimizzazione
                            # (nelle versioni di scikit-learn precedenti alla 1.5
                            # questo parametro si chiamava n_iter)
        random_state=42     # garantisce la riproducibilita dei risultati
    )
    
    
    # Trasformazione dei dati in due dimensioni
    # Applichiamo fit_transform per calcolare le nuove coordinate 2D
    # Nota: t-SNE non dispone di un metodo transform separato, quindi
    # non puo essere riutilizzato su nuovi dati non visti in fase di addestramento
    X_tsne = tsne.fit_transform(X_scaled)
    
    print("\nForma dei dati dopo la riduzione con t-SNE:", X_tsne.shape)
    print("Prime 5 coordinate 2D calcolate:")
    print(X_tsne[:5])
    
    
    # Visualizzazione del risultato
    # Creiamo uno scatter plot colorato in base alla cifra rappresentata,
    # per verificare visivamente se t-SNE ha separato correttamente i gruppi
    plt.figure(figsize=(10, 8))
    scatter = plt.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, cmap="tab10", s=15)
    plt.colorbar(scatter, label="Cifra")
    plt.title("Visualizzazione t-SNE del dataset digits")
    plt.xlabel("Componente t-SNE 1")
    plt.ylabel("Componente t-SNE 2")
    plt.show()
    Forma dei dati (campioni, feature): (1797, 64)
    Numero di classi (cifre 0-9): 10
    Esempio di vettore feature (prima immagine, 64 valori):
    [ 0.  0.  5. 13.  9.  1.  0.  0.  0.  0. 13. 15. 10. 15.  5.  0.  0.  3.
     15.  2.  0. 11.  8.  0.  0.  4. 12.  0.  0.  8.  8.  0.  0.  5.  8.  0.
      0.  9.  8.  0.  0.  4. 11.  0.  1. 12.  7.  0.  0.  2. 14.  5. 10. 12.
      0.  0.  0.  0.  6. 13. 10.  0.  0.  0.]
    
    Forma dei dati dopo la riduzione con t-SNE: (1797, 2)
    Prime 5 coordinate 2D calcolate:
    [[-38.777     -30.429148 ]
     [-15.336499   -2.7883544]
     [  5.937756  -14.502025 ]
     [ 28.00608     2.9412334]
     [-36.394794    2.6174462]]

    023 Riduzione della dimensionalità con t-SNE per la visualizzazione dei dati

    Lezione 01

    Quando un dataset ha decine di feature diventa impossibile visualizzarlo direttamente. t-SNE è una tecnica non lineare che proietta i dati in 2 o 3 dimensioni, preservando le relazioni di vicinanza tra i punti.

    1. Importazione delle librerie e caricamento del dataset
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.manifold import TSNE
    from sklearn.datasets import load_digits
    from sklearn.preprocessing import StandardScaler
    
    # Carichiamo il dataset digits: 1797 immagini 8x8 di cifre scritte
    # a mano (da 0 a 9), gia trasformate in vettori di 64 feature (pixel)
    digits = load_digits()
    X = digits.data      # matrice delle feature: 1797 campioni x 64 feature
    y = digits.target    # etichette delle cifre (0-9), usate solo per colorare il grafico

    Sostituiamo il piccolo array di esempio con il dataset digits, composto da 1797 immagini di cifre scritte a mano rappresentate come vettori di 64 pixel. Questo dataset è uno standard per dimostrare la riduzione della dimensionalità, perché ha una struttura reale su cui t-SNE può lavorare.

    • load_digits carica un dataset con 64 feature per campione, troppe per essere visualizzate direttamente.
    • y contiene le etichette delle cifre, usate solo per colorare il grafico finale, non per l'addestramento di t-SNE.

    Il codice originale utilizzava solo 4 punti bidimensionali, un caso non realistico per t-SNE, che è pensato per dataset con molte più feature e osservazioni.

    t-SNE è un algoritmo non supervisionato: non usa le etichette per calcolare la proiezione, ma solo la struttura interna dei dati.

    2. Esplorazione della struttura del dataset
    print("Forma dei dati (campioni, feature):", X.shape)
    print("Numero di classi (cifre 0-9):", len(np.unique(y)))
    print("Esempio di vettore feature (prima immagine, 64 valori):")
    print(X[0])

    Prima di applicare qualsiasi trasformazione è utile ispezionare la forma dei dati, per capire quante dimensioni stiamo cercando di ridurre e quante classi sono presenti nel dataset.

    • X.shape restituisce 1797 righe e 64 colonne: 64 è la dimensionalità originale da ridurre.
    • np.unique(y) conferma che le classi sono 10, una per ogni cifra da 0 a 9.

    Con 64 dimensioni è impossibile realizzare un grafico a dispersione tradizionale: servono al massimo 2 o 3 assi per un grafico leggibile.

    3. Standardizzazione delle feature
    scaler = StandardScaler()
    X_scaled = scaler.fit_transform(X)

    Standardizziamo le feature per portarle sulla stessa scala, così che nessun pixel domini il calcolo delle distanze solo perché ha valori numerici più grandi.

    • StandardScaler trasforma ogni feature con media 0 e deviazione standard 1.
    • fit_transform calcola i parametri di scaling e applica la trasformazione in un solo passaggio.

    t-SNE si basa sul calcolo di distanze tra punti, quindi è buona pratica standardizzare i dati come già visto negli esempi precedenti sul preprocessing.

    4. Configurazione dell'algoritmo t-SNE
    tsne = TSNE(
        n_components=2,     # numero di dimensioni di output desiderate
        perplexity=30,      # bilancia l'attenzione tra struttura locale e globale
        max_iter=1000,      # numero massimo di iterazioni di ottimizzazione
                            # (nelle versioni di scikit-learn precedenti alla 1.5
                            # questo parametro si chiamava n_iter)
        random_state=42     # garantisce la riproducibilita dei risultati
    )

    Configuriamo il modello specificando quante dimensioni di output vogliamo e alcuni parametri che influenzano la qualità della proiezione finale.

    • perplexity bilancia l'attenzione tra la struttura locale e quella globale dei dati; valori tipici vanno da 5 a 50.
    • max_iter definisce il numero massimo di iterazioni di ottimizzazione; nelle versioni di scikit-learn precedenti alla 1.5 questo parametro si chiamava n_iter.

    Cambiare la perplexity può modificare sensibilmente la forma dei cluster risultanti: è un parametro da esplorare, non da fissare a priori.

    5. Trasformazione dei dati in due dimensioni
    X_tsne = tsne.fit_transform(X_scaled)
    
    print("\nForma dei dati dopo la riduzione con t-SNE:", X_tsne.shape)
    print("Prime 5 coordinate 2D calcolate:")
    print(X_tsne[:5])

    Applichiamo fit_transform per ottenere le nuove coordinate bidimensionali di ogni campione, pronte per essere visualizzate su un grafico.

    • fit_transform è l'unico metodo disponibile: t-SNE non offre un transform separato per nuovi dati.
    • X_tsne.shape conferma che ogni campione ora è rappresentato da sole 2 coordinate.

    A differenza di StandardScaler o PCA, t-SNE non può essere applicato a dati mai visti prima: ogni nuova proiezione richiede un nuovo addestramento completo.

    6. Visualizzazione del risultato
    plt.figure(figsize=(10, 8))
    scatter = plt.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, cmap="tab10", s=15)
    plt.colorbar(scatter, label="Cifra")
    plt.title("Visualizzazione t-SNE del dataset digits")
    plt.xlabel("Componente t-SNE 1")
    plt.ylabel("Componente t-SNE 2")
    plt.show()

    Rappresentiamo i punti su un piano cartesiano, colorando ogni punto in base alla cifra che rappresenta, per verificare se t-SNE ha raggruppato correttamente le cifre simili.

    • c=y colora i punti solo a scopo illustrativo: t-SNE non ha usato questa informazione per calcolare la proiezione.
    • cmap="tab10" assegna un colore distinto a ciascuna delle 10 cifre.

    Nonostante sia un algoritmo non supervisionato, il grafico mostra tipicamente gruppi ben separati per ogni cifra, a conferma che le distanze nello spazio originale contenevano informazione utile.

    Punto chiave da sottolineare

    t-SNE preserva la struttura locale dei dati ma non ha un metodo transform per nuovi campioni, non offre inverse_transform ed è pensato per l'esplorazione visiva, non come step riutilizzabile in una pipeline di produzione.

    In poche parole...

    Abbiamo caricato il dataset digits, standardizzato le feature, applicato t-SNE per ridurre 64 dimensioni a 2 e visualizzato il risultato con uno scatter plot colorato, osservando come cifre simili si raggruppino naturalmente nello spazio ridotto.

    L'ingegneria delle caratteristiche, che comprende selezione, estrazione, analisi delle componenti principali (PCA) e riduzione della dimensionalità, è la pietra angolare della creazione di modelli di apprendimento automatico efficaci. Essa colma il divario tra i dati grezzi e le informazioni utili, garantendo che i modelli vengano addestrati sulle informazioni più rilevanti e significative.

    Test finale lezione 01

    Hai completato la Lezione 01! Prima di passare alla Lezione 02 mettiti alla prova con il test finale: copre pre-elaborazione dei dati, encoding, selezione ed estrazione delle caratteristiche, PCA e t-SNE. Il superamento concorre a sbloccare la lezione successiva e il risultato concorre al certificato di competenze rilasciato al termine del corso.

    Per ottenere le chiavi di accesso alla lezione successiva devi anche spiegare a voce una scelta fatta nel tuo esercizio e risolvere una piccola variante al momento.

    Ti viene fornito il seguente dataset che descrive 12 studenti. Scrivi uno script Python che esegua, nell'ordine, i passaggi indicati sotto. Carica il file .py risultante nell'apposito campo di consegna.

    import pandas as pd
    
    data = {
        'Ore_Studio':        [5, 3, None, 8, 6, 2, 7, 4, None, 9, 1, 6],
        'Assenze':           [2, 5, 3, 0, 1, 8, 0, 4, 6, 0, 9, 1],
        'Voto_Precedente':   [7.5, 6.0, 5.5, None, 8.0, 4.5, 7.0, 6.5, 5.0, 9.0, 4.0, 7.5],
        'Materia_Preferita': ['Matematica', 'Storia', 'Scienze', 'Matematica', 'Arte', 'Storia',
                               'Scienze', 'Arte', 'Matematica', 'Scienze', 'Storia', 'Arte'],
        'Promosso':          [1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1]
    }
    df = pd.DataFrame(data)
    1. Correggi i tipi di dato inferiti automaticamente da pandas, usando i tipi "nullable" appropriati per ciascuna colonna.
    2. Gestisci i valori mancanti nelle colonne numeriche con una strategia a tua scelta, motivando la scelta in un commento.
    3. Codifica la colonna Materia_Preferita con la tecnica di encoding più adatta, motivando la scelta rispetto alle alternative viste a lezione.
    4. Separa le feature (X) dal target (Promosso, y) e usa SelectKBest con chi2 per selezionare le 3 feature più informative. Presta attenzione all'ordine delle operazioni rispetto al punto successivo.
    5. Standardizza le feature selezionate con StandardScaler, stampando media e deviazione standard prima e dopo la trasformazione.
    6. (Bonus, facoltativo) Applica una PCA a 1 componente sulle feature standardizzate e stampa la varianza spiegata.

    Articolo in aggiornamento
    Ultimo aggiornamento: 23 agosto 2026

    Carlo Mainardi