作者:小小明

在一堆基站经纬度数据中,时常涉及三种计算,例如查找某个点最近的N个点,查找某个点指定距离范围内的所有点,将距离小于指定阈值聚类的基站聚类在一起。

下面我们看看计算方法。

import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

首先读取数据集:

excel = pd.io.excel.ExcelFile("point.xlsx")
find = excel.parse("查找")
data = excel.parse("数据库")
# 去除重复数据
find.drop_duplicates(ignore_index=True, inplace=True)
data.drop_duplicates(ignore_index=True, inplace=True)
data.reset_index(drop=False, inplace=True)
print("被查找的点:", find.shape)
display(find.head())
print("经纬度数据库:", data.shape)
display(data.head())

使用sklearn处理经纬度的三种距离计算与地图可视化_git

数据预处理

为了后续计算方便,我们可以将被被查找表对应到经纬度数据库的索引保存起来:

data.rename(columns={"index": "索引"}, inplace=True)
find = pd.merge(find, data, how="left")
find.head()

使用sklearn处理经纬度的三种距离计算与地图可视化_git_02

计算被查找的点是否不在数据库中:

find.query("索引.isnull()", engine="python").shape[0]

返回0,说明所有被查找的坐标点都在数据库内。

为了后续计算方便,我们可以将被被查找表的索引对应到经纬度数据库:

find.index = data.index[data.小区名称.isin(find.小区名称)]

使用sklearn处理经纬度的三种距离计算与地图可视化_数据库_03

将坐标信息可视化一下,看一下整体分布:

plt.figure(dpi=200)
plt.scatter(data.Longitude, data.Latitude, s=10, color='blue', alpha=0.1)
plt.scatter(find.Longitude, find.Latitude, s=1, color='red', alpha=0.1)
plt.show()

使用sklearn处理经纬度的三种距离计算与地图可视化_数据库_04

从图中的颜色深浅可以看到很多点的位置几乎相同,导致颜色叠加的比较深。

下面首先我们来计算一下每个点最近的N个点:

查找每个点最近的N个点

首先提取经纬度数据转成numpy数组:

data_p = data.values[:, 2:4]
find_p = find.values[:, 1:3]
print(data_p.shape, find_p.shape)
(5306, 2) (1773, 2)

创建用于计算两个经纬度距离的函数:

from math import *


def distancefuc(s1, s2):
"创建用于计算两个经纬度距离的函数"
# 经纬度转换成弧度
lon1, lat1 = map(radians, s1)
lon2, lat2 = map(radians, s2)
dlon = lon2 - lon1
dlat = lat2 - lat1
a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2
distance = round(2 * asin(sqrt(a)) * 6371000, 1) # 地球平均半径,6371km
return

可以使用已经封装好的NearestNeighbors创建BallTree模型:

from sklearn.neighbors import NearestNeighbors

nn = NearestNeighbors(metric=distancefuc, algorithm='ball_tree')
nn.fit(data_p)

但我们直接使用内层BallTree类,因为API更加简单直接:

from sklearn.neighbors import BallTree
bt = BallTree(data_p, metric=distancefuc)

由于每个被查找点都存在于数据库中,所以必然找出的n个点会包含自身,但我们可以先找出最近的n+1个点,再去掉自身。

比如我们想找出每个点最近的10个点,可以先找出最近的11个点:

# 结果与nn.kneighbors(find_p, 11, return_distance=True)一样
distances, points = bt.query(find_p, 11, return_distance=True)
print(distances[:1])
print(points[:1])
[[  0.    0.    0.    0.    0.    0.  364.7 364.7 364.7 364.7 364.7]]
[[ 11 15 13 12 2 14 17 4065 18 20 4067]]

下面我们首先将结果整合到表格中:

find["最近点索引"] = points.tolist()
find["距离"] = distances.tolist()

使用sklearn处理经纬度的三种距离计算与地图可视化_数据库_05

可视化检查BallTree查找效果

查看可用的颜色表:

plt.colormaps()

对于连续性colormap可以通过以下方法获取颜色值,可自由设置颜色个数:

cm = plt.get_cmap('gist_rainbow')
NUM_COLORS = 22
colors = [cm(1.*i/NUM_COLORS) for i in range(NUM_COLORS)]

对于离散颜色值使用以下命令获取颜色列表:

colors = plt.get_cmap('tab20b').colors

下面我们随机找20个点,并用不同的颜色可视化每个点最近的10个点:

plt.figure(figsize=(18, 8), dpi=100)
plt.scatter(data.Longitude, data.Latitude, s=20, color='black', alpha=0.1)
for c, p in zip(plt.get_cmap('tab20b').colors, find.sample(20).最近点索引.values):
plt.scatter(data.loc[p].values[:, 2],
data.loc[p].values[:, 3], s=50, color=c, alpha=1)
plt.show()

使用sklearn处理经纬度的三种距离计算与地图可视化_git_06

可以看到颜色相近的正好在一起。

为了更清晰的看到效果,我们将其显示到folium地图上。

首先定义颜色值列表:

colors = ['cyan', 'lightblue', 'rosybrown', 'seagreen', 'sandybrown', 'salmon', 'silver', 'darkmagenta', 'darkorchid', 'honeydew',
'cadetblue', 'thistle', 'blue', 'palegoldenrod', 'cornflowerblue', 'lightseagreen', 'hotpink', 'gainsboro', 'aqua',
'lavender', 'darkturquoise', 'lightgray', 'lemonchiffon', 'cornsilk', 'chocolate', 'saddlebrown', 'darkolivegreen',
'beige', 'tomato', 'darkslateblue', 'darkorange', 'mediumseagreen', 'orchid', 'chartreuse', 'darkblue', 'indianred',
'lightsteelblue', 'navy', 'coral', 'limegreen', 'lime', 'sienna', 'deeppink', 'pink', 'gold', 'greenyellow', 'mediumorchid',
'crimson', 'mediumslateblue', 'aquamarine', 'plum', 'lightcoral', 'orange', 'lightsalmon', 'papayawhip', 'lightgoldenrodyellow',
'snow', 'olive', 'red', 'burlywood', 'mediumblue', 'darkcyan', 'skyblue', 'indigo', 'powderblue', 'darkgoldenrod', 'gray',
'oldlace', 'darkgray', 'blueviolet', 'royalblue', 'khaki', 'darkgreen', 'palegreen', 'dimgray', 'floralwhite', 'firebrick',
'navajowhite', 'mediumpurple', 'steelblue', 'lightcyan', 'ghostwhite', 'lightpink', 'bisque', 'whitesmoke', 'mediumturquoise',
'fuchsia', 'ivory', 'slategray', 'darkviolet', 'deepskyblue', 'seashell', 'mintcream', 'forestgreen', 'mediumspringgreen',
'azure', 'teal', 'green', 'wheat', 'lawngreen', 'lavenderblush', 'yellow', 'slateblue', 'peachpuff', 'mediumvioletred',
'violet', 'peru', 'white', 'orangered', 'lightskyblue', 'darkred', 'aliceblue', 'blanchedalmond', 'mistyrose', 'linen',
'yellowgreen', 'antiquewhite', 'springgreen', 'darkseagreen', 'lightgreen', 'lightslategray', 'goldenrod', 'paleturquoise',
'purple', 'magenta', 'turquoise', 'black', 'tan', 'mediumaquamarine', 'dodgerblue', 'midnightblue', 'palevioletred',
'darkslategray', 'lightyellow', 'maroon', 'brown', 'moccasin', 'olivedrab', 'darksalmon', 'darkkhaki']

folium地图可视化展示

使用pip安装folium后可以直接使用:

import folium
import itertools
from folium import plugins
m = folium.Map(location=[data.Latitude.median(), data.Longitude.median()],
zoom_start=12, zoom_control='False', control_scale=True,
tiles='http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}',
attr='© <a href="http://ditu.amap.com/">高德地图</a>'
)
# 为地图对象添加点击显示经纬度的子功能
# m = m.add_child(folium.LatLngPopup())

incidents = folium.map.FeatureGroup()
for p in data.values[:, [3, 2]]:
incidents.add_child(
folium.CircleMarker( # CircleMarker表示画圆
p.tolist(), # 坐标
radius=1, # 圆圈半径
color='gray', # 标志的外圈颜色
)
)
colors_iter = iter(colors)
for name1, y1, x1, i, ps, ds in find.sample(140).values:
color = next(colors_iter)
for p, d in zip(ps, ds):
j, name2, y2, x2 = data.loc[p]
folium.PolyLine(locations=[(x1, y1), (x2, y2)], color=color).add_to(m)
incidents.add_child(
folium.Circle( # CircleMarker表示画圆
(x2, y2), # 坐标
radius=50, # 圆圈半径
color='blue', # 标志的外圈颜色
tooltip=f"{name2}\n距离\n{name1}\n{d:.2f}米",
fill=True, # 是否填充
fill_color='blue', # 填充颜色
fill_opacity=0.05 # 填充透明度
)
)
incidents.add_child(
folium.CircleMarker( # CircleMarker表示画圆
(x1, y1), # 坐标
radius=1, # 圆圈半径
color='red', # 标志的外圈颜色
tooltip=name1,
)
)
m.add_child(incidents)

使用sklearn处理经纬度的三种距离计算与地图可视化_ide_07

上面是随机选取140个点的地图可视化效果,地图可以任意放大缩小。

使用sklearn处理经纬度的三种距离计算与地图可视化_数据库_08

这是放大后,一些点的可视化效果。

图中红色点表示从被查找点随机选取的140个点,每个红色点与找到的最近10个点会有一个连线,终点会画一个50米的圆,根据填充色的颜色深度可以知道当前点与其他点的重合程度,与其重合的点越多颜色越深。

结果整理

下面我们将查找到的结果组织成每行如下列的格式:

  • 查找点
  • 经度-查找点
  • 维度-查找点
  • 附件点
  • 经度-附件点
  • 维度-附件点
  • 距离-米

实际测试中发现,当找到距离等于0的点超过10个时,也可能不包含自身,所以发现不包含自身时也需要删除:

result = []
for name1, y1, x1, i, ps, ds in find.values:
try:
idx = ps.index(i)
except ValueError:
idx = -1
if idx > 0 or len(ps) > 10:
ps.pop(idx)
ds.pop(idx)
for p, d in zip(ps, ds):
name2, y2, x2 = data.loc[p].values[1:4]
result.append((name1, y1, x1, name2, y2, x2, d))
result = pd.DataFrame(result, columns=["查找点", "经度-查找点",
"维度-查找点", "附件点", "经度-附件点", "维度-附件点", "距离-米"])

使用sklearn处理经纬度的三种距离计算与地图可视化_数据库_09

纯数据处理的整体代码

from sklearn.neighbors import BallTree
from math import *
import pandas as pd

# 数据预处理
excel = pd.io.excel.ExcelFile("point.xlsx")
find = excel.parse("查找")
data = excel.parse("数据库")
# 去除重复数据
find.drop_duplicates(ignore_index=True, inplace=True)
data.drop_duplicates(ignore_index=True, inplace=True)
data.reset_index(drop=False, inplace=True)

data.rename(columns={"index": "索引"}, inplace=True)
find = pd.merge(find, data, how="left")
find.index = data.index[data.小区名称.isin(find.小区名称)]
data_p = data.values[:, 2:4]
find_p = find.values[:, 1:3]


def distancefuc(s1, s2):
"创建用于计算两个经纬度距离的函数"
# 经纬度转换成弧度
lon1, lat1 = map(radians, s1)
lon2, lat2 = map(radians, s2)
dlon = lon2 - lon1
dlat = lat2 - lat1
a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2
distance = round(2 * asin(sqrt(a)) * 6371000, 1) # 地球平均半径,6371km
return distance


bt = BallTree(data_p, metric=distancefuc)
distances, points = bt.query(find_p, 11, return_distance=True)
find["最近点索引"] = points.tolist()
find["距离"] = distances.tolist()
result = []
for name1, y1, x1, i, ps, ds in find.values:
try:
idx = ps.index(i)
except ValueError:
idx = -1
if idx > 0 or len(ps) > 10:
ps.pop(idx)
ds.pop(idx)
for p, d in zip(ps, ds):
name2, y2, x2 = data.loc[p].values[1:4]
result.append((name1, y1, x1, name2, y2, x2, d))
result = pd.DataFrame(result, columns=["查找点", "经度-查找点",
"维度-查找点", "附件点", "经度-附件点", "维度-附件点", "距离-米"])

至此我们就完成了最近N个点的查找。

下面我们再继续演示需求2:

查找每个点N米范围内的所有点

前面我们已经创建了BallTree模型,并加载了经纬度数据库数据,直接使用前面创建的模型即可。

例如我们要查找半径500米范围内的点:

# 与distances, points = nn.radius_neighbors(find_p, 500, return_distance=True)结果一样
points, distances = bt.query_radius(find_p, 500, return_distance=True)
print(distances[:1])
print(points[:1])
[array([364.7, 364.7, 364.7, 364.7, 364.7, 364.7, 364.7, 364.7, 364.7,
0. , 0. , 0. , 0. , 0. , 414.1, 414.1, 414.1, 414.1,
0. ]) ]
[array([ 20, 4065, 18, 1355, 4066, 16, 17, 4067, 19, 12, 2,
13, 11, 15, 1363, 34, 35, 36, 14], dtype=int64) ]
find["最近点索引"] = points.tolist()
find["距离"] = distances.tolist()

使用sklearn处理经纬度的三种距离计算与地图可视化_git_10

下面我通过folium地图可视化看看查找效果:

folium地图可视化展示

同样,随机选140个点,查看查找效果:

import folium
import itertools
from folium import plugins
m = folium.Map(location=[data.Latitude.median(), data.Longitude.median()],
zoom_start=12, zoom_control='False', control_scale=True,
tiles='http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}',
attr='© <a href="http://ditu.amap.com/">高德地图</a>'
)
# 为地图对象添加点击显示经纬度的子功能
# m = m.add_child(folium.LatLngPopup())

incidents = folium.map.FeatureGroup()
for p in data.values[:, [3, 2]]:
incidents.add_child(
folium.CircleMarker( # CircleMarker表示画圆
p.tolist(), # 坐标
radius=1, # 圆圈半径
color='grey', # 标志的外圈颜色
)
)
colors_iter = iter(colors)
for name1, y1, x1, i, ps, ds in find.sample(140).values:
# 对于每个被查找的点画一个500米范围的圆
incidents.add_child(
folium.Circle(
(x1, y1),
radius=500,
color='blue',
tooltip=name1,
)
)
color = next(colors_iter)
for (name2, y2, x2), d in zip(data.loc[ps].values[:, 1:], ds):
incidents.add_child(
folium.CircleMarker( # CircleMarker表示画圆
(x2, y2),
radius=3,
color=color,
tooltip=f"{name2}\n距离\n{name1}\r{d:.2f}米",
fill=True, # 是否填充
fill_color=color, # 填充颜色
fill_opacity=1 # 填充透明度
)
)
incidents.add_child(
folium.CircleMarker( # CircleMarker表示画圆
(x1, y1), # 坐标
radius=1, # 圆圈半径
color='red', # 标志的外圈颜色
tooltip=name1,
)
)
m.add_child(incidents)

使用sklearn处理经纬度的三种距离计算与地图可视化_ide_11

放大再看看:

使用sklearn处理经纬度的三种距离计算与地图可视化_git_12

经过几轮检查,可以发现被找到的点都在蓝色圆的范围内。证明我们使用BallTree模型找到的点都有效。

下面将结果整理成需求1一样的结果形式:

结果整理

result = []
for name1, y1, x1, i, ps, ds in find.values:
ps, ds = ps.tolist(), ds.tolist()
try:
idx = ps.index(i)
except ValueError:
idx = -1
if idx > 0:
ps.pop(idx)
ds.pop(idx)
for p, d in zip(ps, ds):
name2, y2, x2 = data.loc[p].values[1:4]
result.append((name1, y1, x1, name2, y2, x2, d))
result = pd.DataFrame(result, columns=["查找点", "经度-查找点",
"维度-查找点", "附件点", "经度-附件点", "维度-附件点", "距离-米"])

使用sklearn处理经纬度的三种距离计算与地图可视化_数据库_13

需求2整体代码

from sklearn.neighbors import BallTree
from math import *
import pandas as pd

# 数据预处理
excel = pd.io.excel.ExcelFile("point.xlsx")
find = excel.parse("查找")
data = excel.parse("数据库")
# 去除重复数据
find.drop_duplicates(ignore_index=True, inplace=True)
data.drop_duplicates(ignore_index=True, inplace=True)
data.reset_index(drop=False, inplace=True)

data.rename(columns={"index": "索引"}, inplace=True)
find = pd.merge(find, data, how="left")
find.index = data.index[data.小区名称.isin(find.小区名称)]
data_p = data.values[:, 2:4]
find_p = find.values[:, 1:3]


def distancefuc(s1, s2):
"创建用于计算两个经纬度距离的函数"
# 经纬度转换成弧度
lon1, lat1 = map(radians, s1)
lon2, lat2 = map(radians, s2)
dlon = lon2 - lon1
dlat = lat2 - lat1
a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2
distance = round(2 * asin(sqrt(a)) * 6371000, 1) # 地球平均半径,6371km
return distance


bt = BallTree(data_p, metric=distancefuc)
points, distances = bt.query_radius(find_p, 500, return_distance=True)
find["最近点索引"] = points.tolist()
find["距离"] = distances.tolist()
result = []
for name1, y1, x1, i, ps, ds in find.values:
ps, ds = ps.tolist(), ds.tolist()
try:
idx = ps.index(i)
except ValueError:
idx = -1
if idx > 0:
ps.pop(idx)
ds.pop(idx)
for p, d in zip(ps, ds):
name2, y2, x2 = data.loc[p].values[1:4]
result.append((name1, y1, x1, name2, y2, x2, d))
result = pd.DataFrame(result, columns=["查找点", "经度-查找点",
"维度-查找点", "附件点", "经度-附件点", "维度-附件点", "距离-米"])

聚类距离小于N米的点

DBSCAN(Density-Based Spatial Clustering of Applications with Noise,具有噪声的基于密度的聚类方法)是一种基于密度的空间聚类算法。 该算法将具有足够密度的区域划分为簇,并在具有噪声的空间数据库中发现任意形状的簇,它将簇定义为密度相连的点的最大集合。

使用sklearn处理经纬度的三种距离计算与地图可视化_git_14

数据预处理

为了减少无效计算,并方便可视化,下面先将经纬度数据库中经纬度完全相同的基站去重:

excel = pd.io.excel.ExcelFile("point.xlsx")
data = excel.parse("数据库")
# 去除经纬度的重复数据
data.drop_duplicates(subset=["Longitude", "Latitude"],
ignore_index=True, inplace=True)
print("经纬度数据库:", data.shape)
display(data.head())
data_p = data.values[:, 1:]
print(data_p.shape)

使用sklearn处理经纬度的三种距离计算与地图可视化_git_15

下面我们的需求是将半径500米范围内的基站都归为一类:

from sklearn.cluster import DBSCAN

db = DBSCAN(eps=500, min_samples=1, metric=distancefuc, algorithm='ball_tree')
# 调用DBSCAN方法进行训练,labels为每个数据的类别标签
labels = db.fit_predict(data_p)
data["类别"] = labels
data["类别数量"] = data.groupby("类别")["小区名称"].transform("count")

使用sklearn处理经纬度的三种距离计算与地图可视化_ide_16

至此通过密度聚类算法DBSCAN已经将距离小于500米的基站分到同一类,给了一个唯一的类别标签。

计算每个类别下的最大距离

下面,计算一下每个类别下所有基站之间的最大距离:

t = pd.Series(index=data.index)
for i, df_split in data.groupby("类别"):
ps = df_split[["Longitude", "Latitude"]].values
bt = BallTree(ps, metric=distancefuc)
k = len(ps)
_, ds = bt.query(ps, k)
t.loc[df_split.index] = ds.max()
data["最大距离"] =

使用sklearn处理经纬度的三种距离计算与地图可视化_ide_17

可视化聚类效果

import folium
import itertools
from folium import plugins
m = folium.Map(location=[data.Latitude.median(), data.Longitude.median()],
zoom_start=12, zoom_control='False', control_scale=True,
tiles='http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}',
attr='© <a href="http://ditu.amap.com/">高德地图</a>'
)
# 为地图对象添加点击显示经纬度的子功能
m = m.add_child(folium.LatLngPopup())

incidents = folium.map.FeatureGroup()

colors_iter = itertools.cycle(colors)
for i, df_split in data.groupby("类别"):
color = next(colors_iter)
for name, y, x in df_split.values[:, :3]:
incidents.add_child(
folium.Circle(
(x, y),
radius=500,
color=color,
tooltip=name,
)
)
incidents.add_child(
folium.CircleMarker(
(x, y),
radius=1,
color='red',
)
)

m.add_child(incidents)

使用sklearn处理经纬度的三种距离计算与地图可视化_git_18

任意选个位置放大看看:

使用sklearn处理经纬度的三种距离计算与地图可视化_git_19

需求3数据处理整体代码

from math import *
from sklearn.cluster import DBSCAN
import pandas as pd
from sklearn.neighbors import BallTree

data = pd.read_excel("point.xlsx", "数据库")
# 去除经纬度的重复数据
data.drop_duplicates(subset=["Longitude", "Latitude"],
ignore_index=True, inplace=True)
print("经纬度数据库:", data.shape)
data_p = data.values[:, 1:]


def distancefuc(s1, s2):
"创建用于计算两个经纬度距离的函数"
# 经纬度转换成弧度
lon1, lat1 = map(radians, s1)
lon2, lat2 = map(radians, s2)
dlon = lon2 - lon1
dlat = lat2 - lat1
a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2
distance = round(2 * asin(sqrt(a)) * 6371000, 1) # 地球平均半径,6371km
return distance


db = DBSCAN(eps=500, min_samples=1, metric=distancefuc, algorithm='ball_tree')
# 调用DBSCAN方法进行训练,labels为每个数据的类别标签
labels = db.fit_predict(data_p)
data["类别"] = labels
data["类别数量"] = data.groupby("类别")["小区名称"].transform("count")
t = pd.Series(index=data.index)
for i, df_split in data.groupby("类别"):
ps = df_split[["Longitude", "Latitude"]].values
bt = BallTree(ps, metric=distancefuc)
k = len(ps)
_, ds = bt.query(ps, k)
t.loc[df_split.index] = ds.max()
data["最大距离"] =