Generar Un Número Aleatorio Ponderado


Estoy tratando de idear una (buena) manera de elegir un número aleatorio de un rango de números posibles donde a cada número en el rango se le da un peso. En pocas palabras: dado el rango de números (0,1,2) elija un número donde 0 tiene un 80% de probabilidad de ser seleccionado, 1 tiene un 10% de probabilidad y 2 tiene un 10% de probabilidad.

Han pasado unos 8 años desde mi clase de estadísticas universitarias, por lo que puedes imaginar la fórmula adecuada para esto se me escapa en este momento.

Aquí está el 'barato y sucio' método que se me ocurrió. Esta solución utiliza ColdFusion. El tuyo puede usar el idioma que quieras. Soy programador, creo que puedo manejarlo. En última instancia, mi solución tiene que estar en Groovy - escribí este en ColdFusion porque es fácil de escribir/probar rápidamente en 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 ) );    
}

Estoy buscando mejores soluciones, por favor sugiera mejoras o alternativas.

Author: Daniel A. White, 2011-12-08

10 answers

Rejection sampling (como en su solución) es lo primero que viene a la mente, mediante el cual construye una tabla de búsqueda con elementos poblados por su distribución de peso, luego elige una ubicación aleatoria en la tabla y la devuelve. Como opción de implementación, haría una función de orden superior que toma una especificación y devuelve una función que devuelve valores basados en la distribución en la especificación, de esta manera se evita tener que construir la tabla para cada llamada. Las desventajas son que el rendimiento algorítmico de la construcción de la tabla es lineal por el número de elementos y potencialmente podría haber una gran cantidad de uso de memoria para especificaciones grandes (o aquellos con miembros con pesos muy pequeños o precisos, e. g.{0:0.99999, 1:0.00001}). La ventaja es que elegir un valor tiene tiempo constante, lo que podría ser deseable si el rendimiento es crítico. En 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...

Otra estrategia es elegir un número aleatorio en [0,1) e iterar sobre la especificación de peso sumando los pesos, si el número aleatorio es menor que la suma, devuelve el valor asociado. Por supuesto, esto supone que los pesos suman a uno. Esta solución no tiene costos iniciales, pero tiene un rendimiento algorítmico promedio lineal por el número de entradas en la especificación. Por ejemplo, en 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...
 54
Author: maerics,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-12-08 18:45:29

Genere un número aleatorio R entre 0 y 1.

Si R en [0, 0.1) -> 1

Si R en [0.1, 0.2) -> 2

Si R en [0.2, 1] -> 3

Si no puede obtener directamente un número entre 0 y 1, genere un número en un rango que produzca tanta precisión como desee. Por ejemplo, si tiene los pesos para

(1, 83.7%) y (2, 16.3%), rodar un número de 1 a 1000. 1-837 es un 1. 838-1000 es 2.

 18
Author: Thomas Eding,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-01 05:08:12

Esta es más o menos una versión genérica de lo que @trinithis escribió, en Java: Lo hice con ints en lugar de flotadores para evitar errores de redondeo desordenados.

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));
    }
}
 10
Author: Greg Case,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-12-08 18:11:21

Aquí hay 3 soluciones en javascript ya que no estoy seguro en qué idioma lo desea. Dependiendo de sus necesidades, uno de los dos primeros podría funcionar, pero el tercero es probablemente el más fácil de implementar con grandes conjuntos de números.

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]
 8
Author: qw3n,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-12-08 18:00:43

¿Qué tal

Int [ ] números = { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 2 } ;

Luego puede seleccionar aleatoriamente entre números y 0 tendrá una probabilidad del 80%, 1 10% y 2 10%

 5
Author: emory,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-12-08 17:44:37

Utilizo lo siguiente

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

Esta es mi opción aleatoria" ponderada", donde uso una función inversa de " x " (donde x es un aleatorio entre min y max) para generar un resultado ponderado, donde el mínimo es el elemento más pesado, y el máximo el más ligero (menos posibilidades de obtener el resultado)

Así que básicamente, usar weightedRandom(1, 5) significa que las posibilidades de obtener un 1 son más altas que un 2, que son más altas que un 3, que son más altas que un 4, que son más altas que un 5.

Podría no sea útil para su caso de uso, pero probablemente sea útil para las personas que buscan en Google esta misma pregunta.

Después de un intento de 100 iteraciones, me dio:

==================
| Result | Times |
==================
|      1 |    55 |
|      2 |    28 |
|      3 |     8 |
|      4 |     7 |
|      5 |     2 |
==================
 5
Author: Tom Roggero,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-16 16:16:27

Este está en Mathematica, pero es fácil de copiar a otro lenguaje, lo uso en mis juegos y puede manejar pesos decimales:

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

(Ahora estoy hablando con una lista el índice del primer elemento es igual a 0) La idea detrás de esto es que teniendo una lista normalizada pesos hay una posibilidad de pesos[n] para devolver el índice n, por lo que las distancias entre el mínimo y el máximo en el paso n deberían ser pesos[n]. La distancia total desde el mínimo min (que lo ponemos como 0) y el máximo max es la suma de los pesos de la lista .

Lo bueno detrás de esto es que no anexas a ningún array o nido de bucles, y eso aumenta enormemente el tiempo de ejecución.

Aquí está el código en C# sin necesidad de normalizar la lista de pesos y eliminar algún código:

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
Author: Garmekain,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-11 14:57:03

Aquí está la entrada y las relaciones : 0 (80%), 1(10%) , 2 (10%)

Vamos a dibujarlos para que sea fácil de visualizar.

                0                       1        2
-------------------------------------________+++++++++

Vamos a sumar el peso total y llamarlo TR para la relación total. así que en este caso 100. permite obtener aleatoriamente un número de (0-TR) o (0 a 100 en este caso) . 100 siendo tu peso total. Llámalo RN por número aleatorio.

Así que ahora tenemos TR como el peso total y RN como el número aleatorio entre 0 y TR.

Así que imaginemos que elegimos un # aleatorio de 0 a 100. Digamos 21. así que eso es en realidad 21%.

DEBEMOS CONVERTIR / HACER COINCIDIR ESTO CON NUESTROS NÚMEROS DE ENTRADA, PERO ¿CÓMO ?

Permite hacer un bucle sobre cada peso (80, 10, 10) y mantener la suma de los pesos que ya visitamos. en el momento en que la suma de los pesos sobre los que estamos haciendo un bucle es mayor que el número aleatorio RN (21 en este caso), detenemos el bucle y devolvemos esa posición del elemento.

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

Digamos que el número aleatorio (entre 0 y 100) es 83. Vamos a hacerlo de nuevo:

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
 1
Author: j2emanue,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-23 06:32:29

Sugiero usar una comprobación continua de la probabilidad y el resto del número aleatorio.

Esta función establece primero el valor de retorno al último índice posible e itera hasta que el resto del valor aleatorio sea menor que la probabilidad real.

Las probabilidades tienen que sumar a uno.

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
Author: Nina Scholz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-06 11:08:28

Tengo una máquina de ranura y usé el código de abajo para generar números aleatorios. En probabilitiesSlotMachine las claves son la salida en la máquina de ranura, y los valores representan el peso.

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

Ahora para generar una salida aleatoria, utilizo este código:

const random = allSlotMachineResults[Math.floor(Math.random() * allSlotMachineResults.length)]
 0
Author: J. Doe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-10 22:43:48