清洗后字段能用,并不代表已经理解数据。这一课用分布、时间趋势和字段关系检查数据集:引用量是否长尾,某年的记录为何减少,缺失是否集中于某个来源?每张图都要对应一个问题,异常则回到原始记录核对,避免给采集缺口编出业务故事。
前置阅读:第 3 课:清洗与对齐(本课直接用它的产出)、Pandas 数据分析与可视化(基础操作)。
EDA 的组织方式:四类问题,不是随便画画
我把 EDA 组织成四类问题,每类配固定图表,避免”想到哪画到哪”:
| 问题类型 | 典型问题 | 首选图 |
|---|---|---|
| 分布 | 单个字段长什么样 | 直方图、箱线图 |
| 趋势 | 随时间怎么变 | 折线、面积图 |
| 关系 | 两个字段互相影响吗 | 散点、热力图 |
| 异常 | 哪里不对劲 | 以上任何图 + 排查 |
贯穿原则(也是从 Pandas 一篇延续的纪律):每张图必须先写下它要回答的问题,再动手画。没有问题的图只是装饰。
分布:长尾是科研数据的常态
import pandas as pd
import plotly.express as px
df = pd.read_parquet("data/cleaned/papers_2026-08-28.parquet")
# 问题:引用数是什么分布?
fig = px.histogram(df, x="citations", nbins=100,
title="论文引用数分布(原始刻度)")
fig.write_html("eda/citations_hist.html")
先观察当前语料是否呈右偏长尾,不要在画图前就写结论。论文年份、领域与采集筛选都会改变分布。若少量大值占据较大范围,可以进一步考虑:
- 均值没有代表性,看中位数和分位数:
df["citations"].describe(percentiles=[0.5, 0.9, 0.99]) - 画图和建模都要考虑对数变换:
import numpy as np df["log_citations"] = np.log1p(df["citations"]) # log(1+x),容忍 0 fig = px.histogram(df, x="log_citations", nbins=60, title="引用数分布(log1p 刻度)")
对数变换后的分布如果接近正态,系列三的回归建模就顺得多——EDA 的这个发现会直接影响建模决策。
趋势:时间维度上的平台故事
# 问题:各领域的论文产出趋势如何?案例平台关注的领域在涨吗?
yearly = (df.groupby(["year", "field"])
.size().reset_index(name="papers"))
fig = px.line(yearly, x="year", y="papers", color="field",
markers=True, title="各领域年度论文数")
fig.write_html("eda/yearly_trend.html")
趋势图要警惕两个陷阱:
最近年份可能尚未收全,当前年份也没有走完。若曲线末端下降,先排查观察窗口与采集滞后,再讨论领域变化;可排除未完整年份,或明确标注其覆盖范围。不能看到下降就断定是采集问题或领域遇冷。
总量趋势掩盖结构变化。 各领域都在涨时,看占比比看绝对数更有信息量:
yearly["share"] = yearly["papers"] / yearly.groupby("year")["papers"].transform("sum")
px.area(yearly, x="year", y="share", color="field",
title="各领域占比变化")
关系:散点图里的因果陷阱
# 问题:发表年份越久引用越多吗?(显然)那同龄论文里引用和什么相关?
recent = df[df["year"] >= 2020]
fig = px.scatter(recent.sample(5000), x="author_count", y="log_citations",
color="field", opacity=0.4,
trendline="ols", title="作者数 vs 引用数(2020 后,log 刻度)")
sample(5000) 是必须的:十万个点糊在一起什么都看不出来,抽样后结构反而清晰。opacity=0.4 让点的密度可见。
看到相关性时默诵研究方法一篇的纪律:相关不等于因果。作者数和引用正相关,可能是大团队成果影响力大,也可能是高影响力工作吸引更多人署名——EDA 只负责提出假设,验证是后续建模和实验的事。
异常:每个异常都是一个故事
EDA 里最值钱的是”这张图不对劲”的时刻。几个我在科研数据里真实遇到的类型:
# 某一年论文数断崖:查出来是某来源 API 当年变更了字段,采集漏了一批
yearly_total = df.groupby("year").size()
print(yearly_total[yearly_total.diff().abs() > yearly_total.std() * 3])
# 引用数为 -1:某来源用 -1 表示"无数据",清洗时漏处理了
print((df["citations"] < 0).sum())
# 同一标题出现几十次:会议网页采集的分页循环 bug,重复入库
dup = df[df.duplicated("title_norm", keep=False)]
print(dup.groupby("title_norm").size().sort_values(ascending=False).head())
每个异常的处理方式都是固定的三步:定位来源(source 字段在这里派上用场)→ 回 raw 层对账 → 修清洗规则或修采集器,然后重放清洗。这就是上一课坚持留 raw 层的原因——没有 raw 层,这些异常全都无法修复。
产出物:EDA 报告
EDA 的结论落成交互式 HTML 报告(Plotly 图天然是 HTML),按”四类问题”组织,每张图配一段”这张图告诉我们什么 + 对下一步的影响”。这份报告是系列三建模课的输入:特征怎么构造、目标变量要不要变换、哪些数据段要剔除,全部由 EDA 结论驱动。
报告归档到 runs/eda_2026-08-28/,按第 3 课的纪律和数据快照、代码 commit 绑定。
踩坑排查
| 症状 | 原因 | 处理 |
|---|---|---|
| 直方图一根柱子顶破天 | 长尾分布没用对数 | log1p 变换后再看 |
| 散点图一团黑 | 点太多 | sample + opacity |
| 最近一年数据断崖 | 采集滞后 | 标注不完整年份或剔除 |
| 趋势图某条线突然中断 | 该领域某年数据缺失 | 查采集日志,别当成领域趋势 |
| plotly 导出 HTML 巨大 | 全量点都嵌进去了 | 抽样/聚合后再画图 |
| groupby 后画图顺序乱 | 没排序 | 显式 sort_values 再画 |
作品集证据
本课产出:一份按”四类问题”组织的交互式 EDA 报告,含至少一个由你发现并解决的异常案例。“通过 EDA 发现采集管道在某年份的数据缺失并回溯修复”——这种故事比任何技能清单都能证明你的数据分析能力。
练习
- 对 cleaned 数据集画引用数分布的原始/log 双图,写出对数变换对建模的两个影响。
- 做各领域占比面积图,指出占比上升最快的领域并给出数据证据。
- 用分位数对比法找出”同龄论文里引用异常高”的 Top 20,人工看标题总结它们的共性。
- 复现一个异常排查全流程:发现 → 定位来源 → 回 raw 对账 → 修规则 → 重放。
面试常问
Q:EDA 的标准流程是什么? 先审计(缺失、类型、范围),再按四类问题展开:分布(单字段)、趋势(时间)、关系(字段间)、异常(离群与断点)。每张图带着问题画,结论要能指导下一步建模决策,否则就是无效图表。
Q:长尾分布怎么处理? 看场景:统计描述用中位数/分位数代替均值;可视化用对数刻度;建模时对目标变量 log1p 变换(预测完 expm1 回来);必要时分段建模(头部/尾部分开)。
Q:怎么区分”数据问题”和”真实现象”? 三个证据:来源维度切片(异常是否集中在某个 source)、时间维度对账(异常是否从某个采集批次开始)、raw 层回溯(原始数据就这样还是清洗引入)。三处都指向数据本身,才能当真实现象对待。
Q:为什么 EDA 报告要归档而不是画完就扔? EDA 结论依赖特定数据快照。数据更新后结论可能失效,归档报告(绑定快照与 commit)让”当时的判断依据”可追溯;复查时新报告与旧报告 diff,数据漂移一目了然。
💬 评论
这套方案在哪一步卡住了?也欢迎补充你的实现方式或踩坑经验。