第三章:数据清洗与预处理——去重、异常值处理、缺失值填充、时间戳对齐、数据标准化

各位好,我是老张。在区块链审计这行摸爬滚打了快十年,今天咱们聊聊数据清洗与预处理。

说实话,很多人觉得审计的核心是分析,是找漏洞。但我得告诉你,真正决定审计质量的,往往是数据清洗这一步。你想想看,垃圾数据进去,再牛的分析模型也白搭。我在项目中遇到过太多次了,明明逻辑没问题,结果因为数据没洗干净,结论全偏了。

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倍
  • 时间异常:交易时间戳明显早于或晚于区块时间

我处理异常值的流程:

  1. 统计描述:先看数据的分布情况。均值、中位数、标准差、四分位数……这些基础统计量能帮你快速定位异常。
  2. IQR方法:用四分位距(IQR)来判定异常。通常,低于Q1-1.5*IQR或高于Q3+1.5*IQR的数据点被视为异常。
  3. Z-Score方法:对于正态分布的数据,Z-Score绝对值大于3的点可以视为异常。
  4. 业务规则校验:这是最重要的。比如某个代币的精度是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格式,还有的用区块高度代替时间。

时间戳对齐的核心步骤:

  1. 统一格式:把所有时间戳转换为统一的格式。我个人习惯用Unix时间戳(毫秒级),精度高且便于计算。
  2. 时区处理:区块链是去中心化的,节点遍布全球。时间戳通常用UTC,但链下数据可能用本地时间。一定要统一到UTC。
  3. 精度对齐:有的数据精确到秒,有的精确到毫秒。对齐时,建议以最高精度为准。
  4. 区块时间 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 知识体系总览

说了这么多,咱们用一张图来总结一下数据清洗与预处理的完整流程:

数据清洗与预处理流程 原始数据 1. 数据去重 2. 异常值处理 3. 缺失值填充 4. 时间戳对齐 5. 数据标准化 关键要点 • 去重:哈希/组合键 • 异常值:IQR/Z-Score • 缺失值:先分析原因 • 时间戳:统一到UTC • 标准化:对数+Z-Score 常见陷阱 1. 盲目删除异常值 2. 忽略缺失原因 3. 时区未统一 4. 标准化方法选错 最佳实践 • 保留原始数据副本 • 记录每一步操作 • 验证清洗后的数据

这张图把整个流程串起来了。从原始数据开始,经过去重、异常值处理、缺失值填充、时间戳对齐,最后到数据标准化。每一步都有它的意义,缺一不可。

好了,数据清洗与预处理这部分就聊到这儿。记住一句话:数据清洗不是体力活,而是技术活。你花在清洗上的每一分钟,都会在后续分析中加倍回报给你。


专注资料整理