Spaces:
Build error
Build error
File size: 6,955 Bytes
fab9847 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | """
feature_selection.py — 特征筛选漏斗:329个原始指标 → 17个入选特征
=================================================================
完整的特征筛选流程:
Stage 1: 数据可用性筛选(缺失率 < 30%,时间覆盖 > 120月)
Stage 2: 单变量相关性(与油价收益率 |corr| > 0.05)
Stage 3: 共线性过滤(VIF / corr-cluster 去重)
Stage 4: MI / Granger 因果(非线性信息量)
Stage 5: 经济学意义验证(因子组分配)
"""
import pandas as pd
import numpy as np
import os
import glob
from config import DATA_DIR, FEATURES, FACTOR_GROUPS, PRICE_COL
def run_feature_funnel(panel_path, raw_dir=None):
"""执行完整的特征筛选漏斗并返回每阶段结果。"""
raw_dir = raw_dir or DATA_DIR
panel = pd.read_csv(panel_path, index_col=0, parse_dates=True)
# ── Stage 0: Inventory all raw features ──
all_features = set()
file_info = []
csv_files = glob.glob(os.path.join(raw_dir, '*.csv'))
for f in csv_files:
try:
df = pd.read_csv(f, nrows=5)
cols = [c for c in df.columns if c.lower() not in
('date', 'date_str', 'date_num', 'year', 'month', 'day', 'unnamed: 0')]
all_features.update(cols)
file_info.append({'file': os.path.basename(f), 'n_cols': len(cols), 'cols': cols[:10]})
except:
pass
total_raw = len(all_features)
print(f"Stage 0: 原始指标 {total_raw} 个 (来自 {len(csv_files)} 个CSV)")
# ── Stage 1: Availability filter ──
panel_cols = [c for c in panel.columns if c != PRICE_COL and 'target' not in c.lower()
and 'ewma' not in c.lower()]
stage1 = []
for col in panel_cols:
if col not in panel.columns:
continue
series = panel[col]
missing_rate = series.isna().mean()
n_valid = series.notna().sum()
if missing_rate < 0.30 and n_valid >= 120:
stage1.append({
'feature': col,
'missing_rate': round(missing_rate, 3),
'n_valid': int(n_valid),
'mean': round(float(series.mean()), 4) if series.notna().any() else None,
})
stage1_features = [s['feature'] for s in stage1]
print(f"Stage 1: 数据可用性 → {len(stage1_features)} 个 (缺失率<30%, 覆盖>120月)")
# ── Stage 2: Univariate correlation filter ──
ret = panel[PRICE_COL].pct_change(1)
stage2 = []
for feat in stage1_features:
try:
corr = float(panel[feat].corr(ret))
abs_corr = abs(corr)
if abs_corr > 0.03: # Relaxed threshold for monthly data
stage2.append({
'feature': feat,
'corr_with_return': round(corr, 4),
'abs_corr': round(abs_corr, 4),
})
except:
pass
stage2.sort(key=lambda x: x['abs_corr'], reverse=True)
stage2_features = [s['feature'] for s in stage2]
print(f"Stage 2: 单变量相关性 → {len(stage2_features)} 个 (|corr|>0.03)")
# ── Stage 3: Collinearity filter ──
# Remove highly correlated features (keep the one with higher abs_corr to return)
stage3_features = list(stage2_features)
corr_matrix = panel[stage3_features].corr()
to_drop = set()
corr_lookup = {s['feature']: s['abs_corr'] for s in stage2}
for i in range(len(stage3_features)):
if stage3_features[i] in to_drop:
continue
for j in range(i + 1, len(stage3_features)):
if stage3_features[j] in to_drop:
continue
pair_corr = abs(corr_matrix.iloc[i, j])
if pair_corr > 0.85:
f_i, f_j = stage3_features[i], stage3_features[j]
weaker = f_j if corr_lookup.get(f_i, 0) >= corr_lookup.get(f_j, 0) else f_i
to_drop.add(weaker)
stage3_features = [f for f in stage3_features if f not in to_drop]
stage3 = [s for s in stage2 if s['feature'] in stage3_features]
print(f"Stage 3: 共线性过滤 → {len(stage3_features)} 个 (pair |corr|<0.85)")
# ── Stage 4: MI score ──
from sklearn.feature_selection import mutual_info_regression
stage4 = []
X = panel[stage3_features].dropna()
y = panel.loc[X.index, PRICE_COL].pct_change(1).iloc[1:]
X = X.iloc[1:]
valid = y.notna() & X.notna().all(axis=1)
if valid.sum() > 50:
mi_scores = mutual_info_regression(X.loc[valid], y.loc[valid], random_state=42, n_neighbors=5)
for feat, mi_val in sorted(zip(stage3_features, mi_scores), key=lambda x: x[1], reverse=True):
stage4.append({
'feature': feat,
'mi_score': round(float(mi_val), 4),
'corr': round(float(panel[feat].corr(ret)), 4),
})
stage4_features = [s['feature'] for s in stage4]
print(f"Stage 4: MI 非线性筛选 → {len(stage4_features)} 个")
# ── Stage 5: Final selection (match with FEATURES list) ──
final_selected = [f for f in FEATURES if f in panel.columns]
final_rejected = [f for f in stage4_features if f not in final_selected][:10]
# Build factor assignment
stage5 = []
for feat in final_selected:
group = 'Other'
for g, members in FACTOR_GROUPS.items():
if feat in members:
group = g
break
mi_val = next((s['mi_score'] for s in stage4 if s['feature'] == feat), 0)
corr_val = next((s['corr'] for s in stage4 if s['feature'] == feat), 0)
stage5.append({
'feature': feat,
'factor_group': group,
'mi_score': mi_val,
'corr': corr_val,
})
print(f"Stage 5: 最终选择 → {len(final_selected)} 个 (经济学意义+因子分配)")
# ── Build funnel summary ──
funnel = {
'total_raw': total_raw,
'n_csv_files': len(csv_files),
'stages': [
{'stage': 0, 'name': '原始指标', 'count': total_raw, 'rule': f'{len(csv_files)}个CSV文件'},
{'stage': 1, 'name': '数据可用性', 'count': len(stage1_features), 'rule': '缺失率<30%, 覆盖>120月'},
{'stage': 2, 'name': '单变量相关性', 'count': len(stage2_features), 'rule': '|corr(feature, return)|>0.03'},
{'stage': 3, 'name': '共线性去重', 'count': len(stage3_features), 'rule': '组内pair |corr|<0.85'},
{'stage': 4, 'name': 'MI非线性筛选', 'count': len(stage4_features), 'rule': 'MI(feature; return)排序'},
{'stage': 5, 'name': '最终选择', 'count': len(final_selected), 'rule': '经济学意义+因子组分配'},
],
'final_features': stage5,
'rejected_examples': final_rejected,
'file_inventory': file_info[:15], # Top 15 files
}
return funnel
|