ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Bagging+深度学习:财务造假预测实战指南

Bagging+深度学习:财务造假预测实战指南 简介本资源为泰迪杯2021A题《基于Bagging和深度学习的上市公司财务数据造假预测》的完整Python实现面向参加数据挖掘竞赛的学生及对财务风控建模感兴趣的开发者。项目围绕三个赛题小问展开先按行业分类筛选与造假相关的财务指标并比较行业差异再分别识别制造业与其他行业第6年造假公司。方案在问题一采用SMOTE过采样结合决策树、随机森林、ExtraTree与XGBoost四种树模型计算特征重要性均值取前30个特征问题二、三构建DCRN网络融合多层感知机、残差网络与Cross网络并引入BatchNorm、Dropout与提前停止最后用Bagging集成降低方差。资源包共35个文件含15个csv数据、9个py源码、3张png图表及xlsx、pdf、md等说明文档约37.07MB目录涵盖特征工程、模型训练、交叉验证与预测脚本。已有41人学习适合作为赛题复现与深度学习二分类建模的参考。1. 财务造假预测为什么值得用 Bagging 加深度学习重做一遍上市公司财务造假预测本质是一个高维、小样本、极度不平衡的二分类问题。传统做法是拿几十个财务比率喂给逻辑回归或决策树AUC 能到 0.7 就算不错。但真实场景里造假样本占比往往不到 5%单模型很容易被多数类带偏把「不造假」全预测对就能拿 95% 准确率这种模型上线就是灾难。Bagging 加深度学习的组合恰好能同时缓解两个痛点Bagging 通过自助采样降低方差、提升对噪声特征的鲁棒性深度学习则负责从原始财务指标里自动挖掘非线性交互比如「应收账款周转率骤降 毛利率异常高于同行」这种组合信号人工很难穷举网络却能学到。这套方案适合有 Python 基础、想从传统机器学习往深度学习过渡的从业者也适合做风控、审计、量化基本面研究的人拿来当基线。下面从数据构造、模型搭建、训练调参到避坑一步步拆开讲。2. 数据层把财务报表变成模型能吃的张量2.1 造假标签怎么定义才不翻车标签是整个项目的天花板。常见做法是拿监管处罚公告、交易所问询函、审计意见类型非标意见作为正样本来源再按「处罚年份前推 1 到 2 年」对齐财务数据因为造假行为通常持续数年才被揭露直接用处罚当年数据会引入未来信息。负样本从同行业、同规模、同年份的正常公司里匹配避免行业和规模偏差。标签定义要写死一个规则文件比如label_rule.yaml记录正样本来源、时间窗口、匹配比例后续换数据集时只改这个文件不动代码。import pandas as pd import numpy as np def build_labels(penalty_df, financial_df, window2): penalty_df: 处罚公告含 stock_code, penalty_year financial_df: 财务数据含 stock_code, report_year, 各财务指标 window: 处罚年份前推年数默认2年 labels [] for _, row in penalty_df.iterrows(): for offset in range(1, window 1): target_year row[penalty_year] - offset labels.append({ stock_code: row[stock_code], report_year: target_year, label: 1 }) pos_df pd.DataFrame(labels).drop_duplicates() # 负样本同行业同规模匹配这里简化为随机采样同数量 all_neg financial_df[~financial_df[stock_code].isin(pos_df[stock_code])] neg_df all_neg.sample(nlen(pos_df), random_state42).copy() neg_df[label] 0 neg_df neg_df[[stock_code, report_year, label]] return pd.concat([pos_df, neg_df], ignore_indexTrue)这段代码的关键参数是window设 1 太短容易漏掉早期造假设 3 以上会混入太多正常年份噪声实践中 2 年是比较稳的折中。random_state固定保证每次跑结果可复现调参阶段不要改。负样本匹配如果只做随机采样行业偏差会很大建议加一层groupby(industry)再采样代码里为了简洁省略了实际项目务必补上。2.2 财务指标标准化与缺失值处理财务数据缺失是常态尤其是小公司。直接填 0 会误导模型因为 0 和「缺失」语义完全不同。常见做法是比率类指标用行业中位数填充绝对值类指标如总资产先取对数再填充。标准化用 RobustScaler 而不是 StandardScaler因为财务指标里极端值多均值方差容易被拉偏。from sklearn.preprocessing import RobustScaler from sklearn.impute import SimpleImputer def preprocess_financial(df, ratio_cols, abs_cols): df df.copy() # 绝对值列取对数压缩量纲 for col in abs_cols: df[col] np.log1p(df[col].clip(lower0)) # 比率列用中位数填充 imputer SimpleImputer(strategymedian) df[ratio_cols] imputer.fit_transform(df[ratio_cols]) # 鲁棒标准化 scaler RobustScaler() df[ratio_cols abs_cols] scaler.fit_transform(df[ratio_cols abs_cols]) return df, scaler, imputernp.log1p处理的是log(1x)避免 0 值报错。clip(lower0)防止负数取对数财务里总资产为负基本是资不抵债可以单独标记成异常样本。RobustScaler 用中位数和四分位距对极端值不敏感这是血泪经验——用 StandardScaler 时某公司一个异常高的资产负债率能把整个特征分布带偏。2.3 时序滑窗构造样本单年快照信息有限造假往往有连续几年的征兆。用滑窗把连续 3 年财务数据拼成一个样本输入维度从(n_features,)变成(3, n_features)正好喂给后面的 CNN 或 LSTM。滑窗步长设 1重叠采样能扩增样本量小样本场景下很关键。def make_sequences(df, feature_cols, seq_len3): sequences, labels [], [] df df.sort_values([stock_code, report_year]) for code, group in df.groupby(stock_code): values group[feature_cols].values label group[label].values if len(values) seq_len: continue for i in range(len(values) - seq_len 1): sequences.append(values[i:i seq_len]) labels.append(label[i seq_len - 1]) # 用最后一年标签 return np.array(sequences), np.array(labels)seq_len3是常见起点太长样本量骤减太短退化成单年模型。标签取窗口最后一年保证预测时点不泄露未来。如果某公司中间缺年份groupby后直接跳过不要插值补年份插值会引入虚假连续性。3. Bagging 集成层为什么不是简单投票3.1 Bagging 的方差削减原理与参数Bagging 的核心是对训练集做有放回采样训练多个基学习器再平均。对深度学习来说每个基学习器是一个独立初始化的网络Bagging 相当于把「初始化随机性 数据扰动」双重随机化能显著降低预测方差。参数上基学习器数量n_estimators设 10 到 20 就够再多边际收益递减还费算力采样比例max_samples设 0.8留 20% 做袋外验证。from sklearn.ensemble import BaggingClassifier from tensorflow.keras.wrappers.scikit_learn import KerasClassifier def build_dnn(input_dim): model tf.keras.Sequential([ tf.keras.layers.Dense(64, activationrelu, input_shape(input_dim,)), tf.keras.layers.Dropout(0.3), tf.keras.layers.Dense(32, activationrelu), tf.keras.layers.Dense(1, activationsigmoid) ]) model.compile(optimizeradam, lossbinary_crossentropy, metrics[auc]) return model bagging BaggingClassifier( base_estimatorKerasClassifier(build_fnbuild_dnn, input_dimX.shape[1], epochs30, verbose0), n_estimators15, max_samples0.8, max_features0.9, bootstrapTrue, random_state42 ) bagging.fit(X_train, y_train)max_features0.9表示每次采样 90% 特征剩下 10% 随机丢弃进一步增加多样性。bootstrapTrue是有放回采样这是 Bagging 的定义。注意 KerasClassifier 在新版 TF 里已弃用可以换成scikeras.wrappers.KerasClassifier接口一致。3.2 深度学习基学习器的结构选择财务数据是表格型不是图像别一上来就堆 CNN。常见做法是先用全连接网络DNN做基线再试一维卷积Conv1D捕捉局部时序模式。Conv1D 的卷积核在时间轴上滑动能学到「连续两年应收账款异常」这种模式比 DNN 更贴合滑窗输入。def build_cnn(seq_len, n_features): model tf.keras.Sequential([ tf.keras.layers.Conv1D(32, kernel_size2, activationrelu, input_shape(seq_len, n_features)), tf.keras.layers.MaxPooling1D(pool_size2), tf.keras.layers.Flatten(), tf.keras.layers.Dense(32, activationrelu), tf.keras.layers.Dropout(0.3), tf.keras.layers.Dense(1, activationsigmoid) ]) model.compile(optimizertf.keras.optimizers.Adam(1e-3), lossbinary_crossentropy, metrics[auc]) return modelkernel_size2对应两年窗口seq_len3时卷积后长度变 2再池化变 1。如果seq_len更大可以把 kernel_size 调到 3。Dropout 放在全连接层前卷积层后一般不加 Dropout会破坏时序结构。3.3 类别不平衡的处理加权损失与阈值移动造假样本少训练时给正样本更高权重。class_weight按反比设置比如正负比 1:19正类权重设 19。预测阶段不要用默认 0.5 阈值用验证集上的 F1 最大点作为阈值。from sklearn.utils.class_weight import compute_class_weight classes np.unique(y_train) weights compute_class_weight(balanced, classesclasses, yy_train) class_weight dict(zip(classes, weights)) model.fit(X_train, y_train, class_weightclass_weight, epochs30, batch_size64) # 阈值搜索 from sklearn.metrics import f1_score probs model.predict(X_val).ravel() best_thr, best_f1 0.5, 0 for thr in np.arange(0.1, 0.9, 0.05): f1 f1_score(y_val, (probs thr).astype(int)) if f1 best_f1: best_f1, best_thr f1, thr print(f最佳阈值 {best_thr:.2f}, F1 {best_f1:.3f})compute_class_weight的balanced模式自动按样本数反比算权重。阈值搜索步长 0.05 够用太细容易过拟合验证集。最终阈值要固定下来写进配置文件线上推理直接用。4. 训练与评估别被准确率骗了4.1 分层 K 折与时间序列交叉验证财务数据有时间维度随机 K 折会让未来数据泄露到训练集。正确做法是按年份做时间序列交叉验证比如用 2015-2018 训练、2019 验证、2020 测试。如果样本量实在小至少用分层 K 折保证每折正负比例一致。from sklearn.model_selection import StratifiedKFold skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) auc_scores [] for train_idx, val_idx in skf.split(X, y): X_tr, X_val X[train_idx], X[val_idx] y_tr, y_val y[train_idx], y[val_idx] model build_cnn(seq_len3, n_featuresX.shape[2]) model.fit(X_tr, y_tr, class_weightclass_weight, epochs30, verbose0) pred model.predict(X_val).ravel() auc_scores.append(roc_auc_score(y_val, pred)) print(f5折 AUC: {np.mean(auc_scores):.3f} ± {np.std(auc_scores):.3f})shuffleTrue在分层折里可以开因为分层已经保证了分布。如果严格按时间切就把StratifiedKFold换成TimeSeriesSplit但那样每折样本更少AUC 波动会大。4.2 评估指标AUC、KS 与召回率优先财务造假预测里漏报把造假判成正常的代价远高于误报。所以召回率比精确率重要AUC 和 KS 是行业常用指标。KS 值等于max(TPR - FPR)风控里 KS 到 0.3 就算可用0.4 以上算好。from sklearn.metrics import roc_auc_score, roc_curve def ks_score(y_true, y_prob): fpr, tpr, _ roc_curve(y_true, y_prob) return np.max(tpr - fpr) auc roc_auc_score(y_val, probs) ks ks_score(y_val, probs) print(fAUC{auc:.3f}, KS{ks:.3f})AUC 衡量排序能力KS 衡量区分度。两个都看避免单一指标误导。如果 AUC 高但 KS 低说明模型在中间段区分不开可能是特征区分度不够。4.3 特征重要性用置换重要性替代树模型深度学习没有现成的feature_importances_用置换重要性permutation importance来评估。打乱某一特征后看 AUC 下降多少下降越多越重要。from sklearn.inspection import permutation_importance def score_fn(estimator, X, y): return roc_auc_score(y, estimator.predict(X).ravel()) result permutation_importance( bagging, X_val, y_val, scoringscore_fn, n_repeats10, random_state42 ) for i in np.argsort(result.importances_mean)[::-1][:10]: print(f特征 {i}: {result.importances_mean[i]:.4f} ± {result.importances_std[i]:.4f})n_repeats10表示每个特征打乱 10 次取平均次数越多越稳但越慢。置换重要性对特征相关性敏感两个高度相关特征会互相稀释解读时要注意。5. 避坑与排查那些让模型一夜回到解放前的问题5.1 数据泄露未来信息混进训练集现象验证集 AUC 0.95上线后掉到 0.6。原因用了处罚公告年份之后的财务数据或者标准化时用了全量数据的中位数。解决严格按时间切分标准化参数只在训练集上 fit再 transform 验证集。滑窗标签取窗口最后一年不要取中间年。5.2 标签噪声处罚不等于造假现象模型学到的全是「被处罚」特征而不是「造假」特征。原因处罚公告里有些是信息披露违规不是财务造假。解决正样本只保留明确涉及财务造假的处罚类型比如虚增收入、虚减成本其他类型剔除或降权。标签规则文件里加一个fraud_type字段过滤。5.3 过拟合训练集 AUC 0.99验证集 0.7现象训练 loss 一直降验证 loss 先降后升。原因网络太深、样本太少、没加正则。解决减少层数加 Dropout 和 L2 正则早停EarlyStopping设patience5。Bagging 本身也有正则效果基学习器可以适当简单。5.4 类别权重设反正类权重过大导致误报爆炸现象召回率 0.95但精确率 0.05模型把大部分公司都判成造假。原因class_weight设得过于激进或者阈值调得太低。解决权重按反比设就行不要手动加倍。阈值用 F1 最大点不要为了召回牺牲太多精确率。业务上可以接受精确率 0.3 左右再低就没法用了。5.5 随机种子没固定每次跑结果都不一样现象同一份代码跑三次 AUC 差 0.05。原因numpy、tensorflow、sklearn 的随机种子没全设。解决开头统一设np.random.seed(42)、tf.random.set_seed(42)、random.seed(42)Bagging 和 KFold 的random_state也固定。GPU 上还有非确定性算子可以设tf.config.experimental.enable_op_determinism()但会慢一些。6. 进阶技巧把 Bagging 和深度学习串成一条流水线前面几章是分开讲的实际项目里我一般把整个流程封成一个Pipeline从原始财报 CSV 到最终预测概率一条命令跑通。这样做的好处是换数据集时只改配置不动代码逻辑。下面是一个简化版流水线骨架用sklearn.pipeline串起预处理和 Bagging 模型。from sklearn.pipeline import Pipeline from sklearn.base import BaseEstimator, TransformerMixin class FinancialPreprocessor(BaseEstimator, TransformerMixin): def __init__(self, ratio_cols, abs_cols, seq_len3): self.ratio_cols ratio_cols self.abs_cols abs_cols self.seq_len seq_len def fit(self, X, yNone): self.imputer_ SimpleImputer(strategymedian) self.scaler_ RobustScaler() self.imputer_.fit(X[self.ratio_cols]) self.scaler_.fit(X[self.ratio_cols self.abs_cols]) return self def transform(self, X): X X.copy() for col in self.abs_cols: X[col] np.log1p(X[col].clip(lower0)) X[self.ratio_cols] self.imputer_.transform(X[self.ratio_cols]) X[self.ratio_cols self.abs_cols] self.scaler_.transform( X[self.ratio_cols self.abs_cols]) return X pipeline Pipeline([ (prep, FinancialPreprocessor(ratio_cols, abs_cols)), (bagging, bagging) ]) pipeline.fit(train_df, train_labels) probs pipeline.predict_proba(test_df)[:, 1]这个流水线的关键点是fit和transform分离保证标准化参数只在训练集上学。FinancialPreprocessor继承BaseEstimator和TransformerMixin能直接塞进Pipeline。实际部署时把pipeline用joblib.dump存下来线上加载后直接predict_proba。验证方法上我习惯留一个「时间外样本」做最终测试比如用 2021 年数据训练、2022 年测试模拟真实上线场景。如果时间外 AUC 比交叉验证低超过 0.1说明模型过拟合了年份特征得回去检查特征里有没有混入年份相关的东西比如「当年 GDP 增速」这种宏观变量它在训练集和测试集分布不同容易造成虚假相关。还有一个具体技巧Bagging 的基学习器可以异构不必全是 CNN。我试过 5 个 CNN 加 5 个 DNN 加 5 个 XGBoost 混在一起 BaggingAUC 比纯 CNN 高 0.02 左右代价是训练时间翻倍。如果算力有限纯 CNN 加 Bagging 已经够用。异构集成时注意各基学习器的输出概率要校准CNN 和 XGBoost 的概率尺度不一样直接平均会偏向某一方可以用 Platt Scaling 或 Isotonic Regression 先校准再平均。最后说个我踩过的坑有次为了提升效果把财务指标从 30 个扩到 200 个包括各种衍生比率结果验证集 AUC 反而降了 0.03。原因是很多衍生比率高度共线Bagging 的特征采样没能完全解耦噪声被放大了。后来用方差膨胀因子VIF筛掉 VIF 大于 10 的特征降到 60 个左右AUC 才回来。特征不是越多越好财务数据尤其如此每个指标背后都有会计逻辑乱加衍生项不如把原始指标的质量做扎实。希望帮到你。本文还有配套的精品资源点击获取
返回列表