web-dev-qa-db-fra.com

DBSCAN pour le regroupement des données de localisation géographique

J'ai une trame de données avec des paires de latitude et de longitude.

Voici à quoi ressemble mon dataframe.

    order_lat  order_long
0   19.111841   72.910729
1   19.111342   72.908387
2   19.111342   72.908387
3   19.137815   72.914085
4   19.119677   72.905081
5   19.119677   72.905081
6   19.119677   72.905081
7   19.120217   72.907121
8   19.120217   72.907121
9   19.119677   72.905081
10  19.119677   72.905081
11  19.119677   72.905081
12  19.111860   72.911346
13  19.111860   72.911346
14  19.119677   72.905081
15  19.119677   72.905081
16  19.119677   72.905081
17  19.137815   72.914085
18  19.115380   72.909144
19  19.115380   72.909144
20  19.116168   72.909573
21  19.119677   72.905081
22  19.137815   72.914085
23  19.137815   72.914085
24  19.112955   72.910102
25  19.112955   72.910102
26  19.112955   72.910102
27  19.119677   72.905081
28  19.119677   72.905081
29  19.115380   72.909144
30  19.119677   72.905081
31  19.119677   72.905081
32  19.119677   72.905081
33  19.119677   72.905081
34  19.119677   72.905081
35  19.111860   72.911346
36  19.111841   72.910729
37  19.131674   72.918510
38  19.119677   72.905081
39  19.111860   72.911346
40  19.111860   72.911346
41  19.111841   72.910729
42  19.111841   72.910729
43  19.111841   72.910729
44  19.115380   72.909144
45  19.116625   72.909185
46  19.115671   72.908985
47  19.119677   72.905081
48  19.119677   72.905081
49  19.119677   72.905081
50  19.116183   72.909646
51  19.113827   72.893833
52  19.119677   72.905081
53  19.114100   72.894985
54  19.107491   72.901760
55  19.119677   72.905081

Je veux regrouper ces points qui sont les plus proches les uns des autres (distance de 200 mètres) en suivant ma matrice de distance.

from scipy.spatial.distance import pdist, squareform
distance_matrix = squareform(pdist(X, (lambda u,v: haversine(u,v))))

array([[ 0.        ,  0.2522482 ,  0.2522482 , ...,  1.67313071,
     1.05925366,  1.05420922],
   [ 0.2522482 ,  0.        ,  0.        , ...,  1.44111548,
     0.81742536,  0.98978355],
   [ 0.2522482 ,  0.        ,  0.        , ...,  1.44111548,
     0.81742536,  0.98978355],
   ..., 
   [ 1.67313071,  1.44111548,  1.44111548, ...,  0.        ,
     1.02310118,  1.22871515],
   [ 1.05925366,  0.81742536,  0.81742536, ...,  1.02310118,
     0.        ,  1.39923529],
   [ 1.05420922,  0.98978355,  0.98978355, ...,  1.22871515,
     1.39923529,  0.        ]])

Ensuite, j'applique l'algorithme de clustering DBSCAN sur la matrice de distance.

 from sklearn.cluster import DBSCAN

 db = DBSCAN(eps=2,min_samples=5)
 y_db = db.fit_predict(distance_matrix)

Je ne sais pas comment choisir la valeur eps & min_samples. Il regroupe les points qui sont bien trop loin, en un seul cluster (environ 2 km de distance) Est-ce parce qu'il calcule la distance euclidienne en se regroupant? veuillez aider.

19
Neil

DBSCAN est signifié à utiliser sur les données brutes, avec un index spatial pour l'accélération. Le seul outil que je connaisse avec l'accélération pour les distances géographiques est ELKI (Java) - scikit-learn ne le supporte malheureusement que pour quelques distances comme la distance euclidienne (voir sklearn.neighbors.NearestNeighbors). Mais apparemment, vous pouvez essayer de précalculer les distances par paire, donc ce n'est pas (encore) un problème.

Cependant, vous n'avez pas lu la documentation assez attentivement, et votre supposition que DBSCAN utilise une matrice de distance est fausse:

from sklearn.cluster import DBSCAN
db = DBSCAN(eps=2,min_samples=5)
db.fit_predict(distance_matrix)

utilise la distance euclidienne sur les lignes de la matrice de distance , ce qui n'a évidemment aucun sens.

Voir la documentation de DBSCAN (soulignement ajouté):

classe sklearn.cluster.DBSCAN (eps = 0,5, min_samples = 5, metric = 'euclidean' , algorithm = 'auto', leaf_size = 30, p = Aucun, random_state = Aucun)

métrique : chaîne ou appelable

Métrique à utiliser lors du calcul de la distance entre les instances dans un tableau d'entités. Si métrique est une chaîne ou appelable, elle doit être l'une des options autorisées par metrics.pairwise.calculate_distance pour son paramètre métrique. Si la métrique est "précalculée", X est supposé être une matrice de distance et doit être carré. X peut être une matrice clairsemée, auquel cas seulement " des éléments différents de zéro peuvent être considérés comme des voisins pour DBSCAN.

similaire pour fit_predict:

[~ # ~] x [~ # ~] : matrice ou matrice clairsemée (CSR) de forme (n_échantillons, n_fonctionnalités), ou matrice de forme ( n_échantillons, n_échantillons)

Un tableau d'entités ou un tableau de distances entre les échantillons si métrique = `` précalculé ''.

En d'autres termes, vous devez faire

db = DBSCAN(eps=2, min_samples=5, metric="precomputed")
12
Anony-Mousse

Vous pouvez regrouper des données spatiales latitude-longitude avec DBSCAN de scikit-learn sans précalculer une matrice de distance.

db = DBSCAN(eps=2/6371., min_samples=5, algorithm='ball_tree', metric='haversine').fit(np.radians(coordinates))

Cela vient de ce tutoriel sur clustering des données spatiales avec scikit-learn DBSCAN. En particulier, notez que la valeur eps est toujours de 2 km, mais elle est divisée par 6371 pour la convertir en radians. Notez également que .fit() prend les coordonnées en radians pour la métrique haversine.

33
eos

Je ne sais pas quelle implémentation de haversine vous utilisez, mais il semble qu'elle renvoie des résultats en km, donc eps devrait être 0,2, pas 2 pour 200 m.

Pour le paramètre min_samples, Cela dépend de la sortie attendue. Voici quelques exemples. Mes sorties utilisent une implémentation de haversine basée sur cette réponse qui donne une matrice de distance similaire, mais pas identique à la vôtre.

C'est avec db = DBSCAN(eps=0.2, min_samples=5)

[0 -1 -1 -1 1 1 1 -1 -1 1 1 1 2 2 1 1 1 -1 -1 -1 -1 1 -1 -1 -1 -1 -1 1 1 -1 1 1 1 1 1 1 2 0 -1 1 2 2 0 0 0 -1 -1 -1 1 1 1 -1 -1 1 -1 -1 1]

Cela crée trois clusters, 0, 1 Et 2, Et de nombreux échantillons ne tombent pas dans un cluster avec au moins 5 membres et ne sont donc pas affectés à un cluster (indiqué comme -1).

Réessayez avec une valeur min_samples Plus petite:

db = DBSCAN(eps=0.2, min_samples=2)

[0 1 1 2 3 3 3 4 4 3 3 3 5 5 3 3 3 2 6 6 7 3 2 2 8 8 8 3 3 6 3 3 3 3 3 5 0 -1 3 5 5 0 0 0 6 -1 - 1 3 3 3 7 -1 3 -1 -1 3]

Ici, la plupart des échantillons se trouvent à moins de 200 m d'au moins un autre échantillon et tombent donc dans l'un des huit groupes 0 À 7.

Modifié pour ajouter

Il semble que @ Anony-Mousse a raison, même si je n'ai rien vu de mal dans mes résultats. Pour contribuer à quelque chose, voici le code que j'utilisais pour voir les clusters:

from math import radians, cos, sin, asin, sqrt

from scipy.spatial.distance import pdist, squareform
from sklearn.cluster import DBSCAN

import matplotlib.pyplot as plt
import pandas as pd


def haversine(lonlat1, lonlat2):
    """
    Calculate the great circle distance between two points 
    on the earth (specified in decimal degrees)
    """
    # convert decimal degrees to radians 
    lat1, lon1 = lonlat1
    lat2, lon2 = lonlat2
    lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])

    # haversine formula 
    dlon = lon2 - lon1 
    dlat = lat2 - lat1 
    a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
    c = 2 * asin(sqrt(a)) 
    r = 6371 # Radius of earth in kilometers. Use 3956 for miles
    return c * r


X = pd.read_csv('dbscan_test.csv')
distance_matrix = squareform(pdist(X, (lambda u,v: haversine(u,v))))

db = DBSCAN(eps=0.2, min_samples=2, metric='precomputed')  # using "precomputed" as recommended by @Anony-Mousse
y_db = db.fit_predict(distance_matrix)

X['cluster'] = y_db

plt.scatter(X['lat'], X['lng'], c=X['cluster'])
plt.show()
5
Jamie Bull