首页
学习
活动
专区
圈层
工具
发布

盘一盘 Python 系列 4 - Pandas (下)

本文是 Python 系列的第七篇

接着上篇继续后面三个章节

  • 数据表的合并和连接
  • 数据表的重塑和透视
  • 数据表的分组和整合

4

数据表的合并和连接

数据表可以按「键」合并,用 merge 函数;可以按「轴」来连接,用 concat 函数。

4.1

合并

按键 (key) 合并可以分「单键合并」和「多键合并」。

单键合并

单键合并用 merge 函数,语法如下:

pd.merge( df1, df2, how=s, on=c )

c 是 df1 和 df2 共有的一栏,合并方式 (how=s) 有四种:

  1. 左连接 (left join):合并之后显示 df1 的所有行
  2. 右连接 (right join):合并之后显示 df2 的所有行
  3. 外连接 (outer join):合并 df1 和 df2 共有的所有行
  4. 内连接 (inner join):合并所有行 (默认情况)

首先创建两个 DataFrame:

  • df_price:4 天的价格 (2019-01-01 到 2019-01-04)
  • df_volume:5 天的交易量 (2019-01-02 到 2019-01-06)
代码语言:javascript
代码运行次数:0
复制
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
代码语言:javascript
代码运行次数:0
复制
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 展示四种合并。

left join

代码语言:javascript
代码运行次数:0
复制
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 栏,因此丢弃

right join

代码语言:javascript
代码运行次数:0
复制
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 栏,因此丢弃

outer join

代码语言:javascript
代码运行次数:0
复制
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

inner join

代码语言:javascript
代码运行次数:0
复制
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 的数量
代码语言:javascript
代码运行次数:0
复制
porfolio1 = pd.DataFrame({'Asset': ['FX', 'FX', 'IR'], 
                          'Instrument': ['Option', 'Swap', 'Option'], 
                          'Number': [1, 2, 3]})
porfolio1
代码语言:javascript
代码运行次数:0
复制
porfolio2 = pd.DataFrame({'Asset': ['FX', 'FX', 'FX', 'IR'], 
                          'Instrument': ['Option', 'Option', 'Swap', 'Swap'], 
                          'Number': [4, 5, 6, 7]})
porfolio2

在 'Asset' 和 'Instrument' 两个键上做外合并。

代码语言:javascript
代码运行次数:0
复制
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 等等。

代码语言:javascript
代码运行次数:0
复制
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’ 更贴切。

代码语言:javascript
代码运行次数:0
复制
pd.merge( porfolio1, porfolio2,
          on='Asset', 
          suffixes=('1','2'))

4.2

连接

Numpy 数组可相互连接,用 np.concat;同理,Series 也可相互连接,DataFrame 也可相互连接,用 pd.concat。

连接 Series

在 concat 函数也可设定参数 axis,

  • axis = 0 (默认),沿着轴 0 (行) 连接,得到一个更长的 Series
  • axis = 1,沿着轴 1 (列) 连接,得到一个 DataFrame

被连接的 Series 它们的 index 可以重复 (overlapping),也可以不同。

overlapping index

先定义三个 Series,它们的 index 各不同。

代码语言:javascript
代码运行次数:0
复制
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。

代码语言:javascript
代码运行次数:0
复制
pd.concat([s1, s2, s3])
代码语言:javascript
代码运行次数:0
复制
a 0
b 1
c 2
d 3
e 4
f 5
g 6
dtype: int64

沿着「轴 1」连接得到一个 DataFrame。

代码语言:javascript
代码运行次数:0
复制
pd.concat([s1, s2, s3], axis=1)

non-overlapping index

将 s1 和 s3 沿「轴 0」连接来创建 s4,这样 s4 和 s1 的 index 是有重复的。

代码语言:javascript
代码运行次数:0
复制
s4 = pd.concat([s1, s3])
s4
代码语言:javascript
代码运行次数:0
复制
a 0
b 1
f 5
g 6
dtype: int64

将 s1 和 s4 沿「轴 1」内连接 (即只连接它们共有 index 对应的值)

代码语言:javascript
代码运行次数:0
复制
pd.concat([s1, s4], axis=1, join='inner')

hierarchical index

最后还可以将 n 个 Series 沿「轴 0」连接起来,再赋予 3 个 keys 创建多层 Series。

代码语言:javascript
代码运行次数:0
复制
pd.concat( [s1, s1, s3], 
           keys=['one','two','three'])
代码语言:javascript
代码运行次数:0
复制
one a 0
    b 1
two a 0
    b 1
three f 5
      g 6
dtype: int64

连接 DataFrame

连接 DataFrame 的逻辑和连接 Series 的一模一样。

沿着行连接 (axis = 0)

先创建两个 DataFrame,df1 和 df2。

代码语言:javascript
代码运行次数:0
复制
df1 = pd.DataFrame( np.arange(12).reshape(3,4), 
                    columns=['a','b','c','d'])
df1
代码语言:javascript
代码运行次数:0
复制
df2 = pd.DataFrame( np.arange(6).reshape(2,3),
                    columns=['b','d','a'])
df2

沿着行连接分两步

  1. 先把 df1 和 df2 列标签补齐
  2. 再把 df1 和 df2 纵向连起来
代码语言:javascript
代码运行次数:0
复制
pd.concat( [df1, df2] )

得到的 DataFrame 的 index = [0,1,2,0,1],有重复值。如果 index 不包含重要信息 (如上例),可以将 ignore_index 设置为 True,这样就得到默认的 index 值了。

代码语言:javascript
代码运行次数:0
复制
pd.concat( [df1, df2], ignore_index=True )

沿着列连接 (axis = 1)

先创建两个 DataFrame,df1 和 df2。

代码语言:javascript
代码运行次数:0
复制
df1 = pd.DataFrame( np.arange(6).reshape(3,2), 
                    index=['a','b','c'],
                    columns=['one','two'] )
df1
代码语言:javascript
代码运行次数:0
复制
df2 = pd.DataFrame( 5 + np.arange(4).reshape(2,2), 
                    index=['a','c'], 
                    columns=['three','four'])
df2

沿着列连接分两步

  1. 先把 df1 和 df2 行标签补齐
  2. 再把 df1 和 df2 横向连起来
代码语言:javascript
代码运行次数:0
复制
pd.concat( [df1, df2], axis=1 )

5

数据表的重塑和透视

重塑 (reshape) 和透视 (pivot) 两个操作只改变数据表的布局 (layout):

  • 重塑用 stack 和 unstack 函数 (互为逆转操作)
  • 透视用 pivot 和 melt 函数 (互为逆转操作)

5.1

重塑

在〖数据结构之 Pandas (上)〗提到过,DataFrame 和「多层索引的 Series」其实维度是一样,只是展示形式不同。而重塑就是通过改变数据表里面的「行索引」和「列索引」来改变展示形式。

  • 列索引 行索引,用 stack 函数
  • 行索引列索引,用 unstack 函数

单层 DataFrame

创建 DataFrame df (1 层行索引,1 层列索引)

代码语言:javascript
代码运行次数:0
复制
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],名称是代号
  • 列索引 = [行业, 价格, 交易量],名称是特征

stack: 列索引 行索引

列索引 (特征) 变成了行索引,原来的 DataFrame df 变成了两层 Series (第一层索引是代号,第二层索引是特征)。

代码语言:javascript
代码运行次数:0
复制
c2i_Series = df.stack()
c2i_Series
代码语言:javascript
代码运行次数:0
复制
代号 特征
JD   行业 电商
      价格 25.95
      交易量 27113291
AAPL 行业 科技
      价格 172.97
      交易量 18913154
dtype: object

思考:变成行索引的特征和原来行索引的代号之间的层次是怎么决定的?好像特征更靠内一点,代号更靠外一点。

unstack: 行索引列索引

行索引 (代号) 变成了列索引,原来的 DataFrame df 也变成了两层 Series (第一层索引是特征,第二层索引是代号)。

代码语言:javascript
代码运行次数:0
复制
i2c_Series = df.unstack()
i2c_Series
代码语言:javascript
代码运行次数:0
复制
特征 代号
行业 JD 电商
      AAPL 科技
价格 JD 25.95
      AAPL 172.97
交易量 JD 27113291
        AAPL 18913154
dtype: object

思考:变成列索引的特征和原来列索引的代号之间的层次是怎么决定的?这时好像代号更靠内一点,特征更靠外一点。

规律总结

对 df 做 stack 和 unstack 都得到了「两层 Series」,但是索引的层次不同,那么在背后的规律是什么?首先我们先来看看两个「两层 Series」的 index 包含哪些信息 (以及 df 的 index 和 columns)。

代码语言:javascript
代码运行次数:0
复制
df.index, df.columns
代码语言:javascript
代码运行次数:0
复制
c2i_Series.index
代码语言:javascript
代码运行次数:0
复制
i2c_Series.index

定义

  • r = [JD, AAPL],名称是代号
  • c = [行业, 价格, 交易量],名称是特征

那么

  • df 的行索引 = r
  • df 的列索引 = c
  • c2i_Series 的索引 = [r, c]
  • i2c_Series 的索引 = [c, r]

现在可以总结规律:

  1. 当用 stack 将 df 变成 c2i_Series 时,df 的列索引 c 加在其行索引 r 后面得到 [r, c] 做为 c2i_Series 的多层索引
  2. 当用 unstack 将 df 变成 i2c_Series 时,df 的行索引 r 加在其列索引 c 后面得到 [c, r] 做为 i2c_Series 的多层索引

基于层和名称来 unstack

对于多层索引的 Series,unstack 哪一层有两种方法来确定:

  1. 基于层 (level-based)
  2. 基于名称 (name-based)

拿 c2i_Series 举例 (读者也可以尝试 i2c_Series):

代码语言:javascript
代码运行次数:0
复制
代号 特征
JD   行业 电商
     价格 25.95
     交易量 27113291
AAPL 行业 科技
     价格 172.97
     交易量 18913154
dtype: object

1. 基于层来 unstack() 时,没有填层数,默认为最后一层。

代码语言:javascript
代码运行次数:0
复制
c2i_Series.unstack()

c2i_Series 的最后一层 (看上面它的 MultiIndex) 就是 [行业, 价格, 交易量],从行索引转成列索引得到上面的 DataFrame。


2. 基于层来 unstack() 时,选择第一层 (参数放 0)

代码语言:javascript
代码运行次数:0
复制
c2i_Series.unstack(0)

c2i_Series 的第一层 (看上面它的 MultiIndex) 就是 [JD, AAPL],从行索引转成列索引得到上面的 DataFrame。


3. 基于名称来 unstack

代码语言:javascript
代码运行次数:0
复制
c2i_Series.unstack('代号')

c2i_Series 的代号层 (看上面它的 MultiIndex) 就是 [JD, AAPL],从行索引转成列索引得到上面的 DataFrame。

多层 DataFrame

创建 DataFrame df (2 层行索引,1 层列索引)

代码语言:javascript
代码运行次数:0
复制
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 的信息

代码语言:javascript
代码运行次数:0
复制
df.index, df.columns

那么

  • df 的行索引 = [r1, r2]
  • df 的列索引 = c

1. 基于层来 unstack() 时,选择第一层 (参数放 0)

代码语言:javascript
代码运行次数:0
复制
df.unstack(0)

df 被 unstack(0) 之后变成 (行 → 列)

  • 行索引 = r2
  • 列索引 = [c, r1]

重塑后的 DataFrame 这时行索引只有一层 (代号),而列索引有两层,第一层是特征,第二层是地区


2. 基于层来 unstack() 时,选择第二层 (参数放 1)

代码语言:javascript
代码运行次数:0
复制
df.unstack(1)

df 被 unstack(1) 之后变成 (行 → 列)

  • 行索引 = r1
  • 列索引 = [c, r2]

重塑后的 DataFrame 这时行索引只有一层 (地区),而列索引有两层,第一层是地区,第二层是代号


3. 基于层先 unstack(0) 再 stack(0)

代码语言:javascript
代码运行次数:0
复制
df.unstack(0).stack(0)

df 被 unstack(0) 之后变成 (行 → 列)

  • 行索引 = r2
  • 列索引 = [c, r1]

再被 stack(0) 之后变成 (列 → 行)

  • 行索引 = [r2, c]
  • 列索引 = r1

重塑后的 DataFrame 这时行索引有两层,第一层是代号,第二层是特征,而列索引只有一层 (地区)。


4. 基于层先 unstack(0) 再 stack(1)

代码语言:javascript
代码运行次数:0
复制
df.unstack(0).stack(1)

df 被 unstack(0) 之后变成 (行 → 列)

  • 行索引 = r2
  • 列索引 = [c, r1]

再被 stack(1) 之后变成 (列 → 行)

  • 行索引 = [r2, r1]
  • 列索引 = c

重塑后的 DataFrame 这时行索引有两层,第一层是代号,第二层是地区,而列索引只有一层 (特征)。


5. 基于层先 unstack(1) 再 stack(0)

代码语言:javascript
代码运行次数:0
复制
df.unstack(1).stack(0)

df 被 unstack(1) 之后变成 (行 → 列)

  • 行索引 = r1
  • 列索引 = [c, r2]

再被 stack(0) 之后变成 (列 → 行)

  • 行索引 = [r1, c]
  • 列索引 = r2

重塑后的 DataFrame 这时行索引有两层,第一层是地区,第二层是特征,而列索引只有一层 (代号)。


6. 基于层先 unstack(1) 再 stack(1)

代码语言:javascript
代码运行次数:0
复制
df.unstack(1).stack(1)

df 被 unstack(1) 之后变成 (行 → 列)

  • 行索引 = r1
  • 列索引 = [c, r2]

再被 stack(1) 之后变成 (列 → 行)

  • 行索引 = [r1, r2]
  • 列索引 = c

重塑后的 DataFrame 这时行索引有两层,第一层是地区,第二层是特征,而列索引只有一层 (代号)。还原成原来的 df 了


7. 基于层被 stack(),没有填层数,默认为最后一层。

代码语言:javascript
代码运行次数:0
复制
df.stack()
代码语言:javascript
代码运行次数:0
复制
地区 代号 特征
中国 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() 两次,没有填层数,默认为最后一层。

代码语言:javascript
代码运行次数:0
复制
df.unstack().unstack()
代码语言:javascript
代码运行次数:0
复制
特征 代号 地区
行业 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

透视

数据源表通常只包含行和列,那么经常有重复值出现在各列下,因而导致源表不能传递有价值的信息。这时可用「透视」方法调整源表的布局用作更清晰的展示。

知识点

本节「透视」得到的数据表和 Excel 里面的透视表 (pivot table) 是一样的。透视表是用来汇总其它表的数据:

  1. 首先把源表分组,将不同值当做行 (row)、列 (column) 和值 (value)
  2. 然后对各组内数据做汇总操作如排序、平均、累加、计数等

这种动态将·「源表」得到想要「终表」的旋转 (pivoting) 过程,使透视表得以命名。

在 Pandas 里透视的方法有两种:

  • 用 pivot 函数将「一张长表」变「多张宽表」,
  • 用 melt 函数将「多张宽表」变「一张长表」,

本节使用的数据描述如下:

  • 5 只股票:AAPL, JD, BABA, FB, GS
  • 4 个交易日:从 2019-02-21 到 2019-02-26
代码语言:javascript
代码运行次数:0
复制
data = pd.read_csv('Stock.csv', parse_dates=[0], dayfirst=True)
data

从上表看出有 20 行 (5 × 4) 和 8 列,在 Date 和 Symbol 那两列下就有重复值,4 个日期和 5 个股票在 20 行中分别出现了 5 次和 4 次。

从长到宽 (pivot)

当我们做数据分析时,只关注不同股票在不同日期下的 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 来展示。

代码如下:

代码语言:javascript
代码运行次数:0
复制
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 函数返回的是两个透视表。

代码语言:javascript
代码运行次数:0
复制
data.pivot( index='Date',
            columns='Symbol',
            values=['Adj Close','Volume'] )

如果不设置 values 参数,那么 pivot 函数返回的是六个透视表。(源表 data 有八列,两列当了支点,剩下六列用来透视)

代码语言:javascript
代码运行次数:0
复制
all_pivot = data.pivot( index='Date', 
                        columns='Symbol' )
all_pivot

再继续观察下,all_pivot 实际上是个多层 DataFrame (有多层 columns)。假设我们要获取 2019-02-25 和 2019-02-26 两天的 BABA 和 FB 的开盘价,用以下「多层索引和切片」的方法。

代码语言:javascript
代码运行次数:0
复制
all_pivot['Open'].iloc[2:,1:3]

从宽到长 (melt)

pivot 逆反操作是 melt。

  • 前者将「一张长表」变成「多张宽表」
  • 后者将「多张宽表」变成「一张长表」

具体来说,函数 melt 实际是将「源表」转化成 id-variable 类型的 DataFrame,下例将

  • Date 和 Symbol 列当成 id
  • 其他列 Open, High, Low, Close, Adj Close 和 Volume 当成 variable,而它们对应的值当成 value

代码如下:

代码语言:javascript
代码运行次数:0
复制
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 上筛选出我们想要的信息,比如

代码语言:javascript
代码运行次数:0
复制
melted_data[ lambda x: (x.Date=='25/02/2019') 
                     & ((x.Symbol=='BABA')|(x.Symbol=='FB')) ]

在 melted_data 上使用调用函数 (callable function) 做索引,我们得到了在 2019-02-25 那天 BABA 和 FB 的信息。

6

数据表的分组和整合

DataFrame 中的数据可以根据某些规则分组,然后在每组的数据上计算出不同统计量。这种操作称之为 split-apply-combine,

6.1

数据准备

本节使用的数据描述如下:

  • 5 只股票:AAPL, JD, BABA, FB, GS
  • 1 年时期:从 2018-02-26 到 2019-02-26
代码语言:javascript
代码运行次数:0
复制
data = pd.read_csv('1Y Stock Data.csv', parse_dates=[0], dayfirst=True)
data.head(3).append(data.tail(3))

我们目前只对 Adj Close 感兴趣,而且想知道在哪一年份或哪一月份每支股票的 Adj Close 是多少。因此我们需要做两件事:

  1. 只保留 'Date', 'Symbol' 和 ‘Adj Close‘
  2. 从 ‘Date’ 中获取 ‘Year’ 和 ‘Month’ 的信息并插入表中

将处理过后的数据存在 data1 中。

代码语言:javascript
代码运行次数:0
复制
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 来分组:

代码语言:javascript
代码运行次数:0
复制
grouped = data1.groupby('Symbol')
grouped
代码语言:javascript
代码运行次数:0
复制
<pandas.core.groupby.groupby.DataFrameGroupBy 
object at 0x7fbbc7248d68>

又要提起那句说了无数遍的话「万物皆对象」了。这个 grouped 也不例外,当你对如果使用某个对象感到迷茫时,用 dir() 来查看它的「属性」和「内置方法」。以下几个属性和方法是我们感兴趣的:

  • ngroups: 组的个数 (int)
  • size(): 每组元素的个数 (Series)
  • groups: 每组元素在原 DataFrame 中的索引信息 (dict)
  • get_groups(label): 标签 label 对应的数据 (DataFrame)

下面看看这些属性和方法的产出结果。

数据里有 5 只股票,因此有 5 组。

代码语言:javascript
代码运行次数:0
复制
grouped.ngroups
代码语言:javascript
代码运行次数:0
复制
5

一年有 252 个交易日,因此每只股票含 252 条信息。

代码语言:javascript
代码运行次数:0
复制
grouped.size()
代码语言:javascript
代码运行次数:0
复制
Symbol
AAPL 252
BABA 252
FB 252
GS 252
JD 252
dtype: int64

苹果股票 (AAPL) 的索引从 0 到 251,...,一直到高盛股票 (GS) 的索引从 1008 到 1259。

代码语言:javascript
代码运行次数:0
复制
grouped.groups

查查 'GS' 组里的数据的前五行。

代码语言:javascript
代码运行次数:0
复制
grouped.get_group('GS').head()

接下来定义个 print_groups 函数便于打印组的名字和前五行信息。

代码语言:javascript
代码运行次数:0
复制
def print_groups( group_obj ):
    for name, group in group_obj:
        print( name )
        print( group.head() )

用这个函数来调用 grouped (上面用 groupBy 得到的对象)

代码语言:javascript
代码运行次数:0
复制
print_groups( grouped )

这个 print_groups 函数在下面也多次被用到。

多标签分组

groupBy 函数除了支持单标签分组,也支持多标签分组 (将标签放入一个列表中)。

代码语言:javascript
代码运行次数:0
复制
grouped2 = data1.groupby(['Symbol', 'Year', 'Month'])
print_groups( grouped2 )

不难看出在每组左上方,有一个 (Symbol, Year, Month) 元组型的标识:

  • 第一组:(‘AAPL’, 2018, 2)
  • 最后一组:(‘JD’, 2019, 2)

还记得〖数据结构之 Pandas (上)〗提到的重设索引 (set_index) 的操作么?

代码语言:javascript
代码运行次数:0
复制
data2 = data1.set_index(['Symbol', 'Year', 'Month'])
data2.head().append(data2.tail())

对 data1 重设索引之后,产出是一个有 multi-index 的 DataFrame,记做 data2。由于有多层索引,这时我们根据索引的 level 来分组,下面 level = 1 就是对第一层 (Year) 进行分组。

代码语言:javascript
代码运行次数:0
复制
grouped3 = data2.groupby(level=1)
print_groups( grouped3 )

注意每组左上方的标识是 Year。


多层索引中的任意个数的索引也可以用来分组,下面 level = [0,2] 就是对第零层 (Symbol) 和第二层 (Month) 进行分组。

代码语言:javascript
代码运行次数:0
复制
grouped4 = data2.groupby(level=[0, 2])
print_groups( grouped4 )

注意每组左上方的标识是 (Symbol, Month)。

6.3

整合 (aggregating)

做完分组之后 so what?当然是在每组做点数据分析再整合啦。

一个最简单的例子就是上节提到的 size() 函数,用 grouped 对象 (上面根据 Symbol 分组得到的) 来举例。

代码语言:javascript
代码运行次数:0
复制
grouped.size()
代码语言:javascript
代码运行次数:0
复制
Symbol
AAPL 252
BABA 252
FB 252
GS 252
JD 252
dtype: int64

一个更实际的例子是用 mean() 函数计算每个 Symbol 下 1 年时期的股价均值。在获取任意信息就用 DataFrame 的索引或切片那一套方法。

代码语言:javascript
代码运行次数:0
复制
grouped.mean()

除了上述方法,整合还可以用内置函数 aggregate() 或 agg() 作用到「组对象」上。用 grouped4 对象 (上面根据 Symbol, Year, Month 分组得到的) 来举例。

代码语言:javascript
代码运行次数:0
复制
result = grouped4.agg( np.mean )
result.head().append(result.tail())

函数 agg() 其实是一个高阶函数 (见〖Python 入门篇 (下)〗),里面的参数可以是另外一个函数,比如上例的 np.mean。上面代码对每只股票在每年每个月上求均值。


那么参数可以是另外一组函数么?可以的!

代码语言:javascript
代码运行次数:0
复制
result = grouped4.agg( [np.mean, np.std] )
result.head().append(result.tail())

将 np.mean 和 np.std 放进列表中,当成是高阶函数 agg() 的参数。上面代码对每只股票在每年每个月上求均值和标准差。


既然 agg() 是高阶函数,参数当然也可以是匿名函数 (lambda 函数),下面我们定义一个对 grouped 里面每个标签下求最大值和最小值,再求差。注意 lambda 函数里面的 x 就是 grouped。

代码语言:javascript
代码运行次数:0
复制
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 过程有三步:

  1. 根据 key 来 split 成 n 组
  2. 将函数 apply 到每个组
  3. 把 n 组的结果 combine 起来

在看具体例子之前,我们先定一个 top 函数,返回 DataFrame 某一栏中 n 个最大值。

代码语言:javascript
代码运行次数:0
复制
def top( df, n=5, column='Volume' ):
    return df.sort_values(by=column)[-n:]

将 top 函数用到最原始的数据 (从 csv 中读取出来的) 上。

代码语言:javascript
代码运行次数:0
复制
top( data )

从上表可看出,在 Volume 栏取 5 个最大值。

Apply 函数

在 split-apply-combine 过程中,apply 是核心。Python 本身有高阶函数 apply() 来实现它,既然是高阶函数,参数可以是另外的函数了,比如刚定义好的 top()。

将 top() 函数 apply 到按 Symbol 分的每个组上,按每个 Symbol 打印出来了 Volume 栏下的 5 个最大值。

代码语言:javascript
代码运行次数:0
复制
data.groupby('Symbol').apply(top)

上面在使用 top() 时,对于 n 和 column 我们都只用的默认值 5 和 'Volumn'。如果用自己设定的值 n = 1, column = 'Adj Close',写法如下(下面使用在元数据上插入 Year 和 Month 的数据):

代码语言:javascript
代码运行次数:0
复制
data1.groupby(['Symbol','Year']).apply(top, n=1, column='Adj Close')

按每个 Symbol 和 Year 打印出来了 Adj Close 栏下的最大值。

7

总结

【合并数据表】用 merge 函数按数据表的共有列进行左/右/内/外合并。


【连接数据表】用 concat 函数对 Series 和 DataFrame 沿着不同轴连接。

【重塑数据表】用 stack 函数将「列索引」变成「行索引」,用 unstack 函数将「行索引」变成「列索引」。它们只是改变数据表的布局和展示方式而已。


【透视数据表】用 pivot 函数将「一张长表」变成「多张宽表」,用 melt 函数将「多张宽表」变成「一张长表」。它们只是改变数据表的布局和展示方式而已。


【分组数据表】用 groupBy 函数按不同「列索引」下的值分组。一个「列索引」或多个「列索引」就可以。

【整合数据表】用 agg 函数对每个组做整合而计算统计量。

【split-apply-combine】用 apply 函数做数据分析时美滋滋。


至此,我们已经打好 Python Basics 的基础,能用 NumPy 做数组计算,能用 SciPy 做插值、积分和优化 ,能用 Pandas 做数据分析 ,现在已经搞很多事情了。现在我们唯一欠缺的是如何画图或可视化数据,下帖从最基础的可视化工具 Matplotlib 开始讲。Stay Tuned!

下一篇
举报
领券