Les tables de données

Vous retrouverez >ICI< le fichier regroupant l’ensemble des fonctions abordées dans ce chapitre.

Indexation d’une table

Une table ?

Le tableau précédent doit vous rappeler des souvenirs, non ? On y retrouve les pokemons utilisés dans le (4.1).

En fait, puisque les pokemons partagent les mêmes clés. Il semble logique de pouvoir les regrouper dans un tableau.

Sachant qu’un tableau peut être représenté par une liste, il vient que le tableau ci-dessus peut être représenté par la liste suivante où chaque ligne est un dictionnaire qui décrit un pokemon.

pokemon1 = {'Id': 129, 'Name': 'Magikarp','HP': 20, 'DEF': 55, 'SP.DEF': 20,
           'Type1': 'Water', 'Type2': ''}
pokemon2 = {'Id': 130, 'Name': 'Gyarados', 'HP': 95, 'DEF': 79, 'SP.DEF': 100,
            'Type1': 'Water', 'Type2': 'Flying'}
tableau1 = [pokemon1, pokemon2]

ou plus directement,

tableau1 = [{'Id': 129, 'Name': 'Magikarp', 'HP': 20, 'DEF': 55, 'SP.DEF': 20,
             'Type1': 'Water', 'Type2': ''}, {'Id': 130, 'Name': 'Gyarados',
                                              'HP': 95, 'DEF': 79, 'SP.DEF':
                                              100, 'Type1': 'Water', 'Type2':
                                              'Flying'}]

À vous de tester

Écrire la représentation en Python de ce second tableau de la même façon que précédemment.

Vérification ?

… de données ?

On reconnaît ici le tableau précédent ouvert dans un tableur.

Le format couramment utilisé est le csv (Comma Separated Values autrement dit valeurs séparées par une virgule).

Voyons de plus près le contenu du fichier source tableau1.csv avec un éditeur de texte (le Bloc Note par exemple si vous êtes sur Windows).

Vous devriez pouvoir y lire ce qui suit.
"Id","Name","HP","DEF","SP.DEF","Type1","Type2"
129,"Magikarp",20,55,20,"Water",
130,"Gyarados",95,79,100,"Water","Flying"

Sur la première ligne, on y trouve les clés. Sur les suivantes, l’ensemble des valeurs. Les données sont séparées par un séparateur, d’où le nom du format. Les séparateurs peuvent être une tabulation, une virgule (ce qui est le cas ici), un point-virgule, …

Remarque: notez la présence de "" autour des chaînes de caractère et pas autour des nombres. Par la suite, il faudra en tenir compte notamment lorsqu’on voudra trier dans un certain ordre les valeurs d’une colonne. Pour rappel, 20<100 (comparaison de deux entiers), mais que "100"<"20" (la comparaison de deux chaînes de caractères se fait caractère par caractère en commençant à gauche).

À vous de tester

Écrire à la main le possible contenu du fichier source tableau2.csv qui, ouvert avec un tableur, donne cette image.

Vérification ?

Importer une table en Python depuis un fichier csv ?

Cela va faire intervenir:

  • la lecture d’un fichier (fait dans le (1.5)),

  • les méthodes des chaînes de caractères:

    • rstrip() qui retourne la chaîne de caractères éliminée des espaces éventuellement présents en fin de lign);
    • split() qui découpe cette chaîne de caractères suivant le séparateur précisé pour retourner une liste.

Sans plus attendre, voici le code en Python:

def importer_csv(source, separateur=','):
    """
    Importer un fichier dans un tableau
    :param source: (str) lien pointant vers le fichier en entrée
    :param separateur: (str) choix du séparateur
    :param table: (list of dict) tableau créé en conservant le type des
    données présentes
    """
    table = []
    with open(source, 'r') as s:
        cles = s.readline().rstrip().split(separateur)
        ligne = s.readline().rstrip().split(separateur)
        while ligne != ['']:
            dico = {}
            for i in range(len(cles)):
                if ligne[i] != '':
                    dico[eval(cles[i])] = eval(ligne[i])
                else:
                    dico[eval(cles[i])] = ''
            table.append(dico)
            ligne = s.readline().rstrip().split(separateur)
    return table
Autre méthode (avec construction de dictionnaires par compréhension)

Exporter une table en Python vers un fichier csv ?

Là encore, on utilise des méthodes des chaînes de caractères faisant intervenir inévitablement les listes.

Ci-dessous le code Python pour réaliser cette exportation:

def mettre_en_forme(liste):
    """
    Mise en forme de la liste pour écriture dans le fichier csv
    de sorte que conserver la nature des données lors de l'exportation
    d'un tableau vers un fichier csv
    :param liste: (list)
    :return nlle_liste: (list)
    
    >>> mettre_en_forme(['abc', 12])
    ['"abc"', 12]
    """
    nlle_liste = []
    for element in liste:
        if isinstance(element, str):
            element = '\"' + str(element) + '\"'
        else:
            element = str(element)
        nlle_liste.append(element)
    return nlle_liste

def exporter_csv(table, destination='table.csv', mode='w', separateur=','):
    """
    Exporter un tableau en deux dimensions dans un fichier csv (tableur)
    :param table: (list of dict) tableau donné
    :param destination: (str) lien pointant vers le fichier de sortie
    :param mode: (str) 'w' en écriture ou 'a' en écriture par ajout
    :param separateur: (str) choix du séparateur
    :effet de bord: modifie le fichier de destination
    """
    cles = list(table[0].keys())  # liste des clés utilisées
    entete = mettre_en_forme(cles)
    with open(destination, mode) as dest:
        dest.write(separateur.join(entete)+'\n')  # écriture de la première
                                                  # ligne (entête des colonnes)
        for ligne in table:  # écriture des lignes suivantes dans le tableau:
                             # suite des valeurs
            lig = mettre_en_forme(ligne.values())
            dest.write(separateur.join(lig)+'\n')

À vous de tester

a) Importer à l’aide de la fonction importer_csv() le fichier csv tableau1.csv obtenu >PRÉCÉDEMMENT< et comparer le tableau ainsi créé au tableau1 donné au tout >DÉBUT< de la présente page.
b) Exporter à l’aide de la fonction exporter_csv() la table tableau2 donnée >ICI< vers tableau2.csv, puis vérifier son contenu à la réponse obtenue >LÀ<.

Quelques requêtes

Revenons aux pokemons.

Les tableaux donnés jusque là sont de taille extrêmement modeste en comparaison aux tableaux de données réels tant en nombre de lignes (enregistrements/objets) qu’en nombre de colonnes (champs identifiés par leurs clés).

Dans ce cas, si on dispose de tels tableaux, on pourrait souhaiter:

  • conserver seulement certaines colonnes;

  • sélectionner certaines lignes vérifiant certains critères sur les valeurs présentes;

  • trier les lignes suivant une colonne;

  • fabriquer un nouveau tableau en effectuant un jointure de deux tableaux ayant une colonne commune.

Toutes ces actions sont appelées requêtes.

Comment conserver certaines colonnes ?

À faire

Tableau 1

a) Écrire sur papier le nouveau tableau obtenu à partir du tableau 1 après conservation des colonnes ayant pour clés 'Name', 'HP', 'Type1' et 'Type2'.
b) Écrire sur papier sa représentation en Python.

Vérification ?

Vous avez compris qu’il suffit de recopier le tableau en filtrant les clés suivant si elles sont ou non dans ['Name', 'HP', 'Type1', 'Type2'].

Procédons comme suit:

Le filtre (ici un prédicat) utilisé pour répondre au critère souhaité :

def filtre_4col(cle):
    """
    Retourne True si la clé donnée est dans la liste donnée
    :param cle: (type non mutable)
    :param liste: (list) liste de clés
    :return: (bool) True si la clé appartient à la liste donnée
    """
    return cle in ['Name', 'HP', 'Type1', 'Type2']
Autre méthode (définition sur une ligne)

La fonction qui construit le nouveau tableau à partir d’un tableau d’origine et du filtre choisi :

def conserver(table, filtre):
    """
    Recréer un tableau en conservant les colonnes précisées
    :param table: (list of dict) un tableau à deux dimensions
    :param filtre: (function) retourne un booléen
    :return nlle_table: (list if dict) un tableau regroupant les colonnes
    désirées du tableau donné en entrée
    """
    nlle_table = []
    for ligne in table:
        nlle_ligne = {}
        for cle in ligne.keys():
            if filtre(cle):
                nlle_ligne[cle] = ligne[cle]
        nlle_table.append(nlle_ligne)
    return nlle_table
Autre méthode (avec construction de dictionnaires par compréhension)

Ne reste plus qu’à appliquer cela au tableau1.

>>> nv_tableau1 = conserver(tableau1, filtre_4col)
>>> nv_tableau1
[{'Name': 'Magikarp', 'HP': 20, 'Type1': 'Water', 'Type2': ''}, {'Name':
                                                                 'Gyarados',
                                                                 'HP': 95,
                                                                 'Type1':
                                                                 'Water',
                                                                 'Type2':
                                                                 'Flying'}]

Comment sélectionner certaines lignes ?

À faire

Tableau 2

a) Écrire sur papier le tableau obtenu à partir du tableau 2 après sélection des lignes vérifiant 'ATK' < 90 .
b) Écrire sur papier sa représentation en Python.

Vérification ?

Cette fois, on veut recopier le tableau en filtrant les lignes suivant si elles vérifient ou non ligne['ATK'] < 90.

À faire

En vous inspirant des fonctions précédentes, proposer:
a) un prédicat filtre() qui permet de savoir si la ligne donnée en paramètre sera sélectionnée ou non ;
b) une fonction selectionner() qui prend en paramètres la table donnée et le prédicat filtre à notre disposition et qui retourne une nlle_table comprenant uniquement les lignes de table passant le filtre.
c) une vérification du nouveau tableau précédent.

Vérification du filtre ?
Vérification de la fonction de sélection ?
Application au tableau 2

À faire en complément

a) Proposer un prédicat doublons() qui prend en paramètre une table et qui retourne True si la table présente plusieurs lignes identiques, False sinon.
Jeu de tests:

>>> table = [{'a': 1, 'b': 3}, {'a': 1, 'b': 4}, {'a': 1, 'b': 3}, {'a': 2, 'b': 2}]
>>> doublons(table)
True

b) Écrire une fonction effacer_doublons() qui prend en paramètre une table et qui retourne une nouvelle table constituée des lignes distinctes de table.
Jeu de tests:

>>> table = [{'a': 1, 'b': 3}, {'a': 1, 'b': 4}, {'a': 1, 'b': 3}, {'a': 2, 'b': 2}]
>>> nlle_table = effacer_doublons(table)
>>> nlle_table
[{'a': 1, 'b': 3}, {'a': 1, 'b': 4}, {'a': 2, 'b': 2}]

c) Écrire une fonction effacer_finement() qui prend en paramètres une table et une cle et qui retourne une nouvelle table constituée des lignes dont les valeurs associée à cette clé sont toutes différentes.
Jeu de tests:

>>> table = [{'a': 1, 'b': 3}, {'a': 1, 'b': 4}, {'a': 2, 'b': 2}, {'a': 1, 'b': 3}]
>>> nlle_table = effacer_finement(table, 'a')
>>> nlle_table
[{'a': 1, 'b': 3}, {'a': 2, 'b': 2}]

Comment trier une table suivant une colonne ?

À faire

Tableau 2

Écrire sur papier le tableau obtenu à partir du tableau 2 après l’avoir trié dans l’ordre croissant de la vitesse 'SPD'.

Vérification ?

On pourrait faire appel aux fonctions tris étudiés dans le (3.4), mais Python fournit une fonction tri bien plus rapide sorted() qui admet 3 paramètres la table que l’on cherche à trier, le paramètre key (le filtre désignant sur quelle colonne s’effectue le tri) et le paramètre optionnel reverse (un booléen: True si vous souhaitez effectuer un tri dans l’ordre décroissant) et qui retourne la table triée désirée.

Le filtre a utilisé:

def filtre_ordre(ligne):
    """
    Retourne True si la clé donnée est dans la liste donnée
    :param ligne: (dict) dictionnaire décrivant l'objet que la ligne représente
    :return: (bool) True si la ligne vérifie le critère désiré
    """
    return ligne['SPD']
Autre méthode (définition sur une ligne)

Puis, la fonction principale

def trier(table, filtre, croissant=True):
    """
    Trier la table donné selon la clé choisie par le filtre
    :param table: (list of dict) un tableau à deux dimensions
    :param filtre: (function) retourne la valeur à considérer pour le tri

    Exemple: filtre = lambda ligne: ligne['HP']
    """
    return sorted(table, key=filtre, reverse=not(croissant))

Application au tableau2:

>>> nv_tableau2 = trier(tableau2, filtre_ordre, True)
>>> nv_tableau2
[{'Id': 131, 'ATK': 85, 'SP.ATK': 85, 'SPD': 60}, {'Id': 129, 'ATK': 10,
'SP.ATK': 15, 'SPD': 80}, {'Id': 130, 'ATK': 125, 'SP.ATK': 60, 'SPD': 81}]

Comment joindre deux tableaux suivant une colonne ?

À faire

À partir des 2 tableaux précédents, recopier et compléter le tableau suivant.

Vous l’aurez compris qu’en fait ces deux tableaux partagent une même clé 'Id'.

Il vient que créer le tableau souhaité consiste à créer une liste de dictionnaires dont les clés sont obtenues par la concaténations des clés des deux tableaux (sans répétition de la clé commune) et les valeurs sont celles associées dans chacun des tableaux (identiques pour la clé commune).

def joindre(table1, table2, cle1, cle2=None):
    """
    Faire la jonction entre table1 et table2 se faisant suivant les clés
     précisées dans chacune des tables
    :param table1: (list of dict) un tableau à deux dimensions
    :param table2: (list of dict) un tableau à deux dimensions
    :param cle1: (str) clé choisi dans la table1
    :param cle2: (str) clé choisi dans la table2

    Exemple: filtre = lambda ligne: ligne['HP']
    """
    nlle_table = []
    descripteurs1, descripteurs2 = table1[0].keys(), table2[0].keys()
    if cle2 == None:
        cle2 = cle1
    if cle1 in descripteurs1 and cle2 in descripteurs2:
        for ligne1 in table1:
            for ligne2 in table2:
                if ligne1[cle1] == ligne2[cle2]:
                    nlle_ligne = dict(list(ligne1.items()) + list(ligne2.items()))
                    nlle_table.append(nlle_ligne)
    return nlle_table
Autre méthode (usage de la méthode update() des dictionnaires)

Application aux tableau1 et tableau2 suivant la clé 'Id':

>>> tableau3 = joindre(tableau1, tableau2, 'Id')
>>> tableau3
[{'Id': 129, 'Name': 'Magikarp', 'HP': 20, 'DEF': 55, 'SP.DEF': 20, 'Type1':
'Water', 'Type2': '', 'ATK': 10, 'SP.ATK': 15, 'SPD': 80}, {'Id': 130, 'Name':
'Gyarados', 'HP': 95, 'DEF': 79, 'SP.DEF': 100, 'Type1': 'Water', 'Type2':
'Flying', 'ATK': 125, 'SP.ATK': 60, 'SPD': 81}]