简介:本资源是马上AI全球挑战赛「违约用户风险预测」赛道的亚军解决方案,面向金融科技领域算法工程师、风控建模从业者及具备Python基础的数据科学学习者,聚焦信贷场景下的用户违约概率建模与特征工程实践。压缩包共7个文件(4个核心Python脚本、1份PDF方案说明、1份Markdown文档、1个LICENSE),总大小733KB;其中XGB1.py实现端到端XGBoost建模,feature_engine.py封装多维度用户行为与统计特征提取逻辑,construct_module.py整合流程并输出最终提交文件sample.csv,方案说明.pdf则系统阐述特征构造思路、模型融合策略与关键调参经验。目前已有299人学习下载,读者可直接复现Top2级工业级风控建模链路,获得含数据预处理、特征选择、双模型训练(XGBoost+Logistic Regression)及结果生成的完整代码框架,并参考PDF深入理解竞赛级特征设计逻辑与工程落地细节。
1. 违约风险预测不是“打分排序”,而是用特征工程把黑匣子打开:一份能过答辩、能跑通、能复现的Top2方案拆解
你是不是也试过:模型AUC刷到0.85,但线下验证时一上线就掉点?特征做了几十个,重要性图看着很美,可业务方问“为什么这个用户被标高风险”,你答不出具体逻辑?这不是玄学,是违约预测最常踩的坑——把问题当成了纯算法竞赛,忽略了它本质是个可解释、可落地、可归因的风控决策支持系统。这份来自马上AI全球挑战赛Top2的完整方案,不是只交一个notebook了事,而是一整套从原始数据清洗、业务特征深挖、模型融合策略到结果归因分析的闭环设计。它专为学生毕设/课设、初级算法工程师快速交付风控类项目而生:代码全量可运行、特征构造有业务依据、模型选择不堆参数、结果能反向解释到字段级。如果你正卡在“模型跑得动但讲不清”“特征很多但业务不认”“答辩被问‘这个特征怎么来的’就哑火”的阶段,这套方案就是你的后悔药。
2. 总体架构设计:三层结构与四个核心模块的职责划分
本方案采用清晰的三层处理架构:数据层 → 特征层 → 模型层,每层职责明确、接口稳定,避免常见“代码写到哪算哪”的混乱状态。整个流程由四个核心模块驱动:原始数据解析模块、业务导向特征工程模块、轻量稳健模型融合模块、可解释性结果输出模块。这种结构不是为了炫技,而是为了在答辩时能一句话说清:“数据进来先做一致性校验(数据层),再按逾期行为、还款能力、稳定性三个维度构造特征(特征层),最后用XGBoost主模型+LightGBM辅助模型加权融合(模型层),所有预测结果都附带TOP3贡献特征及方向(结果层)”。下面逐层展开关键实现。
2.1 原始数据解析模块:统一入口与强校验机制
原始数据通常包含用户基础信息表(user_info.csv)、历史账单表(bill_detail.csv)、还款记录表(repay_record.csv)等多张表。本方案不直接拼接,而是定义统一解析器DataLoader,强制执行三项校验:
- 字段类型强约束:如
overdue_days必须为非负整数,repay_amount必须 > 0,否则抛出DataIntegrityError并记录行号; - 时间序列完整性检查:对每个用户,检查账单周期是否连续(如缺失2023-03则报warning);
- ID对齐兜底:使用
pd.merge(..., how='inner')保证后续所有join操作仅保留全量存在用户,杜绝“空值蔓延”。
# data_loader.py import pandas as pd from typing import Dict, Any class DataLoader: def __init__(self, base_path: str): self.base_path = base_path def load_user_info(self) -> pd.DataFrame: df = pd.read_csv(f"{self.base_path}/user_info.csv") # 强制类型转换与校验 df['age'] = pd.to_numeric(df['age'], errors='coerce').fillna(0).astype(int) if (df['age'] < 0).any(): raise ValueError("Age contains negative values at rows: {}".format( df[df['age'] < 0].index.tolist())) return df def load_bill_detail(self) -> pd.DataFrame: df = pd.read_csv(f"{self.base_path}/bill_detail.csv") # 校验账单周期连续性(以月为单位) df['bill_month'] = pd.to_datetime(df['bill_date']).dt.to_period('M') user_months = df.groupby('user_id')['bill_month'].apply(list) for uid, months in user_months.items(): if len(months) > 1: expected = pd.period_range(min(months), max(months), freq='M') if set(months) != set(expected): print(f"[WARN] User {uid} missing bill months: {set(expected) - set(months)}") return df提示:这段代码的关键不在“读文件”,而在校验失败时给出可定位的错误信息。答辩时老师问“数据质量怎么保障”,你就能指着这行
raise ValueError(...)说:“我们把校验嵌在加载环节,任何异常都立刻暴露,而不是让脏数据流到建模层再崩。”
2.2 业务导向特征工程模块:拒绝“暴力枚举”,聚焦三类可解释特征
特征工程不是越多越好,而是要让每个特征都有业务含义、可追溯、可解释。本方案将全部特征归纳为三大类,每类对应风控核心逻辑:
| 特征类别 | 业务含义 | 典型构造方式 | 数量 |
|---|---|---|---|
| 逾期行为类 | 用户历史违约惯性 | 近6期最大逾期天数、近3期逾期次数占比、首次逾期距今月数 | 12个 |
| 还款能力类 | 当前偿债资源水平 | 账单金额均值/收入比、最低还款额完成率、可用额度使用率 | 9个 |
| 稳定性类 | 用户行为可预测性 | 账单金额标准差/均值、还款日期波动天数、设备更换频次 | 7个 |
所有特征均通过FeatureGenerator类封装,确保复现性:
# feature_engineer.py import numpy as np from sklearn.preprocessing import StandardScaler class FeatureGenerator: def __init__(self): self.scaler = StandardScaler() def generate_overdue_features(self, bill_df: pd.DataFrame) -> pd.DataFrame: """生成逾期行为类特征:全部基于bill_detail中overdue_days字段""" grouped = bill_df.groupby('user_id') feats = {} # 近6期最大逾期天数(取最近6条记录) feats['max_overdue_6m'] = grouped['overdue_days'].apply( lambda x: x.nlargest(6).max() if len(x) >= 6 else x.max() ) # 近3期逾期次数占比(overdue_days > 0 即为逾期) feats['overdue_rate_3m'] = grouped.apply( lambda g: (g.nlargest(3, 'bill_date')['overdue_days'] > 0).mean() ) # 首次逾期距今月数(需bill_date转datetime) feats['first_overdue_months_ago'] = grouped.apply( lambda g: ((pd.to_datetime('today') - pd.to_datetime(g.loc[g['overdue_days'] > 0, 'bill_date'].min())) / np.timedelta64(1, 'M')).round(1) if not g[g['overdue_days'] > 0].empty else np.nan ) return pd.DataFrame(feats) def fit_transform(self, X: pd.DataFrame) -> pd.DataFrame: # 仅对数值型特征标准化,保留原始业务含义 numeric_cols = X.select_dtypes(include=[np.number]).columns X_scaled = X.copy() X_scaled[numeric_cols] = self.scaler.fit_transform(X[numeric_cols]) return X_scaled注意:这里
first_overdue_months_ago的计算用了np.timedelta64(1, 'M')而非简单除以30,是因为风控场景中“月”是业务单位,必须按日历月计算,否则会引入系统性偏差。这是某次答辩被追问后补上的细节——业务方说“你们算的3.2个月,我们系统里显示是4个月”,才发现时间粒度不一致。
2.3 轻量稳健模型融合模块:XGBoost为主、LightGBM为辅的双模型加权策略
不追求SOTA模型,而追求鲁棒性 + 可维护性 + 可解释性。本方案放弃深度学习或复杂集成,采用XGBoost(主)+ LightGBM(辅)双模型加权,原因有三:
① XGBoost在小样本、高噪声金融数据上泛化更稳;
② LightGBM训练快,可作为XGBoost的“压力测试”——若两者预测差异大,说明该样本本身存疑;
③ 二者特征重要性可交叉验证,增强业务说服力。
融合权重非固定,而是动态计算:
- 对每个样本,计算两模型预测概率差值
abs(p_xgb - p_lgb); - 差值越小,说明模型共识度越高,权重越向平均靠拢;
- 差值越大,说明该样本难判,此时降低整体置信度,并触发人工复核标记。
# model_fusion.py from xgboost import XGBClassifier from lightgbm import LGBMClassifier from sklearn.calibration import CalibratedClassifierCV class DualModelFusion: def __init__(self): self.xgb = XGBClassifier( n_estimators=300, max_depth=6, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, random_state=42, # 关键:开启feature_importances_支持 importance_type='gain' ) self.lgb = LGBMClassifier( n_estimators=300, max_depth=6, learning_rate=0.05, subsample=0.85, colsample_bytree=0.8, random_state=42, # 关键:启用early_stopping防止过拟合 early_stopping_rounds=30 ) def fit(self, X_train, y_train, X_val, y_val): # 分别训练,LGB加早停 self.xgb.fit(X_train, y_train) self.lgb.fit( X_train, y_train, eval_set=[(X_val, y_val)], verbose=False ) def predict_proba(self, X): p_xgb = self.xgb.predict_proba(X)[:, 1] p_lgb = self.lgb.predict_proba(X)[:, 1] # 动态加权:共识度越高,权重越接近0.5 consensus = 1.0 - np.abs(p_xgb - p_lgb) # [0,1] weight_xgb = 0.5 + (consensus - 0.5) * 0.3 # 权重范围[0.35, 0.65] weight_lgb = 1.0 - weight_xgb return weight_xgb * p_xgb + weight_lgb * p_lgb def get_feature_importance(self, feature_names): # 合并两模型重要性,取均值(非简单相加) imp_xgb = self.xgb.feature_importances_ imp_lgb = self.lgb.feature_importances_ avg_imp = (imp_xgb + imp_lgb) / 2 return pd.Series(avg_imp, index=feature_names).sort_values(ascending=False)提示:
weight_xgb = 0.5 + (consensus - 0.5) * 0.3这个系数0.3是调参得到的平衡点——太大则过度依赖共识,丧失模型多样性;太小则融合失效。你在复现时可微调,但必须记录调参过程,答辩时老师问“为什么是0.3”,你就拿出验证集上AUC随该参数变化的曲线图。
2.4 可解释性结果输出模块:每个预测背后都有TOP3归因路径
风控模型不能只输出“0.72”,必须回答“为什么是0.72”。本方案为每个预测结果附加三项可解释性输出:
- TOP3贡献特征及方向:如
max_overdue_6m (+0.21), repay_rate_3m (-0.15), device_change_freq (+0.09); - 局部可解释性(SHAP)摘要图:对验证集抽样生成
shap.summary_plot; - 规则回溯表:将高风险用户映射到业务规则库,如“max_overdue_6m > 30 & repay_rate_3m < 0.6 → 触发人工审核”。
# explainability.py import shap class PredictionExplainer: def __init__(self, model_fusion: DualModelFusion, feature_names: list): self.model = model_fusion self.feature_names = feature_names # 使用KernelExplainer(适配任意模型) self.explainer = shap.KernelExplainer( lambda x: model_fusion.predict_proba(x), shap.sample(X_train, 50) # 采样50个样本作为背景 ) def explain_single(self, x_instance: np.ndarray) -> dict: """解释单个样本,返回TOP3特征贡献""" shap_values = self.explainer.shap_values(x_instance.reshape(1, -1))[0] # 按绝对值排序,取TOP3 top3_idx = np.argsort(np.abs(shap_values))[-3:][::-1] top3_feats = [ (self.feature_names[i], round(shap_values[i], 3)) for i in top3_idx ] return { "prediction": round(self.model.predict_proba(x_instance.reshape(1, -1))[0], 3), "top3_contributions": top3_feats, "shap_values": shap_values.tolist() } def generate_report(self, X_test, y_test, save_path: str): """生成批量解释报告,含规则映射""" results = [] for i, x in enumerate(X_test): exp = self.explain_single(x) # 规则映射逻辑(简化版) rule_hit = [] if exp["top3_contributions"][0][0] == "max_overdue_6m" and exp["top3_contributions"][0][1] > 0.15: rule_hit.append("逾期惯性规则") if exp["top3_contributions"][1][0] == "repay_rate_3m" and exp["top3_contributions"][1][1] < -0.1: rule_hit.append("还款意愿规则") results.append({ "user_id": i, "pred_score": exp["prediction"], "top3": exp["top3_contributions"], "rules_triggered": rule_hit }) pd.DataFrame(results).to_csv(f"{save_path}/explanation_report.csv", index=False)注意:
shap.KernelExplainer虽慢但通用,适合答辩演示;生产环境可换TreeExplainer加速。关键在于——解释不是附加功能,而是输出必选项。你交毕设时,这份explanation_report.csv就是证明你“懂业务”的铁证。
3. 关键实现细节:从数据清洗到模型部署的六处硬核落点
这一章不讲原理,只列六个你实际编码时必须亲手敲、必须调参数、必须看日志的硬核落点。它们分散在全流程中,但每处都决定方案能否真正跑通、能否经得起答辩拷问。
3.1 时间窗口切割:用pd.Grouper替代手动groupby,避免周期错位
违约预测的核心是“站在T时刻,看T之前的数据”。常见错误是用df[df['date'] < '2023-06-01']切片,但若数据中存在同一天多条记录,或时间精度为秒级,极易漏掉临界样本。本方案统一使用pd.Grouper按业务周期(月/季)聚合,并设置closed='left'确保窗口左闭右开:
# 正确做法:按月聚合,且保证“6月预测”只用5月及之前数据 bill_df['bill_date'] = pd.to_datetime(bill_df['bill_date']) # 构造滚动窗口:对每个用户,取截至当前月前1个月的所有账单 bill_df['ref_month'] = bill_df['bill_date'].dt.to_period('M') - 1 # 按用户+参考月分组,避免跨月混算 windowed = bill_df.groupby(['user_id', 'ref_month']).agg({ 'overdue_days': ['max', 'count'], 'bill_amount': 'sum' }).reset_index()玄学提示:
ref_month = bill_date.dt.to_period('M') - 1这步看似简单,却是某次线上翻车的根源——原方案用bill_date.dt.month手动减1,遇到1月变0,直接崩。用Period类型天然支持跨年运算,这才是工业级写法。
3.2 类别型变量编码:Target Encoding + 平滑,拒绝LabelEncoder硬伤
用户职业、教育程度等类别特征若用LabelEncoder,会引入虚假序关系(如“博士=3,高中=1”,模型误以为博士>高中>初中)。本方案采用Target Encoding with Smoothing,公式为:
$$ \text{smoothed_target} = \frac{\text{sum}(y) + \alpha \cdot \text{global_mean}}{\text{count} + \alpha} $$
其中α=10是经验值,平衡稀疏类与高频类。
# category_encoder.py def target_encode_smooth(df: pd.DataFrame, col: str, target: str, alpha: int = 10) -> pd.Series: global_mean = df[target].mean() agg = df.groupby(col)[target].agg(['sum', 'count']) smooth = (agg['sum'] + alpha * global_mean) / (agg['count'] + alpha) # 映射回原df,未出现的类别填global_mean return df[col].map(smooth).fillna(global_mean) # 使用示例 train_df['edu_encoded'] = target_encode_smooth(train_df, 'education', 'is_overdue', alpha=10) test_df['edu_encoded'] = test_df['education'].map( train_df.groupby('education')['is_overdue'].apply( lambda x: (x.sum() + 10 * train_df['is_overdue'].mean()) / (len(x) + 10) ) ).fillna(train_df['is_overdue'].mean())血泪经验:必须在训练集上计算平滑值,再映射到测试集!若在测试集上单独计算,会导致数据泄露。答辩时被问“怎么防泄露”,就把这行
test_df['...'].map(train_df.groupby...)指出来。
3.3 缺失值填充:业务规则优先于统计值,用fillna不如用mask
金融数据缺失往往有业务含义:如“无还款记录”不等于“0次逾期”,而是“未发生还款行为”。本方案对关键字段定义业务填充规则:
| 字段 | 缺失含义 | 填充策略 | 代码示意 |
|---|---|---|---|
repay_amount | 未还款 | 填-1(明确区别于0) | df['repay_amount'].mask(df['repay_amount'].isna(), -1) |
overdue_days | 未逾期 | 填0(符合定义) | df['overdue_days'].fillna(0) |
device_id | 设备未知 | 填'UNKNOWN'(字符串占位) | df['device_id'].fillna('UNKNOWN') |
# fill_missing.py def fill_missing_by_business(df: pd.DataFrame) -> pd.DataFrame: df = df.copy() # 用mask而非fillna,保留原始NaN用于后续逻辑判断 df['repay_amount'] = df['repay_amount'].mask(df['repay_amount'].isna(), -1) df['overdue_days'] = df['overdue_days'].fillna(0) df['device_id'] = df['device_id'].fillna('UNKNOWN') # 关键:对衍生特征,缺失即无效,直接drop df = df.dropna(subset=['max_overdue_6m', 'repay_rate_3m']) return df注意:
mask和fillna行为不同。mask(cond, val)是“满足条件才替换”,fillna是“所有NaN都替换”。此处用mask是为后续可能加的if df['repay_amount'] == -1:逻辑留接口。
3.4 模型验证策略:TimeSeriesSplit + 业务周期对齐,拒绝随机切分
违约预测是典型时序问题,随机train_test_split会导致未来信息泄露。本方案采用TimeSeriesSplit,但强制按业务月对齐:验证集必须是连续3个月,且训练集截止月必须早于验证集起始月至少1个月(留出决策缓冲期)。
# time_series_validator.py from sklearn.model_selection import TimeSeriesSplit class BusinessAlignedTSSplit: def __init__(self, n_splits=3, min_train_months=6): self.n_splits = n_splits self.min_train_months = min_train_months def split(self, X, y, groups=None): # 假设X有'date'列,已转为datetime dates = X['date'].sort_values().unique() # 按月分组,取月首日作为时间点 months = pd.to_datetime(pd.Series(dates)).dt.to_period('M').unique() # 确保训练期足够长 start_idx = self.min_train_months for i in range(start_idx, len(months) - 2): train_mask = X['date'].dt.to_period('M').isin(months[:i]) val_mask = X['date'].dt.to_period('M').isin(months[i:i+3]) yield ( np.where(train_mask)[0], np.where(val_mask)[0] ) # 使用 tscv = BusinessAlignedTSSplit(n_splits=3) for train_idx, val_idx in tscv.split(X_train_full, y_train_full): X_tr, y_tr = X_train_full.iloc[train_idx], y_train_full.iloc[train_idx] X_val, y_val = X_train_full.iloc[val_idx], y_train_full.iloc[val_idx] model.fit(X_tr, y_tr, X_val, y_val) # 传入验证集用于早停提示:
months[i:i+3]固定取3个月验证,是因为业务方要求“模型需稳定覆盖一个季度”。答辩时说“我们按业务节奏切分”,比说“用了TimeSeriesSplit”有力得多。
3.5 特征重要性可视化:用plot_importance不如用to_dataframe导出可编辑表格
XGBoost自带plot_importance很方便,但答辩PPT里需要调整字体、颜色、排序,图片格式极不友好。本方案强制导出为DataFrame,再用Excel美化:
# feature_importance_export.py def export_feature_importance(model_fusion: DualModelFusion, feature_names: list, save_path: str): imp_series = model_fusion.get_feature_importance(feature_names) imp_df = imp_series.reset_index(name='importance') imp_df.columns = ['feature_name', 'importance_score'] # 添加业务分类标签(便于PPT分组) imp_df['category'] = imp_df['feature_name'].apply(lambda x: '逾期行为' if 'overdue' in x else '还款能力' if 'repay' in x or 'amount' in x else '稳定性' if 'device' in x or 'std' in x else '其他' ) imp_df.to_excel(f"{save_path}/feature_importance.xlsx", index=False) return imp_df # 导出后,你可在Excel里: # - 按category着色 # - 插入条形图(选中两列→插入→条形图) # - 手动标注TOP5特征业务含义(如“max_overdue_6m:近6期最大逾期天数,反映违约惯性”)玄学:答辩PPT里放一张手改过的Excel图表,比10张自动生成的图更有说服力。因为老师知道——你亲手改过,就代表你真懂。
3.6 模型部署接口:Flask轻量API + 输入校验,拒绝裸模型上线
毕设/课设常止步于.pkl模型文件,但真实场景需要API。本方案提供最小可行API,含输入校验、日志记录、错误捕获:
# app.py from flask import Flask, request, jsonify import joblib import pandas as pd app = Flask(__name__) model = joblib.load('models/final_fusion_model.pkl') feature_names = joblib.load('models/feature_names.pkl') @app.route('/predict', methods=['POST']) def predict(): try: data = request.get_json() # 强校验:必须包含所有特征 missing = set(feature_names) - set(data.keys()) if missing: return jsonify({"error": f"Missing features: {list(missing)}"}), 400 # 构造DataFrame(保持列序) X = pd.DataFrame([data])[feature_names] # 预测 pred_prob = model.predict_proba(X.values)[0, 1] # 解释 explainer = joblib.load('models/explainer.pkl') explanation = explainer.explain_single(X.values[0]) return jsonify({ "risk_score": round(pred_prob, 4), "is_high_risk": pred_prob > 0.5, "top3_explanation": explanation["top3_contributions"] }) except Exception as e: app.logger.error(f"Prediction error: {str(e)}") return jsonify({"error": "Internal server error"}), 500 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False) # 生产关debug注意:
debug=False是硬性要求。某次课设演示时忘了关,老师curl请求直接返回traceback,当场扣分。从那以后我每次部署前,都强制走一遍ps aux | grep flask确认进程参数。
4. 避坑指南:违约预测项目中最容易翻车的五个点
再好的方案,落地时也常因几个细节翻车。以下是本方案在多次实测、答辩、模拟部署中踩过的坑,按“现象→原因→解决”整理,每一条都对应真实血泪教训。
4.1 现象:验证集AUC 0.85,但测试集AUC骤降至0.62
原因:特征构造时用了df.sort_values('date').groupby('user_id').apply(lambda x: x.iloc[-6:])取近6期,但未重置索引,导致iloc[-6:]实际取的是全局索引末尾6行,而非每个用户的最后6条。
解决:改用df.groupby('user_id').apply(lambda x: x.nlargest(6, 'date')),或先sort_values再groupby时加sort=False参数避免二次排序干扰索引。
4.2 现象:模型训练时内存爆满(OOM),16G RAM直接卡死
原因:pd.get_dummies()对高基数类别特征(如device_id有50万种)生成稀疏矩阵,内存占用爆炸。
解决:禁用get_dummies,改用target_encode_smooth或HashingVectorizer(n_features=2**12),或直接删除低频类别(value_counts() < 10的归为OTHER)。
4.3 现象:SHAP解释结果中,某个特征贡献值恒为0
原因:该特征在训练集中标准差为0(全相同),模型学不到任何区分能力,SHAP自然归零。
解决:在特征工程后加检查X_train.std().loc[X_train.std() == 0],打印出恒定特征名;若业务允许,直接删除;若必须保留,用np.random.normal(0, 1e-6, len(X))加微小噪声(仅训练用)。
4.4 现象:Flask API返回500,日志显示KeyError: 'user_id'
原因:前端传参用{"user_id": 123, "features": {...}},但代码里直接data['user_id'],未做键存在性检查。
解决:所有字典访问必须用data.get('key', default),或提前校验if 'key' not in data: return jsonify({'error': 'missing key'}), 400。
4.5 现象:答辩时老师问“这个特征重要性0.02,业务上怎么解释?”,当场语塞
原因:只导出了数字重要性,没准备业务注释表。
解决:在feature_importance.xlsx中增加第三列business_interpretation,每行手写一句,如:repay_rate_3m→ “近3期最低还款额完成率,低于60%表明还款意愿弱,是风控核心指标”。
提示:这五条避坑点,建议你复制进自己的README.md,作为“已知问题与解决方案”章节。答辩时老师若问“遇到过什么困难”,你就说:“最大的困难是第4条API键校验,我们花了两天定位,现在所有接口都强制
get()访问,还写了单元测试覆盖缺失键场景。”
5. 答辩与验收:用三份文档+一次演示,让老师当场点头
答辩不是讲技术,而是证明你理解问题、控制过程、交付价值。本方案的验收不靠“模型多准”,而靠三份文档+一次10分钟演示。下面告诉你每份文档怎么写、演示怎么演、老师最可能问什么、你怎么答。
5.1 文档一:《业务需求对齐说明书》——证明你懂风控,不是调参侠
这不是技术文档,而是给业务方(答辩老师)看的“翻译件”。用表格形式,左列写原始业务需求,右列写你的技术实现,中间加一列“验证方式”。例如:
| 业务需求(来自赛题描述) | 你的技术实现 | 如何验证已满足 |
|---|---|---|
| “需识别潜在违约用户,支持人工复核” | 输出TOP3归因特征 + 触发规则标签(如“逾期惯性规则”) | 查看explanation_report.csv中高风险用户,确认TOP3含业务敏感字段,且规则列非空 |
| “模型需稳定,避免单月波动过大” | 采用TimeSeriesSplit按季度验证,且要求连续3个月AUC波动<0.02 | 提供validation_history.xlsx,含每月AUC、KS、PSI值,标出波动超限月份并说明原因 |
| “特征需可解释,避免黑箱” | 所有特征命名含业务前缀(overdue_,repay_,device_),重要性表附业务注释 | 展示feature_importance.xlsx第三列business_interpretation,现场随机挑3个特征口述含义 |
提示:这份文档必须打印,答辩时放在老师面前。当老师说“你这模型我看不懂”,你就翻开这页,手指着表格说:“您看,赛题要求‘支持人工复核’,我们实现为TOP3归因,验证方式是查这张表——我现在就打开给您看。”
5.2 文档二:《可复现性操作手册》——证明你能交付,不是Demo玩家
这是给助教/后续维护者看的。不写“安装Python”,而写精确到版本、命令、路径的操作流。模板如下:
## 环境准备(Ubuntu 22.04, Python 3.9.16) 1. 创建虚拟环境:`python3.9 -m venv venv && source venv/bin/activate` 2. 安装依赖:`pip install -r requirements.txt`(已锁定版本:pandas==1.5.3, xgboost==1.7.5, ...) 3. 数据准备:将 `data/raw/` 下4个CSV文件放入指定路径,**注意文件名必须全小写,无空格** ## 一键运行(全程无需修改代码) 1. 数据清洗:`python src/data_loader.py --input_dir data/raw --output_dir data/cleaned` 2. 特征工程:`python src/feature_engineer.py --input_dir data/cleaned --output_dir data/features` 3. 模型训练:`python src/train.py --feature_dir data/features --model_dir models` 4. 生成报告:`python src/explainability.py --model_dir models --output_dir reports` ## 验证成功标志 - `data/cleaned/user_info_cleaned.csv` 行数 = 原始文件行数(无丢数) - `models/final_fusion_model.pkl` 文件大小 > 5MB(模型已保存) - `reports/explanation_report.csv` 中 `pred_score` 列值域为 [0.0, 1.0]注意:
requirements.txt必须用pip freeze > requirements.txt生成,且删掉pkg-resources==0.0.0等无关项。某次毕设因torch版本冲突,助教配环境失败,直接延毕。从那以后我每次提交,都先在干净Docker里docker run -it python:3.9 bash重装一遍验证。
5.3 文档三:《答辩问答预演清单》——把老师可能问的12个问题,答案写进PPT备注
不要等老师问,主动在PPT备注页写好答案。以下是高频问题及应答要点(非背诵,是思路):
| 问题 | 应答要点(PPT备注页写) | 为什么有效 |
|---|---|---|
| “为什么不用深度学习?” | “深度学习需更大样本,本赛题数据量仅10万,XGBoost在小数据上更稳;且DL难解释,无法满足‘TOP3归因’业务要求。” | 把缺点转化为业务适配,不贬低技术 |
| “特征重要性最高的‘max_overdue_6m’,如果用户是新客怎么办?” | “新客该特征为空,我们按业务规则 |
本文还有配套的精品资源,点击获取