数据科学和机器学习(第 29 部分):为 AI 训练目的而选择最佳外汇数据的基本技巧·进阶篇
◍ 用卡方筛掉日历类噪声特征
卡方检验干的事很简单:拿列联表比一比「实际出现次数」和「假设无关时的预期次数」,看两个类别变量是否真有关联。它只吃类别变量,连续数值得先离散化,否则跑出来没意义。 做日线级别行情特征工程时,常见的类别输入有 DAYOFMONTH、DAYOFWEEK、DAYOFYEAR、MONTH。要让卡方有用,得先造一个二分类目标——比如当根日线收盘大于开盘就标 1,否则标 0,这样才能衡量「某天属性」和「涨否」是否显著挂钩。 下面这段 sklearn 流程直接把四个日历特征丢进 SelectKBest(chi2),k='all' 表示全保留只看分数。逐行看:先 import chi2 与 SelectKBest;循环打 target 标签;X 只装四个类别列;fit 后取 scores_ 打印。 跑出来的分数很说明问题:DAYOFYEAR 的 Chi2 分数 14.618,而 DAYOFWEEK 只有 0.047、MONTH 0.490、DAYOFMONTH 0.623。日线天然绑定「年内的第几天」,所以 DAYOFYEAR 对目标的影响倾向最强,做特征筛选时它该留,其余几个大概率只是噪声。外汇与贵金属波动受多重因素驱动,这类统计关联仅作特征参考,实盘仍属高风险。
from sklearn.feature_selection class="kw">import chi2 from sklearn.feature_selection class="kw">import SelectKBest target = [] # Loop through each row in the DataFrame to create the target variable for i in range(len(df)): if df.loc[i, &class="macro">#x27;TARGET_CLOSE&class="macro">#x27;] > df.loc[i, &class="macro">#x27;TARGET_OPEN&class="macro">#x27;]: target.append(class="num">1) else: target.append(class="num">0) X = pd.DataFrame({ &class="macro">#x27;DAYOFMONTH&class="macro">#x27;: df[&class="macro">#x27;DAYOFMONTH&class="macro">#x27;], &class="macro">#x27;DAYOFWEEK&class="macro">#x27;: df[&class="macro">#x27;DAYOFWEEK&class="macro">#x27;], &class="macro">#x27;DAYOFYEAR&class="macro">#x27;: df[&class="macro">#x27;DAYOFYEAR&class="macro">#x27;], &class="macro">#x27;MONTH&class="macro">#x27;: df[&class="macro">#x27;MONTH&class="macro">#x27;] }) chi2_selector = SelectKBest(chi2, k=&class="macro">#x27;all&class="macro">#x27;) chi2_selector.fit(X, target) chi2_scores = chi2_selector.scores_ # Output scores for each feature feature_scores = pd.DataFrame({&class="macro">#x27;Feature&class="macro">#x27;: X.columns, &class="macro">#x27;Chi2 Score&class="macro">#x27;: chi2_scores}) print(feature_scores) Feature Chi2 Score class="num">0 DAYOFMONTH class="num">0.622628 class="num">1 DAYOFWEEK class="num">0.047481 class="num">2 DAYOFYEAR class="num">14.618057 class="num">3 MONTH class="num">0.489713
「用方差分析给行情特征排座次」
做特征选择时,若目标变量是类别型(比如「后市涨/跌」二分类),ANOVA 能一次性比较三组及以上特征的均值差异,看哪些连续特征跟分类结果真有统计关联。它算的是组间方差除以组内方差得到的 F 统计量:F 越大,说明不同类别下该特征的均值拉得越开,关系越可能强。 具体跑法是用 Scikit-learn 的 f_classif 做单变量检验,再借 SelectKBest 按分数挑前 K 个。下面这段是直接对行情数据集做的全量 ANOVA,顺手把 F 值和 p 值都打出来。 [CODE] from sklearn.feature_selection import f_classif # We start by dropping the categorical variables in the dataset X = df.drop(columns=[ "DAYOFMONTH", "DAYOFWEEK", "DAYOFYEAR", "MONTH", "TARGET_CLOSE", "TARGET_OPEN" ]) # Perform ANOVA test selector = SelectKBest(score_func=f_classif, k='all') selector.fit(X, target) # Get the F-scores and p-values anova_scores = selector.scores_ anova_pvalues = selector.pvalues_ # Create a DataFrame to display results anova_results = pd.DataFrame({'Feature': X.columns, 'F-Score': anova_scores, 'p-Value': anova_pvalues}) print(anova_results) Feature F-Score p-Value
- OPEN 3.483736 0.062268
- HIGH 3.627995 0.057103
- LOW 3.400320 0.065480
- CLOSE 3.666813 0.055792
- OPEN_LAG1 3.160177 0.075759
- HIGH_LAG1 3.363306 0.066962
- LOW_LAG1 3.309483 0.069181
- CLOSE_LAG1 3.529789 0.060567
- OPEN_LAG2 3.015757 0.082767
- HIGH_LAG2 3.034694 0.081810
- LOW_LAG2 3.259887 0.071295
- CLOSE_LAG2 3.206956 0.073629
- OPEN_LAG3 3.236211 0.072329
- HIGH_LAG3 3.022234 0.082439
- LOW_LAG3 3.020219 0.082541
- CLOSE_LAG3 3.075698 0.079777
- 30DAY_SMA 2.665990 0.102829
- 7DAY_STDDEV 0.639071 0.424238
- DIFF_LAG1_OPEN 1.237127 0.266293
- DIFF_LAG1_HIGH 0.991862 0.319529
- DIFF_LAG1_LOW 0.131002 0.717472
- DIFF_LAG1_CLOSE 0.198001 0.656435
selector = SelectKBest(score_func=f_classif, k=10) X_selected = selector.fit_transform(X, target) # print the selected feature names selected_features = X.columns[selector.get_support()] print("Selected Features:", selected_features) Selected Features: Index(['OPEN', 'HIGH', 'LOW', 'CLOSE', 'HIGH_LAG1', 'LOW_LAG1', 'CLOSE_LAG1', 'LOW_LAG2', 'CLOSE_LAG2', 'OPEN_LAG3'], dtype='object') [/CODE] 逐行看这段代码:先 drop 掉日历类和目标类字段,只留数值特征进 X;SelectKBest 配 f_classif、k='all' 是跑全量不裁剪,fit 后从 scores_ 和 pvalues_ 取数;最后换 k=10 重新 fit_transform,get_support 掩码挑出分数最高的十个特征。 从打印结果能读出硬数据:CLOSE 的 F 值 3.667 为全场最高,p 值 0.0558 最接近 0.05 显著线;而 DIFF_LAG1_LOW 的 F 仅 0.131、p 值 0.717,基本可判定与原分类目标无关。所有特征 p 值均大于 0.05,说明在样本内它们对分类的贡献都只是「边缘显著」,外汇与贵金属市场本身高风险,这种统计微弱关联拿去实盘前务必扩大样本重测。 挑完前十特征后,后续建模就只用 OPEN/HIGH/LOW/CLOSE 及部分延迟项,省掉 7DAY_STDDEV、DIFF_LAG1_* 这类噪声。下一层包装器思路会换种玩法来评估子集。
from sklearn.feature_selection class="kw">import f_classif # We start by dropping the categorical variables in the dataset X = df.drop(columns=[ "DAYOFMONTH", "DAYOFWEEK", "DAYOFYEAR", "MONTH", "TARGET_CLOSE", "TARGET_OPEN" ]) # Perform ANOVA test selector = SelectKBest(score_func=f_classif, k=&class="macro">#x27;all&class="macro">#x27;) selector.fit(X, target) # Get the F-scores and p-values anova_scores = selector.scores_ anova_pvalues = selector.pvalues_ # Create a DataFrame to display results anova_results = pd.DataFrame({&class="macro">#x27;Feature&class="macro">#x27;: X.columns, &class="macro">#x27;F-Score&class="macro">#x27;: anova_scores, &class="macro">#x27;p-Value&class="macro">#x27;: anova_pvalues}) print(anova_results) Feature F-Score p-Value class="num">0 OPEN class="num">3.483736 class="num">0.062268 class="num">1 HIGH class="num">3.627995 class="num">0.057103 class="num">2 LOW class="num">3.400320 class="num">0.065480 class="num">3 CLOSE class="num">3.666813 class="num">0.055792 class="num">4 OPEN_LAG1 class="num">3.160177 class="num">0.075759 class="num">5 HIGH_LAG1 class="num">3.363306 class="num">0.066962 class="num">6 LOW_LAG1 class="num">3.309483 class="num">0.069181 class="num">7 CLOSE_LAG1 class="num">3.529789 class="num">0.060567 class="num">8 OPEN_LAG2 class="num">3.015757 class="num">0.082767 class="num">9 HIGH_LAG2 class="num">3.034694 class="num">0.081810 class="num">10 LOW_LAG2 class="num">3.259887 class="num">0.071295 class="num">11 CLOSE_LAG2 class="num">3.206956 class="num">0.073629 class="num">12 OPEN_LAG3 class="num">3.236211 class="num">0.072329 class="num">13 HIGH_LAG3 class="num">3.022234 class="num">0.082439 class="num">14 LOW_LAG3 class="num">3.020219 class="num">0.082541 class="num">15 CLOSE_LAG3 class="num">3.075698 class="num">0.079777 class="num">16 30DAY_SMA class="num">2.665990 class="num">0.102829 class="num">17 7DAY_STDDEV class="num">0.639071 class="num">0.424238 class="num">18 DIFF_LAG1_OPEN class="num">1.237127 class="num">0.266293 class="num">19 DIFF_LAG1_HIGH class="num">0.991862 class="num">0.319529 class="num">20 DIFF_LAG1_LOW class="num">0.131002 class="num">0.717472 class="num">21 DIFF_LAG1_CLOSE class="num">0.198001 class="num">0.656435 selector = SelectKBest(score_func=f_classif, k=class="num">10) X_selected = selector.fit_transform(X, target) # print the selected feature names selected_features = X.columns[selector.get_support()] print("Selected Features:", selected_features) Selected Features: Index([&class="macro">#x27;OPEN&class="macro">#x27;, &class="macro">#x27;HIGH&class="macro">#x27;, &class="macro">#x27;LOW&class="macro">#x27;, &class="macro">#x27;CLOSE&class="macro">#x27;, &class="macro">#x27;HIGH_LAG1&class="macro">#x27;, &class="macro">#x27;LOW_LAG1&class="macro">#x27;, &class="macro">#x27;CLOSE_LAG1&class="macro">#x27;, &class="macro">#x27;LOW_LAG2&class="macro">#x27;, &class="macro">#x27;CLOSE_LAG2&class="macro">#x27;, &class="macro">#x27;OPEN_LAG3&class="macro">#x27;], dtype=&class="macro">#x27;object&class="macro">#x27;)
用递归淘汰给特征集瘦身
递归特征消除(RFE)的核心动作很直接:先拿全部特征训一个能排重要度的模型,再每轮踢掉最不重要的那批,直到剩下你指定的数量。上面这段脚本用逻辑回归当底座,硬挑出 10 个特征,从 25 个原始列里筛完,OPEN/HIGH/LOW/CLOSE 及部分 LAG1~LAG3 留了下来,而 30DAY_SMA、7DAY_STDDEV、日历类字段全被标成 False。 RFE 不挑模型,只要你能从模型里拿到特征重要性排名就能挂上用。砍掉噪声列后,过拟合概率会降,样本外表现倾向更稳。但它是贪心算法,未必能掏出全局最优子集;遇到神经网络或大样本,反复重训的算力账单会很难看。 实盘拿去跑外汇或贵金属信号前,先认清这类筛选只是降维手段,不是盈利保证。杠杆品种波动剧烈,特征再干净也挡不住黑天鹅,验证时务必用walk-forward切分。 下面这段代码就是可直接丢进 notebook 跑的瘦身流程,改 n_features_to_select 就能换筛选强度:
from sklearn.feature_selection class="kw">import RFE from sklearn.linear_model class="kw">import LogisticRegression # Prepare the target variable, again y = [] # Loop through each row in the DataFrame to create the target variable for i in range(len(df)): if df.loc[i, &class="macro">#x27;TARGET_CLOSE&class="macro">#x27;] > df.loc[i, &class="macro">#x27;TARGET_OPEN&class="macro">#x27;]: y.append(class="num">1) else: y.append(class="num">0) # Drop future variables from the feature set X = df.drop(columns=["TARGET_CLOSE", "TARGET_OPEN"]) # Initialize the model model = LogisticRegression(max_iter=class="num">10000) # Initialize RFE with the model and number of features to select rfe = RFE(estimator=model, n_features_to_select=class="num">10) # Fit RFE rfe.fit(X, y) selected_features_mask = rfe.support_ # Getting the names of the selected features feature_names = X.columns selected_feature_names = feature_names[selected_features_mask] selected_features = pd.DataFrame({ "Name": feature_names, "Mask": selected_features_mask }) selected_features.head(-class="num">1) # Filter the dataset to keep only the selected features X_selected = X.loc[:, selected_features_mask] class="macro">#for better readability, we convert this into pandas dataframe X_selected_df = pd.DataFrame(X_selected, columns=selected_feature_names) print("Selected Features") X_selected_df.head()
◍ 逐步筛特征的前向与后向路径
包装式特征选择里,顺序法靠对模型贡献的增减来搭特征集,分前向与后向两条路。前向从空集起手,逐个塞特征,直到凑够数量或再加也不提升性能;后向反过来,全量开局,每次踢掉最不重要的那个,留到指定数目为止。 两者手法相反,却往往收敛到相同解、产出同等规模特征子集。好处是逻辑直白、能和任意机器学习算法搭档,挑出最相关特征后模型表现可能更干净。 短板也实在:特征多或样本大时跑得慢,且只看局部改进,未必摸得到全局最优集。外汇与贵金属行情噪声大,用这类筛选做信号模型须认清过拟合风险偏高。 下面这段 Python 演示了用顺序选择器在 10 个特征上做前向与后向,输出索引与名称差异明显——前向选了 [1,7,8,12,17,19,22,23,24,25],后向选了 [2,3,7,10,11,12,13,14,15,16],重合仅 7、12 附近少数滞后项。
from sklearn.feature_selection class="kw">import SequentialFeatureSelector # Create a logistic regression model model = LogisticRegression(max_iter=class="num">10000) # Create a SequentialFeatureSelector object sfs = SequentialFeatureSelector(model, n_features_to_select=class="num">10, direction=&class="macro">#x27;forward&class="macro">#x27;) # Fit the SFS object to the training data sfs.fit(X, target) # Get the selected feature indices selected_features = sfs.get_support(indices=True) selected_features_names = X.columns[selected_features] # get the feature names # Print the selected features print("Selected feature indices:", selected_features) print("Selected feature names:", selected_feature_names) Selected feature indices: [ class="num">1 class="num">7 class="num">8 class="num">12 class="num">17 class="num">19 class="num">22 class="num">23 class="num">24 class="num">25] Selected feature names: Index([&class="macro">#x27;OPEN&class="macro">#x27;, &class="macro">#x27;HIGH&class="macro">#x27;, &class="macro">#x27;LOW&class="macro">#x27;, &class="macro">#x27;CLOSE&class="macro">#x27;, &class="macro">#x27;HIGH_LAG1&class="macro">#x27;, &class="macro">#x27;LOW_LAG1&class="macro">#x27;, &class="macro">#x27;CLOSE_LAG1&class="macro">#x27;, &class="macro">#x27;LOW_LAG2&class="macro">#x27;, &class="macro">#x27;CLOSE_LAG2&class="macro">#x27;, &class="macro">#x27;OPEN_LAG3&class="macro">#x27;], dtype=&class="macro">#x27;object&class="macro">#x27;) # Create a logistic regression model model = LogisticRegression(max_iter=class="num">10000) # Create a SequentialFeatureSelector object sfs = SequentialFeatureSelector(model, n_features_to_select=class="num">10, direction=&class="macro">#x27;backward&class="macro">#x27;) # Fit the SFS object to the training data sfs.fit(X, target) # Get the selected feature indices selected_features = sfs.get_support(indices=True) selected_features_names = X.columns[selected_features] # get the feature names # Print the selected features print("Selected feature indices:", selected_features) print("Selected feature names:", selected_feature_names) Selected feature indices: [ class="num">2 class="num">3 class="num">7 class="num">10 class="num">11 class="num">12 class="num">13 class="num">14 class="num">15 class="num">16] Selected feature names: Index([&class="macro">#x27;OPEN&class="macro">#x27;, &class="macro">#x27;HIGH&class="macro">#x27;, &class="macro">#x27;LOW&class="macro">#x27;, &class="macro">#x27;CLOSE&class="macro">#x27;, &class="macro">#x27;HIGH_LAG1&class="macro">#x27;, &class="macro">#x27;LOW_LAG1&class="macro">#x27;, &class="macro">#x27;CLOSE_LAG1&class="macro">#x27;, &class="macro">#x27;LOW_LAG2&class="macro">#x27;, &class="macro">#x27;CLOSE_LAG2&class="macro">#x27;, &class="macro">#x27;OPEN_LAG3&class="macro">#x27;], dtype=&class="macro">#x27;object&class="macro">#x27;)
「训练里顺手挑特征」
嵌入式方法把特征选择塞进模型训练过程里一起跑,而不是先筛完再训。典型流程就三步:先训机器学习模型,再从中导出特征重要性,最后只保留排名靠前的预测变量。 最常见的两类是 Lasso 回归和基于决策树的方法。Lasso 靠 L1 惩罚把不重要变量的系数直接压到 0,决策树类则按分裂增益给出特征贡献度,两者都能在 MT5 的 Python 环境或外接训练脚本里直接验证。 想落地的话,先用历史 tick 或分钟 Bar 拉一组贵金属波动特征,跑一次带 L1 的回归,看几个系数归零,就能判断哪些因子在当下样本里纯属噪声。外汇与贵金属杠杆高、滑点突变频繁,归零结论只代表回测窗口内的概率倾向,换周期可能反转。