在 MongoDB 聚合框架中计算中位数
Posted
技术标签:
【中文标题】在 MongoDB 聚合框架中计算中位数【英文标题】:Calculate the median in MongoDB aggregation framework 【发布时间】:2013-12-25 16:54:52 【问题描述】:有没有办法使用 MongoDB 聚合框架计算中位数?
【问题讨论】:
AFAIK 没有$median
这样的东西,所以你可能不得不为此使用 map-reduce。
有一个开放的功能请求添加对$median
累加器的支持。请在 MongoDB 问题跟踪器中投票/观看 SERVER-4929。
【参考方案1】:
聚合框架不支持开箱即用的中值。所以你必须自己写一些东西。
我建议您在应用程序级别执行此操作。使用普通的 find() 检索所有文档,对结果集进行排序(在数据库中使用光标的 .sort()
函数或在应用程序中对它们进行排序 - 您的决定),然后获取元素 size / 2
.
当您真的想在数据库级别执行此操作时,您可以使用 map-reduce 执行此操作。 map 函数将发出键和具有单个值的数组 - 您想要获得中位数的值。 reduce-function 只会连接它接收到的结果数组,因此每个键都以一个包含所有值的数组结束。然后 finalize 函数将计算该数组的中位数,再次通过对数组进行排序,然后获取元素编号 size / 2
。
【讨论】:
【参考方案2】:在一般情况下计算中位数有些棘手,因为它涉及对整个数据集进行排序,或者使用深度也与数据集大小成正比的递归。这可能是许多数据库没有开箱即用的中位数运算符的原因(mysql 也没有)。
计算中位数的最简单方法是使用这两个语句(假设我们要计算中位数的属性称为a
,并且我们希望它适用于集合中的所有文档,coll
):
count = db.coll.count();
db.coll.find().sort( "a":1 ).skip(count / 2 - 1).limit(1);
这相当于人家suggest for MySQL。
【讨论】:
我知道不能仅仅为了表示感谢而发表评论......但这很漂亮:) 警告:这是代码轻,但服务器重【参考方案3】:使用聚合框架可以一次性完成。
排序 => 放入数组排序值 => 获取数组大小 => 将大小除以 2 => 获取除法的 Int 值(中位数的左侧) => 将 1 加到左侧(右侧) =>获取左侧和右侧的数组元素 => 两个元素的平均值
这是一个带有 Spring java mongoTemplate 的示例:
该模型是作者(“所有者”)登录的图书列表,目标是获取用户图书的中位数:
GroupOperation countByBookOwner = group("owner").count().as("nbBooks");
SortOperation sortByCount = sort(Direction.ASC, "nbBooks");
GroupOperation putInArray = group().push("nbBooks").as("nbBooksArray");
ProjectionOperation getSizeOfArray = project("nbBooksArray").and("nbBooksArray").size().as("size");
ProjectionOperation divideSizeByTwo = project("nbBooksArray").and("size").divide(2).as("middleFloat");
ProjectionOperation getIntValueOfDivisionForBornLeft = project("middleFloat", "nbBooksArray").and("middleFloat")
.project("trunc").as("beginMiddle");
ProjectionOperation add1ToBornLeftToGetBornRight = project("beginMiddle", "middleFloat", "nbBooksArray")
.and("beginMiddle").project("add", 1).as("endMiddle");
ProjectionOperation arrayElementAt = project("beginMiddle", "endMiddle", "middleFloat", "nbBooksArray")
.and("nbBooksArray").project("arrayElemAt", "$beginMiddle").as("beginValue").and("nbBooksArray")
.project("arrayElemAt", "$endMiddle").as("endValue");
ProjectionOperation averageForMedian = project("beginMiddle", "endMiddle", "middleFloat", "nbBooksArray",
"beginValue", "endValue").and("beginValue").project("avg", "$endValue").as("median");
Aggregation aggregation = newAggregation(countByBookOwner, sortByCount, putInArray, getSizeOfArray,
divideSizeByTwo, getIntValueOfDivisionForBornLeft, add1ToBornLeftToGetBornRight, arrayElementAt,
averageForMedian);
long time = System.currentTimeMillis();
AggregationResults<MedianContainer> groupResults = mongoTemplate.aggregate(aggregation, "book",
MedianContainer.class);
这里是聚合结果:
"aggregate": "book" ,
"pipeline": [
"$group":
"_id": "$owner" ,
"nbBooks":
"$sum": 1
,
"$sort":
"nbBooks": 1
,
"$group":
"_id": null ,
"nbBooksArray":
"$push": "$nbBooks"
,
"$project":
"nbBooksArray": 1 ,
"size":
"$size": ["$nbBooksArray"]
,
"$project":
"nbBooksArray": 1 ,
"middleFloat":
"$divide": ["$size" , 2]
,
"$project":
"middleFloat": 1 ,
"nbBooksArray": 1 ,
"beginMiddle":
"$trunc": ["$middleFloat"]
,
"$project":
"beginMiddle": 1 ,
"middleFloat": 1 ,
"nbBooksArray": 1 ,
"endMiddle":
"$add": ["$beginMiddle" , 1]
,
"$project":
"beginMiddle": 1 ,
"endMiddle": 1 ,
"middleFloat": 1 ,
"nbBooksArray": 1 ,
"beginValue":
"$arrayElemAt": ["$nbBooksArray" , "$beginMiddle"]
,
"endValue":
"$arrayElemAt": ["$nbBooksArray" , "$endMiddle"]
,
"$project":
"beginMiddle": 1 ,
"endMiddle": 1 ,
"middleFloat": 1 ,
"nbBooksArray": 1 ,
"beginValue": 1 ,
"endValue": 1 ,
"median":
"$avg": ["$beginValue" , "$endValue"]
]
【讨论】:
我不知道这是否有效,只是为奉献点赞【参考方案4】:虽然maxiplay's answer 不准确,但它确实将我引向了正确的方向。给定解决方案的问题在于它仅在记录数为偶数时才有效。因为对于奇数条记录,只需取中点的值,无需计算平均值。
这就是我让它工作的方式。
db.collection.aggregate([
"$match": "processingStatus": "Completed" ,
"$sort": "value": 1 ,
"$group":
"_id": "$userId",
"valueArray":
"$push": "$value"
,
"$project":
"_id": 0,
"userId": "$_id",
"valueArray": 1,
"size": "$size": ["$valueArray"]
,
"$project":
"userId": 1,
"valueArray": 1,
"isEvenLength": "$eq": [ "$mod": ["$size", 2] , 0 ] ,
"middlePoint": "$trunc": "$divide": ["$size", 2]
,
"$project":
"userId": 1,
"valueArray": 1,
"isEvenLength": 1,
"middlePoint": 1,
"beginMiddle": "$subtract": [ "$middlePoint", 1] ,
"endMiddle": "$middlePoint"
,
"$project":
"userId": 1,
"valueArray": 1,
"middlePoint": 1,
"beginMiddle": 1,
"beginValue": "$arrayElemAt": ["$stepsArray", "$beginMiddle"] ,
"endValue": "$arrayElemAt": ["$stepsArray", "$endMiddle"] ,
"isEvenLength": 1
,
"$project":
"userId": 1,
"valueArray": 1,
"middlePoint": 1,
"beginMiddle": 1,
"beginValue": 1,
"endValue": 1,
"middleSum": "$add": ["$beginValue", "$endValue"] ,
"isEvenLength": 1
,
"$project":
"userId": 1,
"valueArray": 1,
"median":
"$cond":
if: "$isEvenLength",
then: "$divide": ["$middleSum", 2] ,
else: "$arrayElemAt": ["$stepsArray", "$middlePoint"]
])
【讨论】:
管道中没有stepsArray,其结果始终为null 管道存在问题,其中 $sort 在 $group 之后,它应该在之前。如果您仍然得到空结果,请告诉我。【参考方案5】:从Mongo 4.4
开始,$group
阶段有一个新的聚合运算符 $accumulator
,允许通过 javascript 用户定义的函数在文档分组时自定义累积文档。
因此,为了找到中位数:
// "a" : 25, "b" : 12
// "a" : 89, "b" : 7
// "a" : 25, "b" : 17
// "a" : 25, "b" : 24
// "a" : 89, "b" : 15
db.collection.aggregate([
$group:
_id: "$a",
median:
$accumulator:
accumulateArgs: ["$b"],
init: function() return []; ,
accumulate: function(bs, b) return bs.concat(b); ,
merge: function(bs1, bs2) return bs1.concat(bs2); ,
finalize: function(bs)
bs.sort(function(a, b) return a - b );
var mid = bs.length / 2;
return mid % 1 ? bs[mid - 0.5] : (bs[mid - 1] + bs[mid]) / 2;
,
lang: "js"
])
// "_id" : 25, "median" : 17
// "_id" : 89, "median" : 11
累加器:
在场上积累b
(accumulateArgs
)
被初始化为一个空数组 (init
)
在数组中累积b
项(accumulate
和merge
)
最后对b
项目(finalize
)进行中位数计算
【讨论】:
你好,这是一个很好的解决问题的方法。但我正在开发一个需要相同功能的 Java 项目。你知道我在哪里可以找到有关此的相关文档。谢谢以上是关于在 MongoDB 聚合框架中计算中位数的主要内容,如果未能解决你的问题,请参考以下文章