Traitement de données

On dispose aujourd'hui d'énormément des données statistiques en libre accès. Il est même inscrit dans la loi française un droit au libre accès aux données publiques (loi n° 2016-1321 du 7 octobre 2016 pour une République numérique).

Devant la multitude de données disposibles, il est nécessaire de s'accorder sur un format de présentation commun. C'est le format CSV pour Comma-Separeted Values.

Il s'agit d'un format rudimentaire (et donc facile à manipuler). Les données sont organisées en ligne, une par entrée. Au sein de chaque ligne, les données sont séparées par des virgules.

Remarque : en France, la virgule est le séparateur des nombres décimaux. Les fichiers CSV français peuvent donc soit :

  • utiliser les virgules en séparateur et des points (.) pour écrire les nombres décimaux comme dans les fichiers internationaux
  • utiliser des points-virgules en séparateur et des virgules pour les nombres décimaux
  • utiliser des points-virgules en séparateur et des points pour les nombres décimaux

Il n'y a donc pas de règle générale, il faut traîter nos données au cas par cas...

Nous allons dans ce document voir comment manipuler des données au format CSV

I. Importer des données :

Nous allons travailler avec des données géographiques provenant du site https://www.populationdata.net/monde/.

Deux fichiers nous intéressent :

  • paysPopulation.csv : contient les populations, superficies et densités de différents pays du monde (identifiés par un rang et leur nom)
  • paysVie.csv : contient les espérances de vie de différents pays du monde (identifiés par un rang et leur nom)

Notez que les deux fichiers de présentent pas exactement les mêmes pays...

Commençons par lire les données.

Imaginons que l'on veuille lire un receuil de poèmes. Pour cela il faut :

  1. Ouvrir le livre
  2. Pour chaque ligne (vers) du livre :
    • lire la ligne
    • séparer les mots selon les espaces
    • comprendre le mot
  3. Fermer le livre

Lire un fichier CSV focntionne la même façon. On doit :

  1. Ouvrir le fichier
  2. Pour chaque ligne du fichier
    • lire la ligne
    • séparer les données selon les virgules (ou points-virgules)
    • typer les données (entier, flottant, texte...)
  3. Fermer le fichier

On récupèrera à la fin de ces étapes un tableau contenant différentes entrées, chacune d'entre elle correspondant à une ligne du fichier initial.

En python :

In [2]:
def lire_CSV(adresse, separateur) :
    """
    Lecture d'un fichier CSV indiqué par son adresse sur le disque. Le séparateur de données est fournit en argument
    adresse est une chaîne de caractères correspondant à l'adresse du fichier sur le disque
    separateur est une chaîne de caractères indiquant le séparateur de données dans une ligne (typiquement , ou ;)
    Renvoie une liste de liste, chaque sous-liste correspondant aux données d'une ligne du fichier CSV.
    Attention, les données de sortie sont au format chaîne des caractère
    """
    
    tableau_valeurs = []
    
    #Lecture de l'ensemble des lignes
    with open(adresse) as fichier :
        for ligne in fichier :
            tableau_valeurs.append(ligne.split(separateur))
    
    return tableau_valeurs

Lisons le premier fichier :

In [3]:
populations = lire_CSV("paysPopulation.csv", ";")

# On affiche les trois premières lignes pour information
print(populations[:4])
[['Rang', 'Pays', 'Population', 'Superficie (km²)', 'Densité (hab./km²)\n'], ['194', 'Abkhazie', '246313', '8653', '28.47\n'], ['195', 'Açores', '243862', '2322', '105.02\n'], ['47', 'Afghanistan', '31575018', '652864', '48.36\n']]

On a bien nos données. On peut remarquer que la densité se termine par \n qui est le caractère de retour à la ligne.

Convertissons les données :

  • Le rang est un entier
  • Le pays est une chaîne de caractères
  • La population est un entier
  • Les autres valeurs sont des flottants
In [4]:
# On convertit les valeurs à partir de la ligne 1 car la ligne 0 contient les en-têtes
for ligne in populations[1:] :
    ligne[0] = int(ligne[0]) # le rang est convertir en un entier
    ligne[2] = int(ligne[2]) # la population est convertir en un entier
    ligne[3] = float(ligne[3]) # la superficie est un flottant
    ligne[4] = float(ligne[4]) # la densité est un flottant

print(populations[:4])
[['Rang', 'Pays', 'Population', 'Superficie (km²)', 'Densité (hab./km²)\n'], [194, 'Abkhazie', 246313, 8653.0, 28.47], [195, 'Açores', 243862, 2322.0, 105.02], [47, 'Afghanistan', 31575018, 652864.0, 48.36]]

On peut noter au passage que python ignore le \n en fin de ligne et le supprime.

On peut importer de même notre fichier d'espérance de vie :

In [5]:
esperances = lire_CSV("paysVie.csv", ";")

for ligne in esperances :
    ligne[0] = int(ligne[0]) # le rang est convertir en un entier
    ligne[3] = float(ligne[3]) # l'espérance de vie est un flottant'

print(esperances[:4])
[[77, 'Açores', 'Europe', 77.45], [212, 'Afghanistan', 'Asie', 60.37], [199, 'Afrique du Sud', 'Afrique', 64.0], [62, 'Albanie', 'Europe', 78.55]]

II. Rechercher des données :

On va maintenant chercher des données dans les tables.

Afin d'avoir un outil efficace, on souhaite pouvoir chercher les données répondant à des questions compliquées du type :


Quels pays dont le nom commence par la lettre 'F', ont une population de plus de 20 millions d'habitants ?

On va donc écrire une fonction qui prendra en argument le nom de la table ou chercher et une condition sur les lignes à vérifier :

In [6]:
def cherche_lignes(liste, condition) :
    """
    Parcourt l'ensemble des lignes de la liste et renvoie celles vérifiant la condition données
    liste est une liste de données
    condition est une condition saisie au format texte et portant sur la variable ligne
    Par exemple "ligne[1][0] == 'A'", pour les lignes dont la deuxième valeur débute par 'A' 
    """
    resultat = []
    for ligne in liste :
        if eval(condition) :
            resultat.append(ligne)
    
    return resultat

La réponse à notre question :

In [7]:
print( cherche_lignes(populations, "ligne[1][0] == 'F' and ligne[2] > 20e6"))
[[21, 'France', 66992699, 551695.0, 117.48]]

Il n'y a que la France !

Un autre exemple (facile) :

In [8]:
print( cherche_lignes(esperances, "ligne[3] > 83"))
[[1, 'Andorre', 'Europe', 89.14], [4, 'Hong Kong', 'Asie', 84.3], [5, 'Japon', 'Asie', 83.92], [7, 'Macao', 'Asie', 83.3], [2, 'Monaco', 'Europe', 85.0], [3, 'Saint-Marin', 'Europe', 84.65], [8, 'Singapour', 'Asie', 83.1], [6, 'Suisse', 'Europe', 83.4]]

Qu'a-t-on cherché ?

Un dernier exemple techinque :

In [9]:
print( cherche_lignes(populations, "ligne[1][0] in 'AEIOUY' and ligne[3] > 2_000_000"))
[[33, 'Algérie', 43003767, 2381741.0, 18.06], [249, 'Antarctique', 1500, 14107637.0, 0.0], [42, 'Arabie saoudite', 33413660, 2149690.0, 15.54], [31, 'Argentine', 44494502, 2780400.0, 16.0], [56, 'Australie', 25105503, 7688485.0, 3.27], [2, 'Inde', 1358408567, 3287469.0, 413.21]]

Qu'a-t-on cherché ?

III. Tri d'une table :

Les pays de la table populations sont classés dans l'ordre alphabétique. Comment les classer dans l'ordre croissants de leur populations ?

La fonction sorted de python va nous aider. Cette fonction tri une liste et renvoie la liste triée.

Appliquons-la à populations. On tri à partir de la ligne 1 car la 0 contient les en-têtes :

In [10]:
sorted(populations[1:])[:5]
Out[10]:
[[1, 'Chine', 1394112547, 9596961.0, 145.27],
 [2, 'Inde', 1358408567, 3287469.0, 413.21],
 [3, 'États-Unis', 331883986, 9629047.0, 34.47],
 [4, 'Indonésie', 268674755, 1913579.0, 140.4],
 [5, 'Nigéria', 212871345, 923768.0, 230.44]]

Python a classé les pays dans l'ordre de l'indice ! C'est assez logique.

Il nous faut donc indiquer à python qu'il doit classer les éléments en ne regardant que les populations (l'élément d'indice 2) :

In [11]:
tri_populations = sorted(populations[1:], key= lambda ligne : ligne[2])
In [12]:
tri_populations[:5]
Out[12]:
[[253, 'Géorgie du Sud-et-les Îles Sandwich du Sud', 35, 4190.0, 0.01],
 [252, 'Pitcairn', 50, 47.0, 1.06],
 [251,
  'Terres australes et antarctiques françaises (TAAF)',
  196,
  439672.0,
  0.0],
 [250, 'Vatican', 804, 1.0, 1827.27],
 [249, 'Antarctique', 1500, 14107637.0, 0.0]]

Et si l'on souhaite le tri par la superficie ?

In [13]:
tri_superficie = sorted(populations[1:], key= lambda ligne : ligne[3])
In [14]:
tri_superficie[:5]
Out[14]:
[[250, 'Vatican', 804, 1.0, 1827.27],
 [227, 'Monaco', 38300, 2.0, 18960.4],
 [229, 'Gibraltar', 36974, 7.0, 5437.35],
 [248, 'Tokelau', 1556, 10.0, 155.6],
 [237, 'Nauru', 12696, 21.0, 596.06]]

IV. Fusions de tables :

Nous avons donc deux tableaux reprenant diverses informations pour les mêmes pays.

Il serait plus pratique d'avoir ces données dans le même tableau... Nous allons donc compléter le tableau populations (celui qui contient le plus de pays) en ajoutant les informations manquantes de l'autre tableau (le continent et l'espérance de vie).

Tout d'abord, on ajoute les en-têtes :

In [15]:
populations[0].append('Continent')
populations[0].append('Espérance de vie (années)')
populations[0]
Out[15]:
['Rang',
 'Pays',
 'Population',
 'Superficie (km²)',
 'Densité (hab./km²)\n',
 'Continent',
 'Espérance de vie (années)']

On parcourt ensuite toutes les lignes du premier tableau et pour chacune on cherche si un pays du second tableau a le même nom. Si oui on copie les valeurs, si non on écrit des valeurs d'erreur.

In [16]:
for ligne1 in populations[1:] :
    # Par défaut on donne les valeurs d'erreur
    ligne1.append('?')
    ligne1.append(-1)
    for ligne2 in esperances :
        # On remplace les valeurs d'erreur par les bonnes si les noms des pays correspondent
        if ligne1[1] == ligne2[1] :
            ligne1[-2] = ligne2[2]
            ligne1[-1] = ligne2[3]
            # Le break permet de sortir de la boucle for => inutile de continuer à chercher si on a
            # trouvé une correspondance
            break

print(populations[:4])
[['Rang', 'Pays', 'Population', 'Superficie (km²)', 'Densité (hab./km²)\n', 'Continent', 'Espérance de vie (années)'], [194, 'Abkhazie', 246313, 8653.0, 28.47, '?', -1], [195, 'Açores', 243862, 2322.0, 105.02, 'Europe', 77.45], [47, 'Afghanistan', 31575018, 652864.0, 48.36, 'Asie', 60.37]]