PubMed API × ChatGPTで大量の論文データからキーワードを抜き出す（つもりだった）

## PubMed APIで論文情報を収集する

医学文献データベースのPubMedには、APIが準備されており、
+ 情報を取得したいPMID（PubMedID:論文ID）　
+ 論文検索キーワード

のどちらかで論文をフィルタリングして比較的簡単に、アブストなどを一括で取得することができます。

PubMed APIの使い方はこちらのサイトがわかりやすかったので参考にしてください。
[Pythonで論文情報をまとめてゲットする](
https://lifesciencehack-ai.hatenablog.com/entry/Python%E3%81%A7%E8%AB%96%E6%96%87%E6%83%85%E5%A0%B1%E3%82%92%E3%81%BE%E3%81%A8%E3%82%81%E3%81%A6%E3%82%B2%E3%83%83%E3%83%88%E3%81%99%E3%82%8B%E2%91%A0PubMedAPI%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)

上記サイトをベースに作成したコードを以下にまとめていきます。


```モジュールのインポート.py
!pip install openai
!pip install tiktoken
!pip install requests
!pip install openpyxl

import pandas as pd
import openai
import requests
import json
import openpyxl
import numpy as np
import math
from lxml import etree
import time
import tiktoken
from tiktoken.core import Encoding
```

以下、PubMedから論文情報を取得する３つの関数です。


```.py
# 検索ワード（term）からPMIDをrestart~retmax分まで取得する関数
def eSearch(term, retstart, retmax):
    URL = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&retmode=json'
    option = '&retstart='+str(retstart)+'&retmax='+str(retmax)+'&term='+term
    query = URL + option
    response = requests.get(query)
    response_json = response.json()
    pmids = response_json['esearchresult']['idlist']
    return pmids

# PMIDから論文情報を取得する関数
def eSummary(pmids):
    URL = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&retmode=json&id='
    queries = [URL + pmid for pmid in pmids]
    responses = {}
    for query in queries:
        response = requests.get(query)
        res_json = response.json()['result']
        responses.update(res_json)
        time.sleep(0.5)

    Summaries = [{'pmid':pmid,
                  'Title':responses[pmid]['title'],
                  'Author':responses[pmid]['sortfirstauthor'],
                  'Journal' : responses[pmid]['source'],
                  'Pubdate':responses[pmid]['epubdate']} for pmid in pmids]
    summary_df = pd.DataFrame(Summaries)
    return summary_df

# PMIDから論文のアブストを取得する関数
def eFetch(pmids):
    URL = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&retmode=xml&id='

    queries = [URL + pmid for pmid in pmids]

    responses_abst = {}

    for query in queries:
        response = requests.get(query)
        root = etree.fromstring(response.content)
        pmid = root.find('.//PMID').text#pmidを抽出
        abst = root.findall('.//AbstractText')
        if abst is None:
            abst_text = ''
        else:
            abst_text = ''.join(root.xpath('//Abstract//*/text()'))
        responses_abst[pmid]=abst_text
        time.sleep(0.5)
        abst_df = pd.DataFrame.from_dict(responses_abst, orient='index')
        abst_df.index.name = 'pmid'
        abst_df.columns = ['Abstract']

    return abst_df
```
ここから場合分けです。
### 検索ワードから論文情報を収集する場合
```.py
term = 'Mthfr+polymorphism+Cancer'  # 検索ワード
retmax = 1800 #検索上限数
#まずはeSearchでpmidをすべて取得する。 約1600件近くある
pmids = eSearch(term=term,retstart=0, retmax=retmax)

total_cases = len(pmids)
#件数が多いので、100分割でデータ収集
retmax_split = math.ceil(total_cases / 100)

df= pd.DataFrame()
for i in range(0, total_cases, retmax_split):
    print(i)
    pmids_tmp = eSearch(term=term,retstart = i, retmax=retmax_split)
    summary_df = eSummary(pmids_tmp)
    abst_df = eFetch(pmids_tmp)
    df_tmp = pd.merge(summary_df, abst_df, on='pmid')
    df = pd.concat([df, df_tmp])
```
以上で検索キーワードの論文をdfに格納することができました。

### 事前に用意したPMIDから論文情報を取得する場合
```.py
with open("file_path", "r") as f: # file_pathには用意したファイルのパスを入れてください
    pmids = f.read().splitlines()

total_cases = len(pmids)
#件数が多いので、n分割でデータ収集
n_split = 10
retmax_split = math.ceil(total_cases / n_split)

df= pd.DataFrame()

# n分割してそれぞれの論文データを取得していきます
for i in range(0, total_cases, retmax_split):
    print(i,pmids[i:i+retmax_split])
    pmids_tmp = pmids[i:i+retmax_split]
    summary_df = eSummary(pmids_tmp)
    abst_df = eFetch(pmids_tmp)
    df_tmp = pd.merge(summary_df, abst_df, on='pmid')
    df = pd.concat([df, df_tmp])
```
こちらも以上でdfに論文データを格納できました。

## TitleとAbstractをChatGPTに処理してもらいたかった
以下一応書きましたが、私がやりたいことはgpt-4.0でないと難しそうです。
（3.5-turboだと返答がフォーマット通りに帰ってこないためそのあとのデリミタで分割する処理ができませんでした）
gpt-4.0のAPIはウェイティングリストに並んで順番呼ばれなければいけないのでまだ試せていません。

それでもどのようなコーディングをしているかは共有します。

```.py
# TitleとAbstractを処理したいので、それらが無い列は削除
df = df.dropna(subset=['Title'])
df = df.dropna(subset=['Abstract'])

# OpenAI APIキーを設定
openai.api_key = '自分のAPIキー'

# 膨大なお金がかかると困るので、事前にAPIに投げるトークン数を計算しておきます。
encoding: Encoding = tiktoken.encoding_for_model("gpt-3.5-turbo")
total_tokens = 0  # 総トークン数を初期化

# 以下のuser_messageの部分に自分の所望の処理を入れてください。
for i, row in df.iterrows():
    title = row['Title']
    abstract = row['Abstract']
    user_message = f"次に示す論文のアブストを元に、「100字要約」「行った内容」「結果」を抜き出し、必ずデリミタ'###'で区切る以下のフォーマットで返してください。 100字要約：～～～～###行った内容：～～～～###結果：～～～～\n論文タイトル：{title}\n論文アブスト：{abstract}"
    system_message = "You are a helpful assistant."

    # 各メッセージのトークン数を計算し、総トークン数に加えます。
    total_tokens = total_tokens + len(encoding.encode(user_message))
    total_tokens = total_tokens + len(encoding.encode(system_message))

print(f"The total number of tokens for the API input is {total_tokens}.")
```

今回はuser_message部に __必ずデリミタ'###'で区切る以下のフォーマットで__ と書いてあるにも関わらず
gpt-3.5 turboではデリミタを入れてくれませんでした
```.py
summary_results = []
content_results = []
result_results = []

for i, row in df.iterrows():
    title = row['Title']
    abstract = row['Abstract']
    messages=[
        {"role": "system","content": "You are a helpful assistant."},
        {"role": "user","content": f"次に示す論文のアブストを元に、「100字要約」「行った内容」「結果」を抜き出し、以下のフォーマットで返してください。 100字要約：～～～～###行った内容：～～～～###結果：～～～～\n論文タイトル：{title}\n論文アブスト：{abstract}"},
    ]

    # APIに問い合わせを実施
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages = messages
    )

    # APIからの応答を解析 デリミタ###で分割し、各結果を格納
    message = response['choices'][0]['message']['content']
    summary, content, result = message.split('###')
    summary = summary.replace("100字要約：", "").strip()
    content = content.replace("行った内容：", "").strip()
    result = result.replace("結果：", "").strip()

    # 各要素を対応するリストに格納
    summary_results.append(summary)
    content_results.append(content)
    result_results.append(result)

# 結果を新しいデータフレームに格納
df['100字要約'] = summary_results
df['行った内容'] = content_results
df['結果'] = result_results
```

GPT3.5さんの回答　

>100字要約：ヨーロッパ人の場合、喫煙は前立腺がんのリスクを増加させ、東アジア人の場合、アルコール摂取と性的遅れはリスク要因であり、調理済み野菜の摂取は防護要因であることが明らかになった。\n行った内容：ヨーロッパと東アジアのゲノムワイド関連研究を用いて、ヨーロッパ人および東アジア人におけるモディファイアブル・ビヘイビアが前立腺がんに与える因果効果を評価した。\n結果：ヨーロッパでは喫煙が前立腺がんリスクを増加させる一方、東アジアではアルコール摂取と性的遅れがリスク要因であり、調理済み野菜の摂取は防護要因であることが明らかになった。これらの結果は、前立腺がんのリスク要因を異なる民族間で理解し、行動介入の視点を提供するものである。

\nで結合されたり、###で結合されたり、うまく返してくれないため、
 summary, content, result = message.split('###')　　の部分でエラーが出ます。

GPT 4.0をウェブ上で利用すると、このプロンプトでちゃんと###デリミタで区切って出力してくれます。
GPT4.0のAPIが使えるようになったら、最後まで回せるようにしようと思います。

以上、初PubMed APIとOpenAI APIでしたのでまとめておきました。
未来の自分の参考になれば。