Python量化:基于股票数据筛选和计算特征 -3
由于最近正在搭建我的量化框架和基本项目,所以在此出一点小小的教程并附加代码
brief introduction
本期主要目的:
- 对原始数据进行预处理(排序、填充、去离群、标准化)
- 按股票分组,计算额外的滚动统计指标
- 拼接原始数据和新指标,得到增强后的训练集
代码定义的主要几个函数
- fillna_interpolate_by_stock: 按股票对数据进行插值填充缺失值
- outlier_mad_by_stock: 按股票去除离群点
- z_score_by_stock: 按股票进行标准化
代码的主要包含内容
- 读取训练数据train.csv
- 对数据按stock_id和time_id排序
- 调用fillna_interpolate_by_stock填充缺失值并插值
- 调用outlier_mad_by_stock按股票去除离群点
- 调用z_score_by_stock按股票标准化
- 对数据按stock_id和time_id重新排序
- 设置目标变量label
- 对数据进行拷贝,删除time_id和label
- 初始化空DataFrame存储结果
- 定义calculate_new_columns函数计算滚动统计量如5天均值、10天均值等
- 按股票分组,逐股票调用calculate_new_columns函数计算新的指标
- 拼接所有股票的统计量
- 填充NaN值
- 找到原始数据和新计算指标的公共列
- 从新数据中删除公共列
- 拼接原始数据和新数据
- 将结果保存到新CSV文件
主要功能详解
异常值缺失值处理
包含三个问题:为什么要进行处理、处理的好处在哪、如何进行处理
-
为什么要进行预处理
原始数据存在异常值缺失值,会极大影响后续的数据分析步骤,对数据分析和建模产生不良影响。
案例:如果需要计算一百个人的平均收入,其中一个人的收入是100w,其他人都是1元,100w的缺失会极大影响平均值,收入1元个人在计算平均值的时候也是不可缺失的,因为缺少这个人,平均值会不可避免升高,但是实际衡量中我们需要考虑的是剩下来99个人的情况,因此,需要剔除异常值检查缺失值。
-
预处理好处
减少其对分析和建模结果的干扰,提高模型的质量和可靠性。
-
如何进行预处理
- 检测异常值:通过统计方法、可视化探索或基于模型的方法来识别异常值。
- 确认异常值:验证异常值是否真的是数据中的异常,而不是错误或特殊情况。
- 处理异常值:根据数据集和分析目标,选择适当的处理方法,如删除异常值、替换为缺失值或进行数据转换。
标准化的作用
- 单位一致性:数据集中的特征可能以不同的单位(如米、千米、温度、货币等)进行度量,这些单位的差异可能会对算法的性能产生影响。通过标准化,我们可以消除单位对模型的影响,使得算法能够公平地对待所有的特征。
- 算法性能:许多机器学习算法(如支持向量机、逻辑回归、神经网络等)都假设所有的特征都是在同一尺度上。如果特征的尺度差异很大,不进行标准化可能会导致算法性能下降。
- 梯度下降:对于使用梯度下降方法优化的算法,如果特征的尺度差异很大,可能会导致优化过程变得困难,收敛速度变慢。
- 正则化:正则化是一种用于防止过拟合的技术,它通过向模型的损失函数添加一个惩罚项来限制模型的复杂度。但是,如果数据的尺度不一,正则化可能会对较大尺度的特征施加过多的惩罚,导致模型性能下降。
- 解释性:在某些情况下,标准化可以提高模型的解释性。例如,在线性回归模型中,标准化后的特征系数可以直接反映每个特征对预测结果的影响大小。
– 来自GPT4
滚动指标的计算
需要计算哪些滚动指标
包含两问题:有哪些指标可以计算、如何衡量指标的效用
-
有哪些指标可以计算
典型的:均线(5,10,30,60…), 动量,MACD…
-
如何衡量指标的效用
待续…
代码部分
数据案例(详细的案例数据在我的公众号:梦云星)
time_id | stock_id | factor(1-300) | lable |
---|---|---|---|
1 | 1 | 0.2 | 0.1 |
涉及到的包
import pandas as pd
import numpy as np
from tqdm import tqdm
缺失值填充
def fillna_interpolate_by_stock(data: DataFrame) -> DataFrame:
"""
对每个股票的数据使用线性插值来填充 DataFrame 中的缺失值。
参数:
data (DataFrame): 输入的 DataFrame,包含一个 'stock_id' 列和可能的缺失值。
返回:
DataFrame: 输入的 DataFrame,其中的缺失值已经通过线性插值填充。
"""
# 遍历每个与同一 'stock_id' 关联的行组
for stock, group in tqdm(data.groupby('stock_id'), desc="按股票进行插值"):
# 从该组中删除 'time_id' 列
group_no_time_id = group.drop(columns=['time_id'])
# 对该组(不包括 'time_id')进行线性插值,并更新 'data' 中对应的行
data.loc[group.index, group_no_time_id.columns] = group_no_time_id.interpolate(method="linear")
# 返回已填充缺失值的 DataFrame
return data
异常值处理
from typing import DataFrame
from tqdm import tqdm
import numpy as np
def outlier_mad_by_stock(data: DataFrame, k=1.483) -> DataFrame:
"""
使用中位绝对偏差(MAD)方法识别并处理每一股票的数据中的离群值。
参数:
data (DataFrame): 输入的 DataFrame,包含一个 'stock_id' 列。
k (float): 调整因子,默认为 1.483。
返回:
DataFrame: 输入的 DataFrame,其中的离群值已经被处理。
"""
# 遍历每个与同一 'stock_id' 关联的行组
for stock, group in tqdm(data.groupby('stock_id'), desc="按股票移除离群值"):
# 从该组中删去 'time_id' 列
group_no_time_id = group.drop(columns=['time_id'])
# 计算中位数和 MAD
median = group_no_time_id.median()
mad = (group_no_time_id - median).abs().median()
# 计算上下限
upper_limit = median + 3 * k * mad
lower_limit = median - 3 * k * mad
upper_compression_limit = median + (3.5 * k * mad)
lower_compression_limit = median - (3.5 * k * mad)
# 压缩超过上限的值
above_upper = group_no_time_id > upper_limit
for col in group_no_time_id.columns:
compressed = np.interp(group_no_time_id.loc[above_upper[col], col],
(group_no_time_id[col].min(), group_no_time_id[col].max()),
(upper_limit[col], upper_compression_limit[col]))
group_no_time_id.loc[above_upper[col], col] = compressed
# 压缩低于下限的值
below_lower = group_no_time_id < lower_limit
for col in group_no_time_id.columns:
compressed = np.interp(group_no_time_id.loc[below_lower[col], col],
(group_no_time_id[col].min(), group_no_time_id[col].max()),
(lower_compression_limit[col], lower_limit[col]))
group_no_time_id.loc[below_lower[col], col] = compressed
# 更新原始数据
data.loc[group.index, group_no_time_id.columns] = group_no_time_id
# 返回已处理离群值的 DataFrame
return data
标准化
def z_score_by_stock(data: DataFrame, columns: Optional[List[str]] = None) -> DataFrame:
"""
按股票标准化特定的列。
参数:
data (DataFrame): 输入的 DataFrame,包含一个 'stock_id' 列。
columns (List[str], optional): 需要被标准化的列的列表。默认为 None,此时将标准化除 'time_id' 之外的所有列。
返回:
DataFrame: 输入的 DataFrame,其中指定的列已经被标准化。
"""
# 遍历每个与同一 'stock_id' 关联的行组
for stock, group in tqdm(data.groupby('stock_id'), desc="按股票标准化因子"):
# 如果没有指定要标准化的列,则默认标准化除 'time_id' 之外的所有列
if columns is None:
cols = group.columns.drop(['time_id'])
else:
cols = columns
# 按照 Z-Score 公式进行标准化:(值 - 平均值) / 标准差
data.loc[group.index, cols] = (group[cols] - group[cols].mean()) / group[cols].std()
# 返回已标准化的 DataFrame
return data
计算滚动平均值
import numpy as np
import pandas as pd
from typing import DataFrame
def calculate_new_columns(group: DataFrame, col: str) -> DataFrame:
"""
为给定的分组和列计算新的特征,包括滚动平均值、加权移动平均值和动量。
参数:
group (DataFrame): 输入的 DataFrame。
col (str): 需要计算新特征的列的名称。
返回:
DataFrame: 包含新特征的 DataFrame。
"""
# 初始化新的 DataFrame 以存储新的特征
new_columns = pd.DataFrame(index=group.index)
# 计算 5, 10, 30, 35, 60 天的滚动平均值
for window in [5, 10, 30, 35, 60]:
new_columns[f'{col}_{window}_day_mean'] = group[col].rolling(window=window).mean()
# 计算加权移动平均值 (X),权重为 1 到 30
weights = np.arange(1, 31) # for a 30-day moving window
new_columns[f'{col}_X'] = group[col].rolling(window=30).apply(lambda x: np.sum(weights * x) / np.sum(weights),
raw=True)
# 计算动量 (MTM),即当前值减去前一天的值
new_columns[f'{col}_MTM'] = group[col] - group[col].shift(1)
# 返回包含新特征的 DataFrame
return new_columns
这期教程到此为止,欢迎关注同系列教程
完整的代码数据案例在我的公众号:梦云星
参考:
https://zhuanlan.zhihu.com/p/644489331