javascript の Array.reduce() を使って JSON データを group by (sum) する

最近Mac(Macbook)を初めて触りました。
前の文字を削除する機能（windowsで言うところのdeleteキー）がなくて愕然としています。
deleteキーだと思って電源ボタンを押して毎回冷や汗流してます。。なんで何も反応しないのだろう。。

そんなこんなのqiita初投稿です。修行と思ってMacで書いています。　＃MacもMarkdownも辛い。。
歳はそれなりですが、お手柔らかにお願いします。

# 動機

最近お仕事などで、フロントエンドのjavascript上で多めのデータ（数万から数十万件くらい）を扱うことが多いです。
今回は同じデータソースから複数のグラフを表示するために、javascriptで集計処理を実装することにしました。
いや、こういう場合って、集計処理はおとなしくサーバに任せて、複数のREST API叩いて非同期でデータ取ってきた方が、結局早かったりするわけですが、今回はフロントエンドのプロトタイプ用途でサーバの準備もできない環境だったため、javascriptでやることになりました。

# 要件

プロトタイプでありがちな、「要件がないのが要件」状態だったので、抽象度を高める必要がありました。

- なんかデータ取ってくる　=> とりあえずJSONファイルを用意
- 取ってきたデータから、ドリルダウンできるテーブル用のデータを作る
- 取ってきたデータから、複数のグラフ用のデータを作る
- 後でサーバから取得する方法に変わるかもしれない

複数の粒度のデータファイルをあらかじめ用意するような単純作業はエンジニアにとってツラい作業なので、１つのJSONデータから粒度の異なる集計データを生成できるように、javasciriptで集計処理を実装することにしました。

# 前置きいいからはよ

ということで、ソース載せちゃいます。どーん！

```js:groupby.js
Object.prototype.values = function(){var o=this;var r=[];for(var k in o) if(o.hasOwnProperty(k)){r.push(o[k])}return r};

// JSON配列を指定のキーで集計する
Array.prototype.groupBy = function(keys,sumKeys) {
    var hash = this.reduce(function(res,data) {
        // 集計キーを作成
        var key = keys.reduce(function(s,k) {
            s += data[k];
            return s;
        },'');
        // 初めての集計キー
        if(!(key in res)) {
            // 集計キーをオブジェクトに設定
            var keyList = keys.reduce(function(h,k) {
                h[k] = data[k];
                return h;
            },{});
            // 集計項目の初期値を設定
            res[key] = sumKeys.reduce(function(h,k) {
                h[k] = 0;
                return h;
            }, keyList);
        }
        // データを集計（加算）
        sumKeys.forEach(function(k){
            res[key][k] += data[k];
        });

        return res;
    },{});

    return hash.values();
};
```

（Object.values() は http://qiita.com/Cside/items/5eb5d0f1aff22fe9a0ec から引用させていただきました。）
ざっくりこんな感じです。覚えたてのArray.reduce()をこれでもかと使い倒してます。

# テスト

早速テストしてみます。

## テストデータ

データはこちら。NPBセリーグの2013年から2015年の規定打席に到達した打者成績リストです。
(元データの出典は http://npb.jp/bis/2013/stats/bat_c.html こちら）

```js
var npbScores = [
        {"year":2015,"name":"川端　慎吾","team":"(ヤ)","dasuu":581,"hit":195,"homerun":8,"daten":57,"steal":4},
        {"year":2015,"name":"山田　哲人","team":"(ヤ)","dasuu":557,"hit":183,"homerun":38,"daten":100,"steal":34},
        {"year":2015,"name":"筒香　嘉智","team":"(デ)","dasuu":496,"hit":157,"homerun":24,"daten":93,"steal":0},
        {"year":2015,"name":"ルナ","team":"(中)","dasuu":496,"hit":145,"homerun":8,"daten":60,"steal":11},
        {"year":2015,"name":"ロペス","team":"(デ)","dasuu":516,"hit":150,"homerun":25,"daten":73,"steal":1},
        {"year":2015,"name":"平田　良介","team":"(中)","dasuu":491,"hit":139,"homerun":13,"daten":53,"steal":11},
        {"year":2015,"name":"鳥谷　敬","team":"(神)","dasuu":551,"hit":155,"homerun":6,"daten":42,"steal":9},
        {"year":2015,"name":"福留　孝介","team":"(神)","dasuu":495,"hit":139,"homerun":20,"daten":76,"steal":1},
        {"year":2015,"name":"マートン","team":"(神)","dasuu":544,"hit":150,"homerun":9,"daten":59,"steal":0},
        {"year":2015,"name":"梶谷　隆幸","team":"(デ)","dasuu":520,"hit":143,"homerun":13,"daten":66,"steal":28},
        {"year":2015,"name":"新井　貴浩","team":"(広)","dasuu":426,"hit":117,"homerun":7,"daten":57,"steal":3},
        {"year":2015,"name":"田中　広輔","team":"(広)","dasuu":543,"hit":149,"homerun":8,"daten":45,"steal":6},
        {"year":2015,"name":"ゴメス","team":"(神)","dasuu":520,"hit":141,"homerun":17,"daten":72,"steal":0},
        {"year":2015,"name":"エルナンデス","team":"(中)","dasuu":498,"hit":135,"homerun":11,"daten":58,"steal":5},
        {"year":2015,"name":"雄平","team":"(ヤ)","dasuu":551,"hit":149,"homerun":8,"daten":60,"steal":7},
        {"year":2015,"name":"坂本　勇人","team":"(巨)","dasuu":479,"hit":129,"homerun":12,"daten":68,"steal":10},
        {"year":2015,"name":"畠山　和洋","team":"(ヤ)","dasuu":512,"hit":137,"homerun":26,"daten":105,"steal":0},
        {"year":2015,"name":"大島　洋平","team":"(中)","dasuu":565,"hit":147,"homerun":6,"daten":27,"steal":22},
        {"year":2015,"name":"バルディリス","team":"(デ)","dasuu":465,"hit":120,"homerun":13,"daten":56,"steal":0},
        {"year":2015,"name":"菊池　涼介","team":"(広)","dasuu":562,"hit":143,"homerun":8,"daten":32,"steal":19},
        {"year":2015,"name":"上本　博紀","team":"(神)","dasuu":375,"hit":95,"homerun":4,"daten":31,"steal":19},
        {"year":2015,"name":"長野　久義","team":"(巨)","dasuu":434,"hit":109,"homerun":15,"daten":52,"steal":3},
// ... 以下省略
```

## テストプログラム
テストプログラムは２通り試してみました。

### テスト１
まずはお手並み拝見。３年間のヒット数をチーム別に集計します。

```groupby.js
// test1: チーム別ヒット数
var teamHits = npbScores.groupBy(["team"],["hit"]);
console.log(teamHits);
```

第１引数に集計キー、第２引数が集計項目を指定します。

結果はこうなりました。

```js
[ { team: '(ヤ)', hit: 1695 },
  { team: '(デ)', hit: 1589 },
  { team: '(中)', hit: 1742 },
  { team: '(神)', hit: 2135 },
  { team: '(広)', hit: 1284 },
  { team: '(巨)', hit: 1617 } ]
```

チームごとに３年間の総ヒット数が集計されています。（規定打席に到達した打者のみですが。。）

### テスト２
続いて、集計キーと集計項目を複数指定してみたいと思います。
年度別のチーム打者成績を集計してみます。

```gropuby.js
// test2: 年度別チーム別個人成績
var teamScoreByYear = npbScores.groupBy(["year","team"],["dasuu","hit","homerun","daten","steal"]);
console.log(teamScoreByYear);
```

結果はこうなりました。

```js
[ { year: 2015, team: '(ヤ)', dasuu: 2643, hit: 766, homerun: 82, daten: 355, steal: 48 },
  { year: 2015, team: '(デ)', dasuu: 1997, hit: 570, homerun: 75, daten: 288, steal: 29 },
  { year: 2015, team: '(中)', dasuu: 2050, hit: 566, homerun: 38, daten: 198, steal: 49 },
  { year: 2015, team: '(神)', dasuu: 2485, hit: 680, homerun: 56, daten: 280, steal: 29 },
  { year: 2015, team: '(広)', dasuu: 2061, hit: 541, homerun: 42, daten: 197, steal: 43 },
  { year: 2015, team: '(巨)', dasuu: 913, hit: 238, homerun: 27, daten: 120, steal: 13 },
  { year: 2014, team: '(神)', dasuu: 2532, hit: 751, homerun: 56, daten: 328, steal: 44 },
  { year: 2014, team: '(広)', dasuu: 1569, hit: 472, homerun: 67, daten: 229, steal: 51 },
  { year: 2014, team: '(ヤ)', dasuu: 2511, hit: 784, homerun: 110, daten: 396, steal: 31 },
  { year: 2014, team: '(中)', dasuu: 2383, hit: 705, homerun: 44, daten: 273, steal: 63 },
  { year: 2014, team: '(デ)', dasuu: 1869, hit: 496, homerun: 62, daten: 237, steal: 50 },
  { year: 2014, team: '(巨)', dasuu: 2424, hit: 647, homerun: 75, daten: 280, steal: 58 },
  { year: 2013, team: '(デ)', dasuu: 1818, hit: 523, homerun: 62, daten: 268, steal: 26 },
  { year: 2013, team: '(ヤ)', dasuu: 439, hit: 145, homerun: 60, daten: 131, steal: 0 },
  { year: 2013, team: '(巨)', dasuu: 2514, hit: 732, homerun: 106, daten: 352, steal: 40 },
  { year: 2013, team: '(神)', dasuu: 2455, hit: 704, homerun: 48, daten: 285, steal: 53 },
  { year: 2013, team: '(中)', dasuu: 1800, hit: 471, homerun: 62, daten: 224, steal: 22 },
  { year: 2013, team: '(広)', dasuu: 1044, hit: 271, homerun: 25, daten: 115, steal: 45 } ]
```

年別、チーム別で、打者成績の集計結果が得られました。


# 拡張してみる（コールバックの設定）

とりあえずの要件はこれで満せましたが、おまけでコールバック指定ができるようにしてみます。

```groupby.js
// JSON配列を指定のキーで集計する
Array.prototype.groupBy = function(keys,sumKeys,callback) {

// ... 中略

        // コールバックを実行
        if(typeof callback !== 'undefined') {
            callback(data,res[key]);
        }

        return res;
    },{});

    return hash.values();
};
```
コールバック関数には、集計前の配列データと、集計後の（途中）結果データを引数として取れるようにしました。

# テスト： 選手リストを保存する
上記で実装したコールバック関数を使って、チーム成績のデータに、選手の成績一覧を保存してみます。

```groupby.js
// test3: 年度別チーム別個人成績(選手リストつき)
var teamScoreByYearWithPlayers = npbScores.groupBy(["year","team"],["dasuu","hit","homerun","daten","steal"],function(player,team){
    // 選手をチームに追加
    if(!("players" in team)) {
        team.players = [];
    }
    team.players.push(player);
});
console.log(JSON.stringify(teamScoreByYearWithPlayers));
```

結果はこうなりました。（一部成形しています）

```json
[
{"year":2015,"team":"(ヤ)","dasuu":2643,"hit":766,"homerun":82,"daten":355,"steal":48,
    "players":[
        {"year":2015,"name":"川端　慎吾","team":"(ヤ)","dasuu":581,"hit":195,"homerun":8,"daten":57,"steal":4},
        {"year":2015,"name":"山田　哲人","team":"(ヤ)","dasuu":557,"hit":183,"homerun":38,"daten":100,"steal":34},
        {"year":2015,"name":"雄平","team":"(ヤ)","dasuu":551,"hit":149,"homerun":8,"daten":60,"steal":7},
        {"year":2015,"name":"畠山　和洋","team":"(ヤ)","dasuu":512,"hit":137,"homerun":26,"daten":105,"steal":0},
        {"year":2015,"name":"中村　悠平","team":"(ヤ)","dasuu":442,"hit":102,"homerun":2,"daten":33,"steal":3}
    ]},
{"year":2015,"team":"(デ)","dasuu":1997,"hit":570,"homerun":75,"daten":288,"steal":29,
    "players":[
        {"year":2015,"name":"筒香　嘉智","team":"(デ)","dasuu":496,"hit":157,"homerun":24,"daten":93,"steal":0},
        {"year":2015,"name":"ロペス","team":"(デ)","dasuu":516,"hit":150,"homerun":25,"daten":73,"steal":1},
        {"year":2015,"name":"梶谷　隆幸","team":"(デ)","dasuu":520,"hit":143,"homerun":13,"daten":66,"steal":28},
        {"year":2015,"name":"バルディリス","team":"(デ)","dasuu":465,"hit":120,"homerun":13,"daten":56,"steal":0}
    ]},
```

各チームの成績に、当該年度、各チームの選手データが残っていることが確認できました。

# まとめ
以上、「javascript の Array.reduce() を使って JSON データを group by (sum) する」方法についてお伝えしました。少しでもお役に立てましたら幸いです！ご意見、ご感想お待ちしております！