第三章:数据清洗与预处理——去重、异常值处理、缺失值填充、时间戳对齐、数据标准化
各位好,我是老张。在区块链审计这行摸爬滚打了快十年,今天咱们聊聊数据清洗与预处理。
说实话,很多人觉得审计的核心是分析,是找漏洞。但我得告诉你,真正决定审计质量的,往往是数据清洗这一步。你想想看,垃圾数据进去,再牛的分析模型也白搭。我在项目中遇到过太多次了,明明逻辑没问题,结果因为数据没洗干净,结论全偏了。
3.1 数据去重:别让重复数据骗了你
区块链上的数据,按理说是不可篡改的。但实际审计时,你会发现各种重复记录。比如同一个交易被节点重复广播,或者链下数据采集时接口重试导致的重复。
为什么必须去重?
重复数据会直接扭曲你的统计结果。交易笔数翻倍、金额翻倍、地址出现次数翻倍……这些错误会直接导致审计结论失效。
我常用的去重策略:
- 基于交易哈希去重:这是最基础的方法。每个交易都有唯一的哈希值,直接按哈希去重就行。
- 基于时间戳+地址+金额组合去重:有些场景下,哈希可能被篡改或伪造。这时候我会用「时间戳+发送地址+接收地址+金额」的组合来判定重复。
- 基于区块高度+交易索引去重:对于链上数据,区块高度和交易索引的组合是天然的唯一键。
避坑指南:我曾经在处理一个DeFi项目的交易数据时,发现同一个哈希出现了两次。一开始以为是节点同步问题,后来发现是跨链桥的合约bug,导致同一笔交易被记录到了两条链上。如果只按哈希去重,就会漏掉这个重大漏洞。
# 伪代码示例:基于组合键去重
def deduplicate_transactions(tx_list):
seen = set()
unique_txs = []
for tx in tx_list:
# 组合键:时间戳 + 发送地址 + 接收地址 + 金额
key = (tx['timestamp'], tx['from'], tx['to'], tx['value'])
if key not in seen:
seen.add(key)
unique_txs.append(tx)
return unique_txs
3.2 异常值处理:揪出那些「不对劲」的数据
异常值,说白了就是那些明显偏离正常范围的数据点。在区块链审计中,异常值往往意味着问题所在——可能是黑客攻击、可能是合约漏洞、也可能是数据采集错误。
常见的异常值类型:
- 金额异常:比如一笔转账金额是正常交易的1000倍
- 频率异常:某个地址在1秒内发起了100笔交易
- Gas费异常:Gas费突然飙升至正常值的100倍
- 时间异常:交易时间戳明显早于或晚于区块时间
我处理异常值的流程:
- 统计描述:先看数据的分布情况。均值、中位数、标准差、四分位数……这些基础统计量能帮你快速定位异常。
- IQR方法:用四分位距(IQR)来判定异常。通常,低于Q1-1.5*IQR或高于Q3+1.5*IQR的数据点被视为异常。
- Z-Score方法:对于正态分布的数据,Z-Score绝对值大于3的点可以视为异常。
- 业务规则校验:这是最重要的。比如某个代币的精度是18位,但交易金额出现了小数点后20位,这肯定是异常。
个人经验:我建议不要一上来就删除异常值。先标记,再分析。有时候异常值恰恰是审计发现的关键线索。我记得有一次,一个地址的转账金额全是整数,没有小数——这明显不符合正常交易习惯,后来查出来是洗钱行为。
# 伪代码示例:IQR异常检测
def detect_outliers_iqr(data):
Q1 = np.percentile(data, 25)
Q3 = np.percentile(data, 75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = [x for x in data if x < lower_bound or x > upper_bound]
return outliers
3.3 缺失值填充:别让空数据毁了你的分析
区块链数据按理说应该是完整的,但实际审计中,缺失值很常见。比如链下数据采集时网络中断、某些字段在特定合约中不存在、或者数据源本身就不完整。
缺失值的处理策略:
- 删除法:如果缺失比例很小(比如<5%),直接删除缺失行。简单粗暴,但有效。
- 填充法:
- 均值/中位数填充:适用于数值型字段,比如Gas费、交易金额
- 众数填充:适用于类别型字段,比如交易类型、合约地址
- 前向/后向填充:适用于时间序列数据,比如用上一个区块的数据填充当前区块的缺失值
- 模型预测填充:对于关键字段,我会用机器学习模型来预测缺失值。比如用历史交易数据预测缺失的Gas费。
注意:我曾经在处理一个跨链桥的审计数据时,发现大量缺失的「目标链交易哈希」。一开始以为是数据采集问题,后来发现是跨链桥的合约存在bug,导致部分交易没有成功跨链。如果直接填充了这些缺失值,就会掩盖这个严重问题。
所以,我的原则是:先分析缺失原因,再决定填充策略。
# 伪代码示例:基于时间序列的前向填充
def forward_fill(data, column):
last_valid = None
filled_data = []
for row in data:
if row[column] is not None:
last_valid = row[column]
filled_data.append(row[column])
else:
filled_data.append(last_valid)
return filled_data
3.4 时间戳对齐:让不同数据源「说同一种语言」
区块链审计中,数据往往来自多个源:链上数据、链下数据、交易所数据、钱包数据……这些数据的时间戳格式可能完全不同。有的用Unix时间戳(秒级),有的用毫秒级,有的用ISO 8601格式,还有的用区块高度代替时间。
时间戳对齐的核心步骤:
- 统一格式:把所有时间戳转换为统一的格式。我个人习惯用Unix时间戳(毫秒级),精度高且便于计算。
- 时区处理:区块链是去中心化的,节点遍布全球。时间戳通常用UTC,但链下数据可能用本地时间。一定要统一到UTC。
- 精度对齐:有的数据精确到秒,有的精确到毫秒。对齐时,建议以最高精度为准。
- 区块时间 vs 交易时间:链上数据通常有区块时间和交易时间两个维度。审计时,我一般以区块时间为主,因为它是共识后的时间,更可靠。
避坑指南:我曾经在审计一个DEX项目时,发现链上交易时间和链下订单时间差了整整2小时。一开始以为是时区问题,后来发现是链下服务器的时间没有同步NTP。这个时间差导致我们误判了交易的先后顺序,差点得出错误结论。
# 伪代码示例:时间戳统一转换
import datetime
def normalize_timestamp(ts, source_format):
if source_format == 'unix_seconds':
return ts * 1000 # 转换为毫秒
elif source_format == 'unix_milliseconds':
return ts
elif source_format == 'iso8601':
dt = datetime.datetime.fromisoformat(ts)
return int(dt.timestamp() * 1000)
elif source_format == 'block_height':
# 需要根据区块高度查询对应的时间
return get_block_time(ts)
else:
raise ValueError(f"Unknown format: {source_format}")
3.5 数据标准化:让不同量纲的数据「平起平坐」
区块链审计中,数据的量纲差异巨大。交易金额可能是0.0001 ETH到10000 ETH,Gas费可能是1 Gwei到1000 Gwei,地址出现次数可能是1次到100万次。如果不做标准化,那些数值大的特征会主导分析结果。
常用的标准化方法:
- Min-Max标准化:将数据缩放到[0,1]区间。公式:(x - min) / (max - min)。适用于数据分布比较均匀的情况。
- Z-Score标准化:将数据转换为均值为0、标准差为1的分布。公式:(x - mean) / std。适用于数据近似正态分布的情况。
- 对数变换:对于长尾分布的数据(比如交易金额),取对数可以压缩量纲。公式:log(x+1)。
- Robust标准化:使用中位数和四分位距,对异常值不敏感。公式:(x - median) / IQR。
我的建议:对于区块链审计数据,我通常先用对数变换处理金额类数据,再用Z-Score标准化。因为交易金额往往呈长尾分布,直接Min-Max会被极端值带偏。你想想看,一笔100万ETH的交易和一笔0.1ETH的交易,如果直接Min-Max,小金额几乎被压缩到0了。
# 伪代码示例:对数变换 + Z-Score标准化
import numpy as np
def standardize_amounts(amounts):
# 先取对数
log_amounts = np.log(np.array(amounts) + 1)
# 再Z-Score标准化
mean = np.mean(log_amounts)
std = np.std(log_amounts)
standardized = (log_amounts - mean) / std
return standardized
3.6 知识体系总览
说了这么多,咱们用一张图来总结一下数据清洗与预处理的完整流程:
这张图把整个流程串起来了。从原始数据开始,经过去重、异常值处理、缺失值填充、时间戳对齐,最后到数据标准化。每一步都有它的意义,缺一不可。
好了,数据清洗与预处理这部分就聊到这儿。记住一句话:数据清洗不是体力活,而是技术活。你花在清洗上的每一分钟,都会在后续分析中加倍回报给你。