《利用python进行数据分析》读书笔记之数据聚合与分组的应用


本文中使用的数据集来自: 《利用python进行数据分析》数据集
经过了前两节对分组和聚合的学习,现在来进行数据拆分和聚合的小实战。现在以资源文件中的tips.csv数据集为例,首先对文件进行读取,并添加一个tip_pct(小费百分比)列:

import pandas as pd
import numpy as np

tips = pd.read_csv('../pydata-book-2nd-edition/examples/tips.csv')
tips['tip_pct'] = tips['tip']/tips['total_bill']
print(tips.head())
#    total_bill   tip smoker  day    time  size   tip_pct
# 0       16.99  1.01     No  Sun  Dinner     2  0.059447
# 1       10.34  1.66     No  Sun  Dinner     3  0.160542
# 2       21.01  3.50     No  Sun  Dinner     3  0.166587
# 3       23.68  3.31     No  Sun  Dinner     2  0.139780
# 4       24.59  3.61     No  Sun  Dinner     4  0.146808

我们首先可以写一个函数,该函数的可以返回数据集中小费百分比最高的前五项:

def top(df,n = 5,colunms = 'tip_cpt'):
    """
    返回最大的前若干项
    :param df: 数据集DataFrame
    :param n: 项数
    :param colunms:具体的排列数
    :return:排列好的数据,DataFrame形式
    """
    return df.sort_values(by = colunms)[-n:]
    
print(top(tips,n=6))
#小费百分比最高的前六组
#      total_bill   tip smoker  day    time  size   tip_pct
# 109       14.31  4.00    Yes  Sat  Dinner     2  0.279525
# 183       23.17  6.50    Yes  Sun  Dinner     4  0.280535
# 232       11.61  3.39     No  Sat  Dinner     2  0.291990
# 67         3.07  1.00    Yes  Sat  Dinner     1  0.325733
# 178        9.60  4.00    Yes  Sun  Dinner     2  0.416667
# 172        7.25  5.15    Yes  Sun  Dinner     2  0.710345

如果我们先按照smoker分组,之后给apply方法中传入top函数,则会得到以下结果:

print(tips.groupby('smoker').apply(top))
#             total_bill   tip smoker   day    time  size   tip_pct
# smoker                                                           
# No     88        24.71  5.85     No  Thur   Lunch     2  0.236746
#        185       20.69  5.00     No   Sun  Dinner     5  0.241663
#        51        10.29  2.60     No   Sun  Dinner     2  0.252672
#        149        7.51  2.00     No  Thur   Lunch     2  0.266312
#        232       11.61  3.39     No   Sat  Dinner     2  0.291990
# Yes    109       14.31  4.00    Yes   Sat  Dinner     2  0.279525
#        183       23.17  6.50    Yes   Sun  Dinner     4  0.280535
#        67         3.07  1.00    Yes   Sat  Dinner     1  0.325733
#        178        9.60  4.00    Yes   Sun  Dinner     2  0.416667
#        172        7.25  5.15    Yes   Sun  Dinner     2  0.710345

该过程可以看做先讲分好的组分别使用top函数,之后使用pandas中的cancat将得到的结果黏在一起,并使用分组名作为分组的标签。
若果想传递其他关键则,则可以把这些放在函数的最后进行传递:

print(tips.groupby(['smoker','day']).apply(top,n=1,colunms='total_bill'))
#                  total_bill    tip smoker   day    time  size   tip_pct
# smoker day                                                             
# No     Fri  94        22.75   3.25     No   Fri  Dinner     2  0.142857
#        Sat  212       48.33   9.00     No   Sat  Dinner     4  0.186220
#        Sun  156       48.17   5.00     No   Sun  Dinner     6  0.103799
#        Thur 142       41.19   5.00     No  Thur   Lunch     5  0.121389
# Yes    Fri  95        40.17   4.73    Yes   Fri  Dinner     4  0.117750
#        Sat  170       50.81  10.00    Yes   Sat  Dinner     3  0.196812
#        Sun  182       45.35   3.50    Yes   Sun  Dinner     3  0.077178
#        Thur 197       43.11   5.00    Yes  Thur   Lunch     4  0.115982

压缩分组键

在之前的例子中,得到的结果的行索引都是“分组名+起始的默认索引”,如果我们想要去掉作为第一层索引的分组名索引,可以给groupby函数中传入group_keys=False来实现:

print(tips.groupby('smoker',group_keys=False).apply(top))
#      total_bill   tip smoker   day    time  size   tip_pct
# 88        24.71  5.85     No  Thur   Lunch     2  0.236746
# 185       20.69  5.00     No   Sun  Dinner     5  0.241663
# 51        10.29  2.60     No   Sun  Dinner     2  0.252672
# 149        7.51  2.00     No  Thur   Lunch     2  0.266312
# 232       11.61  3.39     No   Sat  Dinner     2  0.291990
# 109       14.31  4.00    Yes   Sat  Dinner     2  0.279525
# 183       23.17  6.50    Yes   Sun  Dinner     4  0.280535
# 67         3.07  1.00    Yes   Sat  Dinner     1  0.325733
# 178        9.60  4.00    Yes   Sun  Dinner     2  0.416667
# 172        7.25  5.15    Yes   Sun  Dinner     2  0.710345
print(tips.groupby('smoker',as_index=False).apply(top))
#        total_bill   tip smoker   day    time  size   tip_pct
# 0 88        24.71  5.85     No  Thur   Lunch     2  0.236746
#   185       20.69  5.00     No   Sun  Dinner     5  0.241663
#   51        10.29  2.60     No   Sun  Dinner     2  0.252672
#   149        7.51  2.00     No  Thur   Lunch     2  0.266312
#   232       11.61  3.39     No   Sat  Dinner     2  0.291990
# 1 109       14.31  4.00    Yes   Sat  Dinner     2  0.279525
#   183       23.17  6.50    Yes   Sun  Dinner     4  0.280535
#   67         3.07  1.00    Yes   Sat  Dinner     1  0.325733
#   178        9.60  4.00    Yes   Sun  Dinner     2  0.416667
#   172        7.25  5.15    Yes   Sun  Dinner     2  0.710345

分位数与桶分析

我们之前介绍过cut和qcut方法,可以将数据按照箱位或者分位数进行分桶。
cut与qcut函数的使用介绍(分箱)
与groupby方法一起使用这些函数可以对数据集更方便的进行分箱或者分为分析。考虑下面这个DataFrame:

frame = pd.DataFrame({'data1':np.random.randn(1000),
                      'data2':np.random.randn(1000)})

我们使用cut函数将DataFrame中的data1按等长桶分为四份,以Categorical对象的形式返回。我们可以给groupby方法传入Categorical对象来进行对data2数据的分组,之后使用设定好的函数来进行数据的聚合:

quartiles = pd.cut(frame['data1'],4)
print(quartiles[:5])
# 0    (-1.466, -0.000143]
# 1     (-0.000143, 1.465]
# 2     (-0.000143, 1.465]
# 3     (-0.000143, 1.465]
# 4     (-0.000143, 1.465]
# Name: data1, dtype: category
# Categories (4, interval[float64]): [(-2.937, -1.466] < (-1.466, -0.000143] < (-0.000143, 1.465] <
#                                     (1.465, 2.931]]
def get_states(group):
    return {
        'min':group.min(),'max':group.max(),
        'count':group.count(),'mean':group.mean()
    }

grouped = frame['data2'].groupby(quartiles)
print(grouped.apply(get_states).unstack())
#                      count       max      mean       min
# data1
# (-2.937, -1.466]      68.0  2.015287  0.204690 -1.832583
# (-1.466, -0.000143]  414.0  3.048387 -0.028398 -3.179754
# (-0.000143, 1.465]   449.0  2.761953 -0.001406 -3.111809
# (1.465, 2.931]        69.0  2.464505 -0.016313 -2.147503

另外如果我们想要将其分为若干个等大小的组,可以使用qcut函数,在此不再赘述。

使用指定分组值填充缺失值

在处理缺失值时,我们可以使用dropna来去除缺失值或者使用fillna来替换缺失值。
常用缺失值处理函数介绍
假设我们有一个含有缺失值的Series:

s = pd.Series(np.random.randn(6))
s[::2] = np.nan

我们可以使用该Series的平均值来替换Series中的缺失值:

s.fillna(s.mean())

如果你想要按组来将缺失值替换为不同的值,比如下面这个例子中,包含4个东部城市和4个西部城市,我们想要将东部城市的缺失值用东部城市的平均值替换,将西部城市中的缺失值使用西部的平均值进行替换:

import pandas as pd
import numpy as np

np.random.seed(1234)
states = ['Ohio','New York','Vermont','Florida',
          'Oregon','Nevada','California','Idaho']
group_key = ['East'] * 4 + ['west'] * 4
# ['East', 'East', 'East', 'East', 'west', 'west', 'west', 'west']
data = pd.Series(np.random.randn(8),index=states)
data[["Vermont",'Nevada','Idaho']] = np.nan
# Ohio          0.471435
# New York     -1.190976
# Vermont            NaN
# Florida      -0.312652
# Oregon       -0.720589
# Nevada             NaN
# California    0.859588
# Idaho              NaN
# dtype: float64

想要达到之前的目的,我们可以进行以下的操作来实现:

print(data.groupby(group_key).mean())
# East   -0.344064
# west    0.069500
# dtype: float64
fill_mean = lambda g:g.fillna(g.mean())
print(data.groupby(group_key).apply(fill_mean))
# Ohio          0.471435
# New York     -1.190976
# Vermont      -0.344064
# Florida      -0.312652
# Oregon       -0.720589
# Nevada        0.069500
# California    0.859588
# Idaho         0.069500
# dtype: float64

这里发生了什么?可以分解为以下步骤:

  1. 首先goupby函数使用group_key将data分为两组
  2. g.mean()计算每组的平均值,返回一个Series
  3. 将第二部的Series传入g.fillna(),两个组分别将缺失值替换为对应的平均值
  4. 将两个组使用concat进行拼接

随机采样与排列

我们可以使用DataFrame和Series中的sample方法来进行样本的抽取,具体用法见:抽样和置换
这里说明一个具体的小应用,假设我们有一副52张的扑克牌(Series):

# 花色
suits = ['H','S','C','D']
card_val = (list(range(1,11)) + [10] * 3) * 4
base_names = ['A']+list(range(2,11))+['J','Q','K']
cards = []
for suit in suits:
    cards.extend(str(num) + suit for num in base_names)
deck = pd.Series(card_val,index=cards)
print(deck.head(10))
# AH      1
# 2H      2
# 3H      3
# 4H      4
# 5H      5
# 6H      6
# 7H      7
# 8H      8
# 9H      9
# 10H    10
# dtype: int64

从这副牌中拿出5张牌,可以写为:

print(deck.sample(5))
# 9H      9
# 10C    10
# KC     10
# 3S      3
# QH     10
# dtype: int64

如果你想要从每个花色中各抽出2张牌,可以进行如下操作:

def draw(deck,n = 5):
    return deck.sample(n)
get_suit = lambda card: card[-1] #获取花色
print(deck.groupby(get_suit).apply(draw,n = 2))
# C  5C      5
#    JC     10
# D  7D      7
#    AD      1
# H  QH     10
#    4H      4
# S  5S      5
#    10S    10
# dtype: int64

如果我们不想要一级行索引,可以写为:

print(deck.groupby(get_suit,group_keys=False).apply(draw,n = 2))
# 2C      2
# 5C      5
# 10D    10
# AD      1
# 3H      3
# 4H      4
# 4S      4
# 5S      5
# dtype: int64

分组加权平均和相关性

考虑下面这个DataFrame:

np.random.seed(1234)
df = pd.DataFrame({'category':list('aaaabbbb'),
                   'data':np.random.randn(8),
                   'weight':np.random.rand(8)})
#   category      data    weight
# 0        a  0.471435  0.958139
# 1        a -1.190976  0.875933
# 2        a  1.432707  0.357817
# 3        a -0.312652  0.500995
# 4        b -0.720589  0.683463
# 5        b  0.887163  0.712702
# 6        b  0.859588  0.370251
# 7        b -0.636524  0.561196

我们现在想要将数据分为a组和b组,并计算出各自的加权平均和,那么我们就可以进行如下的操作:

get_wavg = lambda g:np.average(g['data'],weights=g['weight'])
print(df.groupby(df['category']).apply(get_wavg))
# category
# a   -0.087454
# b    0.043322
# dtype: float64

为了进一步说明,这里我们使用另外一个从雅虎财经上获取的数据集(已附在文章开头):

close_px = pd.read_csv('../pydata-book-2nd-edition/examples/stock_px_2.csv',parse_dates=True,index_col=0)
#             AAPL   MSFT    XOM     SPX
# 2003-01-02  7.40  21.11  29.22  909.03
# 2003-01-03  7.45  21.14  29.24  908.59
# 2003-01-06  7.45  21.52  29.96  929.01
# 2003-01-07  7.43  21.93  28.95  922.93
# 2003-01-08  7.28  21.31  28.83  909.93

这个数据集中,AAPL、MSFT和XOM是三个公司的某种财经指数(不懂),SPX是当时的标普指数。假设我们想要计算出各数据的变化百分比,之后按年进行分类,计算每年各公司的财经指数和标普指数的相关性,我们可以进行如下的操作:

rets =close_px.pct_change().dropna()
# 计算变化百分比
get_year = lambda x:x.year
#用于得到每组数据的所在年份
spx_corr = lambda x:x.corrwith(x['SPX'])
#用于计算与SPX指数的相关性

by_year = rets.groupby(get_year)
#按年分组
print(by_year.apply(spx_corr))
#           AAPL      MSFT       XOM  SPX
# 2003  0.541124  0.745174  0.661265  1.0
# 2004  0.374283  0.588531  0.557742  1.0
# 2005  0.467540  0.562374  0.631010  1.0
# 2006  0.428267  0.406126  0.518514  1.0
# 2007  0.508118  0.658770  0.786264  1.0
# 2008  0.681434  0.804626  0.828303  1.0
# 2009  0.707103  0.654902  0.797921  1.0
# 2010  0.710105  0.730118  0.839057  1.0
# 2011  0.691931  0.800996  0.859975  1.0
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值