公司动态

Python数据分析实战:从招聘数据清洗到可视化洞察

📅 2026/8/27 8:31:42
Python数据分析实战:从招聘数据清洗到可视化洞察
简介数据清洗与探索性分析EDA是数据分析流程中的核心环节旨在将原始、杂乱的‘脏数据’转化为可供分析的‘干净数据’。其原理在于通过系统性的数据审查、格式统一、缺失值处理及异常值检测确保数据质量与一致性为后续分析奠定可靠基础。这项技术的价值在于能够揭示数据背后的真实规律支撑业务决策。在职场数据分析等应用场景中常需处理如薪资范围、技能关键词等非结构化信息。本文以一份数据分析师招聘数据为例详细演示了如何运用Python的Pandas库进行数据清洗并利用Matplotlib进行可视化最终提炼出关于城市薪资分布、技能需求热点的关键洞察为求职者与招聘方提供数据驱动的市场地图。1. 项目缘起从一份招聘数据说起最近在帮一个做招聘平台的朋友梳理他们的数据他们手头有一份近期的数据分析师职位招聘数据大概300条左右。朋友想让我帮忙看看现在市场上数据分析师这个岗位到底是个什么情况比如哪些城市机会多、薪资水平怎么样、对技能有什么要求。这其实是一个特别典型的职场数据分析场景很多刚入行的朋友或者想转行做数据分析的人都特别需要这样一份“市场地图”来指导自己的学习和求职方向。我拿到数据一看是一个CSV文件字段还挺全的职位名称、公司名称、工作城市、薪资范围比如“15-30K·14薪”、学历要求、工作经验要求还有职位描述里藏着的技能关键词。数据量不大300行正好适合用Python来做一次完整的数据探索分析和可视化。数据量小有小的好处处理速度快可以快速迭代分析思路把每个分析点都做透非常适合作为学习和练手的项目。这个项目的核心就是用Python的Pandas进行数据清洗和探索性分析EDA然后用Matplotlib把分析结果直观地画出来。整个过程会涉及到数据清洗中的字符串解析比如拆解薪资字符串、数据分组聚合、统计描述以及多种类型的图表绘制包括柱状图、饼图、箱线图、词云等。通过这个项目你不仅能学会如何处理一份真实的、有点“脏”的职场数据更能掌握如何通过可视化把数据背后的故事讲清楚。这对于任何想从事数据分析工作的人来说都是一项必备的核心技能。下面我就带你一步步拆解这个项目。2. 数据初探与清洗把“脏数据”变成“干净数据”拿到原始数据的第一步永远不是急着画图而是先了解你的数据长什么样存在哪些问题。这个过程在数据分析里叫“数据探索性分析”的初始阶段核心是理解数据结构和数据质量。2.1 加载数据与概览我用的工具是Jupyter Notebook配合Pandas和Matplotlib。首先导入必要的库然后读取数据。import pandas as pd import numpy as np import matplotlib.pyplot as plt import re from wordcloud import WordCloud, STOPWORDS import jieba # 为了在图中显示中文需要设置中文字体 plt.rcParams[font.sans-serif] [SimHei] # 用来正常显示中文标签 plt.rcParams[axes.unicode_minus] False # 用来正常显示负号 # 加载数据 df pd.read_csv(data_analyst_jobs.csv) print(f数据集形状: {df.shape}) print(df.head()) print(df.info())运行df.info()后我立刻发现了几个典型问题薪资字段是一个字符串格式不统一比如“15-30K·14薪”、“20K以上”、“面议”。我们需要从中提取出最低薪资、最高薪资和薪资单位通常是K并计算一个代表值比如薪资中位数或平均值用于后续分析。工作经验字段可能是“经验不限”、“1-3年”、“3-5年”这样的字符串。为了便于分析最好将其转换为有序的类别或者提取出数字范围。职位描述是一大段文本里面包含了职位要求、技能关键词等。我们需要从中提取出如“Python”、“SQL”、“Tableau”等技能关键词。可能存在缺失值需要检查并决定如何处理。2.2 核心清洗解析薪资字符串薪资字段的清洗是这个项目的关键和难点。我们需要写一个函数来应对各种格式。我的思路是使用正则表达式匹配数字和模式。def parse_salary(salary_str): 解析薪资字符串返回一个字典包含min_salary, max_salary, avg_salary, salary_unit。 处理‘面议’、‘xxx以上’、‘xxx-yyyK·zz薪’等情况。 if pd.isna(salary_str) or salary_str 面议: return {min_salary: np.nan, max_salary: np.nan, avg_salary: np.nan, salary_unit: None} salary_str str(salary_str).upper().replace( , ) # 定义正则表达式模式 # 匹配如 15-30K 20-40K·14薪 pattern_range r(\d\.?\d*)\s*[-~]\s*(\d\.?\d*)\s*([K万]?) # 匹配如 20K以上 25万以上 pattern_single_with_suffix r(\d\.?\d*)\s*([K万]?)以上 # 匹配纯数字可能是K或万为单位需要结合上下文判断这里假设为K pattern_single r^(\d\.?\d*)\s*([K万]?)$ match re.search(pattern_range, salary_str) if match: min_s float(match.group(1)) max_s float(match.group(2)) unit match.group(3) if match.group(3) else K # 默认单位为K # 处理单位转换如果单位是‘万’转换为‘K’1万10K if unit 万: min_s * 10 max_s * 10 unit K avg_s (min_s max_s) / 2 return {min_salary: min_s, max_salary: max_s, avg_salary: avg_s, salary_unit: unit} match re.search(pattern_single_with_suffix, salary_str) if match: min_s float(match.group(1)) unit match.group(2) if match.group(2) else K if unit 万: min_s * 10 unit K # ‘以上’通常表示最低值最高值难以确定这里将最高值设为最低值的1.5倍一种估算平均值为两者均值 max_s min_s * 1.5 avg_s (min_s max_s) / 2 return {min_salary: min_s, max_salary: max_s, avg_salary: avg_s, salary_unit: unit} # 如果都不匹配尝试匹配纯数字 match re.search(r(\d\.?\d*), salary_str) if match: val float(match.group(1)) # 简单判断如果数字很大比如100可能是以‘元’为单位的月薪除以1000转为K。这里根据数据情况调整。 # 更稳妥的做法是看字符串中是否包含‘万’字但前面模式没匹配到。这里假设数据中纯数字多为K。 if val 100: # 假设是‘元’转为K val val / 1000 return {min_salary: val, max_salary: val, avg_salary: val, salary_unit: K} # 如果都无法解析返回NaN return {min_salary: np.nan, max_salary: np.nan, avg_salary: np.nan, salary_unit: None} # 应用函数到薪资列 salary_df df[salary].apply(lambda x: pd.Series(parse_salary(x))) df pd.concat([df, salary_df], axis1) # 查看解析结果 print(df[[salary, min_salary, max_salary, avg_salary]].head(10))注意薪资解析是数据清洗中最容易出错的环节之一。上面的函数是一个基础版本实际数据可能更复杂比如“0.8-1.2万/月”、“年薪30-50万”等。在真实项目中你需要反复检查解析结果抽样对比原始字符串和解析后的数值可能需要迭代多次调整正则表达式。一个实用的技巧是先打印出所有独特的薪资格式df[‘salary’].unique()针对每一种格式设计匹配规则。2.3 其他字段清洗工作经验我们可以将其映射为有序类别或者提取出年限下限。# 定义一个函数将经验字符串转换为数值取范围下限 def exp_to_num(exp): if pd.isna(exp): return np.nan exp str(exp) if 不限 in exp or 无要求 in exp: return 0 # 匹配数字如‘1-3年’ - 1, ‘3-5年’ - 3 match re.search(r(\d)\s*[-~]\s*\d, exp) if match: return int(match.group(1)) # 匹配‘X年以下’或‘X年以上’这里取X match re.search(r(\d)\s*年以下, exp) if match: return int(match.group(1)) match re.search(r(\d)\s*年以上, exp) if match: return int(match.group(1)) # 匹配单个数字如‘5年经验’ match re.search(r^(\d), exp) if match: return int(match.group(1)) return np.nan df[work_exp_num] df[work_experience].apply(exp_to_num) # 也可以创建有序类别 exp_order [经验不限, 1年以下, 1-3年, 3-5年, 5-10年, 10年以上] # 这里需要根据数据实际情况创建分类可能需要进行一些字符串匹配和归类城市字段可能存在“北京”、“北京市”、“北京-朝阳区”等情况。我们需要统一为城市名。# 简单处理取第一个‘-’或‘·’前的部分作为城市 df[city_clean] df[work_city].apply(lambda x: str(x).split(-)[0].split(·)[0].strip())技能关键词提取从职位描述中提取技能。我们先定义一个技能词典然后用简单的字符串匹配。skill_keywords [Python, SQL, R, Excel, Tableau, Power BI, SPSS, SAS, Hive, Spark, Hadoop, 机器学习, 深度学习, 统计学, AB测试, 数据挖掘, 可视化, ETL, MySQL, PostgreSQL, MongoDB] def extract_skills(desc): if pd.isna(desc): return [] desc str(desc).lower() found_skills [] for skill in skill_keywords: if skill.lower() in desc: found_skills.append(skill) return found_skills df[skills_extracted] df[job_description].apply(extract_skills) # 将技能列表展开用于后续计数 all_skills [skill for sublist in df[skills_extracted].dropna() for skill in sublist]清洗完成后我们的数据框df就多了很多有用的列min_salary,max_salary,avg_salary,work_exp_num,city_clean,skills_extracted。数据已经准备好被“拷问”了。3. 数据分析用数字回答关键问题数据洗干净了现在可以开始提出业务问题并用数据来解答。这是数据分析师的核心价值所在——从数据中挖掘洞察。3.1 薪资水平全景哪个城市给钱多这是求职者最关心的问题。我们可以按城市分组计算平均薪资、薪资中位数、职位数量。# 过滤掉薪资为NaN的数据 df_salary_valid df.dropna(subset[avg_salary]) # 按城市分组统计 city_salary_stats df_salary_valid.groupby(city_clean)[avg_salary].agg([count, mean, median, std]).round(1) # 重命名列 city_salary_stats city_salary_stats.rename(columns{count: 职位数, mean: 平均薪资(K), median: 薪资中位数(K), std: 薪资标准差(K)}) # 按职位数降序排列只看职位数较多的城市 city_salary_stats_top city_salary_stats[city_salary_stats[职位数] 5].sort_values(职位数, ascendingFalse) print(city_salary_stats_top)通过这个简单的分组聚合你立刻就能看到比如北京、上海、深圳、杭州这些城市的职位数量最多并且平均薪资也显著高于其他城市。薪资标准差std则反映了该城市薪资的离散程度标准差大说明薪资范围广既有高薪也有低薪岗位。实操心得在分析薪资时中位数median往往比平均数mean更有参考价值。因为平均薪资容易被少数极高薪资的岗位比如总监、专家岗拉高从而扭曲你对大多数岗位薪资水平的认知。中位数代表了“中间位置”的薪资更能反映普通岗位的待遇。所以我的报告里一定会同时呈现平均数和中位数。3.2 经验与薪资的关系资历越深越值钱这几乎是肯定的但我们可以用数据量化这个关系。我们可以绘制薪资avg_salary关于工作经验work_exp_num的散点图并计算相关系数或者按经验分组看薪资分布。# 计算相关系数 correlation df_salary_valid[avg_salary].corr(df_salary_valid[work_exp_num]) print(f平均薪资与工作经验数值化的相关系数: {correlation:.3f}) # 按经验分组使用清洗后的数值或分类 # 为了可视化我们可以将工作经验数值分箱 df_salary_valid[exp_bin] pd.cut(df_salary_valid[work_exp_num], bins[-1, 0, 3, 5, 10, 100], labels[不限, 1-3年, 3-5年, 5-10年, 10年以上]) exp_group_salary df_salary_valid.groupby(exp_bin)[avg_salary].agg([count, mean, median]).round(1) print(exp_group_salary)分析结果很可能显示薪资与工作经验呈正相关。但更有趣的是看增长曲线是不是1-3年到3-5年有一个薪资跃升5年以上增速是否放缓这些洞察对个人职业规划很有价值。3.3 技能需求分析市场最需要什么技能之前我们从职位描述中提取了技能关键词列表all_skills。现在我们来统计这些技能的出现频率。from collections import Counter skill_counts Counter(all_skills) # 转换为DataFrame并排序 skill_df pd.DataFrame(skill_counts.items(), columns[Skill, Count]).sort_values(Count, ascendingFalse) print(skill_df.head(15))不出意外的话Python、SQL、Excel会名列前茅这是数据分析师的“三件套”。Tableau或Power BI等可视化工具以及Hive、Spark等大数据工具的需求量则能反映出市场是偏向传统商业分析还是互联网大数据分析。你还可以计算技能组合的共现情况比如同时要求Python和SQL的职位比例这能帮你规划学习路径的优先级。4. 可视化呈现让数据自己说话分析出的数字和表格是给机器看的可视化图表才是给人看的。好的图表能瞬间传递核心信息。我们使用Matplotlib来绘制一系列图表。4.1 各城市数据分析师职位数量与平均薪资组合图这是一个非常经典的业务图表同时展示“数量”和“水平”。我们可以用双Y轴图左边柱状图表示职位数量右边折线图表示平均薪资。# 准备数据 plot_data city_salary_stats_top.head(10) # 取前10个城市 fig, ax1 plt.subplots(figsize(12, 6)) # 左侧Y轴职位数量柱状图 color_bar skyblue ax1.set_xlabel(城市) ax1.set_ylabel(职位数量, colorcolor_bar) bars ax1.bar(plot_data.index, plot_data[职位数], colorcolor_bar, alpha0.7, label职位数量) ax1.tick_params(axisy, labelcolorcolor_bar) # 在柱子上方显示数量 for bar in bars: height bar.get_height() ax1.annotate(f{int(height)}, xy(bar.get_x() bar.get_width() / 2, height), xytext(0, 3), # 3 points vertical offset textcoordsoffset points, hacenter, vabottom, fontsize9) # 右侧Y轴平均薪资折线图点 ax2 ax1.twinx() color_line coral ax2.set_ylabel(平均薪资 (K), colorcolor_line) line ax2.plot(plot_data.index, plot_data[平均薪资(K)], colorcolor_line, markero, linewidth2, label平均薪资) ax2.tick_params(axisy, labelcolorcolor_line) # 添加图例 lines1, labels1 ax1.get_legend_handles_labels() lines2, labels2 ax2.get_legend_handles_labels() ax2.legend(lines1 lines2, labels1 labels2, locupper left) plt.title(主要城市数据分析师职位数量与平均薪资对比, fontsize14, pad20) plt.xticks(rotation45) # 旋转城市标签避免重叠 plt.tight_layout() plt.show()这张图一目了然地告诉你哪个城市机会最多柱子最高哪个城市薪资最具竞争力折线最高。通常北京、上海会是柱子和折线都高的“双高”城市。4.2 薪资分布箱线图看清全貌识别异常平均数会掩盖分布细节。箱线图可以展示薪资的分布范围、中位数、四分位数以及异常值。# 选取几个重点城市进行对比 key_cities [北京, 上海, 深圳, 杭州, 广州] df_key_cities df_salary_valid[df_salary_valid[city_clean].isin(key_cities)] fig, ax plt.subplots(figsize(10, 6)) # 准备数据列表用于箱线图 data_to_plot [df_key_cities[df_key_cities[city_clean]city][avg_salary].dropna().values for city in key_cities] box ax.boxplot(data_to_plot, labelskey_cities, patch_artistTrue) # 美化箱体 colors [lightblue, lightgreen, pink, wheat, lightgray] for patch, color in zip(box[boxes], colors): patch.set_facecolor(color) ax.set_ylabel(平均薪资 (K)) ax.set_title(重点城市数据分析师薪资分布箱线图, fontsize14, pad20) # 添加网格便于观察 ax.yaxis.grid(True, linestyle--, alpha0.7) plt.tight_layout() plt.show()箱线图能告诉你很多信息箱体越“胖”说明该城市薪资分布越分散中位线箱体内的横线的位置代表了薪资的“一般水平”上下“触须”代表了正常范围之外的“圆圈”或“星号”就是异常高薪或低薪的岗位。通过对比不同城市的箱线图你能清晰地看到城市间的薪资水平差异和分布特点。4.3 技能需求词云最直观的需求热点图文字云能给人最直观的视觉冲击一眼看出哪些技能是“高频词”。# 从skill_df生成词云所需的字典词语:频率 skill_dict dict(zip(skill_df[Skill], skill_df[Count])) # 创建词云对象设置参数 wc WordCloud( width800, height400, background_colorwhite, max_words100, colormapviridis, # 配色方案 font_pathsimhei.ttf # 指定中文字体路径确保中文显示 ).generate_from_frequencies(skill_dict) plt.figure(figsize(12, 8)) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.title(数据分析师岗位技能需求词云, fontsize16, pad20) plt.tight_layout() plt.show()词云图中字体越大代表该技能在职位描述中被提及的次数越多。Python、SQL、数据、分析这些词肯定会非常醒目。你还可以尝试不同的配色方案colormap比如plasma,inferno等让图表更具视觉吸引力。4.4 经验要求分布饼图与薪资趋势折线图子图我们可以用子图将两个相关主题放在一起对比。fig, axes plt.subplots(1, 2, figsize(14, 5)) # 子图1经验要求分布饼图 exp_distribution df[work_experience].value_counts().head(5) # 取前5种经验要求 axes[0].pie(exp_distribution.values, labelsexp_distribution.index, autopct%1.1f%%, startangle90, colorsplt.cm.Set3(np.arange(len(exp_distribution)))) axes[0].set_title(工作经验要求分布前5, fontsize12) # 子图2不同经验水平的平均薪资趋势折线图 # 使用之前分箱后的数据 exp_group_salary axes[1].plot(exp_group_salary.index.astype(str), exp_group_salary[mean], markers, linewidth2, label平均薪资) axes[1].plot(exp_group_salary.index.astype(str), exp_group_salary[median], markero, linewidth2, linestyle--, label薪资中位数) axes[1].set_xlabel(工作经验要求) axes[1].set_ylabel(薪资 (K)) axes[1].set_title(不同经验要求对应的薪资水平, fontsize12) axes[1].legend() axes[1].grid(True, linestyle--, alpha0.5) # 在每个数据点上标注数值 for i, (mean_val, median_val) in enumerate(zip(exp_group_salary[mean], exp_group_salary[median])): axes[1].text(i, mean_val0.5, f{mean_val}, hacenter, vabottom, fontsize9) axes[1].text(i, median_val-1, f{median_val}, hacenter, vatop, fontsize9) plt.suptitle(经验要求分析与薪资趋势, fontsize14) plt.tight_layout() plt.show()左边的饼图告诉你市场对经验的主流要求比如是不是“1-3年”经验的需求最大右边的折线图则展示了随着经验增长薪资是如何变化的。将两者结合你能得出“市场需要多少经验的人”以及“这些人的薪资水平如何”的完整结论。5. 项目复盘与进阶思考做完这个300行数据的小项目其实已经走完了一个数据分析的微型闭环业务理解 - 数据获取与清洗 - 探索性分析 - 可视化呈现 - 报告洞察。每一个环节都有值得深挖的地方。关于数据清洗的再思考我们写的薪资解析函数parse_salary虽然能处理大部分情况但在真实世界中数据可能更“脏”。比如“薪资面议”和“薪资开放”可能混在一起“13薪-16薪”这种福利信息也可能和月薪写在一起。一个更健壮的做法是先做一次全面的唯一值统计人工归类出5-10种主要模式然后为每种模式编写精确的解析逻辑最后用一个“默认处理”来捕获剩余的长尾情况。对于关键字段清洗后一定要做有效性检查比如检查解析出的薪资数值是否在合理范围内比如月薪500K显然不合理。关于可视化的细节打磨Matplotlib功能强大但默认样式比较基础。要让图表更专业、更美观需要花心思调整。配色不要使用默认颜色。可以使用plt.cm.tab10,plt.cm.Set2,plt.cm.viridis等现成的色彩映射或者从网站如ColorBrewer获取适合数据类型的配色方案。字体与布局确保中文字体正确显示我们一开始就设置了。使用plt.tight_layout()自动调整子图间距避免标签重叠。对于复杂的图表手动使用plt.subplots_adjust()调整边距。注释与标注像我们在柱状图上加数字、在折线图上标数值都能极大提升图表的可读性。ax.annotate()和ax.text()是你的好朋友。图表类型选择不要局限于柱状图和折线图。对于技能需求除了词云用条形图barh做排序对比可能更精确。对于城市薪资和职位数量的关系用散点图scatter用点的大小表示职位数量颜色表示平均薪资可以在一张图上展示三个维度信息。项目的延伸方向文本分析深化我们只是简单匹配了技能关键词。可以尝试用jieba对职位描述进行分词然后基于TF-IDF提取关键术语或者进行情感分析看看职位描述的语气是偏挑战性还是偏培养性。关联规则挖掘使用Apriori算法分析技能组合的频繁项集比如“Python SQL Tableau”这个组合经常一起出现。预测模型以avg_salary为因变量以城市、经验、技能转化为0/1特征为自变量构建一个简单的回归模型如线性回归、决策树看看哪些因素对薪资的影响最大。交互式可视化将静态的Matplotlib图表升级为用Plotly或Pyecharts制作的交互式图表可以鼠标悬停查看详情让分析报告更加生动。这个项目虽然数据量小但“麻雀虽小五脏俱全”。它完美地诠释了数据分析的核心流程和价值从杂乱的原始数据中通过系统的清洗、分析和可视化提炼出对决策有指导意义的洞察。对于数据分析师岗位的求职者来说这份分析报告本身就是一份极好的“作品”展示了你的数据处理能力、分析思维和用数据讲故事的能力。下次当你再看到一份招聘数据时希望你能立刻知道从哪里入手如何让它开口说话。本文还有配套的精品资源点击获取