以下文章經過少量修改,主要來源於俊紅的數據分析之路 ,作者張俊紅,以及參考文獻:https://mp.weixin.qq.com/s/sg7VbRkS5xmJJo8FI6Hcvg
今天來解一道題面試中可能經常會被一些面試官拿來“刁難”的題,就是《如何統計連續打卡天數》,當然了這里面的打卡可以換成任意其他行為,比如連續登陸天數,連續學習天數,連續購買天數,這里的天數也是可以換成小時或者別的時間單位的。這個問題的邏輯還是有點復雜,如果要是之前沒遇到過這種問題,當場被問到的時候,肯定會一臉懵。
直接來看實戰,現在有一張表t,這張表存儲了每個員工每天的打卡情況,現在需要統計截止目前每個員工的連續打卡天數,表t如下表所示:
uid | tdate | is_flag |
---|---|---|
1 | 2020/2/1 | 1 |
1 | 2020/2/2 | 0 |
1 | 2020/2/3 | 1 |
1 | 2020/2/4 | 1 |
1 | 2020/2/5 | 0 |
1 | 2020/2/6 | 1 |
1 | 2020/2/7 | 1 |
1 | 2020/2/8 | 1 |
2 | 2020/2/1 | 1 |
2 | 2020/2/2 | 0 |
2 | 2020/2/3 | 0 |
2 | 2020/2/4 | 1 |
2 | 2020/2/5 | 1 |
2 | 2020/2/6 | 1 |
2 | 2020/2/7 | 1 |
2 | 2020/2/8 | 1 |
上表中uid是用戶id,tdate是日期,is_flag是記錄用戶當天是否打卡,1為打卡,0為未打卡。
我們希望得到的結果為:
uid | flag_days |
---|---|
1 | 3 |
2 | 5 |
這個邏輯還是挺難想的,第一個想法就是通過前后數據偏移來實現,就是將is_flag向前移動一行或者向后移動一行,然后和原來的is_flag標簽做差,如果結果為0,說明前后兩天的值是相同的,要么都是0,要么都是1。但是還是不能夠得出我們想要的結果。
再換一種思路:如果是連續打卡,那么打卡日期與一個遞增的數字依次做差的結果值應該是相等的,不理解這句話沒關系,看具體結果你就明白了。
分析思路:
1.先篩選條件打卡is_flag=1,然后所有打了卡的用窗口函數按照用戶id分組按時間排序
2.然后時間日期中的天與排序做差,得到的相同數字即為連續打卡
3.再聚合函數count(日期)一下,按連續打卡分組可得每段時間的連續打卡天數
4.接着就可隨意查看最近的連續打卡天數,歷史最高連續打卡天數,打卡天數大於某個數值的人
我們先獲取每個用戶在這一段時間內所有打卡的排名,是所有打卡的排名哦,利用的是窗口函數的row_number(),代碼如下:
select uid, tdate, row_number() over(partition by uid order by tdate) date_rank from t where is_flag=1
運行上面的代碼,可以得到如下結果:
uid | tdate | date_rank |
---|---|---|
1 | 2020/2/1 | 1 |
1 | 2020/2/3 | 2 |
1 | 2020/2/4 | 3 |
1 | 2020/2/6 | 4 |
1 | 2020/2/7 | 5 |
1 | 2020/2/8 | 6 |
2 | 2020/2/1 | 1 |
2 | 2020/2/4 | 2 |
2 | 2020/2/5 | 3 |
2 | 2020/2/6 | 4 |
2 | 2020/2/7 | 5 |
2 | 2020/2/8 | 6 |
接着再獲取每個打卡日期(tdate)中的日與其打卡日期排名(date_rank)之間的差,比如uid=1的2020/2/3的打卡日期中的3號與其排名(date_rank)2做差等於1,實現代碼如下:
select uid, tdate, date_rank, (date_format(tdate,"%e") - date_rank) as day_cha from ( select uid, tdate, row_number() over(partition by uid order by tdate) date_rank from demo.newtable where is_flag=1 )t1
date_format() 函數:用於以不同的格式顯示日期/時間數據。%e 輸出為月的天數,數值(0-31)
時間日期其他函數還可參考https://www.cnblogs.com/lverkou/p/13055614.html
運行上面的代碼,最后可以得到如下結果:
uid | tdate | date_rank | day_cha |
---|---|---|---|
1 | 2020/2/1 | 1 | 0 |
1 | 2020/2/3 | 2 | 1 |
1 | 2020/2/4 | 3 | 1 |
1 | 2020/2/6 | 4 | 2 |
1 | 2020/2/7 | 5 | 2 |
1 | 2020/2/8 | 6 | 2 |
2 | 2020/2/1 | 1 | 0 |
2 | 2020/2/4 | 2 | 2 |
2 | 2020/2/5 | 3 | 2 |
2 | 2020/2/6 | 4 | 2 |
2 | 2020/2/7 | 5 | 2 |
2 | 2020/2/8 | 6 | 2 |
看上面的結果表,有沒有看出點意思來,連續打卡日期的day_cha都是相等的,比如uid=1的2020/2/3和2020/2/4是連續的,他們的day_cha都是1。到這里,如果我們要獲取連續打卡天數是不是就很容易了。
不過這里面還有一個問題,就是連續打卡天數是截止目前最近的一個 連續打卡天數還是歷史堅持最長的打卡天數,這就是傳說中的口徑問題哈。雖然在我們這個例子里面,這兩種打卡天數的出來的結果是一樣的,但是有的時候會是不一樣的,比如下面這樣的例子:
uid | tdate | is_flag |
---|---|---|
1 | 2020/2/1 | 1 |
1 | 2020/2/2 | 0 |
1 | 2020/2/3 | 1 |
1 | 2020/2/4 | 1 |
1 | 2020/2/5 | 1 |
1 | 2020/2/6 | 0 |
1 | 2020/2/7 | 1 |
1 | 2020/2/8 | 1 |
上面這個例子中,最近連續打卡天數是2,歷史最長的連續打卡天數卻是3。
好了,我們繼續回到解題上,我們先獲取每個用戶歷史所有連續過得的打卡情況,實現代碼如下:
select uid, day_cha, count(tdate) flag_days //后面分組后日期是不一樣的,可以統計數量 from (select uid, tdate, date_rank, (date_format(tdate,"%e") - date_rank) as day_cha from ( select uid, tdate, row_number() over(partition by uid order by tdate) date_rank from demo.newtable where is_flag=1 )t1 )t2 group by uid, day_cha;
運行上面的代碼,得到如下結果:
uid | day_cha | flag_days |
---|---|---|
1 | 0 | 1 |
1 | 1 | 2 |
1 | 2 | 3 |
2 | 0 | 1 |
2 | 2 | 5 |
要獲取最近的連續打卡天數,我們只需要把上表中day_cha這一列最大的值對應的flag_days取出來就可以;要獲取歷史最久的連續打卡天數,我們只需要把上表中flag_days的最大值取出來就可以。直接再來個子查詢就好了。
類似的需求可能還有獲取過去連續打卡天數大於某個值的人,只需要篩選上表中的flag_days即可達到目的。只要能夠生成上面這樣每個人歷史所有連續打卡的情況表,那么大部分連續打卡相關的需求都可以通過上表來獲得。
很經典的一道題,或者是一種業務場景,大家各自多多練習。