概要
Aggregation Pipeline Stageの中でも、今まで全然使う機会がなかった$bucket, $facetを度数分布データを作ることで試しました。 結構勉強になりました。最後Appendixには実際はアプリケーションで行ったほうが楽だと思う処理を無理してAggregationで実装してみました。
環境
- Windows 10
- MongoDB 4.2.1
実行
テストデータ
まずは簡単なデータを用意します。_id
は学生番号と思ってください。
db.score.insert([
{_id:1,成績:{数学:10,英語:90}},
{_id:2,成績:{数学:16,英語:26}},
{_id:3,成績:{数学:80,英語:20}},
{_id:4,成績:{数学:84,英語:94}},
{_id:5,成績:{数学:85,英語:50}},
])
$bucketのテスト
db.score.aggregate([
{$bucket:{
groupBy:"$成績.数学",
boundaries: [0,10,20,30,40,50,60,70,80,90,101],
default: "other",
output:{
数学度数:{$sum:1},
}
}}
])
{ "_id" : 10, "数学度数" : 2 }
{ "_id" : 80, "数学度数" : 3 }
-
groupBy
集計キーにしたいフィールド名 -
boundaries
集計キーの区間- 各区間は[0,10),[10,20),[20,30),[30,40),[40,50),[50,60),[60,70),[70,80),[80,90),[90,101)となり各区間の下限はイコールを含みますが、上限はイコールを含みません。よって最後の区間は100点を含ませたいので101にしています。
-
default
boundariesに含まれないデータがdefault
で指定されたフィールド名で集計されます -
output
出力フィールドの式を記述します。演算子は$group
で使用できるものが使えます - 出力の
_id
は区間の下限は表示されています
$facetのテスト
まずは簡単なテスト
db.score.aggregate([
{$facet:{
lt20:[
{$match:{"成績.数学":{$lt:20}}},
{$project:{数学:"$成績.数学"}}
],
gte20:[
{$match:{"成績.数学":{$gte:20}}},
{$project:{数学:"$成績.数学"}}
]
}}
]).pretty()
{
"lt20" : [
{
"_id" : 1,
"数学" : 10
},
{
"_id" : 2,
"数学" : 16
}
],
"gte20" : [
{
"_id" : 3,
"数学" : 80
},
{
"_id" : 4,
"数学" : 84
},
{
"_id" : 5,
"数学" : 85
}
]
}
解説
$facet
で出力フィールドごとにPipelineが使えて、入力は全ての出力フィールドごとに同じである。また、$facet
の出力ドキュメントは1件であり、各Pipelineの出力結果は配列として出力される。1ドキュメントサイズの16MB制限に注意が必要である。
上記の例では同じ$project
を使っているので、次のようにも書ける。
db.score.aggregate([
{$project:{数学:"$成績.数学"}},
{$facet:{
lt20:[
{$match:{数学:{$lt:20}}}
],
gte20:[
{$match:{数学:{$gte:20}}},
]
}}
]).pretty()
結果はご自分で確かめてください。
度数分布データを作る
db.score.aggregate([
{$facet:{
数学:[
{$bucket:{
groupBy:"$成績.数学",
boundaries: [0,10,20,30,40,50,60,70,80,90,101],
default: "other",
output:{
度数:{$sum:1},
}
}}
],
英語:[
{$bucket:{
groupBy:"$成績.英語",
boundaries: [0,10,20,30,40,50,60,70,80,90,101],
default: "other",
output:{
度数:{$sum:1},
}
}}
],
全体:[
{$group:{
_id:null,学生数:{$sum:1}, 数学平均:{$avg:"$成績.数学"},数学分散:{ $stdDevPop:"$成績.数学" },
英語平均:{$avg:"$成績.英語"},英語分散:{ $stdDevPop:"$成績.英語" }
}},
{$project:{_id:0}}
]
}},
{$project:{
数学:1, 英語:1, 全体:{$arrayElemAt:["$全体", 0]}
}}
]).pretty()
{
"数学" : [
{
"_id" : 10,
"度数" : 2
},
{
"_id" : 80,
"度数" : 3
}
],
"英語" : [
{
"_id" : 20,
"度数" : 2
},
{
"_id" : 50,
"度数" : 1
},
{
"_id" : 90,
"度数" : 2
}
],
"全体" : {
"学生数" : 5,
"数学平均" : 55,
"数学分散" : 34.38604368053993,
"英語平均" : 56,
"英語分散" : 31.086974764360715
}
}
- 数学
- $bucketを使って各区間の数学の度数を計算します
- 英語
- $bucketを使って各区間の英語の度数を計算します
- 全体
- $groupを使って全体の件数、数学と英語の平均・分散を計算します
最後の$projectは全体
は配列の必要がないので、配列の0番目を取り出しています。
相対度数の計算を追加
db.score.aggregate([
{$facet:{
数学:[
{$bucket:{
groupBy:"$成績.数学",
boundaries: [0,10,20,30,40,50,60,70,80,90,101],
default: "other",
output:{
度数:{$sum:1},
}
}}
],
英語:[
{$bucket:{
groupBy:"$成績.英語",
boundaries: [0,10,20,30,40,50,60,70,80,90,101],
default: "other",
output:{
度数:{$sum:1},
}
}}
],
全体:[
{$group:{
_id:null,学生数:{$sum:1}, 数学平均:{$avg:"$成績.数学"},数学分散:{ $stdDevPop:"$成績.数学" },
英語平均:{$avg:"$成績.英語"},英語分散:{ $stdDevPop:"$成績.英語" }
}},
{$project:{_id:0}}
]
}},
{$project:{
数学:1, 英語:1, 全体:{$arrayElemAt:["$全体", 0]}
}},
{$project:{
全体:1,
数学:{
$map:{
input: "$数学",
as :"elm",
in :{点数:"$$elm._id", 度数:"$$elm.度数", 相対度数:{$divide:["$$elm.度数","$全体.学生数"]}}
}
},
英語:{
$map:{
input: "$英語",
as :"elm",
in :{点数:"$$elm._id", 度数:"$$elm.度数", 相対度数:{$divide:["$$elm.度数","$全体.学生数"]}}
}
}
}}
]).pretty()
{
"全体" : {
"学生数" : 5,
"数学平均" : 55,
"数学分散" : 34.38604368053993,
"英語平均" : 56,
"英語分散" : 31.086974764360715
},
"数学" : [
{
"点数" : 10,
"度数" : 2,
"相対度数" : 0.4
},
{
"点数" : 80,
"度数" : 3,
"相対度数" : 0.6
}
],
"英語" : [
{
"点数" : 20,
"度数" : 2,
"相対度数" : 0.4
},
{
"点数" : 50,
"度数" : 1,
"相対度数" : 0.2
},
{
"点数" : 90,
"度数" : 2,
"相対度数" : 0.4
}
]
}
最後に追加した$project
とで$map
演算子を使って相対度数=度数/学生数の計算をして追加します。
大量データを使ってのテスト
次のように乱数を使って1000件のデータを挿入し、上記のAggregationを実行してみてください。
db.score.remove({})
for(let i=1;i <= 1000;i++) {
rnd1 = Math.floor( Math.random() * 101 )
rnd2 = Math.floor( Math.random() * 101 )
db.score.insert({_id:i, 成績:{数学:rnd1, 英語:rnd2}})
}
Appendix
以下の処理については実際はアプリケーションで処理したほうが楽だと思います。
テストデータは最初に使った少量データを使っています。
度数分布を点数順、度数順の二つを作る
英語のみ度数順を追加します。
db.score.aggregate([
{$facet:{
数学:[
{$bucket:{
groupBy:"$成績.数学",
boundaries: [0,10,20,30,40,50,60,70,80,90,101],
default: "other",
output:{
度数:{$sum:1},
}
}}
],
英語:[
{$bucket:{
groupBy:"$成績.英語",
boundaries: [0,10,20,30,40,50,60,70,80,90,101],
default: "other",
output:{
度数:{$sum:1},
}
}}
],
全体:[
{$group:{
_id:null,学生数:{$sum:1}, 数学平均:{$avg:"$成績.数学"},数学分散:{ $stdDevPop:"$成績.数学" },
英語平均:{$avg:"$成績.英語"},英語分散:{ $stdDevPop:"$成績.英語" }
}},
{$project:{_id:0}}
]
}},
{$project:{
数学:1, 英語:1, 全体:{$arrayElemAt:["$全体", 0]}
}},
{$project:{
全体:1,
数学:{
$map:{
input: "$数学",
as :"elm",
in :{点数:"$$elm._id", 度数:"$$elm.度数", 相対度数:{$divide:["$$elm.度数","$全体.学生数"]}}
}
},
英語:{
$map:{
input: "$英語",
as :"elm",
in :{点数:"$$elm._id", 度数:"$$elm.度数", 相対度数:{$divide:["$$elm.度数","$全体.学生数"]}}
}
}
}},
{$project:{
全体:1,
数学:1,
英語:1,
英語2:"$英語"
}},
{$unwind:"$英語2"},
{$sort:{"英語2.度数":-1}},
{$group:{
_id:null,全体:{$first:"$全体"},数学:{$first:"$数学"},英語:{$first:"$英語"},英語度数順:{$push:"$英語2"}
}},
{$project:{_id:0}}
]).pretty()
{
"全体" : {
"学生数" : 5,
"数学平均" : 55,
"数学分散" : 34.38604368053993,
"英語平均" : 56,
"英語分散" : 31.086974764360715
},
"数学" : [
{
"点数" : 10,
"度数" : 2,
"相対度数" : 0.4
},
{
"点数" : 80,
"度数" : 3,
"相対度数" : 0.6
}
],
"英語" : [
{
"点数" : 20,
"度数" : 2,
"相対度数" : 0.4
},
{
"点数" : 50,
"度数" : 1,
"相対度数" : 0.2
},
{
"点数" : 90,
"度数" : 2,
"相対度数" : 0.4
}
],
"英語度数順" : [
{
"点数" : 20,
"度数" : 2,
"相対度数" : 0.4
},
{
"点数" : 90,
"度数" : 2,
"相対度数" : 0.4
},
{
"点数" : 50,
"度数" : 1,
"相対度数" : 0.2
}
]
}
累積度数、累積相対度数
プログラミングではないので値を一時的に貯めておくことが出来ないので累積計算は基本的にはできないので、結構面倒な処理になってしまいました。
db.score.aggregate([
{$facet:{
数学:[
{$bucket:{
groupBy:"$成績.数学",
boundaries: [0,10,20,30,40,50,60,70,80,90,101],
default: "other",
output:{
度数:{$sum:1},
}
}}
],
英語:[
{$bucket:{
groupBy:"$成績.英語",
boundaries: [0,10,20,30,40,50,60,70,80,90,101],
default: "other",
output:{
度数:{$sum:1},
}
}}
],
全体:[
{$group:{
_id:null,学生数:{$sum:1}, 数学平均:{$avg:"$成績.数学"},数学分散:{ $stdDevPop:"$成績.数学" },
英語平均:{$avg:"$成績.英語"},英語分散:{ $stdDevPop:"$成績.英語" }
}},
{$project:{_id:0}}
]
}},
{$project:{
数学:1, 英語:1, 全体:{$arrayElemAt:["$全体", 0]}
}},
{$project:{
全体:1,
数学:{
$map:{
input: "$数学",
as :"elm",
in :{点数:"$$elm._id", 度数:"$$elm.度数", 相対度数:{$divide:["$$elm.度数","$全体.学生数"]}}
}
},
英語:{
$map:{
input: "$英語",
as :"elm",
in :{点数:"$$elm._id", 度数:"$$elm.度数", 相対度数:{$divide:["$$elm.度数","$全体.学生数"]}}
}
}
}},
{$project:{
全体:1,
数学:1,
英語:1,
英語2:"$英語"
}},
{$unwind:"$英語2"},
{$sort:{"英語2.度数":-1}},
{$group:{
_id:null,全体:{$first:"$全体"},数学:{$first:"$数学"},英語:{$first:"$英語"},英語度数順:{$push:"$英語2"}
}},
{$project:{
_id:0,
全体:1,
数学:1,
英語:1,
英語度数順:{$map:{
input:"$英語度数順",
as: "elm",
in: {
点数:"$$elm.点数",
度数:"$$elm.度数",
相対度数:"$$elm.相対度数",
累積: {$slice:["$英語度数順", {$add:[{$indexOfArray:["$英語度数順.点数","$$elm.点数"]},1]}]}}}
}
}},
{$project:{
全体:1,
数学:1,
英語:1,
英語度数順: {$map:{
input: "$英語度数順",
as: "elm",
in: {
点数:"$$elm.点数",
度数:"$$elm.度数",
累積度数: {$sum:"$$elm.累積.度数"},
相対度数:"$$elm.相対度数",
累積相対度数:{$sum:"$$elm.累積.相対度数"},
累積相対度数2: {$divide:[{$sum:"$$elm.累積.度数"},"$全体.学生数"]}
}
}}
}}
]).pretty()
{
"全体" : {
"学生数" : 5,
"数学平均" : 55,
"数学分散" : 34.38604368053993,
"英語平均" : 56,
"英語分散" : 31.086974764360715
},
"数学" : [
{
"点数" : 10,
"度数" : 2,
"相対度数" : 0.4
},
{
"点数" : 80,
"度数" : 3,
"相対度数" : 0.6
}
],
"英語" : [
{
"点数" : 20,
"度数" : 2,
"相対度数" : 0.4
},
{
"点数" : 50,
"度数" : 1,
"相対度数" : 0.2
},
{
"点数" : 90,
"度数" : 2,
"相対度数" : 0.4
}
],
"英語度数順" : [
{
"点数" : 20,
"度数" : 2,
"累積度数" : 2,
"相対度数" : 0.4,
"累積相対度数" : 0.4,
"累積相対度数2" : 0.4
},
{
"点数" : 90,
"度数" : 2,
"累積度数" : 4,
"相対度数" : 0.4,
"累積相対度数" : 0.8,
"累積相対度数2" : 0.8
},
{
"点数" : 50,
"度数" : 1,
"累積度数" : 5,
"相対度数" : 0.2,
"累積相対度数" : 1,
"累積相対度数2" : 1
}
]
}