wikidataからnetoworkxをかましてmermaid記法で紫式部の系図を作成

# 概要
　標題のとおりですが、以下の内容になります。
- wikidataから紫式部の先祖、子孫のデータをjsonで抽出
- jsonから配列データを作成し、netoworkxに入力、距離の近い（〇親等）データを抽出
- mermaid記法で紫式部の系図を作成

# wikidata
 wikidataから、紫式部の先祖、子孫のデータをjsonで抽出します。
Wikidata Query Service を使って、以下のクエリを入力すると、
135件のデータが出力できます（2024/8/12現在）。
Q81731 が紫式部で、P22が父、P25が母で、述語の最後にアスタリスクをつけているのが ZeroOrMorePath というやつです。
```
SELECT DISTINCT  ?human ?father ?mother ?humanLabel ?fatherLabel ?motherLabel ?sex
WHERE
{
{wd:Q81731 wdt:P40* ?human .
    ?human wdt:P31 wd:Q5 .      
    OPTIONAL{?human wdt:P22 ?father .}
    OPTIONAL{?human wdt:P25 ?mother .}
    OPTIONAL{?human wdt:P21 ?sex .}
}
UNION
{
wd:Q81731 wdt:P22* ?human .
    ?human wdt:P31 wd:Q5 .      
    OPTIONAL{?human wdt:P22 ?father .}
    OPTIONAL{?human wdt:P25 ?mother .}
    OPTIONAL{?human wdt:P21 ?sex .}
}
UNION
{
wd:Q81731 wdt:P25* ?human .
    ?human wdt:P31 wd:Q5 .      
    OPTIONAL{?human wdt:P22 ?father .}
    OPTIONAL{?human wdt:P25 ?mother .}
    OPTIONAL{?human wdt:P21 ?sex .}
  }

 SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],ja" }
}
```
ちなみに、以下のURLで、上記のクエリにアクセスできます。
https://w.wiki/Atks

これを、APIでjsonで取り出し、配列を作ります。
```python
# wikidata API からJSONデータを取得
import requests
import re
import pandas as pd

def wikidata2json(loc):
    header= {"content-type": "application/json"}
    r = requests.get(loc,headers=header)
    return r.json()
    
# JSONからリストを生成
def json2list(data):
    print(type(data))
    data_human = data['results']['bindings']
    list_human = []
    paren = re.compile('[\(\)（） ]') 
    for i in data_human:
        # print(i)
        human = i['humanLabel']['value']
        human_id = i['human']['value'].replace(r'http://www.wikidata.org/entity/', '')

        # ネストされたdictにアクセスしたいが、存在しないときはNone
        father = i.get('fatherLabel', {}).get('value', i['humanLabel']['value']+'の父')
        father_id = i.get('father', {}).get('value', human_id +'_father').replace(r'http://www.wikidata.org/entity/', '')
        mother = i.get('motherLabel', {}).get('value', i['humanLabel']['value']+'の母')
        mother_id = i.get('mother', {}).get('value', human_id +'_mother').replace(r'http://www.wikidata.org/entity/', '')
        human = paren.sub('//', human) # △などmarmed で使えない文字がいくつかある
        father = paren.sub('//', father)
        mother = paren.sub('//', mother)
        # mother = i.get('mother', {}).get('value', 0)
        sex_qid = i.get('sex', {}).get('value', 0)
        if sex_qid == 'http://www.wikidata.org/entity/Q6581072':
            sex = 2
        elif sex_qid == 'http://www.wikidata.org/entity/Q6581097':
            sex = 1
        else:
            sex = 0
        list_human.append(['FA00', human, father, mother, sex, 0, human_id, father_id, mother_id])
    return list_human

# 父、母の行が存在しない場合に作成して追加
def single_parent(df_human):
    # new_row = ['Family_ID','Individual_ID','Paternal_ID','Maternal_ID','Sex','Phenotype','human','father','mother']
    for index, row in df_human.iterrows():
        # DataFrame全体ですべての値に存在しなければTrue、そうでなければFalse
        # https://codeseterpie.com/blog/80n4bt37f/
        # 母不在の場合
        if bool(df_human.Individual_ID.ne(row['Maternal_ID']).all().all()):
            new_row = ['FA00', row['Paternal_ID'], 0, 0, 2, 0, 0, 0, 0]
            df_human.loc[str(index)+'_nomaternal'] = new_row
        # 父不在の場合
        if bool(df_human.Individual_ID.ne(row['Paternal_ID']).all().all()):
            new_row = ['FA00', row['Paternal_ID'], 0, 0, 1, 0, 0, 0, 0]
            df_human.loc[str(index)+'_nopaternal'] = new_row
    #条件にマッチしたIndexを取得
    drop_index = df_human.index[df_human['Individual_ID'] == 0]
    #条件にマッチしたIndexを削除
    df_human = df_human.drop(drop_index)
    return df_human

# spouse(配偶者)列追加
def add_spouse(df_human):
    df_human['spouse'] = df_human.apply(lambda x: f"{x['father']}-{x['mother']}", axis=1)
    return df_human

loc ='https://query.wikidata.org/sparql?query=SELECT%20DISTINCT%20%20%3Fhuman%20%3Ffather%20%3Fmother%20%3FhumanLabel%20%3FfatherLabel%20%3FmotherLabel%20%3Fsex%0AWHERE%0A%7B%0A%7Bwd%3AQ81731%20wdt%3AP40%2a%20%3Fhuman%20.%0A%20%20%20%20%3Fhuman%20wdt%3AP31%20wd%3AQ5%20.%20%20%20%20%20%20%0A%20%20%20%20OPTIONAL%7B%3Fhuman%20wdt%3AP22%20%3Ffather%20.%7D%0A%20%20%20%20OPTIONAL%7B%3Fhuman%20wdt%3AP25%20%3Fmother%20.%7D%0A%20%20%20%20OPTIONAL%7B%3Fhuman%20wdt%3AP21%20%3Fsex%20.%7D%0A%7D%0AUNION%0A%7B%0Awd%3AQ81731%20wdt%3AP22%2a%20%3Fhuman%20.%0A%20%20%20%20%3Fhuman%20wdt%3AP31%20wd%3AQ5%20.%20%20%20%20%20%20%0A%20%20%20%20OPTIONAL%7B%3Fhuman%20wdt%3AP22%20%3Ffather%20.%7D%0A%20%20%20%20OPTIONAL%7B%3Fhuman%20wdt%3AP25%20%3Fmother%20.%7D%0A%20%20%20%20OPTIONAL%7B%3Fhuman%20wdt%3AP21%20%3Fsex%20.%7D%0A%7D%0AUNION%0A%7B%0Awd%3AQ81731%20wdt%3AP25%2a%20%3Fhuman%20.%0A%20%20%20%20%3Fhuman%20wdt%3AP31%20wd%3AQ5%20.%20%20%20%20%20%20%0A%20%20%20%20OPTIONAL%7B%3Fhuman%20wdt%3AP22%20%3Ffather%20.%7D%0A%20%20%20%20OPTIONAL%7B%3Fhuman%20wdt%3AP25%20%3Fmother%20.%7D%0A%20%20%20%20OPTIONAL%7B%3Fhuman%20wdt%3AP21%20%3Fsex%20.%7D%0A%20%20%7D%0A%0A%20SERVICE%20wikibase%3Alabel%20%7B%20bd%3AserviceParam%20wikibase%3Alanguage%20%22%5BAUTO_LANGUAGE%5D%2Cja%22%20%7D%0A%7D&format=json'

# wikidataからjsonを取得
data =wikidata2json(loc)
# jsonから配列に変換
list_human = json2list(data)
# 配列を pandas dataframeに変換

df_human = pd.DataFrame(list_human, columns=['#Family_ID','Individual_ID','Paternal_ID','Maternal_ID','Sex','Phenotype','human','father','mother'])
df_human = single_parent(df_human)
# 配偶者蘭を追加
df_human = add_spouse(df_human)
# 重複を削除
df_human.drop_duplicates(inplace=True)

# dataframeから配列の作成
#条件にマッチしたIndexを取得
def drop_index(df):
    df_drop_index= df.index[df['spouse'] == '0-0']
    #条件にマッチしたIndexを削除
    return df.drop(df_drop_index)

# 配偶者spouseから、子個人Individual へ
person_spouse = df_human[['spouse','Individual_ID']]
person_spouse = drop_index(person_spouse)
# person_spouse = person_spouse.reindex(columns=['spouse','Individual_ID'])
# person_spouse
# 配列にする
person_spouse_list = person_spouse.values.tolist()

# 父から配偶者spouse
paternal_spouse = df_human[['Paternal_ID','spouse']]
paternal_spouse = drop_index(paternal_spouse)
# paternal_spouse
paternal_spouse_list = paternal_spouse.values.tolist()
# print(paternal_spouse_list)

# 母から配偶者
maternal_spouse = df_human[['Maternal_ID','spouse']]
maternal_spouse = drop_index(maternal_spouse)
# maternal_spouse
maternal_spouse_list = maternal_spouse.values.tolist()  #.str.spli(',')  #   .to_numpy() #.values
# print(maternal_spouse_list)

# print(person_spouse_list + paternal_spouse_list + maternal_spouse_list)
pedigree_list = person_spouse_list + paternal_spouse_list + maternal_spouse_list
```

# networkx
このままだと、系図を描画したときに、細かすぎるので、networkx に入れて、抽出します。

```python
# 父母が０をdrop 
df_human.columns
df_human_2 = df_human[df_human['Paternal_ID'] != 0]
df_human_2 = df_human_2[df_human_2['Maternal_ID'] != 0]
df_human_2[['Individual_ID','Paternal_ID','Maternal_ID']] #.to_dict()

# 'Paternal_ID','Maternal_ID'のタプルを作って、辞書にするdf_human_2['Paternal_ID'])))
df_human_2['tuple'] = list(zip(df_human_2['Paternal_ID'], df_human_2['Maternal_ID']))
adjacency_dict = dict(zip(df_human_2['Individual_ID'], df_human_2['tuple']))

H = nx.DiGraph(adjacency_dict)  # 有向グラフ (Directed Graph)
list(H.edges())

nx.draw(H, node_color='lightblue', with_labels = True, font_family='Meiryo')
```
ちなみにそのまま描画すると、以下のように意味不明です。
![グラフ画像](https://github.com/wonox/pedigree/blob/main/output.png?raw=trueL "グラフ画像")

## エッジをソースから幅優先探索で反復する。

```python
import itertools
# 先祖と子孫
bfs_G1 = list(nx.bfs_edges(H,source="紫式部",depth_limit = 2,  reverse=True))
bfs_G2 = list(nx.bfs_edges(H,source="紫式部",depth_limit = 2,  reverse=False))
bfs_G = bfs_G1 + bfs_G2
# 多次元配列の要素を平らに
duplication = list(itertools.chain.from_iterable(bfs_G))
# 集合にして要素の重複削除
family_2 = list(set(duplication))
```
こんなリストができます。
```
['藤原雅正', '大弐三位', '高階成章の娘////藤原通宗の妻//', '藤原為信////藤原文範の子//', '藤原定方の娘////藤原雅正正室//', '藤原為時', '藤原為信の娘////藤原為時の妻//', '藤原為信の娘////藤原為時の妻//の母', '源良宗の妻', '高階為家', '紫式部']
```
ちなみに、こういう抽出の仕方をすると、もう少し広めのデータになります。
```python
#  nx.ego_graphを用いて抽出する
# 指定された半径内のノードを中心とした近傍の誘導サブグラフを返す
ego_G = nx.ego_graph(H, '紫式部', radius=6, undirected=True, center=True)
family = [c for c in ego_G.nodes]
```

# mermaid
networkxで抽出したデータから、mermaid記法のデータを作ります。
```python
# in演算子で特定の値を含む行を抽出（isin()と同等）
df_human_3 = df_human_2[df_human_2['Individual_ID'].isin(family_2)]

# dataframe から mermaid用のリストを作る
def person2list(df):
    # 配偶者spouseから、子個人Individual へ
    person_spouse = df[['spouse','Individual_ID']]
    person_spouse = drop_index(person_spouse)
    # 配列にする
    person_spouse_list = person_spouse.values.tolist()

    # 父から配偶者spouse
    paternal_spouse = df[['Paternal_ID','spouse']]
    paternal_spouse = drop_index(paternal_spouse)
    # paternal_spouse
    paternal_spouse_list = paternal_spouse.values.tolist()

    # 母から配偶者
    maternal_spouse = df[['Maternal_ID','spouse']]
    maternal_spouse = drop_index(maternal_spouse)
    # maternal_spouse
    maternal_spouse_list = maternal_spouse.values.tolist()  #.str.spli(',')  #   .to_numpy() #.values
    # print(maternal_spouse_list)

    # print(person_spouse_list + paternal_spouse_list + maternal_spouse_list)
    return person_spouse_list + paternal_spouse_list + maternal_spouse_list

pedigree_list = person2list(df_human_3)

# 配偶者ノードに ( ):::class1　を追加
for i in range(len(pedigree_list)):
    # bool("〇〇")で〇〇が文字列(または数値)の時はtrue、空文字の時はfalseが返る
    if bool(pedigree_list[i][1]):
        #print(pedigree_list[i][1])
        if pedigree_list[i][1].startswith('Q'):
            pedigree_list[i][1] = pedigree_list[i][1] +  '( ):::class1'
            # print(pedigree_list[i][1])

# 2次元配列の重複行を削除
s = list(map(list, set(map(tuple, pedigree_list))))
pedigree_list = list(s)  # その集合からリストを作成

# mermaid用の矢印つきリストを生成する
pedigree_list_arrow = [item[0] + ' --> ' + item[1] for item in pedigree_list]

def generate_report(export_path):
    """
    Markdown のレポートを生成する
    """
    
    # 変更のない固定文を出力する
    text = """\
```mermaid
---
title: familly tree
config:
  theme: base
  themeCSS:
    .node circle {fill: #FAFAFA;}
    .label text {fill: #FAFBF9 !important;} .output {font-size:60px;}
---
graph TD
    classDef class1 fill:#fff,fill-opacity:0
"""

    # ファイルに書き出す
    with open(export_path, 'w', encoding='utf-8') as f:
        f.write(text)
        # 配列を出力
        for d in pedigree_list_arrow:
            f.write("%s\n" % d)

    return text

path = r"test.md"
generate_report(path)
```
できた系図は以下のとおりです。
wikidataにデータがある限りですが、一応、あっているようです。
```mermaid
---
title: familly tree
config:
  theme: base
  themeCSS:
    .node circle {fill: #FAFAFA;}
    .label text {fill: #FAFBF9 !important;} .output {font-size:60px;}
---
graph TD
    classDef class1 fill:#fff,fill-opacity:0
Q759338-Q106410752 --> 紫式部
藤原兼隆 --> Q11623084-Q3012130( ):::class1
紫式部 --> Q11623184-Q81731( ):::class1
藤原雅正 --> Q3090632-Q106255407( ):::class1
藤原為信の娘////藤原為時の妻// --> Q759338-Q106410752( ):::class1
Q1473290-Q3090632_mother --> 藤原雅正
藤原為時 --> Q759338-Q106410752( ):::class1
Q18233501-Q3012130 --> 高階成章の娘////藤原通宗の妻//
藤原定方の娘////藤原雅正正室// --> Q3090632-Q106255407( ):::class1
藤原兼輔 --> Q1473290-Q3090632_mother( ):::class1
Q11623184-Q81731 --> 大弐三位
高階成章 --> Q18233501-Q3012130( ):::class1
Q3090632-Q106255407 --> 藤原為時
Q18233501-Q3012130 --> 高階為家
Q11623084-Q3012130 --> 源良宗の妻
大弐三位 --> Q11623084-Q3012130( ):::class1
大弐三位 --> Q18233501-Q3012130( ):::class1
藤原為信////藤原文範の子// --> Q106410582-Q106410752_mother( ):::class1
藤原宣孝 --> Q11623184-Q81731( ):::class1
Q106410582-Q106410752_mother --> 藤原為信の娘////藤原為時の妻//
藤原為信の娘////藤原為時の妻//の母 --> Q106410582-Q106410752_mother( ):::class1
藤原雅正の母 --> Q1473290-Q3090632_mother( ):::class1
```

:::note info
ちなみに、複雑な方でmermaidを作って、貼ってみたところ、以下のようなエラーになりました。[仕様](https://qiita.com/Qiita/items/c686397e4a0f4f11683d#note---%E8%A3%9C%E8%B6%B3%E8%AA%AC%E6%98%8E)どおりです。
```
ERROR Mermaidをレンダリングできませんでした
文字数は2000文字以内にしてください
```
nx.ego_graphを使ったバージョンは[ここ](https://github.com/wonox/pedigree/blob/main/README.md)に貼ってあるので、ご興味のある方は見てみてください。
:::

# おまけ
せっかくnetworkxに入れたので、平重盛（清盛の子）と紫式部のパスをみてみました。
```python
print(nx.shortest_path(H, source='平重盛', target='紫式部'))
['平重盛', '高階基章の女////平清盛正室//', '高階基章', '高階為家の娘////源家実の妻//', '高階為家', '大弐三位', '紫式部']
```
意外と、近いですね。

# おわりに
最初は、バイオインフォマティクス分野で使うpedファイルを作って、そこからgraphvizで系図が書けるかと試行錯誤しましたが、なかなかむずかしかったので、mermaidにしてしまいました。
ちなみに、こんなdotファイルを作ると、系図が書けます。
```dot
graph G {
        edge [dir=none];
        graph [splines=ortho concentrate=true];
        "紫式部" [shape=box, regular=0, color="black", style="filled" fillcolor="white"];
        "大弐三位" [shape=box, regular=0, color="black", style="filled" fillcolor="white"];
        "高階成章" [shape=box, regular=0, color="black", style="filled" fillcolor="white"];
        {spouse_紫式部_藤原宣孝 [shape=point width=0 style=invis]};
        spouse_紫式部_藤原宣孝 -- 大弐三位
         {rank=same; 紫式部 -- spouse_紫式部_藤原宣孝 -- 藤原宣孝};
        {spouse_大弐三位_高階成章 [shape=point width=0 style=invis]};
        {rank=same; 大弐三位 -- spouse_大弐三位_高階成章 -- 高階成章};
        {children_大弐三位_高階成章 [shape=point width=0 style=invis]};
        spouse_大弐三位_高階成章 -- children_大弐三位_高階成章
        "高階為家" [shape=box, regular=0, color="black", style="filled" fillcolor="white"];
        children_大弐三位_高階成章 -- 高階為家
        children_大弐三位_高階成章 -- 高階成章の娘////藤原通宗の妻//
        "源良宗の妻" [shape=oval, regular=0, color="black", style="filled" fillcolor="white"];
        children_大弐三位_高階成章 -- 源良宗の妻
        {spouse_藤原兼隆_大弐三位 [shape=point width=0 style=invis]};
        {rank=same; 藤原兼隆 -- spouse_藤原兼隆_大弐三位 -- 大弐三位};
        {children_藤原兼隆_大弐三位 [shape=point width=0 style=invis]};
        spouse_藤原兼隆_大弐三位 -- children_藤原兼隆_大弐三位
}
```