0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

独自の確率分布からアイテムを抽籤する機能の実装

Last updated at Posted at 2022-05-27

無作為にレベルを選択し,そのレベルの中から無作為にアイテムを選択するというような機能を実装する必要があった.そこで実装されたコードが次である.

def lootbox_of_item
  case $lootbox
  when 0..40
    rand($level[1])
  when 40..60
    rand($level[2])
  when 60..80
    rand($level[3])
  when 80..90
    rand($level[4])
  when 90..100
    rand($level[5])
  end
end

$level = {
  1 => 1..459,
  2 => 460..1362,
  3 => 1363..1755,
  4 => 1756..2101,
  5 => 2102..2184
}

$lootbox = rand(0..100)

何これ…きも…

lootbox_of_item関数でアイテム番号を選択しているが,全てのwhen文にrand関数を適用していたり,ハッシュの値が何を指すのか分からなかったり,とかく酷いコードである.ゴミをGitHubに上げるな.

というわけで,一度実現したい機能を整理することにした.

ほとんどは Rで学ぶ逆変換(逆関数)法 にて述べられているが,一度振り返ってみる.

確率論による解法

条件

問題は問題番号と重要度をもつ.重要度は出題確率をもつ.

確率質量関数

確率変数 $X$ を問題の重要度とする.確率質量関数$P(X)$ を次のように定義する.

$$P(X=x_k) = p_k $$

分布関数 $F(X)$ は次のように定義される.

$$F(X=x) = P(X \leq x) = \sum_{k=1}^x P(X=k) $$

確率変数 $Y$ を問題の番号とする.確率質量関数 $P(Y)$ を次のように定義する.

$$P(Y \mid X = k) = 1/n_k $$

しかるに $n_k$ は,問題の重要度の集合 $C = \{ 1, 2, \cdots, c\}$ に写像される,各重要度に含まれる問題数をもつ数列の項である.

$$\{n_k\}_{k \in C} $$

重要度 $k$ の問題番号の範囲を $s_k$から $e_k$ とすると,これらは次で求められる.

$$s_k=\sum_{l=1}^{k-1} n_l + 1 $$

$$e_k=\sum_{l=1}^{k} n_l $$

よって,次の等式が成り立つ.

$$e_k=s_k+n_k-1 $$

分布関数 $F(Y)$ は,$s_k \leq y\leq e_k$ を満たす $k$ を用いて,次のように定義される.

\begin{aligned} F(Y=y \mid X = k) &= \sum_{l=s_k}^{y} P(Y=l \mid X = k) \\ &= \frac{y-s_k+1}{n_k} \\&= 1 - \frac{e_k-y}{n_k} \end{aligned}

逆変換法

確率変数 $u$ が一様分布 $[0, 1)$ に従うとする.このとき,関数 $F_X^{-1}(U)$ を次のように定義すると,サンプリングを行うことができる.

$$F_X^{-1}(U=u) = \inf{\{X \mid F(X \leq u)\}} $$

$F_Y^{-1}(U)$ についても同様である.

これで幾分か見通しが良くなった気がする.

Scipyで離散分布を扱う

Scipyには独自の離散分布を扱うことができるクラスがある.

確率変数のリストと確率質量関数のリストをそれぞれ定義し, stats.rv_discreteインスタンスのrvs(random variates)メソッドを呼べば,以上で述べた $u \sim [0, 1)$ の逆変換法のような機能を作れる.

初めに述べたコードは,次のように可読性を高めることができた.ちなみに,PyCallでScipyのモジュールを呼び出している.

  def get_level(levels, probabilities)
    pyfrom :scipy, import: :stats

    xk = levels
    pk = probabilities
    custm = stats.rv_discrete.call({ values: [xk, pk] })
    custm.rvs
  end

  def get_problem_id(level, ctgr_attr)
    range = ctgr_attr[level]['range']
    start = range['start']
    end_ = range['end']
    rand start..end_
  end

ちなみに,レベルと確率分布は別のyamlファイルに分けた.

上で考察したことを吹っ飛ばして機能を実装できてしまった 😲

ちなみに確率の和が1でないとき

raise ValueError("The sum of provided pk is not 1.")

と例外処理される1

  1. https://github.com/scipy/scipy/blob/4cf21e753cf937d1c6c2d2a0e372fbc1dbbeea81/scipy/stats/_distn_infrastructure.py#L3682

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?