この記事では、Python の標準ライブラリと NumPy で使用できる、一様乱数(どの値も等しい確率で得られる乱数)を生成する関数について解説します。
疑似乱数生成器
Python 標準ライブラリの random モジュールには様々な種類の 疑似乱数 を生成する関数が用意されています。疑似乱数は一定の周期をもちますが、この周期が長いほど高品質の疑似乱数であることを意味します。Pythonではメルセンヌツイスタ (Mersenne twister) とよばれる周期 2**19937-1 の生成器(ジェネレータ)を使用して疑似乱数を作りだしていきます。
たとえば、整数の乱数が必要な時は randrange() または randint() を使います。randrange() の引数の指定の仕方は Python エンジニアにはお馴染みの range() 関数とよく似ています。range(10) が 0 ~ 9 の整数を格納したシーケンスを返すように、randrange(10) は 0 ~ 9 の整数の中から無作為に一つの数を選んで返します。
# PYTHON_RANDOM
# In[1]
# randomモジュールをインポート
import random
# 乱数シードを固定
random.seed(0)
# 0~9の乱数を生成
val = random.randrange(10)
print(val)
6
記事の後半で詳しく解説しますが、上のコードでは random.seed() で乱数の種を固定して、皆さんが上のコードをコピペして実行したときに同じ結果になるようにしています。この一文を削除すると、コードを実行するたびに異なる数値が返ります。
複数の乱数をリストに格納したいときは、空のリストを用意して、append() メソッドで乱数を順次追加します。
# In[2]
random.seed(0)
rand_list = []
# 0~9の乱数リストを生成
for i in range(10):
val = random.randrange(10)
rand_list.append(val)
print(rand_list)
[6, 6, 0, 4, 8, 7, 6, 4, 7, 5]
実は randrange() に引き数を一つだけ渡した時は第1引数を省略して第2引数で上限値のみ指定していることになります。このあたりも range() と同じですね。第1引数を明示すれば下限値を設定することもできます。たとえば、サイコロのように 1 ~ 6 の乱数を得たいときは randrange(1, 7) と書きます。
# In[3]
random.seed(3)
dice_list = []
# サイコロを10回振った時の目を記録する
for i in range(10):
val = random.randrange(1, 7)
dice_list.append(val)
print(dice_list)
[2, 5, 5, 2, 3, 5, 4, 6, 5, 1]
randrange() の第3引数は step です。たとえば、randrange(1, 100, 2) は 1 から 2 step 間隔で並んだ整数リスト [1, 3, 5, 7, 9, ... 97, 99] から無作為に一つの要素を抜き出します(つまり奇数の乱数です)。
# In[4]
random.seed(0)
odd_rand_list = []
# 1~99のランダムな奇数のリストを生成
for i in range(10):
val = random.randrange(1, 100, 2)
odd_rand_list.append(val)
print(odd_rand_list)
[49, 97, 53, 5, 33, 65, 63, 51, 39, 61]
0.0 ~ 1.0 の浮動小数点数型乱数が欲しい場合は、random() 関数を用います。
# In[5]
random.seed(0)
# float型の乱数を生成
val = random.random()
print(val)
0.8444218515250481
乱数の上限値と下限値を指定したい場合は、random.uniform() を使います。
# In[6]
random.seed(0)
rand_list = []
# 100~200のfloat型乱数リスト
for i in range(5):
val = random.uniform(100, 200)
rand_list.append(round(val, 2))
print(rand_list)
[184.44, 175.8, 142.06, 125.89, 151.13]
メモリを節約したいならば、ジェネレータ式で乱数ジェネレータを作成しておくのも一つのテクニックです。
# In[7]
random.seed(0)
# 乱数ジェネレータを生成
rand_gen = (random.randrange(10) for x in range(5))
取り出された乱数と、乱数の平方数を出力してみましょう。
# In[8]
for x in rand_gen:
val = (x, x**2)
print(val)
(6, 36) (6, 36) (0, 0) (4, 16) (8, 64)
外部ライブラリの NumPy をインポートすれば、乱数配列を効率的に作成できます。NumPy の乱数生成関数は numpy.random モジュールにまとめられています。関数名や使い方は標準ライブラリの random モジュールとほとんど同じですが、整数型乱数を生成する numpy.random.randint() の引数の指定方法 random.randint() ではなく、random.randrange() と同じです。numpy.random.randint(a, b) の戻り値の上限値は a - 1 であることに注意してください。たとえば、numpy.random.randint(1, 10, 15) は下限値 1, 上限値 9 の乱数が 15 個格納された配列を返します。
# In[9]
import numpy as np
# seedを固定
np.random.seed(1)
# 整数乱数の配列を生成
# 乱数の下限値は1,上限値9,配列の要素数は15
rand_list = np.random.randint(1, 10, 15)
print(rand_list)
[6 9 6 1 1 2 8 7 3 5 6 3 5 3 5]
random.random()
random.random() は 0.0 以上、1 未満の 浮動小数点数型の一様乱数を生成します。
# PYTHON_RANDOM_RANDOM
# In[1]
import random
random.seed(0)
# 0以上1未満のfloat型乱数を生成
val = random.random()
print(val)
0.8444218515250481
random.uniform(a, b)
random.uniform(a, b) は a ≦ b のときは、a ≦ N ≦ b の範囲で浮動小数点数型の一様乱数 N を返します。
# PYTHON_RANDOM_UNIFORM
# In[1]
import random
random.seed(0)
rand_list = []
# 15以上30未満のfloat型乱数リスト
for i in range(5):
val = random.uniform(15, 30)
rand_list.append(round(val, 3))
print(rand_list)
[27.666, 26.369, 21.309, 18.884, 22.669]
a > b のときは、b ≦ N ≦ a の範囲で浮動小数点数型の乱数 N を返します。
# In[2]
import random
random.seed(0)
rand_list = []
# 100以上50未満のfloat型乱数リスト
for i in range(5):
val = random.uniform(100, 50)
rand_list.append(round(val, 3))
print(rand_list)
[57.779, 62.102, 78.971, 87.054, 74.436]
random.randrange(start, stop [, step])
random.randrange(start, stop [, step]) は start から stop-1 まで step 刻みで並んだ数字の中から1つをランダムに選んで返します。たとえば
と記述した場合、1 から 15 まで 2 刻みで並んだ数字
の中から1つを無作為に選んで x に格納します。実際には、この関数は range(start, stop, step) で生成された数字のシーケンスから1つの要素を抜き出すという処理を行なっています。
# PYTHON_RANDOM_RANDRANGE
# In[1]
import random
random.seed(5)
rand_list = []
# [1,3,5,7,9,11,13,15]から無作為に5個選ぶ
for i in range(5):
val = random.randrange(1, 16, 2)
rand_list.append(val)
print(rand_list)
[9, 11, 1, 15, 7]
キーワード引数 step が省略されると、step=1 が設定されたことになり、連番から無作為に数値が選ばれます。
# In[2]
import random
random.seed(0)
rand_list = []
# [1,2,3,4,5]から無作為に5個選ぶ
for i in range(5):
val = random.randrange(1, 6)
rand_list.append(val)
print(rand_list)
[4, 4, 1, 3, 5]
random.randrange() に一つだけ引数を渡して実行する場合、下限値ではなく上限値が指定していることに注意してください。このとき、第1引数の下限値は 0、キーワード引数 step は step=1 が指定されたことになります。
# In[3]
import random
random.seed(0)
rand_list = []
# [0,1,2,3,4]から無作為に5個選ぶ
for i in range(5):
val = random.randrange(6)
rand_list.append(val)
print(rand_list)
[3, 3, 0, 2, 4]
random.randint(a, b)
random.randint(a, b) は、a ≦ N ≦ b の範囲で無作為に選ばれた整数 N を返します。この関数は randrange(a, b+1) のエイリアスです。すなわち、実際にはこの関数の内部で randrange() に引数 a と b+1 を渡して呼び出すという処理を行なっています。引数の指定の仕方が randrange() とは異なっていることに注意してください。
# PYTHON_RANDOM_RANDINT
# In[1]
import random
# 空白のリストを用意
my_list = []
# 1~100の乱数を5個生成してリストの要素に追加
for k in range(5):
x = random.randint(1, 100)
my_list.append(x)
print(my_list)
[38, 44, 98, 83, 34]
ジェネレータの初期化と seed の固定
randomモジュールのジェネレータ(メルセンヌツイスタ)は seed とよばれる初期値をもとに疑似乱数列を生成しています。seed は「種」という意味の英語です(数字を次々と生み出すのでこのようによばれています)。seed値が与えられると疑似乱数列は一意に定まります。random.seed(a) を使うと、seed 値を固定してジェネレータを初期化することができます。具体例を下のサンプルコードで説明します。
# PYTHON_RANDOM_SEED
# In[1]
import random
# 疑似乱数ジェネレータを初期化して乱数シードを固定
random.seed(1)
# 1~10の乱数を3個生成
for k in range(3):
x = random.random()
print(x)
# 1行開ける
print("")
# 疑似乱数ジェネレータを初期化して乱数シードを固定
random.seed(1)
# 1~10の乱数を3個生成
for k in range(3):
x = random.random()
print(x)
0.13436424411240122 0.8474337369372327 0.763774618976614 0.13436424411240122 0.8474337369372327 0.763774618976614
最初の random.seed(1) という記述によって、次の for文では seed を 1 として
0.847433736937232
0.763774618976614
というように乱数を順に生成(ジェネレート)しています。そのあとに、もう一度 random.seed(1) と記述することによって、ジェネレータが初期化され、次の for文で再び seed = 1 の乱数列の 1 番目から
0.847433736937232
0.763774618976614
というように数字が並びます。random.seed(a) の引数 a が省略された場合には、seed として現在のシステム時刻が指定されます。
いくつかの乱数をまとめて作りたい場合、先ほどの randint() のサンプルコードのように for 文を使っても可能ですが、リスト内包表記 (comprehention) を使うと、たった1行で乱数リストをつくることができます。
# PYTHON_RANDOM_COMPREHENTION
# In[1]
import random
# 0から9までの整数乱数のリストを作成
num_list = [random.randrange(10) for i in range(10)]
print(num_list)
[5, 8, 5, 0, 3, 9, 8, 8, 2, 9]
numpy.random.rand()
numpy.random.rand() は 0 以上 1 未満の一様乱数を要素にもつ配列を返します。
引数には配列の形を指定します。たとえば、2, 3 を渡すと、2行3列の乱数配列を返します。
# NUMPY_RANDOM_RAND
# In[1]
import numpy as np
# seedを設定
np.random.seed(10)
# 2行3列の乱数配列
x = np.random.rand(2, 3)
print(x)
[[0.77132064 0.02075195 0.63364823] [0.74880388 0.49850701 0.22479665]]
numpy.random.uniform()
numpy.random.uniform() は指定した範囲内の一様乱数を要素にもつ配列を返します。
引数の low は乱数の下限値、high は乱数の上限値です。
size で戻り値の配列の形状を指定することもできます。
# NUMPY_RANDOM_UNIFORM
# In[1]
import numpy as np
# seedを設定
np.random.seed(0)
# 1~5のfloat型乱数を2×2サイズの配列に格納
x = np.random.uniform(1, 6, (2, 2))
print(x)
[[3.74406752 4.57594683] [4.01381688 3.72441591]]
numpy.random.randint()
numpy.random.randint() は指定範囲内で整数型の一様乱数配列を返します。
引数の low は乱数の下限値、high は乱数の上限値です。
size で戻り値の配列の形状を指定することもできます。
# PYTHON_RANDOM_RANDINT
# In[1]
import numpy as np
# seedを設定
np.random.seed(1)
# 1~9の一様整数乱数を要素にもつ3×3配列を生成
x = np.random.randint(1, 10, (3, 3))
print(x)
[[6 9 6] [1 1 2] [8 7 3]]
random_integers は非推奨です
Jupyter Notebook でコードを書いてnumpy モジュールの random_integers という関数を呼び出そうと思ったら、
Please call randint(1, 100 + 1) instead.
という警告メッセージが表示されました。日本語に訳しておきます:
この関数は廃止されます(非推奨です)。
代わりに randint(1, 100 + 1) を呼び出してください。
警告文が表示されるだけで、まだ使えることは使えるので、Python 本体 (現在の最新版は 3.6.5) から消えたわけではないようです。しかし、こんなに嫌われるには何か理由があるのだろうと思案してみると ...... 確かに、この関数が非推奨になるのも当然かもしれないと思えてきました。
random_integers はランダムな整数を呼びだす関数です。
たとえば "random_integers(100)" と記述すると、1 から 100 までの乱数を生成します。
しかし、上の忠告にもある通り、randint() 関数があるので、わざわざこんな関数を作る必要もないはずです。「似たような機能は極力重複しないようにする」という Python の哲学にも明らかに反しています。
そもそも、random_integers() は引数の指定の仕方が不自然です。Range() 関数は Range(100) と書けば 0 から 99 の数字を返し、リストやタプルなどのイテラブルオブジェクトの要素のインデックスのつけ方も同様です。だから他の人が random_integers() を使ったコードを読むと、いらぬ誤解を与えてしまう可能性もあるわけです。う~む。これはいけませんね(← 使おうとしたくせに)。結論としては非推奨な関数は使わないほうが良いということです(← そんなことは誰でもわかる)。
それにしても最近のエディタは親切ですね。
「これを使ってはいけないよ」
と警告するだけでなく、
「代わりにこの関数を使おうね」
とアドバイスまでくれるのですから。私がプログラミングを学び始めた頃 (つまり大昔) は、コンピュータというものは、こちらが間違えると無愛想なエラーを返すだけなので、必死にどこが間違っているかを目で追っていました。本当に良い時代になったものです。
コメントを書く