# "LOO-CV on transformed data"
> Can different transformations of the data be compared with leave one out cross validation?

- toc: true
- author: Oriol Abril
- badges: true
- categories: [python, arviz, stan]
- tags: [model comparison]
- image: images/nb/loo_regression.png
- use_math: true

Blog post exploring whether or not LOO-CV can be used to compare models that try to explain some data $y$ with models trying to explain the same data after a transformation $z=f(y)$. Inspired by [@tiagocc question](https://discourse.mc-stan.org/t/very-simple-loo-question/9258) on Stan Forums. This post has two sections, the first one is the mathematical derivation of the equations used and their application on a validation example, and the second section is a real example. In addition to the LOO-CV usage examples and explanations, another goal of this notebook is to show and highlight the capabilities of [ArviZ](https://arviz-devs.github.io/arviz/).

```python
import pystan
import pandas as pd
import numpy as np
import arviz as az
import matplotlib.pyplot as plt
```

```python
az.style.use("arviz-darkgrid")
```

## Mathematical derivation and validation example
In the first example, we will compare two equivalent models:

1. $y \sim \text{LogNormal}(\mu, \sigma)$
2. $\log y \sim \text{Normal}(\mu, \sigma)$

### Model definition and execution
Define the data and execute the two models

```python
mu = 2
sigma = 1
logy = np.random.normal(loc=mu, scale=sigma, size=30)
y = np.exp(logy) # y will then be distributed as lognormal
data = {'N': len(y), 'y': y, 'logy': logy}
```

```python
#collapse-hide
lognormal_code = """
data {
  int<lower=0> N;
  vector[N] y;
}

parameters {
  real mu;
  real<lower=0> sigma;
}

model {
    y ~ lognormal(mu, sigma);
}

generated quantities {
    vector[N] log_lik;
    vector[N] y_hat;

    for (i in 1:N) {
        log_lik[i] = lognormal_lpdf(y[i] | mu, sigma);
        y_hat[i] = lognormal_rng(mu, sigma);
    }
}

"""
```

```python
sm_lognormal = pystan.StanModel(model_code=lognormal_code)
fit_lognormal = sm_lognormal.sampling(data=data, iter=1000, chains=4)
```

    INFO:pystan:COMPILING THE C++ CODE FOR MODEL anon_model_af04dcd0464f65fe0e5bbc595b4eb9d6 NOW.

```python
idata_lognormal = az.from_pystan(
    posterior=fit_lognormal,
    posterior_predictive='y_hat',
    observed_data=['y'],
    log_likelihood={'y': 'log_lik'},
)
```

```python
#collapse-hide
normal_on_log_code = """
data {
  int<lower=0> N;
  vector[N] logy;
}

parameters {
  real mu;
  real<lower=0> sigma;
}

model {
    logy ~ normal(mu, sigma);
}

generated quantities {
    vector[N] log_lik;
    vector[N] logy_hat;

    for (i in 1:N) {
        log_lik[i] = normal_lpdf(logy[i] | mu, sigma);
        logy_hat[i] = normal_rng(mu, sigma);
    }
}
"""
```

```python
sm_normal = pystan.StanModel(model_code=normal_on_log_code)
fit_normal = sm_normal.sampling(data=data, iter=1000, chains=4)
```

    INFO:pystan:COMPILING THE C++ CODE FOR MODEL anon_model_acd7c874588f1c862727f931f4dbf916 NOW.

```python
idata_normal = az.from_pystan(
    posterior=fit_normal,
    posterior_predictive='logy_hat',
    observed_data=['logy'],
    log_likelihood={'logy': 'log_lik'},
)
```

Check model convergence. Use `az.summary` to in one view that the effective sample size (ESS) is large enough and $\hat{R}$ is close to one.

```python
az.summary(idata_lognormal)
```

      mean
      sd
      hdi_3%
      hdi_97%
      mcse_mean
      mcse_sd
      ess_mean
      ess_sd
      ess_bulk
      ess_tail
      r_hat

      mu
      1.857
      0.210
      1.485
      2.259
      0.005
      0.004
      1533.0
      1526.0
      1535.0
      1483.0
      1.0

      sigma
      1.145
      0.164
      0.853
      1.451
      0.005
      0.004
      1103.0
      1046.0
      1195.0
      929.0
      1.0

```python
az.summary(idata_normal)
```

      mean
      sd
      hdi_3%
      hdi_97%
      mcse_mean
      mcse_sd
      ess_mean
      ess_sd
      ess_bulk
      ess_tail
      r_hat

      mu
      1.854
      0.205
      1.448
      2.222
      0.005
      0.004
      1456.0
      1456.0
      1446.0
      1187.0
      1.0

      sigma
      1.138
      0.155
      0.862
      1.420
      0.004
      0.003
      1196.0
      1070.0
      1312.0
      1154.0
      1.0

In addition, we can plot the quantile ESS plot for one of them directly with `plot_ess`

```python
az.plot_ess(idata_normal, kind="quantile", color="k");
```

### Posterior validation
Check that both models are equivalent and do indeed give the same result for both parameters.

```python
az.plot_density([idata_lognormal, idata_normal], data_labels=["Lognormal likelihood", "Normal likelihood"]);
```

### Calculate LOO-CV
Now we get to calculate LOO-CV using Pareto Smoothed Importance Sampling as detailed in Vehtari et al., 2017. As we explained above, both models are equivalent, but one is in terms of $y$ and the other in terms of $\log y$. Therefore, their likelihoods will be on different scales, and hence, their expected log predictive density will also be different.

```python
az.loo(idata_lognormal)
```

    Computed from 2000 by 30 log-likelihood matrix

             Estimate       SE
    elpd_loo  -102.50     7.28
    p_loo        1.80        -

```python
az.loo(idata_normal)
```

    Computed from 2000 by 30 log-likelihood matrix

             Estimate       SE
    elpd_loo   -46.57     3.46
    p_loo        1.64        -

We have found that as expected, the two models yield different results despite being actually the same model. This is because. LOO is estimated from the log likelihood, $\log p(y_i\mid\theta^s)$, being $i$ the observation id, and $s$ the MCMC sample id. Following Vehtari et al., 2017, this log likelihood is used to calculate the PSIS weights and to estimate the expected log pointwise predictive density in the following way:

1. Calculate raw importance weights: $r_i^s = \frac{1}{p(y_i\mid\theta^s)}$
2. Smooth the $r_i^s$ (see original paper for details) to get the PSIS weights $w_i^s$
3. Calculate elpd LOO as:

$$ \text{elpd}_{psis-loo} = \sum_{i=1}^n \log \left( \frac{\sum_s w_i^s p(y_i|\theta^s)}{\sum_s w_i^s} \right) $$

This will estimate the out of sample predictive fit of $y$ (where $y$ is the data of the model. Therefore, for the first model, using a LogNormal distribution, we are indeed calculating the desired quantity:

$$ \text{elpd}_{psis-loo}^{(1)} \approx \sum_{i=1}^n \log p(y_i|y_{-i}) $$

Whereas for the second model, we are calculating:

$$ \text{elpd}_{psis-loo}^{(2)} \approx \sum_{i=1}^n \log p(z_i|z_{-i}) $$

being $z_i = \log y_i$. We actually have two different probability density functions, one over $y$ which from here on we will note $p_y(y)$, and $p_z(z)$.

In order to estimate the elpd loo for $y$ from the data in the second model, $z$, we have to describe $p_y(y)$ as a function of $z$ and $p_z(z)$. We know that $y$ and $z$ are actually related, and we can use this relation to find how would the random variable $y$ (which is actually a transformation of the random variable $z$) be distributed. This is done with the Jacobian. Therefore:

$$
p_y(y|\theta)=p_z(z|\theta)|\frac{dz}{dy}|=\frac{1}{|y|}p_z(z|\theta)=e^{-z}p_z(z|\theta)
$$

In the log scale:

$$
\log p_y(y|\theta)=-z + \log p_z(z|\theta)
$$

We apply the results to the log likelihood data of the second model (the normal on the logarithm instead of the lognormal) and check that now the result does coincide with the LOO-CV estimated by the lognormal model.

```python
z = logy
idata_normal.log_likelihood["y"] = -z+idata_normal.log_likelihood.logy
```

```python
az.loo(idata_normal, var_name="y")
```

    Computed from 2000 by 30 log-likelihood matrix

             Estimate       SE
    elpd_loo  -102.30     7.26
    p_loo        1.64        -

## Real example

We will now use as data a subsample of a [real dataset](https://docs.google.com/spreadsheets/d/1gt1Dvi7AnQJiBb5vKaxanTis_sfd4sC4sVoswM1Fz7s/pub#). The subset has been selected using:

```python
df = pd.read_excel("indicator breast female incidence.xlsx").set_index("Breast Female Incidence").dropna(thresh=20).T
df.to_csv("indicator_breast_female_incidence.csv")
```

Below, the data is loaded and plotted for inspection.

```python
df = pd.read_csv("data/indicator_breast_female_incidence.csv", index_col=0)
df.plot(figsize=(9,5.5));
```

In order to show different examples of LOO on transformed data, we will take into account the following models:

$$
\begin{align}
&y=a_1 x+a_0 \\
&y=e^{b_0}e^{b_1 x}  &\rightarrow& \quad\log y = z_1 = b_1 x + b_0\\
&y=c_1^2 x^2 + 2 c_1 c_2 x + c_0^2  &\rightarrow& \quad\sqrt{y} = z_2 = c_1 x + c_0
\end{align}
$$

This models have been chosen mainly because of their simplicity. In addition, they can all be applied using the same Stan code and the data looks kind of linear. This will put the focus of the example on the loo calculation instead of on the model itself. For the online example, the data from Finland has been chosen, but feel free to download the notebook and experiment with it.

```python
y_data = df.Finland
z1_data = np.log(y_data)
z2_data = np.sqrt(y_data)
x_data = df.index/100 # rescale to set both to a similar scale
dict_y = {"N": len(x_data), "y": y_data, "x": x_data}
dict_z1 = {"N": len(x_data), "y": z1_data, "x": x_data}
dict_z2 = {"N": len(x_data), "y": z2_data, "x": x_data}
coords = {"year": x_data}
dims = {"y": ["year"], "log_likelihood": ["year"]}
```

```python
#collapse-hide
lr_code = """
data {
  int<lower=0> N;
  vector[N] x;
  vector[N] y;
}

parameters {
  real b0;
  real b1;
  real<lower=0> sigma_e;
}

model {
  b0 ~ normal(0, 20);
  b1 ~ normal(0, 20);
  for (i in 1:N) {
    y[i] ~ normal(b0 + b1 * x[i], sigma_e);
  }

}

generated quantities {
    vector[N] log_lik;
    vector[N] y_hat;
    for (i in 1:N) {
        log_lik[i] = normal_lpdf(y[i] | b0 + b1 * x[i], sigma_e);
        y_hat[i] = normal_rng(b0 + b1 * x[i], sigma_e);
    }
}
"""
```

```python
sm_lr = pystan.StanModel(model_code=lr_code)
control = {"max_treedepth": 15}
```

    INFO:pystan:COMPILING THE C++ CODE FOR MODEL anon_model_01dac21720941a01839c67cf2ac4a0fc NOW.

```python
fit_y = sm_lr.sampling(data=dict_y, iter=1500, chains=6, control=control)
```

```python
fit_z1 = sm_lr.sampling(data=dict_z1, iter=1500, chains=6, control=control)
```

```python
fit_z2 = sm_lr.sampling(data=dict_z2, iter=1500, chains=6, control=control)
```

```python
idata_y = az.from_pystan(
    posterior=fit_y,
    posterior_predictive='y_hat',
    observed_data=['y'],
    log_likelihood={'y': 'log_lik'},
    coords=coords,
    dims=dims,
)
idata_y.posterior = idata_y.posterior.rename({"b0": "a0", "b1": "a1"})
```

```python
idata_z1 = az.from_pystan(
    posterior=fit_z1,
    posterior_predictive='y_hat',
    observed_data=['y'],
    log_likelihood={'z1': 'log_lik'},
    coords=coords,
    dims=dims,
)
```

```python
idata_z2 = az.from_pystan(
    posterior=fit_z2,
    posterior_predictive='y_hat',
    observed_data=['y'],
    log_likelihood={'z2': 'log_lik'},
    coords=coords,
    dims=dims,
)
idata_z2.posterior = idata_z2.posterior.rename({"b0": "c0", "b1": "c1"})
```

In order to compare the out of sample predictive accuracy, we have to apply the Jacobian transformation to the 2 latter models, so that all of them are in terms of $y$.

Note: we will use LOO instead of Leave Future Out algorithm even though it may be more appropriate because the Jacobian transformation to be applied is the same in both cases. Moreover, PSIS-LOO does not require refitting, and it is already implemented in ArviZ.

The transformation to apply to the second model $z_1 = \log y$ is the same as the previous example:

```python
idata_z1.log_likelihood["y"] = -z1_data.values+idata_z1.log_likelihood.z1
```

In the case of the third model, $z_2 = \sqrt{y}$:

$$ |\frac{dz}{dy}| = |\frac{1}{2\sqrt{y}}| = \frac{1}{2 z_2} \quad \rightarrow \quad \log |\frac{dz}{dy}| = -\log (2 z_2)$$

```python
idata_z2.log_likelihood["y"] = -np.log(2*z2_data.values)+idata_z2.log_likelihood.z2
```

```python
az.loo(idata_y)
```

    Computed from 4500 by 46 log-likelihood matrix

             Estimate       SE
    elpd_loo  -194.26     3.83
    p_loo        1.58        -

```python
print("LOO before Jacobian transformation: {:.2f}".format(az.loo(idata_z1, var_name="z1").loo))
print(az.loo(idata_z1, var_name="y"))
```

    LOO before Jacobian transformation: 70.56
    Computed from 4500 by 46 log-likelihood matrix

             Estimate       SE
    elpd_loo  -100.44     4.47
    p_loo        3.14        -

```python
print("LOO before Jacobian transformation: {:.2f}".format(az.loo(idata_z2, var_name="z2").loo))
print(az.loo(idata_z2, var_name="y"))
```

    LOO before Jacobian transformation: 2.21
    Computed from 4500 by 46 log-likelihood matrix

             Estimate       SE
    elpd_loo  -115.17     3.81
    p_loo        2.92        -

## References
Vehtari, A., Gelman, A., and Gabry, J. (2017):  Practical Bayesian Model Evaluation Using Leave-One-OutCross-Validation and WAIC, _Statistics and Computing_, vol. 27(5), pp. 1413–1432.

---
Comments are not enabled for the blog, to inquiry further about the contents of the post, ask on [ArviZ Issues](https://github.com/arviz-devs/arviz/issues) or [Stan Discourse](https://discourse.mc-stan.org/)


## Causal analysis of the interference in ABM output
Change directory to one directory level up in `../output/`

```python
import os

def moveDir(currentDir, folderName):
    index = os.getcwd().find("python")
    return os.getcwd()[:39] + folderName

os.chdir(moveDir(os.getcwd, "output"))
os.getcwd()
```

    '/home/untik/IDEAprojects/ABM_CausalInf/output'

Select one file and explore it. What are the params defined for this SIM?

```python
import csv

filename = os.listdir()[3]

with open(filename, 'r') as f:
    reader = csv.reader(f)
    for row in reader:
        for element in row:
            print(element)
        break

print(filename)
```

    numPatients=1000
    probInfected=5.0E-4
    probVaccine=0.002
    lambda=0.3
    contagion=1.0
    infectiousness=2.0
    sexOnInfection=2.0
    sexOnVaccine=2.0
    vaccineOnInfection=2.0
    promiscuityPopulation=0.005
    maxPartnerForce=5.0
    randomForce=5.0
    partnerForce=3.0
    970328296.csv

Lets open the whole dataframe:

```python
import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv(filename, skiprows = 1)
df.head()
```

      step
      agent
      sex
      vaccine
      infection
      degree
      contagionDist
      infectiousnessDist
      indInterference

      0
      0
      Patient_0
      False
      False
      False
      3
      0.0
      0.000000
      0.0

      1
      0
      Patient_1
      True
      False
      False
      2
      0.0
      0.415625
      0.0

      2
      0
      Patient_2
      True
      False
      False
      4
      0.0
      0.158387
      0.0

      3
      0
      Patient_3
      True
      False
      False
      3
      0.0
      0.100014
      0.0

      4
      0
      Patient_4
      True
      False
      False
      3
      0.0
      0.131369
      0.0

What is the histogram of the `degree` column for the `step == 0`?

```python
df[df.step == 5].degree.hist(figsize = (10,6), color = 'b')
plt.ylabel('Counts')
plt.xlabel('Degree of nodes')
plt.show()
```

## 1. Reorganise the dataFrame
The infectiousness and contagion do not affect the outcome in their current time but the outcome in a posterior time.
1. Create new, empty columns in the dataFrame: priorContagionDist, priorInfectiousnessDist, and priorIndInterference
2. Fill new columns with prior values of: contagionDist, infectiousnessDist, indInterference

```python
newCols = ['priorContagionDist','priorInfectiousnessDist','priorIndInterference']

for i in newCols:
    df[i] = ""

df.head()
```

      step
      agent
      sex
      vaccine
      infection
      degree
      contagionDist
      infectiousnessDist
      indInterference
      priorContagionDist
      priorInfectiousnessDist
      priorIndInterference

      0
      0
      Patient_0
      False
      False
      False
      3
      0.0
      0.000000
      0.0

      1
      0
      Patient_1
      True
      False
      False
      2
      0.0
      0.415625
      0.0

      2
      0
      Patient_2
      True
      False
      False
      4
      0.0
      0.158387
      0.0

      3
      0
      Patient_3
      True
      False
      False
      3
      0.0
      0.100014
      0.0

      4
      0
      Patient_4
      True
      False
      False
      3
      0.0
      0.131369
      0.0

```python
%%time

i = 1
cols = ['contagionDist','infectiousnessDist','indInterference']

while i < len(df.step.value_counts()):
    for k in range(len(cols)):
        df.loc[df.step == i, newCols[k]] = df.loc[df.step == i-1, cols[k]].values
    i +=1
```

    CPU times: user 42.6 s, sys: 504 ms, total: 43.1 s
    Wall time: 21.7 s

## 2. TableOne
What is the Standard Mean Difference of each variable with regards to `vaccine`?

TableOne src = https://pypi.org/project/tableone/ with both parametric and non-parametric tests.

Documentation: https://media.readthedocs.org/pdf/tableone/latest/tableone.pdf

```python
from tableone import TableOne

columns = ['sex','vaccine','infection','degree',
           'contagionDist','infectiousnessDist','indInterference',
           'priorContagionDist','priorInfectiousnessDist','priorIndInterference',
          ]
categorical = ['sex','infection']
groupby = 'vaccine'

mytable = TableOne(df[df.step == 299], columns, categorical, groupby, pval = True)
mytable
```

      Grouped by vaccine

      isnull
      False
      True
      pval
      ptest

      variable
      level

      n

      430
      570

      sex
      False
      0
      297 (69.07)
      212 (37.19)
      0.000
      Chi-squared

      True

      133 (30.93)
      358 (62.81)

      infection
      False
      0
      277 (64.42)
      381 (66.84)
      0.464
      Chi-squared

      True

      153 (35.58)
      189 (33.16)

      degree

      0
      1.55 (0.72)
      1.56 (0.69)
      0.891
      Two Sample T-test

      contagionDist

      0
      11.49 (3.33)
      11.28 (3.51)
      0.347
      Two Sample T-test

      infectiousnessDist

      0
      18.85 (5.44)
      18.52 (5.78)
      0.351
      Two Sample T-test

      indInterference

      0
      -1.12 (0.66)
      -1.08 (0.69)
      0.353
      Two Sample T-test

      priorContagionDist

      0
      11.42 (3.31)
      11.23 (3.46)
      0.383
      Two Sample T-test

      priorInfectiousnessDist

      0
      18.68 (5.39)
      18.42 (5.70)
      0.460
      Two Sample T-test

      priorIndInterference

      0
      -1.10 (0.69)
      -1.12 (0.73)
      0.684
      Two Sample T-test

[1] Warning, Hartigans Dip Test reports possible multimodal distributions for: degree.
[2] Warning, test for normality reports non-normal distributions for: contagionDist, infectiousnessDist, priorContagionDist, priorIndInterference, priorInfectiousnessDist.

```python
categorical = ['sex','infection']
groupby = 'vaccine'
nonnormal = columns

mytable = TableOne(df[df.step == 299], columns, categorical, groupby, nonnormal, pval = True)
mytable
```

      Grouped by vaccine

      isnull
      False
      True
      pval
      ptest

      variable
      level

      n

      430
      570

      sex
      False
      0
      297 (69.07)
      212 (37.19)
      0.000
      Chi-squared

      True

      133 (30.93)
      358 (62.81)

      infection
      False
      0
      277 (64.42)
      381 (66.84)
      0.464
      Chi-squared

      True

      153 (35.58)
      189 (33.16)

      degree

      0
      2.00 [1.00,2.00]
      2.00 [1.00,2.00]
      0.781
      Kruskal-Wallis

      contagionDist

      0
      12.14 [9.08,14.35]
      11.96 [8.45,14.38]
      0.456
      Kruskal-Wallis

      infectiousnessDist

      0
      19.81 [14.93,22.93]
      19.28 [13.96,23.31]
      0.353
      Kruskal-Wallis

      indInterference

      0
      -1.06 [-1.62,-0.61]
      -1.04 [-1.58,-0.55]
      0.400
      Kruskal-Wallis

      priorContagionDist

      0
      12.07 [8.92,14.27]
      11.99 [8.44,14.24]
      0.519
      Kruskal-Wallis

      priorInfectiousnessDist

      0
      19.73 [14.57,22.85]
      19.35 [14.06,22.88]
      0.444
      Kruskal-Wallis

      priorIndInterference

      0
      -1.04 [-1.57,-0.53]
      -1.05 [-1.63,-0.52]
      0.770
      Kruskal-Wallis

[1] Warning, Hartigans Dip Test reports possible multimodal distributions for: degree.
[2] Warning, test for normality reports non-normal distributions for: contagionDist, infectiousnessDist, priorContagionDist, priorIndInterference, priorInfectiousnessDist.

## 3. Propensity score of confounders
What is the causal effect of the confounder, sex, on the treatment, vaccine?

Since sex looks like a confounder, it will be necessary to calculate a propersity score `ps` using a Logit regression in a IPTW method.

Statsmodels src = https://www.statsmodels.org/stable/index.html

Verbosity of `statsmodels`: https://stackoverflow.com/questions/23018306/how-to-silence-statsmodels-fit-in-python

```python
import statsmodels.api as sm

dat = df[df.step == 150]
model = sm.Logit(endog = dat.vaccine,
                  exog = sm.add_constant(dat[['sex']].astype(float)),
                 ).fit(disp = 0)
model.summary()
```

Logit Regression Results

  Dep. Variable:	      vaccine	       No. Observations:  	    1000

  Model:	               Logit	        Df Residuals:      	     998

  Method:	               MLE	         Df Model:          	       1

  Date:	          Tue, 26 Jun 2018	   Pseudo R-squ.:     	  0.04137

  Time:	              10:33:26	       Log-Likelihood:    	  -634.69

  converged:	           True	         LL-Null:           	  -662.07

   	                       	          LLR p-value:       	 1.352e-13

    	       coef	     std err	      z	      P>|z|	  [0.025	    0.975]

  const	    -1.0190	     0.100	   -10.149	  0.000	    -1.216	    -0.822

  sex	       0.9823	     0.135	     7.275	  0.000	     0.718	     1.247

Create a vector of weights `wt` based on the value of the vaccine and the propensity scores `ps` from the model:

```python
import numpy as np

def getWeights(vaccine, ps):
    w = []
    i = 0
    while i < len(ps):
        if vaccine[i]:
            w.append(1/ps[i])
        else:
            w.append(1/(1 - ps[i]))

        i +=1
    return w

wt = getWeights(vaccine = list(dat.vaccine), ps = model.predict())

# Which are the unique values?
for counter,value in enumerate(np.unique(wt)):
    print('Weight %d: %.5f' % (counter,value))
```

    Weight 0: 1.08298
    Weight 1: 1.17464
    Weight 2: 6.72603
    Weight 3: 13.05128

How can I see the weighted data now? TableOne with weights for Python?

src = https://stackoverflow.com/questions/17689099/using-describe-with-weighted-data-mean-standard-deviation-median-quantil nope

 CANT FIND ANYTHING!

## 4. Estimate Confounder Causal Effect
The causal effect of `sex` on `vaccine` has to be more or less constant though the simulation (look at parameters above) but it is a stochastic simulation, how are the histograms?

A recall from Odds Ratio, probabilities, and Logit regression: https://www.youtube.com/watch?v=8nm0G-1uJzA
 - What if I forget to include the `add_constant` in `statsmodels`?
 - Which implementation is faster? `statsmodels` or `sklearn`?

```python
i = 10
sex = []
sexCnt = []
sexTime = []
numSteps = 299

while i < numSteps:
    dat = df[df.step == i]
    depvar = ['sex']

    try:
        results = sm.Logit(endog = dat.vaccine, exog = dat[depvar].astype(float)).fit(disp = 0)
        resultsCnt = sm.Logit(endog = dat.vaccine, exog = sm.add_constant(dat[depvar].astype(float))).fit(disp = 0)
    except:
        print('Not enough vaccinated agents to calculate')

    sex.append([results.params[0], resultsCnt.params[1]])
    sexTime.append([i,resultsCnt.params[1]])
    i +=1
```

How the Odds Ratio varies thoughout the simulation?

```python
i = 0
ratio = []

while i < numSteps:
    sexVaccine = sum(df.loc[df.step == i, ['sex']].values * df.loc[df.step == i, ['vaccine']].values)[0]
    NoSexVaccine = sum(np.invert(df.loc[df.step == i, ['sex']].values) * df.loc[df.step == i, ['vaccine']].values)[0]

    try:
        ratio.append([i, sexVaccine/NoSexVaccine])
        i +=1
    except:
        print('No enough sex 0 with vaccine')
        i +=1

draw2 = pd.DataFrame(sexTime, columns = ['step','coef'])
draw3 = pd.DataFrame(ratio, columns = ['step','ratio'])

figsize = (25,4)

plt.figure(figsize=figsize)
plt.scatter(draw2.step, np.exp(draw2.coef), color = 'b', label = 'Measured OR')
plt.axhline(2, color = 'green', label = 'True causal effect')
plt.xlabel('Simulation step')
plt.ylabel('OR')
plt.ylim(0,8)
plt.legend()
plt.show()

plt.figure(figsize=figsize)
plt.scatter(draw3.step, draw3.ratio, color = 'r', label = 'Measured ratio')
plt.axhline(2, color = 'green', label = 'True causal effect')
plt.xlabel('Simulation step')
plt.ylabel('VaccineANDsex1 / vaccineANDsex0')
plt.ylim(0,8)
plt.legend()
plt.show()
```

What is the estimated causal effect `sexOnVaccine` and the CI?

```python
import scipy as sp
import scipy.stats

def meanCI(data, confidence=0.975):
    a = 1.0*np.array(data)
    n = len(a)
    m, se = np.mean(a), scipy.stats.sem(a)
    h = se * sp.stats.t._ppf((1+confidence)/2., n-1)
    return m-h, m, m+h

print('\nCI and mean for OR:')
print(meanCI(np.exp(draw2.coef)))
print('\nCI and mean for ratio:')
print(meanCI(draw3.ratio))
```

    CI and mean for OR:
    (2.761886321111088, 2.834629130705034, 2.90737194029898)

    CI and mean for ratio:
    (1.775136009730059, 1.8024799646508571, 1.8298239195716552)

## 5. Estimator of remaining causal effects
Now I will create a weighted model, without confounding, to predict the infection at each step of the simulation:

\begin{align}
\ Infection = Vaccine + Sex + priorInfectiousnessDist + priorContagionDist + infectiousnessDist + contagionDist \\
\end{align}

Source for Jupyter formulas: http://jupyter-notebook.readthedocs.io/en/stable/examples/Notebook/Typesetting%20Equations.html

```python
dat = df[df.step == 100]

model = sm.Logit(endog = dat.vaccine,
                 exog = sm.add_constant(dat[['sex']].astype(float)),
                 ).fit(disp = 0)

wt = getWeights(vaccine = list(dat.vaccine), ps = model.predict())

# Which are the unique values?
for counter,value in enumerate(np.unique(wt)):
    print('Weight %d: %.5f' % (counter,value))
```

    Weight 0: 1.23544
    Weight 1: 1.55380
    Weight 2: 2.80571
    Weight 3: 5.24742

```python
dat = df[df.step == 299]

model = sm.Logit(endog = dat.vaccine,
                 exog = sm.add_constant(dat[['sex']].astype(float)),
                 ).fit(disp = 0)

wt = getWeights(vaccine = list(dat.vaccine), ps = model.predict())

# Which are the unique values?
for counter,value in enumerate(np.unique(wt)):
    print('Weight %d: %.5f' % (counter,value))
```

    Weight 0: 1.37151
    Weight 1: 1.71380
    Weight 2: 2.40094
    Weight 3: 3.69173

```python
model.summary()
```

Logit Regression Results

  Dep. Variable:	      vaccine	       No. Observations:  	    1000

  Model:	               Logit	        Df Residuals:      	     998

  Method:	               MLE	         Df Model:          	       1

  Date:	          Tue, 26 Jun 2018	   Pseudo R-squ.:     	  0.07438

  Time:	              10:43:01	       Log-Likelihood:    	  -632.49

  converged:	           True	         LL-Null:           	  -683.31

   	                       	          LLR p-value:       	 6.613e-24

    	       coef	     std err	      z	      P>|z|	  [0.025	    0.975]

  const	    -0.3371	     0.090	    -3.750	  0.000	    -0.513	    -0.161

  sex	       1.3273	     0.136	     9.786	  0.000	     1.061	     1.593

Why do I get different results for:
 -  `SGDClassifier` from http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.SGDClassifier.html
 -  `LogisticRegression` from http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html

__Solution__: default regularisation term and shuffling. In SGDC is `alpha` that is the inverse of `C` in LogitReg

```python
from sklearn.linear_model import SGDClassifier as logit1
from sklearn.linear_model import LogisticRegression as logit2

coef = []
coef2 = []
for i in range(1000):
    logit_1 = logit1(loss = 'log', penalty='none', alpha = 1e-2, fit_intercept = True, max_iter = 1e5, tol = 1e-2, random_state=i, average=False,)
    logit_1.fit(dat.sex.values.reshape(-1, 1), dat.vaccine)
    coef.append(logit_1.coef_[0][0])

    logit_2 = logit2(fit_intercept = True, max_iter = 1e5, tol = 1e-2, random_state=i, C=100.0)
    logit_2.fit(dat.sex.values.reshape(-1, 1), dat.vaccine)
    coef2.append(logit_2.coef_[0][0])

print("SGDC:  ", meanCI(coef))
print("Logit: ", meanCI(coef2))
```

    SGDC:   (1.3351631390077485, 1.340177005339336, 1.3451908716709235)
    Logit:  (1.325881031308711, 1.325881031308711, 1.325881031308711)

```python

```


# Линейная регрессия

При изучении задач машинного обучения удобно отталкиваться от реальных данных и реальных бизнес проблем. В качестве задачи для обсуждения метода линейной регрессии мы рассмотрим сильно учененный набор данных из конкурса kaggle [House Sales in King County, USA](https://www.kaggle.com/harlfoxem/housesalesprediction).

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

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

Воспользуемся для заргузки данных из CSV файла библиотекой pandas.

```python
import pandas as pd

housesales = pd.read_csv('../datasets/kc_house_data_reduced.csv')
housesales['price'] = housesales['price']/100000
housesales['sqft_living'] = housesales['sqft_living']/1000
```

## Исследование данных

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

```python
%pylab inline
import seaborn as sns  # для красивого стиля графиков
#from matplotlib.ticker import FuncFormatter # для форматирования разметки по оси y
ax = housesales.plot.scatter(x='sqft_living', y='price')
ax.set_xlabel('Жилая площадь, 1k футы$^2$')
ax.set_ylabel('Цена, 100k $')
```

Мы видим, что точек не очень много и между жилой площадью и ценой определенно нет простой зависимости. При этом вполне логично, что есть характерная закономерность: чем больше жилая площадь, тем больше цена. В качестве более сильного предположения можно выдвинуть следующее. Если площадь увеличивается в 2 раза, то и цена увеличивается примерно в 2 раза. Это означает, что мы можем попробовать описать наблюдаемую *зависимость* как *прямо пропорциональную*, и, что еще важнее, как *линейную*.

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

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

Например, для площади 2000 квадратных футов цена примерно может составить 500,000$.

```python
ax = housesales.plot.scatter(x='sqft_living', y='price')
ax.set_xlabel('Жилая площадь, 1k футы$^2$')
ax.set_ylabel('Цена, 100k $')
ax.axvline(x=2, ymax=0.45, color='green', linestyle='--')
ax.axhline(y=5, xmax=0.5, color='red', linestyle='--')
scatter(2, 5, color='red', marker='x', s=40, linewidth=3)
```

## Признаки и целевая переменная

Представим наши данные в табличном виде

```python
housesales.head(10)
```

      sqft_living
      price

      0
      1.180
      2.2190

      1
      2.570
      5.3800

      2
      0.770
      1.8000

      3
      1.960
      6.0400

      4
      1.680
      5.1000

      5
      1.715
      2.5750

      6
      1.060
      2.9185

      7
      1.780
      2.2950

      8
      1.890
      3.2300

      9
      1.160
      4.6800

Каждоя строка состоит из двух чисел: площадь дома и его цена. Обозначим число строка как $n$

```python
n = len(housesales)
n
```

    30

Будем называть признаками те параметры, которые нам будут в задаче даны и целевыми переменными те, которые требуется определить. В данном случае у нас один признак - площадь дома и одна целевая переменная - цена. Каждую строку таблицы будем называть объектом.

Обозначим за $x^{(i)}$ - значение признака для $i$-го объекта, а за $y^{(i)}$ - значение целевой переменной для $i$-го объекта. Индекс $i$ пробегает значения от $1$ до $n$.

Пару $(x^{(i)}, y^{(i)})$ будем называть элементом обучающей выборки. A весь набор таких пар обучающей выборкой и обозначать ее как $(X, Y)$.

Следующим важным элементом является функция $h_\theta(x)$, которую называют гипотезой или моделью. В данной задаче, если на вход этой функции подать площадь дома, то на выходе должны быть вычислена примерная стоимость.

Можно грубо сказать, что все методы машинного обучения направленны на решение одного вопроса: как найти функцию $h$?

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

**TODO**: вставить схему

## Линейная регрессия

В методе линейной регресии предлагается представить гипотезу в виде
$$
h_\theta(x) = \theta_0 + \theta_1 x.
$$

Здесь $\theta_0$ и $\theta_1$ - неизвестные вещественнозначные параметры, которые нам предстоит определить (выучить).

Рассмотрим несколько примеров для различных параметров $\theta_i$. Для простоты попложим $\theta_0= 0$.

```python
ax = housesales.plot.scatter(x='sqft_living', y='price')
ax.set_xlabel('Жилая площадь, 1k футы$^2$')
ax.set_ylabel('Цена, 100k $')

xx = np.linspace(1, 3, 2)
plot(xx, 1 * xx, label='$\\theta_1 = 1$')
plot(xx, 2 * xx, label='$\\theta_1 = 2$')
plot(xx, 3 * xx, label='$\\theta_1 = 3$')
ax.legend(loc=4)
```

## Функция потерь

Чтобы выбрать параметры $\theta_i$ нам необходим числовой критерий $L(\theta_0, \theta_1)$. Допустим мы хотим придумать такой критерий, чтобы чем его значение меньше, тем лучшие значения параметров гипотезы мы выбрали. При этом чтобы этот критейри не принимал отрицательных значений. Это означает, что если критерий равен нулю, то мы считаем, что выбрали наилучшие из возможных значений параметров.

В методе линейно регресси в качестве такого критерий предлагается рассмотреть среднее значение квадрата ошибки.
$$
L(\theta_0, \theta_1) = \frac{1}{2n} \sum\limits_{i=1}^{n}\left(h_\theta(x^{(i)}) - y^{(i)}\right)^2.
$$

$L$ также называют функцией потерь.

## Интуиция за функцией потерь

Чтобы пролить свет на выбор имеено такого вида функции $L$ рассмотрим упрощенный пример задачи регрессии. Обучающая выборка представлена на рисунке, а гипотеза $h$ имеет упрощенный вид
$$
h_\theta(x) = \theta_1 x
$$

```python
# Обучающая выборка
x = np.array([1, 2, 3])
y = np.array([1, 2, 3])

# Будем строить график h(x) и L(theta)
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 5))

# Отметим на левом графике обучающую выборку с помощью зеленых крестиков
ax1.scatter(x, y, s=100, linewidth=3, marker='x', color='green')

# И построим график гипотезы при трех различных значениях theta
theta_1 = 0.75
theta_2 = 1
theta_3 = 1.3

xx = np.linspace(0.5, 3.5, 10)
ax1.plot(xx, theta_1*xx, color='red', label='$\\theta_1={}$'.format(theta_1))
ax1.plot(xx, theta_2*xx, color='blue', label='$\\theta_1={}$'.format(theta_2))
ax1.plot(xx, theta_3*xx, color='green', label='$\\theta_1={}$'.format(theta_3))
ax1.legend(loc=4)
ax1.set_xlim((0, 3.5))
ax1.set_ylim((0, 3.5))
ax1.set_xlabel('$x$')
ax1.set_ylabel('$y$')

# Определим функцию L для вычисления функции потерь
def L(x, y, theta_1):
    n = len(x)
    return 1/(2*n)*np.sum((theta_1*x - y)**2)

# И построим график функции потерь в зависимости от различных значений параметра theta_1
ax2.scatter(theta_1, L(x, y, theta_1), color='red', s=100)
ax2.scatter(theta_2, L(x, y, theta_2), color='blue', s=100)
ax2.scatter(theta_3, L(x, y, theta_3), color='green', s=100)
theta = np.linspace(0.6, 1.4, 20)
L_theta = [L(x, y, theta_1) for theta_1 in theta]
_ = ax2.plot(theta, L_theta)
ax2.set_xlabel('$\\theta$')
ax2.set_ylabel('$L(\\theta)$')
```

Как можно увидеть из правого графика, благодря тому, что функция $L$ имеет квадратичных вид, то у нее есть ровно один минимум, которых автоматически является глобальным. В данном примере этот минимум достугается в точке $\theta_1 = 1$.

## Метод градиентного спука

Будучи уверены, что функция $L$ имеет ровно один минимум мы можем сформулировать задачу поиска наилучшей гипотезы как задачу оптимизации, в которой необходимо минимизировать функцию потерь $L$ по параметрам гипотезы $\theta_0, \theta_1$.

\begin{equation}
\theta_0^*, \theta_1^* = \underset{\theta_0, \theta_1 \in \mathbb{R}}{\mathrm{argmin}} L(\theta_0, \theta_1).
\end{equation}

В данной поставноке мы должны минимизировать функцию сразу по двум переменным. Построим на основе нашей обучающей выборки поверхность $L(\theta_0, \theta_1)$.

```python
def L(x, y, theta_0, theta_1):
    n = len(x)
    h = theta_0 + theta_1*x
    return 1/(2*n)*np.sum((h - y)**2)

X = housesales['sqft_living']
y = housesales['price']

from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')

theta_0 = np.linspace(-30, 30, 100)
theta_1 = np.linspace(-10, 10, 100)
L_theta = np.array([[L(X, y, theta_0i, theta_1j) for theta_0i in theta_0] for theta_1j in theta_1])
theta_0v, theta_1v = np.meshgrid(theta_0, theta_1)

ax.plot_surface(theta_0v, theta_1v, L_theta)
plt.xlabel('$\\theta_0$')
plt.ylabel('$\\theta_1$')
plt.title('Поверхность функции потерь $L(\\theta_0, \\theta_1)$')
```

### Линии уровеня
Для визуального анализа график поверхности не всегда является удобным способом представления функции потерь. Для случая функции 2-х переменных удобно рассмотреть ее графическое представление в виде линий уровня - уривых, на которых функция принимает одинаковое значение.

```python
levels = [2, 5, 10, 50, 100]
CS = plt.contour(theta_0v, theta_1v, L_theta, levels, colors='k')
plt.clabel(CS, inline=1, fontsize=10, colors='k')
plt.xlabel('$\\theta_0$')
plt.ylabel('$\\theta_1$')
plt.title('Линии уровня функции потерь $L(\\theta_0, \\theta_1)$')
```

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

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

$$
grad~ L = \left(\frac{\partial L}{\partial \theta_0}, \frac{\partial L}{\partial \theta_1}\right).
$$

Для пояснение того, как метод градиентного спуска позволяет найти минимум функции, мы вернемся к одномерному случаю, когда зафиксируем $\theta_0 = 0$ и будем рассматривать зависимость функции потерь $L$ только от $\theta_1$. График этой зависимости для задачи оценки стоимост жилья приведен на рисунке ниже.

Суть метода градиентного спуска заключается в следующем. На каждом шаге мы обновляем оценку для аргумента минимума $\theta_1^{(k)}$, начиная с некоторого начального значения $\theta_1^{(0)}$ по правилу
$$
\theta_1^{(k + 1)} = \theta_1^{(k)} - \alpha \frac{\partial L}{\partial \theta_1}(\theta_1^{(k)}).
$$

Здесь $\alpha$ - параметр, который разывают скоростью обучения (learning rate). Удачный выбор значения $\alpha$ может полохительно сказаться на числе шагов алгоритма, которые необходимы для приближения к значения минимума.

Попробуем с помощью визуализации пояснить суть работы алгоритма.

Пусть $\theta_1^{(0)} = 8$. Поскольку производная в точке описывает тангенс угла наклона касательной к функции в этой точке, то как видно из рисунка, производная будет иметь положительный знак
$$
\frac{\partial L}{\partial \theta_1}(\theta_1^{(0)}) > 0.
$$

А следовательно, производная взятая с отрицательным знаком будет направлена в сторону уменьшения значения функции $L$ и это действительно то направление, в котором следует искать минимум.

```python
theta_1 = np.linspace(-5, 9, 100)
L_theta_1 = [L(X, y, 0, theta_1i) for theta_1i in theta_1]
plot(theta_1, L_theta_1)
theta1_0 = 8
scatter(theta1_0, L(X, y, 0, theta1_0), s=50)

def gradL(X, y, theta_0, theta_1):
    n = len(X)
    h = theta_0 + theta_1*X.values
    return 1/(n)*np.sum(X.transpose()@(h - y.values))

arrow(theta1_0, L(X, y, 0, theta1_0), -1, -gradL(X, y, 0, theta1_0), linewidth=2, head_width=0.5, head_length=2, fc='k', ec='k')
vlines(x=theta1_0, ymin=-20, ymax=L(X, y, 0, theta1_0), color='green', alpha=0.3, linestyles='--')
vlines(x=theta1_0 - 1, ymin=-20, ymax=L(X, y, 0, theta1_0 -1), color='green', alpha=0.3, linestyles='--')
ylim(-20, 100)
xlabel('$\\theta_1$')
ylabel('$L(0, \\theta_1)$')
text(x=theta1_0-0.45, y = -35, text='$\\theta_1^{(0)}$', s=50)
text(x=theta1_0-1.45, y = -35, text='$\\theta_1^{(1)}$', s=50)
```

Рассмотрим другую ситуацию. Пусть $\theta_1^{(0)} = -2$, тогда производная в этой точке будет иметь отрицательный знак
$$
\frac{\partial L}{\partial \theta_1}(\theta_1^{(0)}) < 0.
$$
А следовательно взятая с обратным знаком будет подталкивать значение $\theta_1^{(i)}$ вправо, что в таком случае и требуется.

```python
plot(theta_1, L_theta_1)
theta1_0 = -2
scatter(theta1_0, L(X, y, 0, theta1_0), s=50)

def gradL(X, y, theta_0, theta_1):
    n = len(X)
    h = theta_0 + theta_1*X.values
    return 1/(n)*np.sum(X.transpose()@(h - y.values))

arrow(theta1_0, L(X, y, 0, theta1_0), 1, gradL(X, y, 0, theta1_0), linewidth=2, head_width=0.5, head_length=2, fc='k', ec='k')
vlines(x=theta1_0, ymin=-20, ymax=L(X, y, 0, theta1_0), color='green', alpha=0.3, linestyles='--')
vlines(x=theta1_0 + 1, ymin=-20, ymax=L(X, y, 0, theta1_0 +1), color='green', alpha=0.3, linestyles='--')
ylim(-20, 100)
xlabel('$\\theta_1$')
ylabel('$L(0, \\theta_1)$')
text(x=theta1_0-0.45, y = -35, text='$\\theta_1^{(0)}$', s=50)
text(x=theta1_0+0.55, y = -35, text='$\\theta_1^{(1)}$', s=50)
```

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

```python
plot(theta_1, L_theta_1)
theta1_0 = -2
scatter(theta1_0, L(X, y, 0, theta1_0), s=50)

alpha = 0.1
for i in range(8):
    arrow(theta1_0, L(X, y, 0, theta1_0),
          -alpha*gradL(X, y, 0, theta1_0), -alpha*gradL(X, y, 0, theta1_0)**2,
          linewidth=2, head_width=0.5, head_length=2, fc='k', ec='k')
    vlines(x=theta1_0, ymin=-20, ymax=L(X, y, 0, theta1_0), color='green', alpha=0.3, linestyles='--')
    text(x=theta1_0-0.45, y = -35, text='$\\theta_1^{(%d)}$' % i, s=50)
    theta1_0 += -alpha*gradL(X, y, 0, theta1_0)
vlines(x=theta1_0, ymin=-20, ymax=L(X, y, 0, theta1_0), color='green', alpha=0.3, linestyles='--')
ylim(-20, 100)
xlabel('$\\theta_1$')
ylabel('$L(0, \\theta_1)$')
title('$\\alpha = {}$'.format(alpha))
```

Отметим следующее наблюдение, которое хорошо видно на графике выше. Чем ближе мы к минимуму, тем меньше изменяется значение параметра $\theta_1^{(i)}$ и это при том, что скорость обучения постоянна $\alpha = const$. Объяснить этот эффект можно следующим образом. В точке экстремума касательная к функции горизонтальна и проивзодная равна нулю. Следовательно, чем ближе мы к точке оптимума, тем меньше обсолютное значение производной и тем меньше изменяется параметр $\theta_1^{(i)}$. В некоторых случаях это может создавать проблемы с необходимостью использовать очень большое число итераций для достижения результата. В этом случае следует увеличть скорость обучения обучения $\alpha$.

```python
plot(theta_1, L_theta_1)
theta1_0 = -2
scatter(theta1_0, L(X, y, 0, theta1_0), s=50)

alpha = 0.2
for i in range(3):
    arrow(theta1_0, L(X, y, 0, theta1_0),
          -alpha*gradL(X, y, 0, theta1_0), L(X, y, 0, theta1_0 - alpha*gradL(X, y, 0, theta1_0)) - L(X, y, 0, theta1_0),
          linewidth=2, head_width=0.5, head_length=2, fc='k', ec='k')
    vlines(x=theta1_0, ymin=-20, ymax=L(X, y, 0, theta1_0), color='green', alpha=0.3, linestyles='--')
    text(x=theta1_0-0.45, y = -35, text='$\\theta_1^{(%d)}$' % i, s=50)
    theta1_0 += -alpha*gradL(X, y, 0, theta1_0)
vlines(x=theta1_0, ymin=-20, ymax=L(X, y, 0, theta1_0), color='green', alpha=0.3, linestyles='--')
ylim(-20, 100)
xlabel('$\\theta_1$')
ylabel('$L(0, \\theta_1)$')
title('$\\alpha = {}$'.format(alpha))
```

При это следует учитывать, что слишком большое значение $\alpha$ может приводить к тому, что мы будем постоянно перепрыгивать через локальный минимум и алгоритм может не сойтись к искомой точке.

```python
plot(theta_1, L_theta_1)
theta1_0 = -2
scatter(theta1_0, L(X, y, 0, theta1_0), s=50)

alpha = 0.63
for i in range(6):
    arrow(theta1_0, L(X, y, 0, theta1_0),
          -alpha*gradL(X, y, 0, theta1_0), L(X, y, 0, theta1_0 - alpha*gradL(X, y, 0, theta1_0)) - L(X, y, 0, theta1_0),
          linewidth=2, head_width=1, head_length=0.1, fc='k', ec='k')
    vlines(x=theta1_0, ymin=-20, ymax=L(X, y, 0, theta1_0), color='green', alpha=0.3, linestyles='--')
    text(x=theta1_0-0.45, y = -35, text='$\\theta_1^{(%d)}$' % i, s=50)
    theta1_0 += -alpha*gradL(X, y, 0, theta1_0)
vlines(x=theta1_0, ymin=-20, ymax=L(X, y, 0, theta1_0), color='green', alpha=0.3, linestyles='--')
ylim(-20, 100)
xlabel('$\\theta_1$')
ylabel('$L(0, \\theta_1)$')
title('$\\alpha = {}$'.format(alpha))
```

Рекомендуется использовать следующие значения для скорости обучения в порядке их возрастания.
$$
\alpha = 0.01, 0.03, 0.1, 0.3, 1.
$$

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

```python
CS = plt.contour(theta_0v, theta_1v, L_theta, levels, colors='k')
plt.clabel(CS, inline=1, fontsize=10, colors='k')
plt.xlabel('$\\theta_0$')
plt.ylabel('$\\theta_1$')
plt.title('Линии уровня функции потерь $L(\\theta_0, \\theta_1)$')

def gradL(X, y, theta_0, theta_1):
    n = len(X)
    h = theta_0 + theta_1*X.values
    return (1/(n)*np.sum(h - y.values),
            1/(n)*np.sum(X.transpose()@(h - y.values)))

theta0_i = 20
theta1_i = 5

alpha = 0.1
for i in range(1000):
    upd_theta0_i, upd_theta1_i = gradL(X, y, theta0_i, theta1_i)
    if i < 10 or i % 50 == 0:
        scatter(theta0_i, theta1_i, s=20)
    theta0_i -= alpha*upd_theta0_i
    theta1_i -= alpha*upd_theta1_i
```

### Выражения для градиентов

Для реализации метода нам необходимо в явном виде вычислить частные производные функции потерь.
\begin{eqnarray}
\frac{\partial L}{\partial \theta_0} & = & \frac{1}{n}\sum\limits_{i=1}^n (h_\theta(x^{(i)}) - y^{(i)}) \\
\frac{\partial L}{\partial \theta_1} & = & \frac{1}{n}\sum\limits_{i=1}^n (h_\theta(x^{(i)}) - y^{(i)})x^{(i)} \\
\end{eqnarray}

Здесь использовано правило вычисления производной для составной функции.

Используя данное представление мы уже нашли выше оценку для минимума функции $L$. Координаты этой точки равны

```python
print("theta_0* = {:g}, theta_1* = {:g}".format(theta0_i, theta1_i))
```

    theta_0* = 0.431547, theta_1* = 2.16905

Функция потерь при данном значении параметров принимает значение

```python
L(X, y, theta0_i, theta1_i)
```

    1.048716528027162

Построим график для полученной гипотезы

```python
ax = housesales.plot.scatter(x='sqft_living', y='price')
ax.set_xlabel('Жилая площадь, 1k футы$^2$')
ax.set_ylabel('Цена, 100k $')

xx = np.linspace(1, 3, 2)
plot(xx, theta0_i + theta1_i * xx)
```

И вернемся к вопросу, который мы можем задать этой модели. Какова примерная стоимость дома площадью 2000 квадратных футов?

```python
print("Примерная стоимость: {:.2g} * 100k$".format((theta0_i + theta1_i * 2)))
```

    Примерная стоимость: 4.8 * 100k$

### Основные результаты

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

* задача регрессии (regression problem) - задача машинного обучения, в которой по заданому входу нужно предсказать значение из множества вещественных чисел.
* объекты (objects)- сущности, для которых алгоритм делает предсказания
* признаки (features) - числовое представление объектов
* матрица объекты-признаки  - матрица по строкам которой отложены объекты, а по столбцам - признаки
* целевая  переменная (target) - значение, которое необходимо предсказать
* обучающая выборка (training set)- множество пар (объект, значение целевой переменной), которые используются для выбора модели
* линейная регрессия (linear regression) - метод решения задачи регрессии
* гипотеза (hypothesis) - вид функции, которая используется для нахождения целевой переменной
* параметры гипотезы (hypothesis parameters) - неизвестные величины, которые необходимо найти в ходе обучения
* функция потерь (loss function) - штраф за неправильный результат
* градиентный спуск (gradient descent) - метод решения задачи оптимизации, который базируется на использовании градиента функции
* срокость обучения (learning rate) - основной параметр метода градиентного спуска, который отвечает за скорость сходимости

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

## Многомерная линейная регрессия

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

Рассмотрим задачу прогноза цены жилья в зависимости от следующих параметров:
 * количество спальных комнат
 * количество ванных комнат
 * жилая площадь
 * общая площадь
 * количество этажей
 * год постройки

```python
housesales = pd.read_csv('../datasets/house_price/train.csv.gz')[['SalePrice',
                                                              'LotArea',
                                                              'LotFrontage',
                                                              '1stFlrSF',
                                                              'OverallQual',
                                                              'GarageArea',
                                                              'YearBuilt',
                                                              'TotalBsmtSF',
                                                              '2ndFlrSF',]].dropna()
housesales['SalePrice'] = housesales['SalePrice']/100000
housesales.head()
```

      SalePrice
      LotArea
      LotFrontage
      1stFlrSF
      OverallQual
      GarageArea
      YearBuilt
      TotalBsmtSF
      2ndFlrSF

      0
      2.085
      8450
      65.0
      856
      7
      548
      2003
      856
      854

      1
      1.815
      9600
      80.0
      1262
      6
      460
      1976
      1262
      0

      2
      2.235
      11250
      68.0
      920
      7
      608
      2001
      920
      866

      3
      1.400
      9550
      60.0
      961
      7
      642
      1915
      756
      756

      4
      2.500
      14260
      84.0
      1145
      8
      836
      2000
      1145
      1053

Предствим матрицу объекты-признаки как
$$
X = \begin{pmatrix}
x_{11} & x_{12} & \cdots & x_{1m} \\
x_{21} & x_{22} & \cdots & x_{2m}  \\
\vdots & \vdots & \ddots & \vdots \\
x_{n1} & x_{n2} & \cdots & x_{nm} \\
\end{pmatrix}
$$

Здесь $n$ - число объектов и $m$ - число признаков.

Вектор целевых переменных $y$ имеет ту же форму, что и для одного признака
$$
y = \begin{pmatrix}
y_1 \\
y_2 \\
\vdots \\
y_n
\end{pmatrix}
$$

Для многомерного случая гипотеза $h_\theta(x)$ пример следующий вид
$$
h_\theta(x) = \theta_0 + \theta_1x_1 + \theta_2x_2 + \cdots + \theta_mx_m.
$$

Из приведенного веше выражения видно, что слагаемое $\theta_0$ отличается от всех остальных, которые представляют собой произведения различных компанент вектора параметров $\theta$ и вектора признаков $x = (x_1, x_2 , \dots, x_m)$. Чтобы привести выражение для гипотезы к более простому виду мы применим следующий приём. Расширим матрицу объекты-признаки дополнительным столбцом слева, состоящим из одних единиц
$$
X = \begin{pmatrix}
1 & x_{11} & x_{12} & \cdots & x_{1m} \\
1 & x_{21} & x_{22} & \cdots & x_{2m}  \\
\vdots & \vdots & & \ddots & \vdots \\
1 & x_{n1} & x_{n2} & \cdots & x_{nm} \\
\end{pmatrix}
$$
и используем нулевой индекс для обозначения этого столбца ($x_{i0} = 1, \quad i = \overline{1, n}$).

Тогда выражение для гипотезы $h_\theta(x)$ можно переписать в виде
$$
h_\theta(x) = \theta_0 + \theta_1x_1 + \theta_2x_2 + \cdots + \theta_mx_m = \theta_0x_0 + \theta_1x_1 + \theta_2x_2 + \cdots + \theta_mx_m = \sum\limits_{i = 0}^{m}\theta_ix_i = \theta^Tx.
$$
Т.е. представить в виде скалярного произведения ветора параметров $\theta$ и вектора признаков $x$. Здесь и далее под матрицей объектов-признаков $X$ будем понимать расширенную матрицу с единичным первым столбцом.

В качестве функции потерь $L$ как и прежде рассмотрим средний квадрат ошибки
$$
L(\theta) = \frac{1}{2n}\sum\limits_{i=1}^{n}\left(h_\theta(x^{(i)}) - y^{(i)}\right)^2.
$$

Здесь с помощью $x^{(i)} = (x_{i0}, x_{i1}, \dots, x_{im})^T$ обозначена вектор, составленый из строки матрицы $X$.

### Векторная форма градиентного спуска

Рассмотрим как метод градиентного спуска можно обобщить на многомерный случай. Для этого вычислим градиент фукнци потерь $J$ по всем компонентам вектора параметров $\theta$.

\begin{eqnarray}
\frac{\partial L}{\partial \theta_0} & = & \frac{1}{n}\sum\limits_{i=1}^n (h_\theta(x^{(i)}) - y^{(i)}) \\
\frac{\partial L}{\partial \theta_j} & = & \frac{1}{n}\sum\limits_{i=1}^n (h_\theta(x^{(i)}) - y^{(i)})x_{ij}, \quad j = \overline{1, m} \\
\end{eqnarray}

Или, используя прием с дополнительным единичным столбцом ($x_{i0} = 1$), можно переписать выражением для компонент вектора градиента в единообразном стиле
\begin{eqnarray}
\frac{\partial L}{\partial \theta_j} & = & \frac{1}{n}\sum\limits_{i=1}^n (h_\theta(x^{(i)}) - y^{(i)})x_{ij}, \quad j = \overline{0, m} \\
\end{eqnarray}

Наконец, в векторном виде выражение для градиента пример вид
$$
grad~ L(\theta) = \frac{1}{n} X^T(X\theta - y).
$$

Уравнение для обновления параметров $\theta$ для шага градиентного спуска можно также представить в вектрном виде
$$
\theta^{(k+1)} = \theta^{(k)} - \alpha ~grad ~ L(\theta^{(k)}).
$$

### Кривая обучения

В многомерном случае не представляется возможным визуализировать работу метода градиентного спуска с помощью линий уровня, поскольку простанство параметров в общес случае имеет размерность $m+1$. На помощь нам приходит другое представление, которое показывается изменение значений функции потерь в точке $\theta^{(k)}$ в зависимости от номера шага $k$. Такой график называется кривой обучения.

#### Проведем предобработку данных

```python
n = len(housesales)
m = len(housesales.drop('SalePrice', axis=1).columns)
y = housesales['SalePrice'].values.reshape((n, 1))
X = housesales.drop('SalePrice', axis=1).values.reshape((n, m))
X = np.hstack((np.ones((n, 1)), X))
```

#### Градиентный спуск

```python
def grad(y, X, theta):
    n = y.shape[0]
    return 1/n * X.transpose() @ (X @ theta - y)

def L(y, X, theta):
    n = y.shape[0]
    return 1/(2*n)*np.sum(np.power(X @ theta - y, 2))

def fit(y, X, theta_0, alpha=0.001, nsteps = 100):
    theta = np.copy(theta_0)
    loss = [L(y, X, theta)]
    for i in range(nsteps):
        theta -= alpha*grad(y, X, theta)
        loss.append(L(y, X, theta))
    return loss, theta
```

```python
theta_0 = np.zeros((m + 1, 1))
loss_history, theta_star = fit(y, X, theta_0, alpha=1e-10, nsteps=10000)
```

```python
plt.plot(loss_history)
plt.xlabel('$k$')
plt.ylabel('$L(\\theta^{(k)})$')
_ = plt.title('Кривая обучения')
```

```python
L(y, X, theta_star)
```

    0.19708429653845569

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

### Нормирование признаков

Поскольку различные признаки могу иметь отличные размерности, то, для устойчисвости численной процедуры метода градиентного спуска, рекомендуется провести их нормализацию. Для этого каждый признак можно по отдельности нормировать, вычтя из него среднее значение и разделив на средне-квадратичное отклонение
$$
    U^{(j)} = \frac{X^{(j)} - M[X^{(j)}]}{\sqrt{D[X^{(j)}]}}, \quad j = \overline{1, m}.
$$

При этом первый столбце из одних единиц остается неизменным.
$$
    U^{(0)} = \begin{pmatrix} 1 \\ 1 \\ \vdots \\ 1\end{pmatrix}
$$

```python
U = np.ones((n, m + 1))
for j in range(1, m + 1):
    U[:, j] = (X[:, j] - np.mean(X[:, j]))/np.std(X[:, j])

```

```python
loss_history, theta_star = fit(y, U, theta_0, alpha=1e-1, nsteps=50)
```

```python
plt.plot(loss_history)
plt.xlabel('$k$')
plt.ylabel('$L(\\theta^{(k)})$')
_ = plt.title('Кривая обучения')
```

Из графика видно, что при нормализации признаков удалось достичь того же значения функции потерь сделав в 10 раз меньше шагов.

Значение функции потерь для нашей задачи составило

```python
L(y, U, theta_star)
```

    0.07986216026716797

Как же можно улучшить этот результат оставаясь в рамках метода линейно регрессии?

### Нелинейные зависимости

Проведем анализ нашей задачи. Для этого построим зависимость целевой переменной от каждого из признаков по отдельности.

```python
sns.pairplot(housesales, y_vars="SalePrice",
                 x_vars=["LotArea", "LotFrontage", "1stFlrSF", "OverallQual"],
            )
sns.pairplot(housesales, y_vars="SalePrice",
                 x_vars=["GarageArea", "YearBuilt", "TotalBsmtSF", "2ndFlrSF"],
            )
```

По характерному виду некоторых зависимостей можно предположить, что целевая переменная нелинейно зависит от некоторых признаков. Чтобы учеть подобные зависимости мы модем обогатить числовые признаки их квадратами и произведениями различных признаков, получив, таким образом, новую матрицу объекты-признаки.
$$
X = \begin{pmatrix}
1 & x_{11} & x_{12} & \cdots & x_{1m} & x_{11}^2 & x_{11}x_{12} & \cdots & x_{1m}^2\\
1 & x_{21} & x_{22} & \cdots & x_{2m} & x_{21}^2 & x_{21}x_{22} & \cdots & x_{2m}^2 \\
\vdots & \vdots & & \ddots & \vdots \\
1 & x_{n1} & x_{n2} & \cdots & x_{nm} & x_{n1}^2 & x_{n1}x_{n2} & \cdots & x_{nm}^2\\
\end{pmatrix}
$$

После чего используем тот же метод линейно регрессии.

```python
n = len(housesales)
m = len(housesales.drop('SalePrice', axis=1).columns)
y = housesales['SalePrice'].values.reshape((n, 1))

tmpX = np.ones((n, m + 1))
X = np.zeros((n, (m + 1)**2))
tmpX[:, 1:] = housesales.drop('SalePrice', axis=1).values.reshape((n, m))
for i in range(n):
    X_i = tmpX[i, :] .reshape(1, -1)
    X[i, :] = (X_i.T @ X_i).reshape(-1)
```

```python
U = np.ones(X.shape)
for j in range(1, 2*m + 1):
    U[:, j] = (X[:, j] - np.mean(X[:, j]))/np.std(X[:, j])
```

### Нормальное уравнение

```python
theta_0 = np.zeros((U.shape[1], 1))
loss_history, theta_star = fit(y, U, theta_0, alpha=1e-2, nsteps=500)
```

```python
plt.plot(loss_history)
plt.ylim((0, loss_history[10]))
plt.xlabel('$k$')
plt.ylabel('$L(\\theta^{(k)})$')
_ = plt.title('Кривая обучения')
```

Значение функции потерь после процедуры градиентного спуска равно

```python
L(y, U, theta_star)
```

    0.067764053087824239

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

### Нормальное уравнение

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

\begin{equation}
\theta = (X^TX)^{-1}X^T y.
\end{equation}

Вывод данного уравениея оставим в качестве самостоятельного задания.

Данное выражение называется **нормальным уравнением** линейной регрессии.

Его основная вычислительная сложность связана с необходимостью обращения матрицы, которая может быть плохо обусловлена. Это может сказаться на точности вычислений. Метод граидентного спуска наоборот обладает тем свойством, что при удачном выборе скрости обучения будет с каждым шагом сходиться к оптимальному значения параметров $\theta$.

Для устойчивого обращения плохо обусловленной матрицы мы можем использовать алгоритм обращения Мура-Пенроуза (numpy.linalg.pinv)

```python
theta_n = np.linalg.pinv(X.T @ X) @ X.T @ y
```

Значение функции потерь при параметрах, вычисленных с помощью нормального уравнения, равно

```python
L(y, X, theta_n)
```

    0.04344541186281841

Видно, что точное аналитическое решение дало чуть лучший результат по сравнению с методом градиентного спуска. В данном случае это объясняется небольшой размерностью задачи. Даже при учете квадратичных признаков их число m = 81.

Эмпирическое правило выбора метода решения задачи линейной регресии можно сформулировать следующим образом. При числе признаков до 1000 следует использовать нормальное уравниение в противном случае - метод градиентного спуска.

## Резюме

1. Метод линейной регрессии может быть расширен на многомерный случай
2. Для улучшения сходимости метода градиентного спуска следует использовать нормализацию признаков
3. Метод линейной регрессии можно обобщить на нелинейный случай с помощью добавления полиномиальных признаков
4. Аналитическое решения задачи линейной регресии называется **нормальное уравнение**
5. Аналитическое решение следует использовать при числи признаков меньше либо равном 1000

## Источники

1. **Andrew Ng**. Machine Learning - [Linear regression with one variable](https://www.coursera.org/learn/machine-learning/home/week/1), [Linear regression with multiple variables](https://www.coursera.org/learn/machine-learning/home/week/2)
2. **К. В. Воронцов**. Введение в машинное обучение - [Линейна регрессия](https://www.coursera.org/learn/vvedenie-mashinnoe-obuchenie/home/week/4)
3. **Christopher M. Bishop**. Pattern Recognition And Machine Learning - [Linear models for regression]
4. Открытй курс по машинному обучению - [Линейные модели классификации и регрессии](https://habrahabr.ru/company/ods/blog/323890/)

```python

```


# PIT according to Tongtong but in SI-units

# Purpose
Implement PIT accoring to:

[1]
Wang, Tongtong, Guoyuan Li, Baiheng Wu, Vilmar Æsøy, and Houxiang Zhang. “Parameter Identification of Ship Manoeuvring Model Under Disturbance Using Support Vector Machine Method.” Ships and Offshore Structures, May 19, 2021.

# Methodology
* [1] has a an interesing way to do the regression ([1]eq.3.). This will be tested on the present data.

# Setup

```python
# %load imports.py
## Local packages:

%matplotlib inline
%load_ext autoreload
%autoreload 2
%config Completer.use_jedi = False  ## (To fix autocomplete)

## External packages:
import pandas as pd
pd.options.display.max_rows = 999
pd.options.display.max_columns = 999
pd.set_option("display.max_columns", None)

import numpy as np
import os
import matplotlib.pyplot as plt
#if os.name == 'nt':
#    plt.style.use('presentation.mplstyle')  # Windows

import plotly.express as px
import plotly.graph_objects as go

import seaborn as sns
import sympy as sp
from sympy.physics.mechanics import (dynamicsymbols, ReferenceFrame,
                                      Particle, Point)
from sympy.physics.vector.printing import vpprint, vlatex
from IPython.display import display, Math, Latex
from src.substitute_dynamic_symbols import run, lambdify

import pyro

import sklearn
import pykalman
from statsmodels.sandbox.regression.predstd import wls_prediction_std
import statsmodels.api as sm

from scipy.integrate import solve_ivp

## Local packages:
from src.data import mdl

# Load models:
# (Uncomment these for faster loading):
import src.models.vmm_abkowitz  as vmm_abkowitz

from src.symbols import *
from src.parameters import *
import src.symbols as symbols
from src import prime_system
from src.models import regression
from src.visualization.plot import track_plot
from src.equation import Equation
```

## Load test

```python
#id=22773
#id=22616
id=22774
#id=22770

df, units, meta_data = mdl.load(id=id, dir_path='../data/processed/kalman_cut/')
df.index = df.index.total_seconds()
df.index-=df.index[0]
df['t'] = df.index
df.sort_index(inplace=True)
df['-delta'] = -df['delta']
df['V'] = np.sqrt(df['u']**2 + df['v']**2)

df['thrust'] = df['Prop/PS/Thrust'] + df['Prop/SB/Thrust']
df['U'] = df['V']
df['beta'] = -np.arctan2(df['v'],df['u'])
```

```python
meta_data['rho']=1000
meta_data['mass'] = meta_data['Volume']*meta_data['rho']
```

```python
from src.visualization.plot import track_plot
fig,ax=plt.subplots()
#fig.set_size_inches(10,10)
track_plot(df=df, lpp=meta_data.lpp, x_dataset='x0', y_dataset='y0',  psi_dataset='psi', beam=meta_data.beam, ax=ax);
df.plot(y='u')
```

# Ship parameters

```python
T_ = (meta_data.TA + meta_data.TF)/2
L_ = meta_data.lpp
m_ = meta_data.mass
rho_ = meta_data.rho
B_ = meta_data.beam
CB_ = m_/(T_*B_*L_*rho_)
I_z_ = m_*meta_data.KZZ**2
#I_z_=839.725

ship_parameters = {
        'T' : T_,
        'L' : L_,
        'CB' :CB_,
        'B' : B_,
        'rho' : rho_,
        #'x_G' : meta_data.lcg,  # motions are expressed at CG
        'x_G' : 0,  # motions are expressed at CG

        'm' : m_,
        'I_z': I_z_,
        'volume':meta_data.Volume,
    }

ps = prime_system.PrimeSystem(**ship_parameters)  # model

scale_factor = meta_data.scale_factor
ps_ship = prime_system.PrimeSystem(L=ship_parameters['L']*scale_factor, rho=meta_data['rho'])  # ship
ship_parameters_prime = ps.prime(ship_parameters)
```

# Brix parameters

```python
def calculate_prime(row, ship_parameters):
    return run(function=row['brix_lambda'], inputs=ship_parameters)

mask = df_parameters['brix_lambda'].notnull()
df_parameters.loc[mask,'brix_prime'] = df_parameters.loc[mask].apply(calculate_prime, ship_parameters=ship_parameters, axis=1)

df_parameters.loc['Ydelta','brix_prime'] = 0.0004  # Just guessing
df_parameters.loc['Ndelta','brix_prime'] = -df_parameters.loc['Ydelta','brix_prime']/4  # Just guessing

df_parameters['brix_prime'].fillna(0, inplace=True)
#df_parameters['brix_SI'].fillna(0, inplace=True)

df_parameters.head()
```

```python
df_parameters['prime'] = df_parameters['brix_prime']

def to_SI(row):

    prime = row['prime']
    denominator = row['denominator']
    lamda = lambdify(denominator)
    try:
        denominator_value = run(lamda, inputs=ship_parameters)
        SI = prime*denominator_value
    except:
        return None
    else:
        return SI

df_parameters['brix_SI'] = df_parameters.apply(to_SI, axis=1)
```

## Regression model
$$
\begin{array}{lcl}
u'(n+1) - u'(n) = A \cdot X(n) \\
v'(n+1) - v'(n) = B \cdot Y(n) \\
r'(n+1) - r'(n) = C \cdot N(n) \\
\end{array}
$$
where A, B, C are parameter vectors formed by hydrodynamic
derivatives to be identified.

```python
label = sp.symbols('X_qs')
X_eq_ = vmm_abkowitz.X_qs_eq.subs(vmm_abkowitz.X_qs,label)
diff_eq_X = regression.DiffEqToMatrix(ode=X_eq_, label=label, base_features=[delta,u,v,r,thrust])

label = sp.symbols('Y_qs')
Y_eq_ = vmm_abkowitz.Y_qs_eq.subs(vmm_abkowitz.Y_qs,label)
diff_eq_Y = regression.DiffEqToMatrix(ode=Y_eq_, label=label, base_features=[delta,u,v,r])

label = sp.symbols('N_qs')
N_eq_ = vmm_abkowitz.N_qs_eq.subs(vmm_abkowitz.N_qs,label)
diff_eq_N = regression.DiffEqToMatrix(ode=N_eq_, label=label, base_features=[delta,u,v,r])

```

```python
Math(vlatex(diff_eq_X.acceleration_equation))
```

```python
Math(vlatex(diff_eq_X.eq_X))
```

```python
X_martin = diff_eq_X.eq_X.rhs
X_tongtong = X_martin*delta_t/(m-p.Xudot)
```

```python
S = (m-p.Yvdot)*(I_z-p.Nrdot) - (m*x_G-p.Yrdot)*(m*x_G-p.Nvdot)
```

```python
Y_martin = diff_eq_Y.eq_X.rhs
Y_tongtong = Y_martin*delta_t/(S)

N_martin = diff_eq_N.eq_X.rhs
N_tongtong = N_martin*delta_t/(S)
```

```python
subs = {value:key for key,value in p.items()}
X_lambda=lambdify(X_tongtong.subs(subs))
Y_lambda=lambdify(Y_tongtong.subs(subs))
N_lambda=lambdify(N_tongtong.subs(subs))
```

```python
from statsmodels.sandbox.regression.predstd import wls_prediction_std
def show_pred(X,y,results, label):

    display(results.summary())

    X_ = X
    y_ = y
    y_pred = results.predict(X_)

    prstd, iv_l, iv_u = wls_prediction_std(results, exog=X_, alpha=0.05)
    #iv_l*=-1
    #iv_u*=-1

    fig,ax=plt.subplots()
    ax.plot(X_.index,y_, label='Numerical gradient from model test')
    ax.plot(X_.index,y_pred, '--', label='OLS')
    ax.set_ylabel(label)

    ax.fill_between(X_.index, y1=iv_l, y2=iv_u, zorder=-10, color='grey', alpha=0.5, label=r'5\% confidence')
    ax.legend();
```

```python
def regress(data, lamda, diff_eq, y_key):

    y = data[y_key].diff().iloc[1:].copy()
    data = data.iloc[1:].copy()
    X = run(lamda, inputs=data,**ship_parameters,
            **df_parameters['brix_SI'], delta_t=dt_)[0].T

    subs = diff_eq.feature_names_subs()
    columns = list(subs.values())
    X = pd.DataFrame(data=X, index=data.index, columns=columns)

    model = sm.OLS(y,X)
    results = model.fit()

    label=f"{y_key}'(n+1) - {y_key}'(n)"
    show_pred(X=X,y=y,results=results, label=label)

    return model,results
```

```python
#dt_ = np.diff(df_prime.index)  # SI or prime?
dt_ = np.mean(np.diff(df.index))  # SI or prime?
data = df.copy()
```

## X

```python
model_X, results_X = regress(data=data, lamda=X_lambda, diff_eq=diff_eq_X, y_key='u')
```

## Y

```python
model_Y, results_Y = regress(data=data, lamda=Y_lambda, diff_eq=diff_eq_Y, y_key='v')
```

## N

```python
model_N, results_N = regress(data=data, lamda=N_lambda, diff_eq=diff_eq_N, y_key='r')
```

```python
results_summary_X = regression.results_summary_to_dataframe(results_X)
results_summary_Y = regression.results_summary_to_dataframe(results_Y)
results_summary_N = regression.results_summary_to_dataframe(results_N)
```

```python
A_ = results_summary_X['coeff']
B_ = results_summary_Y['coeff']
C_ = results_summary_N['coeff']
```

```python
eq_X_coeff = sp.Eq(X_coeff,(m-p.Xudot)/delta_t*A_coeff)
eq_X_coeff
```

```python
subs = {value:key for key,value in p.items()}
solution_X_lambda = lambdify(eq_X_coeff.rhs.subs(subs))
```

```python
M = sp.matrices.MutableDenseMatrix([
                                [(I_z-p.Nrdot)*delta_t/(S), -(m*x_G-p.Yrdot)*delta_t/(S)],
                                [-(m*x_G-p.Nvdot)*delta_t/(S), (m-p.Yvdot)*delta_t/(S)]
                               ])
```

```python
solution = M.inv()*sp.matrices.MutableDenseMatrix([B_coeff,C_coeff])
subs = {value:key for key,value in p.items()}
solution_YN_lambda = lambdify(solution.subs(subs))
```

```python
parameters_A = run(solution_X_lambda, A_coeff=A_, delta_t=dt_, **df_parameters['brix_SI'],
                  **ship_parameters)
```

```python
parameters_ = run(solution_YN_lambda, B_coeff=B_.values, C_coeff=C_.values, delta_t=dt_,
                  **ship_parameters,
                  **df_parameters['brix_SI'])
```

```python
parameters_[0][0]
```

```python
parameters_B = pd.Series(parameters_[0][0], index=B_.index)
parameters_C = pd.Series(parameters_[1][0], index=C_.index)

df_parameters['regressed'] = parameters_A
df_parameters['regressed'] = df_parameters['regressed'].combine_first(parameters_B)
df_parameters['regressed'] = df_parameters['regressed'].combine_first(parameters_C)

df_parameters['SI'] = df_parameters['regressed'].combine_first(df_parameters['brix_SI'])
```

```python
df_parameters['SI']['Xudot']/m_
```

```python
fig,ax=plt.subplots()
fig.set_size_inches(15,5)

mask = ((df_parameters['brix_prime']!=0) |
        (pd.notnull(df_parameters['regressed']))
       )

df_parameters_all_plot = df_parameters.loc[mask]

df_parameters_all_plot.drop(index=['Xthrust']).plot.bar(y=['brix_SI','regressed'], ax=ax);
```

```python
result_abkowitz = vmm_abkowitz.simulator.simulate(df_=df, parameters=df_parameters['SI'],
                                                  ship_parameters=ship_parameters)
```

```python

```


# Emulate a Lorenz 96 model
The objective of this notebook is to provide a tutorial that emulates a Lorenz 63 model from data only.

The general objective of this work is to present a methodology that consider a time-series ${\bf v}_0,\cdots,{\bf v}_K$ and infer a data-driven model $G({\bf v}_k)$ that give samples from a posteriori distribution representing the uncertainty
$$
{\bf x}_{k+1} \sim p({\bf x}_{k+1}/{\bf x}_k)
$$
and more generally:
$$
{\bf x}_{k+1} \sim p({\bf x}_{k+1}/{\bf x}_0)
$$

## Importation and initializations
This section imports and define few utilities functions. You can execute the cells below and then goes directly to the part 1-data definition.

```python
incolab=True
try:
  import colab
except:
  incolab = False

if incolab:
  print("This notebook runs under colab")
  !pip install wget
  import wget
  wget.download('https://raw.githubusercontent.com/nansencenter/ml-crashcourse/main/utils.py')
```

```python
## Before we start let's import few packages

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
from sklearn.linear_model import LinearRegression
import tensorflow.keras as keras
import properscoring as ps

import tensorflow as tf
from tensorflow.keras import layers
from utils import simulate, plot_L96_2D, dt, rmse, Periodic1DPadding
from utils import forward_L96 as forward
import tensorflow.keras as keras
import tensorflow as tf
from tensorflow.keras import layers
#import tensorflow_probability as tfp
from skimage.util import view_as_blocks

```

```python
## We initialize the random generators so the notebook is fully reproducible

np.random.seed(2021)
tf.random.set_seed(2022)

```

1. Data definition

The Lorenz 96 model is defined on a 40-dimensional state $\mathbf{x}(t)$ by the following equations:
\begin{align}
\frac{\mathrm{d}x_i}{\mathrm{d}t} &= (x_{i+1} - x_{i-2})x_{i-1} + F,
\end{align}
In our case we take the defaults values $F = 8$

We integrate the ODEs (Ordinary Differential Equations) using a runge kutta scheme on discrete time steps $t_k$ such as $t_{k+1}-t_k = \delta t = 0.01$ (by default). We denote $\mathcal{M}$ the inegretion operator (sometimes called the *resolvent*) over a time step $\delta t$, such as:
$$
{\bf x_{k+1}} = \mathcal{M}({\bf x_{k}})
$$

Now we are almost ready to generate all the data. We are defining all the necessary parameters (some of which will be explained later on).

```python
M = 40 # size of the state
K = 50000 # size of the reference simulation
Ktrain=15000 # Number of samples in the training set
Kval = 10000 # Number of samples in the validation set
Ktest = 2000 # Number of samples in the test set (used to assess the forecast skill/uncertainty)
Ntest = 50 # Size of the test set ensemble
x0ref = np.zeros(M) # Reference point to initialize simulations
stdo = 0 #Relative noise on the data
Nens = 50 # Size of the ensemble for uncertainty estimate

```

Now, let's simulate the data for the reference set and the test set

```python
#Reference dataset generation
x0 = x0ref + 1e-3*np.random.randn(M)
x = simulate(forward, K, x0)

# Define the noise field
sigma = np.std(x,axis=0)
noise = stdo*sigma*np.random.randn(K,M)

# Extract the training and validation set (add noise on the training)
Xtrain = x[0:Ktrain] +  noise[0:Ktrain]
ytrain = x[1:Ktrain+1] + noise[1:Ktrain+1]

Xval = x[Ktrain:Ktrain+Kval]
yval = x[Ktrain+1:Ktrain+Kval+1]
ttval = dt*(Ktrain+1)+ dt*np.arange(Kval)
# Test set (ensemble) generation
x0test = x0ref[np.newaxis,:] + 1e-3*np.random.randn(Ntest,M)
xtest = simulate(forward, Ktest, x0test, N=Ntest)
ttest = dt*np.arange(Ktest) #Time scale of the test datset

# Print the size
print(f'Size of the reference set: {x.shape}\nSize of the test set: {xtest.shape}')

```

    Size of the reference set: (50000, 40)
    Size of the test set: (2000, 50, 40)

We now plot the simulations of few time steps of one member of the test simulation

```python
fig, ax = plt.subplots(figsize=(8,3))
cm = ax.imshow(xtest[:600,1].T,extent=[ttest[0],ttest[1],0,M],aspect='auto')
fig.colorbar(cm)
```

## 2. The neural network emulator
First, let's define the list of parameters for the neural network.

```python
stoch = True #To predict the standard deviation
norm_input = True #Standardization of the input vector
nhid1 = 60 # Number of filters in the first hidden layer
nhid2 = 40 # Number of filters in the second hiddent layer
filter1 = 5 # Filter size for the first layer
filter2 = 1 # Filter size of the second layer
epochs = 40 # Number of epochs during optimization
optimizer = 'Adam'# Optimizer used for the training
var = True
```

```python
if var:
    scale_std = np.sqrt
    scale_var = lambda x:x
else:
    scale_std = lambda x:x
    scale_var = np.square
```

Compute the normalization of the input:

```python
if norm_input:
    mean_ = np.mean(Xtrain,axis=(0,1),keepdims=True)
    std_= np.std(Xtrain,axis=(0,1), keepdims=True)
    norm = lambda x: (x-mean_)/std_
    denorm = lambda x: x*std_ + mean_
else:
    norm = lambda x: x
    denorm = lambda x: x
```

Define the deterministic neural network model

```python
inputs = keras.Input(shape=(M,1))
border= (filter1//2+filter2//2)
lay = Periodic1DPadding(padding_size=border)(inputs)
lay = layers.Conv1D(nhid1, filter1, activation='tanh')(lay)
lay = layers.Conv1D(nhid2, filter2, activation='tanh')(lay)
outputs = layers.Conv1D(1,1)(lay)
nn_model_det = keras.Model(inputs=inputs, outputs=outputs, name='deterministic_L96')
nn_model_det.compile(optimizer=optimizer, loss='mse')
nn_model_det.summary()
```

    Model: "deterministic_L96"
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #
    =================================================================
    input_1 (InputLayer)         [(None, 40, 1)]           0
    _________________________________________________________________
    periodic1d_padding (Periodic (None, 44, 1)             0
    _________________________________________________________________
    conv1d (Conv1D)              (None, 40, 60)            360
    _________________________________________________________________
    conv1d_1 (Conv1D)            (None, 40, 40)            2440
    _________________________________________________________________
    conv1d_2 (Conv1D)            (None, 40, 1)             41
    =================================================================
    Total params: 2,841
    Trainable params: 2,841
    Non-trainable params: 0
    _________________________________________________________________

    2021-12-04 12:44:21.891726: I tensorflow/core/platform/cpu_feature_guard.cc:142] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  SSE4.1 SSE4.2
    To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.

**Gaussian version (with mean and standard devation as output)**

We can interpret the loss function as a maximum of likelihood if we make the hypothosesis that the observationfollows a Gaussian law:

$$
p(y/x) = \frac{1}{\sqrt{2\pi\sigma^2}}\exp -\frac{(y - \mu(x))^2}{2\sigma^2}
$$
where $x$ is observed, and $\mu(x)$ represents the mean of the gaussian depending on $x$.
This is a general formula, expressed for simplicity in the scalar case. In our case, we have $x = {\bf x}_k$ and $y = {\bf x}_{k+1}$

Given a set of samples $(x_k,y_k)_{1:K}$, maximizing the likelihood (i.e. finding an estimation so that the observation is the more probable) consist in minimizing the negative log-likelihood:
$$
L = \sum_{k=1}^{K} \left(\frac{\log 2\pi\sigma^2}{2} + \frac{(y_k - \mu(x_k))^2}{2\sigma^2}\right)
$$

1. **Deterministic case**: $\mu(x) \approx G_\mu(x,{\boldsymbol \theta}_\mu)$
We try to identify the parameters of the neural net ${\boldsymbol \theta}_\mu$ that minimze L. As the first term and the denominator of L are a constant (do not depend on ${\boldsymbol \theta}_\mu$), we equivalentely minimize:
$$
L({\boldsymbol \theta}_\mu) =  \sum_{k=1}^{K} (y_k - G_\mu(x,{\boldsymbol \theta}_\mu))^2
$$
which is exactly the mean square loss already introduced.

2. **Gaussian case**: $\mu(x) \approx G_\mu(x_k,{\boldsymbol \theta}_\mu)$  and $\sigma(x) \approx \sqrt{G_\sigma(x,{\boldsymbol \theta}_\sigma)}$
We now assume that the standard deviation depends also on $x$ and that we parameterize it by another neural network.
So we can estimate both the parameters of the neural net for the mean $\mu$ and the standard deviation $\sigma$ by minimizing:
$$
L({\boldsymbol \theta}_\mu, {\boldsymbol \theta}_\sigma) = \sum_{k=1}^{K} \left(\frac{\log 2\pi G_\sigma(x_k,{\boldsymbol \theta}_\sigma)}{2} + \frac{(y_k - G_\mu(x_k,{\boldsymbol \theta}_\mu))^2}{2 G_\sigma(x_k,{\boldsymbol \theta}_\sigma)}\right)
$$
This way the neural net $G=(G_\mu,  G_\sigma)$ gives not only an estimation of the output but also the uncertainty of this estimation in the form of the standard deviation.

```python
def loss_mle_old(y_true, y_pred):
    # scale predictions so that the class probas of each sample sum to 1
    mean_true = y_true
    mean_pred, std_pred = y_pred[...,0], y_pred[...,1]
    # Max to prevent NaN's and Inf's
    log_std = tf.math.log(tf.maximum(std_pred,tf.math.sqrt(tf.keras.backend.epsilon())))
    sq_std = tf.maximum(tf.square(std_pred),tf.keras.backend.epsilon() )
    mse = tf.math.squared_difference(mean_pred, mean_true) / (2*sq_std)
    loss = log_std + mse
    loss = tf.reduce_sum(loss, -1)
    return loss

def loss_mle(y_true, y_pred):
    # scale predictions so that the class probas of each sample sum to 1
    mean_true = y_true
    mean_pred, var_pred = y_pred[...,0], tf.maximum(y_pred[...,1], tf.keras.backend.epsilon())
    # Max to prevent NaN's and Inf's
    log_std = tf.math.log(var_pred)
    mse = tf.math.squared_difference(mean_pred, mean_true) / var_pred
    loss = log_std + mse
    loss = tf.reduce_sum(loss, -1)
    return loss

def loss_mle_tfp(y_true, y_pred):
    mean_true = y_true
    mean_pred, std_pred = y_pred[...,0], y_pred[...,1]
    norm_dist = tfp.distributions.Normal(loc = mean_pred, scale = std_pred)
    loss = - norm_dist . log_prob ( y_true)
    loss = tf.reduce_mean(loss, -1)
    return loss

if stoch:
    inputs = keras.Input(shape=(M,1))
    border= (filter1//2+filter2//2)
    lay = Periodic1DPadding(padding_size=border)(inputs)
    lay = layers.Conv1D(nhid1, filter1, activation='tanh')(lay)
    lay = layers.Conv1D(nhid2, filter2, activation='tanh')(lay)
    mu = layers.Conv1D(1,1)(lay)
    var = layers.Conv1D(1,1, activation='exponential')(lay)
    outputs = layers.Concatenate(name='output')([mu, var])

    nn_model_gauss = keras.Model(inputs=inputs, outputs=outputs, name='gaussian_L96')
    nn_model_gauss.summary()
    #Need a special loss

    nn_model_gauss.compile(optimizer=optimizer, loss=loss_mle_tfp)

```

    Model: "gaussian_L96"
    __________________________________________________________________________________________________
    Layer (type)                    Output Shape         Param #     Connected to
    ==================================================================================================
    input_2 (InputLayer)            [(None, 40, 1)]      0
    __________________________________________________________________________________________________
    periodic1d_padding_1 (Periodic1 (None, 44, 1)        0           input_2[0][0]
    __________________________________________________________________________________________________
    conv1d_3 (Conv1D)               (None, 40, 60)       360         periodic1d_padding_1[0][0]
    __________________________________________________________________________________________________
    conv1d_4 (Conv1D)               (None, 40, 40)       2440        conv1d_3[0][0]
    __________________________________________________________________________________________________
    conv1d_5 (Conv1D)               (None, 40, 1)        41          conv1d_4[0][0]
    __________________________________________________________________________________________________
    conv1d_6 (Conv1D)               (None, 40, 1)        41          conv1d_4[0][0]
    __________________________________________________________________________________________________
    output (Concatenate)            (None, 40, 2)        0           conv1d_5[0][0]
                                                                     conv1d_6[0][0]
    ==================================================================================================
    Total params: 2,882
    Trainable params: 2,882
    Non-trainable params: 0
    __________________________________________________________________________________________________

It is now possible de train the model (optimize the parameters)

First we train the deterministic model

```python
history_d = nn_model_det.fit(norm(Xtrain),ytrain, validation_data=(norm(Xval),yval),epochs=epochs)

```

    2021-12-04 12:44:31.214997: I tensorflow/compiler/mlir/mlir_graph_optimization_pass.cc:185] None of the MLIR Optimization Passes are enabled (registered 2)

    Epoch 1/40
    469/469 [==============================] - 1s 2ms/step - loss: 1.1993 - val_loss: 0.1123
    Epoch 2/40
    469/469 [==============================] - 1s 2ms/step - loss: 0.0822 - val_loss: 0.0614
    Epoch 3/40
    469/469 [==============================] - 1s 2ms/step - loss: 0.0425 - val_loss: 0.0309
    Epoch 4/40
    469/469 [==============================] - 1s 2ms/step - loss: 0.0208 - val_loss: 0.0145
    Epoch 5/40
    469/469 [==============================] - 1s 2ms/step - loss: 0.0090 - val_loss: 0.0060
    Epoch 6/40
    469/469 [==============================] - 1s 2ms/step - loss: 0.0033 - val_loss: 0.0020
    Epoch 7/40
    469/469 [==============================] - 1s 2ms/step - loss: 0.0012 - val_loss: 8.8346e-04
    Epoch 8/40
    469/469 [==============================] - 1s 2ms/step - loss: 6.6665e-04 - val_loss: 6.0059e-04
    Epoch 9/40
    469/469 [==============================] - 1s 2ms/step - loss: 4.7634e-04 - val_loss: 4.4638e-04
    Epoch 10/40
    469/469 [==============================] - 1s 2ms/step - loss: 3.6868e-04 - val_loss: 3.6656e-04
    Epoch 11/40
    469/469 [==============================] - 1s 3ms/step - loss: 2.9527e-04 - val_loss: 3.0380e-04
    Epoch 12/40
    469/469 [==============================] - 1s 2ms/step - loss: 2.4847e-04 - val_loss: 2.5891e-04
    Epoch 13/40
    469/469 [==============================] - 1s 2ms/step - loss: 2.0801e-04 - val_loss: 1.8832e-04
    Epoch 14/40
    469/469 [==============================] - 1s 2ms/step - loss: 1.6997e-04 - val_loss: 1.6742e-04
    Epoch 15/40
    469/469 [==============================] - 1s 2ms/step - loss: 1.5011e-04 - val_loss: 1.6376e-04
    Epoch 16/40
    469/469 [==============================] - 1s 2ms/step - loss: 1.3669e-04 - val_loss: 1.2278e-04
    Epoch 17/40
    469/469 [==============================] - 1s 2ms/step - loss: 1.2653e-04 - val_loss: 1.5751e-04
    Epoch 18/40
    469/469 [==============================] - 1s 2ms/step - loss: 1.1765e-04 - val_loss: 1.0330e-04
    Epoch 19/40
    469/469 [==============================] - 1s 2ms/step - loss: 1.1139e-04 - val_loss: 1.0622e-04
    Epoch 20/40
    469/469 [==============================] - 1s 2ms/step - loss: 9.8885e-05 - val_loss: 8.1010e-05
    Epoch 21/40
    469/469 [==============================] - 1s 2ms/step - loss: 9.9003e-05 - val_loss: 8.4738e-05
    Epoch 22/40
    469/469 [==============================] - 1s 2ms/step - loss: 9.5258e-05 - val_loss: 7.5273e-05
    Epoch 23/40
    469/469 [==============================] - 1s 2ms/step - loss: 8.8185e-05 - val_loss: 1.0842e-04
    Epoch 24/40
    469/469 [==============================] - 1s 2ms/step - loss: 8.5989e-05 - val_loss: 8.5523e-05
    Epoch 25/40
    469/469 [==============================] - 1s 2ms/step - loss: 8.7783e-05 - val_loss: 7.4092e-05
    Epoch 26/40
    469/469 [==============================] - 1s 2ms/step - loss: 7.6701e-05 - val_loss: 4.7714e-05
    Epoch 27/40
    469/469 [==============================] - 1s 2ms/step - loss: 8.2582e-05 - val_loss: 1.8586e-04
    Epoch 28/40
    469/469 [==============================] - 1s 2ms/step - loss: 7.4472e-05 - val_loss: 6.4414e-05
    Epoch 29/40
    469/469 [==============================] - 1s 2ms/step - loss: 7.5089e-05 - val_loss: 7.0183e-05
    Epoch 30/40
    469/469 [==============================] - 1s 2ms/step - loss: 7.3889e-05 - val_loss: 4.4628e-05
    Epoch 31/40
    469/469 [==============================] - 1s 2ms/step - loss: 6.8708e-05 - val_loss: 8.8770e-05
    Epoch 32/40
    469/469 [==============================] - 1s 2ms/step - loss: 7.4850e-05 - val_loss: 1.1404e-04
    Epoch 33/40
    469/469 [==============================] - 1s 2ms/step - loss: 6.5299e-05 - val_loss: 1.4225e-04
    Epoch 34/40
    469/469 [==============================] - 1s 2ms/step - loss: 6.5884e-05 - val_loss: 9.7969e-05
    Epoch 35/40
    469/469 [==============================] - 1s 2ms/step - loss: 7.1631e-05 - val_loss: 4.0321e-05
    Epoch 36/40
    469/469 [==============================] - 1s 2ms/step - loss: 7.0614e-05 - val_loss: 6.8358e-05
    Epoch 37/40
    469/469 [==============================] - 1s 2ms/step - loss: 5.6350e-05 - val_loss: 7.1889e-05
    Epoch 38/40
    469/469 [==============================] - 1s 2ms/step - loss: 6.7227e-05 - val_loss: 1.1339e-04
    Epoch 39/40
    469/469 [==============================] - 2s 4ms/step - loss: 6.1377e-05 - val_loss: 5.5634e-05
    Epoch 40/40
    469/469 [==============================] - 1s 3ms/step - loss: 6.5368e-05 - val_loss: 8.0074e-05

Before assessing the performance of the NN model, we have look at the loss decreasing over the epochs for the training dataset and the validation dataset.

```python
fig, ax = plt.subplots()
ax.semilogy(history_d.history['loss'],color='gray',label='train loss')
ax.semilogy(history_d.history['val_loss'],color='black',label='validation loss')
ax.legend()
ax.set_xlabel('epoch');
ax.set_ylabel('Loss');
```

We now train the stochastic model

```python
if stoch:
    import tensorflow_probability as tfp
    history_s = nn_model_gauss.fit(norm(Xtrain),ytrain, validation_data=(norm(Xval),yval),epochs=epochs)
    fig, ax = plt.subplots()
    ax.plot(history_s.history['loss'],color='gray',label='train loss')
    ax.plot(history_s.history['val_loss'],color='black',label='validation loss')
    ax.legend()
    ax.set_xlabel('epoch');
    ax.set_ylabel('Loss');
```

Prediction of the model on the validation set:

```python
ynn_det = nn_model_det.predict(norm(Xval))
if stoch:
    ynn_sto = nn_model_gauss.predict(norm(Xval))

```

```python
fig, ax = plt.subplots(figsize=(6,6))
ax.plot(yval.ravel(),ynn_det[...,0].ravel(),'.', label='NN')

if stoch:
    ax.plot(yval.ravel(),ynn_sto[...,0].ravel(),'.', alpha=.5,label='NN stoch.')
    ax.legend()
ax.plot([-10,15],[-10,15],'-k');
```

```python
# Calibrate the stochastic NN
obs_mean_err = np.mean(np.square(yval-ynn_sto[:,:,0]))
sim_mean_err = np.mean(np.square(ynn_sto[...,1]))
print(obs_mean_err,sim_mean_err)
calb = np.sqrt(obs_mean_err / sim_mean_err)
cal_mean_err = np.mean(np.square(calb*ynn_sto[...,1]))
print(obs_mean_err,cal_mean_err)
calb=1
print(calb)
```

    0.00012994660245972372 8.722934e-05
    0.00012994660245972372 0.00012994658
    1

```python
if stoch:
    Nt = 800

    fig, ax = plt.subplots(nrows=2,figsize=(10,6),sharex='all')
    d1 = ax[0].imshow(np.abs(yval[:Nt].T-ynn_sto[:Nt,:,0].T),extent=[ttval[0],ttval[Nt],0,M],aspect='auto',cmap='jet',vmax=.1)
    d2 = ax[1].imshow(calb*ynn_sto[:Nt,:,1].T,extent=[ttval[0],ttval[Nt],0,M],aspect='auto',cmap='jet',vmax=.1)
    fig.colorbar(d1, ax=ax, orientation='vertical');
```

Now we compare only one coordinate (15)

```python
if stoch:
    Nt = 2000
    fig, ax = plt.subplots(figsize=(8,4))
    ax.plot(ttval[:Nt],np.abs(yval[:Nt,15]-ynn_sto[:Nt,15,0]), label='True error')
    ax.plot(ttval[:Nt],calb*ynn_sto[:Nt,15,1], label='Estimated std')
    ax.legend()

```

The normalized distribution of the errors is expected to be a Gaussian of mean zero ans standard deviation. We can see there is still a bias.

```python
if stoch:
    delta = (yval[:Nt]-ynn_sto[:Nt,:,0])/calb*ynn_sto[:Nt,:,1]
    plt.hist(delta.ravel(),bins=100, density=True)
    xx = np.arange(-4,4,.01)
    #yy = 1/(np.sqrt(2 * np.pi)) * np.exp( - xx**2 / 2)
    #plt.plot(xx,yy)
    #plt.xlim((-4,4));

```

Define the resolvant of the NN nodel. We have 3 resolvants:
- The output of the determistic neural net ```forward_d```
-  The average of the gaussian neural net: ```forward_sd```
-  The average of the  gaussian neural net perturbed by a random noise of with the standard deviation given by the
neural net: ```forward_ss```

```python
#Deterministic model: constant standard deviation
std_d = np.sqrt(np.mean(np.square(ynn_det.squeeze() - yval)))
print(std_d)

def forward_ds(v):
    epsi = np.random.randn(*v.shape)
    y = nn_model_det(norm(v), training=False)
    return y[...,0] + epsi*std_d

if stoch:
    def forward_ss(v):
        epsi = np.random.randn(*v.shape)
        y = nn_model_gauss(norm(v), training=False)
        return  y[...,0] + calb*epsi*np.sqrt(y[...,1])
```

    0.008948416762669149

Duplicate the test ensemble

```python
Xtest = np.repeat(xtest,Nens,axis=1)

```

Integrate the NN model over the test dataset and plot one trajectory

```python

xtestnn_ds = simulate(forward_ds, Ktest, Xtest[0], M=M, N=Ntest*Nens,burnin=0)
xtestnn_ds = view_as_blocks(xtestnn_ds,(1,Nens,1)).squeeze()
if stoch:
    xtestnn_ss = simulate(forward_ss, Ktest, Xtest[0], M=M, N=Ntest*Nens,burnin=0)
    xtestnn_ss = view_as_blocks(xtestnn_ss,(1,Nens,1)).squeeze()
```

```python
xtestnn_ds_mean = np.mean(xtestnn_ds,axis=-1)
xtestnn_ss_mean = np.mean(xtestnn_ss,axis=-1)

```

```python
Nt=800
itest = 2

fig, ax = plt.subplots(nrows=2, figsize=(12,8),sharex=True)
ax[0].plot(ttest[:Nt],xtestnn_ds[:Nt,itest,15],color='C1',alpha=.25);
ax[0].plot(ttest[:Nt],xtestnn_ds_mean[:Nt,itest,15],color='C1',linewidth=3)
ax[0].plot(ttest[:Nt],xtest[:Nt,itest,15])
ax[1].plot(ttest[:Nt],xtestnn_ss[:Nt,itest,15],color='C1',alpha=.25);
ax[1].plot(ttest[:Nt],xtestnn_ss_mean[:Nt,itest,15],color='C1',linewidth=3)

ax[1].plot(ttest[:Nt],xtest[:Nt,itest,15])
```

Now we can plot the simulation of each of the simulations

```python

if stoch:
    fig, ax = plot_L96_2D(xtest[:800,1],xtestnn_ds_mean[:800,1],ttest[:800], ('True','NN stoch mean'))
    fig.suptitle('Mean of the deterministic neural network')

    fig, ax = plot_L96_2D(xtest[:800,1],xtestnn_ss_mean[:800,1],ttest[:800], ('True','NN stoch perturbed'))
    fig.suptitle('Mean of the stochastic neural network')

```

Now we can assess the forecast skill

```python
if stoch:
    rmse_nn_ds = rmse(xtest, xtestnn_ds_mean, axis=(1,2))
    rmse_nn_ss = rmse(xtest, xtestnn_ss_mean, axis=(1,2))

fig, ax = plt.subplots(figsize=(4,4))
if stoch:
    ax.plot(ttest[:600],rmse_nn_ds[:600], label= 'NN determ. mean')
    ax.plot(ttest[:600],rmse_nn_ss[:600], label= 'NN stoch. mean')

ax.legend();
```

```python
xtestnn_ds.shape
```

    (2000, 50, 40, 50)

```python
crps_ds = ps.crps_ensemble(xtest[:600,:,25], xtestnn_ds[:600,:,25],axis=-1)
crps_ss = ps.crps_ensemble(xtest[:600,:,25], xtestnn_ss[:600,:,25],axis=-1)

```

```python
crps_ds_mean = crps_ds.mean(axis=-1)
crps_ss_mean = crps_ss.mean(axis=-1)
```

```python
Nt=600
plt.plot(ttest[:Nt],crps_ds_mean[:Nt], label='determ')
plt.plot(ttest[:Nt],crps_ss_mean[:Nt], label='stoch')
plt.legend()
```

```python
#TODO:
# - extremes
# - rank histograms at differents time
```