4. 数据清洗与缺失值处理
任务目标
对从交易所API获取的加密货币K线数据,完成三项清洗工作:统一时间戳格式、识别并处理缺失值、检测并处理异常值(如价格跳变、成交量异常)。最终输出一份可直接用于后续策略回测的干净DataFrame。
环境依赖
需要 pandas、numpy。数据来源使用本地CSV(由交易所API导出),避免网络请求干扰清洗逻辑验证。
关键完整代码
import pandas as pd
import numpy as np
def load_raw_data(filepath):
"""加载原始K线数据,模拟交易所返回的格式"""
df = pd.read_csv(filepath)
# 模拟真实场景:时间戳可能是毫秒或秒,且存在缺失行
df['timestamp'] = df['timestamp'].astype(str)
return df
def clean_ohlcv_data(df):
"""
核心清洗函数:
1. 统一时间戳为秒级整数
2. 删除完全重复的行
3. 缺失值处理:前向填充,若开头缺失则删除
4. 异常值处理:价格偏离滚动中位数超过5倍IQR则剔除
"""
df = df.copy()
# --- 1. 时间戳统一 ---
# 判断时间戳位数:13位为毫秒,10位为秒
sample = df['timestamp'].iloc[0]
if len(sample) == 13:
df['timestamp'] = pd.to_numeric(df['timestamp']) // 1000
else:
df['timestamp'] = pd.to_numeric(df['timestamp'])
df['timestamp'] = df['timestamp'].astype(int)
# --- 2. 去重 ---
df = df.drop_duplicates(subset='timestamp', keep='last')
# --- 3. 缺失值处理 ---
# 按时间戳排序
df = df.sort_values('timestamp').reset_index(drop=True)
# 检查时间戳是否连续(假设K线间隔为60秒)
expected_ts = np.arange(df['timestamp'].min(),
df['timestamp'].max() + 60, 60)
missing_ts = set(expected_ts) - set(df['timestamp'])
if missing_ts:
# 构造缺失行,用NaN填充
missing_df = pd.DataFrame({
'timestamp': list(missing_ts),
'open': np.nan, 'high': np.nan, 'low': np.nan,
'close': np.nan, 'volume': np.nan
})
df = pd.concat([df, missing_df], ignore_index=True)
df = df.sort_values('timestamp').reset_index(drop=True)
# 前向填充缺失值(若开头缺失则删除)
df[['open', 'high', 'low', 'close', 'volume']] = \
df[['open', 'high', 'low', 'close', 'volume']].ffill()
df = df.dropna(subset=['open', 'high', 'low', 'close', 'volume'])
# --- 4. 异常值处理 ---
# 对close价格计算滚动中位数和IQR
window = 20
rolling_median = df['close'].rolling(window, center=True).median()
rolling_q1 = df['close'].rolling(window, center=True).quantile(0.25)
rolling_q3 = df['close'].rolling(window, center=True).quantile(0.75)
rolling_iqr = rolling_q3 - rolling_q1
# 偏离中位数超过5倍IQR视为异常
lower_bound = rolling_median - 5 * rolling_iqr
upper_bound = rolling_median + 5 * rolling_iqr
outlier_mask = (df['close'] < lower_bound) | (df['close'] > upper_bound)
# 剔除异常行
df = df[~outlier_mask].reset_index(drop=True)
return df
# 最小验证闭环
if __name__ == "__main__":
# 构造含脏数据的示例CSV(实际使用时替换为真实文件路径)
sample_data = pd.DataFrame({
'timestamp': [1609459200000, 1609459260000, 1609459380000, # 缺1609459320
1609459440000, 1609459500000],
'open': [100.0, 101.0, 102.0, 103.0, 500.0], # 最后一个为异常值
'high': [101.0, 102.0, 103.0, 104.0, 501.0],
'low': [99.0, 100.0, 101.0, 102.0, 499.0],
'close': [100.5, 101.5, 102.5, 103.5, 500.5],
'volume': [1000, 1100, 1200, 1300, 1400]
})
sample_data.to_csv('raw_btc.csv', index=False)
raw = load_raw_data('raw_btc.csv')
cleaned = clean_ohlcv_data(raw)
print(f"原始行数: {len(raw)}")
print(f"清洗后行数: {len(cleaned)}")
print("清洗后数据预览:")
print(cleaned.head())
print("\n时间戳类型:", cleaned['timestamp'].dtype)
print("是否有缺失值:", cleaned.isnull().any().any())
运行与验证
运行方式:将上述代码保存为 clean_ohlcv.py,在命令行执行 python clean_ohlcv.py。代码会自动生成包含脏数据的示例文件并完成清洗。
验证方法:
- 观察输出:原始数据5行,其中包含1个缺失时间戳(1609459320)和1个价格异常值(500.5)。清洗后应保留4行有效数据。
- 检查时间戳列类型为
int64(秒级),且不存在缺失值。 - 异常值500.5应被剔除,最终close价格范围在100~104之间。
通过条件:程序无报错运行,输出显示清洗后行数为4,时间戳为整数秒,无缺失值,且close列最大值小于105。
常见问题
- 时间戳位数判断:若数据源混用秒和毫秒,可先统一转为字符串后按长度判断,本代码已处理该情况。
- K线间隔不固定:本案例假设60秒间隔,若实际为其他周期,需修改
expected_ts中的步长参数。 - 异常值阈值:5倍IQR为经验值,可根据币种波动性调整;若数据量过小(<20行),滚动窗口会失效,需改用全局统计量。