news 2026/8/23 9:24:07

【Python量化实战 #12】选股还在拍脑袋?用 Python 搭多因子打分模型(5 因子实战)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Python量化实战 #12】选股还在拍脑袋?用 Python 搭多因子打分模型(5 因子实战)

连载系列「Python量化实战」第 12 篇(模块六 · 量化策略实战)。上一篇《Python双均线金叉策略完整实现》做的是"择时",本篇转向量化的另一半:“选股”。

Python多因子选股是机构量化的主流框架,思路并不神秘:给每只股票的估值、质量、成长、动量等维度分别打分,加权合成总分后排序取前 N 只。难点在工程侧——PE、ROE、营收增速、动量分散在行情快照和财务报表两类接口里,字段格式不统一、缺失值处理麻烦。本文用 mairui SDK 一站式取齐 5 个核心因子的原始数据,完成z-score 标准化 → 方向调整 → 等权合成 → 排序输出的完整多因子选股模型,全部代码可直接运行。

本文你将得到什么

  • 一套可直接运行的多因子框架:5 因子取数 →to_num清洗 → z-score 标准化 + 方向调整 → 等权合成排序
  • 真实打分输出(15 只样本 Top10)及"综合均衡才能排前"的逻辑解读
  • 负 PE 陷阱、缺失值处理、行业偏斜等 6 个真实避坑点

一、环境准备

本文代码使用 mairui SDK 获取股票数据,安装方法如下:

pipinstallmairui

运行环境:

  • Python 3.9+
  • mairui SDK 1.0.0
  • pandas 2.2+ / numpy
importosimportmairui# 证书从环境变量读取,注册后可在官网获取api=mairui.Client("LICENCE-66D8-9F96-0C7F0FBCD073")# 演示证书,演示证书(读者复制即用,无需替换)

本文数据截至 2026-07-27(行情为实时快照,财务为 2026 年一季报),读者复现时数值会随行情与财报更新而变化。

二、5 个核心因子与数据来源

因子维度字段来源方向
市盈率 PE估值stock_ssjype越低越好
市净率 PB估值stock_ssjysjl越低越好
净资产收益率 ROE质量stock_pershareindexjzcsyl越高越好
营收同比增速成长stock_pershareindexzyyrsrzz越高越好
60日涨跌幅动量stock_ssjyzdf60越高越好

两个接口分工明确:stock_ssjy一次返回估值与动量(pe市盈率、sjl市净率、zdf6060日涨跌幅),动量因子不用自己拉K线计算;stock_pershareindex返回历季财务主要指标,取最后一条即最新报告期。

importnumpyasnpimportpandasaspddefto_num(value):"""接口里缺失值以 '-' 返回,统一转成 NaN。"""try:returnfloat(value)except(TypeError,ValueError):returnnp.nandeffetch_factors(api,code,name):"""取单只股票的 5 个因子原始值。"""snap=api.stock_ssjy(code)# 实时快照:pe / sjl / zdf60snap=snap[0]ifisinstance(snap,list)elsesnap fin_rows=api.stock_pershareindex(code)# 历季财务指标,[-1] 为最新一期fin=fin_rows[-1]iffin_rowselse{}return{"code":code,"name":name,"pe":to_num(snap.get("pe")),"pb":to_num(snap.get("sjl")),"roe":to_num(fin.get("jzcsyl")),"rev_growth":to_num(fin.get("zyyrsrzz")),"momentum_60d":to_num(snap.get("zdf60")),}

关键解释:

  • 财务接口的缺失值是字符串"-"而非 null,直接float()会抛异常,to_num统一兜底;
  • 最新报告期若是一季报,ROE 为单季未年化值——横截面比较时所有股票同期,可比性不受影响,但不要拿去和年度 ROE 混着看。

三、z-score 标准化与方向调整

不同因子量纲差异巨大(PE 几十、增速可能上百),直接相加会被大数值因子主导。标准做法是z-score:减均值除以标准差,把所有因子拉到同一尺度。估值因子取负号(越便宜分越高):

defzscore(series):"""z-score 标准化,NaN 记 0 分(中性,不奖不罚)。"""z=(series-series.mean(skipna=True))/series.std(skipna=True)returnz.fillna(0.0)defbuild_score(factor_df):"""5 因子打分:估值负向,其余正向,等权合成。"""# 亏损股 PE<=0 不具可比性,记 NaN 中性处理factor_df.loc[factor_df["pe"]<=0,"pe"]=np.nan factor_df["score_value_pe"]=-zscore(factor_df["pe"])factor_df["score_value_pb"]=-zscore(factor_df["pb"])factor_df["score_quality"]=zscore(factor_df["roe"])factor_df["score_growth"]=zscore(factor_df["rev_growth"])factor_df["score_momentum"]=zscore(factor_df["momentum_60d"])score_cols=["score_value_pe","score_value_pb","score_quality","score_growth","score_momentum"]factor_df["total_score"]=factor_df[score_cols].mean(axis=1)returnfactor_df.sort_values("total_score",ascending=False).reset_index(drop=True)

关键解释:

  • 亏损股 PE 为负,数值上"最低"但含义完全相反,必须剔除后中性化,这是多因子模型最常见的数据陷阱;
  • NaN 填 0 分表示"该因子不奖不罚",比直接丢弃股票更稳健;
  • 等权是最朴素的加权方式,进阶做法(IC 加权、行业中性化)在文末讨论。

四、完整实战示例与真实运行结果

示例用一个跨行业的 15 只大盘股固定池(便于复现;实际使用可替换为指数成分股,成分股列表可通过官网指数接口获取):

importtime STOCK_POOL={"600519":"贵州茅台","000858":"五粮液","601318":"中国平安","600036":"招商银行","601166":"兴业银行","000333":"美的集团","600900":"长江电力","601088":"中国神华","600030":"中信证券","000651":"格力电器","600276":"恒瑞医药","300750":"宁德时代","601899":"紫金矿业","600887":"伊利股份","601668":"中国建筑",}defmain():api=mairui.Client("LICENCE-66D8-9F96-0C7F0FBCD073")# 演示证书,演示证书(读者复制即用,无需替换)records=[]forcode,nameinSTOCK_POOL.items():records.append(fetch_factors(api,code,name))time.sleep(0.2)# 控制请求频率ranked_df=build_score(pd.DataFrame(records))cols=["code","name","pe","pb","roe","rev_growth","momentum_60d","total_score"]print(ranked_df[cols].head(10).round(2).to_string(index=False))ranked_df.to_csv("factor_scores.csv",index=False,encoding="utf-8-sig")main()

真实运行输出(2026-07-27 盘中快照):

因子采集: 成功 15 只, 失败 0 只 === 多因子打分排序(Top 10)=== code name pe pb roe rev_growth momentum_60d total_score 600030 中信证券 10.15 1.45 3.12 40.91 4.05 0.52 600887 伊利股份 7.73 3.09 9.44 5.47 7.20 0.50 601899 紫金矿业 10.37 4.39 10.40 24.79 -3.87 0.34 601166 兴业银行 4.05 0.47 2.63 -1.06 3.51 0.22 000651 格力电器 9.26 1.48 4.08 3.46 4.68 0.19 601668 中国建筑 3.48 0.40 2.79 -7.85 2.41 0.15 000333 美的集团 12.59 3.09 5.56 2.45 9.12 0.15 600036 招商银行 6.47 0.89 2.96 3.81 0.67 0.14 300750 宁德时代 21.17 4.83 5.97 52.45 -7.36 0.00 000858 五粮液 8.87 2.42 6.50 33.67 -24.51 -0.07

读一下这个排序的逻辑是否自洽:中信证券靠"低估值 + 40.91% 的高营收增速"登顶;宁德时代增速最高(52.45%)但估值最贵、动量为负,各因子互相抵消后总分归零——这正是多因子模型"不偏科"的特点:单一亮点不足以上榜,综合均衡才能排前。打分结果是数据的横截面快照,不是买入名单

五、避坑与进阶

  • 缺失值:财务字段的"-"必须统一转 NaN,且 zscore 后填 0 中性化,不要直接dropna丢股票;
  • 负 PE 陷阱:亏损股 PE 为负会被"低估值"因子误判为最优,务必先剔除;
  • 报告期对齐:横截面所有股票用同一最新报告期,不要混用年报和季报;
  • 小样本失真:15 只股票的 z-score 仅作演示,实际应在全市场或指数成分内计算(全市场列表可用stock_list,共 5000+ 只);
  • 行业偏斜:银行天然低 PB、科技天然高 PE,直接全市场打分会重仓金融——进阶做法是行业内分别 z-score(行业中性化),行业归属可通过官网板块接口获取;
  • 频率限制:批量采集加time.sleep控频,全市场级别的并发拉取方案见本系列第 16 篇。

六、总结与延伸

本文搭出了多因子选股的最小可用框架:两个接口取齐 5 因子 →to_num清洗 → z-score 标准化 + 方向调整 → 等权合成排序,并讨论了负 PE、行业偏斜等真实陷阱。下一篇《从零搭建Python量化选股系统:多条件筛选与历史回测》将把"因子打分"升级成"可配置的选股系统",并与第 11 篇的回测框架打通。


本文为技术演示,打分结果为特定时点的数据快照,不构成投资建议,亦不代表任何个股推荐。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/23 9:22:26

前缀和与哈希表在子数组和问题中的应用:以Codeforces经典题为例

1. 项目概述&#xff1a;从一道经典CF题看前缀和与哈希表的威力最近在Codeforces上刷题&#xff0c;又翻到了ICM Technex 2017和Codeforces Round #400 (Div. 1 Div. 2, combined)这场比赛的C题&#xff0c;题目叫“Molly‘s Chemicals”。这道题可以说是前缀和思想结合哈希表…

作者头像 李华
网站建设 2026/8/23 8:59:24

从求最大值实验深入理解C++模板:泛型编程与参数多态性

1. 从“求最大值”到理解C模板的威力 最近在辅导一些同学做C实验时&#xff0c;发现一个挺有意思的现象。实验题目是“设计一个求三个数中最大值的函数模板”&#xff0c;很多同学的第一反应是&#xff1a;这太简单了&#xff0c;不就是写个 max 函数&#xff0c;然后用 if-…

作者头像 李华
网站建设 2026/8/23 8:56:37

Allreduce算法:大模型分布式训练的核心通信原理与工程实践

1. 项目概述&#xff1a;为什么Allreduce是大模型训练的“生命线”&#xff1f;如果你最近关注过大模型相关的新闻或者技术讨论&#xff0c;大概率会看到“千亿参数”、“万亿token训练”这样的字眼。这些数字背后&#xff0c;是海量的计算和通信开销。一个直观的问题是&#x…

作者头像 李华
网站建设 2026/8/23 8:53:01

Go service如何搭建自己的可观测性?

那天凌晨两点&#xff0c;支付服务又开始报超时了。我打开 Kibana&#xff0c;熟练地输入 "error"&#xff0c;回车。三千条结果。再输入 "timeout"&#xff0c;一千二。然后我开始了那场熟悉的“猜谜游戏”&#xff1a;翻日志、对时间戳、开另一个窗口看监…

作者头像 李华
网站建设 2026/8/23 8:52:33

STM32 SD 卡 + FatFS 实战:掉电丢数据?f_sync 和簇对齐写救你

给温控器加数据记录功能那次&#xff0c;客户要求"断电前至少保留最近 1000 条记录"。我一开始用片上 Flash 轮流擦两页存&#xff0c;每条 16 字节&#xff0c;两页一共只能存 128 条。后来换了 SD 卡&#xff0c;FatFS 一挂&#xff0c;f_open 一个 CSV 文件一行行…

作者头像 李华