ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

小麦知识图谱构建实战:数据源、Python清洗与Neo4j入库

小麦知识图谱构建实战:数据源、Python清洗与Neo4j入库 简介这份资源围绕小麦知识图谱的构建提供从数据源整理到代码实现的完整材料面向农业科研人员、知识图谱初学者及希望将图谱技术落地到垂直领域的开发者。压缩包共27个文件以12个Python脚本、11个文本数据文件为主另含2个CSV表格、1个JSON图谱数据与1份README说明整体约2.4MB结构上分为数据预处理、爬虫采集、Neo4j导入与图谱查询等模块。已有340人学习下载。通过其中的预处理脚本、疾病与肥料相关处理逻辑、数据清洗与实体关系转换代码读者可以掌握非结构化农业数据向图谱节点与边转化的具体做法并借助导入脚本完成图数据库存储与查询验证。资源还附带原始数据与处理后数据便于对照理解数据流转过程适合作为知识图谱入门到实战的参考案例。1. 小麦知识图谱的数据源及代码从零散数据到可查询图谱的落地路径小麦育种和农艺研究里数据散落在品种审定公告、农艺性状表、病害记录、基因注释文件、栽培试验报告里格式从 Excel 到 CSV 到 GFF3 都有。想把这些东西串成一张能查询、能推理的知识图谱第一步不是选图数据库而是搞清楚数据源到底有哪几类、每类怎么清洗、实体和关系怎么定义。这篇内容围绕小麦知识图谱的数据源及代码展开把品种、性状、基因、病害、试验地点这几类核心数据的采集、清洗、本体建模、入库和查询代码讲清楚。适合做农业信息化、育种数据管理、知识图谱构建的工程师也适合想用 Neo4j 把业务数据落成图谱的开发者。读完你能拿到一套可复现的流程从原始表格到 CSV 中间层再到 Cypher 入库脚本和查询验证。2. 小麦知识图谱的数据源盘点与本体设计小麦知识图谱能不能用八成取决于数据源盘得够不够细。很多人一上来就写代码结果实体类型定义混乱后面查询全是坑。这一章先把数据源分类讲清楚再落到本体建模最后给出实体和关系的定义表。2.1 五类核心数据源及各自的清洗难点小麦领域的数据源大致分五类每类的结构差异很大清洗策略也不同。第一类是品种审定数据。来源通常是审定公告的表格字段包括品种名称、审定编号、选育单位、亲本组合、适宜种植区域。难点在于亲本组合是自由文本比如“济麦22/周麦16”需要拆成两个亲本实体加一个杂交关系。审定编号格式不统一有的带年份前缀有的不带得写正则归一化。第二类是农艺性状数据。这类数据一般来自区域试验汇总表字段有株高、穗长、千粒重、生育期、亩穗数。难点是单位不统一株高有厘米也有米千粒重有克也有毫克必须做单位换算并记录原始单位。缺失值多不能简单填零要标记为“未测定”。第三类是基因与分子标记数据。来源可能是 GFF3 注释文件或 QTL 定位结果表字段包括基因 ID、染色体位置、性状关联、标记类型。难点是基因 ID 在不同数据库里命名不一致需要建一张 ID 映射表做对齐。第四类是病害与抗性数据。字段有病害名称、抗性等级、鉴定年份、鉴定地点。难点是抗性等级有文字描述高抗、中抗、感病和数值分级两套体系要统一成数值编码。第五类是试验地点与环境数据。包括地点名称、经纬度、海拔、年均温、降水。难点是地点名称有别名比如“郑州”和“郑州市”要合并经纬度缺失的需要用地理编码补全。把这五类数据源理清楚之后你会发现它们共享一批核心实体品种、基因、性状、病害、地点。这就是本体设计的起点。2.2 本体建模实体、关系与属性怎么定本体建模不是学术游戏它直接决定你后面 Cypher 查询写起来顺不顺。小麦知识图谱我一般定义六类节点和七类关系。节点类型包括Variety品种、Gene基因、Trait性状、Disease病害、Location地点、Trial试验。关系类型包括PARENT_OF亲本关系、HAS_TRAIT品种具有性状、ASSOCIATED_WITH基因关联性状、RESISTANT_TO品种抗病害、LOCATED_AT试验位于地点、TESTED_IN品种参与试验、ENCODES基因编码性状。属性设计上品种节点带 name、approval_code、breeder、release_year性状节点带 name、unit、category基因节点带 gene_id、chromosome、position。这里的关键决策是性状值不放在品种节点上而是放在关系属性里。因为同一品种在不同试验里性状值不同如果把千粒重直接挂在品种节点上你就丢了试验维度。下面这张表是实体和关系的定义可以直接作为建图依据。实体/关系类型关键属性说明Variety节点name, approval_code, breeder品种Gene节点gene_id, chromosome, position基因Trait节点name, unit, category性状Disease节点name, pathogen_type病害Location节点name, lat, lon, altitude地点Trial节点trial_id, year, season试验HAS_TRAIT关系value, unit, year品种-性状ASSOCIATED_WITH关系evidence, p_value基因-性状RESISTANT_TO关系level, year品种-病害PARENT_OF关系cross_year品种-品种提示本体不是一次定死的建议先用这批实体跑通一条查询链路再根据查询需求增补属性。过早追求完备本体往往卡在建模阶段出不来。2.3 从原始表格到中间层 CSV 的字段映射本体定好之后下一步是把原始数据映射成中间层 CSV。中间层的作用是解耦原始数据怎么变只改映射脚本入库逻辑不动。我一般建五个中间文件variety.csv、trait.csv、gene.csv、disease.csv、location.csv再加一个关系文件 relation.csv。每个文件第一行是表头字段名和本体属性对齐。品种表的映射规则审定公告里的“品种名称”映射到 name“审定编号”映射到 approval_code“选育单位”映射到 breeder“审定年份”映射到 release_year。亲本组合单独拆到 relation.csv关系类型写 PARENT_OF。性状表的映射规则每一行是一个品种在一个试验里的一个性状值字段包括 variety_name、trait_name、value、unit、year、location。这里 variety_name 必须和品种表的 name 完全一致否则入库时匹配不上。关系表的映射规则统一用 source_type、source_name、relation_type、target_type、target_name、properties 六列。properties 用 JSON 字符串存关系属性比如{value: 45.2, unit: g, year: 2022}。这个中间层的设计好处是不管原始数据是 Excel 还是数据库导出只要映射脚本把它转成这套 CSV后面的入库代码完全复用。3. 用 Python 做数据清洗与实体对齐的代码实现数据源盘完、本体定完接下来是代码环节。这一章给出可运行的 Python 清洗脚本覆盖品种名称归一化、性状单位换算、基因 ID 对齐三个核心步骤每个步骤都有代码和参数说明。3.1 品种名称归一化与亲本拆分脚本品种名称的脏数据主要有三类带空格、带括号别名、繁简混用。亲本组合的拆分则需要处理多种分隔符。import re import pandas as pd def normalize_variety_name(name: str) - str: 归一化品种名称去空格、去括号别名、繁转简 if pd.isna(name): return # 去掉首尾空格和全角空格 name name.strip().replace(\u3000, ) # 去掉括号及括号内内容如济麦22原代号 name re.sub(r[(].*?[)], , name) # 去掉多余空格 name re.sub(r\s, , name) return name def split_parents(combo: str) - list: 拆分亲本组合支持 / 、 × 三种分隔符 if pd.isna(combo) or combo : return [] # 统一分隔符为 / combo re.sub(r[、×xX], /, combo) parents [normalize_variety_name(p) for p in combo.split(/)] # 过滤空字符串 return [p for p in parents if p] # 示例 raw pd.DataFrame({ 品种名称: [ 济麦22 , 周麦16国审, 郑麦9023], 亲本组合: [济麦22/周麦16, 豫麦13、周麦16, 郑麦9023×豫麦18] }) raw[品种名称] raw[品种名称].apply(normalize_variety_name) raw[亲本列表] raw[亲本组合].apply(split_parents) print(raw)这段代码的逻辑是normalize_variety_name 先做基础清洗再用正则去掉括号别名最后压缩空格。split_parents 先把各种分隔符统一成斜杠再逐个归一化。参数上正则[(].*?[)]用了非贪婪匹配避免把多个括号内容一起吃掉。实际使用时如果品种名称里有必须保留的括号信息比如“中麦175抗病”就要调整正则只去掉特定后缀。3.2 性状单位换算与缺失值标记性状数据的单位混乱是高频问题。下面这个脚本把株高统一成厘米千粒重统一成克缺失值标记为 None 而不是零。import pandas as pd # 单位换算表key 是原始单位value 是换算系数 UNIT_CONVERSION { 株高: {cm: 1.0, 厘米: 1.0, m: 100.0, 米: 100.0}, 千粒重: {g: 1.0, 克: 1.0, mg: 0.001, 毫克: 0.001}, 穗长: {cm: 1.0, 厘米: 1.0, mm: 0.1, 毫米: 0.1}, } def convert_trait_value(trait_name: str, value, unit: str): 按性状类型换算单位缺失值返回 None if pd.isna(value) or pd.isna(unit): return None trait_map UNIT_CONVERSION.get(trait_name) if trait_map is None: return value # 未定义换算规则的性状原样返回 factor trait_map.get(unit.strip()) if factor is None: raise ValueError(f未知单位: {unit} for {trait_name}) return round(float(value) * factor, 2) # 示例 df pd.DataFrame({ trait_name: [株高, 株高, 千粒重, 千粒重], value: [85, 0.85, 45.2, 45200], unit: [cm, m, g, mg] }) df[value_std] df.apply( lambda r: convert_trait_value(r[trait_name], r[value], r[unit]), axis1 ) print(df)逻辑说明UNIT_CONVERSION 用嵌套字典按性状类型组织换算系数这样不同性状可以有不同单位体系。convert_trait_value 先判空再查换算表遇到未知单位直接抛异常而不是静默返回原值这是为了避免脏数据悄悄入库。参数上round 保留两位小数如果业务需要更高精度可以调整。缺失值返回 None在写 CSV 时会变成空字段入库时 Cypher 里用WHERE value IS NOT NULL过滤。3.3 基因 ID 对齐与关系表生成基因 ID 在不同来源里命名不一致需要建映射表。下面脚本演示如何用映射表对齐并生成统一的关系表。import pandas as pd import json # 基因 ID 映射表不同来源的 ID 映射到统一 ID GENE_ID_MAP { TraesCS1A02G001: TaG001, Traes_1AL_123456: TaG001, gene1A001: TaG001, } def align_gene_id(raw_id: str) - str: 对齐基因 ID未命中映射表的返回原值并记录 if pd.isna(raw_id): return return GENE_ID_MAP.get(raw_id.strip(), raw_id.strip()) def build_relation_row(source_type, source_name, rel_type, target_type, target_name, props: dict): 构造统一关系行 return { source_type: source_type, source_name: source_name, relation_type: rel_type, target_type: target_type, target_name: target_name, properties: json.dumps(props, ensure_asciiFalse), } # 示例基因-性状关联 gene_trait pd.DataFrame({ gene_id: [TraesCS1A02G001, Traes_1AL_123456], trait_name: [千粒重, 株高], p_value: [0.01, 0.05], evidence: [QTL定位, 关联分析] }) gene_trait[gene_id_std] gene_trait[gene_id].apply(align_gene_id) relations [] for _, row in gene_trait.iterrows(): relations.append(build_relation_row( Gene, row[gene_id_std], ASSOCIATED_WITH, Trait, row[trait_name], {p_value: row[p_value], evidence: row[evidence]} )) rel_df pd.DataFrame(relations) print(rel_df)逻辑说明GENE_ID_MAP 是手工维护的映射字典实际项目中可能来自数据库表或配置文件。align_gene_id 未命中时返回原值这样不会丢数据但需要在日志里记录未命中项后续人工补映射。build_relation_row 把关系属性序列化成 JSON 字符串方便写入 CSV 后在 Cypher 里用apoc.convert.fromJsonMap解析。参数上ensure_asciiFalse 保证中文正常显示。注意基因 ID 映射表是知识图谱里最容易失控的部分。建议每次入库前统计未命中率超过 5% 就先停下来补映射不要带着大量未对齐 ID 入库。4. Neo4j 入库脚本与 Cypher 查询验证数据清洗完中间层 CSV 就绪接下来是入库和查询。这一章给出 Neo4j 的约束创建、批量导入脚本和三类典型查询覆盖从建图到验证的完整链路。4.1 建约束、批量导入节点与关系的 Cypher 脚本入库前先建唯一性约束避免重复节点。下面脚本用 Cypher 的 LOAD CSV 批量导入。// 创建唯一性约束 CREATE CONSTRAINT variety_name IF NOT EXISTS FOR (v:Variety) REQUIRE v.name IS UNIQUE; CREATE CONSTRAINT gene_id IF NOT EXISTS FOR (g:Gene) REQUIRE g.gene_id IS UNIQUE; CREATE CONSTRAINT trait_name IF NOT EXISTS FOR (t:Trait) REQUIRE t.name IS UNIQUE; CREATE CONSTRAINT disease_name IF NOT EXISTS FOR (d:Disease) REQUIRE d.name IS UNIQUE; CREATE CONSTRAINT location_name IF NOT EXISTS FOR (l:Location) REQUIRE l.name IS UNIQUE; // 导入品种节点 LOAD CSV WITH HEADERS FROM file:///variety.csv AS row MERGE (v:Variety {name: row.name}) SET v.approval_code row.approval_code, v.breeder row.breeder, v.release_year toInteger(row.release_year); // 导入性状节点 LOAD CSV WITH HEADERS FROM file:///trait.csv AS row MERGE (t:Trait {name: row.trait_name}) SET t.unit row.unit, t.category row.category; // 导入基因节点 LOAD CSV WITH HEADERS FROM file:///gene.csv AS row MERGE (g:Gene {gene_id: row.gene_id}) SET g.chromosome row.chromosome, g.position toInteger(row.position);逻辑说明MERGE 保证节点不存在时创建、存在时匹配配合唯一性约束可以安全重复执行。SET 负责更新属性。toInteger 做类型转换CSV 读进来默认是字符串。参数上file:///指向 Neo4j 的 import 目录需要把 CSV 放到该目录下。如果数据量大建议用CALL apoc.periodic.iterate分批提交避免单事务内存溢出。关系导入脚本如下// 导入品种-性状关系 LOAD CSV WITH HEADERS FROM file:///relation.csv AS row WITH row WHERE row.relation_type HAS_TRAIT MATCH (v:Variety {name: row.source_name}) MATCH (t:Trait {name: row.target_name}) MERGE (v)-[r:HAS_TRAIT]-(t) SET r.value toFloat(apoc.convert.fromJsonMap(row.properties).value), r.unit apoc.convert.fromJsonMap(row.properties).unit, r.year toInteger(apoc.convert.fromJsonMap(row.properties).year); // 导入基因-性状关系 LOAD CSV WITH HEADERS FROM file:///relation.csv AS row WITH row WHERE row.relation_type ASSOCIATED_WITH MATCH (g:Gene {gene_id: row.source_name}) MATCH (t:Trait {name: row.target_name}) MERGE (g)-[r:ASSOCIATED_WITH]-(t) SET r.p_value toFloat(apoc.convert.fromJsonMap(row.properties).p_value), r.evidence apoc.convert.fromJsonMap(row.properties).evidence;逻辑说明先用 WITH 过滤关系类型再 MATCH 两端节点MERGE 关系。关系属性从 JSON 字符串解析。参数上apoc.convert.fromJsonMap 需要 APOC 插件支持Neo4j 5.x 需要单独安装。如果不想依赖 APOC可以在 Python 侧把属性拆成独立列Cypher 里直接读列。4.2 三类典型查询品种溯源、性状对比、基因关联入库完成后用查询验证图谱是否可用。下面三类查询覆盖最常见的业务需求。第一类品种溯源查一个品种的所有亲本以及亲本的亲本。MATCH path (v:Variety {name: 济麦22})-[:PARENT_OF*1..3]-(ancestor:Variety) RETURN path;这条查询用变长路径*1..3向上追溯三代亲本。参数上1..3控制追溯深度太深会拖慢查询建议按需调整。第二类性状对比查两个品种在同一性状上的值。MATCH (v:Variety)-[r:HAS_TRAIT]-(t:Trait {name: 千粒重}) WHERE v.name IN [济麦22, 周麦16] RETURN v.name AS variety, r.value AS value, r.unit AS unit, r.year AS year ORDER BY v.name, r.year;这条查询返回两个品种的千粒重历史数据按年份排序。参数上WHERE 里的品种列表可以动态传入。第三类基因关联查与某个性状关联的所有基因。MATCH (g:Gene)-[r:ASSOCIATED_WITH]-(t:Trait {name: 千粒重}) WHERE r.p_value 0.05 RETURN g.gene_id AS gene, g.chromosome AS chr, r.p_value AS p_value, r.evidence AS evidence ORDER BY r.p_value;这条查询按 p 值过滤显著关联返回基因列表。参数上p_value 阈值根据业务调整0.05 是常用显著性水平。4.3 用 Python 驱动 Neo4j 做批量校验除了在 Neo4j Browser 里查生产环境更常用 Python 驱动做批量校验。下面脚本检查入库后节点数和关系数是否符合预期。from neo4j import GraphDatabase URI bolt://localhost:7687 AUTH (neo4j, password) def count_nodes_and_rels(driver): 统计各类节点和关系数量 queries { Variety: MATCH (n:Variety) RETURN count(n) AS c, Gene: MATCH (n:Gene) RETURN count(n) AS c, Trait: MATCH (n:Trait) RETURN count(n) AS c, HAS_TRAIT: MATCH ()-[r:HAS_TRAIT]-() RETURN count(r) AS c, ASSOCIATED_WITH: MATCH ()-[r:ASSOCIATED_WITH]-() RETURN count(r) AS c, } with driver.session() as session: for name, q in queries.items(): result session.run(q) print(f{name}: {result.single()[c]}) driver GraphDatabase.driver(URI, authAUTH) count_nodes_and_rels(driver) driver.close()逻辑说明用官方 neo4j 驱动连接逐个执行统计查询。参数上URI 和 AUTH 按实际环境改。这个脚本适合放在入库流程末尾做自动校验节点数或关系数异常时报警。提示批量校验不要只统计总数还要抽查几条具体记录。总数对得上但关系挂错节点的情况很常见尤其是 MATCH 写错属性名的时候。5. 小麦知识图谱构建的避坑与排查清单这一章记录我在实际项目里踩过的坑每条按现象、原因、解决写。这些坑不解决图谱要么查不出结果要么结果不可信。5.1 品种名称不一致导致关系挂空现象入库后 HAS_TRAIT 关系数量远少于预期查询品种性状返回空。原因品种表里的名称是“济麦22”性状表里写的是“济麦 22”或“济麦22号”MERGE 时匹配不上关系创建失败但 Cypher 不报错。解决入库前统一做名称归一化用第 3 章的 normalize_variety_name 处理所有来源。入库后跑一条校验查询统计没有 HAS_TRAIT 关系的品种数量超过阈值就回查中间层 CSV。5.2 单位未换算导致数值失真现象查询千粒重有的品种是 45有的是 45000量级差三个数量级。原因原始数据里单位混用克和毫克没统一直接入库。解决清洗阶段强制走单位换算函数未定义换算规则的性状抛异常而不是放行。入库后在关系属性里保留原始单位和换算后单位两个字段方便追溯。5.3 基因 ID 未对齐导致关联断裂现象基因-性状关联查询返回的基因数量很少大量已知关联丢失。原因不同来源的基因 ID 命名体系不同映射表覆盖不全未命中的 ID 以原值入库查询时匹配不上。解决入库前统计未命中率超过 5% 停下来补映射。映射表建议用独立 CSV 维护每次入库前加载而不是硬编码在脚本里。5.4 LOAD CSV 路径和权限问题现象Cypher 执行报错“Couldnt load the external resource”或文件找不到。原因CSV 没放在 Neo4j 的 import 目录或者文件权限不对Neo4j 进程读不了。解决确认neo4j.conf里的dbms.directories.import配置把 CSV 放进去。Linux 下注意文件属主和读权限用chmod 644放开。如果 CSV 在远程先用 scp 传到服务器再导入。5.5 大文件导入内存溢出现象导入几万行关系时 Neo4j 报堆内存不足事务回滚。原因单条 LOAD CSV 语句在一个事务里处理全部数据数据量大时内存扛不住。解决用CALL apoc.periodic.iterate分批提交每批 5000 到 10000 行。或者把大 CSV 拆成多个小文件逐个导入。调整dbms.memory.heap.max_size也能缓解但治标不治本。6. 让图谱真正可用的两个进阶技巧图谱建起来只是起点能不能持续用、查得准靠的是两个习惯一是把校验做成自动化二是把查询封装成参数化接口。先说自动化校验。我一般会在入库脚本后面挂一个 Python 校验模块除了统计节点和关系数量还做三类检查孤立节点检查没有任本文还有配套的精品资源点击获取
返回列表