[英]Generate A Weighted Random Number
我正在尝试 devise 一种(好的)方法来从一系列可能的数字中选择一个随机数,其中该范围内的每个数字都被赋予了权重。 简单地说:给定数字范围 (0,1,2) 选择一个数字,其中 0 有 80% 的概率被选中,1 有 10% 的机会,2 有 10% 的机会。
自从我的大学统计数据 class 以来已经过去了大约 8 年,所以你可以想象这个正确的公式现在让我忘记了。
这是我想出的“便宜又脏”的方法。 此解决方案使用 ColdFusion。 您可以使用任何您喜欢的语言。 我是一名程序员,我想我可以处理它的移植。 最终我的解决方案需要在 Groovy 中 - 我在 ColdFusion 中写了这个,因为它很容易在 CF 中快速编写/测试。
public function weightedRandom( Struct options ) {
var tempArr = [];
for( var o in arguments.options )
{
var weight = arguments.options[ o ] * 10;
for ( var i = 1; i<= weight; i++ )
{
arrayAppend( tempArr, o );
}
}
return tempArr[ randRange( 1, arrayLen( tempArr ) ) ];
}
// test it
opts = { 0=.8, 1=.1, 2=.1 };
for( x = 1; x<=10; x++ )
{
writeDump( weightedRandom( opts ) );
}
我正在寻找更好的解决方案,请提出改进或替代方案。
拒绝抽样(例如在您的解决方案中)是第一个想到的事情,您可以构建一个查找表,其中包含按权重分布填充的元素,然后在表中随机选择一个位置并返回它。 作为一种实现选择,我会创建一个高阶函数,它接受一个规范并返回一个函数,该函数根据规范中的分布返回值,这样您就不必为每次调用构建表。 缺点是构建表格的算法性能与项目数量呈线性关系,并且对于大型规范(或那些具有非常小或精确权重的成员,例如 {0:0.99999, 1 :0.00001})。 好处是选择一个值有恒定的时间,如果性能至关重要,这可能是可取的。 在 JavaScript 中:
function weightedRand(spec) {
var i, j, table=[];
for (i in spec) {
// The constant 10 below should be computed based on the
// weights in the spec for a correct and optimal table size.
// E.g. the spec {0:0.999, 1:0.001} will break this impl.
for (j=0; j<spec[i]*10; j++) {
table.push(i);
}
}
return function() {
return table[Math.floor(Math.random() * table.length)];
}
}
var rand012 = weightedRand({0:0.8, 1:0.1, 2:0.1});
rand012(); // random in distribution...
另一种策略是在[0,1)
选择一个随机数并迭代权重规范求和,如果随机数小于总和,则返回相关值。 当然,这假设权重总和为 1。 该解决方案没有前期成本,但平均算法性能与规范中的条目数量呈线性关系。 例如,在 JavaScript 中:
function weightedRand2(spec) {
var i, sum=0, r=Math.random();
for (i in spec) {
sum += spec[i];
if (r <= sum) return i;
}
}
weightedRand2({0:0.8, 1:0.1, 2:0.1}); // random in distribution...
生成一个介于 0 和 1 之间的随机数 R。
如果 R 在 [0, 0.1) -> 1
如果 R 在 [0.1, 0.2) -> 2
如果 R 在 [0.2, 1] -> 3
如果您不能直接获得 0 到 1 之间的数字,请生成一个范围内的数字,该数字将产生所需的精度。 例如,如果你有权重
(1, 83.7%) 和 (2, 16.3%) 掷出一个从 1 到 1000 的数字。1-837 是 1。838-1000 是 2。
我使用以下
function weightedRandom(min, max) {
return Math.round(max / (Math.random() * max + min));
}
这是我的首选“加权”随机数,我使用“x”的反函数(其中 x 是最小值和最大值之间的随机数)来生成加权结果,其中最小值是最重的元素,最大值是最轻(获得结果的机会最小)
所以基本上,使用weightedRandom(1, 5)
意味着获得 1 的机会高于 2,高于 3,高于 4,高于 5。
可能对您的用例没有用,但可能对谷歌搜索同样问题的人有用。
经过 100 次迭代尝试,它给了我:
==================
| Result | Times |
==================
| 1 | 55 |
| 2 | 28 |
| 3 | 8 |
| 4 | 7 |
| 5 | 2 |
==================
这里有 3 个 javascript 解决方案,因为我不确定您想要哪种语言。根据您的需要,前两个中的一个可能有效,但第三个可能是最容易用大量数字实现的。
function randomSimple(){
return [0,0,0,0,0,0,0,0,1,2][Math.floor(Math.random()*10)];
}
function randomCase(){
var n=Math.floor(Math.random()*100)
switch(n){
case n<80:
return 0;
case n<90:
return 1;
case n<100:
return 2;
}
}
function randomLoop(weight,num){
var n=Math.floor(Math.random()*100),amt=0;
for(var i=0;i<weight.length;i++){
//amt+=weight[i]; *alternative method
//if(n<amt){
if(n<weight[i]){
return num[i];
}
}
}
weight=[80,90,100];
//weight=[80,10,10]; *alternative method
num=[0,1,2]
这或多或少是@trinithis 在 Java 中编写的内容的通用版本:我使用整数而不是浮点数来完成它以避免混乱的舍入错误。
static class Weighting {
int value;
int weighting;
public Weighting(int v, int w) {
this.value = v;
this.weighting = w;
}
}
public static int weightedRandom(List<Weighting> weightingOptions) {
//determine sum of all weightings
int total = 0;
for (Weighting w : weightingOptions) {
total += w.weighting;
}
//select a random value between 0 and our total
int random = new Random().nextInt(total);
//loop thru our weightings until we arrive at the correct one
int current = 0;
for (Weighting w : weightingOptions) {
current += w.weighting;
if (random < current)
return w.value;
}
//shouldn't happen.
return -1;
}
public static void main(String[] args) {
List<Weighting> weightings = new ArrayList<Weighting>();
weightings.add(new Weighting(0, 8));
weightings.add(new Weighting(1, 1));
weightings.add(new Weighting(2, 1));
for (int i = 0; i < 100; i++) {
System.out.println(weightedRandom(weightings));
}
}
怎么样
int [] numbers = { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 2 } ;
然后您可以从数字中随机选择,0 将有 80% 的机会,1 10% 和 2 10%
晚了 8 年,但这是我的 4 行解决方案。
pmf[array_index] = P(X=array_index):
var pmf = [0.8, 0.1, 0.1]
cdf[array_index] = F(X=array_index):
var cdf = pmf.map((sum => value => sum += value)(0))
// [0.8, 0.9, 1]
3a) 生成一个随机数。
3b) 获取大于或等于该数字的元素数组。
3c) 返回其长度。
var r = Math.random()
cdf.filter(el => r >= el).length
这个在 Mathematica 中,但很容易复制到另一种语言,我在我的游戏中使用它,它可以处理十进制权重:
weights = {0.5,1,2}; // The weights
weights = N@weights/Total@weights // Normalize weights so that the list's sum is always 1.
min = 0; // First min value should be 0
max = weights[[1]]; // First max value should be the first element of the newly created weights list. Note that in Mathematica the first element has index of 1, not 0.
random = RandomReal[]; // Generate a random float from 0 to 1;
For[i = 1, i <= Length@weights, i++,
If[random >= min && random < max,
Print["Chosen index number: " <> ToString@i]
];
min += weights[[i]];
If[i == Length@weights,
max = 1,
max += weights[[i + 1]]
]
]
(现在我正在谈论列表第一个元素的索引等于 0)这背后的想法是,拥有标准化列表权重有机会weights[n]返回索引n ,因此 min 和 max 之间的距离在步骤n应该是weights[n] 。 与最小最小值(我们将其设为 0)和最大值最大值的总距离是列表权重的总和。
这背后的好处是您不会附加到任何数组或嵌套 for 循环,这会大大增加执行时间。
这是 C# 中的代码,无需标准化权重列表并删除一些代码:
int WeightedRandom(List<float> weights) {
float total = 0f;
foreach (float weight in weights) {
total += weight;
}
float max = weights [0],
random = Random.Range(0f, total);
for (int index = 0; index < weights.Count; index++) {
if (random < max) {
return index;
} else if (index == weights.Count - 1) {
return weights.Count-1;
}
max += weights[index+1];
}
return -1;
}
我建议使用对概率和其余随机数的连续检查。
此函数首先将返回值设置为最后一个可能的索引并迭代,直到剩余的随机值小于实际概率。
概率之和必须为 1。
function getRandomIndexByProbability(probabilities) { var r = Math.random(), index = probabilities.length - 1; probabilities.some(function (probability, i) { if (r < probability) { index = i; return true; } r -= probability; }); return index; } var i, probabilities = [0.8, 0.1, 0.1], count = probabilities.map(function () { return 0; }); for (i = 0; i < 1e6; i++) { count[getRandomIndexByProbability(probabilities)]++; } console.log(count);
.as-console-wrapper { max-height: 100% !important; top: 0; }
这是输入和比率:0 (80%), 1(10%), 2 (10%)
让我们把它们画出来,这样很容易形象化。
0 1 2
-------------------------------------________+++++++++
让我们将总重量相加,并将其称为总比率的 TR。 所以在这种情况下 100. 让我们从 (0-TR) 或 (0 到 100 在这种情况下) 随机获取一个数字。 100 是您的总重量。 将其称为随机数 RN。
所以现在我们将 TR 作为总权重,RN 作为 0 和 TR 之间的随机数。
所以让我们想象一下,我们从 0 到 100 中随机选择了一个#。比如说 21。所以实际上是 21%。
我们必须将其转换/匹配到我们的输入数字,但如何转换?
让我们循环遍历每个权重 (80, 10, 10) 并保留我们已经访问过的权重之和。 当我们循环的权重总和大于随机数 RN(在本例中为 21)时,我们停止循环并返回该元素位置。
double sum = 0;
int position = -1;
for(double weight : weight){
position ++;
sum = sum + weight;
if(sum > 21) //(80 > 21) so break on first pass
break;
}
//position will be 0 so we return array[0]--> 0
假设随机数(0 到 100 之间)是 83。让我们再做一次:
double sum = 0;
int position = -1;
for(double weight : weight){
position ++;
sum = sum + weight;
if(sum > 83) //(90 > 83) so break
break;
}
//we did two passes in the loop so position is 1 so we return array[1]---> 1
我有一台老虎机,我使用下面的代码来生成随机数。 在 probabilitiesSlotMachine 中,键是老虎机中的输出,值表示权重。
const probabilitiesSlotMachine = [{0 : 1000}, {1 : 100}, {2 : 50}, {3 : 30}, {4 : 20}, {5 : 10}, {6 : 5}, {7 : 4}, {8 : 2}, {9 : 1}]
var allSlotMachineResults = []
probabilitiesSlotMachine.forEach(function(obj, index){
for (var key in obj){
for (var loop = 0; loop < obj[key]; loop ++){
allSlotMachineResults.push(key)
}
}
});
现在要生成随机输出,我使用以下代码:
const random = allSlotMachineResults[Math.floor(Math.random() * allSlotMachineResults.length)]
谢谢大家,这是一个有用的线程。 我把它封装成一个方便的函数(Typescript)。 下面的测试(sinon,jest)。 肯定会更紧一点,但希望它是可读的。
export type WeightedOptions = {
[option: string]: number;
};
// Pass in an object like { a: 10, b: 4, c: 400 } and it'll return either "a", "b", or "c", factoring in their respective
// weight. So in this example, "c" is likely to be returned 400 times out of 414
export const getRandomWeightedValue = (options: WeightedOptions) => {
const keys = Object.keys(options);
const totalSum = keys.reduce((acc, item) => acc + options[item], 0);
let runningTotal = 0;
const cumulativeValues = keys.map((key) => {
const relativeValue = options[key]/totalSum;
const cv = {
key,
value: relativeValue + runningTotal
};
runningTotal += relativeValue;
return cv;
});
const r = Math.random();
return cumulativeValues.find(({ key, value }) => r <= value)!.key;
};
测试:
describe('getRandomWeightedValue', () => {
// Out of 1, the relative and cumulative values for these are:
// a: 0.1666 -> 0.16666
// b: 0.3333 -> 0.5
// c: 0.5 -> 1
const values = { a: 10, b: 20, c: 30 };
it('returns appropriate values for particular random value', () => {
// any random number under 0.166666 should return "a"
const stub1 = sinon.stub(Math, 'random').returns(0);
const result1 = randomUtils.getRandomWeightedValue(values);
expect(result1).toEqual('a');
stub1.restore();
const stub2 = sinon.stub(Math, 'random').returns(0.1666);
const result2 = randomUtils.getRandomWeightedValue(values);
expect(result2).toEqual('a');
stub2.restore();
// any random number between 0.166666 and 0.5 should return "b"
const stub3 = sinon.stub(Math, 'random').returns(0.17);
const result3 = randomUtils.getRandomWeightedValue(values);
expect(result3).toEqual('b');
stub3.restore();
const stub4 = sinon.stub(Math, 'random').returns(0.3333);
const result4 = randomUtils.getRandomWeightedValue(values);
expect(result4).toEqual('b');
stub4.restore();
const stub5 = sinon.stub(Math, 'random').returns(0.5);
const result5 = randomUtils.getRandomWeightedValue(values);
expect(result5).toEqual('b');
stub5.restore();
// any random number above 0.5 should return "c"
const stub6 = sinon.stub(Math, 'random').returns(0.500001);
const result6 = randomUtils.getRandomWeightedValue(values);
expect(result6).toEqual('c');
stub6.restore();
const stub7 = sinon.stub(Math, 'random').returns(1);
const result7 = randomUtils.getRandomWeightedValue(values);
expect(result7).toEqual('c');
stub7.restore();
});
});
注意:所有权重都必须是整数
function weightedRandom(items){
let table = Object.entries(items)
.flatMap(([item, weight]) => Array(item).fill(weight))
return table[Math.floor(Math.random() * table.length)]
}
const key = weightedRandom({
"key1": 1,
"key2": 4,
"key3": 8
}) // returns e.g. "key1"
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.