Les recommandations produits sont l'un des rares systèmes de ML qu'une boutique en ligne peut tester proprement : montrer une version à la moitié des visiteurs, l'autre à l'autre moitié, et comparer le chiffre d'affaires. Cela en fait un bon premier projet de ML, et aussi un projet facile à surdimensionner. Cette note décrit l'ordre dans lequel nous les construisons : une baseline de co-vues livrée en quelques semaines, puis le retrieval de candidats et le ranking si les chiffres le justifient, avec du code pour les parties qui tiennent sur une page.
Pour le lecteur côté métier
Ce que valent les recommandations. Salesforce a indiqué que pendant la Cyber Week 2025, l'IA et les agents ont influencé environ 20 % des commandes mondiales, soit environ 67 milliards de dollars de ventes, via des recommandations produits personnalisées et un service client conversationnel. Lisez ce chiffre avec attention : « influencé » est une attribution sur la propre plateforme e-commerce de Salesforce, pas un gain mesuré, et il mélange recommandations et chat. Il vous dit que les recommandations sont désormais la norme sur les grandes boutiques. Il ne vous dit pas ce qu'elles apporteraient à la vôtre. Seul un test sur votre trafic le dit.
Où elles se placent. Chaque emplacement répond à une question différente et doit être mesuré séparément.
| Emplacement | Question à laquelle il répond | Source typique |
|---|---|---|
| Page produit | Quoi d'autre pourrait convenir, si ce n'est pas celui-ci ? | Articles similaires et co-vus |
| Panier | Qu'est-ce qui va avec ce qui est déjà choisi ? | Articles co-achetés, accessoires |
| Pages d'accueil et de catégorie | Que va probablement vouloir cet acheteur aujourd'hui ? | Ranking personnalisé |
| Résultats de recherche | Parmi les articles pertinents, lesquels cet acheteur va-t-il probablement acheter ? | Ranking par-dessus la pertinence de recherche |
| E-mail et push | Qu'est-ce qui fait revenir cet acheteur ? | Les mêmes modèles, exécutés en batch |
Quoi mesurer. Fixez une métrique principale avant d'écrire la moindre ligne de code : le chiffre d'affaires par session ou les commandes par session sur l'ensemble du site, pas les clics sur le widget. Les clics sur les recommandations peuvent augmenter alors que le chiffre d'affaires reste stable, parce que les acheteurs cliquent sur un article recommandé qu'ils auraient trouvé de toute façon. Le taux de clic du widget est un diagnostic utile, pas un critère de succès.
Quand cela n'en vaut pas la peine. Deux cas. Un petit catalogue, quelques centaines de produits que les acheteurs peuvent parcourir en entier, a rarement besoin d'un modèle ; une liste éditorialisée et un tri par popularité font l'affaire. Le faible trafic est le second cas, et l'arithmétique est impitoyable. Pour détecter une hausse relative de 5 % sur un taux de conversion de 2 % (de 2,0 % à 2,1 %) avec les paramètres habituels (seuil de significativité de 5 %, puissance de 80 %), il faut environ 315 000 sessions dans chaque bras. Une boutique avec 100 000 sessions par mois attendrait plus de six mois pour une seule réponse. Une hausse de 10 % demande environ 81 000 sessions par bras. Si votre trafic ne permet pas de répondre à la question, on ne peut pas prouver qu'un modèle aide, et vous payez pour une conviction.
Coût et calendrier. Notre fourchette pour un système de recommandation est de 14k–42k € sur 2–4 mois, selon jusqu'où le projet va au-delà de la baseline. Une répartition illustrative figure à la fin de cette note.
Étape 1 : co-vues avec repli sur la popularité
La baseline est item-to-item : les produits qui apparaissent dans les mêmes sessions sont probablement liés. L'idée est ancienne, et un article d'IEEE Internet Computing de 2003 signé par des ingénieurs d'Amazon décrivait l'approche item-to-item à grande échelle : calculer les articles similaires hors ligne, pour que le coût de la consultation en ligne ne croisse ni avec le nombre de clients ni avec le nombre de produits. C'est toujours la bonne première étape, parce qu'elle ne demande rien d'autre que le journal d'événements, qu'elle est facile à expliquer à un merchandiser, et qu'elle donne à chaque modèle ultérieur un chiffre à battre.
Entrée : une ligne par vue produit (ou ajout au panier, ou achat) avec session_id, item_id et 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))
Trois choix dans ce code comptent plus que le modèle ne comptera jamais.
- La normalisation. Les comptages bruts de co-occurrence recommandent les meilleures ventes à côté de tout. Diviser par la racine carrée du nombre de sessions des deux articles (un score cosinus) récompense les paires qui apparaissent ensemble plus souvent que leur popularité ne le prédit. Un score de lift,
co * n_sessions / (n_a * n_b), pousse davantage vers les paires de niche et exige un seuil minimal plus strict. - Le support minimal. Sans
min_pairs, deux articles vus ensemble une seule fois par une seule personne obtiennent un score parfait. Réglez le seuil sur des données mises de côté, pas à l'œil. - La fenêtre et le signal. Utilisez une fenêtre glissante (60–90 jours est un point de départ courant) pour que les articles saisonniers s'effacent. Les co-vues trouvent des alternatives pour la page produit ; les co-achats trouvent des compléments pour le panier. Construisez les deux avec la même fonction et des types d'événements différents.
Pour les articles qui ont trop peu de voisins, complétez les emplacements avec des articles populaires de la même catégorie :
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
En production, cela devient un job batch nocturne qui écrit les 20 premiers voisins de chaque article dans un key-value store, et une API qui les lit. Cela suffit pour lancer le premier test A/B contre ce que la boutique affiche aujourd'hui.
Étape 2 : récupérer des candidats, puis les classer
L'item-to-item ignore qui est l'acheteur. La personnalisation exige un modèle qui note les articles pour une personne, et noter tout un catalogue à chaque page vue est trop lent. La réponse standard est en deux étapes, décrite dans l'article sur les recommandations de YouTube de Covington, Adams et Sargin (RecSys 2016) : une étape de génération de candidats réduit un corpus de millions d'éléments à quelques centaines, et un modèle de ranking distinct ordonne ces centaines à l'aide d'un ensemble de features bien plus riche.
Génération de candidats avec un modèle two-tower. Un réseau calcule l'embedding de l'acheteur (vues récentes, panier, contexte comme l'appareil et la page d'entrée), un autre celui de l'article (catégorie, marque, prix, texte). L'entraînement rapproche l'embedding d'un acheteur des articles avec lesquels il a interagi. Au moment du service, les embeddings des articles sont précalculés et indexés pour une recherche approximative des plus proches voisins, si bien que le retrieval se résume à un calcul d'embedding et une consultation d'index.
L'astuce d'entraînement habituelle, ce sont les négatifs intra-batch : pour chaque acheteur d'un batch, les articles des autres acheteurs servent de négatifs. Cette astuce a un biais. Les articles populaires apparaissent dans de nombreux batchs, ils sont donc surutilisés comme négatifs et le modèle apprend à les sous-classer. Yi et al. (RecSys 2019) ont montré comment corriger cela en soustrayant du score de chaque article le logarithme de sa fréquence d'échantillonnage estimée, et ont déployé le modèle corrigé chez YouTube sur un corpus de dizaines de millions de vidéos. En PyTorch, le cœur est court :
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)
Ici, log_q est le logarithme de la part de chaque article dans les interactions d'entraînement. Les tours prennent des identifiants pour garder l'exemple court ; dans une vraie boutique, la tour utilisateur prend les N derniers articles vus et le contexte plutôt qu'un identifiant utilisateur, ce qui signifie aussi qu'un visiteur venu pour la première fois obtient un embedding dès son premier clic.
Gardez les listes de co-vues de l'étape 1 comme seconde source de candidats. Le retrieval two-tower et les co-vues échouent de manières différentes, et le ranker peut utiliser les deux.
Ranking. Le ranker voit quelques centaines de candidats par requête et peut se permettre de nombreuses features : les scores de chaque source de candidats, le prix par rapport à la gamme de prix habituelle de l'acheteur, le fait que l'article partage une catégorie avec les vues récentes, la tendance récente des vues de l'article, le nombre de jours depuis son ajout. Un modèle de learning-to-rank par gradient boosting est un premier ranker courant, et ses importances de features sont faciles à inspecter :
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())
Les règles métier après le modèle. Stock, marge et politique commerciale viennent après le ranking, dans du code simple que le métier peut lire et modifier : retirer les articles qui ne peuvent pas être expédiés, plafonner le nombre d'articles par marque dans un bloc, exclure les catégories qui ne doivent pas faire l'objet de ventes croisées et, si la marge compte, appliquer un bonus borné plutôt que de réentraîner le modèle sur la marge. Garder les règles hors du modèle signifie qu'un merchandiser peut les modifier un mardi sans release ML.
L'évaluation hors ligne, et pourquoi elle n'est pas la réponse
Découpez par le temps, pas au hasard : entraînez sur tout ce qui précède une date limite, évaluez sur la semaine suivante. Un découpage aléatoire fait fuiter le futur dans l'entraînement et gonfle toutes les métriques.
Mesurez chaque étape avec sa propre métrique. Pour la génération de candidats, recall@k : parmi les articles avec lesquels l'acheteur a effectivement interagi ensuite, combien figuraient dans la liste de candidats. Pour le ranking, NDCG@k : ces articles se sont-ils retrouvés près du haut.
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
Les chiffres hors ligne décident quel modèle entre dans le test. Ils ne décident pas s'il est mis en production. Les logs ont été produits par l'ancien système, donc le nouveau modèle est noté sur ce que les acheteurs ont fait quand on leur montrait autre chose, et la position sur la page influence les clics indépendamment de la pertinence. Covington et al. écrivaient que « live A/B results are not always correlated with offline experiments », et les deux raisons ci-dessus suffisent pour s'y attendre.
Le test A/B
- Une seule métrique principale, celle fixée au départ, comme le chiffre d'affaires par session. Décidez de la hausse minimale qui vaut la peine d'être détectée et calculez la taille d'échantillon avant le lancement, comme dans le calcul ci-dessus.
- Randomisez par visiteur, pas par page vue, pour qu'un même acheteur ne voie pas les deux versions.
- Faites tourner des semaines entières, au moins deux, parce que les acheteurs de semaine et de week-end se comportent différemment. N'arrêtez pas plus tôt parce que la courbe a l'air bonne au quatrième jour.
- Des métriques garde-fous qui ne doivent pas se dégrader : temps de réponse des pages, part des articles recommandés en rupture de stock au moment du clic, taux de retour des commandes contenant des articles recommandés, et chiffre d'affaires dans les catégories que le nouveau modèle montre moins souvent.
- Gardez l'ancien système comme groupe de contrôle jusqu'à ce que la métrique principale dise le contraire, et gardez l'interrupteur pour revenir en arrière.
Démarrage à froid
Les nouveaux produits n'ont pas d'interactions, donc ni les co-vues ni l'embedding d'identifiant de la tour article ne les connaissent. Placez-les à l'aide de leurs attributs (catégorie, marque, gamme de prix, embeddings de texte et d'image) et réservez un petit nombre d'emplacements dans chaque bloc aux articles récemment ajoutés, pour qu'ils collectent les interactions dont ils ont besoin. Les nouveaux visiteurs reçoivent des candidats basés sur le contexte (la page d'arrivée, la catégorie, l'appareil) et des articles populaires, puis passent au retrieval personnalisé après leurs premières vues dans la session.
Ce que cela coûte, étape par étape
Notre fourchette pour un système de recommandation est de 14k–42k € sur 2–4 mois. Le tableau ci-dessous est une répartition illustrative pour un projet de taille moyenne qui passe par les deux étapes, chiffrée à partir de notre grille tarifaire publiée. Ce n'est ni un devis ni le relevé d'un projet passé.
| Étape | Rôles et heures | Coût |
|---|---|---|
| Audit des données, métrique, conception du test A/B | Data 30 h à 45 €, Senior ML 20 h à 70 € | 2 750 € |
| Étape 1 : co-vues, repli, API | ML 60 h à 50 €, Backend 40 h à 40 € | 4 600 € |
| Étape 2 : retrieval two-tower et ranker | Senior ML 80 h à 70 €, ML 80 h à 50 € | 9 600 € |
| Exécution et lecture du test A/B | Senior ML 15 h à 70 €, Data 20 h à 45 € | 1 950 € |
| Pipelines, monitoring, rollback | MLOps 80 h à 55 € | 4 400 € |
| Transversal à toutes les étapes | QA 30 h à 30 €, PM 50 h à 40 € | 2 900 € |
| Total | 505 h | 26 200 € |
Un projet qui s'arrête après l'étape 1, parce que le premier test a répondu à la question métier, se situe dans le bas de la fourchette. Les catalogues aux nombreuses sources de données, plusieurs emplacements testés séparément ou des budgets de temps de réponse stricts poussent vers le haut. La page sur les systèmes de recommandation décrit le déroulement d'un tel projet, et le calculateur sur /pricing chiffre votre propre périmètre à partir de la même grille tarifaire.
Sources
- Salesforce, Salesforce Data: AI and Agents Propel Cyber Week to Record $336.6B in Global Spend, décembre 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), janvier 2003 — https://ieeexplore.ieee.org/document/1167344
- ACM RecSys, Deep Neural Networks for YouTube Recommendations (Covington, Adams, Sargin), septembre 2016 — https://research.google/pubs/pub45530/
- ACM RecSys, Sampling-Bias-Corrected Neural Modeling for Large Corpus Item Recommendations (Yi et al.), septembre 2019 — https://research.google/pubs/sampling-bias-corrected-neural-modeling-for-large-corpus-item-recommendations/