Генерировать Взвешенное Случайное Число

Я пытаюсь придумать (хороший) способ выбрать случайное число из диапазона возможных чисел, где каждому числу в диапазоне задается вес. Проще говоря: учитывая диапазон чисел (0,1,2), выберите число, где 0 имеет 80% вероятность быть выбранным, 1 имеет 10% шанс и 2 имеет 10% шанс.

прошло около 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 ) );    
}

Я ищу лучшие решения, пожалуйста, предложите улучшения или альтернативы.

10 ответов


отклонения выборки (например, в вашем решении) - это первое, что приходит на ум, когда вы создаете таблицу поиска с элементами, заполненными их распределением веса, а затем выбираете случайное местоположение в таблице и возвращаете его. В качестве выбора реализации я бы сделал функцию более высокого порядка, которая принимает спецификацию и возвращает функцию, которая возвращает значения на основе распределения в спецификации, таким образом, вам не нужно создавать таблицу для каждого вызова. Нижняя сторона является ли алгоритмическая производительность построения таблицы линейной по количеству элементов, и потенциально может быть много использования памяти для больших спецификаций (или с членами с очень маленькими или точными весами, например {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) и повторите над спецификацией веса суммирование весов, если случайное число меньше суммы, возвращает связанное значение. Конечно, это предполагает, что веса складываются в единицу. Это решение не имеет авансовых затрат, но имеет среднюю алгоритмическую производительность, линейную по количеству записей в спецификации. Например, в 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...

генерировать случайное число R между 0 и 1.

Если 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.


Это более или менее обобщенная версия того, что написал @trinithis на Java: я сделал это с помощью ints, а не поплавков, чтобы избежать грязных ошибок округления.

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));
    }
}

вот 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]

как о

int [] числа = { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 2 } ;

затем вы можете случайным образом выбрать из чисел, и 0 будет иметь 80% шанс, 1 10% и 2 10%


Я использую следующие

function weightedRandom(min, max) {
  return Math.round(max / (Math.random() * max + min));
}

Это мой "взвешенный" случайный, где я использую обратную функцию " x " (где x-случайный между min и max) для генерации взвешенного результата, где минимум-самый тяжелый элемент, а максимум-самый легкий (наименьшие шансы получить результат)

Так в основном, используя weightedRandom(1, 5) означает, что шансы получить 1 выше, чем 2, которые выше, чем 3, которые выше, чем 4, которые выше, чем 5.

может быть не полезно для вашего случая использования, но, вероятно, полезно для людей, которые гуглят этот же вопрос.

после 100 итераций попробуйте, он дал мне:

==================
| Result | Times |
==================
|      1 |    55 |
|      2 |    28 |
|      3 |     8 |
|      4 |     7 |
|      5 |     2 |
==================

это в 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) идея этого заключается в том, что с нормализованным списком весом есть шанс веса[n] вернуть индекс n, поэтому расстояния между min и max на шаге n должно быть веса[n]. Общее расстояние от минимального min (который мы ставим как 0) и максимальная сумма список весом.

хорошо, что вы не добавляете к любому массиву или гнезду для циклов, и это сильно увеличивает время выполнения.

вот код в 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;
}

здесь входной сигнал и коэффициенты : 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

Я предлагаю использовать непрерывную проверку вероятности и остальных случайных чисел.

эта функция сначала устанавливает возвращаемое значение последнего возможного индекса и выполняет итерацию, пока остальная часть случайного значения не будет меньше фактической вероятности.

вероятности должны суммироваться в единицу.

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; }

у меня есть slotmachine, и я использовал код ниже для генерации случайных чисел. В probabilitiesSlotMachine ключи являются выходом в slotmachine, а значения представляют вес.

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)]