R: R 数据重塑:tidyr 完整教程
最后更新:2026-08-26
真实项目里 80% 的数据是"宽表"(每行一个实体,字段是属性),但分析时常需要"长表"(每行一个观测值)。这一课我们学 R 数据的"变形术"——tidyr 重塑 + dplyr 连接,把数据从任何形态转换为分析需要的形态。
读完这一课你就能:宽表 ↔ 长表转换、拆分合并列、多表 join 合并、做任意报表。
1. 你将学到
- 宽表 vs 长表的差异
- pivot_longer 宽 → 长
- pivot_wider 长 → 宽
- separate / unite 列拆分与合并
- dplyr 4 种 join(left/right/inner/full)
- 实战:销售数据长宽转换
- 报表生成
2. 一个数据变形的痛点
(1) 痛点:宽表难分析
小赵拿到一份季度销售数据(宽表):
地区 Q1 Q2 Q3 Q4
北京 1000 1200 1100 1500
上海 1500 1800 1700 2000
广州 800 900 1000 1200
manager要求画"季度销售趋势图"——ggplot2 需要长表:
地区 季度 销售额
北京 Q1 1000
北京 Q2 1200
...
如果用 Excel 复制粘贴要 5 分钟;Python pd.melt 一行;R tidyr——
(2) R 的解法
library(tidyr)
# 宽表 → 长表(一行)
sales_long <- sales_wide |>
pivot_longer(
cols = c(Q1, Q2, Q3, Q4),
names_to = "季度",
values_to = "销售额"
)
1 行代码搞定宽长转换。
3. 宽表 vs 长表
(1) 两种形态对比
graph LR
A[宽表 Wide] -->|pivot_longer| B[长表 Long]
B -->|pivot_wider| A
subgraph 宽表示例
C[地区 Q1 Q2 Q3 Q4]
end
subgraph 长表示例
D[地区 季度 销售额]
end
(2) 何时用哪种?
| 场景 | 推荐 |
|---|---|
| 存储数据 | 宽表(节省行数) |
| 人看报表 | 宽表(Excel 友好) |
| ggplot2 画图 | 长表(必需) |
| dplyr 分析 | 长表(group_by 友好) |
| 机器学习 | 宽表(每行 1 个样本) |
4. pivot_longer():宽 → 长
(1) 基本语法
pivot_longer(
data,
cols, # 要转换的列
names_to = "name", # 新列名(原列名)
values_to = "value", # 新列名(原值)
names_prefix = "", # 列名公共前缀
names_sep = NULL, # 分隔符(拆分列名)
names_pattern = NULL, # 正则(提取列名)
values_drop_na = FALSE
)
(2) 基本转换
library(tidyr)
# 宽表
sales_wide <- tibble(
region = c("北京", "上海", "广州"),
Q1 = c(1000, 1500, 800),
Q2 = c(1200, 1800, 900),
Q3 = c(1100, 1700, 1000),
Q4 = c(1500, 2000, 1200)
)
print(sales_wide)
# # A tibble: 3 × 5
# region Q1 Q2 Q3 Q4
# <chr> <dbl> <dbl> <dbl> <dbl>
# 1 北京 1000 1200 1100 1500
# 2 上海 1500 1800 1700 2000
# 3 广州 800 900 1000 1200
# 转长表
sales_long <- sales_wide |>
pivot_longer(
cols = c(Q1, Q2, Q3, Q4),
names_to = "quarter",
values_to = "sales"
)
print(sales_long)
# # A tibble: 12 × 3
# region quarter sales
# <chr> <chr> <dbl>
# 1 北京 Q1 1000
# 2 北京 Q2 1200
# 3 北京 Q3 1100
# 4 北京 Q4 1500
# 5 上海 Q1 1500
# 6 上海 Q2 1800
# 7 上海 Q3 1700
# 8 上海 Q4 2000
# ...
(3) 4 种 cols 选择方式
# 1. 直接列名向量
cols = c(Q1, Q2, Q3, Q4)
# 2. starts_with()(最常用)
cols = starts_with("Q")
# 3. 数字范围
cols = 2:5 # 第 2-5 列
# 4. 列名 pattern(正则)
cols = matches("^Q\\d+$")
# 5. 排除某列
cols = -region
(4) 实战:复杂列名拆分
# 列名含前缀
df <- tibble(
id = 1:3,
sales_2022 = c(100, 200, 300),
sales_2023 = c(150, 250, 350),
profit_2022 = c(10, 20, 30),
profit_2023 = c(15, 25, 35)
)
# 拆分 sales_2022 → 类型=销售, 年份=2022
df_long <- df |>
pivot_longer(
cols = -id,
names_to = c("type", "year"),
names_sep = "_",
values_to = "value"
)
print(df_long)
# # A tibble: 12 × 4
# id type year value
# <int> <chr> <chr> <dbl>
# 1 1 sales 2022 100
# 2 1 sales 2023 150
# 3 1 profit 2022 10
# 4 1 profit 2023 15
# ...
(5) 实战:正则提取
# 列名格式:变量_年份_季度
df <- tibble(
id = 1:2,
sales_2024_Q1 = c(100, 200),
sales_2024_Q2 = c(150, 250),
cost_2024_Q1 = c(50, 80),
cost_2024_Q2 = c(70, 100)
)
# 用正则提取
df_long <- df |>
pivot_longer(
cols = -id,
names_to = c("type", "year", "quarter"),
names_pattern = "(\\w+)_(\\d+)_(\\w+)",
values_to = "value"
)
print(df_long)
5. pivot_wider():长 → 宽
(1) 基本语法
pivot_wider(
data,
id_cols = NULL, # 标识列(保持为行)
names_from = name, # 新列名来源
values_from = value, # 新列值来源
values_fill = NULL, # 缺失值填充
names_prefix = "" # 新列名前缀
)
(2) 基本转换
# 长表转宽表
sales_wide_again <- sales_long |>
pivot_wider(
id_cols = region,
names_from = quarter,
values_from = sales
)
print(sales_wide_again)
# # A tibble: 3 × 5
# region Q1 Q2 Q3 Q4
# <chr> <dbl> <dbl> <dbl> <dbl>
# 1 北京 1000 1200 1100 1500
# ...
(3) 实战:缺失值处理
# 数据有缺失(不是所有 region 都有所有 quarter)
df <- tibble(
region = c("A", "A", "B", "C", "C"),
quarter = c("Q1", "Q2", "Q1", "Q2", "Q3"),
sales = c(100, 200, 150, 250, 300)
)
# 转宽表(缺失位置填 0)
df_wide <- df |>
pivot_wider(
id_cols = region,
names_from = quarter,
values_from = sales,
values_fill = 0
)
print(df_wide)
# # A tibble: 3 × 4
# region Q1 Q2 Q3
# <chr> <dbl> <dbl> <dbl>
# 1 A 100 200 0
# 2 B 150 0 0
# 3 C 0 250 300
6. separate() / unite():拆分合并列
(1) separate() 拆分
# 拆分日期列
df <- tibble(date = c("2024-01-15", "2024-02-20"))
# 按分隔符拆分
df |> separate(date, into = c("year", "month", "day"), sep = "-")
# # A tibble: 2 × 3
# year month day
# <chr> <chr> <chr>
# 1 2024 01 15
# 2 2024 02 20
# 按位置拆分
df |> separate(date, into = c("year", "rest"), sep = 4)
# year rest
# "2024" "-01-15"
# 用正则拆分
df |> separate(date, into = c("year", "month", "day"),
sep = "(-)")
(2) unite() 合并
# 合并年/月/日
df_split <- tibble(
year = c("2024", "2024"),
month = c("01", "02"),
day = c("15", "20")
)
df_split |> unite("date", year, month, day, sep = "-")
# # A tibble: 2 × 1
# date
# <chr>
# 1 2024-01-15
# 2 2024-02-20
7. dplyr 4 种 join
(1) join 类型速查表
graph TB
A[dplyr 4 种 join] --> B[left_join<br/>保左表全部]
A --> C[right_join<br/>保右表全部]
A --> D[inner_join<br/>保交集]
A --> E[full_join<br/>保并集]
style A fill:#fff3cd
style B fill:#cce5ff
style C fill:#d4edda
style D fill:#f8d7da
style E fill:#e1d4ff
(2) SQL 类比
| dplyr | SQL | 含义 |
|---|---|---|
left_join(a, b, by) |
LEFT JOIN |
保留 a 全部 |
right_join(a, b, by) |
RIGHT JOIN |
保留 b 全部 |
inner_join(a, b, by) |
INNER JOIN |
保留 a∩b |
full_join(a, b, by) |
FULL OUTER JOIN |
保留 a∪b |
(3) 实战:2 表 join
# 客户表
customers <- tibble(
id = 1:5,
name = c("Alice", "Bob", "Charlie", "Diana", "Eve")
)
# 订单表
orders <- tibble(
customer_id = c(1, 1, 2, 4, 4, 6),
amount = c(100, 200, 150, 300, 250, 500)
)
# 1. left_join:保客户全部(无订单的为 NA)
customers |> left_join(orders, by = c("id" = "customer_id"))
# # A tibble: 7 × 3 ← 5 客户 + 6 订单 = 7 行(Charlie/Eve 无订单)
# id name amount
# 1 1 Alice 100
# 2 1 Alice 200
# 3 2 Bob 150
# 4 3 Charlie NA ← 无订单
# 5 4 Diana 300
# 6 4 Diana 250
# 7 5 Eve NA ← 无订单
# 2. inner_join:保交集
customers |> inner_join(orders, by = c("id" = "customer_id"))
# # A tibble: 6 × 3 ← 4 个有订单的客户
# 3. full_join:保并集
customers |> full_join(orders, by = c("id" = "customer_id"))
# # A tibble: 7 × 3 ← 包含 id=6 的"无客户"订单
# 4. right_join:保订单全部
customers |> right_join(orders, by = c("id" = "customer_id"))
# # A tibble: 6 × 3 ← 6 个订单
(4) 多键 join
# 按多个列 join
df1 |> left_join(df2, by = c("year", "month", "id"))
8. 实战:销售数据多表分析
下面是一个完整工作流示例,把本课所有重塑知识串起来。
▶ 示例:4 季度销售数据深度分析
# ============================================
# 4 季度销售数据深度分析
# 功能:宽↔长转换 + 多表 join + 报表
# ============================================
library(tidyr)
library(dplyr)
# 1. 准备原始数据(宽表)
sales_wide <- tibble(
region = c("北京", "上海", "广州", "深圳"),
Q1 = c(1000, 1500, 800, 1200),
Q2 = c(1200, 1800, 900, 1400),
Q3 = c(1100, 1700, 1000, 1300),
Q4 = c(1500, 2000, 1200, 1600)
)
# 2. 宽表 → 长表
sales_long <- sales_wide |>
pivot_longer(
cols = c(Q1, Q2, Q3, Q4),
names_to = "quarter",
values_to = "sales"
)
cat("=== 长表(前 8 行)===\n")
print(head(sales_long, 8))
# 3. 计算同比增长
sales_growth <- sales_long |>
group_by(region) |>
arrange(quarter) |>
mutate(
prev_sales = lag(sales),
growth = round((sales - prev_sales) / prev_sales * 100, 2)
)
cat("\n=== 同比增长 ===\n")
print(sales_growth)
# 4. 长表 → 宽表(生成对比报表)
report_wide <- sales_growth |>
select(region, quarter, sales, growth) |>
pivot_wider(
id_cols = region,
names_from = quarter,
values_from = c(sales, growth),
names_glue = "{quarter}_{.value}"
)
cat("\n=== 宽表报表 ===\n")
print(report_wide)
# 5. 拆分季度 → 季度+数字
sales_long2 <- sales_long |>
separate(quarter, into = c("q_prefix", "q_num"), sep = 1, remove = FALSE)
cat("\n=== 拆分季度 ===\n")
print(sales_long2)
# 6. 多表 join:合并客户信息
customers <- tibble(
region = c("北京", "上海", "广州", "深圳", "杭州"),
manager = c("Alice", "Bob", "Charlie", "赵六", "钱七"),
tier = c("A", "A", "B", "A", "C")
)
# left_join:保留所有销售地区
sales_with_manager <- sales_long |>
left_join(customers, by = "region")
cat("\n=== 销售 + 客户经理 ===\n")
print(head(sales_with_manager, 8))
# 7. 按经理汇总
manager_summary <- sales_with_manager |>
group_by(manager, tier) |>
summarise(
总销售 = sum(sales),
平均季度 = round(mean(sales), 2),
最大季度 = max(sales)
) |>
arrange(desc(总销售))
cat("\n=== 经理业绩 ===\n")
print(manager_summary)
# 8. 季度对比报表
quarter_comparison <- sales_long |>
group_by(quarter) |>
summarise(
总销售 = sum(sales),
平均 = round(mean(sales), 2),
最大 = max(sales),
最小 = min(sales)
)
cat("\n=== 季度对比 ===\n")
print(quarter_comparison)
# 9. 复杂宽表(多指标)
complex_report <- sales_with_manager |>
group_by(region) |>
summarise(
总销售 = sum(sales),
平均季度 = round(mean(sales), 2)
) |>
pivot_wider(
names_from = region,
values_from = c(总销售, 平均季度)
)
cat("\n=== 复杂宽表 ===\n")
print(complex_report)
# 10. 反向操作:把上面宽表转回长表(用于画图)
viz_data <- complex_report |>
pivot_longer(
cols = everything(),
names_to = c("metric", "region"),
names_sep = "_",
values_to = "value"
)
cat("\n=== 可视化数据 ===\n")
print(viz_data)
预期输出(节选):
=== 经理业绩 ===
# A tibble: 4 × 5
manager tier 总销售 平均季度 最大季度
<chr> <chr> <int> <dbl> <int>
1 Bob A 7000 1750 2000
2 Alice A 4800 1200 1500
3 赵六 A 5500 1375 1600
4 Charlie B 3900 975 1200
=== 季度对比 ===
# A tibble: 4 × 5
quarter 总销售 平均 最大 最小
<chr> <int> <dbl> <int> <int>
1 Q1 4500 1125 1500 800
2 Q2 5300 1325 1800 900
3 Q3 5100 1275 1700 1000
4 Q4 6300 1575 2000 1200
❓ 常见问题
pivot_longer 的 cols 怎么写?c(Q1, Q2, Q3, Q4) 显式列名 / starts_with("Q") 前缀匹配 / 2:5 数字范围 / matches("^Q\\d+$") 正则。names_sep = "_" 拆分(固定分隔符)或 names_pattern = "(\\w+)_(\\d+)" 拆分(正则)。拆出的多列在 names_to = c("a", "b") 声明。📖 小节
- 宽表:每行 1 个实体,字段是属性(人友好)
- 长表:每行 1 个观测值(分析友好)
pivot_longer()宽 → 长,pivot_wider()长 → 宽pivot_longer关键参数:cols/names_to/values_to/names_sep/names_patternpivot_wider关键参数:id_cols/names_from/values_from/values_fillseparate()拆分列(按分隔符或位置),unite()合并列- dplyr 4 种 join:
left_join(最常用)/right_join/inner_join/full_join - tidyverse 数据流:宽表读入 → pivot_longer → dplyr 分析 → ggplot2 可视化 → pivot_wider 报表
📝 作业
-
基础题:构造 1 个宽表(3 行 × 4 列:地区/Q1/Q2/Q3/Q4),用
pivot_longer转长表,再用pivot_wider转回宽表,验证数据完整无损。 -
基础题:构造 1 个 tibble 含
year_month列("2024-01"),用separate()拆成year和month两列,再用unite()合并回去。 -
基础题:模拟 2 个表(客户表 + 订单表),分别用 4 种 join 合并,记录每种 join 的结果行数差异。
-
进阶题:构造 1 个复杂宽表(列名格式:变量_年份_季度),用
pivot_longer + names_sep一步转长表,再用pivot_wider + names_glue转回宽表加前缀。 -
挑战题:模拟 1 个完整场景:① 读入 4 个分公司 × 4 季度的销售宽表 ② 读入分公司基本信息表(地区/经理/部门)③ left_join 合并 ④ 用 pivot_longer 转长 ⑤ 按经理分组汇总 ⑥ 写出汇总报表(宽表)⑦ 用 ggplot2 画"季度销售趋势图"(长表)。把整个流程的代码和结果截图保存。