news 2026/10/21 23:14:33

基于Python的二手交易网站协同过滤推荐系统实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Python的二手交易网站协同过滤推荐系统实践

前阵子一直在折腾一个带推荐模块的Python二手交易网站,从数据库设计到协同过滤算法落地,磕磕绊绊踩了一堆坑。这个项目看起来很常规,但真正动手之后会发现:二手交易场景对推荐系统的要求跟普通电商完全不一样,且协同过滤在数据稀疏情况下的表现直接影响整个系统体验。这篇文章把整个项目从架构到算法细节都拆开来说,尤其聚焦协同过滤模块是怎么在真实业务里落地的。

这套系统没有停留在传统商城模版的增删改查上,而是围绕“用户需要什么”做个性化推荐。核心价值在于:二手商品标准化程度低,用户也很难用关键词准确描述自己想要的物品,靠搜索框很难完成需求匹配。通过用户的行为日志——浏览、收藏、联系卖家、发布物品,构建兴趣模型,再用协同过滤把“同样对这件商品感兴趣的人,也在看那件商品”这种隐含关系捞出来,才真正让系统的信息流活起来。

如果你正在做电商系统、以推荐算法为核心的应用,或者学习协同过滤怎么跟Web项目结合,这篇内容可以直接作为参考。文章从架构拆解开始,再到算法核心代码、数据模型、性能优化,最后是实际踩坑记录,覆盖一个完整项目的落地全过程。

1. 项目全貌与系统设计思路

1.1 二手交易场景下,推荐引擎要解决什么题

二手物品交易和全新商品电商最大的区别有两点。首先,商品是“非标准化”的。一件九成新的二手相机,它的成色、配件、使用频率都会影响买家判断,而普通电商商品的参数体系在这里基本失效。其次是商品生命周期极短,一台手机可能上架一周就卖出去了,不可能像书籍、数码产品那样长期沉淀稳定的“品类标签”。

这些特点决定了二手交易网站的核心矛盾:用户既不知道平台有什么,也不知道自己想要什么,只能不停地翻列表或者依赖搜索。纯靠搜索和分类导航,体验非常局限。推荐模块的价值就在这里——把用户的历史行为跟商品之间建立关联,主动把用户可能感兴趣的东西推到面前。

具体到这个系统,推荐模块要解决的第一个问题,是提高用户的浏览深度。没有推荐的时候,用户进来可能看两三个商品就离开了;有推荐之后,用户可以在“猜你喜欢”和“相似物品”里持续逛下去。第二个问题是提升交易匹配效率。在信息不标准的场景下,兴趣匹配比关键词搜索更容易连接买卖双方。

1.2 整体架构与Python技术栈选型

系统的技术栈以Python为主,Web层使用Django,数据存储用MySQL,缓存和推荐结果的短时存储用Redis。Django的ORM和Admin后台很适合快速搭建二手交易系统的业务骨架,商品发布、用户注册、订单管理这些模块都能快速落地。

推荐模块则没有和业务代码混在一起,而是独立成一个子模块。原因很实际:推荐逻辑是一次性操作大批量数据的计算型任务,与普通的Web请求在资源占用和运行节奏上完全不一样。如果像普通业务代码那样实时同步计算,用户每次打开首页都去跑一遍相似度计算,数据库和计算节点的压力根本兜不住。所以推荐模块只负责两件事,一是定时离线计算所有物品的相似度,把结果写入Redis和MySQL的相似物品表;二是提供查询接口,在Web请求进来时直接查已经算好的推荐结果。

这种“离线计算+在线读取”的方式虽然不算那么实时,但对于大多数二手交易场景来说足够使用了,而且工程上最稳定、最容易维护。

1.3 为什么选协同过滤,而不是内容推荐

刚做技术方案的时候,我也考虑过基于内容的推荐。比如给每个商品打上标签,然后按用户偏好标签去匹配。但二手商品的标题描述随意性高,标签体系的维护成本极大。一个卖家可能写“自用手机便宜出”,另一个卖家可能写“苹果手机几乎全新”,内容特征本身就不稳定,基于内容的推荐效果不会好。

协同过滤的思路恰恰不需要理解内容。它只关注用户和物品之间的交互行为,只要记录了大量买卖双方的浏览、收藏、联系记录,就能通过行为模式计算物品之间的相似度。这种推荐方式的优势在于完全跳过内容理解的困难,直接利用群体的行为一致性,发现人工规则难以发现的关联。比如“看过单反相机的人,往往也在看摄影背包”,这种关联靠商品标签很难挖出来,但协同过滤天然就能发现。

用户行为数据是这套系统的燃料。所以在项目初期,行为日志的埋点设计就不是可选项,而是一开始就要纳入核心架构的重点。

2. 协同过滤算法:原理拆解与落地细节

2.1 选UserCF还是ItemCF,二手场景要分清

协同过滤算法分UserCF(基于用户的协同过滤)和ItemCF(基于物品的协同过滤)两条路线。UserCF先找到与你兴趣相似的用户,再把这些用户浏览过但你没看过的物品推荐给你。ItemCF则反过来,先找到与你历史喜欢的物品相似的物品,再推荐给你。

对比项UserCFItemCF
计算思路找相似用户,推荐其喜欢的物品找相似物品,推荐相似的物品
适用场景用户数量相对少、物品变化快的场景物品数量相对稳定、兴趣变化慢的场景
推荐理由体现群体兴趣,偶然性更强能清楚解释“因为你看了XX”
实时性用户状态变化频繁,更新成本高物品关系相对稳定,更适合离线更新
二手交易适配用户数量大、用户偏好瞬变,不适合物品虽流动性高,但同类物品有稳定关联,较合适

在二手交易网站这种用户规模远大于物品规模的场景下,UserCF每来一个新行为都可能影响相似用户集合,计算量爆炸,而且二手交易的兴趣变化极快。反观ItemCF,物品之间的相似关系相对稳定,而且“因为你看过XX,所以推荐相似的XX”这种推荐逻辑更容易让用户理解。所以系统最终选的是ItemCF为主体,再用热门物品做兜底。

2.2 没有评分的时候,怎么构建兴趣矩阵

协同过滤的经典算法依赖用户对物品的评分矩阵。但二手交易网站基本没有“评分”这个动作,用户对物品的反馈只能从行为日志里获取。这就需要一个评分映射策略,把不同权重、不同发生时间的行为折算成一个可比较的分数。

我用一个加权行为评分策略:浏览记录计1分,收藏计2分,发起联系(比如点击“我想要”或查看卖家联系方式)计3分,最终完成交易计4分。评分代表的是“用户对这件物品的潜在兴趣强度”。还需要对分数做时间衰减处理,因为一周前的收藏和昨天的收藏,代表的兴趣强烈程度完全不同。这里对每个行为乘一个时间衰减系数。

时间衰减采取指数衰减方式。假设当前时间为current_time,行为发生时间为action_time,衰减系数alpha设为0.5/天,权重计算如:

score = base_weight * exp(-alpha * (current_time - action_time).days)

这样两天前的一次收藏(基础权重2)按衰减后大约是2 * exp(-1),也就是0.73分;而昨天的收藏还有1.2分左右。这个衰减逻辑,让近期的行为在推荐中占据更主导的位置,旧行为逐渐退出影响,贴合二手物品流转快的特性。

2.3 相似度计算:余弦相似度的落地细节

ItemCF的核心是计算物品之间的相似度,这里用的是余弦相似度。有两个物品A和B,把同时对它们产生过行为的用户集合取出来,构成两个向量,然后计算向量间的余弦距离。公式表达如下:

sim(A, B) = (向量A · 向量B) / (|向量A| × |向量B|)

在代码实现时,我并不直接构造一个完整的用户-物品矩阵,那样内存消耗太大,尤其是用户量上来以后,这个矩阵稀疏得可怕。真正的主流的做法是:用字典结构存储每个物品对应的“感兴趣的用户及其分数集合”,然后通过共现用户数量计算相似度。

用实际代码表示就是:

import math from collections import defaultdict def compute_item_similarity(action_data): # action_data: {item_id: {user_id: score}} user_items = defaultdict(dict) # user -> {item: score} item_users = defaultdict(dict) # item -> {user: score} for item_id, users in action_data.items(): for user_id, score in users.items(): item_users[item_id][user_id] = score user_items[user_id][item_id] = score # 共现矩阵 co_occur = defaultdict(lambda: defaultdict(float)) item_norm = defaultdict(float) for item_id, users in item_users.items(): user_score_list = [(u, s) for u, s in users.items()] item_norm[item_id] = math.sqrt(sum(s * s for _, s in user_score_list)) for i in range(len(user_score_list)): u1, s1 = user_score_list[i] for j in range(i + 1, len(user_score_list)): u2, s2 = user_score_list[j] co_occur[item_id][u1] += s1 * s2 co_occur[item_id][u2] += s1 * s2 sim_matrix = defaultdict(dict) for item_id, user_weights in co_occur.items(): for user_id, weight in user_weights.items(): denom = item_norm[item_id] * item_norm[user_id] if denom > 0: sim_matrix[item_id][user_id] = weight / denom return sim_matrix

这里有个简化处理需要注意:co_occur里的键既是物品又是用户,所以计算分母时需要根据当前循环对象确定是物品还是用户。实际项目中建议拆成两个矩阵分别存物品与物品、用户与用户,避免混淆。这个代码是为了讲清逻辑的高度简化版,真正的工程项目里建议直接用物品-用户倒排表来生成物品共现矩阵,再合并归一化。

相似度算完之后,对每个物品只保留TopN(一般取10到20)个最相似的物品,存入Redis和MySQL,作为后续推荐查询的基础。

2.4 推荐列表的生成逻辑

有了物品相似度矩阵后,为某个用户生成推荐列表就非常简单了。把该用户最近有过正向行为的物品作为“种子物品”,把它们最相似的物品汇总,再排除掉用户已经交互过的物品,其余按分数降序排列就是推荐结果。

推荐分数公式为:

prediction_score = sum(seed_item的活跃分数 × 该seed_item与候选物品的相似度)

在代码中一个可能的实现如下:

def generate_recommendations(user_seed_items, sim_matrix, exclude_items, topk=20): scores = defaultdict(float) for seed_item, seed_score in user_seed_items.items(): if seed_item not in sim_matrix: continue for candidate, sim_score in sim_matrix[seed_item].items(): if candidate in exclude_items: continue scores[candidate] += seed_score * sim_score ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) return [item_id for item_id, score in ranked[:topk]]

这个流程看起来简单,但有两个坑会在实战中反复出现。第一个坑是,如果用户最近只产生过一两次行为,种子物品太少,候选推荐结果会非常有限。这时候需要用“热门商品补位”机制:当推荐结果不足TopN时,用全局热门商品补足,避免首页冷场。第二个坑是商品可能已经被卖家下架或售出,推荐列表生成时要把这些不可售状态过滤掉,否则用户点进去是个失效链接,体验很差。

3. 系统核心模块实现与关键代码

3.1 数据库模型设计与相似度表存储

推荐引擎落地的第一步是设计好数据模型。系统的核心表有这几张:用户表、商品表、行为日志表、物品相似度表。

商品表主要保存发布者的用户ID、标题、描述、价格、成色、图片地址、状态(在售、下架、已售)。行为日志表直接决定推荐质量,设计成自增主键加用户ID、物品ID、行为类型(view/favorite/contact/deal)、行为时间四个核心字段。

相似度表的设计则有讲究。不能在查询推荐结果时现场算相似度,而是把离线计算好的结果存起来。我用的物品相似度表包含主键、物品ID、相似物品ID、相似度分数、更新时间几个字段,并对物品ID加索引。查询一个物品的相似物品时,直接按物品ID查这张表就可以了。

这里有个实际工程经验:相似度表没必要存全部两两相似关系,只保留每个物品最相似的TopN就行。因为推荐时根本用不到第21个之后的相似物品,存得越多只是白白增加存储压力。

3.2 推荐引擎的定时更新与增量策略

全量更新在数据量不大时很简单,每月甚至每周跑一次都行。但二手物品交易系统的数据会随着时间增长,而且行为日志增加得很快,固定时间全量更新会导致推荐结果跟不上近期用户行为的变化。所以最终选了折中方案:每天凌晨运行一次全量更新,全量更新结束后,再实时叠加当天的增量行为。

增量更新的思路不复杂。每天记录当天的行为日志,把这些日志转换成类似全量数据的格式,只计算这些新行为涉及的物品的相似度,然后在Redis里把对应的物品相似度列表做合并。这里不需要完全精确,先把推荐效果做出来,收敛速度的问题以后再优化。

代码上我用了一个简单的定时任务调度:

def update_recommend(): logger.info("start full update at %s", datetime.now()) behaviors = load_all_behaviors() item_sim_matrix = compute_item_similarity(behaviors) save_to_mysql(item_sim_matrix) save_to_redis_cached(item_sim_matrix) logger.info("full update finished, item count: %s", len(item_sim_matrix))

增量部分则是获取当天的行为数据,只对行为涉及到的物品重新计算,并和Redis中已有的相似度列表进行加权合并。注意合并时相似度要按item_id维度分别处理,不然数据会越来越乱。

3.3 业务模块如何对接推荐结果

推荐结果不能只停留在计算阶段,需要真正出现在Web页面上。在这个系统里,推荐模块以两种方式暴露给前端:首页的“猜你喜欢”接口和商品详情页的“相似物品推荐”接口。

首页推荐接口的大致工作流是:从Redis缓存中查询当前用户的推荐物品ID列表;不足时用热门物品补位;再联查商品表,过滤掉已下架、已售出的物品;返回最终的商品信息列表。

详情页的“相似物品”则更简单直接,用当前查看的物品ID去查相似度表,取出TopN相似物品,过滤后展示。这个模块是独立于用户行为的,任何用户看到的相似物品都是一样的,但正是这种推荐,让用户在浏览单个商品时有机会不断发现新的相关信息,从而延长停留。

3.4 推荐结果缓存如何设计

Web请求最怕的是每次都去MySQL里翻相似度表和商品表,尤其首页这种热点页面,用户同时进入时很可能把数据库连接打满。推荐结果一定要加缓存。

系统用了两级缓存。第一级是Redis,key设计成按用户维度区分,比如rec:guess:{user_id},value直接存物品ID列表JSON,TTL设置为一小时。第二级是热门推荐列表,比如rec:hot,用全局商品ID列表,TTL设置成十分钟。详情页相似物品也有独立的缓存,key设为rec:similar:{item_id},TTL设置成三小时。

在用户行为发生变化时(比如收藏了一个新物品),可以直接删除该用户的推荐缓存,让用户下次访问时生成新推荐。这样既保证了数据新鲜度,又避免了每次请求都去做复杂计算。

4. 实战中的常见问题与排查方法

4.1 数据稀疏,推荐质量差怎么办

协同过滤最怕的就是数据稀疏。新平台刚上线那会儿,行为日志里的用户和物品交互非常少,很多物品只有一两次浏览,算出来的相似度矩阵异常稀疏。相似度表里能查到的相似物品很少,推荐列表经常补位补成热门榜,等于没有个性化。

这种情况下不要硬上算法,先做规则兜底。我给系统加了三层推荐策略:第一层是个性化协同过滤;第二层是同分类下的热门商品;第三层是全局热门商品。前一层推荐数不足时,再用下一层补齐。等行为数据积累到一定量级后,个性化推荐的占比自然会慢慢变大。

数据采集阶段也可以做一些主动引导。比如在新手用户注册后,引导用户选择感兴趣的物品分类,用这个“人工标注兴趣”补充冷启动数据和行为矩阵。虽然这种数据和真实行为相比有偏差,但至少缓解了前期的稀疏问题。

4.2 冷启动:新用户与新商品怎么推

新用户的推荐本身就是无从下手的,因为没有任何行为记录。这里采用的思路是:在前端第一次加载推荐接口时,如果发现该用户没有任何行为种子,就直接返回热门商品推荐。同时在商品详情页的相似物品模块里,如果遇到一个几乎没有行为的新商品,相似度表查不到任何记录,就用它所在分类下的热门商品作为替代推荐。

热门商品怎么定义也需要设计好。这里用的是“近7天浏览次数 + 收藏次数加权”的方式,计算出一个热力值,然后定期更新。有了热门池,冷启动问题就算不能完全解决,也不至于让新用户进来面对一个空荡荡的首页。

4.3 相似度矩阵与内存性能问题

物品数量上来后,相似度矩阵的维度和内存消耗会快速上升。如果商品有5万条,两两相似度矩阵的存储规模是5万乘5万,光存浮点数就能耗掉几个GB内存。这个量级在开发环境还能跑,但生产环境就不行了。

实际项目里用了一招很管用的手段:先做商品过滤。只保留近90天内产生过至少3次行为的商品进入相似度计算,把长期无行为、无曝光的僵尸商品从候选集合里剔除。这样做有两个好处,一是大幅压缩矩阵维度,二是推荐模块真正关注的是活跃商品,算出来的相似度也更可靠。

另外,物品共现矩阵的计算过程本身也需要优化。在线计算时不要去遍历整个用户物品矩阵,而是先把用户行为按天聚合,然后用“以用户为维度构建物品序列”的思路做共现统计。这里有一个更好的实践是:先在SQL层面把行为日志按用户分组、按时间排序,得到每个用户的“行为物品序列”,再用Python处理这个序列来统计物品之间的共现次数。这样比直接从全量矩阵硬算快一个量级。

4.4 推荐结果的可解释性怎么做

协同过滤推荐的一个问题就是“吃的黑箱”感。用户不知道为什么系统给他推荐了这些商品,而可解释推荐能显著提高信任感。这个系统里做了一件简单但有效的事:在推荐结果旁边标注推荐理由,比如“因为你看过【某商品】,所以为你推荐相似物品”。

实现上就是在生成推荐列表时,把每个候选物品命中的种子物品一起保存下来,接口返回时带上推荐理由字段。比如一个候选物品命中了用户上次收藏的“九成新山地车”,推荐理由就是“和你收藏过的九成新山地车相似”。这个细节能有效提升用户对推荐模块的接受度。

4.5 容易忽视的细节坑

有几个细节问题光靠读算法论文发现不了,只有在实际跑系统的时候才会被反复折磨。

第一个坑是物品状态过滤。推荐结果如果不对商品状态做判断,用户点进推荐物品后发现已经下架或已售,跳出率会很高。生成推荐列表时,一定要把不可售状态商品过滤掉,并且要在缓存失效前同步处理,否则同一件商品可能在多个用户首页显示失效链接。

第二个坑是行为日志的埋点不一致。详情页浏览、列表页浏览、搜索结果点击、收藏、联系卖家,这些行为如果埋点字段不统一,后端解析会非常痛苦。所以行为日志的字段在设计当初就要统一成一个标准格式,线上排查问题才能高效。

第三个坑是时区问题。行为日志的时间和推荐计算的时间如果跨时区,时间衰减计算就会出错。最简单的方式是所有日志统一记录成UTC时间,展示层再转本地时区。千万别存在什么“本地时间字符串”之类的,后端一旦跨服务器部署就会乱掉。

第四个坑是热门补位导致的重复问题。如果个性化推荐结果本身已经包含了热门商品,补位时可能把同一件商品重复推到推荐列表里,前端展示会有重复卡片。处理办法是推荐结果合并后统一做一次去重,以物品ID为主键,后合并的优先保留,逻辑不复杂但非常影响观感。

还有一个容易被忽视的点:用户自己发布的商品,绝对不应该出现在自己的推荐列表里。这种错误在开发环境里很难发现,直到真实用户反馈“我自己卖的东西又推荐给我了”才意识到。所以在最终组装推荐列表的时候,要把用户ID对应的已发布商品集合拿出来,统一排除掉。

5. 离线评估与后续优化思路

推荐模块上线后,自然要回答“推荐效果怎么样”的问题。没有评估的推荐就是自娱自乐。系统里做了两个层面的评估。

离线评估用的是准确率和召回率。把行为日志按时间切分,前70%作为训练集,后30%作为测试集。用训练集计算相似度矩阵,再预测测试集用户的潜在物品,看推荐物品中有多少是用户真实发生行为的物品。另一个指标是覆盖率,计算推荐结果覆盖的商品数占总可推荐商品数的比例。覆盖率太低说明推荐结果集中在那几个头部商品上,挖掘长尾信息的能力不够。

在线层面做的是简单地版本对比:一个页面带推荐模块,另一个页面不带推荐模块,观察用户平均浏览深度、停留时长和转化率。开发环境只能单机自测,生产环境上线后做灰度切量即可依赖真实数据对比。

后续迭代可以考虑引入矩阵分解的协同过滤,比如基于隐语义模型的算法。它能部分解决物品相似度矩阵在大规模数据下的稀疏和扩展问题,把用户和物品映射到低维空间进行兴趣匹配。但要注意的是,如果当前的物品相似度表和热门补位策略已经能够支撑业务,就不要盲目上复杂模型,沿用现有架构先把数据积累起来,效果评估模型之后再做替换。

在做项目总结的时候,可以反复提醒自己:推荐系统跑通只是第一步,关键在于整个链路是否都能稳定服务真实用户需求。协同过滤的坑非常隐蔽,冷启动、数据稀疏、性能膨胀、业务规则冲突,每一个实际跑起来都可能让开发陷入泥潭。个人体会是,先把最简单的协同过滤做到极致,比直接上一堆复杂算法靠谱得多。一个能解释逻辑、能快速定位问题、能稳定支撑接口的推荐模块,已经能超过许多停留在演示层面的系统了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 23:14:09

ASP连接数据库完整指南:Access与SQL Server的配置、排错与安全

ASP 连接数据库的方式选择,几乎就是早期动态站点开发的第一道分水岭。我刚开始做网站那会儿,看到别人写的连接代码能跑、自己照着写就报错,后来才发现问题往往不在代码本身,而在连接串、驱动版本和数据库环境这些容易忽略的细节上…

作者头像 李华
网站建设 2026/10/21 23:13:57

模型服务API设计实战:同步异步流式、接口契约与稳定性保障

1. 为什么从笔记本到正式环境,一个函数会变成一堆问题这个系列写到第四篇,我想换个角度,暂时不聊数据管道和特征服务,讲一讲把模型变成 API 的那段路。很多人跟我一样,最早在 Notebook 里验证模型,效果满意…

作者头像 李华
网站建设 2026/10/21 23:13:50

免费开源矢量图形编辑器Inkscape:核心功能与实操指南

如果你和我一样,曾经被商业矢量软件的授权费劝退过,或者只是想在非工作场合用一套顺手、不折腾的工具做点图形设计,那你大概率听过Inkscape这个名字。作为一款免费开源的矢量图形编辑器,Inkscape在设计师圈子里已经活跃了十几年&a…

作者头像 李华
网站建设 2026/10/21 23:13:09

从三个字母Rea到落地代码:模糊需求澄清与选型实战

前段时间接了个挺有意思的活儿,某团队给过来的需求只有三个字母:Rea。正文空白,关键词空白,摘要空白,连这个代号哪来的都说不清楚。刚开始我还以为文件传输出了纰漏,反复确认了几遍,对面回复说&…

作者头像 李华
网站建设 2026/10/21 23:12:30

MySQL与MongoDB选型对比:数据模型、事务与索引实战指南

最近被问到好几次“MySQL和MongoDB到底怎么选”,正好手头有一个项目同时用到了这两种库,从需求分析到上线维护都踩了一遍,想着干脆把对比和实操经验整理出来。这个标题的覆盖范围很广,但它不是让你二选一,而是让你搞清…

作者头像 李华
网站建设 2026/10/21 23:12:24

用MySQL做数据可视化,SQL才是真正的关键

做数据可视化这几年,我最大的一个体会是:坑不在图表,而在SQL。很多同学一上来就打开BI工具拖拖拽拽,结果图表拉出来了,数据一核对全是错的;也有人费了大功夫搭好Dashboard,结果每次刷新要等十几…

作者头像 李华