Python实现经纬度空间点DBSCAN聚类

2023-11-07

写在前面

博主前期科研工作中,涉及到要对某个地区的一些空间点进行聚类分析,想到读研期间,曾经用DBSCAN聚类算法实现了四线激光雷达扫描的三维点云数据聚类(论文题目:基于改进DBSCAN算法的激光雷达目标物检测方法),当初用matlab实现的,虽说是改进的算法,但改进方法非常原始。DBSCAN是一种非常实用的密度聚类算法,而地理空间的经纬度点聚类,没有其他维度的信息的话,毫无疑问可以用密度聚类。于是博主重新熟悉了一下算法,并做了一些改进,用Python实现,记录在博客里面。

  • 编译环境:Python3.7
  • 编译器:Spyder 4.1.5

算法及实现过程

DBSCAN聚类算法原理

先简单介绍一下DBSCAN聚类算法的原理:

DBSCAN(Density-based spatial clustering of applications with noise)是由Martin Ester[8]等人最早提出的一种基于密度的空间聚类算法,该算法将具有足够密度数据的区域划分为k个不同的簇,并能在具有噪声数据的空间域内发现任意形状的簇,本文记为Cj(j=1,2…k),其中簇定义为密度相连点的最大集合,其基本原理是聚类过程要满足以下两个条件:最大性,对于空间中任意两点p、q,如果p属于簇C,并且p密度可达q,则点q也属于簇C;连接性,对于同属于簇的任意两点p、q,它们彼此是密度相连的。DBSCAN算法具有聚类速度快、能有效处理噪声点、能发现空间中任意形状簇、无需划分聚类个数等优点,但DBSCAN聚类算法也有其缺点,其聚类效果高度依赖输入参数——聚类半径和簇内最少样本点数,在高维数据的聚类中,对距离公式选取非常敏感,存在“维数灾难”。

上面一段引自自己的论文,很不好理解对不对,博主也不打算花精力去解释,因为单独理解这个聚类算法,估计都得写篇博客。想理解原理的,自己在CSDN里面搜,能搜出一大堆博客来,给大家推荐一篇博客,看完基本就明白了。
DBSCAN聚类算法——机器学习(理论+图解+python代码)
简单来说,DBSCAN聚类算法需要有数据点,最好是三维以内,高维数据聚类很复杂;需要一个聚类半径,按照核心点搜索半径邻域内的其他点;需要一个聚类最少点数,也就是一个类里面,最少要包含特定数量的点。

Python实现原始的DBSCAN聚类算法

DBSCAN聚类算法是机器学习的一种,说到用Python做机器学习,那自然少不了sklearn这个包,这个包里面有cluster方法是专门用来聚类的,而这个聚类函数里面,又有个DBSCAN类,我们来看看这个类吧(为了不影响阅读体验,我建议大家直接跳过不要看,太长了)


# -*- coding: utf-8 -*-
"""
DBSCAN: Density-Based Spatial Clustering of Applications with Noise
"""

# Author: Robert Layton <robertlayton@gmail.com>
#         Joel Nothman <joel.nothman@gmail.com>
#         Lars Buitinck
#
# License: BSD 3 clause

import numpy as np
import warnings
from scipy import sparse

from ..base import BaseEstimator, ClusterMixin
from ..utils.validation import _check_sample_weight, _deprecate_positional_args
from ..neighbors import NearestNeighbors

from ._dbscan_inner import dbscan_inner


@_deprecate_positional_args
def dbscan(X, eps=0.5, *, min_samples=5, metric='minkowski',
           metric_params=None, algorithm='auto', leaf_size=30, p=2,
           sample_weight=None, n_jobs=None):
    """Perform DBSCAN clustering from vector array or distance matrix.

    Read more in the :ref:`User Guide <dbscan>`.

    Parameters
    ----------
    X : {array-like, sparse (CSR) matrix} of shape (n_samples, n_features) or \
            (n_samples, n_samples)
        A feature array, or array of distances between samples if
        ``metric='precomputed'``.

    eps : float, default=0.5
        The maximum distance between two samples for one to be considered
        as in the neighborhood of the other. This is not a maximum bound
        on the distances of points within a cluster. This is the most
        important DBSCAN parameter to choose appropriately for your data set
        and distance function.

    min_samples : int, default=5
        The number of samples (or total weight) in a neighborhood for a point
        to be considered as a core point. This includes the point itself.

    metric : string, or callable
        The metric to use when calculating distance between instances in a
        feature array. If metric is a string or callable, it must be one of
        the options allowed by :func:`sklearn.metrics.pairwise_distances` for
        its metric parameter.
        If metric is "precomputed", X is assumed to be a distance matrix and
        must be square during fit.
        X may be a :term:`sparse graph <sparse graph>`,
        in which case only "nonzero" elements may be considered neighbors.

    metric_params : dict, default=None
        Additional keyword arguments for the metric function.

        .. versionadded:: 0.19

    algorithm : {'auto', 'ball_tree', 'kd_tree', 'brute'}, default='auto'
        The algorithm to be used by the NearestNeighbors module
        to compute pointwise distances and find nearest neighbors.
        See NearestNeighbors module documentation for details.

    leaf_size : int, default=30
        Leaf size passed to BallTree or cKDTree. This can affect the speed
        of the construction and query, as well as the memory required
        to store the tree. The optimal value depends
        on the nature of the problem.

    p : float, default=2
        The power of the Minkowski metric to be used to calculate distance
        between points.

    sample_weight : array-like of shape (n_samples,), default=None
        Weight of each sample, such that a sample with a weight of at least
        ``min_samples`` is by itself a core sample; a sample with negative
        weight may inhibit its eps-neighbor from being core.
        Note that weights are absolute, and default to 1.

    n_jobs : int, default=None
        The number of parallel jobs to run for neighbors search. ``None`` means
        1 unless in a :obj:`joblib.parallel_backend` context. ``-1`` means
        using all processors. See :term:`Glossary <n_jobs>` for more details.
        If precomputed distance are used, parallel execution is not available
        and thus n_jobs will have no effect.

    Returns
    -------
    core_samples : ndarray of shape (n_core_samples,)
        Indices of core samples.

    labels : ndarray of shape (n_samples,)
        Cluster labels for each point.  Noisy samples are given the label -1.

    See also
    --------
    DBSCAN
        An estimator interface for this clustering algorithm.
    OPTICS
        A similar estimator interface clustering at multiple values of eps. Our
        implementation is optimized for memory usage.

    Notes
    -----
    For an example, see :ref:`examples/cluster/plot_dbscan.py
    <sphx_glr_auto_examples_cluster_plot_dbscan.py>`.

    This implementation bulk-computes all neighborhood queries, which increases
    the memory complexity to O(n.d) where d is the average number of neighbors,
    while original DBSCAN had memory complexity O(n). It may attract a higher
    memory complexity when querying these nearest neighborhoods, depending
    on the ``algorithm``.

    One way to avoid the query complexity is to pre-compute sparse
    neighborhoods in chunks using
    :func:`NearestNeighbors.radius_neighbors_graph
    <sklearn.neighbors.NearestNeighbors.radius_neighbors_graph>` with
    ``mode='distance'``, then using ``metric='precomputed'`` here.

    Another way to reduce memory and computation time is to remove
    (near-)duplicate points and use ``sample_weight`` instead.

    :func:`cluster.optics <sklearn.cluster.optics>` provides a similar
    clustering with lower memory usage.

    References
    ----------
    Ester, M., H. P. Kriegel, J. Sander, and X. Xu, "A Density-Based
    Algorithm for Discovering Clusters in Large Spatial Databases with Noise".
    In: Proceedings of the 2nd International Conference on Knowledge Discovery
    and Data Mining, Portland, OR, AAAI Press, pp. 226-231. 1996

    Schubert, E., Sander, J., Ester, M., Kriegel, H. P., & Xu, X. (2017).
    DBSCAN revisited, revisited: why and how you should (still) use DBSCAN.
    ACM Transactions on Database Systems (TODS), 42(3), 19.
    """

    est = DBSCAN(eps=eps, min_samples=min_samples, metric=metric,
                 metric_params=metric_params, algorithm=algorithm,
                 leaf_size=leaf_size, p=p, n_jobs=n_jobs)
    est.fit(X, sample_weight=sample_weight)
    return est.core_sample_indices_, est.labels_


class DBSCAN(ClusterMixin, BaseEstimator):
    """Perform DBSCAN clustering from vector array or distance matrix.

    DBSCAN - Density-Based Spatial Clustering of Applications with Noise.
    Finds core samples of high density and expands clusters from them.
    Good for data which contains clusters of similar density.

    Read more in the :ref:`User Guide <dbscan>`.

    Parameters
    ----------
    eps : float, default=0.5
        The maximum distance between two samples for one to be considered
        as in the neighborhood of the other. This is not a maximum bound
        on the distances of points within a cluster. This is the most
        important DBSCAN parameter to choose appropriately for your data set
        and distance function.

    min_samples : int, default=5
        The number of samples (or total weight) in a neighborhood for a point
        to be considered as a core point. This includes the point itself.

    metric : string, or callable, default='euclidean'
        The metric to use when calculating distance between instances in a
        feature array. If metric is a string or callable, it must be one of
        the options allowed by :func:`sklearn.metrics.pairwise_distances` for
        its metric parameter.
        If metric is "precomputed", X is assumed to be a distance matrix and
        must be square. X may be a :term:`Glossary <sparse graph>`, in which
        case only "nonzero" elements may be considered neighbors for DBSCAN.

        .. versionadded:: 0.17
           metric *precomputed* to accept precomputed sparse matrix.

    metric_params : dict, default=None
        Additional keyword arguments for the metric function.

        .. versionadded:: 0.19

    algorithm : {'auto', 'ball_tree', 'kd_tree', 'brute'}, default='auto'
        The algorithm to be used by the NearestNeighbors module
        to compute pointwise distances and find nearest neighbors.
        See NearestNeighbors module documentation for details.

    leaf_size : int, default=30
        Leaf size passed to BallTree or cKDTree. This can affect the speed
        of the construction and query, as well as the memory required
        to store the tree. The optimal value depends
        on the nature of the problem.

    p : float, default=None
        The power of the Minkowski metric to be used to calculate distance
        between points.

    n_jobs : int, default=None
        The number of parallel jobs to run.
        ``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
        ``-1`` means using all processors. See :term:`Glossary <n_jobs>`
        for more details.

    Attributes
    ----------
    core_sample_indices_ : ndarray of shape (n_core_samples,)
        Indices of core samples.

    components_ : ndarray of shape (n_core_samples, n_features)
        Copy of each core sample found by training.

    labels_ : ndarray of shape (n_samples)
        Cluster labels for each point in the dataset given to fit().
        Noisy samples are given the label -1.

    Examples
    --------
    >>> from sklearn.cluster import DBSCAN
    >>> import numpy as np
    >>> X = np.array([[1, 2], [2, 2], [2, 3],
    ...               [8, 7], [8, 8], [25, 80]])
    >>> clustering = DBSCAN(eps=3, min_samples=2).fit(X)
    >>> clustering.labels_
    array([ 0,  0,  0,  1,  1, -1])
    >>> clustering
    DBSCAN(eps=3, min_samples=2)

    See also
    --------
    OPTICS
        A similar clustering at multiple values of eps. Our implementation
        is optimized for memory usage.

    Notes
    -----
    For an example, see :ref:`examples/cluster/plot_dbscan.py
    <sphx_glr_auto_examples_cluster_plot_dbscan.py>`.

    This implementation bulk-computes all neighborhood queries, which increases
    the memory complexity to O(n.d) where d is the average number of neighbors,
    while original DBSCAN had memory complexity O(n). It may attract a higher
    memory complexity when querying these nearest neighborhoods, depending
    on the ``algorithm``.

    One way to avoid the query complexity is to pre-compute sparse
    neighborhoods in chunks using
    :func:`NearestNeighbors.radius_neighbors_graph
    <sklearn.neighbors.NearestNeighbors.radius_neighbors_graph>` with
    ``mode='distance'``, then using ``metric='precomputed'`` here.

    Another way to reduce memory and computation time is to remove
    (near-)duplicate points and use ``sample_weight`` instead.

    :class:`cluster.OPTICS` provides a similar clustering with lower memory
    usage.

    References
    ----------
    Ester, M., H. P. Kriegel, J. Sander, and X. Xu, "A Density-Based
    Algorithm for Discovering Clusters in Large Spatial Databases with Noise".
    In: Proceedings of the 2nd International Conference on Knowledge Discovery
    and Data Mining, Portland, OR, AAAI Press, pp. 226-231. 1996

    Schubert, E., Sander, J., Ester, M., Kriegel, H. P., & Xu, X. (2017).
    DBSCAN revisited, revisited: why and how you should (still) use DBSCAN.
    ACM Transactions on Database Systems (TODS), 42(3), 19.
    """
    @_deprecate_positional_args
    def __init__(self, eps=0.5, *, min_samples=5, metric='euclidean',
                 metric_params=None, algorithm='auto', leaf_size=30, p=None,
                 n_jobs=None):
        self.eps = eps
        self.min_samples = min_samples
        self.metric = metric
        self.metric_params = metric_params
        self.algorithm = algorithm
        self.leaf_size = leaf_size
        self.p = p
        self.n_jobs = n_jobs

    def fit(self, X, y=None, sample_weight=None):
        """Perform DBSCAN clustering from features, or distance matrix.

        Parameters
        ----------
        X : {array-like, sparse matrix} of shape (n_samples, n_features), or \
            (n_samples, n_samples)
            Training instances to cluster, or distances between instances if
            ``metric='precomputed'``. If a sparse matrix is provided, it will
            be converted into a sparse ``csr_matrix``.

        sample_weight : array-like of shape (n_samples,), default=None
            Weight of each sample, such that a sample with a weight of at least
            ``min_samples`` is by itself a core sample; a sample with a
            negative weight may inhibit its eps-neighbor from being core.
            Note that weights are absolute, and default to 1.

        y : Ignored
            Not used, present here for API consistency by convention.

        Returns
        -------
        self

        """
        X = self._validate_data(X, accept_sparse='csr')

        if not self.eps > 0.0:
            raise ValueError("eps must be positive.")

        if sample_weight is not None:
            sample_weight = _check_sample_weight(sample_weight, X)

        # Calculate neighborhood for all samples. This leaves the original
        # point in, which needs to be considered later (i.e. point i is in the
        # neighborhood of point i. While True, its useless information)
        if self.metric == 'precomputed' and sparse.issparse(X):
            # set the diagonal to explicit values, as a point is its own
            # neighbor
            with warnings.catch_warnings():
                warnings.simplefilter('ignore', sparse.SparseEfficiencyWarning)
                X.setdiag(X.diagonal())  # XXX: modifies X's internals in-place

        neighbors_model = NearestNeighbors(
            radius=self.eps, algorithm=self.algorithm,
            leaf_size=self.leaf_size, metric=self.metric,
            metric_params=self.metric_params, p=self.p, n_jobs=self.n_jobs)
        neighbors_model.fit(X)
        # This has worst case O(n^2) memory complexity
        neighborhoods = neighbors_model.radius_neighbors(X,
                                                         return_distance=False)

        if sample_weight is None:
            n_neighbors = np.array([len(neighbors)
                                    for neighbors in neighborhoods])
        else:
            n_neighbors = np.array([np.sum(sample_weight[neighbors])
                                    for neighbors in neighborhoods])

        # Initially, all samples are noise.
        labels = np.full(X.shape[0], -1, dtype=np.intp)

        # A list of all core samples found.
        core_samples = np.asarray(n_neighbors >= self.min_samples,
                                  dtype=np.uint8)
        dbscan_inner(core_samples, neighborhoods, labels)

        self.core_sample_indices_ = np.where(core_samples)[0]
        self.labels_ = labels

        if len(self.core_sample_indices_):
            # fix for scipy sparse indexing issue
            self.components_ = X[self.core_sample_indices_].copy()
        else:
            # no core samples
            self.components_ = np.empty((0, X.shape[1]))
        return self

    def fit_predict(self, X, y=None, sample_weight=None):
        """Perform DBSCAN clustering from features or distance matrix,
        and return cluster labels.

        Parameters
        ----------
        X : {array-like, sparse matrix} of shape (n_samples, n_features), or \
            (n_samples, n_samples)
            Training instances to cluster, or distances between instances if
            ``metric='precomputed'``. If a sparse matrix is provided, it will
            be converted into a sparse ``csr_matrix``.

        sample_weight : array-like of shape (n_samples,), default=None
            Weight of each sample, such that a sample with a weight of at least
            ``min_samples`` is by itself a core sample; a sample with a
            negative weight may inhibit its eps-neighbor from being core.
            Note that weights are absolute, and default to 1.

        y : Ignored
            Not used, present here for API consistency by convention.

        Returns
        -------
        labels : ndarray of shape (n_samples,)
            Cluster labels. Noisy samples are given the label -1.
        """
        self.fit(X, sample_weight=sample_weight)
        return self.labels_

代码写的很牛逼,但是不建议看,因为我实在是没耐心看完。
那我们就用这个DBSCAN来写个聚类的示例吧,用鸢尾花数据。
先看看数据的基本特征:

import matplotlib.pyplot as plt  
import numpy as np  
from sklearn.cluster import KMeans
from sklearn import datasets 
from  sklearn.cluster import DBSCAN
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']
 
iris = datasets.load_iris() 
X = iris.data[:, :4] 
# 看看数据
plt.scatter(X[:, 0], X[:, 1], c="red", marker='o', label='see')  
plt.xlabel('萼片长度')  
plt.ylabel('萼片宽度')  
plt.legend(loc=2)  
plt.show()  

在这里插入图片描述
这就是基本的数据分布情况,代码很简单,我不再一一解释。
接下来我们看下最原始的DBSCAN聚类算法,直接看代码:

dbscan = DBSCAN(eps=0.4, min_samples=9)  # 1
dbscan.fit(X)   # 2 
label_pred = dbscan.labels_  # 3
 
# 绘制聚类结果
x0 = X[label_pred == 0]  # 4
x1 = X[label_pred == 1]  # 4
x2 = X[label_pred == 2]  # 4
x3 = X[label_pred == -1]   # 4
plt.scatter(x0[:, 0], x0[:, 1], c="red", marker='o', label='cluster0')  
plt.scatter(x1[:, 0], x1[:, 1], c="green", marker='*', label='cluster1')  
plt.scatter(x2[:, 0], x2[:, 1], c="blue", marker='+', label='cluster2') 
plt.scatter(x3[:, 0], x3[:, 1], c="black", marker='D', label='noise')  
plt.xlabel('萼片长度')  
plt.ylabel('萼片宽度')  
plt.legend(loc=2)  
plt.show()

我来解释一下我标注的部分:

  1. dbscan = DBSCAN(eps=0.4, min_samples=9) 表示设置参数,聚类半径是0.4,每个类里面的点不少于9个,也就是我前面说的三个参数中的后两个;
  2. dbscan.fit(X) 数据集拟合,机器学习无需多言;
  3. label_pred = dbscan.labels_ 聚类结果,也就是说每个点聚类的情况,如果是-1,说明算法认为这个点是噪声点,我们先来看看聚类结果,如下图(为了方便大家看数据,我把计算得到的label_pred变换了一下,将单列数据变成了6列):
    在这里插入图片描述
    可以看出来,大部分点被归为噪声点,只划分了3个簇,聚类标签分别为0/1/2。
  4. 后面就是根据聚类的标签值,把数据进行分类,并画出来,来看看聚类结果。
    在这里插入图片描述

好了,原理就介绍这么多,写了这么多相信大家对DBSCAN聚类算法有了一定的理解,那下面进入我们的正题。

DBSCAN聚类经纬度点

博主手上有一些经纬度数据点,我想用DBSCAN算法来进行聚类。虽然我前面写了,DBSCAN算法的原始代码不建议看,但是关注源代码里面的这一行代码:

def __init__(self, eps=0.5, *, min_samples=5, metric='euclidean',
                 metric_params=None, algorithm='auto', leaf_size=30, p=None, n_jobs=None):

其中有一个关键参数,metric=‘euclidean’,意思就是距离计算公式是欧式距离,但是我们的点集是经纬度数据点,如果直接用欧式距离来表达两个点之间的经纬度距离点,会不会有问题呢?
不管了,先试试看聚类情况。上完整代码

import pandas as pd
import numpy as np
from sklearn.cluster import DBSCAN
import matplotlib.pyplot as plt
import seaborn as sns
import folium
from sklearn import metrics
sns.set()
## 第一部分
df = pd.read_csv('00-首页数据.csv')
df = df[['lat_Amap', 'lng_Amap']].dropna(axis=0,how='all')  
data = np.array(df)

db = DBSCAN(eps=0.005, min_samples=10).fit(data)
labels = db.labels_
raito = len(labels[labels[:] == -1]) / len(labels)  # 计算噪声点个数占总数的比例
n_clusters_ = len(set(labels)) - (1 if -1 in labels else 0)  # 获取分簇的数目
score = metrics.silhouette_score(data, labels)
 
df['label'] = labels
sns.lmplot('lat_Amap', 'lng_Amap', df, hue='label', fit_reg=False)

## 第二部分
map_ = folium.Map(location=[31.574729, 120.301663], zoom_start=12,
                       tiles='http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}',
                       attr='default')

colors = ['#DC143C', '#FFB6C1', '#DB7093', '#C71585', '#8B008B', '#4B0082', '#7B68EE',
         '#0000FF', '#B0C4DE', '#708090', '#00BFFF', '#5F9EA0', '#00FFFF', '#7FFFAA',
         '#008000', '#FFFF00', '#808000', '#FFD700', '#FFA500', '#FF6347','#DC143C', 
         '#FFB6C1', '#DB7093', '#C71585', '#8B008B', '#4B0082', '#7B68EE',
         '#0000FF', '#B0C4DE', '#708090', '#00BFFF', '#5F9EA0', '#00FFFF', '#7FFFAA',
         '#008000', '#FFFF00', '#808000', '#FFD700', '#FFA500', '#FF6347', 
         '#DC143C', '#FFB6C1', '#DB7093', '#C71585', '#8B008B', '#4B0082', '#7B68EE',
         '#0000FF', '#B0C4DE', '#708090', '#00BFFF', '#5F9EA0', '#00FFFF', '#7FFFAA',
         '#008000', '#FFFF00', '#808000', '#FFD700', '#FFA500', '#FF6347',
         '#DC143C', '#FFB6C1', '#DB7093', '#C71585', '#8B008B', '#4B0082', '#7B68EE',
         '#0000FF', '#B0C4DE', '#708090', '#00BFFF', '#5F9EA0', '#00FFFF', '#7FFFAA',
         '#008000', '#FFFF00', '#808000', '#FFD700', '#FFA500', '#FF6347','#DC143C', 
         '#FFB6C1', '#DB7093', '#C71585', '#8B008B', '#4B0082', '#7B68EE',
         '#0000FF', '#B0C4DE', '#708090', '#00BFFF', '#5F9EA0', '#00FFFF', '#7FFFAA',
         '#008000', '#FFFF00', '#808000', '#FFD700', '#FFA500', '#FF6347', 
         '#DC143C', '#FFB6C1', '#DB7093', '#C71585', '#8B008B', '#4B0082', '#7B68EE',
         '#0000FF', '#B0C4DE', '#708090', '#00BFFF', '#5F9EA0', '#00FFFF', '#7FFFAA',
         '#008000', '#FFFF00', '#808000', '#FFD700', '#FFA500', '#FF6347','#000000']

for i in range(len(data)):
    folium.CircleMarker(location=[data[i][0], data[i][1]],
                        radius=4, popup='popup',
                        color=colors[labels[i]], fill=True,
                        fill_color=colors[labels[i]]).add_to(map_)
    
map_.save('all_cluster.html')

代码很长,我们分两部分来看
第一部分是原始聚类,还有看聚类结果,直接看看sns.lmplot(‘lat_Amap’, ‘lng_Amap’, df, hue=‘label’, fit_reg=False)的效果吧
在这里插入图片描述
聚成了40多个类,噪声占比79%。。
感觉还可以接受啊,但是同学们看懂了的话,肯定会有一个疑问:

db = DBSCAN(eps=0.005, min_samples=10).fit(data)

这行代码里面,这两个关键参数是怎么获取的?
比较尴尬,我只能告诉大家,我是一个个试出来,对比原始数据,感觉这个参数最好。

df['label'] = labels

上面这行代码,是将聚类标签加入到原始数组里面去,看看df
在这里插入图片描述
反正有40+类,对于一个市的数据来说,博主觉得研究内容还算合理。
第二部分在干嘛呢,那是博主在将聚类结果打在地图上,用到了folium这个包,这个包很好用,建议看一下我前一篇博客
当然,我也是根据聚类的标签来画点的颜色的,因此,我定义了40多个颜色,当然,里面是有重复的颜色,不过不影响视觉效果。为了把每个点都用对应的颜色,我写了个循环,看下面的代码:

for i in range(len(data)):
    folium.CircleMarker(location=[data[i][0], data[i][1]],
                        radius=4, popup='popup',
                        color=colors[labels[i]], fill=True,
                        fill_color=colors[labels[i]]).add_to(map_)

循环是最简单的方法,看地图上的效果
在这里插入图片描述
上图中,黑色的圆圈点是噪声点,其他颜色是不同的类。效果还不错,与实际情况一致,也就是说,用欧式距离是可以的,但是博主一直觉得这样做不妥,在网上搜索了很多资料,终于找到方法替换欧式距离了。

经纬度实际距离替换欧式距离并进行聚类

不磨叽,直接上代码

# -*- coding: utf-8 -*-
"""
Created on Fri Jun 12 10:39:07 2020

@author: HP
"""

# -*- coding: utf-8 -*-
"""
Created on Wed May 20 08:32:01 2020

@author: HP
"""

import pandas as pd
import numpy as np
from sklearn.cluster import DBSCAN
import matplotlib.pyplot as plt
import seaborn as sns
import folium
from sklearn import metrics
from  math import radians
from math import tan,atan,acos,sin,cos,asin,sqrt
from scipy.spatial.distance import pdist, squareform
sns.set()

def haversine(lonlat1, lonlat2):
    lat1, lon1 = lonlat1
    lat2, lon2 = lonlat2
    lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])

    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 * 1000


df = pd.read_csv('00-首页数据.csv')

df = df[['lat_Amap', 'lng_Amap']].dropna(axis=0,how='all')
# df['lon_lat'] = df.apply(lambda x: [x['lng_Amap'], x['lat_Amap']], axis=1)
# df = df['lon_lat'].to_frame()
# data = np.array(data)
# plt.figure(figsize=(10, 10))
# plt.scatter(df['lat_Amap'], df['lng_Amap'])
distance_matrix = squareform(pdist(df, (lambda u, v: haversine(u, v))))
db = DBSCAN(eps=500, min_samples=10, metric='precomputed').fit_predict(distance_matrix)


'''
db = DBSCAN(eps=0.038, min_samples=3).fit(data)
'''
labels = db
raito = len(labels[labels[:] == -1]) / len(labels)  # 计算噪声点个数占总数的比例
n_clusters_ = len(set(labels)) - (1 if -1 in labels else 0)  # 获取分簇的数目
# score = metrics.silhouette_score(distance_matrix, labels)
df['label'] = labels
sns.lmplot('lat_Amap', 'lng_Amap', df, hue='label', fit_reg=False)

''' 
df['label'] = labels
sns.lmplot('lat_Amap', 'lng_Amap', df, hue='label', fit_reg=False)
'''
map_all = folium.Map(location=[31.574729, 120.301663], zoom_start=12,
                     tiles='http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}',
                     attr='default')

# colors = ['#DC143C', '#FFB6C1', '#DB7093', '#C71585', '#8B008B', '#4B0082', '#7B68EE',
#          '#0000FF', '#B0C4DE', '#708090', '#00BFFF', '#5F9EA0', '#00FFFF', '#7FFFAA',
#          '#008000', '#FFFF00', '#808000', '#FFD700', '#FFA500', '#FF6347', '#000000']

colors = ['#DC143C', '#FFB6C1', '#DB7093', '#C71585', '#8B008B', '#4B0082', '#7B68EE',
         '#0000FF', '#B0C4DE', '#708090', '#00BFFF', '#5F9EA0', '#00FFFF', '#7FFFAA',
         '#008000', '#FFFF00', '#808000', '#FFD700', '#FFA500', '#FF6347','#DC143C', 
         '#FFB6C1', '#DB7093', '#C71585', '#8B008B', '#4B0082', '#7B68EE',
         '#0000FF', '#B0C4DE', '#708090', '#00BFFF', '#5F9EA0', '#00FFFF', '#7FFFAA',
         '#008000', '#FFFF00', '#808000', '#FFD700', '#FFA500', '#FF6347', 
         '#DC143C', '#FFB6C1', '#DB7093', '#C71585', '#8B008B', '#4B0082', '#7B68EE',
         '#0000FF', '#B0C4DE', '#708090', '#00BFFF', '#5F9EA0', '#00FFFF', '#7FFFAA',
         '#008000', '#FFFF00', '#808000', '#FFD700', '#FFA500', '#FF6347',
         '#DC143C', '#FFB6C1', '#DB7093', '#C71585', '#8B008B', '#4B0082', '#7B68EE',
         '#0000FF', '#B0C4DE', '#708090', '#00BFFF', '#5F9EA0', '#00FFFF', '#7FFFAA',
         '#008000', '#FFFF00', '#808000', '#FFD700', '#FFA500', '#FF6347','#DC143C', 
         '#FFB6C1', '#DB7093', '#C71585', '#8B008B', '#4B0082', '#7B68EE',
         '#0000FF', '#B0C4DE', '#708090', '#00BFFF', '#5F9EA0', '#00FFFF', '#7FFFAA',
         '#008000', '#FFFF00', '#808000', '#FFD700', '#FFA500', '#FF6347', 
         '#DC143C', '#FFB6C1', '#DB7093', '#C71585', '#8B008B', '#4B0082', '#7B68EE',
         '#0000FF', '#B0C4DE', '#708090', '#00BFFF', '#5F9EA0', '#00FFFF', '#7FFFAA',
         '#008000', '#FFFF00', '#808000', '#FFD700', '#FFA500', '#FF6347','#000000']

for i in range(len(df)):
    if labels[i] == -1:
        continue
    else :
        folium.CircleMarker(location=[df.iloc[i,0], df.iloc[i,1]],
                            radius=4, popup='popup',
                            color=colors[labels[i]], fill=True,
                            fill_color=colors[labels[i]]).add_to(map_all)
    
map_all.save('all_cluster.html')
   

敲黑板,我定义的这个函数haversine就是用来求解任意两点之间距离的函数,下面这行代码很关键

distance_matrix = squareform(pdist(df, (lambda u, v: haversine(u, v))))

计算结果如下,矩阵的意义是任意两点之间的距离,对角线上全为0
在这里插入图片描述
再看聚类参数

db = DBSCAN(eps=500, min_samples=10, metric='precomputed').fit_predict(distance_matrix)

这里的metric='precomputed’不再是欧式距离,后面也不再是fit函数,而是fit_predict函数,用这种方法呢,就将原来的欧式距离换成了实际的距离,当然。看看聚类效果:
在这里插入图片描述
由于点太多,我把噪声点去掉了。
再看看用相同的数据点,我用原始的聚类算发的聚类效果:
在这里插入图片描述
我感觉差不多。
写到这里,其实内容已经写完了。
但是还有一个问题并没有解决,就是DBSCAN聚类参数,到底该怎么输入,难道真要一个个去试吗,有没有好的办法来解决呢,能不能自适应呢,肯定是可以的。
继续看下面的内容。

轮廓系数调整输入参数

在前面的代码中,一直有一行代码我没解释

score = metrics.silhouette_score(data, labels)

就是这行代码,从变量的定义来看,我定义了一个得分,metrics.silhouette_score是机器学习中轮廓系数的计算函数,也就是说我可以用这个函数来计算模型的得分,是怎么一个计算过程呢,我这里详细介绍一下:

在聚类算法中,可以使用轮廓系数(Silhouette Coefficient)对聚类样本的聚类效果进行评估,轮廓系数的计算模型如式(1)、式(2)所示。
在这里插入图片描述
上式中, s(i)为样本i的轮廓系数,该值越接近1,说明样本i聚类越合理,越接近-1,说明样本i更应该分类到另外的簇,越接近0,说明样本i在两个簇的边界上; a(i)为样本i到簇内不相似度,为该样本同簇其他样本的平均距离,该值越小,说明该样本越应被聚类到该簇;b(i) 为样本i的簇间不相似度,计算公式如式(3)所示。
在这里插入图片描述
式(3)中, bij表示样本i到某簇Cj 所有样本的平均距离。
根据所有样本的轮廓系数计算平均值,即可得到当前聚类模型的总体轮廓系数值,并依据该值确定输入参数。

注:以上内容来源于本人论文。
就是说,我可以根据轮廓系数来不断调整我的输入参数,直到找到轮廓系数最大值对应的输入参数,这样就找到了最佳参数了。
上流程图
在这里插入图片描述
流程图解释的很清楚了,我再上一段代码来解释调参过程。

res = []
# 迭代不同的eps值
for eps in np.arange(0.001,0.13,0.001):
    # 迭代不同的min_samples值
    for min_samples in range(2,11):
        dbscan = DBSCAN(eps = eps, min_samples = min_samples)
        # 模型拟合
        dbscan.fit(data)
        # 统计各参数组合下的聚类个数(-1表示异常点)
        n_clusters = len([i for i in set(dbscan.labels_) if i != -1])
        # 异常点的个数
        outliners = np.sum(np.where(dbscan.labels_ == -1, 1,0))
        # 统计每个簇的样本个数
        # stats = pd.Series([i for i in dbscan.labels_ if i != -1]).value_counts()
        # 计算聚类得分
        try:
            score = metrics.silhouette_score(data, dbscan.labels_)
        except:
            score = -99
        res.append({'eps':eps,'min_samples':min_samples,'n_clusters':n_clusters,'outliners':outliners, 'score':score})
# 将迭代后的结果存储到数据框中        
result = pd.DataFrame(res)

相关的参数解释如下:

  1. eps的调参范围是[0.001,0.13],这个参数是根据数据特征来获取的,就是说得对数据有一定的认识才能确定调参范围,循环的步长是0.001,意思就是说,最小距离半径是0.001,最大是0.13,注意,这里用的是欧式距离计算;
  2. min_samples的调参范围是[2,11],一个簇内至少得包含两个点吧,如果最少点超过了11,那么会将所有的点聚成同一个类,参数就是这么定的,循环步长是1。

这样就完成了整个调参过程。
实际效果如何和,我只能说不理想。
我们来看一下,上面我认为聚类较好的参数的轮廓系数:
在这里插入图片描述
得分是个负数。。而很多接近于1的轮廓系数对应的聚类效果简直一塌糊涂,不过我尝试了一些其他的数据,总体来说,轮廓系数还是可以来评价的。
至此,相关技术细节都已介绍完毕。
不过大家要注意一下,folium包在目前的使用过程中,可能会出现一些问题,我出现的问题就是地图上的标注没法正常显示,可以用下面的方法来解决:
把folium.py里面原来的http://code.jquery.com/jquery-1.12.4.min.js,替换成https://cdn.bootcdn.net/ajax/libs/jquery/1.12.4/jquery.min.js

总结

列一下博客中的技术细节:

  1. DBSCAN聚类算法原理
  2. Python 机器学习实现DBSCAN聚类过程
  3. 应用欧式距离实现聚类
  4. 通过实际计算实际距离得到距离矩阵实现聚类
  5. 聚类结果上地图
  6. 根据轮廓系数调整聚类参数

这篇博客,总结了博主近半年内研究的东西,涉及到GIS、机器学习,内容比较多。

注:本文为原创文章,且部分内容涉及知识产权归属,仅供学习讨论,若要引用或转载,请注明本文出处

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Python实现经纬度空间点DBSCAN聚类 的相关文章

  • scikit学习逻辑回归方程

    我已经在 iris 数据集上运行了逻辑回归 直到这段代码我才清楚 之后我想形成方程来对测试数据进行评分 该怎么做 我知道我可以使用预测函数对测试进行评分 但是我想查看参数和各自的权重 能否请你帮忙 from sklearn import d
  • 在 while 循环中更改 tkinter 画布中的图像

    我的完整代码是here https gist github com ItsBerry de245ba70376cb07f4dbe2d25c223f5f 我正在尝试使用 tkinter 的画布创建一个小游戏 让人们练习学习高音谱号上的音符 最
  • scikit-learn LinearRegression 的意外交叉验证分数

    我正在尝试学习使用 scikit learn 来完成一些基本的统计学习任务 我认为我已经成功创建了适合我的数据的线性回归模型 X train X test y train y test cross validation train test
  • 如何修复错误“错误:命令错误,退出状态 1:python。”尝试使用 pip 安装 django-heroku 时[重复]

    这个问题在这里已经有答案了 我正在尝试使用 pip 安装 django heroku 但它一直遇到错误 我看到一些建议告诉我要确保 Heroku 中的 Python 版本是最新的 我已经这么做了 推送到 Heroku master 后 我运
  • BeautifulSoup 不适用于某些网站

    我有这个脚本 import urrlib2 from bs4 import BeautifulSoup url http www shoptop ru page urllib2 urlopen url read soup Beautiful
  • swig char ** 作为指向 char * 的指针

    我在使用 swig 和 char 作为指向变量 char 的指针时遇到问题 而不是作为 char 的列表 我找不到将指针包装到 char 的方法 目的是将连接的结果写入指针引用的 char 中 以下是我的代码 文件指针 cpp includ
  • Python:选择多个已安装模块版本之一

    在我的系统上 我多次安装了多个模块 举个例子 numpy 1 6 1安装在标准路径中 usr lib python2 7 dist packages 我有一个更新版本numpy 1 8 0安装于 local python lib pytho
  • 继承类中的python __init__方法[重复]

    这个问题在这里已经有答案了 我想为子类提供一些额外的属性 而不必显式调用新方法 那么有没有办法给继承的类一个 init 不重写的类型方法 init 父类的方法 我编写下面的代码纯粹是为了说明我的问题 因此属性等的命名很糟糕 class in
  • 在 PyQt 中使用 Windows 7 任务栏功能

    我正在寻找有关将一些新的 Windows 7 任务栏功能集成到我的 PyQt 应用程序中的信息 具体来说 如果已经存在使用新进度指示器的可能性 see here http www petri co il wp content uploads
  • 将 Python 3 的“范围”“向后移植”到 Python 2 是一个坏主意吗?

    我的一门课程要求用 Python 完成作业 作为练习 我一直使用如下脚本确保我的程序可以在 Python 2 和 Python 3 中运行 bin bash Run some PyUnit tests python2 test py pyt
  • 导入pytorch时,未安装microsoft Visual C++ Redistributable

    我在一台带有 GPU 的 Windows 机器上工作 我已经在 conda 环境中安装了 pytorch conda install pytorch torchvision cudatoolkit 10 1 c pytorch 然后我运行
  • 带参数的 Python 列表过滤

    python中有没有一种方法可以在列表上调用过滤器 其中过滤函数在调用期间绑定了许多参数 例如有没有办法做这样的事情 gt gt def foo a b c return a lt b and b lt c gt gt myList 1 2
  • Python 中的“finally”总是执行吗?

    对于Python中任何可能的try finally块 是否保证finally块总是会被执行吗 例如 假设我在except block try 1 0 except ZeroDivisionError return finally print
  • python 中的优化标准化

    在优化过程中 对输入参数进行归一化 使它们处于同一数量级 通常会很有帮助 这样收敛效果会更好 例如 如果我们想要最小化 f x 而合理的近似值是 x0 1e3 1e 4 则将 x0 0 和 x0 1 归一化到大约相同的数量级可能会有所帮助
  • Python:如何访问 Lotus Notes 8.5 Inbox 来阅读电子邮件

    我想用 python 创建一个脚本 从 Lotus Notes 8 5 读取电子邮件 然后在 jira 中为每封电子邮件创建一个问题 但当我尝试从 Lotus 读取邮件时 它会返回此错误 Traceback most recent call
  • 使用 PyCharm 分析 Django

    即使在开发环境中 我的应用程序也相当慢 所以我想找出是什么导致它变慢 以便我可以尝试修复它 我了解调试工具栏 根据它的报告 数据库查询和下载的源都不是问题 所以它一定是业务逻辑 但是 我无法使用 Django 服务器运行 PyCharm 分
  • 如果我更改当前工作目录,为什么 __file__ 会变成无效路径?

    执行中test py from tmp import os print os path abspath file os chdir var print os path abspath file output tmp test py var
  • 每次 apache 重新启动时,flask-login 会话都会被破坏

    我正在使用烧瓶登录https github com maxcountryman flask login https github com maxcountryman flask login和领域记住登录用户 http packages py
  • pylint:忽略 rcfile 中的多个

    在我的 django 项目中 我使用的是外部编写的应用程序 但编写得很糟糕 现在我想从我的 pylint 报告中忽略这个应用程序 但是我无法让 pylint 忽略它 Pylint 已经忽略了南方的迁移 如下所示 MASTER ignore
  • Python 线程与 Linux 中的多处理

    基于此question https stackoverflow com questions 807506 threads vs processes in linux我假设创建新流程应该几乎和创造新线程在Linux中 然而 很少的测试显示出截

随机推荐

  • css button阴影效果,css怎么给button设置阴影

    css给button设置阴影的方法 首先创建一个HTML示例文件 然后设置一个button按钮 最后通过给button添加 box shadow 等属性来实现阴影效果即可 本文操作环境 Windows7系统 HTML5 CSS3版 DELL
  • Scene窗口—视图控制栏

    Scene 视图控制栏 在 Scene 视图控制栏中可以选择用于查看场景的各种选项 还可以控制是否启用光照和音频 这些控件仅在开发期间影响 Scene 视图 对构建的游戏没有影响 绘制模式 Draw mode 菜单 绘制模式是 选择描绘场景
  • js弹框带传值父窗口给子框_layui 父页面获取弹窗传递的值 和 父页面传值给子弹窗的方法...

    1 父页面获取子页面 弹窗 的值 现在父页面页面加载方法中定义方法 专门用来获取从子页面的值 document ready function 拿到子窗口中传回的数据 function getChildrenData data console
  • 有奖调研

    桔妹导读 参与滴滴开源问卷调研 前100名有效填写问卷的用户可获得10元滴滴快车出行卡 第99位有效参与问卷的用户可额外获得100元滴滴快车出行卡一张 滴滴开源诚挚邀请您扫码参与开源问卷调研 给我们提出宝贵建议 长按二维码识别 填写问卷 关
  • DOS命令之copy:复制

    DOS 命令 copy 用于将一个文件从一个位置复制到另一个位置 以下是五个示例 说明了如何使用 copy 命令 1 复制文件到另一个目录假设我们有一个名为 test txt 的文件 它位于 C Users username Documen
  • 安全线程的集合

    1 CopyOnWriteArrayList package com kuang unsafe import java util import java util concurrent CopyOnWriteArrayList java u
  • Windows11安装kohya_ss详细步骤(报错、踩坑)

    文章目录 笔者环境 所需环境 安装kohya ss 方式一 带有GUI的kohya ss仓库 方式二 kohya ss核心仓库 题外话 笔者环境 OS windows11 Python 3 10 6 CUDA11 6 所需环境 Python
  • JavaEE初阶(5)多线程案例(定时器、标准库中的定时器、实现定时器、线程池、标准库中的线程池、实现线程池)

    接上次博客 JavaEE初阶 4 线程的状态 线程安全 synchronized volatile wait 和 notify 多线程的代码案例 单例模式 饿汉懒汉 阻塞队列 di Dora的博客 CSDN博客 目录 多线程案例 定时器 标
  • 云计算复习资料

    文章目录 第一章 云计算 一 云计算的概念与特征 1 云计算的概念 2 云计算的特征 3 云计算发展历程 二 云计算的服务类型 1 laaS 1 IaaS的核心技术 2 IaaS的服务优势 2 PaaS 1 PaaS的核心技术 2 PasS
  • SMOTE过采样技术原理与实现

    1 这种操作的原理是什么 目的是什么 目的是合成分类问题中的少数类样本 使数据达到平衡 其中 样本数量过少的类别称为 少数类 原理和思想 合成的策略是对每个少数类样本a 从它的最近邻中随机选一个样本b 然后在a b之间的连线上随机选一点作为
  • 正交矩阵的保范性:正交变换不改变向量的长度(范数)

    在推导使用SVD分解解方程时 用到了正交矩阵的保范性这一性质 1 正交矩阵定义 A mathbf A intercal A A A A
  • QT编程----事件(一)

    review ui 生成 h cpp文件 uic form1 ui o form1 h uic form1 ui i form1 h o form1 cpp C 三个特点 继承 重载 封装 QT程序设计进阶 事件 Qt事件 Qt程序是事件驱
  • JavaScript 根据指定年月获取该月的第一天和最后一天、获取上个月的年月、上个月月底日期

    文章目录 根据指定年月获取该月的第一天和最后一天 获取上个月的年月 上个月月底日期 根据指定年月获取该月的第一天和最后一天 let date new Date let new year date getFullYear 取当前的年份 let
  • Spring内置定时器的使用

    1 Spring内置定时器的使用 在configuration配置类中 引入 EnableSchedule 开启定时器 编写定时器类 在定时方法中添加 Schedule注解 并且使用fixedDelay fixedRate cron表达式用
  • 关于Unsupported major.minor version 52.0报错问题解决方案

    目录 1 问题描述 2 问题分析 3 解决方案 步骤一 删除JDK1 7版本 步骤二 导入JDK1 8版本 步骤三 将新的JDK1 8引入到工程中 4 总结 1 问题描述 在启动项目工程中 当编译class文件的时候会报错一个 java l
  • QClub大连站2013年第一期总结

    今天下午 QClub大连站2013年第一期如期举行 报名差不多50人 到场也有30多人 地点还在老地方 万恒商务大厦 我自己今天的状态不太好 女儿最近感冒 我也跟着有些上火 嗓子疼了三天了 今天开始有点儿咳嗽 还好不是特别严重 开场比较简单
  • python中系统找不到指定文件怎么办_PyCharm-错误-找不到指定文件python.exe的解决方法...

    1 现象 系统提示找不到指定的文件 Error running hello Cannot run program B pystudy venv Scripts python exe in directory python study Cre
  • 微前端解决方案

    目录 微前端解决方案 微前端的整体架构 微前端部署平台 微前端解决方案 在理想的情况下 期望能达到 将一个复杂的单体应用以功能或业务需求垂直的切分成更小的子系统 并且能够达到以下能力 子系统间的开发 发布从空间上完成隔离 子系统可以使用不同
  • debug跳出循环_Java基础-第04章:循环结构「云图智联」

    免费学习视频欢迎关注云图智联 https e yuntuzhilian com 1 什么是循环结构 1 1 为什么要学习循环结构 生活中 有很多 重复的去作某件事 的例子 旋转的钟表指针 滚动的车轮 日复一日的上课等等 同理 在程序中也有很
  • Python实现经纬度空间点DBSCAN聚类

    写在前面 博主前期科研工作中 涉及到要对某个地区的一些空间点进行聚类分析 想到读研期间 曾经用DBSCAN聚类算法实现了四线激光雷达扫描的三维点云数据聚类 论文题目 基于改进DBSCAN算法的激光雷达目标物检测方法 当初用matlab实现的