{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "# 家庭关系本体\n",
    "\n",
    "此示例是 [AI for Beginners Curriculum](http://github.com/microsoft/ai-for-beginners) 的一部分，灵感来源于[这篇博客文章](https://habr.com/post/270857/)。\n",
    "\n",
    "我总是觉得记住家庭成员之间的各种关系很困难。在这个示例中，我们将使用一个定义家庭关系的本体，以及实际的家谱树，展示如何通过自动推理来找到所有的亲属关系。\n",
    "\n",
    "### 获取家谱树\n",
    "\n",
    "作为示例，我们将使用[罗曼诺夫沙皇家族](https://en.wikipedia.org/wiki/House_of_Romanov)的家谱。描述家庭关系最常见的格式是 [GEDCOM](https://en.wikipedia.org/wiki/GEDCOM)。我们将使用 GEDCOM 格式的罗曼诺夫家族树：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "trusted": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0 HEAD\n",
      "1 CHAR UTF8\n",
      "1 GEDC\n",
      "2 VERS 5.5\n",
      "0 @0@ INDI\n",
      "1 NAME Mihail Fedorovich /Romanov/\n",
      "1 SEX M\n",
      "1 BIRT\n",
      "2 DATE 1613\n",
      "1 DEAT \n",
      "2 DATE 1645\n",
      "1 FAMS @41@\n",
      "0 @1@ INDI\n",
      "1 NAME Evdokija Lukjanovna /Streshneva/\n",
      "1 SEX F\n"
     ]
    }
   ],
   "source": [
    "!head -15 data/tsars.ged"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "要使用GEDCOM文件，我们可以使用`python-gedcom`库：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "trusted": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Collecting python-gedcom\n",
      "  Downloading python_gedcom-1.0.0-py2.py3-none-any.whl (35 kB)\n",
      "Installing collected packages: python-gedcom\n",
      "Successfully installed python-gedcom-1.0.0\n"
     ]
    }
   ],
   "source": [
    "import sys\n",
    "!{sys.executable} -m pip install python-gedcom"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这个库解决了一些文件解析的技术问题，但它仍然为我们提供了对树中所有个人和家庭的相当低级的访问。以下是我们如何解析文件并显示所有个人列表的方法：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "trusted": true
   },
   "outputs": [],
   "source": [
    "from gedcom.parser import Parser\n",
    "from gedcom.element.individual import IndividualElement\n",
    "from gedcom.element.family import FamilyElement\n",
    "g = Parser()\n",
    "g.parse_file('data/tsars.ged')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "scrolled": true,
    "trusted": true
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[('@0@', ('Mihail Fedorovich', 'Romanov')),\n",
       " ('@1@', ('Evdokija Lukjanovna', 'Streshneva')),\n",
       " ('@2@', ('Aleksej Mihajlovich', 'Romanov')),\n",
       " ('@3@', ('Marija Ilinichna', 'Miloslavskaja')),\n",
       " ('@4@', ('Natalja Kirillovna', 'Naryshkina')),\n",
       " ('@5@', ('Marfa Matveevna', 'Apraksina')),\n",
       " ('@6@', ('Fedor Alekseevich', 'Romanov')),\n",
       " ('@7@', ('Sofja Aleksevna', 'Romanova')),\n",
       " ('@8@', ('Ivan V Alekseevich', 'Romanov')),\n",
       " ('@9@', ('Praskovja Fedorovna', 'Saltykova')),\n",
       " ('@10@', ('Ekaterina Ivanovna', 'Romanova')),\n",
       " ('@11@', ('Anna Ivanovna', 'Romanova')),\n",
       " ('@12@', ('Fridrih Vilgelm', 'Kurlandskij')),\n",
       " ('@13@', ('Karl Leopold', 'Meklenburg-Shverinskij')),\n",
       " ('@14@', ('Anna Leopoldovna', 'Meklenburg-Shverinskaja')),\n",
       " ('@15@', ('Anton Ulrih', 'Braunshvejg-Volfenbjuttelskij')),\n",
       " ('@16@', ('Ivan VI Antonovich', 'Braunshvejg-Volfenbjuttelskij')),\n",
       " ('@17@', ('Petr I Alekseevich', 'Romanov')),\n",
       " ('@18@', ('Evdokija Fedorovna', 'Lopuhina')),\n",
       " ('@19@', ('Ekaterina I Alekseevna', 'Mihajlova')),\n",
       " ('@20@', ('Aleksej Petrovich', 'Romanov')),\n",
       " ('@21@', ('Sharlotta Kristina', 'Braunshvejg-Volfenbjuttelskaja')),\n",
       " ('@22@', ('Petr II Alekseevich', 'Romanov')),\n",
       " ('@23@', ('Anna Petrovna', 'Romanova')),\n",
       " ('@24@', ('Elizaveta Petrovna', 'Romanova')),\n",
       " ('@25@', ('Karl Fridrih', 'Golshtejn-Gottorpskij')),\n",
       " ('@26@', ('Petr III Fedorovich', 'Romanov')),\n",
       " ('@27@', ('Ekaterina II', 'Alekseevna')),\n",
       " ('@28@', ('Pavel I Petrovich', 'Romanov')),\n",
       " ('@29@', ('Natalja Alekseevna', 'Gessen-Darmshtadskaja')),\n",
       " ('@30@', ('Marija Fedorovna', 'Vjurtembergskaja')),\n",
       " ('@31@', ('Aleksandr I Pavlovich', 'Romanov')),\n",
       " ('@32@', ('Elizaveta Alekseevna', 'Baden-Durlahskaja')),\n",
       " ('@33@', ('Nikolaj I Pavlovich', 'Romanov')),\n",
       " ('@34@', ('Aleksandra Fedorovna', 'Prusskaja')),\n",
       " ('@35@', ('Aleksandr II Nikolaevich', 'Romanov')),\n",
       " ('@36@', ('Marija Aleksandrovna', 'Gessenskaja')),\n",
       " ('@37@', ('Aleksandr III Aleksandrovich', 'Romanov')),\n",
       " ('@38@', ('Marija Fedorovna', 'Datskaja')),\n",
       " ('@39@', ('Nikolaj II Aleksandrovich', 'Romanov')),\n",
       " ('@40@', ('Aleksandra Fedorovna', 'Gessenskaja'))]"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "d = g.get_element_dictionary()\n",
    "[ (k,v.get_name()) for k,v in d.items() if isinstance(v,IndividualElement)]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "以下是我们获取家庭信息的方法。请注意，这会给我们一个**标识符**列表，如果我们想要更清楚，需要将它们转换为名称：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[('@41@', ['@0@', '@1@', '@2@']),\n",
       " ('@42@', ['@2@', '@3@', '@6@', '@7@', '@8@']),\n",
       " ('@43@', ['@8@', '@9@', '@10@', '@11@']),\n",
       " ('@44@', ['@13@', '@10@', '@14@']),\n",
       " ('@45@', ['@15@', '@14@', '@16@']),\n",
       " ('@46@', ['@2@', '@4@', '@17@']),\n",
       " ('@47@', ['@17@', '@18@', '@20@']),\n",
       " ('@48@', ['@20@', '@21@', '@22@']),\n",
       " ('@49@', ['@17@', '@19@', '@23@', '@24@']),\n",
       " ('@50@', ['@25@', '@23@', '@26@']),\n",
       " ('@51@', ['@26@', '@27@', '@28@']),\n",
       " ('@52@', ['@28@', '@30@', '@31@', '@33@']),\n",
       " ('@53@', ['@33@', '@34@', '@35@']),\n",
       " ('@54@', ['@35@', '@36@', '@37@']),\n",
       " ('@55@', ['@37@', '@38@', '@39@'])]"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "d = g.get_element_dictionary()\n",
    "[ (k,[x.get_value() for x in v.get_child_elements()]) for k,v in d.items() if isinstance(v,FamilyElement)]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 获取家庭本体\n",
    "\n",
    "接下来，让我们看看[家庭本体](https://raw.githubusercontent.com/blokhin/genealogical-trees/master/data/header.ttl)，它被定义为一组语义网三元组。这个本体定义了诸如 `isUncleOf`、`isCousinOf` 等许多关系。所有这些关系都是基于基本谓词 `isMotherOf`、`isFatherOf`、`isBrotherOf` 和 `isSisterOf` 定义的。我们将使用自动推理，通过本体推导出所有其他关系。\n",
    "\n",
    "以下是 `isAuntOf` 属性的一个示例定义，它被定义为 `isSisterOf` 和 `isParentOf` 的组合（*姑/姨是某人父母的姐妹*）。\n",
    "\n",
    "```\n",
    "fhkb:isAuntOf a owl:ObjectProperty ;\n",
    "    rdfs:domain fhkb:Woman ;\n",
    "    rdfs:range fhkb:Person ;\n",
    "    owl:propertyChainAxiom ( fhkb:isSisterOf fhkb:isParentOf ) .\n",
    "```\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "trusted": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "@prefix fhkb: <http://www.example.com/genealogy.owl#> .\n",
      "@prefix owl: <http://www.w3.org/2002/07/owl#> .\n",
      "@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n",
      "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n",
      "@prefix xml: <http://www.w3.org/XML/1998/namespace> .\n",
      "@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n",
      "\n",
      "<http://www.example.com/genealogy.owl#> a owl:Ontology .\n",
      "\n",
      "fhkb:DomainEntity a owl:Class .\n",
      "\n",
      "fhkb:Man a owl:Class ;\n",
      "    owl:equivalentClass [ a owl:Class ;\n",
      "            owl:intersectionOf ( fhkb:Person [ a owl:Restriction ;\n",
      "                        owl:onProperty fhkb:hasSex ;\n",
      "                        owl:someValuesFrom fhkb:Male ] ) ] .\n",
      "\n",
      "fhkb:Woman a owl:Class ;\n",
      "    owl:equivalentClass [ a owl:Class ;\n",
      "            owl:intersectionOf ( fhkb:Person [ a owl:Restriction ;\n"
     ]
    }
   ],
   "source": [
    "!head -20 data/onto.ttl"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 构建推理本体\n",
    "\n",
    "为了简化操作，我们将创建一个本体文件，其中包括家庭本体中的原始规则，以及来自我们 GEDCOM 文件的个人事实。我们将遍历 GEDCOM 文件，提取有关家庭和个人的信息，并将其转换为三元组。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "trusted": true
   },
   "outputs": [],
   "source": [
    "!cp data/onto.ttl .\n",
    "\n",
    "gedcom_dict = g.get_element_dictionary()\n",
    "individuals, marriages = {}, {}\n",
    "\n",
    "def term2id(el):\n",
    "    return \"i\" + el.get_pointer().replace('@', '').lower()\n",
    "\n",
    "out = open(\"onto.ttl\",\"a\")\n",
    "\n",
    "for k, v in gedcom_dict.items():\n",
    "    if isinstance(v,IndividualElement):\n",
    "        children, siblings = set(), set()\n",
    "        idx = term2id(v)\n",
    "\n",
    "        title = v.get_name()[0] + \" \" + v.get_name()[1]\n",
    "        title = title.replace('\"', '').replace('[', '').replace(']', '').replace('(', '').replace(')', '').strip()\n",
    "\n",
    "        own_families = g.get_families(v, 'FAMS')\n",
    "        for fam in own_families:\n",
    "            children |= set(term2id(i) for i in g.get_family_members(fam, \"CHIL\"))\n",
    "\n",
    "        parent_families = g.get_families(v, 'FAMC')\n",
    "        if len(parent_families):\n",
    "            for member in g.get_family_members(parent_families[0], \"CHIL\"): # NB adoptive families i.e len(parent_families)>1 are not considered (TODO?)\n",
    "                if member.get_pointer() == v.get_pointer():\n",
    "                    continue\n",
    "                siblings.add(term2id(member))\n",
    "\n",
    "        if idx in individuals:\n",
    "            children |= individuals[idx].get('children', set())\n",
    "            siblings |= individuals[idx].get('siblings', set())\n",
    "        individuals[idx] = {'sex': v.get_gender().lower(), 'children': children, 'siblings': siblings, 'title': title}\n",
    "\n",
    "    elif isinstance(v,FamilyElement):\n",
    "        wife, husb, children = None, None, set()\n",
    "        children = set(term2id(i) for i in g.get_family_members(v, \"CHIL\"))\n",
    "\n",
    "        try:\n",
    "            wife = g.get_family_members(v, \"WIFE\")[0]\n",
    "            wife = term2id(wife)\n",
    "            if wife in individuals: individuals[wife]['children'] |= children\n",
    "            else: individuals[wife] = {'children': children}\n",
    "        except IndexError: pass\n",
    "        try:\n",
    "            husb = g.get_family_members(v, \"HUSB\")[0]\n",
    "            husb = term2id(husb)\n",
    "            if husb in individuals: individuals[husb]['children'] |= children\n",
    "            else: individuals[husb] = {'children': children}\n",
    "        except IndexError: pass\n",
    "\n",
    "        if wife and husb: marriages[wife + husb] = (term2id(v), wife, husb)\n",
    "\n",
    "for idx, val in individuals.items():\n",
    "    added_terms = ''\n",
    "    if val['sex'] == 'f':\n",
    "        parent_predicate, sibl_predicate = \"isMotherOf\", \"isSisterOf\"\n",
    "    else:\n",
    "        parent_predicate, sibl_predicate = \"isFatherOf\", \"isBrotherOf\"\n",
    "    if len(val['children']):\n",
    "        added_terms += \" ;\\n    fhkb:\" + parent_predicate + \" \" + \", \".join([\"fhkb:\" + i for i in val['children']])\n",
    "    if len(val['siblings']):\n",
    "        added_terms += \" ;\\n    fhkb:\" + sibl_predicate + \" \" + \", \".join([\"fhkb:\" + i for i in val['siblings']])\n",
    "    out.write(\"fhkb:%s a owl:NamedIndividual, owl:Thing%s ;\\n    rdfs:label \\\"%s\\\" .\\n\" % (idx, added_terms, val['title']))\n",
    "\n",
    "for k, v in marriages.items():\n",
    "    out.write(\"fhkb:%s a owl:NamedIndividual, owl:Thing ;\\n    fhkb:hasFemalePartner fhkb:%s ;\\n    fhkb:hasMalePartner fhkb:%s .\\n\" % v)\n",
    "\n",
    "out.write(\"[] a owl:AllDifferent ;\\n    owl:distinctMembers (\")\n",
    "for idx in individuals.keys():\n",
    "    out.write(\"    fhkb:\" + idx)\n",
    "for k, v in marriages.items():\n",
    "    out.write(\"    fhkb:\" + v[0])\n",
    "out.write(\"    ) .\")\n",
    "out.close()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "trusted": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "    fhkb:hasFemalePartner fhkb:i34 ;\n",
      "    fhkb:hasMalePartner fhkb:i33 .\n",
      "fhkb:i54 a owl:NamedIndividual, owl:Thing ;\n",
      "    fhkb:hasFemalePartner fhkb:i36 ;\n",
      "    fhkb:hasMalePartner fhkb:i35 .\n",
      "fhkb:i55 a owl:NamedIndividual, owl:Thing ;\n",
      "    fhkb:hasFemalePartner fhkb:i38 ;\n",
      "    fhkb:hasMalePartner fhkb:i37 .\n",
      "[] a owl:AllDifferent ;\n",
      "    owl:distinctMembers (    fhkb:i0    fhkb:i1    fhkb:i2    fhkb:i3    fhkb:i4    fhkb:i5    fhkb:i6    fhkb:i7    fhkb:i8    fhkb:i9    fhkb:i10    fhkb:i11    fhkb:i12    fhkb:i13    fhkb:i14    fhkb:i15    fhkb:i16    fhkb:i17    fhkb:i18    fhkb:i19    fhkb:i20    fhkb:i21    fhkb:i22    fhkb:i23    fhkb:i24    fhkb:i25    fhkb:i26    fhkb:i27    fhkb:i28    fhkb:i29    fhkb:i30    fhkb:i31    fhkb:i32    fhkb:i33    fhkb:i34    fhkb:i35    fhkb:i36    fhkb:i37    fhkb:i38    fhkb:i39    fhkb:i40    fhkb:i41    fhkb:i42    fhkb:i43    fhkb:i44    fhkb:i45    fhkb:i46    fhkb:i47    fhkb:i48    fhkb:i49    fhkb:i50    fhkb:i51    fhkb:i52    fhkb:i53    fhkb:i54    fhkb:i55    ) ."
     ]
    }
   ],
   "source": [
    "!tail onto.ttl"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 推理操作\n",
    "\n",
    "现在我们希望能够使用这个本体进行推理和查询。我们将使用 [RDFLib](https://github.com/RDFLib)，一个用于读取不同格式的 RDF 图、查询等操作的库。\n",
    "\n",
    "对于逻辑推理，我们将使用 [OWL-RL](https://github.com/RDFLib/OWL-RL) 库，它允许我们构建 RDF 图的**闭包**，即添加所有可以推导出的概念和关系。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "trusted": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Requirement already satisfied: rdflib in /home/rg/anaconda3/envs/ai4beg/lib/python3.11/site-packages (6.3.2)\n",
      "Requirement already satisfied: isodate<0.7.0,>=0.6.0 in /home/rg/anaconda3/envs/ai4beg/lib/python3.11/site-packages (from rdflib) (0.6.1)\n",
      "Requirement already satisfied: pyparsing<4,>=2.1.0 in /home/rg/anaconda3/envs/ai4beg/lib/python3.11/site-packages (from rdflib) (3.0.9)\n",
      "Requirement already satisfied: six in /home/rg/anaconda3/envs/ai4beg/lib/python3.11/site-packages (from isodate<0.7.0,>=0.6.0->rdflib) (1.16.0)\n",
      "Collecting git+https://github.com/RDFLib/OWL-RL.git\n",
      "  Cloning https://github.com/RDFLib/OWL-RL.git to /tmp/pip-req-build-lbfzwi3m\n",
      "  Running command git clone --filter=blob:none --quiet https://github.com/RDFLib/OWL-RL.git /tmp/pip-req-build-lbfzwi3m\n",
      "  Resolved https://github.com/RDFLib/OWL-RL.git to commit a77e1791b88b54aace609bc6000aac14c7add4ff\n",
      "  Preparing metadata (setup.py) ... \u001b[?25ldone\n",
      "\u001b[?25hRequirement already satisfied: rdflib>=6.0.2 in /home/rg/anaconda3/envs/ai4beg/lib/python3.11/site-packages (from owlrl==6.0.2) (6.3.2)\n",
      "Requirement already satisfied: isodate<0.7.0,>=0.6.0 in /home/rg/anaconda3/envs/ai4beg/lib/python3.11/site-packages (from rdflib>=6.0.2->owlrl==6.0.2) (0.6.1)\n",
      "Requirement already satisfied: pyparsing<4,>=2.1.0 in /home/rg/anaconda3/envs/ai4beg/lib/python3.11/site-packages (from rdflib>=6.0.2->owlrl==6.0.2) (3.0.9)\n",
      "Requirement already satisfied: six in /home/rg/anaconda3/envs/ai4beg/lib/python3.11/site-packages (from isodate<0.7.0,>=0.6.0->rdflib>=6.0.2->owlrl==6.0.2) (1.16.0)\n"
     ]
    }
   ],
   "source": [
    "!{sys.executable} -m pip install rdflib\n",
    "!{sys.executable} -m pip install git+https://github.com/RDFLib/OWL-RL.git"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "让我们打开本体文件，看看它包含多少个三元组：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "trusted": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Triplets found:669\n"
     ]
    }
   ],
   "source": [
    "import rdflib\n",
    "from owlrl import DeductiveClosure, OWLRL_Extension\n",
    "\n",
    "g = rdflib.Graph()\n",
    "g.parse(\"onto.ttl\", format=\"turtle\")\n",
    "\n",
    "print(\"Triplets found:%d\" % len(g))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在让我们构建闭包，看看三元组的数量如何增加：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "trusted": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Triplets after inference:4246\n"
     ]
    }
   ],
   "source": [
    "DeductiveClosure(OWLRL_Extension).expand(g)\n",
    "print(\"Triplets after inference:%d\" % len(g))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 查询亲属关系\n",
    "\n",
    "现在我们可以查询图谱，查看人与人之间的不同关系。我们可以结合使用 **SPARQL** 语言和 `query` 方法。在我们的例子中，让我们看看家谱中所有的**叔叔**：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "trusted": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Fedor Alekseevich Romanov is uncle of Ekaterina Ivanovna Romanova\n",
      "Aleksandr I Pavlovich Romanov is uncle of Aleksandr II Nikolaevich Romanov\n",
      "Fedor Alekseevich Romanov is uncle of Anna Ivanovna Romanova\n"
     ]
    }
   ],
   "source": [
    "qres = g.query(\n",
    "    \"\"\"SELECT DISTINCT ?aname ?bname\n",
    "       WHERE {\n",
    "          ?a fhkb:isUncleOf ?b .\n",
    "          ?a rdfs:label ?aname .\n",
    "          ?b rdfs:label ?bname .\n",
    "       }\"\"\")\n",
    "\n",
    "for row in qres:\n",
    "    print(\"%s is uncle of %s\" % row)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以尝试不同的家庭关系。例如，可以查看 `isAncestorOf` 关系，它递归地定义了某个人的所有祖先。\n",
    "\n",
    "最后，让我们整理一下！\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "trusted": true
   },
   "outputs": [],
   "source": [
    "!rm onto.ttl"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n---\n\n**免责声明**：  \n本文档使用AI翻译服务[Co-op Translator](https://github.com/Azure/co-op-translator)进行翻译。虽然我们尽力确保翻译的准确性，但请注意，自动翻译可能包含错误或不准确之处。原始语言的文档应被视为权威来源。对于关键信息，建议使用专业人工翻译。我们不对因使用此翻译而产生的任何误解或误读承担责任。\n"
   ]
  }
 ],
 "metadata": {
  "interpreter": {
   "hash": "86193a1ab0ba47eac1c69c1756090baa3b420b3eea7d4aafab8b85f8b312f0c5"
  },
  "kernelspec": {
   "display_name": "Python 3.6",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.2"
  },
  "coopTranslator": {
   "original_hash": "6537d5597320e27b6052b4377b8ff8bb",
   "translation_date": "2025-08-31T10:05:39+00:00",
   "source_file": "lessons/2-Symbolic/FamilyOntology.ipynb",
   "language_code": "zh"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}