Introduction
Dans le domaine de l’observation de la Terre et de l’analyse des données environnementales, il est souvent nécessaire de combiner des observations issues de différentes sources — par exemple, des produits satellitaires provenant d’instruments variés, des mesures au sol, ou encore des sorties de modèles numériques. Ces jeux de données peuvent différer en termes de résolution spatiale, précision de géolocalisation, couverture temporelle ou stratégie d’échantillonnage, rendant les comparaisons ou fusions directes souvent complexes.
Cet article présente des méthodes pratiques pour collocaliser deux jeux de données à la fois dans l’espace et dans le temps, à l’aide de Python. Nous aborderons notamment :
- La fusion de données lorsque les coordonnées et les temps sont strictement identiques
- L’utilisation du système de grille H3 pour des jointures spatiales approximatives
- Le calcul de la proximité spatiale et temporelle via des distances géodésiques précises
- L’optimisation de la colocalisation avec des algorithmes de plus proche voisin (KDTree et BallTree)
Chaque méthode s’applique à des cas d’usage spécifiques, allant de la fusion de données haute résolution pour la détection des feux à la validation satellite-sol ou l’intégration multi-capteurs.
Table des matières
À l’issue de ce guide, vous saurez :
- Choisir la stratégie de colocalisation adaptée à vos jeux de données
- Implémenter des méthodes d’appariement efficaces et scalables pour des volumes importants
- Intégrer des tolérances spatiales et temporelles dans votre logique de fusion
Scénario 1 : Fusion Exacte sur Latitude, Longitude et Temps
Dans de nombreux flux de travail en télédétection, il est nécessaire de combiner des informations provenant de deux jeux de données représentant les mêmes lieux et instants. Si les coordonnées (lat/lon) et les horodatages sont identiques, la colocalisation peut se faire par une simple jointure.
Problème :
Supposons que nous ayons deux DataFrames df1
et df2
contenant les colonnes communes suivantes :
lat
: latitude (float)lon
: longitude (float)time_coverage
: horodatage (en ISO 8601 ou objetdatetime
)
Objectif : fusionner ces jeux de données dans un DataFrame df3
avec les enregistrements correspondants.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import pandas as pd df1 = pd.DataFrame({ 'lat': [34.5, 34.6, 34.7], 'lon': [-120.3, -120.4, -120.5], 'time_coverage': pd.to_datetime([ '2023-10-01T10:00:00Z', '2023-10-01T10:05:00Z', '2023-10-01T10:10:00Z' ]), 'product1_value': [0.1, 0.2, 0.3] }) df2 = pd.DataFrame({ 'lat': [34.5, 34.6, 34.7], 'lon': [-120.3, -120.4, -120.5], 'time_coverage': pd.to_datetime([ '2023-10-01T10:00:00Z', '2023-10-01T10:05:00Z', '2023-10-01T10:10:00Z' ]), 'product2_value': [1.1, 1.2, 1.3] }) |
Fusion sur les clés spatiales et temporelles
Pour colocaliser les deux jeux de données, nous utilisons simplement pandas.merge()
sur les colonnes lat
, lon
et time_coverage
:
1 | df3 = pd.merge(df1, df2, on=['lat', 'lon', 'time_coverage'], how='inner') |
Cela renverra les lignes présentes dans les deux DataFrames avec des coordonnées et un temps identiques.
1 | print(df3) |
Sortie :
1 2 3 4 | lat lon time_coverage product1_value product2_value 0 34.5 -120.3 2023-10-01 10:00:00 0.1 1.1 1 34.6 -120.4 2023-10-01 10:05:00 0.2 1.2 2 34.7 -120.5 2023-10-01 10:10:00 0.3 1.3 |
Remarques
- Cette méthode ne fonctionne que si les latitudes/longitudes et les horodatages correspondent exactement (issus du même maillage ou système d’échantillonnage).
- Dans de nombreux cas réels (résolutions, instruments ou taux d’échantillonnage différents), il faut prévoir une correspondance approximative via des tolérances ou des recherches de plus proches voisins.
Scénario 1b : Fusion Approximative à l’Aide de l’Indexation Spatiale H3
Dans cette première étape, nous supposons que deux jeux de données (df1
et df2
) partagent la même résolution spatiale ou empreinte. Si la latitude, la longitude et l’heure correspondent exactement, on peut effectuer une simple fusion avec merge()
sur ces clés.
Mais que faire si les emplacements sont proches, sans être strictement identiques ?
Lorsque les coordonnées lat/lon ne correspondent pas exactement mais sont voisines, on peut convertir les deux jeux de données à une même résolution H3 et effectuer une fusion sur cette base.
Qu’est-ce que H3 ?: H3 est un système d’indexation géospatiale hiérarchique hexagonal développé par Uber. Il permet de convertir des coordonnées latitude/longitude en un identifiant unique d’hexagone à une résolution donnée (allant d’environ 1000 km² jusqu’à 1 m²), ce qui permet :
- Le regroupement spatial (« spatial binning »)
- Des jointures rapides
- L’agrégation et la synthèse spatiales
Exemple de code:
1 2 3 4 5 6 7 8 9 10 11 12 | import h3 import pandas as pd resolution = 6 # Résolution H3 (~1 km²) df1['h3_index'] = df1.apply(lambda row: h3.geo_to_h3(row['lat'], row['lon'], resolution), axis=1) df2['h3_index'] = df2.apply(lambda row: h3.geo_to_h3(row['lat'], row['lon'], resolution), axis=1) df1['time_rounded'] = df1['time_coverage'].dt.floor('min') df2['time_rounded'] = df2['time_coverage'].dt.floor('min') df3 = pd.merge(df1, df2, on=['h3_index', 'time_rounded'], how='inner', suffixes=('_1', '_2')) |
Exemple
1 | print(df3[['h3_index', 'time_rounded', 'product1_value', 'product2_value']]) |
Output
1 2 3 | h3_index time_rounded product1_value product2_value 0 862a1072ffff 2023-10-01 10:00:00 0.1 1.1 1 862a10c4ffff 2023-10-01 10:05:00 0.2 1.2 |
Quand utiliser H3 ?
- Les jeux de données utilisent des coordonnées lat/lon légèrement différentes mais avec des empreintes spatiales qui se recoupent.
- Vous souhaitez agréger les données spatialement (par exemple : moyenne de la FRP par hexagone).
- Vous travaillez avec des données d'observation de la Terre à grande échelle, en particulier provenant de sources ou de résolutions différentes.
Conseils
- Choisissez la résolution H3 en fonction de l’incertitude spatiale de vos données (par exemple, utilisez les résolutions 6 à 8 pour des pixels de fauchée satellite).
- Vous pouvez également étendre l’agrégation au domaine temporel en utilisant
pd.Grouper
,.dt.floor('15min')
, etc. - Pour accélérer les jointures spatiales sur de gros volumes
Scénario 2 : Colocalisation Approximative par Distance Spatiale et Temporelle
Lorsque deux jeux de données utilisent des grilles différentes de géolocalisation et/ou de temps, une fusion directe ne suffit plus. À la place, nous cherchons à associer chaque observation d’un jeu de données (df1
) à son observation la plus proche dans un autre (df2
) — à la fois spatialement et temporellement.
Pour cela, nous allons calculer :
distance
: la distance orthodromique (ou géodésique) entre deux points (en mètres)time_diff
: la différence absolue entre les deux horodatages (en secondes ou minutes)
Nous allons utiliser la classe pyproj.Geod
pour calculer précisément les distances géodésiques, et une simple soustraction de dates pour la différence temporelle.
Exemple
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import pandas as pd from pyproj import Geod from datetime import timedelta # Ellipsoïde de référence geod = Geod(ellps="WGS84") # Exemple : df1 (par ex. satellite A) df1 = pd.DataFrame({ 'lat': [34.52, 34.65], 'lon': [-120.33, -120.45], 'time_coverage': pd.to_datetime([ '2023-10-01T10:00:00Z', '2023-10-01T10:05:00Z' ]), 'product1_value': [0.1, 0.2] }) # Exemple : df2 (par ex. satellite B, résolution plus grossière) df2 = pd.DataFrame({ 'lat': [34.51, 34.70], 'lon': [-120.30, -120.47], 'time_coverage': pd.to_datetime([ '2023-10-01T10:01:30Z', '2023-10-01T10:08:00Z' ]), 'product2_value': [1.1, 1.3] }) |
Logique Étape par Étape
Nous allons calculer la distance et la différence de temps entre toutes les combinaisons possibles entre df1
et df2
. Ensuite, pour chaque ligne de df1
, nous sélectionnons l'observation la plus proche dans df2
selon une métrique de distance choisie.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | import numpy as np matches = [] for i, row1 in df1.iterrows(): best_match = None min_score = np.inf # Peut être ajusté pour pondérer distance vs. temps for j, row2 in df2.iterrows(): # Distance géodésique (mètres) _, _, distance = geod.inv(row1['lon'], row1['lat'], row2['lon'], row2['lat']) # Différence de temps (secondes) time_diff = abs((row1['time_coverage'] - row2['time_coverage']).total_seconds()) # Score combiné (distance + temps, ajustable selon le cas) score = distance + time_diff if score < min_score: min_score = score best_match = { 'df1_index': i, 'df2_index': j, 'distance': distance, 'time_diff_sec': time_diff, 'product1_value': row1['product1_value'], 'product2_value': row2['product2_value'] } matches.append(best_match) # Convertir la liste de correspondances en DataFrame df_matches = pd.DataFrame(matches) |
Exemple :
1 | print(df_matches[['df1_index', 'df2_index', 'distance', 'time_diff_sec', 'product1_value', 'product2_value']]) |
Résultat :
1 2 3 | df1_index df2_index distance time_diff_sec product1_value product2_value 0 0 0 2711.04689 90.0 0.1 1.1 1 1 1 2709.92131 180.0 0.2 1.3 |
Remarques
- La colonne
distance
est calculée selon l’ellipsoïde terrestre — ce qui est plus précis qu'une simple distance euclidienne. - La
time_diff
est exprimée en secondes ; vous pouvez la convertir en minutes si besoin. - Cette méthode est très coûteuse en calcul pour de grands ensembles de données (
O(N*M)
) ; à l'étape suivante, nous verrons comment l’optimiser à l’aide de structures spatiales comme KDTree et BallTree.
Scénario 3 : Accélérer la Recherche avec KDTree et BallTree
L’approche de mise en correspondance par paires décrite à l’étape 2 devient rapidement coûteuse en calcul lorsqu’on travaille avec de grands ensembles de données d’observation de la Terre.
Au lieu de calculer la distance entre toutes les paires de points, on peut utiliser des algorithmes de recherche de plus proches voisins plus efficaces et évolutifs.
Pourquoi utiliser KDTree ?
scipy.spatial.KDTree
ousklearn.neighbors.BallTree
permettent de retrouver rapidement l’observation la plus proche dans un second jeu de données.- Lorsque les coordonnées spatiales sont denses et uniformément réparties, KDTree est très performant.
- BallTree est plus adapté aux distances angulaires (Haversine) sur une sphère — nous allons illustrer les deux méthodes.
Étape 3A : KDTree pour des coordonnées planes à petite échelle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | from scipy.spatial import cKDTree import numpy as np def latlon_to_xy(lat, lon): R = 6371000 x = np.radians(lon) * R * np.cos(np.radians(lat)) y = np.radians(lat) * R return np.column_stack((x, y)) coords1 = latlon_to_xy(df1['lat'].values, df1['lon'].values) coords2 = latlon_to_xy(df2['lat'].values, df2['lon'].values) tree = cKDTree(coords2) distances, indices = tree.query(coords1, k=1) df_nn = df1.copy() df_nn['nearest_index'] = indices df_nn['distance_m'] = distances df_nn['matched_product2_value'] = df2.loc[indices, 'product2_value'].values df_nn['time_diff_sec'] = abs((df1['time_coverage'].values - df2.loc[indices, 'time_coverage'].values) / np.timedelta64(1, 's')) |
Pourquoi utiliser les coordonnées 3D (x, y, z) au lieu des coordonnées 2D (x, y) ?
La conversion des coordonnées latitude/longitude en coordonnées cartésiennes 3D (x, y, z) peut en effet être plus appropriée dans de nombreuses applications d’observation de la Terre, en particulier lorsque :
Aspect | Projection 2D (latlon_to_xy ) |
Coordonnées cartésiennes 3D (latlon_to_xyz ) |
---|---|---|
Précision | Approximative, plane (Terre plate) | Précise à l’échelle globale (Terre sphérique) |
Cas d’usage | Zones locales (petites distances, <100 km) | Correspondance globale ou sur de grandes zones |
Distorsion de projection | Les erreurs augmentent avec la taille de la zone | Aucune distorsion (sphère unité ou rayon terrestre) |
Efficacité | Légèrement plus rapide (moins de dimensions) | Un peu plus coûteux, mais toujours rapide |
Recommandé : coordonnées cartésiennes 3D pour la correspondance globale
Voici comment modifier votre code :
Étape 1 : Conversion lat/lon en coordonnées cartésiennes 3D
1 2 3 4 5 6 7 8 | def latlon_to_xyz(lat, lon): R = 6371000 # Rayon de la Terre en mètres lat_rad = np.radians(lat) lon_rad = np.radians(lon) x = R * np.cos(lat_rad) * np.cos(lon_rad) y = R * np.cos(lat_rad) * np.sin(lon_rad) z = R * np.sin(lat_rad) return np.column_stack((x, y, z)) |
Étape 2 : Construire l’arbre KDTree et effectuer l’appariement
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # Préparer les coordonnées 3D coords1 = latlon_to_xyz(df1['lat'].values, df1['lon'].values) coords2 = latlon_to_xyz(df2['lat'].values, df2['lon'].values) # Construire le KDTree sur df2 tree = cKDTree(coords2) # Rechercher les voisins les plus proches distances, indices = tree.query(coords1, k=1) # Ajouter les résultats au DataFrame df_nn = df1.copy() df_nn['nearest_index'] = indices df_nn['distance_m'] = distances df_nn['matched_product2_value'] = df2.loc[indices, 'product2_value'].values df_nn['time_diff_sec'] = abs((df1['time_coverage'].values - df2.loc[indices, 'time_coverage'].values) / np.timedelta64(1, 's')) |
Remarques sur l’interprétation :
- La distance euclidienne 3D en coordonnées cartésiennes reste une approximation de la distance sur un grand cercle (haversine) — mais elle est très proche, et plus précise qu'une projection plane (x, y) sur de grandes distances.
- Si vous avez besoin de distances exactes sur un grand cercle, vous pouvez utiliser
BallTree
avec la métrique haversine — mais il faut convertir les latitudes/longitudes en radians.
3B. BallTree (pour données globales ou sphériques)
Lorsqu’on travaille à l’échelle globale, il est préférable d’utiliser BallTree avec la distance de Haversine, qui tient compte de la courbure de la Terre. Pour cela, il faut convertir les coordonnées latitude/longitude en radians.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | from sklearn.neighbors import BallTree radians1 = np.radians(df1[['lat', 'lon']].values) radians2 = np.radians(df2[['lat', 'lon']].values) tree = BallTree(radians2, metric='haversine') dist_rad, indices = tree.query(radians1, k=1) earth_radius = 6371000 dist_m = dist_rad[:, 0] * earth_radius df_nn = df1.copy() df_nn['nearest_index'] = indices[:, 0] df_nn['distance_m'] = dist_m df_nn['matched_product2_value'] = df2.loc[indices[:, 0], 'product2_value'].values df_nn['time_diff_sec'] = abs((df1['time_coverage'].values - df2.loc[indices[:, 0], 'time_coverage'].values) / np.timedelta64(1, 's')) |
Exemple
1 | print(df_nn[['lat', 'lon', 'distance_m', 'time_diff_sec', 'product1_value', 'matched_product2_value']]) |
Résultat:
1 2 3 | lat lon distance_m time_diff_sec product1_value matched_product2_value 0 34.52 -120.33 2711.046 90.0 0.1 1.1 1 34.65 -120.45 2709.921 180.0 0.2 1.3 |
Conseils Finaux
- Utilisez H3 pour des jointures approximatives rapides à grande échelle.
- Privilégiez BallTree pour des calculs globaux avec distances sphériques.
- Intégrez une tolérance temporelle après la recherche spatiale si nécessaire.
- Pour des appariements multiples dans un rayon donné, utilisez
.query_radius()
.