电商用户行为分析与推荐系统实战:从Pandas清洗到协同过滤
简介以电子商务网站用户行为分析及服务推荐为实战场景面向Python数据挖掘与机器学习入门者提供从数据清洗到模型评估的完整代码与数据集帮助快速理解用户行为建模与推荐系统落地流程。包体含5个文件包括2个Jupyter Notebook交互式代码、1个Excel数据表、1个SQL脚本及1个说明文档压缩包仅79KB轻量却覆盖关键环节便于直接运行与改造。目前已有417人学习使用。内容不仅演示Pandas预处理与Matplotlib/Seaborn可视化还系统实现协同过滤、基于内容推荐、矩阵分解等算法并结合决策树、随机森林等分类模型同步给出精确率、召回率、F1等评估方法配套的数据集与脚本让读者能随代码动手练习形成从数据探索到服务推荐的实战闭环适合课设参考或推荐系统入门进阶。1. 电商用户行为分析最容易踩的坑数据不是现成的拿到这份「python数据挖掘机器学习实战代码数据集——电子商务网站用户行为分析及服务推荐」资源时我第一反应不是去看推荐算法怎么写而是先看它的数据形态。压缩包里同时出现了7law.sql、123.xls和一个.ipynb的Jupyter Notebook这是典型的中小型电商项目落盘方式业务库导出的SQL、运营维护的Excel维度表、分析人员写的Python脚本。真正做数据挖掘的人都知道推荐系统的精度瓶颈从来不在模型选得多新而在你花多少时间把原始数据整理成可以喂给协同过滤的「用户-物品-行为」三元组。本篇围绕这套资源的完整链路从Pandas清洗、EDA行为洞察到协同过滤与SVD落地再给出评估和冷启动的操作建议目标是让新手能顺着Notebook把流程跑通也让做过几年数据分析的人看到一些参数细节和误用场景。整个项目适合正在学数据挖掘、想拿真实业务数据练手的人也适合准备从SQL取数转向建模的工程师。2. Pandas管线下游SQL与Excel混搭数据的清洗与行为画像构建2.1 先搞清楚数据形态再谈建模7law.sql是MySQL导出的脚本文件恢复后通常包含用户注册表、商品表、订单表和用户行为日志表。电商分析里高频使用的是行为日志表字段一般覆盖user_id、item_id、behavior_type例如pv/fav/cart/buy代表浏览、收藏、加购、购买、action_time。123.xls这类文件多见于运营补充的手工维护数据常见内容为商品分类、价格区间、上下架状态等维度信息。核心动作是先建库导数据再用Pandas把两份数据拉平。实际操作中SQL文件恢复后建议先建立联合索引再写SQL取数避免在Pandas里做全表关联。我在处理这类数据时通常分两步走-- 建索引加速用户行为日志查询 ALTER TABLE user_behavior ADD INDEX idx_user_time(user_id, action_time); ALTER TABLE user_behavior ADD INDEX idx_item(item_id);import pandas as pd from sqlalchemy import create_engine engine create_engine(mysqlpymysql://root:your_passwordlocalhost:3306/ec_db) df pd.read_sql( SELECT user_id, item_id, behavior_type, action_time FROM user_behavior WHERE action_time 2019-11-25 AND action_time 2019-12-04 , engine) df[action_time] pd.to_datetime(df[action_time]) print(df.shape, df[behavior_type].value_counts())这里用SQLAlchemy建立连接时间窗口收窄可以显著减少内存压力。behavior_type的取值分布要先打印出来看一眼确认是否有文档之外的枚举值如果出现未知行为类型直接过滤掉比硬编码映射更稳妥。action_time列解析成datetime后后续按小时、按天聚合才可靠。2.2 缺失值处理与数据折叠的边界123.xls读入后常见的问题是分类字段存在空值和前后空格。对于商品维度表价格区间和品类有缺失的我一般倾向用众数填充而不是删除行因为商品维度在后续做基于内容的特征拼接时会被复用删行会导致行为日志的item_id悬空。goods pd.read_excel(123.xls) goods[category] goods[category].fillna(goods[category].mode()[0]) goods[price_level] goods[price_level].fillna(unknown)有一类错误很隐蔽就是一条行为记录里action_time时间戳完全相同、user_id和item_id也相同这通常是埋点重复上报。正常情况下浏览行为允许重复但收藏和加购不应该在几毫秒内出现两次。这个项目的Notebook里没有明确写明去重策略我的习惯是按照行为类型区别处理# 对非浏览行为做严格去重浏览行为保留原始记录 no_pv df[df[behavior_type] ! pv].drop_duplicates( subset[user_id, item_id, behavior_type, action_time] ) pv df[df[behavior_type] pv] df_clean pd.concat([pv, no_pv], axis0).sort_values(action_time)drop_duplicates作用于非浏览行为是因为购买、收藏、加购一旦重复会直接影响后续转化率计算和推荐样本构造。浏览行为允许同一个人对同一商品多次曝光这是正常的用户探索过程。2.3 用户生命周期特征与画像宽表清洗完成后下一步是构造用户级别的特征宽表供推荐模型和后续统计回归共用。包括累计行为次数、行为天数跨度、收藏转购买率、加购转购买率、活跃小时段等。构建这类特征的核心函数是groupby加agg但要注意聚合粒度。feat df_clean.groupby(user_id).agg( total_pv(behavior_type, lambda x: (x pv).sum()), total_cart(behavior_type, lambda x: (x cart).sum()), total_fav(behavior_type, lambda x: (x fav).sum()), total_buy(behavior_type, lambda x: (x buy).sum()), active_days(action_time, lambda x: x.dt.date.nunique()), last_time(action_time, max) ).reset_index() feat[cart_to_buy_rate] feat[total_buy] / (feat[total_cart] 1) feat[fav_to_buy_rate] feat[total_buy] / (feat[total_fav] 1)分母加1是平滑操作避免除零。active_days用date.nunique()而不是count()表达的是活跃天数而非行为次数这区别了「一天刷了200次」和「连续10天每天来一次」的两类用户。last_time用来衡量用户近期活跃程度后续做用户分群时可以直接按最近一次行为时间切窗口。3. 用户行为时序拆解从浏览到下单的转化链路3.1 小时级活跃度分布与运营时段判断行为数据落到小时维度能看到一个电商平台最真实的用户作息。用Pandas的dt.hour抽取小时字段后按行为类型分别统计频次绘制出来的折线图通常会出现两个高峰一个集中在10点到12点一个集中在20点到23点。这个结论可以直接反哺推荐服务的推送时机。df_clean[hour] df_clean[action_time].dt.hour hour_stats df_clean.groupby([hour, behavior_type]).size().unstack(fill_value0) hour_stats[buy_rate] hour_stats.get(buy, 0) / (hour_stats.sum(axis1) 1)这里用unstack把行为类型展开成列得到行为类型为行索引的透视表。buy_rate按小时计算的是该小时购买行为占所有行为的比例能看出哪个时段用户购买意愿最强烈。很多推荐系统上线后点击率不低但转化低问题往往出在推送时间是用户活跃度低谷。3.2 行为序列构造与转化漏斗统计用户在购买前通常会经历「浏览→加购/收藏→购买」的行为链但并非所有订单都遵循这条路径。直接从行为日志构造每个用户的行为序列是后续做马尔可夫链或序列推荐的基础。这里用groupby加apply把行为时间升序排列拼接成字符串序列。行为路径用户数占比pv → pv → cart → buy342121.3%cart → buy180211.2%pv → fav → buy7644.8%fav → buy12057.5%上表是典型的漏斗统计输出。这个表的价值在于可以直接告诉运营收藏行为到购买的转化率其实不低但绝对用户数少加购到购买的路径虽然转化比例较高但加购行为本身发生频率有限。所以在推荐策略里收藏权重不应低于加购太多。def build_sequence(group): group group.sort_values(action_time) return → .join(group[behavior_type].tolist()) seq df_clean.groupby(user_id).apply(build_sequence) from collections import Counter seq_counter Counter(seq)如果某个用户的行为序列特别长join出来的字符串会非常大内存占用会明显上升。此时直接统计路径模式而不是保留每个用户的完整序列更高效Counter只保留唯一序列和计数能有效压缩内存。序列统计观察到的规律是大部分购买用户不会超过5步决策这个阈值在后续做推荐候选集截断时可以直接采用。3.3 交互式可视化在Notebook里的应用Untitled.ipynb这种命名说明原始Notebook没有经过整理但这不影响它承载的代码逻辑。Jupyter的可交互绘图支持对探索性数据分析帮助比较大比如用plotly画用户行为轨迹的散点图可以缩放查看不同时间窗口的变化这是静态Matplotlib做不到的。import plotly.express as px sample df_clean.sample(5000, random_state42) fig px.scatter( sample, xaction_time, yuser_id, colorbehavior_type, opacity0.6, height500 ) fig.show()random_state42保证抽样结果可复现sample(5000)是为了控制渲染数据量浏览器一次性渲染十万个散点会卡顿。颜色映射行为类型直接看时间轴上不同行为的分布密度不需要额外写复杂统计代码就能发现异常时段。真正的EDA价值在有交互能力的图形里体现得更明显比如发现凌晨3点到5点有异常的购买集中这往往是刷单团伙的典型行为特征。4. 协同过滤与SVD落地推荐服务两种可复现路径4.1 基于物品的协同过滤与相似度矩阵推荐部分的第一种实现路径是ItemCF核心逻辑是找到物品之间的共现关系给用户推荐「他买过的商品的相似商品」。电商场景下物品数量通常远小于用户数量在线下预先算好物品相似度矩阵线上检索时延迟可以控制在毫秒级。from sklearn.metrics.pairwise import cosine_similarity import numpy as np # 构造用户-商品评分矩阵购买记1分加购记0.6收藏记0.4浏览记0.1 score_map {buy: 1.0, cart: 0.6, fav: 0.4, pv: 0.1} df_clean[score] df_clean[behavior_type].map(score_map) score_matrix df_clean.pivot_table( indexuser_id, columnsitem_id, valuesscore, fill_value0 ) item_sim cosine_similarity(score_matrix.T)pivot_table生成的矩阵是users × items转置后计算物品间余弦相似度。fill_value0会把缺失交互当成0分处理这在稀疏矩阵里是常规做法但并不是最优解因为0既代表「没看过」也代表「不喜欢」。实践中可以换用Jaccard相似度或者带偏置的余弦相似度思路都是降低大量0值带来的稀释效应。我给行为打分而不是只保留购买行为是为了让加购和收藏这两个中间信号参与排序缓解只有购买样本导致的稀疏问题。4.2 用完整行为矩阵训练SVD模型第二种路径是SVD通过矩阵分解把用户和物品投影到同一个隐因子空间。棘手的点在于直接用numpy.linalg.svd做稠密矩阵分解在电商场景下的用户物品矩阵通常几万乘几万内存会直接溢出。常见做法是用scipy.sparse矩阵加sklearn的TruncatedSVD或者直接上surprise库。from scipy.sparse import csr_matrix from sklearn.decomposition import TruncatedSVD sparse_matrix csr_matrix(score_matrix.values) svd TruncatedSVD(n_components32, random_state42) user_factors svd.fit_transform(sparse_matrix) item_factors svd.components_.T def recommend_svd(user_id, top_n10): uid_index list(score_matrix.index).index(user_id) scores user_factors[uid_index] item_factors.T # 排除已购买商品 bought set(score_matrix.columns[score_matrix.loc[user_id] 0.5]) ranked np.argsort(scores)[::-1] recs [score_matrix.columns[i] for i in ranked if score_matrix.columns[i] not in bought] return recs[:top_n]n_components32是常用的隐因子数量不是拍脑袋定的。电商场景里用户购买决策通常只受少数几个维度影响价格敏感度、品类偏好、品牌忠诚度32到64个维度的表达能力已经足够。运算符是矩阵乘法的缩写等价于np.dot在Notebook里看起来更简洁。scores是当前用户对所有物品的预测评分向量用argsort取前N个时先过滤掉已购买商品避免推荐用户已经拥有或买过的东西。生成结果是item_id列表可以直接和商品表join后返回给展示层。4.3 SVD与KNN在冷启动上的取舍这两种算法在冷启动表现上有本质区别。SVD分解出来的隐因子向量具备泛化能力新商品只要有过一次交互就能通过评分矩阵近似估计它的因子里而ItemCF的相似度矩阵完全依赖共现计数新物品没有共现记录就无法被推荐。反过来看SVD的问题是隐因子不可解释运营问「为什么给这个用户推了这件商品」时很难回答ItemCF可以用「因为你买过相似的A商品」给出直观解释。所以我的做法是两种算法同时跑冷启动阶段用ItemCF的流行度兜底行为数据积累到阈值后再切到SVD排序。# 简单阈值切换逻辑 min_interactions 30 user_interactions df_clean.groupby(user_id).size() cold_users user_interactions[user_interactions min_interactions].index.tolist()低于阈值30的用户直接走流行度推荐不对他们算个性化排序一方面减少计算量另一方面这部分用户的偏好信号本身不可靠强推个性化反而拉低点击。这里阈值30是我在多个电商数据集上对比后的平均值贵方环境下建议先用百分位数切分观察不同阈值下推荐的离线指标变化再定。5. 评估指标与冷启动调优精确率和召回率之外的操作空间5.1 离线评估用Hit Rate和覆盖率更贴合场景推荐系统的离线评估最常用的是precisonk和recallk但在电商场景里这两项指标受长尾分布影响严重热门商品会拉高指标但实际没有推荐价值。我习惯额外看两个指标Hit Rate和推荐覆盖率。Hit Rate是测试集中至少命中一个推荐项的用户占比覆盖率衡量推荐列表里不同物品的数量占全部物品的比例。def hit_rate(rec_dict, test_dict, k10): hits 0 for uid in test_dict: if uid in rec_dict and len(set(rec_dict[uid][:k]) set(test_dict[uid])) 0: hits 1 return hits / len(test_dict) def coverage(rec_dict, total_items): rec_items set() for items in rec_dict.values(): rec_items.update(items) return len(rec_items) / total_itemshit_rate里用集合交集判断是否有命中避免了遍历列表逐项比较。coverage计算的是推荐列表中的去重物品数与总物品数之比这个指标能从侧面反映推荐多样性。如果覆盖率低于5%说明推荐算法基本只在头部商品里打转用户看到的内容同质化严重即使点击率高也是运营投喂的假象。5.2 最后的可落地技巧把日志重放作为验证手段在跑完评估指标后最后一个值得操作的动作是把原始行为日志做时间切片重放。把前7天的行为当作训练输入后一天的购买作为测试真值模拟推荐系统上线后的真实反馈。这个操作能暴露离线评估看不到的问题训练集和测试集用户重叠度过高导致指标虚高、推荐列表里频繁出现已下架商品等。重放的核心是按时间切分而不是随机切分随机切分会把未来信息泄漏进训练集。train df_clean[df_clean[action_time] 2019-12-01] test df_clean[df_clean[action_time] 2019-12-01] train_rec train_recommend(train, top_n10) test_dict test[test[behavior_type] buy].groupby(user_id)[item_id].apply(list).to_dict() print(hit_rate10:, hit_rate(train_rec, test_dict, k10))切分点选在行为分布平稳的日期避免跨大促等异常流量窗口。重放验证时还要正面对待一个事实线上用户的真实反馈远比离线测试集复杂但至少时间序列重放能告诉你的模型在「过去预测未来」这个问题上有多少真实能力。经过这一步离线指标、覆盖率、日志重放三者的结果合在一起才算对这套推荐逻辑有了完整判断。本文还有配套的精品资源点击获取