news 2026/10/14 5:01:56

Power Query描述性统计:M语言函数与批量模板实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Power Query描述性统计:M语言函数与批量模板实战指南

做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.S
List.StdevP总体标准差除以n,对应Excel的STDEV.P
List.Variance样本方差标准差的平方,口径与上一致
List.VarianceP总体方差除以n,口径注意区分
List.Sum求和忽略null
List.Max / List.Min最大值 / 最小值空List返回null
List.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.EXC:

List.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,把字段个数、空值情况、均值、标准差、极值全部摊开看一遍。这个习惯帮我挡掉了大量后面要做返工的脏数据问题。

另一个习惯是给统计查询写好命名和注释。描述性统计表通常会被其他查询引用,过两个月再打开文件时,如果不记得每个字段的计算口径,又得从头排查。我会在查询里加一个注释步骤,写明统计窗口、标准差口径、空值的处理方式,这比任何文档都管用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 5:01:36

Cesium自定义影像图层变形与空白问题排查实战

做 Cesium 开发这几年&#xff0c;被影像图层折腾的次数真不少。尤其是自定义影像源&#xff0c;地图上的影像要么拉伸得不像样&#xff0c;要么边缘全是空白&#xff0c;偶尔还会出现整块扭曲变形。前一阵子给某个三维可视化项目对接本地影像数据&#xff0c;这三种症状一次性…

作者头像 李华
网站建设 2026/10/14 5:01:09

系统性能指标全解:从响应时间到资源利用率的实战指南

技术圈聊架构&#xff0c;最后都会落到一个很朴素的问题上&#xff1a;系统到底扛不扛得住。而判断"扛不扛得住"&#xff0c;靠的不是感觉&#xff0c;是一系列量化的性能指标。这些年我参与过不少系统的架构评审、性能压测和线上问题排查&#xff0c;见过太多"…

作者头像 李华
网站建设 2026/10/14 5:00:37

基于SpringBoot+SSM的校园资料分享系统开发实践与核心技术解析

1. 项目整体定位与功能拆解1.1 这个系统到底在解决什么问题校园资料分享系统&#xff0c;本质上是把“学生找学习资料”这件极其低频但极其刚需的事情&#xff0c;收拢到一个统一平台里。说它低频&#xff0c;是因为大部分学生找资料的频率并不高&#xff0c;可能一学期就那么几…

作者头像 李华
网站建设 2026/10/14 5:00:14

MATLAB实现齿轮裂纹刚度计算与故障诊断

1. 项目背景与核心价值齿轮传动系统作为机械装备的核心部件&#xff0c;其啮合刚度特性直接影响着整机的振动噪声水平和疲劳寿命。传统解析法在计算含缺陷齿轮的时变啮合刚度时&#xff0c;往往难以准确反映齿根裂纹等局部损伤对刚度分布的影响。势能法通过考虑齿轮副在啮合过程…

作者头像 李华
网站建设 2026/10/14 4:59:38

大模型时代提示工程减法思维:精简提示词与技能编排实战

1. 模型能力跃迁后的提示工程思路转变1.1 从“堆料”到“精简”的认知拐点过去两年里&#xff0c;我经手过不少基于大语言模型的应用项目&#xff0c;从早期的简单问答机器人到后来的复杂多步推理系统&#xff0c;有一个感受越来越强烈&#xff1a;模型本身的能力在快速提升&am…

作者头像 李华
网站建设 2026/10/14 4:59:36

Linux运维常用脚本实战:巡检、日志清理与备份

简介&#xff1a;Shell脚本是自动化运维的基石&#xff0c;通过封装高频命令&#xff0c;能将系统巡检、日志清理和数据备份从重复劳动变成标准化操作。日常运维中&#xff0c;磁盘使用率告警、进程异常、日志膨胀等问题频发&#xff0c;手动执行free、df、pgrep不仅效率低&…

作者头像 李华