药物靶点数据模型:药物节点、靶点节点、相互作用关系
好了,到了第四节课。前面三节课我们聊完了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就能脑补出整个图谱骨架的样子:
常见问题与设计建议
- 属性冗余怎么办? 我有一次把药物的所有别名都塞到一个节点里,结果查询时
WHERE d.name = 'Aspirin'找不到,因为别名是作为列表存的。后来我把别名单独做成:Alias节点,通过[:HAS_ALIAS]关联。这个模式的扩展性更好。 - 关系方向怎么定? 一般从药物指向靶点。如果后续需要反向查询(如“哪些药物作用于这个靶点”),Cypher里用
MATCH (d:Drug)-[:INTERACTS_WITH]->(t:Target {uniprot_id: 'P23219'})即可,不需要双向边。 - 要不要加唯一约束? 一定要。我见过一个库里有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%的返工时间。