Random\Randomizer::getFloat
(PHP 8 >= 8.3.0)
Random\Randomizer::getFloat — 等確率に選んだ float の値を取得する
説明
$min
, float $max
, Random\IntervalBoundary $boundary
= Random\IntervalBoundary::ClosedOpen): float指定した区間に一様に分散した浮動小数点数の中から、等確率に選んだ値を返します。
精度が限られているため、
全ての実数が正確に浮動小数点数として表現できるとは限りません。
数値が正確に表現できない場合、値は表現できるもっとも近い値に丸められます。
さらに浮動小数点数の値は、数直線全体に均等に分布しているわけではありません。
浮動小数点数の値は 2進数の指数を使うので、隣り合うふたつの値の距離は、
それぞれの2のべき乗ごとに2倍になります。
つまり: 1.0
と 2.0
の間で表現できる 浮動小数点数 の数は、2.0
と 4.0
の間で表現できる数、4.0
と 8.0
の間で表現できる数、8.0
と 16.0
の間で表現できる数と同じ... といった具合です。
こうした理由から、例えば2つの整数を割るなどして、 要求された区間内の任意の数をランダムサンプリングすると、 分布が偏る可能性があります。 必要な丸めを行うと、 浮動小数点数の値によっては別の値より多くの頻度で返されることがあります。 浮動小数点数の値の密度が変わる2のべき乗周辺の値については、特にそれが当てはまります。
Random\Randomizer::getFloat() は、 指定した区間内で正確に表現でき、 かつ一様に分散した浮動小数点数の可能な限り大きな集合から、 等確率に値を返すアルゴリズムを実装します。 選択可能な浮動小数点数の間の距離 (ステップサイズ) は、 密度が最も小さい浮動小数点数の間の距離、つまり、 絶対値がより大きな区間境界の浮動小数点数の距離に一致します。 これは、区間が2のべき乗を1つ以上横切る場合、 与えられた区間内で表現可能な浮動小数点数の値がすべて返されるとは限らないということです。 ステップは、絶対値の大きい区間境界から始まります。 これは、ステップを正確に表現可能な浮動小数点数と一致させるためです。
閉区間の境界は、常に選択可能な浮動小数点数の集合に含まれます。 よって、区間のサイズがステップサイズの正確な倍数でなく、 かつ絶対値が小さな方の境界が閉じている場合、 その境界とそれにもっとも近い選択可能な浮動小数点数との間の距離は、 ステップサイズよりも小さくなります。
このメソッドが返す float の値を後処理すると、 値の一様な分布を壊す可能性があります。なぜなら、 数学演算に含まれる中間的な浮動小数点数の値は暗黙のうちに丸められているからです。 指定された区間は、できるだけ望ましい区間と一致させるべきですし、 丸め処理はこのメソッドが選択した値をユーザーに表示させる直前に明示的に実行すべきです。
サンプルの浮動小数点表現を使った、アルゴリズムの説明
アルゴリズムがどのように動作するかの例を示すために、
3ビットの仮数を使用する浮動小数点表現を考えてみましょう。
この表現は、連続する2のべき乗の間で、8つの異なる浮動小数点数を表現することができます。
つまり、1.0
と 2.0
の間では 0.125
のすべてのステップサイズを正確に表現でき、
2.0
と 4.0
の間では 0.25
のすべてのステップサイズを正確に表現できるということです。
実際には、PHP の float は 52 ビットの仮数を使用しており、
それぞれの 2 のべき乗の間で 252
個の異なる値を表現することができます。
つまり、以下の数が、1.0
と 4.0
の範囲で正確に表現できるということです。
1.0
1.125
1.25
1.375
1.5
1.625
1.75
1.875
2.0
2.25
2.5
2.75
3.0
3.25
3.5
3.75
4.0
さてここで、$randomizer->getFloat(1.625, 2.5, IntervalBoundary::ClosedOpen)
がコールされた場合を考えてみましょう。
つまり、1.625
から始まり、2.5
までのランダムな値 (ただし 2.5
そのものは含みません)
を要求するということです。
このメソッドのアルゴリズムは、
まず絶対値の大きい境界(2.5
)でのステップサイズを決定します。
今回の境界の場合、ステップサイズは 0.25
です。
ここで、要求された区間のサイズは 0.875
であり、
0.25
の正確な倍数ではないことに注意してください。
もしアルゴリズムが下界 1.625
からステップを開始すると、
正確に表現できない 2.125
が現れた時点で、暗黙の丸めが発生します。
そのため、アルゴリズムは上界 2.5
からステップを開始するのです。
選択可能な値は、以下のとおりです:
2.25
2.0
1.75
1.625
2.5
は選択可能な値に含まれません。
なぜなら、要求された区間の上界が開いているからです。
1.625
は選択可能な値に含まれます。
この値は最も近い値 1.75
との距離が 0.125
であり、以前決定したステップサイズ 0.25
より小さいのですが、
それにも関わらず、含まれます。
その理由は、要求された区間は下側の境界(1.625
)で閉じており、
閉じた境界は常に含まれるからです。
最後に、アルゴリズムは4つの選択可能な値から等確率で値をランダムに選び、それを返します。
2つの整数値を割るやり方ではなぜダメなのか
上の例では、2のべき乗で区切られたそれぞれの区間に、
表現可能な浮動小数点数が8個あります。
ランダムな浮動小数点数を生成するのに、
なぜ2つの整数を割る方法がうまくいかないのかを示すために、
0.0
から 1.0
を含まない右開きの区間に、16個の一様に分散した浮動小数点数があるとしましょう。
そのうち半分は、0.5
から 1.0
までの間で8つの値として正確に表現できますが、
残りの半分は 0.0
から 1.0
までの間でステップサイズが 0.0625
の値です。
これらの値は、0
から 15
の間のランダムな整数を 16
で割ることによって、以下のうちのひとつを簡単に生成することができます:
0.0
0.0625
0.125
0.1875
0.25
0.3125
0.375
0.4375
0.5
0.5625
0.625
0.6875
0.75
0.8125
0.875
0.9375
このランダムな浮動小数点の値は、
1.625
から 2.75
まで
(2.75
を含まない) の右開きの区間に、
区間の大きさ(0.875
) を掛け合わせ、
最小の 1.625
を足すことで拡大変換できます。
この、いわゆるアフィン変換の結果は、以下のような値になります:
1.625
は、1.625
に丸められます1.679
は、1.625
に丸められます1.734
は、1.75
に丸められます1.789
は、1.75
に丸められます1.843
は、1.875
に丸められます1.898
は、1.875
に丸められます1.953
は、2.0
に丸められます2.007
は、2.0
に丸められます2.062
は、2.0
に丸められます2.117
は、2.0
に丸められます2.171
は、2.25
に丸められます2.226
は、2.25
に丸められます2.281
は、2.25
に丸められます2.335
は、2.25
に丸められます2.390
は、2.5
に丸められます2.445
は、2.5
に丸められます
2.5
が、
開区間であるため除外されるべきなのに、返されてしまう点に注意してください。
また、2.0
と 2.25
が、
他の値に比べて2倍の確率で返されることにも注意しましょう。
パラメータ
min
-
区間の下限
max
-
区間の上限
boundary
-
区間の境界を戻り値に含めるかどうかを指定します。
戻り値
min
,
max
, boundary
で指定した区間に一様に分散した float の中から、等確率に選んだ値を返します。
min
と max
が戻り値に含まれるかどうかは、
boundary
の値次第です。
エラー / 例外
-
min
の値が有限(is_finite())でない場合、 ValueError がスローされます。 -
max
の値が有限(is_finite())でない場合、 ValueError がスローされます。 - 指定された区間に何も値が含まれない場合、 ValueError がスローされます。
-
Random\Randomizer::$engine
に存在する Random\Engine::generate() メソッド がスローした、あらゆる Throwable がスローされます。
例
例1 Random\Randomizer::getFloat() の例
<?php
$randomizer = new \Random\Randomizer();
// 緯度の粒度は経度の粒度の2倍であることに注意しましょう。
//
// 緯度については、-90 から 90 の値をとります
// 経度については、180 という値は取れますが、-180 は取れません。
// なぜなら、-180 と 180 は同じ経度を指しているからです。
printf(
"Lat: %+.6f Lng: %+.6f",
$randomizer->getFloat(-90, 90, \Random\IntervalBoundary::ClosedClosed),
$randomizer->getFloat(-180, 180, \Random\IntervalBoundary::OpenClosed),
);
?>
上の例の出力は、 たとえば以下のようになります。
Lat: +69.244304 Lng: -53.548951
注意
注意:
このメソッドは、望ましい振る舞いを得るために » Drawing Random Floating-Point Numbers from an Interval. Frédéric Goualard, ACM Trans. Model. Comput. Simul., 32:3, 2022 に記されている γ-section アルゴリズムを実装しています。
参考
- Random\Randomizer::nextFloat() - 半開区間 [0.0, 1.0) から、float の値を取得する
- Random\Randomizer::getInt() - 等確率に選ばれる整数を取得する