web-dev-qa-db-fra.com

Générer des nombres aléatoires avec une distribution donnée (numérique)

J'ai un fichier avec quelques probabilités pour différentes valeurs, par exemple:

1 0.1
2 0.05
3 0.05
4 0.2
5 0.4
6 0.2

Je voudrais générer des nombres aléatoires en utilisant cette distribution. Un module existant qui gère cela existe-t-il? C’est assez simple de coder vous-même (construire la fonction de densité cumulative, générer une valeur aléatoire [0,1] et choisir la valeur correspondante), mais il semble que cela devrait être un problème courant et probablement que quelqu'un a créé une fonction/module pour il.

J'ai besoin de cela parce que je veux générer une liste des anniversaires (qui ne suivent aucune distribution dans le module standard random).

100
pafcu

scipy.stats.rv_discrete pourrait être ce que vous voulez. Vous pouvez fournir vos probabilités via le paramètre values. Vous pouvez ensuite utiliser la méthode rvs() de l'objet de distribution pour générer des nombres aléatoires.

Comme l'a souligné Eugene Pakhomov dans les commentaires, vous pouvez également passer un paramètre de mot clé p à numpy.random.choice() , par exemple.

numpy.random.choice(numpy.arange(1, 7), p=[0.1, 0.05, 0.05, 0.2, 0.4, 0.2])

Si vous utilisez Python 3.6 ou supérieur, vous pouvez utiliser random.choices() de la bibliothèque standard - voir le réponse de Mark Dickinson .

91
Sven Marnach

Depuis Python 3.6, il existe une solution à cela dans la bibliothèque standard de Python, à savoir random.choices .

Exemple d'utilisation: définissons une population et des poids correspondant à ceux de la question du PO:

>>> from random import choices
>>> population = [1, 2, 3, 4, 5, 6]
>>> weights = [0.1, 0.05, 0.05, 0.2, 0.4, 0.2]

Maintenant, choices(population, weights) génère un seul exemple:

>>> choices(population, weights)
4

L'argument facultatif réservé aux mots clés k permet de demander plusieurs échantillons à la fois. Ceci est précieux car il existe un travail préparatoire que random.choices Doit effectuer chaque fois qu'il est appelé, avant de générer des échantillons; en générant plusieurs échantillons à la fois, nous n’avons à faire ce travail préparatoire qu’une seule fois. Ici, nous générons un million d'échantillons et utilisons collections.Counter Pour vérifier que la distribution obtenue correspond approximativement aux poids que nous avons donnés.

>>> million_samples = choices(population, weights, k=10**6)
>>> from collections import Counter
>>> Counter(million_samples)
Counter({5: 399616, 6: 200387, 4: 200117, 1: 99636, 3: 50219, 2: 50025})
81
Mark Dickinson

Un avantage à générer la liste en utilisant CDF est que vous pouvez utiliser la recherche binaire. Bien que vous ayez besoin de O(n) temps et d’espace pour le prétraitement, vous pouvez obtenir k nombres dans O (k log n). Comme les listes Python normales ne sont pas efficaces, vous pouvez utiliser le module array.

Si vous insistez sur l’espace constant, vous pouvez procéder comme suit: O(n) temps, O(1) espace.

def random_distr(l):
    r = random.uniform(0, 1)
    s = 0
    for item, prob in l:
        s += prob
        if s >= r:
            return item
    return item  # Might occur because of floating point inaccuracies
25
sdcvvc

Peut-être qu'il est un peu tard. Mais vous pouvez utiliser numpy.random.choice() , en passant le paramètre p:

val = numpy.random.choice(numpy.arange(1, 7), p=[0.1, 0.05, 0.05, 0.2, 0.4, 0.2])
14
Ramon Martinez

(OK, je sais que vous demandez une pellicule rétractable, mais peut-être que ces solutions maison ne sont tout simplement pas assez succinctes à votre goût. :-)

pdf = [(1, 0.1), (2, 0.05), (3, 0.05), (4, 0.2), (5, 0.4), (6, 0.2)]
cdf = [(i, sum(p for j,p in pdf if j < i)) for i,_ in pdf]
R = max(i for r in [random.random()] for i,c in cdf if c <= r)

J'ai pseudo-confirmé que cela fonctionne en lorgnant la sortie de cette expression:

sorted(max(i for r in [random.random()] for i,c in cdf if c <= r)
       for _ in range(1000))
12
Marcelo Cantos

J'ai écrit une solution pour tirer des échantillons aléatoires d'une distribution continue personnalisée .

J'avais besoin de cela pour un cas d'utilisation similaire au vôtre (c'est-à-dire pour générer des dates aléatoires avec une distribution de probabilité donnée).

Vous avez juste besoin de la fonction random_custDist Et de la ligne samples=random_custDist(x0,x1,custDist=custDist,size=1000). Le reste est de la décoration ^^.

import numpy as np

#funtion
def random_custDist(x0,x1,custDist,size=None, nControl=10**6):
    #genearte a list of size random samples, obeying the distribution custDist
    #suggests random samples between x0 and x1 and accepts the suggestion with probability custDist(x)
    #custDist noes not need to be normalized. Add this condition to increase performance. 
    #Best performance for max_{x in [x0,x1]} custDist(x) = 1
    samples=[]
    nLoop=0
    while len(samples)<size and nLoop<nControl:
        x=np.random.uniform(low=x0,high=x1)
        prop=custDist(x)
        assert prop>=0 and prop<=1
        if np.random.uniform(low=0,high=1) <=prop:
            samples += [x]
        nLoop+=1
    return samples

#call
x0=2007
x1=2019
def custDist(x):
    if x<2010:
        return .3
    else:
        return (np.exp(x-2008)-1)/(np.exp(2019-2007)-1)
samples=random_custDist(x0,x1,custDist=custDist,size=1000)
print(samples)

#plot
import matplotlib.pyplot as plt
#hist
bins=np.linspace(x0,x1,int(x1-x0+1))
hist=np.histogram(samples, bins )[0]
hist=hist/np.sum(hist)
plt.bar( (bins[:-1]+bins[1:])/2, hist, width=.96, label='sample distribution')
#dist
grid=np.linspace(x0,x1,100)
discCustDist=np.array([custDist(x) for x in grid]) #distrete version
discCustDist*=1/(grid[1]-grid[0])/np.sum(discCustDist)
plt.plot(grid,discCustDist,label='custom distribustion (custDist)', color='C1', linewidth=4)
#decoration
plt.legend(loc=3,bbox_to_anchor=(1,0))
plt.show()

Continuous custom distribution and discrete sample distribution

Les performances de cette solution sont certes améliorables, mais je préfère la lisibilité.

2
Markus Dutschke

Faites une liste d’éléments, en fonction de leur weights:

items = [1, 2, 3, 4, 5, 6]
probabilities= [0.1, 0.05, 0.05, 0.2, 0.4, 0.2]
# if the list of probs is normalized (sum(probs) == 1), omit this part
prob = sum(probabilities) # find sum of probs, to normalize them
c = (1.0)/prob # a multiplier to make a list of normalized probs
probabilities = map(lambda x: c*x, probabilities)
print probabilities

ml = max(probabilities, key=lambda x: len(str(x)) - str(x).find('.'))
ml = len(str(ml)) - str(ml).find('.') -1
amounts = [ int(x*(10**ml)) for x in probabilities]
itemsList = list()
for i in range(0, len(items)): # iterate through original items
  itemsList += items[i:i+1]*amounts[i]

# choose from itemsList randomly
print itemsList

Une optimisation peut consister à normaliser les montants en fonction du plus grand diviseur commun afin de réduire la liste des cibles.

En outre, this pourrait être intéressant.

1
khachik
from __future__ import division
import random
from collections import Counter


def num_gen(num_probs):
    # calculate minimum probability to normalize
    min_prob = min(prob for num, prob in num_probs)
    lst = []
    for num, prob in num_probs:
        # keep appending num to lst, proportional to its probability in the distribution
        for _ in range(int(prob/min_prob)):
            lst.append(num)
    # all elems in lst occur proportional to their distribution probablities
    while True:
        # pick a random index from lst
        ind = random.randint(0, len(lst)-1)
        yield lst[ind]

Vérification:

gen = num_gen([(1, 0.1),
               (2, 0.05),
               (3, 0.05),
               (4, 0.2),
               (5, 0.4),
               (6, 0.2)])
lst = []
times = 10000
for _ in range(times):
    lst.append(next(gen))
# Verify the created distribution:
for item, count in Counter(lst).iteritems():
    print '%d has %f probability' % (item, count/times)

1 has 0.099737 probability
2 has 0.050022 probability
3 has 0.049996 probability 
4 has 0.200154 probability
5 has 0.399791 probability
6 has 0.200300 probability
1
Saksham Varma

Une autre réponse, probablement plus rapide :)

distribution = [(1, 0.2), (2, 0.3), (3, 0.5)]  
# init distribution  
dlist = []  
sumchance = 0  
for value, chance in distribution:  
    sumchance += chance  
    dlist.append((value, sumchance))  
assert sumchance == 1.0 # not good assert because of float equality  

# get random value  
r = random.random()  
# for small distributions use lineair search  
if len(distribution) < 64: # don't know exact speed limit  
    for value, sumchance in dlist:  
        if r < sumchance:  
            return value  
else:  
    # else (not implemented) binary search algorithm  
1
Lucas Moeskops

vous voudrez peut-être jeter un coup d'œil à NumPy distributions d'échantillonnage aléatoire

1
Manuel Salvadores

sur la base d’autres solutions, vous générez une distribution cumulative (sous forme d’entier ou de float comme vous le souhaitez), vous pouvez ensuite utiliser une bissecte pour le rendre rapide

c'est un exemple simple (j'ai utilisé des entiers ici)

l=[(20, 'foo'), (60, 'banana'), (10, 'monkey'), (10, 'monkey2')]
def get_cdf(l):
    ret=[]
    c=0
    for i in l: c+=i[0]; ret.append((c, i[1]))
    return ret

def get_random_item(cdf):
    return cdf[bisect.bisect_left(cdf, (random.randint(0, cdf[-1][0]),))][1]

cdf=get_cdf(l)
for i in range(100): print get_random_item(cdf),

le get_cdf La fonction le convertirait de 20, 60, 10, 10 en 20, 20 + 60, 20 + 60 + 10, 20 + 60 + 10 + 10

maintenant nous choisissons un nombre aléatoire allant jusqu'à 20 + 60 + 10 + 10 en utilisant random.randint nous utilisons ensuite une bissecte pour obtenir rapidement la valeur réelle

1
Muayyad Alsadi

Aucune de ces réponses n'est particulièrement claire ou simple.

Voici une méthode simple et claire qui fonctionne.

accumulate_normalize_probabilities prend un dictionnaire p qui mappe les symboles sur des probabilités OU fréquences. Il génère une liste utilisable de nuplets à partir desquels faire la sélection.

def accumulate_normalize_values(p):
        pi = p.items() if isinstance(p,dict) else p
        accum_pi = []
        accum = 0
        for i in pi:
                accum_pi.append((i[0],i[1]+accum))
                accum += i[1]
        if accum == 0:
                raise Exception( "You are about to explode the universe. Continue ? Y/N " )
        normed_a = []
        for a in accum_pi:
                normed_a.append((a[0],a[1]*1.0/accum))
        return normed_a

Rendements:

>>> accumulate_normalize_values( { 'a': 100, 'b' : 300, 'c' : 400, 'd' : 200  } )
[('a', 0.1), ('c', 0.5), ('b', 0.8), ('d', 1.0)]

Pourquoi ça marche

L'étape accumulation transforme chaque symbole en un intervalle entre lui-même et les symboles précédents, probabilité ou fréquence (ou 0 dans le cas du premier symbole). Ces intervalles peuvent être utilisés pour sélectionner (et ainsi échantillonner la distribution fournie) en parcourant simplement la liste jusqu'à ce que le nombre aléatoire dans l'intervalle 0.0 -> 1.0 (préparé précédemment) soit inférieur ou égal au point final de l'intervalle du symbole actuel.

La normalisation nous libère de la nécessité de nous assurer que tout a une valeur. Après normalisation, le "vecteur" des probabilités est égal à 1,0.

Le reste du code permettant de sélectionner et de générer un échantillon arbitrairement long à partir de la distribution est présenté ci-dessous:

def select(symbol_intervals,random):
        print symbol_intervals,random
        i = 0
        while random > symbol_intervals[i][1]:
                i += 1
                if i >= len(symbol_intervals):
                        raise Exception( "What did you DO to that poor list?" )
        return symbol_intervals[i][0]


def gen_random(alphabet,length,probabilities=None):
        from random import random
        from itertools import repeat
        if probabilities is None:
                probabilities = dict(Zip(alphabet,repeat(1.0)))
        Elif len(probabilities) > 0 and isinstance(probabilities[0],(int,long,float)):
                probabilities = dict(Zip(alphabet,probabilities)) #ordered
        usable_probabilities = accumulate_normalize_values(probabilities)
        gen = []
        while len(gen) < length:
                gen.append(select(usable_probabilities,random()))
        return gen

Utilisation:

>>> gen_random (['a','b','c','d'],10,[100,300,400,200])
['d', 'b', 'b', 'a', 'c', 'c', 'b', 'c', 'c', 'c']   #<--- some of the time
0
Cris Stringfellow