尧图精选

Python数据分析实战:招聘数据清洗与可视化大屏全流程

🕒 发布时间:2026/10/2 3:20:57 📁 来源:尧图网络
很多人学Python数据分析卡在一个尴尬的位置教程看了一堆pandas常用函数也能背出来但真拿到一份现实数据还是不知道从哪下手。我之前也有这个阶段直到自己完整做完一个招聘数据分析项目才算把数据处理→分析建模→可视化展示这条链路彻底跑通。这个项目做的事情不算复杂拿一份脱敏后的招聘岗位数据用pandas做清洗和分析找出薪资、城市、经验、学历、行业之间的规律最后用pyecharts拼出一块可以演示的可视化大屏并配好源码、设计文档和调试笔记。它非常适合想积累完整数据分析项目经验的人应届生可以用它来丰富简历里的项目经历后端或全栈开发也可以从中参考大屏的实现方式。下面我把整个项目从0到1的完整过程展开讲包含技术选型逻辑、数据清洗细节、指标设计思路、大屏实现方法以及我实际踩过的五个正经坑。1. 项目定位与整体架构1.1 为什么我选招聘数据做练手项目先回答一个很多人会问的问题练手数据集那么多titanic、iris、电商订单都不错为什么非要用招聘数据因为招聘数据在字段丰富度和清洗难度上卡得刚刚好。一条招聘岗位记录里通常包含岗位名称、城市、薪资范围、经验要求、学历要求、公司类型、公司规模、行业领域、技能标签等多个字段。这些字段覆盖了数据分析最常见的处理维度分析数值字段要用聚合和分组分析分类字段要做归一化和编码分析文本字段要用词频统计分析多字段关系要做交叉透视。一套流程走下来几乎把pandas和可视化库的常规操作全过了一遍。更关键的是招聘数据的分析结论贴近真实业务场景。titanic的分析结论很难落地但招聘数据的分析可以直接写成求职思路哪个城市的数据岗位薪资更高经验从1-3年到3-5年的薪资涨幅有多大硕士学历的溢价值不值得用两年时间去换。这种能讲出业务价值、能引发讨论的项目在面试和简历里都很有说服力。1.2 技术栈选型的思路项目用到的核心工具不算多但每个模块的选型我都想过为什么不用替代品环节选用方案备选方案选型理由数据处理pandas纯Python做list/dict计算pandas的groupby、pivot_table在交叉分析时效率高太多数据获取样例CSV脱敏模拟数据requestsBeautifulSoup爬虫项目重点在分析链路样例数据可控可复现不涉及平台访问压力可视化pyechartsMatplotlib、Plotlypyecharts生成的是ECharts配置浏览器展示效果好适合做大屏Web托管FlaskDjango、纯静态HTMLFlask轻量模板渲染和接口返回JSON都方便一个文件就能启动文档MarkdownWord、在线文档和代码放在一起git管理方便这里有一个我自己坚持的原则不是每个环节都要用最复杂的技术。爬虫可以写但为了让项目更聚焦我用了脱敏后的模拟数据字段设计完全对齐真实招聘平台的数据结构。如果你后续想换成真实数据爬虫模块可以直接替换掉样例数据加载逻辑分析、可视化部分完全不用改。1.3 交付清单与目录结构标题里写了源码文档调试可视化大屏我把这四块都作为独立交付物来规划。最终的工程目录长这样recruitment-analysis/ ├── data/ │ └── recruitment_sample.csv # 脱敏样例数据 ├── src/ │ ├── data_clean.py # 数据清洗模块 │ ├── analysis.py # 分析指标计算模块 │ ├── visualize.py # 图表生成模块 │ └── app.py # Flask入口 ├── output/ │ ├── charts/ # 图表输出目录 │ └── dashboard.html # 大屏静态HTML ├── docs/ │ ├── design.md # 设计文档 │ └── debug.md # 调试笔记 ├── requirements.txt └── README.md这样拆的好处是每一块都能单独运行和验证。清洗脚本单独跑一遍输出的干净数据存成cleaned_data.csv分析脚本读取清洗后的数据输出指标JSON和图表文件Flask启动后把图表拼进大屏模板。任何一步出了问题都能快速定位在哪个模块这也是后期调试效率高的原因。2. 数据准备与清洗2.1 数据字段设计与合规说明先声明一点项目里使用的数据是脱敏模拟数据字段结构参考了招聘平台的公开信息但数值和文本都做了随机化偏移不涉及任何真实个人隐私和企业敏感信息。如果你打算自己采集数据建议优先使用平台官方开放的API或公开数据集如果使用爬虫务必控制访问频率、遵守目标站点的访问条款并仅用于个人学习研究。我用到的数据字段设计如下字段名含义示例清洗后目标job_title岗位名称Python开发工程师保留原值city工作城市北京-海淀区提取北京salary薪资描述15-20K·14薪拆成low/high/avg三列experience经验要求3-5年映射成经验年限区间education学历要求本科映射成学历等级分company_type公司类型民营保留company_size公司规模500-2000人映射成人数数值industry所属行业互联网/电子商务提取主行业tags技能标签Python,MySQL,Flask拆成技能词列表2.2 薪资文本的拆解薪资字段是这个项目里最典型的脏数据。原始数据里常见这样几种写法15-20K·14薪、10-15K、6-8K·13薪、还有直接写面议的。如果直接拿这个字符串做排序或均值计算结果一定是错的必须拆开变成数值。我的拆解逻辑是先用正则提取数字下限和上限再单独提取月薪数最后统一换算成年薪和月薪均值。核心代码大概是这样的import pandas as pd import re def parse_salary(salary_str: str) - tuple: 将薪资文本拆成(下限月薪, 上限月薪, 月均月薪, 年薪) if not isinstance(salary_str, str) or salary_str.strip() in (面议, ): return (None, None, None, None) # 提取 15-20 中的 15 和 20 nums re.findall(r(\d(?:\.\d)?), salary_str) if len(nums) 2: return (None, None, None, None) low_k float(nums[0]) high_k float(nums[1]) avg_k (low_k high_k) / 2 # 提取 14薪 类似写法中的月份数 month_match re.search(r(\d)薪, salary_str) months int(month_match.group(1)) if month_match else 12 return ( low_k * 1000, high_k * 1000, avg_k * 1000, avg_k * 1000 * months ) df[[salary_low, salary_high, salary_avg, salary_year]] ( df[salary].apply(lambda x: pd.Series(parse_salary(x))) )面议的处理方式是置为空值并做标记。因为面议通常意味着薪资区间较大或需要根据人选定薪简单填均值反而会引入偏差。在后续分析中可以用行业均值填充或直接剔除具体看样本量。这个取舍一定要写在文档里否则别人看你的分析结果时会疑惑为什么样本数少了。2.3 分类字段归一化与清洗校验分类字段的清洗相对机械但细节很多。城市字段要处理北京-海淀区这种带区县的写法用split(-)[0]提取城市经验字段要把经验不限归为0把1-3年映射成中值210年以上映射成12学历字段映射成分级分值时我用的是不限1大专2本科3硕士4博士5方便后续计算学历与薪资的相关性。清洗完之后我写了一个check_data_quality函数做最终校验def check_data_quality(df: pd.DataFrame) - dict: report { 总记录数: len(df), 空值率: df.isnull().mean().to_dict(), 重复记录数: df.duplicated().sum(), 薪资范围: (df[salary_avg].min(), df[salary_avg].max()), 城市数量: df[city].nunique(), } return report这一步看着不起眼但特别值得养成习惯。清洗完不校验后面的分析就是对着一盘不知道干不干净的数据做菜结果可想而知。我当时跑完整套清洗后发现薪资区间最小值不到3000元一查是几条1-3K·24薪这类异常文本混了进来正则把它们拆成了离谱的值。后来加了一条过滤规则把salary_avg低于2000的记录单独放到异常清单里人工复核问题才解决。3. 分析指标设计3.1 先用业务问题倒推指标做数据分析最忌讳的事情是打开DataFrame看到什么就mean、sum、value_counts一顿乱算最后得到一堆数字但讲不出结论。正确做法是先想清楚这个项目要回答什么业务问题再由问题倒推需要的指标。我给这个项目定了五个问题哪些岗位薪资高、需求量也大不同城市的薪资水平和岗位机会差距有多大经验年限带来的薪资增长是不是线性的学历对薪资的影响有多大行业、公司规模和薪资之间有什么关系每个问题都能对应一个可以在求职中直接使用的结论。比如问题3的答案如果发现1-3年到3-5年的涨幅最大而5-10年到10年以上涨幅趋缓那准备跳槽的人就能有更理性的薪资预期。3.2 核心指标的计算实现指标计算用pandas的groupby和pivot_table就能完成不需要高深的算法。举几个关键实现岗位薪资与需求量TOP10这是大屏的核心图表之一。按岗位分组同时算平均薪资格和岗位数量再按平均薪资排序取前10。job_stats ( cleaned_df.groupby(job_title) .agg( 平均薪资(salary_avg, mean), 岗位数量(salary_avg, count) ) .sort_values(平均薪资, ascendingFalse) .head(10) .reset_index() )经验年限分箱与薪资曲线为了看经验对薪资的影响我先建一个经验中值列再按经验区间分组计算平均薪资同时算环比增幅。exp_levels [经验不限, 1-3年, 3-5年, 5-10年, 10年以上] exp_order pd.CategoricalDtype(categoriesexp_levels, orderedTrue) exp_salary ( cleaned_df.assign(经验cleaned_df[experience].astype(exp_order)) .groupby(经验, observedTrue)[salary_avg] .agg([mean, count]) .reset_index() ) exp_salary[环比涨幅] exp_salary[mean].pct_change() * 100这里有个小坑如果不设置orderedTruegroupby会按拼音或出现顺序排序图表上的横轴顺序就是乱的。学历溢价学历分成等级分之后按学历分组算平均薪资再算相邻学历之间的差值。edu_salary ( cleaned_df.groupby(education)[salary_avg] .agg([mean, count]) .reindex([不限, 大专, 本科, 硕士, 博士]) ) edu_salary[相邻学历溢价] edu_salary[mean].diff()行业与公司规模交叉表用pivot_table做二维交叉行是行业列是公司规模段值是平均薪资。这个表直接决定了大屏上的热力图有没有信息量。pivot pd.pivot_table( cleaned_df, valuessalary_avg, indexindustry, columnscompany_size, aggfuncmean, marginsTrue, )3.3 指标与图表的映射关系计算出来的指标不能直接堆在大屏上要先确定每一块图表要回答哪个问题。我习惯用一张表来梳理这个映射业务问题核心指标图表类型大屏位置城市机会差异各城市岗位数量、平均薪资柱状图左中部岗位供需与薪资岗位数量、平均薪资TOP10条形图左上部经验与薪资关系经验分箱平均薪资、环比涨幅折线图底部学历溢价各学历平均薪资、样本量柱状图饼图右中部行业回报差异行业平均薪资交叉公司规模热力图右上部这张映射表既是大屏的设计蓝图也是设计文档里的核心内容。我在实际做的时候是先画了这张表才开始写可视化代码而不是反过来。4. 可视化大屏架构与实现4.1 为什么最后选了pyecharts加HTML大屏一开始我也想省事把所有图表堆在Jupyter Notebook里。Notebook在探索分析阶段确实舒服但做展示有个致命问题它是线性滚动式的信息没有层级演示时观众容易迷失。可视化大屏的本质是信息面板扫一眼就能看懂全局。所以必须固定布局、统一配色、突出核心指标再配合局部图表联动。我用的是pyecharts生成ECharts配置把它嵌入到自制的HTML页面里再用CSS Grid做分栏布局。整个过程不依赖任何前端框架一个app.py加一个index.html就能跑起来。这里有个离线部署的细节ECharts的JS文件我会下载到本地static目录引用而不是用CDN。这样在无外网环境下大屏也能正常打开演示时不用赌网络状况。4.2 大屏的模块划分与布局大屏按1920x1080设计尺寸写死在CSS里缩放时通过transform: scale()整体适配。布局分成三层顶部大屏标题加4个核心指标卡片展示岗位总量、平均月薪、覆盖城市数、样本行业数。这4个数字是用Flask接口从清洗后的数据实时计算出来的不是写死的。中部分成左、中、右三栏。左栏放岗位薪资TOP10条形图和城市岗位数量TOP10柱状图中栏放职位薪资分布玫瑰图和行业占比饼图右栏放学历薪资对比柱状图和学历样本量饼图。底部放经验-薪资折线图带环比涨幅标注和热门技能词云。布局框架的CSS方式描述如下外层一个#dashboard容器用display: grid; grid-template-columns: repeat(6, 1fr); grid-template-rows: auto 1fr auto;顶部跨6列中部三栏各跨2列底部跨6列。每个图表容器固定高度内部用flex: 1让ECharts自适应。4.3 Flask托管与图表加载方式图表加载方式我试过两种。第一种是pyecharts从render_embed()输出HTML片段直接拼进Jinja2模板好处是代码简单坏处是数据和图表配置耦合在一起想单独刷新某一块图表比较麻烦。第二种是接口返回图表配置JSON前端用ECharts实例加载。我用的是第二种。核心逻辑是visualize.py里每个图表函数返回一个options字典Flask接口/api/chart/name把字典转成JSON返回前端页面在加载时调用ECharts的setOption渲染。# src/app.py from flask import Flask, render_template, jsonify from src.visualize import get_chart_options app Flask(__name__) app.route(/) def index(): return render_template(index.html) app.route(/api/overview) def overview(): return jsonify(get_overview_stats()) app.route(/api/chart/name) def chart(name): options get_chart_options(name) return jsonify(options) if __name__ __main__: app.run(host0.0.0.0, port8000, debugFalse)前端模板里每个容器div上会标注一个>
上一篇/下一篇内容由系统自动关联 返回资讯列表 →