ネットの海の片隅で

技術ネタの放流、あるいは不法投棄。

正規乱数を生成するgem「RandomBell」

Rubyで一様な乱数が欲しければRandomやSecureRandomを使えば良いと思うのですが、「偏った乱数」が欲しいこともあると思います。

イメージとしてはゲームのパラメータとかですかね。

例えば、あるダンジョンで出てくる敵のレベルを20-30の間でランダムに決めたいときに、全てのレベルの敵が均等に出てくるとちょっと困ると思います。ここでやりたいことは

  • 基本的にはLv25くらいの敵が出現
  • たまにLv20やLv30の敵も出して緩急をつける

みたいな感じです。

ゲームに限らず、「ある程度まとまっている一方で適度に散っている数字」が欲しいケースは結構あると思うので、そういったときに使えるgemを作ってみました。

方針

  1. 今までは得た乱数のべき乗をとったりして重みを付けていた。
  2. 得られる乱数の分布が砲弾型になって美しくない。
  3. どういう分布が良いんだ?
  4. 釣り鐘型が良いと思う。
  5. それって正規分布じゃね?

→ 返ってくる乱数の確率密度分布が正規分布に従うような擬似乱数生成器をつくる。

インストール

Rubygemsrandom_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

で出力できます。

f:id:s_osa:20140606115537p:plain

統計学が最強の学問である

*1:返り値が正規分布しているかどうかの検定をするテストとか欲しいですw