原文:
annas-archive.org/md5/d2f94efd019a2e2cb5c4fa9f260d63c
译者:飞龙 协议:CC BY-NC-SA 4.0
第四章:使用 NumPy 进行数值计算
计算机是无用的。它们只能给出答案。 巴勃罗·毕加索
介绍
本章介绍了 Python
的基本数据类型和数据结构。尽管 Python
解释器本身已经带来了丰富的数据结构,但 NumPy
和其他库以有价值的方式添加了这些数据结构。
本章组织如下:
数据数组
本节详细讨论了数组的概念,并说明了在 Python 中处理数据数组的基本选项。
NumPy 数据结构
本节致力于介绍 NumPy
ndarray
类的特性和功能,并展示了该类对科学和金融应用的一些好处。
代码向量化
本节说明了,由于 NumPy
的数组类,向量化代码很容易实现,从而导致代码更紧凑,性能更好。
本章涵盖了以下数据结构:
对象类型 | 含义 | 用法/模型 |
---|---|---|
ndarray(常规) | n 维数组对象 | 大量数值数据的大数组 |
ndarray(记录) | 二维数组对象 | 以列组织的表格数据 |
本章组织如下:
“数据数组”
本节讨论了使用纯 Python 代码处理数据数组的方法。
[待添加链接]
这是关于常规 NumPy
ndarray
类的核心部分;它是几乎所有数据密集型 Python 使用案例中的主要工具。
[待添加链接]
这个简短的部分介绍了用于处理带有列的表格数据的结构化(或记录)ndarray
对象。
“代码的向量化”
在本节中,讨论了代码的向量化及其好处;该部分还讨论了在某些情况下内存布局的重要性。
数据数组
前一章表明 Python
提供了一些非常有用和灵活的通用数据结构。特别是,list
对象可以被认为是一个真正的工作马,具有许多方便的特性和应用领域。在一般情况下,使用这样一个灵活的(可变的)数据结构的代价在于相对较高的内存使用量,较慢的性能或两者兼有。然而,科学和金融应用通常需要对特殊数据结构进行高性能操作。在这方面最重要的数据结构之一是数组。数组通常以行和列的形式结构化其他(基本)相同数据类型的对象。
暂时假设我们仅使用数字,尽管这个概念也可以推广到其他类型的数据。在最简单的情况下,一维数组在数学上表示为向量,通常由float
对象内部表示为实数的一行或一列元素组成。在更普遍的情况下,数组表示为i × j 矩阵的元素。这个概念在三维中也可以推广为i × j × k 立方体的元素以及形状为i × j × k × l × …的一般n维数组。
线性代数和向量空间理论等数学学科说明了这些数学结构在许多科学学科和领域中的重要性。因此,设计一个专门的数据结构类来方便和高效地处理数组可能是非常有益的。这就是Python
库NumPy
的作用所在,其ndarray
类应运而生。在下一节介绍其强大的ndarray
类之前,本节展示了两种处理数组的替代方法。
使用 Python 列表的数组
在转向NumPy
之前,让我们首先用上一节介绍的内置数据结构构建数组。list
对象特别适用于完成这项任务。一个简单的list
已经可以被视为一维数组:
In [1]: v = [0.5, 0.75, 1.0, 1.5, 2.0] # ①
①
list
对象与数字。
由于list
对象可以包含任意其他对象,它们也可以包含其他list
对象。通过嵌套list
对象,可以轻松构建二维和更高维的数组:
In [2]: m = [v, v, v] # ①
m # ②
Out[2]: [[0.5, 0.75, 1.0, 1.5, 2.0],
[0.5, 0.75, 1.0, 1.5, 2.0],
[0.5, 0.75, 1.0, 1.5, 2.0]]
①
list
对象与list
对象…
②
… 得到一个数字矩阵。
我们还可以通过简单的索引选择行或通过双重索引选择单个元素(然而,选择整列并不那么容易):
代码语言:javascript复制In [3]: m[1]
Out[3]: [0.5, 0.75, 1.0, 1.5, 2.0]
In [4]: m[1][0]
Out[4]: 0.5
嵌套可以进一步推广到更一般的结构:
代码语言:javascript复制In [5]: v1 = [0.5, 1.5]
v2 = [1, 2]
m = [v1, v2]
c = [m, m] # ①
c
Out[5]: [[[0.5, 1.5], [1, 2]], [[0.5, 1.5], [1, 2]]]
In [6]: c[1][1][0]
Out[6]: 1
①
立方数。
请注意,刚刚介绍的对象组合方式通常使用对原始对象的引用指针。这在实践中意味着什么?让我们看看以下操作:
代码语言:javascript复制In [7]: v = [0.5, 0.75, 1.0, 1.5, 2.0]
m = [v, v, v]
m
Out[7]: [[0.5, 0.75, 1.0, 1.5, 2.0],
[0.5, 0.75, 1.0, 1.5, 2.0],
[0.5, 0.75, 1.0, 1.5, 2.0]]
现在修改v
对象的第一个元素的值,看看m
对象会发生什么变化:
In [8]: v[0] = 'Python'
m
Out[8]: [['Python', 0.75, 1.0, 1.5, 2.0],
['Python', 0.75, 1.0, 1.5, 2.0],
['Python', 0.75, 1.0, 1.5, 2.0]]
通过使用copy
模块的deepcopy
函数,可以避免这种情况:
In [9]: from copy import deepcopy
v = [0.5, 0.75, 1.0, 1.5, 2.0]
m = 3 * [deepcopy(v), ] # ①
m
Out[9]: [[0.5, 0.75, 1.0, 1.5, 2.0],
[0.5, 0.75, 1.0, 1.5, 2.0],
[0.5, 0.75, 1.0, 1.5, 2.0]]
In [10]: v[0] = 'Python' # ②
m # ③
Out[10]: [[0.5, 0.75, 1.0, 1.5, 2.0],
[0.5, 0.75, 1.0, 1.5, 2.0],
[0.5, 0.75, 1.0, 1.5, 2.0]]
①
使用物理副本而不是引用指针。
②
因此,对原始对象的更改…
③
… 不再有任何影响。
Python 数组类
Python 中有一个专用的array
模块可用。正如您可以在文档页面上阅读到的(参见https://docs.python.org/3/library/array.html):
该模块定义了一种对象类型,可以紧凑地表示基本值的数组:字符、整数、浮点数。数组是序列类型,并且行为非常像列表,只是存储在其中的对象类型受到限制。类型在对象创建时通过使用类型代码(一个单个字符)来指定。
考虑以下代码,将一个list
对象实例化为一个array
对象。
In [11]: v = [0.5, 0.75, 1.0, 1.5, 2.0]
In [12]: import array
In [13]: a = array.array('f', v) # ①
a
Out[13]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0])
In [14]: a.append(0.5) # ②
a
Out[14]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0, 0.5])
In [15]: a.extend([5.0, 6.75]) # ②
a
Out[15]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0, 0.5, 5.0, 6.75])
In [16]: 2 * a # ③
Out[16]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0, 0.5, 5.0, 6.75, 0.5, 0.75, 1.0, 1.5, 2.0, 0.5, 5.0, 6.75])
①
使用float
作为类型代码实例化array
对象。
②
主要方法的工作方式类似于list
对象的方法。
③
虽然“标量乘法”原理上可行,但结果不是数学上预期的;而是元素被重复。
尝试附加与指定数据类型不同的对象会引发TypeError
。
In [17]: # a.append('string') # ①
In [18]: a.tolist() # ②
Out[18]: [0.5, 0.75, 1.0, 1.5, 2.0, 0.5, 5.0, 6.75]
①
仅能附加float
对象;其他数据类型/类型代码会引发错误。
②
然而,如果需要这样的灵活性,array
对象可以轻松转换回list
对象。
array
类的一个优点是它具有内置的存储和检索功能。
In [19]: f = open('array.apy', 'wb') # ①
a.tofile(f) # ②
f.close() # ③
In [20]: with open('array.apy', 'wb') as f: # ④
a.tofile(f) # ④
In [21]: !ls -n arr* # ⑤
-rw-r--r--@ 1 503 20 32 29 Dez 17:08 array.apy
①
打开一个用于写入二进制数据的磁盘上的文件。
②
将array
数据写入文件。
③
关闭文件。
④
或者,可以使用with
上下文执行相同的操作。
⑤
这显示了磁盘上写入的文件。
与以前一样,从磁盘读取数据时,array
对象的数据类型很重要。
In [22]: b = array.array('f') # ①
In [23]: with open('array.apy', 'rb') as f: # ②
b.fromfile(f, 5) # ③
In [24]: b # ④
Out[24]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0])
In [25]: b = array.array('d') # ⑤
In [26]: with open('array.apy', 'rb') as f:
b.fromfile(f, 2) # ⑥
In [27]: b # ⑦
Out[27]: array('d', [0.0004882813645963324, 0.12500002956949174])
①
使用类型代码float
创建一个新的array
对象。
②
打开文件以读取二进制数据…
③
…并在b
对象中读取五个元素。
④
使用类型代码double
创建一个新的array
对象。
⑤
从文件中读取两个元素。
⑥
类型代码的差异导致“错误”的数字。
⑦
常规 NumPy 数组
显然,使用list
对象构成数组结构有些作用。但这并不是真正方便的方式,而且list
类并没有为此特定目标而构建。它的范围更广泛,更一般。array
类已经稍微更专业一些,提供了一些有用的特性来处理数据数组。然而,某种“高度”专业化的类因此可能真的对处理数组类型的结构非常有益。
基础知识
这样一个专门的类就是numpy.ndarray
类,它的特定目标是方便且高效地处理n维数组,即以高性能的方式。这个类的基本处理最好通过示例来说明:
In [28]: import numpy as np # ①
In [29]: a = np.array([0, 0.5, 1.0, 1.5, 2.0]) # ②
a
Out[29]: array([ 0. , 0.5, 1. , 1.5, 2. ])
In [30]: type(a) # ②
Out[30]: numpy.ndarray
In [31]: a = np.array(['a', 'b', 'c']) # ③
a
Out[31]: array(['a', 'b', 'c'],
dtype='<U1')
In [32]: a = np.arange(2, 20, 2) # ④
a
Out[32]: array([ 2, 4, 6, 8, 10, 12, 14, 16, 18])
In [33]: a = np.arange(8, dtype=np.float) # ⑤
a
Out[33]: array([ 0., 1., 2., 3., 4., 5., 6., 7.])
In [34]: a[5:] # ⑥
Out[34]: array([ 5., 6., 7.])
In [35]: a[:2] # ⑥
Out[35]: array([ 0., 1.])
①
导入numpy
包。
②
通过list
对象中的浮点数创建一个ndarray
对象。
③
通过list
对象中的字符串创建一个ndarray
对象。
④
np.arange
的工作方式类似于range
。
⑤
然而,它接受附加输入dtype
参数。
⑥
对于一维的ndarray
对象,索引的工作方式与平常一样。
ndarray
类的一个重要特性是内置方法的多样性。例如:
In [36]: a.sum() # ①
Out[36]: 28.0
In [37]: a.std() # ②
Out[37]: 2.2912878474779199
In [38]: a.cumsum() # ③
Out[38]: array([ 0., 1., 3., 6., 10., 15., 21., 28.])
①
所有元素的总和。
②
元素的标准偏差。
③
所有元素的累积和(从索引位置 0 开始)。
另一个重要特性是对ndarray
对象定义的(向量化的)数学运算:
In [39]: l = [0., 0.5, 1.5, 3., 5.]
2 * l # ①
Out[39]: [0.0, 0.5, 1.5, 3.0, 5.0, 0.0, 0.5, 1.5, 3.0, 5.0]
In [40]: a
Out[40]: array([ 0., 1., 2., 3., 4., 5., 6., 7.])
In [41]: 2 * a # ②
Out[41]: array([ 0., 2., 4., 6., 8., 10., 12., 14.])
In [42]: a ** 2 # ③
Out[42]: array([ 0., 1., 4., 9., 16., 25., 36., 49.])
In [43]: 2 ** a # ④
Out[43]: array([ 1., 2., 4., 8., 16., 32., 64., 128.])
In [44]: a ** a # ⑤
Out[44]: array([ 1.00000000e 00, 1.00000000e 00, 4.00000000e 00,
2.70000000e 01, 2.56000000e 02, 3.12500000e 03,
4.66560000e 04, 8.23543000e 05])
①
与list
对象的“标量乘法”导致元素的重复。
②
相比之下,使用ndarray
对象实现了适当的标量乘法,例如。
③
这个计算每个元素的平方值。
④
这解释了ndarray
的元素作为幂。
⑤
这个计算每个元素的自身的幂。
NumPy
包的另一个重要功能是通用函数。它们在一般情况下对ndarray
对象以及基本 Python 数据类型进行操作。然而,当将通用函数应用于 Python float
对象时,需要注意与math
模块中相同功能的性能降低。
In [45]: np.exp(a) # ①
Out[45]: array([ 1.00000000e 00, 2.71828183e 00, 7.38905610e 00,
2.00855369e 01, 5.45981500e 01, 1.48413159e 02,
4.03428793e 02, 1.09663316e 03])
In [46]: np.sqrt(a) # ②
Out[46]: array([ 0. , 1. , 1.41421356, 1.73205081, 2. ,
2.23606798, 2.44948974, 2.64575131])
In [47]: np.sqrt(2.5) # ③
Out[47]: 1.5811388300841898
In [48]: import math # ④
In [49]: math.sqrt(2.5) # ④
Out[49]: 1.5811388300841898
In [50]: # math.sqrt(a) # ⑤
In [51]: %timeit np.sqrt(2.5) # ⑥
703 ns ± 17.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [52]: %timeit math.sqrt(2.5) # ⑦
107 ns ± 1.48 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
①
逐个元素计算指数值。
②
计算每个元素的平方根。
③
计算 Python float
对象的平方根。
④
相同的计算,这次使用math
模块。
⑤
math.sqrt
不能直接应用于ndarray
对象。
⑥
将通用函数np.sqrt
应用于 Python float
对象……
⑦
……比使用math.sqrt
函数的相同操作慢得多。
多维度
切换到多维度是无缝的,并且到目前为止呈现的所有特征都适用于更一般的情况。特别是,索引系统在所有维度上保持一致:
代码语言:javascript复制In [53]: b = np.array([a, a * 2]) # ①
b
Out[53]: array([[ 0., 1., 2., 3., 4., 5., 6., 7.],
[ 0., 2., 4., 6., 8., 10., 12., 14.]])
In [54]: b[0] # ②
Out[54]: array([ 0., 1., 2., 3., 4., 5., 6., 7.])
In [55]: b[0, 2] # ③
Out[55]: 2.0
In [56]: b[:, 1] # ④
Out[56]: array([ 1., 2.])
In [57]: b.sum() # ⑤
Out[57]: 84.0
In [58]: b.sum(axis=0) # ⑥
Out[58]: array([ 0., 3., 6., 9., 12., 15., 18., 21.])
In [59]: b.sum(axis=1) # ⑦
Out[59]: array([ 28., 56.])
①
用一维数组构造二维ndarray
对象。
②
选择第一行。
③
选择第一行的第三个元素;在括号内,索引由逗号分隔。
④
选择第二列。
⑤
计算所有值的总和。
⑥
沿第一个轴计算总和,即按列计算。
⑦
沿第二轴计算总和,即按行计算。
有多种方法可以初始化(实例化)ndarray
对象。一种方法如前所述,通过np.array
。然而,这假定数组的所有元素已经可用。相比之下,也许我们希望首先实例化ndarray
对象,以便在执行代码期间生成的结果后来填充它们。为此,我们可以使用以下函数:
In [60]: c = np.zeros((2, 3), dtype='i', order='C') # ①
c
Out[60]: array([[0, 0, 0],
[0, 0, 0]], dtype=int32)
In [61]: c = np.ones((2, 3, 4), dtype='i', order='C') # ②
c
Out[61]: array([[[1, 1, 1, 1],
[1, 1, 1, 1],
[1, 1, 1, 1]],
[[1, 1, 1, 1],
[1, 1, 1, 1],
[1, 1, 1, 1]]], dtype=int32)
In [62]: d = np.zeros_like(c, dtype='f16', order='C') # ③
d
Out[62]: array([[[ 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.0, 0.0],
[ 0.0, 0.0, 0.0, 0.0],
[ 0.0, 0.0, 0.0, 0.0]]], dtype=float128)
In [63]: d = np.ones_like(c, dtype='f16', order='C') # ③
d
Out[63]: array([[[ 1.0, 1.0, 1.0, 1.0],
[ 1.0, 1.0, 1.0, 1.0],
[ 1.0, 1.0, 1.0, 1.0]],
[[ 1.0, 1.0, 1.0, 1.0],
[ 1.0, 1.0, 1.0, 1.0],
[ 1.0, 1.0, 1.0, 1.0]]], dtype=float128)
In [64]: e = np.empty((2, 3, 2)) # ④
e
Out[64]: array([[[ 0.00000000e 000, -4.34540174e-311],
[ 2.96439388e-323, 0.00000000e 000],
[ 0.00000000e 000, 1.16095484e-028]],
[[ 2.03147708e-110, 9.67661175e-144],
[ 9.80058441e 252, 1.23971686e 224],
[ 4.00695466e 252, 8.34404939e-309]]])
In [65]: f = np.empty_like(c) # ④
f
Out[65]: array([[[0, 0, 0, 0],
[9, 0, 0, 0],
[0, 0, 0, 0]],
[[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]]], dtype=int32)
In [66]: np.eye(5) # ⑤
Out[66]: array([[ 1., 0., 0., 0., 0.],
[ 0., 1., 0., 0., 0.],
[ 0., 0., 1., 0., 0.],
[ 0., 0., 0., 1., 0.],
[ 0., 0., 0., 0., 1.]])
In [67]: g = np.linspace(5, 15, 15) # ⑥
g
Out[67]: array([ 5. , 5.71428571, 6.42857143, 7.14285714,
7.85714286, 8.57142857, 9.28571429, 10. ,
10.71428571, 11.42857143, 12.14285714, 12.85714286,
13.57142857, 14.28571429, 15. ])
①
用零预先填充的ndarray
对象。
②
用 1 预先填充的ndarray
对象。
③
相同,但采用另一个ndarray
对象来推断形状。
④
ndarray
对象不预先填充任何内容(数字取决于内存中存在的位)。
⑤
创建一个由 1 填充对角线的方阵作为ndarray
对象。
⑥
创建一个一维ndarray
对象,其中数字之间的间隔均匀分布;所使用的参数是start
、end
、num
(元素数量)。
使用所有这些函数,我们可以提供以下参数:
shape
要么是一个int
,一个``int s
序列,或者是对另一个 numpy.ndarray
的引用
dtype
(可选)
一个dtype
——这些是NumPy
特定的numpy.ndarray
对象的数据类型
order
(可选)
存储元素在内存中的顺序:C
表示C
风格(即,逐行),或F
表示Fortran
风格(即,逐列)
在这里,NumPy
如何通过ndarray
类专门构建数组的方式,与基于list
的方法进行比较变得明显:
-
ndarray
对象具有内置的维度(轴)。 -
ndarray
对象是不可变的,其形状是固定的。 - 它仅允许单一数据类型(
numpy.dtype
)用于整个数组。
相反,array
类只共享允许唯一数据类型(类型代码,dtype
)的特性。
order
参数的作用在本章稍后讨论。表 4-1 提供了numpy.dtype
对象的概述(即,NumPy
允许的基本数据类型)。
表 4-1。NumPy dtype 对象
dtype | 描述 | 示例 |
---|---|---|
t | 位域 | t4 (4 位) |
b | 布尔 | b(true 或 false) |
i | 整数 | i8 (64 位) |
u | 无符号整数 | u8 (64 位) |
f | 浮点数 | f8 (64 位) |
c | 复数浮点数 | c16 (128 位) |
O | 对象 | 0 (对象指针) |
S, a | 字符串 | S24 (24 个字符) |
U | Unicode | U24 (24 个 Unicode 字符) |
V | 其他 | V12 (12 字节数据块) |
元信息
每个ndarray
对象都提供访问一些有用属性的功能。
In [68]: g.size # ①
Out[68]: 15
In [69]: g.itemsize # ②
Out[69]: 8
In [70]: g.ndim # ③
Out[70]: 1
In [71]: g.shape # ④
Out[71]: (15,)
In [72]: g.dtype # ⑤
Out[72]: dtype('float64')
In [73]: g.nbytes # ⑥
Out[73]: 120
①
元素的数量。
②
用于表示一个元素所使用的字节数。
③
维度的数量。
④
ndarray
对象的形状。
⑤
元素的dtype
。
⑥
内存中使用的总字节数。
重塑和调整大小
虽然ndarray
对象默认是不可变的,但有多种选项可以重塑和调整此类对象。一般情况下,第一个操作只是提供相同数据的另一个视图,而第二个操作一般会创建一个新的(临时)对象。
In [74]: g = np.arange(15)
In [75]: g
Out[75]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
In [76]: g.shape # ①
Out[76]: (15,)
In [77]: np.shape(g) # ①
Out[77]: (15,)
In [78]: g.reshape((3, 5)) # ②
Out[78]: array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
In [79]: h = g.reshape((5, 3)) # ③
h
Out[79]: array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]])
In [80]: h.T # ④
Out[80]: array([[ 0, 3, 6, 9, 12],
[ 1, 4, 7, 10, 13],
[ 2, 5, 8, 11, 14]])
In [81]: h.transpose() # ④
Out[81]: array([[ 0, 3, 6, 9, 12],
[ 1, 4, 7, 10, 13],
[ 2, 5, 8, 11, 14]])
①
原始ndarray
对象的形状。
②
重塑为两个维度(内存视图)。
③
创建新对象。
④
新ndarray
对象的转置。
在重塑操作期间,ndarray
对象中的元素总数保持不变。在调整大小操作期间,此数字会更改,即它要么减少(“向下调整”),要么增加(“向上调整”)。
In [82]: g
Out[82]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
In [83]: np.resize(g, (3, 1)) # ①
Out[83]: array([[0],
[1],
[2]])
In [84]: np.resize(g, (1, 5)) # ①
Out[84]: array([[0, 1, 2, 3, 4]])
In [85]: np.resize(g, (2, 5)) # ①
Out[85]: array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]])
In [86]: n = np.resize(g, (5, 4)) # ②
n
Out[86]: array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 0],
[ 1, 2, 3, 4]])
①
两个维度,向下调整。
②
两个维度,向上调整。
堆叠是一种特殊操作,允许水平或垂直组合两个ndarray
对象。但是,“连接”维度的大小必须相同。
In [87]: h
Out[87]: array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]])
In [88]: np.hstack((h, 2 * h)) # ①
Out[88]: array([[ 0, 1, 2, 0, 2, 4],
[ 3, 4, 5, 6, 8, 10],
[ 6, 7, 8, 12, 14, 16],
[ 9, 10, 11, 18, 20, 22],
[12, 13, 14, 24, 26, 28]])
In [89]: np.vstack((h, 0.5 * h)) # ②
Out[89]: array([[ 0. , 1. , 2. ],
[ 3. , 4. , 5. ],
[ 6. , 7. , 8. ],
[ 9. , 10. , 11. ],
[ 12. , 13. , 14. ],
[ 0. , 0.5, 1. ],
[ 1.5, 2. , 2.5],
[ 3. , 3.5, 4. ],
[ 4.5, 5. , 5.5],
[ 6. , 6.5, 7. ]])
①
水平堆叠两个ndarray
对象。
②
垂直堆叠两个ndarray
对象。
另一个特殊操作是将多维ndarray
对象展平为一维对象。可以选择是按行(C
顺序)还是按列(F
顺序)进行展平。
In [90]: h
Out[90]: array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]])
In [91]: h.flatten() # ①
Out[91]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
In [92]: h.flatten(order='C') # ①
Out[92]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
In [93]: h.flatten(order='F') # ②
Out[93]: array([ 0, 3, 6, 9, 12, 1, 4, 7, 10, 13, 2, 5, 8, 11, 14])
In [94]: for i in h.flat: # ③
print(i, end=',')
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,
In [95]: for i in h.ravel(order='C'): # ④
print(i, end=',')
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,
In [96]: for i in h.ravel(order='F'): # ④
print(i, end=',')
0,3,6,9,12,1,4,7,10,13,2,5,8,11,14,
①
平铺的默认顺序是C
。
②
用F
顺序展平。
③
flat
属性提供了一个平坦的迭代器(C
顺序)。
④
ravel()
方法是flatten()
的另一种选择。
布尔数组
比较和逻辑操作通常在ndarray
对象上像在标准 Python 数据类型上一样逐元素地进行。默认情况下,评估条件会产生一个布尔ndarray
对象(dtype
为bool
)。
In [164]: h
Out[164]: array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]])
In [150]: h > 8 # ①
Out[150]: array([[False, False, False],
[False, False, False],
[False, False, False],
[ True, True, True],
[ True, True, True]], dtype=bool)
In [151]: h <= 7 # ②
Out[151]: array([[ True, True, True],
[ True, True, True],
[ True, True, False],
[False, False, False],
[False, False, False]], dtype=bool)
In [152]: h == 5 # ③
Out[152]: array([[False, False, False],
[False, False, True],
[False, False, False],
[False, False, False],
[False, False, False]], dtype=bool)
In [158]: (h == 5).astype(int) # ④
Out[158]: array([[0, 0, 0],
[0, 0, 1],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]])
In [165]: (h > 4) & (h <= 12) # ⑤
Out[165]: array([[False, False, False],
[False, False, True],
[ True, True, True],
[ True, True, True],
[ True, False, False]], dtype=bool)
①
值是否大于…?
②
值是否小于或等于…?
③
值是否等于…?
④
以整数值 0 和 1 表示True
和False
。
⑤
值是否大于…且小于或等于…?
此类布尔数组可用于索引和数据选择。注意以下操作会展平数据。
代码语言:javascript复制In [153]: h[h > 8] # ①
Out[153]: array([ 9, 10, 11, 12, 13, 14])
In [155]: h[(h > 4) & (h <= 12)] # ②
Out[155]: array([ 5, 6, 7, 8, 9, 10, 11, 12])
In [157]: h[(h < 4) | (h >= 12)] # ③
Out[157]: array([ 0, 1, 2, 3, 12, 13, 14])
①
给我所有大于…的值。
②
给我所有大于… 且小于或等于…的值。
③
给我所有大于… 或小于或等于…的值。
在这方面的一个强大工具是np.where()
函数,它允许根据条件是True
还是False
来定义操作/操作。应用np.where()
的结果是一个与原始对象相同形状的新ndarray
对象。
In [159]: np.where(h > 7, 1, 0) # ①
Out[159]: array([[0, 0, 0],
[0, 0, 0],
[0, 0, 1],
[1, 1, 1],
[1, 1, 1]])
In [160]: np.where(h % 2 == 0, 'even', 'odd') # ②
Out[160]: array([['even', 'odd', 'even'],
['odd', 'even', 'odd'],
['even', 'odd', 'even'],
['odd', 'even', 'odd'],
['even', 'odd', 'even']],
dtype='<U4')
In [163]: np.where(h <= 7, h * 2, h / 2) # ③
Out[163]: array([[ 0. , 2. , 4. ],
[ 6. , 8. , 10. ],
[ 12. , 14. , 4. ],
[ 4.5, 5. , 5.5],
[ 6. , 6.5, 7. ]])
①
在新对象中,如果为True
,则设置为1
,否则设置为0
。
②
在新对象中,如果为True
,则设置为even
,否则设置为odd
。
③
在新对象中,如果为True
,则将h
元素设置为两倍,否则将h
元素设置为一半。
后续章节提供了关于ndarray
对象上这些重要操作的更多示例。
速度比较
在转向具有NumPy
的结构化数组之前,让我们暂时保持常规数组,并看看专业化在性能方面带来了什么。
以一个简单的例子为例,假设我们想要生成一个形状为 5,000 × 5,000 元素的矩阵/数组,填充了(伪)随机的标准正态分布的数字。然后我们想要计算所有元素的总和。首先,纯Python
方法,我们使用list
推导来实现:
In [97]: import random
I = 5000
In [98]: %time mat = [[random.gauss(0, 1) for j in range(I)]
for i in range(I)] # ①
CPU times: user 20.9 s, sys: 372 ms, total: 21.3 s
Wall time: 21.3 s
In [99]: mat[0][:5] # ②
Out[99]: [0.02023704728430644,
-0.5773300286314157,
-0.5034574089604074,
-0.07769332062744054,
-0.4264012594572326]
In [100]: %time sum([sum(l) for l in mat]) # ③
CPU times: user 156 ms, sys: 1.93 ms, total: 158 ms
Wall time: 158 ms
Out[100]: 681.9120404070142
In [101]: import sys
sum([sys.getsizeof(l) for l in mat]) # ④
Out[101]: 215200000
①
通过嵌套的列表推导来创建矩阵。
②
从所绘制的数字中选择一些随机数。
③
首先在列表推导中计算单个list
对象的总和;然后计算总和的总和。
④
添加所有list
对象的内存使用量。
现在让我们转向NumPy
,看看同样的问题是如何在那里解决的。为了方便,NumPy
子库random
提供了许多函数来实例化一个ndarray
对象,并同时填充它(伪)随机数:
In [102]: %time mat = np.random.standard_normal((I, I)) # ①
CPU times: user 1.14 s, sys: 170 ms, total: 1.31 s
Wall time: 1.32 s
In [103]: %time mat.sum() # ②
CPU times: user 29.5 ms, sys: 1.32 ms, total: 30.8 ms
Wall time: 29.7 ms
Out[103]: 2643.0006104377485
In [104]: mat.nbytes # ③
Out[104]: 200000000
In [105]: sys.getsizeof(mat) # ③
Out[105]: 200000112
①
使用标准正态分布的随机数字创建ndarray
对象;速度约快 20 倍。
②
计算ndarray
对象中所有值的总和;速度约快 6 倍。
③
NumPy
方法也节省了一些内存,因为ndarray
对象的内存开销与数据本身的大小相比微不足道。
我们观察到以下情况:
语法
尽管我们使用了几种方法来压缩纯Python
代码,但NumPy
版本更加紧凑和易读。
性能
生成ndarray
对象的速度大约快了 20 倍,求和的计算速度大约快了 6 倍,比纯Python
中的相应操作更快。
使用 NumPy 数组
使用NumPy
进行基于数组的操作和算法通常会导致代码紧凑、易读,并且与纯Python
代码相比具有显著的性能改进。
结构化 NumPy 数组
ndarray
类的专业化显然带来了许多有价值的好处。然而,太窄的专业化可能对大多数基于数组的算法和应用程序来说是一个太大的负担。因此,NumPy
提供了允许每列具有不同dtype
的结构化或记录ndarray
对象。什么是“每列”?考虑以下结构化数组对象的初始化:
In [106]: dt = np.dtype([('Name', 'S10'), ('Age', 'i4'),
('Height', 'f'), ('Children/Pets', 'i4', 2)]) # ①
In [107]: dt # ①
Out[107]: dtype([('Name', 'S10'), ('Age', '<i4'), ('Height', '<f4'), ('Children/Pets', '<i4', (2,))])
In [108]: dt = np.dtype({'names': ['Name', 'Age', 'Height', 'Children/Pets'],
'formats':'O int float int,int'.split()}) # ②
In [109]: dt # ②
Out[109]: dtype([('Name', 'O'), ('Age', '<i8'), ('Height', '<f8'), ('Children/Pets', [('f0', '<i8'), ('f1', '<i8')])])
In [110]: s = np.array([('Smith', 45, 1.83, (0, 1)),
('Jones', 53, 1.72, (2, 2))], dtype=dt) # ③
In [111]: s # ③
Out[111]: array([('Smith', 45, 1.83, (0, 1)), ('Jones', 53, 1.72, (2, 2))],
dtype=[('Name', 'O'), ('Age', '<i8'), ('Height', '<f8'), ('Children/Pets', [('f0', '<i8'), ('f1', '<i8')])])
In [112]: type(s) # ④
Out[112]: numpy.ndarray
①
复杂的dtype
是由几部分组成的。
②
实现相同结果的替代语法。
③
结构化ndarray
以两条记录实例化。
④
对象类型仍然是numpy.ndarray
。
从某种意义上说,这个构造与初始化SQL
数据库中的表格的操作非常接近。我们有列名和列数据类型,可能还有一些附加信息(例如,每个string
对象的最大字符数)。现在可以通过它们的名称轻松访问单个列,并通过它们的索引值访问行:
In [113]: s['Name'] # ①
Out[113]: array(['Smith', 'Jones'], dtype=object)
In [114]: s['Height'].mean() # ②
Out[114]: 1.7749999999999999
In [115]: s[0] # ③
Out[115]: ('Smith', 45, 1.83, (0, 1))
In [116]: s[1]['Age'] # ④
Out[116]: 53
①
通过名称选择一列。
②
在选定的列上调用方法。
③
选择一条记录。
④
选择记录中的一个字段。
总之,结构化数组是常规numpy.ndarray
对象类型的泛化,因为数据类型只需在每列上保持相同,就像在SQL
数据库表格上的上下文中一样。结构化数组的一个优点是,列的单个元素可以是另一个多维对象,不必符合基本的NumPy
数据类型。
结构化数组
NumPy
提供了除了常规数组之外,还提供了结构化(记录)数组,允许描述和处理类似表格的数据结构,每个(命名的)列具有各种不同的数据类型。它们将SQL
表格类似的数据结构带到了Python
中,大部分具备常规ndarray
对象的优点(语法、方法、性能)。
代码的向量化
代码的矢量化是一种获得更紧凑代码并可能更快执行的策略。其基本思想是对复杂对象进行“一次性”操作或应用函数,而不是通过循环遍历对象的单个元素。在Python
中,函数式编程工具,如map
和filter
,提供了一些基本的矢量化手段。然而,NumPy
在其核心深处内置了矢量化。
基本矢量化
正如我们在上一节中学到的,简单的数学运算,如计算所有元素的总和,可以直接在ndarray
对象上实现(通过方法或通用函数)。还可以进行更一般的矢量化操作。例如,我们可以按元素将两个NumPy
数组相加如下:
In [117]: np.random.seed(100)
r = np.arange(12).reshape((4, 3)) # ①
s = np.arange(12).reshape((4, 3)) * 0.5 # ②
In [118]: r # ①
Out[118]: array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11]])
In [119]: s # ②
Out[119]: array([[ 0. , 0.5, 1. ],
[ 1.5, 2. , 2.5],
[ 3. , 3.5, 4. ],
[ 4.5, 5. , 5.5]])
In [120]: r s # ③
Out[120]: array([[ 0. , 1.5, 3. ],
[ 4.5, 6. , 7.5],
[ 9. , 10.5, 12. ],
[ 13.5, 15. , 16.5]])
①
具有随机数的第一个ndarray
对象。
②
具有随机数的第二个ndarray
对象。
③
逐元素加法作为矢量化操作(无循环)。
NumPy
还支持所谓的广播。这允许在单个操作中组合不同形状的对象。我们之前已经使用过这个功能。考虑以下示例:
In [121]: r 3 # ①
Out[121]: array([[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]])
In [122]: 2 * r # ②
Out[122]: array([[ 0, 2, 4],
[ 6, 8, 10],
[12, 14, 16],
[18, 20, 22]])
In [123]: 2 * r 3 # ③
Out[123]: array([[ 3, 5, 7],
[ 9, 11, 13],
[15, 17, 19],
[21, 23, 25]])
①
在标量加法期间,标量被广播并添加到每个元素。
②
在标量乘法期间,标量也广播并与每个元素相乘。
③
此线性变换结合了两个操作。
这些操作也适用于不同形状的ndarray
对象,直到某个特定点为止:
In [124]: r
Out[124]: array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11]])
In [125]: r.shape
Out[125]: (4, 3)
In [126]: s = np.arange(0, 12, 4) # ①
s # ①
Out[126]: array([0, 4, 8])
In [127]: r s # ②
Out[127]: array([[ 0, 5, 10],
[ 3, 8, 13],
[ 6, 11, 16],
[ 9, 14, 19]])
In [128]: s = np.arange(0, 12, 3) # ③
s # ③
Out[128]: array([0, 3, 6, 9])
In [129]: # r s # ④
In [130]: r.transpose() s # ⑤
Out[130]: array([[ 0, 6, 12, 18],
[ 1, 7, 13, 19],
[ 2, 8, 14, 20]])
In [131]: sr = s.reshape(-1, 1) # ⑥
sr
Out[131]: array([[0],
[3],
[6],
[9]])
In [132]: sr.shape # ⑥
Out[132]: (4, 1)
In [133]: r s.reshape(-1, 1) # ⑥
Out[133]: array([[ 0, 1, 2],
[ 6, 7, 8],
[12, 13, 14],
[18, 19, 20]])
①
长度为 3 的新一维ndarray
对象。
②
r
(矩阵)和s
(向量)对象可以直接相加。
③
另一个长度为 4 的一维ndarray
对象。
④
新s
(向量)对象的长度现在与r
对象的第二维长度不同。
⑤
再次转置r
对象允许进行矢量化加法。
⑥
或者,s
的形状可以更改为(4, 1)
以使加法起作用(但结果不同)。
通常情况下,自定义的Python
函数也适用于numpy.ndarray
。如果实现允许,数组可以像int
或float
对象一样与函数一起使用。考虑以下函数:
In [134]: def f(x):
return 3 * x 5 # ①
In [135]: f(0.5) # ②
Out[135]: 6.5
In [136]: f(r) # ③
Out[136]: array([[ 5, 8, 11],
[14, 17, 20],
[23, 26, 29],
[32, 35, 38]])
①
实现对参数x
进行线性变换的简单 Python 函数。
②
函数f
应用于 Python 的float
对象。
③
同一函数应用于ndarray
对象,导致函数的向量化和逐个元素的评估。
NumPy
所做的是简单地将函数f
逐个元素地应用于对象。在这种意义上,通过使用这种操作,我们并不避免循环;我们只是在Python
级别上避免了它们,并将循环委托给了NumPy
。在NumPy
级别上,对ndarray
对象进行循环处理是由高度优化的代码来完成的,其中大部分代码都是用C
编写的,因此通常比纯Python
快得多。这解释了在基于数组的用例中使用NumPy
带来性能优势的“秘密”。
内存布局
当我们首次使用np.zero
初始化numpy.ndarray
对象时,我们提供了一个可选参数用于内存布局。这个参数大致指定了数组的哪些元素会被连续地存储在内存中。当处理小数组时,这几乎不会对数组操作的性能产生任何可测量的影响。然而,当数组变大并且取决于要在其上实现的(财务)算法时,情况可能会有所不同。这就是内存布局发挥作用的时候(参见,例如多维数组的内存布局)。
要说明数组的内存布局在科学和金融中的潜在重要性,考虑以下构建多维ndarray
对象的情况:
In [137]: x = np.random.standard_normal((1000000, 5)) # ①
In [138]: y = 2 * x 3 # ②
In [139]: C = np.array((x, y), order='C') # ③
In [140]: F = np.array((x, y), order='F') # ④
In [141]: x = 0.0; y = 0.0 # ⑤
In [142]: C[:2].round(2) # ⑥
Out[142]: array([[[-1.75, 0.34, 1.15, -0.25, 0.98],
[ 0.51, 0.22, -1.07, -0.19, 0.26],
[-0.46, 0.44, -0.58, 0.82, 0.67],
...,
[-0.05, 0.14, 0.17, 0.33, 1.39],
[ 1.02, 0.3 , -1.23, -0.68, -0.87],
[ 0.83, -0.73, 1.03, 0.34, -0.46]],
[[-0.5 , 3.69, 5.31, 2.5 , 4.96],
[ 4.03, 3.44, 0.86, 2.62, 3.51],
[ 2.08, 3.87, 1.83, 4.63, 4.35],
...,
[ 2.9 , 3.28, 3.33, 3.67, 5.78],
[ 5.04, 3.6 , 0.54, 1.65, 1.26],
[ 4.67, 1.54, 5.06, 3.69, 2.07]]])
①
一个在两个维度上具有较大不对称性的ndarray
对象。
②
对原始对象数据进行线性变换。
③
这将创建一个二维ndarray
对象,其顺序为C
(行优先)。
④
这将创建一个二维ndarray
对象,其顺序为F
(列优先)。
⑤
内存被释放(取决于垃圾收集)。
⑥
从C
对象中获取一些数字。
让我们看一些关于两种类型的ndarray
对象的基本示例和用例,并考虑它们在不同内存布局下执行的速度:
In [143]: %timeit C.sum() # ①
4.65 ms ± 73.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [144]: %timeit F.sum() # ①
4.56 ms ± 105 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [145]: %timeit C.sum(axis=0) # ②
20.9 ms ± 358 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [146]: %timeit C.sum(axis=1) # ③
38.5 ms ± 1.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [147]: %timeit F.sum(axis=0) # ②
87.5 ms ± 1.37 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [148]: %timeit F.sum(axis=1) # ③
81.6 ms ± 1.66 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [149]: F = 0.0; C = 0.0
①
计算所有元素的总和。
②
每行计算和(“许多”)。
③
计算每列的总和(“少”)。
我们可以总结性能结果如下:
- 当计算所有元素的总和时,内存布局实际上并不重要。
- 对
C
-orderedndarray
对象的求和在行和列上都更快(绝对速度优势)。 - 使用
C
-ordered(行优先)ndarray
对象,对行求和相对比对列求和更快。 - 使用
F
-ordered(列优先)ndarray
对象,对列求和相对比对行求和更快。
结论
NumPy
是 Python 中数值计算的首选包。ndarray
类是专门设计用于处理(大)数值数据的高效方便的类。强大的方法和 NumPy
的通用函数允许进行向量化的代码,大部分避免了在 Python 层上的慢循环。本章介绍的许多方法也适用于 pandas
及其 DataFrame
类(见 第五章)
更多资源
有用的资源提供在:
- http://www.numpy.org/
优秀的 NumPy
介绍书籍包括:
- McKinney, Wes(2017):Python 数据分析。第 2 版,O’Reilly,北京等。
- VanderPlas, Jake(2016):Python 数据科学手册。O’Reilly,北京等。
第五章:数据分析与 pandas
数据!数据!数据!没有数据,我无法制造砖头! 夏洛克·福尔摩斯
简介
本章讨论的是pandas
,这是一个专注于表格数据的数据分析库。pandas
在最近几年已经成为一个强大的工具,不仅提供了强大的类和功能,还很好地封装了来自其他软件包的现有功能。结果是一个用户界面,使得数据分析,特别是金融分析,成为一项便捷和高效的任务。
在pandas
的核心和本章中的是DataFrame
,一个有效处理表格形式数据的类,即以列为组织的数据。为此,DataFrame
类提供了列标签以及对数据集的行(记录)进行灵活索引的能力,类似于关系数据库中的表或 Excel 电子表格。
本章涵盖了以下基本数据结构:
对象类型 | 意义 | 用途/模型为 |
---|---|---|
DataFrame | 带有索引的二维数据对象 | 表格数据以列组织 |
Series | 带有索引的一维数据对象 | 单一(时间)数据系列 |
本章组织如下:
“DataFrame 类”
本章从使用简单且小的数据集探索pandas
的DataFrame
类的基本特征和能力开始;然后通过使用NumPy
的ndarray
对象并将其转换为DataFrame
对象来进行处理。
“基本分析” 和 “基本可视化”
本章还展示了基本的分析和可视化能力,尽管后面的章节在这方面更深入。
“Series 类”
本节简要介绍了pandas
的Series
类,它在某种程度上代表了DataFrame
类的一个特殊情况,只包含单列数据。
“GroupBy 操作”
DataFrame
类的一大优势在于根据单个或多个列对数据进行分组。
“复杂选择”
使用(复杂)条件允许从DataFrame
对象中轻松选择数据。
“串联、连接和合并”
将不同数据集合并为一个是数据分析中的重要操作。pandas
提供了多种选项来完成这样的任务。
“性能方面”
与 Python 一般一样,pandas
在一般情况下提供了多种选项来完成相同的目标。本节简要讨论潜在的性能差异。
DataFrame 类
本节涵盖了DataFrame
类的一些基本方面。这个类非常复杂和强大,这里只能展示其中一小部分功能。后续章节提供更多例子并揭示不同的方面。
使用 DataFrame 类的第一步
从相当基本的角度来看,DataFrame
类被设计用来管理带索引和标签的数据,与SQL
数据库表或电子表格应用程序中的工作表并没有太大的不同。考虑以下创建DataFrame
对象的示例:
In [1]: import pandas as pd # ①
In [2]: df = pd.DataFrame([10, 20, 30, 40], # ②
columns=['numbers'], # ③
index=['a', 'b', 'c', 'd']) # ④
In [3]: df # ⑤
Out[3]: numbers
a 10
b 20
c 30
d 40
①
导入pandas
。
②
将数据定义为list
对象。
③
指定列标签。
④
指定索引值/标签。
⑤
显示DataFrame
对象的数据以及列和索引标签。
这个简单的例子已经展示了当涉及到存储数据时DataFrame
类的一些主要特性:
数据
数据本身可以以不同的形状和类型提供(list
、tuple
、ndarray
和dict
对象都是候选对象)。
标签
数据以列的形式组织,可以具有自定义名称。
索引
存在可以采用不同格式(例如,数字、字符串、时间信息)的索引。
与此类DataFrame
对象一起工作通常非常方便和高效,例如,与常规的ndarray
对象相比,当您想要像扩大现有对象一样时,后者更为专业和受限。以下是展示在DataFrame
对象上进行典型操作的简单示例:
In [4]: df.index # ①
Out[4]: Index(['a', 'b', 'c', 'd'], dtype='object')
In [5]: df.columns # ②
Out[5]: Index(['numbers'], dtype='object')
In [6]: df.loc['c'] # ③
Out[6]: numbers 30
Name: c, dtype: int64
In [7]: df.loc[['a', 'd']] # ④
Out[7]: numbers
a 10
d 40
In [8]: df.iloc[1:3] # ⑤
Out[8]: numbers
b 20
c 30
In [9]: df.sum() # ⑥
Out[9]: numbers 100
dtype: int64
In [10]: df.apply(lambda x: x ** 2) # ⑦
Out[10]: numbers
a 100
b 400
c 900
d 1600
In [11]: df ** 2 # ⑧
Out[11]: numbers
a 100
b 400
c 900
d 1600
①
index
属性和Index
对象。
②
columns
属性和Index
对象。
③
选择与索引c
对应的值。
④
选择与索引a
和d
对应的两个值。
⑤
通过索引位置选择第二行和第三行。
⑥
计算单列的总和。
⑦
使用apply()
方法以向量化方式计算平方。
⑧
直接应用向量化,就像使用ndarray
对象一样。
与NumPy
的ndarray
对象相反,可以在两个维度上扩大DataFrame
对象:
In [12]: df['floats'] = (1.5, 2.5, 3.5, 4.5) # ①
In [13]: df
Out[13]: numbers floats
a 10 1.5
b 20 2.5
c 30 3.5
d 40 4.5
In [14]: df['floats'] # ②
Out[14]: a 1.5
b 2.5
c 3.5
d 4.5
Name: floats, dtype: float64
①
使用提供的float
对象作为tuple
对象添加新列。
②
选择此列并显示其数据和索引标签。
整个DataFrame
对象也可以用来定义新列。在这种情况下,索引会自动对齐:
In [15]: df['names'] = pd.DataFrame(['Yves', 'Sandra', 'Lilli', 'Henry'],
index=['d', 'a', 'b', 'c']) # ①
In [16]: df
Out[16]: numbers floats names
a 10 1.5 Sandra
b 20 2.5 Lilli
c 30 3.5 Henry
d 40 4.5 Yves
①
基于DataFrame
对象创建另一个新列。
数据附加工作方式类似。但是,在以下示例中,我们看到通常应避免的副作用——索引被简单的范围索引替换:
代码语言:javascript复制In [17]: df.append({'numbers': 100, 'floats': 5.75, 'names': 'Jil'},
ignore_index=True) # ①
Out[17]: numbers floats names
0 10 1.50 Sandra
1 20 2.50 Lilli
2 30 3.50 Henry
3 40 4.50 Yves
4 100 5.75 Jil
In [18]: df = df.append(pd.DataFrame({'numbers': 100, 'floats': 5.75,
'names': 'Jil'}, index=['y',])) # ②
In [19]: df
Out[19]: floats names numbers
a 1.50 Sandra 10
b 2.50 Lilli 20
c 3.50 Henry 30
d 4.50 Yves 40
y 5.75 Jil 100
In [20]: df = df.append(pd.DataFrame({'names': 'Liz'}, index=['z',])) # ③
In [21]: df
Out[21]: floats names numbers
a 1.50 Sandra 10.0
b 2.50 Lilli 20.0
c 3.50 Henry 30.0
d 4.50 Yves 40.0
y 5.75 Jil 100.0
z NaN Liz NaN
In [22]: df.dtypes # ④
Out[22]: floats float64
names object
numbers float64
dtype: object
①
通过dict
对象添加新行;这是一个临时操作,在此期间索引信息会丢失。
②
这基于具有索引信息的DataFrame
对象附加行;原始索引信息被保留。
③
这将不完整的数据行附加到DataFrame
对象中,导致NaN
值。
④
单列的不同dtypes
;这类似于带有NumPy
的记录数组。
尽管现在存在缺失值,但大多数方法调用仍将起作用。例如:
代码语言:javascript复制In [23]: df[['numbers', 'floats']].mean() # ①
Out[23]: numbers 40.00
floats 3.55
dtype: float64
In [24]: df[['numbers', 'floats']].std() # ②
Out[24]: numbers 35.355339
floats 1.662077
dtype: float64
①
对指定的两列求平均值(忽略具有NaN
值的行)。
②
对指定的两列计算标准差(忽略具有NaN
值的行)。
DataFrame 类的第二步
本小节中的示例基于具有标准正态分布随机数的ndarray
对象。它探索了进一步的功能,如使用DatetimeIndex
来管理时间序列数据。
In [25]: import numpy as np
In [26]: np.random.seed(100)
In [27]: a = np.random.standard_normal((9, 4))
In [28]: a
Out[28]: array([[-1.74976547, 0.3426804 , 1.1530358 , -0.25243604],
[ 0.98132079, 0.51421884, 0.22117967, -1.07004333],
[-0.18949583, 0.25500144, -0.45802699, 0.43516349],
[-0.58359505, 0.81684707, 0.67272081, -0.10441114],
[-0.53128038, 1.02973269, -0.43813562, -1.11831825],
[ 1.61898166, 1.54160517, -0.25187914, -0.84243574],
[ 0.18451869, 0.9370822 , 0.73100034, 1.36155613],
[-0.32623806, 0.05567601, 0.22239961, -1.443217 ],
[-0.75635231, 0.81645401, 0.75044476, -0.45594693]])
尽管可以更直接地构造DataFrame
对象(如前所示),但通常使用ndarray
对象是一个很好的选择,因为pandas
将保留基本结构,并且“只”会添加元信息(例如,索引值)。它还代表了金融应用和一般科学研究的典型用例。例如:
In [29]: df = pd.DataFrame(a) # ①
In [30]: df
Out[30]: 0 1 2 3
0 -1.749765 0.342680 1.153036 -0.252436
1 0.981321 0.514219 0.221180 -1.070043
2 -0.189496 0.255001 -0.458027 0.435163
3 -0.583595 0.816847 0.672721 -0.104411
4 -0.531280 1.029733 -0.438136 -1.118318
5 1.618982 1.541605 -0.251879 -0.842436
6 0.184519 0.937082 0.731000 1.361556
7 -0.326238 0.055676 0.222400 -1.443217
8 -0.756352 0.816454 0.750445 -0.455947
①
从ndarray
对象创建DataFrame
对象。
表 5-1 列出了DataFrame
函数接受的参数。在表中,“array-like”意味着类似于ndarray
对象的数据结构,例如list
。Index
是pandas
Index
类的一个实例。
表 5-1. DataFrame 函数的参数
参数 | 格式 | 描述 |
---|---|---|
data | ndarray/dict/DataFrame | DataFrame的数据;dict可以包含Series,ndarray,list等 |
index | Index/array-like | 要使用的索引;默认为range(n) |
columns | Index/array-like | 要使用的列标题;默认为range(n) |
dtype | dtype,默认为None | 要使用/强制的数据类型;否则,它会被推断 |
copy | bool,默认为None | 从输入复制数据 |
与结构化数组一样,正如我们已经看到的那样,DataFrame
对象具有可以直接通过分配具有正确数量元素的list
来定义的列名。这说明您可以在需要时定义/更改DataFrame
对象的属性:
In [31]: df.columns = ['No1', 'No2', 'No3', 'No4'] # ①
In [32]: df
Out[32]: No1 No2 No3 No4
0 -1.749765 0.342680 1.153036 -0.252436
1 0.981321 0.514219 0.221180 -1.070043
2 -0.189496 0.255001 -0.458027 0.435163
3 -0.583595 0.816847 0.672721 -0.104411
4 -0.531280 1.029733 -0.438136 -1.118318
5 1.618982 1.541605 -0.251879 -0.842436
6 0.184519 0.937082 0.731000 1.361556
7 -0.326238 0.055676 0.222400 -1.443217
8 -0.756352 0.816454 0.750445 -0.455947
In [33]: df['No2'].mean() # ②
Out[33]: 0.70103309414564585
①
通过list
对象指定列标签。
②
现在选择列变得很容易。
要高效处理金融时间序列数据,必须能够处理时间索引。这也可以被视为pandas
的一项重要优势。例如,假设我们的四个列中的九个数据条目对应于从 2019 年 1 月开始的每月末数据。然后,可以使用date_range()
函数生成DatetimeIndex
对象,如下所示:
In [34]: dates = pd.date_range('2019-1-1', periods=9, freq='M') # ①
In [35]: dates
Out[35]: DatetimeIndex(['2019-01-31', '2019-02-28', '2019-03-31', '2019-04-30',
'2019-05-31', '2019-06-30', '2019-07-31', '2019-08-31',
'2019-09-30'],
dtype='datetime64[ns]', freq='M')
①
创建一个DatetimeIndex
对象。
表 5-2 列出了date_range
函数的参数。
表 5-2。date_range
函数的参数
参数 | 格式 | 描述 |
---|---|---|
start | string/datetime | 生成日期的左边界 |
end | string/datetime | 生成日期的右边界 |
periods | integer/None | 期数(如果start或end为None) |
freq | string/DateOffset | 频率字符串,例如,5D代表 5 天 |
tz | string/None | 本地化索引的时区名称 |
normalize | bool,默认为None | 规范化start和end为午夜 |
name | string,默认为None | 结果索引的名称 |
以下代码将刚刚创建的DatetimeIndex
对象定义为相关的索引对象,从而使原始数据集生成时间序列:
In [36]: df.index = dates
In [37]: df
Out[37]: No1 No2 No3 No4
2019-01-31 -1.749765 0.342680 1.153036 -0.252436
2019-02-28 0.981321 0.514219 0.221180 -1.070043
2019-03-31 -0.189496 0.255001 -0.458027 0.435163
2019-04-30 -0.583595 0.816847 0.672721 -0.104411
2019-05-31 -0.531280 1.029733 -0.438136 -1.118318
2019-06-30 1.618982 1.541605 -0.251879 -0.842436
2019-07-31 0.184519 0.937082 0.731000 1.361556
2019-08-31 -0.326238 0.055676 0.222400 -1.443217
2019-09-30 -0.756352 0.816454 0.750445 -0.455947
在使用date_range
函数生成DatetimeIndex
对象时,频率参数freq
有多种选择。表 5-3 列出了所有选项。
表 5-3。date_range
函数的频率参数值
别名 | 描述 |
---|---|
B | 工作日频率 |
C | 自定义工作日频率(实验性的) |
D | 日历日频率 |
W | 周频率 |
M | 月度末频率 |
BM | 工作月末频率 |
MS | 月初频率 |
BMS | 工作月初频率 |
Q | 季度末频率 |
BQ | 工作季度末频率 |
QS | 季度初频率 |
BQS | 工作季度初频率 |
A | 年度末频率 |
BA | 工作年度末频率 |
AS | 年度初频率 |
BAS | 工作年度初频率 |
H | 每小时频率 |
T | 分钟频率 |
S | 每秒频率 |
L | 毫秒 |
U | 微秒 |
在某些情况下,以ndarray
对象的形式访问原始数据集是值得的。例如,values
属性直接提供了对它的访问。
In [38]: df.values
Out[38]: array([[-1.74976547, 0.3426804 , 1.1530358 , -0.25243604],
[ 0.98132079, 0.51421884, 0.22117967, -1.07004333],
[-0.18949583, 0.25500144, -0.45802699, 0.43516349],
[-0.58359505, 0.81684707, 0.67272081, -0.10441114],
[-0.53128038, 1.02973269, -0.43813562, -1.11831825],
[ 1.61898166, 1.54160517, -0.25187914, -0.84243574],
[ 0.18451869, 0.9370822 , 0.73100034, 1.36155613],
[-0.32623806, 0.05567601, 0.22239961, -1.443217 ],
[-0.75635231, 0.81645401, 0.75044476, -0.45594693]])
In [39]: np.array(df)
Out[39]: array([[-1.74976547, 0.3426804 , 1.1530358 , -0.25243604],
[ 0.98132079, 0.51421884, 0.22117967, -1.07004333],
[-0.18949583, 0.25500144, -0.45802699, 0.43516349],
[-0.58359505, 0.81684707, 0.67272081, -0.10441114],
[-0.53128038, 1.02973269, -0.43813562, -1.11831825],
[ 1.61898166, 1.54160517, -0.25187914, -0.84243574],
[ 0.18451869, 0.9370822 , 0.73100034, 1.36155613],
[-0.32623806, 0.05567601, 0.22239961, -1.443217 ],
[-0.75635231, 0.81645401, 0.75044476, -0.45594693]])
数组和数据框
通常情况下,您可以从ndarray
对象中生成DataFrame
对象。但是,您也可以通过使用DataFrame
类的values
属性或NumPy
的np.array()
函数轻松地从DataFrame
生成ndarray
对象。
基本分析
像NumPy
的ndarray
对象一样,pandas
的DataFrame
类内置了许多便利方法。作为入门,考虑info()
方法和 describe()。
In [40]: df.info() # ①
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 9 entries, 2019-01-31 to 2019-09-30
Freq: M
Data columns (total 4 columns):
No1 9 non-null float64
No2 9 non-null float64
No3 9 non-null float64
No4 9 non-null float64
dtypes: float64(4)
memory usage: 360.0 bytes
In [41]: df.describe() # ②
Out[41]: No1 No2 No3 No4
count 9.000000 9.000000 9.000000 9.000000
mean -0.150212 0.701033 0.289193 -0.387788
std 0.988306 0.457685 0.579920 0.877532
min -1.749765 0.055676 -0.458027 -1.443217
25% -0.583595 0.342680 -0.251879 -1.070043
50% -0.326238 0.816454 0.222400 -0.455947
75% 0.184519 0.937082 0.731000 -0.104411
max 1.618982 1.541605 1.153036 1.361556
①
提供有关数据、列和索引的元信息。
②
为每列提供有用的摘要统计信息(针对数值数据)。
此外,您可以轻松地按列或按行获取和累积和,平均值,如下所示:
代码语言:javascript复制In [42]: df.sum() # ①
Out[42]: No1 -1.351906
No2 6.309298
No3 2.602739
No4 -3.490089
dtype: float64
In [43]: df.mean() # ②
Out[43]: No1 -0.150212
No2 0.701033
No3 0.289193
No4 -0.387788
dtype: float64
In [44]: df.mean(axis=0) # ②
Out[44]: No1 -0.150212
No2 0.701033
No3 0.289193
No4 -0.387788
dtype: float64
In [45]: df.mean(axis=1) # ③
Out[45]: 2019-01-31 -0.126621
2019-02-28 0.161669
2019-03-31 0.010661
2019-04-30 0.200390
2019-05-31 -0.264500
2019-06-30 0.516568
2019-07-31 0.803539
2019-08-31 -0.372845
2019-09-30 0.088650
Freq: M, dtype: float64
In [46]: df.cumsum() # ④
Out[46]: No1 No2 No3 No4
2019-01-31 -1.749765 0.342680 1.153036 -0.252436
2019-02-28 -0.768445 0.856899 1.374215 -1.322479
2019-03-31 -0.957941 1.111901 0.916188 -0.887316
2019-04-30 -1.541536 1.928748 1.588909 -0.991727
2019-05-31 -2.072816 2.958480 1.150774 -2.110045
2019-06-30 -0.453834 4.500086 0.898895 -2.952481
2019-07-31 -0.269316 5.437168 1.629895 -1.590925
2019-08-31 -0.595554 5.492844 1.852294 -3.034142
2019-09-30 -1.351906 6.309298 2.602739 -3.490089
①
逐列求和。
②
逐列平均值。
③
逐行平均值。
④
逐列累积和(从第一个索引位置开始)。
DataFrame
对象也按预期理解NumPy
通用函数:
In [47]: np.mean(df) # ①
Out[47]: No1 -0.150212
No2 0.701033
No3 0.289193
No4 -0.387788
dtype: float64
In [48]: np.log(df) # ②
/Users/yves/miniconda3/envs/base/lib/python3.6/site-packages/ipykernel_launcher.py:1: RuntimeWarning: invalid value encountered in log
"""Entry point for launching an IPython kernel.
Out[48]: No1 No2 No3 No4
2019-01-31 NaN -1.070957 0.142398 NaN
2019-02-28 -0.018856 -0.665106 -1.508780 NaN
2019-03-31 NaN -1.366486 NaN -0.832033
2019-04-30 NaN -0.202303 -0.396425 NaN
2019-05-31 NaN 0.029299 NaN NaN
2019-06-30 0.481797 0.432824 NaN NaN
2019-07-31 -1.690005 -0.064984 -0.313341 0.308628
2019-08-31 NaN -2.888206 -1.503279 NaN
2019-09-30 NaN -0.202785 -0.287089 NaN
In [49]: np.sqrt(abs(df)) # ③
Out[49]: No1 No2 No3 No4
2019-01-31 1.322787 0.585389 1.073795 0.502430
2019-02-28 0.990616 0.717091 0.470297 1.034429
2019-03-31 0.435311 0.504977 0.676777 0.659669
2019-04-30 0.763934 0.903796 0.820196 0.323127
2019-05-31 0.728890 1.014757 0.661918 1.057506
2019-06-30 1.272392 1.241614 0.501876 0.917843
2019-07-31 0.429556 0.968030 0.854986 1.166857
2019-08-31 0.571173 0.235958 0.471593 1.201340
2019-09-30 0.869685 0.903578 0.866282 0.675238
In [50]: np.sqrt(abs(df)).sum() # ④
Out[50]: No1 7.384345
No2 7.075190
No3 6.397719
No4 7.538440
dtype: float64
In [51]: 100 * df 100 # ⑤
Out[51]: No1 No2 No3 No4
2019-01-31 -74.976547 134.268040 215.303580 74.756396
2019-02-28 198.132079 151.421884 122.117967 -7.004333
2019-03-31 81.050417 125.500144 54.197301 143.516349
2019-04-30 41.640495 181.684707 167.272081 89.558886
2019-05-31 46.871962 202.973269 56.186438 -11.831825
2019-06-30 261.898166 254.160517 74.812086 15.756426
2019-07-31 118.451869 193.708220 173.100034 236.155613
2019-08-31 67.376194 105.567601 122.239961 -44.321700
2019-09-30 24.364769 181.645401 175.044476 54.405307
①
逐列平均值。
②
逐元素自然对数;会发出警告,但计算会继续进行,导致多个NaN
值。
③
绝对值的逐元素平方根 …
④
… 以及结果的逐列平均值。
⑤
数值数据的线性变换。
NumPy 通用函数
通常情况下,您可以将NumPy
通用函数应用于pandas
的DataFrame
对象,只要它们可以应用于包含相同类型数据的ndarray
对象。
pandas
相当容错,以捕获错误并在相应的数学运算失败时仅放置NaN
值。不仅如此,正如之前简要展示的那样,您还可以在许多情况下像处理完整数据集一样处理这些不完整数据集。这非常方便,因为现实往往被不完整的数据集所表征,这比人们希望的更常见。
基本可视化
通常情况下,一旦数据存储在DataFrame
对象中,数据的绘制就只需一行代码即可(参见图 5-1):
In [52]: from pylab import plt, mpl # ①
plt.style.use('seaborn') # ①
mpl.rcParams['font.family'] = 'serif' # ①
%matplotlib inline
In [53]: df.cumsum().plot(lw=2.0, figsize=(10, 6)); # ②
# plt.savefig('../../images/ch05/pd_plot_01.png')
①
自定义绘图样式。
②
将四列的累积和绘制成折线图。
图 5-1。DataFrame
对象的折线图
基本上,pandas
提供了一个围绕 matplotplib
(参见第七章)的包装器,专门设计用于 DataFrame
对象。表 5-4 列出了 plot
方法接受的参数。
表 5-4。plot 方法的参数
参数 | 格式 | 描述 |
---|---|---|
x | 标签/位置,默认为 None | 仅当列值为 x 刻度时使用 |
y | 标签/位置,默认为 None | 仅当列值为 y 刻度时使用 |
subplots | 布尔值,默认为 False | 在子图中绘制列 |
sharex | 布尔值,默认为 True | x 轴共享 |
sharey | 布尔值,默认为 False | y 轴共享 |
use_index | 布尔值,默认为 True | 使用 DataFrame.index 作为 x 刻度 |
stacked | 布尔值,默认为 False | 堆叠(仅用于柱状图) |
sort_columns | 布尔值,默认为 False | 绘图前按字母顺序排序列 |
title | 字符串,默认为 None | 绘图标题 |
grid | 布尔值,默认为 False | 水平和垂直网格线 |
legend | 布尔值,默认为 True | 标签的图例 |
ax | matplotlib axis 对象 | 用于绘图的 matplotlib axis 对象 |
style | 字符串或列表/字典 | 线绘图风格(对每列) |
kind | “line”/“bar”/“barh”/“kde”/“density” | 绘图类型 |
logx | 布尔值,默认为 False | x 轴的对数缩放 |
logy | 布尔值,默认为 False | y 轴的对数缩放 |
xticks | 序列,默认为 Index | 绘图的 x 刻度 |
yticks | 序列,默认为 Values | 绘图的 y 刻度 |
xlim | 2-元组,列表 | x 轴的边界 |
ylim | 2-元组,列表 | y 轴的边界 |
rot | 整数,默认为 None | x 刻度的旋转 |
secondary_y | 布尔值/序列,默认为 False | 次要 y 轴 |
mark_right | 布尔值,默认为 True | 次要轴的自动标记 |
colormap | 字符串/colormap 对象,默认为 None | 用于绘图的色图 |
kwds | 关键字 | 传递给 matplotlib 的选项 |
作为另一个示例,考虑绘制相同数据的柱状图(参见图 5-1)。
代码语言:javascript复制In [54]: df.plot(kind='bar', figsize=(10, 6)); # ①
# plt.savefig('../../images/ch05/pd_plot_02.png')
①
使用 kind
参数来改变绘图类型。
图 5-2。DataFrame 对象的柱状图
Series 类
到目前为止,我们主要使用 pandas
的 DataFrame
类。Series
类是另一个与 pandas
一起提供的重要类。它的特点是只有一列数据。从这个意义上说,它是 DataFrame
类的一个特化,共享许多但不是所有的特征和功能。通常,当从多列 DataFrame
对象中选择单列时,会得到一个 Series
对象:
In [55]: type(df)
Out[55]: pandas.core.frame.DataFrame
In [56]: s = df['No1']
In [57]: s
Out[57]: 2019-01-31 -1.749765
2019-02-28 0.981321
2019-03-31 -0.189496
2019-04-30 -0.583595
2019-05-31 -0.531280
2019-06-30 1.618982
2019-07-31 0.184519
2019-08-31 -0.326238
2019-09-30 -0.756352
Freq: M, Name: No1, dtype: float64
In [58]: type(s)
Out[58]: pandas.core.series.Series
主要的DataFrame
方法也适用于Series
对象。举例来说,考虑mean()
和plot()
方法(见图 5-3):
In [59]: s.mean()
Out[59]: -0.15021177307319458
In [60]: s.plot(lw=2.0, figsize=(10, 6));
# plt.savefig('../../images/ch05/pd_plot_03.png')
图 5-3。一个 Series 对象的线性图
分组操作
pandas
具有强大且灵活的分组功能。它们与SQL
中的分组以及 MicrosoftExcel
中的数据透视表类似。为了有东西可以分组,我们添加了一列,指示相应数据所属的季度:
In [61]: df['Quarter'] = ['Q1', 'Q1', 'Q1', 'Q2', 'Q2',
'Q2', 'Q3', 'Q3', 'Q3']
df
Out[61]: No1 No2 No3 No4 Quarter
2019-01-31 -1.749765 0.342680 1.153036 -0.252436 Q1
2019-02-28 0.981321 0.514219 0.221180 -1.070043 Q1
2019-03-31 -0.189496 0.255001 -0.458027 0.435163 Q1
2019-04-30 -0.583595 0.816847 0.672721 -0.104411 Q2
2019-05-31 -0.531280 1.029733 -0.438136 -1.118318 Q2
2019-06-30 1.618982 1.541605 -0.251879 -0.842436 Q2
2019-07-31 0.184519 0.937082 0.731000 1.361556 Q3
2019-08-31 -0.326238 0.055676 0.222400 -1.443217 Q3
2019-09-30 -0.756352 0.816454 0.750445 -0.455947 Q3
现在,我们可以按Quarter
列进行分组,并且可以输出单个组的统计信息:
In [62]: groups = df.groupby('Quarter') # ①
In [63]: groups.size() # ②
Out[63]: Quarter
Q1 3
Q2 3
Q3 3
dtype: int64
In [64]: groups.mean() # ③
Out[64]: No1 No2 No3 No4
Quarter
Q1 -0.319314 0.370634 0.305396 -0.295772
Q2 0.168035 1.129395 -0.005765 -0.688388
Q3 -0.299357 0.603071 0.567948 -0.179203
In [65]: groups.max() # ④
Out[65]: No1 No2 No3 No4
Quarter
Q1 0.981321 0.514219 1.153036 0.435163
Q2 1.618982 1.541605 0.672721 -0.104411
Q3 0.184519 0.937082 0.750445 1.361556
In [66]: groups.aggregate([min, max]).round(2) # ⑤
Out[66]: No1 No2 No3 No4
min max min max min max min max
Quarter
Q1 -1.75 0.98 0.26 0.51 -0.46 1.15 -1.07 0.44
Q2 -0.58 1.62 0.82 1.54 -0.44 0.67 -1.12 -0.10
Q3 -0.76 0.18 0.06 0.94 0.22 0.75 -1.44 1.36
①
根据Quarter
列进行分组。
②
给出组中的行数。
③
给出每列的均值。
④
给出每列的最大值。
⑤
给出每列的最小值和最大值。
也可以通过多个列进行分组。为此,引入另一列,指示索引日期的月份是奇数还是偶数:
代码语言:javascript复制In [67]: df['Odd_Even'] = ['Odd', 'Even', 'Odd', 'Even', 'Odd', 'Even',
'Odd', 'Even', 'Odd']
In [68]: groups = df.groupby(['Quarter', 'Odd_Even'])
In [69]: groups.size()
Out[69]: Quarter Odd_Even
Q1 Even 1
Odd 2
Q2 Even 2
Odd 1
Q3 Even 1
Odd 2
dtype: int64
In [70]: groups[['No1', 'No4']].aggregate([sum, np.mean])
Out[70]: No1 No4
sum mean sum mean
Quarter Odd_Even
Q1 Even 0.981321 0.981321 -1.070043 -1.070043
Odd -1.939261 -0.969631 0.182727 0.091364
Q2 Even 1.035387 0.517693 -0.946847 -0.473423
Odd -0.531280 -0.531280 -1.118318 -1.118318
Q3 Even -0.326238 -0.326238 -1.443217 -1.443217
Odd -0.571834 -0.285917 0.905609 0.452805
这就是对pandas
和DataFrame
对象的介绍。后续部分将使用这个工具集来处理真实世界的金融数据。
复杂选择
数据选择通常通过在列值上制定条件来完成,并可能逻辑地组合多个这样的条件。考虑以下数据集。
代码语言:javascript复制In [71]: data = np.random.standard_normal((10, 2)) # ①
In [72]: df = pd.DataFrame(data, columns=['x', 'y']) # ②
In [73]: df.info() # ②
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10 entries, 0 to 9
Data columns (total 2 columns):
x 10 non-null float64
y 10 non-null float64
dtypes: float64(2)
memory usage: 240.0 bytes
In [74]: df.head() # ③
Out[74]: x y
0 1.189622 -1.690617
1 -1.356399 -1.232435
2 -0.544439 -0.668172
3 0.007315 -0.612939
4 1.299748 -1.733096
In [75]: df.tail() # ④
Out[75]: x y
5 -0.983310 0.357508
6 -1.613579 1.470714
7 -1.188018 -0.549746
8 -0.940046 -0.827932
9 0.108863 0.507810
①
具有标准正态分布随机数的ndarray
对象。
②
具有相同随机数的DataFrame
对象。
③
通过head()
方法获得前五行。
④
通过tail()
方法获得最后五行。
下面的代码说明了 Python 的比较运算符和逻辑运算符在两列值上的应用。
代码语言:javascript复制In [76]: df['x'] > 0.5 # ①
Out[76]: 0 True
1 False
2 False
3 False
4 True
5 False
6 False
7 False
8 False
9 False
Name: x, dtype: bool
In [77]: (df['x'] > 0) & (df['y'] < 0) # ②
Out[77]: 0 True
1 False
2 False
3 True
4 True
5 False
6 False
7 False
8 False
9 False
dtype: bool
In [78]: (df['x'] > 0) | (df['y'] < 0) # ③
Out[78]: 0 True
1 True
2 True
3 True
4 True
5 False
6 False
7 True
8 True
9 True
dtype: bool
①
检查x
列中的值是否大于 0.5。
②
检查x
列中的值是否为正且y
列中的值是否为负。
③
检查x
列中的值是否为正或y
列中的值是否为负。
使用结果布尔Series
对象,复杂数据(行)的选择很简单。
In [79]: df[df['x'] > 0] # ①
Out[79]: x y
0 1.189622 -1.690617
3 0.007315 -0.612939
4 1.299748 -1.733096
9 0.108863 0.507810
In [80]: df[(df['x'] > 0) & (df['y'] < 0)] # ②
Out[80]: x y
0 1.189622 -1.690617
3 0.007315 -0.612939
4 1.299748 -1.733096
In [81]: df[(df.x > 0) | (df.y < 0)] # ③
Out[81]: x y
0 1.189622 -1.690617
1 -1.356399 -1.232435
2 -0.544439 -0.668172
3 0.007315 -0.612939
4 1.299748 -1.733096
7 -1.188018 -0.549746
8 -0.940046 -0.827932
9 0.108863 0.507810
①
所有x
列的值大于 0.5 的行。
②
所有x
列的值为正且y
列的值为负的行。
③
所有列中 x
的值为正或列中 y
的值为负的所有行(这里通过各自的属性访问列)。
比较运算符也可以一次应用于完整的 DataFrame
对象。
In [82]: df > 0 # ①
Out[82]: x y
0 True False
1 False False
2 False False
3 True False
4 True False
5 False True
6 False True
7 False False
8 False False
9 True True
In [83]: df[df > 0] # ②
Out[83]: x y
0 1.189622 NaN
1 NaN NaN
2 NaN NaN
3 0.007315 NaN
4 1.299748 NaN
5 NaN 0.357508
6 NaN 1.470714
7 NaN NaN
8 NaN NaN
9 0.108863 0.507810
①
DataFrame
对象中哪些值是正数?
②
选择所有这样的值,并在所有其他位置放置 NaN
。
连接、合并和拼接
本节介绍了在形式上为 DataFrame
对象的两个简单数据集组合的不同方法。这两个简单数据集是:
In [84]: df1 = pd.DataFrame(['100', '200', '300', '400'],
index=['a', 'b', 'c', 'd'],
columns=['A',])
In [85]: df1
Out[85]: A
a 100
b 200
c 300
d 400
In [86]: df2 = pd.DataFrame(['200', '150', '50'],
index=['f', 'b', 'd'],
columns=['B',])
In [87]: df2
Out[87]: B
f 200
b 150
d 50
拼接
拼接或附加基本上意味着将行从一个 DataFrame
对象添加到另一个 DataFrame
对象。这可以通过 append()
方法或 pd.concat()
函数完成。一个主要问题是如何处理索引值。
In [88]: df1.append(df2) # ①
Out[88]: A B
a 100 NaN
b 200 NaN
c 300 NaN
d 400 NaN
f NaN 200
b NaN 150
d NaN 50
In [89]: df1.append(df2, ignore_index=True) # ②
Out[89]: A B
0 100 NaN
1 200 NaN
2 300 NaN
3 400 NaN
4 NaN 200
5 NaN 150
6 NaN 50
In [90]: pd.concat((df1, df2)) # ③
Out[90]: A B
a 100 NaN
b 200 NaN
c 300 NaN
d 400 NaN
f NaN 200
b NaN 150
d NaN 50
In [91]: pd.concat((df1, df2), ignore_index=True) # ④
Out[91]: A B
0 100 NaN
1 200 NaN
2 300 NaN
3 400 NaN
4 NaN 200
5 NaN 150
6 NaN 50
①
将来自 df2
的数据附加为 df1
的新行。
②
做同样的事情,但忽略了索引。
③
具有与第一个相同的效果,并且…
④
第二个追加操作,分别。
连接
在连接这两个数据集时,DataFrame
对象的顺序也很重要,但方式不同。只使用第一个 DataFrame
对象的索引值。这种默认行为称为左连接。
In [92]: df1.join(df2) # ①
Out[92]: A B
a 100 NaN
b 200 150
c 300 NaN
d 400 50
In [93]: df2.join(df1) # ②
Out[93]: B A
f 200 NaN
b 150 200
d 50 400
①
df1
的索引值相关。
②
df2
相关的索引值。
一共有四种不同的连接方法可用,每种方法都会导致索引值和相应数据行的处理方式不同。
代码语言:javascript复制In [94]: df1.join(df2, how='left') # ①
Out[94]: A B
a 100 NaN
b 200 150
c 300 NaN
d 400 50
In [95]: df1.join(df2, how='right') # ②
Out[95]: A B
f NaN 200
b 200 150
d 400 50
In [96]: df1.join(df2, how='inner') # ③
Out[96]: A B
b 200 150
d 400 50
In [97]: df1.join(df2, how='outer') # ④
Out[97]: A B
a 100 NaN
b 200 150
c 300 NaN
d 400 50
f NaN 200
①
左连接是默认操作。
②
右连接与颠倒 DataFrame
对象的顺序相同。
③
内连接仅保留那些在两个索引中都找到的索引值。
④
外连接保留来自两个索引的所有索引值。
也可以基于空的 DataFrame
对象进行连接。在这种情况下,列会被顺序创建,导致行为类似于左连接。
In [98]: df = pd.DataFrame()
In [99]: df['A'] = df1 # ①
In [100]: df
Out[100]: A
0 NaN
1 NaN
2 NaN
3 NaN
In [101]: df['B'] = df2 # ②
In [102]: df
Out[102]: A B
0 NaN NaN
1 NaN NaN
2 NaN NaN
3 NaN NaN
①
df1
作为第一列 A
。
②
df2
作为第二列 B
。
利用字典组合数据集的方式产生了类似外连接的结果,因为列是同时创建的。
代码语言:javascript复制In [103]: df = pd.DataFrame({'A': df1['A'], 'B': df2['B']}) # ①
In [104]: df
Out[104]: A B
a 100 NaN
b 200 150
c 300 NaN
d 400 50
f NaN 200
①
DataFrame
对象的列被用作 dict
对象中的值。
合并
虽然连接操作是基于要连接的 DataFrame
对象的索引进行的,但合并操作通常是在两个数据集之间共享的列上进行的。为此,将新列 C
添加到原始的两个 DataFrame
对象中:
In [105]: c = pd.Series([250, 150, 50], index=['b', 'd', 'c'])
df1['C'] = c
df2['C'] = c
In [106]: df1
Out[106]: A C
a 100 NaN
b 200 250.0
c 300 50.0
d 400 150.0
In [107]: df2
Out[107]: B C
f 200 NaN
b 150 250.0
d 50 150.0
默认情况下,此情况下的合并操作基于单个共享列 C
进行。然而,还有其他选项可用。
In [108]: pd.merge(df1, df2) # ①
Out[108]: A C B
0 100 NaN 200
1 200 250.0 150
2 400 150.0 50
In [109]: pd.merge(df1, df2, on='C') # ①
Out[109]: A C B
0 100 NaN 200
1 200 250.0 150
2 400 150.0 50
In [110]: pd.merge(df1, df2, how='outer') # ②
Out[110]: A C B
0 100 NaN 200
1 200 250.0 150
2 300 50.0 NaN
3 400 150.0 50
①
默认在列 C
上合并。
②
外部合并也是可能的,保留所有数据行。
还有许多其他类型的合并操作可用,以下代码示例了其中的一些:
代码语言:javascript复制In [111]: pd.merge(df1, df2, left_on='A', right_on='B')
Out[111]: A C_x B C_y
0 200 250.0 200 NaN
In [112]: pd.merge(df1, df2, left_on='A', right_on='B', how='outer')
Out[112]: A C_x B C_y
0 100 NaN NaN NaN
1 200 250.0 200 NaN
2 300 50.0 NaN NaN
3 400 150.0 NaN NaN
4 NaN NaN 150 250.0
5 NaN NaN 50 150.0
In [113]: pd.merge(df1, df2, left_index=True, right_index=True)
Out[113]: A C_x B C_y
b 200 250.0 150 250.0
d 400 150.0 50 150.0
In [114]: pd.merge(df1, df2, on='C', left_index=True)
Out[114]: A C B
f 100 NaN 200
b 200 250.0 150
d 400 150.0 50
In [115]: pd.merge(df1, df2, on='C', right_index=True)
Out[115]: A C B
a 100 NaN 200
b 200 250.0 150
d 400 150.0 50
In [116]: pd.merge(df1, df2, on='C', left_index=True, right_index=True)
Out[116]: A C B
b 200 250.0 150
d 400 150.0 50
性能方面
本章中的许多示例说明了使用 pandas
可以实现相同目标的多个选项。本节比较了用于逐元素添加两列的此类选项。首先,使用 NumPy
生成的数据集。
In [117]: data = np.random.standard_normal((1000000, 2)) # ①
In [118]: data.nbytes # ①
Out[118]: 16000000
In [119]: df = pd.DataFrame(data, columns=['x', 'y']) # ②
In [120]: df.info() # ②
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1000000 entries, 0 to 999999
Data columns (total 2 columns):
x 1000000 non-null float64
y 1000000 non-null float64
dtypes: float64(2)
memory usage: 15.3 MB
①
带有随机数字的 ndarray
对象。
②
带有随机数字的 DataFrame
对象。
第二,一些完成任务的性能值的选项。
代码语言:javascript复制In [121]: %time res = df['x'] df['y'] # ①
CPU times: user 5.68 ms, sys: 14.5 ms, total: 20.1 ms
Wall time: 4.06 ms
In [122]: res[:3]
Out[122]: 0 0.387242
1 -0.969343
2 -0.863159
dtype: float64
In [123]: %time res = df.sum(axis=1) # ②
CPU times: user 44 ms, sys: 14.9 ms, total: 58.9 ms
Wall time: 57.6 ms
In [124]: res[:3]
Out[124]: 0 0.387242
1 -0.969343
2 -0.863159
dtype: float64
In [125]: %time res = df.values.sum(axis=1) # ③
CPU times: user 16.1 ms, sys: 1.74 ms, total: 17.8 ms
Wall time: 16.6 ms
In [126]: res[:3]
Out[126]: array([ 0.3872424 , -0.96934273, -0.86315944])
In [127]: %time res = np.sum(df, axis=1) # ④
CPU times: user 39.7 ms, sys: 8.91 ms, total: 48.7 ms
Wall time: 47.7 ms
In [128]: res[:3]
Out[128]: 0 0.387242
1 -0.969343
2 -0.863159
dtype: float64
In [129]: %time res = np.sum(df.values, axis=1) # ⑤
CPU times: user 16.1 ms, sys: 1.78 ms, total: 17.9 ms
Wall time: 16.6 ms
In [130]: res[:3]
Out[130]: array([ 0.3872424 , -0.96934273, -0.86315944])
①
直接操作列(Series
对象)是最快的方法。
②
这通过在 DataFrame
对象上调用 sum()
方法来计算总和。
③
这通过在 ndarray
对象上调用 sum()
方法来计算总和。
④
这通过在 DataFrame
对象上调用 np.sum()
方法来计算总和。
⑤
这通过在 ndarray
对象上使用通用函数 np.sum()
方法来计算总和。
最后,更多基于 eval()
和 apply()
方法的选项。
In [131]: %time res = df.eval('x y') # ①
CPU times: user 13.3 ms, sys: 15.6 ms, total: 28.9 ms
Wall time: 18.5 ms
In [132]: res[:3]
Out[132]: 0 0.387242
1 -0.969343
2 -0.863159
dtype: float64
In [133]: %time res = df.apply(lambda row: row['x'] row['y'], axis=1) # ②
CPU times: user 22 s, sys: 71 ms, total: 22.1 s
Wall time: 22.1 s
In [134]: res[:3]
Out[134]: 0 0.387242
1 -0.969343
2 -0.863159
dtype: float64
# tag::PD_34[]
①
eval()
是专门用于评估(复杂)数值表达式的方法;可以直接访问列。
②
最慢的选项是逐行使用 apply()
方法;这就像在 Python 级别上循环遍历所有行。
注意
pandas
通常提供多种选项来实现相同的目标。如果不确定,应该比较一些选项,以确保在时间紧迫时获得最佳性能。在简单示例中,执行时间相差数个数量级。
结论
pandas
是数据分析的强大工具,并已成为所谓 PyData 栈的核心包。它的 DataFrame
类特别适用于处理任何类型的表格数据。对这种对象的大多数操作都是矢量化的,这不仅使代码简洁,而且通常性能很高,与 NumPy
的情况一样。此外,pandas
还使得处理不完整的数据集变得方便,例如,使用 NumPy
并不那么方便。在本书的许多后续章节中,pandas
和 DataFrame
类将是核心,当需要时还将使用和说明其他功能。
进一步阅读
pandas
是一个文档齐全的开源项目,既有在线文档,也有可供下载的 PDF 版本。¹。以下页面提供了所有资源:
- http://pandas.pydata.org/
至于 NumPy
,在书籍形式上推荐的参考资料是:
- McKinney, Wes (2017): Python 数据分析. 第二版, O’Reilly, 北京等地。
- VanderPlas, Jake (2016): Python 数据科学手册. O’Reilly, 北京等地。
¹ 在撰写本文时,PDF 版本共有 2,207 页(版本 0.21.1)。 l float64 y 1000000 non-null float64 dtypes: float64(2) memory usage: 15.3 MB
代码语言:javascript复制①
带有随机数字的 `ndarray` 对象。
②
带有随机数字的 `DataFrame` 对象。
第二,一些完成任务的性能值的选项。
```py
In [121]: %time res = df['x'] df['y'] # ①
CPU times: user 5.68 ms, sys: 14.5 ms, total: 20.1 ms
Wall time: 4.06 ms
In [122]: res[:3]
Out[122]: 0 0.387242
1 -0.969343
2 -0.863159
dtype: float64
In [123]: %time res = df.sum(axis=1) # ②
CPU times: user 44 ms, sys: 14.9 ms, total: 58.9 ms
Wall time: 57.6 ms
In [124]: res[:3]
Out[124]: 0 0.387242
1 -0.969343
2 -0.863159
dtype: float64
In [125]: %time res = df.values.sum(axis=1) # ③
CPU times: user 16.1 ms, sys: 1.74 ms, total: 17.8 ms
Wall time: 16.6 ms
In [126]: res[:3]
Out[126]: array([ 0.3872424 , -0.96934273, -0.86315944])
In [127]: %time res = np.sum(df, axis=1) # ④
CPU times: user 39.7 ms, sys: 8.91 ms, total: 48.7 ms
Wall time: 47.7 ms
In [128]: res[:3]
Out[128]: 0 0.387242
1 -0.969343
2 -0.863159
dtype: float64
In [129]: %time res = np.sum(df.values, axis=1) # ⑤
CPU times: user 16.1 ms, sys: 1.78 ms, total: 17.9 ms
Wall time: 16.6 ms
In [130]: res[:3]
Out[130]: array([ 0.3872424 , -0.96934273, -0.86315944])
①
直接操作列(Series
对象)是最快的方法。
②
这通过在 DataFrame
对象上调用 sum()
方法来计算总和。
③
这通过在 ndarray
对象上调用 sum()
方法来计算总和。
④
这通过在 DataFrame
对象上调用 np.sum()
方法来计算总和。
⑤
这通过在 ndarray
对象上使用通用函数 np.sum()
方法来计算总和。
最后,更多基于 eval()
和 apply()
方法的选项。
In [131]: %time res = df.eval('x y') # ①
CPU times: user 13.3 ms, sys: 15.6 ms, total: 28.9 ms
Wall time: 18.5 ms
In [132]: res[:3]
Out[132]: 0 0.387242
1 -0.969343
2 -0.863159
dtype: float64
In [133]: %time res = df.apply(lambda row: row['x'] row['y'], axis=1) # ②
CPU times: user 22 s, sys: 71 ms, total: 22.1 s
Wall time: 22.1 s
In [134]: res[:3]
Out[134]: 0 0.387242
1 -0.969343
2 -0.863159
dtype: float64
# tag::PD_34[]
①
eval()
是专门用于评估(复杂)数值表达式的方法;可以直接访问列。
②
最慢的选项是逐行使用 apply()
方法;这就像在 Python 级别上循环遍历所有行。
注意
pandas
通常提供多种选项来实现相同的目标。如果不确定,应该比较一些选项,以确保在时间紧迫时获得最佳性能。在简单示例中,执行时间相差数个数量级。
结论
pandas
是数据分析的强大工具,并已成为所谓 PyData 栈的核心包。它的 DataFrame
类特别适用于处理任何类型的表格数据。对这种对象的大多数操作都是矢量化的,这不仅使代码简洁,而且通常性能很高,与 NumPy
的情况一样。此外,pandas
还使得处理不完整的数据集变得方便,例如,使用 NumPy
并不那么方便。在本书的许多后续章节中,pandas
和 DataFrame
类将是核心,当需要时还将使用和说明其他功能。
进一步阅读
pandas
是一个文档齐全的开源项目,既有在线文档,也有可供下载的 PDF 版本。¹。以下页面提供了所有资源:
- http://pandas.pydata.org/
至于 NumPy
,在书籍形式上推荐的参考资料是:
- McKinney, Wes (2017): Python 数据分析. 第二版, O’Reilly, 北京等地。
- VanderPlas, Jake (2016): Python 数据科学手册. O’Reilly, 北京等地。
¹ 在撰写本文时,PDF 版本共有 2,207 页(版本 0.21.1)。