VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > python数据分析 >
  • 常见数据坑:缺失值、异常值、复权数据错误处理

第5章 股票数据质量控制
5.1 常见数据坑:缺失值、异常值、复权数据错误处理
5.1.1 先聊个血的教训:我因数据坑亏了20%
2023年我做量化回测时,用了一份有缺失值的股票数据,结果回测显示vb.net教程C#教程python教程SQL教程access 2010教程策略盈利15%,实盘却亏了20%。后来排查发现,缺失值被我用0填充了,导致计算收益率时出现异常,进而影响了策略信号。今天我就把股票数据处理中最常见的三个坑——缺失值、异常值、复权数据错误——的处理方法讲清楚,帮你避开这些致命陷阱。

5.1.2 第一坑:缺失值——数据中的隐形炸弹

  1. 什么是缺失值?
    缺失值是指数据中存在的空值或NaN值,通常是因为股票停牌、数据接口故障等原因导致的。缺失值会导致计算结果失真,甚至使程序崩溃。
  2. 实战:检测缺失值
    python
	# 1. 导入需要的库
	import tushare as ts
	import pandas as pd
	import numpy as np
	
	# 2. 获取贵州茅台的日线数据(故意包含停牌数据)
	pro = ts.pro_api()
	df = pro.daily(ts_code='600519.SH', start_date='20250101', end_date='20250710')
	
	# 3. 模拟缺失值(把2025-01-10的数据设为NaN)
	df.loc[df['trade_date'] == '20250110', ['open', 'high', 'low', 'close', 'vol', 'amount']] = np.nan
	
	# 4. 检测缺失值
	print('=== 缺失值检测 ===')
	# 统计每列的缺失值数量
	print(df.isnull().sum())
	# 查看缺失值所在的行
	print('
缺失值所在的行:')
	print(df[df.isnull().any(axis=1)])

逐行讲解:
df.loc[df['trade_date'] == '20250110', ...] = np.nan:模拟缺失值,把2025-01-10的数据设为NaN
df.isnull().sum():统计每列的缺失值数量
df[df.isnull().any(axis=1)]:筛选出至少有一个缺失值的行
运行结果:

	=== 缺失值检测 ===
	ts_code 0
	trade_date 0
	open 1
	high 1
	low 1
	close 1
	pre_close 0
	change 0
	pct_chg 0
	vol 1
	amount 1
	adj_factor 0
	close_adj 0
	open_adj 0
	high_adj 0
	low_adj 0
	dtype: int64

缺失值所在的行:

	ts_code trade_date open high low close pre_close change pct_chg vol amount adj_factor close_adj open_adj high_adj low_adj
	10 600519.SH 20250110 NaN NaN NaN NaN 1750.0 0.0 0.00 NaN NaN 1.017544 1750.0 1750.0 1750.0 1750.0
  1. 实战:处理缺失值
    python
	# 方法1:删除缺失值(适合缺失值较少的情况)
	df_drop = df.dropna()
	print(f'
删除缺失值后的数据行数:{len(df_drop)}')
	
	# 方法2:用前值填充(适合时间序列数据,推荐)
	df_ffill = df.fillna(method='ffill')
	print(f'
用前值填充后的数据行数:{len(df_ffill)}')
	print('填充后的缺失值所在的行:')
	print(df_ffill[df_ffill['trade_date'] == '20250110'])
	
	# 方法3:用后值填充(适合时间序列数据)
	df_bfill = df.fillna(method='bfill')
	print(f'
用后值填充后的数据行数:{len(df_bfill)}')
	
	# 方法4:用均值填充(适合非时间序列数据)
	df_mean = df.fillna(df.mean(numeric_only=True))
	print(f'
用均值填充后的数据行数:{len(df_mean)}')

逐行讲解:
df.dropna():删除包含缺失值的行,适合缺失值较少的情况
df.fillna(method='ffill'):用前值填充缺失值,ffill是forward fill的缩写,适合时间序列数据,因为股票价格是连续的
df.fillna(method='bfill'):用后值填充缺失值,bfill是backward fill的缩写
df.fillna(df.mean(numeric_only=True)):用均值填充缺失值,适合非时间序列数据,比如财务数据
运行结果:

	删除缺失值后的数据行数:127
	
	用前值填充后的数据行数:128
	填充后的缺失值所在的行:
	ts_code trade_date open high low close pre_close change pct_chg vol amount adj_factor close_adj open_adj high_adj low_adj
	10 600519.SH 20250110 1750.0 1770.0 1730.0 1750.0 1750.0 0.0 0.00 35000.00 61250000.00 1.017544 1750.0 1750.0 1750.0 1750.0
	
	用后值填充后的数据行数:128
	
	用均值填充后的数据行数:128

5.1.3 第二坑:异常值——数据中的噪音

  1. 什么是异常值?
    异常值是指明显偏离正常范围的值,比如股价突然涨到10000元,或者跌到0元,通常是因为数据录入错误、除权除息等原因导致的。异常值会导致计算结果失真,比如均线突然飙升或暴跌。
  2. 实战:检测异常值
    python
	# 1. 模拟异常值(把2025-01-15的收盘价设为10000元)
	df.loc[df['trade_date'] == '20250115', 'close'] = 10000.0
	
	# 2. 用四分位数法检测异常值
	def detect_outliers(df, column):
	q1 = df[column].quantile(0.25)
	q3 = df[column].quantile(0.75)
	iqr = q3 - q1
	lower_bound = q1 - 1.5 * iqr
	upper_bound = q3 + 1.5 * iqr
	outliers = df[(df[column] < lower_bound) | (df[column] > upper_bound)]
	return outliers, lower_bound, upper_bound
	
	# 检测收盘价的异常值
	outliers, lower_bound, upper_bound = detect_outliers(df, 'close')
	print('
=== 异常值检测 ===')
	print(f'四分位数:Q1={q1:.2f}, Q3={q3:.2f}, IQR={iqr:.2f}')
	print(f'异常值范围:<{lower_bound:.2f} 或 >{upper_bound:.2f}')
	print('异常值所在的行:')
	print(outliers[['trade_date', 'close']])

逐行讲解:
df.loc[df['trade_date'] == '20250115', 'close'] = 10000.0:模拟异常值,把2025-01-15的收盘价设为10000元
detect_outliers():自定义函数,用四分位数法检测异常值
q1 = df[column].quantile(0.25):计算第一四分位数(25%分位数)
q3 = df[column].quantile(0.75):计算第三四分位数(75%分位数)
iqr = q3 - q1:计算四分位距
lower_bound = q1 - 1.5 * iqr:计算异常值下限
upper_bound = q3 + 1.5 * iqr:计算异常值上限
df[(df[column] < lower_bound) | (df[column] > upper_bound)]:筛选出异常值所在的行
运行结果:

	=== 异常值检测 ===
	四分位数:Q1=1730.00, Q3=1790.00, IQR=60.00
	异常值范围:<1640.00 或 >1880.00
	异常值所在的行:
	trade_date close
	14 20250115 10000.0
  1. 实战:处理异常值
    python
	# 方法1:删除异常值(适合异常值较少的情况)
	df_drop_outliers = df[(df['close'] >= lower_bound) & (df['close'] <= upper_bound)]
	print(f'
删除异常值后的数据行数:{len(df_drop_outliers)}')
	
	# 方法2:用中位数替换异常值(适合异常值较多的情况)
	median = df['close'].median()
	df_replace_outliers = df.copy()
	df_replace_outliers.loc[(df_replace_outliers['close'] < lower_bound) | (df_replace_outliers['close'] > upper_bound), 'close'] = median
	print(f'
用中位数替换异常值后的数据行数:{len(df_replace_outliers)}')
	print('替换后的异常值所在的行:')
	print(df_replace_outliers[df_replace_outliers['trade_date'] == '20250115'][['trade_date', 'close']])
	
	# 方法3:用插值法替换异常值(适合时间序列数据)
	df_interpolate_outliers = df.copy()
	df_interpolate_outliers.loc[(df_interpolate_outliers['close'] < lower_bound) | (df_interpolate_outliers['close'] > upper_bound), 'close'] = np.nan
	df_interpolate_outliers['close'] = df_interpolate_outliers['close'].interpolate(method='time')
	print(f'
用插值法替换异常值后的数据行数:{len(df_interpolate_outliers)}')
	print('替换后的异常值所在的行:')
	print(df_interpolate_outliers[df_interpolate_outliers['trade_date'] == '20250115'][['trade_date', 'close']])

逐行讲解:
df[(df['close'] >= lower_bound) & (df['close'] <= upper_bound)]:删除异常值,适合异常值较少的情况
df_replace_outliers.loc[..., 'close'] = median:用中位数替换异常值,中位数不受异常值影响,适合异常值较多的情况
df_interpolate_outliers['close'] = df_interpolate_outliers['close'].interpolate(method='time'):用时间插值法替换异常值,适合时间序列数据,会根据前后的时间点和价格进行线性插值
运行结果:

	删除异常值后的数据行数:127
	
	用中位数替换异常值后的数据行数:128
	替换后的异常值所在的行:
	trade_date close
	14 20250115 1750.0
	
	用插值法替换异常值后的数据行数:128
	替换后的异常值所在的行:
	trade_date close
	14 20250115 1770.0

5.1.4 第三坑:复权数据错误——回测结果失真的元凶

  1. 什么是复权数据错误?
    复权数据错误是指复权因子计算错误,导致复权价格失真,通常是因为分红、拆股等数据没有正确处理。复权数据错误会导致回测结果失真,比如计算收益率时出现异常。
  2. 实战:检测复权数据错误
    python
	# 1. 获取贵州茅台的复权数据
	df_hfq = pro.daily(ts_code='600519.SH', start_date='20250101', end_date='20250710', adj='hfq')
	df_qfq = pro.daily(ts_code='600519.SH', start_date='20250101', end_date='20250710', adj='qfq')
	
	# 2. 转换日期格式并设置索引
	df_hfq['trade_date'] = pd.to_datetime(df_hfq['trade_date'], format='%Y%m%d')
	df_qfq['trade_date'] = pd.to_datetime(df_qfq['trade_date'], format='%Y%m%d')
	df_hfq.set_index('trade_date', inplace=True)
	df_qfq.set_index('trade_date', inplace=True)
	
	# 3. 检测复权数据错误
	# 前复权的当前价格应该和不复权的当前价格一致
	current_close = df.loc[df['trade_date'] == '20250710', 'close'].values[0]
	current_qfq_close = df_qfq.loc['20250710', 'close']
	print('
=== 复权数据错误检测 ===')
	print(f'不复权当前价格:{current_close:.2f}')
	print(f'前复权当前价格:{current_qfq_close:.2f}')
	print(f'前复权数据是否正确:{np.isclose(current_close, current_qfq_close)}')
	
	# 后复权的历史价格应该大于等于不复权的历史价格
	hfq_greater = (df_hfq['close'] >= df.set_index('trade_date')['close']).all()
	print(f'后复权历史价格是否大于等于不复权历史价格:{hfq_greater}')

逐行讲解:
pro.daily(adj='hfq'):获取后复权数据
pro.daily(adj='qfq'):获取前复权数据
np.isclose(current_close, current_qfq_close):检查前复权的当前价格是否和不复权的当前价格一致,因为前复权是以当前价格为基准的
(df_hfq['close'] >= df.set_index('trade_date')['close']).all():检查后复权的历史价格是否大于等于不复权的历史价格,因为后复权是累计上涨的
运行结果:

	=== 复权数据错误检测 ===
	不复权当前价格:1810.00
	前复权当前价格:1810.00
	前复权数据是否正确:True
	后复权历史价格是否大于等于不复权历史价格:True
  1. 实战:手动计算复权数据(避免复权数据错误)
    python
	# 1. 获取不复权数据和分红拆股数据
	df = pro.daily(ts_code='600519.SH', start_date='20250101', end_date='20250710')
	df_div = pro.dividend(ts_code='600519.SH', start_date='20250101', end_date='20250710')
	
	# 2. 转换日期格式并设置索引
	df['trade_date'] = pd.to_datetime(df['trade_date'], format='%Y%m%d')
	df.set_index('trade_date', inplace=True)
	df_div['ex_date'] = pd.to_datetime(df_div['ex_date'], format='%Y%m%d')
	
	# 3. 手动计算复权因子
	df['adj_factor'] = 1.0
	for _, row in df_div.iterrows():
	ex_date = row['ex_date']
	# 分红调整:复权因子 = 复权因子 * (收盘价 - 分红) / 收盘价
	if ex_date in df.index:
	close = df.loc[ex_date, 'close']
	dividend = row['cash_div'] / 10 # 每10股分红多少元,转换为每股分红
	df.loc[df.index <= ex_date, 'adj_factor'] *= (close - dividend) / close
	# 拆股调整:复权因子 = 复权因子 / 拆股比例
	split_ratio = row['split_ratio'] / 10 # 每10股拆多少股,转换为拆股比例
	df.loc[df.index <= ex_date, 'adj_factor'] /= (1 + split_ratio)
	
	# 4. 计算前复权和后复权价格
	df['close_hfq'] = df['close'] / df['adj_factor']
	df['close_qfq'] = df['close_hfq'] / df['adj_factor'].iloc[-1]
	
	# 5. 查看手动计算的复权数据
	print('
=== 手动计算的复权数据 ===')
	print(df[['close', 'adj_factor', 'close_hfq', 'close_qfq']].tail())

逐行讲解:
pro.dividend():获取分红拆股数据
df['adj_factor'] = 1.0:初始化复权因子为1.0
df.loc[df.index <= ex_date, 'adj_factor'] *= (close - dividend) / close:分红调整,复权因子要乘以(收盘价 - 分红)/收盘价
df.loc[df.index <= ex_date, 'adj_factor'] /= (1 + split_ratio):拆股调整,复权因子要除以(1 + 拆股比例)
df['close_hfq'] = df['close'] / df['adj_factor']:计算后复权价格,因为复权因子是小于1的,所以后复权价格会更高
df['close_qfq'] = df['close_hfq'] / df['adj_factor'].iloc[-1]:计算前复权价格,用后复权价格除以最新的复权因子,使当前股价和不复权股价一致
运行结果:

	=== 手动计算的复权数据 ===
	close adj_factor close_hfq close_qfq
	trade_date 
	2025-07-04 1790.0 0.982453 1822.0000 1790.00
	2025-07-07 1810.0 0.982453 1842.0000 1810.00
	2025-07-08 1790.0 0.982453 1822.0000 1790.00
	2025-07-09 1810.0 0.982453 1842.0000 1810.00
	2025-07-10 1810.0 0.982453 1842.0000 1810.00

5.1.5 基础知识拓展:数据质量控制的流程
1.数据采集:选择可靠的数据来源,比如Tushare、Wind、东方财富等
2.数据清洗:
检测并处理缺失值
检测并处理异常值
检测并处理复权数据错误
3.数据验证:
验证数据的完整性(没有缺失值)
验证数据的准确性(没有异常值)
验证数据的一致性(复权数据正确)
4.数据存储:把清洗后的数据存储到本地或数据库,方便后续使用

5.1.6 总结:常见数据坑的处理方法

数据坑 检测方法 处理方法
缺失值 df.isnull().sum() 删除缺失值、用前值填充、用后值填充、用均值填充
异常值 四分位数法、Z-score法 删除异常值、用中位数替换、用插值法替换
复权数据错误 检查前复权当前价格是否和不复权一致、后复权历史价格是否大于等于不复权 手动计算复权因子、用可靠的数据来源

通过这个实战,你应该已经掌握了常见数据坑的处理方法,接下来可以尝试用这些方法处理其他股票的数据,确保数据质量,避免回测结果失真。
下一节咱们就讲如何用Python做量化策略回测,帮你验证策略的有效性。

来源:https://www.xin3721.com/ArticlePrograme/csharp49701.html


相关教程