hscredit.core.models.explainability.reports 源代码

"""不依赖 SHAP 计算的轻量模型重要性报告。"""

from typing import Any, List, Optional, Union

import numpy as np
import pandas as pd

from ..base import BaseRiskModel


def _as_1d_importance(values: Any) -> np.ndarray:
    """将不同模型暴露的特征重要性压平成一维数组."""
    arr = np.asarray(values, dtype=float)
    if arr.ndim == 0:
        arr = arr.reshape(1)
    elif arr.ndim > 1:
        arr = arr[0] if arr.shape[0] == 1 else np.mean(np.abs(arr), axis=0)
    return arr.ravel()


def _infer_feature_names(
    model: Any,
    X: Optional[Union[np.ndarray, pd.DataFrame]],
    n_features: int,
) -> List[str]:
    """从输入数据或模型属性推断特征名."""
    if isinstance(X, pd.DataFrame):
        return X.columns.tolist()
    if hasattr(model, "feature_names_in_"):
        return list(model.feature_names_in_)
    if hasattr(model, "_feature_names"):
        return list(model._feature_names)
    if hasattr(model, "feature_names"):
        names = getattr(model, "feature_names")
        if names is not None:
            return list(names)
    return [f"feature_{i}" for i in range(n_features)]


[文档] def model_explain_report( model: BaseRiskModel, X: Optional[Union[np.ndarray, pd.DataFrame]] = None, importance_type: str = "gain", top_n: Optional[int] = None, normalize: bool = True, ) -> pd.DataFrame: """生成模型特征解释报告. 不依赖 SHAP,优先复用模型自身 ``get_feature_importances``,再依次回退到 ``feature_importances_`` / ``coef_``,用于没有安装解释扩展包时的基础模型解释。 :param model: 已训练模型 :param X: 特征矩阵,可选,用于推断特征名 :param importance_type: 模型重要性类型,默认 ``'gain'`` :param top_n: 返回前 N 个特征,None 表示全部返回 :param normalize: 是否增加归一化重要性列,默认 True :return: 模型解释报告 DataFrame,列名为中文 **参考样例** >>> report = model_explain_report(model, X_test, importance_type='coef') >>> print(report[['特征名', '重要性', '排名']].head()) """ if top_n is not None and ( not isinstance(top_n, int) or isinstance(top_n, bool) or top_n <= 0 ): raise ValueError("top_n 必须是正整数或 None") source = "get_feature_importances" direction = None if hasattr(model, "get_feature_importances"): importances = model.get_feature_importances(importance_type) if isinstance(importances, pd.Series): feature_names = importances.index.astype(str).tolist() values = importances.to_numpy(dtype=float) else: values = _as_1d_importance(importances) feature_names = _infer_feature_names(model, X, len(values)) elif hasattr(model, "feature_importances_"): source = "feature_importances_" values = _as_1d_importance(getattr(model, "feature_importances_")) feature_names = _infer_feature_names(model, X, len(values)) elif hasattr(model, "coef_"): source = "coef_" raw_coef = np.asarray(getattr(model, "coef_"), dtype=float) coef = _as_1d_importance(raw_coef) if raw_coef.ndim == 1 or (raw_coef.ndim == 2 and raw_coef.shape[0] == 1): direction = np.sign(coef) values = np.abs(coef) feature_names = _infer_feature_names(model, X, len(values)) else: raise ValueError("模型未提供可解释的特征重要性或系数") if len(feature_names) != len(values): feature_names = [f"feature_{i}" for i in range(len(values))] result = pd.DataFrame( { "特征名": feature_names, "重要性": values, "重要性类型": importance_type if source == "get_feature_importances" else source, "来源": source, } ) if direction is not None and len(direction) == len(result): direction_map = {1.0: "正向", -1.0: "负向", 0.0: "无方向"} result["影响方向"] = [direction_map.get(float(v), "无方向") for v in direction] result["排名"] = result["重要性"].rank(method="first", ascending=False).astype("Int64") result = result.sort_values(["排名", "特征名"]).reset_index(drop=True) if normalize: total = float(np.nansum(np.abs(result["重要性"].to_numpy(dtype=float)))) result["归一化重要性"] = result["重要性"] / total if total > 0 else np.zeros(len(result), dtype=float) if top_n is not None: result = result.head(top_n).reset_index(drop=True) return result