ARTICLE DETAIL

资讯详情

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

招聘薪资预测机器学习项目实战:从爬虫数据清洗到Django+Vue部署

招聘薪资预测机器学习项目实战:从爬虫数据清洗到Django+Vue部署 1. 为什么拿招聘薪资预测作为机器学习练手项目如果你在学校里学了一大堆机器学习理论什么线性回归、决策树、混淆矩阵背得滚瓜烂熟但一到自己动手做项目就懵那很正常。理论跟实战之间差着一条数据预处理的鸿沟而这恰恰是招聘网站、招聘数据分析最锻炼人的地方。我最初的想法很简单做一个求职信息数据分析系统重点是薪资预测。招聘网站上海量的岗位信息天然带结构化字段——职位名称、城市、经验要求、学历要求、公司规模、薪资范围这些数据标签整齐非常适合用来训练模型。更关键的是薪资预测不是一个纯学术问题它背后有真实的应用场景求职者想知道我这个条件在北京能拿多少钱HR想知道我开出的薪资在市场上有没有竞争力。技术栈也很直接后端用Django做Web框架前端用Vue机器学习部分用随机森林算法做薪资预测模型。这套组合覆盖了数据采集、数据清洗、特征工程、模型训练、系统开发的全流程做完一个项目你对整个Python生态的理解会上一个台阶。网上很多源码我后来也是主要参考了热门Python项目库里的招聘数据分析项目再根据自己的需求做了不少改造。这个项目最适合谁两类人。第一类是正在准备毕业设计的计算机专业学生第二类是学完机器学习基础但缺少完整项目经验的初学者。它不是那种照着教程敲一遍就完事的demo而是真的能跑起来、能看数据可视化图表、能输入条件得到薪资预测结果的一个完整系统。2. 数据是地基爬虫和数据集构建决定了模型的上限2.1 确定数据源和数据字段做机器学习项目第一步不是选算法而是搞数据。这一步做完后面模型的准确率什么样基本就定了八成。我从招聘网站采集的字段包括职位名称所在城市公司名称和规模工作经验要求学历要求薪资范围月薪区间职位发布时间职位描述摘要这里有个很关键的点招聘网站上的薪资大多是一个范围比如15k-25k不是单一数值。直接拿字符串训练模型是不可能的需要把区间转化成单一数值。我采用的是取中位数即(1525)/220这个20k就是该职位的薪资标签。有人可能会问为什么不取上限或下限取中位数最合理因为对于应聘者来说期望值通常落在区间中间偏上的位置对于企业来说中位数也能反映出市场行情的中心趋势。当然你也可以试一下上分位数、下分位数作为特征交叉验证但中位数作为基准已经很稳定了。2.2 数据爬取中的坑爬虫部分我用的是Python的requests加BeautifulSoup和Selenium组合。requests的缺点很明显——不少招聘网站有反爬机制一个IP在短时间内访问太频繁会被封。我的做法是设置随机的User-Agent加上time.sleep随机延时尽量模拟正常访问频率。如果遇到动态加载的页面就用Selenium渲染之后再去拿数据。数据量方面我最终积累了大约3万条有效岗位记录。这个量级对随机森林来说不算多但已经能出效果了。如果你在本地练习也可以先拿几千条跑通流程后面再逐步扩充。这里提醒一下爬虫内容合规是底线。不要爬取非公开的数据不要绕过登录和反爬机制去暴力抓取个人学习研究用公开页面信息即可。项目里要把源数据做脱敏处理防止个人信息泄露。2.3 数据清洗的三个关键动作清洗是脏活但不做的话后面的特征工程全是错的。我的清洗流程去重同一家公司的同一个职位会重复发布多条记录根据公司名职位名城市这三个字段去重。处理缺失值有些职位缺少学历要求或公司规模这些字段的缺失量不算大直接用未知填充。如果缺失比例超过30%这个字段果断丢弃。薪资异常值处理有些职位写的薪资明显异常比如月薪100k的保安、月薪2k的程序员这类样本会严重干扰模型训练用箱线图找出来把超过99分位数的全部剔除。清洗完的数据大概剩2.5万条左右质量明显变好了。3. 特征工程把文字变成机器能懂的数字做招聘薪资预测特征工程是整个项目里最有技术含量的一步比模型调参重要得多。原始数据里的职位名称学历要求都是文本必须量化。重点说几个我做过的特征构造思路3.1 职位类别特征职位名称这个字段的文本太杂比如高级Java开发工程师Java开发工程师银行项目资深Java架构师如果直接扔给模型分词处理又会引入大量噪音。更好的方式是做一个职位映射函数人工建立规则把职位归并成几个大类后端开发Java、Python、Go、C等前端开发Vue、React、Web前端算法工程师机器学习、深度学习、NLP、CV数据类数据分析、数据挖掘、数据工程师运维类运维、DevOps、SRE产品运营类、市场销售类、设计类映射完成之后每个职位都能得到一个类别id这就把一个高基数的文本特征压缩成了一个低基数的类别特征。后续加特征交叉也能在这个基础上做。3.2 城市等级特征我之前试过直接用城市名做类别标签结果模型泛化能力很差。后来换了一个思路城市分组。北上广深作为一线城市单独一组杭州、成都、武汉、南京这几个新一线一组省会城市一组地级市一组。这样分类的目的是降低维度同时传递一个更本质的信息——城市等级对薪资有结构性影响。3.3 经验要求编码经验要求常见的有经验不限1-3年3-5年5-10年10年以上这本来是有序类别直接映射成数值要有意义我用的映射规则是经验不限01-3年23-5年45-10年710年以上12这里取区间的中点而不是下限比如1-3年映射成2年而不是1这样更接近实际用人标准。3.4 学历要求编码学历要求用序数编码大专1本科2硕士3博士4。经验要求和学历要求这两个特征天然就是有序的直接映射数值是合理的。如果是无序类别特征比如公司行业做成哑变量我这里为了控制维度对高频类别用one-hot低频类别归并成其他。3.5 公司规模编码公司规模是0-20人20-99人100-499人500-999人1000-9999人10000人以上我用区间上限的对数值来编码。为什么用对数因为公司规模对薪资的影响是非线性的小公司每多20人对薪资影响很大大公司多500人对薪资影响很小用原始数值会让大公司规模特征的权重过大。这一套特征工程做完之后每个职位已经变成了一组数字向量接下来就可以喂给随机森林了。4. 随机森林薪资预测模型从训练到评估4.1 为什么选随机森林而不选线性回归招聘薪资和特征之间不是简单的线性关系。打个比方你在小城市的一个创业公司工作经验和学历很好但薪资未必比得上大厂应届生这就是特征之间的非线性交互。线性回归很难捕捉这种模式。随机森林的优势是不需要做特征标准化、天然处理非线性关系、对异常值不敏感、还能输出特征重要性。对于一个以能跑通、能解释为首要目标的教学项目来说几乎是最优选择了。XGBoost和LightGBM的效果可能会略好一点但调参难度更大而且对初学者不友好。先用随机森林跑通全流程再去试那些Boosting模型是更踏实的路线。模型跑出来之后我加了一组对比实验LightGBM的效果稍微好一点但随机森林胜在稳定和可解释性强最终交付选的还是随机森林。4.2 数据集划分和默认参数数据划分直接调scikit-learn的train_test_split测试集占比20%随机种子固定为42保证每次跑的结果一样方便对比实验。初始模型先用默认参数跑一遍基本效果不会差到哪里去。代码如下from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) model RandomForestRegressor(n_estimators200, random_state42) model.fit(X_train, y_train) y_pred model.predict(X_test) print(MAE:, mean_absolute_error(y_test, y_pred)) print(RMSE:, mean_squared_error(y_test, y_pred, squaredFalse)) print(R2:, r2_score(y_test, y_pred))我用默认参数跑的第一次结果R2大概在0.6左右平均绝对误差MAE4.2k。也就是说预测值和真实薪资平均差4200块钱这个精度已经能说明问题了——毕竟招聘网站上的薪资区间本身上下浮动就有5k甚至更多。4.3 网格搜索调参n_estimators和max_depth是关键我用的GridSearchCV调参重点关注三个参数n_estimators树的数量我从50试到300步长50max_depth树的最大深度从5到20min_samples_split内部节点再划分所需的最小样本数从2到10最终优化后的参数是n_estimators250max_depth14min_samples_split5min_samples_leaf2。这里有一个值得注意的细节当n_estimators超过200之后模型的性能提升幅度会变得很小这个时候继续增加树数量只会让训练时间变长收益不大。max_depth设得太大容易过拟合设得太小欠拟合14这个值是在交叉验证结果上折中出来的。R2从0.6提升到了0.7左右MAE降到了3.8k。对一个薪资预测任务来说这个水平已经可以拿得出手了。4.4 特征重要性排序意料之中又有点意外训练完之后我打印了特征重要性排名第一的是城市等级这点不意外一线城市和新一线的薪资差距确实是决定性的。排名第二是经验要求也合理。有点意外的是职位类别特征排名并不靠前后来想想也对——同样是后端开发初级和资深的薪资差距比后端和算法的平均薪资差距大得多。这个特征重要性结果除了能帮你理解模型还很有实用价值你在做可视化系统的时候可以从特征重要性最高的几个维度入手做城市薪资分布图、经验-薪资趋势图用户在看这些事情的时候会觉得信息密度特别高。5. Django后端把模型封装成Web接口5.1 项目分层设计系统层面前后端分离Django只负责提供API接口和数据管理Vue负责页面展示。具体来看Django侧我用的是标准的rest_framework架构。模型文件直接加载训练好的pkl放在一个独立的service模块里和业务逻辑解耦。核心目录结构backend/ ├── data_analysis/ │ ├── views.py # 视图函数接收请求并返回数据 │ ├── urls.py # 路由配置 │ └── models.py # 数据模型职务信息表 ├── ml/ │ ├── salary_model.pkl # 训练好的随机森林模型 │ ├── predict.py # 薪资预测服务 │ └── feature_engineering.py # 特征构造逻辑 └── manage.py5.2 核心接口设计我设计了三个主要APIGET /api/jobs/返回岗位数据列表支持按城市、职位类别、学历、经验筛选配合分页返回。GET /api/statistics/返回统计数据用于前端展示包括薪资分布直方图、城市平均薪资、经验薪资曲线等。POST /api/predict/接收前端传来的岗位特征城市、职位类别、经验、学历、公司规模调用模型预测薪资范围。预测接口的核心逻辑是这样的import joblib import numpy as np model joblib.load(ml/salary_model.pkl) def predict_salary(city_level, job_category, experience, education, company_size): features np.array([[city_level, job_category, experience, education, company_size]]) salary model.predict(features)[0] # 转成范围方便前端展示 lower int(salary * 0.85) upper int(salary * 1.15) return {min: lower, max: upper, mid: int(salary)}预测结果为什么要转成范围而不是直接给一个精确值因为任何模型预测都有误差给一个区间会显得更诚实也更符合用户的预期——用户本来就习惯看到15k-20k这种表达方式。同时这个操作也把模型的不确定性隐藏在了合理的置信区间里。5.3 跨域问题处理前后端分离项目第一个坑就是跨域。我用的是django-cors-headers在settings.py加上配置把前端地址加进白名单CORS_ALLOWED_ORIGINS [ http://localhost:8080, ]这个配置看起来简单但如果你不设白名单直接开CORS_ALLOW_ALL等于把后端的API暴露给了所有域名安全性会有隐患。本地项目无所谓部署上线时必须收紧。5.4 数据库选型我用的SQLite做开发环境零配置开箱即用对这个小项目完全够。如果你需要跑在CentOS服务器上并且数据量上来之后可以换成MySQLDjango的ORM层切换成本很低。6. Vue前端数据可视化和交互设计6.1 技术栈和项目初始化前端用Vue 3加Vite加Element Plus图表用的是ECharts。Vue 3的组合式API写起来非常舒服代码复用比Vue 2的Options API清晰很多。frontend/ ├── src/ │ ├── views/ │ │ ├── Dashboard.vue # 数据总览大屏 │ │ ├── JobTable.vue # 岗位信息表格 │ │ └── Predict.vue # 薪资预测工具 │ ├── api/ │ │ └── index.js # axios封装和接口定义 │ └── App.vue6.2 数据可视化大屏Dashboard.vue是整个系统最有视觉冲击力的部分。我用ECharts做了以下图表全国岗位薪资热力图以城市为中心用地图形式展示平均薪资颜色越深薪资越高。职位类别人数分布饼图展示后端、前端、算法等类别的岗位数量占比。经验-薪资折线图横轴是经验区间纵轴是平均薪资直观看到职业成长曲线。学历-薪资柱状图对比不同学历对应的薪资中位数。可视化本身项目里有很多现成的代码可以参考但我的经验是图表的颜色搭配要有统一主题色不要每个图表一个风格。另外ECharts的初始化要放到onMounted里执行组件销毁的时候记得调用dispose清理实例不然会有内存泄漏问题。6.3 薪资预测表单交互Predict.vue是一个表单页面用户选择城市、职位类别、经验要求、学历、公司规模点击预测薪资按钮前端发POST请求到后端拿到结果后用卡片展示薪资区间。这个页面有两个体验细节值得注意一是表单字段全部用下拉选择器不要用文本框。用户输入的自由文本根本无法直接做特征映射下拉选择既保证了数据规范性又提高了输入速度。二是把预测结果和同城市同类职位的平均薪资放在一起展示用户会更容易理解预测结果。别忘了用户的真实需求不是看一个孤立的数字而是想知道自己处于什么水平。7. 部署上线和项目强化的进阶思路7.1 本地部署步骤部署的流程比较标准化。后端用uwsgi加nginx前端打包之后放在nginx的静态目录里用一个location块把API请求代理给后端npm run build构建出来的dist目录直接扔到nginx的html目录下配置文件核心块如下server { listen 80; server_name your_domain; location /api/ { proxy_pass http://127.0.0.1:8000; } location / { root /var/www/html/dist; index index.html; } }这里有个坑Vue的history路由模式刷新页面会404必须在nginx配置里加上try_files参数。不然用户点到一个子页面刷新一下整个页面就白屏了。解决办法location / { root /var/www/html/dist; index index.html; try_files $uri $uri/ /index.html; }7.2 项目展示的亮点包装简历或者毕业设计答辩的时候光是能跑起来不够你的项目必须要有亮点可讲。我建议你在完成基础功能后选其中一个方向做强在特征工程上加一个职位描述关键词特征用TF-IDF提取职位描述里的高频词比如高并发分布式微服务算法优化这些词对薪资有显著影响。在模型上做一次对比实验把线性回归、随机森林、XGBoost的结果放在表格里对比说明为什么选随机森林这会让你的报告格外有说服力。在系统功能上加入薪资异常检测模块用模型计算每个岗位的理论薪资然后判断这个岗位是虚高还是偏低实用性市场上很看重。7.3 数据更新的运维思路招聘数据是时效性很强的上个月的模型和这个月的市场行情已经有偏差。模型在训练完之后至少要留一个数据更新的入口。我建议要么用管理后台导入CSV数据要么单独写一个爬虫更新脚本定期入库再跑模型。虽然这不影响系统的演示效果但会让你对模型上线后的生命周期管理有一个真实的体感面试官问到了你也能接得住。8. 实际效果和踩坑记录最终系统跑通之后整个流程的体验还是很直观的进入Dashboard页面能看到全国热门城市的薪资分布点进JobTable查看具体的岗位信息再到Predict页面选择自己模拟的条件点击一下马上能看到预测薪资区间。整体效果用几个数字来说明模型R2约0.7平均绝对误差约3.8k预测接口单次请求耗时约180ms前端页面首屏加载时间约1.2s。对一个课程设计级别的项目来说这个完成度已经算是比较能打的。最后说说我在这个项目里踩到的几个具体的坑pkl模型版本兼容问题。我在本地用scikit-learn 1.2训练的模型部署到服务器上发现scikit-learn版本不一致加载报错。解决办法是统一两边环境版本或者要求服务器上装的包和训练环境一致。用joblib.dump保存模型文件时一定要把scikit-learn版本号记下来。ECharts数据格式不匹配。后端返回的数据结构是Python字典列表前端ECharts要求特定的格式如果你不做好数据转换图表就是空的。解决方法是前端在拿到数据后先做一层map操作适配不要指望后端专门配合前端格式。随机森林模型预测值偏保守。随机森林的一个固有特点是预测值会向训练数据的均值回归也就是说高薪职位预测值往往偏低、低薪职位预测值偏高。你不用太纠结这一点展示的时候说明这个特性就行这也是我们刚才说的把结果输出成区间的一个额外理由。整个项目耗时约三周其中数据处理花了四天模型搭建五天前后端开发五天联调测试三天。走的弯路不少但也正因为踩过这些坑我对整个数据分析和机器学习项目开发流程的认知才算真正建立起来了。如果你也打算从零做一套类似的系统我建议重点把你的时间分配给数据清洗和特征工程模型反而是整个项目里最省心的一环。
返回列表