ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Python的商品推荐系统全流程:从数据清洗到协同过滤与混合排序

基于Python的商品推荐系统全流程:从数据清洗到协同过滤与混合排序 简介一套基于Python实现的商品推荐系统完整方案面向推荐系统初学者与中高级开发者也适合用于课程设计、毕业设计或比赛实战。项目基于约15万用户、12万商品的脱敏数据集由于数据已预处理读者可直接聚焦算法实现。内容覆盖从数据加载到推荐结果生成的核心链路包括Pandas的DataFrame操作、特征选择与缺失值处理、分类特征编码以及基于内容、协同过滤、SVD/ALS矩阵分解等经典推荐算法同时涉及训练集与测试集划分、K折交叉验证、准确率与召回率评估、网格搜索调参和模型集成优化。资源包共7个文件以4个Python脚本为核心按召回、构图、排序和最终求解等推荐模块组织辅以2个二进制数据文件和1个Markdown说明文档整体约37.37MB。代码结构清晰可直接运行帮助读者快速理解大规模推荐系统的工程化实现。目前已有414人学习下载是理论与实践结合的优质学习素材。1. 商品推荐系统不是模型比赛而是数据工程一个推荐系统能不能用七成看数据怎么组织三成才看模型。很多团队拿着公开数据集跑出漂亮的离线指标一上线就被用户点“不感兴趣”问题往往不是算法不够新而是把用户行为、商品属性、时间上下文这三类信息揉在一起时出了错。用 Python 做商品推荐系统最大的优势不是某个库多厉害而是从 pandas 清洗、surprise 建模到 FastAPI 上线能用一条技术栈把全流程串起来不用在不同语言之间来回倒数据。这篇文章要讲的是一个能落地的推荐系统由哪几块组成协同过滤怎么从零手写混合排序的参数怎么调以及离线指标好看但线上翻车时问题到底出在哪。2. 数据准备是推荐系统的地基先搞清楚用户与商品的关系2.1 用户行为数据从日志到评分矩阵缺一步都跑不起来商品推荐系统最常见的输入是隐式反馈比如点击、加购、下单、收藏。很多人拿到数据后第一件事就是建一个“用户-物品”矩阵把购买算 1、没购买算 0。这个做法能跑通但有两个隐患第一购买行为本身有季节性波动第二热门商品被几乎所有人买过会让相似度计算失真。我一般会把原始行为日志整理成一张长表至少包含user_id、item_id、behavior_type、timestamp、scene_id五个字段。behavior_type用来区分浏览、收藏、加购、下单这四类行为的权重完全不同下单权重最高加购次之收藏再降一档浏览最低。如果只把下单看成正样本会丢掉大量“看了没买但后来又买了”的弱信号。一个可行的清洗脚本如下import pandas as pd df pd.read_csv(behavior_log.csv, parse_dates[timestamp]) df df.drop_duplicates(subset[user_id, item_id, behavior_type, timestamp]) # 行为权重映射下单4、加购3、收藏2、点击1 weight_map {click: 1, fav: 2, cart: 3, buy: 4} df[weight] df[behavior_type].map(weight_map) # 剔除异常用户行为数小于5的用户保留意义不大 user_active df.groupby(user_id)[weight].sum() valid_users user_active[user_active 5].index df df[df[user_id].isin(valid_users)] # 剔除异常商品被点击次数过多但转化极低的商品往往是引流垃圾 item_stats df.groupby(item_id)[behavior_type].agg( [count, lambda s: (s buy).mean()] ).rename(columns{count: total, lambda_0: buy_rate}) valid_items item_stats[(item_stats[total] 10) (item_stats[buy_rate] 0.9)].index df df[df[item_id].isin(valid_items)] print(df.groupby(behavior_type).size())这段代码里drop_duplicates不是拍脑袋加的。同一个用户在同一秒对同一商品产生了点击和加购日志系统可能会写两条不去重会让后续矩阵出现重复计数。weight_map的取值不是固定的如果你的业务里“收藏”比“加购”更能代表兴趣可以调整权重但要注意权重差距不要超过一个数量级否则矩阵数值会退化成只认下单。剔除异常用户和商品的关键在于阈值。用户行为总数少于 5可能是注册机商品被点击很多但购买率低于某个值可能是内容包装得很吸引人但实际品质差。这里的阈值要按你的业务分布调不能照抄。跑完清洗后你应该打印每个行为类型的数量确认点击、收藏、加购、下单的占比大致符合漏斗如果下单占比超过 10%多半是日志上报埋点有问题。2.2 构建用户-物品矩阵内存能放下的都要显式构造清洗完之后下一步是把长表展开成矩阵。常见的做法是pivot_table但要注意稀疏度。假设有 10 万用户、30 万商品矩阵就是 10 万乘 30 万直接存成 dense DataFrame 会占用几百 GB。所以必须用稀疏矩阵。我一般用scipy.sparse.csr_matrix存交互权重而不是用 pandas 透视表。原因是后续相似度计算、矩阵分解都依赖稀疏运算csr_matrix能省内存而且sklearn里的模型大多直接接受稀疏输入。import numpy as np from scipy.sparse import csr_matrix user_ids df[user_id].astype(category).cat.codes.values item_ids df[item_id].astype(category).cat.codes.values weights df[weight].values # 用户/商品编码表的反查字典后续展示推荐结果要用 user_decoder dict(enumerate(df[user_id].astype(category).cat.categories)) item_decoder dict(enumerate(df[item_id].astype(category).cat.categories)) matrix csr_matrix( (weights, (user_ids, item_ids)), shape(user_ids.max() 1, item_ids.max() 1), dtypenp.float32 ) # 打印稀疏度稀疏度高于 99.9% 是常态 sparsity 1 - (matrix.nnz / (matrix.shape[0] * matrix.shape[1])) print(f矩阵形状: {matrix.shape}, 稀疏度: {sparsity:.4%})这段代码里astype(category)会自动把字符串用户 ID 映射成连续的整数编码省去手写字典的麻烦。csr_matrix的行是用户、列是商品数值不是简单的 0/1而是加权的行为权重。这个矩阵后续既能用来算用户相似度也能用来算商品相似度。需要特别注意的是csr_matrix的切片和行操作很方便但如果要对矩阵做逐列计算效率会低因为压缩存储按行优化。你后面如果要对商品列做归一化先把矩阵转成csc_matrix再操作会快很多。这个细节在数据量大时能差出好几倍时间。2.3 特征工程把时间衰减和商品属性揉进去纯粹的用户-物品交互矩阵只能回答“谁和谁像”回答不了“为什么推荐”。要让推荐结果可解释还得补充商品侧特征和用户侧特征。商品侧至少要包括品类、价格档位、上架时间、好评率。用户侧至少包括近 30 天下单类别偏好、价格敏感度、活跃时段。时间衰减是这里最容易被忽略的。三个月前的加购和昨天的加购对当前兴趣的指示作用完全不同。常见做法是在构建矩阵时对交互权重乘一个衰减因子比如exp(-lambda * days_since_interaction)。lambda 按业务周期调如果商品是日用品周期短lambda 取 0.05如果是耐用品周期长lambda 取 0.01。import numpy as np from datetime import datetime ref_date datetime(2024, 6, 1) df[days_since] (ref_date - df[timestamp]).dt.days # 时间衰减权重lambda 可调日用品 0.05耐用品 0.01 df[decay] np.exp(-0.03 * df[days_since]) df[final_weight] df[weight] * df[decay]这样做的意义是让近期的行为在相似度计算中占据更高话语权。注意ref_date必须是一个固定的锚点日期不能用pd.Timestamp.now()否则每次跑训练结果都会变模型上线以后离线指标天天漂移排查起来非常痛苦。特征工程做到这里你手上的数据已经不是原始日志而是一个带权重的稀疏矩阵和两张属性表。后面的协同过滤、混合排序都基于这些数据展开。常见做法是先把特征落盘成 parquet训练时再加载不要每次从原始日志现算否则训练一次要跑半小时迭代实验效率太低。3. 用 Python 实现协同过滤从零手写物品相似度与评分预测3.1 为什么先从物品相似度入手ItemCF 的工程优势协同过滤分两大类基于用户的 UserCF 和基于物品的 ItemCF。商品推荐系统里ItemCF 是更稳妥的起点。原因是商品数量通常比用户数量少一个数量级物品相似度矩阵的规模可控而且物品的属性相对稳定相似度矩阵可以每天只更新一次。UserCF 的劣势在于用户量太大用户相似度矩阵存不下来另外用户兴趣变化快实时计算成本极高。ItemCF 的核心假设是用户对商品 A 感兴趣是因为他之前喜欢的商品 B 和 A 相似。所以第一步是求出任意两个商品之间的相似度。相似度不能只用“同时被购买次数”衡量因为热门商品会和所有商品共现导致相似度虚高。经典做法是拿商品的共现次数除以商品流行度的几何平均或者直接用余弦相似度。我用余弦相似度来写因为实现简单且效果稳定。公式不多说直接看代码from sklearn.metrics.pairwise import cosine_similarity # item_matrix 是商品-用户矩阵行为商品列为用户 item_matrix matrix.T.tocsr() item_sim cosine_similarity(item_matrix, dense_outputFalse) # 只保留每个商品最相似的 top 200其余置零控制内存 top_k 200 indices item_sim.indices.reshape(item_sim.shape[0], -1) data item_sim.data.reshape(item_sim.shape[0], -1) for i in range(data.shape[0]): row data[i] if row.shape[0] top_k: keep np.argpartition(row, -top_k)[-top_k:] mask np.ones(row.shape[0], dtypebool) mask[keep] False row[mask] 0 item_sim item_sim.maximum(item_sim.T) # 保证对称cosine_similarity对稀疏矩阵dense_outputFalse返回的还是稀疏矩阵这一步必须做否则商品数一多内存直接爆。reshape是为了逐行处理argpartition是取前 top_k 的快速方法比argsort快得多。最后maximum(item_sim.T)是为了让相似度矩阵对称因为理论上来讲 A 对 B 的相似度和 B 对 A 的相似度应该一样但余弦计算在稀疏场景下可能出现不对称需要强制修正。3.2 评分预测加权求和与归一化有了商品相似度矩阵后对用户预测某商品 i 的评分就是找用户历史行为里与 i 最相似的若干商品按相似度加权求和。这里有个容易踩坑的点如果直接用相似度做权重热门商品会把所有评分拉高所以要先对用户历史行为做一个归一化。def predict_score(user_id, item_id, item_sim, matrix, top_n50): # 取该用户在矩阵中的行 user_row matrix[user_id] # 找出用户历史上有行为的商品索引 interacted_items user_row.indices if item_id not in interacted_items: # 如果商品已经交互过不再预测排除已知点击/购买 return 0.0 # 从相似度矩阵中取出 item_id 对应的行 sim_row item_sim[item_id].toarray().ravel() # 只取用户交互过的商品 valid_indices np.intersect1d(interacted_items, sim_row.nonzero()[0]) if valid_indices.size 0: return 0.0 # 加权求和相似度当作权重 weights sim_row[valid_indices] scores user_row[0, valid_indices].toarray().ravel() return float(np.dot(weights, scores) / np.sum(weights))这个版本的函数适合理解原理不适合直接上线。原因在于每次预测都要对sim_row做toarray()如果相似度矩阵是稀疏的这一操作会把一行展开成稠密数组内存和时间都浪费。真实工程里我会把item_sim按 CSR 格式切片只取非零元素参与计算。更重要的问题是用户历史行为里有下单、加购、收藏、点击权重不同但在scores里只取了原始权重。如果你在数据准备阶段把final_weight直接写进矩阵这里的scores就已经带上了行为权重和时间衰减不需要额外处理。如果矩阵里存的是 0/1这里的预测就会把所有行为一视同仁结果是推荐列表偏向点击多但转化差的商品。3.3 生成推荐列表过滤已购、惩罚热门、保证多样性评分预测函数只能算单个商品的分数生成推荐列表时要对全量商品做一遍预测然后排序取 top N。全量商品遍历在商品数几万时勉强能扛超过十万就太慢。工程上常见做法是用向量化方式一次算出一个用户对所有商品的评分def recommend_items(user_id, item_sim, matrix, top_n10): user_row matrix[user_id] interacted_items user_row.indices # 相似度矩阵乘以用户行得到用户对所有物品的加权评分 scores item_sim.T.dot(user_row.T).toarray().ravel() # 已交互商品置为负无穷避免重复推荐 scores[interacted_items] -np.inf # 热门惩罚把每个商品的热度取对数减去一个惩罚项 item_popularity np.asarray(matrix.sum(axis0)).ravel() pop_penalty 0.2 * np.log(item_popularity 1) scores - pop_penalty # 取 top_n top_indices np.argsort(scores)[-top_n:][::-1] return [(item_decoder[i], scores[i]) for i in top_indices]这段代码的核心是item_sim.T.dot(user_row.T)。它把物品相似度矩阵转置后与用户向量相乘等价于将用户交互过的每个商品的相似度行按权重加总得到一个用户对全量商品的评分向量。比逐个商品预测快几个数量级。pop_penalty是可调项。如果不做热门惩罚推荐列表会变成全网爆款合集用户觉得“这还用你推荐”。惩罚系数 0.2 是我常用的起点你可以按业务调整如果你的商品冷门长尾居多系数可以降到 0.1如果爆款占营收大头系数可以升到 0.3。注意np.log(item_popularity 1)是为了压热度长尾直接用热度会让最热门商品被过度惩罚。生成推荐列表后还需要按业务规则过滤比如下架商品、无库存商品、限售地区商品。这些规则不应该写在推荐算法里而应该放在推荐结果后置过滤阶段单独维护一份黑名单表。4. 混合推荐与排序让协同过滤不再自嗨4.1 协同过滤的短板冷启动和热门偏差纯 ItemCF 有两个硬伤新商品没有任何交互记录相似度矩阵里它的行是空的新用户只有一两条行为预测分数极不稳定。这两个问题靠算法本身解决不了必须叠加其他推荐策略。实际业务中我会把推荐结果分成几路召回。第一路是 ItemCF负责给有历史行为的老用户挖掘相似商品。第二路是热门榜负责冷启动用户取近 7 天按 UV 排序的热门商品。第三路是内容召回基于商品的品类、品牌、价格档位做规则匹配比如用户近期浏览了某品牌的耳机就把同品牌同价位的耳机召回。第四路是协同过滤的 UserCF 变体找出与当前用户最相似的 50 个用户把这些人近期购买的商品拿过来。这四路召回各有特点但最终展示给用户的只有一个位置。所以需要一个排序层把多路结果融合打分。4.2 用加权融合和逻辑回归做排序最简单的融合方式是加权线性融合每一路召回结果带一个分数乘以权重后相加。这种方式可解释性强调试容易但权重需要人工调整。更稳的做法是用逻辑回归排序把每一路的得分和特征作为特征把用户是否点击/购买作为标签学习一组最优权重。逻辑回归在推荐排序里不是因为它模型简单才常用而是因为特征可解释、训练快、方便做线上调试。如果你准备用逻辑回归做排序特征至少包含ItemCF 得分、热门得分、内容匹配分、商品平均评分、商品价格档位、用户过去 7 天活跃度、同一品牌商品的历史点击率。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split # 假设你已经把每一路得分和标签拼成了特征矩阵 X # 最后一列是标签: 0 未点击/未购买, 1 点击/购买 X features_df.drop(label, axis1).values y features_df[label].values X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) model LogisticRegression(C1.0, max_iter1000, class_weightbalanced) model.fit(X_train, y_train) print(训练集AUC:, roc_auc_score(y_train, model.predict_proba(X_train)[:, 1])) print(测试集AUC:, roc_auc_score(y_test, model.predict_proba(X_test)[:, 1])) print(权重:, dict(zip(features_df.columns[:-1], model.coef_[0])))class_weightbalanced非常重要。用户点击行为通常只有 10% 左右正负样本极度不平衡如果不加这个参数模型会学出“永远预测负样本”的傻瓜模型。C1.0是正则化强度过小会让特征权重趋近于零过大会过拟合线上噪声一般先在 0.1、1、10 里粗筛。评估指标不能只看 AUC。AUC 只衡量排序质量不看具体推荐位上的命中率。上线前我更关心的是 Precision5用户是否点击了推荐列表的前 5 个商品。这个指标能反映排序层有没有把用户最想要的东西顶到最前面。4.3 多路召回融合的调参顺序权重参数怎么调很多人一上来就上贝叶斯优化结果调到过拟合。我的血泪经验是先固定排序模型人工调三路召回的比例关系等离线指标不再明显提升时再动排序模型的正则化参数。具体步骤是先把 ItemCF、热门、内容三路召回的权重设成 1:1:1记录离线指标。单独调一路比如把 ItemCF 权重从 1 调到 1.5、2看 Precision5 和覆盖率变化。覆盖率下降说明该路权重过大把用户带进了一个小圈子覆盖率上升说明该路补充了长尾信息可以保持。所有路都调完后再统一回看 AUC 有没有崩。调参过程中你要记住一个常识离线 AUC 再高如果覆盖率持续下降线上用户很快会感到腻。覆盖率这个指标往往比精准率更重要它决定了推荐系统的天花板。5. 避坑与排查商品推荐系统最常见的 5 个翻车现场5.1 离线指标与线上效果相差过大现象离线 AUC 从 0.78 提升到 0.81线上点击率反而下降。原因离线评测使用的是历史行为数据你预测的是“用户过去会点什么”。但线上推荐会改变用户行为用户看到推荐后点了更多商品这些新交互没有进入离线测试集造成评估偏差。另外离线测试集如果用了随机切分没有按时间切分会导致模型学到未来信息。解决离线评测必须按时间切分。用前 70% 天的行为做训练后 30% 天的行为做验证。上线前做一个影子评测把推荐结果记录下来但不展示给用户等过一天再对比“影子推荐”和“线上当前推荐”哪个更符合用户真实行为。5.2 新商品永远没有出头之日现象刚上架的新品没有任何交互ItemCF 和热门召回都拿不到它于是它永远得不到曝光陷入死循环。原因协同过滤只认历史交互新品注定在相似度矩阵里是零向量。热门榜又是一周累计的 UV新品排不进去。解决给召回层加一路“新品召回”取上架 24 小时内的商品按类目分配固定坑位。排序层给新品一个单独的is_new特征让模型学会“新品点击率通常偏低但仍值得尝试”。另外可以将 ItemCF 的相似度矩阵更新频率提高从每日一次改成每六小时一次让新品在进入矩阵后尽快获得相似商品关联。5.3 商品相似度矩阵被一个超热门商品污染现象相似度矩阵里几乎所有商品都和某个爆款相似推荐列表里全是爆款的“平替”用户觉得推荐没新意。原因余弦相似度对热门商品没有惩罚。一个被所有人购买过的商品它的向量几乎和任何商品都有一定夹角导致它的相似度行非零元素极多。解决在数据准备阶段做热门商品降权。经典做法是加 IDF 惩罚该商品被交互的用户越多它在相似度计算中的权重越低。实现时可以在商品-用户矩阵里对列做缩放每列除以log(1 用户数)再算余弦相似度。这样爆款对相似度的影响会被压住。5.4 用户 ID 和商品 ID 编码不一致导致推荐结果张冠李戴现象线上推荐列表里出现莫名其妙的商品排查发现是 ID 编码表切换导致的。原因训练时用了astype(category)自动编码编码表是临时生成的。线上服务如果加载了旧编码表用户 ID 映射到错误的索引取出来的推荐商品自然全错。解决把编码表持久化每次训练后都保存user_encoder.json和item_encoder.json线上加载时必须和训练时的编码表严格一致。如果业务 ID 有变化比如商品下架重上架导致 ID 被重用要在数据清洗阶段就排除掉这些脏 ID不要等到训练完才发现映射错乱。5.5 Python 环境依赖导致推理服务无法启动现象模型训练一切正常但线上推理服务启动时报ImportError或者numpy版本冲突。原因推荐系统常用的pandas、scikit-learn、scipy版本更新很快本地训练环境和线上容器环境之间没有锁版本导致numpyABI 不兼容。解决用requirements.txt锁定所有关键依赖的精确版本比如numpy1.26.4、scikit-learn1.4.2。更稳的做法是构建 Docker 镜像时把训练的同一个 Python 环境打包进去训练和推理使用同一个镜像而不是从零配线上环境。上线前先在 staging 环境跑一遍python -c import sklearn, scipy, pandas; print(ok)验证依赖。6. 从离线评估到线上 A/B推荐系统是否真的变好得用数据说话离线评估指标只能告诉你“模型有没有学歪”不能告诉你“用户是否更满意”。我习惯在离线阶段同时看四个指标AUC、Precision5、覆盖率、多样性。AUC 看排序能力Precision5 看推荐位质量覆盖率看推荐结果是否局限于热门商品多样性看推荐列表内部是否重复。覆盖率计算公式是推荐出来的不同商品数 / 全量商品数。如果这个数值长期低于 5%说明推荐系统已经变成了爆款复读机。多样性我会用推荐列表中商品品类去重后的数量来衡量如果一次推荐 20 个商品里面有 18 个是同品类说明相似度计算过于集中在某个品类上。线上评估我一般用 A/B 测试。切分用户流量时要注意不能按用户 ID 的哈希直接切一半因为老用户和新用户对推荐系统的依赖程度不同。合理做法是按用户活跃度分层把用户分成新用户、7 天活跃、30 天活跃三组每组内部再随机分流。这样能避免对照组和实验组用户构成不同导致的偏差。实验周期至少跑 7 天。因为推荐系统的效果有滞后性用户今天看到的推荐可能三天后才决定购买。跑 7 天能覆盖一个完整的购物周期。如果追求更精确的结果可以跑 14 天把周末和促销日都包含进来。最终我判断一个推荐系统改动是否成功的标准很简单实验组的人均点击商品数是否有显著提升同时人均下单转化不能下降。点击量提升很多但转化下降说明推荐把用户引流到了标题党商品这是典型的推荐系统翻车。相反点击量微降但转化显著上升说明推荐结果更精准这个改动值得全量。养成一个习惯每次上线新模型都把模型输出的推荐列表样本存一份隔天人工看一眼。不是看整体指标而是看具体用户面对的具体推荐内容。这个习惯救过我很多次有一次离线指标全绿人工一看才发现推荐列表里有一半是用户已经退货的商品原因是清洗时漏掉了退货行为。这种问题指标看不出来只有人眼能看出来。推荐系统不是精调一个算法就能做好的它是数据、特征、模型、工程四件事的持续迭代。希望这篇笔记帮你避开最常见的坑把第一个能用、能评估、能量化的推荐系统做出来。本文还有配套的精品资源点击获取
返回列表