LoginSignup
2
4

More than 1 year has passed since last update.

【令和最新】NumPyの乱数・シャッフル・ランダム抽出(`np.random`)使い方まとめ

Last updated at Posted at 2022-07-08

NumPyの乱数・シャッフル・ランダム抽出(np.random)使い方まとめ

Numpyでは、2019年にリリースされたバージョン1.17より新たな乱数生成器が実装されました。しかしそれから3年以上経過した記事執筆時点でも、既に非推奨となっている古い仕様(np.random直下にある諸関数の使用)が依然として多くの人に用いられ続けています(なお前後比較・変更点は『numpy.randomのGeneratorをためしてみる - Qiita』を参照してください)。

この要因としては、古い内容のまま放置されているある種の権威を持った記事や(例えば「Numpy ランダム」などでググると古い仕様を紹介した記事『Numpyによる乱数生成まとめ - Qiita』かなり上位に表示されます)、SEO対策だけは頑張る質の悪い泡沫プログラミング入門サイトの記事などが、新しい仕様を新規参入者に教えず、その結果さらに新規参入までもが既に非推奨となっているコードの拡散に協力してしまっているということがあるでしょう(また、多くの人は公式ドキュメントを見ようとはしません)。

本記事では、上記のような古い記事や低質な記事の代替となるような、バージョン1.17以降のNumPyの乱数の基礎的な使い方から一般的に使われる関数のAPI実装および使用例についての情報を提供するものです。

基本的な使い方

np.random.default_rng()で、乱数生成器オブジェクトをインスタンス化したのち、各種メソッドで乱数を発生できます。

import numpy as np

# 乱数生成器オブジェクトの作成
rng = np.random.default_rng()

# 乱数の作成
rfloat = rng.random()
print(rfloat)

以下乱数生成器オブジェクトを変数rngに格納しているものとして説明します。

シード

np.random.default_rng()にはシード値を渡すことができます。

NumPyの乱数生成器は擬似乱数といって、本当のランダムではありません。極端にいえば$y=f(x)$のような関数になっており、初期条件$x$が決まれば結果$y$が決まっていますが、通常$x$には実行したときの現在時刻などが入るために、結果がランダムになっています。

私達がサイコロを振るとき普通は適当に投げるので結果はランダムですが、もし完全に同じ位置から同じ角度・速度で投げたら(それが可能だとしたら)、決まった目が出るというようなイメージをしてください。

シード値を渡すと、同じ位置・角度・速度でサイコロを投げることができます。以下を何度か実行してみてください。

random_seed.py
import numpy as np

# シードを設定しない場合
rng = np.random.default_rng()
print('↓実行するたび値が変わる')
print('1投目: ', rng.integers(1, 7))
print('2投目: ', rng.integers(1, 7))
print('3投目: ', rng.integers(1, 7))

# シードを設定
rng = np.random.default_rng(12345)
print('↓毎回同じ結果(5と2と5)になる')
print('1投目: ', rng.integers(1, 7))
print('2投目: ', rng.integers(1, 7))
print('3投目: ', rng.integers(1, 7))

なお、たまに勘違いする方がいますが、1投目と2投目が同じ結果になるというわけではありません。何度やっても、1投目同士、2投目同士、…、N投目同士が必ず同じ結果になるということです。次のコードでそれを確かめられます。

random_seed2.py
import numpy as np

rng1, rng2 = np.random.default_rng(123), np.random.default_rng(123)

# 以下は無限ループになる
while rng1.integers(1, 7) == rng2.integers(1, 7):
    pass

乱数値・配列を新規作成

ランダム整数値・配列を作成 -.integers()

rng.integers(low, high=None, size=None, dtype=np.int64, endpoint=False)
引数 説明
low 結果が取り得る値の最小値(0のとき省略可能)
high 結果が取り得る値の最大値
size 結果の配列の形。省略するとスカラー値
dtype 結果配列のデータ型。デフォルトはint64
endpoint デフォルトはFalseで右開区間。Trueにすると閉区間
>>> rng = np.random.default_rng(0)
>>> rng.integers(1, 7)
6

>>> rng.integers(1, 7, 10)
array([4, 4, 2, 2, 1, 1, 1, 2, 5, 4], dtype=int64)

>>> rng.integers([10, 20, 30])
array([ 9, 10, 18], dtype=int64)

lowには最小値・highには最大値を設定しますが、ここで、結果が取り得る値はlow以上high未満になることに注意してください(range()関数のstart/stop引数をイメージしてください)。

rng.integers(1, 7)
# 結果は{1, 2, 3, 4, 5, 6}のいずれかで、`7`は出ない

low=0の場合は省略できます。

rng.integers(0, 7)
rng.integers(7)
# どちらも結果は{0, 1, 2, 3, 4, 5, 6}のいずれか

結果が取り得る値をlow以上high以下にするには、endpoint引数をTrueに指定します。

rng.integers(1, 7)
rng.integers(1, 7, endpoint=False)
# どちらも結果は{1, 2, 3, 4, 5, 6}のいずれかで、`7`は出ない

rng.integers(1, 7, endpoint=True)
# 結果は{1, 2, 3, 4, 5, 6, 7}のいずれか

なおlow/highには小数を渡すこともできます。この場合小数点以下は切り捨てられて解釈されます。この使い方は推奨しません。また浮動小数の精度には注意してください。

rng.integers(1, 7.9)
# 結果は{1, 2, 3, 4, 5, 6}のいずれかで、`7`は出ない

rng.integers(5, (0.7+0.1)*10)
# 結果は{1, 2, 3, 4, 5, 6}のいずれかで、`7`は出ない

size引数を省略するかNoneを指定すると(ここまでの例のように)結果はスカラー値になります。
size引数に整数を渡すと、その長さの1次元配列が返却されます。タプルを渡すと2次元以上の配列も作成できます。

rng.integers(1, 7, 10).shape  # -> (10,)
rng.integers(1, 7, (3, 5)).shape  # -> (3, 5)

配列の要素ごとに取りうる値の区間を変更したい場合は、low/high引数をリストにします。

rng.integers([0, 10, 20], [10, 20, 30])
rng.integers([0, 10, 20], [10, 20, 30], size=3)
# どちらも以下にほぼ等しい
np.array([rng.integers(0, 10), rng.integers(10, 20), rng.integers(20, 30)])

lowの長さ/highの長さ/sizeの値が一致していなくても、可能であれば適宜ブロードキャストされます。

rng.integers([0, 10, 20], [10, 20, 30], size=(2, 3))
# 以下にほぼ等しい
np.array([[rng.integers(0, 10), rng.integers(10, 20), rng.integers(20, 30)],
          [rng.integers(0, 10), rng.integers(10, 20), rng.integers(20, 30)]])


rng.integers(low=[0, 10], high=[[20], [30]], size=(3, 2, 2))
# どのような結果になるか考えてみましょう

low/high/sizeの値の組み合わせが適切でない場合はエラーが出ます。

rng.integers([0, 10, 20], [10, 20, 30], size=4)
# raise ValueError

ランダム小数値・配列を作成 -.random()

rng.random(size=None, dtype=np.float64, out=None)
引数 説明
size 結果の配列の形。省略するとスカラー値
dtype 結果配列のデータ型。デフォルトはfloat64
out 既存の配列に要素を代入する場合に使用
>>> rng = np.random.default_rng(0)
>>> rng.random()
0.6369616873214543

>>> rng.random(3)
array([0.26978671, 0.04097352, 0.01652764])

>>> rng.random((2, 5))
array([[0.81327024, 0.91275558, 0.60663578, 0.72949656, 0.54362499],
       [0.93507242, 0.81585355, 0.0027385 , 0.85740428, 0.03358558]])

.integers()と同様に、size引数を省略するかNoneを指定すると結果はスカラー値になり、size引数に整数を渡すとその長さの1次元配列が返却されます。タプルを渡すと2次元以上の配列も作成できます。

.integers()と異なりlow/high引数は存在せず、結果が取り得る値は必ず0以上1未満になります。
最小値lowと最大値highを指定したい場合は、変換式$(high - low) * random() + low$を適用します。

low, high = 1, 7
arr = rng.random(10)

(high - low) * arr + low
# -> 1以上7未満の値を10個並べた配列

ランダムなバイト文字列を作成 -.bytes()

rng.bytes(length)

lengthには文字列の長さを指定します。

>>> rng = np.random.default_rng(0)
>>> rng.bytes(0)
b''

>>> rng.bytes(1)
b'\xcf'

>>> rng.bytes(10)
b'!\xd7\xd9\x82\xf8\xbd\x10E\xb8\xe8'

正規分布に基づいて乱数値・配列を作成 -.normal()

rng.normal(loc=0.0, scale=1.0, size=None)
引数 説明
loc 平均値$μ$
scale 標準偏差$σ$
size 結果の配列の形。省略するとスカラー値

loc/scaleをともに省略すると標準正規分布に基づくようになります。
標準正規分布の場合は.standard_normal()を用いることもできます。

import matplotlib.pyplot as plt

arr = rng.normal(size=100000)
plt.hist(arr, bins=100)

その他の確率分布に基づいて乱数値・配列を作成

二項分布
rng.binomial(n, p, size=None)  # {n: 試行回数, p: 発生確率}

# レア封入率3%の100連ガチャを10万人が引いたときの、各プレイヤーが引いたレアの枚数
arr = rng.binomial(100, 0.03, 100000)
(arr == 0).sum()  # -> 約4700人が一枚も引けなかった
ポアソン分布
rng.poisson(lam=1.0, size=None)  # {lam: 平均発生回数λ}
カイ二乗分布
rng.chisquare(df, size=None)  # {df: 自由度}

既存の配列をシャッフル・ランダム並び替え -.shuffle()/.permutation()

rng.shuffle(x, axis=0)
rng.permutation(x, axis=0)
引数 説明
x シャッフル元の配列
axis aが多次元配列の場合にシャッフルする軸

-.shuffle()は既存の配列をシャッフルします。引数として与えた配列xを変更します
-.permutation()は引数として与えた配列xを並び替えた新しい配列を返します

>>> rng = np.random.default_rng(0)
>>> arr = np.array(['a', 'b', 'c', 'd', 'e', 'f', 'g'])

>>> rng.permutation(arr)
array(['c', 'e', 'd', 'g', 'f', 'a', 'b'], dtype='<U1')
>>> arr
array(['a', 'b', 'c', 'd', 'e', 'f', 'g'], dtype='<U1')

>>> rng.shuffle(arr)
>>> arr
array(['f', 'c', 'e', 'g', 'b', 'a', 'd'], dtype='<U1')

axis引数はシャッフルする軸を指定します。

>>> rng = np.random.default_rng(0)
>>> arr = np.array([[11, 12, 13],
...                 [21, 22, 23],
...                 [31, 32, 33]])

# 行(axis=0)並び替え
>>> rng.permutation(arr, axis=0)
array([[31, 32, 33],
       [11, 12, 13],
       [21, 22, 23]])

# 列(axis=1)並び替え
>>> rng.permutation(arr, axis=1)
array([[13, 12, 11],
       [23, 22, 21],
       [33, 32, 31]])

多次元配列の各要素をシャッフル・ランダム並び替え -.permuted()

rng.permuted(x, axis=None, out=None)
引数 説明
x シャッフル元の配列
axis シャッフルする軸
out 既存の配列に要素を代入する場合に使用
  • 前項の.permutation()は多次元配列の軸を並び替えます。
    -.permuted()は多次元配列の軸内の要素をシャッフルします。
>>> rng = np.random.default_rng(0)
>>> arr = np.array([[11, 12, 13, 14, 15],
...                 [21, 22, 23, 24, 25],
...                 [31, 32, 33, 34, 35]])

#`.permutation()`は列`[[13], [23], [33]]`が維持されている
>>> rng.permutation(arr, axis=1)
array([[13, 15, 14, 11, 12],
       [23, 25, 24, 21, 22],
       [33, 35, 34, 31, 32]])

#`.permuted()`は各行の要素をそれぞれシャッフルする
>>> rng.permutation(arr, axis=1)
array([[15, 12, 13, 11, 14],
       [21, 23, 24, 25, 22],
       [34, 33, 31, 32, 35]])

ランダムサンプリング抽出・ガチャ・くじ引き -.choice()

rng.choice(a, size=None, replace=True, p=None, axis=0, shuffle=True)
引数 説明
a 配列(母集団)
size 結果の配列の形。省略するとスカラー値
replace デフォルトはTrueで重複あり。Falseにすると重複なし
p aに対応する離散確率分布
axis aが多次元配列の場合にサンプリングする軸
shuffle 組み合わせが欲しい(結果の順番を気にしない)場合はFalse

配列aから任意の数sizeだけ要素を取り出します。

>>> rng = np.random.default_rng(0)
>>> arr = np.array(['a', 'b', 'c', 'd', 'e', 'f', 'g'])

>>> rng.choice(arr, 3)
array(['f', 'e', 'd'], dtype='<U1')

>>> rng.choice(arr, 10)
array(['b', 'c', 'a', 'a', 'a', 'b', 'f', 'e', 'g', 'd'], dtype='<U1')

引数pに確率分布の配列を与えると、aから抽出される確率に重みをつけることができます。
なおpの長さがaと同じでない場合や、pの総和が1.0(全確率)でない場合はエラーが出ます。

>>> rng = np.random.default_rng(0)
>>> arr = np.array(['N', 'R', 'SR', 'SSR'])

>>> s = rng.choice(arr, 10000, p=[0.5, 0.3, 0.15, 0.05])
>>> np.unique(s, return_counts=True)
(array(['N', 'R', 'SR', 'SSR'], dtype='<U3'),
 array([4990, 2989, 1523,  498], dtype=int64))

デフォルトのreplace=Trueでは、重複ありでサンプリングします(いわゆる『袋からボールを取り出した後、ボールを袋に戻してから2個目を取り出す』)が、replace=Falseに設定すると重複なしでサンプリングします。
なおreplace=Falseの場合はsizelen(a)より多いとエラーが出ます。sizelen(a)が等しいときは並び替えとなるので.permutation(x)と同じ操作をすることになります。

この重複なし(replace=False)抽出の場合、結果配列の並び順が気にならないときがあります(高校数学でいう「順列」ではなく「組み合わせ」を求める場合)。例えばくじが複数回引かれるとき、1人目と2人目がそれぞれ引く場合は1つ目と2つ目の結果を区別する必要があるのに対して、同じ人が複数回引く場合は1つ目と2つ目を区別する必要はありません。
この場合はshuffle=Falseを設定すると、処理速度が速くなります。

>>> rng = np.random.default_rng(0)
>>> arr = np.array(['a', 'b', 'c', 'd', 'e', 'f', 'g'])

# 重複あり(replace=True)
>>> rng.choice(arr, 7)
array(['f', 'e', 'd', 'b', 'c', 'a', 'a'], dtype='<U1')

# 重複なし(replace=False)
>>> rng.choice(arr, 7, replace=False)
array(['a', 'g', 'b', 'c', 'd', 'f', 'e'], dtype='<U1')

# 重複なし、組み合わせ抽出(速い)
>>> rng.choice(arr, 7, replace=False, shuffle=False)
array(['a', 'b', 'c', 'd', 'e', 'f', 'g'], dtype='<U1')

第一引数aに整数を設定するとnp.arange(a)が元の配列と解釈されます(そのためreplace=Trueの場合は.integers(a)と同じ操作をすることになります)。

>>> rng = np.random.default_rng(0)
>>> rng.choice(10, 7, replace=False)
array([9, 0, 1, 5, 2, 4, 3], dtype=int64)

多次元配列の場合はaxis引数で抽出する軸を設定します。

>>> rng = np.random.default_rng(0)
>>> arr = np.array([[11, 12, 13],
...                 [21, 22, 23],
...                 [31, 32, 33]])

# 行(axis=0)サンプリング
>>> rng.choice(arr, 1, axis=0)
array([[31, 32, 33]])

# 列(axis=1)サンプリング
>>> rng.choice(arr, 1, axis=1)
array([[12],
       [22],
       [32]])

おまけ

.perumutation()/.choice(replace=False)を高速に

配列のサイズがおよそ100未満の場合はrng.permutation(x)よりもx[rng.random(len(x)).argsort()]のほうが高速に処理できます。

>>> rng = np.random.default_rng(0)
>>> arr = np.array(['a', 'b', 'c', 'd', 'e', 'f', 'g'])

# 一般的な方法
>>> rng.permutation(arr)
array(['c', 'e', 'd', 'g', 'f', 'a', 'b'], dtype='<U1')

# 速い方法
>>> idx = rng.random(len(arr)).argsort()
... idx
array([4, 2, 3, 0, 6, 1, 5], dtype=int64)
>>> arr[idx]
array(['e', 'c', 'd', 'a', 'g', 'b', 'f'], dtype='<U1')

この方法は.perumutation()を複数回行う場合に特に有用です。

>>> rng = np.random.default_rng(0)
>>> arr = np.array(['a', 'b', 'c', 'd', 'e', 'f', 'g'])

# 一般的な方法
>>> np.vstack([rng.permutation(arr) for _ in range(4)])
array([['c', 'e', 'd', 'g', 'f', 'a', 'b'],
       ['f', 'c', 'e', 'g', 'b', 'a', 'd'],
       ['c', 'b', 'd', 'g', 'a', 'e', 'f'],
       ['e', 'a', 'g', 'c', 'f', 'b', 'd']], dtype='<U1')

# 速い方法
>>> arr[rng.random((4, len(arr))).argsort()]
array([['c', 'd', 'a', 'b', 'g', 'f', 'e'],
       ['a', 'g', 'e', 'd', 'f', 'c', 'b'],
       ['a', 'd', 'e', 'c', 'b', 'f', 'g'],
       ['c', 'e', 'a', 'f', 'b', 'd', 'g']], dtype='<U1')

.choice(replace=False)は以下のようにします。

>>> rng = np.random.default_rng(0)
>>> arr = np.array(['a', 'b', 'c', 'd', 'e', 'f', 'g'])

# 一般的な方法
>>> np.vstack([rng.choice(arr, 3, replace=False) for _ in range(4)])
array([['d', 'g', 'e'],
       ['g', 'f', 'a'],
       ['d', 'g', 'f'],
       ['d', 'g', 'f']], dtype='<U1')

# 速い方法
>>> arr[rng.random((4, len(arr))).argsort()[:, :3]]
array([['b', 'd', 'f'],
       ['d', 'e', 'b'],
       ['b', 'a', 'f'],
       ['b', 'e', 'a']], dtype='<U1')
2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4