Python按日期范围求和

1 投票
1 回答
802 浏览
提问于 2025-04-18 16:15

我有一个列表,里面包含了时间戳和对应的值,这些数据是从我的mongodb数据库里拿来的。

(mongodb里的数据是这样组织的:)

"timestamp" : ISODate("2014-01-01T00:00:00.000Z"),
realPower" : {
        "0" : {
            "0": 545.5,
            "15" : 614.5,
            "30" : 586.25,
            "45" : 565.75
        },
        "1" : {
            "0" : 574.5,
            "15" : 549.5,
            "30" : 564,
            "45" : 545.75
        },
    ( … )
        "22" : {
            "0" : 604.75,
            "15" : 605,
            "30" : 605,
            "45" : 605
        },
        "23" : {
            "0" : 604.75,
            "15" : 605,
            "30" : 605,
            "45" : 604.5
        }
    }
}

我把mongodb里的数据转换成了下面这个列表。

列表项(一天的数据):

[datetime.datetime(2014, 1, 1, 1, 0), 545.5]
[datetime.datetime(2014, 1, 1, 1, 15), 614.5]
[datetime.datetime(2014, 1, 1, 1, 30), 586.25]
[datetime.datetime(2014, 1, 1, 1, 45), 565.75]
(...)
[datetime.datetime(2014, 1, 1, 23, 45), 604.5]

我有一个方法可以为我的数据生成一个很好的时间间隔:

def date_span(start_date, end_date, data):
    delta = datetime.timedelta(hours=15)
    current_date = start_date.replace(minute=0)
    while current_date < end_date:
        yield current_date
        current_date += delta

但是,我该如何把列表里的数据和新的时间段数据结合起来并求和呢?我想按照给定的时间来汇总这些值。比如说,按小时、按天、按周、按月、按年来汇总。有没有什么建议?

1 个回答

1

目前你存储数据的方式并不太适合你的需求。你可以进一步改进你的“转换”,以这种方式存储数据:

{ "timestamp": ISODate("2014-01-01T00:00:00.000Z"), "realPower": 545.5 }
{ "timestamp": ISODate("2014-01-01T00:15:00.000Z"), "realPower": 614.5 }
{ "timestamp": ISODate("2014-01-01T00:30:00.000Z"), "realPower": 586.25 }
{ "timestamp": ISODate("2014-01-01T00:45:00.000Z"), "realPower": 565.75 }
{ "timestamp": ISODate("2014-01-01T01:00:00.000Z"), "realPower": 574.5 }
{ "timestamp": ISODate("2014-01-01T01:15:00.000Z"), "realPower": 549.5 }
{ "timestamp": ISODate("2014-01-01T01:30:00.000Z"), "realPower": 564 }
{ "timestamp": ISODate("2014-01-01T01:45:00.000Z"), "realPower": 545.75 }
{ ... }
{ "timestamp": ISODate("2014-01-01T23:00:00.000Z"), "realPower": 604.75 }
{ "timestamp": ISODate("2014-01-01T23:15:00.000Z"), "realPower": 605 }
{ "timestamp": ISODate("2014-01-01T23:30:00.000Z"), "realPower": 605 }
{ "timestamp": ISODate("2014-01-01T23:45:00.000Z"), "realPower": 604.5 }

原因在于你现在的“子文档”结构在服务器端聚合时效果不好。这主要是因为“你数据的一部分”被当作“键”来表示,这种做法并不太好。

虽然有些情况下使用“子文档”来表示一个区间是有意义的,但通常这些情况涉及在特定区间内保持“离散值的桶”,主要目的是避免使用“嵌套数组”,因为嵌套数组在更新时通常会带来麻烦。

但在建议的结构中,你的查询就简单多了,只需应用聚合框架。这里有日期操作符可以用来处理特定区间的分组:

db.collection.aggregate([
    // Match documents between dates
    { "$match": { 
        "timestamp": { "$gte": startDate, "$lte": endDate }
    }},
    // Group by hour
    { "$group": {
        "_id": {
            "year": { "$year": "$timestamp" },
            "month": { "$month": "$timestamp" },
            "day": { "$dayOfMonth": "$timestamp" },
            "hour": { "$hour": "$timestamp" }
        },
        "avgPower": { "$avg": "$realPower" }
    }}
])

基本上,你需要从时间戳值中定义一个“分组键”,然后对你想要的其他值应用任何分组累加操作符,在这个例子中是平均值。

除了使用日期聚合操作符,你还可以将日期对象转换为纪元时间戳值,然后进行区间的日期计算。在这里,epochDate是一个传入的日期对象,表示“1970-01-01”,这是纪元的起始日期:

db.collection.aggregate([
    //Match documents between dates
    { "$match": { 
        "timestamp": { "$gte": startDate, "$lte": endDate }
    }},
    //Group by day: 1000 * 60 * 60 * 24 = milliseconds in a day
    { "$group": {
        "_id": {
            "$subtract": [
                { "$subtract": [
                    "$timestamp", epochDate
                ]},
                { "$mod": [
                    { "$subtract": [
                        "$timestamp", epochDate
                    ]},
                    1000 * 60 * 60 * 24
                ]}
            ]
        },
        "sumPower": { "$sum": "$realPower" }
    }}
])

如果你需要,结果的时间戳值可以再转换回日期对象。这里的关键是,像“减去”一个日期对象与另一个日期对象的操作,会得到以毫秒表示的差值,结果是一个数字。


不过,使用当前的结构,你需要在服务器端用mapReduce来处理。这会慢很多,因为需要“解释”代码。

所以在一个映射器中,按月分组进行“求和”:

function() {
    var values = [];

    var realPower = this.realPower;
    for ( var k in realPower ) {
        for ( var i in k ) {
            values.push( realPower[k][i] );
        }
    }

    emit(
        { 
            "year": this.timestamp.getFullYear(),
            "month": this.timestamp.getMonth() + 1
        },
        { "values": values }
    );
}

然后是一个归约器:

function(key,values) {
    var result = { "values": [] };

    values.forEach(function(value) {
        value.values.forEach(function(item) {
            result.values.push( item );
        }
    }
}

并在最终函数中处理“求和”,以防某个分组只发出了一个键:

function(key,value) {

    return Array.sum( value.values );

}

最后用查询调用mapReduce:

results = db.collection.inline_map_reduce(
    map,
    reduce, 
    query={ "timestamp": { "$gte": startDate, "$lte": endDate } },
    finalize=finalize
)

总的来说,这种方式更复杂,速度也慢。正如你在“映射器”定义中看到的,必须遍历“子文档”结构,或者选择“特定”的键,比如按小时累加时的情况。


无论如何,服务器端处理通常是更好的选择,因为你的数据库服务器通常比应用服务器更强大,或者至少应该是这样。

尝试改变数据结构。查询和进一步聚合的好处远大于一次性数据处理的成本。

撰写回答