Tech - De l'importance de vérifier ses données

Comment perdre du temps avec des données de mauvaises qualités bien qu'elles proviennent de sources sûres

Introduction

Afin de réaliser un de nos tutoriels (Le problème du voyageur de commerce) nous avons eu besoin de récupérer les coordonnées des communes françaises. La première source de données que nous avons trouvé sur Internet se trouve sur le site data.gouv.fr et nous n'avions aucune raison de douter de la qualité des données fournies. Ce fut une très grave erreur et une grosse perte de temps. Le tutoriel présente un algorithme que nous avons construit en javascript. Avec les difficultés rencontrées pour l'affiner il nous a fallu faire du pas à pas et rentrer dans le détail. C'est là que nous nous sommes rendu compte que les données que nous avions récupérées étaient de mauvaises qualités, 2 communes pouvant avoir les mêmes coordonnées par exemple, d'autres étaient manquantes. Ce tutoriel présente comment analyser des données afin de s'assurer qu'elles soient complètes et cohérentes. Pour cela nous allons utiliser le langage R qui est très efficace pour ce genre d'analyses bien qu'il soit possible de faire la même chose avec Excel.

Analyses standards

Nous commençons par effectuer une analyse sur la nature des données récupérées et leur complétude. Les données que nous avions au départ proviennent du site data.gouv.fr. Le fichier CSV peut aussi être récupéré sur notre site eucircos_regions_departements_circonscriptions_communes_gps.zip. Pour le charger nous utilisons le très performant package data.table, ce n'est pas nécessaire ici vu le nombre de lignes limitées mais autant partir avec les meilleures bases. Il nous faut juste préciser l'encodage du fichier. On affiche ensuite les premières lignes de nos données.

> require(data.table)
> DT <- fread("c:/temp/eucircos_regions_departements_circonscriptions_communes_gps.csv", encoding = "UTF-8")
> head(DT)
  EU_circo code_région  nom_région chef.lieu_région numéro_département nom_département      préfecture
1  Sud-Est          82 Rhône-Alpes             Lyon                 01             Ain Bourg-en-Bresse
2  Sud-Est          82 Rhône-Alpes             Lyon                 01             Ain Bourg-en-Bresse
3  Sud-Est          82 Rhône-Alpes             Lyon                 01             Ain Bourg-en-Bresse
4  Sud-Est          82 Rhône-Alpes             Lyon                 01             Ain Bourg-en-Bresse
5  Sud-Est          82 Rhône-Alpes             Lyon                 01             Ain Bourg-en-Bresse
6  Sud-Est          82 Rhône-Alpes             Lyon                 01             Ain Bourg-en-Bresse
  numéro_circonscription          nom_commune codes_postaux code_insee  latitude longitude éloignement
1                      1             Attignat         01340       1024 46.283333  5.166667        1.21
2                      1             Beaupont         01270       1029      46.4  5.266667        1.91
3                      1                 Bény         01370       1038 46.333333  5.283333        1.51
4                      1            Béreyziat         01340       1040 46.366667      5.05        1.71
5                      1 Bohas-Meyriat-Rignat         01250       1245 46.133333       5.4        1.01
6                      1      Bourg-en-Bresse         01000       1053      46.2  5.216667        1.00

On supprime ensuite les colonnes qui nous sont inutiles.

> DT[, c("EU_circo","code_région", "nom_région", "chef-lieu_région", "préfecture", "numéro_circonscription", "éloignement"):=NULL]
> head(DT)
   numéro_département nom_département          nom_commune codes_postaux code_insee  latitude longitude
1:                 01             Ain             Attignat         01340       1024 46.283333  5.166667
2:                 01             Ain             Beaupont         01270       1029      46.4  5.266667
3:                 01             Ain                 Bény         01370       1038 46.333333  5.283333
4:                 01             Ain            Béreyziat         01340       1040 46.366667      5.05
5:                 01             Ain Bohas-Meyriat-Rignat         01250       1245 46.133333       5.4
6:                 01             Ain      Bourg-en-Bresse         01000       1053      46.2  5.216667
    

On peut commencer à voir l'intérêt du language R avec la fonction summary

> summary(DT)
 numéro_département nom_département    nom_commune        codes_postaux        code_insee      latitude          longitude        
 Length:36840       Length:36840       Length:36840       Length:36840       Min.   : 1001   Length:36840       Length:36840      
 Class :character   Class :character   Class :character   Class :character   1st Qu.:24577   Class :character   Class :character  
 Mode  :character   Mode  :character   Mode  :character   Mode  :character   Median :48191   Mode  :character   Mode  :character  
                                                                             Mean   :46298                                        
                                                                             3rd Qu.:67043                                        
                                                                             Max.   :97617

Il y a un souci sur la latitude et la longitude qui sont reconnues comme des caractères. De la même façon le code INSEE est reconnu comme un nombre alors qu'il s'agit de caractères. Nous effectuons les conversions nécessaires.

> DT <- DT[, latitude:=as.numeric(latitude)]
> DT <- DT[, longitude:=as.numeric(longitude)]
> DT <- DT[, code_insee:=as.character(code_insee)]
> summary(DT)
 numéro_département nom_département    nom_commune        codes_postaux       code_insee           latitude       longitude      
 Length:36840       Length:36840       Length:36840       Length:36840       Length:36840       Min.   :41.39   Min.   :-4.7667  
 Class :character   Class :character   Class :character   Class :character   Class :character   1st Qu.:45.22   1st Qu.: 0.6833  
 Mode  :character   Mode  :character   Mode  :character   Mode  :character   Mode  :character   Median :47.43   Median : 2.6167  
                                                                                                Mean   :47.00   Mean   : 2.7324  
                                                                                                3rd Qu.:48.85   3rd Qu.: 4.8500  
                                                                                                Max.   :51.08   Max.   : 9.5167  
                                                                                                NA's   :2962    NA's   :3024  

C'est avec cette dernière commande summary que tout l'intérêt de R se révèle. Elle permet de caractériser chacune des colonnes du fichier. Ainsi les 5 premières colonnes sont renseignées pour chaque ligne. En revanche la colonne latitude contient 2962 NA's (qui signifie Not Available). De la même façon la colonne longitude possède 3024 NA's et étrangement ce n'est pas le même nombre. Nous terminons ici cette première analyse de surface. Elle nous a déjà permise de constater que les données récupérées ne sont pas de très bonnes qualités, certaines communes (3000 quand même, sur 36800) ne possèdent pas de coordonnées géographiques, soit presque 10%.

Nous verrons dans de futurs tutoriels sur l'analyse de données toute la puissance de R concernant l'analyse de données numériques. Les coordonnées sont bien des nombres mais ça ne ferait aucun sens d'étudier leur moyennes, distributions ou de faire des corrélations avec le nom des villes par exemple.

Analyses spécifiques

Nous poursuivons en nous concentrant de manière spécifique sur la latitude et la longitude, la fonction summary nous fournit d'autres informations intéressantes : les valeurs Min. et Max. de ces deux colonnes. Elles peuvent nous permettre de dessiner un rectangle par-dessus la France comme dans la carte ci-dessous.

Dans l'ensemble on encadre plutôt correctement la France métropolitaine (ce qui signifie qu'il n'y a pas de données pour les DOM-TOM par ailleurs). Mais un problème au niveau du Finistère se manifeste déjà. On peut être sûr que Ouessant n'est pas référencée ou ne possède pas de coordonnées.

> DT[DT$nom_commune == 'Ouessant']
   numéro_département nom_département nom_commune codes_postaux code_insee latitude longitude
1:                 29       Finistère    Ouessant         29242      29155       NA        NA

La commune est bien référencée mais sans latitude et longitude. Avec 3000 coordonnées manquantes on s'en doutait bien, mais on penchait plutôt pour de grosses villes comme Paris avec ses quartiers.

> DT[DT$nom_commune == 'Paris']
    numéro_département nom_département nom_commune           codes_postaux code_insee latitude longitude
 1:                 75           Paris       Paris 75001 75002 75003 75004      75056       NA        NA
 2:                 75           Paris       Paris             75005 75006      75056       NA        NA
 3:                 75           Paris       Paris             75006 75007      75056       NA        NA
 4:                 75           Paris       Paris             75008 75009      75056       NA        NA
 5:                 75           Paris       Paris                   75010      75056       NA        NA
 6:                 75           Paris       Paris             75011 75020      75056       NA        NA
 7:                 75           Paris       Paris             75011 75012      75056       NA        NA
 8:                 75           Paris       Paris                   75012      75056       NA        NA
 9:                 75           Paris       Paris                   75013      75056       NA        NA
10:                 75           Paris       Paris             75013 75014      75056       NA        NA
11:                 75           Paris       Paris                   75014      75056       NA        NA
12:                 75           Paris       Paris                   75015      75056       NA        NA
13:                 75           Paris       Paris                   75015      75056       NA        NA
14:                 75           Paris       Paris                   75016      75056       NA        NA
15:                 75           Paris       Paris                   75016      75056       NA        NA
16:                 75           Paris       Paris                   75017      75056       NA        NA
17:                 75           Paris       Paris             75017 75018      75056       NA        NA
18:                 75           Paris       Paris                   75018      75056       NA        NA
19:                 75           Paris       Paris             75018 75019      75056       NA        NA
20:                 75           Paris       Paris                   75019      75056       NA        NA
21:                 75           Paris       Paris                   75020      75056       NA        NA

C'est pas mal aussi sur Paris, 21 lignes mais 0 coordonnée. On apprend également que certaines lignes sont complètement en double comme les lignes 12 et 13 (c'est en fait la colonne numéro_circonscription supprimée au début qui les différencie). Comme l'usage que nous faisons de ces données nécessitent la présence des coordonnées, nous supprimons toutes les lignes qui possèdent au moins un NA.

> DT <- na.omit(DT)
> summary(DT)
 numéro_département nom_département    nom_commune        codes_postaux       code_insee           latitude       longitude      
 Length:33814       Length:33814       Length:33814       Length:33814       Length:33814       Min.   :41.39   Min.   :-4.7667  
 Class :character   Class :character   Class :character   Class :character   Class :character   1st Qu.:45.22   1st Qu.: 0.6833  
 Mode  :character   Mode  :character   Mode  :character   Mode  :character   Mode  :character   Median :47.45   Median : 2.6167  
                                                                                                Mean   :47.00   Mean   : 2.7324  
                                                                                                3rd Qu.:48.85   3rd Qu.: 4.8500  
                                                                                                Max.   :51.08   Max.   : 9.5167

Poursuivons en vérifiant que tous les couples (latitude, longitude) sont bien distincts. A priori différentes communes doivent posséder des coordonnées différentes.

> dupe = DT[,c('latitude','longitude')]
> dupeDT <- DT[duplicated(dupe) | duplicated(dupe, fromLast=TRUE),]
> dupeDT <- dupeDT[order(dupeDT$latitude)]
> head(dupeDT, n = 20)
    numéro_département nom_département            nom_commune codes_postaux code_insee latitude longitude
 1:                 2A    Corse-du-Sud                Fozzano         20143      20118 41.70000  9.000000
 2:                 2A    Corse-du-Sud Santa-Maria-Figaniella         20143      20310 41.70000  9.000000
 3:                 2A    Corse-du-Sud               Cargiaca         20164      20066 41.71667  9.050000
 4:                 2A    Corse-du-Sud      Loreto-di-Tallano         20165      20146 41.71667  9.050000
 5:                 2A    Corse-du-Sud     Serra-di-Scopamène         20127      20278 41.75000  9.100000
 6:                 2A    Corse-du-Sud             Sorbollano         20152      20285 41.75000  9.100000
 7:                 2A    Corse-du-Sud              Guargualé         20128      20132 41.83333  8.933333
 8:                 2A    Corse-du-Sud             Urbalacone         20128      20331 41.83333  8.933333
 9:                 2A    Corse-du-Sud           Cardo-Torgia         20190      20064 41.86667  8.983333
10:                 2A    Corse-du-Sud      Santa-Maria-Siché         20190      20312 41.86667  8.983333
11:                 2B     Haute-Corse Santo-Pietro-di-Venaco         20250      20315 42.23333  9.166667
12:                 2B     Haute-Corse                 Venaco         20231      20341 42.23333  9.166667
13:                 2B     Haute-Corse                  Campi         20270      20053 42.26667  9.416667
14:                 2B     Haute-Corse                  Moïta         20270      20161 42.26667  9.416667
15:                 2B     Haute-Corse        Canale-di-Verde         20230      20057 42.28333  9.466667
16:                 2B     Haute-Corse                Chiatra         20230      20088 42.28333  9.466667
17:                 2B     Haute-Corse     Santa-Maria-Poggio         20221      20311 42.33333  9.500000
18:                 2B     Haute-Corse     Valle-di-Campoloro         20221      20335 42.33333  9.500000
19:                 2B     Haute-Corse             Piedicroce         20229      20219 42.36667  9.350000
20:                 2B     Haute-Corse           Piedipartino         20229      20221 42.36667  9.350000

Le tableau obtenu possède 2168 entrées et comme vous pouvez le voir avec les premières lignes de nombreuses communes possèdent les mêmes coordonnées. Est-ce normal ? Nous pouvons le vérifier en confrontant nos données à celles fournies par un autre service. En utilisant l'API de geocoding de Google (API Google) avec les deux premières villes. Pour Fozzano nous obtenons (41.69947, 9.001941) et pour Santa-Maria-Figaniella (41.715337, 9.001842). Les coordonnées sont certes proches mais les deux villes sont distantes d'environ 1 km. Nous pourrions nous intéresser à la définition du centre d'une commune mais ce qui est sûr c'est que peu importe la définition il n'y a aucune raison de trouver des doublons dans un jeu de données. Cela indique soit des arrondis trop fort soit des données mal construites.

Etudier la source des données

Sur le site data.gouv.fr il est indiqué que les données proviennent du travail de http://www.galichon.com/codesgeo/. Il serait peut-être bon d'aller y jeter un oeil. On arrive sur une page qui propose plusieurs informations dont le référentiel des communes avec les coordonnées. On note aussi dans le bas de page un copyright 2000 - 2013 ce qui n'est pas très bon signe. Une page Avertissement attire notre attention. On peut y relever les deux blocs de texte suivant :

Les coordonnées géographiques sont exprimées dans le système géodésique international WGS84 et arrondies à la minute la plus proche. Celles disponibles sur le site de l'IGN sont exprimées dans le système ED50 et sont données avec une précision d'une seconde. La formule pour passer d'un système à l'autre n'est pas triviale mais étant donné les arrondis cités précédemment, on peut considérer que les deux systèmes ne font qu'un (l'écart entre les deux systèmes étant de l'ordre de 5 à 15 secondes). Pour plus d'information (en anglais), je vous conseille de visiter la page "Geographic and Projected Coordinate System Transformations" disponible sur http://www.petroconsultants.com/epsg/guid7.html.

Sources des données des coordonnées géographiques : GEOnet Names Server (http://www.nima.mil/gns/html/cntry_files.html) base mise à jour le 4 avril 2002

Sans rentrer dans les détails techniques on apprend que des arrondis ont été réalisés et que la base remonte à 2002 ! data.gouv.fr ne le mentionne pas directement alors qu'il s'agit pourtant d'informations importantes.

Les données de La Poste

Après avoir perdu beaucoup de temps avec les données de data.gouv.fr nous avons trouvé un jeu de données fourni par La Poste et disponible sur ce site. L'accueil est déjà bien plus agréable, le site repose sur un framework PHP justement dédié au partage de données. Les différents onglets nous permettent facilement de voir les données fournies, leur couverture spatiale (et cette fois il y a les DOM-TOM) et la page d'accueil présente les principales informations. On peut relever qu'il n'est pas fait mention de la colonne coordonnees_gps. Nous pouvons refaire notre analyse sur le fichier exporté en CSV (laposte_hexasmal.zip).

> require(data.table)
> DT <- fread("c:/temp/laposte_hexasmal.csv", encoding = "UTF-8")
> head(DT)
   Code_commune_INSEE         Nom_commune Code_postal Libelle_acheminement Ligne_5              coordonnees_gps
1:              80355         FRESNEVILLE       80140          FRESNEVILLE           49.9469630616, 1.753960976
2:              80365            FRICAMPS       80290             FRICAMPS         49.7720118421, 1.95186211928
3:              80368 FRIVILLE ESCARBOTIN       80130  FRIVILLE ESCARBOTIN         50.0912781795, 1.52364516053
4:              80379               GLISY       80440                GLISY         49.8341850031, 2.39954269272
5:              80387        GRATTEPANCHE       80680         GRATTEPANCHE         49.8142899245, 2.29952854065
6:              80393               GRUNY       80700                GRUNY         49.7015900422, 2.77539756139
> DT[, c("latitude", "longitude") := tstrsplit(coordonnees_gps, ", ", fixed=TRUE)]
> DT[, c("Ligne_5", "coordonnees_gps"):=NULL]
> DT <- DT[, Code_postal:=as.character(Code_postal)]
> DT[, Code_postal:=str_pad(Code_postal, 5, "left", "0")]
> DT <- DT[, latitude:=as.numeric(latitude)]
> DT <- DT[, longitude:=as.numeric(longitude)]
> summary(DT)
 Code_commune_INSEE Nom_commune        Code_postal        Libelle_acheminement    latitude        longitude       
 Length:39201       Length:39201       Length:39201       Length:39201         Min.   :-21.34   Min.   :-61.7800  
 Class :character   Class :character   Class :character   Class :character     1st Qu.: 45.14   1st Qu.:  0.6835  
 Mode  :character   Mode  :character   Mode  :character   Mode  :character     Median : 47.39   Median :  2.6841  
                                                                               Mean   : 46.70   Mean   :  2.7568  
                                                                               3rd Qu.: 48.83   3rd Qu.:  4.9798  
                                                                               Max.   : 51.07   Max.   : 55.7545  
                                                                               NA's   :267      NA's   :267 

Par rapport à l'analyse précédente nous avons juste dû séparer la colonne coordonnees_gps en deux colonnes latitude et longitude et fait en sorte que tous les codes postaux aient bien 5 caractères. Après appel à la fonction summary on constate que le fichier possède 39201 lignes (contre 36800 pour celui data.gouv.fr) mais qu'il existe encore des NA's pour la latitude et la longitude (au moins ici leur nombre est identique). Remarquons aussi que les Min. et Max. sont bien différents puisque les données incluent les DOM-TOM. Nous avons analysé les NA's qui correspondent uniquement à la Polynésie Française et à des îles comme Saint-Martin ou Saint-Barthélémy. Ca peut être gênant pour certains usages mais ça ne l'est pas dans notre contexte.

Si nous avons cherché un autre référentiel que celui de data.gouv.fr c'est qu'il n'était pas satisfaisant pour le département des Hauts-de-Seine comme vous pouvez le voir dans la carte ci-dessous. A côté celui de La Poste est bien complet pour ce département et nous avons également présenté les données renvoyées par l'API de geocoding de Google.

Si l'on considère les données en rouge de data.gouv.fr, on constate que certaines villes sont manquantes (Asnière-Sur-Seine et Courbevoie), plusieurs sont placées en dehors du polygone de la commune (Rueil-Malmaison par exemple) et enfin d'autres possèdent les mêmes coordonnées (Vaucresson et Marnes-La-Coquette ou bien Fontenay-Aux-Roses et Sceaux). Les deux autres jeux de données (Google et La Poste) possèdent des différences mais sont dans l'ensemble bien meilleurs et ne contiennent pas les erreurs de celles de data.gouv.fr. Par simplicité et parce que le référentiel est directement compilé, nous avons retenu celui de La Poste. Pour Google, il faut utiliser leur API et récupérer une à une les coordonnées des communes de France, ce qui avec les limitations imposées par l'API peut prendre un peu de temps.

A ce moment-là on s'est dit qu'on avait trouvé le bon référentiel mais c'était avant d'en faire l'analyse des doublons.

> DT <- na.omit(DT)
> DT <- unique(DT, by = "Code_commune_INSEE")
> dupe = DT[,c('latitude','longitude')]
> dupeDT <- DT[duplicated(dupe) | duplicated(dupe, fromLast=TRUE),]
> dupeDT <- dupeDT[order(dupeDT$latitude)]
> nrow(dupeDT)
[1] 34061

Vous ne rêvez pas, les données fournies par La Poste contiennent plus de 35923 lignes avec un code INSEE différent mais 34061 d'entres-elles ont leurs coordonnées géographiques dupliquées au moins une fois. Nous avons arrêté de sourire en regardant le département de la Meuse dont les données ont été extraites avec le code suivant.

> DT55 <- DT[substr(Code_postal, 1, 2) == "55", ]
> fwrite(DT55, "c:/temp/error-demo-55.csv")

Il y a plus de 500 communes dans la Meuse mais seulement 29 coordonnées géographiques différentes dans le référentiel de La Poste. On peut les retrouver en vert dans la carte ci-dessus avec le nombre de communes ayant les mêmes coordonnées. A côté en rouge on retrouve les données de data.gouv.fr qui semble d'une incroyable précision du coup même s'il manque beaucoup de communes.

Mise à jour La Poste

Suite à un commentaire que nous avons laissé sur leur site les données fournies par La Poste ont été mises à jour, il y avait eu une erreur dans le traitement des coordonnées géographiques. Les nouvelles données sont ici : laposte_hexasmal_update.zip. Reprenons toute l'analyse avec ces nouvelles données.

> require(data.table)
> DT <- fread("c:/temp/laposte_hexasmal_update.csv", encoding = "UTF-8")
> # On refait tout le traitement jusqu'à l'affichage du résumé
> summary(DT)
Code_commune_INSEE Nom_commune        Code_postal        Libelle_acheminement    latitude        longitude       
 Length:39201       Length:39201       Length:39201       Length:39201         Min.   :-21.34   Min.   :-61.7796  
 Class :character   Class :character   Class :character   Class :character     1st Qu.: 45.13   1st Qu.:  0.6662  
 Mode  :character   Mode  :character   Mode  :character   Mode  :character     Median : 47.38   Median :  2.6871  
                                                                               Mean   : 46.70   Mean   :  2.7569  
                                                                               3rd Qu.: 48.82   3rd Qu.:  4.9811  
                                                                               Max.   : 51.07   Max.   : 55.7545  
                                                                               NA's   :269      NA's   :269 
> DT <- na.omit(DT)
> DT <- unique(DT, by = "Code_commune_INSEE")
> dupe = DT[,c('latitude','longitude')]
> dupeDT <- DT[duplicated(dupe) | duplicated(dupe, fromLast=TRUE),]
> nrow(dupeDT)
[1] 0

Nous retrouvons 269 NA's, 2 de plus qu'avant la mise à jour. En revanche après avoir enlevé les codes INSEE en double il reste 35921 communes dont aucune ne partage les mêmes coordonnées !

Le changement est logiquement radical. Toutes les communes existent et possèdent des coordonnées pour chacune des cellules. Il manque uniquement la commune de Culey (code INSEE 55138). Dans tous les cas on ne peut que saluer la réactivité de La Poste et la qualité des données qu'ils proposent.

Synthèse

Nous venons de voir ensemble comment analyser un jeu de données de manière générale (valeurs manquantes, Min et Max...) et de manière spécifique (en se concentrant sur les coordonnées). Il en ressort qu'aucun des jeux de données étudiés ne peut être considéré comme complet ni fiable. La Poste a fait des progrès considérables suite à un commentaire que nous avons laissé mais il manque toujours certaines communes et nous n'avons pas étudié dans le détail les autres départements. Si l'on veut utiliser ces données pour la présence de coordonnées géographiques, il faut simplement y renoncer ou bien faire une analyse précise sur les données de La Poste qui sont de bonnes qualités malgré tout. Dans notre cas et pour le tutoriel cité en introduction nous avons préféré partir sur le centroid du polygone de chaque commune.

En réalité nous avons à peine commencer notre analyse. Concernant spécifiquement les communes nous devons aussi analyser le fichier geoJSON, est-il correct ? Chaque année des communes sont créées, d'autres disparaissent par fusion. A quelle année correspond ce geoJSON ? Et du coup à quelle année correspond la liste des communes que nous avons récupérée ? Comment un jeu de données définit le centre d'une commune (église, mairie...) ? Nous avons vu dans un précédent tutoriel (Optimisation du geoJSON) qu'il est possible de réduire la taille de nos fichiers geoJSON, celui que nous avons récupéré est-il conforme à notre usage ? Peu de sites font cas de ces problèmes mais dans un contexte professionnel il est important de se poser ces questions. Elles feront l'objet de futurs tutoriels dans la section TECH.

COMMENTAIRES

Etienne


Simplement merci de partager ces informations.

ericfrigot


Recevoir un 25 décembre un tel message ! Merci, ça fait vraiment plaisir, j'avais bien galéré sur ce sujet.