本帖最后由 zhuqitian 于 2017-3-7 11:21 编辑
问题:广告投放周期范围内某个时间片段的蹦失率(跳失率)计算
需求分析:单位时间内落地页中只访问了一次商品页用户数 / 单位时间内用户总数
比如20170307这一天 用户A 访问商品A1 10次
用户B 访问商品A1 1次
用户C 访问商品A1 2次
蹦失率 = 1 / (3) = 0.333
细节讨论:若是用户A在2017-03-07 23:00:00 前点击商品页后1小时内没有点击第二次算蹦失用户,若是当天最后一小时内点了两次算非蹦失用户
传统实现思路:取访问时间的日期加小时做分组,24小时内有任何一小时pv>=2就算非蹦失用户,这种实现思路效率低下,比如用户第一小时就已经
是非蹦失用户了就应该打上非蹦失用户标志了还要多计算23小时,这不是重点,重点是这跟业务逻辑有出入,访问时间是要精确到秒的,比如用户A 在
2017-03-07 01:50:59访问了商品A1 下一次方式是在 2017-03-07 02:01:00业务中这是算非蹦失用户的,所以必须精确到秒
先实现个最细粒度的计算:维度:visit_date访问日期、pid商品id、user_id用户id 取蹦失uv和总uv相比后得出商品id在当天的蹦失率
为什么要先实现最细粒度的:为了迎接不断变化的业务需求,我们的代码要足够健壮可拓展且高效
sql语句:
[mw_shl_code=sql,true]insert overwrite local directory '/opt/study/etl/data/lost'
select pid, vdate, count(*)/uv
from (
select
user_id
,pid
,substr(visit_time,0,10) as vdate
,count(distinct user_id) as uv
,TimeDiff('1h',concat_ws(',',collect_set(visit_time))) as islose
from table
where pt_date='20170302'
group by
user_id
,pid
,substr(visit_time,0,10)) t
where t.islose = '0'
group by pid, vdate,uv;[/mw_shl_code]
udf:
[mw_shl_code=java,true]package test;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.apache.hadoop.hive.ql.exec.UDF;
/**
* 算时间差,用于计算蹦失率
* 目前支持时间单位:小时,天,周,月,年
*
* @author 圣斗士宙斯
*
*/
public class UDFTimeDiff extends UDF {
private static String isLose = "0";
private static final SimpleDateFormat sdf = new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss");
private static final String splitStr = ",";
private static List<String> list = new ArrayList<String>();
private static final long oneHour = 1000 * 60 * 60;
public String evaluate(final String timeDiff,
final String dateTime) {
String unitTimeDiff = timeDiff.substring(1);
if (timeDiff.length() == 2) {
String[] dataTimes = dateTime.split(splitStr);
// <=1 表示蹦失
if (dataTimes.length > 1) {
if ("h".equalsIgnoreCase(unitTimeDiff)) {
timeDiff(timeDiff, 1, dateTime);
} else if ("d".equalsIgnoreCase(unitTimeDiff)) {
timeDiff(timeDiff, 24, dateTime);
} else if ("w".equalsIgnoreCase(unitTimeDiff)) {
timeDiff(timeDiff, 24 * 7, dateTime);
} else if ("m".equalsIgnoreCase(unitTimeDiff)) {
timeDiff(timeDiff, 24 * 30, dateTime);
} else if ("y".equalsIgnoreCase(unitTimeDiff)) {
timeDiff(timeDiff, 24*365, dateTime);
}
} else {
return isLose;
}
} else {
try {
throw new Exception("第一个参数应该是两位字符串:1h表示1小时,"
+ "1d表示一天,1w表示一周,1m表示一个月,1y表示一年");
} catch (Exception e) {
e.printStackTrace();
System.out.println("请输入两位有效字符串!");
}
}
return isLose;
}
public static String timeDiff(final String timeDiff,final int n,
final String dateTime) {
String[] dataTimes = dateTime.split(splitStr);
for (int i = 0; i < dataTimes.length; i++) {
list.add(dataTimes);
Collections.sort(list);
}
int startTimeDiff = Integer.valueOf(timeDiff.substring(0, 1));
long diff;
Date timeN;
Date timeM;
long timeNLong = 0;
long timeMLong = 0;
for (int i = 0; i < list.size() - 1; i++) {
while ("0".equals(isLose)) {
try {
timeN = sdf.parse(list.get(i));
timeM = sdf.parse(list.get(i + 1));
timeNLong = timeN.getTime();
timeMLong = timeM.getTime();
diff = timeMLong - timeNLong;
if (diff <= startTimeDiff * n *oneHour) {
isLose = "1";
} else {
isLose = "0";
}
} catch (ParseException e) {
e.printStackTrace();
}
}
}
return isLose;
}
public static void main(String[] args) {
System.out.println(new UDFTimeDiff().evaluate("1h", "2017-01-08 08:41:14,2017-01-08 08:33:32"));
}
}
[/mw_shl_code]
讨论:在需求变更为按周,按月计算商品蹦失率时,我只需要稍微调整下传参和group by语法就ok,udf代码不用动
在java代码和hiveql语句中并不能保证最优,如果大家有更好的思路或者语法、参数可以提供,在此感谢!
有些商品的蹦失率计算口径可能不一样,比如粗粒度的有这种:一天某商品某用户pv<=1就算蹦失
接下来就可以把hiveql代码标准化一下可以参考我的上一篇文章 Hive高级编程之 hive -f 传参 http://www.aboutyun.com/forum.php?mod=viewthread&tid=21166&extra=
|
|