Mise à plat de données opendata en Python (mise à jour)

L’exploitation des données ouvertes est souvent tributaire du modèle des données publiées. Il arrive souvent que les données soient publiées dans un format spécifique difficilement exploitable car présentant des informations qui relèveraient d’une représentation hiérarchique mais qui par commodité sont livrées sans réelle organisation.

Cet article vise à montrer comment il est possible en utilisant la librairie Python/pandas de mettre à plat les données opendata sous la forme d’une table unique.

Au passage, il montre comment visualiser l'index hiérarchique d'un DataFrame avec la librairie ETE Toolkit.

In [1]:
import pandas as pnd

Exemple n° 1 : France Très Haut Débit

Le premier exemple est tiré du site France Très Haut Débit. Il propose un fichier au format Excel avec par commune : les couvertures par technologie (DSL, câble et fibre) et par débit (éligible, 3, 8, 30 et 100 Mbits/s). Au total, le fichier présente 23 colonnes.

Le principe de mise à plat consiste à :

  • transférer les colonnes singulières vers l’index
  • hiérarchiser les colonnes relatives aux technologies et aux débits
  • renvoyer les colonnes hiérarchisées vers l’index
  • supprimer l’index

Chargement du fichier Excel

In [2]:
df = pnd.read_excel("FranceTHD_Open_Data_Observatoire_Juin2015.xlsx",\
                    sheetname="Communes",\
                    header=1)
df.head()
Out[2]:
Département Code INSEE Commune éligibles 3 Mbit/s et + 8 Mbit/s et + 30 Mbit/s et + 100 Mbit/s et + éligibles.1 3 Mbit/s et +.1 ... éligibles.2 3 Mbit/s et +.2 8 Mbit/s et +.2 30 Mbit/s et +.2 100 Mbit/s et +.2 éligibles.3 3 Mbit/s et +.3 8 Mbit/s et +.3 30 Mbit/s et +.3 100 Mbit/s et +.3
0 AIN 01001 L'Abergement-Clémenciat 1.000 0.448 0.052 0.000 0.000 1.000 0.448 ... 0.0 0.0 0.0 0.0 0.0 0.000 0.000 0.000 0.000 0.000
1 AIN 01002 L'Abergement-de-Varey 0.676 0.594 0.571 0.571 0.571 0.124 0.024 ... 0.0 0.0 0.0 0.0 0.0 0.571 0.571 0.571 0.571 0.571
2 AIN 01004 Ambérieu-en-Bugey 1.000 0.966 0.794 0.234 0.000 1.000 0.966 ... 0.0 0.0 0.0 0.0 0.0 0.000 0.000 0.000 0.000 0.000
3 AIN 01005 Ambérieux-en-Dombes 1.000 0.990 0.942 0.667 0.004 1.000 0.985 ... 0.0 0.0 0.0 0.0 0.0 0.004 0.004 0.004 0.004 0.004
4 AIN 01006 Ambléon 1.000 1.000 1.000 0.934 0.934 1.000 1.000 ... 0.0 0.0 0.0 0.0 0.0 0.934 0.934 0.934 0.934 0.934

5 rows × 23 columns

Transfert des 3 colonnes singulières vers l'index

In [3]:
new_index = list(df.columns.values[0:3])
df.set_index(new_index, inplace=True)
df.head()
Out[3]:
éligibles 3 Mbit/s et + 8 Mbit/s et + 30 Mbit/s et + 100 Mbit/s et + éligibles.1 3 Mbit/s et +.1 8 Mbit/s et +.1 30 Mbit/s et +.1 100 Mbit/s et +.1 éligibles.2 3 Mbit/s et +.2 8 Mbit/s et +.2 30 Mbit/s et +.2 100 Mbit/s et +.2 éligibles.3 3 Mbit/s et +.3 8 Mbit/s et +.3 30 Mbit/s et +.3 100 Mbit/s et +.3
Département Code INSEE Commune
AIN 01001 L'Abergement-Clémenciat 1.000 0.448 0.052 0.000 0.000 1.000 0.448 0.052 0.000 0 0.0 0.0 0.0 0.0 0.0 0.000 0.000 0.000 0.000 0.000
01002 L'Abergement-de-Varey 0.676 0.594 0.571 0.571 0.571 0.124 0.024 0.000 0.000 0 0.0 0.0 0.0 0.0 0.0 0.571 0.571 0.571 0.571 0.571
01004 Ambérieu-en-Bugey 1.000 0.966 0.794 0.234 0.000 1.000 0.966 0.794 0.234 0 0.0 0.0 0.0 0.0 0.0 0.000 0.000 0.000 0.000 0.000
01005 Ambérieux-en-Dombes 1.000 0.990 0.942 0.667 0.004 1.000 0.985 0.937 0.663 0 0.0 0.0 0.0 0.0 0.0 0.004 0.004 0.004 0.004 0.004
01006 Ambléon 1.000 1.000 1.000 0.934 0.934 1.000 1.000 1.000 0.000 0 0.0 0.0 0.0 0.0 0.0 0.934 0.934 0.934 0.934 0.934

Création d'une hiérarchie avec les colonnes répétées

In [4]:
df.columns = [["Tout"] * 5 + ["DSL"] * 5 + ["Câble"] * 5 + ["Fibre"] * 5,\
              ["1 Mbit", "3 Mbit", "8 Mbit", "30 Mbit", "100 Mbit"] * 4]
df.columns.names = ["Technologie", "Débit"]
df.head()
Out[4]:
Technologie Tout DSL Câble Fibre
Débit 1 Mbit 3 Mbit 8 Mbit 30 Mbit 100 Mbit 1 Mbit 3 Mbit 8 Mbit 30 Mbit 100 Mbit 1 Mbit 3 Mbit 8 Mbit 30 Mbit 100 Mbit 1 Mbit 3 Mbit 8 Mbit 30 Mbit 100 Mbit
Département Code INSEE Commune
AIN 01001 L'Abergement-Clémenciat 1.000 0.448 0.052 0.000 0.000 1.000 0.448 0.052 0.000 0 0.0 0.0 0.0 0.0 0.0 0.000 0.000 0.000 0.000 0.000
01002 L'Abergement-de-Varey 0.676 0.594 0.571 0.571 0.571 0.124 0.024 0.000 0.000 0 0.0 0.0 0.0 0.0 0.0 0.571 0.571 0.571 0.571 0.571
01004 Ambérieu-en-Bugey 1.000 0.966 0.794 0.234 0.000 1.000 0.966 0.794 0.234 0 0.0 0.0 0.0 0.0 0.0 0.000 0.000 0.000 0.000 0.000
01005 Ambérieux-en-Dombes 1.000 0.990 0.942 0.667 0.004 1.000 0.985 0.937 0.663 0 0.0 0.0 0.0 0.0 0.0 0.004 0.004 0.004 0.004 0.004
01006 Ambléon 1.000 1.000 1.000 0.934 0.934 1.000 1.000 1.000 0.000 0 0.0 0.0 0.0 0.0 0.0 0.934 0.934 0.934 0.934 0.934

Visualisation de l'index hiérarchique

In [5]:
from ete3 import Tree, TreeStyle, TextFace, add_face_to_node

def make_tree(multi_index):
    # textstyle spécifique
    def my_layout(node):
            tf = TextFace(node.name, tight_text=True, ftype='Calibri', fsize=12)
            add_face_to_node(tf, node, column=0, position="branch-right")

    ts = TreeStyle()
    ts.show_leaf_name = False
    ts.show_scale = False
    ts.branch_vertical_margin = 3
    ts.layout_fn = my_layout

    # construction de l'arbre
    t = Tree()
    for branch in zip(*multi_index.labels): # on balaie chaque branche (de la racine jusqu'aux feuilles)
        current_node = t
        for i, j in enumerate(branch): # on balaie chaque branche de la racine jusqu'aux feuilles
            # récupération du nom du noeud
            node_name = multi_index.levels[i][int(j)]
            nodes = current_node.search_nodes(name=node_name)
            if nodes: # le noeud existe déjà et devient le noeud courrant
                current_node = nodes[0]
            else: # le noeud n'existe pas : il est ajouté au noeud courrant et devient le noeud courrant
                child = Tree(node_name+';', format=8)
                current_node.add_child(child=child,dist=1)
                current_node = child
    return t, ts
    

t, ts = make_tree(df.columns)
t.render("%%inline", tree_style=ts)
Out[5]:

Mise à plat des données

Après mise à plat, les données peuvent être représentées sous la forme d’une table à 6 colonnes seulement : département, code INSEE, commune, technologie, débit et valeur, bien plus facile à exploiter.

In [6]:
# noms des colonnes hiérarchiques
current_columns = df.columns.names
# noms des colonnes de la table finale
new_columns = df.index.names + current_columns + ["Value"]
# empilement des colonnes hiérarchiques vers l'index puis annulation de l'index
df = df.stack(current_columns).reset_index()
df.columns = new_columns
df.head()
Out[6]:
Département Code INSEE Commune Technologie Débit Value
0 AIN 01001 L'Abergement-Clémenciat Câble 1 Mbit 0.0
1 AIN 01001 L'Abergement-Clémenciat Câble 100 Mbit 0.0
2 AIN 01001 L'Abergement-Clémenciat Câble 3 Mbit 0.0
3 AIN 01001 L'Abergement-Clémenciat Câble 30 Mbit 0.0
4 AIN 01001 L'Abergement-Clémenciat Câble 8 Mbit 0.0

Exemple n° 2 : France Couverture 2G/3G/4G

Le second exemple, toujours dans le domaine des télécoms, est tiré du site OpenDataSoft. Il propose un fichier CSV avec par commune : les couvertures en population et surfacique par opérateur (Orange, Bouygues, SFR et Free) et par technologie (2G, 3G et 4G). Au total, le fichier présente 37 colonnes.

Chargement du fichier CSV

In [7]:
df2 = pnd.read_csv("couverture-2g-3g-4g-en-france-par-operateur-juillet-2015.csv",\
                 sep=";")
df2.head()
Out[7]:
CODE POSTAL CODE INSEE NOM COMMUNE CODE DEPARTEMENT SURFACE COMMUNE (km²) POPULATION COMMUNE Orange France Couverture population 4G Bouygues Telecom Couverture population 4G SFR Couverture population 4G Free Mobile Couverture population 4G ... Bouygues Telecom Couverture population 2G SFR Couverture population 2G Free Mobile Couverture population 2G Par au moins un opérateur Couverture population 2G Orange France Couverture surfacique 2G Bouygues Telecom Couverture surfacique 2G SFR Couverture surfacique 2G Free Mobile Couverture surfacique 2G Par au moins un opérateur Couverture surfacique 2G coordonnees
0 17510 17105 CHIVES 17 20.642005 355.32 0.00 0.0 0.0 0.0 ... 1.00 1.0 1.0 1.0 1.00 0.99 1.0 1.00 1.0 45.9508835378, -0.121918052323
1 17600 17112 LA CLISSE 17 4.896771 591.78 0.09 0.0 0.0 0.0 ... 1.00 1.0 1.0 1.0 1.00 1.00 1.0 1.00 1.0 45.7320429784, -0.75493581331
2 17150 17116 CONSAC 17 8.915911 239.74 0.00 0.0 0.0 0.0 ... 1.00 1.0 1.0 1.0 1.00 1.00 1.0 1.00 1.0 45.4182495786, -0.588917528415
3 17130 17118 CORIGNAC 17 10.850210 358.69 0.00 0.0 0.0 0.0 ... 0.63 1.0 1.0 1.0 0.99 0.85 1.0 0.99 1.0 45.2448604983, -0.39280322021
4 17330 17124 COURANT 17 15.586269 371.00 0.00 0.0 0.0 0.0 ... 1.00 1.0 1.0 1.0 1.00 1.00 1.0 1.00 1.0 46.0375312829, -0.587834905531

5 rows × 37 columns

Transfert des 7 colonnes singulières vers l'index

In [8]:
new_index = list(df2.columns.values[0:6]) + [df2.columns.values[-1]]
df2.set_index(new_index, inplace=True)
df2.head()
Out[8]:
Orange France Couverture population 4G Bouygues Telecom Couverture population 4G SFR Couverture population 4G Free Mobile Couverture population 4G Par au moins un opérateur Couverture population 4G Orange France Couverture surfacique 4G Bouygues Telecom Couverture surfacique 4G SFR Couverture surfacique 4G Free Mobile Couverture surfacique 4G Par au moins un opérateur Couverture surfacique 4G ... Orange France Couverture population 2G Bouygues Telecom Couverture population 2G SFR Couverture population 2G Free Mobile Couverture population 2G Par au moins un opérateur Couverture population 2G Orange France Couverture surfacique 2G Bouygues Telecom Couverture surfacique 2G SFR Couverture surfacique 2G Free Mobile Couverture surfacique 2G Par au moins un opérateur Couverture surfacique 2G
CODE POSTAL CODE INSEE NOM COMMUNE CODE DEPARTEMENT SURFACE COMMUNE (km²) POPULATION COMMUNE coordonnees
17510 17105 CHIVES 17 20.642005 355.32 45.9508835378, -0.121918052323 0.00 0.0 0.0 0.0 0.00 0.0 0.0 0.0 0.0 0.0 ... 1.0 1.00 1.0 1.0 1.0 1.00 0.99 1.0 1.00 1.0
17600 17112 LA CLISSE 17 4.896771 591.78 45.7320429784, -0.75493581331 0.09 0.0 0.0 0.0 0.09 0.2 0.0 0.0 0.0 0.2 ... 1.0 1.00 1.0 1.0 1.0 1.00 1.00 1.0 1.00 1.0
17150 17116 CONSAC 17 8.915911 239.74 45.4182495786, -0.588917528415 0.00 0.0 0.0 0.0 0.00 0.0 0.0 0.0 0.0 0.0 ... 1.0 1.00 1.0 1.0 1.0 1.00 1.00 1.0 1.00 1.0
17130 17118 CORIGNAC 17 10.850210 358.69 45.2448604983, -0.39280322021 0.00 0.0 0.0 0.0 0.00 0.0 0.0 0.0 0.0 0.0 ... 1.0 0.63 1.0 1.0 1.0 0.99 0.85 1.0 0.99 1.0
17330 17124 COURANT 17 15.586269 371.00 46.0375312829, -0.587834905531 0.00 0.0 0.0 0.0 0.00 0.0 0.0 0.0 0.0 0.0 ... 1.0 1.00 1.0 1.0 1.0 1.00 1.00 1.0 1.00 1.0

5 rows × 30 columns

Création d'une hiérarchie avec les colonnes répétées

In [9]:
df2.columns = [["Population"] * 15 + ["Surfacique"] * 15,\
              (["4G"] * 5 + ["3G"] * 5 + ["2G"] * 5) * 2,\
              ["Orange", "Bouygues", "SFR", "Free", "Any"] * 6]
df2.columns.names = ["Couverture", "Technologie", "Opérateur"]
df2.head()
Out[9]:
Couverture Population ... Surfacique
Technologie 4G 3G ... 3G 2G
Opérateur Orange Bouygues SFR Free Any Orange Bouygues SFR Free Any ... Orange Bouygues SFR Free Any Orange Bouygues SFR Free Any
CODE POSTAL CODE INSEE NOM COMMUNE CODE DEPARTEMENT SURFACE COMMUNE (km²) POPULATION COMMUNE coordonnees
17510 17105 CHIVES 17 20.642005 355.32 45.9508835378, -0.121918052323 0.00 0.0 0.0 0.0 0.00 0.0 0.0 0.0 0.0 0.0 ... 1.0 1.00 1.0 1.0 1.0 1.00 0.99 1.0 1.00 1.0
17600 17112 LA CLISSE 17 4.896771 591.78 45.7320429784, -0.75493581331 0.09 0.0 0.0 0.0 0.09 0.2 0.0 0.0 0.0 0.2 ... 1.0 1.00 1.0 1.0 1.0 1.00 1.00 1.0 1.00 1.0
17150 17116 CONSAC 17 8.915911 239.74 45.4182495786, -0.588917528415 0.00 0.0 0.0 0.0 0.00 0.0 0.0 0.0 0.0 0.0 ... 1.0 1.00 1.0 1.0 1.0 1.00 1.00 1.0 1.00 1.0
17130 17118 CORIGNAC 17 10.850210 358.69 45.2448604983, -0.39280322021 0.00 0.0 0.0 0.0 0.00 0.0 0.0 0.0 0.0 0.0 ... 1.0 0.63 1.0 1.0 1.0 0.99 0.85 1.0 0.99 1.0
17330 17124 COURANT 17 15.586269 371.00 46.0375312829, -0.587834905531 0.00 0.0 0.0 0.0 0.00 0.0 0.0 0.0 0.0 0.0 ... 1.0 1.00 1.0 1.0 1.0 1.00 1.00 1.0 1.00 1.0

5 rows × 30 columns

Visualisation de l'index hiérarchique

In [10]:
t, ts = make_tree(df2.columns)
t.render("%%inline", tree_style=ts)
Out[10]:

Mise à plat des données

A nouveau, après mise à plat, les données peuvent être représentées sous la forme d’une table à 11 colonnes seulement : codes postal et INSEE, commune, département, surface, population, coordonnées, type, technologie, opérateur et valeur.

In [11]:
# noms des colonnes hiérarchiques
current_columns = df2.columns.names
# noms des colonnes de la table finale
new_columns = df2.index.names + current_columns + ["Value"]
# empilement des colonnes hiérarchiques vers l'index puis annulation de l'index
df2 = df2.stack(current_columns).reset_index()
df2.columns = new_columns
df2.head()
Out[11]:
CODE POSTAL CODE INSEE NOM COMMUNE CODE DEPARTEMENT SURFACE COMMUNE (km²) POPULATION COMMUNE coordonnees Couverture Technologie Opérateur Value
0 17510 17105 CHIVES 17 20.642005 355.32 45.9508835378, -0.121918052323 Population 2G Any 1.00
1 17510 17105 CHIVES 17 20.642005 355.32 45.9508835378, -0.121918052323 Population 2G Bouygues 0.44
2 17510 17105 CHIVES 17 20.642005 355.32 45.9508835378, -0.121918052323 Population 2G Free 0.18
3 17510 17105 CHIVES 17 20.642005 355.32 45.9508835378, -0.121918052323 Population 2G Orange 0.97
4 17510 17105 CHIVES 17 20.642005 355.32 45.9508835378, -0.121918052323 Population 2G SFR 1.00

N.B. : la séparation de la latitude et de la longitude de la colonne « coordonnées » n’est pas traitée dans cet exemple.