在做数据分析类的项目时,经常需要回答这样一个问题:两个数值字段之间到底有没有线性关系?比如用户的登录次数和消费金额、商品的浏览量和成交量。传统做法是把数据导出到分析工具里算皮尔逊相关系数,但如果数据本身就存在MongoDB里,来回搬运既浪费带宽又增加代码复杂度。本文就来聊聊如何在聚合管道中直接计算相关系数,以及为什么你找不到一个叫$correlation的现成操作符。

先说清楚:MongoDB并没有$correlation这个操作符
很多文章或者AI生成的内容会提到$correlation,但你翻遍官方文档就会发现,聚合管道的累加器操作符里只有$sum、$avg、$stdDevPop、$stdDevSamp、$min、$max这些,并没有直接计算相关系数的操作符。这一点必须先澄清,否则按照不存在的语法写出来的管道会直接报错,提示无法识别的表达式。
不过没有现成操作符并不代表没办法。皮尔逊相关系数的公式本身只涉及求和、求乘积、平方和以及标准差这几个基本运算,而这些MongoDB聚合表达式全部支持。换句话说,我们可以把数学公式手动翻译成聚合管道语句,让数据库在服务端完成整个计算过程,这就是本文的核心思路。
另外要提一句,如果你使用的是Atlas的SQL接口或者外接分析引擎,个别平台会提供CORR这类聚合函数,但那已经不属于原生MongoDB聚合管道的范畴了,迁移成本和兼容性都要单独考虑。对于绝大多数场景,自己拼一条管道是更可控的方案。
皮尔逊相关系数的公式拆解
皮尔逊相关系数的计算公式为:r等于x与y的协方差,除以x的标准差与y的标准差的乘积。展开写成累加形式就是:分子是n乘以xy的累加和,减去x累加和与y累加和的乘积;分母是两个平方项的乘积再开方,每个平方项分别是n乘以对应变量的平方累加和减去累加和的平方。
对照这个公式,我们需要在$group阶段同时累计五个量:文档数n、x的累加和、y的累加和、xy乘积的累加和、x平方和y平方各自的累加和。这五个量都能用$sum配合表达式拿到,之后在$project阶段做除法和开方即可。
标准差部分可以偷个懒,用$stdDevPop直接算总体标准差,这样公式可以简化为协方差除以两个标准差的乘积,代码会清爽不少。下面给出一份可以直接跑的完整示例。
完整的聚合管道实现
假设有一个订单集合orders,每条文档包含views(浏览次数)和amount(成交金额)两个数值字段,我们想看这两个字段的线性相关程度。先插入一批测试数据:
db.orders.insertMany([
{ views: 12, amount: 350 },
{ views: 25, amount: 780 },
{ views: 33, amount: 1100 },
{ views: 45, amount: 1500 },
{ views: 52, amount: 1750 },
{ views: 68, amount: 2100 },
{ views: 75, amount: 2450 },
{ views: 90, amount: 2800 },
{ views: 105, amount: 3300 },
{ views: 120, amount: 3800 }
])然后用下面这条管道计算相关系数,写法上用简化公式,协方差除以两个总体标准差的乘积:
db.orders.aggregate([
{
// 第一阶段:分组累计所有中间量
$group: {
_id: null,
n: { $sum: 1 },
sumX: { $sum: "$views" },
sumY: { $sum: "$amount" },
sumXY: { $sum: { $multiply: ["$views", "$amount"] } },
sumX2: { $sum: { $multiply: ["$views", "$views"] } },
sumY2: { $sum: { $multiply: ["$amount", "$amount"] } },
stdX: { $stdDevPop: "$views" },
stdY: { $stdDevPop: "$amount" }
}
},
{
// 第二阶段:套用皮尔逊公式
$project: {
_id: 0,
n: 1,
// 协方差 = E(xy) - E(x) * E(y)
covariance: {
$subtract: [
{ $divide: ["$sumXY", "$n"] },
{ $multiply: [
{ $divide: ["$sumX", "$n"] },
{ $divide: ["$sumY", "$n"] }
]}
]
},
stdX: 1,
stdY: 1
}
},
{
// 第三阶段:相关系数 r = 协方差 / (标准差x * 标准差y)
$project: {
n: 1,
correlation: {
$divide: [
"$covariance",
{ $multiply: ["$stdX", "$stdY"] }
]
}
}
}
])上面这组数据浏览量和金额基本是线性递增的,跑出来的r值会非常接近1,说明强正相关。如果换成一组随机波动的数据,r值会向0靠拢。需要注意,当某个字段是常数(标准差为0)时,分母会变成0,聚合会返回null,业务代码里要提前处理这种边界情况,或者在管道里用$cond做保护。
按维度分组计算相关系数
实际业务中更常见的需求是分组计算,比如按商品类目分别看浏览量与成交金额的相关性。这只需要把$group阶段的_id从null换成类目字段即可,管道其余部分完全不用动:
db.orders.aggregate([
{
$group: {
_id: "$category",
n: { $sum: 1 },
sumX: { $sum: "$views" },
sumY: { $sum: "$amount" },
sumXY: { $sum: { $multiply: ["$views", "$amount"] } },
stdX: { $stdDevPop: "$views" },
stdY: { $stdDevPop: "$amount" }
}
},
{
$project: {
_id: 0,
category: "$_id",
sampleCount: "$n",
correlation: {
$divide: [
{ $subtract: [
{ $divide: ["$sumXY", "$n"] },
{ $multiply: [
{ $divide: [{ $divide: ["$sumX", "$n"] }, 1] },
{ $divide: ["$sumY", "$n"] }
]}
]},
{ $multiply: ["$stdX", "$stdY"] }
]
}
}
}
])分组统计时特别要留意样本量问题。皮尔逊相关系数对样本量很敏感,如果某个类目只有两三条数据,算出来的r值没有统计意义,甚至可能出现绝对值虚高的情况。建议在结果中保留sampleCount字段,由应用层过滤掉样本量过小的分组,避免得出误导性的结论。
性能与精度方面的考量
从性能角度看,这种聚合方式只需要对集合(或过滤后的子集)做一次遍历,所有中间量都在$group阶段一次性累计完成,服务端不会产生额外的排序开销。如果数据量大,配合合理的过滤条件($match尽量放在管道最前面并命中索引),整体执行效率是不错的,比把几十万条文档拉到应用端再计算要快得多。
精度方面有一个小坑值得注意。$stdDevPop内部实现做了数值稳定性处理,但手动的乘积累加在数值很大时可能出现精度损失。如果字段值普遍很大(比如纳秒级时间戳),建议先用$subtract减去一个基准值再做计算,等价于对数据做了平移,而相关系数对平移是不变的,结果不受影响。
最后总结一下:MongoDB虽然没有名为$correlation的聚合操作符,但皮尔逊相关系数完全可以借助$group、$sum、$multiply、$stdDevPop等基础运算符在数据库端直接算出来。掌握这个思路之后,类似的相关性统计需求都可以不依赖外部工具,一条聚合管道解决,既简化了架构,也让计算贴近数据本身。
MongoDB聚合管道相关系数皮尔逊相关修改时间:2026-09-11 07:08:37