各キーに可変長のリストがある辞書があります。例:
_d = {
'a': [1, 3, 2],
'b': [6],
'c': [0, 0]
}
_
値の長さで重み付けされたランダムな辞書キーを取得するクリーンな方法はありますか? random.choice(d.keys())
はキーに均等に重みを付けますが、上記の場合、_'a'
_が約半分の時間で返されるようにします。
これはうまくいくでしょう:
random.choice([k for k in d for x in d[k]])
辞書の値の総数を常に知っていますか?もしそうなら、これは次のアルゴリズムで簡単に行うことができます。これは、順序付きリストからいくつかのアイテムを確率的に選択するときにいつでも使用できます。
このアルゴリズムには、新しいリストを生成する必要がないという利点があります。これは、辞書が大きい場合に重要です。あなたのプログラムは、合計を計算するためのKキーのループ、平均して途中で終了するキーの別のループ、および0から1の間の乱数を生成するためにかかる費用のみを支払います。このような乱数の生成はプログラミングで非常に一般的なアプリケーションであるため、ほとんどの言語にはそのような関数の高速実装があります。 In Python 乱数ジェネレーターMersenne Twisterアルゴリズム のC実装、これは非常に高速である必要があります。さらに、ドキュメントには次のように記載されています。この実装はスレッドセーフです。
これがコードです。より多くのPythonic機能を使用したい場合は、クリーンアップできると確信しています。
#!/usr/bin/python
import random
def select_weighted( d ):
# calculate total
total = 0
for key in d:
total = total + len(d[key])
accept_prob = float( 1.0 / total )
# pick a weighted value from d
n_seen = 0
for key in d:
current_key = key
for val in d[key]:
dice_roll = random.random()
accept_prob = float( 1.0 / ( total - n_seen ) )
n_seen = n_seen + 1
if dice_roll <= accept_prob:
return current_key
dict = {
'a': [1, 3, 2],
'b': [6],
'c': [0, 0]
}
counts = {}
for key in dict:
counts[key] = 0
for s in range(1,100000):
k = select_weighted(dict)
counts[k] = counts[k] + 1
print counts
これを100回実行した後、選択キーを次の回数取得します。
{'a': 49801, 'c': 33548, 'b': 16650}
これらは、次の期待値にかなり近いものです。
{'a': 0.5, 'c': 0.33333333333333331, 'b': 0.16666666666666666}
編集:Milesは、私の元の実装に重大なエラーがあることを指摘しましたが、その後修正されました。すみません!
繰り返される値を持つ新しい、おそらく大きなリストを作成せずに:
def select_weighted(d):
offset = random.randint(0, sum(d.itervalues())-1)
for k, v in d.iteritems():
if offset < v:
return k
offset -= v
あなたの辞書がメモリに収まることを考えると、random.choiceメソッドは合理的であるはずです。しかし、そうでないと仮定すると、次の手法は、増加する重みのリストを使用し、bisectを使用してランダムに選択された重みを見つけることです。
>>> import random, bisect
>>> items, total = [], 0
>>> for key, value in d.items():
total += len(value)
items.append((total, key))
>>> items[bisect.bisect_left(items, (random.randint(1, total),))][1]
'a'
>>> items[bisect.bisect_left(items, (random.randint(1, total),))][1]
'c'
各キーがその値の長さに等しい回数繰り返されるリストを作成します。あなたの例では:_['a', 'a', 'a', 'b', 'c', 'c']
_。次に、random.choice()
を使用します。
編集:または、あまりエレガントではありませんが、より効率的に、これを試してください:辞書内のすべての値の長さの合計S
を取得します(この値をキャッシュして無効にするか、編集時に最新の状態に保つことができます予想される正確な使用パターンに応じて、辞書)。 0からSまでの乱数を生成し、辞書キーを線形検索して、乱数が含まれる範囲を見つけます。
これが、データ表現を変更または追加せずに実行できる最善の方法だと思います。
これは、私が以前に与えた答えに基づいたコードです Pythonでの確率分布 が、長さを使用して重みを設定しています。反復マルコフ連鎖を使用するため、すべての重みの合計が何であるかを知る必要はありません。現在、最大長を計算しますが、それが遅すぎる場合は変更するだけです
self._maxw = 1
に
self._maxw = max lenght
削除します
for k in self._odata:
if len(self._odata[k])> self._maxw:
self._maxw=len(self._odata[k])
これがコードです。
import random
class RandomDict:
"""
The weight is the length of each object in the dict.
"""
def __init__(self,odict,n=0):
self._odata = odict
self._keys = list(odict.keys())
self._maxw = 1 # to increase speed set me to max length
self._len=len(odict)
if n==0:
self._n=self._len
else:
self._n=n
# to increase speed set above max value and comment out next 3 lines
for k in self._odata:
if len(self._odata[k])> self._maxw:
self._maxw=len(self._odata[k])
def __iter__(self):
return self.next()
def next(self):
while (self._len > 0) and (self._n>0):
self._n -= 1
for i in range(100):
k=random.choice(self._keys)
rx=random.uniform(0,self._maxw)
if rx <= len(self._odata[k]): # test to see if that is the value we want
break
# if you do not find one after 100 tries then just get a random one
yield k
def GetRdnKey(self):
for i in range(100):
k=random.choice(self._keys)
rx=random.uniform(0,self._maxw)
if rx <= len(self._odata[k]): # test to see if that is the value we want
break
# if you do not find one after 100 tries then just get a random one
return k
#test code
d = {
'a': [1, 3, 2],
'b': [6],
'c': [0, 0]
}
rd=RandomDict(d)
dc = {
'a': 0,
'b': 0,
'c': 0
}
for i in range(100000):
k=rd.GetRdnKey()
dc[k]+=1
print("Key count=",dc)
#iterate over the objects
dc = {
'a': 0,
'b': 0,
'c': 0
}
for k in RandomDict(d,100000):
dc[k]+=1
print("Key count=",dc)
試験結果
Key count= {'a': 50181, 'c': 33363, 'b': 16456}
Key count= {'a': 50080, 'c': 33411, 'b': 16509}
私はこれを言うだろう:
random.choice("".join([k * len(d[k]) for k in d]))
これにより、dの各kがその値の長さと同じ数のチャンスを得ることが明らかになります。もちろん、文字である長さ1の辞書キーに依存しています。
かなり後に:
table = "".join([key * len(value) for key, value in d.iteritems()])
random.choice(table)
私はこれを思い付くために他の答えのいくつかを修正しました。もう少し設定可能です。キーの生成方法を指示するには、リストとラムダ関数の2つの引数が必要です。
def select_weighted(lst, weight):
""" Usage: select_weighted([0,1,10], weight=lambda x: x) """
thesum = sum([weight(x) for x in lst])
if thesum == 0:
return random.choice(lst)
offset = random.randint(0, thesum - 1)
for k in lst:
v = weight(k)
if offset < v:
return k
offset -= v
このためのベースコードを提供してくれたsthに感謝します。
import numpy as np
my_dict = {
"one": 5,
"two": 1,
"three": 25,
"four": 14
}
probs = []
elements = [my_dict[x] for x in my_dict.keys()]
total = sum(elements)
probs[:] = [x / total for x in elements]
r = np.random.choice(len(my_dict), p=probs)
print(list(my_dict.values())[r])
# 25