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

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

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层,后续的指标计算基于该层进行二次封装。要实现统一指标计算,上述的埋点数据需要进一步抽象,可以抽象为:时间、维度、度量。

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

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

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

    搭建一个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推荐系统

    机器学习PIPELINE

    pipeline这个词,应该来自linux。在linux体系下的各种命令工具的处理,支持pipeline,即管道机制,例如:

    cat xxx | awk '{xxxx}' | sort | uniq 

    这是一种良好的接口规范,工具的功能有公共的接口规范,就像流水线一样,一步接着一步。机器学习的处理过程,也可以是pipeline。实际上scikit-learn开发了整套的pipeline机制,并封装到 sklearn.pipline命名空间下面。首先,我们看看这个库都有什么:

    pipeline.FeatureUnion(transformer_list[, …])    Concatenates results of multiple transformer objects.
    pipeline.Pipeline(steps[, memory])  Pipeline of transforms with a final estimator.
    pipeline.make_pipeline(*steps, **kwargs)    Construct a Pipeline from the given estimators.
    pipeline.make_union(*transformers, **kwargs)    Construct a FeatureUnion from the given trans

    可以看出,最关键的是 FeatureUnion、Pipeline,我们继续看看这2个对象都可以实现什么功能。

    Pipeline

    sklearn中把机器学习处理过程抽象为estimator,其中estimator都有fit方法,表示“喂”数据进行初始化or训练。
    estimator有2种:
    1、特征变换(transformer)
    可以理解为特征工程,即:特征标准化、特征正则化、特征离散化、特征平滑、onehot编码等
    该类型统一由一个transform方法,用于fit数据之后,输入新的数据,进行特征变换。

    2、预测器(predictor)
    即各种模型,所有模型fit进行训练之后,都要经过测试集进行predict所有,有一个predict的公共方法

    上面的抽象的好处即可实现机器学习的pipeline,显然特征变换是可能并行的(FeatureUnion)可以实现,变换在训练集、测试集之间都需要统一,所以pipeline可以达到模块化的目的。举个NLP处理的例子:

    # 生成训练数据、测试数据
    X_train, X_test, y_train, y_test = train_test_split(X, y)
    
    # pipeline定义
    pipeline = Pipeline([
            ('vect', CountVectorizer()),
            ('tfidf', TfidfTransformer()),
            ('clf', RandomForestClassifier())
    ])
    
    # train classifier
    pipeline.fit(X_train, y_train)
    
    # predict on test data
    y_pred = pipeline.predict(X_test)

    显然,看起来pipeline训练过程只需要fit和predict,其实在pipeline内部传输过程,自动调用了fit\transform

    FeatureUnion

    上面看到特征变换往往需要并行化处理,即FeatureUnion所实现的功能。直接看例子:

    pipeline = Pipeline([
    ('features', FeatureUnion([
        ('text_pipeline', Pipeline([
            ('vect', CountVectorizer(tokenizer=tokenize)),
            ('tfidf', TfidfTransformer())
        ])),
        ('findName', FineNameExtractor())
    ])),
    
    ('clf', RandomForestClassifier())
    ])

    看起来,pipeline还可以嵌套pipeline,整个机器学习处理流程就像流水工人一样。上面自定义了一个pipeline处理对象FineNameExtractor,该对象是transformer,实际上自定义个transformer是很简单的,创建一个对象,继承自BaseEstimator, TransformerMixin即可,直接上代码:

    from sklearn.base import BaseEstimator, TransformerMixin
    class FineNameExtractor(BaseEstimator, TransformerMixin):
    
        def find_name(self, text):
            return True
    
        def fit(self, X, y=None):
            return self
    
        def transform(self, X):
            X_tagged = pd.Series(X).apply(self.find_name)
            return pd.DataFrame(X_tagged)

    执行一个PIPELINE,可能还少了点什么,再加上自动调参就完美了,是的,sklearn的调参通过GridSearchCV实现,pipeline+gridsearch简直是绝配。GridSearchCV实际上也有fit、predict方法,所以,你会发现,整个sklearn的机器学习是高效抽象的,代码可以写的很简洁。

    机器学习PIPELINE