Edited at

javascriptの.map、.reduceで区間の平均値を計算する

More than 1 year has passed since last update.

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で、受信したデーターをグラフ表示する際、区間の平均値を表示するところで使いました。