压在数据分析同学身上的”三座大山”

最近跟数据分析同学闲聊,收集了一波数据分析同学被挑战的问题。比较有趣的是,不同部门的数据同学提的问题,很大部分问题是重叠的。问题简述如下:

1、这个数据为啥不一致,靠谱不靠谱?
2、计算为啥这么慢,跑一段sql的事情要这么久么
3、筛选维度为啥不能自由组合,多选+汇总+展开都支持
4、数据需求就跑sql,为啥那么慢呢
5、虽然没数据,逻辑你先写吧
6、我要这个数据,下午给我
7、我们什么时候搞点机器学习项目
8、自己模仿跑sql,挑战结果不一致
9、报表数据不能跨年查询
10、报表上线后很长一段时间都会被反复追问口径和逻辑,用的哪个表,怎么算的
11、希望在报表侧自由选时间段并且数据是时间段内按人数去重汇总
...

有些问题比较类似,进行了合并,并命名为压在数据分析同学身上的"三座大山",再简述如下:

数据计算慢

1、查询慢,这个报表能不能用,跨年查询卡死了
2、计算慢,日报还没推送,今天还没出,为啥这么慢
3、实现慢,这个需求只要简单跑个sql,为啥那么慢

数据维度繁杂

1、导数支持,要汇总也要细分,更要明细,报表麻烦支持下
2、实时去重,希望根据不同时间,去重统计UV
3、多维支持,再加几个维度吧,现在十多个维度还有点少

数据准确性

1、指标差异,这2个指标看起来一样,数值对不上
2、对数差异,模仿跑了下sql,结果不一致,给我解释解释
3、逻辑验证,逻辑复杂,计算逻辑和口径无法确认准确性

暂不讨论解决方案,要完整的解决这些问题 ,底层计算方案、BI方案是需要review的。

不过,我们从中可以窥探出,需求方理解的数据需求实现,特别是PB数据需求实现,是有理解偏差的。可能很多产品或者前端同学对“大数据”意味着什么,并没有概念,所以需求就可能想当然。

简单挑几个点讲讲。

“数据计算慢”的问题,如果需求方说:“帮忙跑个sql”,并且附带一句:“这个需求很简单”,一般的数据分析师会默念或者画圈圈。“实现慢”是按需求者视角,不考虑实现者的需求并行负担。另外的真正的“计算慢”的问题一般跟实现相关,数据倾斜的概率比较大,数据分析师是要好好解决的。

“数据维度”的问题,从开源的实现方案看,apache kylin可以部分解决"数据维度"的问题,但是多维度也是需要预先定义好的。

只要统计数据又要明细数据是见过最多的统计需求,问题是,统计需求又要明细,那么意味着原始数据存储的量是特别大,必要性要好好评估的。往往指标数据、监控数据或者原始记录的需求放一起是不对。

“数据准确性”的问题,当然,准确性是数据分析的基础要求,往往挑战来自于不同的报表实现,过多的特殊逻辑导致。整个数据的DWS层需要统一管理,这样应用层只取一处数据可以缓解的这个问题。

压在数据分析同学身上的”三座大山”

聊一聊商业分析的主要关注点

商业,总体上关注成本和收入,钱是安身立命之本,所以从宏观维度看,成本构成、收入贡献是核心关注点,比如资产负载表、利润表、现金流量表。

从执行层面,可以继续细化,如下图:

  • 商家Business、用户Customer、客户关系CRM
  • 渠道Channels
  • 关键活动、核心资源、核心伙伴
  • CRM

    每个商业活动,我们希望不是一锤子买卖,所以客户关系建立和维系是很重要的,客户的发生和维系需要渠道能力支撑。

    CRM主要关注三个方面:
    1、用户信息建立,用户信息化
    2、关键活动记录,用户个性化
    3、用户关系管理,用户关怀

    渠道Channels

    渠道,包含收入渠道、支出渠道,包含各种触达用户的各种工具,比如:小程序、APP、PC、WEB、线下柜台。不同渠道成本、收入的效率是不一样的。渠道的种类和数量决定了商业规模,成本、收入的效率决定商业是否能否持续开展。

    渠道的管理也是需要不断拓展和维系的。用户、渠道、CRM我们可以统称为3C。渠道的关注三个主题:降本、提效、增收。不同的考核对象关注不同的指标。

    关键活动

    买货、卖货,需要很多的活动去开展,即拓展和运营。而拓展和运营受限于我们的所拥有的核心资源、核心伙伴。我们可以统称三者为3K。

    关键活动主要关注:
    1、活动目标
    2、投入成本
    3、效果情况

    核心资源、核心伙伴

    核心资源和核心伙伴是发展壮大的主要因素,商业活动不可能一人成事。每个关键活动,需要供应商、合作方协作推动。

    核心资源即竞争力,主要关注:
    1、研发资源
    2、客户资源
    3、品牌价值

    核心伙伴,主要考虑合作商家情况,比如供应商、开发商。

    聊一聊商业分析的主要关注点

    数据分析基础——关于埋点和数据的生成

    数据分析一般从ETL开始,数据分析师一般较少关注埋点和数据的生成、存储。我们补个缺,聊一聊数据埋点相关细节。希望看完本文之后你可以更好的跟前端、产品同学battle。

    一、埋点类型

    从数据生成方式,我们可以分为三类:
    1、全自动埋点
    2、半自动埋点
    3、手动埋点

    为了理解这三种埋点类型,我们先了解下技术背景。一般可以把一个页面或者模块的可视化生命周期抽象为:

    def init()    # 初始化
    def onShow()  # 可见
    def onHide()  # 隐藏到后台
    def onClose() # 关闭退出

    从程序实现角度,埋点的处理可以插入这4个可视化周期之一,这4个周期主要生成曝光、启动等非用户主动操作的指标。而曝光指标埋点上报写在onShow()中,即在页面渲染完上报。有一个关键点,手速太快曝光过多对数据有一定影响,从经验角度,需要卡延迟,比如显示500毫秒以上该模块曝光才上报。

    而用户行为产生一般有以下交互场景:

    def onHover()       # 获焦点
    def onClick()       # 点击
    def onLongTouch()   #  长按点击
    def onDoubleClick() # 双击

    从统计角度,一般只需获焦、点击行为,特别在移动端场景基本没有长按、双击场景,所以用户行为主要贡献点击类指标,通过点击的模块衍生出兴趣相关的指标。

    回来讲三种埋点类型:
    1、全自动埋点
    全自动意味着埋点求全,因为不知道你要什么,所以给你所有。埋点sdk通过代码插桩的方式mix in上述的几个可视化和点击事件。根据控件树,获取控件id和上下文的控件id,进行全量上报。

    2、半自动埋点
    只对可完全抽象的场景进行自动化埋点,比如主要页面和入口的曝光、点击。由于业务场景一般涉及自定义参数,比如渠道、活动、内容、场景等程序无法自动生成的上报进行自定义埋点开发。

    3、手动埋点
    即根据产品需求,case by case埋点。

    所有的埋点数据最后需要解析回来,也就是数据解读,全自动埋点解放了前端人力,但数据解读比较困难,并且带来的问题是控件id随着前端的重构会变得十分脆弱,ETL逻辑维护困难;而手动埋点又过于死板。相对合理的方式是:半自动埋点。

    然而,我们仍然需要优化埋点管理,我们需要抽象出脱离前端模块实现的埋点方案,即前端模块重构、下线不至于所有后续的指标统计推倒重来。下面我们继续聊怎么做到这一点。

    可以从4w来阐述,即who、where、when、what

    二、who: 我是谁

    我是谁包含:用户是谁、模块是谁。
    用户是谁,包含:appid、openid、sessionid,即app标识、用户标识、会话临时标识。更进一步,除了用户 标识,关于“用户是谁”可以包含终端信息,比如:终端品牌、型号、硬件id、imei等。

    模块是谁,包含:页面、模块两个维度,遵从树状结构设计,需要知道来源是谁、去向是谁。一个页面一般有多个模块,每个模又细分多种元素,所以模块需要统一的标识,前面说到前端控件ID存在重构丢失、重复的问题的,所以每个控件的生成需要从类对象的维度统一抽象,即定义抽象基类:

    class base_module
    {
         var pre_page     # 上一级页面
         var next_page    # 下一级页面
         var element_id   # 模块id
    }

    模块ID和模块的上下文是很关键的信息,ID不单单只是一个标记,ID隐含的信息可以包括:
    1、归属的页面
    2、归属的模块
    3、元素标识
    从这三个层面看,ID是一个多级结构,是有规则生成的,可以参考以下结构:

        页面.模块.元素

    由于数据上报和存储统一utf8存储,可以直接用中文定义模块id,或者换成字母递增:

        首页-热门推荐-运营位
        a1.b1.c1

    上面两者的定义是等效的。另外pre_page、next_page直接取a1这一维度的信息。归属的模块提取a1.b1这个维度的信息,a1.b1.c1则可定位到元素。如果模块之间嵌套多个模块,原则上可以继续拆分到4级,即a1.b1.c1.d1,过多层级需要考虑实际的使用场景。

    三、where: 在哪里

    who从某种意义上解决了部分的where的问题,从产品维度一般where指的是模块的位置信息。大部分的产品为tab页面,即多个页面,页面内容相似,页面内分为多个楼层,每个楼层有多个区块,可以定义以下结构:

    class base_module
    {
         var tab_idx         # tab页面id或者顺序,可缺省为0
         var pos_x           # 区块位置
         var pos_y           # 楼层位置
    }

    每个楼层可能出现两行,位置pos_x递增即可。位置信息可用于热力图绘制。

    where信息又包含用户或者设备所在的地域信息,即ip或者经纬度信息:

    class base_module
    {
         var ip              # ip,粗粒度的用户位置
         var longitude       # 经度
         var latitude        # 维度
    }

    三、when: 什么时候发生

    when是时间信息,从数据维度有2种时间:用户侧产生的时间、数据到达服务器的时间。分别记录即可。

    四、what: 内容是什么

    模块是个容器,可以承载文本、图片、多媒体,模块也可以只是一个简单的占位符。我们统称为内容,那么内容可以包含content-type、content_id。

    class base_module
    {
         var content-type     # 内容类型
         var content_id       # 内容id或者叫资源id
    }

    内容是个业务相关的,一般由业务cgi直接下发,也即前端、后台根据协议直接透传上报内容信息,可以是json结构。内容id涉及到内容分类或者更多的结构化分类信息 ,一般通过业务DB关联提取。

    综上所述,我们得到,数据埋点的通用结构如下:

    class base_module
    {
        var app_id
        var open_id
        var session_id
    
        var device_id
        var device_model
        var device_imei
    
        var pre_page         # 上一级页面
         var next_page       # 下一级页面
         var element_id      # 模块id
    
         var tab_idx         # tab页面id或者顺序,可缺省为0
         var pos_x           # 区块位置
         var pos_y           # 楼层位置
         var ip              # ip,粗粒度的用户位置
         var longitude       # 经度
         var latitude        # 维度
    
         var content-type    # 内容类型
         var content_id      # 内容id或者叫资源id
    
         var time_stamp      # 前端数据时间
    }

    五、不同终端的埋点问题

    现在的应用大概可以分为五种:h5、pc、安卓、ios、小程序。不同的终端类型获取数据的能力是有限的,比如:imei是移动端特有,h5、小程序一般获取不了硬件id,用户id体系较弱。安卓体系可以获取的信息最全,但安卓定制化较高,不同厂商之间硬件id可能重复。

    六、埋点数据质量

    统一的接口规范,那么埋点数据才有可测性,也减少不同人员之间的沟通成本。数据的质量从行和列两个维度看,“列”是约定定义集合,基本不动,可以自动化查是否缺漏。“行”即数据是否多报或者少报,这块是需要一个实时日志系统进行辅助验证的。该系统可根据openid白名单,实时反映数据上报情况即可。

    七、统一指标的设计

    埋点数据最终落地到数据集群,形成ods层,后续的指标计算基于该层进行二次封装。要实现统一指标计算,上述的埋点数据需要进一步抽象,可以抽象为:时间、维度、度量。

    时间:所有数据的计算是一定带有时间的,比如:分钟、天、周、月
    维度:即分类,可以有渠道分类、区块分类、用户群分类
    度量:大抵可以分为计数度量、求和度量、人数度量

    如果时间明确、维度一致、度量固定,是可以实现一个统一的指标计算系统的。当应用方需要使用到哪一方面的数据,直接提取即可。

    数据分析基础——关于埋点和数据的生成

    聚类分析的过程和两个常用的聚类算法

    聚类分析过程

    一般聚类分析的数据源是需要相对干净的,即需要做统一的特征清洗、特征变换过程,即空值、非法值、异常值、类别变量等的处理。主要过程如下:

    数据采集:我们可以认为是统一的ETL过程,这里涉及埋点、转发、存储、提取等过程。是典型的数据分析前置过程。

    特征变换/特征选择:聚类对异常数据特别敏感,同时原始数据直接进入聚类分析不大现实。特征处理包含行维度、列维度的处理,行维度主要包括:空值、非法值、异常值等方面的处理,而列维度涉及降维处理,冗余的列对聚类影响较大,所以一般聚类分析之前会做一次PCA。

    聚类分析、聚类评估:下文将重点举例描述

    结果解读:无监督学习,一般没有label,聚类的数量也是未知的,需要结合业务知识进一步解读聚类结果,比如从用户画像维度进一步切分同个分类的数据,从统计维度挖掘特征。

    知识发现:解读结果之后,需要落地实现或者输出报告,我们把这个过程称为知识发现的过程。聚类结果产生的label结果,往往可以作为监督学习的来源。

    下面举两个经典的聚类分析算法进一步说明。

    Kmeans

    基于距离聚类的聚类算法

    算法步骤:
    1、根据设定分类数量,随机生成N个中心点
    2、每个点计算与中心点距离,按最近距离合并分类
    3、基于2重新计算每个分类的中心点
    4、重复2~3,直到中心点收敛

    sklearn实现

    from sklearn.cluster import KMeans
    
    kmeans = KMeans(n_clusters=3)
    pred = kmeans.fit_predict(X)

    聚类效果

    GMM

    高斯混合模型是几个高斯分布的叠加,每一个分布代表一个分类

    算法步骤:
    1、设定分类数量
    2、对每一个高斯分布均值、方差随机初始化
    3、计算每个样本的在各个高斯分布的概率、权重值,Expectation-step
    4、根据最大似然重新估算高斯分布均值、方差,Maximization-step
    5、重复3~4直到高斯分布均值和方差收敛

    sklearn实现

    from sklearn.mixture import GaussianMixture
    
    gmm = GaussianMixture(n_components=3)
    gmm = gmm.fit(X)
    pred_gmm = gmm.predict(X)

    聚类效果

    模型评价

    GMM和KMeans分类结果有一定的随机性,可能得到局部优化解,需要多次微调。
    GMM是概率分类结果,可以对比topN分类,比如文档聚类;GMM计算代价更大,大数据集是个负担。

    评估指标

    聚类算法的评估主要衡量两个方面:同类紧密程度、类间分散程度;从不同指标看,同类越紧密、类间越分散,聚类效果越好
    基本上为无监督聚类,所以“Rand Index”之类的指标不是太实用,常用EIBOW方法 ,根据SSE评估聚类效果,另外对于聚类结果评估可以采用以下两种方式:

    1、Silhouette index

        s = (b-a)/max(a,b)
        ai:同分类内,单点与其他点的平均距离
        bi:分类中的点与最近分类点的平均距离
        a:所有点ai的均值
        b:所有点bi的均值

    基于轮廓的衡量方法,可能不适合 Single link、Complete link、DBSCANE等聚类方法。

    2、Calinski-Harabasz
    CH指标通过“同类”、“类间”协方差矩阵的迹(各个变量的方差)衡量,计算速度较快。

    聚类分析的过程和两个常用的聚类算法

    搭建一个mini推荐系统

    如果一个系统有用户和内容,那么根据不同用户推荐不同的内容,我们可以认为这就是推荐系统。我们接着细分:用户分为:新用户、老用户;内容分为:新内容、老内容

    所以一个mini推荐系统的推荐流程如下:

    WX20190531-191957@2x

    • 判断用户类型
      一般业务系统存储用户全表,即工程上可实现新旧用户判断。

    • 全局热点推荐
      按不同品类,可以出不同的排行榜,基于排行榜数据,可对新用户推荐排行榜内容,解决冷启动的问题。

    • 协同推荐
      对于留存用户,也即老用户,我们一般可以拿到用户的user-item的点击记录或者购买记录,采用user-based 或者item-based的协同过滤方案都可以实现推荐过程。
      其中 user-based,即为用户推荐用户,再根据推荐的用户提取用户的观看或者购买记录进行推荐;
      而item-based,主要根据用户点击的item,根据item间的相似度,推荐类似的item。

    一般协同过滤涉及比较大型的矩阵运算,为了降解矩阵计算过程,可以考虑FunkSVD方案,即把矩阵计算巧妙的采用梯度下降的优化方法实现。

    • 过滤观看历史
      没有人喜欢购买或者观看相同的物品,如果想要也可以从购物车或者观看历史找到,所以过滤观看历史是必要的。

    • 推荐内容补充
      推荐内容的数量可能较少,这个时候就需要补充推荐数据。基于content-based的内容推荐策略,可用于内容的补充,特别是新内容挖掘。

    上面是一个 mini推荐系统的实现过程,推荐过程也即排序过程,所以从工程实现上,可以把推荐分为两步:粗排、精排。”粗排“一般 考虑大的类目偏好的排序,矩阵维度可控;”精排“基于"粗排"结果进一步从内容池召回数据,然后经过re-rank,推荐给用户。

    搭建一个mini推荐系统