ARTICLE DETAIL

资讯详情

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

数据挖掘实战:A股高送转预测的样本定义、特征工程与模型选型

数据挖掘实战:A股高送转预测的样本定义、特征工程与模型选型 简介基于数据挖掘的上市公司高送转预测项目面向金融数据分析与量化建模学习者提供完整Python源码与配套文档说明。资源围绕“因子筛选—模型构建—高送转预测”主线综合运用经济学逻辑与数理统计方法挑选重要因子并基于决策树、随机森林、XGBoost及Stacking集成模型完成预测可直接用于复现第8年上市公司高送转结果。压缩包共15个文件约79KB主要包含3个Python脚本主程序、特征工程、Stacking集成、5个CSV特征重要性结果、1个PNG可视化图、1个Markdown说明文档及依赖环境文件目录划分清晰便于按模块学习和二次开发。目前已有73人学习下载适合正在做数据挖掘课程设计、毕业设计或金融预测竞赛的读者参考借鉴。通过该资源可掌握特征选择、多模型对比与集成学习的完整实现思路是一份实用性强的高分项目模板。1. 高送转预测不只是看公告数据挖掘能把不确定性变成概率每年三四月份总有一批股票在披露高送转预案前提前走强等公告落地再冲进去的散户基本接在情绪高点。高送转并不是随机事件它的“弹药”就写在每股资本公积、未分配利润、总股本这些财务数据里只是普通投资者没有精力把几千家公司的数据算一遍。基于数据挖掘的上市公司高送转预测做的就是这件事把“这家公司会不会高送转”从拍脑袋的问题转成一个有特征、有标签、可重复计算的概率模型。这篇文章按数据挖掘项目的完整链路来讲从样本定义、特征工程到模型选型、源码交付和踩坑点适合课程设计、毕业设计、以及量化选股初筛的开发者直接照着复现。2. 先把数据和标签理清楚高送转预测的数据集、样本定义与特征工程做数据挖掘项目最怕一上来就跑模型。高送转预测里“什么是高送转”“样本按哪一年对齐”“正样本占多少”这三个问题不先定下来后面所有指标都是空中楼阁。这一章先把数据侧的地基打牢。2.1 正负样本怎么定义10送转5是分界线时间窗口必须错开一期A股对“高送转”没有唯一的法定标准常见做法是以“每10股送转合计”来划分送股和转增都计入合计大于等于5股算高送转更严格的项目可以取10股作为阈值。阈值不同正样本占比差异很大5股口径下正样本通常占全市场的5%到8%10股口径会降到2%到3%。对课程设计来说5股口径更容易做出可解释的结果。样本对齐是关键中的关键。要用第T年的财务数据去预测T1年年报披露期前后公告的高送转两者必须错开一年。很多初学者把当年实施高送转的公司直接和当年财报匹配模型还没开始就已经“偷看答案”了。import pandas as pd # 公告数据每家公司每年实施的高送转方案 ann pd.read_csv(high_send_announcements.csv, parse_dates[implement_date]) ann[impl_year] ann[implement_date].dt.year # 送股与转增合计作为“每10股送转”口径 ann[send_per_10] ann[bonus_share_per_10] ann[capital_reserve_per_10] # 阈值5 记为正样本代表“这家公司当年做了高送转” ann[label] (ann[send_per_10] 5).astype(int) # 错位一期实施年份减1得到需要用哪一年财报去预测 label_df ann[[stock_code, impl_year, label]].rename( columns{impl_year: feat_year} ) label_df[feat_year] label_df[feat_year] - 1这段代码的作用是把“实施年份”转换成“特征年份”。最终训练用的宽表是用 stock_code feat_year 关联财报特征用 label 当作目标。这样构造的样本在时间上天然满足“用过去预测未来”的逻辑。需要注意如果数据源里公告日期覆盖不全有些公司的送转方案会在同一年内公告但跨年实施此时建议一律以公告日期所在年份为准避免重复计数。2.2 特征清单每股资本公积、每股未分配利润、股价和总股本为什么是主力高送转的本质是上市公司把账面上的资本公积和未分配利润转成股本不需要掏真金白银。因此特征选取的逻辑很直接公司手里有没有“存货”愿不愿意分以及分了之后股本会变成什么样子。下面这张表是这类项目里最常用的特征集合不在多而在口径对。特征计算口径方向为什么有用每股资本公积资本公积/总股本正向转增股本的直接来源每股未分配利润未分配利润/总股本正向送股的利润基础总股本报告期末总股本负向大股本高送转动力弱每股股价报告期末收盘价正向高股价有拆股动机基本每股收益归母净利润/加权股本正向盈利能力强才有底气净利润同比增速(本期-上年同期)/上年同期正向成长型公司更愿意送转上市年限当前年份-上市年份负向次新股高送转倾向更明显构造特征时财务字段要从合并报表口径取尤其注意“资本公积”要区分母公司报表和合并报表。同一个字段如果换了数据源口径经常不一致后面避坑章会专门展开。# 财务特征宽表按股票代码报告期合并 fin pd.read_csv(financial_factors.csv) fin[feat_year] pd.to_datetime(fin[report_date]).dt.year feat fin[[stock_code, feat_year, total_share, per_capital_reserve, per_undistributed_profit, close_price, eps, profit_yoy, list_year]].copy() # 构造复合特征股本扩张空间 feat[reserve_per_share_sq] feat[per_capital_reserve] ** 2 feat[list_age] feat[feat_year] - feat[list_year]复合特征“每股资本公积的平方”看起来有点拍脑袋但它实际上是在模拟“资本公积充裕程度加速影响决策”的非线性关系。树模型不一定需要这种手工构造但逻辑回归需要因为线性模型无法自行捕捉交互项。这也是为什么特征工程不能省模型越简单特征越要替模型把逻辑想清楚。2.3 特征清洗与样本划分分位截断、行业中位数填充按年份切分不随机打乱清洗这一步决定了逻辑回归能不能快速收敛。财务数据天然带极端值比如某一年某公司资本公积突然暴增如果不处理一个样本就能把整个逻辑回归的系数权重拉偏。常用的办法是分位数截断加标准化。缺失值则不建议直接补0财务字段缺失往往意味着“数据源没有覆盖”而不是“值为0”用行业中位数填充更接近真实分布。import numpy as np def winsorize(s, lower0.01, upper0.99): q_low, q_high s.quantile(lower), s.quantile(upper) return s.clip(q_low, q_high) feat[per_capital_reserve] winsorize(feat[per_capital_reserve]) feat[per_undistributed_profit] winsorize(feat[per_undistributed_profit]) feat[total_share] winsorize(feat[total_share]) # 缺失值按行业分组填充更贴近同类公司水平 feat[per_capital_reserve] feat.groupby(industry)[ per_capital_reserve].transform(lambda x: x.fillna(x.median()))样本划分必须按年份切不要用 train_test_split 默认的随机切分。原因很直接这个项目的预测对象是未来年份如果用随机切分同一家公司在训练集和测试集同时出现模型会“记住”这家公司的偏好测试集分数虚高拿到真实年份上去预测立刻露馅。正确做法是简单粗暴的按年份切比如2015到2019做训练2020和2021做验证。train feat[feat[feat_year].between(2015, 2019)].copy() test feat[feat[feat_year].between(2020, 2021)].copy() X_train train.drop(columns[stock_code, feat_year, label, industry]) y_train train[label] X_test test.drop(columns[stock_code, feat_year, label, industry]) y_test test[label]按年份切分之后要检查一个东西训练集和测试集的正样本比例。如果2020年市场整体高送转比例骤降模型学到的先验概率和预测期不一致需要单独记录并在最终结果里按年份分开报告而不是混在一起看一个总准确率。3. 两种靠谱模型落地逻辑回归跑通全链路XGBoost优化不平衡样本模型选型不必追求花哨。在这个项目里第一版能跑、能解释、能交文档比涨0.5%的AUC重要得多。因此路线是先逻辑回归把全链路跑通再上XGBoost处理非线性因素和样本不平衡。3.1 用逻辑回归跑通最小流程class_weight怎么调概率才能当排序分数用逻辑回归在这个场景的价值有两点一是特征贡献度直接看系数写文档时能明确说出“每股资本公积每提高一个标准差高送转概率提升多少”二是 predict_proba 输出的概率天然适合做排序而不是硬切一个阈值。缺点是它假设特征和标签之间的关系是线性的所以前面举的“每股资本公积平方”这类手工特征就派上了用场。from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_s scaler.fit_transform(X_train) X_test_s scaler.transform(X_test) # class_weightbalanced让少数类样本获得更高权重 # C正则化强度默认1.0过拟合时降到0.1 model LogisticRegression(class_weightbalanced, C1.0, max_iter1000) model.fit(X_train_s, y_train) # 输出正样本概率用于后续排序 prob model.predict_proba(X_test_s)[:, 1] coefficients pd.Series(model.coef_[0], indexX_train.columns)class_weight 是这里最容易忽略的参数。正样本只有6%时逻辑回归的默认目标是“让整体损失最小”它会倾向于把所有样本都预测成负类因为哪怕全判负准确率也有94%。加上 balanced 之后模型会在损失函数里按类别频率放大少数类的错误代价训练出的概率才能把真正的高送转公司排到前面去。C值则控制正则化强度如果训练集AUC高、验证集AUC低优先把C从1.0往0.1调。3.2 换XGBoost提升分数scale_pos_weight和max_depth先调这两个逻辑回归跑通之后换XGBoost是这类项目最自然的进阶路径。高送转决策里存在明显的交互效应比如“每股资本公积高但股价很低”和“每股资本公积高且股价也高”可能是两种完全不同的送转逻辑线性模型抓不住这种组合树模型可以。XGBoost参数很多但对这个场景真正敏感的就是少数几个。from xgboost import XGBClassifier xgb XGBClassifier( n_estimators300, max_depth4, learning_rate0.1, scale_pos_weight12, # 负样本数/正样本数先粗估 eval_metricauc, use_label_encoderFalse, subsample0.9, # 小幅防过拟合 colsample_bytree0.9 ) xgb.fit( X_train, y_train, eval_set[(X_test, y_test)], verboseFalse )scale_pos_weight 是处理不平衡样本的核心旋钮直接取“负样本数除以正样本数”作为起点。比如训练集里负样本9400条、正样本600条这个值就是15左右然后根据验证集AUC上下微调。max_depth 在这个数据规模下不要超过6财务表格特征少、样本量也就一两万条深度过大会把训练集背下来真实年份预测立刻崩。learning_rate 固定0.1是稳妥选择如果追求极致可以降到0.05并增大n_estimators但对课程设计来说收益有限。3.3 评估别只盯准确率用召回率、精确率和AUC轮流看不平衡分类里准确率是最大的陷阱。模型全预测“不高送转”准确率轻松过90%但这个模型毫无用处。真正要关心的是三个东西AUC看整体排序能力召回率看“真实高送转的公司我抓住了多少”精确率看“预测名单里有多少是真货”。更有操作性的评估方式是放弃0.5阈值直接按概率取前20%作为候选名单然后计算这份名单的召回率和精确率。from sklearn.metrics import roc_auc_score def evaluate_top_ratio(label, prob, ratio0.2): df pd.DataFrame({label: label, prob: prob}) df df.sort_values(prob, ascendingFalse) top_n int(len(df) * ratio) top_df df.head(top_n) recall top_df[label].sum() / max(df[label].sum(), 1) precision top_df[label].mean() return { top_ratio: ratio, recall: recall, precision: precision, auc: roc_auc_score(label, prob) } metrics evaluate_top_ratio(y_test, prob, ratio0.2)这份输出要怎么看如果整体正样本比例是6%随机抽取20%名单时精确率期望也是6%左右。模型给出的精确率如果只有7%说明和瞎猜差不多如果能到15%到20%意味着预测名单里的高送转密度是随机抽样的2到3倍这才叫有业务价值。AUC低于0.7时也不用急着调参先回头查特征对齐通常是标签或口径问题。4. 交付一个能跑的项目源码目录结构、配置文件与文档说明怎么写这个标题带“源码文档说明”所以项目能不能拿高分代码组织方式比模型分数更关键。一份杂乱无章的大脚本放到真实业务里是不可维护的要在结构上体现出工程意识。4.1 源码目录至少分四层数据预处理、特征、训练、预测分开最稳妥的目录结构是数据、源码、文档三件套分层。数据预处理、特征工程、训练、预测各自独立成脚本谁也不会把谁的变量改乱评审也能一眼看出流程顺序。high_send_predict/ ├── config.py # 所有可调参数集中管理 ├── data/ │ ├── raw/ # 原始财务与公告数据 │ └── processed/ # 清洗后的特征宽表 ├── src/ │ ├── data_preprocess.py # 数据加载、缺失值、去极值 │ ├── feature_engineering.py # 特征构造与筛选 │ ├── train.py # 模型训练与参数调优 │ └── predict.py # 加载模型输出预测名单 ├── docs/ │ └── 项目文档说明.md └── requirements.txt这个结构不用太复杂四层就够了。很多人写课程设计喜欢一个 .py 从头写到尾后面改一个字段要全局搜索输出结果也难追溯。拆开之后任何一步想重跑都只动对应脚本。data_preprocess.py 的输出是“干净的宽表”feature_engineering.py 的输入是宽表、输出是“带特征和标签的建模表”train.py 只管读入建模表、产出模型文件predict.py 最后负责把预测名单导出成 CSV。各层通过 CSV 或 pickle 文件通信不要脚本之间互相 import 函数否则依赖关系乱掉之后没人敢改代码。4.2 把参数集中到config里换年份、换阈值只改配置不动代码参数散落在各个脚本里是这类项目最影响维护体验的问题。阈值、年份、名单比例这些变量今天在这里改一个明天在那里改一个实验结果就对不上了。把所有可能变化的量集中到 config.py 里实验才能复现。# config.py DATA_PATH data/raw OUTPUT_PATH data/processed MODEL_SAVE_PATH models/xgb_model.json SEND_THRESHOLD 5.0 # 每10股送转多少才算高送转 TRAIN_START_YEAR 2015 TRAIN_END_YEAR 2019 TEST_YEARS [2020, 2021] # 时间外验证年份 FEATURE_COLS [ per_capital_reserve, per_undistributed_profit, total_share, close_price, eps, profit_yoy, reserve_per_share_sq, list_age ] MODEL_PARAMS { n_estimators: 300, max_depth: 4, learning_rate: 0.1, scale_pos_weight: 12, subsample: 0.9, colsample_bytree: 0.9, eval_metric: auc } TOP_RATIO 0.2 # 最终预测名单取前20%实际改起来很方便想验证“10送转10”口径下模型表现只需要把 SEND_THRESHOLD 改成 10其他脚本一概不动。写文档时也只需要复制这份 config 放在附录里评审看到的第一印象就是“可复现”。4.3 文档说明的两个硬要求字段口径表加从零复现步骤“文档说明”是高分项目的临门一脚。常见问题是文档把模型原理抄了三大页真正关键的字段口径表和复现步骤却一笔带过。一份文档写得及不及格可以看两条标准数据集的每个字段是不是都有明确口径说明一个完全没接触过该项目的人能否照着文档把整个流程跑完。字段口径表是必须有的。至少包含字段名、来源、表类型、含义、口径说明、缺失处理方式。比如“资本公积”要写清楚是母公司口径还是合并报表口径单位是元还是万元来自哪张报表、哪个报告期。这个表的价值在排错时立刻体现因为很多数据问题不是代码问题而是口径理解不一致。复现步骤部分我一般会这样写“下载或导出一份原始数据到 data/raw运行 python src/data_preprocess.py再依次运行 feature_engineering.py、train.py、predict.py最后查看 output 目录下的预测名单 CSV。”每步一行命令不额外解释环境安装因为这是前置常识。写完文档之后自己照着从头敲一遍第三遍还不用翻代码文档就合格了。5. 五个高频踩坑记录特征泄漏口径混乱别让预测死在测试集上这部分是没有现成数据看不出来的深坑也是这类项目里最容易反复翻车的地方。每一条都是真实项目里会被打回重做的级别提前避开后面省很多事。5.1 用实施当年的报表预测AUC虚高到0.99现象训练集AUC接近0.99测试集AUC掉到0.6以下差异大得离谱。原因把高送转实施当年的财务数据也放进了特征表相当于模型已经提前看到了结果。解决严格把特征年份设置为实施年份减1并且在合并特征时用财务报告期小于预测日开始时间做过滤条件不要只靠字段名里的年份。5.2 正样本只占6%准确率90%的模型却是废的现象模型在测试集上准确率92%预测名单里却几乎没有高送转公司。原因整体正样本比例太低模型通过“全预测否”就能拿到很高准确率损失函数里没有对少数类做惩罚。解决在逻辑回归里加 class_weightbalanced在XGBoost里设置 scale_pos_weight评估指标改成AUC和top20%召回率不再看准确率。5.3 相同字段换一个数据源分布差十倍现象同一个“每股资本公积”字段从A数据源下载和B数据源下载数值范围和分位数完全不同。原因有的数据源给的是母公司资本公积除总股本有的给的是合并报表有的还给的是期末值有的用期初值。解决拿到新数据先做分位数检查和抽样核对随机挑10家公司手工算一遍。口径问题在项目初期花一小时确认比最后返工强得多。5.4 ST股和次新股混进样本模型学了一堆“僵尸公司”规律现象重要特征的系数方向不合理比如上市年限越短、越不可能高送转和常识相反。原因样本里包含了大量ST股这类公司资本公积异常但根本没有送转能力刚上市不足一年的新股又有很强的送转冲动单独成簇。解决统一剔除ST状态股票、剔除上市不满244个交易日的次新样本以及那些长期停牌导致财务数据异常的公司。5.5 阈值选了0.5预测名单里一个高送转都抓不到现象模型输出的正样本概率主要集中在0.05到0.3之间拿0.5作阈值一个正样本都没有。原因不平衡数据集上模型学到的先验概率本身就很低0.5是虚构出来的“自然分界线”不适合这个场景。解决用top分位作为名单标准。取概率最高的20%作为候选再用每股资本公积大于1、每股未分配利润大于0等业务规则做二次筛选名单质量会明显提升。6. 用一个回溯验证让预测结果可信而不是只贴出准确率模型训练完之后最容易被追问的一个问题是你这个模型放到过去的年份里能选出真正高送转的公司吗与其只放一个测试集AUC不如做一个按年份滚动的回溯验证这也是工作中验证预测类模型最常用的做法。6.1 滚动时间外验证每年重新训练一次把数据按年份滚动切分每一轮只用当年之前的数据训练预测当年然后记录当年的名单表现。这样做能真实模拟“站在历史时点做预测”的信息约束也最容易发现特征泄漏。for test_year in [2019, 2020, 2021]: tr feat[feat[feat_year] test_year] te feat[feat[feat_year] test_year] # 训练模型、预测概率复用前面代码 metrics evaluate_top_ratio(te[label], xgb.predict_proba(te[FEATURE_COLS])[:, 1]) print(test_year, metrics)6.2 用覆盖率判断名单质量而不是纠结单年涨跌每年输出三行指标基准发生率、top20%名单覆盖率、提升倍数。基准发生率是当年全市场真正高送转的公司比例top20%覆盖率的含义是“概率最高的20%名单里真实高送转公司占多少”。提升倍数如果稳定在1.5倍以上说明模型在时间外表现能超过随机水平模型才值得对外讲结果。我自己做这类项目时吃过一个亏第一版模型在测试集上AUC做到0.78以为没问题了结果滚动验证时发现只有第一年有效后面两年和随机抽签差不多。原因就是训练数据只覆盖了一个特殊年份。后来固定按三年滚动验证每一年单独看指标才真正把模型调稳。如果你打算把预测结果落到“选股策略”上还可以在回溯名单上做更简单的落地验证持有预测名单前20%的公司60个交易日对比同期沪深300收益。不需要做成复杂的量化回测一个累计收益曲线就足够说明模型是否具备参考价值。把这三件事放进项目文档的最终章节这个“源码加文档说明”的高分项目就站得住脚了。希望这些思路帮你在复现时少走一段弯路。本文还有配套的精品资源点击获取
返回列表