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内置函数bin() oct()等实现进制转换
Dec 30 Python
在Python中使用列表生成式的教程
Apr 27 Python
利用Anaconda完美解决Python 2与python 3的共存问题
May 25 Python
Python批量合并有合并单元格的Excel文件详解
Apr 05 Python
Python学习_几种存取xls/xlsx文件的方法总结
May 03 Python
windows10环境下用anaconda和VScode配置的图文教程
Mar 30 Python
Django 解决上传文件时,request.FILES为空的问题
May 20 Python
python如何绘制疫情图
Sep 16 Python
基于python获取本地时间并转换时间戳和日期格式
Oct 27 Python
python time()的实例用法
Nov 03 Python
pdf论文中python画的图Type 3 fonts字体不兼容的解决方案
Apr 24 Python
pytorch 实现在测试的时候启用dropout
May 27 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版(5)
2006/10/09 PHP
PHP中,文件上传
2006/12/06 PHP
thinkphp5.0自定义验证规则使用方法
2017/11/16 PHP
php 可变函数使用小结
2018/06/12 PHP
学习YUI.Ext第七日-View&amp;JSONView Part Two-一个画室网站的案例
2007/03/10 Javascript
js的压缩及jquery压缩探讨(提高页面加载性能/保护劳动成果)
2013/01/29 Javascript
JavaScript对象学习经验整理
2013/10/12 Javascript
上传图片js判断图片尺寸和格式兼容IE
2014/09/01 Javascript
关于jquery中动态增加select,事件无效的快速解决方法
2016/08/29 Javascript
微信小程序 选择器(时间,日期,地区)实例详解
2016/11/16 Javascript
jQuery设置Easyui校验规则(推荐)
2016/11/21 Javascript
js案例之鼠标跟随jquery版(实例讲解)
2017/07/21 jQuery
基于IView中on-change属性的使用详解
2018/03/15 Javascript
浅谈 Webpack 如何处理图片(开发、打包、优化)
2019/05/15 Javascript
koa2 从入门到精通(小结)
2019/07/23 Javascript
vue.js实现数据库的JSON数据输出渲染到html页面功能示例
2019/08/03 Javascript
JavaScript this关键字指向常用情况解析
2020/09/02 Javascript
在Python的web框架中编写创建日志的程序的教程
2015/04/30 Python
代码分析Python地图坐标转换
2018/02/08 Python
python selenium 对浏览器标签页进行关闭和切换的方法
2018/05/21 Python
Python中__slots__属性介绍与基本使用方法
2018/09/05 Python
Python设计模式之职责链模式原理与用法实例分析
2019/01/11 Python
详解python websocket获取实时数据的几种常见链接方式
2019/07/01 Python
Python爬虫运用正则表达式的方法和优缺点
2019/08/25 Python
利用python中集合的唯一性实现去重
2020/02/11 Python
如何提高python 中for循环的效率
2020/04/15 Python
深入浅析Python代码规范性检测
2020/07/31 Python
Python实现石头剪刀布游戏
2021/01/20 Python
基于CSS3制作立体效果导航菜单
2016/01/12 HTML / CSS
HTML5引入的新数组TypedArray介绍
2012/12/24 HTML / CSS
全球知名旅游社区法国站点:TripAdvisor法国
2016/08/03 全球购物
英国复古服装购物网站:Collectif
2019/10/30 全球购物
淘宝中秋节活动方案
2014/01/31 职场文书
员工辞退通知书
2015/04/17 职场文书
律师函格式范本
2015/05/27 职场文书
你会写请假条吗?
2019/06/26 职场文书