4、交易数据清洗:处理原始交易数据,提取Gas Price、Value、Input Data,处理缺失值与异常值
好,咱们进入实战环节。上一章我们把原始区块数据扒下来了,但说实话,那玩意儿跟毛坯房一样——不能直接住人。原始交易数据里充斥着各种噪音、缺失值和格式问题。你直接拿它跑策略,结果会非常难看。
我刚开始做链上数据分析时,就吃过这个亏。从节点拉了一堆数据,兴冲冲地跑回测,结果收益率曲线跟过山车似的。后来一查,原来是某笔交易的Gas Price字段是空的,程序直接报错跳过了整段逻辑。嗯,从那以后,数据清洗成了我流程里的第一道铁闸。
核心观点:数据清洗不是体力活,是策略的生命线。脏数据进,垃圾策略出。
4.1 原始交易数据长什么样?
从以太坊节点或者公共RPC拉回来的原始交易数据,通常是一个JSON对象数组。每个对象包含几十个字段。但对我们量化策略来说,真正核心的只有三个:Gas Price、Value、Input 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是常态」,从而做出错误判断。
我常用的异常值处理方法:
- 基于百分位数: 剔除Gas Price超过99.9%分位数的交易。
- 基于业务逻辑: Value超过某个阈值(比如10000 ETH)的交易单独标记,不直接剔除。
- 基于上下文: 同区块内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 完整的数据清洗流程
好了,把上面这些步骤串起来,就是一个完整的数据清洗流水线。我画了一张流程图,帮你理清逻辑:
这张图把整个流程串起来了。你按照这个顺序处理,基本不会出大问题。
4.7 实战中的几个坑
最后,分享几个我在实战中踩过的坑,你注意避开:
- 精度丢失: Python的
float处理大数时会丢失精度。我建议用Decimal或者直接保留Wei单位,只在最后展示时转成ETH。 - 时间戳对齐: 交易数据本身没有时间戳,需要从区块数据里拿。记得做左连接,别搞错顺序。
- 重复交易: 同一个交易哈希可能出现在多个数据源中。去重时以区块高度和交易索引为准。
- 空区块: 有些区块没有交易,你的DataFrame会是空的。提前做空值判断,别让程序崩了。
总结一下:数据清洗的核心就三件事——格式转换、缺失值填充、异常值标记。别想着一步到位,先跑通流程,再慢慢优化。我当年第一个版本的数据清洗脚本只有50行,后来慢慢迭代到300行。够用就好。
好了,这一章的内容就到这儿。数据清洗是基础中的基础,但也是决定策略成败的关键一步。你把这章的内容吃透了,后面的回测才能跑得稳。
公众号:蓝海资料掘金营,微信deep3321