
简介本资源是泰迪杯2021A题《基于Bagging和深度学习的上市公司财务数据造假预测》的完整Python实现方案面向金融风控、数据科学竞赛选手及机器学习进阶学习者聚焦于小样本、高不平衡场景下的财务异常识别建模。压缩包共35个文件含15个CSV数据集含原始附件1/2及预处理后数据、9个核心Python脚本涵盖DCRN模型构建、Bagging集成、特征重要性分析与交叉验证、3张关键结果图如baggingdcrn.png、1份PDF项目说明文档及配套环境配置与README整体大小37.07MB。已有38人学习下载。读者可直接复现从行业特征筛选SMOTE多树模型融合权重、制造业与其他行业分组建模到DCRN网络设计残差Cross结构BatchNormDropout、Bagging集成优化的全流程代码模块清晰、注释完备特别适合深入理解金融欺诈检测中深度学习与集成方法协同建模的实战逻辑。 做了一段时间的金融风控方向项目我越来越觉得“财务数据造假预测”是个特别适合入门的实战选题。它不像很多算法项目只调一个模型就完事而是要把数据处理、特征工程、模型选型、结果解释整个链路完整走一遍。今天要拆解的这个项目核心任务很清晰基于上市公司的财务指标数据用Bagging和深度学习结合的方式训练一个能识别财务造假嫌疑的分类模型并把整套训练流程和Python源码整理交付。项目拿到的压缩包里包含了训练数据、项目说明、Python训练脚本和模型调用示例。从实际复现的角度看这已经是一个非常标准的机器学习项目结构。适合谁看两类人最合适一类是准备往风控、反欺诈方向发展的算法工程师和数据挖掘从业者另一类是想找一个“有多模型融合、有特征工程、有业务落地感”的练手项目来提升简历含金量的学生。这篇文章我会从数据准备、算法选型、代码实现到坑点复盘把整个项目完整过一遍。1. 项目在做什么为什么盯上财务数据造假预测1.1 一句话理解这个项目财务数据造假预测本质上是一个二分类问题。输入是某上市公司最近几期的财务指标比如资产负债率、应收账款周转率、经营现金流、净利润增速、审计意见类型等输出是这家公司是否存在财务造假的概率。模型不复杂复杂的是怎么从一堆看似正常的报表数字里把造假行为留下的痕迹“显影”出来。我实际跑下来之后最大的感受是这项目表面上是在训练模型实际上是在训练你对业务数据的敏感度。同样的算法拿去做鸢尾花分类你根本不用关心特征含义但放到财务场景里每一个特征都对应一个真实业务动作。比如应收账款异常增加可能意味着公司放宽了信用政策来冲收入经营现金流长期低于净利润可能说明利润只是账面数字。对这些业务逻辑理解得越深特征工程做得越好模型的提升空间就越大。1.2 造假公司在报表上留下的“数字指纹”财务造假并不是完全无迹可寻的。根据国内外大量处罚案例的经验造假公司在报表上通常会留下几类特征盈利质量异常净利润很高但经营现金流很低两者长期背离。营运能力异常存货周转率或应收账款周转率明显低于行业平均水平说明账面收入没有对应的实物流转。偿债能力异常资产负债率异常攀升同时短期借款增长迅猛疑似需要外部资金来维持账面运营。审计信号异常审计意见不是标准无保留意见或者频繁更换会计师事务所。这些特征不一定会同时出现但也正因为造假手段多样化单一模型很难覆盖所有模式。这也是项目里同时引入Bagging和深度学习的原因用集成学习吃透表格数据的统计规律用深度学习捕捉特征之间的非线性交互两者互补。1.3 为什么不用单一模型而是Bagging加深度学习很多人会问做表格数据XGBoost、LightGBM不是更强吗为什么还要折腾深度学习我在实际复现里体会到的原因有两个。第一集成学习确实稳定RandomForest这类Bagging模型对噪声容忍度很高在处理高维表格数据时不容易过拟合这在财务数据这种“信号弱、噪声大”的场景里非常关键。第二深度学习的价值在于自动提取高阶特征。比如“应收账款增速和收入增速的比值”这种复合变量传统方法需要手工构造而多层网络理论上可以从原始特征里自己学出来。所以项目里的策略不是“二选一”而是“一起上”先让两种模型各自学习再用融合策略把结果组合起来。这种做法在工程上比单纯堆模型效果要扎实得多也方便后续在特征层面做取舍优化。2. 数据准备与特征工程模型的地基2.1 数据从哪来标签怎么定义训练财务造假预测模型最核心的不是算法而是标签。沪深两市的财务数据可以通过CSMAR、Wind这类金融数据库导出也可以直接从巨潮资讯网下载公开财报。真正的难点在于怎么定义“这家公司造假了”。我的做法是以监管机构发布过的处罚公告、问询函、关注函作为正样本来源把在被正式处罚或问询后确认涉假的上市公司标记为1其他正常公司标记为0。值得一提的是正样本的判定往往滞后于造假发生的时间所以建模时要选择合适的观测窗口。通常做法是取造假行为被发现前1到2年的财务数据作为特征而不是用处罚当年的数据。如果时间对齐没做对模型学到的东西会完全失真。2.2 特征怎么构造才有区分度财务指标原始字段非常多但很多是高度相关的全扔进模型反而会干扰训练。我从五个维度做了筛选和加工盈利能力净资产收益率、毛利率、净利润率、扣非净利润占比。营运能力应收账款周转率、存货周转率、总资产周转率。现金流质量经营现金流/净利润、经营现金流/营业收入。偿债能力资产负债率、流动比率、速动比率。审计与结构信息审计意见类型、是否更换事务所、其他应收款占比。另外我强烈建议加入一阶差分和同比变化量。比如“应收账款增速-营业收入增速”如果这个值为正且持续扩大就说明公司可能是靠“赊销”堆收入这在很多造假案里是共同特征。这类衍生特征在模型里的重要性往往比原始指标高出一大截。2.3 预处理和样本不平衡处理财务数据最大的问题是缺失值多且分布不均衡。很多指标在不同报告期才披露样本里天然存在空洞。我的处理顺序是先删除缺失率超过50%的特征避免引入太多噪声。对剩余缺失值用中位数或同类均值填充不推荐用简单零填充会让模型学到错误的偏置。对连续型特征做标准化或RobustScaler因为财务数据里经常有极端值普通StandardScaler会被极端值带偏。然后是样本不平衡问题。造假公司在全市场里毕竟是少数负样本可能只有正样本的十分之一甚至更低。不处理的话模型大概率会全部预测为正常。项目里用了两种策略一是给模型训练加class_weight参数让少数类获得更高权重二是用SMOTE做正样本过采样。不过要提醒一句SMOTE之后一定只能在训练集上做千万不能把验证集也放进去合成否则会产生严重的数据泄露。注意财务数据造假预测里各种“看起来合理”的预处理步骤都可能引入未来信息。标准化、填充、过采样这些操作都必须严格切分在训练集内部完成再直接套用到验证集和测试集。这个细节是模型上线后效果是否可靠的分水岭。3. 算法方案和Python实现Bagging与深度学习的融合3.1 Bagging的数学直觉为什么要“投票”Bagging的英文全称是Bootstrap Aggregating核心思想只有一句话从原始训练集里用有放回抽样的方式生成多个子集分别训练多个基学习器最后通过投票或平均的方式得到最终结果。为什么这么干有效原因是它显著降低了模型的方差。单个决策树很容易被训练数据里的小波动带偏但如果你训练100棵树每棵树只在某个随机子集上看问题最后大家的“平均意见”就会比单棵树稳定很多。随机森林就是在Bagging的基础上又加了一层特征随机抽样进一步降低树与树之间的相关性让投票结果更可靠。这个思路放在财务场景里非常合理。财务数据本身信噪比低一个特征偶尔会有误导性但100个模型一起判断就不会被单个噪声特征带偏。3.2 深度学习部分怎么设计表格数据不需要太深的网络很多人一听“深度学习”大脑里就浮现大语言模型、CNN、Transformer那一堆东西。但在这个项目里深度学习承担的任务很简单从表格特征里提取非线性组合。因此最简单的多层感知机MLP就够用。我的网络设计是这样输入层接标准化后的特征向量中间叠加两层全连接层激活函数选ReLU每个全连接层后面加Dropout做随机失活输出层用Sigmoid输出造假概率。隐藏层维度可以设成输入特征数的1.5倍到2倍不用太宽太宽容易过拟合。如果你的样本量不大层数控制在2到3层就够了过深的网络在几千条样本上基本会炸。另外还要强调一点财务数据通常是按季度或年度记录的天然带时序属性。如果后续想扩展可以考虑把每一期的特征做序列建模用LSTM或者Transformer编码器去捕捉时间维度上的变化趋势而不是只用截面特征。但这个项目目前的深度学习部分不做序列处理就把当期特征当成静态向量来用也是完全合理的做法。3.3 模型融合的三种做法项目里把Bagging和深度学习放在一起常见的融合方式有三种我分别实验过方案ABagging套DNN。把深度网络当作基学习器用Bootstrap采样的不同子集分别训练多个DNN最后做概率平均。方案B自编码器提取重构误差特征再喂给随机森林。利用深度自编码器对正常样本重构能力强的特点算出每一家样本的重构误差把它作为一个新特征和原始特征一起输入随机森林。方案CStacking层级融合。第一层训练随机森林、XGBoost、DNN等多个基学习器第二层用逻辑回归或简单的MLP把它们的输出概率组合起来。我在项目里最终选择的路线是“方案B方案C”的混合思路先用自编码器做异常检测得到重构误差异常分再把它和原始特征合并后交给随机森林训练最后再用一个简单的逻辑回归把随机森林和DNN的结果做Stacking。整体效果比单模型稳定不少而且每个环节都能解释方便调试。3.4 核心代码路演下面是我从项目里简化整理出来的代码骨架逻辑足够跑通一个小型实验。首先是最基本的随机森林训练模块import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.preprocessing import RobustScaler data pd.read_csv(financial_data.csv) features [col for col in data.columns if col not in [label, company_code, report_date]] X data[features].copy() y data[label].copy() # 缺失值填充 X X.fillna(X.median()) # 标准化 scaler RobustScaler() X_scaled scaler.fit_transform(X) # 时间切分避免随机切分带来的未来数据泄露 train_idx data[report_date] 2020-01-01 val_idx data[report_date] 2020-01-01 X_train, X_val X_scaled[train_idx], X_scaled[val_idx] y_train, y_val y[train_idx], y[val_idx] rf RandomForestClassifier( n_estimators300, max_depth12, min_samples_leaf5, class_weightbalanced, n_jobs-1, random_state42 ) rf.fit(X_train, y_train) y_pred_prob rf.predict_proba(X_val)[:, 1]然后是DNN部分的训练代码用PyTorch实现一个简单MLPimport torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset X_train_t torch.tensor(X_train, dtypetorch.float32) y_train_t torch.tensor(y_train.values, dtypetorch.float32) val_t torch.tensor(X_val, dtypetorch.float32) dataset TensorDataset(X_train_t, y_train_t) loader DataLoader(dataset, batch_size64, shuffleTrue) class DnnClassifier(nn.Module): def __init__(self, n_features): super().__init__() self.net nn.Sequential( nn.Linear(n_features, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, 32), nn.ReLU(), nn.Dropout(0.2), nn.Linear(32, 1), nn.Sigmoid() ) def forward(self, x): return self.net(x).squeeze(-1) model DnnClassifier(n_featuresX_train.shape[1]) optimizer optim.Adam(model.parameters(), lr1e-3) loss_fn nn.BCELoss() for epoch in range(50): model.train() for xb, yb in loader: optimizer.zero_grad() out model(xb) loss loss_fn(out, yb) loss.backward() optimizer.step() model.eval() with torch.no_grad(): dnn_prob model(val_t).numpy()最后把两个模型的输出合并做Stackingimport numpy as np from sklearn.linear_model import LogisticRegression stack_features np.column_stack([ y_pred_prob, # 随机森林输出 dnn_prob, # DNN输出 rf_oob_trick_score # 如果有的话可以再加一个异常分特征 ]) meta_model LogisticRegression() meta_model.fit(stack_features, y_val) final_prob meta_model.predict_proba(stack_features)[:, 1]这段代码展示了核心思路但实际项目里还会有更完整的自编码器重构误差计算、特征重要性分析和模型保存部分逻辑都是一脉相承的。4. 训练、评估与调优一分钱一分货一分数据一分模型4.1 评估指标别只盯着准确率财务造假检测是典型的“类别不平衡问题”正样本可能只有5%如果模型全部预测为正常类别准确率依然很高但一点用都没有。我看模型只看三个指标AUC综合反映了模型对正负样本的区分能力0.85以上基本可用。召回率在所有真实造假样本里模型找出了多少。这个指标在风控场景里比精确率更重要因为漏掉一个造假公司损失可能是几十亿。PR曲线下的面积在正样本极少的情况下PR曲线比ROC曲线更能反映真实性能。在项目实践里我调参的目标往往不是让AUC无限高而是在保持AUC不跌的前提下尽量提升召回率。因为后面还要结合人工复核宁可模型多报一些嫌疑公司也不能让造假公司悄悄溜走。4.2 时间切分不要随机打乱数据表格数据项目里很多人习惯直接train_test_split(random_state42)随机切分但财务数据绝对不行。财报数据是强时序数据用未来数据训练再去预测过去就是典型的数据泄露。正确的做法是像上面代码展示的那样按报告期切分比如前5年做训练近1年做验证再往后做测试。我实测下来随机切分时模型AUC可能到0.95看着非常漂亮但换成时间切分后掉到0.82这才是真实水平。原因很简单随意切分会让模型在训练阶段“见过”未来的公司样本相当于开卷考试。项目说明里如果没有特别强调这一点你在自己复现时一定要补上这一步否则模型上线后会被市场教育得很惨。4.3 参数调整心得先说随机森林。n_estimators我一般放在300左右更多树对效果提升有限训练时间却成倍增长。max_depth设为8到15之间比较合理太深容易过拟合太浅学不到特征交互。max_features建议从sqrt(n_features)开始调这个参数对随机森林效果的影响往往比树的深度更大。class_weightbalanced可以自动根据样本比例调整权重非常适合财务造假这种不平衡场景。再说DNN。学习率先用1e-3跑20个epoch如果损失下降很慢或者震荡明显降到1e-4。BatchSize在64到128之间选择一个太小训练不稳定太大容易卡在局部最优。Dropout我习惯设0.2到0.4之间具体要看验证集的表现。最重要的一个技巧是早停法监控验证集AUC连续5轮不上升就停止训练省时间还能防过拟合。5. 常见问题速查与避坑经验5.1 压缩包解压报错怎么办项目压缩包是zip格式但很多人在解压时会碰到“file is not a zip file”或者“invalid zip archivecould not find EOCD”这类报错。我遇到过两次原因总结下来就是三种文件下载不完整。压缩包本身没下载完文件大小都不对重新下载一次基本能解决。文件被加密或者压缩格式不是标准zip。有些项目打包时会用7z或者分卷压缩这时直接改后缀名没有用要用对应工具解压。压缩包内部目录带中文或特殊字符在Windows默认解压工具下会乱码或失败。可以改用Bandizip这类支持UTF-8编码解码的工具能省掉一堆麻烦。还有一个更隐蔽的情况解压后源码跑不起来提示缺少数据文件。这时优先检查项目说明里是否有“数据文件单独放置”的说明财务数据因体积问题经常和代码分开存放缺了数据文件光有源码等于空转。5.2 环境依赖版本冲突项目源码对Python和几个核心库的版本是有要求的。我在复现时踩过的坑包括Python版本太新或太旧导致sklearnAPI不兼容。比如老版本用的sklearn.cross_validation在新版本里改成了sklearn.model_selection。PyTorch和TensorFlow混用。有些项目会同时引入两种框架导致CUDA版本冲突或内存叠加。建议优先统一到一种框架上项目里深度学习部分用PyTorch更轻量。lightgbm和xgboost在Windows下有时会缺DLL。如果项目里用到这两个库建议用conda直接安装预编译版本。拿到源码后第一件事就是按项目说明建虚拟环境再按requirements.txt装依赖不要直接用全局环境跑。如果不按照项目说明的版本去装大概率会遇到“这个函数不存在”或者“那个模块导入失败”的连锁报错。5.3 模型效果不佳的排查顺序如果自己替换数据后模型AUC只有0.6出头不用急着换模型按下面表格里的方向排查症状大概率原因解决办法训练集AUC接近1验证集AUC低过拟合特征太多或模型太复杂增加正则项、减少特征数量、调低max_depth预测结果几乎全是0类别不平衡没处理加class_weight或做SMOTE过采样特征重要性集中在少数几个字段特征衍生不够增加交叉特征、差分特征验证集波动很大时间切分不均匀某年份样本量骤减用滚动窗口交叉验证测试集效果远低于验证集数据分布变化或行业差异按行业做特征标准化或分行业建模实际项目里财务数据造假预测的效果上限往往由特征决定而不是由模型决定。如果原始字段列表太单薄最好先补充衍生特征再看模型调参。6. 后续扩展与落地建议6.1 还能往哪些方向延伸这个项目的框架搭建起来之后可以很容易向以下几个方向扩展。第一是引入NLP文本信息上市公司的年报、管理层讨论与分析、审计报告都是文本里面往往藏着比财务数字更明显的“话术”。比如管理层讨论非常含糊、避实就虚或者频繁使用复杂表述解释业绩波动都可以作为辅助信号。第二是图神经网络公司之间存在担保、关联交易、客户供应商关系用图结构做风险传播识别比单看一家公司更有全局视野。第三是时序模型把多家公司的多期报表数据组织成时间序列用LSTM或Transformer编码器捕捉趋势突变。但需要注意的是每扩展一个方向数据和特征工程的复杂度都会成倍上升代码量和成也解释成本也会上升。入门阶段先把当前项目吃透再考虑这些延展。6.2 几点对“项目落地”的个人建议我最后想说的是这种模型虽然叫“造假预测”但它的输出在真实业务里只能作为风险线索不能用来直接下定论。一个公司被模型标记为高风险只意味着它在统计特征上和历史上的造假公司相似不意味着它一定有问题。真正落地时模型负责筛出需要重点关注的名单再由分析师或审计师去做人工核查。所以在写项目说明或者给别人交付代码时一定要在文档里写清楚这个边界避免使用者误读结果。我个人的做法是在输出概率的同时保留特征重要性或SHAP解释这样用户可以看到模型为什么给出这个判断提高最终决策的可信度。另外从项目后续优化的角度看如果数据集可以持续更新建议用滚动训练的策略每个月或每个季度把新增的财报数据和事件数据并入重新训练一次而不是训练一个模型用到底。财务造假的模式也是会演化的模型只有跟着新样本一起变才能保持长期的可用性。本文还有配套的精品资源点击获取