2012-05-29 16 views
34

Se ho una collezione di elementi in una lista. Voglio scegliere da quella lista secondo un'altra lista di pesi.Scelta ponderata breve e semplice

Ad esempio, la mia collezione è ['one', 'two', 'three'] ei pesi sono [0.2, 0.3, 0.5], mi aspetto che il metodo mi dia "tre" in circa la metà di tutti i disegni.

Qual è il modo più semplice per farlo?

risposta

11

Questa funzione richiede due argomenti: un elenco di pesi e una lista contenente gli oggetti da scegliere:

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] 

non fa uso di alcun loop pitone.

+2

I commenti sembrano essere fuorviante. 'cumsum()' fornisce i valori cumulativi, non i valori booleani. Per essere chiari, funziona, ma i commenti non corrispondono a quello che sta realmente accadendo. –

+0

Ho modificato per correggere e anche mettere la docstring su una riga, come raccomandato in [PEP 257] (http://www.python.org/dev/peps/pep-0257/#one-line-docstrings). –

3

Se non si desidera utilizzare numpy, è possibile seguire lo stesso metodo con qualcosa di simile:

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

Usiamo itertools.takewhile() per evitare il controllo dei valori una volta che arriviamo al punto vogliamo fermare, in caso contrario, questa è essenzialmente la stessa idea di Mischa Obrecht's answer, solo senza numpy.

4

È possibile utilizzare l'multinomial distribution (da NumPy) per fare quello che vuoi. Per esempio.

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) 

Così l'elemento in prima posizione si avvicinò 20 volte, l'elemento in seconda posizione si avvicinò 32 volte, e l'elemento in terza posizione si avvicinò 48 volte, più o meno quello che ci si aspetterebbe data i pesi.

Se hai difficoltà a comprendere la distribuzione multinomiale, ho trovato il documentation davvero utile.

+2

Nota puoi ridurre la costruzione dei risultati a 'itertools.chain.from_iterable ([elements [i]] * count, per i, contare in enumerate (indices [0]))', che sarà più veloce. –

+1

In effetti, puoi migliorarlo ulteriormente sostituendo la moltiplicazione della lista con 'itertools.repeat (elements [i], count)'. –

1

su cui costruire Maus' answer, che è grande se si vuole ottenere ripetutamente valori casuali ponderati, se si voleva solo un singolo valore, si può fare questo in modo molto semplice, combinando numpy.random.multinomial() e 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 Fracassato la tua modifica con la mia per sbaglio, ritorna al tuo (migliore) link. –

2

Come sull'inizializzazione dell'elenco solo per abbinare le tue scelte con i pesi previsti. Qui sto facendo una lista di 100 valori che rappresentano la percentuale desiderata di "pull".

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

Non è cumulativo, ma sembra che potrebbe essere quello che cercate.

+0

sembra che abbia lo stesso effetto, ma allocare un vettore 3 * 100 solo per fare una scelta sembra un po 'eccessivo. Soprattutto se lo usassi nel contesto, il problema è venuto prima, che è una simulazione Monte Carlo, dove vuoi essere il più veloce possibile ... –

+0

Devi aggiungere queste informazioni alla domanda. Ma, la tua unica allocazione dell'elenco una volta, chiamando "random.choice()" sarà veloce. – monkut

+0

sì, ma direi che se c'è un modo economico e un modo costoso per ottenere lo stesso risultato, è ovvio che si sceglie quello economico. Giudici che governano? :) –

60

Dal versione 1.7 è possibile utilizzare 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

Questa risposta deve essere convalidata. –

+0

Soluzione perfetta 'l = [scelta (elementi, p = pesi) per _ nell'intervallo (1000)]' e 'dal contatore Importa collezioni; Counter (l) 'deliver:' Counter ({'three': 498, 'two': 281, 'one': 221}) '. – user2016508

7

Dal Python 3.6, si può fare ponderata scelta casuale (con sostituzione) utilizzando random.choices.

casuale.scelte (popolazione, pesi = None, *, cum_weights = Nessuno, k = 1) utilizzo

Esempio:

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