SQL29 计算用户的平均次日留存率

困难 通过率:48.58% 时间限制:1秒 空间限制:256M

描述

题目:现在运营想要查看用户在某天刷题后第二天还会再来刷题的平均概率。请你取出相应数据。

示例:question_practice_detail

id device_id quest_id result date
1 2138 111 wrong 2021-05-03
2 3214 112 wrong 2021-05-09
3 3214 113 wrong 2021-06-15
4 6543 111 right 2021-08-13
5 2315 115 right 2021-08-13
6 2315 116 right 2021-08-14
7 2315 117 wrong 2021-08-15
……

根据示例,你的查询应返回以下结果:

avg_ret
0.3000

示例1

输入:

  1. drop table if exists `user_profile`;
  2. drop table if exists `question_practice_detail`;
  3. drop table if exists `question_detail`;
  4. CREATE TABLE `user_profile` (
  5. `id` int NOT NULL,
  6. `device_id` int NOT NULL,
  7. `gender` varchar(14) NOT NULL,
  8. `age` int ,
  9. `university` varchar(32) NOT NULL,
  10. `gpa` float,
  11. `active_days_within_30` int ,
  12. `question_cnt` int ,
  13. `answer_cnt` int
  14. );
  15. CREATE TABLE `question_practice_detail` (
  16. `id` int NOT NULL,
  17. `device_id` int NOT NULL,
  18. `question_id`int NOT NULL,
  19. `result` varchar(32) NOT NULL,
  20. `date` date NOT NULL
  21. );
  22. CREATE TABLE `question_detail` (
  23. `id` int NOT NULL,
  24. `question_id`int NOT NULL,
  25. `difficult_level` varchar(32) NOT NULL
  26. );
  27. INSERT INTO user_profile VALUES(1,2138,'male',21,'北京大学',3.4,7,2,12);
  28. INSERT INTO user_profile VALUES(2,3214,'male',null,'复旦大学',4.0,15,5,25);
  29. INSERT INTO user_profile VALUES(3,6543,'female',20,'北京大学',3.2,12,3,30);
  30. INSERT INTO user_profile VALUES(4,2315,'female',23,'浙江大学',3.6,5,1,2);
  31. INSERT INTO user_profile VALUES(5,5432,'male',25,'山东大学',3.8,20,15,70);
  32. INSERT INTO user_profile VALUES(6,2131,'male',28,'山东大学',3.3,15,7,13);
  33. INSERT INTO user_profile VALUES(7,4321,'male',28,'复旦大学',3.6,9,6,52);
  34. INSERT INTO question_practice_detail VALUES(1,2138,111,'wrong','2021-05-03');
  35. INSERT INTO question_practice_detail VALUES(2,3214,112,'wrong','2021-05-09');
  36. INSERT INTO question_practice_detail VALUES(3,3214,113,'wrong','2021-06-15');
  37. INSERT INTO question_practice_detail VALUES(4,6543,111,'right','2021-08-13');
  38. INSERT INTO question_practice_detail VALUES(5,2315,115,'right','2021-08-13');
  39. INSERT INTO question_practice_detail VALUES(6,2315,116,'right','2021-08-14');
  40. INSERT INTO question_practice_detail VALUES(7,2315,117,'wrong','2021-08-15');
  41. INSERT INTO question_practice_detail VALUES(8,3214,112,'wrong','2021-05-09');
  42. INSERT INTO question_practice_detail VALUES(9,3214,113,'wrong','2021-08-15');
  43. INSERT INTO question_practice_detail VALUES(10,6543,111,'right','2021-08-13');
  44. INSERT INTO question_practice_detail VALUES(11,2315,115,'right','2021-08-13');
  45. INSERT INTO question_practice_detail VALUES(12,2315,116,'right','2021-08-14');
  46. INSERT INTO question_practice_detail VALUES(13,2315,117,'wrong','2021-08-15');
  47. INSERT INTO question_practice_detail VALUES(14,3214,112,'wrong','2021-08-16');
  48. INSERT INTO question_practice_detail VALUES(15,3214,113,'wrong','2021-08-18');
  49. INSERT INTO question_practice_detail VALUES(16,6543,111,'right','2021-08-13');
  50. INSERT INTO question_detail VALUES(1,111,'hard');
  51. INSERT INTO question_detail VALUES(2,112,'medium');
  52. INSERT INTO question_detail VALUES(3,113,'easy');
  53. INSERT INTO question_detail VALUES(4,115,'easy');
  54. INSERT INTO question_detail VALUES(5,116,'medium');
  55. INSERT INTO question_detail VALUES(6,117,'easy');

复制

输出:

  1. 0.3000

解析:

  对于这道题,对于SQL能力很弱的我来说根本无从下手,实际上我在大学期间的数据库学的也不怎么样,而对于这道题中使用的一些技巧我在大学期间根本没有学,因此在这道题的解析中我会以知识点解析很详细的初学者模式学习这道题,而非解析这道题。

  牛客网官方的解题方式使用了两种解法:

  解法一,表里的数据可以看作是全部第一天来刷题了的,那么我们需要构造出第二天来了的字段,因此可以考虑用left join把第二天来了的拼起来,限定第二天来了的可以用date_add(date1,interval 1 day)=date2筛选,并用device_id限定是同一个用户。

  关于解法一,我们是先将表里的每一条数据都独立的来看,将它们都看成是第一天来刷题的,即使是有同一个人连续刷题刷了好几天,我们也将这些连续的数据独立分开来看,将每一条信息都分别看成第一天来刷题的,然后我们再进行一个合表,而合表条件我们使用date_add(date1,interval 1 day)=date2来进行匹配,这个是什么意思呢?实际上就是让date1加一天,然后看和date2是否相等,这个操作实际上就是让原数据中的日期加一天,然后让加过的表和之前的刷题信息表进行一次匹配,看看有多少能匹配上的,因为原来的信息表中保留的是原始的时间信息,而新表中的时间信息都加了一天,这样一匹配的话,有些信息的日期就在原信息中不存在了,这是因为其第二天没有刷题,因此没有这第二天的系信息,同时我们还要使用device_id限定是同一个用户,这样一来就能确保是每个人是否在第二天刷题了,这个思路实际上就是看一看某个人是否每条刷题记录都有第二天的记录。

  SQL脚本的书写方式如下:

  1. select count(date2) / count(date1) as avg_ret
  2. from(
  3. select
  4. distinct qpd.device_id,
  5. qpd.date as date1,
  6. uniq_id_date.date as date2
  7. from question_practice_detail as qpd
  8. left join(
  9. select distinct device_id, date
  10. from question_practice_detail
  11. ) as uniq_id_date
  12. on qpd.device_id=uniq_id_date.device_id
  13. and date_add(qpd.date,interval 1 day)=uniq_id_date.date
  14. ) as id_last_next_date

  关于这个脚本,使用了分步查询,其中最大的外层查询,是在查询内层结果中的date2的数量与date1的数量的比值,内层查询则是对一个同样的表(这里也用了一个简单的分步,旨在去掉多余没用的数据)进行了自身的左连接,这里我们如果想要对一个同样的表进行自身的连接并且使用自身的字段时,可以加上别名加以区分,而连接的条件就是其中一个表的日期加一后和另一个表的相等,且设备id仍然相等,这样就得到了一个用户,连续两天刷题的匹配结果,而需要注意的是,左连接会保留左侧表和右侧表没有匹配的信息,因此我们使用date1的数量就可以获取到原表中所有的刷题信息,而右侧匹配到的就是有第二天刷题的信息,这样做比值,就可以得到我们想要的结果了。

  在这个问题中其实非常重要的一个问题就是:什么是用户的平均次日存留率?这个怎么计算是个问题,在题目中解释道:用户在某天刷题后第二天还会再来刷题的平均概率,这个是什么意思呢?我们在解决问题的时候一定要学会理解问题并简化问题,在某天刷题之后第二天还会来,其意思就是,针对于用户的每一条记录,在第二天还有这个人的刷题记录,并且这个第二天是一个相对的第二天,是一个相对概念,也就是说如果我连着刷题刷了三天:10号,11号,12号,那么11号相对于10号就是第二天,而12号相对于11号也是第二天,总体上这三天中存在两个第二天,这就让我们必须单独的看待每一天,然后找每一天的第二天,而不能在内心中存在12号是10号的第三天的想法,这种想法尽管首先上就是不对的,但是一些人在做题的时候内心还是隐约有这种顾虑,比如我,进而难以很好的理解这道题的意思,这道题的第二天就是指相对的第二天,我们必须要独立的来看待每一天,然后找到每一天的第二天,然后我们要求的概率,实际上就是所有的有第二天的信息所占的比例,也就是说所有有第二天记录的记录在所有记录中所占据的比例,如上边我举的例子,三天中有两天有第二天,而总共有三天,那么我第二天还来刷题的概率就是2/3,这三天之中。有两天拥有第二天,有一天不拥有,这个概率就是这样来的。

  解法二,用lead函数将同一个用户连续两天的记录拼接起来。先按用户分组partition by device_id,再按日期升序排序order by date,再两两拼接(最后一个默认和null拼接),即lead(date) over (partition by device_id order by date)

  关于解法二我基本上完全不懂,因为我不知道lead函数是什么意思,因此我查了一下:LEAD()函数是一个窗口函数,允许我们向前看多行并从当前访问行的数据,LEAD()函数对于计算同一结果集中当前行和后续行之间的差异非常有用,LEAD()函数的用法:

  1. LEAD(<expression>[,offset[, default_value]]) OVER (
  2. PARTITION BY (expr)
  3. ORDER BY (expr)
  4. )

  其中expression是LEAD()函数返回的值experssion从offset-th有序分区排,offset是从当前行向前行的行数,以获取值,offset必须是一个非负整数,如果offset为零,则LEAD()函数计算expression当前行的值,如果省略offset,则LEAD()函数默认使用一个。default_value,如果没有后续行,则LEAD()函数返回default_value。例如,如果offset是1,则最后一行的返回值为default_value。如果未指定default_value,则函数返回NULL。PARTITION BY子句将结果集中的行划分LEAD()为应用函数的分区,如果PARTITION BY未指定子句,则结果集中所有行都被视为单个分区,ORDER BY子句是确定函数中行的顺序用的。

MySQL LEAD()函数示例

  1. /*
  2. 我们将使用示例数据库中的orders和customers表进行演示:
  3. 以下语句查找每个客户的订单日期和下一个订单日期:
  4. */
  5. SELECT
  6. customerName,/*字段*/
  7. orderDate,/*字段*/
  8. LEAD(orderDate,1) OVER (/*获取下一行的orderDate*/
  9. PARTITION BY customerNumber /*按照customerName分组*/
  10. ORDER BY orderDate /*排序*/) nextOrderDate/*命名为nextOrderDate*/
  11. FROM
  12. orders
  13. INNER JOIN customers USING (customerNumber);

  我们将获得一个这样的结果:

  1. +------------------------------------+------------+---------------+
  2. | customerName | orderDate | nextOrderDate |
  3. +------------------------------------+------------+---------------+
  4. | Atelier graphique | 2013-05-20 | 2014-09-27 |
  5. | Atelier graphique | 2014-09-27 | 2014-11-25 |
  6. | Atelier graphique | 2014-11-25 | NULL |
  7. | Signal Gift Stores | 2013-05-21 | 2014-08-06 |
  8. | Signal Gift Stores | 2014-08-06 | 2014-11-29 |
  9. | Signal Gift Stores | 2014-11-29 | NULL |
  10. | Australian Collectors, Co. | 2013-04-29 | 2013-05-21 |
  11. | Australian Collectors, Co. | 2013-05-21 | 2014-02-20 |
  12. | Australian Collectors, Co. | 2014-02-20 | 2014-11-24 |
  13. | Australian Collectors, Co. | 2014-11-24 | 2014-11-29 |
  14. | Australian Collectors, Co. | 2014-11-29 | NULL |
  15. | La Rochelle Gifts | 2014-07-23 | 2014-10-29 |
  16. | La Rochelle Gifts | 2014-10-29 | 2015-02-03 |
  17. | La Rochelle Gifts | 2015-02-03 | 2015-05-31 |
  18. | La Rochelle Gifts | 2015-05-31 | NULL |
  19. | Baane Mini Imports | 2013-01-29 | 2013-10-10 |
  20. | Baane Mini Imports | 2013-10-10 | 2014-10-15 |
  21. ...
  22. /*
  23. 在此示例中,我们首先将客户编号的结果集划分为多个分区。然后,我们按订单日期对每个分区进行排序。最后,LEAD()函数应用于每个分区以获取下一个订单日期。
  24. 一旦后续行穿过分区边界,nextOrderDate每个分区的最后一行中的值就是NULL。
  25. */

  现在我们知道了LEAD()的用法,实际上就是找下n行的n个字段,因此我们就可以理解这道题的解法了:先是按用户分组,然后按照时间升序排序,然后我们将连续两天的记录拼合起来,然后我们就可以检查两个字段的日期差了,只要日期差为1,那就说明这是一个连续刷题的记录,而为0的话说明这天的第二天没有连续刷题,然后日期差为1的记录/所有的记录即为我们想要求的概率,这里可以使用平均值计算出来,也就是求日期差为1的个数的平均值。

  SQL脚本:

  1. select avg(if(datediff(date2, date1)=1, 1, 0)) as avg_ret/*差值为1则为1,否则为0,然后求这个字段相加的平均值*/
  2. from (
  3. select
  4. distinct device_id,
  5. date as date1,
  6. lead(date) over (partition by device_id order by date) as date2/*获取相应的分组以及分组内下行的日期*/
  7. from (
  8. select distinct device_id, date/*简化表格*/
  9. from question_practice_detail
  10. ) as uniq_id_date
  11. ) as id_last_next_date

SQL29 计算用户的平均次日留存率的更多相关文章

  1. 游戏数据分析中“次日留存率”与“游戏生命周期第N天上线率”的SAS实现

    在游戏行业,次日留存率是个很重要的指标,对于评价一款游戏的优劣具有很重要的参考价值. 下面先看以下相关的定义: 用户留存:统计时间区间内,新登用户在随后不同时期的登录使用情况. 日次留存率:日新登用户 ...

  2. 编写一个ComputerAverage抽象类,类中有一个抽象方法求平均分average,可以有参数。定义 Gymnastics 类和 School 类,它们都是 ComputerAverage 的子类。Gymnastics 类中计算选手的平均成绩的方法是去掉一个最低分,去掉一个最高分,然后求平均分;School 中计算平均分的方法是所有科目的分数之和除以总科目数。 要求:定义ComputerAv

    题目: 编写一个ComputerAverage抽象类,类中有一个抽象方法求平均分average,可以有参数. 定义 Gymnastics 类和 School 类,它们都是 ComputerAverag ...

  3. 小白学数据分析--留存率分析_I次日留存率突然下降了50%

    小白学数据分析--留存率分析_I次日留存率突然下降了50% 最近在做留存分析时,遇到了不少的情况,也经常会有人问我,为什么我的游戏突然次日留存率降了一半.如果留存率是单单作为一个简单的指标的话,那对你 ...

  4. 活跃天数计算用户等级模仿QQ的升级方式

    QQ等级的算法:设当前等级为N,达到当前等级最少需要的活跃天数为D,当前活跃天数为Dc,升级剩余天数为Dr,则: 从而推出: 好了,引述完成,懒得写字了,贴出代码: 复制内容到剪贴板 代码: < ...

  5. ac命令根据/var/log/wtmp文件登录退出时间计算用户连接时间

    ac命令根据/var/log/wtmp文件登录退出时间计算用户连接时间

  6. /* * 有五个学生,每个学生有3门课的成绩,从键盘输入以上数据 *(包括学生号,姓名,三门课成绩),计算出平均成绩, *将原有的数据和计算出的平均分数存放在磁盘文件"stud"中。 */

    1.Student类:类中有五个变量,分别是学号,姓名,三门成绩 package test3; public class Student { private int num; private Stri ...

  7. js用img代替ajax js心跳 向服务器定时传送参数 主要计算用户在线时长

    html: <!doctype html><html><head><meta charset="utf-8"><title&g ...

  8. MySQL学习_计算用户支付方式占比_20161104

    计算用户支付方式占比 SELECT b.*#根据城市ID 年月排序 FROM ( SELECT a.* FROM ( #纵向合并 SELECT b1.ID,a1.城市,a1.收款方式,DATE_FOR ...

  9. 代码实现:有五个学生,每个学生有3门课的成绩,从键盘输入以上数据 (包括学生号,姓名,三门课成绩),计算出平均成绩,将原有的数据和计算出的平均分数存放在磁盘文件"stud"中。

    import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.ut ...

  10. MySQL 的 RowNum 实现(排行榜计算用户排名)

    1. 计算用户排名最高效的方法 例如:通过用户分享个数排名,那么自己的排名就是:比自己分享数多的用户个数 + 1 ' and `count` > '自己分享个数' 缺点:当多个用户分享个数相同的 ...

随机推荐

  1. ciscn 2022 misc 部分wp

    ​ 目录 everlasting_night ez_usb everlasting_night 提示是注意png数据块 然后注意图片通道数据可以用来lsb解码 下载是一张图片,尝试几种方法之后没有太大 ...

  2. 安装与配置FTP服务器

    概:   文件传输协议 (File Transfer Protocol,FTP),用于在网络上进行文件传输的协议.如果用户需要将文件从本机发送到另一台计算机,可以使用FTP上传操作:反之,用户可以使用 ...

  3. Maven 聚合工程的创建

    简单场景举例 聚合工程创建示例 说明: 创建 Maven Project:表示创建 maven 项目,new Project 方式创建 创建 Maven Module:表示创建 maven 项目,ne ...

  4. 论文笔记 - SIMILAR: Submodular Information Measures Based Active Learning In Realistic Scenarios

    motivation Active Learning 存在的重要问题:现实数据极度不平衡,有许多类别很少见(rare),又有很多类别是冗余的(redundancy),又有些数据是 OOD 的(out- ...

  5. 2021-2022 ICPC, NERC, Northern Eurasia Onsite (Unrated, Online Mirror, ICPC Rules, Teams Preferred) J. Job Lookup

    题意 n个节点,n<=200,你需要构造这n个几点成为一棵树,并且这棵树的中序遍历为1-n; 你构造树的节点之间的最短路构成一个n×n的最短距离矩阵d: 同时给你n×n的权重矩阵c:最最小的Σd ...

  6. windows socket网络编程--事件选择模型

    目录 事件选择模型概述 API详解 工作原理 代码实现 事件选择模型概述 Winsock提供了另一种有用的异步事件通知I/O模型--WSAEventSelect模型.这个模型与WSAAsyncSele ...

  7. Ubuntu环境下LLVM 15.0 完全编译 附windows编译LLVM master

    1. 预先安装 sudo apt install ninja-build sudo apt install llvm clang # 第一次编译需要 sudo apt-get install libn ...

  8. Redux 的困扰与如何技术选型

    文章的名字我想了很久,备选项有"我再不推荐 Redux","Redux 为什么令我头疼","Redux 进化启示录"等等.通过这一系列名字我 ...

  9. C ++:树

    C++:树 树的概念: 所谓"树"是输就结构的一种,树大概可以分为两大类: 有根树 和 无根树 有根树使有一个确定的根节点,反之为无根树 · 子节点:从树根开始,通过树边向下扩展的 ...

  10. NavBarControl 通过代码添加

    看到网上很多都是直接在控件上面添加的 而我的是保存在数据库读取后添加的. 后来自己摸索了一下. 通过代码添加 navBarControl1.Groups.Add(new NavBarGroup( st ...