颜力刚Ligang Yan

量化选股学习笔记(一):从 AUC 0.76 到“避开亏损公司”

用 5 年美股年报数据学量化选股:为什么随机划分会骗人、为什么不该预测“涨不涨”、40 个单因子测下来,几乎所有有效信号都在讲同一件事。

quantmachine-learningfactor-investingpython

这是我学习量化选股的第一篇笔记,记录从一个看起来很好的结果出发,一步步发现问题、再把方法做对的过程。代码由 Claude Code 编写,我负责提问、读代码和决定方向。

数据是 Kaggle 上公开的「200+ Financial Indicators of US Stocks (2014–2018)」:5 个年份文件,每个文件是当年美股公司年报里的约 220 个财务指标,加上下一年的股价涨跌幅。清洗后一共 19,754 行(公司 × 年份)。

本文是学习记录,不构成任何投资建议。

起点:一个“76% 准”的模型

一开始我做的是课程作业:用 20 个财务比率预测公司属于哪个行业。传统机器学习里最好的是 HistGradientBoosting,测试集 macro F1 0.645。我还试了深度学习(MLP、ResNet、FT-Transformer),结果全部输给树模型,最好的神经网络只有 0.606。这和学界的结论一致:在中小规模的表格数据上,梯度提升树通常仍然最强。

然后我把同一套方法换了个目标:用 2018 年的年报,预测股价在 2019 年是涨还是跌。随机森林在测试集上的 ROC-AUC 达到 0.764,把测试股票按模型打分分成 5 组,最被看好的一组中位数涨了 26%,最不被看好的一组跌了 11%。

对股价预测来说,这个结果好得可疑。后面所有的工作,都是在回答一个问题:这个 0.76 有多少是真的?

第一课:先看数据,市场情绪决定了大部分涨跌

把 5 年的数据都拉下来之后,第一个发现就推翻了原来的做法:

年报年份 → 涨跌年份 上涨股票比例 中位数涨幅
2014 → 2015 43% −5.5%
2015 → 2016 70% +18.0%
2016 → 2017 64% +11.5%
2017 → 2018 28% −16.3%
2018 → 2019 68% +17.8%

每年上涨股票的比例和中位数涨幅

上涨比例在 28% 到 70% 之间摆动。这是整个市场在动,不是哪一家公司的年报能决定的。原来的模型只见过 2018 → 2019 这一个牛市年。

第二个发现:同一批公司年年都在,3,542 家公司 5 年都出现。公司的财务指标年年几乎不变(营收的年度排名相关系数 0.99,毛利率 0.96),但股价涨跌没有延续性(0.05)。

第二课:怎么划分数据,决定了分数的意义

我用同一个模型、同一个目标,只改变训练集和测试集的划分方式:

  • 随机划分:把 5 年混在一起,随机抽 20% 做测试。
  • Walk-forward(向前滚动):每次只用更早的年份训练,预测下一年。比如用 2014–2016 年训练,测 2017 年。这才是真实的使用方式:你永远只能用过去预测未来。
测试年份 随机划分 Walk-forward 虚高了多少
2015 → 2016 0.693 0.564 +0.129
2016 → 2017 0.641 0.600 +0.041
2017 → 2018 0.603 0.553 +0.050
2018 → 2019 0.755 0.720 +0.035

同一个模型、同样的测试年份,只有划分方式不同

两个结论:

  1. 随机划分会泄漏。 同一个测试年份,随机划分的分数每年都更高。原因是训练集里已经有测试那一年 80% 的公司,模型提前知道了“那一年什么样的股票赢了”。这不需要同一家公司出现在两边,只要是同一年就够了,所以按公司划分也去不掉这种泄漏。
  2. 只测一年会误导人。 诚实的 walk-forward 分数在 0.55 到 0.72 之间,平均只有 0.61。原来的 0.76 恰好测在了最容易预测的一年。必须报告每一个测试年份,而不是一个数字。

还有一个更深的问题:模型判断不了“今年有多少股票会涨”。2017 → 2018 年,模型估计 59% 的股票会涨,实际只有 28%;那一年它的准确率是 43%,而“永远猜跌”能有 72%。但它的 ROC-AUC 每年都高于 0.5。它的排序有信息,涨跌判断没有。

第三课:不要问“涨不涨”,要问“谁比谁好”

这让我明白了量化选股和“预测股价”的区别:

  • 时序预测:这一只股票明天涨还是跌?价格接近随机游走,非常难。
  • 横截面预测:在同一个时间点,这么多股票里哪些会比别的好?这是量化选股做的事,学界几十年的研究表明这里存在可以利用的规律(因子)。

衡量横截面信号的标准指标是 Rank IC:每一期里,模型打分和实际收益的排名相关系数。它和 ROC-AUC 的区别用一个例子就能说清楚:

股票 模型打分 实际涨跌
A 0.9 +30%
B 0.7 +5%
C 0.6 −8%
D 0.4 +2%
E 0.2 −25%

这时 ROC-AUC 是 0.833,Rank IC 是 0.9。如果把 A 和 D 的打分对调(模型把只涨了 2% 的排第一,把涨了 30% 的排第四),ROC-AUC 不变,还是 0.833,因为 A 和 D 都是“涨了”;但 Rank IC 掉到 0.3。ROC-AUC 只问“涨的是不是排在跌的前面”,Rank IC 问的是“收益越高的是不是排得越靠前”,而后者才决定一个组合能赚多少钱。

评价一个因子时,我每年算一个 Rank IC,然后看两个数:均值(有多强)和 ICIR = 均值 ÷ 标准差(有多稳定)。

第四课:40 个单因子,其实在讲同一个故事

接下来我先不用任何模型,把每个指标单独当作一个“因子”来检验:只按这一个数字给股票排序,靠不靠谱?一共测了 40 个,分 5 类:

类别 例子 学界出处
价值 盈利收益率、账面市值比、EBITDA/EV、净派息收益率 Basu (1977)、Fama-French (1992)
质量 ROA、ROE、毛利/总资产、应计项目、Piotroski F-score Novy-Marx (2013)、Sloan (1996)
投资成长 资产增长率、股票发行、销售增长、研发强度 Cooper 等 (2008)、Pontiff-Woodgate (2008)
规模与安全 市值、杠杆、利息保障倍数、Altman Z-score Banz (1981)、Altman (1968)
动量 12 个月动量、行业动量、长期反转 Jegadeesh-Titman (1993)

一个原则:每个因子的方向都事先按经济学逻辑定好,比如“规模越小越好”“负债越少越好”,绝不看了结果再改。如果数据显示和理论相反,就诚实地记为“理论错了”。

40 个因子每年的 Rank IC 和 ICIR

最初看起来有很多有效的因子:ROA、ROE、净利率、盈利收益率、股票发行……IC 都在 0.10 到 0.17 之间,5 年全对。但我用了一个后来变成常规检查的方法,把样本拆开再看,结论就完全变了。

我测了一个最简单的“因子”:这家公司赚钱吗?赚钱记 1,亏损记 0,没有任何其他信息。 它的 Rank IC 是 +0.169,5 年全对,和最好的因子一样强。赚钱公司下一年的中位数收益是 +7.4%,亏损公司是 −8.4%。

然后只在赚钱的公司内部重新算 IC:

因子 全部公司 只看赚钱的公司
ROA +0.150 −0.017
ROE +0.155 +0.009
盈利收益率 +0.156 +0.007

在已经赚钱的公司之间,多赚一点还是少赚一点,几乎没有区别。这些因子的效果主要来自“把亏损公司排到后面”。

更有意思的是,很多看起来属于其他类别的信号,拆开看也是在识别亏损公司:

看起来是 实际上是
价值:账面市值比无效 按账面看“最便宜”的 20% 里,40% 是亏损公司(全体是 34%);按盈利收益率选的最便宜 20% 里只有 1%
投资:增发越多越差 增发最多的 20% 里,66% 是亏损公司:它们靠不断增发续命
规模:大公司反而跑赢 市值最小的 20% 里,64% 是亏损公司,中位数收益 −6.3%;最大的一组是 +10.0%
质量:应计项目每年都和理论相反 应计项目“最好”的 20% 里 76% 是亏损公司:巨额资产减值让利润远低于现金流
动量:长期反转不反转 两年前跌得最多的公司里 70% 在亏损,它们之后继续跌

第五课:在赚钱公司内部仍然有效的,是现金流和财务稳健

真正在“避开亏损公司”之外提供了额外信息的因子只有几个:

因子 赚钱公司内 IC
自由现金流率 +0.089
Altman Z-score(破产风险评分) +0.080
净利率 +0.051
利息保障倍数 +0.051

Altman Z-score 还验证了学界的“困境之谜”:按“风险越高、补偿越高”的理论,破产风险高的股票收益应该更高,但实际上是越安全的表现越好。

第六课:信号几乎全部来自最差的那 20%

把每个因子每年分成 5 组,看每组的中位数收益(减去当年全部股票的中位数):

因子 最差 20% 第 2 组 第 3 组 第 4 组 最好 20%
ROA −21.6 +0.2 +3.3 +3.0 +3.1
盈利收益率 −20.9 −3.4 +3.7 +4.5 +2.7
自由现金流率 −10.9 −7.6 +0.4 +4.8 +5.5

8 个代表性因子的五分组收益

收益差超过 10 个百分点的 15 个因子,全部是最差一组大跌、其余四组差别不大。这些因子擅长找出输家,不擅长挑出赢家。只有自由现金流率从中间往上还在继续上升。

对只能买、不能卖空的普通投资者来说,这意味着:避开最差的 20%,就能拿到大部分好处。

第七课:40 个因子,大约只有 8 个独立的故事

计算 40 个因子之间的相关性再聚类:最大的一类有 16 个因子(盈利能力、含利润的价值因子、规模、股票发行、派息收益率),其中 5 个盈利指标彼此的平均相关系数高达 0.81。

40 个因子之间的相关性,按聚类排序

右下角那一大块深蓝,就是“盈利 vs 亏损”这一个故事。主动管理基本定律说 IR ≈ IC × √breadth,这里的 breadth 指的是独立的赌注数,不是因子的个数。再加第 6 个盈利指标,几乎不会带来新信息。

第二个有信号的故事是“财务安全”。自由现金流率和 Altman Z-score 的相关系数只有 0.27,两者都在赚钱公司内部有效,是下一步做多因子组合最有希望的一对。

和学界的对照

一致的:

  • 账面市值比和资产增长率(Fama-French 的 HML 和 CMA 因子的基础)在 2010 年代减弱,这几年正是价值投资“失去的十年”。
  • 应计效应在 2000 年后消失:在赚钱公司内部,它的 IC 是 −0.009。
  • 困境之谜成立;净派息收益率好于只看股息率。

不一致的:

  • 规模效应反过来了。但这和 Asness 等人 (2018) 的论文《Size Matters, If You Control Your Junk》一致:小公司里的“垃圾股”拖累了规模效应。
  • 动量很弱。部分原因是数据只有年度频率,做不了标准的“12-1 动量”(跳过最近一个月、每月重新排序);另外 2016 年正好发生了一次典型的动量崩溃:2015 年跌得最多的股票,2016 年中位数涨了 19.1%,涨得最多的只涨了 10.9%。

踩过的坑

这些问题不处理,结论会悄悄出错:

  1. 比率陷阱:去年亏损时“增长率”没有意义;净利润为负时,“现金流 ÷ 净利润”会反过来,亏损又烧钱的公司反而显得“质量很好”。解决办法是用总资产做分母。
  2. 缺失值不是“不及格”:在 pandas 里和 NaN 比较的结果是 False。算 F-score 这种“通过几项检查”的分数时,如果直接把它当 0 分,会冤枉数据缺失的公司。
  3. 符号约定:现金流量表里分红、回购、资本开支都是负数(现金流出)。
  4. 数据提供方算好的比率不一定对:数据里的 ROA 列和“净利润 ÷ 总资产”只有 1.4% 吻合,EV/EBITDA 只有 50% 吻合,所以全部用原始数字自己算。
  5. 数据错误:有 16 行“下一年涨幅”超过 +10,000%,最大的是 +2,418,601%。所以比较收益时一律用中位数。
  6. 并列值:52% 的公司不分红。最初分组时按文件行顺序把这些并列的公司拆到不同组,造出了 4.8 个百分点的假差异。现在并列的公司永远分在同一组。
  7. 构造动量时不能偷看未来:“过去一年的涨幅”要从上一年的文件里取,并且要确认它在预测期开始之前就已经知道。
  8. 不能看了结果再改方向:规模、杠杆这些和理论相反的因子,方向只能根据更早的年份来决定。
  9. 我自己的预测也错过:我一开始认为随机划分会让分数严重虚高,实际上虚高只有平均 0.06,更大的问题是测在了最容易的一年。我还一度认为“赚得越多越好”,直到“是否盈利”这个最简单的基准打败了几乎所有盈利指标。把复杂的指标和最简单的基准比较,是这一轮最有用的习惯。

局限和下一步

这些结论都是“在 2014–2019 年的这批美股年度数据里”成立的:

  • 只有 5 年:每个因子只有 5 个 IC 值,t 值和 ICIR 都不可靠。
  • 只有年度数据:做不了标准的动量,也只能一年调仓一次。
  • 可能有幸存者偏差:中途退市、破产的公司可能不在数据里,而它们大概率正是亏损公司。
  • 时间对不齐:年报 2–3 月才发布,涨跌幅却从 1 月 1 日算起。

下一步:

  1. 行业中性化:盈利类因子最差的 20% 里,医疗健康公司的占比约是正常的 3 倍,主要是亏损的生物科技公司。要检验“避开亏损公司”在每个行业内部是否依然有效,还是只是“避开生物科技”。
  2. 多因子合成:每个故事选一个代表,再加上“现金流 + 财务稳健”,看合成后能不能稳定地超过最好的单因子。
  3. 换成月度数据:用带财报发布日期、包含已退市公司的数据,重新检验今天的所有结论。