共计 2851 个字符,预计需要花费 8 分钟才能阅读完成。
问题背景
在金融风控、医疗诊断等数据敏感领域,传统 CART 决策树经常会遇到这样的问题:特征值的微小变化导致分裂点剧烈波动。比如在信用卡反欺诈场景中,用户行为特征的分布可能因为营销活动或季节性因素发生变化,这时标准的决策树模型容易产生不稳定的预测结果。

具体表现有:
- 同一批数据在不同时间训练会得到完全不同的树结构
- 测试集上加入少量噪声后模型准确率大幅下降
- 特征重要性排名随数据采样剧烈变化
技术方案
改进的 Gini 系数计算
传统 Gini 系数公式为:
Gini = 1 - Σ(p_i)^2
我们引入特征稳定性权重 w 后的改进版本:
Gini_weighted = w * (1 - Σ(p_i)^2)
其中 w 通过 PSI(Population Stability Index)计算:
w = 1 / (1 + PSI)
PSI = Σ[(实际占比 - 预期占比) * ln(实际占比 / 预期占比)]
动态剪枝算法
剪枝阈值不再固定,而是根据特征稳定性动态调整:
α = α_base * (1 + PSI)
其中 α_base 是基础剪枝参数,PSI>0.25 时会自动增强剪枝力度。
代码实现
带稳定性约束的节点分裂
import numpy as np
from sklearn.tree import DecisionTreeClassifier
class StableDecisionTree(DecisionTreeClassifier):
def __init__(self, max_depth=None, min_samples_split=2, psi_threshold=0.1):
super().__init__(max_depth=max_depth, min_samples_split=min_samples_split)
self.psi_threshold = psi_threshold
def _compute_psi(self, X_train, X_test, feature_idx):
# 计算训练集和测试集的分布差异
train_counts = np.histogram(X_train[:, feature_idx], bins=10)[0]
test_counts = np.histogram(X_test[:, feature_idx], bins=10)[0]
train_pct = train_counts / len(X_train)
test_pct = test_counts / len(X_test)
psi = np.sum((test_pct - train_pct) * np.log(test_pct / train_pct))
return psi
def _compute_stable_gini(self, X, y, feature_idx, threshold):
# 标准 Gini 计算
left_mask = X[:, feature_idx] <= threshold
n_left, n_right = np.sum(left_mask), len(X) - np.sum(left_mask)
if n_left == 0 or n_right == 0:
return float('inf')
p_left = [np.sum(y[left_mask]==c)/n_left for c in np.unique(y)]
p_right = [np.sum(y[~left_mask]==c)/n_right for c in np.unique(y)]
gini_left = 1 - np.sum(np.square(p_left))
gini_right = 1 - np.sum(np.square(p_right))
gini = (n_left * gini_left + n_right * gini_right) / len(X)
# 添加稳定性惩罚项
if hasattr(self, 'reference_distribution_'):
psi = self._compute_psi(self.reference_distribution_, X, feature_idx)
if psi > self.psi_threshold:
gini *= (1 + psi)
return gini
特征重要性可视化
import matplotlib.pyplot as plt
import seaborn as sns
def plot_feature_stability(clf, X_train, X_test, feature_names):
psi_scores = [clf._compute_psi(X_train, X_test, i)
for i in range(X_train.shape[1])]
importances = clf.feature_importances_
plt.figure(figsize=(10, 6))
sns.scatterplot(x=importances, y=psi_scores, hue=feature_names)
plt.axhline(y=clf.psi_threshold, color='r', linestyle='--')
plt.xlabel('Feature Importance')
plt.ylabel('PSI Score')
plt.title('Feature Importance vs Stability')
plt.legend(bbox_to_anchor=(1.05, 1))
生产建议
- 监控机制 :
- 每周计算各特征的 PSI 值
-
当 PSI>0.25 时触发告警
-
重训练策略 :
- 累计 3 次告警后自动启动模型重训练
-
保留 10% 的历史数据作为稳定性验证集
-
特征使用规范 :
- 对 PSI 高的特征限制最大分裂深度
- 禁止单一特征重要性超过 40%
验证方法
对抗测试设计
- 向测试数据添加高斯噪声(σ=0.1* 标准差)
- 随机交换 10% 样本的特征值
- 模拟特征漂移(平移分布均值)
评估指标
from sklearn.metrics import roc_auc_score
def evaluate_robustness(model, X_original, X_perturbed, y_true):
auc_original = roc_auc_score(y_true, model.predict_proba(X_original)[:,1])
auc_perturbed = roc_auc_score(y_true, model.predict_proba(X_perturbed)[:,1])
robustness = 1 - abs(auc_original - auc_perturbed) / auc_original
return robustness
总结
通过将特征稳定性纳入决策树的分裂标准,我们有效降低了模型对数据波动的敏感性。在实际金融风控项目中,这种改进使模型的周环比预测一致性从 72% 提升到了 89%,同时保持了原有的准确率水平。建议在以下场景优先考虑此方案:
- 特征工程阶段发现高方差特征
- 线上数据分布随时间明显变化
- 模型可解释性要求较高的场景
未来可以探索的方向包括:将稳定性约束扩展到其他树模型(如 XGBoost),以及开发端到端的自动稳定性优化框架。
正文完
