1. 从一份空白的项目正文说起:用户画像到底要解决什么
拿到“淘宝用户行为分析——用户画像”这个题目的时候,项目正文是空的,关键词和摘要描述也没给。这种情况其实在真实工作里特别常见——业务方甩过来一句话,剩下的全靠你自己去拆。我做过好几个电商域的数据分析项目,每次遇到这种“只有一个大标题”的需求,第一反应不是急着写代码,而是先把这句话翻译成可执行的问题清单。
用户画像这四个字听起来很虚,但它本质上就是一件事:把平台上成千上万个行为碎片,压缩成若干个能被人脑理解、能被业务直接使用的标签集合。淘宝这种体量的平台,用户行为数据大致分几类——浏览、点击、收藏、加购、下单、支付、评价、退款,再加上时间戳和商品类目。这些数据单独看没什么意义,但把它们按用户维度聚合起来,就能回答一些很实际的问题:这个人是价格敏感型还是品质导向型?他习惯深夜下单还是午休时逛?他是在囤货还是在比价?
我见过不少同行做用户画像,上来就堆机器学习模型,聚类、降维、神经网络一套组合拳,最后产出几十个簇,业务方看一眼就扔了。问题出在哪?出在他们把用户画像当成了一个“技术展示项目”,而不是一个“业务沟通工具”。用户画像的最终消费者是运营、产品、市场这些人,他们需要的是“这个用户群体叫什么名字、有什么特征、我该对他们做什么”,而不是“第17号簇的轮廓系数是0.63”。
所以这个项目的核心目标应该定成:基于淘宝用户行为数据,构建一套可解释、可落地、能直接指导运营动作的用户标签体系。具体来说,要完成三件事——第一,把原始行为日志清洗成用户级别的宽表;第二,从多个维度提取有业务含义的特征;第三,用合适的分析方法把用户分成若干可命名的群体,并给出每个群体的运营建议。
适合谁来参考这篇内容?如果你是有一定SQL和Python基础的数据分析初学者,想找一个完整的项目练手,这篇会给你一条从数据到结论的完整链路。如果你是已经做过一些报表但没系统做过画像的从业者,这里面关于特征设计和业务解读的部分应该对你有用。如果你是完全零基础的小白,也不用慌,我会把每个步骤的“为什么”讲清楚,你跟着逻辑走就能理解。
提示:用户画像项目最容易翻车的地方不是技术,而是“标签和业务脱节”。后面我会专门用一节讲怎么避免这个问题。
2. 数据准备:淘宝行为日志长什么样,怎么把它变成能用的宽表
2.1 原始数据的典型结构和字段含义
淘宝用户行为数据在公开数据集和实际业务库里的字段设计略有差异,但核心结构是相通的。一个典型的用户行为日志表通常包含以下字段:
| 字段名 | 含义 | 示例值 | 备注 |
|---|---|---|---|
| user_id | 用户唯一标识 | 1002345 | 脱敏后的整数或哈希串 |
| item_id | 商品唯一标识 | 5678901 | 对应商品维度表 |
| category_id | 商品类目ID | 3021 | 层级类目,可映射到一级类目 |
| behavior_type | 行为类型 | pv / cart / fav / buy | 浏览/加购/收藏/购买 |
| timestamp | 行为发生时间 | 1620000000 | Unix时间戳或标准时间格式 |
这份数据的时间跨度通常是一个月到三个月不等。我拿到的模拟数据集大概有1000万行左右,用户量在10万级别,商品量在100万级别。这个体量用单机Pandas处理会有点吃力,但用SQL做聚合或者用Spark做分布式处理都很轻松。如果你只是练手,可以先用采样后的数据跑通流程,再考虑全量。
这里有个细节值得注意:behavior_type的取值在不同数据集里可能不一样。有的数据集把“加入购物车”和“收藏”合并成一个“cart”类,有的则分开。你在动手之前一定要先确认字段的实际取值分布,用一句简单的SQL就能查:
SELECT behavior_type, COUNT(*) AS cnt FROM user_behavior GROUP BY behavior_type ORDER BY cnt DESC;这个查询结果会告诉你两件事:一是数据里到底有哪几种行为,二是每种行为的量级差异。通常pv的量级是cart和fav的几十倍,buy又是cart的几分之一。这个漏斗比例本身就是重要的业务信息,后面做转化分析时会反复用到。
2.2 数据清洗中最容易踩的三个坑
第一个坑是时间戳的时区问题。很多公开数据集的时间戳是UTC时间,而淘宝的业务分析需要按北京时间来划分“早中晚”和“工作日/周末”。如果你直接拿UTC时间做小时分布,会发现凌晨3点的活跃度异常高——那其实是北京时间上午11点。处理方式很简单,在SQL里加一个时区偏移,或者在Python里用pd.to_datetime转换后统一加8小时。
第二个坑是重复行为的判定。同一个用户在同一秒内对同一个商品连续点了5次,这5次都算pv吗?从数据记录的角度算,但从用户意图的角度,这很可能是一次误触或页面卡顿导致的重复上报。我的做法是:对于pv类行为,按“用户-商品-分钟”粒度去重,只保留该分钟内是否有浏览记录;对于buy类行为,则不去重,因为每一笔支付都是真实的交易事件。
第三个坑是异常用户的干扰。有些用户是爬虫或测试账号,行为模式极其规律——比如每隔整点就浏览一次,或者一天内浏览了10万个不同商品。这些账号如果不剔除,会严重扭曲后续的聚类结果。识别方法可以用简单的统计规则:单日pv超过5000次的用户、浏览商品数超过2000个的用户、所有行为都集中在整点时刻的用户,先标记出来人工抽查,确认是异常后剔除。
2.3 构建用户级宽表的完整SQL思路
清洗完原始日志后,下一步是把它聚合成“一行一个用户”的宽表。这个宽表是后续所有分析的基础。我通常会用CTE(公共表表达式)分步构建,这样逻辑清晰,也方便调试。
WITH user_base AS ( SELECT user_id, COUNT(*) AS total_actions, COUNT(DISTINCT item_id) AS distinct_items, COUNT(DISTINCT category_id) AS distinct_categories, SUM(CASE WHEN behavior_type = 'pv' THEN 1 ELSE 0 END) AS pv_cnt, SUM(CASE WHEN behavior_type = 'cart' THEN 1 ELSE 0 END) AS cart_cnt, SUM(CASE WHEN behavior_type = 'fav' THEN 1 ELSE 0 END) AS fav_cnt, SUM(CASE WHEN behavior_type = 'buy' THEN 1 ELSE 0 END) AS buy_cnt, MIN(timestamp) AS first_action_time, MAX(timestamp) AS last_action_time FROM user_behavior_clean GROUP BY user_id ), user_time AS ( SELECT user_id, COUNT(DISTINCT DATE(timestamp)) AS active_days, COUNT(DISTINCT HOUR(timestamp)) AS active_hours, SUM(CASE WHEN HOUR(timestamp) BETWEEN 0 AND 6 THEN 1 ELSE 0 END) AS night_actions, SUM(CASE WHEN DAYOFWEEK(timestamp) IN (1, 7) THEN 1 ELSE 0 END) AS weekend_actions FROM user_behavior_clean GROUP BY user_id ) SELECT b.*, t.active_days, t.active_hours, t.night_actions, t.weekend_actions, b.buy_cnt * 1.0 / NULLIF(b.pv_cnt, 0) AS buy_rate, b.cart_cnt * 1.0 / NULLIF(b.pv_cnt, 0) AS cart_rate, b.fav_cnt * 1.0 / NULLIF(b.pv_cnt, 0) AS fav_rate FROM user_base b JOIN user_time t ON b.user_id = t.user_id;这段SQL跑完,你就得到了一张用户宽表,每一行代表一个用户,列包含了行为总量、行为结构、活跃时间特征和转化率指标。这张表大概有10万行左右,用Pandas读进来做后续分析毫无压力。
注意:
NULLIF这个函数很关键,防止分母为0导致报错。很多新手写转化率时忘了处理除零,结果整个查询挂掉。
3. 特征工程:从行为宽表里提炼出有业务含义的标签
3.1 为什么不能直接把原始统计量丢进聚类
很多人做用户画像时,直接把pv_cnt、cart_cnt、buy_cnt这些原始计数丢进KMeans,跑完发现分出来的群体完全没法解释——因为原始计数受用户活跃度影响太大,一个重度用户的所有计数都高,一个轻度用户的所有计数都低,聚类结果实际上只是把用户按“活跃程度”分了个层,而不是按“行为偏好”分。
正确的做法是把绝对量转化为相对量或比率。比如:
- 用
buy_rate = buy_cnt / pv_cnt代替buy_cnt,衡量的是“看了多少就买了”的效率,而不是“买了多少”的总量。 - 用
cart_rate = cart_cnt / pv_cnt衡量加购倾向。 - 用
night_ratio = night_actions / total_actions衡量夜间活跃偏好。 - 用
weekend_ratio = weekend_actions / total_actions衡量周末活跃偏好。 - 用
category_diversity = distinct_categories / distinct_items衡量跨类目探索程度。
这些比率型特征有一个共同特点:它们的取值范围相对稳定,不受用户活跃度绝对量的影响,能真正反映行为模式差异。
3.2 我常用的六维特征体系
经过多个项目的迭代,我总结出一套六维特征体系,每个维度下选2-3个核心指标,总共12-15个特征。这套体系的好处是每个维度都有明确的业务解释,聚类结果出来后很容易命名。
维度一:活跃度。用active_days(活跃天数)和total_actions(总行为数)来衡量。这两个指标反映用户是“偶尔来逛逛”还是“每天都泡在上面”。
维度二:转化效率。用buy_rate(浏览-购买转化率)和cart_rate(加购率)来衡量。高转化率用户是平台的优质客户,低转化率用户可能需要运营干预。
维度三:价格敏感度。这个维度在原始数据里没有直接字段,但可以通过间接方式构造。一个常用的代理指标是cart_abandon_rate——加购了但最终没买的比例。加购后放弃的比例越高,说明用户对价格越敏感,可能在等降价或比价。
维度四:时间偏好。用night_ratio和weekend_ratio来衡量。夜间活跃用户和周末活跃用户的运营触达时机完全不同。
维度五:类目广度。用distinct_categories和category_diversity来衡量。类目广度高的用户是“逛型用户”,喜欢探索;类目广度低的是“目标型用户”,来了就买特定品类。
维度六:互动深度。用fav_rate(收藏率)和cart_rate来衡量。收藏和加购是购买的前置行为,互动深度高的用户购买意愿更强。
把这六个维度的特征拼成一张矩阵,每一行是一个用户,每一列是一个特征。在丢进聚类之前,记得做标准化——因为不同特征的量纲差异很大,比如active_days的范围是1到30,而buy_rate的范围是0到1,不标准化的话,active_days会主导距离计算。
from sklearn.preprocessing import StandardScaler features = ['active_days', 'total_actions', 'buy_rate', 'cart_rate', 'cart_abandon_rate', 'night_ratio', 'weekend_ratio', 'distinct_categories', 'category_diversity', 'fav_rate'] scaler = StandardScaler() X_scaled = scaler.fit_transform(df[features])3.3 特征筛选:哪些特征该留,哪些该扔
不是特征越多越好。我试过把20多个特征全丢进去,结果聚类结果反而变差了——因为特征之间高度相关,相当于给某些维度加了权重,扭曲了距离计算。
筛选特征的方法我常用两种。第一种是相关性分析,计算特征两两之间的Pearson相关系数,如果两个特征相关系数超过0.85,就只保留业务解释性更强的那一个。比如total_actions和active_days的相关系数通常很高,但active_days的业务含义更清晰(“这个月来了几天”比“点了多少次”更好理解),所以保留active_days。
第二种是基于聚类效果的迭代筛选。先用全部特征跑一次KMeans,看轮廓系数和簇的大小分布;然后逐个剔除特征,观察轮廓系数是否提升。如果剔除某个特征后轮廓系数明显上升,说明这个特征在引入噪声,应该去掉。
实操心得:特征筛选不要一次做完就定死。我通常会在聚类完成后,回头检查每个簇在哪些特征上差异最大,如果发现某个簇的区分主要靠一个业务上不太重要的特征,那就考虑把这个特征去掉重新跑。
4. 聚类分析:把十万用户分成几群才合理
4.1 K值选择:肘部法则和轮廓系数哪个更靠谱
KMeans最核心的参数就是K值——到底分几群。常用的方法有两种:肘部法则(Elbow Method)和轮廓系数(Silhouette Score)。
肘部法则的做法是:让K从2跑到10,每次计算簇内平方和(inertia),然后画一条曲线,找曲线拐点。拐点之后的K值,inertia下降变缓,说明增加簇数量带来的收益递减。这个方法直观,但拐点有时候不明显,特别是数据分布比较连续的时候。
轮廓系数的做法是:对每个K值计算所有样本的轮廓系数均值,取值在-1到1之间,越接近1说明簇内越紧凑、簇间越分离。这个方法比肘部法则更量化,但计算量大,10万用户跑10个K值大概需要几分钟。
我的经验是两个方法结合看。先用肘部法则缩小范围,比如发现拐点在4到6之间,然后在这几个K值上算轮廓系数,选轮廓系数最高的那个。但最终决定K值的不是数学指标,而是业务可解释性。如果K=5的轮廓系数比K=4高一点点,但K=5分出来的第五个簇只有几百人且特征不明显,那我还是选K=4。
from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score inertias = [] silhouettes = [] K_range = range(2, 11) for k in K_range: kmeans = KMeans(n_clusters=k, random_state=42, n_init=10) labels = kmeans.fit_predict(X_scaled) inertias.append(kmeans.inertia_) silhouettes.append(silhouette_score(X_scaled, labels, sample_size=10000)) # 打印结果,人工判断 for k, inertia, sil in zip(K_range, inertias, silhouettes): print(f"K={k}, Inertia={inertia:.0f}, Silhouette={sil:.3f}")4.2 聚类结果的业务命名:怎么把数字簇变成“人话”
聚类跑完之后,你得到的是0、1、2、3这样的簇编号,业务方看不懂。你需要把每个簇的特征均值拉出来,和整体均值对比,找出这个簇最突出的特征,然后给它起一个业务名字。
举个例子,假设K=4,四个簇的特征对比如下:
| 特征 | 簇0 | 簇1 | 簇2 | 簇3 | 整体均值 |
|---|---|---|---|---|---|
| active_days | 18 | 3 | 12 | 8 | 10 |
| buy_rate | 0.02 | 0.01 | 0.15 | 0.05 | 0.06 |
| cart_abandon_rate | 0.3 | 0.5 | 0.2 | 0.7 | 0.4 |
| night_ratio | 0.1 | 0.2 | 0.05 | 0.6 | 0.2 |
| category_diversity | 0.8 | 0.3 | 0.4 | 0.5 | 0.5 |
看完这张表,命名就出来了:
- 簇0:活跃天数高、类目多样性高、转化率低——这是“闲逛型用户”,天天来逛但不太买。
- 簇1:活跃天数低、各项指标都低——这是“沉睡型用户”,偶尔来一次,需要唤醒。
- 簇2:转化率高、加购放弃率低——这是“目标明确型用户”,来了就买,是平台的核心价值客户。
- 簇3:夜间活跃度高、加购放弃率高——这是“夜间比价型用户”,晚上逛得多但犹豫,对价格敏感。
这种命名方式的好处是,运营一看就知道该对每个群体做什么:闲逛型用户推内容种草,沉睡型用户发召回券,目标明确型用户做交叉销售,夜间比价型用户推限时折扣。
4.3 聚类之外:RFM模型作为交叉验证
KMeans是无监督方法,分出来的簇没有绝对的对错。为了验证聚类结果的合理性,我通常会同时跑一个RFM模型作为交叉验证。
RFM是三个维度的缩写:Recency(最近一次购买距今多久)、Frequency(购买频率)、Monetary(购买金额)。淘宝行为数据里如果没有金额字段,可以用购买次数代替Monetary,变成RF模型。
把RFM的八种用户类型(重要价值客户、重要保持客户、重要发展客户、重要挽留客户、一般价值客户、一般保持客户、一般发展客户、一般挽留客户)和KMeans的簇做交叉表,如果两个方法分出来的群体有较高的重叠度,说明聚类结果比较稳健;如果差异很大,就需要回头检查特征设计或K值选择。
# 假设已经算好了RFM得分和KMeans标签 cross_tab = pd.crosstab(df['kmeans_label'], df['rfm_segment']) print(cross_tab)这个交叉表还能帮你发现一些隐藏群体。比如某个KMeans簇里大部分是“重要挽留客户”,说明这个群体虽然购买力强但最近不活跃,需要重点召回。
5. 画像落地:标签体系怎么建、怎么用、怎么迭代
5.1 从聚类结果到用户标签表的工程化转换
聚类分析是在Python里跑的,但业务方用的时候不可能每次都跑一遍脚本。你需要把结果固化成一张用户标签表,存在数据库里,每天或每周更新。
这张标签表的结构大概是这样的:
| user_id | segment_name | segment_id | r_score | f_score | last_active_date | update_time |
|---|---|---|---|---|---|---|
| 1002345 | 闲逛型用户 | 0 | 3 | 2 | 2024-06-01 | 2024-06-02 |
| 1002346 | 目标明确型 | 2 | 5 | 4 | 2024-06-01 | 2024-06-02 |
更新逻辑是:每天增量计算新用户的行为特征,用训练好的Scaler和KMeans模型预测簇标签,然后写入标签表。注意Scaler和KMeans模型要持久化保存,不能每次重新训练,否则簇编号会变,历史标签就对不上了。
import joblib # 保存模型 joblib.dump(scaler, 'scaler.pkl') joblib.dump(kmeans, 'kmeans.pkl') # 加载模型做增量预测 scaler = joblib.load('scaler.pkl') kmeans = joblib.load('kmeans.pkl') new_labels = kmeans.predict(scaler.transform(new_user_features))5.2 运营侧怎么用这些标签:三个真实场景
场景一:沉睡用户召回。运营筛选出“沉睡型用户”标签,且last_active_date在30天前的用户,推送一张满减券。这里的关键是券的门槛要低——沉睡用户本来就不活跃,门槛太高反而不会用。我见过一个案例,给沉睡用户发“满200减20”的券,核销率不到1%;改成“满50减10”后,核销率涨到8%。
场景二:闲逛用户种草。闲逛型用户的特点是浏览多、购买少。运营可以给他们推送“猜你喜欢”的个性化推荐,或者发一些内容型种草素材,而不是直接发券。因为闲逛型用户对价格不一定敏感,他们缺的是购买理由。
场景三:夜间比价用户限时刺激。夜间比价型用户加购放弃率高,说明他们在犹豫。运营可以在晚上8点到10点这个时间段,给他们推送“限时2小时折扣”的通知,制造紧迫感。实测下来,限时折扣对这类用户的转化提升比普通优惠券高出一截。
5.3 画像迭代:多久更新一次,怎么评估效果
用户画像不是一次性的项目,而是一个持续迭代的系统。我的建议是聚类模型每季度重新训练一次,标签表每天更新。为什么是季度?因为用户的消费习惯和平台的活动节奏都有季节性,用夏季数据训练的模型直接套到冬季,效果会打折扣。
评估画像效果不能只看技术指标,要看业务指标。具体来说,跟踪三个数:
- 标签覆盖率:有多少用户被打上了标签。如果覆盖率低于80%,说明特征计算或模型预测环节有问题。
- 标签稳定性:同一个用户连续两周的标签是否一致。如果标签频繁跳变,说明特征波动太大或模型不稳定。
- 运营响应率:针对某个标签群体做的运营活动,转化率是否高于大盘。如果某个标签群体的运营响应率持续低于大盘,说明这个标签没有区分度,需要考虑重新设计特征或调整K值。
实操心得:我习惯在标签表里加一个
confidence字段,记录这个用户属于当前簇的置信度(可以用到簇中心的距离的倒数来近似)。运营做高成本活动时,只选confidence高的用户,避免浪费预算在边界用户上。
6. 踩过的坑和几条实在建议
做用户画像项目这些年,有几个坑我反复踩过,也看别人踩过,这里挑三个最有代表性的说一下。
第一个坑:把相关性当因果性。聚类结果显示“夜间活跃用户加购放弃率高”,这不代表“因为夜间活跃所以放弃率高”。真实原因可能是夜间用户有更多时间比价,或者夜间推送的促销信息更多导致用户犹豫。解读画像结果时,一定要区分“这个群体有什么特征”和“为什么有这个特征”,前者是数据事实,后者需要业务假设和AB测试验证。
第二个坑:忽略数据的时间窗口。用一个月的数据做画像,和用三个月的数据做画像,结果可能完全不同。一个月的数据可能刚好覆盖了大促,用户行为被促销扭曲;三个月的数据更能反映常态。我的做法是至少用两个月的数据,并且把大促期间的数据单独标记,分析时可以选择性排除。
第三个坑:标签体系过度设计。一开始就想建几百个标签,结果每个标签的覆盖率都很低,运营根本用不起来。正确的做法是先从5到8个核心标签做起,跑通“标签生成-运营使用-效果回收”的闭环,再逐步扩展。标签的价值不在于多,而在于每个标签都能对应一个明确的运营动作。
最后分享一个我常用的检查清单,每次画像项目交付前过一遍:
- 特征是否做了标准化?没做的话聚类结果不可信。
- 簇的大小是否均衡?如果某个簇只有几十人,考虑合并或剔除。
- 每个簇是否有业务名字?没有名字的簇等于没分。
- 标签表是否有更新机制?静态标签表一周后就失效了。
- 是否和RFM做了交叉验证?单一方法的结论不够稳。
- 运营侧是否知道怎么用?交付时附一份“标签使用指南”,比给一堆图表有用得多。
这套流程我在多个电商数据分析项目里跑过,从数据清洗到标签落地大概需要两周左右,其中特征工程和业务解读占了大头,真正跑模型的时间反而很少。如果你也在做类似的项目,建议把更多精力花在“理解业务”和“设计特征”上,而不是调参上。模型选KMeans还是DBSCAN,对最终业务价值的影响,远不如特征选得对不对。