☰
Django+协同过滤:大学生科技竞赛推荐系统设计与实现
2026/10/20 8:06:50 网站建设 项目流程

每年三月份开始,学校里的竞赛通知就陆续多起来了,数学建模、电子设计、挑战杯、计算机设计大赛、蓝桥杯……信息散落在学院群、辅导员转发、海报栏、教务系统里。想参赛的学生要么刷不到合适的竞赛,要么被淹没在一堆和自己专业方向无关的通知里。这次要分享的项目,就是针对这个场景去做的一个东西:一个基于Python的Django框架、核心使用协同过滤算法的大学生科技竞赛推荐系统,内部代号叫oy4h20w1(就是个项目编号,不花哨)。它解决的就是“竞赛信息过载、学生找竞赛靠刷群、推荐全靠人工判断”这几个老问题。

如果你正准备做类似的校园Web系统,或者想学习怎么把协同过滤算法真正落进一个Web项目里,这篇内容应该能帮你省不少时间。我会把整体设计思路、算法选型细节、Django实现方式,以及我在实际开发中踩过的一些坑,按项目推进的顺序拆开讲。不是说教式的方案文档,就是我做完这个项目之后的一些真实复盘。

1. 项目背景与整体思路拆解

1.1 竞赛推荐要解决的三个真实痛点

先说为什么需要这么一套系统,而不是简单地做一个竞赛信息发布网站。本质上,校园科技竞赛推荐面临的问题和电商推荐非常像:竞赛数量多、学生精力有限、信息匹配效率低。

第一个痛点是信息过载。一个综合性高校一年下来的各类竞赛通知,少则几十条,多则上百条。这些通知分散在不同渠道,学生其实很难主动去获取全量信息。更麻烦的是,很多竞赛的报名窗口只有两三周,学生知道的时候往往已经截止了。

第二个痛点是缺乏个性化。大家都在发同一个通知列表,但不同学生的专业方向、参赛经历、技术水平差得很远。一个电子系的学生和一个经管系的学生,适合的竞赛范围基本没有重叠。如果所有人看到的都是同一份榜单,转化率一定很低。

第三个痛点是热门竞赛的马太效应。如果没有算法干预,通知里总是那几个知名度高的竞赛反复出现,冷门但含金量高的竞赛很难被匹配到合适的人。这就浪费了竞赛资源,也埋没了不少学生。

所以在设计系统的时候,我明确了几条原则:信息集中管理、按学生画像差异化展示竞赛、用行为数据驱动推荐、管理员后台要能方便维护竞赛数据和报名信息。这就是整个项目的核心骨架。

1.2 技术组合为什么是Python + Django + 协同过滤

这个组合看起来偏“课程设计”,但实际上它的选型逻辑是经得起推敲的。

首先说Python。这类推荐系统的核心工作量在数据处理和算法实现上,Python的生态优势非常明显。Pandas处理矩阵、Numpy做向量计算、Scikit-learn直接提供余弦相似度函数,十分钟就能把推荐算法原型跑起来。如果用Java写,光是把相似度矩阵的代码调通就得花不少时间。对于竞赛推荐这个场景,数据量级就是几千人、几百个竞赛,完全不需要引入Spark、Flink这种重型计算框架,Python单机处理绰绰有余。

然后是Django。我见过不少项目把前端Vue、后端Flask、数据库MySQL、缓存Redis全拆开,看起来挺工业化的,但实际开发和维护成本对个人项目来说偏高。Django的优势在于“一体集成”:User认证模块自带登录注册,Admin后台直接生成对应数据模型的增删改查界面,ORM把数据库操作封装得很干净。竞赛管理的后台基本不用额外开发,这能省出二三十个小时的工作量,把精力聚焦到推荐算法和前端体验上。

最后是协同过滤。有人可能会问,现在推荐系统不都搞深度学习了吗,怎么还在用协同过滤?原因很简单:竞赛推荐系统的用户行为数据非常稀疏,一个学生一学期最多产生几十条有效行为记录,这点数据喂给神经网络基本学不出来。协同过滤在这个数据量级下效果稳、可解释性强、计算开销低,是性价比最高的选择。哪怕是后期换更强的模型,先跑通协同过滤,也是在给后续算法打数据基础。

1.3 系统功能模块的合理边界划分

做Web系统最忌讳一上来就堆功能,把系统做成一个大杂烩。我在做需求拆解的时候,把系统分成了三个功能域。

域名是学生端基础功能,包括注册登录、修改个人资料、浏览竞赛列表、查看竞赛详情、搜索竞赛、收藏竞赛和在线报名。这些功能负责最基础的用户交互和信息展示,是行为数据的来源。

第二个域是推荐服务,这是项目的核心。它负责读取用户的历史行为,离线或实时地计算推荐结果,在学生端首页展示“为你推荐”列表、在竞赛详情页展示“同类竞赛推荐”,同时处理新用户和新竞赛的冷启动兜底。

第三个域是管理后台,基于Django Admin实现。管理员录入竞赛信息、调整竞赛状态、查看报名名单、导出报名统计表格。这个后台看着不起眼,但实际使用频率最高,竞赛负责老师要天天用。

这三个域之间的数据流是单向清晰的:学生在前端产生行为数据,行为数据沉淀到数据库,推荐算法读取行为数据产出推荐列表,推荐列表回写到缓存供前端读取。整体闭环简单,但每个环节都值得抠细节,后面展开讲。

2. 协同过滤算法的选型与落地细节

2.1 UserCF和ItemCF在竞赛场景下怎么选

协同过滤有两条经典路线:基于用户的协同过滤(UserCF)和基于物品的协同过滤(ItemCF)。两条路线的数学逻辑完全对偶,但适用场景差别很大。

UserCF的核心逻辑是“找到和你相似的人,推荐他们喜欢的物品”。假设A同学和B同学都浏览了数学建模、都收藏了电子设计大赛,系统就会认为A和B兴趣相似,然后B报名的挑战杯就会被推荐给A。

ItemCF的核心逻辑是“找到和你喜欢物品相似的物品,推荐给你”。逻辑是:如果很多人同时关注了数学建模和计算机设计大赛,这两个竞赛就被认定为相似竞赛,那么收藏了数学建模的学生,系统会给他推荐计算机设计大赛。

在竞赛推荐这个场景里,我最终选了UserCF作为主力算法。原因有三个:

第一,竞赛场景的用户数量级远小于物品数量级,一个学院几千个学生、几十上百个竞赛,基于用户计算相似度矩阵的代价更小。

第二,学生的竞赛兴趣往往横跨多个类别。UserCF更容易跨类别挖掘兴趣,ItemCF则倾向于“同类推荐”,容易把推荐结果限制在单一赛道里。

第三,学生之间的“圈层效应”明显。同一实验室、同一专业、同一社团的学生,参赛偏好高度一致。UserCF本质上就是在利用群体的隐性兴趣,跟这个场景的气质很搭。

但这不代表ItemCF没用。我在竞赛详情页单独放了一个“相似竞赛推荐”的模块,用的就是ItemCF思路。用户在查看某个竞赛时,会看到与它高度相关的其他竞赛,这在提升浏览深度上效果比UserCF好。所以我的建议是:两者不冲突,主推UserCF,辅助用ItemCF,结合起来用。

2.2 用户-竞赛评分矩阵的构建与行为权重设计

协同过滤的第一步,是把用户行为转换成评分矩阵。矩阵的行是学生,列是竞赛,单元格的值代表学生对竞赛的兴趣程度。

这里有一个关键决策:显式评分还是隐式反馈。我在系统里没有做“给竞赛打分”的功能,因为让用户主动打分在产品上不现实,参与率极低。所以方案采用了隐式反馈,就是收集用户自然产生的行为数据:浏览、收藏、报名。

行为类型不同,代表的需求强度完全不同。我设置了一套权重规则:

行为类型权重值设计理由
浏览详情页0.5只表示最低限度的兴趣,噪音较大
收藏竞赛1.0表达明确兴趣,说明学生有持续关注意愿
报名参赛3.0最强正向信号,代表确定的需求

为什么报名权重是3而不是5?我试过5,结果发现只要学生报过一次名,推荐列表会被同类型竞赛塞满,多样性很差。3这个值是调参调出来的,强度够,又不至于湮没其他行为的贡献。

有了行为权重之后,矩阵的构建就简单了:遍历学生的行为记录,把对应行为权重累加到矩阵单元格里。如果一个学生既浏览又报名了同一个竞赛,得分就是0.5 + 3.0 = 3.5。

但是这里有个稀疏性问题。一个学生一学期可能就产生了一二十条行为记录,而竞赛总数有上百个,矩阵稀疏度会超过90%。稀疏矩阵带来的问题是相似度计算不准确和人冷启动困难,这个我后面专门讲排查技巧。

2.3 相似度计算原理解读:余弦公式和它的取舍

相似度计算是协同过滤的心脏。我选择的是余弦相似度,原因值得展开说。

余弦相似度的公式长这样:两个向量A和B的余弦值等于A和B的点积除以各自模长的乘积,即sim(A,B) = (A·B) / (|A| × |B|)。放在推荐场景里,向量A就是A学生对所有竞赛的评分分布,向量B同理。余弦相似度衡量的是两个向量在方向上的一致性,而不是数值大小。

可以这样理解:一个学生点了20次鼠标产生的评分向量,和另一个学生点了40次产生的向量,余弦相似度只看兴趣方向是否一致,不看绝对数值。这非常适合行为数据,因为每个人的活跃度和使用频率不同,直接比绝对值没有意义。

为什么不选皮尔逊相关系数?皮尔逊需要对每个向量做中心化处理,把评分减去用户平均分。在评分矩阵非常稀疏的时候,很多用户的平均分会被大量零值拉低,中心化的效果会很奇怪,计算开销也更大。而Jaccard相似度只看两个用户是否有交集,忽略了行为的差异权重,比较粗糙。所以我最终一共测试了三种相似度:余弦、皮尔逊、Jaccard,离线数据上余弦的推荐效果综合最好。

代码实现我放在第3部分,这里先把思路讲清楚:构造矩阵、算相似度、找近邻、近邻未评过分的物品加权评分。推荐就是这么一条线走下来的。

2.4 冷启动问题的兜底策略

协同过滤的天生短板是冷启动,项目里必须专门处理,否则新用户和新竞赛会一直拿不到推荐。

新用户的处理方案是分层兜底。完全没有行为数据的学生,系统采用“热度推荐+基础画像过滤”策略:按浏览量、报名人数和时间衰减因子计算一个热度分,再按学生的年级和专业方向做粗粒度过滤。比如一个计算机系的新生,系统给他推荐编程类竞赛和入门级竞赛,因为这类竞赛对新手更友好。等学生产生了第一次浏览或收藏行为,立即触发一次基于单条行为的轻量推荐,不用等到行为积累很久。

新竞赛的处理方案是新竞赛曝光加权。新录入的竞赛在推荐列表里会被人工加上一个时间收益系数,通常刚上线的前两周乘以1.5的权重参与排序。这样保证新竞赛有机会被看到,不然协同过滤永远倾向于给老竞赛导流量。

冷启动这块我建议不要在算法本身上抠太深,规则兜底更实用。校园场景的推荐系统,用户量级不大,规则能覆盖90%的冷启动问题,剩下的交给时间。

3. 基于Django的核心实现过程

3.1 数据模型设计:三张核心表清理数据流

Django里写推荐系统,第一步是把数据模型设计好。我建了三张核心表,模型代码大致是这样的:

from django.db import models from django.contrib.auth.models import User class Competition(models.Model): """竞赛信息表""" title = models.CharField(max_length=200, verbose_name='竞赛名称') category = models.CharField(max_length=50, verbose_name='竞赛类别', db_index=True) level = models.CharField(max_length=20, choices=( ('school', '校级'), ('province', '省级'), ('national', '国家级') ), verbose_name='竞赛级别') organizer = models.CharField(max_length=100, verbose_name='主办单位') start_date = models.DateField(verbose_name='报名开始时间') end_date = models.DateField(verbose_name='报名截止时间') description = models.TextField(verbose_name='竞赛简介') created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = '竞赛信息' ordering = ['-created_at'] class Behavior(models.Model): """用户行为表:协同过滤的数据来源""" user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='学生') competition = models.ForeignKey(Competition, on_delete=models.CASCADE, verbose_name='竞赛') action = models.CharField(max_length=20, choices=( ('view', '浏览'), ('favorite', '收藏'), ('register', '报名') ), verbose_name='行为类型') weight = models.FloatField(default=0.5, verbose_name='行为权重') created_at = models.DateTimeField(auto_now_add=True, verbose_name='发生时间') class Meta: verbose_name = '用户行为记录' indexes = [ models.Index(fields=['user', 'competition']), models.Index(fields=['competition']), ] class RecommendedCompetition(models.Model): """推荐结果缓存表,避免每次都实时算协同过滤""" user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='学生') competition = models.ForeignKey(Competition, on_delete=models.CASCADE, verbose_name='竞赛') score = models.FloatField(verbose_name='推荐得分') reason_type = models.CharField(max_length=20, default='cf', verbose_name='推荐来源') is_active = models.BooleanField(default=True, verbose_name='是否生效') created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = '推荐结果缓存' ordering = ['-score']

为什么要单建一张行为表,而不是在报名表上加字段?因为行为表除了记录报名,还要记录浏览和收藏,这些数据对推荐算法来说是原料。Data为王,没有这张表,协同过滤就是无米之炊。而行为表的索引设计也很重要,因为算法要按用户查记录、按竞赛查记录,这两个维度的查询频率都很高。

推荐结果缓存表是实践中加上去的。一旦用户数据量上来,每次访问首页都现算矩阵是不现实的,所以每天定时算好,第二天直接读缓存。这张表对性能的改善是非常明显的。

3.2 协同过滤算法核心代码实现

算法模块我单独放在一个django app里,叫recommend,核心代码写在algorithm.py中。实现代码如下:

# recommend/algorithm.py import numpy as np import pandas as pd from sklearn.metrics.pairwise import cosine_similarity def build_user_item_matrix(behaviors, user_ids, competition_ids): """ 构建用户-竞赛评分矩阵 behaviors: 行为表查询集 user_ids: 所有用户ID列表 competition_ids: 所有竞赛ID列表 """ matrix = pd.DataFrame(0.0, index=user_ids, columns=competition_ids) for b in behaviors.iterator(): matrix.loc[b.user_id, b.competition_id] += b.weight matrix = matrix.replace(0, np.nan) return matrix.fillna(0) def user_based_cf(matrix, target_user_id, top_n=10): """ 基于用户的协同过滤 1. 计算目标用户和其他用户的余弦相似度 2. 找到目标用户未交互过的竞赛 3. 按相似用户对该竞赛的评分加权汇总 """ if target_user_id not in matrix.index: return [] # 计算用户相似度矩阵 sim_matrix = cosine_similarity(matrix, matrix) sim_df = pd.DataFrame(sim_matrix, index=matrix.index, columns=matrix.index) target_row = matrix.loc[target_user_id] # 目标用户尚未产生行为的竞赛 unrated = target_row[target_row == 0].index.tolist() scores = {} for comp in unrated: # 找到评价过该竞赛的用户 rated_users = matrix.index[matrix[comp] > 0].tolist() if not rated_users: continue # 取目标用户与这些用户的相似度 sims = sim_df.loc[target_user_id, rated_users] # 该竞赛在这些用户中的评分 ratings = matrix.loc[rated_users, comp] # 加权打分:相似度乘以评分的加权平均值 denom = sims.sum() if denom == 0: continue score = (sims * ratings).sum() / denom scores[comp] = score if not scores: return [] ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_n] return [(comp_id, round(score, 4)) for comp_id, score in ranked] def item_based_cf(matrix, target_user_id, interacted_competitions, top_n=10): """ 基于物品的协同过滤,用于竞赛详情页的相似竞赛推荐 """ sim_matrix = cosine_similarity(matrix.T, matrix.T) sim_df = pd.DataFrame(sim_matrix, index=matrix.columns, columns=matrix.columns) scores = {} for comp in matrix.columns: if comp in interacted_competitions: continue base_comps = [c for c in interacted_competitions if c in sim_df.columns] if not base_comps: scores[comp] = 0 continue scores[comp] = sim_df.loc[comp, base_comps].mean() ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_n] return [(comp_id, round(score, 4)) for comp_id, score in ranked]

几个实现细节值得说明。

第一,build_user_item_matrix里有一个小技巧:先用NaN替换0,再fillna(0)。这样做的目的是为了后续扩展:如果后期要区分“缺失值”和“真实的0分”,数据结构已经预留了口子。

第二,预测分数的公式是加权平均,不是简单的加权和。加权和的问题在于相似用户多的竞赛天然得分高,结果会偏向热门竞赛;加权平均能抵消用户数量差异带来的偏差,推荐结果更公平。

第三,cosine_similarity一次性算完整个矩阵的相似度,内存占用是O(n²)。在几百到几千用户量级没问题,但如果数据量真涨到几万用户,这一步就要优化,按批次计算或者改用Spark。

3.3 推荐服务与Django视图的集成方式

算法写好了,怎么融入到Django项目里?我采用了一个独立的服务层,不直接在views.py里写算法逻辑。

服务层的逻辑是这样的:

# recommend/services.py from django.utils import timezone from .models import RecommendedCompetition from .algorithm import build_user_item_matrix, user_based_cf def refresh_user_recommendation(user_id, user_ids=None, competition_ids=None): """ 重新计算某个用户的推荐列表并写缓存表 """ # 查询行为数据 from competition.models import Behavior, Competition behaviors = Behavior.objects.filter(created_at__gte=timezone.now() - timezone.timedelta(days=180)) if user_ids is None: user_ids = list(Behavior.objects.values_list('user_id', flat=True).distinct()) if competition_ids is None: competition_ids = list(Competition.objects.values_list('id', flat=True)) matrix = build_user_item_matrix(behaviors, user_ids, competition_ids) result = user_based_cf(matrix, user_id, top_n=10) # 清空旧推荐 RecommendedCompetition.objects.filter(user_id=user_id).delete() # 写入新推荐 for comp_id, score in result: if not Competition.objects.filter(id=comp_id).exists(): continue RecommendedCompetition.objects.create( user_id=user_id, competition_id=comp_id, score=score, reason_type='cf' ) def get_recommendations_for_user(user): """ 给视图层调用的接口,优先读缓存,缓存未命中再实时计算 """ cached = RecommendedCompetition.objects.filter(user=user, is_active=True).select_related('competition') if cached.exists(): return [item.competition for item in cached[:10]] refresh_user_recommendation(user.id) return [item.competition for item in RecommendedCompetition.objects.filter(user=user).select_related('competition')[:10]]

然后views.py里的写法就非常清爽:

# recommend/views.py from django.shortcuts import render from .services import get_recommendations_for_user def home(request): user = request.user if user.is_authenticated: rec_competitions = get_recommendations_for_user(user) else: rec_competitions = Competition.objects.order_by('-created_at')[:10] context = { 'recommendations': rec_competitions, } return render(request, 'recommend/home.html', context)

这样分层的好处很明显:views只处理HTTP相关内容,services负责业务编排,algorithm纯做计算。以后换算法实现、加缓存机制、改数据存储,都只动对应层,不会牵连其他代码。

3.4 性能优化:缓存策略与异步重算

协同过滤的实时计算在大学校园这种规模下本身不慢,几百个学生的相似度矩阵算一下就是毫秒级。但有几类操作会让用户明显感受到卡顿:第一次访问推荐页但缓存没有命中、管理员维护竞赛后触发全量重算、行为表数据量增长后每次查询变慢。

我采用的优化方案是“定期全量预计算+实时增量兜底”,具体分三层:

第一层,每天凌晨用Celery定时任务,对活跃用户批量重算推荐结果。Django里用Celery比较成熟,写一个task装饰的函数,在beat schedule里配上cron表达式(比如每天凌晨3点跑一次),遍历活跃用户列表,逐个调用refresh_user_recommendation。

第二层,用户访问推荐页时优先读Redis缓存。Django手动设置Redis缓存的代码很简单,key设计成类似rec:{user_id}:v1,value存竞赛ID列表JSON。缓存命中直接返回,不查数据库。

第三层,当用户产生了一次关键行为(收藏或报名)后,实时触发该用户的推荐重算。这样用户会感觉推荐结果“越来越懂我”,体验提升很明显。重算用异步任务,不要直接在请求里同步等待。

有一个细节需要注意:清理和重建缓存数据时,先把旧的推荐结果复制到一个临时集合,再删除旧记录。直接delete再create会造成短暂的空窗期,用户访问时会返回空推荐报告。我踩过这个坑,当时就是直接把Recommendation表清空了,结果早起的用户看到了大半个页面的空列表。

4. 常见问题与排查技巧实录

4.1 行为数据太少,推荐结果不稳定怎么办

现象是:推荐列表里总是那两三个热门竞赛,换一个用户结果几乎一模一样,算法看起来像是“坏了”。

排查思路顺着数据分析走。我当时的做法是写了一个统计脚本,导出所有行为记录,按用户维度统计行为数。结果发现80%的用户行为数少于5条。这个数据量下,任何协同过滤算法都算不出个性化差异,推荐结果自然趋同。

解决方法是多管齐下。第一,降低行为权重的起步门槛,把详情页的浏览行为也纳入统计,扩大行为数据来源。第二,在前端产品上引导用户表达偏好:注册时让用户勾选感兴趣的竞赛方向(比如算法、硬件、创新创业、人文社科),这些偏好作为特征向量叠加到评分矩阵里,相当于把显式和隐式反馈结合起来。第三,设置一个保留规则:如果某个用户的行为数小于3条,直接用热度推荐,不进入协同过滤流程。

4.2 新用户和新竞赛的冷启动问题

新用户没有历史行为,新竞赛没有用户交互,这两类问题在系统上线初期尤其明显。

新用户的问题,我在算法层面用了“兴趣标签兜底”,上线后效果不错。具体做法是在注册流程里增加竞赛方向勾选,用户选择后立刻能产生一个初始画像。推荐页的逻辑是:先用标签匹配出候选竞赛池,再结合竞赛热度分排序。这样新用户第一次访问推荐页,看到的已经是符合自己方向的竞赛,而不是一个空白页面。

新竞赛的问题,我的做法是“探索率机制”。在最终推荐列表的10个位置里,固定预留2个位置给最近上线的竞赛。推荐结果的排序权重里也加了时间衰减因子:距离报名截止日越近的竞赛,权重越高。这两个手段保证了新竞赛在上线初期就能获得曝光机会,帮助算法快速积累它的行为数据。

4.3 推荐结果越来越单一,缺少多样性

协同过滤有一个通病:内容收敛。用户收藏了几个竞赛之后,推荐结果越来越集中在同一类,最后全是同质化内容。这在竞赛场景里很要命,因为学生参加竞赛是分阶段的:大一参加入门赛,大二可能转向学科竞赛,大三开始琢磨创新创业。如果推荐系统只盯着历史行为,就会一直给学生推旧赛道的竞赛。

我的解决方案是给推荐列表增加“类别打散”逻辑。具体做法是:算法算出的TopN结果先按竞赛类别分组,每个类别最多允许出现2个竞赛,再按分数排序穿插展示。同时,在榜单底部固定增加一个“其他方向的热门竞赛”模块,主动向用户推荐其类别之外的竞赛,刺激多元化兴趣的发现。

效果最明显的一次优化是提高了收藏行为的权重(从1.0调到2.0)。因为收藏比浏览更能反映真实兴趣,调高后推荐结果和用户主动搜索的关键词重合度提升了不少。

4.4 同步计算导致页面卡顿和数据库压力过大

系统上线一段时间后,行为表里积累了几十万条记录,推荐计算开始出现性能问题。最明显的现象是:用户第一次访问推荐页时,页面加载要3到5秒,因为算法现算矩阵;管理员刷新管理后台时,数据库连接池容易被占满。

排查发现两个根因。第一,build_user_item_matrix每次都要全量查行为表,几十万条记录全部从MySQL读出来做矩阵,代价太高。第二,相似度矩阵的计算没有缓存,频繁重复计算。

优化的核心思路是“削峰填谷”:把勺计算高峰转移到夜间。我写了一个每日定时任务,专门负责全量计算和缓存刷新。用户访问页面时,只读缓存表。同时,对行为查询增加了时间范围过滤,只加载最近180天的行为数据,因为太久远的行为对推荐的意义不大。这两个改动之后,推荐页接口的响应时间稳定在200毫秒以内,数据库负载也降了下来。

4.5 推荐效果怎么评估:离线指标与在线反馈

做推荐系统一定要有评估机制,不然优化方向全凭感觉。我在这套系统里主要做了两个层面的评估。

离线评估用的是“时间切分法”:把行为数据按时间排序,前80%作为训练集,后20%作为测试集。算法在训练集上计算推荐列表,看测试集中用户实际交互过的竞赛是否出现在推荐结果里,计算Precision@K和Recall@K两个指标。K我取的10,也就是推荐列表长度。这个评估方式能直观看到算法改动带来的效果变化。

在线评估的指标是推荐位点击率(CTR)和报名转化率。CTR是推荐列表里被点击的竞赛数量除以推荐总数量,转化率统计的是推荐列表中的竞赛最终被报名的比例。系统维护两组统计:一组记录从“推荐位”进入详情页的行为,另一组记录从其他位进入的行为。对比之后的结论很直观:推荐位的报名转化率是普通位入口的1.5倍左右,说明算法确实在起作用。

4.6 后续可以扩展的升级方向

如果这个系统要继续往下做,有几个值得投入的方向。

第一,算法层面可以从协同过滤升级到矩阵分解或混合推荐。用Surprise库或者LightFM引入隐因子模型,能更好地处理稀疏矩阵,预测学生-竞赛的潜在匹配度。

第二,引入更丰富的特征。比如学生的年级、专业、历史参赛成绩、竞赛的含金量、指导教师方向,这些都可以做成特征向量喂给模型。协同过滤只用了行为数据,特征增强会带来明显提升。

第三,做分发和运营层面的功能。比如竞赛报名截止前自动站内信提醒、同专业学生参赛竞赛的热榜推送、竞赛结束后的成绩公开展示与经验沉淀。这些虽然不直接影响推荐效果,但对整个系统的留存和活跃度有明显的帮助作用。

我个人在实际操作中的体会是:这类系统的技术门槛其实不高,真正的难点在数据采集和应用场景的匹配。很多团队做推荐系统,把精力放在调算法上,反而忽略了行为记录的质量。这个项目做下来,我最深的感受是:先把行为数据采集做规范(什么行为记什么权、哪些行为值得记),推荐结果就会自动变好,比反复调参管用得多。最后再分享一个小建议,如果你也要做类似的Django推荐系统,千万别等项目写完再补行为日志表,一定在数据库设计的第一天就把记录逻辑定清楚,不然后面每次改字段都是一次痛苦的迁移。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询