基于Spark的用户行为路径分析

一、研究背景

互联网行业越来越重视自家客户的一些行为偏好了,无论是电商行业还是金融行业,基于用户行为可以做出很多东西,电商行业可以归纳出用户偏好为用户推荐商品,金融行业可以把用户行为作为反欺诈的一个点,本文主要介绍其中一个重要的功能点,基于行为日志统计用户行为路径,为运营人员提供更好的运营决策。可以实现和成熟产品如adobe analysis类似的用户行为路径分析。最终效果如图。使用的是开源大数据可视化工具。如图所示,用户行为路径的数据非常巨大,uv指标又不能提前计算好(时间段未定),如果展示5级,一个页面的数据量就是10的5次方,如果有几千个页面,数据量是无法估量的,所以只能进行实时计算,而Spark非常适合迭代计算,基于这样的考虑,Spark是不错的选择。

 

二、解决方案

 

1.流程描述

客户搜索某一起始页面的行为路径明细数据时,RPC请求到后台,调用spark-submit脚本启动spark程序,Spark程序实时计算并返回数据,前端Java解析数据并展现。

 

2.准备工作

首先要有行为数据啦,用户行为日志数据必须包含必须包含以下四个字段,访问时间、设备指纹、会话id、页面名称,其中页面名称可以自行定义,用来标示一种或者一类页面,每次用户请求的时候上报此字段,服务器端收集并存储,此页面名称最好不要有重复,为后续分析打下基础。

 

然后对行为日志进行一级清洗(基于Hive),将数据统一清洗成如下格式。设备指纹是我另一个研究的项目,还没时间贴出来。会话id就是可以定义一个会话超时时间,即20分钟用户如果没有任何动作,等20分钟过后再点击页面就认为这是下个一会话id,可通过cookie来控制此会话id。

 

 

A、B、C代表页面名称,清洗过程采用row_number函数,concat_ws函数,具体用法可以百度。清洗完之后落地到hive表,后续会用到。T+1清洗此数据。

 

3.弄清楚递归的定义

递归算法是一种直接或者间接调用自身函数或者方法的算法。Java递归算法是基于Java语言实现的递归算法。递归算法的实质是把问题分解成规模缩小的同类问题的子问题,然后递归调用方法来表示问题的解。递归算法对解决一大类问题很有效,它可以使算法简洁和易于理解。递归算法,其实说白了,就是程序的自身调用。它表现在一段程序中往往会遇到调用自身的那样一种coding策略,这样我们就可以利用大道至简的思想,把一个大的复杂的问题层层转换为一个小的和原问题相似的问题来求解的这样一种策略。递归往往能给我们带来非常简洁非常直观的代码形势,从而使我们的编码大大简化,然而递归的思维确实很我们的常规思维相逆的,我们通常都是从上而下的思维问题, 而递归趋势从下往上的进行思维。这样我们就能看到我们会用很少的语句解决了非常大的问题,所以递归策略的最主要体现就是小的代码量解决了非常复杂的问题。

 

递归算法解决问题的特点:   

 

递归就是方法里调用自身。   

在使用递增归策略时,必须有一个明确的递归结束条件,称为递归出口。    

递归算法解题通常显得很简洁,但递归算法解题的运行效率较低。所以一般不提倡用递归算法设计程序。

在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储。递归次数过多容易造成栈溢出等,所以一般不提倡用递归算法设计程序。

 

在做递归算法的时候,一定要把握住出口,也就是做递归算法必须要有一个明确的递归结束条件。这一点是非常重要的。其实这个出口是非常好理解的,就是一个条件,当满足了这个条件的时候我们就不再递归了。

 

4.多叉树的基本知识

 

三、Spark处理

 

流程概述:

1.构建一个多叉树的类,类主要属性描述,name全路径如A_B_C,childList儿子链表,多叉树的构建和递归参考了这里(https://www.oschina.net/code/snippet_107039_38466)

 

2.按时间范围读取上一步预处理的数据,递归计算每一级页面的属性指标,并根据页面路径插入到初始化的Node类根节点中。

 

3.递归遍历上一步初始化的根节点对象,并替换其中的name的id为名称,其中借助Spark DataFrame查询数据。

 

4.将root对象转化成json格式,返回前端。

 

附上代码如下:

import java.util

 

import com.google.gson.Gson

import org.apache.spark.SparkContext

import org.apache.log4j.{Level, Logger => LG}

import org.apache.spark.sql.DataFrame

import org.apache.spark.sql.hive.HiveContext

 

/**

  * 用户行为路径实时计算实现

  * Created by chouyarn on 2016/12/12.

  */

 

/**

  * 树结构类

  *

  * @param name      页面路径

  * @param visit     访次

  * @param pv        pv

  * @param uv        uv

  * @param childList 儿子链表

  */

class Node(

            var name: String,

            var path:Any,

            var visit: Any,

            var pv: Any,

            var uv: Any,

            var childList: util.ArrayList[Node]) extends Serializable {

  /**

    * 添加子节点

    *

    * @param node 子节点对象

    * @return

    */

  def addNode(node: Node) = {

    childList.add(node)

  }

 

  /**

    * 遍历节点,深度优先

    */

  def traverse(): Unit = {

    if (childList.isEmpty)

      return

    //    node.

    val childNum = childList.size

 

    for (i <- 0 to childNum - 1) {

      val child: Node = childList.get(i)

      child.name = child.name.split("_").last//去除前边绝对路径

      child.traverse()

    }

  }

 

  /**

    * 遍历节点,深度优先

    */

  def traverse(pages:DataFrame): Unit = {

    if (childList.isEmpty||childList.size()==0)

      return

    //    node.

    val childNum = childList.size

 

    for (i <- 0 to childNum - 1) {

      val child: Node = childList.get(i)

      child.name = child.name.split("_").last

      val id =pages.filter("page_id='"+child.name+"'").select("page_name").first().getString(0)//替换id为name

      child.name = id

      child.traverse(pages)

    }

  }

 

  /**

    * 动态插入节点

    *

    * @param node 节点对象

    * @return

    */

  def insertNode(node: Node): Boolean = {

    val insertName = node.name

    if (insertName.stripSuffix("_" + insertName.split("_").last).equals(name)) {

      //      node.name=node.name.split("_").last

      addNode(node)

      true

    } else {

      val childList1 = childList

      val childNum = childList1.size

      var insetFlag = false

      for (i <- 0 to childNum - 1) {

        val childNode = childList1.get(i)

        insetFlag = childNode.insertNode(node)

        if (insetFlag == true)

          true

      }

      false

    }

  }

}

 

/**

  * 处理类

  */

class Path extends CleanDataWithRDD {

  LG.getRootLogger.setLevel(Level.ERROR)//控制spark日志输出级别

 

  val sc: SparkContext = SparkUtil.createSparkContextYarn("path")

  val hiveContext = new HiveContext(sc)

 

  override def handleData(conf: Map[String, String]): Unit = {

 

    val num = conf.getOrElse("depth", 5)//路径深度

    val pageName = conf.getOrElse("pageName", "")//页面名称

    //    val pageName = "A_C"

    val src = conf.getOrElse("src", "")//标示来源pc or wap

 

    val pageType = conf.getOrElse("pageType", "")//向前或者向后路径

    val startDate = conf.getOrElse("startDate", "")//开始日期

    val endDate = conf.getOrElse("endDate", "")//结束日期

    //        保存log缓存以保证后续使用

    val log = hiveContext.sql(s"select fpid,sessionid,path " +

      s"from specter.t_pagename_path_sparksource " +

      s"where day between '$startDate' and '$endDate' and path_type=$pageType and src='$src' ")

      .map(s => {

        (s.apply(0) + "_" + s.apply(1) + "_" + s.apply(2))

      }).repartition(10).persist()

 

    val pages=hiveContext.sql("select page_id,page_name from specter.code_pagename").persist()//缓存页面字典表

    // 本地测试数据

    // val log = sc.parallelize(Seq("fpid1_sessionid1_A_B",

    //      "fpid2_sessionid2_A_C_D_D_B_A_D_A_F_B",

    //      "fpid1_sessionid1_A_F_A_C_D_A_B_A_V_A_N"))

    var root: Node = null

    /**

      * 递归将计算的节点放入树结构

      *

      * @param pageName 页面名称

      */

    def compute(pageName: String): Unit = {

      val currenRegex = pageName.r //页面的正则表达式

      val containsRdd = log.filter(_.contains(pageName)).persist() //包含页面名称的RDD,后续步骤用到

      val currentpv = containsRdd.map(s => {//计算pv

        currenRegex findAllIn (s)

      }).map(_.mkString(","))

        .flatMap(_.toString.split(","))

        .filter(_.size > 0)

        .count()

 

      val tempRdd = containsRdd.map(_.split("_")).persist() //分解后的RDD

      val currentuv = tempRdd.map(_.apply(0)).distinct().count() //页面uv

      val currentvisit = tempRdd.map(_.apply(1)).distinct().count() //页面访次

 

      //      初始化根节点或添加节点

      if (root == null) {

        root = new Node(pageName,pageName.hashCode, currentvisit, currentpv, currentuv, new util.ArrayList[Node]())

      } else {

        root.insertNode(new Node(pageName,pageName.hashCode, currentvisit, currentpv, currentuv, new util.ArrayList[Node]()))

      }

 

      if (pageName.split("_").size == 5||tempRdd.isEmpty()) {//递归出口

        return

      } else {

        //          确定下个页面名称正则表达式

        val nextRegex =

        s"""${pageName}_[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}""".r

        // 本地测试       

        // val nextRegex =s"""${pageName}_[A-Z]""".r

        val nextpvMap = containsRdd.map(s => {//下一级路径的pv数top9

          nextRegex findAllIn (s)

        }).map(_.mkString(","))

          .flatMap(_.toString.split(","))

          .filter(_.size > 0)

          .map(s => (s.split("_").last, 1))

          .filter(!_._1.contains(pageName.split("_")(0)))

          .reduceByKey(_ + _).sortBy(_._2, false).take(9).toMap

 

        nextpvMap.keySet.foreach(key => {//递归计算

          compute(pageName + "_" + key)

        })

      }

    }

    //触发计算

    compute(pageName)

    val gson: Gson = new Gson()

 

    root.traverse(pages)

    root.name=pages.filter("page_id='"+pageName+"'").select("page_name").first().getString(0)

    println(gson.toJson(root))//转化成JSON并打印,Alibaba fsatjson不可用,还是google得厉害。

  }

 

  override def stop(): Unit = {

    sc.stop()

  }

}

 

object Path {

  def main(args: Array[String]): Unit = {

    //    println("ss".hashCode)

    var num=5

    try {

      num=args(5).toInt

    }catch {

      case e:Exception =>

    }

 

    val map = Map("pageName" -> args(0),

      "pageType" -> args(1),

      "startDate" -> args(2),

      "endDate" -> args(3),

      "src" -> args(4),

      "depth" -> num.toString)

    val path = new Path()

    path.handleData(map)

  }

}

 

四、总结

Spark基本是解决了实时计算行为路径的问题,缺点就是延迟稍微有点高,因为提交Job之后要向集群申请资源,申请资源和启动就耗费将近30秒,后续这块可以优化。据说spark-jobserver提供一个restful接口,为Job预启动容器,博主没时间研究有兴趣的可以研究下啦。

 

fastjson在对复杂对象的转换中不如Google 的Gson。

 

使用递归要慎重,要特别注意出口条件,若出口不明确,很有可能导致死循环。

### 回答1: 基于Spark的电商用户行为分析系统的代码可以包含以下几个方面的实现: 1. 数据采集:通过编写Spark Streaming代码实时采集电商网站用户行为数据,例如浏览商品、下单、支付等行为数据,可以使用Kafka作为消息队列来接收数据。 2. 数据清洗和预处理:对采集到的用户行为数据进行清洗和预处理,去除无效数据,处理缺失值、异常值等,以确保数据质量和准确性。 3. 数据存储:将预处理后的数据存储到Hadoop分布式文件系统(HDFS)或者NoSQL数据库(如HBase、Cassandra)中,以便后续的离线分析。 4. 数据分析:利用Spark的分布式计算能力,编写Spark SQL或Spark DataFrame代码对存储在HDFS或NoSQL数据库中的用户行为数据进行离线分析,如用户留存率分析、用户购买路径分析、热门商品推荐等。也可以使用Spark MLlib进行用户行为的机器学习建模,例如通过用户历史数据构建推荐模型。 5. 可视化展示:将分析结果通过数据可视化工具(如ECharts、D3.js)展示出来,生成各类图表、报表,以便业务人员进行数据解读和决策。 在代码实现过程中,可以利用Spark提供的各种接口和算子,如Spark Streaming实现实时数据采集和处理,Spark SQL和DataFrame实现数据分析和查询,Spark MLlib实现机器学习算法,以及各类数据连接器和工具来处理数据存储和可视化展示。还需要注意分布式计算中的数据分区和并行计算,以优化Spark作业的执行性能。 ### 回答2: 基于Spark的电商用户行为分析系统的代码主要包括以下几个模块: 1. 数据预处理模块:这个模块用于将原始的电商用户行为数据进行清洗和处理,例如去除无效数据、处理缺失值、转换数据格式等。代码中会使用Spark的DataFrame API或SQL语句来完成数据的预处理工作。 2. 特征提取模块:这个模块用于从用户行为数据中提取有效、有意义的特征供后续的分析使用。代码中会使用Spark的特征提取工具库,如MLlib或ML库,来进行特征的提取和转换。 3. 用户行为分析模块:这个模块用于基于提取的特征对电商用户行为数据进行分析,例如用户购买行为的预测、用户兴趣分类等。代码中会使用机器学习算法,如分类、回归、聚类等,来进行用户行为的分析和预测。 4. 结果可视化模块:这个模块用于将分析得到的结果可视化展示,以便用户更直观地理解分析结果。代码中会使用数据可视化工具,如Matplotlib、Seaborn或Plotly等,来进行结果的可视化展示。 5. 分布式计算模块:这个模块用于实现Spark的分布式计算能力,以支持对大规模数据集的处理和分析。代码中会使用Spark的分布式计算框架,如Spark Core或Spark SQL,来实现分布式计算任务。 总之,基于Spark的电商用户行为分析系统的代码主要包括数据预处理、特征提取、用户行为分析、结果可视化和分布式计算等模块。通过这些模块的组合和调用,能够构建一个高效、可扩展的用户行为分析系统,实现对电商用户行为的深入理解和预测。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值