9.10 数组排序

本节是《Python 数据科学手册》(Python Data Science Handbook)的摘录。

译者:飞龙

协议:CC BY-NC-SA 4.0

到目前为止,我们主要关注使用 NumPy 访问和操作数组数据的工具。本节介绍与 NumPy 数组中的值的排序相关的算法。

这些算法是计算机科学入门课程中最受欢迎的主题:如果你曾经上过这些课,你可能对插入排序,选择排序,归并排序,快速排序,冒泡排序,甚至更多的东西,有一些回忆(或者是噩梦,取决于你的性格)。

所有这些都是完成类似任务的方法:对列表或数组中的值排序。例如,简单的选择排序重复查找列表中的最小值,并进行交换直到列表是有序的。我们可以在几行 Python 中编写代码:

import numpy as npdef selection_sort(x):for i in range(len(x)):swap = i + np.argmin(x[i:])(x[i], x[swap]) = (x[swap], x[i])return xx = np.array([2, 1, 4, 3, 5])
selection_sort(x)# array([1, 2, 3, 4, 5])

正如任何计算机科学专业的一年级学生都会告诉你的那样,出于它的简单性,选择排序很有用,但是对于较大的数组来说太慢了。对于N个元素的列表,它需要N个循环,每个循环都执行大约N个比较,来查找要交换的值。

就通常用于表示这些算法的“大 O”记号而言(参见“大 O 记号”),选择排序平均是O(n^2)的:如果你将列表中的项目数加倍,执行时间将增加大约四倍。

尽管选择排序比我最喜欢的排序算法,bogosort要好得多:

def bogosort(x):while np.any(x[:-1] > x[1:]):np.random.shuffle(x)return xx = np.array([2, 1, 4, 3, 5])
bogosort(x)# array([1, 2, 3, 4, 5])

这种愚蠢的排序方法纯粹依赖于机会:它反复应用数组的随机打乱,直到结果是有序的。平均规模为O(n * n!),这显然应该永远不会用于任何实际计算。

幸运的是,Python包含内置的排序算法,这些算法比刚刚展示的任何简单算法都高效得多。 我们将首先查看 Python 内置函数,然后查看 NumPy 中包含的,并针对 NumPy 数组优化的例程。

NumPy 中的快速排序:np.sortnp.argsort

尽管 Python 内置了sortsorted函数来处理列表,但我们不会在这里讨论它们,因为 NumPy 的np.sort函数效率更高,对于我们的目的更有用。

默认情况下,np.sort使用O(nlogn)的快速排序算法,但归并排序和堆排序也可用。 对于大多数应用程序,默认的快速排序绰绰有余。

x = np.array([2, 1, 4, 3, 5])
np.sort(x)# array([1, 2, 3, 4, 5])

如果你希望对数组原地排序,则可以使用数组的sort方法:

x.sort()
print(x)# [1 2 3 4 5]

相关函数是argsort,它返回已排序元素的下标:

x = np.array([2, 1, 4, 3, 5])
i = np.argsort(x)
print(i)# [1 0 3 2 4]

此结果的第一个元素给出最小元素的索引,第二个值给出第二小元素的索引,依此类推。然后,如果需要,可以使用这些索引(通过花式索引)构造有序数组:

x[i]# array([1, 2, 3, 4, 5])

沿行或列的排序

NumPy 排序算法的一个有用特性是,能够使用axis参数来排序多维数组的特定行或列。例如:

rand = np.random.RandomState(42)
X = rand.randint(0, 10, (4, 6))
print(X)'''
[[6 3 7 4 6 9][2 6 7 4 3 7][7 2 5 4 1 7][5 1 4 0 9 5]]
'''# 排序 X 的每一列
np.sort(X, axis=0)'''
array([[2, 1, 4, 0, 1, 5],[5, 2, 5, 4, 3, 7],[6, 3, 7, 4, 6, 7],[7, 6, 7, 4, 9, 9]])
'''# 排序 X 的每一行
np.sort(X, axis=1)'''
array([[3, 4, 6, 6, 7, 9],[2, 3, 4, 6, 7, 7],[1, 2, 4, 5, 7, 7],[0, 1, 4, 5, 5, 9]])
'''

请记住,这会将每个行或列视为一个独立的数组,并且行或列值之间的任何关系都将丢失!

部分排序:分区

有时我们对排序整个数组不感兴趣,但只想在数组中找到k个最小值。 NumPy 在np.partition函数中提供了它。np.partition接受一个数组和一个数字K;结果是一个新数组,最小的K个值在分区左边,任意顺序的剩下的值在右边:

x = np.array([7, 2, 3, 1, 6, 5, 4])
np.partition(x, 3)# array([2, 1, 3, 4, 6, 5, 7])

请注意,结果数组中的前三个值是数组中的三个最小值,其余数组位置包含其余值。在这两个分区中,元素具有任意顺序。

与排序类似,我们可以沿多维数组的任意轴进行分区:

np.partition(X, 2, axis=1)'''
array([[3, 4, 6, 7, 6, 9],[2, 3, 4, 7, 6, 7],[1, 2, 4, 5, 7, 7],[0, 1, 4, 5, 9, 5]])
'''

结果是一个数组,其中每行中的前两个槽包含该行中的最小值,其余值填充剩余的槽。

最后,就像计算有序索引的np.argsort一样,np.argpartition来计算分区的索引。我们将在下一节中看到它。

示例:K 最近邻

让我们快速了解如何沿着多个轴使用这个argsort函数,来查找集合中每个点的最近邻居。我们首先在二维平面上创建一组 10 个随机点。使用标准惯例,我们将在10x2数组中存放它们:

X = rand.rand(10, 2)

为了了解这些点的外观,让我们快速绘制散点图:

%matplotlib inline
import matplotlib.pyplot as plt
import seaborn; seaborn.set() # 绘图风格
plt.scatter(X[:, 0], X[:, 1], s=100);

现在我们将计算每对点之间的距离。回想一下,两点之间的平方距离是每个维度的平方差的总和;使用由 NumPy 提供的,高效广播(“数组计算:广播”)和聚合(“聚合:最小值,最大值和之间的一切”)的例程,我们可以在一行代码中计算平方距离矩阵:

dist_sq = np.sum((X[:, np.newaxis, :] - X[np.newaxis, :, :]) ** 2, axis=-1)

这个操作有很多内容,如果你不熟悉 NumPy 的广播规则,可能会有点混乱。 当你遇到这样的代码时,将其分解为子步骤会很有用:

# 对于每一对点
# 计算坐标的差
differences = X[:, np.newaxis, :] - X[np.newaxis, :, :]
differences.shape# (10, 10, 2)# 计算坐标的差
sq_differences = differences ** 2
sq_differences.shape# (10, 10, 2)# 对坐标差求和来获取距离平方
dist_sq = sq_differences.sum(-1)
dist_sq.shape# (10, 10)

为了仔细检查我们正在做什么,我们应该看到这个矩阵的对角线(即每个点和它自身之间的距离)都是零:

dist_sq.diagonal()# array([ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.])

就是这样!使用转换的成对的平方距离,我们现在可以使用np.argsort对每行排序。 最左边的列将给出最近邻居的索引:

nearest = np.argsort(dist_sq, axis=1)
print(nearest)'''
[[0 3 9 7 1 4 2 5 6 8][1 4 7 9 3 6 8 5 0 2][2 1 4 6 3 0 8 9 7 5][3 9 7 0 1 4 5 8 6 2][4 1 8 5 6 7 9 3 0 2][5 8 6 4 1 7 9 3 2 0][6 8 5 4 1 7 9 3 2 0][7 9 3 1 4 0 5 8 6 2][8 5 6 4 1 7 9 3 2 0][9 7 3 0 1 4 5 8 6 2]]
'''

请注意,第一列按顺序给出数字 0 到 9:这是因为每个点的最近邻居就是它自己,这就是我们所期望的。

通过在这里使用完整的排序,在这种情况下,我们实际上完成的工作比我们需要的更多。 如果我们只是对最近的k个邻居感兴趣,我们所需要的就是对每一行进行分区,以便最小的k + 1个平方距离首先出现,更大的距离填充数组的剩余位置。 我们可以使用np.argpartition函数执行此操作:

K = 2
nearest_partition = np.argpartition(dist_sq, K + 1, axis=1)

为了可视化这个邻居网络,让我们快速绘制点和线,它表示从每个点到其两个最近邻居的连接:

plt.scatter(X[:, 0], X[:, 1], s=100)# 绘制每个点到它的两个最近邻的直线
K = 2for i in range(X.shape[0]):for j in nearest_partition[i, :K+1]:# 绘制 X[i] 到 X[j] 的直线# 使用一些 zip 魔法来实现plt.plot(*zip(X[j], X[i]), color='black')

图中的每个点都有到两个最近邻居的绘制的线。看一眼,有些点有两条以上的线可能看起来很奇怪:这是因为如果 A 是 B 的两个最近邻之一,这并不一定意味着 B 是 A 的两个最近邻点之一。

虽然这种方法的广播和逐行排序,可能看起来不像编写循环那么简单,但事实证明,这是在 Python 中对这些数据进行操作的一种非常有效的方法。

你可能会尝试通过手动循环数据,并单独对每组邻居进行排序,来执行相同类型的操作,但这几乎肯定会产生比我们使用的向量化版本更慢的算法。 这种方法的优点在于,它的编写方式与输入数据的大小无关:我们可以在任意数量的维度中轻松计算 100 或 1,000,000 个点的邻居,并且代码看起来相同。

最后,我会注意到,在进行非常大的最近邻搜索时,有基于树的算法和/或近似算法,可以变为O(nlogn)或更好,而不是O(n^2)的暴力算法。 其中一个例子是 KD-Tree,在 Scikit-learn 中实现。

注:大 O 记号

大 O 记号是一种方法,描述算法所需操作数量随输入大小增长的变化。为了正确使用它,需要深入研究计算机科学理论的领域,并仔细区分它与相关的小 o 符号,大 θ\thetaθ 符号,大 Ω\OmegaΩ 符号,以及可能的许多变体。

虽然这些区别使算法规模,外部计算机科学理论考试和迂腐的博客评论者的评论更加精确,但你很少会在实践中看到这种区别。

在数据科学领域更常见的是使用不太严格的大 O 记号:作为算法规模的一般(如果不精确)描述。向理论家和学生道歉,这是我们将在本书中使用的解释。

在这种宽松的意义上,大 O 记号会告诉你,在增加数据量时算法将花费多少时间。如果你有一个O(N)(读取N阶)的算法,它在长度为N = 1,000的列表上运行需要 1 秒,那么对于长度N = 5,000,你应该预计大约需要 5 秒。如果你有一个O(N^2)(读取N方阶)的算法,对于N = 1000它需要 1 秒,那么对于N = 5000,你应该预计它需要大约 25 秒。

出于我们的目的,N通常表示数据集大小的某些方面(点数,维数等)。 当试图分析数十亿或数万亿的样本时,O(N)O(N^2)之间的差异可能并不是微不足道!

请注意,大 O 记号本身不会告诉你计算的实际时钟时间,而只会告诉你在更改N时的规模。通常,例如,O(N)算法被认为具有比O(N^2)算法更好的规模,并且有充分的理由。 但是对于小型数据集,规模更好的算法可能不会更快。

例如,在给定问题中,O(N^2)算法可能需要 0.01 秒,而“更好的”O(N)算法可能需要 1 秒。然而,将N按比例放大 1000 倍,O(N)算法将胜出。

在比较算法的性能时,即使是这个松散版本的大 O 记号也非常有用,在讨论算法如何扩展时,我们将在整本书中使用这种记号。

数据科学 IPython 笔记本 9.10 数组排序相关推荐

  1. 数据科学 IPython 笔记本 8.10 自定义颜色条

    8.10 自定义颜色条 原文:Customizing Colorbars 译者:飞龙 协议:CC BY-NC-SA 4.0 本节是<Python 数据科学手册>(Python Data S ...

  2. 数据科学 IPython 笔记本 7.10 组合数据集:合并和连接

    7.10 组合数据集:合并和连接 原文:Combining Datasets: Merge and Join 译者:飞龙 协议:CC BY-NC-SA 4.0 本节是<Python 数据科学手册 ...

  3. 数据科学 IPython 笔记本 翻译完成

    原文:donnemartin/data-science-ipython-notebooks 译者:飞龙 协议:CC BY-NC-SA 4.0 欢迎任何人参与和完善:一个人可以走的很快,但是一群人却可以 ...

  4. 数据科学 IPython 笔记本 8.9 自定义图例

    8.9 自定义图例 原文:Customizing Plot Legends 译者:飞龙 协议:CC BY-NC-SA 4.0 本节是<Python 数据科学手册>(Python Data ...

  5. 数据科学 IPython 笔记本 8.7 密度和等高线图

    8.7 密度和等高线图 原文:Density and Contour Plots 译者:飞龙 协议:CC BY-NC-SA 4.0 本节是<Python 数据科学手册>(Python Da ...

  6. 数据科学 IPython 笔记本 8.4 简单的折线图

    8.4 简单的折线图 原文:Simple Line Plots 译者:飞龙 协议:CC BY-NC-SA 4.0 本节是<Python 数据科学手册>(Python Data Scienc ...

  7. 数据科学 IPython 笔记本 8.3 Matplotlib 可视化

    8.3 Matplotlib 可视化 原文:Visualization with Matplotlib 译者:飞龙 协议:CC BY-NC-SA 4.0 本节是<Python 数据科学手册> ...

  8. 数据科学 IPython 笔记本 8.14 自定义 Matplotlib:配置和样式表

    8.14 自定义 Matplotlib:配置和样式表 原文:Customizing Matplotlib: Configurations and Stylesheets 译者:飞龙 协议:CC BY- ...

  9. 数据科学 IPython 笔记本 8.12 文本和注解

    8.12 文本和注解 原文:Text and Annotation 译者:飞龙 协议:CC BY-NC-SA 4.0 本节是<Python 数据科学手册>(Python Data Scie ...

最新文章

  1. “电商+金融”融合发展 开辟金融创新新路径
  2. 神策数据林美天于大数据与人工智能分享沙龙分享
  3. leetcode 572. 另一个树的子树(Java版)
  4. MicroBlaze程序融合fpga的bit并烧写入Flash方法
  5. html在状态栏中显示时间,html网页时间显示代码和倒计时代码大全
  6. android缓存框架SimpleCache
  7. HDU 3790 最短路径问题 (SPFA)
  8. sort、qsort排序
  9. IBM OmniFind Enterprise Starter Edition
  10. 计算机如何制作U盘启动盘,如何制作u盘启动盘三种方式教你
  11. QFIL and FASTBOOT
  12. 2020年被“冰封”的猫眼、淘票票、大麦们,还能看见春天吗?
  13. 常用编码说明-GB2312
  14. 腐蚀rust服务器命令_服务器指令_腐蚀rust服务器命令一览 腐蚀rust有哪些服务器命令_3DM单机...
  15. Spark 名词解释
  16. 某百度程序员:每天十点上班,午休两小时,每天闲逛,晚上八点就下班!
  17. Axure谷歌浏览器扩展程序下载及安装方法(免翻墙)
  18. Java对数组对象进行排序
  19. 【密码学】DES加解密原理及其Java实现算法
  20. 高斯判别分析(GDA)公式推导

热门文章

  1. (95)FPGA仿真文件保存(VCD文件)
  2. (56)FPGA条件选择有优先级(case)
  3. (2)zynq FPGA AXI_Lite总线介绍
  4. vuex commit 模块_一分钟上手Vuex
  5. ios 请在设置中打开相机权限_在iOS中请求摄像头权限对话框启动(Prime权限)
  6. 用winformz时间格式不正确_霜冻不可怕,用生态防寒布正确预防减少损失
  7. TCP协议的部分解析(3)
  8. SVD 与 PCA 的直观解释(2): 特征值与特征向量
  9. C语言中全局变量和局部变量,作用域与生命周期的相关问题。
  10. 嵌入式Linux系统编程学习之二十二内存映射