2012-05-29 25 views
34

Si tengo una colección de elementos en una lista. Quiero elegir de esa lista según otra lista de pesos.Elección ponderada corta y simple

Por ejemplo mi colección es ['one', 'two', 'three'] y los pesos son [0.2, 0.3, 0.5], la que sería de esperar el método para mí 'tres' da en aproximadamente la mitad de todos los sorteos.

¿Cuál es la forma más fácil de hacerlo?

Respuesta

11

Esta función toma dos argumentos: Una lista de los pesos y una lista que contiene los objetos para elegir:

from numpy import cumsum 
from numpy.random import rand 
def weightedChoice(weights, objects): 
    """Return a random item from objects, with the weighting defined by weights 
    (which must sum to 1).""" 
    cs = cumsum(weights) #An array of the weights, cumulatively summed. 
    idx = sum(cs < rand()) #Find the index of the first weight over a random value. 
    return objects[idx] 

que no utiliza ningún bucles pitón.

+2

Los comentarios parecen ser engañosa. 'cumsum()' da los valores acumulativos, no los valores booleanos. Para ser claros, esto funciona, pero los comentarios no coinciden con lo que realmente está sucediendo. –

+0

He corregido para corregir, y también puse el docstring en una línea, como se recomienda en [PEP 257] (http://www.python.org/dev/peps/pep-0257/#one-line-docstrings). –

3

Si no desea utilizar numpy, se puede seguir el mismo método con algo como esto:

from random import random 
from itertools import takewhile 

def accumulate(iterator): 
    """Returns a cumulative sum of the elements. 
    accumulate([1, 2, 3, 4, 5]) --> 1 3 6 10 15""" 
    current = 0 
    for value in iterator: 
     current += value 
     yield current 

def weightedChoice(weights, objects): 
    """Return a random item from objects, with the weighting defined by weights 
    (which must sum to 1).""" 
    limit = random() 
    return objects[sum(takewhile(bool, (value < limit for value in accumulate(weights))))] 

Utilizamos itertools.takewhile() para evitar la comprobación de valores una vez que alcanzamos el punto queremos detener, de lo contrario, esta es esencialmente la misma idea que Mischa Obrecht's answer, simplemente sin numpy.

4

Puede usar el multinomial distribution (desde numpy) para hacer lo que quiera. P.ej.

elements = ['one', 'two', 'three'] 
weights = [0.2, 0.3, 0.5] 


import numpy as np 

indices = np.random.multinomial(100, weights, 1) 
#=> array([[20, 32, 48]]), YMMV 

results = [] #A list of the original items, repeated the correct number of times. 
for i, count in enumerate(indices[0]): 
    results.extend([elements[i]]*count) 

Así el elemento en la primera posición subió 20 veces, el elemento en la segunda posición llegó hasta 32 veces, y el elemento en la tercera posición llegó hasta 48 veces, más o menos lo que cabría esperar dadas las pesas.

Si está teniendo dificultades para envolver su cabeza en la distribución multinomial, encontré que documentation realmente útil.

+2

Tenga en cuenta que puede reducir su creación de resultados a 'itertools.chain.from_iterable ([elements [i]] * count, para i, count en enumerate (indices [0]))', que será más rápido. –

+1

De hecho, puede mejorarlo aún más si reemplaza la multiplicación de la lista con 'itertools.repeat (elementos [i], count)' también. –

1

para construir sobre Maus' answer, que es grande si usted desea conseguir repetidamente valores aleatorios ponderados, si sólo quería un solo valor, se puede hacer esto de manera muy sencilla mediante la combinación de numpy.random.multinomial() y itertools.compress():

from itertools import compress 
from numpy.random import multinomial 

def weightedChoice(weights, objects): 
    """Return a random item from objects, with the weighting defined by weights 
    (which must sum to 1).""" 
    return next(compress(objects, multinomial(1, weights, 1)[0])) 
+0

@aix Rompió su edición con la mía por accidente, regresó a su (mejor) enlace. –

2

Cómo acerca de simplemente inicializar su lista para que coincida con sus elecciones con los pesos esperados. Aquí estoy haciendo una lista de 100 valores que representan el porcentaje de "atracción" deseado.

>>> import random 
>>> elements = ['one', 'two', 'three'] 
>>> weights = [0.2, 0.3, 0.5] 
>>> 
>>> # get "sum" of result list of lists (flattens list) 
>>> choices = sum([[element] * int(weight * 100)for element, weight in zip(elements, weights)], []) 
>>> random.choice(choices) 
three 

No es acumulativo, pero parece que podría ser lo que tu buscas.

+0

parece que tiene el mismo efecto, pero asignar un vector 3 * 100 solo para hacer una elección parece un poco exagerado. Especialmente si lo usara en el contexto surgió el problema primero, que es una simulación de Monte Carlo, donde quieres estar lo más rápido posible ... –

+0

Deberías agregar esa información a la pregunta. Pero, su única asignación de la lista una vez, llamando "random.choice()" será rápido. – monkut

+0

sí, pero yo diría que si hay una forma barata y una forma costosa de lograr el mismo resultado, es evidente que uno elige el más barato. ¿Jueces que gobiernan? :) –

60

Desde versión 1.7 puede utilizar numpy.random.choice():

elements = ['one', 'two', 'three'] 
weights = [0.2, 0.3, 0.5] 

from numpy.random import choice 
print(choice(elements, p=weights)) 
+3

Esta respuesta debería ser validada. –

+0

Solución perfecta 'l = [elección (elementos, p = pesos) para _ en el rango (1000)]' y 'del contador de importación de colecciones; El contador (l) 'entrega:' Contador ({'tres': 498, 'dos': 281, 'uno': 221}) '. – user2016508

7

Como Python 3.6, se puede hacer una elección aleatoria ponderada (con reemplazo) usando random.choices.

al azar.opciones (población, pesos = Ninguno, *, cum_weights = Ninguno, k = 1)

Ejemplo de uso:

import random 
random.choices(['one', 'two', 'three'], [0.2, 0.3, 0.5], k=10) 
# ['three', 'two', 'three', 'three', 'three', 
# 'three', 'three', 'two', 'two', 'one']