跳到内容

第 12 卷 / 第 2 章 / 第 1 课

2.1 数据剖析、类型与缺失机制:先解释空值,再决定怎样补 ​

温度列同时包含空白、N/A、-999 和真正的零。零可能是有效观测,其他值是否表示缺失要看来源契约。先把各种标记合并、再把所有空值填满,可能让错误披着合理数值的外衣继续流转。

本课目标 ​

  • 建立结构化数据剖析报告;
  • 区分物理类型、逻辑类型与业务约束;
  • 理解 MCAR、MAR、MNAR 和结构性不适用;
  • 根据分析目标选择删除、填补、标记或建模。

1. 数据质量不只有空值数量 ​

常见维度:

  • completeness:必需字段是否存在;
  • validity:值是否满足类型、范围和枚举;
  • uniqueness:业务键是否唯一;
  • consistency:跨字段、跨表是否一致;
  • timeliness:是否在承诺时间到达;
  • accuracy:是否接近真实世界事实;
  • integrity:引用与关系是否完整。

accuracy 往往不能只靠表内统计验证。例如温度 35 在范围内,却可能来自校准错误的传感器。

2. 先保留原始文本再解析 ​

本课 Python 代码按顺序运行,使用 pandas 3.0 和 NumPy。CSV 小样本直接写在代码中,方便核对解析行为。

python
from io import StringIO
import numpy as np
import pandas as pd

csv_text = """sensor_id,observed_at,temperature,status
A,2026-08-03T10:00:00Z,0,ok
A,2026-08-03T10:01:00Z,,
A,2026-08-03T10:02:00Z,N/A,
B,2026-08-03T10:00:00Z,20,ok
B,2026-08-03T10:01:00Z,-999,
B,bad-date,oops,warning
B,2026-08-03T10:03:00Z,inf,warning
"""
raw = pd.read_csv(StringIO(csv_text), dtype="string", keep_default_na=False)
print(raw.shape)
print(raw.dtypes)
print(raw.head())

先按文本读取可以观察源文件究竟使用哪些缺失标记,避免 NA、NULL 或空字符串被库默认规则提前合并。生产管道可直接声明 schema,但失败记录仍应保留原文和错误原因。

建立列级 profile:

python
def profile_text_column(series: pd.Series) -> pd.Series:
    normalized = series.str.strip()
    return pd.Series({
        "rows": len(series),
        "null": series.isna().sum(),
        "empty": normalized.eq("").sum(),
        "distinct": normalized.nunique(dropna=False),
        "top_values": normalized.value_counts(dropna=False).head(5).to_dict(),
    })

print(profile_text_column(raw["temperature"]))

DataFrame.info() 会自行打印并返回 None。需要结构化报告时应自己计算,而不是 print(df.info())。

这里去首尾空白是该字段的剖析策略,不应全表照搬;有些标识符中的空格有实际意义。原文仍保留,null 与 empty 也分别统计库中的空值和空字符串。

3. 物理类型不等于逻辑类型 ​

CSV 中所有值都是文本。解析成 int64 仍不能说明该列是可相加的数值:邮政编码、订单号和电话号码可能只是在字符集上由数字组成。

每列应声明:

text
physical type:存储表示
logical type:日期、金额、类别、标识符等
unit:°C、ms、bytes
nullable:何时允许空
constraints:范围、正则、枚举、跨字段关系

日期 01/02/2026 在不同区域可解释为 1 月 2 日或 2 月 1 日。不要让解析库猜;契约应规定 ISO 8601 或明确格式。

python
parsed_time = pd.to_datetime(
    raw["observed_at"],
    format="%Y-%m-%dT%H:%M:%SZ",
    errors="coerce",
    utc=True,
)
invalid_time = parsed_time.isna() & raw["observed_at"].str.strip().ne("")

errors="coerce" 会把错误变成 NaT,所以必须另存 invalid_time,否则格式错误会被伪装成普通缺失。

解析成功只说明格式和时区可处理,未来时间、事件先后关系仍需另行校验。

4. 统一缺失标记但保留原因 ​

python
MISSING_TOKENS = {"", "N/A", "NA", "NULL", "-999"}

temperature_text = raw["temperature"].str.strip()
source_missing = temperature_text.isin(MISSING_TOKENS)
missing_reason = pd.Series(pd.NA, index=raw.index, dtype="string")
missing_reason = missing_reason.mask(source_missing, "source_missing")
temperature = pd.to_numeric(
    temperature_text.mask(source_missing), errors="coerce"
)
parse_failed = temperature.isna() & ~source_missing
missing_reason = missing_reason.mask(parse_failed, "parse_error")
nonfinite = temperature.notna() & ~np.isfinite(temperature)
missing_reason = missing_reason.mask(nonfinite, "nonfinite")
temperature = temperature.mask(nonfinite)

这里把 -999 当缺失只是数据契约示例。如果业务允许真实 -999,这个规则会毁掉有效值。占位符必须按来源和字段配置,不能全表统一替换。

source_missing 只表示命中了来源缺失标记,并不解释统计上的缺失原因;具体标记仍在 raw 中。inf 可以解析成数值,却不满足有限值约束。解析错误和无穷值都不能悄悄领到一个替补温度。

5. 缺失机制决定偏差 ​

统计学常区分:

  • MCAR:缺失与已观测、未观测值都无关;
  • MAR:给定已观测变量后,缺失不再依赖缺失值本身;
  • MNAR:即使考虑已观测变量,缺失仍与未观测值有关。

“传感器偶尔断连”不自动证明 MCAR;断连可能与温度、地点或设备负载相关。MAR/MNAR 通常无法仅凭当前数据完全验证,需要采集机制、领域知识和敏感性分析。

还有结构性不适用:例如未取消订单没有 cancellation_reason。它不是数据丢失,不应填成“平均取消原因”。最好显式编码 not_applicable 或由状态字段解释。

6. 处理策略服务于下游问题 ​

删除 ​

删除行或列会改变样本组成。只有缺失机制和分析目标允许时才合理。不存在普遍适用的“缺失超过 70% 就删列”;低覆盖字段可能对小群体关键,高覆盖字段也可能因偏差不可用。

单值填补 ​

中位数填补比均值填补更不易受极端值影响,但仍会改变方差和相关性。缺失指示可以保留一部分信息,是否改善模型要验证;填补策略也应在报告中披露。

python
if not temperature.notna().any():
    raise ValueError("No valid training temperature for median imputation")
median_temperature = temperature.median()
clean = raw.assign(
    temperature_raw=raw["temperature"],
    temperature=temperature.mask(source_missing, median_temperature),
    temperature_missing=temperature.isna(),
    temperature_missing_reason=missing_reason,
)

上一段把小样本视为训练数据:有效温度是 0 和 20,中位数为 10。只填来源缺失,解析错误和非有限值继续留空待处理。训练列全空时,需要明确兜底策略或报错。

时序前向填充 ​

ffill 隐含“上次状态持续有效”,并要求先按实体和事件时间排序。跨设备直接前填会把一个设备状态填给另一个。

python
frame = raw.assign(
    observed_at=parsed_time,
    status=raw["status"].replace("", pd.NA),
)
invalid_order = frame["observed_at"].isna() | frame["sensor_id"].eq("")
ordering_quarantine = frame.loc[invalid_order].copy()
ordered = frame.loc[~invalid_order].sort_values(["sensor_id", "observed_at"])
if ordered.duplicated(["sensor_id", "observed_at"]).any():
    raise ValueError("Equal event times require a source sequence rule")
ordered["status"] = ordered.groupby("sensor_id")["status"].ffill(limit=3)

limit=3 限制连续缺失的行数,不限制经过的时间。采样不规则时,三行可能横跨数小时,还需要最大有效时长规则。示例先隔离无效时间;同一设备时间相同的记录则需要约定来源序号。

模型或多重填补 ​

模型方法能利用其他变量,但效果取决于填补模型和缺失机制假设。多重填补生成多份合理数据,再合并估计值以及填补内、填补间的不确定性;只选一份数据会丢掉这层信息。可参阅 statsmodels MICE 文档与 scikit-learn 填补指南。

7. 训练集与评估集不能互相泄漏 ​

用于模型的中位数、众数、编码表和填补模型只能在训练集拟合,再原样应用到验证、测试和线上数据:

text
fit(training) → imputation parameters
transform(training / validation / test / online)

先对全数据填补再切分,会让验证集分布影响训练过程,产生数据泄漏。时间序列还应按时间边界拟合,不能用未来数据填过去。

常见误区 ​

  • 空值都代表同一件事:未知、不适用、解析失败和权限遮蔽应区分。
  • 缺失比例决定处理方法:机制、用途、群体影响和不确定性更重要。
  • 中位数填补不会引入偏差:它仍改变分布和关系。
  • errors="coerce" 已完成清洗:它隐藏了解析失败,必须保留错误标记。

练习 ​

  1. 为温度列设计原始值、解析值和 missing reason 三列。
  2. 给一个可能属于 MCAR、MAR、MNAR 的采集案例,并说明还需什么证据。
  3. 找一个低覆盖但业务价值高、不应直接删除的字段。
  4. 把全量填补代码改成 fit/transform,验证测试集不参与参数计算。

小结 ​

缺失值不是一个 pandas API 问题。先保留原文、声明逻辑类型、识别缺失原因,再选择与下游决策相容的处理方法。填补得到的是建模假设,不是恢复出的真实观测。

下一课处理另一类危险数据:它们不为空,甚至看起来格式完全合法,却可能是极端事件、传感器故障或重复投递。

Built with VitePress | Software Systems Atlas