Python实现一个带权无回置随机抽选函数的方法


Posted in Python onJuly 24, 2019

需求

有一个抽奖应用,从所有参与的用户抽出K位中奖用户(K=奖品数量),且要根据每位用户拥有的抽奖码数量作为权重。

如假设有三个用户及他们的权重是: A(1), B(1), C(2)。希望抽到A的概率为25%,抽到B的概率为25%, 抽到C的概率为50%。

分析

比较直观的做法是把两个C放到列表中抽选,如[A, B, C, C], 使用Python内置的函数random.choice[A, B, C, C], 这样C抽到的概率即为50%。

这个办法的问题是权重比较大的时候,浪费内存空间。

更一般的方法是,将所有权重加和4,然后从[0, 4)区间里随机挑选一个值,将A, B, C占用不同大小的区间。[0,1)是A, [1,2)是B, [2,4)是C。

使用Python的函数random.ranint(0, 3)或者int(random.random()*4)均可产生0-3的随机整数R。判断R在哪个区间即选择哪个用户。

接下来是寻找随机数在哪个区间的方法,

一种方法是按顺序遍历列表并保存已遍历的元素权重综合S,一旦S大于R,就返回当前元素。

from operator import itemgetter

users = [('A', 1), ('B', 1), ('C', 2)]

total = sum(map(itemgetter(1), users))

rnd = int(random.random()*total) # 0~3

s = 0
for u, w in users:
  s += w
  if s > rnd:
   return u

不过这种方法的复杂度是O(N), 因为要遍历所有的users。

可以想到另外一种方法,先按顺序把累积加的权重排成列表,然后对它使用二分法搜索,二分法复杂度降到O(logN)(除去其他的处理)

users = [('A', 1), ('B', 1), ('C', 2)]

cum_weights = list(itertools.accumulate(map(itemgetter(1), users))) # [1, 2, 4]

total = cum_weights[-1]

rnd = int(random.random()*total) # 0~3

hi = len(cum_weights) - 1
index = bisect.bisect(cum_weights, rnd, 0, hi)

return users(index)[0]

Python内置库random的choices函数(3.6版本后有)即是如此实现,random.choices函数签名为 random.choices(population, weights=None, *, cum_weights=None, k=1) population是待选列表, weights是各自的权重,cum_weights是可选的计算好的累加权重(两者选一),k是抽选数量(有回置抽选)。 源码如下:

def choices(self, population, weights=None, *, cum_weights=None, k=1):
  """Return a k sized list of population elements chosen with replacement.
  If the relative weights or cumulative weights are not specified,
  the selections are made with equal probability.
  """
  random = self.random
  if cum_weights is None:
    if weights is None:
      _int = int
      total = len(population)
      return [population[_int(random() * total)] for i in range(k)]
    cum_weights = list(_itertools.accumulate(weights))
  elif weights is not None:
    raise TypeError('Cannot specify both weights and cumulative weights')
  if len(cum_weights) != len(population):
    raise ValueError('The number of weights does not match the population')
  bisect = _bisect.bisect
  total = cum_weights[-1]
  hi = len(cum_weights) - 1
  return [population[bisect(cum_weights, random() * total, 0, hi)]
      for i in range(k)]

更进一步

因为Python内置的random.choices是有回置抽选,无回置抽选函数是random.sample,但该函数不能根据权重抽选(random.sample(population, k))。

原生的random.sample可以抽选个多个元素但不影响原有的列表,其使用了两种算法实现, 保证了各种情况均有良好的性能。 (源码地址:random.sample)

第一种是部分shuffle,得到K个元素就返回。 时间复杂度是O(N),不过需要复制原有的序列,增加内存使用。

result = [None] * k
n = len(population)
pool = list(population) # 不改变原有的序列
for i in range(k):
  j = int(random.random()*(n-i))
  result[k] = pool[j]
  pool[j] = pool[n-i-1] # 已选中的元素移走,后面未选中元素填上
return result

而第二种是设置一个已选择的set,多次随机抽选,如果抽中的元素在set内,就重新再抽,无需复制新的序列。 当k相对n较小时,random.sample使用该算法,重复选择元素的概率较小。

selected = set()
selected_add = selected.add # 加速方法访问
for i in range(k):
  j = int(random.random()*n)
  while j in selected:
    j = int(random.random()*n)
  selected_add(j)
  result[j] = population[j]
return result

抽奖应用需要的是带权无回置抽选算法,结合random.choices和random.sample的实现写一个函数weighted_sample。

一般抽奖的人数都比奖品数量大得多,可选用random.sample的第二种方法作为无回置抽选,当然可以继续优化。

代码如下:

def weighted_sample(population, weights, k=1):
  """Like random.sample, but add weights.
  """
  n = len(population)
  if n == 0:
    return []
  if not 0 <= k <= n:
    raise ValueError("Sample larger than population or is negative")
  if len(weights) != n:
    raise ValueError('The number of weights does not match the population')

  cum_weights = list(itertools.accumulate(weights))
  total = cum_weights[-1]
  if total <= 0: # 预防一些错误的权重
    return random.sample(population, k=k)
  hi = len(cum_weights) - 1

  selected = set()
  _bisect = bisect.bisect
  _random = random.random
  selected_add = selected.add
  result = [None] * k
  for i in range(k):
    j = _bisect(cum_weights, _random()*total, 0, hi)
    while j in selected:
      j = _bisect(cum_weights, _random()*total, 0, hi)
    selected_add(j)
    result[i] = population[j]
  return result

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python itertools模块详解
May 09 Python
Python中存取文件的4种不同操作
Jul 02 Python
Pandas DataFrame中的tuple元素遍历的实现
Oct 23 Python
Python列表list操作相关知识小结
Jan 29 Python
python GUI库图形界面开发之PyQt5复选框控件QCheckBox详细使用方法与实例
Feb 28 Python
使用python实现时间序列白噪声检验方式
Jun 03 Python
Python2及Python3如何实现兼容切换
Sep 01 Python
通俗讲解python 装饰器
Sep 07 Python
pytorch 中forward 的用法与解释说明
Feb 26 Python
python爬虫之利用selenium模块自动登录CSDN
Apr 22 Python
使用Pytorch实现two-head(多输出)模型的操作
May 28 Python
Python利用机器学习算法实现垃圾邮件的识别
Jun 28 Python
Django的用户模块与权限系统的示例代码
Jul 24 #Python
python3字符串操作总结
Jul 24 #Python
django数据关系一对多、多对多模型、自关联的建立
Jul 24 #Python
django如何自己创建一个中间件
Jul 24 #Python
django如何通过类视图使用装饰器
Jul 24 #Python
django 类视图的使用方法详解
Jul 24 #Python
django如何实现视图重定向
Jul 24 #Python
You might like
discuz安全提问算法
2007/06/06 PHP
PHP的压缩函数实现:gzencode、gzdeflate和gzcompress的区别
2016/01/27 PHP
CI框架中类的自动加载问题分析
2016/11/21 PHP
PHP简单实现解析xml为数组的方法
2018/05/02 PHP
URL地址中的#符号使用说明
2011/02/12 Javascript
js加载读取内容及显示与隐藏div示例
2014/02/13 Javascript
使用javascript实现雪花飘落的效果
2015/01/13 Javascript
同一个网页中实现多个JavaScript特效的方法
2015/02/02 Javascript
用jQuery向div中添加Html文本内容的简单实现
2016/07/13 Javascript
浅谈JavaScript中变量和函数声明的提升
2016/08/09 Javascript
JS实现数组按升序及降序排列的方法
2017/04/26 Javascript
axios发送post请求,提交图片类型表单数据方法
2018/03/16 Javascript
vue2.0 资源文件assets和static的区别详解
2018/04/08 Javascript
实例讲解v-if和v-show的区别
2019/01/31 Javascript
javascript数组的定义及操作实例
2019/11/10 Javascript
element中的$confirm的使用
2020/04/26 Javascript
python交互式图形编程实例(一)
2017/11/17 Python
Python读写及备份oracle数据库操作示例
2018/05/17 Python
python3.4+pycharm 环境安装及使用方法
2019/06/13 Python
python中break、continue 、exit() 、pass终止循环的区别详解
2019/07/08 Python
Python3 合并二叉树的实现
2019/09/30 Python
Python实现线性插值和三次样条插值的示例代码
2019/11/13 Python
基于python requests selenium爬取excel vba过程解析
2020/08/12 Python
用sleep间隔进行python反爬虫的实例讲解
2020/11/30 Python
adidas美国官网:adidas US
2016/09/21 全球购物
活动策划邀请函
2014/02/06 职场文书
《玩具柜台前的孩子》教学反思
2014/02/13 职场文书
责任担保书范文
2014/05/21 职场文书
会展策划与管理专业求职信
2014/06/09 职场文书
教师节标语大全
2014/10/07 职场文书
四风查摆剖析材料
2014/10/10 职场文书
逃课检讨书
2015/01/26 职场文书
有关水浒传的读书笔记
2015/06/25 职场文书
2019年特色火锅店的创业计划书模板
2019/08/28 职场文书
解读Vue组件注册方式
2021/05/15 Vue.js
MySQL系列之十三 MySQL的复制
2021/07/02 MySQL