资讯详情

资讯详情

Bagging+深度学习:财务造假预测中的不平衡数据实战

简介这份资源面向金融风控、数据挖掘方向的开发者与高年级学生提供一套完整的上市公司财务数据造假预测方案涵盖从特征工程到深度学习建模的全流程。包内共33个文件以15个csv数据集、9个Python脚本为主辅以xlsx原始附件、png结果图、pdf说明文档与md运行指南压缩包约35.46MB结构清晰便于按模块复现。方案先用SMOTE过采样配合决策树、随机森林、ExtraTree、XGBoost四种树模型计算特征重要性均值筛选制造业与其他行业前30个关键因子再以多层感知机、残差网络与Cross网络构建DCRN模型通过短路连接、外积交互与Batch Normalize层缓解梯度问题并舍弃过采样、改用Bagging集成降低方差。已有191人学习读者可获取完整源码、数据集、训练模型与运行说明直接用于复现实验、对比树模型与深度模型效果或迁移到其他二分类风控场景。1. 财务造假预测这件事为什么单模型总是差一口气上市公司财务造假预测本质是一个极度不平衡的二分类问题几千家样本里真正被处罚或公开认定造假的可能只有几十到一两百家正负比动辄 1:50 甚至更极端。很多开发者第一次拿到这类数据集用逻辑回归或者单棵决策树跑一遍AUC 看着还行一到召回率就崩了——真正有问题的公司几乎全被漏掉。这不是模型不行而是单模型在这种高噪声、小正样本、特征维度又高的表格数据上方差压不住。Bagging 加深度学习这套组合解决的正是这个痛点。Bagging 通过有放回抽样训练多个基学习器再投票把方差降下来深度学习负责在财务比率、现金流结构、关联交易占比这些非线性特征里挖出人工规则写不出来的交互关系。两者叠在一起既能扛住样本不平衡带来的抖动又比纯树模型多一层表征能力。这套方案适合谁适合手里有财报数据、想快速搭一个可复现基线、又不想从零调参的算法工程师和金融风控方向的学生。下面我把从数据到模型到跑通的完整路径拆开讲参数和坑都标清楚。2. 数据从哪来、特征怎么选财务造假预测的数据地基2.1 数据集结构与标签定义这类项目的数据集通常由三块拼成财务报表主表、财务比率衍生表、标签表。主表包含资产总计、负债合计、营业收入、净利润、经营活动现金流净额等原始科目衍生表是在主表基础上算出来的比率比如资产负债率、毛利率、应收账款周转率、现金流与净利润比值。标签表一般是一列股票代码加一列是否造假造假样本来自监管处罚记录正常样本是同期未被处罚的公司。拿到数据后第一件事不是急着建模而是确认标签的时间对齐。财务造假有个特点处罚公告往往滞后于造假行为两到三年。如果你用 2023 年的处罚标签去匹配 2023 年的财报模型学到的是“被查出来的公司长什么样”而不是“正在造假的公司长什么样”。常见做法是把标签年份往前推两年用 T-2 年的财报预测 T 年的造假状态。这一步不做后面 AUC 再高都是自欺欺人。import pandas as pd import numpy as np # 读取三张表假设文件名为 fin_main.csv / fin_ratio.csv / label.csv main pd.read_csv(fin_main.csv) ratio pd.read_csv(fin_ratio.csv) label pd.read_csv(label.csv) # 标签年份前推两年避免用处罚年份直接对齐财报 label[report_year] label[punish_year] - 2 # 按股票代码和报告年份合并 df main.merge(ratio, on[stock_code, report_year], howinner) df df.merge(label[[stock_code, report_year, is_fraud]], on[stock_code, report_year], howleft) # 未匹配到的样本标记为正常 df[is_fraud] df[is_fraud].fillna(0).astype(int) print(总样本数:, len(df)) print(造假样本数:, df[is_fraud].sum()) print(正负比: 1 :, round((len(df) - df[is_fraud].sum()) / max(df[is_fraud].sum(), 1), 1))这段代码的核心逻辑是时间对齐和表连接。report_year由处罚年份减 2 得到保证特征和标签在时间上不穿越。合并用 inner join 保证主表和比率表都有记录标签用 left join 避免丢掉没有处罚记录的正常样本。最后打印正负比如果低于 1:30后面采样策略要格外小心。2.2 缺失值与异常值的处理边界财务数据缺失是常态尤其是中小板公司。处理方式不能一刀切用均值填充因为缺失本身可能携带信息——比如某公司连续两年不披露某项关联交易金额这本身就是风险信号。我一般会分三步走先算每个特征的缺失率缺失率超过 60% 的直接删列缺失率在 10% 到 60% 之间的加一列缺失标记再填充缺失率低于 10% 的用行业中位数填充。异常值同理。资产负债率超过 1 不一定错可能是资不抵债但毛利率超过 100% 或者为负几千大概率是科目口径问题。处理方式是先做分位数截断把 1% 以下和 99% 以上的值拉到边界而不是直接删样本因为造假样本本来就少删不起。# 缺失率统计与分策略填充 missing_rate df.isnull().mean().sort_values(ascendingFalse) # 缺失率超过 60% 的列直接删除 drop_cols missing_rate[missing_rate 0.6].index.tolist() df df.drop(columnsdrop_cols) # 缺失率 10%-60% 的列加标记后填充 mid_missing missing_rate[(missing_rate 0.1) (missing_rate 0.6)].index.tolist() for col in mid_missing: df[col _is_missing] df[col].isnull().astype(int) df[col] df[col].fillna(df[col].median()) # 缺失率低于 10% 的列用中位数填充 low_missing missing_rate[missing_rate 0.1].index.tolist() for col in low_missing: if col in df.columns: df[col] df[col].fillna(df[col].median()) # 分位数截断异常值 num_cols df.select_dtypes(include[np.number]).columns.tolist() for col in num_cols: if col in [is_fraud, report_year]: continue low, high df[col].quantile(0.01), df[col].quantile(0.99) df[col] df[col].clip(low, high)这里的关键参数是 0.6 和 0.1 两个阈值。0.6 是经验值超过这个比例的特征即使填充也是噪声为主0.1 以下用中位数填充风险可控。加缺失标记这一步很多人会省但在造假预测里缺失模式本身就是特征省掉会丢信息。截断用 1% 和 99% 分位数比 3σ 更稳因为财务数据分布厚尾严重。2.3 特征筛选别把几百列比率一股脑塞进去财务比率动辄上百列很多是高度共线的比如流动比率和速动比率、总资产周转率和固定资产周转率。全塞进去深度学习模型也能跑但训练慢、解释性差、过拟合风险高。我一般先用方差过滤去掉近似常量的列再用皮尔逊相关系数去掉共线对最后用树模型的特征重要性做一轮粗筛保留前 60 到 80 列。from sklearn.feature_selection import VarianceThreshold from sklearn.ensemble import RandomForestClassifier # 去掉方差接近 0 的特征 selector VarianceThreshold(threshold0.01) X df.drop(columns[stock_code, report_year, is_fraud]) y df[is_fraud] X_var pd.DataFrame(selector.fit_transform(X), columnsX.columns[selector.get_support()]) # 用随机森林做一轮特征重要性排序 rf RandomForestClassifier(n_estimators200, max_depth8, random_state42, n_jobs-1) rf.fit(X_var, y) importance pd.Series(rf.feature_importances_, indexX_var.columns).sort_values(ascendingFalse) # 保留前 70 个特征 top_features importance.head(70).index.tolist() X_final X_var[top_features] print(最终特征数:, X_final.shape[1])方差阈值 0.01 是去掉那些几乎不变的列比如某年所有公司都一样的行业标记。随机森林用 200 棵树、深度 8是为了快速拿到重要性排序不追求最优性能。保留前 70 个特征是平衡信息量和训练成本的折中实际可以按验证集表现微调。注意特征筛选必须在训练集上做再应用到验证集和测试集否则会引入选择偏差。3. Bagging 和深度学习怎么拼从基学习器到集成策略3.1 Bagging 的抽样逻辑与基学习器选择Bagging 的核心是有放回抽样每次从训练集里抽同样大小的样本但允许重复。对于造假预测这种正样本极少的数据直接对全量做有放回抽样很多基学习器可能一个正样本都抽不到。所以实际做法是分层抽样先按标签分层在每个层内做有放回抽样保证每个基学习器的正负比和原始数据一致。基学习器选什么常见组合是决策树、随机森林、梯度提升树各来几个。但既然标题里带了深度学习我一般会把 Bagging 的基学习器设成一个小型 MLP这样集成的是多个神经网络的预测。MLP 结构不用太深两层隐藏层、每层 64 到 128 个神经元就够了因为财务表格数据的特征交互复杂度有限太深反而过拟合。from sklearn.ensemble import BaggingClassifier from sklearn.neural_network import MLPClassifier from sklearn.model_selection import train_test_split # 分层划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( X_final, y, test_size0.2, stratifyy, random_state42 ) # 定义 MLP 基学习器 base_mlp MLPClassifier( hidden_layer_sizes(128, 64), activationrelu, solveradam, alpha0.001, batch_size64, learning_rate_init0.001, max_iter300, early_stoppingTrue, random_state42 ) # Bagging 集成分层抽样 bagging BaggingClassifier( estimatorbase_mlp, n_estimators15, max_samples0.8, max_features0.7, bootstrapTrue, bootstrap_featuresFalse, random_state42, n_jobs-1 ) bagging.fit(X_train, y_train) print(训练集 AUC:, bagging.score(X_train, y_train)) print(测试集 AUC:, bagging.score(X_test, y_test))n_estimators15是基学习器数量太少方差降不下来太多训练时间线性增长15 到 25 之间比较合适。max_samples0.8表示每个基学习器抽 80% 的样本留一点差异度。max_features0.7表示每个基学习器随机选 70% 的特征进一步增加多样性。MLP 的alpha0.001是 L2 正则化系数财务数据噪声大正则不能太弱。early_stoppingTrue让验证集损失不下降时提前停省时间也防过拟合。3.2 深度学习分支用类别权重和 Focal Loss 对抗不平衡Bagging 解决的是方差问题但正样本太少导致的梯度偏向问题还得靠损失函数。标准交叉熵在 1:50 的正负比下模型会把所有样本预测为负类就能拿到 98% 的准确率但召回率为零。常见做法有两种一是给正样本加权权重设为负正样本比的倒数二是用 Focal Loss让模型聚焦在难分类的样本上。import torch import torch.nn as nn class FocalLoss(nn.Module): def __init__(self, alpha0.75, gamma2.0): super().__init__() self.alpha alpha self.gamma gamma def forward(self, logits, targets): # logits: (N, 1), targets: (N,) probs torch.sigmoid(logits).squeeze() targets targets.float() # 正样本权重 alpha负样本权重 1-alpha alpha_t self.alpha * targets (1 - self.alpha) * (1 - targets) pt probs * targets (1 - probs) * (1 - targets) loss -alpha_t * ((1 - pt) ** self.gamma) * torch.log(pt 1e-8) return loss.mean() class FraudMLP(nn.Module): def __init__(self, input_dim): super().__init__() self.net nn.Sequential( nn.Linear(input_dim, 128), nn.BatchNorm1d(128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, 64), nn.BatchNorm1d(64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, 1) ) def forward(self, x): return self.net(x)Focal Loss 里alpha0.75表示正样本权重更高gamma2.0是聚焦参数越大越关注难样本。网络结构里 BatchNorm 放在 Linear 之后、ReLU 之前是标准做法能加速收敛。Dropout 设 0.3因为财务特征之间相关性高dropout 太强会丢信息。输出层是 1 维配合 sigmoid 做二分类。3.3 把 Bagging 和深度学习串起来两种集成路径路径一Bagging 包深度学习。就是 3.1 里的做法用 BaggingClassifier 包 MLP每个基学习器是一个独立训练的 MLP最后投票。优点是实现简单sklearn 一把梭缺点是每个 MLP 独立训练没法共享底层表征训练慢。路径二深度学习内部做 Bagging。在 PyTorch 里手动实现每次 epoch 前对训练集做有放回抽样训练一个子网络跑完所有子网络后对预测取平均。这种方式可以在子网络之间共享部分底层参数训练效率更高但代码复杂度上去了。我一般先用路径一快速验证方案可行性确认 AUC 和召回率能到预期后再切路径二做性能优化。路径一在 15 个基学习器、每个 MLP 跑 300 epoch 的情况下单卡大概 20 到 30 分钟能跑完路径二能压到 10 分钟左右。# 路径二手动 Bagging 深度学习 def train_bagged_mlp(X_train, y_train, n_models10, epochs50, sample_ratio0.8): input_dim X_train.shape[1] models [] for i in range(n_models): # 有放回抽样 idx np.random.choice(len(X_train), sizeint(len(X_train) * sample_ratio), replaceTrue) X_boot torch.FloatTensor(X_train.iloc[idx].values) y_boot torch.LongTensor(y_train.iloc[idx].values) model FraudMLP(input_dim) optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) criterion FocalLoss(alpha0.75, gamma2.0) model.train() for epoch in range(epochs): optimizer.zero_grad() logits model(X_boot) loss criterion(logits, y_boot) loss.backward() optimizer.step() models.append(model) return models def predict_ensemble(models, X_test): X_tensor torch.FloatTensor(X_test.values) preds [] for model in models: model.eval() with torch.no_grad(): logits model(X_tensor) probs torch.sigmoid(logits).squeeze().numpy() preds.append(probs) return np.mean(preds, axis0)n_models10是子网络数量sample_ratio0.8控制每个子网络的训练集比例。weight_decay1e-4是 Adam 的 L2 正则和 Focal Loss 配合用。预测时把所有子网络的概率取平均这是 Bagging 投票的标准做法。注意每个子网络训练前要重新抽样不能共用同一份数据否则集成就退化成单模型了。4. 训练、验证与调参把 AUC 和召回率同时拉起来4.1 评估指标怎么选别只看准确率造假预测里准确率是最没用的指标。一个把所有样本判为正常的模型准确率能到 98%但召回率为零。我一般看三个指标AUC 衡量排序能力召回率衡量抓造假的能力F1 衡量精确率和召回率的平衡。如果业务上更怕漏掉造假公司召回率优先如果更怕误伤正常公司精确率优先。from sklearn.metrics import roc_auc_score, recall_score, precision_score, f1_score # 用 Bagging 模型预测测试集 y_prob bagging.predict_proba(X_test)[:, 1] y_pred (y_prob 0.5).astype(int) print(AUC:, round(roc_auc_score(y_test, y_prob), 4)) print(召回率:, round(recall_score(y_test, y_pred), 4)) print(精确率:, round(precision_score(y_test, y_pred), 4)) print(F1:, round(f1_score(y_test, y_pred), 4))阈值 0.5 是默认值但在不平衡数据里往往不是最优。可以画 PR 曲线找 F1 最大的阈值。如果业务要求召回率不低于 0.8就把阈值往下调牺牲一点精确率换召回。4.2 关键超参数与调参顺序这套方案里影响最大的超参数有五个Bagging 的基学习器数量、MLP 的隐藏层结构、学习率、Dropout 比例、Focal Loss 的 alpha 和 gamma。调参顺序我一般按影响从大到小来先定隐藏层结构再调学习率和 Dropout然后调 Focal Loss 参数最后调 Bagging 的基学习器数量。超参数推荐范围影响调参建议隐藏层结构(128,64) 到 (256,128,64)容量与过拟合先试 (128,64)不够再加学习率1e-4 到 1e-3收敛速度从 1e-3 开始不收敛降一个量级Dropout0.2 到 0.5正则强度过拟合就加欠拟合就减Focal alpha0.6 到 0.9正样本权重召回低就加精确低就减Focal gamma1.0 到 3.0难样本聚焦从 2.0 开始微调基学习器数量10 到 30方差降低到 20 以后收益递减调参时用分层 K 折交叉验证不要用单次划分因为正样本少单次划分的验证集可能只有几个正样本指标抖动大。5 折交叉验证取平均结果稳得多。4.3 训练过程监控与早停深度学习训练最怕两件事过拟合和梯度消失。监控手段是每个 epoch 记录训练损失和验证集 AUC如果训练损失持续下降但验证 AUC 开始下降就是过拟合该早停。如果训练损失几乎不降可能是学习率太小或者梯度消失检查 BatchNorm 有没有加对。# 训练过程监控示例 train_losses, val_aucs [], [] best_auc 0 patience 10 wait 0 for epoch in range(200): model.train() optimizer.zero_grad() logits model(torch.FloatTensor(X_train.values)) loss criterion(logits, torch.LongTensor(y_train.values)) loss.backward() optimizer.step() train_losses.append(loss.item()) # 验证 model.eval() with torch.no_grad(): val_logits model(torch.FloatTensor(X_test.values)) val_prob torch.sigmoid(val_logits).squeeze().numpy() val_auc roc_auc_score(y_test, val_prob) val_aucs.append(val_auc) # 早停逻辑 if val_auc best_auc: best_auc val_auc wait 0 torch.save(model.state_dict(), best_model.pth) else: wait 1 if wait patience: print(f早停于 epoch {epoch}最佳 AUC: {best_auc:.4f}) breakpatience10表示验证 AUC 连续 10 个 epoch 不提升就停。保存最佳模型权重而不是最后一个 epoch 的权重因为最后一个 epoch 可能已经过拟合了。这个监控逻辑在路径一和路径二里都适用。5. 避坑与排查财务造假预测里最容易翻车的五个地方5.1 时间穿越用未来数据预测过去现象验证集 AUC 高到 0.95 以上上线后效果暴跌。原因特征里混入了未来信息比如用了处罚公告年份的财务数据去预测同年的造假标签。解决严格按报告年份对齐标签年份前推两到三年所有特征只取报告年份及之前的数据。5.2 样本泄漏训练集和测试集有重叠公司现象测试集表现异常好换一批公司就崩。原因同一家公司的不同年份样本被分到了训练集和测试集模型记住了这家公司的特征。解决按公司划分训练集和测试集同一家公司的所有年份要么全在训练集要么全在测试集。5.3 正样本太少导致交叉验证不稳定现象5 折交叉验证每折的 AUC 波动超过 0.1。原因正样本总数可能只有一两百每折验证集里正样本只有几十个指标抖动大。解决用分层 K 折保证每折正负比一致或者用重复分层 K 折取多次平均。5.4 特征标准化在树模型和神经网络之间不通用现象Bagging 里混了树模型和 MLP树模型不需要标准化MLP 需要一起处理导致树模型性能下降。原因标准化改变了特征的数值尺度树模型的分裂点会变。解决树模型和神经网络分开处理树模型用原始特征神经网络用标准化后的特征最后在预测层面集成。5.5 阈值选择拍脑袋现象模型 AUC 不错但业务方说抓出来的全是误报。原因分类阈值用了默认的 0.5没有根据业务需求调整。解决画 PR 曲线根据业务能接受的误报率反推阈值或者用代价敏感学习给漏报和误报设不同的代价。6. 进阶技巧用 SHAP 做特征归因让模型不再是黑匣子模型跑通只是第一步业务方最常问的问题是“为什么这家公司被判定为高风险”。这时候 SHAP 就派上用场了。SHAP 基于博弈论里的 Shapley 值能给每个样本的每个特征算出一个贡献值正贡献推高造假概率负贡献拉低。对于 Bagging 加深度学习的集成模型可以用 KernelSHAP 做近似虽然慢一点但比深度学习原生的归因方法稳。import shap # 用训练集的一个子集做背景数据加速 SHAP 计算 background X_train.sample(100, random_state42) explainer shap.KernelExplainer(bagging.predict_proba, background) # 对测试集前 50 个样本做归因 shap_values explainer.shap_values(X_test.iloc[:50], nsamples200) # 输出单个样本的特征贡献 sample_idx 0 contributions pd.Series(shap_values[1][sample_idx], indexX_test.columns) print(推高造假概率的前五个特征:) print(contributions.sort_values(ascendingFalse).head(5)) print(拉低造假概率的前五个特征:) print(contributions.sort_values().head(5))background用 100 个样本做背景分布太多会慢太少不准。nsamples200是 KernelSHAP 的采样次数越大越准也越慢200 是精度和速度的折中。shap_values[1]取的是正类的 SHAP 值因为二分类输出两个类别。输出里正贡献最大的特征就是这家公司被判定为高风险的主要原因可以直接拿给业务方看。我自己的习惯是每次模型迭代后都跑一遍全局 SHAP 摘要图看特征重要性排序有没有大变化。如果某次迭代后某个之前不重要的特征突然排到前三大概率是数据管道出了问题比如某列缺失值填充逻辑改了或者某张表的连接键对错了。这个习惯帮我提前发现过好几次数据事故比等模型上线后报警要划算得多。另外一个小技巧把 SHAP 值和原始财务比率对照着看。如果模型认为“应收账款周转率异常低”是主要风险信号那就去翻这家公司的应收账款账龄结构看是不是有大额长期挂账。模型给的是统计关联业务验证给的是因果确认两者结合才能让风控策略真正落地。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →