文 / 老汪(汽车零部件财务与商业智能实战派)
专栏:【Excel BI 财务建模实战课讲义(第2阶段)】 · 期数:EP041
核心函数:Table.Group/List.Sum/Table.RowCount/Number.Round
📌 老汪开篇导读:车间月度成本结算与多维分组聚合的工业刚需
各位财务同仁、成本会计、车间统计员与经营分析BP:
在咱们汽车零部件制造企业(Tier 1 一级供应商)月末开月度生产经营分析会的时候,财务部和制造部最怕面对的就是一堆毫无归集的“长明细账”:
– 压铸车间有几百张工单在交替压铸铝合金控制臂和电池壳体;
– 机加车间的 CNC 产线 A 组、B 组夜以继日地跑削切工时;
– 冲压焊接线更是几十道工序频繁切换。
每到月末结账,工厂总经理和生产副总总会把报表往桌上一扔:
“老汪,别给我看几千行的流水账!我就要看:每个车间、每条产线,这个月一共结了多少个工单?总产量是多少?实际耗用了多少工时?到底超耗了多少标准工时?哪条线产生的废品损失最严重?而且——当我看到某条线数据异常时,你能不能立刻把该线的原始明细完整给我下钻穿透看出来?!”
很多习惯在 Excel 里用“数据透视表”的伙伴可能会说:“老汪,这不就是个透视表的事儿吗?”
平时做单机临时分析确实可以随手拉个透视表,但在构建端到端全自动化的财务 ETL 管道中,数据透视表存在致命短板:
1. 无法在管道内存中继续流转:透视表是界面的终点产物,不能作为下游清洗步骤的输入源;
2. 无法原生保留明细子表(Sub-table):透视表汇总后明细就没了,无法实现“一行汇总指标 + 一个内置子表对象”的高级结构嵌套;
3. 无法应对复杂的复合指标运算:比如在汇总的同时计算 [实际工时] - [标准工时] 的净差异率、或者按局部连续状态分组。
在 Power Query M 函数库中,承载所有多维统计与指标汇聚的核武器级函数,正是——Table.Group!
🛠️ 第一部分:老法师拆解底层——Table.Group 五大参数全景解密
很多初学者在界面上点击“分组依据”按钮,只能做非常简单的单列单指标求和。想要随心所欲驾驭多字段、多指标并行统计与子表嵌套,必须吃透它的完整 M 语法签名:
Table.Group(
table as table, // 1. 目标数据表
key as any, // 2. 分组字段(单列名 或 多列复合键)
aggregatedColumns as list, // 3. 聚合计算定义列表(核心核心!)
optional groupKind as nullable number,// 4. 可选:分组模式(全局分组 vs 局部连续分组)
optional comparer as nullable function// 5. 可选:自定义比较逻辑
) as table
1. 第 2 参数:单列 vs 多列复合键(Composite Key)
- 单字段分组:直接写字符串,如
"车间名称"; - 多字段复合分组:用大括号列表传入多个字段,如
{"车间名称", "生产产线"}。M 引擎会自动以两者的笛卡尔唯一组合作为统计主键!
2. 第 3 参数:聚合列表结构与神级标识符 each _
这是 Table.Group 的灵魂所在!它是一个由列表构成的嵌套列表:
{
{"工单笔数", each Table.RowCount(_), Int64.Type},
{"完工总产量", each List.Sum([#"完工数量(件)"]), Int64.Type},
{"明细子表", each _, type table}
}
💡 老汪独家内功心法:
在each表达式后面的下划线_,代表什么?
它代表当前这一组数据筛选出来的全部行构成的独立子表(Sub-table)!
– 如果你想算这组有多少行,就对子表求行数:Table.RowCount(_);
– 如果你想算子表某列的总和,就对子表的列做列表求和:List.Sum([#"完工数量(件)"])([列名]实际上是_[列名]的语法糖);
– 如果你想把这组原始数据原封不动封存在一个单元格里,就直接写:each _!这一招能瞬间把表格变成“主子表嵌套结构”,在后续报表穿透和高级过滤中堪称神技!
3. 第 4 参数:GroupKind(全局分组 vs 局部连续分组)
GroupKind.Global(默认,取值0):全局归集。只要分组字段相同,不管行散落在哪里,全表归为一组(相当于 SQL 的GROUP BY);GroupKind.Local(取值1):局部连续分组!仅当相邻连续行的字段相同时才分为一组,一旦中间出现不同值就开启全新组!
(制造实战:用于计算产线“连续停机批次”、“连续废品发生次数”,遇到正常批次中断,立即重开新组!这是传统 SQL 极难写而 PQ 举重若轻的高阶功能)。
🏭 第二部分:汽车零部件制造工单“月度经营看板”实战
1. 业务场景背景
某汽车底盘零部件制造基地,月末收集到各车间 50 余笔工单流水(涉及精密压铸车间、CNC机加车间、冲压焊接车间):
– 包含字段:工单号、车间名称、生产产线、零件图号、零件名称、完工数量(件)、标准工时(h)、实际工时(h)、废品损失(元)。
– 管理要求:
按 【车间名称 + 生产产线】 复合分组,同步输出 6 大经营指标:
1. 工单笔数(该产线月度开单频次);
2. 完工总产量(件)(产线总产出);
3. 总标准工时(h)(产线理论应耗工时);
4. 总实际工时(h)(车间实际打卡与报工工时);
5. 工时超耗差异(h)(实际工时 - 标准工时,超耗为正,节约为负);
6. 累计废品损失(元)(品质缺陷损失考核金额)。
💻 第三部分:随堂课件完整 M 脚本工程实现
💡 老汪敲黑板(免改路径直接刷新秘籍):
平时做财务分析最怕换台电脑一刷新就报错“找不到文件路径”。老汪在给各位学员配套的实操课件【配置】表 A1 单元格中预置了自适应公式,大家把本课案例包解压后直接点击 Excel 的【全部刷新】即可秒级重现,绝不报红字!大家可以把这套自适应架构无缝迁移到公司的日常模型中! 实操指南点此篇文章:《实操指南》
// =========================================================================
// 专栏:【Excel BI 财务建模实战课讲义(第2阶段)】· EP041
// 模块:Table.Group 表格多维分组聚合与指标多重汇总
// 讲师:老汪(汽车零部件财务与商业智能实战派)
// =========================================================================
let
// ---------------------------------------------------------------------
// 步骤 1:【Q001 自适应路径规范】动态定位当前工作簿同级目录
// ---------------------------------------------------------------------
当前目录 = try
let
rawPath = Text.From(Excel.CurrentWorkbook(){[Name="当前路径"]}[Content]{0}[Column1]),
cleanPath = Text.Trim(rawPath),
fixedPath = if Text.EndsWith(cleanPath, "\") then cleanPath else cleanPath & "\"
in
fixedPath
otherwise
"./",
// ---------------------------------------------------------------------
// 步骤 2:动态读取同级「原始业务数据表.xlsx」中工单生产与工时明细流水
// ---------------------------------------------------------------------
数据文件路径 = 当前目录 & "原始业务数据表.xlsx",
源文件 = Excel.Workbook(File.Contents(数据文件路径), null, true),
流水表 = 源文件{[Item="工单生产与工时明细流水", Kind="Sheet"]}[Data],
提升的标题 = Table.PromoteHeaders(流水表, [PromoteAllScalars=true]),
更改的类型 = Table.TransformColumnTypes(提升的标题, {
{"工单号", type text},
{"车间名称", type text},
{"生产产线", type text},
{"零件图号", type text},
{"零件名称", type text},
{"完工数量(件)", Int64.Type},
{"标准工时(h)", type number},
{"实际工时(h)", type number},
{"废品损失(元)", type number}
}),
// ---------------------------------------------------------------------
// 步骤 3:核心算子 Table.Group 执行多维度复合分组与 6 重指标并行聚合
// 【老汪深度拆解】:
// 1. 分组键:{"车间名称", "生产产线"}(双字段复合主键)
// 2. 聚合列表:嵌套列表,每个子项定义 {列名, each 计算表达式, 类型}
// 3. each 内部的下划线 _ 代表该分组对应的“子数据表”!
// ---------------------------------------------------------------------
分组聚合结果 = Table.Group(
更改的类型,
{"车间名称", "生产产线"},
{
// 指标 1:工单笔数(计算当前分组子表的总行数)
{"工单笔数", each Table.RowCount(_), Int64.Type},
// 指标 2:完工总产量(从子表中提取完工数量列并求和)
{"完工总产量(件)", each List.Sum([#"完工数量(件)"]), Int64.Type},
// 指标 3:总标准工时
{"总标准工时(h)", each Number.Round(List.Sum([#"标准工时(h)"]), 1), type number},
// 指标 4:总实际工时
{"总实际工时(h)", each Number.Round(List.Sum([#"实际工时(h)"]), 1), type number},
// 指标 5:工时超耗差异(实际工时 - 标准工时,正数代表超耗,负数代表节约)
{"工时超耗差异(h)", each Number.Round(List.Sum([#"实际工时(h)"]) - List.Sum([#"标准工时(h)"]), 1), type number},
// 指标 6:累计废品损失金额
{"累计废品损失(元)", each Number.Round(List.Sum([#"废品损失(元)"]), 1), type number},
// 指标 7(老法师高阶特技):保留子表数据,供后续穿透钻取明细!
{"分组明细表", each _, type table}
}
),
// ---------------------------------------------------------------------
// 步骤 4:移除明细子表列(仅保留汇总看板交付经营层,亦可按需保留)
// ---------------------------------------------------------------------
移除子表列 = Table.RemoveColumns(分组聚合结果, {"分组明细表"}),
// ---------------------------------------------------------------------
// 步骤 5:按车间与产线规范排序
// ---------------------------------------------------------------------
最终排序 = Table.Sort(移除子表列, {{"车间名称", Order.Ascending}, {"生产产线", Order.Ascending}})
in
最终排序
📊 第四部分:清洗前后数据效果对照表
1. 原始工单生产与工时流水表(50余行明细节选)
| 工单号 | 车间名称 | 生产产线 | 零件图号 | 零件名称 | 完工数量(件) | 标准工时(h) | 实际工时(h) | 废品损失(元) |
|---|---|---|---|---|---|---|---|---|
| WO-202611-002 | 精密压铸车间 | 产线-DC01-500T | P-8101-ARM | 铝合金副车架左控制臂毛坯 | 120 | 36.0 | 38.5 | 450.0 |
| WO-202611-003 | 精密压铸车间 | 产线-DC01-500T | P-8102-ARM | 铝合金副车架右控制臂毛坯 | 110 | 33.0 | 32.0 | 0.0 |
| WO-202611-004 | 精密压铸车间 | 产线-DC02-800T | P-8201-CASE | 电池包托盘下壳体压铸件 | 45 | 45.0 | 52.0 | 1280.0 |
| WO-202611-006 | CNC机加车间 | 产线-CNC-A组 | P-8101-ARM-M | 副车架左臂精密机加件 | 100 | 50.0 | 48.0 | 0.0 |
| WO-202611-007 | CNC机加车间 | 产线-CNC-A组 | P-8102-ARM-M | 副车架右臂精密机加件 | 95 | 47.5 | 51.0 | 320.0 |
| WO-202611-008 | CNC机加车间 | 产线-CNC-B组 | P-8305-DISC | 通风制动盘卡钳支架 | 200 | 60.0 | 58.5 | 0.0 |
| WO-202611-010 | 冲压焊接车间 | 产线-WLD-总成01 | P-8901-SUB | 前副车架总成机器人焊接 | 80 | 40.0 | 42.0 | 850.0 |
2. 最终输出:车间产线月度经营汇总看板
| 车间名称 | 生产产线 | 工单笔数 | 完工总产量(件) | 总标准工时(h) | 总实际工时(h) | 工时超耗差异(h) | 累计废品损失(元) |
|---|---|---|---|---|---|---|---|
| CNC机加车间 | 产线-CNC-A组 | 10 | 1,075 | 536.2 | 534.6 | -1.6 | 1,120.0 |
| CNC机加车间 | 产线-CNC-B组 | 10 | 2,100 | 627.0 | 650.7 | 23.7 | 2,240.0 |
| 冲压焊接车间 | 产线-WLD-总成01 | 10 | 915 | 453.8 | 448.2 | -5.6 | 2,975.0 |
| 精密压铸车间 | 产线-DC01-500T | 10 | 1,270 | 379.5 | 380.7 | 1.2 | 1,575.0 |
| 精密压铸车间 | 产线-DC02-800T | 10 | 525 | 522.5 | 548.1 | 25.6 | 4,480.0 |
🚦 第五部分:老汪实战避坑指南(三大血泪教训)
- 别用错聚合函数作用域,区分
Table函数与List函数:
– 在Table.Group的第三参数中,_是一整张子表。如果要对列求和,绝不能写Table.Sum(PQ里没有这个函数!),必须写List.Sum([列名])!因为取出来的列本身是一个列表(List); - 多列复合分组时,大括号层级别嵌套错:
– 分组键必须包在一层大括号内:{"车间名称", "生产产线"};
– 聚合定义必须是双重嵌套:{ {"列1", ...}, {"列2", ...} };如果漏掉一层大括号,M 引擎会报类型不匹配错误(We cannot convert a value of type Record to type List); - 空值(
null)对求和与平均的影响:
–List.Sum遇到null会自动忽略并正常计算,但如果整列全部都是null,它会返回null而不是0!如果需要参与加减差额运算,务必套上List.Sum(...) ?? 0或try...otherwise 0防御。
🎁 老汪结语与随堂课件包下载
掌握了 Table.Group 的深度用法,你就真正拥有了在 Power Query 内存管道中随意开辟“多维汇总维度”的顶级内功。无论是月度成本中心分摊、供应商对账核销、还是产线效率看板,你都可以在纯 M 代码中以毫秒级的极速一次性搞定!
老汪已经把本节课涉及的原始业务数据表、最终完成版带PQ查询工程文件以及学员操作指引指南全部打包上传。大家在文末点击即可免费下载实操演练!















暂无评论内容