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中实现对list做减法操作介绍
Jan 09 Python
python简单实现计算过期时间的方法
Jun 09 Python
Python检测网站链接是否已存在
Apr 07 Python
浅谈python函数之作用域(python3.5)
Oct 27 Python
Python实现将MySQL数据库表中的数据导出生成csv格式文件的方法
Jan 11 Python
使用python itchat包爬取微信好友头像形成矩形头像集的方法
Feb 21 Python
Python判断两个文件是否相同与两个文本进行相同项筛选的方法
Mar 01 Python
python爬虫 基于requests模块发起ajax的get请求实现解析
Aug 20 Python
利用python绘制数据曲线图的实现
Apr 09 Python
Spark处理数据排序问题如何避免OOM
May 21 Python
解决pycharm不能自动保存在远程linux中的问题
Feb 06 Python
Python爬虫之爬取某文库文档数据
Apr 21 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中的函数嵌套层数限制分析
2011/06/13 PHP
一个PHP的QRcode类与大家分享
2011/11/13 PHP
5种PHP创建数组的实例代码分享
2014/01/17 PHP
ThinkPHP5.0 图片上传生成缩略图实例代码说明
2018/06/20 PHP
Jquery+JSon 无刷新分页实现代码
2010/04/01 Javascript
JavaScript中的排序算法代码
2011/02/22 Javascript
JavaScript与DOM组合动态创建表格实例
2012/12/23 Javascript
js获取网页高度(详细整理)
2012/12/28 Javascript
JS、DOM和JQuery之间的关系示例分析
2014/04/09 Javascript
在HTML中插入JavaScript代码的示例
2015/06/03 Javascript
jquery点击缩略图切换视频播放特效代码分享
2015/09/15 Javascript
JS实现数字格式千分位相互转换方法
2016/08/01 Javascript
完美解决spring websocket自动断开连接再创建引发的问题
2017/03/02 Javascript
vue.js的提示组件
2017/03/02 Javascript
深入理解vue2.0路由如何配置问题
2017/07/18 Javascript
基于JS抓取某高校附近共享单车位置 使用web方式展示位置变化代码实例
2019/08/27 Javascript
[49:30]DOTA2-DPC中国联赛正赛 Dragon vs Dynasty BO3 第二场 3月4日
2021/03/11 DOTA
详解Python操作RabbitMQ服务器消息队列的远程结果返回
2016/06/30 Python
Python抓取框架Scrapy爬虫入门:页面提取
2017/12/01 Python
python使用mysql的两种使用方式
2018/03/07 Python
python实现俄罗斯方块游戏
2020/03/25 Python
Python语言快速上手学习方法
2018/12/14 Python
torch 中各种图像格式转换的实现方法
2019/12/26 Python
Django-imagekit的使用详解
2020/07/06 Python
python实现图片,视频人脸识别(opencv版)
2020/11/18 Python
Python 获取异常(Exception)信息的几种方法
2020/12/29 Python
简单介绍CSS3中Media Query的使用
2015/07/07 HTML / CSS
香港交友网站:be2香港
2018/07/22 全球购物
adidas马来西亚官网:adidas MY
2020/09/12 全球购物
银行实习自我鉴定
2013/10/12 职场文书
小学毕业演讲稿
2014/04/25 职场文书
项目经理任命书内容
2014/06/06 职场文书
机械专业技术员求职信
2014/06/14 职场文书
学院党的群众路线教育实践活动第一阶段情况汇报
2014/10/25 职场文书
2014年机关党建工作总结
2014/11/11 职场文书
Python字符串格式化方式
2022/04/07 Python