3. 数据质量探查实战
逐笔成交数据是清洗与稳健统计工作的起点,而数据质量探查则是决定后续清洗策略的依据。本节以一份模拟的逐笔成交样本为例,完整走一遍“加载—类型检查—缺失统计—重复识别—分布概览”的探查链路。需要说明的是,下文出现的行数、缺失比例、重复条数等数值均为教学示例的合理假设,用于演示判断逻辑,并非真实市场数据的实测结果。
一、样本结构与加载
假设样本文件 trades_sample.csv 包含以下字段:trade_id(成交编号)、symbol(证券代码)、trade_time(成交时间,精确到毫秒)、price(成交价)、volume(成交量,股)、amount(成交金额,元)、side(主动方向,B/S)、trade_type(成交类型,如连续竞价、大宗、盘后固定价格)。加载时最关键的一点是不要让解析器擅自推断类型,尤其是证券代码、成交编号这类“看起来像数字”的字段,一旦被转成整型,前导零会丢失,后续按代码关联就会出错。
import pandas as pd
df = pd.read_csv(
"trades_sample.csv",
dtype={
"trade_id": "string",
"symbol": "string",
"side": "string",
"trade_type": "string",
},
parse_dates=["trade_time"],
encoding="utf-8",
)
print(df.shape) # 示例输出: (50000, 8)
print(df.dtypes)
这里把 trade_time 交给 parse_dates 处理,其余数值列保留默认推断,加载后再统一核对。若文件来自不同券商或不同导出工具,编码可能是 GBK,此时需显式指定 encoding="gbk",否则中文列名或成交类型会出现乱码,直接影响后续分类统计。
二、字段类型检查
类型检查的目标不是“看一眼 dtypes”,而是核对业务语义与存储类型是否一致。逐笔成交中常见的类型错位有三类:一是价格、金额被读成字符串(源文件含千分位逗号或货币符号);二是成交量被读成浮点(源文件含空值导致整列升格);三是时间列仍是 object 而非 datetime。可以按下面的方式逐列核对并给出期望类型:
expected = {
"trade_id": "string", "symbol": "string",
"trade_time": "datetime64[ns]",
"price": "float64", "volume": "float64",
"amount": "float64", "side": "string",
"trade_type": "string",
}
for col, want in expected.items():
got = str(df[col].dtype)
flag = "OK" if got == want else "需转换"
print(f"{col:12s} 期望={want:18s} 实际={got:18s} {flag}")
若 price 实际为 object,通常说明存在 "1,234.50" 这类带分隔符的字符串,应先 str.replace(",", "") 再 astype(float);若 volume 为 float64 但业务上应为整数股,则要检查是否存在小数成交量(部分品种允许零股或基金份额小数,需按品种规则判断,不能一刀切取整)。类型检查的结论应落到“哪些列需要转换、转换是否会损失信息”上,而不是简单罗列 dtype。
三、缺失值统计与重复记录识别
缺失统计要同时看缺失数量、缺失比例和缺失的分布位置。比例决定处理方式,位置决定缺失是否具有系统性。例如 side 缺失若集中在某几个 trade_type 上,往往说明该类成交本身不区分主动方向(如集合竞价撮合),属于“结构性缺失”,不应简单填充。
miss = df.isna().sum().to_frame("缺失数")
miss["缺失比例"] = (miss["缺失数"] / len(df)).round(4)
print(miss.sort_values("缺失数", ascending=False))
# 观察缺失是否与成交类型相关
print(df[df["side"].isna()]["trade_type"].value_counts())
重复记录识别不能只依赖 duplicated() 的整行比对。逐笔成交中更常见的是“业务重复”:同一 trade_id 出现多次,或 symbol + trade_time + price + volume 完全一致但编号不同。前者多为导出重复,后者可能是真实的同价同量多笔成交,也可能是撮合回报重复推送,必须结合编号规则判断。
dup_full = df.duplicated().sum() # 整行重复
dup_id = df.duplicated(subset=["trade_id"]).sum() # 编号重复
dup_key = df.duplicated(
subset=["symbol", "trade_time", "price", "volume"]
).sum()
print(dup_full, dup_id, dup_key)
示例中假设整行重复 12 条、trade_id 重复 35 条、业务键重复 210 条。整行重复可直接删除;trade_id 重复需先确认编号是否全局唯一,若唯一则保留首条;业务键重复的 210 条中,大部分可能是同价同量的连续成交,不能贸然去重,应保留并在清洗阶段用“同编号去重、同键标记”区分处理。
四、基础分布概览
分布概览的作用是发现“量纲异常”和“极端值”,为后续稳健统计提供依据。对价格、成交量、金额应看分位数而非只看均值,因为逐笔数据的均值极易被大单拉偏。对类别字段则看取值集合与占比,确认是否存在非法枚举。
num_cols = ["price", "volume", "amount"]
print(df[num_cols].describe(
percentiles=[0.001, 0.01, 0.5, 0.99, 0.999]
).T)
for c in ["side", "trade_type"]:
print(df[c].value_counts(dropna=False))
假设示例中 price 的 0.1% 分位为 0.00、99.9% 分位为 980.00,而中位数为 12.35,说明存在价格为 0 的异常记录;volume 最大值达到 9.9×10⁷ 股,明显偏离中位数 800 股,需核查是否为大宗成交或单位错误(股与手混用)。side 的取值应只有 B、S 及缺失,若出现 "b"、"BUY"、" " 等变体,说明枚举未规范化,需在清洗阶段统一。
五、探查结论如何转化为清洗动作
探查的终点是一份可执行的结论清单,而不是一堆数字。以本节示例为例,可归纳为:price 存在 0 值与千分位字符串,需先转数值再置零为缺失;volume 存在量纲混用嫌疑,需按品种核对单位;side 存在结构性缺失与枚举变体,需按 trade_type 分层处理;重复记录需按“整行删除、编号去重、业务键标记”三级策略处理。下面的图示把这条从原始样本到清洗动作的探查链路与关键判断点集中呈现,便于对照复查。
把上述结论落到代码上,可以形成一段可复用的探查脚本骨架:先加载并锁定关键列类型,再逐列核对期望类型,接着输出缺失表与三类重复计数,最后打印分位数与类别取值。每次更换样本文件时重跑同一脚本,对比输出差异,就能快速判断新数据是否引入了新的质量问题。这种“固定脚本 + 差异对比”的做法,比每次临时写查询更可靠,也便于把探查结论沉淀为团队共享的清洗规则。
side 缺失可能是正常的,而同价同量的连续成交去重则可能误删真实成交。正确顺序是先记录、再判断、后处理,并保留原始样本的只读副本以便回溯。完成本节探查后,样本的字段类型、缺失结构、重复层级和分布形态都已明确,后续的异常值识别与稳健统计才有可靠的输入基础。判断探查是否达标的简单标准是:每一列都能回答“它是什么类型、缺多少、为什么缺、有没有重复、分布是否合理”这五个问题,且每个答案都能对应到一条具体的清洗动作。