Python-本体论教程-三-

260 阅读56分钟

Python 本体论教程(三)

原文:Ontologies with Python

协议:CC BY-NC-SA 4.0

九、使用医学术语、PyMedTermino 和 UMLS

在这一章中,我们将看到如何使用 PyMedTermino 将 UMLS 的主要医学术语导入 Python,PyMedTermino 是一个允许将这些术语集成到 Owlready 中的模块。我们还将看到如何使用 UMLS 统一概念(CUI)将这些术语联系在一起。

9.1 UML

UMLS(统一医学语言系统)是来自生物医学领域的 400 多个术语的集合。UMLS 还集成了不同术语之间的映射。UMLS 由美国国家医学图书馆(NLM)制作,可在以下地址在线注册后免费下载:

www.nlm.nih.gov/research/umls/licensedcontent/umlsknowledgesources.html

但是,请注意,UMLS 中包含的某些术语不能在某些国家自由使用。SNOMED CT 公司的情况尤其如此,该公司必须获得由国家或机构或公司支付的许可证。

在撰写本文时,最新的版本是 2019AB(完整版(umls-2019AB-full.zip))。要将 UMLS 与 Owlready 和 PyMedTermino 一起使用,您需要下载这个文件(大约 4.8 GB)。但是,不需要解压(PyMedTermino 会帮你做)。

9.2 从 UMLS 引进术语

PyMedTermino 是一个 Python 模块,允许访问医学术语。PyMedTermino 的第 2 版直接包含在 Owlready 中,所以不需要安装。但是,请注意,导入 UMLS 数据需要 Python 的 3.7 版本(或更高版本)(另一方面,一旦 UMLS 数据被导入,如果您愿意,您可以在 Python 3.6 中使用它)。

owlready2.pymedtermino2模块允许您通过全局函数import_umls()将全部或部分 UMLS 数据导入 Owlready quadstore:

import_umls("./path/to/umls-2019AB-full.zip",↲
            terminologies = [...],↲
            langs = [...] )

该函数的第一个参数是包含 UMLS 数据的 ZIP 文件的路径,这是我们之前下载的。在前面的示例中,这是一个本地路径,但也可以是一个完整的路径,例如,“/home/jbl Amy/download/umls-2020 aa-full . zip”或“C:\ \ Downloads \ \ umls-2020 aa-full . zip”,这取决于您保存该文件的位置。

第二个参数是要导入的术语列表。如果缺少此参数,将导入所有术语。以下网页列出了 UMLS 的术语和相关代码:

www.nlm.nih.gov/research/umls/sourcereleasedocs/index.html

第三个参数指示要导入的语言,例如,“en”表示英语,“fr”表示法语。如果缺少此参数,将导入所有语言。

例如,我们可以导入术语 CIM10 和 SNOMED CT(英文版,UMLS 代码为 ICD10 和 SNOMEDCT_US)以及 CUI UMLS(被视为伪术语),如下所示:

>>> from owlready2 import *
>>> from owlready2.pymedtermino2 import *
>>> from owlready2.pymedtermino2.umls import *

>>> default_world.set_backend(filename = "pymedtermino.sqlite3")
>>> import_umls("umls-2020AA-full.zip",↲
                terminologies = ["ICD10", "SNOMEDCT_US", "CUI")
>>> PYM = get_ontology("http://PYM/").load()
>>> default_world.save()

UMLS 数据导入大约需要 5-10 分钟。“PYM”在这里是“PyMedTermino”的缩写。

默认情况下,PyMedTermino 激活注释属性label(对应于术语概念的术语)和synonyms(对应于同义词)的全文搜索(参见 8.7)。如果您不想激活它,您必须在调用import_umls()功能时添加选项fts_index = False

请注意,UMLS 包括几个术语翻译,例如,ICD10 有德语(代码“DMDICD10”)和荷兰语(代码“ICD10DUT”)。但是,不包括法文翻译。PyMedTermino 2 有一个用于导入法语 ICD10(代码“CIM10”)的特定模块,可以按如下方式使用:

>>> from owlready2.pymedtermino2.icd10_french import *
>>> import_icd10_french()
>>> default_world.save()

9.3 初始进口后装载术语

显然,下一次我们想要使用导入的术语时,我们将不再需要调用导入函数。我们现在只需要以下三行代码:

>>> from owlready2 import *
>>> default_world.set_backend(filename = "pymedtermino.sqlite3")
>>> PYM = get_ontology("http://PYM/").load()

这三行代码重新加载了 quadstore(使用set_backend()方法)和 PYM (PyMedTermino)本体。不要忘记调用load();有必要加载与本体相关联的 Python 方法。

9.4 使用 ICD10

PyMedTermino 使用相同的界面提供对所有术语的访问。我们将在这里看到 ICD10 和 SNOMED CT 术语,但对于其他术语,功能仍然类似。

国际疾病分类第 10 版(ICD10)是一个被广泛使用的疾病分类。例如,它在法国用于医院的医疗经济编码。ICD10 包括大约 12,000 个概念。它以树的形式组织,21 个根概念对应于疾病的主要章节:癌症、传染病、心血管疾病、肺病等等。(注意,在美国,ICD9(第 9 版)仍在大量使用。可以使用术语代码“ICD9CM”获得。)

我们可以如下获得英文 ICD10 术语:

>>> ICD10 = PYM["ICD10"]

>>> ICD10
PYM["ICD10"] # ICD10

>>> ICD10.name
'ICD10'

PyMedTermino 以如下方式显示概念:“terminals[code]# concept label”(对于有多个标签的概念,只显示一个,从首选标签中选择)。请注意,概念标签前面有一个#字符,因此如果您在 Python 中复制粘贴概念,它将被视为注释。这允许复制粘贴 PyMedTermino 概念或概念列表。

术语对象和术语概念是已经存在的类。因此,我们可以使用类方法来操作术语,例如,subclasses()方法可以获得 ICD10 的子类,也就是说,前面提到的 21 个疾病章节:

>>> list(ICD10.subclasses())
[ ICD10["K00-K93.9"] # Diseases of the digestive system
, ICD10["C00-D48.9"] # Neoplasms
...]

但是,PyMedTermino 提供了额外的属性和方法来方便术语的操作。例如,children属性直接返回子概念列表,而不必像以前那样通过生成器。此外,子概念按代码排序(在 UMLS 并非总是如此),如下例所示:

>>> ICD10.children
[ ICD10["A00-B99.9"] # Certain infectious and parasitic diseases
, ICD10["C00-D48.9"] # Neoplasms
, ICD10["D50-D89.9"] # Diseases of blood and blood-forming organs and
                     # certain disorders involving the immune mechanisms
, ICD10["E00-E90.9"] # Endocrine, nutritional and metabolic diseases
, ICD10["F00-F99.9"] # Mental, behavioural disorders
, ICD10["G00-G99.9"] # Diseases of the nervous system
, ICD10["H00-H59.9"] # Diseases of the eye and adnexa
, ICD10["H60-H95.9"] # Diseases of the ear and mastoid process
, ICD10["I00-I99.9"] # Diseases of the circulatory system
, ICD10["J00-J99.9"] # Diseases of the respiratory system
, ICD10["K00-K93.9"] # Diseases of the digestive system
, ICD10["L00-L99.9"] # Diseases of the skin and subcutaneous tissue
, ICD10["M00-M99.9"] # Diseases of the musculoskeletal system and
                     # connective tissue
, ICD10["N00-N99.9"] # Diseases of the genitourinary system
, ICD10["O00-O99.9"] # Pregnancy, childbirth and the puerperium
, ICD10["P00-P96.9"] # Certain conditions originating in the
                     # perinatal period
, ICD10["Q00-Q99.9"] # Congenital malformations, deformations
                     # and chromosomal abnormalities
, ICD10["R00-R99.9"] # Symptoms, signs and abnormal clinical and
                     # laboratory findings, not elsewhere classified

, ICD10["S00-T98.9"] # Injury, poisoning and certain other
                     # consequences of external causes
, ICD10["V01-Y98.9"] # External causes of morbidity and mortality
, ICD10["Z00-Z99.9"] # Factors influencing health status and
                     # contact with health services
]

我们可以在层次结构中向下显示,例如,第一章(传染病)的儿童:

>>> ICD10.children[0].children
[ ICD10["A00-A09.9"] # Intestinal infectious diseases
, ICD10["A15-A19.9"] # Tuberculosis
, ICD10["A20-A28.9"] # Certain zoonotic bacterial diseases
, ICD10["A30-A49.9"] # Other bacterial diseases
, ICD10["A50-A64.9"] # Infections with a predominantly sexual mode
                     # of transmission
...]

为了从代码中直接访问一个概念,我们可以索引术语。例如,在 ICD10 中,编码为“I10”的概念对应于原发性高血压:

>>> ICD10["I10"]
ICD10["I10"] # Essential (primary) hypertension

派梅特米诺将 IRI 与每个概念联系起来,形式为“http://PYM/ / ”, for example:

>>> ICD10["I10"].iri
'http://PYM/ICD10/I10' 

因此,概念的名称(或标识符)与其代码相对应:

>>> ICD10["I10"].name
'I10'

terminology属性用于获取概念所属的术语:

>>> ICD10["I10"].terminology
PYM["ICD10"] # ICD10

概念标签可通过首选标签的label注释和其他标签的synonyms注释进行访问;这些 OWL 注释可以作为 Python 属性来访问:

>>> ICD10["I10"].label
['Essential (primary) hypertension']
>>> ICD10["I10"].synonyms
[]

根据术语的不同,概念可以有一个或多个标签和零个或多个同义词(在 ICD10 中,概念只有一个标签,没有同义词)。

parents属性提供了对父概念的访问(即更一般的概念):

>>> ICD10["I10"].parents
[ICD10["I10-I15.9"] # Hypertensive diseases
]

ICD10 是一个单轴分类,也就是说,每个概念只有一个亲本(除了没有亲本的主要章节)。然而,PyMedTermino 可以用相同的接口处理所有的术语;这就是为什么在 CIM10 中,parents属性只返回一个父对象的列表。

ancestor_concepts()descendant_concepts()方法分别返回祖先和后代概念的列表。它们类似于ancestors()descendants();但是,它们返回列表(而不是集合),它们只返回 UMLS 概念(特别是,ancestor_concepts()返回的列表不包括Thing)。

>>> ICD10["I10"].ancestor_concepts()
[ ICD10["I10"]       # Essential (primary) hypertension
, ICD10["I10-I15.9"] # Hypertensive diseases
, ICD10["I00-I99.9"] # Diseases of the circulatory system
]
>>> ICD10["I00-I99.9"].descendant_concepts()
[ ICD10["I00-I99.9"] # Diseases of the circulatory system
, ICD10["I00-I02.9"] # Acute rheumatic fever
, ICD10["I00"] # Rheumatic fever without mention of heart↲ involvement
, ICD10["I01"] # Rheumatic fever with heart involvement
, ICD10["I01.0"] # Acute rheumatic pericarditis
, ICD10["I01.1"] # Acute rheumatic endocarditis
, ICD10["I01.2"] # Acute rheumatic myocarditis
, ICD10["I01.8"] # Other acute rheumatic heart disease
, ICD10["I01.9"] # Acute rheumatic heart disease, unspecified
, ICD10["I02"] # Rheumatic chorea
, ICD10["I02.0"] # Rheumatic chorea with heart involvement

...]

默认情况下,这两种方法在它们返回的列表中包含初始概念。如果要避免这种情况,必须使用可选参数include_self = False,例如:

>>> ICD10["I10"].ancestor_concepts(include_self = False)
[ ICD10["I10-I15.9"] # Hypertensive diseases
, ICD10["I00-I99.9"] # Diseases of the circulatory system
]

当应用于术语对象时,descendant_concepts()方法还可以浏览术语的所有概念(注意,这需要加载所有 CIM10 概念,即内存中超过 10,000 个概念,这需要一些时间!):

>>> ICD10.descendant_concepts(include_self = False)
[ ICD10["A00-B99.9"] # Certain infectious and parasitic↲ diseases
, ICD10["A00-A09.9"] # Intestinal infectious diseases
, ICD10["A00"] # Cholera
, ICD10["A00.0"] # Cholera due to Vibrio cholerae 01, biovar↲ cholerae
...]

使用 Python 函数issubclass()可以测试一个概念是否是另一个概念的后代:

>>> issubclass(ICD10["I10"], ICD10["I00-I99.9"])
True

search()方法允许您通过标签和同义词搜索概念。字符“*”可以用作单词末尾的通配符,并且可以包括由空格分隔的几个关键字(至于这种方法所基于的全文搜索,请参见 8.7)。例如,我们可以用一个以“高血压”开头的词来搜索所有概念:

>>> ICD10.search("hypertension*")
[ ICD10["K76.6"] # Portal hypertension
, ICD10["I15.0"] # Renovascular hypertension
, ICD10["G93.2"] # Benign intracranial hypertension
, ICD10["I10"] # Essential (primary) hypertension
, ICD10["I27.0"] # Primary pulmonary hypertension
, ICD10["I15"] # Secondary hypertension
, ICD10["I15.9"] # Secondary hypertension, unspecified
...]

同样,我们可以用一个以“高血压”开头的词和另一个以“pulmo”开头的词来搜索所有概念:

>>> ICD10.search("hypertension* pulmo*")
[ICD10["I27.0"] # Primary pulmonary hypertension
]

9.5 使用 SNOMED CT

SNOMED CT(医学系统命名法—临床术语)是比 ICD10 更丰富、更完整的医学术语。注意,如前所述,SNOMED CT 在某些国家不能自由使用。

与 ICD10 一样,我们可以访问 SNOMED CT 术语及其概念,以及标签、父代、子代、祖先和后代概念。

>>> SNOMEDCT_US = PYM["SNOMEDCT_US"]

>>> SNOMEDCT_US["45913009"]
SNOMEDCT_US["45913009"]  # Laryngitis

>>> SNOMEDCT_US["45913009"].parents
[ SNOMEDCT_US["129134004"] # Inflammatory disorder of
                           # upper respiratory tract
, SNOMEDCT_US["363169009"] # Inflammation of specific body organs
, SNOMEDCT_US["60600009"] # Disorder of the larynx
]

>>> SNOMEDCT_US["45913009"].children
[ SNOMEDCT_US["1282001"] # Perichondritis of larynx
, SNOMEDCT_US["14969004"] # Catarrhal laryngitis
, SNOMEDCT_US["17904003"] # Hypertrophic laryngitis
...]

SNOMED CT 定义了标签(label)和同义词(synonyms):

>>> SNOMEDCT_US["45913009"].label
['Laryngitis']

>_>_> SNOMEDCT_US["45913009"].synonyms
['Laryngitis (disorder)']

与 ICD10 不同,SNOMED CT 允许一个概念有几个父概念:因此它是一个多轴术语。在前面的例子中,概念“喉炎”有三个父代:“炎性上呼吸道疾病”、“特定器官炎症”和“喉部疾病”。

此外,SNOMED CT 不仅限于疾病:它还描述解剖结构(器官、器官的一部分等。,称为“身体结构”或“发现部位”),形态学(也就是说,疾病的类型,“相关形态学”),活体物种,化学物质,等等。SNOMED CT 还包括连接这些不同元素的横向链接。

这个信息在概念的父类中找到,以限制的形式(类型为一些或【仅 ):

>>> SNOMEDCT_US["45913009"].is_a
[ SNOMEDCT_US["363169009"] # Inflammation of specific body organs
, SNOMEDCT_US["60600009"]  # Disorder of the larynx
, SNOMEDCT_US["129134004"] # Inflammatory disorder
                           # of upper respiratory tract

, PYM.unifieds.some(CUI["C0023067"]  # Laryngitis
), PYM.mapped_to.some(ICD10["J04.0"] # Acute laryngitis
), PYM.groups.some(<Group 22731_0>   # mapped_to=Acute↲ laryngitis
), PYM.has_associated_morphology.some(SNOMEDCT_US["23583003"] #Inflammation
), PYM.groups.some(<Group 22731_1>
#has_associated_morphology=Inflammation;↲
                       has_finding_site=Laryngeal structure
), PYM.has_finding_site.some(SNOMEDCT_US["4596009"] # Laryngeal↲ structure
), PYM.unifieds.only(CUI["C0023067"] # Laryngitis
)]

然而,限制并不容易处理。幸运的是,Owlready 允许将它们作为类属性来访问(参见 6.3)。例如,从像喉炎这样的疾病中,我们可以获得相应的解剖结构和形态:

>>> SNOMEDCT_US["45913009"].has_finding_site
[SNOMEDCT_US["4596009"] # Laryngeal structure
]

>>> SNOMEDCT_US["45913009"].has_associated_morphology
[SNOMEDCT_US["409774005"] # Inflammatory morphology
]

get_class_properties()方法允许您获得给定概念的所有可用属性:

>>> SNOMEDCT_US["45913009"].get_class_properties()
{PYM.mapped_to,
 PYM.unifieds,
 PYM.has_associated_morphology,
 PYM.groups,
 PYM.has_finding_site,
 PYM.terminology, rdf-schema.label,
 PYM.synonyms,
 PYM.subset_member,
 PYM.ctv3id,
 PYM.type_id,
 PYM.case_significance_id,
 PYM.definition_status_id,
 PYM.active,
 PYM.effective_time}

我们在属性集中找到了注释labelsynonyms,以及has_associated_morphologyhas_finding_site

当涉及几个解剖结构和/或形态时,有趣的是知道哪个形态适用于哪个解剖结构。团体允许这样做。在下面的例子中,概念“肝脾肿大”与两种解剖结构和一种形态相关联:

>>> SNOMEDCT_US["36760000"]
SNOMEDCT_US["36760000"] # Hepatosplenomegaly

>>> SNOMEDCT_US["36760000"].has_finding_site
[ SNOMEDCT_US["181268008"] # Entire liver
, SNOMEDCT_US["181279003"] # Entire spleen
]

>>> SNOMEDCT_US["36760000"].has_associated_morphology
[SNOMEDCT_US["442021009"] # Enlargement
]

我们可能想知道形态学是否与第一解剖结构(即肝脏)、第二解剖结构(即脾脏)或两者都相关。群组允许回答此问题;它们可通过group属性获得:

>>> SNOMEDCT_US["36760000"].groups
[ <Group 18807_4> # has_finding_site=Entire liver ;
                  # has_associated_morphology=Enlargement
, <Group 18807_3> # has_finding_site=Entire spleen ;
                  #has_associated_morphology=Enlargement
, <Group 18807_0> # mapped_to=Hepatomegaly with splenomegaly,
                  # not elsewhere classified
]

在前面的示例中,我们有三个组:

  • 第一个描述了肝脏的扩大。

  • 第二个描述了脾脏的扩大。

  • 第三个描述了与另一个术语的对应关系,但不包含解剖结构或形态学。

因此,在这里,形态学涉及两种解剖结构。请注意,组的确切顺序可能会有所不同:您将拥有相同的组,但顺序不一定相同。

可以单独查询每个组,例如,前面的第二个组:

>>> SNOMEDCT_US["36760000"].groups[0].get_class_properties()
{PYM.has_associated_morphology,
 PYM.has_finding_site}

>>> SNOMEDCT_US["36760000"].groups[0].has_associated_morphology
[SNOMEDCT_US["442021009"] # Enlargement
]

>>> SNOMEDCT_US["36760000"].groups[0].has_finding_site
[SNOMEDCT_US["181268008"] # Entire liver
]

PyMedTermino 还允许您在另一个方向上导航,也就是说,从解剖结构或形态学出发,前往疾病。例如,我们可以得到所有涉及玻璃体的疾病如下:

>>> SNOMEDCT_US["181268008"].finding_site_of
[ SNOMEDCT_US["80660001"] # Mauriac's syndrome
, SNOMEDCT_US["93369005"] # Congenital microhepatia
, SNOMEDCT_US["192008"] # Congenital syphilitic hepatomegaly
, SNOMEDCT_US["80378000"] # Neonatal hepatosplenomegaly
...]

当然,在 SNOMED CT 中可以进行全文搜索,其工作方式与 CIM10 相同。

9.6 运用统一概念(崔)

UMLS 定义了统一的概念(CUI,概念唯一标识符),允许在术语之间导航。这些 CUI 可以与 PyMedTermino 一起导入,使用特殊的术语代码“CUI”。请注意,当仅导入某些术语时,PyMedTermino 仅导入所选术语使用的 Cui。如果您想要访问所有 Cui,您将需要导入所有 UMLS。

>>> CUI = PYM["CUI"]

unifieds属性使得获得与任何术语概念相关联的统一概念成为可能(这里我们取 ICD10):

>>> ICD10["I10"]
ICD10["I10"] # Essential (primary) hypertension

>>> ICD10["I10"].unifieds
[CUI["C0085580"] # Essential hypertension
]

统一的概念都有一个标签和同义词(来自导入的术语,因此取决于对这些术语的选择):

>>> CUI["C0085580"].synonyms
['Essential (primary) hypertension',
 'Idiopathic hypertension',
 'Primary hypertension',
 'Systemic primary arterial hypertension',
 'Essential hypertension (disorder)']

originals属性是unifieds的逆属性;它允许获取与统一概念相关联的原始术语的概念;

>>> CUI["C0085580"].originals
[ SNOMEDCT_US["59621000"] # Essential hypertension
, ICD10["I10"] # Essential (primary) hypertension
]

这些统一的概念允许在术语之间导航,我们将在下一节中看到。

最后,“SRC”伪术语(来源的缩写)列出了 UMLS 和/或 PyMedTermino 的所有术语。这是一种“术语的术语”。所以,PyMedTermino 的根本概念是http://PYM/SRC/SRC:

>>> PYM["SRC"]["SRC"]
PYM["SRC"] # Metathesaurus Source Terminology Names

>>> PYM["SRC"]["SRC"].iri
'http://PYM/SRC/SRC'

9.7 术语之间的映射

使用 UMLS 现有的链接,>>操作符允许你从一个术语转换到另一个术语。注意,这个操作符不应该与 Python 提示>>>混淆(三个>字符对两个)。这种操作通常被称为“映射”、“代码转换”或“对应”。对于映射概念,如果存在 UMLS“mapped _ to”关系,PyMedTermino 会使用它们。当它们不存在时,PyMedTermino 使用统一概念(CUI)在术语之间导航。以下示例将 ICD10 概念“E11”映射到 SNOMED CT:

>>> ICD10["E11"]
ICD10["E11"] # Non-insulin-dependent diabetes mellitus

>>> ICD10["E11"] >> SNOMEDCT_US
Concepts([
  SNOMEDCT_US["44054006"] # Type 2 diabetes mellitus
])

这里,ICD10 概念“E11”对应于 SNOMEDCT 概念“44054006”,两者都代表二型糖尿病。概念 CIM 10“E11”没有“mapped_to”关系;我们可以验证如下:

>>> ICD10["E11"].mapped_to
[]

因此,Cui 用于执行映射。

我们也可以反方向映射,从 SNOMED CT 到 ICD10:

>>> SNOMEDCT_US["44054006"] >> ICD10
Concepts([
  ICD10["E11.9"] # Non-insulin-dependent diabetes mellitus↲ without complications
])

我们注意到获得的概念不是我们以前在 IC D10(“E11”,非胰岛素依赖型糖尿病)中获得的概念。实际上,SNOMED CT 认为没有任何并发症说明的一般概念“二型糖尿病糖尿病”对应于没有并发症的糖尿病。UMLS 在 SNOMED CT 概念上有一个“映射到”关系,我们可以验证如下:

>>> SNOMEDCT_US["44054006"].mapped_to
[ICD10["E11.9"] # Non-insulin-dependent diabetes mellitus↲
                  without complications
]

从 SNOMED CT 映射到 ICD10 时,PyMedTermino 使用了此关系。

映射总是返回一组概念(在下一节中描述)。当起始概念对应于到达术语中的几个概念时,该集合可以包含几个概念,如下例所示:

>>> ICD10["N80.0"]
ICD10["N80.0"] # Endometriosis of uterus

>>> ICD10["N80.0"] >> SNOMEDCT_US
Concepts([
  SNOMEDCT_US["784314006"] # Uterine adenomyosis
, SNOMEDCT_US["76376003"] # Endometriosis of uterus
, SNOMEDCT_US["237115002"] # Endometriosis of myometrium
, SNOMEDCT_US["198247003"] # Endometriosis interna
])

9.8 操作概念集

PYM.Concepts类用于创建一组概念。这个类继承自 Python 的set类(参见 2.4.7 ),因此具有相同的方法来计算两个集合的交集、并集、减法等。它为术语添加了特定的方法。例如,lowest_common_ancestors()方法允许计算几个概念最接近的共同祖先:

>>> PYM.Concepts([ICD10["E11.1"], ICD10["E12.0"]]).lowest_↲common_ancestors()
Concepts([
  ICD10["E10-E14.9"] # Diabetes mellitus
])

这种方法适用于“概括”几个概念,并将它们组合成一个更高层次的概念。

find()方法使得寻找一个集合中的第一个概念成为可能,该集合是一个给定概念(包括概念本身)的后代。例如,我们可以创建一组四个概念:

>>> cs = PYM.Concepts([
...     SNOMEDCT_US["49260003"], SNOMEDCT_US["371438008"],
...     SNOMEDCT_US["373137001"], SNOMEDCT_US["300562000"],
... ])
>>> cs
Concepts([
  SNOMEDCT_US["300562000"] # Genitourinary tract problem
, SNOMEDCT_US["373137001"] # Immobile heart valve
, SNOMEDCT_US["49260003"]  # Idioventricular rhythm
, SNOMEDCT_US["371438008"] # Urolith
])

然后,我们可以搜索心脏概念的存在(这里,301095005 是“心脏发现”的 SNOMED CT 代码):

>>> cs.find(SNOMEDCT_US["301095005"])
SNOMEDCT_US["373137001"] # Immobile heart valve

extract()方法类似,但是返回从作为参数传递的概念开始的所有概念的子集,例如,在这里,所有心脏概念:

>>> cs.extract(SNOMEDCT_US["301095005"])
Concepts([
  SNOMEDCT_US["373137001"] # Immobile heart valve
, SNOMEDCT_US["49260003"]  # Idioventricular rhythm
])

subtract()方法返回一个包含相同概念的新集合,除了那些从参数中传递的概念派生的概念。subtract_update()方法执行相同的操作,但是修改了传入的参数集,而不是返回一个新的。

keep_most_generic()keep_most_specific()方法分别只允许保留最通用或最具体的概念。在下面的例子中,概念 SNOMED CT 300562000(“泌尿生殖道问题”)已被删除,因为它没有 371438008(“尿结石”)具体:

>>> cs.keep_most_specific()
>>> cs
Concepts([
  SNOMEDCT_US["373137001"] # Immobile heart valve
, SNOMEDCT_US["371438008"] # Urolith
, SNOMEDCT_US["49260003"]  # Idioventricular rhythm
])

all_subsets()方法返回集合中所有可能的子集,例如:

>>> cs = PYM.Concepts([
...     SNOMEDCT_US["49260003"],
...     SNOMEDCT_US["371438008"],
...     SNOMEDCT_US["373137001"],
... ])
>>> cs.all_subsets()
[Concepts([
]), Concepts([
  SNOMEDCT_US["373137001"] # Immobile heart valve
]), Concepts([
  SNOMEDCT_US["371438008"] # Urolith
]), Concepts([
  SNOMEDCT_US["373137001"] # Immobile heart valve
, SNOMEDCT_US["371438008"] # Urolith
]), Concepts([
  SNOMEDCT_US["49260003"] # Idioventricular rhythm
]), Concepts([
  SNOMEDCT_US["373137001"] # Immobile heart valve
, SNOMEDCT_US["49260003"] # Idioventricular rhythm
]), Concepts([
  SNOMEDCT_US["49260003"] # Idioventricular rhythm
, SNOMEDCT_US["371438008"] # Urolith
]), Concepts([
  SNOMEDCT_US["373137001"] # Immobile heart valve
, SNOMEDCT_US["49260003"] # Idioventricular rhythm
, SNOMEDCT_US["371438008"] # Urolith
])]

方法is_semantic_subset()is_semantic_superset()is_semantic_disjoint()semantic_intersection()类似于 Python 集合的同名方法,但是它们考虑了概念之间的层次关系。在下面的例子中,两个集合的交集是空的,但不是语义交集,因为尿结石是一个泌尿问题:

>>> cs1 = PYM.Concepts([SNOMEDCT_US["371438008"]])
>>> cs2 = PYM.Concepts([SNOMEDCT_US["106098005"]])
>>> cs1
Concepts([
  SNOMEDCT_US["371438008"] # Urolith
])
>>> cs2
Concepts([
  SNOMEDCT_US["106098005"] # Urinary system finding
])
>>> cs1.intersection(cs2)
Concepts([
])
>>> cs1.semantic_intersection(cs2)
Concepts([
  SNOMEDCT_US["371438008"] # Urolith
])

但是,要小心,这些语义操作没有考虑到概念的可能的共同后代。在下面的例子中,“传染病”和“泌尿系统问题”这两个概念的交集是空的,而尿路感染确实存在:

>>> cs1 = PYM.Concepts([SNOMEDCT_US["40733004"]])
>>> cs2 = PYM.Concepts([SNOMEDCT_US["106098005"]])
>>> cs1
Concepts([
  SNOMEDCT_US["40733004"] # Disorder due to infection
])
>>> cs2
Concepts([
  SNOMEDCT_US["106098005"] # Urinary system finding
])
>>> cs1.semantic_intersection(cs2)
Concepts([
  ])

我们将在后面(10.7)看到如何实现一个真正的语义交集,它考虑到了共同的后代。

PyMedTermino 还允许映射一组概念,总是使用运算符“>>”。由于映射操作本身返回一组概念,因此可以将这些操作链接起来。例如,我们可以通过强制通过 CUI 从 SNOMED CT 映射到 CIM10,如下所示:

>>> SNOMEDCT_US["44054006"] >> CUI >> ICD10
Concepts([
  ICD10["E11"] # Non-insulin-dependent diabetes mellitus
])

相反,当“mapped_to”关系出现在 UMLS 时,直接映射(如前所述)可能返回不同的结果:

>>> SNOMEDCT_US["44054006"] >> ICD10
Concepts([
  ICD10["E11.9"] # Non-insulin-dependent diabetes
                 # mellitus without complications
])

通过 CUI 保证了可逆映射。

9.9 将所有术语导入 UMLS

terminologies参数丢失时,import_umls()函数导入 UMLS 中存在的所有术语。因此,我们可以按如下方式导入所有 UMLS(注意,这至少需要 20 GB 的磁盘空间、16 GB 的 RAM 和一个多小时):

>>> from owlready2 import *
>>> from owlready2.pymedtermino2 import *
>>> from owlready2.pymedtermino2.umls import *

>>> default_world.set_backend(filename = "pymedtermino.sqlite3",
...                          sqlite_tmp_dir = "/home/jiba/tmp")
>>> import_umls("umls-2020AA-full.zip")
>>> PYM = get_ontology("http://PYM/").load()

注意,当调用set_backend()方法时,我们添加了可选的sqlite_tmp_dir参数,该参数指示存储大型临时文件的目录路径(见 8.8.1)。

然后,要一次搜索所有术语中的概念,可以使用PYM.search()方法:

>>> PYM.search("hypertension*")
[ SNOMEDCT_US["123800009"] # Goldblatt hypertension
, SNOMEDCT_US["70272006"] # Malignant hypertension
, ICD10["K76.6"]# Portal hypertension
, SNOMEDCT_US["34742003"] # Portal hypertension
, SNOMEDCT_US["70995007"] # Pulmonary hypertension
, SNOMEDCT_US["28119000"] # Renal hypertension
, ICD10["I15.0"] # Renovascular hypertension
...]

9.10 示例:将细菌本体论与 UMLS 联系起来

我们现在可以把细菌的本体论和 UMLS 联系起来。为此,我们将在这个本体论的概念和的统一概念(崔)之间建立联系。由于这些是类,我们将使用 Owlready 的类属性(见 6.3)。

下面几行代码可以将三类细菌(假单胞菌、链球菌和葡萄球菌)链接到相应的 CUI(我们使用search()进行搜索)。这些关系放在一个新的本体中,命名为“bacteria_umls.owl”。

>>> onto = get_ontology("bacteria.owl").load()
>>> onto_bacteria_umls = get_ontology("http://↲lesfleursdunormal.fr/
static/_downloads/bacteria_umls.owl")

>>> CUI = PYM["CUI"]

>>> with onto_bacteria_umls:
...     onto.Pseudomonas   .mapped_to = [ CUI["C0033808"] ]
...     onto.Streptococcus .mapped_to = [ CUI["C0038402"] ]
...     onto.Staphylococcus.mapped_to = [ CUI["C0038170"] ]

>>> onto_bacteria_umls.save("bacteria_umls.owl")

我们已经为我们的关系重用了 UMLS mapped_to object 属性。

我们可以验证它确实是一个类属性,也就是说,一个 OWL 限制:

>>> onto.Pseudomonas.mapped_to
[CUI["C0033808"] # Pseudomonas
]
>>> onto.Pseudomonas.is_a
[bacteria.Bacterium,
 bacteria.has_shape.some(bacteria.Rod),
 bacteria.has_shape.only(bacteria.Rod),
 bacteria.has_grouping.some(bacteria.Isolated | bacteria.InPair),
 bacteria.gram_positive.value(False),
 PYM.mapped_to.some(CUI["C0033808"] # Pseudomonas
)]

例如,可以将这些 Cui 映射到 SNOMED CT:

>>> SNOMEDCT_US = PYM["SNOMEDCT_US"]

>>> onto.Pseudomonas.mapped_to[0] >> SNOMEDCT_US
Concepts([
  SNOMEDCT_US["5274006"] # Chryseomonas
, SNOMEDCT_US["57032008"] # Pseudomonas
])

这里,UMLS 的“假单胞菌”概念对应于 SNOMED CT 中的假单胞菌,但也对应于 Chryseomonas,一种后来被归入假单胞菌属的细菌属(命名为绿脓假单胞菌)。

还可以将统一概念 CUI 翻译成 SNOMED CT,然后通过“causative_agent_of”关系(与“has_causative_agent”相反)恢复相关疾病:

>>> diseases = [
...     disease

...     for snomedct in onto.Pseudomonas.mapped_to[0] >>↲ SNOMEDCT_US
...     for disease in snomedct.causative_agent_of
... ]
>>> diseases
[ SNOMEDCT_US["127201000119101"] # Septic shock co-occurrent
     # with acute organ dysfunction due to Pseudomonas
, SNOMEDCT_US["16664009"]  # Malignant otitis media
, SNOMEDCT_US["448813005"] # Sepsis due to Pseudomonas
...]

这给了我们一个由细菌引起的疾病列表。

9.11 示例:多术语浏览器

在 Python 终端中用 PyMedTermino 进行医学术语的咨询是完全可能的;然而,这很快就变得费力了。因此,我们将建立一个多术语的“迷你浏览器”,既可以通过关键字搜索概念,也可以在各种术语中导航。该浏览器将使用 Python Flask模块制作一个动态网站(见 4.12),并将整合 PyMedTermino 中所有可用的术语。

以下程序描述了多术语“迷你浏览器”:

# File termino_browser.py
from owlready2 import *
default_world.set_backend(filename = "pymedtermino.sqlite3")
PYM = get_ontology("http://PYM/").load()

from flask import Flask, url_for, request
app = Flask(__name__)

def repr_concept(concept):
    return """[<a href="%s">%s:%s</a>] %s""" % (
        url_for("concept_page", iri = concept.iri),
        concept.terminology.name,
        concept.name,
        concept.label.first() )

def repr_relations(entity, border = False):
    if border: html = """<table style="border: 1px solid #aaa;">"""
    else:      html = """<table>"""
    for Prop in entity.get_class_properties():
        for value in Prop[entity]:
            if issubclass(value, PYM.Concept):
                value = repr_concept(value)
            elif issubclass(value, PYM.Group):
                value = repr_relations(value, True)
            html += """<tr><td>%s:""" % Prop.name
            html += """</td><td> %s</td></tr>""" % value

    html += """</table>"""
    return html

@app.route('/')
def homepage():
    html ="""
<html><body>
  Search in all terminologies:
  <form action="/search">
    <input type="text" name="keywords"/>
    <input type="submit"/>
  </form>
  Or <a href="%s">browse the entire hierarchy</a>
</body></html>""" % url_for("concept_page", iri = "http://PYM/↲SRC/SRC")
    return html

@app.route('/search')
def search_page():
    keywords = request.args.get("keywords", "")
    html = """<html><body>Recherche "%s":<br/>\n""" % keywords
    keywords = " " .join("%s"* % word for word in keywords.↲split())
    results = PYM.search(keywords)
    for concept in results:
        html += """%s<br/>""" % repr_concept(concept)
    html += """</body></html>"""
    return html

@app.route('/concept/<path:iri>')
def concept_page(iri):
    concept = IRIS[iri]
    html  = """<html><body>"""
    html += """<h2>%s</h2>""" % repr_concept(concept)
    html += """<h3>Ancestor concept (except parents)</h3>"""
    html += """%s<br/>""" % repr_concept(concept.terminology)
    ancestors = set(concept.ancestor_concepts(include_self =↲ False))
    ancestors = ancestors - set(concept.parents)
    ancestors = list(ancestors)
    ancestors.sort(key = lambda t: len(t.ancestor_concepts()))
    for ancestor in ancestors

:
        html += """%s<br/>""" % repr_concept(ancestor)

    html += """<h3>Parent concepts</h3>"""
    for parent in concept.parents:
        html += """%s<br/>""" % repr_concept(parent)

    html += """<h3>Relations</h3>"""
    html += repr_relations(concept)

    if not concept.name == "CUI":
        html += """<h3>Child concepts</h3>"""
        for child in concept.children:
            html += """%s<br/>""" % repr_concept(child)

    html += """</body></html>"""
    return html

import werkzeug.serving
werkzeug.serving.run_simple("localhost", 5000, app)

该程序首先导入 Owlready 并使用 PyMedTermino 加载 quadstore,然后导入 Flask。然后,它创建两个效用函数:

  • repr_concept(),它将用于在 HTML 中表示一个概念,使用它的标签、术语和代码,并带有一个到概念页面的链接。

  • repr_relations(),它将用于在 HTML 中表示概念或组的(非层次)关系。该函数返回一个 HTML 表,每个属性和属性值对应一行。这个函数是递归的:如果为一个概念调用它,如果必要的话,它将为这个概念的每个组调用它自己。

然后,程序创建三个网页:

  • 根页面(路径“/”),它提供了一个搜索字段和一个到 PyMedTermino 的根概念的链接。

  • 搜索页面(路径)/搜索?keywords = entered _ keywords”),它列出了文本搜索的结果。这个页面的工作方式类似于我们为 DBpedia 创建的页面(见 8.8.2)。

  • 概念页面(路径“/概念/概念 _IRI”),显示给定概念的属性:祖先概念(不包括父概念)、父概念、关系和子概念。

    为了方便阅读,我们去掉了祖先的父母,我们按照祖先自己拥有的祖先概念的数量对祖先列表进行了排序。这允许在列表的开始具有较少祖先的概念,因此是最一般的,而在列表的底部是最具体的。

    此外,对于“CUI”分类,子概念的显示已被停用,因为它没有层次结构。因此,所有 Cui(超过 20,000 个)都是该分类的直接子分类,这会导致页面太长!

以下截图显示了最终的术语浏览器:

img/502592_1_En_9_Figa_HTML.jpg

img/502592_1_En_9_Figb_HTML.jpg

img/502592_1_En_9_Figc_HTML.jpg

9.12 摘要

在本章中,您已经学习了如何从 UMLS 导入医学术语,以及如何将它们作为本体来访问。我们已经看到了如何将概念从一个术语映射到另一个术语,以及如何设计一个简单的术语浏览器。

十、混合 Python 和 OWL

在这一章中,我们将看到如何在同一个类中混合 Python 方法和 OWL 逻辑构造函数。

10.1 向 OWL 类添加 Python 方法

有了 Owlready,OWL 类(几乎)和其他类一样是 Python 类。因此,在这些类中包含 Python 方法是可能的。下面是一个简单的例子,根据药品的单价(每盒)和盒中的药片数量来计算每片药品的价格:

>>> from owlready2 import *
>>> onto = get_ontology("http://test.org/drug.owl#")
>>> with onto:
...     class Drug(Thing): pass
...
...     class price(Drug >> float, FunctionalProperty): ...         pass
...     class nb_tablet(Drug >> int, FunctionalProperty): ...         pass
...
...     class Drug(Thing):
...         def get_price_per_tablet(self):
...             return self.price / self.nb_tablet

请注意,药品类定义了两次:第一次定义是一个前向声明,以便能够在属性定义中使用该类(参见 5.8)。还要注意,由于我们创建了一个新的本体,我们在本体 IRI 的末尾集成了分隔符(这里是# )(参见 5.1)。

然后可以对该类的个体调用该方法:

>>> my_drug = Drug(price = 10.0, nb_tablet = 5)
>>> my_drug.get_price_per_tablet()
2.0

在本体中,通常只使用类和子类,而不使用个体(例如在基因本体中就是这种情况),因为类表示的能力更强。在这种情况下,Python 允许您定义“类方法”,这些方法将在类(或它的一个子类)上调用,并将该类(或子类)作为第一个参数。

下面是与前面相同的示例,但是使用了类:

>>> with onto:
...     class Drug(Thing): pass
...
...     class price(Drug >> float, FunctionalProperty): ...         pass
...     class nb_tablet(Drug >> int, FunctionalProperty): ...         pass
...
...     class Drug(Thing):
...         @classmethod
...         def get_price_per_tablet(self):
...             return self.price / self.nb_tablet

然后可以在该类及其子类上调用该方法:

>>> class MyDrug(Drug): pass
>>> MyDrug.price = 10.0
>>> MyDrug.nb_tablet = 5
>>> MyDrug.get_price_per_tablet()
2.0

但是,要小心,要使两种类型的方法(个体和类)共存在一起;有必要使用不同的方法名。

10.2 将 Python 模块与本体相关联

当本体不是完全用 Python 创建的(如我们在前面的例子中所做的那样),而是从 OWL 文件中加载时,Python 方法可以在单独的 Python 模块(.py文件)中定义。这个文件可以手动导入或者通过注释链接到本体;在这种情况下,Owlready 将在加载本体时自动导入 Python 模块。

例如,以下名为“细菌. py”的文件在细菌本体的细菌和葡萄球菌类中添加了一个方法:

# File bacteria.py
from owlready2 import *

onto = get_ontology("http://lesfleursdunormal.fr/static/↲
_downloads/bacteria.owl#")

with onto:
    class Bacterium(Thing):
        def my_method(self):
            print("It is a bacterium!")

    class Staphylococcus(Thing):
        def my_method(self):
            print("It is a staphylococcus!")

请注意,我们没有加载细菌本体(即,我们没有调用.load()),因为它将由主程序来完成。还要注意,我们没有指出葡萄球菌的超类(即细菌):事实上,它已经出现在 OWL 文件中,所以没有必要在这里第二次断言它!另一方面,有必要将 Thing 作为一个超类来说明这个新类是由 Owlready 管理的 OWL 类,而不是普通的 Python 类。一般来说,当用方法创建一个单独的 Python 文件时,最好只把方法放在里面,而保留其余的本体(超类、属性、关系、等)。)以猫头鹰来限制冗余。

手动导入

然后,我们可以加载本体并手动导入文件“bacteria.py”:

>>> from owlready2 import *
>>> onto = get_ontology("bacteria.owl").load()
>>> import bacteria

然后,我们创造了一个葡萄球菌,我们称我们的方法为:

>>> my_bacterium = onto.Staphylococcus()
>>> my_bacterium.my_method()
It is a staphylococcus!

自动导入

为此,有必要用 Protégé编辑本体,并添加一个注释,指明相关 Python 模块的名称。这个标注叫做 python_module,在本体“owlready_ontology.owl”中定义,需要导入。以下是步骤:

  1. 启动 Protégé,加载细菌本体。

  2. 转到 Protégé的“活动本体”选项卡。

  3. Import the ontology “owlready_ontology” by clicking the “+” button to the right of “Direct imports”. The ontology can be imported from the local copy which is in the installation directory of Owlready or from its IRI: www.lesfleursdunormal.fr/static/_downloads/owlready_ontology.owl.

    img/502592_1_En_10_Fig1_HTML.jpg

    图 10-1

    Protégé中的“python_module”注释

  4. 在“本体头”部分添加注释。标注类型为“python_module”,值为模块的名称,这里是细菌(见图 10-1 )。您也可以使用 Python 包,例如“my_module.my_package”。

现在,我们不再需要导入“细菌”模块:owl 已经在每次加载细菌本体时自动完成了这项工作。在下面的例子中,我们将细菌本体(带有注释“python_module”)保存在一个名为“bacteria_owl_python.owl”的新 OWL 文件中:

>>> from owlready2 import *
>>> onto = get_ontology("bacteria_owl_python.owl").load()
>>> my_bacterium = onto.Staphylococcus()
>>> my_bacterium.my_method()
It is a staphylococcus!

10.3 带类型推断的多态性

我们在 7.2 节中已经看到,在推理过程中,个体的类和类的超类可以被修改。在这种情况下,可用的方法可能会改变。此外,在多态的情况下,也就是说,当几个类不同地实现同一方法时,给定个体或类的方法实现可能会改变。这就是“带类型推断的多态性”。

这里有一个简单的例子:

>>> my_bacterium = onto.Bacterium(gram_positive = True,
...         has_shape = onto.Round(),
...         has_grouping = [onto.InCluster()] )
>>> my_bacterium.my_method()
It is a bacterium!

我们创造了一种细菌。当我们执行该方法时,它是类 Bacterium 的实现,因此被称为。我们现在叫推理者。

>>> sync_reasoner()

推理者推断该细菌实际上是一种葡萄球菌(由于其亲缘关系)。现在,如果我们调用方法,它是葡萄球菌类的实现,被称为:

>>> my_bacterium.my_method()
It is a staphylococcus!

10.4 自省

自省是一种高级的对象编程技术,它包括在不知道对象的情况下“分析”对象,例如,为了获得其属性及其值的列表。

对于个体的内省,get_properties()方法允许获得个体至少有一个关系的属性列表。

>>> onto.unknown_bacterium.get_properties()
{bacteria.has_shape,
 bacteria.has_grouping,
 bacteria.gram_positive,
 bacteria.nb_colonies}

然后有可能获得和/或修改这些关系。getattr(object, attribute)setattr(object, attribute, value) Python 函数允许您读取或写入任何 Python 对象的属性,前提是该属性的名称在变量中是已知的(参见 2.9.4),例如:

>>> for prop in onto.unknown_bacterium.get_properties():
...     print(prop.name, "=",
...           getattr(onto.unknown_bacterium, prop.python_name))
has_grouping = [bacteria.in_cluster1]
has_shape = bacteria.round1
gram_positive = True
nb_colonies = 6

返回的值与通常的语法“individual.property”相同:它是功能属性的单个值和其他属性的值列表。然而,在进行自省时,更容易的是一般地对待所有属性,不管它们是否是功能性的。在这种情况下,替代语法“property[individual]”是更可取的,因为它总是返回一个值列表,即使在函数属性上调用时也是如此,例如:

>>> for prop in onto.unknown_bacterium.get_properties():
...     print(prop.name, "=", prop[onto.unknown_bacterium])
has_grouping = [bacteria.in_cluster1]
has_shape = [bacteria.round1]
gram_positive = [True]
nb_colonies = [6]

对于类内省来说,get_class_properties()方法的工作方式类似于个体的工作方式。它返回属性,对于这些属性,该类至少有一个存在限制(或者是通用的,这取决于类属性的类型;参见 6.3):

>>> onto.Pseudomonas.get_class_properties()
{bacteria.gram_positive,
 bacteria.has_shape,
 bacteria.has_grouping}

owl 已经考虑了父类,也考虑了等价类。语法“property[class]”可用于获取和/或修改类的存在限制。

最后,INDIRECT_get_properties()INDIRECT_get_class_properties()方法以相同的方式工作,但是也返回间接属性(即,从父类继承)。

另外,constructs()方法允许你浏览所有引用一个类或属性的构造函数。例如,我们可以查找引用 InSmallChain 类的构造函数:

>>> list(onto.InSmallChain.constructs())
[  bacteria.Bacterium
 & bacteria.has_shape.some(bacteria.Round)
 & bacteria.has_shape.only(bacteria.Round)
 & bacteria.has_grouping.some(bacteria.InSmallChain)
 & bacteria.has_grouping.only(Not(bacteria.Isolated))
 & bacteria.gram_positive.value(True)]

这里,我们只得到一个构造,它是一个交集,包含一个存在性限制,以类 InSmallChain 为值。然后我们可以使用这个构造函数的subclasses()方法来获得使用它的所有类的列表:

>>> constructor = list(onto.InSmallChain.constructs())[0]
>>> constructor.subclasses()
[bacteria.Streptococcus]

因此,我们找到了我们设置了这种限制的链球菌类别(见 3.4.8)。

10.5 向后阅读限制

这些限制使得在本体的类的级别上定义关系成为可能,例如,“假单胞菌具有 _ 形状一些杆”。Owlready 通过语法“Class.property”提供了对这些关系的简单访问(参见 4.5.4):

>>> onto.Pseudomonas.has_shape
bacteria.Rod

但如何“向后”解读这个存在限制,也就是说,从杆类,回到假单胞菌类?即使我们定义了反向属性(我们可以称之为“is_shape_of”),它也不能回答我们的问题,如下例所示:

>>> with onto:
...     class is_shape_of(ObjectProperty):
...         inverse = onto.has_shape

>>> onto.Rod.is_shape_of
[]

的确,从逻辑的角度来看,以下两个命题是不同的:

  • “假单胞菌有 _ 形一些杆状”

  • “杆是某些假单胞菌的形状”

第一个表明所有假单胞菌都有杆状,这是真的。第二个表示所有的杆形状都是假单胞菌的形状,这不是同一个意思(也不是真的)。例如,橄榄球的杆状形状不是假单胞菌的形状。

同样,对于以下两个命题:

  • “细胞核是某些细胞的一部分”

  • “细胞有部分细胞核”

第一个表明每个细胞核都是细胞的一部分。第二种表示每个细胞都有细胞核,这是不一样的:在生物学中,第一个命题成立,第二个命题不成立(如细菌是没有细胞核的细胞)。

然而,能够反向阅读存在关系有时是有用的。Owlready 允许这样做:这可以通过组合constructs()subclasses()方法来实现,正如我们在上一节中所做的那样。inverse_restrictions()方法自动化了这一点:

>>> set(onto.Rod.inverse_restrictions(onto.has_shape))
{bacteria.Pseudomonas, bacteria.Bacillus}

注意,在删除重复项之后,我们使用了set()来显示由inverse_restrictions()返回的生成器。

10.6 示例:使用基因本体和管理“部分”关系

基因本体(GO)是生物信息学中广泛使用的本体(见 4.7)。GO 由三部分组成:生物过程、分子功能和细胞成分。第三部分描述了细胞的不同组成部分:细胞膜、细胞核、细胞器(如线粒体)等等。管理起来特别复杂,因为它既包括使用 is-a 关系的“经典”继承层次,也包括“部分”关系层次。在第二个层次中,称为部分关系,单元被分解成子部分,然后分解成子部分,依此类推。因此,这个层次的根是整个细胞,叶子是不可分割的部分。

OWL 和 Owlready 有关系和方法来管理继承层次结构(subclasses()descendants()ancestors()等)。;参见 4.5.3)。另一方面,在 Owlready 中既没有用于部分关系的标准 OWL 关系,也没有特定的方法。我们将在这里看到如何在 GO 类中添加访问子部分和超部分的方法,同时考虑部分和 is-a 关系。

GO 相当大(几乎 200 MB),加载需要几十秒甚至几分钟,这取决于计算机的能力和 OWL 文件的下载时间。因此,我们将加载 GO 并将 Owlready quadstore 存储在一个文件中(参见 4.7)。此外,我们将在这里使用手动导入将我们的 Python 方法与 OWL 类关联起来(参见 10.2.1),这样就不必通过添加“python_module”注释来修改 GO。

GO 使用人类无法直接理解的任意标识符。下表总结了我们以后需要的 GO 标识符:

|

GO identifier

|

标签

| | --- | --- | | GO_0005575 | 细胞 _ 成分 | | BFO_0000050 | 的一部分 | | BFO_0000051 | has_part |

# File go_part_of.py
from owlready2 import *

default_world.set_backend(filename = "quadstore.sqlite3")
go = get_ontology("http://purl.obolibrary.org/obo/go.owl#").↲load()
obo = go.get_namespace("http://purl.obolibrary.org/obo/")
default_world.save()

def my_render(entity):
    return "%s:%s" % (entity.name, entity.label.first())
set_render_func(my_render)

with obo:
    class GO_0005575(Thing):
        @classmethod
        def subparts(self):
            results = list(self.BFO_0000051)
            results.extend(self.inverse_restrictions↲(obo.BFO_0000050))
            return results

        @classmethod
        def transitive_subparts(self):
            results = set()
            for descendant in self.descendants():
                results.add(descendant)
                for subpart in descendant.subparts():
                    results.update(subpart.transitive_↲subparts())
            return results

        @classmethod
        def superparts(self):
            results = list(self.BFO_0000050)
            results.extend(self.inverse_restrictions(obo.↲BFO_0000051))
            return results

        @classmethod
        def transitive_superparts(self):
            results = set()
            for ancestor in self.ancestors():
                if not issubclass(ancestor, GO_0005575):↲ continue
                results.add(ancestor)
                for superpart in ancestor.superparts():
                    if issubclass(superpart, GO_0005575):
                        results.update(superpart.transitive_↲superparts())
            return results

该模块在类 GO_0005575(即 cellular_component)中定义了四个类方法。subparts()允许获取组件的所有子零件。该方法考虑了关系 BFO_0000051 (has-part)以及关系 BFO_0000050 (part-of)的反向读取,这与我们使用。间接 _BFO_0000051(见 6.3)。transitive_subparts()方法以传递的方式返回子部分,考虑了子类和传递性(如果 A 是 B and B 的子部分,是 C 的子部分,那么 A 也是 C 的子部分)。对于超级零件,superparts()transitive_superparts()方法的工作方式相同。

然后,我们可以导入该模块并访问 GO 和“部分”关系。在下面的例子中,我们正在查看核仁的部分关系,核仁是位于细胞核中的一个组件:

>>> from owlready2 import *
>>> from go_part_of import *

>>> nucleolus = go.search(label = "nucleolus")[0]

>>> print(nucleolus.subparts())
[GO_0005655:nucleolar ribonuclease P complex,
 GO_0030685:nucleolar preribosome,
 GO_0044452:nucleolar part,
 GO_0044452:nucleolar part,
 GO_0101019:nucleolar exosome (RNase complex)]

>>> print(nucleolus.superparts())
[GO_0031981:nuclear lumen]

这里,直接关系(不考虑传递性)不太能提供信息。传递关系要丰富得多:

>>> nucleolus.transitive_subparts()
{GO_0034388:Pwp2p-containing subcomplex of 90S preribosome,
 GO_0097424:nucleolus-associated heterochromatin,
 GO_0005736:DNA-directed RNA polymerase I complex,
 GO_0005731:nucleolus organizer region,
 GO_0101019:nucleolar exosome (RNase complex),
 [...] }

>>> nucleolus.transitive_superparts()
{GO_0031981:nuclear lumen,
 GO_0005634:nucleus,
 GO_0043226:organelle,
 GO_0044464:cell part,
 GO_0005623:cell

,
 GO_0005575:cellular_component,
 [...] }

10.7 示例:蛋白质的“约会网站”

现在,我们将使用“go_part_of.py”模块的功能为蛋白质创建一个“约会网站”。这个网站允许你输入两个蛋白质的名字,这个网站决定了它们可以在细胞的哪个区域相遇(如果相遇是可能的!).从生物学的角度来看,这很重要,因为没有共同“交汇点”的两种蛋白质无法相互作用。

为此,我们将使用

  • Flask Python 模块制作一个动态网站(见 4.12)。

  • MyGene Python 模块,用于在 MyGene 服务器上执行搜索,并检索与这两种蛋白质相关联的 GO 概念。这个模块允许你搜索基因(和它们编码的蛋白质)。MyGene 的用法如下:

import mygene
mg = mygene.MyGeneInfo()
dico = mg.query(’name:"<gene_name>"’,
                fields = "<searched fields>",
                species = "<species>",
                size = <number of genes to search for>)

对 MyGene 的调用返回一个字典,该字典本身包含列表和其他字典。例如,我们可以搜索与胰岛素相关的所有 GO 术语,如下所示:

>>> import mygene
>>> mg = mygene.MyGeneInfo()
>>> dict = mg.query(’name:"insulin"’,
...     fields = "go.CC.id,go.MF.id,go.BP.id,"
...     species = "human",
...     size = 1)
>>> dict
{’max_score’: 13.233688, ’took’: 17, ’total’: 57,
’hits’: [{’_id’: ’3630’, ’_score’: 13.233688,
          ’go’: {’BP’: [{’id’: ’GO:0002674’},
                        {’id’: ’GO:0006006’}, [...] ]}}]}

“去吧。CC.id”,“去吧。MF.id”和“go。BP.id”代表 GO 的三个主要部分(分别是细胞成分、分子功能和生物过程)。对于我们的交友网站,我们只会用“CC”。虽然它们来源于基因本体论,但它们实际上描述了基因产物在细胞中的定位,即蛋白质(一般而言),而不是基因本身(对于真核细胞,基因通常保留在细胞核中)。

更多信息请访问 MyGene 网站:

http://docs.mygene.info/en/latest/

  • Owlready 和基因本体(GO)来生成描述两种蛋白质的细胞区室的 GO 术语的语义交集。一个“简单的”交集(在术语的集合意义上)是不够的:交集必须考虑继承的“是-a”关系和“部分-的”关系。例如,只存在于膜中的蛋白质 A 和只存在于线粒体中的蛋白质 B 可以在线粒体的膜中相遇。的确,线粒体膜是一种膜,它是线粒体的一部分,如下图所示:

img/502592_1_En_10_Figa_HTML.png

下面的程序描述了蛋白质年代测定网站。它从导入和初始化所有模块开始:

  • 已经准备好了

  • 我们在上一节中创建的“go_part_of”模块

  • 我的基因

然后,定义search_protein()函数。它将一个蛋白质名称(英文)作为输入,例如“insulin”,并返回 MyGene 中与之相关的细胞成分类型(“CC”)的所有 GO 术语。为此,我们检查至少找到一个结果(在英语中为 hit ),然后我们得到“CC”。如果只找到一个 CC,MyGene 返回它;否则,它就是一个列表。为了便于处理,我们系统地创建了一个名为cc的列表。然后我们遍历这个列表,提取 go 标识符。MyGene 返回的标识符的格式是“GO: 0002674 ”,而不是 OWL 版本的 GO 中的“GO_0002674”。所以我们把所有的“:”都换成“_”。最后,我们使用obo名称空间(从 go_part_of 模块导入)恢复相应本体的概念。

semantic_intersection()函数分四步执行包含细胞成分 GO 概念的两个集合的语义交集:

  1. 我们创建了两个集合,subparts1 和 subparts2,包含与这两个蛋白质中的每一个相关联的成分以及它们的子部分。为此,我们重用了静态方法 transitive_subparts(),它是我们在上一节的 go_part_of.py 模块中定义的。然后,考虑到 is-a 和 part-of 关系,我们有了两种蛋白质中每一种都能遇到的所有成分的集合。

  2. 我们用操作符“&”计算这两个集合的交集(Python 中的集合见 2.4.7),我们称结果为common_components

  3. 我们现在必须简化common_components集合。事实上,它包括我们正在寻找的概念,也包括它们的所有后代和子部分(在前面的例子中有“膜”和“线粒体”,因此我们有“线粒体的膜”,也有“线粒体的内膜”和“线粒体的外膜”)。为了加快下一步的处理,我们首先创建一个缓存(使用字典)。该缓存将common_components中的每个 GO 概念与其所有(可传递的)子部分相匹配。

  4. 我们创建一个新的集合,largest_common_components,它在开始时是空的。我们给它添加了所有的概念common_components,它不是common_components中另一个概念的子部分。请注意“for”循环中“else”的使用,它允许您在循环迭代完所有项时执行指令(也就是说,没有遇到“break”;参见 2.6)。最后,我们返回largest_common_components

程序的其余部分用 Flask 定义了两个网页。第一个(路径“/”)是一个基本表单,有两个文本字段用于输入蛋白质的名称,还有一个按钮用于验证。第二个(path)/result”)计算并显示结果。它首先调用两次search_protein()函数,每个蛋白质调用一次,然后调用semantic_intersection()函数。最后,它生成一个网页,显示与第一种蛋白质、第二种蛋白质相关的成分,以及它们可能相遇的成分。

# File dating_site.py
from owlready2 import *
from go_part_of import *

from flask import Flask, request
app = Flask(__name__)

import mygene
mg = mygene.MyGeneInfo()

def search_protein(protein_name):
    r = mg.query(’name:"%s"’ % protein_name, fields =↲"go.CC.id", sspecies = "human", size = 1)
    if not "go" in r["hits"][0]: return set()

    cc = r["hits"][0]["go"]["CC"]
    if not isinstance(cc, list): cc = [cc]

    components = set()
    for dict in cc:
        go_id = dict["id"]
        go_term = obo[go_id.replace(":", "_")]
        if go_term: components.add(go_term)

    return components

def semantic_intersection(components1, components2):
    subparts1 = set()
    for component in components1:
        subparts1.update(component.transitive_subparts())

    subparts2 = set()
    for component in components2:
        subparts2.update(component.transitive_subparts())

    common_components = subparts1 & subparts2

    cache = { component: component.transitive_subparts()↲
              for component in common_components }

    largest_common_components = set()
    for component in common_components:
        for component2 in common_components:
            if (not component2 is component) and↲
               (component in cache[component2]): break
        else:
            largest_common_components.add(component)

    return largest_common_components

@app.route(’/’)
def entry_page():
    html  = """
<html><body>
  <form action="/result">
    Protein 1: <input type="text" name="prot1"/><br/><br/>
    Protein 2: <input type="text" name="prot2"/><br/><br/>
    <input type="submit"/>
  </form>
</body></html>"""
    return html

@app.route(’/result’)
def result_page():
    prot1 = request.args.get("prot1", " ")
    prot2 = request.args.get("prot2", " ")

    components1 = search_protein(prot1)
    components2 = search_protein(prot2)

    common_components = semantic_intersection(components1,↲ components2)

    html  = """<html><body>"’"
    html += """<h3>Components for protein #1 (%s)</h3>""" % prot1
    if components1:
        html += "<br/>".join(sorted(str(component)↲
                             for component in components1))
    else:
        html += "(none)<br/>"

    html += """<h3>Components for protein #2 (%s)</h3>""" %↲ prot2
    if components2:
        html += "<br/>".join(sorted(str(component)↲
                             for component in components2))
    else:
        html += "(none)<br/>"

    html += """<h3>Possible dating sites</h3>"""
    if common_components:
        html += "<br/>".join(sorted(str(component)↲
                             for component in common_components))
    else:
        html += "(none)<br/>"

    html += """</body></html>"""
    return html

import werkzeug.serving
werkzeug.serving.run_simple("localhost", 5000, app)

为了测试我们的约会网站,这里有一些蛋白质名称的例子:胰蛋白酶,细胞色素 C,胰岛素,胰岛素降解酶,胰岛素受体,胰高血糖素,血红蛋白,弹性蛋白酶,颗粒酶 B,核心蛋白聚糖,β-2-微球蛋白,等等。

以下截图显示了获得的交友网站及其用途:

img/502592_1_En_10_Figb_HTML.jpg

img/502592_1_En_10_Figc_HTML.jpg

10.8 摘要

在这一章中,你已经学习了如何混合 Python 和 OWL,以便将 Python 方法与具有丰富语义的 OWL 类相关联。我们还看到了如何对 OWL 类和实体进行自省。

十一、使用 RDF 三元组和世界

在这一章中,我们将看到如何直接访问 Owlready 的 RDF quadstore,以及如何创建几个隔离的“世界”,每个世界都有自己的 quadstore。

11.1 RDF 三元组

RDF(资源描述框架)是资源和元数据的形式化描述的图形模型。特别是,任何 OWL 本体都可以用 RDF 图的形式来表达。RDF 图由一组 RDF 三元组(主语、谓语、宾语)组成。谓词对应于 OWL 意义上的属性。在细菌的本体中,描述个体“未知 _ 细菌”的三元组的两个例子是

(http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#unknown_bacterium,
 http://www.w3.org/1999/02/22-rdf-syntax-ns#type,
 http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#Bacterium)

(http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#unknown_bacterium,
 http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#gram_positive, true)

第一个三元组通过 RDF 谓词“类型”指示个体所属的类,第二个指示细菌的革兰氏状态。

其他更复杂的 OWL 构造函数,比如类限制,可以使用图中的几个 RDF 三元组和空白节点来描述。例如,假单胞菌类的限制“gram_positive value false”被转换成四个 RDF 三元组,如下所示:

(http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#Pseudomonas,
 http://www.w3.org/1999/02/22-rdf-syntax-ns#type,
 _:7)

(_:7,
 http://www.w3.org/1999/02/22-rdf-syntax-ns#type,
 http://www.w3.org/2002/07/owl#Restriction)

(_:7,
 http://www.w3.org/2002/07/owl#onProperty,
 http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#gram_positive)

(_:7,
 http://www.w3.org/2002/07/owl#hasValue,
 false)

这里,“_:7”是一个空节点(即一个匿名实体)。此节点的名称没有意义(并且可能因执行而异);只有它参与的关系才是重要的。

Owlready 允许用dump()方法显示一个本体的所有 RDF 三元组:

>>> from owlready2 import *
>>> onto = get_ontology("bacteria.owl").load()

>>> onto.graph.dump()
<http://lesfleursdunormal.fr/static/_downloads/bacteria.owl>↲
    <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>↲
    <http://www.w3.org/2002/07/owl#Ontology> .
<http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#Shape>↲
    <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>↲
    <http://www.w3.org/2002/07/owl#Class> .
[...]

也可以在default_world上调用dump(),以显示四元组存储中的所有 RDF 三元组(即default_world.graph.dump())

11.2 用 RDFlib 操作 RDF 三元组

11.2.1 读取 RDF 三元组

RDFlib 是一个 Python 模块,允许你操作 RDF 图和三元组。对于存储在 RDF 图中的 OWL 本体,我们可以使用 RDFlib 来操作这个图。然而,与 Owlready 不同,RDFlib 没有考虑 OWL 特有的语义,因此不允许利用 OWL 的表达能力或执行自动推理。

Owlready 使用了与 RDFlib 不同的 quadstore。但是,可以使用as_rdflib_graph()方法使 Owlready quadstore 与 RDFlib 兼容,如下所示:

>>> from rdflib import *
>>> graph = default_world.as_rdflib_graph()

产生的 graph 对象是一个与 RDFlib 兼容的 quadstore。

RDFlib 图由三个元素组成:实体(由 URI 标识并用URIRef()函数创建)、空白节点(用BNode()函数创建)和数据(整数或实数、字符串等)。在名称文字下分组,并用Literal()函数创建)。RDF 图的triples(subject, predicate, object)方法允许您浏览三元组的子集;这三个参数中的每一个都可以取值None,该值被视为通配符。因此,为了浏览具有给定主题的三元组集合,我们将为两个参数predicateobject传递值None

例如,我们可以显示关于 Staphylococcus 类的所有 RDF 三元组,如下所示(注意:为了便于阅读,已经添加了换行符,但是如果运行这个示例,这些换行符将不会出现在屏幕上):

>>> list(graph.triples((URIRef("http://lesfleursdunormal.fr/↲static/
_downloads/bacteria.owl#Staphylococcus"), None, None)))
[(rdflib.term.URIRef('http://lesfleursdunormal.fr/static/↲
                      _downloads/bacteria.owl#Staphylococcus'),
  rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-↲ns#type'),
  rdflib.term.URIRef('http://www.w3.org/2002/07/owl#Class')),

 (rdflib.term.URIRef('http://lesfleursdunormal.fr/static/↲
                      _downloads/bacteria.owl#Staphylococcus'),
  rdflib.term.URIRef('http://www.w3.org/2000/01/rdf-↲schema#subClassOf'),
  rdflib.term.URIRef('http://lesfleursdunormal.fr/static/↲
                      _downloads/bacteria.owl#Coccus')),

 (rdflib.term.URIRef('http://lesfleursdunormal.fr/static/↲
                      _downloads/bacteria.owl#Staphylococcus'),
  rdflib.term.URIRef('http://www.w3.org/2002/07/↲owl#equivalentClass'),
  rdflib.term.BNode('20'))
]

11.2.2 使用 RDFlib 创建新的 RDF 三元组

RDFlib 允许您访问三元组,也可以使用add((subject, predicate, object))方法创建新的三元组。在添加三元组时,需要指明它们将被添加到哪个本体中。这可以通过两种不同的方式实现:

  • 要么以 Owlready 的方式,用一个“有本体:……”阻止:

  • 或者像 RDFlib 一样,使用我们通过get_context()方法获得的上下文图:

>>> with onto:
...     graph.add((
...       URIRef("http://www.test.org/t.owl#MyClass"),
...       URIRef("http://www.w3.org/1999/02/22-rdf-↲syntax-ns#type"),
...       URIRef("http://www.w3.org/2002/07/↲owl#Class"),
... ))

>>> graph2 = graph.get_context(onto)
>>> graph2.add((
...     URIRef("http://www.test.org/t.owl#MyClass2"),
...     URIRef("http://www.w3.org/1999/02/22-rdf-↲syntax-ns#type"),
...     URIRef("http://www.w3.org/2002/07/owl#Class"),
... ))

get_context()方法将前一示例中的 Owlready 本体或其 IRI(以来自 RDFlib 的URIRef对象的形式)作为参数,如下例所示:

>>> graph2 = graph.get_context(URIRef("http://↲lesfleursdunormal.fr
/static/_downloads/bacteria.owl"))

RDF 空白节点可以使用图来创建。BNode()方法,如下所示:

>>> with onto:
...     new_blank_node = graph.BNode()

然后,空白节点可以与 RDFlib 一起使用:

>>> with onto:
...     graph.add((
...       URIRef("http://www.test.org/t.owl#MyClass"),
...       URIRef("http://www.w3.org/1999/02/22-rdf-syntax-↲ns#type"),
...       new_blank_node,
... ))

请注意,通过 RDFlib 添加 RDF 三元组可能不会更新 Owlready 中的相应对象,如果它们已经从 quadstore 加载。另一方面,如果对象还没有被加载,可以在创建后使用 RDFlib 加载。

11.2.3 使用 RDFlib 删除 RDF 三元组

最后,RDFlib 允许您用remove((subject, predicate, object))方法删除三元组:

>>> graph.remove((
...     URIRef("http://www.test.org/t.owl#MyClass"),
...     URIRef("http://www.w3.org/1999/02/22-rdf-syntax-↲ns#type"),
...     URIRef("http://www.w3.org/2002/07/owl#Class"),
... ))

remove()方法接受使用None作为主语、谓语和/或宾语的通配符。例如,我们可以删除主题为“ http://www.test.org/t.owl#MyClass2 的所有三元组,如下所示:

>>> graph.remove((
...     URIRef("http://www.test.org/t.owl#MyClass2"),
...     None,
...     None,
... ))

再次注意,通过 RDFlib 删除 RDF 三元组可能不会更新 Owlready 中相应的对象。

11.3 执行 SPARQL 请求

SPARQL (SPARQL 协议和 RDF 查询语言)是一种用于在 RDF 图中搜索的查询语言。这种语言有点像关系数据库的 SQL(结构化查询语言),但它专用于 RDF 图数据库。

RDFlib 包含一个 SPARQL 引擎,可以与 Owlready 一起使用。

11.3.1 使用 SPARQL 搜索

SPARQL 允许您进行比 Owlready 的search()方法更复杂的搜索;但是,对于简单的搜索,最好使用search(),因为性能更好。

RDFlib graph 对象的query()方法允许您执行 SPARQL 查询,并以 RDFlib 格式(也就是说,以URIRefBNodeLiteral的形式)返回结果。查询的 WHERE 子句由一个或多个 RDF 三元组组成,它可以包含实体(由实体的 IRI 标识)和变量,变量的名称以“?”为前缀。在下面的例子中,我们寻找所有的实体。b 类细菌,在哪里?b 是一个变量:

>>> graph = default_world.as_rdflib_graph()
>>> list(graph.query("""
... SELECT ?b WHERE {
... ?b
... <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
... <http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#Bacterium>.
... }""" ))
[(rdflib.term.URIRef('http://lesfleursdunormal.fr/static/↲
_downloads/bacteria.owl#unknown_bacterium'),)]

query_owlready()方法以同样的方式工作,但是以 Owlready 格式返回结果(即作为 Owlready 对象或 Python 数据类型):

>>> list(graph.query_owlready("""
... SELECT ?b WHERE {
... ?b
... <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
... <http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#Bacterium>.
... }""" ))
[[bacteria.unknown_bacterium]]

SPARQL 允许您执行涉及多个变量的搜索。例如,我们可以寻找所有类型为“包含”的细菌。这项研究需要两个变量,在这里注明?b(细菌)和?r(它的分组),和三个三元组。可以按如下方式完成:

>>> list(graph.query_owlready("""
... SELECT ?b WHERE {
... ?b
... <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
... <http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#Bacterium>.
...
... ?b
... <http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#has_grouping>
... ?r .
...
... ?r
... <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
... <http://lesfleursdunormal.fr/static/_downloads/bacteria.↲owl#InCluster>.
... }""" ))
[[bacteria.unknown_bacterium]]

最后,default_world.sparql_query()方法是直接执行 SPARQL 搜索并以 Owlready 格式检索结果的快捷方式(与query_owlready()一样):

>>> list(default_world.sparql_query("""..."""))

使用尖括号(<...>)之间的 IRI,可以很容易地将 Owlready 对象集成到 SPARQL 查询中,例如:

>>> individual = onto.unknown_bacterium
>>> list(default_world.sparql_query("""
... SELECT ?class WHERE {
...     <%s> a ?class .
...     ?class a <http://www.w3.org/2002/07/owl#Class> .
... }""" % individual.iri))
[[bacteria.Bacterium]]

这个 SPARQL 查询搜索单个“未知细菌”所属的所有类。在这个例子中,属性“a”是“<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>”的快捷方式。

SPARQL 前缀

Owlready 和 RDFlib 还允许使用 SPARQL 前缀,以便简化 SPARQL 查询的编写并缩短 IRI。前缀是用bind()方法声明的,如下所示:

graph.bind("prefix", "base IRI")

然后,以基本 IRI 开始的虹膜可以写成“前缀:IRI 的结束”的形式,没有尖括号。在下面的例子中,我们为 OWL 定义了一个前缀,然后我们使用这个前缀继续前面的例子:

>>> graph = default_world.as_rdflib_graph()
>>> graph.bind("owl", "http://www.w3.org/2002/07/owl#")
>>> individual = onto.unknown_bacterium
>>> list(default_world.sparql_query("""
... SELECT ?class WHERE {
...     <%s> a ?class .
...     ?class a owl:Class .
... }""" % individual.iri))
[[bacteria.Bacterium]]

11.3.3 使用 SPARQL 创建 RDF 三元组

“INSERT”类型的 SPARQL 查询允许创建 RDF 三元组。它们是用 RDFlib graph 对象的update()方法执行的。和前面的 RDFlib 一样(见 11.2.2),有必要指出三元组将在哪个本体中创建。这可以通过两种不同的方式实现:

  • 要么以 Owlready 的方式,用一个“有本体:……”阻止:

  • 或者像 RDFlib 一样,使用我们通过get_context()方法获得的上下文图:

>>> with onto:
...     graph.update("""
... INSERT {
...     <http://www.test.org/t.owl#MyClass>
...     <http://www.w3.org/1999/02/22-rdf-syntax-↲ns#type>
...     <http://www.w3.org/2002/07/owl#Class> .
... } WHERE {}""")

>>> graph  = default_world.as_rdflib_graph()
>>> graph2 = graph.get_context(onto)
>>> graph2.update("""
... INSERT {
...     <http://www.test.org/t.owl#MyClass2>
...     <http://www.w3.org/1999/02/22-rdf-syntax-ns↲#type>
...     <http://www.w3.org/2002/07/owl#Class> .
... } WHERE {}""")

更复杂的查询可以包括“WHERE”部分。以下示例查找所有类(“WHERE”部分)并向它们添加注释(“INSERT”部分):

>>> graph2.update("""
... INSERT {
...     ?class
...     <http://www.w3.org/2000/01/rdf-schema#comment>
...     "This entity is a class!."
... } WHERE {
...     ?class a <http://www.w3.org/2002/07/owl#Class> .
... }
... """)

请注意,通过 SPARQL 添加 RDF 三元组可能不会更新 Owlready 中的相应对象,如果它们已经加载到 Python 中的话。

11.3.4 使用 SPARQL 删除 RDF 三元组

RDFlib graph 对象的update()方法也允许执行“删除”查询来删除 RDF 三元组。这些请求可以包含“WHERE”部分。以下示例删除了先前添加的注释:

>>> graph2.update("""
... DELETE {
...     ?class
...     <http://www.w3.org/2000/01/rdf-schema#comment>
...     "This entity is a class!."
... } WHERE {
...     ?class a <http://www.w3.org/2002/07/owl#Class> .
... }
... """)

请注意,通过 SPARQL 删除 RDF 三元组可能不会更新 Owlready 中的相应对象,如果它们已经加载到 Python 中的话。

11.4 使用 Owlready 访问 RDF 三元组

Owlready 也有直接访问 RDF quadstore 的方法。这些方法比使用 RDFlib 更复杂,也不太标准,但它们也更快。

为了减少 quadstore 的体积,owl 已经用名为 storid (store ID)的“缩写 IRIs”替换了实体的 IRIs。这些是严格正整数形式的任意代码。_abbreviate()_unabbreviate()方法分别允许将 IRI 转换成 storid 或 storid 转换成 IRI。如果 IRI 尚未收到缩写,则_abbreviate()会自动创建一个新代码,并保存在 quadstore 中。

在下面的例子中,葡萄球菌类的 IRI 对应于 storid 324(注意:storid 的确切值可能因 quadstore 的不同而不同,这取决于本体中实体的创建顺序):

>>> default_world._abbreviate(onto.Staphylococcus.iri)
323
>>> default_world._unabbreviate(323)
'http://lesfleursdunormal.fr/static/_downloads/bacteria.owl#Staphylococcus'

任何实体的storid属性都允许您检索其 storid:

>>> onto.Staphylococcus.storid
323

_get_by_storid()方法允许相反的操作,也就是说,从 storid 中获取一个实体:

>>> default_world._get_by_storid(323)
bacteria.Staphylococcus

空节点在 quadstore 中也由 storid 表示,但它们是严格的负整数。

Owlready quadstore 使用传统形式的 RDF 三元组(主语、谓语、宾语)存储两个实体之间的关系。另一方面,实体和数据类型值之间的关系以(主语、谓语、值、数据类型)四元组的形式存储。该值可以是整数(Python int类型)、实数(Python float类型)或字符串(Python str类型)。类型可以是表示本地化字符串语言的字符串,前缀为“@”(例如“@en”或“@fr”),也可以是数据类型的 storid(参见表 4-1 中 OWL 支持的 IRIs),如果没有指定数据类型,则为 0(对应于 OWL 中的 PlainLiteral)。

_get_triples_spod_spod(subject, predicate, object_or_value, datatype)方法的行为类似于 RDFlib 的triples()方法。我们可以获得葡萄球菌类在 quadstore 中的三元组,如下所示:

>>> default_world._get_triples_spod_spod(323, None, None, None)
[(323, 6, 11, None),
 (323, 9, 321, None),
 (323, 33, -20, None)]

因为这些是对应于两个实体之间关系的三元组,所以不使用数据类型(每个元组的第四个值),它等于None

_get_obj_triples_spo_spo(subject, predicate, object)_get_data_triples_spod_spod(subject, predicate, value, type)方法的工作方式类似于_get_triples_spod_spod(),但是它们仅限于两个实体之间的关系(前者)以及实体和数据类型值之间的关系(后者)。

_unabbreviate()方法可用于解码之前获得的结果:

>>> default_world._unabbreviate(6)
'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'
>>> default_world._unabbreviate(11)
'http://www.w3.org/2002/07/owl#Class'
[...]

default_world._to_rdf(entity_or_data)方法使得将任何实体或数据类型值转换成 RDF 成为可能。当对实体调用时,它返回一对(storid,None),当对数据类型值调用时,它返回一对(value,type),如下例所示:

>>> default_world._to_rdf(8)
(8, 43)
>>> default_world._to_rdf(onto.Staphylococcus)
(323, None)

它的对应物default_world._to_python(object_or_value, datatype)方法执行相反的操作。

>>> default_world._to_python(8, 43)
8
>>> default_world._to_python(323, None)
bacteria.Staphylococcus

_has_obj_triple_spo(subject, predicate, object)_has_data_triple_spod(subject, predicate, data, datatype)方法用于验证四元组存储中 RDF 三元组的存在。例如,我们可以验证葡萄球菌类的第一个三联体的存在,如下所示:

>>> default_world._has_obj_triple_spo(323, 6, 11)
True

_del_obj_triple_spo(subject, predicate, object)_del_data_triple_spod(subject, predicate, data, datatype)方法允许删除一个或多个 RDF 三元组(在参数是None的情况下,几个三元组被删除,它充当小丑)。例如,我们可以删除葡萄球菌类的第一个三元组,如下所示:

>>> default_world._del_obj_triple_spo(323, 6, 11)

但是要小心,这些方法不会更新相应的 Owlready 对象:Staphylococcus 类在 Python 中仍然是 coccus 的子类:

>>> onto.Staphylococcus.is_a
[bacteria.Coccus]

_add_obj_triple_spo(subject, predicate, object)_add_data_triple_spod (subject, predicate, data, datatype)方法添加了一个 RDF 三元组。它们必须被应用于一个本体(而不是default_world),以便指定三元组将被插入的本体。例如,要重新创建之前删除的三元组,我们将执行以下操作:

>>> onto._add_obj_triple_spo(323, 6, 11)

同样,这两个方法不更新 Owlready Python 对象。

最后,Owlready 有许多优化的方法,用于在 quadstore 中执行特定类型的搜索。这些方法的名称遵循以下模式:

_get_<element>_triple<plural>_<inputs>_<output>()

在哪里

  • 表示在 quadstore 的哪个部分进行搜索:

    • =(空):用于在整个 quadstore 中搜索

    • = obj:用于搜索两个实体之间的关系

    • =数据:用于搜索实体和数据类型值之间的关系

  • 表示返回多少结果:

    • = s:返回在 quadstore 中找到的所有结果

    • = (empty):返回在 quadstore 中找到的第一个结果

  • 表示方法的参数。它是以下字符的组合:

    • c:本体标识符

    • s:三元组的主题(一个实体的 storid 或一个空白节点)

    • p:三元组的谓词(一个属性的 storid)

    • o:三元组的对象(实体的 storid、空白节点或数据类型值)

    • d:数据类型(空字符串、数据类型的 storid 或以“@”为前缀的两个字母的语言代码)

  • 表示方法的返回值。它是与条目相同的字符的组合。

以下是可用的优化方法:

  • _get_triples_s_p(), _get_triples_s_pod(), _get_triples_sp_od(), _get_triples_spod_spod()

  • _get_triple_sp_od()

  • _get_obj_triples_sp_o(), _get_obj_triples_sp_co(), _get_obj_triples_spo_spo(), _get_obj_triples_cspo_cspo(), _get_obj_triples_s_po( ), _get_obj_triples_po_s()

  • _get_obj_triple_po_s(), _get_obj_triple_sp_o()

  • _get_data_triples_s_pod(), _get_data_triples_sp_od(), _get_data_triples_spod_spod()

  • _get_data_triple_sp_od()

例如,_get_obj_triples_sp_o()方法只在两个实体(“对象”)之间的关系中进行搜索;它将一个主语和一个谓语(“sp”)作为参数,并返回一个对象列表(“o”)。我们可以如下获得葡萄球菌类的父类的 storid(storid 323)(6 是典型 RDF 属性的 storid):

>>> list(default_world._get_obj_triples_sp_o(323, 6))
[11]

11.5 直接查询 SQLite3 数据库

Owlready quadstore 实现为一个 SQLite3 数据库。它包含三个主表:

  • 资源,它将 IRIs 映射到 storids

  • objs,它包含两个实体之间关系的四元组

  • datas,它包含实体和数据之间关系的四元组

最后,“quads”视图是一个只读的伪表,包含来自 objs 表和 datas 表的记录(对于 objs,d = NULL)。

下表显示了这些表的模式:

|

四边形视图

| | --- | | rowid INTEGER | c 整数 | s 整数 | p 整数 | BLOB(肉球) | d 整数 |

|

约会对象

| | --- | | rowid INTEGER | c 整数 | s 整数 | p 整数 | BLOB(肉球) | d 整数 |

|

对象表

| | --- | | rowid INTEGER | c 整数 | s 整数 | p 整数 | o 整数 |

|

资源表

| | --- | | 斯托里德整数 | iri TEXT |

这些字段是

  • storid:quad store 中的标识符

  • 鸢尾:与鹳鸟有关的 iri

  • rowid:SQL 表中的行标识符

  • c:一个本体标识符——1 代表加载的第一个本体,2 代表第二个本体,依此类推

  • 学生:三元组的主题,也就是鹳鸟

  • p:三元组的谓词,即属性的 storid

  • o:三元组的对象,即 storid(用于 objs 表)、datatype 值(用于 datas 表的 integer、float 或 string)或两者中的任何一个(用于 quads 视图)

  • d:o 中值的数据类型是下列之一:

    • 实体的None(SQL 中的NULL)(在四元视图中)

    • 表示数据类型的 storid(对于 datas 或 quads 表)

    • 由两个字母组成的语言代码,形式为“@langue”,例如,“@en”表示英语,或者“@fr”表示法语(表示 datas 和 quads 表中的本地化文本)

execute()方法允许您直接在数据库上执行 SQL 查询。例如,下面的 SQL 查询可以搜索所有类型为 InCluster 的细菌(我们已经在第 11.3.1 节的 SPARQL 中执行了该查询):

>>> default_world.graph.execute("""
... SELECT q1.s
... FROM objs q1, objs q2, objs q3
... WHERE q1.p=%s AND q1.o=%s
...   AND q2.s=q1.s AND q2.p=%s
...   AND q3.s=q2.o AND q3.p=%s AND q3.o=%s
...  """% (rdf_type, onto.Bacterium.storid,
...        onto.has_grouping.storid,
...        rdf_type, onto.InCluster.storid)
...  ).fetchall()
[(327,)]
>>> default_world.graph._unabbreviate(327)
'http://lesfleursdunormal.fr/static/_downloads/bacteria.owl#↲
unknown_bacterium'

这个查询使用了三次 objs 表(这对应于 SPARQL 查询的三个三元组)。

为了帮助编写 SQL 查询,可以从 Owlready 的search()方法产生的查询中获得灵感。由search()返回的伪列表的sql_request()方法显示 SQL 查询和相应的参数(其值将替换“?”查询的)。这里有一个例子:

>>> default_world.search(iri = "*Bacteri*").sql_request()
('SELECT DISTINCT q1_1.s FROM objs q1_1, resources↲
  WHERE resources.storid = q1_1.s AND resources.iri GLOB ?',↲
['*Bacteri*'])

11.6 添加对自定义数据类型的支持

declare_datatype()全局函数允许在 Owlready 中声明附加的数据类型。该函数有四个参数:数据类型 Python 类、其 IRI、解析器函数和序列化器函数。serializer 函数负责数据类型的序列化,也就是说,将它转换为字符串。解析器函数负责相反的操作:它读取字符串并返回 Python 数据类型值。

以下示例添加了对“hexBinary”数据类型(在 XML-Schema 中定义)的支持。它首先创建一个名为“Hex”的 Python 类来管理十六进制值。然后,它定义解析器函数。这个函数读取一个十六进制值(以 XML-Schema 的格式)并返回一个十六进制实例。serializer 函数接受一个十六进制实例,并返回一个字符串格式的十六进制值(我们删除了前两个字符,用“[2:]”,因为 Python 在十六进制值的开头添加了“0x”,而 XML-Schema 没有)。最后,我们声明新的数据类型。

>>> class Hex(object):
...     def __init__(self, value):
...         self.value = value

>>> def parse_hex(s):
...     return Hex(int(s, 16))

>>> def serialize_hex(x):
...     h = hex(x.value)[2:]
...     if len(h) % 2 != 0: return "%s" % h
...     return h

>>> declare_datatype(Hex, "http://www.w3.org/2001/↲XMLSchema#hexBinary", parse_hex, serialize_hex)

我们现在可以创建一个本体,并在数据属性中使用十六进制值:

>>> onto = get_ontology("http://www.test.org/test_hex.owl")

>>> with onto:
...     class has_hex(Thing >> Hex): pass
...     class MyClass(Thing): pass
...     c = MyClass()
...     c.has_hex.append(Hex(14))

我们可以验证 quadstore 的内容:

>>> onto.graph.dump()
<http://www.test.org/t.owl>
      <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>
      <http://www.w3.org/2002/07/owl#Ontology> .
[...]
<http://www.test.org/t.owl#myclass1>
      <http://www.test.org/t.owl#has_hex>
      0e^^<http://www.w3.org/2001/XMLSchema#hexBinary> .

注意,该值以十六进制格式存储在 quadstore 中(这里,“0e”是 14 的十六进制表示)。

我们也可以使用 XML 模式 hexBinary 数据类型加载本体。但是,注意在加载这样的本体之前必须调用declare_datatype()

11.7 创造几个孤立的世界

Owlready 使得创造几个孤立的“世界”成为可能,有时被称为“语言的宇宙”。这使得可以独立地多次加载相同的本体,也就是说,在一个副本上进行的修改不会影响另一个副本。它对于同时加载同一个本体的几个不兼容版本也很有用。

默认情况下,Owlready 只创建了一个名为default_world的世界。World类允许你创造一个独立于default_world的新世界。

>>> from owlready2 import *
>>> my_world = World()

每个世界都存储在一个单独的 quadstore 中。每个都可以通过set_backend()方法存储在 RAM 和/或磁盘的不同文件中(参见第 4.7 节)。一般来说,我们应用于default_world的所有方法都可以应用于世界,例如search()as_rdflib_graph()。此外,几个全局函数实际上是default_world方法的快捷方式。因此,当使用多个世界时,您必须调用方法,而不是全局快捷键。以下是这些简化函数和相应方法的列表:

|

快捷全局功能

|

对应方法

| | --- | --- | | get_ontology() | World.get_ontology() | | get_namespace() | World.get_namespace() | | IRIS[iri] | World[iri] | | sync_reasoner() | sync_reasoner(world) |

下面的例子说明了世界的隔离,创建一个与default_world分离的新世界,然后在每个世界中加载细菌的本体。在default_world中,我们删除了葡萄球菌类,但它仍然存在于另一个世界。

>>> onto = get_ontology("http://lesfleursdunormal.fr/↲
static/_downloads/bacteria.owl#").load()
>>> onto2 = my_world.get_ontology("http://lesfleursdunormal.↲fr/
static/_downloads/bacteria.owl#").load()
>>> destroy_entity(onto.Staphylococcus)
>>> onto.Staphylococcus
None
>>> onto2.Staphylococcus
bacteria.Staphylococcus

最后,OWL ThingNothing类的subclasses()descendants()方法假设它们是为default_world调用的(事实上,这些类是所有世界共享的)。如果不是这种情况,则需要将所需的世界作为参数传递,例如:

>>> list(Thing.descendants(world = my_world))

11.8 摘要

在本章中,您已经学习了如何直接访问 Owlready quadstore 中的 RDF 三元组并执行 SPARQL 查询。我们还看到了如何创建几个孤立的世界,例如,加载同一个本体的几个副本。