【问题标题】:Calculate the median in MongoDB aggregation framework在 MongoDB 聚合框架中计算中位数
【发布时间】:2013-12-25 16:54:52
【问题描述】:

有没有办法使用 MongoDB 聚合框架计算中位数?

【问题讨论】:

  • AFAIK 没有 $median 这样的东西,所以你可能不得不为此使用 map-reduce。
  • 有一个开放的功能请求添加对$median 累加器的支持。请在 MongoDB 问题跟踪器中投票/观看 SERVER-4929

标签: mongodb aggregation-framework median


【解决方案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 项目。你知道我在哪里可以找到有关此的相关文档。谢谢
    猜你喜欢
    • 2018-09-25
    • 1970-01-01
    • 2016-12-21
    • 2015-11-19
    • 1970-01-01
    • 2013-07-20
    • 1970-01-01
    • 1970-01-01
    • 2014-05-11
    相关资源
    最近更新 更多