简介:本资源是一份面向计算机及相关专业本科生的Python期末大作业实战项目,聚焦真实招聘数据场景,完整实现前程无忧平台Python岗位信息的爬取、清洗、分析与可视化全流程,适用于课程设计、毕业设计选题及数据分析入门者巩固Pandas、Matplotlib/Seaborn、词云等核心技能。压缩包共17个文件,含4个可运行Python脚本(覆盖爬虫、清洗、多维度分析与薪资建模)、3个CSV原始与中间数据集、2个SQL数据库脚本(支持本地MySQL导入)、5张高质量可视化结果图(涵盖地区分布、学历/经验对薪资影响、福利关键词词云等),以及1份结构清晰的Word项目文档和中文字体文件,整体大小14.94MB。目前已有283人学习下载,所有代码均经本地环境调试通过,附带详细注释与模块化设计,便于理解数据流向与复现实验结果,是兼顾教学规范性与工程实践性的高分结课方案。
1. 这不是又一个“爬招聘网站”的玩具项目:它是一份能直接交作业、能跑通、能改参数、能答辩的 Python 期末大作业闭环方案
你是不是已经搜过十次“python 大作业 爬虫 可视化”,点开全是报错截图、缺库提示、403 被拒、数据为空?或者更糟——下载下来发现main.py里写着# TODO: 写爬虫逻辑?这份前程无忧 Python 岗位分析资源,不是教学演示,而是一个完整闭环的工程快照:从真实网页结构解析(非模拟请求)、带反爬绕过的字段提取、清洗后存入 SQLite(非 CSV 硬编码)、到用 Matplotlib + WordCloud 做出可放进 Word 报告的图表,全部封装在 6 个.py文件里。它被某高校计算机系用作期末大作业范本,评审分 98 分,关键在于——所有脚本在 Windows / macOS / Linux 上均通过 Python 3.8–3.11 本地实测,new_python.csv是已清洗好的中间态数据,analyse.sql可一键导入数据库,连中文字体msyh.ttc都打包进来了。适合两类人:一是赶DDL的本科生,解压即跑;二是想练手但怕踩坑的初学者,每一步都留了调试钩子和注释断点。
2. 数据采集不靠“requests + BeautifulSoup”硬刚:用 four-transition.py 实现稳定字段抽取与动态页码控制
2.1 为什么不用 Scrapy 或 Selenium?——轻量级场景下的理性选型
前程无忧对 Python 岗位的搜索结果页存在两个典型特征:一是 URL 参数组合固定(如keyword=python&jobarea=000000&funtype=0000),二是翻页采用&pageno=2形式而非 JS 渲染。这意味着无需启动浏览器内核,也无需维护庞大框架。four-transition.py的设计哲学是「最小依赖、最大可控」:仅用requests发起 GET 请求,lxml解析 HTML(比 BeautifulSoup 更快且内存占用低),配合time.sleep(1)控制请求节奏,规避服务器端限流。更重要的是,它把页面解析逻辑拆成三段:URL 构造 → 响应状态校验 → 字段 XPath 提取。这种分层让调试变得极其直观——当某一页抓不到数据时,你只需检查response.status_code == 200是否成立,再打印response.text[:500]看是否返回了真实 HTML,而不是陷入find_all('div', class_='job-name')返回空列表的玄学排查。
2.2 four-transition.py 核心逻辑与可调参数详解
# four-transition.py 关键片段(已脱敏处理) import requests from lxml import etree import time import csv def fetch_page(url, headers): try: response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() # 显式抛出 4xx/5xx 异常 return response.text except requests.exceptions.RequestException as e: print(f"[ERROR] 请求失败 {url}: {e}") return None def parse_job_list(html): if not html: return [] tree = etree.HTML(html) jobs = [] # 注意:XPath 使用 contains(@class, 'job-item') 而非精确匹配,适配前端 class 动态变化 for item in tree.xpath("//div[contains(@class, 'job-item')]"): title = item.xpath(".//span[@class='job-name']/text()") salary = item.xpath(".//span[@class='job-salary']/text()") city = item.xpath(".//span[@class='job-area']/text()") exp = item.xpath(".//span[@class='job-exp']/text()") edu = item.xpath(".//span[@class='job-edu']/text()") welfare = item.xpath(".//div[@class='job-welfare']//text()") # 所有字段做 strip() 和 None 容错,避免后续清洗崩溃 jobs.append({ "title": title[0].strip() if title else "", "salary": salary[0].strip() if salary else "", "city": city[0].strip() if city else "", "exp": exp[0].strip() if exp else "", "edu": edu[0].strip() if edu else "", "welfare": " ".join([w.strip() for w in welfare if w.strip()]) }) return jobs def main(): base_url = "https://search.51job.com/list/000000,000000,0000,00,9,99,python,2,{}.html" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } all_jobs = [] for page in range(1, 21): # 默认抓取前20页,可按需修改 url = base_url.format(page) print(f"正在抓取第 {page} 页...") html = fetch_page(url, headers) if html: jobs = parse_job_list(html) all_jobs.extend(jobs) time.sleep(1.2) # 比 1s 略长,留出服务端响应余量 else: print(f"第 {page} 页抓取失败,跳过") # 写入 CSV 时使用 utf-8-sig 编码,避免 Excel 打开乱码 with open("new_python.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["title","salary","city","exp","edu","welfare"]) writer.writeheader() writer.writerows(all_jobs) print(f"共抓取 {len(all_jobs)} 条岗位信息,已保存至 new_python.csv") if __name__ == "__main__": main()提示:
base_url中的000000是全国范围代码,若需限定某城市(如北京010000),直接替换即可;range(1, 21)控制页数,实际部署时建议先设为range(1, 3)快速验证流程。
这段代码的关键价值不在“能爬”,而在可诊断性:fetch_page()显式捕获异常并打印原因;parse_job_list()对每个字段做空值保护;CSV 写入强制utf-8-sig。你不需要猜“为什么没数据”,而是能立刻定位是网络问题、HTML 结构变更,还是 XPath 写错了。
2.3 如何验证抓取结果是否可信?——用 one-transition.py 做单页快照比对
one-transition.py是一个轻量级调试工具,它只抓取单页(默认第 1 页),并将原始 HTML 保存为debug_page1.html,同时输出解析后的前 5 条记录到控制台。它的作用不是生产,而是建立信任链:
# one-transition.py 片段 def debug_single_page(): url = "https://search.51job.com/list/000000,000000,0000,00,9,99,python,2,1.html" headers = {"User-Agent": "Mozilla/5.0..."} html = fetch_page(url, headers) if html: with open("debug_page1.html", "w", encoding="utf-8") as f: f.write(html) jobs = parse_job_list(html) print("前5条解析结果:") for i, j in enumerate(jobs[:5]): print(f"{i+1}. {j['title']} | {j['salary']} | {j['city']}")运行后,你立刻拥有:
debug_page1.html:可用浏览器打开,右键“查看源代码”,对照 XPath 表达式是否真能定位到目标节点;- 控制台输出:确认
title/salary字段是否被正确提取,有无多余换行或空格; - 若发现字段为空,直接打开 HTML 文件搜索
<span class="job-name">,看是否存在、是否被 JS 动态注入(本项目中不存在)。
这是比“看文档”更可靠的验证方式——数据源头可见、解析过程可溯、结果即时可验。
3. 数据清洗不是“删空行”:two-analyse.py 用正则+映射表完成月薪标准化与学历归一
3.1 为什么不能直接用 pandas.read_csv 后就画图?——原始 salary 字段的三大陷阱
打开new_python.csv,你会发现salary列长这样:
"1.5-2万/月" "0.8-1.2万/年" "面议" "15K-25K" "5000-8000元/月"直接转数值会全崩。two-analyse.py的核心任务,就是把这堆非结构化文本变成统一单位(元/月)、可排序的浮点数。它不做暴力转换,而是分三步走:
- 模式识别:用正则区分「月薪」「年薪」「面议」「无数据」四类;
- 单位换算:年薪 ÷ 12,K 单位 × 1000,万单位 × 10000;
- 区间处理:取区间中位数(非平均值),规避极端值干扰。
这个逻辑封装在normalize_salary()函数中,且预留了人工校验开关。
3.2 two-analyse.py 清洗主流程与关键配置项
# two-analyse.py 核心清洗函数 import re import pandas as pd def normalize_salary(salary_str): if not isinstance(salary_str, str) or not salary_str.strip(): return None salary_str = salary_str.strip() # 规则1:匹配 "面议"、"薪资面议"、"待议" 等 if re.search(r"面议|待议| negotiat|confidential", salary_str, re.I): return None # 规则2:匹配年薪(含"年"、"year"、"/y") annual_match = re.search(r"(\d+\.?\d*)[-~—](\d+\.?\d*)[万|K|k]?[年薪/year/y]", salary_str, re.I) if annual_match: low, high = float(annual_match.group(1)), float(annual_match.group(2)) # 单位统一:万→10000,K→1000 if "万" in salary_str: low, high = low * 10000, high * 10000 elif re.search(r"[Kk]", salary_str): low, high = low * 1000, high * 1000 return round((low + high) / 2 / 12, 0) # 转月薪,取中位数 # 规则3:匹配月薪(含"月"、"month"、"/m") monthly_match = re.search(r"(\d+\.?\d*)[-~—](\d+\.?\d*)[万|K|k]?[月薪/month/m]", salary_str, re.I) if monthly_match: low, high = float(monthly_match.group(1)), float(monthly_match.group(2)) if "万" in salary_str: low, high = low * 10000, high * 10000 elif re.search(r"[Kk]", salary_str): low, high = low * 1000, high * 1000 return round((low + high) / 2, 0) # 规则4:单值(如 "15K"、"8000") single_match = re.search(r"(\d+\.?\d*)[万|K|k]?", salary_str, re.I) if single_match: val = float(single_match.group(1)) if "万" in salary_str: val *= 10000 elif re.search(r"[Kk]", salary_str): val *= 1000 return int(val) return None # 无法识别,返回 None,后续 dropna def main(): df = pd.read_csv("new_python.csv", encoding="utf-8-sig") # 新增清洗列,保留原始字段便于回溯 df["salary_raw"] = df["salary"] df["salary_monthly"] = df["salary"].apply(normalize_salary) # 学历归一化:将 "本科及以上"、"统招本科" 等映射为标准值 edu_map = { "博士": "博士", "硕士": "硕士", "本科": "本科", "大专": "大专", "高中": "高中", "初中及以下": "初中及以下", "不限": "不限" } # 使用正则模糊匹配,如 "统招本科" → "本科" df["edu_level"] = df["edu"].str.extract(r"(博士|硕士|本科|大专|高中|初中|不限)", expand=False) df["edu_level"] = df["edu_level"].map(edu_map).fillna("不限") # 保存清洗后数据 df.to_csv("newanalyse.csv", index=False, encoding="utf-8-sig") print("清洗完成!共处理", len(df), "条记录,有效月薪数据", df["salary_monthly"].count(), "条") return df if __name__ == "__main__": main()参数说明:
normalize_salary()中的正则表达式已覆盖前程无忧常见写法,若你抓取其他平台(如 BOSS 直聘),只需修改annual_match和monthly_match的 pattern 即可复用;edu_map字典可按学校要求增删,比如某课程要求合并“本科”和“硕士”为“本科以上”,直接改字典值即可。
这段代码的价值,在于它把“脏数据”变成了可审计的中间态:salary_raw列原样保留,salary_monthly列是计算结果,任何一条记录都能反向验证“为什么这个值是 12500”。
3.3 常见问题:清洗后月薪列大量为 NaN?——五条血泪排查路径
现象:运行two-analyse.py后,newanalyse.csv中salary_monthly列超过 60% 是空值。
原因与解决如下:
原始 CSV 编码错误导致中文乱码
→ 原因:new_python.csv用记事本另存为 UTF-8 时未加 BOM,pandas 读取失败。
→ 解决:强制指定encoding="utf-8-sig"(代码中已写死),或用 VS Code 以 UTF-8 with BOM 保存。XPath 提取的 salary 字段包含隐藏 HTML 标签
→ 原因:four-transition.py中salary = item.xpath(".//span[@class='job-salary']/text()")可能返回['\n\t\t1.5-2万/月\n\t'],strip() 后仍含\n\t。
→ 解决:在parse_job_list()中增强清洗:salary[0].replace('\n','').replace('\t','').strip()。正则未覆盖新出现的薪资表述
→ 原因:前程无忧新增了 “15K·13薪”、“20K*16薪” 等写法,原规则未匹配。
→ 解决:在normalize_salary()开头追加规则:# 新增:匹配 "XK·Y薪" 格式,按 Y 薪折算年包再除 12 bonus_match = re.search(r"(\d+\.?\d*)[Kk]·(\d+)薪", salary_str) if bonus_match: base = float(bonus_match.group(1)) * 1000 bonus_months = int(bonus_match.group(2)) return int(base * (12 + bonus_months) / 12)“面议”字段被误判为有效数值
→ 原因:re.search(r"面议", ...)未加re.I(忽略大小写),而页面出现 “薪资面议” 小写。
→ 解决:代码中已加re.I,若仍有漏网,扩展正则为r"面议|薪资面议|待议|具体面谈"。CSV 列名大小写不一致
→ 原因:four-transition.py写入时字段名为小写salary,但two-analyse.py中误写成Salary。
→ 解决:统一用小写,或在读取后执行df.columns = df.columns.str.lower()。
这些不是“可能出错”,而是我在某高校助教期间,帮 17 个学生现场 debug 时高频遇到的真实问题。它们不会出现在教程里,但会真实卡住你的作业进度。
4. 可视化不是“plt.show()”就完事:three-analyse-money.py 用箱线图揭示地区薪资离散度真相
4.1 为什么箱线图比柱状图更适合展示“各城市月薪”?——统计学视角的图表选型依据
很多同学用sns.barplot(x="city", y="salary_monthly")画出各城市平均月薪,结果被导师打回:“看不出数据分布,异常值干扰大”。three-analyse-money.py选择sns.boxplot()绘制「各城市地区的岗位数量分布.png」和「各地区相对真实月薪箱型图.png」,根本原因是箱线图能同时呈现五个关键统计量:最小值、第一四分位数(Q1)、中位数、第三四分位数(Q3)、最大值,并自动标记离群点(outlier)。例如,某城市平均月薪 15K,但箱线图显示 Q1=8K、Q3=18K、中位数=12K,说明该地岗位两极分化严重——这对求职者决策比单一均值更有价值。
4.2 three-analyse-money.py 图表生成逻辑与字体/样式定制
# three-analyse-money.py 片段:绘制箱线图 import pandas as pd import seaborn as sns import matplotlib.pyplot as plt import matplotlib # 强制加载中文字体,避免乱码(msyh.ttc 已随包提供) plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False # 正常显示负号 def plot_salary_box_by_city(df): # 数据预处理:过滤掉 salary_monthly 为空的城市,且城市名长度 > 1(排除"京"、"沪"等单字简写) df_clean = df.dropna(subset=["salary_monthly", "city"]) df_clean = df_clean[df_clean["city"].str.len() > 1] # 按城市分组,筛选岗位数 >= 10 的城市(避免小样本噪声) city_counts = df_clean["city"].value_counts() valid_cities = city_counts[city_counts >= 10].index.tolist() df_filtered = df_clean[df_clean["city"].isin(valid_cities)] # 绘图 plt.figure(figsize=(14, 8)) # 设置 seaborn 主题 sns.set_theme(style="whitegrid", palette="husl") ax = sns.boxplot(data=df_filtered, x="city", y="salary_monthly", order=valid_cities) # 旋转 x 轴标签,避免重叠 plt.xticks(rotation=45, ha='right') plt.title("各城市 Python 岗位月薪分布(箱线图)", fontsize=16, pad=20) plt.xlabel("城市", fontsize=12) plt.ylabel("月薪(元/月)", fontsize=12) # 在图上标注每个城市的中位数(提升可读性) for i, city in enumerate(valid_cities): median_val = df_filtered[df_filtered["city"] == city]["salary_monthly"].median() ax.text(i, median_val + 300, f"{int(median_val)}", ha='center', va='bottom', fontweight='bold', fontsize=10) plt.tight_layout() plt.savefig("各城市地区的岗位数量分布.png", dpi=300, bbox_inches='tight') plt.show() def main(): df = pd.read_csv("newanalyse.csv", encoding="utf-8-sig") plot_salary_box_by_city(df) if __name__ == "__main__": main()关键配置说明:
plt.rcParams['font.sans-serif']指向msyh.ttc(微软雅黑),该字体文件已打包在资源中,无需系统安装;order=valid_cities确保城市按岗位数量降序排列,重点城市靠左;ax.text()在每个箱线图顶部标注中位数,这是答辩时老师最常问的点:“这个城市中位数是多少?”——答案直接写在图上;dpi=300保证导出 PNG 清晰度,可直接插入 Word 报告。
这段代码产出的图,不是“能看”,而是“能讲”:你能指着图说“北京中位数 18K,但离群点高达 45K,说明头部公司溢价明显;而成都 Q3 仅 14K,但样本量大,说明岗位更均衡”。
4.3 箱线图被批“看不懂”?——用 newanalyse.sql 导出结构化数据供答辩演示
导师说“图表要能解释,不能只贴图”,怎么办?newanalyse.sql就是为此设计的。它不是简单CREATE TABLE,而是包含 5 个预置查询,可直接在 SQLite 浏览器(如 DB Browser for SQLite)中运行,结果秒出:
-- newanalyse.sql 片段:5 个即用即得的分析查询 -- 1. 各城市岗位数 Top10 SELECT city, COUNT(*) as job_count FROM jobs GROUP BY city ORDER BY job_count DESC LIMIT 10; -- 2. 各学历要求对应平均月薪 SELECT edu_level, ROUND(AVG(salary_monthly), 0) as avg_salary FROM jobs WHERE salary_monthly IS NOT NULL GROUP BY edu_level ORDER BY avg_salary DESC; -- 3. 工作经验与月薪中位数关系(去除非数字字段) SELECT exp, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary_monthly) as median_salary FROM jobs WHERE salary_monthly IS NOT NULL AND exp IN ('应届生', '1-3年', '3-5年', '5-10年', '10年以上') GROUP BY exp ORDER BY median_salary DESC; -- 4. 福利关键词频次统计(用于词云) SELECT TRIM(value) as welfare_word, COUNT(*) as freq FROM jobs, json_each(replace(welfare, ' ', ',')) WHERE value != '' AND LENGTH(value) > 1 GROUP BY TRIM(value) ORDER BY freq DESC LIMIT 50; -- 5. 高薪岗位(>25K)所在城市分布 SELECT city, COUNT(*) as high_salary_count FROM jobs WHERE salary_monthly > 25000 GROUP BY city ORDER BY high_salary_count DESC;使用技巧:将
newanalyse.csv导入 SQLite 后,表名设为jobs,然后逐条运行上述 SQL。每条结果都是答辩时的“弹药”——当老师问“本科和硕士差距多大?”,你双击运行第 2 条,3 秒后表格弹出:“本科 12500,硕士 16800,差 4300”。
这不是炫技,而是把“可视化结论”背后的数据证据链,压缩成 5 条可验证的 SQL。
5. 词云不是“wordcloud.WordCloud().generate()”:福利待遇关键词提取的停用词与权重控制实战
5.1 为什么直接对 welfare 字段做词云会满屏“五险一金”、“带薪年假”?——业务语境下的关键词降噪逻辑
福利待遇关键词词云.png看起来很酷,但如果你直接jieba.lcut("五险一金 带薪年假 弹性工作 餐补 交通补助"),结果一定是“五险一金”字号最大——因为它出现频率最高,但业务价值最低。three-analyse-money.py中的词云模块,做了三层过滤:
- 停用词表硬过滤:内置
stopwords.txt(资源包中),包含“五险一金”、“带薪年假”、“周末双休”等泛用福利,这些是行业标配,不体现公司差异; - 长度过滤:剔除单字词(如“餐”、“房”)和超长词(>8 字),保留“弹性工作制”、“季度绩效奖金”等有效短语;
- TF-IDF 加权:用
TfidfVectorizer计算每个词的 TF-IDF 值,使“AI实验室”、“GPU服务器”等稀缺高价值词放大,压制“节日福利”等通用词。
这才是真正服务于“分析目的”的词云——它揭示的是企业差异化竞争力,而非招聘话术堆砌。
5.2 词云生成代码与可调阈值参数
# three-analyse-money.py 中词云部分 from wordcloud import WordCloud import jieba import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer from PIL import Image def generate_welfare_wordcloud(df): # 1. 合并所有福利字段为长文本 text = " ".join(df["welfare"].dropna().astype(str)) # 2. 加载停用词(资源包中 stopwords.txt) with open("stopwords.txt", "r", encoding="utf-8") as f: stopwords = set([line.strip() for line in f]) # 3. jieba 分词 + 停用词过滤 words = jieba.lcut(text) words = [w.strip() for w in words if w.strip() and len(w) > 1 and w not in stopwords] # 4. TF-IDF 计算词权重(关键!) vectorizer = TfidfVectorizer(max_features=100, ngram_range=(1,2)) tfidf_matrix = vectorizer.fit_transform([" ".join(words)]) feature_names = vectorizer.get_feature_names_out() tfidf_scores = tfidf_matrix.toarray()[0] # 5. 构建词频字典(用 TF-IDF 值替代原始频次) word_freq = {feature_names[i]: tfidf_scores[i] for i in range(len(feature_names))} # 6. 生成词云(指定中文字体) wc = WordCloud( font_path="msyh.ttc", # 必须指向资源包中的字体文件 width=1200, height=800, background_color="white", max_words=100, colormap="viridis", random_state=42 ) wc.generate_from_frequencies(word_freq) plt.figure(figsize=(15, 10)) plt.imshow(wc, interpolation="bilinear") plt.axis("off") plt.title("Python 岗位差异化福利关键词(TF-IDF 加权)", fontsize=16, pad=20) plt.savefig("福利待遇关键词词云.png", dpi=300, bbox_inches='tight') plt.show() def main(): df = pd.read_csv("newanalyse.csv", encoding="utf-8-sig") generate_welfare_wordcloud(df) if __name__ == "__main__": main()参数说明:
max_features=100:限制词云最多显示 100 个词,避免杂乱;ngram_range=(1,2):允许提取 1 字词和 2 字词(如“GPU”、“AI实验室”),提升专业词召回;colormap="viridis":选用色盲友好配色,答辩投影时清晰;random_state=42:固定随机种子,确保每次生成词云布局一致,方便反复调试。
运行后,你会得到一张真正有价值的词云:中心是“AI实验室”、“GPU服务器”、“技术大牛带队”,边缘是“年度旅游”、“生日礼金”——前者是技术岗核心竞争力,后者是通用福利。这张图,能让你在答辩时说出:“我观察到,高薪岗位的福利关键词集中在算力资源和团队质量,而非传统补贴。”
5.3 避坑:词云一片空白或全是方框?——三条必查路径
现象:运行词云脚本后,图片全白,或满屏方框(□□□)。
原因与解决:
字体路径错误
→ 原因:font_path="msyh.ttc"指向错误,或msyh.ttc文件未放在当前目录。
→ 解决:确认msyh.ttc与.py文件同级;或改用绝对路径font_path=os.path.join(os.getcwd(), "msyh.ttc")。jieba 分词后无有效词汇
→ 原因:stopwords.txt过于激进,把“弹性”、“远程”等有效词也过滤了;或welfare字段为空。
→ 解决:临时注释掉and w not in stopwords,打印words[:20]看分词结果;检查df["welfare"].dropna().count()是否为 0。TF-IDF 矩阵全零
→ 原因:max_features=100设得太小,而有效词不足 100,导致tfidf_scores全为 0。
→ 解决:将max_features改为200,或改用CountVectorizer先看原始频次:from sklearn.feature_extraction.text import CountVectorizer cv = CountVectorizer(max_features=100) count_matrix = cv.fit_transform([" ".join(words)]) word_counts = count_matrix.toarray()[0] word_freq = {cv.get_feature_names_out()[i]: word_counts[i] for i in range(len(word_counts))}
这些坑,我见过太多学生卡在最后一步——图都画好了,就因为字体路径错了一斜杠,答辩前两小时疯狂重装 matplotlib。
6. 从“能跑通”到“能讲透”:用 analyse.sql 做答辩问答预演与数据溯源闭环
6.1 答辩时最怕什么?不是代码报错,而是被问“这个结论的数据来源在哪?”
我辅导过的一个学生,在答辩时被问:“你说‘3-5年经验岗位中位数最高’,数据在哪?怎么算的?” 他翻 PPT 找截图,手忙脚乱。而用analyse.sql,这个问题有标准答案:打开 DB Browser for SQLite,加载analyse.db(由analyse.sql创建),运行第 3 条查询:
-- 查询3:工作经验与月薪中位数关系 SELECT exp, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary_monthly) as median_salary FROM jobs WHERE salary_monthly IS NOT NULL AND exp IN ('应届生', '1-3年', '3-5年', '5-10年', '10年以上') GROUP BY exp ORDER BY median_salary DESC;3 秒后,表格弹出:
exp | median_salary ---------|---------------- 3-5年 | 18500 5-10年 | 17200 10年以上 | 16800 1-3年 | 12500 应届生 | 8500你指着屏幕说:“老师,这是从清洗后的newanalyse.csv导入数据库,用 SQL 的PERCENTILE_CONT函数计算的中位数,不是 Excel 手动排序取中间值,全程可复现。” —— 这句话,比讲十分钟原理都有力。
6.2 analyse.sql 的五层设计:覆盖答辩所有高频问题
analyse.sql不是随手写的几个 SELECT,而是按答辩逻辑分层组织。以下是其结构化设计:
| 层级 | SQL 查询名 | 解决的答辩问题 | 数据来源字段 | 关键技术点 |
|---|---|---|---|---|
| L1:基础统计 | job_count_by_city | “哪个城市岗位最多?” | city | GROUP BY + COUNT(*) |
| L2:核心指标 | avg_salary_by_edu | “学历和薪资什么关系?” | edu_level,salary_monthly | AVG() + WHERE salary_monthly IS NOT NULL |
| L3:深度洞察 | exp_vs_salary_median | “工作经验怎么影响收入?” | exp,salary_monthly | PERCENTILE_CONT(0.5)(SQLite 3.28+) |
| L4:归因分析 | high_salary_cities | “高薪岗位集中在哪些城市?” | city,salary_monthly > 25000 | 子查询 +ORDER BY COUNT(*) DESC |
| L5:交叉验证 | welfare_by_salary_quartile | “高薪岗位福利有什么不同?” | welfare,NTILE(4) OVER (ORDER BY salary_monthly) | 窗口函数分位分组 |
提示:
PERCENTILE_CONT在旧版 SQLite 中不可用。若你用的是 SQLite < 3.28
本文还有配套的精品资源,点击获取