Adloun

Traiter des données en tables

Cours complet · NSI (première), chapitre 6 · première, spécialité numérique et sciences informatiques

Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre

Les deux chapitres précédents ont donné deux structures. En les combinant, on obtient celle qui porte la quasi-totalité des données du monde réel : un tableau de dictionnaires partageant les mêmes clés. Le programme le dit ainsi — « les données organisées en table correspondent à une liste de p-uplets nommés qui partagent les mêmes descripteurs ».

Résultats sportifs, relevés météo, catalogues, horaires de transport, données ouvertes des administrations : tout cela arrive en tables, et le plus souvent dans un fichier CSV.

Hors programme : Ce n'est pas encore une base de données

Le programme est net : cette rubrique « permet de préparer les élèves à aborder la notion de base de données qui ne sera présentée qu'en classe terminale », et il s'agit de travailler « dans un langage de programmation ordinaire et non dans un système de gestion de bases de données ». Aucun SQL ici, aucun SGBD : du Python, des tableaux et des dictionnaires.

6.1 Ce qu'est une table

Définition 6.1Table, descripteur, enregistrement

Une table est un tableau d'enregistrements qui possèdent tous les mêmes champs. Le nom d'un champ s'appelle un descripteur — c'est le titre de la colonne ; un enregistrement est une ligne.


eleves = [
    {"nom": "Nour",    "classe": "1G3", "moyenne": 15.5,  "option": "NSI"},
    {"nom": "Camille", "classe": "1G1", "moyenne": 12.0,  "option": "SVT"},
    {"nom": "Yanis",   "classe": "1G3", "moyenne": 9.5,   "option": "NSI"},
]
Figure : Anatomie d'une table. Chaque ligne est un enregistrement ; tous partagent les mêmes

descripteurs — c'est ce qui distingue une table d'un simple tableau de dictionnaires.</div>

Deux façons de désigner une case, et le programme les autorise toutes deux : le tableau de p-uplets nommés ci-dessus — eleves[0][&quot;moyenne&quot;] — ou le tableau doublement indexé du chapitre 4 — t[0][2]. La première est préférable dès que les colonnes ont un sens : [&quot;moyenne&quot;] reste juste si l'on insère une colonne, [2] devient faux en silence. C'est l'argument du chapitre 4 sur les p-uplets nommés, appliqué à des milliers de lignes.

6.2 Importer une table

Capacité attendue

« Importer une table depuis un fichier texte tabulé ou un fichier CSV. »

Définition 6.2CSV

Un fichier CSV (comma-separated values) est un fichier texte où chaque ligne est un enregistrement et où les champs sont séparés par un caractère convenu — le plus souvent la virgule, parfois le point-virgule ou une tabulation. La première ligne porte généralement les descripteurs.


nom,classe,moyenne,option
Nour,1G3,15.5,NSI
Camille,1G1,12.0,SVT
Yanis,1G3,9.5,NSI
Sofia,1G2,17.25,NSI
Adam,1G1,13.0,SES

import csv

def importer(chemin, separateur=","):
    """Table lue depuis un fichier CSV à ligne d'en-tête.

    Précondition  : le fichier existe, est encodé en UTF-8, et sa première
                    ligne porte les descripteurs.
    Postcondition : toutes les lignes renvoyées ont exactement les mêmes clés.
    """
    with open(chemin, "r", encoding="utf-8", newline="") as f:
        table = [dict(ligne) for ligne in csv.DictReader(f, delimiter=separateur)]
    if len(table) > 0:
        descripteurs = set(table[0].keys())
        assert all(set(l.keys()) == descripteurs for l in table), "lignes heterogenes"
    return table
ImportantTout ce qui sort d'un CSV est du texte

C'est le piège numéro un du chapitre. Un fichier texte ne contient que des caractères : après l'import, ligne[&quot;moyenne&quot;] vaut la chaîne &quot;15.5&quot;, pas le nombre . Les conséquences sont silencieuses et fausses :


"9.5" > "15.5"          # True  <- chaines : '9' vient apres '1'
9.5 > 15.5              # False <- la verite

Une table triée par moyenne sans conversion place donc les mauvais élèves en tête, sans le moindre message d'erreur. Convertir les colonnes numériques fait partie de l'import, pas des finitions.


def convertir(table, colonne, fonction):
    """Applique fonction à une colonne de la table, sur place.

    Précondition : la colonne existe dans toutes les lignes et toutes ses
                   valeurs sont acceptables par fonction.
    """
    assert all(colonne in ligne for ligne in table), "colonne absente"
    for ligne in table:
        ligne[colonne] = fonction(ligne[colonne])


eleves = importer("eleves.csv")
convertir(eleves, "moyenne", float)      # à faire AVANT tout tri, toute comparaison
AttentionTrois autres pièges du CSV
  • L'encodage. Le fichier ne dit pas le sien (chapitre 2). Un CSV produit par un tableur français est souvent en latin-1 ou cp1252, et le lire en UTF-8 échoue ou abîme les accents.
  • Le séparateur. Sur un système configuré en français, un tableur écrit volontiers des points-virgules, parce que la virgule y sert de séparateur décimal. D'où le paramètre separateur.
  • Les champs contenant le séparateur. Une adresse comme &quot;12, rue des Lilas&quot; contient une virgule ; le format l'entoure alors de guillemets. Découper naïvement sur les virgules avec ligne.split(&quot;,&quot;) casse sur ces lignes-là — c'est exactement ce que le module csv sait faire et qu'il ne faut pas réécrire.

Repère historique : Des cartes perforées aux données ouvertes

L'idée de traiter mécaniquement des données en tables précède l'ordinateur d'un demi-siècle. Pour le recensement américain de 1890, Herman Hollerith met au point une machine à cartes perforées : une carte par personne, une position par caractéristique. Le dépouillement, qui avait pris près de huit ans en 1880, est ramené à quelques mois. L'entreprise fondée par Hollerith deviendra IBM.

La formalisation moderne, elle, date de 1970, quand Edgar Codd propose le modèle relationnel — les données comme un ensemble de tables, interrogeables par des opérations mathématiques. C'est ce modèle qui fonde les bases de données que vous étudierez en terminale ; les quatre opérations de ce chapitre — sélectionner, trier, dédoublonner, fusionner — en sont les ancêtres directs, écrits à la main.

6.3 Rechercher dans une table

Capacité attendue

« Rechercher les lignes d'une table vérifiant des critères exprimés en logique propositionnelle. »

« Logique propositionnelle » désigne ici les opérateurs booléens du chapitre 2 : and, or, not, combinés en une condition. La recherche s'écrit alors en une ligne : on garde les enregistrements qui vérifient la condition.


def selection(table, critere):
    """Lignes de la table qui vérifient le critère.

    critere est une FONCTION prenant une ligne et renvoyant un booléen.
    Postcondition : le résultat est un sous-ensemble de la table, dans le
                    même ordre, et sa longueur est au plus celle de la table.
    """
    return [ligne for ligne in table if critere(ligne)]


def bons_nsi(ligne):
    return ligne["option"] == "NSI" and ligne["moyenne"] >= 14

selection(eleves, bons_nsi)        # Nour et Sofia

Méthode : Écrire un critère composé

Traduire l'énoncé mot à mot, en gardant les parenthèses de la logique :

En françaisEn Python
en NSI et au-dessus de 14`l["option"] == "NSI" and l["moyenne"] >= 14`
en 1G1 ou en 1G3`l["classe"] == "1G1" or l["classe"] == "1G3"`
pas en NSI`not (l["option"] == "NSI")`

Rappel du chapitre 2 : and et or s'évaluent en court-circuit. Placer à gauche le test qui protège celui de droite — utile si une colonne peut manquer.

6.4 Doublons et cohérence

Le programme demande explicitement que « la recherche de doublons » et « les tests de cohérence d'une table » soient présentés. Une table importée d'ailleurs est rarement propre.


def doublons(table, colonne):
    """Valeurs de la colonne qui apparaissent plus d'une fois."""
    effectifs = {}
    for ligne in table:
        v = ligne[colonne]
        effectifs[v] = effectifs.get(v, 0) + 1
    return [v for v, n in effectifs.items() if n > 1]

C'est le compteur du chapitre 5, appliqué à une colonne. Chercher les doublons en comparant chaque ligne à toutes les autres coûterait comparaisons ; le dictionnaire ramène le travail à un seul parcours — un premier aperçu de ce que le chapitre 7 appellera le coût d'un algorithme.

Définition 6.3Domaine de valeurs

Le domaine d'une colonne est l'ensemble des valeurs qu'elle a le droit de prendre. Une moyenne appartient à , une classe à une liste connue, un mois aux entiers de 1 à 12.


def hors_domaine(table, colonne, valide):
    """Lignes dont la valeur de colonne sort du domaine.

    valide est une fonction qui dit si une valeur est acceptable.
    """
    return [ligne for ligne in table if not valide(ligne[colonne])]


def moyenne_valide(x):
    return isinstance(x, float) and 0.0 <= x <= 20.0

hors_domaine(eleves, "moyenne", moyenne_valide)     # [] si la table est saine
Important

Vérifier le domaine attrape ce qu'aucune autre vérification ne voit : une moyenne de due à un point décimal manquant, une classe écrite &quot;1g3&quot; au lieu de &quot;1G3&quot;, une colonne restée en texte parce qu'on a oublié de la convertir. Ces défauts ne font jamais planter le programme — ils faussent le résultat. Contrôler la table à l'import coûte cinq lignes ; retrouver l'erreur plus tard en coûte cent.

6.5 Trier une table

Capacité attendue

« Trier une table suivant une colonne. »

Un point mérite d'être vu plutôt que lu : trier une table ne trie pas une colonne, il réordonne des lignes — chaque enregistrement reste d'un bloc.

Figure : Trier sur la colonne `annee`. Les lignes changent de place entières : un enregistrement ne se disloque jamais.

Le programme autorise ici l'emploi d'une fonction toute faite : « une fonction de tri intégrée au système ou à une bibliothèque peut être utilisée ». Les algorithmes de tri seront écrits à la main au chapitre 7 ; ici, on s'en sert.


def cle_moyenne(ligne):
    return ligne["moyenne"]

classement = sorted(eleves, key=cle_moyenne, reverse=True)

sorted renvoie une nouvelle table et laisse l'originale intacte — ce qui est presque toujours ce qu'on veut. Le paramètre key reçoit une fonction qui, pour chaque ligne, donne la valeur sur laquelle comparer.

AttentionTrier une colonne restée en texte

Reprenons le piège de l'import. Sans convertir(eleves, &quot;moyenne&quot;, float), le tri compare des chaînes :


"9.5", "17.25", "15.5", "13.0", "12.0"     <- ordre des CHAÎNES
17.25, 15.5, 13.0, 12.0, 9.5               <- l'ordre attendu

La table est bien triée — dans un ordre qui n'a aucun sens. Aucune erreur, aucun avertissement.

iRemarqueTrier selon deux colonnes

Pour trier par classe, puis par moyenne à l'intérieur de chaque classe, il suffit que la fonction clé renvoie un p-uplet : les p-uplets se comparent composante par composante, de gauche à droite.


def cle_classe_puis_moyenne(ligne):
    return (ligne["classe"], -ligne["moyenne"])

sorted(eleves, key=cle_classe_puis_moyenne)

Le signe moins inverse l'ordre sur la seule moyenne, ce que reverse=True n'aurait pas permis puisqu'il inverse tout. Encore un service rendu par le p-uplet du chapitre 4.

6.6 Fusionner deux tables

Capacité attendue

« Construire une nouvelle table en combinant les données de deux tables. »

Deux tables se combinent quand elles partagent une colonne — la clé de rapprochement. Ici, une table d'élèves et une table de classes, reliées par la colonne classe.


classe,professeur,salle
1G1,Benali,204
1G2,Roux,112
1G3,Diallo,305

def fusion(table1, table2, cle):
    """Table combinant les lignes des deux tables qui partagent la même clé.

    Précondition  : la colonne cle existe dans les deux tables, et ses valeurs
                    sont uniques dans table2.
    Postcondition : le résultat a au plus autant de lignes que table1 ; chaque
                    ligne porte les descripteurs des deux tables.
    """
    assert all(cle in l for l in table1) and all(cle in l for l in table2)
    assert len(doublons(table2, cle)) == 0, "cle non unique dans table2"

    index = {ligne[cle]: ligne for ligne in table2}     # accès direct, chapitre 5
    resultat = []
    for ligne in table1:
        if ligne[cle] in index:
            combinee = dict(ligne)
            combinee.update(index[ligne[cle]])
            resultat.append(combinee)
    assert len(resultat) <= len(table1)
    return resultat
Figure : La fusion rapproche les lignes par l'égalité des valeurs de la clé. C'est pourquoi

le domaine de valeurs décide du succès de l'opération.</div>

ImportantC'est le domaine de valeurs qui décide du succès

Le rapprochement se fait sur l'égalité des valeurs de la clé. Il suffit donc d'un écart d'écriture — &quot;1G3&quot; d'un côté, &quot;1g3&quot; ou &quot; 1G3&quot; de l'autre — pour que la ligne disparaisse de la fusion. Sans erreur, sans avertissement : le résultat est simplement plus court.

C'est pourquoi le programme demande que « la notion de domaine de valeurs soit mise en évidence » précisément à propos de la fusion. Toujours comparer le nombre de lignes obtenu à celui attendu — c'est le contrôle qui coûte une ligne et détecte le désastre.


complete = fusion(eleves, classes, "classe")
if len(complete) < len(eleves):
    print("Attention :", len(eleves) - len(complete), "ligne(s) sans correspondance")

Piste de projet : Exploiter des données ouvertes

Choisir un jeu de données ouvert réel — qualité de l'air, fréquentation des transports, résultats électoraux, catalogue d'une médiathèque — et en faire une petite étude : import, contrôle de cohérence et de domaine, recherches par critères composés, tri, fusion avec une seconde table, et quelques conclusions présentées à la classe.

L'intérêt tient à ce que les données réelles sont sales : accents mal encodés, colonnes vides, doublons, valeurs aberrantes. Le rapport doit dire combien de lignes ont été écartées et pourquoi — c'est cette honnêteté-là qui distingue une étude d'une illustration.

Continuer sur Adloun : animation, QCM, fiches, exercices