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