Une version pondérée de random.choice


246

J'avais besoin d'écrire une version pondérée de random.choice (chaque élément de la liste a une probabilité différente d'être sélectionné). Voici ce que j'ai trouvé:

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

Cette fonction me semble trop complexe et moche. J'espère que tout le monde ici pourra offrir des suggestions pour l'améliorer ou d'autres façons de le faire. L'efficacité n'est pas aussi importante pour moi que la propreté et la lisibilité du code.

Réponses:


297

Depuis la version 1.7.0, NumPy a une choicefonction qui prend en charge les distributions de probabilité.

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

Notez que probability_distributionc'est une séquence dans le même ordre de list_of_candidates. Vous pouvez également utiliser le mot clé replace=Falsepour modifier le comportement afin que les éléments dessinés ne soient pas remplacés.


11
D'après mes tests, c'est un ordre de grandeur plus lent que random.choicespour les appels individuels. Si vous avez besoin de beaucoup de résultats aléatoires, il est vraiment important de les choisir tous en même temps en les ajustant number_of_items_to_pick. Si vous le faites, c'est un ordre de grandeur plus rapide.
jpmc26

2
Cela ne fonctionne pas avec les tuples, etc. ("ValueError: a must be 1-dimension"), dans ce cas, on peut demander à numpy de choisir l' index dans la liste, c'est len(list_of_candidates)-à- dire , puis fairelist_of_candidates[draw]
xjcl

218

Depuis Python 3.6 il existe une méthode choicesde larandom module.

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

Notez que random.choicesl'échantillon avec remplacement , selon les documents :

Renvoyer un k liste dimensionnée d'éléments choisis dans la population avec remplacement.

Si vous avez besoin d'échantillonner sans remplacement, alors, comme l' indique la réponse brillante de @ ronan-paixão , vous pouvez utiliser numpy.choice, dont l' replaceargument contrôle un tel comportement.


4
C'est tellement plus rapide que numpy.random.choice. En choisissant parmi une liste de 8 éléments pondérés 10 000 fois, numpy.random.choice a pris 0,3286 seconde alors que random.choices a pris 0,0416 seconde, environ 8 fois plus vite.
Anton Codes

@AntonCodes Cet exemple est choisi par cerise. numpy va avoir des frais généraux à temps constant qui random.choicesne le font pas, donc bien sûr, il est plus lent sur une liste minuscule de 8 éléments, et si vous choisissez 10 000 fois dans une telle liste, vous avez raison. Mais pour les cas où la liste est plus longue (selon la façon dont vous testez, je vois des points de rupture entre 100-300 éléments), np.random.choicecommence à surperformer random.choicespar un écart assez large. Par exemple, y compris l'étape de normalisation avec l'appel numpy, j'obtiens une accélération de près de 4x random.choicespour une liste d'éléments de 10k.
ggorlen

Cela devrait être la nouvelle réponse basée sur l'amélioration des performances signalée par @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
Vous pouvez supprimer une opération et gagner un peu de temps en inversant les instructions à l'intérieur de la boucle for:upto +=w; if upto > r
knite

5
enregistrer une variable en supprimant jusqu'à et en décrémentant simplement r du poids à chaque fois. La comparaison est alorsif r < 0
JnBrymn

@JnBrymn Vous devez vérifier r <= 0. Considérons un ensemble d'entrée de 1 éléments et un rouleau de 1,0. L'assertion échouera alors. J'ai corrigé cette erreur dans la réponse.
moooeeeep

1
@Sardathrion, vous pouvez utiliser un pragma pour marquer la boucle for comme partielle:# pragma: no branch
Ned Batchelder

1
@ mLstudent33 Je n'utilise pas Udacity.
Anton Codes

70
  1. Disposez les poids dans une distribution cumulative.
  2. Utilisez random.random () pour choisir un flottant aléatoire 0.0 <= x < total.
  3. Recherchez la distribution à l'aide de bisect.bisect comme indiqué dans l'exemple à 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'

Si vous devez faire plus d'un choix, divisez-le en deux fonctions, une pour construire les poids cumulatifs et une autre pour diviser en deux jusqu'à un point aléatoire.


5
C'est plus efficace que la réponse de Ned. Fondamentalement, au lieu de faire une recherche linéaire (O (n)) parmi les choix, il fait une recherche binaire (O (log n)). +1!
NHDaly

indice de tuple hors de portée si random () arrive à retourner 1.0
Jon Vaughan

10
Cela s'exécute toujours en O(n)raison du calcul de la distribution cumulée.
Lev Levitsky

6
Cette solution est meilleure dans le cas où plusieurs appels à weighted_choice sont nécessaires pour le même ensemble de choix. Dans ce cas, vous pouvez créer la somme cumulée une fois et effectuer une recherche binaire à chaque appel.
Amos

1
@ JonVaughan random() ne peut pas renvoyer 1.0. D'après la documentation, il renvoie un résultat dans l'intervalle semi-ouvert [0.0, 1.0), c'est-à-dire qu'il peut retourner exactement 0,0, mais ne peut pas retourner exactement 1,0. La plus grande valeur qu'il peut renvoyer est 0,99999999999999988897769753748434595763683319091796875 (que Python imprime en 0,9999999999999999, et est le plus grand flottant 64 bits inférieur à 1).
Mark Amery

21

Si cela ne vous dérange pas d'utiliser numpy, vous pouvez utiliser numpy.random.choice .

Par exemple:

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

Si vous savez combien de sélections vous devez faire à l'avance, vous pouvez le faire sans boucle comme ceci:

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

15

Brut, mais peut être suffisant:

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

Est-ce que ça marche?

# 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()

Tirages:

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

Suppose que tous les poids sont des nombres entiers. Ils n'ont pas besoin d'ajouter jusqu'à 100, je viens de le faire pour rendre les résultats des tests plus faciles à interpréter. (Si les poids sont des nombres à virgule flottante, multipliez-les tous par 10 jusqu'à ce que tous les poids> = 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
Bien, je ne suis pas sûr de pouvoir supposer que tous les poids sont des entiers.
Colin

1
Il semble que vos objets soient dupliqués dans cet exemple. Ce serait inefficace (tout comme la fonction de conversion des poids en nombres entiers). Néanmoins, cette solution est une bonne doublure si les poids entiers sont petits.
wei2912

Les primitives seront dupliquées, mais les objets auront uniquement des références dupliquées, pas les objets eux-mêmes. (c'est pourquoi vous ne pouvez pas créer une liste de listes en utilisant [[]]*10- tous les éléments de la liste externe pointent vers la même liste.
PaulMcG

@PaulMcG No; rien que les références ne seront jamais dupliquées. Le système de type de Python n'a aucun concept de primitives. Vous pouvez confirmer que même avec par exemple un intvous obtenez toujours beaucoup de références au même objet en faisant quelque chose comme [id(x) for x in ([99**99] * 100)]et observez que idrenvoie la même adresse mémoire à chaque appel.
Mark Amery

14

Si vous avez un dictionnaire pondéré au lieu d'une liste, vous pouvez écrire ceci

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

Notez que [k for k in items for dummy in range(items[k])]produit cette liste['a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'c', 'b', 'b', 'b', 'b', 'b']


10
Cela fonctionne pour les petites valeurs de la population totale, mais pas pour les grands ensembles de données (par exemple, la population américaine par État finirait par créer une liste de travail contenant 300 millions d'éléments).
Ryan

@Ryan Indeed. Cela ne fonctionne pas non plus pour les poids non entiers, qui sont un autre scénario réaliste (par exemple, si vos poids sont exprimés en probabilités de sélection).
Mark Amery

12

À partir de Python v3.6, random.choicespourrait être utilisé pour renvoyer un listdes éléments de taille spécifiée à partir de la population donnée avec des poids facultatifs.

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

  • population : listcontenant des observations uniques. (Si vide, soulève IndexError)

  • poids : plus précisément les poids relatifs requis pour effectuer des sélections.

  • cum_weights : poids cumulatifs requis pour effectuer des sélections.

  • k : taille ( len) de la listsortie. (Par défaut len()=1)


Quelques mises en garde:

1) Il utilise un échantillonnage pondéré avec remplacement afin que les éléments tirés soient remplacés plus tard. Les valeurs de la séquence de poids en elles-mêmes n'ont pas d'importance, mais leur rapport relatif le fait.

Contrairement à np.random.choicece qui ne peut prendre que des probabilités comme poids et qui doit également assurer la somme des probabilités individuelles jusqu'à 1 critère, il n'y a pas de telles réglementations ici. Tant qu'ils appartiennent à des types numériques ( int/float/fractionsauf le Decimaltype), ceux-ci fonctionneront toujours.

>>> 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) Si ni poids ni cum_weights ne sont spécifiés, les sélections sont effectuées avec une probabilité égale. Si une séquence de poids est fournie, elle doit être de la même longueur que la population séquence de .

La spécification à la fois des poids et des cum_weights soulève a TypeError.

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

3) cum_weights sont généralement le résultat d'une itertools.accumulatefonction qui est vraiment pratique dans de telles situations.

De la documentation liée:

En interne, les poids relatifs sont convertis en poids cumulatifs avant d'effectuer des sélections, donc la fourniture des poids cumulatifs économise du travail.

Donc, soit l'approvisionnement weights=[12, 12, 4]soit cum_weights=[12, 24, 28]notre cas artificiel produit le même résultat et ce dernier semble être plus rapide / efficace.


11

Voici la version qui est incluse dans la bibliothèque standard de 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)]

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

Je suis probablement trop tard pour apporter quelque chose d'utile, mais voici un extrait simple, court et très efficace:

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]

Pas besoin de trier vos probabilités ou de créer un vecteur avec votre cmf, et il se termine une fois qu'il a trouvé son choix. Mémoire: O (1), temps: O (N), avec un temps de fonctionnement moyen ~ N / 2.

Si vous avez des poids, ajoutez simplement une ligne:

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
Plusieurs choses ne vont pas avec cela. Superficiellement, il y a des noms de variables typés et aucune justification n'est donnée pour l'utiliser par exemple np.random.choice. Mais plus intéressant, il existe un mode de défaillance où cela déclenche une exception. Faire probabilities = weights / sum(weights)ne garantit pas que cela probabilitiesse résumera à 1; par exemple, si weightsest [1,1,1,1,1,1,1]alors probabilitiesne fera que la somme de 0,9999999999999998, inférieure à la plus grande valeur de retour possible random.random(qui est 0,9999999999999999). Alors choice <= cmfn'est jamais satisfait.
Mark Amery

2

Si votre liste de choix pondérés est relativement statique et que vous souhaitez un échantillonnage fréquent, vous pouvez effectuer une étape de prétraitement O (N), puis effectuer la sélection dans O (1), en utilisant les fonctions de cette réponse associée .

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

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

1

J'ai regardé l'autre thread pointé et j'ai trouvé cette variation dans mon style de codage, cela retourne l'index de choix à des fins de comptage, mais il est simple de renvoyer la chaîne (alternative de retour commentée):

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

Cela dépend du nombre de fois que vous souhaitez échantillonner la distribution.

Supposons que vous souhaitiez échantillonner la distribution K fois. Ensuite, la complexité de temps à l' aide np.random.choice()chaque fois est O(K(n + log(n)))quand nest le nombre d'éléments dans la distribution.

Dans mon cas, j'ai dû échantillonner la même distribution plusieurs fois de l'ordre de 10 ^ 3 où n est de l'ordre de 10 ^ 6. J'ai utilisé le code ci-dessous, qui pré-calcule la distribution cumulative et l'échantillonne O(log(n)). La complexité globale du temps est 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

Si vous possédez Python 3 et avez peur d'installer numpyou d'écrire vos propres boucles, vous pouvez faire:

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]

Parce que vous pouvez tout construire à partir d'un sac d'adaptateurs de plomberie! Bien que ... je dois admettre que la réponse de Ned, bien que légèrement plus longue, est plus facile à comprendre.


0

Une solution générale:

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

Voici une autre version de weighted_choice qui utilise numpy. Passez le vecteur de poids et il renverra un tableau de 0 contenant un 1 indiquant quel bac a été choisi. Par défaut, le code ne fait qu'un seul tirage, mais vous pouvez transmettre le nombre de tirages à effectuer et les décomptes par bac tiré seront retournés.

Si le vecteur de poids n'est pas égal à 1, il sera normalisé pour qu'il le fasse.

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

Une autre façon de procéder, en supposant que nous avons des poids au même index que les éléments du tableau d'éléments.

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

Supposons maintenant que nous devons échantillonner 3 éléments dans 1 essai. Vous pouvez supposer qu'il y a trois boules R, G, B présentes en grande quantité par rapport à leurs poids donnés par le tableau de poids, le résultat suivant pourrait être possible:

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

vous pouvez également penser le nombre d'éléments à sélectionner comme le nombre d'essais binomiaux / multinomiaux dans un ensemble. Ainsi, l'exemple ci-dessus peut toujours fonctionner comme

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

Il y a une conférence à ce sujet par Sébastien Thurn dans le cours gratuit Udacity AI for Robotics. Fondamentalement, il fait un tableau circulaire des poids indexés à l'aide de l'opérateur mod %, définit une variable bêta à 0, choisit au hasard un index, pour les boucles via N où N est le nombre d'indices et dans la boucle for, incrémente d'abord bêta par la formule:

beta = beta + échantillon uniforme de {0 ... 2 * Weight_max}

puis imbriqué dans la boucle for, une boucle while ci-dessous:

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

select p[index]

Passez ensuite à l'indice suivant pour rééchantillonner en fonction des probabilités (ou probabilité normalisée dans le cas présenté dans le cours).

Le lien de la conférence: https://classroom.udacity.com/courses/cs373/lessons/48704330/concepts/487480820923

Je suis connecté à Udacity avec mon compte d'école donc si le lien ne fonctionne pas, c'est la leçon 8, vidéo numéro 21 d'Intelligence Artificielle pour la Robotique où il donne des cours sur les filtres à particules.


-1

Une façon consiste à randomiser sur le total de tous les poids, puis à utiliser les valeurs comme points limites pour chaque var. Voici une implémentation brute en tant que générateur.

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

Utiliser numpy

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

NumPy a déjà np.random.choice, comme mentionné dans la réponse acceptée qui est ici depuis 2014. Quel est l'intérêt de rouler le vôtre?
Mark Amery

-1

J'avais besoin de faire quelque chose comme ça très rapidement très simple, à partir de la recherche d'idées, j'ai finalement construit ce modèle. L'idée est de recevoir les valeurs pondérées sous la forme d'un json de l'api, qui est ici simulé par le dict.

Ensuite, traduisez-le dans une liste dans laquelle chaque valeur se répète proportionnellement à son poids, et utilisez simplement random.choice pour sélectionner une valeur dans la liste.

Je l'ai essayé avec 10, 100 et 1000 itérations. La distribution semble assez solide.

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

Je n'aimais pas la syntaxe de ceux-là. Je voulais vraiment préciser quels étaient les articles et quelle était leur pondération. Je me rends compte que j'aurais pu utiliser random.choicesmais à la place j'ai rapidement écrit le cours ci-dessous.

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

Fournissez à random.choice () une liste pré-pondérée:

Solution et 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)

Production:

['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
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.