Genera numeri casuali con una data distribuzione (numerica)


132

Ho un file con alcune probabilità per valori diversi, ad esempio:

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

Vorrei generare numeri casuali usando questa distribuzione. Esiste un modulo esistente che gestisce questo? È abbastanza semplice programmare da soli (costruire la funzione di densità cumulativa, generare un valore casuale [0,1] e selezionare il valore corrispondente) ma sembra che questo dovrebbe essere un problema comune e probabilmente qualcuno ha creato una funzione / modulo per esso.

Ne ho bisogno perché voglio generare un elenco di compleanni (che non seguono alcuna distribuzione nel randommodulo standard ).


2
Diverso da random.choice()? Si crea l'elenco principale con il numero corretto di occorrenze e si sceglie uno. Questa è una domanda duplicata, ovviamente.
S. Lott,

1
possibile duplicato della scelta ponderata casuale
S. Lott

2
@ S.Lott non richiede molta memoria per grandi differenze nella distribuzione?
Lucas Moeskops,

2
@ S.Lott: il tuo metodo di scelta andrebbe probabilmente bene per un numero limitato di occorrenze, ma preferirei evitare di creare elenchi enormi quando non è necessario.
pafcu,

5
@ S.Lott: OK, circa 10000 * 365 = 3650000 = 3,6 milioni di elementi. Non sono sicuro dell'utilizzo della memoria in Python, ma è almeno 3,6 MB * 4 MB = 14,4 MB. Non una grande quantità, ma non qualcosa che dovresti ignorare quando c'è un metodo altrettanto semplice che non richiede la memoria aggiuntiva.
pafcu,

Risposte:


118

scipy.stats.rv_discretepotrebbe essere quello che vuoi. Puoi fornire le tue probabilità tramite il valuesparametro. È quindi possibile utilizzare il rvs()metodo dell'oggetto di distribuzione per generare numeri casuali.

Come sottolineato da Eugene Pakhomov nei commenti, puoi anche passare un pparametro di parola chiave a numpy.random.choice(), ad es

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

Se stai usando Python 3.6 o successivo, puoi usarlo random.choices()dalla libreria standard - vedi la risposta di Mark Dickinson .


9
Sulla mia macchina numpy.random.choice()è quasi 20 volte più veloce.
Eugene Pakhomov,

9
fa esattamente la stessa cosa alla domanda originale. Ad esempio:numpy.random.choice(numpy.arange(1, 7), p=[0.1, 0.05, 0.05, 0.2, 0.4, 0.2])
Eugene Pakhomov,

1
@EugenePakhomov È carino, non lo sapevo. Vedo che c'è una risposta che lo menziona ulteriormente, ma non contiene alcun codice di esempio e non ha molti voti positivi. Aggiungerò un commento a questa risposta per una migliore visibilità.
Sven Marnach,

2
Sorprendentemente, rv_discrete.rvs () funziona in O (len (p) * size) tempo e memoria! Mentre choice () sembra funzionare nel tempo ottimale O (len (p) + log (len (p)) * size).
alyaxey,

3
Se stai usando Python 3.6 o versioni successive, c'è un'altra risposta che non richiede alcun pacchetto di componenti aggiuntivi.
Mark Ransom,

113

Da Python 3.6, c'è una soluzione per questo nella libreria standard di Python, vale a dire random.choices.

Esempio di utilizzo: impostiamo una popolazione e pesi corrispondenti a quelli della domanda del 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]

Ora choices(population, weights)genera un singolo campione:

>>> choices(population, weights)
4

L'argomento facoltativo solo parola chiave kconsente di richiedere più di un campione alla volta. Questo è prezioso perché c'è del lavoro preparatorio che random.choicesdeve fare ogni volta che viene chiamato, prima di generare qualsiasi campione; generando molti campioni contemporaneamente, dobbiamo fare quel lavoro preparatorio una sola volta. Qui generiamo un milione di campioni e utilizziamo collections.Counterper verificare che la distribuzione che otteniamo corrisponda approssimativamente ai pesi che abbiamo dato.

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

Esiste una versione di Python 2.7 per questo?
abbas786,

1
@ abbas786: non integrato, ma le altre risposte a questa domanda dovrebbero funzionare su Python 2.7. Puoi anche cercare l'origine Python 3 per random.choices e copiarlo, se così inclinato.
Mark Dickinson,

27

Un vantaggio nel generare l'elenco usando CDF è che puoi usare la ricerca binaria. Mentre hai bisogno di O (n) tempo e spazio per la preelaborazione, puoi ottenere k numeri in O (k log n). Poiché i normali elenchi Python sono inefficienti, è possibile utilizzare il arraymodulo.

Se insisti nello spazio costante, puoi fare quanto segue; O (n) tempo, O (1) spazio.

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

L'ordine delle coppie (item, prob) nell'elenco è importante nella tua implementazione, giusto?
stackoverflowuser2010

1
@ stackoverflowuser2010: non dovrebbe importare (errori di modulo in virgola mobile)
sdcvvc

Bello. Ho scoperto che questo è il 30% più veloce di scipy.stats.rv_discrete.
Aspen,

1
Molte volte questa funzione genererà un KeyError a causa dell'ultima riga.
imrek,

@DrunkenMaster: non capisco. Sei consapevole di l[-1]restituire l'ultimo elemento dell'elenco?
sdcvvc,

15

Forse è un po 'tardi. Ma puoi usare numpy.random.choice(), passando il pparametro:

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

1
L'OP non vuole usare random.choice()- vedere i commenti.
pobrelkey,

5
numpy.random.choice()è completamente diverso random.choice()e supporta la distribuzione di probabilità.
Eugene Pakhomov,

14

(OK, so che stai chiedendo un involucro termoretraibile, ma forse quelle soluzioni coltivate in casa non erano abbastanza sintetiche per i tuoi gusti. :-)

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)

Ho pseudo-confermato che questo funziona osservando l'output di questa espressione:

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

Questo sembra impressionante. Giusto per mettere le cose nel contesto, ecco i risultati di 3 esecuzioni consecutive del codice sopra: ['Count of 1 with prob: 0.1 is: 113', 'Count of 2 with prob: 0.05 is: 55', 'Count of 3 con prob: 0,05 è: 50 ',' Conteggio di 4 con prob: 0,2 è: 201 ',' Conteggio di 5 con prob: 0,4 è: 388 ',' Conteggio di 6 con prob: 0,2 è: 193 ']. ............. ['Conteggio di 1 con prob: 0.1 è: 77', 'Conteggio di 2 con prob: 0.05 è: 60', 'Conteggio di 3 con prob: 0.05 è: 51 ',' Count of 4 with prob: 0.2 is: 193 ',' Count of 5 with prob: 0.4 is: 438 ',' Count of 6 with prob: 0.2 is: 181 '] ........ ..... e
Vaibhav,

['Count of 1 with prob: 0.1 is: 84', 'Count of 2 with prob: 0.05 is: 52', 'Count of 3 with prob: 0.05 is: 53', 'Count of 4 with prob: 0.2 is: 210 ',' Conteggio di 5 con prob: 0.4 è: 405 ',' Conteggio di 6 con prob: 0.2 è: 196 ']
Vaibhav

Una domanda, come posso restituire max (i ..., se "i" è un oggetto?
Vaibhav,

@Vaibhav inon è un oggetto.
Marcelo Cantos,

6

Ho scritto una soluzione per estrarre campioni casuali da una distribuzione continua personalizzata .

Ne avevo bisogno per un caso d'uso simile al tuo (ovvero generare date casuali con una data distribuzione di probabilità).

Hai solo bisogno della funtion random_custDiste della linea samples=random_custDist(x0,x1,custDist=custDist,size=1000). Il resto è decorazione ^^.

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

Distribuzione personalizzata continua e distribuzione discreta del campione

Le prestazioni di questa soluzione sono sicuramente migliorabili, ma preferisco la leggibilità.


1

Crea un elenco di elementi, in base al loro 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

Un'ottimizzazione potrebbe essere quella di normalizzare gli importi dal massimo comune divisore, per ridurre l'elenco di destinazione.

Inoltre, questo potrebbe essere interessante.


Se l'elenco di elementi è ampio, potrebbe essere necessaria molta memoria aggiuntiva.
pafcu,

@pafcu Concordato. Solo una soluzione, la seconda che mi è venuta in mente (la prima è stata quella di cercare qualcosa come "pitone probabilità di peso" :)).
Khachik,

1

Un'altra risposta, probabilmente più veloce :)

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

Verifica:

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

sulla base di altre soluzioni, si genera una distribuzione cumulativa (come numero intero o float qualunque cosa ti piace), quindi puoi usare bisect per renderlo veloce

questo è un semplice esempio (ho usato numeri interi qui)

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

il get_cdf funzione lo converte da 20, 60, 10, 10 a 20, 20 + 60, 20 + 60 + 10, 20 + 60 + 10 + 10

ora scegliamo un numero casuale fino a 20 + 60 + 10 + 10 usando random.randintquindi usiamo bisect per ottenere il valore reale in modo rapido



0

Nessuna di queste risposte è particolarmente chiara o semplice.

Ecco un metodo chiaro e semplice che è garantito per funzionare.

accumulate_normalize_probabilities prende un dizionario pche mappa i simboli a probabilità O frequenze. Produce un elenco utilizzabile di tuple da cui effettuare la selezione.

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

I rendimenti:

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

Perché funziona

L' accumulo fase di trasforma ciascun simbolo in un intervallo tra se stesso e la probabilità o la frequenza dei simboli precedenti (o 0 nel caso del primo simbolo). Questi intervalli possono essere usati per selezionare (e quindi campionare la distribuzione fornita) semplicemente scorrendo l'elenco fino a quando il numero casuale nell'intervallo 0,0 -> 1,0 (preparato in precedenza) è inferiore o uguale al punto finale dell'intervallo del simbolo corrente.

La normalizzazione ci libera dalla necessità di assicurarci che tutto sommi un certo valore. Dopo la normalizzazione, il "vettore" delle probabilità si somma a 1,0.

Il resto del codice per la selezione e la generazione di un campione arbitrariamente lungo dalla distribuzione è di seguito:

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

Utilizzo:

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

-1

Ecco un modo più efficace per farlo:

Basta chiamare la seguente funzione con l'array 'pesi' (assumendo gli indici come gli articoli corrispondenti) e il no. di campioni necessari. Questa funzione può essere facilmente modificata per gestire la coppia ordinata.

Restituisce gli indici (o gli articoli) campionati / raccolti (con sostituzione) usando le rispettive probabilità:

def resample(weights, n):
    beta = 0

    # Caveat: Assign max weight to max*2 for best results
    max_w = max(weights)*2

    # Pick an item uniformly at random, to start with
    current_item = random.randint(0,n-1)
    result = []

    for i in range(n):
        beta += random.uniform(0,max_w)

        while weights[current_item] < beta:
            beta -= weights[current_item]
            current_item = (current_item + 1) % n   # cyclic
        else:
            result.append(current_item)
    return result

Una breve nota sul concetto usato nel ciclo while. Riduciamo il peso dell'articolo corrente dalla beta cumulativa, che è un valore cumulativo costruito in modo uniforme a caso, e incrementiamo l'indice corrente per trovare l'oggetto, il cui peso corrisponde al valore della beta.

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.