飞道的博客

Pandas简明教程

380人阅读  评论(0)

1. Pandas概览

1.1 为什么要用Pandas?

Pandas是一个基于NumPy的分析结构化数据的工具集,NumPy为其提供了高性能的数据处理能力。Pandas被普遍用于数据挖掘和数据分析,同时也提供数据清洗、数据I/O、可视化等辅助功能。

作为Python科学计算的基础软件包,NumPy已经足够强大,却不够完美,因为NumPy不支持含有异构列的表格数据。所谓异构列表格数据,是指在一个二维数据结构中允许不同的列各自拥有不同的数据类型。尽管NumPy支持任意维度的数据结构,但在实际工作中,无论是传统软件开发领域还是机器学习领域,我们面对的数据大多都是二维异构列数据。Pandas正是为处理此类数据应运而生的,它为处理和SQL或Excel表类似的异构列表格数据提供了灵活、便捷的数据结构,从而迅速成为Python的核心数据分析支持库。

1.2 Pandas的特点

Pandas诞生于2008年,最初是专为金融、统计领域的数据处理者而非程序员量身定制的,并以最符合使用者思维习惯的方式提供了几乎所有可能需要的功能。Pandas追求的目标似乎是尽可能屏蔽所有软件工程的概念,仅保留数据的物理属性和逻辑。仅举一例(类似的例子有很多)证明我的观点:只需一行代码即可实现网络数据的抓取和解析,用户无需了解http协议和html解析技术。

>>> data = pd.read_html('http://ditu.92cha.com/dizhen.php')
>>> data[0].head()
              发震时刻  震级(M)   经度(°)  纬度(°)  深度(千米)   参考位置
0  2020-05-29 03:34:23    4.2  120.80  23.63    26.0           台湾南投县
1  2020-05-29 00:35:59    3.9   78.40  34.27    20.0          克什米尔地区
2  2020-05-28 21:33:47    3.1   83.43  41.25    10.0      新疆阿克苏地区库车市
3  2020-05-28 17:46:49    5.9 -175.35 -27.43    10.0          克马德克群岛
4  2020-05-28 17:11:38    3.6  117.38  44.67    15.0  内蒙古锡林郭勒盟西乌珠穆沁旗

简洁如斯,夫复何求!但是,对用户过分的迁就和溺爱,其实是一把双刃剑。正如Pandas之父Wes McKinney所说,Pandas正在背离他最初所期望的简洁和易用,变得越来越臃肿和不可控制。

我非常认同Wes McKinney的观点,甚至觉得当Pandas抛弃了panel这个概念的时候,就已经走火入魔了。panel是Pandas最初为处理更高维数据提出的方案,非常接近HDF或者netCDF的理念。Pandas后来使用了“层次化索引”处理更高维数据,导致结构趋于复杂,使得程序员无法专注于事务逻辑的处理。

不过,瑕不掩瑜。即便有那么一点点遗憾,也难掩Pandas的光华。Pandas不只是简洁,它还拥有出众的数据处理能力、完备的辅助功能。归纳起来,Pandas有以下五大特点。

  1. 具有极强的自适应能力。无论是Python还是NumPy的数据对象,即便是结构不规则的数据,也可以轻松转换为DataFrame对象。Pandas还会自动处理缺失数据,类似于NumPy的掩码数组。
  2. 遗传自NumPy的基因为其提供了快速的数据组织和处理能力。Pandas支持任意增删数据列,支持合并、连接、重塑、透视数据集,支持聚合、转换、切片、花式索引、子集分解等操作。
  3. 完善的时间序列。Pandas支持日期范围生成、频率转换、移动窗口统计、移动窗口线性回归、日期位移等时间序列功能。
  4. 拥有最全面的IO工具。Pandas支持读取文本文件(CSV等支持分隔符的文件)、Excel 文件、HDF文件、SQL表数据、json数据、html数据,甚至可以直接从url下载并解析数据,也可以将数据保存为CSV文件或Excel文件。
  5. 对用户友好的显示格式。不管数据如何复杂,Pandas总是试图展现给你最清晰的结构,它会自动对齐对象和标签,必要时也可以忽略标签。

1.3 安装和使用

由于Pandas依赖NumPy,安装之前最好先安装NumPy、Matplotlib等SciPy家族的其他模块。当然,如果没有,安装过程也会自动为您安装各种依赖包的。

PS C:\Users\xufive> pip install pandas

类似于导入NumPy模块时使用简写,导入Pandas模块时,通常都会简写为pd,这几乎成为程序员们约定俗成的规则。下面的代码,构建了一个带标签的二维数据表格。北京、广州、上海、杭州是每列数据的标签,所有列的标签称为列名;2020、2019、2018是每一行数据的标签,所有行的标签称为索引。这个带标签的二维数据表格,就是Pandas最核心的数据结构DataFrame,所有关于Pandas的操作和技巧,几乎都是针对DataFrame这个结构的。

>>> 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
       北京       广州       上海       杭州
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

本文所有的示例代码中,如果使用了pd或者np的模块简写,意味着在此之前已经使用下面的语句导入了Pandas和NumPy模块。

>>> import numpy as np
>>> import pandas as pd

2. 数据结构

学习Pandas的最好方式是从了解它的数据结构开始。很多人说,Pandas很简单,只有Series和DataFrame两种数据结构。但是,不要忘记,不管是Series还是DataFrame,它们都有一个索引对象Index,Index也是Pandas的基本数据结构之一。

2.1 索引数组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'])

使用数组、列表、迭代器等都可以创建索引对象。索引对象看起来像一维数组,但无法修改元素的值。这一点非常重要,惟其如此,才能保证多个数据结构之间的安全共享。
实际上,索引对象有很多种类型,除了一维索引数据组,还有时间纳秒级戳索引、层次化索引等。此外,索引对象也有删除、插入、连接、交集、并集等操作。这些内容会在后面的应用中被用到。

2.2 带标签的一维同构数组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的每一个数据对应一个索引,但索引是允许重复的。

2.3 带标签的二维异构表格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对象时,指定索引。这里直接使用日期字符串做索引,正确的做法是使用日期索引对象。我们将在讨论时间序列时,正式介绍DatetimeIndex类的使用。

>>> 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对象时,同时指定索引和列标签。如果没有指定索引或列标签,则会自动添加从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')

3. 基本操作

DataFrame几乎被打造成了一个全能小怪兽:它有字典的影子,有NumPy数组的性能,甚至继承了NumPy数组的很多属性和方法;它可以在一个结构内存储和处理多种不同类型的数据;它看起来像是二维的结构,却能处理更高维度的数据;它可以处理包括日期时间在内的任意类型的数据,它能读写几乎所有的数据格式;它提供了多到不可胜数的方法,并派生出无穷的操作技巧。想在较短的篇幅内详尽介绍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)

3.1 数据预览

3.1.1 显示开始的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

3.1.2 查看均值、方差、极值的统计概要

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

3.1.3 转置

>>> 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]

3.1.4 排序

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

3.2 数据选择

3.2.1 行选择

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

3.2.2 列选择

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

3.2.3 行列选择

使用行列选择器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

3.2.4 条件选择

熟悉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

3.3 改变数据结构

3.3.1 重新索引

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

3.3.2 删除行或列

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

3.3.3 行扩展

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

3.3.4 列扩展

直接对新列赋值,即可实现列扩展。赋值时,数据长度必须要和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

3.4 改变数据类型

类似于NumPy数组,Series对象提供了astype()的方法用来改变数据类型。不过astype()只是返回了一个新的Series对象,并没有真正改变原有的Series对象。DataFrame对象没有提供改变某一列数据类型的方法,如果要这样做,可以对这一列重新赋值。

>>> stock['涨跌幅'].dtype
dtype('float64')
>>> stock['涨跌幅'] = stock['涨跌幅'].astype('float32').values
>>> stock['涨跌幅'].dtype
dtype('float32')

3.5 广播与矢量化运算

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

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

4. 高级应用

DataFrame作为数据分析的利器,其作用体现在两个方面:一是用来暂存形式复杂的数据,二是提供高效的处理手段。前一节的基本操作,偏重于暂存数据,本节的高级应用,则偏重于如何高效地处理数据。

4.1 分组

分组与聚合是数据处理中最常见的应用场景。比如,对于多只股票多个交易日的成交量分析,需要按股票和交易日两种分类方式进行统计。

>>> 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()函数按照日期分组,返回的分组结果是一个迭代器,遍历这个迭代器,可以得到三个由组名(日期)和该组的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()函数按照股票代码分组,返回的分组结果是一个迭代器,遍历这个迭代器,可以得到五个由组名(股票代码)和该组的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

4.2 聚合

理解了分组,接下来就可以根据分组做点什么了。比如,统计所有股票每天的成交总额和成交总量等等。

>>> 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)、最大值和最小值(min和max)、第一个和最后一个有效值(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

聚合函数还可以对不同的列实施不同的函数操作。比如,下面的代码对成交额实施均值操作,对成交量实施自定义的波动幅度函数。

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

4.3 层次化索引

在讨论分组和聚合的例子中,日期-股票代码-成交额和成交量,这样的数据结构俨然已经是三维的了,却依然可以用DataFrame暂存和处理。这样的数据结构,尽管可以通过分组获得多个二维的DataFrame对象,但毕竟无法直接索引或者选择。层次化索引可以很好地解决这个问题,为DataFrame处理更高维数据指明了方向。

这里仍然直接使用日期字符串做索引,正确的做法是使用日期索引对象。我们将在讨论时间序列时,正式介绍DatetimeIndex类的使用。

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

4.4 表级广播函数

行列级广播函数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()并无新意,不过,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

4.5 日期时间索引对象

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(std)

给定起止时间、序列长度或者分割步长,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')

4.6 数据可视化

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对象的数据可视化,通常以横轴表示索引,以纵轴表示数据。

>>> 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对象的数据可视化,也是以横轴表示索引,多列数据可以绘制在画布(figure)的同一个子图(axes)上,也可以绘制在同一张画布的多个子图(axes)上。

>>> 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要简洁得多。

需要了解一些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()

下图显示了普通柱状图、堆叠柱状图和水平的堆叠柱状图绘制在同一张画布上的效果。

4.7 数据I/O

作为数据处理的利器,数据的输入输出自然是必不可少的功能。Pandas可以读取不同格式、不同来源的数据,也可以将数据保存成各种格式的数据文件。

4.7.1 读写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

4.7.2 读写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

4.7.2 读写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

转载:https://blog.csdn.net/xufive/article/details/106419511
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场