ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

客户画像引擎实战:特征工程、标签体系与流失预测建模全解析

客户画像引擎实战:特征工程、标签体系与流失预测建模全解析

在接手这个AI客户管理系统项目之前,我们采购过几套现成的标签工具,最后都因为"标签算得出来但业务不认账"而搁浅。运营同事提了一个非常具体的问题:每个月都有客户悄悄流失,销售却只能在对方续费前一周才察觉异常。这个问题听起来像是跟进策略导致的,但往深了挖,真正的问题是系统压根没把"谁快要流失"这件事提前算出来。客户画像引擎就是干这个的:把散落的日志、订单、客服记录加工成特征,再基于特征生成标签,最终落到流失预测模型里输出风险名单。

这篇文章直接讲我落地这套画像引擎时踩过的坑和最终沉淀下来的方案,覆盖架构设计、特征工程的细节、标签体系的存储方案、流失预测样本与阈值处理的完整思路。适合正在做或准备做客户画像、用户增长、会员运营系统的工程师和产品同学,不管你是从零开始还是已经有一套半成品,这中间关于标签口径、时间穿越、样本定义的部分都会很有参考价值。

1. 画像引擎的整体设计与架构取舍

在讲具体代码和算法之前,得先把画像引擎在整个系统中的位置说清楚。它不是一个独立的服务,而是嵌在AI客户管理系统里的一层"加工车间",上游接数据,下游接业务动作。

1.1 引擎在系统中的边界与职责划分

我们最终把画像引擎划成四层:数据接入层、特征计算层、标签存储层、服务应用层。数据接入层负责把CRM里的客户基础信息、订单系统的交易记录、埋点系统的行为日志、客服工单系统的交互记录统一汇总到数仓的ODS层;特征计算层是核心,既做离线批量计算,也做实时流式计算,产出原始特征表;标签存储层把特征加工成面向业务的标签,存到宽表和在线存储里;服务应用层对外提供画像查询API、人群圈选API和预测结果订阅接口。

这里最关键的取舍是"特征"和"标签"必须分开建表。很多项目图省事,特征和标签混在一张宽表里,后遗症非常大:特征更新频率是小时级的,标签有些是月级静态的,有些是实时跳变的,混在一起会互相拖累。更现实的坑是,不同业务方对标签业务口径有常见调整,但特征是底层的客观计算逻辑,不该跟着业务口径变。分开之后,改标签不用重算特征,这是架构上的第一道护城河。

1.2 为什么不用图数据库,而是宽表加标签表

立项时团队内部争论过一波,有人提议用图数据库把客户、行为、偏好建成实体关系图,理论上能表达复杂关系。实话讲,图库在探索式分析里确实好用,比如看一个客户的影响链路。但作为画像引擎的基础存储,图库的性能在千万级节点下做批量圈选是非常痛苦的,而运营每天都要做"近30天有购买且最近7天未访问的高价值客户"这类即时圈选,宽表加标签表配合列存引擎是在性能、成本和易理解性之间最平衡的方案。幸好当时没被"技术先进"的执念带跑,否则后续很多工程问题都会在这上面爆发。

1.3 标签生命周期的管理视角

画像标签不是算一次就完事了,它是有生命周期的。我们在系统里把标签划分成四个状态:待发布、已发布、已下线、已归档。每次标签逻辑变更,要灰度对比新旧逻辑覆盖的客户数分布,差异超过5%就要回滚排查。很多人忽略这个环节,会觉得"标签不就改个SQL"。实际上一线运营对标签有依赖,一旦标签口径悄悄变了,他们手上的名单和业绩报表对不上,信任感瞬间崩塌。所以标签版本管理是画像引擎里最容易被低估、但最影响口碑的模块。

2. 特征工程:决定画像质量的核心命脉

特征工程这部分是整篇文章字数最多的环节,因为画像引擎的上限是由特征决定的,模型只是把特征里面蕴含的信号榨出来。

2.1 RFM不是简单算三个字段

传统RFM模型把客户按最近消费时间(Recency)、消费频率(Frequency)、消费金额(Monetary)各打一个分值再做组合分群。这个思路有普适性,但如果照着教科书算,落地一定会踩坑。比如"最近一次消费距今天数"这个字段,很多代码直接取订单表max(order_time),然后跟当前日期做差。听起来没问题,但对一个上月刚续费、本月又有新建单的客户来说,max(order_time)只能代表最近一次行为,代表不了消费习惯的变化趋势。我们在实际生产里做了改造,不再只算单一数值,而是拆成多个视角:最近一次成交距今的天数、过去7天/30天/90天的成交频次和金额、成交间隔的中位数和标准差、以及金额的移动平均趋势。

# 生产环境中的RFM计算示例(部分字段) import pandas as pd import numpy as np def build_rfm_features(order_df, ref_date): # order_df: 订单表, 包含 customer_id, order_time, amount # ref_date: 计算截止日期, 防止数据穿越 odf = order_df[order_df['order_time'] <= ref_date].copy() odf['order_time'] = pd.to_datetime(odf['order_time']) odf['days_since'] = (ref_date - odf['order_time']).dt.days # 分组计算基础指标 rfm = odf.groupby('customer_id').agg( last_order_days=('days_since', 'min'), order_cnt_90d=('order_id', lambda x: (odf.loc[x.index, 'days_since'] <= 90).sum()), order_amount_90d=('amount', lambda x: odf.loc[x.index, 'days_since'][odf.loc[x.index, 'days_since'] <= 90].sum()), order_interval_mean=('days_since', lambda x: np.diff(np.sort(x))[:].mean() if len(x) > 1 else np.nan), ).reset_index() # 最近7天是否有购买,作为独立强特征 rfm['has_order_7d'] = (rfm['last_order_days'] <= 7).astype(int) return rfm

这段代码里有几个细节值得反复琢磨。一是所有聚合计算都必须在同一个ref_date下面做,否则不同批次的数据算出来的特征天然有偏差;二是90天窗口用的是序号从订单集合里过滤,而不是单独再写一堆布尔掩码,因为groupby里的lambda拿的是切片的index,要对原表做关联才能取到对应的行;三是成交间隔的中位数比平均值更能反映客户真实购买节奏,因为平均值容易被几次大促订单拉偏。

2.2 行为特征的正确打开方式:把动作拆细

订单是"结果行为",但流失预测最怕的就是只看结果不看过程。一个客户今天没下单,他昨天还活跃在App里浏览商品详情,明天可能就会回来成交;而一个客户的访问都是在站外广告点击跳转,说明他的忠诚度还没建立。我们在特征设计里把行为拆成了四个维度:访问深度(页面浏览数、停留时长、人均浏览商品数)、互动行为(加购、收藏、分享、领取优惠券)、渠道行为(每个渠道近30天访问次数、最近访问来源)、客服交互(最近一次工单时间、工单类型、处理时长)。

这中间最容易被忽略的是"客服交互类"特征。我们后来通过特征重要性排序发现,近30天内提交过投诉工单的客户,流失概率是普通客户的4倍以上。这个信号在订单数据里完全看不出来,但在客服工单表里非常显著。如果你的系统里也有售后记录或投诉数据,务必把它纳入特征体系,光靠交易行为做流失预测,等于在一个蒙眼状态下去猜客户意图。

2.3 时序特征的滑窗与衰减

行为是随时间变化的,客户的态度会衰减,也会因为某个触发事件突然升温。如果特征只算"近30天总访问次数",会把30天前的老黄历和昨夜的活跃混为一谈。我们的做法是对核心行为指标引入时间衰减加权。常用的衰减函数是指数衰减:最近一天权重为1,三天前的权重约等于0.5,两周前的权重就压到0.1以下了。这个思想跟电商平台做商品热度排名时的时间衰减一脉相承。

def time_decay_feature(event_df, half_life_days=7): # 对事件序列按时间衰减累加,半衰期7天 now = event_df['event_time'].max() delta_days = (now - event_df['event_time']).dt.total_seconds() / 86400 weight = 0.5 ** (delta_days / half_life_days) return (event_df['event_value'] * weight).sum()

滑窗加衰减的组合,在实验里比单纯滑窗AUC高出3到5个点。这里面还有一层业务意义:一个客户如果短期内高频操作但没有任何交易转化,说明他可能在犹豫或者比价,流失风险反而在上升。为了捕捉这类模式,我建议把行为特征进一步拆成"访问频率"和"交易转化率"两个独立特征,不要合成一个动作指标,让模型自己学习两者之间的交互。

2.4 特征处理:什么时候该归一化,什么时候不该

很多初学的同学拿到特征上来就StandardScaler一把梭。实际在树模型里,归一化不会带来收益,反而会在调参时引入困扰。我们的特征处理经验是两套方案并行:进入树模型(XGB、LightGBM)的特征只做缺失值填充和异常值截断,不做归一化;进入逻辑回归或深度学习模型的数值特征,才统一做z-score标准化,同时把偏态强的特征做log1p变换。这类细节写的时候很短,但在生产环境里如果踩错,会影响模型收敛效率和解释性。

特征选择方面,我们以LightGBM的特征重要性为基准,配合相关性矩阵做初筛。相关性大于0.8的两个特征只保留业务含义更清晰的那个。比如"消费总额"和"平均客单价"高度相关,但客单价在不同渠道差异很大,我们最终保留总额,而把客单价做成"相对品类均值"的比值,这样反而保留了更多信息。

3. 标签体系与画像存储的工程实现

特征工程做完麻袋里全是原料,标签体系是分拣打包的过程。这个环节决定了业务方看到的到底是什么。

3.1 四级标签体系怎么设计

我们把标签分成四级。原始标签是从客户信息表直接同步来的,比如行业、地域、客户来源,这些字段基本不变,属于静态标签。事实标签是基于历史数据统计出来的客观事实,比如近30天订单数、最近登录时间、累计消费金额,这些数据动态更新但逻辑固定。规则标签是业务规则和特征组合打分得到的判断类标签,像"高价值客户""沉睡客户",规则可配置,需要业务确认。预测标签则完全来自算法模型,比如"未来30天流失概率""未来3个月购买A品类概率",这类标签只能由模型产线生成,逻辑对业务是一个相对黑盒。

规则标签的设计里有个易踩的坑:同一个标签可能有多个规则版本,比如"高价值客户"在一部定义是"累计消费>1万",在二部定义是"近一年消费>8000且复购>3次"。我们在标签元数据表里给每个标签加了口径来源和生效时间,同时支持多版本灰度。业务侧看到的标签名字一样,但背后版本不同,逐渐收敛统一。

3.2 画像存储选型:宽表、列存、缓存的三角关系

画像数据的存储选型没法一张表打天下。离线计算完的特征和标签,主副本落在ClickHouse里,字段几百个,用列存引擎做任意维度的组合圈选都非常快。线上实时查询走Redis缓存,把高频访问的标签组合和单客户画像快照放进去,单客户画像读取能在10毫秒内返回。全文检索场景交给Elasticsearch,客户名、手机号、标签名模糊搜索由ES承担。

存储引擎适用场景实时性备注
ClickHouse离线特征表、宽表、人群圈选分钟级列式存储,适合批量聚合
Redis单客户画像快照、高频标签读取毫秒级需控制Key过期策略
Elasticsearch标签检索、名单查询、运营圈人界面秒级适合fuzzy query

ClickHouse在大宽表场景要注意稀疏列的问题。我们最初把300多个特征全塞进一张表,很多行只有几个字段有值,压缩比虽然还行,但写入和查询的CPU浪费不少。后来按业务域拆成客户基础画像、交易特征画像、行为特征画像、预测特征画像四张宽表,通过客户ID关联,效果比一张超级宽表好得多。

3.3 画像服务API设计的几个实战细节

画像引擎对外提供的API要覆盖三个场景:查询单个客户画像的详情页、批量圈选人群包、以及标签变化的实时订阅。单客户画像的API返回一个JSON对象,结构上要区分"基础信息""交易标签""行为标签""预测标签",每类里再给出标签名、标签值、置信度、最近一次计算时间。很多人忽略置信度字段,但对预测标签特别重要,业务侧要根据置信度决定是否进入人工跟进池。

批量圈选API的查询条件要支持标签组合、且支持并集和交集。我们最开始只支持取交集,运营想把"高价值且高流失风险"的人群和"高活跃且最近未购买"的人群合并,开发就得专门写个接口。后来在查询解析层引入了简单的逻辑语法,用AND、OR、NOT组合标签条件,前端的圈人界面灵活性立刻提升了一大截。这里再补一句,批量圈选接口一定要做超时控制和结果上限,如果不做限制,一次圈出全量客户、再导出几十万行的场景很常见,很容易拖垮服务。

4. 流失预测建模:从特征到决策的一公里

画像引擎最终的杀手锏是流失预测模型。这一步做得好,前面的特征工程就真正兑现价值了。

4.1 正负样本定义是模型成败的胜负手

流失预测的样本定义,几乎是整个建模环节里业务差异最大、试错成本最高的部分。首先"流失"本身要定义清楚。我们最后定为:未来30天内没有任何登录行为、也没有任何订单行为的存量付费客户,就算流失。如果客户本来就是免费用户,那判断标准又不一样。样本的定义会直接影响模型学到的模式,如果定义太松,模型会把"低活跃用户"全部误判成流失;定义太紧,模型又来不及在客户真正流失前发出预警。

样本的构造方式是观察窗口加预测窗口:用客户在过去90天的特征作为输入,看他未来30天是否流失作为标签。训练数据集是每个月滚动抽样的,因为同一批客户在不同观察窗口里的特征和标签组合是不一样的,这样既能扩充样本量,又能让模型适应不同时间段的数据分布。

# 流失预测样本构造伪代码 # observe_end: 观察窗口截止日 # pred_end: 预测窗口截止日, 一般 observe_end + 30天 def build_dataset(feature_table, action_log, observe_end): obs_df = feature_table[feature_table['dt'] == observe_end].copy() # 贴上标签: 未来30天是否仍有活跃 future_act = action_log[ (action_log['event_time'] > observe_end) & (action_log['event_time'] <= observe_end + timedelta(days=30)) ].groupby('customer_id')['event_id'].count().reset_index() future_act.columns = ['customer_id', 'future_act_cnt'] obs_df = obs_df.merge(future_act, on='customer_id', how='left') obs_df['label_churn'] = (obs_df['future_act_cnt'].fillna(0) == 0).astype(int) return obs_df

这代码就是整条建模流程的生命线,它有问题模型全部白搭。实操时要特别小心时区问题,所有的事件时间统一用业务当地时区,避免凌晨的边界操作被归到错误的观察窗口里。

4.2 防止标签泄漏:建模工程师最容易翻车的地方

标签泄漏是流失预测里最隐蔽的坑。常见的泄漏有这么几类。第一类是特征里包含了未来信息,比如把"未来30天是否续费"作为特征喂给模型,这在程序上检查很难发现,因为字段名可能写成"last_renewal_flag",看似是历史数据,实际上加工程序里透传了未来。第二类是把归一化或缺失值统计的统计量在全量数据上计算,导致训练集的统计信息里混入了测试集的信息。第三类是最容易翻车的:特征表是把所有历史数据和今天的数据一起算出来的,但从业务时间来看,部分特征实际已经包含预测窗口的痕迹。

我们团队后来立了一条铁律:所有特征表必须在每个计算日期上做严格时间约束,用create_date或业务日期分区,绝对不允许用"当前时间"傻乎乎地更新全表。同时在Schema里要备注每个特征的可计算时点,由数据字典自动校验,凡是带未来依赖的字段在特征注册时直接拒绝。

4.3 模型选型与调参的实战心得

流失预测这个场景里,我们对比了逻辑回归、XGBoost和LightGBM三套方案。逻辑回归解释性最强,给业务讲"过去30天登录次数每下降10%,流失概率上升2个百分点"很清楚,但预测效果有限。XGBoost和LightGBM的效果明显更好,最终我们生产环境用的是LightGBM,主要原因是训练速度快、对稀疏特征友好、调参相对简单。

核心参数的经验值如下:learning_rate=0.05,num_leaves=48,feature_fraction=0.6,bagging_fraction=0.8,min_child_samples=50。这些参数不是拍脑袋定的,是在验证集上做了网格搜索加早停确定的。关键是每棵树都很浅,模型不容易过拟合,同时因为流失预测的特征数量在200左右,叶子数太高反而会让模型学到训练集里面的噪声模式。

样本不均衡方面,流失客户比例约在5%-10%之间,直接训练的话模型会把所有客户都判定为不流失,因为这样准确率都很高。我们通过scale_pos_weight参数调节正负样本权重,权重设成负样本数除以正样本数,同时在评估时优先看召回率和PR曲线而不是准确率。运营真正关心的是"流失里有百分之多少被我们提前抓到",这个对应召回率,而名单的质量对应精确率,两者需要平衡。

4.4 阈值不是默认0.5,按效益曲线去卡

模型输出的是连续的分数,从0到1。默认取0.5看"是否流失"是一个惯性思维,实际业务场景里非常不经济。我们把预测分数按分位切成10档,然后统计每一档的客户数、预测流失率、实际流失率、平均客户价值,据此做一张"流失预警效益表"。比如分数最高的10%客户,实际流失率60%,平均年贡献2万,那么运营把话术资源分配在这一档,挽回成功率哪怕只有10%,带来的收益也相当可观;而分数在0.3档的客户,实际流失率只有12%,投入人力去跟进就很不划算。

分数档位客户占比实际流失率建议运营动作
0.9-1.02%68%客户成功经理一对一干预
0.8-0.93%45%定向优惠券 + 电话回访
0.7-0.85%28%推送关怀内容
0.5-0.710%15%自动化邮件触达
<0.580%5%不做主动干预

按这张表来决定阈值,比盲目卡0.5要科学得多。说到底,流失预测的价值不是把分数算准,而是让有限的运营预算在干预动作上创造最高回报。

5. 工程化落地的坑与排查实录

这块内容每一条都是从生产事故里总结出来的,没有一条是文档里会写的。

5.1 标签漂移和数据口径不一致

最典型的事故是"活跃"的定义。运营说活跃是指有登录,产品说活跃是指有业务操作,技术那边埋点里定义又有自己的逻辑,最后统计出来的活跃率和流失率相差将近一倍。标签口径不一致,等于给预测模型喂了错数据还浑然不知。我们后面做了统一指标字典,每个标签必须绑定指标ID、口径描述、负责部门和生效日期,任何改动要走到变更流程。上线数据质量监控,每天对关键标签的分布做环比对比,波动超过阈值自动报警,同时把报警推送到相关负责人的群里。

5.2 冷启动客户怎么打标签

新注册的客户没什么历史行为,RFM算不出来,时序特征全是缺失值,模型预测流失概率就变成瞎猜。我们的做法是为冷启动客户单独设计一套规则标签,依靠注册来源、行业类型、首次访问落地页等元信息做基础分群,等客户积累了7天行为数据后再切到正式模型。这套逻辑在代码上注意要用"数据覆盖天数"作为一个软开关,而不是简单判断"是否新客",因为有些客户注册半年了还是只来了一次,行为稀疏程度和新客差不多,对他们也用元信息补特征更合理。

5.3 运营不信任预测结果,怎么办

模型建完,运营同事第一个反应往往是"你凭什么说这个客户会流失,他明明刚签了合同"。这是画像引擎落地面临的最大阻力,比任何技术问题都难。我们的解决办法是给预测结果配"可解释的Top因子"。模型每给出一个流失概率,同时输出排名前3的特征因子,比如"近14天登录次数从上月均值下降80%""最近一次客服工单为投诉类型""近30天访问频次低于同行业均值"。这样销售跟进时心里有数,也愿意把模型名单作为辅助而不是抗拒它。

后续又做了灰度验证:从高流失名单里随机抽了一半客户做干预实验,另一组照常。一个季度后,干预组的流失率比对照组低了差不多8个百分点。有了这个数字,运营从"用不用"变成"怎么用得更好",系统才算真正被接受。

5.4 模型监控与定期重训节奏

流失客户的行为模式会随着产品和市场变化而变化,模型上线一个月后效果可能已经开始衰减。我们在生产环境里监控两个核心指标:PSI(群体稳定性指数)和每日平均AUC。PSI超过0.1需要排查特征分布变化原因,超过0.2就要考虑模型下线重训。重训节奏定为每月一次,不是越频繁越好,因为样本窗口至少要覆盖一个完整的业务周期。遇到大促、版本改版、行业政策变动等特殊情况,就要启动临时重训流程,把新数据快速吸收进模型里。

6. 写在最后的实践经验

这套画像引擎从第一版粗糙的特征表到现在,前后迭代了大半年。我最大的体会是画像引擎的项目里,技术只是三成,剩余七成都是业务理解和数据治理的问题。特征工程看似是写SQL和Python,实际要花大量时间去理解客户是怎么用产品的、订单是怎么流转的、客服是怎么跟客户交互的。很多开发同学觉得浪费时间,但恰恰是这些理解让特征有灵魂,让标签有解释力,让预测结果有说服力。

如果只留一句话给准备做类似系统的朋友:不要一上来就堆模型,先花两周把标签口径和特征时点理清楚,后面能省下一个月的返工时间。项目走到后面,反而不是模型不够好,而是很多问题都出在数据没对齐、标签没版本化、特征有穿越这类看起来很基础的地方。把这些基础打牢,所谓AI客户管理系统才真正跑得起来。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进