首页 > 编程开发 > python数据分析 >
-
常见数据坑:缺失值、异常值、复权数据错误处理
第5章 股票数据质量控制
5.1 常见数据坑:缺失值、异常值、复权数据错误处理
5.1.1 先聊个血的教训:我因数据坑亏了20%
2023年我做量化回测时,用了一份有缺失值的股票数据,结果回测显示vb.net教程C#教程python教程SQL教程access 2010教程策略盈利15%,实盘却亏了20%。后来排查发现,缺失值被我用0填充了,导致计算收益率时出现异常,进而影响了策略信号。今天我就把股票数据处理中最常见的三个坑——缺失值、异常值、复权数据错误——的处理方法讲清楚,帮你避开这些致命陷阱。
5.1.2 第一坑:缺失值——数据中的隐形炸弹
-
什么是缺失值?
缺失值是指数据中存在的空值或NaN值,通常是因为股票停牌、数据接口故障等原因导致的。缺失值会导致计算结果失真,甚至使程序崩溃。 -
实战:检测缺失值
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
-
实战:处理缺失值
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 第二坑:异常值——数据中的噪音
-
什么是异常值?
异常值是指明显偏离正常范围的值,比如股价突然涨到10000元,或者跌到0元,通常是因为数据录入错误、除权除息等原因导致的。异常值会导致计算结果失真,比如均线突然飙升或暴跌。 -
实战:检测异常值
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
-
实战:处理异常值
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 第三坑:复权数据错误——回测结果失真的元凶
-
什么是复权数据错误?
复权数据错误是指复权因子计算错误,导致复权价格失真,通常是因为分红、拆股等数据没有正确处理。复权数据错误会导致回测结果失真,比如计算收益率时出现异常。 -
实战:检测复权数据错误
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
-
实战:手动计算复权数据(避免复权数据错误)
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










