跳到内容

第 12 卷 / 第 3 章 / 第 1 课

3.1 EDA 与分布诊断:先理解一行数据,再解释一张图 ​

清洗后的表有十万行、五十列。数据平台的仪表板能在一秒内画出直方图,却不会告诉你每一行是一次任务、一次重试,还是每天的任务汇总。粒度不清,任何均值都可能把单位算错。

本课目标 ​

  • 从数据生成过程、观察单位和覆盖范围开始 EDA;
  • 为数值、类别、比例和时间变量选择摘要;
  • 识别直方图分箱、长尾、截断和缺失造成的假象;
  • 将探索发现写成可验证假设,而不是直接下结论。

1. 先写数据说明卡 ​

在画图前回答:

text
一行代表什么?
主键或重复单位是什么?
覆盖哪些时间、地区与群体?
数据如何抽样或过滤?
哪些字段在采集后计算?
缺失和迟到如何处理?
当前版本经历了哪些清洗?

若同一 mission_id 因重试出现多行,事件失败数与任务失败数回答的是两个问题。若一行是每日汇总,等权均值描述平均一天,按任务数加权则描述平均一次任务。先写清单位,再按计算器。

EDA 应同时查看 raw 与 curated:raw 帮助发现采集、解析和缺失问题;curated 用于理解清洗后分析数据。只在清洗后探索,会看不到规则删除了什么。

2. 建立覆盖与粒度检查 ​

以下代码使用 pandas 3.0 和 Matplotlib,按顺序运行。八条事件对应四个任务,后面的事件摘要不能直接标成任务摘要。

python
import pandas as pd

frame = pd.DataFrame({
    "event_id": [f"e{i}" for i in range(8)],
    "mission_id": [f"M{i // 2}" for i in range(8)],
    "occurred_at": pd.to_datetime(
        ["2026-08-01T10:00:00Z"] * 4 + ["2026-08-03T10:00:00Z"] * 4
    ),
    "mission_type": ["survey", "repair"] * 4,
    "duration_minutes": [5, 10, 12, 20, 40, 60, 120, None],
})
overview = {
    "rows": len(frame),
    "missions": frame["mission_id"].nunique(),
    "min_time": frame["occurred_at"].min(),
    "max_time": frame["occurred_at"].max(),
    "missing_event_ids": frame["event_id"].isna().sum(),
    "duplicate_event_ids": frame["event_id"].dropna().duplicated().sum(),
}
print(overview)

行数、实体数和事件数的比例能快速暴露粒度。再按来源、日期、schema version 和状态分组,检查某个分区是否缺失或突然激增。

3. 数值变量看中心、离散和形状 ​

python
duration = frame["duration_minutes"]
summary = duration.describe(percentiles=[0.01, 0.1, 0.5, 0.9, 0.99])
summary["missing_rate"] = duration.isna().mean()
print(summary)

均值适合表达总和分摊,但对长尾敏感;中位数描述典型位置,却不保留总量信息;标准差依赖均值和平方偏差,IQR 对极端值更稳健。

报告分位数时还需说明:是否包含失败记录、零值、截尾值和缺失填补值。摘要数字只有连同分析集合才可复现。

4. 直方图由分箱决定 ​

python
import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2, figsize=(12, 4))
duration.plot.hist(bins=20, ax=axes[0], title="20 bins")
duration.plot.hist(bins=80, ax=axes[1], title="80 bins")
for axis in axes:
    axis.set_xlabel("Duration (minutes)")
plt.tight_layout()
plt.show()

分箱太宽会隐藏多峰,太窄会放大随机波动。至少尝试几种宽度,并把业务边界、测量精度和样本量纳入选择。

这个小样本只有七个已观测时长,80 箱图大部分是空白。它演示分箱敏感性,不能证明存在 80 种运行状态。图中不包含缺失时长,应同时报告缺失率。

对跨数量级正值可考虑对数坐标,但要说明零和负值如何处理。对数坐标展示相对变化,不应在图注中伪装成线性尺度。

5. 长尾与截断透露生成过程 ​

持续时间上限集中在恰好 60、120 分钟,可能来自超时规则;整数在 999 或 9999 堆积,可能是上限编码;某个时间点后精度突然改变,可能是 schema 或设备版本升级。

这些结构不应立即平滑。先按以下维度切分:

  • source / device model;
  • schema version;
  • 时间与发布批次;
  • 地区、渠道、任务类型;
  • 是否被清洗或截尾。

双峰可能来自两类群体混合,也可能来自批次错误。图形提出假设,来源元数据帮助验证。

6. 类别变量同时看数量和比例 ​

python
counts = frame["mission_type"].value_counts(dropna=False)
shares = frame["mission_type"].value_counts(
    dropna=False, normalize=True
)
category_summary = pd.DataFrame({"count": counts, "share": shares})
print(category_summary)

只看百分比会隐藏小样本,只看数量又难比较规模不同的时期。图表应同时提供分母。

高基数类别不要画上千根柱。可展示 top-N 与“其他”,但“其他”内部可能变化很大,分析表仍需保留完整类别。

未知、缺失和不适用应分开。把空值排除后重新归一化,会让已知类别比例看起来稳定,掩盖采集质量下降。

7. 比率必须回到分子分母 ​

一行 success_rate=0.8 可能来自 4/5,也可能来自 8000/10000,两者不确定性完全不同。

EDA 中保留:

text
success_count
attempt_count
success_rate = success_count / attempt_count

比较分组率时先画分母分布,并检查不同任务难度、曝光和观察窗口。平均各行 rate 通常不等于总成功数除以总尝试数;选择宏平均还是微平均取决于问题。

例如两组分别为 9/10 和 1/100,宏平均是 45.5%,汇总率则是 10/110,约 9.09%。两者都没算错:前者给每组相同权重,后者给每次尝试相同权重。尝试数为零的组,比率未定义,不能当作零成功率。

8. 时间序列先区分缺零与真零 ​

某天没有行,可能是没有事件,也可能是分区没到。重采样填零前必须确认业务语义。

python
observed = (
    frame.assign(day=frame["occurred_at"].dt.floor("D"))
    .groupby(["day", "mission_type"], dropna=False)
    .size().rename("observed_events").reset_index()
)
coverage = pd.MultiIndex.from_product(
    [pd.date_range("2026-08-01", "2026-08-03", tz="UTC"), ["survey", "repair"]],
    names=["day", "mission_type"],
).to_frame(index=False)
coverage["partition_complete"] = [True, True, True, False, True, True]
daily = coverage.merge(observed, on=["day", "mission_type"], how="left", validate="1:1")
daily["events"] = daily["observed_events"].fillna(0).astype("Int64").where(
    daily["partition_complete"]
)
print(daily)

事件数还应结合活跃用户、设备在线数或曝光量。总量增长可能只是覆盖面扩大,不代表单位表现改善。

覆盖清单声明 8 月 2 日 survey 分区完整,所以无行可记为零;当天 repair 分区不完整,events 保持缺失。生产中的完整性要来自接入清单,不能看事件表猜。重采样计数可能自动把空时间箱写成零,按列计数还会跳过空 ID;示例因此使用行数和显式覆盖网格。业务日若按本地时区定义,也应先转换时区再按日分桶。

9. EDA 输出是问题清单 ​

记录格式:

text
观察:v3 设备的温度分布比 v2 高约 8°C
可能解释:校准差异、部署地区不同、真实硬件变化
需要数据:设备地区、校准记录、版本切换日期
验证方式:同地区同工况配对比较
当前结论:未确定,不能合并版本后直接比较

把观察、解释和结论分开,可避免探索阶段把偶然模式写成事实。

常见误区 ​

  • 清洗前不能做 EDA:raw profiling 正是发现清洗需求的重要手段。
  • 均值和中位数谁更真实:它们回答不同问题。
  • 直方图形状是数据唯一属性:分箱与坐标会改变呈现。
  • 比率可以脱离分母比较:小样本率波动更大,权重也不同。

练习 ​

  1. 给一张事件表写数据说明卡,明确行粒度与重复单位。
  2. 用三种分箱画同一分布,记录哪些结构稳定存在。
  3. 构造宏平均与微平均结论相反的比率数据。
  4. 找出一段时间序列中的“缺日期”,判断是零事件还是缺分区。

小结 ​

EDA 先审问数据生成过程,再审问数值。粒度、覆盖、缺失、分母和版本决定图中形状能否解释。探索的产物应是一组有证据需求的假设,不是漂亮图后的确定语气。

下一课把单变量扩展为关系图,并处理相关、混杂、分组悖论与不确定性表达。

Built with VitePress | Software Systems Atlas