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 小样本直接写在代码中,方便核对解析行为。
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:
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 仍不能说明该列是可相加的数值:邮政编码、订单号和电话号码可能只是在字符集上由数字组成。
每列应声明:
physical type:存储表示
logical type:日期、金额、类别、标识符等
unit:°C、ms、bytes
nullable:何时允许空
constraints:范围、正则、枚举、跨字段关系日期 01/02/2026 在不同区域可解释为 1 月 2 日或 2 月 1 日。不要让解析库猜;契约应规定 ISO 8601 或明确格式。
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. 统一缺失标记但保留原因
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% 就删列”;低覆盖字段可能对小群体关键,高覆盖字段也可能因偏差不可用。
单值填补
中位数填补比均值填补更不易受极端值影响,但仍会改变方差和相关性。缺失指示可以保留一部分信息,是否改善模型要验证;填补策略也应在报告中披露。
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 隐含“上次状态持续有效”,并要求先按实体和事件时间排序。跨设备直接前填会把一个设备状态填给另一个。
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. 训练集与评估集不能互相泄漏
用于模型的中位数、众数、编码表和填补模型只能在训练集拟合,再原样应用到验证、测试和线上数据:
fit(training) → imputation parameters
transform(training / validation / test / online)先对全数据填补再切分,会让验证集分布影响训练过程,产生数据泄漏。时间序列还应按时间边界拟合,不能用未来数据填过去。
常见误区
- 空值都代表同一件事:未知、不适用、解析失败和权限遮蔽应区分。
- 缺失比例决定处理方法:机制、用途、群体影响和不确定性更重要。
- 中位数填补不会引入偏差:它仍改变分布和关系。
errors="coerce"已完成清洗:它隐藏了解析失败,必须保留错误标记。
练习
- 为温度列设计原始值、解析值和 missing reason 三列。
- 给一个可能属于 MCAR、MAR、MNAR 的采集案例,并说明还需什么证据。
- 找一个低覆盖但业务价值高、不应直接删除的字段。
- 把全量填补代码改成
fit/transform,验证测试集不参与参数计算。
小结
缺失值不是一个 pandas API 问题。先保留原文、声明逻辑类型、识别缺失原因,再选择与下游决策相容的处理方法。填补得到的是建模假设,不是恢复出的真实观测。
下一课处理另一类危险数据:它们不为空,甚至看起来格式完全合法,却可能是极端事件、传感器故障或重复投递。