← Articles

INSATutorat : former les binômes de tutorat avec Gale-Shapley

· 6 min de lecture · mis à jour le

Go · Algorithmique · Web

Au département STPI de l’INSA Rouen, les étudiants de deuxième année (STPI2) peuvent devenir tuteurs de ceux de première année (STPI1). Jusqu’ici, tout se faisait à la main : récupérer les inscriptions, comparer les emplois du temps, trouver des binômes qui ont des créneaux en commun, puis compter les heures en fin de semestre. C’était long et fastidieux.

L’idée de la plateforme m’est venue à l’été 2024. Quand j’ai découvert le Travail d’Initiative Personnelle (TIP), un projet libre d’une cinquantaine d’heures, j’en ai fait mon sujet : les étudiants s’inscrivent, leurs emplois du temps sont récupérés automatiquement, l’application forme les binômes, et le suivi des séances se fait au même endroit.

L'espace tuteur d'INSATutorat : nombre de séances, de tutorés et d'heures, dernières séances et heures saisies par chaque tutoré

L’espace tuteur, avec les données de démonstration du projet.

Une campagne de tutorat

Tout tourne autour de la « campagne » : une par semestre, soit deux par an. Je n’ai pas codé le semestre en dur, pour que l’outil survive à un changement d’organisation du tutorat.

  1. Le responsable crée la campagne depuis l’espace admin, et les étudiants concernés la voient apparaître sur leur tableau de bord.
  2. Les tuteurs choisissent leurs matières et le nombre de tutorés qu’ils acceptent ; les tutorés choisissent les matières où ils ont besoin d’aide.
  3. Chacun indique ses disponibilités en dehors de ses cours.
  4. Le responsable lance l’affectation automatique. Il peut toujours corriger à la main, avant comme après, et des inscriptions tardives restent possibles.
  5. Pendant le semestre, les tuteurs notent le contenu de leurs séances et les tutorés saisissent leurs heures. Le responsable voit à tout moment le volume horaire de chacun.

Comment c’est construit

Le client et le serveur sont séparés. Le front-end est en Nuxt (Vue.js) et ne fait qu’appeler l’API. Le back-end est une API REST en Go : il porte la logique métier et parle à une base SQL via l’ORM Gorm, qui gère aussi les migrations.

L’authentification idéale aurait été le CAS de l’INSA, qui aurait donné directement les groupes des étudiants. Sans accès au CAS, la connexion se fait par un lien envoyé par e-mail, avec un jeton temporaire. La session est ensuite portée par un cookie HttpOnly et Secure, qu’aucun script de la page ne peut lire, et un middleware la vérifie avant chaque route protégée.

Récupérer les emplois du temps

La seule source à la fois ouverte et à jour pour les emplois du temps, c’était agendas.insa-rouen.fr, une vieille plateforme qui ne sait exporter qu’un flux RSS. Chaque cours y est un <item> dont il faut extraire les morceaux utiles :

<item>
  <title>Mar Mai 20 2025: STPI22-M7-TD-3</title>
  <ev:startdate>2025-05-20T15:00:00</ev:startdate>
  <ev:enddate>2025-05-20T16:30:00</ev:enddate>
  <description>Mar Mai 20 2025 15:00: <br/><br/>STPI22-M7-TD-03<br/>…</description>
  <ev:location>DU-B-RJ-01</ev:location>
</item>

Le nom du cours, le créneau, la salle et le groupe sont mélangés entre le titre, les dates et une description en HTML. Les transformer en structures propres m’a pris plus de temps que prévu ; un export JSON en aurait économisé une bonne partie.

Pour chaque créneau de la semaine, l’application retient ensuite une valeur cc : −1-1 si l’étudiant s’est déclaré indisponible, 00 s’il est libre, et sinon le nombre de fois où il a cours sur ce créneau dans le semestre. Dans la base, ça donne un tableau par jour de la semaine :

{"1": [3, 13, 0, 0, 13, 10, 0], "2": [11, 15, 1, 0, 14, 9, 0], "3": [8, 9, 14, 0, 0, 12, 0]}

Lundi a bien l’indice 1 : Go fait commencer la semaine le dimanche, à l’américaine.

Donner un score à un créneau

Il fallait ensuite mesurer à quel point deux emplois du temps sont compatibles. Ma première formule était V(c)=11+cV(c)=\frac{1}{1+c} : 1 pour un créneau libre, de moins en moins quand les cours s’accumulent. Elle punissait beaucoup trop vite. Pour un étudiant qui a cours 2 fois dans le semestre sur un créneau et un autre qui n’y a cours qu’une fois, le créneau tombe à 13×12≈0,17\frac{1}{3}\times\frac{1}{2}\approx0{,}17, alors qu’il est presque toujours libre pour les deux.

Après quelques essais sur GeoGebra, j’ai retenu une décroissance exponentielle :

V(c)=e−c8V(c)=e^{-\frac{c}{8}} si c≥0c\ge0, et V(c)=0V(c)=0 si l’étudiant est indisponible.

Avec 2 et 4 cours, le créneau garde un score proche de 0,5, et la fonction ne s’effondre que quand les cours deviennent fréquents. Le score d’un binôme sur un créneau est le produit des scores des deux étudiants, et son score global la somme sur tous les créneaux.

Former les binômes

Avoir un score pour chaque paire ne suffit pas : il faut encore décider qui va avec qui. Mon premier réflexe a été un algorithme glouton, où chaque tuteur prend à son tour le tutoré le plus compatible. Un exemple à deux tuteurs suffit à voir le problème :

Tutoré CTutoré D
Tuteur A27,1025,70
Tuteur B32,2020,15

Le glouton donne C à A (27,10), et B hérite de D (20,15). Pourtant B et C s’entendent mieux que tous les autres : il aurait fallu donner D à A et C à B.

Je suis donc passé à l’algorithme de Gale et Shapley, celui des « mariages stables », qu’utilisent aussi Parcoursup et le SCEI pour les concours d’écoles d’ingénieurs. Un appariement est stable quand il n’existe aucune paire d’étudiants qui préféreraient tous les deux être ensemble plutôt qu’avec leur partenaire actuel. Ce n’est pas forcément l’appariement qui maximise la somme des scores, mais personne n’a de raison de le contester. L’algorithme tourne matière par matière ; je me suis appuyé sur une implémentation open source en Go.

Restait le cas où les nombres ne tombent pas juste. Gale-Shapley associe deux ensembles de même taille, alors que les tuteurs proposent souvent plus de places qu’il n’y a de tutorés. S’il manque des places, l’application le signale au responsable, qui augmente des quotas. S’il y en a trop, j’ajoute des « tutorés factices » avec un score de −1 jusqu’à égaliser les deux côtés. L’algorithme explore alors toutes les possibilités, et ce sont les places des tuteurs les moins compatibles qui restent vides, plutôt que celles du premier tuteur de la liste.

Ce que ça a donné

La plateforme a servi à accompagner plus de 400 étudiants. J’ai mesuré un peu plus de 49 heures d’édition de code, pour 49 heures prévues au cahier des charges. Sans surprise, le fichier sur lequel j’ai passé le plus de temps est celui qui génère les affectations.

J’ai utilisé l’IA pour des composants répétitifs du front-end, sur une stack que je connaissais déjà. Le back-end en Go, qui porte le score et l’affectation, je l’ai écrit entièrement moi-même : c’était la partie dont je voulais apprendre quelque chose.

Comme prévu dans le cahier des charges, le code est public, sur GitHub. Les documents du TIP sont aussi disponibles :