news 2026/10/28 18:11:31

从卡方分箱到标准分:申请评分卡全链路实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从卡方分箱到标准分:申请评分卡全链路实战

简介:面向金融信贷风控与数据挖掘学习者的Python申请评分卡模型数据集,围绕申请评分卡从数据准备到规则落地的完整链路,提供可训练、可复现的样本与配套代码,适合希望理解信用风险量化建模的初中级读者。压缩包共14个文件、约9.22MB,包含7个pkl、5个py、1个csv及1个模型权重文件:pkl保存训练测试数据、分箱与WOE映射等中间产物,py覆盖建模流程和评分卡函数,csv提供原始申请样本,便于按模块对照学习。已有1704人学习下载。读者可据此复现数据清洗、特征选择、分箱与WOE转换、逻辑回归及树模型训练评估,并借助模型文件与编码字典理解评分映射和评分卡生成过程,积累风控建模中的排错与调参思路。

1. 申请评分卡要先想清分数怎么解释,再考虑怎么算

风控团队里流传一句话:评分卡往往是讨论出来的,而非单纯算出来的。业务要分数能解释,算法要指标好看,而多数从网上拿到的 python 评分卡 申请评分 数据集,打开只有一张宽表和一段lr.fit(),中间的分箱、WOE 映射、刻度换算全被省略了。这份资源把申请评分(A 卡)从原始申请表字段到标准分的链路完整跑通:好坏样本定义、缺失与异常处理、变量初筛、卡方分箱、WOE 编码、逻辑回归系数符号校验、KS/AUC/PSI 评估,最后落到一张能交给审批系统打分的评分卡表。它适合两类人:想入门信贷风控建模的开发者,以及需要快速搭一套可解释基线、再做二次开发的数据分析同学。

2. 项目结构与技术栈:从数据字典到评分卡的目录骨架

拿到一份数据集,我通常会先不看代码,而是花十分钟看字段和目录。字段决定你能不能用它讲完整个业务故事,目录决定你接手后改起来疼不疼。这类申请评分卡项目有个共同特征:数据不复杂,但每一步的口径都敏感,错一个定义,后面所有指标准确度都会明显下降。

2.1 数据集字段长什么样:四类变量加一个标签

典型的申请评分卡宽表按来源可以分成四组,理解分组比记住字段名重要,因为后面做变量筛选和分箱约束时,分组方式直接决定你怎么定规则。

字段组代表字段建模里的角色
身份与人口属性age、gender、education、marital_status、city_level弱变量居多,主要用来做业务解释和分层
申请信息loan_amount、term、purpose、income、dti强业务变量,但要防「申请时点不可得」的泄漏
征信与多头query_count_3m、overdue_count_6m、credit_utilA 卡的主力变量,IV 通常最高
行为与设备device_score、app_use_days加分项,缺失率高,口径不稳
标签target1 表示坏样本,0 表示好样本

这里最容易踩的坑是标签口径。常见做法是用「放款后前 6 个月出现 M1+(逾期超过 30 天)」定义为坏,观察期结束仍正常还款的定义为好,中间处于灰色状态的样本(比如只还了几期就结清)要么剔除,要么单独标记。我在项目里一般会在数据字典里写死三件事:表现期多长、坏的定义阈值、灰样本怎么处理。这三条不写清,别人复现你的结果一定是另一套数字,这个差异不能只归因于随机种子。

2.2 目录与技术栈:让脚本能一口气跑完

这类项目常见的目录组织方式是这样的,核心思路是把「配置」「数据」「流程」「产出」四层分开,避免所有逻辑挤在一个 notebook 里:

scorecard_project/ ├── conf/ │ ├── config.yaml # 路径、字段名、分箱参数、刻度参数 │ └── rules.py # 业务规则清洗,比如年龄上下限、收入截断 ├── data/ │ ├── raw/ # 原始申请宽表 │ └── processed/ # 切分后的 train / test / oot ├── src/ │ ├── preprocess.py # 缺失、异常、样本切分 │ ├── selection.py # IV、相关性、VIF 三件套 │ ├── binning.py # 卡方分箱 + 单调性合并 │ ├── woe.py # WOE 映射表生成与套用 │ ├── model.py # 逻辑回归训练与系数校验 │ └── scorecard.py # A/B 刻度换算,输出评分卡表 ├── output/ │ ├── model.pkl │ ├── scorecard.csv # 上线用的打分表 │ └── report/ # KS、PSI、评分分布图 └── run.py # 串起全流程的入口

技术栈上不用追求新潮,申请评分卡的诉求是可解释、可复现、可审计,所以选型偏保守:pandas + numpy 做数据处理,scikit-learn 训练逻辑回归,matplotlib 出分布图,joblib 落模型,PyYAML 管配置。分箱如果不想手写,可以用 scorecardpy 或 toad 这类专门做评分卡的库,但我建议至少自己实现一遍卡方分箱,因为线上出问题时你要能说清每个箱的边界是怎么来的。

2.3 环境搭建与依赖锁定

环境这一步经常被跳过,然后在新机器上因为版本差异导致分箱结果对不上。我一般直接锁版本,把依赖写进 requirements 里:

python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install pandas==2.1.4 numpy==1.26.2 scikit-learn==1.3.2 \ matplotlib==3.8.2 joblib==1.3.2 pyyaml==6.0.1 pip freeze > requirements.txt

参数上没什么玄学,关键是 numpy 和 pandas 的版本组合别乱跳,GROUPBY 在observed参数上的行为差异会让分箱统计少一行,这种问题排查起来非常费时。装完之后先跑一遍python -c "import pandas, sklearn; print(pandas.__version__)",确认解释器没串到系统环境,再往下走。

3. 数据清洗与变量初筛:把申请表变成可建模的宽表

数据清洗这一章看着枯燥,但它决定了模型上限。评分卡项目里有句话:分箱决定下限,变量决定上限,清洗决定你有没有资格谈上限。我见过太多人拿到数据直接fillna(0)然后开始调参,最后模型在测试集上 AUC 0.85,一上 OOT 直接掉到 0.6,原因往往藏在清洗阶段。

3.1 好坏样本定义与时间切分

先切时间,再谈其他。申请评分卡最忌讳随机切分,因为随机切分会让未来信息泄漏到训练集,指标虚高得离谱。常见的做法是按申请月份切三段:

import pandas as pd df = pd.read_parquet("data/raw/apply_base.parquet") df["apply_month"] = pd.to_datetime(df["apply_date"]).dt.to_period("M") # 训练集:较早的月份;测试集:中间月份;OOT:最近月份 train = df[df["apply_month"] <= "2023-06"] test = df[(df["apply_month"] > "2023-06") & (df["apply_month"] <= "2023-09")] oot = df[df["apply_month"] > "2023-09"] # 剔除灰样本:既没坏也没走完表现期的记录 for name, part in [("train", train), ("test", test), ("oot", oot)]: part = part[part["target"].notna()] print(name, part.shape, "坏样本率 %.4f" % part["target"].mean())

逻辑说明:apply_month用to_period("M")转成月份粒度,方便按窗口切片;OOT 必须是时间上最靠后的一段,这样验证的才是「未来样本」上的表现。参数上,训练集比例一般占到 60% 到 70%,测试集用于调参和选模型,OOT 只在前两者都定稿后跑一次,跑多了就等于拿 OOT 调参,验证意义就没了。坏样本率要打印出来看,各段之间差异超过一倍,说明业务策略或客群发生了明显变化,这时候先别建模,去问业务发生了什么。

3.2 缺失值、异常值与业务规则清洗

缺失值不要一律填 0,先分清缺失类型。征信查询次数缺失,可能是「从没查过」,填 0 是合理的;收入缺失更可能是「客户没填」,填 0 会把好人误判成高危。我的处理习惯是给每个高缺失字段加一个xxx_isnull指示变量,缺失值本身用中位数或特殊值填补,让模型自己学缺失有没有信息量。

import numpy as np def clean_apply(df): df = df.copy() # 1) 业务规则:年龄不在 18-65 的直接剔除,属于不可授信人群 df = df[(df["age"] >= 18) & (df["age"] <= 65)] # 2) 收入做截断,避免极端值把分箱边界拉飞 df["income"] = df["income"].clip(lower=0, upper=df["income"].quantile(0.99)) # 3) 高缺失字段保留缺失指示,再用中位数填补 for col in ["credit_util", "query_count_3m"]: df[f"{col}_isnull"] = df[col].isna().astype(int) df[col] = df[col].fillna(df[col].median()) # 4) 类别型字段统一转小写,防止「本科 / 本科 」这种脏值分裂成两箱 for col in ["education", "purpose", "marital_status"]: df[col] = df[col].astype(str).str.strip().str.lower() return df train, test, oot = map(clean_apply, [train, test, oot])

逻辑说明:第 1 步是硬规则,不符合授信年龄的直接出局,不要指望模型学;第 2 步的 99 分位截断是我比较常用的做法,既保留分布形态又不会被几个极端值带偏;第 3 步的缺失指示变量在信用场景里经常能贡献几个 IV 点,值得保留;第 4 步看着琐碎,但字符串清洗不做,后面分箱时你会看到「本科」和「本科 」各占一箱,WOE 还是反的,白白增加一项排查工作。

还有一条血泪经验:清洗逻辑必须作用在训练集上再套到测试和 OOT,中位数、分位点这些统计量只能用训练集算。我见过直接在全量数据上算 median 的写法,指标好看,上线就翻车。

3.3 IV、相关性与 VIF 三件套筛变量

变量筛选分三步走,顺序别乱。先算 IV 过滤掉几乎没区分度的字段,再看两两相关性剔除冗余,最后用 VIF 处理多重共线性。逻辑回归对共线性很敏感,两个高度相关的变量同时进模型,系数符号可能反掉,解释起来非常尴尬。

import numpy as np def calc_iv(df, col, target="target", bins=10, eps=1e-6): """对连续变量先等频分箱,再算每箱 WOE 和总 IV""" s = df[[col, target]].copy() if s[col].nunique() > bins: s["bin"] = pd.qcut(s[col], q=bins, duplicates="drop") else: s["bin"] = s[col] g = s.groupby("bin", observed=True)[target].agg(["count", "sum"]) g.columns = ["total", "bad"] g["good"] = g["total"] - g["bad"] bad_all, good_all = g["bad"].sum(), g["good"].sum() g["woe"] = np.log(((g["bad"] + eps) / bad_all) / ((g["good"] + eps) / good_all)) g["iv"] = (g["bad"] / bad_all - g["good"] / good_all) * g["woe"] return g["iv"].sum() features = [c for c in train.columns if c not in ("target", "apply_date", "apply_month")] iv_table = pd.Series({c: calc_iv(train, c) for c in features}).sort_values(ascending=False) print(iv_table.head(20))

逻辑说明:qcut做等频分箱,duplicates="drop"处理大量重复值的情况;eps是为了防止某一箱坏样本或好样本为 0 时取对数报错。参数上,IV 的经验阈值是:低于 0.02 基本没用,0.02 到 0.1 是弱变量,0.1 到 0.3 中等,0.3 以上强,但超过 0.5 要警惕,多半是标签泄漏或者分箱过细导致的虚高。算完 IV 之后,相关性高于 0.7 的两两变量保留 IV 高的那个,VIF 大于 5 的逐个剔除,这三步做完通常能从几十个字段收敛到 10 到 15 个进模型。

4. 分箱、WOE 与逻辑回归:一条样本走到最终分数

这是整份资源最值得看的部分,也是绝大多数 demo 省略的部分。申请评分卡之所以在监管场景下站得住,靠的就是「每个箱有明确的 WOE、每个变量有明确的系数、每一分能反推回 odds」这条闭环。跳过这一层,模型就是个黑匣子。

4.1 卡方分箱与单调性约束

分箱的目标不是分得越细越好,而是让每个箱内的坏样本率稳定、相邻箱之间有趋势。工业界常用卡方分箱做初始切分,再强制合并保证单调。下面是我常用的简化实现,先把连续变量切成 20 个等频初始箱,再自底向上合并相邻且 WOE 差异最小的箱,直到满足箱数上限和单调性:

import numpy as np import pandas as pd def mono_bin(df, col, target="target", max_bins=8, min_pct=0.05): s = df[[col, target]].dropna().copy() # 初始切 20 段等频箱 s["bin"] = pd.qcut(s[col], q=20, duplicates="drop") stat = s.groupby("bin", observed=True)[target].agg(["count", "sum"]) stat.columns = ["total", "bad"] stat["bad_rate"] = stat["bad"] / stat["total"] # 合并小箱:样本占比低于 min_pct 的与相邻箱合并 while len(stat) > max_bins: stat["pct"] = stat["total"] / stat["total"].sum() idx = stat["pct"].idxmin() pos = stat.index.get_loc(idx) merge_to = max(0, pos - 1) if pos > 0 else 1 left, right = sorted([pos, merge_to]) new_bin = stat.index[left] stat.iloc[left] = stat.iloc[left] + stat.iloc[right] stat = stat.drop(stat.index[right]) stat["bad_rate"] = stat["bad"] / stat["total"] # 单调性检查:坏样本率不单调则合并差异最小的相邻箱 while len(stat) > 2: diffs = np.abs(np.diff(stat["bad_rate"].values)) i = int(np.argmin(diffs)) stat.iloc[i] = stat.iloc[i] + stat.iloc[i + 1] stat = stat.drop(stat.index[i + 1]) stat["bad_rate"] = stat["bad"] / stat["total"] rates = stat["bad_rate"].values if np.all(np.diff(rates) >= 0) or np.all(np.diff(rates) <= 0): break return stat

逻辑说明:第一段qcut(q=20)是初始粗分,箱太细会导致 WOE 抖动;min_pct=0.05控制最小箱样本占比,小于 5% 的箱和邻居合并,避免出现 3 个样本一箱的情况,这种箱在 OOT 上根本稳不住;单调性循环里用np.diff找坏样本率差异最小的一对相邻箱合并,直到整体单调或只剩两箱。参数上max_bins=8是经验值,变量多的时候 5 到 6 箱也够用,箱越多越容易过拟合,也越难在评分卡表里向业务解释。

分箱结果一定要人工过一遍。我一般会把每个箱的边界、样本量、坏样本率导成表,重点看三件事:边界是不是落在业务上有意义的点(比如查询次数 3 次、6 次),有没有哪个箱样本量特别少,坏样本率有没有反常识的跳变。发现异常先回头查数据,不要急着让算法去修。

4.2 WOE 映射:训练集算,全量套

WOE 编码的本质是把每个箱映射成一个对数几率,让逻辑回归能直接吃类别信息。这里有个纪律:WOE 映射表只能用训练集算,然后原样套到测试集和 OOT 上。用全量算 WOE 是最典型的泄漏之一,指标会虚高 3 到 5 个点。

def fit_woe_map(df, col, bin_edges, target="target", eps=1e-6): """在训练集上拟合 WOE 映射表""" s = df[[col, target]].copy() s["bin"] = pd.cut(s[col], bins=bin_edges, include_lowest=True) g = s.groupby("bin", observed=True)[target].agg(["count", "sum"]) g.columns = ["total", "bad"] g["good"] = g["total"] - g["bad"] bad_all, good_all = g["bad"].sum(), g["good"].sum() g["woe"] = np.log(((g["bad"] + eps) / bad_all) / ((g["good"] + eps) / good_all)) g["iv"] = (g["bad"] / bad_all - g["good"] / good_all) * g["woe"] return g[["woe", "iv"]].reset_index() def transform_woe(df, col, woe_map): """把 WOE 表套到任意数据集上""" s = df.copy() s["bin"] = pd.cut(s[col], bins=woe_map["bin"].cat.categories, include_lowest=True) s = s.merge(woe_map[["bin", "woe"]], on="bin", how="left") # 套不上的箱(OOT 出现超出训练集范围的取值)统一填 0,等价于中性 return s["woe"].fillna(0).values

逻辑说明:fit_woe_map里的pd.cut用的是固定的bin_edges,这个边界必须来自训练集分箱结果,不能每次重新切;transform_woe用merge把 WOE 贴回去,套不上的填 0。参数上eps同样防零;填 0 这个动作要在日志里记下来,如果 OOT 有大量样本落在训练集边界之外,说明客群漂移了,得回头审视分箱边界是不是切得太窄。

WOE 的符号方向要统一。坏样本占比越高 WOE 应该越负(用坏/好算的话),这样模型系数为正时代表该变量越危险分越低,解释起来才顺。项目里我一般会在生成映射表之后强制检查一遍符号,有反的就调换分子分母,别留着让后面的人猜。

4.3 逻辑回归训练与系数符号校验

评分卡用逻辑回归不是为了精度,是为了可解释和可审计。训练本身几行代码,难的是训完之后那几条校验:系数符号是否符合业务常识、p 值是否显著、VIF 是否可控。

from sklearn.linear_model import LogisticRegression import numpy as np # X_woe 是各变量 WOE 拼成的矩阵 model = LogisticRegression( penalty="l2", C=1.0, solver="lbfgs", class_weight="balanced", max_iter=1000, random_state=42 ) model.fit(X_woe_train, y_train) coef = pd.Series(model.coef_[0], index=X_woe_train.columns).sort_values() print("截距 %.4f" % model.intercept_[0]) print(coef) # 符号校验:WOE 用「坏/好」定义时,危险变量系数应为负 expected_negative = ["query_count_3m_woe", "overdue_count_6m_woe", "credit_util_woe"] bad_sign = [c for c in expected_negative if coef.get(c, 0) > 0] print("符号异常的变量:", bad_sign)

逻辑说明:C=1.0是正则强度的倒数,评分卡场景不建议调得太小,系数被压得太狠会失去业务含义;class_weight="balanced"处理好坏样本不均衡,坏样本占比通常只有 3% 到 8%;max_iter=1000是为了避免 lbfgs 在标准化不好的数据上不收敛。校验那段是关键:如果 WOE 按「坏样本占比高 → WOE 负」定义,那风险变量的系数应该是负的,分数才会随风险升高而下降;出现正号先别改代码,回去看这个变量的分箱是不是反了,或者 WOE 的分子分母写颠倒了。

还有一点,进模型前把 WOE 矩阵做一次标准化不是必须的,但收敛会更稳,系数也更方便比较大小。要不要保留截距取决于你的刻度换算方式,我习惯保留,后面直接进公式。

4.4 从 odds 到标准分:A/B 刻度换算

最后一步是把模型输出的对数几率换算成业务能看懂的标准分。公式不复杂,但参数定义一定要写清,不然交接的时候又是一轮扯皮:

import numpy as np def logit_to_score(logit_bad, base_score=600, base_bad_odds=1/20, pdo=50): """ logit_bad: 模型输出的坏样本对数几率 ln(p/(1-p)) base_score: 基准分,对应 base_bad_odds 这个坏 odds base_bad_odds: 基准坏 odds,1/20 表示基准人群坏样本概率约 4.76% pdo: 坏 odds 翻倍时分数下降的幅度 """ factor = pdo / np.log(2) offset = base_score + factor * np.log(base_bad_odds) return offset - factor * logit_bad # 用模型系数直接生成评分卡:score = (offset - factor*beta0) - factor*sum(beta_i * woe_i) factor = 50 / np.log(2) offset = 600 + factor * np.log(1/20) base_points = offset - factor * model.intercept_[0] card_rows = [] for col in X_woe_train.columns: beta = coef[col] for _, row in woe_maps[col].iterrows(): points = -factor * beta * row["woe"] card_rows.append({"var": col, "bin": str(row["bin"]), "woe": row["woe"], "points": points}) card = pd.DataFrame(card_rows) print(card.head(), "\n基础分:", round(base_points, 2))

逻辑说明:factor = pdo / ln2是每一倍 odds 对应的分数步长,pdo=50意味着坏 odds 翻一倍,分数降 50 分;offset把基准分锚定到base_bad_odds上,基准坏 odds 取 1/20 是行业里比较常见的设定,对应约 5% 的基准坏账率;base_points是截距项折算出来的基础分,加上各变量每个箱的分值,就是完整的评分卡表。业务同学拿到这张表,看到「查询次数 3 个月内超过 6 次扣 18 分」,比看系数直观得多。

这张表生成后要做一致性回算:随机抽 100 条样本,用评分卡表逐项加总得到的分数,和模型predict_proba换算出来的分数对比,误差应该小于 0.5 分。对不上就是哪一步的 WOE 映射或者符号搞错了,这个回算我每次上线前都做,能挡掉大部分低级错误。

5. 模型验证与上线排查:KS、AUC、PSI 和几个高频翻车现场

模型训完不等于能用。申请评分卡的验证有一套固定动作,但真正让项目卡住的往往不是指标不够,而是一些看起来很小的口径问题。这一章把验证和排查放在一起讲,因为很多指标异常本身就是排查线索。

5.1 区分度、稳定性与分数分布三张表

三个指标各管一段:AUC 和 KS 管区分能力,PSI 管稳定性,分数分布管业务落地的可行性。

from sklearn.metrics import roc_auc_score, roc_curve def ks_auc(y_true, prob): fpr, tpr, _ = roc_curve(y_true, prob) return roc_auc_score(y_true, prob), float(np.max(tpr - fpr)) def psi(base, compare, bins=10): """PSI:以训练集分数分布为基准,比较测试/OOT 是否漂移""" edges = np.quantile(base, np.linspace(0, 1, bins + 1)) edges[0], edges[-1] = -np.inf, np.inf b = np.histogram(base, bins=edges)[0] / len(base) c = np.histogram(compare, bins=edges)[0] / len(compare) b, c = np.clip(b, 1e-6, None), np.clip(c, 1e-6, None) return float(np.sum((c - b) * np.log(c / b))) for name, X_, y_ in [("test", X_woe_test, y_test), ("oot", X_woe_oot, y_oot)]: p = model.predict_proba(X_)[:, 1] auc, ks = ks_auc(y_, p) print(f"{name}: AUC={auc:.4f} KS={ks:.4f} PSI={psi(prob_train, p):.4f}")

逻辑说明:ks_auc直接返回两个核心指标,AUC 低于 0.65 基本不用往下走,KS 在 0.3 以上算可接受,0.4 以上算不错;psi用训练集分数分位点切箱,比较各箱占比变化,经验阈值是小于 0.1 稳定、0.1 到 0.25 需要关注、大于 0.25 就要考虑重训。参数上bins=10是通用设定,样本量小时可以降到 5。三张表要一起看:AUC 掉了但 PSI 正常,多半是标签口径变了;AUC 没掉但 PSI 很大,说明客群结构变了,模型可能在某个人群上悄悄失效。

分数分布还要单独出一张图,看训练集和 OOT 的分数均值、中位数、5% 分位点。业务最关心的是通过率,如果 OOT 上高分人群占比明显缩水,意味着同样 cutoff 下通过率会掉,这个信息要提前同步给策略同学,而不是等上线后被动发现。

5.2 五个高频翻车现场:现象、原因与解决

现象一:训练集 AUC 0.9,OOT 掉到 0.6。原因通常是时间泄漏,比如用了「放款后 3 个月的还款状态」这类申请时点拿不到的字段,或者随机切分让同期样本同时进了训练和测试。解决办法是把字段按「申请时点是否可得」重新过一遍,所有贷后字段直接剔掉,切分强制按时间。

现象二:某个变量 WOE 单调,但模型系数符号反了。原因多半是 WOE 定义方向不统一,有的变量用坏/好,有的用好/坏,混在一起系数就乱了。解决方法是统一在映射表生成阶段固定分母,生成完打印每个变量的 WOE 与坏样本率对照表,符号不对的当场调换。

现象三:分箱在训练集很漂亮,OOT 上大量样本落不进任何箱。原因是分箱边界完全由训练集分位点决定,客群一漂移,超出边界的值就被cut成 NaN。解决办法是在transform_woe里显式处理越界值,把他们归到首尾箱,同时在日志里统计越界比例,超过 5% 就要重新审视分箱策略。

现象四:评分卡回算和模型预测对不上,差几分。原因通常是浮点精度或者 WOE 映射表在小数位上被截断,也可能是基础分里截距项的符号写反。解决办法是回算时保留 6 位小数,把base_points和各项分值分开打印,逐项对比找出偏差来源。

现象五:PSI 正常,但业务反馈通过率异常。原因是分数刻度参数被动过,比如有人为了让分数落在 300 到 850 之间,私自改了base_score或pdo,导致同一份模型输出对应的分数整体平移。解决办法是把刻度参数写进配置文件并做版本管理,任何改动都要跑一遍回算和分布对比,确认分布形态没变。

6. 二次开发:把这份 Demo 改成能进生产的评分卡流水线

把项目跑通只是第一步,真正要用起来,得把脚本式的流程改成配置驱动。我一般会做两件事:把字段清单、分箱参数、刻度参数全部收进config.yaml,让run.py只负责按配置串流程;再给每个阶段加一份中间产物和校验日志,出问题时能定位到具体哪一步。

# conf/config.yaml 的关键片段 data: train_end: "2023-06" test_end: "2023-09" features: numeric: ["age", "income", "query_count_3m", "credit_util"] categorical: ["education", "purpose"] binning: max_bins: 8 min_pct: 0.05 score: base_score: 600 base_bad_odds: 0.05 pdo: 50

配置化的好处是换一版数据、换一个客群,只改 yaml 不动代码,也方便做参数回溯。上线后的监控我建议至少盯三个数:月度 PSI、月度 KS、分数分布均值。PSI 连续两个月超过 0.1,或者 KS 掉超过 20%,就触发重训评估,不要等到坏账抬头才反应。重训也不是简单重跑一遍,新旧模型的分数要先做映射对齐,保证同一批客户的分数不会因为换模型突然跳变,不然策略侧会很难受。

这套流程我踩过的坑里,印象最深的是把 WOE 映射表用全量数据算,指标好看得离谱,上线后 OOT 直接崩。从那以后我每次生成映射表都会强制只传训练集进去,函数签名里直接写死参数名train_df,并且跑完打印一行「映射表来自训练集,样本量 N」。希望这些内容能帮到你,少走几段我当年走过的弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 18:11:31

Linux 下 Qt 嵌入网页:QCefView 编译、桥接与打包排障

简介&#xff1a;面向Linux平台桌面端开发者的QCefView集成示例&#xff0c;适合需要把CEF浏览器内核嵌入Qt界面的开发者参考&#xff0c;可用于理解Linux环境下Qt与CEF结合时的工程组织、接口封装和运行依赖部署。压缩包共85个文件&#xff0c;整体约269.34MB&#xff0c;以pa…

作者头像 李华
网站建设 2026/10/28 18:08:52

Agent长期记忆设计对比:六大开源项目选型指南

最近我在调一个客服Agent&#xff0c;对话轮次超过二十&#xff0c;它就把关键信息忘得干干净净——用户刚说完的收货地址&#xff0c;下一轮就开始“我这边暂时查不到”。这几乎是每个长期做Agent开发的人都绕不过的坎&#xff1a;模型上下文窗口再大&#xff0c;也包不住用户…

作者头像 李华
网站建设 2026/10/28 18:07:56

Linux下QCefView嵌入CEF:Qt编译部署与双向通信避坑

简介&#xff1a;面向 Linux 桌面端开发者的 QCefView 集成示例&#xff0c;聚焦在 Qt 应用中嵌入 CEF 浏览器视图的入门实践&#xff0c;适合具备一定 C/Qt 基础、希望快速理解 QCefView 初始化、页面加载与工程组织方式的读者。压缩包共 85 个文件&#xff0c;约 269.34MB&am…

作者头像 李华
网站建设 2026/10/28 18:07:54

C# WPF 实现 FFmpeg 命令行参数自动生成与转码进度封装

简介&#xff1a;FFmpegAutoGenDemo.rar 是一套面向 C# 与 WPF 开发者的音视频处理示例工程&#xff0c;主要解决在桌面应用中调用 FFmpeg 原生接口时封装繁琐、调试门槛较高的问题。工程借助 FFmpeg.AutoGen 将 C 接口映射为 C# 类库&#xff0c;示例涉及音视频读取与解码、编…

作者头像 李华
网站建设 2026/10/28 18:07:42

YOLO与DeepSeek在智慧农业中的融合应用

1. 项目概述&#xff1a;当YOLO遇上DeepSeek的农业革命在江苏某现代化农场的监控室里&#xff0c;老张正盯着屏幕上实时跳动的数据流——无人机航拍画面中&#xff0c;YOLO算法用红色方框精准标记着每一株出现黄叶病害的水稻&#xff0c;同时DeepSeek大模型正在生成包含病害类型…

作者头像 李华
网站建设 2026/10/28 18:04:37

8位浮点数E4M3与E5M2:深度学习中的高效数值表示

1. 浮点数基础与8位格式的诞生背景浮点数作为计算机科学中最基础的数据表示方式之一&#xff0c;其发展历程映射了整个计算技术的演进轨迹。在深度学习爆发式增长的今天&#xff0c;传统32位单精度浮点&#xff08;FP32&#xff09;甚至64位双精度浮点&#xff08;FP64&#xf…

作者头像 李华