Traitement des données via Python¶
Nous voici donc avec des données issues d'un fichier texte au format CSV et stockées dans une liste de dictionnaires. Nous pouvons effectuer des traitements, que nous appellerons requêtes dans le monde des bases de données.
Rappel sur les données¶
Rappelons que nous avons deux listes à notre disposition : countries.csv et cities.csv
L_PAYS
une liste de 251 états¶
avec pour chacun les informations suivantes :
code
: un code de deux lettres majusculesnom
: le nom en anglais du pays, commence par une majusculecapitale
: le nom de la capitale, commence par une majusculeaire
: la superficie en km2 du payspop
: le nombre d'habitantscontinent
: un code de deux lettres majuscules pour le continent (voir ci-dessous la liste des continents)
La liste des continents est :
- 'AF' : Afrique
- 'AN' : Antartique
- 'AS' : Asie
- 'EU' : Europe
- 'NA' : Amérique du Nord
- 'OC' : Océanie
- 'SA' : Amérique du Sud
L_VILLES
une liste de plus de 196000 villes¶
de plus de 500 habitants avec pour chacune les informations suivantes :
code
: un code type entier pour chacune des villes (attention ce code n'a rien à voir avec un code postal)nom
: le nom de la ville (attention dans la langue du pays)lat
: la latitude en système décimallong
: la longitude en système décimalcode_pays
: le code du pays de la ville (deux lettres majuscules)pop
: le nombre d'habitants
Traitements¶
Même lors de manipulations simples, il faut garder en tête certains aspects que l'on retrouvera sur les bases de données et qui concernent :
- la fiabilité des données : toutes nos données sont elles correctes ? certaines valeurs ne sont-elles pas manquantes ? ou en double ?
- le type des données : connait-on le type de chacune des valeurs ? (texte, float, int ...)
Lors de la récupération de données sur le net, il pourra s'avérer nécessaire de les nettoyer pour garantir ces aspects. Un défaut de fiabilité ou de type pourra faire échouer nos scripts de traitement.
Même avec des données fiables il faudra rester vigilant lors des traitements. Par exemple, lors de recherches textuelles on veillera à harmoniser la casse. Si les données manipulées sont des nombres, on fera bien attention à caster les données issues de la table et qui en général ont été récupérées au format str
.
Rechercher dans une table¶
L'opération la plus commune concerne probablement la recherche d'une ou plusieurs données répondant à un critère simple. Par exemple, obtenir les informations concernant la France.
Une première requête directe :
[pays for pays in L_PAYS if pays['nom'].lower() == 'france']
On peut faire un travail un peu plus générique ; voici le code d'une fonction pour la recherche d'un pays par le nom :
def recherche_par_nom(l_pays, nom):
return [pays for pays in l_pays if pays['nom'].lower() == nom.lower()]
Et la requête concernant la France :
>>> recherche_par_nom(L_PAYS, 'France')
[{'code': 'FR',
'nom': 'France',
'capitale': 'Paris',
'aire': '547030',
'pop': '66987244',
'continent': 'EU'}]
Le critère de recherche peut être plus compliqué et engendrer plus d'une réponse :
def recherche_par_superficie_min(l_pays, aire_min):
return [pays for pays in l_pays if float(pays['aire']) > aire_min]
Et une requête possible (la liste de pays de plus de 10 millions de km\(^2\)):
>>> grands_pays = recherche_par_superficie_min(L_PAYS, 1000000)
>>> grands_pays
[{'code': 'AQ',
'nom': 'Antarctica',
'capitale': '',
'aire': '14000000',
'pop': '0',
'continent': 'AN'},
{'code': 'RU',
'nom': 'Russia',
'capitale': 'Moscow',
'aire': '17100000',
'pop': '144478050',
'continent': 'EU'}]
Les trois exemples ci-dessus utilisent les tableaux en compréhension de Python. Ce n'est pas pour rien : en effet, les requêtes que nous verrons plus tard en langage SQL se rapprochent de cette construction en compréhension. Prenons le premier exemple et décortiquons la requête :
- on cherche quoi ? des
pays
: on va mettre nos résultats dans une nouvelle table, ici un tableau donc. Cela explique le début :[pays ...]
en SQL nous aurons unSELECT * ...
- où cherche-t-on ? dans le tableau des pays... on a la suite :
[pays for pays in l_pays ....]
et la version SQLSELECT * FROM TABLE_PAYS ...
- quel critère ? le nom doit être égal à la valeur
France
(avec cette précaution sur la casse de la donnée sauvegardée) ce qui explique la fin de notre construction[ ... if pays['nom'].lower() == 'france']
et la version SQL... WHERE nom = 'France'
Détecter des doublons¶
La présence de doublons parmi les données, quelles soient en table ou dans un SGBD n'est pas souhaitable. Un des aspects du nettoyage des données concerne donc la détection et l'élimination de ces doublons. Mais cela peut s'avérer une opération délicate.
Cas 1 : les enregistrements sont identifiés par un code¶
Dans notre exemple de pays, chaque pays est identifié de façon unique par un code. On peut alors en un parcours de la table obtenir le nombre d'apparitions de chaque pays dans la table :
histogramme_pays = {}
for pays in L_PAYS:
code = pays['code']
histogramme[code] = histogramme_pays.get(code, 0) + 1
Il suffit ensuite de s'interesser aux pays qui apparaissent plus d'une fois. On a alors deux cas de figures :
- les différentes instances ont exactement les mêmes valeurs sur tous les descripteurs : on élimine pour ne garder qu'un seul enregistrement ;
- certaines valeurs diffèrent, là une intervention humaine risque d'être nécessaire pour déterminer quoi faire (garder les données plus récentes par exemple).
Cas 2 : les enregistrements ne sont pas forcément identifiés clairement¶
C'est le cas par exemple de notre table des villes. Chaque ligne de ville possède un code mais probablement propre à la base d'où est issu le fichier. Dès lors, il est délicat d'identifier clairement les villes qui seraient en doublon. Le nom bien sûr ne suffit pas : 11 villes portent le nom Paris. Même associé au pays cela n'est pas déterminant : 9 villes aux Etats-Unis portent le nom Paris.
Trier une table¶
Pouvoir effectuer un tri sur nos données est primordial. Que se soit pour obtenir le ou les premiers (connaître les admis à un concours) ou pour effectuer un affichage (tri par ordre alphabétique pour afficher les résultats d'un concours ou d'un examen).
Python offre une méthode permettant de trier une list
en place et une fonction permettant de créer une list
triée, sans modifier la list
originale.
Dans cette section, nous avons nommé en majuscules nos deux tableaux L_PAYS
et L_VILLES
: ce n'est pas pour rien, ce sont des constantes, et nous ne souhaitons pas les modifier. Aussi, nous privilégions la fonction sorted
qui va créer un nouveau tableau trié.
Par exemple pour trier les pays par superficie décroissante, nous allons commencer par écrire une fonction qui, pour un pays donné, retourne sa superficie sous la forme d'un float
:
def superficie(pays):
return float(pays['aire'])
Cette fonction nous sert de critère de tri via la fonction sorted
(attention au paramètre reverse
qui donne dans l'ordre décroissant, par défaut le tri se faisant dans l'ordre croissant) :
pays_par_superficie_decroissante = sorted(L_PAYS, key=superficie, reverse=True)
On peut mixer plusieurs critères :
par_continent_et_population_croissante = sorted(L_PAYS, key=lambda p: (p['continent'], population(p)))
Dans l'exemple ci-dessus, on se sert la fois d'une lambda expression pour ne pas créer une fonction supplémentaire, un peu articielle, et un tuple : la valeur de la première composante correspond au premier critère de recherche, ici le nom du continent puis on ajoute dans l'ordre les autres critères.
Fusionner des tables¶
On distingue deux sortes de fusion :
- on ajoute des enregistrements possédant les mêmes champs : il s'agit alors de concaténer les tables, on prenant garde à l'ordre des champs.
- on souhaite ajouter des champs à une table : on a alors affaire à une jointure opération basée sur le produit cartésien des enregistrements en ne gardant que certaines combinaisons (nous détaillerons cela dans la chapitre consacré aux Bases de Données).
Ajouter des enregistrements¶
Exposons cette manipulation sur des tables simples. Pierre et Marie décident de mettre en commun leurs carnets de contacts respectifs. Ces données sont accessibles dans les deux fichiers : contact_pierre.csv et conctact_marie.csv. Il s'agit de deux tables partageant les même descripteurs, pas forcément dans le même ordre (ce qui n'est pas grave, tant qu'on n'écrit pas de données dans un fichier).
Une première façon de faire consiste à concaténer les deux listes puis à chasser les doublons.
Une autre consiste à parcourir l'une des deux tables et pour chaque enregistrement parcourir l'autre table et voir dans quelle mesure il s'agit d'un enregistrement similaire à garder dans notre table en construction. C'est une jointure (produit cartésien).
Lorsque dans les tables il n'y a pas un descripteur qui identifie de façon unique chaque enregistrement (on parle de clé primaire dans le monde des bases de données), cela peut être délicat de décider du rapprochement de deux enregistrements. Ici, pour simplifier, nous supposerons que le couple (nom, prenom) est discriminant.
Dans la vidéo, nous réalisons cette fusion.
Ajouter des champs¶
La table L_DEVISES
issue du fichier currencies.csv contient les devises officielles de 268 régions du Monde (pays et autres). Attention certains pays possèdent plusieurs devises. Cette table possède 3 descripteurs :
Country
: le pays ou la région concernéCurrency
: le nom de la deviseCode
est un code de 3 lettres de la devise.
On pourrait ajouter les descripteurs code_monnaie
et nom_monnaie
à notre table de pays. Pour les pays où ces informations manquent, il faut y mettre la chaîne de caractères vide. Ci-dessous un petit script permettant la création d'une nouvelle table fusion de L_PAYS
et L_DEVISES
:
l_pays_devises = []
for pays in L_PAYS:
trouve = False
for devise in L_DEVISES:
if devise['Country'].lower() == pays['nom'].lower():
trouve = True
copie_pays = dict(pays, code_monnaie=devise['Code'],
nom_monnaie=devise['Currency'])
l_pays_devises.append(copie_pays)
if not trouve:
copie_pays = dict(pays, code_monnaie='', nom_monnaie='')
l_pays_devises.append(copie_pays)
Notez la double boucle qui rend cette opération coûteuse. Dès que les tables sont volumineuses, les opérations de jointures doivent être particulièrement optimisées.
Notre nouvelle table possède des doublons puisque certains pays ont plusieurs monnaies.
Les carnets de contacts
Avec l'exemple plus simple des carnets de contacts, nous avons créé une table des contacts communs à Pierre et Marie. La table
adresses.csv contient des descripteurs communs à cette table plus un descripteur adresse
(adresse postale). Nous pourrions fusionner les tables pour ajouter l'information d'adresse au carnet.
Voici comment. Afin d'optimiser cette jointure, nous profitons de la structure de dictionnaire : d_commun
est un dictionnaire des enregistrements de la table commune. Quelques uns des dictionnaires de ce dictionnaire :
{('derand', 'michel'): {'nom': 'derand',
'prenom': 'michel',
'tel': '0011',
'email': 'michel.derand@jaiunmail.com',
'adresse': '12, ch. du Paradis'},
('zertig', 'anais'): {'nom': 'zertig',
'prenom': 'anais',
'tel': '3220',
'email': 'anais.zertig@jaiunmail.com',
'adresse': '1, imp. des Rosiers'},
('bremond', 'julie'): {'nom': 'bremond',
'prenom': 'julie',
'tel': '',
'email': 'julie.bremond56@facedebique.com'},
...
```
Et la table des adresses postales, nommées `l_adresses` :
```python
[{'nom': 'derand',
'prenom': 'michel',
'tel': '0011',
'adresse': '12, ch. du Paradis'},
{'nom': 'zertig',
'prenom': 'anais',
'tel': '3220',
'adresse': '1, imp. des Rosiers'},
{'nom': 'menate', 'prenom': 'luc', 'tel': '', 'adresse': '42, bd. de la Pie'},
{'nom': 'vernier',
'prenom': 'alex',
'tel': '5342',
'adresse': '2, rue du Navire'}]
```
L'idée est donc de parcourir les enregistrements de notre table d'adresses, de créer le couple nom, prénom et de mettre à jour l'enregistrement correspondant dans le dictionnaire :
```python
for entree in l_adresses:
nom, prenom = entree['nom'], entree['prenom']
if (nom, prenom) not in d_commun:
d_carnet[nom, prenom] = {}
for cle in entree:
d_carnet[nom, prenom][cle] = entree[cle]
C'est l'efficacité du test d'appartenance d'une clé à un dictionnaire qui ici optimise notre produit cartésien : nous ne croisons finalement qu'avec l'enregistrement adéquat.
Il faudra être vigilant : au moment d'écrire cette nouvelle table dans un fichier se rappeler que certains enregistrements n'ont pas tous les champs. Par exemple, l'enregistrement de Julie Bremond ne possède pas de champ pour l'adresse.