hscredit.report.mining.multi_label 源代码

"""多标签联合规则挖掘.

支持同时针对多个标签(如 MOB3@30 和 MOB6@30)挖掘规则,
并分析规则在不同标签下的有效性差异。
"""

import copy

import numpy as np
import pandas as pd
from typing import Union, List, Dict, Optional, Any
from .base import BaseRuleMiner, _mining_workload
from .single_feature import SingleFeatureRuleMiner


def _multi_label_mining_worker(task):
    """针对单个标签运行完整的单特征候选挖掘。"""
    miner, data, features, label, candidate_min_samples = task
    labels = miner._resolved_labels()
    child_n_jobs = 1 if miner.n_jobs is None or miner.n_jobs in (1, 1.0) else -1
    child = SingleFeatureRuleMiner(
        target=label,
        max_n_bins=miner.n_bins,
        min_lift=1.0,
        min_samples=candidate_min_samples,
        exclude_cols=[lb for lb in labels if lb != label] + (miner.exclude_cols or []),
        n_jobs=child_n_jobs,
        parallel_backend=miner.parallel_backend,
        parallel_config=miner.parallel_config,
    )
    child.fit(data[features + [label]])
    rules = child.get_rules(min_lift=1.0, min_samples=candidate_min_samples)
    ordered_rules = []
    for rule in rules:
        if isinstance(rule, dict):
            expression = rule.get("规则", rule.get("rule", rule.get("expression", "")))
        else:
            expression = getattr(rule, "expr", "")
        if expression:
            ordered_rules.append((expression, rule))
    return label, ordered_rules


[文档] class MultiLabelRuleMiner(BaseRuleMiner): """多标签规则挖掘器. 支持同时针对多个标签挖掘规则,并分析规则在不同标签下的有效性差异。 典型应用场景: - 长短期标签都有效的强规则(稳定拒绝规则) - 仅短期标签有效(可能是偶发风险,谨慎使用) - 仅长期标签有效(长期风险,可做预警规则) - 两标签均无效(噪声规则,丢弃) :param labels: 标签列名列表,如 ['mob3_30', 'mob6_30'] :param label_names: 标签中文名列表,如 ['短期标签(MOB3@30)', '长期标签(MOB6@30)'] :param min_support: 最小支持度(规则覆盖率),默认 0.02 :param min_lift: 最小 LIFT 值,默认 1.5 :param max_rules: 每个特征最大规则数,默认 10 :param n_bins: 数值特征分箱数,默认 10 :param exclude_cols: 需要排除的列名列表 **参考样例** >>> miner = MultiLabelRuleMiner( ... labels=['mob3_30', 'mob6_30'], # 同时分析两个标签 ... label_names=['短期标签(MOB3@30)', '长期标签(MOB6@30)'], ... min_support=0.02, # 规则覆盖率>2% ... min_lift=1.5, # LIFT值>1.5 ... ) >>> miner.fit(df, features=['age', 'income', 'credit_score']) >>> rules = miner.get_rules(effectiveness='both') # 获取两标签均有效的规则 >>> report = miner.get_effectiveness_matrix() # 获取规则有效性矩阵 """ def __setattr__(self, name, value): """仅跟踪构造完成后的外部 target 赋值,将其切换为显式模式。""" if name == 'target' and self.__dict__.get('_track_target_assignment', False): object.__setattr__(self, '_target_is_auto', False) object.__setattr__(self, name, value) def __init__( self, labels: Optional[List[str]] = None, label_names: Optional[List[str]] = None, min_support: float = 0.02, min_lift: float = 1.5, max_rules: int = 10, n_bins: int = 10, exclude_cols: Optional[List[str]] = None, n_jobs: Optional[Union[int, float]] = -1, parallel_backend: Optional[str] = None, parallel_config: Optional[Dict[str, Any]] = None, ): super().__init__( target=labels[0] if labels else 'target', exclude_cols=exclude_cols, n_jobs=n_jobs, parallel_backend=parallel_backend, parallel_config=parallel_config, ) self._auto_target_value = self.target self._target_is_auto = True self._track_target_assignment = True self.labels = labels self.label_names = label_names self.min_support = min_support self.min_lift = min_lift self.max_rules = max_rules self.n_bins = n_bins self._rules: List[Dict[str, Any]] = [] def _resolved_labels(self) -> List[str]: """返回内部使用的标签列表,不改写公开构造参数。""" return list(self.labels) if self.labels is not None else [] def _resolved_label_names(self) -> List[str]: """返回内部展示名称;未提供时按标签名展示。""" labels = self._resolved_labels() return list(self.label_names) if self.label_names is not None else labels def _set_auto_target(self, target: str) -> None: """更新自动派生 target,不触发外部显式赋值标记。""" object.__setattr__(self, 'target', target) object.__setattr__(self, '_auto_target_value', target) object.__setattr__(self, '_target_is_auto', True)
[文档] def fit( self, X: Union[pd.DataFrame, np.ndarray], y=None, features: Optional[List[str]] = None, ) -> 'MultiLabelRuleMiner': """拟合多标签规则挖掘器. :param X: 输入数据 DataFrame,须包含 labels 指定的标签列 :param y: 忽略 :param features: 需要挖掘的特征列表,为 None 时自动选择数值特征 :return: self """ working = copy.deepcopy(self) working._rules = [] if not isinstance(X, pd.DataFrame): raise ValueError("X 必须为 DataFrame,且须包含标签列") df = X.copy() labels = working._resolved_labels() label_names = working._resolved_label_names() if getattr(working, '_target_is_auto', True): working._set_auto_target(labels[0] if labels else 'target') # 验证标签列存在 missing_labels = [lb for lb in labels if lb not in df.columns] if missing_labels: raise ValueError(f"标签列缺失: {missing_labels}") # 确定特征列 if features is None: exclude = set(labels) | set(working.exclude_cols or []) features = [c for c in df.columns if c not in exclude and pd.api.types.is_numeric_dtype(df[c])] # 对每个标签独立运行单特征规则挖掘 label_rules = {} # {label: {rule_expr: rule}} all_rule_exprs = [] seen_rule_exprs = set() # 候选规则放宽 LIFT/样本量约束,最终是否有效由各标签下的 min_lift 单独判定 candidate_min_samples = max(1, int(working.min_support * len(df))) tasks = [ (working, df, list(features), label, candidate_min_samples) for label in labels ] mined_by_label = working._parallel_execute( _multi_label_mining_worker, tasks, task_labels=labels, default_backend="threading", has_parallel_children=True, workload=_mining_workload( df, len(tasks), operation="多标签规则挖掘", cost_per_item=20.0, has_parallel_children=True, ), ) for label, rules in mined_by_label: label_rules[label] = {} for expression, rule in rules: label_rules[label][expression] = rule if expression not in seen_rule_exprs: seen_rule_exprs.add(expression) all_rule_exprs.append(expression) # 合并规则,为每条规则计算各标签的指标 merged_rules = [] for expr in all_rule_exprs: mask = df.eval(expr) n_match = mask.sum() support = n_match / len(df) if support < working.min_support: continue rule_info = { '规则': expr, '覆盖样本数': int(n_match), '覆盖率': round(support * 100, 2), } effective_labels = [] for i, label in enumerate(labels): lname = label_names[i] if i < len(label_names) else label overall_rate = df[label].mean() rule_rate = df.loc[mask, label].mean() if n_match > 0 else 0 lift = rule_rate / overall_rate if overall_rate > 0 else 0 rule_info[f'{lname}_坏率'] = round(rule_rate * 100, 2) rule_info[f'{lname}_LIFT'] = round(lift, 4) rule_info[f'{lname}_有效'] = lift >= working.min_lift if lift >= working.min_lift: effective_labels.append(lname) # 判断规则分类 if len(effective_labels) == len(labels): rule_info['规则类型'] = '强规则(全标签有效)' rule_info['建议'] = '稳定拒绝规则' elif len(effective_labels) > 0: rule_info['规则类型'] = f'部分有效({",".join(effective_labels)})' rule_info['建议'] = '谨慎使用/预警规则' else: rule_info['规则类型'] = '无效规则' rule_info['建议'] = '放弃' merged_rules.append(rule_info) # 按第一个标签的 LIFT 降序排序 if labels: first_lift_col = ( f'{label_names[0]}_LIFT' if label_names else f'{labels[0]}_LIFT' ) merged_rules.sort(key=lambda r: r.get(first_lift_col, 0), reverse=True) working._rules = merged_rules working._is_fitted = True self._commit_fitted_state(working) return self
[文档] def get_rules( self, effectiveness: str = 'any', min_lift_per_label: Optional[float] = None, top_n: Optional[int] = None, ) -> pd.DataFrame: """获取筛选后的规则表. :param effectiveness: 筛选条件 - 'both': 所有标签均有效 - 'any': 任一标签有效(默认) - 'short_only': 仅第一个标签有效 - 'long_only': 仅最后一个标签有效 - 'all': 不做筛选 :param min_lift_per_label: 覆盖最小 LIFT 阈值,为 None 时使用 self.min_lift :param top_n: 返回前 N 条规则 :return: 规则 DataFrame """ if not self._is_fitted: raise ValueError("请先调用 fit()") min_lift = self.min_lift if min_lift_per_label is None else min_lift_per_label rules = self._rules.copy() labels = self._resolved_labels() label_names = self._resolved_label_names() lift_columns = [ f'{label_names[i]}_LIFT' if i < len(label_names) else f'{label}_LIFT' for i, label in enumerate(labels) ] def effective(rule): return [rule.get(column, 0) >= min_lift for column in lift_columns] if effectiveness == 'both': rules = [rule for rule in rules if all(effective(rule))] elif effectiveness == 'any': rules = [rule for rule in rules if any(effective(rule))] elif effectiveness == 'short_only': rules = [rule for rule in rules if effective(rule)[0]] elif effectiveness == 'long_only': rules = [rule for rule in rules if effective(rule)[-1]] if top_n: rules = rules[:top_n] return pd.DataFrame(rules)
[文档] def get_effectiveness_matrix(self) -> pd.DataFrame: """规则有效性矩阵:行=规则,列=各标签,格=LIFT值. :return: 有效性矩阵 DataFrame """ if not self._is_fitted: raise ValueError("请先调用 fit()") labels = self._resolved_labels() label_names = self._resolved_label_names() rows = [] for rule in self._rules: row = {'规则': rule['规则']} for i, label in enumerate(labels): lname = label_names[i] if i < len(label_names) else label row[f'{lname}_LIFT'] = rule.get(f'{lname}_LIFT', 0) row['规则类型'] = rule.get('规则类型', '') rows.append(row) return pd.DataFrame(rows)
[文档] def get_report(self) -> pd.DataFrame: """获取完整规则分析报告. :return: 含规则分类和业务解读的 DataFrame """ return self.get_rules(effectiveness='all')