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

# 概要
Aggregation Pipeline Stageの中でも、今まで全然使う機会がなかった\$bucket, \$facetを度数分布データを作ることで試しました。 結構勉強になりました。最後**Appendix**には実際はアプリケーションで行ったほうが楽だと思う処理を無理してAggregationで実装してみました。

# 環境

- Windows 10
- MongoDB 4.2.1 

# 実行

## テストデータ

まずは簡単なデータを用意します。`_id`は学生番号と思ってください。

```javascript
db.score.insert([
  {_id:1,成績:{数学:10,英語:90}},
  {_id:2,成績:{数学:16,英語:26}},
  {_id:3,成績:{数学:80,英語:20}},
  {_id:4,成績:{数学:84,英語:94}},
  {_id:5,成績:{数学:85,英語:50}},
])
```

## $bucketのテスト

```javascript
db.score.aggregate([
  {$bucket:{
        groupBy:"$成績.数学",
        boundaries: [0,10,20,30,40,50,60,70,80,90,101],
        default: "other",
        output:{
            数学度数:{$sum:1},
        }
  }}
])
```

```javascript
{ "_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のテスト

まずは簡単なテスト

```javascript
db.score.aggregate([
  {$facet:{
     lt20:[
       {$match:{"成績.数学":{$lt:20}}},
       {$project:{数学:"$成績.数学"}}  
     ],
    gte20:[
       {$match:{"成績.数学":{$gte:20}}},
       {$project:{数学:"$成績.数学"}}  
    ]
  }}
]).pretty()
```

```javascript
{
	"lt20" : [
		{
			"_id" : 1,
			"数学" : 10
		},
		{
			"_id" : 2,
			"数学" : 16
		}
	],
	"gte20" : [
		{
			"_id" : 3,
			"数学" : 80
		},
		{
			"_id" : 4,
			"数学" : 84
		},
		{
			"_id" : 5,
			"数学" : 85
		}
	]
}
```

**解説**

`$facet`で出力フィールドごとにPipelineが使えて、入力は全ての出力フィールドごとに同じである。また、`$facet`の出力ドキュメントは１件であり、各Pipelineの出力結果は配列として出力される。１ドキュメントサイズの16MB制限に注意が必要である。

上記の例では同じ`$project`を使っているので、次のようにも書ける。

```javascript
db.score.aggregate([
  {$project:{数学:"$成績.数学"}},
  {$facet:{
     lt20:[
       {$match:{数学:{$lt:20}}}
     ],
    gte20:[
       {$match:{数学:{$gte:20}}},
    ]
  }}
]).pretty()
```
結果はご自分で確かめてください。

## 度数分布データを作る

```javascript
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()
```

```javascript
{
	"数学" : [
		{
			"_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番目を取り出しています。 

### 相対度数の計算を追加

```javascript
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()
```

```javascript
{
	"全体" : {
		"学生数" : 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を実行してみてください。

```javascript
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

以下の処理については実際はアプリケーションで処理したほうが楽だと思います。
テストデータは最初に使った少量データを使っています。

## 度数分布を点数順、度数順の二つを作る
英語のみ度数順を追加します。

```javascript
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()
```


```javascript
{
	"全体" : {
		"学生数" : 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
		}
	]
}
```

## 累積度数、累積相対度数

プログラミングではないので値を一時的に貯めておくことが出来ないので累積計算は基本的にはできないので、結構面倒な処理になってしまいました。

```javascript
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()
```

```javascript
{
	"全体" : {
		"学生数" : 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
		}
	]
}
```