NumPy
前回、NumPyで多次元配列をソートしてそのインデックスを返すargsortを紹介しました。

今回はNumPyを使って重複のないランダムな数値のリストを作成する方法を紹介します。
通常、NumPyの乱数ジェネレータを使ってランダムな数値を取得すると、どうしても重複が避けられません。
import numpy as np
rng = np.random.default_rng()
data = rng.integers(0, 10, 5)
print(data)
実行結果
[3 0 1 1 0]
ということで何とか重複しない様にランダムな数値のリストを作成するプログラムを組んでみたと言うのが今回のお話です。
それでは始めていきましょう。
1.ランダムにピックし、リストにないものを追加
まず試したのが、ランダムに数値をピックし、リストに加えていきます。
ただしリストに追加する際、リストに存在しない数値だった場合だけ追加すると言う方法です。
自作関数を作成しますが、「int_nooverlap(start, end, samples)」として、取得する数値のリストの範囲を「start」と「end」で指定し、取得する数値の数を「samples」として指定しています。
import numpy as np
import sys
def int_nooverlap(start, end, samples):
if end - start < samples:
print("Sample number should be less than numbers from start to end.")
sys.exit(1)
else:
rng = np.random.default_rng()
data = []
while len(data) < samples:
pick_data = rng.integers(start, end)
if not pick_data in data:
data.append(pick_data)
return data
data = int_nooverlap(0, 10, 10)
print(data)
作成した関数の最初の3行は準備する数値の数よりも、取得する数の方が多い場合、必ず重複がでてしまうので、それを避けるためのエラー処理です。
最後に「sys.exit(1)」でエラーとしてプログラムを終了しています。
if end - start < samples:
print("Sample number should be less than numbers from start to end.")
sys.exit(1)
ちなみに「sys.exit()」に関してはこちらの記事で紹介していますので、良かったらどうぞ。

「else:」以降が重複を含まない数値のリストを作成する部分です。
rng = np.random.default_rng()
data = []
while len(data) < samples:
pick_data = rng.integers(start, end)
if not pick_data in data:
data.append(pick_data)
return data
ここではwhile文を使って、何度も乱数ジェネレータから数値を取得し、リストdataにない場合は追加することを繰り返します。
そしてリストdata の中に保存された数値の数が変数samplesに達した場合、while文を終了し、リストdata を返すとしています。
2.数値のリストを作成し、そこから一つずつランダムに取得
次にまずは数値の範囲からリストを作成し、そこから一つずつランダムに取得するという方法を試しました。
import numpy as np
import sys
def int_nooverlap(start, end, samples):
if end - start < samples:
print("Sample number should be less than numbers from start to end.")
sys.exit(1)
else:
rng = np.random.default_rng()
initial_data = list(range(start, end))
data = []
while len(data) < samples:
pick_index = rng.integers(len(initial_data))
data.append(initial_data[pick_index])
del initial_data[pick_index]
return data
data = int_nooverlap(0, 10, 10)
print(data)
先ほどのプログラムとは「else:」以降が異なっています。
まず「range関数」を使って、指定された範囲の数値のリストを作成しています。
initial_data = list(range(start, end))
そしてランダムにそのリストのインデックスを取得し、そのインデックスを使って値を別のリストdataに格納します。
格納後は元の数値のリストinitial_dataから削除します。
それをリストdataに格納された要素数が変数samplesに達するまで繰り返します。
while len(data) < samples:
pick_index = rng.integers(len(initial_data))
data.append(initial_data[pick_index])
del initial_data[pick_index]
どちらが速いか?
せっかく2つ作成したので、どちらが速いかマジックコマンド「%%timeit」を使って試してみました。
それぞれランダムな数値のリストとしてn個の数値のリストからn個取得する様にしました。
10 | 100 | 1000 | 10000 | |
1 | 116 us | 1.81 ms | 85.8 ms | 10.6 s |
2 | 67 us | 0.298 ms | 2.91 ms | 34.4 ms |
見てわかる様に2番の「数値のリストを作成し、そこから一つずつランダムに取得」の方が圧倒的に速いと言う結果になりました。
それは少し考えてみたら分かったのですが、1番のプログラムではピックする値が最後の方になるにつれ、すでにピックした数値と被ってしまう可能性が高くなり、何度もピックし直すことになります。
特に元のリストの数が増えれば増えるほどピックし直す確率が上がるので、時間が指数関数的に増加してしまいます。
2番のプログラムでは一度ピックした値は2度とピックされないので、単純にピックする数値の数に従って増えていくだけです。
それならばとn個の数値のリストからn/10個、つまり10分の1個の数値を取得する際にかかる時間を測定してみました。
100個から10個 | 1000個から100個 | 10000個から1000個 | 100000個から10000個 | |
1 | 62.6 us | 397 us | 12.6 ms | 1 s |
2 | 67.8 us | 327 us | 4.32 ms | 157 ms |
こちらでも1番の「ランダムにピックし、リストにないものを追加」の方が取得する個数が増えるにつれ、処理に時間が伸びるという結果になりました。
こちらの場合は最高でも10回に1回重複するだけで、それほど違いはでないと思ったのですが、取得する数値の数が増えるにつれ、重複する回数が多くなり、結果、処理時間が長くなるのだと考えられます。
ということで結果としては2番の「数値のリストを作成し、そこから一つずつランダムに取得」の方が圧倒的に処理時間が短くなるのでオススメです。
また今回の様にプログラムによって処理時間が大きく異なることも多々あるということを頭の片隅に置いてプログラミングしたいなと思った件でした。
次回は前々回に紹介したargsortでソートの方法を変えた場合の処理時間を検討してみたので、その結果を紹介します。
ではでは今回はこんな感じで。
コメント