hscredit.core.models.boosting.xgboost_model 源代码

"""XGBoost风控模型.

基于XGBoost实现的风控模型,支持自定义损失函数和评估指标。

**依赖**
pip install xgboost

**参考样例**

>>> from hscredit.core.models import XGBoost
>>> model = XGBoost(
...     max_depth=5,          # 树最大深度
...     learning_rate=0.1,    # 学习率
...     n_estimators=100,     # 树数量
...     eval_metric=['auc', 'ks']  # 评估指标
... )
>>> model.fit(X_train, y_train)           # 训练模型
>>> proba = model.predict_proba(X_test)    # 预测概率
>>> report = model.generate_report(X_train, y_train, X_test, y_test)  # 生成评估报告
"""

import logging
from typing import Any, Dict, List, Optional, Tuple, Union
import numpy as np
import pandas as pd

logger = logging.getLogger(__name__)

try:
    import xgboost as xgb

    XGBOOST_AVAILABLE = True
except ImportError:
    XGBOOST_AVAILABLE = False
    xgb = None

from ..base import BaseRiskModel, resolve_custom_objective


[文档] class XGBoost(BaseRiskModel): """XGBoost风控模型 - 基于内部建模经验优化. 基于XGBoost的二分类模型,针对风控场景优化。 参考内部建模经验,支持自动scale_pos_weight计算、KS评估指标等。 **参数** :param max_depth: 树最大深度,默认6 - 内部经验: 特征>1000时建议5-13,否则3-9 :param learning_rate: 学习率,默认0.1 - 内部经验: 一般0.01-0.3,常用0.1 :param n_estimators: 树的数量,默认100 - 内部经验: 样本>10000时50-300,否则20-100 :param min_child_weight: 叶子节点最小样本权重和,默认1 - 内部经验: 样本>10000时10-2000,否则10-300 :param subsample: 样本采样比例,默认0.8 - 内部经验: 常用0.6-0.9防止过拟合 :param colsample_bytree: 特征采样比例,默认0.8 - 内部经验: 常用0.6-0.9防止过拟合 :param colsample_bylevel: 每层的特征采样比例,默认1.0 :param reg_alpha: L1正则化系数,默认0 - 内部经验: 常用0, 0.01, 0.1, 1, 10, 100 :param reg_lambda: L2正则化系数,默认1 :param scale_pos_weight: 正负样本权重比,默认'auto' - 'auto': 自动计算 (当bad_rate<0.05时) - float: 自定义权重 - 内部经验: 当bad_rate<0.05时设置0.05 * n_samples / n_positive :param gamma: 节点分裂所需的最小损失减少,默认0 :param max_delta_step: 每棵树权重改变的最大步长,默认0 :param tree_method: 树构建算法,默认'hist' - 'hist': 直方图算法(推荐,速度快) - 'exact': 精确贪心算法 - 'approx': 近似算法 - 'gpu_hist': GPU直方图算法 :param objective: 目标函数,默认'binary:logistic' :param eval_metric: 评估指标,可选列表 - 支持'ks'作为自定义评估指标(风控常用) - 多个指标时,默认使用第一个指标进行早停 :param early_stopping_rounds: 早停轮数,默认None - 连续N轮没有改善则停止训练 - XGBoost 2.0+ 需要在构造函数中传入此参数 :param early_stopping_metric: 用于早停的评估指标名称,默认None(使用第一个指标) - 当eval_metric有多个时,指定用哪个指标进行早停判断 - XGBoost 2.0+ 使用方式: 传入metric名称,如 'auc', 'logloss', 'error' - 注意: 指标名称必须是eval_metric中指定的名称之一 - 例如: eval_metric=['auc','logloss']时,可指定'auc'或'logloss'作为早停指标 :param early_stopping_data: 用于早停的验证集名称,默认None(使用第一个验证集) - XGBoost内部会自动命名为'validation_0', 'validation_1'等 - 通常不需要指定,除非有多个验证集 :param validation_fraction: 验证集比例,默认0.2 :param random_state: 随机种子,默认None :param n_jobs: 并行任务数,默认-1 :param verbose: 是否输出详细信息,默认False :param params: XGBoost原生参数字典,默认None - 如果传入,将覆盖其他参数设置 - 可直接使用XGBoost原生参数名 :param kwargs: 其他XGBoost参数 **属性** :ivar feature_importances_: 特征重要性 :ivar evals_result_: 训练过程评估结果 :ivar best_iteration_: 最佳迭代次数 :ivar best_score_: 最佳得分 :ivar scale_pos_weight_: 实际使用的scale_pos_weight值 :ivar booster_: 底层XGBoost模型 **参考样例** >>> # 基础使用 >>> model = XGBoost(max_depth=5, learning_rate=0.1) >>> model.fit(X_train, y_train) >>> # 自动处理不平衡数据 >>> model = XGBoost(scale_pos_weight='auto') >>> model.fit(X_train, y_train) >>> # 使用KS作为评估指标 >>> model = XGBoost(eval_metric='ks') >>> model.fit(X_train, y_train) >>> # 使用原生XGBoost参数 >>> params = {'max_depth': 5, 'learning_rate': 0.05, 'subsample': 0.8} >>> model = XGBoost(params=params) >>> model.fit(X_train, y_train) >>> # 早停设置 - 使用多个评估指标,指定logloss作为早停指标(越小越好) >>> model = XGBoost( ... n_estimators=1000, ... eval_metric=['auc', 'logloss'], ... early_stopping_rounds=10, ... early_stopping_metric='logloss' ... ) >>> model.fit(X_train, y_train) >>> print(f'最佳迭代次数: {model.best_iteration_}') >>> # 早停设置 - 使用AUC作为早停指标(越大越好) >>> model = XGBoost( ... n_estimators=1000, ... eval_metric=['auc', 'logloss'], ... early_stopping_rounds=10, ... early_stopping_metric='auc' ... ) >>> model.fit(X_train, y_train) **引用** 基于 XGBoost 梯度提升框架,见 Chen, T. & Guestrin, C. (2016). *XGBoost: A Scalable Tree Boosting System.* KDD; 文档 https://xgboost.readthedocs.io/ 。 """ def __init__( self, max_depth: int = 6, learning_rate: float = 0.1, n_estimators: int = 100, min_child_weight: float = 1, subsample: float = 0.8, colsample_bytree: float = 0.8, colsample_bylevel: float = 1.0, reg_alpha: float = 0, reg_lambda: float = 1, scale_pos_weight: Union[str, float] = "auto", gamma: float = 0, max_delta_step: float = 0, tree_method: str = "hist", objective: str = "binary:logistic", eval_metric: Union[str, List[str], None] = None, early_stopping_rounds: Optional[int] = None, early_stopping_metric: Optional[str] = None, early_stopping_data: Optional[str] = None, validation_fraction: float = 0.2, random_state: Optional[int] = None, n_jobs: int = -1, verbose: bool = False, params: Optional[Dict[str, Any]] = None, scorecard_params: Optional[Dict[str, Any]] = None, **kwargs, ): if not XGBOOST_AVAILABLE: raise ImportError("XGBoost未安装,请使用 pip install xgboost 安装") # 保存原生params参数 self.params = params # 用于sklearn get_params兼容性 self._native_params = params or {} # 从params中提取参数(如果提供了原生参数) max_depth = self._native_params.get("max_depth", max_depth) learning_rate = self._native_params.get("learning_rate", learning_rate) n_estimators = self._native_params.get("n_estimators", n_estimators) min_child_weight = self._native_params.get("min_child_weight", min_child_weight) subsample = self._native_params.get("subsample", subsample) colsample_bytree = self._native_params.get("colsample_bytree", colsample_bytree) colsample_bylevel = self._native_params.get("colsample_bylevel", colsample_bylevel) reg_alpha = self._native_params.get("reg_alpha", reg_alpha) reg_lambda = self._native_params.get("reg_lambda", reg_lambda) scale_pos_weight = self._native_params.get("scale_pos_weight", scale_pos_weight) gamma = self._native_params.get("gamma", gamma) max_delta_step = self._native_params.get("max_delta_step", max_delta_step) tree_method = self._native_params.get("tree_method", tree_method) objective = self._native_params.get("objective", objective) random_state = self._native_params.get("random_state", random_state) n_jobs = self._native_params.get("n_jobs", n_jobs) # 从params中提取early_stopping_rounds(优先级最高) early_stopping_rounds = self._native_params.get("early_stopping_rounds", early_stopping_rounds) super().__init__( objective=objective, eval_metric=eval_metric, early_stopping_rounds=early_stopping_rounds, validation_fraction=validation_fraction, random_state=random_state, n_jobs=n_jobs, verbose=verbose, scorecard_params=scorecard_params, **kwargs, ) # 早停相关参数 self.early_stopping_metric = early_stopping_metric self.early_stopping_data = early_stopping_data # XGBoost特有参数 self.max_depth = max_depth self.learning_rate = learning_rate self.n_estimators = n_estimators self.min_child_weight = min_child_weight self.subsample = subsample self.colsample_bytree = colsample_bytree self.colsample_bylevel = colsample_bylevel self.reg_alpha = reg_alpha self.reg_lambda = reg_lambda self._scale_pos_weight_input = scale_pos_weight # 保存原始输入 self.scale_pos_weight = scale_pos_weight if scale_pos_weight != "auto" else 1.0 self.gamma = gamma self.max_delta_step = max_delta_step self.tree_method = tree_method self.scale_pos_weight_ = None # 实际使用的值
[文档] def fit( self, X: Union[np.ndarray, pd.DataFrame], y: Optional[Union[np.ndarray, pd.Series]] = None, sample_weight: Optional[np.ndarray] = None, eval_set: Optional[List[Tuple]] = None, **fit_params, ) -> "XGBoost": """训练XGBoost模型. 支持两种调用方式: 1. 常规方式: fit(X, y) 2. scorecardpipeline风格: fit(X) 在init中指定target :param X: 特征矩阵 :param y: 目标变量,可选 :param sample_weight: 样本权重 :param eval_set: 验证集列表 :param fit_params: 其他fit参数 :return: self """ # 准备数据(支持从X中提取target) X, y, sample_weight = self._prepare_data(X, y, sample_weight, extract_target=True, training=True) self._validate_probability_scorecard_labels(y) # 保存特征信息 self.n_features_in_ = X.shape[1] self.classes_ = np.unique(y) # 自动计算scale_pos_weight(内部建模经验:当bad_rate<0.05时) if self._scale_pos_weight_input == "auto": pos_ratio = np.mean(y == 1) if pos_ratio < 0.05: # 内部经验: 0.05 * n_samples / n_positive self.scale_pos_weight = 0.05 * len(y) / np.sum(y == 1) if self.verbose: logger.info(f"自动计算scale_pos_weight: {self.scale_pos_weight:.2f} (bad_rate={pos_ratio:.4f})") else: self.scale_pos_weight = 1.0 else: self.scale_pos_weight = self._scale_pos_weight_input self.scale_pos_weight_ = self.scale_pos_weight # 创建验证集 auto_eval_split = eval_set is None and self.validation_fraction > 0 and self.early_stopping_rounds is not None sw_val = None if auto_eval_split: X_train, X_val, y_train, y_val, sw_train, sw_val = self._create_eval_set(X, y, sample_weight) eval_set = [(X_val, y_val)] sample_weight = sw_train else: X_train, y_train = X, y # 处理用户传入的 eval_set - 确保与训练数据格式一致(numpy数组) if eval_set is not None: processed_eval_set = [] for eval_X, eval_y in eval_set: # 将验证集转换为 numpy 数组(与训练数据保持一致) if isinstance(eval_X, pd.DataFrame): eval_X = eval_X.values if isinstance(eval_y, pd.Series): eval_y = eval_y.values processed_eval_set.append((eval_X, eval_y)) eval_set = processed_eval_set # 构建参数 - 在构造函数中传入所有参数 params = { "max_depth": self.max_depth, "learning_rate": self.learning_rate, "n_estimators": self.n_estimators, "min_child_weight": self.min_child_weight, "subsample": self.subsample, "colsample_bytree": self.colsample_bytree, "colsample_bylevel": self.colsample_bylevel, "reg_alpha": self.reg_alpha, "reg_lambda": self.reg_lambda, "scale_pos_weight": self.scale_pos_weight, "gamma": self.gamma, "max_delta_step": self.max_delta_step, "tree_method": self.tree_method, "objective": self.objective, "n_jobs": self.n_jobs, "random_state": self.random_state, "verbosity": 2 if self.verbose else 0, } # 处理评估指标 wants_ks = False if self.eval_metric is not None: converted_metrics = self._convert_metrics(self.eval_metric) metric_list = [converted_metrics] if isinstance(converted_metrics, str) else list(converted_metrics) wants_ks = any(str(metric).lower() == "ks" for metric in metric_list) native_metrics = [metric for metric in metric_list if str(metric).lower() != "ks"] if native_metrics: params["eval_metric"] = native_metrics[0] if isinstance(converted_metrics, str) else native_metrics # 处理早停参数(XGBoost 2.0+ 在构造函数中传入) callbacks = [] if wants_ks and eval_set: callbacks.append(self._create_ks_callback(eval_set)) if self.early_stopping_rounds is not None and eval_set: # 如果指定了早停指标,使用EarlyStopping回调 if self.early_stopping_metric is not None: try: from xgboost.callback import EarlyStopping callbacks.append( EarlyStopping( rounds=self.early_stopping_rounds, metric_name=self.early_stopping_metric, data_name=self.early_stopping_data, save_best=True, ) ) if self.verbose: logger.info( f"使用早停: rounds={self.early_stopping_rounds}, " f"metric='{self.early_stopping_metric}'" ) except ImportError: # 回退到旧方式 params["early_stopping_rounds"] = self.early_stopping_rounds if self.verbose: logger.info(f"使用早停: rounds={self.early_stopping_rounds} (默认指标)") else: # 未指定早停指标,使用默认方式(第一个eval_metric) params["early_stopping_rounds"] = self.early_stopping_rounds if self.verbose: logger.info(f"使用早停: rounds={self.early_stopping_rounds} (默认使用第一个指标)") if callbacks: params["callbacks"] = callbacks # 更新kwargs参数 params.update(self.kwargs) # 最后更新原生params(优先级最高) params.update(self._native_params) # 解析自定义损失(BaseLoss 实例 -> sklearn 包装器可用的目标函数) params["objective"] = resolve_custom_objective(params.get("objective")) # 创建模型 self._model = xgb.XGBClassifier(**params) # 训练 - fit时不传早停参数(已在构造函数中传入) fit_kwargs = dict(fit_params) if auto_eval_split: self._split_row_aligned_fit_param(fit_kwargs, "base_margin", "base_margin_eval_set") if sw_val is not None: fit_kwargs.setdefault("sample_weight_eval_set", [sw_val]) if eval_set: fit_kwargs["eval_set"] = eval_set if sample_weight is not None: fit_kwargs["sample_weight"] = sample_weight fit_kwargs["verbose"] = self.verbose # 执行训练 self._model.fit(X_train, y_train, **fit_kwargs) # 保存结果 self._best_iteration = getattr(self._model, "best_iteration", None) self._best_score = getattr(self._model, "best_score", None) self._evals_result = getattr(self._model, "evals_result_", {}) self._is_fitted = True self._fit_probability_scorecard(X, y) return self
@property def best_iteration_(self): """最佳迭代次数.""" return self._best_iteration @property def best_score_(self): """最佳得分.""" return self._best_score def _ks_metric(self, y_pred, dtrain): """KS评估指标(用于XGBoost内部评估). 参考内部建模常用KS作为评估指标。 """ from sklearn.metrics import roc_curve y_true = dtrain.get_label() fpr, tpr, _ = roc_curve(y_true, y_pred, pos_label=1) ks = abs(tpr - fpr).max() return "KS", ks @staticmethod def _create_ks_callback(eval_set): """创建逐轮计算 KS 的 XGBoost 回调,支持与原生指标并存。""" from sklearn.metrics import roc_curve matrices = [xgb.DMatrix(eval_X, label=eval_y) for eval_X, eval_y in eval_set] class KSEvaluationCallback(xgb.callback.TrainingCallback): def after_iteration(self, model, epoch, evals_log): for index, matrix in enumerate(matrices): prediction = model.predict(matrix, iteration_range=(0, epoch + 1)) labels = matrix.get_label() fpr, tpr, _ = roc_curve(labels, prediction, pos_label=1) value = float(np.max(np.abs(tpr - fpr))) dataset_log = evals_log.setdefault(f"validation_{index}", {}) dataset_log.setdefault("ks", []).append(value) return False return KSEvaluationCallback()
[文档] def predict(self, X: Union[np.ndarray, pd.DataFrame]) -> np.ndarray: """预测类别标签. 支持传入包含target列的数据框(scorecardpipeline风格)。 基于 predict_proba 取阈值,确保自定义损失(原始分数输出)下也能返回正确类别。 """ self._require_fitted() proba = self.predict_proba(X) indices = np.argmax(proba, axis=1) return np.asarray(self.classes_)[indices]
[文档] def predict_proba(self, X: Union[np.ndarray, pd.DataFrame]) -> np.ndarray: """预测概率. 支持传入包含target列的数据框(scorecardpipeline风格)。 当使用自定义损失函数(objective 为可调用对象)时,XGBoost 返回的是 未经过链接函数转换的原始分数(raw margin,一维数组),此处自动应用 sigmoid 转换为概率并补齐为二维 (n_samples, 2) 输出,与内置目标保持一致。 """ self._require_fitted() X, _, _ = self._prepare_data(X, extract_target=True) proba = np.asarray(self._model.predict_proba(X)) # 自定义损失返回一维原始分数,应用 sigmoid 并补齐为两列概率 if proba.ndim == 1: p1 = 1.0 / (1.0 + np.exp(-proba)) proba = np.column_stack([1.0 - p1, p1]) return proba
[文档] def get_feature_importances(self, importance_type: str = "gain") -> pd.Series: """获取特征重要性. :param importance_type: 重要性类型,可选: - 'gain': 平均增益 (默认) - 'weight': 分裂次数 - 'cover': 平均覆盖度 - 'total_gain': 总增益 - 'total_cover': 总覆盖度 :return: 特征重要性Series """ self._require_fitted() importances = self._model.feature_importances_ # 创建Series importance_series = pd.Series(importances, index=self.feature_names_in_, name="importance").sort_values( ascending=False ) self._feature_importances = importance_series return importance_series
@property def feature_importances_(self) -> np.ndarray: """特征重要性属性 (兼容sklearn风格). 直接在包装类上暴露重要性,兼容sklearn RFE/SFS等组件的 importance_getter。 """ self._require_fitted() if self._feature_importances is None: self._feature_importances = self.get_feature_importances() return self._feature_importances.values
[文档] def get_booster(self) -> "xgb.Booster": """获取底层XGBoost booster对象. :return: XGBoost Booster对象 """ self._require_fitted() return self._model.get_booster()
[文档] def plot_tree(self, num_trees: int = 0, **kwargs): """绘制树结构. :param num_trees: 树的索引 :param kwargs: 其他绘图参数 """ self._require_fitted() return xgb.plot_tree(self._model, num_trees=num_trees, **kwargs)
[文档] def plot_importance(self, max_num_features: int = 10, **kwargs): """绘制特征重要性. :param max_num_features: 显示的最大特征数 :param kwargs: 其他绘图参数 """ self._require_fitted() return xgb.plot_importance(self._model, max_num_features=max_num_features, **kwargs)
[文档] def get_leaf_indices(self, X: Union[np.ndarray, pd.DataFrame]) -> np.ndarray: """获取叶子节点索引. 返回每棵树上的叶子节点索引,用于GBDT+LR等场景。 :param X: 特征矩阵 :return: 叶子节点索引,形状 (n_samples, n_trees) **参考样例** >>> model = XGBoost(n_estimators=50) >>> model.fit(X, y) >>> leaf_indices = model.get_leaf_indices(X) >>> print(leaf_indices.shape) """ self._require_fitted() X = self._prepare_data(X)[0] return self._model.apply(X)
def _convert_metrics(self, metrics: Union[str, List[str]]) -> Union[str, List[str]]: """转换评估指标名称. :param metrics: 指标名称或列表 :return: XGBoost格式的指标名称 """ metric_map = { "auc": "auc", "logloss": "logloss", "error": "error", "rmse": "rmse", "mae": "mae", "map": "map", "merror": "merror", "mlogloss": "mlogloss", } if isinstance(metrics, str): return metric_map.get(metrics.lower(), metrics) return [metric_map.get(m.lower(), m) for m in metrics]
[文档] def save_model(self, path: str): """保存底层XGBoost模型(原生格式). :param path: 保存路径(.json/.ubj/.bin 格式) """ self._require_fitted() self._model.save_model(path) self._save_score_transformer_sidecar(path)
[文档] def load_model(self, path: str) -> "XGBoost": """加载底层XGBoost模型(原生格式). :param path: 模型路径 :return: self """ self._model = xgb.XGBClassifier() self._model.load_model(path) self._is_fitted = True self.classes_ = getattr(self, "classes_", np.array([0, 1])) if not hasattr(self, "feature_names_in_"): n_feat = self._model.n_features_in_ if hasattr(self._model, "n_features_in_") else 0 self.feature_names_in_ = [f"feature_{i}" for i in range(n_feat)] self.n_features_in_ = n_feat self._load_score_transformer_sidecar(path) return self