🖋2026.01.26
correction
assistée par ordinateur
Pourquoi passer dix heures à faire des corrections lorsqu’on peut passer cinq heures à faire un système qui automatise partiellement les corrections, et puis passer quand même dix heures à faire des corrections?
Bon, il y a effectivement quelques révisions à prévoir pour réutiliser “mon” système, mais même si le résultat n’a pas été tout à fait à la hauteur de mes attentes cette session, je pense que les concepts de base sont raisonnablement sains. Mais prenons les choses étape par étape.
1. Le besoin
Une partie de mes corrections était sur papier. Pas d’automatisation par là, juste une solide grille de correction permettant de ne pas trop agoniser sur chaque demi-point à donner.
Mais deux parties d’examen étaient susceptibles de recevoir un peu d’aide technologique: une question de maths en Excel, et un examen de machine learning avec des fonctions Python à compléter.
Ce que je voulais éviter, c’est de devoir:
- Ouvrir (et refermer) 300 fichiers Excel pour aller regarder
les formules écrites par les étudiant.es une à une.
- Ouvrir, exécuter et refermer 200 fichiers Python, et possiblement les modifier pour tester des fonctions individuelles.
Il faut sans doute le préciser au vu de la situation actuelle, mais l’objectif n’est évidemment pas de balancer tout ça dans ChatGPT, Copilot ou autre et de lui “demander” de me coter tout ça pendant que je bois un café. Je veux me faciliter la vie et rester le plus correct possible dans mes cotes.
Non, ce que je veux, c’est pouvoir me faciliter la vie. Tester automatiquement ce qui est testable automatiquement, et me mettre en avant les informations utiles pour corriger chaque étudiant.e sans devoir aller manuellement ouvrir tous les fichiers soumis.
Idéalement, j’aimerais avoir quelque chose qui va me détecter facilement quand quelque chose est juste, pour que je puisse concentrer mon attention sur les cas où il y a des erreurs pour voir si ce sont des erreurs très problématiques ou non. Ou pas des erreurs du tout: tout système automatique aura ses faux positifs.
2. Le plan
Ce que j’avais en tête était, en principe, raisonnablement simple:
- Récupérer, pour chaque étudiant.e, le(s) fichier(s) remplis.
- Pour Excel:
- Récupérer le contenu des cellules à modifier.
- Comparer ce contenu avec les formules attendues.
- Créer un fichier “rapport” par étudiant.e avec le contenu des cellules et une note indiquant si c’est juste ou pas.
- Pour Python:
- Copier les fichiers dans un environnement d’évaluation avec
des fichiers
pytestpermettant de tester les différentes fonctions implémentées. - Lancer
pytestet récupérer le résultat. - Créer un fichier “rapport” par étudiant avec le résultat des tests et une copie du code.
- Copier les fichiers dans un environnement d’évaluation avec
des fichiers
Ensuite, dans les deux cas, créer aussi un fichier “index” avec la liste des étudiant.es et un lien vers leur rapport. Tout ça idéalement en HTML pour pouvoir facilement naviguer d’un rapport à l’autre.
Python reste mon langage de prédilection, c’est donc de que j’utiliserai pour essayer de faire tout ça.
3. Premier écueil: la récupération des fichiers
Il y a plusieurs configurations possibles pour faire passer des examens sur machine à la Haute-École. Dans celle utilisée pour le cours de mathématiques, les étudiant.es reçoivent des identifiants temporaire pour le temps de l’examen, et doivent mettre leurs fichiers à la fin sur un disque réseau. À la fin de l’examen, on peut récupérer le contenu de ces disques, identifiés par le login temporaire. Les feuilles de login sont distribuées avec les énoncés lors de l’examen, et il n’y a donc pas de correspondance connue a priori entre les comptes et les étudiant.es.
Par conséquent, les consignes indiquaient clairement que les
étudiant.es devaient renommer le fichier Excel fourni
(Probabilites.xlsx) avec le format
NOM_PRENOM.xlsx. Ils devaient aussi dans le même
examen faire un projet Java qu’ils devaient également appeler
NOM_Prenom et mettre à la racine du disque réseau.
Et on leur demandait également de mettre leur nom et prénom sur
la feuille de login qu’on récupérait à la fin de l’examen.
On aurait pu simplement décider que le non respect des consignes amènerait un 0, et ne pas corriger si le fichier Excel n’était pas correctement renommer, mais on est gentils. On va donc essayer de faire un minimum d’effort pour trouver toutes les correspondances entre les étudiant.es et leur compte.
Pour commencer, je parcours tous les dossiers et, dans
chaque, dossier, j’utilise la méthode glob de pathlib
pour rechercher tous les fichiers .xlsx qui se
trouvent dans le dossier ou ses sous-dossiers:
xlsx_files = list(f.glob("**/*.xlsx")). S’il y en a
un qui ne s’appelle pas Probabilites.xlsx, je
récupère le nom du fichier et considère que c’est le nom de mon
étudiant.e. C’est le cas de base, pour celleux qui ont respecté
les consignes.
Sinon, je cherche s’il y a un dossier à la racine qui correspondrait au projet Java, et je note l’association. Et sinon, je note simplement le numéro de session, et espère qu’on retrouve une feuille de login avec un nom dessus. Sur un peu moins de 300 étudiant.es qui ont passé l’examen, je n’en ai qu’un seul au final qui a rempli quelque chose et qui n’a laissé aucun moyen de l’identifier.
La seconde configuration, utilisée pour l’examen en Python, rend les choses un peu plus faciles: les étudiant.es uploadent leur projet sur une plateforme Moodle, où iels sont connecté.es avec leur propre compte. J’ai donc alors un fichier ZIP par étudiant.e avec son nom dessus.
4. Tester Excel, c’est galère
Il y a plusieurs librairies qui permettent de s’attaquer à un
fichier Excel en Python. Une des façons les plus simples est
d’utiliser pandas et sa
fonction read_excel.
On peut y faire:
import pandas as pd
file_content = pd.read_excel("/path/to/excel/file.xlsx", header=None)Pour récupérer le contenu du fichier dans une DataFrame. Problème: on récupère uniquement les valeurs des cellules. Mais je ne veux pas juste les valeurs: je veux récupérer les formules. En effet, dans l’exercice proposé, une bonne partie des valeurs à trouver leur sont déjà données dans le fichier, et iels devaient justement trouver la formule permettant de les retrouver!
Pandas utilise openpyxl
en arrière-fond. Avec openpyxl, on peut ouvrir un
fichier en récupérant soit les valeurs, soit les formules, et
ensuite adresser directement les cellules:
from openpyxl import load_workbook
wb = load_workbook("/path/to/excel/file.xlsx", data_only=False)
sheet = wb["Sheet1"]
g4 = sheet["G4"].value # formule dans la cellule G4
wb_val = load_workbook("/path/to/excel/file.xlsx", data_only=True)
sheet_val = wb_val["Sheet1"]
g4_val = sheet_val["G4"].value # valeur dans la cellule G4Et j’ai — malheureusement après avoir choisi de ne pas
utiliser cette libraire — fini par trouver un moyen d’accéder
aussi aux graphes présents sur la feuille, et à récupérer
quelles données ont été utilisées pour réaliser ce graphe, et de
quel type de graphe il s’agit. Le problème, c’est que c’est
assez mal documenté, et qu’on doit accéder à des attributs
“privés” (au sens pythonesque de “marqué par un _
comme ne faisant pas partie de l’API”) pour pouvoir jouer
avec:
all_charts = sheet._charts
for chart in all_charts:
print(chart.tagname) # donnera le type de graphe
for ser in chart.ser: # parcourir les ranges
print(ser.val.numRef.f) # formule du range
print([pt.v for pt in ser.val.numRef.numCache.pt]) # valeurs utilisées dans le grapheC’est un peu dégueu, et probablement pas très stable, mais ça
peut marcher. Malheureusement, je n’ai pas trouvé ça tout de
suite, et je suis tombé sur des recommandations pour xlwings, que
j’ai finalement utilisé. xlwings permet de
récupérer le contenu d’un fichier Excel avec ses formules et ses
valeurs, ainsi que d’extraire les graphes et de les sauvegarder
en fichiers images. Par contre, s’il y a un moyen de retrouver
les données utilisées pour créer le graphe, je ne l’ai pas
trouvé. Et xlwings est leeeeeent. Je pense que la
prochaine fois, je reviendrai sur openpyxl.
xlwings propose une API similaire:
import xlwings as xw
wb = xw.Book("/path/to/excel/file.xlsx")
sheet = wb.sheets['Sheet1']
g4 = sheet['G4'].formula
g4_val = sheet['G4'].value
# extraction des graphes
charts = sheet.charts
for chart in charts:
chart.to_png("/path/to/chart.png")Avec xlwings, j’ai donc pu faire à peu près ce
que j’avais en tête:
- Récupérer les formules des cellules qui m’intéressent.
- Récupérer le graphe s’il y en a un, l’extraire et le sauvegarder en PNG.
- Comparer les formules à des “formules attendues”.
- Générer un rapport avec tout ça.
Le rapport obtenu ressemble à ça (anonymisé ici):
Le problème principal qui m’a tout de même fait rouvrir une bonne partie des fichiers Excels, c’est que le nombre de différentes manières équivalentes d’écrire les formules était juste trop grand. Entre les parenthèses, les différentes manières possibles d’arriver au résultat, ou le choix (tout à fait raisonnable!) fait par certain.es d’utiliser des cellules “intermédiaires” pour découper les formules en éléments plus simples… on ne s’en sort pas trop, et énormément d’étudiant.es avaient une réponse correcte à laquelle je n’avais pas pensé.
Notes pour le futur
- Utiliser
openpyxldirectement. - Mettre dans le rapport les formules et les valeurs des cellules attendus, mais aussi de tout autre cellule non-nulle modifiée par l’étudiant.e.
- Tester d’abord sur les valeurs en excluant
certaines formules “non-autorisées” pour y arriver, plutôt que
de tester sur les formules. Par exemple ici, certain.es ont
tenté le coup de “recopier” les valeurs qui étaient déjà données
(avec par exemple
=C4pour récupérer dans la cellule où ils devaient mettre la formule la valeur déjà fournie correspondante): la valeur serait correcte, mais la formule pas. - Récupérer les ranges et valeurs du graphe et les mettre dans le rapport, quitte à recréer le graphe en matplotlib sur base de ces valeurs plutôt que d’essayer de l’extraire depuis Excel si je veux l’avoir quand même affiché.
5. Tester Python, c’est plus facile?
J’avais mis toutes les chances de mon côté avec l’énoncé de
mon examen de Machine Learning en Python: les étudiant.es
avaient des fonctions à compléter dont iels n’étaient pas
supposé.es changer le nom ou la signature, et avec des
spécifications très claires sur les entrées et sorties
attendues. Le scénario idéal pour pouvoir déployer des tests
unitaires. J’avais donc préparé une batterie de tests avec
pytest. Mon plan était donc: pour chaque
étudiant.e, récupérer les deux fichiers de code qu’ils devaient
modifier, les mettre dans un environnement de test avec mes
fichiers pytest prêts à l’emploi, lancer les tests,
récupérer les résultats, et mettre ces résultats avec le code
dans un fichier “rapport” par étudiant.e.
La correction devait ainsi être raisonnablement rapide: si les tests d’une fonction sont passés, c’est juste; sinon, on jette un coup d’oeil au code pour voir si l’étudiant.e a quand même compris quelque chose.
Premier écueil: les étudiant.es ont été un peu trop
enthousiastes sur l’auto-complétion, et ont souvent inclus des
libraires imprévues dans leurs imports. Je ne spécifiais nulle
part dans l’énoncé qu’il fallait limiter les import
aux librairies nécessaires, donc je n’ai pas voulu pénaliser ça…
mais je n’avais pas moi-même installé ces librairies dans mon
environnement de test, donc les tests unitaires foiraient
complètement. Heureusement, ces imports excessifs n’étaient pas
utilisés ensuite dans le code, et pouvaient donc être
automatiquement enlevés avec autoflake.
J’ai fait les évaluations depuis mon PC-de-la-maison qui a
encore Windows dessus, donc j’ai fait un bon vieux script
.bat pour parcourir les dossiers, lancer
autoflake, lancer les tests et mettre les résultats
dans un fichier:
REM Run autoflake and pytest in all directories
for /f %%i in ('dir /b/ad') do (
echo Autoflake in "%%i"
autoflake --in-place --remove-all-unused-imports "%%i/src/conseiller_fraude.py"
autoflake --in-place --remove-all-unused-imports "%%i/src/detection_fraude.py"
echo pytest in "%%i"
pytest "%%i/tests/test_conseiller_fraude.py" --no-header -tb=no >> "%%i.txt"
pytest "%%i/tests/test_detection_fraude.py" --no-header -tb=no >> "%%i.txt"
)Le pytest aurait pu être fait en une fois, mais
le faire séparément sur les deux fichiers permet de tout de même
avoir les résultats si un des deux fichiers plante complètement,
par exemple pour cause d’import foireux.
Les rapport obtenus ressemblent à ceci:
Le problème principal que j’ai eu avec ces tests n’était ici pas technique, mais conceptuel: les tests unitaires que j’avais prévu ne capturaient pas bien les critères sur lesquels je voulais les évaluer. Du coup, à part si tous les tests d’une méthode passaient (ce qui n’arrivait malheureusement pas très souvent à part pour les méthodes les plus simples), je devais tout de même souvent repasser sur le code ligne par ligne avec ma grille de critères pour voir ce qui passait ou non. C’était un peu bête de ma part: j’ai écrit les tests unitaires comme si je développais le programme, au lieu de les écrire avec la grille d’évaluation en tête. Probablement parce que je n’avais pas encore écrit la grille d’évaluation, ce qui n’aide pas.