Module 10 — Projet : moteur de recommandation évalué
Neuf modules de mécanismes. Ce dernier les assemble dans un projet complet sur le fil rouge du catalogue InSkillML : préparer les données, comparer plusieurs approches sur un découpage temporel honnête, produire un tableau de métriques comparatif, planifier un A/B test. Chaque décision du code renvoie au module qui la justifie.
Découpage temporel : la seule évaluation honnête
Un système de recommandation prédit le futur à partir du passé. L'évaluation doit reproduire ce régime. Or le découpage aléatoire habituel — 80 % pour l'entraînement, 20 % pour la validation — laisse fuiter du futur dans l'entraînement : un apprenant peut avoir en validation un cours qu'il n'a jamais suivi au moment du dernier cours de son entraînement, et pourtant l'ordre chronologique s'inverse pour la moitié des cas. Sur MovieLens, ce simple découpage fausse le NDCG de 15 à 25 % à la hausse par rapport au découpage temporel.
Le découpage temporel se définit par une date de coupure unique : tout ce qui précède est entraînement, tout ce qui suit est test. Sur le catalogue InSkillML :
- Entraînement : toutes les interactions avant le 1er août 2026.
- Validation : toutes les interactions du 1er au 15 août.
- Test : toutes les interactions du 16 au 31 août.
import pandas as pd
inter = pd.read_csv("interactions.csv") # colonnes : apprenant, cours, date, minutes
inter["date"] = pd.to_datetime(inter["date"])
seuil_train = pd.Timestamp("2026-08-01")
seuil_val = pd.Timestamp("2026-08-15")
d_train = inter[inter["date"] < seuil_train]
d_val = inter[(inter["date"] >= seuil_train) & (inter["date"] < seuil_val)]
d_test = inter[inter["date"] >= seuil_val]
Ce découpage a une conséquence subtile : certains apprenants n'apparaissent que dans le test (arrivés en août). Le moteur doit alors leur fournir une recommandation de démarrage à froid — c'est réaliste, et c'est le module 7 en pratique.
Trois modèles comparés
On compare trois approches sur ce même découpage, avec la même métrique et le même sous-ensemble d'évaluation, comme l'exigeait le module 5.
import numpy as np
from scipy.sparse import csr_matrix
from sklearn.metrics.pairwise import cosine_similarity
from surprise import SVD, Dataset, Reader
import implicit
def matrice_creuse(df, n_u, n_i):
return csr_matrix(
(df["minutes"], (df["apprenant"], df["cours"])),
shape=(n_u, n_i),
)
# --- Modele 1 : popularite globale (baseline honnete). ---
pop = d_train["cours"].value_counts().sort_index()
# --- Modele 2 : filtrage objet-objet par similarite cosinus (module 2). ---
R_tr = matrice_creuse(d_train, n_u=30000, n_i=500)
S = cosine_similarity(R_tr.T, dense_output=False)
S.setdiag(0); S.eliminate_zeros()
# --- Modele 3 : factorisation implicite pondérée par confiance (module 9). ---
mf = implicit.als.AlternatingLeastSquares(
factors=64, regularization=0.05, alpha=40, iterations=30,
)
mf.fit(R_tr.T.tocsr())
Le premier modèle sert de référence : recommander toujours les mêmes cours populaires. Un moteur qui ne fait pas mieux que cette baseline n'a rien apporté. C'est le repère qui empêche l'auto-satisfaction.
Tableau des métriques
On évalue chaque modèle sur les métriques du module 8, plus une couverture et une diversité :
def evaluer(recommandations_par_u, verite_par_u, plongements_cours, k=10):
n_appre = len(verite_par_u)
rappels, ndcgs = [], []
diversites = []
cours_recos = set()
for u, reco in recommandations_par_u.items():
pertinents = verite_par_u.get(u, set())
if not pertinents:
continue
top = reco[:k]
# rappel@k
rappels.append(len(set(top) & pertinents) / len(pertinents))
# ndcg@k
gains = np.array([1 if c in pertinents else 0 for c in top])
dcg = np.sum(gains / np.log2(np.arange(2, len(gains) + 2)))
ideal = np.sort(gains)[::-1]
idcg = np.sum(ideal / np.log2(np.arange(2, len(ideal) + 2)))
ndcgs.append(dcg / idcg if idcg > 0 else 0.0)
# diversite intra-liste
embs = plongements_cours[top]
sim = embs @ embs.T
n = sim.shape[0]
somme = (sim.sum() - np.trace(sim)) / (n * (n - 1))
diversites.append(1 - somme)
# couverture
cours_recos.update(top)
return {
"rappel@10": np.mean(rappels),
"ndcg@10": np.mean(ndcgs),
"diversite": np.mean(diversites),
"couverture": len(cours_recos) / plongements_cours.shape[0],
}
Un tableau typique obtenu sur InSkillML :
| Modèle | Rappel@10 | NDCG@10 | Diversité | Couverture |
|---|---|---|---|---|
| Popularité globale | 0,081 | 0,120 | 0,15 | 3 % |
| Voisinage objet-objet | 0,143 | 0,215 | 0,38 | 41 % |
| Factorisation implicite | 0,187 | 0,286 | 0,42 | 58 % |
| Hybride (LightFM) | 0,201 | 0,308 | 0,48 | 63 % |
La lecture ne se réduit pas au meilleur chiffre. La popularité globale a une couverture ridicule mais elle est honnête — elle ne prétend rien de plus qu'un top des ventes. Le voisinage objet-objet quadruple la couverture et double le rappel. La factorisation implicite l'améliore encore ; l'hybride LightFM ajoute la robustesse pour les cours froids, ce qui explique sa couverture supérieure malgré un gain plus modeste sur le rappel.
Plan de test en ligne (A/B)
Aucune de ces métriques hors ligne ne suffit à décider un déploiement. Elles sont utiles pour éliminer les mauvais candidats, pas pour couronner le meilleur. Le passage en production exige un A/B test contrôlé.
Le protocole minimal :
- Groupe témoin (contrôle) : 50 % des apprenants, moteur en production actuel.
- Groupe test : 50 % des apprenants, nouveau moteur.
- Durée : au moins deux semaines, pour couvrir l'effet de nouveauté et absorber le bruit hebdomadaire.
- Métriques primaires : taux d'inscription à partir de la page d'accueil, taux d'achèvement des cours recommandés.
- Métriques garde-fous : diversité intra-utilisateur, couverture du catalogue, taux de désinscription.
- Journalisation complète : chaque impression avec position, comme l'exigeait le module 9.
La taille d'échantillon nécessaire dépend de l'effet minimum détectable. Pour détecter un gain de 2 % sur une inscription à 10 %, il faut environ 20 000 apprenants par bras. En dessous, on prend des décisions sur du bruit.
Limites du moteur
Ce projet réaliste laisse plusieurs limites ouvertes qu'il est important de nommer, faute de quoi on croit tenir plus que ce qu'on tient.
Le moteur n'est pas séquentiel. Il ignore l'ordre dans lequel un apprenant a suivi ses cours et donc la progression pédagogique. Un apprenant qui vient de finir « Introduction à Python » et un autre qui l'a fini il y a trois mois se voient traités de la même façon. Les architectures GRU4Rec et SASRec du domaine RNN et transformeur combleraient ce manque.
Le moteur ne modélise pas l'objectif métier. Il maximise le clic passé, pas l'inscription payante ni l'achèvement. En production réelle, on pondère souvent chaque interaction par sa valeur métier (une inscription vaut 10 minutes visionnées, un certificat acheté vaut 50) — cet arbitrage relève d'une décision produit, pas d'un algorithme.
Le moteur s'auto-alimente. Sans exploration contrôlée (module 7), il apprend uniquement de ce qu'il a lui-même exposé. Une fraction de 5 à 10 % d'exploration par Thompson Sampling est le minimum viable.
Le moteur n'a pas de contrainte d'équité. Il peut cristalliser des groupes démographiques dans des parcours étroits. C'est un sujet du cours 24 (éthique) qu'il faut aborder avant toute mise en production.
En pratique, l'ordre qui rapporte le plus de qualité par heure investie est : (1) découpage temporel et baseline de popularité, (2) factorisation implicite ALS, (3) journalisation complète des impressions, (4) hybride LightFM pour le démarrage à froid, (5) modèle à deux tours pour la queue longue et la fraîcheur, (6) exploration Thompson pour casser la boucle. Aller plus loin sans avoir fait ces six étapes rapporte peu.
En résumé
- Le découpage temporel est la seule évaluation honnête ; le découpage aléatoire fausse le NDCG de 15 à 25 %.
- Comparer plusieurs modèles sur le même sous-ensemble avec les mêmes métriques ; garder la popularité comme baseline non triviale.
- Un tableau qui suit rappel, NDCG, diversité, couverture en parallèle est la vue minimale ; regarder un seul chiffre trompe.
- L'A/B test contrôlé avec métriques primaires et garde-fous décide du déploiement ; deux semaines minimum et 20 000 apprenants par bras pour détecter 2 %.
Module suivant : la récapitulation et l'examen de 40 questions qui clôt le parcours.