Ti è mai capitato di aprire una piattaforma di streaming e trovarti davanti un film che non conoscevi, guardarlo… e adorarlo? Nessuno alla piattaforma ha visto quel film per te. Nessun algoritmo ha «capito» la trama, la fotografia o la colonna sonora. Eppure il consiglio era azzeccato. Il trucco si chiama collaborative filtering, e la cosa sorprendente è quanto poco gli serva per funzionare: una tabella di voti e un po’ di geometria. In questo tutorial lo costruiamo da zero, in entrambe le varianti classiche — user-based e item-based — con codice che puoi eseguire subito.
La saggezza dei vicini, i fattori latenti e dove il collaborativo si ferma sono nel libro.
Ti servono Python 3.12, pandas e scikit-learn. Se non ricordi cos’è la similarità coseno, ti basta sapere che misura quanto due vettori «puntano nella stessa direzione»: vale 1 se sono allineati, 0 se non c’entrano niente l’uno con l’altro. La useremo per dire quanto due persone (o due film) si somigliano.
Il problema: consigliare senza capire
Pensa a come chiedi consiglio nella vita reale. Non vai da un critico cinematografico: vai da quell’amico che, ogni volta, ama gli stessi film che ami tu. Non gli chiedi perché gli è piaciuto qualcosa — ti fidi della storia dei vostri gusti in comune. «A Marco è piaciuto, a me piace sempre quello che piace a Marco, quindi probabilmente piacerà anche a me.»
Il collaborative filtering è esattamente questo meccanismo, automatizzato. L’intuizione fondante: i gusti passati predicono i gusti futuri, e persone simili restano simili. Non serve sapere nulla del contenuto — né genere, né attori, né trama. Servono solo i voti. È il motivo per cui lo stesso identico algoritmo che consiglia film può consigliare libri, ristoranti o playlist senza cambiare una riga.
Un recommender collaborativo non capisce niente di cinema: sa solo che qualcuno molto simile a te ha già scelto al posto tuo.
La matrice utenti×item: la struttura dati che regge tutto
Tutto parte da un’unica struttura: una tabella con gli utenti sulle righe, gli item (film, libri, prodotti) sulle colonne, e nei valori il voto che quell’utente ha dato a quell’item. Le celle vuote — cioè quasi tutte, nella realtà — sono i voti che non conosciamo. Fare raccomandazioni significa letteralmente riempire i buchi: stimare che voto daresti a ciò che non hai ancora visto, e proporti le celle con la stima più alta.
La matrice utenti×item: ogni cella è un voto, i buchi sono ciò che vogliamo stimare. Il metodo user-based guarda le righe simili alla tua; l’item-based guarda le colonne simili a quella incognita.
Costruiamola in pandas. Prima l’ambiente — io uso uv, ma un normale python -m venv è altrettanto valido:
# nel terminale
uv venv && source .venv/bin/activate
uv pip install pandas scikit-learn
import numpy as np
import pandas as pd
# righe = utenti, colonne = film, valori = voti da 1 a 5
# NaN = quell'utente non ha (ancora) visto quel film
dati = {
"Inception": [5, 4, np.nan, 1, np.nan],
"Interstellar": [4, np.nan, 4, np.nan, 1],
"Notting Hill": [1, 2, np.nan, 5, 4],
"Love Actually": [np.nan, 1, 2, 4, 5],
"Matrix": [5, 5, 4, np.nan, 2],
}
R = pd.DataFrame(dati, index=["Anna", "Bruno", "Carla", "Dario", "Elena"])
print(R)
Guarda i dati un attimo: Anna e Bruno amano la fantascienza e snobbano le commedie romantiche; Dario ed Elena sono l’esatto opposto; Carla assomiglia ai primi due. Sono cinque utenti e cinque film, apposta: con una matrice che entra in uno schermo puoi verificare a mano che l’algoritmo ragioni bene, prima di lanciarlo su milioni di righe.
User-based: trova i tuoi gemelli di gusto
Prima strategia: per stimare il voto di Carla su «Notting Hill», cerca gli utenti più simili a Carla che quel film l’hanno visto, e fai una media pesata dei loro voti. C’è però un dettaglio che fa la differenza: le persone usano le scale in modo diverso. C’è chi dà 5 a tutto e chi non supera mai il 3 — come i professori di matematica per cui «6 e mezzo» è un complimento. Per confrontarli, centriamo i voti di ciascuno sulla propria media: così contano gli scostamenti («più del suo solito, meno del suo solito»), non i numeri assoluti.
from sklearn.metrics.pairwise import cosine_similarity
# centra ogni utente sulla propria media: un 4 dato da un generoso
# vale meno di un 4 dato da un severo
medie = R.mean(axis=1)
R_centrata = R.sub(medie, axis=0).fillna(0) # i buchi diventano 0 = neutro
# similarità coseno tra tutte le coppie di utenti (righe)
sim_utenti = pd.DataFrame(
cosine_similarity(R_centrata),
index=R.index, columns=R.index,
)
print(sim_utenti.round(2))
def stima_user_based(utente, film, k=2):
"""Stima il voto di `utente` per `film` dai k vicini più simili."""
voti = R[film].dropna().drop(utente, errors="ignore") # chi l'ha visto
vicini = sim_utenti.loc[utente, voti.index].nlargest(k)
if vicini.abs().sum() == 0:
return medie[utente] # nessun vicino utile: media utente
# media pesata degli SCARTI dei vicini, riportata sulla scala dell'utente
scarti = voti[vicini.index] - medie[vicini.index]
return medie[utente] + (vicini * scarti).sum() / vicini.abs().sum()
print("Carla × Notting Hill:", round(stima_user_based("Carla", "Notting Hill"), 2))
print("Carla × Inception: ", round(stima_user_based("Carla", "Inception"), 2))
Esegui e osserva: la stima per «Notting Hill» esce bassa (i simili di Carla lo detestano), quella per «Inception» esce alta. L’algoritmo non ha idea che uno sia una commedia romantica e l’altro fantascienza — l’ha dedotto dalla geometria dei voti. Nota anche la matrice sim_utenti: Anna–Bruno nettamente positivi, Anna–Dario negativi. Gusti opposti producono similarità negativa, e la formula li usa correttamente come «consiglieri al contrario».
Item-based: dai gusti delle persone alle affinità tra oggetti
Seconda strategia, speculare: invece di confrontare le righe, confronta le colonne. Due film sono simili se le stesse persone li hanno votati in modo simile. È il classico «chi ha comprato questo ha comprato anche…» del libraio: non sa perché quei due libri vadano insieme, sa che vanno insieme. Per stimare Carla su «Notting Hill», guardi i film che Carla ha già votato e pesi i suoi voti con la similarità tra quei film e «Notting Hill».
# similarità tra film: stessa identica funzione, matrice trasposta
sim_item = pd.DataFrame(
cosine_similarity(R_centrata.T),
index=R.columns, columns=R.columns,
)
print(sim_item.round(2))
def stima_item_based(utente, film, k=2):
"""Stima pesando i voti già dati dall'utente su item simili a `film`."""
visti = R.loc[utente].dropna() # film già votati dall'utente
sims = sim_item.loc[film, visti.index]
# teniamo solo i vicini con similarità POSITIVA: "diverso da" non
# dice quale voto daresti, quindi non lo usiamo come peso
vicini = sims[sims > 0].nlargest(k)
if vicini.sum() == 0:
return medie[utente] # nessun item affine: media utente
return (vicini * visti[vicini.index]).sum() / vicini.sum()
print("Carla × Notting Hill:", round(stima_item_based("Carla", "Notting Hill"), 2))
# la raccomandazione finale: stima tutti i film NON visti, ordina, proponi
def raccomanda(utente, k=2):
non_visti = R.columns[R.loc[utente].isna()]
stime = {f: stima_item_based(utente, f, k) for f in non_visti}
return pd.Series(stime).sort_values(ascending=False)
print(raccomanda("Carla"))
Lancia il codice: la stima per «Notting Hill» resta bassa (intorno al 2) e raccomanda mette «Inception» in cima alla lista di Carla — le due strategie, su questi dati, concordano. Ma allora perché esistono due varianti, se fanno la stessa cosa? Questione di stabilità e di scala. I gusti di un utente cambiano di settimana in settimana; l’affinità tra «Notting Hill» e «Love Actually» invece è praticamente eterna. Per questo, nei sistemi reali, la matrice di similarità tra item si può precalcolare offline e servire in millisecondi — è il motivo per cui Amazon già nel 2003 scelse l’item-based per il suo catalogo. Regola pratica: molti più utenti che item (e-commerce, streaming) → item-based; il contrario, o gusti che contano più del catalogo → user-based.
Onestà: quando il collaborative filtering non basta
Tre limiti veri, prima che ti innamori del metodo. Primo, il cold start: un utente nuovo non ha voti, quindi non ha vicini — per lui il sistema è cieco (per questo le piattaforme ti chiedono «scegli 3 titoli che ti piacciono» alla registrazione). Lo stesso vale per un item appena aggiunto al catalogo. Secondo, la sparsità: nella realtà la matrice è vuota al 99% e oltre — come il registro delle firme di un condominio da diecimila appartamenti — e con pochi voti in comune le similarità diventano rumore. Terzo, la scala: con milioni di utenti la matrice completa non entra in memoria; i sistemi seri passano alla fattorizzazione di matrici (SVD e parenti) o a modelli neurali, che comprimono utenti e item in vettori densi di poche decine di dimensioni.
E se hai un catalogo di 50 prodotti e 200 clienti? Non ti serve niente di tutto questo: una lista dei più venduti per categoria batte qualsiasi recommender. Il collaborative filtering inizia a ripagare quando i voti sono tanti e i gusti eterogenei — non prima.
Prova tu
Primo esercizio: aggiungi te stesso alla matrice R come sesta riga, vota tre dei cinque film con i tuoi gusti reali e lancia raccomanda sul tuo nome. La proposta ti convince? Prova poi a cambiare k da 2 a 4 e osserva come cambiano le stime. Secondo esercizio, più ambizioso: scarica il dataset MovieLens «ml-latest-small» (100.000 voti reali, CSV pronto per pandas), costruisci la matrice con pivot_table e misura l’errore medio delle tue stime nascondendo il 10% dei voti come test set.
Per approfondire: il capitolo 9 di Mining of Massive Datasets di Leskovec, Rajaraman e Ullman (gratuito su mmds.org) tratta i recommender system in profondità, dalla similarità coseno fino alla fattorizzazione UV che ha vinto il Netflix Prize.

I commenti sono riservati agli iscritti.
Accedi per commentare