ARTICLE DETAIL

资讯详情

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

Python职位推荐系统:协同过滤与Flask服务完整实现

Python职位推荐系统:协同过滤与Flask服务完整实现 简介一套完整的Python职位推荐系统实现方案面向具备Python基础、希望深入理解推荐算法工程化落地的开发者和学生。方案涵盖用户冷启动处理、基于物品与用户的协同过滤itemCF_IUF、userCF_IIF、相似度计算、结果评估以及可视化展示并包含数据库操作和结果导出脚本可直接运行或二次开发。压缩包共79个文件包括47个Python源码、17张运行生成的图表、5个HTML展示页面、4个Markdown说明文档、4个CSV推荐结果、1份流程说明Word文档及1个配置文件整体仅942KB目录按多次迭代版本组织便于对照算法调优过程。已有1130人学习使用适合作为课程设计、毕业设计或推荐系统入门参考能帮助读者快速搭建一套可复用的职位推荐流程。1. Python职位推荐系统一次打通数据、算法与服务的完整实现海投简历的人最郁闷的不是能力差而是不知道哪些职位真正适合自己筛简历的HR则反过来被不匹配的简历淹没。职位推荐系统要解决的就是让求职者看到对路的职位让职位找到对路的人。这套基于Python的职位推荐系统核心不是某个黑匣子算法而是把用户与职位的交互行为用协同过滤转成可解释、可落地的推荐结果。整个项目顺着真实业务链路走pandas清洗行为日志成评分矩阵numpy与sklearn算相似度Flask把推荐结果包成接口一次覆盖Python数据分析与可视化、基础算法和Web服务三个方向。适合做课程设计、毕业设计或者想把推荐算法变成完整系统的Python开发者。有Python基础语法就能跟下来pycharm或vscode配好解释器就能跑。2. 系统设计与数据准备职位行为数据如何变成可计算的矩阵2.1 推荐系统整体架构与选型理由我习惯把职位推荐拆成两层离线计算层和在线服务层。离线层负责读行为日志、清洗数据、计算相似度矩阵在线层只做一件事——接收用户ID查矩阵聚合打分返回Top N。这样分的理由很直接协同过滤的相似度计算是O(n²)起步如果每个请求都现场算一遍用户一多接口必然变慢。离线算好、在线查表是这个场景下的默认做法。选型上我会优先用协同过滤而不是深度模型。原因不是深度学习不好而是职位推荐场景里行为数据通常很稀疏一个用户一天浏览的职位数量有限正样本本来就少。协同过滤只需要“用户-行为-职位”三列数据就能运转而且推荐结果可以解释成“因为你看过Python开发职位所以推荐相似的岗位”这种解释性在业务侧非常值钱。深度模型可以留到后期做排序第一版召回阶段没有必要给自己加复杂度。这个项目里我设计了两张核心表表名关键字段用途user_job_behavioruser_id, job_id, behavior, timestamp构建用户-职位评分矩阵job_infojob_id, title, city, salary_min, salary_max, tags职位画像与内容相似度行为字段建议至少区分浏览、收藏、投递三种投递权重最高收藏次之浏览最低。这个比例不需要精确到什么程度但量级必须拉开否则“随便看看”和“真心想去”在分数上拉不开差距。2.2 行为数据预处理从原始日志到评分矩阵数据准备工作里最绕不开的是把行为日志变成评分矩阵。我一般会先做三件小事解析时间字段、行为转权重、按用户加职位去重。import pandas as pd # 读取原始行为日志 df pd.read_csv(user_job_behavior.csv) df[timestamp] pd.to_datetime(df[timestamp]) # 行为权重浏览 0.5 / 收藏 1.0 / 投递 2.0 behavior_weight {view: 0.5, collect: 1.0, deliver: 2.0} df[weight] df[behavior].map(behavior_weight) # 同一用户对同一职位重复行为取权重最大的那次 df df.groupby([user_id, job_id], as_indexFalse)[weight].max() # 过滤行为数过少的用户阈值设为 5 user_active df.groupby(user_id)[job_id].nunique() valid_users user_active[user_active 5].index df df[df[user_id].isin(valid_users)]这段代码里解析时间字段是为了后面按时间切分训练集和验证集防止模型偷看“未来的行为”。groupby取max是为了避免用户反复看同一个职位导致权重被重复叠加一个职位对一个人来说只看“最强烈的行为”就够了。过滤活跃用户则是为了保证每个用户至少有几个行为记录否则连余弦相似度都算不出来。参数说明行为权重0.5/1.0/2.0是按“决策成本”设计的投递比浏览贵得多过滤阈值5是个经验值数据量大可以提到10数据少就降到3我平时用5比较多。清洗完之后用一行pivot_table把长表转成宽表# 用户-职位评分矩阵缺失值填 0 user_job_matrix df.pivot_table( indexuser_id, columnsjob_id, valuesweight, fill_value0 ) print(user_job_matrix.shape)到这里数据准备阶段的核心产物就齐了一张用户-职位评分矩阵一份职位画像向量。后面算法部分吃的就是这两份数据矩阵喂给协同过滤画像向量留给冷启动和混合推荐。接下来进入核心环节把这两份数据变成真正的推荐列表。2.3 职位画像构造文本特征向量化职位推荐系统和电商推荐有个明显差异新职位上架时没有任何用户行为协同过滤根本没法推荐它。这时候需要职位画像兜底。职位表里的tags字段通常长这样“Python,爬虫,后端”把它拆成多热编码向量后续才能算内容相似度。from sklearn.preprocessing import MultiLabelBinarizer jobs pd.read_csv(job_info.csv) jobs[tags_list] jobs[tags].str.split(,) mlb MultiLabelBinarizer() tag_vec mlb.fit_transform(jobs[tags_list]) tag_df pd.DataFrame(tag_vec, columnsmlb.classes_) print(tag_df.shape)这段代码把tags列拆成列表再用MultiLabelBinarizer转成0/1矩阵。比如“Python工程师”和“Python爬虫工程师”两个职位在“Python”这一列上都是1后续就能直接算内容相似度。这部分是给新职位冷启动和混合推荐准备的第6章里会用到。数据来源方面如果自己抓数据一般是用python爬虫去招聘站点采集职位列表但要注意脱敏和站点访问规则。我更建议在课程设计阶段用公开数据集伪造一部分行为日志先把流程跑通再接真实数据。很多人在这个环节就开始纠结数据量其实这个项目验证逻辑用几千条行为记录完全够用。3. 协同过滤核心实现UserCF与ItemCF的代码级对比3.1 算法原理与选型为什么职位推荐首选ItemCF协同过滤分两种基于用户的UserCF和基于物品的ItemCF。UserCF的直觉是“和你相似的人投了什么你也可能投”ItemCF的直觉是“你看过Python职位那其他Python相关职位也值得推给你”。对比维度UserCFItemCF基础矩阵用户-职位职位-用户相似度对象用户与用户职位与职位实时性要求用户相似度要实时算或频繁更新职位相似度离线算好在线查表冷启动表现新用户无行为基本没法推新用户有1次行为就能顺藤摸瓜计算成本用户量大时矩阵爆炸职位数相对可控职位推荐场景有一个典型特征候选职位数量远小于活跃用户数量。招聘平台职位可能只有几万个活跃用户却是百万级。所以ItemCF的相似度矩阵规模更可控推荐结果解释性也更强是我在这个项目里的首选。用Python实现时两者的代码差异其实就一个转置的距离但工程落地的体验差很多。3.2 UserCF实现用户相似度计算与推荐聚合先看UserCF。相似度一般用余弦相似度对稀疏行为矩阵来说它衡量的是两个用户行为向量的夹角重合的职位越多向量方向越接近。import numpy as np import pandas as pd from sklearn.metrics.pairwise import cosine_similarity # 计算用户-用户相似度矩阵 user_sim cosine_similarity(user_job_matrix) user_sim_df pd.DataFrame( user_sim, indexuser_job_matrix.index, columnsuser_job_matrix.index ) def recommend_by_user(user_id, recall_users10, top_k10): # 和该用户最像的N个用户 sim_scores user_sim_df[user_id].sort_values(ascendingFalse) sim_scores sim_scores.drop(user_id) # 聚合相似用户的职位得分 rec_scores {} for uid, sim in sim_scores.head(recall_users).items(): items user_job_matrix.loc[uid] for job_id, weight in items[items 0].items(): rec_scores[job_id] rec_scores.get(job_id, 0) sim * weight # 过滤已交互职位 watched user_job_matrix.loc[user_id] watched watched[watched 0].index ranked sorted(rec_scores.items(), keylambda x: x[1], reverseTrue) ranked [(job_id, score) for job_id, score in ranked if job_id not in watched] return ranked[:top_k]逻辑说明先算用户相似度矩阵再对目标用户取前N个相似用户把这些用户产生过行为的职位按相似度加权汇总最后剔除目标用户已经看过、收藏过、投递过的职位。关键点在权重设计上相似度0.9的用户投递的职位得分会是相似度0.3用户的3倍这样推荐列表才不会被弱关系用户带偏。参数说明recall_users是召回用户数默认10top_k是最终推荐条数。召回数太大会引入不相似用户的噪声太小则覆盖不足。这个参数我一般从5开始试观察推荐列表的多样性再逐步调整。UserCF的坑也很明显用户规模到10万级时用户相似度矩阵是10万乘10万稠密存储会直接吃光内存。所以实际项目里我几乎不会用纯UserCF做在线服务更多是把它作为冷启动期的实验基线。3.3 ItemCF实现离线相似矩阵与在线查表ItemCF的做法是把矩阵转置按职位算相似度。和UserCF最大的区别在于职位相似度矩阵可以离线算好服务启动时加载一次就行。# 转置矩阵行职位列用户 job_user_matrix user_job_matrix.T job_sim cosine_similarity(job_user_matrix) job_sim_df pd.DataFrame( job_sim, indexjob_user_matrix.index, columnsjob_user_matrix.index )接下来是推荐聚合。对用户交互过的每个职位找出和它最相似的N个职位按用户对该职位的兴趣权重加权def recommend_by_job(user_id, item_neighbors10, top_k10): items user_job_matrix.loc[user_id] watched items[items 0].index rec_scores {} for job_id, weight in items[items 0].items(): sim_scores job_sim_df[job_id].sort_values(ascendingFalse) sim_scores sim_scores.drop(job_id) for nb, sim in sim_scores.head(item_neighbors).items(): if nb in watched: continue rec_scores[nb] rec_scores.get(nb, 0) weight * sim ranked sorted(rec_scores.items(), keylambda x: x[1], reverseTrue) return ranked[:top_k]逻辑说明外层循环遍历用户交互过的职位内层循环取每个职位的Top N相似职位。用户对原职位的权重乘以职位间相似度得到候选职位得分。用户投递过的职位权重是2.0它带出来的相似职位得分自然比浏览带出来的高。这里有一个很实用的优化热门职位降权。如果一个职位被几千人投递过它几乎和所有职位都“相似”会导致热门职位频繁霸榜。业界常用IUF逆用户频率来惩罚# 每个职位被多少个不同用户交互过 job_freq df.groupby(job_id)[user_id].nunique() job_penalty 1 / np.log1p(job_freq) # 在聚合循环里乘上惩罚系数 score weight * sim * job_penalty.loc[nb]log1p保证了分母不为0惩罚系数随交互人数平滑下降既压了热门职位又不会让长尾职位完全消失。这个细节加上之后推荐列表的“惊喜感”会明显改善属于性价比很高的一行代码。4. 推荐服务落地Flask接口与离线算好的相似度矩阵4.1 Flask接口设计请求参数与返回格式前面算好了job_sim_df接下来把它变成能用的HTTP服务。选Flask的理由很直接这里只需要推荐接口和行为上报接口两个路由不需要重型Web框架Flask在Python社区里是最常见的方案部署也省心。from flask import Flask, request, jsonify app Flask(__name__) # 启动时加载离线计算好的模型文件 user_job_matrix load_user_job_matrix(data/user_job_matrix.csv) job_sim_df load_job_sim(data/job_sim.npy, data/job_index.csv) app.route(/recommend, methods[POST]) def recommend(): payload request.get_json() user_id str(payload[user_id]) top_k int(payload.get(top_k, 10)) if user_id in user_job_matrix.index: result recommend_by_job(user_id, job_sim_df, user_job_matrix, top_ktop_k) else: result recommend_by_popularity(top_k) return jsonify({user_id: user_id, top_k: top_k, jobs: result})逻辑说明这里做了冷启动分支老用户走ItemCF新用户走热门推荐。推荐接口返回的是职位ID和得分列表职位标题、城市、薪资这些展示信息由前端再查一次职位表补齐。这样设计的好处是推荐服务只关心“算哪些职位”展示逻辑完全解耦。参数说明top_k默认10业务上推荐列表一轮展示10到20个比较合适超过20个用户的浏览深度基本达不到。接口用POST是因为请求体是JSON后续要加用户偏好标签、地域过滤等复杂参数时GET的URL长度会成为限制。提示user_id必须统一转成字符串。行为表里是int接口传入的JSON里可能是字符串不转的话“1”和1会被当成两个完全不同的用户推荐结果会莫名其妙漏掉一大片。4.2 离线计算与在线服务分离矩阵缓存与更新策略在线服务启动时加载离线算好的矩阵但矩阵本身不能永远不更新。我一般会写一个offline_update.py里面重放数据清洗和相似度计算逻辑把结果存到磁盘在线服务只负责读文件。# offline_update.py import numpy as np import pandas as pd from sklearn.metrics.pairwise import cosine_similarity def main(): df pd.read_csv(data/user_job_behavior.csv) # 复用第2章的清洗逻辑得到 user_job_matrix user_job_matrix build_matrix(df) # 计算职位相似度并保存 job_sim cosine_similarity(user_job_matrix.T) np.save(data/job_sim.npy, job_sim) pd.Series(user_job_matrix.columns).to_csv(data/job_index.csv, indexFalse) if __name__ __main__: main()这个脚本的意义在于把“重计算”和“对外服务”彻底分开。线上服务启动时用load_job_sim读取npy文件请求进来直接查矩阵响应时间基本都在毫秒级。职位数量1万时相似度矩阵是1万乘1万的float32数组占内存约400MB单机可以承受超过5万职位时建议改用annoy这类近邻索引稠密矩阵在内存和查询耗时上都会失控。定时更新用cron最简单的做法# 每天凌晨2点重算输出日志方便排查 0 2 * * * cd /opt/job-recommend python offline_update.py logs/update.log 21重算频率按职位活跃度调整。职位库变化慢就一天一次变化快就一小时一次。日志一定要留后面矩阵文件损坏、数据源抽风时全靠它定位问题。4.3 行为回流让下一次推荐更准没有行为回流的推荐系统是死系统。用户今天投递了什么职位明天的推荐就应该避开或加强同类。所以推荐服务里必须有一个行为上报接口app.route(/event, methods[POST]) def report_event(): data request.get_json() user_id str(data[user_id]) job_id str(data[job_id]) behavior data[behavior] # view / collect / deliver ts data[timestamp] insert_event(user_id, job_id, behavior, ts) return jsonify({status: ok})上报接口把行为写入MySQL或追加到行为日志文件离线更新脚本每天重算时把新行为纳入计算用户下一次请求就能拿到更新后的推荐。投递行为权重调成2.0的意义在这里体现得最明显它是用户用行动投票必须在聚合时权重最高。5. 避坑指南职位推荐系统开发中五个典型的翻车现场5.1 矩阵与相似度计算的坑稀疏、类型、内存坑1相似度全为0推荐结果返回空列表。现象接口正常响应但recommendations是空数组前端渲染出来一片空白。原因行为数据太稀疏余弦相似度对全零向量返回0或者热门职位降权把低频职位权重直接压成了0。解决先检查user_job_matrix的非零元素比例低于1%就先做数据增强比如把浏览行为也纳入评分降权系数用1 / np.log1p(freq)平滑不要直接除以频次计算相似度时对分母加一个极小值避免除零。坑2user_id类型不一致pivot_table结果全是NaN。现象矩阵形状是0行或者所有值都是NaN推荐结果匹配不上职位信息。原因行为表里user_id被读成int接口传入的却是字符串pandas索引匹配时两者完全对不上。解决所有ID统一转字符串。我在数据清洗第一行就写df[user_id] df[user_id].astype(str)这个习惯能省掉后面很多玄学问题。坑3用户一多内存直接爆掉。现象20万用户跑UserCF16G内存的机器直接卡死。原因UserCF要装下20万乘20万的相似度矩阵稠密float64就是320G单机根本不可能。解决改ItemCF职位数量通常是用户数量的几十分之一存储上把评分矩阵换成scipy.sparse的csr_matrix如果确实要用UserCF就分块计算或者换faiss近似近邻。5.2 数据时序与冷启动的坑评估失效与热门推荐失控坑4离线评估指标很漂亮上线效果一塌糊涂。现象验证集上精确率、召回率都很好看线上用户反馈却很一般。原因数据没按时间切分训练集里混入了用户未来的行为相似度矩阵“偷看”了答案这属于典型的评估集构造错误。解决行为数据按timestamp排序后前80%做训练后20%做验证验证集里的职位如果出现在训练集中计算命中时要格外小心至少要把用户已交互的职位全部排除出负样本候选。坑5冷启动用户收到一排“无关爆款职位”。现象新注册用户没有行为记录推荐接口返回的是全站最热门的几个职位推过去用户根本不点。原因冷启动直接回退到“全站热门”没有利用用户注册时填写的城市、期望岗位、学历这些信息。解决热门推荐也要加硬规则过滤城市匹配、薪资区间匹配、学历要求匹配再来一层内容相似度用注册时勾选的职位标签做向量召回。推荐服务里加一个规则管道先过滤再排序顺序不能反过来。6. 进阶评估离线指标与混合推荐帮你把参数调准参数调着调着就变成玄学所以推荐质量必须用指标说话。我一般固定跑三个离线指标精确率、召回率、覆盖率。def evaluate(predictions, ground_truth, k10): hits 0 pred_count 0 for user, pred in predictions.items(): pred pred[:k] hits len(set(pred) set(ground_truth[user])) pred_count len(pred) precision hits / pred_count if pred_count else 0 recall hits / sum(len(v) for v in ground_truth.values()) if ground_truth else 0 # 覆盖率推荐结果覆盖的职位数占全部职位数的比例 covered len(set(job for jobs in predictions.values() for job in jobs[:k])) coverage covered / total_job_num return precision, recall, coverage精确率衡量推荐列表里有多少是用户真实投递的召回率衡量用户投过的职位里有多少被推荐到了覆盖率衡量推荐列表是否只盯着少数热门职位。三个指标要一起看只追精确率会让推荐越收越窄最后只剩那几类热门职位。混合推荐把ItemCF和内容相似度合并起来两个信号互补ItemCF管“和你投过的职位相似”内容相似度管“新职位和你的标签匹配”。final_score 0.6 * itemcf_score 0.4 * content_score权重怎么定我一般先把ItemCF权重固定成0.7内容权重从0.1开始往上加每加一档重算一次召回率找曲线的拐点。两个权重一起调的话结果变了根本不知道是谁的锅这是我调过的血泪教训。项目包里的代码就是按这三步组织的清洗、算矩阵、封装接口文档里连数据集字段说明都留了照着跑一遍就能复现出文中的推荐结果。从那以后我每次做推荐系统都强制先花30分钟做数据检查user_id类型、时间顺序、活跃用户分布这三项过一遍再碰算法。希望帮到你。本文还有配套的精品资源点击获取
返回列表