Machine Learning con la Regressione Logistica in Python
Scopri come funziona la regressione logistica, come addestrare classificatori binari e multiclasse in Python con scikit-learn, valutarli e regolare la regolarizzazione.
La regressione logistica è un algoritmo di classificazione supervisionata che stima la probabilità che un campione appartenga a una determinata classe. Nonostante la parola "regressione" nel nome, viene utilizzata per compiti di classificazione — prevedere un'etichetta categorica come spam/non-spam, malattia/sano o clic/nessun-clic.
Questo capitolo tratta:
- Come funziona la regressione logistica (la funzione sigmoid e il log-odds)
- Costruire un classificatore binario in Python con
scikit-learn - Valutare un classificatore oltre alla semplice accuratezza
- Gestire problemi multiclasse
- La regolarizzazione e quando modificarla
- Il ridimensionamento delle feature e perché è importante
- Quando usare la regressione logistica rispetto ad altri classificatori
Come Funziona la Regressione Logistica
Da Regressione Lineare alle Probabilità
La regressione lineare prevede un valore continuo. Se si provasse a usarla per la classificazione, le previsioni potrebbero cadere al di fuori di [0, 1], rendendo impossibile interpretarle come probabilità. La regressione logistica risolve questo problema passando la combinazione lineare attraverso la funzione sigmoid:
σ(z) = 1 / (1 + e^(-z))Dove z = w₀ + w₁x₁ + w₂x₂ + … + wₙxₙ è la somma ponderata delle feature di input. La sigmoid comprime qualsiasi numero reale nell'intervallo (0, 1), fornendo una stima di probabilità valida.
Confine di Decisione
Il modello predice la classe 1 quando la probabilità supera una soglia (predefinita 0.5), e la classe 0 altrimenti:
ŷ = 1 if σ(z) ≥ 0.5
ŷ = 0 if σ(z) < 0.5La soglia σ(z) = 0.5 corrisponde a z = 0, che definisce il confine di decisione — un iperpiano nello spazio delle feature che separa le due classi.
Log-Odds (Logit)
Prendendo il logaritmo del rapporto delle probabilità si mostra perché il modello è lineare nei parametri:
log(p / (1 - p)) = w₀ + w₁x₁ + … + wₙxₙOgni coefficiente wᵢ rappresenta la variazione del log-odds per un aumento di un'unità nella feature xᵢ, mantenendo costanti le altre feature. Questo rende la regressione logistica interpretabile.
Come Vengono Appresi i Parametri
A differenza della regressione lineare, non esiste una soluzione in forma chiusa. Il modello minimizza la funzione di costo log-loss (entropia incrociata) utilizzando un ottimizzatore iterativo (di default lbfgs in scikit-learn):
Loss = -1/m Σ [ yᵢ log(p̂ᵢ) + (1 - yᵢ) log(1 - p̂ᵢ) ]Una log-loss inferiore significa che le probabilità predette sono meglio calibrate rispetto alle etichette reali.
Classificazione Binaria in Python
L'esempio seguente utilizza il dataset Breast Cancer Wisconsin — 569 campioni, 30 feature numeriche, target binario (maligno = 1, benigno = 0). È incluso in scikit-learn, quindi non sono necessari file esterni.
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score, classification_report
# 1. Load data
data = load_breast_cancer()
X, y = data.data, data.target # X: (569, 30) y: 0=malignant, 1=benign
# 2. Split into train (80 %) and test (20 %)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 3. Scale features — logistic regression converges faster and more reliably
# when features are on a similar scale
scaler = StandardScaler()
X_train_sc = scaler.fit_transform(X_train)
X_test_sc = scaler.transform(X_test)
# 4. Train
clf = LogisticRegression(max_iter=1000, random_state=42)
clf.fit(X_train_sc, y_train)
# 5. Evaluate
y_pred = clf.predict(X_test_sc)
print(f"Accuracy: {accuracy_score(y_test, y_pred):.3f}")
print(classification_report(y_test, y_pred, target_names=data.target_names))Output atteso:
Accuracy: 0.974
precision recall f1-score support
malignant 0.98 0.95 0.96 43
benign 0.97 0.99 0.98 71
accuracy 0.97 114
macro avg 0.97 0.97 0.97 114
weighted avg 0.97 0.97 0.97 114Il modello raggiunge circa il 97% di accuratezza sui dati non visti. Si noti che LogisticRegression di scikit-learn aggiunge di default la regolarizzazione L2 (C=1.0), che aiuta la generalizzazione.
Perché il Ridimensionamento delle Feature è Importante
La regressione logistica utilizza un'ottimizzazione basata sul gradiente. Senza ridimensionamento, una feature con valori grandi (ad es., raggio medio ~14) domina gli aggiornamenti del gradiente, rallentando la convergenza o causando il fallimento del solver. StandardScaler trasforma ogni feature con media zero e varianza unitaria.
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
data = load_breast_cancer()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Without scaling — needs more iterations, may warn about convergence
clf_raw = LogisticRegression(max_iter=200, random_state=42)
clf_raw.fit(X_train, y_train)
print(f"Unscaled accuracy : {clf_raw.score(X_test, y_test):.3f}")
# With scaling
scaler = StandardScaler()
clf_sc = LogisticRegression(max_iter=200, random_state=42)
clf_sc.fit(scaler.fit_transform(X_train), y_train)
print(f"Scaled accuracy : {clf_sc.score(scaler.transform(X_test), y_test):.3f}")Addestrare sempre lo scaler solo sul set di addestramento, quindi usare lo stesso scaler già adattato per trasformare sia il set di addestramento che quello di test — questo previene il data leakage.
Valutare un Classificatore
L'accuratezza da sola può essere fuorviante quando le classi sono sbilanciate. Usa una matrice di confusione e le metriche derivate da essa.
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt
data = load_breast_cancer()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
scaler = StandardScaler()
X_train_sc = scaler.fit_transform(X_train)
X_test_sc = scaler.transform(X_test)
clf = LogisticRegression(max_iter=1000, random_state=42)
clf.fit(X_train_sc, y_train)
y_pred = clf.predict(X_test_sc)
cm = confusion_matrix(y_test, y_pred)
disp = ConfusionMatrixDisplay(cm, display_labels=data.target_names)
disp.plot(cmap="Blues")
plt.title("Logistic Regression — Breast Cancer")
plt.tight_layout()
plt.savefig("confusion_matrix.png", dpi=150)
plt.show()Metriche chiave dalla matrice di confusione:
| Metrica | Formula | Significato |
|---|---|---|
| Precisione | TP / (TP + FP) | Di tutti i positivi previsti, quanti sono davvero positivi |
| Recall (Sensibilità) | TP / (TP + FN) | Di tutti i positivi reali, quanti il modello ha rilevato |
| F1-score | 2 × (P × R) / (P + R) | Media armonica di precisione e recall |
| Specificità | TN / (TN + FP) | Di tutti i negativi reali, quanti il modello ha correttamente escluso |
Nella diagnosi medica, il recall (sensibilità) è spesso più importante della precisione — un tumore maligno non rilevato è peggio di un falso allarme.
Punteggi di Probabilità e la Curva AUC-ROC
Invece di una previsione definitiva, puoi recuperare la probabilità della classe positiva con predict_proba():
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import roc_auc_score
data = load_breast_cancer()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
scaler = StandardScaler()
clf = LogisticRegression(max_iter=1000, random_state=42)
clf.fit(scaler.fit_transform(X_train), y_train)
# Probability of the positive class (benign = 1)
y_proba = clf.predict_proba(scaler.transform(X_test))[:, 1]
print(f"AUC-ROC: {roc_auc_score(y_test, y_proba):.3f}")Output atteso:
AUC-ROC: 0.997Un AUC vicino a 1.0 significa che il modello classifica i campioni positivi sopra quelli negativi quasi perfettamente. Consulta il capitolo Curva AUC-ROC per come tracciare e interpretare la curva completa.
Classificazione Multiclasse
Quando il target ha più di due classi, scikit-learn estende automaticamente la regressione logistica. Da scikit-learn 1.5 in poi, il solver lbfgs usa sempre l'approccio multinomiale (softmax), che addestra un singolo modello con uno strato di output softmax e minimizza l'entropia incrociata su tutte le classi congiuntamente. Questo è generalmente più accurato della vecchia strategia One-vs-Rest (OvR), che addestrava un classificatore binario separato per ogni classe.
Il dataset Iris ha tre specie di fiori — un esempio naturale di classificazione multiclasse:
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score
data = load_iris()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
scaler = StandardScaler()
X_train_sc = scaler.fit_transform(X_train)
X_test_sc = scaler.transform(X_test)
# From scikit-learn 1.5+, multinomial softmax is the default for lbfgs
clf = LogisticRegression(solver="lbfgs", max_iter=1000, random_state=42)
clf.fit(X_train_sc, y_train)
y_pred = clf.predict(X_test_sc)
print(f"Accuracy: {accuracy_score(y_test, y_pred):.3f}")
# Class probabilities for the first three test samples
print("\nClass probabilities (first 3 samples):")
for proba in clf.predict_proba(X_test_sc)[:3]:
print([f"{p:.3f}" for p in proba])Output atteso:
Accuracy: 1.000
Class probabilities (first 3 samples):
['0.011', '0.876', '0.113']
['0.964', '0.036', '0.000']
['0.000', '0.003', '0.997']Regolarizzazione
La regolarizzazione penalizza i coefficienti grandi per prevenire l'overfitting. scikit-learn fornisce due tipi tramite il parametro penalty:
| Parametro | Tipo | Effetto |
|---|---|---|
penalty='l2' (default) | Ridge | Riduce tutti i coefficienti verso zero; mantiene tutte le feature |
penalty='l1' | Lasso | Porta alcuni coefficienti esattamente a zero; selezione implicita delle feature |
penalty='elasticnet' | Mix | Combina L1 e L2; richiede solver='saga' |
penalty=None | Nessuna | Nessuna regolarizzazione; da usare solo se i dati sono grandi e puliti |
La forza della regolarizzazione è controllata da C (l'inverso della forza di regolarizzazione — un C più piccolo significa una regolarizzazione più forte):
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
import numpy as np
data = load_breast_cancer()
X, y = data.data, data.target
results = {}
for C in [0.001, 0.01, 0.1, 1.0, 10.0, 100.0]:
pipe = Pipeline([
("scaler", StandardScaler()),
("clf", LogisticRegression(C=C, max_iter=1000, random_state=42)),
])
scores = cross_val_score(pipe, X, y, cv=5, scoring="accuracy")
results[C] = scores.mean()
print(f"C={C:7.3f} CV accuracy: {scores.mean():.4f} ± {scores.std():.4f}")Questo utilizza la cross-validation per trovare il valore di C che generalizza meglio. Per una ricerca sistematica su più iperparametri, consulta Grid Search.
Usare una Pipeline
Una Pipeline concatena la pre-elaborazione e il modello in un singolo oggetto. Questo previene il data leakage accidentale e semplifica la cross-validation e il deployment:
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score
data = load_breast_cancer()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
pipe = Pipeline([
("scaler", StandardScaler()),
("clf", LogisticRegression(C=1.0, max_iter=1000, random_state=42)),
])
pipe.fit(X_train, y_train)
y_pred = pipe.predict(X_test)
print(f"Accuracy: {accuracy_score(y_test, y_pred):.3f}")
# Predict probabilities on a new sample (raw, unscaled)
new_sample = X_test[:1] # first test sample
print(f"Predicted class : {pipe.predict(new_sample)[0]}")
print(f"Class probability : {pipe.predict_proba(new_sample)[0]}")Output atteso:
Accuracy: 0.974
Predicted class : 1
Class probability : [0.11359025 0.88640975]La pipeline gestisce il ridimensionamento internamente — si chiama predict() con i valori delle feature grezze.
Ispezionare i Coefficienti
I coefficienti addestrati rivelano quali feature spingono la previsione verso ogni classe. Valori assoluti più grandi significano un'influenza più forte:
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
import numpy as np
data = load_breast_cancer()
X, y = data.data, data.target
scaler = StandardScaler()
clf = LogisticRegression(max_iter=1000, random_state=42)
clf.fit(scaler.fit_transform(X), y)
# Sort by absolute coefficient value
coefs = clf.coef_[0] # shape (n_features,) for binary classification
sorted_idx = np.argsort(np.abs(coefs))[::-1]
print(f"{'Feature':<35} {'Coefficient':>12}")
print("-" * 48)
for i in sorted_idx[:5]:
print(f"{data.feature_names[i]:<35} {coefs[i]:>12.4f}")Output atteso (prime 5 feature per peso assoluto):
Feature Coefficient
------------------------------------------------
worst texture -1.3206
radius error -1.2893
worst radius -1.0266
area error -0.9989
worst area -0.9947I coefficienti negativi (dopo il ridimensionamento) spingono verso la classe 0 (maligno); i coefficienti positivi spingono verso la classe 1 (benigno).
Vantaggi e Limitazioni
Quando usare la regressione logistica
- Hai bisogno di stime di probabilità, non solo etichette di classe.
- La relazione tra le feature e il log-odds è approssimativamente lineare.
- Hai bisogno di un modello interpretabile — i coefficienti sono significativi.
- Come baseline veloce prima di provare modelli più complessi come gli Alberi di Decisione o i metodi ensemble.
- I dataset sono grandi (la regressione logistica scala bene con molti campioni).
Limitazioni
| Limitazione | Mitigazione |
|---|---|
| Assume un confine di decisione lineare | Usa le feature polinomiali o passa all'Albero di Decisione / K-Nearest Neighbors |
| Sensibile alla scala delle feature | Applica sempre StandardScaler o MinMaxScaler |
| Difficoltà con feature altamente correlate | Elimina o regolarizza con L1 (penalty='l1') |
| Non adatta per interazioni tra feature molto complesse | Usa metodi ensemble o reti neurali |
Regressione Logistica vs. Classificatori Correlati
| Algoritmo | Confine di decisione | Scaling necessario | Output probabilistico |
|---|---|---|---|
| Regressione Logistica | Lineare | Sì | Sì (calibrato) |
| Albero di Decisione | Non lineare (allineato agli assi) | No | Sì (meno calibrato) |
| K-Nearest Neighbors | Non lineare (basato sulle istanze) | Sì | Sì |
| Regressione Lineare | Lineare (output continuo) | Sì | No |
Punti Chiave
- La regressione logistica stima una probabilità usando la funzione sigmoid; la classe viene assegnata sogliando quella probabilità.
- Ridimensiona sempre le feature con
StandardScalerprima dell'addestramento — accelera la convergenza e migliora l'accuratezza. - Usa una Pipeline per raggruppare il ridimensionamento e il modello; previene il data leakage e semplifica il deployment.
- Valuta con precisione, recall, F1 e AUC-ROC piuttosto che con la sola accuratezza, specialmente con dati sbilanciati. Consulta i capitoli sulla Matrice di Confusione e sulla Curva AUC-ROC.
- Controlla l'overfitting con il parametro
C(più piccolo = regolarizzazione più forte); usa la cross-validation o la grid search per ottimizzarlo. - Per i problemi multiclasse, usa
solver="lbfgs"(il default); scikit-learn 1.5+ usa sempre softmax (multinomiale) che gestisce bene le classi sovrapposte.