Hasilkan angka acak dengan distribusi (numerik) yang diberikan

132

Saya punya file dengan beberapa probabilitas untuk nilai yang berbeda, misalnya:

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

Saya ingin menghasilkan angka acak menggunakan distribusi ini. Apakah ada modul yang menangani hal ini? Cukup mudah untuk membuat kode sendiri (membangun fungsi kepadatan kumulatif, menghasilkan nilai acak [0,1] dan memilih nilai yang sesuai) tetapi sepertinya ini harus menjadi masalah umum dan mungkin seseorang telah membuat fungsi / modul untuk Itu.

Saya memerlukan ini karena saya ingin membuat daftar ulang tahun (yang tidak mengikuti distribusi apa pun di randommodul standar ).

pafcu
sumber
2
Selain random.choice()? Anda membangun daftar master dengan jumlah kemunculan yang tepat dan memilih satu. Ini adalah pertanyaan rangkap, tentu saja.
S.Lott
1
kemungkinan duplikat dari pilihan tertimbang acak
S.Lott
2
@ S.Lott bukankah itu sangat intensif untuk perbedaan besar dalam distribusi?
Lucas Moeskops
2
@ S.Lott: Metode pilihan Anda mungkin akan baik-baik saja untuk sejumlah kecil kejadian tapi saya lebih suka menghindari membuat daftar besar ketika itu tidak perlu.
pafcu
5
@ S.Lott: Oke, sekitar 10.000 * 365 = 3650000 = 3,6 juta elemen. Saya tidak yakin tentang penggunaan memori dalam Python, tapi setidaknya 3,6M * 4B = 14,4MB. Bukan jumlah yang besar, tetapi bukan sesuatu yang harus Anda abaikan ketika ada metode yang sama-sama sederhana yang tidak memerlukan memori ekstra.
pafcu

Jawaban:

118

scipy.stats.rv_discretemungkin apa yang Anda inginkan. Anda dapat menyediakan probabilitas Anda melalui valuesparameter. Anda kemudian dapat menggunakan rvs()metode objek distribusi untuk menghasilkan angka acak.

Seperti yang ditunjukkan oleh Eugene Pakhomov di komentar, Anda juga dapat meneruskan pparameter kata kunci ke numpy.random.choice(), misalnya

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

Jika Anda menggunakan Python 3.6 atau lebih tinggi, Anda bisa menggunakan random.choices()dari pustaka standar - lihat jawabannya oleh Mark Dickinson .

Sven Marnach
sumber
9
Di mesin saya numpy.random.choice()hampir 20 kali lebih cepat.
Eugene Pakhomov
9
ia melakukan persis sama dengan pertanyaan aslinya. Misalnya:numpy.random.choice(numpy.arange(1, 7), p=[0.1, 0.05, 0.05, 0.2, 0.4, 0.2])
Eugene Pakhomov
1
@EugenePakhomov Bagus, saya tidak tahu itu. Saya dapat melihat ada jawaban yang menyebutkan hal ini lebih lanjut, tetapi tidak mengandung kode contoh apa pun dan tidak memiliki banyak upvotes. Saya akan menambahkan komentar pada jawaban ini untuk visibilitas yang lebih baik.
Sven Marnach
2
Anehnya, rv_discrete.rvs () bekerja dalam waktu dan memori O (len (p) *)! Sementara choice () tampaknya berjalan dalam waktu O (len (p) + log (len (p)) * ukuran) yang optimal.
alyaxey
3
Jika Anda menggunakan Python 3.6 atau yang lebih baru, ada jawaban lain yang tidak memerlukan paket addon.
Mark Ransom
113

Sejak Python 3.6, ada solusi untuk ini di perpustakaan standar Python, yaitu random.choices.

Contoh penggunaan: mari atur populasi dan timbangan yang cocok dengan yang ada di pertanyaan OP:

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

Sekarang choices(population, weights)menghasilkan satu sampel:

>>> choices(population, weights)
4

Argumen opsional hanya kata kunci kmemungkinkan seseorang untuk meminta lebih dari satu sampel sekaligus. Ini berharga karena ada beberapa pekerjaan persiapan yang random.choicesharus dilakukan setiap kali dipanggil, sebelum menghasilkan sampel apa pun; dengan menghasilkan banyak sampel sekaligus, kita hanya perlu melakukan pekerjaan persiapan itu sekali saja. Di sini kami menghasilkan sejuta sampel, dan digunakan collections.Counteruntuk memeriksa apakah distribusi yang kami dapatkan kurang lebih sama dengan bobot yang kami berikan.

>>> 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})
Mark Dickinson
sumber
Apakah ada versi Python 2.7 untuk ini?
abbas786
1
@ abbas786: Tidak ada di dalamnya, tetapi jawaban lain untuk pertanyaan ini harus bekerja pada Python 2.7. Anda juga bisa mencari sumber Python 3 untuk random.choices dan menyalinnya, jika mau.
Mark Dickinson
27

Keuntungan membuat daftar menggunakan CDF adalah Anda dapat menggunakan pencarian biner. Meskipun Anda membutuhkan O (n) waktu dan ruang untuk preproses, Anda bisa mendapatkan angka k dalam O (k log n). Karena daftar Python normal tidak efisien, Anda dapat menggunakan arraymodul.

Jika Anda bersikeras pada ruang konstan, Anda dapat melakukan hal berikut; O (n) waktu, O (1) ruang.

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
sdcvvc
sumber
Urutan (item, prob) pasangan dalam daftar penting dalam implementasi Anda, bukan?
stackoverflowuser2010
1
@ stackoverflowuser2010: Seharusnya tidak masalah (kesalahan modulo di floating point)
sdcvvc
Bagus. Saya menemukan ini 30% lebih cepat dari scipy.stats.rv_discrete.
Aspen
1
Cukup beberapa kali fungsi ini akan melempar KeyError karena baris terakhir.
imrek
@DrunkenMaster: Saya tidak mengerti. Apakah Anda sadar l[-1]mengembalikan elemen terakhir dari daftar?
sdcvvc
15

Mungkin agak terlambat. Tapi Anda bisa menggunakan numpy.random.choice(), melewati pparameter:

val = numpy.random.choice(numpy.arange(1, 7), p=[0.1, 0.05, 0.05, 0.2, 0.4, 0.2])
Ramon Martinez
sumber
1
OP tidak ingin menggunakan random.choice()- lihat komentar.
pobrelkey
5
numpy.random.choice()sama sekali berbeda dari random.choice()dan mendukung distribusi probabilitas.
Eugene Pakhomov
14

(Oke, saya tahu Anda meminta psikiater, tetapi mungkin solusi buatan sendiri itu tidak cukup ringkas untuk keinginan Anda. :-)

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)

Saya pseudo-confirm bahwa ini berfungsi dengan melihat keluaran ekspresi ini:

sorted(max(i for r in [random.random()] for i,c in cdf if c <= r)
       for _ in range(1000))
Marcelo Cantos
sumber
Ini terlihat mengesankan. Singkatnya, berikut adalah hasil dari 3 eksekusi berturut-turut dari kode di atas: ['Hitung 1 dengan prob: 0,1 adalah: 113', 'Hitung 2 dengan prob: 0,05 adalah: 55', 'Hitung dari 3 dengan prob: 0,05 adalah: 50 ',' Hitungan 4 dengan prob: 0,2 adalah: 201 ',' Hitungan 5 dengan prob: 0,4 adalah: 388 ',' Hitungan 6 dengan prob: 0,2 adalah: 193 ']. ............. ['Hitungan 1 dengan prob: 0,1 adalah: 77', 'Hitungan 2 dengan prob: 0,05 adalah: 60', 'Hitungan 3 dengan prob: 0,05 adalah: 51 ',' Hitungan 4 dengan prob: 0.2 adalah: 193 ',' Hitungan 5 dengan prob: 0.4 adalah: 438 ',' Hitungan 6 dengan prob: 0.2 adalah: 181 '] ........ ..... dan
Vaibhav
['Hitungan 1 dengan prob: 0,1 adalah: 84', 'Hitungan 2 dengan prob: 0,05 adalah: 52', 'Hitungan 3 dengan prob: 0,05 adalah: 53', 'Hitungan 4 dengan prob: 0,2 adalah: 210 ',' Hitung 5 dengan prob: 0,4 adalah: 405 ',' Hitung 6 dengan prob: 0,2 adalah: 196 ']
Vaibhav
Sebuah pertanyaan, bagaimana cara mengembalikan max (i ..., jika 'i' adalah objek?
Vaibhav
@Vaibhav ibukan objek.
Marcelo Cantos
6

Saya menulis solusi untuk menggambar sampel acak dari distribusi kontinu kustom .

Saya membutuhkan ini untuk kasus penggunaan yang serupa dengan milik Anda (yaitu menghasilkan tanggal acak dengan distribusi probabilitas yang diberikan).

Anda hanya perlu funtion random_custDistdan garis samples=random_custDist(x0,x1,custDist=custDist,size=1000). Sisanya adalah dekorasi ^^.

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

Distribusi kustom berkelanjutan dan distribusi sampel terpisah

Kinerja solusi ini pasti dapat ditingkatkan, tetapi saya lebih suka keterbacaan.

Markus Dutschke
sumber
1

Buat daftar barang, berdasarkan pada 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

Optimalisasi mungkin untuk menormalkan jumlah dengan pembagi umum terbesar, untuk membuat daftar target lebih kecil.

Juga, ini mungkin menarik.

khachik
sumber
Jika daftar item besar, ini mungkin menggunakan banyak memori tambahan.
pafcu
@ pafcu Setuju. Hanya sebuah solusi, yang kedua muncul di pikiran saya (yang pertama adalah mencari sesuatu seperti "weight probability python" :)).
khachik
1

Jawaban lain, mungkin lebih cepat :)

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  
Lucas Moeskops
sumber
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]

Verifikasi:

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
Saksham Varma
sumber
1

berdasarkan solusi lain, Anda menghasilkan distribusi akumulatif (sebagai bilangan bulat atau mengapung apa pun yang Anda suka), maka Anda dapat menggunakan dua bagian untuk membuatnya cepat

ini adalah contoh sederhana (saya menggunakan bilangan bulat di sini)

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

itu get_cdf fungsi akan mengubahnya dari 20, 60, 10, 10 menjadi 20, 20 + 60, 20 + 60 + 10, 20 + 60 + 10 + 10

sekarang kita memilih nomor acak hingga 20 + 60 + 10 + 10 menggunakan random.randintmaka kita menggunakan dua bagian untuk mendapatkan nilai aktual dengan cara cepat

Muayyad Alsadi
sumber
0

Tidak satu pun dari jawaban ini yang jelas atau sederhana.

Ini adalah metode yang jelas dan sederhana yang dijamin akan berhasil.

akumulasi_normalisasi_probabilitas mengambil kamus pyang memetakan simbol untuk probabilitas ATAU frekuensi. Ini menghasilkan daftar tuple yang dapat digunakan untuk melakukan seleksi.

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

Hasil:

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

Mengapa ini berhasil?

Langkah akumulasi mengubah setiap simbol menjadi interval antara dirinya dan probabilitas atau frekuensi simbol sebelumnya (atau 0 dalam kasus simbol pertama). Interval ini dapat digunakan untuk memilih dari (dan dengan demikian sampel distribusi yang disediakan) dengan hanya melangkah melalui daftar sampai angka acak dalam interval 0,0 -> 1,0 (disiapkan sebelumnya) kurang atau sama dengan titik akhir interval simbol saat ini.

The normalisasi melepaskan kita dari kebutuhan untuk memastikan semuanya jumlah untuk beberapa nilai. Setelah normalisasi, "vektor" probabilitas berjumlah 1,0.

The sisa kode untuk seleksi dan menghasilkan sampel sewenang-wenang panjang dari distribusi di bawah ini:

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

Penggunaan:

>>> 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
Cris Stringfellow
sumber
-1

Inilah cara yang lebih efektif untuk melakukan ini:

Panggil saja fungsi berikut dengan array 'bobot' Anda (dengan asumsi indeks sebagai item yang sesuai) dan no. sampel yang dibutuhkan. Fungsi ini dapat dengan mudah dimodifikasi untuk menangani pasangan yang dipesan.

Mengembalikan indeks (atau item) yang diambil / dipilih (dengan penggantian) menggunakan probabilitas masing-masing:

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

Catatan singkat tentang konsep yang digunakan dalam loop sementara. Kami mengurangi berat item saat ini dari beta kumulatif, yang merupakan nilai kumulatif yang dibangun secara seragam secara acak, dan menambah indeks saat ini untuk menemukan item, berat yang cocok dengan nilai beta.

Vaibhav
sumber