Comment Collocaliser Deux Jeux de Données en Python dans l’Espace et le Temps ?

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.

À 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 objet datetime)

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 ou sklearn.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().