【维克】模块3总结:从一行空数据,到一个能跑的模型

0 阅读12分钟

我花了三个月,走完了整个模块3。

回头看——

这12篇讲的其实就是一件事:

怎么把一堆乱七八糟的金融数据,变成模型能吃的东西。

你可能觉得每篇拆开看都挺零碎的。

今天讲特征工程,明天讲归一化,后天讲PCA,大后天又讲缺失值……

学完之后——

"好像每篇都懂了。"

"但合在一起,还是不知道从哪开始。"

别急。

今天这篇,我不讲新知识。

我把这12篇串成一条线。

从你拿到一个空的CSV文件开始,到模型能跑出第一行结果为止——

中间每一步,该怎么走。

一张图:模块3的完整地图

先看全貌。

整个模块3,可以分成五个阶段:

阶段1:数据长什么样

阶段2:怎么造因子(特征工程)

阶段3:怎么让因子"干净"(预处理)

阶段4:怎么降维压缩(特征选择)

阶段5:怎么存、怎么取(数据工程)

每个阶段,对应几篇内容:

| 阶段 | 对应篇目 | 核心问题 | | --- | --- | --- | | 阶段1:认识数据 | 第38-40篇 | 金融数据和普通数据有什么不同?时间序列、平稳性、随机游走 | | 阶段2:造因子 | 第69-73篇 | 特征工程怎么做?动量、波动率、成交量、时间序列特征 | | 阶段3:洗干净 | 第74、78、79篇 | 归一化、去极值、缺失值处理 | | 阶段4:精简压缩 | 第75-76篇 | 特征选择、PCA降维 | | 阶段5:工程落地 | 第77篇 | CSV/Parquet/SQLite/HDF5怎么选 |

五个阶段。从认识数据,到工程落地。

这就是模块3的全部。

阶段1:先搞清楚数据的脾气

在第38篇,我说过一句话:

"金融数据不是普通的时间序列。它有记忆,但不多。"

这句话,到现在我仍然觉得是模块3最重要的起点。

你拿到一份股票数据,第一反应是什么?

画图。

画收盘价走势,画成交量柱状图,画收益率分布。

画完之后你会发现:

• 价格曲线像是在"随机游走"——昨天涨不意味着今天一定涨

• 收益率分布"胖尾巴"——极端涨跌比正态分布预测的要多

• 波动率会"聚类"——大的波动后面跟大的波动,小的跟小的

这些不是"有趣的观察"。

这些是数据的脾气

你不了解这些脾气,直接套模型——

用线性回归预测价格?价格不平稳,回归全是假的。

用正态分布假设收益率?胖尾巴会让你的风险估计严重低估。

忽略波动率聚类?你的模型会在市场剧烈波动时全面崩溃。

阶段1的教训:金融数据有自己的规律,你得先尊重它,再建模。

关键知识点:

• 时间序列的特殊性(顺序不能打乱,数据点之间有依赖)

• 平稳性(均值和方差不能随时间漂移)

• 白噪声与随机游走(价格本身不可预测,可预测的是收益率的某些特征)

• ADF检验(怎么判断数据平不平稳)

阶段2:造因子——从原始数据到"有用的数字"

这是模块3最核心的部分。

第69篇讲过一个故事——

我把原始数据直接丢进模型,跑出来的结果一塌糊涂。

导师说:

"你见过厨师直接把原材料端上桌吗?"

原材料是食材。因子是菜品。

中间需要加工。

这个过程,就叫特征工程

我们造了四类因子:

第一类:动量特征(第70篇)

过去N天的收益率。

// python
# 5日动量
df['momentum_5d'] = df.groupby('code')['close'].pct_change(5)

# 20日动量
df['momentum_20d'] = df.groupby('code')['close'].pct_change(20)

逻辑很简单:涨过的可能继续涨,也可能反转。

动量因子能不能用,取决于市场环境和回测验证。

第二类:波动率特征(第71篇)

过去N天收益率的标准差。

// python
# 20日历史波动率
df['volatility_20d'] = df.groupby('code')['daily_return'].transform(
lambda x: x.rolling(20).std()
)

波动率告诉我们:这只股票"脾气大不大"。

脾气大的股票,风险高,但机会也可能大。

脾气小的股票,稳,但可能也没什么弹性。

第三类:成交量特征(第72篇)

量比、OBV(能量潮)、资金流向。

// python
# 量比
df['volume_ratio'] = df['volume'] / df.groupby('code')['volume'].transform(
lambda x: x.rolling(5).mean()
)

价格告诉你"涨跌了多少"。

成交量告诉你"涨跌背后的力量有多大"。

量价配合才是完整的信号。

价格涨了,量也放大了——这是真涨。

价格涨了,量反而缩了——这是虚涨,小心反转。

第四类:时间序列特征(第73篇)

日期相关的特征——周几、月初月末、节假日前后。

// python
# 星期几
df['day_of_week'] = df['date'].dt.dayofweek

# 是否月初(前3个交易日)
df['is_month_start'] = df.groupby(['code', df['date'].dt.to_period('M')]).cumcount() < 3

看起来很简单。

但A股确实存在"周一效应"、"月末效应"。

这些不是迷信——是行为金融学的规律。

阶段2的核心收获:

因子不是越多越好。

每个因子都要能回答一个问题:"这个因子在捕捉什么信息?"

答不上来——这个因子就是噪音。

阶段3:洗干净——让因子变成模型能吃的样子

造完因子,不能直接喂模型。

因为模型对数据有"口味要求":

• 数值范围不能差太大(不然大数 dominate 小数)

• 不能有极端值(不然一个离群点带偏整个模型)

• 不能有空值(不然模型直接报错)

这就是阶段3——数据预处理

归一化/标准化(第74篇)

把数据缩放到统一的范围。

// python
# 方法1:Min-Max归一化 → [0, 1]
df['momentum_norm'] = (df['momentum_5d'] - df['momentum_5d'].min()) / \
(df['momentum_5d'].max() - df['momentum_5d'].min())

# 方法2:Z-score标准化 → 均值0,标准差1
df['momentum_zscore'] = (df['momentum_5d'] - df['momentum_5d'].mean()) / \
df['momentum_5d'].std()

量化里更常用Z-score。 因为金融数据经常有负值,Min-Max归一化会把它压到0附近,失去信息。

还有一个坑:一定要按"训练集"的均值和标准差来标准化测试集。 不能用全局的——那就是前视偏差。

去极值(第78篇)

第78篇讲过一个故事——一个极端值487,让一个本来没用的因子"看起来"很有效。

去极值的方法:

// python
# MAD法(中位数绝对偏差)
median = df['factor'].median()
mad = (df['factor'] - median).abs().median()
upper = median + 5 * 1.4826 * mad
lower = median - 5 * 1.4826 * mad

# 缩尾处理(Winsorize)——不是删除,而是截断
df['factor_clean'] = df['factor'].clip(lower, upper)

缩尾 > 删除。 因为极端值虽然可能是噪音,但也可能是重要的信号。缩尾保留了"这个值确实很极端"的信息,只是不让它过度影响模型。

缺失值处理(第79篇)

三种类型,三种态度:

| 类型 | 原因 | 处理方式 | | --- | --- | --- | | 停牌日 | 无交易 | 前值填充(ffill) | | 数据源bug | 系统故障 | 删除或插值 | | 财务指标空值 | 非报告期 | 最近公布值填充 + is_missing标志 |

铁律:永远用ffill,永远不用bfill。 因为在t时刻,你只能知道t之前的数据。

阶段3的核心收获:

预处理不是"随便处理一下"。

每一步处理都要问:"我有没有用到未来的信息?"

如果有——你的回测就是假的。

阶段4:精简压缩——用更少的特征做更多的事

造了30个因子。

模型吃得消吗?

技术上——吃得消。

但实际上:

• 因子越多,越容易过拟合

• 很多因子之间高度相关(比如5日动量和10日动量)

• 模型训练时间随特征数量指数增长

所以你需要精简

特征选择(第75篇)

三种方法:

// python
# 1. 过滤法:相关性筛选
correlation = df[['factor1', 'factor2', ..., 'return']].corr()
# 删除与目标变量相关性<0.02的因子
# 删除彼此相关性>0.95的因子(共线性)

# 2. 包装法:递归特征消除(RFE)
from sklearn.feature_selection import RFE
from sklearn.linear_model import LinearRegression
rfe = RFE(LinearRegression(), n_features_to_select=10)
rfe.fit(X, y)

# 3. 嵌入法:Lasso正则化
from sklearn.linear_model import Lasso
lasso = Lasso(alpha=0.01)
lasso.fit(X, y)
# Lasso会自动把不重要的特征系数压缩到0

PCA降维(第76篇)

如果你不想"选"特征——你可以"压缩"特征。

PCA把你的30个因子压缩成10个主成分。

每个主成分是原来30个因子的线性组合。

// python
from sklearn.decomposition import PCA

pca = PCA(n_components=10)
X_pca = pca.fit_transform(X_scaled)

# 看看前10个主成分解释了多少方差
print(f"累计解释方差: {pca.explained_variance_ratio_.sum():.2%}")

PCA的代价:可解释性丧失。

你原来的30个因子,每个都有明确含义——动量、波动率、成交量……

压缩成10个主成分之后,每个成分是什么含义?你不知道。

但模型不在乎。它在乎的是:数据干净、维度低、信息保留够。

阶段4的核心收获:

特征工程的上限,决定了模型的上限。

但特征不是越多越好——少而精 > 多而杂。

阶段5:工程落地——选对存储方案

最后一个阶段,最短,但不能忽略。

第77篇讲过——

当你有500只股票×3000个交易日×50个因子 = 7500万条数据。

你怎么存?

| 格式 | 大小 | 读取速度 | 适用场景 | | --- | --- | --- | --- | | CSV | 2GB | 慢 | 调试、分享 | | Parquet | 200MB | 快 | 日常回测 | | SQLite | 300MB | 中 | 需要SQL查询 | | HDF5 | 150MB | 最快 | 大规模计算 |

日常用Parquet。 压缩率高、读取快、Python生态兼容好。

// python
# 保存
df.to_parquet('factor_data.parquet', compression='snappy')

# 读取
df = pd.read_parquet('factor_data.parquet')

一行代码的事。但选错格式,你可能要多等10倍的读取时间。

完整流程:从0到1

现在,把所有阶段拼在一起。

假设你明天要跑一个选股策略。完整流程是这样的:

Step 1: 获取数据
→ 从Tushare/AKShare拉取日线+财务数据
→ 存入Parquet

Step 2: 认识数据
→ 画价格图、收益率分布、波动率时序
→ 做ADF检验,确认平稳性
→ 如果不平稳 → 差分

Step 3: 造因子
→ 动量因子(5/10/20日)
→ 波动率因子(20/60日)
→ 成交量因子(量比、OBV)
→ 时间因子(周几、月初月末)

Step 4: 洗数据
→ 缺失值处理(停牌ffill、成交量填0、财务指标+is_missing标志)
→ 去极值(MAD缩尾)
→ 标准化(Z-score,按训练集)

Step 5: 精简
→ 相关性筛选(删除高度共线的因子)
→ 或PCA压缩到10个主成分

Step 6: 建模
→ 线性回归 / XGBoost / LSTM
→ 训练集/测试集按时间切分(不能随机split)

Step 7: 回测验证
→ 计算夏普、最大回撤、年化收益
→ 检查有没有前视偏差

Step 8: 实盘/模拟盘验证

这就是模块3给你的东西——一条从原始数据到可执行策略的完整流水线。

模块3教会我的三件事

最后,说点私人的。

回顾这12篇,我觉得最重要的不是哪个技术细节——而是三个认知转变。

认知1:数据决定上限,模型只是逼近

你用了最复杂的深度学习模型,但数据全是噪音——结果不如一个用干净数据训练的线性回归。

花80%的时间在数据处理上,花20%的时间在模型选择上。

这个比例,对新手来说反直觉。但做多了你就知道——这才是真相。

认知2:前视偏差是最大的敌人

你的回测夏普2.0,实盘亏成狗。

90%的概率——是前视偏差。

用全局均值填充缺失值?前视偏差。

用bfill填充停牌日?前视偏差。

用测试集的标准化参数处理训练集?前视偏差。

随机split训练集测试集(不按时间)?前视偏差。

每一次数据处理,都问自己:"这个操作,在真实交易的时候,我能做到吗?"

做不到——就是前视偏差。

认知3:缺失和极端值不是噪音,是信号

新手看到NaN,第一反应:删掉。

新手看到极端值,第一反应:去掉。

老手看到NaN,会问:为什么空?

老手看到极端值,会问:这个极端背后发生了什么?

数据里的异常,往往藏着市场的情绪。

停牌可能意味着重大事件。

成交量为零可能意味着跌停封板。

PE为负可能意味着公司亏损。

这些信息,不是"脏数据"——是"珍贵信号"。

下一步:模块4预告

模块3讲完了。

你知道了怎么获取数据、造因子、清洗、降维、存储。

但这些——只是"备料"。

模块4,我们开始"做菜"。

模块4是经典策略解析

从第81篇开始,我会一个一个拆解量化交易里最经典的策略类型:

• 动量策略(追涨杀跌的学问)

• 均值回归(高抛低吸的数学)

• 配对交易(两只股票之间的套利)

• 趋势跟踪(让利润奔跑)

• 统计套利(数学驱动的无风险收益?)

• 波动率策略(交易"不确定性")

• 日内策略(分钟级的博弈)

• 事件驱动(财报、并购、分红)

每个策略,我都会讲:

1. 核心逻辑(它赚的是什么钱?)

2. 数学原理(背后的公式不复杂,但很重要)

3. 参数选择(怎么调参数,怎么避免过拟合)

4. 优缺点(什么行情赚,什么行情亏)

5. 实战注意事项(回测容易踩的坑)

从模块4开始——

我们从"数据处理者",变成"策略开发者"。

准备好了吗?

OVER。