# Cas Spéciaux de Prétraitement : Données Temporelles, Géospatiales et Semi-Structurées > [!note] Rappel sur le Contexte > Ce chapitre s'appuie sur vos connaissances en Python et en manipulation de données avec des bibliothèques comme Pandas et NumPy. Il vous préparera aux techniques plus avancées de *Data Mining* et de *Machine Learning* qui seront abordées dans les prochains cours. # ❶ Prétraitement des Données Temporelles Les données temporelles (ou séries chronologiques) sont des observations enregistrées au fil du temps. Elles sont fondamentales dans de nombreux domaines : cours de bourse, prévisions météorologiques, consommation d'énergie, journaux d'événements (logs), etc. Leur nature séquentielle introduit des défis et des opportunités spécifiques. ## Spécificités des Données Temporelles > [!definition] Données Temporelles > Une série temporelle est une séquence d'observations mesurées à des intervalles de temps successifs. L'ordre des observations est crucial et porte une information significative. Les défis incluent : * **Format hétérogène** : Les dates et heures peuvent être représentées de multiples façons. * **Irrégularité** : Les intervalles entre les observations peuvent ne pas être uniformes. * **Saisonnalité et Tendance** : Présence de motifs récurrents et de mouvements à long terme. * **Dépendance temporelle** : La valeur à un instant $t$ dépend souvent des valeurs passées. ## Conversion en Objets `datetime` La première étape essentielle est de s'assurer que les colonnes de date/heure sont correctement interprétées par Python, idéalement comme des objets `datetime` de Pandas. > [!tip] Utilisation de `pd.to_datetime()` > La fonction `pd.to_datetime()` est l'outil principal pour convertir des chaînes de caractères ou d'autres formats en objets `datetime`. ```python import pandas as pd # Exemple de données brutes data = { 'timestamp_str': ['2023-01-01 10:00:00', '2023-01-01 11:30:00', '2023-01-02 09:00:00'], 'valeur': [10, 12, 15] } df = pd.DataFrame(data) print("Avant conversion:") print(df.info()) # Conversion de la colonne 'timestamp_str' en datetime df['timestamp'] = pd.to_datetime(df['timestamp_str']) print("\nAprès conversion:") print(df.info()) print(df) ``` > [!note] Gestion des Erreurs > Si la conversion échoue pour certaines valeurs (formats incohérents), vous pouvez utiliser l'argument `errors='coerce'` pour remplacer les valeurs invalides par `NaT` (Not a Time). > `df['timestamp'] = pd.to_datetime(df['timestamp_str'], errors='coerce')` ## Extraction de Caractéristiques Temporelles Une fois les dates au format `datetime`, il est possible d'extraire de nombreuses informations utiles qui peuvent servir de nouvelles caractéristiques pour les modèles. ```python # Extraction de caractéristiques df['annee'] = df['timestamp'].dt.year df['mois'] = df['timestamp'].dt.month df['jour'] = df['timestamp'].dt.day df['heure'] = df['timestamp'].dt.hour df['jour_semaine'] = df['timestamp'].dt.dayofweek # Lundi=0, Dimanche=6 df['nom_jour_semaine'] = df['timestamp'].dt.day_name() df['semaine_annee'] = df['timestamp'].dt.isocalendar().week.astype(int) df['est_weekend'] = df['timestamp'].dt.dayofweek >= 5 print("\nAprès extraction de caractéristiques:") print(df) ``` Ces nouvelles colonnes peuvent capturer la saisonnalité, les cycles hebdomadaires ou journaliers, et améliorer la performance des modèles. ## Rééchantillonnage et Fenêtrage (Resampling & Rolling Windows) Le rééchantillonnage permet de modifier la fréquence des données temporelles (ex: passer de données horaires à quotidiennes ou mensuelles). Les fenêtres glissantes (rolling windows) calculent des statistiques sur une période de temps donnée. > [!example] Rééchantillonnage > Supposons que nous ayons des données horaires et que nous voulions la somme quotidienne. ```python # Créons une série temporelle avec un index datetime df_ts = df.set_index('timestamp') print("\nDataFrame avec index temporel:") print(df_ts) # Rééchantillonnage quotidien (somme des valeurs par jour) df_daily_sum = df_ts['valeur'].resample('D').sum() print("\nRééchantillonnage quotidien (somme):") print(df_daily_sum) # Rééchantillonnage quotidien (moyenne des valeurs par jour) df_daily_mean = df_ts['valeur'].resample('D').mean() print("\nRééchantillonnage quotidien (moyenne):") print(df_daily_mean) ``` Les codes de fréquence pour `resample()` sont variés : 'D' (jour), 'W' (semaine), 'M' (mois), 'H' (heure), etc. > [!example] Fenêtres Glissantes (Rolling Windows) > Calculer une moyenne mobile sur 2 observations. ```python # Moyenne mobile sur 2 périodes df_ts['moyenne_mobile_2'] = df_ts['valeur'].rolling(window=2).mean() print("\nMoyenne mobile sur 2 périodes:") print(df_ts) ``` Les fenêtres glissantes sont très utiles pour lisser les données, identifier des tendances à court terme ou créer des caractéristiques basées sur le passé (lags). ## Gestion des Fuseaux Horaires et des Jours Fériés * **Fuseaux Horaires** : Il est crucial de gérer correctement les fuseaux horaires, surtout si les données proviennent de différentes régions. Pandas permet de localiser (`tz_localize`) et de convertir (`tz_convert`) les fuseaux horaires. ```python # Localiser les données à un fuseau horaire # df_ts_utc = df_ts.index.tz_localize('UTC') # Convertir vers un autre fuseau horaire # df_ts_paris = df_ts_utc.tz_convert('Europe/Paris') ``` * **Jours Fériés** : Les jours fériés peuvent avoir un impact significatif sur les données (ex: ventes, trafic). Il est souvent utile de créer une caractéristique binaire indiquant si un jour est férié ou non. Des bibliothèques comme `holidays` peuvent aider à générer ces listes. # ❷ Prétraitement des Données Géospatiales Les données géospatiales représentent des informations liées à des emplacements sur Terre. Elles sont cruciales dans la logistique, l'urbanisme, l'environnement, la santé publique, l'agriculture de précision, etc. ## Spécificités des Données Géospatiales > [!definition] Données Géospatiales > Informations décrivant la position, la forme et les relations spatiales d'entités géographiques. Elles sont souvent représentées par des coordonnées (latitude, longitude) et des géométries complexes. Les défis incluent : * **Formats variés** : WKT (Well-Known Text), GeoJSON, Shapefiles, KML, GPX, etc. * **Systèmes de Coordonnées (CRS)** : Différentes projections de la Terre (WGS84, Lambert 93, UTM). La gestion du CRS est fondamentale. * **Types de géométries** : Points, Lignes, Polygones. * **Calculs spécifiques** : Distances, intersections, contenance, union, différence. * **Volumétrie** : Les données géospatiales peuvent être très volumineuses. ## La Bibliothèque `geopandas` : Le Couteau Suisse `geopandas` est une extension de Pandas qui facilite la manipulation des données géospatiales en Python. Elle introduit les concepts de `GeoSeries` (une colonne de géométries) et de `GeoDataFrame` (un DataFrame avec une colonne spéciale pour les géométries). Elle s'appuie sur `shapely` pour les géométries et `fiona` pour la lecture/écriture des formats de fichiers. > [!tip] Installation de `geopandas` > `conda install geopandas` (recommandé pour gérer les dépendances) ou `pip install geopandas shapely fiona pyproj` ```python import pandas as pd import geopandas from shapely.geometry import Point, Polygon, LineString # Création d'un DataFrame classique geo_data = { 'lieu': ['Paris', 'Lyon', 'Marseille'], 'latitude': [48.8566, 45.7640, 43.2965], 'longitude': [2.3522, 4.8357, 5.3698], 'population': [2141000, 518000, 870000] } df_geo = pd.DataFrame(geo_data) # Conversion en GeoDataFrame # Création d'une GeoSeries de Points à partir de latitude et longitude geometry = [Point(xy) for xy in zip(df_geo['longitude'], df_geo['latitude'])] gdf = geopandas.GeoDataFrame(df_geo, geometry=geometry) print("GeoDataFrame des villes:") print(gdf.head()) print(f"\nType de la colonne 'geometry': {type(gdf.geometry)}") print(f"CRS du GeoDataFrame: {gdf.crs}") # Par défaut, il peut être None ou déduit ``` > [!note] Géométries `shapely` > Les objets géométriques dans `geopandas` sont des objets `shapely` : > * `Point(longitude, latitude)` : un point unique. > * `LineString([(lon1, lat1), (lon2, lat2), ...])` : une ligne. > * `Polygon([(lon1, lat1), (lon2, lat2), ...])` : un polygone (liste de coordonnées pour l'extérieur, puis pour les trous éventuels). ## Systèmes de Coordonnées (CRS) et Reprojection Le CRS (Coordinate Reference System) définit comment les coordonnées sont interprétées. Il est crucial de s'assurer que toutes les données géospatiales utilisées ensemble partagent le même CRS, ou de les reprojeter. > [!definition] CRS (Coordinate Reference System) > Un CRS est un système qui permet de localiser des points sur Terre. Il se compose d'un datum (modèle mathématique de la Terre) et d'une projection cartographique. > * **CRS Géographiques** (non projetés) : Utilisent des coordonnées angulaires (latitude, longitude). Le plus courant est **WGS84 (EPSG:4326)**. Les distances et surfaces sont déformées. > * **CRS Projetés** : Utilisent des coordonnées planes (x, y) après une projection cartographique. Ils préservent mieux les distances et surfaces dans une zone spécifique. Ex: **Lambert 93 (EPSG:2154)** pour la France métropolitaine. ```python # Définir le CRS si absent (WGS84 est standard pour lat/lon) gdf.set_crs(epsg=4326, inplace=True) print(f"\nCRS après définition: {gdf.crs}") # Reprojection vers un CRS projeté (Lambert 93 pour la France) # Cela transforme les coordonnées angulaires en mètres gdf_lambert = gdf.to_crs(epsg=2154) print("\nGeoDataFrame reprojeté en Lambert 93:") print(gdf_lambert.head()) print(f"Nouveau CRS: {gdf_lambert.crs}") # Les coordonnées 'geometry' ont changé pour être en mètres print(f"Coordonnées de Paris en Lambert 93: {gdf_lambert.loc[0, 'geometry']}") ``` > [!warning] Toujours vérifier et homogénéiser le CRS ! > Travailler avec des données de CRS différents sans reprojection peut mener à des erreurs d'analyse spatiales majeures. ## Calculs de Distance et d'Aire Une fois les données dans un CRS projeté (où les unités sont métriques), les calculs de distance et d'aire sont directs. Pour les CRS géographiques (WGS84), il faut utiliser des formules sphériques comme Haversine ou des fonctions dédiées. > [!theorem] Formule de Haversine (Rappel) > La distance $d$ entre deux points $(\phi_1, \lambda_1)$ et $(\phi_2, \lambda_2)$ sur une sphère de rayon $R$ est donnée par : > $ d = 2R \arcsin\left(\sqrt{\sin^2\left(\frac{\phi_2 - \phi_1}{2}\right) + \cos(\phi_1)\cos(\phi_2)\sin^2\left(\frac{\lambda_2 - \lambda_1}{2}\right)}\right) $ > où $\phi$ est la latitude et $\lambda$ est la longitude, et les angles doivent être en radians. $R$ est le rayon de la Terre (environ 6371 km). ```python import numpy as np def haversine_distance(lat1, lon1, lat2, lon2): R = 6371 # Rayon de la Terre en kilomètres lat1_rad, lon1_rad, lat2_rad, lon2_rad = map(np.radians, [lat1, lon1, lat2, lon2]) dlat = lat2_rad - lat1_rad dlon = lon2_rad - lon1_rad a = np.sin(dlat / 2)**2 + np.cos(lat1_rad) * np.cos(lat2_rad) * np.sin(dlon / 2)**2 c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a)) return R * c # Distance entre Paris et Lyon (avec Haversine) dist_paris_lyon = haversine_distance(gdf.loc[0, 'latitude'], gdf.loc[0, 'longitude'], gdf.loc[1, 'latitude'], gdf.loc[1, 'longitude']) print(f"\nDistance Haversine entre Paris et Lyon: {dist_paris_lyon:.2f} km") # Distance entre Paris et Lyon (avec Shapely/Geopandas après reprojection) # La distance est calculée en unités du CRS (ici, mètres) distance_shapely = gdf_lambert.loc[0, 'geometry'].distance(gdf_lambert.loc[1, 'geometry']) / 1000 # en km print(f"Distance Shapely (Lambert 93) entre Paris et Lyon: {distance_shapely:.2f} km") ``` ## Opérations Spatiales Fondamentales `geopandas` et `shapely` permettent une multitude d'opérations spatiales : 1. **Buffer (Zone tampon)** : Créer une zone autour d'une géométrie. ```python # Créer un buffer de 10km autour de Paris (en Lambert 93, donc en mètres) paris_buffer = gdf_lambert.loc[0, 'geometry'].buffer(10000) # 10000 mètres = 10 km print(f"\nType de l'objet buffer: {type(paris_buffer)}") # print(paris_buffer) # Le polygone du buffer ``` 2. **Intersection, Union, Différence** : Combiner ou comparer des géométries. ```python # Exemple simple d'intersection (nécessite deux géométries qui se chevauchent) # Créons deux polygones simples poly1 = Polygon([(0, 0), (0, 2), (2, 2), (2, 0)]) poly2 = Polygon([(1, 1), (1, 3), (3, 3), (3, 1)]) intersection_poly = poly1.intersection(poly2) print(f"\nIntersection de deux polygones: {intersection_poly}") ``` 3. **Relations spatiales** : Tester si une géométrie contient, est contenue dans, ou touche une autre. * `contains(other)` : `self` contient `other`. * `within(other)` : `self` est contenu dans `other`. * `intersects(other)` : `self` et `other` se croisent. ```python # Vérifier si Lyon est dans le buffer de Paris (après reprojection) # Il faut reprojeter le buffer de Paris au CRS d'origine pour la comparaison si Lyon n'est pas reprojeté # Ou reprojeter Lyon au CRS du buffer # Pour simplifier, nous allons comparer les géométries après reprojection is_lyon_in_paris_buffer = paris_buffer.contains(gdf_lambert.loc[1, 'geometry']) print(f"Lyon est-elle dans un rayon de 10km de Paris? {is_lyon_in_paris_buffer}") # Devrait être False ``` 4. **Spatial Join** : Joindre deux GeoDataFrames basés sur leurs relations spatiales. C'est une opération très puissante. > [!example] Spatial Join > Supposons que nous ayons un GeoDataFrame de points (par exemple, des magasins) et un GeoDataFrame de polygones (par exemple, des quartiers). On peut joindre les magasins aux quartiers dans lesquels ils se trouvent. ```python # Créons un GeoDataFrame de quartiers fictifs (en Lambert 93) quartier_a = Polygon([(650000, 6800000), (660000, 6800000), (660000, 6810000), (650000, 6810000)]) quartier_b = Polygon([(670000, 6820000), (680000, 6820000), (680000, 6830000), (670000, 6830000)]) gdf_quartiers = geopandas.GeoDataFrame( {'nom_quartier': ['Quartier A', 'Quartier B'], 'geometry': [quartier_a, quartier_b]}, crs="EPSG:2154" ) # Créons un point fictif de magasin (en Lambert 93) magasin_point = Point(655000, 6805000) gdf_magasin = geopandas.GeoDataFrame( {'nom_magasin': ['Magasin X'], 'geometry': [magasin_point]}, crs="EPSG:2154" ) # Effectuer un spatial join pour trouver dans quel quartier se trouve le magasin magasin_in_quartier = geopandas.sjoin(gdf_magasin, gdf_quartiers, how="inner", op="within") print("\nMagasin après spatial join:") print(magasin_in_quartier) # Le résultat montre que 'Magasin X' est dans 'Quartier A' ``` ## Géocodage et Géodécodage * **Géocodage** : Convertir une adresse textuelle en coordonnées géographiques (latitude, longitude). * **Géodécodage** : Convertir des coordonnées géographiques en une adresse textuelle. Des bibliothèques comme `geopy` ou des APIs web (OpenStreetMap Nominatim, Google Maps API) sont utilisées pour cela. ```python # Exemple de géocodage avec geopy (nécessite l'installation: pip install geopy) from geopy.geocoders import Nominatim geolocator = Nominatim(user_agent="my-data-science-app") location = geolocator.geocode("10 Downing Street, London") if location: print(f"\nGéocodage de '10 Downing Street, London': Latitude={location.latitude}, Longitude={location.longitude}") else: print("\nGéocodage échoué pour '10 Downing Street, London'.") ``` ## Lecture et Écriture de Fichiers Géospatiaux `geopandas` peut facilement lire et écrire la plupart des formats de fichiers géospatiaux : ```python # Lecture d'un Shapefile (exemple conceptuel) # gdf_regions = geopandas.read_file("chemin/vers/regions.shp") # Écriture d'un GeoDataFrame en GeoJSON # gdf.to_file("mes_villes.geojson", driver="GeoJSON") ``` ## IV. Prétraitement des Données Semi-Structurées et Imbriquées (JSON/XML) Les données semi-structurées, comme les fichiers JSON (JavaScript Object Notation) ou XML (Extensible Markup Language), sont très courantes, notamment avec les APIs web. Elles ne suivent pas un schéma fixe comme les bases de données relationnelles, mais contiennent des balises ou des clés qui organisent les données. ## Spécificités des Données Semi-Structurées > [!definition] Données Semi-Structurées > Données qui ne résident pas dans un format de base de données relationnelle mais qui contiennent des balises ou d'autres marqueurs pour séparer les éléments sémantiques et imposer une hiérarchie d'enregistrements et de champs. Les défis incluent : * **Structure imbriquée** : Les données peuvent contenir des listes d'objets ou des dictionnaires dans des dictionnaires. * **Clés manquantes** : Tous les objets n'ont pas forcément toutes les clés. * **Hétérogénéité** : Des types de données variés au sein d'une même structure. ## Aplatissement (Flattening) des Données JSON L'objectif est de transformer une structure JSON imbriquée en un format tabulaire (DataFrame) où chaque colonne représente une caractéristique. > [!example] Aplatissement d'un JSON > Supposons un JSON avec des informations utilisateur et des détails sur ses commandes. ```python import pandas as pd import json json_data = [ { "user_id": "U001", "name": "Alice", "contact": {"email": "[email protected]", "phone": "123-456-7890"}, "orders": [ {"order_id": "O101", "amount": 100, "status": "shipped"}, {"order_id": "O102", "amount": 50, "status": "pending"} ] }, { "user_id": "U002", "name": "Bob", "contact": {"email": "[email protected]"}, # Pas de téléphone pour Bob "orders": [ {"order_id": "O201", "amount": 200, "status": "delivered"} ] } ] # Convertir en DataFrame, pandas gère bien les dictionnaires imbriqués au premier niveau df_json = pd.json_normalize(json_data, sep='_') # Utilise '_' comme séparateur pour les clés imbriquées print("DataFrame après aplatissement simple:") print(df_json) # Pour les listes imbriquées (comme 'orders'), il faut souvent un traitement supplémentaire # On peut "exploser" la colonne 'orders' si chaque commande doit être une ligne séparée df_orders = pd.json_normalize(json_data, record_path='orders', meta=['user_id', 'name']) print("\nDataFrame des commandes (après explosion):") print(df_orders) # On peut ensuite fusionner si nécessaire, ou traiter séparément. ``` > [!tip] `pd.json_normalize()` > Cette fonction est extrêmement utile pour aplatir des données JSON. > * `record_path` : Spécifie le chemin vers la liste d'enregistrements à aplatir. > * `meta` : Spécifie les clés du dictionnaire parent à inclure dans les enregistrements aplatis. > * `sep` : Le séparateur pour les noms de colonnes imbriquées (par défaut '.'). Le prétraitement des données semi-structurées consiste souvent à naviguer dans la hiérarchie pour extraire les informations pertinentes et les transformer en un format tabulaire plat, prêt pour l'analyse. # ➡️ C'est la fin ! Ce chapitre vous a introduit à la diversité des données et aux méthodes de prétraitement spécifiques qu'elles requièrent. La maîtrise des techniques pour les données temporelles, géospatiales et semi-structurées est une compétence précieuse qui élargit considérablement votre capacité à travailler avec des jeux de données complexes et réels. > [!note] Points Clés à Retenir > * **Données Temporelles** : Conversion en `datetime`, extraction de caractéristiques (année, mois, jour, heure), rééchantillonnage, fenêtres glissantes. > * **Données Textuelles** : Nettoyage de base (minuscules, ponctuation, chiffres), tokenisation, suppression des mots vides. > * **Données Géospatiales** : Compréhension des coordonnées (lat/lon), `geopandas` comme outil central, gestion des CRS et reprojection, calculs de distance et d'aire, opérations spatiales (buffer, intersection, spatial join). > * **Données Semi-structurées (JSON)** : Aplatissement des structures imbriquées avec `pd.json_normalize()`. --- - Cours précèdent: [[Cours 1 - Pré-traiter avec Python]] - Prochain cours: [[Exercices - Pré-traiter avec Python]] - Page d'accueil de la compétence: [[Pré-traiter avec Python]] # 🗓️ Historique - Dernière MAJ: `13-Octobre-2025` - Rédigé par: [[Hamilton DE ARAUJO]]