Skip to content

2021/2022-s2

L'épreuve dure 1h30.

Elle se déroule en mode examen, pour lequel l'accès au web est bloqué, excepté les sites suivants :

Vous devez travailler directement sur le fichier traite_csv.py fourni et présent dans le dossier exam se trouvant sur le bureau de votre session d'examen. Pour chaque question ci dessous, vous devez compléter un ou plusieurs TODO du fichier traite_csv.py. Ces TODO sont identifiés par des numéros correspondant au numéro de la question.

Vous devez envoyer votre travail sur un serveur, toutes les 15 minutes environ, en cliquant sur ENVOYER se trouvant également sur le bureau.

À la fin de l'examen il faut cliquer sur TERMINER, toujours sur le bureau, et ne surtout pas éteindre la machine à la main en appuyant sur le bouton de mise/arrêt sous tension.

Enfin, le module traceur.py vous est fourni à côté du fichier traite_csv.py à compléter pour vous aider à déboguer visuellement si cela vous aide.

Contexte

On s’intéresse pour cet examen à la lecture et au traitement d’un fichier de notes au format CSV. Dans tous l'examen, nous travaillerons avec le fichier notes.csv fourni et présent dans le dossier exam.

Le format CSV est un format de fichier très simple pour le stockage de tableaux de données. Un fichier CSV contient un unique tableau 2D. Chaque ligne du fichier est une ligne du tableau et sur chaque ligne les données de chaque colonne sont séparées par une virgule. Dans notre cas, une ligne correspond aux données d’un étudiant et les colonnes sont les suivantes :

  • prénom (chaîne de caractères);
  • nom (chaîne de caractères);
  • note à l’examen numéro 0 (entier) ;
  • note à l’examen numéro 1 (entier) ;
  • note à l’examen numéro 2 (entier).

Si l’on examine par exemple les trois premières lignes du fichier notes.csv affichées ci-dessous on voit que François Pignon a une note de 15 à l’examen 0, 16 à l’examen 1 et 6 à l’examen 2. À la ligne suivante, on passe à un nouvel étudiant nommé Juste Leblanc et ensuite à une étudiante nommée Marlène Sasseur.

1
2
3
François,Pignon,15,16,6
Juste,Leblanc,15,0,8
Marlène,Sasseur,12,16,9

Partie 1 : lecture d'un fichier CSV

On se propose de stocker les données de chaque étudiant dans un namedtuple que l'on appellera Etudiant. Ce namedtuple se compose de trois attributs :

  • prenom qui est une chaîne de caractères ;
  • nom qui est une chaîne de caractères ;
  • notes qui est un tuple composé de trois entiers compris entre 0 et 20 représentant les notes au trois examens (notes[0] est la note à l'examen 0).

Question 0

Analysez le code de la fonction fournie teste puis appelez cette dernière (#TODO 0 à la fin du fichier) lorsque le programme traite_csv.py est invoqué en tant que programme principal.

Dans la suite, on testera donc l'implémentation de nos fonctions en lançant traite_csv.py en tant que programme principal et en analysant les sorties générées par la fonction teste.

Question 1

Définissez le namedtuple Etudiant (#TODO 1.0 et #TODO 1.1) et implémentez la fonction lit_fichier (#TODO 1.2).

On rappelle que toute chaîne de caractères Python dispose d’une méthode split permettant de la décomposer en sous-chaînes selon un séparateur donné. Par exemple, "Ah bon,il n'a pas de prénom,?".split(",") renvoie la list ["Ah bon", "il n'a pas de prénom", "?"].

Question 2

Implémentez la fonction génératrice lit_fichier_gen (#TODO 2) qui renvoie un générateur permettant d'itérer sur tous les étudiants présents dans le fichier CSV passé en paramètre. L'objectif est exactement le même que celui de lit_fichier mais la fonction génératrice permet d'économiser du temps et de la mémoire dans le cas où le fichier n'a pas besoin d'être parcouru intégralement.

Partie 2 : analyse des données

On va maintenant écrire un certain nombre de fonctions d'analyse et de tri des étudiants. Ces fonctions prennent toutes en entrée une list contenant des instances du namedtuple Etudiant telle que renvoyée par la fonction lit_fichier implémentée dans la partie 1.

On cherche lorsque c'est possible à réaliser les traitements en une seule passe sur les données. On considère également que la list d'étudiants passée en paramètre de ces fonctions est toujours non vide.

Question 3

Implémentez la fonction calcule_moyennes (#TODO 3) qui calcule la moyenne de la promotion pour chacun des trois examens.

Question 4

Implémentez la fonction génératrice recupere_etudiants_avec_un_20 (#TODO 4) qui renvoie un générateur permettant d'itérer sur les étudiants ayant au moins une note de 20/20.

Question 5

Implémentez la fonction recupere_minis_maxis_names (TODO 5) qui renvoie les étudiants ayant la note minimale pour chaque examen.

Question 6

Implémentez la fonction recupere_classement (TODO 6) qui renvoie les étudiants classés selon leur moyenne.

Partie 3 : histogrammes

On cherche à visualiser graphiquement les notes obtenues à chaque épreuve. Comme le montre l'image ci-dessous, on se propose de dessiner pour chaque note possible (de 0 à 20) une barre verticale sous forme d’un rectangle rouge. Pour chaque note n, la largeur du rectangle est fixée à 20 et sa hauteur à 20 fois le nombre d’étudiants ayant une note de n.

Question 7

Complétez la fonction genere_histogramme (TODO 7) qui prend un itérable sur les étudiants, ainsi qu’un numéro d’épreuve (entre 0 et 2), générant un histogramme en SVG et renvoyant le nom du fichier créé. Par souci de simplicité, on n'utilisera pas de module svg.py. On générera donc directement le code SVG depuis traite_csv.py en se basant sur le code SVG affiché ci-dessous.

Voici ce que doit générer votre fonction genere_histogramme pour l'examen 0 sur le fichier notes.csv présent dans votre dossier exam.

histogramme

Et voici le code SVG de cet histogramme :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<svg width='420' height='120'>
<rect width='420' height='120' fill='white'/>
<text x='0' y='120'>0</text>
<text x='20' y='120'>1</text>
<text x='40' y='120'>2</text>
<text x='60' y='120'>3</text>
<rect width='20' height='20' x='80' y='80' fill='red'/>
<text x='80' y='120'>4</text>
<text x='100' y='120'>5</text>
<rect width='20' height='40' x='120' y='60' fill='red'/>
<text x='120' y='120'>6</text>
<rect width='20' height='60' x='140' y='40' fill='red'/>
<text x='140' y='120'>7</text>
<rect width='20' height='80' x='160' y='20' fill='red'/>
<text x='160' y='120'>8</text>
<rect width='20' height='20' x='180' y='80' fill='red'/>
<text x='180' y='120'>9</text>
<rect width='20' height='20' x='200' y='80' fill='red'/>
<text x='200' y='120'>10</text>
<text x='220' y='120'>11</text>
<rect width='20' height='100' x='240' y='0' fill='red'/>
<text x='240' y='120'>12</text>
<rect width='20' height='60' x='260' y='40' fill='red'/>
<text x='260' y='120'>13</text>
<rect width='20' height='40' x='280' y='60' fill='red'/>
<text x='280' y='120'>14</text>
<rect width='20' height='80' x='300' y='20' fill='red'/>
<text x='300' y='120'>15</text>
<rect width='20' height='20' x='320' y='80' fill='red'/>
<text x='320' y='120'>16</text>
<rect width='20' height='20' x='340' y='80' fill='red'/>
<text x='340' y='120'>17</text>
<rect width='20' height='20' x='360' y='80' fill='red'/>
<text x='360' y='120'>18</text>
<text x='380' y='120'>19</text>
<rect width='20' height='40' x='400' y='60' fill='red'/>
<text x='400' y='120'>20</text>
</svg>

Correction globale

Cliquez ici pour révéler la correction.

Voici une proposition de correction.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#!/usr/bin/env python3

"""Lecture et analyse d'un fichier CSV contenant des notes."""

# TODO 1.0 : ajoutez ici l'import dont vous avez besoin pour `namedtuple`
from collections import namedtuple


# TODO 1.1 : définissez ici le namedtuple Etudiant.
# Le namedtuple doit s'appeler EXACTEMENT Etudiant et ses attributs
# doivent EXACTEMENT être `prenom`, `nom` et `notes`
Etudiant = namedtuple("Etudiant", ["prenom", "nom", "notes"])


def lit_fichier(nom_fichier):
    """Lit le fichier dont le nom est passé en paramètre.

    Renvoie une `list` contenant les instances
    d'`Etudiant` correspondant aux lignes dans le fichier.
    """
    # TODO 1.2 : implémentez la fonction `lit_fichier`
    etudiants = []
    with open(nom_fichier, "r", encoding="utf_8") as fichier_etudiants:
        for ligne_etudiant in fichier_etudiants:
            donnees = ligne_etudiant.split(",")
            prenom = donnees[0]
            nom = donnees[1]
            note0 = int(donnees[2])
            note1 = int(donnees[3])
            note2 = int(donnees[4])
            etudiants.append(Etudiant(prenom, nom, (note0, note1, note2)))
    return etudiants


def lit_fichier_gen(nom_fichier):
    """Lit le fichier dont le nom est passé en paramètre.

    Renvoie un générateur permettant d'itérer sur les instances
    d'`Etudiant` correspondant aux lignes dans le fichier.
    """
    # TODO 2 : implémentez la fonction `lit_fichier_gen`
    with open(nom_fichier, "r", encoding="utf_8") as fichier_etudiants:
        for ligne_etudiant in fichier_etudiants:
            donnees = ligne_etudiant.split(",")
            prenom = donnees[0]
            nom = donnees[1]
            note0 = int(donnees[2])
            note1 = int(donnees[3])
            note2 = int(donnees[4])
            yield Etudiant(prenom, nom, (note0, note1, note2))


def calcule_moyennes(etudiants):
    """Calcule la moyenne pour chaque examen de la `list` d'étudiants passée en paramètre.

    Renvoi un tuple contenant 3 entiers :
      - la moyenne de la `list` pour l'examen 0 ;
      - la moyenne de la `list` pour l'examen 1 ;
      - la moyenne de la `list` pour l'examen 2.
    """
    # TODO 3 : implémentez la fonction `calcule_moyennes`
    totaux = [0] * 3
    for etudiant in etudiants:
        for i in range(3):
            totaux[i] += etudiant.notes[i]
    return tuple(totaux[i] / len(etudiants) for i in range(3))


def recupere_etudiants_avec_un_20(etudiants):
    """Cherche les étudiants ayant au moins un 20/20.

    Renvoie un générateur permettant d'itérer sur les instances
    d'Etudiant de la liste etudiants passée en paramètre ayant
    au moins une note de 20/20.
    """
    # TODO 4 : implémentez la fonction génératrice `recupere_etudiants_avec_un_20`
    return (etu for etu in etudiants if 20 in etu.notes)


def recupere_minis_maxis_names(etudiants):
    """Cherche les étudiants ayant la note minimale pour chaque examen.

    Renvoie un `tuple` contenant 3 éléments : 1 élément pour chaque examen.
    Chacun de ces trois éléments est lui même un tuple à 2 éléments :
        - une `list` contenant le prénom concaténé au nom sans espace des étudiants
          ayant la note minimale pour l'examen
        - une `list` contenant le prénom concaténé au nom sans espace des étudiants
          ayant la note maximale pour l'examen

    Concernant la concaténation du prénom et du nom, par exemple pour le premier étudiant
    du fichier `notes.csv` présent dans votre dossier examen le résultat doit être
    exactement `FrançoisPignon`

    Parce que ça ne change pas la complexité et par soucis de simplicité, on s'autorise ici
    à parcourir la `list` donnée en paramètres deux fois.
    """
    # TODO 5 : implémentez la fonction `recupere_minis_maxis_names`
    minis = [20] * 3
    maxis = [0] * 3
    minis_maxis_names = (([], []), ([], []), ([], []))
    for etu in etudiants:
        for i in range(3):
            minis[i] = min(minis[i], etu.notes[i])
            maxis[i] = max(maxis[i], etu.notes[i])
    for etu in etudiants:
        for i in range(3):
            if etu.notes[i] == minis[i]:
                minis_maxis_names[i][0].append(f"{etu.prenom}{etu.nom}")
            if etu.notes[i] == maxis[i]:
                minis_maxis_names[i][1].append(f"{etu.prenom}{etu.nom}")
    return minis_maxis_names


def recupere_classement(etudiants):
    """Renvoie les étudiants classés selon leur moyenne.

    Calcule la moyenne des trois examens de chaque étudiant.
    Les coefficients sont 1, 1 et 2 pour les épreuves 0, 1 et 2 respectivement.

    Renvoie une `list` de tuples à deux éléments contenant le nom et la moyenne de l'étudiant.
    Cette liste est triée de la moins bonne à la meilleure moyenne.
    """
    # TODO 6 : implémentez la fonction `recupere_classement`
    moyenne_gen = (
        (e.nom, sum(n * c for n, c in zip(e.notes, (1, 1, 2))) / 4) for e in etudiants
    )
    return sorted(moyenne_gen, key=lambda e: e[1])


def genere_histogramme(etudiants, num_exam):
    """Génère un histogramme des notes en SVG.

    Prend un `iterable` sur les etudiants et le numéro d'un examen.
    Génère un histogramme en baton des notes en svg a l'aide de rectangles.
    Renvoie le nom du fichier généré.
    """
    # TODO 7 : implémentez la fonction `genere_histogramme`
    distribution = [0] * 21
    for etudiant in etudiants:
        distribution[etudiant.notes[num_exam]] += 1
    hauteur = (max(distribution) + 1) * 20
    nom_fichier = f"distribution_{num_exam}.svg"
    with open(nom_fichier, "w", encoding="utf_8") as svg:
        print(f"<svg width='420' height='{hauteur}'>", file=svg)
        print(f"<rect width='420' height='{hauteur}' fill='white'/>", file=svg)
        for note, occurrence in enumerate(distribution):
            if occurrence != 0:
                print(
                    f"<rect width='20' height='{occurrence * 20}' "
                    f"x='{note * 20}' y='{hauteur - (occurrence + 1) * 20}' "
                    f"fill='red'/>",
                    file=svg,
                )
            print(
                f"<text x='{note * 20}' y='{hauteur}'>{note}</text>",
                file=svg,
            )
        print("</svg>", file=svg)
    return nom_fichier


def teste():
    """Teste toutes les fonctions ci-dessus."""
    etudiants = lit_fichier("notes.csv")
    etudiants_gen = lit_fichier_gen("notes.csv")
    etudiants_gen_list = list(etudiants_gen)
    print(f"{etudiants[4] = }, {etudiants_gen_list[4] = }")
    assert etudiants == etudiants_gen_list
    print("moyennes de la promo =", calcule_moyennes(etudiants))
    print("étudiants avec au moins un 20 =", recupere_etudiants_avec_un_20(etudiants))
    print("minis_maxis_names =", recupere_minis_maxis_names(etudiants))
    print("classement =", recupere_classement(etudiants))
    genere_histogramme(lit_fichier_gen("notes.csv"), 0)
    genere_histogramme(lit_fichier_gen("notes.csv"), 1)
    genere_histogramme(lit_fichier_gen("notes.csv"), 2)


# TODO 0 : appelez la fonction teste si le programme
# est invoqué en tant que programme principal
if __name__ == "__main__":
    teste()