Go言語製 抽選Botの精度が怪しいので検証してみた話

# いきさつ
ACALL株式会社では、slackで抽選をするbotがあって、会議のファシリテーターとか
誰か担当を決めるときにそのbotを使って決めています。

Go + AWS LambdaでSlackの社員抽選botを作った話
https://blog.acall.jp/2019/11/develop-slack-lottery-bot/

こんな感じで当選するのですが、
<img width="738" alt="8c967525c648c218ea742cd5bfa515e8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23578/14a3b9c4-0c73-a596-2e5c-d9be2dc6a746.png">


このbotの精度がどうも怪しい。
私と弊社CTOがやたら当選する気がする！

<img width="755" alt="d34c8bf56910f5a3db67231b5117a886.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23578/7b4033ed-974a-98a7-56f3-fcfc0c012b0c.png">

最初は「まあプログラムのランダム性ってそんなもんだよね。ハハハ」なんて言ってたもんですが、
なかなかの頻度で当選するので、これはもしやバグが寝てるのでは・・・・と思い、精度を検証してみました。

# 前提
* goのバージョンは1.14rc1を使用

# 検証内容
### 抽出処理の確認
まず、抽選botのランダム処理は下記のようになっています。

```go
func lotteryOneUserFromUsers(userIDs []string) string {
	rand.Seed(time.Now().UnixNano())
	userID := userIDs[rand.Intn(len(userIDs))]
	return userID
}
```

userIDsには抽選対象のユーザーが入っているわけですが、この処理自体はシンプルで怪しいところはない。
というわけで、この処理を複数回実行して確率を算出します。

### 確認用コード
下記のようなコードを用意しました。

```go:probability.go
package main

import (
  "math/rand"
  "time"
  "fmt"
  "strconv"
)

func main() {
  userIDs := []string{"CTO","EM","山田","鈴木","藤田","田中","中島","北川","西山","川口","遠藤"}
  var lottery_cnt map[string]int = make(map[string]int)
  const LOOP_CNT int = 1000
  //初期化
  for _, s := range userIDs {
    lottery_cnt[s] = 0
  }

  var userID string
  for i := 0; i < LOOP_CNT; i++ {
    userID = lotteryOneUserFromUsers(userIDs)
    lottery_cnt[userID] += 1
  } 

  fmt.Println("抽選実施回数は" + strconv.Itoa(LOOP_CNT) + "回です")
  fmt.Println("----------------------------")
  sum := 0
  for k, v := range lottery_cnt {
    fmt.Println(k + " : " + strconv.FormatFloat((float64(v)/float64(LOOP_CNT)*100.0), 'f', 1, 64) + "% （" + strconv.Itoa(v) + "回）")
    sum += v
  }
}

// 抽選処理実体
func lotteryOneUserFromUsers(userIDs []string) string {
	rand.Seed(time.Now().UnixNano())
  userID := userIDs[rand.Intn(len(userIDs))]
	return userID
}
```

LOOP_CNTで抽選する回数を設定しています。

### いざ検証
さて、上記のコードを使ってまずは1,000回の抽選で検証してみます。

```terminal
~/d/s/lottery ❯❯❯ go run probability.go
抽選実施回数は1000回です
----------------------------
山田 : 7.5% （75回）
鈴木 : 10.8% （108回）
西山 : 7.3% （73回）
遠藤 : 8.8% （88回）
中島 : 10.0% （100回）
北川 : 9.9% （99回）
川口 : 9.9% （99回）
CTO : 7.8% （78回）
EM : 10.1% （101回）
藤田 : 9.8% （98回）
田中 : 8.1% （81回）
```

ほうほう・・・それなりのゆらぎがあります。
ちなみにEMが私です。
理論上は回数を重ねれば重なるほど確率は均一になるはずなので、回数を増やします。
次は１万回でやってみます。

```terminal
~/d/s/lottery ❯❯❯ go run probability.go
抽選実施回数は10000回です
----------------------------
遠藤 : 8.6% （856回）
EM : 9.4% （936回）
山田 : 9.2% （921回）
鈴木 : 9.1% （914回）
藤田 : 9.1% （908回）
中島 : 9.6% （960回）
川口 : 9.3% （927回）
CTO : 8.9% （889回）
田中 : 9.0% （900回）
北川 : 9.2% （921回）
西山 : 8.7% （868回）
```

前回よりも均一化されてきました。ほぼ９％前後です。
では一気に、5０万件まで増やしてみます。

```terminal
~/d/s/lottery ❯❯❯ go run probability.go
抽選実施回数は500000回です
----------------------------
北川 : 9.1% （45357回）
西山 : 9.1% （45277回）
川口 : 9.1% （45562回）
CTO : 9.0% （45103回）
山田 : 9.2% （45825回）
藤田 : 9.1% （45574回）
田中 : 9.1% （45283回）
EM : 9.2% （45765回）
鈴木 : 9.1% （45417回）
中島 : 9.1% （45630回）
遠藤 : 9.0% （45207回）
```

ほぼゆらぎはなくなりましたね！
5０万回やれば問題ないと言えそうです。

**が！**

会社の抽選BOTで5０万回もやるわけはない。
実際に弊社Slackの抽選bot利用回数を調べると１６３回の抽選が行われていました。
というわけで１６３回で検証してみます。

```terminal
~/d/s/lottery ❯❯❯ go run probability.go
抽選実施回数は163回です
----------------------------
西山 : 9.2% （15回）
川口 : 12.9% （21回）
藤田 : 7.4% （12回）
中島 : 10.4% （17回）
北川 : 10.4% （17回）
鈴木 : 7.4% （12回）
田中 : 9.2% （15回）
遠藤 : 6.1% （10回）
CTO : 7.4% （12回）
EM : 10.4% （17回）
山田 : 9.2% （15回）
```

ふーむ・・・・多い人で１２％。前後2〜３％程度のゆらぎですね。
母数が少ないとけっこうバラけます。

### まとめ
slack上でのbot実行回数の総回数もすくないし、当選確率が均一化されないのは仕方ない。
よく当選する気がするのは気のせいかもしれない。気のせいでしょうきっと。
これからもこのbotは運用されるので、確率が均一化されていくのを期待します！