简介:面向Python数据挖掘与机器学习初学者、电商运营分析人员及需要完成课程设计的学生,这套资料包以电子商务网站用户行为分析和服务推荐为实战场景,覆盖数据读取、清洗、可视化、特征工程与推荐建模的完整流程,能够帮助读者快速掌握个性化推荐系统的构建方法。压缩包共包含5个文件:2个Jupyter Notebook代码文件承载核心分析与建模过程,1个Excel数据表提供用户行为样本数据,1个SQL脚本用于数据管理、1个文本说明辅助项目阅读,整体大小仅79KB,轻量便捷。目前已有417人学习下载。项目从探索性数据分析入手,使用Pandas完成缺失值处理、异常检测与数据聚合,结合Matplotlib和Seaborn绘制用户活跃度及购买行为分布,随后逐步实现协同过滤、矩阵分解等经典推荐算法,并介绍准确率、召回率等模型评估指标。代码与数据集配套可直接运行,读者可对照实验感受全流程,也可修改特征或替换模型,迁移应用到其他推荐业务中。
1. 电商用户行为分析:从日志到购买概率的完整落地链路
一个电商业务上经常被问的问题是“这个用户逛了半小时,为什么没下单”。标题里的“电子商务网站用户行为分析及服务推荐”并不是某个独立算法,而是一整条链路:把每天几千万行的行为日志清洗成可分析的宽表,把“会不会买”压成一个特征问题,再把模型分数和物品协同过滤一起输出成推荐列表。我在接过这类项目时最大的感触是,真正卡住进度的从来不是模型,而是数据清洗、会话切分、标签定义和负采样这一连串前置决定。这篇文章面对的读者是写过 pandas、跑过 sklearn,但没把完整串联过的分析师、校招学生和想转数据挖掘的后端开发,下面从拿到 csv 之后的第一步开始讲。
2. 电商行为数据预处理:日志清洗、会话切分与行为漏斗统计
数据文件一拿到手,先别急着建模。行为数据里最常见的坑是字段含义模糊、时间戳单位不一致、行为枚举五花八门。这些不处理干净,后面所有聚合并起来的特征都是脏的。这一章先做四件事:统一列名和枚举、解析时间戳、切分会话、统计漏斗。
2.1 行为日志的字段结构与类型映射
网上流传的电商行为数据集大多来自公开比赛或教学项目,字段结构高度相似,通常是五到六列:用户标识、商品标识、类目标识、行为类型、时间戳,有的带价格字段。拿到原始数据先打印表头和 dtypes,把列名统一成下面这张表的样子,后面所有代码都基于这个命名。
| 字段 | 类型 | 业务含义 | 处理要点 |
|---|---|---|---|
| user_id | string | 用户标识 | 区分 cookie 和 user 两种粒度,统一为 user |
| goods_id | string | 商品标识 | 商品侧特征的聚合键 |
| cate_id | string | 商品所属类目 | 用于用户类目偏好特征 |
| action | string | 点击/加购/下单 | 必须归一化为同一枚举 |
| act_ts | bigint | 行为发生的 Unix 时间戳 | 先转成 datetime,再切日、周、会话 |
| price | double | 商品价格 | 可能为空,为空时后续金额特征用行为次数代替 |
提示:不同来源的数据列名可能完全不同,第一步先用 rename 把列名对齐,不要让同一份数据里出现两种叫法。
2.2 时间戳解析、枚举归一化与去重
import pandas as pd import numpy as np df = pd.read_csv('user_behavior.csv', names=['user_id', 'goods_id', 'cate_id', 'action', 'act_ts'], header=None) # 行为枚举归一化:view/click 都归为 pv,add_cart 归为 cart,order/buy 归为 buy action_map = {'view': 'pv', 'click': 'pv', 'add_cart': 'cart', 'cart': 'cart', 'order': 'buy', 'buy': 'buy'} df['action'] = df['action'].str.strip().str.lower().map(action_map).fillna('pv') # Unix时间戳转 datetime,便于按日聚合 df['act_dt'] = pd.to_datetime(df['act_ts'], unit='s') df = df.sort_values(['user_id', 'act_ts']).reset_index(drop=True) # 同一用户同一商品同一动作在同一秒内重复,通常是埋点重复上报 df = df.drop_duplicates(['user_id', 'goods_id', 'action', 'act_ts'])逻辑说明:枚举归一化是行为特征正确性的基础,真实日志里 view 和 click 经常混用,统一成 pv 才能保证漏斗统计口径一致。sort_values 按用户和时间排序,后面切会话时依赖相邻行的时间差。drop_duplicates 去掉的是重复埋点,真实的埋点日志里这种重复占比可能很高,不洗掉会导致行为次数虚高。
参数说明:unit='s' 表示时间戳单位是秒,部分数据集给的是毫秒,要在 to_datetime 里改成 unit='ms',否则时间会整体偏移到 1970 年附近,切窗口时特征全部失真。
2.3 会话切分:30 分钟无操作视为一次新会话
商品推荐里很多共现信号依赖于“同一次会话内被一起查看或购买”,没有 session_id 的日志需要自己切。常见做法是设定一个时间阈值,用户两次相邻行为超过阈值就认为是新会话。
grp = df.groupby('user_id')['act_ts'] # 与上一次行为的时间差,单位是秒 df['ts_gap'] = grp.diff() # 超过1800秒(30分钟)视为一次新会话起点;每个用户的第一条记录也是新会话 df['is_new_session'] = (df['ts_gap'] > 1800) | (df['ts_gap'].isna()) df['session_rank'] = df.groupby('user_id')['is_new_session'].cumsum() df['session_id'] = df['user_id'].astype(str) + '_' + df['session_rank'].astype(str)逻辑说明:diff()算的是用户相邻行为的时间间隔,第一行的结果天然是 NaN,用isna()让它成为新会话起点。随后按用户累计,每跨过一个 30 分钟间隔 session_rank 就加一,把 session_id 拼成用户_序号,后面做物品共现时直接用 userId 和 session_rank 就可以保证不串用户。
参数说明:1800 秒不是固定标准。常见的会话超时在 20 到 30 分钟,如果你的数据分析显示用户回访间隔普遍较短,可以缩到 900 秒;阈值越小,会话数量越多、每个会话内的行为越短,直接影响后面 ItemCF 的共现质量。
2.4 行为漏斗与用户基础统计
action_cnt = df.groupby('action').size() print(action_cnt) pv = action_cnt.get('pv', 0) cart = action_cnt.get('cart', 0) buy = action_cnt.get('buy', 0) print(f"点击->加购: {cart / max(pv, 1):.4f}") print(f"加购->下单: {buy / max(cart, 1):.4f}")逻辑说明:行为漏斗不只是汇报用的指标。如果加购到下单的转化率远高于点击到加购,说明用户的主要决策发生在加购之后,特征里应该强化加购相关的时间窗口变量,而不是一味放大点击类特征。get('pv', 0)的写法是为了避免某些小数据集里没有某种行为时直接 KeyError。
参数说明:这段统计同时帮你判断行为稀疏程度,如果加购行为总数只有几百条,后面建模时就要考虑把“加购”和“下单”合并成“强意图行为”,否则正样本太少,模型很难学出区分度。
3. 用户特征工程与标签体系:RFM 变形、时间窗口和类目偏好
数据挖掘项目里,模型准确率从 0.6 提到 0.7,大部分靠的是特征而不是换算法。行为数据的特点就是单条日志不值钱,聚合后的行为密度才值钱。所以建模之前先把用户侧、商品侧、用户-商品交叉侧三类特征建好,后面无论换什么分类器都只是改一行代码的事。
3.1 特征工程优先于模型选择
行为数据维度不高,特征稀疏且量级在百万级时,特征工程带来的收益通常比调整模型结构更大。我在做这个项目时一定是先建一套包含行为频次、行为类型占比、活跃度、购买力分层的用户宽表,再跑一个简单模型看特征重要性。如果某个模型跑出来的 AUC 很低,先不要怀疑算法,回头检查特征里是不是缺了“最近加购但未购买”这类强意图变量。
3.2 用户侧 RFM 变形:没有金额就用行为次数
RFM 本来是零售领域的经典框架,M 指消费金额。但这套电商行为数据集里很多没有可靠的 price 字段,所以常见的做法是让 M 退化为购买次数、F 退化为行为总次数、R 用距离最后一次行为的自然日数。这样既保留了 RFM 的“最近活跃度 + 行为频率 + 价值度”三层含义,又不受字段缺失影响。
| 特征名 | 计算方式 | 适用场景 |
|---|---|---|
| r_days | 数据最大日期 - 最后一次行为日期 | 判断流失风险 |
| f_cnt | 行为总次数 | 识别整体活跃度 |
| f_buy | 购买次数 | 代替消费金额作为价值度 |
| f_cart | 加购次数 | 比点击更接近购买意图 |
| behavior_entropy | 各行为类型的香农熵 | 区分“随便逛”和“认真选” |
today = df['act_dt'].max().normalize() user_agg = df.groupby('user_id').agg( r_days=('act_dt', lambda s: (today - s.max().normalize()).days), f_cnt=('act_ts', 'count'), f_buy=('action', lambda s: (s == 'buy').sum()), f_cart=('action', lambda s: (s == 'cart').sum()), f_pv=('action', lambda s: (s == 'pv').sum()), ).reset_index() # 行为熵:行为分布越均匀,说明用户越接近真实购买决策 def behavior_entropy(s): vc = s.value_counts(normalize=True) return -(vc * np.log2(vc)).sum() entropy = df.groupby('user_id')['action'].apply(behavior_entropy).rename('behavior_entropy') user_agg = user_agg.merge(entropy, on='user_id')逻辑说明:r_days用“数据里最大日期”而不是系统当前日期,是为了避免离线训练时拿未来的时间点去计算特征,这一点在时间序列类项目里是硬约束。behavior_entropy 算的是用户内部的行为分布熵:行为集中在“点击”一种时熵低,说明用户在大量浏览但没有明确目标;点击、加购、购买都有分布时熵高,说明已经进入了比较和筛选阶段。
参数说明:熵的计算里用了normalize=True,得到的是每个行为类型在该用户内的占比,避免活跃用户天然拥有更高的熵,保证不同量级用户之间可比。
3.3 时间窗口特征:行为意图会衰减
全量行为特征会把一个月前的加购和今天的加购当成同权重处理,这在电商场景里是错的。用户对商品兴趣衰减很快,常见做法是切 3 天、7 天两个窗口,把窗口内的行为单独聚合。窗口列比全量列对模型更重要,它捕捉的是“最近这段时间用户处于什么状态”。
import datetime def window_agg(days): cutoff = df['act_dt'].max() - datetime.timedelta(days=days) win = df[df['act_dt'] >= cutoff] return win.groupby('user_id').agg( **{f'pv_{days}d': ('action', lambda s: (s == 'pv').sum()), f'cart_{days}d': ('action', lambda s: (s == 'cart').sum()), f'buy_{days}d': ('action', lambda s: (s == 'buy').sum())} ).reset_index() for d in (3, 7): user_agg = user_agg.merge(window_agg(d), on='user_id')逻辑说明:窗口特征能回答两类业务问题:3 天窗口捕捉的是近期的即时意图,适合判断“用户是不是马上要买”;7 天窗口捕捉的是短中期偏好,适合做推荐召回时的粗筛。用agg(**{...})的写法是 pandas 的命名聚合,输出列名直接带窗口后缀,不用再手动重命名。
参数说明:窗口数量不是越多越好,3、7、30 天取两个已足够。窗口太密会产生高度共线的特征,把模型训练速度拖慢,但对 AUC 没有任何实质增益。
3.4 商品侧热度与用户-商品交叉特征
用户侧特征之后补商品侧和交叉侧。商品热度是典型的长尾分布,直接用计数容易被爆款商品带偏,常见的处理是转成百分位排名。用户-类目交叉特征则用来刻画“这个用户对哪个类目最敏感”。
goods_hot = df.groupby('goods_id')['action'].size().rank(pct=True).rename('hot_rank') cate_cnt = df.groupby(['user_id', 'cate_id'])['action'].size().reset_index() # 用户行为最多的类目作为主类目 user_main_cate = cate_cnt.loc[cate_cnt.groupby('user_id')['action'].idxmax()] user_main_cate = user_main_cate[['user_id', 'cate_id']].rename(columns={'cate_id': 'main_cate'}) user_agg = user_agg.merge(goods_hot, left_on='goods_id', right_index=True, how='left') user_agg = user_agg.merge(user_main_cate, on='user_id', how='left')逻辑说明:hot_rank 的数值落在 0 到 1 之间,表达的是商品在一段时间内的相对热度,不受数量级影响。主类目特征相当于把“用户长期偏好在哪个类目”压缩成一个离散变量,后面构建推荐候选集时,这个列可以直接用来限定召回范围。注意合并时用 how='left',避免用户没有对应商品热度时整行被丢弃。
4. 机器学习实战:用 LightGBM 分类器预测下单概率
特征表建好后,进入二分类建模。这个项目的任务定义是:给一个用户,预测他接下来一段时间发生购买行为的概率。分类器我一般直接用 LightGBM,它在百万级行为数据、特征稀疏且含有大量离散类目时,训练速度和 AUC 表现都明显优于逻辑回归和随机森林。
4.1 标签定义与训练集构建
样本单位是 user_id,标签是“该用户是否有过下单行为”。有下单行为的用户作为正样本,没有的作为负样本。负样本不能全部拿进来,否则训练集会极度不平衡,常见做法是按正样本数量的 3 倍采样。
from sklearn.model_selection import train_test_split label = df.groupby('user_id')['action'].apply(lambda s: int((s == 'buy').any())) label = label.rename('label') train_df = user_agg.merge(label, on='user_id') pos = train_df[train_df['label'] == 1] neg = train_df[train_df['label'] == 0].sample(n=len(pos) * 3, random_state=42) train_df = pd.concat([pos, neg]).sample(frac=1, random_state=42)逻辑说明:正负样本比例定为 1:3 是推荐场景里比较常用的做法。太低的负采样会让模型把大量用户预测成正样本,推荐列表全是商品刷屏;太高的负采样又会让数据规模翻几倍,但 AUC 提升有限。random_state固定下来是为了让多次实验之间可比。
参数说明:如果数据里购买行为本身极稀疏,可以先看 label 的分布。正样本占比低于 1% 时,优先考虑把“加购”纳入正样本定义,而不是继续提高负采样倍数。
4.2 LightGBM 训练与关键参数
import lightgbm as lgb features = ['r_days', 'f_cnt', 'f_buy', 'f_cart', 'f_pv', 'pv_3d', 'cart_7d', 'buy_7d', 'hot_rank', 'behavior_entropy'] X = train_df[features] y = train_df['label'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42) model = lgb.LGBMClassifier( n_estimators=300, learning_rate=0.05, num_leaves=31, colsample_bytree=0.8, reg_alpha=0.1, random_state=42, verbosity=-1 ) model.fit(X_train, y_train) print(f"AUC: {model.score(X_test, y_test):.4f}")| 参数 | 默认值 | 建议值 | 作用 |
|---|---|---|---|
| n_estimators | 100 | 300 | 提高迭代上限,配合 learning_rate 一起收敛 |
| learning_rate | 0.1 | 0.05 | 小步长降低过拟合,代价是训练变慢 |
| num_leaves | 31 | 31~63 | 叶子数越大模型复杂度越高,验证集 AUC 下降就回退 |
| colsample_bytree | 1.0 | 0.8 | 每棵树随机取一部分列,增强鲁棒性 |
| reg_alpha | 0.0 | 0.1 | L1 正则,行为数据稀疏时很有用 |
| verbosity | 1 | -1 | 关闭训练过程刷屏输出 |
提示:n_estimators 和 learning_rate 要一起调。先固定 learning_rate=0.05,把 n_estimators 逐步加到验证集 AUC 收敛,再回头微调学习率。调参顺序颠倒会浪费大量时间。
4.3 评估:AUC 之外还要看 TopN 召回率
AUC 衡量的是整体排序能力,但推荐服务真正关心的是 TopN 里能不能命中真实购买用户。一个分类器 AUC 高、TopN 召回低,通常说明正样本分布特殊,例如少数高价值用户贡献了大部分购买,模型排序结果被他们占据。
pred = model.predict_proba(X_test)[:, 1] top100_idx = X_test.index[np.argsort(pred)[::-1][:100]] hit = y_test.loc[top100_idx].sum() recall_top100 = hit / max(y_test.sum(), 1) print(f"Recall@100: {recall_top100:.4f}")逻辑说明:argsort 按预测概率从大到小排序,取前 100 个用户索引,再统计这 100 个里真实下单用户占全部真实下单用户的比例。这个指标直接对应推荐场景的“召回多少”问题,和 AUC 一起看才能判断模型是否真的可用。如果 Recall@100 明显偏低,先回去检查负采样比例,而不是继续调参。
5. 服务推荐落地:协同过滤与模型分混合排序
模型分数预测的是“这个人买不买”,但推荐列表还要回答“推荐给他什么”。这一步直接用下单概率排序会翻车:用户间分数差异极小,TopN 会被活跃用户的高频行为商品霸占。常见的落地思路是把模型分当门槛,把物品协同过滤和热度榜当候选集来源。
5.1 为什么最终推荐不直接按模型概率排
概率模型的结果适合做人群分层,不适合直接当商品排序分。把同一用户的候选商品按模型分排序时,商品之间的分数差往往非常小,排序结果容易抖动。常见做法是先用 ItemCF 和热度榜分别生成两个候选池,再用模型分做截断和重排,只保留预测购买概率高于阈值的个性化结果。
5.2 物品协同过滤的最小可用 ItemCF
协同过滤不需要训练模型,核心是利用行为会话内的共现关系。下面代码基于 session_id 构建商品共现矩阵,计算相似度并输出 TopN。
pairs = df[df['action'].isin(['cart', 'buy'])] \ .groupby('session_id')['goods_id'].apply(list) from collections import defaultdict, Counter co_occur = defaultdict(Counter) for items in pairs: for gid in items: co_occur[gid].update(set(items) - {gid}) goods_pop = df.groupby('goods_id')['action'].size() def similar_items(gid, topn=10): sim = { other: cnt / (goods_pop[gid] + goods_pop[other] - cnt) for other, cnt in co_occur[gid].items() } return sorted(sim.items(), key=lambda x: x[1], reverse=True)[:topn]逻辑说明:共现只取 add_cart 和 buy 行为,过滤掉大量无效点击,保证商品相关性来自真实购买意向。相似度公式是 Jaccard 的变体,分子是共同出现次数,分母是两个商品各自的总行为次数减去共现次数,天然惩罚了热门商品被过度关联的问题。set(items) - {gid}去掉同一个会话内的自身项,避免商品和自己算相似。
5.3 混合排序与冷启动处理
推荐列表的生成规则通常这样定:用户最近 7 天有加购行为且模型分高于 0.7,取 ItemCF 的 Top20;低于这个阈值时,直接用用户主类目对应的热度榜商品。两个候选池合并后过滤掉已购商品,再按“个性来源优先、热度来源靠后”的顺序打散展示。
线上评估建议用 AB 实验,一半流量走纯热度榜,一半流量走混合推荐,观察 30 天下单率而不是只看点击率。离线产出按天调度,模型和共现表每天重新计算一次,推荐结果写回 Redis,建议按user_id -> JSON 数组存储 TopN 列表,TTL 设为 24 小时防止线上读取到过期推荐。冷启动用户没有行为序列,直接从热度榜开始,收集两三天的行为数据后再进入 ItemCF。
本文还有配套的精品资源,点击获取