Dal Dato Grezzo al Progetto Completo – Tecniche Applicate

Questo modulo segna il passaggio dalla teoria alla pratica intensiva. Ci immergeremo nell'applicazione concreta delle tecniche di Data Science e Machine Learning apprese finora. Copriremo l'intero flusso di lavoro, partendo dall'analisi esplorativa e dalla preparazione avanzata dei dati, passando per la visualizzazione efficace, fino alla realizzazione di un progetto end-to-end che integri tutte queste competenze. L'obiettivo è renderti autonomo/a nell'affrontare un problema di data science nel mondo reale.


6.1 – Analisi Esplorativa (EDA) e Pulizia Dati Profonda 🔍

Prima di poter costruire qualsiasi modello, dobbiamo conoscere a fondo i nostri dati. Questa fase è cruciale per scoprire pattern, identificare problemi e formulare ipotesi. È un lavoro investigativo che pone le basi per tutto il lavoro successivo. Non sottovalutare questa fase: dati di scarsa qualità portano inevitabilmente a modelli di scarsa qualità (Garbage In, Garbage Out).

Approfondimento: L'Arte dell'EDA e della Pulizia

🐍 Codice Dimostrativo (EDA e Pulizia con Pandas/Seaborn/Sklearn)

Python

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.impute import SimpleImputer

# Creare un dataset di esempio più realistico
data = {
    'Eta': [25, 30, np.nan, 45, 22, 30, 50, 35, 45, 22],
    'Genere': ['M', 'F', 'M', 'F', 'F', 'M', np.nan, 'F', 'M', 'F'],
    # Reddito con un outlier evidente (200000)
    'Reddito': [50000, 60000, 55000, 75000, 48000, 62000, 200000, 70000, 78000, 50000],
    'Punteggio': [3.5, 4.0, 3.8, 4.5, 3.2, 4.1, 4.8, 3.9, 4.6, 3.5]
}
df = pd.DataFrame(data)

# Aggiungere una riga duplicata intenzionalmente
df = pd.concat([df, df.iloc[0:1]], ignore_index=True)

print("--- Dataset Iniziale ---")
print(df)

print("\\n--- Info Iniziali e Conteggio Missing Values ---")
df.info() # Tipi di dato e non-null counts
print("\\nValori Mancanti per Colonna:")
print(df.isnull().sum()) # Conteggio NaN per colonna
print("\\nNumero Righe Duplicate:")
print(df.duplicated().sum()) # Conteggio righe duplicate

# --- Pulizia Dati ---
print("\\n--- Inizio Pulizia ---")

# 1. Rimuovi duplicati (mantenendo la prima occorrenza)
df_cleaned = df.drop_duplicates().copy() # Usare .copy() per evitare SettingWithCopyWarning
print(f"Shape dopo drop_duplicates: {df_cleaned.shape}")

# 2. Imputa 'Eta' con la mediana (più robusta all'outlier nel Reddito rispetto alla media)
median_imputer_eta = SimpleImputer(strategy='median')
# fit_transform richiede input 2D, anche per una sola colonna -> df_cleaned[['Eta']]
# Il risultato è un array NumPy, lo riassegnamo alla colonna
df_cleaned['Eta'] = median_imputer_eta.fit_transform(df_cleaned[['Eta']])
print("Valore mediano usato per imputare Eta:", median_imputer_eta.statistics_[0])

# 3. Imputa 'Genere' con la moda (valore più frequente)
mode_imputer_genere = SimpleImputer(strategy='most_frequent')
df_cleaned['Genere'] = mode_imputer_genere.fit_transform(df_cleaned[['Genere']])
print("Valore moda usato per imputare Genere:", mode_imputer_genere.statistics_[0])

print("\\n--- Dopo Pulizia e Imputazione Semplice ---")
print("Valori Mancanti Ora:")
print(df_cleaned.isnull().sum()) # Dovrebbe essere zero
print("Prime righe del df pulito e imputato:")
print(df_cleaned.head())

# 4. Identificazione Outlier nel Reddito usando il metodo IQR
print("\\n--- Identificazione Outlier (Reddito con IQR) ---")
Q1 = df_cleaned['Reddito'].quantile(0.25)
Q3 = df_cleaned['Reddito'].quantile(0.75)
IQR = Q3 - Q1
limite_inf = Q1 - 1.5 * IQR
limite_sup = Q3 + 1.5 * IQR
print(f"Q1 Reddito: {Q1}, Q3 Reddito: {Q3}, IQR: {IQR}")
print(f"Limiti per outlier: Inferiore={limite_inf}, Superiore={limite_sup}")

outliers_reddito = df_cleaned[(df_cleaned['Reddito'] < limite_inf) | (df_cleaned['Reddito'] > limite_sup)]
print(f"Outlier Reddito Identificati ({len(outliers_reddito)}):")
print(outliers_reddito)

# Qui potremmo decidere come trattare l'outlier.
# Esempio: rimuoverlo per analisi successive focalizzate sulla maggioranza dei dati.
df_no_outlier = df_cleaned[~((df_cleaned['Reddito'] < limite_inf) | (df_cleaned['Reddito'] > limite_sup))]
print(f"\\nShape dopo rimozione outlier Reddito: {df_no_outlier.shape}")

# --- EDA Visuale sul df_cleaned (prima della rimozione outlier per vederlo) ---
print("\\n--- EDA Visuale (sul df con outlier per confronto) ---")

# Impostazioni globali per i plot
sns.set_theme(style="whitegrid")
plt.figure(figsize=(16, 12)) # Aumenta dimensione figura
plt.suptitle("Analisi Esplorativa Visuale del Dataset Pulito", fontsize=16, y=1.02)

# Istogramma e KDE per Età
plt.subplot(2, 3, 1)
sns.histplot(data=df_cleaned, x='Eta', kde=True, bins=5) # bins=5 per pochi dati
plt.title('Distribuzione Età (Dopo Imputazione)')

# Countplot per Genere
plt.subplot(2, 3, 2)
sns.countplot(data=df_cleaned, x='Genere', palette='viridis')
plt.title('Distribuzione Genere (Dopo Imputazione)')

# Boxplot Reddito (mostra chiaramente l'outlier)
plt.subplot(2, 3, 3)
sns.boxplot(data=df_cleaned, y='Reddito', palette='magma')
plt.title('Box Plot Reddito (con Outlier)')

# Scatter plot Età vs Reddito, colorato per Genere
plt.subplot(2, 3, 4)
sns.scatterplot(data=df_cleaned, x='Eta', y='Reddito', hue='Genere', s=100, alpha=0.7) # s aumenta dimensione punti
plt.title('Età vs Reddito per Genere')

# Boxplot Punteggio per Genere (confronto distribuzioni)
plt.subplot(2, 3, 5)
sns.boxplot(data=df_cleaned, x='Genere', y='Punteggio', palette='viridis')
plt.title('Distribuzione Punteggio per Genere')

# Matrice di correlazione (solo variabili numeriche)
plt.subplot(2, 3, 6)
corr_matrix = df_cleaned[['Eta', 'Reddito', 'Punteggio']].corr()
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', fmt=".2f", linewidths=.5)
plt.title('Matrice di Correlazione Numerica')

plt.tight_layout(rect=[0, 0, 1, 0.98]) # Aggiusta layout per titolo principale
plt.show()

# Pairplot per una visione d'insieme delle relazioni a coppie (sul df senza outlier)
print("\\n--- Pairplot (sul df senza outlier Reddito) ---")
sns.pairplot(df_no_outlier, hue='Genere', palette='viridis', diag_kind='kde')
plt.suptitle('Pair Plot delle Features (Senza Outlier Reddito)', y=1.02)
plt.show()

💪 Esercizi Pratici

  1. Titanic EDA: Carica il dataset Titanic (seaborn.load_dataset('titanic')). Esegui un'analisi EDA completa:
    • Controlla tipi di dati e valori mancanti (info(), isnull().sum()).
    • Calcola statistiche descrittive (describe(include='all')).
    • Crea almeno 3 visualizzazioni significative:
      • Distribuzione dell'età (Age) usando sns.histplot.
      • Tasso di sopravvivenza (Survived) per classe (Pclass) usando sns.countplot o sns.barplot.
      • Relazione tra età (Age) e prezzo biglietto (Fare), colorando per sopravvivenza (Survived) usando sns.scatterplot.
    • Documenta brevemente le tue osservazioni principali in celle Markdown.
  2. Titanic Outlier Detection (Fare): Sul dataset Titanic, identifica gli outlier nella colonna Fare usando il metodo Z-score. Considera outlier i punti con |Z-score| > 3.
    • Calcola lo Z-score per ogni valore di Fare.
    • Filtra il DataFrame per trovare le righe considerate outlier. Quante ne trovi?
    • Discuti brevemente: in base al contesto del Titanic, questi alti valori di Fare sono probabilmente errori o rappresentano biglietti reali molto costosi (es. suite)? Come influirebbe questa considerazione sulla decisione di rimuoverli o trattarli?
  3. Titanic Imputazione Avanzata (Age): Imputa i valori mancanti nella colonna Age del Titanic usando KNNImputer da sklearn.impute.
    • Usa n_neighbors=5 (valore comune di default).
    • Considera le feature Pclass e Fare come quelle su cui basare la similarità per l'imputazione (dovrai prima fare OHE su Pclass e magari imputare/scalare Fare se necessario per KNNImputer).
    • Crea un istogramma della distribuzione dell'età dopo l'imputazione con KNNImputer.
    • Confrontala visivamente con la distribuzione che otterreesti imputando Age semplicemente con la mediana. Quale approccio ti sembra catturare meglio la variabilità originale (escludendo i NaN)?

💡 Miglioramenti Didattici e Spunti di Riflessione


6.2 – Feature Engineering e Selezione: i Dati ✨

Una volta che i dati sono puliti, possiamo iniziare a modellarli e trasformarli per renderli più "digeribili" e informativi per i nostri algoritmi di Machine Learning. Questa fase, spesso chiamata Feature Engineering, è dove la conoscenza del dominio e la creatività possono fare una grande differenza. Successivamente, selezioniamo le feature più promettenti.

Approfondimento: Creare e Selezionare le Feature Migliori

import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder, OneHotEncoder, StandardScaler, MinMaxScaler, PolynomialFeatures, RobustScaler
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn import set_config
from sklearn.linear_model import LogisticRegression

# Dataset di esempio
df = pd.DataFrame({
    'Eta': [25, 45, 30, 50, 35],
    'Reddito': [50000, 80000, 65000, 95000, 200000],  # outlier
    'Categoria': ['A', 'B', 'A', 'C', 'B'],
    'Priorita': ['Media', 'Alta', 'Bassa', 'Media', 'Alta']
})

# Variabile target
y_target = np.array([0, 1, 0, 1, 1])

print("--- Dataset Originale ---")
print(df)

# Label Encoding per 'Priorita'
priorita_map = {'Bassa': 0, 'Media': 1, 'Alta': 2}
df['Priorita'] = df['Priorita'].map(priorita_map)

print("\\nDataFrame con Priorita Mappata:")
print(df)

# Definizione colonne
numeric_features = ['Eta', 'Reddito', 'Priorita']
categorical_features = ['Categoria']

# Pipeline per numeriche
numeric_transformer = Pipeline(steps=[
    ('scaler', RobustScaler())
])

# Pipeline per categoriche
categorical_transformer = Pipeline(steps=[
    ('onehot', OneHotEncoder(handle_unknown='ignore', drop='first'))
])

# ColumnTransformer
preprocessor = ColumnTransformer(
    transformers=[
        ('num', numeric_transformer, numeric_features),
        ('cat', categorical_transformer, categorical_features)
    ],
    remainder='passthrough'
)

# Applichiamo solo il preprocessing
print("\\n--- Output del solo Preprocessor ---")
X_processed = preprocessor.fit_transform(df)

try:
    feature_names_out = preprocessor.get_feature_names_out()
    df_processed = pd.DataFrame(X_processed, columns=feature_names_out)
    print("DataFrame dopo Preprocessing:")
    print(df_processed)
except Exception as e:
    print(f"Errore nel recuperare i nomi delle feature: {e}")
    print("Output del preprocessor come array NumPy:")
    print(X_processed)

# Feature selection
print("\\n--- Feature Selection con SelectKBest ---")
selector = SelectKBest(score_func=f_classif, k=3)
X_selected = selector.fit_transform(X_processed, y_target)

selected_indices = selector.get_support(indices=True)
try:
    selected_features = [feature_names_out[i] for i in selected_indices]
    df_selected = pd.DataFrame(X_selected, columns=selected_features)
    print("Feature selezionate:")
    print(df_selected)
except NameError:
    print(f"Indici selezionati: {selected_indices}")
    print(X_selected)

# Pipeline completa
print("\\n--- Pipeline Completa con Classificatore ---")
full_pipeline_with_selection = Pipeline(steps=[
    ('preprocessing', preprocessor),
    ('selection', SelectKBest(score_func=f_classif, k=3)),
    ('classifier', LogisticRegression(random_state=42))
])

set_config(display='diagram')
print("Pipeline definita.")
# display(full_pipeline_with_selection)  # Solo in Jupyter

💪 Esercizi Pratici

💡 Miglioramenti Didattici e Spunti di Riflessione


6.3 – Visualizzazione Dati Avanzata: Comunicare Insight 📊