Python 金融编程第二版(二)(1)https://developer.aliyun.com/article/1559402
布尔数组
比较和逻辑操作通常在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
。
⑤
值是否大于…且小于或等于…?
此类布尔数组可用于索引和数据选择。注意以下操作会展平数据。
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
类(见 第五章)
更多资源
有用的资源提供在:
优秀的 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
在一般情况下提供了多种选项来完成相同的目标。本节简要讨论潜在的性能差异。
Python 金融编程第二版(二)(3)https://developer.aliyun.com/article/1559404