跳转至

拒绝推断与分布适配

建模样本通常只来自审批通过的客户,会引入选择偏差。SuperModelingFactory 提供拒绝推断样本加权两类工具来修正。

1. 拒绝推断 —— RejectInferenceFactory

工厂方法

from Modeling_Tool import RejectInferenceFactory

inferrer = RejectInferenceFactory.create(
    method="parceling",
    target_col="bad_flag",
    score_col="prob",
)
df_combined = inferrer.infer(
    df_approved=approved_df,
    df_rejected=rejected_df,
    score_col="prob",
)

支持的推断方法

方法 适用场景 思路
hard_cutoff 老模型评分阈值明确 拒绝样本中分数 > 阈值的标记为坏
fuzzy 阈值附近过渡 按分数分段给予不同的坏样本概率
parceling 分箱映射 把拒绝样本按分数分箱,按箱坏率赋标签
simple_augment 快速近似 拒绝样本按整体坏样本率赋标签

各类使用示例

from Modeling_Tool import ParcelingInferrer

inferrer = ParcelingInferrer(target_col="bad_flag", score_col="prob", n_parcels=10)
df_combined = inferrer.infer(approved_df, rejected_df, score_col="prob")
from Modeling_Tool import HardCutoffInferrer

inferrer = HardCutoffInferrer(
    target_col="bad_flag",
    score_col="prob",
    cutoff=0.5,
)
df_combined = inferrer.infer(approved_df, rejected_df, score_col="prob")
from Modeling_Tool import FuzzyAugmentInferrer

inferrer = FuzzyAugmentInferrer(
    target_col="bad_flag",
    score_col="prob",
    weight_factor=0.8,
)
df_combined = inferrer.infer(approved_df, rejected_df, score_col="prob")
from Modeling_Tool import SimpleAugmentInferrer

inferrer = SimpleAugmentInferrer(target_col="bad_flag", score_col="prob")
df_combined = inferrer.infer(approved_df, rejected_df, score_col="prob")

自定义推断器

继承 RejectInferrer 抽象类(infer(self, df_approved, df_rejected, score_col=None)):

import pandas as pd
from Modeling_Tool import RejectInferrer

class MyInferrer(RejectInferrer):
    def infer(self, df_approved, df_rejected, score_col=None):
        # 自定义逻辑
        score_col = score_col or self.score_col
        df_rejected = df_rejected.copy()
        df_rejected[self.target_col] = 0   # 默认全部好
        df_rejected.loc[df_rejected[score_col] > 0.3, self.target_col] = 1
        return pd.concat([df_approved, df_rejected], ignore_index=True)

2. 分布适配 —— DistributionAdaptation

用于训练集与 OOT 分布不一致的场景。

密度比估计

from Modeling_Tool import DistributionAdaptation

adapter = DistributionAdaptation(method="density_ratio")
sample_weights = adapter.estimate_density_ratio(
    X_train=train_df[features],
    X_oot=oot_df[features],
)

# 在训练时使用 sample_weight
import lightgbm as lgb
dtrain = lgb.Dataset(train_X, train_y, weight=sample_weights)

协变量偏移修正

adapter = DistributionAdaptation(method="covariate_shift")
weights = adapter.covariate_shift_weighting(train_X, oot_X)

fit / get_weights 接口

也可用统一的 fit(X_train, X_oot) + get_weights()

adapter = DistributionAdaptation(method="density_ratio")
adapter.fit(X_train=train_df[features], X_oot=oot_df[features])
sample_weights = adapter.get_weights()

3. 完整流水线

from Modeling_Tool import RejectInferenceFactory, DistributionAdaptation

# 1) 拒绝推断(先扩样本)
inferrer = RejectInferenceFactory.create("parceling", target_col="bad_flag", score_col="prob")
df_combined = inferrer.infer(approved_df, rejected_df, score_col="prob")

# 2) 分布适配(再加权)
adapter = DistributionAdaptation(method="density_ratio")
adapter.fit(X_train=df_combined[features], X_oot=oot_df[features])
sample_weights = adapter.get_weights()

# sample_weights 可作为下游训练器的样本权重(当前 SMF 无内置加权训练接口)。

4. Pipeline 层:样本权重自动传递

v0.3.16 起,RejectInferencePipeline 内部会把每个 RI 方法生成的 _weight 列自动作为 sample_weight 传给下游 GradientBoostingModel.fit,无需用户显式配置:

RI 方法 是否生成 _weight 训练时是否加权
simple_augment ❌(每条拒绝样本被复制成 1 条 good,权重视为 1.0)
hard_cutoff
fuzzy_augment ✅(每条拒绝样本被复制成 good + bad 两行,权重分别为 1-p(bad)p(bad)
parceling ✅(按 score 分段随机赋标签,权重按段内 bad 率)

历史 bug(v0.3.15 及以前)

在 v0.3.15 及更早版本中,RejectInferencePipeline_weightwgt= 传给 GradientBoostingModel.fit。由于 fit 的正确参数名是 sample_weightwgt 会被 **kwargs 静默吞掉,样本权重从未真正生效。 v0.3.16 已修复,fuzzy_augment / parceling 的加权训练现在按预期工作。

如果你在 v0.3.15 或更早版本训练过 RI 后模型,建议在 v0.3.16 重新训练一次, 对比 KS/AUC 差异确认权重是否显著影响模型。

5. Pipeline 层:输入 & 配置校验(v0.3.17)

v0.3.17 收紧了 RejectInferencePipeline 的 fail-fast 校验,把两类此前会在深层 代码里报错或静默数据泄漏的情况都抬到 run() 起始的 _validate_input / _validate_ri_approved_config 阶段。

5.1 隐式 prescore 需要 target_col

即使 train_prescore=False,只要输入 DataFrame 中缺少 score_col(默认 "prescore_prob"),Pipeline 仍会走一次隐式 prescore 训练(_run 内部逻辑: if cfg.train_prescore or cfg.score_col not in data.columns: _fit_prescore(...))。

历史版本上,_validate_input 只在 train_prescore=True 时才检查 target_col 是否存在,所以上述隐式路径下缺 target 会在 _fit_prescore 里报一个位置奇怪的 KeyError。v0.3.17 起,只要 Pipeline 判断出任何一条 prescore 训练路径会被触发_validate_input 就在最外层显式 raise KeyError,报错信息包含 train_prescorescore_col 的实际状态,便于定位。

对应的合法组合:

train_prescore score_col 是否在数据中 target_col 是否必需
True 有或无 ✅ 必需
False ✅ 已提供 ❌ 可选(Pipeline 直接使用外部分数)
False ❌ 缺失(隐式训练) ✅ 必需(v0.3.17 起显式要求)

5.2 oot_frac + ri_validation_frac 必须 < 1.0

RejectInferencePipeline._sample_validation_ids 会在按 oot_frac 抽出 OOT 之后, 从剩余样本里再按 ri_validation_frac 抽验证集。如果两者之和 ≥ 1,剩余池会为空, 历史版本会静默 fallback 到 pool = approved.copy(),验证集因此会与 OOT 有交集, 造成 OOT 行泄漏进验证指标

v0.3.17 起,_validate_ri_approved_config 会在 run() 起始直接抛 ValueError

cfg = RejectInferencePipelineConfig(oot_frac=0.5, ri_validation_frac=0.5)
RejectInferencePipeline(cfg).run(df)
# ValueError: oot_frac (0.5) + ri_validation_frac (0.5) = 1.000 must be < 1.0
# to leave a non-empty training pool disjoint from OOT and validation

同时新增了单独的边界约束:oot_frac 必须在 [0, 1) 内(0 允许,表示禁用 OOT 切分)。

6. Pipeline 层:Pre-score 加固(v0.3.18)

v0.3.18 起,RejectInferencePipeline 对 pre-score 训练路径新增两处显式校验, 用来在早期暴露过去只能靠观察输出才发现的静默错误。

6.1 score_col 覆盖警告

当同时满足以下两个条件时:

  • cfg.train_prescore=True(用户显式要求训练 pre-score)
  • cfg.score_col 对应的列已存在于输入 DataFrame 中

Pipeline 会在真正开始训练前触发 UserWarning

train_prescore=True will overwrite existing column 'prescore_prob' in input data.
Set train_prescore=False to reuse the supplied scores, or rename cfg.score_col to
a fresh column name to avoid this overwrite.

历史行为(v0.3.17 及以前):Pipeline 会直接用新训练的模型分数覆盖用户提供的列, 没有任何提示。想要保留原有分数的调用方通常在下游对比时才发现分数被换过。

处理建议:

  • 想复用已有分数 → train_prescore=False
  • 想训练新分数但保留旧列 → 把 cfg.score_col 改成一个不存在的新列名(例如 "prescore_prob_v2");
  • 确认要覆盖 → 忽略警告即可,行为与 0.3.17 一致。

6.2 空 approved 样本快速失败

_fit_prescore 会先过滤出 (approved_col==1) & target_col.notna() 的行。若该子集为空, v0.3.17 及以前会一路把空 DataFrame 传给 SampleSplitter,最终在 sklearn 内部报一个 难以定位的 stratify 错误。v0.3.18 起改为在 _fit_prescore 入口直接抛出:

ValueError: Cannot train pre-score model: no rows satisfy
('approved'==1 & 'badflag' not null). Diagnostics: approved=0,
target-observed=0, total=1200. Check approved_col/target_col semantics or
supply a pre-computed score_col and set train_prescore=False.

错误信息里同时给出 approved 计数、target-observed 计数和总行数,方便快速判断是审批标识 错了、标签缺失、还是批次全部被拒。

常见问题

哪种拒绝推断方法最准确

学术上没有定论。Parceling 在多数实证研究中表现稳定,建议优先尝试; 业务上一般配合 AB 测试验证。

推断后的样本要不要做 SMOTE

通常不需要。拒绝推断已经增加了少数类样本,再做 SMOTE 可能放大噪声。

OOT 样本量极小(< 1000)时如何评估加权效果
  1. 把 OOT 拆成多段时间窗口分别评估
  2. 用 PSI 而非 AUC 做主要指标