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.

Fichiers à télécharger :

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']

 

Activité
Pour éviter les répétitions, convertir la liste en ensemble (set) avec la fonction set().
Écrire l'instruction permettant de lister les codes de toutes les monnaies qui s’appellent 'Dollar'.
Écrire l'instruction permettant de lister les noms des pays de plus de 100 millions d'habitants, sous la forme (Pays, Population (en Millions d'habitants)).

 

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 fonction sorted() 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.

 

Activité
Écrire les instructions permettant d'afficher les 10 pays les moins peuplés, dans l'ordre inverse de leur population, sous la forme (pays, population)

 

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 :

  1. trier tous les pays par populations décroissantes ;
  2. 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.

 

Activité
Écrire les instructions permettant de d'afficher les 8 pays possédant la plus grande densité de population, dans l'ordre inverse de densité, sous la forme (Pays, densité).

 

 

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 que villes.Name plutôt que villes['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

 

Activité
Pour éviter les répétitions, utiliser la méthode .unique() qui s’applique à une série et non une trame de données.
Écrire l'instruction permettant de lister les codes de toutes les monnaies qui s’appellent 'Dollar'.
Écrire l'instruction permettant de lister les noms des pays de plus de 100 millions d'habitants, sous la forme (Pays, Population).

 

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')

 

Activité
Écrire les instructions permettant d'afficher les 10 pays les moins peuplés, sous la forme (pays, population).

 

On peut trier selon plusieurs critères, en spécifiant éventuellement les monotonies.

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 et Population : 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.

Vous aimerez aussi...

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *