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爬虫之使用Scrapy框架编写爬虫
Nov 07 Python
Java Web开发过程中登陆模块的验证码的实现方式总结
May 25 Python
flask使用session保存登录状态及拦截未登录请求代码
Jan 19 Python
Python实现读取Properties配置文件的方法
Mar 29 Python
Python对象与引用的介绍
Jan 24 Python
Python打包模块wheel的使用方法与将python包发布到PyPI的方法详解
Feb 12 Python
Python post请求实现代码实例
Feb 28 Python
python实现控制台输出彩色字体
Apr 05 Python
Django def clean()函数对表单中的数据进行验证操作
Jul 09 Python
pytorch MSELoss计算平均的实现方法
May 12 Python
教你用Python爬取英雄联盟皮肤原画
Jun 13 Python
Pandas 数据编码的十种方法
Apr 20 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
虫族 ZERG 概述
2020/03/14 星际争霸
php+mysql 实现身份验证代码
2010/03/24 PHP
PHP中执行MYSQL事务解决数据写入不完整等情况
2014/01/07 PHP
php获取网页中图片、DIV内容的简单方法
2014/06/19 PHP
php出现内存位置访问无效错误问题解决方法
2014/08/16 PHP
Thinkphp中volist标签mod控制一定记录的换行BUG解决方法
2014/11/04 PHP
php使用递归函数实现数字累加的方法
2015/03/16 PHP
PHP Static延迟静态绑定用法分析
2016/03/16 PHP
PHP面向对象自动加载机制原理与用法分析
2016/10/14 PHP
php 可变函数使用小结
2018/06/12 PHP
在b/s开发中经常用到的javaScript技术
2006/08/23 Javascript
浅谈tudou土豆网首页图片延迟加载的效果
2010/06/23 Javascript
用jquery的方法制作一个简单的导航栏
2014/06/23 Javascript
在JS数组特定索引处指定位置插入元素的技巧
2014/08/24 Javascript
javascript进行四舍五入方法汇总
2014/12/16 Javascript
javascript判断并获取注册表中可信任站点的方法
2015/06/01 Javascript
基于JS实现回到页面顶部的五种写法(从实现到增强)
2016/09/03 Javascript
ES6中的rest参数与扩展运算符详解
2017/07/18 Javascript
详解HTML5 使用video标签实现选择摄像头功能
2017/10/25 Javascript
微信小程序实现换肤功能
2018/03/14 Javascript
angular6的table组件开发的实现示例
2018/12/26 Javascript
[44:41]Fnatic vs Liquid 2018国际邀请赛小组赛BO2 第二场 8.16
2018/08/17 DOTA
详解Python的Django框架中的templates设置
2015/05/11 Python
Python构建图像分类识别器的方法
2019/01/12 Python
Python3.5运算符操作实例详解
2019/04/25 Python
Python实战之制作天气查询软件
2019/05/14 Python
Python小程序 控制鼠标循环点击代码实例
2019/10/08 Python
python3中利用filter函数输出小于某个数的所有回文数实例
2019/11/24 Python
电大自我鉴定范文
2013/10/01 职场文书
大学生冰淇淋店商业计划书
2014/01/14 职场文书
民政局办理协议离婚(范本)
2014/10/25 职场文书
教师个人年终总结
2015/02/11 职场文书
公司考勤管理制度
2015/08/04 职场文书
宝宝满月祝酒词
2015/08/10 职场文书
springboot中一些比较常用的注解总结
2021/06/11 Java/Android
Spring Boot实现文件上传下载
2022/08/14 Java/Android