Random\Randomizer::getFloat

等確率に選んだ float の値を取得する

説明

public float Random\Randomizer::getFloat(float $min, float $max, Random\IntervalBoundary $boundary = Random\IntervalBoundary::ClosedOpen)

指定した区間に一様に分散した浮動小数点数の中から、等確率に選んだ値を返します。

精度が限られているため、 全ての実数が正確に浮動小数点数として表現できるとは限りません。 数値が正確に表現できない場合、値は表現できるもっとも近い値に丸められます。 さらに浮動小数点数の値は、数直線全体に均等に分布しているわけではありません。 浮動小数点数の値は 2進数の指数を使うので、隣り合うふたつの値の距離は、 それぞれの2のべき乗ごとに2倍になります。 つまり: 1.02.0 の間で表現できる 浮動小数点数 の数は、2.04.0 の間で表現できる数、4.08.0 の間で表現できる数、8.016.0 の間で表現できる数と同じ... といった具合です。

こうした理由から、例えば2つの整数を割るなどして、 要求された区間内の任意の数をランダムサンプリングすると、 分布が偏る可能性があります。 必要な丸めを行うと、 浮動小数点数の値によっては別の値より多くの頻度で返されることがあります。 浮動小数点数の値の密度が変わる2のべき乗周辺の値については、特にそれが当てはまります。

Random\Randomizer::getFloat は、 指定した区間内で正確に表現でき、 かつ一様に分散した浮動小数点数の可能な限り大きな集合から、 等確率に値を返すアルゴリズムを実装します。 選択可能な浮動小数点数の間の距離 (ステップサイズ) は、 密度が最も小さい浮動小数点数の間の距離、つまり、 絶対値がより大きな区間境界の浮動小数点数の距離に一致します。 これは、区間が2のべき乗を1つ以上横切る場合、 与えられた区間内で表現可能な浮動小数点数の値がすべて返されるとは限らないということです。 ステップは、絶対値の大きい区間境界から始まります。 これは、ステップを正確に表現可能な浮動小数点数と一致させるためです。

閉区間の境界は、常に選択可能な浮動小数点数の集合に含まれます。 よって、区間のサイズがステップサイズの正確な倍数でなく、 かつ絶対値が小さな方の境界が閉じている場合、 その境界とそれにもっとも近い選択可能な浮動小数点数との間の距離は、 ステップサイズよりも小さくなります。

警告

このメソッドが返す float の値を後処理すると、 値の一様な分布を壊す可能性があります。なぜなら、 数学演算に含まれる中間的な浮動小数点数の値は暗黙のうちに丸められているからです。 指定された区間は、できるだけ望ましい区間と一致させるべきですし、 丸め処理はこのメソッドが選択した値をユーザーに表示させる直前に明示的に実行すべきです。

サンプルの浮動小数点表現を使った、アルゴリズムの説明

アルゴリズムがどのように動作するかの例を示すために、 3ビットの仮数を使用する浮動小数点表現を考えてみましょう。 この表現は、連続する2のべき乗の間で、8つの異なる浮動小数点数を表現することができます。 つまり、1.02.0 の間では 0.125 のすべてのステップサイズを正確に表現でき、 2.04.0 の間では 0.25 のすべてのステップサイズを正確に表現できるということです。 実際には、PHP の float は 52 ビットの仮数を使用しており、 それぞれの 2 のべき乗の間で 252 個の異なる値を表現することができます。 つまり、以下の数が、1.04.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.02.25 が、 他の値に比べて2倍の確率で返されることにも注意しましょう。

パラメータ

min

区間の下限

max

区間の上限

boundary

区間の境界を戻り値に含めるかどうかを指定します。

戻り値

min, max, boundary で指定した区間に一様に分散した float の中から、等確率に選んだ値を返します。 minmax が戻り値に含まれるかどうかは、 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
  • Random\Randomizer::getInt