【Python】6ケタの英数字が5ケタになってしまう問題をシミュレート

## 概要
職場でこんなトラブルに遭遇しました。
「英数字6ケタのパスワードが、5ケタで生成されており、エラーになった」

Pythonを使って、このエラーを再現してみました。

## 原因

「英数字6ケタ」で大量にランダム生成されたパスワード文字列のうち、ひとつだけ「数字のみ5ケタ」になっていました。
"012345"のように、先頭が0・残りが数字で生成されたため、Excelファイルでやりとりする際に先頭のゼロが省略されて"12345"になったようです。
(なんでmkpasswdのオプションで指定しないの…)

## 発生確率

この事象が起きる確率を計算してみます。

まず、ある1ケタの取りうる値は、[0-9, a-z]なので、10+26＝36種類です。
つまり、0になる確率は、1/36。
一方、数字0-9になる確率は、10/36です。

今回の事象は、先頭1ケタが0・残りの5ケタが数字になる時なので、
(1/36) * (10/36) * (10/36) * (10/36) * (10/36) * (10/36)
= 0.0000459393658… となります。 

パーセントにすると、0.0046％。
メガバンクの普通預金金利＝0.001％の4倍強ですね。
(パーセントの意味合いが異なりますが)
分数にすると、1/21767.8になります。

## 再現

このパスワード生成をPythonで再現してみます。
事象が発生するまでパスワード生成を繰り返して、
何回目で発生したかを記録したり、複数回発生させた平均値を出したりしてみます。

## コード

```py
import re
import random
from statistics import mean

#可変値
turns = 10
passwd_len = 6

#エラー率の計算（％）
err =  100 * (1 / 36) * (10 / 36) ** (passwd_len - 1)

'''
#ループを使って数字と小文字のリストを作成
#→固定値にしたのでコメントアウト
char_list = []

#数字
for j in range(10):
    char_list.append(str(j))

#小文字アルファベット
for i in range(97, 123):
    char_list.append(chr(i))
'''

#固定値。数字と小文字のリスト
char_list = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',]

#エラーになった時の試行回数を格納するリスト
challenge_count = []

print('文字列の長さ：' + str(passwd_len))
print(f'エラー率：{err:.8f}%')
print(str(round(100 / err, 1)) + '回に1回はエラーになる計算')
print('--------')

for turn in range(turns):
    i = 1
    while True:
        passwd = ''.join(random.choices(char_list, k=passwd_len))
        if passwd[0] == '0' and len(re.findall('[0-9]', passwd)) == passwd_len:
            print(passwd, ': ', i, '回目')
            challenge_count.append(i)
            break
        i += 1

print('--------')
print('試行回数 :',turn + 1)
print('最小値 :',min(challenge_count))
print('最大値 :',max(challenge_count))
print('平均値 :',mean(challenge_count))

```

## 実行結果

```py
文字列の長さ：6
エラー率：0.00459394%
21767.8回に1回はエラーになる計算
--------
061109 :  63096 回目
085726 :  17000 回目
075952 :  9555 回目
078113 :  28085 回目
061468 :  17485 回目
042766 :  13637 回目
045780 :  9737 回目
023303 :  11550 回目
092385 :  19823 回目
039489 :  31942 回目
--------
試行回数 : 10
最小値 : 9555
最大値 : 63096
平均値 : 22191
```

## 文字列の長さを増やしてみる（6→8）
```py
文字列の長さ：8
エラー率：0.00035447%
282111.0回に1回はエラーになる計算
--------
09152768 :  743009 回目
01711507 :  66256 回目
09156634 :  65775 回目
09544840 :  892213 回目
09402244 :  497038 回目
05226558 :  43195 回目
00636774 :  682084 回目
09395383 :  7369 回目
07805687 :  564570 回目
02889286 :  442124 回目
--------
試行回数 : 10
最小値 : 7369
最大値 : 892213
平均値 : 400363.3
```

## 試行回数を増やしてみる（10→1000）

※実行結果のprintを一時的にコメントアウト
```py
for turn in range(turns):
    i = 1
    while True:
        passwd = ''.join(random.choices(char_list, k=passwd_len))
        if passwd[0] == '0' and len(re.findall('[0-9]', passwd)) == passwd_len:
            #print(passwd, ': ', i, '回目')
            challenge_count.append(i)
            break
        i += 1
```

平均値が理論上の値に近づいたことと、最小値・最大値のブレが大きくなったことが確認できます。
最小値4ってすごいな。

```py
文字列の長さ：6
エラー率：0.00459394%
21767.8回に1回はエラーになる計算
--------
--------
試行回数 : 1000
最小値 : 4
最大値 : 161107
平均値 : 21707.447
```


## 所感
* パスワードの長さや試行回数を増やすと、処理時間がだいぶ長くなります。処理時間の視える化や、処理を軽くするための修正が必要。
* 宝くじやガチャのシミュレータなんかがよく公開されていますが、今回のプログラムを応用して作れそうです。
* 結果をグラフで表示して分析とかできればいいんですが、Pythonの知識以上に数学・統計の知識が求められそうです。
* 0.0046%の確率でも、何度も繰り返せばエラーは必ず起きます。そうならないように、mkpasswdのオプションを指定しておきましょう。
```
mkpasswd -l 6 -d 2 -C 0 -s 0
```