点滴静注したバンコマイシン*1の血中濃度データが得られたとして、これを2-コンパートメントモデルに当てはめ、薬物動態パラメータを求めてみます。
2-コンパートメント点滴静注モデル
0次速度の吸収過程を持つ2-コンパートメントモデルの薬物量の変化は次のような2つの微分方程式で記述できます。
この連立微分方程式を解くと、以下の血中濃度推移を求める式が得られます。
ここで、A, B, α, βはそれぞれ
です。また、これらの関係からを求めることもできます。
ここで、点滴終了時をとすると(1)の式はでした。
では次に、点滴終了後()の血中濃度推移を求める式ですが、次のように表すことができます。
式を整理すると、
となります。
これらの式を使って薬物動態パラメータを求めていきます。
残差法で回帰直線のパラメータを求める
それでは、血中濃度のデータをこのモデルに当てはめ、A, B, α, βを求めていきたいと思います。その方法については、こちらのページを参考に考えてみました。
「血中濃度データに1-コンパートメント急速静注モデルを当てはめる」でやったように、血中濃度を対数変換し、片対数プロットして線形回帰しますが、2-コンパートメントモデルの場合は下図のように1つの直線ではなく、2つの直線が組み合わさっているような推移を示します。
# VCM 1.0gを1時間で点滴静注したときの8点の血中濃度データ t = np.array([1.0, 1.5, 2.0, 3.0, 5.0, 7.0, 12.0, 24.0]) # 時間 c = np.array([49.5, 31.6, 23.3, 13.8, 8.9, 6.4, 3.2, 1.2]) # 血中濃度
まず、上の(2)式を次のように書き直します。
このとき、tが大きい部分ではα>βであるため前のPの項は無視して、次のような近似式で表すことができます。
この状態では組織への分布が完了し、コンパートメント間で平衡状態となっており、これをβ相(消失相)と言います。その前の、薬が中心コンパートメントから各組織への分布が完了するまでがα相(分布相)です。
次に、この式を常用対数表示にすると、
となります。あとは、測定データにこの回帰直線を当てはめ傾きβと切片Qを求めます。
次はα相です。上の(4)式を変形して
となります。この左辺の真数部分は『測定した血中濃度の値 -各時間におけるβ相の直線上の値』を意味します。
これでα相とβ相の2つの直線の式が得られました。
では実際にコードを書いていきます。
import numpy as np import matplotlib.pyplot as plt x0 = 1000 # 投与量(mg) T = 1.0 # 点滴時間(hr) # VCM 1.0gを1時間で点滴静注したときの8点の血中濃度データ t = np.array([1.0, 1.5, 2.0, 3.0, 5.0, 7.0, 12.0, 24.0]) c = np.array([49.5, 31.6, 23.3, 13.8, 8.9, 6.4, 3.2, 1.2]) # 最小二乗法により回帰直線のintercept, slopeを返す def least_squares_method(xdata, ydata): X = np.zeros((len(xdata), 2), float) # データ数×2列の行列を作る X[:, 0] = 1 X[:, 1] = xdata logy = np.log10(ydata) # 血中濃度の常用対数をとる (theta, residuals, rank, s) = np.linalg.lstsq(X, logy, rcond=None) return theta[0], theta[1] # α・β相の傾きと切片を求める # 後半の4点をβ相とする intercept_q, slope_b = least_squares_method(t[4:8] - T, c[4:8]) # 最初の4点の実測値 - 消失相(β相)の外挿した直線上の値との差を求める. c_diff = [c_data - pow(10, intercept_q + slope_b * (x - T)) for (x, c_data) in zip(t[:4], c[:4])] # 前半の4点をα相とする intercept_p, slope_a = least_squares_method(t[:4] - T, c_diff) # 傾きからalpha, betaを求める alpha = slope_a * -2.303 beta = slope_b * -2.303 # 切片からA, Bを求める A = 10 ** intercept_p / (1 - np.exp(-alpha * T)) B = 10 ** intercept_q / (1 - np.exp(-beta * T)) print('A: {}, alpha: {}'.format(A, alpha)) print('B: {}, beta: {}'.format(B, beta))
最小二乗法により回帰直線の切片と傾きを求めるやり方は「血中濃度データに1-コンパートメント急速静注モデルを当てはめる」でやったのと同じですが、関数にしています。
まずβ相ですが、t, cともに後半の4点を[4:8]のようにスライスを使って指定し、関数に渡しています。
次のα相ですが、まず(5)式の左辺の真数の部分です。
c_diff = [c_data - pow(10, intercept_q + slope_b * (x - T)) for (x, c_data) in zip(t[:4], c[:4])]
数式のは、先に求めた切片と傾きからpow()を使って10のべき乗として計算しています。そしてここで求めたc_diffとtの前半の4点でα相の傾きと切片を求めます。*2
これでα、β相の2つの直線の傾きと切片がわかったので、あとはA、B、α、βを求めるだけです。私の環境では、次のような値になりました。
A: 55.17473179954318, alpha: 1.0960384175663902 B: 121.92396189775874, beta: 0.1027524118430043
このA、B、α、βを(3)の式に入れてプロットしたものが、冒頭のグラフ右側になります。
非線形最小二乗法によるフィッティング
最小二乗法では、測定されたデータにうまくフィットするような関数のパラメータ(係数)を求めますが、コンパートメントモデルのような複雑な関数の場合はパラメータを求めるための方程式を解析的に解くことができず、反復計算によって求めることになります。
この反復計算を始める際には、パラメータの値としてある程度真値に近い初期値を指定してやる必要があります。ここまでのところで、対数変換したデータに回帰直線を当てはめ、2-コンパートメント点滴静注モデルの関数のパラメータを求めました。
ここではこのパラメータの値を初期値として、scipyの関数を用いた非線形最小二乗法でのフィッティングをしてみます。
from scipy import optimize # VCM 1.0gを1時間で点滴静注したときの8点の血中濃度データ t = np.array([1.0, 1.5, 2.0, 3.0, 5.0, 7.0, 12.0, 24.0]) c = np.array([49.5, 31.6, 23.3, 13.8, 8.9, 6.4, 3.2, 1.2]) # A: 55.17473179954318, alpha: 1.0960384175663902 # B: 121.92396189775874, beta: 0.1027524118430043 # 先に求めた値を初期値として使う init_param = [A, alpha, B, beta] # パラメータをフィッティングしたい関数を定義する def func(param, t, c, T=1.0): A = param[0] alpha = param[1] B = param[2] beta = param[3] return A * (np.exp(-alpha * (t-T)) - np.exp(-alpha * t)) \ + B * (np.exp(-beta * (t-T)) - np.exp(-beta * t)) - c result_param = optimize.leastsq(func, init_param, args=(t, c))
コードは続きとなりますので、既にA、B、α、βには先に求めた値が入っているものとします。
まず一行目はモジュールのインポートです。次に、初期値として各パラメータの値をリストにした後、点滴終了後の式(3)をdef funcとして定義しています。ここは方程式が=0となるように記述するので、
と書いています。
そして一番下がleastsq関数ですが、後ろの引数args=()で測定データを渡しています。このleastsqという関数はLevenerg-Marquardt 法というアルゴリズムを使用しており、”初期値が正解から離れている場合は収束が遅くなるが、安定して収束する方法”とのこと。
そしてA、α、B、βは次のような値になりました。
[ 48.51929027 1.25873556 116.69010502 0.13384337]
これらのパラメータを使い、点滴中を(1)式、点滴終了後を(3)式で血中濃度推移をプロットすると下の図のようになります。
ちなみに、同じscipyのcurve_fitを使うと、今回のケースでは初期値がなくてもほぼ同様にフィッティングすることができます(leastsqの場合とはA、Bの値が逆になっていて、数値も微妙に違いますが)。内部的にはleastsqを呼び出しているようなのですが、初期値がなくても良いというのはより使いやすそう。
# モデル関数. 第一引数は独立変数, 第二引数以降はフィッティングパラメータ def func2(t, A, B, alpha, beta): return A * (np.exp(-alpha * (t-1.0)) - np.exp(-alpha * t)) \ + B * (np.exp(-beta * (t-1.0)) - np.exp(-beta * t)) # expがオーバーフローする等の警告が出るが結果は出るのでとりあえず無視. import warnings warnings.filterwarnings('ignore') result, cov = optimize.curve_fit(func2, t, c) print(result)
薬物動態パラメータを求める
ページ冒頭で示したA、B、α、βの式からを求める式を導くことができます。
A、Bの式を変形して
これをが未知数の2元連立1次方程式として解くと、
となります。は式変形するだけです。
また、は0~(点滴終了まで)と~∞のAUCの和*3で求められ、以下のようになります(×となっていますが、A、Bに1/が入っているのでは消えます)。
これは、「1-コンパートメント点滴静注繰り返し投与ー最高・最低血中濃度とAUCー」で見たように、繰り返し投与時の投与間隔のAUCと同じであり、また2-コンパートメント急速静注モデルでのAUCとも同じになります。
定常状態での分布容積についてみてみます。定常状態では中心・抹消コンパートメント間での薬物の行き来は見かけ上なくなり、両コンパートメントの薬物濃度は等しいと考えると、は以下の式で表されます。
はとの和であるため、
では、コードで書いてみます。
k21 = (A * alpha * beta + B * alpha * beta) / (A * alpha + B * beta) ke = (alpha * beta) / k21 k12 = alpha + beta - k21 - ke Vd1 = x0 / T / (A * alpha + B * beta) print('AUC(μg・hr/mL): ', (A + B) * T) print('t_half(hr)(β相): ', np.log(2) / beta) print('ke(hr^-1): ', ke) print('k12(hr^-1): ', k12) print('k21(hr^-1): ', k21) print('CL(mL/min):', ke * Vd1 * 1000 / 60) print('Vd1(L): ', Vd1) print('Vss(L): ', Vd1 * (1 + k12 / k21))
結果は、次のようになりました。
AUC(μg・hr/mL): 165.20939529041303 t_half(hr)(β相): 5.1787934102458175 ke(hr^-1): 0.4642057638710173 k12(hr^-1): 0.5654448622472403 k21(hr^-1): 0.36292831194850056 CL(mL/min): 100.88207536483745 Vd1(L): 13.039313582439902 Vss(L): 33.354655840630905
インタビューフォーム記載の値と比べてみました。
単位は上と同じです。
投与量(g) | ||||||||
---|---|---|---|---|---|---|---|---|
1.0 | 166 | 5.23 | 0.52 | 0.72 | 0.41 | 105.1 | 12.5 | 33.2 |
どれも微妙に違いますね・・。まあこんなもんかな、というかんじでしょうか。*4
参考
- 徹底解説 薬物動態の数学ー微積分と対数、非線形ー
- はじめての薬物速度論-薬物動態の基礎
最後に、コード全文を載せておきます。
import numpy as np import matplotlib.pyplot as plt from scipy import optimize x0 = 1000 # 投与量(mg) T = 1.0 # 点滴時間(hr) # VCM 1.0gを1時間で点滴静注したときの8点の血中濃度データ t = np.array([1.0, 1.5, 2.0, 3.0, 5.0, 7.0, 12.0, 24.0]) c = np.array([49.5, 31.6, 23.3, 13.8, 8.9, 6.4, 3.2, 1.2]) # 最小二乗法により回帰直線のintercept, slopeを返す def least_squares_method(xdata, ydata): X = np.zeros((len(xdata), 2), float) # データ数×2列の行列を作る X[:, 0] = 1 X[:, 1] = xdata logy = np.log10(ydata) # 血中濃度の常用対数をとる (theta, residuals, rank, s) = np.linalg.lstsq(X, logy, rcond=None) return theta[0], theta[1] # α・β相の傾きと切片を求める # 後半の4点をβ相とする intercept_q, slope_b = least_squares_method(t[4:8] - T, c[4:8]) # 最初の4点の実測値 - 消失相(β相)の外挿した直線上の値との差を求める. c_diff = [c_data - pow(10, intercept_q + slope_b * (x - T)) for (x, c_data) in zip(t[:4], c[:4])] # 前半の4点をα相とする intercept_p, slope_a = least_squares_method(t[:4] - T, c_diff) # 傾きからalpha, betaを求める alpha = slope_a * -2.303 beta = slope_b * -2.303 # 切片からA, Bを求める A = 10 ** intercept_p / (1 - np.exp(-alpha * T)) B = 10 ** intercept_q / (1 - np.exp(-beta * T)) print('A: {}, alpha: {}'.format(A, alpha)) print('B: {}, beta: {}'.format(B, beta)) plt.figure(figsize=(11, 6)) plt.subplots_adjust(wspace=0.35, hspace=0.35) # plotのためx軸の値を作成 x = np.arange(0, 25, 0.01) # 左側グラフ(片対数プロット) plt.subplot(1, 2, 1) # プロットはlogスケールで表示するので変換前の値にしておく alpha_line = np.power(10, intercept_p + slope_a * (x - T)) # α相の直線 beta_line = np.power(10, intercept_q + slope_b * (x[500:] - T)) # β相の直線[500:] beta_ex = np.power(10, intercept_q + slope_b * (x[:500] - T)) # β相の外挿部分[:499] # 各直線のプロット plt.plot(x, alpha_line, 'red', label='A='+str(round(A, 1))+', α='+str(round(alpha, 3))) plt.plot(x[500:], beta_line, 'blue', label='B='+str(round(B, 1))+', β='+str(round(beta, 3))) plt.plot(x[:500], beta_ex, 'blue', linestyle='--') # 実測点 - β相の外挿線のプロット plt.plot(t[:4], c_diff, '.', color='lime', label='実測値-β相の外挿線', alpha=0.6, markersize=8) plt.yscale('log', basey=10) plt.ylim(1, 200) # 右側グラフ(ノーマルプロット) plt.subplot(1, 2, 2) time = np.arange(1, 25, 0.01) c_form = A * (1 - np.exp(-alpha * T)) * np.exp(-alpha * (time - T)) \ + B * (1 - np.exp(-beta * T)) * np.exp(-beta * (time - T)) plt.plot(time, c_form, 'magenta', label='$A(e^{-α(t-T)}-e^{-αt}) + B(e^{-β(t-T)}-e^{-βt})$') plt.ylim(0, 60) # 2つのグラフで共通する設定は以下で行う axs = plt.gcf().get_axes() for ax in axs: # current axsを設定 plt.sca(ax) # 実測データをプロット plt.plot(t, c, '.', color='green', label='実測値', alpha=0.6, markersize=8) # 軸ラベルを表示 plt.ylabel('血中濃度[μg/mL]') plt.xlabel('時間[hr]') # 横軸の範囲を指定する plt.xlim(0, 25) # グリッドの表示 plt.grid(which='both', linestyle='dashed') # ラベルに応じた凡例を表示 plt.legend() plt.show() # 上で得られた値を初期値として2コンパートメントモデルの点滴静注時の数式をフィッティングする. # scipyのoptimize.leastsqを用いて非線形回帰を行い, A, alpha, B, betaを求める init_param = [A, alpha, B, beta] print('init_param :', init_param) # パラメータをフィッティングしたい関数を定義する def func(param, t, c, T=1.0): A = param[0] alpha = param[1] B = param[2] beta = param[3] return A * (np.exp(-alpha * (t-T)) - np.exp(-alpha * t)) \ + B * (np.exp(-beta * (t-T)) - np.exp(-beta * t)) - c result_param = optimize.leastsq(func, init_param, args=(t, c)) A = result_param[0][0] alpha = result_param[0][1] B = result_param[0][2] beta = result_param[0][3] k21 = (A * alpha * beta + B * alpha * beta) / (A * alpha + B * beta) ke = (alpha * beta) / k21 k12 = alpha + beta - k21 - ke Vd1 = x0 / T / (A * alpha + B * beta) print('AUC(μg・hr/mL): ', (A + B) * T) print('t_half(hr)(β相): ', np.log(2) / beta) print('ke(hr^-1): ', ke) print('k12(hr^-1): ', k12) print('k21(hr^-1): ', k21) print('CL(mL/min):', ke * Vd1 * 1000 / 60) print('Vd1(L): ', Vd1) print('Vss(L): ', Vd1 * (1 + k12 / k21)) # 2-コンパートメント点滴静注モデル def calc_conc(time, A, alpha, B, beta, T=1.0): c = [] for t in time: if t <= T: # 点滴開始~終了時 c.append(A * (1 - np.exp(-alpha * t)) + B * (1 - np.exp(-beta * t))) else: # 点滴終了後 c.append(A * (np.exp(-alpha * (t - T)) - np.exp(-alpha * t)) + B * (np.exp(-beta * (t - T)) - np.exp(-beta * t))) return c time = np.arange(0, 25, 0.01) plt.plot(time, calc_conc(time, A, alpha, B, beta), color='red') plt.plot(t, c, 'b.', label='実測値', alpha=0.6, markersize=8) plt.ylabel('血中濃度[μg/mL]') plt.xlabel('時間[hr]') plt.ylim(0, 60) plt.xlim(0, 25) plt.grid(which='both', linestyle='dashed') plt.legend() plt.title('2-コンパートメント点滴静注モデル') plt.show()