Menu

R 语言数据重塑:pivot_wider 和 pivot_longer(宽格式与长格式)

宽格式和长格式数据的区别,以及如何相互转换:tidyr 的 pivot_longer() 和 pivot_wider()、旧的 spread/gather,以及基础 R 的 reshape()。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

宽格式与长格式:同一份数据,两种形状

所有数据重塑的问题,归根结底都是同一个区别,所以我们先把它看清楚。下面是一份很小的数据集,两个城市的季度销售额,分别用两种形状构建:

信息完全相同,结构却不一样。宽格式每个城市一行、每个季度一列,季度信息藏在列名里。长格式每个观测值(城市 × 季度)一行,季度降级成了一个普通的列,所有测量值都放在同一列 sales 里。

两种形状都不算“正确”,它们服务于不同的对象。宽格式是给人看的,也是电子表格导出的样子:紧凑,一眼就能比较。长格式是给工具用的。在两者之间转换叫作透视(pivoting,也叫重塑 reshaping,说的是同一件事)。

为什么分析时长格式更好

看看宽格式把“季度”这个概念变成了什么:它把一个变量拆碎成了多个列名。列名是没法参与计算的。到了长格式,quarter 重新变回了数据,分组分析工具箱里的所有工具都能用了:

  • aggregate(sales ~ quarter, ...) 或 group_by(quarter):在宽格式下做不到,因为每个季度是单独的一列,只能手动处理(分组汇总全都假定数据是长格式)。
  • ggplot2 把列映射到图形属性上:aes(x = quarter, y = sales, color = city) 要求这三列都存在。用宽格式数据画图是一场持续的战斗;用长格式数据画图只要一行。
  • 新增 Q3 只是增加几行,结构不变;而宽格式要多出一列,后续每一步都得知道这件事。

实用的原则是:用长格式存储和分析,用宽格式展示。 这就是为什么下面两种转换用得那么多:数据从电子表格以宽格式进来,为了分析转成长格式,最后为了报告里的表格再转回宽格式。

宽转长:pivot_longer()

现代的数据重塑由 tidyr 包(tidyverse 中 dplyr 的兄弟包)负责。这里的代码片段是静态的,因为沙盒只运行基础 R。转成长格式需要做三个决定,每个决定对应一个参数:

library(tidyr)

long <- pivot_longer(wide,
    cols      = c(Q1, Q2),      # which columns to stack
    names_to  = "quarter",      # new column that receives the old NAMES
    values_to = "sales"         # new column that receives the cell VALUES
)

逐步看一下:cols 指定要拆散的列(也可以用 Q1:Q2 这样的范围语法,或 starts_with("Q") 这样的辅助函数,列有二十个时很方便)。每个被选中的单元格变成一行;它来自哪一列,就记在 quarter 中,数值本身放进 sales。没有列在 cols 中的列(这里是 city)会被当作标识列,在各行中重复。结果和上面打印出来的 long 数据框完全一样。

长转宽:pivot_wider()

反方向需要做两个决定:新列名从哪里来,以及用什么填充:

wide <- pivot_wider(long,
    names_from  = quarter,   # distinct values here become new columns
    values_from = sales      # these values fill the cells
)

quarter 中每个不同的值(Q1、Q2)都会变成一列;每个单元格用城市和季度都匹配的那一行的 sales 值填充。其余的列(city)作为行标识,每种不同的组合输出一行。某个城市缺了某个季度,那个单元格就是 NA(用 values_fill 参数可以换成别的值,比如计数数据用 values_fill = 0)。

在老教程里你还会遇到上一代的函数:spread() 就是 pivot_wider(),gather() 就是 pivot_longer()。从 tidyr 1.0 起它们就已被取代,虽然仍能用,但认得出来就够了,不值得专门学。

基础 R 有 reshape(),先说句实话

基础 R 不用装包也能做到,用的是 reshape()。这个函数以难懂出名,它自己的文档过去甚至为此道过歉。它是围绕纵向研究的术语设计的(idvar、timevar、direction),这些参数名和日常数据对应得很别扭,每次用完都会忘。不过它确实能用:

注意输出的列名:sales.Q1、sales.Q2,值列的名字被拼接到了每一列上。应急时、在不能有任何依赖的环境里,或者在老代码中遇到它时,这样也没问题。但如果你一年要重塑数据不止一次,老实说还是建议 install.packages("tidyr"):在所有数据整理任务中,只有这一项是基础 R 的工具带来的麻烦真的比省下的依赖更多。

转宽格式时的重复键陷阱

转宽格式的前提是,每个“标识 + 名字”组合只对应一个值。如果 Lima 有两行 Q1,那唯一的 Q1 单元格该放哪个数?pivot_wider() 拒绝猜测:它会给出警告(values are not uniquely identified),并在单元格里放一个列表列,也就是一个嵌套了列表的数据框,你接下来对它做的任何操作都会出问题。

看到这个警告时,不要急着用 values_fn 这个逃生出口,先问问为什么会有重复。通常是数据的粒度比你以为的更细(是按天的行,而不是按季度,所以要先汇总再转宽格式),或者上游的连接操作复制了行。只有当你能明确说出重复值应该按哪种方式聚合时,values_fn = mean(或 sum)才是正当的修复办法。reshape() 在这方面更糟:它会悄悄保留第一个重复值,丢掉其余的,只给出一条很容易被滚动过去的警告。

要点回顾

  • 宽格式:变量藏在列名里,适合阅读。长格式:每个观测值一行,适合分析和 ggplot2。
  • 用长格式存储和分析,用宽格式展示。
  • pivot_longer(cols, names_to, values_to) 把列堆叠成行;没有列出的列成为重复的标识列。
  • pivot_wider(names_from, values_from) 把行展开成列;空缺处填 NA。
  • spread()/gather() 是旧名字;基础 R 的 reshape() 能用,但出了名地别扭。
  • “标识 + 名字”组合重复会让转宽格式产生歧义:先汇总,而不是简单地把警告压下去。

下一篇:从文件中读取真实数据,read.csv() 以及它的那些坑。

常见问题

R 中宽格式数据和长格式数据有什么区别?

数据相同,形状不同。宽格式把一个变量分散到多列中(比如每个季度一列),每个对象一行,方便人阅读。长格式把它堆叠成行,每个观测值一行,有一列存名字、一列存值,适合分组分析和 ggplot2。

R 中怎么把宽格式数据转成长格式?

用 tidyr 的 pivot_longer(df, cols = Q1:Q4, names_to = "quarter", values_to = "sales"),把选中的列堆叠成两个新列:原来的列名放进 names_to 指定的列,单元格的值放进 values_to 指定的列。

R 中怎么把长格式数据转成宽格式?

用 tidyr 的 pivot_wider(df, names_from = quarter, values_from = sales),把行展开成列:names_from 中每个不同的值都会变成一列,并用对应的 values_from 值填充。缺失的组合会变成 NA。

R 中用什么替代了 spread 和 gather?

在 tidyr 1.0(2019 年)中,pivot_wider() 取代了 spread(),pivot_longer() 取代了 gather()。旧函数仍然可以用,你会在一些老教程里看到它们,但新代码都应该使用 pivot_* 这一对,它们的参数更清晰,功能也更多。

Coddy 编程语言插图

和 Coddy 一起学编程

开始