hscredit.report.mining.tree_extractor 源代码

"""决策树规则提取模块.

支持从多种树模型中提取规则,包括:
- 决策树 (Decision Tree)
- 随机森林 (Random Forest)
- 卡方决策树 (Chi-square Tree)
- GBDT
- 孤立森林 (Isolation Forest)
"""

import copy

import numpy as np
import pandas as pd
from typing import Union, List, Dict, Optional, Any
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
import warnings

from .base import (
    BaseRuleMiner,
    FeatureNames,
    _mining_workload,
    format_feature_context,
    resolve_feature_map,
)
from ...core.rules.rule import Rule
from ...utils.parallel import _current_parallel_budget, resolve_n_jobs, validate_parallel_config

# 从 hscredit.core.models 统一导入 sklearn 模型
from ...core.models import (
    DecisionTreeClassifier,
    RandomForest,
    GradientBoosting,
)


def _normalize_tree_algorithm(algorithm: str) -> str:
    """校验树算法名称,同时保留构造参数的原始 sklearn 契约。"""
    resolved = algorithm.lower()
    if resolved == "xgb":
        resolved = "gbdt"
        warnings.warn("'xgb'算法已弃用,请使用'gbdt'", DeprecationWarning)
    if resolved not in TreeRuleExtractor.VALID_ALGORITHMS:
        raise ValueError(
            f"不支持的算法: {resolved},可选: {TreeRuleExtractor.VALID_ALGORITHMS}"
        )
    return resolved


def _tree_extract_worker(task):
    """从一棵独立树提取规则。"""
    extractor, tree, tree_id = task
    return extractor._extract_from_tree(tree, tree_id=tree_id)


def _gbdt_tree_extract_worker(task):
    """提取并评估一棵独立 GBDT 基学习器的规则。"""
    extractor, tree, tree_id = task
    accepted = []
    for rule in extractor._extract_from_tree(tree, tree_id=tree_id):
        mask = extractor._apply_conditions(rule["conditions"], extractor.X_train_)
        hit_count = mask.sum()
        if hit_count >= 5:
            hit_bad = extractor.y_train_[mask].sum()
            badrate = hit_bad / hit_count
            if badrate >= 0.05:
                rule["sample_count"] = int(hit_count)
                rule["class_probability"] = badrate
                accepted.append(rule)
    return accepted


def _isolation_tree_extract_worker(task):
    """从一棵独立孤立树提取异常路径规则。"""
    extractor, estimator, tree_id, scores, anomaly_mask = task
    return extractor._extract_from_isolation_tree(
        estimator, tree_id, scores, anomaly_mask
    )


def _tree_rule_report_worker(task):
    """构造并评估一条独立树规则。"""
    ordinal, rule_item, expression, datasets, target, feature_map = task
    rule = Rule(
        expr=expression,
        name=f"TreeRule_{rule_item.get('rule_id', ordinal)}",
        description=expression,
        weight=float(rule_item.get("importance", 0)),
        n_jobs=1,
    )
    metadata = dict(rule_item)
    if feature_map is not None:
        used_features = [condition["feature"] for condition in rule_item.get("conditions", [])]
        input_fields, field_meanings = format_feature_context(used_features, feature_map)
        metadata.update({"入参字段": input_fields, "字段含义": field_meanings})
    if datasets is not None:
        report_df = rule.report(datasets=datasets, target=target)
        hit_rows = (
            report_df[report_df["分箱"] == "命中"]
            if "分箱" in report_df.columns
            else pd.DataFrame()
        )
        if not hit_rows.empty:
            hit = hit_rows.iloc[0].to_dict()
            metadata.update(
                {
                    "命中样本数": hit.get("样本总数"),
                    "命中样本占比": hit.get("样本占比"),
                    "命中坏样本率": hit.get("坏样本率"),
                    "命中LIFT值": hit.get("LIFT值"),
                    "坏账改善": hit.get("坏账改善"),
                    "风险拒绝比": hit.get("风险拒绝比"),
                }
            )
    rule.metadata_ = metadata
    rule.metric_score_ = metadata.get("命中LIFT值", metadata.get("importance", 0))
    return rule


[文档] class TreeRuleExtractor(BaseRuleMiner): """树模型规则提取器. 支持多种树模型的规则提取,包括决策树、随机森林、GBDT、XGBoost和孤立森林。 代码风格参考hscredit的binning模块,fit方法兼容scorecardpipeline风格。 支持通过**kwargs传入任意sklearn树模型参数。 :param algorithm: 算法类型,'dt', 'rf', 'chi2', 'gbdt', 'xgb', 'isf' :param target: 目标变量列名,默认为'target' :param exclude_cols: 需要排除的列名列表 :param features: 参与建树的字段名或字段名列表,默认None(使用全部候选字段) :param feature_map: 字段名到字段含义的映射,用于规则报告展示 :param max_depth: 树的最大深度,默认5 :param min_samples_split: 分裂节点最小样本数,默认10 :param min_samples_leaf: 叶子节点最小样本数,默认5 :param n_estimators: 森林中树的数量,默认10 :param max_features: 每棵树考虑的最大特征数,默认'sqrt' :param test_size: 测试集比例,默认0.3 :param random_state: 随机种子,默认42 :param feature_trends: 特征趋势字典,如{'age': 1}表示正相关 :param chi2_threshold: 卡方分箱阈值,默认3.841 :param kwargs: 其他树模型参数,直接传递给底层sklearn模型: - DecisionTreeClassifier: criterion, splitter, max_leaf_nodes, etc. - RandomForestClassifier: bootstrap, oob_score, class_weight, etc. - GradientBoostingClassifier: learning_rate, subsample, loss, etc. - IsolationForest: contamination, max_samples, etc. **参考样例** >>> extractor = TreeRuleExtractor(algorithm='dt', max_depth=5) # 决策树提取:从单棵树提取可解释规则 >>> extractor.fit(df) >>> rules = extractor.extract_rules() >>> extractor = TreeRuleExtractor( ... algorithm='rf', # 随机森林提取:多棵树投票,提取泛化性更好的规则 ... n_estimators=50, ... max_depth=10, ... class_weight='balanced', ... bootstrap=True, ... oob_score=True ... ) >>> extractor.fit(X, y) >>> rules = extractor.extract_rules() >>> extractor = TreeRuleExtractor( ... algorithm='isf', # 孤立森林异常检测:识别偏离正常模式的异常样本规则(无监督) ... contamination=0.05, ... max_samples=256 ... ) >>> extractor.fit(X) # 无监督,不需要y >>> anomaly_rules = extractor.extract_rules() """ VALID_ALGORITHMS = {'dt', 'rf', 'chi2', 'gbdt', 'xgb', 'isf'} def __init__( self, algorithm: str = 'dt', target: str = 'target', exclude_cols: Optional[List[str]] = None, max_depth: int = 5, min_samples_split: int = 10, min_samples_leaf: int = 5, n_estimators: int = 10, max_features: str = 'sqrt', test_size: float = 0.3, random_state: int = 42, feature_trends: Optional[Dict[str, int]] = None, chi2_threshold: float = 3.841, n_jobs: Optional[Union[int, float]] = -1, parallel_backend: Optional[str] = None, parallel_config: Optional[Dict[str, Any]] = None, features: FeatureNames = None, feature_map: Optional[Dict[str, str]] = None, **kwargs ): super().__init__( target=target, exclude_cols=exclude_cols, n_jobs=n_jobs, parallel_backend=parallel_backend, parallel_config=parallel_config, features=features, feature_map=feature_map, ) self.algorithm = algorithm self._resolved_algorithm = _normalize_tree_algorithm(algorithm) self.max_depth = max_depth self.min_samples_split = min_samples_split self.min_samples_leaf = min_samples_leaf self.n_estimators = n_estimators self.max_features = max_features self.test_size = test_size self.random_state = random_state self.feature_trends = feature_trends self.chi2_threshold = chi2_threshold self.model_kwargs = kwargs # 存储额外的模型参数 self.model_ = None self.encoders_ = {} self.feature_names_ = [] self.rules_ = [] self.is_fitted_ = False
[文档] def fit( self, X: Union[pd.DataFrame, np.ndarray], y: Optional[Union[pd.Series, np.ndarray]] = None, feature_names: FeatureNames = None, **kwargs ) -> 'TreeRuleExtractor': """在临时副本中拟合,成功后原子提交模型与编码状态。""" working = copy.deepcopy(self) working._fit_inplace(X, y, feature_names=feature_names, **kwargs) self._commit_fitted_state(working) return self
def _fit_inplace( self, X: Union[pd.DataFrame, np.ndarray], y: Optional[Union[pd.Series, np.ndarray]] = None, feature_names: FeatureNames = None, **kwargs ) -> 'TreeRuleExtractor': """拟合提取器. :param X: 训练数据 :param y: 目标变量(监督学习需要) :param feature_names: 本次拟合使用的字段名或字段名列表,优先于构造参数 ``features`` :param kwargs: 额外参数,可覆盖初始化参数 :return: self """ self._reset_fitted_state() # 更新参数 for key, value in kwargs.items(): if hasattr(self, key): setattr(self, key, value) elif key in self.model_kwargs: self.model_kwargs[key] = value self._resolved_algorithm = _normalize_tree_algorithm(self.algorithm) validate_parallel_config(self.parallel_backend, self.parallel_config) self._effective_model_workers() X, y = self._check_input_data(X, y, feature_names=feature_names) # 保存特征名 self.feature_names_ = list(X.columns) # 编码类别型特征 X_encoded = self._encode_categorical_features(X) # 初始化模型 self.model_ = self._initialize_model() if self._resolved_algorithm == 'isf': # 孤立森林不需要y self.model_.fit(X_encoded) self.X_train_ = X_encoded self.X_ = X else: # 监督学习需要y if y is None: raise ValueError(f"算法 '{self._resolved_algorithm}' 需要目标变量y") # 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split( X_encoded, y, test_size=self.test_size, random_state=self.random_state ) self.X_train_ = X_train self.X_test_ = X_test self.y_train_ = y_train self.y_test_ = y_test self.X_ = X # 卡方分箱预处理 if self._resolved_algorithm == 'chi2': X_train = self._chi2_preprocess(X_train, y_train) X_test = self._chi2_preprocess(X_test, y_test, fit=False) # 训练模型 self.model_.fit(X_train, y_train) # 保存训练后的数据 self.X_train_ = X_train self.X_test_ = X_test self.is_fitted_ = True return self def _reset_fitted_state(self) -> None: """清除上一轮模型派生状态;仅在事务 working 副本中调用。""" self.model_ = None self.encoders_ = {} self.feature_names_ = [] self.rules_ = [] self.is_fitted_ = False for attribute in ( "X_", "X_train_", "X_test_", "y_train_", "y_test_", "chi2_bins_", ): self.__dict__.pop(attribute, None) def _effective_model_workers(self) -> int: """解析底层树模型的工作数,并遵守活跃的父层预算。""" budget = _current_parallel_budget() workers = resolve_n_jobs( self.n_jobs, available_budget=budget.available, ) or 1 if budget.depth > 0: workers = min(workers, budget.available) return max(1, workers) def _encode_categorical_features(self, X: pd.DataFrame) -> pd.DataFrame: """编码类别型特征. :param X: 输入数据 :return: 编码后的数据 """ X_encoded = X.copy() self.encoders_ = {} for col in X.columns: if not pd.api.types.is_numeric_dtype(X[col]): le = LabelEncoder() X_encoded[col] = le.fit_transform(X[col].astype(str)) self.encoders_[col] = le return X_encoded.fillna(0) def _initialize_model(self): """初始化模型,支持通过**kwargs传入任意参数. 从 hscredit.core.models 导入模型,通过 get_native_model() 获取底层 sklearn 模型。 """ # 构建基础参数字典 base_params = { 'random_state': self.random_state } if self._resolved_algorithm == 'dt': base_params.update({ 'max_depth': self.max_depth, 'min_samples_split': self.min_samples_split, 'min_samples_leaf': self.min_samples_leaf, 'n_jobs': self._effective_model_workers(), }) base_params.update(self.model_kwargs) return DecisionTreeClassifier(**base_params) elif self._resolved_algorithm in ['rf', 'chi2']: base_params.update({ 'n_estimators': self.n_estimators, 'max_depth': self.max_depth, 'min_samples_split': self.min_samples_split, 'min_samples_leaf': self.min_samples_leaf, 'max_features': self.max_features, 'n_jobs': self._effective_model_workers() }) base_params.update(self.model_kwargs) # 使用 hscredit 的 RandomForest return RandomForest(**base_params) elif self._resolved_algorithm == 'gbdt': base_params.update({ 'n_estimators': self.n_estimators, 'max_depth': self.max_depth, 'min_samples_split': self.min_samples_split, 'min_samples_leaf': self.min_samples_leaf, 'max_features': self.max_features, }) base_params.update(self.model_kwargs) # 使用 hscredit 的 GradientBoosting return GradientBoosting(**base_params) elif self._resolved_algorithm == 'isf': # 孤立森林直接使用 sklearn(hscredit 暂无 IsolationForestRiskModel) from sklearn.ensemble import IsolationForest base_params.update({ 'n_estimators': self.n_estimators, 'max_samples': min(256, 1000), # 限制最大样本数 'contamination': 0.1, 'n_jobs': self._effective_model_workers() }) base_params.update(self.model_kwargs) return IsolationForest(**base_params) def _chi2_preprocess( self, X: pd.DataFrame, y: pd.Series, fit: bool = True ) -> pd.DataFrame: """卡方分箱预处理. :param X: 特征数据 :param y: 目标变量 :param fit: 是否拟合 :return: 分箱后的数据 """ from scipy.stats import chi2_contingency if fit: self.chi2_bins_ = {} X_binned = X.copy() for col in X.columns: if not pd.api.types.is_numeric_dtype(X[col]): continue if fit: # 初始等频分箱 from sklearn.preprocessing import KBinsDiscretizer discretizer = KBinsDiscretizer( n_bins=10, encode='ordinal', strategy='quantile' ) try: bins = discretizer.fit_transform(X[[col]].dropna()).flatten() bin_edges = discretizer.bin_edges_[0].copy() # 合并相似分箱 while len(bin_edges) > 2: chi2_scores = [] for i in range(len(bin_edges) - 2): merged_bins = bins.copy() merged_bins[merged_bins == i + 1] = i try: contingency = pd.crosstab(merged_bins, y) chi2_stat, _, _, _ = chi2_contingency(contingency) chi2_scores.append((i, chi2_stat)) except Exception: chi2_scores.append((i, float('inf'))) if not chi2_scores: break min_idx, min_chi2 = min(chi2_scores, key=lambda x: x[1]) if min_chi2 < self.chi2_threshold: bins[bins > min_idx] -= 1 bin_edges = np.delete(bin_edges, min_idx + 1) else: break self.chi2_bins_[col] = bin_edges except Exception: continue # 应用分箱 if col in getattr(self, 'chi2_bins_', {}): X_binned[col] = pd.cut(X[col], bins=self.chi2_bins_[col], labels=False) return X_binned.fillna(0)
[文档] def extract_rules(self) -> List[Dict[str, Any]]: """提取规则. :return: 规则列表 """ self._check_fitted() if self._resolved_algorithm == 'dt': self.rules_ = self._extract_from_tree(self.model_, tree_id=0) elif self._resolved_algorithm in ['rf', 'chi2']: self.rules_ = self._extract_from_forest() elif self._resolved_algorithm == 'gbdt': self.rules_ = self._extract_from_gbdt() elif self._resolved_algorithm == 'isf': self.rules_ = self._extract_from_isolation_forest() # 过滤规则 self.rules_ = self._filter_rules(self.rules_) # 去重 self.rules_ = self._deduplicate_rules(self.rules_) # 计算重要性 for rule in self.rules_: rule['importance'] = self._calculate_rule_importance(rule) # 排序 self.rules_.sort(key=lambda x: x.get('importance', 0), reverse=True) return self.rules_
def _extract_from_tree( self, tree_model, tree_id: int = 0 ) -> List[Dict[str, Any]]: """从单棵树提取规则. :param tree_model: 树模型 (sklearn Tree 或 hscredit RiskModel) :param tree_id: 树ID :return: 规则列表 """ # 如果是 hscredit 模型,获取底层 sklearn 模型 if hasattr(tree_model, 'get_native_model'): native_model = tree_model.get_native_model() tree = native_model.tree_ else: tree = tree_model.tree_ rules = [] def recurse(node_id, conditions): """递归遍历树.""" if tree.feature[node_id] == -2: # 叶子节点 # 计算叶子节点的实际坏账率 badrate = self._calculate_leaf_badrate(conditions) # 使用 n_node_samples 获取节点样本数(版本无关)。 # sklearn>=1.3 起 tree_.value 存储的是归一化比例(每节点求和为 1), # 不能再用 value.sum() 还原样本数,否则恒为 1。 total = tree.n_node_samples[node_id] if total == 0: return predicted_class = 1 if badrate > 0.5 else 0 rule = { 'rule_id': len(rules), 'conditions': conditions.copy(), 'predicted_class': predicted_class, 'class_name': 'bad' if predicted_class == 1 else 'good', 'class_probability': badrate, 'sample_count': int(total), 'tree_id': tree_id } rules.append(rule) else: # 非叶子节点 feature = self.feature_names_[tree.feature[node_id]] threshold = tree.threshold[node_id] # 左子树 (<=) left_conditions = conditions + [{ 'feature': feature, 'threshold': threshold, 'operator': '<=' }] recurse(tree.children_left[node_id], left_conditions) # 右子树 (>) right_conditions = conditions + [{ 'feature': feature, 'threshold': threshold, 'operator': '>' }] recurse(tree.children_right[node_id], right_conditions) recurse(0, []) return rules def _get_native_model(self): """获取底层 sklearn 模型. :return: 底层 sklearn 模型对象 """ if hasattr(self.model_, 'get_native_model'): return self.model_.get_native_model() return self.model_ def _extract_from_forest(self) -> List[Dict[str, Any]]: """从随机森林提取规则. :return: 规则列表 """ native_model = self._get_native_model() tasks = [(self, tree, index) for index, tree in enumerate(native_model.estimators_)] extracted = self._parallel_execute( _tree_extract_worker, tasks, task_labels=[f"树 {index}" for index in range(len(tasks))], default_backend="loky", has_parallel_children=False, workload=_mining_workload( self.X_train_, len(tasks), operation="随机森林规则提取", cost_per_item=8.0, capability="process_safe", ), ) return [rule for tree_rules in extracted for rule in tree_rules] def _extract_from_gbdt(self) -> List[Dict[str, Any]]: """从GBDT提取规则. :return: 规则列表 """ native_model = self._get_native_model() tasks = [ (self, native_model.estimators_[index, 0], index) for index in range(native_model.n_estimators_) ] extracted = self._parallel_execute( _gbdt_tree_extract_worker, tasks, task_labels=[f"树 {index}" for index in range(len(tasks))], default_backend="loky", has_parallel_children=False, workload=_mining_workload( self.X_train_, len(tasks), operation="GBDT规则提取", cost_per_item=8.0, capability="process_safe", ), ) return [rule for tree_rules in extracted for rule in tree_rules] def _extract_from_isolation_forest(self) -> List[Dict[str, Any]]: """从孤立森林提取规则. :return: 规则列表 """ native_model = self._get_native_model() # 计算异常分数 scores = native_model.score_samples(self.X_train_) threshold = np.percentile(scores, 10) # 取异常分数最低的10% anomaly_mask = scores < threshold tasks = [ (self, estimator, tree_idx, scores, anomaly_mask) for tree_idx, estimator in enumerate(native_model.estimators_) ] extracted = self._parallel_execute( _isolation_tree_extract_worker, tasks, task_labels=[f"树 {index}" for index in range(len(tasks))], default_backend="loky", has_parallel_children=False, workload=_mining_workload( self.X_train_, len(tasks), operation="孤立森林规则提取", cost_per_item=8.0, capability="process_safe", ), ) return [rule for tree_rules in extracted for rule in tree_rules] def _extract_from_isolation_tree(self, estimator, tree_idx, scores, anomaly_mask): """从一棵孤立树按既有深度和纯度约束提取规则。""" tree = estimator.tree_ rules = [] def extract_path(node_id, conditions, depth): if depth > 3: # 限制深度 return if tree.feature[node_id] == -2: # 叶子 mask = self._apply_conditions(conditions, self.X_train_) hit_count = mask.sum() if hit_count >= 5: anomaly_count = anomaly_mask[mask].sum() purity = anomaly_count / hit_count if purity >= 0.3: # 异常纯度要求 rule = { 'rule_id': len(rules), 'conditions': conditions.copy(), 'predicted_class': 1, 'class_name': 'anomaly', 'class_probability': purity, 'sample_count': int(hit_count), 'tree_id': tree_idx, 'anomaly_score': scores[mask].mean() } rules.append(rule) else: feature = self.feature_names_[tree.feature[node_id]] thresh = tree.threshold[node_id] # 左子树 left_cond = conditions + [{ 'feature': feature, 'threshold': thresh, 'operator': '<=' }] extract_path(tree.children_left[node_id], left_cond, depth + 1) # 右子树 right_cond = conditions + [{ 'feature': feature, 'threshold': thresh, 'operator': '>' }] extract_path(tree.children_right[node_id], right_cond, depth + 1) extract_path(0, [], 0) return rules def _apply_conditions( self, conditions: List[Dict], X: pd.DataFrame ) -> pd.Series: """应用条件到数据. :param conditions: 条件列表 :param X: 数据 :return: 布尔掩码 """ mask = pd.Series(True, index=X.index) for cond in conditions: feature = cond['feature'] threshold = cond['threshold'] operator = cond['operator'] if operator == '<=': mask &= X[feature] <= threshold elif operator == '>': mask &= X[feature] > threshold elif operator == '<': mask &= X[feature] < threshold elif operator == '>=': mask &= X[feature] >= threshold elif operator == '==': mask &= X[feature] == threshold return mask def _calculate_leaf_badrate(self, conditions: List[Dict]) -> float: """计算叶子节点的坏账率. :param conditions: 条件列表 :return: 坏账率 """ if not hasattr(self, 'y_train_'): return 0.5 mask = self._apply_conditions(conditions, self.X_train_) hit_count = mask.sum() if hit_count == 0: return 0.0 hit_bad = self.y_train_[mask].sum() return hit_bad / hit_count def _filter_rules(self, rules: List[Dict]) -> List[Dict]: """根据feature_trends过滤规则. :param rules: 规则列表 :return: 过滤后的规则列表 """ if not self.feature_trends: return rules filtered = [] for rule in rules: valid = True for cond in rule['conditions']: feature = cond['feature'] operator = cond['operator'] if feature in self.feature_trends: trend = self.feature_trends[feature] # 正相关:只保留>方向的规则 if trend == 1 and operator in ['<=', '<']: valid = False break # 负相关:只保留<=方向的规则 if trend == -1 and operator in ['>', '>=']: valid = False break if valid: filtered.append(rule) return filtered def _deduplicate_rules( self, rules: List[Dict], similarity_threshold: float = 0.9 ) -> List[Dict]: """规则去重. :param rules: 规则列表 :param similarity_threshold: 相似度阈值 :return: 去重后的规则列表 """ if not rules: return [] unique_rules = [] for rule in rules: # 生成规则签名 conditions = rule['conditions'] signature = '|'.join([ f"{c['feature']}{c['operator']}{c['threshold']:.4f}" for c in sorted(conditions, key=lambda x: x['feature']) ]) # 检查是否已存在相似规则 is_duplicate = False for existing in unique_rules: existing_sig = '|'.join([ f"{c['feature']}{c['operator']}{c['threshold']:.4f}" for c in sorted(existing['conditions'], key=lambda x: x['feature']) ]) # 计算相似度 if signature == existing_sig: is_duplicate = True break if not is_duplicate: unique_rules.append(rule) return unique_rules def _calculate_rule_importance(self, rule: Dict) -> float: """计算规则重要性. :param rule: 规则字典 :return: 重要性分数 """ sample_count = rule.get('sample_count', 0) class_prob = rule.get('class_probability', 0) # 坏样本权重更高 weight = 2.0 if rule.get('class_name') in ['bad', 'anomaly'] else 1.0 return sample_count * class_prob * weight
[文档] def get_rules( self, top_n: int = 100, min_samples: int = 10, min_confidence: float = 0.0, target: Optional[str] = None, datasets: Optional[pd.DataFrame] = None ) -> List[Rule]: """获取挖掘规则(直接返回Rule对象). 当传入 datasets 时,规则效果通过 Rule.report 计算并回填到 rule.metadata_。 """ if not self.rules_: self.extract_rules() target_col = target or self.target feature_map = resolve_feature_map(self.feature_map) tasks = [] labels = [] for rule_item in self.rules_[:top_n]: if rule_item['sample_count'] < min_samples: continue if rule_item['class_probability'] < min_confidence: continue expr = self._rule_to_string(rule_item) ordinal = len(tasks) tasks.append((ordinal, rule_item, expr, datasets, target_col, feature_map)) labels.append(f"规则 {rule_item.get('rule_id', ordinal)}") return self._parallel_execute( _tree_rule_report_worker, tasks, task_labels=labels, default_backend="threading", has_parallel_children=False, workload=_mining_workload( datasets if datasets is not None else self.X_train_, len(tasks), operation="树规则报告", cost_per_item=10.0, ), )
[文档] def get_rule_objects( self, top_n: int = 100, min_samples: int = 10, min_confidence: float = 0.0, target: Optional[str] = None, datasets: Optional[pd.DataFrame] = None ) -> List[Rule]: """获取Rule对象列表(与get_rules保持一致).""" return self.get_rules( top_n=top_n, min_samples=min_samples, min_confidence=min_confidence, target=target, datasets=datasets )
[文档] def get_rules_dataframe( self, top_n: int = 100, datasets: Optional[pd.DataFrame] = None, target: Optional[str] = None, min_samples: int = 10, min_confidence: float = 0.0 ) -> pd.DataFrame: """获取规则DataFrame(以rule_expr+Rule.report命中结果为准).""" rule_objs = self.get_rules( top_n=top_n, min_samples=min_samples, min_confidence=min_confidence, datasets=datasets, target=target, ) if not rule_objs: return pd.DataFrame() data = [] for i, rule in enumerate(rule_objs): md = getattr(rule, 'metadata_', {}) or {} report_row = { '规则编号': md.get('rule_id', i), '规则表达式': rule.expr, '命中样本数': md.get('命中样本数'), '命中样本占比': md.get('命中样本占比'), '命中坏样本率': md.get('命中坏样本率'), '命中LIFT值': md.get('命中LIFT值'), '坏账改善': md.get('坏账改善'), '风险拒绝比': md.get('风险拒绝比'), } if '入参字段' in md: report_row = { '规则编号': report_row.pop('规则编号'), '入参字段': md['入参字段'], '字段含义': md['字段含义'], **report_row, } data.append(report_row) return pd.DataFrame(data)
def _rule_to_string(self, rule: Dict) -> str: """将规则转换为可用于Rule的表达式字符串. :param rule: 规则字典 :return: pandas eval规则表达式 """ conditions = rule['conditions'] if not conditions: return "True" parts = [] for c in conditions: feature = c['feature'] feature_expr = f"`{feature}`" if not str(feature).isidentifier() else str(feature) threshold = c['threshold'] if isinstance(threshold, np.generic): threshold = threshold.item() parts.append(f"({feature_expr} {c['operator']} {repr(threshold)})") return " & ".join(parts)
[文档] def get_feature_importance(self) -> pd.DataFrame: """获取特征重要性. :return: 特征重要性DataFrame """ # 获取底层模型 native_model = self._get_native_model() if not hasattr(native_model, 'feature_importances_'): raise ValueError(f"算法 '{self._resolved_algorithm}' 不支持特征重要性") importance = native_model.feature_importances_ result = pd.DataFrame({ 'feature': self.feature_names_, 'importance': importance }).sort_values('importance', ascending=False) feature_map = resolve_feature_map(self.feature_map) if feature_map is not None: result.insert(1, '入参字段', result['feature']) result.insert(2, '字段含义', result['feature'].map(feature_map).fillna('')) return result
def _check_fitted(self): """检查是否已拟合.""" if not self.is_fitted_: raise RuntimeError("请先调用fit()方法")