[{"content":"摘要 本文介绍了一套完整的高级特征工程方法论，包括自动化特征工程、时间序列技术、交叉特征策略、特征选择方法以及金融和风险控制领域的特定特征。我们提供了实用的代码框架，以帮助数据科学家和机器学习工程师在实际项目中应用这些高级技术。\n关键词 特征工程，自动化特征提取，时间序列，交叉特征，特征选择，金融风险控制\n1. 引言与理论基础 特征工程是机器学习成功的核心。尽管深度学习等先进技术在某些领域减少了对特征工程的需求，但对于表格型数据和结构化数据，高质量的特征工程仍然起着至关重要的作用。\n传统的特征工程方法依赖于领域知识和经验，而现代方法则结合了自动化工具、时间序列分析、复杂交互识别和优化的特征选择技术。\n1.1 特征工程的关键挑战 高维稀疏性：大量的潜在特征需要识别真正有用的部分 时间依赖性：时间序列数据中的模式演化 交互效应：高阶特征交互的发现和建模 领域适应性：不同业务场景下的特征构造策略 1.2 方法分类 我们将特征工程方法分为以下几类：\n自动化特征工程 时间序列特征处理 交叉特征生成与选择 特征选择技术 域特定特征工程 2. 自动化特征工程框架 2.1 FeatureTools 实践框架 FeatureTools 是一个开源的自动化特征工程技术库，支持深度特化功能组合（Deep Feature Synthesis）来创建新的特征集。\n1 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 import featuretools as ft import pandas as pd import numpy as np from featuretools.primitives import ( Count, Mean, Std, Max, Min, Sum, TimeSinceLast, TimeSinceFirst, NMostCommon ) class AutomatedFeatureToolsFramework: \u0026#34;\u0026#34;\u0026#34; 自动化特征工程框架 - 基于 FeatureTools \u0026#34;\u0026#34;\u0026#34; def __init__(self, target_entity=\u0026#39;main\u0026#39;): self.target_entity = target_entity self.es = None # entityset self.feature_matrix = None self.features = None def setup_entityset(self, data_dict): \u0026#34;\u0026#34;\u0026#34; 设置实体集结构 Args: data_dict: 包含数据框的字典 {entity_name: DataFrame} \u0026#34;\u0026#34;\u0026#34; es = ft.EntitySet(\u0026#34;automated_framework\u0026#34;) # 添加所有实体 for entity_name, df in data_dict.items(): es.add_dataframe( dataframe_name=entity_name, dataframe=df, index=df.columns[0] # 默认第一列作为索引 ) # 建立实体关系（根据实际需求调整） # 示例关系建立逻辑 entity_list = list(data_dict.keys()) for i, entity1 in enumerate(entity_list): for j in range(i+1, len(entity_list)): entity2 = entity_list[j] # 检查是否可以建立关系（通过共享列） shared_cols = set(data_dict[entity1].columns) \u0026amp; set(data_dict[entity2].columns) if shared_cols: # 这里需要根据实际数据结构调整关系定义 pass self.es = es return self def generate_features(self, target_entity, max_depth=2): \u0026#34;\u0026#34;\u0026#34; 生成特征矩阵和特征定义 \u0026#34;\u0026#34;\u0026#34; if self.es is None: raise ValueError(\u0026#34;EntitySet 未初始化，请先调用 setup_entityset\u0026#34;) # 定义要使用的基元 agg_primitives = [ \u0026#39;count\u0026#39;, \u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;, \u0026#39;std\u0026#39;, \u0026#39;min\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;n_most_common\u0026#39; # 取最常见的前N个值 ] trans_primitives = [ \u0026#39;absolute\u0026#39;, \u0026#39;negate\u0026#39;, \u0026#39;add_numeric\u0026#39;, \u0026#39;subtract_numeric\u0026#39;, \u0026#39;multiply_numeric\u0026#39;, \u0026#39;divide_numeric\u0026#39;, \u0026#39;modulo_numeric\u0026#39;, \u0026#39;and\u0026#39;, \u0026#39;or\u0026#39;, \u0026#39;equal\u0026#39;, \u0026#39;not_equal\u0026#39;, \u0026#39;less_than\u0026#39;, \u0026#39;greater_than\u0026#39;, \u0026#39;less_than_equal_to\u0026#39;, \u0026#39;greater_than_equal_to\u0026#39; ] # 深度特征合成 self.feature_matrix, self.features = ft.dfs( entityset=self.es, target_dataframe_name=target_entity, agg_primitives=agg_primitives, trans_primitives=trans_primitives, max_depth=max_depth, n_jobs=-1, # 并行化处理 verbose=True ) return self.feature_matrix, self.features def handle_relationships(self, relationships): \u0026#34;\u0026#34;\u0026#34; 手动添加关系 Args: relationships: 关系列表 [(parent_variable, child_variable)] \u0026#34;\u0026#34;\u0026#34; for parent_var, child_var in relationships: self.es.add_relationship(parent_var, child_var) return self 2.2 TSFresh 实践框架 Tsfresh 是专门用于时间序列特征提取的自动化工具，提供了超过700种时间序列特征计算方法。\n1 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 from tsfresh import extract_features, select_features from tsfresh.utilities.dataframe_functions import impute from tsfresh.feature_extraction import ComprehensiveFCParameters from sklearn.feature_selection import VarianceThreshold class AutomatedTSFreshFramework: \u0026#34;\u0026#34;\u0026#34; 自动时间序列特征工程框架 - 基于 TSFresh \u0026#34;\u0026#34;\u0026#34; def __init__(self): self.extract_settings = None self.selected_features = None def set_extraction_settings(self, kind_to_fc_parameters=None): \u0026#34;\u0026#34;\u0026#34; 设置提取参数 \u0026#34;\u0026#34;\u0026#34; if kind_to_fc_parameters is None: # 使用全面的特征参数 self.extract_settings = ComprehensiveFCParameters() else: self.extract_settings = kind_to_fc_parameters return self def extract_time_series_features(self, df, column_id, column_sort, column_kind=None, extra_fbursts_features=None, workers=1): \u0026#34;\u0026#34;\u0026#34; 提取时间序列特征 Args: df: 时间序列数据框 (必须包含 id, time, value 列) column_id: ID列名 column_sort: 时间排序列名 column_kind: 可选，多变量时间序列的类型列名 extra_fbursts_features: 额外的特征参数 workers: 并行处理线程数 \u0026#34;\u0026#34;\u0026#34; # 提取特征 extracted_features = extract_features( df, column_id=column_id, column_sort=column_sort, column_kind=column_kind, default_fc_parameters=self.extract_settings, n_jobs=workers ) # 处理缺失值 extracted_features = impute(extracted_features) return extracted_features def select_relevant_features(self, X, y, test_for_binary_target_cat_correlation=True, ml_task=\u0026#39;classification\u0026#39;): \u0026#34;\u0026#34;\u0026#34; 特征选择 Args: X: 特征矩阵，由tsfresh输出获得 y: 目标向量 test_for_binary_target_cat_correlation: 是否测试二分类目标 ml_task: 机器学习任务类型 (\u0026#39;classification\u0026#39; 或 \u0026#39;regression\u0026#39;) \u0026#34;\u0026#34;\u0026#34; # 特征选择 self.selected_features = select_features( X, y, test_for_binary_target_cat_correlation=test_for_binary_target_cat_correlation, ml_task=ml_task ) return self.selected_features def extract_advanced_timeseries_features(self, df, ids, columns): \u0026#34;\u0026#34;\u0026#34; 高级时间序列特征提取 Args: df: 包含时间序列数据的数据框 ids: 要提取特征的时间序列IDs列表 columns: 要分析的列 Returns: 包含高级特征的DataFrame \u0026#34;\u0026#34;\u0026#34; result_features = {} for col in columns: for i in ids: series = df[df.index == i][col].dropna() features = self._calculate_advanced_features(series) for feat_name, feat_value in features.items(): result_features[f\u0026#39;{col}_{i}_{feat_name}\u0026#39;] = feat_value return pd.DataFrame([result_features]) def _calculate_advanced_features(self, series): \u0026#34;\u0026#34;\u0026#34; 计算高级时间序列特征 \u0026#34;\u0026#34;\u0026#34; features = {} # 统计特征 features[\u0026#39;mean\u0026#39;] = series.mean() features[\u0026#39;std\u0026#39;] = series.std() features[\u0026#39;skewness\u0026#39;] = series.skew() features[\u0026#39;kurtosis\u0026#39;] = series.kurtosis() # 极值特征 features[\u0026#39;min\u0026#39;] = series.min() features[\u0026#39;max\u0026#39;] = series.max() features[\u0026#39;median\u0026#39;] = series.median() # 变化特性 if len(series) \u0026gt; 1: diff_series = series.diff().dropna() features[\u0026#39;diff_mean\u0026#39;] = diff_series.mean() features[\u0026#39;diff_std\u0026#39;] = diff_series.std() features[\u0026#39;change_percentage\u0026#39;] = (diff_series.abs() \u0026gt; 0).sum() / len(diff_series) # 趋势强度 x = np.arange(len(series)) if len(set(x)) \u0026gt; 1 and len(set(series.values)) \u0026gt; 1: trend_coeffs = np.polyfit(x, series.values, 1) features[\u0026#39;trend_slope\u0026#39;] = trend_coeffs[0] features[\u0026#39;trend_strength\u0026#39;] = abs(trend_coeffs[0]) / series.std() return features 2.3 AutoFeat 实践框架 AutoFeat 是一个结合符号回归和线性模型的自动化特征工程工具。\n1 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 try: from autofeat import AutoFeatRegressor, AutoFeatClassifier except ImportError: # 提供Mock实现或跳过 pass class AutomatedAutoFeatFramework: \u0026#34;\u0026#34;\u0026#34; 自动化特征工程框架 - 基于 AutoFeat \u0026#34;\u0026#34;\u0026#34; def __init__(self, task_type=\u0026#39;regression\u0026#39;, max_opts=50, n_bins=100): \u0026#34;\u0026#34;\u0026#34; Args: task_type: 任务类型 (\u0026#39;regression\u0026#39; 或 \u0026#39;classification\u0026#39;) max_opts: 最大函数组合操作数 n_bins: 计算特征重要性的分箱数 \u0026#34;\u0026#34;\u0026#34; self.task_type = task_type self.max_opts = max_opts self.n_bins = n_bins self.model = None self.feature_names = None def fit_and_transform(self, X, y): \u0026#34;\u0026#34;\u0026#34; 拟合AutoFeat模型并转换特征 \u0026#34;\u0026#34;\u0026#34; if self.task_type == \u0026#39;regression\u0026#39;: self.model = AutoFeatRegressor( max_opts=self.max_opts, n_bins=self.n_bins, verbose=1 ) else: self.model = AutoFeatClassifier( max_opts=self.max_opts, n_bins=self.n_bins, verbose=1 ) # 在这里我们需要模拟fit_transform的行为 # 因为AutoFeat通常只提供fit/predict分离的方法 X_transformed = self.model.fit(X, y).transform(X) if hasattr(self.model, \u0026#39;transform\u0026#39;) else X return X_transformed, self.model def select_best_features(self, af_model, X_original, threshold=0.01): \u0026#34;\u0026#34;\u0026#34; 基于AutoFeat的选择函数，选择最佳特征 Args: af_model: 已训练的AutoFeat模型 X_original: 原始特征 threshold: 特征重要性阈值 \u0026#34;\u0026#34;\u0026#34; # 如果存在特征重要性属性，则根据其进行筛选 if hasattr(af_model, \u0026#39;featureimps_\u0026#39;) and af_model.featureimps_ is not None: feature_mask = af_model.featureimps_ \u0026gt; threshold n_features_before = X_original.shape[1] X_selected = X_original[:, feature_mask] if isinstance(X_original, np.ndarray) else X_original.loc[:, feature_mask] n_features_after = X_selected.shape[1] print(f\u0026#34;特征数量从 {n_features_before} 减少到 {n_features_after}\u0026#34;) return X_selected else: print(\u0026#34;模型不支持特征重要性评估\u0026#34;) return X_original 3. 时间序列特征工程框架 3.1 滞后特征与滚动统计 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 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 import pandas as pd import numpy as np from typing import Dict, List, Union from sklearn.preprocessing import StandardScaler, MinMaxScaler import warnings warnings.filterwarnings(\u0026#39;ignore\u0026#39;) class TimeSeriesFeatureEngineering: \u0026#34;\u0026#34;\u0026#34; 时间序列特征工程框架 \u0026#34;\u0026#34;\u0026#34; def __init__(self): self.scalers = {} self.lag_features = [] self.rolling_features = [] def create_lag_features(self, df: pd.DataFrame, value_column: str, group_column: str = None, lags: List[int] = [1, 2, 3, 5, 7, 14, 30]): \u0026#34;\u0026#34;\u0026#34; 创建滞后特征 Args: df: 输入数据框 value_column: 值列名 group_column: 分组列名（如果有的话） lags: 滞后期数列表 \u0026#34;\u0026#34;\u0026#34; lag_df = df.copy() if group_column: # 按组计算滞后（适用于面板数据） for lag in lags: lag_df[f\u0026#39;{value_column}_lag_{lag}\u0026#39;] = lag_df.groupby(group_column)[value_column].shift(lag) else: # 全局滞后 for lag in lags: lag_df[f\u0026#39;{value_column}_lag_{lag}\u0026#39;] = lag_df[value_column].shift(lag) self.lag_features = [f\u0026#39;{value_column}_lag_{lag}\u0026#39; for lag in lags] return lag_df def create_rolling_features(self, df: pd.DataFrame, value_column: str, group_column: str = None, windows: List[int] = [3, 7, 14, 30], functions: List[str] = [\u0026#39;mean\u0026#39;, \u0026#39;std\u0026#39;, \u0026#39;min\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;sum\u0026#39;]): \u0026#34;\u0026#34;\u0026#34; 创建滚动窗口特征 Args: df: 输入数据框 value_column: 值列名 group_column: 分组列名 windows: 窗口大小列表 functions: 聚合函数列表 \u0026#34;\u0026#34;\u0026#34; roll_df = df.copy() if group_column: # 按组进行滚动计算 grouped = df.groupby(group_column) else: # 整体滚动计算 grouped = df.groupby(lambda x: \u0026#39;\u0026#39;) for window in windows: for func in functions: if group_column: roll_feature = grouped[value_column].rolling(window=window).agg(func).reset_index(level=0, drop=True) roll_df[f\u0026#39;{value_column}_roll_{window}_{func}\u0026#39;] = roll_feature else: roll_feature = df[value_column].rolling(window=window).agg(func) roll_df[f\u0026#39;{value_column}_roll_{window}_{func}\u0026#39;] = roll_feature self.rolling_features.append(f\u0026#39;{value_column}_roll_{window}_{func}\u0026#39;) # 为了保持索引一致，将groupby的结果重新索引 if group_column and not roll_df.index.equals(df.index): roll_df = roll_df.reindex(df.index) return roll_df def create_expanding_features(self, df: pd.DataFrame, value_column: str, group_column: str = None): \u0026#34;\u0026#34;\u0026#34; 创建扩展窗口特征（从开始到当前时间点） \u0026#34;\u0026#34;\u0026#34; expand_df = df.copy() if group_column: expanding_mean = df.groupby(group_column)[value_column].expanding().mean().reset_index(level=0, drop=True) expanding_std = df.groupby(group_column)[value_column].expanding().std().reset_index(level=0, drop=True) expanding_min = df.groupby(group_column)[value_column].expanding().min().reset_index(level=0, drop=True) expanding_max = df.groupby(group_column)[value_column].expanding().max().reset_index(level=0, drop=True) else: expanding_mean = df[value_column].expanding().mean() expanding_std = df[value_column].expanding().std() expanding_min = df[value_column].expanding().min() expanding_max = df[value_column].expanding().max() expand_df[f\u0026#39;{value_column}_expand_mean\u0026#39;] = expanding_mean expand_df[f\u0026#39;{value_column}_expand_std\u0026#39;] = expanding_std expand_df[f\u0026#39;{value_column}_expand_min\u0026#39;] = expanding_min expand_df[f\u0026#39;{value_column}_expand_max\u0026#39;] = expanding_max return expand_df def create_cyclical_encoding(self, dates: Union[pd.Series, pd.DatetimeIndex], encode_cos: bool = True): \u0026#34;\u0026#34;\u0026#34; 创建周期性编码（如月份、日、小时等的sin/cos编码） \u0026#34;\u0026#34;\u0026#34; # 将输入转换为日期时间类型 if isinstance(dates, pd.Series): dt = pd.to_datetime(dates) else: dt = dates # 获取时间特性进行周期编码 features = {} # 天内小时的循环编码 hours_in_day = 24 features[\u0026#39;hour_sin\u0026#39;] = np.sin(2 * np.pi * dt.hour / hours_in_day) features[\u0026#39;hour_cos\u0026#39;] = np.cos(2 * np.pi * dt.hour / hours_in_day) # 每天（月的天数）的循环编码 days_in_month = 31 # 最大28-31，这里使用最大值 features[\u0026#39;day_sin\u0026#39;] = np.sin(2 * np.pi * dt.day / days_in_month) features[\u0026#39;day_cos\u0026#39;] = np.cos(2 * np.pi * dt.day / days_in_month) # 月的循环编码 months_in_year = 12 features[\u0026#39;month_sin\u0026#39;] = np.sin(2 * np.pi * dt.month / months_in_year) features[\u0026#39;month_cos\u0026#39;] = np.cos(2 * np.pi * dt.month / months_in_year) # 年内的天数循环编码 day_of_year = dt.dt.dayofyear days_in_year = 365 # 平年，闰年需特殊处理 features[\u0026#39;dayofyear_sin\u0026#39;] = np.sin(2 * np.pi * day_of_year / days_in_year) features[\u0026#39;dayofyear_cos\u0026#39;] = np.cos(2 * np.pi * day_of_year / days_in_year) # 如果需要cosine编码，则也返回它们 if not encode_cos: # 只保留每个维度的一个部分（如正弦） for key in [\u0026#39;hour\u0026#39;, \u0026#39;day\u0026#39;, \u0026#39;month\u0026#39;, \u0026#39;dayofyear\u0026#39;]: del features[key + \u0026#39;_cos\u0026#39;] # 构建DataFrame result_df = pd.DataFrame(features) return result_df def create_time_based_features(self, df: pd.DataFrame, datetime_column: str): \u0026#34;\u0026#34;\u0026#34; 从datetime列创建时间相关的特征 \u0026#34;\u0026#34;\u0026#34; df = df.copy() df[datetime_column] = pd.to_datetime(df[datetime_column]) # 一周的天数 df[\u0026#39;weekday\u0026#39;] = df[datetime_column].dt.weekday df[\u0026#39;is_weekend\u0026#39;] = (df[datetime_column].dt.weekday \u0026gt;= 5).astype(int) # 一月的天数和季度 df[\u0026#39;day_of_month\u0026#39;] = df[datetime_column].dt.day df[\u0026#39;quarter\u0026#39;] = df[datetime_column].dt.quarter # 周数和工作周标志 df[\u0026#39;week_of_year\u0026#39;] = df[datetime_column].dt.isocalendar().week.astype(int) df[\u0026#39;is_month_start\u0026#39;] = df[datetime_column].dt.is_month_start.astype(int) df[\u0026#39;is_month_end\u0026#39;] = df[datetime_column].dt.is_month_end.astype(int) return df def create_change_features(self, df: pd.DataFrame, value_column: str): \u0026#34;\u0026#34;\u0026#34; 创建变化相关特征 \u0026#34;\u0026#34;\u0026#34; df = df.copy() # 前后差分变化 df[f\u0026#39;{value_column}_diff_1\u0026#39;] = df[value_column].diff() df[f\u0026#39;{value_column}_pct_change_1\u0026#39;] = df[value_column].pct_change() # 移动平均的变化率 df[f\u0026#39;{value_column}_ma_3\u0026#39;] = df[value_column].rolling(window=3).mean() df[f\u0026#39;{value_column}_ma_7\u0026#39;] = df[value_column].rolling(window=7).mean() df[f\u0026#39;{value_column}_vs_ma_7\u0026#39;] = df[value_column] / df[f\u0026#39;{value_column}_ma_7\u0026#39;] return df def create_volatility_features(self, df: pd.DataFrame, value_column: str, periods: List[int] = [5, 10, 20]): \u0026#34;\u0026#34;\u0026#34; 创建波动率特征 \u0026#34;\u0026#34;\u0026#34; df = df.copy() for period in periods: # 计算百分比回报 returns = df[value_column].pct_change() # 滚动波动率 volatility = returns.rolling(window=period).std() df[f\u0026#39;{value_column}_volatility_{period}\u0026#39;] = volatility return df def scale_features(self, df: pd.DataFrame, columns: List[str], method: str = \u0026#39;standard\u0026#39;): \u0026#34;\u0026#34;\u0026#34; 标准化特征 Args: df: 数据框 columns: 需要标准化的列名列表 method: 缩放方法 (\u0026#39;standard\u0026#39;, \u0026#39;min-max\u0026#39;) \u0026#34;\u0026#34;\u0026#34; scaler_map = { \u0026#39;standard\u0026#39;: StandardScaler(), \u0026#39;min-max\u0026#39;: MinMaxScaler() } if method not in scaler_map: raise ValueError(f\u0026#34;不支持的缩放方法: {method}. 支持的方法: {list(scaler_map.keys())}\u0026#34;) df_scaled = df.copy() for col in columns: if col not in self.scalers: self.scalers[col] = scaler_map[method] scaled_value = self.scalers[col].fit_transform(df[[col]]) else: scaled_value = self.scalers[col].transform(df[[col]]) df_scaled[f\u0026#39;{col}_scaled\u0026#39;] = scaled_value return df_scaled 3.2 循环编码详解 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 def demonstrate_cyclical_encoding(): \u0026#34;\u0026#34;\u0026#34; 演示循环编码的工作原理 \u0026#34;\u0026#34;\u0026#34; import matplotlib.pyplot as plt # 创建一年的数据 dates = pd.date_range(start=\u0026#39;2023-01-01\u0026#39;, end=\u0026#39;2023-12-31\u0026#39;, freq=\u0026#39;D\u0026#39;) ts_fe = TimeSeriesFeatureEngineering() # 创建循环特征 cyclical_features = ts_fe.create_cyclical_encoding(dates) # 可视化 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 4)) # 月的编码可视化 ax1.plot(month_sin := cyclical_features[\u0026#39;month_sin\u0026#39;], label=\u0026#39;Month Sin\u0026#39;) ax1.plot(month_cos := cyclical_features[\u0026#39;month_cos\u0026#39;], label=\u0026#39;Month Cos\u0026#39;) ax1.set_title(\u0026#39;Cyclical Encoding: Month\u0026#39;) ax1.legend() # 天的编码可视化 ax2.plot(cyclical_features[\u0026#39;day_sin\u0026#39;], label=\u0026#39;Day Sin\u0026#39;) ax2.plot(cyclical_features[\u0026#39;day_cos\u0026#39;], label=\u0026#39;Day Cos\u0026#39;) ax2.set_title(\u0026#39;Cyclical Encoding: Day of Month\u0026#39;) ax2.legend() plt.tight_layout() plt.show() return cyclical_features 4. 交叉特征工程策略 4.1 低阶到高阶交叉特征生成 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 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 from itertools import combinations, combinations_with_replacement from sklearn.preprocessing import PolynomialFeatures from sklearn.base import BaseEstimator, TransformerMixin import pandas as pd import numpy as np class CrossFeatureEngineering: \u0026#34;\u0026#34;\u0026#34; 交叉特征工程框架 \u0026#34;\u0026#34;\u0026#34; def __init__(self): self.interaction_terms = [] self.candidate_pairs = [] def create_polynomial_interactions(self, X: pd.DataFrame, degree: int = 2, interaction_only: bool = True): \u0026#34;\u0026#34;\u0026#34; 创建多项式交叉特征 Args: X: 输入特征矩阵 degree: 多项式的度数 interaction_only: 是否仅创建交互项（排除平方项） \u0026#34;\u0026#34;\u0026#34; poly = PolynomialFeatures( degree=degree, interaction_only=interaction_only, include_bias=False ) X_poly = poly.fit_transform(X.select_dtypes(include=[np.number])) # 获取特征名 feature_names = [f\u0026#39;interaction_{i}\u0026#39; for i in range(X_poly.shape[1])] X_poly_df = pd.DataFrame(X_poly, columns=feature_names[2:]) # 排除常数项（第一个），因为PolynomialFeatures总是包含它（即使include_bias=False，也会有原始特征） X_poly_df.columns = list(X.columns) + [ f\u0026#39;poly_x{i}\u0026#39; for i in range(degree, X_poly.shape[1])] return X_poly, feature_names, poly def create_manual_interactions(self, df: pd.DataFrame, numeric_columns: List[str], operators: List[str] = [\u0026#39;*\u0026#39;, \u0026#39;/\u0026#39;, \u0026#39;+\u0026#39;, \u0026#39;-\u0026#39;]): \u0026#34;\u0026#34;\u0026#34; 创建手动指定的交叉特征 Args: df: 输入数据框 numeric_columns: 数值列名列表 operators: 运算符列表 \u0026#34;\u0026#34;\u0026#34; df_manual = df.copy() for i, col1 in enumerate(numeric_columns): for j, col2 in enumerate(numeric_columns): if i \u0026gt;= j: # 避免重复和自乘 continue for op in operators: new_col = f\u0026#39;{col1}_{op}_{col2}\u0026#39; if op == \u0026#39;*\u0026#39;: df_manual[new_col] = df_manual[col1] * df_manual[col2] elif op == \u0026#39;/\u0026#39;: # 防止除以0 df_manual[new_col] = df_manual[col1] / (df_manual[col2] + 1e-8) elif op == \u0026#39;+\u0026#39;: df_manual[new_col] = df_manual[col1] + df_manual[col2] elif op == \u0026#39;-\u0026#39;: df_manual[new_col] = df_manual[col1] - df_manual[col2] self.interaction_terms.append(new_col) return df_manual def create_ratios(self, df: pd.DataFrame, numerator_cols: List[str], denominator_cols: List[str], safe_division: bool = True): \u0026#34;\u0026#34;\u0026#34; 创建比率特征 Args: df: 输入数据框 numerator_cols: 分子列名列表 denominator_cols: 分母列名列表 safe_division: 是否安全除法（避免除零） \u0026#34;\u0026#34;\u0026#34; df_ratios = df.copy() for num_col in numerator_cols: for den_col in denominator_cols: if num_col != den_col: new_col = f\u0026#39;{num_col}_over_{den_col}\u0026#39; if safe_division: df_ratios[new_col] = df_ratios[num_col] / (df_ratios[den_col] + 1e-8) else: df_ratios[new_col] = df_ratios[num_col] / df_ratios[den_col] self.interaction_terms.append(new_col) return df_ratios def create_threshold_features(self, df: pd.DataFrame, columns: List[str], thresholds: List[float] = [0.1, 0.25, 0.5, 0.75]): \u0026#34;\u0026#34;\u0026#34; 基于给定阈值创建二进制特征 Args: df: 输入数据框 columns: 列名列表 thresholds: 阈值列表 \u0026#34;\u0026#34;\u0026#34; df_thresh = df.copy() for col in columns: for thr in thresholds: new_col = f\u0026#39;{col}_above_{thr}\u0026#39; df_thresh[new_col] = (df_thresh[col] \u0026gt; thr).astype(int) self.interaction_terms.append(new_col) return df_thresh def detect_potential_interactions(self, df: pd.DataFrame, target_col: str, method: str = \u0026#39;correlation\u0026#39;, top_n: int = 20): \u0026#34;\u0026#34;\u0026#34; 检测潜在的交叉特征对 Args: df: 输入数据框 target_col: 目标列 method: 检测方法 (\u0026#39;correlation\u0026#39;, \u0026#39;mutual_info\u0026#39;) top_n: 返回前n个最可能的配对 \u0026#34;\u0026#34;\u0026#34; if method == \u0026#39;correlation\u0026#39;: correlations = df.corr()[target_col].abs().sort_values(ascending=False) return correlations.head(top_n) else: # Mutual Information或其他 from sklearn.feature_selection import mutual_info_regression X = df.drop(columns=[target_col]).select_dtypes(include=[np.number]) y = df[target_col] mi_scores = mutual_info_regression(X, y) mi_result = pd.Series(mi_scores, index=X.columns).sort_values(ascending=False) return mi_result.head(top_n) def high_order_interactions_selector(self, df: pd.DataFrame, interaction_candidates: List[str], target: pd.Series, top_k: int = 50, selection_method: str = \u0026#39;mi\u0026#39;): \u0026#34;\u0026#34;\u0026#34; 高阶交叉特征选择器 Args: df: 数据框 interaction_candidates: 候选交叉特征列表 target: 目标系列 top_k: 选择的特征数量 selection_method: 选择方法 (\u0026#39;mi\u0026#39;, \u0026#39;anova\u0026#39;, \u0026#39;rf\u0026#39;) \u0026#34;\u0026#34;\u0026#34; X = df[interaction_candidates] X_cleaned = X.fillna(X.mean()) # 清理缺失值 from sklearn.feature_selection import mutual_info_regression, f_regression from sklearn.ensemble import RandomForestRegressor if selection_method == \u0026#39;mi\u0026#39;: scores = mutual_info_regression(X_cleaned, target) elif selection_method == \u0026#39;anova\u0026#39;: scores, _ = f_regression(X_cleaned, target) elif selection_method == \u0026#39;rf\u0026#39;: rf = RandomForestRegressor(n_estimators=100, random_state=42) rf.fit(X_cleaned, target) scores = rf.feature_importances_ else: raise ValueError(f\u0026#34;不支持的选择方法: {selection_method}\u0026#34;) # 获取得分最高的k个特征 sorted_idx = np.argsort(scores)[::-1] top_idx = sorted_idx[:top_k] selected_features = [interaction_candidates[i] for i in top_idx] return selected_features, scores[top_idx] def create_logic_interactions(self, df: pd.DataFrame, bool_features: List[str], operators: List[str] = [\u0026#39;\u0026amp;\u0026#39;, \u0026#39;|\u0026#39;, \u0026#39;^\u0026#39;]): \u0026#34;\u0026#34;\u0026#34; 创建布尔逻辑操作交互 Args: df: 输入数据框 bool_features: 布尔特征列表 operators: 逻辑运算符 (\u0026amp;, |, ^) \u0026#34;\u0026#34;\u0026#34; df_logic = df.copy() for i, col1 in enumerate(bool_features): for j, col2 in enumerate(bool_features): # 跳过自身比较 if i \u0026gt;= j: continue for op in operators: new_col = f\u0026#39;{col1}_{op}_{col2}\u0026#39; if op == \u0026#39;\u0026amp;\u0026#39;: df_logic[new_col] = df_logic[col1] \u0026amp; df_logic[col2] elif op == \u0026#39;|\u0026#39;: df_logic[new_col] = df_logic[col1] | df_logic[col2] elif op == \u0026#39;^\u0026#39;: df_logic[new_col] = df_logic[col1] ^ df_logic[col2] df_logic[new_col] = df_logic[new_col].astype(int) self.interaction_terms.append(new_col) return df_logic 4.2 陷阱检测与质量验证 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 class InteractionValidator: \u0026#34;\u0026#34;\u0026#34; 交叉特征质量与验证工具 \u0026#34;\u0026#34;\u0026#34; def __init__(self): pass def detect_duplicate_features(self, df: pd.DataFrame, tol: float = 1e-8): \u0026#34;\u0026#34;\u0026#34; 检测重复特征 \u0026#34;\u0026#34;\u0026#34; duplicates = [] cols = df.select_dtypes(include=[np.number]).columns for i in range(len(cols)): for j in range(i+1, len(cols)): if abs(df[cols[i]] - df[cols[j]]).mean() \u0026lt; tol: duplicates.append((cols[i], cols[j])) return duplicates def detect_multicollinearity(self, df: pd.DataFrame, threshold: float = 0.9): \u0026#34;\u0026#34;\u0026#34; 检测多重共线性 \u0026#34;\u0026#34;\u0026#34; corr_matrix = df.select_dtypes(include=[np.number]).corr().abs() high_corr_pairs = [] for i in range(len(corr_matrix.columns)): for j in range(i+1, len(corr_matrix.columns)): if corr_matrix.iloc[i, j] \u0026gt; threshold: high_corr_pairs.append(( corr_matrix.columns[i], corr_matrix.columns[j], corr_matrix.iloc[i, j] )) return high_corr_pairs def evaluate_feature_quality(self, X: pd.DataFrame, y: pd.Series, selected_features: List[str] = None): \u0026#34;\u0026#34;\u0026#34; 评估特征质量 \u0026#34;\u0026#34;\u0026#34; if selected_features: X_use = X[selected_features].copy() else: X_use = X.copy() from sklearn.feature_selection import mutual_info_regression from sklearn.metrics import mean_squared_error # 特征统计信息 results = { \u0026#39;std_zero\u0026#39;: [], \u0026#39;corr_with_target_high\u0026#39;: [], \u0026#39;unique_values_low\u0026#39;: [] # 唯一值很少的特征 } for col in X_use.columns: if X_use[col].std() \u0026lt; 1e-8: results[\u0026#39;std_zero\u0026#39;].append(col) if len(X_use[col].unique()) \u0026lt;= 2: results[\u0026#39;unique_values_low\u0026#39;].append(col) # 目标相关性 mi_scores = mutual_info_regression(X_use, y) target_corr_pairs = X_use.corrwith(y).abs() results[\u0026#39;corr_with_target_high\u0026#39;] = target_corr_pairs[target_corr_pairs \u0026gt; 0.9].index.tolist() return results 5. 特征选择技术框架 5.1 置换重要性 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 from sklearn.metrics import accuracy_score, roc_auc_score, mean_squared_error from sklearn.model_selection import cross_val_score import numpy as np from tqdm import tqdm import warnings warnings.filterwarnings(\u0026#39;ignore\u0026#39;) class PermutationImportanceSelector: \u0026#34;\u0026#34;\u0026#34; 基于置换重要性的特征选择器 \u0026#34;\u0026#34;\u0026#34; def __init__(self, model, X_test, y_test, scoring_func=None, n_repeats=10): \u0026#34;\u0026#34;\u0026#34; Args: model: 训练好的模型 X_test: 测试特征矩阵 y_test: 测试标签 scoring_func: 评分函数，如果为None则使用默认的准确率 n_repeats: 重复次数（用于获取重要性的置信区间） \u0026#34;\u0026#34;\u0026#34; self.model = model self.X_test = X_test.copy() self.y_test = y_test self.scoring_func = scoring_func self.n_repeats = n_repeats self.baseline_score = None self.importances = None def calculate_permutation_importance(self): \u0026#34;\u0026#34;\u0026#34; 计算置换重要性 \u0026#34;\u0026#34;\u0026#34; if self.scoring_func is None: # 根据目标变量判断是分类还是回归 if len(np.unique(self.y_test)) \u0026lt;= 20 and self.y_test.dtype in [int, \u0026#39;int64\u0026#39;, \u0026#39;int32\u0026#39;]: # 假设整数标签是分类问题 self.scoring_func = accuracy_score if len(np.unique(self.y_test)) \u0026lt;= 2 else \\ (lambda y_true, y_pred: roc_auc_score(y_true, y_pred, multi_class=\u0026#39;ovr\u0026#39;)) else: # 回归问题 self.scoring_func = mean_squared_error # 获取基准分数（原性能） y_pred = self.model.predict(self.X_test) if callable(self.scoring_func): self.baseline_score = self.scoring_func(self.y_test, y_pred) else: self.baseline_score = self.model.score(self.X_test, self.y_test) importances = [] for col in self.X_test.columns: col_scores = [] for _ in range(self.n_repeats): # 复制测试数据 X_permuted = self.X_test.copy() # 打乱该列 X_permuted[col] = np.random.permutation(X_permuted[col]) # 获取预测并评估 y_pred_permuted = self.model.predict(X_permuted) if callable(self.scoring_func): permuted_score = self.scoring_func(self.y_test, y_pred_permuted) else: permuted_score = self.model.score(X_permuted, self.y_test) # 重要性 = (baseline - permuted_score)，越大表明该特征越重要 col_scores.append(self.baseline_score - permuted_score) importances.append({ \u0026#39;feature\u0026#39;: col, \u0026#39;importance_mean\u0026#39;: np.mean(col_scores), \u0026#39;importance_std\u0026#39;: np.std(col_scores), \u0026#39;importance_scores\u0026#39;: col_scores }) self.importances = pd.DataFrame(importances).sort_values(\u0026#39;importance_mean\u0026#39;, axis=0, ascending=False).reset_index(drop=True) return self.importances def select_features_by_importance(self, threshold=None, n_features=None): \u0026#34;\u0026#34;\u0026#34; 根据重要性选择特征 Args: threshold: 重要性阈值 n_features: 选择的特征数量 \u0026#34;\u0026#34;\u0026#34; if self.importances is None: self.calculate_permutation_importance() if threshold is not None: selected_features = self.importances[ self.importances[\u0026#39;importance_mean\u0026#39;] \u0026gt;= threshold ][\u0026#39;feature\u0026#39;].tolist() elif n_features is not None: selected_features = self.importances.head(n_features)[\u0026#39;feature\u0026#39;].tolist() else: # 默认情况下选择重要性大于0的特征 selected_features = self.importances[ self.importances[\u0026#39;importance_mean\u0026#39;] \u0026gt; 0 ][\u0026#39;feature\u0026#39;].tolist() return selected_features 5.2 SHAP 值重要性 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 import shap import pandas as pd import numpy as np from sklearn.model_selection import train_test_split class SHAPFeatureSelector: \u0026#34;\u0026#34;\u0026#34; 基于SHAP值的特征选择器 \u0026#34;\u0026#34;\u0026#34; def __init__(self, model, X_train, feature_names=None): \u0026#34;\u0026#34;\u0026#34; Args: model: 训练好的模型 X_train: 训练数据（用于构建背景数据） feature_names: 特征名称 \u0026#34;\u0026#34;\u0026#34; self.model = model self.X_train = X_train if feature_names is None: if hasattr(X_train, \u0026#39;columns\u0026#39;): self.feature_names = X_train.columns.tolist() else: self.feature_names = [f\u0026#39;feature_{i}\u0026#39; for i in range(X_train.shape[1])] else: self.feature_names = feature_names self.explainer = None self.shap_values = None self.feature_importances = None self.summary_plot_ready = False def calculate_shap_values(self, X_explain=None, method=\u0026#39;auto\u0026#39;): \u0026#34;\u0026#34;\u0026#34; 计算SHAP值 Args: X_explain: 需要解释的数据，如果不提供则使用部分训练数据 method: 解释方法 (\u0026#39;auto\u0026#39;, \u0026#39;permutation\u0026#39;, \u0026#39;tree\u0026#39;) \u0026#34;\u0026#34;\u0026#34; if X_explain is None: # 如果没有提供解释数据，默认使用训练数据的一小部分 if len(self.X_train) \u0026gt; 100: X_explain = self.X_train.sample(100) else: X_explain = self.X_train # 根据模型类型选择合适的explainer try: if method == \u0026#39;tree\u0026#39; or hasattr(self.model, \u0026#39;tree_\u0026#39;) or \u0026#39;lightgbm\u0026#39; in str(type(self.model)): self.explainer = shap.TreeExplainer(self.model) elif method == \u0026#39;permutation\u0026#39;: self.explainer = shap.PermutationExplainer(self.model.predict, X_explain) else: # auto/default self.explainer = shap.Explainer(self.model.predict, X_explain) self.shap_values = self.explainer(X_explain) # 计算特征重要性（平均SHAP绝对值） self.feature_importances = np.mean(np.abs(self.shap_values.values), axis=0) feature_importance_df = pd.DataFrame({ \u0026#39;feature\u0026#39;: self.feature_names, \u0026#39;shap_importance\u0026#39;: self.feature_importances }).sort_values(\u0026#39;shap_importance\u0026#39;, ascending=False).reset_index(drop=True) self.summary_plot_ready = True return feature_importance_df, self.shap_values except Exception as e: print(f\u0026#34;SHAP计算出错: {e}\u0026#34;) print(\u0026#34;可能是模型不兼容或缺少shap包，请检查安装和模型类型\u0026#34;) return None def select_features_by_shap(self, threshold=None, n_features=None): \u0026#34;\u0026#34;\u0026#34; 根据SHAP值选择特征 Args: threshold: SHAP阈值 n_features: 选择的特征数量 \u0026#34;\u0026#34;\u0026#34; if self.feature_importances is None: feature_importance_df, _ = self.calculate_shap_values() if feature_importance_df is None: return [] # 如果SHAP失败，返回空列表 else: feature_importance_df = pd.DataFrame({ \u0026#39;feature\u0026#39;: self.feature_names, \u0026#39;shap_importance\u0026#39;: self.feature_importances }).sort_values(\u0026#39;shap_importance\u0026#39;, ascending=False).reset_index(drop=True) if threshold is not None: selected_features = feature_importance_df[ feature_importance_df[\u0026#39;shap_importance\u0026#39;] \u0026gt;= threshold ][\u0026#39;feature\u0026#39;].tolist() elif n_features is not None: selected_features = feature_importance_df.head(n_features)[\u0026#39;feature\u0026#39;].tolist() else: # 默认情况：选择前20个特征或大于最小值的特征 selected_features = feature_importance_df.head(20)[\u0026#39;feature\u0026#39;].tolist() return selected_features def powershap_selection(self, X_train, y_train, alpha=0.01, power=2): \u0026#34;\u0026#34;\u0026#34; PowerShap方法 - 结合SHAP和统计检验的混合特征选择方法 \u0026#34;\u0026#34;\u0026#34; try: from powershap import PowerShap selector = PowerShap( model=self.model, alpha=alpha, power=power, automatic=True ) X_selected = selector.fit_transform(X_train, y_train) selected_features = X_train.columns[selector.support_].tolist() return selected_features, selector except ImportError: print(\u0026#34;PowerShap未安装，跳过此功能\u0026#34;) return [], None 5.3 遗传算法特征选择 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 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 from geneticalgorithm import geneticalgorithm as ga import numpy as np from sklearn.model_selection import cross_val_score from sklearn.pipeline import Pipeline from sklearn.base import BaseEstimator, TransformerMixin class GeneticFeatureSelector: \u0026#34;\u0026#34;\u0026#34; 基于遗传算法的特征选择器 \u0026#34;\u0026#34;\u0026#34; def __init__(self, estimator, X, y, cv=5, scoring=\u0026#39;accuracy\u0026#39;, n_population=50, n_generations=100, crossover_probability=0.5, mutation_probability=0.2): \u0026#34;\u0026#34;\u0026#34; Args: estimator: 指标估计器 X: 输入特征 y: 目标变量 cv: 交叉验证折数 scoring: 评分指标 n_population: 种群大小 n_generations: 代数 crossover_probability: 交叉概率 mutation_probability: 变异概率 \u0026#34;\u0026#34;\u0026#34; self.estimator = estimator self.X = X self.y = y self.cv = cv self.scoring = scoring self.n_population = n_population self.n_generations = n_generations self.crossover_probability = crossover_probability self.mutation_probability = mutation_probability self.selected_features = [] self.feature_support = None self.best_fitness = None def fitness_function(self, solution): \u0026#34;\u0026#34;\u0026#34; 适应度函数 - 使用交叉验证分数作为适应度 \u0026#34;\u0026#34;\u0026#34; # 将二进制解转换为特征掩码 selected_indices = np.where(solution == 1)[0] if len(selected_indices) == 0: # 如果没有选择任何特征，返回最小值 return 0.001 X_subset = self.X[:, selected_indices].copy() if isinstance(self.X, np.ndarray) else self.X.iloc[:, selected_indices] try: cv_scores = cross_val_score( self.estimator, X_subset, self.y, cv=self.cv, scoring=self.scoring ) fitness = cv_scores.mean() except Exception as e: print(f\u0026#34;计算交叉验证分数错误: {e}\u0026#34;) fitness = 0.001 # 错误情况下给予惩罚分数 return fitness def optimize(self): \u0026#34;\u0026#34;\u0026#34; 运行遗传算法优化 \u0026#34;\u0026#34;\u0026#34; dim = self.X.shape[1] # 遗传算法参数 algorithm_param = { \u0026#39;max_num_iteration\u0026#39;: self.n_generations, \u0026#39;population_size\u0026#39;: self.n_population, \u0026#39;mutation_probability\u0026#39;: self.mutation_probability, \u0026#39;elit_ratio\u0026#39;: 0.05, \u0026#39;crossover_probability\u0026#39;: self.crossover_probability, \u0026#39;parents_portion\u0026#39;: 0.2, \u0026#39;crossover_type\u0026#39;: \u0026#39;single_point\u0026#39;, \u0026#39;max_iteration_without_improv\u0026#39;: 20 } model = ga( function=self.fitness_function, dimension=dim, variable_type=\u0026#39;bool\u0026#39;, # 二进制变量 algorithm_parameters=algorithm_param ) model.run() self.feature_support = model.output_dict[\u0026#39;variable\u0026#39;].astype(bool) self.best_fitness = model.output_dict[\u0026#39;function\u0026#39;] self.selected_features = [i for i, is_selected in enumerate(self.feature_support) if is_selected] print(f\u0026#34;最优适应度: {self.best_fitness}\u0026#34;) print(f\u0026#34;选中特征数量: {len(self.selected_features)} / {dim}\u0026#34;) return self.selected_features, self.feature_support, self.best_fitness def transform(self, X): \u0026#34;\u0026#34;\u0026#34; 应用特征选择变换到新数据 \u0026#34;\u0026#34;\u0026#34; if self.feature_support is None: raise ValueError(\u0026#34;未运行优化，请先调用optimize函数\u0026#34;) selected_indices = np.where(self.feature_support)[0] return X[:, selected_indices] if isinstance(X, np.ndarray) else X.iloc[:, selected_indices] class FeatureSelectorHybrid: \u0026#34;\u0026#34;\u0026#34; 混合特征选择方法 - 结合多种技术和验证方法 \u0026#34;\u0026#34;\u0026#34; def __init__(self, primary_method=\u0026#39;permutation\u0026#39;, secondary_methods=None): \u0026#34;\u0026#34;\u0026#34; Args: primary_method: 主特征选择方法 secondary_methods: 次要验证方法列表 \u0026#34;\u0026#34;\u0026#34; self.primary_method = primary_method self.secondary_methods = secondary_methods or [\u0026#39;variance_threshold\u0026#39;, \u0026#39;correlation\u0026#39;] self.selected_by_primary = [] self.selected_final = [] def fit_selection(self, X, y, model=None): \u0026#34;\u0026#34;\u0026#34; 同步执行多重选择方法 \u0026#34;\u0026#34;\u0026#34; method_outputs = {} # 1. 方差阈值 from sklearn.feature_selection import VarianceThreshold var_selector = VarianceThreshold(threshold=0.01) X_var_filtered = var_selector.fit_transform(X) method_outputs[\u0026#39;variance_threshold\u0026#39;] = var_selector.get_support(indices=True) # 2. 相关性过滤 from sklearn.feature_selection import SelectKBest, f_classif if len(np.unique(y)) \u0026lt; 20: # 分类问题 selector = SelectKBest(score_func=f_classif, k=min(50, X.shape[1])) else: # 回归问题 from sklearn.feature_selection import f_regression selector = SelectKBest(score_func=f_regression, k=min(50, X.shape[1])) X_corr_filtered = selector.fit_transform(X, y) method_outputs[\u0026#39;correlation\u0026#39;] = selector.get_support(indices=True) # 3. 主选择方法 if self.primary_method == \u0026#39;permutation\u0026#39; and model is not None: # 分割数据用于排列测试 from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42) model.fit(X_train, y_train) # 重新训练模型 perm_selector = PermutationImportanceSelector(model, X_val, y_val) importances = perm_selector.calculate_permutation_importance() # 选择重要性较高的特征 threshold_val = importances[\u0026#39;importance_mean\u0026#39;].quantile(0.25) selected_indices = importances[importances[\u0026#39;importance_mean\u0026#39;] \u0026gt;= threshold_val].index method_outputs[\u0026#39;permutation\u0026#39;] = selected_indices.values # 4. 使用交集作为最终选择 final_indices = set(method_outputs[\u0026#39;variance_threshold\u0026#39;]).intersection( set(method_outputs[\u0026#39;correlation\u0026#39;]) ) # 进一步与主方法取交集（如果有） if \u0026#39;permutation\u0026#39; in method_outputs: final_indices = final_indices.intersection(set(method_outputs[\u0026#39;permutation\u0026#39;])) self.selected_final = list(final_indices) self.selected_by_primary = method_outputs.get(self.primary_method, []) return self.selected_final if self.selected_final else self.selected_by_primary def get_consensus_features(self, feature_lists, consensus_ratio=0.6): \u0026#34;\u0026#34;\u0026#34; 获取多个特征列表的共识特征 Args: feature_lists: 特征列表的列表 consensus_ratio: 达成共识的比例 \u0026#34;\u0026#34;\u0026#34; if not feature_lists: return [] from collections import Counter all_features = [item for sublist in feature_lists for item in sublist] feature_counts = Counter(all_features) total_methods = len(feature_lists) consensus_features = [ feat for feat, count in feature_counts.items() if count / total_methods \u0026gt;= consensus_ratio ] return consensus_features 6. 金融风控领域的领域特定特征 6.1 风控比率与指标 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 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 import pandas as pd import numpy as np from typing import List, Dict, Union from datetime import datetime, timedelta class FintechRiskFeatures: \u0026#34;金融风控领域特定特征工程类\u0026#34; def __init__(self): self.user_profiles = {} self.applications = {} self.transactions = {} def create_risk_ratios(self, df: pd.DataFrame, balance_cols: List[str], limit_cols: List[str], income_col: str = None): \u0026#34;\u0026#34;\u0026#34; 创建风险比率特征 Args: df: 包含账户信息的数据 balance_cols: 余额列名列表 limit_cols: 授信额度列名列表 income_col: 收入列名（可选） \u0026#34;\u0026#34;\u0026#34; risk_df = df.copy() # 信用利用率 - 最重要的风控指标之一 for limit_col in limit_cols: for bal_col in balance_cols: usage_ratio = f\u0026#34;{bal_col}_utilization_over_{limit_col}\u0026#34; risk_df[usage_ratio] = risk_df[bal_col] / (risk_df[limit_col] + 1e-8) # 总负债/总收入比（如果收入数据可用） if income_col and income_col in df.columns: total_debt = sum([df[col].fillna(0) for col in balance_cols if col != income_col]) risk_df[\u0026#39;total_debt_over_income\u0026#39;] = total_debt / (risk_df[income_col] + 1e-8) # 多头借贷比例 credit_types = [col for col in df.columns if \u0026#39;credit\u0026#39; in col.lower()] total_active_credit_lines = df[credit_types].apply( lambda row: (row \u0026gt; 0).sum(), axis=1 ) risk_df[\u0026#39;multiple_credit_lines_ratio\u0026#39;] = total_active_credit_lines / len(credit_types) return risk_df def create_payment_behavior_indicators(self, payment_history_df: pd.DataFrame): \u0026#34;\u0026#34;\u0026#34; 从还款记录创建行为指标 Args: payment_history_df: 包含还款记录的数据框 需要至少包含：user_id, pay_date, due_date, actual_payment_amount, scheduled_payment_amount \u0026#34;\u0026#34;\u0026#34; pay_behavior_df = payment_history_df.copy() # 计算延期天数 pd.options.mode.chained_assignment = None # disable warning pay_behavior_df[\u0026#39;days_past_due\u0026#39;] = ( pd.to_datetime(pay_behavior_df[\u0026#39;actual_payment_date\u0026#39;]) - pd.to_datetime(pay_behavior_df[\u0026#39;due_date\u0026#39;]) ).dt.days.apply(lambda x: max(0, x)) # 确保非负 # 付款充足度 pay_behavior_df[\u0026#39;pay_sufficiency\u0026#39;] = ( pay_behavior_df[\u0026#39;actual_payment_amount\u0026#39;] / (pay_behavior_df[\u0026#39;scheduled_payment_amount\u0026#39;] + 1e-8) ) # 违约标识 pay_behavior_df[\u0026#39;is_delinquency\u0026#39;] = np.where( pay_behavior_df[\u0026#39;days_past_due\u0026#39;] \u0026gt; 0, 1, 0 ) # 对数据按客户分组并创建聚合指标 user_summary = pay_behavior_df.groupby(\u0026#39;user_id\u0026#39;).agg({ \u0026#39;days_past_due\u0026#39;: [\u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;std\u0026#39;, \u0026#39;count\u0026#39;], \u0026#39;pay_sufficiency\u0026#39;: [\u0026#39;min\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;mean\u0026#39;], \u0026#39;is_delinquency\u0026#39;: [\u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;] # 历史违约次数/违约频率 }).fillna(0) # 填充NaN，对于从未违约的用户 # 扁平化列名 user_summary.columns = [\u0026#39;_\u0026#39;.join(col).strip() for col in user_summary.columns.values] user_summary = user_summary.add_prefix(\u0026#39;behavior_\u0026#39;) return user_summary.reset_index() def create_transaction_patterns(self, trans_df: pd.DataFrame): \u0026#34;\u0026#34;\u0026#34; 从交易数据创建行为模式 Args: trans_df: 包含交易数据的数据框 需要包含：user_id, trans_date, transaction_amount, transaction_type \u0026#34;\u0026#34;\u0026#34; trans_patterns_df = trans_df.copy() # 转换日期列 trans_patterns_df[\u0026#39;trans_date\u0026#39;] = pd.to_datetime(trans_patterns_df[\u0026#39;trans_date\u0026#39;]) # 按时间计算交易频率 trans_patterns_df[\u0026#39;time_since_last_trans\u0026#39;] = trans_patterns_df.groupby(\u0026#39;user_id\u0026#39;)[ \u0026#39;trans_date\u0026#39; ].diff().dt.days.fillna(0) # 交易金额统计 user_trans_stats = trans_patterns_df.groupby(\u0026#39;user_id\u0026#39;).agg({ \u0026#39;transaction_amount\u0026#39;: [ \u0026#39;mean\u0026#39;, \u0026#39;std\u0026#39;, \u0026#39;min\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;sum\u0026#39; ], \u0026#39;trans_date\u0026#39;: [ \u0026#39;count\u0026#39;, lambda x: x.nunique() # 交易总次数, 不同日期数 ], # 转换为数值特征的交易类型计数 \u0026#39;transaction_type\u0026#39;: lambda x: x.value_counts().to_dict() # 类型分布 }).fillna(0) # 扁平化列名 user_trans_stats.columns = [ \u0026#39;_\u0026#39;.join(col).strip().rstrip(\u0026#39;_\u0026#39;) if col[1] != \u0026#39;\u0026lt;lambda\u0026gt;\u0026#39; else f\u0026#39;{col[0]}_unqiued_days\u0026#39; for col in user_trans_stats.columns.values ] user_trans_stats.rename(columns={\u0026#39;trans_date_\u0026lt;lambda\u0026gt;\u0026#39;: \u0026#39;unique_transaction_days\u0026#39;}, inplace=True) # 添加一些复合特征 # 平均每日交易额 user_trans_stats[\u0026#39;avg_daily_spend\u0026#39;] = user_trans_stats[\u0026#39;transaction_amount_sum\u0026#39;] / ( user_trans_stats[\u0026#39;unique_transaction_days\u0026#39;] + 1e-8 ) # 交易额变异系数（衡量支出稳定性） user_trans_stats[\u0026#39;transaction_amount_cv\u0026#39;] = user_trans_stats[\u0026#39;transaction_amount_std\u0026#39;] / ( user_trans_stats[\u0026#39;transaction_amount_mean\u0026#39;] + 1e-8 ) return user_trans_stats.reset_index() def create_time_window_features(self, df: pd.DataFrame, date_col: str, value_col: str, windows: List[int] = [7, 14, 30]): \u0026#34;\u0026#34;\u0026#34; 创建时间窗内滚动聚合特征 Args: df: 时间序列格式的交易或事件数据 date_col: 日期列 value_col: 数值列 windows: 周期天数组 \u0026#34;\u0026#34;\u0026#34; df = df.copy() df[date_col] = pd.to_datetime(df[date_col]) df = df.sort_values([date_col]) for window in windows: df[f\u0026#39;{value_col}_last_{window}d_sum\u0026#39;] = df[value_col].rolling( window=f\u0026#39;{window}D\u0026#39;, min_periods=1 ).sum() df[f\u0026#39;{value_col}_last_{window}d_mean\u0026#39;] = df[value_col].rolling( window=f\u0026#39;{window}D\u0026#39;, min_periods=1 ).mean() df[f\u0026#39;{value_col}_last_{window}d_max\u0026#39;] = df[value_col].rolling( window=f\u0026#39;{window}D\u0026#39;, min_periods=1 ).max() df[f\u0026#39;{value_col}_last_{window}d_count\u0026#39;] = df[value_col].rolling( window=f\u0026#39;{window}D\u0026#39;, min_periods=1 ).count() return df.fillna(0) def create_aggregated_bureau_features(self, bureau_df: pd.DataFrame): \u0026#34;\u0026#34;\u0026#34; 创建征信数据聚类特征 Args: bureau_df: 征信记录数据框 应包含：person_id, loan_type, amount, status, start_date, close_date \u0026#34;\u0026#34;\u0026#34; df = bureau_df.copy() df[\u0026#39;start_date\u0026#39;] = pd.to_datetime(df[\u0026#39;start_date\u0026#39;]) df[\u0026#39;close_date\u0026#39;] = pd.to_datetime(df[\u0026#39;close_date\u0026#39;]) # 计算信用历史长度 df[\u0026#39;credit_history_length_days\u0026#39;] = ( df[\u0026#39;close_date\u0026#39;] - df[\u0026#39;start_date\u0026#39;] ).dt.days.fillna(-1) # 未结清贷款 # 分组聚合 bureau_agg = df.groupby(\u0026#39;person_id\u0026#39;).agg({ # 贷款数量 \u0026#39;amount\u0026#39;: [\u0026#39;count\u0026#39;, \u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;std\u0026#39;], # 当前活跃贷款数 \u0026#39;status\u0026#39;: lambda x: (x == \u0026#39;Active\u0026#39;).sum(), # 信用历史长度统计 \u0026#39;credit_history_length_days\u0026#39;: [\u0026#39;mean\u0026#39;, \u0026#39;sum\u0026#39;, \u0026#39;max\u0026#39;], # 不同类型贷款的数量 \u0026#39;loan_type\u0026#39;: lambda x: x.nunique() }).fillna(0) # 扁平化列名 bureau_agg.columns = [ \u0026#39;bureau_\u0026#39; + \u0026#39;_\u0026#39;.join(col).strip().rstrip(\u0026#39;\u0026lt;lambda\u0026gt;\u0026#39;) for col in bureau_agg.columns.values ] # 手动修正列名 bureau_agg.rename(columns={ \u0026#39;bureau_status_\u0026lt;lambda\u0026gt;\u0026#39;: \u0026#39;bureau_active_loans\u0026#39;, \u0026#39;bureau_loan_type_\u0026lt;lambda\u0026gt;\u0026#39;: \u0026#39;bureau_unique_loan_types\u0026#39; }, inplace=True) return bureau_agg.reset_index() def create_scorecard_features(self, raw_data: pd.DataFrame): \u0026#34;\u0026#34;\u0026#34; 创建评分卡专用特征 Args: raw_data: 原始申请者数据 包括基本信息和申请信息 \u0026#34;\u0026#34;\u0026#34; features = raw_data.copy() # 职业风险等级 - 基于职业种类 occupation_mapping = { \u0026#39;Manager\u0026#39;: 1, \u0026#39;Director\u0026#39;: 1, \u0026#39;Senior Manager\u0026#39;: 1, # 低风险 \u0026#39;Engineer\u0026#39;: 2, \u0026#39;Doctor\u0026#39;: 2, \u0026#39;Lawyer\u0026#39;: 2, \u0026#39;Scientist\u0026#39;: 2, # 中低风险 \u0026#39;Officer\u0026#39;: 3, \u0026#39;Analyst\u0026#39;: 3, \u0026#39;Teacher\u0026#39;: 3, \u0026#39;Nurse\u0026#39;: 3, # 中等风险 \u0026#39;Worker\u0026#39;: 4, \u0026#39;Clerk\u0026#39;: 4, \u0026#39;Driver\u0026#39;: 4, # 中高风险 \u0026#39;Student\u0026#39;: 5, \u0026#39;Unemployed\u0026#39;: 5 # 高风险 } features[\u0026#39;occupation_risk_level\u0026#39;] = features.get(\u0026#39;job_title\u0026#39;, pd.Series([5]*len(features))).map( occupation_mapping ).fillna(5).astype(int) # 意图特征 - 申请行为分析 features[\u0026#39;intent_indicator\u0026#39;] = features[\u0026#39;requested_loan_amount\u0026#39;] / ( features.get(\u0026#39;income\u0026#39;, pd.Series([1e5]*len(features))) + 1e-8 ) # 社会经济地位指标 features[\u0026#39;socioeconomic_index\u0026#39;] = ( (features.get(\u0026#39;income\u0026#39;, pd.Series([1e5]*len(features))) / 10000) + ((features.get(\u0026#39;age\u0026#39;, pd.Series([30]*len(features))) - 18) / 50) + (features.get(\u0026#39;education_level\u0026#39;, pd.Series([2]*len(features))) - 1) * 0.5 ) / 3 # 时间趋势特征 today = pd.Timestamp.now() features[\u0026#39;years_with_current_employer\u0026#39;] = ( today.year - pd.to_datetime(features.get(\u0026#39;employment_start_date\u0026#39;, pd.Series(today)] # 默认使用当前日期 if \u0026#39;employment_start_date\u0026#39; in features.columns: features[\u0026#39;years_with_current_employer\u0026#39;] = ( pd.to_datetime(features[\u0026#39;employment_start_date\u0026#39;]) - today ).dt.days / 365.25 # 创建离散化特征 - 对评分卡建模有用 for col in [\u0026#39;age\u0026#39;, \u0026#39;income\u0026#39;, \u0026#39;requested_loan_amount\u0026#39;]: if col in features.columns: bin_labels = [f\u0026#39;{col}_bin_{i}\u0026#39; for i in range(5)] features[f\u0026#39;{col}_category\u0026#39;], bins = pd.qcut( features[col], q=5, labels=bin_labels, duplicates=\u0026#39;drop\u0026#39; ).factorize() # 将类别转换为数字 6.2 行为序列特征 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 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 def create_behavioral_sequence_features(self, user_activities_df: pd.DataFrame): \u0026#34;\u0026#34;\u0026#34; 从用户活动数据创建行为序列特征 Args: user_activities_df: 包含用户活动数据的数据框 应包含：user_id, activity_type, activity_date, activity_details \u0026#34;\u0026#34;\u0026#34; activities = user_activities_df.copy() activities[\u0026#39;activity_date\u0026#39;] = pd.to_datetime(activities[\u0026#39;activity_date\u0026#39;]) activities = activities.sort_values([\u0026#39;user_id\u0026#39;, \u0026#39;activity_date\u0026#39;]) # 计算活动频率 user_activity_freq = activities.groupby([\u0026#39;user_id\u0026#39;, \u0026#39;activity_type\u0026#39;]).size().reset_index(name=\u0026#39;freq\u0026#39;) user_activity_pivot = user_activity_freq.pivot( index=\u0026#39;user_id\u0026#39;, columns=\u0026#39;activity_type\u0026#39;, values=\u0026#39;freq\u0026#39; ).fillna(0).add_prefix(\u0026#39;act_\u0026#39;) # 活跃度指标 time_diffs = activities.groupby(\u0026#39;user_id\u0026#39;)[\u0026#39;activity_date\u0026#39;].diff().dt.days.fillna(0) activities.loc[:, \u0026#39;days_since_last_activity\u0026#39;] = time_diffs # 计算用户持续活跃天数统计 user_engagement_metrics = activities.groupby(\u0026#39;user_id\u0026#39;).agg({ \u0026#39;days_since_last_activity\u0026#39;: [\u0026#39;mean\u0026#39;, \u0026#39;min\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;std\u0026#39;], \u0026#39;activity_date\u0026#39;: [\u0026#39;nunique\u0026#39;, \u0026#39;count\u0026#39;], # 独一日期和总活动数 }).fillna(0) # 扁平化列名 user_engagement_metrics.columns = [ \u0026#39;engagement_\u0026#39; + \u0026#39;_\u0026#39;.join(col).strip() for col in user_engagement_metrics.columns.values ] # 合并所有行为特征 result = pd.concat([ user_engagement_metrics, user_activity_pivot ], axis=1).fillna(0) return result.reset_index() def create_risk_segmentation_features(self, df: pd.DataFrame): \u0026#34;\u0026#34;\u0026#34; 创建风险细分特征 Args: df: 包含客户信息的数据框 \u0026#34;\u0026#34;\u0026#34; risk_features = df.copy() # 基于信用评分的风险分级 if \u0026#39;credit_score\u0026#39; in df.columns: risk_features[\u0026#39;credit_grade\u0026#39;] = pd.cut( df[\u0026#39;credit_score\u0026#39;], bins=[0, 550, 650, 750, 850, np.inf], labels=[\u0026#39;Very Poor\u0026#39;, \u0026#39;Poor\u0026#39;, \u0026#39;Fair\u0026#39;, \u0026#39;Good\u0026#39;, \u0026#39;Excellent\u0026#39;] ).cat.codes # 收入等级划分 if \u0026#39;income\u0026#39; in df.columns: risk_features[\u0026#39;income_bracket\u0026#39;] = pd.qcut( df[\u0026#39;income\u0026#39;], q=5, labels=[\u0026#39;Bottom 20%\u0026#39;, \u0026#39;20-40%\u0026#39;, \u0026#39;Mid 20%\u0026#39;, \u0026#39;40-80%\u0026#39;, \u0026#39;Top 20%\u0026#39;] ).cat.codes # 综合风险评分 if \u0026#39;credit_score\u0026#39; in df.columns and \u0026#39;income\u0026#39; in df.columns: risk_features[\u0026#39;composite_risk_score\u0026#39;] = ( 0.4 * (df[\u0026#39;credit_score\u0026#39;] / df[\u0026#39;credit_score\u0026#39;].max()) + 0.3 * (df.get(\u0026#39;income\u0026#39;, 0) / df.get(\u0026#39;income\u0026#39;, 0).max()) + 0.3 * (df.get(\u0026#39;age\u0026#39;, 0) / df.get(\u0026#39;age\u0026#39;, 0).max()).fillna(0) ) return risk_features # 7. 高级特征工程流水线 class AdvancedFeatureEngineeringPipeline: \u0026#34;\u0026#34;\u0026#34; 高级特征工程流水线整合上述所有方法 \u0026#34;\u0026#34;\u0026#34; def __init__(self): self.feature_processors = { \u0026#39;tsfresh\u0026#39;: AutomatedTSFreshFramework(), \u0026#39;featuretools\u0026#39;: AutomatedFeatureToolsFramework(), \u0026#39;autofeat\u0026#39;: AutomatedAutoFeatFramework(), \u0026#39;time_series\u0026#39;: TimeSeriesFeatureEngineering(), \u0026#39;cross_features\u0026#39;: CrossFeatureEngineering(), \u0026#39;selector\u0026#39;: FeatureSelectorHybrid(), \u0026#39;fintech_risk\u0026#39;: FintechRiskFeatures() } def run_complete_pipeline(self, raw_data: Dict[str, pd.DataFrame], target_col: str = None, task_type: str = \u0026#39;classification\u0026#39;, enable_ts_features: bool = True, enable_cross_features: bool = True, enable_selection: bool = True): \u0026#34;\u0026#34;\u0026#34; 运行完整特征工程流水线 Args: raw_data: 原始数据字典 target_col: 目标列名 task_type: 任务类型 (\u0026#39;classification\u0026#39;, \u0026#39;regression\u0026#39;) enable_ts_features: 是否启用时间序列特征 enable_cross_features: 是否启用交叉特征 enable_selection: 是否启用特征选择 \u0026#34;\u0026#34;\u0026#34; processed_data = {} # 1. 自动化特征工程 print(\u0026#34;步骤 1: 运行自动化特征工程...\u0026#34;) if \u0026#39;main\u0026#39; in raw_data: try: feat_tools_framework = AutomatedFeatureToolsFramework() feature_matrix, feature_defs = ( feat_tools_framework .setup_entityset(raw_data) .generate_features(\u0026#39;main\u0026#39;) ) if target_col and target_col in raw_data[\u0026#39;main\u0026#39;].columns: y = raw_data[\u0026#39;main\u0026#39;][target_col] X = feature_matrix.drop(columns=[target_col]) else: print(\u0026#34;警告: 未找到目标列，跳过监督特征选择\u0026#34;) X = feature_matrix y = None except Exception as e: print(f\u0026#34;特征工具出错: {e}\u0026#34;) # 备选方案 - 简单的特征合并 X = pd.concat(raw_data.values(), axis=1).fillna(0) y = raw_data.get(\u0026#39;main\u0026#39;, pd.DataFrame()).get(target_col) else: # 如果没有\u0026#39;main\u0026#39;实体，使用第一个数据框 first_key = list(raw_data.keys())[0] X = raw_data[first_key].copy() if target_col in X.columns: y = X.pop(target_col) # 从X中移除目标列并保存 else: y = None # 2. 时间序列特征（如果适用） if enable_ts_features and \u0026#39;timeseries\u0026#39; in raw_data: print(\u0026#34;步骤 2: 生产时间序列特征...\u0026#34;) ts_feat_eng = TimeSeriesFeatureEngineering() ts_features = ts_feat_eng.create_lag_features( raw_data[\u0026#39;timeseries\u0026#39;], value_column=raw_data[\u0026#39;timeseries\u0026#39;].select_dtypes( include=[np.number]).columns[0] ) # 合并时间序列特征与主特征矩阵 X = pd.concat([X, ts_features.drop(columns=raw_data[\u0026#39;timeseries\u0026#39;].columns)], axis=1) # 3. 交叉特征 if enable_cross_features and len(X.select_dtypes(include=[np.number]).columns) \u0026gt; 1: print(\u0026#34;步骤 3: 创建交叉特征...\u0026#34;) cross_engineer = CrossFeatureEngineering() numeric_cols = X.select_dtypes(include=[np.number]).columns[:10] # 只使用前10个特征，避免过多交互 X_cross = cross_engineer.create_manual_interactions(X, numeric_cols.tolist()) X = X_cross # 4. 风控特征（如果适用） if \u0026#39;risk_data\u0026#39; in raw_data: print(\u0026#34;步骤 4: 创建风险领域专用特征...\u0026#34;) risk_engineer = FintechRiskFeatures() if \u0026#39;bureau\u0026#39; in raw_data: bureau_risk_features = risk_engineer.create_aggregated_bureau_features(raw_data[\u0026#39;bureau\u0026#39;]) X = X.merge(bureau_risk_features, left_on=\u0026#39;user_id\u0026#39;, right_on=\u0026#39;person_id\u0026#39;, how=\u0026#39;left\u0026#39;) # 5. 特征选择（如果提供目标变量） if enable_selection and y is not None: print(\u0026#34;步骤 5: 执行特征选择...\u0026#34;) # 将数据分割为训练集和测试集用于选择 from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # 拟合简单模型用于选择 if task_type == \u0026#39;classification\u0026#39;: from sklearn.ensemble import RandomForestClassifier model = RandomForestClassifier(n_estimators=50, random_state=42) else: from sklearn.ensemble import RandomForestRegressor model = RandomForestRegressor(n_estimators=50, random_state=42) model.fit(X_train, y_train) # 使用混合选择方法 hybrid_selector = FeatureSelectorHybrid(primary_method=\u0026#39;permutation\u0026#39;) selected_indices = hybrid_selector.fit_selection(X_train, y_train, model=model) # 裁剪特征集合 X = X.iloc[:, selected_indices] if len(selected_indices) \u0026gt; 0 else X print(f\u0026#34;最终特征矩阵形状: {X.shape}\u0026#34;) return X, y # 8. 决策流程图说明 def print_decision_flow(): \u0026#34;\u0026#34;\u0026#34; 打印特征工程决策流程 \u0026#34;\u0026#34;\u0026#34; flow_chart = \u0026#34;\u0026#34;\u0026#34; 高级特征工程决策流程图 ======================== 开始分析数据结构 | v ┌─────────────────┐ │ 评估数据特征 │ │ - 数据量级 │ │ - 数据结构 │ │ - 时间序列性 │ │ - 业务领域 │ └─────────┬───────┘ | v ┌─────────────────┐ │ 自动化特征提取 │ │ ? │ │ 适合复杂关系数据│ └─────┬─────────┬─┘ │ 是 │ 否 v v ┌─────────┐ ┌─────────────┐ │ TSFresh │ │ 手工设计 │ │FeatTools│ │ 或领域特征 │ │AutoFeat │ └─────────────┘ └─────────┘ | v ┌─────────────────┐ │ 时间序列特征 │ │ ? │ └─────┬───────────┘ │ 是 v ┌─────────────────┐ │ 滞后特征 │ │ 滚动统计 │ │ 循环编码 │ │ 变化特征 │ └─────────────────┘ | v ┌─────────────────┐ │ 交叉特征工程 │ │ ? │ └─────┬───────────┘ │ 是 v ┌─────────────────┐ │ 低到高阶交叉 │ │ 比率特征 │ │ 逻辑交互 │ └─────────────────┘ | v ┌─────────────────┐ │ 域特定特征 │ │ ? │ │ (金融/医疗/等) │ └─────┬───────────┘ │ 是 v ┌─────────────────┐ │ 预定义模板 │ │ 风险评分 │ │ 行为指标 │ └─────────────────┘ | v ┌─────────────────┐ │ 特征选择 │ │ ? │ └─────┬───────────┘ │ 是 v ┌─────────────────┐ │ - 置换重要性 │ │ - SHAP分析 │ │ - 遗传算法 │ │ - 统计测试 │ └─────────────────┘ | v ┌─────────────────┐ │ 特征质量验证 │ │ - 重复性检测 │ │ - 多重共线检测 │ │ - 有效性评估 │ └─────────────────┘ | v ┌─────────────────┐ │ 清理最终特征集 │ └─────────────────┘ | v ┌─────────────────┐ │ 构建模型管道 │ └─────────────────┘ \u0026#34;\u0026#34;\u0026#34; print(flow_chart) # 9. 使用示例 def example_usage(): \u0026#34;\u0026#34;\u0026#34; 演示如何使用完整的特征工程框架 \u0026#34;\u0026#34;\u0026#34; print(\u0026#34;=== 高级特征工程框架演示 ===\\n\u0026#34;) # 示例数据创建 np.random.seed(42) n_samples = 1000 # 创建示例主表数据 demo_main_data = pd.DataFrame({ \u0026#39;user_id\u0026#39;: range(n_samples), \u0026#39;age\u0026#39;: np.random.randint(18, 80, n_samples), \u0026#39;income\u0026#39;: np.random.lognormal(10, 1, n_samples), \u0026#39;credit_score\u0026#39;: np.random.normal(650, 100, n_samples), \u0026#39;requested_amount\u0026#39;: np.random.lognormal(10, 0.8, n_samples), \u0026#39;target\u0026#39;: np.random.binomial(1, 0.1, n_samples) }) # 创建示例历史数据 n_records_per_user = 5 demo_history_data = pd.DataFrame({ \u0026#39;user_id\u0026#39;: np.repeat(range(n_samples), n_records_per_user), \u0026#39;transaction_amount\u0026#39;: np.random.normal(1000, 200, n_samples * n_records_per_user), \u0026#39;days_ago\u0026#39;: np.random.randint(1, 365, n_samples * n_records_per_user) }) # 准备数据字典 sample_data = { \u0026#39;main\u0026#39;: demo_main_data, \u0026#39;history\u0026#39;: demo_history_data } print(f\u0026#34;输入数据结构:\u0026#34;) for name, df in sample_data.items(): print(f\u0026#34;{name}: 形状={df.shape}, 列名={df.columns.tolist()}\u0026#34;) print(f\u0026#34;\\ntarget列值分布:\\n{demo_main_data[\u0026#39;target\u0026#39;].value_counts()}\u0026#34;) # 实例化流水线 pipeline = AdvancedFeatureEngineeringPipeline() # 运行完整流水线 processed_X, processed_y = pipeline.run_complete_pipeline( raw_data=sample_data, target_col=\u0026#39;target\u0026#39;, task_type=\u0026#39;classification\u0026#39;, enable_ts_features=True, enable_cross_features=True, enable_selection=True ) print(f\u0026#34;\\n处理后的数据:\u0026#34;) print(f\u0026#34;特征矩阵形状: {processed_X.shape}\u0026#34;) print(f\u0026#34;目标向量形状: {processed_y.shape if processed_y is not None else \u0026#39;None\u0026#39;}\u0026#34;) print(\u0026#34;\\n特征工程完成! 演示结束.\u0026#34;) # 在适当位置添加主执行块 if __name__ == \u0026#34;__main__\u0026#34;: print(\u0026#34;运行高级特征工程框架演示...\u0026#34;) print_decision_flow() print(\u0026#34;\\n\u0026#34; + \u0026#34;=\u0026#34;*60) example_usage() 结论 本文详细介绍了现代高级特征工程的各种方法和技术：\n自动化特征工程：使用FeatureTools、TSFresh和AutoFeat自动生成新特征，大幅减少手工工作\n时间序列特征：通过滞后特征、滚动统计、循环编码等方式捕捉时间维度上的关键模式\n交叉特征工程：系统地探索不同特征之间的交互作用，构建具有洞察力的复合特征\n特征选择技术：结合多种方法（置换重要性、SHAP值、遗传算法）精简特征空间\n领域特定特征：特别是针对金融风控领域的专业特征工程实践\n本框架提供了一个全面、可落地的技术解决方案，涵盖了从数据预处理到特征选择的整个流程，并特别关注金融风控领域的实际应用。\n通过这个框架，数据科学团队能高效地实现高级特征工程技术，提高模型预测能力，尤其是在处理表格型数据和时间序列数据的实际业务场景中，能够显著提升模型性能。\n","permalink":"https://liugangjian.github.io/zh/posts/advanced-feature-engineering-framework/","summary":"系统介绍自动化特征工程、时间序列特征、交叉特征、特征选择与金融风控领域的特征构建方法。","title":"高级特征工程框架：从理论到实践"},{"content":"摘要 本文系统分析 Kaggle Home Credit Default Risk 竞赛方案，介绍从数据预处理、特征工程到模型集成的完整机器学习流程。文章讨论该大规模信用风险预测任务中的架构选择、实现策略与性能优化方法，涵盖数据质量评估、关系型数据库的特征提取、梯度提升模型优化和堆叠集成，为类似金融风险结构化数据预测任务提供实践参考。\n关键词 信用风险建模、梯度提升、特征工程、模型堆叠、LightGBM、XGBoost、CatBoost、机器学习流水线\n1. 引言与业务背景 1.1 问题领域与研究动机 金融服务普及已成为全球经济发展的重要挑战。传统信用评估高度依赖既有信用记录、稳定就业记录和可抵押资产；按照世界银行 2017 年全球普惠金融数据库的统计，全球约有 17 亿成年人无法获得正规银行服务，这些标准使他们难以进入传统信贷体系。\nHome Credit Group 是一家在 10 多个国家开展业务的国际非银行金融机构，主要服务缺乏既有信用记录、通常被传统银行拒绝的“信用不可见”人群。核心业务挑战是利用替代数据，在约 5 分钟的有限时间内完成准确的违约预测。\n1.2 信用风险预测任务 任务定义：预测违约概率（PD）的二分类问题。\n\\[ Y_i = \\begin{cases} 1, \u0026 \\text{if client } i \\text{ defaults (90+ days past due)} \\\\\\\\ 0, \u0026 \\text{if client } i \\text{ repays as scheduled} \\end{cases} \\]评价指标：受试者工作特征曲线下面积（ROC-AUC）。\n选择 AUC，是因为它对类别不平衡相对稳健，重点衡量排序能力，而非绝对概率的校准程度：\n\\[ \\text{AUC} = \\int_0^1 \\text{TPR}(\\tau) \\, d(\\text{FPR}(\\tau)) \\]其中，\\(\\text{TPR}\\) 表示真正率，\\(\\text{FPR}\\) 表示阈值 \\(\\tau\\) 下的假正率。\n指标解读：\nAUC = 0.50：随机预测，没有区分能力。 AUC ∈ [0.60, 0.70]：表现较差。 AUC ∈ [0.70, 0.80]：表现可接受。 AUC ∈ [0.80, 0.90]：表现良好。 AUC \u0026gt; 0.90：表现优秀，但需要核查过拟合。 1.3 信用评估中的替代数据 该竞赛数据集体现了向替代信用评分转变的思路，涉及非传统数据形态：\n传统数据 替代性代理数据 数据提供方 信用评分 手机充值模式、通话时长 电信运营商 收入证明 POS 交易记录、租金支付历史 支付机构、房产平台 银行流水 分期还款历史、信用卡账单 消费金融公司 就业证明 电商活动、社交媒体互动 互联网平台 1.4 竞赛成果与方法影响 2018 年 Home Credit Default Risk 竞赛吸引了全球 7,194 支队伍。优秀方案体现了以下方法创新：\n特征工程：从基础聚合发展到时间窗口特征和趋势分析。 集成架构：系统应用多层堆叠策略。 数据预处理：改进缺失值填补与异常值处理。 这些方法也已应用于保险欺诈检测、营销响应预测和客户流失建模等相关领域。\n2. 数据集架构与表结构分析 2.1 数据规模与关联结构 数据集由七张相互关联的关系表组成，总记录数超过 5,000 万，体现了企业金融系统常见的复杂关系型数据库结构。\n数据表统计概览：\n数据表 行数 存储大小 说明 主键 外键 application_{train,test} 307,511 / 48,744 45MB / 7MB 主申请表 SK_ID_CURR — bureau 1,716,428 172MB 征信记录 SK_ID_BUREAU SK_ID_CURR bureau_balance 27,299,925 574MB 每月征信状态 — SK_ID_BUREAU previous_application 1,670,214 150MB 历史申请 SK_ID_PREV SK_ID_CURR installments_payments 13,605,401 730MB 分期还款记录 — SK_ID_PREV POS_CASH_balance 10,001,358 970MB POS 现金贷款账单 — SK_ID_PREV credit_card_balance 3,840,312 400MB 信用卡账单 — SK_ID_PREV 2.2 实体关系模型 数据库采用分层关联结构，包含三个主要标识符域：\n1 2 3 SK_ID_CURR: Client-level identifier (primary entity key) SK_ID_PREV: Previous application identifier (transaction-level) SK_ID_BUREAU: External credit bureau record identifier 关系拓扑：\n1 2 3 4 5 6 application [1] ───\u0026lt;N\u0026gt;─── bureau [1] ───\u0026lt;N\u0026gt;─── bureau_balance │ ├─\u0026lt;N\u0026gt;─── previous_application [1] ───\u0026lt;N\u0026gt;─── installments_payments │ ├─\u0026lt;N\u0026gt;─── POS_CASH_balance │ └─\u0026lt;N\u0026gt;─── credit_card_balance └─\u0026lt;N\u0026gt;─── credit_card_balance 一对多（1:N）关系要求在特征工程中进行聚合，将时间序列和多条记录转换为机器学习模型可用的静态特征向量。\n2.3 字段详解 2.3.1 申请表：主实体 申请表是核心实体，训练集部分包含目标标签。\n人口统计与申请特征：\n1 2 3 4 5 6 7 8 9 10 SK_ID_CURR: Integer (primary identifier) TARGET: Binary (0=non-default, 1=default) - training set only CODE_GENDER: Categorical (M/F/XNA) FLAG_OWN_CAR: Binary (Y/N) FLAG_OWN_REALTY: Binary (Y/N) CNT_CHILDREN: Integer (count of children) AMT_INCOME_TOTAL: Float (annual income in local currency) AMT_CREDIT: Float (loan amount requested) AMT_ANNUITY: Float (monthly installment amount) AMT_GOODS_PRICE: Float (price of goods being financed) 时间特征：以相对申请日期的天数编码，负值表示过去。\n1 2 3 4 5 DAYS_BIRTH: Integer (age in days, e.g., -10000 ≈ 27.4 years) DAYS_EMPLOYED: Integer (employment duration, special value 365243 indicates unemployed) DAYS_REGISTRATION: Integer (registration change recency) DAYS_ID_PUBLISH: Integer (identity document issuance recency) DAYS_LAST_PHONE_CHANGE: Integer (mobile phone change recency) 外部评分特征：具有较强预测能力的归一化评分。\n1 2 3 4 EXT_SOURCE_1: Float [0,1] (normalized external score 1) EXT_SOURCE_2: Float [0,1] (normalized external score 2) EXT_SOURCE_3: Float [0,1] (normalized external score 3) # Sourced from third-party credit bureaus 2.3.2 征信表：外部信用历史 记录客户与外部金融机构之间的信贷关系。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 SK_ID_CURR: Integer (foreign key to application) SK_ID_BUREAU: Integer (unique bureau record identifier) CREDIT_ACTIVE: Categorical (Active/Closed/Sold/Demand/Bad debt) CREDIT_CURRENCY: Categorical (currency code) DAYS_CREDIT: Integer (days since credit application) CREDIT_DAY_OVERDUE: Integer (current days past due) DAYS_CREDIT_ENDDATE: Integer (remaining duration to maturity) DAYS_ENDDATE_FACT: Integer (actual closure date) AMT_CREDIT_MAX_OVERDUE: Float (maximum historical overdue amount) CNT_CREDIT_PROLONG: Integer (count of credit prolongations) AMT_CREDIT_SUM: Float (total credit exposure) AMT_CREDIT_SUM_DEBT: Float (outstanding debt) AMT_CREDIT_SUM_LIMIT: Float (credit limit) AMT_CREDIT_SUM_OVERDUE: Float (current overdue amount) CREDIT_TYPE: Categorical (loan type: consumer, mortgage, etc.) DAYS_CREDIT_UPDATE: Integer (recency of bureau update) AMT_ANNUITY: Float (monthly payment obligation) 2.3.3 征信月度余额表：征信状态变化 每条征信记录的月度状态快照，可用于趋势分析。\n1 2 3 4 5 6 7 8 9 10 11 SK_ID_BUREAU: Integer (foreign key to bureau) MONTHS_BALANCE: Integer (relative month index, -1=last month, -2=two months ago) STATUS: Categorical encoding: \u0026#39;0\u0026#39;: Current (no delinquency) \u0026#39;1\u0026#39;: 1-29 days past due \u0026#39;2\u0026#39;: 30-59 days past due \u0026#39;3\u0026#39;: 60-89 days past due \u0026#39;4\u0026#39;: 90-119 days past due \u0026#39;5\u0026#39;: 120-149 days past due \u0026#39;C\u0026#39;: Closed (paid off) \u0026#39;X\u0026#39;: Status unknown 2.3.4 历史申请表 Home Credit 系统内部的历史申请。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 SK_ID_CURR: Integer (foreign key) SK_ID_PREV: Integer (unique previous application identifier) NAME_CONTRACT_TYPE: Categorical (Cash/Consumer/Revolving loans) AMT_ANNUITY: Float (proposed monthly payment) AMT_APPLICATION: Float (requested amount) AMT_CREDIT: Float (approved amount) AMT_DOWN_PAYMENT: Float (down payment amount) RATE_DOWN_PAYMENT: Float (down payment ratio) RATE_INTEREST_PRIMARY: Float (primary interest rate) RATE_INTEREST_PRIVILEGED: Float (preferential interest rate) NAME_CONTRACT_STATUS: Categorical (Approved/Refused/Canceled/Unused) DAYS_DECISION: Integer (days since decision) CODE_REJECT_REASON: Categorical (rejection reason if applicable) NAME_CLIENT_TYPE: Categorical (New/Repeat customer) CNT_PAYMENT: Integer (proposed term in months) 2.3.5 分期还款表 细粒度还款交易记录。\n1 2 3 4 5 6 7 8 SK_ID_CURR: Integer (foreign key) SK_ID_PREV: Integer (foreign key to previous_application) NUM_INSTALMENT_VERSION: Integer (version of installment schedule) NUM_INSTALMENT_NUMBER: Integer (installment sequence number) DAYS_INSTALMENT: Integer (scheduled payment date) DAYS_ENTRY_PAYMENT: Integer (actual payment date) AMT_INSTALMENT: Float (scheduled amount) AMT_PAYMENT: Float (actual amount paid) 衍生指标：\n逾期天数（DPD）：\\(DPD = DAYS_ENTRY_PAYMENT - DAYS_INSTALMENT\\) 还款金额偏差：\\(\\Delta AMT = AMT_PAYMENT - AMT_INSTALMENT\\) 2.4 数据质量概况 类别分布：\n1 2 3 Class 0 (Non-default): 282,686 observations (91.93%) Class 1 (Default): 24,825 observations (8.07%) Imbalance Ratio: 11.4:1 缺失值概览：\nEXT_SOURCE_1：缺失率 56.38%。 EXT_SOURCE_3：缺失率 19.83%。 AMT_ANNUITY：缺失率 0.003%。 OCCUPATION_TYPE：缺失率 31.35%。 异常编码：\nDAYS_EMPLOYED = 365,243（约 1,000 年）：表示失业的哨兵值。 CODE_GENDER = \u0026lsquo;XNA\u0026rsquo;：未指定的性别类别。 AMT_INCOME_TOTAL：出现 117,000,000 的极端值，可能是数据错误。 3. 系统架构与流水线设计 3.1 框架选择：Steppy 流水线架构 方案采用 Steppy，这是面向模块化、可复现数据科学流程的轻量级机器学习流水线库。Steppy 借鉴 Apache Airflow、Spotify Luigi 等工作流编排系统的设计原则，并针对机器学习任务进行适配。\n使用流水线框架的原因：\n传统命令式机器学习代码存在以下架构局限：\n1 2 3 4 5 6 7 # Anti-pattern: Tightly coupled workflow data = load_data() data = clean_data(data) data = extract_features(data) X_train, X_test, y_train, y_test = split_data(data) model = train_model(X_train, y_train) predictions = model.predict(X_test) 主要不足：\n耦合：修改某个阶段，需要理解下游依赖。 可复现性：中间结果难以缓存或进行版本管理。 并行化：顺序执行限制了计算资源优化。 实验追踪：难以系统比较不同参数配置。 Steppy 的声明式方法：\n1 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 from steppy.base import BaseTransformer class DataLoader(BaseTransformer): \u0026#34;\u0026#34;\u0026#34;Loads raw data from persistent storage.\u0026#34;\u0026#34;\u0026#34; def transform(self, filepath): data = pd.read_csv(filepath) return {\u0026#39;data\u0026#39;: data} class DataCleaningTransformer(BaseTransformer): \u0026#34;\u0026#34;\u0026#34;Applies data quality transformations.\u0026#34;\u0026#34;\u0026#34; def transform(self, data): cleaned = self._handle_outliers(data) cleaned = self._impute_missing(cleaned) return {\u0026#39;cleaned_data\u0026#39;: cleaned} def _handle_outliers(self, df): # Implementation pass class FeatureExtractionTransformer(BaseTransformer): \u0026#34;\u0026#34;\u0026#34;Engineers features from cleaned data.\u0026#34;\u0026#34;\u0026#34; def transform(self, cleaned_data): features = self._aggregate_features(cleaned_data) return {\u0026#39;features\u0026#39;: features} 设计原则：\n标准接口：所有组件继承 BaseTransformer，提供 fit() 和 transform() 方法。 显式数据流：使用带命名键的字典传递输入输出，便于追踪。 可组合性：通过 Step 和 Adapter 抽象连接各步骤。 持久化：中间产物支持缓存与检查点。 3.2 端到端流水线 阶段 1：数据读取与清洗\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def build_data_ingestion_pipeline(config): \u0026#34;\u0026#34;\u0026#34;Constructs the data loading and cleaning pipeline stage.\u0026#34;\u0026#34;\u0026#34; # Load all seven tables raw_data = DataLoader(config.data_paths).transform() # Apply table-specific cleaning transformers cleaning_transformers = { \u0026#39;application\u0026#39;: ApplicationCleaning(), \u0026#39;bureau\u0026#39;: BureauCleaning(), \u0026#39;bureau_balance\u0026#39;: BureauBalanceCleaning(), \u0026#39;previous_application\u0026#39;: PreviousApplicationCleaning(), \u0026#39;installments_payments\u0026#39;: InstallmentPaymentsCleaning(), \u0026#39;pos_cash_balance\u0026#39;: PosCashBalanceCleaning(), \u0026#39;credit_card_balance\u0026#39;: CreditCardBalanceCleaning() } cleaned_data = { table: transformer.transform(raw_data[table]) for table, transformer in cleaning_transformers.items() } return cleaned_data 阶段 2：特征工程\n1 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 def build_feature_engineering_pipeline(cleaned_data): \u0026#34;\u0026#34;\u0026#34;Constructs the feature extraction pipeline stage.\u0026#34;\u0026#34;\u0026#34; # Table-specific feature extraction bureau_features = BureauFeatureExtractor().transform( cleaned_data[\u0026#39;bureau\u0026#39;], cleaned_data[\u0026#39;bureau_balance\u0026#39;] ) prev_app_features = PreviousApplicationFeatureExtractor().transform( cleaned_data[\u0026#39;previous_application\u0026#39;] ) installment_features = InstallmentFeatureExtractor().transform( cleaned_data[\u0026#39;installments_payments\u0026#39;] ) # Feature consolidation all_features = FeatureConcatenator().transform([ cleaned_data[\u0026#39;application\u0026#39;], bureau_features, prev_app_features, installment_features ]) # Categorical encoding encoded_features = CategoricalEncoder().transform( all_features, method=\u0026#39;target_encoding\u0026#39; ) return { \u0026#39;features\u0026#39;: encoded_features, \u0026#39;target\u0026#39;: cleaned_data[\u0026#39;application\u0026#39;][\u0026#39;TARGET\u0026#39;], \u0026#39;feature_names\u0026#39;: encoded_features.columns.tolist() } 阶段 3：模型训练\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 def build_model_training_pipeline(feature_data, config): \u0026#34;\u0026#34;\u0026#34;Constructs the model training pipeline stage.\u0026#34;\u0026#34;\u0026#34; # Train/validation split X_train, X_valid, y_train, y_valid = train_test_split( feature_data[\u0026#39;features\u0026#39;], feature_data[\u0026#39;target\u0026#39;], test_size=0.2, stratify=feature_data[\u0026#39;target\u0026#39;], random_state=config.random_seed ) # Model initialization and training model = GradientBoostingModel(config.model_params) model.fit(X_train, y_train, validation_data=(X_valid, y_valid)) # Performance evaluation validation_auc = roc_auc_score(y_valid, model.predict(X_valid)) return { \u0026#39;model\u0026#39;: model, \u0026#39;validation_auc\u0026#39;: validation_auc, \u0026#39;feature_importance\u0026#39;: model.feature_importances_ } 阶段 4：构建堆叠集成（Stacking）\n1 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 def build_stacking_ensemble(base_models, meta_learner, X, y, X_test): \u0026#34;\u0026#34;\u0026#34; Implements two-level stacking ensemble architecture. Level 1: Base learners generate out-of-fold predictions Level 2: Meta-learner trains on base model outputs \u0026#34;\u0026#34;\u0026#34; # Generate OOF predictions oof_predictions = {} test_predictions = {} for name, model in base_models.items(): oof_pred, test_pred = generate_oof_predictions( model, X, y, X_test, n_folds=5 ) oof_predictions[name] = oof_pred test_predictions[name] = test_pred # Train meta-learner meta_features = np.column_stack([ oof_predictions[name] for name in base_models.keys() ]) meta_learner.fit(meta_features, y) # Generate final predictions meta_test_features = np.column_stack([ test_predictions[name] for name in base_models.keys() ]) final_predictions = meta_learner.predict_proba(meta_test_features)[:, 1] return final_predictions 3.3 项目目录组织 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 open-solution-home-credit/ ├── src/ # Source code modules │ ├── __init__.py │ ├── pipeline_manager.py # Orchestration layer │ ├── pipelines.py # Pipeline definitions (9 model configurations) │ ├── pipeline_blocks.py # Step factory methods │ ├── feature_extraction.py # Feature transformers (20+ implementations) │ ├── data_cleaning.py # Data quality transformers (7 table-specific) │ ├── models.py # Model wrappers (LGB/XGB/CTB/NN/RF/LR) │ ├── pipeline_config.py # Configuration constants and hyperparameters │ ├── hyperparameter_tuning.py # Optimization strategies │ ├── callbacks.py # Training monitoring callbacks │ ├── utils.py # Utility functions │ └── neptune_hacks.py # Offline experiment tracking support ├── configs/ # Configuration files │ └── neptune.yaml # Main configuration (paths/hyperparameters) ├── data/ # Data directory │ ├── raw/ # Original competition data │ └── workdir/ # Intermediate processing artifacts ├── notebooks/ # Exploratory data analysis ├── blog/ # Documentation │ └── images/ # Visualization assets ├── main.py # CLI entry point ├── requirements.txt # Dependency specification └── README.md # Project documentation 3.4 配置管理 项目采用混合配置策略：YAML 管理实验参数，Python 模块管理代码级常量。\n实验配置（configs/neptune.yaml）：\n1 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 parameters: # Data paths train_filepath: /data/application_train.csv test_filepath: /data/application_test.csv # Model selection pipeline_name: lightGBM # Feature toggles use_application: true use_bureau: true use_bureau_balance: true use_previous_application: true use_installments_payments: true use_pos_cash_balance: true use_credit_card_balance: true # LightGBM hyperparameters lgbm__objective: binary lgbm__metric: auc lgbm__num_leaves: 35 lgbm__learning_rate: 0.02 lgbm__n_estimators: 5000 lgbm__min_child_samples: 70 lgbm__subsample: 1.0 lgbm__colsample_bytree: 0.03 lgbm__reg_lambda: 100.0 lgbm__reg_alpha: 0.0 # Cross-validation configuration n_cv_splits: 5 validation_size: 0.2 stratified_cv: true shuffle: true random_seed: 90210 代码级配置（src/pipeline_config.py）：\n1 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 \u0026#34;\u0026#34;\u0026#34;Constants and aggregation recipes for feature engineering.\u0026#34;\u0026#34;\u0026#34; import numpy as np # Reproducibility constants RANDOM_SEED = 90210 DEV_SAMPLE_SIZE = 1000 # Column type definitions CATEGORICAL_COLUMNS = [ \u0026#39;CODE_GENDER\u0026#39;, \u0026#39;FLAG_OWN_CAR\u0026#39;, \u0026#39;FLAG_OWN_REALTY\u0026#39;, \u0026#39;NAME_TYPE_SUITE\u0026#39;, \u0026#39;NAME_INCOME_TYPE\u0026#39;, \u0026#39;NAME_EDUCATION_TYPE\u0026#39;, \u0026#39;NAME_FAMILY_STATUS\u0026#39;, \u0026#39;NAME_HOUSING_TYPE\u0026#39;, \u0026#39;OCCUPATION_TYPE\u0026#39;, \u0026#39;WEEKDAY_APPR_PROCESS_START\u0026#39;, \u0026#39;ORGANIZATION_TYPE\u0026#39;, \u0026#39;FONDKAPREMONT_MODE\u0026#39;, \u0026#39;HOUSETYPE_MODE\u0026#39;, \u0026#39;WALLSMATERIAL_MODE\u0026#39;, \u0026#39;EMERGENCYSTATE_MODE\u0026#39; ] NUMERICAL_COLUMNS = [ \u0026#39;AMT_INCOME_TOTAL\u0026#39;, \u0026#39;AMT_CREDIT\u0026#39;, \u0026#39;AMT_ANNUITY\u0026#39;, \u0026#39;AMT_GOODS_PRICE\u0026#39;, \u0026#39;DAYS_BIRTH\u0026#39;, \u0026#39;DAYS_EMPLOYED\u0026#39;, \u0026#39;DAYS_REGISTRATION\u0026#39;, \u0026#39;DAYS_ID_PUBLISH\u0026#39;, \u0026#39;EXT_SOURCE_1\u0026#39;, \u0026#39;EXT_SOURCE_2\u0026#39;, \u0026#39;EXT_SOURCE_3\u0026#39; ] # Aggregation recipes for feature extraction BUREAU_AGGREGATION_RECIPES = [ ([\u0026#39;SK_ID_CURR\u0026#39;], [ (\u0026#39;SK_ID_BUREAU\u0026#39;, \u0026#39;count\u0026#39;), (\u0026#39;AMT_CREDIT_SUM\u0026#39;, [\u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;std\u0026#39;]), (\u0026#39;AMT_CREDIT_SUM_DEBT\u0026#39;, [\u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;]), (\u0026#39;AMT_CREDIT_SUM_OVERDUE\u0026#39;, [\u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;]), (\u0026#39;DAYS_CREDIT\u0026#39;, [\u0026#39;min\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;mean\u0026#39;]), (\u0026#39;CREDIT_DAY_OVERDUE\u0026#39;, [\u0026#39;sum\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;mean\u0026#39;]), (\u0026#39;CNT_CREDIT_PROLONG\u0026#39;, \u0026#39;sum\u0026#39;) ]) ] PREVIOUS_APPLICATION_AGGREGATION_RECIPES = [ ([\u0026#39;SK_ID_CURR\u0026#39;], [ (\u0026#39;SK_ID_PREV\u0026#39;, \u0026#39;count\u0026#39;), (\u0026#39;AMT_APPLICATION\u0026#39;, [\u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;]), (\u0026#39;AMT_CREDIT\u0026#39;, [\u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;]), (\u0026#39;AMT_DOWN_PAYMENT\u0026#39;, [\u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;]), (\u0026#39;RATE_INTEREST_PRIMARY\u0026#39;, [\u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;]), (\u0026#39;DAYS_DECISION\u0026#39;, [\u0026#39;min\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;mean\u0026#39;]) ]) ] 这种配置分工带来以下特点：\n易用性：通过 YAML 快速调整实验，无需修改代码。 类型安全：Python 模块提供编译阶段校验。 覆盖能力：支持命令行和环境变量覆盖配置。 4. 探索性数据分析与质量评估 4.1 EDA 方法框架 这里的探索性数据分析（EDA）围绕五个基本问题展开：\n数据质量：有哪些异常、缺失值或编码不一致？ 分布特征：各特征的集中趋势、离散程度与分布形态如何？ 业务洞察：不同人群是否表现出不同的行为？ 预测信号：哪些特征与目标变量存在统计关联？ 特征工程方向：哪些变换或聚合可能提高预测能力？ 4.2 关键发现 4.2.1 类别不平衡分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import pandas as pd import matplotlib.pyplot as plt import seaborn as sns # Load training data train_df = pd.read_csv(\u0026#39;data/application_train.csv\u0026#39;) # Class distribution analysis target_distribution = train_df[\u0026#39;TARGET\u0026#39;].value_counts() print(\u0026#34;Class Distribution:\u0026#34;) print(target_distribution) print(f\u0026#34;\\nClass Proportions:\u0026#34;) print(train_df[\u0026#39;TARGET\u0026#39;].value_counts(normalize=True)) # Output: # Class Distribution: # 0 282686 # 1 24825 # Name: TARGET, dtype: int64 # # Class Proportions: # 0 0.919271 # 1 0.080729 # Name: TARGET, dtype: float64 解读：11.4:1 的类别比例要求谨慎选择指标。仅预测多数类就能达到 91.9% 的准确率，因此准确率可能产生误导，应结合 AUC 和精确率—召回率指标。\n4.2.2 外部评分的预测能力 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # Analyze EXT_SOURCE features fig, axes = plt.subplots(1, 3, figsize=(18, 5)) for idx, col in enumerate([\u0026#39;EXT_SOURCE_1\u0026#39;, \u0026#39;EXT_SOURCE_2\u0026#39;, \u0026#39;EXT_SOURCE_3\u0026#39;]): # Distribution comparison sns.kdeplot( data=train_df[train_df[\u0026#39;TARGET\u0026#39;] == 0][col].dropna(), label=\u0026#39;Non-default\u0026#39;, ax=axes[idx], fill=True, alpha=0.5 ) sns.kdeplot( data=train_df[train_df[\u0026#39;TARGET\u0026#39;] == 1][col].dropna(), label=\u0026#39;Default\u0026#39;, ax=axes[idx], fill=True, alpha=0.5 ) axes[idx].set_title(f\u0026#39;{col} Distribution by Target\u0026#39;) axes[idx].legend() plt.tight_layout() plt.savefig(\u0026#39;images/ext_source_kde.png\u0026#39;, dpi=150) 主要观察：\n违约客户的外部评分普遍更低。 EXT_SOURCE_1 的区分能力最强，信息值（IV）最高。 缺失率存在差异：EXT_SOURCE_1 为 56.4%，EXT_SOURCE_2 为 0.2%，EXT_SOURCE_3 为 19.8%。 4.2.3 收入分布分析 1 2 3 4 5 6 7 8 9 10 # Income distribution with log transformation train_df[\u0026#39;AMT_INCOME_TOTAL_LOG\u0026#39;] = np.log1p(train_df[\u0026#39;AMT_INCOME_TOTAL\u0026#39;]) # Descriptive statistics print(train_df[\u0026#39;AMT_INCOME_TOTAL\u0026#39;].describe()) # Detect extreme outliers q99 = train_df[\u0026#39;AMT_INCOME_TOTAL\u0026#39;].quantile(0.99) extreme_outliers = train_df[train_df[\u0026#39;AMT_INCOME_TOTAL\u0026#39;] \u0026gt; q99 * 10] print(f\u0026#34;\\nExtreme outliers (\u0026gt;10x 99th percentile): {len(extreme_outliers)}\u0026#34;) 统计概览：\n1 2 3 4 5 6 7 8 count 3.075110e+05 mean 1.687979e+05 std 2.371894e+05 min 2.565000e+04 25% 1.125000e+05 50% 1.471500e+05 75% 2.025000e+05 max 1.170000e+08 # Data quality concern 启示：分布明显右偏，偏度约为 3.2，适合考虑对数变换。最大值为 117M，而均值为 168K，这种极端差异提示可能存在需要处理的录入错误。\n4.2.4 时间特征分析：年龄与违约风险 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # Age calculation and risk profiling train_df[\u0026#39;AGE_YEARS\u0026#39;] = -train_df[\u0026#39;DAYS_BIRTH\u0026#39;] / 365.25 # Binned analysis train_df[\u0026#39;AGE_BIN\u0026#39;] = pd.cut( train_df[\u0026#39;AGE_YEARS\u0026#39;], bins=[0, 25, 30, 35, 40, 45, 50, 60, 100], labels=[\u0026#39;\u0026lt;25\u0026#39;, \u0026#39;25-30\u0026#39;, \u0026#39;30-35\u0026#39;, \u0026#39;35-40\u0026#39;, \u0026#39;40-45\u0026#39;, \u0026#39;45-50\u0026#39;, \u0026#39;50-60\u0026#39;, \u0026#39;60+\u0026#39;] ) default_by_age = train_df.groupby(\u0026#39;AGE_BIN\u0026#39;)[\u0026#39;TARGET\u0026#39;].agg([\u0026#39;mean\u0026#39;, \u0026#39;count\u0026#39;]) print(default_by_age) # Visualization plt.figure(figsize=(10, 6)) default_by_age[\u0026#39;mean\u0026#39;].plot(kind=\u0026#39;bar\u0026#39;, color=\u0026#39;steelblue\u0026#39;) plt.title(\u0026#39;Default Rate by Age Cohort\u0026#39;) plt.xlabel(\u0026#39;Age Group\u0026#39;) plt.ylabel(\u0026#39;Default Rate\u0026#39;) plt.axhline(y=train_df[\u0026#39;TARGET\u0026#39;].mean(), color=\u0026#39;r\u0026#39;, linestyle=\u0026#39;--\u0026#39;, label=\u0026#39;Overall Average\u0026#39;) plt.legend() plt.tight_layout() plt.savefig(\u0026#39;images/default_rate_by_age.png\u0026#39;, dpi=150) 发现：违约率与年龄呈负向关系，25 岁以下客户的违约率约为 40—50 岁客户的 2.5 倍。这与信用风险理论中关于收入稳定性及金融经验的认识一致。\n4.2.5 就业状态的异常编码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # Investigate DAYS_EMPLOYED anomaly anomaly_count = (train_df[\u0026#39;DAYS_EMPLOYED\u0026#39;] == 365243).sum() anomaly_rate = anomaly_count / len(train_df) print(f\u0026#34;Anomalous DAYS_EMPLOYED (365243): {anomaly_count} ({anomaly_rate:.2%})\u0026#34;) # Compare default rates train_df[\u0026#39;EMPLOYMENT_STATUS\u0026#39;] = np.where( train_df[\u0026#39;DAYS_EMPLOYED\u0026#39;] == 365243, \u0026#39;Unemployed/Unknown\u0026#39;, \u0026#39;Employed\u0026#39; ) employment_risk = train_df.groupby(\u0026#39;EMPLOYMENT_STATUS\u0026#39;)[\u0026#39;TARGET\u0026#39;].mean() print(\u0026#34;\\nDefault Rate by Employment Status:\u0026#34;) print(employment_risk) 结果：\n1 2 3 4 5 Anomalous DAYS_EMPLOYED (365243): 55,374 (18.01%) Default Rate by Employment Status: Employed 0.0753 Unemployed/Unknown 0.1047 解读：365,243 是哨兵编码，约相当于 1,000 年，表示失业或数据不可用。该人群违约率较高（10.5% 对比 7.5%），说明这一编码具有业务意义。\n4.3 数据预处理策略 基于 EDA 发现，构建系统化的预处理流水线：\n1 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 class ApplicationDataCleaner(BaseTransformer): \u0026#34;\u0026#34;\u0026#34; Implements data quality transformations for the primary application table. \u0026#34;\u0026#34;\u0026#34; def transform(self, df: pd.DataFrame) -\u0026gt; Dict[str, pd.DataFrame]: df_cleaned = df.copy() # 1. Sentinel value treatment df_cleaned[\u0026#39;DAYS_EMPLOYED\u0026#39;].replace(365243, np.nan, inplace=True) df_cleaned[\u0026#39;CODE_GENDER\u0026#39;].replace(\u0026#39;XNA\u0026#39;, np.nan, inplace=True) # 2. Infinity value handling df_cleaned = df_cleaned.replace([np.inf, -np.inf], np.nan) # 3. Categorical missing value imputation categorical_columns = df_cleaned.select_dtypes( include=[\u0026#39;object\u0026#39;] ).columns df_cleaned[categorical_columns] = df_cleaned[ categorical_columns ].fillna(\u0026#39;Unknown\u0026#39;) # 4. Numerical features: preserve missing values # Gradient boosting models handle missing values natively return {\u0026#39;application_cleaned\u0026#39;: df_cleaned} 5. 特征工程方法 5.1 聚合问题 这个数据集的核心特征工程难点来自关系结构：同一客户在征信、历史申请和还款等附属表中拥有多条记录，而预测模型需要每位客户对应一个固定维度的特征向量。\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 Client A - Bureau Records: ├─ Record 1: SK_ID_BUREAU=101, AMT_CREDIT_SUM=5000, DAYS_CREDIT=-365 ├─ Record 2: SK_ID_BUREAU=102, AMT_CREDIT_SUM=3000, DAYS_CREDIT=-180 ├─ Record 3: SK_ID_BUREAU=103, AMT_CREDIT_SUM=8000, DAYS_CREDIT=-90 └─ Record 4: SK_ID_BUREAU=104, AMT_CREDIT_SUM=2000, DAYS_CREDIT=-30 Required Transformation (Single Row): - bureau_count: 4 - bureau_amt_sum: 18000 - bureau_amt_mean: 4500 - bureau_amt_max: 8000 - bureau_days_min: -365 - bureau_days_max: -30 5.2 聚合方法 聚合算子：\n算子 数学定义 使用场景 业务含义 COUNT \\(n = |{r_1, r_2, \u0026hellip;, r_n}|\\) 记录频次 贷款或申请次数 SUM \\(\\Sigma = \\sum_{i=1}^{n} x_i\\) 总敞口 累计债务、总还款额 MEAN \\(\\mu = \\frac{1}{n}\\sum_{i=1}^{n} x_i\\) 集中趋势 平均贷款金额 MEDIAN \\(\\tilde{x} = Q_2(x)\\) 稳健的集中趋势 收入中位数，对异常值较稳健 MAX/MIN \\(\\max(x), \\min(x)\\) 极值 最大贷款、最早记录 STD \\(\\sigma = \\sqrt{\\frac{1}{n}\\sum_{i=1}^{n}(x_i - \\mu)^2}\\) 波动程度 收入稳定性、还款一致性 NUNIQUE \\(|{x_1, x_2, \u0026hellip;}|\\) 不同值的数量 不同放贷机构的数量 5.3 各数据表的特征提取 5.3.1 征信特征工程 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 class BureauFeatureExtractor(BaseTransformer): \u0026#34;\u0026#34;\u0026#34; Extracts aggregated features from credit bureau records. \u0026#34;\u0026#34;\u0026#34; def transform(self, bureau: pd.DataFrame) -\u0026gt; Dict[str, pd.DataFrame]: # Primary aggregations bureau_agg = bureau.groupby(\u0026#39;SK_ID_CURR\u0026#39;).agg({ # Exposure metrics \u0026#39;SK_ID_BUREAU\u0026#39;: \u0026#39;count\u0026#39;, \u0026#39;AMT_CREDIT_SUM\u0026#39;: [\u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;min\u0026#39;, \u0026#39;std\u0026#39;], \u0026#39;AMT_CREDIT_SUM_DEBT\u0026#39;: [\u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;], \u0026#39;AMT_CREDIT_SUM_OVERDUE\u0026#39;: [\u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;], # Delinquency metrics \u0026#39;CNT_CREDIT_PROLONG\u0026#39;: [\u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;], \u0026#39;CREDIT_DAY_OVERDUE\u0026#39;: [\u0026#39;sum\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;mean\u0026#39;], # Temporal metrics \u0026#39;DAYS_CREDIT\u0026#39;: [\u0026#39;min\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;mean\u0026#39;], \u0026#39;DAYS_CREDIT_ENDDATE\u0026#39;: [\u0026#39;min\u0026#39;, \u0026#39;max\u0026#39;], \u0026#39;DAYS_CREDIT_UPDATE\u0026#39;: [\u0026#39;min\u0026#39;, \u0026#39;max\u0026#39;], }) # Flatten multi-level columns bureau_agg.columns = [ \u0026#39;_\u0026#39;.join(col).strip() for col in bureau_agg.columns.values ] # Active credit subset analysis active_mask = bureau[\u0026#39;CREDIT_ACTIVE\u0026#39;] == \u0026#39;Active\u0026#39; active_loans = bureau[active_mask].groupby(\u0026#39;SK_ID_CURR\u0026#39;).agg({ \u0026#39;AMT_CREDIT_SUM\u0026#39;: [\u0026#39;sum\u0026#39;, \u0026#39;count\u0026#39;], \u0026#39;AMT_CREDIT_SUM_DEBT\u0026#39;: \u0026#39;sum\u0026#39;, }) active_loans.columns = [ \u0026#39;bureau_active_\u0026#39; + \u0026#39;_\u0026#39;.join(col) for col in active_loans.columns ] # Combine feature sets features = bureau_agg.join(active_loans, how=\u0026#39;left\u0026#39;) return {\u0026#39;bureau_features\u0026#39;: features} 生成特征示例：\n1 2 3 4 5 6 7 8 9 10 { \u0026#39;SK_ID_BUREAU_count\u0026#39;: 5, # Total credit relationships \u0026#39;AMT_CREDIT_SUM_sum\u0026#39;: 45000, # Total credit exposure \u0026#39;AMT_CREDIT_SUM_mean\u0026#39;: 9000, # Average loan size \u0026#39;AMT_CREDIT_SUM_max\u0026#39;: 20000, # Maximum single exposure \u0026#39;DAYS_CREDIT_min\u0026#39;: -730, # Oldest relationship \u0026#39;DAYS_CREDIT_max\u0026#39;: -30, # Most recent relationship \u0026#39;bureau_active_AMT_CREDIT_SUM_sum\u0026#39;: 15000, # Active exposure \u0026#39;bureau_active_SK_ID_BUREAU_count\u0026#39;: 2, # Number of active accounts } 5.3.2 历史申请特征工程 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 class PreviousApplicationFeatureExtractor(BaseTransformer): \u0026#34;\u0026#34;\u0026#34; Extracts features from historical Home Credit applications. \u0026#34;\u0026#34;\u0026#34; def transform(self, prev_app: pd.DataFrame) -\u0026gt; Dict[str, pd.DataFrame]: # Core aggregations prev_agg = prev_app.groupby(\u0026#39;SK_ID_CURR\u0026#39;).agg({ # Application frequency \u0026#39;SK_ID_PREV\u0026#39;: \u0026#39;count\u0026#39;, # Approval metrics \u0026#39;NAME_CONTRACT_STATUS\u0026#39;: [ lambda x: (x == \u0026#39;Approved\u0026#39;).sum(), lambda x: (x == \u0026#39;Refused\u0026#39;).sum(), lambda x: (x == \u0026#39;Canceled\u0026#39;).sum() ], # Financial metrics \u0026#39;AMT_APPLICATION\u0026#39;: [\u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;min\u0026#39;], \u0026#39;AMT_CREDIT\u0026#39;: [\u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;], \u0026#39;AMT_DOWN_PAYMENT\u0026#39;: [\u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;], \u0026#39;AMT_ANNUITY\u0026#39;: [\u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;], # Pricing metrics \u0026#39;RATE_INTEREST_PRIMARY\u0026#39;: [\u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;], \u0026#39;RATE_DOWN_PAYMENT\u0026#39;: [\u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;], # Temporal metrics \u0026#39;DAYS_DECISION\u0026#39;: [\u0026#39;min\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;mean\u0026#39;], }) # Derived metrics total_apps = prev_agg[(\u0026#39;SK_ID_PREV\u0026#39;, \u0026#39;count\u0026#39;)] approved_apps = prev_agg[(\u0026#39;NAME_CONTRACT_STATUS\u0026#39;, \u0026#39;\u0026lt;lambda_0\u0026gt;\u0026#39;)] prev_agg[\u0026#39;approval_rate\u0026#39;] = approved_apps / total_apps prev_agg[\u0026#39;credit_to_application_ratio\u0026#39;] = ( prev_agg[(\u0026#39;AMT_CREDIT\u0026#39;, \u0026#39;sum\u0026#39;)] / prev_agg[(\u0026#39;AMT_APPLICATION\u0026#39;, \u0026#39;sum\u0026#39;)] ) # Flatten column structure prev_agg.columns = [ \u0026#39;_\u0026#39;.join(col).strip() if isinstance(col, tuple) else col for col in prev_agg.columns ] return {\u0026#39;previous_application_features\u0026#39;: prev_agg} 关键衍生特征：\napproval_rate：历史获批概率。 credit_to_application_ratio：获批金额与申请金额的比率。 avg_down_payment_rate：典型首付行为。 5.3.3 分期还款特征工程 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 class InstallmentFeatureExtractor(BaseTransformer): \u0026#34;\u0026#34;\u0026#34; Extracts repayment behavior features from installment records. \u0026#34;\u0026#34;\u0026#34; def transform(self, installments: pd.DataFrame) -\u0026gt; Dict[str, pd.DataFrame]: # Calculate derived metrics installments[\u0026#39;DPD\u0026#39;] = ( installments[\u0026#39;DAYS_ENTRY_PAYMENT\u0026#39;] - installments[\u0026#39;DAYS_INSTALMENT\u0026#39;] ) installments[\u0026#39;AMT_DIFF\u0026#39;] = ( installments[\u0026#39;AMT_PAYMENT\u0026#39;] - installments[\u0026#39;AMT_INSTALMENT\u0026#39;] ) # Aggregations install_agg = installments.groupby(\u0026#39;SK_ID_CURR\u0026#39;).agg({ # Volume metrics \u0026#39;NUM_INSTALMENT_VERSION\u0026#39;: \u0026#39;count\u0026#39;, # Delinquency metrics \u0026#39;DPD\u0026#39;: [\u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;sum\u0026#39;, lambda x: (x \u0026gt; 0).sum()], # Payment amount metrics \u0026#39;AMT_INSTALMENT\u0026#39;: [\u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;], \u0026#39;AMT_PAYMENT\u0026#39;: [\u0026#39;sum\u0026#39;, \u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;], \u0026#39;AMT_DIFF\u0026#39;: [ \u0026#39;mean\u0026#39;, \u0026#39;sum\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;min\u0026#39;, lambda x: (x \u0026gt; 0).sum() ], }) # Flatten columns install_agg.columns = [ \u0026#39;_\u0026#39;.join(col).strip() for col in install_agg.columns.values ] return {\u0026#39;installment_features\u0026#39;: install_agg} 关键衍生指标：\nDPD_mean：平均逾期天数。 DPD_max：最严重的一次逾期。 AMT_DIFF_mean：平均还款金额偏差，反映多还或少还。 5.4 时间窗口特征 假设：近期行为比历史平均水平包含更强的预测信号。\n1 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 class TemporalWindowFeatureExtractor(BaseTransformer): \u0026#34;\u0026#34;\u0026#34; Extracts time-windowed aggregations for trend analysis. \u0026#34;\u0026#34;\u0026#34; def transform( self, data: pd.DataFrame, time_col: str = \u0026#39;MONTHS_BALANCE\u0026#39; ) -\u0026gt; Dict[str, pd.DataFrame]: window_sizes = [3, 6, 12, 24] # months all_features = {} for window in window_sizes: # Subset to recent history recent_mask = data[time_col] \u0026gt;= -window recent_data = data[recent_mask] # Window-specific aggregations window_agg = recent_data.groupby(\u0026#39;SK_ID_CURR\u0026#39;).agg({ \u0026#39;AMT_BALANCE\u0026#39;: [\u0026#39;mean\u0026#39;, \u0026#39;max\u0026#39;, \u0026#39;sum\u0026#39;], \u0026#39;SK_ID_PREV\u0026#39;: \u0026#39;count\u0026#39;, }) # Rename with window suffix window_agg.columns = [ f\u0026#39;{col}_last_{window}m\u0026#39; for col in window_agg.columns ] all_features[f\u0026#39;window_{window}m\u0026#39;] = window_agg return all_features 5.5 类别变量编码 编码策略选择：\n方法 适用情况 优点 缺点 标签编码 有序类别，如教育水平 简单、维度低 对无序类别引入虚假顺序 独热编码 低基数无序类别，如性别 不假设类别有序 可能导致维度膨胀 目标编码 高基数类别，如职业、地区 捕捉与目标的关系 存在过拟合风险 频率编码 高基数标识符 简单，可反映出现频率 存在信息损失 实现：\n1 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 from category_encoders import TargetEncoder, OneHotEncoder class CategoricalEncodingPipeline(BaseTransformer): \u0026#34;\u0026#34;\u0026#34; Applies appropriate encoding strategies by variable type. \u0026#34;\u0026#34;\u0026#34; def __init__(self): self.encoders = {} def fit(self, X: pd.DataFrame, y: pd.Series): # Target encoding for high-cardinality features high_cardinality = [ \u0026#39;OCCUPATION_TYPE\u0026#39;, \u0026#39;ORGANIZATION_TYPE\u0026#39;, \u0026#39;NAME_FAMILY_STATUS\u0026#39; ] for col in high_cardinality: encoder = TargetEncoder(cols=[col], smoothing=10.0) encoder.fit(X[[col]], y) self.encoders[col] = encoder return self def transform(self, X: pd.DataFrame) -\u0026gt; Dict[str, pd.DataFrame]: X_encoded = X.copy() for col, encoder in self.encoders.items(): X_encoded[col] = encoder.transform(X[[col]]) return {\u0026#39;features_encoded\u0026#39;: X_encoded} 5.6 特征选择 目标：降低维度、消除噪声、提高训练效率。\n1 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 from sklearn.feature_selection import mutual_info_classif, SelectKBest class FeatureSelectionTransformer(BaseTransformer): \u0026#34;\u0026#34;\u0026#34; Selects top-K features based on mutual information. \u0026#34;\u0026#34;\u0026#34; def __init__(self, k: int = 500): self.k = k self.selector = None def fit(self, X: pd.DataFrame, y: pd.Series): self.selector = SelectKBest( score_func=mutual_info_classif, k=self.k ) self.selector.fit(X, y) self.selected_features = X.columns[ self.selector.get_support() ].tolist() return self def transform(self, X: pd.DataFrame) -\u0026gt; Dict[str, pd.DataFrame]: X_selected = X[self.selected_features] return { \u0026#39;features\u0026#39;: X_selected, \u0026#39;feature_names\u0026#39;: self.selected_features } 6. 模型选择、训练与评估 6.1 梯度提升决策树 理论基础：\n梯度提升通过函数空间中的梯度下降，构建弱学习器（通常为决策树）的加性集成：\n\\[F_m(x) = F_{m-1}(x) + \\nu \\cdot h_m(x)\\]其中，$h_m(x)$ 是针对伪残差拟合的弱学习器：\n\\[r_{im} = -\\left[\\frac{\\partial L(y_i, F(x_i))}{\\partial F(x_i)}\\right]_{F=F_{m-1}}\\]处理表格数据的优势：\n自动建模特征交互：树分裂天然能够表示特征组合。 缺失值处理：原生支持缺失值，无需预先填补。 非线性表达能力：捕捉复杂的决策边界。 可解释性：支持特征重要性和部分依赖分析。 6.2 LightGBM、XGBoost 与 CatBoost 对比 算法特性：\n特性 LightGBM XGBoost CatBoost 树生长方式 按叶生长 按层生长 按层生长 分裂点搜索 基于直方图 直方图 + 精确搜索 对称树 关键优化 GOSS、EFB 缓存感知访问 有序提升 类别特征支持 有限支持 手动编码 原生支持 训练速度 最快 中等 中等 内存效率 最优 中等 良好 基于梯度的单边采样（GOSS）：LightGBM 保留梯度较大、误差较高的样本，同时随机抽取小梯度样本，在维持数据分布的同时加快训练。\n互斥特征捆绑（EFB）：LightGBM 将很少同时为非零值的互斥特征捆绑，在不丢失信息的前提下降低维度。\n有序提升（Ordered Boosting）：CatBoost 通过训练数据的有序排列消除预测偏移，提供无偏梯度估计。\n选择建议：\n快速实验：LightGBM，具有 10 倍训练速度优势。 追求最高准确性：XGBoost，提升幅度较小但较稳定。 类别数据丰富：CatBoost，原生处理类别特征。 6.3 LightGBM 实现 超参数配置：\n1 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 import lightgbm as lgb # Model configuration LGBM_PARAMS = { \u0026#39;objective\u0026#39;: \u0026#39;binary\u0026#39;, \u0026#39;metric\u0026#39;: \u0026#39;auc\u0026#39;, \u0026#39;boosting_type\u0026#39;: \u0026#39;gbdt\u0026#39;, # Tree structure \u0026#39;num_leaves\u0026#39;: 35, \u0026#39;max_depth\u0026#39;: -1, \u0026#39;min_child_samples\u0026#39;: 70, # Learning dynamics \u0026#39;learning_rate\u0026#39;: 0.02, \u0026#39;n_estimators\u0026#39;: 5000, # Regularization \u0026#39;reg_lambda\u0026#39;: 100.0, \u0026#39;reg_alpha\u0026#39;: 0.0, # Sampling \u0026#39;subsample\u0026#39;: 1.0, \u0026#39;colsample_bytree\u0026#39;: 0.03, # Categorical handling \u0026#39;categorical_feature\u0026#39;: \u0026#39;auto\u0026#39;, \u0026#39;verbose\u0026#39;: -1, \u0026#39;random_state\u0026#39;: 42 } 训练过程：\n1 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 from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score # Data preparation X_train, X_valid, y_train, y_valid = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) # Dataset construction train_dataset = lgb.Dataset(X_train, label=y_train) valid_dataset = lgb.Dataset(X_valid, label=y_valid, reference=train_dataset) # Model training model = lgb.train( LGBM_PARAMS, train_dataset, num_boost_round=5000, valid_sets=[train_dataset, valid_dataset], valid_names=[\u0026#39;train\u0026#39;, \u0026#39;valid\u0026#39;], callbacks=[ lgb.early_stopping(stopping_rounds=100), lgb.log_evaluation(period=100) ] ) # Performance evaluation y_pred = model.predict(X_valid, num_iteration=model.best_iteration) validation_auc = roc_auc_score(y_valid, y_pred) print(f\u0026#39;Validation AUC: {validation_auc:.4f}\u0026#39;) # Feature importance analysis importance_df = pd.DataFrame({ \u0026#39;feature\u0026#39;: model.feature_name(), \u0026#39;importance_gain\u0026#39;: model.feature_importance(importance_type=\u0026#39;gain\u0026#39;), \u0026#39;importance_split\u0026#39;: model.feature_importance(importance_type=\u0026#39;split\u0026#39;) }).sort_values(\u0026#39;importance_gain\u0026#39;, ascending=False) print(\u0026#34;\\nTop 20 Features by Gain:\u0026#34;) print(importance_df.head(20)) 超参数调整建议：\nnum_leaves：以 \\(2^{\\text{max\\_depth}}\\) 为参考基准，适当降低以控制过拟合。 learning_rate：通常在 0.01—0.1 范围内，较低的值需要更多迭代。 reg_lambda：对于噪声较多的数据，可从 1 增至 100。 colsample_bytree：对于高维特征，可从 1.0 降至 0.3。 6.4 XGBoost 实现 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 import xgboost as xgb XGB_PARAMS = { \u0026#39;objective\u0026#39;: \u0026#39;binary:logistic\u0026#39;, \u0026#39;eval_metric\u0026#39;: \u0026#39;auc\u0026#39;, \u0026#39;max_depth\u0026#39;: 6, \u0026#39;learning_rate\u0026#39;: 0.05, \u0026#39;subsample\u0026#39;: 0.8, \u0026#39;colsample_bytree\u0026#39;: 0.8, \u0026#39;reg_alpha\u0026#39;: 0.1, \u0026#39;reg_lambda\u0026#39;: 1.0, \u0026#39;tree_method\u0026#39;: \u0026#39;hist\u0026#39;, \u0026#39;seed\u0026#39;: 42 } # DMatrix construction dtrain = xgb.DMatrix(X_train, label=y_train, enable_categorical=True) dvalid = xgb.DMatrix(X_valid, label=y_valid, enable_categorical=True) # Training eval_results = {} model = xgb.train( XGB_PARAMS, dtrain, num_boost_round=1000, evals=[(dtrain, \u0026#39;train\u0026#39;), (dvalid, \u0026#39;valid\u0026#39;)], evals_result=eval_results, early_stopping_rounds=100, verbose_eval=100 ) # Evaluation y_pred = model.predict(dvalid) auc = roc_auc_score(y_valid, y_pred) 6.5 CatBoost 实现 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 from catboost import CatBoostClassifier, Pool # Identify categorical features categorical_features = [ i for i, col in enumerate(X_train.columns) if X_train[col].dtype == \u0026#39;object\u0026#39; ] # Data pools train_pool = Pool(X_train, y_train, cat_features=categorical_features) valid_pool = Pool(X_valid, y_valid, cat_features=categorical_features) # Model configuration model = CatBoostClassifier( iterations=1000, learning_rate=0.05, depth=6, l2_leaf_reg=3.0, early_stopping_rounds=100, verbose=100, random_seed=42 ) # Training model.fit(train_pool, eval_set=valid_pool) # Evaluation y_pred = model.predict_proba(valid_pool)[:, 1] auc = roc_auc_score(y_valid, y_pred) 6.6 交叉验证与折外预测 使用交叉验证的原因：\n稳定性评估：降低单次训练集与测试集划分带来的方差。 防止过拟合：验证模型的泛化能力。 生成 OOF：为模型集成提供无偏的折外预测。 分层 K 折实现：\n1 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 from sklearn.model_selection import StratifiedKFold N_FOLDS = 5 kf = StratifiedKFold(n_splits=N_FOLDS, shuffle=True, random_state=42) oof_predictions = np.zeros(len(X_train)) test_predictions = np.zeros(len(X_test)) fold_scores = [] for fold, (train_idx, valid_idx) in enumerate(kf.split(X_train, y_train)): print(f\u0026#39;\\nFold {fold + 1}/{N_FOLDS}\u0026#39;) # Data partitioning X_tr, X_val = X_train.iloc[train_idx], X_train.iloc[valid_idx] y_tr, y_val = y_train.iloc[train_idx], y_train.iloc[valid_idx] # Model training model = lgb.LGBMClassifier(**LGBM_PARAMS) model.fit( X_tr, y_tr, eval_set=[(X_val, y_val)], callbacks=[lgb.early_stopping(100), lgb.log_evaluation(0)] ) # Out-of-fold predictions oof_predictions[valid_idx] = model.predict_proba(X_val)[:, 1] # Test set predictions (ensemble across folds) test_predictions += model.predict_proba(X_test)[:, 1] / N_FOLDS # Fold-level evaluation fold_auc = roc_auc_score(y_val, oof_predictions[valid_idx]) fold_scores.append(fold_auc) print(f\u0026#39;Fold AUC: {fold_auc:.4f}\u0026#39;) # Aggregate performance overall_auc = roc_auc_score(y_train, oof_predictions) print(f\u0026#39;\\nOverall OOF AUC: {overall_auc:.4f} (+/- {np.std(fold_scores):.4f})\u0026#39;) 7. 集成学习与模型融合 7.1 集成学习理论 单模型的局限：\n不同模型存在各自的不足：\nLightGBM：容易在稀疏特征上过拟合。 XGBoost：训练计算成本较高。 CatBoost：为获得稳健性，可能牺牲少量准确性。 集成的优势：\n降低方差：平均预测有助于减少波动。 降低偏差：不同模型捕捉互补模式。 稳定性：减轻单个模型失效的影响。 7.2 两层堆叠架构 架构说明：\n第 1 层：基学习器，采用不同的梯度提升实现。\nLightGBM：按叶优化。 XGBoost：按层生长，使用精确贪心算法。 CatBoost：有序提升。 第 2 层：元学习器，采用简单的线性模型。\n逻辑回归或岭回归。 原因：基学习器已提取充分信号，复杂的元学习器可能过拟合。 7.3 折外预测的生成 关键约束：训练元学习器所用的预测，必须来自未使用该目标样本训练的基模型，以防止数据泄漏。\n数据泄漏提醒：\n1 2 3 # INCORRECT: Training set predictions (data leakage) model.fit(X_train, y_train) train_pred = model.predict(X_train) # Model has seen these instances! 正确生成 OOF：\n1 2 3 4 5 6 7 8 9 10 11 12 from sklearn.model_selection import StratifiedKFold kf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) oof_preds = np.zeros(len(X_train)) for train_idx, valid_idx in kf.split(X_train, y_train): X_tr, X_val = X_train[train_idx], X_train[valid_idx] y_tr = y_train[train_idx] model.fit(X_tr, y_tr) # Predict on held-out validation set only oof_preds[valid_idx] = model.predict_proba(X_val)[:, 1] 7.4 Stacking 实现 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 from sklearn.linear_model import LogisticRegression from typing import Dict, Tuple class StackingEnsemble: \u0026#34;\u0026#34;\u0026#34; Two-level stacking ensemble with OOF prediction generation. \u0026#34;\u0026#34;\u0026#34; def __init__( self, base_models: Dict[str, object], meta_learner: object ): self.base_models = base_models self.meta_learner = meta_learner self.base_predictions = {} def fit( self, X: pd.DataFrame, y: pd.Series, cv: int = 5 ) -\u0026gt; \u0026#39;StackingEnsemble\u0026#39;: \u0026#34;\u0026#34;\u0026#34; Generate OOF predictions and train meta-learner. \u0026#34;\u0026#34;\u0026#34; kf = StratifiedKFold( n_splits=cv, shuffle=True, random_state=42 ) # Matrix to store OOF predictions n_models = len(self.base_models) oof_features = np.zeros((len(X), n_models)) # Generate OOF predictions for each base model for idx, (name, model) in enumerate(self.base_models.items()): print(f\u0026#39;Generating OOF predictions: {name}...\u0026#39;) for train_idx, valid_idx in kf.split(X, y): X_tr = X.iloc[train_idx] X_val = X.iloc[valid_idx] y_tr = y.iloc[train_idx] # Fit on training fold model.fit(X_tr, y_tr) # Predict on validation fold oof_features[valid_idx, idx] = ( model.predict_proba(X_val)[:, 1] ) self.base_predictions[name] = oof_features[:, idx].copy() # Train meta-learner on OOF features print(\u0026#39;Training meta-learner...\u0026#39;) self.meta_learner.fit(oof_features, y) # Retrain base models on full dataset print(\u0026#39;Retraining base models on full data...\u0026#39;) for name, model in self.base_models.items(): model.fit(X, y) return self def predict(self, X: pd.DataFrame) -\u0026gt; np.ndarray: \u0026#34;\u0026#34;\u0026#34; Generate ensemble predictions. \u0026#34;\u0026#34;\u0026#34; # Generate base model predictions n_models = len(self.base_models) base_features = np.zeros((len(X), n_models)) for idx, (name, model) in enumerate(self.base_models.items()): base_features[:, idx] = model.predict_proba(X)[:, 1] # Meta-learner prediction return self.meta_learner.predict_proba(base_features)[:, 1] # Usage base_models = { \u0026#39;lightgbm\u0026#39;: lgb.LGBMClassifier(**lgb_params), \u0026#39;xgboost\u0026#39;: xgb.XGBClassifier(**xgb_params), \u0026#39;catboost\u0026#39;: CatBoostClassifier(**ctb_params, verbose=0) } meta_model = LogisticRegression( C=1.0, solver=\u0026#39;lbfgs\u0026#39;, max_iter=1000 ) ensemble = StackingEnsemble(base_models, meta_model) ensemble.fit(X_train, y_train) final_predictions = ensemble.predict(X_test) 7.5 超参数优化 方法对比：\n方法 策略 优势 局限 计算成本 网格搜索 穷举所有组合 覆盖全面 规模呈指数增长 高 随机搜索 随机采样 探索效率较高 可能遗漏较优组合 中等 贝叶斯优化 概率代理模型 样本利用效率高 实现较复杂 低至中等 贝叶斯优化实现：\n1 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 from skopt import BayesSearchCV from skopt.space import Real, Integer # Define search space search_spaces = { \u0026#39;num_leaves\u0026#39;: Integer(20, 50), \u0026#39;learning_rate\u0026#39;: Real(0.01, 0.1, prior=\u0026#39;log-uniform\u0026#39;), \u0026#39;min_child_samples\u0026#39;: Integer(10, 100), \u0026#39;reg_lambda\u0026#39;: Real(1e-8, 10.0, prior=\u0026#39;log-uniform\u0026#39;), \u0026#39;subsample\u0026#39;: Real(0.5, 1.0), \u0026#39;colsample_bytree\u0026#39;: Real(0.3, 1.0) } # Bayesian optimization opt = BayesSearchCV( lgb.LGBMClassifier( objective=\u0026#39;binary\u0026#39;, metric=\u0026#39;auc\u0026#39;, boosting_type=\u0026#39;gbdt\u0026#39;, n_estimators=1000, verbose=-1 ), search_spaces, n_iter=50, scoring=\u0026#39;roc_auc\u0026#39;, cv=3, n_jobs=-1, random_state=42, verbose=1 ) opt.fit(X_train, y_train) print(f\u0026#39;Best CV Score: {opt.best_score_:.4f}\u0026#39;) print(f\u0026#39;Optimal Parameters: {opt.best_params_}\u0026#39;) 7.6 性能汇总 模型配置 交叉验证 AUC 公开排行榜 私有排行榜 相对提升 LightGBM（单模型） 0.7902 0.791 0.792 基线 XGBoost（单模型） 0.7854 0.787 0.788 -0.004 CatBoost（单模型） 0.7881 0.789 0.790 -0.002 简单平均 0.7920 0.793 0.794 +0.002 Stacking（LGB+XGB+CTB+LR） 0.8053 0.807 0.808 +0.016 主要认识：\n堆叠集成相对最佳单模型提升了 1.6 个百分点。 在 Kaggle 排行榜中，AUC 提升 0.01 往往对应数百名的排名变化。 简单的逻辑回归元学习器通过减少过拟合，优于更复杂的替代方案。 8. 结论与最佳实践 8.1 完整技术流程回顾 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Raw Data (7 tables, 50M+ records) ↓ Data Preprocessing ├─ Anomaly detection and treatment ├─ Missing value imputation └─ Categorical encoding ↓ Feature Engineering (1,000+ features) ├─ Aggregation operations (count/sum/mean/max/std) ├─ Temporal window features (3m/6m/12m/24m) ├─ Ratio and interaction features └─ Target encoding for high-cardinality variables ↓ Model Development ├─ LightGBM (primary model) ├─ XGBoost (accuracy complement) └─ CatBoost (robustness validation) ↓ Ensemble Construction ├─ Out-of-fold prediction generation ├─ Meta-learner training └─ Final prediction aggregation ↓ Submission (AUC 0.808, Top 5%) 8.2 核心技术认识 数据架构：\n关系型数据库结构需要系统化的聚合策略。 一对多关系需要谨慎提取特征，避免信息损失。 时间序列比静态快照提供更丰富的信号。 特征工程：\n领域知识从根本上决定了特征构造的可能空间。 均值、中位数和最大值等不同聚合函数，隐含不同的业务假设。 时间窗口特征比历史平均值更能捕捉行为趋势。 建模策略：\n梯度提升仍是结构化数据预测的领先方法。 交叉验证兼顾稳定性评估和集成准备。 堆叠集成能够带来持续、显著的性能提升。 8.3 可复现的工程实践 流水线架构：模块化设计便于组件测试与替换。 配置管理：集中管理参数，便于实验追踪。 开发模式：通过子采样策略（--dev_mode）缩短迭代周期。 实验追踪：系统记录实验日志，避免重复计算。 8.4 后续研究方向 短期优化：1—2 周\n探索人工设定之外的特征交互。 根据验证集表现优化加权集成。 细化超参数搜索空间。 中期扩展：1—2 个月\n使用深度学习提取特征，例如自编码器表示。 使用图神经网络对关系数据建模。 通过 SHAP 值分解分析模型可解释性。 长期探索：3 个月以上\n构建在线学习系统，适应分布漂移。 建设用于生产模型验证的 A/B 测试框架。 使用联邦学习架构开展跨机构协作。 8.5 推荐资源 官方文档：\nLightGBM 文档 XGBoost 文档 CatBoost 文档 代表性论文：\nChen, T., \u0026amp; Guestrin, C. (2016). XGBoost: A Scalable Tree Boosting System. KDD. Ke, G., et al. (2017). LightGBM: A Highly Efficient Gradient Boosting Decision Tree. NIPS. Prokhorenkova, L., et al. (2018). CatBoost: Unbiased Boosting with Categorical Features. NIPS. 竞赛资源：\nKaggle 竞赛讨论区 优秀方案总结 总结 本文对 Home Credit Default Risk 竞赛方案的分析，展示了机器学习方法在实际信用风险评估中的系统应用。项目的价值不仅在于取得的 AUC（0.808），还体现在：\n工程规范：流水线架构保证可复现性和可维护性。 以数据为中心：探索性分析直接指导特征工程决策。 系统优化：从单模型逐步改进为复杂集成。 基本原则：\n特征工程决定理论性能上限，机器学习算法则逐步逼近这一上限。持续投入数据理解与特征构建，通常比仅调整超参数更有效。\n本文的方法也可迁移到以下相关领域：\n保险欺诈检测。 营销响应建模。 客户流失预测。 信用评分系统开发。 本文对 Kaggle Home Credit Default Risk 竞赛方案进行了完整技术分析。开源实现见 GitHub。\n","permalink":"https://liugangjian.github.io/zh/posts/kaggle-home-credit-credit-risk-prediction-guide/","summary":"系统分析 Kaggle Home Credit Default Risk 竞赛方案，梳理从数据预处理、特征工程到模型集成的完整机器学习流程。","title":"信用风险预测技术指南"},{"content":" Gavin Liu 信用风险建模与金融科技 GitHub 邮件 LinkedIn 工作经历 平安数字银行 数据建模师 2026年8月 — 至今 湖南财信金控 项目经理（借调） 2024年6月 — 2026年8月 湖南数据产业集团 软件开发工程师 2021年7月 — 2024年6月 字节跳动 软件开发工程师（实习） ✦ 因出色表现获得转正录用机会 2020年6月 — 2021年6月 教育经历 清华大学 工程管理硕士 2024 — 2027（预计） 北京大学 软件工程学士 2019 — 2021 荣誉与奖项 MCM/ICM 一等奖（Meritorious Winner） 学生指导 2018、2020 CUMCM 全国二等奖 省级一等奖 2018 发表论文 转向计算机科学之前，在材料科学领域的早期研究。\n[1] Yin P, Zheng T, Wu Y, Liu G J, et al. Achieving efficient thick active layer and large area ternary polymer solar cells by incorporating a new fused heptacyclic non-fullerene acceptor[J]. Journal of Materials Chemistry A, 2018, 6(41): 20313-20326. DOI:10.1039/C8TA06836D. [2] Wang J, Yin P, Wu Y, Liu G J, et al. Synthesis and optoelectronic property manipulation of conjugated polymer photovoltaic materials based on benzo[d]-dithieno[3,2-b;2′,3′-f]azepine[J]. Polymer, 2018, 147(000): 12. ","permalink":"https://liugangjian.github.io/zh/about/","summary":"\u003cdiv class=\"resume-header\"\u003e\n    \u003cdiv class=\"resume-avatar\"\u003e\n        \u003c!-- Replace with your actual avatar URL --\u003e\n        \u003cimg src=\"https://github.com/liugangjian.png\" alt=\"Gangjian Liu\"\u003e\n    \u003c/div\u003e\n    \u003cdiv class=\"resume-info\"\u003e\n        \u003ch2\u003eGavin Liu\u003c/h2\u003e\n        \u003cdiv class=\"tagline\"\u003e信用风险建模与金融科技\u003c/div\u003e\n        \u003cdiv class=\"social-links\"\u003e\n            \u003ca href=\"https://github.com/liugangjian\" aria-label=\"GitHub\" title=\"GitHub\" target=\"_blank\" rel=\"noopener noreferrer me\"\u003e\n                \n        \u003csvg class=\"social-mark\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\" focusable=\"false\"\u003e\u003cpath d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8\"/\u003e\u003c/svg\u003e\n        \n                \u003cspan\u003eGitHub\u003c/span\u003e\n            \u003c/a\u003e\n            \u003ca href=\"mailto:liuangjian@pku.edu.cn\" aria-label=\"邮件\" title=\"邮件\"\u003e\n                \n        \u003csvg class=\"social-mark\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\" focusable=\"false\"\u003e\u003cpath d=\"M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1zm13 2.383-4.708 2.825L15 11.105zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741M1 11.105l4.708-2.897L1 5.383z\"/\u003e\u003c/svg\u003e\n        \n                \u003cspan\u003e邮件\u003c/span\u003e\n            \u003c/a\u003e\n            \u003ca href=\"https://www.linkedin.com/in/gavin-liu-2791931a0/\" aria-label=\"LinkedIn\" title=\"LinkedIn\" target=\"_blank\" rel=\"noopener noreferrer me\"\u003e\n                \n        \u003csvg class=\"social-mark\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\" focusable=\"false\"\u003e\u003cpath d=\"M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z\"/\u003e\u003c/svg\u003e\n        \n                \u003cspan\u003eLinkedIn\u003c/span\u003e\n            \u003c/a\u003e\n        \u003c/div\u003e\n\n    \u003c/div\u003e\n\u003c/div\u003e\n\n\u003cdiv class=\"resume-section\"\u003e\n    \u003ch2\u003e工作经历\u003c/h2\u003e\n    \u003cdiv class=\"timeline\"\u003e\n\n         \u003cdiv class=\"timeline-item\"\u003e\n            \u003cimg src=\"/images/about/paobank_logo.jpeg\" alt=\"Ping An Digital Bank logo\" class=\"timeline-logo\"\u003e\n            \u003cdiv class=\"timeline-content\"\u003e\n                \u003ch3\u003e平安数字银行\u003c/h3\u003e\n                \u003cdiv class=\"role\"\u003e数据建模师\u003c/div\u003e\n                \u003cdiv class=\"date\"\u003e2026年8月 — 至今\u003c/div\u003e\n            \u003c/div\u003e\n        \u003c/div\u003e\n\n        \u003cdiv class=\"timeline-item\"\u003e\n            \u003cimg src=\"/images/about/cf.png\" alt=\"Company Logo\" class=\"timeline-logo\"\u003e\n            \u003cdiv class=\"timeline-content\"\u003e\n                \u003ch3\u003e湖南财信金控\u003c/h3\u003e\n                \u003cdiv class=\"role\"\u003e项目经理（借调）\u003c/div\u003e\n                \u003cdiv class=\"date\"\u003e2024年6月 — 2026年8月\u003c/div\u003e\n            \u003c/div\u003e\n        \u003c/div\u003e\n\n\n\n        \u003cdiv class=\"timeline-item\"\u003e\n            \u003cimg src=\"/images/about/cf.png\" alt=\"Lab Logo\" class=\"timeline-logo\"\u003e\n            \u003cdiv class=\"timeline-content\"\u003e\n                \u003ch3\u003e湖南数据产业集团\u003c/h3\u003e\n                \u003cdiv class=\"role\"\u003e软件开发工程师\u003c/div\u003e\n                \u003cdiv class=\"date\"\u003e2021年7月 — 2024年6月\u003c/div\u003e\n            \u003c/div\u003e\n        \u003c/div\u003e\n\n        \u003cdiv class=\"timeline-item\"\u003e\n            \u003cimg src=\"/images/about/bytedance.png\" alt=\"Lab Logo\" class=\"timeline-logo\"\u003e\n            \u003cdiv class=\"timeline-content\"\u003e\n                \u003ch3\u003e字节跳动\u003c/h3\u003e\n                \u003cdiv class=\"role\"\u003e软件开发工程师（实习）\u003c/div\u003e\n                \u003cspan class=\"highlight-note\"\u003e✦ 因出色表现获得转正录用机会\u003c/span\u003e\n                \u003cdiv class=\"date\"\u003e2020年6月 — 2021年6月\u003c/div\u003e\n            \u003c/div\u003e\n        \u003c/div\u003e\n\n    \u003c/div\u003e\n\u003c/div\u003e\n\n\u003cdiv class=\"resume-section\"\u003e\n    \u003ch2\u003e教育经历\u003c/h2\u003e\n    \u003cdiv class=\"timeline\"\u003e\n\n        \u003cdiv class=\"timeline-item\"\u003e\n            \u003cimg src=\"/images/about/thu.svg\" alt=\"清华大学校徽\" class=\"timeline-logo\"\u003e\n            \u003cdiv class=\"timeline-content\"\u003e\n                \u003ch3\u003e清华大学\u003c/h3\u003e\n                \u003cdiv class=\"role\"\u003e工程管理硕士\u003c/div\u003e\n                \u003cdiv class=\"date\"\u003e2024 — 2027（预计）\u003c/div\u003e\n            \u003c/div\u003e\n        \u003c/div\u003e\n\n        \u003cdiv class=\"timeline-item\"\u003e\n            \u003cimg src=\"/images/about/pku.png\" alt=\"University Logo\" class=\"timeline-logo\"\u003e\n            \u003cdiv class=\"timeline-content\"\u003e\n                \u003ch3\u003e北京大学\u003c/h3\u003e\n                \u003cdiv class=\"role\"\u003e软件工程学士\u003c/div\u003e\n                \u003cdiv class=\"date\"\u003e2019 — 2021\u003c/div\u003e\n            \u003c/div\u003e\n        \u003c/div\u003e\n\n    \u003c/div\u003e\n\u003c/div\u003e\n\n\u003cdiv class=\"resume-section\"\u003e\n    \u003ch2\u003e荣誉与奖项\u003c/h2\u003e\n    \u003cdiv class=\"awards-grid\"\u003e\n        \u003c!-- \u003cdiv class=\"award-card\"\u003e\n            \u003cimg src=\"/images/about/k.png\" alt=\"Kaggle\" class=\"award-icon\"\u003e\n            \u003cdiv class=\"award-info\"\u003e\n                \u003cdiv class=\"award-title\"\u003eKaggle Competition\u003c/div\u003e\n                \u003cdiv class=\"award-badge\"\u003e🥇 Gold Medalist\u003c/div\u003e\n                \u003cdiv class=\"award-date\"\u003e2026\u003c/div\u003e\n            \u003c/div\u003e\n        \u003c/div\u003e --\u003e\n        \u003cdiv class=\"award-card award-card--gold\"\u003e\n            \u003cimg src=\"/images/about/comap.png\" alt=\"COMAP\" class=\"award-icon\"\u003e\n            \u003cdiv class=\"award-info\"\u003e\n                \u003ch3 class=\"award-title\"\u003eMCM/ICM\u003c/h3\u003e\n                \u003cdiv class=\"award-results\"\u003e\n                    \u003cdiv class=\"award-badge\"\u003e一等奖（Meritorious Winner）\u003c/div\u003e\n                    \u003cdiv class=\"award-badge\"\u003e学生指导\u003c/div\u003e\n                \u003c/div\u003e\n                \u003cdiv class=\"award-date\"\u003e2018、2020\u003c/div\u003e\n            \u003c/div\u003e\n        \u003c/div\u003e\n        \u003cdiv class=\"award-card\"\u003e\n            \u003cimg src=\"/images/about/cumcm.png\" alt=\"CUMCM\" class=\"award-icon\"\u003e\n            \u003cdiv class=\"award-info\"\u003e\n                \u003ch3 class=\"award-title\"\u003eCUMCM\u003c/h3\u003e\n                \u003cdiv class=\"award-results\"\u003e\n                    \u003cdiv class=\"award-badge\"\u003e全国二等奖\u003c/div\u003e\n                    \u003cdiv class=\"award-badge\"\u003e省级一等奖\u003c/div\u003e\n                \u003c/div\u003e\n                \u003cdiv class=\"award-date\"\u003e2018\u003c/div\u003e\n            \u003c/div\u003e\n        \u003c/div\u003e\n    \u003c/div\u003e\n\u003c/div\u003e\n\n\u003cdiv class=\"resume-section\"\u003e\n    \u003ch2\u003e发表论文\u003c/h2\u003e\n    \u003cp class=\"section-note\"\u003e转向计算机科学之前，在材料科学领域的早期研究。\u003c/p\u003e","title":"关于我"},{"content":"","permalink":"https://liugangjian.github.io/zh/archives/","summary":"按年份浏览全部文章，或通过标签查找感兴趣的主题。","title":"文章"},{"content":"","permalink":"https://liugangjian.github.io/zh/projects/","summary":"","title":"项目"}]