4、交易数据清洗:处理原始交易数据,提取Gas Price、Value、Input Data,处理缺失值与异常值

好,咱们进入实战环节。上一章我们把原始区块数据扒下来了,但说实话,那玩意儿跟毛坯房一样——不能直接住人。原始交易数据里充斥着各种噪音、缺失值和格式问题。你直接拿它跑策略,结果会非常难看。

我刚开始做链上数据分析时,就吃过这个亏。从节点拉了一堆数据,兴冲冲地跑回测,结果收益率曲线跟过山车似的。后来一查,原来是某笔交易的Gas Price字段是空的,程序直接报错跳过了整段逻辑。嗯,从那以后,数据清洗成了我流程里的第一道铁闸。

核心观点:数据清洗不是体力活,是策略的生命线。脏数据进,垃圾策略出。

4.1 原始交易数据长什么样?

从以太坊节点或者公共RPC拉回来的原始交易数据,通常是一个JSON对象数组。每个对象包含几十个字段。但对我们量化策略来说,真正核心的只有三个:Gas PriceValueInput Data

我截取一个真实的数据片段给你看(已脱敏):

{
  "hash": "0xabc...123",
  "nonce": "42",
  "blockHash": "0xdef...456",
  "blockNumber": "0x10d4f3a",
  "transactionIndex": "0x5",
  "from": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18",
  "to": "0x...",
  "value": "0x2386f26fc10000",
  "gas": "0x5208",
  "gasPrice": "0x3b9aca00",
  "input": "0x095ea7b3000000000000000000000000...",
  "v": "0x1c",
  "r": "0x...",
  "s": "0x..."
}

你看,value 是十六进制字符串,gasPrice 也是十六进制。直接拿来用?不行。得先做格式转换。

4.2 提取与转换:从十六进制到可计算数值

我个人习惯,第一步先把所有数值字段从十六进制转成十进制。这里有个坑——以太坊的数值单位是Wei,1 ETH = 10^18 Wei。你如果直接拿Wei去算,数字会大得吓人,而且容易溢出。

我一般这样处理:

def extract_tx_fields(tx):
    """
    提取交易核心字段,并转换为可计算格式
    """
    # Gas Price: 从hex转wei,再转gwei(常用单位)
    gas_price_wei = int(tx['gasPrice'], 16)
    gas_price_gwei = gas_price_wei / 1e9
    
    # Value: 从hex转wei,再转eth
    value_wei = int(tx['value'], 16)
    value_eth = value_wei / 1e18
    
    # Input Data: 保留原始hex,但去掉'0x'前缀
    input_data = tx['input'][2:] if tx['input'].startswith('0x') else tx['input']
    
    return {
        'gas_price_gwei': gas_price_gwei,
        'value_eth': value_eth,
        'input_data': input_data,
        'from': tx['from'],
        'to': tx['to'],
        'block_number': int(tx['blockNumber'], 16)
    }

小技巧:我建议你保留一份原始hex字段的副本,方便后续排查。有时候精度问题需要回溯原始数据。

4.3 处理缺失值:不能直接扔掉

原始数据里,缺失值很常见。比如某些合约创建交易没有 to 地址,某些交易没有 input 数据(普通转账)。

你想想看,如果直接把这些行删掉,你的样本量会缩水,而且会引入偏差——比如你只分析有 input 的交易,那你就漏掉了所有普通转账,策略会偏向合约交互。

我的处理原则是:

  • Gas Price 缺失: 用同区块内其他交易的中位数填充。因为同一区块的Gas Price通常比较接近。
  • Value 缺失: 默认为0。很多合约交互确实不转ETH。
  • Input Data 缺失: 保留空字符串,不要填充假数据。后续分析时单独标记。
  • To 地址缺失: 这是合约创建交易,单独归类,不要混入普通交易。
def fill_missing_values(df):
    # Gas Price: 按区块分组填充中位数
    df['gas_price_gwei'] = df.groupby('block_number')['gas_price_gwei']\
                              .transform(lambda x: x.fillna(x.median()))
    
    # Value: 缺失填0
    df['value_eth'] = df['value_eth'].fillna(0.0)
    
    # Input Data: 缺失填空字符串
    df['input_data'] = df['input_data'].fillna('')
    
    return df

注意:千万不要用全局均值填充Gas Price!不同区块的Gas Price差异巨大,用全局均值会引入严重偏差。我见过有人这么干,回测结果看起来很美,实盘直接崩了。

4.4 异常值检测:那些离谱的数据

链上数据里,异常值比传统金融数据更夸张。为什么?因为有人会故意发送极端交易来测试合约,或者做MEV攻击。

我曾经遇到一笔交易,Gas Price设了10000 Gwei,而当时全网平均只有20 Gwei。这明显是异常。如果不处理,你的策略可能会误以为「高Gas Price是常态」,从而做出错误判断。

我常用的异常值处理方法:

  1. 基于百分位数: 剔除Gas Price超过99.9%分位数的交易。
  2. 基于业务逻辑: Value超过某个阈值(比如10000 ETH)的交易单独标记,不直接剔除。
  3. 基于上下文: 同区块内Gas Price偏离中位数超过10倍的,标记为可疑。
def detect_outliers(df):
    # Gas Price: 超过99.9%分位数的标记为异常
    gas_upper = df['gas_price_gwei'].quantile(0.999)
    df['gas_price_outlier'] = df['gas_price_gwei'] > gas_upper
    
    # Value: 超过1000 ETH的标记
    df['value_outlier'] = df['value_eth'] > 1000
    
    # 综合标记
    df['is_outlier'] = df['gas_price_outlier'] | df['value_outlier']
    
    return df

我的习惯:异常值不直接删除,而是加标记列。这样你在回测时可以尝试「包含异常值」和「排除异常值」两种方案,看看策略的鲁棒性如何。

4.5 Input Data 的初步解析

Input Data 是交易中最复杂也最有价值的部分。它包含了调用合约函数的具体参数。但原始数据是一长串十六进制,没法直接用。

我一般做两步处理:

  • 提取函数选择器: Input Data的前4个字节(8个十六进制字符)就是函数选择器。比如 0x095ea7b3 是ERC20的 approve 函数。
  • 判断交易类型: 如果Input Data为空,说明是普通ETH转账;如果不为空,说明是合约调用。
def parse_input_data(input_hex):
    if not input_hex or input_hex == '0x':
        return {'type': 'transfer', 'selector': None}
    
    # 去掉0x前缀
    clean = input_hex[2:] if input_hex.startswith('0x') else input_hex
    
    # 提取函数选择器
    selector = clean[:8] if len(clean) >= 8 else clean
    
    return {
        'type': 'contract_call',
        'selector': selector,
        'raw_length': len(clean)
    }

4.6 完整的数据清洗流程

好了,把上面这些步骤串起来,就是一个完整的数据清洗流水线。我画了一张流程图,帮你理清逻辑:

交易数据清洗流程 原始交易数据 提取 Gas Price / Value / Input 十六进制 → 十进制 / ETH单位 缺失值填充(中位数/0/空) 异常值标记(不删除) gasPrice → gwei value → eth input → 函数选择器 Gas Price: 同区块中位数 Value: 0 Input: 空字符串 99.9% 分位数阈值 Value > 1000 ETH

这张图把整个流程串起来了。你按照这个顺序处理,基本不会出大问题。

4.7 实战中的几个坑

最后,分享几个我在实战中踩过的坑,你注意避开:

  • 精度丢失: Python的 float 处理大数时会丢失精度。我建议用 Decimal 或者直接保留Wei单位,只在最后展示时转成ETH。
  • 时间戳对齐: 交易数据本身没有时间戳,需要从区块数据里拿。记得做左连接,别搞错顺序。
  • 重复交易: 同一个交易哈希可能出现在多个数据源中。去重时以区块高度和交易索引为准。
  • 空区块: 有些区块没有交易,你的DataFrame会是空的。提前做空值判断,别让程序崩了。

总结一下:数据清洗的核心就三件事——格式转换、缺失值填充、异常值标记。别想着一步到位,先跑通流程,再慢慢优化。我当年第一个版本的数据清洗脚本只有50行,后来慢慢迭代到300行。够用就好。

好了,这一章的内容就到这儿。数据清洗是基础中的基础,但也是决定策略成败的关键一步。你把这章的内容吃透了,后面的回测才能跑得稳。


公众号:蓝海资料掘金营,微信deep3321