Skip to content

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 majuscules
  • nom : le nom en anglais du pays, commence par une majuscule
  • capitale : le nom de la capitale, commence par une majuscule
  • aire : la superficie en km2 du pays
  • pop : le nombre d'habitants
  • continent : 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écimal
  • long : la longitude en système décimal
  • code_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 un SELECT * ...
  • où cherche-t-on ? dans le tableau des pays... on a la suite : [pays for pays in l_pays ....] et la version SQL SELECT * 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 :

  1. les différentes instances ont exactement les mêmes valeurs sur tous les descripteurs : on élimine pour ne garder qu'un seul enregistrement ;
  2. 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 :

  1. 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.
  2. 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 devise
  • Code 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.