利用Python进行数据分析:数据聚合与分组运算(基于DataFrame)

利用Python进行数据分析:数据聚合与分组运算

在将数据集加载、融合、准备好之后,通常就是计算分组统计或生成透视表。pandas提供了一个灵活高效的groupby功能,对数据集进行切片、切块、摘要等操作。

  • 使用一个或多个键(形式可以是函数、数组或DataFrame列名)分割pandas对象。
  • 计算分组的概述统计,比如数量、平均值或标准差,或是用户定义的函数。
  • 应用组内转换或其他运算,如规格化、线性回归、排名或选取子集等。
  • 计算透视表或交叉表。
  • 执行分位数分析以及其它统计分组分析。

文章目录

  • 利用Python进行数据分析:数据聚合与分组运算
    • GroupBy机制
    • 对分组进行迭代
    • 分组方式
      • 通过列名分组
      • 通过字典进行分组
      • 通过函数进行分组
      • 通过索引级别分组
    • 数据聚合
      • 统计数量
      • 描述统计
      • 组内排名及排序后筛选
      • 多函数应用
    • apply: 一般性的“拆分-应用-合并”
      • 用特定于分组的值填充缺失值
      • 用特定于分组的值矫正记录
      • 分组加权平均

GroupBy机制

下图为pandas的GroupBy机制的简单示意图:

  • split: 根据提供的一个或多个键拆分pandas对象(如,Series、DataFrame)
  • apply: 将一个函数应用到各个分组并产生一个新值
  • combine: 将函数执行结果合并到最终对象中

利用Python进行数据分析:数据聚合与分组运算(基于DataFrame)_第1张图片

# 导入包
import pandas as pd
import numpy as np
df = pd.DataFrame({'key1' : ['a', 'a', 'b', 'b', 'a'],
                   'key2' : ['one', 'two', 'one', 'two', 'one'],
                   'data1' : np.random.randn(5),
                   'data2' : np.random.randn(5)})
df
key1 key2 data1 data2
0 a one 0.115582 0.283518
1 a two -0.413824 -0.333613
2 b one -0.138182 -0.462852
3 b two 0.887625 0.756710
4 a one 1.404586 -0.084644

对分组进行迭代

GroupBy对象支持迭代,可以产生一组二元元组(由分组名和数据块组成)。可以对数据片段执行自定义操作。

for (k1,k2), group in df.groupby(['key1','key2']):
    print((k1,k2))
    print(group)
('a', 'one')
  key1 key2     data1     data2
0    a  one  0.115582  0.283518
4    a  one  1.404586 -0.084644
('a', 'two')
  key1 key2     data1     data2
1    a  two -0.413824 -0.333613
('b', 'one')
  key1 key2     data1     data2
2    b  one -0.138182 -0.462852
('b', 'two')
  key1 key2     data1    data2
3    b  two  0.887625  0.75671

groupby默认是在axis=0上进行分组的,通过设置也可以在其他任何轴上进行分组。拿上面例子中的df来说,我们可以根据dtype对列进行分组:

grouped = df.groupby(df.dtypes, axis=1)
for dtype, group in grouped:
    print(dtype)
    print(group)
float64
      data1     data2
0  0.115582  0.283518
1 -0.413824 -0.333613
2 -0.138182 -0.462852
3  0.887625  0.756710
4  1.404586 -0.084644
object
  key1 key2
0    a  one
1    a  two
2    b  one
3    b  two
4    a  one

分组方式

通过列名分组

通常,分组信息就位于相同的要处理DataFrame中。上一小节中的示例即是将列名(可以是字符串、数字或其他Python对象)用作分组键。

通过字典进行分组

现在,假设已知列的分组关系,并希望根据分组计算列的和:

people = pd.DataFrame(np.random.randn(5, 5),
                      columns=['a', 'b', 'c', 'd', 'e'],
                      index=['Joe', 'Steve', 'Wes', 'Jim', 'Travis'])
people
a b c d e
Joe 0.577199 -0.017674 0.822495 0.236686 -0.186748
Steve 0.403552 -1.162806 0.123285 -0.534835 -1.333789
Wes 2.398688 -1.268945 0.134579 1.204143 0.569019
Jim 0.493204 1.242734 0.399500 1.080515 -3.530037
Travis 0.691376 -0.239679 1.542502 1.445370 0.035521

可以通过字典指定列的分组关系:

mapping = {'a': 'red', 'b': 'red', 'c': 'blue',
           'd': 'blue', 'e': 'red', 'f' : 'orange'}
people.groupby(mapping, axis =1).sum()
blue red
Joe 1.059181 0.372778
Steve -0.411549 -2.093043
Wes 1.338722 1.698763
Jim 1.480015 -1.794099
Travis 2.987872 0.487218

通过函数进行分组

任何被当做分组键的函数都会在各个索引值上被调用一次,其返回值就会被用作分组名称。比如,在上一小节的DataFrame示例中,其索引值为人名,可以根据其字符串长度进行分组,传入len函数即可:

people.groupby(len).sum()
a b c d e
3 -3.875764 2.603252 2.365206 -2.131822 0.033895
5 0.197823 0.324744 -0.465613 1.005641 0.491256
6 1.530411 -0.863593 -1.362848 -0.701741 -0.307557

此外,也可以将函数跟数组、列表、字典、Series混合使用。

通过索引级别分组

对于层次化索引数据集,可以根据轴索引的一个级别进行聚合:

columns = pd.MultiIndex.from_arrays([['US', 'US', 'US', 'JP', 'JP'],
                                     [1, 3, 5, 1, 3]],
                                    names=['cty', 'tenor'])
hier_df = pd.DataFrame(np.random.randn(4, 5), columns=columns)
hier_df
cty US JP
tenor 1 3 5 1 3
0 -1.230404 1.182842 0.091481 3.029558 0.422223
1 -0.848426 1.463393 -0.220321 -0.576434 1.587866
2 0.036692 -1.020126 0.247783 0.460074 -0.047801
3 -0.275390 -0.554516 -1.301136 0.079235 0.502412

要根据级别分组,使用level关键字传递级别序号或名字:

hier_df.groupby(level='cty', axis=1).count()
cty JP US
0 2 3
1 2 3
2 2 3
3 2 3

数据聚合

聚合指的是任何能够从数组产生标量值的数据转换过程。

选项 说明
count 分组中非NA值的数量
nunique 分组中非重复值的数量(NA值也统计)
unique 分组中非重复值(NA值也统计)
sum 非NA值的和
mean 非NA值的平均值
median 非NA值的算术中位数
std、var 无偏(分母为n-1)标准差和方差
min、max 非NA值的最小值和最大值
prod 非NA值的积
first、last 非NA值的第一个和最后一个

下面具体列出一些常用示例:

统计数量

统计分组后,各组内数量:GroupBy的size方法,它可以返回一个含有分组大小的Series。

比如,以下使用key1、key2列对df进行分组,并统计每组记录数量:

df
key1 key2 data1 data2
0 a one -0.213740 0.082804
1 a two -0.871891 -0.232010
2 b one 0.454973 0.246116
3 b two -0.702433 1.957607
4 a one -1.136174 1.726216
df.groupby(['key1','key2']).size()
key1  key2
a     one     2
      two     1
b     one     1
      two     1
dtype: int64

使用reset_index()可以将结果转化为DataFrame形式:

df.groupby(['key1','key2']).size().reset_index()
key1 key2 0
0 a one 2
1 a two 1
2 b one 1
3 b two 1

描述统计

df.groupby('key1')['data1'].describe()
count mean std min 25% 50% 75% max
key1
a 3.0 -0.740601 0.475025 -1.136174 -1.004032 -0.871891 -0.542815 -0.213740
b 2.0 -0.123730 0.818410 -0.702433 -0.413082 -0.123730 0.165621 0.454973

组内排名及排序后筛选

比如,以下记录为学生英语四六级考试成绩数据,选取学号(xh)、考试时间(kssj)、英语级别(yyjb)、总分(zf)四列,并筛选少量记录用于示例:

score = pd.read_csv('xs_yysljkscjsj.csv', usecols=['xh','kssj','yyjb','zf'],low_memory=False)
# 仅保留英语四六级考试成绩
score = score[score.yyjb.isin(['CET4','CET6'])]
# 随机选择5名学生
score = score[score.xh.isin(score.xh.sample(5))]
score.sort_values('xh').head(10)
yyjb xh kssj zf
46568 CET4 0992447 201006 488
49582 CET6 0992447 201012 390
58822 CET6 0992447 201106 396
64551 CET6 0992447 201112 421
80369 CET6 0992447 201212 416
127382 CET6 1143140 201412 356
109293 CET6 1143140 201312 319
118854 CET6 1143140 201406 333
92735 CET6 1143140 201212 327
75478 CET4 1143140 201206 439

对于同一名学生、同一外语水平考试,存在多次考试的情况:

score.groupby(['xh','yyjb']).size()
xh        yyjb
0992447   CET4    1
          CET6    4
1143140   CET4    1
          CET6    5
1932108   CET4    1
          CET6    2
1957104   CET4    1
          CET6    3
q0151111  CET6    1
dtype: int64

场景一: 每名学生、每门考试仅保留最高成绩记录。可以先根据总分进行排序,后执行分组及数据聚合操作。

score.sort_values('zf',ascending=False).groupby(['xh','yyjb']).first()
kssj zf
xh yyjb
0992447 CET4 201006 488
CET6 201112 421
1143140 CET4 201206 439
CET6 201412 356
1932108 CET4 202012 466
CET6 202106 311
1957104 CET4 201912 500
CET6 202112 539
q0151111 CET6 200506 0

如果希望选取最高的几条记录,可以使用head()命令:

score.sort_values('zf',ascending=False).groupby(['xh','yyjb']).head(2)
yyjb xh kssj zf
283135 CET6 1957104 202112 539
197433 CET4 1957104 201912 500
279294 CET6 1957104 202106 498
46568 CET4 0992447 201006 488
266499 CET4 1932108 202012 466
75478 CET4 1143140 201206 439
64551 CET6 0992447 201112 421
80369 CET6 0992447 201212 416
127382 CET6 1143140 201412 356
118854 CET6 1143140 201406 333
276130 CET6 1932108 202106 311
251893 CET6 q0151111 200506 0
282211 CET6 1932108 202112 0

场景二:每名学生、每门考试仅保留最新的一次成绩记录。可以先根据时间戳进行排序,后执行分组及数据聚合操作。

score.sort_values('kssj',ascending = False).groupby(['xh','yyjb']).first()
kssj zf
xh yyjb
0992447 CET4 201006 488
CET6 201212 416
1143140 CET4 201206 439
CET6 201412 356
1932108 CET4 202012 466
CET6 202112 0
1957104 CET4 201912 500
CET6 202112 539
q0151111 CET6 200506 0

场景三:计算每门考试的成绩排名。根据英语级别分组后,在组内使用rank()函数。

score['rank'] = score.groupby('yyjb')['zf'].rank(ascending = False)
score[score.yyjb == 'CET4']
yyjb xh kssj zf rank
46568 CET4 0992447 201006 488 2.0
75478 CET4 1143140 201206 439 4.0
197433 CET4 1957104 201912 500 1.0
266499 CET4 1932108 202012 466 3.0

多函数应用

希望对不同的列使用不同的聚合函数,或一次应用多个函数,需要使用agg()方法。

一次应用多个函数:如果传入一组函数或函数名,得到的DataFrame的列就会以相应的函数命名:

grouped = score.groupby('yyjb')
functions = ['mean', 'median', 'count']
grouped['zf'].agg(functions)
mean median count
yyjb
CET4 473.25 477 4
CET6 316.20 356 15

如果传入的是一个由(name,function)元组组成的列表,则各元组的第一个元素就会被用作DataFrame的列名(可以将这种二元元组列表看做一个有序映射):

tfunctions = [('平均分','mean'),('中位数','median'), ('记录数','count')]
grouped['zf'].agg(tfunctions)
平均分 中位数 记录数
yyjb
CET4 473.25 477 4
CET6 316.20 356 15

对不同的列应用不同的函数。具体的办法是向agg传入一个从列名映射到函数的字典:

grouped.agg({
    'zf': ['max','min','mean'],
    'kssj': 'nunique'
})
zf kssj
max min mean nunique
yyjb
CET4 500 439 473.25 4
CET6 539 0 316.20 12

apply: 一般性的“拆分-应用-合并”

自定义函数,通过apply命令应用于各分组。

用特定于分组的值填充缺失值

假设你需要对不同的分组填充不同的值。一种方法是将数据分组,并使用apply和一个能够对各数据块调用fillna的函数即可。

states = ['Ohio', 'New York', 'Vermont', 'Florida',
          'Oregon', 'Nevada', 'California', 'Idaho']
regions = ['East'] * 4 + ['West'] * 4
data = pd.DataFrame({'data':np.random.randn(8),
                     'region':regions},
                    index=states)
data.loc[['Vermont', 'Nevada', 'Idaho'],'data'] = np.nan
data
data region
Ohio 1.393924 East
New York -0.522025 East
Vermont NaN East
Florida 0.484822 East
Oregon 1.547784 West
Nevada NaN West
California -0.953454 West
Idaho NaN West

使用同一地区的平均值填补缺失值,可以使用"region"字段分组,再用分组平均值填充NA值,定义缺失值填充函数:

fill_mean = lambda g:g.fillna(g.mean())
data.groupby('region').apply(fill_mean)
data region
region
East Ohio 1.393924 East
New York -0.522025 East
Vermont 0.452240 East
Florida 0.484822 East
West Oregon 1.547784 West
Nevada 0.297165 West
California -0.953454 West
Idaho 0.297165 West

从上面的例子中可以看出,分组键会跟原始对象的索引共同构成结果对象中的层次化索引。将group_keys=False传入groupby即可禁止该效果:

data.groupby('region',group_keys=False).apply(fill_mean)
data region
Ohio 1.393924 East
New York -0.522025 East
Vermont 0.452240 East
Florida 0.484822 East
Oregon 1.547784 West
Nevada 0.297165 West
California -0.953454 West
Idaho 0.297165 West

用特定于分组的值矫正记录

另一个类似于上一小节的应用场景时,数据在填写或转录过程存在错误或缺失,希望使用同一组内最高频出现的值填补缺失值:

data['region_code'] = [1,1,1,np.nan,0,0,0,0]
data
data region region_code
Ohio 1.393924 East 1.0
New York -0.522025 East 1.0
Vermont NaN East 1.0
Florida 0.484822 East NaN
Oregon 1.547784 West 0.0
Nevada NaN West 0.0
California -0.953454 West 0.0
Idaho NaN West 0.0

下面希望,使用同一"region"出现次数最多的"region_code"对缺失值进行填充:

fill_mod = lambda g:g.fillna(g.value_counts().index[0])
data.groupby('region')['region_code'].apply(fill_mod)
Ohio          1.0
New York      1.0
Vermont       1.0
Florida       1.0
Oregon        0.0
Nevada        0.0
California    0.0
Idaho         0.0
Name: region_code, dtype: float64

有的时候,存在数据填写的错误,比如如下Oregon的区域码填写成了1。

data['region_code'] = [1,1,1,1,1,0,0,0]
data
data region region_code
Ohio 1.393924 East 1
New York -0.522025 East 1
Vermont NaN East 1
Florida 0.484822 East 1
Oregon 1.547784 West 1
Nevada NaN West 0
California -0.953454 West 0
Idaho NaN West 0

下面希望,同一"region"的"region_code"应保持一致,注意此处使用的是transform功能:

data.groupby('region')['region_code'].transform(lambda g:g.value_counts().index[0])
Ohio          1
New York      1
Vermont       1
Florida       1
Oregon        0
Nevada        0
California    0
Idaho         0
Name: region_code, dtype: int64

分组加权平均

根据groupby的“拆分-应用-合并”范式,可以进行DataFrame的列与列之间或两个Series之间的运算(比如分组加权平均)。

 df = pd.DataFrame({'category': ['a', 'a', 'a', 'a',
                                 'b', 'b', 'b', 'b'],
                    'data': np.random.randn(8),
                    'weights': np.random.rand(8)})
df
category data weights
0 a 0.442123 0.173591
1 a -0.033089 0.269083
2 a 0.813126 0.440622
3 a -0.653577 0.526061
4 b -0.363492 0.471360
5 b 0.759573 0.619617
6 b 0.301565 0.453474
7 b -1.908321 0.596430

然后可以利用category计算分组加权平均数:

def get_wavg(g,value,weight):
    return np.average(g[value], weights=g[weight])
grouped = df.groupby('category')

注意下方为在apply函数中传入参数的方法:

grouped.apply(get_wavg,'data','weights')
category
a    0.058399
b   -0.327958
dtype: float64

往期:
利用Python进行数据分析:准备工作
利用Python进行数据分析:缺失数据(基于DataFrame)
利用Python进行数据分析:数据转换(基于DataFrame)
利用Python进行数据分析:数据规整(基于DataFrame)

你可能感兴趣的:(数据分析,数据分析,python,数据挖掘)