Le but de cette activité est de manipuler les fichiers CSV.
De nos jours, une des utilisations de l'informatique est de pouvoir stocker et traiter un grand nombre de données, par exemple les produits disponibles, les commandes et comptes clients dans un site marchand, les données médicales des patients dans un hôpital, ... Pour cela il faut organiser le stockage des données afin de pouvoir accéder facilement à des informations précises.
Même s'il existe de logiciels dédiés pour le traitement de données, il est assez facile en Python de mettre en oeuvre des opérations de base : chargement de données structurées, recherche d'une donnée particulière, suppression des doublons, fusion de données.
Par exemple, un tableau contenant le nom des élèves de la classe avec les notes obtenues à chacun des devoirs est un exemple de données structurées : on peut retrouver facilement la note d'un élève donné à un devoir donné en sélectionnant la ligne et la colonne correspondante.
On parle d'indexation de tables quand on crée une structure de données à partir de données en table.
En base de données, un index est une structure de données auxiliaire permettant un accès rapide aux données.
Dans cet exercice, nous allons voir comment indexer les données issues d'un fichier CSV de façon pratique et efficace.
Les fichiers CSV sont des fichiers texte. Pour charger les données d'un fichier CSV nous pouvons utiliser les méthodes classiques de lecture de fichier que nous avons vu au chapitre 8 en utilisant la méthode split
notamment. Cependant, il nous faudra alors charger les données, puis découper chaque ligne pour récupérer les attributs de chaque entrée, c'est à dire créer des fonctions dédiées à la forme de la table chargée.
Heureusement le langage Python propose un module csv
qui va largement nous faciliter la tâche : il contient notamment des fonctions utilitaires pour lire et écrire des fichiers CSV.
Les instructions suivantes permettent de charger le module csv
, d'ouvrir le fichier CSV dans une variable fichier
et d'en récupérer les données sous la forme d'une liste de listes dans la variable table
:
import csv
fichier = open('Inventeurs.csv', 'r', encoding = 'utf-8')
table = list(csv.reader(fichier))
La variable table
contient alors une liste de listes de chaînes de caractères comme on peut le voir en exécutant la cellule suivante :
print(table)
table[0]
?américain = []
for i in range(1, 11):
if table[i][2] == 'Etats-Unis':
américain.append(table[i][1])
américain # permet d'afficher le contenu de la liste
Est-ce pratique ?
table[2][3]
? Est approprié ? Comment remédier à cela ?Ecrire la réponse ici
On constate que la structure de données de listes de listes n'est pas idéale puisque la première liste de table
contient le nom des champs et qu'il faudra donc l'ignorer dans le traitement des données mais qu'il faudra en récupérer les informations pour connaître la correspondance entre le nom des attributs et leur position dans les sous-listes.
Une autre solution consiste alors en le choix d'un autre type de structure de données : une liste de dictionnaires partageant les mêmes clés. Pour cela, le module csv
dispose d'une fonction nommée DictReader
que l'on l'utilise de la façon suivante :
import csv
fichier = open('Inventeurs.csv', 'r', encoding = 'utf-8')
table = list(csv.DictReader(fichier))
Remarque : lorsque le séparateur des données du fichier CSV n'est plus la virgule mais, par exemple, le point-virgule, on précise un paramètre supplémentaire nommé delimiter
dans la fonction DictReader
. Par exemple, dans le cas du point-virgule, on écrira
table = list(csv.DictReader(fichier, delimiter = ';'))
Avec cette méthode, la variable table
contient cette fois une liste de dictionnaires comme on peut le voir en exécutant la cellule suivante :
print(table)
# Ecrire l'instruction ici
table[0]['Naissance']
Il peut être utile que les dates soient stockées sous forme d'entier et donc que le type de ces données soit int
. Il faut donc, dans la plupart des cas, écrire une fonction de validation (ou une procédure si la fonction ne renvoie rien) qui transtype les données obtenues sous forme de chaînes de caractères dans le type adéquat.
None
.def validation_table(table_données):
'''
table_données est une liste de dictionnaires obtenue à partir d'un
fichier CSV formaté comme le fichier Inventeurs.csv
'''
for i in range(len(table_données)):
table_données[i]['Naissance'] = int(table_données[i]['Naissance'])
if table_données[i]['Mort'] != "":
table_données[i]['Mort'] = int(table_données[i]['Mort'])
else:
table_données[i]['Mort'] = None
return table_données
La cellule suivante affiche la table des inventeurs après validation des données afin de vérifier que le résultat obtenu est cohérent avec ce que l'on désire.
table = validation_table(table)
table
Il peut également être utile de vérifier la cohérence des données au moment de cette étape de validation. Par exemple, on pourrait également vouloir vérifier que l'année de mort n'est pas inférieure à l'année de naissance pour éviter des erreurs futures, ou bien que l'on ne stocke pas deux fois la même information, autrement dit qu'il n'y a pas deux dictionnaires identiques dans la liste table
(on parle alors de suppression de doublons). Les modifications à réaliser à l'étape de validation vont donc dépendre du type de données que l'on manipule et de comment on veut les traiter. Il faudra pour cela avoir connaissance du type unique de chacun des champs et des traitements que l'on voudra réaliser.
Le module csv
permet de sauvegarder dans un fichier des données issues d'une liste de dictionnaires préalablement créée. Pour cela on utilise les instructions suivantes où la variable nom_fichier
est une chaîne de caractères donnant le nom du fichier à créer et la variable liste_champs
contient la liste des champs de la table table
(liste de dictionnaires) considérée:
with open(nom_fichier,'w', encoding = 'utf-8') as fichiercsv:
liste_champs = table[0].keys()
writer = csv.DictWriter(fichiercsv, liste_champs)
writer.writeheader()
writer.writerows(table)
Noter que l'on récupère ici la liste des champs avec la méthode keys
de façon à éviter des éventuelles erreurs de saisie.
Spécialités.csv
à l'aide de la méthode décrite ci-dessus. L'ouvrir ensuite pour vérifier qu'il a été correctement créé.# Ecrire les intructions de création ici
liste = [{'Nom': 'HOARAU', 'Prénom': 'Maëlys', 'Spé1': 'Maths', 'Spé2': 'Physique-chimie'}, {'Nom': 'LECONTE', 'Prénom': 'Jérémy', 'Spé1': 'SVT', 'Spé2': 'NSI'}, {'Nom': 'ALDEBARAN', 'Prénom': 'Vincent', 'Spé1': 'HGGSP', 'Spé2': 'NSI'}]
with open('Spécialités.csv','w', encoding = 'utf-8') as fichiercsv:
liste_champs = liste[0].keys()
writer = csv.DictWriter(fichiercsv, liste_champs)
writer.writeheader()
writer.writerows(liste)
# Ecrire les intructions de vérification ici
fichier = open('Spécialités.csv', 'r', encoding = 'utf-8')
liste2 = list(csv.reader(fichier))
print(liste2)
Maintenant que nous savons comment récupérer des données en table à partir d'un fichier CSV, nous allons voir comment exploiter ces données. On va pouvoir extraire des données particulières, tester la présence de certaines données, faire des statistiques, etc. Ces opérations de manipulation de données sont appelées des requêtes.
En général on ne recherchera pas une ligne particulière mais plutôt une donnée associée à la valeur d'un attribut. Par exemple, on pourrait vouloir rechercher si un inventeur du nom de Pascal
est présent dans la table Inventeurs.csv
et ensuite obtenir éventuellement des informations liées à cette entrée.
On reprend ici la fichier Inventeurs.csv
. Exécuter la cellule suivante pour récupérer ses données dans table (donnée ici sous la forme d'une liste de dictionnaires)
import csv
with open('Inventeurs.csv','r', encoding = 'utf-8') as fichier:
table = list(csv.DictReader(fichier))
table = validation_table(table)
table
def appartient(nom, table_données):
'''
nom est une chaîne de caractères
table_données est une liste de dictionnaires obtenue à partir d'un
fichier CSV formaté comme le fichier Inventeurs.csv
Renvoie True si l'inventeur nom est dans table_données
'''
for inventeur in table_données:
if inventeur['Nom'] == nom:
return True
return False
Les deux cellules ci-dessous permettent de tester votre code. Elles doivent toutes renvoyer True
lors de leur exécution.
appartient('Turing', table) == True
appartient('Mozart', table) == False
pays
renvoyant le pays d'origine d'un inventeur dont le nom nom
est passé en argument. Par exemple, l'instruction pays('Bell', table)
renvoie Ecosse
.def pays(nom, table_données):
# code à compléter
for inventeur in table_données:
if inventeur['Nom'] == nom:
return inventeur['Pays']
Les trois cellules ci-dessous permettent de tester votre code. Elles doivent toutes renvoyer True
lors de leur exécution.
pays('Bell', table) == 'Ecosse'
pays('Turing', table) == 'Angleterre'
pays('Mozart', table) == None
Remarque : il se peut que plusieurs entrées aient le même attribut (par exemple le même nom). Dans ce cas on peut affiner sa recherche en donnant d'autres attributs pour identifier une entrée particulière. Il suffira alors d'ajouter des paramètres à la fonction.
Quelquefois on désire récupérer des données issues de plusieurs entrées. On parle alors d'agrégation de données. On peut par exemple compter le nombre de lignes réalisant une certaine condition et on pourra alors produire des statistiques sur ces données.
Par exemple, si on veut compter tous les inventeurs nés en 1955, on procèdera de l'une des façons suivantes :
def inventeurs_nes_en(annee, table_données):
nb = 0
for inventeur in table_données:
if inventeur['Naissance'] == annee:
nb += 1
return nb
ou
def inventeurs_nes_en(annee, table_données):
return len([inventeur for inventeur in table_données if inventeur['Naissance'] == annee])
L'appel inventeurs_nes_en(1955, table)
renvoie alors 3
.
inventeurs_pays
renvoyant le nombre d'inventeurs d'un même pays pays
dans la table table
. Par exemple, l'instruction inventeurs_pays('Angleterre', table)
renvoie 3
. def inventeurs_pays(pays, table_donnees):
# code à compléter
return len([inventeur for inventeur in table_donnees if inventeur['Pays'] == pays])
Les trois cellules ci-dessous permettent de tester votre code. Elles doivent toutes renvoyer True
lors de leur exécution.
inventeurs_pays('Angleterre', table) == 3
inventeurs_pays('Hongrie', table) == 1
inventeurs_pays('Allemagne', table) == 0
def age_moyen_vivant(table_données):
s_age = 0 # somme des âges des inventeurs
nb = 0 # accumulateur pour compter les inventeurs
for inventeur in table_données:
if inventeur['Mort'] == None: # l'inventeur est vivant
age = 2025 - inventeur['Naissance']
s_age += age
nb += 1
return s_age / nb # renvoie la moyenne
La cellule ci-dessous doit renvoyer True
lors de son exécution.
age_moyen_vivant(table) == 70.5
Jusqu'à présent nous avons extrait une seule information à partir d'une table de données : le résultat d'un test , la valeur d'un attribut, ou encore une valeur agrégée. L'opération appelée sélection consiste cette fois à extraire plusieurs lignes (ou entrées) vérifiant une condition.
Par exemple, pour extraire tous les inventeurs anglais de la table inventeurs.csv
, on peut procéder par compréhension de la façon suivante :
anglais = [inventeur for inventeur in table if inventeur['Pays] == 'Angleterre']
Lorsque le critère est plus complexe, on définit en général au préalable une fonction de test.
Remarque : pour procéder à la sélection, on peut également choisir de créer une liste vide et la remplir au fur et à mesure avec la méthode append
.
vivants = [inventeur for inventeur in table if inventeur['Mort'] == None]
La cellule ci-dessous doit renvoyer True
lors de son exécution.
vivants == [{'Nom': 'Berners-Lee', 'Prénom': 'Timothee', 'Pays': 'Angleterre', 'Naissance': 1955, 'Mort': None, 'Invention': 'HTML'},
{'Nom': 'Gates', 'Prénom': 'Bill', 'Pays': 'Etats-Unis', 'Naissance': 1955, 'Mort': None, 'Invention': 'Microsoft'},
{'Nom': 'Krivine', 'Prénom': 'Jean-Louis', 'Pays': 'France', 'Naissance': 1939, 'Mort': None, 'Invention': 'Lambda-calcul'}, {'Nom': 'Torvalds', 'Prénom': 'Linus', 'Pays': 'Finlande', 'Naissance': 1969, 'Mort': None, 'Invention': 'Linux'}]
Il se peut que l'on veuille construire une nouvelle table de données ne contenant que certaines colonnes. Par exemple, extraire uniquement les noms des inventeurs de la table d'inventeurs, ou encore, seulement leur nom et leur invention. On dit alors que l'on procède à une projection.
Par exemple, pour extraire les noms des inventeurs, on peut procéder comme suit :
noms = [inventeur['Nom'] for inventeur in table]
De même, pour extraire les noms des inventeurs et leur invention, on peut procéder comme suit :
noms_et_inventions = [{'Nom':inv['Nom'], 'Invention':inv['Invention']} for inv in table]
nouvelle_table = [{'Nom': inv['Nom'], 'Prénom': inv['Prénom'], 'Age': 2023 - inv['Naissance']} for inv in table if inv['Mort'] == None]
La cellule ci-dessous doit renvoyer True
lors de son exécution.
nouvelle_table == [{'Nom': 'Berners-Lee', 'Prénom': 'Timothee', 'Age': 68}, {'Nom': 'Gates', 'Prénom': 'Bill', 'Age': 68},
{'Nom': 'Krivine', 'Prénom': 'Jean-Louis', 'Age': 84},
{'Nom': 'Torvalds', 'Prénom': 'Linus', 'Age': 54}]
On peut vouloir trier une table suivant une colonne. Par exemple, on peut vouloir trier les inventeurs selon leur année de naissance. Cette manipulation est assez complexe puisqu'il s'agit de classer les entrées dans l'ordre croissant des valeurs d'un champ donné. Heureusement, Python propose la fonction sorted
qui peut être appliquée sur un dictionnaire en définissant au préalable une fonction renvoyant les valeurs que l'on peut comparer.
Par exemple, pour trier notre table d'inventeurs selon l'année de naissance, on commence par définir la fonction suivante renvoyant l'année de naissance d'un inventeur donné :
def naissance(inventeur):
return inventeur['Naissance']
On peut alors définir une nouvelle table dans laquelle les inventeurs de la table table
ont été trié par ordre croissant sur leur année de naissance.
tri_naissance = sorted(table, key = naissance)
Noter que l'on peut également trier les données par ordre décroissante en ajoutant le paramètre reverse = True
comme suit :
tri_naissance = sorted(table, key = naissance, reverse = True)
def invention(inventeur):
return inventeur["Invention"]
tri_invention = sorted(table, key = invention)
La cellule ci-dessous doit renvoyer True
lors de son exécution.
tri_invention == [{'Nom': 'Jobs', 'Prénom': 'Steve', 'Pays': 'Etats-Unis', 'Naissance': 1955, 'Mort': 2011, 'Invention': 'Apple'},
{'Nom': 'Berners-Lee', 'Prénom': 'Timothee', 'Pays': 'Angleterre', 'Naissance': 1955, 'Mort': None, 'Invention': 'HTML'},
{'Nom': 'Krivine', 'Prénom': 'Jean-Louis', 'Pays': 'France', 'Naissance': 1939, 'Mort': None, 'Invention': 'Lambda-calcul'},
{'Nom': 'Torvalds', 'Prénom': 'Linus', 'Pays': 'Finlande', 'Naissance': 1969, 'Mort': None, 'Invention': 'Linux'},
{'Nom': 'Gates', 'Prénom': 'Bill', 'Pays': 'Etats-Unis', 'Naissance': 1955, 'Mort': None, 'Invention': 'Microsoft'},
{'Nom': 'Dijkstra', 'Prénom': 'Edsger Wybe', 'Pays': 'Pays-Bas', 'Naissance': 1930, 'Mort': 2002, 'Invention': 'algorithme du plus court chemin'},
{'Nom': 'Boole', 'Prénom': 'George', 'Pays': 'Angleterre', 'Naissance': 1815, 'Mort': 1864, 'Invention': 'algèbre de Boole'},
{'Nom': 'Von Neumann', 'Prénom': 'John', 'Pays': 'Hongrie', 'Naissance': 1903, 'Mort': 1957, 'Invention': 'architecture des ordinateurs'},
{'Nom': 'Turing', 'Prénom': 'Alan', 'Pays': 'Angleterre', 'Naissance': 1912, 'Mort': 1954, 'Invention': 'intelligence artificielle'},
{'Nom': 'Hopper', 'Prénom': 'Grace', 'Pays': 'Etats-Unis', 'Naissance': 1906, 'Mort': 1992, 'Invention': 'premier compilateur et langage COBOL'},
{'Nom': 'Shannon', 'Prénom': 'Claude', 'Pays': 'Etats-Unis', 'Naissance': 1916, 'Mort': 2001, 'Invention': "théorie de l'information"},
{'Nom': 'Bell', 'Prénom': 'Alexander Graham', 'Pays': 'Ecosse', 'Naissance': 1847, 'Mort': 1922, 'Invention': 'téléphone'}]
Quand on travaille avec des données en table, il est fréquent d'avoir plusieurs jeux de données. Il alors souvent utile de fusionner toutes ces données en une seule table. Plusieurs cas sont possibles : les tables contiennent le même type d'information (par exemple les prénoms des enfants nés en 2007 et ceux des enfants nés en 2008 dans une même ville) ou bien elles contiennent des informations complémentaires (les notes obtenues à une épreuve associées à numéro de candidat, et l'identité des candidats associée à chaque numéro). Dans le premier cas on voudra réaliser une réunion de tables et dans le second à une opération de jointure.
Le langage Python permet de procéder très simplement grâce à l'opérateur de concaténation +
qui est compatible avec les dictionnaires. Il s'agit tout simplement de charger les jeux de données dans deux dictionnaires et de les concaténer.
Par exemple, on dispose des deux tables d'inventeurs (fichiers Inventeurs1.csv
et Inventeurs2.csv
) que l'on désire réunir en un seul fichier Inventeurs_complet.csv
.
import csv
fInv1 = open('Inventeurs1.csv', 'r', encoding = 'utf-8')
fInv2 = open('Inventeurs2.csv', 'r', encoding = 'utf-8')
tInv1 = list(csv.DictReader(fInv1, delimiter = ';'))
tInv2 = list(csv.DictReader(fInv2, delimiter = ';'))
print('tInv1',':',tInv1)
print('\n')
print('tInv2',':',tInv2)
tInvFus = tInv1 + tInv2
print('\n')
print('tInvFus',':',tInvFus)
Inventeurs_complet.csv
, puis l'ouvrir pour vérifier qu'il a été correctement créé.# Ecrire les intructions ici
with open("Inventeurs_complet.csv", 'w', encoding = 'utf8') as fichier:
writer = csv.DictWriter(fichier, tInvFus[1].keys(), delimiter = ',')
writer.writeheader()
writer.writerows(tInvFus)
Remarque : pour que des opérations sur cette réunion de tables soient possibles, il faut s'assurer que les deux tables sont construites de la même façon : les champs et les types de données doivent être les mêmes.
Quelquefois, on veut produire une nouvelle table à partir de deux tables distinctes en fusionnant certaines informations. Cela n'est possible que si les deux tables contiennent une information commune non ambigüe, c'est-à-dire qu'il n'est pas possible de fusionner deux informations qui ne vont pas ensemble. Par exemple, si l'on veut fusionner les notes obtenues à l'épreuve de NSI associées à un numéro de candidat, avec la correspondance entre les numéros d'anonymat des élèves et leur identité, il ne faut pas qu'il existe deux numéros de candidat identiques. Il faut ainsi avoir un identifiant unique commun aux deux tables pour réaliser une opération de jointure.
Ce document est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 4.0 International.
Pour toute question : charles.poulmaire@ac-versailles.fr ou pascal.remy@ac-versailles.fr