web-dev-qa-db-fra.com

Une version pondérée de random.choice

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.

200
Colin

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.

252
Ronan Paixão

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.

144
vishes_shell
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"
132
Ned Batchelder
  1. Organiser les poids dans une distribution cumulative.
  2. Utilisez random.random () pour sélectionner un nombre aléatoire 0.0 <= x < total.
  3. Effectuez une recherche dans la distribution en utilisant bisect.bisect , comme indiqué dans l'exemple présenté à 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, l’une pour construire les poids cumulatifs et l’autre pour bissecter à un point aléatoire.

68
Raymond Hettinger

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)
19
pweitzman

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)
16
PaulMcG

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

15
Maxime

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

11
Nickil Maveli

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

10
Raymond Hettinger

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')
3
phihag

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]
2
AShelly

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]
2
ArturJ
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
whi

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]
1
Uppinder Chugh

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

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])
1
Tony Veijalainen

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]
1
Mark

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

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)
0
Stas Baskin

Utiliser numpy

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

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