はじめに
例えば、60%の確率でa、25%の確率でb、15%の確率でcを出力するようなプログラムを実装します。
※本稿では、実装例として言語はTypeScriptで書いていますが、本稿の主旨に言語は関係ありません。
失敗例
この問題の解法としてパッと思いつくのは、以下のようなif文で分岐させるコードです。
/// 失敗例
// [1,100]の区間でランダムな整数を返す関数
const rand = () => Math.floor(Math.random() * 100) + 1
// 小数点第一位で丸める関数
const round = v => Math.round(v * 10) / 10
// 試行回数
const N = 10000
let a = 0, b = 0, c = 0
for (let i = 0; i < N; i++) {
if (rand() <= 60) {
// 60%の確率でa
a++
} else if (rand() <= 25) {
// 25%の確率でb
b++
} else {
// 15%の確率でc
c++
}
}
// 結果出力
console.log(`a: ${round(a / N * 100)}%\nb: ${round(b / N * 100)}%\nc: ${round(c / N * 100)}%`)
このプログラムは一見正解な気がしますが、実際の結果は
a: 59.7%
b: 9.7%
c: 30.6%
みたいになります。aは60%に近い結果となっていますが、bとcはかなり違っています。これは何故でしょうか?
失敗例のコードだと、bのブロックに入る実際の確率は、
(100% - 60%) * 25% = 10%
です。これはaではない(確率40%)かつbである(確率25%)という条件になってしまっているからです。
正解例
正解は以下のようなコードになります。
/// 正解例
// 小数点第一位で丸める関数
const round = v => Math.round(v * 10) / 10
// 試行回数
const N = 10000
let a = 0, b = 0, c = 0
for (let i = 0; i < N; i++) {
switch (randomWithProbabilityTable({
a: 60,
b: 25
})) {
case "a":
a++
break
case "b":
b++
break
default:
c++
}
}
// 結果出力
console.log(`a: ${round(a / N * 100)}%\nb: ${round(b / N * 100)}%\nc: ${round(c / N * 100)}%`)
randomWithProbabilityTable
関数の実装は以下の通りです。
function randomWithProbabilityTable<
TABLE extends { [key: string]: number }
>(probabilityTable: TABLE): keyof TABLE | null {
// 確率テーブルを確率の低い順にソート
const table = Object.entries(probabilityTable)
.sort(([, a], [, b]) => a - b)
.reduce(
(result, [key, val]) => ({ ...result, [key]: val }),
{}
) as TABLE
// [1,100]の区間でランダムな整数を生成
const rand = Math.floor(Math.random() * 100) + 1
let rate = 0
for (const key in table) {
rate += table[key]
if (rand <= rate) {
return key
}
}
return null
}
このプログラムの実行結果は以下のようになります。
a: 60.3%
b: 24.8%
c: 14.9%
だいたい期待した通りの数値に近い結果となっています。
確率テーブルに則った結果を得たいときは、正解例のコードのようにひと手間必要です。
randomWithProbabilityTable
のような関数を1つ用意しておけば、引数を変えるだけで色々な場面で使い回せます。
参考