药物靶点数据模型:药物节点、靶点节点、相互作用关系

好了,到了第四节课。前面三节课我们聊完了Neo4j的安装和基本操作,今天终于要动真格的了——设计药物靶点知识图谱的数据模型。

我个人习惯在动手建图之前,先用纸笔画一遍节点和关系长什么样。你别嫌老土,这个习惯帮我避过好几次坑。我记得有一次项目里,团队直接上手写Cypher建节点,结果跑了三天发现关系类型命名不统一,最后不得不重新导入数据。嗯,那种滋味可不好受。

所以今天我们就好好拆解一下:药物节点、靶点节点、以及它们之间的相互作用关系,到底该怎么设计、怎么存、怎么查。

数据模型的底层思维

你想想看,药物靶点知识图谱本质上就三个要素:

  • 实体——药物、靶点(蛋白质/基因)
  • 属性——描述实体的特征
  • 关系——连接实体的语义

说白了,就是把传统关系型数据库里那些散落在不同表里的数据,变成了一张网。这张网在Neo4j里跑起来特别快,尤其是做多跳查询的时候。

经验之谈: 节点标签用单数还是复数?我个人统一用单数,比如 :Drug 而不是 :Drugs。原因是Cypher查询里 MATCH (d:Drug) 读起来更自然,而且后续加索引也方便。

药物节点设计

先说药物。一个药物节点应该存什么?直接贴我常用的标签和属性表:

属性名 类型 说明
drugbank_id string DrugBank唯一标识,做节点主键
name string 通用名,比如“Ibuprofen”
smiles string 化学结构式SMILES表示
molecular_weight float 分子量(单位Da)
logP float 脂水分配系数,反映亲脂性
description string 简短描述,可选

标签我习惯用 :Drug。为什么不带“小分子”或“生物制剂”子类?因为一开始模型尽量扁平,后续需要再扩展子标签不迟。我遇到过一个项目,一上来就建了10多个子标签,结果很多节点既是小分子又是抑制剂,搞得查询时要写一大堆 OR,维护成本直线上升。

靶点节点设计

靶点通常是蛋白质,有时是RNA或DNA。我们以最常见的蛋白质靶点为例:

属性名 类型 说明
uniprot_id string UniProt ID,主键
name string 蛋白质全名,如“Cyclooxygenase-1”
gene_symbol string 基因符号,如“PTGS1”
protein_class string 蛋白质分类,如“Oxidoreductase”
organism string 物种,“Homo sapiens”
sequence string 氨基酸序列(可选,太长时建议单独存文件)

标签用 :Target。注意,我用的是 uniprot_id 而不是 target_id,因为UniProt ID是业界标准,方便和外部数据库做关联。我曾经犯过一个错:自己编了一套内部ID,结果和同事分享数据时对不上号,最后花了三天做映射。所以能用标准标识符就别搞自定义。

相互作用关系设计

这是整个图谱的核心——边。关系类型我用 :INTERACTS_WITH,方向从药物指向靶点。关系上要不要存属性?当然要!而且这里最能体现知识图谱的价值。

属性名 类型 说明
action_type string 作用类型,如“inhibitor”、“agonist”
affinity_type string 亲和力类型,如“IC50”、“Ki”、“Kd”
affinity_value float 亲和力数值,单位nM
reference string 来源文献或数据库ID(PubMed ID等)
confidence string 可信度等级,如“high”、“medium”
避坑: 我曾把亲和力数值存成字符串,结果后续做数值过滤时发现 MATCH ()-[r:INTERACTS_WITH]->() WHERE r.affinity_value < 100 居然不报错但结果全错。为啥?因为字符串比较是按字典序,“50”比“100”大。所以数值属性一定要用 float 或 int,别偷懒。

完整的Cypher建示例

看一段创建节点和关系的代码:

// 创建药物节点
CREATE (d:Drug {
  drugbank_id: "DB01050",
  name: "Ibuprofen",
  smiles: "CC(C)CC1=CC=C(C=C1)C(C)C(=O)O",
  molecular_weight: 206.28,
  logP: 3.97
})

// 创建靶点节点
CREATE (t:Target {
  uniprot_id: "P23219",
  name: "Cyclooxygenase-1",
  gene_symbol: "PTGS1",
  protein_class: "Oxidoreductase",
  organism: "Homo sapiens"
})

// 创建相互作用关系
MATCH (d:Drug {drugbank_id: "DB01050"})
MATCH (t:Target {uniprot_id: "P23219"})
CREATE (d)-[:INTERACTS_WITH {
  action_type: "inhibitor",
  affinity_type: "IC50",
  affinity_value: 0.5,
  reference: "PMID:12345678",
  confidence: "high"
}]->(t)

你看,建好三个语句,一对药物-靶点关系就进图了。实际项目中我一般先用 LOAD CSV 批量导入,但测试阶段用 CREATE 更直观。

数据模型的整体结构

我把刚才讲的内容归纳成一张关系图。你看着SVG就能脑补出整个图谱骨架的样子:

药物-靶点知识图谱核心数据模型 :Drug drugbank_id (PK) name smiles molecular_weight logP :Target uniprot_id (PK) name gene_symbol protein_class organism :INTERACTS_WITH action_type affinity_type affinity_value 说明:关系从药物指向靶点,表示“药物作用于靶点” 每个节点拥有唯一主键,关系可携带亲和力、作用类型等属性 实际应用中可扩展子节点(如疾病、通路),保持核心骨架不变

常见问题与设计建议

  1. 属性冗余怎么办? 我有一次把药物的所有别名都塞到一个节点里,结果查询时WHERE d.name = 'Aspirin'找不到,因为别名是作为列表存的。后来我把别名单独做成 :Alias 节点,通过 [:HAS_ALIAS] 关联。这个模式的扩展性更好。
  2. 关系方向怎么定? 一般从药物指向靶点。如果后续需要反向查询(如“哪些药物作用于这个靶点”),Cypher里用 MATCH (d:Drug)-[:INTERACTS_WITH]->(t:Target {uniprot_id: 'P23219'}) 即可,不需要双向边。
  3. 要不要加唯一约束? 一定要。我见过一个库里有3个一模一样的“Ibuprofen”节点,查询结果总是多条,排查半天才知道是导入时忘了建约束。推荐在导入数据前就执行:
CREATE CONSTRAINT unique_drug_id FOR (d:Drug) REQUIRE d.drugbank_id IS UNIQUE;
CREATE CONSTRAINT unique_target_id FOR (t:Target) REQUIRE t.uniprot_id IS UNIQUE;
核心要点回顾:
  • 节点:药物(:Drug)和靶点(:Target),各用标准ID做主键
  • 关系::INTERACTS_WITH,方向药物→靶点,携带作用类型、亲和力等属性
  • 设计原则:扁平标签、标准标识符、数值属性用数值类型、提前建约束
  • 扩展思路:别名节点、疾病节点、通路节点都可以挂在现有模型上,不影响核心骨架

好,这就是第四节课的全部内容。这个数据模型看似简单,但它是整个药物发现知识图谱的基石。你往后加疾病、加实验、加文献,底层的Drug和Target节点纹丝不动。记住,一开始把模型做稳,后面省掉80%的返工时间。