最近在社区看了看,好多小伙伴都对简书抽奖相关的事情感兴趣,这次我们用数据探索一下。
这次的数据来源是抽奖页面最下方的中奖名单,这玩意:

如果大家仔细观察过的话,中奖名单中的信息都有一个相同的条件:奖项大于“收益加成卡 100”。
这个名单的数据来源是简书的一个接口,于是我写了一点代码,每天自动保存新增的中奖数据。
然后,把这个采集脚本放到服务器上,跑它几个月。
前几天一看,数据量快达到二十五万了,索性就拿出来做下分析。
本次使用的是简书抽奖数据,包含 2021.12.29 到 2022.08.05 共 219 天中,所有奖项高于“收益加成卡 100”的抽奖记录。
数据共有 241755 条,存储在 MongoDB 中,占用空间 7.79MB。
数据结构如下:
{
"_id": 23812870,
"time": {
"$date": {
"$numberLong": "1640782059000"
}
},
"reward_name": "收益加成卡100",
"user": {
"id": 24124389,
"url": "https://www.jianshu.com/u/2f7a3e46c654",
"name": "薇叶儿"
}
}
初始化数据库连接,分批次导入数据,转换为 DataFrame:
df = pd.DataFrame()
temp = []
for item in db.find({}):
item["user_id"] = item["user"]["id"]
item["user_url"] = item["user"]["url"]
item["user_name"] = item["user"]["name"]
del item["user"]
temp.append(item)
if len(temp) == 1000:
df = df.append(temp)
temp.clear()
del temp
这里需要注意,由于 MongoDB 是文档型数据库,导出的格式是 JSON,包含嵌套关系,但便于我们分析的 DataFrame 格式应该是扁平的,因此我们需要对数据进行展开。
这里需要展开的字段较少,可以手动进行,在数据字段较多的情况下,可以考虑写一个工具函数实现展开,或使用 MongoDB 聚合功能实现展开,后者实现难度略高,但性能更好。
查看部分数据:
df.head()

查看数据类型:
df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 241000 entries, 0 to 999
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 _id 241000 non-null int64
1 time 241000 non-null datetime64[ns]
2 reward_name 241000 non-null object
3 user_id 241000 non-null int64
4 user_url 241000 non-null object
5 user_name 241000 non-null object
dtypes: datetime64[ns](1), int64(2), object(3)
memory usage: 12.9+ MB
其实这里可以发现,_id 列和 user_id 列的数据没有必要使用 int64 存储,存在内存浪费,但这份数据并不算很大,占用的内存空间不会影响我们的分析,这里无需处理。
使用以下代码查看 DataFrame 占用的内存空间:
print(df.memory_usage(deep=True).sum() / 1024 / 1024, "MB")
72.4241714477539 MB
先对数据进行简单的按月聚合,获得每月的中奖人次:
grouper = pd.Grouper(key="time", freq="M")
month_df = df.groupby(grouper)["_id"].count().reset_index()
month_df.rename(columns={"time": "month", "_id": "count"}, inplace=True)
month_df["月份"] = month_df["month"].apply(lambda x: x.month)
month_df.drop([0, 8], inplace=True)
month_df
开头和结尾采集的数据不完整,这里将其删去,结果如下:
| month | count |
|---|---|
| 1 | 33056 |
| 2 | 29944 |
| 3 | 35346 |
| 4 | 33758 |
| 5 | 34688 |
| 6 | 32368 |
| 7 | 34554 |
直观一些,我们来画个图:
(
Line()
.add_xaxis(list(month_df["month"]))
.add_yaxis("中奖人数", list(month_df["count"]))
.set_global_opts(
title_opts=opts.TitleOpts(title="月份与中奖人数关系")
)
.render_notebook()
)

由此可见,在这段时间内,中奖率并没有发生显著变化。
type_df = df.groupby("reward_name")["_id"].count().reset_index()
type_df.rename(columns={"_id": "中奖人数"}, inplace=True)
type_df.sort_values("中奖人数", ascending=False, inplace=True)
type_df["综合中奖率"] = type_df["中奖人数"].apply(lambda x: f"{round(x / 241755 * 100, 3)}%")
type_df
| reward_name | 中奖人数 | 综合中奖率 |
|---|---|---|
| 收益加成卡100 | 231776 | 95.872% |
| 收益加成卡1万 | 9112 | 3.769% |
| 四叶草徽章 | 104 | 0.043% |
| 锦鲤头像框1年 | 8 | 0.003% |
到这里,事情变得有意思了起来。
不出所料,100 加成卡以绝对优势位居榜首,综合中奖率在 95% 以上。
10000 加成卡的中奖率也尚可接受,高于 3%。
四叶草徽章的中奖率低了点啊,在简书取消徽章合成玩法后,这个徽章已经失去了用途,变成了装饰品,一个装饰品居然拥有比能带来实际收益的物品(10000 加成卡)更低的中奖率,简书紧握着四叶草徽章不放,是单纯不想更改概率,还是这个徽章将被开发出新的用途?
锦鲤头像框的中奖率更低,半年中只有 8 位简友中奖,这里放上他们的个人主页链接,大家可以去看看他们是否佩戴了头像框:
飞鸿雪舞
李本意小姐姐
c3e6b92fe89c
陌爻凉
消消乐的日常
eggplant1223
暮沉误
漠北兄弟
眼尖的简友们可能已经发现了,还有两种奖励没有出现过,它们是:
关于免费开连载这一奖励,我听说社区中有小伙伴中过奖,可能这个接口本身就不会返回这个奖项的中奖人,所以无法被采集到。
至于招财猫头像框,我从来没在社区看到过,有看到的小伙伴麻烦评论区指个路,谢谢啦~
这一问题的研究基于一个假设:中奖率不随时间而变化。
首先我们来看一天中的抽奖人数分布:
temp_df = df.copy()
temp_df["hour"] = temp_df["time"].apply(lambda x: x.hour)
hour_df = temp_df.groupby("hour")["_id"].count().reset_index()
del temp_df
(
Bar()
.add_xaxis(list(hour_df["hour"]))
.add_yaxis("抽奖人数", list(hour_df["_id"]))
.set_global_opts(
title_opts=opts.TitleOpts(title="抽奖人数分布(小时)")
)
.render_notebook()
)

早晚抽奖的人数多于中午,深夜抽奖人数最少。
然后是一周中的人数分布:
temp_df = df.copy()
temp_df["week"] = temp_df["time"].apply(lambda x: x.weekday())
week_df = temp_df.groupby("week")["_id"].count().reset_index()
del temp_df
(
Bar()
.add_xaxis(list(week_df["week"]))
.add_yaxis("抽奖人数", list(week_df["_id"]))
.set_global_opts(
title_opts=opts.TitleOpts(title="抽奖人数分布(周)")
)
.render_notebook()
)

没有明显起伏,这说明简书的活跃用户量基本不随星期而改变。
众所周知,简书的用户 ID 是有序的,越早注册的用户,UID 越小。
在这份数据中,UID 最小,也就是注册时间最早的用户是 alue,时间是 2022 年 6 月 17 日。
他算是简书的元老级用户,主页最新文章的发布时间是昨天,看信息应该是一名全栈工程师。
(还可能是 Wolai 用户?)
UID 最大,注册时间最晚的用户是 简悦58,时间是 2022 年 8 月 5 日。
她的账号也是在这一天注册的,早晨注册,下午尝试抽奖,并且抽中了简书给她的第一张 100 加成卡。
接下来,我们将所有用户按照 UID 分组,间隔为一百万,并画出柱状图:

新用户参与抽奖的频率更高,或者说现在在简书活跃的大多是新用户。
这是否意味着大量的用户流失?
要解决这个问题,我们需要比较准确地衡量出奖品的价值。
前面已经给出了各奖品的中奖率,我们将中奖率取倒数,就获得了(至少在设计者看来)的奖品价值:
(为了提升精确度,此处中奖百分比保留 7 位有效数字,转换成小数就是 9 位精度)
temp_dict = {
"收益加成卡100": 0.958722674,
"收益加成卡1万": 0.037691051,
"四叶草徽章": 0.000430188,
"锦鲤头像框1年": 0.000033091
}
reward_to_value = {key: round(1 / value, 3)
for key, value in temp_dict.items()
}
for key, value in reward_to_value.items():
print(f"{key} {value}")
| 奖品 | 价值 |
|---|---|
| 收益加成卡100 | 1 |
| 收益加成卡1万 | 26 |
| 四叶草徽章 | 2324 |
| 锦鲤头像框1年 | 30219 |
这里发现 100 加成卡和 10000 加成卡的价值比是 1:26,而两者的绝对价值比为 1:100,这一设计使抽到 10000 加成卡的利益远大于 100 加成卡,因此带动了抽奖机会的获取,同时提升了广告曝光量。
按照铜牌会员的加成卡获取量折算,四叶草徽章(永久)价值 103.80 元,锦鲤头像框(1 年)价值 1349.73 元。
如果按照白金会员折算,四叶草徽章(永久)价值 79.56 元,锦鲤头像框(1 年)价值 1034.58 元。
如果简书想要卖这两个徽章,也许可以参考这两个价格作为用户心理预期。
不过锦鲤卖到这个价格真的会有人买吗?
接下来,根据奖品价值计算每个用户的获奖总价值:
user_id_list = [x for x in db.distinct("user.id")]
def job(user_id, data):
item_reward_data = {key: 0 for key in reward_to_value.keys()}
reward_value = 0
for item in data:
item_reward_data[item["reward_name"]] += 1
reward_value += reward_to_value[item["reward_name"]]
return (user_id, item_reward_data, reward_value)
futures = []
with ThreadPoolExecutor(max_workers=8) as pool:
for user_id in user_id_list:
data = db.find({"user.id": user_id})
future = pool.submit(job, user_id, data)
futures.append(future)
pool.shutdown(wait=True)
user_reward_value = []
for future in futures:
user_id, item_reward_data, reward_value = future.result()
user_data = db.find_one({"user.id": user_id})["user"]
user_reward_value.append({
"user": user_data,
"reward_data": item_reward_data,
"reward_value": reward_value
})
del futures
(这里不使用线程池也能获得可接受的运行时间,当时我忘了给数据库建索引,误以为存在性能问题,所以做了多线程优化)
获取最幸运的用户:
max_reward_user = {"reward_value": 0}
for user in user_reward_value:
if user["reward_value"] > max_reward_user["reward_value"]:
max_reward_user = user
print(max_reward_user)
最幸运的用户是 c3e6b92fe89c,她的奖品总价值为 33107,获奖明细如下:
| 奖品名称 | 获奖次数 |
|---|---|
| 收益加成卡100 | 330 |
| 收益加成卡1万 | 9 |
| 四叶草徽章 | 1 |
| 锦鲤头像框1年 | 1 |
抽到最多次 10000 加成卡的用户:
max_10000_user = {"reward_data": {"收益加成卡1万": 0}}
for user in user_reward_value:
if user["reward_data"]["收益加成卡1万"] > max_10000_user["reward_data"]["收益加成卡1万"]:
max_10000_user = user
print(max_10000_user)
抽到最多次 10000 加成卡的用户是 舜真如心,她在没有抽到过四叶草徽章和锦鲤头像框的情况下,获得了价值 904 的奖品。
抽到最多次 100 加成卡的用户是 王别二,404 张 100 加成卡,平均一天中 2 个,想必是抽奖常客吧。
100 加成卡获取数量分布如下:

10000 加成卡获取数量分布如下:

最后,来统计一下大家的奖品价值情况:
(为了避免高价值奖品获得者将图像顶起影响分析,本图表不考虑四叶草徽章和锦鲤头像框的价值)
for user in user_reward_value:
if user["reward_data"]["四叶草徽章"] != 0:
user["reward_value"] -= reward_to_value["四叶草徽章"] * user["reward_data"]["四叶草徽章"]
if user["reward_data"]["锦鲤头像框1年"] != 0:
user["reward_value"] -= reward_to_value["锦鲤头像框1年"] * user["reward_data"]["锦鲤头像框1年"]
temp_dict = {}
for user in user_reward_value:
group = int(user["reward_value"] / 10)
if temp_dict.get(group):
temp_dict[group] += 1
else:
temp_dict[group] = 1
total_dict = {}
for i in range(max(temp_dict.keys())):
if not temp_dict.get(i):
total_dict[i] = 0
else:
total_dict[i] = temp_dict[i]
del temp_dict
x = list(total_dict.keys())
x.sort()
y = list(total_dict.values())
(
Bar()
.add_xaxis(x)
.add_yaxis("人数", y, category_gap=0)
.set_global_opts(
title_opts=opts.TitleOpts(title="奖品价值分布(每 10 价值为一组)"),
)
.set_series_opts(
label_opts=opts.LabelOpts(is_show=False)
)
.render_notebook()
)

所以,抽奖是一件需要长期积累的事情,有时也需要一点运气。
以上是本次数据分析的全部内容,希望对大家有所帮助。
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳
我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我
我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01 客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02 数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit
文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co
我正在尝试在Rails上安装ruby,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf
文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,
SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手