Generar un número aleatorio ponderado

Resuelto Todd Sharp asked hace 13 años • 16 respuestas

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 del rango se le asigna un peso. En pocas palabras: dado el rango de números (0,1,2), elija un número donde 0 tenga un 80% de probabilidad de ser seleccionado, 1 tenga un 10% de probabilidad y 2 tenga un 10% de probabilidad.

Han pasado aproximadamente 8 años desde mi clase de estadística en la universidad, así que puedes imaginar que en este momento se me escapa la fórmula adecuada para esto.

Este es el método "barato y sucio" que se me ocurrió. Esta solución utiliza ColdFusion. El tuyo puede usar el idioma que quieras. Soy programador, creo que puedo manejar la portabilidad. En última instancia, mi solución debe estar en Groovy; escribí esta 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. Sugiera mejoras o alternativas.

Todd Sharp avatar Dec 09 '11 00:12 Todd Sharp
Aceptado

El muestreo de rechazo (como en su solución) es lo primero que le viene a la mente, mediante el cual crea 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 tome una especificación y devuelva una función que devuelva valores basados ​​en la distribución de la especificación, de esta manera evita tener que crear la tabla para cada llamada. Las desventajas son que el rendimiento algorítmico de la creación de la tabla es lineal según la cantidad de elementos y podría haber un gran uso de memoria para especificaciones grandes (o aquellas con miembros con pesos muy pequeños o precisos, por ejemplo, {0:0.99999, 1). :0.00001}). La ventaja es que elegir un valor tiene un 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 [0,1)e iterar sobre la especificación de peso sumando los pesos; si el número aleatorio es menor que la suma, devolver el valor asociado. Por supuesto, esto supone que los pesos suman 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...
maerics avatar Dec 08 '2011 17:12 maerics

Genera 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 tienes las pesas para

(1, 83,7%) y (2, 16,3%), saca un número del 1 al 1000. 1-837 es un 1. 838-1000 es 2.

Thomas Eding avatar Dec 08 '2011 17:12 Thomas Eding

Yo uso lo siguiente

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

Este es mi método aleatorio "ponderado", donde uso una función inversa de "x" (donde x es aleatorio entre mínimo y máximo) 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)

Básicamente, usar weightedRandom(1, 5)significa que las posibilidades de obtener un 1 son mayores que un 2, que son mayores que un 3, que son mayores que un 4, que son mayores que un 5.

Puede que 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 intentarlo 100 iteraciones, me dio:

==================
| Result | Times |
==================
|      1 |    55 |
|      2 |    28 |
|      3 |     8 |
|      4 |     7 |
|      5 |     2 |
==================
tomasdev avatar Oct 16 '2017 16:10 tomasdev

Aquí hay 3 soluciones en javascript, ya que no estoy seguro de en qué idioma lo desea. Dependiendo de sus necesidades, una de las dos primeras podría funcionar, pero la tercera es probablemente la 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]
qw3n avatar Dec 08 '2011 18:12 qw3n

Esta es más o menos una versión genérica de lo que escribió @trinithis en Java: lo hice con enteros en lugar de flotantes para evitar errores de redondeo complicados.

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));
    }
}
Greg Case avatar Dec 08 '2011 18:12 Greg Case