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

如果大家仔细观察过的话,中奖名单中的信息都有一个相同的条件:奖项大于“收益加成卡 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
暮沉误
漠北兄弟
眼尖的简友们可能已经发现了,还有两种奖励没有出现过,它们是:
- 免费开 1 次连载
- 招财猫头像框 1 年
关于免费开连载这一奖励,我听说社区中有小伙伴中过奖,可能这个接口本身就不会返回这个奖项的中奖人,所以无法被采集到。
至于招财猫头像框,我从来没在社区看到过,有看到的小伙伴麻烦评论区指个路,谢谢啦~
大家喜欢在什么时候抽奖?
这一问题的研究基于一个假设:中奖率不随时间而变化。
首先我们来看一天中的抽奖人数分布:
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()
)

所以,抽奖是一件需要长期积累的事情,有时也需要一点运气。
以上是本次数据分析的全部内容,希望对大家有所帮助。
网友评论