1. 这节究竟解决什么实际问题?¶
2.3 特征工程 的具体任务是:把原始价格转换成只能在当时计算出来的收益、波动和技术特征。 本节将明确输入数据、计算过程、输出及其在后续研究中的用途。
2. 前置知识¶
掌握 DataFrame 的行、列、索引与缺失值;建议先完成收益率一节。 对公式的理解应包括变量定义、单位、时间索引和适用条件。
3. 数据说明¶
默认使用固定 CSV;在线接口只用于扩展。必须记录来源、下载日、字段定义、时区和复权方式。 运行前先检查 shape、日期范围、列名与缺失值。数据看起来能画图,不代表它适合回答研究问题。
4. 学习目标与完成标准¶
完成本节后,你应能用自己的话解释 2.3 特征工程 的输入、计算步骤、输出和局限;尤其能说明:把原始价格转换成只能在当时计算出来的收益、波动和技术特征。 你还应能独立改动一个参数并解释结果为什么变化,指出至少一个会让结论失真的假设。
5. 核心概念与公式¶
原始数据需要先完成字段核验、缺失值处理、异常检查和来源记录,随后才能用于特征计算。
特征只由时点 t 及以前的信息计算;目标值可以在未来,但绝不能混入特征列。 看到公式时依次确认:每个符号的单位是什么?它属于哪个时点?分母会不会为零?它是描述性统计量、预测量,还是交易规则?
6. 从零实现¶
原始代码按照“准备输入 → 计算中间量 → 得到结果 → 解释输出”的顺序组织。每个单元应检查变量类型、索引、维度和代表性数值。中间变量用于核对公式实现和定位计算错误。
7. 成熟库实现¶
Pandas 负责加载、对齐和清洗;数据源库只提供原料,不保证研究可用性。 使用库函数前先阅读参数含义、默认值和返回对象;库输出一个数字,并不说明这个数字适合你的交易假设。
8. 两种实现的差异¶
从零实现便于核对公式、调试和识别隐含假设;成熟库适合在计算逻辑已验证后复用。两种实现依赖相同的数据口径与时点假设,使用库函数不会改变输入数据的质量。
9. 预期结果与检查范围¶
你应看到维度正确的 Series/DataFrame、日期单调递增的索引,以及能支持“把原始价格转换成只能在当时计算出来的收益、波动和技术特征。”这一任务的表格或图形。金融日收益率均值通常接近 0;若出现极大的连续收益、空表、全 NaN 或索引错位,应先排查数据和对齐逻辑,而不是立即相信结果。
10. 常见错误与诊断¶
把日期当字符串排序、重复索引、错误前向填充、混用 Close 与 Adj Close、把未来标签泄漏进特征。
诊断顺序:先打印类型与 shape,再看 head()/tail(),接着检查日期和 NaN,最后才比较数值。这样能避免在错误的对象上反复改参数。
11. 研究偏差检查¶
数据修订、退市股票缺失和接口字段变化都会制造看不见的幸存者偏差或未来函数。 结果至少应与一个简单基线比较,并明确哪些部分只是教学演示。
12. 三层练习与答案¶
- 基础练习:验证一个中间变量、补全一段小计算,并用一句话解释输出。
- 研究练习:改变窗口、样本区间、约束或标的,记录哪些结论仍成立。
- 开放项目:把本节方法嵌入模块项目,报告基线、失败结果和限制。
题目在 ../exercises/02_data/03_feature_engineering_exercises.ipynb;完整推导、可运行参考实现与结果检查在 ../solutions/02_data/03_feature_engineering_solutions.ipynb。请先独立作答,再打开答案。
13. 延伸阅读¶
延伸材料:pandas 用户指南。阅读记录应包括研究假设、数据频率、样本范围和结论适用条件。
14. 下一节¶
完成本模块后,进入下一模块的概览页,并把本节的检查清单带到后续研究中。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
import seaborn as sns
from statsmodels.tsa.stattools import adfuller
# 下载数据
raw = yf.download('AAPL', start='2020-01-01', end='2024-01-01', progress=False)
df = pd.DataFrame({
'Open': raw['Open'].squeeze(),
'High': raw['High'].squeeze(),
'Low': raw['Low'].squeeze(),
'Close': raw['Close'].squeeze(),
'Volume': raw['Volume'].squeeze(),
})
print(f'原始数据准备就绪: {df.shape}')
原始数据准备就绪: (1006, 5)
# 默认使用固定样本,保证课程与 CI 不依赖实时接口。
USE_OFFLINE_DATA = True
if USE_OFFLINE_DATA:
from pathlib import Path
import sys
docs_root = next(parent for parent in [Path.cwd(), *Path.cwd().parents]
if (parent / 'utils' / 'offline_data.py').exists())
sys.path.insert(0, str(docs_root))
from utils.offline_data import configure_yfinance_offline
configure_yfinance_offline(yf)
else:
print('在线下载为扩展步骤;请核对数据字段、时区和下载日期。')
import matplotlib.pyplot as plt
# 1. 设置系统自带的中文字体(这里使用黑体 SimHei)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 如果你想用微软雅黑,可以改成 ['Microsoft YaHei']
# 2. 解决更换字体后,负号(-)显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
1. 为什么“价格”不能直接做特征? (平稳性原则)¶
QuantEcon 核心观点: 大多数量化模型要求输入数据是平稳的 (Stationary)。平稳意味着数据的均值和方差不会随着时间发生巨大漂移。
价格是不平稳的(今天 100 元,明年可能 200 元),直接用价格训练模型会导致“伪回归”。我们需要使用收益率或价格差分。
# ADF 检验:判别数据是否平稳 (p-value < 0.05 则平稳)
def check_stationarity(series, name):
result = adfuller(series.dropna())
print(f"{name} ADF p-value: {result[1]:.5f} {'(平稳 )' if result[1] < 0.05 else '(不平稳 )'}")
check_stationarity(df['Close'], "收盘价")
check_stationarity(df['Close'].pct_change(), "日收益率")
print("结论:在特征工程中,请尽量使用收益率或振幅,而不是原始价格。")
收盘价 ADF p-value: 0.52759 (不平稳 ) 日收益率 ADF p-value: 0.00000 (平稳 ) 结论:在特征工程中,请尽量使用收益率或振幅,而不是原始价格。
2. 构造基础特征矩阵¶
# 收益率特征
df['ret_1d'] = df['Close'].pct_change()
df['ret_5d'] = df['Close'].pct_change(5)
# 波动与动量
df['vol_20d'] = df['ret_1d'].rolling(20).std()
df['ma_ratio'] = df['Close'] / df['Close'].rolling(20).mean() - 1
# 价格形态特征
df['upper_shadow'] = (df['High'] - np.maximum(df['Close'], df['Open'])) / df['Close']
df['lower_shadow'] = (np.minimum(df['Close'], df['Open']) - df['Low']) / df['Close']
print("特征构建完成")
特征构建完成
def rolling_scale(series, window=60):
mean = series.rolling(window).mean()
std = series.rolling(window).std()
return (series - mean) / std
df['ret_scaled'] = rolling_scale(df['ret_1d'])
plt.figure(figsize=(12, 4))
plt.plot(df['ret_1d'], label='原始收益率', alpha=0.5)
plt.plot(df['ret_scaled'], label='滚动标准化收益率', alpha=0.8)
plt.title("滚动标准化:确保每个时间点的数据仅包含该点之前的统计信息")
plt.legend()
plt.show()
4. 滞后特征与未来标签 (Target Engineering)¶
在预测任务中:
- 特征 ($X$):必须是过去的数据(如昨日收益率
shift(1))。 - 标签 ($y$):是未来的数据(如明日收益率
shift(-1))。
# 特征:滞后一天 (当日收盘时,我们知道昨天的收盘情况)
df['feat_ret_lag1'] = df['ret_1d'].shift(1)
df['feat_vol_lag1'] = df['vol_20d'].shift(1)
# 标签:明天的收益率
df['target_y'] = df['ret_1d'].shift(-1)
feature_cols = ['feat_ret_lag1', 'feat_vol_lag1', 'ma_ratio']
final_df = df[feature_cols + ['target_y']].dropna()
print("特征与标签对齐完成。可以喂给机器学习模型了。")
final_df.tail(3)
特征与标签对齐完成。可以喂给机器学习模型了。
| feat_ret_lag1 | feat_vol_lag1 | ma_ratio | target_y | |
|---|---|---|---|---|
| Date | ||||
| 2023-12-26 | -0.005547 | 0.009071 | -0.004032 | 0.000518 |
| 2023-12-27 | -0.002841 | 0.009103 | -0.004222 | 0.002226 |
| 2023-12-28 | 0.000518 | 0.009086 | -0.003087 | -0.005424 |
练习¶
- 尝试对成交量进行 ADF 检验,看看成交量序列是否平稳?如果不平稳,该如何转换?
- 构造一个“动量交互特征”:
ma_ratio * ret_scaled,观察它在市场大幅波动时的表现。 - 使用
df['Close'].diff()计算价格一阶差分,并对结果进行 ADF 检验。
下一模块 → ../03_indicators/01_trend_indicators.ipynb(我们将把这些处理好的特征转化为具体的交易信号)