空気ガチャを回しましょう

#はじめに
くだらない中で学んだことが多かったので共有

アイデアの先人
[空気の成分30連ガチャ](https://shindanmaker.com/658862)

空気を構成している要素、どこまで言えますか?
窒素、酸素、二酸化炭素、アルゴン...あとは知りませんでした。

[こちらのサイト](https://galileo15640215.herokuapp.com/air_gacha/)でガチャを回せます。

#空気ガチャ排出率
空気を構成している成分は[空気 - Wikipedia](https://ja.wikipedia.org/wiki/%E7%A9%BA%E6%B0%97#%E6%88%90%E5%88%86)によると

|成分|化学式|体積比 割合（vol%）|
|---|---|---:|
|窒素|N2|78.084|
|酸素|O2|20.9476|
|アルゴン|Ar|0.934|
|二酸化炭素|CO2|0.0390|
|ネオン|Ne|0.001818|
|ヘリウム|He|0.000524|
|メタン|CH4|0.000181|
|クリプトン|Kr|0.000114|
|二酸化硫黄|SO2|0.0001|
|水素|H2|0.00005|
|一酸化二窒素|N2O|0.000032|
|キセノン|Xe|0.0000087|
|オゾン|O3|0.000007|
|二酸化窒素|NO2|	0.000002|
|ヨウ素|I2|0.000001|

空気を構成している成分の体積比をガチャの排出率とします。
※Wikipedia内の「表2: 乾燥空気の微量成分」についてはあまりにも微量なので今回は無視します。

#ソースコード
```air_gacha.py
"""
https://ja.wikipedia.org/wiki/%E7%A9%BA%E6%B0%97#%E6%88%90%E5%88%86
表1: 乾燥空気の主な組成（国際標準大気、1975年）
成分	化学式	体積比 割合（vol%）	ppm	ppb	備考
窒素	N2	78.084	780,840	-	[12]
酸素	O2	20.9476	209,476	-	[12]
アルゴン	Ar	0.934	9,340	-	[12]
二酸化炭素	CO2	0.0390	390	-	+*2011年の値[13][12][注 2]
ネオン	Ne	0.001818	18.18	-	[12]
ヘリウム	He	0.000524	5.24	-	[12]
メタン	CH4	0.000181	1.81	1813±2	+2011年の値[13][12][注 3]
クリプトン	Kr	0.000114	1.14	-	[12]
二酸化硫黄	SO2	0.0001>	1>	-	*[12]
水素	H2	0.00005	0.5	-	[12]
一酸化二窒素	N2O	0.000032	0.32	324.2±0.1	+*2011年の値[13][12][注 4]
キセノン	Xe	0.0000087	0.087	87	[12]
オゾン	O3	0.000007>	0.07>	70>	*[注 5][12]
二酸化窒素	NO2	0.000002>	0.02>	20>	*[12]
ヨウ素	I2	0.000001>	0.01>	10>	*[12]
"""
import random
import time
from decimal import Decimal
dic = {"窒素 N2":78.084, "酸素 O2":20.9476, "アルゴン Ar":0.934, "二酸化炭素 CO2":0.0390, "ネオン Ne":0.001818, "ヘリウム He":0.000524, "メタン CH4":0.000181, "クリプトン Kr":0.000114, "二酸化硫黄 SO2":0.0001, "水素 H2":0.00005, "一酸化二窒素 N2O":0.000032, "キセノン Xe":0.0000087, "オゾン O3":0.000007, "二酸化窒素 NO2":0.000002, "ヨウ素 I2":0.000001}
com = ["窒素", "酸素", "アルゴン", "二酸化炭素", "ネオン", "ヘリウム", "メタン", "クリプトン", "二酸化硫黄", "水素", "一酸化二窒素", "キセノン", "オゾン", "二酸化窒素", "ヨウ素"]
che = ["N2", "O2", "Ar", "CO2", "Ne", "He", "CH4", "Kr", "SO2", "H2", "N2O", "Xe", "O3", "NO2", "I2"]
keys = []
for i in dic.keys():
    keys.append(i)
su = 0
for i in dic.keys():
    su += int(Decimal(str(dic[i]))*(10**7))
    dic[i] = int(Decimal(str(dic[i]))*(10**7))
out = [0 for i in range(len(keys))]
que = 10**6
for q in range(que): #10**6は10秒, 10**7は2分かかる
    t = time.perf_counter()
    random.seed(t)
    ra = random.randint(0, su)
    res = 0
    if 0 <= ra < dic[keys[0]]:
        out[0] += 1
    else:
        res += dic[keys[0]]
        for i in range(1, len(keys)):
            """
            if i >= 5:
                print("!!!!!!!!!!!!")
                print(out)
            """
            if res <= ra < res + dic[keys[i]]:
                out[i] += 1
                break
            res += dic[keys[i]]

#半角, 全角による文字の"揺らぎ"を整形するための関数 
import unicodedata
def left(digit, msg):
    for c in msg:
        if unicodedata.east_asian_width(c) in ('F', 'W', 'A'):
            digit -= 2
        else:
            digit -= 1
    return msg + ' '*digit

print("空気ガチャ" + "{:.0e}".format(que) + "(" + str(que) + ")" + "連")
for i in range(len(keys)):
    print(left(12, com[i]), left(3, che[i]), "{:>7}".format(out[i]))

```
#実行結果
`que = 10**6`のとき

```
空気ガチャ1e+06(1000000)連
窒素         N2   780580
酸素         O2   209579
アルゴン     Ar     9406
二酸化炭素   CO2     391
ネオン       Ne       32
ヘリウム     He        5
メタン       CH4       3
クリプトン   Kr        1
二酸化硫黄   SO2       2
水素         H2        1
一酸化二窒素 N2O       0
キセノン     Xe        0
オゾン       O3        0
二酸化窒素   NO2       0
ヨウ素       I2        0
```
`que = 10**7`のとき

```
空気ガチャ1e+07(10000000)連
窒素         N2  7809540
酸素         O2  2092942
アルゴン     Ar    93396
二酸化炭素   CO2    3808
ネオン       Ne      214
ヘリウム     He       54
メタン       CH4      12
クリプトン   Kr       11
二酸化硫黄   SO2       6
水素         H2       10
一酸化二窒素 N2O       6
キセノン     Xe        1
オゾン       O3        0
二酸化窒素   NO2       0
ヨウ素       I2        0
```
#注意ポイント
####浮動小数点数の誤差
float型の扱いには注意しましょう。
コンピュータの内の2進数の世界では小数を扱うとき、正確な値を扱うことができません。

```.py
print(0.1 + 0.2)
#実行結果: 0.30000000000000004
```
どこから現れた0.00000000000000004!!!!!!!!
そこでPythonで小数点以下を扱うときは`Decimal()`を使うとよいでしょう。
[15. 浮動小数点演算、その問題と制限 — Python 3.8.3 ドキュメント](https://docs.python.org/ja/3/tutorial/floatingpoint.html)

```.py
from decimal import Decimal
print(Decimal("0.1") + Decimal("0.2"))
#実行結果: 0.3
```
コード内で

```.py
for i in dic.keys():
    su += int(Decimal(str(dic[i]))*(10**7))
    dic[i] = int(Decimal(str(dic[i]))*(10**7))
```
としている部分が誤差の回避に該当しています。
10**7を掛けて排出率を整数で扱おうとしているときに、計算が行われているので浮動小数点型だと誤差が発生してしまいますね。

####ランダムの作成
[time.time()は精度があまりよくない？ - Qiita]
(https://qiita.com/takeopy/items/170d0e1ddbf02ef9fbb9)
for文が回るのが早すぎるので`time.time()`では同じ時刻を生成してしまい、シード値が同じになる可能性が高いです(Windows環境では実際そうだった)。
そこで精度の高い`time.perf_counter()`を使います。

```.py
t = time.perf_counter()
random.seed(t)
ra = random.randint(0, su)
```

####半角, 全角による文字の"揺らぎ"を整形
[Pythonのunicodedataライブラリを使って、全角文字も半角文字も揃えて表示する - Qiita](https://qiita.com/delicious-locomoco/items/1adfe3ede4247b72759d)
見やすい出力を心がけましょう。

```.py
for i in range(len(keys)):
    print(left(12, com[i]), left(3, che[i]), "{:>7}".format(out[i]))
```

#感想
確率は偏る。