Python数据分析
Pandas是一个基于NumPy的分析结构化数据的工具集,NumPy为其提供了高性能的数据处理能力。Pandas被普遍用于数据挖掘和数据分析,同时也提供数据清洗、数据I/O、数据可视化等辅助功能。
Pandas简介
作为Python科学计算的基础软件包,NumPy已经足够强大,却不够完美,因为NumPy不支持异构列表格数据。异构列表格数据是指在一个二维数据结构中允许不同的列拥有不同的数据类型。 尽管NumPy支持任意维度的数据结构,但在实际工作中,无论是传统软件开发领域还是机器学习领域,使用的数据大多数都是二维异构列表格数据。Pandas正是为处理此类数据而生的,它为处理和SQL或Excel表类似的异构列表格数据提供了灵活、便捷的数据结构,从而迅速成为Python 的核心数据分析支持库。
Pandas是Python生态环境下非常重要的数据分析包,它是一个开源的、有BSD开源协议的库。正因为有它的存在,基于Python的数据分析才能大放异彩,为世人所瞩目。
Pandas吸纳了NumPy中的很多精华,然在数据分析方面“青出于蓝而胜于蓝”。二者最大的不同在于,Pandas在设计之初就是倾向于支持图表和混杂数据运算的,相比之下,NumPy显得“纯洁”很多,它是基于数组构建的,NumPy中的数组一旦被设置为某种数据类型(如整型或浮点型),就会从一而终,不得改变。
Pandas是基于NumPy构建的数据分析包,但它含有比ndarray更为高级的数据结构和操作工具,如Series类型、DataFrame类型等。有了这些高级数据的辅佐,使得通过Pandas进行数据分析变得更加便捷与高效。Pandas除了可以通过管理索引来快速访问数据、执行分析和转换运算,还可用于高效绘图,只需寥寥几行代码,一个栩栩如生的数据可视化图便可“扑面而来”(当然,它用了Matplotlib 作为后端支持)。
此外,Pandas还是数据读取“小能手”,支持从多种数据存储文件(如CSV、TXT、Excel、HDF5等)中读取数据,支持从数据库(如SQL)中读取数据,还支持从Web(如JSON、HTML等)中读取数据。
85后的韦斯·麦金尼(Wes McKinney)于2009年发布了Pandas的第一个版本,此后这个项目一直在缓慢地自我迭代。在2020年,Pandas已迈入1.0时代。
Pandas的使用便捷,离不开高效的底层数据结构的支持。Pandas主要有三种数据结构:Series(类似于一维数组)、DataFrame(类似于二维数组)和Panel(类似于三维数组)。由于Panel并不常用,因此,新版本的Pandas已经将其列为过时(Deprecated)的数据结构。
安装和使用
Pandas可以使用pip命令直接安装,安装命令如下。如果默认的模块安装源下载速度慢,可以使用-i参数选择下载速度更快的清华、阿里、中科大等镜像源。
pip install pandas
因为NumPy是Pandas的依赖包,如果当前系统没有安装NumPy,上面的安装命令还会自动安装最新版本的NumPy模块。类似导入NumPy模块时使用简写,导入Pandas模块时,pandas通常都会简写为pd,这几乎成为程序员们约定俗成的规则。
import pandas as pd
idx = ['2020','2019','2018']
colname = ['北京','广州','上海','杭州']
data = [[35200.00, 30500.00,31800.00,26300.00],
[35500.00,31300.00,32200.00,28100.00],
[34900.00,29600.00,30100.00,24700.00]]
df = pd.DataFrame(data, columns=colname, index=idx)
df
print(df)
# 北京 广州 上海 杭州
# 2020 35200.0 30500.0 31800.0 26300.0
# 2019 35500.0 31300.0 32200.0 28100.0
# 2018 34900.0 29600.0 30100.0 24700.0
上面的代码构建了一个带标签的二维数据表格。北京、广州、上海、杭州是每列数据的标签,所有列的标签称为列名;2020、2019、2018是每一行数据的标签,所有行的标签称为索引。 这个带标签的二维数据表格就是Pandas最核心的数据结构DataFrame,所有关于Pandas的操作和技巧几乎都是针对DataFrame这个结构的。
Pandas的特点
Pandas诞生于2008年,最初是专为金融、统计领域的数据处理者而非程序员量身定制的,其以符合使用者思维习惯的方式提供了几乎所有可能需要的功能。 Pandas追求的目标是尽可能屏蔽所有软件工程的概念,仅保留数据的物理属性和逻辑。例如:用户无须了解HTTP协议和HTML解析技术,只需一行代码即可实现网络数据的抓取和解析,其代码形式如下。
>>> data = pd.read_html('http://www.nssdc.ac.cn/')
>>> data[1]
0 1
0 2020年3月3日发布嫦娥四号首批科学数据 2020-03-03
1 HXMT首批观测数据 2019-04-10
2 2018年9月19日发布地磁Dst指数数据集 2018-09-20
3 2018年9月7日发布地磁Ap指数数据集 2018-09-20
4 2018年8月27日发布地磁Kp指数数据集 2018-09-20
但是,对用户过分的“迁就和溺爱”其实是一把双刃剑。正如Pandas之父Wes McKinney(韦斯·麦金尼)所说,Pandas正在背离他最初所期望的简洁和易用,变得越来越臃肿和不可控制。我非常认同Wes McKinney的观点,甚至觉得当Pandas“抛弃”了panel这个概念时,就已经“走火入魔”了。panel是Pandas最初为处理更高维数据提出的方案,非常接近HDF或netCDF的理念。后来Pandas使用“层次化索引”来处理更高维数据,导致其结构趋于复杂,使得程序员无法专注于事务逻辑的处理。
不过,瑕不掩瑜,虽然因此让人感到有点遗憾,但这也难掩Pandas的光芒。Pandas不只是简洁,还拥有出众的数据处理能力和完备的辅助功能。归纳起来,Pandas有以下5大特点。
- 具有极强的自适应能力。无论是Python还是NumPy的数据对象,即使是结构不规则的数据也可以轻松转换为DataFrame。Pandas还可以自动处理缺失数据,类似NumPy的掩码数组。
- NumPy为其提供了快速的数据组织和处理能力。Pandas支持任意增删数据列,支持合并、连接、重塑、透视数据集,支持聚合、转换、切片、花式索引、子集分解等操作。
- 完善的时间序列。Pandas支持日期范围生成、频率转换、移动窗口统计、移动窗口线性回归、日期位移等时间序列功能。
- 拥有全面的I/O工具。Pandas支持读取文本文件(CSV等支持分隔符的文件)、Excel文件、HDF文件、SQL表数据、json数据、html数据,甚至可以直接从url下载并解析数据,也可以将数据保存为CSV文件或Excel文件。
- 对用户友好的显示格式。不管数据复杂程度如何,Pandas展现出的数据结构总是最清晰的,它支持自动对齐对象和标签,必要时也可以忽略标签。
Pandas的数据结构
学习Pandas的最佳方式是从了解它的数据结构开始。有些人说,Pandas很简单,只有Series和DataFrame两种数据结构。 但是不管是Series还是DataFrame,它们都有一个索引Index,Index也是Pandas基本的数据结构之一。
索引Index
索引类似一维数组,在Pandas的其他数据结构中作为标签使用。虽然不了解索引的信息也可以使用Pandas,但要想精通Pandas,深刻理解索引(如层次化索引MultiIndex)是非常有必要的。
>>> pd.Index([3,4,5])
Int64Index([3, 4, 5], dtype='int64')
>>> pd.Index(['x','y','z'])
Index(['x', 'y', 'z'], dtype='object')
>>> pd.Index(range(3))
RangeIndex(start=0, stop=3, step=1)
>>> idx = pd.Index(['x','y','z'])
使用数组、列表、迭代器等都可以创建索引。索引看起来像一维数组,但无法修改元素的值。这一点非常重要,只有这样才能保证多个数据结构之间的安全共享。
实际上,索引有很多种类型,除了一维索引数据组,还有时间纳秒级戳索引、层次化索引等。此外,索引也有删除、插入、连接、交集、并集等操作。
带标签的一维同构数组Series
Series是由一组同一类型的数据和一组与数据对应的标签(Index)组成的数据结构,数据对应的标签又称为索引,索引是允许重复的。Pandas提供了多种生成Series的方式。
以下代码使用整型列表和字符串列表创建了两个Series。因为没有指定索引,Series生成器自动添加了默认索引。默认索引是从0开始的整型序列。
>>> pd.Series([0,1,2]) # 用列表生成Series,使用默认索引
0 0
1 1
2 2
dtype: int64
>>> pd.Series(['a','b','c']) # 用列表生成Series,使用默认索引
0 a
1 b
2 c
dtype: object
创建Series时,也可以同时指定索引。不过,索引长度一定要和列表长度相等,否则会抛出异常。另外,Series生成器也接受迭代对象作为参数。
>>> pd.Series([0,1,2], index=['a','b','c']) # 用列表生成Series
a 0
b 1
c 2
dtype: int64
>>> pd.Series(range(3), index=list('abc')) # 用迭代对象生成Series
a 0
b 1
c 2
dtype: int64
使用字典创建Series时,如果没有指定索引,则使用字典的键作为索引;如果指定了索引,则不要求和字典的键匹配。
>>> pd.Series({'a':1,'b':2,'c':3}) # 用字典生成Series,使用字典的键做索引
a 1
b 2
c 3
dtype: int64
>>> pd.Series({'a':1,'b':2,'c':3}, index=list('abxy')) # 指定索引
a 1.0
b 2.0
x NaN
y NaN
dtype: float64
Series有很多属性和方法,其中大部分和NumPy类似,甚至完全一致。这些属性和方法会在后面的应用中被用到,初学者现阶段了解下面这三个属性即可。
>>> s = pd.Series({'a':1,'b':2,'c':3})
>>> s.dtype # Series的数据类型是最重要的三个属性之一
dtype('int64')
>>> s.values # Series的数组是最重要的三个属性之二
array([1, 2, 3], dtype=int64)
>>> s.index # Series的索引是最重要的三个属性之三
Index(['a', 'b', 'c'], dtype='object')
深刻理解Series需要牢记两点:第一,Series的所有数据都是同一种数据类型,也就是一个Series一定有一个数据类型;第二,Series的每一个数据对应一个索引,但索引是允许重复的。
带标签的二维异构表格DataFrame
DataFrame可以看作由多个Series组成的二维表格型数据结构。每一个Series作为DataFrame的一列,每一列都有一个列名,都可以拥有独立的数据类型,所有的Series共用一个索引。列名称为DataFrame的列标签,索引称为DataFrame的行标签。
需要说明一点,DataFrame虽然是二维结构的,但并不意味着它不能处理更高维度的数据。事实上,依赖层次化索引,DataFrame可以轻松处理高维度数据。
创建DataFrame的方法有多种。例如,二维NumPy数组或掩码数组,由数组、列表、元组、字典、Series等组成的字典或列表等,甚至是DataFrame,都可以转换为DataFrame。对于结构不规则的数据,也可以轻松转换,因为DataFrame生成器拥有极强的容错能力。
将字典转换为DataFrame是最常见的创建方法,字典的键对应DataFrame的列,键名自动称为列名。如果没有指定索引,则使用默认索引。
>>> data = {
'华东科技': [1.91, 1.90, 1.86, 1.84],
'长安汽车': [11.27, 11.14, 11.28, 11.71],
'紫金矿业': [7.89, 7.79, 7.61, 7.50],
'重庆啤酒': [50.46, 50.17, 50.28, 50.28]
}
>>> pd.DataFrame(data)
华东科技 长安汽车 紫金矿业 重庆啤酒
0 1.91 11.27 7.89 50.46
1 1.90 11.14 7.79 50.17
2 1.86 11.28 7.61 50.28
3 1.84 11.71 7.50 50.28
创建DataFrame时可以指定索引。这里直接使用日期字符串做索引,正确的做法是使用日期索引对象,其代码如下。
>>> idx = ['2020-03-10','2020-03-11','2020-03-12','2020-03-13']
>>> pd.DataFrame(data, index=idx)
华东科技 长安汽车 紫金矿业 重庆啤酒
2020-03-10 1.91 11.27 7.89 50.46
2020-03-11 1.90 11.14 7.79 50.17
2020-03-12 1.86 11.28 7.61 50.28
2020-03-13 1.84 11.71 7.50 50.28
在创建DataFrame时,即使数据以字典形式提供,也可以指定列标签,DataFrame生成器不要求列标签和字典的键全部匹配。对于不存在的键,DataFrame生成器会自动填补NaN值。
>>> data = {
'华东科技': [1.91, 1.90, 1.86, 1.84],
'长安汽车': [11.27, 11.14, 11.28, 11.71],
'紫金矿业': [7.89, 7.79, 7.61, 7.50],
'重庆啤酒': [50.46, 50.17, 50.28, 50.28]
}
>>> idx = ['2020-03-10','2020-03-11','2020-03-12','2020-03-13']
>>> colnames = ['华东科技', '长安汽车', '杭钢股份', '紫金矿业', '重庆啤酒']
>>> pd.DataFrame(data, columns=colnames, index=idx)
华东科技 长安汽车 杭钢股份 紫金矿业 重庆啤酒
2020-03-10 1.91 11.27 NaN 7.89 50.46
2020-03-11 1.90 11.14 NaN 7.79 50.17
2020-03-12 1.86 11.28 NaN 7.61 50.28
2020-03-13 1.84 11.71 NaN 7.50 50.28
二维数组或列表也可以直接转换成DataFrame,同时可以指定索引和列标签。如果没有指定索引或列标签,DataFrame生成器则会自动添加从0开始的索引对象作为索引或列标签。
>>> data = np.array([
[ 1.91, 11.27, 7.89, 50.46],
[ 1.9 , 11.14, 7.79, 50.17],
[ 1.86, 11.28, 7.61, 50.28],
[ 1.84, 11.71, 7.5 , 50.28]
])
>>> idx = ['2020-03-10','2020-03-11','2020-03-12','2020-03-13']
>>> colnames = ['华东科技', '长安汽车', '紫金矿业', '重庆啤酒']
>>> pd.DataFrame(data, columns=colnames, index=idx)
华东科技 长安汽车 紫金矿业 重庆啤酒
2020-03-10 1.91 11.27 7.89 50.46
2020-03-11 1.90 11.14 7.79 50.17
2020-03-12 1.86 11.28 7.61 50.28
2020-03-13 1.84 11.71 7.50 50.28
DataFrame可以看作多个Series的集合,每个Series都可以拥有各自独立的数据类型,因此,DataFrame没有自身唯一的数据类型,自然也就没有dtype属性了。不过,DataFrame多了一个dtypes属性,这个属性的类型是Series类。除了dtypes属性,DataFrame的values属性、index属性、columns属性也都非常重要,需要牢记在心。
>>> df = pd.DataFrame(data, columns=colnames, index=idx)
>>> df.dtypes # dtypes属性,是由所有列的数据类型组成的Series
华东科技 float64
长安汽车 float64
紫金矿业 float64
重庆啤酒 float64
dtype: object
>>> df.values # DataFrame的重要属性之一
array([[ 1.91, 11.27, 7.89, 50.46],
[ 1.9 , 11.14, 7.79, 50.17],
[ 1.86, 11.28, 7.61, 50.28],
[ 1.84, 11.71, 7.5 , 50.28]])
>>> df.index # DataFrame的重要属性之一
DatetimeIndex(['2020-03-10', '2020-03-11', '2020-03-12', '2020-03-13'],
dtype='datetime64[ns]', freq=None)
>>> df.columns # DataFrame的重要属性之一
Index(['华东科技', '长安汽车', '紫金矿业', '重庆啤酒'], dtype='object')
基本操作
DataFrame几乎被打造成了一个“全能小怪兽”:它有字典的影子,有NumPy数组的性能,甚至继承了NumPy数组的很多属性和方法;它可以在一个结构内存储和处理多种不同类型的数据; 它看起来像是二维的结构,却能处理更高维度的数据;它可以处理包括日期时间在内的任意类型的数据,它能读写几乎所有的数据格式;它提供了数不胜数的方法,派生出无穷的操作技巧。想在较短的篇幅内详尽讲解DataFrame的操作是不现实的,本节只对DataFrame最基本、最核心的操作进行讲解。
为了便于演示,这里构造一个多只股票在同一天的开盘价、收盘价、成交量等信息的DataFrame,并以stack命名,其代码如下。
>>> data = np.array([
[10.70, 11.95, 10.56, 11.71, 789.10, 68771048],
[7.28, 7.59, 7.17, 7.50, 57.01, 7741802],
[48.10, 50.59, 48.10, 50.28, 223.06, 4496598],
[66.70, 69.28, 66.66, 68.92, 1196.14, 17662768],
[7.00, 7.35, 6.93, 7.11, 783.15, 109975919],
[2.02, 2.10, 2.01, 2.08, 56.32, 27484360]
])
>>> colnames = ['开盘价','最高价','最低价','收盘价','成交额','成交量']
>>> idx = ['000625.SZ','000762.SZ','600132.SH','600009.SH','600126.SH','000882.SZ']
>>> stock = pd.DataFrame(data, columns=colnames, index=idx)
数据预览
显示开始的5行和最后的5行
>>> stock.head() # 开始的5行
开盘价 最高价 最低价 收盘价 成交额 成交量
000625.SZ 10.70 11.95 10.56 11.71 789.10 68771048.0
000762.SZ 7.28 7.59 7.17 7.50 57.01 7741802.0
600132.SH 48.10 50.59 48.10 50.28 223.06 4496598.0
600009.SH 66.70 69.28 66.66 68.92 1196.14 17662768.0
600126.SH 7.00 7.35 6.93 7.11 783.15 109975919.0
>>> stock.tail() # 最后的5行
开盘价 最高价 最低价 收盘价 成交额 成交量
000762.SZ 7.28 7.59 7.17 7.50 57.01 7741802.0
600132.SH 48.10 50.59 48.10 50.28 223.06 4496598.0
600009.SH 66.70 69.28 66.66 68.92 1196.14 17662768.0
600126.SH 7.00 7.35 6.93 7.11 783.15 109975919.0
000882.SZ 2.02 2.10 2.01 2.08 56.32 27484360.0
查看均值、方差、极值等统计量
>>> stock.describe()
开盘价 最高价 最低价 收盘价 成交额 成交量
count 6.000000 6.000000 6.000000 6.00000 6.000000 6.000000e+00
mean 23.633333 24.810000 23.571667 24.60000 517.463333 3.935542e+07
std 26.951297 28.016756 26.975590 27.91178 472.508554 4.166194e+07
min 2.020000 2.100000 2.010000 2.08000 56.320000 4.496598e+06
25% 7.070000 7.410000 6.990000 7.20750 98.522500 1.022204e+07
50% 8.990000 9.770000 8.865000 9.60500 503.105000 2.257356e+07
75% 38.750000 40.930000 38.715000 40.63750 787.612500 5.844938e+07
max 66.700000 69.280000 66.660000 68.92000 1196.140000 1.099759e+08
转置
>>> stock.T
000625.SZ 000762.SZ ... 600126.SH 000882.SZ
开盘价 10.70 7.28 ... 7.000000e+00 2.02
最高价 11.95 7.59 ... 7.350000e+00 2.10
最低价 10.56 7.17 ... 6.930000e+00 2.01
收盘价 11.71 7.50 ... 7.110000e+00 2.08
成交额 789.10 57.01 ... 7.831500e+02 56.32
成交量 68771048.00 7741802.00 ... 1.099759e+08 27484360.00
[6 rows x 6 columns]
排序
>>> stock.sort_index(axis=0) # 按照索引排序
开盘价 最高价 最低价 收盘价 成交额 成交量
000625.SZ 10.70 11.95 10.56 11.71 789.10 68771048.0
000762.SZ 7.28 7.59 7.17 7.50 57.01 7741802.0
000882.SZ 2.02 2.10 2.01 2.08 56.32 27484360.0
600009.SH 66.70 69.28 66.66 68.92 1196.14 17662768.0
600126.SH 7.00 7.35 6.93 7.11 783.15 109975919.0
600132.SH 48.10 50.59 48.10 50.28 223.06 4496598.0
>>> stock.sort_index(axis=1) # 按照列标签排序
开盘价 成交量 成交额 收盘价 最低价 最高价
000625.SZ 10.70 68771048.0 789.10 11.71 10.56 11.95
000762.SZ 7.28 7741802.0 57.01 7.50 7.17 7.59
600132.SH 48.10 4496598.0 223.06 50.28 48.10 50.59
600009.SH 66.70 17662768.0 1196.14 68.92 66.66 69.28
600126.SH 7.00 109975919.0 783.15 7.11 6.93 7.35
000882.SZ 2.02 27484360.0 56.32 2.08 2.01 2.10
>>> stock.sort_values(by='成交量') # 按照指定列的数值排序
开盘价 最高价 最低价 收盘价 成交额 成交量
600132.SH 48.10 50.59 48.10 50.28 223.06 4496598.0
000762.SZ 7.28 7.59 7.17 7.50 57.01 7741802.0
600009.SH 66.70 69.28 66.66 68.92 1196.14 17662768.0
000882.SZ 2.02 2.10 2.01 2.08 56.32 27484360.0
000625.SZ 10.70 11.95 10.56 11.71 789.10 68771048.0
600126.SH 7.00 7.35 6.93 7.11 783.15 109975919.0
数据选择
行选择
DataFrame支持类似数组或列表的切片操作,如stock[2:3]
,但不能像stock[2]
这样直接索引。
>>> stock[2:3] # 切片
开盘价 最高价 最低价 收盘价 成交额 成交量
600132.SH 48.1 50.59 48.1 50.28 223.06 4496598.0
>>> stock[::2] # 步长为2的切片
开盘价 最高价 最低价 收盘价 成交额 成交量
000625.SZ 10.7 11.95 10.56 11.71 789.10 68771048.0
600132.SH 48.1 50.59 48.10 50.28 223.06 4496598.0
600126.SH 7.0 7.35 6.93 7.11 783.15 109975919.0
还可以对行标签(索引)切片。切片顺序基于DataFrame的Index,返回结果包含指定切片的两个索引项,类似数学上的闭区间。
>>> stock['000762.SZ':'600009.SH']
开盘价 最高价 最低价 收盘价 成交额 成交量
000762.SZ 7.28 7.59 7.17 7.50 57.01 7741802.0
600132.SH 48.10 50.59 48.10 50.28 223.06 4496598.0
600009.SH 66.70 69.28 66.66 68.92 1196.14 17662768.0
列选择 DataFrame仅允许选择单列数据返回Series。如果要选择多列,必须同时指定选择的行。
>>> stock['开盘价'] # 选择单列,也可以使用stock.开盘价
000625.SZ 10.70
000762.SZ 7.28
600132.SH 48.10
600009.SH 66.70
600126.SH 7.00
000882.SZ 2.02
Name: 开盘价, dtype: float64
行列选择
使用行列选择器loc可以同时选择行和列。行选择使用切片,列选择使用列表。
>>> stock.loc['000762.SZ':'600009.SH', ['开盘价', '收盘价', '成交量']]
开盘价 收盘价 成交量
000762.SZ 7.28 7.50 7741802.0
600132.SH 48.10 50.28 4496598.0
600009.SH 66.70 68.92 17662768.0
如果想和访问二维数组一样访问DataFrame,可以使用at、iat或iloc等行列选择器。
>>> stock.at['000762.SZ', '开盘价']
7.28
>>> stock.iat[1, 0]
7.28
>>> stock.iloc[1:4, 0:3]
开盘价 最高价 最低价
000762.SZ 7.28 7.59 7.17
600132.SH 48.10 50.59 48.10
600009.SH 66.70 69.28 66.66
条件选择 如果熟悉NumPy,就可以很容易理解DataFrame的条件选择。
>>> stock[(stock['成交额']>500)&(stock['开盘价']>10)] # 支持复合条件
开盘价 最高价 最低价 收盘价 成交额 成交量
000625.SZ 10.7 11.95 10.56 11.71 789.10 68771048.0
600009.SH 66.7 69.28 66.66 68.92 1196.14 17662768.0
>>> stock[stock['成交额'].isin([56.32,57.01,223.06])] # 使用isin()筛选多个特定值
开盘价 最高价 最低价 收盘价 成交额 成交量
000762.SZ 7.28 7.59 7.17 7.50 57.01 7741802.0
600132.SH 48.10 50.59 48.10 50.28 223.06 4496598.0
000882.SZ 2.02 2.10 2.01 2.08 56.32 27484360.0
改变数据结构
重新索引
DataFrame的reindex( )函数可以重新定义行标签或列标签,并返回一个新的对象,原有的数据结构不会被改变。重新索引既可以删除已有的行或列,也可以增加新的行或列。如果不指定填充值,新增的行或列的值默认为NaN。
>>> stock.reindex(index=idx, columns=colnames)
开盘价 收盘价 成交额 成交量 涨跌幅
000762.SZ 7.28 7.50 57.01 7741802.0 NaN
000625.SZ 10.70 11.71 789.10 68771048.0 NaN
600132.SH 48.10 50.28 223.06 4496598.0 NaN
000955.SZ NaN NaN NaN NaN NaN
>>> stock.reindex(index=idx, columns=colnames, fill_value=0)
开盘价 收盘价 成交额 成交量 涨跌幅
000762.SZ 7.28 7.50 57.01 7741802.0 0.0
000625.SZ 10.70 11.71 789.10 68771048.0 0.0
600132.SH 48.10 50.28 223.06 4496598.0 0.0
000955.SZ 0.00 0.00 0.00 0.0 0.0
删除行或列
DataFrame的drop( )函数可以删除指定轴的指定项,返回一个新的对象,原有的数据结构不会被改变。
>>> stock.drop(['000762.SZ', '600132.SH'], axis=0) # 删除指定行
开盘价 最高价 最低价 收盘价 成交额 成交量
000625.SZ 10.70 11.95 10.56 11.71 789.10 68771048.0
600009.SH 66.70 69.28 66.66 68.92 1196.14 17662768.0
600126.SH 7.00 7.35 6.93 7.11 783.15 109975919.0
000882.SZ 2.02 2.10 2.01 2.08 56.32 27484360.0
>>> stock.drop(['成交额', '最高价', '最低价'], axis=1) # 删除指定列
开盘价 收盘价 成交量
000625.SZ 10.70 11.71 68771048.0
000762.SZ 7.28 7.50 7741802.0
600132.SH 48.10 50.28 4496598.0
600009.SH 66.70 68.92 17662768.0
600126.SH 7.00 7.11 109975919.0
000882.SZ 2.02 2.08 27484360.0
- 行扩展
DataFrame的append( )函数可以在对象的尾部追加另一个DataFrame,实现行扩展。行扩展并不强制要求两个DataFrame列标签匹配。行扩展会返回一个新的数据结构,其列标签是两个DataFrame列标签的并集。行扩展不会改变原有的数据结构。
>>> idx = ['600161.SH', '600169.SH']
>>> colnames = ['开盘价', '收盘价', '成交额', '成交量', '涨跌幅']
>>> data = np.array([
[31.00, 32.16, 284.02, 8932594, 0.03],
[2.02, 2.13, 115.87, 54146894, 0.05]
])
>>> s = pd.DataFrame(data, columns=colnames, index=idx)
>>> stock.append(s)
开盘价 最高价 最低价 收盘价 成交额 成交量 涨跌幅
000625.SZ 10.70 11.95 10.56 11.71 789.10 68771048.0 NaN
000762.SZ 7.28 7.59 7.17 7.50 57.01 7741802.0 NaN
600132.SH 48.10 50.59 48.10 50.28 223.06 4496598.0 NaN
600009.SH 66.70 69.28 66.66 68.92 1196.14 17662768.0 NaN
600126.SH 7.00 7.35 6.93 7.11 783.15 109975919.0 NaN
000882.SZ 2.02 2.10 2.01 2.08 56.32 27484360.0 NaN
600161.SH 31.00 NaN NaN 32.16 284.02 8932594.0 0.03
600169.SH 2.02 NaN NaN 2.13 115.87 54146894.0 0.05
Pandas命名空间下的concat( )函数也可以实现多个DataFrame的垂直连接,用起来比append( )函数更方便。
>>> pd.concat((stock, s))
开盘价 最高价 最低价 收盘价 成交额 成交量 涨跌幅
000625.SZ 10.70 11.95 10.56 11.71 789.10 68771048.0 NaN
000762.SZ 7.28 7.59 7.17 7.50 57.01 7741802.0 NaN
600132.SH 48.10 50.59 48.10 50.28 223.06 4496598.0 NaN
600009.SH 66.70 69.28 66.66 68.92 1196.14 17662768.0 NaN
600126.SH 7.00 7.35 6.93 7.11 783.15 109975919.0 NaN
000882.SZ 2.02 2.10 2.01 2.08 56.32 27484360.0 NaN
600161.SH 31.00 NaN NaN 32.16 284.02 8932594.0 0.03
600169.SH 2.02 NaN NaN 2.13 115.87 54146894.0 0.05
- 列扩展
直接对新列赋值即可实现列扩展。赋值时,数据长度必须和DataFrame的长度匹配。这里需要特别说明一点,其他改变数据结构的操作都是返回新的数据结构,原有的数据结构不会被改变,而赋值操作会改变原有的数据结构。
>>> stock['涨跌幅'] = [0.02, 0.03, 0.05, 0.01, 0.02, 0.03]
>>> stock
开盘价 最高价 最低价 收盘价 成交额 成交量 涨跌幅
000625.SZ 10.70 11.95 10.56 11.71 789.10 68771048.0 0.02
000762.SZ 7.28 7.59 7.17 7.50 57.01 7741802.0 0.03
600132.SH 48.10 50.59 48.10 50.28 223.06 4496598.0 0.05
600009.SH 66.70 69.28 66.66 68.92 1196.14 17662768.0 0.01
600126.SH 7.00 7.35 6.93 7.11 783.15 109975919.0 0.02
000882.SZ 2.02 2.10 2.01 2.08 56.32 27484360.0 0.03
改变数据类型
类似NumPy数组,Series提供了astype( )函数来改变数据类型。不过astype( )函数只是返回了一个新的Series,并没有真正改变原有的Series。DataFrame没有提供改变某一列数据类型的方法,如果想要这样做,则需要对这一列重新赋值。
>>> stock['涨跌幅'].dtype
dtype('float64')
>>> stock['涨跌幅'] = stock['涨跌幅'].astype('float32').values
>>> stock['涨跌幅'].dtype
dtype('float32')
广播与矢量化运算
Pandas是基于NumPy数组的扩展,继承了NumPy数组的广播和矢量化特性。不管是Series内部,还是Series之间,甚至是DataFrame之间,所有的运算都支持广播和矢量化。此外,NumPy数组的数学和统计函数几乎都可以应用在Pandas的数据结构上。
>>> stock
开盘价 最高价 最低价 收盘价 成交额 成交量
000625.SZ 10.70 11.95 10.56 11.71 789.10 34385524.0
000762.SZ 7.28 7.59 7.17 7.50 57.01 3870901.0
600132.SH 48.10 50.59 48.10 50.28 223.06 2248299.0
600009.SH 66.70 69.28 66.66 68.92 1196.14 8831384.0
600126.SH 7.00 7.35 6.93 7.11 783.15 54987959.5
000882.SZ 2.02 2.10 2.01 2.08 56.32 13742180.0
>>> stock['成交量'] /= 2 # 成交量减半
>>> stock
开盘价 最高价 最低价 收盘价 成交额 成交量
000625.SZ 10.70 11.95 10.56 11.71 789.10 17192762.00
000762.SZ 7.28 7.59 7.17 7.50 57.01 1935450.50
600132.SH 48.10 50.59 48.10 50.28 223.06 1124149.50
600009.SH 66.70 69.28 66.66 68.92 1196.14 4415692.00
600126.SH 7.00 7.35 6.93 7.11 783.15 27493979.75
000882.SZ 2.02 2.10 2.01 2.08 56.32 6871090.00
>>> stock['最高价'] += stock['最低价'] # 最高价加上最低价
>>> stock
开盘价 最高价 最低价 收盘价 成交额 成交量
000625.SZ 10.70 22.51 10.56 11.71 789.10 17192762.00
000762.SZ 7.28 14.76 7.17 7.50 57.01 1935450.50
600132.SH 48.10 98.69 48.10 50.28 223.06 1124149.50
600009.SH 66.70 135.94 66.66 68.92 1196.14 4415692.00
600126.SH 7.00 14.28 6.93 7.11 783.15 27493979.75
000882.SZ 2.02 4.11 2.01 2.08 56.32 6871090.00
# 开盘价标准化:去中心化(减开盘价均值),再除以开盘价的标准差
>>> stock['开盘价'] = (stock['开盘价']-stock['开盘价'].mean())/stock['开盘价'].std()
>>> stock
开盘价 最高价 最低价 收盘价 成交额 成交量
000625.SZ -0.479878 22.51 10.56 11.71 789.10 17192762.00
000762.SZ -0.606774 14.76 7.17 7.50 57.01 1935450.50
600132.SH 0.907810 98.69 48.10 50.28 223.06 1124149.50
600009.SH 1.597944 135.94 66.66 68.92 1196.14 4415692.00
600126.SH -0.617163 14.28 6.93 7.11 783.15 27493979.75
000882.SZ -0.801940 4.11 2.01 2.08 56.32 6871090.00
对DataFrame进行广播运算同样是可行的,两个DataFrame之间也可以进行算术运算。两个DataFrame进行算术运算时,对应列标签的索引项之间做算术运算,无对应项的元素自动填充NaN值,其代码如下。
>>> df_a = pd.DataFrame(np.arange(6).reshape((2,3)), columns=list('abc'))
>>> df_b = pd.DataFrame(np.arange(6,12).reshape((3,2)), columns=list('ab'))
>>> df_a
a b c
0 0 1 2
1 3 4 5
>>> df_b
a b
0 6 7
1 8 9
2 10 11
>>> df_a + 1 # 对DataFrame的广播运算
a b c
0 1 2 3
1 4 5 6
>>> df_a + df_b # 两个DataFrame的矢量运算
a b c
0 6.0 8.0 NaN
1 11.0 13.0 NaN
2 NaN NaN NaN
6.3.6 行列级广播函数 NumPy数组的大部分数学函数和统计函数都是广播函数,可以被隐式地映射到数组的各个元素上。NumPy数组支持自定义广播函数。Pandas的apply( )函数类似于NumPy数组的自定义广播函数,可以将函数映射到DataFrame的特定行或列上,也就是以行或列的一维数组作为函数的输入参数,而不是以行或列的各个元素作为函数的输入参数。这是Pandas的apply( )函数有别于NumPy数组自定义广播函数的地方,其代码如下。
>>> stock
开盘价 最高价 最低价 收盘价 成交额 成交量
000625.SZ -0.479878 22.51 10.56 11.71 789.10 17192762.00
000762.SZ -0.606774 14.76 7.17 7.50 57.01 1935450.50
600132.SH 0.907810 98.69 48.10 50.28 223.06 1124149.50
600009.SH 1.597944 135.94 66.66 68.92 1196.14 4415692.00
600126.SH -0.617163 14.28 6.93 7.11 783.15 27493979.75
000882.SZ -0.801940 4.11 2.01 2.08 56.32 6871090.00
>>> f = lambda x:(x-x.min())/(x.max()-x.min()) # 定义归一化函数
>>> stock.apply(f, axis=0) # 0轴(行方向)归一化
开盘价 最高价 最低价 收盘价 成交额 成交量
000625.SZ 0.134199 0.139574 0.132251 0.144075 0.642891 0.609356
000762.SZ 0.081323 0.080786 0.079814 0.081089 0.000605 0.030766
600132.SH 0.712430 0.717439 0.712916 0.721125 0.146286 0.000000
600009.SH 1.000000 1.000000 1.000000 1.000000 1.000000 0.124822
600126.SH 0.076994 0.077145 0.076102 0.075254 0.637671 1.000000
000882.SZ 0.000000 0.000000 0.000000 0.000000 0.000000 0.217936
>>> stock.apply(f, axis=1) # 1轴(列方向)归一化
开盘价 最高价 最低价 收盘价 成交额 成交量
000625.SZ 0.0 1.337183e-06 6.421236e-07 7.090122e-07 0.000046 1.0
000762.SZ 0.0 7.939634e-06 4.018068e-06 4.188571e-06 0.000030 1.0
600132.SH 0.0 8.698333e-05 4.198038e-05 4.391963e-05 0.000198 1.0
600009.SH 0.0 3.042379e-05 1.473429e-05 1.524610e-05 0.000271 1.0
600126.SH 0.0 5.418336e-07 2.745024e-07 2.810493e-07 0.000029 1.0
000882.SZ 0.0 7.148705e-07 4.092422e-07 4.194298e-07 0.000008 1.0
高级应用
DataFrame作为数据分析的利器,其作用体现在两个方面:一是用来暂存形式复杂的数据,二是提供高效的处理手段。前一节偏重于讲解暂存数据方面,本节则偏重于讲解如何高效地处理数据。
分组
分组与聚合是数据处理中最常见的应用场景。例如,对多只股票多个交易日的成交量分析,需要按股票和交易日两种分类方式进行统计,其代码如下。
>>> data = {
'日期': ['2020-03-11','2020-03-11','2020-03-11','2020-03-11','2020-03-11',
'2020-03-12','2020-03-12','2020-03-12','2020-03-12','2020-03-12',
'2020-03-13','2020-03-13','2020-03-13','2020-03-13','2020-03-13'],
'代码': ['000625.SZ','000762.SZ','600132.SH','600009.SH','000882.SZ',
'000625.SZ','000762.SZ','600132.SH','600009.SH','000882.SZ',
'000625.SZ','000762.SZ','600132.SH','600009.SH','000882.SZ'],
'成交额': [422.08,73.65,207.04,510.59,63.28,
471.78,59.2,156.82,853.83,52.84,
789.1,57.01,223.06,1196.14,56.32],
'成交量': [37091400,9315300,4127800,7233100,28911100,
42471700,7724200,3143100,12350400,24828900,
68771048,7741802,4496598,17662768,27484360]
}
>>> vo = pd.DataFrame(data)
>>> vo
日期 代码 成交额 成交量
0 2020-03-11 000625.SZ 422.08 37091400
1 2020-03-11 000762.SZ 73.65 9315300
2 2020-03-11 600132.SH 207.04 4127800
3 2020-03-11 600009.SH 510.59 7233100
4 2020-03-11 000882.SZ 63.28 28911100
5 2020-03-12 000625.SZ 471.78 42471700
6 2020-03-12 000762.SZ 59.20 7724200
7 2020-03-12 600132.SH 156.82 3143100
8 2020-03-12 600009.SH 853.83 12350400
9 2020-03-12 000882.SZ 52.84 24828900
10 2020-03-13 000625.SZ 789.10 68771048
11 2020-03-13 000762.SZ 57.01 7741802
12 2020-03-13 600132.SH 223.06 4496598
13 2020-03-13 600009.SH 1196.14 17662768
14 2020-03-13 000882.SZ 56.32 27484360
使用groupby( )函数按照日期分组,返回的分组结果是一个迭代器,遍历这个迭代器,可以得到3个由组名(日期)和该组的DataFrame组成的元组,其代码如下。
>>> for name, df in vo.groupby('日期'):
print('组名:%s'%name)
print('-------------------------------------------')
print(df)
print()
组名:2020-03-11
-------------------------------------------
日期 代码 成交额 成交量
0 2020-03-11 000625.SZ 422.08 37091400
1 2020-03-11 000762.SZ 73.65 9315300
2 2020-03-11 600132.SH 207.04 4127800
3 2020-03-11 600009.SH 510.59 7233100
4 2020-03-11 000882.SZ 63.28 28911100
组名:2020-03-12
-------------------------------------------
日期 代码 成交额 成交量
5 2020-03-12 000625.SZ 471.78 42471700
6 2020-03-12 000762.SZ 59.20 7724200
7 2020-03-12 600132.SH 156.82 3143100
8 2020-03-12 600009.SH 853.83 12350400
9 2020-03-12 000882.SZ 52.84 24828900
组名:2020-03-13
-------------------------------------------
日期 代码 成交额 成交量
10 2020-03-13 000625.SZ 789.10 68771048
11 2020-03-13 000762.SZ 57.01 7741802
12 2020-03-13 600132.SH 223.06 4496598
13 2020-03-13 600009.SH 1196.14 17662768
14 2020-03-13 000882.SZ 56.32 27484360
使用groupby( )函数按照股票代码分组,返回的分组结果是一个迭代器,遍历这个迭代器,可以得到5个由组名(股票代码)和该组的DataFrame组成的元组,其代码如下。
>>> for name, df in vo.groupby('代码'):
print('组名:%s'%name)
print('-------------------------------------------')
print(df)
print()
组名:000625.SZ
-------------------------------------------
日期 代码 成交额 成交量
0 2020-03-11 000625.SZ 422.08 37091400
5 2020-03-12 000625.SZ 471.78 42471700
10 2020-03-13 000625.SZ 789.10 68771048
组名:000762.SZ
-------------------------------------------
日期 代码 成交额 成交量
1 2020-03-11 000762.SZ 73.65 9315300
6 2020-03-12 000762.SZ 59.20 7724200
11 2020-03-13 000762.SZ 57.01 7741802
组名:000882.SZ
-------------------------------------------
日期 代码 成交额 成交量
4 2020-03-11 000882.SZ 63.28 28911100
9 2020-03-12 000882.SZ 52.84 24828900
14 2020-03-13 000882.SZ 56.32 27484360
组名:600009.SH
-------------------------------------------
日期 代码 成交额 成交量
3 2020-03-11 600009.SH 510.59 7233100
8 2020-03-12 600009.SH 853.83 12350400
13 2020-03-13 600009.SH 1196.14 17662768
组名:600132.SH
-------------------------------------------
日期 代码 成交额 成交量
2 2020-03-11 600132.SH 207.04 4127800
7 2020-03-12 600132.SH 156.82 3143100
12 2020-03-13 600132.SH 223.06 4496598
聚合
理解了分组,接下来就可以根据分组进行数据处理。例如,统计所有股票每天的成交总额和成交总量等,其代码如下。
>>> vo.groupby('日期').sum() # 按日期统计全部股票的成交总额和成交总量
成交额 成交量
日期
2020-03-11 1276.64 86678700
2020-03-12 1594.47 90518300
2020-03-13 2321.63 126156576
>>> vo.groupby('代码').mean() # 统计单只股票多个交易日的平均成交额和平均成交量
成交额 成交量
代码
000625.SZ 560.986667 4.944472e+07
000762.SZ 63.286667 8.260434e+06
000882.SZ 57.480000 2.707479e+07
600009.SH 853.520000 1.241542e+07
600132.SH 195.640000 3.922499e+06
可以直接应用在分组结果上的函数包括计数(count)、求和(sum)、均值(mean)、中位数(median)、有效值的乘积(prod)、方差(var)和标准差(std)、最大值(max)和最小值(min)、第一个有效值(first)和最后一个有效值(last)等。
如果对分组结果实施自定义的函数,或对分组结果做更多的统计分析,此时就要用到聚合函数agg( ),其代码如下。
>>> def scope(x): # 返回最大值和最小值之差(波动幅度)的函数
return x.max()-x.min()
>>> vo.groupby('代码').agg(scope) # 统计每只股票成交额和成交量的波动幅度
成交额 成交量
代码
000625.SZ 367.02 31679648
000762.SZ 16.64 1591100
000882.SZ 10.44 4082200
600009.SH 685.55 10429668
600132.SH 66.24 1353498
>>> vo.groupby('代码').agg(['mean', scope]) # 统计成交额和成交量的均值和波动幅度
成交额 成交量
mean scope mean scope
代码
000625.SZ 560.986667 367.02 4.944472e+07 31679648
000762.SZ 63.286667 16.64 8.260434e+06 1591100
000882.SZ 57.480000 10.44 2.707479e+07 4082200
600009.SH 853.520000 685.55 1.241542e+07 10429668
600132.SH 195.640000 66.24 3.922499e+06 1353498
聚合函数agg( )还可以对不同的列实施不同的函数操作。例如,下面的代码对成交额实施均值操作,对成交量实施自定义的波动幅度函数。
>>> vo.groupby('代码').agg({'成交额':'mean', '成交量':scope})
成交额 成交量
代码
000625.SZ 560.986667 31679648
000762.SZ 63.286667 1591100
000882.SZ 57.480000 4082200
600009.SH 853.520000 10429668
600132.SH 195.640000 1353498
层次化索引
在分组和聚合的例子中,日期-股票代码-成交额和成交量这样的数据结构就是三维的了,依然可以用DataFrame暂存和处理。这样的数据结构尽管可以通过分组获得多个二维的DataFrame,但毕竟无法直接索引或选择。层次化索引可以很好地解决这个问题,为DataFrame处理更高维度的数据指明了方向。
这里依旧使用日期字符串做索引,正确的做法是使用日期索引,其代码如下。
>>> dt = ['2020-03-11', '2020-03-12','2020-03-13']
>>> sc = ['000625.SZ','000762.SZ','600132.SH','600009.SH','600126.SH']
>>> cn = ['成交额', '成交量']
>>> idx = pd.MultiIndex.from_product([dt, sc], names=['日期', '代码'])
>>> data = np.array([
[422.08, 37091400],
[73.65, 9315300],
[207.04, 4127800],
[510.59, 7233100],
[63.28, 28911100],
[471.78, 42471700],
[59.2, 7724200],
[156.82, 3143100],
[853.83, 12350400],
[52.84, 24828900],
[789.1, 68771048],
[57.01, 7741802],
[223.06, 4496598],
[1196.14, 17662768],
[56.32, 27484360]
])
>>> vom1 = pd.DataFrame(data, index=idx, columns=cn)
>>> vom1
成交额 成交量
日期 代码
2020-03-11 000625.SZ 422.08 37091400.0
000762.SZ 73.65 9315300.0
600132.SH 207.04 4127800.0
600009.SH 510.59 7233100.0
600126.SH 63.28 28911100.0
2020-03-12 000625.SZ 471.78 42471700.0
000762.SZ 59.20 7724200.0
600132.SH 156.82 3143100.0
600009.SH 853.83 12350400.0
600126.SH 52.84 24828900.0
2020-03-13 000625.SZ 789.10 68771048.0
000762.SZ 57.01 7741802.0
600132.SH 223.06 4496598.0
600009.SH 1196.14 17662768.0
600126.SH 56.32 27484360.0
层次化索引数据vom1有日期和代码两个索引项。层次化索引还有另外一种形式,即在行标签上使用层次化索引,其代码如下。
>>> dt = ['2020-03-11', '2020-03-12','2020-03-13']
>>> sc = ['000625.SZ','000762.SZ','600132.SH','600009.SH','000882.SZ']
>>> cn = ['成交额', '成交量']
>>> cols = pd.MultiIndex.from_product([dt, cn], names=['日期', '数据'])
>>> data = np.array([
[422.08, 37091400, 471.78, 42471700, 789.1, 68771048],
[73.65, 9315300, 59.2, 7724200, 57.01, 7741802],
[207.04, 4127800, 156.82, 3143100, 223.06, 4496598],
[510.59, 7233100, 853.83, 12350400, 1196.14, 17662768],
[63.28, 28911100, 52.84, 24828900, 56.32, 27484360]
])
>>> vom2 = pd.DataFrame(data, index=sc, columns=cols)
>>> vom2
日期 2020-03-11 2020-03-12 2020-03-13
数据 成交额 成交量 成交额 成交量 成交额 成交量
000625.SZ 422.08 37091400.0 471.78 42471700.0 789.10 68771048.0
000762.SZ 73.65 9315300.0 59.20 7724200.0 57.01 7741802.0
600132.SH 207.04 4127800.0 156.82 3143100.0 223.06 4496598.0
600009.SH 510.59 7233100.0 853.83 12350400.0 1196.14 17662768.0
000882.SZ 63.28 28911100.0 52.84 24828900.0 56.32 27484360.0
层次化索引数据的索引和选择类似普通的DataFrame对象。
>>> vom1.loc['2020-03-11']
成交额 成交量
second
000625.SZ 422.08 37091400.0
000762.SZ 73.65 9315300.0
600132.SH 207.04 4127800.0
600009.SH 510.59 7233100.0
000882.SZ 63.28 28911100.0
>>> vom1.loc['2020-03-11', '000625.SZ']
成交额 422.08
成交量 37091400.00
Name: (2020-03-11, 000625.SZ), dtype: float64
>>> vom1.loc['2020-03-11', '000625.SZ']['成交量']
37091400.0
>>> vom2['2020-03-11']
数据 成交额 成交量
000625.SZ 422.08 37091400.0
000762.SZ 73.65 9315300.0
600132.SH 207.04 4127800.0
600009.SH 510.59 7233100.0
000882.SZ 63.28 28911100.0
>>> vom2['2020-03-11', '成交额']
000625.SZ 422.08
000762.SZ 73.65
600132.SH 207.04
600009.SH 510.59
000882.SZ 63.28
Name: (2020-03-11, 成交额), dtype: float64
>>> vom2.loc['000625.SZ']
日期 数据
2020-03-11 成交额 422.08
成交量 37091400.00
2020-03-12 成交额 471.78
成交量 42471700.00
2020-03-13 成交额 789.10
成交量 68771048.00
Name: 000625.SZ, dtype: float64
>>> vom2.loc['000625.SZ'][:,'成交额']
日期
2020-03-11 422.08
2020-03-12 471.78
2020-03-13 789.10
Name: 000625.SZ, dtype: float64
表级广播函数
行列级广播函数apply( )可以把一个计算函数映射到DataFrame的行或列上,并以行或列的一维数组作为计算函数的输入参数。与apply( )函数类似,表级广播函数pipe( )可以把一个计算函数映射到DataFrame的每一个元素上,并以每一个元素作为计算函数的第一个输入参数,其代码如下。
>>> def scale(x, k): # 对x进行缩放,缩放系数为k
return x*k
>>> vom1.pipe(scale, 0.2) # 对vom1所有数据执行缩放函数,缩放系数为0.2
成交额 成交量
first second
2020-03-11 000625.SZ 84.416 7418280.0
000762.SZ 14.730 1863060.0
600132.SH 41.408 825560.0
600009.SH 102.118 1446620.0
000882.SZ 12.656 5782220.0
2020-03-12 000625.SZ 94.356 8494340.0
000762.SZ 11.840 1544840.0
600132.SH 31.364 628620.0
600009.SH 170.766 2470080.0
000882.SZ 10.568 4965780.0
2020-03-13 000625.SZ 157.820 13754209.6
000762.SZ 11.402 1548360.4
600132.SH 44.612 899319.6
600009.SH 239.228 3532553.6
000882.SZ 11.264 5496872.0
pipe( )函数将DataFrame作为首个参数,这为链式调用提供了可能性。链式调用以其代码的简洁和易读特性,受到了很多程序员的追捧,其代码如下。
>>> def adder(x, dx):
return x+dx
>>> vom1.pipe(scale, 0.2).pipe(adder, 5) # 链式调用
成交额 成交量
first second
2020-03-11 000625.SZ 89.416 7418285.0
000762.SZ 19.730 1863065.0
600132.SH 46.408 825565.0
600009.SH 107.118 1446625.0
000882.SZ 17.656 5782225.0
2020-03-12 000625.SZ 99.356 8494345.0
000762.SZ 16.840 1544845.0
600132.SH 36.364 628625.0
600009.SH 175.766 2470085.0
000882.SZ 15.568 4965785.0
2020-03-13 000625.SZ 162.820 13754214.6
000762.SZ 16.402 1548365.4
600132.SH 49.612 899324.6
600009.SH 244.228 3532558.6
000882.SZ 16.264 5496877.0
日期时间索引
Pandas对于日期时间类型的数据也有很好的支持,提供了很多非常实用的函数,可以非常方便地生成、转换日期时间索引。DatetimeIndex类是索引数组的一种,也是常用的日期时间序列生成和转换工具,既可以由日期时间字符串列表直接生成日期时间索引,也可以将字符串类型的索引、Series转换成日期时间索引。
>>> pd.DatetimeIndex(['2020-03-10', '2020-03-11', '2020-03-12'])
pd.DatetimeIndex(pd.Index(['2020-03-10', '2020-03-11', '2020-03-12']))
>>> idx = pd.Index(['2020-03-10', '2020-03-11', '2020-03-12'])
>>> sdt = pd.Series(['2020-03-10', '2020-03-11', '2020-03-12'])
>>> idx
Index(['2020-03-10', '2020-03-11', '2020-03-12'], dtype='object')
>>> sdt
0 2020-03-10
1 2020-03-11
2 2020-03-12
dtype: object
>>> pd.DatetimeIndex(idx)
DatetimeIndex(['2020-03-10', '2020-03-11', '2020-03-12'],
dtype='datetime64[ns]', freq=None)
>>> pd.DatetimeIndex(sdt)
DatetimeIndex(['2020-03-10', '2020-03-11', '2020-03-12'],
dtype='datetime64[ns]', freq=None)
转换函数pd.to_datetime( )的功能类似DatetimeIndex类,可以将各种格式的日期时间字符串转换为日期时间索引。
>>> pd.to_datetime(['2020-03-10', '2020-03-11', '2020-03-12', '2020-03-13'])
DatetimeIndex(['2020-03-10', '2020-03-11', '2020-03-12', '2020-03-13'],
dtype='datetime64[ns]', freq=None)
>>> pd.to_datetime(idx)
DatetimeIndex(['2020-03-10', '2020-03-11', '2020-03-12'],
dtype='datetime64[ns]', freq=None)
>>> pd.to_datetime(sdt)
0 2020-03-10
1 2020-03-11
2 2020-03-12
dtype: datetime64[ns]
给定起止时间、序列长度或分割步长,date_range( )函数也可以快速创建日期时间索引。分割步长使用L、S、T、H、D、M分别表示毫秒、秒、分钟、小时、天、月等,还可以加上数字,如3H表示分割步长为3小时,其代码如下。
>>> pd.date_range(start='2020-05-12', end='2020-05-18')
DatetimeIndex(['2020-05-12', '2020-05-13', '2020-05-14', '2020-05-15',
'2020-05-16', '2020-05-17', '2020-05-18'],
dtype='datetime64[ns]', freq='D')
>>> pd.date_range(start='2020-05-12 08:00:00', periods=6, freq='3H')
DatetimeIndex(['2020-05-12 08:00:00', '2020-05-12 11:00:00',
'2020-05-12 14:00:00', '2020-05-12 17:00:00',
'2020-05-12 20:00:00', '2020-05-12 23:00:00'],
dtype='datetime64[ns]', freq='3H')
>>> pd.date_range(start='08:00:00', end='9:00:00', freq='15T')
DatetimeIndex(['2020-05-13 08:00:00', '2020-05-13 08:15:00',
'2020-05-13 08:30:00', '2020-05-13 08:45:00',
'2020-05-13 09:00:00'], dtype='datetime64[ns]', freq='15T')
数据可视化
Pandas的数据可视化是基于Matplotlib的一个封装,但封装得不够彻底,很多地方仍然离不开Matplotlib。例如,脱离ipython或jupyter的环境,必须要使用pyplot.show( )函数才能显示绘图结果,解决中文显示问题也必须要显式地导入matplotlib.pyplot包,除非手动修改Matplotlib的字体配置文件。因此,使用Pandas的数据可视化功能前,需要导入模块并设置默认字体。本节的所有示例,均假定已经执行了如下代码。
>>> import numpy as np
>>> import pandas as pd
>>> import matplotlib.pyplot as plt
>>> plt.rcParams['font.sans-serif'] = ['FangSong']
>>> plt.rcParams['axes.unicode_minus'] = False
Pandas的数据可视化API提供了绘制折线图、柱状图、箱线图、直方图、散点图、饼图等功能。对于Series和DataFrame的数据可视化,通常以x轴表示索引,以y轴表示数据。
>>> idx = pd.date_range(start='08:00:00',end='9:00:00',freq='T') # 间隔1分钟
>>> y = np.sin(np.linspace(0,2*np.pi,61)) # 0到2π之间的61个点的正弦值
>>> s = pd.Series(y, index=idx) # 创建Series,索引是时间序列
>>> s.plot() # 绘制折线图
<matplotlib.axes._subplots.AxesSubplot object at 0x0000029D4EC95C08>
>>> plt.show() # 显示绘图结果
上面的代码调用Series的plot( )函数绘制了一条正弦曲线,如图6-1所示。Series的索引是一个日期时间序列,从8时到9时,间隔1分钟。
对DataFrame的数据可视化也是以x轴表示索引,多列数据既可以绘制在画布的同一个子图上,也可以绘制在同一张画布的多个子图上,其代码如下。
>>> data = np.random.randn(10,4)
>>> idx = pd.date_range('08:00:00', periods=10, freq='H')
>>> df = pd.DataFrame(data, index=idx, columns=list('ABCD'))
>>> df.plot()
<matplotlib.axes._subplots.AxesSubplot object at 0x0000029D525FA548>
>>> plt.show()
DataFrame的plot( )函数同时绘制了4列数据并自动生成了图例,这显然比直接使用Matplotlib要简洁得多,如图6-2所示。
了解Matplotlib的概念和函数,才能使用Pandas的数据可视化API在同一张画布上绘制多个子图的柱状图,其代码如下。
>>> df = pd.DataFrame(np.random.rand(10,4),columns=list('ABCD'))
>>> fig = plt.figure( )
>>> ax = fig.add_subplot(131)
>>> df.plot.bar(ax=ax)
<matplotlib.axes._subplots.AxesSubplot object at 0x0000029D52A4B288>
>>> ax = fig.add_subplot(132)
>>> df.plot.bar(ax=ax, stacked=True)
<matplotlib.axes._subplots.AxesSubplot object at 0x0000029D56808308>
>>> ax = fig.add_subplot(133)
>>> df.plot.barh(ax=ax, stacked=True)
<matplotlib.axes._subplots.AxesSubplot object at 0x0000029D52606B08>
>>> plt.show()
普通柱状图、堆叠柱状图和水平的堆叠柱状图绘制在同一张画布上的效果。
Pandas的可视化绘图函数和Matplotlib基本相同,本节不再重复讲解。另外,关于标题、坐标轴名称、标注、网格、颜色、线型等绘图样式的修改,请参考本书的第5章。
数据I/O
Pandas作为数据处理的利器,数据的输入输出自然是必不可少的功能。Pandas既可以读取不同格式、不同来源的数据,也可以将数据保存成各种格式的数据文件。
- 读写CSV格式的数据文件
写CSV格式的数据文件时,索引会被写入首列(0列);读取数据时,如果没有指定首列(0列)为索引,则读取的数据被自动添加默认索引,其代码如下。
>>> df = pd.DataFrame(np.random.rand(10,4),columns=list('ABCD')) # 生成模拟数据
>>> df
A B C D
0 0.367409 0.542233 0.468111 0.732681
1 0.465060 0.172522 0.939913 0.654894
2 0.455698 0.487195 0.980735 0.752743
3 0.951230 0.940689 0.455013 0.682672
4 0.283269 0.421182 0.024713 0.245193
5 0.297696 0.981307 0.513994 0.698454
6 0.034707 0.688815 0.530870 0.921954
7 0.159914 0.185290 0.489379 0.299581
8 0.213631 0.950752 0.128683 0.499867
9 0.403379 0.269299 0.173059 0.939896
>>> df.to_csv('random.csv') # 保存为CSV格式的数据文件
>>> df = pd.read_csv('random.csv') # 读取CSV格式的数据文件
>>> df
Unnamed: 0 A B C D
0 0 0.367409 0.542233 0.468111 0.732681
1 1 0.465060 0.172522 0.939913 0.654894
2 2 0.455698 0.487195 0.980735 0.752743
3 3 0.951230 0.940689 0.455013 0.682672
4 4 0.283269 0.421182 0.024713 0.245193
5 5 0.297696 0.981307 0.513994 0.698454
6 6 0.034707 0.688815 0.530870 0.921954
7 7 0.159914 0.185290 0.489379 0.299581
8 8 0.213631 0.950752 0.128683 0.499867
9 9 0.403379 0.269299 0.173059 0.939896
读取数据时可以使用index_col参数指定首列(0列)为索引。
>>> df = pd.read_csv(r'D:\NumPyFamily\data\random.csv', index_col=0)
>>> df
A B C D
0 0.367409 0.542233 0.468111 0.732681
1 0.465060 0.172522 0.939913 0.654894
2 0.455698 0.487195 0.980735 0.752743
3 0.951230 0.940689 0.455013 0.682672
4 0.283269 0.421182 0.024713 0.245193
5 0.297696 0.981307 0.513994 0.698454
6 0.034707 0.688815 0.530870 0.921954
7 0.159914 0.185290 0.489379 0.299581
8 0.213631 0.950752 0.128683 0.499867
9 0.403379 0.269299 0.173059 0.939896
- 读写Excel文件
读写Excel文件时需要用sheet_name参数指定表名。写Excel文件时,索引会被写入首列(0列);读取数据时,如果没有指定首列(0列)为索引,则会自动添加默认索引,其代码如下。
>>> idx = pd.date_range('08:00:00', periods=10, freq='H')
>>> df = pd.DataFrame(np.random.rand(10,4),columns=list('ABCD'),index=idx)
>>> df
A B C D
2020-05-14 08:00:00 0.760846 0.926615 0.325205 0.525448
2020-05-14 09:00:00 0.845306 0.176587 0.764530 0.674024
2020-05-14 10:00:00 0.697167 0.861391 0.519662 0.443900
2020-05-14 11:00:00 0.461842 0.418028 0.844132 0.661985
2020-05-14 12:00:00 0.661543 0.619015 0.647476 0.473730
2020-05-14 13:00:00 0.941277 0.740208 0.249476 0.097356
2020-05-14 14:00:00 0.425394 0.639996 0.093368 0.904685
2020-05-14 15:00:00 0.886753 0.153370 0.820338 0.922392
2020-05-14 16:00:00 0.253917 0.068124 0.831815 0.703694
2020-05-14 17:00:00 0.999562 0.894684 0.395017 0.862102
>>> df.to_excel('random.xlsx', sheet_name='随机数')
>>> df = pd.read_excel('random.xlsx', sheet_name='随机数')
>>> df
Unnamed: 0 A B C D
0 2020-05-14 08:00:00 0.760846 0.926615 0.325205 0.525448
1 2020-05-14 09:00:00 0.845306 0.176587 0.764530 0.674024
2 2020-05-14 10:00:00 0.697167 0.861391 0.519662 0.443900
3 2020-05-14 11:00:00 0.461842 0.418028 0.844132 0.661985
4 2020-05-14 12:00:00 0.661543 0.619015 0.647476 0.473730
5 2020-05-14 13:00:00 0.941277 0.740208 0.249476 0.097356
6 2020-05-14 14:00:00 0.425394 0.639996 0.093368 0.904685
7 2020-05-14 15:00:00 0.886753 0.153370 0.820338 0.922392
8 2020-05-14 16:00:00 0.253917 0.068124 0.831815 0.703694
9 2020-05-14 17:00:00 0.999562 0.894684 0.395017 0.862102
读取数据时,可以使用index_col参数指定首列(0列)为索引。
>>> df = pd.read_excel('random.xlsx', sheet_name='随机数', index_col=0)
>>> df
A B C D
2020-05-14 08:00:00 0.760846 0.926615 0.325205 0.525448
2020-05-14 09:00:00 0.845306 0.176587 0.764530 0.674024
2020-05-14 10:00:00 0.697167 0.861391 0.519662 0.443900
2020-05-14 11:00:00 0.461842 0.418028 0.844132 0.661985
2020-05-14 12:00:00 0.661543 0.619015 0.647476 0.473730
2020-05-14 13:00:00 0.941277 0.740208 0.249476 0.097356
2020-05-14 14:00:00 0.425394 0.639996 0.093368 0.904685
2020-05-14 15:00:00 0.886753 0.153370 0.820338 0.922392
2020-05-14 16:00:00 0.253917 0.068124 0.831815 0.703694
2020-05-14 17:00:00 0.999562 0.894684 0.395017 0.862102
- 读写HDF文件
将数据写入HDF文件时,需要使用key参数指定数据集的名字。如果HDF文件已经存在,to_hdf( )函数会以追加方式写入新的数据集,其代码如下。
>>> idx = pd.date_range('08:00:00', periods=10, freq='H')
>>> df = pd.DataFrame(np.random.rand(10,4),columns=list('ABCD'),index=idx)
>>> df
A B C D
2020-05-14 08:00:00 0.677705 0.644192 0.664254 0.207009
2020-05-14 09:00:00 0.211001 0.596230 0.080490 0.526014
2020-05-14 10:00:00 0.333805 0.687243 0.938533 0.524056
2020-05-14 11:00:00 0.975474 0.575015 0.717171 0.820018
2020-05-14 12:00:00 0.236850 0.955453 0.483227 0.297570
2020-05-14 13:00:00 0.945418 0.977319 0.807121 0.526502
2020-05-14 14:00:00 0.902363 0.106375 0.744314 0.445091
2020-05-14 15:00:00 0.931304 0.253368 0.567823 0.199252
2020-05-14 16:00:00 0.168369 0.916201 0.669356 0.155653
2020-05-14 17:00:00 0.511406 0.277680 0.332807 0.141315
>>> df.to_hdf('random.h5', key='random')
>>> df = pd.read_hdf('random.h5', key='random')
>>> df
A B C D
2020-05-14 08:00:00 0.677705 0.644192 0.664254 0.207009
2020-05-14 09:00:00 0.211001 0.596230 0.080490 0.526014
2020-05-14 10:00:00 0.333805 0.687243 0.938533 0.524056
2020-05-14 11:00:00 0.975474 0.575015 0.717171 0.820018
2020-05-14 12:00:00 0.236850 0.955453 0.483227 0.297570
2020-05-14 13:00:00 0.945418 0.977319 0.807121 0.526502
2020-05-14 14:00:00 0.902363 0.106375 0.744314 0.445091
2020-05-14 15:00:00 0.931304 0.253368 0.567823 0.199252
2020-05-14 16:00:00 0.168369 0.916201 0.669356 0.155653
2020-05-14 17:00:00 0.511406 0.277680 0.332807 0.141315
Pandas扩展
如同NumPy数组被很多科学计算包视为底层的数据容器一样,Pandas定义的以DataFrame为代表的数据结构正在成为越来越多的数据处理和可视化模块的依赖包。以Pandas为基础构建的项目中,不乏Statsmodels和Seaborn这样的“明星级”模块。
统计扩展模块Statsmodels
Statsmodels是著名的统计扩展模块,其使用Pandas作为计算的底层数据容器,与Pandas密不可分。Statsmodels提供了比Pandas更加强大的统计数据功能,包括计量经济学、分析和建模等。Statsmodels模块既可以通过pip命令进行安装,也可以从GitHub上下载。以下列出了Statsmodels模块的主要应用接口。
- stasmodels.api用于线性回归(普通最小二乘线性回归、加权最小二乘线性回归、广义最小二乘线性回归)。
stasmodels.api.OLS(endog,exog,missing,hasconst) stasmodels.api.OLS(endog,exog,missing,hasconst).fit( ) stasmodels.api.GLS(endog,exog,sigma,missing,hasconst) stasmodels.api.WLS(endog,exog,weights,missing,hasconst)
-
statsmodels.tsa用于时间序列建模分析。statsmodels.tsa包括基本的线性时间序列模型:自回归模型AR、向量自回归模型VAR、自回归移动平均模型ARMA,以及非线性的马尔可夫转化模型Markov switching dynamic regression and autoregression。另外也可以获取时间序列的描述统计量,例如该序列的自相关系数和偏自相关系数。模型的参数估计方法可使用条件最大似然和条件最小二乘,卡尔曼滤波也在可选参数范围内。
-
statsmodels.stats提供了丰富的统计检验工具,可以独立应用在statsmodel.api或statsmodel.tsa建立的统计模型中。 statsmodels.stats.jarque_beta函数用于正态性检验。 statsmodels.stats.robust_skewness函数和statsmodels.stats.robust_kurtosis函数用于计算偏度和峰度,一般也用于正态性检验。 statsmodels.stats.acorr_ljungbox函数可以给出更详细的L-B检验统计量和P值,包含大样本情况下的Q统计量检验和小样本情况下改进后的L-B检验。 (4)statsmodels.graphics为各种统计模型提供了绘图工具。
qqplot函数(在statsmodels.api.qqplot也有绘制QQ图的工具)。 boxplots类中的小提琴图statsmodels.graphics.boxplots.violinplot函数。 相关系数类中的statsmodels.graphics.correlation.plot_corr函数。 回归模型中的statsmodels.graphics.regressionplots.plot_regress_exog函数。 时间序列模型中的statsmodels.graphics.tsaplots.plot_acf和plot_pacf函数。 下面使用美国的宏观经济数据简单演示Statsmodels模块的使用。
>>> import numpy as np
>>> import pandas as pd
>>> import statsmodels.api as sm
>>> import matplotlib.pyplot as plt
>>> from statsmodels.datasets.longley import load_pandas
>>> df = load_pandas().data
>>> df
TOTEMP GNPDEFL GNP UNEMP ARMED POP YEAR
0 60323.0 83.0 234289.0 2356.0 1590.0 107608.0 1947.0
1 61122.0 88.5 259426.0 2325.0 1456.0 108632.0 1948.0
2 60171.0 88.2 258054.0 3682.0 1616.0 109773.0 1949.0
3 61187.0 89.5 284599.0 3351.0 1650.0 110929.0 1950.0
4 63221.0 96.2 328975.0 2099.0 3099.0 112075.0 1951.0
5 63639.0 98.1 346999.0 1932.0 3594.0 113270.0 1952.0
6 64989.0 99.0 365385.0 1870.0 3547.0 115094.0 1953.0
7 63761.0 100.0 363112.0 3578.0 3350.0 116219.0 1954.0
8 66019.0 101.2 397469.0 2904.0 3048.0 117388.0 1955.0
9 67857.0 104.6 419180.0 2822.0 2857.0 118734.0 1956.0
10 68169.0 108.4 442769.0 2936.0 2798.0 120445.0 1957.0
11 66513.0 110.8 444546.0 4681.0 2637.0 121950.0 1958.0
12 68655.0 112.6 482704.0 3813.0 2552.0 123366.0 1959.0
13 69564.0 114.2 502601.0 3931.0 2514.0 125368.0 1960.0
14 69331.0 115.7 518173.0 4806.0 2572.0 127852.0 1961.0
15 70551.0 116.9 554894.0 4007.0 2827.0 130081.0 1962.0
该DataFrame共有7列数据,第一列是总就业数,为因变量;后几列分别是GNP平减指数、GNP、失业数、武装力量规模、人口、年份。下面用后几列作为解释变量,对因变量做基础的最小二乘法回归,其代码如下。
>>> y = load_pandas().endog
>>> X = load_pandas().exog
>>> X = sm.add_constant(X)
>>> ols_model = sm.OLS(y,X).fit()
>>> ols_model.summary()
<class 'statsmodels.iolib.summary.Summary'>
"""
OLS Regression Results
============================================================================
Dep. Variable: TOTEMP R-squared: 0.995
Model: OLS Adj. R-squared: 0.992
Method: Least Squares F-statistic: 330.3
Date: Thu, 14 May 2020 Prob (F-statistic): 4.98e-10
Time: 14:49:12 Log-Likelihood: -109.62
No. Observations: 16 AIC: 233.2
Df Residuals: 9 BIC: 238.6
Df Model: 6
Covariance Type: nonrobust
===============================================================================
coef std err t P>|t| [0.025 0.975]
----------------------------------------------------------------------------
const -3.482e+06 8.9e+05 -3.911 0.004 -5.5e+06 -1.47e+06
GNPDEFL 15.0619 84.915 0.177 0.863 -177.029 207.153
GNP -0.0358 0.033 -1.070 0.313 -0.112 0.040
UNEMP -2.0202 0.488 -4.136 0.003 -3.125 -0.915
ARMED -1.0332 0.214 -4.822 0.001 -1.518 -0.549
POP -0.0511 0.226 -0.226 0.826 -0.563 0.460
YEAR 1829.1515 455.478 4.016 0.003 798.788 2859.515
===============================================================================
Omnibus: 0.749 Durbin-Watson: 2.559
Prob(Omnibus): 0.688 Jarque-Bera (JB): 0.684
Skew: 0.420 Prob(JB): 0.710
Kurtosis: 2.434 Cond. No. 4.86e+09
===============================================================================
Warnings:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 4.86e+09. This might indicate that there are
strong multicollinearity or other numerical problems.
"""
结果显示,R方为0.995,F统计量为330,P值远小于0.05,说明回归方程显著,该模型比较合适。下面是各个协变量系数的估计值、t检验统计量和P值。P值小于0.05,说明该变量显著,能够用来回归因变量。最后的Warnings值得注意,条件数过大,存在多重共线性,可能是因为我们使用的数据的各个变量之间存在较强的线性相关关系。
可视化扩展Seaborn
类似Pandas的数据可视化API,Seaborn也是基于Matplotlib实现的可视化库。它提供面向数据集的高度交互式的API,可以让用户轻松画出更加美观的图形。Seaborn的美观主要体现在配色更加和谐,以及图形元素的样式更加细腻上。Seaborn高度兼容NumPy和Pandas的数据结构,可以在绘制图表时进行统计估计,汇总观察结果,并可视化统计模型的拟合以强调数据集中的模式。
Seaborn的安装方法非常简单,可以使用pip命令直接安装。
pip install seaborn
下面以Seaborn自带的fmri数据集为例,展示Seaborn的使用方法和绘图风格。
>>> import numpy as np
>>> import pandas as pd
>>> import matplotlib.pyplot as plt
>>> import seaborn as sns
>>> fn = r'D:\NumPyFamily\data\fmri.csv'
>>> ds = pd.read_csv(fn)
>>> ds
subject timepoint event region signal
0 s13 18 stim parietal -0.017552
1 s5 14 stim parietal -0.080883
2 s12 18 stim parietal -0.081033
3 s11 18 stim parietal -0.046134
4 s10 18 stim parietal -0.037970
... ... ... ... ... ...
1059 s0 8 cue frontal 0.018165
1060 s13 7 cue frontal -0.029130
1061 s12 7 cue frontal -0.004939
1062 s11 7 cue frontal -0.025367
1063 s0 0 cue parietal -0.006899
[1064 rows x 5 columns]
>>> sns.set(style='darkgrid')
>>> sns.relplot(x='timepoint', y='signal', hue='event', style='event', col='region',
kind='line', data=ds)
<seaborn.axisgrid.FacetGrid object at 0x000001C44AB657C8>
>>> plt.show()