ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

A股产业链数据集实战:从行业分类到上下游图谱构建与因子挖掘

A股产业链数据集实战:从行业分类到上下游图谱构建与因子挖掘

简介:这份数据集面向金融量化研究者、知识图谱工程师与产业链分析从业者,系统整理A股4654家上市公司的产业链结构化关系,覆盖公司、行业、产品三类核心实体,包含公司所属行业、行业上下级隶属、产品上游原材料、下游应用、公司主营产品及产品细分小类共6种语义关系,可用于知识图谱构建、图神经网络训练、供应链风险建模等任务。资源包共23个文件,约5.4MB,以8个JSON数据文件为主体,分别承载公司基础信息、511个行业节点、9.5万条产品记录及各类归属与上下游关系;另附build_graph.py图谱构建脚本、requirements.txt依赖清单、readme.md说明文档与10张可视化示意图,便于快速导入Neo4j、NetworkX或PyTorch Geometric等框架。目前已有24人学习下载。读者可直接获得清洗整合后的权威关系数据、开箱即用的建图脚本与直观的产业链示意图,省去从零采集与对齐实体的大量成本,适合作为产业链分析、图计算实验与供应链研究的现成数据底座。

1. 拆开一份 A 股产业链数据集:从行业分类到上下游关联,它到底能干什么

做行业研究或者量化选股的朋友大概率都遇到过这种局面:想跑一个「上下游传导」的逻辑,比如上游原材料涨价对中游制造毛利的影响,结果发现手里只有一张申万行业分类表,颗粒度粗到「电子」「化工」这种一级门类,根本没法把「某公司给另一家公司供货」这层关系表达出来。行业分类解决的是「它属于哪个筐」,产业链关系解决的是「谁给谁供货、谁卡谁的脖子」,这是两码事。

这份 A 股上市公司产业链关系数据集,核心就是把后一层关系补上。它把上市公司按行业归属、主营产品、上下游关联三个维度组织起来,本质是一份可以往知识图谱方向搭的结构化底表。适合三类人:做行业比较研究但嫌手工整理太慢的分析者、想给选股因子加「产业链位置」维度的量化开发者、以及需要快速搭一个产业关系 Demo 的数据工程同学。下面按「它长什么样 → 怎么读进来 → 怎么用 → 坑在哪」的顺序拆。

2. 数据结构拆解:行业、产品、上下游三张表怎么咬合

拿到一份关系型数据集,第一件事不是急着写查询,而是先搞清楚它的实体和关系是怎么设计的。这份数据集的骨架其实就三个实体:公司、行业、产品,外加两类关系:公司归属行业、公司关联上下游。理解了这个模型,后面所有操作都是顺理成章的事。

2.1 实体表与关系表的字段设计

常见的组织方式是把「公司—行业」和「公司—产品」做成宽表或映射表,把「公司—公司」的上下游关系单独抽成一张边表。边表是整份数据里最有价值的部分,因为它承载了图谱的连通性。典型字段大致如下:

表名关键字段说明
公司主表公司代码、公司名称、所属行业主体信息,代码是唯一键
行业分类表行业代码、行业名称、层级一般分一/二/三级,层级决定颗粒度
产品关系表公司代码、主营产品、产品类别一个公司可对应多个产品
上下游边表上游代码、下游代码、关系类型关系类型区分供应/客户/同链

这里要特别留意「关系类型」这个字段。很多数据集只给一个「关联」标记,不区分方向,用起来会很别扭——因为供应链是有向的,上游到下游和下游到上游在分析里完全是两个结论。如果拿到的边表没有方向字段,就得靠行业层级或者产品类别自己推断,这一步后面会讲。

2.2 行业分类的层级与颗粒度选择

行业分类通常不是单层,而是一级门类下面挂二级、二级下面挂三级。颗粒度选哪一层,直接决定你后面能问出什么问题。一级门类适合做宏观的板块轮动,三级分类才适合做「同链条内公司对比」。

我一般会先把行业表按层级展开看一眼分布,确认三级分类下每个行业至少有几家公司,避免出现某个三级行业只有一家公司、做对比时没有参照系的情况。用一段简单的探查代码就能摸清:

import pandas as pd # 读入行业分类表,假设列名为 industry_code / industry_name / level / parent_code ind = pd.read_csv("industry.csv", dtype={"industry_code": str}) # 看每个层级的行业数量分布 print(ind.groupby("level")["industry_code"].nunique()) # 看三级行业下的公司数量,找出只有单一公司的“孤儿行业” merged = pd.read_csv("company.csv", dtype={"industry_code": str}) count_by_ind = merged.groupby("industry_code").size().sort_values() print(count_by_ind.head(20)) # 数量最少的行业,做对比时要小心

这段代码的逻辑很直接:先确认层级结构是否完整,再定位那些样本过少的行业。参数上,dtype强制把代码读成字符串是关键,否则像000001这种前导零会被 pandas 当成整数吃掉,后面关联时全对不上,这是血泪经验。

2.3 上下游边表的连通性检查

边表拿到手,别急着跑图算法,先做连通性体检。一份产业链边表如果大量公司是孤立节点,那它作为图谱的价值就大打折扣。检查方法就是统计每个节点的度数,看孤立点和超级节点的分布。

import networkx as nx edges = pd.read_csv("supply_chain_edges.csv", dtype={"upstream": str, "downstream": str}) G = nx.from_pandas_edgelist(edges, source="upstream", target="downstream", create_using=nx.DiGraph()) # 孤立节点:既没有上游也没有下游 isolated = [n for n in G.nodes if G.degree(n) == 0] print("孤立节点数:", len(isolated)) # 度数最高的节点,往往是产业链里的“枢纽”公司 top_nodes = sorted(G.degree, key=lambda x: x[1], reverse=True)[:10] print("枢纽节点:", top_nodes)

逻辑说明:用有向图DiGraph是因为供应链有方向。G.degree(n)统计的是入度加出度,孤立节点意味着这家公司在边表里没有任何关联记录。如果孤立比例超过三成,说明这份边表覆盖不全,做全图分析时要标注这个局限,别拿它当完整图谱用。参数上create_using决定图类型,做上下游传导必须用有向图,用无向图会把方向信息丢掉。

3. 把数据集读进图数据库:从 CSV 到可查询的产业链图谱

CSV 能看不能查,真正要让产业链关系「活」起来,得把它灌进图数据库或者至少建好索引。这一章讲落地路径:选型理由、导入步骤、以及怎么验证导入没出错。

3.1 图数据库选型:为什么不是直接上 Neo4j

一提知识图谱,很多人第一反应是 Neo4j。但对这份数据集来说,先别急着上重型图库。判断标准很简单:如果你的查询主要是「某公司的上下游是谁」这种一两跳的邻接查询,用 pandas 加邻接表就够了;只有当你要做多跳路径搜索、最短路径、社区发现这类操作时,图数据库的收益才明显。

我一般的做法是分两步走:先用 pandas 把邻接关系建好,跑通业务逻辑;确认需要多跳查询了,再迁到图库。这样避免一开始就陷入图库的部署和运维,把精力浪费在环境上。如果确实要上图库,Neo4j 的 Cypher 语法对产业链这种「节点—关系—节点」模型表达很自然,是常见选择。

3.2 用 Cypher 批量导入节点与关系

假设已经装好图库,导入分两步:先建节点,再建关系。节点导入用LOAD CSV,关系导入时用MATCH把两端节点找出来再连边。

// 第一步:导入公司节点,company_code 作为唯一约束键 CREATE CONSTRAINT company_code IF NOT EXISTS FOR (c:Company) REQUIRE c.code IS UNIQUE; LOAD CSV WITH HEADERS FROM 'file:///company.csv' AS row MERGE (c:Company {code: row.company_code}) SET c.name = row.company_name, c.industry = row.industry_code; // 第二步:导入上下游关系,先匹配两端再建边 LOAD CSV WITH HEADERS FROM 'file:///supply_chain_edges.csv' AS row MATCH (up:Company {code: row.upstream}) MATCH (down:Company {code: row.downstream}) MERGE (up)-[:SUPPLIES_TO {type: row.relation_type}]->(down);

逻辑说明:MERGE而不是CREATE,是为了防止重复导入时产生重复节点和重复边,这是幂等导入的关键。CREATE CONSTRAINT建唯一约束不仅保证数据干净,还会自动建索引,后面按代码查节点会快很多。参数上,file:///指向的是图库的 import 目录,不是本地任意路径,这点新手经常翻车——文件放错目录会直接报找不到文件。

3.3 导入后的验证查询

导入完必须验证,否则后面分析全建立在错误数据上。验证分三层:节点数对不对、关系数对不对、抽样路径能不能查通。

// 节点与关系总量 MATCH (c:Company) RETURN count(c) AS company_count; MATCH ()-[r:SUPPLIES_TO]->() RETURN count(r) AS edge_count; // 抽一家公司,看它的直接上下游 MATCH (c:Company {code: '000001'})-[:SUPPLIES_TO]->(down) RETURN c.name, collect(down.name) AS downstream; // 两跳路径:上游的上游 MATCH path = (a:Company {code: '000001'})<-[:SUPPLIES_TO*1..2]-(up) RETURN up.name, length(path);

逻辑说明:前两条是总量核对,跟原始 CSV 的行数对一下,差太多说明导入有丢失。第三条验证方向对不对——SUPPLIES_TO是从上游指向下游,所以查某公司的下游要用出边,查上游要用入边,方向搞反了结果会完全错。*1..2表示一到两跳,这是图库相比 SQL 自连接最舒服的地方,多跳查询不用写一堆 JOIN。

4. 避坑与排查:产业链数据落地时最容易翻车的五件事

这份数据集本身结构不复杂,但真正用起来,坑往往出在数据清洗和语义理解上。下面五条是我踩过或者见别人踩过的,按「现象 → 原因 → 解决」记下来。

现象一:公司代码关联后大量匹配不上。原因基本是前导零被吞。Excel 打开 CSV 再另存,000001会变成1,导入后跟主表对不上。解决办法是全程用代码读,dtype指定字符串,绝不经过 Excel 中转;如果已经污染了,用zfill(6)补零。

现象二:同一家公司出现多个名称,去重后节点数对不上。原因是简称、全称、曾用名混用。解决是先以公司代码为唯一键建节点,名称只作为属性,不要拿名称当关联键。名称可以重复,代码不会。

现象三:上下游关系方向反了,传导逻辑全错。现象是分析「原材料涨价影响」时,结果指向了上游而不是下游。原因是边表的方向定义没看清,或者导入时 source/target 写反。解决是导入后一定抽样验证方向,用一家你熟悉的公司做基准,看它的上下游是否符合常识。

现象四:行业分类层级混用,一级和三级混在一列里。现象是分组统计时数量对不上。原因是分类表里不同层级的代码长度或前缀规则不同,没做区分。解决是按level字段过滤,或者用parent_code建树,分析时明确指定用哪一层。

现象五:把「同行业」当成「同产业链」。这是语义层面的坑,也是最隐蔽的。两家公司同属一个三级行业,不代表它们有上下游关系;反过来,上下游公司可能分属不同行业。做因子时如果把这两个概念混用,逻辑就站不住。解决是明确区分:行业分类用于横向对比,上下游边表用于纵向传导,两者不要互相替代。

提示:导入前先跑一遍连通性和唯一性检查,比导入后返工省事得多。数据量不大时,全部在 pandas 里验证一遍再入库。

5. 进阶用法:用产业链位置构造一个可回测的因子

数据落到图里只是第一步,真正体现价值的是把它变成可计算的指标。产业链位置可以量化成几个维度:上游度(一家公司离终端消费有多远)、枢纽度(连接了多少上下游)、链条集中度(它的上下游是否集中在少数几家公司)。这几个指标都能从边表直接算出来,而且天然适合做选股因子的输入。

5.1 计算上游度与枢纽度

上游度可以用节点到「终端节点」的平均最短路径长度来近似,路径越长说明越靠上游。枢纽度直接用度数中心性。networkx 里都有现成函数。

import networkx as nx import pandas as pd edges = pd.read_csv("supply_chain_edges.csv", dtype={"upstream": str, "downstream": str}) G = nx.from_pandas_edgelist(edges, "upstream", "downstream", create_using=nx.DiGraph()) # 枢纽度:度数中心性,归一化 degree_cent = nx.degree_centrality(G) # 上游度:到所有出度为 0 的终端节点的平均最短路径 terminals = [n for n in G.nodes if G.out_degree(n) == 0] upstream_score = {} for node in G.nodes: lengths = [] for t in terminals: try: lengths.append(nx.shortest_path_length(G, node, t)) except nx.NetworkXNoPath: continue upstream_score[node] = sum(lengths) / len(lengths) if lengths else None factor = pd.DataFrame({ "code": list(G.nodes), "hub_score": [degree_cent[n] for n in G.nodes], "upstream_score": [upstream_score[n] for n in G.nodes], }) print(factor.sort_values("upstream_score", ascending=False).head(10))

逻辑说明:degree_centrality衡量节点连接广度,值越高说明这家公司在链条里越像枢纽。上游度用「到终端节点的平均最短路径」近似,路径越长越靠上游。参数上,terminals定义为出度为 0 的节点,也就是没有下游的终端环节;如果数据里终端节点定义不同,这个集合要相应调整。注意NetworkXNoPath的捕获——图里可能存在不连通的部分,不处理会直接抛异常中断。

5.2 因子验证与常见误用

算出来的因子别直接拿去用,先做两件事:一是看分布,确认没有极端值主导;二是跟行业做正交,避免它其实只是在区分行业。因为产业链位置和行业高度相关,如果不做行业中性化,你测出来的可能只是行业效应,不是产业链效应。

我一般会把因子按行业分组后做 z-score 标准化,再合并回全样本,这样比较的是「同行业内的产业链位置差异」。这一步做完,因子的解释力往往更干净。验证时用简单的分组收益或者 IC 就行,不用一上来就上复杂模型。

从那以后我每次拿到一份关系型数据集,都会先花半小时把实体、关系、方向、唯一键这四件事确认清楚,再动手写分析代码。这份产业链数据集的价值不在数据量,而在它把「谁连着谁」这层关系显式表达了出来,剩下的就看你用它问出什么问题。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进