先别急着怀疑人生。dplyr::group_by()之后只显示一条记录我在帮身边同事和朋友排查问题时遇到过太多次——十个里有八个是虚惊一场数据根本没丢只是控制台显示规则和你的预期对不上但剩下两个确实是代码逻辑出了问题你对分组计算的理解和 dplyr 的实际执行方式之间隔着一条分组返回值到底有几行的鸿沟。这篇就把这两种情况彻底拆开第一部分教你怎么用几秒钟判断数据是真丢还是假丢后面几个部分逐一分析summarise()折叠、mutate()向量回收、因子空组被丢弃这几类真正导致只剩一行的根因最后给出一套可以照着操作的排查链路和常用写法的对照表。无论你是刚接触 tidyverse 的新手还是要帮别人 debug 的老手应该都能在里面找到对应的场景。1. 先分清显示只给一行和数据只剩一行这是完全不同的两个问题1.1 控制台少打了几行不代表你的数据真的只剩一行我见过最典型的一次误报是这样的某个开发者在自己的环境里跑了一段非常常规的代码library(dplyr) set.seed(2024) df - tibble::tibble( grp rep(c(A, B, C), each 10), val rnorm(30) ) df %% group_by(grp) %% summarise(mean_val mean(val))这段代码本身完全正确输出的result应该是一个三行的 tibble分别是对应 A、B、C 三组算出的均值。但他在 RStudio 的控制台里看到的输出却好像只有一行或几行于是跑来问我数据是不是被吞了。问题出在 RStudio 和 R 的 tibble 打印机制上。tibble 默认只打印tibble.print_max指定的行数默认是 20 行一旦行数超过这个阈值控制台只显示前面一部分行和后面一小部分中间用省略号带过。更隐蔽的问题是很多人为了调试方便会在.Rprofile里设置options(tibble.print_max 1)或options(dplyr.print_max 1)之后所有输出在控制台里都只显示第一行。排查了半天代码最后发现是配置问题这种情况一点都不少见。1.2 判断数据真实行数的三个命令遇到只有一条记录的显示时我建议你第一反应不要去看控制台输出而是直接确认数据的真实维度。最直接的三条命令result %% nrow() # 真实行数 result %% dim() # 行列数 result %% str() # 完整结构包括分组信息 result %% glimpse() # 列概况和类型nrow()返回真实行数str()能看到当前对象是否为grouped_df分组后的 DataFrame以及分组键是什么。如果你发现nrow()明明返回 100但控制台只显示一行那极大概率是打印配置问题。View(result)是 RStudio 里最直观的核验方式弹出来的表格会展示完整数据不会因为打印阈值少显示内容。提示print(result, n Inf)可以强制显示所有行print(result, width Inf)可以显示所有列。在调试阶段这两个参数比反复改 options 要省事得多。2. summarise() 折叠为什么明明有很多组最后只剩一条2.1 分组键只有一个取值summarise 自然只有一行确认完数据真实行数之后如果 nrow() 真的返回 1那我们就要开始盯代码了。第一个要排查的就是summarise()。很多人对summarise()的直觉是我按某个列分组它应该保留原来的多行记录但实际上summarise()的工作方式是把每一组压缩成一行输出行数 组数。如果分组键那一列只有一个取值那么无论原始数据有 100 行还是 10000 行输出永远是一行。df_single - tibble::tibble( group rep(all, 100), value rnorm(100) ) df_single %% group_by(group) %% summarise(mean_value mean(value))这里group列只有all一种取值所以输出只有一行。这个场景在真实项目中很常见——比如某人想按某个条件分组但条件列是从外部读入的数据恰好这一批数据里所有行都满足同一个条件或者条件列在数据清洗阶段被不小心覆盖成了常量。这不是 bug是summarise()的正常行为但非常容易让人觉得代码坏了。2.2 .groups 参数summarise 之后的分组状态被很多人忽略dplyr 1.0 之后summarise()的输出默认会携带一条警告提示summarise()正在 ungrouping 输出建议用.groups参数覆盖默认行为。很多人直接忽略这条警告但它背后藏着一个隐患当你有多个分组变量时summarise()之后结果可能仍然保留部分分组状态导致下一步的mutate()或filter()作用于错误的分组层级。df_two - tibble::tibble( group rep(c(A, B), each 10), sub rep(c(x, y), length.out 20), value rnorm(20) ) df_two %% group_by(group, sub) %% summarise(mean_val mean(value))默认.groups drop_last时结果会丢弃最后一个分组变量sub的分组信息只保留group的分组状态。如果你紧接着写mutate(...)它会在group这个层级上按组操作而不是对整个结果做全局操作。一旦统计口径错了结果看起来就像数据少了甚至在某组只有一个取值时连锁反应就会造成只剩一行的假象。建议在每次summarise()时显式指定.groups参数如果你后面还要按组操作用.groups keep如果你希望彻底取消分组用.groups drop如果你只做单一分组变量其实无所谓但养成显式声明的习惯可以少踩很多坑。2.3 一个直接把多行压成一行的写法summarise 里放了普通运算还有一种情况极其常见用户想计算每行占组内总和的百分比于是写出了类似下面的代码df %% group_by(grp) %% summarise(ratio val / sum(val))运行之后 dplyr 会立刻抛出一个报错大意是ratio的长度必须是 1一个 summary value但实际长度为 10因为val在每组里有 10 个值而summarise()要求每个输出列都必须是单值向量。这类报错正是标题里说的报错解析的核心场景——报错本身其实是 dplyr 在帮你定位问题。如果你的目的是保留每一行的原数据同时算占比正确写法是把summarise()换成mutate()因为mutate()允许标量回收会把sum(val)这个单值自动填充到组内的每一行再逐行除以val。3. mutate() 与向量回收想保留原行数却误用了压缩函数3.1 标量回收是分组计算的正确基础也是误用的来源标题里实际应多条记录这个描述指向的另一大类场景就是用户真的想要多条记录但写代码时用错了动词。mutate()和summarise()的关键差异在于返回值长度不同summarise()每组返回一行相当于把组压缩成一个摘要。mutate()每组返回与组内行数相同的向量相当于给每一行增加或覆盖一列。mutate()能做到这一点依赖的是 R 的向量回收机制。当你写df %% group_by(grp) %% mutate(grp_mean mean(val))时mean(val)返回一个长度为 1 的标量dplyr 会把它自动广播到当前组的每一行。所以最终结果行数和原始数据一致每组都有对应的均值列。这个机制用对了是神器用错了就是灾难。比如有人写df %% group_by(grp) %% mutate(ratio val / sum(val))时没问题因为sum(val)是标量val / sum(val)会逐行算出占比行数不变。但同样这段代码如果套在summarise()里就变成了报错或者有的人在summarise()里用sum(val) / n()也算均值结果每组返回一行看起来就是记录数骤减。3.2 summarise 里放除法/比例计算会报什么错报错信息怎么读很多开发者第一次看到must be length 1, not length 10这类报错时第一反应是慌第二反应是怀疑自己分组条件写错了。其实报错就已经把所有信息给全了dplyr 提示你在summarise()的返回列里出现了一个长度大于 1 的向量而这个函数不接受这样的输出。这时候正确的排查路径是df %% group_by(grp) %% mutate(ratio val / sum(val)) # 每行保留ratio 是占比如果你想要的是把每组压缩成一行但又希望这一行里包含某些逐行信息那你需要显式决定如何把多行合并成一行比如用mean(val / sum(val))、weighted.mean(...)或first(val)。把逐行操作交给mutate()把组级摘要交给summarise()这两个函数的职责划分清楚之后很多诡异的结果都能立刻恢复正常。提示如果一段代码在group_by()之后只想保留每组的最后一行或第一行slice_tail()、slice_head()才是顺手的选择不要硬用summarise()加上各种奇奇怪怪的聚合拼出来。4. 空白组被丢弃因子水平明明有多个汇总结果却少了行4.1 默认 group_by 不保留空组第三种真正导致只有一条记录应该多条的根因和因子有关。R 里的因子factor有一个特点它允许你声明可出现的水平即使数据里没有某个水平的实际观测值。举个例子你的数据是一个调查问卷结果weekday列是因子水平是周一、周二、周三、周四、周五。但这一批受访者只在周一和周二的记录周三到周五一行都没有。当你执行df_week - tibble::tibble( weekday factor( c(周一, 周一), levels c(周一, 周二, 周三, 周四, 周五) ), score c(88, 92) ) df_week %% group_by(weekday) %% summarise(avg_score mean(score))默认情况下dplyr 不会为那些没有数据的因子水平生成空组所以输出只有一行周一而不是预期的五行周一至周五其余为 NA 或 0。如果你脑子里想的是因子有五个水平所以结果应该有五条那这个结果就会让你误以为数据被吞掉了。4.2 .drop FALSE 的正确打开方式解决办法是在group_by()里显式声明.drop FALSE告诉 dplyr不要把空的因子水平丢掉df_week %% group_by(weekday, .drop FALSE) %% summarise(avg_score mean(score), .groups drop)这样输出就会包含周一、周二、周三、周四、周五五个组没有观测值的组会用NaN或NA填充取决于聚合函数。如果你希望空组显示 0可以在summarise()里用coalesce()或直接在mutate()中把NA替换掉。还有另一个相关技巧tidyr::complete()可以按因子水平或数据中出现的所有组合补全空行适合做宽表填充。但如果你只是想在分组汇总里保留所有水平.drop FALSE是最轻量的选择。这个坑非常隐蔽因为大部分时候因子水平在数据里都有观测问题只会在某段时间数据量偏少、恰好某个分类缺失时突然爆发。报错没有代码不红但结果就是少了几行特别容易让人怀疑人生。5. 一条链路排查完这个一行问题可复现5.1 五步定位法如果你也被这个问题缠住了不要东一榔头西一棒子地试我建议按下面这套顺序一条链走下来。每一步的目的都很明确最后一定能定位到具体原因。第一步确认真实行数。result %% nrow()如果 nrow() 大于 1请直接跳到文章第 1 节再检查打印规则如果 nrow() 真的等于 1继续往下走。第二步检查分组键的基数。df %% summarise(n_distinct(group_col))如果分组键只有一个取值那么summarise()输出一行是必然结果。你需要回头问一句这个分组列是不是被意外覆盖成了常量是否应该在分组前先mutate()生成一个更细粒度的分组键第三步检查分组键的类型和水平。df$group_col %% levels() # 如果是因子 df$group_col %% unique() # 实际出现的取值如果水平的数量明显大于唯一取值的数量说明存在空组问题用.drop FALSE解决。第四步检查 summarise() 里的输出列。看一眼summarise()的花括号里写的是标量聚合mean()、sum()、n()、max()、min()还是向量运算。如果是向量运算要么换成mutate()要么把向量运算包在某个聚合函数里。这里最常见的就是占比计算误写在summarise()里。第五步检查警告信息。R 不会平白无故警告。如果你在运行结果上方看到summarise()相关的 ungrouping 警告先停下把它当作有效信息思考一下.groups参数是否影响了你后续操作的分组状态。5.2 从报错信息反推错误类型R 的报错虽然有时候格式吓人但其实信息非常结构化。拿summarise()里放val / sum(val)这个例子来说Error in dplyr::summarise(): ! Problem while computing ratio val/sum(val). Caused by error in dplyr::summarise(): ! Columns ratio must be length 1 (a summary value), not 10.这个报错一共给了你三个关键信息出错的计算表达式是哪个列、出错的函数是summarise()、以及出错的本质是长度不匹配。很多人只看到Error两个字就慌了其实只要报错信息里出现了must be length 1 (a summary value)你几乎可以立刻锁定问题出在我把逐行操作写进了摘要函数。报错不是敌人是调试指路牌。我把这种排查逻辑总结成了一张决策表方便你截图或记笔记现象优先怀疑验证方式通常解法控制台显示一行nrow 远超 1tibble 打印配置nrow()、View()options(tibble.print_max 20)或print(n Inf)nrow 等于 1分组键只有单一取值分组键被覆盖或本来就是常量n_distinct(group_col)修正分组列或接受这是summarise()的正常行为nrow 等于 1因子水平没丢但分组键是多值空组被丢弃levels()vsunique()group_by(.drop FALSE)summarise 报错长度必须为 1向量运算误入摘要函数看报错中的列名换成mutate()或在外面包聚合函数结果行数少于预期且上方有 ungrouping 警告.groups参数影响后续操作str(result)看分组信息显式使用.groups drop或.groups keep6. 分组计算的正确姿势六种常用需求的写法对照6.1 需求与写法的对照表排查完问题最终还是要回到正确的写法上来。我把日常工作中分组计算的常见需求整理成一个对照表每次拿不准时直接查这张表基本能避开大部分坑需求正确写法行数变化极易犯的错误每组汇总成一行均值、总数等group_by() summarise()行数变为组数保留所有行同时附带组内统计量group_by() mutate()行数不变每组取前 N 行或后 N 行group_by() slice_head(n N)每组最多 N 行按组计数频数统计count()或 group_by() summarise(n n())行数变为组数只保留满足组级条件的整组数据group_by() filter(n() 10)保留符合条件的组的所有行因子水平有缺失但仍要保留空组group_by(v, .drop FALSE)行数至少等于水平数忘记.drop参数这里特别想强调一下slice_head()和head()的区别。head()不会感知分组信息它只会从整个数据框顶部取 N 行而slice_head()在分组后的数据框上执行时会按照分组各自取前 N 行。我曾经见过有人想在每一组中取出最大值所在的那一行写了group_by(grp) | arrange(desc(value)) | head(1)结果只拿到了整个数据框的最大值所在行其他组全丢了。正确写法是slice_max(value, n 1)它会感知分组信息为每一组都返回一行。6.2 ungroup() 的收尾习惯很多人跑完summarise()就直接把结果往下游传但忽略了分组元数据可能还残留在对象上。dplyr 的grouped_df会在你后续调用mutate()、filter()、left_join()时继续生效一旦下游代码基于现在应该是全局数据的假设写逻辑结果就会非常不可控。我个人的习惯是当一段分组计算结束时如果后续操作不应该再按组进行就在管道末尾显式加上ungroup()。这样写出来的代码可读性更高也不会把隐性的分组状态带到下一步。比如df %% group_by(grp) %% summarise(mean_val mean(val), .groups drop) %% ungroup() %% mutate(global_rank min_rank(desc(mean_val)))虽然.groups drop已经取消了分组但再写一个ungroup()也无伤大雅还能让阅读代码的人一眼就知道从这里开始分组状态彻底结束了。最后再分享一个小经验每次在group_by()之后发现结果行数和预期不一致不要急着改代码先跑一遍nrow()和n_distinct()。把预期的行数和真实的行数摆到一起看绝大多数情况下问题原因自己就浮出来了。如果这篇排查思路能帮你少抓一次头发那这个坑踩得也算值了。