▶️ 查看代码
company revenue
1 锦城科技 600
2 蜀汉制造 480
revenue company
1 450 浣花实业
数据分析与R语言
2026年09月29日
Joining & Reshaping Data
rbind() 的安全替代品重要
今天是项目二"数据准备"的收官课。前三次课学了读写、筛选排序、变换汇总,今天补上最后两块拼图:多表合并与数据重塑。
复习一下第9课的内容,热热身
1. 单选:mutate() 和 summarise() 最主要的区别是?
A. mutate() 改列名,summarise() 不改
B. mutate() 加/改列、行数不变;summarise() 压成汇总行
C. mutate() 只能用于数值列,summarise() 不限类型
D. mutate() 必须配 group_by(),summarise() 不用
2. 单选:按"评级"分组,算每组的平均营收和公司数,写法是?
A. group_by() 在前、summarise() 在后
B. summarise() 在前、group_by() 在后
C. 用 mutate() 代替 summarise()
D. 用 filter() + arrange()
3. 填空:group_by() 单独使用不会立刻显示分组效果,必须配合 ____ 或 ____ 才能看到实际结果。
4. 填空:summarise() 汇总完后,若不想保留分组状态、避免 R 输出多余提示信息,应加上参数 ____。
第6课的rbind/cbind/merge还够用吗?
第9课的 mutate() 和 summarise() 最大的区别是什么?group_by() 为什么一定要配合它们使用?
答案:mutate() 新增列但行数不变;summarise() 把数据压缩成汇总行。group_by() 本身"看不见分组状态",必须配合 summarise()(或 mutate())才能看到分组的实际作用。
有——这就是 bind_rows() 与 join 系列 要解决的问题。
问题二:如果数据是"一行一家公司、四个季度分四列",但你想按季度分组统计、或画一张"季度趋势图",需要"一行一个季度"的格式——怎么转?
这是今天全新的知识点——数据重塑。把合并和重塑都补齐,项目二"数据准备"的工具链就到齐了。
替代 rbind() 的更安全写法
第 2 行:岷山科技 的 revenue 是 NA——「信息缺失」≠「营收为零」。
| 对比项 | rbind() |
bind_rows() |
|---|---|---|
| 列顺序不一致 | 同样按列名对齐(data.frame 不受影响) | 按列名对齐 |
| 两边列不齐 | 直接报错(列数不同就停) | 缺的列自动补 NA
|
| 类型不一致 | 静默强转(按 base R 规则把整列悄悄换成另一种类型) | 兼容类型自动统一(整数+小数→小数);不兼容就直接报错 |
| 一次合并多张表 | 只能逐个写 rbind(a, b, c)
|
还能直接吃一个列表 |
任务:h1 是上半年业绩,new_company 是新增公司。
bind_rows() 把三家公司的业绩合并成一张表;revenue 一列的数据框,用 bind_rows() 合并进去,观察 company 列会出现什么。思考:第 2 步里补出来的 NA,在业务上意味着"营收为零"还是"这家公司不知名"?
本课最大难点:四种join的语义区别
提示
表一有 5 家公司,company 与 revenue 一一对应。这张表本课会反复用到,建议现在就跟着敲一遍。
重要
表二的公司名单和表一不一样——锦江生物只在表一,浣花实业只在表二。第 6 课学过的 merge() 默认只保留两边都有的记录,其实就是 inner join;dplyr 把它拆成了后面四种写法。
提示
口诀:inner=交集(只留共同的);left=以左为准;right=以右为准;full=全部都要。缺的部分统一补 NA。
company revenue rating
1 锦城科技 1200 A
2 蜀汉制造 980 B
3 天府物流 1560 A
company revenue rating
1 锦城科技 1200 A
2 蜀汉制造 980 B
3 天府物流 1560 A
4 青羊金融 760 <NA>
5 锦江生物 2100 <NA>
重要
看行数:inner 3 行、left 5 行;多出的 2 行(青羊金融、锦江生物)rating 是 NA。
company revenue rating
1 锦城科技 1200 A
2 蜀汉制造 980 B
3 天府物流 1560 A
4 浣花实业 NA B
company revenue rating
1 锦城科技 1200 A
2 蜀汉制造 980 B
3 天府物流 1560 A
4 青羊金融 760 <NA>
5 锦江生物 2100 <NA>
6 浣花实业 NA B
提示
right_join(a, b) 等价于 left_join(b, a)——调换表的顺序即可;full_join() 行数最多(6 = 5 + 4 − 3)。
场景:df1 是"本月有交易的客户",df2 是"已完成实名认证的客户"。找出"有交易但未认证"的客户——合规排查的常规动作。
提示
anti_join(a, b) 只留"在 a 里、但 b 里找不到"的记录,而且不会多出 b 的列。它相当于 left_join(a, b) %>% filter(is.na(rating)),但语义更直白。
如果 df2 里同一家公司出现了两条记录(比如两条评级历史),left_join() 会怎样?
警告
锦城科技被"复制"成两行——行数从 5 变成 6,如果这是营收表,这笔营收就被重复计算了一次。
重要
count() 是合并前的体检动作:只要有一家公司出现两次以上,join 出来的行数就不可信。解决办法有两条:换一个本来就唯一的标识(如统一社会信用代码),或者——把几个字段一起当键(3.11 起展开)。
提示
rev_year 只有 4 行、2 家公司——锦城科技、蜀汉制造各出现两次。单看 company 这一列,已经不能唯一确定一条记录。
提示
两张表都用「公司 + 年份」才能唯一确定一条记录,而且 rating 只在表二有。下一页看"只按公司合并"会出什么事。
警告
两个年份被交叉配对了:有一行是「2024 年的营收」配上「2025 年的评级」。注意 year 被自动改名成 year.x / year.y——R 在替我们报警。
重要
加上 year 之后回到 4 行,年份与评级一一对应。现实的 GDP 数据同样是「国家 + 年份」才唯一——一个字段不够,就多给几个。
band_members、band_instruments 看结构,再用 inner_join()、left_join()、full_join() 按 name 合并,比较行数与 NA 分布;left_join(rev_year, rat_year, by = "company") 有几行,再换成 by = c("company", "year") 数一遍——说出两个数字差在哪、为什么会差。同一份信息,不同的"摆放形状"
重要
两种格式装的是同一份信息,只是"摆放的形状"不同——这就是"重塑"。宽格式像 Excel 报表;长格式不好直读,但更适合分组统计和画图。哪个更"正确"没有标准答案,取决于后面要做什么分析。
company Q1 Q2 Q3 Q4
1 锦城科技 280 310 295 315
2 蜀汉制造 230 240 250 260
3 天府物流 380 400 390 410
4 青羊金融 190 185 195 190
提示
Q1~Q4 这四列,每一格都装着一个数值。想按季度分组统计、或画一张季度趋势图,得先把这四列"收"起来——翻到下一页。
提示
三个参数各管一件事:cols 指定拆哪些列;names_to 是装原来列名的新列;values_to 是装格子数值的新列。先别运行——猜猜结果有多少行,翻到下一页看。
# A tibble: 16 × 3
company quarter revenue
<chr> <chr> <dbl>
1 锦城科技 Q1 280
2 锦城科技 Q2 310
3 锦城科技 Q3 295
4 锦城科技 Q4 315
5 蜀汉制造 Q1 230
6 蜀汉制造 Q2 240
7 蜀汉制造 Q3 250
8 蜀汉制造 Q4 260
# ℹ 8 more rows
重要
4 家公司 × 4 个季度 = 16 行。长格式里"一行"的含义变小了:不再是"一家公司",而是"一家公司的一个季度"。
提示
pivot_wider() 是 pivot_longer() 的反向操作。四个参数的分工:宽变长看 names_to / values_to(列名和数值各自进哪一列),长变宽看 names_from / values_from(哪一列的取值去当新列名、哪一列去填数)。记混了就回头看 4.1 那张图。
tidyr 自带 relig_income(宗教信仰与家庭收入调查,宽格式:每行一个宗教,各收入区间分列,格子里是人数)。用 pivot_longer() 把除 religion 外的所有列转成长格式,新增 income 列(收入区间名)和 count 列(人数),再用 group_by() + summarise() 计算每个收入区间的信众总人数。
提示
提示:cols = -religion 表示"除 religion 外的所有列都要拆开",不用逐个列出收入区间的列名——列特别多的时候这个写法省事得多。
项目二四课工具的集中检验
实际工作中,财务数据很多时候不是存在 CSV/Excel 里,而是存在企业的数据库系统中(如 MySQL、SQL Server):
警告
这部分不作为考核要求,仅作了解。课堂时间紧张时这一页可以直接跳过,不影响后续内容。
任务:分析"不同评级的公司,季度营收走势如何"。
思路——四步,缺一不可:
company 接上——left_join()
quarter + revenue)——pivot_longer()
group_by(rating, quarter)
summarise(mean(revenue))
rating_tbl <- data.frame(company = c("锦城科技", "蜀汉制造", "天府物流", "青羊金融"),
rating = c("A", "B", "A", "C")) # 综合实战用的评级表
res <- wide_data %>%
left_join(rating_tbl, by = "company") %>% # 1 合并评级
pivot_longer(cols = Q1:Q4, names_to = "quarter", # 2 宽变长
values_to = "revenue") %>%
group_by(rating, quarter) %>% # 3 分组
summarise(avg_revenue = mean(revenue), .groups = "drop") # 4 汇总
res %>% pivot_wider(names_from = quarter, values_from = avg_revenue) # 再转宽:一张报告表# A tibble: 3 × 5
rating Q1 Q2 Q3 Q4
<chr> <dbl> <dbl> <dbl> <dbl>
1 A 330 355 342. 362.
2 B 230 240 250 260
3 C 190 185 195 190
长表算、宽表看:四个动作串起来得到 12 行长表,一转就是 3 行报表。
billboard(2000 年公告牌单曲逐周排名,宽格式:每行一首歌,wk1~wk76 是上榜以来每周的名次)——探究"哪些歌曲长期霸榜":
pivot_longer() 把 wk1~wk76 转成长格式,新增 week 和 rank 两列,并用 values_drop_na = TRUE 剔除已下榜、没有名次的记录;group_by() 按 track 分组,summarise() 算出每首歌的最高名次(min(rank))和上榜周数(n());arrange() 按上榜周数从多到少排序。思考:上榜周数最多的那首歌,最高名次是不是第一名?"持续热度"和"巅峰热度"是一回事吗?
"数据准备"完整流程在此闭环
flowchart LR
A["第7课<br/>数据读写<br/>类型转换"] --> B["第8课<br/>dplyr筛选<br/>排序"]
B --> C["第9课<br/>变换与<br/>汇总"]
C --> D["第10课(今天)<br/>合并与<br/>重塑"]
style A fill:#2e5984,color:#fff
style B fill:#7a4419,color:#fff
style C fill:#375623,color:#fff
style D fill:#6b3d8f,color:#fff
重要
四次课合起来,就是一条完整的"数据准备"标准流程:读入数据 → 规范类型 → 筛选排序 → 变换汇总 → 合并重塑——这是几乎所有数据分析任务的"标配前奏"。
对照一下,今天这几件事你都做到了吗?
bind_rows() 按列名合并两张表,并说出它与 rbind() 的差别by = c("company", "year") 修正anti_join() 找出"没匹配上"的记录pivot_longer() / pivot_wider() 在宽、长格式之间互相转换提示
有没打勾的,下课后找同学或老师补上——项目三"数据清洗"会直接在这些"准备好的数据"上继续加工。
请在纸条或在线问卷上简要回答:
left_join() 和 right_join() 最大的区别是什么?dplyr 综合应用(变换汇总 + 表连接 + 长宽转换),共 6 题。第 3、4、5 题是今天讲的新工具:
inner_join() 单字段合并left_join() 双字段合并pivot_wider() 长转宽重要
提交要求:在实验报告4 的模板里,把每题"代码 + 运行结果"的截图粘贴到相应位置,并在"结果与分析"里写出结论;文件命名为"学号_姓名_实验报告4"。本次作业计入平时成绩的"实验报告"部分。
提示
本讲加练(选做,2 题)——实验报告4 已覆盖本讲大部分新工具,下面两处做个补充:
bind_rows() vs rbind():造两张列数不同的表,分别用两个函数合并,比较结果差异;pivot_longer():把一张宽格式季度表转成长格式,再按季度分组算平均营收——注意实验报告4 第 5 题只练了反向的 pivot_wider(),两个方向的参数别记混。项目三:数据清洗——把"准备好的数据"变成"可以直接分析的数据"
提示
课前准备:保留好项目二四课的代码与数据文件。项目三要解决的是数据中"脏数据"的问题——这是从"准备好的数据"往"可以直接分析的数据"迈出的关键一步。
第10讲:数据合并与重塑
"Join is not just a technical operation; it is a business decision." 关联字段必须唯一且准确(如按统一社会信用代码而非公司简称合并),"一一对应、不重不漏"是财务数据整合的基本职业操守;选择哪种 join,本身就是对业务规则的理解——技术选择的背后,是业务判断。
朱 奇 | 锦城大学 · 财会学院