【问题标题】:Random Python dictionary key, weighted by values随机 Python 字典键,按值加权
【发布时间】:2010-11-06 13:39:38
【问题描述】:

我有一个字典,其中每个键都有一个可变长度的列表,例如:

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

有没有一种干净的方法来获取随机字典键,按其值的长度加权? random.choice(d.keys()) 将平等地加权键,但在上述情况下,我希望 'a' 大约有一半的时间返回。

【问题讨论】:

标签: python random dictionary


【解决方案1】:

对于 Python 3.6+ 需要提及 random.choices

import random
raffle_dict = {"Person 1": [1,2], "Person 2": [1]}
random.choices(list(raffle_dict.keys()), [len(w[1]) for w in raffle_dict.items()], k=1)[0]

random.choices 返回一个样本列表,所以 k=1 如果您只需要一个,我们将获取列表中的第一项。如果您的字典已经有权重,只需去掉 len 或更好:

raffle_dict = {"Person 1": 1, "Person 2": 10}
random.choices(list(raffle_dict.keys()), raffle_dict.values(), k=1)[0]

另见this questionthis tutorial

【讨论】:

    【解决方案2】:
    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
    

    【讨论】:

      【解决方案3】:

      我修改了其他一些答案来提出这个问题。它的可配置性更高一些。它需要 2 个参数、一个列表和一个 lambda 函数来告诉它如何生成密钥。

      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
      

      多亏了这个的基本代码。

      【讨论】:

        【解决方案4】:

        我会这样说:

        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)
        

        【讨论】:

          【解决方案5】:

          无需构造一个新的、可能带有重复值的大列表:

          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
          

          【讨论】:

          • 我正在为我正在编写的应用程序使用类似的情况,该应用程序的性能很重要。这似乎是最有效的解决方案。
          • 对于其他使用它的人:对于 Python 3,将 itervalues() 更改为 values(),并将 iteritems() 更改为 items()
          【解决方案6】:

          这里有一些代码基于我之前为probability distribution in 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}
          

          【讨论】:

            【解决方案7】:

            你总是知道字典中值的总数吗?如果是这样,这可能很容易通过以下算法实现,只要您想从有序列表中概率性地选择某些项目,就可以使用该算法:

            1. 遍历您的键列表。
            2. 生成一个介于 0 和 1 之间的均匀分布的随机值(也称为“掷骰子”)。
            3. 假设此键有 N_VALS 个值与之关联,并且整个字典中有 TOTAL_VALS 个总值,以 N_VALS / N_REMAINING 的概率接受此键,其中 N_REMAINING 是列表中剩余的项目数。

            此算法的优点是不必生成任何新列表,如果您的字典很大,这一点很重要。您的程序只需为 K 键上的循环支付费用以计算总数,在键上的另一个循环平均会在中途结束,以及生成介于 0 和 1 之间的随机数的成本。生成这样的随机数是编程中非常常见的应用程序,因此大多数语言都可以快速实现此类功能。在 Python 中,random number generatorMersenne Twister algorithm 的 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 指出了我最初的实现中的一个严重错误,该错误已得到纠正。很抱歉!

            【讨论】:

            • 您可以在其中插入一些pythonism,但总的来说我喜欢这种方法。干得好。
            • 如果使用“水库采样”方法,您实际上不需要知道字典中值的总数。见stackoverflow.com/questions/321637/…cs.umd.edu/~samir/498/vitter.pdf
            • 这一行没有做任何事情:accept_prob = float( 1.0 / total )
            • 唯一用到的地方是:'if dice_roll
            【解决方案8】:

            鉴于您的 dict 适合内存, 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'
            

            【讨论】:

            • 是否有可能在 Python 中有一个不适合内存的字典,比如绑定的 Perl 哈希?这很有趣,但我不明白你的意思。
            • 字典可以放入内存,但是这个脚本会在网络服务器上运行,所以我想尽量减少内存使用
            • +1:这是最快最有效的方案;如果您预先计算“items”数组,它可以在 O(log |d|) 时间内做出加权随机选择
            • 致 JT 和 R:字典不是由计数组成的,它已经有枚举值。因此,枚举键列表的内存使用受(并且可能远小于)dict 本身的约束。所以我只是试图解决普遍有效的内存问题,同时指出在这种特定情况下它可能不是问题。
            【解决方案9】:

            制作一个列表,其中每个键的重复次数等于其值的长度。在您的示例中:['a', 'a', 'a', 'b', 'c', 'c']。然后使用random.choice()

            编辑:或者,不那么优雅但更有效,试试这个:取字典中所有值的长度之和,S(你可以缓存和无效这个值,或者在你编辑时保持它是最新的字典,具体取决于您预期的确切使用模式)。生成一个从 0 到 S 的随机数,并通过字典键进行线性搜索以找到您的随机数所在的范围。

            我认为这是您在不更改或添加数据表示的情况下可以做的最好的事情。

            【讨论】:

            • 我的字典可能很大,因此创建一个新列表会很昂贵。有没有更清洁的方法?
            • 这似乎不是一个好主意,因为它可能会产生大量数据
            【解决方案10】:

            这可行:

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

            【讨论】:

            • Python 是炸弹。
            • 这与大卫·塞勒的回答有同样的问题。它将使用大量内存来构建该临时列表。
            • 我非常喜欢这个解决方案,非常简洁。
            • 您可以通过使用 iteritems() 避免用 k 索引 d:random.choice([key for key, values in d.iteritems() for x in values])
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2013-09-28
            • 2015-05-25
            • 2014-06-11
            • 1970-01-01
            • 1970-01-01
            • 2021-08-02
            • 2021-08-06
            相关资源
            最近更新 更多