做Power BI的人基本都遇过这类时刻数据清洗已经在Power Query里做完了下一步想快速知道当前这张表里各数值字段的均值、中位数、标准差大概长什么样有没有异常高或异常低的离群值。多数人的第一反应是把数据复制回Excel算一遍或者直接去报表页拖几个DAX度量值但其实这一步完全可以在Power Query里顺路做完——M语言里有一整套描述性统计的现成函数配合一个小自定义函数就能把当前数据的描述性统计直接生成一张可刷新的结果表不用来回切工具。这篇文章我就按这个思路把Power Query做描述性统计分析的函数清单、M代码和踩坑经验完整讲一遍。适合刚接触M语言的新手照着抄也适合已经在做数据质量校验的读者看看怎么把统计摘要下沉到建模之前给后续工作省掉一堆手工活。1. 先想明白描述性统计放进Power Query做到底是图什么1.1 Power Query统计的本质在数据管道里提前固化结果很多人一听Power Query做统计分析第一反应是这不该是DAX的活吗。其实两者解决的问题完全不同。DAX做的是交互式分析计算发生在报表引擎里切片器一切、钻取一打数值立刻跟着筛选上下文变化而Power Query做统计发生在数据加载之前计算的对象是当前这一步的数据——也就是经过清洗、转换之后、即将进入数据模型的这张表。这个区别带来一个很实际的优势统计结果是跟着刷新任务自动重算的。你不需要每次数据更新之后重新复制粘贴、重新拉公式只要原始数据一变Power Query刷新跑一遍描述性统计表自动就出来了。对需要定期核查数据质量的场景来说这种可刷新比会交互值钱得多。我在实际项目里最常用的场景有三个一是数据质量检查看每个字段的空值数量、极值、均值有没有超出业务常识二是给上游建模提供参考比如决定某个字段要不要进模型、要不要做标准化处理三是生成固定的统计摘要表直接丢给业务方做汇报素材。这三类需求都对交互性没有要求但对稳定性和可追溯性有要求Power Query刚好合适。1.2 什么场景别用PQ及时转向DAX反过来说如果你需要做的是用户点一个切片器报表里的均值跟着变这种动态分析那就别在Power Query里硬做。Power Query计算的是全局静态结果它没有报表筛选上下文的概念切片器不可能触发它重新计算。这种情况下老老实实写DAX度量值比如用AVERAGE、MEDIAN这类聚合函数交给报表引擎去处理。另外如果统计结果的精度需要由当前行上下文决定比如每个产品类别里高于本类别均值的产品这也是DAX的优势场景。Power Query适合做的是整表视图和分组汇总视图这两种静态描述理解了这条边界后面选型就不会纠结。2. M语言统计函数速查计算口径、参数和注意事项2.1 常用统计函数及其口径M语言做描述性统计核心都是围绕List列表类型转的。你先把一列数据用Table.Column取出来变成一个List剩下的统计工作就是挑函数的问题。下面这张表是我日常真正用到的函数每个都标注了计算口径函数作用计算口径与注意事项List.Average算术平均值自动忽略null但会对文本报错List.Median中位数忽略null偶数个数值时取中间两数平均List.StdDev样本标准差除以n-1对应Excel的STDEV.SList.StdevP总体标准差除以n对应Excel的STDEV.PList.Variance样本方差标准差的平方口径与上一致List.VarianceP总体方差除以n口径注意区分List.Sum求和忽略nullList.Max / List.Min最大值 / 最小值空List返回nullList.Count元素个数包含null在内List.Quantile分位数第二参数填0到1之间的小数可指定四分位模式标准差这东西看似简单实际最容易闹口径纠纷。List.StdDev算的是样本标准差也就是除以n-1这是Excel里STDEV.S的算法也是大多数统计软件默认的估计方式。但如果你手上的数据本身就是全量总体比如整年全国订单都在这张表里那应该用List.StdevP除以n。选错了结果会差一截后面的误判也随之而来。List.Quantile也有个容易被忽略的点第二参数填0.25得到的是第一四分位数填0.5得到的是中位数填0.75是第三四分位数填0.9就得到第90百分位。部分版本里还支持通过options参数指定四分位模式用来对齐Excel的QUARTILE.INC和QUARTILE.EXCList.Quantile(有效列表, 0.25, [ QuartileMode QuartileMode.Inclusive ])如果你的编辑器版本不支持这个options参数直接用两个参数的基础写法也不会出错只是计算细节可能和Excel的旧函数略有差异心里有数就行。2.2 容易被忽略的Table.Profile一张表的统计量自动全集单列统计可以用上面这些List函数但如果你想把整张表的数值列一口气全部过一遍最省事的是Table.Profile。这个函数名字很多人不熟悉但它做的事情恰好就是当前数据的描述性统计分析——输入一张表输出一张新的统计表每一行对应原表的一个字段每一列对应一个统计量。let 当前数据 清洗后的表, 统计结果 Table.Profile(当前数据) in 统计结果Table.Profile输出的表里通常包含这些列不同版本字段名稍有差异Count计数、NullCount空值计数、EmptyCount空字符串计数、Min最小值、Max最大值、Median中位数、Average平均值、StdDev标准差新一点的构建版本还会带上Sum、Mode、ErrorCount等。一次性就能看到整张表的体检报告非常适合在建模前做数据全貌摸底。需要注意Table.Profile会把所有字段都纳入统计包括文本和日期字段。文本字段的Min、Max按字典序排日期字段按时间序排这些信息看个大概没问题真正要做数值专项统计时还是回到List函数逐个字段处理更精确。2.3 众数没有现成函数用Table.Group补一个描述性统计六件套里平均值、中位数、极值、方差、标准差M语言都有现成函数唯独众数是个例外。M语言标准库没有提供List.Mode之类的直接函数需要自己做分组计数再取最大值。我一般这么写let 源 List.RemoveNulls(Table.Column(当前数据, 产品类别)), 临时表 Table.FromList(源, Splitter.SplitByNothing(), {值}), 分组 Table.Group(临时表, 值, {{次数, each Table.RowCount(_), Int64.Type}}), 排序 Table.Sort(分组, {{次数, Order.Descending}}), 众数 try 排序[值]{0} otherwise null in 众数思路不复杂先把目标列转成列表再转成临时表按值分组统计次数降序排序后取第一行。用try包一层是为了防止全表空值导致取不到第一行时直接报错。多模态的情况如果有你可以把排序后的结果表整体输出而不是只取第一行。3. 从当前数据到统计结果表一套可以直接抄的M代码3.1 UI层面的快捷入口在写代码之前先提一下编辑器的快捷按钮。选中一列数值切到转换选项卡在统计信息组里能看到平均值、中位数、最小值、最大值、计数、非重复计数、求和、标准差这几个按钮。点一下当前列就会被替换成对应的统计值M代码也会自动生成。这里有两个实操细节要提醒。第一这种按钮默认是替换操作会把你选中的原始列干掉如果你只是想把统计值放到新列里需要先复制原列或者看你的Power BI版本里添加列选项卡是否也有对应的统计信息按钮——有的版本已经补上这个入口用那个就不会破坏原列。第二按钮生成的代码非常简单说白了就是调用一次List函数但换成你自己写的时候建议先手动把null清理掉再做统计这样结果更可控。3.2 单列统计模板先给一个可以直接抄的单列统计模板。这里当前数据我用的占位名称实际写的时候换成你自己查询里最后一步的名字就行let 当前数据 清洗完成后的表, 数值列 Table.Column(当前数据, 销售额), 有效列表 List.RemoveNulls(数值列), 记录总数 List.Count(数值列), 有效记录数 List.Count(有效列表), 空值数量 记录总数 - 有效记录数, 平均值 List.Average(有效列表), 中位数 List.Median(有效列表), 样本标准差 List.StdDev(有效列表), 总体标准差 List.StdevP(有效列表), 最小值 List.Min(有效列表), 最大值 List.Max(有效列表), 下四分位 List.Quantile(有效列表, 0.25), 上四分位 List.Quantile(有效列表, 0.75), 统计结果 [ 字段名 销售额, 记录数 记录总数, 有效数 有效记录数, 空值数 空值数量, 平均值 平均值, 中位数 中位数, 样本标准差 样本标准差, 总体标准差 总体标准差, 最小值 最小值, 最大值 最大值, 下四分位 下四分位, 上四分位 上四分位 ] in Table.FromRecords({统计结果})这里有几个细节值得说。先取Table.Column得到整列列表再通过List.RemoveNulls生成一个剔除空值的有效列表后面的平均值、中位数、标准差全部基于有效列表去算这样才能保证多个统计量之间的可比较性。记录总数和有效记录数放在一起顺手就得到了空值数——这四个数本身就是数据质量检查里最基础的指标。3.3 批量统计所有数值列单列模板只能处理一个字段实际工作中一张表动辄十几个数值列一个个复制粘贴太蠢了。正确做法是写一个自定义函数再用List.Transform批量跑一遍。先定义一个函数查询假设这个查询叫fn描述统计(输入表 as table, 字段 as text) as record let 原始列表 Table.Column(输入表, 字段), 有效列表 List.RemoveNulls(原始列表), 结果 [ 字段名 字段, 记录数 List.Count(原始列表), 有效数 List.Count(有效列表), 空值数 List.Count(原始列表) - List.Count(有效列表), 平均值 try List.Average(有效列表) otherwise null, 中位数 try List.Median(有效列表) otherwise null, 标准差 try List.StdDev(有效列表) otherwise null, 最小值 try List.Min(有效列表) otherwise null, 最大值 try List.Max(有效列表) otherwise null ] in 结果然后在一个新查询里调用它自动识别表里的全部数值字段let 当前数据 清洗完成后的表, 模式表 Table.Schema(当前数据), 数值字段 Table.SelectRows(模式表, each [Kind] number)[Name], 统计列表 List.Transform(数值字段, each fn描述统计(当前数据, _)), 结果表 Table.FromRecords(统计列表) in 结果表Table.Schema这个函数返回的是数据表的列结构信息其中Kind这一列记录了每列的数据类型类别number就是数值类型。用这种方式筛选字段比手工维护一个字段名单要聪明得多——源表结构一旦增加新字段刷新后自动就纳入统计了。函数里每个统计项都用try包一下是因为某些列虽然在Schema里显示是number实际却可能混入出错的值try能保证单列出错时不影响整批任务。3.4 分组统计的写法单列批量统计回答的是整张表的数值长什么样但业务问题经常是每个类别分别长什么样这时候用Table.Group按业务维度分组再在组内做描述性统计let 当前数据 清洗完成后的表, 分组结果 Table.Group(当前数据, 产品类别, { {记录数, each Table.RowCount(_), Int64.Type}, {销售额均值, each List.Average(Table.Column(_, 销售额)), type number}, {销售额中位数, each List.Median(Table.Column(_, 销售额)), type number}, {销售额标准差, each List.StdDev(Table.Column(_, 销售额)), type number}, {销售额合计, each List.Sum(Table.Column(_, 销售额)), type number}, {销售额最小值, each List.Min(Table.Column(_, 销售额)), type number}, {销售额最大值, each List.Max(Table.Column(_, 销售额)), type number} }) in 分组结果Table.Group的每一组里下划线_代表当前分组形成的小表再配合Table.Column取指定列就可以继续使用之前那套List函数。这种写法生成的表天然就是一张分组描述性统计表每一行是一个产品类别每一列是一个统计口径导入模型后直接可以用。分组统计时有个容易踩的坑分组依据列本身的类型如果发生了变化比如文本列混入了数值分组结果会莫名其妙多出几个组。解决思路是在分组之前先对分组依据列做一次Table.TransformColumnTypes把类型彻底锁死再分。4. 当前数据到底动态在哪刷新、参数与相对时间窗口4.1 刷新机制统计结果为什么能自动更新标题里的当前数据我理解有两层含义一是查询管道中当前这一步的表二是每次刷新时最新的数据。Power Query的这个特性正好把两层都覆盖到了——查询里所有步骤都是延迟执行的每次刷新都会从源头重新读取数据一路执行到统计步骤所以统计表永远基于最新数据重算这是它天然的优势。理解这个机制后你就能放心地把统计查询独立建出来让它依赖某个清洗好的基础查询。基础查询一改统计查询刷新后自动跟随源数据一更新整条链跟着变动。这里的关键是保持查询之间的引用关系清晰不要在统计查询里复制粘贴基础数据而是通过查询名称直接引用。4.2 参数化切换统计范围光有自动更新还不够很多场景需要同一个统计模板切换不同的统计范围。Power Query里的参数正好干这个。在主页选项卡里打开管理参数新建一个文本类型的参数比如叫统计维度值填华东区然后在统计查询里引用它let 当前数据 Table.SelectRows(清洗完成后的表, each [区域] 统计维度), 统计结果 Table.Profile(当前数据) in 统计结果这样当你需要切换统计区域时不用去改查询步骤只改参数值再刷新即可。参数还可以用于切换统计字段、切换分组依据、切换时间范围本质上就是把变化的部分从代码里剥离出来集中到一个可维护的入口。4.3 相对时间窗口的落地写法当前数据还有个高频场景是统计最近一段时间的数据比如最近7天、最近30天的订单统计。M语言里用DateTime.LocalNow()取当前时间再用Date.AddDays往前推窗口写法如下let 当前日期 DateTime.Date(DateTime.LocalNow()), 窗口起点 Date.AddDays(当前日期, -30), 当前数据 Table.SelectRows(明细表, each [订单日期] 窗口起点 and [订单日期] 当前日期), 统计结果 Table.Profile(当前数据) in 统计结果这里有个易错点如果订单日期列是datetime类型而窗口起点是date类型比较运算会因为类型不匹配报错或漏数据。稳妥做法是先统一格式——把日期列用Table.TransformColumnTypes改成type date再参与比较。另外DateTime.LocalNow()在每次刷新时都会取新值所以这个窗口会随时间自动滚动正好符合当前数据的语义。5. 统计结果出现异常时我实际踩过的坑和处理方法5.1 看着像数字其实列的类型是文本这是我在实际项目里遇到最多的坑。某些数据源导出的文件里数值列因为混入了一个空字符串或者千分位逗号整列被自动识别成文本表面上看每个单元格都像数字但List.Average一执行就直接报错。解决方法是统计之前先把类型强制转换当前数据 Table.TransformColumnTypes(清洗完成后的表, {{销售额, type number}})会在转换过程中把文本转不了的单元格标记成错误值所以转换后用Table.Profile看一眼ErrorCount或者统计时对List加try保护就能把这类脏数据暴露出来。5.2 空值、0值和错误值对统计结果的三重影响空值、0值、错误值这三者在描述性统计里的待遇完全不同。M语言里List.Average会自动忽略null但不会忽略0——一个没有记录和一个数值是0在业务上是两回事在均值计算里却是完全不同的两种结果。如果你不先做List.RemoveNulls空值倒是能跳过但只要混入几个真正的0值均值就会被明显拉低。我的处理习惯是先统计空值数量再决定是剔除还是补充。如果是数据采集漏了空值应该剔除如果业务口径里缺失等于0那就要人为把null替换成0再统计。这个决策必须前置否则后面所有统计量都建立在错误的假设上。错误值又是另一回事它们既不会像null一样被忽略也不会像数字一样参与计算而是直接让整个List操作报错。所以凡是从不可靠数据源取数统计前的try保护都值得写。5.3 样本标准差还是总体标准差口径导致的结论偏差标准差口径选错了结果相差可能超过10%这在业务分析里足以改变结论。举个例子说明某指标样本标准差假设是15用总体标准差算出来可能是14.2看似差别不大但当你用均值正负一个标准差来判断异常范围时边界就会偏移某些边缘值就被误划进了异常区。实操中我的判断规则很简单数据是抽样得到的就用List.StdDev数据是全量记录就用List.StdevP。如果拿不准把两个都算出来放在统计表里让下游自己选这比拍脑袋定一个口径更负责任。5.4 大数据量下别反复取列Table.Column这个操作会把整列数据实体化成列表一次两次没事但如果你在一个循环或者大表的分组逻辑里反复调用它性能会快速恶化。我碰到过一张七八百万行的表分组统计时在Table.Group的聚合函数里写了五六个Table.Column调用结果刷新时间从几十秒暴涨到十分钟以上。优化思路有两条一是如果能用Table.Profile一次性出全量概要就不要每个字段单独跑统计二是如果确实需要对多个字段分别做List函数计算先用Table.Buffer或者List.Buffer把数据缓存起来避免反复从源表取列。另外分组统计里能合并的聚合尽量在同一个Table.Group里完成一次遍历输出多个统计量而不是拆成多次Table.Group再合并。6. 选型参考Power Query、DAX和Excel的分工6.1 三者的边界对比用一张表说清楚我平时怎么在Power Query、DAX和Excel之间做选择维度Power QueryDAXExcel计算时机数据刷新时一次性完成报表交互时即时计算手动触发或刷新交互性无静态结果强跟随切片器与筛选上下文弱需手工切换适合场景数据质量检查、固定摘要、建模前探索报表可视化、动态聚合、钻取分析快速临时分析、一次性探索结果可复用性高可作为查询被其他查询引用高可在多张报表页复用度量值低容易复制到别处就断了引用维护成本中函数封装好后批量复用中度量值多了需要整理口径高容易手工改错说实话这三者并不是替代关系。我现在的工作流里Power Query负责的是进入模型之前的一切体检数据质量、描述性统计、异常值识别都在这个阶段解决DAX负责的是进入模型之后的一切动态分析均值、同比、分组对比跟随用户交互实时变化Excel则只留在数据量小、一次性探索的场景里用。6.2 我的实操习惯最后分享一点个人经验。我在拿到一张新表的第一个动作永远不是急着建关系、写度量值而是先在Power Query里跑一遍Table.Profile把字段个数、空值情况、均值、标准差、极值全部摊开看一遍。这个习惯帮我挡掉了大量后面要做返工的脏数据问题。另一个习惯是给统计查询写好命名和注释。描述性统计表通常会被其他查询引用过两个月再打开文件时如果不记得每个字段的计算口径又得从头排查。我会在查询里加一个注释步骤写明统计窗口、标准差口径、空值的处理方式这比任何文档都管用。