Le raccomandazioni di prodotto sono uno dei pochi sistemi di ML che un negozio online può testare in modo pulito: si mostra a metà dei visitatori una versione, all'altra metà l'altra, e si confrontano i ricavi. Questo le rende un buon primo progetto di ML, e anche un progetto facile da sovradimensionare. Questa nota descrive l'ordine in cui le costruiamo: una baseline di co-visualizzazioni che va in produzione in poche settimane, poi retrieval dei candidati e ranking se i numeri lo giustificano, con il codice per le parti che stanno in una pagina.
Per chi legge dal lato business
Quanto valgono le raccomandazioni. Salesforce ha riportato che durante la Cyber Week 2025 IA e agenti hanno influenzato circa il 20% degli ordini globali, circa 67 miliardi di $ di vendite, tramite raccomandazioni di prodotto personalizzate e servizio clienti conversazionale. Quel numero va letto con attenzione: "influenzato" è un'attribuzione sulla piattaforma commerce di Salesforce stessa, non un incremento misurato, e mescola raccomandazioni e chat. Dice che le raccomandazioni oggi sono standard nei grandi negozi. Non dice cosa aggiungerebbero al vostro. Lo dice solo un test sul vostro traffico.
Dove si collocano. Ogni posizionamento risponde a una domanda diversa e va misurato separatamente.
| Posizionamento | Domanda a cui risponde | Fonte tipica |
|---|---|---|
| Pagina prodotto | Cos'altro potrebbe andare bene, se non questo? | Articoli simili e co-visualizzati |
| Carrello | Cosa si abbina a ciò che è già stato scelto? | Articoli acquistati insieme, accessori |
| Home e pagine di categoria | Cosa vorrà probabilmente oggi questo cliente? | Ranking personalizzato |
| Risultati di ricerca | Quali articoli pertinenti comprerà probabilmente questo cliente? | Ranking sopra la rilevanza di ricerca |
| E-mail e push | Cosa riporta indietro questo cliente? | Gli stessi modelli, eseguiti in batch |
Cosa misurare. Concordate una metrica primaria prima di scrivere qualsiasi codice: ricavi per sessione o ordini per sessione sull'intero sito, non i clic sul widget. I clic sulle raccomandazioni possono salire mentre i ricavi restano piatti, perché i clienti cliccano un articolo raccomandato che avrebbero trovato comunque. Il click-through del widget è una diagnostica utile, non un criterio di successo.
Quando non ne vale la pena. Due casi. Un catalogo piccolo, qualche centinaio di prodotti che i clienti possono sfogliare per intero, raramente ha bisogno di un modello; una lista curata e un ordinamento per popolarità bastano. Il traffico basso è il secondo caso, e l'aritmetica non perdona. Per rilevare un incremento relativo del 5% su un tasso di conversione del 2% (dal 2,0% al 2,1%) con le impostazioni abituali (significatività del 5%, potenza dell'80%) servono circa 315.000 sessioni per ciascun braccio. Un negozio con 100.000 sessioni al mese aspetterebbe più di sei mesi per una sola risposta. Un incremento del 10% richiede circa 81.000 sessioni per braccio. Se il vostro traffico non può rispondere alla domanda, non si può dimostrare che un modello aiuti, e state pagando per una convinzione.
Costi e tempi. La nostra fascia per un sistema di raccomandazione è 14k–42k € in 2–4 mesi, a seconda di quanto il progetto va oltre la baseline. Una ripartizione illustrativa è alla fine di questa nota.
Fase 1: co-visualizzazioni con un fallback di popolarità
La baseline è item-to-item: i prodotti che compaiono nelle stesse sessioni sono probabilmente correlati. L'idea è vecchia, e un articolo del 2003 su IEEE Internet Computing scritto da ingegneri di Amazon descriveva l'approccio item-to-item su larga scala: calcolare gli articoli simili offline, così che il costo della ricerca online non cresca con il numero di clienti o di prodotti. È ancora il primo passo giusto, perché non richiede altro che il log degli eventi, è facile da spiegare a un merchandiser e dà a ogni modello successivo un numero da battere.
Input: una riga per ogni visualizzazione di prodotto (o aggiunta al carrello, o acquisto) con session_id, item_id e ts.
import numpy as np
import pandas as pd
def co_view_scores(events, max_items_per_session=50, min_pairs=3):
# one row per session and item; long sessions are capped to keep the join small
s = (events.sort_values("ts")
.drop_duplicates(["session_id", "item_id"], keep="last")
.groupby("session_id").tail(max_items_per_session)[["session_id", "item_id"]])
pairs = s.merge(s, on="session_id", suffixes=("_a", "_b"))
pairs = pairs[pairs.item_id_a != pairs.item_id_b]
co = pairs.groupby(["item_id_a", "item_id_b"]).size().rename("co").reset_index()
co = co[co.co >= min_pairs] # drop pairs seen in too few sessions
n_item = s.groupby("item_id").size() # sessions that viewed each item
co["score"] = co.co / np.sqrt(co.item_id_a.map(n_item) * co.item_id_b.map(n_item))
return co
def top_k(co, k=20):
return (co.sort_values(["item_id_a", "score"], ascending=[True, False])
.groupby("item_id_a").head(k))
Tre scelte in questo codice contano più di quanto conterà mai il modello.
- Normalizzazione. I conteggi grezzi di co-occorrenza raccomandano i bestseller accanto a tutto. Dividere per la radice quadrata dei conteggi di sessione di entrambi gli articoli (uno score coseno) premia le coppie che compaiono insieme più spesso di quanto preveda la loro popolarità. Uno score di lift,
co * n_sessions / (n_a * n_b), spinge ancora di più verso le coppie di nicchia e richiede un conteggio minimo più severo. - Supporto minimo. Senza
min_pairs, due articoli visti insieme una sola volta da una persona ottengono uno score perfetto. La soglia va regolata su dati tenuti da parte, non a occhio. - Finestra e segnale. Usate una finestra mobile (60–90 giorni è un punto di partenza comune) così che gli articoli stagionali escano di scena. Le co-visualizzazioni trovano alternative per la pagina prodotto; i co-acquisti trovano complementi per il carrello. Costruite entrambi con la stessa funzione e tipi di evento diversi.
Per gli articoli con troppo pochi vicini, riempite gli slot con articoli popolari della stessa categoria:
def recommend(item_id, recs, catalog, popular_by_cat, k=10):
own = recs.loc[recs.item_id_a == item_id, "item_id_b"].tolist()[:k]
if len(own) < k: # fallback: popular in the same category
cat = catalog.set_index("item_id").at[item_id, "category"]
own += [i for i in popular_by_cat[cat] if i != item_id and i not in own][: k - len(own)]
return own
In produzione questo diventa un job batch notturno che scrive i primi 20 vicini per articolo in un key-value store, e un'API che li legge. Basta per eseguire il primo test A/B contro ciò che il negozio mostra oggi.
Fase 2: recuperare i candidati, poi ordinarli
L'item-to-item ignora chi è il cliente. La personalizzazione richiede un modello che dia un punteggio agli articoli per una persona, e dare un punteggio all'intero catalogo a ogni visualizzazione di pagina è troppo lento. La risposta standard sono due fasi, descritte nell'articolo sulle raccomandazioni di YouTube di Covington, Adams e Sargin (RecSys 2016): una fase di generazione dei candidati riduce un corpus di milioni a centinaia di articoli, e un modello di ranking separato ordina quelle centinaia usando un insieme di feature molto più ricco.
Generazione dei candidati con un modello two-tower. Una rete produce l'embedding del cliente (visualizzazioni recenti, carrello, contesto come dispositivo e pagina di ingresso), un'altra quello dell'articolo (categoria, marca, prezzo, testo). L'addestramento avvicina l'embedding di un cliente agli articoli con cui ha interagito. Al momento del serving gli embedding degli articoli sono precalcolati e indicizzati per una ricerca approssimata dei vicini più prossimi, quindi il retrieval è un calcolo di embedding e una ricerca nell'indice.
Il trucco di addestramento abituale sono i negativi in-batch: per ogni cliente in un batch, gli articoli degli altri clienti fanno da negativi. Questo trucco ha un bias. Gli articoli popolari compaiono in molti batch, quindi sono sovrautilizzati come negativi e il modello impara a sottovalutarli nel ranking. Yi et al. (RecSys 2019) hanno mostrato come correggerlo sottraendo dallo score di ogni articolo il logaritmo della sua frequenza di campionamento stimata, e hanno messo in produzione il modello corretto su YouTube su un corpus di decine di milioni di video. In PyTorch il nucleo è breve:
import torch
import torch.nn as nn
import torch.nn.functional as F
class Tower(nn.Module):
def __init__(self, n_ids, dim=64):
super().__init__()
self.emb = nn.Embedding(n_ids, dim)
self.mlp = nn.Sequential(nn.Linear(dim, dim), nn.ReLU(), nn.Linear(dim, dim))
def forward(self, ids):
return F.normalize(self.mlp(self.emb(ids)), dim=-1)
user_tower, item_tower = Tower(n_users), Tower(n_items)
def in_batch_loss(user_ids, item_ids, log_q, temperature=0.05):
u = user_tower(user_ids) # [B, d]
v = item_tower(item_ids) # [B, d]
logits = u @ v.T / temperature # other items in the batch act as negatives
logits = logits - log_q[item_ids] # logQ correction for popular items
labels = torch.arange(len(user_ids)) # the diagonal holds the true pairs
return F.cross_entropy(logits, labels)
Qui log_q è il logaritmo della quota di interazioni di addestramento di ciascun articolo. Le torri prendono ID per tenere piccolo l'esempio; in un negozio reale la torre utente prende gli ultimi N articoli visti e il contesto invece di un ID utente, il che significa anche che un visitatore alla prima visita ottiene un embedding già dopo un clic.
Tenete le liste di co-visualizzazione della fase 1 come seconda fonte di candidati. Il retrieval two-tower e le co-visualizzazioni sbagliano in modi diversi, e il ranker può usare entrambi.
Ranking. Il ranker vede qualche centinaio di candidati per richiesta e può permettersi molte feature: gli score di ogni fonte di candidati, il prezzo rispetto alla fascia di prezzo abituale del cliente, se l'articolo condivide una categoria con le visualizzazioni recenti, l'andamento recente delle visualizzazioni dell'articolo, i giorni da quando l'articolo è stato aggiunto. Un modello learning-to-rank gradient-boosted è un primo ranker comune, e le sue feature importance sono facili da ispezionare:
import lightgbm as lgb
# one row per (request, candidate); label 1 if the candidate was clicked or bought
features = ["covis_score", "two_tower_score", "price_vs_user_median",
"same_category_as_viewed", "item_views_7d_trend", "days_since_added"]
train = train.sort_values("request_id")
ranker = lgb.LGBMRanker(objective="lambdarank", n_estimators=300, learning_rate=0.05)
ranker.fit(train[features], train["label"],
group=train.groupby("request_id", sort=False).size().to_numpy())
Regole di business dopo il modello. Disponibilità, margine e policy vanno dopo il ranking, in codice semplice che il business può leggere e modificare: escludere gli articoli che non possono essere spediti, limitare il numero di articoli per marca in un blocco, escludere le categorie che non vanno proposte in cross-selling e, se il margine conta, applicare un boost limitato invece di riaddestrare il modello sul margine. Tenere le regole fuori dal modello significa che un merchandiser può cambiarle di martedì senza un rilascio di ML.
Valutazione offline, e perché non è la risposta
Dividete per tempo, non a caso: addestrate su tutto ciò che precede una data di taglio e valutate sulla settimana successiva. Una divisione casuale fa trapelare il futuro nell'addestramento e gonfia ogni metrica.
Misurate ogni fase con la sua metrica. Per la generazione dei candidati, recall@k: degli articoli con cui il cliente ha effettivamente interagito dopo, quanti erano nella lista dei candidati. Per il ranking, NDCG@k: quegli articoli sono finiti vicino alla cima?
def recall_at_k(recommended, relevant, k=20):
return len(set(recommended[:k]) & relevant) / len(relevant)
def ndcg_at_k(recommended, relevant, k=10):
dcg = sum(1 / np.log2(i + 2) for i, item in enumerate(recommended[:k]) if item in relevant)
ideal = sum(1 / np.log2(i + 2) for i in range(min(len(relevant), k)))
return dcg / ideal
I numeri offline decidono quale modello entra nel test. Non decidono se va in produzione. I log sono stati prodotti dal vecchio sistema, quindi il nuovo modello viene valutato su ciò che i clienti hanno fatto quando veniva mostrato loro qualcos'altro, e la posizione sulla pagina condiziona i clic indipendentemente dalla pertinenza. Covington et al. hanno scritto che "live A/B results are not always correlated with offline experiments", e le due ragioni sopra bastano per aspettarselo.
Il test A/B
- Una metrica primaria, quella concordata all'inizio, come i ricavi per sessione. Decidete l'incremento minimo che vale la pena rilevare e calcolate la dimensione del campione prima del lancio, come nell'aritmetica sopra.
- Randomizzate per visitatore, non per visualizzazione di pagina, così che un cliente non veda entrambe le versioni.
- Fate girare settimane intere, almeno due, perché i clienti dei giorni feriali e del weekend si comportano in modo diverso. Non fermatevi in anticipo perché la curva sembra buona al quarto giorno.
- Metriche di guardrail che non devono peggiorare: tempo di risposta della pagina, quota di articoli raccomandati esauriti al momento del clic, tasso di reso sugli ordini che contengono articoli raccomandati e ricavi nelle categorie che il nuovo modello mostra meno spesso.
- Tenete il vecchio sistema come controllo finché la metrica primaria non dice altro, e tenete l'interruttore per il rollback.
Cold start
I nuovi prodotti non hanno interazioni, quindi né le co-visualizzazioni né l'embedding dell'ID nella torre articolo li conoscono. Collocateli usando gli attributi (categoria, marca, fascia di prezzo, embedding di testo e immagini) e riservate un piccolo numero di slot in ogni blocco agli articoli aggiunti di recente, così che raccolgano le interazioni di cui hanno bisogno. I nuovi visitatori ricevono candidati basati sul contesto (la pagina di arrivo, la categoria, il dispositivo) e articoli popolari, e passano al retrieval personalizzato dopo le prime visualizzazioni nella sessione.
Quanto costa, fase per fase
La nostra fascia per un sistema di raccomandazione è 14k–42k € in 2–4 mesi. La tabella seguente è una ripartizione illustrativa per un progetto di medie dimensioni che attraversa entrambe le fasi, prezzata dal nostro listino pubblicato. Non è un preventivo né il resoconto di un progetto passato.
| Fase | Ruoli e ore | Costo |
|---|---|---|
| Audit dei dati, metrica, design del test A/B | Data 30 h a 45 €, Senior ML 20 h a 70 € | 2.750 € |
| Fase 1: co-visualizzazioni, fallback, API | ML 60 h a 50 €, Backend 40 h a 40 € | 4.600 € |
| Fase 2: retrieval two-tower e ranker | Senior ML 80 h a 70 €, ML 80 h a 50 € | 9.600 € |
| Esecuzione e lettura del test A/B | Senior ML 15 h a 70 €, Data 20 h a 45 € | 1.950 € |
| Pipeline, monitoraggio, rollback | MLOps 80 h a 55 € | 4.400 € |
| Trasversale a tutte le fasi | QA 30 h a 30 €, PM 50 h a 40 € | 2.900 € |
| Totale | 505 h | 26.200 € |
Un progetto che si ferma dopo la fase 1, perché il primo test ha risposto alla domanda di business, si colloca nella parte bassa della fascia. Cataloghi con molte fonti di dati, più posizionamenti testati separatamente o budget di tempo di risposta rigidi spingono verso la parte alta. La pagina sui sistemi di raccomandazione descrive come si svolge un progetto del genere, e il calcolatore su /pricing prezza il vostro perimetro con lo stesso listino.
Fonti
- Salesforce, Salesforce Data: AI and Agents Propel Cyber Week to Record $336.6B in Global Spend, dicembre 2025 — https://www.salesforce.com/news/press-releases/2025/12/05/cyber-week-ai-agents-sales/
- IEEE Internet Computing, Amazon.com Recommendations: Item-to-Item Collaborative Filtering (Linden, Smith, York), gennaio 2003 — https://ieeexplore.ieee.org/document/1167344
- ACM RecSys, Deep Neural Networks for YouTube Recommendations (Covington, Adams, Sargin), settembre 2016 — https://research.google/pubs/pub45530/
- ACM RecSys, Sampling-Bias-Corrected Neural Modeling for Large Corpus Item Recommendations (Yi et al.), settembre 2019 — https://research.google/pubs/sampling-bias-corrected-neural-modeling-for-large-corpus-item-recommendations/