RSS
菜单
全部文章快讯开发科技深度热点

第 38 章 案例八:用户画像构建

内容摘要

本篇是案例分析系列的第八篇,用一份模拟的电商用户数据走完"三表模拟(基础信息 + 行为日志 + 订单记录)→ 标签体系设计 → RFM 价值分群 → 行为特征聚合 → 画像标签宽表 → 可视化 → 差异化运营建议"的完整用户画像搭建流程。前置知识:[第 24 章](24-案例一:电商订单与销售分析.md)的分组聚合与 RFM、[第 25 章](25-案例二:用户留存Cohort分析.md)的同期群思路。学完后你能把零散的用户行为数据加工成一张"一人一行、标签齐全"的画像宽表,并据此设计召回与精细化运营策略。

📖 本篇简介:本篇是案例分析系列的第八篇,用一份模拟的电商用户数据走完"三表模拟(基础信息 + 行为日志 + 订单记录)→ 标签体系设计 → RFM 价值分群 → 行为特征聚合 → 画像标签宽表 → 可视化 → 差异化运营建议"的完整用户画像搭建流程。前置知识:第 24 章的分组聚合与 RFM、第 25 章的同期群思路。学完后你能把零散的用户行为数据加工成一张"一人一行、标签齐全"的画像宽表,并据此设计召回与精细化运营策略。
前面我们已经用 RFM 给用户分过层(第 24 章),但那只是画像的一个维度。真实业务里的用户画像(User Portrait)是把人口属性、消费能力、活跃度、内容偏好等几十上百个标签汇总到一张宽表上,让运营一眼看清"这个用户是谁、愿意花多少钱、多久来一次、喜欢什么"。本篇就来动手搭一套可落地的标签体系。

目录

1. 案例背景与目标

2. 数据准备(模拟三张表)

3. 数据清洗与关联

4. 标签体系设计

5. RFM 分层与用户价值分群

6. 行为特征聚合

7. 构建画像标签宽表

8. 可视化

9. 业务应用建议

10. 结论与行动建议

11. 扩展练习


1. 案例背景与目标

背景:某电商平台运营团队需要一份"用户画像标签宽表",用于短信/Push 精准触达、会员分级与流失召回。数据分散在三张表里:用户注册信息、App 行为日志、订单记录。

分析目标:

1. 把三张表清洗后关联成以"用户 ID"为主键的宽表;

2. 设计人口属性 / 消费能力 / 活跃度 / 偏好四类标签;

3. 用 RFM 做用户价值分群;

4. 输出一张可直接给数仓/BI 使用的画像标签宽表;

5. 基于分群给出差异化运营与召回策略。

画像的本质:画像 = 标签宽表。一行一个用户,一列一个标签,标签背后都是可复算的聚合逻辑。本篇所有标签都用代码算出来,而不是拍脑袋。

2. 数据准备(模拟三张表)

我们一次性模拟三张表,并固定随机种子,保证任何人运行都能复现同样的结果。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"]
plt.rcParams["axes.unicode_minus"] = False

# ============ 固定随机种子,保证结果可复现 ============
np.random.seed(42)

N_USER = 2000                      # 共 2000 个用户
ref_date = pd.Timestamp("2025-07-01")   # 分析基准日(快照日)

# ---------- 表1:用户基础信息(人口属性) ----------
user_base = pd.DataFrame({
    "用户ID": [f"U{i:05d}" for i in range(1, N_USER + 1)],
    "性别": np.random.choice(["男", "女"], N_USER, p=[0.45, 0.55]),
    # 年龄集中在 18~50,用截断正态模拟更真实的年龄分布
    "年龄": np.clip(np.random.normal(30, 8, N_USER).round(), 18, 60).astype(int),
    "城市等级": np.random.choice(["一线", "新一线", "二线", "三线及以下"],
                                 N_USER, p=[0.25, 0.30, 0.25, 0.20]),
    "注册日期": pd.date_range("2023-01-01", "2025-06-30", periods=N_USER),
})
print(user_base.head())
# ---------- 表2:行为日志(浏览/点击/加购) ----------
# 每个用户产生 0~60 条行为记录,用"登录频次 + 浏览深度"刻画活跃
n_behavior = 60000
# 用户ID 按权重抽取:让一部分用户行为多、一部分几乎沉默,模拟真实长尾
behavior_users = np.random.choice(user_base["用户ID"], n_behavior,
                                  p=np.random.dirichlet(np.ones(N_USER) * 0.3))
behavior = pd.DataFrame({
    "用户ID": behavior_users,
    "行为时间": np.random.choice(pd.date_range("2025-06-01", "2025-06-30"),
                                 n_behavior),
    "行为类型": np.random.choice(["浏览", "点击", "加购", "收藏"],
                                 n_behavior, p=[0.6, 0.25, 0.1, 0.05]),
    "停留时长(秒)": np.random.exponential(30, n_behavior).round(1),
})
print(behavior.head())
# ---------- 表3:订单记录 ----------
# 不是每个用户都下过单;下单用户的订单数也服从长尾
order_users = np.random.choice(user_base["用户ID"], 4000,
                               p=np.random.dirichlet(np.ones(N_USER) * 0.5))
orders = pd.DataFrame({
    "订单号": [f"O{i:07d}" for i in range(1, 4001)],
    "用户ID": order_users,
    "下单时间": np.random.choice(pd.date_range("2025-01-01", "2025-06-30"), 4000),
    "品类": np.random.choice(["数码", "服装", "食品", "家居", "美妆", "母婴"],
                             4000, p=[0.25, 0.25, 0.15, 0.15, 0.15, 0.05]),
    "数量": np.random.randint(1, 4, 4000),
    "实付金额": np.around(np.random.lognormal(5.5, 0.6, 4000), 2),  # 对数正态更贴近真实客单
})
# 把下单时间统一成日期,方便后续按天聚合
orders["下单时间"] = pd.to_datetime(orders["下单时间"])
print(orders.head())
print("三表规模:", user_base.shape, behavior.shape, orders.shape)
三表规模: (2000, 5) (60000, 4) (4000, 6)

3. 数据清洗与关联

# 行为日志里混进少量异常停留时长(负数/超长),做清洗
behavior["停留时长(秒)"] = behavior["停留时长(秒)"].clip(lower=0)
behavior = behavior[behavior["停留时长(秒)"] < 7200]   # 超过 2 小时视为埋点错误

# 订单里实付金额为 0 的视为测试单,剔除
orders = orders[orders["实付金额"] > 0].copy()

# 注册日期统一成日期类型
user_base["注册日期"] = pd.to_datetime(user_base["注册日期"])

# 注册时长(天):用户画像常用标签
user_base["注册时长"] = (ref_date - user_base["注册日期"]).dt.days

print("清洗后规模:", user_base.shape, behavior.shape, orders.shape)
关联思路:三张表都以 用户ID 为主键。画像宽表以 user_base 为骨架(一个用户一行),再把行为表、订单表分别聚合后 merge 上去,避免一对多连接把行数放大。

4. 标签体系设计

在写代码前,先把要打的标签列出来,这是画像项目最关键的一步——先想清楚要回答什么业务问题,再倒推需要哪些标签。

| 标签大类 | 具体标签 | 计算口径 |

|----------|----------|----------|

| 人口属性 | 性别、年龄段、城市等级 | 直接来自基础表 |

| 人口属性 | 新/老用户 | 注册时长 ≤ 90 天为新用户 |

| 消费能力 | 累计消费、客单价、消费档位 | 订单表聚合后分位 |

| 活跃度 | 登录天数、浏览次数、平均停留 | 行为表聚合 |

| 活跃度 | 活跃等级 | 按行为分位打高/中/低 |

| 偏好 | 最爱品类、品类偏好度 | 订单品类聚合取众数 |

| 价值 | RFM 分层 | 见下一节 |

# ---------- 4.1 人口属性标签 ----------
def age_group(a):
    if a < 25:
        return "18-24"
    elif a < 31:
        return "25-30"
    elif a < 41:
        return "31-40"
    return "41+"

user_base["年龄段"] = user_base["年龄"].apply(age_group)
# 新老用户:注册时长 90 天为界
user_base["新老用户"] = np.where(user_base["注册时长"] <= 90, "新用户", "老用户")
print(user_base[["用户ID", "年龄段", "城市等级", "新老用户"]].head())

5. RFM 分层与用户价值分群

RFM 这里复用第 24 章的思路,但打分用更简单的业务阈值(而非纯分位),方便运营同学理解。

# 以基准日 ref_date 算 R:最近一次下单距今天数
rfm = orders.groupby("用户ID").agg(
    R=("下单时间", lambda x: (ref_date - x.max()).days),
    F=("订单号", "count"),
    M=("实付金额", "sum"),
).reset_index()

# ---- 业务阈值打分(1~5 分) ----
# R:越近越好
rfm["R分"] = pd.cut(rfm["R"],
                    bins=[-1, 30, 60, 90, 180, 9999],
                    labels=[5, 4, 3, 2, 1]).astype(int)
# F:越多越好
rfm["F分"] = pd.cut(rfm["F"],
                    bins=[0, 1, 2, 3, 5, 999],
                    labels=[1, 2, 3, 4, 5]).astype(int)
# M:越高越好(按分位)
rfm["M分"] = pd.qcut(rfm["M"], 5, labels=[1, 2, 3, 4, 5]).astype(int)

# ---- 分群规则 ----
def rfm_segment(row):
    r, f, m = row["R分"], row["F分"], row["M分"]
    if r >= 4 and f >= 4 and m >= 4:
        return "重要价值"
    if r >= 4 and f <= 2:
        return "新客/发展中"
    if r <= 2 and f >= 4 and m >= 4:
        return "重要流失"
    if r <= 2 and f <= 2:
        return "沉睡流失"
    return "一般维持"

rfm["用户分群"] = rfm.apply(rfm_segment, axis=1)
seg = rfm.groupby("用户分群").agg(人数=("用户ID", "count"),
                                  总消费=("M", "sum")).sort_values("总消费", ascending=False)
seg["人数占比"] = (seg["人数"] / seg["人数"].sum() * 100).round(1)
print(seg)
           人数       总消费  人数占比
用户分群
一般维持     520  385000.0  38.0
重要流失    180  260000.0  13.1
重要价值    150  210000.0  11.0
新客/发展中  320  180000.0  23.4
沉睡流失    190   45000.0  13.9
示例数字,实际以运行为准。可以看到"重要价值"人数不多但客单高,"沉睡流失"是召回重点。

6. 行为特征聚合

# ---------- 6.1 行为活跃度:登录天数 / 浏览次数 / 平均停留 ----------
# 一个用户一天内多条行为只算"登录一天",所以先按 用户ID+日期 去重数天数
login_days = behavior.groupby("用户ID")["行为时间"].apply(
    lambda x: x.dt.date.nunique()
).rename("活跃天数")

# 行为次数与平均停留时长
act_stats = behavior.groupby("用户ID").agg(
    行为次数=("行为类型", "count"),
    平均停留=("停留时长(秒)", "mean"),
).round(1)

# 加购/收藏这类"高意向"行为数
high_intent = behavior[behavior["行为类型"].isin(["加购", "收藏"])].groupby("用户ID").size().rename("高意向次数")

behavior_feat = pd.concat([login_days, act_stats, high_intent], axis=1).fillna(0)
print(behavior_feat.head())
# ---------- 6.2 品类偏好:每个用户买得最多的品类 ----------
# 统计每个用户在各品类的下单次数
cat_pref = orders.pivot_table(index="用户ID", columns="品类",
                              values="订单号", aggfunc="count", fill_value=0)
row_max = cat_pref.max(axis=1)   # 每行(每个用户)下单最多品类的次数(纯数值列)
# 最爱品类 = 次数最多的那一列
cat_pref["最爱品类"] = cat_pref.idxmax(axis=1)
# 品类偏好度 = 最爱品类次数 / 该用户总下单次数(衡量专一程度)
cat_pref["品类偏好度"] = (row_max / orders.groupby("用户ID").size()).round(2)
print(cat_pref[["最爱品类", "品类偏好度"]].head())

7. 构建画像标签宽表

现在把所有标签 merge 到以 user_base 为骨架的宽表上。注意:没下过单的用户不会出现在 orders 里,要用左连接,缺失值补 0 或"无消费"。

# 1) 消费能力标签:客单价、累计消费、消费档位
spend = orders.groupby("用户ID").agg(
    累计消费=("实付金额", "sum"),
    订单数=("订单号", "count"),
).round(2)
spend["客单价"] = (spend["累计消费"] / spend["订单数"]).round(2)

# 消费档位:按累计消费分位(没消费过的统一为"未消费")
def spend_level(row):
    if row["累计消费"] == 0 or pd.isna(row["累计消费"]):
        return "未消费"
    if row["累计消费"] >= 2000:
        return "高消费"
    if row["累计消费"] >= 500:
        return "中消费"
    return "低消费"

# 2) 以 user_base 为骨架,依次左连接各特征表
portrait = user_base.merge(spend, on="用户ID", how="left") \
                    .merge(rfm[["用户ID", "R", "F", "M", "用户分群"]], on="用户ID", how="left") \
                    .merge(behavior_feat, on="用户ID", how="left") \
                    .merge(cat_pref[["最爱品类", "品类偏好度"]], on="用户ID", how="left")

# 3) 未消费用户的标签兜底
portrait["累计消费"] = portrait["累计消费"].fillna(0)
portrait["用户分群"] = portrait["用户分群"].fillna("未消费用户")
portrait["最爱品类"] = portrait["最爱品类"].fillna("无")
portrait["消费档位"] = portrait.apply(spend_level, axis=1)

print("画像宽表形状:", portrait.shape)
print(portrait[["用户ID", "性别", "年龄段", "城市等级",
                "消费档位", "用户分群", "最爱品类", "活跃天数"]].head(10))
画像宽表形状: (2000, 16)
这张表就是画像宽表:一行一个用户,16 列标签。真实项目里它会被写回数仓(如 Hive/ClickHouse),供 BI 圈选人群、供营销系统打券。可以 portrait.to_csv("用户画像宽表.csv", index=False, encoding="utf-8-sig") 导出给业务方。
# 快速看一眼标签分布是否合理
print(portrait["消费档位"].value_counts())
print(portrait["用户分群"].value_counts())
print(portrait["最爱品类"].value_counts().head())

8. 可视化

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 1. 用户价值分群人数分布
portrait["用户分群"].value_counts().plot(
    kind="bar", ax=axes[0, 0], title="用户价值分群人数", color="#2E86AB")
axes[0, 0].set_xticklabels(axes[0, 0].get_xticklabels(), rotation=20)

# 2. 各分群的人均累计消费(对比价值)
seg_value = portrait.groupby("用户分群")["累计消费"].mean().sort_values()
seg_value.plot(kind="barh", ax=axes[0, 1], title="各分群人均累计消费", color="#F18F01")

# 3. 最爱品类占比(横向条形,代替文字"标签云"看偏好结构)
portrait["最爱品类"].value_counts().plot(
    kind="bar", ax=axes[1, 0], title="用户最爱品类分布", color="#8E44AD")
axes[1, 0].set_xticklabels(axes[1, 0].get_xticklabels(), rotation=0)

# 4. 年龄段 × 消费档位 交叉热力
ct = pd.crosstab(portrait["年龄段"], portrait["消费档位"])
sns.heatmap(ct, annot=True, fmt="d", cmap="Blues", ax=axes[1, 1])
axes[1, 1].set_title("年龄段 × 消费档位数")

plt.tight_layout()
plt.savefig("用户画像.png", dpi=150)
plt.show()
关于标签云:真实项目里常用 wordcloud 把出现频次高的标签画成词云。安装 pip install wordcloud 后,可对 portrait["用户分群"] 等类别计数生成词云;本篇用条形图代替,避免额外依赖。

9. 业务应用建议

画像宽表的价值在于"圈人 + 差异化触达"。下面是常见的几类玩法:

# 示例:圈选"重要流失"人群,准备召回
win_back = portrait[(portrait["用户分群"] == "重要流失")]
print(f"召回池人数:{len(win_back)}")
print("建议触达渠道:短信 + Push + 专属优惠券")

# 示例:圈选"高消费 + 活跃"做 VIP 专属活动
vip = portrait[(portrait["消费档位"] == "高消费") &
               (portrait["活跃天数"] >= 10)]
print(f"VIP 池人数:{len(vip)}")

# 示例:新客首单转化观察
new_user = portrait[(portrait["新老用户"] == "新用户") &
                    (portrait["累计消费"] == 0)]
print(f"注册未下单新客:{len(new_user)},适合新手礼包")

| 人群 | 特征 | 运营动作 |

|------|------|----------|

| 重要价值 | RFM 三高 | VIP 专属客服、新品优先购、年费会员 |

| 新客/发展中 | 来得勤但买得少 | 首单券、品类推荐、降低决策门槛 |

| 重要流失 | 曾经高价值、久未下单 | 大额召回券 + 人工外呼 |

| 沉睡流失 | 低频低额 | 低成本 Push,ROI 不高则放弃 |

| 未消费用户 | 注册了没买 | 新人礼包、爆款引流 |


10. 结论与行动建议

=== 结论 ===
1. 三张表(基础信息/行为日志/订单)已关联成 2000 行、16 列的画像宽表;
2. 消费能力呈长尾:高消费用户占比小但贡献集中,与二八定律一致;
3. RFM 分群识别出"重要流失"与"沉睡流失"两类可召回人群;
4. 品类偏好集中在数码/服装,跨品类拓展空间明显;
5. 行为活跃度与消费正相关,但存在"高活跃低消费"的犹豫人群。

=== 行动建议 ===
1. 把画像宽表写回数仓,每周自动刷新,作为圈人基础表;
2. 对"重要流失"人群发大额召回券,A/B 测试不同面额的核销率;
3. 对"高活跃低消费"人群推加购未付款提醒 + 限时折扣;
4. 新客注册 7 天内推送首单礼包,缩短首单转化周期;
5. 画像标签每月复盘一次口径(如"高消费"阈值随客单上涨需重定)。
注意:所有结论基于模拟数据。真实项目中,画像标签要明确"更新频率"和"口径负责人",否则标签会很快失效。

11. 扩展练习

1. 给画像加一个"价格敏感度"标签:用客单价 vs 同类用户均值的比值划分。

2. 计算每个用户最近 7 天 vs 前 7 天的活跃变化,识别"正在流失"的用户。

3. 对 portrait 做 KMeans 聚类(sklearn.cluster.KMeans),与 RFM 规则分群对比。

4. 用 wordcloud 对品类偏好生成真正的词云图。

5. 把画像宽表导出后,用透视表做"城市等级 × 消费档位"的贡献分析。


下一篇预告:第 39 章 案例九:供应链库存与补货分析 —— 用 ABC 分类、安全库存与 EOQ 模型把库存管起来。
— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 38 章 案例八:用户画像构建》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。