Introduction
La ludothèque sait quels jeux sont utilisables ce soir. Elle veut maintenant une liste transparente d'alternatives similaires, assez expliquée pour qu'une personne choisisse. Vous calculerez des distances sur deux caractéristiques publiques, afficherez des écarts signés en unités d'origine et conserverez tous les ex æquo du seuil.
Ce laboratoire suppose les notions Python d'admissibilité et MinMax du précédent, mais démarre indépendamment : toutes les entrées et la fonction auxiliaire complète sont présentes, sans fichier ni processus antérieur. Vous réaliserez le classement et de vrais JSON affichés sans complétion par le lecteur. Les 50 minutes sont une estimation de l'auteur. La similarité de contenu ne prédit pas les goûts et ne démontre pas les performances sur un grand catalogue.
Mesurez chaque alternative admissible
Dans cette étape, vous construirez une table complète des distances et montrerez ce que chaque candidat change par rapport à la référence.
Ouvrez l’onglet Terminal et entrez dans l’espace indépendant. Ouvrir ou reconnecter cette entrée ne réinstalle pas l’espace de travail et n’efface pas votre progression. cd définit le répertoire de ce terminal ; répétez-le dans tout nouveau terminal.
cd /home/labex/project/board-games
Le catalogue possède des clés uniques game_id, des titres d'affichage, la durée minutes, la note complexity de 1 à 5 et les bornes inclusives min_players/max_players. Le CSV indépendant contient un true/false par ID dans un autre ordre. night.json donne l'exemple, les players et le k souhaité. La référence peut être indisponible ou inadaptée : seuls les candidats doivent convenir ce soir. Les mêmes titres ou caractéristiques avec des ID distincts sont légitimes, distance zéro comprise. Seul le reference_id exact est exclu.
cat affiche un texte sans le modifier. Relisez la requête et l'auxiliaire fourni, qui réalise le travail précédent.
cat night.json
cat suitability.py
Exécutez cette interface existante avant d’ajouter un calcul de distance. --mode suitable appelle choose_games ; --output, obligatoire, nomme le JSON à écrire. Sans autre option d’entrée, le lanceur lit catalog.csv, availability.csv et night.json dans le répertoire courant. Le lecteur affiche uniquement le fichier que vous avez enregistré.
python3 planner.py --mode suitable --output suitable.json
python3 show_results.py suitable.json
Vous devez voir les six ID admissibles alphabétiques left, near, raw, right, same et zero, avec l’échelle ci-dessous. C’est un résultat d’admissibilité fonctionnel, pas une recommandation terminée : le module de classement comporte encore des régions d’implémentation vides.
choose_games associe les états par ID et renvoie tous les ID admissibles plus une échelle calculée sur tout le catalogue, avant filtrage. L'ordre est minutes, puis complexity. scale.min vaut [10, 1] et scale.range, [100, 4]. Réutilisez-les pour requête et candidats : un prêt ne doit pas déplacer la règle. Il n'y a ni labels cibles ni séparation supervisée entraînement/test.
planner.py gère options et formats avec coursekit.py, sans choisir ni classer. Modifiez seulement suitability.py, ranking.py et les résultats ; conservez les CSV/JSON, planner.py, coursekit.py, show_results.py et les deux starter-*.py.
Pour une valeur x, MinMax donne (x - minimum) / range. Une caractéristique constante garde une étendue zéro mais utilise le diviseur un ; la requête appartient au catalogue, donc ses écarts sur cette caractéristique sont nuls. Pour requête normalisée q et candidat c, la distance euclidienne vaut sqrt((c_minutes-q_minutes)^2 + (c_complexity-q_complexity)^2). Les deux caractéristiques ont le même poids après normalisation. Une petite distance signifie similarité selon cette règle, pas plaisir supérieur.
L'explication brute est distincte : minutes_delta et complexity_delta valent candidat moins référence, avant normalisation. Un delta de minutes négatif indique un jeu plus court. Ces colonnes doivent provenir du vrai candidat portant cet ID, jamais d'une autre ligne ou d'une distance inventée.
Examinez le squelette de classement avant de le modifier. from suitability import choose_games importe l’auxiliaire local. Les lignes fournies prepared, by_id et reference relient cette sortie et la requête aux enregistrements du catalogue indexés par ID. La liste rows vide, la boucle des candidats, le tri et le retour final fournissent la structure environnante. Trois commentaires marquent les régions à compléter ; ils ne calculent ni les distances ni la sélection manquantes.
cat ranking.py
Créez une sauvegarde du source. cp copie le fichier courant ; cette sauvegarde est la base inachevée, pas une réponse complète.
cp ranking.py ranking.py.bak
np.where choisit un pour une étendue nulle et l'étendue réelle sinon. np.linalg.norm calcule la longueur euclidienne du vecteur à deux éléments. float convertit NumPy en nombres compatibles JSON. La clé de tri est une paire : distance non arrondie, puis game_id si les distances sont exactement égales. La courte lambda définit cette clé. Ne triez pas des distances préalablement arrondies. Les derniers bits de formules équivalentes peuvent inverser des valeurs très proches ; les décimales affichées ne constituent pas la règle de tri enregistrée.
Ouvrez le module dans nano. Comme au premier laboratoire, LC_ALL=C.UTF-8 s’applique uniquement à cette invocation de l’éditeur. Gardez quatre espaces par niveau d’indentation ; Python utilise l’indentation pour regrouper les instructions.
LC_ALL=C.UTF-8 nano ranking.py
Appuyez sur Ctrl+W, recherchez ## Metric parameters et appuyez sur Entrée. Descendez au pass qui suit, appuyez sur Home, puis une fois sur Ctrl+K pour retirer cette ligne provisoire. Insérez ces cinq lignes avec leurs quatre espaces initiaux. Préservez rows = [] et la fonction environnante.
minimum = np.array(prepared['scale']['min'])
ranges = np.array(prepared['scale']['range'])
divisor = np.where(ranges == 0, 1, ranges)
query = np.array([reference['minutes'], reference['complexity']])
query_scaled = (query - minimum) / divisor
Appuyez ensuite sur Ctrl+W, recherchez ## Candidate record et appuyez sur Entrée. Descendez au pass de cette région, appuyez sur Home, puis une fois sur Ctrl+K. Insérez le bloc suivant. Il se trouve dans la boucle existante : ses instructions commencent donc par huit espaces ; les champs dans rows.append en utilisent douze. Gardez inchangés la ligne for, game = by_id[game_id] et le rows.sort ultérieur.
values = np.array([game['minutes'], game['complexity']])
candidate_scaled = (values - minimum) / divisor
distance = float(np.linalg.norm(candidate_scaled - query_scaled))
difference = values - query
rows.append({
'game_id': game_id, 'title': game['title'], 'distance': distance,
'minutes_delta': float(difference[0]),
'complexity_delta': float(difference[1]),
})
Laissez pour l’instant la région ## Cutoff selection avec cutoff = None et selected = []. Cela rend cette table intermédiaire lisible tout en laissant honnêtement la sélection inachevée. Appuyez sur Ctrl+O, Entrée, puis Ctrl+X pour enregistrer et quitter. Si vous remarquez une modification non voulue avant d’enregistrer, Ctrl+X, puis N abandonne cette session d’édition ; rouvrez et corrigez uniquement la région voulue.
Lancez planner.py en mode recommend. --output nomme la sortie réelle. Sans autres options, il lit catalog.csv, availability.csv, night.json dans le répertoire courant ; --catalog, --availability, --request donnent des chemins explicites.
python3 planner.py --mode recommend --output recommendations.json
Affichez votre JSON sauvegardé avec le lecteur. Il ouvre uniquement ce fichier, sans recalculer depuis le catalogue ni ajouter de lignes manquantes.
python3 show_results.py recommendations.json
Vous devez voir six candidats. zero est à zéro, near et same vers 0.1, left/right à 0.3. raw est vers 0.5001 malgré une minute supplémentaire : sa complexité augmente de deux points. Minutes +/- et Complexity +/- montrent les différences signées d'origine. Cette première version garde volontairement Cutoff: None et aucune sélection ; vous ajouterez la règle ensuite.
Si un SyntaxError ou un IndentationError apparaît, lisez son fichier et son numéro de ligne, rouvrez ranking.py et corrigez cette ligne ou région. N’écrasez pas tout le module pour réparer une seule ligne. Une erreur de fichier absent signifie souvent que le terminal est dans le mauvais répertoire : pwd l’affiche et le cd initial rétablit le bon emplacement, comme pratiqué au premier laboratoire. cp ranking.py.bak ranking.py restaure la base inachevée uniquement si vous voulez délibérément abandonner le travail de cette étape. Préservez les fichiers d’entrée plutôt que de les modifier pour faire passer un résultat.
Conservez tout l'ensemble au seuil
Dans cette étape, vous terminerez la sélection et utiliserez le même programme avec moins de candidats, puis aucun.
Continuez dans l’onglet Terminal, dans le répertoire suivant.
cd /home/labex/project/board-games
k représente un minimum visé lorsqu'il y a assez de jeux admissibles, pas exactement k lignes. Calculez et triez d'abord toutes les distances admissibles. Avec N candidats, le seuil est la distance à la position min(k, N). Les indices Python commençant à zéro, l'indice est min(k, N) - 1. Conservez tous les candidats dont la distance non arrondie est au plus cutoff + 1e-9. La marge absolue 1e-9 définit l'égalité au seuil ; ce n'est pas un regroupement pour le tri et elle n'autorise aucun arrondi préalable.
Avec moins de k, renvoyez les candidats réels. Sans candidat, renvoyez candidates et selected_ids vides et un seuil JSON null (None en Python). Aucun repli vers un jeu indisponible ou inadapté. Prendre d'abord les k plus proches de tout le catalogue, puis filtrer après, peut perdre des alternatives légales suivantes ; le filtrage préalable l'évite.
La requête normale a k=2. zero est le plus proche ; near et same partagent le seuil selon la règle : les trois sont sélectionnés. L'ordre suit les vraies distances non arrondies et game_id départage les égalités exactes. Gardez les six lignes pour montrer tous les candidats examinés.
Sauvegardez votre table de distances fonctionnelle avant d’ajouter la sélection. Cette copie conserve les paramètres métriques et les enregistrements des candidats que vous venez de tester.
cp ranking.py ranking-distances.py.bak
Ouvrez le même module, sans remplacer son travail précédent.
LC_ALL=C.UTF-8 nano ranking.py
Appuyez sur Ctrl+W, recherchez ## Cutoff selection et appuyez sur Entrée. Descendez à cutoff = None, appuyez sur Home, puis deux fois sur Ctrl+K pour retirer uniquement cette ligne et selected = []. Insérez ce bloc avec quatre espaces pour if et else, et huit dans chaque branche. Le retour final reste après le bloc, à son indentation existante.
if rows:
cutoff = rows[min(request['k'], len(rows)) - 1]['distance']
selected = [row['game_id'] for row in rows
if row['distance'] <= cutoff + 1e-9]
else:
cutoff = None
selected = []
La garde if rows empêche d’indexer une liste vide. Une compréhension de liste conserve les ID correspondants dans l’ordre trié existant. Préservez les paramètres métriques, la boucle des candidats et le tri de l’étape 1. Enregistrez avec Ctrl+O, Entrée et quittez avec Ctrl+X. Pour revenir en arrière dans cette étape, cp ranking-distances.py.bak ranking.py restaure la version testée de la table des distances, dont la sélection est encore incomplète ; refaites la modification du seuil plutôt que de répéter le fichier entier.
Le JSON doit contenir scale.min, scale.range dans l'ordre des caractéristiques ; chaque candidat a game_id, title, distance numérique, minutes_delta, complexity_delta signés ; ajoutez cutoff et selected_ids. Gardez des nombres finis à leur précision calculée, pas des chaînes arrondies. Des explications inoffensives sont permises. L'évaluation compare chaque ID avec des tolérances absolue et relative 1e-8, distinctes du seuil d'égalité 1e-9. Les formules équivalentes et choix ordinaires sont acceptés ; les goûts ne sont pas évalués. Les deux interfaces doivent fonctionner : choose_games renvoie eligible_ids alphabétiques et l'échelle complète, rank_games la recommandation. Un classement indépendant est permis sans appel interne à l'auxiliaire. Aucun autre résultat d'admissibilité n'est exigé dans l'espace de travail.
Régénérez et lisez le résultat normal.
python3 planner.py --mode recommend --output recommendations.json
python3 show_results.py recommendations.json
zero, near et same doivent maintenant être sélectionnés, les autres lignes restant visibles. cat recommendations.json montre les distances JSON non arrondies.
Deux requêtes fournies ne changent que la soirée, pas le catalogue ou les emprunts. Examinez-les avec cat. --request choisit une requête pour une seule commande ; il ne modifie pas night.json et ne crée pas de variable de terminal. Une autre sortie préserve le résultat normal.
cat sparse-night.json
python3 planner.py --mode recommend --request sparse-night.json --output sparse.json
python3 show_results.py sparse.json
Huit joueurs ne peuvent utiliser que right, même si k=5. Un seul candidat honnête est correct. L'échelle complète reste identique et sa distance vaut toujours 0.3.
cat empty-night.json
python3 planner.py --mode recommend --request empty-night.json --output empty.json
python3 show_results.py empty.json
Vingt joueurs n'ont aucun jeu adapté. Attendez une table sans lignes, Cutoff: None et un message explicite. Dans Terminal, cat empty.json affiche le seuil sauvegardé null. Le lecteur montre votre résultat vide, sans choisir de repli.
Les trois fichiers recommendations.json, sparse.json, empty.json sont obligatoires et doivent correspondre à leurs requêtes, régénérables depuis les entrées intactes. Pour récupérer une modification accidentelle du code, rouvrez nano et corrigez la région concernée en préservant votre autre travail terminé ; relancez ensuite les trois commandes. cp starter-ranking.py ranking.py restaure seulement le starter incomplet pour recommencer. Rouvrir le terminal ne réinitialise pas les données et ne lance pas setup.
Résumé
Vous avez mesuré tous les candidats admissibles avec la même échelle du catalogue, expliqué leurs distances par des écarts réels signés et sélectionné tous les ex æquo sans arrondi préalable. Trois fichiers couvrent une soirée ordinaire, des candidats insuffisants et leur absence. Il s'agit de similarité de contenu transparente, pas de prédire ce qu'une personne aimera.



