STAGの備忘録

みんなブログを書いている、書いていないのは俺だけ

プール方式による効率的PCR検査

はじめに

この記事はとある朝日新聞デジタルの記事"多くの検体「まとめて」PCR検査で、件数を増やせる?"に書かれていた、PCR検査の手法が面白いなと思い、自分なりに色々実験した結果と考察を載せたものです。なお私はPCR検査自体についての専門的知識は全く持っていません。この記事はあくまで極端に理想化された状況での思考実験だと思ってください。

PCR検査とは

WikipediaによるとPCR検査とは

DNAサンプルから特定領域を数百万〜数十億倍に増幅する一連の反応またはその技術である

DNAポリメラーゼと呼ばれる酵素の働きを利用して、一連の温度変化のサイクルを経て任意の遺伝子領域やゲノム領域のコピーを指数関数的(ねずみ算的、連鎖的)に増幅することで、少量のDNAサンプルからその詳細を研究するに十分な量にまで増幅することが目的である

のような手法です。専門的な内容については門外漢なのでなんとも言えませんが、一回の検査にとても時間がかかる検査方法のようで、これを患者一人一人に行っているので多大なコストがかかっているようです。

プール方式PCR検査

前述の朝日新聞デジタルの記事によると

理論的には500検体のうち1検体にでもウイルスが含まれていれば、ウイルスの核酸は増幅され陽性に出るはずです。

とあるようにPCR検査を一人一人に行うのではなく検体をいくつかにまとめて検査することで初めに陽性の検体の当たりをつけて、効率化を図っているようです。なんとも豪快で本当にそれでいいのか?という感じですが、この記事の記述から以下のことが推測されます。 - 検体を混ぜることはPCR検査を行うことと比べて簡単に行うできる - 検体に1つでもウイルスが含まれていれば、他の検体と混ぜた後PCR検査をしても検出できる - 検体を混ぜてPCR検査しても一回の検査にかかる時間はあまり変わらない

もし陽性率が十分低いならば、確かに一つずつ検査するよりも、一度混ぜたものを一度検査し、それが陽性ならばもう一度一つずつ再検査するという手法でPCR検査を効率化できるように思います。このような手法はプール方式などと呼ばれているようです。

問題定義

では実際どれくらい効率化できるのかシミュレーションしてみます。現実的には様々な問題があると思いますが、以下では一旦忘れて、極端に理想化した状況で問題を定義します。 N個の検体が得られたとします。以下の操作を行い全ての検体について陽性かどうかをできるだけ少ない検査回数で判断したい、とします。

  • N個の検体をM個ずつに分けてそれぞれを混ぜる(プーリング)、これはPCR検査と比べて短時間で行うことができるとする
  • プーリングした検体をPCR検査する、ただし検体をどれだけプーリングしても検査1回の所要時間は一定であるとする
    • プーリングした検体の検査結果が陰性なら検査終了
    • 陽性ならばプーリングに使用したM個の検体全てを一つずつ再検査する

明らかに、全ての検体について一つずつPCR検査すればN回の検査で終了することがわかります。以下ではプーリングによってどれだけ効率化できるのかシミュレーションします。

シミュレーション

サンプルサイズN=100,000、サンプル数1000とし、色々なプーリングサイズMと各検体の陽性率を設定してシミュレーションしました。用いたPythonコードを示しながら結果をみます。 必要なライブラリをimportします

import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
from numpy.random import rand

各種パラメータを設定します。(ここでは陽性率0.01の場合を示しています)

sample_number = 10**5 #サンプルサイズ
positive_rate = 0.01 #陽性率
sample_size = 1000 #サンプル数
#プーリングサイズの候補
pool_sizes = [2,3,4,5,8,10,15,20,25,50,75,100,200,500,1000,5000]

サンプルとプーリングサイズに対して合計検査回数を返す関数をここで定義しておきます。

#合計検査数を返す関数
def pcr(pool_size,sample):
#pool_size:一度の検査でプールするサンプルの数
#sample:サンプル
    count=0
    count += (len(sample)+pool_size-1)//pool_size#プーリングの検査回数
    count += len(np.unique(sample[0:-1:pool_size]))*pool_size#陽性だったプーリングの再検査
    return count

これで準備ができたので実際に乱数でデータを生成し、実験します。

#データ生成
samples = [np.insert((rand(sample_number)<positive_rate).cumsum(),0,0) for _ in range(sample_size)] 
#この後の処理の効率化のためcumsum(累積和)を取っています
#シミュレーションを行い結果を保存
rslt = dict()
for pool_size in pool_sizes:
    rslt[pool_size] = [pcr(pool_size,sample) for sample in samples]
rslt_binary[positive_rate] = [binary_pcr(sample) for sample in samples] 

最後に箱ひげ図で各プーリングサイズでの結果を可視化します。

#箱ひげ図をプロット
plt.rcParams["font.size"] = 18
points = tuple(rslt.values())
fig, ax = plt.subplots()
bp = ax.boxplot(points)
ax.set_xticklabels(tuple(rslt.keys()))
plt.title("陽性率"+str(int(100*positive_rate))+"%の場合")
plt.xlabel("プーリングサイズ")
plt.xticks(rotation=45)
plt.ylabel("合計検査回数")
plt.axhline(sample_number,color='black',ls='--')

シミュレーション結果

陽性率が1%,5%,10%,30%の場合の結果は以下のようになりました。 f:id:QDSN:20200831224457p:plain どのグラフも初めプーリングサイズをあげると効率が上がりますが、上げすぎると今度は逆に効率が悪くなっていることがわかります。このようにプーリングサイズは大きすぎても小さすぎてもだめで、陽性率に応じて最適な値が存在しそうです。また各検体の陽性率が小さいほど効率化が期待でき、陽性率が1%の場合100,000件の検体に対して最大で約20000回ほどの検査で十分であることがわかります(約5倍の効率化)。反対に各検体の陽性率が大きいときは、プーリングしてもあまり効率化はできないどころか、一つずつ検査するよりも効率が悪くなることも確かめられました。

更に効率的なプーリングサイズ決定アルゴリズム

更に効率よく検査を行う方法はないでしょうか?先程の方法では途中の検査の結果によらずプーリングサイズを固定していましたが、プーリングサイズを検査結果に応じて動的に変化させることでより効率よく検査することが可能そうです(例えば最初は大きなプーリングサイズをとり、その後プーリングサイズを小さくしていく)。ここではプーリングサイズを二分探索的に変化させることで更に効率化を図ります。 具体的なアルゴリズムを述べます。 1.サンプルを前半部分と後半部分のちょうど半分に分けます(奇数個あるときは端数を後半に入れる) 2. 前半部分と後半部分をそれぞれプーリングして検査します 3. 前半、後半それぞれについて   4.陰性ならば検査終了   5.陽性ならば再び前半、後半に分けて2から繰り返す

このアルゴリズム二分プーリング検査と呼んでおきます。二分プーリングPCR検査では最初にプーリングサイズを固定する必要がなく、その都度の検査結果に応じて、サンプルを半分ずつプーリングして検査することによって動的にプーリング数を変化させながら検査を進めることができます。直感的にこちらの方がプーリングサイズを固定するより効率が良さそうですね。 では実装します。二分プーリング検査は再帰関数を使うと比較的きれいに書けます。

def binary_pcr(sample):
   N = len(sample)
   if(N<=1): #サンプルが1個以下ならちゃんと検査する
       return N
   if(sample[0] == sample[-1]): #プールしたものが陰性ならそれ以降検査しない
       return 1
   #それ以外の場合左半分と右半分で再び混ぜて検査する
   return binary_pcr(spcm[:N//2]) + binary_pcr(spcm[N//2:])

二分プーリング検査の結果

二分プーリング検査のシミュレーション結果は以下のようになりました。 f:id:QDSN:20200831224414p:plain

プーリングサイズを固定していた先程の方法に比べて、更に良い効率で検査ができていることがわかります。(特に陽性率1%の場合で検体数100,000件に対して合計検査回数が6000回弱ですので16倍ほどの効率アップが見込めます)

検査回数の期待値

検査回数の期待値を出してみようと思います。ここでは簡単のため検体の数は2の冪乗個ある場合を考えます。今検体の数が8個の場合で、プーリングの手順を少し可視化すると次の図のようになることがわかります。 f:id:QDSN:20200831225447p:plain 各水色のブロックがプーリングした検体とみなすことができます。大きいブロックで陽性が出るたびに小さいブロックに分割されてもう一度プーリングされ検査されるイメージです。このブロック単位で何回検査が行われるかの期待値を求めてみましょう。今検体の数を 2^{n} 、陽性率を p とします。検体数が 2^{k} のブロックに注目するとこのブロックが検査に回されるためには、そのひとつ上の 2^{k+1} のブロックが陽性である必要があります。よってそのブロックが陽性になる確率を求めれば良いです。注意としてひとつ上のブロックが陽性になるときそのさらに上のブロックも必ず陽性になるのでそれらを考慮する必要はありません。さて 2^{k+1} のブロックが陽性になる確率はプールされた検体のうち少なくとも陽性になる確率なので$(1-(1-p)^{2^{k+1}}$ です。 $2^{k}$ のブロックは合計 $2^{n-k}$ 個あります。よって検査回数の期待値 $EX$ は全ての $k$ についてこれらを足し合わせて

$$ EX=\sum_{k=0}^{n-1}(1-(1-p)^{2^{k+1}})\cdot2^{n-k} $$

となります。また最初に必ず1回を検査するのでこれに$1$を加えた値が全体に期待値となります。 この検査回数の期待値と検体数の比を $n,p$ を色々変えた場合でみてみます。 f:id:QDSN:20200831232809p:plain 陽性率が20%を超えると比が1を超え、プール方式の検査が非効率であることがわかります。次に陽性率が1~10%の場合をみてみると f:id:QDSN:20200831233225p:plain となり陽性率が2%を下回ると8割以上検査回数が削減されることがわかります。

まとめ

理想的な問題設定のなか、検体をまとめてPCR検査を行うことでコストのかかるPCR検査の回数を減らせることがシミュレーションによって確認できました。ただしプーリングサイズを初めに固定する方法では陽性率によって適切な値を選ぶ必要があります。一方でプーリングした検査の結果に応じて、動的に次の検査のプーリング数を決める二分プール検査ではそのようなことを考える必要がない上、高性能でした。またどちらの手法に関しても陽性率が低いほど効率化の度合いが大きいように見えました。

注意事項

  1. まず大前提として日本ではプール方式でのPCR検査は行われていないようです
  2. そもそもPCR検査には偽陽性偽陰性が一定の確率で出てくるため、プール方式を用いても完璧な検査は行えません
  3. 現実ではプールした検体が検査をすり抜ける可能性もあるようです
  4. 際限なく検体をプール出来るという仮定も現実的にはかなり怪しいです

現実問題、プール方式のPCR検査を実施していないのには、専門家や現場の方々にしか分からない理由があるのでしょう。