今回の記事は参考文献『4ステップ 臨床力UPエクササイズ4 TDM領域』の『Step3 薬物治療の個別化(薬物投与設計)の実践』の項目を基に書いています。
この項目の概要としては、喘息症状が悪化した患者のスクリーニングを通して薬物クリアランスの変動を考慮した投与設計を行っていく、という内容で、その中に喫煙時と非喫煙時の実測のトラフ値からkeを推定する、という部分があります。
参考文献の中では、Excelでkeを変化させながら定常状態におけるトラフ値が実測値に近づくようフィッティングしていますが、ここではPythonでコードを書いてやってみたいと思います。
- 喫煙がテオフィリンの血中濃度に与える影響
- 定常状態におけるトラフ値
- Sympyで方程式を代数的に解く
- Css_minの式を可視化する
- Scipyで方程式の数値解を求める
- 求めたkeを使って繰り返し投与を描画
- 参考
喫煙がテオフィリンの血中濃度に与える影響
テオフィリンは主にCYP1A2で代謝されますが、喫煙によりこの酵素が誘導され、クリアランスが上昇することが知られています。そのため、喫煙時にはテオフィリン血中濃度が低下し、非喫煙時よりも高い用量が必要になることがあります。
定常状態におけるトラフ値
前回記事でも書きましたが、最低血中濃度(トラフ値)は定常状態での血中濃度を表す式において、のときとなるため以下の式になります。
このがある特定の値のときのkeを求める、ということになります。
Sympyで方程式を代数的に解く
上で示した式において、ke以外のパラメータの値()はすべて既知の値であるとします。ここで、非喫煙時のトラフ値が9μg/mL、喫煙時のトラフ値が
3μg/mLだったとして、それぞれのトラフ値におけるkeを求めます。
import sympy # 変数を定義 ke = sympy.Symbol('ke') F = 1 # バイオアベイラビリティ D = 200 # 投与量(mg) vd = 0.46 * 63.6 # 分布容積(L/kg) * 理想体重(kg) ka = 0.36 # 吸収速度定数(hr^-1) tau = 12 # 投与間隔(hr) trough = 9 # 実測トラフ値(μg/mL) # 数式fが見にくくなるのでAとして先にまとめておく A = F * D * ka / vd / (ka - ke) # 数式を定義 f = A * sympy.exp(-ke * tau) / (1 - sympy.exp(-ke * tau)) \ - A * sympy.exp(-ka * tau) / (1 - sympy.exp(-ka * tau)) - trough # 方程式を解く print(sympy.solve(f, ke))
各パラメータは参考文献のもの(これもインタビューフォームの値です)をそのまま使用しています。分布容積については、この問題で想定する患者は100kgですが、実測体重で薬物量を計算すると過量となるためBMIが22となる理想体重で計算しています。
トラフ値はまずは非喫煙時として9を設定しました。また、sympy.solve()では()内の式が=0とした場合の解が得られるため、数式fではtroughを移項した形にしています。
このようにして、今までと同様にSympyのsolve()を使って解こうとしましたが、次のようなエラーメッセージが表示されうまくいきません。
No algorithms are implemented to solve equation
「方程式を解くためのアルゴリズムが実装されていない」と出ています。どうやら代数計算では解けないようなので、数値計算で解を求めることにしました。
そこで、調べたところsympyで非線形方程式を数値的に解く関数としてnsolve()というものがあり、これを使おうと思ったのですが、関数に渡す初期値がシビア(正解に近い値でないと解が得られなかった)だったため、結局今までも何度かお世話になっているscipy.optimizeを使用することにしました。
Css_minの式を可視化する
先にsympyで解きたい方程式fを可視化してグラフの概形を確認しておきます。
sympyではplottingモジュールを使うことで、定義した数式をそのまま渡すだけでプロットすることができます。上で書いたコードの下に、次のように書きます。
上で示したコードのprint(sympy.solve(f, ke))はエラーが出るので消しておいて下さい。
from sympy.plotting import plot # keが[0.01, 0.15]の範囲でプロット plot(f, (ke, 0.01, 0.15))
keの範囲はインタビューフォームの値から当たりをつけて上記で設定してみました。
これを見るとだいたい0.05を過ぎたあたりに解がありそうです。ちなみに、このグラフだけみると単純な形をしているように見えますが、keの範囲を広げてmatplotlibでプロットすると次のような形をしていました。
Scipyで方程式の数値解を求める
scipy.optimize モジュールのnewton()関数を使います。公式ドキュメントによれば、Newton-Raphson法(または割線法)を使用して実関数または複素関数のゼロを求めるとのこと。ではやってみます。
from scipy import optimize import numpy as np F = 1 # バイオアベイラビリティ D = 200 # 投与量(mg) vd = 0.46 * 63.6 # 分布容積(L/kg) * 理想体重(kg) ka = 0.36 # 吸収速度定数(hr^-1) tau = 12 # 投与間隔(hr) def func(ke, param, trough): _F = param[0] _D = param[1] _vd = param[2] _ka = param[3] _tau = param[4] _A = _F * _D * _ka / _vd / (_ka - ke) return _A * np.exp(-ke * _tau) / (1 - np.exp(-ke * _tau)) \ - _A * np.exp(-_ka * _tau) / (1 - np.exp(-_ka * _tau)) - trough param = [F, D, vd, ka, tau] trough1 = 9 # 非喫煙時の実測トラフ値(μg/mL) # optimize.newton(根を求めたい関数, 初期推定値, 追加の引数) ke1 = optimize.newton(func, 0.1, args=(param, trough1)) print('ke1:', ke1) # 0.05255675488882937 trough2 = 3 # 喫煙時の実測トラフ値(μg/mL) ke2 = optimize.newton(func, 0.1, args=(param, trough2)) print('ke2:', ke2) # 0.12123491149221788
コメントに書きましたがnewton()の引数は前から順に根を求めたい関数, 初期推定値, 追加の引数となっています(他にも導関数や許容誤差、反復の最大数などを指定できる)。
今度はうまく解が求まりました。上でみたグラフの値とも一致しているようにみえます。
求めたkeを使って繰り返し投与を描画
この推定したkeを使って繰り返し投与をプロットしてみました。描画部分のプログラムは前回記事で作成したものをほぼそのまま使っています。
color: blue Css_min: 8.999999999999266 ------------------------------ color: red Css_min: 3.0000000000000018 ------------------------------
定常状態でのCss_minも9と3μg/mLとなっています。
参考文献ではこの推定されたkeについて、個別化されたパラメータを使用していないため正確ではないとしつつも、喫煙によりクリアランスが2倍以上上昇したと考え、アミノフィリンの投与設計、維持投与量の考察へと話が続いていきます。
参考
- 4ステップ 臨床力UPエクササイズ4TDM領域
最後に、今回のコードを載せておきます。
import numpy as np from scipy import optimize import matplotlib.pyplot as plt # 喫煙時と非喫煙時のトラフ値からkeを推定する class PoOneComp2: STEP = 0.01 # 経過時間の刻み幅 def __init__(self, F, D, vd, ka, ke, interval): self.F = F self.D = D self.vd = vd self.ka = ka self.ke = ke self.interval = interval self.A = F * D * ka / vd / (ka - ke) self.t = np.linspace(0, interval, num=int(interval / PoOneComp2.STEP), endpoint=False) # n回目の1投与区間の血中濃度 def get_section_conc(self, n): c = self.A * np.exp(-self.ke * self.t) * (1 - np.exp(-n * self.ke * self.interval)) \ / (1 - np.exp(-self.ke * self.interval)) \ - self.A * np.exp(-self.ka * self.t) * (1 - np.exp(-n * self.ka * self.interval)) \ / (1 - np.exp(-self.ka * self.interval)) return c.tolist() # 定常状態における最低血中濃度 def get_cmin(self): cmin = self.A * np.exp(-self.ke * self.interval) / (1 - np.exp(-self.ke * self.interval)) \ - self.A * np.exp(-self.ka * self.interval) / (1 - np.exp(-self.ka * self.interval)) return cmin # 定常状態における最高血中濃度 def get_cmax(self): tmax = self.get_tmax() cmax = self.A * np.exp(-self.ke * tmax) / (1 - np.exp(-self.ke * self.interval)) \ - self.A * np.exp(-self.ka * tmax) / (1 - np.exp(-self.ka * self.interval)) return cmax # 定常状態における最高血中濃度到達時間 def get_tmax(self): tmax = np.log(self.ka / self.ke * (1 - np.exp(-self.ke * self.interval)) / (1 - np.exp(-self.ka * self.interval))) / (self.ka - self.ke) return tmax # AUC(投与間隔) def get_auc(self): return self.F * self.D / self.ke / self.vd # 定常状態における平均血中濃度 def get_cssave(self): return self.get_auc() / self.interval if __name__ == '__main__': F = 1 # バイオアベイラビリティ D = 200 # 投与量(mg) vd = 0.46 * 63.6 # 分布容積(L/kg) * 理想体重(kg) ka = 0.36 # 吸収速度定数(hr^-1) tau = 12 # 投与間隔(hr) days = 5 # 何日投与するか # optimize.newton()でf(ke)=0を解く def func(ke, param, trough): _F = param[0] _D = param[1] _vd = param[2] _ka = param[3] _tau = param[4] _A = _F * _D * _ka / _vd / (_ka - ke) return _A * np.exp(-ke * _tau) / (1 - np.exp(-ke * _tau)) \ - _A * np.exp(-_ka * _tau) / (1 - np.exp(-_ka * _tau)) - trough param = [F, D, vd, ka, tau] trough1 = 9 # 非喫煙時の実測トラフ値(μg/mL) # newton(根を求めたい関数, 初期推定値, 追加の引数) ke1 = optimize.newton(func, 0.1, args=(param, trough1)) print('ke1:', ke1) trough2 = 3 # 喫煙時の実測トラフ値(μg/mL) ke2 = optimize.newton(func, 0.1, args=(param, trough2)) print('ke2:', ke2) # インスタンス生成 po1 = PoOneComp2(F, D, vd, ka, ke1, tau) # 非喫煙時(blue) po2 = PoOneComp2(F, D, vd, ka, ke2, tau) # 喫煙時(red) # 描画区間の投与回数 n = int(days * 24 / tau) conc1, conc2 = [], [] # key: 色, value: (インスタンス, 投与回数, 血中濃度, 実測トラフ値, 推定したke) po = {'blue': (po1, n, conc1, trough1, ke1), 'red': (po2, n, conc2, trough2, ke2)} period = 24 * days time = np.linspace(0, period, int(period / PoOneComp2.STEP), endpoint=False) for color, value in po.items(): _po, n, conc, tgh, ke = value # 各クラス毎に1~n回目の投与までの血中濃度を加えていく for i in range(1, n + 1): conc.extend(_po.get_section_conc(i)) # 1回投与量, 1日投与回数 dose = int(_po.D) num = int(24 / _po.interval) plt.plot(time, conc, c=color, alpha=0.8, label='$\\rm C_{trough}$='+str(tgh) + 'μg/mLでフィッティング\nke='+str(round(ke,4))) print('color: ', color) print('Css_min: ', _po.get_cmin()) print('-' * 30) plt.title('実測トラフ値からのke推定(イメージ図)') # 軸ラベルを表示 plt.ylabel('血中濃度[μg/mL]') plt.xlabel('時間[hr]') plt.xlim(0, ) # 目盛りを24時間毎にする plt.xticks(np.arange(0, time[-1] + 24, 24)) plt.ylim(0, 16) plt.grid(linestyle='dashed') plt.legend() plt.show()