cover_image

货拉拉广告投放中的归因平台建设

大数据技术团队 货拉拉技术
2023年08月03日 10:17

背景

对于广告主和市场营销部人员,合理的规渠道营销预算,实现ROI最大的是每个人非常关心的目标,但是面对现今各式各样的推广渠道时,常常会有如下的一些困扰:

  • • 用户来自哪个渠道,具体哪个广告计划?

  • • 哪个渠道带来的新用户最多,哪个渠道适合促活?

  • • 哪个渠道的转化最好,付费用户最多?

  • • 如何低成本跑量?

  • • 如何优化广告模型?

而这些归因平台都可以帮助到你,借助归因平台,我们可以:

  • • 了解每个渠道下详细的归因数据,计算不同渠道的ROI,用有限的资源分配在有效的渠道上,让产出最大化

  • • 了解不同渠道的用户价值、受众目标和行为习惯,快速调整投放策略。

  • • 动态调节渠道的转化量及人群,优化广告模型

  • • 提供更加客观的理由以取得市场预算。

百货商店之父约翰.沃纳梅克(John Wanamaker,1838~1922)说过:“我在广告上的投资有一半是无用的,但是问题是我不知道是哪一半。”

而在广告投归因技术的加持下,约翰大叔可以轻松的知道哪一半广告是无用的,当你有很好的洞察力时,很容易做出好的选择。

业务简介

 广告归因平台(Ad Attribution Platform)是一种用于追踪和分析广告投放效果的平台,它可以帮助市场人员了解用户是如何通过不同的广告渠道、媒体或创意找到并使用我们的产品或服务。了解哪些广告活动和渠道在吸引潜在客户,并帮助市场人员发掘并确认广告投放的效果和价值优化媒体模型,从而有针对性的优化投放策略,提高转化率和提升ROI。常见的广告归因平台包括AppsFlyer、Adjust、Kochava、友盟等。

为了方便大家理解该平台,我们对业务流程做个大概的了解,归因流程会因归因方式的不同而有所变动,大致分为两种,点击归因(映射归因)和链路归因(渠道归因),我们以较常见的点击归因做为示例,来了解广告投放中的归因流程。

点击归因示例:

图片

1.广告投放

广告投放是广告归因的起点,需要根据广告投放的渠道、目标受众、投放时段等因素确定广告投放的策略和计划,选择适合的广告平台、设置广告位和预算、目的是让公司的产品、服务曝光

2.用户点击

用户的点击是指用户点击广告并进入落地页的行为,这是广告效果追踪和分析中非常重要的一环,也是用户了解到产品、服务的触点

3.媒体上报点击数据

在用户点击后,媒体发送用户的点击数据(触点数据),点击数据中包含imei、oaid、idfa、mac等一个或多个设备唯一标识及该广告的相关参数

4.上报埋点数据

将自有APP运行过程中所记录的数据上传到服务器或云端,总之,APP埋点数据的上报需要合理规划和精细化设计,同时需要注意数据的准确性、通信安全性和稳定性,确保数据分析和统计的准确性和可靠性。

5.消费埋点数据

埋点数据一般较多,我们将APP上报的埋点数据推送至数据采集端push到Kafka,然后在归因平台中消费上报的埋点数据

6.归因

是整个归因环节的核心,这里涉及到归因模型的选择,目前国内使用的都是末次归因也就是最终点击,以及归因方式的选择,数据去重等等,最后我们将埋点数据进行处理后和媒体提供的点击数据进行匹配,如果匹配成功则归因成功,反之亦然;

7.数据上报

将成功归因的数据回传给媒体,媒体根据回传的数据优化投放模型,扩展人群,精准推送

8.数据分析

将媒体消费、点击量、展示量、点击、下载、付费等数据回收,根据归因分析的结果和结论对广告投放策略和计划进行优化和调整,例如优化广告定向、调整广告位和投放时间等。分析时需要综合考虑多个因素和因素之间的相互作用,例如广告类型、投放渠道、投放时段等。

架构设计

技术架构

图片

广告归因的技术架构需要考虑到数据采集、数据处理、数据存储、数据匹配、数据上报、服务配置和数据可视化等方面,我们将其拆分为4个服务。

数据采集

  • • 外网服务对外暴露接口,提供所有媒体的下发数据、授权回调,包括数据处理、数据采集

  • • 对外提供新老设备判断能力,支持媒体前置去重

  • • 服务对稳定性及弹性伸缩要求较高

实时归因

  • • 只处理归因相关的逻辑,没有媒体的概念,与媒体解耦

  • • 成功归因的归因数据推送至数据上报Kafka

  • • 需实时消费所有Kafka中的埋点数据,进行设备匹配,对服务性能要求较高支持弹性伸缩

数据上报

  • • 消费数据上报Kafka,提供独立的数据外发服务,保障外发数据风险可控、可终止

  • • 能真正归因上的数据体量并不大,所以对性能要求不高

归因配置、数据监控及数据查询平台

  • • 内网可视化配置平台,提供新媒体配置接入、埋点管理、归因计划管理、数据监控、数据查询、联调等服务

服务架构

图片

数据收集

广告归因的第一步是采集广告数据,也是广告归因的重要环节,需要对广告触达用户的行为和反应进行准确的记录和收集,例如媒体下发的用户曝光、点击、下载等数据,记录用户点击广告的时间、地理位置、设备信息、浏览行为等,该类数据中必须包含设备的唯一标识和媒体回传所需的数据,缺失设备唯一标识会导致归因精度下降或无法归因,缺失媒体回传所需数据会导致上报失败,其他媒体数据则根据自身的业务需求自取。

技术难点和解决方案

数据层主要难点是数据稳定、数据存储、数据查询在投放中媒体下发的数据一旦丢失则很有可能导致归因失败或归因错误。每天千万的曝光、百万的点击数据存储也是个不小的考验,在进行实时归因时也需要数据能及时查询

技术实现

稳定性方案: 在广告投放中发现潮汐现象非常明显,在每天的9:30、15:00会有波高峰,凌晨4:00左右跌入谷底周而复始,因此我们引入Kafka,一是可以削峰填谷得到一个相对平稳的流量,二是高峰期服务压力也能得到缓解进而保障数据完整。

图片

数据存储方案: 我们使用HBase存储数据,主要基于以下原因:

  • • HBase可以存储海量的数据,实时读写随机访问超大规模数据

  • • 空字段不占用空间,吞吐量大

  • • 具有高可扩展性,能动态地扩展整个存储系统。

  • • 业务上我们不需要JOIN

它可以增量存储海量的点击流和用户交互数据,并已经成功地应用在该领域。

难点:rowkey的设计,避免数据热点,注意长度,一般设计成定长。建议越短越好,不要超过 16 个字节

数据查询方案: 我们通过引入Redis,来避免在波峰时数据消费不及时问题,例如在广告投放高峰期,用户点击广告后马上下载APP并打开,在业务介绍中的第5点,如果比第3步的数据先到,就会导致归因时查询不到对应的点击数据,而实际上用户是点击了广告,高峰时数据积压会导致Kafka未及时消费造成的数据延时,当然引入Redis并不是必须,因为数据适当的延时可以规避一些作弊行为,正常用户从点击->激活我们一般认为超过30S比较合理。合理配置consumer和服务节点,消费延时很少超过30S,但Redis的引入可以减低数据延时风险。 RedisKey的设计: 需要明确查询条件,哪些字段需要参与查询,并注意长度,建议越短越好,我们数据拆分为两类多条,一类为精准归因数据,另一类为模糊归因数据

  • • 精准归因使用:归因前缀att+推广端ID+设备标识符

  • • 模糊归因使用:归因前缀att+推广端ID+IP+osVersion+Model,模糊归因窗口为2min

/**
 * 伪代码
 * 点击数据Redis过期时间
 * 单位分
 */

@Value("${redisClickDataExp:30}")
private Integer redisClickDataExp;

final String[] IDENTIFIER_FIELDS = {"oaid""imei""androidId""idfa"};

// 处理精准归因
for (String field : IDENTIFIER_FIELDS) {
    String key = "att:" + PromoteClient.USER.getCode() + field + attributionDataMap.get(field);
    redisTemplate.opsForValue().set(DigestUtils.md5DigestAsHex(key.getBytes()), JsonUtil.toJSONString(attributionData),  redisClickDataExp, TimeUnit.SECONDS);
}

// 处理模糊归因
String fuzzyKey = "att:" + PromoteClient.USER.getCode() + attributionData.getProperties().getIp() + attributionData.getProperties().getOsVersion() + attributionData.getProperties().getModel();
redisTemplate.opsForValue().set(DigestUtils.md5DigestAsHex(fuzzyKey.getBytes()), JsonUtil.toJSONString(attributionData),  2, TimeUnit.MINUTES);

实时归因

是整个归因系统的核心部分,我们希望整个归因过程是可控、可靠、可追溯

1.埋点获取

技术难点和解决方案

公司所推广的资产较多,每个产品上的埋点比较丰富,为了能实时消费海量的上报的埋点数据,我们将神策埋点数据推送到公司Kafka集群,在消费埋点数据时,我们首先需要辨别哪些是该回传目标对应的埋点,这其实也很好实现写几个判断就好了,但在实际应用时会发现,市场部同事会阶段性添加转化目标,致使我们每次都需获取新的埋点,而且端上也会有埋点版本迭代,埋点的变更意味着我们也需要发版跟进。随着获取的埋点越来越多,if else变的不易维护,急需让业务逻辑和数据分离,基于这些痛点我们引入了drools作为我们获取埋点的核心,我们可以在运行时动态配置规则引擎来获取自己想要的埋点,代码也变的易于维护。

技术实现

消费埋点Kafka,每个consumer对应一个规则引擎实例,使用规则引擎动态配置埋点规则筛选我们需要的埋点

图片

归因平台消费Kafka

// 伪代码
// 消费Kafka
@Override
public void handle(ConsumerRecord<Object, Object> record) {
 // 反序列化埋点数据
 AttributionData attributionData = JsonUtil.parseJson(String.valueOf(record.value()), AttributionData.class);

 // 根据埋点规则筛选需要的埋点
 StatelessKieSession kieSession = evenTrackRulesService.getKieSession();
 kieSession.execute(attributionData);
}

前面我们也介绍我们拆分了服务,所以我们在配置平台配置我们想要的规则,然后通过MQ推送到归因平台

图片

归因平台--动态更新

// 伪代码

@Override
public StatelessKieSession getKieSession() {
    if (kieSession == null) {
        kieSession = kieContainer.newStatelessKieSession();
        kieSession.setGlobal("ss", strategyService);
    }
    return kieSession;
}

/**
 * 动态加载规则
 * 每次规则推送时执行
 * @param ruleEntityMap
 * @return
 */

private KieContainer loadContainer(Map<String, EventTrackRuleEntity> ruleEntityMap) {
    KieServices ks = KieServices.Factory.get();
    KieRepository kr = ks.getRepository();
    KieFileSystem kfs = ks.newKieFileSystem();
    for (Map.Entry<String, EventTrackRuleEntity> map : ruleEntityMap.entrySet()) {
        String drl = map.getValue().getRule();
        kfs.write("src/main/resources/" + drl.hashCode() + ".drl", drl);
    }
    KieBuilder kb = ks.newKieBuilder(kfs);
    kb.buildAll();
    if (kb.getResults().hasMessages(Message.Level.ERROR)) {
        log.error("rule online failed, msg: {}", kb.getResults().toString());
        throw new BizException(BizErrorCode.DROOL_K_CONTAINER_ERR, kb.getResults().toString());
    }
    KieContainer kContainer = ks.newKieContainer(kr.getDefaultReleaseId());
    return kContainer;
}

规则示例

package mkt
import cn.huolala.mkt.model.dto.AttributionData
import cn.huolala.mkt.service.EventTrackConsumerService
global cn.huolala.mkt.service.StrategyService ss

rule "0"
  when
    $e : AttributionData(projectId == 4 && event=="login_success")
    then
      ss.webUserHandle($e, drools.getRule().getName());
end

2.去重

我们投放广告主要目的是为了拉新,而媒体侧是无法判断投放的用户是否为广告主已有的用户,这时就需要我们来完成去重的工作,有时用户可能重复多次触发同一个埋点,重复埋点数据也会导致多次归因,我们也需要规避这类重复的数据,所以做好去重是我们筛选出有效新用户的重要环节。

技术难点和解决方案

设备唯一标识去重: Android:AndroidID 去重,iOS:IDFA 或 IDFV 去重,小程序:OPEN_ID 去重。

业务唯一标识去重: 例如用户可以使用userId去重,userId和手机号绑定,为什么不直接用手机号去重呢?因为手机属于用户敏感信息,埋点要注意保护用户的个人隐私。司机可以使用driverId去重,driverId和身份证绑定等业务中的唯一标识去重。利用用户在业务系统的唯一标识进行去重,可以避免设备去重不严谨或设备唯一标识缺失等情况下的去重,该去重方式在过滤H5类广告的老用户时非常有效。

埋点标识(推荐): 设备唯一标识去重、业务唯一标识去重、两种去重方式的非常依赖于实时库的数据量,如果数据足够全去重效果很好,但海量的数据又会增加机器成本和维护成本,所以可以采用端上去重端上埋点时,添加是否新用户字段,而且端上后台就是业务库,他们有全量的数据,也不需要我们再去处理海量数据的存储问题

深度回传去重: 深度回传指用户在使用认证、选择车型、下单等操作的转化目标回传,通常在用户转换漏斗的下方。虽然国内大媒体都有该去重逻辑,但为了适配一些小众媒体我们也建设了该能力,该去重方法是在归因之后上报之前,根据设备唯一标识+事件进行去重

图片

技术实现

//伪代码

// 判断是否设备唯一标识去重
if (Boolean.TRUE.equals(attributionData.isDeviceIdDeduplication())) {
    //检查是否已经激活过,仅通过device_id去重
    Long installedId = eventTrackAppInstallService.findInstalledIdByDeviceId(deviceId);
    if (installedId != null) {
        return;
    }
}

// 判断是否设备唯一标识去重
if (Boolean.TRUE.equals(attributionData.isUserIdDeduplication())) {
    // userId 为业务唯一ID
    Long installedId = eventTrackAppInstallService.findInstalledIdByUserId(userId);
    if (installedId != null) {
        return;
    }
}

// 判断是否设备唯一标识去重
if (Boolean.TRUE.equals(attributionData.isDeepDeduplication())) {
    // 根据归因数据查询
    Long installedId = eventTrackAppInstallService.findInstalledIdByUploadId(uploadId);
    if (installedId != null) {
        return;
    }
}

3.归因模型

归因模型用于在多个媒体同时投放时确定广告功劳和转化功劳分配给转化路径中的哪个用户触点,可以理解为为处理同一个用户在多家媒体中看到了广告并发生了转化的功劳的归属划分问题提供的一套解决方案。

  • • 最终点击:将转化功劳全部归于客户最后点击的那个广告和相应的关键字。

  • • 首次点击:将转化功劳全部归于客户首次点击的那个广告和相应的关键字。

  • • 平均:将转化功劳平均分配给转化路径上的所有广告互动。

  • • 时间衰减:广告互动越接近转化发生时间,分配的功劳就越多。

  • • 根据位置:为客户首次广告互动及相应关键字以及最终广告互动及相应关键字分别分配 40% 的功劳,将其余 60% 的功劳平均分配给转化路径上的其他广告互动。

图片

想了解更多归因模型可以查阅广告归因模型解析[1]

4.归因逻辑

归因是我们整个系统中最重要的部分,但它的原理并不复杂。

映射归因

技术难点和解决方案

其主要原因是广告归因数据的断裂导致追踪数据无法透传,需要通过设备唯一标识来重新映射追踪数据,那具体是怎么归因呢?

Android: 窗口期 + 四层归因,根据归因精度依次是:imei > oaid > androidId > Model + version + ip + 2min

iOS: 窗口期 + IDFA,实际大部分拿到是IDFV,iOS自从2020年发布iOS14后IDFA的获取需要单独经过用户允许,行业预计只有20%左右的用户会进行授权;而如果80%的用户不进行授权,那么绝大部分APP的都不能进行标识用户身份、归因、推荐、唤醒等。行业中也有解决方案,比如使用Device Fingerprint,设备fingerprint就是利用设备的各种信息:操作系统、语言、浏览器、SDK 版本等,进行归因,但fingerprint的归因准确性只有75~80% 左右,目前iOS的投入不是重点,所以这块暂时继续使用IDFA归因

小程序: 窗口期 + OPEN_ID,是同一应用/小程序/公众号下的用户唯一标识,我们通过该标识可以很轻松的完成归因

链接归因

渠道归因该归因过程较为简单,而且还不涉及到用户隐私(主要指用户设备号)所以就不过多介绍了,该归因方式也有一定局限,因为舍弃了设备标识,所以该链路上投放的带来的用户,无法做人群分析、促活、也无法进行RTA去重

5.转化目标绑定

在实际的投放中,有时候会为了追求流量会将浅层事件回传到深度目标上,以此来达到深度放量的效果,在广告流量正常后又需要将埋点事件和转化目标回归正常,在设计时可以考虑以下几个方面:

  • • 将媒体的转化目标配置化可枚举放在媒体配置之中

  • • 将页面的埋点配置化可枚举

  • • 建立归因计划,在计划中确定转化目标和埋点的映射关系

上报层

主要解决国内千姿百态的媒体API接入,目前公司已接入20+广告公司,在后期新媒体接入或媒体接口升级时,可以做到少量开发快速接入,当然也支持限流、深度去重、以及窗口期限制等能力,我们重点关注一下怎么通过配置化接入媒体。在进行配置设计时,需要对所有媒体的接口进行抽象和归纳,我们可以将其大致理解为一个可配置化的HTTP请求,那顺着这思路,我们知道发起一个HTTP请求的必要条件,包含请求地址、请求方式、请求头、请求参数,而在这些必要条件中相信大家最关注的肯定是请求头和请求参数,接下来我们就重点讲解一下这两个部分。

请求头

  • • 技术难点和解决方案 除了常规的 content-type: application/json,主要难点来自媒体的上报认证字段,我们根据密钥的生成方式拆分成三类配置:

静态密钥: 该类可以理解固定密钥较简单,一般小媒体会使用该种方式,由媒体提供密钥在上报时直接透传,那我们只需给其一个key-val配置的List即可解决。

动态密钥: 该类密钥指在已有的请求数据、账户密钥、上报地址根据一定加密算法进行加密组合,如常见MD5加密,我们会在该方式中对部分媒体独有的加密过程进行封装,将其作为一种特殊的可选加密方式,比如华为的密钥生成,我们就将其封装为**huawei Encode,**以此来处理这些特殊的媒体。

授权密钥: 该类密钥通过签名进行应用授权的方式获取上报的密钥

请求参数

技术难点和解决方案

  • • 数据结构,每家媒体都有自己独有的数据结构,怎么通用化

技术实现

  • • 我们将整个上报请求体的数据结构做成一种树型结构,这样每一层数据结构都只需要指向他的父节点,就可以拼装出期望的数据结构。

  • • 举个例子比如现在有一种结构体要求如下

    {
        "account":xxxx,
        "device_type":xxxx,
        "action":{
            "user_id:":xxxx,
            "click_id":xxxxx,
            "oaid":xxxx,
            }
        "track":{
            "track_time":xxxx
            }
    }
  • • 那么我们将他做成树型结构后得到以下关系:

account ->根节点(body)

device_type ->根节点(body)

action ->根节点(body)

user_id ->action

click_id ->action

oaid ->action

track ->根节点(body)

track_time ->track
  • • 当我们得到一个完整数据结构后,再进行参数拼接时只需要需上报数据装进对应的节点,最终将每个节点都装进其对应的父节点,就可以很灵活的拼装出我们所需的数据包。

  • • 对于归因数据字段和媒体上报字段我们再做个映射关系即可

  • • 对于归因字段的数据处理比如MD5、BASE64、URL Encode我们可以使用配置化方法处理

访问层

访问层主要使用前端VUE框架,这也是比较常见的框架在这里就不做详细介绍了。如果对报表展示比较感兴趣的可以看看货拉拉云台系统架构设计 - 掘金[2]

建设成果

整个系统的配置项比较多,我们挑其中比较重要的功能做个介绍

媒体配置化

功能介绍

通过配置的形式新增媒体,研发或业务同事拿到媒体接入API文档后,在系统中配置媒体的上报地址、上报参数、上报认证、上报成功code等关键信息。

解决痛点

  • • 缩短新媒体接入的开发周期,接入新媒体由原来的编码式改为了简单配置做到少开发或不开发即可实现接入。

    • 配置的方式实现媒体API文档升级需求,不再依赖发版。

    图片


埋点命中规则配置化

功能介绍

通过配置规则引擎,在千万埋点数据中获取指定的埋点数据

解决痛点

  • • 解决端上埋点迭代时平台埋点判定条件不能及时更新

  • • 解决每次新增转化目标时需要发版,业务方可以根据自己需求获取已上报的指定埋点

  • • 解决埋点调整时需要研发发版,业务方可以通过图形化界面或者drools规则进行调整

    图片

归因计划配置化

功能介绍

是我们归因平台中最重要的部分,业务方通过该配置确定推广ref与媒体的对应关系、确定确定埋点与转化目标的对应关系,确定投放的账号与转化目标的对应关系,以及转化目标的依赖。其中推广ref是指我们在媒体投放广告时加的自定义标识,用于标识某条具体的广告计划。

解决痛点

  • • 业务方可实时调整埋点与转化目标的关系,进而影响媒体的模型优化

  • • 业务方可以对转化目标配置转化目标依赖,比如下单依赖注册,注册依赖激活。

  • • 业务方可以配置限流比例、投放账号等

图片

后续演进

数据分析

  • • 在平台评估各个媒体的广告投放的效果

  • • 投放带来的人群是否符合预期

  • • 分析指定转换目标的数据,划分成相应人群

反作弊

  • • 打通埋点系统,用户可以通过可视化界面配置已有的埋点

  • • 强化服务上报能力

    1. 1. 知乎:全部为失败的,因为返回的是一张图片无法解析

    2. 2. 请求体可以配置数组类型数据

数据监控

  • • 建立数据全链路追踪能力,实现数据生命链路可视化

  • • 监听数据状态,提供异常阈值告警

接入Market API

  • • 打通主流媒体,在平台实现广告创建、投放,物料、账户管理等

  • • 广告投入支持ABTest,科学投放

总结

广告归因是一个数据量大、链路长、分支多、潮汐现象明显的的项目,既要对业务高度抽象概括又要对细节了如指掌。本文提出了一种广告归因的技术设计方案,包括数据收集、技术架构设计、数据处理方案、数据可视化方案、技术实现步骤、技术难点和解决方案等内容。通过本文的介绍,相信读者对广告归因技术有了更深入的了解,也对如何实现广告归因有了更具体的思路和方向。

引用链接

[1] 广告归因模型解析: https://www.yuque.com/cindyrenshengnan/fa769c/cbp6dlppfbgwe2bk
[2] 货拉拉云台系统架构设计 - 掘金: https://juejin.cn/post/7211008551761543229


作者简介:雷星星,来自货拉拉/技术中心/大数据技术与产品部,专注打代码

继续滑动看下一个
货拉拉技术
向上滑动看下一个