盘一盘 Python 系列 4 - Pandas (下)
本文是 Python 系列的第七篇
数据结构之 Pandas (下)
基本可视化之 Matplotlib
统计可视化之 Seaborn
交互可视化之 Bokeh
炫酷可视化之 PyEcharts
机器学习之 Sklearn
深度学习之 TensorFlow
深度学习之 Keras
深度学习之 PyTorch
深度学习之 MXnet
接着上篇继续后面三个章节
数据表的合并和连接
数据表的重塑和透视
数据表的分组和整合
数据表可以按「键」合并,用 merge 函数;可以按「轴」来连接,用 concat 函数。
4.1
合并
按键 (key) 合并可以分「单键合并」和「多键合并」。
单键合并用 merge 函数,语法如下:
pd.merge( df1, df2, how=s, on=c )
c 是 df1 和 df2 共有的一栏,合并方式 (how=s) 有四种:
左连接 (left join):合并之后显示 df1 的所有行
右连接 (right join):合并之后显示 df2 的所有行
外连接 (outer join):合并 df1 和 df2 共有的所有行
内连接 (inner join):合并所有行 (默认情况)
首先创建两个 DataFrame:
df_price:4 天的价格 (2019-01-01 到 2019-01-04)
df_volume:5 天的交易量 (2019-01-02 到 2019-01-06)
df_price = pd.DataFrame( {'Date': pd.date_range('2019-1-1', periods=4),
'Adj Close': [24.42, 25.00, 25.25, 25.64]})
df_price
df_volume = pd.DataFrame( {'Date': pd.date_range('2019-1-2', periods=5),
'Volume' : [56081400, 99455500, 83028700, 100234000, 73829000]})
df_volume
接下来用 df_price 和 df_volume 展示四种合并。
pd.merge( df_price, df_volume, how='left' )
按 df_price 里 Date 栏里的值来合并数据
df_volume 里 Date 栏里没有 2019-01-01,因此 Volume 为 NaN
df_volume 里 Date 栏里的 2019-01-05 和 2019-01-06 不在 df_price 里 Date 栏,因此丢弃
pd.merge( df_price, df_volume, how='right' )
按 df_volume 里 Date 栏里的值来合并数据
df_price 里 Date 栏里没有 2019-01-05 和 2019-01-06,因此 Adj Close 为 NaN
df_price 里 Date 栏里的 2019-01-01 不在 df_volume 里 Date 栏,因此丢弃
pd.merge( df_price, df_volume, how='outer' )
按 df_price 和 df_volume 里 Date 栏里的所有值来合并数据
df_price 里 Date 栏里没有 2019-01-05 和 2019-01-06,因此 Adj Close 为 NaN
df_volume 里 Date 栏里没有 2019-01-01,因此 Volume 为 NaN
pd.merge( df_price, df_volume, how='inner' )
按 df_price 和 df_volume 里 Date 栏里的共有值来合并数据
df_price 里 Date 栏里的 2019-01-01 不在 df_volume 里 Date 栏,因此丢弃
df_volume 里 Date 栏里的 2019-01-05 和 2019-01-06 不在 df_price 里 Date 栏,因此丢弃
多键合并用的语法和单键合并一样,只不过 on=c 中的 c 是多栏。
pd.merge( df1, df2, how=s, on=c )
首先创建两个 DataFrame:
portfolio1:3 比产品 FX Option, FX Swap 和 IR Option 的数量
portfolio2:4 比产品 FX Option (重复名称), FX Swap 和 IR Swap 的数量
porfolio1 = pd.DataFrame({'Asset': ['FX', 'FX', 'IR'],
'Instrument': ['Option', 'Swap', 'Option'],
'Number': [1, 2, 3]})
porfolio1
porfolio2 = pd.DataFrame({'Asset': ['FX', 'FX', 'FX', 'IR'],
'Instrument': ['Option', 'Option', 'Swap', 'Swap'],
'Number': [4, 5, 6, 7]})
porfolio2
在 'Asset' 和 'Instrument' 两个键上做外合并。
pd.merge( porfolio1, porfolio2,
on=['Asset','Instrument'],
how='outer')
df1 和 df2 中两个键都有 FX Option 和 FX Swap,因此可以合并它们中 number 那栏。
df1 中有 IR Option 而 df2 中没有,因此 Number_y 栏下的值为 NaN
df2 中有 IR Swap 而 df1 中没有,因此 Number_x 栏下的值为 NaN
当 df1 和 df2 有两个相同的列 (Asset 和 Instrument) 时,单单只对一列 (Asset) 做合并产出的 DataFrame 会有另一列 (Instrument) 重复的名称。这时 merge 函数给重复的名称加个后缀 _x, _y 等等。
pd.merge( porfolio1, porfolio2,
on='Asset' )
当没设定 merge 函数里参数 how 时,默认为 inner (内合并)。在 Asset 列下,df1 有 2 个 FX 和 1 个 IR,df2 有 3 个 FX 和 1 个 IR,内合并完有 8 行 (2×3+1×1)。
如果觉得后缀 _x, _y 没有什么具体含义时,可以设定 suffixes 来改后缀。比如 df1 和 df2 存储的是 portoflio1 和 portfolio2 的产品信息,那么将后缀该成 ‘1’ 和 ‘2’ 更贴切。
pd.merge( porfolio1, porfolio2,
on='Asset',
suffixes=('1','2'))
4.2
连接
Numpy 数组可相互连接,用 np.concat;同理,Series 也可相互连接,DataFrame 也可相互连接,用 pd.concat。
在 concat 函数也可设定参数 axis,
axis = 0 (默认),沿着轴 0 (行) 连接,得到一个更长的 Series
axis = 1,沿着轴 1 (列) 连接,得到一个 DataFrame
被连接的 Series 它们的 index 可以重复 (overlapping),也可以不同。
先定义三个 Series,它们的 index 各不同。
s1 = pd.Series([0, 1], index=['a', 'b'])
s2 = pd.Series([2, 3, 4], index=['c', 'd', 'e'])
s3 = pd.Series([5, 6], index=['f', 'g'])
沿着「轴 0」连接得到一个更长的 Series。
pd.concat([s1, s2, s3])
a 0
b 1
c 2
d 3
e 4
f 5
g 6
dtype: int64
沿着「轴 1」连接得到一个 DataFrame。
pd.concat([s1, s2, s3], axis=1)
将 s1 和 s3 沿「轴 0」连接来创建 s4,这样 s4 和 s1 的 index 是有重复的。
s4 = pd.concat([s1, s3])
s4
a 0
b 1
f 5
g 6
dtype: int64
将 s1 和 s4 沿「轴 1」内连接 (即只连接它们共有 index 对应的值)
pd.concat([s1, s4], axis=1, join='inner')
最后还可以将 n 个 Series 沿「轴 0」连接起来,再赋予 3 个 keys 创建多层 Series。
pd.concat( [s1, s1, s3],
keys=['one','two','three'])
one a 0
b 1
two a 0
b 1
three f 5
g 6
dtype: int64
连接 DataFrame 的逻辑和连接 Series 的一模一样。
先创建两个 DataFrame,df1 和 df2。
df1 = pd.DataFrame( np.arange(12).reshape(3,4),
columns=['a','b','c','d'])
df1
df2 = pd.DataFrame( np.arange(6).reshape(2,3),
columns=['b','d','a'])
df2
沿着行连接分两步
先把 df1 和 df2 列标签补齐
再把 df1 和 df2 纵向连起来
pd.concat( [df1, df2] )
得到的 DataFrame 的 index = [0,1,2,0,1],有重复值。如果 index 不包含重要信息 (如上例),可以将 ignore_index 设置为 True,这样就得到默认的 index 值了。
pd.concat( [df1, df2], ignore_index=True )
先创建两个 DataFrame,df1 和 df2。
df1 = pd.DataFrame( np.arange(6).reshape(3,2),
index=['a','b','c'],
columns=['one','two'] )
df1
df2 = pd.DataFrame( 5 + np.arange(4).reshape(2,2),
index=['a','c'],
columns=['three','four'])
df2
沿着列连接分两步
先把 df1 和 df2 行标签补齐
再把 df1 和 df2 横向连起来
pd.concat( [df1, df2], axis=1 )
重塑 (reshape) 和透视 (pivot) 两个操作只改变数据表的布局 (layout):
重塑用 stack 和 unstack 函数 (互为逆转操作)
透视用 pivot 和 melt 函数 (互为逆转操作)
5.1
重塑
在〖数据结构之 Pandas (上)〗提到过,DataFrame 和「多层索引的 Series」其实维度是一样,只是展示形式不同。而重塑就是通过改变数据表里面的「行索引」和「列索引」来改变展示形式。
列索引 → 行索引,用 stack 函数
行索引 → 列索引,用 unstack 函数
创建 DataFrame df (1 层行索引,1 层列索引)
symbol = ['JD', 'AAPL']
data = {'行业': ['电商', '科技'],
'价格': [25.95, 172.97],
'交易量': [27113291, 18913154]}
df = pd.DataFrame( data, index=symbol )
df.columns.name = '特征'
df.index.name = '代号'
df
从上表中可知:
行索引 = [JD, AAPL],名称是代号
列索引 = [行业, 价格, 交易量],名称是特征
列索引 (特征) 变成了行索引,原来的 DataFrame df 变成了两层 Series (第一层索引是代号,第二层索引是特征)。
c2i_Series = df.stack()
c2i_Series
代号 特征
JD 行业 电商
价格 25.95
交易量 27113291
AAPL 行业 科技
价格 172.97
交易量 18913154
dtype: object
思考:变成行索引的特征和原来行索引的代号之间的层次是怎么决定的?好像特征更靠内一点,代号更靠外一点。
行索引 (代号) 变成了列索引,原来的 DataFrame df 也变成了两层 Series (第一层索引是特征,第二层索引是代号)。
i2c_Series = df.unstack()
i2c_Series
特征 代号
行业 JD 电商
AAPL 科技
价格 JD 25.95
AAPL 172.97
交易量 JD 27113291
AAPL 18913154
dtype: object
思考:变成列索引的特征和原来列索引的代号之间的层次是怎么决定的?这时好像代号更靠内一点,特征更靠外一点。
对 df 做 stack 和 unstack 都得到了「两层 Series」,但是索引的层次不同,那么在背后的规律是什么?首先我们先来看看两个「两层 Series」的 index 包含哪些信息 (以及 df 的 index 和 columns)。
df.index, df.columns
c2i_Series.index
i2c_Series.index
定义
r = [JD, AAPL],名称是代号
c = [行业, 价格, 交易量],名称是特征
那么
df 的行索引 = r
df 的列索引 = c
c2i_Series 的索引 = [r, c]
i2c_Series 的索引 = [c, r]
现在可以总结规律:
当用 stack 将 df 变成 c2i_Series 时,df 的列索引 c 加在其行索引 r 后面得到 [r, c] 做为 c2i_Series 的多层索引
当用 unstack 将 df 变成 i2c_Series 时,df 的行索引 r 加在其列索引 c 后面得到 [c, r] 做为 i2c_Series 的多层索引
对于多层索引的 Series,unstack 哪一层有两种方法来确定:
基于层 (level-based)
基于名称 (name-based)
拿 c2i_Series 举例 (读者也可以尝试 i2c_Series):
代号 特征
JD 行业 电商
价格 25.95
交易量 27113291
AAPL 行业 科技
价格 172.97
交易量 18913154
dtype: object
1. 基于层来 unstack() 时,没有填层数,默认为最后一层。
c2i_Series.unstack()
c2i_Series 的最后一层 (看上面它的 MultiIndex) 就是 [行业, 价格, 交易量],从行索引转成列索引得到上面的 DataFrame。
2. 基于层来 unstack() 时,选择第一层 (参数放 0)
c2i_Series.unstack(0)
c2i_Series 的第一层 (看上面它的 MultiIndex) 就是 [JD, AAPL],从行索引转成列索引得到上面的 DataFrame。
3. 基于名称来 unstack
c2i_Series.unstack('代号')
c2i_Series 的代号层 (看上面它的 MultiIndex) 就是 [JD, AAPL],从行索引转成列索引得到上面的 DataFrame。
创建 DataFrame df (2 层行索引,1 层列索引)
data = [ ['电商', 101550, 176.92],
['电商', 175336, 25.95],
['金融', 60348, 41.79],
['金融', 36600, 196.00] ]
midx = pd.MultiIndex( levels=[['中国','美国'],
['BABA', 'JD', 'GS', 'MS']],
labels=[[0,0,1,1],[0,1,2,3]],
names = ['地区', '代号'])
mcol = pd.Index(['行业','雇员','价格'], name='特征')
df = pd.DataFrame( data, index=midx, columns=mcol )
df
从上表中可知:
行索引第一层 = r1 = [中国, 美国],名称是地区
行索引第二层 = r2 = [BABA, JD, GS, MS],名称是代号
列索引 = c = [行业, 雇员, 价格],名称是特征
查看 df 的 index 和 columns 的信息
df.index, df.columns
那么
df 的行索引 = [r1, r2]
df 的列索引 = c
1. 基于层来 unstack() 时,选择第一层 (参数放 0)
df.unstack(0)
df 被 unstack(0) 之后变成 (行 → 列)
行索引 = r2
列索引 = [c, r1]
重塑后的 DataFrame 这时行索引只有一层 (代号),而列索引有两层,第一层是特征,第二层是地区。
2. 基于层来 unstack() 时,选择第二层 (参数放 1)
df.unstack(1)
df 被 unstack(1) 之后变成 (行 → 列)
行索引 = r1
列索引 = [c, r2]
重塑后的 DataFrame 这时行索引只有一层 (地区),而列索引有两层,第一层是地区,第二层是代号。
3. 基于层先 unstack(0) 再 stack(0)
df.unstack(0).stack(0)
df 被 unstack(0) 之后变成 (行 → 列)
行索引 = r2
列索引 = [c, r1]
再被 stack(0) 之后变成 (列 → 行)
行索引 = [r2, c]
列索引 = r1
重塑后的 DataFrame 这时行索引有两层,第一层是代号,第二层是特征,而列索引只有一层 (地区)。
4. 基于层先 unstack(0) 再 stack(1)
df.unstack(0).stack(1)
df 被 unstack(0) 之后变成 (行 → 列)
行索引 = r2
列索引 = [c, r1]
再被 stack(1) 之后变成 (列 → 行)
行索引 = [r2, r1]
列索引 = c
重塑后的 DataFrame 这时行索引有两层,第一层是代号,第二层是地区,而列索引只有一层 (特征)。
5. 基于层先 unstack(1) 再 stack(0)
df.unstack(1).stack(0)
df 被 unstack(1) 之后变成 (行 → 列)
行索引 = r1
列索引 = [c, r2]
再被 stack(0) 之后变成 (列 → 行)
行索引 = [r1, c]
列索引 = r2
重塑后的 DataFrame 这时行索引有两层,第一层是地区,第二层是特征,而列索引只有一层 (代号)。
6. 基于层先 unstack(1) 再 stack(1)
df.unstack(1).stack(1)
df 被 unstack(1) 之后变成 (行 → 列)
行索引 = r1
列索引 = [c, r2]
再被 stack(1) 之后变成 (列 → 行)
行索引 = [r1, r2]
列索引 = c
重塑后的 DataFrame 这时行索引有两层,第一层是地区,第二层是特征,而列索引只有一层 (代号)。还原成原来的 df 了。
7. 基于层被 stack(),没有填层数,默认为最后一层。
df.stack()
地区 代号 特征
中国 BABA 行业 电商
雇员 101550
价格 176.92
JD 行业 电商
雇员 175336
...
美国 GS 雇员 60348
价格 41.79
MS 行业 金融
雇员 36600
价格 196
Length: 12, dtype: object
df 被 stack() 之后变成 (列 → 行)
行索引 = [r1, r2, c]
列索引 = []
重塑后的 Series 只有行索引,有三层,第一层是地区,第二层是代号,第三层是特征。
8. 基于层被 unstack() 两次,没有填层数,默认为最后一层。
df.unstack().unstack()
特征 代号 地区
行业 BABA 中国 电商
美国 NaN
JD 中国 电商
美国 NaN
GS 中国 NaN
...
价格 JD 美国 NaN
GS 中国 NaN
美国 41.79
MS 中国 NaN
美国 196
Length: 24, dtype: object
df 被第一次 unstack() 之后变成 (行 → 列)
行索引 = r1
列索引 = [c, r2]
df 被第二次 unstack() 之后变成 (行 → 列)
行索引 = []
列索引 = [c, r2, r1]
重塑后的 Series 只有列索引 (实际上是个转置的 Series),有三层,第一层是特征,第二层是代号,第三层是地区。
5.2
透视
数据源表通常只包含行和列,那么经常有重复值出现在各列下,因而导致源表不能传递有价值的信息。这时可用「透视」方法调整源表的布局用作更清晰的展示。
首先把源表分组,将不同值当做行 (row)、列 (column) 和值 (value)
然后对各组内数据做汇总操作如排序、平均、累加、计数等
这种动态将·「源表」得到想要「终表」的旋转 (pivoting) 过程,使透视表得以命名。
在 Pandas 里透视的方法有两种:
用 pivot 函数将「一张长表」变「多张宽表」,
用 melt 函数将「多张宽表」变「一张长表」,
本节使用的数据描述如下:
5 只股票:AAPL, JD, BABA, FB, GS
4 个交易日:从 2019-02-21 到 2019-02-26
data = pd.read_csv('Stock.csv', parse_dates=[0], dayfirst=True)
data
从上表看出有 20 行 (5 × 4) 和 8 列,在 Date 和 Symbol 那两列下就有重复值,4 个日期和 5 个股票在 20 行中分别出现了 5 次和 4 次。
当我们做数据分析时,只关注不同股票在不同日期下的 Adj Close,那么可用 pivot 函数可将原始 data「透视」成一个新的 DataFrame,起名 close_price。在 pivot 函数中
将 index 设置成 ‘Date’
将 columns 设置成 ‘Symbol’
将 values 设置 ‘Adj Close’
close_price 实际上把 data[‘Date’] 和 data[‘Symbol’] 的唯一值当成支点(pivot 就是支点的意思) 创建一个 DataFrame,其中
行标签 = 2019-02-21, 2019-02-22, 2019-02-25, 2019-02-26
列标签 = AAPL, JD, BABA, FB, GS
在把 data[‘Adj Close’] 的值放在以如上的行标签和列标签创建的 close_price 来展示。
代码如下:
close_price = data.pivot( index='Date',
columns='Symbol',
values='Adj Close' )
close_price
如果觉得 Adj Close 不够,还想加个 Volume 看看,那么就把 values 设置成 ['Adj Close', 'Volume']。这时支点还是 data[‘Date’] 和 data[‘Symbol’],但是要透视的值增加到 data[['Adj Close', 'Volume']] 了。pivot 函数返回的是两个透视表。
data.pivot( index='Date',
columns='Symbol',
values=['Adj Close','Volume'] )
如果不设置 values 参数,那么 pivot 函数返回的是六个透视表。(源表 data 有八列,两列当了支点,剩下六列用来透视)
all_pivot = data.pivot( index='Date',
columns='Symbol' )
all_pivot
再继续观察下,all_pivot 实际上是个多层 DataFrame (有多层 columns)。假设我们要获取 2019-02-25 和 2019-02-26 两天的 BABA 和 FB 的开盘价,用以下「多层索引和切片」的方法。
all_pivot['Open'].iloc[2:,1:3]
pivot 逆反操作是 melt。
前者将「一张长表」变成「多张宽表」
后者将「多张宽表」变成「一张长表」
具体来说,函数 melt 实际是将「源表」转化成 id-variable 类型的 DataFrame,下例将
Date 和 Symbol 列当成 id
其他列 Open, High, Low, Close, Adj Close 和 Volume 当成 variable,而它们对应的值当成 value
代码如下:
melted_data = pd.melt( data, id_vars=['Date','Symbol'] )
melted_data.head(5).append(melted_data.tail(5))
新生成的 DataFrame 有 120 行 (4 × 5 × 6)
4 = data['Date'] 有 4 个日期
5 = data['Symbol'] 有 5 只股票
6 = Open, High, Low, Close, Adj Close 和 Volume 这 6 个变量
在新表 melted_data 中
在参数 id_vars 设置的 Date 和 Symbol 还保持为 columns
此外还多出两个 columns,一个叫 variable,一个叫 value
variable 列下的值为 Open, High, Low, Close, Adj Close 和 Volume
value 列下的值为前者在「源表 data」中的值
函数 melt 可以生成一张含有多个 id 的长表,然后可在 id 上筛选出我们想要的信息,比如
melted_data[ lambda x: (x.Date=='25/02/2019')
& ((x.Symbol=='BABA')|(x.Symbol=='FB')) ]
在 melted_data 上使用调用函数 (callable function) 做索引,我们得到了在 2019-02-25 那天 BABA 和 FB 的信息。
DataFrame 中的数据可以根据某些规则分组,然后在每组的数据上计算出不同统计量。这种操作称之为 split-apply-combine,
6.1
数据准备
本节使用的数据描述如下:
5 只股票:AAPL, JD, BABA, FB, GS
1 年时期:从 2018-02-26 到 2019-02-26
data = pd.read_csv('1Y Stock Data.csv', parse_dates=[0], dayfirst=True)
data.head(3).append(data.tail(3))
我们目前只对 Adj Close 感兴趣,而且想知道在哪一年份或哪一月份每支股票的 Adj Close 是多少。因此我们需要做两件事:
只保留 'Date', 'Symbol' 和 ‘Adj Close‘
从 ‘Date’ 中获取 ‘Year’ 和 ‘Month’ 的信息并插入表中
将处理过后的数据存在 data1 中。
data1 = data[['Date', 'Symbol', 'Adj Close']]
data1.insert( 1, 'Year', pd.DatetimeIndex(data1['Date']).year )
data1.insert( 2, 'Month', pd.DatetimeIndex(data1['Date']).month )
data1.head(3).append(data1.tail(3))
6.2
分组 (grouping)
用某一特定标签 (label) 将数据 (data) 分组的语法如下:
data.groupBy( label )
首先我们按 Symbol 来分组:
grouped = data1.groupby('Symbol')
grouped
<pandas.core.groupby.groupby.DataFrameGroupBy
object at 0x7fbbc7248d68>
又要提起那句说了无数遍的话「万物皆对象」了。这个 grouped 也不例外,当你对如果使用某个对象感到迷茫时,用 dir() 来查看它的「属性」和「内置方法」。以下几个属性和方法是我们感兴趣的:
ngroups: 组的个数 (int)
size(): 每组元素的个数 (Series)
groups: 每组元素在原 DataFrame 中的索引信息 (dict)
get_groups(label): 标签 label 对应的数据 (DataFrame)
下面看看这些属性和方法的产出结果。
数据里有 5 只股票,因此有 5 组。
grouped.ngroups
5
一年有 252 个交易日,因此每只股票含 252 条信息。
grouped.size()
Symbol
AAPL 252
BABA 252
FB 252
GS 252
JD 252
dtype: int64
苹果股票 (AAPL) 的索引从 0 到 251,...,一直到高盛股票 (GS) 的索引从 1008 到 1259。
grouped.groups
查查 'GS' 组里的数据的前五行。
grouped.get_group('GS').head()
接下来定义个 print_groups 函数便于打印组的名字和前五行信息。
def print_groups( group_obj ):
for name, group in group_obj:
print( name )
print( group.head() )
用这个函数来调用 grouped (上面用 groupBy 得到的对象)
print_groups( grouped )
这个 print_groups 函数在下面也多次被用到。
groupBy 函数除了支持单标签分组,也支持多标签分组 (将标签放入一个列表中)。
grouped2 = data1.groupby(['Symbol', 'Year', 'Month'])
print_groups( grouped2 )
⁞
不难看出在每组左上方,有一个 (Symbol, Year, Month) 元组型的标识:
第一组:(‘AAPL’, 2018, 2)
最后一组:(‘JD’, 2019, 2)
还记得〖数据结构之 Pandas (上)〗提到的重设索引 (set_index) 的操作么?
data2 = data1.set_index(['Symbol', 'Year', 'Month'])
data2.head().append(data2.tail())
对 data1 重设索引之后,产出是一个有 multi-index 的 DataFrame,记做 data2。由于有多层索引,这时我们根据索引的 level 来分组,下面 level = 1 就是对第一层 (Year) 进行分组。
grouped3 = data2.groupby(level=1)
print_groups( grouped3 )
注意每组左上方的标识是 Year。
多层索引中的任意个数的索引也可以用来分组,下面 level = [0,2] 就是对第零层 (Symbol) 和第二层 (Month) 进行分组。
grouped4 = data2.groupby(level=[0, 2])
print_groups( grouped4 )
⁞
注意每组左上方的标识是 (Symbol, Month)。
6.3
整合 (aggregating)
做完分组之后 so what?当然是在每组做点数据分析再整合啦。
一个最简单的例子就是上节提到的 size() 函数,用 grouped 对象 (上面根据 Symbol 分组得到的) 来举例。
grouped.size()
Symbol
AAPL 252
BABA 252
FB 252
GS 252
JD 252
dtype: int64
一个更实际的例子是用 mean() 函数计算每个 Symbol 下 1 年时期的股价均值。在获取任意信息就用 DataFrame 的索引或切片那一套方法。
grouped.mean()
除了上述方法,整合还可以用内置函数 aggregate() 或 agg() 作用到「组对象」上。用 grouped4 对象 (上面根据 Symbol, Year, Month 分组得到的) 来举例。
result = grouped4.agg( np.mean )
result.head().append(result.tail())
函数 agg() 其实是一个高阶函数 (见〖Python 入门篇 (下)〗),里面的参数可以是另外一个函数,比如上例的 np.mean。上面代码对每只股票在每年每个月上求均值。
那么参数可以是另外一组函数么?可以的!
result = grouped4.agg( [np.mean, np.std] )
result.head().append(result.tail())
将 np.mean 和 np.std 放进列表中,当成是高阶函数 agg() 的参数。上面代码对每只股票在每年每个月上求均值和标准差。
既然 agg() 是高阶函数,参数当然也可以是匿名函数 (lambda 函数),下面我们定义一个对 grouped 里面每个标签下求最大值和最小值,再求差。注意 lambda 函数里面的 x 就是 grouped。
result = grouped.agg( lambda x: np.max(x)-np.min(x) )
result.head().append(result.tail())
上面代码对每只股票在 Date, Year, Month 和 Adj Close 上求「最大值」和「最小值」的差。真正有价值的信息在 Adj Close 那一栏,但我们来验证一下其他几栏。
Date: 365 days,合理,一年数据
Year: 1,合理,2019 年和 2018 年
Month: 11,合理,12 月和 1 月。
6.4
split-apply-combine
前几节做的事情的实质就是一个 split-apply-combine 的过程,如下图所示:
该 split-apply-combine 过程有三步:
根据 key 来 split 成 n 组
将函数 apply 到每个组
把 n 组的结果 combine 起来
在看具体例子之前,我们先定一个 top 函数,返回 DataFrame 某一栏中 n 个最大值。
def top( df, n=5, column='Volume' ):
return df.sort_values(by=column)[-n:]
将 top 函数用到最原始的数据 (从 csv 中读取出来的) 上。
top( data )
从上表可看出,在 Volume 栏取 5 个最大值。
在 split-apply-combine 过程中,apply 是核心。Python 本身有高阶函数 apply() 来实现它,既然是高阶函数,参数可以是另外的函数了,比如刚定义好的 top()。
将 top() 函数 apply 到按 Symbol 分的每个组上,按每个 Symbol 打印出来了 Volume 栏下的 5 个最大值。
data.groupby('Symbol').apply(top)
上面在使用 top() 时,对于 n 和 column 我们都只用的默认值 5 和 'Volumn'。如果用自己设定的值 n = 1, column = 'Adj Close',写法如下(下面使用在元数据上插入 Year 和 Month 的数据):
data1.groupby(['Symbol','Year']).apply(top, n=1, column='Adj Close')
按每个 Symbol 和 Year 打印出来了 Adj Close 栏下的最大值。
【合并数据表】用 merge 函数按数据表的共有列进行左/右/内/外合并。
【连接数据表】用 concat 函数对 Series 和 DataFrame 沿着不同轴连接。
【重塑数据表】用 stack 函数将「列索引」变成「行索引」,用 unstack 函数将「行索引」变成「列索引」。它们只是改变数据表的布局和展示方式而已。
【透视数据表】用 pivot 函数将「一张长表」变成「多张宽表」,用 melt 函数将「多张宽表」变成「一张长表」。它们只是改变数据表的布局和展示方式而已。
【分组数据表】用 groupBy 函数按不同「列索引」下的值分组。一个「列索引」或多个「列索引」就可以。
【整合数据表】用 agg 函数对每个组做整合而计算统计量。
【split-apply-combine】用 apply 函数做数据分析时美滋滋。
至此,我们已经打好 Python Basics 的基础,能用 NumPy 做数组计算,能用 SciPy 做插值、积分和优化 ,能用 Pandas 做数据分析 ,现在已经搞很多事情了。现在我们唯一欠缺的是如何画图或可视化数据,下帖从最基础的可视化工具 Matplotlib 开始讲。Stay Tuned!
按二维码关注王的机器
迟早精通机学金工量投