Ramda で階層的なグルーピング
JavaScript 用の関数型ライブラリ Ramda で階層的なグルーピングを行ってみます。
ソースは http://github.com/fits/try_samples/tree/master/blog/20180220/
はじめに
概要
今回は、以下のデータに対して階層的なグルーピングと集計処理を適用します。
データ
const data = [ {category: 'A', item: 'A01', date: '2018-02-01', value: 1}, {category: 'A', item: 'A02', date: '2018-02-01', value: 1}, {category: 'A', item: 'A01', date: '2018-02-01', value: 1}, {category: 'A', item: 'A01', date: '2018-02-02', value: 20}, {category: 'A', item: 'A03', date: '2018-02-03', value: 2}, {category: 'B', item: 'B01', date: '2018-02-02', value: 1}, {category: 'A', item: 'A03', date: '2018-02-03', value: 5}, {category: 'A', item: 'A01', date: '2018-02-02', value: 2}, {category: 'B', item: 'B01', date: '2018-02-03', value: 3}, {category: 'B', item: 'B01', date: '2018-02-04', value: 1}, {category: 'C', item: 'C01', date: '2018-02-01', value: 1}, {category: 'B', item: 'B01', date: '2018-02-04', value: 10} ]
具体的には、上記を category
item
date
の順に階層的にグルーピングした後、value
の合計値を算出して以下のようにします。
処理結果
{ A: { A01: { '2018-02-01': 2, '2018-02-02': 22 }, A02: { '2018-02-01': 1 }, A03: { '2018-02-03': 7 } }, B: { B01: { '2018-02-02': 1, '2018-02-03': 3, '2018-02-04': 11 } }, C: { C01: { '2018-02-01': 1 } } }
Ramda インストール
Ramda は以下のようにインストールしておきます。
> npm install ramda
実装
(a) 階層的なグルーピングと集計
まずは、処理方法を確認するため、順番に処理を実施してみます。
1. category でグルーピング(1層目)
指定項目によるグルーピング処理は R.groupBy
で行えます。
category でグルーピングする処理は以下のようになります。
category グルーピング処理
const R = require('ramda') const data = [ {category: 'A', item: 'A01', date: '2018-02-01', value: 1}, {category: 'A', item: 'A02', date: '2018-02-01', value: 1}, {category: 'A', item: 'A01', date: '2018-02-01', value: 1}, {category: 'A', item: 'A01', date: '2018-02-02', value: 20}, {category: 'A', item: 'A03', date: '2018-02-03', value: 2}, {category: 'B', item: 'B01', date: '2018-02-02', value: 1}, {category: 'A', item: 'A03', date: '2018-02-03', value: 5}, {category: 'A', item: 'A01', date: '2018-02-02', value: 2}, {category: 'B', item: 'B01', date: '2018-02-03', value: 3}, {category: 'B', item: 'B01', date: '2018-02-04', value: 1}, {category: 'C', item: 'C01', date: '2018-02-01', value: 1}, {category: 'B', item: 'B01', date: '2018-02-04', value: 10} ] const res1 = R.groupBy(R.prop('category'), data) console.log(res1)
category グルーピング結果
{ A: [ { category: 'A', item: 'A01', date: '2018-02-01', value: 1 }, { category: 'A', item: 'A02', date: '2018-02-01', value: 1 }, { category: 'A', item: 'A01', date: '2018-02-01', value: 1 }, { category: 'A', item: 'A01', date: '2018-02-02', value: 20 }, { category: 'A', item: 'A03', date: '2018-02-03', value: 2 }, { category: 'A', item: 'A03', date: '2018-02-03', value: 5 }, { category: 'A', item: 'A01', date: '2018-02-02', value: 2 } ], B: [ { category: 'B', item: 'B01', date: '2018-02-02', value: 1 }, { category: 'B', item: 'B01', date: '2018-02-03', value: 3 }, { category: 'B', item: 'B01', date: '2018-02-04', value: 1 }, { category: 'B', item: 'B01', date: '2018-02-04', value: 10 } ], C: [ { category: 'C', item: 'C01', date: '2018-02-01', value: 1 } ] }
2. item でグルーピング(2層目)
category のグルーピング結果を item で更にグルーピングするには res1
の値部分 ([ { category: 'A', ・・・}, ・・・ ]
等) に R.groupBy
を適用します。
これは R.mapObjIndexed
で実施できます。
item グルーピング処理
const res2 = R.mapObjIndexed(R.groupBy(R.prop('item')), res1) console.log(res2)
item グルーピング結果
{ A: { A01: [ [Object], [Object], [Object], [Object] ], A02: [ [Object] ], A03: [ [Object], [Object] ] }, B: { B01: [ [Object], [Object], [Object], [Object] ] }, C: { C01: [ [Object] ] } }
3. date でグルーピング(3層目)
更に date でグルーピングするには R.mapObjIndexed
を重ねて R.groupBy
を適用します。
date グルーピング処理
const res3 = R.mapObjIndexed(R.mapObjIndexed(R.groupBy(R.prop('date'))), res2) console.log(res3)
date グルーピング結果
{ A: { A01: { '2018-02-01': [Array], '2018-02-02': [Array] }, A02: { '2018-02-01': [Array] }, A03: { '2018-02-03': [Array] } }, B: { B01: { '2018-02-02': [Array], '2018-02-03': [Array], '2018-02-04': [Array] } }, C: { C01: { '2018-02-01': [Array] } } }
4. value の合計
最後に、R.groupBy
の代わりに value を合計する処理(以下の sumValue
)へ R.mapObjIndexed
を階層分だけ重ねて適用すれば完成です。
value 合計処理
const sumValue = R.reduce((a, b) => a + b.value, 0) const res4 = R.mapObjIndexed(R.mapObjIndexed(R.mapObjIndexed(sumValue)), res3) console.log(res4)
value 合計結果
{ A: { A01: { '2018-02-01': 2, '2018-02-02': 22 }, A02: { '2018-02-01': 1 }, A03: { '2018-02-03': 7 } }, B: { B01: { '2018-02-02': 1, '2018-02-03': 3, '2018-02-04': 11 } }, C: { C01: { '2018-02-01': 1 } } }
(b) N階層のグルーピングと集計
次は、汎用的に使えるような実装にしてみます。
任意の処理に対して指定回数だけ R.mapObjIndexed
を重ねる処理があると便利なので applyObjIndexedN
として実装しました。
(a) で実施したように、階層的なグルーピングは R.mapObjIndexed
を階層分重ねた R.groupBy
を繰り返し適用していくだけですので R.reduce
で実装できます。(以下の groupByMulti
)
ちなみに、階層的にグルーピングする実装例は Ramda の Cookbook(groupByMultiple) にありましたが、変数へ再代入したりと手続き的な実装内容になっているのが気になりました。
sample.js
const R = require('ramda') const data = [ {category: 'A', item: 'A01', date: '2018-02-01', value: 1}, {category: 'A', item: 'A02', date: '2018-02-01', value: 1}, {category: 'A', item: 'A01', date: '2018-02-01', value: 1}, {category: 'A', item: 'A01', date: '2018-02-02', value: 20}, {category: 'A', item: 'A03', date: '2018-02-03', value: 2}, {category: 'B', item: 'B01', date: '2018-02-02', value: 1}, {category: 'A', item: 'A03', date: '2018-02-03', value: 5}, {category: 'A', item: 'A01', date: '2018-02-02', value: 2}, {category: 'B', item: 'B01', date: '2018-02-03', value: 3}, {category: 'B', item: 'B01', date: '2018-02-04', value: 1}, {category: 'C', item: 'C01', date: '2018-02-01', value: 1}, {category: 'B', item: 'B01', date: '2018-02-04', value: 10} ] /* 指定回数(n)だけ R.mapObjIndexed を重ねた任意の処理(fn)を data を引数にして実行する処理 */ const applyObjIndexedN = R.curry((n, fn, data) => R.reduce( (a, b) => R.mapObjIndexed(a), fn, R.range(0, n) )(data) ) // 階層的なグルーピング処理 const groupByMulti = R.curry((fields, data) => R.reduce( (a, b) => applyObjIndexedN(b, R.groupBy(R.prop(fields[b])), a), data, R.range(0, fields.length) ) ) const cols = ['category', 'item', 'date'] const sumValue = R.reduce((a, b) => a + b.value, 0) const sumMultiGroups = R.pipe( groupByMulti(cols), // グルーピング処理 applyObjIndexedN(cols.length, sumValue) // 合計処理 ) console.log( sumMultiGroups(data) )
実行結果
> node sample.js { A: { A01: { '2018-02-01': 2, '2018-02-02': 22 }, A02: { '2018-02-01': 1 }, A03: { '2018-02-03': 7 } }, B: { B01: { '2018-02-02': 1, '2018-02-03': 3, '2018-02-04': 11 } }, C: { C01: { '2018-02-01': 1 } } }