สร้างตัวเลขสุ่มด้วยการแจกแจง (ตัวเลข) ที่กำหนด


133

ฉันมีไฟล์ที่มีความน่าจะเป็นสำหรับค่าต่างๆเช่น:

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

ฉันต้องการสร้างตัวเลขสุ่มโดยใช้การแจกแจงนี้ มีโมดูลที่จัดการกับสิ่งนี้อยู่หรือไม่ การเขียนโค้ดด้วยตัวคุณเองค่อนข้างง่าย (สร้างฟังก์ชันความหนาแน่นสะสมสร้างค่าสุ่ม [0,1] และเลือกค่าที่สอดคล้องกัน) แต่ดูเหมือนว่าปัญหานี้จะเป็นปัญหาทั่วไปและอาจมีคนสร้างฟังก์ชัน / โมดูลสำหรับ มัน.

ฉันต้องการสิ่งนี้เพราะฉันต้องการสร้างรายการวันเกิด (ซึ่งไม่เป็นไปตามการแจกแจงใด ๆ ในrandomโมดูลมาตรฐาน)


2
นอกเหนือจากrandom.choice()? คุณสร้างรายการหลักด้วยจำนวนครั้งที่เหมาะสมและเลือกหนึ่งรายการ นี่เป็นคำถามที่ซ้ำกันแน่นอน
ล็อตต์


2
@ S.Lott ไม่ได้ใช้หน่วยความจำที่เข้มข้นมากสำหรับความแตกต่างในการแจกจ่าย
Lucas Moeskops

2
@ S.Lott: วิธีการที่คุณเลือกอาจใช้ได้ดีสำหรับเหตุการณ์จำนวนน้อย แต่ฉันอยากจะหลีกเลี่ยงการสร้างรายการขนาดใหญ่เมื่อไม่จำเป็น
pafcu

5
@ S.Lott: ตกลงประมาณ 10,000 * 365 = 3650000 = 3.6 ล้านองค์ประกอบ ฉันไม่แน่ใจเกี่ยวกับการใช้หน่วยความจำใน Python แต่อย่างน้อย 3.6M * 4B = 14.4MB ไม่ใช่จำนวนมาก แต่ไม่ใช่สิ่งที่คุณควรละเลยเมื่อมีวิธีการง่ายๆพอ ๆ กันที่ไม่ต้องใช้หน่วยความจำเพิ่มเติม
pafcu

คำตอบ:


119

scipy.stats.rv_discreteอาจเป็นสิ่งที่คุณต้องการ คุณสามารถระบุความน่าจะเป็นของคุณผ่านทางvaluesพารามิเตอร์ จากนั้นคุณสามารถใช้rvs()วิธีการของวัตถุการกระจายเพื่อสร้างตัวเลขสุ่ม

ตามที่ Eugene Pakhomov ระบุไว้ในความคิดเห็นคุณยังสามารถส่งผ่านpพารามิเตอร์คำหลักไปที่numpy.random.choice()เช่น

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

หากคุณกำลังใช้งูหลาม 3.6 หรือสูงกว่าคุณสามารถใช้random.choices()จากห้องสมุดมาตรฐาน - ดูคำตอบโดย Mark ดิกคินสัน


9
บนเครื่องของฉันnumpy.random.choice()เร็วขึ้นเกือบ 20 เท่า
Eugene Pakhomov

9
มันไม่เหมือนกับคำถามเดิมทุกประการ เช่น:numpy.random.choice(numpy.arange(1, 7), p=[0.1, 0.05, 0.05, 0.2, 0.4, 0.2])
Eugene Pakhomov

1
@EugenePakhomov ดีจังฉันไม่รู้ ฉันเห็นว่ามีคำตอบที่กล่าวถึงเรื่องนี้เพิ่มเติม แต่ไม่มีโค้ดตัวอย่างและไม่ได้รับการโหวตมากนัก ฉันจะเพิ่มความคิดเห็นในคำตอบนี้เพื่อการมองเห็นที่ดีขึ้น
Sven Marnach

2
น่าแปลกใจที่ rv_discrete.rvs () ทำงานใน O (len (p) * size) เวลาและหน่วยความจำ! ในขณะที่ตัวเลือก () ดูเหมือนว่าจะทำงานในเวลา O (len (p) + log (len (p)) * ขนาดที่เหมาะสมที่สุด
alyaxey

3
หากคุณใช้Python 3.6หรือใหม่กว่ามีคำตอบอื่นที่ไม่ต้องใช้แพ็คเกจเสริมใด ๆ
Mark Ransom

114

ตั้งแต่ Python 3.6 มีวิธีแก้ปัญหานี้ในไลบรารีมาตรฐานของ Python ได้แก่random.choices.

ตัวอย่างการใช้งาน: มาตั้งค่าประชากรและน้ำหนักที่ตรงกันในคำถามของ 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]

ตอนนี้choices(population, weights)สร้างตัวอย่างเดียว:

>>> choices(population, weights)
4

อาร์กิวเมนต์คำหลักเท่านั้นที่เป็นทางเลือกkช่วยให้สามารถร้องขอมากกว่าหนึ่งตัวอย่างพร้อมกัน สิ่งนี้มีค่าเนื่องจากมีงานเตรียมการบางอย่างที่random.choicesต้องทำทุกครั้งที่เรียกก่อนที่จะสร้างตัวอย่างใด ๆ ด้วยการสร้างตัวอย่างจำนวนมากพร้อมกันเราจะต้องทำงานเตรียมการนั้นเพียงครั้งเดียว ที่นี่เราสร้างตัวอย่างเป็นล้านตัวอย่างและใช้collections.Counterเพื่อตรวจสอบว่าการแจกแจงที่เราได้รับนั้นตรงกับน้ำหนักที่เราให้ไว้หรือไม่

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

มี Python 2.7 เวอร์ชันนี้หรือไม่?
abbas786

1
@ abbas786: ไม่ได้อยู่ในตัว แต่คำตอบอื่น ๆ สำหรับคำถามนี้ควรใช้ได้กับ Python 2.7 คุณยังสามารถค้นหาซอร์ส Python 3 สำหรับ random.choices และคัดลอกสิ่งนั้นหากเอียงมาก
Mark Dickinson

27

ข้อดีของการสร้างรายการโดยใช้ CDF คือคุณสามารถใช้การค้นหาแบบไบนารี ในขณะที่คุณต้องการเวลาและพื้นที่ O (n) สำหรับการประมวลผลล่วงหน้าคุณจะได้รับหมายเลข k ใน O (k log n) เนื่องจากรายการ Python ปกติไม่มีประสิทธิภาพคุณสามารถใช้arrayโมดูลได้

หากคุณยืนยันในพื้นที่คงที่คุณสามารถทำสิ่งต่อไปนี้ได้ O (n) เวลา O (1) ช่องว่าง

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

ลำดับของคู่ (item, prob) ในรายการมีความสำคัญต่อการนำไปใช้งานของคุณใช่ไหม
stackoverflowuser2010

1
@ stackoverflowuser2010: ไม่ควรสำคัญ (ข้อผิดพลาดของโมดูโลในจุดลอยตัว)
sdcvvc

ดี ฉันพบว่าสิ่งนี้เร็วกว่า scipy.stats.rv_discrete 30%
แอสเพน

1
หลายครั้งที่ฟังก์ชั่นนี้จะส่ง KeyError เนื่องจากบรรทัดสุดท้าย
imrek

@DrunkenMaster: ฉันไม่เข้าใจ คุณทราบหรือไม่ว่าl[-1]จะส่งคืนองค์ประกอบสุดท้ายของรายการ
sdcvvc

15

บางทีมันอาจจะเป็นสาย แต่คุณสามารถใช้numpy.random.choice()โดยส่งผ่านpพารามิเตอร์:

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

1
OP ไม่ต้องการใช้random.choice()- ดูความคิดเห็น
pobrelkey

5
numpy.random.choice()แตกต่างอย่างสิ้นเชิงกับrandom.choice()และสนับสนุนการแจกแจงความน่าจะเป็น
Eugene Pakhomov

14

(ตกลงฉันรู้ว่าคุณกำลังขอฟิล์มหด แต่บางทีวิธีแก้ปัญหาที่ปลูกเองที่บ้านก็ไม่กระชับเพียงพอสำหรับความชอบของคุณ :-)

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)

ฉันหลอกยืนยันว่าสิ่งนี้ได้ผลโดยการจ้องมองผลลัพธ์ของนิพจน์นี้:

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

นี่ดูน่าประทับใจ เพียงแค่ใส่สิ่งต่างๆในบริบทนี่คือผลลัพธ์จากการประหารชีวิต 3 ครั้งติดต่อกันของรหัสข้างต้น: ['Count of 1 with prob: 0.1 is: 113', 'Count of 2 with prob: 0.05 is: 55', 'Count of 3 ที่มีแนวโน้ม: 0.05 คือ: 50 ',' จำนวน 4 ที่มีแนวโน้ม: 0.2 คือ: 201 ',' จำนวน 5 ที่มีแนวโน้ม: 0.4 คือ: 388 ',' จำนวน 6 ที่มีแนวโน้ม: 0.2 คือ: 193 '] ............. ['Count of 1 with prob: 0.1 is: 77', 'Count of 2 with prob: 0.05 is: 60', 'Count of 3 with prob: 0.05 is: 51 ',' จำนวน 4 ที่มีพร็อบ: 0.2 คือ: 193 ',' จำนวน 5 ที่มีพร็อบ: 0.4 คือ: 438 ',' จำนวน 6 ที่มีพร็อบ: 0.2 คือ: 181 '] ........ ..... และ
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 ',' จำนวน 5 ที่มีแนวโน้ม: 0.4 คือ: 405 ',' จำนวน 6 ที่มีแนวโน้ม: 0.2 คือ: 196 ']
Vaibhav

คำถามฉันจะคืนค่าสูงสุดได้อย่างไร (i ... ถ้า 'i' เป็นวัตถุ
Vaibhav

@Vaibhav iไม่ใช่สิ่งของ
Marcelo Cantos

6

ผมเขียนโซลูชั่นสำหรับการวาดภาพตัวอย่างที่สุ่มจากการจัดจำหน่ายอย่างต่อเนื่องที่กำหนดเอง

ฉันต้องการสิ่งนี้สำหรับกรณีการใช้งานที่คล้ายกันกับคุณ (เช่นการสร้างวันที่สุ่มพร้อมการแจกแจงความน่าจะเป็นที่กำหนด)

คุณเพียงแค่ต้อง funtion และบรรทัดrandom_custDist samples=random_custDist(x0,x1,custDist=custDist,size=1000)ที่เหลือคือการตกแต่งครับ ^^.

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

การกระจายแบบกำหนดเองอย่างต่อเนื่องและการกระจายตัวอย่างแบบไม่ต่อเนื่อง

ประสิทธิภาพของโซลูชันนี้ไม่น่าจะเป็นไปได้อย่างแน่นอน แต่ฉันชอบความสามารถในการอ่านมากกว่า


1

จัดทำรายการตาม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

การเพิ่มประสิทธิภาพอาจเป็นการทำให้จำนวนปกติโดยตัวหารร่วมที่ยิ่งใหญ่ที่สุดเพื่อทำให้รายการเป้าหมายมีขนาดเล็กลง

นอกจากนี้สิ่งนี้อาจน่าสนใจ


หากรายการมีขนาดใหญ่อาจใช้หน่วยความจำเพิ่มเติมจำนวนมาก
pafcu

@pafcu เห็นด้วย วิธีแก้ปัญหาอย่างที่สองที่อยู่ในความคิดของฉัน (อันแรกคือการค้นหาบางอย่างเช่น "weight probability python" :))
khachik

1

อีกคำตอบน่าจะเร็วกว่านี้ :)

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]

ยืนยัน:

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

จากโซลูชันอื่นคุณสร้างการแจกแจงแบบสะสม (เป็นจำนวนเต็มหรือลอยอะไรก็ได้ที่คุณต้องการ) จากนั้นคุณสามารถใช้ bisect เพื่อทำให้เร็ว

นี่เป็นตัวอย่างง่ายๆ (ฉันใช้จำนวนเต็มที่นี่)

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

get_cdfฟังก์ชั่นจะเปลี่ยนจาก 20, 60, 10, 10 เข้าไป 20, 20 + 60, 20 + 60 + 10 20 + 60 + 10 + 10

ตอนนี้เราเลือกตัวเลขสุ่มได้สูงสุด 20 + 60 + 10 + 10 โดยใช้random.randintแล้วเราใช้ bisect เพื่อให้ได้ค่าที่แท้จริงอย่างรวดเร็ว


0

คุณอาจต้องการดูการแจกแจงการสุ่มตัวอย่าง NumPy Random


3
ฟังก์ชั่น numpy ดูเหมือนจะรองรับเฉพาะการแจกแจงจำนวน จำกัด โดยไม่รองรับการระบุของคุณเอง
pafcu

1
การเชื่อมโยงการปรับปรุงdocs.scipy.org/doc/numpy-1.14.0/reference/routines.random.htmlแทน docs.scipy.org/doc/numpy/reference/routines.random.html
Anki

0

ไม่มีคำตอบใดที่ชัดเจนหรือเรียบง่ายเป็นพิเศษ

นี่คือวิธีการง่ายๆที่ชัดเจนซึ่งรับประกันได้ว่าจะได้ผล

collect_normalize_probabilitiesใช้พจนานุกรมpที่จับคู่สัญลักษณ์กับความน่าจะเป็นหรือความถี่ มันแสดงรายการสิ่งที่สามารถใช้งานได้ซึ่งจะทำการเลือก

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

อัตราผลตอบแทน:

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

ทำไมมันถึงได้ผล

สะสมขั้นตอนที่จะเปิดแต่ละสัญลักษณ์ลงในช่วงเวลาระหว่างตัวเองและสัญลักษณ์ความน่าจะเป็นก่อนหรือความถี่ (หรือ 0 ในกรณีของสัญลักษณ์แรก) ช่วงเวลาเหล่านี้สามารถใช้เพื่อเลือกจาก (และตัวอย่างการแจกแจงที่ให้มา) โดยเพียงแค่ก้าวผ่านรายการจนกระทั่งตัวเลขสุ่มในช่วง 0.0 -> 1.0 (เตรียมไว้ก่อนหน้านี้) น้อยกว่าหรือเท่ากับจุดสิ้นสุดของช่วงเวลาของสัญลักษณ์ปัจจุบัน

การทำให้เป็นมาตรฐานทำให้เราหลุดพ้นจากความต้องการที่จะทำให้แน่ใจว่าทุกอย่างรวมเป็นมูลค่าบางอย่าง หลังจากการทำให้เป็นมาตรฐาน "เวกเตอร์" ของความน่าจะเป็นจะรวมเป็น 1.0

ส่วนที่เหลือของรหัสสำหรับการเลือกและสร้างตัวอย่างยาวโดยพลการจากการกระจายอยู่ด้านล่าง:

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

การใช้งาน:

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

นี่คือวิธีที่มีประสิทธิภาพมากขึ้นในการดำเนินการนี้:

เพียงเรียกใช้ฟังก์ชันต่อไปนี้ด้วยอาร์เรย์ 'น้ำหนัก' ของคุณ (สมมติว่าดัชนีเป็นรายการที่เกี่ยวข้อง) และเลขที่ ของตัวอย่างที่ต้องการ ฟังก์ชันนี้สามารถแก้ไขได้อย่างง่ายดายเพื่อจัดการกับคู่ที่สั่งซื้อ

ส่งคืนดัชนี (หรือรายการ) ที่สุ่มตัวอย่าง / หยิบ (พร้อมการแทนที่) โดยใช้ความน่าจะเป็นตามลำดับ:

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

หมายเหตุสั้น ๆ เกี่ยวกับแนวคิดที่ใช้ใน while loop เราลดน้ำหนักของรายการปัจจุบันจากเบต้าสะสมซึ่งเป็นค่าสะสมที่สร้างขึ้นอย่างสม่ำเสมอแบบสุ่มและเพิ่มดัชนีปัจจุบันเพื่อค้นหาสินค้าซึ่งมีน้ำหนักตรงกับค่าของเบต้า

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.