Traitement des données en tables
Une des utilisations principales de l’informatique de nos jours est le traitement de quantités importantes de données dans des domaines très variés :
un site de commerce en ligne peut avoir à gérer des bases données pour des dizaines de milliers d’articles en vente, de clients, de commandes ;
un hôpital doit pouvoir accéder efficacement à tous les détails de traitements de ses patients ;
etc …
Mais si les logiciels de gestion de base de données (SGDB) sont des programmes hautement spécialisés pour effectuer ce genre de tâches le plus efficacement et sûrement possible, il est facile de mettre en œuvre les opérations de base dans un langage de programmation comme Python.
Données en table
Organisées en table, les données se présentent sous la forme suivante :
En informatique, une table de données correspondent à une liste de p-uplets nommés qui partagent les mêmes descripteurs.
Exemple de p-uplet nommé (syntaxe Python) :
{'Id' : 1, 'Nom' : 'NAYMAR', 'Prénom' : 'Jean'}
En Python, on parle de Clés et de Valeurs.
Le format CSV
Le format CSV (pour comma separated values, soit en français valeurs séparées par des virgules) est un format très pratique pour représenter des données structurées.
Dans ce format, chaque ligne représente un enregistrement et, sur une même ligne, les différents champs de l’enregistrement sont réparés par une virgule (d’où le nom).
En pratique, on peut spécifier le caractère utilisé pour séparer les différents champs et on utilise fréquemment un point-virgule, une tabulation ou deux points pour cela.
Dans la suite, nous allons utiliser deux fichiers nommés countries.csv
et cities.csv
qui contiennent quelques données sur les différents pays et villes du monde.
Les premières lignes de countries.csv
sont :
ISO;Name;Capital_Id;Area;Population;Continent;Currency_Code;Currency_Name AD;Andorra;3041563;468;84000;EU;EUR;Euro AE;United Arab Emirates;292968;82880;4975593;AS;AED;Dirham AF;Afghanistan;1138958;647500;29121286;AS;AFN;Afghani AG;Antigua and Barbuda;3576022;443;86754;NA;XCD;Dollar AI;Anguilla;3573374;102;13254;NA;XCD;Dollar AL;Albania;3183875;28748;2986952;EU;ALL;Lek ...
Remarques :
- Les valeurs sont clairement séparés par des points-virgules.
- Comme c’est souvent le cas, la première ligne est utilisée pour indiquer le nom des différents champs : c’est l’entête.
Dans ce cas, le premier enregistrement se trouve sur la deuxième ligne du fichier.- La signification des différents champs est transparente.
à part le champ nommé Capital_Id dont les valeurs sont des numéros d’identifiants de villes que l’on trouvera dans le fichier nommécities.csv
.
Les tableurs (MS Excel, LibreOffice Calc, …) sont capables d’ouvrir ce genre de fichier. Ils permettent de les afficher sous forme de tableau et d’en manipuler facilement les données :
Les données sont issues du site http://www.geonames.org et ont été légèrement simplifiées.
Bibliothèque csv
Importation des données
Une façon de charger un fichier CSV en Python est d’utiliser la bibliothèque csv
(fournie avec la distribution de Python).
Voici une portion de code permettant de charger le fichier countries.csv
, avec des points-virgules comme délimitations.
import csv pays = [] with open('countries.csv', newline='') as csvfile: # Ouverture du fichier .csv reader = csv.reader(csvfile, delimiter=';') # Création d'un objet "lecteur", à itérer for row in reader: pays.append(row)
Dans ce cas, les résultats sont stockés sous forme d’un tableau de tableaux (ou liste de listes au sens de Python).
On peut ainsi obtenir la liste des noms des champs (ligne d’en-tête) :
>>> pays[0] ['ISO', 'Name', 'Capital_Id', 'Area', 'Population', 'Continent', 'Currency_Code', 'Currency_Name']
Puis le premier enregistrement :
>>> pays[1] ['AD', 'Andorra', '3041563', '468', '84000', 'EU', 'EUR', 'Euro']
Cette structure n’est pas la plus satisfaisante car le lien entre les valeurs du tableau pays[1]
et le nom des enregistrements, contenus dans pays[0]
, n’est pas direct.
Dictionnaire ordonné
La bibliothèque csv
possède une fonction DictReader()
qui retourne un objet DictReader
: un itérateur contenant un dictionnaire ordonné (OrderedDict
) pour chaque enregistrement, la première ligne étant utilisée pour nommer les différents champs.
pays = [] with open('countries.csv', newline='') as csvfile: reader = csv.DictReader(csvfile, delimiter=';') # Objet DictReader (itérateur) for row in reader: pays.append(dict(row)) # Conversion du type OrderedDict en dictionnaire
La conversion du dictionnaire ordonné en dictionnaire (
dict(row)
) permet uniquement d’avoir un affichage plus plaisant.
Cette fois, on obtient un tableau de p-uplets représentés sous forme de dictionnaire :
>>> pays[0] {'ISO': 'AD', 'Name': 'Andorra', 'Capital_Id': '3041563', 'Area': '468', 'Population': '84000', 'Continent': 'EU', 'Currency_Code': 'EUR', 'Currency_Name': 'Euro'}
Exploitation des données
Nous allons donner deux types d’utilisation simples des données que l’on vient de charger : tout d’abord, l’interrogation des données pour récupérer telle ou telle information, puis le tri des données.
Interrogations
On peut traduire en Python des questions simples.
Par exemple : « quels sont les pays où l’on paye en euro ? »
>>> [p['Name']for p in pays if p['Currency_Code'] =='EUR'] ['Andorra', 'Austria', 'Belgium', ..., 'Vatican', 'Mayotte']
Tris
Pour exploiter les données, il peut être intéressant de les trier. Une utilisation possible est l’obtention du classement des entrées selon tel ou tel critère. Une autre utilisation vient du fait que, comme présenté dans la partie algorithmique du programme, la recherche dichotomique dans un tableau trié est bien plus efficace que la recherche séquentielle dans un tableau quelconque.
Tri selon un unique critère
On ne peut pas directement trier le tableau pays
… car cela ne veut rien dire. Il faut indiquer selon quels critères on veut effectuer ce tri.
Pour cela, on appelle la fonction sorted()
ou la méthode .sort()
, avec l’argument supplémentaire key
qui est une fonction renvoyant la valeur utilisée pour le tri.
Rappel : la méthode
.sort()
trie la liste en place, alors que la fonctionsorted()
renvoie une nouvelle liste correspondant à la liste triée, la liste initiale étant laissée intacte.
Par exemple, si l’on veut trier les pays par leur superficie, on doit spécifier la clé 'Area'
. Pour cela, on définit une fonction appropriée :
def cle_superficie(p): return p['Area']
Ainsi, pour classer les pays par superficie décroissante, on effectue :
pays.sort(key=cle_superficie, reverse=True)
Mais un petit problème demeure. Si on récupère les noms des 5 premiers pays ainsi classés, le résultat est étonnant :
>>> [(p['Name'], p['Area'])for p in pays[:5]] [('Canada','9984670'), ('SouthKorea','98480'), ('UnitedStates','9629091'), ('NetherlandsAntilles','960'), ('China','9596960')]
On ne s’attend pas à trouver la Corée du Sud parmi eux. La raison est que lors de l’import, tous les champs sont considérés comme des chaînes de caractères, et le tri utilise l’ordre du dictionnaire. Ainsi, de même que 'aa'
arrive avant 'b'
, '10'
arrive avant '2'
. Cela apparaît ici en regardant les superficies qui commencent par 998, puis par 984, par 962, etc. Pour remédier à cela, on modifie la fonction de clé :
def cle_superficie(p): return float(p['Area'])
On a alors le résultat espéré :
>>> [(p['Name'], p['Area']) for p in sorted(pays, key=cle_superficie, reverse=True)[:5]] [('Russia','17100000.0'), ('Canada','9984670.0'), ('UnitedStates','9629091.0'), ('China','9596960.0'), ('Brazil','8511965.0')]
On peut également procéder d’une autre manière :
>>> sorted([(p['Name'], float(p['Area'])) for p in pays], key=lambda p:p[1], reverse=True)[:5] [('Russia',17100000.0), ('Canada',9984670.0), ('UnitedStates',9629091.0), ('China',9596960.0), ('Brazil',8511965.0)]
L’avantage est que la conversion en nombre apparait dans le résultat.
Tri selon plusieurs critères
Supposons maintenant que l’on veut trier les pays selon deux critères : tout d’abord le continent, puis le nom du pays. On peut faire cela en définissant une fonction de clé qui renvoie une paire (continent, nom du pays) :
def cle_combinee(p): return (p['Continent'], p['Name'])
Ainsi,
>>> [(p['Continent'], p['Name']) for p in sorted(pays, key=cle_combinee)] [('AF','Algeria'), ('AF','Angola'), ..., ('AF','Zambia'), ('AF','Zimbabwe'), ('AS','Afghanistan'), ('AS','Armenia'), ..., ('SA','Uruguay'), ('SA','Venezuela')]
Cependant, dans ce tri, les deux critères ont été utilisés pour un ordre croissant. Supposons maintenant que l’on veuille trier les pays par continent et, pour chaque continent, avoir les pays par population décroissante. La méthode précédente n’est pas applicable, car on a utilisé une unique clé (composée de deux éléments) pour un tri croissant.
À la place, nous allons procéder en deux étapes :
- trier tous les pays par populations décroissantes ;
- trier ensuite le tableau obtenu par continents croissants.
Ainsi :
def cle_population(p): return int(p['Population']) def cle_continent(p): return p['Continent'] >>> pays.sort(key = cle_population, reverse = True) >>> pays.sort(key = cle_continent) >>> [(p['Name'], p['Continent'], p['Population']) for p in pays] [('Nigeria','AF','154000000'), ('Ethiopia','AF','88013491'), ..., ('Seychelles','AF','88340'), ('SaintHelena','AF','7460'), ('China','AS','1330044000'), ('India','AS','1173108018'), ..., ('FrenchGuiana','SA','195506'), ('FalklandIslands','SA','2638')]
Pour que cela soit possible, la fonction de tri de Python vérifie une propriété très importante : la stabilité. Cela signifie que lors d’un tri, si plusieurs enregistrements ont la même clé, l’ordre initial des enregistrements est conservé.
Ainsi, si on a trié les pays par ordre décroissant de population puis par continent, les pays d’un même continent seront regroupés en conservant l’ordre précédent, ici la population décroissante.
Conclusion
Nous l’avons vu, il est assez facile d’écrire en Python des commandes simples pour exploiter un ensemble de données.
Cependant, une utilisation plus poussée va vite donner lieu à des programmes fastidieux : notamment, pour pouvoir exploiter les capitales des pays, nous allons devoir utiliser des données présentes dans un fichier supplémentaire.
Pour remédier à ce problème, nous allons utiliser la bibliothèque pandas qui permet d’exprimer de façon simple, lisible et concise ce genre de manipulation de données.
Bibliothèque pandas
La bibliothèque pandas
( https://pandas.pydata.org/ ) ne fait pas partie de la distribution de Python, il faut donc l’installer :
pip install pandas
Lecture de fichiers
De façon classique en Python, nous allons commencer par importer le module, puis charger deux fichiers CSV :
import pandas pays = pandas.read_csv("countries.csv", delimiter=";", keep_default_na=False) villes = pandas.read_csv("cities.csv", delimiter=";")
On spécifie explicitement le caractère utilisé pour délimiter les champs du fichier, ici un point-virgule.
Remarque : l’option
keep_default_na=False
est nécessaire à cause de la gestion des données manquantes. Une absence est parfois précisée spécifiquement en écrivant'NA'
plutôt que de ne rien mettre. Ainsi, à la base, la lecture de'NA'
est interprété comme une donnée manquante. On est obligé de désactiver ce traitement de base pour pouvoir utiliser la valeur'NA'
comme code de l’Amérique du Nord.
La fonction read_csv()
renvoie des objets de type DataFrame
, qui possèdent des propriétés et des méthodes particulièrement adaptées au traitement des données.
L’affichage d’un extrait de la table sous forme de tableau est très claire :
>> pays ISO Name Capital_Id ... Continent Currency_Code Currency_Name 0 AD Andorra 3041563 ... EU EUR Euro 1 AE United Arab Emirates 292968 ... AS AED Dirham 2 AF Afghanistan 1138958 ... AS AFN Afghani 3 AG Antigua and Barbuda 3576022 ... NA XCD Dollar 4 AI Anguilla 3573374 ... NA XCD Dollar .. .. ... ... ... ... ... ... 243 YE Yemen 71137 ... AS YER Rial 244 YT Mayotte 921815 ... AF EUR Euro 245 ZA South Africa 964137 ... AF ZAR Rand 246 ZM Zambia 909137 ... AF ZMW Kwacha 247 ZW Zimbabwe 890299 ... AF ZWL Dollar [248 rows x 8 columns]
La bibliothèque pandas propose quelques commandes utiles :
villes.head()
affiche les premières entrées de la table ;villes.sample(7)
affiche 7 enregistrements de la table pris au hasard ;villes.columns
retourne la liste des champs ;villes.dtypes
affiche la liste des champs avec, à chaque fois, le type de données correspondant.
Ainsi, on a :
>>> villes.dtypes Id int64 Name object Latitude float64 Longitude float64 Country_ISO object Population int64 dtype: object
On remarque en particulier que pandas
a reconnu que les champs latitude, longitude et population correspondent à des données numériques.
On peut aussi avoir des informations statistiques (bien sûr, seules celles concernant la population sont pertinentes) :
>>> villes.describe() Id Latitude Longitude Population count 2.433800e+04 24338.000000 24338.000000 2.433800e+04 mean 2.678504e+06 27.818974 14.003089 1.136149e+05 std 1.823194e+06 23.071433 71.640034 4.733138e+05 min 1.057000e+04 -54.810840 -176.174530 0.000000e+00 25% 1.269059e+06 15.270907 -47.851943 2.197725e+04 50% 2.521372e+06 34.453770 13.836505 3.545900e+04 75% 3.515904e+06 44.699797 74.529257 7.405375e+04 max 1.207700e+07 78.223340 179.364510 2.231547e+07
Enfin, on peut facilement ne conserver que les champs qui nous intéressent.
Par exemple, si l’on ne veut que les noms des villes et leurs coordonnées, on utilise :
villes[['Name','Latitude','Longitude']] Name Latitude Longitude 0 les Escaldes 42.50729 1.53414 1 Andorra la Vella 42.50779 1.52109 2 Umm Al Quwain City 25.56473 55.55517 3 Ras Al Khaimah City 25.78953 55.94320 4 Zayed City 23.65416 53.70522 ... ... ... ... 24333 Bulawayo -20.15000 28.58333 24334 Bindura -17.30192 31.33056 24335 Beitbridge -22.21667 30.00000 24336 Epworth -17.89000 31.14750 24337 Chitungwiza -18.01274 31.07555 [24338 rows x 3 columns]
Trames de données et Séries
Les tables lues dans les fichiers CSV sont stockés par pandas
sous forme de trame de données (type DataFrame
). On peut les voir comme un tableau de p-uplets nommés appelés séries (type Series
).
Par exemple, l’enregistrement numéro 10 (obtenu grâce à la méthode .loc()
) s’obtient en exécutant :
>>> villes.loc[10] Id 292688 Name Ar Ruways Latitude 24.1103 Longitude 52.7306 Country_ISO AE Population 16000 Name: 10, dtype: object >>> type(villes) <class 'pandas.core.frame.DataFrame'> >>> type(villes.loc[10]) <class 'pandas.core.series.Series'>
et son nom s’obtient comme pour un dictionnaire :
>>> villes.loc[10]['Name'] 'Ar Ruways'
On peut également obtenir l’ensemble des valeurs d’un seul champ d’une trame de données, toujours sous la forme d’une série :
>>> villes['Name'] 0 les Escaldes 1 Andorra la Vella 2 Umm Al Quwain City 3 Ras Al Khaimah City 4 Zayed City ... 24333 Bulawayo 24334 Bindura 24335 Beitbridge 24336 Epworth 24337 Chitungwiza Name: Name, Length: 24338, dtype: object >>> type(villes['Name']) <class 'pandas.core.series.Series'>
Remarque :
pandas
permet d’utiliser une syntaxe légère en n’écrivant quevilles.Name
plutôt quevilles['Name']
.
Attention, il faut différencier :
- la série
villes['Name']
; - la trame de données à un seul champ
villes[['Name']]
.
Exploitation des données
Interrogations simples
Reprenons les interrogations présentées dans la première partie, et exprimons-les à l’aide de pandas.
- Noms des pays où l’on paye en euros
On sélectionne la bonne valeur de Currency_Code
ainsi :
pays[pays.Currency_Code =='EUR']
Ensuite, on ne garde que les noms des pays ainsi obtenus, pour obtenir :
pays[pays.Currency_Code == 'EUR'].Name
Tris
Les méthodes .nlargerst()
et .nsmallest()
permettent de déterminer les plus grands et plus petits éléments selon un critère donné.
Ainsi, pour obtenir les 10 pays les plus grands en superficie et les 5 moins peuplés, on peut écrire :
pays.nlargest(10,'Area') pays.nsmallest(5,'Population')
Le tri d’une une trame de données s’effectue à l’aide de la méthode .sort_values()
, comme par exemple :
villes.sort_values(by='Population')
On peut trier selon plusieurs critères, en spécifiant éventuellement les
.Ainsi, pour classer par continent puis par superficie décroissante (avec une sélection pertinente de champs) :
pays.sort_values(by=['Continent','Area'], ascending=[True, False])[['Continent','Name','Area']]
Manipulation de données
Création d’un nouveau champ
Il est très facile de créer de nouveaux champs à partir d’anciens.
Par exemple, pour calculer la densité de chaque pays, il suffit d’exécuter :
pays['Density'] = pays.Population / pays.Area
Tracés de graphiques
La bibliothèque pandas
permet également de réaliser toutes sortes de graphiques en exploitant les bibliothèques numpy
et matplotlib
:
import numpy import matplotlib.pyplot as plt
Les séries peuvent faire l’objet de graphiques.
Par exemple, on peut réaliser une carte des villes utilisant la projection de Mercator en effectuant :
villes['projection_y'] = numpy.arcsinh(numpy.tan(villes.Latitude * numpy.pi / 180)) villes.plot.scatter(x='Longitude', y='projection_y') plt.show()
Fusion de tables
Dans la table des pays, la capitale est indiquée par un numéro (champ Capital_Id
) … qui correspond au champ Id
de la table des villes. Pour récupérer le nom de la capitale de chaque pays, nous allons fusionner les tables en effectuant une jointure. Ainsi, nous allons faire correspondre le champ Capital_Id
de pays
et le champ Id
de villes
.
Cela se fait à l’aide de la méthode .merge()
:
pandas.merge(pays, villes, left_on='Capital_Id', right_on='Id')
Cependant, en procédant ainsi, il va y avoir un conflit entre les champs des deux tables. Cela apparaît en listant les champs de la table obtenue :
>>> pandas.merge(pays, villes, left_on='Capital_Id', right_on='Id').columns Index(['ISO', 'Name_x', 'Capital_Id', 'Area', 'Population_x', 'Continent', 'Currency_Code', 'Currency_Name', 'Id', 'Name_y', 'Latitude', 'Longitude', 'Country_ISO', 'Population_y'], dtype='object')
On voit que des tables initiales contiennent toutes les deux des champs
Name
etPopulation
: l’opération de fusion rajoute donc automatiquement les suffixes_x
et_y
pour marquer la référence à la première table initiale ou à la seconde.
Pour rendre cela plus lisible, nous allons :
- ne garder que les colonnes de
villes
qui nous intéressent, ici l’identifiant et le nom ; - renommer ces colonnes pour éviter les collisions avec les champs de
pays
:
villes[['Id', 'Name']].rename(columns={'Id':'Capital_Id', 'Name':'Capital_name'})
Et c’est cette nouvelle table que nous allons fusionner avec la table pays
(dont nous ne garderons pas toutes les colonnes non plus) :
pays_et_capitales = pandas.merge(pays[['ISO', 'Name', 'Capital_Id', 'Continent']], villes[['Id', 'Name']].rename(columns = {'Id':'Capital_Id', 'Name':'Capital_Name'}), on = 'Capital_Id')
La liste des pays d’Océanie et leurs capitales s’obtient alors facilement :
pays_et_capitales[pays_et_capitales.Continent == 'OC']
Conclusion
La bibliothèque pandas
est un outil intéressant pour s’initier à la manipulation de données. En particulier, le rôle central qu’y jouent les trames de données permet de manipuler les enregistrements quasiment comme s’il s’agissait de p-uplets nommés.
Cependant, pour gérer des données organisées de façon plus complexe, notamment lorsqu’il y a plusieurs tables en relation, de très nombreux enregistrements, … il faudra utiliser un système de gestion de bases de données (SGDB).
Ces derniers possèdent en plus de très nombreux avantages, comme l’accès aux données par plusieurs utilisateurs en même temps par exemple.