美文网首页简书の个人活动⛺️拾光书屋
用二十万条数据解读简书抽奖逻辑

用二十万条数据解读简书抽奖逻辑

作者: 初心不变_叶子 | 来源:发表于2022-08-08 14:45 被阅读0次

    最近在社区看了看,好多小伙伴都对简书抽奖相关的事情感兴趣,这次我们用数据探索一下。

    数据集

    这次的数据来源是抽奖页面最下方的中奖名单,这玩意:

    如果大家仔细观察过的话,中奖名单中的信息都有一个相同的条件:奖项大于“收益加成卡 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()
    )
    

    所以,抽奖是一件需要长期积累的事情,有时也需要一点运气。

    以上是本次数据分析的全部内容,希望对大家有所帮助。

    相关文章

      网友评论

        本文标题:用二十万条数据解读简书抽奖逻辑

        本文链接:https://www.haomeiwen.com/subject/oipwwrtx.html