3. 数据探查与剖析实战

拿到一批来自不同实验设备、不同采集批次的数据后,最忌讳的做法是直接开始清洗。清洗规则必须由数据本身的问题决定,而问题需要先被"看见"。本节围绕四类探查手段展开:描述性统计看数值分布与量纲,频率分布看类别构成与异常取值,缺失值矩阵看缺失的位置与模式,唯一值分析看主键、重复与常量列。四者组合,能在十几分钟内形成对数据集结构与问题的整体判断。

一、先建立数据画像:字段清单与类型判定

探查的第一步不是算统计量,而是把字段按语义分组。假设我们拿到三份实验数据:设备A导出的run_a.csv(含sample_id, batch, temp_c, pressure_kpa, ph, operator, measure_time),设备B导出的run_b.csv(含sample_id, batch, temp, press, ph_value, measure_time),以及一份人工记录的meta.xlsx(含sample_id, group, reagent_lot)。此时应先把字段分为四类:标识列(sample_id)、分组列(batch、group、operator)、测量列(temp、pressure、ph)、时间列(measure_time)。类型判定要警惕"看起来是数字的字符串"——例如temp列中混入"25.3*"或"ND",pandas 读入后整列会变成 object 类型,后续统计全部失效。因此读入时应显式指定 dtype 或先用 pd.to_numeric(errors='coerce') 试探转换,统计转换后产生的 NaN 数量,这个数量本身就是一条重要问题线索。

二、描述性统计:量纲、偏态与可疑极值

对测量列调用 describe() 并补充偏度与缺失率,是性价比最高的动作。以温度为例,若设备A的temp_c给出 count=1180、mean=24.8、std=1.9、min=22.1、50%=24.7、max=26.3,而设备B的temp给出 count=1195、mean=297.6、std=2.1、min=295.2、max=299.8,则一眼可判断:B的温度单位是开尔文,需减去273.15后再合并。压力列若出现 min=0 而 25% 分位数是 101.2,说明存在零值占位,很可能是缺失被填成了0,这类"伪有效值"必须靠最小值与低分位数对比才能发现。pH 列若 max=14.2,则超出理论范围,属于录入或传感器饱和问题。偏度绝对值大于1时,提示分布长尾,后续标准化应考虑对数变换或稳健缩放,而不是直接做 z-score。

描述性统计还应横向对比:同一物理量在不同来源下的均值差、标准差比。若设备A的 pH 标准差为0.05而设备B为0.42,在排除真实实验差异后,应怀疑B的传感器漂移或读数精度不足,这类判断直接影响后续是否对B做平滑或降权。

三、频率分布:类别构成与"隐形异常值"

对分组列做 value_counts(dropna=False),重点看三件事:类别是否拼写不一致(如 Batch01 与 batch01、张三 与 张三 带尾空格)、是否存在低频类别(出现1~2次的批次往往是录入错误或测试残留)、缺失是否被编码成特殊字符串("-"、"NA"、"未知")。对测量列则用分箱后的直方图或 value_counts 观察离散化取值:若某压力列 90% 的记录集中在 101.3,其余散落,说明该列可能大部分是默认值填充。频率分布还能暴露"哨兵值",例如温度列中 -999 出现 37 次,这类值在均值上会被稀释,但在频次表里非常醒目。

多源实验数据集探查:四类视图 → 问题定位 → 清洗决策 run_a.csv(设备A) temp_c / pressure_kpa run_b.csv(设备B) temp / press / ph_value meta.xlsx(人工记录) group / reagent_lot ① 描述性统计 count / mean / std min / 25% / 50% / max 偏度、缺失率 看:量纲、零值占位、越界 看:长尾与来源间差异 例:B温度均值297.6 → 开尔文 ② 频率分布 value_counts(dropna=False) 分箱直方图 类别拼写与低频类 看:大小写/空格不一致 看:哨兵值 -999、默认值堆积 例:压力列90%为101.3 ③ 缺失值矩阵 isna() 按行/列聚合 缺失率热力矩阵 共缺模式(同现) 看:整列缺失、整段缺失 看:缺失是否与批次相关 例:B设备第3批整段为空 ④ 唯一值分析 nunique / 重复行计数 主键唯一性校验 常量列识别 看:sample_id 是否重复 看:单值列(无信息量) 例:operator 仅1人 → 常量列 问题清单 · 单位不一致 · 零值/哨兵占位 · 越界读数 · 类别拼写不一 · 低频异常批次 · 整列/整段缺失 · 缺失与批次相关 · 主键重复 · 常量列 · 类型被污染 · 长尾需变换 → 决定清洗规则 → 决定合并策略 → 决定是否回采 探查结论示例: B设备温度需 K→℃ 换算;压力零值按缺失处理;batch 大小写统一;sample_id 重复 12 条需去重;operator 为常量列可剔除;pH 越界 3 条回查原始记录。

四、缺失值矩阵:位置比数量更重要

缺失率本身只是标量,真正有价值的是缺失的空间模式。做法是先 df.isna() 得到布尔矩阵,再按列求均值得到缺失率,按行求和得到每条记录的缺失字段数,最后按批次分组统计缺失率。三种典型模式对应三种处理策略:随机散点缺失(各批次缺失率接近,字段间无共现)可考虑插补;整列缺失(某字段缺失率接近100%)应直接剔除该字段并回溯采集配置;整段缺失(某批次某时间段全部为空)往往意味着设备停机或文件截断,属于结构性缺失,插补会制造虚假数据,正确做法是标记该段为无效并回采。若发现 ph 与 pressure 的缺失高度共现(同一批记录同时为空),说明二者来自同一采集通道,应作为一个整体判断而非分别插补。

五、唯一值分析:主键、重复与常量列

对每个字段计算 nunique(dropna=False),并检查 sample_id 是否满足唯一性。若 len(df) - df['sample_id'].nunique() = 12,说明存在12条重复主键,需进一步用 duplicated(subset='sample_id', keep=False) 定位,判断是完全重复行还是同一样本多次测量(后者应保留但需加测量序号)。nunique=1 的列是常量列,例如 operator 全为同一人,对建模无信息量,可在合并前剔除以免干扰。此外,nunique 异常高的类别列(如 batch 有200个取值而样本仅1200条)提示编码粒度过细或存在自由文本污染,需要规范化。

六、把四类视图串成一次探查流程

实际操作可按如下顺序执行,每步产出一条结论再进入下一步:

import pandas as pd
a = pd.read_csv("run_a.csv", dtype=str)
b = pd.read_csv("run_b.csv", dtype=str)
m = pd.read_excel("meta.xlsx", dtype=str)

# 1 类型试探:统计无法转数值的个数
for col in ["temp_c", "pressure_kpa", "ph"]:
    bad = pd.to_numeric(a[col], errors="coerce").isna().sum()
    print(col, "非数值/缺失:", bad)

# 2 描述性统计(转换后)
num = a[["temp_c", "pressure_kpa", "ph"]].apply(pd.to_numeric, errors="coerce")
print(num.describe().T)
print("偏度:", num.skew())

# 3 频率分布
print(a["batch"].value_counts(dropna=False).head(10))
print(a["pressure_kpa"].value_counts().head(5))

# 4 缺失矩阵:按批次看缺失率
miss = a.isna().groupby(a["batch"]).mean()
print(miss)

# 5 唯一值
print("sample_id 重复数:", a["sample_id"].duplicated().sum())
print(a.nunique(dropna=False).sort_values())

示例中若输出显示 temp 非数值37条、pressure_kpa 最小值0且频次表首位为101.3、batch 出现 Batch01 与 batch01 两个键、sample_id 重复12条、operator 唯一值为1,则探查结论可直接写成一张问题—证据—处置的对照表,作为下一节清洗规则的输入。需要强调的是,上述数值均为教学示例,真实项目应以实际输出为准;探查阶段不修改数据,只记录证据,避免"边看边改"导致问题被掩盖而无法追溯。

探查阶段建议保留原始文件只读,所有统计在副本上执行,并把每步输出(缺失率、频次表、重复主键清单)落盘为独立文件。这样后续清洗规则可以逐条对应到具体证据,复盘时能回答"为什么这条记录被剔除"。