In [26]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.optimize import curve_fit
from itertools import product

Загрузка данных

Фермент: простагландин-H-синтаза (PGHS) из везикулярных желёз барана (Ovis aries).
Варьируемый субстрат: арахидоновая кислота.
Ингибитор: напроксен.

In [2]:
data = pd.read_csv("2019_практикум-3_302_Мыларщиков.tsv", sep='\t', header=[0, 1, 2], index_col=None)
In [5]:
data.head()
Out[5]:
S = 3.33 uM S = 10 uM ... S = 50 uM S = 100 uM
I = 0 uM I = 0.07 uM I = 0.15 uM I = 0 uM I = 0.07 uM ... I = 0.07 uM I = 0.15 uM I = 0 uM I = 0.07 uM I = 0.15 uM
time O2_consumption time O2_consumption time O2_consumption time O2_consumption time O2_consumption ... time O2_consumption time O2_consumption time O2_consumption time O2_consumption time O2_consumption
0 0.027 -0.36289 0.000 -0.03134 0.001 -0.03144 0.004 -0.04063 -0.003 0.04278 ... -0.004 0.00912 0.001 -0.00351 -0.003 0.02321 -0.005 0.00124 -0.003 -0.01784
1 0.215 0.14050 0.188 0.05712 0.251 0.03946 0.254 0.27471 0.184 0.23044 ... 0.184 0.15540 0.251 0.04202 0.184 0.11886 0.183 0.14673 0.247 -0.00687
2 0.465 0.39874 0.438 0.36312 0.501 0.32248 0.441 0.41464 0.434 0.94287 ... 0.434 0.48167 0.438 0.40094 0.434 0.56719 0.433 0.28224 0.497 0.16422
3 0.652 0.37638 0.625 0.31569 0.688 0.60313 0.691 0.40558 0.684 0.87516 ... 0.621 0.84679 0.688 0.69910 0.622 1.39156 0.683 0.44571 0.685 0.21143
4 0.902 0.36800 0.875 0.48220 0.938 0.53487 0.879 1.17166 0.872 1.06271 ... 0.871 0.99755 0.876 0.82471 0.872 1.80049 0.870 0.37729 0.935 0.54192

5 rows × 24 columns

In [6]:
data.columns
Out[6]:
MultiIndex(levels=[['S = 10 uM', 'S = 100 uM', 'S = 3.33 uM', 'S = 50 uM'], ['I = 0 uM', 'I = 0.07 uM', 'I = 0.15 uM'], ['O2_consumption', 'time']],
           codes=[[2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1], [0, 0, 1, 1, 2, 2, 0, 0, 1, 1, 2, 2, 0, 0, 1, 1, 2, 2, 0, 0, 1, 1, 2, 2], [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0]])
In [7]:
s_conc = [3.33, 10, 50, 100]
i_conc = [0, 0.07, 0.15]
In [14]:
plt.figure(figsize=(20, 15))
for i, conc in enumerate(product(i_conc, s_conc)):
    plt.subplot(3, 4, i+1)
    x = data.loc[:, (f'S = {conc[1]} uM', f'I = {conc[0]} uM', 'time')].dropna()
    y = data.loc[:, (f'S = {conc[1]} uM', f'I = {conc[0]} uM', 'O2_consumption')].dropna()
    plt.plot(x, y, 'bo')
    plt.title(f'S = {conc[1]} uM I = {conc[0]} uM')
    plt.xlabel('time')
    plt.ylabel('O2 consumption')
plt.tight_layout()

Задание 1. Рассчёт начальной скорости реакции.

Воспользуемся методом начальных скоростей и аппроксимируем первые 5 секунд реакции. Для начала посмотрим на эти 5 секунд.

In [17]:
plt.figure(figsize=(20, 15))
for i, conc in enumerate(product(i_conc, s_conc)):
    plt.subplot(3, 4, i+1)
    time = data.loc[:, (f'S = {conc[1]} uM', f'I = {conc[0]} uM', 'time')].dropna()
    cons = data.loc[:, (f'S = {conc[1]} uM', f'I = {conc[0]} uM', 'O2_consumption')].dropna()
    plt.plot(time[time < 5], cons[time < 5], 'bo')
    plt.title(f'S = {conc[1]} uM I = {conc[0]} uM')
    plt.xlabel('time')
    plt.ylabel('O2 consumption')
plt.tight_layout()

Данных для каждого эксперимента в первые 5 секунд немного, но они вполне неплохие.

In [19]:
o2_speed = list()
for conc in product(i_conc, s_conc):
    time = data.loc[:, (f'S = {conc[1]} uM', f'I = {conc[0]} uM', 'time')].dropna()
    cons = data.loc[:, (f'S = {conc[1]} uM', f'I = {conc[0]} uM', 'O2_consumption')].dropna()
    slope, intercept = np.polyfit(x=time[time < 5],
                                  y=cons[time < 5],
                                  deg=1)
    o2_speed.append([slope, intercept])
o2_speed = np.array(o2_speed, dtype=float)

Для удобства соберём данные в таблицу.

In [55]:
speeds = pd.DataFrame({'speed': o2_speed[:, 0] / 2,
                       'inhibitor' : [i[0] for i in list(product(i_conc, s_conc))],
                       'substrate' : [i[1] for i in list(product(i_conc, s_conc))]})
In [31]:
speeds
Out[31]:
speed inhibitor substrate
0 0.342296 0.00 3.33
1 0.615720 0.00 10.00
2 0.846730 0.00 50.00
3 0.927009 0.00 100.00
4 0.172186 0.07 3.33
5 0.387310 0.07 10.00
6 0.577962 0.07 50.00
7 0.577946 0.07 100.00
8 0.143529 0.15 3.33
9 0.279603 0.15 10.00
10 0.397574 0.15 50.00
11 0.377811 0.15 100.00

Посмотрим, как у нас прошли аппроксимации.

In [33]:
plt.figure(figsize=(20, 15))
for i, conc in enumerate(product(i_conc, s_conc)):
    plt.subplot(3, 4, i+1)
    time = data.loc[:, (f'S = {conc[1]} uM', f'I = {conc[0]} uM', 'time')].dropna()
    cons = data.loc[:, (f'S = {conc[1]} uM', f'I = {conc[0]} uM', 'O2_consumption')].dropna()
    plt.plot(time[time < 5], cons[time < 5], 'bo')
    plt.plot(time[time < 5], time[time < 5] * o2_speed[i, 0] + o2_speed[i, 1], color='red')
    plt.title(f'S = {conc[1]} uM I = {conc[0]} uM')
    plt.xlabel('time')
    plt.ylabel('O2 consumption')
plt.tight_layout()

В целом, вполне хорошо.

Поглядим на зависимость начальной скорости реакции от концентрации субстрата при разных концентрациях ингибитора.

In [34]:
sns.pointplot(data=speeds, x='substrate', y='speed', hue='inhibitor')
Out[34]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f0fe0a7bda0>

Задание 2. Графическое определение типа ингибирование в обратных координатах.

In [56]:
speeds['reverse_speed'] = 1 / speeds['speed']
speeds['reverse_substrate'] = 1 / speeds['substrate']
In [57]:
for inh in i_conc:
    x = speeds.query(f'inhibitor == {inh}')['reverse_substrate']
    y = speeds.query(f'inhibitor == {inh}')['reverse_speed']
    plt.plot(x, y, 'o', label=inh)
plt.legend(title='Inhibitor, uM')
plt.xlabel('1 / S')
plt.ylabel('1 / V')
Out[57]:
Text(0, 0.5, '1 / V')

Это всё классно, конечно, но аппроксимировать по 4 точкам — это довольно слабо.

In [58]:
reverse_approx = list()
for inh in i_conc:
    x = speeds.query(f'inhibitor == {inh}')['reverse_substrate']
    y = speeds.query(f'inhibitor == {inh}')['reverse_speed']
    slope, intercept = np.polyfit(x=x,
                                  y=y,
                                  deg=1)
    reverse_approx.append((slope, intercept))
In [59]:
for i, inh in enumerate(i_conc):
    x = speeds.query(f'inhibitor == {inh}')['reverse_substrate']
    y = speeds.query(f'inhibitor == {inh}')['reverse_speed']
    plt.plot(x, y, 'o', label=inh)
    app_x = np.linspace(-0.3, 0.5, 10)
    app_y = app_x * reverse_approx[i][0] + reverse_approx[i][1]
    plt.plot(app_x, app_y, color='red')
plt.legend(title='Inhibitor, uM')
plt.xlabel('1 / S')
plt.ylabel('1 / V')
plt.axhline(0, color='black')
plt.axvline(0, color='black')
plt.show()

Судя по графику, прямые не пересекаются в одной точке. Но точно понятно, что прямые не параллельны и что они не пересекаются на оси оординат выше нуля. Вдобавок, точки пересечения близки к оси абсцисс левее нуля, поэтому из анализа графика можно сделать вывод, что здесь наблюдается неконкурентное ингибирование.

Причём, судя по тому, что с ростом концентрации ингибитора прямые вращаются против часовой стрелки, то это действительно ингибирование, а не активация. Иначе говоря, в общей схеме ингибирования, здесь $\alpha = 1$, а $0 \leq \beta \leq 1$.

Задание 3. Определение кинетических параметров реакций.

Проведём аппроксимацию начальных скоростей в прямых и двойных обратных координатах для всех указанных в задании механизмов ингибирования и определим точность аппроксимации с помощью MSE.

In [90]:
def competitive_direct(x, vm, km, ki):
    s, i = x
    return vm * s / (km * (1 + i / ki) + s)

def competitive_reverse(x, vm, km, ki):
    s, i = x
    return 1 / vm + km * (1 + i / ki) / vm / s

def noncompetitive_direct(x, vm, km, ki):
    s, i = x
    return vm * s / (1 + i / ki) / (km + s)

def noncompetitive_reverse(x, vm, km, ki):
    s, i = x
    return (1 + i / ki) / vm + km * (1 + i / ki) / vm / s

def uncompetitive_direct(x, vm, km, ki):
    s, i = x
    return vm * s / (1 + i / ki) / (km / (1 + i / ki) + s)

def uncompetitive_reverse(x, vm, km, ki):
    s, i = x
    return (1 + i / ki) / vm + km / vm / s

def mse(x1, x2):
    if type(x1) != np.ndarray or type(x2) != np.ndarray:
        raise ValueError(f"Arguments are of not supported type: x1 {type(x1)} and x2 {type(x2)}")
    return np.square(x1 - x2).mean()
In [91]:
inh_funcs = {func.__name__: func
             for func in [competitive_direct,
                          competitive_reverse,
                          noncompetitive_direct,
                          noncompetitive_reverse,
                          uncompetitive_direct,
                          uncompetitive_reverse]}
In [93]:
approx_results = list()
approx_keys = ['Vm', 'Km', 'Ki', 'inh_type', 'inh_coord', 'i_conc']
for func_name, func in inh_funcs.items():
    inh_type, inh_coord = func_name.split("_")
    for inh in i_conc:
        approx_data = speeds.query(f'inhibitor == {inh}')
        if inh_coord == 'direct':
            x = approx_data['substrate']
            y = approx_data['speed']
        elif inh_coord == 'reverse':
            x = approx_data['reverse_substrate']
            y = approx_data['reverse_speed']
        popt, pcov = curve_fit(func, (x, inh), y, bounds=(0, 'inf'))
        approx_results.append(dict(zip(approx_keys, list(popt) + [inh_type, inh_coord, inh])))
In [96]:
inh_results = pd.DataFrame.from_records(approx_results)
In [97]:
inh_results
Out[97]:
Ki Km Vm i_conc inh_coord inh_type
0 1.000000 5.963017e+00 0.968330 0.00 direct competitive
1 0.940461 6.969584e+00 0.643361 0.07 direct competitive
2 0.911584 4.808723e+00 0.420347 0.15 direct competitive
3 1.000000 2.206650e-90 0.587815 0.00 reverse competitive
4 6939.105996 2.629090e-11 0.337581 0.07 reverse competitive
5 962.642768 1.020888e-11 0.254698 0.15 reverse competitive
6 1.000000 5.963017e+00 0.968330 0.00 direct noncompetitive
7 0.741181 7.488331e+00 0.704123 0.07 direct noncompetitive
8 0.547340 5.600000e+00 0.535544 0.15 direct noncompetitive
9 1.000000 2.206650e-90 0.587815 0.00 reverse noncompetitive
10 0.094181 1.909481e-17 0.588434 0.07 reverse noncompetitive
11 0.159976 5.350370e-26 0.493484 0.15 reverse noncompetitive
12 1.000000 5.963017e+00 0.968330 0.00 direct uncompetitive
13 0.716943 8.219467e+00 0.706177 0.07 direct uncompetitive
14 0.520579 7.213577e+00 0.541466 0.15 direct uncompetitive
15 1.000000 2.206650e-90 0.587815 0.00 reverse uncompetitive
16 0.000685 8.239093e-17 34.851616 0.07 reverse uncompetitive
17 0.002027 4.513270e-18 19.104286 0.15 reverse uncompetitive
In [100]:
melted_params = pd.melt(inh_results,
                        id_vars=['i_conc', 'inh_coord', 'inh_type'],
                        value_vars=['Ki', 'Km', 'Vm'],
                        var_name='parameter',
                        value_name='value')
In [105]:
inh_coord = list(melted_params['inh_coord'].unique())
inh_type = list(melted_params['inh_type'].unique())
In [106]:
print(inh_coord, inh_type)
['direct', 'reverse'] ['competitive', 'noncompetitive', 'uncompetitive']
In [117]:
plt.figure(figsize=(15, 10))
for i, item in enumerate(product(inh_coord, inh_type)):
    plt.subplot(2, 3, i + 1)
    plot_data = melted_params.query(f'inh_coord == "{item[0]}" and inh_type == "{item[1]}"')
    for inh in i_conc:
        plot_plot_data = plot_data.query(f"i_conc == {inh}")
        plt.plot(plot_plot_data['parameter'], plot_plot_data['value'], 'o', label=inh)
    plt.legend(title='Inhibitor, uM')
    plt.title(f"{item[1]} inhibition in {item[0]} coordinates")

Мы видим, что в прямых координатах все типы ингибирования согласуются между собой в полученных значениях параметров, в то время как в обратных координатах согласованность пропадает. Если сравнивать прямые и обратные координаты для каждого типа ингибирования, окажется, что:

  1. У конкурентного ингибирования очень сильно различаются полученные для $K_i$ значения;
  2. У неконкурентного ингибирования значительно различаются полученные для $K_M$ значения;
  3. У бесконкурентного ингибирования значительно различаются полученные для $K_M$ и $V_m$ значения.

Но в целом, в пределах 1 порядка данные совпадают только для неконкурентного ингибирования, что ещё раз подтверждает нашу гипотезу о механизме ингибирования.

Давайте посмотрим на $MSE$ скоростей реакций по нашим оценкам кинетических параметров, то есть то, насколько хорошо полученные параметры объясняют реакцию.

In [160]:
mse_results = list()
mse_keys = ['inh_type', 'inh_coord', 'i_conc', 'mse']
for func_name, func in inh_funcs.items():
    inh_type, inh_coord = func_name.split("_")
    for inh in i_conc:
        approx_data = speeds.query(f'inhibitor == {inh}')
        if inh_coord == 'direct':
            x = approx_data['substrate']
            y = approx_data['speed']
        elif inh_coord == 'reverse':
            x = approx_data['reverse_substrate']
            y = approx_data['reverse_speed']
        params = inh_results.query(f'inh_coord == "{inh_coord}" and inh_type == "{inh_type}" and i_conc == {inh}')
        ki, km, vm = params['Ki'].item(), params['Km'].item(), params['Vm'].item()
        y_pred = func((x, inh), vm, km, ki)
        error = mse(np.array(y), np.array(y_pred))
        mse_results.append(dict(zip(mse_keys, [inh_type, inh_coord, inh, error])))
In [151]:
mse_df = pd.DataFrame.from_records(mse_results)
In [152]:
mse_df
Out[152]:
i_conc inh_coord inh_type mse
0 0.00 direct competitive 0.000154
1 0.07 direct competitive 0.000452
2 0.15 direct competitive 0.000268
3 0.00 reverse competitive 0.538249
4 0.07 reverse competitive 2.819205
5 0.15 reverse competitive 3.249418
6 0.00 direct noncompetitive 0.000154
7 0.07 direct noncompetitive 0.000452
8 0.15 direct noncompetitive 0.000268
9 0.00 reverse noncompetitive 0.538249
10 0.07 reverse noncompetitive 2.819205
11 0.15 reverse noncompetitive 3.249418
12 0.00 direct uncompetitive 0.000154
13 0.07 direct uncompetitive 0.000452
14 0.15 direct uncompetitive 0.000268
15 0.00 reverse uncompetitive 0.538249
16 0.07 reverse uncompetitive 2.819205
17 0.15 reverse uncompetitive 3.249418
In [153]:
inh_coord = list(melted_params['inh_coord'].unique())
inh_type = list(melted_params['inh_type'].unique())
In [155]:
plt.figure(figsize=(15, 10))
for i, item in enumerate(product(inh_coord, inh_type)):
    plt.subplot(2, 3, i + 1)
    plot_data = mse_df.query(f'inh_coord == "{item[0]}" and inh_type == "{item[1]}"')
    plt.plot(plot_data['i_conc'], plot_data['mse'], 'o')
    plt.title(f"{item[1]} inhibition in {item[0]} coordinates")
    plt.xlabel("Inhibitor, uM")
    plt.ylabel("MSE")
plt.tight_layout()

Удивительно, насколько согласованными оказались значения $MSE$! Я проверил, ошибок в вычислениях нет. Эта одинаковость смущает. Более того, ошибки в прямых координатах маленькие, а в обратных большие.

Давайте исследуем скорости настоящие и предсказанные для разных способов аппроксимации.

In [165]:
speed_results = list()
speed_keys = ['inh_type', 'inh_coord', 'i_conc', 'true_speed', 'predicted_speed']
for func_name, func in inh_funcs.items():
    inh_type, inh_coord = func_name.split("_")
    for inh in i_conc:
        approx_data = speeds.query(f'inhibitor == {inh}')
        if inh_coord == 'direct':
            x = approx_data['substrate']
            y = approx_data['speed']
        elif inh_coord == 'reverse':
            x = approx_data['reverse_substrate']
            y = approx_data['reverse_speed']
        params = inh_results.query(f'inh_coord == "{inh_coord}" and inh_type == "{inh_type}" and i_conc == {inh}')
        ki, km, vm = params['Ki'].item(), params['Km'].item(), params['Vm'].item()
        y_pred = func((x, inh), vm, km, ki)
        speed_results.append(dict(zip(speed_keys, [inh_type, inh_coord, inh, y, y_pred])))
In [166]:
speed_compare = pd.DataFrame.from_records(speed_results)
In [167]:
speed_compare
Out[167]:
i_conc inh_coord inh_type predicted_speed true_speed
0 0.00 direct competitive 0 0.346985 1 0.606608 2 0.865151 3 ... 0 0.342296 1 0.615720 2 0.846730 3 ...
1 0.07 direct competitive 4 0.198033 5 0.367880 6 0.559558 7 ... 4 0.172186 5 0.387310 6 0.577962 7 ...
2 0.15 direct competitive 8 0.156748 9 0.269453 10 0.378010 1... 8 0.143529 9 0.279603 10 0.397574 1...
3 0.00 reverse competitive 0 1.701216 1 1.701216 2 1.701216 3 ... 0 2.921446 1 1.624116 2 1.181014 3 ...
4 0.07 reverse competitive 4 2.96225 5 2.96225 6 2.96225 7 2.... 4 5.807680 5 2.581914 6 1.730219 7 ...
5 0.15 reverse competitive 8 3.926217 9 3.926217 10 3.926217 1... 8 6.967222 9 3.576497 10 2.515253 1...
6 0.00 direct noncompetitive 0 0.346985 1 0.606608 2 0.865151 3 ... 0 0.342296 1 0.615720 2 0.846730 3 ...
7 0.07 direct noncompetitive 4 0.198034 5 0.367880 6 0.559558 7 ... 4 0.172186 5 0.387310 6 0.577962 7 ...
8 0.15 direct noncompetitive 8 0.156747 9 0.269453 10 0.378010 1... 8 0.143529 9 0.279603 10 0.397574 1...
9 0.00 reverse noncompetitive 0 1.701216 1 1.701216 2 1.701216 3 ... 0 2.921446 1 1.624116 2 1.181014 3 ...
10 0.07 reverse noncompetitive 4 2.962519 5 2.962519 6 2.962519 7 ... 4 5.807680 5 2.581914 6 1.730219 7 ...
11 0.15 reverse noncompetitive 8 3.92645 9 3.92645 10 3.92645 11 ... 8 6.967222 9 3.576497 10 2.515253 1...
12 0.00 direct uncompetitive 0 0.346985 1 0.606608 2 0.865151 3 ... 0 0.342296 1 0.615720 2 0.846730 3 ...
13 0.07 direct uncompetitive 4 0.198034 5 0.367880 6 0.559558 7 ... 4 0.172186 5 0.387310 6 0.577962 7 ...
14 0.15 direct uncompetitive 8 0.156748 9 0.269453 10 0.378010 1... 8 0.143529 9 0.279603 10 0.397574 1...
15 0.00 reverse uncompetitive 0 1.701216 1 1.701216 2 1.701216 3 ... 0 2.921446 1 1.624116 2 1.181014 3 ...
16 0.07 reverse uncompetitive 4 2.96255 5 2.96255 6 2.96255 7 2.... 4 5.807680 5 2.581914 6 1.730219 7 ...
17 0.15 reverse uncompetitive 8 3.92648 9 3.92648 10 3.92648 11 ... 8 6.967222 9 3.576497 10 2.515253 1...
In [185]:
inh_coord = list(melted_params['inh_coord'].unique())
inh_type = list(melted_params['inh_type'].unique())
plt.figure(figsize=(15, 10))
for i, item in enumerate(product(inh_coord, inh_type)):
    plt.subplot(2, 3, i + 1)
    plot_data = speed_compare.query(f'inh_coord == "{item[0]}" and inh_type == "{item[1]}"')
    for inh in i_conc:
        plot_plot_data = plot_data.query(f'i_conc == {inh}')
        #print(plot_plot_data['true_speed'])
        plt.plot(plot_plot_data.loc[:, 'true_speed'].item(), plot_plot_data.loc[:, 'predicted_speed'].item(), 'o', label=inh)
    plt.legend(title='Inhibitor, uM')
    plt.title(f"{item[1]} inhibition in {item[0]} coordinates")
    plt.xlabel("True speed")
    plt.ylabel("Predicted speed")
plt.tight_layout()

Что можно понять из этих графиков?

  1. Скорости предсказываются почти что одинаково между разными типами ингибирования;
  2. В прямых координатах скорости предсказаны хорошо;
  3. В обратных координатах скорости предсказаны плохо.
In [186]:
speeds
Out[186]:
speed inhibitor substrate reverse_speed reverse_substrate
0 0.342296 0.00 3.33 2.921446 0.3003
1 0.615720 0.00 10.00 1.624116 0.1000
2 0.846730 0.00 50.00 1.181014 0.0200
3 0.927009 0.00 100.00 1.078738 0.0100
4 0.172186 0.07 3.33 5.807680 0.3003
5 0.387310 0.07 10.00 2.581914 0.1000
6 0.577962 0.07 50.00 1.730219 0.0200
7 0.577946 0.07 100.00 1.730264 0.0100
8 0.143529 0.15 3.33 6.967222 0.3003
9 0.279603 0.15 10.00 3.576497 0.1000
10 0.397574 0.15 50.00 2.515253 0.0200
11 0.377811 0.15 100.00 2.646829 0.0100

Как это можно объяснить?

  1. В обратных координатах концентрации субстрата очень похожи между собой (0.01, 0.02, 0.1 и 0.3) в абсолютных, а скорости отличаются сильнее. По-видимому, из-за этого $MSE$ в обратных координатах большая.
  2. В прямых координатах концентрации субстрата и скорости хорошо разделимы между собой, поэтому предсказания удаются лучше.
  3. Ввиду недостатка данных (4 значения субстрата) предсказание параметров (3 шт) не может быть выполнено хорошо. Видимо, поэтому предсказанные скорости и кинетические параметры (в прямых координатах) похожи между разными типами ингибирования.

Теперь определим средние кинетические параметры для разных типов ингибирования.

In [187]:
inh_coords = list(melted_params['inh_coord'].unique())
inh_types = list(melted_params['inh_type'].unique())
In [191]:
mean_records = list()
mean_keys = ['inh_type', 'inh_coord', 'Km', 'Vm', 'Ki']
for inh_coord, inh_type in product(inh_coords, inh_types):
    mean_data = inh_results.query(f'inh_coord == "{inh_coord}" and inh_type == "{inh_type}"')
    km = mean_data['Km'].mean()
    vm = mean_data['Vm'].mean()
    ki = mean_data.query("i_conc != 0")['Ki'].mean()
    mean_records.append(dict(zip(mean_keys, [inh_type, inh_coord, km, vm, ki])))
In [192]:
mean_df = pd.DataFrame.from_records(mean_records)
In [193]:
mean_df
Out[193]:
Ki Km Vm inh_coord inh_type
0 0.926022 5.913775e+00 0.677346 direct competitive
1 0.644261 6.350449e+00 0.735999 direct noncompetitive
2 0.618761 7.132020e+00 0.738657 direct uncompetitive
3 3950.874382 1.216659e-11 0.393365 reverse competitive
4 0.127079 6.364937e-18 0.556578 reverse noncompetitive
5 0.001356 2.896807e-17 18.181239 reverse uncompetitive
In [194]:
mean_df.query('inh_coord == "direct" and inh_type == "noncompetitive"')
Out[194]:
Ki Km Vm inh_coord inh_type
1 0.644261 6.350449 0.735999 direct noncompetitive

Задание 4. Аппроксимация в координатах Диксона.

Построим зависимость обратной скорости от концентрации ингибитора для каждого значения концентрации субстрата.

In [200]:
for s in s_conc:
    plot_data = speeds.query(f"substrate == {s}")
    plt.plot(plot_data['inhibitor'], plot_data['reverse_speed'], 'o', label=s)
plt.legend(title="Substrate, uM")
plt.xlabel("Inhibitor, uM")
plt.ylabel("1 / V")
plt.show()

Линейно аппроксимируем зависимости.

In [201]:
dickson_approx = list()
for s in s_conc:
    x = speeds.query(f'substrate == {s}')['inhibitor']
    y = speeds.query(f'substrate == {s}')['reverse_speed']
    slope, intercept = np.polyfit(x=x,
                                  y=y,
                                  deg=1)
    dickson_approx.append((slope, intercept))
In [203]:
for i, s in enumerate(s_conc):
    x = speeds.query(f'substrate == {s}')['inhibitor']
    y = speeds.query(f'substrate == {s}')['reverse_speed']
    plt.plot(x, y, 'o', label=s)
    app_x = np.linspace(-0.3, 0.15, 10)
    app_y = app_x * dickson_approx[i][0] + dickson_approx[i][1]
    plt.plot(app_x, app_y, color='red')
plt.legend(title='Substrate, uM')
plt.xlabel('Inhibitor, uM')
plt.ylabel('1 / V')
plt.axhline(0, color='black')
plt.axvline(0, color='black')
plt.show()

Полученные прямые пересекаются на оси абсцисс слева от нуля. Какой это тип ингибирования? Давайте выводить.
В координатах Диксона уравнения для трёх типов ингибирования выглядят следующим образом:

  • Конкурентное: $\frac{1}{V} = \frac{1}{V_m} + \frac{K_M}{V_m \cdot S} + \frac{K_M}{V_m \cdot K_i \cdot S} \cdot I$;
  • Неконкурентное: $\frac{1}{V} = \frac{1}{V_m} + \frac{K_M}{V_m \cdot S} + (\frac{1}{V_m \cdot K_i} + \frac{K_M}{V_m \cdot K_i \cdot S}) \cdot I$;
  • Бесконкурентное: $\frac{1}{V} = \frac{1}{V_m} + \frac{K_M}{V_m \cdot S} + \frac{1}{V_m \cdot K_i} \cdot I$.

Пересечение прямых означает, что $\frac{1}{V}$ не зависит от $S$. Значит, для каждого механизма надо выяснить, при каком значении $I$ $\frac{1}{V}$ не зависит от $S$ и найти это значение $\frac{1}{V}$.
Заметим, что произвольная функция $f(x_1, x_2, ...)$ не зависит от $x_i$ тогда и только тогда, когда $\frac{\partial f}{\partial x_i} = 0$. Таким образом, нам надо взять производные $\frac{\partial}{\partial S}(\frac{1}{V})$ для каждого механизма, приравнять к $0$ и найти искомую точку.

Сделаем это.

  • Конкурентное: $\frac{\partial}{\partial S}(\frac{1}{V}) = -\frac{K_M}{V_M \cdot S^2} - \frac{I \cdot K_M}{V_m \cdot K_i \cdot S^2} = 0$, откуда следует, что $I = -K_i$ и $\frac{1}{V} = \frac{1}{V_m}$, то есть точка пересечения во II квадранте;
  • Неконкурентное: $\frac{\partial}{\partial S}(\frac{1}{V}) = -\frac{K_M}{V_M \cdot S^2} - \frac{I \cdot K_M}{V_m \cdot K_i \cdot S^2} = 0$, откуда следует, что $I = -K_i$ и $\frac{1}{V} = 0$, то есть точка пересечения на оси абсцисс левее нуля;
  • Бесконкурентное: $\frac{\partial}{\partial S}(\frac{1}{V}) = \frac{K_M}{V_m} \neq 0$, то есть точки пересечения нет, а значит, прямые параллельны.

В наших данных мы видим, что у нас неконкурентное ингибирование (согласуется с предыдущими данными).

Теперь определим кинетические параметры в координатах Диксона для трёх механизмов и узнаем точность определения (всё как в задании 3).

In [224]:
def competitive_dickson(x, vm, km, ki):
    i, s = x
    return 1 / vm + km / vm / s + km * i / vm / ki / s

def noncompetitive_dickson(x, vm, km, ki):
    i, s = x
    return 1 / vm + km / vm / s + (1 / vm / ki + km / vm / ki / s) * i

def uncompetitive_dickson(x, vm, km, ki):
    i, s = x
    return 1 / vm + km / vm / s + i / vm / ki
In [225]:
dickson_funcs = {func.__name__: func
                 for func in [competitive_dickson,
                              noncompetitive_dickson,
                              uncompetitive_dickson]}
In [226]:
dickson_records = list()
dickson_keys = ['Vm', 'Km', 'Ki', 'inh_type', 'inh_coord', 's_conc']
for func_name, func in dickson_funcs.items():
    inh_type, inh_coord = func_name.split("_")
    for s in s_conc:
        approx_data = speeds.query(f'substrate == {s}')
        x = approx_data['inhibitor']
        y = approx_data['reverse_speed']
        popt, pcov = curve_fit(func, (x, s), y, bounds=(0, 'inf'))
        dickson_records.append(dict(zip(dickson_keys, list(popt) + [inh_type, inh_coord, s])))
/home/dmitry/miniconda3/lib/python3.7/site-packages/scipy/optimize/minpack.py:787: OptimizeWarning: Covariance of the parameters could not be estimated
  category=OptimizeWarning)
/home/dmitry/miniconda3/lib/python3.7/site-packages/scipy/optimize/minpack.py:787: OptimizeWarning: Covariance of the parameters could not be estimated
  category=OptimizeWarning)
/home/dmitry/miniconda3/lib/python3.7/site-packages/scipy/optimize/minpack.py:787: OptimizeWarning: Covariance of the parameters could not be estimated
  category=OptimizeWarning)
/home/dmitry/miniconda3/lib/python3.7/site-packages/scipy/optimize/minpack.py:787: OptimizeWarning: Covariance of the parameters could not be estimated
  category=OptimizeWarning)
/home/dmitry/miniconda3/lib/python3.7/site-packages/scipy/optimize/minpack.py:787: OptimizeWarning: Covariance of the parameters could not be estimated
  category=OptimizeWarning)
/home/dmitry/miniconda3/lib/python3.7/site-packages/scipy/optimize/minpack.py:787: OptimizeWarning: Covariance of the parameters could not be estimated
  category=OptimizeWarning)
/home/dmitry/miniconda3/lib/python3.7/site-packages/scipy/optimize/minpack.py:787: OptimizeWarning: Covariance of the parameters could not be estimated
  category=OptimizeWarning)
/home/dmitry/miniconda3/lib/python3.7/site-packages/scipy/optimize/minpack.py:787: OptimizeWarning: Covariance of the parameters could not be estimated
  category=OptimizeWarning)
/home/dmitry/miniconda3/lib/python3.7/site-packages/scipy/optimize/minpack.py:787: OptimizeWarning: Covariance of the parameters could not be estimated
  category=OptimizeWarning)
/home/dmitry/miniconda3/lib/python3.7/site-packages/scipy/optimize/minpack.py:787: OptimizeWarning: Covariance of the parameters could not be estimated
  category=OptimizeWarning)
/home/dmitry/miniconda3/lib/python3.7/site-packages/scipy/optimize/minpack.py:787: OptimizeWarning: Covariance of the parameters could not be estimated
  category=OptimizeWarning)
/home/dmitry/miniconda3/lib/python3.7/site-packages/scipy/optimize/minpack.py:787: OptimizeWarning: Covariance of the parameters could not be estimated
  category=OptimizeWarning)
In [228]:
dickson_df = pd.DataFrame.from_records(dickson_records)
In [410]:
dickson_df
Out[410]:
Ki Km Vm inh_coord inh_type s_conc
0 0.066788 3.970749 0.669269 dickson competitive 3.33
1 0.048598 6.263532 0.991261 dickson competitive 10.00
2 0.014000 6.059366 0.970775 dickson competitive 50.00
3 0.005713 6.044507 1.009710 dickson competitive 100.00
4 0.122799 2.821290 0.563897 dickson noncompetitive 3.33
5 0.126187 2.586107 0.767122 dickson noncompetitive 10.00
6 0.129526 2.984256 0.917524 dickson noncompetitive 50.00
7 0.100237 2.871291 0.979496 dickson noncompetitive 100.00
8 0.075911 2.056844 0.493819 dickson uncompetitive 3.33
9 0.113431 1.124529 0.678039 dickson uncompetitive 10.00
10 0.123252 2.545440 0.909925 dickson uncompetitive 50.00
11 0.097747 2.546730 0.976405 dickson uncompetitive 100.00
In [411]:
dickson_df.query('inh_type == "noncompetitive"')
Out[411]:
Ki Km Vm inh_coord inh_type s_conc
4 0.122799 2.821290 0.563897 dickson noncompetitive 3.33
5 0.126187 2.586107 0.767122 dickson noncompetitive 10.00
6 0.129526 2.984256 0.917524 dickson noncompetitive 50.00
7 0.100237 2.871291 0.979496 dickson noncompetitive 100.00
In [232]:
melted_dickson = pd.melt(dickson_df,
                         id_vars=['s_conc', 'inh_coord', 'inh_type'],
                         value_vars=['Ki', 'Km', 'Vm'],
                         var_name='parameter',
                         value_name='value')
In [233]:
plt.figure(figsize=(15, 5))
for i, item in enumerate(inh_types):
    plt.subplot(1, 3, i + 1)
    plot_data = melted_dickson.query(f'inh_type == "{item}"')
    for s in s_conc:
        plot_plot_data = plot_data.query(f"s_conc == {s}")
        plt.plot(plot_plot_data['parameter'], plot_plot_data['value'], 'o', label=s)
    plt.legend(title='Substrate, uM')
    plt.title(f"{item} inhibition in Dickson coordinates")

Наименьший разброс по величинам параметров наблюдается у неконкурентного ингибирования.

Проверим точность предсказаний по полученным параметрам.

In [235]:
mse_records = list()
mse_keys = ['inh_type', 'inh_coord', 's_conc', 'mse']
for func_name, func in dickson_funcs.items():
    inh_type, inh_coord = func_name.split("_")
    for s in s_conc:
        approx_data = speeds.query(f'substrate == {s}')
        x = approx_data['inhibitor']
        y = approx_data['reverse_speed']
        params = dickson_df.query(f'inh_type == "{inh_type}" and s_conc == {s}')
        ki, km, vm = params['Ki'].item(), params['Km'].item(), params['Vm'].item()
        y_pred = func((x, s), vm, km, ki)
        error = mse(np.array(y), np.array(y_pred))
        mse_records.append(dict(zip(mse_keys, [inh_type, inh_coord, s, error])))
In [237]:
mse_dickson = pd.DataFrame.from_records(mse_records)
In [238]:
mse_dickson
Out[238]:
inh_coord inh_type mse s_conc
0 dickson competitive 0.221098 3.33
1 dickson competitive 0.000484 10.00
2 dickson competitive 0.001197 50.00
3 dickson competitive 0.001429 100.00
4 dickson noncompetitive 0.221098 3.33
5 dickson noncompetitive 0.000484 10.00
6 dickson noncompetitive 0.001197 50.00
7 dickson noncompetitive 0.001429 100.00
8 dickson uncompetitive 0.221098 3.33
9 dickson uncompetitive 0.000484 10.00
10 dickson uncompetitive 0.001197 50.00
11 dickson uncompetitive 0.001429 100.00
In [240]:
plt.figure(figsize=(15, 5))
for i, item in enumerate(inh_types):
    plt.subplot(1, 3, i + 1)
    plot_data = mse_dickson.query(f'inh_type == "{item}"')
    plt.plot(plot_data['s_conc'], plot_data['mse'], 'o')
    plt.title(f"{item} inhibition in Dickson coordinates")
    plt.xlabel("Substrate, uM")
    plt.ylabel("MSE")

Для всех механизмов есть небольшая ошибка при малой концентрации субстрата, а так она очень мала. По ошибке нельзя ничего сказать о том, какой механизм лучше.

Посчитаем средние значения параметров.

In [241]:
mean_records = list()
mean_keys = ['inh_type', 'inh_coord', 'Km', 'Vm', 'Ki']
for inh_type in inh_types:
    mean_data = dickson_df.query(f'inh_type == "{inh_type}"')
    km = mean_data['Km'].mean()
    vm = mean_data['Vm'].mean()
    ki = mean_data['Ki'].mean()
    mean_records.append(dict(zip(mean_keys, [inh_type, inh_coord, km, vm, ki])))
In [242]:
mean_dickson = pd.DataFrame.from_records(mean_records)
In [243]:
mean_dickson
Out[243]:
Ki Km Vm inh_coord inh_type
0 0.033775 5.584539 0.910254 dickson competitive
1 0.119687 2.815736 0.807010 dickson noncompetitive
2 0.102585 2.068386 0.764547 dickson uncompetitive
In [258]:
mean_dickson.query('inh_type == "noncompetitive"')
Out[258]:
Ki Km Vm inh_coord inh_type
1 0.119687 2.815736 0.80701 dickson noncompetitive

Сравним полученные средние значения параметров с тем, что получили в прямых и двойных обратных координатах.

In [248]:
all_means = pd.concat([mean_dickson, mean_df], axis=0)
In [249]:
all_means
Out[249]:
Ki Km Vm inh_coord inh_type
0 0.033775 5.584539e+00 0.910254 dickson competitive
1 0.119687 2.815736e+00 0.807010 dickson noncompetitive
2 0.102585 2.068386e+00 0.764547 dickson uncompetitive
0 0.926022 5.913775e+00 0.677346 direct competitive
1 0.644261 6.350449e+00 0.735999 direct noncompetitive
2 0.618761 7.132020e+00 0.738657 direct uncompetitive
3 3950.874382 1.216659e-11 0.393365 reverse competitive
4 0.127079 6.364937e-18 0.556578 reverse noncompetitive
5 0.001356 2.896807e-17 18.181239 reverse uncompetitive
In [251]:
melted_means = pd.melt(all_means, id_vars = ['inh_coord', 'inh_type'], value_vars=['Ki', 'Km', 'Vm'], var_name='parameter')
In [257]:
plt.figure(figsize=(15, 5))
for i, inh_coord in enumerate(['dickson', 'direct', 'reverse']):
    plt.subplot(1, 3, i + 1)
    sns.scatterplot(data=melted_means.query(f'inh_coord == "{inh_coord}"'), x='parameter', y='value', hue='inh_type')
    plt.ylim(0, 8)
    plt.title(inh_coord)
plt.tight_layout()

Кинетические параметры более-менее согласуются для трёх механизмов в координатах Диксона и прямых координатах. При этом, наибольшие проблемы вызвала $K_M$ — то у неё большой разброс, то экстремально маленькие значения (в обратных координатах).

Из полученных результатов нельзя сказать, какие координаты лучше использовать — Диксона или прямые. В жизни это зависит от задачи: если у нас много разных концентраций субстрата, то логично использовать прямые координаты, а если много разных концентраций ингибитора, то Диксона.

Задание 5. Совместная аппроксимация.

Будем аппроксимировать скорости реакций в прямых координатах и координатах Диксона сразу по всем концентрациям субстрата и ингибитора.

In [275]:
joint_funcs = {func.__name__: func
               for func in [competitive_direct,
                            noncompetitive_direct,
                            uncompetitive_direct,
                            competitive_dickson,
                            noncompetitive_dickson,
                            uncompetitive_dickson]}
In [276]:
joint_records = list()
joint_keys = ['Vm', 'Km', 'Ki', 'inh_type', 'inh_coord']
for func_name, func in joint_funcs.items():
    inh_type, inh_coord = func_name.split("_")
    x_s = speeds['substrate']
    i_s = speeds['inhibitor']
    if inh_coord == 'direct':
        y = speeds['speed']
        x = [x_s, i_s]
    elif inh_coord == 'dickson':
        y = speeds['reverse_speed']
        x = [i_s, x_s]
    popt, pcov = curve_fit(func, x, y, bounds=(0, 'inf'))
    joint_records.append(dict(zip(joint_keys, list(popt) + [inh_type, inh_coord])))
In [278]:
joint_df = pd.DataFrame.from_records(joint_records)
In [279]:
joint_df
Out[279]:
Ki Km Vm inh_coord inh_type
0 0.012652 4.197519 0.866809 direct competitive
1 0.119820 6.287788 0.978236 direct noncompetitive
2 0.098935 7.888290 1.008128 direct uncompetitive
3 0.052623 3.189594 0.636047 dickson competitive
4 0.121981 7.587058 1.014909 dickson noncompetitive
5 0.033126 24.535560 2.044122 dickson uncompetitive
In [280]:
melted_joint = pd.melt(joint_df, id_vars=['inh_coord', 'inh_type'], value_vars=['Ki', 'Km', 'Vm'], var_name='parameter')

Посмотрим на то, как разные механизмы дают параметры в одних и тех же координатах.

In [284]:
plt.figure(figsize=(10, 5))
for i, item in enumerate(['dickson', 'direct']):
    plt.subplot(1, 2, i + 1)
    plot_data = melted_joint.query(f'inh_coord == "{item}"')
    sns.scatterplot(data=plot_data, x='parameter', y='value', hue='inh_type')
    plt.title(f"Inhibition in {item} coordinates")

Как и в других наблюдениях, $K_i$ и $V_m$ похожи, а $K_M$ различаются, притом весьма сильно, как между координатами, так и внутри них.

Посмотрим на то, как один и тот же механизм работает в разных координатах.

In [290]:
plt.figure(figsize=(15, 5))
for i, item in enumerate(inh_types):
    plt.subplot(1, 3, i + 1)
    plot_data = melted_joint.query(f'inh_type == "{item}"')
    sns.scatterplot(data=plot_data, x='parameter', y='value', hue='inh_coord')
    plt.title(f"{item} inhibiton")

Как и в других наблюдениях, $K_i$ и $V_m$ похожи, а $K_M$ различаются для одного и того же механизма в разных координатах. При этом, для неконкурентного ингибирования различие в $K_i$ и $V_m$ минимально.

In [298]:
print("MSE\ttype")
for item in inh_types:
    compare_data = joint_df.query(f'inh_type == "{item}"')
    direct_data = compare_data.query(f'inh_coord == "direct"').loc[:, ['Ki', 'Km', 'Vm']].T
    dickson_data = compare_data.query(f'inh_coord == "dickson"').loc[:, ['Ki', 'Km', 'Vm']].T
    error = mse(np.array(direct_data), np.array(dickson_data))
    print(f"{error:.4f}\t{item}")
MSE	type
0.3569	competitive
0.5632	noncompetitive
92.7364	uncompetitive

Оказывается, что у конкурентного ингибирования $MSE$ различий между координатами меньше, чем у неконкурентного. Это достигнуто меньшей разницей в значениях $K_M$. И вновь вывод в том, что величина $MSE$ не может быть критерием отбора механизмов.

In [301]:
plt.figure(figsize=(15, 10))
for i, item in enumerate(inh_types):
    plt.subplot(2, 3, i + 1)
    plot_data = melted_joint.query(f'inh_type == "{item}"')
    sns.scatterplot(data=plot_data, x='parameter', y='value', hue='inh_coord')
    plt.title(f"{item} inhibiton by joint approximation")
for i, item in enumerate(inh_types):
    plt.subplot(2, 3, i + 4)
    plot_data = melted_means.query(f'inh_type == "{item}"')
    sns.scatterplot(data=plot_data, x='parameter', y='value', hue='inh_coord')
    plt.ylim(0, 20)
    plt.title(f"{item} inhibiton by mean data")

Параметры, полученные для одного и того же механизма разными способами, сходятся между собой в разных координатах.

Давайте порисуем в 3D.

In [302]:
from mpl_toolkits.mplot3d import Axes3D

Нарисуем проволочную поверхность того, как предсказывают поведение системы разные типы ингибирования в прямых координатах и координатах Диксона.

In [393]:
fig = plt.figure(figsize=(15, 10))
for i, (func_name, func) in enumerate(joint_funcs.items()):
    inh_type, inh_coord = func_name.split("_")
    substrate = s_conc
    inhibitor = i_conc
    xs, ys = np.meshgrid(substrate, inhibitor)
    ki, km, vm, _, _ = joint_df.query(f'inh_coord == "{inh_coord}" and inh_type == "{inh_type}"').values.squeeze()
    if inh_coord == 'direct':
        zs = func((xs, ys), vm, km, ki)
    elif inh_coord == 'dickson':
        zs = func((ys, xs), vm, km, ki)
    ax = fig.add_subplot(2, 3, i+1, projection='3d')
    ax.plot_wireframe(xs, ys, zs)
    ax.scatter(speeds['substrate'],
               speeds['inhibitor'],
               speeds['speed'] if inh_coord == 'direct' else speeds['reverse_speed'],
               color='red')
    plt.title(f"{inh_coord} {inh_type}")
    ax.set_xlabel("substrate, uM")
    ax.set_ylabel("inhibitor, uM")
    ax.set_zlabel("V" if inh_coord == 'direct' else "1 / V")
plt.tight_layout()

Неконкурентное ингибирование наилучшим образом описывает поведение системы в данных координатах.

Посмотрим на предсказываемое состояние системы в других точках.

In [401]:
fig = plt.figure(figsize=(15, 10))
for i, (func_name, func) in enumerate(joint_funcs.items()):
    inh_type, inh_coord = func_name.split("_")
    substrate = np.logspace(0, 2, 20)
    inhibitor = np.linspace(0, 0.2, 20)
    xs, ys = np.meshgrid(substrate, inhibitor)
    ki, km, vm, _, _ = joint_df.query(f'inh_coord == "{inh_coord}" and inh_type == "{inh_type}"').values.squeeze()
    if inh_coord == 'direct':
        zs = func((xs, ys), vm, km, ki)
    elif inh_coord == 'dickson':
        zs = func((ys, xs), vm, km, ki)
    ax = fig.add_subplot(2, 3, i+1, projection='3d')
    ax.plot_surface(xs, ys, zs, color='#1F77B4DD')
    ax.scatter(speeds['substrate'],
               speeds['inhibitor'],
               speeds['speed'] if inh_coord == 'direct' else speeds['reverse_speed'],
               color='red')
    plt.title(f"{inh_coord} {inh_type}")
    ax.set_xlabel("substrate, uM")
    ax.set_ylabel("inhibitor, uM")
    ax.set_zlabel("V" if inh_coord == 'direct' else "1 / V")
plt.tight_layout()

Зачем? Просто красиво. Можно увидеть, что неконкурентное и бесконкурентное ингибирования очень аккуратно описывают систему в прямых координатах (но неконкуретное аккуратнее). В координатах Диксона лучше себя ведёт неконкурентное, правильнее описывая всю систему в целом и поведение около нулевых концентраций в частности.

Задание 6. Рассчёт общего случая.

Рассчитаем параметры системы в общем случае.

In [412]:
def general_direct(x, vm, km, ki, a, b):
    s, i = x
    return vm * s * (1 + b * i / a / ki) / (km * (1 + i / ki) + s * (1 + i / a / ki))
In [415]:
popt, pcov = curve_fit(general_direct,
                       [speeds['substrate'],
                        speeds['inhibitor']],
                       speeds['speed'],
                       bounds=(0, 'inf'))
vm, km, ki, a, b = list(popt.squeeze())
In [417]:
print("Vm\tKm\tKi\ta\tb")
print(f"{vm:.4f}\t{km:.4f}\t{ki:.4f}\t{a:.4f}\t{b}")
Vm	Km	Ki	a	b
0.9729	6.0815	0.1023	1.2099	2.379811501206359e-14
In [423]:
fig = plt.figure(figsize=(5, 5))
substrate = s_conc
inhibitor = i_conc
xs, ys = np.meshgrid(substrate, inhibitor)
zs = general_direct((xs, ys), vm, km, ki, a, b)
ax = fig.add_subplot(111, projection='3d')
ax.plot_wireframe(xs, ys, zs, color='#1F77B4DD')
ax.scatter(speeds['substrate'],
           speeds['inhibitor'],
           speeds['speed'],
           color='red')
plt.title("General inhibiton")
ax.set_xlabel("Substrate, uM")
ax.set_ylabel("Inhibitor, uM")
ax.set_zlabel("V, uM/s")
plt.tight_layout()

Нарисовало практически идеально.

In [422]:
fig = plt.figure(figsize=(5, 5))
substrate = np.logspace(0, 2, 20)
inhibitor = np.linspace(0, 0.2, 20)
xs, ys = np.meshgrid(substrate, inhibitor)
zs = general_direct((xs, ys), vm, km, ki, a, b)
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(xs, ys, zs, color='#1F77B4DD')
ax.scatter(speeds['substrate'],
           speeds['inhibitor'],
           speeds['speed'],
           color='red')
plt.title("General inhibiton")
ax.set_xlabel("Substrate, uM")
ax.set_ylabel("Inhibitor, uM")
ax.set_zlabel("V, uM/s")
plt.tight_layout()

И весьма красиво :)

Посмотрим, какие скорости выдаст $K_M$ как концентрация субстрата.

In [425]:
half_v = general_direct((km, np.array(i_conc)), vm, km, ki, a, b)
In [442]:
half_v
Out[442]:
array([0.48645893, 0.29943261, 0.20802768])
In [445]:
sns.pointplot(data=speeds, x='substrate', y='speed', hue='inhibitor')
plt.axvline(km / 10, color='red')  # не знаю, почему такая проблема.
plt.axhline(half_v[0], color='navy')
plt.axhline(half_v[1], color='orange')
plt.axhline(half_v[2], color='green')
plt.ylim(0, 1)
plt.show()

Всё сходится со здравым смыслом.