尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
R语言dplyr核心函数group_by:分组聚合、分组残留与实战用法详解
接手一份订单明细表的时候我最先做的不是急着算汇总而是先把 dplyr 包加载进来然后对数据集执行 group_by() 分组。这个习惯是踩了无数次坑之后换来的。说白了group_by() 的核心作用就是把整个数据集按照给定变量切成逻辑上的一块一块让后面所有的聚合、排名、筛选都自动以组为单元工作。这篇文章不讲虚的就围绕这个函数把原理、玩法、坑和实战全部摊开讲一遍无论你是刚接触 R 的新手还是在 tidyverse 里泡了好几年但偶尔被分组残留问题绊一跤的老手都能从这里拿走点东西。1. 分组不是拆分数据而是给数据贴上分组标签1.1 先看一次最简单的分组操作我见过不少初学者第一次运行 group_by() 之后盯着控制台看了半天然后冒出一句数据怎么没变 这其实正是理解这个函数的关键。我用一份极简的模拟数据演示一下。library(dplyr) df - tibble( department c(研发, 研发, 研发, 市场, 市场, 销售, 销售), employee c(A, B, C, D, E, F, G), score c(85, 92, 78, 88, 95, 70, 82) ) grouped - df %% group_by(department) grouped此时打印 grouped你会看到表格下方多了一行提示Groups: department [3]。行数还是 7列还是 3数据本身一个字节都没动。区别在于这张表现在被打上了按部门分成了 3 组的标签。R 心里清楚研发那三行是一组市场那两行是一组销售那两行是一组。所以我会反复跟人说group_by() 不是真的把数据拆成了三个独立的小表而是给原表贴上了一层分组信息。这个理解特别重要因为它决定了你后面所有的操作逻辑。1.2 分组信息到底存在哪里如果你好奇这层分组信息到底放了什么可以用 attr() 去看看。attr(grouped, groups)返回的是一张小 tibble记录着每个组的分组键取值以及一个名为.rows的列表列里面是每一组对应的行号。比如研发这一组的.rows就包含c(1, 2, 3)。打个比方这就像给一本相册贴彩色标签红色的标签代表旅行照片蓝色的代表美食照片。相册的物理顺序没有变但当你只想要蓝色标签的照片时你只需要跟着标签去翻对应的那几页。group_by() 就是贴标签的动作后续的 summarise()、mutate() 就相当于只处理某种颜色的筛选动作。另外值得留意的是分组后的对象类型会变成grouped_df它本质上还是一个 data.frame所以大多数普通的 data.frame 操作仍然能作用在它上面只是部分函数会根据分组信息表现不同。1.3 新手第一个常识错误group_by 不等于汇总很多人以为执行完 group_by() 就会自动得到一个每个部门一行、带部门平均值的结果这是误解。group_by() 本身不返回任何统计摘要它只是改变后续操作的执行粒度。想要每组一行的汇总结果必须由 summarize()或拼写为 summarise()来承担。df %% group_by(department) %% summarise( 平均分 mean(score), 人数 n() )执行之后就能看到研发、市场、销售各一行这才是大多数人想要的分组汇总结果。所以请记住分组是过程汇总才是目的。如果只调用 group_by() 而不接任何后续操作那你只是给数据贴了标签但没有利用这些标签。2. 聚合、变换、筛选、排序分组之后能做的四类计算当数据被贴上分组标签之后dplyr 里有四类操作会默认按组工作这四类正好对应日常数据分析中最常用的动作。2.1 聚合summarise() 让每组变成一行分组汇总的核心就是 summarise()。它接收一系列聚合表达式对每一个分组分别计算然后输出一个新的 tibble每组占一行。df %% group_by(department) %% summarise( 平均分 mean(score), 最高分 max(score), 人数 n() )这里有几个高频使用的聚合函数我整理成了表格方便你直接查。函数含义常见场景n()组内行数统计每组的记录条数sum(x)组内求和销售额、成本、流量汇总mean(x)组内均值客单价、平均耗时median(x)组内中位数对偏态分布更稳健的集中趋势sd(x)组内标准差稳定性、波动程度min(x) / max(x)组内极值最低价、最高分n_distinct(x)组内去重计数活跃用户数、独立门店数first(x) / last(x)组内首个/末个首单时间、最后状态需要注意两个细节。第一summarise() 返回结果里分组变量会自动作为前几列出现不用手动 select()。第二summarise() 会丢掉未在聚合表达式中出现的普通列这是符合预期的行为因为那些列在组内有多行取值无法确定用哪一个。2.2 变换mutate() 把计算结果温柔地放回原表跟 summarise() 不同mutate() 不会减少行数。它会保留原有每一行然后把计算结果作为新列加回去。当数据处于分组状态时mutate() 计算的对象就是每个分组内部。df %% group_by(department) %% mutate( 组内排名 row_number(desc(score)), 相对均差 score - mean(score) )这个操作能玩出的花样非常多。比如你想算每个员工在自己部门内的绩效排名又不想丢失原始明细就可以用 group_by() mutate() row_number()。再比如电商场景里你想知道每个商品在自己所属类目下的销售额占比或者想算每个用户在自己注册月份里的消费排行都是一回事先按维度分组再在组内做计算。我特别推荐把分组后的 mutate()理解为逐组执行。每一组都会拿到一个独立的小环境组与组之间互不干扰。2.3 筛选filter() 和 slice() 只留下满足条件的行分组筛选最常见的需求是提取每组里最优的那几条。这里有两种写法。# 用 filter()条件写在组内 df %% group_by(department) %% filter(score max(score)) # 用 slice_max()直接按数值取前 n 名 df %% group_by(department) %% slice_max(score, n 1)两段代码的结果一样每个部门返回最高分员工的那一行。但 slice_max() 更省心它内部处理了并列情形的逻辑而 filter() 里如果恰好有两个人同分max(score) 会同时返回两行。类似的还有 slice_min() 取最低slice_head() 取每组前几行slice_sample() 每组随机抽几行。这些函数在排行榜、Top N 分析、抽样场景里非常实用。说句题外话我最常用的组合其实是 group_by() summarise() 算出排名指标再用 filter() 保留行数较少的关键结果比如每个门店销售额超过平均水平的品类。这种筛法对运营处理非常有效。2.4 排序别指望分组帮你自动做组内排序这里有个高频误区我必须单独拿出来强调。有人以为 group_by() 之后再用 arrange() 就会自动得到组内排序于是写下# 你以为会得到每个部门内部按分数排好序的结果 df %% group_by(department) %% arrange(desc(score))这个预期的方向是对的但 arrange() 并不是专门为分组设计的排序工具。它默认按照你传入的排序列做全局排序是否附带分组效果依赖版本和上下文非常容易踩坑。更好的做法有两条一条是把分组变量显式写进排序列例如arrange(department, desc(score))另一条是先分组再用 mutate() 生成组内排名例如df %% group_by(department) %% mutate(组内排名 row_number(desc(score))) %% arrange(department, 组内排名)说真的如果你只想看数据那就直接用 arrange() 把分组列和排序列都写清楚如果你想用组内排名参与后续计算那就老老实实走 mutate() row_number() 的路线。依赖隐式行为迟早要吃亏。3. 多变量分组与分组残留细节决定成败3.1 多变量分组group_by(store, category)实际数据里按一个变量分组往往不够。你要按门店看业绩又想知道门店里哪些品类贡献最大这就得用两个变量来分组。sales %% group_by(store, category) %% summarise(销售额 sum(revenue))group_by() 里传入多个变量时它会把它们组合成笛卡尔式交叉分组。举个例子如果有 3 个门店和 4 个品类那就是 3 × 4 12 个组。summarise() 输出里store 和 category 分别作为前两列每个组合占一行。多变量分组在业务上非常常见区域 × 品类、日期 × 渠道、用户 × 月份都是典型组合。你唯一要注意的是分组变量太多时组数会指数级膨胀遇到数据量又大又稀疏的情况性能会明显下降。3.2 .drop 参数因子里的空组要不要保留当分组变量是因子factor时group_by() 默认会丢弃那些在数据里没有出现的水平。但有些业务场景恰恰需要保留这些空组比如做完整的月度报表某个月某条产品线完全没销量你仍然希望那一行出现在结果里数值为 0。df - tibble( month factor(c(1, 1, 2), levels 1:3), value c(10, 20, 30) ) # 默认情况下月份 3 会被丢弃 df %% group_by(month) %% summarise(total sum(value)) # 保留空组 df %% group_by(month, .drop FALSE) %% summarise(total sum(value))加上.drop FALSE之后月份 3 也会作为一组出现在结果里total 为 0。这个参数在补齐时间序列、周度月度对齐、分类别汇总时非常实用很多人到很晚才会发现它。3.3 查看分组状态的几个实用函数清楚了分组标签之后有时候你想知道当前数据被分成了几组、每组行数多少、分组键分别是什么。dplyr 提供了几个专门的小函数。group_vars(sales) # 返回分组变量名 n_groups(sales) # 返回组数 group_keys(sales) # 返回分组键的唯一组合 group_size(sales) # 返回每组的行数这些函数最大的价值在于调试。当你在管道里写了很长的链式操作不确定分组状态是什么样的时候随手敲一下 group_vars()就能立刻知道当前对象是按什么分组的。我写复杂管道时几乎每写几步就会用它们排查一次。3.4 最容易被忽略的分组残留问题summarise() 有一个默认行为它在多变量分组之后只移除部分分组信息而不是全部移除。具体来说如果原始分组是 store category然后你执行 summarise()默认情况下结果会继续保留 category 这个分组。result - sales %% group_by(store, category) %% summarise(销售额 sum(revenue))此时 result 仍是按 category 分组的 grouped tibble。如果你没有意识到这一点紧接着在 result 上做 mutate() 或者 filter()计算会自动按 category 逐组进行而不是按你直觉里的整个 result 表进行。这一条就是无数人踩过的分组残留坑。解决办法很简单在 summarise() 里显式指定.groups参数或者干脆在管道末尾追加 ungroup()。.groups有四个取值我列在下面。取值行为drop_last默认值移除最后一个分组变量上面的例子会保留 categorydrop完全移除分组信息恢复普通 tibblekeep保留全部分组信息rowwise转成逐行模式每行一组我的习惯是只要 summarise() 后面还要做全局计算就立刻写.groups drop省得后面被分组残留坑到。如果你喜欢更直白的风格summarise() 之后紧跟 ungroup() 效果一样。4. 实战一份门店销售数据从分组到结论的全流程理论讲再多不如跑一遍真实案例。下面我构造一份模拟的门店销售明细数据把 group_by() 的各种玩法串起来走一遍。4.1 造一份能动手的模拟数据set.seed(2024) sales - tibble( store rep(c(门店A, 门店B, 门店C), each 24), region rep(c(北区, 东区, 南区), each 24), category rep(rep(c(数码, 家电, 服饰, 食品), each 6), times 3), month rep(1:6, times 12), revenue round(runif(72, 50, 200), 1), cost round(runif(72, 30, 120), 1) )这份数据一共 72 行3 个门店每个门店 4 个品类每个品类 6 个月。字段有门店、所属区域、品类、月份、销售额、成本。结构足够简单但已经能覆盖绝大多数分组需求。4.2 任务一按门店汇总业绩计算成本率第一个需求看每个门店整体的销售额、成本和成本率。store_summary - sales %% group_by(store) %% summarise( 总销售额 sum(revenue), 总成本 sum(cost), 订单数 n(), .groups drop ) %% mutate(成本率 总成本 / 总销售额) store_summary这里我的习惯是一口气在 summarise() 里完成所有聚合然后用.groups drop把分组清掉再接 mutate() 计算成本率。如果不加.groups drop结果仍然带着门店分组后面的 mutate() 也会安然执行看起来没区别但一旦后续逻辑复杂起来残留分组就会是隐患。4.3 任务二门店内各品类的销售额占比第二个需求看每个门店内部哪个品类贡献更高。最容易犯的错误就是忽略 summarise() 的分组残留默认值。# 错误示范依赖默认分组残留 bad_result - sales %% group_by(store, category) %% summarise(销售额 sum(revenue)) %% mutate(占比 销售额 / sum(销售额))这段代码里summarise() 之后结果默认还按照 category 分组所以 mutate() 里的sum(销售额)是按品类跨门店合计而不是该门店的合计。最后算出来的占比完全不是你想表达的意思。正确做法是先把分组清掉再按 store 分组一次或者直接用 ungroup()sales %% group_by(store, category) %% summarise(销售额 sum(revenue), .groups drop) %% group_by(store) %% mutate(占比 销售额 / sum(销售额)) %% arrange(store, desc(占比))这才是门店内部结构占比的准确解法。4.4 任务三提取每个门店销售额最高的两个品类这个需求用 slice_max() 非常顺手。sales %% group_by(store, category) %% summarise(销售额 sum(revenue), .groups drop) %% group_by(store) %% slice_max(销售额, n 2)先按 store category 汇总得到每个门店每个品类的小计然后再按 store 分组用 slice_max() 取每组前两名。整个过程读起来跟念一句话一样这就是 dplyr 管道表达力强的地方。4.5 任务四按月看每个品类的环比增速最后一个需求想知道每一个品类每月的销售额环比增长情况。这里需要先把数据折叠到品类 × 月份的粒度再按品类分组用 lag() 做环比。sales %% group_by(category, month) %% summarise(销售额 sum(revenue), .groups drop) %% group_by(category) %% arrange(category, month) %% mutate(环比增速 销售额 / lag(销售额) - 1)这里的核心逻辑是按 category 分组之后在组内用 arrange() 把月份排好lag() 取上个月的销售额从而算出环比。如果漏掉分组lag() 会跨品类取上一行结果整个就乱了。4.6 这套流程背后的一点思考把四个任务串起来看你会发现所有需求都逃不开同一个模式先想清楚以什么为组再用 summarise() 聚合、用 mutate() 在组内计算、用 slice_max() 提取关键行。这里面的关键不是代码多花哨而是你心里对这个组的边界足够清晰。我在实际做项目时凡是需要多层嵌套的分组逻辑都会先在草稿纸上写下第一层按什么分组、第二层按什么分组、哪一步清空分组能省下大量调试时间。分组残留问题在这套流程里尤其突出。因为环节一多你很容易忘记当前对象到底带着哪一层分组于是后面的 mutate() 或 filter() 就悄悄改变了语义。调试半天才反应过来其实查一下 group_vars() 就能立刻定位。5. 踩坑合集与性能思考真实数据里活下来的经验5.1 坑一summarise 后的分组残留这个坑我在前面已经反复提到因为它真的太普遍了。尤其是多个分组变量时summarise() 默认只移除最后一个分组变量以外的分组剩下的分组会悄悄影响后续操作。我的建议是要么在 summarise() 里写死.groups drop要么管道末尾加 ungroup()不要心存侥幸觉得这次不会出事。5.2 坑二忘记 ungroup() 导致下游工具行为异常分组残留的影响不止于 dplyr 内部。把做过 group_by() 的 tibble 直接交给 ggplot2 画图、交给 purrr 做 map 遍历、或者转成 data.frame 后再做其他处理时有些操作会感知到分组信息逐组执行有些则不会。这种不确定性会让你在排查问题时非常痛苦。一个典型的场景是连环画折线图你想画多条门店的月度趋势线结果却只画出了一条线或者线的连接关系莫名其妙。这时候大概率是数据还带着分组属性。解决办法就是在传给绘图函数之前明确调用 ungroup()把它变成普通 tibble。5.3 坑三sort 依赖隐式行为前面反复强调组内排序不要依赖 group_by() 对 arrange() 的隐式影响。你想要的组内排名用 mutate() row_number() 去生成排名列你想要的展示顺序用 arrange() 显式写出所有排序键。两步分开逻辑清清楚楚。5.4 坑四把 n() 当成普通函数n() 是专门用于分组上下文的计数函数它只能出现在 summarise()、mutate()、filter() 这类支持分组表达式的调用里不接收任何参数。如果你尝试在全局环境里单独调用 n()会直接报错。很多人刚开始不习惯这种只能在特定上下文中使用的函数但这正是理解 dplyr 分组逻辑的突破口。5.5 性能思考数据量上去了怎么办分组操作本身的开销不大真正影响性能的是组数和聚合函数的复杂度。比如几百万行的数据分成几十组做 sum() 和 mean()dplyr 完全扛得住如果分成几十万个组每个组还要做复杂的计算那速度就会明显下滑。遇到这种情况我一般先做三件事第一检查是不是做了不必要的过度分组。业务上很多分组维度是可以裁剪的比如每个用户每一秒这种粒度往往没必要。第二考虑用 data.table 的by语法替代。同样的聚合逻辑DT[, .(total sum(x)), by group]在超大表上的速度通常比 dplyr 快而且内存控制更好。我并不会因为自己是 dplyr 用户就排斥 data.table不同场景该换就换。第三如果数据本身在数据库里直接用 dbplyr 把 group_by() 和 summarise() 下推到 SQL 执行避免把几千万行全部加载进内存再分组。让数据库引擎去承担计算压力往往是最稳妥的方案。5.6 顺手到值得养成的几个小技巧最后分享几个我长期使用的小习惯单个看起来不起眼组合起来能省不少事。第一个是 group_by() 配合 across() 批量聚合。你想同时求多个数值列的均值可以这样写sales %% group_by(store) %% summarise( across(c(revenue, cost), mean, .names 平均{.col}) )第二个是用 cur_group_id() 给每组打一个数字编号。当你需要把分组结果传给自定义函数时这个编号可以作为组标识sales %% group_by(store, category) %% mutate(group_id cur_group_id())第三个是我个人的强制习惯任何管道里只要出现过 group_by()我在写下一个需要全局计算的步骤之前一定会先确认当前对象的分组状态。最省心的做法就是该分组时分组算完之后立刻 ungroup() 恢复普通表绝不让分组残留拖到下一个环节。group_by() 从来不是个复杂的函数它做的只是给数据贴标签但它带来的思考方式转变却非常关键数据不再是一整块而是由一组一组的小块构成所有计算都以小组为单位展开。把这个心智模型建立起来dplyr 这台机器才算真正开顺手了。
RELATED

相关推荐

电力行业国产化ERP成本管理方案:从核算到管控的落地实践

电力行业国产化ERP成本管理方案:从核算到管控的落地实践

电力行业的ERP建设,一直是个“看着标准化、落地极复杂”的领域。特别是成本管理模块,发、输、配、售四个环节的业务逻辑差异巨大,资产密集程度在全部工业门类里都排在最前列,加上电价监管对成本分摊的精细化要求,传统通…

📅 2026/10/15 23:01:12
软件工程教程第2版课后习题答案怎么用?计算题推导与复习索引指南

软件工程教程第2版课后习题答案怎么用?计算题推导与复习索引指南

简介:本资源为《软件工程教程》(第2版)(吴迪、马宏茹、丁万宁主编)的课后习题参考答案,面向高校软件工程课程学生及备考复习者,帮助对照教材章节核验选择题、填空题与简答题的作答思路。压缩包内共1个doc文档&#xff…

📅 2026/10/15 22:56:10
Django开发社区物业管理系统:报修与缴费模块实战详解

Django开发社区物业管理系统:报修与缴费模块实战详解

社区物业管理系统听起来平平无奇,但真正上手做过的人才知道,要把报修、缴费、公告、访客这些看似简单的场景理清楚,背后要趟的坑一点不比做一个电商平台少。这个“城市生活e家平台”的项目,我前后花了差不多三个月时间&#xff0c…

📅 2026/10/15 22:56:10
MORE NEWS

更多资讯

📰

Web自动化测试环境搭建指南:Selenium与Python实战

1. 为什么Web自动化测试的环境搭建总让人卡在第一步做过Web自动化测试的人都有一个共同感受:写测试脚本本身可能只占三成时间,剩下七成全耗在环境搭建和调试上。浏览器驱动版本对不上、依赖包装完跑不起来、元素定位一会儿找得到一会儿找不到——这些问题…

📰

滚动时域最优视角规划:面向真实场景的3D探索决策系统

1. 这不是“找角度”的玄学,而是一套可计算、可落地的3D空间探索决策系统你有没有试过让一个机器人在陌生的地下管道里自主前进?它面前不是一条笔直的走廊,而是不断分叉、塌陷、堆满杂物的三维迷宫。激光雷达扫出一片点云,但只覆盖…

📰

华为单板硬件机考实战指南:电源检查、信号诊断与故障注入法

简介:本资源是面向华为硬件开发/设计工程师岗位应聘者的2024届单板硬件机考专项复习资料,聚焦高频考点与易错点解析,助力求职者高效突破硬件基础理论瓶颈。内容覆盖RC定时电路电容选型、二极管温度特性、电解电容低温行为、IGBT原理、信号抖动…

📰

华为硬件机考实操指南:供电验证、信号完整性与故障定位

简介:本资源是面向华为硬件开发/设计工程师岗位应聘者的2024届单板硬件机考专项复习资料,聚焦高频考点与易错点解析,助力应届生高效备考、精准突破。内容覆盖RC定时电路电容选型、二极管温度特性、电解电容低温行为、IGBT原理、信号抖动与匹配…

📰

HAPS原型验证调试全指南:从信号注入到Deep Trace实战

简介:本资源是Synopsys官方发布的《HAPS调试软件protocompiler debugger用户手册》(2021年9月版),面向数字电路原型验证工程师、FPGA开发人员及SoC硬件验证从业者,聚焦HAPS高性能原型验证平台的底层调试能力建设。手册…

📰

632流程变革框架:从分层架构到落地执行的全链路指南

简介:这份PPT资源聚焦美的集团632项目流程变革框架规划设计方案,面向企业流程管理、IT规划与数字化转型从业者,以及需要研究大型集团流程重构案例的学习者。内容围绕六大运营系统与三大管理平台展开,涵盖EPF企业流程框架的L1至L4设…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬