数据!数据!数据!没有黏土我无法造出砖来!pandas数据分析

数据!数据!数据!没有黏土我无法造出砖来!
——夏洛克•福尔摩斯

本章介绍pandas,这是一个聚焦于表格数据的数据分析程序库。pandas是一个强大的工具,它不仅提供了许多实用类和函数,而且很好地封装了来自其他软件包的功能。该工具提供了一个用户接口,能够让用户方便且高效地实现数据分析,特别是金融分析。

本章介绍如下基本数据结构:

对象类型含义用途
DataFrame带索引的二维数据对象按列组织的表格数据
S eries 带索引的一维数据对象单一(时间)数据序列

本章的组织如下。

DataFrame类

本节首先用简单的小数据集来探索pandas中DataFrame类的基本特性和功能;然后说明如何将NumPy中的ndarray对象转换为DataFrame对象。

基本分析与基本可视化

这两节介绍基本分析与可视化功能(后续的章节将深入介绍这些主题)。

Series类

本节短小精悍,介绍了pandas的Series类。从某种意义上讲,它是DataFrame的一种特例,只有一列数据。

GroupBy操作

DataFrame的优势之一是根据一列或者多列来分组数据。这一节探索pandas的分组能力。

复杂选择

这一节介绍如何使用(复杂)条件,轻松地从DataFrame对象中选择数据。

联接、连接和合并

将不同数据集合而为一是数据分析中的重要操作。pandas提供了实现这一任务的不同选项,本节将对此进行介绍。

性能特征

pandas往往提供实现相同目标的多个选项,这符合Python的常规。本节简单介绍潜在的性能差异。

5.1 DataFrame类

pandas(以及本章)的核心是DataFrame,它是设计用于高效处理表格数据(也就是以列进行组织的数据)的类。为此,DataFrame提供了列标签以及数据集中各行(记录)的索引功能,这与关系数据库的一个表或者Excel电子表格类似。

本节介绍pandas中 DataFrame类的一些根本特性。该类非常复杂且强大,这里只能介绍其中的一小部分功能。后续章节将提供更多示例,揭示其不同方面的特征。

5.1.1 使用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。

❷ 定义列表对象形式的数据。

❸ 指定列标签。

❹ 指定索引值/标签。

❺ 显示DataFrame对象的数据以及列和索引标签。

这个简单的例子已经说明了DataFrame在存储数据上的主要特性,具体如下。

  • 数据本身可以用不同组成及类型(列表、元组、ndarray和字典对象都是候选者)展现。
  • 数据以列的方式被组织,可以自定义列名(标签)。
  • 索引可以采用不同的格式(如数值、字符串、时间信息)。

使用这种DataFrame对象总体上相当方便和高效,例如,当您想要进行扩大现有对象等工作时,我推荐你使用DataFrame对象。相比之下,常规的ndarray对象更专门化,也更受限制。与此同时,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的值。

❹ 选择对应于索引ab的两个值。

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

❻ 计算单列总和。

❼ 使用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

❶ 添加一个新列,该包含以元组形式提供的浮点数对象。

❷ 选择该列并显示其数据和索引标签。

也可以使用整个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对象创建另一个新列。

附加数据的方法也类似。但是,在下面的例子中,我们会看到在操作过程中必须避免的一个副作用——索引被简单的编号索引代替:

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]:    numbers floats    names
         a       10   1.50   Sandra
         b       20   2.50    Lilli
         c       30   3.50    Henry
         d       40   4.50     Yves
         y       100  5.75      Jil

In [20]: df = df.append(pd.DataFrame({'names': 'Liz'}, index=['z',]),
                        sort=False) ❸
In [21]: df
Out[21]:    numbers floats    names
         A     10.0   1.50   Sandra
         b     20.0   2.50    Lilli
         c     30.0   3.50    Henry
         d     40.0   4.50     Yves
         y     100.0  5.75      Jil
         z     NaN     NaN      Liz

In [22]: df.dtypes ❹
Out[22]: numbers float64
         floats float64
         names object
         dtype: object

❶ 通过一个字典对象附加新行;这是一个临时操作,操作期间索引信息丢失。

❷ 根据带索引信息的DataFrame对象附加行;原始索引信息保留。

❸ 向DataFrame对象附加不完整的数据行,生成NaN值。

❹ 返回各列的不同dtype,这与结构化ndarray对象类似。

尽管此时有遗漏的值,但大部的分方法调用仍然可以正常工作:

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值的行)。

5.1.2 使用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函数使用的参数。表中,“类似数组”意味着和ndarray对象类似的数据结构——如列表对象。“索引”是pandas的Index类的一个实例。

表5-1 DataFrame函数参数

参数格式描述
datandarray/ dict /DataFrameDataFrame数据;dict可以包含序列、ndarray和列表
index索引/类似数组使用的索引;默认为range(n)
columns索引/类似数组使用的列标题;默认为range(n)
dtypedtype,默认None使用/强制的数据类型;否则通过推导得出
copy布尔值,默认None从输入复制数据

和结构数组一样,我们已经了解,DataFrame对象可以通过指定一个具有合适数量元素的列表来直接定义列名。下面的例子说明,我们可以随时定义/更改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.7010330941456459

❶ 通过列表对象指定列标签。

❷ 选择一列很容易。

为了高效处理金融事件序列数据,我们还必须很好地处理时间索引。这也可以视为pandas的一个重要优势。例如,假定分为4列的9个数据项对应于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字符串/日期时间生成日期的左界
end字符串/日期时间生成日期的右界
periods整数/None期数(如果start或者end空缺)
freq字符串/日期偏移频率字符串,例如5D(5天)
tz字符串/None本地化索引的时区名称
normalize布尔值,默认Nonestartend规范化为午夜
name字符串,默认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有多种选择。表6-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]])

数组和DataFrame

通常可以从一个ndarray对象生成DataFrame对象。也可以通过NumPy的np.array函数或DataFrame类的values属性,来从DataFrame对象生成ndarray对象。

5.2 基本分析

和NumPy的ndarray类一样,pandas的DataFrame类有多个便利的内建方法。首先考虑infodescribe方法:

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

❶ 提供关于数据、列和索引的元信息。

❷ 提供有用的每列汇总统计(对于数值数据)信息。

此外,你可以轻松地求得按列/行计算的总和、平均值和累计总和:

In [43]: df.sum() ❶
Out[43]: No1   -1.351906
         No2    6.309298
         No3    2.602739
         No4   -3.490089
         dtype: float64

In [44]: df.mean()
Out[44]: No1   -0.150212
         No2    0.701033
         No3    0.289193
         No4   -0.387788
         dtype: float64

In [45]: df.mean(axis=0) ❷
Out[45]: No1   -0.150212
         No2    0.701033
         No3    0.289193
         No4   -0.387788
         dtype: float64

In [46]: df.mean(axis=1) ❸
Out[46]: 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 [47]: df.cumsum() ❹
Out[47]:                  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 [48]: np.mean(df) ❶
Out[48]: No1   -0.150212
         No2    0.701033
         No3    0.289193
         No4   -0.387788
         dtype: float64

In [49]: np.log(df) ❷
Out[49]: 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 [50]: np.sqrt(abs(df)) ❸
Out[50]:                  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 [51]: np.sqrt(abs(df)).sum() ❹
Out[51]: No1    7.384345
         No2    7.075190
         No3    6.397719
         No4    7.538440
         dtype: float64

In [52]: 100 * df + 100 ❺
Out[52]:                   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通用函数适用于ndarray对象的所有情况下,都可以将这些函数应用到包含相同数据的pandas的DataFrame对象。

pandas有相当强的容错能力,它可以捕捉错误,在对应数学运算失败时放入NaN值。不仅如此,正如前面已经展示的,在许多情况下,它还可以将这些不完整的数据集当成完整数据集来使用。这种做法很方便,因为现实中不完整数据集往往比人们预想的要多。

5.3 基本可视化

如果数据存储在DataFrame对象中,那么数据图表的绘制通常只需要一行代码(见图5-1)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UDnt9Y1E-1588055920052)(http://write.epubit.com:9000/api/storage/getbykey/original?key=1910a30dd4cece65faeb)]

图5-1 DataFrame对象的折线图

In [53]: from pylab import plt, mpl ❶
         plt.style.use('seaborn') ❶
         mpl.rcParams['font.family'] = 'serif' ❶
         %matplotlib inline

In [54]: df.cumsum().plot(lw=2.0, figsize=(10, 6)); ❷

❶ 定制绘图样式。

❷ 绘制4列累计总和的折线图。

基本上,pandas提供了专为DataFrame对象设计的matplotplib(参见第7章)包装器。plot()方法的参数如表5-4所示。

表5-4 plot方法参数

参数格式描述
x标签/位置,默认None只在列值为x刻度时使用
y标签/位置,默认None只在列值为x刻度时使用
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标签图例
axmatplotlib轴对象绘图使用的matplotlib轴对象
style字符串或者列表/字典绘图线形(每列)
kindline/bar/barh/kde/density图表类型
logx布尔值,默认Falsex轴的对数刻度
logy布尔值,默认Falsey轴的对数刻度
xticks序列,默认Indexx轴刻度
yticks序列,默认Valuesy轴刻度
xlim二元组,列表x轴界限
ylim二元组,列表y轴界限
rot整数,默认None旋转x刻度
secondary_y布尔值/序列,默认False第二个y轴
mark_right布尔值,默认True第二个y轴自动设置标签
colormap字符串/颜色映射对象,默认None用于绘图的颜色映射
kwds关键字传递给matplotlib选项

再举个例子,考虑相同数据的柱状图(见图5-2):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qulzxc1C-1588055920064)(http://write.epubit.com:9000/api/storage/getbykey/original?key=19100ee5cdfe00b32e76)]

图5-2 DataFrame对象的柱状图

In [55]: df.plot.bar(figsize=(10, 6), rot=15); ❶
# df.plot(kind='bar', figsize=(10, 6)) ❷

plot.bar()绘制柱状图。

❷ 替代语法:使用kind参数改变绘图类型。

5.4 Series类

迄今为止,本章主要介绍了pandas的DataFrame类。Series是pandas自带的另一个重要的类。它的特点是只有一列数据。从这个意义上讲,它是DataFrame类的特例,两者之间有许多共同特性和功能,但不完全相同。从多列的DataFrame对象上选取一列,可以得到Series对象:

In [56]: type(df)
Out[56]: pandas.core.frame.DataFrame

In [57]: S = pd.Series(np.linspace(0, 15, 7), name='series')

In [58]: S
Out[58]: 0     0.0
         1     2.5
         2     5.0
         3     7.5
         4     10.0
         5     12.5
         6     15.0
         Name: series, dtype: float64

In [59]: type(S)
Out[59]: pandas.core.series.Series

In [60]: s = df['No1']

In [61]: s
Out[61]: 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 [62]: type(s)
Out[62]: pandas.core.series.Series

DataFrame的主要方法也可用于Series对象,我们以meanplot方法为例(见图5-3):

In [63]: s.mean()
Out[63]: -0.15021177307319458

In [64]: s.plot(lw=2.0, figsize=(10, 6));

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nNf1wQwP-1588055920069)(http://write.epubit.com:9000/api/storage/getbykey/original?key=191090fffa80d7070209)]

图5-3 Series对象的折线图

5.5 GroupBy操作

pandas具备强大而灵活的分组功能,工作方式类似于SQL中的分组和Microsoft Excel中的透视表。为了进行分组,我们添加一列,表示对应索引数据所属的季度:

In [65]: df['Quarter'] = ['Q1', 'Q1', 'Q1', 'Q2', 'Q2',
                          'Q2', 'Q3', 'Q3', 'Q3']
         df
Out[65]:                  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 [66]: groups = df.groupby('Quarter') ❶

In [67]: groups.size() ❷
Out[67]: Quarter
         Q1    3
         Q2    3
         Q3    3
         dtype: int64

In [68]: groups.mean() ❸
Out[68]:               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 [69]: groups.max() ❹
Out[69]:               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 [70]: groups.aggregate([min, max]).round(2) ❺
Out[70]:           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列分组。

❷ 给出每个分组的行数。

❸ 给出每的列均值。

❹ 给出每列的最大值。

❺ 给出每列的最小值和最大值。

分组也可以在多列上进行。为此,我们添加另一列,表示索引日期的月份是奇数还是偶数:

In [71]: df['Odd_Even'] = ['Odd', 'Even', 'Odd', 'Even', 'Odd', 'Even',
                          'Odd', 'Even', 'Odd']

In [72]: groups = df.groupby(['Quarter', 'Odd_Even'])

In [73]: groups.size()
Out[73]: Quarter  Odd_Even
         Q1       Even        1
                  Odd         2
         Q2       Even        2
                  Odd         1
         Q3       Even        1
                  Odd         2
         dtype: int64

In [74]: groups[['No1', 'No4']].aggregate([sum, np.mean])
Out[74]:                        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对象使用的介绍到此结束,下面将这组工具应用到现实世界的金融数据中。

5.6 复杂选择

数据选择往往是通过列值上的条件公式实现的,也可以通过符合逻辑的方式来组合多个条件。考虑如下数据集。

In [75]: data = np.random.standard_normal((10, 2)) ❶

In [76]: df = pd.DataFrame(data, columns=['x', 'y']) ❷

In [77]: 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 [78]: df.head() ❸
Out[78]:           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 [79]: df.tail() ❹
Out[79]:           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方法取得前5行。

❹ 通过tail方法取得最后5行。

下面的代码说明了Python比较运算符和逻辑运算符在两列值上的应用:

In [80]: df['x'] > 0.5 ❶
Out[80]: 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 [81]: (df['x'] > 0) & (df['y'] < 0) ❷
Out[81]: 0     True
         1    False
         2    False
         3     True
         4     True
         5    False
         6    False
         7    False
         8    False
         9    False
         dtype: bool

In [82]: (df['x'] > 0) | (df['y'] < 0) ❸
Out[82]: 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对象,就可以很容易地实现复杂数据(行)的选择。另外,也可以使用query方法并以字符串对象的形式来传递条件:

In [83]: df[df['x'] > 0] ❶
Out[83]:           x         y
         0  1.189622 -1.690617
         3  0.007315 -0.612939
         4  1.299748 -1.733096
         9  0.108863  0.507810

In [84]: df.query('x > 0') ❶
Out[84]:           x         y
         0  1.189622 -1.690617
         3  0.007315 -0.612939
         4  1.299748 -1.733096
         9  0.108863  0.507810

In [85]: df[(df['x'] > 0) & (df['y'] < 0)] ❷
Out[85]:           x         y
         0  1.189622 -1.690617
         3  0.007315 -0.612939
         4  1.299748 -1.733096

In [86]: df.query('x > 0 & y < 0') ❷
Out[86]:           x         y
         0  1.189622 -1.690617
         3  0.007315 -0.612939
         4  1.299748 -1.733096

In [87]: df[(df.x > 0) | (df.y < 0)] ❸
Out[87]:           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 [88]: df > 0 ❶
Out[88]:        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 [89]: df[df > 0] ❷
Out[89]:           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

5.7 联接、连接和合并

本节简单介绍连接DataFrame对象形式的两个简单数据集的不同方法。这两个简单数据集如下所示:

In [90]: df1 = pd.DataFrame(['100', '200', '300', '400'],
                             index=['a', 'b', 'c', 'd'],
                             columns=['A',])

In [91]: df1
Out[91]:       A
         a   100
         b   200
         c   300
         d   400

In [92]: df2 = pd.DataFrame(['200', '150', '50'],
                             index=['f', 'b', 'd'],
                             columns=['B',])

In [93]: df2
Out[93]:       B
         f 200
         b 150
         d 50

5.7.1 联接

联接(Concatenation)或者附加(Appending)本质上指的是将一个DataFrame对象中的行添加到另一个DataFrame对象上,这可通过append方法或者pd.concat函数完成。需要认真考虑的是索引值的处理方法:

In [94]: df1.append(df2, sort=False) ❶
Out[94]:      A    B
         a  100  NaN
         b  200  NaN
         c  300  NaN
         d  400  NaN
         f  NaN  200
         b  NaN  150
         d  NaN   50

In [95]: df1.append(df2, ignore_index=True, sort=False) ❷
Out[95]:      A    B
         0  100  NaN
         1  200  NaN
         2  300  NaN
         3  400  NaN
         4  NaN  200
         5  NaN  150
         6  NaN   50

In [96]: pd.concat((df1, df2), sort=False) ❸
Out[96]:      A    B
         a  100  NaN
         b  200  NaN
         c  300  NaN
         d  400  NaN
         f  NaN  200
         b  NaN  150
         d  NaN   50

In [97]: pd.concat((df1, df2), ignore_index=True, sort=False) ❹
Out[97]:      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中作为新行。

❷ 完成同样的工作,但忽略索引。

❸ 与第一个附加操作效果相同。

❹ 与第二个附加操作效果相同。

5.7.2 连接

连接两个数据集时,DataFrame对象的顺序很重要。只有第一个DataFrame对象的索引值被使用,这种默认行为称为左连接(left join):

In [98]: df1.join(df2) ❶
Out[98]:      A    B
         a  100  NaN
         b  200  150
         c  300  NaN
         d  400   50

In [99]: df2.join(df1) ❷
Out[99]:      B    A
         f  200  NaN
         b  150  200
         d   50  400

df1的索引值有意义。

df2的索引值有意义。

共有4种不同的连接方法,每种方法都导致索引值和对应数据行的不同处理行为:

In [100]: df1.join(df2, how='left') ❶
Out[100]:      A    B
          a  100  NaN
          b  200  150
          c  300  NaN
          d  400   50

In [101]: df1.join(df2, how='right') ❷
Out[101]:      A    B
          f  NaN  200
          b  200  150
          d  400   50

In [102]: df1.join(df2, how='inner') ❸
Out[102]:      A    B
          b  200  150
          d  400   50

In [103]: df1.join(df2, how='outer') ❹
Out[103]:      A    B
          a  100  NaN
          b  200  150
          c  300  NaN
          d  400   50
          f  NaN  200

❶ 左连接是默认操作。

❷ 右连接相当于颠倒了DataFrame对象的顺序。

❸ 内连接只保留在两个索引中都存在的索引值。

❹ 外连接保留两个索引中的所有索引值。

连接也可以基于空的DataFrame对象。在这种情况下,列按顺序被创建,这与左连接类似:

In [104]: df = pd.DataFrame()

In [105]: df['A'] = df1['A'] ❶

In [106]: df
Out[106]:      A
          a  100
          b  200
          c  300
          d  400

In [107]: df['B'] = df2 ❷

In [108]: df
Out[108]:      A    B
          a  100  NaN
          b  200  150
          c  300  NaN
          d  400   50

df1为第一列A。

df2为第二列B。

使用字典来组合数据集可以得到类似于外连接的结果,因为列是同时创建的:

In [109]: df = pd.DataFrame({'A': df1['A'], 'B': df2['B']})

In [110]: df
Out[110]:      A    B
          a  100  NaN
          b  200  150
          c  300  NaN
          d  400   50
          f  NaN  200

DataFrame对象的各列用作字典对象的值

5.7.3 合并

连接操作根据待连接的DataFrame对象的索引进行,而合并操作通常在两个数据集共享的某列上进行。为此,在两个原始的DataFrame对象上添加一个新列C

In [111]: c = pd.Series([250, 150, 50], index=['b', 'd', 'c'])
          df1['C'] = c
          df2['C'] = c

In [112]: df1
Out[112]:      A      C
          a  100    NaN
          b  200  250.0
          c  300   50.0
          d  400  150.0

In [113]: df2
Out[113]:      B      C
          f  200    NaN
          b  150  250.0
          d   50  150.0

默认情况下,这种合并操作根据单一共享列C进行。但也存在其他可能,例如外合并:

In [114]: pd.merge(df1, df2) ❶
Out[114]:      A      C    B
          0  100    NaN  200
          1  200  250.0  150
          2  400  150.0   50

In [115]: pd.merge(df1, df2, on='C') ❶
Out[115]:      A      C    B
          0  100    NaN  200
          1  200  250.0  150
          2  400  150.0   50

In [116]: pd.merge(df1, df2, how='outer')
Out[116]:      A      C    B
          0  100    NaN  200
          1  200  250.0  150
          2  300   50.0  NaN
          3  400  150.0   50

❶ 默认合并根据C列进行。

❷ 也可以进行外合并,保留所有数据行。

可用的合并操作类型还有很多,下面的代码演示了其中的几种:

In [117]: pd.merge(df1, df2, left_on='A', right_on='B')
Out[117]:      A    C_x    B  C_y
          0  200  250.0  200  NaN

In [118]: pd.merge(df1, df2, left_on='A', right_on='B', how='outer')
Out[118]:      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 [119]: pd.merge(df1, df2, left_index=True, right_index=True)
Out[119]:      A    C_x    B    C_y
          b  200  250.0  150  250.0
          d  400  150.0   50  150.0

In [120]: pd.merge(df1, df2, on='C', left_index=True)
Out[120]:      A      C    B
          f  100    NaN  200
          b  200  250.0  150
          d  400  150.0   50

In [121]: pd.merge(df1, df2, on='C', right_index=True)
Out[121]:      A      C    B
          a  100    NaN  200
          b  200  250.0  150
          d  400  150.0   50

In [122]: pd.merge(df1, df2, on='C', left_index=True, right_index=True)
Out[122]:      A    C      B
          b  200  250.0  150
          d  400  150.0   50

5.8 性能特征

本章的许多例子说明,用pandas实现同一个目标往往有多种选择。本节以按元素加总两列数值为例来这些选择进行对比。首先用NumPy生成数据集:

In [123]: data = np.random.standard_normal((1000000, 2)) ❶

In [124]: data.nbytes ❶
Out[124]: 16000000

In [125]: df = pd.DataFrame(data, columns=['x', 'y']) ❷

In [126]: 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对象。

其次,实现手边任务的某选择性能还不错:

In [127]: %time res = df['x'] + df['y'] ❶
          CPU times: user 7.35 ms, sys: 7.43 ms, total: 14.8 ms
          Wall time: 7.48 ms

In [128]: res[:3]
Out[128]: 0    0.387242
          1   -0.969343
          2   -0.863159
          dtype: float64

In [129]: %time res = df.sum(axis=1) ❷
          CPU times: user 130 ms, sys: 30.6 ms, total: 161 ms
          Wall time: 101 ms

In [130]: res[:3]
Out[130]: 0    0.387242
          1   -0.969343
          2   -0.863159
          dtype: float64

In [131]: %time res = df.values.sum(axis=1) ❸
          CPU times: user 50.3 ms, sys: 2.75 ms, total: 53.1 ms
          Wall time: 27.9 ms

In [132]: res[:3]
Out[132]: array([ 0.3872424 , -0.96934273, -0.86315944])

In [133]: %time res = np.sum(df, axis=1) ❹
          CPU times: user 127 ms, sys: 15.1 ms, total: 142 ms
          Wall time: 73.7 ms

In [134]: res[:3]
Out[134]: 0    0.387242
          1   -0.969343
          2   -0.863159
          dtype: float64

In [135]: %time res = np.sum(df.values, axis=1) ❺
          CPU times: user 49.3 ms, sys: 2.36 ms, total: 51.7 ms
          Wall time: 26.9 ms

In [136]: res[:3]
Out[136]: array([ 0.3872424 , -0.96934273, -0.86315944])

❶ 直接使用列(Series对象)是最快的方法。

❷ 调用DataFrame对象的sum方法来计算总和。

❸ 调用ndarray对象的sum方法来计算总和。

❹ 调用DataFrame对象的np.sum方法来计算总和。

❺ 调用ndarray对象的np.sum方法来计算总和。

最后,还有两种选择可以计算元素和,它们分别基于evalapply方法[1]

The application of the eval() method requires the numexpr package to   
be installed.
In [137]: %time res = df.eval('x + y') ❶
          CPU times: user 25.5 ms, sys: 17.7 ms, total: 43.2 ms
          Wall time: 22.5 ms

In [138]: res[:3]
Out[138]: 0    0.387242
          1   -0.969343
          2   -0.863159
          dtype: float64

In [139]: %time res = df.apply(lambda row: row['x'] + row['y'], axis=1)
          CPU times: user 19.6 s, sys: 83.3 ms, total: 19.7 s
          Wall time: 19.9 s

In [140]: res[:3]
Out[140]: 0    0.387242
          1   -0.969343
          2   -0.863159
          dtype: float64

eval是专门用于计算(复杂)数值表达式的方法,可以直接处理数据列。

❷ 最慢的选择是逐行使用apply方法,这就像在Python级别上循环访问所有行。

明智的选择

pandas常常为实现统一目标提供多个选择。如果不确定使用哪一个,那么在时间是关键因素的情况下,比较各种选择,选择性能最好的一种。在上面这个简单的例子里,不同选择的执行时间相差好几个数量级。

5.9 结语

pandas是强大的数据分析工具,已经成为所谓的PyData栈的核心软件包。它的DataFrame类特别适合于处理任何类型的表格数据。这些对象上的大部分操作是向量化的,这不仅可以得到和NumPy类似的简洁代码,而且还可以得到高性能。此外,pandas能够很方便地处理不完整数据集(NumPy做不到这一点)。pandas和DataFrame类将是本书后续多个章节的核心,在必要时将使用并介绍它们的更多特性。

5.10 延伸阅读

pandas是一个开源项目,你既可以阅读在线文档,也可以下载其PDF版本[2]。官网上还提供了一些附加资源。

至于NumPy,我们建议的pandas参考书如下。

  • McKinney, Wes (2017). Python for Data Analysis. Sebastopol, CA: O’Reilly.
  • VanderPlas, Jake (2016). Python Data Science Handbook. Sebastopol, CA: O’Reilly.

[1] 要想使用eval方法需要安装numexpr软件包。——原注

[2] 在本书编写期间,PDF文档共有2500页。——原注

本文摘自《Python金融大数据分析 第2版》[德] 伊夫·希尔皮斯科(Yves Hilpisch) 著,姚军 译

  • 金融科技算法交易量化金融教程书籍
  • 详细讲解使用Python分析处理金融大数据的专业图书
  • 将人工智能应用于金融开发的实战指南,金融应用开发领域从业人员的常备读物

Python已成为数据驱动AI、金融优先选择的编程语言。现在,一些大型的投资银行和对冲资金均使用Python及其生态系统来构建核心交易与风险管理系统。在本书中,作者向开发人员和量化分析人员介绍了使用Python程序库与工具,完成金融数据科学、算法交易和计算金融任务的方法。

Python与金融:Python交互式金融分析与程序开发入门。
基本知识:学习Python数据类型与结构、NumPy、pandas及其DataFrame类、面向对象编程。

金融数据科学:探索用于金融时间序列数据、I/O操作、推断统计学和机器学习的Python技术与程序库。
算法交易:使用Python来验证和部署自动算法交易策略。
衍生品分析:开发灵活、强大的Python期权、衍生品定价和风险管理程序库。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值