如果您有50年的温度天气数据(例如),您将如何计算该时间段的3个月间隔的移动平均线?你能用一个查询做到这一点,还是必须有多个查询?
Example Data
01/01/2014 = 40 degrees
12/31/2013 = 38 degrees
12/30/2013 = 29 degrees
12/29/2013 = 31 degrees
12/28/2013 = 34 degrees
12/27/2013 = 36 degrees
12/26/2013 = 38 degrees
.....
答案 0 :(得分:5)
我倾向于在MongoDB中执行此操作的方法是在文档中为每天的值保留过去90天的运行总和,例如
{"day": 1, "tempMax": 40, "tempMaxSum90": 2232}
{"day": 2, "tempMax": 38, "tempMaxSum90": 2230}
{"day": 3, "tempMax": 36, "tempMaxSum90": 2231}
{"day": 4, "tempMax": 37, "tempMaxSum90": 2233}
每当需要将新数据点添加到集合中时,您可以使用两个简单查询,一个加法和一个减法(如此伪装代码)来有效地计算下一个和,而不是读取和求和90个值:
tempMaxSum90(day) = tempMaxSum90(day-1) + tempMax(day) - tempMax(day-90)
每天的90天移动平均值仅为90天的总和除以90.
如果你想在不同的时间尺度上提供移动平均线(例如1周,30天,90天,1年),你可以简单地用每个文件维持一系列总和而不是一个总和,一个总和对于每个所需的时间尺度。
这种方法需要额外的存储空间和额外的处理来插入新数据,但是在大多数时间序列图表方案中都是合适的,因为新数据的收集速度相对较慢,需要快速检索。
答案 1 :(得分:4)
agg框架现在具有内置的$map
和$reduce
和$range
,因此数组处理更为直接。下面是一个示例,它针对您希望通过某些谓词过滤的一组数据计算移动平均值。基本设置是每个文档都包含可过滤的条件和一个值,例如
{sym: "A", d: ISODate("2018-01-01"), val: 10}
{sym: "A", d: ISODate("2018-01-02"), val: 30}
这里是:
// This controls the number of observations in the moving average:
days = 4;
c=db.foo.aggregate([
// Filter down to what you want. This can be anything or nothing at all.
{$match: {"sym": "S1"}}
// Ensure dates are going earliest to latest:
,{$sort: {d:1}}
// Turn docs into a single doc with a big vector of observations, e.g.
// {sym: "A", d: d1, val: 10}
// {sym: "A", d: d2, val: 11}
// {sym: "A", d: d3, val: 13}
// becomes
// {_id: "A", prx: [ {v:10,d:d1}, {v:11,d:d2}, {v:13,d:d3} ] }
//
// This will set us up to take advantage of array processing functions!
,{$group: {_id: "$sym", prx: {$push: {v:"$val",d:"$date"}} }}
// Nice additional info. Note use of dot notation on array to get
// just scalar date at elem 0, not the object {v:val,d:date}:
,{$addFields: {numDays: days, startDate: {$arrayElemAt: [ "$prx.d", 0 ]}} }
// The Juice! Assume we have a variable "days" which is the desired number
// of days of moving average.
// The complex expression below does this in python pseudocode:
//
// for z in range(0, size of value vector - # of days in moving avg):
// seg = vector[n:n+days]
// values = seg.v
// dates = seg.d
// for v in seg:
// tot += v
// avg = tot/len(seg)
//
// Note that it is possible to overrun the segment at the end of the "walk"
// along the vector, i.e. not enough date-values. So we only run the
// vector to (len(vector) - (days-1).
// Also, for extra info, we also add the number of days *actually* used in the
// calculation AND the as-of date which is the tail date of the segment!
//
// Again we take advantage of dot notation to turn the vector of
// object {v:val, d:date} into two vectors of simple scalars [v1,v2,...]
// and [d1,d2,...] with $prx.v and $prx.d
//
,{$addFields: {"prx": {$map: {
input: {$range:[0,{$subtract:[{$size:"$prx"}, (days-1)]}]} ,
as: "z",
in: {
avg: {$avg: {$slice: [ "$prx.v", "$$z", days ] } },
d: {$arrayElemAt: [ "$prx.d", {$add: ["$$z", (days-1)] } ]}
}
}}
}}
]);
这可能会产生以下输出:
{
"_id" : "S1",
"prx" : [
{
"avg" : 11.738793632512115,
"d" : ISODate("2018-09-05T16:10:30.259Z")
},
{
"avg" : 12.420766702631376,
"d" : ISODate("2018-09-06T16:10:30.259Z")
},
...
],
"numDays" : 4,
"startDate" : ISODate("2018-09-02T16:10:30.259Z")
}
答案 2 :(得分:0)
我不相信聚合框架可以在当前版本(2.6)中的多个日期执行此操作,或者至少在没有一些严肃的体操的情况下不能这样做。原因是聚合管道一次处理一个文档而只处理一个文档,因此有必要以某种方式为包含前3个月相关信息的每一天创建一个文档。这可以作为计算平均值的$group
阶段,这意味着前一阶段将产生每天约90个拷贝的记录,其中一些区别密钥可用于$group
。
因此,我没有看到在单个聚合中一次为多个日期执行此操作的方法。如果有人找到了解决方法的话,我会很高兴出错并且必须编辑/删除这个答案,即使它太复杂也不实用。 PostgreSQL PARTITION类型函数可以在这里完成工作;也许有一天会添加这个功能。
答案 3 :(得分:0)
我想我可能会对自己的问题有一个答案。 Map Reduce会做到这一点。首先使用emit将每个文档映射到它应该被平均的邻居,然后使用reduce来平均每个数组......并且新的平均值数组应该是移动平均值,因为它是超时的。 id将是您关心的新日期间隔
我想我需要更好地理解map-reduce ...
:)
例如......如果我们想在内存中进行(稍后我们可以创建集合)
GIST https://gist.github.com/mrgcohen/3f67c597a397132c46f7
看起来不错吗?
答案 4 :(得分:0)
可接受的答案对我有所帮助,但是我花了一段时间了解它的工作原理,因此我想我会解释自己的方法来帮助他人。特别是在您的情况下,我认为我的回答会有所帮助
这理想地适用于较小的数据集
首先将数据按天分组,然后将数组中的所有天都附加到每天:
{
"$sort": {
"Date": -1
}
},
{
"$group": {
"_id": {
"Day": "$Date",
"Temperature": "$Temperature"
},
"Previous Values": {
"$push": {
"Date": "$Date",
"Temperature": "$Temperature"
}
}
}
这会给您留下一条看起来像这样的记录(它将被正确订购):
{"_id.Day": "2017-02-01",
"Temperature": 40,
"Previous Values": [
{"Day": "2017-03-01", "Temperature": 20},
{"Day": "2017-02-11", "Temperature": 22},
{"Day": "2017-01-18", "Temperature": 03},
...
]},
现在,每一天都附加了所有日期,我们需要从“前值”数组中删除比此_id.Day字段更新的项目,因为移动平均线是向后看的:
{
"$project": {
"_id": 0,
"Date": "$_id.Date",
"Temperature": "$_id.Temperature",
"Previous Values": 1
}
},
{
"$project": {
"_id": 0,
"Date": 1,
"Temperature": 1,
"Previous Values": {
"$filter": {
"input": "$Previous Values",
"as": "pv",
"cond": {
"$lte": ["$$pv.Date", "$Date"]
}
}
}
}
},
“以前的值”数组中的每个项目将仅包含小于或等于每个记录的日期的日期:
{"Day": "2017-02-01",
"Temperature": 40,
"Previous Values": [
{"Day": "2017-01-31", "Temperature": 33},
{"Day": "2017-01-30", "Temperature": 36},
{"Day": "2017-01-29", "Temperature": 33},
{"Day": "2017-01-28", "Temperature": 32},
...
]}
现在我们可以选择平均窗口大小,因为数据是按天计算的,因此我们将在一周内获取数组的前7条记录;每月30;或3个月90天:
{
"$project": {
"_id": 0,
"Date": 1,
"Temperature": 1,
"Previous Values": {
"$slice": ["$Previous Values", 0, 90]
}
}
},
要平均以前的温度,我们展开“以前的值”数组,然后按日期字段分组。展开操作将执行以下操作:
{"Day": "2017-02-01",
"Temperature": 40,
"Previous Values": {
"Day": "2017-01-31",
"Temperature": 33}
},
{"Day": "2017-02-01",
"Temperature": 40,
"Previous Values": {
"Day": "2017-01-30",
"Temperature": 36}
},
{"Day": "2017-02-01",
"Temperature": 40,
"Previous Values": {
"Day": "2017-01-29",
"Temperature": 33}
},
...
看到“日期”字段是相同的,但是我们现在有了“先前值”数组中每个先前日期的文档。 现在我们可以重新分组,然后平均上一个值。温度以获得移动平均值:
{"$group": {
"_id": {
"Day": "$Date",
"Temperature": "$Temperature"
},
"3 Month Moving Average": {
"$avg": "$Previous Values.Temperature"
}
}
}
就是这样!我知道将每条记录加入每条记录并不理想,但这在较小的数据集上效果很好