Rechercher dans le blog

Téléchargement automatique, préparation de données, manipulation des SeDF (spatially enabled dataframes) et publication avec l'API Python d'ArcGIS

Dans ce cinquième tutoriel de le série de l'été consacrée à la prise en main de l'API Python d'ArcGIS, nous allons voir ensemble comment tirer parti de l'API pour réaliser des opérations de préparation des données. Nous verrons plusieurs opérations sur les DataFrames pandas, qui permettent de manipuler les données de manière très efficace. Pour gérer les données spatiales, nous apprendrons à utiliser les Spatially enabled DataFrames, à les utiliser pour de l'analyse spatiale et à les publier en tant que feature layer sur notre portail. 
 
Comme chaque semaine, vous pouvez suivre le tutoriel ci-dessous pas à pas pour obtenir les explications des différentes étapes, et écrire le code par vous-même ou retrouver le code complet et commenté sur mon Github ou alors le récupérer via cet item ArcGIS
Notez qu'un compte Creator ou supérieur avec des privilèges de publication de contenu sont nécessaires. Vous pouvez exécuter ce Notebook directement dans ArcGIS Notebooks sur ArcGIS Online (disponible par défaut si votre administrateur vous a accordé les droits nécessaires), dans ArcGIS Enterprise (disponible avec le rôle serveur correspondant) ou en le téléchargeant et en l'ouvrant sur ArcGIS Pro (plus d'infos dans la doc). Vous pouvez également l'exécuter dans n'importe quel environnement d'exécution de votre choix, mais veillez dans ce cas à avoir installé les packages nécessaires.

1 - Connexion au GIS et import des bibliothèques

Nous commençons par importer les différentes bibliothèques nécessaires, puis par nous connecter au SIG. Pour cela, nous importons l'objet GIS du module gis, qui représente notre organisation. La connexion est ensuite à adapter selon votre environnement et votre méthode d'authentification. Voir l'aide en ligne pour plus d'informations.Si vous êtes connectés en tant qu'admin, la cellule va vous renvoyer un message rouge pour vous prévenir qu'il faut faire attention aux opérations que vous utilisez. Cela n'empêche pas votre code de s'exécuter correctement.
Si votre environnement n'est pas directement connecté à votre SIG, vous devrez lui passer vos informations d'authentification via la cellule suivante, que vous devez bien sûr mettre à jour avec vos propres informations :

2 - Téléchargement des données

Pour ce tutoriel, nous allons utiliser les données d'historique journalier du compteur de trafic routier de Bordeaux Métropole auxquelles nous accéderons via datagouv. Ces données sont mises à jour quotidiennement, et contiennent le comptage du nombre de véhicules enregistrés par les différents capteurs placés sur la chaussée à différents endroits de la métropole. 
 

2a - Récupération de la date

Les données mises à jour sont accessibles depuis datagouv durant la journée du lendemain de leur collecte. Afin d'éviter toute erreur selon l'heure à laquelle vous testez ce tutoriel, nous utiliserons pour cet exercice les données d'avant-hier.
Nous pouvons récupérer la date d'avant-hier grâce à la bibliothèque datetime et à ses fonctions datetime.now()et timedelta(). Avec strftime(), nous la formatons selon la forme suivante : AAAA-MM-JJ (ex : 2024-07-04 pour le 4 juillet 2024) car c'est sous ce format que les dates sont enregistrées dans les données auxquelles nous allons accéder.

2b - Création d'un dossier cible et téléchargement des données

Dans la cellule suivante, nous définissons une fonction qui nous permet de créer un dossier et qui a comme paramètre le nom du dossier que nous allons créer.
Nous appelons la fonction en passant en argument le nom que nous souhaitons donner à notre dossier, qui va venir ici chercher dynamiquement la date d'avant-hier récupérée ci-dessus.
Si vous utilisez ArcGIS Notebooks, notez que cette fonction vient des extraits de code organisés par Esri auxquels vous avez accès dans votre environnement :
Le dossier est maintenant créé et prêt à accueillir nos données. Je créée une nouvelle fonction qui me permet de télécharger des données à partir d'un url. Je récupère pour cela l'adresse à laquelle je retrouve le CSV des données de comptage sur datagouv :
Après exécution de la fonction, je peux retrouver la donnée téléchargée directement dans le dossier que j'ai créé juste avant. 
L'URL à laquelle les données sont disponibles étant stable, ce genre de fonction peut être utilisée pour automatiser la mise à jour quotidienne de données et d'applications. Nous verrons ce genre de workflow plus en détail la semaine prochaine.
En attendant, je vous propose de regarder un peu plus en détail ce que contiennent les données et comment nous pouvons les nettoyer et les préparer avant de les publier sur notre portail.

3 - Lecture en DataFrame pandas

La donnée que nous avons téléchargée est au format CSV. Nous allons désormais la convertir en DataFrame, car cette structure de données offre plusieurs avantages en termes de facilité de manipulation et de transformation des données.
Nous utilisons pour cela la fonction read_csv de la bibliothèque pandas (que nous avions importée dans la première cellule de ce Notebook), puis nous affichons le DataFrame dans le Notebook afin de voir à quoi ressemblent nos données.

4 - Préparation et nettoyage des données

Nous allons maintenant voir ensemble plusieurs opérations sur les DataFrames afin de préparer les données avant leur publication.

 

4a - Suppression des valeurs manquantes (NaN)

Une première bonne pratique en terme de nettoyage de données est de supprimer les valeurs manquantes (NaN), qui peuvent générer plusieurs problèmes lorsque vous souhaiterez analyser vos données. Pour cela, pandas possède la méthode dropna() :
Vous devriez maintenant avoir un nombre d'enregistrements légèrement inférieur.
 

4b - Renommage des colonnes et suppression des colonnes inutiles

Le jeu de données que nous utilisons possède des noms de colonnes très propres : pas d'espace, pas d'accents, pas de caractères spéciaux ou exotiques. C'est très important, car sinon vous pourriez rencontrer plusieurs problèmes une fois vos données publiées. Cependant, ce n'est malheureusement pas toujours le cas. Je vais donc vous montrer comment renommer les colonnes, ici en donnant des nom un peu plus parlants.
Pandas possède une fonction rename(), dans laquelle je passe simplement en argument un dictionnaire contenant en clés les noms actuels des colonnes que je souhaite renommer et en valeurs les nouveaux noms :
Lorsque l'on manipule des jeux de données de taille relativement importante, nous pouvons essayer de les réduire afin d'en améliorer les performances. Une manière très simple d'arriver à cela est de supprimer les colonnes dont nous n'avons pas besoin pour nos analyses ou pour identifier nos données.
Nous utilisons la fonction drop() afin de supprimer 3 colonnes de notre dataframe, dont nous passons les labels en argument de la fonction sous forme de liste. Le paramètre axis permet de choisir si l'on supprime des lignes (0) ou des colonnes (1).

4c - Filtre selon la date

La donnée que nous avons téléchargé contient tout un historique de données de comptage routier. Ce qui nous intéresse, c'est les données d'avant-hier. Nous allons donc filtrer notre dataframe en fonction d'un filtre sur la colonne date, où nous garderons uniquement les lignes pour lesquelles la colonne date est égale à la valeur contenue dans la variable date_av_hier calculée au début du Notebook :
Voici un petit peu plus d'explications sur comment fonctionne le filtrage d'un DataFrame : 
df_trafic_drop_colonnes['date'] == date_av_hier crée une série de valeurs booléennes (True ou False), où chaque élément est True si la valeur dans la colonne date est égale à la variable date_av_hier et False sinon. df_trafic_drop_colonnes[df_trafic_drop_colonnes['date'] == date_av_hier] utilise cette série de booléens pour filtrer df_trafic_drop_colonnes, ne gardant que les lignes où la condition est True. En d'autres termes, seules les lignes dont la date correspond à date_av_hier sont conservées.
Nous assignons ce DataFrame filtré dans la nouvelle variable df_av_hier.
 

4d - Tri par valeur décroissante

Enfin, nous allons trier notre dataframe selon des valeurs de comptage décroissante, de manière à faire apparaître en premier les capteurs ayant enregistré le plus de trafic. Nous utilisons la méthode sort_values en indiquant la colonne sur laquelle nous appliquons le tri, ainsi que l'ordre de tri (ascending = False permet d'obtenir un ordre décroissant).
Je me permets aussi d'imprimer une fraction du DataFrame, ici uniquement les 5 premiers enregistrements grâce à la méthode head() (à l'inverse, tail() permet d'imprimer uniquement les 5 derniers).
Nos données sont maintenant prêtes pour la suite !

5 - Transformation d'un DataFrame en Spatially enabled DataFrame

Alors pour l'instant, tout ce qu'on a vu pourrait aussi bien s'appliquer à des géographiques qu'à données de modélisation financière, et les vrais géographes parmi vous sont peut-être en train de ronger leur frein et de se demander quand est-ce que je vais enfin vous montrer des cartes. Respirez un coup, on y arrive !
Avec l'API Python d'ArcGIS, il est possible de transformer les DataFrames classiques en Spatially
Enabled Dataframes si ceux-ci possèdent des informations de latitude et de longitude. Cela va nous permettre ensuite d'utiliser pleinement la composante spatiale de nos données.
 

5a - Création de deux colonnes latitude et longitude

Afin de pouvoir transformer un df en sedf, nous devons pouvoir accéder à deux colonnes distinctes contenant la longitude et la latitude. Pour l'instant, nos données ne possèdent qu'une seule colonne stockant ces deux valeurs séparées par une virgule.
Nous allons transformer notre dataframe pour qu'il corresponde aux prérequis. La méthode str.split() permet de séparer la chaîne contenant latitude et longitude, et nous passons la virgule en argument afin qu'elle soit utilisée comme délimitateur. expand = True indique que chaque élément résultant de cette division (ici, la latitude et la longitude) doit être stocké dans une colonne séparée. Nous assignons ces valeurs à deux nouvelles colonnes se nommant latitude et longitude.
Les valeurs de latitude et de longitude doivent être de type numérique float, hors elles sont pour l'instant de type object (c'est en fait un string spécifique à pandas). Cela est facilement vérifiable en accédant à la propriété dtypes du DataFrame, qui renvoie le type de donnée de chaque colonne :
Heureusement, nous pouvons très facilement convertir le format avec la méthode astype() :
Nous pouvons vérifier que cela a bien fonctionné :

5b - Transformation en SeDF

Notre DataFrame est prêt à être converti en Spatial enabled Dataframe. Dans l'API Python d'ArcGIS, tout ce qui touche aux SeDF est géré par la classe GeoAccessor, à laquelle nous accédons dans le code avec le point d'accès .spatial.
Nous utilisons la méthode from_xy() du GeoAccessor, dans laquelle nous passons en argument le dataframe à convertir, la colonne x contenant la longitude, la colonne y contenant la latitude et le wikd contenant la référence spatiale :
Vous pouvez voir qu'une nouvelle colonne SHAPE a été ajoutée au DataFrame, qui nous permet d'accéder aux informations géographiques de notre donnée, et donc de l'afficher sur une carte ou de réaliser des opérations d'analyses spatiales dessus.
Nous pouvons vérifier quel type de géométrie (point, polyligne, polygone) possède notre donnée en appelant la propriété geometry_type :
Notez que grâce aux capacités de géocodage d'ArcGIS, il est possible de convertir un DataFrame en Spatially enabled DataFrame directement grâce à une colonne contenant uniquement une adresse avec la méthode from_df(), si par exemple vous ne possédez pas les coordonnées X et Y précises.

6 - Affichage du SeDF sur une carte

Maintenant que nous avons un SeDF, nous pouvons très facilement l'afficher sur une carte. Commençons par créer une carte dans le Notebook comme nous avons appris à le dans le tutoriel précédent :
La méthode plot() du SeDF me permet ensuite d'afficher mes données très facilement sur la carte que je viens de créer en passant la carte en argument :
Vous devriez voir cela sur votre carte :

7 - Création d'un SeDF à partir d'un feature layer du portail

Nous avons vu comment créer un SeDF à partir d'un DataFrame, mais sachez qu'il est également possible de lire d'autres formats de données : des feature layers publiés sur le portail (méthode from_layer), des classes d'entités (méthode from_featureclass), depuis une table (from_table) ou encore depuis des données parquet (from_parquet) et des GeoDatFrame Geopandas (from_geodataframe).
Voyons ensemble comment récupérer une couche d'entités (feature layer) publiée sur le portail et la lire en SeDF.

 

7a - Recherche du feature layer

De la même manière que dans le tutoriel précédent, nous utilisons la fonction search() de l'API Python d'ArcGIS pour rechercher un feature layer sur notre portail, et nous récupérons le premier item retourné par la liste.
Ici, je recherche une couche d'entités contenant les limites administratives de la commune de Bordeaux.

  • Si vous êtes utilisateur ArcGIS Online, vous y aurez également accès grâce au paramètre outside_org.
  • Si vous êtes utilisateur ArcGIS Enterprise, vous devrez publier votre propre couche d'entités hébergées, par exemple à partir des données mises à disposition par l'open data de Bordeaux Métropole, que vous pouvez télécharger ici, en sélectionnant uniquement Bordeaux avant la publication. Vous devrez ensuite modifier l'id de la requête pour qu'il corresponde à celui de votre item publié.

7b - Lecture du feature layer en SeDF

Grâce à la méthode méthode from_layer, il est maintenant très facile de lire cette couche d'entités en tant que SeDF. En argument de celle-ci, nous passons l'item du feature layer récupéré juste au-dessus. Il faut préciser l'index de la couche en accédant à la propriété layers du feature layer (0 pour la première couche, 1 si on souhaite accéder à la deuxième, etc).
Nous pouvons ré-accéder au type de géométrie, et constatons qu'il s'agit cette fois-ci d'un polygone.

7c - Affichage sur la carte

Ajoutons ce nouvel SeDF à la carte du dessus :
Voici ce que vous devez voir apparaître :

8 - Analyse spatiale à partir de deux SeDF

Nous allons chercher à récupérer uniquement les compteurs situés à l'intérieur de la commune de Bordeaux. Pour cela, nous allons faire une analyse spatiale très basique en comparant la géométrie des limites de la commune à la géométrie des points contenant l'emplacement des compteurs.

8a - Accès à la géométrie du SeDF d'emprise de la commune

Nous commençons par accéder à la géométrie de la commune. Pour cela, nous sélectionnons la première (et seule) ligne du SeDF avec iloc[0], puis accédons à sa colonne SHAPE qui contient la géométrie de notre donnée. 
Selon l'interface dans laquelle vous travaillez, la cellule vous renverra soit un tracé de la géométrie (image de gauche), soit la liste contenant les coordonnées de tous les sommets de la géométrie (image de droite). Il s'agit du même objet.

8b - Requête sur le SeDF de trafic selon la géométrie des contours de la commune

La classe GeoSeriesAccessor nous permet de réaliser des opérations en utilisant la géométrie des sedf. On y accède dans le code avec l'accesseur .geom appliqué à la colonne SHAPE du sedf.
Pour un exemple simple, nous pouvons par exemple utiliser la propriété centroid du GeoSeriesAccessor pour obtenir le centroïde du polygone représentant la commune de Bordeaux :
Plutôt sympa mais... ça ne sert à rien pour notre tutoriel ! Donc revenons à nos moutons, et utilisons le GeoSeriesAccessor pour quelque chose de plus utile.
La méthode disjoint() permet de comparer deux géométries, et renvoie une série de booléens : True si les sont géométries des entités comparées sont disjointes (ne s'intersectent pas) et False sinon (les données s'intersectent). J'utilise donc cette méthode sur le sedf contenant les compteurs, et passe en argument la géométrie du sedf contenant les limites de bordeaux récupérée au-dessus.
== False me permet ensuite d'inverser le résultat, et de passer à True les géométries non disjointes et à False les géométries disjointes.

J'utilise ensuite cette série booléenne pour filtrer le dataframe des compteurs de trafic. En passant la série en entrée de la propriété loc de mon dataframe, j'accède uniquement aux lignes pour lesquelles la valeur du booléen est True. La fonction copy() permet de créer une copie du dataframe filtré, afin d'éviter les problèmes liés à la modification d'une vue sur l'original.

8c - Vérification des résultats sur une carte

Nous créons une seconde carte, sur laquelle nous allons afficher le nouvel SeDF contenant uniquement les points dans la commune de Bordeaux, ainsi que le SeDF contenant les limites administratives de la ville. Cela nous permet de vérifier que la sélection spatiale a bien fonctionné :
Le résultat final ressemble à cela :
Parfait, ça fonctionne, on est content.

9 - Publication du SeDF en tant que feature layer sur le portail

La dernière étape consiste à publier notre sedf en tant que feature layer sur le portail. Pour cela, je peux très simplement utiliser la méthode to_featurelayer du GeoAccessor. Je donne un titre et des tags à mon feature layer, et je réindique la variable de connexion à mon SIG. Le paramètre sanitize_columns me permet de m'assurer que les noms de colonnes sont bien des chaînes de caractères, que les caractères invalides seront retirés, etc., si j'ai loupé quelque chose pendant le nettoyage de mes données.
Dans le prochain tutoriel, nous verrons comment intégrer ces opérations de préparation et publication des données dans un workflow plus global de mise à jour de données et de cartes, toujours avec l'API Python d'ArcGIS.

Aucun commentaire:

Enregistrer un commentaire