百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

Python 数据分析——SciPy 空间算法库-spatial

bigegpt 2024-08-28 12:08 7 浏览

本节介绍scipy.spatial模块中提供的K-d树、凸包、沃罗诺伊图、德劳内三角化等空间算法类的使用方法。

一、计算最近旁点

众所周知,对于一维的已排序的数值,可以使用二分法快速找到与指定数值最近的数值。在下面的例子中,在一个升序排序的随机数数组x中,使用numpy.searchsorted()搜索离0.5最近的数。排序算法的时间复杂度为O(NlogN),而每次二分搜索的时间复杂度为O(logN)。

x = np.sort(np.random.rand(100))
idx = np.searchsorted(x, 0.5)
print x[idx], x[idx - 1] #距离0.5最近的数是这两个数中的一个
0.542258714465 0.492205345391

类似的算法可以推广到N维空间,spatial模块提供的cKDTree类使用K-d树快速搜索N维空间中的最近点。在下面的例子中,用平面上随机的100个点创建cKDTree对象,并对其搜索与targets中每个点距离最近的3个点。cKDTree.query()返回两个数组dist和idx,dist[i, :]是距离targets[i]最近的3个点的距离,而idx[i, :]是这些最近点的下标:

from scipy import spatial
np.random.seed(42)
N = 100
points = np.random.uniform(-1, 1, (N, 2))
kd = spatial.cKDTree(points)

targets = np.array([(0, 0), (0.5, 0.5), (-0.5, 0.5), (0.5, -0.5), (-0.5, -0.5)])
dist, idx = kd.query(targets, 3)
dist                         idx
-----------------------------------------  --------------
[[ 0.15188266,  0.21919416,  0.27647793],  [[48, 73, 81],
[ 0.09595807,  0.15745334,  0.22855398],   [37, 78, 43],
[ 0.05009422,  0.17583445,  0.1807312 ],   [79, 22, 92],
[ 0.11180181,  0.16618122,  0.18127473],   [35, 58,  6],
[ 0.19015485,  0.19060739,  0.19361173]]   [83,  7, 42]]

cKDTree.query_ball_point()搜索与指定点在一定距离之内的所有点,它只返回最近点的下标,由于每个目标点的近旁点数不一定相同,因此idx2数组中的每个元素都是一个列表:

r = 0.2
idx2 = kd.query_ball_point(targets, r)
idx2
array([[48], [37, 78], [79, 92, 22], [58, 35, 6], [7, 55, 83, 42]], dtype=object)

cKDTree.query_pairs()找到points中距离小于指定值的每一对点,它返回的是一个下标对的集合对象。下面的程序使用集合差集运算找出所有距离在0.08到0.1之间的点对:

idx3 = kd.query_pairs(0.1) - kd.query_pairs(0.08)
idx3
{(1, 46),     (3, 21),     (3, 82),     (3, 95),     (5, 16),     (9, 30),
(10, 87),    (11, 42),    (11, 97),    (18, 41),    (29, 74),    (32, 51),
(37, 78),    (39, 61),    (41, 61),    (50, 84),    (55, 83),    (73, 81)}

在图1中,与target中的每个点(用五角星表示)最近的点用与其相同的颜色标识:

图1 用cKDTree寻找近旁点

cKDTree的所有搜索方法都有一个参数p,用于定义计算两点之间距离的函数。读者可以尝试使用不同的p参数,观察图1的变化:

·p = 1:绝对值之和作为距离

·p = 2:欧式距离

·p = np.inf:最大坐标差值作为距离

此外,cKDTree.query_ball_tree()可以在两棵K-d树之间搜索距离小于给定值的所有点对。

distance子模块中的pdist()计算一组点中每对点的距离,而cdist()计算两组点中每对点的距离。由于pdist()返回的是一个压缩之后的一维数组,需要用squareform()将其转换成二维数组。dist1[i, j]是points中下标为i和j的两个点的距离,dist2[i, j]是points[i]和targets[j]之间的距离。

from scipy.spatial import distance
dist1 = distance.squareform(distance.pdist(points))
dist2 = distance.cdist(points, targets)
dist1.shape  dist2.shape
-----------  -----------
(100, 100)   (100, 5)

下面使用np.min()在dist2中搜索points中与targets距离最近的点,其结果与cKDTree.query()的结果相同:

print dist[:, 0] # cKDTree.query()返回的与targets最近的距离
print np.min(dist2, axis=0)
[ 0.15188266  0.09595807  0.05009422  0.11180181  0.19015485]
[ 0.15188266  0.09595807  0.05009422  0.11180181  0.19015485]

为了找到points中最近的点对,需要将dist1对角线上的元素填充为无穷大:

dist1[np.diag_indices(len(points))] = np.inf
nearest_pair = np.unravel_index(np.argmin(dist1), dist1.shape)
print nearest_pair, dist1[nearest_pair]
(22, 92) 0.00534621024816

用cKDTree.query()可以快速找到这个距离最近的点对:在K-d树中搜索它自己包含的点,找到与每个点最近的两个点,其中距离最近的点就是它本身,距离为0,而距离第二近的点就是每个点的最近旁点,然后只需要找到这些距离中最小的那个即可:

dist, idx = kd.query(points, 2)
print idx[np.argmin(dist[:, 1])], np.min(dist[:, 1])
[22 92] 0.00534621024816

让我们看一个使用K-d树提高搜索速度的实例。下面的start和end数组保存用户登录和离开网站的时间,对于任意指定的时刻time,计算该时刻在线用户的数量。

N = 1000000
start = np.random.uniform(0, 100, N)
span = np.random.uniform(0.01, 1, N)
span = np.clip(span, 2, 100)
end = start + span

下面的naive_count_at()采用逐个比较的方法计算指定时间的在线用户数量:

def naive_count_at(start, end, time):
mask = (start<time)&(end>time)
return np.sum(mask)

图2显示了如何使用K-d树实现快速搜索。图中每点的横坐标为start,纵坐标为end。由于end大于start,因此所有的点都在y=x斜线的上方。图中阴影部分表示满足(start<time)&(end>time)条件的区域。该区域中的点数为时刻time时的在线人数。

图2 使用二维K-d树搜索指定区间的在线用户

tree.count_neighbors(other, r, p=2)可以统计tree中到other的距离小于r的点数,其中p为计算距离的范数。距离按照如下公式计算:

当p=2时,上面的公式就是欧几里得空间中的向量长度。当p=∞时,该距离变为各个轴上的最大绝对值:

N∞(x)=||x||∞=max(|x1|,|x2|…,|xn|)

当使用p=∞时可以计算某正方形区域之内的点数。我们将该正方形的中心设置为(time - max_time, time + max_time),正方形的边长为2 * max_time,即r = max_time。其中max_time为end中的最大值。下面的KdSearch类实现了该算法:

class KdSearch(object):
def __init__(self, start, end, leafsize=10):
self.tree = spatial.cKDTree(np.c_[start, end], leafsize=leafsize)
self.max_time = np.max(end)

def count_at(self, time):
max_time = self.max_time
to_search = spatial.cKDTree([[time - max_time, time + max_time]])
return self.tree.count_neighbors(to_search, max_time, p=np.inf)

naive_count_at(start, end, 40) == KdSearch(start, end).count_at(40)
True

下面比较运算时间,由结果可知创建K-d树需要约0.5秒时间,K-d树的搜索速度则为线性搜索的17倍左右。

请读者研究点数N和leafsize参数与创建K-d树和搜索时间之间的关系。

%time ks = KdSearch(start, end, leafsize=100)
%timeit naive_search(start, end, 40)
%timeit ks.count_at(40)
Wall time: 484 ms
100 loops, best of 3: 3.85 ms per loop
1000 loops, best of 3: 221 μs per loop

二、凸包

所谓凸包是指N维空间中的一个区域,该区域中任意两点之间的线段都完全被包含在该区域之中,二维平面上的凸多边形就是典型的凸包。ConvexHull可以快速计算包含N维空间中点的集合的最小凸包。下面先看一个二维的例子:points2d是一组二维平面上的随机点,ch2d是这些点的凸包对象。ConvexHull.simplices是凸包的每条边线的两个顶点在points2d中的下标,由于它的形状为(5, 2),因此凸包由5条线段构成。对于二维的情况,ConvexHull.vertices是凸多边形的每个顶点在points2d中的下标,按逆时针方向的顺序排列。

np.random.seed(42)
points2d = np.random.rand(10, 2)
ch2d = spatial.ConvexHull(points2d)
ch2d.simplices   ch2d.vertices
--------------  ---------------
[[2, 5],        [5, 2, 6, 1, 0]
[2, 6],
[0, 5],
[1, 6],
[1, 0]]

使用matplotlib中的Polygon对象可以绘制如图3所示的多边形。

图3 二维平面上的凸包

poly = pl.Polygon(points2d[ch2d.vertices], fill=None, lw=2, color="r", alpha=0.5)
ax = pl.subplot(aspect="equal")
pl.plot(points2d[:, 0], points2d[:, 1], "go")
for i, pos in enumerate(points2d):
pl.text(pos[0], pos[1], str(i), color="blue")
ax.add_artist(poly)

三维空间中的凸包是一个凸多面体,每个面都是一个三角形。在下面的例子中,由simplices的形状可知,所得到的凸包由38个三角形面构成:

np.random.seed(42)
points3d = np.random.rand(40, 3)
ch3d = spatial.ConvexHull(points3d)
ch3d.simplices.shape
(38, 3)

下面的程序用TVTK直观地显示凸包,图4中所有的绿色圆球表示points3d中的点,由红色线段构成的三角形面表示凸多面体上的面。没有和红色线段相连的点就是凸包之内的点:

图4 三维空间中的凸包

from scpy2 import vtk_convexhull, vtk_scene, vtk_scene_to_array
actors = vtk_convexhull(ch3d)
scene = vtk_scene(actors, viewangle=22)
%array_image vtk_scene_to_array(scene)
scene.close()

如果读者希望采用三维交互界面,可以在Notebook中执行如下代码:

%gui qt
from scpy2 import ivtk_scene
ivtk_scene(actors)

三、沃罗诺伊图

沃罗诺伊图(Voronoi Diagram)是一种空间分割算法,它根据指定的N个胞点将空间分割为N个区域,每个区域中的所有坐标点都与该区域对应的胞点最近。

points2d = np.array([[0.2, 0.1], [0.5, 0.5], [0.8, 0.1],
[0.5, 0.8], [0.3, 0.6], [0.7, 0.6], [0.5, 0.35]])
vo = spatial.Voronoi(points2d)
vo.vertices           vo.regions     vo.ridge_vertices
--------------------  -----------------  -----------------
[[ 0.5   ,  0.045 ],   [[-1, 0, 1],       [[-1, 0],
[ 0.245 ,  0.351 ],   [-1, 0, 2],        [0, 1],
[ 0.755 ,  0.351 ],   [],                [-1, 1],
[ 0.3375,  0.425 ],   [6, 4, 3, 5],      [0, 2],
[ 0.6625,  0.425 ],   [5, -1, 1, 3],     [-1, 2],
[ 0.45  ,  0.65  ],   [4, 2, 0, 1, 3],   [3, 5],
[ 0.55  ,  0.65  ]]   [6, -1, 2, 4],     [3, 4],
[6, -1, 5]]        [4, 6],
[5, 6],
[1, 3],
[-1, 5],
[2, 4],
[-1, 6]]

使用voronoi_plot_2d()可以将沃罗诺伊图显示为图表,效果如图5(左)所示。图中蓝色小圆点为points2d指定的胞点,红色大圆点表示Voronoi.vertices中的点,图中为每个vertices点标注了下标。由虚线和实线将空间分为7个区域,以虚线为边的区域为无限大的区域,一直向外延伸,全部由实线构成的区域为有限区域。每个区域都以vertices中的点为顶点。

图5 沃罗诺伊图将空间分割为多个区域

Voronoi.regions是区域列表,其中每个区域由一个列表(忽略空列表)表示,列表中的整数为vertices中的序号,包含-1的区域为无限区域。例如[6, 4, 3, 5]为图中正中心的那块区域。

Voronoi.ridge_vertices是区域分割线列表,每条分割线由vertices中的两个序号构成,包含-1的分割线为图中的虚线,其长度为无限长。

如果希望将每块区域以不同颜色填充,但由于外围的区域是无限大的,因此无法使用matplotlib绘图,可以在外面添加4个点将整个区域围起来,这样每个points2d中的胞点都对应一个有限区域。在图5(右)中,黑圈点为points2d指定的胞点,将空间中与其最近的区域填充成胞点的颜色。

bound = np.array([[-100, -100], [-100,  100],
[ 100,  100], [ 100, -100]])
vo2 = spatial.Voronoi(np.vstack((points2d, bound)))

使用沃罗诺伊图可以解决最大空圆问题:找到一个半径最大的圆,使得其圆心在一组点的凸包区域之内,并且圆内没有任何点。根据图5可知最大空圆必定是以vertices为圆心,以与最近胞点的距离为半径的圆中的某一个。为了找到胞点与vertices之间的联系,可以使用Voronoi.point_region属性。point_region[i]是与第i个胞点(points2d[i])最近的区域的编号。例如由下面的输出可知,下标为5的蓝色点与下标为6的区域对应,而由Voronoi.regions[6]可知,该区域由编号为6、2、4的三个vertices点(图中红色的点)构成。因此vertices中与胞点points2d[5]最近的点的序号为[2, 4, 6]。

print vo.point_region
print vo.regions[6]
[0 3 1 7 4 6 5]
[6, -1, 2, 4]

下面是计算最大空圆的程序,效果如图6所示。程序中:?使用pylab.Polygon.contains_point()判断用ConvexHull计算的凸包多边形是否包含vertices中的点,用以在?处剔除圆心在凸包之外的圆。

图6 使用沃罗诺伊图计算最大空圆

?vertice_point_map是一个字典,它的键为vertices点的下标,值为与其最近的几个points2d点的序号。整个字典使用point_region和regions构建,注意这里剔除了所有在凸包之外的vertices点。

?对于vertice_point_map中的每一对点,找到距离最大的那对点,即可得出圆心坐标和圆的半径。

from collections import defaultdict

n = 50
np.random.seed(42)
points2d = np.random.rand(n, 2)
vo = spatial.Voronoi(points2d)
ch = spatial.ConvexHull(points2d)
poly = pl.Polygon(points2d[ch.vertices]) ?
vs = vo.vertices
convexhull_mask = [poly.contains_point(p, radius=0) for p in vs] ?

vertice_point_map = defaultdict(list) ?
for index_point, index_region in enumerate(vo.point_region):
region = vo.regions[index_region]
if -1 in region: continue
for index_vertice in region:
if convexhull_mask[index_vertice]:
vertice_point_map[index_vertice].append(index_point)

def dist(p1, p2):
return ((p1-p2)**2).sum()**0.5

max_cicle = max((dist(points2d[pidxs[0]], vs[vidx]), vs[vidx]) ?
for vidx, pidxs in vertice_point_map.iteritems())
r, center = max_cicle
print "r = ", r, ", center = ", center
r =  0.174278456762 , center =  [ 0.46973363  0.59356531]

四、德劳内三角化

德劳内三角化算法对给定的点集合的凸包进行三角形分割,使得每个三角形的外接圆都不含任何点。下面的程序演示了德劳内三角化的效果:

Delaunay对象dy的simplices属性是每个三角形的顶点在points2d中的下标。可以通过三角形的顶点坐标计算其外接圆的圆心,不过这里使用同一点集合的沃罗诺伊图的vertices属性,vertices就是每个三角形的外接圆的圆心。

x = np.array([46.445, 263.251, 174.176, 280.899, 280.899,
189.358, 135.521, 29.638, 101.907, 226.665])
y = np.array([287.865, 250.891, 287.865, 160.975, 54.252,
160.975, 232.404, 179.187, 35.765, 71.361])
points2d = np.c_[x, y]
dy = spatial.Delaunay(points2d)
vo = spatial.Voronoi(points2d)
dy.simplices            vo.vertices
------------  --------------------------------
[[8, 5, 7],   [[ 104.58977484,  127.03566055],
[1, 5, 3],    [ 235.1285    ,  198.68143374],
[5, 6, 7],    [ 107.83960707,  155.53682482],
[6, 0, 7],    [  71.22104881,  228.39479887],
[0, 6, 2],    [ 110.3105    ,  291.17642838],
[6, 1, 2],    [ 201.40695449,  227.68436282],
[1, 6, 5],    [ 201.61895891,  226.21958623],
[9, 5, 8],    [ 152.96231864,   93.25060083],
[4, 9, 8],    [ 205.40381294,  -90.5480267 ],
[5, 9, 3],    [ 235.1285    ,  127.45701644],
[9, 4, 3]]    [ 267.91709907,  107.6135    ]]

下面是用delaunay_plot_2d()绘制德劳内三角化的结果,此外还绘制每个外接圆及其圆心。可以看到三角形的所有顶点都不在任何外接圆之内,效果如图7所示:

图7 德劳内三角形的外接圆与圆心?

cx, cy = vo.vertices.T

ax = pl.subplot(aspect="equal")
spatial.delaunay_plot_2d(dy, ax=ax)
ax.plot(cx, cy, "r.")
for i, (cx, cy) in enumerate(vo.vertices):
px, py = points2d[dy.simplices[i, 0]]
radius = np.hypot(cx - px, cy - py)
circle = pl.Circle((cx, cy), radius, fill=False, ls="dotted")
ax.add_artist(circle)
ax.set_xlim(0, 300)
ax.set_ylim(0, 300)

相关推荐

当Frida来“敲”门(frida是什么)

0x1渗透测试瓶颈目前,碰到越来越多的大客户都会将核心资产业务集中在统一的APP上,或者对自己比较重要的APP,如自己的主业务,办公APP进行加壳,流量加密,投入了很多精力在移动端的防护上。而现在挖...

服务端性能测试实战3-性能测试脚本开发

前言在前面的两篇文章中,我们分别介绍了性能测试的理论知识以及性能测试计划制定,本篇文章将重点介绍性能测试脚本开发。脚本开发将分为两个阶段:阶段一:了解各个接口的入参、出参,使用Python代码模拟前端...

Springboot整合Apache Ftpserver拓展功能及业务讲解(三)

今日分享每天分享技术实战干货,技术在于积累和收藏,希望可以帮助到您,同时也希望获得您的支持和关注。架构开源地址:https://gitee.com/msxyspringboot整合Ftpserver参...

Linux和Windows下:Python Crypto模块安装方式区别

一、Linux环境下:fromCrypto.SignatureimportPKCS1_v1_5如果导包报错:ImportError:Nomodulenamed'Crypt...

Python 3 加密简介(python des加密解密)

Python3的标准库中是没多少用来解决加密的,不过却有用于处理哈希的库。在这里我们会对其进行一个简单的介绍,但重点会放在两个第三方的软件包:PyCrypto和cryptography上,我...

怎样从零开始编译一个魔兽世界开源服务端Windows

第二章:编译和安装我是艾西,上期我们讲述到编译一个魔兽世界开源服务端环境准备,那么今天跟大家聊聊怎么编译和安装我们直接进入正题(上一章没有看到的小伙伴可以点我主页查看)编译服务端:在D盘新建一个文件夹...

附1-Conda部署安装及基本使用(conda安装教程)

Windows环境安装安装介质下载下载地址:https://www.anaconda.com/products/individual安装Anaconda安装时,选择自定义安装,选择自定义安装路径:配置...

如何配置全世界最小的 MySQL 服务器

配置全世界最小的MySQL服务器——如何在一块IntelEdison为控制板上安装一个MySQL服务器。介绍在我最近的一篇博文中,物联网,消息以及MySQL,我展示了如果Partic...

如何使用Github Action来自动化编译PolarDB-PG数据库

随着PolarDB在国产数据库领域荣膺桂冠并持续获得广泛认可,越来越多的学生和技术爱好者开始关注并涉足这款由阿里巴巴集团倾力打造且性能卓越的关系型云原生数据库。有很多同学想要上手尝试,却卡在了编译数据...

面向NDK开发者的Android 7.0变更(ndk android.mk)

订阅Google官方微信公众号:谷歌开发者。与谷歌一起创造未来!受Android平台其他改进的影响,为了方便加载本机代码,AndroidM和N中的动态链接器对编写整洁且跨平台兼容的本机...

信创改造--人大金仓(Kingbase)数据库安装、备份恢复的问题纪要

问题一:在安装KingbaseES时,安装用户对于安装路径需有“读”、“写”、“执行”的权限。在Linux系统中,需要以非root用户执行安装程序,且该用户要有标准的home目录,您可...

OpenSSH 安全漏洞,修补操作一手掌握

1.漏洞概述近日,国家信息安全漏洞库(CNNVD)收到关于OpenSSH安全漏洞(CNNVD-202407-017、CVE-2024-6387)情况的报送。攻击者可以利用该漏洞在无需认证的情况下,通...

Linux:lsof命令详解(linux lsof命令详解)

介绍欢迎来到这篇博客。在这篇博客中,我们将学习Unix/Linux系统上的lsof命令行工具。命令行工具是您使用CLI(命令行界面)而不是GUI(图形用户界面)运行的程序或工具。lsoflsof代表&...

幻隐说固态第一期:固态硬盘接口类别

前排声明所有信息来源于网络收集,如有错误请评论区指出更正。废话不多说,目前固态硬盘接口按速度由慢到快分有这几类:SATA、mSATA、SATAExpress、PCI-E、m.2、u.2。下面我们来...

新品轰炸 影驰SSD多款产品登Computex

分享泡泡网SSD固态硬盘频道6月6日台北电脑展作为全球第二、亚洲最大的3C/IT产业链专业展,吸引了众多IT厂商和全球各地媒体的热烈关注,全球存储新势力—影驰,也积极参与其中,为广大玩家朋友带来了...