以前、Qiita.comにも投稿しましたが、こちらにも掲載しておきます。

javascriptの.map、.reduceを使って、時系列に記録されたデーターの区間ごとの平均値を計算しました。Javascript and MapReduceというサイトを参考にしました。

やりたいこと

次のような時系列データーがあるとします。この例ではd1とd2というデーターが30秒ごとのタイムスタンプで記録されています。例は12件、6分間のデーターですが、実際にはこれが延々と続いていると考えてください。

var data = [
    {"created":"2016-04-04T15:00:20.199Z","d1":3.2,"d2":1},
    {"created":"2016-04-04T15:00:50.200Z","d1":3.1,"d2":1.1},
    {"created":"2016-04-04T15:01:20.204Z","d1":3.2,"d2":1.1},
    {"created":"2016-04-04T15:01:50.201Z","d1":3.2,"d2":1.1},
    {"created":"2016-04-04T15:02:20.204Z","d1":3.1,"d2":1.1},
    {"created":"2016-04-04T15:02:50.206Z","d1":3, "d2":1.1},
    {"created":"2016-04-04T15:03:20.193Z","d1":2.9,"d2":1},
    {"created":"2016-04-04T15:03:50.199Z","d1":3.3,"d2":1.1},
    {"created":"2016-04-04T15:04:20.196Z","d1":2.9,"d2":1.1},
    {"created":"2016-04-04T15:04:50.202Z","d1":3, "d2":1},
    {"created":"2016-04-04T15:05:20.197Z","d1":3.1,"d2":1.1},
    {"created":"2016-04-04T15:05:50.196Z","d1":3.1,"d2":1.1}
];

やりたいことは、この時系列のデーターから5分間とか10分間とかの区間の平均値を計算することです。計算する区間は1分間、2分間、3分間、…20分間、30分間、60分間と60を割り切れる数とし、60分以上は考えないことにします。以下は平均する区間を2分にした例で説明します。

.map

最初にやることは.map()で30秒ごとのデーターをn分、ここでは2分ごとのデーターにまとめることです。

    var n = 2;
    var result = data
    .map(function(item) {
        var _d = new Date(item.created);
        var d = _d.getFullYear() + '-' + (_d.getMonth() + 1) + '-' + _d.getDate()
            + ' ' + _d.getHours() + ':' + Math.floor(_d.getMinutes() / n) * n + ':0';
        return {key: d, value: {d1: item.d1, d2: item.d2}};
    });
    console.log(result);

タイムスタンプの分の部分をn分ごとに切り捨てて、同じ時刻にまとめています。これで元のデーターは次のようになります。時刻のフォーマットが元は協定世界時(UTC)で、処理後は現地時間になっていますが、支障はないので気にしないことにします。

var data = [
    {key: "2016-4-5 0:0:0", value: {"d1":3.2,"d2":1}},
    {key: "2016-4-5 0:0:0", value: {"d1":3.1,"d2":1.1}},
    {key: "2016-4-5 0:0:0", value: {"d1":3.2,"d2":1.1}},
    {key: "2016-4-5 0:0:0", value: {"d1":3.2,"d2":1.1}},
    {key: "2016-4-5 0:2:0", value: {"d1":3.1,"d2":1.1}},
    {key: "2016-4-5 0:2:0", value: {"d1":3, "d2":1.1}},
    {key: "2016-4-5 0:2:0", value: {"d1":2.9,"d2":1}},
    {key: "2016-4-5 0:2:0", value: {"d1":3.3,"d2":1.1}},
    {key: "2016-4-5 0:4:0", value: {"d1":2.9,"d2":1.1}},
    {key: "2016-4-5 0:4:0", value: {"d1":3, "d2":1}},
    {key: "2016-4-5 0:4:0", value: {"d1":3.1,"d2":1.1}},
    {key: "2016-4-5 0:4:0", value: {"d1":3.1,"d2":1.1}}
];

.reduce

次に.reduce()でn分ごとのデーターを足しこみ、データーの件数もカウントします。

    var n = 2;
    var result = data
    .map(function(item) {
        var _d = new Date(item.created);
        var d = _d.getFullYear() + '-' + (_d.getMonth() + 1) + '-' + _d.getDate()
            + ' ' + _d.getHours() + ':' + Math.floor(_d.getMinutes() / n) * n + ':0';
        return {key: d, value: {d1: item.d1, d2: item.d2}};
    })
    .reduce(function(last, now) {
        var index = last[0].indexOf(now.key);
        if (index == -1) {
            last[0].push(now.key);
            last[1].push(now.value);
            last[2].push(1);
        } else {
            last[1][index].d1 += now.value.d1;
            last[1][index].d2 += now.value.d2;
            last[2][index] += 1;
        }
        return last;
    }, [[], [], []]);
    console.log(result);

最初に空の初期値[[], [], []]を渡し、.map()の出力に対して、配列の1番目にタイムスタンプ、2番目にデーターを足し込んだ値、3番目にデーター件数をカウントしていきます。データーは次のように加工されます。30秒ごとのデーターを2分ごとに区切って計算すれば、データー件数が「4」になるのは自明な気もします。実際に使うときは外部のセンサーで測定して送信されたデーターで、到着間隔がゆらいだり、データーが欠落している可能性があることを想定したため、データー件数をカウントするようにしました。

[
    ["2016-4-5 0:0:0", "2016-4-5 0:2:0", "2016-4-5 0:4:0"],
    [{d1: 12.7, d2: 4.3}, {d1: 12.3, d2: 4.3}, {d1: 12.1, d2: 4.3}],
    [4, 4, 4]
]

出力

最後に、この配列を列ごとにまとめて、平均値を計算すればできあがりです。元のデーターと同じ形式で出力することにします。

    var n = 2;
    var result = data
    .map(function(item) {
        var _d = new Date(item.created);
        var d = _d.getFullYear() + '-' + (_d.getMonth() + 1) + '-' + _d.getDate()
            + ' ' + _d.getHours() + ':' + Math.floor(_d.getMinutes() / n) * n + ':0';
        return {key: d, value: {d1: item.d1, d2: item.d2}};
    })
    .reduce(function(last, now) {
        var index = last[0].indexOf(now.key);
        if (index == -1) {
            last[0].push(now.key);
            last[1].push(now.value);
            last[2].push(1);
        } else {
            last[1][index].d1 += now.value.d1;
            last[1][index].d2 += now.value.d2;
            last[2][index] += 1;
        }
        return last;
    }, [[], [], []]);
    var zip = [];
    result[0].forEach(function(key, index) {
        var jsondata = {created: key};
        jsondata.d1 = result[1][index].d1 / result[2][index];
        jsondata.d2 = result[1][index].d2 / result[2][index];
        zip.push(jsondata);
    });
    console.log(zip);

次のような結果が得られました。

[
    {created: "2016-4-5 0:0:0", d1: 3.175, d2: 1.075},
    {created: "2016-4-5 0:2:0", d1: 3.075, d2: 1.075},
    {created: "2016-4-5 0:4:0", d1: 3.025, d2: 1.075}
]

この処理は、[IoTクラウドサービスAmbient](https://ambidata.io)で、受信したデーターをグラフ表示する際、区間の平均値を表示するところで使いました。