我正在参加「掘金·启航计划」
摘要
年年公司年会都是阳光普照奖,真的是狗,抽奖逻辑还是自己写的,道理给谁讲啊,难道幸运值这么差?(欢迎大家在评论区给我提供新的抽奖玩法思路)
所以来探讨分析常用的几种抽奖算法和数据分析的关系,如何设计公平的抽奖算法,并运用数据分析方法对抽奖结果进行评估。文章首先介绍了几种常见的抽奖算法,包括随机抽奖、概率抽奖等,重点分析了它们的优劣和适用场景。
最后,文章介绍了数据分析在抽奖算法中的应用,包括如何收集、处理和分析抽奖数据,以评估抽奖结果的公平性和合理性。数据分析可以帮助我们发现潜在的问题和漏洞,提高抽奖的质量和公平性。
公司年会中奖率分析
基础数据
奖品数量:95个(特等奖、1-6等奖)
抽奖人数:380人
中奖率:95 / 380 = 0.25 (25%中奖率已经算很高了,可惜中奖名单里就是没有我)
抽奖算法
常见抽奖算法
随机抽取算法
随机抽奖算法是一种通过随机数生成器来抽取获奖者的算法。该算法通常从一个参与者列表中随机选择一个获奖者,确保抽奖过程是公平的,没有人为干预的可能性。该算法可以简单地将参与者列表中的每个人平等地分配获胜概率,然后通过生成随机数来确定获奖者。
function uniformDistribution(totalParticipants) {
// 生成0到1之间的随机数
const randomNum = Math.random();
// 将0到1之间的范围等分,每一份的大小为1/totalParticipants
const range = 1 / totalParticipants;
// 计算中奖者的编号
const winnerIndex = Math.floor(randomNum / range);
return winnerIndex;
}
// 抽奖人数
const totalParticipants = 100;
// 中奖者编号
const winnerIndex = uniformDistribution(totalParticipants);
console.log(`中奖者编号为: ${winnerIndex}`);
优点:
- 简单易行,是随机抽样的基础。
- 其他随机抽样方式的基石,如分层抽样。
- 检验其他随机抽样好坏的依据。
- 当总体容量较大时,能够解决编号困难的问题。
- 当总体标志变异程度较大时,简单随机抽样的样本的代表性比分层抽样好。
- 当总体各单位之间较为分散时,使用简单随机抽样比较方便。
- 能够得到一个成比例的样本。
- 实施灵活方便,还可以依托各级行政管理机构进行组织与实行。
- 样本在总体中分布比较均匀,样本代表性较高。
- 有利于降低抽样误差,提高调查精度。
缺点:
- 当总体中个体数过多时,对总体单位进行编号会比较困难。
- 当总体标志变异程度较大时,简单随机抽样的样本的代表性比分层抽样小。
- 当总体各单位之间较为分散时,使用简单随机抽样会比较困难。
- 需要提前知道总体单位的详细信息,当要依据有关标志排队时,需要更为详细的信息,会比较复杂。
- 当间隔和调查对象的循环周期重合时会影响调查的精度。
- 抽样误差的计算较为困难。
均匀分布算法
均匀分布算法是一种随机抽奖算法,它通过将奖品或奖项均匀地分配到参与者列表中,确保每个参与者都有相同的获胜概率。该算法的实现方式是将参与者列表中的每个人平等地分配获胜概率,然后通过生成随机数来确定获奖者。具体来说,该算法需要一个参与者列表和一个奖品列表,然后按照参与者列表中每个元素的个数作为权重来分配奖品列表中的奖品。
function uniformDistribution(totalParticipants) {
// 生成0到1之间的随机数
const randomNum = Math.random();
// 将0到1之间的范围等分,每一份的大小为1/totalParticipants
const range = 1 / totalParticipants;
// 计算中奖者的编号
const winnerIndex = Math.floor(randomNum / range);
return winnerIndex;
}
// 抽奖人数
const totalParticipants = 100;
// 中奖者编号
const winnerIndex = uniformDistribution(totalParticipants);
console.log(`中奖者编号为: ${winnerIndex}`);
优点:
- 简单易行,能够保证每个参与者都有相同的获胜概率。
- 适用于参与者数量较多且获胜概率相等的场景。
- 抽样过程透明,不会受到任何主观因素的影响。
- 由于该方法使用的概率是相同的,产生的样本结果可以以一定的置信度来进行统计分析。
缺点:
- 当参与者数量较少时,该算法的代表性可能会下降,因为每个参与者的获奖概率会受到其他参与者的数量和位置的影响。
- 需要知道参与者的详细信息,当要依据有关标志排队时,需要更为详细的信息,会比较复杂。
- 必须有完整样本档案,样本来自总体,因此需要总体数据完整,才能保证样本抽取的正确性。
概率抽奖算法
概率抽奖算法是一种基于随机数的抽奖算法,它通过一定的概率分布来决定获奖者。具体来说,该算法需要根据奖品的数量和参与抽奖的人数,制定一个概率分布表,表中列出每个参与者获奖的概率和对应的编号或姓名。然后,通过生成随机数来确定获奖者,即根据概率分布表中的概率值来选择获奖者。
function probabilityLottery(probabilities) {
// 计算所有概率的总和
const totalProbability = probabilities.reduce((acc, cur) => acc + cur, 0);
// 生成0到总概率之间的随机数
const randomNum = Math.random() * totalProbability;
// 遍历所有概率,计算中奖者的编号
let winnerIndex = -1;
let sum = 0;
for (let i = 0; i < probabilities.length; i++) {
sum += probabilities[i];
if (randomNum < sum) {
winnerIndex = i;
break;
}
}
return winnerIndex;
}
// 抽奖人数
const totalParticipants = 100;
// 每个参与者的中奖概率
const probabilities = new Array(totalParticipants).fill(1 / totalParticipants);
// 中奖者编号
const winnerIndex = probabilityLottery(probabilities);
console.log(`中奖者编号为: ${winnerIndex}`);
优点:
- 可以根据具体情况制定不同的概率分布,使得每个参与者都有一定的获奖概率,可以提高参与者的积极性。
- 可以实现自动化抽奖,提高抽奖效率。
缺点:
- 概率分布表的制定需要考虑多种因素,如奖品数量、参与人数、中奖概率等,较为复杂。
- 概率抽奖算法适用于高概率奖品数量大于参与人数的情况。
- 当最大概率的奖品抽完了,根据概率论原理,之后会出现一直抽到但奖品没有的情况。
总之,概率抽奖算法的优点是能够提高参与者的积极性和抽奖效率,但缺点是概率分布表制定较为复杂,且在奖品数量和参与人数等方面有较高的要求。
洗牌算法
洗牌算法是一种随机重排数组元素的算法,可以用来解决很多随机性问题,例如在n个不同的数中随机取出m个数(m<n)等。洗牌算法的实现方式有很多种,其中比较常用的有Fisher-Yates shuffle和Knuth-Durstenfeld shuffle等。
Fisher-Yates shuffle算法的基本思想是从数组的最后一个元素开始,依次取前面未处理过的元素,随机将其与当前元素交换位置,直到所有元素都处理完毕。该算法的时间复杂度为O(n),空间复杂度为O(1)。
Knuth-Durstenfeld shuffle算法的基本思想是在原始数组上对数字进行交互,省去了额外O(n)的空间。该算法的时间复杂度也为O(n),但空间复杂度为O(n)。
洗牌算法的应用非常广泛,例如在游戏开发中可以用来随机生成地图、敌人等;在数据分析中可以用来随机选择样本等。
function fisherYatesShuffle(participants) {
const arr = participants.slice(); // 复制一份参与者列表,避免修改原数组
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1)); // 生成0到i之间的随机整数
[arr[i], arr[j]] = [arr[j], arr[i]]; // 交换第i个和第j个元素
}
return arr;
}
function fisherYatesLottery(participants, winnersCount) {
// 随机洗牌参与者列表
const shuffledParticipants = fisherYatesShuffle(participants);
// 取前winnersCount个参与者作为中奖者
const winners = shuffledParticipants.slice(0, winnersCount);
return winners;
}
// 抽奖人数
const totalParticipants = 100;
// 中奖人数
const winnersCount = 5;
// 参与者列表
const participants = new Array(totalParticipants).fill().map((_, i) => i + 1);
// 中奖者列表
const winners = fisherYatesLottery(participants, winnersCount);
console.log(`中奖者列表为: ${winners}`);
优点:
- 时间复杂度低:洗牌算法的时间复杂度为O(n),不需要额外的空间。
- 应用广泛:洗牌算法可以用于随机生成地图、敌人等游戏开发中的场景,也可以用于随机选择样本等数据分析中的场景。
缺点:
- 无法保证每个数出现在所有位置上的概率相等:洗牌算法是一种随机重排数组元素的算法,无法保证每个数出现在所有位置上的概率相等。
- 额外开辟了一个List:洗牌算法需要额外开辟一个List来存储随机数,这会增加算法的复杂度。
- 元素移动操作可能导致性能下降:洗牌算法在实现时需要将元素移动到新的位置,这可能会导致性能下降。
公司年会抽奖算法
- 每轮抽奖会将抽奖时间的时间戳(如:1677401017990)和幸运数字(抽奖嘉宾现场说出1-999中的数字)相加,得到一个13位的抽奖数字
- 将抽奖数字整除以签到总人数,取余数,该余数即为中奖用户的中奖标识
- 如中奖人数为多人,则第一人按(2)的方式抽出,后续人将通过算法(sha256、fnv-hash)将13位抽奖数字进行计算从而得到新的抽奖数字,重复2-3步操作,直到抽取到本轮所有获奖人数
- 每个签到码只有一次中奖机会,如果签到码重复中奖,则会重新生成抽奖数字进行重复2-3步操作。
function getLuckDog(userList, count, defaultFinalValue) {
// 解构用户列表
let tempArray = [...userList];
// 记录中奖者列表
const res = [];
// 记录当前幸运数字 最终值
let finalValue = defaultFinalValue;
// 遍历抽奖次数
for (let i = 0; i < count; i++) {
if (tempArray.length > 0) {
// 随机打乱排序
tempArray.sort(() => 0.5 - Math.random()); // 每次抽奖前随机打断顺序
// 当前抽奖人数
const tempArrayLen = tempArray.length;
// 幸运儿下标值
const luckyDogNum = finalValue % tempArrayLen;
// 记录幸运儿
res[i] = tempArray[luckyDogNum];
// 过滤中奖人员
tempArray = tempArray.filter((v) => v.id !== res[i].id);
// 更新当前幸运数字 最终值
finalValue = getNewFinalValue(finalValue);
}
}
return res;
}
// 可查看测试 https://codesandbox.io/s/test-35ek4m?file=/demo.js
function getNewFinalValue(finalValue) {
const newFinalValue = fnv.hash(sha256(finalValue.toString())).dec();
return Number(newFinalValue);
}
数据分析
表格数据描述(仅供参考分析):
- 共抽取100轮
- 每次抽取5名幸运儿
个人觉得随机性越高会越好些,所以觉得公司年会算法和洗牌算法相对来说较好一些。
| 名称 | 0次 | 1次 | 2次 | 3次 | 4次 | 5次 | 6次 | 7次 |
|---|---|---|---|---|---|---|---|---|
| 公司年会算法 | 89 | 127 | 73 | 41 | 17 | 6 | 1 | 0 |
| 随机抽取算法 | 90 | 120 | 82 | 35 | 25 | 1 | 1 | 0 |
| 均匀分布算法 | 91 | 122 | 78 | 42 | 11 | 9 | 0 | 1 |
| 概率抽奖算法 | 97 | 116 | 94 | 36 | 17 | 4 | 0 | 0 |
| 洗牌算法 | 82 | 124 | 87 | 46 | 11 | 4 | 0 | 0 |