做数据清洗的人应该都有过这种经历:运营或财务发来一张表,看起来整整齐齐,一导入 Power Query 就原形毕露——表头占了两行,前几列每隔几行才有值,合并单元格导出来之后只剩下第一格有内容。这时候Excel里那套“双击填充柄”的肌肉记忆就失灵了,因为PQ里根本没有下拉手柄。但问题总要解决,所以这篇我来把 Power Query 里关于“填充”的玩法一次讲透:从基础的空值向下填充,到多级表头重塑、分组动态补全、脏数据排查,再到带累积逻辑的高级写法。内容偏实操,适合天天要跟乱七八糟表格打交道的Excel重度用户和数据分析新人。
1. 为什么要重学“填充”:Excel习惯与PowerQuery的思维差异
先说你最熟悉的场景:Excel里有一列数据,每隔几行才有一个值,中间全是空单元格。正常人的第一反应是选中第一个有值的格子,鼠标放到右下角变成黑色十字,双击,完事。这个动作在PQ里不存在,因为Power Query不是单元格级工具,它是表格级转换工具。你打开PQ编辑器时,看到的是每一列的完整预览,想给某个空单元格填值,你甚至没办法直接点进去改——改了也没意义,刷新一次数据就还原了。
所以理解PQ里的填充,要先把思维从“单元格操作”切换到“列规则转换”。填充的本质是:把一列中的空值(null)替换为该列上方向下或下方向上最近的非空值。这是一个规则,不是一次性动作。正因为它是规则,所以在数据刷新后,PQ会重新执行一遍这个替换逻辑,行数变了、位置变了,填充结果依然正确。这也就是标题里“动态”二字的真正含义——不是Excel里写死一个公式然后把单元格拖到某一行,而是让每一步操作都变成可重跑的数据流。
用一句话概括:在Excel里你手动填的是“结果”,在PQ里你定义的是“方法”。这也是为什么很多从Excel转PQ的人觉得别扭——他们还在找“下拉填充”按钮,而PQ希望你先想清楚:空值是什么?最近的非空值来自哪个方向?有没有分组边界?想清楚这三件事,填充就是顺手的事。
动态性还有第二层意思:当数据源里新加了行,甚至新加了一整组分类,只要你的PQ步骤是“按列填充空值”,新数据进来后规则会自动套用。这一点在逆透视场景里特别明显,后面我会专门拿多级表头案例演示。简单说,日常维护报表,与其每天手工补几个空单元格,不如花十分钟把填充规则写好,一次投入长期复用。
动手前再提醒一句背景知识:PQ里的“表”可以理解成一个二维数组,每一步操作都会生成一张新表,步骤之间形成链条,你随时可以点任一步骤看中间结果。填充只是链条里的一环,它解决的是“空值位置不对”的问题,解决完之后,这张表才能继续做透视、逆透视或加载回Excel。
2. 基础功:Table.FillDown与Table.FillUp的正确打开方式
先上最简单也是最常用的两个函数:Table.FillDown和Table.FillUp。含义一目了然——向下填充、向上填充。
在PQ编辑器的界面里,对应操作是:选中要处理的列,切到“转换”选项卡,找到“填充”下拉菜单,选择“向下”或“向上”。界面操作走一遍就行了,但真正要理解透,还得看M语言函数签名:
Table.FillDown(表, {列1, 列2, ...}) Table.FillUp(表, {列1, 列2, ...})第二个参数是列名的列表,如果忽略,默认只填充表中含null值的列。建议每次都显式写出要填充的列名,因为默认行为在列多的时候很容易误伤。
我拿一个最常见的“合并单元格导出表”举例。某公司发来的销售明细里,A列是区域,但每个区域只有第一行有值,剩下的都是null:
| 区域 | 城市 | 销售额 |
|---|---|---|
| 华东 | 上海 | 1200 |
| null | 杭州 | 800 |
| null | 南京 | 950 |
| 华南 | 广州 | 1100 |
| null | 深圳 | 1300 |
很直观,要的就是把“华东”往下填到所有null行,把“华南”填到后面两行。在PQ里两步搞定:
let 源 = Excel.CurrentWorkbook(){[Name="销售明细"]}[Content], 规范化类型 = Table.TransformColumnTypes(源, {{"区域", type text}, {"城市", type text}, {"销售额", Int64.Type}}), 向下填充 = Table.FillDown(规范化类型, {"区域"}) in 向下填充这段M代码里,Table.FillDown只会替换“区域”列里的null值,城市列和销售额列里的null不会被碰,除非你把它们也加进参数。这个细节很重要——很多新人给整表做填充,结果把本来应该空着的地方也填了一堆重复值。
再说一个容易被忽略的前提:填充前必须先排序。因为FillDown的逻辑是“上一行的值传递给下一行”,如果原始数据乱序,填充结果就错乱。举个极端例子,上面那张表如果先按销售额降序排列,区域列的顺序就变成华东、华南、华南、华东、华东,这时候向下填充会把华南带到华东的后面几行,完全错位。所以我的习惯是:在填充步骤前面紧跟一个“排序列”步骤,哪怕只是按索引或编号排序,也要确保填充方向和数据逻辑方向一致。
实际操作时还有个细节,你可以在PQ里右键列名,选择“替换值”,把null替换成指定内容,这是另一种“填值”思路。但注意替换值针对的是单元格里的具体内容,不是按上下位置推导的;而FillDown是真正的位置记忆填充。两者适用场景完全不同:想给某些缺失值统一写成“未知”,用替换值;想按上下关系补全分类标签,用FillDown。
顺带提一下,界面里如果在“填充”下拉框里同时看到“向下”和“向上”,说明你选中了多列且这些列里都有null值。如果你只想处理其中某一列,一定要先只选中那一列再点填充,否则所有列都会被填一遍。这个小地方我踩过,因为当时选了一个“区域”列和一个“备注”列,想只填区域,结果备注里的null也全被上一行值替代了,排查了半天。
3. 多级表头与合并单元格:逆透视前的动态填充组合拳
填充这个动作,很多时候不是为了直接交差,而是为了给后续步骤铺路。最典型的就是多级表头数据的整理。
举个例子,某门店的季度业绩表,表头结构是两行:第一行是“华东区”“华南区”,第二行才是各个门店名。第三行开始是数据,每一行第一列是指标名称(比如销售额、毛利额、客流量),后面每列是一个门店的指标值。这种表如果直接“将第一行作为标题”,只会得到两个标题:华东区、华南区,下面门店名变成数据行,整个结构就塌了。
正确的处理顺序是先填充、再提升标题、最后逆透视。完整思路是这样的:
第一,选定原始数据区域时,把两行表头都包含进去,甚至把下面数据第一行也包含进去。
第二,进入PQ后,先创建一个“索引列”,记录当前行的原始顺序,防止后面重组时乱掉。
第三,选中所有列,执行一次向下填充。这一步的意义在于:把“华东区”这个值从第一行填充到它所属的所有列上,“华南区”同理。这样第一行就变成了完整的分组标题行。
第四,再选中数据区域,“将第一行作为标题”,此时列名是“华东区”“华南区”等分组名,而第二行真正的门店名信息被挤进了数据区。
第五,这一步骤是关键:用“逆透视其他列”功能,或者更可控的做法是选中“指标”列和“分组”列,然后对剩下的列做逆透视。逆透视之后,一张多级表头的数据就变成了一维表:分组列、指标列、门店列、值列。
这里你就能体会到动态的好处:如果下个月门店增加了两列,只要源数据区域能自动扩展(比如用Excel表格对象而不是普通区域),PQ刷新后,这新增的两列会自动进入逆透视结果,完全不需要更改步骤。我在给某公司做月度经营报表时,就靠这个结构解决了“每月加门店”的重复劳动。
再补充一个变体场景:不是多级表头,而是从某些系统导出的明细表,合并单元格导出后只有第一行有值,中间偶尔又出现了一个值。这就是“分组内部有不连续的非空值”。这时候不能简单FillDown到底,因为中间出现的那个值,业务上可能是“改换了负责人”或“切换了版本”,你得先确认要不要把它也往下带。如果确认需要,那还是按之前的方式FillDown即可;如果不需要,就得先给这个值打标记,甚至单独生成一列来处理。这属于数据语义判断,不能纯靠函数解决。
多级表头的处理还牵涉一个常见问题:逆透视后“值”列类型不稳定。比如销售额是数字,但客流量是文本描述,合并下来之后,PQ会自动把整列转成文本,导致数据无法求和。解决方法是先进PQ里对每个指标列做类型清洗,或者干脆在逆透视后用“提取值”+“转换数据类型”双步骤处理。要是看到值列左侧有“123”图标,就说明类型被强制成文本了,需要手动改回小数。
4. 带组别的动态填充:让每个小组独立补全
进阶一步:数据里的空值不是全域连续,而是“每个分组内部各自有一段空”。比如一张人员明细表,每组的“负责人”字段只在组内第一行有值,其他行全是null。如果你全局直接FillDown,组与组之间的边界就被冲破了——上一组的负责人会串到下一组的空白行上。
这种问题的本质是:填充逻辑要识别“组边界”,只在组内向下传递。而Power Query里没有一个叫“分组填充”的按钮,得用Table.Group配合每个子表单独FillDown。
我先给一段精简的M代码示例:
let 源 = Excel.CurrentWorkbook(){[Name="分组明细"]}[Content], 规范类型 = Table.TransformColumnTypes(源, {{"组别", type text}, {"负责人", type text}, {"金额", Int64.Type}}), 分组填充 = Table.Group(规范类型, {"组别"}, {"临时表", each Table.FillDown(_, {"负责人"})}), 展开临时表 = Table.ExpandTableColumn(分组填充, "临时表", {"行号", "负责人", "金额"}, {"行号", "负责人", "金额"}) in 展开临时表代码的关键在each Table.FillDown(_, {"负责人"})。这里的_是每个组切出来的子表,你在这个子表上做FillDown,就不会跨组污染。Table.Group按“组别”分组后,每组内部各行保留着原始顺序和所有原始字段。最后展开临时表时,记得把需要的原始字段全部列出来,否则结果里只会剩分组字段和临时表里指定的字段。
这中间有个容易翻车的点:Table.Group里每一组生成的临时表,如果你还在子表上做了排序、筛选,展开时可能导致行顺序和原始顺序不一致,最终结果混乱。所以我在对源表做任何操作之前,都会先加索引列,分组填充完之后再按索引列排序回来。顺序是:加索引 → 分组填充 → 展开临时表 → 按索引排回原序 → 删索引列。这套流程稳定,不会丢顺序。
如果你不想用Table.Group,还有一个“取巧”的写法:如果每个组的第一行都非空,而且组和组之间的值不会重复,你可以先SortCase按“组别+行号”排序,再全局FillDown,最后把整表按索引排回来。这是因为全局FillDown只认“上一行的值”,只要你排好序,让每一组都以非空负责人开头,填充结果就等于分组填充。不过这种方案经不起细节推敲——如果组内首行恰好是null,或者组间负责人名字一样,就会出错。所以我建议直接用Table.Group方案,代码多几行,胜在逻辑清晰。
分组场景里另一个高频需求是“生成组内序号”:比如每个用户有多笔订单,你想标记这是该用户第几笔订单。这个也属于广义的“动态填充”——不是补空值,而是根据组内位置计算序号。最简单的做法是:先全局加索引列,然后按用户分组,在组内加一个从0开始的组内索引列,最后展开。等于是“给数据补上组内位置信息”。我实际经验是,很多复杂计算都依赖组内编号,比如“每个组累计到当前行的金额”就必须先在组内排序,再做累计。
5. 真实脏数据下的排查经验:null、空文本与填充失效
前四章讲的都是理想场景:空值就是null,填充就生效。但实际项目里,最大的坑恰恰是“看起来空,但不是null”。
Excel里输入空字符串的方式不少:公式返回"",系统导出带空格的假空格,或者干脆是"NULL"、"-"这种占位符。而Table.FillDown有个死脾气——它只认null,不认空字符串。你看到单元格是空白,但它实际上是长度为零的文本,填充函数动都不动。这是很多人“填充没生效”的第一大原因。
排查方法其实很简单:选中这一列,看PQ底部的状态栏,如果显示“空”的个数为0,但预览里肉眼看着有空白格,那这些空格就极有可能是空字符串。再一个方法:右键列,选择“替换值”,在“要查找的值”里什么都别输入,点击“确定”,如果系统提示“找到N个匹配项”,那就实锤了——那一列里真的有N个空字符串。
解决办法是把空字符串统一转成null,然后再填充。界面上:“转换”选项卡 → “替换值”,要查找的内容留空,替换为null。M语言写法:
let 源 = ..., 空转空值 = Table.ReplaceValue(源, "", null, Replacer.ReplaceValue, {"区域", "城市"}) in 空转空值如果表里有多个“假空”值,比如既有""又有"-"还有"NULL",那第二步是连续做几次替换,把所有这些统一成null,再进FillDown。这一步我称之为“数据净化”,是填充分的前置流程。
第二类常见问题是“排序被填充步骤接歪了”。前面说过,FillDown依赖上一行,如果数据源在PQ外部刷新后顺序改变,填充结果就跟着变。举个例子:你按“日期”排序后做了FillDown,每周五外部数据源更新,新增了一行,这行恰好在排序时被排到中间,填充效果就会和你预期不完全一致。这时候需要检查顺序稳定性。最稳妥的做法是在源表里就维护好一个唯一递增的ID列,或者进入PQ后立刻添加索引列,然后所有排序、填充都围绕着索引列做可控操作。别依赖Excel表格默认的行顺序,那玩意在刷新时不会给你任何承诺。
第三类问题是“合并查询产生的null导致填充失效”。比如你拿主表去匹配维表,维表里没有对应的键值,合并查询就会在新增列里生成null。你把这些null往下填充,看起来是“延续上一个匹配结果”,但这只是权宜之计。我在某次做客户经理名匹配时,用了FillDown去补“客户经理”列,结果两个客户经理同名,导致边界混淆,一批订单被归错人。后来我改成按“日期”排序+按“客户分组建索引”的方式做,才把边界锁死。
第四类问题是“PQ的惰性求值导致填充结果不稳定”。这个比较高级,但遇到一次你就记住了:当你在某个步骤里调用了比较“重”的操作,比如合并查询、分组、排序后再FillDown,有时预览表看着正确,一刷新却有几个null没填上。原因大概率是PQ为了性能采用了惰性求值,排序结果在内部不稳定,填充取到的“上一行”和你预期不同。一个有效但需要谨慎使用的救援手段是Table.Buffer:在排序之后、填充之前,加一个缓冲步骤,把当前表快照到内存里,然后再填充。代码很简单:
let 源 = ..., 排序 = Table.Sort(源, {{"日期", Order.Ascending}}), 缓冲 = Table.Buffer(排序), 填充 = Table.FillDown(缓冲, {"负责人"}) in 填充Table.Buffer的核心作用是固定行的物理顺序,防止后续步骤因为逻辑优化而改变行的相对位置。但它不是万能的:大表用Buffer会占用内存,如果表有几百万行,反而拖慢速度。我自己的经验是,小表能用就用,大表慎用,优先检查排序和分组逻辑是不是真的稳定。
6. 动态填充的进阶玩法:累计标记与结转值
聊点更泛化的“动态填充”:有时候你要补的不是别人给的空单元格,而是需要自己生成一列“跟随行数变化”的值。比如每笔订单流水的结余、库存的结转、标记“截止目前是否首次出现”。这些计算在Excel里通常用公式下拉实现,在PQ里同样有对应的做法,核心是理解List.Generate和List.Accumulate。
先看最常用的“组内累计”。假设你有一列“是否首次购买”,每行值为“是”或“否”,你想生成一列“该客户第几次购买”。如果用Excel,用COUNTIFS往下拖;在PQ里,思路是生成一个累计列表,再把列表按行索引挂回原表。
let 源 = Excel.CurrentWorkbook(){[Name="流水"]}[Content], 排序 = Table.Sort(源, {{"客户", Order.Ascending}, {"日期", Order.Ascending}}), 加索引 = Table.AddIndexColumn(排序, "idx", 0, 1, Int64.Type), 是否首次列 = Table.Column(加索引, "是否首次"), 累计列表 = List.Generate( () => [s = 0, i = 0], each [i] < List.Count(是否首次列), each [s = [s] + (if 是否首次列{[i]} = "是" then 1 else 0), i = [i] + 1], each [s] ), 添加累计列 = Table.AddColumn(加索引, "购买次数", each 累计列表{[idx]}, Int64.Type) in 添加累计列这段代码不短,但你不用背它,理解结构即可:List.Generate维护一个状态s,逐行扫描“是否首次列”,遇到“是”就加1,遇到“否”就保持不变,最后生成一个和表行数等长的累计列表。Table.AddColumn里each 累计列表{[idx]}的意思就是按当前行的索引,从累计列表里取出对应的累计值。跑出来的结果就和Excel里下拉公式一模一样。
再进一步,List.Accumulate适合做“余额结转”或“库存结转”。假设流水表里有一列“变动数量”,你要生成一列“累计结余”。设初始结余为0,每一行等于上一行结余加本行变动:
let 流水 = Excel.CurrentWorkbook(){[Name="库存流水"]}[Content], 加索引 = Table.AddIndexColumn(流水, "idx", 0, 1, Int64.Type), 变动列 = Table.Column(加索引, "变动数量"), 累计结余 = List.Accumulate( 变动列, {0}, (state, cur) => state & {List.Last(state) + cur} ), 添加结余列 = Table.AddColumn(加索引, "结余", each 累计结余{[idx]}, Int64.Type) in 添加结余列这里的初始值{0}是LIST方式,因为我们要保存每一步的累计结果。如果期初库存不为0,就把0替换成{期初值}。List.Last(state)每次取当前最后一项,然后加当前行的变动量。这个写法和Excel里=上一行结余+本行变动完全等价,但好处是——数据源增加几千行,刷新后整列自动重算,不用再检查公式有没有拖到位。
最后说一句关于“条件填充”的思考。有时候你要填的值不是简单的上下复制,而是“按某一行的状态来决定其他列的下一个值”。比如订单表里每一行只有“发货日期”或“完成日期”,你想生成“阶段标记”:如果整合完之后某行属于未发货,就取上一行状态。这种逻辑也可以拆成:先做同列的FillDown,再做跨列的TransformRows,或者用自定义列写if [列A] = null then [列B] else [列A]。本质上,PQ的填充不局限于内置函数,你完全可以组合索引列、自定义列和List函数,把任何Excel公式能干的活搬过来。
我个人现在的习惯是:不管多简单的填充需求,都在PQ里建一条完整步骤,而不是在原表里手工补几个值。原因是刷新是PQ的命根子,手工补的值刷新一次就没了,而你定义成步骤之后,将来数据源任何变化,只需点“刷新预览”看是否正常。实操里我一般会把源表复制一份作为备份查询,所有清洗都在副本上做,万一改坏了,还能回到源查询重新来。这种习惯看着保守,但在折腾复杂动态填充时,省下的时间不是一点半点。