11.3 数据分组与聚合(入门)¶

在数据分析中,我们常通过“分组(groupby) + 聚合(aggregate)”来比较不同类别的统计特征。
例如:比较不同地区、性别、职业的平均成绩、数量、占比等。

学习目标¶

  • 了解 分组-应用-合并(split–apply–combine) 的基本思想。
  • 掌握 groupby() 的常见用法:单列/多列分组、as_index、size vs count、nunique。
  • 会使用 agg() 执行多指标聚合,包含命名聚合与按列指定函数。
  • 会用 value_counts()、crosstab()、pivot_table() 做频次与透视分析。
  • 能计算占比与通过率等实用指标并解释结果。

11.3.1 语法要点(常用就这些)¶

  • 分组:df.groupby('列名') 或 df.groupby(['列1','列2'])
  • 基本聚合:mean(), sum(), count(), max(), min(), median(), std(), nunique()
  • 多函数聚合:df.groupby('地区')['成绩'].agg(['mean','max','min'])
  • 按列指定函数:df.groupby('地区').agg({'成绩':['mean','max'], '姓名':'nunique'})
  • 命名聚合(推荐,列名更清晰):
    df.groupby('地区').agg(
        平均分=('成绩','mean'),
        最高分=('成绩','max'),
        人数=('姓名','count')
    )
    
  • size() vs count():
    • size() 统计组内行数(不管是否缺失);
    • count() 统计非缺失值的数量(按列)。
  • as_index:groupby(..., as_index=False) 可让分组键变成普通列,便于后续合并与展示。
  • 频次:value_counts()(一维),pd.crosstab()(二维频次表)。
  • 透视表:pivot_table(values=..., index=..., columns=..., aggfunc=...)

11.3.2 示范:学生成绩按地区分组¶

In [1]:
import pandas as pd

# 创建示例数据
data = {
    '姓名': ['张三', '李四', '王五', '赵六', '孙七'],
    '地区': ['北方', '北方', '南方', '南方', '北方'],
    '成绩': [85, 80, 90, 92, 88]
}
df = pd.DataFrame(data)

# 按地区分组,计算平均成绩
mean_scores = df.groupby('地区')['成绩'].mean()
mean_scores
C:\Users\Zhouq\AppData\Roaming\Python\Python39\site-packages\pandas\core\computation\expressions.py:21: UserWarning: Pandas requires version '2.8.4' or newer of 'numexpr' (version '2.8.3' currently installed).
  from pandas.core.computation.check import NUMEXPR_INSTALLED
C:\Users\Zhouq\AppData\Roaming\Python\Python39\site-packages\pandas\core\arrays\masked.py:60: UserWarning: Pandas requires version '1.3.6' or newer of 'bottleneck' (version '1.3.5' currently installed).
  from pandas.core import (
Out[1]:
地区
北方    84.333333
南方    91.000000
Name: 成绩, dtype: float64

11.3.3 常见聚合函数示例¶

In [2]:
# 多种聚合函数
agg_result = df.groupby('地区')['成绩'].agg(['mean', 'max', 'min', 'count', 'std'])
agg_result
Out[2]:
mean max min count std
地区
北方 84.333333 88 80 3 4.041452
南方 91.000000 92 90 2 1.414214

命名聚合(更易读的列名)¶

In [3]:
agg_named = df.groupby('地区', as_index=False).agg(
    平均分=('成绩','mean'),
    最高分=('成绩','max'),
    最低分=('成绩','min'),
    人数=('姓名','count')
)
agg_named
Out[3]:
地区 平均分 最高分 最低分 人数
0 北方 84.333333 88 80 3
1 南方 91.000000 92 90 2

11.3.4 举例(贴近人文社科)¶

例 1:多列分组(地区 × 性别)比较平均分与人数¶

In [4]:
df2 = pd.DataFrame({
    '姓名': ['张三','李四','王五','赵六','孙七','周八','吴九','郑十'],
    '地区': ['北方','北方','南方','南方','北方','南方','南方','北方'],
    '性别': ['男','女','男','女','男','男','女','女'],
    '成绩': [85,80,90,92,88,76,95,83]
})
group2 = df2.groupby(['地区','性别']).agg(
    平均分=('成绩','mean'),
    人数=('姓名','count')
)
group2
Out[4]:
平均分 人数
地区 性别
北方 女 81.5 2
男 86.5 2
南方 女 93.5 2
男 83.0 2

例 2:频次统计与占比(value_counts + crosstab)¶

  • 统计不同地区学生的数量与占比。
  • 构造二项分类(是否及格 ≥ 60 分)并计算分组通过率。
In [5]:
# 频次与占比
地区频次 = df2['地区'].value_counts()
地区占比 = df2['地区'].value_counts(normalize=True)  # 归一化为比例
地区频次, 地区占比
Out[5]:
(地区
 北方    4
 南方    4
 Name: count, dtype: int64,
 地区
 北方    0.5
 南方    0.5
 Name: proportion, dtype: float64)
In [6]:
# 是否及格(>=60)
df2['是否及格'] = (df2['成绩'] >= 60).astype(int)

# 分组通过率:方式1 - groupby + mean(因为 1/0 的均值就是比例)
pass_rate1 = df2.groupby('地区', as_index=False)['是否及格'].mean().rename(columns={'是否及格':'通过率'})
pass_rate1
Out[6]:
地区 通过率
0 北方 1.0
1 南方 1.0
In [7]:
# 分组通过率:方式2 - crosstab 归一化
table = pd.crosstab(df2['地区'], df2['是否及格'], normalize='index')
table.rename(columns={0:'未及格占比',1:'及格占比'}, inplace=True)
table
Out[7]:
是否及格 及格占比
地区
北方 1.0
南方 1.0

例 3:agg 按列指定不同函数(含去重人数)¶

In [8]:
agg_mix = df2.groupby('地区').agg({
    '成绩':['mean','median','max'],
    '姓名':'nunique'  # 去重后的人数(考虑同名时可换用唯一ID)
})
agg_mix
Out[8]:
成绩 姓名
mean median max nunique
地区
北方 84.00 84.0 88 4
南方 88.25 91.0 95 4

例 4:pivot_table 做“地区 × 性别”的平均成绩透视表¶

In [9]:
pivot_avg = pd.pivot_table(
    df2,
    values='成绩',
    index='地区',
    columns='性别',
    aggfunc='mean'
)
pivot_avg
Out[9]:
性别 女 男
地区
北方 81.5 86.5
南方 93.5 83.0

例 5:自定义聚合(例如“通过率”“分位数差(IQR)”等)¶

In [10]:
import numpy as np

def pass_rate(x):
    return (x >= 60).mean()

def iqr(x):
    q1 = np.percentile(x, 25)
    q3 = np.percentile(x, 75)
    return q3 - q1

custom_agg = df2.groupby('地区').agg(
    通过率=('成绩', pass_rate),
    四分位距=('成绩', iqr)
)
custom_agg
Out[10]:
通过率 四分位距
地区
北方 1.0 3.50
南方 1.0 6.25

11.3.5 常用函数方法速查(表)¶

函数 说明
mean() 平均值
median() 中位数
mode() 众数
std() 标准差
var() 方差
cov() 协方差
sum() 总和
min(), max() 最小/最大值
groupby() 数据分组
agg() 多函数聚合
value_counts() 各值出现频次/占比
pivot_table() 数据透视表
quantile() 分位数
skew() 偏态
kurtosis()/kurt() 峰度
unique() 唯一值数组
nunique() 唯一值数量
rank() 排名
rolling() 滑动窗口
diff() 差分
pct_change() 百分比变化

11.3.6 注意事项(常见坑)¶

  1. 缺失值:count() 会忽略缺失,size() 不忽略;计算占比前先明确分母是谁。
  2. 分组键类型:人名可能重名,事件名可能同名,尽量使用唯一ID作为分组键。
  3. as_index:若后续要 merge 或打印更友好的表,groupby(..., as_index=False) 会更方便。
  4. 多函数聚合的列名:用命名聚合让结果列名清晰,避免多重列索引带来的困惑。
  5. 样本量差异:不同组样本量差异大时,只比较均值可能误导,建议同时报告人数/置信区间等信息。

11.3.7 练习(建议先做再看答案)¶

使用下表(可复制下一格代码生成)完成各小题:

  • 数据:
    姓名, 地区, 性别, 学科, 成绩
    张三, 北方, 男, 语文, 85
    李四, 北方, 女, 数学, 80
    王五, 南方, 男, 数学, 90
    赵六, 南方, 女, 语文, 92
    孙七, 北方, 男, 英语, 88
    周八, 南方, 男, 语文, 76
    吴九, 南方, 女, 英语, 95
    郑十, 北方, 女, 英语, 83

题目: 1) 按“地区”计算平均成绩与人数。
2) 按“地区 × 性别”计算平均成绩,并输出为透视表。
3) 计算各地区的通过率(成绩≥60)。
4) 统计每个学科在不同地区的人数频次表与占比表。

In [11]:
# 生成练习数据
import pandas as pd

exam = pd.DataFrame({
    '姓名': ['张三','李四','王五','赵六','孙七','周八','吴九','郑十'],
    '地区': ['北方','北方','南方','南方','北方','南方','南方','北方'],
    '性别': ['男','女','男','女','男','男','女','女'],
    '学科': ['语文','数学','数学','语文','英语','语文','英语','英语'],
    '成绩': [85,80,90,92,88,76,95,83]
})
exam
Out[11]:
姓名 地区 性别 学科 成绩
0 张三 北方 男 语文 85
1 李四 北方 女 数学 80
2 王五 南方 男 数学 90
3 赵六 南方 女 语文 92
4 孙七 北方 男 英语 88
5 周八 南方 男 语文 76
6 吴九 南方 女 英语 95
7 郑十 北方 女 英语 83
In [ ]:
# === 在此作答(你可以新增更多单元格)===

# 1) 地区:平均成绩与人数


# 2) 地区 × 性别:平均成绩(透视表)


# 3) 各地区通过率(成绩>=60)


# 4) 学科在不同地区的人数频次表与占比表

11.3.8 参考答案(对照自检)¶

仅供参考,真实研究请结合样本量、置信区间与情境解释。

In [12]:
# 1) 参考答案:地区 平均成绩 + 人数
ans1 = exam.groupby('地区', as_index=False).agg(
    平均分=('成绩','mean'),
    人数=('姓名','count')
)
ans1
Out[12]:
地区 平均分 人数
0 北方 84.00 4
1 南方 88.25 4
In [13]:
# 2) 参考答案:地区 × 性别 平均成绩(透视表)
ans2 = pd.pivot_table(exam, values='成绩', index='地区', columns='性别', aggfunc='mean')
ans2
Out[13]:
性别 女 男
地区
北方 81.5 86.5
南方 93.5 83.0
In [14]:
# 3) 参考答案:各地区通过率(成绩>=60)
exam['是否及格'] = (exam['成绩'] >= 60).astype(int)
ans3 = exam.groupby('地区', as_index=False)['是否及格'].mean().rename(columns={'是否及格':'通过率'})
ans3
Out[14]:
地区 通过率
0 北方 1.0
1 南方 1.0
In [15]:
# 4) 参考答案:人数频次与占比表(学科×地区)
freq = pd.crosstab(exam['学科'], exam['地区'])
prop = pd.crosstab(exam['学科'], exam['地区'], normalize='index')  # 每个学科内的地区占比
freq, prop
Out[15]:
(地区  北方  南方
 学科        
 数学   1   1
 英语   2   1
 语文   1   2,
 地区        北方        南方
 学科                    
 数学  0.500000  0.500000
 英语  0.666667  0.333333
 语文  0.333333  0.666667)

11.3 小结¶

  • groupby + agg 是分类比较的万金油:先分组、再计算、最后合并成可读表。
  • 频次与占比常用 value_counts / crosstab,多维比较用 pivot_table。
  • 解释结果时,请同时关注样本量与组内差异,避免只看均值下结论。

下一步:尝试把聚合结果与原表 merge 做成报告表,或结合可视化(柱状图/热力图)呈现。