简介:一份基于 Python 的 Boss 直聘数据可视化分析系统源码,面向正在准备期末大作业、毕业设计或想提升数据综合能力的高校学生,解决招聘岗位数据从爬取、清洗到分析、展示的完整链路问题。压缩包仅 426KB,共 26 个文件,以 12 个 HTML 图表页面、2 个 Jupyter Notebook、4 个 CSV 数据集和爬虫.py 为主,配套 .gitignore、.iml 等工程文件,目录结构清晰,能快速定位脚本、数据与图表。目前已有 787 人学习下载。内含岗位爬虫脚本、岗位与公司 CSV 原始数据、数据清洗和关联分析 Notebook,以及学历要求、经验要求、招聘城市、工资待遇、大厂招聘比例等多维度可视化 HTML 页面,覆盖从数据采集到结论呈现的全流程;项目经过严格调试,评审得分 95 分以上,可直接运行,适合期末大作业参考,也能作为 Python 数据分析综合项目演练。
1. 基于python的boss直聘数据可视化分析系统到底能做什么
基于python的boss直聘数据可视化分析系统,听起来像是个爬虫项目,真正卡人的地方反倒在数据底座和可视化表达。它做的事是一条完整链路:用python采集boss直聘上的岗位数据,存进本地数据库,用pyecharts把薪资、学历、经验、城市画成能讲出结论的图,最后用flask串成一个答辩现场能打开的演示页面。它解决的是期末作业里最常见的两类问题——只有代码没有结果图,或者有图却讲不出数据含义。适合python课设、数据可视化课设,以及想拿真实数据练手的入门学习者。只想练爬虫的不必做可视化;只想做可视化的,又缺一份能自圆其说的数据来源;这个标题恰好把两边接上。
2. 数据底盘这么做:字段清单、SQLite 存储与混合采集方案
采集代码可以往后放,字段和存储方案要先定。原因很简单:可视化阶段所有图表的观感,都取决于字段设计。字段抓少了,后期补采要重新登录、重新翻页,成本比一开始把数据字典列全高得多。我一般先把字段清单和数据格式定死,再决定用什么库、怎么采集。这个顺序不要反过来,否则返工的时候会发现清洗代码、建表语句、图表脚本全要跟着改。
2.1 boss直聘岗位数据里值得入库的十个字段
boss直聘网页版把岗位数据以内嵌JSON的形式放在页面源码里,抓取前先明确每个字段将来用在哪个图里。字段建得杂没关系,但每个字段都得有去处。建议优先入库下面这一组,字段名统一用小写下划线,方便pandas直接操作。
| 存库字段 | 含义 | 可视化用途 |
|---|---|---|
| job_id | 岗位唯一ID | 去重、断点续采 |
| job_name | 岗位名称 | 查看招聘方向细分 |
| salary | 薪资原始字符串 | 解析成低/高/中位数值 |
| city | 城市名 | 城市岗位量与薪资对比 |
| experience | 经验要求 | 经验门槛分布 |
| degree | 学历要求 | 学历门槛分布 |
| brand_name | 公司名称 | 头部雇主统计 |
| industry | 公司行业 | 行业分布 |
| skill_tags | 技能标签列表 | 词云、技能要求统计 |
| pub_time | 发布时间 | 过滤过期岗位 |
字段选定之后,心里要有一条完整的分析链:job_name告诉你市场在往哪个方向招人,salary告诉你行情区间,degree和experience告诉你门槛,industry和skill_tags告诉你技能方向。每抓一个字段,都是往这条链上补一块。反过来,抓到一堆没想好怎么用的字段,最后只会堆出一面没法讲解的图表墙。
这里多说一句:网上流传的各种“boss直聘字段表”版本很旧,页面改版后很多字段对不上。判断标准只有一个——以你自己浏览器里返回的JSON为准。打开开发者工具看一眼真实结构,比抄任何模板都靠谱。
2.2 为什么选SQLite:建表语句与两条存储纪律
期末作业的数据量在几百到几千条,SQLite一个单文件就能装下,pandas可以直接读写,不需要装MySQL,也不用处理端口占用、root密码过期这些环境问题。只有两种情况我才会换MySQL:多人协作写同一个项目,或者数据量到了几十万条。期末作业基本碰不到。
import sqlite3 from pathlib import Path db_path = Path("jobs.db") conn = sqlite3.connect(db_path) conn.execute( """ CREATE TABLE IF NOT EXISTS job_post ( job_id TEXT PRIMARY KEY, job_name TEXT, salary TEXT, city TEXT, experience TEXT, degree TEXT, brand_name TEXT, industry TEXT, skill_tags TEXT, pub_time TEXT, crawl_date TEXT ) """ ) conn.commit() conn.close()两条存储纪律必须遵守。第一,salary字段存原始字符串,解析出的数值列放另外的字段。解析规则可能写错,原始串还在就能重新解析,不用重新爬。第二,skill_tags是列表,序列化成JSON字符串再入库,可视化时loads回来即可。直接存成python列表会得到一行带方括号的文本,后期SQL查询会很难受。crawl_date字段记录采集日期,后面想验证“不同时间段薪资有没有变化”时直接group by就能查。
2.3 混合采集方案:为什么纯接口方案对新手不友好
boss直聘网页版对未登录的requests请求基本返回登录跳转页,接口参数里还混着加密token,纯接口方式对新手不友好。常见做法是混合采集:先用playwright弹出一个真实浏览器,扫码登录一次,把cookie落盘;随后把cookie喂给requests,批量抓列表页。这样兼顾了稳定性和速度,周末跑一两个晚上,数据就够了。
采集规模要提前控制。三五个关键词、两三个城市、每个3到5页,就能拿到几百到上千条数据,出图足够。页码从1开始,每页10条,翻到20页以后重复率上升,频率稍高还会触发风控。对期末作业来说,数据量不是越大越好,能讲清楚一套分析链更重要。项目目录建议固定成下面这个结构,后面所有脚本按这个约定落位:
boss_job_analysis/ ├── login_get_cookie.py # 登录一次,保存 cookie ├── crawler.py # 列表页采集 ├── clean.py # 清洗与入库 ├── charts.py # pyecharts 出图 ├── app.py # flask 演示页 ├── jobs.db # 数据文件 ├── zp_cookies.json # cookie 文件 ├── charts/ # 图表输出目录 └── templates/ # flask 模板目录3. 从爬虫到入库:建立岗位数据集的完整链路
字段、存储和采集方案定了之后,剩下的就是按步骤把数据从页面搬到SQLite。下面这四步是完整链路:登录拿cookie、抓列表页、清洗、入库。每一步的坑我都标在代码后面。第一次跑通的时候不要把期望放在一次成功上,页面结构、字段名、cookie有效期都可能是变量,debug的思路比代码本身更重要。
3.1 用playwright登录一次,把cookie落盘
# login_get_cookie.py import json from playwright.sync_api import sync_playwright def save_cookie(): with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context( user_agent=( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" ) ) page = context.new_page() page.goto( "https://www.zhipin.com/web/geek/job", wait_until="domcontentloaded", ) input("请在弹出的浏览器里扫码登录,完成后回到终端按回车...") cookies = context.cookies() with open("zp_cookies.json", "w", encoding="utf-8") as f: json.dump(cookies, f, ensure_ascii=False) browser.close() if __name__ == "__main__": save_cookie()playwright需要先pip install playwright,再执行playwright install chromium。headless=False不能省,headless模式下看不到窗口,也没法在手机上确认登录。input()是暂停点,作用是把脚本卡在登录完成之后、cookie还没失效之前;扫码完成回终端按回车,再把cookie写盘。cookie落盘格式保持playwright列表原样,每项含name、value、domain、expires。requests只需要name和value,但保留完整结构便于排查过期时间。cookie有效期通常几小时到几天,不要提前很久存着等答辩用,现场失效会很尴尬。
3.2 用requests批量抓列表页,解析内嵌JSON
# crawler.py import json import random import re import time import requests def load_cookie(path="zp_cookies.json"): with open(path, "r", encoding="utf-8") as f: cookie_list = json.load(f) # playwright 导出的列表转成 requests 需要的 dict return {c["name"]: c["value"] for c in cookie_list} def fetch_job_list(keyword="python", city_code="101010100", pages=5): cookie_dict = load_cookie() headers = { "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" ), "Referer": "https://www.zhipin.com/web/geek/job", "Accept-Language": "zh-CN,zh;q=0.9", } all_jobs = [] for page in range(1, pages + 1): url = "https://www.zhipin.com/web/geek/job" params = {"query": keyword, "city": city_code, "page": page} resp = requests.get( url, params=params, headers=headers, cookies=cookie_dict, timeout=15, ) resp.raise_for_status() # 首屏数据内嵌在 script 标签里 match = re.search( r"<script>window\.__INITIAL_STATE__=(.*?)</script>", resp.text, re.S, ) if not match: print(f"第 {page} 页没解析到 INITIAL_STATE,可能被跳转或风控") break state = json.loads(match.group(1)) job_list = state.get("jobList") or state.get("jobCardList") or [] all_jobs.extend(job_list) print(f"第 {page} 页抓到 {len(job_list)} 条,累计 {len(all_jobs)} 条") time.sleep(random.uniform(3, 8)) return all_jobsparams传中文关键词时,让requests自己处理URL编码,不要手工拼URL。手工拼出来的中文URL经常被服务端判定为非法请求。分页从page=1开始,每页10条,pages控制在5以内。city_code常见值:北京101010100,上海101020100,深圳101280600;其他城市打开搜索页看URL里的city参数,以实际为准。
匹配INITIAL_STATE用re.search,不用等页面渲染完成,这是混合采集比纯selenium快的原因。每页抓完sleep 3到8秒随机,固定间隔更容易被识别成脚本行为。sleep低于3秒,翻到第3页很容易触发验证码。state里优先取jobList,取不到就试jobCardList,这是给页面结构变化留的兼容。两边都没有就break,别继续翻页浪费请求。
3.3 数据清洗:薪资区间转数值、学历经验归一
# clean.py import re import pandas as pd def split_salary(s): # 输入形如:"20-40K"、"20-40K·15薪"、"面议" if not isinstance(s, str): return None, None, None s = s.strip() if s in ("面议", "—", ""): return None, None, None m = re.match(r"(\d+(?:\.\d+)?)\s*[-~—]\s*(\d+(?:\.\d+)?)", s) if not m: return None, None, None low = float(m.group(1)) high = float(m.group(2)) return low, high, (low + high) / 2 def clean_jobs(raw_jobs): df = pd.DataFrame(raw_jobs) df[["salary_low", "salary_high", "salary_mid"]] = df["salary"].apply( lambda x: pd.Series(split_salary(x)) ) df = df.dropna(subset=["job_id"]).drop_duplicates(subset=["job_id"]) # 学历、经验做顺序化处理,后面图表排序用 df["degree_std"] = df["degree"].fillna("其他") df["experience_std"] = df["experience"].fillna("不限") return df薪资必须用正则解析,不能replace("K","")之后split("-")。“20-40K·15薪”这类带薪资结构的字符串,会让float("15薪")直接报错。正则只取数字区间,不匹配就返回None,让整行薪资变成缺失值,而不是中断整个采集流程。面议统一返回None,导入SQLite时表现为NULL,可视化阶段过滤掉。这里不能填0,否则薪资分布图左端会多出一根异常柱子,答辩时很难解释。
job_id做drop_duplicates是去重底线。页面里对应字段通常是加密字符串,如果缺失,就用job_name加city加brand_name拼一个业务主键。学历和经验字段先做标准化,学历里“学历不限”改成“不限”,经验里“经验不限”改成“不限”,否则图表图例会出现同一含义的两种叫法。
3.4 入库与断点续采:只插入不存在的job_id
# save.py import sqlite3 import pandas as pd def save_to_sqlite(df, db_path="jobs.db"): conn = sqlite3.connect(db_path) existing = pd.read_sql("SELECT job_id FROM job_post", conn) new_df = df[~df["job_id"].isin(existing["job_id"])] new_df.to_sql("job_post", conn, if_exists="append", index=False) conn.close() print(f"新增 {len(new_df)} 条,跳过 {len(df) - len(new_df)} 条已存在")先查已存在的job_id集合再插入,这是断点续采的关键。采集中断后重新运行,不会产生重复行。to_sql的if_exists="append"表示表的创建交给2.2的建表语句,后续只追加。不要在每次运行时先drop表再重建,否则断点续采就没有意义。跑完这一步,jobs.db里就是一份可查询、可反复使用的岗位数据集,后续所有可视化都从这张表读。
4. 用pyecharts把数据变成能回答问题的四张图
数据入库只是第一步,可视化才是期末作业的得分点。这里说的可视化不是把图堆起来,而是每一张图都对应一个明确的业务问题。答辩时最怕的状态是图很多但每张都说不清;反过来,带着问题选图,每一张都能当证据用。我列了四个问题:薪资给到多少?学历卡在哪一档?经验要求集不集中?城市之间差多少?下面四张图分别回答这些问题。
4.1 画图之前,先写一份问题清单
很多作业翻车不是爬虫没写对,而是图表的分析目标不明确。拿到数据第一件事不是render,而是把结论假设写在纸上。比如“我猜测python岗位大多在北京,薪资中位数在20到30K之间,学历以本科为主”,然后画图去验证。这样每张图都有一个可以讲出来的结论,老师问起来你直接说这张图证明了什么,和对着图现场编原因,完全是两个分数。这个问题清单不需要写进代码,写进答辩讲稿就行,但它的价值比多画五张图更高。
4.2 薪资分布:把"20-40K"变成可统计的数值再分桶
# charts_salary.py import sqlite3 import pandas as pd from pyecharts import options as opts from pyecharts.charts import Bar def load_data(db_path="jobs.db"): conn = sqlite3.connect(db_path) df = pd.read_sql("SELECT * FROM job_post", conn) conn.close() df = df[df["salary_mid"].notna()] return df def salary_dist(df): bins = [0, 10, 15, 20, 25, 30, 40, 50, 100] labels = [ "0-10K", "10-15K", "15-20K", "20-25K", "25-30K", "30-40K", "40-50K", "50K以上", ] df["salary_bin"] = pd.cut(df["salary_mid"], bins=bins, labels=labels, right=False) dist = df["salary_bin"].value_counts().sort_index() bar = ( Bar() .add_xaxis(dist.index.tolist()) .add_yaxis("岗位数", dist.values.tolist()) .set_global_opts( title_opts=opts.TitleOpts(title="python岗位薪资分布"), xaxis_opts=opts.AxisOpts(name="薪资区间"), yaxis_opts=opts.AxisOpts(name="岗位数"), ) ) bar.render("charts/salary_dist.html")bins不是随手定的。0到10K一档容纳实习和初级岗,10到30K之间每5K一档,体现20-25K和25-30K这种关键差异;50K以上合并,因为高薪样本少,细分会让柱状图尾部稀碎。pd.cut的right=False让10K归到10-15K而不是0-10K,整数边界才不会错位。labels必须和bins产生的区间数一致,9个边界产生8个区间,这里少写一个labels就会直接报错。
value_counts之后要sort_index,让横坐标按薪资从低到高排列,而不是按数量从高到低。这个排序决定读者第一眼能不能看出分布形状。薪资分布图是所有图表里信息量最大的一张,因为它直接回答“市场给多少钱”这个问题。
4.3 学历与经验门槛:两张环形图看准入条件
# charts_degree.py import pandas as pd from pyecharts import options as opts from pyecharts.charts import Pie def degree_dist(df): degree_order = ["不限", "大专", "本科", "硕士", "博士", "其他"] df["degree_std"] = pd.Categorical( df["degree_std"], categories=degree_order, ordered=True ) counts = df["degree_std"].value_counts().sort_index() pie = ( Pie() .add( series_name="学历要求", data_pair=[list(z) for z in zip(counts.index, counts.values)], radius=["40%", "68%"], label_opts=opts.LabelOpts(formatter="{b}: {d}%"), ) .set_global_opts( title_opts=opts.TitleOpts(title="python岗位学历要求分布"), legend_opts=opts.LegendOpts(orient="vertical", pos_top="12%"), ) ) pie.render("charts/degree_pie.html")学历不是数值,默认value_counts按数量排,会把“本科”排到最前面。用pd.Categorical固定顺序,图例才会按不限、大专、本科、硕士、博士排列,这才像门槛从低到高。环形图用radius=["40%","68%"]实现,内圈留白,视觉上比实心饼图清爽很多。formatter里的{d}%是占比模板,直接在扇区旁边标百分比,答辩时不用凑近数格子。经验分布图用同样结构,categories换成["不限","1年以内","1-3年","3-5年","5-10年","10年以上"],把experience_std替换degree_std即可,不重复贴代码。
4.4 城市与行业对比:横向条形图避免数值打架
# charts_city.py import pandas as pd from pyecharts import options as opts from pyecharts.charts import Bar def city_compare(df): city_stats = ( df.groupby("city") .agg(job_count=("job_id", "count"), salary_median=("salary_mid", "median")) .sort_values("job_count") ) bar = ( Bar() .add_xaxis(city_stats.index.tolist()) .add_yaxis("岗位数", city_stats["job_count"].tolist()) .reversal_axis() .set_series_opts(label_opts=opts.LabelOpts(position="right")) .set_global_opts( title_opts=opts.TitleOpts(title="各城市python岗位数量对比") ) ) bar.render("charts/city_count.html")city字段在清洗阶段要做标准化,“北京”和“北京市”并成“北京”,否则同一个城市会裂成两根柱子。岗位数和薪资中位数量纲差十倍,不要塞进同一张图,分两张摆放对比更清楚。sort_values("job_count")默认升序,配合reversal_axis之后,岗位数最多的城市会出现在条形图最上面,这个细节不处理好图表方向就是反的。
行业分布可以用同样方式做Top10横向条形图,也可以把skill_tags聚合成词云。词云更适合展示“需要会什么”而不是“哪些行业在招人”,如果要用词云,注意pyecharts的WordCloud需要额外扩展包支持,网络不稳定时安装容易失败,准备一个普通柱状图做备选方案。
4.5 用flask把图表串成一个演示页
# app.py from flask import Flask, render_template from pyecharts.render import render_embed app = Flask(__name__) @app.route("/") def dashboard(): df = load_data() charts = [ render_embed(salary_dist(df)), render_embed(degree_dist(df)), render_embed(city_compare(df)), ] return render_template("dashboard.html", charts=charts) if __name__ == "__main__": app.run(host="127.0.0.1", port=8000, debug=False)<!-- templates/dashboard.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>boss直聘数据可视化分析</title> </head> <body> {% for chart in charts %} {{ chart|safe }} {% endfor %} </body> </html>render_embed把pyecharts图表输出成完整的div和script,模板里必须加|safe,防止Jinja2把脚本转义成字符串。漏掉safe,页面会显示一串转义字符而不是图表。debug=False这个细节要在答辩前就设好,开着debug模式时,页面一旦报错会把完整堆栈暴露出来,演示现场很尴尬。
访问地址用127.0.0.1:8000,不要用localhost。部分机器localhost会走IPv6解析导致连接失败,127.0.0.1是写死的回环地址,兼容性最好。跑起来之后,浏览器里就是几个可以滚动的图表页面,这就是flask和echarts数据可视化结合的标准形态。
5. 绕开采集与展示的五个坑:现象、原因、解决
采集和分析跑通一遍之后,把最常踩的坑按出现频率排一遍。前三条集中在采集和清洗期,后两条集中在展示期。每条都按“现象、原因、解决”的结构写,方便你对照排查。
5.1 采集到第3页突然遇到验证码页
现象:脚本还在正常翻页,某一页的resp.text里突然出现大量“验证”相关字样,jobList解析为空,程序break退出。
原因:访问频率超过风控阈值,或者cookie被判定为异常环境。固定间隔的sleep比随机间隔更容易触发风控,连续几个请求间隔完全一样是明显的脚本特征。
解决:先停5到10分钟再继续,不要换个UA就硬刚,那解决不了问题。把sleep下限从3秒提高到5秒,上限保持8秒。恢复之后不要并发开多个终端同时跑同一个cookie。触发一次验证码后,这个cookie的寿命会明显缩短,最好回3.1重新扫码登录一次。
5.2 薪资字段带"·15薪",float转换直接报错
现象:split_salary处理“20-40K”正常,跑到“20-40K·15薪”的时候float转换报错,整个清洗流程中断。
原因:薪资字符串后面还带薪资结构说明,replace("K","")再split("-")只能取到前半段,后半段“40K·15薪”里的“15薪”没法转成数字。
解决:用正则只匹配第一段数字区间,代码就是3.3里的split_salary。重点是不匹配时统一返回None,让这一行薪资变成缺失值,而不是让异常中断整个采集。面议、薪酬范围缺失、格式异常全部走同一条路,可视化阶段过滤缺失值即可。
5.3 jobList字段突然KeyError,页面返回结构变了
现象:re匹配到了INITIAL_STATE,但json.loads之后state["jobList"]报KeyError,代码崩溃。
原因:页面结构更新过,或者当前返回的其实是登录跳转页,state里根本没有岗位列表字段。网上流传的解析代码大多是老版本的,字段名和层级对不上。
解决:先print(resp.url)确认请求还停在搜索URL上,再看resp.text前500个字符确认不是登录页。字段真实名称以浏览器开发者工具Network面板里接口返回的JSON为准。解析时一律用state.get("jobList") or state.get("jobCardList")或者None兜底,取不到就break并打印调试信息,别在不确定结构的情况下继续翻页。
5.4 图表白屏:把echarts资源从CDN换成本地文件
现象:pyecharts生成的HTML用浏览器打开是空白,控制台提示echarts.min.js加载失败。
原因:pyecharts默认从公共CDN加载ECharts资源,在只开放内网、外网不稳定的演示环境里,CDN资源加载不出来,图表自然白屏。
解决:先把echarts.min.js下载到项目static目录,然后在生成图表之前设置本地资源地址。
from pyecharts.globals import CurrentConfig CurrentConfig.ONLINE_HOST = "http://127.0.0.1:8000/static/"设置之后,pyecharts生成的HTML里所有脚本引用都会指向本地。答辩现场跑flask时,图表资源全部走127.0.0.1,完全不依赖外网。这个配置要放在import图表组件之前执行,放在render之后不生效。
5.5 数据有了,图也画了,却讲不出结论
现象:数据入库了,图也render出来了,老师问“这张图想说明什么”,支支吾吾答不上来。
原因:先画图后想结论,图表只是把数据倒出来,没有回答任何业务问题。图表本身不会替你说话,分析结论需要自己组织。
解决:画图前先写问题清单,每张图配一句话结论。薪资分布图的结论是“python岗位中位数集中在20-30K档”,学历环形图的结论是“本科是绝对门槛”,城市对比图的结论是“北京上海岗位量领先”。答辩按结论倒着讲,先抛结论再指图,图的证据价值自然就出来了。
下面这张表是采集期的快速排查指引,结合前面几条看可以省掉不少debug时间。
| 现场现象 | 优先排查顺序 |
|---|---|
| 列表页出现验证码 | 停一会再试,必要时重新登录取cookie |
| jobList KeyError | 看resp.url和resp.text前500字符 |
| 清洗时float报错 | 打印原始salary字符串,检查是否带额外文字 |
| 图表白屏 | 用本地echarts.min.js替代CDN |
| flask页面打不开 | 改用127.0.0.1访问,不要用localhost |
6. 期末答辩前的最后三步:把演示做成加分项
6.1 演示开场先交数据字典
答辩前几分钟,老师还没进入状态,这时候给一张数据字典是建立信任最快的方式。我一般会打一页纸,写明采集关键词、城市范围、有效记录数、清洗规则。例:关键词python、java、数据分析,覆盖北京上海深圳,有效记录800条,薪资面议剔除,按job_id去重。这一页纸比任何模板都管用,它把作业从“代码堆”变成“一份可审计的小型数据报告”。
| 项 | 取值 |
|---|---|
| 采集关键词 | python / java / 数据分析 |
| 覆盖城市 | 北京、上海、深圳 |
| 有效岗位数 | 800 |
| 清洗规则 | job_id去重、面议剔除、薪资转中位数 |
| 采集时间 | 2024-12-01 |
6.2 按"结论-证据"讲图,不要按图讲图
演示顺序我固定为:市场需求概览、薪资分布、学历经验门槛、城市对比。每张图只留一句结论。比如先讲“招聘需求集中在20-30K薪资段,本科以上占比接近七成”,然后把图切出来。先结论后证据,听的人不需要自己看图猜重点。顺序讲完大概六分钟,剩下的时间留给老师提问,比对着图从标题讲到图例要高效得多。
6.3 两个必被追问的问题怎么答
追问一:为什么用中位数不用均值。薪资是右偏分布,极少数高薪岗位会把均值拉高,中位数代表普通求职者能拿到的典型水平。回答完可以补一句:图上每一档占比都能复现,清洗代码和jobs.db都在项目里,随时可以重新算。
追问二:八百条数据有代表性吗。先承认局限,再拉回方法论:这是课程设计规模的数据集,验证的是从采集、清洗到可视化的完整链路;要支撑更强结论,可以扩城市、扩关键词、拉长采集周期。硬说“有代表性”反而容易被追着问细节,说明样本边界才是更稳的回答。
我做这类期末大作业吃过最大的亏,是把图堆满整页,老师一问这张图说明什么,我盯着图看了十几秒说不出话。后来每张图先写结论再画图,演示按结论倒着讲,效果完全不一样。希望帮到你。
本文还有配套的精品资源,点击获取