Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
OpenDocCN
ds-ipynb-zh
提交
58dd8dfd
D
ds-ipynb-zh
项目概览
OpenDocCN
/
ds-ipynb-zh
10 个月 前同步成功
通知
1
Star
74
Fork
24
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
D
ds-ipynb-zh
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
前往新版Gitcode,体验更适合开发者的 AI 搜索 >>
提交
58dd8dfd
编写于
1月 04, 2019
作者:
W
wizardforcel
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
9.10-9.11
上级
23192bb3
变更
4
隐藏空白更改
内联
并排
Showing
4 changed file
with
496 addition
and
0 deletion
+496
-0
docs/9.10.md
docs/9.10.md
+291
-0
docs/9.11.md
docs/9.11.md
+205
-0
img/9-10-1.png
img/9-10-1.png
+0
-0
img/9-10-2.png
img/9-10-2.png
+0
-0
未找到文件。
docs/9.10.md
0 → 100644
浏览文件 @
58dd8dfd
## 9.10 数组排序
> 本节是[《Python 数据科学手册》](https://github.com/jakevdp/PythonDataScienceHandbook)(Python Data Science Handbook)的摘录。
>
> 译者:[飞龙](https://github.com/wizardforcel)
>
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
到目前为止,我们主要关注使用 NumPy 访问和操作数组数据的工具。本节介绍与 NumPy 数组中的值的排序相关的算法。
这些算法是计算机科学入门课程中最受欢迎的主题:如果你曾经上过这些课,你可能对插入排序,选择排序,归并排序,快速排序,冒泡排序,甚至更多的东西,有一些回忆(或者是噩梦,取决于你的性格)。
所有这些都是完成类似任务的方法:对列表或数组中的值排序。
例如,简单的选择排序重复查找列表中的最小值,并进行交换直到列表是有序的。我们可以在几行 Python 中编写代码:
```
py
import
numpy
as
np
def
selection_sort
(
x
):
for
i
in
range
(
len
(
x
)):
swap
=
i
+
np
.
argmin
(
x
[
i
:])
(
x
[
i
],
x
[
swap
])
=
(
x
[
swap
],
x
[
i
])
return
x
x
=
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`
要好得多:
```
py
def
bogosort
(
x
):
while
np
.
any
(
x
[:
-
1
]
>
x
[
1
:]):
np
.
random
.
shuffle
(
x
)
return
x
x
=
np
.
array
([
2
,
1
,
4
,
3
,
5
])
bogosort
(
x
)
# array([1, 2, 3, 4, 5])
```
这种愚蠢的排序方法纯粹依赖于机会:它反复应用数组的随机打乱,直到结果是有序的。平均规模为
`O(n * n!)`
,这显然应该永远不会用于任何实际计算。
幸运的是,Python包含内置的排序算法,这些算法比刚刚展示的任何简单算法都高效得多。 我们将首先查看 Python 内置函数,然后查看 NumPy 中包含的,并针对 NumPy 数组优化的例程。
### NumPy 中的快速排序:``np.sort``和``np.argsort``
尽管 Python 内置了
``sort``
和
``sorted``
函数来处理列表,但我们不会在这里讨论它们,因为 NumPy 的
``np.sort``
函数效率更高,对于我们的目的更有用。
默认情况下,
``np.sort``
使用
`O(nlogn)`
的快速排序算法,但归并排序和堆排序也可用。 对于大多数应用程序,默认的快速排序绰绰有余。
```
py
x
=
np
.
array
([
2
,
1
,
4
,
3
,
5
])
np
.
sort
(
x
)
# array([1, 2, 3, 4, 5])
```
如果你希望对数组原地排序,则可以使用数组的
``sort``
方法:
```
py
x
.
sort
()
print
(
x
)
# [1 2 3 4 5]
```
相关函数是
``argsort``
,它返回已排序元素的下标:
```
py
x
=
np
.
array
([
2
,
1
,
4
,
3
,
5
])
i
=
np
.
argsort
(
x
)
print
(
i
)
# [1 0 3 2 4]
```
此结果的第一个元素给出最小元素的索引,第二个值给出第二小元素的索引,依此类推。然后,如果需要,可以使用这些索引(通过花式索引)构造有序数组:
```
py
x
[
i
]
# array([1, 2, 3, 4, 5])
```
#### 沿行或列的排序
NumPy 排序算法的一个有用特性是,能够使用
``axis``
参数来排序多维数组的特定行或列。例如:
```
py
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`
个值在分区左边,任意顺序的剩下的值在右边:
```
py
x
=
np
.
array
([
7
,
2
,
3
,
1
,
6
,
5
,
4
])
np
.
partition
(
x
,
3
)
# array([2, 1, 3, 4, 6, 5, 7])
```
请注意,结果数组中的前三个值是数组中的三个最小值,其余数组位置包含其余值。在这两个分区中,元素具有任意顺序。
与排序类似,我们可以沿多维数组的任意轴进行分区:
```
py
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`
数组中存放它们:
```
py
X
=
rand
.
rand
(
10
,
2
)
```
为了了解这些点的外观,让我们快速绘制散点图:
```
py
%
matplotlib
inline
import
matplotlib.pyplot
as
plt
import
seaborn
;
seaborn
.
set
()
# 绘图风格
plt
.
scatter
(
X
[:,
0
],
X
[:,
1
],
s
=
100
);
```
![
png
](
../img/9-10-1.png
)
现在我们将计算每对点之间的距离。回想一下,两点之间的平方距离是每个维度的平方差的总和;使用由 NumPy 提供的,高效广播(“数组计算:广播”)和聚合(“聚合:最小值,最大值和之间的一切”)的例程,我们可以在一行代码中计算平方距离矩阵:
```
py
dist_sq
=
np
.
sum
((
X
[:,
np
.
newaxis
,
:]
-
X
[
np
.
newaxis
,
:,
:])
**
2
,
axis
=-
1
)
```
这个操作有很多内容,如果你不熟悉 NumPy 的广播规则,可能会有点混乱。 当你遇到这样的代码时,将其分解为子步骤会很有用:
```
py
# 对于每一对点
# 计算坐标的差
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)
```
为了仔细检查我们正在做什么,我们应该看到这个矩阵的对角线(即每个点和它自身之间的距离)都是零:
```
py
dist_sq
.
diagonal
()
# array([ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
```
就是这样!使用转换的成对的平方距离,我们现在可以使用
``np.argsort``
对每行排序。 最左边的列将给出最近邻居的索引:
```
py
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``
函数执行此操作:
```
py
K
=
2
nearest_partition
=
np
.
argpartition
(
dist_sq
,
K
+
1
,
axis
=
1
)
```
为了可视化这个邻居网络,让我们快速绘制点和线,它表示从每个点到其两个最近邻居的连接:
```
py
plt
.
scatter
(
X
[:,
0
],
X
[:,
1
],
s
=
100
)
# 绘制每个点到它的两个最近邻的直线
K
=
2
for
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'
)
```
![
png
](
../img/9-10-2.png
)
图中的每个点都有到两个最近邻居的绘制的线。看一眼,有些点有两条以上的线可能看起来很奇怪:这是因为如果 A 是 B 的两个最近邻之一,这并不一定意味着 B 是 A 的两个最近邻点之一。
虽然这种方法的广播和逐行排序,可能看起来不像编写循环那么简单,但事实证明,这是在 Python 中对这些数据进行操作的一种非常有效的方法。
你可能会尝试通过手动循环数据,并单独对每组邻居进行排序,来执行相同类型的操作,但这几乎肯定会产生比我们使用的向量化版本更慢的算法。 这种方法的优点在于,它的编写方式与输入数据的大小无关:我们可以在任意数量的维度中轻松计算 100 或 1,000,000 个点的邻居,并且代码看起来相同。
最后,我会注意到,在进行非常大的最近邻搜索时,有基于树的算法和/或近似算法,可以变为
`O(nlogn)`
或更好,而不是
`O(n^2)`
的暴力算法。 其中一个例子是 KD-Tree,
[
在 Scikit-learn 中实现
](
http://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KDTree.html
)
。
### 注:大 O 记号
大 O 记号是一种方法,描述算法所需操作数量随输入大小增长的变化。为了正确使用它,需要深入研究计算机科学理论的领域,并仔细区分它与相关的小 o 符号,大 $
\t
heta$ 符号,大 $
\O
mega$ 符号,以及可能的许多变体。
虽然这些区别使算法规模,外部计算机科学理论考试和迂腐的博客评论者的评论更加精确,但你很少会在实践中看到这种区别。
在数据科学领域更常见的是使用不太严格的大 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 记号也非常有用,在讨论算法如何扩展时,我们将在整本书中使用这种记号。
docs/9.11.md
0 → 100644
浏览文件 @
58dd8dfd
## 9.11 结构化数据:NumPy 的结构化数组
> 本节是[《Python 数据科学手册》](https://github.com/jakevdp/PythonDataScienceHandbook)(Python Data Science Handbook)的摘录。
虽然我们的数据通常可以通过同构数组来很好地表示,但有时并非如此。 本节演示了 NumPy 结构化数组和记录数组的用法,它们为复合异构数据提供了有效的存储。 虽然这里展示的模式对于简单操作很有用,但像这样的场景通常适合使用 Pandas
`Dataframe`
,我们将在第三章中探索。
```
py
import
numpy
as
np
```
想象一下,我们有很多人的多个数据类别(比如姓名,年龄和体重),我们希望存储这些值以便在 Python 程序中使用。可以将它们存储在三个独立的数组中:
```
py
name
=
[
'Alice'
,
'Bob'
,
'Cathy'
,
'Doug'
]
age
=
[
25
,
45
,
37
,
19
]
weight
=
[
55.0
,
85.5
,
68.0
,
61.5
]
```
但这有点笨拙。 这里没有任何东西告诉我们三个数组是相关的;如果我们可以使用单一结构来存储所有这些数据,那将更自然。NumPy 可以使用结构化数组处理这个问题,结构化数组是具有复合数据类型的数组。
回想一下,之前我们使用这样的表达式创建了一个简单的数组:
```
py
x
=
np
.
zeros
(
4
,
dtype
=
int
)
```
我们可以使用复合数据类型规范,以相似方式创建结构化数组:
```
py
# 使用结构化数组的复合数据类型
data
=
np
.
zeros
(
4
,
dtype
=
{
'names'
:(
'name'
,
'age'
,
'weight'
),
'formats'
:(
'U10'
,
'i4'
,
'f8'
)})
print
(
data
.
dtype
)
# [('name', '<U10'), ('age', '<i4'), ('weight', '<f8')]
```
这里
`'U10'`
表示“最大长度为 10 的 Unicode 字符串”,
`'i4'`
表示 4 字节(即 32 位)整数,
`'f8'`
表示 8 字节(即 64 位)浮点数。我们将在下一节中讨论这些类型代码的其他选项。
现在我们已经创建了一个空的容器数组,我们可以使用我们的值列表填充数组:
```
py
data
[
'name'
]
=
name
data
[
'age'
]
=
age
data
[
'weight'
]
=
weight
print
(
data
)
'''
[('Alice', 25, 55.0) ('Bob', 45, 85.5) ('Cathy', 37, 68.0)
('Doug', 19, 61.5)]
'''
```
正如我们所希望的那样,数据现在被安排在一个方便的内存块中。结构化数组的便利之处在于,你现在可以通过索引或名称来引用值:
```
py
# 获取所有名称
data
[
'name'
]
'''
array(['Alice', 'Bob', 'Cathy', 'Doug'],
dtype='<U10')
'''
# 获取数据的第一行
data
[
0
]
# ('Alice', 25, 55.0)
# 获取最后一行的名称
data
[
-
1
][
'name'
]
# 'Doug'
```
使用布尔掩码,你甚至可以执行一些更复杂的操作,例如过滤年龄:
```
py
# 获取年龄小于 30 的名称
data
[
data
[
'age'
]
<
30
][
'name'
]
'''
array(['Alice', 'Doug'],
dtype='<U10')
'''
```
请注意,如果你想进行任何比这些更复杂的操作,你应该考虑下一章中介绍的 Pandas 包。正如我们所看到的,Pandas 提供了
``Dataframe``
对象,它是一个构建在 NumPy 数组上的结构,它提供了各种有用的数据操作功能,类似于我们在这里展示的东西,以及更多。
### 创建结构化数组
可以通过多种方式规定结构化数组数据类型。之前,我们见过了字典方法:
```
py
np
.
dtype
({
'names'
:(
'name'
,
'age'
,
'weight'
),
'formats'
:(
'U10'
,
'i4'
,
'f8'
)})
# dtype([('name', '<U10'), ('age', '<i4'), ('weight', '<f8')])
```
为清楚起见,可以使用 Python 类型或 NumPy
`dtype`
来指定数字类型:
```
py
np
.
dtype
({
'names'
:(
'name'
,
'age'
,
'weight'
),
'formats'
:((
np
.
str_
,
10
),
int
,
np
.
float32
)})
# dtype([('name', '<U10'), ('age', '<i8'), ('weight', '<f4')])
```
复合类型也可以指定为元组列表:
```
py
np
.
dtype
([(
'name'
,
'S10'
),
(
'age'
,
'i4'
),
(
'weight'
,
'f8'
)])
# dtype([('name', 'S10'), ('age', '<i4'), ('weight', '<f8')])
```
如果类型的名称对你无关紧要,则可以在逗号分隔的字符串中单独指定类型:
```
py
np
.
dtype
(
'S10,i4,f8'
)
# dtype([('f0', 'S10'), ('f1', '<i4'), ('f2', '<f8')])
```
缩短的字符串格式代码可能看起来令人困惑,但它们建立在简单的原则之上。第一个(可选)字符是
``<``
或
``>``
,分别表示“小端”或“大端”,并规定了有效位的顺序约定。下一个字符指定数据类型:字符,字节,整数,浮点等(参见下表)。最后一个或多个字符表示对象的大小(以字节为单位)。
| 字符 | 描述 | 示例 |
| --------- | ----------- | ------- |
|
``'b'``
| 字节 |
``np.dtype('b')``
|
|
``'i'``
| 符号整数 |
``np.dtype('i4') == np.int32``
|
|
``'u'``
| 无符号整数 |
``np.dtype('u1') == np.uint8``
|
|
``'f'``
| 浮点 |
``np.dtype('f8') == np.int64``
|
|
``'c'``
| 复数浮点 |
``np.dtype('c16') == np.complex128``
|
|
``'S'``
,
``'a'``
| 字符串 |
``np.dtype('S5')``
|
|
``'U'``
| Unicode 字符串 |
``np.dtype('U') == np.str_``
|
|
``'V'``
| 原始数据(void) |
``np.dtype('V') == np.void``
|
### 更高级的复合类型
可以定义更高级的复合类型。例如,你可以创建一个类型,其中每个元素包含一个数组或矩阵。在这里,我们将创建一个带有
``mat``
成分的数据类型,该成分由
`3x3`
浮点矩阵组成:
```
py
tp
=
np
.
dtype
([(
'id'
,
'i8'
),
(
'mat'
,
'f8'
,
(
3
,
3
))])
X
=
np
.
zeros
(
1
,
dtype
=
tp
)
print
(
X
[
0
])
print
(
X
[
'mat'
][
0
])
'''
(0, [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]])
[[ 0. 0. 0.]
[ 0. 0. 0.]
[ 0. 0. 0.]]
'''
```
现在
``X``
数组中的每个元素都包含一个
``id``
和一个
`3x3`
矩阵。为什么要使用它而不是简单的多维数组,或者 Python 字典呢?原因是这个 NumPy
``dtype``
直接映射到 C 结构定义,因此包含数组内容的缓冲区,可以在适当编写的 C 程序中直接访问。
如果你发现自己为处理结构化数据的遗留 C 或 Fortran 库编写 Python 接口,你可能会发现结构化数组非常有用!
### 记录数组:具有扭曲的结构化阵列
NumPy 还提供了
``np.recarray``
类,它与刚刚描述的结构化数组几乎相同,但有一个附加功能:字段可以作为属性而不是字典的键来访问。
回想一下,我们以前写过:
```
py
data
[
'age'
]
# array([25, 45, 37, 19], dtype=int32)
```
如果我们将数据视为记录数组,我们可以通过更少的敲键盘来访问它:
```
py
data_rec
=
data
.
view
(
np
.
recarray
)
data_rec
.
age
# array([25, 45, 37, 19], dtype=int32)
```
缺点是对于记录数组,即使使用相同的语法,访问字段会有一些额外的开销。 我们在这里可以看到:
```
py
%
timeit
data
[
'age'
]
%
timeit
data_rec
[
'age'
]
%
timeit
data_rec
.
age
'''
1000000 loops, best of 3: 241 ns per loop
100000 loops, best of 3: 4.61 µs per loop
100000 loops, best of 3: 7.27 µs per loop
'''
```
更方便的记号是否值得额外开销,取决于你自己的应用。
### 转向 Pandas
关于结构化和记录数组的这一部分,有意放在本章的最后部分,因为它很好地介绍了我们将要介绍的下一个包:Pandas。
在某些情况下,最好了解这里讨论的结构化数组,特别是在你使用 NumPy 数组来映射到 C,Fortran 或其他语言的二进制数据格式的情况下。
对于结构化数据的日常使用,Pandas 包是一个更好的选择,我们将在下一章中深入讨论它。
img/
10_32_0
.png
→
img/
9-10-1
.png
浏览文件 @
58dd8dfd
文件已移动
img/
10_46_0
.png
→
img/
9-10-2
.png
浏览文件 @
58dd8dfd
文件已移动
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录