clavier ouvert

Blog d'enseignement d'Adrien Foucart.
Toutes les opinions présentées ici n'engagent que moi. Blog garanti sans pub, sans traqueurs, et 100% rédigé par un humain.

🖋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:

  1. Ouvrir (et refermer) 300 fichiers Excel pour aller regarder les formules écrites par les étudiant.es une à une.
  2. 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:

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 G4

Et 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 graphe

C’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:

Le rapport obtenu ressemble à ça (anonymisé ici):

agrandir

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

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:

agrandir

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.

Commentaires, remarques, erreurs qu'il faut absolument me faire remarquer? Contactez-moi sur Mastodon ou par mail (adrien@adfoucart.be)