web-dev-qa-db-ja.com

ランダムPython辞書キー、値で重み付け

各キーに可変長のリストがある辞書があります。例:

_d = {
 'a': [1, 3, 2],
 'b': [6],
 'c': [0, 0]
}
_

値の長さで重み付けされたランダムな辞書キーを取得するクリーンな方法はありますか? random.choice(d.keys())はキーに均等に重みを付けますが、上記の場合、_'a'_が約半分の時間で返されるようにします。

34
hoju

これはうまくいくでしょう:

random.choice([k for k in d for x in d[k]])
34
sth

辞書の値の総数を常に知っていますか?もしそうなら、これは次のアルゴリズムで簡単に行うことができます。これは、順序付きリストからいくつかのアイテムを確率的に選択するときにいつでも使用できます。

  1. キーのリストを繰り返し処理します。
  2. 0から1の間で均一に分布したランダム値を生成します(別名「サイコロを振る」)。
  3. このキーにN_VALS値が関連付けられており、ディクショナリ全体にTOTAL_VALSの合計値があるとすると、確率N_VALS/N_REMAININGでこのキーを受け入れます。ここで、N_REMAININGはリストに残っているアイテムの数です。

このアルゴリズムには、新しいリストを生成する必要がないという利点があります。これは、辞書が大きい場合に重要です。あなたのプログラムは、合計を計算するための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は、私の元の実装に重大なエラーがあることを指摘しましたが、その後修正されました。すみません!

17
James Thompson

繰り返される値を持つ新しい、おそらく大きなリストを作成せずに:

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

あなたの辞書がメモリに収まることを考えると、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'
6
A. Coady

各キーがその値の長さに等しい回数繰り返されるリストを作成します。あなたの例では:_['a', 'a', 'a', 'b', 'c', 'c']_。次に、random.choice()を使用します。

編集:または、あまりエレガントではありませんが、より効率的に、これを試してください:辞書内のすべての値の長さの合計Sを取得します(この値をキャッシュして無効にするか、編集時に最新の状態に保つことができます予想される正確な使用パターンに応じて、辞書)。 0からSまでの乱数を生成し、辞書キーを線形検索して、乱数が含まれる範囲を見つけます。

これが、データ表現を変更または追加せずに実行できる最善の方法だと思います。

3
David Seiler

これは、私が以前に与えた答えに基づいたコードです 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}
1
Rex Logan

私はこれを言うだろう:

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)
1
hughdbrown

私はこれを思い付くために他の答えのいくつかを修正しました。もう少し設定可能です。キーの生成方法を指示するには、リストとラムダ関数の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に感謝します。

0
Gattster
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
0
bcosta12