3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

MongoDB $bucket,$facetを使って度数分布のデータを作る

Posted at

概要

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にしています。
  • defaultboundariesに含まれないデータが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
		}
	]
}
3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?