52 数据框缺失值处理
本章定位(复习与补充训练层):本章对应正课第16章《缺失值处理 英国零售商数据清洗》(章节 16),用于复习、补缺与额外练习。建议先不看讲解,直接尝试下方平台任务,再对照解析补弱项。本章不属于必修主线。先做本章『动手与思考』第 1 题与平台任务自测,通过即可跳过本章。
52.1 本章学习目标
通过本章的复习与补充训练,你将能够:
- 说出缺失值三种机制(MCAR、MAR、MNAR)的含义,并说明机制对处理策略选择的影响
- 用
isnull/notnull完成缺失值计数与定位,用可视化把握缺失模式 - 按场景选择删除(
dropna)或插补(前向/后向填充、均值、插值)策略,并解释金融时间序列为何常用前向填充 - 独立完成本章平台任务,再对照讲解补弱项
为什么缺失值值得专门花一章处理?先看它在金融数据中的真实来源与后果。
52.2 引言缺失值的普遍性与挑战
缺失值(Missing Values)是现实世界数据分析中无法回避的普遍问题。无论是社会科学的问卷调查、金融市场的交易数据,还是自然科学实验观测,数据缺失几乎无处不在。
52.2.1 金融数据缺失的典型场景
在金融市场中,数据缺失尤为常见:
- 停牌事件:股票因重大事项停牌,期间没有交易数据。例如,中国平安(601318.SH)在2024年某次并购期间停牌10个交易日
- 新股上市:新上市公司上市前没有历史数据,构建长期面板数据时会产生”左截断”问题
- 节假日效应:A股市场在春节、国庆期间休市,而海外市场继续交易
- 数据源故障:数据供应商的技术问题可能导致部分数据暂时不可用
- 公司行为:退市公司被从指数成分股中剔除,历史数据不再维护
52.2.2 缺失值处理的重要性
为什么不能简单忽略缺失值?
统计偏倚:如果缺失不是随机的,删除缺失数据会导致样本有偏,从而得到错误的结论。例如,研究股票收益时如果删除了所有退市公司的数据,会高估股票的真实收益(因为退市公司通常是表现差的)。
信息损失:简单删除会丢弃有效数据。在样本量有限的情况下(如中国A股只有约5000只股票),每一笔数据都珍贵。
算法要求:大多数统计模型和机器学习算法(如线性回归、神经网络)无法直接处理缺失值。
52.3 缺失值的数学基础
52.3.1 缺失值的定义
设数据集 \(D = \{x_1, x_2, \ldots, x_n\}\),我们实际观测到的是:
\[ D_{\text{observed}} = \{x_i | i \in I_{\text{observed}}\} \] 其中 \(I_{\text{observed}} \subset \{1, 2, \ldots, n\}\) 是观测索引集合。
缺失值集合为:
\[ D_{\text{missing}} = \{x_i | i \in I_{\text{missing}}\} \] 其中 \(I_{\text{missing}} = \{1, 2, \ldots, n\} \setminus I_{\text{observed}}\)。
52.3.2 Pandas中的缺失值表示
Pandas使用NaN(Not a Number)表示数值型缺失值,这是IEEE 754浮点数标准的一部分。
NaN的特殊性质:
NaN != NaN(自反性不成立)NaN + x = NaN, ∀x (吸收律)NaN * x = NaN, ∀x
# =============================================================================
# 题目:探索NaN的特殊数学性质
# =============================================================================
# 本任务演示NaN(Not a Number)在Python中的反直觉特性,这些特性源于IEEE 754浮点数标准
# ==================== 导入必要的库 ====================
import numpy as np # NumPy数值计算库
import pandas as pd # Pandas数据分析库
# ==================== 创建NaN值 ====================
# np.nan是NumPy库中表示缺失值的常量
# NaN是IEEE 754浮点数标准中定义的特殊值,用于表示无效或未定义的数值运算结果
nan_value = np.nan
# 注意:NaN虽然是float类型,但它不等于任何值,包括它自己
# ==================== 演示NaN的比较特性 ====================
print('NaN的比较特性:')
# nan_value == nan_value:这是最反直觉的特性
# 结果为False!因为NaN在数学上被定义为"不等于任何值,包括它自己"
# 这是IEEE 754标准的规定,用于标识无效的数值运算结果
print(f'NaN == NaN: {nan_value == nan_value}') # 输出: False!
# nan_value != nan_value:由于NaN不等于它自己,所以"不等于"为True
print(f'NaN != NaN: {nan_value != nan_value}') # 输出: True!
# nan_value is nan_value:使用is运算符检查对象身份
# is比较的是两个引用是否指向同一个对象,这里显然是同一个对象
# 因此结果为True(这是Python对象的身份比较,不是数值比较)
print(f'NaN is NaN: {nan_value is nan_value}') # 输出: True
# ==================== 演示NaN的运算特性 ====================
print('\nNaN的运算特性:')
# NaN + 100:任何数与NaN进行算术运算,结果都是NaN
# 这被称为"吸收律"(absorbing law),NaN会"污染"整个计算过程
print(f'NaN + 100: {nan_value + 100}') # 输出: NaN
# NaN * 2:同理,NaN乘以任何数(包括0)结果仍是NaN
print(f'NaN * 2: {nan_value * 2}') # 输出: NaN
# ==================== 关键要点总结 ====================
# 在金融数据分析中:
# 1. 永远不要用 == 或 != 来判断NaN,应该使用 pd.isna() 或 np.isnan()
# 2. NaN具有传染性,任何涉及NaN的运算结果都是NaN
# 3. 这意味着如果数据中存在缺失值,必须先处理才能进行计算关键要点:永远不要用 == 判断NaN,应使用 pd.isna() 或 np.isnan()。
52.4 缺失值的识别与诊断
52.4.1 基础检测方法
平台任务1(平台原始代码)
以下代码与教学平台任务要求完全一致:
任务要求:读入金砖四国指数数据,分别用 isnull() 与 isna() 判断每一列是否存在缺失值,再取出存在缺失值的行。请将代码原样输入教学平台(注释除外),判定以平台为准。
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
index_bric = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220820/xlsx/1560916966116974592.xlsx",sheet_name="Sheet1",header=0,index_col=0) #导入数据
print(index_bric.isnull().any()) #查找每一列是否存在缺失值(用isnull函数)
print(index_bric.isna().any()) #查找每一列是否存在缺失值(用isna函数)
print(index_bric[index_bric.isnull().values==True]) #查找存在缺失值所在的行预期输出(OSS 直链本机实测,数据为 20 个交易日(2019-06-03 至 06-28)的金砖四国指数;具体以平台运行结果为准):
isnull() 与 isna() 的结果完全相同——RTS 指数、Sensex30 指数、上证综指三列含缺失,IBOVESPA 指数无缺失:
IBOVESPA指数 False
RTS指数 True
Sensex30指数 True
上证综指 True
dtype: bool
第三行代码按元素掩码显示,只有缺失位置显示数值、其余位置为 NaN,共 3 行(2019-06-05、06-07、06-12)含缺失:
IBOVESPA指数 RTS指数 Sensex30指数 上证综指
交易日
2019-06-05 95998.75 1303.35 NaN 2861.4181
2019-06-07 97821.26 1325.95 39615.8984 NaN
2019-06-12 98320.88 NaN 39756.8086 2909.3796
# =============================================================================
# 题目:检测数据框中的缺失值
# =============================================================================
# 本任务演示如何在实际金融数据中识别和统计缺失值,这是数据清洗的第一步
# 缺失值可能源于停牌、数据源故障、新股上市等原因
# ==================== 导入必要的库 ====================
import pandas as pd # Pandas数据分析库
import numpy as np # NumPy数值计算库,用于创建NaN值
# ==================== 创建包含缺失值的示例数据 ====================
# 场景:某日5只股票的交易数据,其中部分数据缺失
# data是一个字典,键是列名,值是该列的数据
data = {
# 股票代码列:5只A股的代码(上海交易所.SH,深圳交易所.SZ)
'股票代码': ['600519.SH', '000858.SZ', '600036.SH', '601318.SH', '000001.SZ'],
# 收盘价列:第2和第5只股票收盘价缺失(np.nan表示缺失值)
# 缺失原因可能是:停牌、数据延迟、数据源错误
'收盘价': [1850.0, np.nan, 45.2, 52.8, np.nan],
# 涨跌幅列:第3只股票涨跌幅缺失
'涨跌幅': [0.05, -0.02, np.nan, -0.01, 0.03],
# 成交量列:第3只股票成交量缺失
'成交量': [1200, 3500, np.nan, 5600, 2800]
}
# 将字典转换为Pandas数据框(DataFrame)
# DataFrame是二维表格数据结构,类似于Excel工作表
df = pd.DataFrame(data)
# ==================== 显示原始数据 ====================
print('原始数据:')
print(df) # 打印完整数据框,NaN会显示为NaN
# ==================== 缺失值检测方法1:isna() ====================
print('\n缺失值检测:')
# df.isna():检测每个单元格是否为缺失值
# 返回一个布尔类型的数据框,True表示是缺失值,False表示不是缺失值
print(df.isna())
# 输出示例:
# 股票代码 收盘价 涨跌幅 成交量
# 0 False False False False
# 1 False True False False <- 第1行收盘价缺失
# 2 False False True True <- 第2行涨跌幅和成交量缺失
# ==================== 缺失值统计:每列缺失值数量 ====================
print('\n每列缺失值数量:')
# df.isna().sum():统计每列的缺失值数量
# isna()返回布尔值,sum()会将True计为1,False计为0
print(df.isna().sum())
# 输出解读:收盘价有2个缺失,涨跌幅有1个缺失,成交量有1个缺失
# ==================== 缺失值统计:每列缺失值比例 ====================
print('\n每列缺失值比例:')
# df.isna().sum() / len(df) * 100:计算每列缺失值占总行数的百分比
# len(df):数据框的总行数(这里是5行)
# .round(2):保留两位小数
print((df.isna().sum() / len(df) * 100).round(2))
# 输出解读:收盘价缺失40%(2/5),涨跌幅缺失20%(1/5)
# ==================== 完整行统计 ====================
# df.dropna():删除包含任何缺失值的行
# 这里只用于统计,不修改原数据框
complete = df.dropna()
print(f'\n完整行数: {len(complete)} / {len(df)}')
# 输出解读:完整行(无缺失值)有多少行,占总行数的比例
# 这有助于评估数据质量:如果完整行太少,可能需要重新获取数据52.4.2 缺失值模式分析
理解哪些变量倾向于一起缺失,有助于诊断缺失原因:
# ==================== 缺失值模式分析 ====================
# 目标:理解哪些变量倾向于一起缺失,有助于诊断缺失原因
# 场景:如果收盘价和成交量总是同时缺失,说明股票可能停牌(无交易)
# 定义函数:计算变量间的缺失模式相关性
def missing_correlation(df):
"""
计算变量间的缺失模式相关性
原理:
1. df.isna() 将数据转换为布尔值(True=缺失,False=不缺失)
2. .corr() 计算这些布尔值的相关系数
解读:
- 相关系数接近1:两个变量倾向于同时缺失
- 相关系数接近-1:一个缺失时另一个倾向于不缺失
- 相关系数接近0:缺失模式独立
"""
return df.isna().corr()
# 调用函数计算缺失相关性
miss_corr = missing_correlation(df)
print('缺失模式相关性:')
print(miss_corr.round(2)) # .round(2)保留两位小数
# 输出示例解读:
# 收盘价 涨跌幅 成交量
# 收盘价 1.00 0.50 0.50
# 涨跌幅 0.50 1.00 1.00 <- 涨跌幅和成交量完全同步缺失
# 成交量 0.50 1.00 1.00
# 这说明涨跌幅和成交量有共同的缺失原因(如停牌)如果两个变量的缺失相关性接近1,说明它们倾向于同时缺失,这可能提示它们有共同的缺失原因。
52.5 缺失值删除策略
52.5.1 删除方法
平台任务2(平台原始代码)
以下代码与教学平台任务要求完全一致:
任务要求:对同一份数据用 dropna() 删除含缺失值的行,检查删除后是否仍有缺失,并对比删除前后的形状参数。请将代码原样输入教学平台(注释除外),判定以平台为准。
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
index_bric = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220820/xlsx/1560916966116974592.xlsx",sheet_name="Sheet1",header=0,index_col=0) #导入数据
index_bric_dropna = index_bric.dropna() #删除存在缺失值的行数并创建一个新的数据框
print(index_bric_dropna.isnull().any()) # 输出缺失值检查结果
print(index_bric.shape) #查看原数据框的形状参数
print(index_bric_dropna.shape) #查看新数据框的形状参数预期输出(OSS 直链本机实测;具体以平台运行结果为准):
删除后四列的缺失检查全部为 False,形状由 (20, 4) 变为 (17, 4)——任务一中定位到的 3 行含缺失数据(06-05、06-07、06-12)被整体删除:
IBOVESPA指数 False
RTS指数 False
Sensex30指数 False
上证综指 False
dtype: bool
(20, 4)
(17, 4)
# ==================== 缺失值删除策略 ====================
# 删除缺失值是处理缺失数据的最直接方法,但会造成信息损失
# 需要根据缺失比例和业务需求选择合适的删除策略
# ==================== 策略1:删除包含任何缺失值的行 ====================
# df.dropna(how='any'):只要一行中有任何一个缺失值,就删除整行
# how='any'是默认值,可以省略不写
# 适用场景:对数据完整性要求高,不能容忍任何缺失
df_any = df.dropna(how='any')
print('\n删除任何缺失值的行:')
print(df_any)
# 输出解读:只有所有列都没有缺失值的行才会被保留
# 在本例中,第1、2、4行(索引0、2、3)会被保留
# ==================== 策略2:删除全部为缺失值的行 ====================
# df.dropna(how='all'):只有当一行中所有值都缺失时,才删除该行
# 适用场景:容忍部分缺失,只删除完全无效的行
df_all = df.dropna(how='all')
print('\n删除全部为缺失值的行:')
print(df_all)
# 输出解读:本例中没有任何一行是完全缺失的,所以所有行都保留
# ==================== 策略3:删除特定列中有缺失值的行 ====================
# df.dropna(subset=['收盘价', '涨跌幅']):只在指定列中检查缺失值
# subset参数:指定要检查的列名列表
# 适用场景:某些关键指标缺失就无法分析,如核心财务指标
df_subset = df.dropna(subset=['收盘价', '涨跌幅'])
print('\n在收盘价或涨跌幅有缺失的行被删除:')
print(df_subset)
# 输出解读:只要收盘价或涨跌幅任一缺失,该行就会被删除
# 即使成交量没有缺失,如果收盘价缺失也会被删除删除策略的适用场景:
| 场景 | 推荐策略 | 理由 |
|---|---|---|
| 缺失<5%且MCAR | dropna() |
信息损失小,无偏估计 |
| 关键指标缺失 | dropna(subset=[...]) |
关键指标缺失无法分析 |
| 缺失>50% | 考虑删除该变量 | 信息太少,插补不可靠 |
| MNAR | 谨慎处理 | 删除会导致严重偏倚 |
52.6 缺失值填充方法
52.6.1 常数填充
# ==================== 缺失值填充方法1:常数填充 ====================
# 填充缺失值是删除的替代方案,可以保留数据行
# 但填充值可能引入偏差,需要根据业务场景选择填充值
# ==================== 方法1:用0填充 ====================
# df.fillna(0):将所有缺失值填充为0
# 适用场景:成交量、金额等数值型数据,缺失表示"没有交易"
# 警告:0填充可能低估真实值,造成统计偏差
df_zero = df.fillna(0)
print('\n用0填充:')
print(df_zero)
# 输出解读:所有NaN都被替换为0
# 注意:股票价格为0是不合理的,所以0填充不适合价格数据
# ==================== 方法2:用均值填充 ====================
# df['收盘价'].mean():计算收盘价列的均值(忽略NaN)
# df.fillna({'收盘价': mean_price}):只填充收盘价列,其他列保持不变
# 适用场景:数据分布对称,缺失随机(MCAR)
mean_price = df['收盘价'].mean() # 计算均值:(1850.0 + 45.2 + 52.8) / 3
df_mean = df.fillna({'收盘价': mean_price})
print('\n用均值填充:')
print(df_mean)
# 输出解读:收盘价的缺失值被均值(约649元)填充
# 注意:均值受极端值影响大,如果数据有异常值,均值会被拉偏
# ==================== 方法3:用中位数填充 ====================
# df['收盘价'].median():计算收盘价列的中位数(忽略NaN)
# 中位数是排序后位于中间位置的值,对异常值更稳健
# 适用场景:数据分布偏斜或有极端异常值
median_price = df['收盘价'].median() # 计算中位数:45.2和52.8之间
df_median = df.fillna({'收盘价': median_price})
print(f'\n用中位数填充(中位数={median_price:.2f}):')
print(df_median)
# 输出解读:收盘价的缺失值被中位数填充
# 优势:中位数不受极端值影响,比如茅台的高价不会影响其他股票的填充值52.6.2 前向与后向填充
平台任务3(平台原始代码)
以下代码与教学平台任务要求完全一致:
任务要求:用 fillna(method="ffill") 对缺失值向前补齐,检查补齐效果,并查看 2019-06-04 至 06-12 的数据。请将代码原样输入教学平台(注释除外),判定以平台为准。
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
index_bric = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220820/xlsx/1560916966116974592.xlsx",sheet_name="Sheet1",header=0,index_col=0) #导入数据
#注:新版Pandas推荐等价写法为 index_bric.ffill(),平台判定仍以本行原代码为准
index_bric_ffill = index_bric.fillna(method="ffill") #向前补齐
print(index_bric_ffill.isnull().any()) # 输出缺失值检查结果
print(index_bric_ffill.loc["2019-06-04":"2019-06-12"]) # 输出2019-06-04预期输出(OSS 直链本机实测;具体以平台运行结果为准):
补齐后四列缺失检查全部为 False,随后打印 06-04 至 06-12 共 7 行数据:
IBOVESPA指数 False
RTS指数 False
Sensex30指数 False
上证综指 False
dtype: bool
IBOVESPA指数 RTS指数 Sensex30指数 上证综指
交易日
2019-06-04 97380.28 1307.55 40083.5391 2862.2803
2019-06-05 95998.75 1303.35 40083.5391 2861.4181
2019-06-06 97204.85 1319.85 39529.7188 2827.7978
2019-06-07 97821.26 1325.95 39615.8984 2827.7978
2019-06-10 97466.69 1335.71 39784.5195 2852.1302
2019-06-11 98960.00 1343.33 39950.4609 2925.7162
2019-06-12 98320.88 1343.33 39756.8086 2909.3796
判读要点:向前补齐的痕迹清晰可见——06-05 的 Sensex30 补入的是 06-04 的 40083.5391,06-07 的上证综指补入的是 06-06 的 2827.7978,06-12 的 RTS 补入的是 06-11 的 1343.33。本机 pandas 2.3.3 下运行会伴随一条 FutureWarning(fillna(method=...) 已弃用、建议改用 ffill()),属版本提示而非报错,结果不受影响。
平台任务4(平台原始代码)
以下代码与教学平台任务要求完全一致:
任务要求:用 fillna(method="bfill") 对缺失值向后补齐,检查补齐效果,并查看 2019-06-04 至 06-13 的数据。请将代码原样输入教学平台(注释除外),判定以平台为准。
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd # 导入Pandas数据分析库
index_bric = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220820/xlsx/1560916966116974592.xlsx",sheet_name="Sheet1",header=0,index_col=0) #导入数据
#注:新版Pandas推荐等价写法为 index_bric.bfill(),平台判定仍以本行原代码为准
index_bric_bfill = index_bric.fillna(method="bfill") #向后补齐
print(index_bric_bfill.isnull().any()) # 输出缺失值检查结果
print(index_bric_bfill.loc["2019-06-04":"2019-06-13"]) # 输出2019-06-04预期输出(OSS 直链本机实测;具体以平台运行结果为准):
补齐后四列缺失检查全部为 False,随后打印 06-04 至 06-13 共 8 行数据:
IBOVESPA指数 False
RTS指数 False
Sensex30指数 False
上证综指 False
dtype: bool
IBOVESPA指数 RTS指数 Sensex30指数 上证综指
交易日
2019-06-04 97380.28 1307.55 40083.5391 2862.2803
2019-06-05 95998.75 1303.35 39529.7188 2861.4181
2019-06-06 97204.85 1319.85 39529.7188 2827.7978
2019-06-07 97821.26 1325.95 39615.8984 2852.1302
2019-06-10 97466.69 1335.71 39784.5195 2852.1302
2019-06-11 98960.00 1343.33 39950.4609 2925.7162
2019-06-12 98320.88 1346.98 39756.8086 2909.3796
2019-06-13 98773.70 1346.98 39741.3594 2910.7406
判读要点:向后补齐用的是”未来”的值——06-05 的 Sensex30 补入的是 06-06 的 39529.7188,06-07 的上证综指补入的是 06-10 的 2852.1302,06-12 的 RTS 补入的是 06-13 的 1346.98,与任务三的向前补齐方向相反。同样会伴随 FutureWarning(fillna(method=...) 已弃用、建议改用 bfill()),属版本提示而非报错。
金融应用:前向填充是处理停牌数据的标准方法,假设停牌期间价格保持不变。
52.6.3 插值法
# ==================== 缺失值填充方法3:插值法 ====================
# 插值法根据已知数据点的模式,估算缺失值
# 比简单填充更智能,可以反映数据的趋势和变化
# ==================== 创建时间序列数据 ====================
# pd.date_range('2024-01-01', periods=10):生成10个连续日期
# 场景:模拟某金融产品在10个交易日的价格,其中多个日期价格缺失
dates = pd.date_range('2024-01-01', periods=10)
ts_data = pd.DataFrame({
'日期': dates, # 日期列
'价格': [10.5, np.nan, np.nan, 11.2, np.nan, 11.8, np.nan, np.nan, 12.5, 12.8]
# 价格列:第2、3、5、7、8个交易日价格缺失(NaN)
})
# ==================== 线性插值 ====================
# .interpolate(method='linear'):线性插值方法
# 原理:在两个已知点之间画一条直线,用直线上的值填充缺失值
# 公式:缺失值 = 前值 + (后值 - 前值) × (位置比例)
ts_linear = ts_data.copy() # 复制原数据,避免修改原始数据框
ts_linear['价格_线性插值'] = ts_linear['价格'].interpolate(method='linear')
# 新增列"价格_线性插值",将缺失值填充
print('线性插值:')
print(ts_linear)
# 输出解读:
# - 第2个位置(索引1):10.5和11.2之间的线性插值
# - 第3个位置(索引2):继续线性趋势
# - 线性插值假设价格在两个已知点之间均匀变化52.7 金融应用停牌数据处理
52.7.1 案例招商银行停牌期间数据处理
# =============================================================================
# 金融应用案例:招商银行停牌期间的数据处理
# =============================================================================
# 停牌是A股市场的常见现象,公司因重大事项停牌期间没有交易数据
# 本案例演示两种常用处理策略:前向填充 vs 删除
# ==================== 创建停牌场景模拟数据 ====================
# pd.date_range('2024-01-01', periods=10):生成10个交易日
dates = pd.date_range('2024-01-01', periods=10)
suspension_data = pd.DataFrame({
'日期': dates,
# 收盘价:第4-8个交易日(索引3-7)缺失,模拟停牌5个交易日
'收盘价': [10.5, 10.8, 11.0, np.nan, np.nan, np.nan,
np.nan, np.nan, 11.5, 11.7]
})
# ==================== 标记停牌期 ====================
# suspension_data['收盘价'].isna():检测收盘价是否缺失
# True表示停牌(无交易),False表示正常交易
# 结果存储在新列"是否停牌"中
suspension_data['是否停牌'] = suspension_data['收盘价'].isna()
print('停牌数据:')
print(suspension_data)
# 输出解读:
# - 前3天(索引0-2):正常交易,有收盘价
# - 中5天(索引3-7):停牌,收盘价为NaN,是否停牌为True
# - 后2天(索引8-9):复牌,恢复交易
# ==================== 策略1:前向填充 ====================
# 在金融分析中,前向填充是处理停牌数据的标准方法
# 原因:停牌期间虽然无交易,但股票仍持有,假设价值保持不变
df_strategy1 = suspension_data.copy() # 复制数据,避免修改原始数据
# df_strategy1['收盘价'].ffill():
# 将停牌期间的NaN用停牌前的最后一个价格填充
# 即第4-8天的价格都用11.0填充
df_strategy1['收盘价'] = df_strategy1['收盘价'].ffill()
print('\n策略1 - 前向填充:')
print(df_strategy1)
# 输出解读:停牌期间价格保持为11.0(停牌前最后价格)
# 优点:保留时间序列的连续性,便于计算收益率
# 缺点:低估真实波动率,延迟反映价格跳跃
# ==================== 策略2:删除停牌行 ====================
# 某些分析(如波动率计算)不能包含人为填充的数据
# df.dropna(subset=['收盘价']):删除收盘价缺失的所有行
df_strategy2 = suspension_data.dropna(subset=['收盘价'])
print('\n策略2 - 删除停牌行:')
print(df_strategy2)
# 输出解读:只保留有实际交易数据的5天
# 优点:数据真实,没有人为填充
# 缺点:破坏时间连续性,丢失停牌期间的信息52.7.2 策略比较
前向填充:
- ✓ 简单直观
- ✗ 低估真实波动率
- ✗ 延迟反映价格跳跃
删除行:
- ✓ 避免虚假数据
- ✗ 破坏时间连续性
- ✗ 可能丢失重要信息
52.8 插补方法比较
52.8.1 不同插值方法的评估
# =============================================================================
# 题目:评估不同插值方法的准确性
# =============================================================================
# 本任务通过模拟数据,比较线性插值和样条插值的预测误差
# 均方误差(MSE)越小,说明插值方法越准确
# ==================== 导入NumPy库 ====================
import numpy as np # NumPy数值计算库,用于生成测试数据
# ==================== 创建测试数据 ====================
# np.linspace(0, 10, 20):生成0到10之间均匀分布的20个点
# x代表时间或位置,y代表观测值
x = np.linspace(0, 10, 20)
np.random.seed(42) # 固定随机种子,保证每次运行生成相同的噪声和缺失位置
# y_true:生成"真实"数据,用于验证插值方法的准确性
# np.sin(x):正弦函数,模拟周期性数据
# np.random.normal(0, 0.1, 20):添加均值为0、标准差为0.1的正态分布噪声
y_true = np.sin(x) + np.random.normal(0, 0.1, 20)
# ==================== 人为制造缺失值 ====================
# y_missing:复制真实数据,然后随机设置部分值为NaN
# np.random.choice(20, 5, replace=False):
# 从20个位置中随机选择5个不重复的位置
# y_missing[...] = np.nan:将这5个位置的值设为缺失
y_missing = y_true.copy()
y_missing[np.random.choice(20, 5, replace=False)] = np.nan
# ==================== 方法1:线性插值 ====================
# pd.Series(y_missing):将NumPy数组转换为Pandas序列
# .interpolate(method='linear'):线性插值方法
# 原理:在两个已知点之间画直线
y_linear = pd.Series(y_missing).interpolate(method='linear')
# ==================== 方法2:样条插值 ====================
# .interpolate(method='cubic'):三次样条插值方法
# 原理:使用三次多项式拟合数据点,曲线更平滑
# 优点:可以捕捉非线性趋势
# 缺点:可能过拟合,对噪声敏感
y_spline = pd.Series(y_missing).interpolate(method='cubic')
# ==================== 计算插值误差(MSE) ====================
# MSE (Mean Squared Error):均方误差
# 公式:MSE = Σ(真实值 - 预测值)² / n
# MSE越小,说明插值结果越接近真实值
print(f'线性插值MSE: {np.mean((y_true - y_linear)**2):.4f}')
print(f'样条插值MSE: {np.mean((y_true - y_spline)**2):.4f}')
# ==================== 结果解读 ====================
# 如果线性插值MSE < 样条插值MSE:
# 说明数据接近线性变化,线性插值更合适
# 如果样条插值MSE < 线性插值MSE:
# 说明数据有非线性趋势,样条插值更合适
# 在金融数据中:
# - 短期缺失:线性插值通常足够
# - 长期缺失:建议使用删除或领域知识填充52.9 本章小结
要点:
- 诊断三板斧:
isnull()/notnull()生成布尔掩码,.any()按列判断有无缺失,.sum()/.mean()给出缺失计数与比例 - 删除策略:
dropna()默认删除任一列含缺失的行,how='all'只删全缺失行,subset限定只检查哪些列 - 填充策略:常数或统计量用
fillna(value),前值填充用ffill、后值填充用bfill,平滑补缺用interpolate - 时间序列缺口优先前向填充或插值;跨个股的面板数据先
groupby再分组填充,避免把上一只股票的值”串”到下一只 - 处理方案必须回到缺失机制(MCAR/MAR/MNAR)与业务含义来论证,不能只看代码是否跑通
易错点:
- NaN 不等于自身,
x == np.nan永远为False,判断缺失必须用isnull() - 均值填充会削弱波动、破坏自相关结构;
bfill使用了未来信息,在回测中造成前视偏差 fillna(method='ffill')在新版 Pandas 中已被ffill()取代,本章平台代码块已注明两种写法,平台判定以原始代码为准dropna()与fillna()默认返回新对象,不重新赋值(或不加inplace=True)时原数据框不会改变- 序列开头的缺失值做前向填充时没有前值可用,结果仍是 NaN,补齐后要再次检查
52.10 动手与思考
以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。
输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断。
import numpy as np import pandas as pd s = pd.Series([1.0, np.nan, 3.0, np.nan, 5.0]) print(s.ffill().tolist()) print(s.dropna().tolist()) print(s.isnull().sum())参考答案(先写下你的预测再点开)
解题思路:
ffill()用前一个有效值填充,位置 1 的 NaN 取 1.0、位置 3 的 NaN 取 3.0,得到[1.0, 1.0, 3.0, 3.0, 5.0];dropna()直接删除缺失行,剩[1.0, 3.0, 5.0];isnull()生成布尔掩码后.sum()计数,原序列有 2 个 NaN。# 验证脚本:前向填充、删除与缺失计数 import numpy as np # 导入NumPy库 import pandas as pd # 导入pandas库 s = pd.Series([1.0, np.nan, 3.0, np.nan, 5.0]) # 含两个缺失值的序列 print(s.ffill().tolist()) # 前向填充:缺失位置复制前一个有效值 print(s.dropna().tolist()) # 删除缺失:序列缩短 print(s.isnull().sum()) # 缺失值计数预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
[1.0, 1.0, 3.0, 3.0, 5.0] [1.0, 3.0, 5.0] 2回扣主线:诊断三板斧与删除/填充策略见第 章节 16 章与本章”要点”第 1—3 条。
自测回忆:不看正文,分别说出
isnull().any()、dropna()、ffill()、bfill()完成的操作;再判断:对同一列分别做fillna(0)与ffill(),均值与标准差的变化方向通常有何不同。参考答案(点开前请先独立完成)
解题思路:
isnull().any()逐列判断是否存在至少一个缺失;dropna()删除任一列含缺失的行(默认how='any');ffill()用前一个有效值填充(前向填充);bfill()用后一个有效值填充(后向填充)。方向判断:fillna(0)把 0 强行注入序列——0 通常远离价格类数据的中心,相当于人为插入离群值,均值被拉向 0、标准差通常显著放大;ffill()用相邻的真实值填充,填充值就在原分布的局部附近,均值与标准差通常只有微小变化。以平台任务数据的上证综指列为例(含 1 个缺失,2019-06-07):原序列均值 2923.60、标准差 57.50;fillna(0)后均值降至 2777.42、标准差剧增至 656.13;ffill()后均值 2918.81、标准差 59.93,几乎不变。# 验证脚本:两种填充对均值与标准差的影响(OSS直链数据,拉取日期2026-08-27) import pandas as pd # 导入pandas库 index_bric = pd.read_excel('https://huoran.oss-cn-shenzhen.aliyuncs.com/20220820/xlsx/1560916966116974592.xlsx', sheet_name='Sheet1', header=0, index_col=0) # 读取金砖指数数据 col = index_bric['上证综指'] # 取含1个缺失值的列 print('原始 均值{:.4f} 标准差{:.4f}'.format(col.mean(), col.std())) # 原序列统计量 print('fill0 均值{:.4f} 标准差{:.4f}'.format(col.fillna(0).mean(), col.fillna(0).std())) # 用0填充 print('ffill 均值{:.4f} 标准差{:.4f}'.format(col.ffill().mean(), col.ffill().std())) # 前向填充预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
原始 均值2923.5987 标准差57.5043 fill0 均值2777.4188 标准差656.1282 ffill 均值2918.8087 标准差59.9299若 OSS 直链失效,可到教学平台运行平台任务同款代码,或用任一含缺失的单列数据演练,代码不变。
回扣主线:“均值填充会削弱波动、0 填充不适合价格数据”的告诫见第 章节 16 章与本章”易错点”第 2 条。
变式任务(平台任务同型改造):对平台任务2的数据框,把
dropna()换成dropna(how='all'),先预测两种删除后shape的差异(行数相差多少取决于数据形态,说出你的判断依据),再上平台验证。参考答案(点开前请先独立完成)
解题思路:判断依据是”含缺失的行是否整行全缺失”。
dropna()默认how='any':任一列缺失即删行;dropna(how='all'):只有整行全缺失才删。本数据 20 行 4 列,只有 3 行含缺失(06-05、06-07、06-12),且每行仅 1 列缺失、其余 3 列均有值,没有任何一行是全缺失行。因此预测:dropna()删 3 行得 (17, 4),dropna(how='all')一行不删仍为 (20, 4),两者相差 3 行。以下在 OSS 直链数据上实跑验证(拉取日期 2026-08-27;若链接失效,可到教学平台验证,或用任一”行内部分缺失”的数据演练同样结论)。# 变式脚本:两种删除策略的形状对照 import pandas as pd # 导入pandas库 index_bric = pd.read_excel('https://huoran.oss-cn-shenzhen.aliyuncs.com/20220820/xlsx/1560916966116974592.xlsx', sheet_name='Sheet1', header=0, index_col=0) # 读取金砖指数数据 print(index_bric.shape) # 原始形状 print(index_bric[index_bric.isnull().any(axis=1)]) # 查看含缺失的行:每行只有1列缺失 print(index_bric.dropna().shape) # how='any':任一列缺失即删行 print(index_bric.dropna(how='all').shape) # how='all':仅删全缺失行预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
(20, 4) IBOVESPA指数 RTS指数 Sensex30指数 上证综指 交易日 2019-06-05 95998.75 1303.35 NaN 2861.4181 2019-06-07 97821.26 1325.95 39615.8984 NaN 2019-06-12 98320.88 NaN 39756.8086 2909.3796 (17, 4) (20, 4)注意:以上为变式代码;列表 52.5 的平台任务仍须按原始代码原样输入教学平台。
回扣主线:
dropna()与how='all'/subset的策略差异见第 章节 16 章与本章”要点”第 2 条。思考题:停牌造成的价格缺失用
ffill填充,相当于假设”停牌期间价格保持不变”。这一假设对波动率计算有什么影响?对按日收益率进行的回测又有什么影响?参考答案(点开前请先独立完成)
解题思路:对波动率——停牌期间价格不变意味着这些交易日的收益率为 0,而 0 收益对样本方差的贡献几乎为零,相当于用一串”安静的假日子”稀释了真实的风险:停牌前后本应体现为复牌跳空的一次性大幅波动,被摊到一段零收益区间里,日频波动率与年化波动率都被系统性低估,且停牌越久低估越重。对回测——第一,收益率序列被人为平滑,回撤、夏普比率等风险调整指标失真(风险偏小、比率偏高);第二,停牌期间”价格保持不变”暗示可以按该价格成交,而实际上停牌根本无法交易,复牌日的大幅跳空以单日暴涨/暴跌的形态出现,若策略按日收益率调仓,会高估停牌期间的可执行性、低估复牌时点的冲击成本。稳妥做法是保留缺失、在计算统计量时跳过停牌日,或对停牌日单独打标后做敏感性分析。
回扣主线:“序列开头的缺失值前向填充无前值可用”“处理方案回到业务含义论证”的告诫见第 章节 16 章与本章”要点”第 5 条、“易错点”第 2 条。