ARTICLE DETAIL

资讯详情

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

协同过滤电影推荐系统实战:基于MovieLens的UserCF与ItemCF实现

协同过滤电影推荐系统实战:基于MovieLens的UserCF与ItemCF实现 简介这是一套基于Python实现的协同过滤电影推荐系统源码面向推荐系统学习者、数据挖掘初学者及希望上手算法工程的Python开发者。项目基于MovieLens经典数据集完整覆盖UserCF、ItemCF、LFM等推荐算法模块从数据加载、相似度计算、模型训练到随机预测与热门推荐均有对应脚本便于理解推荐系统的完整落地流程。压缩包共58个文件包括9个Python算法脚本、8个编译后的字节码文件、6个XML工程配置、5个PKL序列化数据缓存、3个DAT数据集及README、Shell脚本等整体约51.37MB目录结构清晰。目前已有1098人浏览学习。读者可从中获得可运行的推荐系统代码骨架、预处理的相似度矩阵与测试集划分、以及MovieLens 100K/1M原始数据方便直接调试、二次开发或作为课程设计参考。1. 协同过滤电影推荐系统这个 MovieLens 源码包能让你少走三天弯路如果你正在学 Python 数据挖掘或者想用 MovieLens 数据集搭一个能真实跑出 Top-N 结果的电影推荐系统这个基于协同过滤的源码包值得拿来做起点。它不依赖任何深度学习框架核心逻辑是用 Pandas 和 NumPy 把「用户-电影-评分」三元组转成用户物品矩阵然后分别实现 UserCF 与 ItemCF 两条路线的相似度计算、近邻选取和推荐列表生成。源码从 CSV 加载、稀疏矩阵构建、余弦相似度到 Precision、Recall、Coverage 离线评估一条链路全部打通新手能逐行读懂老手换数据集只需要改文件路径和两个超参数。我自己第一次复现时因为没搞懂 K 值与稀疏度的关系调了两天参数召回率都上不去这份源码把均值归一化和边界条件收在函数内部省下来的时间足够你再跑三组对照实验。2. 数据准备与 EDA先把 MovieLens 的评分矩阵结构摸透大部分翻车都发生在数据还没理解清楚就急着算相似度。MovieLens 的 ratings.csv 只有四列——userId、movieId、rating、timestamp但就是这四列决定了你后面所有推荐逻辑的走向。我一般拿到数据集先做三件事看一眼评分分布、算一下矩阵稀疏度、确认时间戳跨度然后再决定用 UserCF 还是 ItemCF。2.1 评分分布与时间跨度先知道数据长什么样MovieLens 的 ml-latest-small 大约包含 10 万条评分评分值是 0.5 到 5.0 的离散档位。加载完数据的第一步不是训练模型而是确认评分分布是否正常——比如评分是否大量集中在 4 分和 5 分有没有异常空值时间戳覆盖到哪一天。源码里的数据加载函数做了三件事读 CSV、把 timestamp 转成 datetime、按 userId 排序。下面这段是加载和探索的代码import pandas as pd # 指定 dtype 可以减少内存占用评分用 float32 足够 ratings pd.read_csv(data/ratings.csv, dtype{userId: int32, movieId: int32, rating: float32}) movies pd.read_csv(data/movies.csv, dtype{movieId: int32}) # timestamp 是 Unix 秒转成 datetime 才能做时间序列切分 ratings[ts] pd.to_datetime(ratings[timestamp], units) ratings ratings.sort_values([userId, ts]).reset_index(dropTrue) # 看评分分布 print(ratings[rating].value_counts().sort_index()) # 看用户数、电影数、时间跨度 print(f用户数: {ratings[userId].nunique()}) print(f电影数: {ratings[movieId].nunique()}) print(f时间范围: {ratings[ts].min()} ~ {ratings[ts].max()})这段代码里dtype 参数指定了 int32 与 float32在大数据集上能把内存占用压缩到默认 int64/float64 的一半左右处理 ml-20m 时会明显感觉到差别。按 userId 和时间排序的目的是为了后续做时间序列的 train/test 切分时能直接拿到每个用户最早到最晚的评分轨迹。拿到输出后我习惯先看 value_counts 的结果。MovieLens 的评分分布通常会偏向高分3 分以下的样本占比很小这在后续评估里会导致低分电影的推荐权重天然偏弱。时间跨度也很关键如果数据跨越了多年直接随机切分会让测试集里出现和训练集同一时期的电影评估结果会虚高。2.2 用户物品矩阵与稀疏度fill_value0 到底改变了什么接下来是最核心的一步把长表转成宽表也就是用户物品矩阵。这里最常见的错误是直接用 pivot_table 保留 NaN然后在算相似度时被 NaN 传染整个矩阵的运算结果全是 NaN。源码里用的是 fill_value0用无交互来填充缺失评分。# 构建用户-电影评分矩阵行是用户列是电影 user_item_matrix ratings.pivot_table( indexuserId, columnsmovieId, valuesrating, fill_value0 ).astype(float32) n_users, n_items user_item_matrix.shape n_ratings len(ratings) sparsity 1 - n_ratings / (n_users * n_items) print(f矩阵形状: {n_users} x {n_items}) print(f稀疏度: {sparsity:.4f})参数说明pivot_table 的 index 和 columns 分别指定了矩阵的行列索引values 指定填充值来源fill_value0 表示把没有评分的格子填成 0。astype(float32) 是为了后续矩阵运算时的内存和速度。fill_value0 的代价是原本缺失的位置会被当成“评分为 0”参与余弦相似度计算从而轻微拉低所有相似度。在小数据集上这个影响可以忽略但如果你的场景里用户行为极度稀疏用 0 填充会让相似度矩阵整体偏向负数方向。更稳妥的做法是保留 NaN在计算相似度时用 mask 只统计共同评分的维度或者用np.nan_to_num处理。我个人的习惯是先用 0 填充分分钟把流程跑通再换保留 NaN 的方案做对比看指标差异是否在可接受范围内。稀疏度的含义是“矩阵中非零元素的比例的补集”MovieLens 的数据集稀疏度通常都在 98% 以上。这个数字直接影响算法选型。用户数远大于物品数且评分极端稀疏时ItemCF 通常更稳用户数不多比如 500 人以内且共同评分足够时UserCF 的推荐结果更有惊喜。下表是我在复现时的一个简单选型依据数据特征推荐倾向理由用户数 物品数稀疏度 99%ItemCF物品间共同评分比用户间共同评分多相似度更可靠用户数 物品数稀疏度 95%UserCF用户相似度有足够共同评分支撑推荐结果更有针对性新用户占比高ItemCF 流行度兜底新用户没有历史评分UserCF 无法找到近邻如果数据结果那一列稀疏度超过 0.99我建议直接跳到 ItemCF 路线这也是源码包默认把 ItemCF 作为主推方案的原因。把矩阵构建和稀疏度确认好后面的相似度计算才不会是空中楼阁。3. UserCF 与 ItemCF 实现相似度计算背后的参数细节用户物品矩阵准备好以后就进入协同过滤的核心环节——相似度计算。源码包里两个算法的整体框架是一样的先算相似度矩阵再取近邻最后加权聚合出推荐得分。区别只在于相似度是算在用户维还是物品维。3.1 UserCF找相似用户用近邻评分加权生成 Top-NUserCF 的思路是“与你兴趣相似的人喜欢的电影你大概率也会喜欢”。实现分四步计算用户间相似度、取目标用户前 K 个近邻、用近邻评分做加权求和、排除用户已看过的电影后取 Top-N。import numpy as np def cosine_similarity(mat): 输入用户物品矩阵返回归一化后的余弦相似度矩阵 # 对每行做 L2 归一化避免评分尺度影响 norm np.linalg.norm(mat, axis1, keepdimsTrue) mat_norm mat / (norm 1e-9) # 归一化后的点积就是余弦相似度 return np.dot(mat_norm, mat_norm.T) # 计算用户相似度矩阵 user_sim cosine_similarity(user_item_matrix.values) def user_cf_recommend(user_id, top_k10, top_n10): # 把 userId 转成矩阵行索引 uid_idx list(user_item_matrix.index).index(user_id) # 取相似度最高的前 top_k 个用户排除自己 sim_scores user_sim[uid_idx] neighbors np.argsort(-sim_scores)[1:top_k 1] # 近邻的评分矩阵和相似度权重 neighbor_ratings user_item_matrix.values[neighbors] # (top_k, n_items) weights sim_scores[neighbors].reshape(-1, 1) # (top_k, 1) # 加权求和并除以权重和得到每个电影的加权平均分 score np.sum(neighbor_ratings * weights, axis0) / np.sum(weights, axis0) # 把用户已经评过分的电影得分置零避免推荐重复 rated_by_user user_item_matrix.values[uid_idx] 0 score[rated_by_user] 0 # 按得分降序取前 top_n 个电影索引 top_indices np.argsort(-score)[:top_n] return top_indices逻辑说明cosine_similarity 里先对每行做 L2 归一化这样点积就等于余弦相似度比用 sklearn 的 cosine_similarity 更直白也方便理解背后的几何意义。neighbor_ratings 是近邻用户的评分矩阵weights 是当前用户与这些近邻的相似度两者相乘再归一化得到的就是每个电影在当前用户视角下的加权平均分。参数说明top_k 控制近邻数量是 UserCF 最重要的超参数。K 太小相似度高的近邻太少推荐结果带噪声K 太大低相似度用户进来稀释权重推荐结果趋近全局热门。top_n 是最终推荐列表长度一般取 5、10、20。这里有个细节是argsort(-sim_scores)[1:top_k 1]索引从 1 开始是为了跳过用户自己——自己与自己相似度恒为 1不排除的话会直接污染结果。这个实现没有做评分均值归一化如果某个用户整体打分区間偏高他的所有评分都会在加权求和里放大。改进方式是在计算前对每行减掉用户平均分算完再加回来这个思路后面避坑章节还会谈到。3.2 ItemCF找相似电影更适合电影推荐场景ItemCF 的思路是“和你喜欢的电影相似的电影你也会喜欢”。它先计算电影之间的相似度然后在推荐时用用户已经评过分的电影去加权计算未评分电影的得分。实现如下# 转置矩阵让每行代表一部电影 item_sim cosine_similarity(user_item_matrix.values.T) def item_cf_recommend(user_id, top_k10, top_n10): uid_idx list(user_item_matrix.index).index(user_id) # 当前用户的评分向量长度等于电影数 user_ratings user_item_matrix.values[uid_idx] rated_idx np.where(user_ratings 0)[0] if len(rated_idx) 0: return [] # 冷启动用户直接返回空需要兜底 # 未评分电影索引 unrated_idx np.where(user_ratings 0)[0] # 只取用户已评分电影与未评分电影的相似度子矩阵 sim_sub item_sim[np.ix_(unrated_idx, rated_idx)] # (unrated_count, rated_count) ratings_sub user_ratings[rated_idx] # (rated_count,) # 加权平均分母加 1e-9 防止除零 score np.dot(sim_sub, ratings_sub) / (np.sum(sim_sub, axis1) 1e-9) # 按得分降序取前 top_n 个未评分电影 top_indices unrated_idx[np.argsort(-score)[:top_n]] return top_indices逻辑说明item_sim 是物品间相似度矩阵user_ratings是当前用户对所有电影的评分。核心操作是sim_sub只取用户已评分的电影和未评分电影之间的相似度然后用点积算出未评分电影的加权得分。这个实现里np.ix_的作用是取子矩阵避免构造完整相似度矩阵再切片造成内存浪费。参数说明top_k 在 ItemCF 里通常比 UserCF 更敏感。因为电影数量通常少于用户数量相似度矩阵规模小top_k 可以适当调大。但过大的 top_k 会把和当前电影只有微弱相似度的长尾电影也拉进来导致推荐列表过于发散。另外这里的加权平均只用了相似度做权重没有考虑每个已评分电影本身的热门程度也就是没有做 Inverse User Frequency后面避坑里会说。3.3 余弦相似度与皮尔逊相似度什么时候换源码包里默认用余弦相似度但实际调参时换一次皮尔逊相似度往往有意外收获。皮尔逊相似度在余弦基础上做了中心化按行减去用户平均分消除不同用户的打分偏好。比如用户 A 习惯打 3-4 分用户 B 习惯打 4-5 分余弦相似度会把这种尺度差异算进去皮尔逊则只看变化趋势。def pearson_similarity(mat): 按行中心化后再算余弦等价于皮尔逊相关系数 mat_centered mat - mat.mean(axis1, keepdimsTrue) norm np.linalg.norm(mat_centered, axis1, keepdimsTrue) mat_norm mat_centered / (norm 1e-9) return np.dot(mat_norm, mat_norm.T)指标优点缺点余弦相似度计算简单数值稳定适合稀疏矩阵不区分用户打分习惯全局热门会占优皮尔逊相似度消除用户评分偏差推荐结果更有个人特色中心化后矩阵更稀疏共同评分少的用户对结果不稳定Jaccard 相似度只看共同评分有无适合极度稀疏场景忽略评分数值只适合做冷启动替代我一般会在 UserCF 上用皮尔逊在 ItemCF 上用余弦。原因是 UserCF 受用户个人评分尺度影响更大中心化带来的提升明显ItemCF 里的电影评分分布本身就相对稳定余弦足够换皮尔逊反而可能放大噪声。如果你遇到推荐结果总是偏向高分热门电影先别急着调 K把相似度函数换一下可能更有效。4. 离线评估与调优Precision、Recall、Coverage 怎么算才可信推荐系统跑出推荐列表不算完成能证明这个列表比随机推荐强才算真正落地。离线评估最让人头疼的是划分方式其次是指标的计算口径。源码包的评估模块做了两个关键决定使用时间序列切分以及同时计算 Precision 和 Coverage 两个维度避免只看单个指标自嗨。4.1 时间戳切分用真实时序模拟线上行为随机切分在推荐系统评估里是经典陷阱。如果训练集和测试集来自同一时间段某个电影的评分同时出现在两边相当于考试时把答案提前给了模型。MovieLens 的数据带时间戳按时间切分是更贴近线上场景的做法用前 80% 的时间段做训练后 20% 做测试。# 先看时间分布选一个分位点做切分边界 split_ts ratings[ts].quantile(0.8) train ratings[ratings[ts] split_ts] test ratings[ratings[ts] split_ts] print(f切分时间点: {split_ts}) print(f训练集评分条数: {len(train)}, 测试集评分条数: {len(test)}) # 重点测试集的用户必须在训练集里出现过否则冷启动用户没法评估 test_users test[userId].unique() train_users set(train[userId].unique()) eval_users [u for u in test_users if u in train_users] print(f可评估用户数: {len(eval_users)} / {len(test_users)})逻辑说明quantile(0.8) 取的是评分时间分布的 80 分位点也就是说所有评分里大约 80% 发生在这个时间点之前20% 之后。这比简单按条数前 80% 切分更符合真实时序语义因为用户评分频率不均匀。参数说明切分比例 0.8/0.2 是按经验来的如果你的数据时间跨度特别长也可以考虑 0.9/0.1。切分后必须检查训练集和测试集用户的重合度测试集里新用户无法通过协同过滤生成推荐如果不过滤掉评估结果会被冷启动用户大幅拉低让你误以为算法不行。4.2 评估指标实现命中率、覆盖率与平均流行度评估时对每个可评估用户执行推荐然后累计计算指标。PrecisionN 衡量推荐列表里有多少是用户真正看过的电影RecallN 衡量用户看过的电影里有多少被推荐出来Coverage 衡量推荐系统覆盖了多少电影。def evaluate_recommender(recommend_func, user_item_matrix, test_df, top_n10): test_users test_df[userId].unique() precisions, recalls [], [] recommended_movies set() for uid in test_users: # 该用户在测试集里看过的电影 test_movies set(test_df[test_df[userId] uid][movieId]) if len(test_movies) 0: continue # 调用推荐函数返回电影索引 rec_indices recommend_func(uid, top_ntop_n) rec_movie_ids set(user_item_matrix.columns[rec_indices]) # 推荐与真实观看的交集 hit len(rec_movie_ids test_movies) precisions.append(hit / top_n) recalls.append(hit / len(test_movies)) recommended_movies.update(rec_movie_ids) precision float(np.mean(precisions)) recall float(np.mean(recalls)) hit_rate float(np.mean([p 0 for p in precisions])) coverage len(recommended_movies) / user_item_matrix.shape[1] return { precision: precision, recall: recall, hit_rate: hit_rate, coverage: coverage }逻辑说明precisions 列表存每个用户的 Precision最后取平均是宏平均避免用户评分条数不均导致大用户主导结果。hit_rate 表示有多少用户至少命中一部推荐电影这个指标比 Precision 更直观地反映推荐系统“有没有用”。参数说明top_n 在这里既是推荐列表长度也是 Precision 的分母。如果用户测试集里看过的电影数远小于 top_nRecall 必然偏低这是正常现象不必惊讶。coverage 计算的是推荐系统覆盖的电影数占总电影数的比例覆盖率过低说明推荐结果集中在头部热门长尾电影永远没有曝光机会。还有一个人群指标值得关注——平均推荐流行度。可以统计每次推荐列表中电影的平均被评分次数如果一个算法的 Precision 挺高但推荐的平均流行度也极高说明它只是在推荐热门电影。这类结果上线后用户会觉得“推荐了个寂寞”因为这些电影用户早就从其他渠道知道了。def average_popularity(rec_indices, user_item_matrix): item_count user_item_matrix.values.sum(axis0) # 每部电影被评分次数 return float(np.mean(item_count[rec_indices]))我一般会在调参时把四个指标列成一张表每次改完参数都重跑一遍。只看 Precision 的话很容易掉进“调参把自己调进去”的坑里。5. 实战避坑MovieLens 协同过滤最容易翻车的五个场景协同过滤的坑很多不是算法原理上的而是数据和实现细节上的。这里有五条我复现时真实踩过的记录每条都是现象、原因、解决一条线供你对照检查。5.1 冷启动用户推荐全空别让兜底逻辑缺席现象测试集里部分用户 ID 在训练集中没有出现推荐函数返回空列表评估脚本直接报错或者把这些用户跳过。原因协同过滤的本质是找历史行为相似的近邻新用户没有任何评分记录相似度矩阵里对应行全是 0排序后拿不到任何有效近邻。解决在推荐函数入口加一个兜底分支当用户评分记录为空时返回全局最热门的 N 部电影。def recommend_with_fallback(user_id, top_n10): uid_idx list(user_item_matrix.index).index(user_id) user_ratings user_item_matrix.values[uid_idx] if user_ratings.sum() 0: # 冷启动返回全局热门电影兜底 popularity user_item_matrix.values.sum(axis0) dummy np.zeros(user_item_matrix.shape[1]) dummy[user_ratings 0] -1 # 已看过的排最后 return np.argsort(-popularity - dummy)[:top_n] # 正常走 ItemCF 推荐 return item_cf_recommend(user_id, top_ntop_n)这条兜底逻辑要在评估和线上同时生效否则离线测试通过、上线后新用户还是什么都刷不出来。5.2 推荐列表全是《肖申克的救赎》热门偏差怎么压现象不管给哪个用户做推荐Top-10 里至少有五部是全局评分最多的电影ItemCF 和 UserCF 结果高度重合。原因热门电影和几乎所有其他电影都有共同评分相似度计算时天然占优。得分加权求和没有对物品流行度做惩罚热门电影拿到的权重永远是最大的。解决在计算得分时对热门电影做惩罚常见做法是引入 Inverse User Frequency 权重每个电影根据它的被评分次数取对数倒数。被评分次数越多相似度贡献权重越小。# 计算每个电影的逆用户频率 item_frequency user_item_matrix.values.sum(axis0) # (n_items,) iuf np.log((n_users 1) / (item_frequency 1)) # 对数变换 # 在 ItemCF 的加权打分里乘上 IUF # 修改 item_cf_recommend 内部的得分计算 # score np.dot(sim_sub * iuf_sub, ratings_sub) / (np.sum(sim_sub, axis1) 1e-9)我见过不少教程直接忽略这个处理导致复现出来的推荐列表千篇一律。加上 IUF 后Coverage 通常会明显提升推荐结果的个人化程度也会上得来。5.3 UserCF 在稀疏数据上召回率惨淡先查共同评分数现象用 UserCF 在 ml-latest-small 上跑评估Recall10 不到 0.05换 ItemCF 后直接翻倍。原因MovieLens 的用户数远多于电影数用户之间的共同评分记录非常少相似度矩阵里绝大部分值接近 0。UserCF 找到的近邻根本算不上“相似”加权结果自然不准。解决如果数据稀疏度超过 99%默认用 ItemCF别在 UserCF 上浪费调参时间。如果项目需求里必须用 UserCF先做一下维度压缩——对用户物品矩阵做 SVD 截断把行降维到 50 维左右再算相似度效果会比直接用原始矩阵好很多。from sklearn.decomposition import TruncatedSVD def user_cf_with_svd(user_item_matrix, n_components50): svd TruncatedSVD(n_componentsn_components, random_state42) user_latent svd.fit_transform(user_item_matrix.values) # (n_users, 50) # 用降维后的向量算相似度 return cosine_similarity(user_latent)这算是一个折中方案SVD 本身会损失一部分信息但稀疏度高时损失的信息里噪声占大头。5.4 随机切分让指标虚高时间泄漏是隐性翻车点现象同一份代码用 train_test_split 随机切分时 Precision10 接近 0.12换成时间切分后掉到 0.07以为是代码写错了。原因随机切分把同一部电影的评分同时放入训练集和测试集ItemCF 在训练时已经见过这部电影和用户其他电影的关系测试时相当于开卷考试。时间切分后训练集里看不到未来评分指标自然下降。解决评估标准统一用时间切分。如果数据没有时间戳可以按 userId 或 itemId 分组做留一法保证测试集中的评分-物品对不在训练集出现。# 留一法切分每个用户保留最新一条评分做测试 train_list, test_list [], [] for uid, group in ratings.groupby(userId): group group.sort_values(ts) test_list.append(group.iloc[-1]) train_list.append(group.iloc[:-1]) train pd.concat(train_list) test pd.concat(test_list)时间切分带来的指标下降不是坏事它更接近真实线上推荐的表现模型上线后不会被打脸。5.5 全量相似度矩阵内存爆掉稀疏矩阵与分批计算现象换到 ml-20m 数据集后程序跑到相似度计算直接 MemoryError进程被系统杀掉。原因余弦相似度实现里用了np.dot(mat_norm, mat_norm.T)在 ml-20m 上用户数约 13 万生成的相似度矩阵是 13 万乘 13 万就算用 float32 也要占用约 676GB。任何一台常规机器都扛不住。解决不要一次性生成全量相似度矩阵改用稀疏矩阵存储或者分批计算近邻。常见做法是只对每个用户计算与其有共同评分记录的用户相似度用 scipy.sparse 保存或者用 Annoy、Faiss 这类近似最近邻库只保留每个用户前 K 个最近邻。from scipy.sparse import csr_matrix # 把评分矩阵转成稀疏格式只存非零元素 user_item_sparse csr_matrix(user_item_matrix.values) # 对每个用户只存 top_k 个相似用户的相似度 from scipy.sparse import linalg # 实际项目中建议用 faiss.IndexFlatIP 做向量检索在我自己的实践中超过 1 万用户的场景我就不再使用全量相似度矩阵了近似最近邻的召回损失很小内存占用能降到原来的几十分之一。6. 把推荐系统封装成命令行工具从离线评估到可交付的推荐结果前面几章的代码都是函数级别的验证真正要交付或者做实验需要把推荐逻辑封装成一个固定的入口。源码包最后把推荐算法、矩阵加载、评估一体封装成了 CLI 工具方便每次换参数直接跑不用改代码。6.1 用 argparse 封装推荐入口CLI 的意义是把参数和数据路径从代码里剥离开。用 argparse 定义五个参数用户 ID、推荐数量、算法类型、近邻数、输出路径。这样换用户、换参数都不用打开编辑器。import argparse import pandas as pd def parse_args(): parser argparse.ArgumentParser(descriptionMovieLens 协同过滤推荐) parser.add_argument(--user-id, typeint, requiredTrue, help目标用户 ID) parser.add_argument(--top-n, typeint, default10, help推荐电影数量默认 10) parser.add_argument(--model, choices[user, item], defaultitem, help推荐算法user 或 item默认 item) parser.add_argument(--top-k, typeint, default10, help近邻数量默认 10) parser.add_argument(--output, typestr, defaultrecommendations.csv, help推荐结果输出路径) return parser.parse_args()参数说明--user-id 是必填参数没有指定就报错退出--model 用 choices 限定成 user 或 item避免手滑输错--top-k 和 --top-n 分离设计方便单独观察近邻数对结果的影响。if __name__ __main__: args parse_args() # 加载数据与矩阵省略已展示过的代码 matrix build_user_item_matrix() movies_df load_movies() # 根据 model 参数选择算法 if args.model user: rec_indices user_cf_recommend(args.user_id, args.top_k, args.top_n) else: rec_indices item_cf_recommend(args.user_id, args.top_k, args.top_n) # 把电影索引转回电影标题 rec_movie_ids matrix.columns[rec_indices] rec_titles movies_df[movies_df[movieId].isin(rec_movie_ids)][title].tolist() print(rec_titles)这个入口的灵魂在于把数据加载和推荐逻辑分离。数据加载只在主函数里执行一次推荐函数不关心数据从哪来只接收矩阵和参数这样单测也好写。6.2 导出 CSV把推荐结果接到其他系统推荐结果光打印在终端上不够落地到 CSV 才能接到其他系统或者做人工审核。导出时加上推荐得分和排名方便后续分析。def export_recommendations(rec_indices, matrix, movies_df, user_id, output_path): rec_movie_ids matrix.columns[rec_indices] rec_scores matrix.values[list(matrix.index).index(user_id), rec_indices] # 构造输出 DataFrame rec_df pd.DataFrame({ userId: user_id, movieId: rec_movie_ids, title: movies_df[movies_df[movieId].isin(rec_movie_ids)] [title].values, score: rec_scores, rank: range(1, len(rec_indices) 1) }) rec_df.to_csv(output_path, indexFalse, encodingutf-8-sig) print(f推荐结果已导出: {output_path})导出时用 utf-8-sig 编码是因为很多 Windows 上的 Excel 打开无 BOM 的 UTF-8 文件会乱码这是被逼出来的血泪经验。从那以后我每次拿到一套新的评分数据都会强制走一遍完整流程先跑数据加载和稀疏度打印确认数据特征再选算法训练后用时间切分做评估记录至少四维度指标推荐函数必须带冷启动兜底哪怕数据里没有冷启动用户也要留分支最后所有参数走 CLI 传参不许在源码里硬编码。这套习惯帮我挡掉了不少上线前的低级问题希望帮到你。本文还有配套的精品资源点击获取
返回列表