简介:这份资源面向希望入门或进阶Python数据分析的职场学习者,以数据分析师岗位招聘数据为实战对象,完整演示从数据清洗到可视化呈现的分析流程。包内共10个文件,涵盖ipynb交互式笔记本、py脚本、csv原始数据、html分析报告、ttf中文字体及jpg配图等,压缩包约40.89MB,兼顾代码运行与结果查阅。项目围绕Pandas数据加载与缺失值处理、描述性统计、探索性分析(EDA)以及Matplotlib图表绘制展开,重点呈现薪资分布、工作经验与薪资关系等职场关注维度,并涉及重复值检测、异常值处理与图表可读性优化等细节。源码约300行,结构清晰,适合对照学习如何组织一个完整的数据分析项目。目前已有372人学习,可作为课程作业、简历项目或自学练手的参考素材。
1. 从一份 300 行的数据分析师岗位分析源码说起
招聘网站上的数据分析师岗位,薪资跨度能从 8K 拉到 40K,要求里有的写“精通 Excel 即可”,有的列了 Python、SQL、Tableau、机器学习一长串。想搞清楚这个岗位到底在招什么人、哪些技能是硬门槛、哪些只是加分项,靠一页页翻招聘信息不现实。这份约 300 行的 Python 源码,做的就是抓一批数据分析师岗位的招聘数据,用 pandas 做探索性分析,再用 matplotlib 把薪资分布、技能词频、城市差异画出来。它不依赖爬虫框架,不依赖数据库,一个 CSV 加两个库就能跑通全流程。适合刚学完 pandas 基础、想找一个完整项目练手的入门者,也适合需要快速搭一个岗位分析原型、再往里面填自己业务数据的从业者。代码量不大,但覆盖了数据分析项目最核心的几步:读数据、清洗、分组聚合、可视化输出。
2. 数据探索分析:从原始 CSV 到可用 DataFrame
2.1 先搞清楚数据长什么样
拿到任何一份招聘数据,第一步不是急着画图,而是把 DataFrame 的 shape、dtypes、head 和缺失值情况看一遍。这份源码里对应的就是数据探索分析部分,通常集中在文件前 80 行左右。常见做法是先读 CSV,然后立刻做一次结构体检。
import pandas as pd import numpy as np # 读取招聘数据,注意编码,中文 CSV 常见 utf-8 或 gbk df = pd.read_csv("data.csv", encoding="utf-8") # 结构体检四件套 print(df.shape) # 行数、列数 print(df.dtypes) # 每列类型 print(df.head(3)) # 前 3 行 print(df.isnull().sum()) # 每列缺失值数量逻辑说明:shape让你知道样本量够不够支撑后续分组,dtypes决定哪些列需要转数值,head帮你确认列名和内容是否对得上,isnull().sum()直接暴露哪些字段需要清洗。参数上,encoding如果报UnicodeDecodeError,换成gbk或gb18030再试,这是中文数据最常见的翻车点。
2.2 薪资字段的清洗是整份代码的分水岭
招聘数据里最脏的往往是薪资列,格式可能是“15-25K·13薪”“1.5-2万/月”“面议”。这份源码的核心处理逻辑就是把薪资区间拆成最低、最高、中位数三个数值列。如果这一步不做,后面所有按薪资分组的分析都是空的。
import re def parse_salary(s): if pd.isna(s) or "面议" in str(s): return pd.Series([np.nan, np.nan, np.nan]) s = str(s).lower().replace("·13薪", "").replace("·14薪", "") # 匹配 15-25k 或 1.5-2万 两种常见格式 m = re.search(r"(\d+\.?\d*)-(\d+\.?\d*)(k|万)", s) if not m: return pd.Series([np.nan, np.nan, np.nan]) low, high, unit = float(m.group(1)), float(m.group(2)), m.group(3) if unit == "万": low, high = low * 10, high * 10 # 统一成 K return pd.Series([low, high, (low + high) / 2]) df[["salary_low", "salary_high", "salary_mid"]] = df["salary"].apply(parse_salary)逻辑说明:parse_salary返回一个 Series,apply之后会自动展开成三列。正则(\d+\.?\d*)-(\d+\.?\d*)(k|万)覆盖了“15-25K”和“1.5-2万”两种写法,单位统一成 K 是为了后续比较。参数上,\d+\.?\d*允许整数和小数,·13薪这类后缀直接去掉,不影响月薪区间。清洗完记得df[["salary_low","salary_mid"]].describe()看一眼分布,如果中位数明显偏离常识,说明正则漏了某种格式。
2.3 技能关键词的拆分与统计
岗位要求字段通常是一长串文本,比如“Python、SQL、Excel、Tableau”。要统计哪些技能出现频率最高,得先把这列拆成独立的词,再展开计数。
from collections import Counter # 假设技能列叫 skills,用顿号或逗号分隔 all_skills = [] for row in df["skills"].dropna(): parts = re.split(r"[、,,/]", row) all_skills.extend([p.strip() for p in parts if p.strip()]) skill_count = Counter(all_skills).most_common(15) skill_df = pd.DataFrame(skill_count, columns=["skill", "count"]) print(skill_df)逻辑说明:re.split用多个分隔符同时切分,strip去掉空格,Counter直接出词频。参数上,分隔符集合[、,,/]可以根据实际数据增减,如果发现“Python”和“python”被算成两个词,加一步.lower()统一大小写。这一步的输出skill_df就是后面画条形图的输入。
3. matplotlib 可视化:把分析结果画成能看的图
3.1 薪资分布直方图与中位数标注
数据探索完,最有信息量的第一张图是薪资分布。源码里通常用直方图加一条中位数竖线,一眼看出市场集中区间。
import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] # 中文显示 plt.rcParams["axes.unicode_minus"] = False fig, ax = plt.subplots(figsize=(8, 5)) ax.hist(df["salary_mid"].dropna(), bins=20, color="#4C72B0", edgecolor="white") median = df["salary_mid"].median() ax.axvline(median, color="red", linestyle="--", label=f"中位数 {median:.1f}K") ax.set_xlabel("月薪中位值(K)") ax.set_ylabel("岗位数量") ax.set_title("数据分析师薪资分布") ax.legend() plt.tight_layout() plt.savefig("salary_dist.png", dpi=150) plt.show()逻辑说明:hist的bins=20是经验值,样本几百条时 15 到 25 之间比较合适,太少看不出分布,太多锯齿明显。axvline画中位数参考线,legend显示数值。参数上,font.sans-serif设成SimHei是 Windows 下的常见做法,Mac 可以换Arial Unicode MS,Linux 需要确认系统装了中文字体,否则中文全是方块。dpi=150保证保存的图够清晰。
3.2 技能词频横向条形图
技能统计结果用横向条形图最直观,因为技能名长短不一,横着放不用旋转标签。
fig, ax = plt.subplots(figsize=(8, 6)) skill_df_sorted = skill_df.sort_values("count") ax.barh(skill_df_sorted["skill"], skill_df_sorted["count"], color="#55A868") ax.set_xlabel("出现次数") ax.set_title("数据分析师岗位技能要求 Top 15") for i, v in enumerate(skill_df_sorted["count"]): ax.text(v + 0.5, i, str(v), va="center", fontsize=9) plt.tight_layout() plt.savefig("skill_bar.png", dpi=150) plt.show()逻辑说明:sort_values升序排列后barh从下往上画,最长的在最上面。ax.text在每根柱子末尾标数值,v + 0.5是偏移量,避免文字压住柱子。参数上,figsize高度给 6 是因为 15 个技能需要足够行距,如果技能更多,按每行 0.4 英寸往上加。
3.3 城市薪资对比:分组箱线图
不同城市的薪资差异是求职者最关心的维度之一。箱线图能同时展示中位数、四分位和异常值。
top_cities = df["city"].value_counts().head(6).index df_city = df[df["city"].isin(top_cities)] fig, ax = plt.subplots(figsize=(9, 5)) df_city.boxplot(column="salary_mid", by="city", ax=ax, grid=False) ax.set_xlabel("城市") ax.set_ylabel("月薪中位值(K)") ax.set_title("主要城市数据分析师薪资对比") plt.suptitle("") # 去掉 pandas 自动加的标题 plt.tight_layout() plt.savefig("city_salary.png", dpi=150) plt.show()逻辑说明:先取岗位数量前 6 的城市,避免小样本城市干扰。boxplot的by参数按城市分组,grid=False去掉网格线让图更干净。plt.suptitle("")是必须的,否则 pandas 会自动加一个多余的总标题。参数上,如果某个城市数据点太少,箱体会压成一条线,这时候要么合并城市,要么在图上标注样本量。
4. 避坑与常见问题排查
4.1 中文显示成方块
现象:图上所有中文标签变成一个个小方块,英文和数字正常。原因:matplotlib 默认字体不含中文字形。解决:在绘图前设置plt.rcParams["font.sans-serif"] = ["SimHei"],Mac 用["Arial Unicode MS"],Linux 先fc-list :lang=zh确认有哪些中文字体,再填对应名称。设完还不行就清一下 matplotlib 缓存,删掉~/.matplotlib/fontlist-*.json重启内核。
4.2 薪资正则匹配不到“万”字格式
现象:salary_mid大量为 NaN,describe 出来 count 只有个位数。原因:正则只写了k没写万,或者“万”前面是“1.5-2万”这种小数。解决:正则改成(\d+\.?\d*)-(\d+\.?\d*)(k|万),匹配到“万”后乘 10 统一成 K。改完用df[df["salary_mid"].isna()]["salary"].head(20)抽查没匹配上的原始值,看还漏了什么格式。
4.3 apply 返回多列时报错
现象:df[["a","b","c"]] = df["x"].apply(func)报ValueError: Columns must be same length as key。原因:函数在某些行返回了标量而不是 Series,导致展开失败。解决:确保函数每条分支都return pd.Series([...]),包括异常分支。调试时先df["x"].apply(func).head()看返回结构,确认每行都是等长 Series 再赋值。
4.4 箱线图某个城市只有一条线
现象:箱线图里某个城市的箱子变成一条横线,看不出分布。原因:该城市样本量太少,或者薪资值全部相同。解决:先df["city"].value_counts()看各城市样本量,少于 10 条的考虑合并到“其他”或直接剔除。如果值确实相同,检查是不是薪资清洗时把区间压成了一个点。
4.5 保存图片空白
现象:savefig出来的图片是空白或者被裁切。原因:savefig在show之后调用,或者tight_layout没生效。解决:把savefig放在show之前,加bbox_inches="tight"参数自动裁掉多余白边。如果用了tight_layout还是裁切,试试plt.subplots_adjust手动留边距。
5. 进阶技巧:让这份源码适配你自己的数据
这份代码跑通之后,最有价值的动作是把它套到你手头真实的招聘数据上。我一般会先改三个地方:列名映射、薪资解析、技能词典。列名映射是因为不同来源的 CSV 列名五花八门,与其改代码逻辑,不如在读入后加一层 rename。
col_map = { "岗位名称": "title", "公司": "company", "薪资范围": "salary", "城市": "city", "技能要求": "skills" } df = df.rename(columns=col_map)逻辑说明:把中文列名统一成代码里用的英文名,后面所有分析逻辑不用动。参数上,col_map的键必须和 CSV 实际列名完全一致,包括空格,建议先print(df.columns.tolist())确认。
技能词典是第二个要改的。原始代码用正则切分,但真实数据里“机器学习”和“ML”、“自然语言处理”和“NLP”会被算成不同词。常见做法是加一个归一化字典,在Counter之前做一次替换。
alias = { "ml": "机器学习", "nlp": "自然语言处理", "etl": "ETL", "bi": "BI" } all_skills = [alias.get(s.lower(), s) for s in all_skills]逻辑说明:alias.get命中就替换,没命中保留原词。参数上,字典的键统一小写,因为前面已经.lower()过。这一步做完再统计,词频表会干净很多。
最后一个技巧是给图表加一个统一的样式函数,避免每张图重复写字体和尺寸设置。
def setup_style(): plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False plt.rcParams["figure.dpi"] = 120 plt.rcParams["savefig.bbox"] = "tight" setup_style()逻辑说明:把字体、负号、DPI、保存边距一次性设好,后面每张图直接plt.subplots就行。参数上,figure.dpi设 120 在屏幕上看和保存都够用,要出版级再单独给savefig传dpi=300。
从那以后我每次拿到新的招聘数据,都强制先跑一遍shape、dtypes、isnull三件套,再动任何分析逻辑。这份 300 行的源码最大的价值不是它画了什么图,而是它把“读数据 → 清洗 → 分组 → 可视化”这条链路完整走了一遍,你可以在任何一个环节替换成自己的处理方式。希望帮到你。
本文还有配套的精品资源,点击获取