☰
豆瓣电影用户画像推荐系统:混合协同过滤与内容推荐实战
2026/10/12 20:35:17 网站建设 项目流程

简介:这是一套面向高校计算机相关专业学生的Python毕业设计/课程设计资源,主题为基于混合推荐算法的用户画像推荐系统,并使用豆瓣电影数据集作为实验数据。资源定位于帮助完成推荐系统类课题的初、中级学习者,既可用于毕设、课设、日常作业,也可作为项目立项演示或算法验证的快速原型。压缩包共包含13个文件,整体大小约144.43MB。文件类型以zip分卷源码包、PNG示意图、Markdown说明文档、properties配置文件、csv数据文件及dat数据文件为主,结构清晰,便于按模块查看代码、说明文档与数据文件;其中还附有部署说明和项目说明,能帮助理解系统搭建思路与运行流程。目前已有38人浏览学习。资源提供完整经过测试的代码及配套设计文档,覆盖数据、后端服务与部署要点,上手即可复现项目效果;具备一定技术基础后,也可在此代码上修改扩展,实现其他推荐功能。若环境配置或运行遇到问题,还提供远程指导支持,性价比较高,适合需要快速完成推荐系统项目的人群。

1. 用户画像推荐系统:把豆瓣评分数据变成“懂你”的TopN推荐

“用户画像推荐系统 + 混合推荐算法 + 豆瓣电影数据集”这套组合,几乎是Python方向毕设里最经典的一道题:数据现成、算法公开、出效果快。它解决的问题很具体——给定几十万条豆瓣电影评分记录,如何为一个只打过《霸王别姬》和《肖申克的救赎》的用户,从几千部电影里挑出他大概率会喜欢的前十部。做法是先给每个用户建一张偏好画像,再把协同过滤和基于内容的推荐按权重叠加,让算法既懂“相似的人”也懂“相似的电影”。适合想做推荐系统方向、但不想从零造轮子的毕设学生,也适合想在内部数据上验证混合策略的算法新人。这个方向性价比高,难点不在模型深度,而在数据清洗、混合策略的设计和评估指标的选型,而这些恰好是答辩时最容易问住人的地方。

2. 豆瓣数据集预处理与画像构建:先把脏数据变成能喂算法的特征

推荐系统的第一课不是调库,而是处理数据。豆瓣电影数据集通常拆成三个文件:users.csv(用户ID、性别、年龄、职业)、movies.csv(电影ID、标题、类型)、ratings.csv(用户ID、电影ID、评分、时间戳)。这个结构和MovieLens一致,所以你后续想换数据源验证,代码基本不用动。第一步就是先配置好python运行环境(装好pandas、numpy、scikit-learn即可,不需要深度学习框架),把它当普通数据工程任务来做。

2.1 字段读入和基础清洗:别让编码和空值拖垮全局

拿到数据先别急着算相似度。先读进来看一眼数据结构,确认编码——豆瓣数据集常见的是utf-8或gbk编码,Windows下用gbk居多,读不对就全是乱码。除此之外还有两个典型脏点:users.csv里工业界没什么用的zip字段;ratings.csv里评分字段偶尔出现空值;movies.csv里电影类型是用|分隔的多标签。这三处不处理,后面的画像和相似度计算都会出错。

import pandas as pd import numpy as np # 读入三个表,编码按实际情况调整 users = pd.read_csv('users.csv', encoding='utf-8') movies = pd.read_csv('movies.csv', encoding='utf-8') ratings = pd.read_csv('ratings.csv', encoding='utf-8') # 丢弃无用字段 users = users.drop(columns=['zip'], errors='ignore') # 去重和空值处理 ratings = ratings.dropna(subset=['user_id', 'movie_id', 'rating']) ratings = ratings.drop_duplicates(subset=['user_id', 'movie_id']) # 过滤评分记录少于20条的用户,缓解后续矩阵稀疏问题 user_cnt = ratings['user_id'].value_counts() valid_users = user_cnt[user_cnt >= 20].index ratings = ratings[ratings['user_id'].isin(valid_users)] # 把电影类型字符串拆成列表,后面画像构建要用 movies['genres_list'] = movies['genres'].str.split('|')

这段代码的核心动作是过滤“低活跃用户”。为什么阈值取20而不是5或50?电影评分场景下,评过20部以上的用户基本形成了稳定的口味偏向,少于20部的用户画像极其稀疏,对他们的推荐效果差、还拖累整体评估指标——不放宽,后边你基于全部数据建的画像矩阵会是一个稀疏得没法看的黑匣子。如果你的数据集规模小,可以放宽到10,但不要低于5;如果数据量足够大,阈值调到25~30会让推荐效果更稳定,因为留下的都是表达明确的用户。

2.2 用户画像怎么建:从高分行为里提取类型偏好

用户画像听起来玄学,实际上落地就是个偏好向量。我常用的做法是:只取用户评过4分及以上的电影(豆瓣语境下4星以上才是“喜欢”),把这些高分电影的类型按频次累加,得到用户对各类电影的偏好权重。比如用户A打了高分的是《霸王别姬》(剧情/爱情)和《星际穿越》(科幻/冒险),那他的画像向量里剧情、爱情、科幻、冒险就有值,其他类型是0。这一步把“人”变成了一个可计算的稀疏向量,后面的内容推荐就直接拿这个向量去匹配电影。

# 只保留高分记录,评分>=4视为“喜欢” high_rated = ratings[ratings['rating'] >= 4] # 关联电影类型 merged = high_rated.merge( movies[['movie_id', 'genres_list']], on='movie_id', how='left' ) # 对每个用户统计其高分电影的类型频次,作为画像向量 def build_profile(group): profile = {} for genres in group['genres_list']: if not isinstance(genres, list): continue for g in genres: profile[g] = profile.get(g, 0) + 1 return profile # 得到 {user_id: {'剧情': 8, '爱情': 5, ...}} 形式的画像 user_profiles = merged.groupby('user_id').apply(build_profile)

这里有一个细节要提醒:如果你的ratings里存在用户没有高分电影的情况,build_profile返回空字典,这个用户后面在做内容推荐时必须单独用“默认类型”兜底,否则他的内容得分全部是0。还有一个常见误用是拿全部评分(包括低分)去建画像,这会让“喜欢动作片但对某几部动作片打了低分”的用户画像失真。只取高分是更稳的工程选择,因为它更贴近真实的消费意愿。

2.3 画像归一化和年龄职业扩展:让静态特征当配角而不是主角

画像向量建好后,还要做两件事:归一化和可选扩展。归一化很好理解——用户A看了30部剧情片、用户B看了3部剧情片,他们都喜欢剧情片,但权重数值差10倍,直接拿去和协同过滤分数相加会被数值大的用户带偏。所以权重要除以该用户所有类型权重的总和,或者除以最大值,得到0~1之间的分布。扩展则是把用户的年龄、职业加进去作为辅助特征,比如24岁以下的用户对动画和喜剧有天然偏置,但这个偏置不等于个人行为偏好,权重只能给很小,别让静态特征反客为主。

def normalize_profile(profile): total = sum(profile.values()) if total == 0: return profile return {k: v / total for k, v in profile.items()} user_profiles = {uid: normalize_profile(prof) for uid, prof in user_profiles.items()}

归一化后的画像向量有一个额外的好处:它天然变成了概率分布,后续算用户之间相似度时可以直接用KL散度或余弦相似度,不需要再做标准化。这个细节在答辩时讲出来,比单纯说“我建了用户画像”有说服力得多。

3. 混合推荐算法实现:协同过滤与内容推荐如何互补

混合推荐这个标题词,核心不是“两个算法跑出来取平均”,而是搞清楚两个算法各自的失效场景,再用策略把短板互补。单一协同过滤的问题很明确:新用户只有一两部评分时,找相似用户基本靠猜,推荐质量很差;单一内容推荐则永远在推“同类型安全牌”,没有惊喜度,用户如果只打过两部爱情片,它就会一直推爱情片,毫无多样性。混合的价值就是把“别人喜欢什么”和“你喜欢什么”两条线索拧成一股绳。

3.1 三种混合策略:加权、切换、级联,毕设选哪种

常见的混合方式有三条路。加权混合最简单,两个算法的得分线性相加,权重从0.6比0.4起步调;切换混合是按用户行为量判断——评分少于10条的用户走内容推荐,超过10条走协同过滤,各管一段;级联混合是先内容推荐粗召回,再协同过滤精排。毕设项目我一般建议选加权混合,原因很实际:它实现最简单、参数只有一个alpha、调参逻辑清晰,而且答辩时你能理直气壮地说“我对比了alpha取0.3~0.9的效果后选了0.7”。论文里还能画一张alpha与准确率的折线图,这是加分的可视化素材。

3.2 协同过滤模块:基于物品的ItemCF为什么更适合电影场景

电影推荐场景下,基于物品的协同过滤(ItemCF)通常比基于用户的UserCF稳定。原因有两个:一是电影数量远小于用户数量,物品相似度矩阵更小、算起来更快;二是电影是长生命周期物品,昨天的《肖申克的救赎》和今天的《霸王别姬》在“被喜欢”的意义上依然相似,物品相似度可以提前离线算好存着,线上只做查表和聚合。实现时最核心的是相似度算法选型——直接用余弦相似度会把热门电影的权重无限放大,因为《流浪地球》被几百万人评过,它的向量天然长,和谁都像。常见做法是使用调整余弦相似度(对每个用户的评分减去该用户的平均分后再算余弦),这一步能显著压低热门偏差。

from sklearn.metrics.pairwise import cosine_similarity # 构建用户-物品评分矩阵,缺失值先随便填 rating_matrix = ratings.pivot_table( index='user_id', columns='movie_id', values='rating' ) # 用每个用户的均值填充缺失值,保留用户自身的评分尺度 rating_filled = rating_matrix.apply( lambda row: row.fillna(row.mean()), axis=1 ) # 调整余弦相似度:把每个用户减去自己的平均分 rating_centered = rating_filled.sub(rating_filled.mean(axis=1), axis=0) # 计算物品-物品相似度矩阵(电影数少,可以直接全量算) item_sim = cosine_similarity(rating_centered.T) item_sim_df = pd.DataFrame( item_sim, index=rating_matrix.columns, columns=rating_matrix.columns )

填充策略要用用户均值而不是全局均值,原因在于不同用户的打分尺度不同——有人给3分算失望,有人给3分算还不错。减去用户均值的本质是做一个“相对于这个人平均口味”的偏移,剩下的正负号才有比较意义。物品维度如果在几千部,这个全量cosine_similarity几秒就能跑完;万一上了几万部电影,再考虑用NearestNeighbors只算TopK邻居,不要硬算全矩阵。这一节的热搜词“python构建邻接矩阵”其实就是同一个东西——物品相似度矩阵本质上就是一张加权无向图的邻接矩阵,你后续想扩展做图上的标签传播,直接拿这个DataFrame转就行。

3.3 内容推荐模块:用画像匹配电影类型

内容推荐的逻辑比协同过滤简单——拿到用户的画像向量,对候选电影逐部计算类型匹配度即可。前面建画像时已经给每个用户算了类型权重,后面要做的就是把画像和每部电影的类型做交集打分。注意只取用户画像里权重最高的前3个类型参与计算,不要全量交叉,否则一个用户画像里权重很低的冷门类型会把结果搅浑。

def content_score(user_id, movie_id, user_profiles, movies, top_n_types=3): profile = user_profiles.get(user_id, {}) if not profile: return 0.0 # 取权重最高的前N个类型作为核心口味 top_types = sorted( profile.items(), key=lambda x: x[1], reverse=True )[:top_n_types] movie_genres = movies.loc[ movies['movie_id'] == movie_id, 'genres_list' ].values if len(movie_genres) == 0: return 0.0 movie_genres = movie_genres[0] score = 0.0 max_weight = top_types[0][1] for g, w in top_types: if g in movie_genres: score += w # 除以最大权重,让得分落在0~1区间 return score / max_weight

这个函数里top_n_types是唯一的敏感参数。取1,结果太专一;取5,画像里的弱偏好开始捣乱。3是经验值——“用户的长期稳定口味通常可以用两三个强标签概括”,这在豆瓣电影场景下尤其明显。如果你后续想加惊喜度,可以把top_n_types调成2并放宽候选集范围,牺牲一点精准率换多样性,答辩时也能多讲一个“多样性与准确率的权衡”故事。

3.4 加权混合主流程:一个alpha调全身

最后是把两个模块拧在一起。我的通常做法是:协同过滤给每个候选电影一个得分(归一化到0~1),内容推荐给每个候选电影一个得分(同样0~1),然后加权求和排序取TopN。候选集取并集而不是交集——只在协同过滤里出现的电影,内容分给0;只在内容推荐里出现的电影,协同过滤分给0。这样保证了召回覆盖。

def hybrid_recommend(user_id, k=20, alpha=0.7): # 协同过滤候选:取物品相似度Top20的邻居电影 cf_candidates = get_cf_candidates(user_id, item_sim_df, rating_matrix, top=50) # 内容候选:取用户画像匹配度最高的50部 content_candidates = get_content_candidates( user_id, user_profiles, movies, top=50 ) # 先归一再混合 cf_scores = normalize_scores(cf_candidates) content_scores = normalize_scores(content_candidates) merged_scores = {} all_movie_ids = set(cf_scores.keys()) | set(content_scores.keys()) for movie_id in all_movie_ids: merged_scores[movie_id] = ( alpha * cf_scores.get(movie_id, 0.0) + (1 - alpha) * content_scores.get(movie_id, 0.0) ) # 排除用户已经看过的电影 watched = set(ratings[ratings['user_id'] == user_id]['movie_id']) ranked = sorted( merged_scores.items(), key=lambda x: x[1], reverse=True ) return [mid for mid, score in ranked if mid not in watched][:k]

alpha的调参顺序有一个经验规律:先固定0.7跑一轮,观察推荐结果里“热门片占比”和“冷门片占比”;如果热门片太多、结果和人人的榜单没区别,说明alpha偏大,往0.5降;如果推荐结果全是用户口味的复制品、没有新鲜感,说明alpha偏小,往0.8升。整个调参过程不需要写复杂脚本,手动跑几组、看结果就能定位。等调到满意后,再在评估集上把alpha扫一遍画折线图,论文里这张图非常加分。

4. 毕设级推荐系统的五个典型坑:现象、原因与解决

这个方向看着简单,真跑起来处处是坑。下面的每一条都是常见翻车现场,按“现象→原因→解决”三件套写清楚,你照着排查就能少走很多弯路。

4.1 评分矩阵稀疏:大部分用户只有个位数评分

现象:推荐结果完全不像“推荐”,更像热播榜,因为相似度计算时稀疏向量之间全是零,算出来的相似度要么是0要么是负值,最后只能靠热门电影补位。

原因:电影评分天然服从长尾分布,大部分用户只评过5~10部电影,评过50部以上的忠实用户是少数,而你的协同过滤恰恰需要足够的行为量才能算出有意义的相似度。

解决:三层做法。第一层是建矩阵前就过滤低活跃用户,评分数少于20的直接剔除,这是性价比最高的一招;第二层是用用户均值填充空缺失位而不是用零填充,这样算余弦时不会把“没看过”当成“不喜欢”;第三层是限制每个用户只取最近的50条评分参与计算,保证数据新鲜度同时控制矩阵规模。

4.2 热门电影偏差:余弦相似度让《肖申克》和谁都像

现象:相似电影结果里出现大量“高分热门片”,比如《肖申克的救赎》同时是《教父》和《海贼王》的最相似电影,推荐结果严重偏向热门爆款。

原因:热门电影的评分次数多、评分向量长,余弦相似度对向量长度不敏感不假,但向量里非零值极多、和其他电影的交集面积天然大,在相似性上占尽便宜。这就是所谓的“热门偏差”。

解决:改用调整余弦相似度,先减去用户均值再算相似度,让“喜欢程度”变成“相对个人口味的偏移”,热门片如果没有真正打到更多人心里,偏移值会被均分拉低。除此之外还可以给相似度加一个惩罚系数:sim / log(1 + n_i),其中n_i是电影被评次数,下一节给出的就是这种简化版平滑逻辑。实测下来,这两个手段合在一起能显著拉开“真相似”和“看起来都热门”的距离。

4.3 训练测试划分不当:随机划分让模型“偷看”答案

现象:评估时精准率很高(比如0.35以上),但实际换一批数据就崩;答辩时被问“你的模型在线下这么好,为什么线上不行”,当场卡住。

原因:划分训练集和测试集时按“随机行”切分,同一条用户的评分记录一部分在训练集、一部分在测试集,模型在训练时其实已经见过测试集的用户偏好,这就是数据泄漏。最常见做法是按用户切分,但按“用户”切会忽略时间因素——用户上个月的行为和这个月的行为不该用同一个生成方式对待。

解决:毕设项目推荐按时间戳切分,每部电影留下最近的一次评分做测试,其余做训练。这样既避免了随机切分的泄漏,又模拟了“用历史预测未来”的真实推荐场景。切分后代码里加一行断言:训练集的用户评分时间全部早于测试集,把验证固化到流程里。

4.4 新用户和新电影的冷启动:画像和协同过滤同时失效

现象:对评分记录少于5条的新用户,协同过滤找邻居找不到,内容推荐画像几乎是零向量,推荐结果退化成热门榜;对上架不久的新电影,没有任何评分,协同过滤完全没有它的位置。

原因:协同过滤依赖历史行为,内容推荐依赖画像积累,而冷启动的本质是“行为数据缺失”,这不是算法问题,是数据问题。

解决:最实用的一套冷启动策略是给新用户直接用“类型偏好问卷”做初始化——让他勾选喜欢的类型,把这个选择映射成画像初始值,再放进内容推荐模块走单路推荐。新电影则用它的类型字段走内容推荐路,先以较低权重混入混合结果,积累几条评分后权重再逐步提升。这就是前面提到“切换混合”策略的典型应用场景。毕设里能讲清冷启动策略,比多优化两个点的准确率更有价值。

4.5 评估只看RMSE:预测评分和推荐TopN根本是两回事

现象:论文里写RMSE=0.89,看着数值很小,结果推荐列表里用户一部都不想看。

原因:RMSE评估的是“预测评分与真实评分的误差”,但推荐系统的真实目标不是预测打几分,而是把用户真正会喜欢的有限几部电影送到列表前几位。评分预测准不等于排序准,绝大多数用户对0.2分的预测误差毫无感知,但对你把垃圾片推上第3位感知极强。

解决:离线评估改用排序类指标,准确率、召回率、覆盖率、多样性。具体到代码层面,推荐列表取Top10,计算Precision@10和Recall@10。这一步的评估代码就是下一章的主内容——评估和调参是一体的,评估指标定错了,后面所有调参都是白调。

5. 离线评估与调参验证:用Precision和Recall说服答辩老师

离线评估不复杂,但必须做对划分、做对统计口径。我自己一直沿用一个极简评估函数:输入测试集里“用户真实看过的电影集合”,输出推荐列表,然后算TopK命中率。

def evaluate_hybrid(test_data, recommend_func, k=10): """ test_data: {user_id: [真实看过且不在训练集的电影id]} recommend_func: 接收user_id和k,返回推荐的电影id列表 """ total_hits = 0 total_actual = 0 covered_items = set() for user_id, actual_list in test_data.items(): rec_list = recommend_func(user_id, k=k) rec_set = set(rec_list) actual_set = set(actual_list) total_hits += len(rec_set & actual_set) total_actual += len(actual_set) covered_items.update(rec_set) precision = total_hits / (len(test_data) * k) recall = total_hits / max(total_actual, 1) coverage = len(covered_items) / len(movies['movie_id']) return { 'precision@{}'.format(k): precision, 'recall@{}'.format(k): recall, 'coverage': coverage }

参数说明:k是推荐列表长度,毕设里取10最直观,取20更贴近真实场景。total_hits是所有用户命中数之和,注意这里用的是“所有用户加总”,不是每个用户算完再平均,这样算出来的全局指标更稳定,不会因为某个用户行为特别多或特别少而剧烈波动。coverage是一个常被忽略但答辩老师爱问的指标——它回答“你的系统是只会推头部热门,还是能覆盖到长尾电影”,覆盖率低于10%是一个危险信号,说明你的推荐系统本质上退化成热榜。

最后的调参建议就一条:先固定alpha=0.7、k=10跑出第一版评估结果,把精确率和召回率记下来,再逐个调alpha(0.3到0.9,步长0.1)和画像的top_n_types(2到5),每次只动一个变量,记录成表。你会发现一个规律:alpha越高,精准率越好、覆盖率越差;alpha越低,多样性越好、精准率小幅下降。这个此消彼长的曲线,才是混合推荐真正值得写进论文里的结论。

我自己的习惯是评估脚本要跑到能一键输出三行指标,并且把每次调参的记录存成CSV,答辩时直接展示“alpha从0.3升到0.7,Precision@10从0.12升到了0.21,同时覆盖率下降6个百分点”这种真实对比,比任何文字描述都有说服力。毕竟做推荐系统的人最清楚,一个没人看的“高准确率推荐”,和一篇没有评估数字的论文一样,都只是自我安慰。这个项目我踩过的坑基本都在上面了,照着做能少折腾好几天,希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询