Sentiment analysis in Python

Come fa un programma a capire se una recensione è entusiasta o furiosa? In questo tutorial costruiamo un classificatore di sentiment completo con scikit-learn, da testo grezzo a polarità, e guardiamo dentro il modello per scoprire quali parole pesano davvero.

Hai mai scorso le recensioni di un ristorante chiedendoti: «ma in media, la gente è contenta o no?». Con dieci recensioni le leggi tutte. Con diecimila, no. Eppure un’azienda che riceve diecimila recensioni al mese ha bisogno di quella risposta ogni giorno. La sentiment analysis serve esattamente a questo: insegnare a un programma a leggere un testo e stimarne la polarità — positivo, negativo, a volte neutro.

In questo tutorial costruiamo un classificatore di sentiment completo in Python, dall’inizio alla fine: testo grezzo in ingresso, etichetta di polarità in uscita. E alla fine apriremo il cofano per vedere quali parole hanno convinto il modello — perché un classificatore che non sai spiegare è un classificatore di cui non ti puoi fidare.

Il problema: i modelli mangiano numeri, non parole

Se hai già seguito il tutorial sulla regressione logistica sai che i modelli di machine learning lavorano su vettori di numeri. Se non lo ricordi, ti basta sapere questo: un classificatore è una funzione che prende in ingresso una lista di numeri e restituisce una classe. Il problema della sentiment analysis, quindi, non è tanto il classificatore — è trasformare una frase in numeri senza perdere il suo significato.

Lo strumento classico è TF-IDF (term frequency – inverse document frequency). L’idea è semplice se la pensi come un investigatore che pesa gli indizi: una parola che compare in tutte le recensioni («il», «che», «pizza» se parliamo di pizzerie) è un indizio che non vale niente, come un’impronta digitale trovata in un luogo pubblico. Una parola rara nel corpus ma frequente in quella recensione («immangiabile», «sublime») è un indizio pesantissimo. TF-IDF assegna a ogni parola un peso proporzionale a quanto è distintiva.

Un classificatore di sentiment non capisce le frasi: impara quali parole, pesate bene, spostano l’ago verso il positivo o il negativo.

Da testo a polarità: la pipeline Testo grezzo «Servizio lento, cibo immangiabile, mai più.» Vettore TF-IDF lento → 0.48 immangiabile → 0.71 cibo → 0.09 Regressione logistica somma pesata + soglia negativo positivo Le parole distintive pesano molto, quelle comuni quasi nulla: la polarità emerge dalla somma.

La pipeline completa: il testo diventa un vettore di pesi TF-IDF, il classificatore somma i contributi e sceglie la polarità.

Prepara l’ambiente

Ti serve Python 3.12 e un ambiente virtuale pulito. Se usi uv (il gestore di pacchetti moderno, velocissimo), bastano due righe; con il classico venv funziona uguale:

# Con uv (consigliato)
uv venv && source .venv/bin/activate
uv pip install scikit-learn

# Oppure, alla vecchia maniera
python3.12 -m venv .venv && source .venv/bin/activate
pip install scikit-learn

Apri VS Code o PyCharm, crea un file sentiment.py e sei pronto. Niente GPU, niente API: gira tutto sul tuo portatile in meno di un secondo.

Il codice completo: dal corpus alla polarità

Per rendere il tutorial eseguibile senza scaricare nulla, usiamo un piccolo corpus di recensioni in italiano scritto a mano. Nella pratica reale lo sostituirai con migliaia di esempi (ne parliamo alla fine), ma la struttura del codice resta identica al 100%.

# sentiment.py — classificatore di sentiment con scikit-learn
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.metrics import classification_report

# Il corpus: recensioni etichettate (1 = positivo, 0 = negativo).
# Nella realta' ne servono migliaia: qui bastano per capire il meccanismo.
recensioni = [
    # --- positive ---
    "Cibo eccellente e servizio impeccabile, torneremo di sicuro",
    "Esperienza fantastica, personale gentile e piatti curati",
    "Ottimo rapporto qualita' prezzo, consigliatissimo",
    "Pizza sublime, la migliore della citta'",
    "Ambiente accogliente e dolci strepitosi, che scoperta",
    "Tutto perfetto, dalla cantina al dessert",
    "Porzioni generose e sapori autentici, bravissimi",
    "Location splendida e antipasti deliziosi",
    "Serata meravigliosa, ci siamo sentiti coccolati",
    "Piatti eccellenti e presentazione impeccabile",
    "Menu creativo e ingredienti freschissimi, promosso",
    "Chef bravissimo, cena indimenticabile",
    "Accoglienza calorosa e servizio attento",
    "Pesce freschissimo e cottura perfetta",
    "Personale cordiale e ambiente rilassante",
    "Sapori genuini e porzioni abbondanti, ottimo",
    "Antipasto di mare delizioso, davvero eccellente",
    "Prezzo onesto e qualita' sorprendente",
    "Dolce spettacolare, cucina meravigliosa",
    "Consiglio vivamente, esperienza fantastica",
    "Vini pregiati e abbinamenti azzeccati",
    "Cucina raffinata, che meraviglia",
    "Tavolo con vista e servizio impeccabile",
    "Ristorante splendido, piatti curati e gustosi",
    # --- negative ---
    "Servizio lento e cibo immangiabile, mai piu'",
    "Attesa infinita, piatti freddi e conto salato",
    "Personale scortese, esperienza pessima e deludente",
    "Carne bruciata e vino sa di tappo, evitate",
    "Locale sporco e rumoroso, cena da dimenticare",
    "Prezzi assurdi per porzioni ridicole, che delusione",
    "Pasta scotta e camerieri sgarbati, terribile",
    "Prenotazione persa e tavolo traballante, un disastro",
    "Servizio pessimo e cameriere maleducato",
    "Attesa interminabile e piatti tiepidi",
    "Conto gonfiato e porzioni misere",
    "Cibo insipido, mai piu' torneremo",
    "Pesce non fresco, serata rovinata",
    "Prenotazione ignorata, pessima organizzazione",
    "Camerieri distratti e cucina lentissima",
    "Prezzi esagerati per una qualita' scadente",
    "Ordine sbagliato due volte, che disastro",
    "Carne dura e contorno freddo, evitate",
    "Ambiente caotico e servizio sciatto",
    "Dolce industriale e caffe' bruciato, deludente",
    "Delusione totale, esperienza da dimenticare",
    "Sala fredda e attesa infinita, pessimo",
    "Piatti scotti e sapori inesistenti, terribile",
    "Cameriere sgarbato e conto salato, evitate",
]
# 24 recensioni positive seguite da 24 negative
etichette = [1] * 24 + [0] * 24

# Dividiamo in train e test: il modello va valutato su frasi MAI viste
X_train, X_test, y_train, y_test = train_test_split(
    recensioni, etichette, test_size=0.25, random_state=42, stratify=etichette
)

# La pipeline: vettorizzazione TF-IDF + regressione logistica.
# ngram_range=(1, 2) cattura anche coppie di parole come "mai piu'".
modello = make_pipeline(
    TfidfVectorizer(ngram_range=(1, 2), lowercase=True),
    LogisticRegression(max_iter=1000),
)
modello.fit(X_train, y_train)

# Valutazione sul test set
print(classification_report(y_test, modello.predict(X_test),
                            target_names=["negativo", "positivo"]))

# Il momento della verita': frasi nuove di zecca
nuove = [
    "Serata meravigliosa, piatti eccellenti",
    "Servizio pessimo e attesa infinita",
]
for frase, prob in zip(nuove, modello.predict_proba(nuove)):
    polarita = "positivo" if prob[1] > 0.5 else "negativo"
    print(f"{frase!r} -> {polarita} (p={prob[1]:.2f})")

Esegui con python sentiment.py: il modello assegna la polarità corretta a entrambe le frasi nuove. Con appena qualche decina di esempi le probabilità non sono nettissime — restano vicine al mezzo — ma cadono dal lato giusto. Nota il parametro stratify: garantisce che train e test abbiano la stessa proporzione di positivi e negativi. Con un corpus così piccolo, senza stratificazione rischieresti un test set tutto di un solo colore.

Apri il cofano: quali parole pesano?

Una regressione logistica ha un coefficiente per ogni parola del vocabolario: positivo se spinge verso la classe «positivo», negativo altrimenti. Possiamo guardarli — ed è qui che il modello smette di essere una scatola nera:

import numpy as np

# Estraiamo vettorizzatore e classificatore dalla pipeline
vec = modello.named_steps["tfidfvectorizer"]
clf = modello.named_steps["logisticregression"]

parole = np.array(vec.get_feature_names_out())
pesi = clf.coef_[0]

ordine = np.argsort(pesi)
print("Parole piu' NEGATIVE:", parole[ordine[:5]])
print("Parole piu' POSITIVE:", parole[ordine[-5:]])

Vedrai comparire «pessima», «pessimo», «mai più» da un lato e «impeccabile», «fantastica», «meravigliosa» dall’altro. Il modello ha imparato da solo un piccolo dizionario dei sentimenti, pesato sui dati. È come assaggiare la minestra di un cuoco e riuscire a dedurre la sua lista della spesa: se il risultato ti convince, gli ingredienti erano quelli giusti.

Onestà prima di tutto: dove questo approccio si rompe

TF-IDF più regressione logistica è la baseline giusta per iniziare: veloce, interpretabile, gratis. Ma devi conoscerne i limiti:

  • Sarcasmo e ironia: «Complimenti, due ore di attesa, davvero il massimo» contiene solo parole positive. Il modello lo classificherà positivo, e sbaglierà. Le parole da sole non bastano quando il significato è nell’intonazione.
  • Negazioni lunghe: «non è affatto male» viene spesso letto come negativo per via del «male». I bigrammi (ngram_range=(1, 2)) aiutano, ma non risolvono.
  • Dominio diverso: un modello addestrato su recensioni di ristoranti non funziona su tweet politici. «Bomba» è positivo per una pizzeria, molto meno in cronaca.
  • Corpus minuscolo: le nostre poche decine di esempi servono a capire il meccanismo, non a produrre un modello serio. Sotto i mille esempi etichettati, diffida di qualsiasi metrica.

Quando questi limiti diventano un problema reale, il passo successivo è un modello transformer pre-addestrato e fine-tuned per il sentiment (su Hugging Face ne trovi anche per l’italiano, come i modelli della famiglia UmBERTo o feel-it). E se hai pochi testi al giorno, un LLM via API fa sentiment eccellente senza addestrare nulla — ma occhio: per milioni di recensioni al mese i costi esplodono, e la baseline scikit-learn torna improvvisamente attraente. Lo strumento giusto dipende dal volume, non dalla moda.

Prova tu

Due esercizi, in ordine di ambizione:

  • Aggiungi al corpus 6 recensioni neutre («Nella media, niente di speciale») con etichetta 2 e trasforma il problema in classificazione a tre classi. La pipeline non cambia di una riga: osserva come cambiano precision e recall per la classe neutra, che è sempre la più difficile.
  • Scarica un dataset vero di recensioni in italiano (su Hugging Face cerca «italian sentiment»), riaddestra la stessa pipeline su qualche migliaio di esempi e confronta le parole più pesate con quelle del nostro giocattolo. Quante sopravvivono?

Per approfondire: il capitolo sulla classificazione di testi di Speech and Language Processing di Jurafsky e Martin (disponibile gratuitamente online, terza edizione) è la spiegazione più chiara in circolazione di TF-IDF, naive Bayes e regressione logistica applicati al linguaggio. E la documentazione ufficiale di scikit-learn sulla feature extraction testuale merita mezz’ora del tuo tempo.

I commenti sono riservati agli iscritti.

Accedi per commentare