Una versione ponderata di random.choice


246

Avevo bisogno di scrivere una versione ponderata di random.choice (ogni elemento nell'elenco ha una probabilità diversa di essere selezionato). Questo è quello che mi è venuto in mente:

def weightedChoice(choices):
    """Like random.choice, but each element can have a different chance of
    being selected.

    choices can be any iterable containing iterables with two items each.
    Technically, they can have more than two items, the rest will just be
    ignored.  The first item is the thing being chosen, the second item is
    its weight.  The weights can be any numeric values, what matters is the
    relative differences between them.
    """
    space = {}
    current = 0
    for choice, weight in choices:
        if weight > 0:
            space[current] = choice
            current += weight
    rand = random.uniform(0, current)
    for key in sorted(space.keys() + [current]):
        if rand < key:
            return choice
        choice = space[key]
    return None

Questa funzione mi sembra eccessivamente complessa e brutta. Spero che tutti qui possano offrire alcuni suggerimenti su come migliorarlo o modi alternativi per farlo. L'efficienza non è importante per me quanto la pulizia e la leggibilità del codice.

Risposte:


297

Dalla versione 1.7.0, NumPy ha una choicefunzione che supporta le distribuzioni di probabilità.

from numpy.random import choice
draw = choice(list_of_candidates, number_of_items_to_pick,
              p=probability_distribution)

Si noti che probability_distributionè una sequenza nello stesso ordine di list_of_candidates. È inoltre possibile utilizzare la parola chiave replace=Falseper modificare il comportamento in modo che gli elementi disegnati non vengano sostituiti.


11
Secondo i miei test, questo è un ordine di grandezza più lento rispetto random.choicesalle singole chiamate. Se hai bisogno di molti risultati casuali, è davvero importante sceglierli tutti in una volta regolandoli number_of_items_to_pick. Se lo fai, è un ordine di grandezza più veloce.
jpmc26,

2
Questo non funziona con le tuple ecc. ("ValueError: a deve essere monodimensionale"), quindi in tal caso si può chiedere a numpy di selezionare l' indice nell'elenco, cioè len(list_of_candidates), e quindi farelist_of_candidates[draw]
xjcl

218

Da Python 3.6 esiste un metodo choicesdal randommodulo.

Python 3.6.1 (v3.6.1:69c0db5050, Mar 21 2017, 01:21:04)
Type 'copyright', 'credits' or 'license' for more information
IPython 6.0.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import random

In [2]: random.choices(
...:     population=[['a','b'], ['b','a'], ['c','b']],
...:     weights=[0.2, 0.2, 0.6],
...:     k=10
...: )

Out[2]:
[['c', 'b'],
 ['c', 'b'],
 ['b', 'a'],
 ['c', 'b'],
 ['c', 'b'],
 ['b', 'a'],
 ['c', 'b'],
 ['b', 'a'],
 ['c', 'b'],
 ['c', 'b']]

Si noti che random.choicesverrà campione con la sostituzione , secondo i documenti :

Restituisce un kelenco di dimensioni degli elementi scelti dalla popolazione con la sostituzione.

Se hai bisogno di campionare senza sostituzione, allora come afferma la brillante risposta di @ ronan-paixão , puoi usare numpy.choice, il cui replaceargomento controlla tale comportamento.


4
Questo è molto più veloce di numpy.random.choice. Scegliendo da un elenco di 8 articoli ponderati 10.000 volte, numpy.random.choice ha impiegato 0,3286 secondi, mentre come random.choices ha impiegato 0,0416 secondi, circa 8 volte più veloce.
Anton Codes,

@AntonCodes Questo esempio è selezionato ciliegia. numpy avrà un sovraccarico a tempo costante che random.choicesnon lo fa, quindi ovviamente è più lento in un elenco minuscolo di 8 elementi, e se scegli 10k volte da tale elenco, hai ragione. Ma per i casi in cui l'elenco è più grande (a seconda di come stai testando, vedo i punti di interruzione tra 100-300 elementi), np.random.choiceinizia a sovraperformare random.choicescon un gap abbastanza ampio. Ad esempio, incluso il passaggio di normalizzazione insieme alla chiamata intorpidita, ottengo un eccesso di velocità quasi 4x random.choicesper un elenco di 10k elementi.
Ggorlen,

Questa dovrebbe essere la nuova risposta basata sul miglioramento delle prestazioni segnalato da @AntonCodes.
Wayne Workman,

132
def weighted_choice(choices):
   total = sum(w for c, w in choices)
   r = random.uniform(0, total)
   upto = 0
   for c, w in choices:
      if upto + w >= r:
         return c
      upto += w
   assert False, "Shouldn't get here"

10
Puoi eliminare un'operazione e risparmiare un po 'di tempo invertendo le istruzioni all'interno del ciclo for:upto +=w; if upto > r
knite

5
salvare una variabile eliminando fino a e semplicemente diminuendo r del peso ogni volta. Il confronto è quindiif r < 0
JnBrymn

@JnBrymn Devi controllare r <= 0. Considera un set di input di 1 elementi e un rotolo di 1,0. L'asserzione fallirà allora. Ho corretto quell'errore nella risposta.
Moooeeeep

1
@Sardathrion potresti usare un pragma per contrassegnare il ciclo for come parziale:# pragma: no branch
Ned Batchelder

1
@ mLstudent33 Non utilizzo Udacity.
Anton Codes

70
  1. Disporre i pesi in una distribuzione cumulativa.
  2. Usa random.random () per scegliere un float casuale 0.0 <= x < total.
  3. Cerca nella distribuzione usando bisect.bisect come mostrato nell'esempio su http://docs.python.org/dev/library/bisect.html#other-examples .
from random import random
from bisect import bisect

def weighted_choice(choices):
    values, weights = zip(*choices)
    total = 0
    cum_weights = []
    for w in weights:
        total += w
        cum_weights.append(total)
    x = random() * total
    i = bisect(cum_weights, x)
    return values[i]

>>> weighted_choice([("WHITE",90), ("RED",8), ("GREEN",2)])
'WHITE'

Se è necessario effettuare più di una scelta, suddividerla in due funzioni, una per costruire i pesi cumulativi e un'altra per tagliare in due punti casuali.


5
Questo è più efficiente della risposta di Ned. Fondamentalmente, invece di fare una ricerca lineare (O (n)) attraverso le scelte, sta facendo una ricerca binaria (O (log n)). +1!
NHDaly

indice di tupla fuori intervallo se random () restituisce 1,0
Jon Vaughan,

10
Ciò si verifica ancora a O(n)causa del calcolo della distribuzione cumulativa.
Lev Levitsky,

6
Questa soluzione è migliore nel caso in cui siano necessarie più chiamate a weighted_choice per lo stesso insieme di scelte. In tal caso è possibile creare una somma cumulativa una volta ed eseguire una ricerca binaria su ogni chiamata.
Amos,

1
@JonVaughan random() non può restituire 1.0. Secondo i documenti, restituisce un risultato nell'intervallo di metà apertura [0.0, 1.0), vale a dire che può restituire esattamente 0,0, ma non può restituire esattamente 1,0. Il valore più grande che può restituire è 0.99999999999999988897769753748434595763683319091796875 (che Python stampa come 0.999999999999999999 ed è il float a 64 bit più grande inferiore a 1).
Mark Amery,

21

Se non ti dispiace usare numpy, puoi usare numpy.random.choice .

Per esempio:

import numpy

items  = [["item1", 0.2], ["item2", 0.3], ["item3", 0.45], ["item4", 0.05]
elems = [i[0] for i in items]
probs = [i[1] for i in items]

trials = 1000
results = [0] * len(items)
for i in range(trials):
    res = numpy.random.choice(items, p=probs)  #This is where the item is selected!
    results[items.index(res)] += 1
results = [r / float(trials) for r in results]
print "item\texpected\tactual"
for i in range(len(probs)):
    print "%s\t%0.4f\t%0.4f" % (items[i], probs[i], results[i])

Se sai quante selezioni devi fare in anticipo, puoi farlo senza un ciclo come questo:

numpy.random.choice(items, trials, p=probs)

15

Greggio, ma può essere sufficiente:

import random
weighted_choice = lambda s : random.choice(sum(([v]*wt for v,wt in s),[]))

Funziona?

# define choices and relative weights
choices = [("WHITE",90), ("RED",8), ("GREEN",2)]

# initialize tally dict
tally = dict.fromkeys(choices, 0)

# tally up 1000 weighted choices
for i in xrange(1000):
    tally[weighted_choice(choices)] += 1

print tally.items()

stampe:

[('WHITE', 904), ('GREEN', 22), ('RED', 74)]

Presuppone che tutti i pesi siano numeri interi. Non devono aggiungere fino a 100, l'ho fatto solo per facilitare l'interpretazione dei risultati del test. (Se i pesi sono numeri in virgola mobile, moltiplicarli tutti per 10 ripetutamente fino a quando tutti i pesi> = 1.)

weights = [.6, .2, .001, .199]
while any(w < 1.0 for w in weights):
    weights = [w*10 for w in weights]
weights = map(int, weights)

1
Bene, non sono sicuro di poter supporre che tutti i pesi siano numeri interi, comunque.
Colin,

1
Sembra che i tuoi oggetti sarebbero duplicati in questo esempio. Sarebbe inefficiente (e così è la funzione per convertire i pesi in numeri interi). Tuttavia, questa soluzione è un buon one-liner se i pesi interi sono piccoli.
wei2912

Le primitive saranno duplicate, ma gli oggetti avranno solo riferimenti duplicati, non gli oggetti stessi. (ecco perché non puoi creare un elenco di elenchi utilizzando [[]]*10- tutti gli elementi dell'elenco esterno puntano allo stesso elenco.
PaulMcG

@PaulMcG No; nient'altro che riferimenti saranno mai duplicati. Il sistema di tipi di Python non ha alcun concetto di primitivi. Puoi confermare che anche con un esempio intstai ancora ottenendo molti riferimenti allo stesso oggetto facendo qualcosa di simile [id(x) for x in ([99**99] * 100)]e osserva che idrestituisce lo stesso indirizzo di memoria ad ogni chiamata.
Mark Amery,

14

Se hai un dizionario ponderato anziché un elenco, puoi scriverlo

items = { "a": 10, "b": 5, "c": 1 } 
random.choice([k for k in items for dummy in range(items[k])])

Si noti che [k for k in items for dummy in range(items[k])]produce questo elenco['a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'c', 'b', 'b', 'b', 'b', 'b']


10
Questo funziona per piccoli valori di popolazione totale, ma non per insiemi di dati di grandi dimensioni (ad esempio popolazione degli Stati Uniti per stato finirebbe per creare una lista di lavoro con 300 milioni di elementi al suo interno).
Ryan,

@Ryan In effetti. Inoltre, non funziona con pesi non interi, che rappresentano un altro scenario realistico (ad esempio se i pesi sono espressi come probabilità di selezione).
Mark Amery,

12

A partire da Python v3.6, random.choicespotrebbe essere usato per restituire uno listdi elementi di dimensioni specificate dalla popolazione data con pesi opzionali.

random.choices(population, weights=None, *, cum_weights=None, k=1)

  • popolazione : listcontenente osservazioni uniche. (Se vuoto, solleva IndexError)

  • pesi : pesi relativi più precisamente necessari per effettuare le selezioni.

  • cum_weights : pesi cumulativi necessari per effettuare le selezioni.

  • k : size ( len) listdell'output. (Impostazione predefinita len()=1)


Pochi avvertimenti:

1) Si avvale di campionamento ponderato con sostituzione in modo che gli elementi estratti vengano successivamente sostituiti. I valori nella sequenza dei pesi in sé non contano, ma il loro rapporto relativo è importante.

A differenza di ciò np.random.choiceche può assumere solo le probabilità sotto forma di pesi e che deve garantire la somma delle probabilità individuali fino a 1 criterio, non esistono norme di questo tipo. Fintanto che appartengono a tipi numerici ( int/float/fractiontranne il Decimaltipo), questi funzionerebbero comunque.

>>> import random
# weights being integers
>>> random.choices(["white", "green", "red"], [12, 12, 4], k=10)
['green', 'red', 'green', 'white', 'white', 'white', 'green', 'white', 'red', 'white']
# weights being floats
>>> random.choices(["white", "green", "red"], [.12, .12, .04], k=10)
['white', 'white', 'green', 'green', 'red', 'red', 'white', 'green', 'white', 'green']
# weights being fractions
>>> random.choices(["white", "green", "red"], [12/100, 12/100, 4/100], k=10)
['green', 'green', 'white', 'red', 'green', 'red', 'white', 'green', 'green', 'green']

2) Se non vengono specificati né pesicum_weights , le selezioni vengono effettuate con uguale probabilità. Se viene fornita una sequenza di pesi , deve avere la stessa lunghezza della sequenza di popolazione .

Specificare sia pesi che cum_weights aumenta a TypeError.

>>> random.choices(["white", "green", "red"], k=10)
['white', 'white', 'green', 'red', 'red', 'red', 'white', 'white', 'white', 'green']

3) i cum_weights sono in genere il risultato di una itertools.accumulatefunzione che è davvero utile in tali situazioni.

Dalla documentazione collegata:

Internamente, i pesi relativi vengono convertiti in pesi cumulativi prima di effettuare le selezioni, quindi fornire i pesi cumulativi consente di risparmiare lavoro.

Quindi, fornire weights=[12, 12, 4]o cum_weights=[12, 24, 28]per il nostro caso inventato produce lo stesso risultato e quest'ultimo sembra essere più veloce / efficiente.


11

Ecco la versione che è inclusa nella libreria standard per Python 3.6:

import itertools as _itertools
import bisect as _bisect

class Random36(random.Random):
    "Show the code included in the Python 3.6 version of the Random class"

    def choices(self, population, weights=None, *, cum_weights=None, k=1):
        """Return a k sized list of population elements chosen with replacement.

        If the relative weights or cumulative weights are not specified,
        the selections are made with equal probability.

        """
        random = self.random
        if cum_weights is None:
            if weights is None:
                _int = int
                total = len(population)
                return [population[_int(random() * total)] for i in range(k)]
            cum_weights = list(_itertools.accumulate(weights))
        elif weights is not None:
            raise TypeError('Cannot specify both weights and cumulative weights')
        if len(cum_weights) != len(population):
            raise ValueError('The number of weights does not match the population')
        bisect = _bisect.bisect
        total = cum_weights[-1]
        return [population[bisect(cum_weights, random() * total)] for i in range(k)]

Fonte: https://hg.python.org/cpython/file/tip/Lib/random.py#l340


2
import numpy as np
w=np.array([ 0.4,  0.8,  1.6,  0.8,  0.4])
np.random.choice(w, p=w/sum(w))

2

Probabilmente sono troppo tardi per contribuire con qualcosa di utile, ma ecco uno snippet semplice, breve e molto efficiente:

def choose_index(probabilies):
    cmf = probabilies[0]
    choice = random.random()
    for k in xrange(len(probabilies)):
        if choice <= cmf:
            return k
        else:
            cmf += probabilies[k+1]

Non c'è bisogno di ordinare le tue probabilità o creare un vettore con il tuo cmf e termina quando trova la sua scelta. Memoria: O (1), tempo: O (N), con tempo di funzionamento medio ~ N / 2.

Se hai pesi, aggiungi semplicemente una riga:

def choose_index(weights):
    probabilities = weights / sum(weights)
    cmf = probabilies[0]
    choice = random.random()
    for k in xrange(len(probabilies)):
        if choice <= cmf:
            return k
        else:
            cmf += probabilies[k+1]

1
Diverse cose non vanno in questo. Superficialmente, ci sono alcuni nomi di variabili typoed e non c'è giustificazione data per l'utilizzo di questo corso, per esempio, np.random.choice. Ma più interessante, c'è una modalità di errore in cui ciò genera un'eccezione. Fare probabilities = weights / sum(weights)non garantisce che probabilitiessarà pari a 1; per esempio, se weightsè [1,1,1,1,1,1,1]allora probabilitiessi sommerà solo a 0.999999999999999998, inferiore al valore di ritorno più grande possibile di random.random(che è 0.999999999999999999). Allora choice <= cmfnon è mai soddisfatto.
Mark Amery,

2

Se l'elenco delle scelte ponderate è relativamente statico e si desidera un campionamento frequente, è possibile eseguire una fase di preelaborazione O (N), quindi effettuare la selezione in O (1), utilizzando le funzioni in questa risposta correlata .

# run only when `choices` changes.
preprocessed_data = prep(weight for _,weight in choices)

# O(1) selection
value = choices[sample(preprocessed_data)][0]

1

Ho guardato l'altro thread appuntito e ho trovato questa variazione nel mio stile di codifica, questo restituisce l'indice di scelta ai fini del conteggio, ma è semplice restituire la stringa (alternativa restituita commentata):

import random
import bisect

try:
    range = xrange
except:
    pass

def weighted_choice(choices):
    total, cumulative = 0, []
    for c,w in choices:
        total += w
        cumulative.append((total, c))
    r = random.uniform(0, total)
    # return index
    return bisect.bisect(cumulative, (r,))
    # return item string
    #return choices[bisect.bisect(cumulative, (r,))][0]

# define choices and relative weights
choices = [("WHITE",90), ("RED",8), ("GREEN",2)]

tally = [0 for item in choices]

n = 100000
# tally up n weighted choices
for i in range(n):
    tally[weighted_choice(choices)] += 1

print([t/sum(tally)*100 for t in tally])

1

Dipende da quante volte vuoi campionare la distribuzione.

Supponiamo di voler campionare la distribuzione K volte. Quindi, la complessità temporale che utilizza np.random.choice()ogni volta è O(K(n + log(n)))quando nè il numero di elementi nella distribuzione.

Nel mio caso, avevo bisogno di campionare la stessa distribuzione più volte dell'ordine di 10 ^ 3 dove n è dell'ordine di 10 ^ 6. Ho usato il codice seguente, che precompone la distribuzione cumulativa e lo campiona O(log(n)). La complessità temporale complessiva è O(n+K*log(n)).

import numpy as np

n,k = 10**6,10**3

# Create dummy distribution
a = np.array([i+1 for i in range(n)])
p = np.array([1.0/n]*n)

cfd = p.cumsum()
for _ in range(k):
    x = np.random.uniform()
    idx = cfd.searchsorted(x, side='right')
    sampled_element = a[idx]

1

Se ti capita di avere Python 3 e hai paura di installare numpyo scrivere i tuoi loop, potresti fare:

import itertools, bisect, random

def weighted_choice(choices):
   weights = list(zip(*choices))[1]
   return choices[bisect.bisect(list(itertools.accumulate(weights)),
                                random.uniform(0, sum(weights)))][0]

Perché puoi costruire qualsiasi cosa con un sacchetto di adattatori idraulici! Anche se ... devo ammettere che la risposta di Ned, sebbene leggermente più lunga, è più facile da capire.


0

Una soluzione generale:

import random
def weighted_choice(choices, weights):
    total = sum(weights)
    treshold = random.uniform(0, total)
    for k, weight in enumerate(weights):
        total -= weight
        if total < treshold:
            return choices[k]

0

Ecco un'altra versione di weighted_choice che utilizza numpy. Passa il vettore dei pesi e restituirà una matrice di 0 contenente un 1 che indica quale bin è stato scelto. Per impostazione predefinita, il codice esegue solo una singola estrazione, ma è possibile passare il numero di estrazioni da effettuare e verranno restituiti i conteggi per cestino estratti.

Se il vettore dei pesi non si somma a 1, verrà normalizzato in modo da farlo.

import numpy as np

def weighted_choice(weights, n=1):
    if np.sum(weights)!=1:
        weights = weights/np.sum(weights)

    draws = np.random.random_sample(size=n)

    weights = np.cumsum(weights)
    weights = np.insert(weights,0,0.0)

    counts = np.histogram(draws, bins=weights)
    return(counts[0])

0

Un altro modo per farlo, supponendo che abbiamo pesi allo stesso indice degli elementi nella matrice degli elementi.

import numpy as np
weights = [0.1, 0.3, 0.5] #weights for the item at index 0,1,2
# sum of weights should be <=1, you can also divide each weight by sum of all weights to standardise it to <=1 constraint.
trials = 1 #number of trials
num_item = 1 #number of items that can be picked in each trial
selected_item_arr = np.random.multinomial(num_item, weights, trials)
# gives number of times an item was selected at a particular index
# this assumes selection with replacement
# one possible output
# selected_item_arr
# array([[0, 0, 1]])
# say if trials = 5, the the possible output could be 
# selected_item_arr
# array([[1, 0, 0],
#   [0, 0, 1],
#   [0, 0, 1],
#   [0, 1, 0],
#   [0, 0, 1]])

Ora supponiamo, dobbiamo campionare 3 articoli in 1 prova. Si può presumere che ci siano tre sfere R, G, B presenti in grande quantità in rapporto ai loro pesi dati dall'array di pesi, il risultato potrebbe essere il seguente:

num_item = 3
trials = 1
selected_item_arr = np.random.multinomial(num_item, weights, trials)
# selected_item_arr can give output like :
# array([[1, 0, 2]])

puoi anche pensare al numero di elementi da selezionare come numero di prove binomiali / multinomiali all'interno di un set. Quindi, l'esempio sopra può ancora funzionare come

num_binomial_trial = 5
weights = [0.1,0.9] #say an unfair coin weights for H/T
num_experiment_set = 1
selected_item_arr = np.random.multinomial(num_binomial_trial, weights, num_experiment_set)
# possible output
# selected_item_arr
# array([[1, 4]])
# i.e H came 1 time and T came 4 times in 5 binomial trials. And one set contains 5 binomial trails.

0

C'è una lezione su questo di Sebastien Thurn nel corso gratuito Udacity AI for Robotics. Fondamentalmente crea una matrice circolare dei pesi indicizzati usando l'operatore mod %, imposta una variabile beta su 0, sceglie casualmente un indice, per i cicli attraverso N dove N è il numero di indici e nel ciclo for per prima cosa incrementa la beta con la formula:

beta = beta + campione uniforme da {0 ... 2 * Weight_max}

e quindi nidificato nel ciclo for, un ciclo while per sotto:

while w[index] < beta:
    beta = beta - w[index]
    index = index + 1

select p[index]

Quindi passa all'indice successivo per ricampionare in base alle probabilità (o probabilità normalizzata nel caso presentato nel corso).

Il link della lezione: https://classroom.udacity.com/courses/cs373/lessons/48704330/concepts/487480820923

Ho effettuato l'accesso a Udacity con il mio account scolastico, quindi se il collegamento non funziona, è la lezione 8, video numero 21 di Intelligenza artificiale per la robotica, dove tiene lezioni sui filtri antiparticolato.


-1

Un modo è di randomizzare sul totale di tutti i pesi e quindi utilizzare i valori come punti limite per ogni var. Ecco un'implementazione grezza come generatore.

def rand_weighted(weights):
    """
    Generator which uses the weights to generate a
    weighted random values
    """
    sum_weights = sum(weights.values())
    cum_weights = {}
    current_weight = 0
    for key, value in sorted(weights.iteritems()):
        current_weight += value
        cum_weights[key] = current_weight
    while True:
        sel = int(random.uniform(0, 1) * sum_weights)
        for key, value in sorted(cum_weights.iteritems()):
            if sel < value:
                break
        yield key

-1

Usando numpy

def choice(items, weights):
    return items[np.argmin((np.cumsum(weights) / sum(weights)) < np.random.rand())]

NumPy ha già np.random.choice, come menzionato nella risposta accettata che è stata qui dal 2014. Qual è il punto di far rotolare il tuo?
Mark Amery,

-1

Avevo bisogno di fare qualcosa del genere molto velocemente, molto semplice, dalla ricerca di idee ho finalmente creato questo modello. L'idea è ricevere i valori ponderati in una forma di json dall'API, che qui è simulato dal dict.

Quindi traducilo in un elenco in cui ogni valore si ripete in proporzione al suo peso e usa semplicemente random.choice per selezionare un valore dall'elenco.

L'ho provato con 10, 100 e 1000 iterazioni. La distribuzione sembra piuttosto solida.

def weighted_choice(weighted_dict):
    """Input example: dict(apples=60, oranges=30, pineapples=10)"""
    weight_list = []
    for key in weighted_dict.keys():
        weight_list += [key] * weighted_dict[key]
    return random.choice(weight_list)

-1

Non mi piaceva la sintassi di nessuno di questi. Volevo davvero solo specificare quali fossero gli articoli e quale fosse il peso di ciascuno. Mi rendo conto che avrei potuto usare, random.choicesma invece ho scritto rapidamente la classe qui sotto.

import random, string
from numpy import cumsum

class randomChoiceWithProportions:
    '''
    Accepts a dictionary of choices as keys and weights as values. Example if you want a unfair dice:


    choiceWeightDic = {"1":0.16666666666666666, "2": 0.16666666666666666, "3": 0.16666666666666666
    , "4": 0.16666666666666666, "5": .06666666666666666, "6": 0.26666666666666666}
    dice = randomChoiceWithProportions(choiceWeightDic)

    samples = []
    for i in range(100000):
        samples.append(dice.sample())

    # Should be close to .26666
    samples.count("6")/len(samples)

    # Should be close to .16666
    samples.count("1")/len(samples)
    '''
    def __init__(self, choiceWeightDic):
        self.choiceWeightDic = choiceWeightDic
        weightSum = sum(self.choiceWeightDic.values())
        assert weightSum == 1, 'Weights sum to ' + str(weightSum) + ', not 1.'
        self.valWeightDict = self._compute_valWeights()

    def _compute_valWeights(self):
        valWeights = list(cumsum(list(self.choiceWeightDic.values())))
        valWeightDict = dict(zip(list(self.choiceWeightDic.keys()), valWeights))
        return valWeightDict

    def sample(self):
        num = random.uniform(0,1)
        for key, val in self.valWeightDict.items():
            if val >= num:
                return key

-1

Fornire random.choice () con un elenco pre-ponderato:

Soluzione e test:

import random

options = ['a', 'b', 'c', 'd']
weights = [1, 2, 5, 2]

weighted_options = [[opt]*wgt for opt, wgt in zip(options, weights)]
weighted_options = [opt for sublist in weighted_options for opt in sublist]
print(weighted_options)

# test

counts = {c: 0 for c in options}
for x in range(10000):
    counts[random.choice(weighted_options)] += 1

for opt, wgt in zip(options, weights):
    wgt_r = counts[opt] / 10000 * sum(weights)
    print(opt, counts[opt], wgt, wgt_r)

Produzione:

['a', 'b', 'b', 'c', 'c', 'c', 'c', 'c', 'd', 'd']
a 1025 1 1.025
b 1948 2 1.948
c 5019 5 5.019
d 2008 2 2.008
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.