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 je suis venu avec:
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 faire quelques suggestions pour l'améliorer ou d'autres moyens de le faire. L'efficacité n'est pas aussi importante pour moi que la propreté et la lisibilité du code.
Depuis la version 1.7.0, NumPy a une fonction choice
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_distribution
est une séquence du même ordre de list_of_candidates
. Vous pouvez également utiliser le mot-clé replace=False
pour modifier le comportement de sorte que les éléments dessinés ne soient pas remplacés.
Depuis Python3.6 , il existe une méthode choices
à partir de random
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']]
Et les gens ont également mentionné qu'il y a numpy.random.choice
qui supporte les poids, MAIS il ne supporte pas Tableaux 2D , et ainsi de suite.
Alors, fondamentalement, vous pouvez obtenir ce que vous voulez (voir update ) avec le random.choices
intégré si vous avez 3.6.x Python .
UPDATE: Comme @ roganjosh aimablement mentionné, random.choices
ne peut pas renvoyer de valeurs sans remplacement, comme indiqué dans le docs :
Retourne une liste d'éléments de taille
k
choisis dans la population avec remplacement .
Et la réponse brillante de @ ronan-paixão indique que numpy.choice
a l'argument replace
, qui contrôle un tel comportement.
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"
0.0 <= x < total
.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, l’une pour construire les poids cumulatifs et l’autre pour bissecter à un point aléatoire.
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)
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()
Impressions:
[('WHITE', 904), ('GREEN', 22), ('RED', 74)]
Suppose que tous les poids sont des entiers. Ils ne doivent pas totaliser 100, je l'ai juste fait pour faciliter l'interprétation des résultats du test. (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)
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']
À partir de Python _v3.6
_, random.choices
peut être utilisé pour renvoyer un list
d'éléments de taille spécifiée à partir de la population donnée avec des pondérations optionnelles.
random.choices(population, weights=None, *, cum_weights=None, k=1)
population: list
contenant des observations uniques. (Si vide, soulève IndexError
)
pondérations: plus précisément les pondérations relatives requises pour effectuer les sélections.
cum_weights: poids cumulatifs requis pour effectuer des sélections.
k: taille (len
) du list
à afficher. (len()=1
par défaut)
Quelques mises en garde:
1) Il utilise un échantillonnage pondéré avec remplacement pour que les éléments tirés soient remplacés plus tard. Les valeurs dans la séquence de pondération en soi importent peu, mais leur rapport relatif l’importe.
Contrairement à _np.random.choice
_ qui ne peut prendre que des probabilités comme pondération et qui doit également garantir la somme des probabilités individuelles selon un critère, il n’existe pas de telle réglementation ici. Tant qu'ils appartiennent à des types numériques (_int/float/fraction
_ sauf le type Decimal
), ceux-ci fonctionneraient quand même.
_>>> 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 poids est fournie, elle doit avoir la même longueur que la séquence population.
Spécifier à la fois poids et cum_weights génère un 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 de itertools.accumulate
qui sont vraiment pratiques dans de telles situations.
De la documentation liée:
En interne, les poids relatifs sont convertis en poids cumulés avant d'effectuer des sélections. Par conséquent, fournir les poids cumulés économise du travail.
Donc, soit fournir _weights=[12, 12, 4]
_ ou _cum_weights=[12, 24, 28]
_ pour notre cas artificiel produit le même résultat et ce dernier semble être plus rapide/efficace.
Voici la version incluse dans la bibliothèque standard pour 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#l34
J'aurais besoin que la somme des choix soit 1, mais cela fonctionne quand même
def weightedChoice(choices):
# Safety check, you can remove it
for c,w in choices:
assert w >= 0
tmp = random.uniform(0, sum(c for c,w in choices))
for choice,weight in choices:
if tmp < weight:
return choice
else:
tmp -= weight
raise ValueError('Negative values in input')
Si votre liste de choix pondérés est relativement statique et que vous souhaitez effectuer un échantillonnage fréquent, vous pouvez effectuer une étape de prétraitement O(N), puis effectuer la sélection dans O (1), à l'aide des fonctions de cette réponse connexe .
# run only when `choices` changes.
preprocessed_data = prep(weight for _,weight in choices)
# O(1) selection
value = choices[sample(preprocessed_data)][0]
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]
Inutile de trier vos probabilités ou de créer un vecteur avec votre cmf, et celle-ci se termine une fois le choix effectué. Mémoire: O (1), heure: O (N), durée moyenne ~ 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]
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))
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é temporelle en utilisant np.random.choice()
à chaque fois est O(K(n + log(n)))
lorsque n
est le nombre d'éléments de la distribution.
Dans mon cas, je devais échantillonner la même distribution plusieurs fois de l'ordre de 10 ^ 3, n étant de l'ordre de 10 ^ 6. J'ai utilisé le code ci-dessous, qui calcule la distribution cumulative et l'échantille dans O(log(n))
. La complexité temporelle globale 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]
Voici une autre version de weighted_choice utilisant numpy. Passez dans le vecteur poids et il retournera un tableau de 0 contenant un 1 indiquant quel bac a été choisi. Le code par défaut consiste à faire un seul tirage au sort, mais vous pouvez indiquer le nombre de tirages à effectuer et les décomptes par bac tiré seront retournés.
Si le vecteur de pondération ne correspond pas à 1, il sera alors normalisé.
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])
J'ai regardé l'autre fil pointé et j'ai trouvé cette variation dans mon style de codage. Cela retourne l'index de choix pour le 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])
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]
Je n'ai pas aimé la syntaxe de ceux-ci. 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.choices
mais j'ai plutôt 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
J'avais besoin de faire quelque chose comme ça très vite, très simple, depuis 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 simulé ici par le dict.
Puis traduisez-le en 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)
Utiliser numpy
def choice(items, weights):
return items[np.argmin((np.cumsum(weights) / sum(weights)) < np.random.Rand())]
Une solution consiste à randomiser le total de tous les poids, puis à utiliser les valeurs comme points limites pour chaque variable. 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