Prontoの使い方

概要

今回扱うのはオントロジーデータをPythonで処理するためのパッケージである'Pronto'です。 英語であっても使用例があまりなく、日本語ではほとんど紹介する記事がありませんでしたので、かんたんな使ってみた的な記事を書いてみようかと思います。 ある単語を入力すると、その単語の属するカテゴリーを表示できる関数をつくるまでを書いていこうかと思います!

オントロジーデータとは

詳しい説明はWikipediaやらなんやらをご参照いただくとして、今回扱う範囲での説明を少しだけ。 ライフサイエンス分野でもドメイン特有の単語や遺伝子・タンパク質のような情報に概念的なタグ付けをして、コンピューターによって扱いやすい形にして検索や分類、文脈の認識などに役立てようという動きが長らく行われているようです。...大変説明が難しく自分の日本語力じゃうまく伝えられる気がしませんが、なんとなく触れてみるとわかる、そういった類のものなんだと思います(説明放棄)。

有名所としてはGene Ontology(GO)PRoteinOntologyなど、一度は研究で扱ったり見たことがあったりするのではないでしょうか。他にもontobeeBioPortalといったウェブサイトにて、様々なライフサイエンス関連のオントロジーの存在を確認することができます。 これらは遺伝子名やタンパク質名などに対し、どういう親概念を持っているとか、生化学パスウェイの何に属しているのかみたいな情報が付与されており、マイクロアレイやRNA-seqの結果の分類とかどんなパスウェイ関連の遺伝子が動いてるかみたいな解析のとき使われている気がします。こういうのなんていうんでしたっけ?なんかカッコいい言い方があったはずですがど忘れしました。

…まあこんなニッチな記事はProntoというキーワードやPythonオントロジーデータ処理みたいなキーワードで検索してくる人くらいしかいないでしょうから、こんな話はそんなにする必要がないものと思いますので次に進みます。

オントロジーデータのファイル形式について

オントロジーデータは一般にjson形式やowl(Web Ontology Language)形式、またライフサイエンス分野ではobo(Open Biomedical Ontology)形式といった形態で配布されていることが多いです。 オントロジーデータの1つのtermを構成する要素は、固有のID、名前、定義、上位概念などです。これを表現する形式として上記の3種のフォーマットが使われています。

今回例として扱うのは、Disease Ontologyです。こちらのオントロジーは人の病気を病因によって階層化、分類しているデータになります。 頻繁に更新されており、またライセンスがCC0でGithubにデータが公開されているので、大変扱いやすいです。

このようなオントロジーデータをプログラミングで扱う方法としては、 - 提供されているAPIを使う - データをダウンロードしてきて頑張ってパースする といったことが考えられます。

少量の利用であればまずは提供されているAPIがないかどうか探してみましょう。特に大きなデータベースとなるとダウンロードしたり更新したりするのも一苦労なので、APIで叩ければこの辺の悩みを気にせず済みます。 しかし、こういったオントロジーデータの提供をしてくださっているのはアカデミアなどの非営利機関が多いかと思いますので、あまり大量のトラフィックを流すのは礼儀としてどうなのかということもありますし、速度的な要件で自前で準備をするべき局面もあるかと思います。

フォーマットもある程度決まっていますので、自分でパースするプログラムを書く事もできるかもしれませんが、PythonではPronto というパッケージがobo, owl形式のオントロジーデータを扱うことができるので、今回はこれを使っていきたいと思います。

prontoのインストール

pipでもcondaでも提供があります。

$ pip install --user pronto
$ conda install -c bioconda pronto

オントロジーから単語を検索する関数を作ってみる

    import pronto
    import re
    import pandas as pd
    from multiprocessing import Pool
    import uuid

    ont = pronto.Ontology("PATH TO DOID.OBO EXIST/doid.obo")

    def search_disease_category(search_word):
        idIsExist = False
        #検索する深さ   
        layer = 2
        
        print('searching:', search_word)

        for term in ont.terms():
            #もっと厳密に検索をかけるならUnicode正規化するのが良いかもしれないが、とりあえず辞書と検索ワードどちらも小文字化して比較している
            if str(search_word).lower() == term.name.lower():
                #print(term.name)
                id = term.id
                idIsExist = True
            else:
                synonyms = term.synonyms
                for w in synonyms:
                    try:
                        #ここの正規表現はDOIDでシノニムがクオーテーションに囲まれているのでそれを外すために使用
                        #ex) Synonym('metabolic disease', scope='EXACT') 
                        if str(search_word).lower() == re.search(r'((?<=\').*(?=\'\,))', str(w)).group().lower():
                            id = term.id
                            idIsExist = True
                    
                    except:
                        TypeError

        if idIsExist:
            #print(id)
            print(search_word, "'s category found!")
   
   #ProntoのTermはイテレータオブジェクト
           #あまりPythonのイテレータの扱い方に詳しくないので下記は強引な解決策。動いているからヨシ!(いつか気が向いたら直すかも)
            sup = ont[id].superclasses()
            sup2 = ont[id].superclasses()

            i=0
            while True:
                try:
                    next(sup)
                    i+=1

                except StopIteration:
                    break

            while layer < i:
                next(sup2)
                layer+=1

            return [search_word, next(sup2).name]
        
        else:
            print(search_word, "'s category does not found")
            return [search_word, None]

    #print(search_disease_category('type 2 diabetes'))
    #print(search_disease_category('type 2 diabetes'))
    #print(search_disease_category('hoge'))
    #print(search_disease_category('Ocular hypertension'))
    #print(search_disease_category('Myelodysplastic syndrome'))

    if __name__ == '__main__':
        search_words = list(['type 2 diabetes','Ocular hypertension','Myelodysplastic syndrome','hoge','metabolic disease']) 

        #コア数に応じてPoolの中身は変える
        P = Pool(4)
        category = P.map(search_disease_category, search_words)

        filename = str(uuid.uuid4()) + '.csv' 
        pd.DataFrame(category).to_csv(filename)

結果

こんな感じのcsvが出ると思います。

,0,1
0,type 2 diabetes,disease of metabolism
1,Ocular hypertension,disease of anatomical entity
2,Myelodysplastic syndrome,disease of cellular proliferation
3,hoge,
4,metabolic disease,disease of metabolism

また、上記Pythonスクリプトのlayerの値を変えれば出力されるカテゴリーの粒度が変えられますので、使用用途に応じて変更してみると良いかもしれません。