第四章:数据工程——构建股票关系图
说实话,很多做量化的人把精力都花在模型上,却忽略了数据工程。我个人觉得,这其实是本末倒置。你想想看,再牛的图神经网络,喂进去一堆脏数据,出来的结果能靠谱吗?
这一章,我们就来聊聊怎么构建股票关系图。说白了,就是把股票之间的关联关系,用图的形式表达出来。我做过不少项目,踩过很多坑,今天把这些经验都分享给你。
4.1 为什么要构建股票关系图?
传统的量化分析,往往只盯着个股的价格、成交量这些时间序列数据。但股票不是孤岛。一只股票的涨跌,往往会带动同行业的股票,或者影响它的上下游企业。
举个例子。我记得有一次,某家芯片设计公司突然宣布断供。结果呢?它的下游客户——那些手机厂商,股价当天就跌了。而它的竞争对手,股价反而涨了。这种联动关系,传统的时间序列模型很难捕捉到。
图神经网络的优势就在这里。它能天然地处理这种「关系」信息。你只需要把股票当作节点,把它们的关联关系当作边,剩下的交给模型去学。
4.2 三种核心关系:行业、供应链、股东
构建股票关系图,最常用的就是这三种关系。我一个个来说。
4.2.1 行业关联
这是最直观的关系。同行业的股票,往往受到相同的宏观因素影响。比如,所有白酒股都会受到「禁酒令」的影响。
怎么构建?很简单。用申万行业分类或者GICS行业分类。同一行业分类下的股票,两两之间连一条边。
嗯,这里要注意一点。行业分类的粒度很重要。太粗了(比如「制造业」),关系太弱。太细了(比如「白酒Ⅲ」),又可能样本太少。我个人习惯用申万二级行业分类,平衡性比较好。
4.2.2 供应链关联
这个就更有意思了。A公司是B公司的上游供应商,或者C公司是D公司的大客户。这种关系往往能带来很强的股价联动。
我在项目中遇到过这样的情况:某家汽车零部件公司,它的最大客户是一家新能源车企。结果新能源车企的销量一公布,零部件公司的股价立马跟着动。这种关系,光看行业分类是看不出来的。
供应链数据从哪里来?我后面会讲数据源。这里先记住:供应链关系是有方向的。A→B 表示 A 是 B 的供应商。在构建图的时候,可以用有向边来表示。
4.2.3 股东关联
这个稍微复杂一点。两家公司如果有共同的机构股东,或者有交叉持股,那么它们的股价走势往往也会趋同。
为什么?因为机构投资者在做资产配置时,往往会同时买入或卖出同一板块的股票。或者,交叉持股意味着两家公司的利益深度绑定。
我曾经踩过一个坑。当时我直接把所有共同股东都算作关联,结果图变得非常稠密,模型根本跑不动。后来我加了一个阈值:只有共同持股比例超过5%的,才建立关联。效果好了很多。
4.3 数据源:Tushare 与 Yahoo Finance
数据从哪里来?我主要用两个数据源。
4.3.1 Tushare(国内数据)
Tushare 是国内最常用的金融数据接口之一。它提供了非常全面的A股数据,包括行业分类、股东信息、财务数据等。
我个人比较推荐用 Tushare Pro 版本。虽然需要积分,但数据质量比免费版好很多。尤其是股东数据,免费版经常缺字段。
下面是一个获取行业分类的示例代码:
import tushare as ts
# 设置token
ts.set_token('你的token')
# 初始化pro接口
pro = ts.pro_api()
# 获取所有股票的行业分类
df = pro.stock_basic(
exchange='',
list_status='L',
fields='ts_code,symbol,name,industry'
)
print(df.head())
嗯,这里要注意。Tushare 的行业分类默认是申万一级行业。如果你需要二级或三级分类,需要额外调用 industry_code 接口。
4.3.2 Yahoo Finance(海外数据)
如果你做的是美股或者港股,Yahoo Finance 是个不错的选择。它免费,而且数据覆盖面广。
不过,Yahoo Finance 的行业分类比较粗糙。它用的是自己的分类体系,只有十几个大类。对于供应链数据,Yahoo Finance 基本不提供。你需要从其他渠道获取。
下面是一个获取美股行业分类的示例:
import yfinance as yf
# 获取苹果公司的信息
aapl = yf.Ticker("AAPL")
# 获取行业信息
info = aapl.info
print(f"行业: {info['sector']}")
print(f"子行业: {info['industry']}")
4.4 数据清洗实战
数据拿到手,不等于就能直接用。清洗这一步,往往比获取数据更耗时。我一般会做以下几件事。
4.4.1 处理缺失值
行业分类缺失?股东数据不全?这是家常便饭。我的处理原则是:
- 行业分类缺失:直接剔除这只股票。因为行业关联是基础关系,缺失了就没法建图。
- 股东数据缺失:可以保留,但只建立行业关联和供应链关联。
- 供应链数据缺失:这个比较棘手。我一般会用文本挖掘的方法,从年报中提取客户和供应商信息。
4.4.2 去重与标准化
同一个公司,在不同数据源里可能有不同的名字。比如「贵州茅台」和「贵州茅台酒股份有限公司」。需要做实体对齐。
我常用的方法是:先用股票代码作为唯一标识。如果代码不一致,再用公司名称做模糊匹配。
# 简单的去重示例
df_clean = df.drop_duplicates(subset=['ts_code'])
# 标准化公司名称
df_clean['name'] = df_clean['name'].str.replace('股份有限公司', '')
df_clean['name'] = df_clean['name'].str.replace('有限公司', '')
4.4.3 时间对齐
这个坑我踩过好几次。不同数据源的数据更新频率不一样。Tushare 的财务数据是季度更新,而 Yahoo Finance 的价格数据是日更新。
如果你要构建动态图(随时间变化的图),一定要做好时间对齐。我一般会以交易日为基准,把财务数据、股东数据都对齐到最近的交易日。
4.5 知识体系总览
说了这么多,我画了一张图,帮你把这一章的核心逻辑串起来。
4.6 实战:从零构建一个简单的股票关系图
光说不练假把式。我们来写一段完整的代码,从数据获取到图构建,一步到位。
import tushare as ts
import pandas as pd
import networkx as nx
# 1. 获取数据
pro = ts.pro_api('你的token')
df_stocks = pro.stock_basic(exchange='', list_status='L')
# 2. 构建行业关系
industry_groups = df_stocks.groupby('industry')
G = nx.Graph()
for industry, group in industry_groups:
stocks = group['ts_code'].tolist()
# 同一行业内的股票两两相连
for i in range(len(stocks)):
for j in range(i+1, len(stocks)):
G.add_edge(stocks[i], stocks[j], relation='industry')
# 3. 添加节点特征(以市盈率为例)
for code in G.nodes():
try:
df_fin = pro.daily_basic(ts_code=code, trade_date='20240131')
if not df_fin.empty:
pe = df_fin['pe'].values[0]
G.nodes[code]['pe'] = pe
except:
G.nodes[code]['pe'] = None
print(f"图构建完成:{G.number_of_nodes()} 个节点,{G.number_of_edges()} 条边")
这段代码很简单,但已经能跑通。你可以在它的基础上,加入供应链和股东关系。
好了,这一章的内容就到这里。数据工程是个细活,急不来。你慢慢做,把每一步都走扎实了,后面模型训练的时候会轻松很多。