对比MySQL和Pandas计算用户最大连续登录天数

数据来源于道才的文章:《用SQL计算用户连续登录天数》

需求如下:

image-20220412173314764

计算每一个用户的最大连续登录天数,由左变换到右边。

MySQL8.0窗口函数实现

实现思路:

  1. 对用户ID和登录日期去重
  2. 对每个用户ID按照日期顺序进行编号
  3. 将登录日期减去编号对应的天数,使连续的日期转换为同一天

将连续日期转换为同一个日期之后就可以按照这个字段分组,后面就简单了。下面我们一步步看:

对用户ID和登录日期去重

SELECT DISTINCT role_id,$part_date `date` FROM role_login;

对每个用户ID按照日期顺序进行编号,并将登录日期减去该编号对应的天数(可以一步到位):

SELECT
	role_id,`date`,
	DATE_SUB(`date`,INTERVAL (row_number() OVER(PARTITION BY role_id ORDER BY `date`)) DAY) data_group
FROM(
	SELECT DISTINCT role_id,$part_date `date` FROM role_login
) a;

image-20220412175802441

从结果我们可以看到已经成功的使连续的日期都转换到同一天。

然后我们就可以基于该结果统计每个用户的所有连续日期段:

SELECT
	role_id,
	MIN(DATE) begin_date,
	MAX(DATE) end_date,
	COUNT(*) continuous_days
FROM(
	SELECT
		role_id,`date`,
		DATE_SUB(`date`,INTERVAL (row_number() OVER(PARTITION BY role_id ORDER BY `date`)) DAY) data_group
	FROM(
		SELECT DISTINCT role_id,$part_date `date` FROM role_login
	) a
) b
GROUP BY role_id,data_group;

结果:

image-20220412180255118

然后通过窗口函数标注每个用户的连续日期排名:

SELECT
	role_id,begin_date,end_date,
	continuous_days ,
	row_number() OVER (PARTITION BY role_id ORDER BY continuous_days DESC) rk
FROM(
	SELECT
		role_id,
		MIN(DATE) begin_date,
		MAX(DATE) end_date,
		COUNT(*) continuous_days
	FROM(
		SELECT
			role_id,`date`,
			DATE_SUB(`date`,INTERVAL (row_number() OVER(PARTITION BY role_id ORDER BY `date`)) DAY) data_group
		FROM(
			SELECT DISTINCT role_id,$part_date `date` FROM role_login
		) a
	) b
	GROUP BY role_id,data_group
) c;

image-20220412210048535

注意:有时同一个用户的最大连续日期可能存在多个,这里以第一个为准;如果需要获取全部的最大日期可以使用rankdense_rank窗口函数,可以保证天数一致时排名一致。

SELECT
	role_id,begin_date,end_date,continuous_days max_continuous_days
FROM(
	SELECT
		role_id,begin_date,end_date,
		continuous_days ,
		row_number() OVER (PARTITION BY role_id ORDER BY continuous_days DESC) rk
	FROM(
		SELECT
			role_id,
			MIN(DATE) begin_date,
			MAX(DATE) end_date,
			COUNT(*) continuous_days
		FROM(
			SELECT
				role_id,`date`,
				DATE_SUB(`date`,INTERVAL (row_number() OVER(PARTITION BY role_id ORDER BY `date`)) DAY) data_group
			FROM(
				SELECT DISTINCT role_id,$part_date `date` FROM role_login
			) a
		) b
		GROUP BY role_id,data_group
	) c
) d
WHERE rk=1
ORDER BY max_continuous_days DESC,role_id;

成功得到结果:

image-20220412181513870

MySQL变量实现(5.0也可以使用)

在MySQL5.0中并没有窗口函数,但是我们可以通过变量来实现。

思路仍然与上述相同,首先我们对数据集去重并排序:

SELECT DISTINCT role_id,$part_date `date` FROM role_login
ORDER BY role_id,$part_date;

然后利用变量逐行扫描数据集:

SELECT
	role_id,`date`,
	IF(DATE_ADD(`date`,INTERVAL -1 DAY)=@prev_date,@r,@r:=@r+1) group_id, -- 日期变化大于1天(不连续)改变r值
	@prev_date:=`date` -- 记录前一条记录的日期
FROM(
	SELECT DISTINCT role_id,$part_date `date` FROM role_login
	ORDER BY role_id,$part_date
) a,(SELECT @prev_date:=NULL,@r:=0) t;

从结果可以看到,对于每个用户下连续的日期都给出了完全相同的分组编号:

image-20220412184641070

然后就可以计算连续天数了:

SELECT
	role_id,
	MIN(DATE) begin_date,
	MAX(DATE) end_date,
	COUNT(*) continuous_days
FROM(
	SELECT
		role_id,`date`,
		IF(DATE_ADD(@prev_date,INTERVAL 1 DAY)=`date`,@r,@r:=@r+1) group_id, -- 日期变化大于1天(不连续)改变r值
		@prev_date:=`date` -- 记录前一条记录的日期
	FROM(
		SELECT DISTINCT role_id,$part_date `date` FROM role_login
		ORDER BY role_id,$part_date
	) a,(SELECT @prev_date:=NULL,@r:=0) t
) b
GROUP BY role_id,group_id;

image-20220412180255118

然后借助变量标注排名:

SELECT
	role_id,begin_date,end_date,continuous_days ,
	IF(@prev_id=role_id,@r2:=@r2+1,@r2:=1) rk,
	@prev_id:=role_id -- 记录前一条记录的用户ID
FROM(
	SELECT
		role_id,
		MIN(DATE) begin_date,
		MAX(DATE) end_date,
		COUNT(*) continuous_days
	FROM(
		SELECT
			role_id,`date`,
			IF(DATE_ADD(@prev_date,INTERVAL 1 DAY)=`date`,@r,@r:=@r+1) group_id, -- 日期变化大于1天(不连续)改变r值
			@prev_date:=`date` -- 记录前一条记录的日期
		FROM(
			SELECT DISTINCT role_id,$part_date `date` FROM role_login
			ORDER BY role_id,$part_date
		) a,(SELECT @prev_date:=NULL,@r:=0) t1
	) b
	GROUP BY role_id,group_id
) c,(SELECT @prev_id:=NULL,@r2:=0) t2
ORDER BY role_id,continuous_days DESC;

image-20220412212230817

可以看到变量已经成功实现了row_number的效果。

最后再进行一次过滤即可:

SELECT
	role_id,begin_date,end_date,continuous_days max_continuous_days
FROM(
	SELECT
		role_id,begin_date,end_date,continuous_days ,
		IF(@prev_id=role_id,@r2:=@r2+1,@r2:=1) rk,
		@prev_id:=role_id -- 记录前一条记录的用户ID
	FROM(
		SELECT
			role_id,
			MIN(DATE) begin_date,
			MAX(DATE) end_date,
			COUNT(*) continuous_days
		FROM(
			SELECT
				role_id,`date`,
				IF(DATE_ADD(@prev_date,INTERVAL 1 DAY)=`date`,@r,@r:=@r+1) group_id, -- 日期变化大于1天(不连续)改变r值
				@prev_date:=`date` -- 记录前一条记录的日期
			FROM(
				SELECT DISTINCT role_id,$part_date `date` FROM role_login
				ORDER BY role_id,$part_date
			) a,(SELECT @prev_date:=NULL,@r:=0) t1
		) b
		GROUP BY role_id,group_id
	) c,(SELECT @prev_id:=NULL,@r2:=0) t2
	ORDER BY role_id,continuous_days DESC
) d
WHERE rk=1
ORDER BY max_continuous_days DESC,role_id;

成功得到结果:

image-20220412181513870

Pandas实现

下面我们用MySQL窗口函数的实现思路,用pandas实现一遍。

首先读取数据集并去重:

import pandas as pd

df = pd.read_excel("role_login.xlsx")
df = df[["role_id", "$part_date"]].drop_duplicates()
df

image-20220412190540604

对每个用户ID按照日期顺序进行编号,并将登录日期减去该编号对应的天数:

data_group = df["$part_date"]-pd.to_timedelta(df.groupby("role_id")["$part_date"].rank(method="dense"), unit='d')

统计每个用户的所有连续日期段:

data_group = df["$part_date"]-pd.to_timedelta(df.groupby("role_id")["$part_date"].rank(method="dense"), unit='d')
df = df.groupby(["role_id", data_group], as_index=False).agg(
    begin_date=("$part_date", "min"),
    end_date=("$part_date", "max"),
    max_continuous_days=("$part_date", "count")
)
df

image-20220412191207133

然后我们过滤出每个用户的最大连续日期:

ids = df.groupby("role_id")["max_continuous_days"].idxmax()
r1 = df.loc[ids].sort_values(
    ["max_continuous_days", "role_id"], ascending=[False, True])
r1

按照窗口函数的思路代码如下:

mask=df.groupby("role_id")["max_continuous_days"].rank(method="first", ascending=False) == 1
r2 = df[mask].sort_values(["max_continuous_days", "role_id"], ascending=[False, True])
r2

两种思路的结果一致:

(r1 == r2).all()
role_id                True
begin_date             True
end_date               True
max_continuous_days    True
dtype: bool

但是窗口函数的思路相对idxmax快了近百倍:

image-20220412201216421

所以针对这取分组最大的问题还是使用rank函数效果更高一些。

RANK 和 DENSE_RANK的差异

本题在一个用户存在多个最大连续日期时只要求取第一个,如果需要取每个用户所有的最大连续日期,则需要使用rankdense_rank窗口函数。

首先我们看看pandas中rank函数的几种method的差异:

import pandas as pd

t1 = pd.DataFrame(data={
    
    'num': [2, 4, 4, 8, 8]})
t1['default_rank'] = t1['num'].rank()
t1['min_rank'] = t1['num'].rank(method='min')
t1['max_rank'] = t1['num'].rank(method='max')
t1['dense_rank'] = t1['num'].rank(method='dense')
t1['first_rank'] = t1['num'].rank(method='first')
t1

结果:

num default_rank min_rank max_rank dense_rank first_rank
0 2 1.0 1.0 1.0 1.0 1.0
1 4 2.5 2.0 3.0 2.0 2.0
2 4 2.5 2.0 3.0 2.0 3.0
3 8 4.5 4.0 5.0 3.0 4.0
4 8 4.5 4.0 5.0 3.0 5.0

可以看到MySQL的rank函数相当于pandas的min_rank,row_number相当于first_rank:

SELECT
    cookieid,
    createtime,
    pv,
    RANK() OVER(PARTITION BY cookieid ORDER BY pv desc) AS rn1,
    DENSE_RANK() OVER(PARTITION BY cookieid ORDER BY pv desc) AS rn2,
    ROW_NUMBER() OVER(PARTITION BY cookieid ORDER BY pv DESC) AS rn3
FROM xxm_t2
WHERE cookieid = 'cookie1';
cookieid day           pv       rn1     rn2     rn3
-------------------------------------------------- 
cookie1 2022-04-12      7       1       1       1
cookie1 2022-04-11      5       2       2       2
cookie1 2022-04-15      4       3       3       3
cookie1 2022-04-16      4       3       3       4
cookie1 2022-04-13      3       5       4       5
cookie1 2022-04-14      2       6       5       6
cookie1 2022-04-10      1       7       6       7

猜你喜欢

转载自blog.csdn.net/as604049322/article/details/124135668