
简介面向Python数据挖掘与机器学习初学者、电商运营分析人员及需要完成课程设计的学生这套资料包以电子商务网站用户行为分析和服务推荐为实战场景覆盖数据读取、清洗、可视化、特征工程与推荐建模的完整流程能够帮助读者快速掌握个性化推荐系统的构建方法。压缩包共包含5个文件2个Jupyter Notebook代码文件承载核心分析与建模过程1个Excel数据表提供用户行为样本数据1个SQL脚本用于数据管理、1个文本说明辅助项目阅读整体大小仅79KB轻量便捷。目前已有417人学习下载。项目从探索性数据分析入手使用Pandas完成缺失值处理、异常检测与数据聚合结合Matplotlib和Seaborn绘制用户活跃度及购买行为分布随后逐步实现协同过滤、矩阵分解等经典推荐算法并介绍准确率、召回率等模型评估指标。代码与数据集配套可直接运行读者可对照实验感受全流程也可修改特征或替换模型迁移应用到其他推荐业务中。1. 电商用户行为分析从日志到购买概率的完整落地链路一个电商业务上经常被问的问题是“这个用户逛了半小时为什么没下单”。标题里的“电子商务网站用户行为分析及服务推荐”并不是某个独立算法而是一整条链路把每天几千万行的行为日志清洗成可分析的宽表把“会不会买”压成一个特征问题再把模型分数和物品协同过滤一起输出成推荐列表。我在接过这类项目时最大的感触是真正卡住进度的从来不是模型而是数据清洗、会话切分、标签定义和负采样这一连串前置决定。这篇文章面对的读者是写过 pandas、跑过 sklearn但没把完整串联过的分析师、校招学生和想转数据挖掘的后端开发下面从拿到 csv 之后的第一步开始讲。2. 电商行为数据预处理日志清洗、会话切分与行为漏斗统计数据文件一拿到手先别急着建模。行为数据里最常见的坑是字段含义模糊、时间戳单位不一致、行为枚举五花八门。这些不处理干净后面所有聚合并起来的特征都是脏的。这一章先做四件事统一列名和枚举、解析时间戳、切分会话、统计漏斗。2.1 行为日志的字段结构与类型映射网上流传的电商行为数据集大多来自公开比赛或教学项目字段结构高度相似通常是五到六列用户标识、商品标识、类目标识、行为类型、时间戳有的带价格字段。拿到原始数据先打印表头和 dtypes把列名统一成下面这张表的样子后面所有代码都基于这个命名。字段类型业务含义处理要点user_idstring用户标识区分 cookie 和 user 两种粒度统一为 usergoods_idstring商品标识商品侧特征的聚合键cate_idstring商品所属类目用于用户类目偏好特征actionstring点击/加购/下单必须归一化为同一枚举act_tsbigint行为发生的 Unix 时间戳先转成 datetime再切日、周、会话pricedouble商品价格可能为空为空时后续金额特征用行为次数代替提示不同来源的数据列名可能完全不同第一步先用 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], headerNone) # 行为枚举归一化view/click 都归为 pvadd_cart 归为 cartorder/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], units) df df.sort_values([user_id, act_ts]).reset_index(dropTrue) # 同一用户同一商品同一动作在同一秒内重复通常是埋点重复上报 df df.drop_duplicates([user_id, goods_id, action, act_ts])逻辑说明枚举归一化是行为特征正确性的基础真实日志里 view 和 click 经常混用统一成 pv 才能保证漏斗统计口径一致。sort_values 按用户和时间排序后面切会话时依赖相邻行的时间差。drop_duplicates 去掉的是重复埋点真实的埋点日志里这种重复占比可能很高不洗掉会导致行为次数虚高。参数说明units 表示时间戳单位是秒部分数据集给的是毫秒要在 to_datetime 里改成 unitms否则时间会整体偏移到 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(normalizeTrue) return -(vc * np.log2(vc)).sum() entropy df.groupby(user_id)[action].apply(behavior_entropy).rename(behavior_entropy) user_agg user_agg.merge(entropy, onuser_id)逻辑说明r_days用“数据里最大日期”而不是系统当前日期是为了避免离线训练时拿未来的时间点去计算特征这一点在时间序列类项目里是硬约束。behavior_entropy 算的是用户内部的行为分布熵行为集中在“点击”一种时熵低说明用户在大量浏览但没有明确目标点击、加购、购买都有分布时熵高说明已经进入了比较和筛选阶段。参数说明熵的计算里用了normalizeTrue得到的是每个行为类型在该用户内的占比避免活跃用户天然拥有更高的熵保证不同量级用户之间可比。3.3 时间窗口特征行为意图会衰减全量行为特征会把一个月前的加购和今天的加购当成同权重处理这在电商场景里是错的。用户对商品兴趣衰减很快常见做法是切 3 天、7 天两个窗口把窗口内的行为单独聚合。窗口列比全量列对模型更重要它捕捉的是“最近这段时间用户处于什么状态”。import datetime def window_agg(days): cutoff df[act_dt].max() - datetime.timedelta(daysdays) win df[df[act_dt] cutoff] return win.groupby(user_id).agg( **{fpv_{days}d: (action, lambda s: (s pv).sum()), fcart_{days}d: (action, lambda s: (s cart).sum()), fbuy_{days}d: (action, lambda s: (s buy).sum())} ).reset_index() for d in (3, 7): user_agg user_agg.merge(window_agg(d), onuser_id)逻辑说明窗口特征能回答两类业务问题3 天窗口捕捉的是近期的即时意图适合判断“用户是不是马上要买”7 天窗口捕捉的是短中期偏好适合做推荐召回时的粗筛。用agg(**{...})的写法是 pandas 的命名聚合输出列名直接带窗口后缀不用再手动重命名。参数说明窗口数量不是越多越好3、7、30 天取两个已足够。窗口太密会产生高度共线的特征把模型训练速度拖慢但对 AUC 没有任何实质增益。3.4 商品侧热度与用户-商品交叉特征用户侧特征之后补商品侧和交叉侧。商品热度是典型的长尾分布直接用计数容易被爆款商品带偏常见的处理是转成百分位排名。用户-类目交叉特征则用来刻画“这个用户对哪个类目最敏感”。goods_hot df.groupby(goods_id)[action].size().rank(pctTrue).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_ongoods_id, right_indexTrue, howleft) user_agg user_agg.merge(user_main_cate, onuser_id, howleft)逻辑说明hot_rank 的数值落在 0 到 1 之间表达的是商品在一段时间内的相对热度不受数量级影响。主类目特征相当于把“用户长期偏好在哪个类目”压缩成一个离散变量后面构建推荐候选集时这个列可以直接用来限定召回范围。注意合并时用 howleft避免用户没有对应商品热度时整行被丢弃。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, onuser_id) pos train_df[train_df[label] 1] neg train_df[train_df[label] 0].sample(nlen(pos) * 3, random_state42) train_df pd.concat([pos, neg]).sample(frac1, random_state42)逻辑说明正负样本比例定为 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_size0.2, random_state42) model lgb.LGBMClassifier( n_estimators300, learning_rate0.05, num_leaves31, colsample_bytree0.8, reg_alpha0.1, random_state42, verbosity-1 ) model.fit(X_train, y_train) print(fAUC: {model.score(X_test, y_test):.4f})参数默认值建议值作用n_estimators100300提高迭代上限配合 learning_rate 一起收敛learning_rate0.10.05小步长降低过拟合代价是训练变慢num_leaves3131~63叶子数越大模型复杂度越高验证集 AUC 下降就回退colsample_bytree1.00.8每棵树随机取一部分列增强鲁棒性reg_alpha0.00.1L1 正则行为数据稀疏时很有用verbosity1-1关闭训练过程刷屏输出提示n_estimators 和 learning_rate 要一起调。先固定 learning_rate0.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(fRecall100: {recall_top100:.4f})逻辑说明argsort 按预测概率从大到小排序取前 100 个用户索引再统计这 100 个里真实下单用户占全部真实下单用户的比例。这个指标直接对应推荐场景的“召回多少”问题和 AUC 一起看才能判断模型是否真的可用。如果 Recall100 明显偏低先回去检查负采样比例而不是继续调参。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, topn10): sim { other: cnt / (goods_pop[gid] goods_pop[other] - cnt) for other, cnt in co_occur[gid].items() } return sorted(sim.items(), keylambda x: x[1], reverseTrue)[: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。本文还有配套的精品资源点击获取