# Python per la Data Science: NumPy, Pandas e Scikit-Learn nel 2026 > Guida pratica a NumPy 2.1, Pandas 2.2 e Scikit-Learn 1.6 con Python 3.12. Dalla pulizia dei dati al feature engineering fino alla pipeline ML completa, con esempi di codice eseguibili passo dopo passo. - Published: 2026-04-07 - Updated: 2026-04-07 - Author: Anthony Fillion-Maillet - Tags: data-science, python, numpy, pandas, scikit-learn, machine-learning, tutorial - Reading time: 10 min --- Nel 2026, Python resta il linguaggio di riferimento per la data science non per moda, ma per la solidità del suo ecosistema. Tre librerie in particolare costituiscono la spina dorsale di qualsiasi progetto analitico: NumPy per il calcolo numerico ad alte prestazioni, Pandas per la manipolazione di dati tabulari e Scikit-Learn per la costruzione di modelli di machine learning. Padroneggiare queste tre librerie consente di gestire l'intero ciclo di vita di un progetto data science — dall'acquisizione dei dati grezzi alla messa in produzione del modello addestrato. Questo articolo illustra un percorso completo attraverso un caso pratico: l'analisi e la previsione delle promozioni aziendali. > **Versioni utilizzate** > > Tutti gli esempi di codice sono stati sviluppati e verificati con Python 3.12+, NumPy 2.1, Pandas 2.2 e Scikit-Learn 1.6. Le versioni precedenti potrebbero presentare differenze nelle API, in particolare per il comportamento Copy-on-Write di Pandas 2.2 e le nuove compatibilità Array API di NumPy 2.1. ## Operazioni sugli Array NumPy per Calcoli Efficienti NumPy sostituisce le liste Python e i cicli classici con operazioni vettorizzate basate internamente su codice C ottimizzato. Invece di iterare elemento per elemento con un ciclo `for`, le funzioni NumPy operano sull'intero array simultaneamente, garantendo incrementi di velocità che vanno da 10 a 100 volte rispetto al Python puro. Questo vantaggio diventa determinante quando si lavora con dataset di grandi dimensioni, dove anche operazioni semplici come il calcolo di una media possono richiedere tempi significativi con le strutture dati native. L'esempio seguente mostra le operazioni fondamentali: creazione di array da liste Python, aritmetica vettorizzata tra array e il Boolean Indexing, un meccanismo che permette di filtrare i dati senza ricorrere a cicli espliciti. ```python # numpy_basics.py import numpy as np # Create arrays from different sources prices = np.array([29.99, 49.99, 19.99, 99.99, 39.99]) quantities = np.arange(1, 6) # [1, 2, 3, 4, 5] # Vectorized arithmetic — no loops needed revenue = prices * quantities print(revenue) # [29.99, 99.98, 59.97, 399.96, 199.95] # Statistical aggregations print(f"Total revenue: ${revenue.sum():.2f}") # $789.85 print(f"Mean price: ${prices.mean():.2f}") # $47.99 print(f"Std deviation: ${prices.std():.2f}") # $27.64 # Boolean indexing — filter without loops premium_mask = prices > 40 premium_items = prices[premium_mask] # [49.99, 99.99] ``` Il Boolean Indexing rappresenta uno dei pattern fondamentali di NumPy: l'espressione `prices > 40` genera un array di valori `True` e `False` che funge direttamente da maschera di selezione. Questo stesso principio si ritrova nei DataFrame di Pandas, che internamente si appoggiano proprio su NumPy. Le funzioni di aggregazione statistica — `sum()`, `mean()`, `std()` — operano per default sull'intero array, ma possono essere applicate lungo assi specifici nei contesti multidimensionali grazie al parametro `axis`, come si vedrà nella sezione successiva. ## Reshape e Broadcasting in NumPy Nella pratica quotidiana della data science, gli array multidimensionali sono onnipresenti: dati di vendita organizzati per prodotto e mese, valori dei pixel nelle immagini, matrici di feature per i modelli di machine learning. NumPy offre strumenti potenti per la trasformazione di queste strutture tramite `reshape()`, oltre al meccanismo di Broadcasting che consente operazioni aritmetiche tra array di dimensioni diverse senza necessità di copie esplicite dei dati. ```python # numpy_reshape.py import numpy as np # Monthly sales data: 4 products x 3 months sales = np.array([ [120, 150, 130], # Product A [200, 180, 220], # Product B [90, 110, 95], # Product C [300, 280, 310], # Product D ]) # Column-wise mean (average per month) monthly_avg = sales.mean(axis=0) # [177.5, 180.0, 188.75] # Row-wise sum (total per product) product_totals = sales.sum(axis=1) # [400, 600, 295, 890] # Normalize each product relative to its own max normalized = sales / sales.max(axis=1, keepdims=True) # keepdims=True preserves the shape for broadcasting print(normalized[0]) # [0.8, 1.0, 0.867] — Product A relative to its peak # Reshape for Scikit-Learn (requires 2D input) flat_sales = sales.flatten() # 1D array of 12 values reshaped = flat_sales.reshape(-1, 1) # 12x1 column vector ``` Il parametro `axis` controlla la direzione dell'aggregazione: `axis=0` aggrega lungo le colonne (attraverso le righe), mentre `axis=1` aggrega lungo le righe (attraverso le colonne). Nella normalizzazione, il parametro `keepdims=True` mantiene la shape `(4, 1)` nel risultato di `max()`, permettendo al Broadcasting di funzionare correttamente durante la divisione con l'array originale `(4, 3)`. L'ultima operazione — `reshape(-1, 1)` — converte un vettore monodimensionale in una matrice a colonna singola, un requisito obbligatorio per gli input di Scikit-Learn. Il valore `-1` indica a NumPy di calcolare automaticamente la dimensione mancante. ## Manipolazione e Pulizia dei DataFrame con Pandas Pandas rappresenta lo strumento principale per la gestione dei dati tabulari in Python. Un DataFrame si comporta in modo simile a una tabella di un database relazionale o a un foglio Excel, ma offre capacità di trasformazione decisamente superiori. Nella pratica, la pulizia dei dati costituisce la fase più dispendiosa in termini di tempo in qualsiasi progetto di data science: valori mancanti, outlier, duplicati e tipi di dato incoerenti richiedono un trattamento sistematico e riproducibile. L'approccio consigliato prevede la costruzione di una catena di trasformazioni che lascia intatto il DataFrame originale e salva il risultato in una nuova variabile. L'esempio seguente lavora con un dataset di 1.500 profili di candidati per un processo di assunzione. ```python # pandas_cleaning.py import pandas as pd import numpy as np # Load and inspect raw data df = pd.read_csv("candidates.csv") print(df.shape) # (1500, 8) print(df.dtypes) # Check column types print(df.isna().sum()) # Count missing values per column # Clean in a reproducible chain df_clean = ( df .dropna(subset=["salary", "experience_years"]) # Drop rows missing critical fields .assign( salary=lambda x: x["salary"].clip(lower=20000, upper=500000), # Cap outliers experience_years=lambda x: x["experience_years"].astype(int), hired_date=lambda x: pd.to_datetime(x["hired_date"], errors="coerce"), ) .drop_duplicates(subset=["email"]) # Remove duplicate candidates .query("experience_years >= 0") # Filter invalid entries .reset_index(drop=True) ) print(f"Cleaned: {len(df)} -> {len(df_clean)} rows") ``` Il method chaining (concatenazione di metodi) consente di esprimere l'intera pipeline di pulizia come un'unica espressione leggibile. Ogni metodo restituisce un nuovo DataFrame, creando una catena immutabile che facilita il debug e la riproduzione dei risultati. Il metodo `.assign()` con funzioni lambda permette di creare o sovrascrivere colonne in modo dichiarativo, mentre `.clip()` limita i valori numerici entro un intervallo definito per gestire gli outlier. Il parametro `errors="coerce"` in `pd.to_datetime()` converte le date non valide in `NaT` (Not a Time) invece di generare un'eccezione, rendendo la pipeline robusta anche in presenza di dati corrotti. Per approfondire le tecniche di pulizia dei dati nei colloqui tecnici, si consultino le [domande sul feature engineering](/technologies/data-science/interview-questions/feature-engineering). > **Pandas 2.2 Copy-on-Write** > > A partire da Pandas 2.2, il meccanismo Copy-on-Write (CoW) è attivo per default. Questo significa che un'istruzione come `df2 = df[['col_a', 'col_b']]` non crea più una vista modificabile sull'originale, ma ritarda la copia fino al momento in cui `df2` viene effettivamente modificato. Il vantaggio principale riguarda la riduzione del consumo di memoria e l'eliminazione del famigerato `SettingWithCopyWarning`. Per il codice nuovo non servono adattamenti particolari, ma il codice legacy che modifica slice in-place potrebbe richiedere una revisione. ## Aggregazioni GroupBy e Feature Engineering con Pandas Dopo la fase di pulizia, il passo successivo consiste nell'estrarre informazioni aggregate e costruire nuove feature che possano alimentare un modello di machine learning. L'operazione GroupBy di Pandas segue il paradigma split-apply-combine: suddivide il DataFrame in gruppi basati su una o più colonne, applica una funzione di aggregazione a ciascun gruppo e ricombina i risultati in un nuovo DataFrame. La vera potenza emerge quando si combinano le aggregazioni con il feature engineering, creando variabili derivate che catturano relazioni non evidenti nei dati grezzi. Il rapporto tra lo stipendio individuale e la media del dipartimento, ad esempio, fornisce un indicatore relativo molto più informativo del valore assoluto. ```python # pandas_groupby.py import pandas as pd # Aggregate candidate stats by department dept_stats = ( df_clean .groupby("department") .agg( avg_salary=("salary", "mean"), median_experience=("experience_years", "median"), headcount=("email", "count"), max_salary=("salary", "max"), ) .sort_values("avg_salary", ascending=False) ) print(dept_stats.head()) # Create features for ML: encode categorical + add aggregated stats df_features = ( df_clean .assign( # Ratio of individual salary to department average salary_ratio=lambda x: x["salary"] / x.groupby("department")["salary"].transform("mean"), # Time since hire in days tenure_days=lambda x: (pd.Timestamp.now() - x["hired_date"]).dt.days, # Binary encoding is_senior=lambda x: (x["experience_years"] >= 5).astype(int), ) ) ``` Il metodo `.transform()` rappresenta un elemento chiave del feature engineering con Pandas: a differenza di `.agg()` che restituisce un valore per gruppo, `.transform()` restituisce un valore per ogni riga del DataFrame originale, mantenendo l'allineamento degli indici. Questo consente di calcolare statistiche a livello di gruppo (come la media dello stipendio per dipartimento) e assegnarle direttamente a ciascuna riga, rendendo possibile il calcolo di rapporti e deviazioni rispetto al gruppo di appartenenza. La sintassi con Named Aggregation (argomenti con nome in `.agg()`) migliora la leggibilità rispetto alla notazione con dizionari utilizzata nelle versioni precedenti di Pandas. ## Costruire una Pipeline Scikit-Learn da Zero Scikit-Learn adotta un'architettura a pipeline che incapsula preprocessing e modello in un unico oggetto coerente. Questo approccio risolve uno dei problemi più comuni nei progetti di machine learning: la separazione tra le trasformazioni applicate ai dati di addestramento e quelle applicate ai dati di test. Con una Pipeline, la stessa sequenza di operazioni viene garantita in entrambi i casi, eliminando il rischio di inconsistenze. Il `ColumnTransformer` estende questo concetto consentendo di applicare trasformazioni diverse a sottoinsiemi di colonne: scalatura per le variabili numeriche, codifica one-hot per le variabili categoriche. Il risultato viene assemblato automaticamente in una matrice di feature pronta per l'addestramento. ```python # sklearn_pipeline.py from sklearn.model_selection import train_test_split from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.ensemble import GradientBoostingClassifier from sklearn.metrics import classification_report import pandas as pd # Define column groups numeric_features = ["salary", "experience_years", "salary_ratio", "tenure_days"] categorical_features = ["department", "role_level"] target = "promoted" # Split before any preprocessing X = df_features[numeric_features + categorical_features] y = df_features[target] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # Build the preprocessing + model pipeline preprocessor = ColumnTransformer( transformers=[ ("num", StandardScaler(), numeric_features), # Scale numeric columns ("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features), # Encode categories ] ) pipeline = Pipeline([ ("preprocessor", preprocessor), ("classifier", GradientBoostingClassifier( n_estimators=200, learning_rate=0.1, max_depth=4, random_state=42, )), ]) # Train and evaluate pipeline.fit(X_train, y_train) y_pred = pipeline.predict(X_test) print(classification_report(y_test, y_pred)) ``` Il `ColumnTransformer` accetta una lista di tuple `(nome, trasformatore, colonne)` e applica ciascun trasformatore esclusivamente alle colonne specificate. Lo `StandardScaler` normalizza le feature numeriche sottraendo la media e dividendo per la deviazione standard, garantendo che tutte le variabili contribuiscano equamente al modello. L'`OneHotEncoder` con `handle_unknown="ignore"` gestisce in modo sicuro eventuali categorie presenti nel set di test ma assenti nell'addestramento, evitando errori in produzione. La suddivisione dei dati con `stratify=y` assicura che la distribuzione della variabile target sia proporzionalmente rappresentata sia nel training set che nel test set, un dettaglio fondamentale quando si lavora con classi sbilanciate. ## Cross-Validation e Ottimizzazione degli Iperparametri La valutazione di un modello su un singolo split train/test fornisce una stima puntuale delle prestazioni, soggetta alla varianza introdotta dalla particolare suddivisione scelta. La cross-validation risolve questo problema suddividendo ripetutamente i dati in fold diversi, producendo una distribuzione di punteggi che offre una misura molto più affidabile della capacità di generalizzazione del modello. Il `GridSearchCV` combina la cross-validation con una ricerca sistematica nello spazio degli iperparametri, valutando tutte le combinazioni possibili dei valori specificati. Con il parametro `n_jobs=-1`, la ricerca viene parallelizzata su tutti i core disponibili della CPU, riducendo significativamente i tempi di calcolo. ```python # sklearn_tuning.py from sklearn.model_selection import cross_val_score, GridSearchCV import numpy as np # 5-fold cross-validation on the full pipeline scores = cross_val_score(pipeline, X_train, y_train, cv=5, scoring="f1") print(f"F1 scores: {scores}") print(f"Mean F1: {scores.mean():.3f} (+/- {scores.std() * 2:.3f})") # Grid search over hyperparameters param_grid = { "classifier__n_estimators": [100, 200, 300], "classifier__max_depth": [3, 4, 5], "classifier__learning_rate": [0.05, 0.1, 0.2], } grid_search = GridSearchCV( pipeline, param_grid, cv=5, scoring="f1", n_jobs=-1, # Use all CPU cores verbose=1, ) grid_search.fit(X_train, y_train) print(f"Best params: {grid_search.best_params_}") print(f"Best F1: {grid_search.best_score_:.3f}") # Evaluate the best model on held-out test set best_model = grid_search.best_estimator_ print(classification_report(y_test, best_model.predict(X_test))) ``` La sintassi con doppio underscore (`classifier__n_estimators`) costituisce la convenzione di Scikit-Learn per accedere ai parametri dei componenti annidati all'interno di una Pipeline. Il prefisso `classifier` corrisponde al nome assegnato allo step nella definizione della Pipeline, mentre il suffisso dopo `__` indica il parametro specifico dell'estimatore. Questa notazione permette di ottimizzare simultaneamente i parametri del preprocessore e del modello in un'unica Grid Search. Il punteggio F1 viene scelto come metrica di valutazione in quanto bilancia precisione e recall, risultando particolarmente adatto nei casi di classificazione con classi sbilanciate come la previsione delle promozioni. > **Attenzione al Data Leakage** > > Lo split dei dati deve avvenire sempre prima di qualsiasi operazione di preprocessing. Se si calcola la media o la deviazione standard sull'intero dataset e solo successivamente si esegue la suddivisione train/test, le informazioni del test set contaminano il training set, producendo metriche di valutazione ottimistiche e inaffidabili. La Pipeline di Scikit-Learn previene questo problema automaticamente, applicando il `fit` dei trasformatori esclusivamente sui dati di addestramento. ## Salvare e Caricare Modelli per la Produzione Una volta identificato il modello migliore, il passaggio alla produzione richiede la serializzazione dell'intera Pipeline — non solo del classificatore, ma anche dei trasformatori di preprocessing. La libreria `joblib`, inclusa nell'ecosistema Scikit-Learn, offre una serializzazione efficiente per oggetti contenenti array NumPy di grandi dimensioni, risultando significativamente più veloce rispetto al modulo `pickle` standard di Python. Salvare la Pipeline completa garantisce che le stesse identiche trasformazioni (scalatura, codifica) vengano applicate ai nuovi dati in fase di inferenza, eliminando il rischio di disallineamento tra le feature di addestramento e quelle di produzione. ```python # sklearn_export.py import joblib from pathlib import Path # Save the complete pipeline (preprocessor + model) model_dir = Path("models") model_dir.mkdir(exist_ok=True) joblib.dump(best_model, model_dir / "promotion_model_v1.joblib") # Load and predict in a different process loaded_model = joblib.load(model_dir / "promotion_model_v1.joblib") new_data = pd.DataFrame({ "salary": [75000], "experience_years": [4], "salary_ratio": [1.05], "tenure_days": [730], "department": ["Engineering"], "role_level": ["Mid"], }) prediction = loaded_model.predict(new_data) probability = loaded_model.predict_proba(new_data)[:, 1] print(f"Promoted: {bool(prediction[0])}, Confidence: {probability[0]:.2%}") ``` Il file `.joblib` contiene l'intero grafo della Pipeline: il `ColumnTransformer` con i parametri appresi durante il `fit` (media e deviazione standard per lo `StandardScaler`, categorie note per l'`OneHotEncoder`) e il `GradientBoostingClassifier` addestrato. Il metodo `predict_proba()` restituisce le probabilità per ciascuna classe, consentendo di definire soglie personalizzate di decisione. In un contesto di produzione reale, si consiglia di aggiungere il versionamento del modello — come mostra il suffisso `_v1` nel nome del file — e di registrare le metriche di valutazione associate a ciascuna versione per facilitare il confronto e l'eventuale rollback. ## Conclusione Il percorso illustrato in questo articolo copre l'intero ciclo di vita di un progetto di data science con Python, dalla manipolazione dei dati grezzi alla messa in produzione del modello. I punti chiave da ricordare: - **NumPy** fornisce le fondamenta per il calcolo numerico efficiente: operazioni vettorizzate, Boolean Indexing e Broadcasting eliminano la necessità dei cicli Python e garantiscono prestazioni ottimali. - **Pandas** gestisce l'intero workflow di data wrangling: caricamento, pulizia con method chaining, aggregazioni GroupBy e feature engineering con `.transform()`. Con Pandas 2.2, il Copy-on-Write migliora ulteriormente la gestione della memoria. - **Scikit-Learn** struttura il processo di modellazione attraverso le Pipeline: il `ColumnTransformer` applica trasformazioni eterogenee, il `GridSearchCV` ottimizza gli iperparametri e la serializzazione con `joblib` porta il modello in produzione. - Lo split dei dati deve sempre precedere il preprocessing per evitare il data leakage. - La cross-validation con punteggio F1 offre una valutazione robusta delle prestazioni, in particolare con classi sbilanciate. - Il salvataggio della Pipeline completa — non solo del modello — garantisce la coerenza tra ambiente di sviluppo e produzione. Per mettere alla prova queste competenze in un contesto di colloquio tecnico, si esplorino le domande pratiche disponibili sulla piattaforma. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/it/blog/data-science/python-data-science-numpy-pandas-scikit-learn-2026