
简介这份资源围绕京东借贷需求预测这一真实金融业务场景系统整理了机器学习从数据预处理到模型上线的完整实践链路适合具备一定Python与机器学习基础、希望积累金融风控与需求预测项目经验的学习者。压缩包共8个文件以5个py脚本为主辅以1个md说明文档、1个numbers特征设计表和1个xlsx特征清单整体约575KB体积轻量却覆盖了特征工程、模型训练与集成等关键环节。内容涉及数据清洗与标准化、特征选择、LightGBM与XGBoost等模型训练、stacking集成以及模型评估调优并配有特征设计说明便于读者理解每一步的输入输出与设计意图。已有223人学习下载可作为金融借贷需求预测方向的实战参考帮助读者掌握从原始数据到预测结果的完整建模思路与脚本组织方式。1. 借贷需求预测这套源码到底能跑出什么结果金融风控圈里有个共识借贷需求预测比违约预测更难做。违约预测有明确的标签需求预测的标签往往是连续值而且受营销节奏、节假日、用户生命周期影响极大。JDD_Loan_Forecasting-master 这个包就是冲着这个场景来的——用机器学习把用户未来的借贷需求量化出来帮资金端提前做头寸安排。包里的文件结构很直白lightGBM_JDD.py是主模型genfeature.py负责特征生成stacking.py做模型融合lr_xgb_ensamble.py是逻辑回归加 XGBoost 的集成方案useless.py看名字就知道是废弃实验的回收站。配套的features_list.xlsx和特征设计.numbers是特征字典README.md交代了运行顺序。这套东西适合谁如果你手头有用户行为宽表想快速搭一个需求预测的 baseline或者想看看工业界怎么把 LightGBM 和 Stacking 串起来这个包能省你不少搭框架的时间。但别指望开箱即用——数据格式、特征列名、标签定义都得按你自己的业务改。2. 特征工程流水线从原始行为到模型输入2.1 特征设计的底层逻辑借贷需求预测的特征体系核心就三类历史借贷行为、用户活跃度、外部触发信号。features_list.xlsx里列的特征大概率围绕这几个维度展开。历史借贷行为包括过去 N 个月的借款次数、平均借款金额、还款准时率、当前待还笔数用户活跃度看登录频次、浏览理财页面的时长、点击借款入口的次数外部触发信号可能是营销活动曝光、额度调整通知、利率变动提醒。为什么这么设计因为借贷需求本质上是「意愿 × 能力 × 触发」的乘积。意愿靠活跃度捕捉能力靠历史还款表现衡量触发靠营销事件标记。genfeature.py干的就是把原始日志按用户 ID 聚合再按时间窗口切片生成这些衍生指标。2.2 特征生成脚本的实操拆解genfeature.py通常是一个批处理脚本输入是用户行为明细表输出是宽表。我一般会按下面的结构改import pandas as pd import numpy as np from datetime import timedelta # 读取原始行为日志假设字段user_id, event_time, event_type, amount raw pd.read_csv(user_behavior.csv, parse_dates[event_time]) # 定义观察窗口和预测窗口 OBS_DAYS 90 # 用过去90天行为做特征 PRED_DAYS 30 # 预测未来30天是否有借贷需求 # 按用户聚合历史借贷行为 loan_events raw[raw[event_type] loan_apply].copy() loan_agg loan_events.groupby(user_id).agg( loan_cnt_90d(event_time, count), loan_amt_avg(amount, mean), loan_amt_sum(amount, sum), last_loan_days(event_time, lambda x: (pd.Timestamp.now() - x.max()).days) ).reset_index() # 活跃度特征登录、浏览、点击 active_events raw[raw[event_type].isin([login, view_loan_page, click_loan_btn])] active_pivot active_events.pivot_table( indexuser_id, columnsevent_type, valuesevent_time, aggfunccount ).fillna(0).reset_index() active_pivot.columns [user_id, login_cnt, view_cnt, click_cnt] # 合并特征宽表 features loan_agg.merge(active_pivot, onuser_id, howleft).fillna(0) # 生成标签未来30天内是否有借款申请 future_loan loan_events[loan_events[event_time] pd.Timestamp.now() timedelta(days0)] future_loan[label] 1 label_df future_loan[[user_id, label]].drop_duplicates() features features.merge(label_df, onuser_id, howleft) features[label] features[label].fillna(0) features.to_csv(feature_wide_table.csv, indexFalse)这段代码的逻辑说明先按事件类型拆出借贷行为和活跃行为分别聚合后再 merge。OBS_DAYS和PRED_DAYS是两个关键参数——观察窗口太短特征噪声大太长用户行为漂移严重。我一般会跑 30/60/90 三组对比看验证集 AUC 选最优。last_loan_days这个特征很关键它捕捉了用户的「借贷冷却期」很多用户还完款后一段时间内不会再借。参数怎么改如果你的数据量小把loan_cnt_90d换成loan_cnt_30d避免稀疏。如果用户行为事件类型多active_pivot那步可以加更多列但注意维度爆炸。2.3 特征筛选与特征字典的用法features_list.xlsx不是摆设。打开它你会看到特征名、含义、计算逻辑、预期方向正相关/负相关。我一般会拿它做两件事一是核对genfeature.py生成的列是否齐全二是做初步的特征筛选——把预期方向与实际 IC 值符号相反的特征标出来大概率是计算逻辑写反了或者数据泄漏。常见做法是用 LightGBM 的feature_importance做一轮粗筛再结合 IV 值信息价值做细筛。useless.py里可能就有这类实验代码虽然名字叫 useless但里面的特征淘汰逻辑值得翻一翻。3. LightGBM 主模型训练参数、调参与验证策略3.1 为什么选 LightGBM 而不是 XGBoostlightGBM_JDD.py是主模型脚本选 LightGBM 而不是 XGBoost核心原因是直方图算法 Leaf-wise 生长策略。借贷需求预测的特征宽表通常有几百列样本量在百万级LightGBM 的训练速度比 XGBoost 快 3 到 5 倍内存占用也低。而且 Leaf-wise 策略在拟合非线性关系时更容易收敛到低 loss。但 Leaf-wise 有个坑容易过拟合。所以num_leaves不能设太大一般控制在2^max_depth以内。我通常从num_leaves31起步配合min_data_in_leaf50做正则。3.2 训练脚本的核心参数与代码import lightgbm as lgb from sklearn.model_selection import StratifiedKFold from sklearn.metrics import roc_auc_score import pandas as pd import numpy as np # 读取特征宽表 df pd.read_csv(feature_wide_table.csv) X df.drop([user_id, label], axis1) y df[label] # 定义参数二分类任务 params { objective: binary, metric: auc, boosting_type: gbdt, num_leaves: 31, max_depth: 6, learning_rate: 0.05, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 5, min_data_in_leaf: 50, lambda_l2: 1.0, verbose: -1, seed: 42 } # 5折交叉验证 skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) auc_scores [] best_iters [] for fold, (train_idx, val_idx) in enumerate(skf.split(X, y)): X_train, X_val X.iloc[train_idx], X.iloc[val_idx] y_train, y_val y.iloc[train_idx], y.iloc[val_idx] dtrain lgb.Dataset(X_train, labely_train) dval lgb.Dataset(X_val, labely_val, referencedtrain) model lgb.train( params, dtrain, num_boost_round2000, valid_sets[dval], callbacks[lgb.early_stopping(stopping_rounds100), lgb.log_evaluation(period200)] ) y_pred model.predict(X_val, num_iterationmodel.best_iteration) auc roc_auc_score(y_val, y_pred) auc_scores.append(auc) best_iters.append(model.best_iteration) print(fFold {fold1} AUC: {auc:.4f}, Best Iter: {model.best_iteration}) print(fMean AUC: {np.mean(auc_scores):.4f} /- {np.std(auc_scores):.4f})逻辑说明StratifiedKFold保证每折的正负样本比例一致这对借贷需求预测很重要——正样本通常只占 5% 到 15%。early_stopping防止过拟合stopping_rounds100意味着验证集 AUC 连续 100 轮不提升就停。best_iteration是最终模型该用的树数量。参数怎么调learning_rate和num_leaves是联动关系。学习率降到 0.02num_leaves可以提到 63但训练时间翻倍。我一般先用 0.05 跑一轮看 baseline再决定要不要细调。feature_fraction和bagging_fraction低于 0.7 时模型方差会明显增大除非特征维度极高超过 1000 列否则不建议压太低。3.3 验证策略的坑时间泄漏与样本权重借贷需求预测最容易翻车的地方是时间泄漏。如果你用随机 K 折未来信息会通过特征聚合窗口漏进训练集。正确做法是按时间切分用前 6 个月训练后 1 个月验证。lightGBM_JDD.py如果用的是随机 K 折你得改成TimeSeriesSplit。另一个坑是样本权重。借贷需求有强烈的季节性节假日前后需求激增。如果不加权重模型会偏向平稳期样本。常见做法是按月份给样本加权需求高峰月的权重设为 1.5 到 2.0。4. Stacking 融合与 LRXGB 集成怎么叠才不翻车4.1 Stacking 的两层结构设计stacking.py实现的是典型的两层 Stacking第一层用多个基模型LightGBM、XGBoost、逻辑回归分别预测第二层用一个小模型通常是逻辑回归或浅层树模型把第一层的输出作为特征再预测。为什么这么叠单一模型容易在某个特征子空间上过拟合Stacking 通过多模型投票降低方差。但 Stacking 的坑在于第一层模型的交叉验证预测必须严格用 out-of-fold 方式生成否则第二层会过拟合。from sklearn.model_selection import StratifiedKFold from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier import xgboost as xgb import lightgbm as lgb import numpy as np # 第一层基模型 base_models { lgb: lgb.LGBMClassifier(n_estimators500, num_leaves31, learning_rate0.05), xgb: xgb.XGBClassifier(n_estimators500, max_depth6, learning_rate0.05), lr: LogisticRegression(C1.0, max_iter1000) } # 生成 out-of-fold 预测 skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) oof_preds np.zeros((X.shape[0], len(base_models))) test_preds np.zeros((X.shape[0], len(base_models))) for i, (name, model) in enumerate(base_models.items()): for fold, (train_idx, val_idx) in enumerate(skf.split(X, y)): X_train, X_val X.iloc[train_idx], X.iloc[val_idx] y_train, y_val y.iloc[train_idx], y.iloc[val_idx] model_clone model.__class__(**model.get_params()) model_clone.fit(X_train, y_train) oof_preds[val_idx, i] model_clone.predict_proba(X_val)[:, 1] print(f{name} OOF AUC: {roc_auc_score(y, oof_preds[:, i]):.4f}) # 第二层逻辑回归融合 meta_model LogisticRegression(C0.5, max_iter1000) meta_model.fit(oof_preds, y) final_pred meta_model.predict_proba(oof_preds)[:, 1] print(fStacking AUC: {roc_auc_score(y, final_pred):.4f})逻辑说明oof_preds存的是每个基模型在 out-of-fold 上的预测概率第二层用这些概率做输入。C0.5是第二层逻辑回归的正则强度比默认的 1.0 更保守防止第二层过拟合。如果第一层模型 AUC 差异很大比如 LR 只有 0.65LGB 有 0.78第二层可能会给 LR 分配负权重这时候要么去掉弱模型要么加非负约束。4.2 LRXGB 集成的适用场景lr_xgb_ensamble.py走的是另一条路逻辑回归捕捉线性关系XGBoost 捕捉非线性关系两者加权平均。这种方案比 Stacking 简单可解释性也更好。权重怎么定常见做法是按验证集 AUC 做网格搜索或者直接用scipy.optimize.minimize优化权重。我一般会先跑 LR 和 XGB 单独看 AUC如果差距在 0.03 以内加权平均能提升 0.005 到 0.01如果差距超过 0.05加权平均基本没用不如直接上 Stacking。4.3 融合模型的线上部署注意点融合模型上线时最大的坑是特征一致性。第一层模型用的特征必须和第二层训练时完全一致包括缺失值填充方式、归一化参数。我见过有人离线用 StandardScaler 归一化线上忘了同步结果 LR 的预测直接崩掉。建议把预处理逻辑封装成sklearn.Pipeline离线训练和线上推理共用同一套代码。5. 避坑与排查那些让我重跑过模型的坑5.1 特征列顺序不一致导致预测结果全错现象离线验证 AUC 0.78线上部署后预测值全部集中在 0.5 附近。原因训练时特征列顺序是 A、B、C线上推理时数据框列顺序变成了 C、A、B。LightGBM 按列索引读取特征顺序错了等于特征全乱。解决在genfeature.py输出时固定列顺序保存一个feature_order.json线上推理前按这个顺序重排。5.2 标签定义窗口与特征窗口重叠现象验证集 AUC 高得离谱0.95上线后效果暴跌。原因特征计算窗口是过去 90 天标签定义是「未来 30 天是否有借款」但代码里pd.Timestamp.now()取的是当前时间导致部分未来信息漏进了特征。解决所有时间窗口必须基于一个固定的「观察点日期」特征只用观察点之前的数据标签只用观察点之后的数据。这个日期要作为参数传入不能硬编码。5.3 LightGBM 的min_data_in_leaf设太小导致过拟合现象训练集 AUC 0.92验证集 AUC 0.71差距超过 0.2。原因min_data_in_leaf默认是 20对于百万级样本来说太小叶子节点里只有几十个样本模型把噪声也学进去了。解决把min_data_in_leaf提到 100 到 200同时配合lambda_l2加到 5.0 到 10.0。如果还过拟合降低num_leaves到 15。5.4 Stacking 第二层用了原始特征现象Stacking 的 AUC 比单模型还低。原因第二层训练时不小心把原始特征也拼进去了导致第二层模型直接记住了原始特征与标签的关系第一层输出被忽略。解决第二层的输入只能是第一层的 out-of-fold 预测绝对不能混入原始特征。代码里用oof_preds单独建 DataFrame不要和X合并。5.5 样本权重与交叉验证的冲突现象加了时间衰减权重后交叉验证的 AUC 波动极大±0.05。原因StratifiedKFold按标签分层但权重是按时间衰减的导致某些折的权重分布极不均匀。解决改用TimeSeriesSplit或者手动按时间分桶后再做分层。权重归一化到均值为 1避免某些折的总权重过大。6. 从离线到线上模型监控与迭代的一个实用技巧模型上线不是终点。借贷需求预测的模型衰减速度比违约预测快得多——用户行为模式可能两三个月就变一轮。我一般会做两件事PSI 监控和滚动重训。PSIPopulation Stability Index用来监控特征分布漂移。计算方式很简单把训练集的特征分箱统计每个箱的样本占比再对线上最近 7 天的数据做同样分箱算 PSI 值。PSI 超过 0.2 就触发告警超过 0.3 就得重训。import numpy as np import pandas as pd def calculate_psi(expected, actual, bins10): 计算单个特征的 PSI # 按训练集分位数分箱 breakpoints np.percentile(expected, np.linspace(0, 100, bins 1)) breakpoints[0] -np.inf breakpoints[-1] np.inf expected_counts np.histogram(expected, binsbreakpoints)[0] / len(expected) actual_counts np.histogram(actual, binsbreakpoints)[0] / len(actual) # 避免除零 expected_counts np.where(expected_counts 0, 0.0001, expected_counts) actual_counts np.where(actual_counts 0, 0.0001, actual_counts) psi np.sum((actual_counts - expected_counts) * np.log(actual_counts / expected_counts)) return psi # 对每个特征计算 PSI train_df pd.read_csv(feature_wide_table.csv) online_df pd.read_csv(online_feature_last7d.csv) psi_results {} for col in train_df.columns: if col in [user_id, label]: continue psi_results[col] calculate_psi(train_df[col].values, online_df[col].values) # 输出 PSI 最高的 10 个特征 psi_series pd.Series(psi_results).sort_values(ascendingFalse) print(psi_series.head(10))这段代码的逻辑breakpoints用训练集的分位数确定保证每个箱的样本量大致均衡。0.0001是平滑项防止 log(0)。PSI 排名前 10 的特征就是漂移最严重的优先检查这些特征的计算逻辑是否还适用。滚动重训的策略每周用最近 6 个月的数据重训一次保留旧模型做 A/B 对比。如果新模型在验证集上 AUC 提升超过 0.005就切流否则继续观察。我一般会把lightGBM_JDD.py和stacking.py封装成 Airflow DAG每天跑特征生成每周跑训练每月跑一次全量 Stacking。从那以后我每次上线新模型前都强制走一遍 PSI 检查——哪怕离线指标再好看特征漂移没控住线上就是玄学。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。