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

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

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

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

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

## 基本的な使い方

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

```python
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$には実行したときの現在時刻などが入るために、結果がランダムになっています。

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

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

```python: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))
```

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

```python: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()`

```python
rng.integers(low, high=None, size=None, dtype=np.int64, endpoint=False)
```

| 引数       | 説明                                                |
| :--------- | :-------------------------------------------------- |
| `low`      | 結果が取り得る値の最小値（`0`のとき省略可能）       |
| `high`     | 結果が取り得る値の最大値                            |
| `size`     | 結果の配列の形。省略するとスカラー値                |
| `dtype`    | 結果配列のデータ型。デフォルトは`int64`             |
| `endpoint` | デフォルトは`False`で右開区間。`True`にすると閉区間 |

```python:例
>>> 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`引数をイメージしてください）。

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

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

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

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

```python
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`には小数を渡すこともできます。この場合小数点以下は切り捨てられて解釈されます。この使い方は推奨しません。また浮動小数の精度には注意してください。

```python
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次元以上の配列も作成できます。

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

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

```python
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`の値が一致していなくても、可能であれば適宜ブロードキャストされます。

```python
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`の値の組み合わせが適切でない場合はエラーが出ます。

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

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

```python
rng.random(size=None, dtype=np.float64, out=None)
```

| 引数    | 説明                                      |
| :------ | :---------------------------------------- |
| `size`  | 結果の配列の形。省略するとスカラー値      |
| `dtype` | 結果配列のデータ型。デフォルトは`float64` |
| `out`   | 既存の配列に要素を代入する場合に使用      |

```python:例
>>> 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$を適用します。

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

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

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

```python
rng.bytes(length)
```

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

```python:例
>>> 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()`

```python
rng.normal(loc=0.0, scale=1.0, size=None)
```

| 引数    | 説明                                 |
| :------ | :----------------------------------- |
| `loc`   | 平均値$μ$                            |
| `scale` | 標準偏差$σ$                          |
| `size`  | 結果の配列の形。省略するとスカラー値 |

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

```python
import matplotlib.pyplot as plt

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

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

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

# レア封入率3%の100連ガチャを10万人が引いたときの、各プレイヤーが引いたレアの枚数
arr = rng.binomial(100, 0.03, 100000)
(arr == 0).sum()  # -> 約4700人が一枚も引けなかった
```

```python:ポアソン分布
rng.poisson(lam=1.0, size=None)  # {lam: 平均発生回数λ}
```

```python:カイ二乗分布
rng.chisquare(df, size=None)  # {df: 自由度}
```

等

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

```python
rng.shuffle(x, axis=0)
rng.permutation(x, axis=0)
```

| 引数   | 説明                                    |
| :----- | :-------------------------------------- |
| `x`    | シャッフル元の配列                      |
| `axis` | `a`が多次元配列の場合にシャッフルする軸 |

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

```python:例
>>> 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`引数はシャッフルする軸を指定します。

```python:例
>>> 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()`

```python
rng.permuted(x, axis=None, out=None)
```

| 引数   | 説明                                 |
| :----- | :----------------------------------- |
| `x`    | シャッフル元の配列                   |
| `axis` | シャッフルする軸                     |
| `out`  | 既存の配列に要素を代入する場合に使用 |

- 前項の`.permutation()`は多次元配列の軸を並び替えます。
-`.permuted()`は多次元配列の軸内の要素をシャッフルします。

```python:例
>>> 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()`

```python
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`だけ要素を取り出します。

```python:例
>>> 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`（全確率）でない場合はエラーが出ます。

```python:例
>>> 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`では、重複ありでサンプリングします（いわゆる『袋からボールを取り出した後、ボールを袋に戻してから２個目を取り出す』）が、`replace=False`に設定すると重複なしでサンプリングします。
なお`replace=False`の場合は`size`が`len(a)`より多いとエラーが出ます。`size`と`len(a)`が等しいときは並び替えとなるので`.permutation(x)`と同じ操作をすることになります。

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

```python:例
>>> 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)`と同じ操作をすることになります）。

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

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

```python:例
>>> 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()]`のほうが高速に処理できます。

```python:例
>>> 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()`を複数回行う場合に特に有用です。

```python:例
>>> 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)`は以下のようにします。

```python:例
>>> 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')
```