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.

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.
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.
# 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.
# 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 vectorIl 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.
# 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.
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.
# 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.
Pronto a superare i tuoi colloqui su Data Science & ML?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
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.
# 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.
# 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.
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.
# 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
ColumnTransformerapplica trasformazioni eterogenee, ilGridSearchCVottimizza gli iperparametri e la serializzazione conjoblibporta 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.
Inizia a praticare!
Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.
Tag
Condividi
Articoli correlati

Algoritmi di Machine Learning Spiegati: Guida Completa per i Colloqui Tecnici
Una guida completa agli algoritmi di machine learning per affrontare i colloqui tecnici nel 2026: regressione, classificazione, clustering, metriche di valutazione e regolarizzazione con esempi in Python e scikit-learn.

Top 25 Domande di Colloquio per Data Scientist nel 2026
Le 25 domande più frequenti nei colloqui per data scientist nel 2026, con risposte dettagliate, esempi di codice Python e strategie per affrontare ogni argomento con sicurezza.