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使用mysqldb连接数据库操作方法示例详解
Dec 03 Python
使用Python压缩和解压缩zip文件的教程
May 06 Python
PyCharm使用教程之搭建Python开发环境
Jun 07 Python
python实现桌面壁纸切换功能
Jan 21 Python
python简单贪吃蛇开发
Jan 28 Python
Django中使用极验Geetest滑动验证码过程解析
Jul 31 Python
python自动循环定时开关机(非重启)测试
Aug 26 Python
Python3之字节串bytes与字节数组bytearray的使用详解
Aug 27 Python
python2.7实现复制大量文件及文件夹资料
Aug 31 Python
Python爬取新型冠状病毒“谣言”新闻进行数据分析
Feb 16 Python
Jupyter Notebook 文件默认目录的查看以及更改步骤
Apr 14 Python
python中pathlib模块的基本用法与总结
Aug 17 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
在PHP中使用与Perl兼容的正则表达式
2006/11/26 PHP
php执行sql语句的写法
2009/03/10 PHP
基于PHP文件操作的详细诠释
2013/06/21 PHP
PHP中SSO Cookie登录分析和实现
2015/11/06 PHP
与jquery serializeArray()一起使用的函数,主要来方便提交表单
2011/01/31 Javascript
js汉字转拼音实现代码
2013/02/06 Javascript
jQuery调用RESTful WCF示例代码(GET方法/POST方法)
2014/01/26 Javascript
IE中图片的onload事件无效问题和解决方法
2014/06/06 Javascript
微信小程序实现底部导航
2018/11/05 Javascript
Vue数据双向绑定的深入探究
2018/11/27 Javascript
Vue 后台管理类项目兼容IE9+的方法示例
2019/02/20 Javascript
Element中的Cascader(级联列表)动态加载省\市\区数据的方法
2019/03/27 Javascript
javascript系统时间设置操作示例
2019/06/17 Javascript
Element Collapse 折叠面板的使用方法
2020/07/26 Javascript
Django与遗留的数据库整合的方法指南
2015/07/24 Python
将Django框架和遗留的Web应用集成的方法
2015/07/24 Python
Python实现购物车功能的方法分析
2017/11/10 Python
Python时间戳使用和相互转换详解
2017/12/11 Python
python 编码规范整理
2018/05/05 Python
使用Python进行QQ批量登录的实例代码
2018/06/11 Python
Python 支持向量机分类器的实现
2020/01/15 Python
Python3列表List入门知识附实例
2020/02/09 Python
分享一个H5原生form表单的checkbox特效代码
2018/02/26 HTML / CSS
KIEHL’S科颜氏官方旗舰店:源自美国的顶级护肤品牌
2018/06/07 全球购物
Chicco婴儿用品美国官网:汽车座椅、婴儿推车、高脚椅等
2018/11/05 全球购物
个人工作表现评价材料
2014/09/21 职场文书
学校机关党总支领导班子整改工作方案
2014/10/26 职场文书
追悼会答谢词
2015/01/05 职场文书
骨干教师个人总结
2015/02/11 职场文书
2019年学校消防安全责任书(2篇)
2019/10/09 职场文书
两行代码解决Jupyter Notebook中文不能显示的问题
2021/04/24 Python
go语言求任意类型切片的长度操作
2021/04/26 Golang
js中Map和Set的用法及区别实例详解
2022/02/15 Javascript
一文搞清楚MySQL count(*)、count(1)、count(col)区别
2022/03/03 MySQL
Spring Boot项目如何优雅实现Excel导入与导出功能
2022/06/10 Java/Android
JavaScript设计模式之原型模式详情
2022/06/21 Javascript