Python 金融编程第二版(二)

2024-06-10 08:20:31 浏览数 (2)

原文: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维数组。

线性代数和向量空间理论等数学学科说明了这些数学结构在许多科学学科和领域中的重要性。因此,设计一个专门的数据结构类来方便和高效地处理数组可能是非常有益的。这就是PythonNumPy的作用所在,其ndarray类应运而生。在下一节介绍其强大的ndarray类之前,本节展示了两种处理数组的替代方法。

使用 Python 列表的数组

在转向NumPy之前,让我们首先用上一节介绍的内置数据结构构建数组。list对象特别适用于完成这项任务。一个简单的list已经可以被视为一维数组:

代码语言:javascript复制
In [1]: v = [0.5, 0.75, 1.0, 1.5, 2.0]  # ①

list对象与数字。

由于list对象可以包含任意其他对象,它们也可以包含其他list对象。通过嵌套list对象,可以轻松构建二维和更高维的数组:

代码语言:javascript复制
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对象会发生什么变化:

代码语言:javascript复制
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函数,可以避免这种情况:

代码语言:javascript复制
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对象。

代码语言:javascript复制
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

代码语言:javascript复制
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类的一个优点是它具有内置的存储和检索功能。

代码语言:javascript复制
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对象的数据类型很重要。

代码语言:javascript复制
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维数组,即以高性能的方式。这个类的基本处理最好通过示例来说明:

代码语言:javascript复制
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类的一个重要特性是内置方法的多样性。例如:

代码语言:javascript复制
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对象定义的(向量化的)数学运算

代码语言:javascript复制
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模块中相同功能的性能降低。

代码语言:javascript复制
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对象,以便在执行代码期间生成的结果后来填充它们。为此,我们可以使用以下函数:

代码语言:javascript复制
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对象,其中数字之间的间隔均匀分布;所使用的参数是startendnum(元素数量)。

使用所有这些函数,我们可以提供以下参数:

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对象都提供访问一些有用属性的功能。

代码语言:javascript复制
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对象默认是不可变的,但有多种选项可以重塑和调整此类对象。一般情况下,第一个操作只是提供相同数据的另一个视图,而第二个操作一般会创建一个新的(临时)对象。

代码语言:javascript复制
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对象中的元素总数保持不变。在调整大小操作期间,此数字会更改,即它要么减少(“向下调整”),要么增加(“向上调整”)。

代码语言:javascript复制
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对象。但是,“连接”维度的大小必须相同。

代码语言:javascript复制
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顺序)进行展平。

代码语言:javascript复制
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对象(dtypebool)。

代码语言:javascript复制
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 表示TrueFalse

值是否大于…且小于或等于…?

此类布尔数组可用于索引和数据选择。注意以下操作会展平数据。

代码语言: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对象。

代码语言:javascript复制
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推导来实现:

代码语言:javascript复制
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对象,并同时填充它(伪)随机数:

代码语言:javascript复制
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对象。什么是“每列”?考虑以下结构化数组对象的初始化:

代码语言:javascript复制
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对象的最大字符数)。现在可以通过它们的名称轻松访问单个列,并通过它们的索引值访问行:

代码语言:javascript复制
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中,函数式编程工具,如mapfilter,提供了一些基本的矢量化手段。然而,NumPy在其核心深处内置了矢量化。

基本矢量化

正如我们在上一节中学到的,简单的数学运算,如计算所有元素的总和,可以直接在ndarray对象上实现(通过方法或通用函数)。还可以进行更一般的矢量化操作。例如,我们可以按元素将两个NumPy数组相加如下:

代码语言:javascript复制
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还支持所谓的广播。这允许在单个操作中组合不同形状的对象。我们之前已经使用过这个功能。考虑以下示例:

代码语言:javascript复制
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对象,直到某个特定点为止:

代码语言:javascript复制
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。如果实现允许,数组可以像intfloat对象一样与函数一起使用。考虑以下函数:

代码语言:javascript复制
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对象的情况:

代码语言:javascript复制
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对象的基本示例和用例,并考虑它们在不同内存布局下执行的速度:

代码语言:javascript复制
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-ordered ndarray 对象的求和在行和列上都更快(绝对速度优势)。
  • 使用 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 类”

本章从使用简单且小的数据集探索pandasDataFrame类的基本特征和能力开始;然后通过使用NumPyndarray对象并将其转换为DataFrame对象来进行处理。

“基本分析” 和 “基本可视化”

本章还展示了基本的分析和可视化能力,尽管后面的章节在这方面更深入。

“Series 类”

本节简要介绍了pandasSeries类,它在某种程度上代表了DataFrame类的一个特殊情况,只包含单列数据。

“GroupBy 操作”

DataFrame类的一大优势在于根据单个或多个列对数据进行分组。

“复杂选择”

使用(复杂)条件允许从DataFrame对象中轻松选择数据。

“串联、连接和合并”

将不同数据集合并为一个是数据分析中的重要操作。pandas提供了多种选项来完成这样的任务。

“性能方面”

与 Python 一般一样,pandas在一般情况下提供了多种选项来完成相同的目标。本节简要讨论潜在的性能差异。

DataFrame 类

本节涵盖了DataFrame类的一些基本方面。这个类非常复杂和强大,这里只能展示其中一小部分功能。后续章节提供更多例子并揭示不同的方面。

使用 DataFrame 类的第一步

从相当基本的角度来看,DataFrame类被设计用来管理带索引和标签的数据,与SQL数据库表或电子表格应用程序中的工作表并没有太大的不同。考虑以下创建DataFrame对象的示例:

代码语言:javascript复制
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类的一些主要特性:

数据

数据本身可以以不同的形状和类型提供(listtuplendarraydict对象都是候选对象)。

标签

数据以列的形式组织,可以具有自定义名称。

索引

存在可以采用不同格式(例如,数字、字符串、时间信息)的索引。

与此类DataFrame对象一起工作通常非常方便和高效,例如,与常规的ndarray对象相比,当您想要像扩大现有对象一样时,后者更为专业和受限。以下是展示在DataFrame对象上进行典型操作的简单示例:

代码语言:javascript复制
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对应的值。

选择与索引ad对应的两个值。

通过索引位置选择第二行和第三行。

计算单列的总和。

使用apply()方法以向量化方式计算平方。

直接应用向量化,就像使用ndarray对象一样。

NumPyndarray对象相反,可以在两个维度上扩大DataFrame对象:

代码语言:javascript复制
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对象也可以用来定义新列。在这种情况下,索引会自动对齐:

代码语言:javascript复制
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来管理时间序列数据。

代码语言:javascript复制
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将保留基本结构,并且“只”会添加元信息(例如,索引值)。它还代表了金融应用和一般科学研究的典型用例。例如:

代码语言:javascript复制
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对象的数据结构,例如listIndexpandas 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对象的属性:

代码语言:javascript复制
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对象,如下所示:

代码语言:javascript复制
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对象定义为相关的索引对象,从而使原始数据集生成时间序列:

代码语言:javascript复制
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属性直接提供了对它的访问。

代码语言:javascript复制
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属性或NumPynp.array()函数轻松地从DataFrame生成ndarray对象。

基本分析

NumPyndarray对象一样,pandasDataFrame类内置了许多便利方法。作为入门,考虑info()方法和 describe()。

代码语言:javascript复制
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通用函数:

代码语言:javascript复制
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通用函数应用于pandasDataFrame对象,只要它们可以应用于包含相同类型数据的ndarray对象。

pandas相当容错,以捕获错误并在相应的数学运算失败时仅放置NaN值。不仅如此,正如之前简要展示的那样,您还可以在许多情况下像处理完整数据集一样处理这些不完整数据集。这非常方便,因为现实往往被不完整的数据集所表征,这比人们希望的更常见。

基本可视化

通常情况下,一旦数据存储在DataFrame对象中,数据的绘制就只需一行代码即可(参见图 5-1):

代码语言:javascript复制
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 类

到目前为止,我们主要使用 pandasDataFrame 类。Series 类是另一个与 pandas 一起提供的重要类。它的特点是只有一列数据。从这个意义上说,它是 DataFrame 类的一个特化,共享许多但不是所有的特征和功能。通常,当从多列 DataFrame 对象中选择单列时,会得到一个 Series 对象:

代码语言:javascript复制
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):

代码语言:javascript复制
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中的数据透视表类似。为了有东西可以分组,我们添加了一列,指示相应数据所属的季度:

代码语言:javascript复制
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列进行分组,并且可以输出单个组的统计信息:

代码语言:javascript复制
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

这就是对pandasDataFrame对象的介绍。后续部分将使用这个工具集来处理真实世界的金融数据。

复杂选择

数据选择通常通过在列值上制定条件来完成,并可能逻辑地组合多个这样的条件。考虑以下数据集。

代码语言: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对象,复杂数据(行)的选择很简单。

代码语言:javascript复制
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 对象。

代码语言:javascript复制
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 对象的两个简单数据集组合的不同方法。这两个简单数据集是:

代码语言:javascript复制
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() 函数完成。一个主要问题是如何处理索引值。

代码语言:javascript复制
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 对象的索引值。这种默认行为称为左连接

代码语言:javascript复制
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 对象进行连接。在这种情况下,列会被顺序创建,导致行为类似于左连接。

代码语言:javascript复制
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 对象中:

代码语言:javascript复制
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 进行。然而,还有其他选项可用。

代码语言:javascript复制
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 生成的数据集。

代码语言:javascript复制
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() 方法的选项。

代码语言:javascript复制
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 并不那么方便。在本书的许多后续章节中,pandasDataFrame 类将是核心,当需要时还将使用和说明其他功能。

进一步阅读

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() 方法的选项。

代码语言:javascript复制
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 并不那么方便。在本书的许多后续章节中,pandasDataFrame 类将是核心,当需要时还将使用和说明其他功能。

进一步阅读

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)。

0 人点赞