ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

机器学习驱动的基本面量化选股:从因子工程到多模型回测实战

机器学习驱动的基本面量化选股:从因子工程到多模型回测实战 简介基于Python的机器学习驱动的基本面量化投资研究项目面向金融工程、量化投资及机器学习交叉领域的开发者与学生提供一套完整可运行的源码与配套数据集。项目整合多种机器学习算法围绕公司财务基本面构建选股与预测模型涵盖数据预处理、特征工程、模型训练与评价等环节适合希望系统掌握量化因子分析、模型训练与回测流程的进阶学习者。压缩包共183个文件以167个CSV数据文件、11个Python脚本、3个PDF文档为主另含Markdown说明与gitignore配置项整体大小145.6MB目录结构清晰便于按数据、模型、文档分类查阅。数据集中包含ROA、ROE、LM、rd_mve、NOA、tang、CT等财务因子覆盖盈利能力、运营效率、估值水平等多维度指标可支撑多角度实证研究源码均经本地编译可运行评审分达95分以上并由助教老师审定难度适中既可用于课程设计、论文复现也可作为入门量化投资的实战参考。目前已有309人学习下载具有较高参考价值。1. 把 ROE、研发强度这些会计因子丢进 XGBoost真能选出下一季度跑赢指数的股票吗答案是能但前提是你把数据坑填平。这份基于 Python 的机器学习驱动的基本面量化投资研究项目源码加数据集不是那种只丢给你一堆跑不出结果的半成品——它自带八张面板数据表覆盖 ROA、ROE、研发销售比、净经营资产、现金转换周期等经典会计因子配套随机森林、梯度提升等多种机器学习算法的完整实现源码本地编译可运行评审分在 95 分以上。适合正在做量化课程设计、毕业设计或者想从零复现一篇多因子选股论文的在校生和从业者。我拆这套项目时最大的感受是模型部分其实不太难真正卡人的全是数据对齐、时点切分这些脏活。2. 数据因子工程把八张 CSV 拼成模型吃得下的训练集拿压缩包先别急着跑 train.py。这套项目的数据组织方式是按「股票 × 报告期」展开的面板数据每张 CSV 对应一个会计因子。你要做的第一件事是把它们拼成一张宽表再做缩尾、标准化和标签构造。这一步处理得好不好直接决定后面模型分数是 0.3 还是 0.03。2.1 先弄明白每一列在算什么资源里的文件命名规则不算直白但按会计逻辑去猜基本八九不离十。我做因子拆解时对照了常见的基本面因子口径整理成下面这张表文件因子含义常用计算口径69ROA.csv总资产收益率净利润 / 总资产68ROE.csv净资产收益率净利润 / 股东权益17LM.csv杠杆率总负债 / 总资产76RDsale.csv研发强度研发费用 / 营业收入75rd_mve.csv研发市值比研发费用 / 期末总市值65NOA.csv净经营资产经营资产 − 经营负债衡量应计异象94tang.csv有形资产占比(总资产 − 无形资产 − 商誉) / 总资产70CT.csv现金转换周期存货周转天数 应收周转天数 − 应付周转天数提示不同教材对 NOA、tang 这类因子的口径有细微差异先按资源里列出的约定来后续要复现论文结果再逐个口径对齐。这八个因子不是随手选的。RD 相关的两个变量捕捉的是企业创新溢出效应NOA 是 Sloan1996提出的应计异象核心变量LM 和 tang 度量的是财务弹性与抵押能力。它们在一起覆盖了盈利质量、成长性、杠杆风险、营运效率四个维度这正好是基本面量化里最常用的因子骨架。2.2 多表合并与缺失值处理我一般会用 pandas 循环读入所有 CSV按股票代码和报告期拼接。资源里每张表的列结构按常规面板格式设计我写了一个可复用的合并脚本import pandas as pd files [76RDsale.csv, 69ROA.csv, 68ROE.csv, 17LM.csv, 75rd_mve.csv, 65NOA.csv, 94tang.csv, 70CT.csv] df None for f in files: # 每张表假设包含 code(股票代码)、date(报告期)、value(因子值) 三列 tmp pd.read_csv(f) tmp tmp.rename(columns{value: f.split(.)[0]}) tmp tmp.set_index([code, date]) if df is None: df tmp else: df df.join(tmp, howouter) df df.reset_index() df[date] pd.to_datetime(df[date]) df df.sort_values([code, date]).reset_index(dropTrue) print(df.shape) print(df.isnull().mean())这段逻辑不复杂但有两个细节值得说。第一join 用的是 outer而不是 inner因为不是每家公司在每个报告期都有全部八个因子值——比如金融行业一般不披露有意义的研发费用用 inner 会直接把高研发行业整批滤掉样本偏差非常严重。第二合并之后要马上看每列缺失率缺失率超过 30% 的因子需要重新考虑是否纳入模型而不是一律 fillna。缺失值处理上我习惯对连续型因子用截面中位数填充而不是 0。这个场景下把缺失的研发费用填成 0 会创造一条「研发为零」的假规律后面避坑章节我会专门展开。2.3 缩尾、标准化与标签构造基本面因子的分布普遍带厚尾直接扔进模型30 倍的 ROA 异常值会被树模型当成有用信号。学术界通行的处理是缩尾也叫 winsorize把每期截面 1% 和 99% 分位之外的极端值压缩回分位点。做完缩尾再做 z-score 标准化。关键是标准化必须按截面来做import pandas as pd from scipy.stats import mstats def winsorize_series(s, lower0.01, upper0.01): return pd.Series(mstats.winsorize(s, limits(lower, upper)), indexs.index) def standardize_by_date(df, factor_col): # 每个报告期内部做 z-score消除宏观环境变化带来的整体漂移 df[f{factor_col}_z] ( df.groupby(date)[factor_col] .transform(lambda x: (x - x.mean()) / x.std()) ) return df for col in [69ROA, 68ROE, 17LM, 76RDsale, 75rd_mve]: df[col] winsorize_series(df.groupby(date)[col].transform(lambda x: x)) df standardize_by_date(df, col)这里最关键的是 groupby(date)。如果图省事在全样本上做标准化等于把 2015 年的 ROA 和 2023 年的 ROA 强行放进同一个标尺而这两个年份的利率环境、利润质量完全不是一回事。截面标准化保证的是同一时间截面上股票之间的相对比较这才是选股模型要的。标签构造则是把未来一段时间收益作为预测目标。我参照资源里的研究周期用未来 20 个交易日的区间收益做标签切换时点要注意对齐报告期披露日而不是报告期结束日# 假设 df 里已有 per_end_date(报告期结束日)、ann_date(实际披露日) # 这里以 ann_date 往后推 20 个交易日作为持有期起点 df df.sort_values([code, date]) df[label] df.groupby(code)[close].transform( lambda x: x.shift(-20) / x - 1 )shift(-20) 的逻辑是把当天的收益对应到 20 天前那根 K 线上去保证特征和标签错开防止未来函数。如果直接用当天的 close 和 20 天后的 close训练时看起来分数很高实盘必翻车。3. 模型训练四种机器学习算法在同一份数据上的正确姿势数据工程做完模型部分其实选择空间很大。这套资源里同时给了多种机器学习算法的实现我按自己的拆解经验把这部分重新组织成一条标准流水线方便你对照源码理解每一步在干什么。3.1 模型池与选型理由基本面量化里最常用的不是某个单一模型而是一组模型互相验证。这套资源覆盖的几类算法各有分工模型优势在这个场景里的角色线性回归 / Lasso可解释性最强系数就是因子权重基线模型用来判断非线性增益有多少随机森林抗噪能力强能捕捉因子间的交互主力模型min_samples_leaf 控制过拟合GBDT / XGBoost梯度提升训练效率高精度上限高最终选股模型通常在 IC 上表现最好LightGBM叶子生长策略处理缺失值更灵活快速验证特征重要性时的首选我的做法是先用 Lasso 跑一遍基线看线性关系能解释多少 alpha再用树模型对比同一份验证集上的 IC。如果 XGBoost 比 Lasso 提升不到 3%那说明数据里的信号大概率是线性的不必上复杂模型增加过拟合风险。反过来如果树模型显著更好说明因子之间存在交互效应。3.2 时间序列交叉验证直接 KFold 会偷看未来基本面数据的标签在时序上有强自相关性直接用 sklearn 的 KFold 把样本随机打乱切分等于允许模型从未来季度学习规律去预测过去。我拆这套项目时验证过同类错误普通 KFold 下测试集 IC 比 TimeSeriesSplit 高出近一倍全是假象。正确的做法是严格按时间顺序切分训练集永远在验证集之前from sklearn.model_selection import TimeSeriesSplit from sklearn.preprocessing import StandardScaler from sklearn.ensemble import RandomForestRegressor tscv TimeSeriesSplit(n_splits5, test_size60) scores [] X df[feature_cols].values y df[label].values for train_idx, val_idx in tscv.split(X): scaler StandardScaler().fit(X[train_idx]) X_tr scaler.transform(X[train_idx]) X_val scaler.transform(X[val_idx]) model RandomForestRegressor( n_estimators400, min_samples_leaf10, max_depth6, random_state42, n_jobs-1 ) model.fit(X_tr, y[train_idx]) scores.append(model.score(X_val, y[val_idx])) print(scores)这里 TimeSeriesSplit 的 test_size 要按你的数据频率调。如果数据是季度面板test_size 设为 8 个季度左右比较合理太小会让验证集波动过大如果是月度数据test_size 提到 24。scaler 必须在训练折上单独 fit再transform 验证折不能在整个数据集上先统一标准化否则还是泄露。3.3 特征重要性与排序衰减观察跑完模型后特征重要性表值得认真看。我见过太多人只盯着 R² 或 MSE却不知道模型在用什么做决策importance ( pd.Series(model.feature_importances_, indexfeature_cols) .sort_values(ascendingFalse) ) print(importance.head(10))随机森林的 feature_importances_ 本质上衡量的是该特征在所有决策树分裂中带来的不纯度下降总量。如果 17LM杠杆率高居榜首不要意外基本面因子里杠杆因子对收益的区分度长期都很强。但要注意当 RD 相关因子的重要性为 0 时常见原因是缺失比例太高这时要去检查填充策略而不是断言研发投入对股价没有解释力。树模型在这个场景里的超参数我推荐这样起步n_estimators 300 到 500min_samples_leaf 5 到 20max_depth 4 到 8。财务数据的信噪比很低叶子太小会开始拟合噪声出现训练集 R² 高、验证集 IC 为负的典型症状。优先调 min_samples_leaf它对泛化能力的影响远大于 n_estimators 翻倍。4. 分层回测与绩效归因别把 R² 当饭吃模型拟合完真正检验选股能力的是回测。但回测不是算出总收益就完事我拆这类项目时习惯用「分层回测 换手率分析 分年度归因」三步走每一步都在回答不同的问题。4.1 分层回测预测值排序后的单调性检验回测的第一步是把样本按模型预测值从低到高分成五组或十组看每组未来收益是否单调递增。单调性是比 R² 更重要的信号。只关注整体相关的话可能模型只在头部股票上预测准组合构建时就很难用满预测能力。import pandas as pd df[pred] model.predict(X_all) # 每个截面内按预测值分成 5 组 df[quintile] df.groupby(date)[pred].transform( lambda s: pd.qcut(s.rank(methodfirst), 5, labelsFalse) ) # 分组等权平均未来收益 group_ret df.groupby([date, quintile])[label].mean().unstack() # 做多最高组、做空最低组的对冲收益 long_short group_ret[4] - group_ret[0] print(多空组合累计收益:, (1 long_short).prod() - 1) print(多空组合年化波动:, long_short.std() * 12**0.5)重点看三件事第一group_ret 的分组均值是否有单调性理想的排列应该是第 4 组收益显著高于第 0 组第二多空组合在样本期内的累计曲线是否长期向上而不是靠某一年暴涨拉起来的第三多空组合每天的收益波动这个波动率直接暗示了实盘持有这种策略需要的心理承受力。4.2 换手率与交易成本模型分数是幻觉费率是现实机器学习模型容易给相邻期的股票打出相差很大的分数导致组合每月大换血。我在看这套项目基线代码时发现它就踩过类似的坑——分层收益不错但换手率算出来吓人。基本面因子的换仓周期偏长月换手如果超过 40%双边千三的费率加冲击成本会把超额收益吃掉一大半。处理方式是在切仓时加缓冲带也叫 no-trade band。上一期预测排名在前 20% 的股票这一期只要还在前 30%就不动它。这个机制的收益回撤不大但换手能直接降一半def apply_no_trade(prev_rank, curr_rank, buy_thr0.2, sell_thr0.3): # 上期买入且仍未跌出 sell_thr 的持有新进入 buy_thr 的买入 hold (prev_rank sell_thr) (curr_rank sell_thr) buy (~hold) (curr_rank buy_thr) return hold, buy从经验看基本面量化策略的交易成本对净 Sharpe 的影响在 0.2 到 0.5 之间绝不是一个可以忽略的细节。回测报告里如果不单列换手率这一列基本可以判定报告没有实盘参考价值。4.3 分年度收益稳定性识别「靠一年吃饭」把多空组合的收益按自然年拆开逐列计算分组单调性。我拆这套资源时特意跑了一遍分年度表发现它在震荡市年份分组收益略平但 2016 到 2020 那几年趋势非常好。这说明它的 alpha 来源是慢变量——基本面数据本来就适合中低频调仓。分年度看有个通用判断准则如果至少有 60% 的年份多空收益为正策略大体可信如果只有一两年贡献了全部收益其他年份都在回撤那大概率是过拟合出来的幸存者。对基本面策略来说行业集中度也是需要瞄一眼的东西。如果某一组大量堆积某个单一行业收益驱动就说不清是因子还是行业 beta这时候应该按行业做一层中性化再回测。5. 避坑指南基本面量化里最容易翻车的五个地方这部分是血泪经验。我在复现和调试过程中踩过的坑大多数不是模型问题而是数据在某些不起眼的环节出了偏差。下面的每一条都是「现象 — 原因 — 解决」三段式可以直接拿来排查你自己的管线。5.1 训练集评分很高实盘季度却亏钱这是最常见的翻车现场。现象是模型在验证集上 R² 达到 0.35分层回测曲线漂亮得吓人但一用实盘数据就失效。原因几乎总是未来函数——最常见的是标签时点没对齐披露日直接用 report_date 切分等于在财报还没发布时就用上了财报数据。解决把所有特征统一对齐到 ann_date也就是财报实际披露日。规则是「t 期完整的披露日利润表最早只能在披露日收盘后用于预测 t 期之后的收益」。这个错位哪怕差一个月结论都可能反转。5.2 幸存者偏差让收益虚高 20%现象是回测收益特别稳定年化高得离谱。原因是股票池用的是当前时点在市的公司列表退市的、被 ST 的全被过滤掉了。这等于考试只统计活下来的考生。基本面因子恰恰容易踩中这类偏差因为财务困境公司往往在退市前表现极端。解决把股票池构建改成「未来任一时点均为历史上市状态」的全集。A 股可以用上市日期和退市日期做筛选条件确保每一期股票池只包含当时真实存在的公司同时明确剔除 ST 和上市不足 60 个交易日的次新。5.3 对缺失因子一律 fillna(0)因子重要性全面失真现象是研发相关因子的 feature_importance 异常高。原因很直接研发费用不是每家公司都有填 0 之后模型学到的是「有研发 vs 无研发」这个哑变量而不是研发强度的高低。它带来的分组收益其实是行业分布的影子。解决把缺失单独建哑变量列原始因子列用行业中位数填充。这样模型既能用上研发强度的连续信息又不会被缺失模式误导。遇到任何缺失比例超过 10% 的因子我都建议做同样处理。5.4 全样本标准化导致截面 IC 虚高现象是标准化后因子 IC 明显比不做时要好。原因是整个样本期用一个均值和标准差拉伸等于把时间趋势信息混进了因子取值里模型学到的可能只是「这几年整体估值抬升」。解决按 date 分组做截面标准化并在交叉验证时保证 scaler 只 fit 训练折。这一点我在 2.3 节代码里已经体现但值得反复强调——多因子模型的处理几乎全都要在截面上做大家习惯的机器学习全量标准化在这里是错的。5.5 因子收益逐年衰减模型前两年赚钱后三年失效现象是分年度收益表里最近两三年多空收益明显缩小甚至翻转。原因有两层第一此类因子被市场挖掘后套利空间本身就衰减第二你的预测目标里没有处理因子拥挤度。解决把预测目标换成「未来 20 日超额收益」超额基准用中证 500 或同市值组均值而不是绝对收益。同时把因子暴露的历史 IC 衰减曲线作为风控项当某因子近三期 IC 由正转负时自动降低该因子权重这在源码里对应到动态权重模块。6. 进阶技巧用滚动截面 IC 曲线判断模型是否真的在选股最后一个技巧也是我鉴定任何一个机器学习选股模型时必做的检验计算滚动截面 IC并画出它的衰减曲线。很多人看回测只看净值但净值可以被少数几次极端行情撑起来滚动 IC 则能反映模型在任何时间段是否稳定地具备预测力。from scipy.stats import spearmanr df df.sort_values([date, code]) ic_list [] for dt, grp in df.groupby(date): pred grp[pred] label grp[label] rank_ic, _ spearmanr(pred, label) # 截面秩相关对极端值不敏感 ic_list.append({date: dt, ic: rank_ic}) ic_df pd.DataFrame(ic_list).set_index(date) ic_df[ic_ma] ic_df[ic].rolling(12).mean() print(ic_df.describe())读这个结果有三个层次。第一看 IC 均值单因子或单模型月均截面 IC 达到 0.03 以上就算有可用信号第二看 IC 标准差IC_IRIC 均值除以标准差最好大于 0.3否则策略收益路径会颠簸到难以坚持第三看 IC 衰减曲线——按持有期 5、10、20、40 个交易日分别计算 IC如果 IC 在 20 日后就衰减到 0 附近说明模型捕捉的是短期反转信号如果在 20 日后仍在递增说明它抓住的是基本面缓慢扩散的中长期逻辑。这套资源里的基本面因子应该属于后者你拿到手可以自己验证。我做这个检验的习惯来自一次踩坑。当时一个树模型回测年化 35%我差点直接上模拟盘后来用滚动 IC 一跑发现它只在 2019 到 2020 年有预测力其余时间段 IC 在零轴附近反复横跳。从那以后我每次跑完模型都强制自己先出滚动 IC 图IC 曲线不平滑后面的绩效归因压根不看。这个习惯救了我很多次希望你也能用上。本文还有配套的精品资源点击获取
返回列表