3.项目记录将ODS层的数据处理成DWD层

将ODS层的数据通过清洗转换处理成DWD层的数据,保存成parquet格式

主要工作:

  • 数据规范处理,将一些字段处理成同一的规范(时间,日期,空字符统一...)
  • 将设备id,和用户账号同时为空的记录过滤
  • 将分析用的一个关键字段缺失的记录过滤
  • 过滤时间不符合的记录
  • 将数据扁平化
  • session分割(对App的用户两次操作的时间)

    1,对于web端日志,按天然session分割,不需处理

    2,对于app日志,由于使用了登录保持技术,导致app进入后台很长时间后,再恢复前台,依然是同一个session,不符合session分析定义,需要按事件间隔时间切割(30分钟)

    3,对于wx小程序日志,与app类似,session有效期很长,需要按事件间隔时间切割(30分钟)

  • 转换特定字段,例如gps,ip转换为位置信息(公司内部的位置词库)
  • 维度集成,将某些字段转换业务信息
  • 对数据进行业务分析之后,打上指定类型的标记
  • 形成系统内部的用户唯一标识

这里主要说一下[形成系统内部的唯一标识],其余的都是常规的处理,不在此赘述

这里碰到的问题就是  如何确定用户的唯一标识以及技术方案

1.直接使用设备ID

  • 同一用户在不同设备使用会被认为不同的用户,对后续的分析统计有影响。

  •  

    不同用户在相同设备使用会被认为是一个用户,也对后续的分析统计有影响。

2.一个设备绑定一个用户

  • 一个设备 ID 只能和一个登录 ID 关联,而事实上一台设备可能有多个用户使用。

  • 一个登录 ID 只能和一个设备 ID 关联,而事实上一个用户可能用一个登录 ID 在多台设备上登录。

3.一个用户绑定多个设备

  • 一个设备 ID 只能和一个登录 ID 关联,而事实上一台设备可能有多个用户使用。

  • 一个设备 ID 一旦跟某个登录 ID 关联或者一个登录 ID 和一个设备 ID 关联,就不能解除(自动解除)。

    而事实上,设备 ID 和登录 ID 的动态关联才应该是更合理的

4.动态绑定

方案设计

  • 当一个设备从来没有登陆过的时候,只能使用设备id,从登陆之后这个设备就id会和第一次登陆的用户id绑定
  • 如果一个设备一直被一个用户使用,那么就是和用户id绑定,如果偶尔出现一次其他用户使用这个设备,那么不会修改
  • 但是如果这个设备有两个用户,设备和登陆次数多的用户绑定,如果一个用户有两个手机,并且这两个设备id都是和此用户绑定,那么就按照两个用户计算

具体实现

对用户的日志数据进行批处理,针对上面的设计生成用户全局唯一标识对照表,每天都将表中的数据和日志进行匹配校验,保证数据的有效和及时性,

最终形成一个根据设备ID匹配用户编号的表

设备号用户列表(评分)
设备1用户1(300),用户2(900)
设备2用户2(200),用户3(400)
设备3用户1(700),用户3(200)

通过设备编号,以及评分找到没有登陆账号情况时候最有可能的用户是谁,为每条记录绑定一个Guid,最后将文件保存成parquet文件导入到数据库中,每天对表中的设备号对应的用户进行评分更新,将设备和最新的用户进行绑定

1.找出日志中的每个设备,用户的最早登陆时间
2.对每个设备进行聚合开窗,对同一设备的不同登陆用户的顺序进行加权打分
3.加上之前的结果,求出上表

核心代码(已简化)

  Logger.getLogger("org").setLevel(Level.WARN)
    val spark = SparkSession.builder().master("local[*]").appName("用户唯一标识映射").getOrCreate()
    val frame: DataFrame = spark.read.json("dw_etl\\data\\logdata\\event_log")

    //需要对每个数据进行处理,生成初步的映射关系表
    frame.printSchema()
    import spark.implicits._
    import org.apache.spark.sql.functions._
    val infoto3col = frame.select("account", "deviceId", "timeStamp")
    //将数据分两份,一份是有用户登陆的,另一份是没有用户登录的
    val fun1=s=>{StringUtils.isBlank(s)}
    spark.udf.register("isBlank",fun1)
    //登陆账号不是空的
    val accountIdIsNotNull = infoto3col.where("!isBlank(account)")
    //将登陆账号为空的过滤掉
    //然后对deviceId和accountID做分组聚合
    //找出同一个设备,同一个用户的最早登陆时间
    val devAndAccount = accountIdIsNotNull.groupBy('deviceId, 'account).agg(min('timeStamp) as 'ts)
    //根据设备id进行开窗,通过登陆时间排序,进行差别打分
    val spec = Window.partitionBy('deviceId).orderBy('ts)
    val rowNumAndDevAndAccount = devAndAccount.select('deviceId, 'account, 'ts, row_number() over (spec) as 'rownum).orderBy('deviceId, 'account)
    //对不同的登陆顺序进行加权评分
    rowNumAndDevAndAccount.selectExpr("deviceId", "account", "ts", "rownum","100-(rownum-1)*10 as score").show()
    //然后将此列表和之前的列表进行相加,求出得分最高的,就是该用户的可能性最大

输出结果如下: 

+------------+-------+-------------+-----------+-----------+
|    deviceId|account|      ts   | rownum | score     |
+------------+-------+-------------+-----------+-----------+
|111111111111|   1221|1598866481882|     3|   80|
|111111111111|   1223|1598861484882|     2|   90|
|111111111111|   1224|1598861481882|     1|  100|
|aaaaaaaaaaaa|   1221|1598861483882|     1|  100|
|aaaaaaaaaaaa|   1224|1598864481882|     2|   90|
|bbbbbbbbbbbb|   1224|1598861482882|     1|  100|
|szqwdecx78RA|   1221|1598861487882|     1|  100|
|szqwdecx78RA|   1223|1598862481882|     2|   90|
|szqwdecx78RA|   1224|1598865481882|     3|   80|
+------------+-------+-------------+-----------+-----------+

改进之处

在处理日志新老访客的时候,主要使用的是对设备ID和全局唯一标识同时判断,如果该设备使用过公司产品,那么就指定为老访客,但是考虑到用户更换手机登录而误判为新用户,所以加上了全局唯一标识,同时满足就是新用户,在设计之初,使用的是广播变量对用户设备表进行广播,但是随着数据量越来越大,该表的体积也渐渐不适合进行广播,然后有使用的单例对象在Executor端进行初始化,但是并没有真正解决根本性的问题,所以使用了布隆过滤器,迅速提高了新老处理的速度

布隆过滤器存在一定的误差,有一个专门的公式计算,可以通过调节拟定长度和hash次数降低误差

如果大量得数据都需要进行序列化持久到下一个stag得时候,可以使用Kryo序列化,还有avro跨平台序列化方式,极大得减少了序列化文件的大小,降低了网络和磁盘压力

核心代码(简化)

    public static void main(String[] args) {
        // 数据集,可以存在内存中,也可以是一个输入流
        String[] arr = "aa,bb,cc,dd,ee,abc,ddb,cce,xxy,xxa,abx,axy,aby,wr".split(",");
        //使用的时候直接创建一个布隆过滤,然后拟定的数量和精度
        BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 10000000, 0.00001);
        // 映射数据进布隆过滤器,将
        for(String s:arr){
            bloomFilter.put(s);//将数据集重点饿数据放入到其中
        }

        // 使用布隆过滤器来判断一个字符串是否存在于参考数据集中
        System.out.println(bloomFilter.mightContain("bbb"));
        System.out.println(bloomFilter.mightContain("bcc"));
        System.out.println(bloomFilter.mightContain("cce"));
        System.out.println(bloomFilter.mightContain("xxx"));
    }

 IDmapping的总体思路

加载之前的idmapping数据,将今天最早登陆该设备的用户+100分,对于第二个登陆的+90,依次类推,给每个设备计算出登陆过的用户的所有的分数,然后根据分数的降序,求第一条,此时的guid就是对应的分数最高的,然后

首先对日志中的数据根据设备和用户联合分组,开窗取出最小的日期,(这样过滤掉了一个用户的相同行为)

然后开窗找出同一个设备id下的不同登陆顺序,打上行号,将100相减行号*10,得到的就是差别得分,返回得分最高的作为guid

这时候就形成了IDmapping

   val preUidScore = spark.sql(
      """
        |
        |select
        |deviceid,
        |uid_score.account as account,
        |uid_score.timestamp as ts,
        |uid_score.score as score
        |
        |from preidmp lateral view explode(uid_list) tmp as uid_score
        |
        |""".stripMargin)
  val todayUidScoreResult = spark.sql(
      """
        |select
        |deviceid,
        |account,
        |ts,
        |score
        |from
        |(
        |select
        |
        |deviceid,
        |account,
        |ts,
        |score,
        |row_number() over(partition by deviceid order by account desc) as rn
        |
        |from
        |(
        |select
        |
        |deviceid,
        |account,
        |min(ts) as ts,
        |sum(score) as score
        |
        |from whole
        |group by deviceid,account
        |) o1
        |) o2
        |where !(rn>1 and account is null)
        |
        |""".stripMargin)

 

val finalResult = spark.sql(
      """
        |select
        |   deviceid,
        |   collect_list(if(account is not null,struct(account,timestamp,score),null)) as uid_list,
        |   max(guid) as guid
        |from
        |  (
        |     select
        |       deviceid,
        |       account,
        |       ts as timestamp,
        |       score,
        |       first_value(if(account is not null,account,deviceid)) over(partition by deviceid order by score desc,ts) as guid
        |     from
        |       tod
        |  ) o
        |group by deviceid
        |
        |""".stripMargin)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值