简介:面向金融科技与知识图谱方向的学习者与开发者,这份资源提供了一套十万级别的产业链知识图谱数据集,覆盖上市公司、行业、产品三类实体,节点规模超10万、关系边达16万,可支撑图谱构建、关系推理与产业分析等实践场景。包内共25个文件,以json数据文件为主,辅以png可视化图、xml配置与Python构建脚本,压缩包约5.42MB,结构清晰便于直接加载使用。数据涵盖上市公司所属行业、行业上下级、产品上下游、公司主营产品及产品小类等6大类关系,包含上市公司4654家、行业511个、产品95559条、上游材料56824条等具体记录。已有438人学习下载,适合需要真实产业链语料开展图谱建模、查询验证或教学演示的读者,可快速复现从数据到图谱的完整流程。
1. 十万级产业链图谱到底长什么样:三类实体、16 万条关系边
拿到这份产业链知识图谱资源时,我第一反应不是看代码,而是先数实体和边。上市公司、行业、产品三类实体,节点规模 10w+,关系边 16w,这个量级放在 Neo4j 单机里属于「能跑但需要调优」的区间。它解决的核心问题很具体:把一家上市公司、它所属的行业、它生产的产品串成一张可查询、可推理的网,而不是散落在 Excel 和公告 PDF 里的孤立字段。适合谁?做金融数据中台、投研知识库、产业链风险传导分析的团队,以及想拿真实规模图数据练手 Neo4j 和命名实体识别的工程师。如果你只想要几百个节点的玩具图,这份资源反而会让你在导入和索引上多花时间。下面我按「先看清结构、再动手导入、最后避坑」的顺序拆一遍。
2. 三类实体怎么建模:上市公司、行业、产品的本体设计
2.1 为什么是这三类实体,而不是更多
产业链图谱最容易翻车的地方是实体类型膨胀。一上来把「高管」「供应商」「客户」「专利」全塞进去,节点类型冲到十几类,关系边直接爆炸,查询性能断崖式下跌。这份资源只保留上市公司、行业、产品三类,是一个很务实的取舍。上市公司是资金和信息的锚点,行业是分类维度,产品是上下游传导的载体。三者构成「公司属于行业、公司生产产品、产品归属行业」的基本闭环,足够支撑「某行业下有哪些公司」「某产品涉及哪些上市公司」这类高频查询。
从本体建模角度看,这三类实体对应的是金融领域最稳定的语义层。行业分类可以对齐申万或中信,产品可以对齐国民经济行业分类,上市公司有统一社会信用代码和股票代码。常见做法是先用结构化分析方法画实体-联系图,把实体属性定下来再写导入脚本,而不是边导边改。我一般会先把三类实体的属性列成表,确认哪些是唯一键、哪些是冗余字段,再决定 Neo4j 里的标签和索引。
| 实体类型 | 建议唯一键 | 典型属性 | 索引策略 |
|---|---|---|---|
| 上市公司 | 股票代码 | 名称、行业代码、注册资本 | 股票代码唯一约束 |
| 行业 | 行业代码 | 名称、层级、父行业 | 行业代码唯一约束 |
| 产品 | 产品编码 | 名称、所属行业、规格 | 产品编码唯一约束 |
这张表不是让你照抄,而是提醒你:三类实体各自要有唯一键,否则 10w 节点里会出现大量重复。命名实体识别阶段如果没做归一化,「贵州茅台」和「茅台」会变成两个节点,后面关系边就对不上了。
2.2 关系边的方向与基数怎么定
16w 条关系边听起来多,拆开看其实就几种:公司-行业(属于)、公司-产品(生产)、产品-行业(归属)。方向必须固定,否则查询时要从两个方向各写一遍。我的习惯是统一成「主体 -> 客体」:上市公司 -> 行业表示属于,上市公司 -> 产品表示生产,产品 -> 行业表示归属。基数上,一家公司可以属于多个行业(多元化经营),一个行业有多家公司,一个产品可以有多家公司生产,所以都是多对多。
这里有个容易被忽略的点:关系边上的属性。比如「公司-产品」这条边可以带营收占比、是否主营。如果只建关系不带属性,后面想做「某公司主营产品」就得回表查,图谱的价值就打了折扣。资源里 16w 条边如果都带上一两个属性,查询灵活度会高很多。建边的时候用 MERGE 而不是 CREATE,避免重复导入时边数翻倍,这是血泪经验。
// 建立三类实体的唯一约束,防止重复节点 CREATE CONSTRAINT company_code IF NOT EXISTS FOR (c:Company) REQUIRE c.code IS UNIQUE; CREATE CONSTRAINT industry_code IF NOT EXISTS FOR (i:Industry) REQUIRE i.code IS UNIQUE; CREATE CONSTRAINT product_code IF NOT EXISTS FOR (p:Product) REQUIRE p.code IS UNIQUE; // 建立公司到行业的关系,带营收占比属性 MATCH (c:Company {code: $companyCode}) MATCH (i:Industry {code: $industryCode}) MERGE (c)-[r:BELONGS_TO]->(i) SET r.revenueRatio = $ratio, r.year = $year;上面这段 Cypher 里,CREATE CONSTRAINT是幂等操作,重复执行不会报错。MERGE保证同一条边只建一次,SET把营收占比和年份写到边上。参数$companyCode、$industryCode从导入脚本传入,$ratio是浮点数,$year是整数。如果你用 LOAD CSV 批量导入,把这段逻辑放进apoc.periodic.iterate里分批跑,别一次性提交 16w 条边,否则事务内存撑不住。
3. 从原始数据到 Neo4j:导入脚本与批量写入参数
3.1 数据清洗与实体对齐
原始数据大概率是 CSV 或 JSON,字段名不统一,上市公司名称可能带「股份有限公司」后缀,行业名称可能有「制造业-专用设备」这种层级。导入前必须做两件事:字段映射和实体对齐。字段映射是把源字段改成图谱属性名,实体对齐是把同一实体的不同写法归并到一个唯一键。常见做法是先用 pandas 做一轮清洗,把名称转成代码,再导出成 Neo4j 能吃的 CSV。
import pandas as pd # 读取原始上市公司数据 df = pd.read_csv("listed_companies.csv", dtype=str) # 字段映射:源字段 -> 图谱属性 df = df.rename(columns={ "股票代码": "code", "公司名称": "name", "所属行业代码": "industry_code", "注册资本": "registered_capital" }) # 实体对齐:去掉名称里的后缀,统一成简称 df["name"] = df["name"].str.replace("股份有限公司", "", regex=False) df["name"] = df["name"].str.replace("有限公司", "", regex=False) # 去重:同一股票代码只保留一条 df = df.drop_duplicates(subset=["code"]) # 导出成 Neo4j 导入用的 CSV df[["code", "name", "industry_code", "registered_capital"]].to_csv( "companies_clean.csv", index=False, encoding="utf-8" )这段脚本的关键在drop_duplicates(subset=["code"]),如果源数据里同一家公司出现多次,不去重会导致节点数虚高。encoding="utf-8"是必须的,Neo4j 默认按 UTF-8 读 CSV,用 GBK 会乱码。清洗完的 CSV 字段名要和 Cypher 里的属性名一致,否则 LOAD CSV 时对不上。
3.2 用 LOAD CSV 和 APOC 批量导入
10w 节点、16w 边,用单条 CREATE 逐条写会慢到怀疑人生。正确姿势是 LOAD CSV 加 APOC 分批提交。LOAD CSV 负责读文件,APOC 的apoc.periodic.iterate负责把大事务拆成小批次,每批 5000 到 10000 条,避免内存溢出。
// 导入上市公司节点 LOAD CSV WITH HEADERS FROM 'file:///companies_clean.csv' AS row CALL apoc.merge.node( ['Company'], {code: row.code}, {name: row.name, registered_capital: row.registered_capital}, {} ) YIELD node RETURN count(node); // 分批导入公司-行业关系 CALL apoc.periodic.iterate( "LOAD CSV WITH HEADERS FROM 'file:///company_industry.csv' AS row RETURN row", "MATCH (c:Company {code: row.company_code}) MATCH (i:Industry {code: row.industry_code}) MERGE (c)-[r:BELONGS_TO]->(i) SET r.revenueRatio = toFloat(row.revenue_ratio)", {batchSize: 5000, parallel: false} );apoc.merge.node的第一个参数是标签列表,第二个是唯一键,第三个是创建时设置的属性,第四个是匹配时更新的属性。apoc.periodic.iterate的第一个参数是驱动查询,第二个是每批执行的操作,batchSize控制每批条数。parallel: false是因为 MERGE 在并发下可能产生重复边,单线程更稳。导入前记得把 CSV 放到 Neo4j 的 import 目录,否则file:///找不到文件。
导入完成后用MATCH (n) RETURN count(n)验证节点数,用MATCH ()-[r]->() RETURN count(r)验证边数。如果边数明显超过 16w,检查是不是 MERGE 写成了 CREATE,或者重复跑了导入脚本。
4. 查询性能与索引:10w 节点下怎么不卡
4.1 索引和约束的取舍
10w 节点不算大,但没有索引的查询会全图扫描,几秒变几十秒。三类实体的唯一键都要建唯一约束,唯一约束自带索引。除此之外,如果经常按名称模糊查询,可以加全文索引。但全文索引会占内存,节点规模再大一个量级就要谨慎。
// 查看已有索引 SHOW INDEXES; // 为行业名称建全文索引,支持模糊检索 CREATE FULLTEXT INDEX industryName IF NOT EXISTS FOR (i:Industry) ON EACH [i.name]; // 用全文索引查询包含「新能源」的行业 CALL db.index.fulltext.queryNodes('industryName', '新能源') YIELD node, score RETURN node.name, score ORDER BY score DESC LIMIT 10;SHOW INDEXES先确认约束是否生效,CREATE FULLTEXT INDEX建全文索引,db.index.fulltext.queryNodes是调用方式。全文索引对中文分词支持一般,如果查询效果不好,退回CONTAINS加普通索引也能用,只是性能差一些。
4.2 多跳查询的写法与边界
产业链图谱的价值在多跳查询,比如「某上市公司 -> 行业 -> 同行业其他公司 -> 这些公司的产品」。两跳以内性能没问题,三跳以上要控制中间结果集大小。常见做法是先用索引把起点缩小,再展开关系,最后用 LIMIT 截断。
// 查询某公司同行业的所有公司及其产品,限制返回条数 MATCH (c:Company {code: '600519'})-[:BELONGS_TO]->(i:Industry) MATCH (other:Company)-[:BELONGS_TO]->(i) MATCH (other)-[:PRODUCES]->(p:Product) RETURN other.name AS company, collect(p.name) AS products LIMIT 50;这段查询先从股票代码定位公司,再找同行业公司,再找这些公司的产品。collect(p.name)把产品聚合成列表,避免一行一个产品导致结果集膨胀。LIMIT 50是保护,不加的话同行业公司多的时候会返回大量数据。如果查询还是慢,在BELONGS_TO和PRODUCES关系上建关系索引,或者把中间结果用 APOC 缓存。
提示:多跳查询前先用
PROFILE看执行计划,确认走的是索引而不是全图扫描。执行计划里出现AllNodesScan就说明索引没命中。
5. 避坑与排查:导入和查询中最容易翻车的五件事
5.1 现象:导入后节点数比预期多一倍
原因:重复执行导入脚本,且用了 CREATE 而不是 MERGE。 解决:所有节点和关系导入统一用 MERGE,导入前先MATCH (n) DETACH DELETE n清空,或者用唯一约束让重复写入失败。
5.2 现象:LOAD CSV 报错「Couldn't load the external resource」
原因:CSV 文件没放在 Neo4j 的 import 目录,或者路径用了绝对路径。 解决:把文件放到neo4j/import/下,Cypher 里用file:///文件名.csv相对路径。Docker 部署的话要确认 import 目录挂载正确。
5.3 现象:中文属性显示乱码
原因:CSV 编码不是 UTF-8,或者 Neo4j 启动参数没设编码。 解决:导出 CSV 时强制encoding="utf-8",Neo4j 配置文件里确认dbms.default_charset=utf-8。已经导入的乱码数据只能删了重导。
5.4 现象:多跳查询越来越慢,最后超时
原因:中间结果集没有限制,笛卡尔积爆炸。 解决:每一跳之后用WITH加LIMIT截断,或者把查询拆成多步,用 APOC 把中间结果存成临时节点。三跳以上查询尽量预计算成物化视图。
5.5 现象:关系边数对不上,少了或多了
原因:MERGE 时匹配条件不完整,或者源数据里有空值导致 MATCH 失败。 解决:导入前用 pandas 检查关联字段的空值,空值行直接丢弃。MERGE 时确保两端节点的唯一键都存在,用MATCH而不是OPTIONAL MATCH,匹配不到就跳过并记录日志。
6. 进阶技巧:用图谱做产业链风险传导查询
图谱建好之后,真正体现价值的是风险传导分析。比如某行业出现政策利空,怎么快速找到受影响的公司和产品?思路是从行业节点出发,反向查公司,再正向查产品,最后按营收占比排序。这个查询用两跳就能完成,但要注意关系方向。
// 输入行业代码,查询受影响的公司和产品,按营收占比降序 MATCH (i:Industry {code: $industryCode})<- [r:BELONGS_TO]-(c:Company) MATCH (c)-[:PRODUCES]->(p:Product) RETURN c.name AS company, r.revenueRatio AS ratio, collect(p.name) AS products ORDER BY ratio DESC LIMIT 100;$industryCode是参数,从外部传入。r.revenueRatio是关系边上的属性,排序用它比用公司注册资本更贴近业务。collect(p.name)把产品聚合成列表,一行一个公司。这个查询在 10w 节点、16w 边的规模下,命中索引后通常在百毫秒级返回。
验证图谱是否建对,我一般会跑三个检查:一是随机抽一家上市公司,看它的行业和产品是否合理;二是查一个行业下的公司数量,和外部数据源对一下量级;三是跑一次全图连通性检查,看有没有孤立节点。孤立节点往往是实体对齐没做好,或者关系边导入时匹配失败留下的。
从那以后我每次导入图数据,都强制先跑一遍唯一约束和空值检查,再分批导入,最后用连通性查询兜底。这套流程帮我省了不少返工时间。希望帮到你。
本文还有配套的精品资源,点击获取