Pymetamapで英語電子カルテからUMLS conceptを抽出する

はじめに

NIHが提供しているMetamapというツールを使うと英語テキストから病名,薬剤名,解剖学的部位などを抽出することができます.

metamap.nlm.nih.gov

Metamapは本来Javaで書かれており,Pythonでデータ分析をする際に扱いづらかったのですが,
世の中には親切な方がいるもので,PymetamapというPythonラッパーが作られていました.
そこで今回はPymetamapを使って英語電子カルテから病名抽出を行いたいと思います.

動作環境

準備

Metamapはすでにインストールされているものとします.詳しくはこちらを参照してください.

radiology-nlp.hatenablog.com

PymetamapはこちらのGitHubリポジトリをCloneして入手します.

github.com

cd ~
git clone git@github.com:AnthonyMRios/pymetamap.git

注: pip install pymetamap でインストールしないでくださいPyPIで提供されているものはバージョンが古く,Metamapの一部のオプションに対応していません.
それでも普通に使うぶんには問題ないのですが,高度な使い方をしようとするとエラーが生じる場合があります.

本題

まずはMetamapを動かすための仮想サーバーを起動しておきます.

cd /dir/to/metamap
./bin/skrmedpostctl start
>>> Starting skrmedpostctl:
>>> started.

次に対象とする英語電子カルテを用意し,文単位に区切っておきます.

import nltk
import re
import pymetamap
from typing import Dict, Tuple, List
from pymetamap import MetaMap

text = 'This is a 62 year-old woman with a history of with significant past medical history of diabtes mellitus type 2, hypertension, hyperlipidemia, CAD s/p CABG who comes with three weeks of shortness of breath and dyspnea on exertion.'
tokenizer = nltk.tokenize.punkt.PunktSentenceTokenizer()
sentences = tokenizer.sentences_from_text(text)

Metamapのインスタンスを読み込みます.

# XX にはMetamapのバージョン番号を入れる.2020年版であれば path = '(中略)/bin/metamap20'
path = '/path/to/metamap/bin/metamapXX'
mm = MetaMap.get_instance(path)

すると簡単にUMLS conceptが抽出できます.

concepts, error = mm.extract_concepts(sentences, range(len(sentences)))

print(concepts)
>>> [ConceptMMI(index='0', mm='MMI', score='7.02', preferred_name='Medical History', cui='C0262926', semtypes='[fndg]', trigger='["History"-tx-1-"history"-noun-0,"History of MEDICAL"-tx-1-"medical history of"-noun-0]', location='TX', pos_info='36/7;69/18', tree_codes=''),
 ConceptMMI(index='0', mm='MMI', score='5.18', preferred_name='Dyspnea on exertion', cui='C0231807', semtypes='[sosy]', trigger='["Dyspnoea on exertion"-tx-1-"dyspnea on exertion"-noun-0]', location='TX', pos_info='210/19', tree_codes=''),
 ConceptMMI(index='0', mm='MMI', score='5.18', preferred_name='Hyperlipidemia', cui='C0020473', semtypes='[dsyn]', trigger='["Hyperlipidaemia, NOS"-tx-1-"hyperlipidemia"-noun-0]', location='TX', pos_info='127/14', tree_codes=''),
 ConceptMMI(index='0', mm='MMI', score='5.18', preferred_name='Hyperlipidemia, CTCAE', cui='C4555212', semtypes='[fndg]', trigger='["Hyperlipidemia"-tx-1-"hyperlipidemia"-noun-0]', location='TX', pos_info='127/14', tree_codes=''),
...

この出力結果からでは,テキスト中のどの語がUMLS conceptとして認識されたのか直接はわかりません. そこでもう少し分かりやすく見ていきましょう.

def convert_concepts_to_superficial_concept_pair(
    concepts: List[pymetamap.Concept.ConceptMMI],
    sentences: List[str]
) -> List[Dict]:
    entities = []
    for concept in concepts:
        sentence_id = int(concept.index)
        # テキスト中の位置が文字数ベースで示されている (1-indexedであることに注意)
        raw_pos_info: List[Tuple[str, str]] = re.findall(r'(\d+)/(\d+)', concept.pos_info)
        positions = [{'start' : int(pos_info[0]) - 1, 'end' : int(pos_info[0]) + int(pos_info[1]) - 1} for pos_info in raw_pos_info]
        superficials = [sentences[sentence_id][pos['start']:pos['end']] for pos in positions]
        # それぞれの UMLS concept が参照している表出形を "superficials" キーに入れる
        entities.append({'superficials':superficials, 'concept':concept})
    return entities

entities = convert_concepts_to_superficial_concept_pair(
    concepts, sentences
)

それぞれの UMLS concept が文中のどの単語に該当しているかを辞書のリストとして格納しました.

print(entities[0])
>>> {'superficials': ['history', 'medical history of'],
 'concept': ConceptMMI(index='0', mm='MMI', score='7.02', preferred_name='Medical History', cui='C0262926', semtypes='[fndg]', trigger='["History"-tx-1-"history"-noun-0,"History of MEDICAL"-tx-1-"medical history of"-noun-0]', location='TX', pos_info='36/7;69/18', tree_codes='')}

ちょっと情報量が多いので,表出形,semantic type,標準形,CUI (Concept Unique Identifier)だけを抜き出して表示してみましょう.
それぞれの UMLS concept は pymetamap.Concept.ConceptMMI というオブジェクトとして与えられるため,各属性にも簡単にアクセスすることができます.

for entity in entities:
    superficials = entity['superficials']
    concept = entity['concept']
    print(f'{superficials}\t{concept.semtypes}\t{concept.preferred_name}\t{concept.cui}')

>>> ['history', 'medical history of']    [fndg]  Medical History C0262926
['dyspnea on exertion']    [sosy]  Dyspnea on exertion C0231807
['hyperlipidemia'] [dsyn]  Hyperlipidemia  C0020473
['hyperlipidemia'] [fndg]  Hyperlipidemia, CTCAE   C4555212
['hypertension']   [fndg]  Hypertension, CTCAE C1963138
['hypertension']   [dsyn]  Hypertensive disease    C0020538
['hyperlipidemia'] [fndg]  Serum lipids high (finding) C0428465
['CABG']   [topp]  Coronary Artery Bypass Surgery  C0010055
['of shortness', 'breath']    [sosy]  Dyspnea C0013404
...