第9讲:数据变换与汇总——mutate()、group_by()、summarise()

数据分析与R语言

2026年09月23日

本讲导览

Data Transformation & Summary with dplyr

本讲路线

  • 一、复习导入 ——arrange/filter/select 够用吗?
  • 二、mutate() ——新增或修改列
  • 三、summarise() ——把数据“压缩”成汇总结果
  • 四、group_by() + summarise() ——分组汇总
  • 五、综合实战 ——五个动词串成一条流水线
  • 六、课堂总结与 Exit Ticket ——dplyr 五大动词到齐

上节课的 arrange/filter/select 都是“挑挑拣拣”,今天的 mutate/summarise 才是“算”。

课前小测

复习一下第 8 课的内容,热热身

小测(1/3):单选 · 多列排序

单选:想先按评级升序、再按营收降序、最后按公司名升序排列,正确写法是?

  • A. arrange(rating, desc(revenue), company)
  • B. arrange(desc(rating, revenue), company)
  • C. arrange(rating | desc(revenue) | company)
  • D. arrange(desc(rating), revenue, company)

小测(2/3):单选 · 多条件筛选

单选:filter() 里要保留“评级为 A 且 营收大于 1000”的行,正确写法是?

  • A. filter(rating == "A" | revenue > 1000)
  • B. filter(rating == "A" & revenue > 1000)
  • C. filter(rating == "A", revenue < 1000)
  • D. filter(rating = "A" & revenue > 1000)

小测(3/3):两道填空

  1. 填空:多条件排序时,应在 arrange() 中用逗号依次写出排序列;若某列要降序,需要加 ____。

  2. 填空:想从数据框中“排除”某一列(比如 rating),select() 的写法是 select(____)

参考答案

一、复习导入与新课导论

arrange/filter/select 够用吗?

1.1 复习提问

上节课学的 arrange()、filter()、select() 分别解决什么问题?用管道符怎么把它们串起来?

答案:

  • arrange() 排序
  • filter() 筛选行
  • select() 选列

三者合起来是“排队、挑数据、瘦身”,但都只是对已有信息做整理。

1.2 情境导入——两个引导问题

问题一:如果想根据“营收”和“成本”这两列,计算出一列新的“利润率”,之前学的 arrange/filter/select 能做到吗?

不能——这三个动词都不会新增列,它们只在原有列之间做选择。这就是今天第一个新动词 mutate() 要解决的问题。

问题二:第 6 课学的 tapply(),能按评级分组算平均营收——dplyr 有没有类似功能,而且更灵活(同时按两个变量分组、同时算好几个指标)?

有——这就是 group_by() + summarise() 要解决的问题,也是今天最核心的组合。

1.3 先把本讲要用的数据准备好

▶️ 查看代码
library(dplyr)              # 本讲要用(含管道符 %>%)
company_data <- data.frame( # 6 家公司的虚构财务数据
  company = c("锦城科技", "蜀汉制造", "天府物流", "青羊金融", "锦江生物", "浣花实业"),
  revenue = c(1200.5, 980, 1560.3, 760.2, 2100, 890.75),
  cost    = c(800, 650, 1020, 500, 1400, 600),
  rating  = c("A", "B", "A", "C", "A", "B"),
  region  = c("西南", "西南", "西部", "东部", "西南", "西部"))
company_data
   company revenue cost rating region
1 锦城科技    1200  800      A   西南
2 蜀汉制造     980  650      B   西南
3 天府物流    1560 1020      A   西部
4 青羊金融     760  500      C   东部
5 锦江生物    2100 1400      A   西南
6 浣花实业     891  600      B   西部

二、mutate()——新增或修改列

在数据框右边“加一列”,行数不变

2.1 mutate() 是怎么加列的

mutate() 做的事:在右边加一列,行数一行不变公司营收锦城科技1200蜀汉制造980天府物流1560青羊金融760锦江生物2100浣花实业891公司营收利润率锦城科技12000.334蜀汉制造9800.337天府物流15600.346青羊金融7600.342锦江生物21000.333浣花实业8910.326行数:6 行 → 6 行一家公司还是一行,只是右边多了一列「算出来」的数深蓝 = 新增的列(默认放最右)

2.2 新增一列:营收增长预测

▶️ 查看代码
# 假设营收增长 10%,新增一列预测值(新列名 = 新列)
company_data %>% 
  mutate(revenue_expected = revenue * 1.1) %>% 
  select(company, revenue, revenue_expected)
   company revenue revenue_expected
1 锦城科技    1200             1321
2 蜀汉制造     980             1078
3 天府物流    1560             1716
4 青羊金融     760              836
5 锦江生物    2100             2310
6 浣花实业     891              980

2.3 新列名 vs 已有列名

写不存在的列名=新建一列;写已有列名=用计算结果覆盖原列:

▶️ 查看代码
company_data %>% 
  mutate(revenue = revenue / 10000) %>%   # 已有列名 → 覆盖 revenue 原列
  select(company, revenue)
   company revenue
1 锦城科技  0.1201
2 蜀汉制造  0.0980
3 天府物流  0.1560
4 青羊金融  0.0760
5 锦江生物  0.2100
6 浣花实业  0.0891

2.4 新列放在哪里:.before 与 .after

新列默认放到最右侧。想指定位置,可以用 .after = 或 .before =(写列名或列号都行):

▶️ 查看代码
company_data %>% 
  mutate(profit = revenue - cost, .after = cost) %>%   # 新列紧跟 cost 之后
  select(company, revenue, cost, profit)
   company revenue cost profit
1 锦城科技    1200  800    400
2 蜀汉制造     980  650    330
3 天府物流    1560 1020    540
4 青羊金融     760  500    260
5 锦江生物    2100 1400    700
6 浣花实业     891  600    291

2.5 一次新增多列——“链式计算”

▶️ 查看代码
company_data %>%
  mutate(
    profit      = revenue - cost,      # 第一列:利润
    profit_rate = profit / revenue     # 第二列:直接引用刚算出的 profit
  ) %>%
  select(company, rating, profit, profit_rate)
   company rating profit profit_rate
1 锦城科技      A    400       0.334
2 蜀汉制造      B    330       0.337
3 天府物流      A    540       0.346
4 青羊金融      C    260       0.342
5 锦江生物      A    700       0.333
6 浣花实业      B    291       0.326

后面的新列可以直接引用前面刚算出的新列——顺序写反了会报错。

2.6 课堂练习:新增“是否达标”列

用 mutate() 新增一列“是否达标”(营收是否超过 1000 亿元),并想一想:这一列会是什么数据类型?

提示:结合第 2 课学的逻辑型——revenue > 1000 的比较结果,要么是 TRUE、要么是 FALSE。

2.7 课堂练习:用内置数据练 mutate()

用 dplyr 自带的 starwars 数据集完成:新增身高米列 height_m(height 的单位是厘米,除以 100 换算),把它放到 height 后面,最后筛选出身高超过 2 米的角色。

三、summarise()——把数据“压缩”成汇总结果

从“每一行”到“一个数”

3.1 summarise() 是怎么压的

summarise() 做的事:把 6 行压成 1 行汇总值公司营收锦城科技1200蜀汉制造980天府物流1560青羊金融760锦江生物2100浣花实业891总营收平均最高最低家数74921249210076066 行 → 1 行:公司这一列没了,只剩「算出来」的数行数从 6 变成 1 —— 这是本讲最需要记住的一句话。

3.2 基本用法:一次算出五个汇总量

▶️ 查看代码
company_data %>%
  summarise(
    total_revenue = sum(revenue),
    avg_revenue   = mean(revenue),
    max_revenue   = max(revenue),
    min_revenue   = min(revenue),
    n_company     = n()        # n():一共有多少行(多少家公司)
  )
  total_revenue avg_revenue max_revenue min_revenue n_company
1          7492        1249        2100         760         6

3.3 命名与不命名:结果表好不好读

不写列名,R 会自己拼一个很长的名字——能跑,但结果表不好读:

▶️ 查看代码
company_data %>%
  summarise(
    sum(revenue),
    mean(revenue),
    max(revenue),
    min(revenue),
    n()
  )
  sum(revenue) mean(revenue) max(revenue) min(revenue) n()
1         7492          1249         2100          760   6

养成写列名的习惯:表头一列一列都看得懂,后面接 arrange() 排序、导出 CSV 都不会出问题。

3.4 count():分组计数的快捷写法

count() 把「分组 → 计数 → 汇总」三步合成一步company_data %>% count(rating)company_data %>% group_by(rating) %>%summarise(n = n())≡ratingnA3B2C1ratingnA3B2C1结果一模一样——但 count() 一行就写完了。

3.5 count() 的三种常见写法

▶️ 查看代码
company_data %>% count(rating)                       # 最常用:按一个变量计数
  rating n
1      A 3
2      B 2
3      C 1
▶️ 查看代码
company_data %>% count(rating, name = "n_company")   # 改计数列的名字
  rating n_company
1      A         3
2      B         2
3      C         1

3.6 用两个变量计数:count(rating, region)

count() 也可以一次按两个变量计数——比手写 group_by() + summarise() 省事得多:

▶️ 查看代码
company_data %>% count(rating, region)
  rating region n
1      A   西南 2
2      A   西部 1
3      B   西南 1
4      B   西部 1
5      C   东部 1

3.7 课堂练习:内置数据的汇总

  • 用 starwars 计算平均身高、最高身高和角色总数
  • 用 count(species) 找出出现次数最多的物种(注意缺失值)

3.8 mutate() vs summarise():行数的区别

一个是「横向加宽」,一个是「纵向压缩」——方向完全不同mutate():6 行 → 6 行每行右侧多一格,行数一行没少summarise():6 行 → 1 行6 行被压成 1 行,只剩汇总值
▶️ 查看代码
company_data %>% mutate(profit = revenue - cost) %>% nrow()   # mutate() 后:还是 6 行
[1] 6
▶️ 查看代码
company_data %>% summarise(total = sum(revenue)) %>% nrow()   # summarise() 后:只剩 1 行
[1] 1

3.9 课堂练习:总成本与平均成本

用 summarise() 一次算出总成本(sum(cost))和平均成本(mean(cost)),看看结果有几行几列?

四、group_by() + summarise()——分组汇总

dplyr 里使用频率最高的组合之一

4.1 group_by() 是怎么分组的

group_by() 做的事:先按评级把行分成几堆,再对每一堆算一个汇总公司营收评级锦城科技1200A蜀汉制造980B天府物流1560A青羊金融760C锦江生物2100A浣花实业891B评级平均营收公司数A16203B9352C76013 堆 → 3 行:A 组 3 家算出一个均值,B 组 2 家一个,C 组 1 家一个颜色 = 分组归属

4.2 单变量分组

▶️ 查看代码
company_data %>%
  group_by(rating) %>%            # 按评级分组
  summarise(
    avg_revenue   = mean(revenue),
    total_revenue = sum(revenue),
    n_company     = n()           # 每个评级有几家公司
  )
# A tibble: 3 × 4
  rating avg_revenue total_revenue n_company
  <chr>        <dbl>         <dbl>     <int>
1 A            1620.         4861.         3
2 B             935.         1871.         2
3 C             760.          760.         1

4.3 多变量分组——group_by(a, b)

▶️ 查看代码
company_data %>%
  group_by(rating, region) %>%     # 同时按评级和地区分组
  summarise(
    avg_revenue = mean(revenue),
    .groups = "drop"               # 汇总完不保留分组状态
  )
# A tibble: 5 × 3
  rating region avg_revenue
  <chr>  <chr>        <dbl>
1 A      西南         1650.
2 A      西部         1560.
3 B      西南          980 
4 B      西部          891.
5 C      东部          760.

多写一个变量就多一个维度——这是 group_by() 比 tapply() 灵活的地方。

4.4 .by:只作用于这一次的分组

.by 是许多 dplyr 动词支持的临时分组参数,只对当前这一次操作生效:

▶️ 查看代码
company_data %>% 
  summarise(
    avg_revenue = mean(revenue), 
    n_company = n(), 
    .by = rating
  )
  rating avg_revenue n_company
1      A        1620         3
2      B         935         2
3      C         760         1

写完就结束——.by 不会把分组状态带到后面,所以也不用再写 ungroup()。

4.5 组内计算:group_by() 状态能一路带下去

group_by() 的分组状态会带到后面每一步,所以它还能配合 mutate() 做“组内计算”——行数不变,但每行多了一个“本组均值”:

▶️ 查看代码
company_data %>% 
  mutate(group_avg = mean(revenue), .by = rating) %>%   # 组内均值:行数不变
  select(company, rating, revenue, group_avg)
   company rating revenue group_avg
1 锦城科技      A    1200      1620
2 蜀汉制造      B     980       935
3 天府物流      A    1560      1620
4 青羊金融      C     760       760
5 锦江生物      A    2100      1620
6 浣花实业      B     891       935

4.6 进阶:组内排名

▶️ 查看代码
company_data %>%
  group_by(rating) %>%
  mutate(revenue_rank_in_group = rank(desc(revenue))) %>%   # 组内排名
  arrange(rating, revenue_rank_in_group) %>% 
  select(company, rating, revenue, revenue_rank_in_group)
# A tibble: 6 × 4
# Groups:   rating [3]
  company  rating revenue revenue_rank_in_group
  <chr>    <chr>    <dbl>                 <dbl>
1 锦江生物 A        2100                      1
2 天府物流 A        1560.                     2
3 锦城科技 A        1200.                     3
4 蜀汉制造 B         980                      1
5 浣花实业 B         891.                     2
6 青羊金融 C         760.                     1

4.7 课堂练习:用 .by 做分组分析

用 starwars 按 gender 计算平均身高和角色数,再用 .by = species 给每个角色加上其所属物种的平均身高。

4.8 Excel 的“最后一行汇总”

类似 Excel 底部“总计行”的效果,可以用 bind_rows() 把一行汇总结果拼回原表——先看一眼,下节课细讲:

▶️ 查看代码
summary_row <- company_data %>%
  summarise(company = "合计/平均", revenue = sum(revenue), cost = mean(cost))
bind_rows(company_data, summary_row)
    company revenue cost rating region
1  锦城科技    1200  800      A   西南
2  蜀汉制造     980  650      B   西南
3  天府物流    1560 1020      A   西部
4  青羊金融     760  500      C   东部
5  锦江生物    2100 1400      A   西南
6  浣花实业     891  600      B   西部
7 合计/平均    7492  828   <NA>   <NA>

4.9 为什么不推荐把汇总行放进数据框

上一页那种做法看起来方便,实际上是个坑:

  • 数据框要求每一行是一个观测值,而“合计/平均”不是一家公司;
  • 一旦混进去,后面的分组汇总、筛选、排序都会被这个假观测值污染;
  • 图表也会把它当成一家真实公司画出来。

正确做法:汇总结果单独保存(summary_row)或单独展示,不要塞回原表。

五、综合实战——五个动词串成一条流水线

从原始数据一路算到有业务意义的结论

5.1 实战任务

模拟一次真实的财务分析任务:从 company_data 出发,回答——“哪个评级的公司盈利能力最强?”

  1. 筛选出营收超过 800 亿元的公司(回顾第 8 课 filter())
  2. 计算每家公司的利润率(mutate() 新增列)
  3. 按评级分组(group_by())
  4. 计算各评级的平均利润率(summarise())
  5. 按平均利润率从高到低排序(arrange())

建议先自己用管道写一遍,再对照下一页的参考实现。

5.2 参考实现

五个动词、五行代码——从原始数据一路算到“哪个评级的公司盈利能力最强”,这就是 dplyr 管道工作流的完整威力。

5.3 学生自主练习

用 R 内置数据集 airquality 探究“气温”与“臭氧浓度”的关系:

  • 用 filter() 剔除臭氧浓度(Ozone)缺失的记录
  • 用 mutate() 新增一列 Temp_C,把华氏温度换算成摄氏(保留 1 位小数)
  • 用 group_by() 按月份(Month)分组
  • 用 summarise() 算出每月的平均臭氧、平均气温、有效记录数
  • 用 arrange() 按平均臭氧浓度从高到低排序

想一想:平均臭氧最高的月份,平均气温是不是也最高?这符合“气温越高、光化学反应越强、臭氧越多”的常识吗?

六、课堂总结与Exit Ticket

dplyr 五大动词到齐

6.1 本讲知识回顾

flowchart LR
    A["arrange()<br/>排序(第8讲)"] --> B["filter()<br/>筛选行(第8讲)"]
    B --> C["select()<br/>选列瘦身(第8讲)"]
    C --> D["mutate()<br/>新增列·行数不变"]
    D --> E["summarise()<br/>压缩成汇总行"]
    E --> F["group_by()<br/>分组后再汇总"]

    style A fill:#2e5984,color:#fff
    style B fill:#2e5984,color:#fff
    style C fill:#2e5984,color:#fff
    style D fill:#7a4419,color:#fff
    style E fill:#375623,color:#fff
    style F fill:#6b3d8f,color:#fff

dplyr 五大动词到齐了:arrange() 排序、filter() 筛选行、select() 选列(上节课)+ mutate() 新增列、summarise() 汇总(本节课)。

6.2 学习成果自查

对照一下,今天这几件事你都做到了吗?

  • ✓ 能够使用 mutate() 新增单列和多列(含链式计算)
  • ✓ 能够说出 mutate() 与 summarise() 在行数变化上的本质区别
  • ✓ 能够使用 summarise() 计算 sum / mean / max / min / n() 等统计量
  • ✓ 能够使用 group_by() + summarise() 完成单变量和多变量分组汇总
  • ✓ 能够用管道把 filter/mutate/group_by/summarise/arrange 串成完整流水线

有没打勾的,下课后一定要找同学或老师补上——下节课会直接用到今天的分组汇总。

6.3 Exit Ticket —— 写给自己的三个问题

请花 1 分钟,在纸条或在线问卷上简要回答:

  1. mutate() 和 summarise() 最大的区别是什么?
  2. group_by() 单独使用会发生什么?为什么一定要配合 summarise() 或 mutate()?
  3. 今天写的五动词流水线里,你觉得哪一步最能体现“财务分析”的味道?

课后作业

数据变换与汇总综合练习

  1. 用 read_csv() 读入第 7、8 课处理过的财务数据,用 mutate() 新增至少 2 列衍生指标(如利润率、营收增长率等,数据与公式自定)
  2. 用 summarise() 计算整体的总营收、平均营收、最高与最低营收
  3. 用 group_by() + summarise() 完成:
    • 按单一变量(如评级)分组,计算每组的平均营收与公司数量
    • 按两个变量同时分组(如评级 + 地区),计算每组的平均营收
  4. 用管道符把 filter()、mutate()、group_by()、summarise()、arrange() 中至少 4 个动词串成一条完整流水线,得出一个你自己的财务分析结论,并用一句话写出这个结论说明了什么

下讲预告

第10讲:数据合并与重塑——bind_rows()/bind_cols()/join 系列函数 与 数据形状转换

下讲内容

  • 把多张表“拼”在一起:行合并、列合并、按关键字段连接
  • pivot_longer() / pivot_wider():数据从“宽”到“长”、从“长”到“宽”
  • 项目二的收官课——dplyr 工具链到此完整闭环

谢谢!

第9讲:数据变换与汇总

"Numbers have an important story to tell." 数据汇总不是简单的技术操作,而是管理决策的重要依据——口径、维度选错,会误导判断;对业务口径保持审慎,才是负责任的数据分析。

朱 奇 | 锦城大学 · 财会学院