2020-08-08--Pandas-09--分组聚合

import numpy as np
import pandas as pd

index = pd.Index(data=["Tom", "Bob", "Mary", "James", "Andy", "Alice"], name="name")

data = {
    "age": [18, 30, 35, 18, np.nan, 30],
    "city": ["Bei Jing ", "Shang Hai ", "Guang Zhou", "Shen Zhen", np.nan, " "],
    "sex": ["male", "male", "female", "male", np.nan, "female"],
    "income": [3000, 8000, 8000, 4000, 6000, 7000]
}

user_info = pd.DataFrame(data=data, index=index)
print(user_info)
#         age        city     sex  income
# name                                   
# Tom    18.0   Bei Jing     male    3000
# Bob    30.0  Shang Hai     male    8000
# Mary   35.0  Guang Zhou  female    8000
# James  18.0   Shen Zhen    male    4000
# Andy    NaN         NaN     NaN    6000
# Alice  30.0              female    7000

将对象分割成组

在进行分组统计前,首先要做的就是进行分组。既然是分组,就需要依赖于某个信息。
比如,依据性别来分组。直接调用
user_info.groupby(user_info["sex"])或者user_info.groupby("sex")即可完成按照性别分组。

在该分组的过程中,分组值会忽略NaN,None,NaT。

grouped  = user_info.groupby(user_info["sex"])
print(grouped.groups)
# {'female': ['Mary', 'Alice'], 'male': ['Tom', 'Bob', 'James']}

grouped  = user_info.groupby("sex")
print(grouped.groups)
# {'female': ['Mary', 'Alice'], 'male': ['Tom', 'Bob', 'James']}
print(type(grouped.groups))
# <class 'pandas.io.formats.printing.PrettyDict'>
连续分组

你可能会想,能不能先按照性别来分组,再按照年龄进一步分组呢?答案是可以的

grouped  = user_info.groupby(["sex","age"])
print(grouped.groups)
# {
# ('male', 18.0): ['Tom', 'James'], 
# ('male', 30.0): ['Bob'],
#  ('female', 35.0): ['Mary'], 
# (nan, nan): ['Andy'], 
# ('female', 30.0): ['Alice']
}

grouped是一个DataFrame分组对象

关闭排序

默认情况下,groupby 会在操作过程中对数据进行排序。如果为了更好的性能,可以设置 sort=False。

grouped  = user_info.groupby(["sex","age"],sort=False)
print(grouped.groups)
# {('male', 18.0): ['Tom', 'James'], ('male', 30.0): ['Bob'], ('female', 35.0): ['Mary'], (nan, nan): ['Andy'], ('female', 30.0): ['Alice']}

选择列

在使用 groupby 进行分组后,可以使用切片 [] 操作来完成对某一列的选择。

c = user_info.groupby('sex')
print(c)
# <pandas.core.groupby.generic.DataFrameGroupBy object at 0x000002423A2C9D88>
print(c['city'])
# <pandas.core.groupby.generic.SeriesGroupBy object at 0x000002423A2C9A48>

遍历分组

在对数据进行分组后,可以进行遍历。

grouped  = user_info.groupby("sex")

for name, group in grouped:
    print("name: {}".format(name))
    print("group: {}".format(group))
    print("--------------")
# name: female
# group:         age        city     sex  income
# name
# Mary   35.0  Guang Zhou  female    8000
# Alice  30.0              female    7000
# --------------
# name: male
# group:         age        city   sex  income
# name
# Tom    18.0   Bei Jing   male    3000
# Bob    30.0  Shang Hai   male    8000
# James  18.0   Shen Zhen  male    4000
# --------------

遍历后每一项的group都是一个DataFrame对象,name是分组的名称。

如果是根据多个字段来分组的,每个组的名称是一个元组。

grouped  = user_info.groupby(["sex", "age"])

for name, group in grouped:
    print("name: {}".format(name))
    print("group: {}".format(group))
    print("--------------")
# name: ('female', 30.0)
# group:         age city     sex  income
# name                            
# Alice  30.0       female    7000
# --------------
# name: ('female', 35.0)
# group:        age        city     sex  income
# name                                  
# Mary  35.0  Guang Zhou  female    8000
# --------------
# name: ('male', 18.0)
# group:         age       city   sex  income
# name                                
# Tom    18.0  Bei Jing   male    3000
# James  18.0  Shen Zhen  male    4000
# --------------
# name: ('male', 30.0)
# group:        age        city   sex  income
# name                                
# Bob   30.0  Shang Hai   male    8000
# --------------

选择一个组

分组后,我们可以通过 get_group 方法来选择其中的某一个组。

1.获取male分组的数据,返回DataFrame对象

grouped  = user_info.groupby("sex")
c = grouped.get_group("male")
print(c)
#         age        city   sex  income
# name                                 
# Tom    18.0   Bei Jing   male    3000
# Bob    30.0  Shang Hai   male    8000
# James  18.0   Shen Zhen  male    4000

2.多个分组条件时
获取male分组以及age=18分组数据。

c = user_info.groupby(["sex", "age"]).get_group(("male", 18))
print(c)
#         age       city   sex  income
# name
# Tom    18.0  Bei Jing   male    3000
# James  18.0  Shen Zhen  male    4000

多个分组条件时,在选择一个分组的时候,参数必须完整,(每个分组条件的值都要有)

聚合

分组的目的是为了统计,统计的时候需要聚合,所以我们需要在分完组后来看下如何进行聚合。常见的一些聚合操作有:计数、求和、最大值、最小值、平均值等。

agg()

想要实现聚合操作,一种方式就是调用 agg 方法。
1.获取不同分类下所包含的数量

c = user_info.groupby('sex')
# print(c)
x = c.agg(len)
print(x)
#         age  city  income
# sex
# female  2.0     2       2
# male    3.0     3       3


# 获取不同性别下所包含的人数
grouped = user_info.groupby("sex")
c = grouped.agg(len).age
print(c)
# sex
# female    2.0
# male      3.0
# Name: age, dtype: float64

2.获取不同分类下age的最大值

c = grouped.agg(max).age
print(c)
# sex
# female    35.0
# male      30.0
# Name: age, dtype: float64

3.如果是根据多个键来进行聚合,默认情况下得到的结果是一个多层索引结构。

grouped = user_info.groupby(["sex", "age"])
rs = grouped.agg(len)
print(rs)
#              city  income
# sex    age
# female 30.0     1       1
#        35.0     1       1
# male   18.0     2       2
#        30.0     1       1
grouped = user_info.groupby(["sex", "age"])
rs = grouped.agg(len)
print(rs)
#              city  income
# sex    age
# female 30.0     1       1
#        35.0     1       1
# male   18.0     2       2
#        30.0     1       1
print(rs.reset_index())
#       sex   age  city  income
# 0  female  30.0     1       1
# 1  female  35.0     1       1
# 2    male  18.0     2       2
# 3    male  30.0     1       1

有两种方式可以避免出现多层索引,上边是第一种。对包含多层索引的对象调用 reset_index 方法。

另外一种方式是在分组时,设置参数 as_index=False。

grouped = user_info.groupby(["sex", "age"], as_index=False)
c = grouped.agg(len)
print(c)
#       sex   age  city  income
# 0  female  30.0     1       1
# 1  female  35.0     1       1
# 2    male  18.0     2       2
# 3    male  30.0     1       1

4.Series 和 DataFrame 都包含了 describe 方法,我们分组后一样可以使用 describe 方法来查看数据的情况。

grouped = user_info.groupby("sex")
c = grouped.describe()
print(c)
#          age                        ...  income                        
#        count  mean       std   min  ...     25%     50%     75%     max
# sex                                 ...                                
# female   2.0  32.5  3.535534  30.0  ...  7250.0  7500.0  7750.0  8000.0
# male     3.0  22.0  6.928203  18.0  ...  3500.0  4000.0  6000.0  8000.0

一次应用多个聚合操作

有时候进行分组后,不单单想得到一个统计结果,有可能是多个。比如想统计出不同性别下的一个收入的总和和平均值。

grouped = user_info.groupby("sex")
c = grouped['income'].agg([np.sum, np.mean])
print(c)
#           sum  mean
# sex
# female  15000  7500
# male    15000  5000
c = grouped.agg([np.sum, np.mean])
print(c)
#          age       income      
#          sum  mean    sum  mean
# sex                            
# female  65.0  32.5  15000  7500
# male    66.0  22.0  15000  5000

如果想将统计结果进行重命名,可以传入字典。

grouped = user_info.groupby("sex")
c = grouped["income"].agg([np.sum, np.mean]).rename(columns={"sum": "income_sum", "mean": "income_mean"})
print(c)
#         income_sum  income_mean
# sex                            
# female       15000         7500
# male         15000         5000

对DataFrame列应用不同的聚合操作

有时候可能需要对不同的列使用不同的聚合操作。例如,想要统计不同性别下人群的年龄的均值以及收入的总和。

grouped = user_info.groupby("sex")
c = grouped.agg({"age": np.mean, "income": np.sum}).rename(columns={"age": "age_mean", "income": "income_sum"})
print(c)
#         age_mean  income_sum
# sex                         
# female      32.5       15000
# male        22.0       15000

transform 操作

前面进行聚合运算的时候,得到的结果是一个以分组名作为索引的结果对象。虽然可以指定 as_index=False ,但是得到的索引也并不是元数据的索引。如果我们想使用原数组的索引的话,就需要进行 merge 转换。
transform方法简化了这个过程,它会把 func 参数应用到所有分组,然后把结果放置到原数组的索引上(如果结果是一个标量,就进行广播)


# 通过 agg 得到的结果的索引是分组名
grouped = user_info.groupby("sex")
c = grouped["income"].agg(np.mean)
print(c)
# sex
# female    7500
# male      5000
# Name: income, dtype: int64

# 不改变数据的索引,但是数据的值为平均值
c = grouped['income'].transform(np.mean)
print(c)
# name
# Tom      5000.0
# Bob      5000.0
# Mary     7500.0
# James    5000.0
# Andy        NaN
# Alice    7500.0
# Name: income, dtype: float64

apply 操作

除了 transform 操作外,还有更神奇的 apply 操作。

apply 会将待处理的对象拆分成多个片段,然后对各片段调用传入的函数,最后尝试用 pd.concat() 把结果组合起来。func 的返回值可以是 Pandas 对象或标量,并且数组对象的大小不限。

1.比如想要统计不同性别最高收入的前n个值,可以通过下面这种方式实现。

grouped = user_info.groupby("sex")
 
def f1(ser, num=2):
    return ser.nlargest(num).tolist()

c = grouped["income"].apply(f1)
print(c)
# sex
# female    [8000, 7000]
# male      [8000, 4000]
# Name: income, dtype: object

另外,如果想要获取不同性别下的年龄的均值,通过 apply 可以如下实现。


apply()是对其他操作的扩展,因为使用agg()时,只能使用其内置的函数,而apply()可以自定义函数。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342