正規乱数を生成するgem「RandomBell」
Rubyで一様な乱数が欲しければRandomやSecureRandomを使えば良いと思うのですが、「偏った乱数」が欲しいこともあると思います。
イメージとしてはゲームのパラメータとかですかね。
例えば、あるダンジョンで出てくる敵のレベルを20-30の間でランダムに決めたいときに、全てのレベルの敵が均等に出てくるとちょっと困ると思います。ここでやりたいことは
- 基本的にはLv25くらいの敵が出現
- たまにLv20やLv30の敵も出して緩急をつける
みたいな感じです。
ゲームに限らず、「ある程度まとまっている一方で適度に散っている数字」が欲しいケースは結構あると思うので、そういったときに使えるgemを作ってみました。
方針
- 今までは得た乱数のべき乗をとったりして重みを付けていた。
- 得られる乱数の分布が砲弾型になって美しくない。
- どういう分布が良いんだ?
- 釣り鐘型が良いと思う。
- それって正規分布じゃね?
→ 返ってくる乱数の確率密度分布が正規分布に従うような擬似乱数生成器をつくる。
インストール
Rubygemsにrandom_bell
という名前で上がっていますので、
$ gem install random_bell
または
# Gemfile gem 'random_bell'
$ bundle install
でインストールできます。
使い方
基本
bell = RandomBell.new bell.rand #=> 0.596308234257563 bell.rand #=> 0.4986734346841275 bell.rand #=> 0.6441978387725272
非常にシンプルな感じです。
返ってくる乱数の分布はこんな感じになります。
+0.050: *** +0.100: ***** +0.150: ********* +0.200: ************* +0.250: ********************** +0.300: ************************** +0.350: ********************************** +0.400: **************************************** +0.450: *********************************************** +0.500: ************************************************* +0.550: ************************************************* +0.600: *********************************************** +0.650: ************************************** +0.700: ********************************** +0.750: ************************** +0.800: ********************* +0.850: ************* +0.900: ******** +0.950: ***** +1.000: ***
分布の中心を指定
bell = RandomBell.new(mu: 0.75)
+0.050: +0.100: +0.150: +0.200: +0.250: * +0.300: ** +0.350: ***** +0.400: ********* +0.450: ************* +0.500: ******************** +0.550: *************************** +0.600: *********************************** +0.650: **************************************** +0.700: ************************************************ +0.750: ************************************************* +0.800: ************************************************* +0.850: ******************************************** +0.900: ******************************************* +0.950: *********************************** +1.000: *************************
標準偏差を指定
bell = RandomBell.new(sigma: 0.5)
+0.050: ******************************* +0.100: ********************************* +0.150: ********************************* +0.200: ************************************* +0.250: ****************************************** +0.300: ******************************************** +0.350: *********************************************** +0.400: ********************************************* +0.450: ************************************************* +0.500: *********************************************** +0.550: ************************************************ +0.600: *********************************************** +0.650: ****************************************** +0.700: ******************************************** +0.750: ********************************************* +0.800: ****************************************** +0.850: **************************************** +0.900: ***************************************** +0.950: ********************************* +1.000: **********************************
範囲を指定
bell = RandomBell.new(range: 0.4..1.2)
+0.440: ********************************************* +0.480: ************************************************* +0.520: ************************************************** +0.560: ************************************************* +0.600: ******************************************** +0.640: ***************************************** +0.680: *********************************** +0.720: ***************************** +0.760: ************************ +0.800: ******************** +0.840: ************* +0.880: ********** +0.920: ******* +0.960: **** +1.000: *** +1.040: * +1.080: * +1.120: +1.160: +1.200:
組み合わせ
上記3種類の引数を組み合わせれば、
bell = RandomBell.new(mu: 25, sigma: 2, range: 20..30)
+20.500: ** +21.000: ***** +21.500: ********* +22.000: ************* +22.500: ******************** +23.000: ************************* +23.500: ********************************* +24.000: ************************************* +24.500: ******************************************** +25.000: ************************************************ +25.500: ********************************************** +26.000: ******************************************* +26.500: **************************************** +27.000: ******************************** +27.500: ************************* +28.000: ******************* +28.500: ************ +29.000: ******* +29.500: ***** +30.000: **
こんな感じで冒頭の要求を満たすことができます。
パフォーマンス
ベンチマークを取ってみました。
コード
require 'benchmark' require 'securerandom' require 'random_bell' REPEAT = 10 ** 6 Benchmark.bmbm do |b| b.report("Random "){ rand = Random.new ; REPEAT.times{ rand.rand } } b.report("SecureRandom"){ REPEAT.times{ SecureRandom.random_number } } b.report("RandomBell "){ rand = RandomBell.new ; REPEAT.times{ rand.rand } } end
結果
Rehearsal ------------------------------------------------ Random 0.130000 0.000000 0.130000 ( 0.131620) SecureRandom 2.510000 0.010000 2.520000 ( 2.546658) RandomBell 1.290000 0.000000 1.290000 ( 1.296140) --------------------------------------- total: 3.940000sec user system total real Random 0.090000 0.000000 0.090000 ( 0.084740) SecureRandom 2.210000 0.000000 2.210000 ( 2.215300) RandomBell 1.280000 0.000000 1.280000 ( 1.279669)
- Randomより10-15倍くらい遅い
- SecureRandomより2倍弱速い
くらいですかね。
使い方によりますが、たいていのケースで実用に足る速度が出せていると思います。
リポジトリ
https://github.com/s-osa/random_bell に置いてあります。
PRお待ちしています。*1
おまけ
上に掲載しているヒストグラムは
puts bell.to_histogram
で出力できます。