# Семинар 5

```python
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split

%matplotlib inline
```

```python
plt.rcParams['figure.figsize'] = (15, 7)
```

## Линейная классификация

### Постановка задачи классификации

Пусть задана обучающая выборка $X = \left\{ \left( x_i, y_i \right) \right\}_{i=1}^l, x_i \in \mathbb{X}, y_i \in \mathbb{Y},$ — $l$ пар объект-ответ, где
$\mathbb{X}$ — пространство объектов,
$\mathbb{Y}$ — пространство ответов.

### Логистическая регрессия

Рассмотрим в качестве верхней оценки пороговой функции потерь логистическую функцию:

$$\tilde{L}(M) = \log (1 + \exp(-M)).$$

Таким образом, необходимо решить следующую оптимизационную задачу:
$$\frac{1}{l} \sum_{i=1}^l \tilde{L} (M_i) = \frac{1}{l} \sum_{i=1}^l \log (1 + \exp (-y_i \langle w, x_i \rangle)) \to \min_w$$

Получившийся метод обучения называется **логистической регрессией**.

Одно из полезных свойств логистической регрессии, которое будет изучено нами несколько позднее, — тот факт, что она позволяет предсказывать помимо метки класса ещё и вероятность принадлежности каждому из них, что может быть полезным в некоторых задачах.

**Пример**: Вы работаете в банке и хотите выдавать кредиты только тем клиентам, которые вернут его с вероятностью не меньше 0.9.

Попробуем сконструировать функцию потерь из других соображений.
Если алгоритм $b(x) \in [0, 1]$ действительно выдает вероятности, то
они должны согласовываться с выборкой.
С точки зрения алгоритма вероятность того, что в выборке встретится объект $x_i$ с классом $y_i$,
равна $b(x_i)^{[y_i = +1]} (1 - b(x_i))^{[y_i = -1]}$.

Исходя из этого, можно записать правдоподобие выборки (т.е. вероятность получить такую выборку
с точки зрения алгоритма):
$$
    Q(a, X)
    =
    \prod_{i = 1}^{\ell}
        b(x_i)^{[y_i = +1]} (1 - b(x_i))^{[y_i = -1]}.
$$
Данное правдоподобие можно использовать как функционал для обучения алгоритма --
с той лишь оговоркой, что удобнее оптимизировать его логарифм:
$$
    -\sum_{i = 1}^{\ell} \left(
        [y_i = +1] \log b(x_i)
        +
        [y_i = -1] \log (1 - b(x_i))
    \right)
    \to
    \min
$$
Данная функция потерь называется логарифмической (log-loss).

Мы хотим предсказывать вероятности, то есть, чтобы наш алгоритм предсказывал числа в интервале [0, 1]. Этого легко достичь, если положить $b(x) = \sigma(\langle w, x \rangle)$,
где в качестве $\sigma$ может выступать любая монотонно неубывающая функция
с областью значений $[0, 1]$.
Мы будем использовать сигмоидную функцию: $\sigma(z) = \frac{1}{1 + \exp(-z)}$.
Таким образом, чем больше скалярное произведение $\langle w, x \rangle$,
тем больше будет предсказанная вероятность.

Подставим трансформированный ответ линейной модели в логарифмическую функцию потерь:
\begin{align*}
    -\sum_{i = 1}^{\ell} &\left(
        [y_i = +1]
        \log \frac{1}{1 + \exp(-\langle w, x_i \rangle)}
        +
        [y_i = -1]
        \log \frac{\exp(-\langle w, x_i \rangle)}{1 + \exp(-\langle w, x_i \rangle)}
    \right)
    =\\
    &=
    -\sum_{i = 1}^{\ell} \left(
        [y_i = +1]
        \log \frac{1}{1 + \exp(-\langle w, x_i \rangle)}
        +
        [y_i = -1]
        \log \frac{1}{1 + \exp(\langle w, x_i \rangle)}
    \right)
    =\\
    &=
    \sum_{i = 1}^{\ell} \left(
        [y_i = +1]
        \log (1 + \exp(-\langle w, x_i \rangle))
        +
        [y_i = -1]
        \log (1 + \exp(\langle w, x_i \rangle))
    \right)
    =\\
    &=
    \sum_{i = 1}^{\ell}
        \log \left(
            1 + \exp(-y_i \langle w, x_i \rangle)
        \right).
\end{align*}

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

### Пример обучения логистической регрессии
#### Определение спама по тексту электронного письма

Попробуем при помощи моделей линейной классификации построить алгоритм, отделяющий спам от нормальной почты. Для экспериментов воспользуемся небольшим набором данных с [UCI](https://archive.ics.uci.edu/ml/datasets.html). Объекты в датасете соответствуют письмам, которые описаны признаками на основе текста письма, спам — положительный пример для классификации, хорошее письмо — отрицательный пример.

```python
spam_data = pd.read_csv('spam_data.csv')
spam_data.columns
```

    Index(['word_freq_make', 'word_freq_address', 'word_freq_all', 'word_freq_3d',
           'word_freq_our', 'word_freq_over', 'word_freq_remove',
           'word_freq_internet', 'word_freq_order', 'word_freq_mail',
           'word_freq_receive', 'word_freq_will', 'word_freq_people',
           'word_freq_report', 'word_freq_addresses', 'word_freq_free',
           'word_freq_business', 'word_freq_email', 'word_freq_you',
           'word_freq_credit', 'word_freq_your', 'word_freq_font', 'word_freq_000',
           'word_freq_money', 'word_freq_hp', 'word_freq_hpl', 'word_freq_george',
           'word_freq_650', 'word_freq_lab', 'word_freq_labs', 'word_freq_telnet',
           'word_freq_857', 'word_freq_data', 'word_freq_415', 'word_freq_85',
           'word_freq_technology', 'word_freq_1999', 'word_freq_parts',
           'word_freq_pm', 'word_freq_direct', 'word_freq_cs', 'word_freq_meeting',
           'word_freq_original', 'word_freq_project', 'word_freq_re',
           'word_freq_edu', 'word_freq_table', 'word_freq_conference',
           'char_freq_;', 'char_freq_(', 'char_freq_[', 'char_freq_!',
           'char_freq_$', 'char_freq_#', 'capital_run_length_average',
           'capital_run_length_longest', 'capital_run_length_total', 'spam'],
          dtype='object')

```python
X, y = spam_data.iloc[:, :-1].values, spam_data.iloc[:, -1].values
X.shape, y.shape
```

    ((4601, 57), (4601,))

```python
spam_data['spam'].value_counts()
```

    0    2788
    1    1813
    Name: spam, dtype: int64

### Обучение логистической регрессии

Разделим выборку на обучающую и тестовую в отношении 80/20 и обучим логистическую регрессию при помощи объекта [LogisticRegression](http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html).

```python
from sklearn.linear_model import LogisticRegression

X_train = X[:int(len(X) * 0.8)]
y_train = y[:int(len(X) * 0.8)]
X_test = X[int(len(X) * 0.8):]
y_test = y[int(len(X) * 0.8):]
```

```python
??LogisticRegression
```

```python
lr = LogisticRegression(max_iter=3000, solver='lbfgs', random_state=13)
lr.fit(X_train, y_train)
```

    LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
                       intercept_scaling=1, l1_ratio=None, max_iter=3000,
                       multi_class='auto', n_jobs=None, penalty='l2',
                       random_state=13, solver='lbfgs', tol=0.0001, verbose=0,
                       warm_start=False)

```python
def pred_with_th(X, model, th=0.5):
  th = min(max(0, th),1)
  probs = model.predict_proba(X)
  labels = np.zeros(X.shape[0])
  labels = probs > th
  return labels
```

Вычислим долю правильных ответов при помощи соответствующей функции из модуля [sklearn.metrics](http://scikit-learn.org/stable/modules/classes.html#module-sklearn.metrics).

```python
from sklearn.metrics import accuracy_score
print(accuracy_score(y_train, lr.predict(X_train)))
print(accuracy_score(y_test, lr.predict(X_test)))
```

    0.93125
    0.7958740499457112

В чем проблема?

```python
X_train = X[:int(len(X) * 0.8)]
y_train = y[:int(len(X) * 0.8)]
X_test = X[int(len(X) * 0.8):]
y_test = y[int(len(X) * 0.8):]
```

```python
y_train.mean(), y_test.mean()
```

    (0.24239130434782608, 1.0)

```python
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=13)
y_train.mean(), y_test.mean()
```

    (0.39565217391304347, 0.38762214983713356)

```python
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=13)
y_train.mean(), y_test.mean()
```

    (0.39402173913043476, 0.3941368078175896)

```python
# training
lr = LogisticRegression(max_iter=3000, solver='lbfgs')
lr.fit(X_train, y_train)
```

    LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
                       intercept_scaling=1, l1_ratio=None, max_iter=3000,
                       multi_class='auto', n_jobs=None, penalty='l2',
                       random_state=None, solver='lbfgs', tol=0.0001, verbose=0,
                       warm_start=False)

```python
print(accuracy_score(y_train, lr.predict(X_train)))
print(accuracy_score(y_test, lr.predict(X_test)))
```

    0.9375
    0.9120521172638436

```python
lr.predict_proba(X_train).sum(axis=1)
```

    array([1., 1., 1., ..., 1., 1., 1.])

Теперь будем смотреть на AUC-ROC:

```python
from sklearn.metrics import roc_auc_score
print(roc_auc_score(y_train, lr.predict_proba(X_train)[:, 1]))
print(roc_auc_score(y_test, lr.predict_proba(X_test)[:, 1]))
```

    0.9792709138704191
    0.959082516267267

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

```python
np.logspace(-5, 1, 5)
```

    array([1.00000000e-05, 3.16227766e-04, 1.00000000e-02, 3.16227766e-01,
           1.00000000e+01])

```python
np.linspace(10**(-5), 10, 5)
```

    array([1.0000000e-05, 2.5000075e+00, 5.0000050e+00, 7.5000025e+00,
           1.0000000e+01])

То, какая метрика будет использоваться, определяется параметром `'scoring'`.

```python
from sklearn.metrics import SCORERS
sorted(SCORERS.keys())
```

    ['accuracy',
     'adjusted_mutual_info_score',
     'adjusted_rand_score',
     'average_precision',
     'balanced_accuracy',
     'completeness_score',
     'explained_variance',
     'f1',
     'f1_macro',
     'f1_micro',
     'f1_samples',
     'f1_weighted',
     'fowlkes_mallows_score',
     'homogeneity_score',
     'jaccard',
     'jaccard_macro',
     'jaccard_micro',
     'jaccard_samples',
     'jaccard_weighted',
     'max_error',
     'mutual_info_score',
     'neg_brier_score',
     'neg_log_loss',
     'neg_mean_absolute_error',
     'neg_mean_gamma_deviance',
     'neg_mean_poisson_deviance',
     'neg_mean_squared_error',
     'neg_mean_squared_log_error',
     'neg_median_absolute_error',
     'neg_root_mean_squared_error',
     'normalized_mutual_info_score',
     'precision',
     'precision_macro',
     'precision_micro',
     'precision_samples',
     'precision_weighted',
     'r2',
     'recall',
     'recall_macro',
     'recall_micro',
     'recall_samples',
     'recall_weighted',
     'roc_auc',
     'roc_auc_ovo',
     'roc_auc_ovo_weighted',
     'roc_auc_ovr',
     'roc_auc_ovr_weighted',
     'v_measure_score']

```python
from sklearn.model_selection import GridSearchCV
```

```python
GridSearchCV()
```

```python
from sklearn.model_selection import GridSearchCV
grid_searcher = GridSearchCV(
    LogisticRegression(max_iter=3000, solver='liblinear', random_state=13),
    param_grid={
        'C': np.logspace(-5, 1, 6),
        'penalty': ['l1', 'l2']
    },
    cv=5,
    scoring='roc_auc',
    n_jobs=-1,
    verbose=5
)
```

```python
np.logspace(-5, 1, 6).shape
```

    (6,)

Параметр `cv=5` говорит, что во время поиска оптимальных параметров будет использоваться кросс-валидация с 5 фолдами. Давайте вспомним, что это такое:

*Source: https://scikit-learn.org/stable/modules/cross_validation.html*

В нашем случае, выборка будет разделена на 5 частей, и на каждой из 5 итераций часть данных будет становиться тестовой выборкой, а другая часть - обучающей. Посчитав метрики на каждой итерации, мы сможем усреднить их в конце и получить достаточно точную оценку качества нашего алгоритма.

```python
%%time

grid_searcher.fit(X_train, y_train);
```

    Fitting 5 folds for each of 12 candidates, totalling 60 fits

    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 2 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  14 tasks      | elapsed:    1.2s

    CPU times: user 225 ms, sys: 48.2 ms, total: 274 ms
    Wall time: 2.62 s

    [Parallel(n_jobs=-1)]: Done  60 out of  60 | elapsed:    2.5s finished

    GridSearchCV(cv=5, error_score=nan,
                 estimator=LogisticRegression(C=1.0, class_weight=None, dual=False,
                                              fit_intercept=True,
                                              intercept_scaling=1, l1_ratio=None,
                                              max_iter=3000, multi_class='auto',
                                              n_jobs=None, penalty='l2',
                                              random_state=13, solver='liblinear',
                                              tol=0.0001, verbose=0,
                                              warm_start=False),
                 iid='deprecated', n_jobs=-1,
                 param_grid={'C': array([1.00000000e-05, 1.58489319e-04, 2.51188643e-03, 3.98107171e-02,
           6.30957344e-01, 1.00000000e+01]),
                             'penalty': ['l1', 'l2']},
                 pre_dispatch='2*n_jobs', refit=True, return_train_score=False,
                 scoring='roc_auc', verbose=5)

```python
best_model = grid_searcher.best_estimator_
```

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

```python
print(roc_auc_score(y_train, grid_searcher.predict_proba(X_train)[:, 1]))
print(roc_auc_score(y_test, grid_searcher.predict_proba(X_test)[:, 1]))
```

    0.9797957321787536
    0.9601686463856551

Полные результаты поиска гиперпараметров:

```python
pd.DataFrame(grid_searcher.cv_results_)
```

      mean_fit_time
      std_fit_time
      mean_score_time
      std_score_time
      param_C
      param_penalty
      params
      split0_test_score
      split1_test_score
      split2_test_score
      split3_test_score
      split4_test_score
      mean_test_score
      std_test_score
      rank_test_score

      0
      0.009100
      0.001268
      0.003039
      0.000044
      1e-05
      l1
      {'C': 1e-05, 'penalty': 'l1'}
      0.747449
      0.766457
      0.788754
      0.773601
      0.730134
      0.761279
      0.020469
      12

      1
      0.016491
      0.002699
      0.004468
      0.003114
      1e-05
      l2
      {'C': 1e-05, 'penalty': 'l2'}
      0.853920
      0.845400
      0.847766
      0.855969
      0.819283
      0.844467
      0.013173
      10

      2
      0.011038
      0.000997
      0.003154
      0.000277
      0.000158489
      l1
      {'C': 0.00015848931924611142, 'penalty': 'l1'}
      0.807623
      0.800166
      0.822251
      0.810109
      0.783354
      0.804701
      0.012822
      11

      3
      0.024759
      0.003065
      0.002863
      0.000131
      0.000158489
      l2
      {'C': 0.00015848931924611142, 'penalty': 'l2'}
      0.910809
      0.910020
      0.920782
      0.923968
      0.894580
      0.912032
      0.010287
      8

      4
      0.015270
      0.005803
      0.002957
      0.000080
      0.00251189
      l1
      {'C': 0.0025118864315095794, 'penalty': 'l1'}
      0.895183
      0.879488
      0.895775
      0.898048
      0.873110
      0.888321
      0.010066
      9

      5
      0.050225
      0.021429
      0.003762
      0.001585
      0.00251189
      l2
      {'C': 0.0025118864315095794, 'penalty': 'l2'}
      0.951446
      0.956827
      0.954538
      0.954523
      0.952165
      0.953900
      0.001918
      7

      6
      0.019940
      0.002419
      0.003180
      0.000326
      0.0398107
      l1
      {'C': 0.03981071705534969, 'penalty': 'l1'}
      0.964288
      0.969530
      0.960275
      0.962927
      0.970690
      0.965542
      0.003964
      6

      7
      0.058486
      0.013591
      0.003087
      0.000152
      0.0398107
      l2
      {'C': 0.03981071705534969, 'penalty': 'l2'}
      0.971641
      0.975043
      0.962935
      0.964999
      0.973744
      0.969672
      0.004827
      5

      8
      0.021890
      0.002675
      0.002899
      0.000132
      0.630957
      l1
      {'C': 0.630957344480193, 'penalty': 'l1'}
      0.975414
      0.979419
      0.967373
      0.971965
      0.982132
      0.975261
      0.005246
      1

      9
      0.093863
      0.038499
      0.003230
      0.000209
      0.630957
      l2
      {'C': 0.630957344480193, 'penalty': 'l2'}
      0.975978
      0.978707
      0.967040
      0.970311
      0.980980
      0.974603
      0.005197
      3

      10
      0.025545
      0.003543
      0.002979
      0.000221
      10
      l1
      {'C': 10.0, 'penalty': 'l1'}
      0.973504
      0.974741
      0.968409
      0.972715
      0.982047
      0.974283
      0.004429
      4

      11
      0.117871
      0.044142
      0.002907
      0.000285
      10
      l2
      {'C': 10.0, 'penalty': 'l2'}
      0.975591
      0.978151
      0.967620
      0.972097
      0.982434
      0.975179
      0.005065
      2

Лучшие гиперпараметры:

```python
grid_searcher.best_params_
```

    {'C': 0.630957344480193, 'penalty': 'l1'}

Лучший скор модели на кросс-валидации:

```python
grid_searcher.best_score_
```

    0.9752605535797123

Мы также можем выделить лучшую модель:

```python
lr = grid_searcher.best_estimator_
```

    LogisticRegression(C=0.630957344480193, class_weight=None, dual=False,
                       fit_intercept=True, intercept_scaling=1, l1_ratio=None,
                       max_iter=3000, multi_class='auto', n_jobs=None, penalty='l1',
                       random_state=13, solver='liblinear', tol=0.0001, verbose=0,
                       warm_start=False)

Оценку модели на кросс-валидации мы можем получить и без перебора гиперпараметров:

```python
from sklearn.model_selection import cross_val_score
cv_score = cross_val_score(lr, X_train, y_train, scoring='roc_auc', cv=5)
print(cv_score)
print(cv_score.mean())
```

    [0.97541364 0.97941859 0.96737282 0.97196536 0.98213236]
    0.9752605535797123

Вместо перебора по сетке можно перебирать гиперпараметры, сгенерированные из заданного распределения.

```python
from scipy.stats import uniform
from sklearn.model_selection import RandomizedSearchCV
#lr = LogisticRegression(max_iter=3000, solver='liblinear', random_state=13)
lr = SVC(C=C, kernel='rbf')
distributions = dict(C=uniform(loc=0.0001, scale=10))
clf = RandomizedSearchCV(lr, distributions, n_iter=50, cv=5, verbose=5, scoring='roc_auc', random_state=13, n_jobs=-1)
```

```python
%%time
clf.fit(X_train, y_train)
```

```python
clf.best_score_
```

    0.9753687954229164

```python
print(roc_auc_score(y_train, clf.predict_proba(X_train)[:, 1]))
print(roc_auc_score(y_test, clf.predict_proba(X_test)[:, 1]))
```

    0.9802837482603989
    0.9596008965510431

Для некоторых моделей из `sklearn` можно сразу применить кросс-валидацию:

```python
from sklearn.linear_model import LogisticRegressionCV
lr = LogisticRegressionCV(max_iter=3000, solver='lbfgs', cv=5, random_state=13)
lr.fit(X_train, y_train)
```

```python
lr.C_
```

```python
print(roc_auc_score(y_train, lr.predict_proba(X_train)[:, 1]))
print(roc_auc_score(y_test, lr.predict_proba(X_test)[:, 1]))
```

# SVM

Рассмотрим теперь другой подход к построению функции потерь,
основанный на максимизации зазора между классами.
Будем рассматривать линейные классификаторы вида
$$
    a(x) = sign (\langle w, x \rangle + b), \qquad w \in R^d, b \in R.
$$

### Разделимый случай
Будем считать, что существуют такие параметры $w_*$ и $b_*$,
что соответствующий им классификатор $a(x)$ не допускает ни одной ошибки
на обучающей выборке.
В этом случае говорят, что выборка __линейно разделима__.

Пусть задан некоторый классификатор $a(x) = sign (\langle w, x \rangle + b)$.
Заметим, что если одновременно умножить параметры $w$ и $b$
на одну и ту же положительную константу,
то классификатор не изменится.
Распорядимся этой свободой выбора и отнормируем параметры так, что

\begin{equation}
\label{eq:svmNormCond}
    \min_{x \in X} | \langle w, x \rangle + b| = 1.
\end{equation}

Можно показать, что расстояние от произвольной точки $x_0 \in R^d$ до гиперплоскости,
определяемой данным классификатором, равно

$$
    \rho(x_0, a)
    =
    \frac{
        |\langle w, x \rangle + b|
    }{
        \|w\|
    }.
$$

Тогда расстояние от гиперплоскости до ближайшего объекта обучающей выборки равно

$$
    \min_{x \in X}
    \frac{
        |\langle w, x \rangle + b|
    }{
        \|w\|
    }
    =
    \frac{1}{\|w\|} \min_{x \in X} |\langle w, x \rangle + b|
    =
    \frac{1}{\|w\|}.
$$

Данная величина также называется __отступом (margin)__.

Таким образом, если классификатор без ошибок разделяет обучающую выборку,
то ширина его разделяющей полосы равна $\frac{2}{\|w\|}$.
Известно, что максимизация ширины разделяющей полосы приводит
к повышению обобщающей способности классификатора.
Вспомним также, что на повышение обобщающей способности направлена и регуляризация,
которая штрафует большую норму весов -- а чем больше норма весов,
тем меньше ширина разделяющей полосы.

Итак, требуется построить классификатор, идеально разделяющий обучающую выборку,
и при этом имеющий максимальный отступ.
Запишем соответствующую оптимизационную задачу,
которая и будет определять метод опорных векторов для линейно разделимой выборки (hard margin support vector machine):
\begin{equation}
\label{eq:svmSep}
    \left\{
        \begin{aligned}
            & \frac{1}{2} \|w\|^2 \to \min_{w, b} \\
            & y_i \left(
                \langle w, x_i \rangle + b
            \right) \geq 1, \quad i = 1, \dots, \ell.
        \end{aligned}
    \right.
\end{equation}

### Неразделимый случай
Рассмотрим теперь общий случай, когда выборку
невозможно идеально разделить гиперплоскостью.
Это означает, что какие бы $w$ и $b$ мы не взяли,
хотя бы одно из ограничений в предыдущей задаче будет нарушено:

$$
    \exists x_i \in X:\
    y_i \left(
        \langle w, x_i \rangle + b
    \right) < 1.
$$

Сделаем эти ограничения "мягкими", введя штраф $\xi_i \geq 0$ за их нарушение:

$$
    y_i \left(
        \langle w, x_i \rangle + b
    \right) \geq 1 - \xi_i, \quad i = 1, \dots, \ell.
$$

Отметим, что если отступ объекта лежит между нулем и
единицей ($0 \leq y_i \left( \langle w, x_i \rangle + b \right) < 1$),
то объект верно классифицируется, но имеет ненулевой штраф $\xi > 0$.
Таким образом, мы штрафуем объекты за попадание внутрь разделяющей полосы.

Величина $\frac{1}{\|w\|}$ в данном случае называется мягким отступом (soft margin).
С одной стороны, мы хотим максимизировать отступ, с другой -- минимизировать
штраф за неидеальное разделение выборки $\sum_{i = 1}^{\ell} \xi_i$.
Эти две задачи противоречат друг другу: как правило, излишняя подгонка под
выборку приводит к маленькому отступу, и наоборот -- максимизация отступа
приводит к большой ошибке на обучении.
В качестве компромисса будем минимизировать взвешенную сумму двух указанных величин.
Приходим к оптимизационной задаче,
соответствующей методу опорных векторов для линейно неразделимой выборки (soft margin support vector machine)
\begin{equation}
\label{eq:svmUnsep}
    \left\{
        \begin{aligned}
            & \frac{1}{2} \|w\|^2 + C \sum_{i = 1}^{\ell} \xi_i \to \min_{w, b, \xi} \\
            & y_i \left(
                \langle w, x_i \rangle + b
            \right) \geq 1 - \xi_i, \quad i = 1, \dots, \ell, \\
            & \xi_i \geq 0, \quad i = 1, \dots, \ell.
        \end{aligned}
    \right.
\end{equation}
Чем больше здесь параметр $C$, тем сильнее мы будем настраиваться на обучающую выборку.

Исследуем зависимость положения разделяющей гиперплоскости в методе опорных векторов в зависимости от значения гиперпараметра $C$.

Сгенерируем двумерную искуственную выборку из двух различных нормальных распределений:

```python
class_size=500

mean0 = [7, 5]
cov0 = [[4, 0], [0, 1]]  # diagonal covariance
mean1 = [0, 0]
cov1 = [[4, 0], [0, 2]]
data0 = np.random.multivariate_normal(mean0, cov0, class_size)
data1 = np.random.multivariate_normal(mean1, cov1, class_size)
data = np.vstack((data0, data1))
y = np.hstack((-np.ones(class_size), np.ones(class_size)))

plt.scatter(data0[:, 0], data0[:, 1], c='red', s=50)
plt.scatter(data1[:, 0], data1[:, 1], c='green', s=50)
plt.legend(['y = -1', 'y = 1'])
axes = plt.gca()
axes.set_xlim([-5,15])
axes.set_ylim([-5,10])
plt.show()
```

```python
from sklearn.svm import SVC

SVM_classifier = SVC(C=0.01, kernel='linear') # changing C here
SVM_classifier.fit(data, y)
```

    SVC(C=0.01, break_ties=False, cache_size=200, class_weight=None, coef0=0.0,
        decision_function_shape='ovr', degree=3, gamma='scale', kernel='linear',
        max_iter=-1, probability=False, random_state=None, shrinking=True,
        tol=0.001, verbose=False)

```python
w_1 = SVM_classifier.coef_[0][0]
w_2 = SVM_classifier.coef_[0][1]
w_0 = SVM_classifier.intercept_[0]

plt.scatter(data0[:, 0], data0[:, 1], c='red', s=50)
plt.scatter(data1[:, 0], data1[:, 1], c='green', s=50)
plt.legend(['y = -1', 'y = 1'])
x_arr = np.linspace(-10, 15, 3000)
plt.plot(x_arr, -(w_0 + w_1 * x_arr) / w_2)
axes = plt.gca()
axes.set_xlim([-5,15])
axes.set_ylim([-5,10])
plt.show()
```

```python
plt.scatter(data0[:, 0], data0[:, 1], c='red', s=50, label='y = -1')
plt.scatter(data1[:, 0], data1[:, 1], c='green', s=50, label='y = +1')
#plt.legend(['y = -1', 'y = 1'])
x_arr = np.linspace(-10, 15, 3000)
colors = ['red', 'orange', 'green', 'blue', 'magenta']

for i, C in enumerate([0.0001, 0.01,  1, 100, 10000]):
    SVM_classifier = SVC(C=C, kernel='linear')
    SVM_classifier.fit(data, y)
    w_1 = SVM_classifier.coef_[0][0]
    w_2 = SVM_classifier.coef_[0][1]
    w_0 = SVM_classifier.intercept_[0]
    plt.plot(x_arr, -(w_0 + w_1 * x_arr) / w_2, color=colors[i], label='C='+str(C))

axes = plt.gca()
axes.set_xlim([-5,15])
axes.set_ylim([-5,10])
plt.legend(loc=0)
plt.show()
```

Гиперпараметр $C$ отвечает за то, что является более приоритетным для классификатора, — "подгонка" под обучающую выборку или максимизация ширины разделяющей полосы.
 - При больших значениях $C$ классификатор сильно настраивается на обучение, тем самым сужая разделяющую полосу.
 - При маленьких значениях $C$ классификатор расширяет разделяющую полосу, при этом допуская ошибки на некоторых объектах обучающей выборки.

04 ML/05 svm/sem05_logreg_svm seminar.ipynb


```python
import devito
from matplotlib import pyplot as plt
import numpy as np
%matplotlib inline
```

```python
Lx = 100.0   # width of the mantle in the x direction []
Ly = 100.0   # thickness of the mantle in the y direction []
nx = 401     # number of points in the x direction
ny = 401     # number of points in the y direction
dx = Lx / (nx - 1)  # grid spacing in the x direction []
dy = Ly / (ny - 1)  # grid spacing in the y direction []
g = 9.81  # gravity acceleration [m/s^2]
alpha = 0.025 # friction coefficient for natural channels in good condition
# Maximum wave propagation time [s]
Tmax = 1.
dt = 1/4500.
nt = (int)(Tmax/dt)
print(dt, nt)
```

    0.00022222222222222223 4500

```python
from devito import Grid, TimeFunction, Function

grid = Grid(shape=(ny, nx), extent=(Ly, Lx))
eta = TimeFunction(name='eta', grid=grid, space_order=2)
M  = TimeFunction(name='M', grid=grid, space_order=2)
N  = TimeFunction(name='N', grid=grid, space_order=2)
h  = Function(name='h', grid=grid)
D  = Function(name='D', grid=grid)

print(N.data.shape)
```

    Allocating memory for N(2, 405, 405)

    (2, 401, 401)

```python
import numpy
# Define the locations along a gridline.
x = numpy.linspace(0.0, Lx, num=nx)
y = numpy.linspace(0.0, Ly, num=ny)

# Define initial eta, M, N
X, Y = numpy.meshgrid(x,y) # coordinates X,Y required to define eta, h, M, N

# Define constant ocean depth profile h = 50 m
h.data[:] = 50

# Define initial eta Gaussian distribution [m]
eta0 = 0.5 * numpy.exp(-((X-50)**2/10)-((Y-50)**2/10))

# Define initial M and N
M0 = 100. * eta0
N0 = 0. * M0
D0 = eta0 + 50.
```

    Allocating memory for h(403, 403)

```python
eta.data[0] = eta0.copy()
M.data[0]   = M0.copy()
N.data[0]   = N0.copy()
D.data[:] = D0.copy()

print(eta0.shape, h.data.shape)
```

    Allocating memory for eta(2, 405, 405)
    Allocating memory for M(2, 405, 405)
    Allocating memory for D(403, 403)

    (401, 401) (401, 401)

```python
from devito import sqrt
frictionTerm = g * alpha**2 * sqrt(M**2 + N**2 ) / D**(7./3.)
frictionTerm
```

$\displaystyle \frac{0.00613125 \sqrt{M^{2}{\left(t,x,y \right)} + N^{2}{\left(t,x,y \right)}}}{D^{2.33333333333333}{\left(x,y \right)}}$

```python
from devito import Eq, solve
pde_eta = Eq(eta.dt + M.dxc + N.dyc)
pde_M   = Eq(M.dt + (M**2/D).dxc + (M*N/D).dyc + g*D*eta.forward.dxc + frictionTerm*M)
pde_N   = Eq(N.dt + (M*N/D).dxc + (N**2/D).dyc + g*D*eta.forward.dyc + frictionTerm*N)

# Defining boundary conditions
x, y = grid.dimensions
t = grid.stepping_dim
bc_left   = Eq(eta[t+1, 0, y], eta[t+1, 1, y])
bc_right  = Eq(eta[t+1, nx-1, y], eta[t+1, nx-2, y])
bc_top    = Eq(eta[t+1, x, 0], eta[t+1, x, 1])
bc_bottom = Eq(eta[t+1, x, ny-1], eta[t+1, x, ny-2])

stencil_eta = solve(pde_eta, eta.forward)
stencil_M   = solve(pde_M, M.forward)
stencil_N   = solve(pde_N, N.forward)

update_eta  = Eq(eta.forward, stencil_eta, subdomain=grid.interior)
update_M    = Eq(M.forward, stencil_M, subdomain=grid.interior)
update_N    = Eq(N.forward, stencil_N, subdomain=grid.interior)
eq_D        = Eq(D, eta.forward + h)

update_eta
```

$\displaystyle \eta{\left(t + dt,x,y \right)} = dt \left(- \frac{\partial}{\partial x} M{\left(t,x,y \right)} - \frac{\partial}{\partial y} N{\left(t,x,y \right)} + \frac{\eta{\left(t,x,y \right)}}{dt}\right)$

```python
update_M
```

$\displaystyle M{\left(t + dt,x,y \right)} = dt \left(- \frac{0.00613125 \sqrt{M^{2}{\left(t,x,y \right)} + N^{2}{\left(t,x,y \right)}} M{\left(t,x,y \right)}}{D^{2.33333333333333}{\left(x,y \right)}} - 9.81 D{\left(x,y \right)} \frac{\partial}{\partial x} \eta{\left(t + dt,x,y \right)} - \frac{\partial}{\partial x} \frac{M^{2}{\left(t,x,y \right)}}{D{\left(x,y \right)}} - \frac{\partial}{\partial y} \frac{M{\left(t,x,y \right)} N{\left(t,x,y \right)}}{D{\left(x,y \right)}} + \frac{M{\left(t,x,y \right)}}{dt}\right)$

```python
update_N
```

$\displaystyle N{\left(t + dt,x,y \right)} = dt \left(- \frac{0.00613125 \sqrt{M^{2}{\left(t,x,y \right)} + N^{2}{\left(t,x,y \right)}} N{\left(t,x,y \right)}}{D^{2.33333333333333}{\left(x,y \right)}} - 9.81 D{\left(x,y \right)} \frac{\partial}{\partial y} \eta{\left(t + dt,x,y \right)} - \frac{\partial}{\partial y} \frac{N^{2}{\left(t,x,y \right)}}{D{\left(x,y \right)}} - \frac{\partial}{\partial x} \frac{M{\left(t,x,y \right)} N{\left(t,x,y \right)}}{D{\left(x,y \right)}} + \frac{N{\left(t,x,y \right)}}{dt}\right)$

```python
from devito import Operator

optime = Operator([update_eta, bc_left, bc_right, bc_top, bc_bottom,
                   update_M, update_N, eq_D])
```

    Operator `Kernel` generated in 1.31 s
      * lowering.Clusters: 0.73 s (56.0 %)
         * specializing.Clusters: 0.60 s (46.1 %)
            * factorize: 0.39 s (30.0 %)
      * lowering.IET: 0.30 s (23.1 %)
    Flops reduction after symbolic optimization: [112 --> 75]

```python
plt.imshow(eta.data[0])
```

```python
plt.imshow(M.data[0])
```

```python
plt.imshow(N.data[0])
```

```python
print(optime.ccode)
```

    #define _POSIX_C_SOURCE 200809L
    #define START_TIMER(S) struct timeval start_ ## S , end_ ## S ; gettimeofday(&start_ ## S , NULL);
    #define STOP_TIMER(S,T) gettimeofday(&end_ ## S, NULL); T->S += (double)(end_ ## S .tv_sec-start_ ## S.tv_sec)+(double)(end_ ## S .tv_usec-start_ ## S .tv_usec)/1000000;

    #include "stdlib.h"
    #include "math.h"
    #include "sys/time.h"
    #include "xmmintrin.h"
    #include "pmmintrin.h"

    struct dataobj
    {
      void *restrict data;
      int * size;
      int * npsize;
      int * dsize;
      int * hsize;
      int * hofs;
      int * oofs;
    } ;

    struct profiler
    {
      double section0;
      double section1;
      double section2;
    } ;

    int Kernel(struct dataobj *restrict D_vec, struct dataobj *restrict M_vec, struct dataobj *restrict N_vec, const float dt, struct dataobj *restrict eta_vec, struct dataobj *restrict h_vec, const float h_x, const float h_y, const int i0x_ltkn, const int i0x_rtkn, const int i0y_ltkn, const int i0y_rtkn, const int time_M, const int time_m, const int x_M, const int x_m, const int y_M, const int y_m, struct profiler * timers)
    {
      float (*restrict D)[D_vec->size[1]] __attribute__ ((aligned (64))) = (float (*)[D_vec->size[1]]) D_vec->data;
      float (*restrict M)[M_vec->size[1]][M_vec->size[2]] __attribute__ ((aligned (64))) = (float (*)[M_vec->size[1]][M_vec->size[2]]) M_vec->data;
      float (*restrict N)[N_vec->size[1]][N_vec->size[2]] __attribute__ ((aligned (64))) = (float (*)[N_vec->size[1]][N_vec->size[2]]) N_vec->data;
      float (*restrict eta)[eta_vec->size[1]][eta_vec->size[2]] __attribute__ ((aligned (64))) = (float (*)[eta_vec->size[1]][eta_vec->size[2]]) eta_vec->data;
      float (*restrict h)[h_vec->size[1]] __attribute__ ((aligned (64))) = (float (*)[h_vec->size[1]]) h_vec->data;

      /* Flush denormal numbers to zero in hardware */
      _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);
      _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

      for (int time = time_m, t0 = (time)%(2), t1 = (time + 1)%(2); time <= time_M; time += 1, t0 = (time)%(2), t1 = (time + 1)%(2))
      {
        /* Begin section0 */
        START_TIMER(section0)
        for (int i0x = i0x_ltkn + x_m; i0x <= -i0x_rtkn + x_M; i0x += 1)
        {
          #pragma omp simd aligned(M,N,eta:32)
          for (int i0y = i0y_ltkn + y_m; i0y <= -i0y_rtkn + y_M; i0y += 1)
          {
            eta[t1][i0x + 2][i0y + 2] = dt*((5.0e-1F*(N[t0][i0x + 2][i0y + 1] - N[t0][i0x + 2][i0y + 3]))/h_y + (5.0e-1F*(M[t0][i0x + 1][i0y + 2] - M[t0][i0x + 3][i0y + 2]))/h_x + eta[t0][i0x + 2][i0y + 2]/dt);
          }
        }
        STOP_TIMER(section0,timers)
        /* End section0 */
        /* Begin section1 */
        START_TIMER(section1)
        for (int y = y_m; y <= y_M; y += 1)
        {
          eta[t1][2][y + 2] = eta[t1][3][y + 2];
          eta[t1][402][y + 2] = eta[t1][401][y + 2];
        }
        STOP_TIMER(section1,timers)
        /* End section1 */
        /* Begin section2 */
        START_TIMER(section2)
        for (int x = x_m; x <= x_M; x += 1)
        {
          eta[t1][x + 2][2] = eta[t1][x + 2][3];
          eta[t1][x + 2][402] = eta[t1][x + 2][401];
        }
        for (int i0x = i0x_ltkn + x_m; i0x <= -i0x_rtkn + x_M; i0x += 1)
        {
          #pragma omp simd aligned(D,M,N,eta:32)
          for (int i0y = i0y_ltkn + y_m; i0y <= -i0y_rtkn + y_M; i0y += 1)
          {
            float r7 = 1.0F/dt;
            float r6 = 1.0F/h_y;
            float r5 = 1.0F/h_x;
            float r4 = 1.0F/D[i0x + 1][i0y + 2];
            float r3 = 1.0F/D[i0x + 2][i0y + 1];
            float r2 = 1.0F/D[i0x + 1][i0y + 1];
            float r1 = pow(D[i0x + 1][i0y + 1], -2.33333333333333F);
            float r0 = sqrt(pow(M[t0][i0x + 2][i0y + 2], 2) + pow(N[t0][i0x + 2][i0y + 2], 2));
            M[t1][i0x + 2][i0y + 2] = dt*(r0*(-6.13125e-3F*r1*M[t0][i0x + 2][i0y + 2]) + r2*(r5*(M[t0][i0x + 2][i0y + 2]*M[t0][i0x + 2][i0y + 2]) + r6*M[t0][i0x + 2][i0y + 2]*N[t0][i0x + 2][i0y + 2]) + r5*(-r3*M[t0][i0x + 3][i0y + 2]*M[t0][i0x + 3][i0y + 2] - 4.905F*(-eta[t1][i0x + 1][i0y + 2] + eta[t1][i0x + 3][i0y + 2])*D[i0x + 1][i0y + 1]) + r6*(-r4*M[t0][i0x + 2][i0y + 3]*N[t0][i0x + 2][i0y + 3]) + r7*M[t0][i0x + 2][i0y + 2]);
            N[t1][i0x + 2][i0y + 2] = dt*(r0*(-6.13125e-3F*r1*N[t0][i0x + 2][i0y + 2]) + r2*(r5*M[t0][i0x + 2][i0y + 2]*N[t0][i0x + 2][i0y + 2] + r6*(N[t0][i0x + 2][i0y + 2]*N[t0][i0x + 2][i0y + 2])) + r5*(-r3*M[t0][i0x + 3][i0y + 2]*N[t0][i0x + 3][i0y + 2]) + r6*(-r4*N[t0][i0x + 2][i0y + 3]*N[t0][i0x + 2][i0y + 3] - 4.905F*(-eta[t1][i0x + 2][i0y + 1] + eta[t1][i0x + 2][i0y + 3])*D[i0x + 1][i0y + 1]) + r7*N[t0][i0x + 2][i0y + 2]);
          }
        }
        for (int x = x_m; x <= x_M; x += 1)
        {
          #pragma omp simd aligned(D,eta,h:32)
          for (int y = y_m; y <= y_M; y += 1)
          {
            D[x + 1][y + 1] = h[x + 1][y + 1] + eta[t1][x + 2][y + 2];
          }
        }
        STOP_TIMER(section2,timers)
        /* End section2 */
      }

      return 0;
    }

```python
optime(time=nt, dt=dt)
plt.imshow(eta.data[1])
```

```python
plt.imshow(N.data[1])
```

```python
plt.imshow(M.data[1])
```

```python
from devito import Grid, TimeFunction, Function, sqrt, Eq, solve, Operator
import numpy
def Shallow_water_2D(eta0, M0, N0, h0, g, alpha, nt, dx, dy, dt):
    """
    Computes and returns the discharge fluxes M, N and wave height eta from
    the 2D Shallow water equation using the FTCS finite difference method.

    Parameters
    ----------
    eta0 : numpy.ndarray
        The initial wave height field as a 2D array of floats.
    M0 : numpy.ndarray
        The initial discharge flux field in x-direction as a 2D array of floats.
    N0 : numpy.ndarray
        The initial discharge flux field in y-direction as a 2D array of floats.
    h : numpy.ndarray
        Bathymetry model as a 2D array of floats.
    g : float
        gravity acceleration.
    alpha : float
        Manning's roughness coefficient.
    nt : integer
        Number fo timesteps.
    dx : float
        Spatial gridpoint distance in x-direction.
    dy : float
        Spatial gridpoint distance in y-direction.
    dt : float
        Time step.
    """
    grid = Grid(shape=(ny, nx), extent=(Ly, Lx))
    eta = TimeFunction(name='eta', grid=grid, space_order=2)
    M   = TimeFunction(name='M', grid=grid, space_order=2)
    N   = TimeFunction(name='N', grid=grid, space_order=2)
    h   = Function(name='h', grid=grid)
    D   = Function(name='D', grid=grid)

    D0 = eta0.copy() + h0.copy()

    eta.data[0] = eta0.copy()
    M.data[0]   = M0.copy()
    N.data[0]   = N0.copy()
    D.data[:]   = D0.copy()

    frictionTerm = g * alpha**2 * sqrt(M**2 + N**2 ) / D**(7./3.)

    pde_eta = Eq(eta.dt + M.dxc + N.dyc)
    pde_M   = Eq(M.dt + (M**2/D).dxc + (M*N/D).dyc + g*D*eta.forward.dxc + frictionTerm*M)
    pde_N   = Eq(N.dt + (M*N/D).dxc + (N**2/D).dyc + g*D*eta.forward.dyc + frictionTerm*N)

    # Defining boundary conditions
    x, y       = grid.dimensions
    t          = grid.stepping_dim
    bc_left    = Eq(eta[t+1, 0, y], eta[t+1, 1, y])
    bc_right   = Eq(eta[t+1, nx-1, y], eta[t+1, nx-2, y])
    bc_top     = Eq(eta[t+1, x, 0], eta[t+1, x, 1])
    bc_bottom  = Eq(eta[t+1, x, ny-1], eta[t+1, x, ny-2])

    stencil_eta = solve(pde_eta, eta.forward)
    stencil_M   = solve(pde_M, M.forward)
    stencil_N   = solve(pde_N, N.forward)

    update_eta  = Eq(eta.forward, stencil_eta, subdomain=grid.interior)
    update_M    = Eq(M.forward, stencil_M, subdomain=grid.interior)
    update_N    = Eq(N.forward, stencil_N, subdomain=grid.interior)
    eq_D        = Eq(D, eta.forward + h)

    optime = Operator([update_eta, bc_left, bc_right, bc_top, bc_bottom,
                       update_M, update_N, eq_D])

    op(time=nt, dt=dt)
    print(eta.data[1])
    return eta.data[1]
```

## Example I: Tsunami in an ocean with constant depth

After writing the `Shallow_water_2D` code and all the required functions attached to it, we can define and run our first 2D Tsunami modelling run.

Let's assume that the ocean model is $ L_x = 100\; m$ in x-direction and $L_y = 100\; m$ in y-direction. The model is discretized with $nx=401$ gridpoints in x-direction and $ny=401$ gridpoints in y-direction, respectively.

In this first modelling run, we assume a constant bathymetry $h=50\;m$. The initial wave height field $\eta_0$ is defined as a Gaussian at the center of the model, with a half-width of 10 m and an amplitude of 0.5 m. Regarding the initial discharge fluxes, we assume that

\begin{equation}
\begin{split}
M_0(x,y) &= 100 \eta_0(x,y)\\
N_0(x,y) &= 0\\
\end{split}\notag
\end{equation}

Furthermore, Dirichlet boundary conditions for the discharge fluxes $M,\;N$ are assumed at all boundaries:

\begin{equation}
\begin{split}
M(0,y) &= M(L_x,y) = M(x,0) = M(x,L_y) = 0\\
N(0,y) &= N(L_x,y) = N(x,0) = N(x,L_y) = 0\\
\end{split} \notag
\end{equation}

For the wave height field $\eta$, it is essential that Neumann boundary conditions are set at all boundaries:

\begin{equation}
\begin{split}
&\frac{\partial \eta}{\partial x}(0,y) = 0\\
&\frac{\partial \eta}{\partial x}(L_x,y) = 0\\
&\frac{\partial \eta}{\partial y}(x,0) = 0\\
&\frac{\partial \eta}{\partial y}(x,L_y) = 0\\
\end{split}\notag
\end{equation}

in order to avoid the occurence of high frequency artifacts, when waves are interacting with the boundaries. Notice, that the assumed boundary conditions lead to significant boundary reflections which might be not realistic for a given problem. However, to keep the problem simple, we will not try to mitigate the boundary reflections.

```python
Lx = 100.0   # width of the mantle in the x direction []
Ly = 100.0   # thickness of the mantle in the y direction []
nx = 401     # number of points in the x direction
ny = 401     # number of points in the y direction
dx = Lx / (nx - 1)  # grid spacing in the x direction []
dy = Ly / (ny - 1)  # grid spacing in the y direction []
g = 9.81  # gravity acceleration [m/s^2]
alpha = 0.025 # friction coefficient for natural channels in good condition
# Maximum wave propagation time [s]
Tmax = 1.
dt = 1/4500.
nt = (int)(Tmax/dt)
print(dt, nt)
```

    0.00022222222222222223 4500

```python
# Define the locations along a gridline.
x = numpy.linspace(0.0, Lx, num=nx)
y = numpy.linspace(0.0, Ly, num=ny)

# Define initial eta, M, N
X, Y = numpy.meshgrid(x,y) # coordinates X,Y required to define eta, h, M, N

# Define constant ocean depth profile h = 50 m
h0 = 50. * numpy.ones_like(X)

# Define initial eta Gaussian distribution [m]
eta0 = 0.5 * numpy.exp(-((X-50)**2/10)-((Y-50)**2/10))

# Define initial M and N
M0 = 100. * eta0
N0 = 0. * M0
D0 = eta0 + 50.
```

```python
eta = Shallow_water_2D(eta0, M0, N0, h0, g, alpha, nt, dx, dy, dt)

#print(eta.data[0])
plt.imshow(eta)
```

```python
print(op.ccode)
```

    #define _POSIX_C_SOURCE 200809L
    #define START_TIMER(S) struct timeval start_ ## S , end_ ## S ; gettimeofday(&start_ ## S , NULL);
    #define STOP_TIMER(S,T) gettimeofday(&end_ ## S, NULL); T->S += (double)(end_ ## S .tv_sec-start_ ## S.tv_sec)+(double)(end_ ## S .tv_usec-start_ ## S .tv_usec)/1000000;

    #include "stdlib.h"
    #include "math.h"
    #include "sys/time.h"
    #include "xmmintrin.h"
    #include "pmmintrin.h"

    struct dataobj
    {
      void *restrict data;
      int * size;
      int * npsize;
      int * dsize;
      int * hsize;
      int * hofs;
      int * oofs;
    } ;

    struct profiler
    {
      double section0;
      double section1;
      double section2;
    } ;

    int Kernel(struct dataobj *restrict D_vec, struct dataobj *restrict M_vec, struct dataobj *restrict N_vec, const float dt, struct dataobj *restrict eta_vec, struct dataobj *restrict h_vec, const float h_x, const float h_y, const int i0x_ltkn, const int i0x_rtkn, const int i0y_ltkn, const int i0y_rtkn, const int time_M, const int time_m, const int x_M, const int x_m, const int y_M, const int y_m, struct profiler * timers)
    {
      float (*restrict D)[D_vec->size[1]] __attribute__ ((aligned (64))) = (float (*)[D_vec->size[1]]) D_vec->data;
      float (*restrict M)[M_vec->size[1]][M_vec->size[2]] __attribute__ ((aligned (64))) = (float (*)[M_vec->size[1]][M_vec->size[2]]) M_vec->data;
      float (*restrict N)[N_vec->size[1]][N_vec->size[2]] __attribute__ ((aligned (64))) = (float (*)[N_vec->size[1]][N_vec->size[2]]) N_vec->data;
      float (*restrict eta)[eta_vec->size[1]][eta_vec->size[2]] __attribute__ ((aligned (64))) = (float (*)[eta_vec->size[1]][eta_vec->size[2]]) eta_vec->data;
      float (*restrict h)[h_vec->size[1]] __attribute__ ((aligned (64))) = (float (*)[h_vec->size[1]]) h_vec->data;

      /* Flush denormal numbers to zero in hardware */
      _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);
      _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

      for (int time = time_m, t0 = (time)%(2), t1 = (time + 1)%(2); time <= time_M; time += 1, t0 = (time)%(2), t1 = (time + 1)%(2))
      {
        /* Begin section0 */
        START_TIMER(section0)
        for (int i0x = i0x_ltkn + x_m; i0x <= -i0x_rtkn + x_M; i0x += 1)
        {
          #pragma omp simd aligned(M,N,eta:32)
          for (int i0y = i0y_ltkn + y_m; i0y <= -i0y_rtkn + y_M; i0y += 1)
          {
            eta[t1][i0x + 2][i0y + 2] = dt*((5.0e-1F*(N[t0][i0x + 2][i0y + 1] - N[t0][i0x + 2][i0y + 3]))/h_y + (5.0e-1F*(M[t0][i0x + 1][i0y + 2] - M[t0][i0x + 3][i0y + 2]))/h_x + eta[t0][i0x + 2][i0y + 2]/dt);
          }
        }
        STOP_TIMER(section0,timers)
        /* End section0 */
        /* Begin section1 */
        START_TIMER(section1)
        for (int y = y_m; y <= y_M; y += 1)
        {
          eta[t1][2][y + 2] = eta[t1][3][y + 2];
          eta[t1][402][y + 2] = eta[t1][401][y + 2];
        }
        STOP_TIMER(section1,timers)
        /* End section1 */
        /* Begin section2 */
        START_TIMER(section2)
        for (int x = x_m; x <= x_M; x += 1)
        {
          eta[t1][x + 2][2] = eta[t1][x + 2][3];
          eta[t1][x + 2][402] = eta[t1][x + 2][401];
        }
        for (int i0x = i0x_ltkn + x_m; i0x <= -i0x_rtkn + x_M; i0x += 1)
        {
          #pragma omp simd aligned(D,M,N,eta:32)
          for (int i0y = i0y_ltkn + y_m; i0y <= -i0y_rtkn + y_M; i0y += 1)
          {
            float r7 = 1.0F/dt;
            float r6 = 1.0F/h_y;
            float r5 = 1.0F/h_x;
            float r4 = 1.0F/D[i0x + 1][i0y + 2];
            float r3 = 1.0F/D[i0x + 2][i0y + 1];
            float r2 = 1.0F/D[i0x + 1][i0y + 1];
            float r1 = pow(D[i0x + 1][i0y + 1], -2.33333333333333F);
            float r0 = sqrt(pow(M[t0][i0x + 2][i0y + 2], 2) + pow(N[t0][i0x + 2][i0y + 2], 2));
            M[t1][i0x + 2][i0y + 2] = dt*(r0*(-6.13125e-3F*r1*M[t0][i0x + 2][i0y + 2]) + r2*(r5*(M[t0][i0x + 2][i0y + 2]*M[t0][i0x + 2][i0y + 2]) + r6*M[t0][i0x + 2][i0y + 2]*N[t0][i0x + 2][i0y + 2]) + r5*(-r3*M[t0][i0x + 3][i0y + 2]*M[t0][i0x + 3][i0y + 2] - 4.905F*(-eta[t1][i0x + 1][i0y + 2] + eta[t1][i0x + 3][i0y + 2])*D[i0x + 1][i0y + 1]) + r6*(-r4*M[t0][i0x + 2][i0y + 3]*N[t0][i0x + 2][i0y + 3]) + r7*M[t0][i0x + 2][i0y + 2]);
            N[t1][i0x + 2][i0y + 2] = dt*(r0*(-6.13125e-3F*r1*N[t0][i0x + 2][i0y + 2]) + r2*(r5*M[t0][i0x + 2][i0y + 2]*N[t0][i0x + 2][i0y + 2] + r6*(N[t0][i0x + 2][i0y + 2]*N[t0][i0x + 2][i0y + 2])) + r5*(-r3*M[t0][i0x + 3][i0y + 2]*N[t0][i0x + 3][i0y + 2]) + r6*(-r4*N[t0][i0x + 2][i0y + 3]*N[t0][i0x + 2][i0y + 3] - 4.905F*(-eta[t1][i0x + 2][i0y + 1] + eta[t1][i0x + 2][i0y + 3])*D[i0x + 1][i0y + 1]) + r7*N[t0][i0x + 2][i0y + 2]);
          }
        }
        for (int x = x_m; x <= x_M; x += 1)
        {
          #pragma omp simd aligned(D,eta,h:32)
          for (int y = y_m; y <= y_M; y += 1)
          {
            D[x + 1][y + 1] = h[x + 1][y + 1] + eta[t1][x + 2][y + 2];
          }
        }
        STOP_TIMER(section2,timers)
        /* End section2 */
      }

      return 0;
    }

```python

```

tinkered with examples/Shallow water 2D.ipynb


# Mock exam 2018 - solutions

## Question 1

### (a) **definition of a Normal form game**:

An \\(N\\) player normal form game consists of:

- A finite set of $N$ players
- Strategy spaces for the players: $\{S_1,S_2,S_3,\dots,S_N\}$;
- Payoff functions for the players: $u_i:S_1\times S_2\dots\times S_N\to\mathbb{R}$

[2]

### (b) **Identifying best responses**

We have:

- $u_r((1, 0), (y, 1-y))= y - 2 (1 - y)=3y-2$ and $u_r((0, 1), (y, 1-y))= -2y + (1 - y)=1-3y$
- $u_c((x, 1 - x), (1, 0))=-2x+(1-x)=1-3x$ and $u_c((x, 1- x), (0, 1))=x-2(1-x)=3x-2$

```python
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
plt.figure()
ys = [0, 1]
plt.plot(ys, [3 * y - 2 for y in ys], label="$u_r((1, 0), (y, 1-y))$")
plt.plot(ys, [1 - 3 * y for y in ys], label="$u_r((0, 1), (y, 1-y))$")
plt.legend()
plt.xlabel("$y$")
plt.title("Utility to row player");
```

[1]

```python
plt.figure()
xs = [0, 1]
plt.plot(xs, [1 - x * 3 for x in xs], label="$u_c((x, 1 - x), (1, 0))$")
plt.plot(xs, [3 * x - 2 for x in xs], label="$u_c((x, 1 - x), (0, 1))$")
plt.legend()
plt.xlabel("$x$")
plt.title("Utility to column player");
```

[1]

The best responses are then given by:

$$
\sigma_r^* = \begin{cases}
(0,1)&\text{ if }y< 1/2\\
(1,0)&\text{ if }y> 1/2\\
\text{indifferent}&\text{ otherwise}
\end{cases}
$$

$$
\sigma_c^* = \begin{cases}
(1,0)&\text{ if }x< 1/2\\
(0,1)&\text{ if }x> 1/2\\
\text{indifferent}&\text{ otherwise}
\end{cases}
$$

[1]

### **(c) Proof of best response condition**

$(A\sigma_c^T)_i$ is the utility of the row player when they play their $i$th strategy. Thus:

$$\sigma_rA\sigma_c^T=\sum_{i=1}^{m}{\sigma_r}_i(A\sigma_c^T)_i$$

[2]

Let $u=\max_{k}(A\sigma_c^T)_k$. Thus:

$$
\begin{aligned}
\sigma_rA\sigma_c^T&=\sum_{i=1}^{m}{\sigma_r}_i(u - u + (A\sigma_c^T)_i)\\
                   &=\sum_{i=1}^{m}{\sigma_r}_iu - \sum_{i=1}^{m}{\sigma_r}_i(u - (A\sigma_c^T)_i)\\
                   &=u - \sum_{i=1}^{m}{\sigma_r}_i(u - (A\sigma_c^T)_i)
\end{aligned}$$

[2]

We know that $u - (A\sigma_c^T)_i\geq 0$, thus the largest $\sigma_rA\sigma_c^T$ can be is $u$ which occurs iff ${\sigma_r}_i > 0 \Rightarrow (A\sigma_c^T)_i = u$ as required.

[1]

### **(d) Finding the Nash equilibrium**

There are no pairs of **pure** strategies that are best response to each other (visibile from the payoff matrices).
We consider mixed strategies $\sigma_r=(x, 1- x)$ and $\sigma_c=(y, 1-y)$  [1]

For $\sigma_r$ to be a best response to $\sigma_c$, from the theorem we have:

$$u_r((1, 0), \sigma_c)=u_r((0, 1), \sigma_c)\Rightarrow 3y-2=1-3y\Rightarrow y=1/2$$
$$u_c(\sigma_r, (1, 0))=u_c(\sigma_r, (0, 1))\Rightarrow 1-3x=3x-2\Rightarrow x=1/2$$

[2]

The Nash equilibrium is $((1/2, 1/2), (1/2, 1/2))$.

This is confirmed by the best response findings of question (b): these two strategies are best responses to each other. [1]

### **(e) Discussing the Knight et al. 2017 paper**

- (i): This paper builds a Markov queueing model based on strategic interactions between two hospitals [1]. It measures the Price of Anarchy to see the effect on patients of rational behaviour [1]. It does all this by comparing the Nash equilibrium to the optimal behaviour [1].
- (ii): The main theoretic result is concerning the behaviour of the best response functions. [1] The paper proves that they are increasing functions which ensures there is a single point of intersection. [1]
- (iii): Various possibilities each worth [2]:
   - A stationary queueing model whilst in practice a time dependent model would be more appropriate.
   - Only considering interaction between two hospitals.
   - Just a single cutoff strategy.
- (iv): Depending on the choice of limiting factor [3]:
   - Use a non stationary queueing model, potentially simulating the queueing process in a time dependent way.
   - Consider a multi player game although this becomes harder to generalise the theoretic result.
   - Use multiple cutoff strategies (this would be the simplest approach).

##  Question 2

### (a) Definition of repeated game

Given a two player game $(A,B)\in{\mathbb{R}^{m\times n}}^2$, referred to as a **stage** game, a $T$-stage repeated game is a game in which players play that stage game for $T > 0$ periods. Players make decisions based on the full history of play over all the periods.

[2]

### (b) Definition of a strategy in a repeated game

Given a two player stage game $(A,B)\in{\mathbb{R}^{m\times n}}^2$, repeated to give a $T$-stage repeated game. A strategy is a mapping from the entire history of play to an action of the stage game:

$$
\bigcup_{t=0}^{T-1}H(t)\to a
$$

where:

- $H(t)$ is the history of player of **both** players up until stage $t$ ($H(0)=(\emptyset, \emptyset)$)
- $a$ is an action (for either player) of the **stage** game

[2]

### (c) Size of history space

For a given integer value of $0\leq t< T$ there are $|S_1|^t$ possible histories for player 1 and $|S_2|^t$ possible histories for player 2. Thus the total number of histories is: [2]

$$\sum_{t=0}^{T - 1}|S_1|^t|S_2|^t$$

[1]

which gives:

$$\sum_{t=0}^{T - 1}|S_1|^t|S_2|^t=\sum_{t=0}^{T - 1}(|S_1||S_2|)^t=\frac{1 - (|S_1||S_2|)^T}{1 - |S_1||S_2|}$$

[2]

### (d) Listing strategy spaces

$S_1\{r_1, r_2\}$, $S_2=\{c_1, c_2, c_3\}$ and $T=2$

$$\{(\emptyset, \emptyset), (r_1, c_1), (r_1, c_2), (r_1, c_3), (r_2, c_1), (r_2, c_2), (r_2, c_3)\}$$

[3]

### **(e) Sequence of stage Nash theorem**

Consider the following strategy:

> The row/column player should play action $a_{r/c}$ regardless of the play of any previous strategy profiles. [2]

where $(a_{r}, a_{c})$ is a given stage Nash equilibrium.

Using backwards induction, this is a Nash equilibrium for the last stage game. Thus, at the last stage, no player has a reason to deviate. Similarly at the $T-1$th stage. The proof follows.

[2]

### **(f) Pure Nash equilibria**

The pure Nash equilibria:

$$A = \begin{pmatrix}\underline{3} & \underline{6} & \underline{1}\\1 & 2 & 0\\\end{pmatrix}
\qquad
B = \begin{pmatrix}0 & \underline{7} & \underline{7}\\\underline{20} & 1 & 0\\\end{pmatrix}$$

Thus, for our example we have the four Nash equilibria:

- $(r_1r_1, c_2c_2)$ with utility: (12, 14). [1]
- $(r_1r_1, c_2c_3)$ with utility: (7, 14). [1]
- $(r_1r_1, c_3c_2)$ with utility: (7, 14). [1]
- $(r_1r_1, c_3c_3)$ with utility: (2, 14). [1]

### **(g) Note state Nash, equilibria**

Consider the following two strategies:

1. For the row player:

   $$(\emptyset, \emptyset) \to r_2$$
   $$(r_2, c_1) \to r_1$$
   $$(r_2, c_2) \to r_1$$
   $$(r_2, c_3) \to r_1$$

   [2]

2. For the column player:

   $$(\emptyset, \emptyset) \to c_1$$
   $$(r_1, c_1) \to c_3$$
   $$(r_2, c_1) \to c_2$$

   [2]

This is a Nash equilibria because:

1. If the row player deviates, they would only be rational to do so in the first stage, if they did they would gain 2 in that stage but lose 5 in the second stage. Thus they have no incentive to deviate.
2. If the column player deviates, they would only do so in the first stage and gain no utility.

[2]

## Question 3

### (a) Defining the row/col best response polytopes

For a two player game $(A, B)\in{\mathbb{R}^{m\times n}_{>0}}^2$ the row/column player best response polytope $\mathcal{P}$/$\mathcal{Q}$ is defined by:

[1]

$$
\mathcal{P} = \left\{x\in\mathbb{R}^{m}\;|\;x\geq 0; xB\leq 1\right\}
$$

[1]

$$
\mathcal{Q} = \left\{y\in\mathbb{R}^{n}\;|\; Ay\leq 1; y\geq 0 \right\}
$$

[1]

### (b) Degeneracy of the game

The column player has two best responses to the first row thus the game is degenerate. [1]

### (c) Obtain half space definitions:

Directly apply the definition: [1]

Note that any modifications of $A, B$ are accepted (so as to make them $>0$).

For $\mathcal{P}$ :

$$
\begin{aligned}
x_1 &\geq 0\\
x_2 &\geq 0\\
20x_2 & \leq 1\\
7 x_1 + x_2 & \leq 1\\
7 x_1& \leq 1\\
\end{aligned}
$$

[2]

For $\mathcal{Q}$ :

$$
\begin{aligned}
3y_1 +6y_2 + y_3 & \leq 1\\
y_1 +2y_2 & \leq 1\\
y_1 &\geq 0\\
y_2 &\geq 0\\
y_3 &\geq 0\\
\end{aligned}
$$

[2]

### (d) Confirming labelling:

There are an infinite number of possible vertices (based on potential modifications of $A, B$). Thus it makes sense to consider the strategies that correspond to the normalised vertices given (recall the question is not asking to find the vertices):

For $\mathcal{P}$:

- $x=(0, 0)\text{ with labels: }\{0, 1\}$: $x_1=0$ and $x_2=0$
- $x=(1/7, 0)\rightarrow \sigma_r=(1, 0)\text{ with labels: }\{1, 3, 4\}$.  Label $1$ because $x_2=0$. Labels $\{3, 4\}$ because the 2nd and 3rd columns are best responses.
- $x=(0, 1/20)\rightarrow \sigma_r=(0, 1)\text{ with labels: }\{0, 2\}$: Label $0$ because $x_1=0$. Label $2$ because the first column is a best response.
- $x=(19/140, 1/20)\rightarrow \sigma_r=(19/26, 7/26)\text{ with labels: }\{2, 3\}$: we have $\sigma_r B = (70/13, 70/13, 133/26)$ thus columns 1 and 2 are best responses.

[2]

For $\mathcal{Q}$:

- $y=(0, 0, 0)\text{ with labels: }\{2, 3, 4\}$: $y_1=0$, $y_2=0$ and $y_3=0$
- $y=(0, 0, 1)\rightarrow \sigma_c=(0, 0, 1)\text{ with labels: }\{0, 2, 3\}$: 2, 3 is immediate, 0 because best response to 3rd column is first row.
- $y=(0, 1/6, 0)\rightarrow \sigma_c=(0, 1, 0)\text{ with labels: }\{0, 2, 4\}$: 2, 4 is immediate, 0 because best response to 2nd column is first row.
- $y=(1/3, 0, 0)\rightarrow \sigma_c=(1, 0, 0)\text{ with labels: }\{0, 3, 4\}$: 3, 4 is immediate, 0 because best response to 1st column is first row.

[2]

```python
import nashpy as nash

A = np.array([[3, 6, 1],
              [1, 2, 0]])
B = np.array([[0, 7, 7],
              [20, 1, 0]])

row_halfspaces = nash.polytope.build_halfspaces(B.transpose())
for vertex, labels in nash.polytope.non_trivial_vertices(row_halfspaces):
    print(np.round(vertex / sum(vertex), 4))
```

    [1. 0.]
    [0. 1.]
    [0.7308 0.2692]

```python
col_halfspaces = nash.polytope.build_halfspaces(A)
for vertex, labels in nash.polytope.non_trivial_vertices(col_halfspaces):
    print(np.round(vertex / sum(vertex), 4))
```

    [0. 0. 1.]
    [0. 1. 0.]
    [1. 0. 0.]

### (e) The vertex enumeration algorithm

For a nondegenerate 2 player game $(A, B)\in{\mathbb{R}^{m\times n}_{>0}}^2$ the following algorithm returns all nash equilibria:

1. For all pairs of vertices of the best response polytopes
2. Check if the vertices have full labels
3. Return the normalised probabilities

[2]

### (f) Use the vertex enumeration algorithm

The only vertex pairs with a full set of labels:

- $x=(1/7, 0)\text{ with labels: }\{1, 3, 4\}$ and $y=(0, 0, 1)\text{ with labels: }\{0, 2, 3\}$ [2]
- $x=(1/7, 0)\text{ with labels: }\{1, 3, 4\}$ and $y=(0, 1/6, 0)\text{ with labels: }\{0, 2, 4\}$ [2]

This corresponds to:

$$\{((1, 0), (0, 0, 1)), ((1, 0), (0, 1, 0))\}$$ [1]

```python
game = nash.Game(A, B)
for eq in game.vertex_enumeration():
    print([np.round(s, 2) for s in eq])
```

    [array([1., 0.]), array([0., 0., 1.])]
    [array([1., 0.]), array([0., 1., 0.])]

### (g) The L-H Algorithm

For a nondegenerate 2 player game $(A, B)\in{\mathbb{R}^{m\times n}_{>0}}^2$ the following algorithm returns a nash equilibria:

1. Start at the artificial equilibrium: $(0, 0)$
2. Choose a label to drop.
3. Remove this label from the corresponding vertex by traversing an edge of the corresponding polytope to another vertex.
4. The new vertex will now have a duplicate label in the other polytope. Remove this label from the vertex of the other polytope and traverse an edge of that polytope to another vertex.
5. Repeat step 4 until the pair of vertices is fully labelled.

### (h) Using the L-H Algorithm

Note that the game is degenerate but that we can still attempt to use the algorithm:

- $((0, 0), (0, 0, 0))$ have labels: $\{0, 1\}, \{2, 3, 4\}$. Drop 0 (arbitrary decision) in $\mathcal{P}$. [1]
- $\to ((1/7, 0), (0, 0, 0))$ have labels: $\{1, 3, 4\}, \{2, 3, 4\}$. In $\mathcal{Q}$ drop 3 (arbitrary decision, could have dropped 4). [1]
- $\to ((1/7, 0), (0, 1/6, 0))$ have labels: $\{1, 3, 4\}, \{0, 2, 4\}$. [1]

We have a fully labelled vertex pair (and one of the same equilibria as before).

There are numerous possible implimentations of this algorithm, here is another:

- $((0, 0), (0, 0, 0))$ have labels: $\{0, 1\}, \{2, 3, 4\}$. Drop 2 (arbitrary decision) in $\mathcal{Q}$.
- $\to ((0, 0), (1/3, 0, 0))$ have labels: $\{0, 1\}, \{0, 3, 4\}$. In $\mathcal{P}$ drop 0.
- $\to ((1/7, 0), (1/3, 0, 0))$ have labels: $\{1, 3, 4\}, \{0, 3, 4\}$. In $\mathcal{Q}$ drop 4 (arbitrary decision, could have dropped 3).
- $\to ((1/7, 0), (0, 0, 1))$ have labels $\{1, 3, 4\}, \{0, 2, 3\}$

We have a fully labelled vertex pair (and one of the same equilibria as before).

## Question 4

### (a) Definition of a Moran process on a game

Consider a matrix $A\in\mathbb{R}^{m\times n}$ representing a game with two strategies.

$$
A=
\begin{pmatrix}
    a & b\\
    c & d
\end{pmatrix}
$$

The Moran process is as follows:

- At a given time step: all individuals play all other individuals.
- Obtain their fitness as given by the game.
- Randomly select an individual proportional to their fitness as an individual to be reproduced
- Uniformly select an individual to be replaced
- Proceed to the next time step.
- The process terminates when there is only one type of individual in the population.

[4]

### (b) Theorem

Given a birth death process, the fixation probability $x_i$ is given by:

$$x_i=\frac{1+\sum_{j=1}^{i-1}\prod_{k=1}^j\gamma_k}{1+\sum_{j=1}^{N-1}\prod_{k=1}^j\gamma_k}$$

where:

$$
\gamma_k = \frac{p_{k,k-1}}{p_{k,k+1}}
$$

[2]

The Moran process is a birth death process where the transition probabilities are then given by:

$$p_{i,i+1}=\frac{if_{1i}}{if_{1i} + (N-i)f_{2i}}\frac{N-i}{N}$$
$$p_{i,i-1}=\frac{(N-i)f_{2i}}{if_{1i} + (N-i)f_{2i}}\frac{i}{N}$$

[2]

which gives:

$$\gamma_i=\frac{f_{2i}}{f_{1i}}$$

[1]

thus (using the general birth death process result):

$$
x_i=\frac{1+\sum_{j=1}^{i-1}\prod_{k=1}^j\frac{f_{2k}}{f_{1k}}}{1+\sum_{j=1}^{N-1}\prod_{k=1}^j\frac{f_{2k}}{f_{1k}}}
$$

[1]

### (c) Moran process for the game

Assuming $i$ individuals of the first type, for this game we have $N=3$ and $(a, b, c, d)=(0, 2, r, 0)$ the fitness of both types is given respectively by:

$$f_{1i}=\frac{a(i-1)+b(N-i)}{N-1}=\frac{6-2i}{2}=3-i$$
$$f_{2i}=\frac{c(i)+d(N-i-1)}{N-1}=\frac{ri}{2}=\frac{ri}{2}$$

which gives:

$$\gamma_i=\frac{f_{2i}}{f_{1i}}=\frac{ri}{6-2i}$$

thus:

$$
x_1=\frac{1}{1+\sum_{j=1}^{2}\prod_{k=1}^j\frac{rk}{6-2k}}=\frac{1}{1 + \frac{r}{4} + \frac{r}{4}\frac{2r}{2}}=\frac{1}{\frac{r^{2}}{4} + \frac{r}{4} + 1}
$$

for $r=2$ we get:

$$
x_1 = 2 / 5
$$

Some code to verify the result:

```python
def theoretic_fixation(N, game, i=1):
    """
    Calculate x_i as given by the above formula
    """
    f_ones = np.array([(game[0, 0] * (i - 1) + game[0, 1] * (N - i)) / (N - 1) for i in range(1, N)])
    f_twos = np.array([(game[1, 0] * i + game[1, 1] * (N - i - 1)) / (N - 1) for i in range(1, N)])
    gammas = f_twos / f_ones
    return (1 + np.sum(np.cumprod(gammas[:i-1]))) / (1 + np.sum(np.cumprod(gammas)))
```

```python
import sympy as sym
import numpy as np
r = sym.symbols("r")
game = np.array([[sym.S(0), sym.S(2)], [sym.S(r), sym.S(0)]])
x_1 = theoretic_fixation(N=3, game=game)
x_1, x_1.subs({r: 2})
```

    (1/(r**2/4 + r/4 + 1), 2/5)

### (d) Finding an $r$

Finding $r$ such that $x_1>.9$ corresponds to:

$$\frac{1}{\frac{r^{2}}{4} + \frac{r}{4} + 1}>9/10$$

which corresponds to:

$$
\begin{aligned}
\left(\frac{r^{2}}{4} + \frac{r}{4} + 1\right)9/10 & < 1 &&\text{[1]}\\
\left(\frac{r^{2}}{4} + \frac{r}{4} + 1\right) & < 10/9 && \text{[1]}\\
\frac{r^{2}}{4} + \frac{r}{4} - \frac{1}{9}& < 0 && \text{[1]} \\
r ^ 2 + r - \frac{4}{9}& < 0 && \text{[1]} \\
\end{aligned}$$

The roots of this polynomial are $-4/3, 1/3$ [2], thus: $r<1/3$ ensures $x_1>9/10$. [1]

To ensure a high probability of fixation we need the fitness of an individual of the second type encountering an individual of the first type to be at most $1/3$. [1]

```python
sym.solveset(r ** 2 + r  - sym.S(4) / 9, r)
```

    {-4/3, 1/3}


# Review of PK results for Erlotinib and Gefitinib from [[1]]

Reproduces the PKn modelling results from [[1](ref)] by incrementally reproducing

1. the structural model,
2. the error model,
3. the population model.

The parameters for the forward simulation are taken from [[1](ref)].

<a name="ref"> [1] </a> Eigenmann et. al., Combining Nonclinical Experiments with Translational PKPD Modeling to Differentiate Erlotinib and Gefitinib, Mol Cancer Ther (2016)

## 1. Structural model

The structural model according to [[1](ref)] consists of a one-compartmental PK model

\begin{align}
\frac{\text{d}A}{\text{d}t} &= - k_e A + \text{Dose}\\
C &= \frac{A}{V}.
\end{align}

Here
- $A$ is the drug amount in the blood plasma in $[$mg$]$,
- $C$ the drug concentration in the blood plasma in $[$mg/L$]$, and
- $k_e$ the drug elimination rate in $[$1/day$]$.

The Dose variable is representative for direct or indirect dose administration, i.e. bolus injection into the blood plasma or injection into a secondary dose compartment which is linearly connected to the blood plasma. In either case we will refer to the blood plasma as dose compartment. If the drug is administered indirectly we will refer to the secondary compartment as depot compartment.

The study investigated oral dosing strategies which can be modelled by an indirect drug administration
\begin{align}
\frac{\text{d}A_{d}}{\text{d}t} &= r_{\text{dose}} - k_a A_{d}\\
\text{Dose} &= k_a A_{d},
\end{align}

where
- $A_d$ is the drug amount in the depot compartment in $[$mg$]$,
- $r_{\text{dose}}$ the dose rate at which the dose is applied to the depot compartment in $[$mg/day$]$ and
- $k_a$ the absorption rate from the depot compartment to the blood plasma in $[$1/day$]$.

### 1.1 Build Structural Model

The structural model is constructed with myokit. For details please check the pkpd.model module.

```python
import myokit

from pkpd import model as m

# Build tumor growth inhibition model with default parameters
model = m.create_one_comp_pk_model()

# Validate model
model.validate()

# Check units
model.check_units(mode=myokit.UNIT_TOLERANT)

# Print model
print(model.code())
```

    [[model]]
    # Initial values
    central.amount = 0

    [central]
    dot(amount) = -k_e * amount
        in [mg]
    conc = amount / volume
        in [mg/L]
    k_e = 0
        in [1/day]
    time = 0 bind time
        in [day]
    volume = 1
        in [L]

### 1.2 Set oral administration

```python
# set route of administration
model.set_roa(dose_comp='central', indirect=True)

# Validate model
model.validate()

# Check units
model.check_units(mode=myokit.UNIT_TOLERANT)

# Print model
print(model.code())
```

    [[model]]
    # Initial values
    central.amount = 0
    depot.amount   = 0

    [central]
    dot(amount) = -k_e * amount + depot.k_a * depot.amount
        in [mg]
    conc = amount / volume
        in [mg/L]
    k_e = 0
        in [1/day]
    time = 0 bind time
        in [day]
    volume = 1
        in [L]

    [depot]
    dot(amount) = -k_a * amount + dose_rate * regimen
    dose_rate = 0
    k_a = 0
    regimen = 0 bind pace

### 1.3 Illustrate predicted mean plasma concentration for A677 patient-derived tumour explants

The intitial conditions and parameters are set according to the inferred values for A667 in Table 1 in [[1](ref)].

### 1.3.1 Treated tumour growth

We can model the distribution of Erlotinib in mice when treated with a daily oral dose of 25 mg/kg/day from day 2-15. For simplicity we just assume that the subject weighs 0.02kg.

#### 1.3.2.1 Set dosing regimen

```python
# define regimen
amount = 25 * 0.02  # mg (25 mg per kg)
duration = 0.001 # How long does it take to apply oral dose
start = 0 # start on the first day
period = 1 # dose every day
multiplier = 14 # apply the dose 14 times

model.set_regimen(
    amount=amount,
    duration=duration,
    start=start,
    period=period,
    multiplier=multiplier)

# Validate model
model.validate()

# Check units
model.check_units(mode=myokit.UNIT_TOLERANT)

# Print model
print(model.code())
```

    [[model]]
    # Initial values
    central.amount = 0
    depot.amount   = 0

    [central]
    dot(amount) = -k_e * amount + depot.k_a * depot.amount
        in [mg]
    conc = amount / volume
        in [mg/L]
    k_e = 3.87
        in [1/day]
    time = 0 bind time
        in [day]
    volume = 1.4
        in [L]

    [depot]
    dot(amount) = -k_a * amount + dose_rate * regimen
        in [mg]
    dose_rate = 500
    k_a = 55
        in [1/day]
    regimen = 0 bind pace

#### 1.3.2.2 Set PK parameters for Erlotinib and simulate

```python
# Set PK parameters
# Define state and parameter names
names = [
    'central.amount',
    'depot.amount',
    'central.k_e',
    'central.volume',
    'depot.k_a',
]

# Define intial conditions and values according to reference.
values = [
    0,  # central.amount
    0,  # depot.amount
    7.56,  # k_e
    0.127,  # volume
    55,  # k_a
]

# Define units of values
units = [
    'mg',  # central.amount
    'mg',  # depot.amount
    '1/day',  # k_e
    'L',  # volume
    '1/day',  # k_a
]

# set variables
model.set_values(names, values, units)

# Validate model
model.validate()

# Check units
model.check_units(mode=myokit.UNIT_TOLERANT)

# Run simulation for one day
_, regimen = model.dosing_regimen()
simulation = myokit.Simulation(model, regimen)
result = simulation.run(1)

# Get tumour volume - time curve
time_key = 'central.time'
conc_key = 'central.conc'

time_erlo = result[time_key]
conc_erlo = result[conc_key]
```

#### 1.3.2.3 Set PK parameters for Gefitinib and simulate

Note that in [[1](ref)] the volume of distribution $V$ was assumed to be dose dependent. We assume dose independent $V$.

```python
# Set PK parameters
# Define state and parameter names
names = [
    'central.amount',
    'depot.amount',
    'central.k_e',
    'central.volume',
    'depot.k_a',
]

# Define intial conditions and values according to reference.
values = [
    0,  # central.amount
    0,  # depot.amount
    3.87,  # k_e
    1.4,  # volume
    55,  # k_a
]

# Define units of values
units = [
    'mg',  # central.amount
    'mg',  # depot.amount
    '1/day',  # k_e
    'L',  # volume
    '1/day',  # k_a
]

# set variables
model.set_values(names, values, units)

# Validate model
model.validate()

# Check units
model.check_units(mode=myokit.UNIT_TOLERANT)

# Run simulation for one day
_, regimen = model.dosing_regimen()
simulation = myokit.Simulation(model, regimen)
result = simulation.run(1)

# Get tumour volume - time curve
time_key = 'central.time'
conc_key = 'central.conc'

time_gefi = result[time_key]
conc_gefi = result[conc_key]
```

#### 1.3.2.4 Illustrate treated tumour growth

```python
import matplotlib.pyplot as plt

# Illustrate time series
fig = plt.figure(figsize=(12, 6))

# Semi log plot
plt.semilogy(time_erlo, conc_erlo, label='Erlotinib')
plt.semilogy(time_gefi, conc_gefi, label='Gefitinib')

# Set x and y axis label
plt.xlabel('Time in [day]')
plt.ylabel('Plasma concentration in [mg/L]')

# Set y limits
plt.ylim([1E-3, 1E1])

# Create legend
plt.legend()

plt.show()
```

## 2. Error Model

In [[1](ref)] the discrepancy between the structural model and the observations through measurement noise and modelling assumptions was modelled by a combined error model

\begin{align}
V^{\text{obs}}_T-V_T \sim (a + b V_T) \varepsilon ,
\end{align}
where
- $V^{\text{obs}}_T$ is the observed tumour volume at the measured time points in $[$mm$^3]$,
- $V_T$ the predicted tumour volume at the different time points in $[$mm$^3]$,
- $a$ a real constant in $[$mm$^3]$,
- $b$ a dimensionless real constant, and
- $\varepsilon $ a dimensionless, standard normal distributed random variable, i.e. $\varepsilon \sim \mathcal{N}(0, 1)$.

In other words, the measured volume of the tumour at different time points can be predicted with

\begin{align}
V^{\text{pred}}_T= V_T + \text{Noise} ,
\end{align}

based on the deterministic predictions from the structural model $V_T$ and the expected deviations from measurement noise and modelling assumptions $\text{Noise}\sim (a + b V_T) \varepsilon$.

### 2.1 Illustrate error model

We can visualise the error model by showing how the error is expected to grow with the values of $V_T$, and by showing how the measurements are expected to scatter around the predictions of the structural model.

For illustration purposes, uncertainty in $a$ and $b$ is neglected for the moment and the mean values of $a$ and $b$ are taken from [[1](ref)].

#### 2.1.1 Structural model + noise versus structural model

```python
import numpy as np
import matplotlib.pyplot as plt

# a and b values from reference
a = 14.1  # in mm^3
b = 0.0907  # dimensionless

# Define span of relevant V_T values
volume_t = np.linspace(100, 1000)

# Predicted volume plus mean noise (=0)
volume_plus_mean_noise = volume_t + 0

# Predicted volume plus 1-sigma noise
one_sigma_noise = (a + b * volume_t) * 1

# Predicted volume plus 2-sigma noise
two_sigma_noise = (a + b * volume_t) * 2

# Predicted volume plus 3-sigma noise
three_sigma_noise = (a + b * volume_t) * 3

# Create loglog plot
fig = plt.figure(figsize=(12, 6))

# Structural model versus mean prediction
plt.plot(volume_t, volume_plus_mean_noise, linestyle='solid', color='black', label='mean')

# Shade 1-sigma interval (68.3%)
plt.fill_between(
    x=volume_t,
    y1=volume_t + one_sigma_noise,
    y2=volume_t - one_sigma_noise,
    color='gray',
    alpha=0.5,
    label='68.3%')

# Shade 2-sigma interval (95.4%)
plt.fill_between(
    x=volume_t,
    y1=volume_t + two_sigma_noise,
    y2=volume_t - two_sigma_noise,
    color='gray',
    alpha=0.3,
    label='95.4%')

# Shade 3-sigma interval (99.7%)
plt.fill_between(
    x=volume_t,
    y1=volume_t + three_sigma_noise,
    y2=volume_t - three_sigma_noise,
    color='gray',
    alpha=0.1,
    label='99.7%')

# Create x and y label
plt.xlabel(r'$V_T$ in [mm$^3$]')
plt.ylabel(r'$V^{pred}_T$ in [mm$^3$]')

# Create legend
plt.legend()

plt.show()
```

#### 2.1.2 Illustrate uncertainty in untreated tumour growth from measurement error and structural model assumptions

```python
# Same as 1.3.1.1
# Define state and parameter names
names = [
    'central.time',
    'central.volume_tumor',
    'central.kappa',
    'central.lambda_0',
    'central.lambda_1',
]

# Define intial conditions and values according to reference.
values = [
    0,  # time
    122,  # volume_tumor
    0,  # kappa (growth in absence of drug)
    0.0971,  # lambda_0
    127,  # lambda_1
]

# Define units of values
units = [
    'day',  # time
    'mm^3',  # volume_tumor
    'L/mg/day',  # kappa
    '1/day',  # lambda_0
    'mm^3/day',  # lambda_1
]

# set variables
model.set_values(names, values, units)

# Same as 1.3.2.3
# Run simulation for 30 days
simulation = myokit.Simulation(model)
result = simulation.run(30)

# Get tumour volume - time curve
time_key = 'central.time'
tumor_key = 'central.volume_tumor'

time = np.array(result[time_key])
volume = np.array(result[tumor_key])

# Compute noise intervals
a = 14.1  # in mm^3
b = 0.0907  # dimensionless
one_sigma_noise = (a + b * volume) * 1
two_sigma_noise = (a + b * volume) * 2
three_sigma_noise = (a + b * volume) * 3

# Illustrate time series
fig = plt.figure(figsize=(12, 6))

# semi log plot
plt.semilogy(time, volume, linestyle='solid', color='black', label='mean')

# Shade 1-sigma interval (68.3%)
plt.fill_between(
    x=time,
    y1=volume + one_sigma_noise,
    y2=volume - one_sigma_noise,
    color='gray',
    alpha=0.5,
    label='68.3%')

# Shade 2-sigma interval (95.4%)
plt.fill_between(
    x=time,
    y1=volume + two_sigma_noise,
    y2=volume - two_sigma_noise,
    color='gray',
    alpha=0.3,
    label='95.4%')

# Shade 3-sigma interval (99.7%)
plt.fill_between(
    x=time,
    y1=volume + three_sigma_noise,
    y2=volume - three_sigma_noise,
    color='gray',
    alpha=0.1,
    label='99.7%')

# set x and y axis label
plt.xlabel('%s in %s' % (time_key.split('.')[1], model.var(time_key).unit()))
plt.ylabel('%s in %s' % (tumor_key.split('.')[1], model.var(tumor_key).unit()))

# Create legend
plt.legend()

plt.show()
```

#### 2.1.2 Illustrate uncertainty in Erlotinib treated tumour growth from measurement error and structural model assumptions

```python
# Same as 1.3.2.1
# Define regimen
amount = 25 * 0.02  # mg (25 mg per kg)
duration = 0.001 # How long does it take to apply oral dose
start = 2 # start on the first day
period = 1 # dose every day
multiplier = 14 # apply the dose 14 times

model.set_regimen(
    amount=amount,
    duration=duration,
    start=start,
    period=period,
    multiplier=multiplier)

# Same as 1.3.2.2
# Set PK parameters
# Define state and parameter names
names = [
    'central.amount',
    'depot.amount',
    'central.k_e',
    'central.kappa',
    'central.volume',
    'depot.k_a',
]

# Define intial conditions and values according to reference.
values = [
    0,  # central.amount
    0,  # depot.amount
    7.56,  # k_e
    0.117,  # kappa (growth in absence of drug)
    0.127,  # volume
    55,  # k_a
]

# Define units of values
units = [
    'mg',  # central.amount
    'mg',  # depot.amount
    '1/day',  # k_e
    'L/mg/day',  # kappa
    'L',  # volume
    '1/day',  # k_a
]

# set variables
model.set_values(names, values, units)

# Validate model
model.validate()

# Check units
model.check_units(mode=myokit.UNIT_TOLERANT)

# Run simulation for 30 days
_, regimen = model.dosing_regimen()
simulation = myokit.Simulation(model, regimen)
result = simulation.run(30)

# Get tumour volume - time curve
time_key = 'central.time'
tumor_key = 'central.volume_tumor'

time = np.array(result[time_key])
volume = np.array(result[tumor_key])

# Compute noise intervals
a = 14.1  # in mm^3
b = 0.0907  # dimensionless
one_sigma_noise = (a + b * volume) * 1
two_sigma_noise = (a + b * volume) * 2
three_sigma_noise = (a + b * volume) * 3

# Illustrate time series
fig = plt.figure(figsize=(12, 6))

# semi log plot
plt.semilogy(time, volume, linestyle='solid', color='black', label='mean')

# Shade 1-sigma interval (68.3%)
plt.fill_between(
    x=time,
    y1=volume + one_sigma_noise,
    y2=volume - one_sigma_noise,
    color='gray',
    alpha=0.5,
    label='68.3%')

# Shade 2-sigma interval (95.4%)
plt.fill_between(
    x=time,
    y1=volume + two_sigma_noise,
    y2=volume - two_sigma_noise,
    color='gray',
    alpha=0.3,
    label='95.4%')

# Shade 3-sigma interval (99.7%)
plt.fill_between(
    x=time,
    y1=volume + three_sigma_noise,
    y2=volume - three_sigma_noise,
    color='gray',
    alpha=0.1,
    label='99.7%')

# set x and y axis label
plt.xlabel('%s in %s' % (time_key.split('.')[1], model.var(time_key).unit()))
plt.ylabel('%s in %s' % (tumor_key.split('.')[1], model.var(tumor_key).unit()))

# Create legend
plt.legend()

plt.show()
```

#### 2.1.3 Illustrate uncertainty in Gefitinib treated tumour growth from measurement error and structural model assumptions

```python
# Same as 1.3.2.1
# Define regimen
amount = 25 * 0.02  # mg (25 mg per kg)
duration = 0.001 # How long does it take to apply oral dose
start = 2 # start on the first day
period = 1 # dose every day
multiplier = 15 # apply the dose 14 times

model.set_regimen(
    amount=amount,
    duration=duration,
    start=start,
    period=period,
    multiplier=multiplier)

# Same as 1.3.2.3
# Set PK parameters
# Define state and parameter names
names = [
    'central.amount',
    'depot.amount',
    'central.k_e',
    'central.kappa',
    'central.volume',
    'depot.k_a',
]

# Define intial conditions and values according to reference.
values = [
    0,  # central.amount
    0,  # depot.amount
    3.87,  # k_e
    0.43,  # kappa (growth in absence of drug)
    1.4,  # volume
    55,  # k_a
]

# Define units of values
units = [
    'mg',  # central.amount
    'mg',  # depot.amount
    '1/day',  # k_e
    'L/mg/day',  # kappa
    'L',  # volume
    '1/day',  # k_a
]

# set variables
model.set_values(names, values, units)

# Validate model
model.validate()

# Check units
model.check_units(mode=myokit.UNIT_TOLERANT)

# Run simulation for 30 days
_, regimen = model.dosing_regimen()
simulation = myokit.Simulation(model, regimen)
result = simulation.run(30)

# Get tumour volume - time curve
time_key = 'central.time'
tumor_key = 'central.volume_tumor'

time = np.array(result[time_key])
volume = np.array(result[tumor_key])

# Compute noise intervals
a = 14.1  # in mm^3
b = 0.0907  # dimensionless
one_sigma_noise = (a + b * volume) * 1
two_sigma_noise = (a + b * volume) * 2
three_sigma_noise = (a + b * volume) * 3

# Illustrate time series
fig = plt.figure(figsize=(12, 6))

# semi log plot
plt.semilogy(time, volume, linestyle='solid', color='black', label='mean')

# Shade 1-sigma interval (68.3%)
plt.fill_between(
    x=time,
    y1=volume + one_sigma_noise,
    y2=volume - one_sigma_noise,
    color='gray',
    alpha=0.5,
    label='68.3%')

# Shade 2-sigma interval (95.4%)
plt.fill_between(
    x=time,
    y1=volume + two_sigma_noise,
    y2=volume - two_sigma_noise,
    color='gray',
    alpha=0.3,
    label='95.4%')

# Shade 3-sigma interval (99.7%)
plt.fill_between(
    x=time,
    y1=volume + three_sigma_noise,
    y2=volume - three_sigma_noise,
    color='gray',
    alpha=0.1,
    label='99.7%')

# set x and y axis label
plt.xlabel('%s in %s' % (time_key.split('.')[1], model.var(time_key).unit()))
plt.ylabel('%s in %s' % (tumor_key.split('.')[1], model.var(tumor_key).unit()))

# Create legend
plt.legend()

plt.show()
```

## 3. Population model

So far we have simulated the tumour growth for a fixed set of PK and PD parameters for Erlotinib and Gefitinib. This equivalent to assuming that Erlotinib and Gefinitib will affect all patients exactly the same, and that the only deviations of the observations from the predictions is due to measurement noise or some flaws of the structural model. However, it is well established that individuals react vastly differently to drugs.

In [[1](ref)] the inter-individual variation is addressed by assuming that the structural model parameters $V$, $k_e$, $\lambda_0$, $\lambda_1$, $\kappa $ and the inital tumour volume $V_{T,0}$ vary across individuals in a population (The absorption coefficient $k_a$ appears to have been fixed across indiviudals withoour further justification). In particular, all parameters $\psi = (V, k_e, \lambda_0, \lambda_1, \kappa, V_{T,0})$ were assumed to be normally distributed in the population
\begin{align}
\psi _i \sim \mathcal{N}(\bar{\psi }_i, \sigma _i).
\end{align}

As a result, the response of an individual patient can be simulated by drawing one realisation of $\psi $ from the distribution, solve the structural model and add uncertainty according to the error model. In fact, so far we have set $\psi = \bar{\psi }$ and simulated therefore a virtual *mean* patient of the population.

### 3.1 Illustrate uncertainty in tumour growth through inter-individual variability (assuming no measurement noise and an exact structural model)

Because the dependence of the tumour growth on the strutural parameters $\psi $ there is no straightforward analytical way to find the uncertainty intervals as before with the error model. So we will find the uncertainty approximately, by simulating the tumour growth for many virtual patients, and estimate the error bounds.

#### 3.1.1 Untreated tumour growth

#### 3.1.1.1 Set fixed intial conditions and parameters

```python
# Define state and parameter names
names = [
    'central.time',
    'central.kappa'
]

# Define intial conditions and values according to reference.
values = [
    0,  # time
    0  # kappa (growth in absence of drug)
]

# Define units of values
units = [
    'day',  # time
    'L/mg/day'  # kappa
]

# set variables
model.set_values(names, values, units)

# Validate model
model.validate()

# Check units
model.check_units(mode=myokit.UNIT_TOLERANT)
```

#### 3.1.1.2 Simulate $N$ individuals

```python
# Define population size
N = 1000

# Define mean and variance of structural model parameters
psi_mean = np.array([
    0.0971,  # lambda_0 in [1/day]
    127,  # lambda_1 in [mm^3/day]
    122,  # inital tumour volume in [mm^3]
])
#TODO: not clear whether variation is given relative or not
psi_var = np.array([
    [0.0456, 0, 0],  # lambda_0 in [1/day]  TODO: reduced std by factor 0.1 to what is given in table. Typo?
    [0, 0.710, 0], # lambda_1 in [mm^3/day]
    [0, 0, 0.368]  # inital tumour volume in [mm^3]
]) ** 2

# Define logging time
time = np.linspace(0, 30)

# Insantiate simulation
sim = myokit.Simulation(model)

# Instantiate result container
result = np.empty(shape=(N, len(time)))

# Solve structural model for individuals
for idn, n in enumerate(range(N)):
    # Reset simulation
    sim.reset()

    # draw individual from distribution
    psi = np.random.multivariate_normal(
        mean=psi_mean,
        cov=psi_var
    )

    # make sure parameters are positive
    psi = np.clip(psi, 0, None)

    # Set parameter values
    # Define state and parameter names
    names = [
        'central.lambda_0',
        'central.lambda_1'
    ]

    # Set parameters
    for idname, name in enumerate(names):
        sim.set_constant(name, psi[idname])

    # Set inital conditions
    sim.set_default_state([0.0, psi[2], 0.0])

    # Simulate
    result[idn, :] = np.array(sim.run(31, log=['central.volume_tumor'], log_times=time)['central.volume_tumor'])
```

#### 3.1.1.3 Illustrate variation of untreated tumour growth through interindividual variability

```python
# Illustrate time series
fig = plt.figure(figsize=(12, 6))

# Compute median growth
median = np.median(result, axis=0)

# semi log plot
plt.semilogy(time, median, linestyle='solid', color='black', label='median')

# Shade 1-sigma interval (68.3%),
# i.e. 0.159 to 0.841 precentile
p_left = 15.9
p_right = 84.1
plt.fill_between(
    x=time,
    y1=np.percentile(result, p_left, axis=0),
    y2=np.percentile(result, p_right, axis=0),
    color='gray',
    alpha=0.5,
    label='68.3%')

# # Shade 2-sigma interval (95.4%)
# i.e. 0. 023 to 0.977 precentile
p_left = 2.3
p_right = 97.7
plt.fill_between(
    x=time,
    y1=np.percentile(result, p_left, axis=0),
    y2=np.percentile(result, p_right, axis=0),
    color='gray',
    alpha=0.3,
    label='95.4%')

# # Shade 3-sigma interval (99.7%)
# i.e. 0. 013 to 0.987 precentile
p_left = 0.1
p_right = 99.9
plt.fill_between(
    x=time,
    y1=np.percentile(result, p_left, axis=0),
    y2=np.percentile(result, p_right, axis=0),
    color='gray',
    alpha=0.1,
    label='99.7%')

# set x and y axis label
plt.xlabel('Time [days]')
plt.ylabel(r'Volume tumour in [mm$^3$]')

# Create legend
plt.legend()

plt.show()
```

### 3.1.2 Erlotinib treated tumour growth

#### 3.1.2.1 Set fixed initial values and parameters

```python
# Same as 1.3.2.2
# Set PK parameters
# Define state and parameter names
names = [
    'central.amount',
    'depot.amount',
    'depot.k_a',
]

# Define intial conditions and values according to reference.
values = [
    0,  # central.amount
    0,  # depot.amount
    55,  # k_a
]

# Define units of values
units = [
    'mg',  # central.amount
    'mg',  # depot.amount
    '1/day',  # k_a
]

# set variables
model.set_values(names, values, units)

# Validate model
model.validate()

# Check units
model.check_units(mode=myokit.UNIT_TOLERANT)
```

#### 3.1.3.2 Medium dose

```python
# Same as 1.3.2.1
# Define regimen
amount = 25 * 0.02  # mg (25 mg per kg)
duration = 0.001 # How long does it take to apply oral dose
start = 2 # start on the first day
period = 1 # dose every day
multiplier = 14 # apply the dose 14 times

model.set_regimen(
    amount=amount,
    duration=duration,
    start=start,
    period=period,
    multiplier=multiplier)

# Define population size
N = 1000

# Define mean and variance of structural model parameters
psi_mean = np.array([
    0.127,  # volume in [L]
    7.56,  # k_e in [1/day]
    0.117
    0.0971,  # lambda_0 in [1/day]
    127,  # lambda_1 in [mm^3/day]
    122,  # inital tumour volume in [mm^3]
])
#TODO: not clear whether variation is given relative or not
psi_var = np.array([
    [0.0456, 0, 0],  # lambda_0 in [1/day]  TODO: reduced std by factor 0.1 to what is given in table. Typo?
    [0, 0.710, 0], # lambda_1 in [mm^3/day]
    [0, 0, 0.368]  # inital tumour volume in [mm^3]
]) ** 2

# Define logging time
time = np.linspace(0, 30)

# Insantiate simulation
sim = myokit.Simulation(model)

# Instantiate result container
result = np.empty(shape=(N, len(time)))

# Solve structural model for individuals
for idn, n in enumerate(range(N)):
    # Reset simulation
    sim.reset()

    # draw individual from distribution
    psi = np.random.multivariate_normal(
        mean=psi_mean,
        cov=psi_var
    )

    # make sure parameters are positive
    psi = np.clip(psi, 0, None)

    # Set parameter values
    # Define state and parameter names
    names = [
        'central.lambda_0',
        'central.lambda_1'
    ]

    # Set parameters
    for idname, name in enumerate(names):
        sim.set_constant(name, psi[idname])

    # Set inital conditions
    sim.set_default_state([0.0, psi[2], 0.0])

    # Simulate
    result[idn, :] = np.array(sim.run(31, log=['central.volume_tumor'], log_times=time)['central.volume_tumor'])

# Run simulation for 30 days
_, regimen = model.dosing_regimen()
simulation = myokit.Simulation(model, regimen)
result = simulation.run(30)

# Get tumour volume - time curve
time_key = 'central.time'
tumor_key = 'central.volume_tumor'

time = np.array(result[time_key])
volume = np.array(result[tumor_key])

# Compute noise intervals
a = 14.1  # in mm^3
b = 0.0907  # dimensionless
one_sigma_noise = (a + b * volume) * 1
two_sigma_noise = (a + b * volume) * 2
three_sigma_noise = (a + b * volume) * 3

# Illustrate time series
fig = plt.figure(figsize=(12, 6))

# semi log plot
plt.semilogy(time, volume, linestyle='solid', color='black', label='mean')

# Shade 1-sigma interval (68.3%)
plt.fill_between(
    x=time,
    y1=volume + one_sigma_noise,
    y2=volume - one_sigma_noise,
    color='gray',
    alpha=0.5,
    label='68.3%')

# Shade 2-sigma interval (95.4%)
plt.fill_between(
    x=time,
    y1=volume + two_sigma_noise,
    y2=volume - two_sigma_noise,
    color='gray',
    alpha=0.3,
    label='95.4%')

# Shade 3-sigma interval (99.7%)
plt.fill_between(
    x=time,
    y1=volume + three_sigma_noise,
    y2=volume - three_sigma_noise,
    color='gray',
    alpha=0.1,
    label='99.7%')

# set x and y axis label
plt.xlabel('%s in %s' % (time_key.split('.')[1], model.var(time_key).unit()))
plt.ylabel('%s in %s' % (tumor_key.split('.')[1], model.var(tumor_key).unit()))

# Create legend
plt.legend()

plt.show()
```

```python

```

### 3.1.2 Gefitinib treated tumour growth

```python

```

## 3.2 Final predictions

The final predictions for tumour growth in the population are now a composite of the strutural model, the distribution of structural model parameters in the population and the expected deviations of the observations from the model through measurement error and modelling assumptions. So assuming that we had infinite data and the population level parameters $\bar{\psi} $, $\sigma $ and $a$ and $b$ could be determined with hundert percent precision, this is the best we can do with modelling the population response to Erlotinib and Gefitinib. However, in [[1](ref)] the parameters were inferred from finite data sets, and therefore carry uncertainty as well. As a result, the model predictions carry uncertainty from from inter-individual variability, deviations from observatios and predictions, and uncertainty in the model parameters.

```python

```