在 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 项(accumulatemerge) 最后对b项目(finalize)进行中位数计算

【讨论】:

你好,这是一个很好的解决问题的方法。但我正在开发一个需要相同功能的 Java 项目。你知道我在哪里可以找到有关此的相关文档。谢谢

以上是关于在 MongoDB 聚合框架中计算中位数的主要内容,如果未能解决你的问题,请参考以下文章

在 SQL Server 中计算中位数的函数

如何计算 PrestoSQL 中的中位数?

Terra R - 使用自定义函数加速栅格数据的聚合()

分位数计算

MySQL中位数计算方法

如何计算百分位数?