## Notebook Synopsis:

Here I develop a set of models similar to that of Step-1-, here substituting the single trend component for a piecewise trend sub-model. Specifically I:

* Load the training data generated and saved in previous NB.
* Develop and combine piecewise trend, seasonal, and residual noise submodels similar to  previous NB.
* Compare models using WAIC or PSIS-LOOCV.
* Retain and save models predicted to perform better.

```python
import pickle
import pathlib
from platform import python_version as pyver

import pandas as pd
import numpy as np
import pymc3 as pm
import theano.tensor as tt

from sklearn.preprocessing import MinMaxScaler

import arviz as ar
import matplotlib.pyplot as pl
import matplotlib.dates as mdates
from matplotlib import rcParams
```

```python
def print_ver(pkg, name=None):
    try:
        print(f'{pkg.__name__}: {pkg.__version__}')
    except AttributeError:
        print(f'{name}: {pkg}')
```

```python
print_ver(pyver(), 'python')
for pi in [np, pd, pm, ar]:
    print_ver(pi)
```

    python: 3.7.3
    numpy: 1.16.4
    pandas: 0.24.2
    pymc3: 3.7
    arviz: 0.4.1

```python
%matplotlib inline

years = mdates.YearLocator(day=1)
months = mdates.MonthLocator(bymonthday=1)
rcParams['xtick.major.size'] = 8
rcParams['xtick.minor.size'] = 4
rcParams['xtick.minor.visible'] = True
rcParams['xtick.labelsize'] = 16
rcParams['ytick.labelsize'] = 16
rcParams['axes.labelsize'] = 16
rcParams['axes.titlesize'] = 18
rcParams['axes.formatter.limits'] = (-3, 2)
```

```python
df = pd.read_csv('../../dataJar/manning.csv')
```

```python
df.head()
```

      ds
      y

      0
      2007-12-10
      9.590761

      1
      2007-12-11
      8.519590

      2
      2007-12-12
      8.183677

      3
      2007-12-13
      8.072467

      4
      2007-12-14
      7.893572

```python
df['ds'] = pd.to_datetime(df['ds'])
df['y_scaled'] = df.y / df.y.max()
df['t'] = (df.ds - df.ds.min()) / (df.ds.max() - df.ds.min())
df.plot(x='ds', y='y', figsize=(16, 6),)
```

### Modeling a Piecewise Trend:

Within the context of Generalized Additive Models(GAMs), which arise from the simple additive combination of submodels, I develop here a set of models following $$y(t) = g(t) + s(t) + ar1(t)$$
where \\(y(t)\\) is the modeled signal (chlorophyll in the AOSTZ sector), \\(g(t) \\) is the trend (i.e *rate of change*) sub-model, \\(s(t)\\) is the seasonal sub-model, \\(ar1(t)\\) is the AR1 residual.

The piecewise model is implemented by inserting a fixed number of changepoints such that $$g(t) = (k + a(t)^T\delta)t + (m + a(t)^T\gamma)$$
where \\(k\\) is the base trend, modified by preset changpoints stored in a vector \\(s\\). At each unique changepoint \\(s_j\\) the trend is adjusted by \\(\delta_j\\), stored in a vector \\(\delta\\), everytime \\(t\\) surpasses a changepoint \\(s_j\\). Used for this purpose, \\(a(t)\\) is basically a vectorized switchboard that turns on for a given switchpoint such that
\begin{equation}
a(t) =
    \begin{cases} 1 , & \text{if $t \geq s_j$} \\
0 , & \text{otherwise}
\end{cases}
\end{equation}
The second part, \\( m + a(t)^T\gamma\\) ensures the segments defined by the switchpoints are connected. Here, \\(m\\) is an offset parameter, and \\(\gamma_j\\) is set to \\(-s_j\delta_j\\).

The issue though is to find the right number of preset changepoint that will capture actual changepoints while not bogging down the inference. Moreover, for the sake of practicality, these will need to be regularly spaced. Here I try several setups including, one change point at the beginning of the year, and one for every season (4pts/year), one every two months (6pts/year), and one for every month (as many changepoints as data points). The idea is then to put a rather restrictive  Laplace prior on \\(\delta\\) to rule out unlikely changepoints, effectively setting the corresponding \\(\delta_j\\) to 0.

First is to define some [helper functions as in the previous notebook](./Step-1-Modeling_AOSTZ_with_pymc3-simpletrend_fourier_seasonality_ar1_residual.ipynb#helpers):

```python
def fourier_series(t, p=12, n=1):
    """
    input:
    ------
    t [numpy array]: vector of time index
    p [int]: period
    n [int]: number of fourier components

    output:
    -------
    sinusoids [numpy array]: 2D array of cosines and sines
    """
    p = p / t.size
    wls = 2 * π * np.arange(1, n+1) / p
    x_ = wls * t[:, None]
    sinusoids = np.concatenate((np.cos(x_), np.sin(x_)), axis=1)
    return sinusoids

def seasonality(mdl, n_fourier, t):
    """
    m [pymc3 Model class]: model object
    n_fourier [int]: number of fourier components
    t [numpy array]: vector of time index
    """
    with mdl:
        σ = pm.Exponential('σ', 1)
        f_coefs = pm.Normal('fourier_coefs', 0, sd=1, shape=(n_fourier*2))
        season = tt.dot(fourier_series(t, n=n_fourier), f_coefs)
    return season

def piecewise_trend(mdl, s, t, a_t, obs,
                    k_prior_scale=5, δ_prior_scale=0.05, m_prior_scale=5):
    """
    input:
    ------

    mdl [pymc3 Model class]: model object
    s [numpy array]: changepoint vector
    t [numpy array]: time vector
    obs [numpy array]: vector of observations
    a_t [numpy int array]: 2D (t*s) adjustment indicator array
    k_prior_scale [float]: base trend normal prior scale parameter (default=5)
    δ_prior_scale [float]: trend adjustment laplace prior scale param. (default=0.05)
    m_prior_scale [float]: base offset normal prior scale param. (default=5)
    """

    with mdl:
        # Priors:
        k = pm.Normal('k', 0, k_prior_scale) # base trend prior
        if δ_prior_scale is None:
            δ_prior_scale = pm.Exponential('τ', 1.5)
        δ = pm.Laplace('δ', 0, δ_prior_scale, shape=s.size) # rate of change prior
        m  = pm.Normal('m', 0, m_prior_scale) # offset prior
        γ = -s * δ
        trend = (k + tt.dot(a_t, δ)) * t + (m + tt.dot(a_t, γ))
    return trend

def ar1_residual(mdl, n_obs):
    with mdl:
        k_ = pm.Uniform('k', -1.1, 1.1)
        tau_ = pm.Gamma('tau', 10, 3)
        ar1 = pm.AR1('ar1', k=k_, tau_e=tau_, shape=n_obs)
    return ar1

def changepoint_setup(t, n_changepoints, s_start=None, s=None, changepoint_range=1):
    """
    input:
    ------
    t [numpy array]: time vector
    n_changepoints [int]: number of changepoints to consider
    s [numpy array]: user-specified changepoint vector (default=None)
    s_start [int]: changepoint start index (default=0)
    changepoint_range[int]: adjustable time proportion (default=1)

    output:
    -------
    s [numpy array]: changepoint vector
    a_t [numpy int array]: 2D (t*s) adjustment indicator array
    """
    if s is None:
        if s_start is None:
            s = np.linspace(start=0, stop=changepoint_range*t.max(),
                            num=n_changepoints+1)[1:]
        else:
            s = np.linspace(start=s_start, stop=changepoint_range*t.max(),
                            num=n_changepoints)
    a_t = (t[:,None] > s) * 1
    return a_t, s

def model_runner(t_, obs_s, add_trend=False, add_season=False, add_AR1=False,
                **payload):
    mdl = pm.Model()
    a_t, s = None, None
    with mdl:
        y_ = 0
        σ = pm.HalfCauchy('σ', 2.5)
        if add_trend:
            n_switches = payload.pop('n_switches', t_.size)
            s_start = payload.pop('s_start', None)
            s = payload.pop('s', None)
            chg_pt_rng = payload.pop('changepoint_range', 1)
            k_prior_scale = payload.pop('k_prior_scale', 5)
            δ_prior_scale = payload.pop('δ_prior_scale', 0.05)
            m_prior_scale = payload.pop('m_prior_scale', 5)
            a_t, s = changepoint_setup(t_, n_switches, s_start=s_start, s=s,
                                       changepoint_range=chg_pt_rng)
            trend_ = piecewise_trend(mdl, s, t_, a_t, obs_s,
                                     k_prior_scale, δ_prior_scale, m_prior_scale)
            y_ += trend_
        if add_season:
            n_fourier = payload.pop('n_fourier', 4)
            season = seasonality(mdl, n_fourier=n_fourier, t=t_)
            y_ += season

        if add_AR1:
            ar1 = ar1_residual(mdl, obs_s.size)
            y_ += ar1
        pm.Normal('obs', mu=y_, sd=σ, observed=obs_s)
    return mdl, a_t, s

def sanity_check(m, df):
    """
    :param m: (pm.Model)
    :param df: (pd.DataFrame)
    """
    # Sample from the prior and check of the model is well defined.
    y = pm.sample_prior_predictive(model=m, vars=['obs'])['obs']
    pl.figure(figsize=(16, 6))
    pl.plot(y.mean(0), label='mean prior')
    pl.fill_between(np.arange(y.shape[1]), -y.std(0), y.std(0), alpha=0.25, label='standard deviation')
    pl.plot(df['y_scaled'], label='true value')
    pl.legend()

```

```python
mdl_trend_only, A, s_pts = model_runner(df.t, df.y_scaled, add_trend=True,
                             n_switches=25, changepoint_range=0.8)
```

```python
# And run the sanity check
sanity_check(mdl_trend_only, df)
```

```python
with mdl_trend_only:
    aprox = pm.find_MAP()
```

    /accounts/ekarakoy/anaconda3/envs/southern_ocean/lib/python3.7/site-packages/pymc3/tuning/starting.py:61: UserWarning: find_MAP should not be used to initialize the NUTS sampler, simply call pymc3.sample() and it will automatically initialize NUTS in a better way.
      warnings.warn('find_MAP should not be used to initialize the NUTS sampler, simply call pymc3.sample() and it will automatically initialize NUTS in a better way.')
    logp = 4,025.2, ||grad|| = 142.95: 100%|██████████| 498/498 [00:00<00:00, 588.37it/s]

```python
def det_trend(k, m, delta, t, s, A):
    return (k + np.dot(A, delta)) * t + (m + np.dot(A, (-s * delta)))

g = det_trend(aprox['k'], aprox['m'], aprox['δ'],
              df['t'], s_pts, A) * df['y'].max()

pl.figure(figsize=(16, 6))
pl.title('$g(t)$')
pl.plot(g)
pl.scatter(np.arange(df.shape[0]), df.y, s=0.5, color='black')
```

```python
mdl_trend_season, A, s_pts = model_runner(df.t, df.y_scaled, add_trend=True,
                                          n_switches=25, changepoint_range=0.8,
                                          δ_prior_scale=None, n_fourier=4)
```

```python
sanity_check(mdl_trend_season, df)
```

```python

```


# Selección óptima de portafolios II

Entonces, tenemos que:
- La LAC describe las posibles selecciones de riesgo-rendimiento entre un activo libre de riesgo y un activo riesgoso.
- Su pendiente es igual al radio de Sharpe del activo riesgoso.
- La asignación óptima de capital para cualquier inversionista es el punto tangente de la curva de indiferencia del inversionista con la LAC.

Para todo lo anterior, supusimos que ya teníamos el portafolio óptimo (activo riesgoso).

En la clase pasada aprendimos a hallar este portafolio óptimo si el conjunto de activos riesgosos estaba conformado únicamente por dos activos:

$$w_{1,EMV}=\frac{(E[r_1]-r_f)\sigma_2^2-(E[r_2]-r_f)\sigma_{12}}{(E[r_2]-r_f)\sigma_1^2+(E[r_1]-r_f)\sigma_2^2-((E[r_1]-r_f)+(E[r_2]-r_f))\sigma_{12}}.$$

- Sin embargo, la complejidad del problema crece considerablemente con el número de variables, y la solución analítica deja de ser viable cuando mencionamos que un portafolio bien diversificado consta aproximadamente de 50-60 activos.
- En esos casos, este problema se soluciona con rutinas numéricas que hagan la optimización por nosotros, porque son una solución viable y escalable a más variables.

**Objetivos:**
- ¿Cuál es el portafolio óptimo de activos riesgosos cuando tenemos más de dos activos?
 - ¿Cómo construir la frontera de mínima varianza cuando tenemos más de dos activos?

*Referencia:*
- Notas del curso "Portfolio Selection and Risk Management", Rice University, disponible en Coursera.
___

## 1. Maximizando el radio de Sharpe

### ¿Qué pasa si tenemos más de dos activos riesgosos?

En realidad es algo muy similar a lo que teníamos con dos activos.
- Para dos activos, construir la frontera de mínima varianza es trivial: todas las posibles combinaciones.
- Con más de dos activos, recordar la definición: la frontera de mínima varianza es el lugar geométrico de los portafolios que proveen el mínimo riesgo para un nivel de rendimiento dado.

 Ver en el tablero.

Analíticamente:
- $n$ activos,
- caracterizados por $(\sigma_i,E[r_i])$,
- cada uno con peso $w_i$, con $i=1,2,\dots,n$.

Entonces, buscamos los pesos tales que

\begin{align}
\min_{w_1,\dots,w_n} & \quad \sum_{i=1}^{n}w_i^2\sigma_i^2+\sum_{i=1}^{n}\sum_{j=1,j\neq i}^{n}w_iw_j\sigma_{ij}\\
\text{s.a.}          & \quad \sum_{i=1}^{n}w_i=1, w_i\geq0\\
                     & \quad \sum_{i=1}^{n}w_iE[r_i]=\bar{\mu},
\end{align}

donde $\bar{\mu}$ corresponde a un nivel de rendimiento objetivo.

**Obviamente, tendríamos que resolver este problema para muchos niveles de rendimiento objetivo.**

-  Explicar relación con gráfica.

-  Recordar clase 10.

Lo anterior se puede escribir vectorialmente como:

\begin{align}
\min_{\boldsymbol{w}} & \quad \boldsymbol{w}^T\Sigma\boldsymbol{w}\\
\text{s.a.}          & \quad \boldsymbol{1}^T\boldsymbol{w}=1, \boldsymbol{w}\geq0\\
                     & \quad E[\boldsymbol{r}^T]\boldsymbol{w}=\bar{\mu},
\end{align}

donde:
- $\boldsymbol{w}=\left[w_1,\dots,w_n\right]^T$ es el vector de pesos,
- $\boldsymbol{1}=\left[1,\dots,1\right]^T$ es un vector de unos,
- $E[\boldsymbol{r}]=\left[E[r_1],\dots,E[r_n]\right]^T$ es el vector de rendimientos esperados, y
- $\Sigma=\left[\begin{array}{cccc}\sigma_{1}^2 & \sigma_{12}  & \dots  & \sigma_{1n} \\
                                   \sigma_{21}  & \sigma_{2}^2 & \dots  & \sigma_{2n} \\
                                   \vdots       & \vdots       & \ddots & \vdots      \\
                                   \sigma_{n1}  & \sigma_{n2}  & \dots  & \sigma_{n}^2\end{array}\right]$ es la matriz de varianza-covarianza.

**Esta última forma es la que comúnmente usamos al programar, por ser eficiente y escalable a problemas de N variables.**

### Entonces, ¿para cuántos niveles de rendimiento objetivo tendríamos que resolver el anterior problema con el fin de graficar la frontera de mínima varianza?
- Observar que el problema puede volverse muy pesado a medida que incrementamos el número de activos en nuestro portafolio...
- Una tarea bastante compleja.

### Sucede que, en realidad, sólo necesitamos conocer dos portafolios que estén sobre la *frontera de mínima varianza*.
- Si logramos encontrar dos portafolios sobre la frontera, entonces podemos a la vez encontrar todas las posibles combinaciones de estos dos portafolios para trazar la frontera de mínima varianza.
- Ver el caso de dos activos.

### ¿Qué portafolios usar?
Hasta ahora, hemos estudiando profundamente como hallar dos portafolios muy importantes que de hecho yacen sobre la frontera de mínima varianza:
1. Portafolio de EMV: máximo SR.
2. Portafolio de mínima varianza: básicamente, el mismo problema anterior, sin la restricción de rendimiento objetivo.

Luego, tomar todas las posibles combinaciones de dichos portafolios usando las fórmulas para dos activos de medias y varianzas:
- w: peso para el portafolio EMV,
- 1-w: peso para le portafolio de mínima varianza.

## 2. Ejemplo ilustrativo.

Retomamos el ejemplo de mercados de acciones en los países integrantes del $G5$: EU, RU, Francia, Alemania y Japón.

```python
# Importamos pandas y numpy
import pandas as pd
import numpy as np
```

```python
# Resumen en base anual de rendimientos esperados y volatilidades
annual_ret_summ = pd.DataFrame(columns=['EU', 'RU', 'Francia', 'Alemania', 'Japon'], index=['Media', 'Volatilidad'])
annual_ret_summ.loc['Media'] = np.array([0.1355, 0.1589, 0.1519, 0.1435, 0.1497])
annual_ret_summ.loc['Volatilidad'] = np.array([0.1535, 0.2430, 0.2324, 0.2038, 0.2298])

annual_ret_summ.round(4)
```

      EU
      RU
      Francia
      Alemania
      Japon

      Media
      0.1355
      0.1589
      0.1519
      0.1435
      0.1497

      Volatilidad
      0.1535
      0.243
      0.2324
      0.2038
      0.2298

```python
# Matriz de correlación
corr = pd.DataFrame(data= np.array([[1.0000, 0.5003, 0.4398, 0.3681, 0.2663],
                                    [0.5003, 1.0000, 0.5420, 0.4265, 0.3581],
                                    [0.4398, 0.5420, 1.0000, 0.6032, 0.3923],
                                    [0.3681, 0.4265, 0.6032, 1.0000, 0.3663],
                                    [0.2663, 0.3581, 0.3923, 0.3663, 1.0000]]),
                    columns=annual_ret_summ.columns, index=annual_ret_summ.columns)
corr.round(4)
```

      EU
      RU
      Francia
      Alemania
      Japon

      EU
      1.0000
      0.5003
      0.4398
      0.3681
      0.2663

      RU
      0.5003
      1.0000
      0.5420
      0.4265
      0.3581

      Francia
      0.4398
      0.5420
      1.0000
      0.6032
      0.3923

      Alemania
      0.3681
      0.4265
      0.6032
      1.0000
      0.3663

      Japon
      0.2663
      0.3581
      0.3923
      0.3663
      1.0000

```python
# Tasa libre de riesgo
rf = 0.05
```

Esta vez, supondremos que tenemos disponibles todos los mercados de acciones y el activo libre de riesgo.

#### 1. Construir la frontera de mínima varianza

##### 1.1. Encontrar portafolio de mínima varianza

```python
# Importamos funcion minimize del modulo optimize de scipy
from scipy.optimize import minimize
```

```python
## Construcción de parámetros
# 1. Sigma: matriz de varianza-covarianza Sigma = S.dot(corr).dot(S)
S = np.diag(annual_ret_summ.loc['Volatilidad', :].values)
Sigma = S.dot(corr).dot(S)
# 2. Eind: rendimientos esperados activos individuales
Eind = annual_ret_summ.loc['Media', :].values
```

```python
# Función objetivo
def varianza(w, Sigma):
    return w.T.dot(Sigma).dot(w)
```

```python
# Dato inicial
n = len(Eind)
w0 = np.ones(n) / n
# Cotas de las variables
bnds = ((0, 1),) * n
# Restricciones
cons = {'type': 'eq', 'fun': lambda w: w.sum() - 1}
```

```python
# Portafolio de mínima varianza
minvar = minimize(fun=varianza, x0=w0, args=(Sigma,), bounds=bnds, constraints=cons)
minvar
```

         fun: 0.01861776391061502
         jac: array([0.03718246, 0.03881475, 0.03859101, 0.03755156, 0.0370423 ])
     message: 'Optimization terminated successfully.'
        nfev: 49
         nit: 7
        njev: 7
      status: 0
     success: True
           x: array([6.17797049e-01, 3.46944695e-18, 0.00000000e+00, 2.09394358e-01,
           1.72808594e-01])

```python
# Pesos, rendimiento y riesgo del portafolio de mínima varianza
w_minvar = minvar.x
E_minvar = Eind.T.dot(w_minvar)
s_minvar = (w_minvar.T.dot(Sigma).dot(w_minvar))**0.5
w_minvar, E_minvar, s_minvar
```

    (array([6.17797049e-01, 3.46944695e-18, 0.00000000e+00, 2.09394358e-01,
            1.72808594e-01]),
     0.13962903688859557,
     0.13644692708381168)

##### 1.2. Encontrar portafolio EMV

```python
# Función objetivo
def minus_SR(w, Sigma, Eind, rf):
    sp = (w.T.dot(Sigma).dot(w))**0.5
    Ep = Eind.T.dot(w)
    SR = (Ep - rf) / sp
    return -SR
```

```python
# Dato inicial
n = len(Eind)
w0 = np.ones(n) / n
# Cotas de las variables
bnds = ((0, 1),) * n
# Restricciones
cons = {'type': 'eq', 'fun': lambda w: w.sum() - 1}
```

```python
# Portafolio EMV
EMV = minimize(fun=minus_SR, x0=w0, args=(Sigma, Eind, rf), bounds=bnds, constraints=cons)
EMV
```

         fun: -0.6644372965632436
         jac: array([-0.36088951, -0.36076408, -0.36036385, -0.36108153, -0.36062376])
     message: 'Optimization terminated successfully.'
        nfev: 35
         nit: 5
        njev: 5
      status: 0
     success: True
           x: array([0.50714174, 0.07470888, 0.02471533, 0.18943972, 0.20399434])

```python
# Pesos, rendimiento y riesgo del portafolio EMV
w_EMV = EMV.x
E_EMV = Eind.T.dot(w_EMV)
s_EMV = (w_EMV.T.dot(Sigma).dot(w_EMV))**0.5
w_EMV, E_EMV, s_EMV
```

    (array([0.50714174, 0.07470888, 0.02471533, 0.18943972, 0.20399434]),
     0.1420657564717856,
     0.13856199365687238)

##### 1.3. Construir frontera de mínima varianza

También debemos encontrar la covarianza (o correlación) entre estos dos portafolios:

```python
# Covarianza entre los portafolios
cov = w_EMV.T.dot(Sigma).dot(w_minvar)
cov
```

    0.018690275386034134

```python
# Correlación entre los portafolios
corr = cov / (s_EMV * s_minvar)
corr
```

    0.9885708894197612

```python
# Vector de w
w = np.linspace(0, 3, 100)
```

```python
# DataFrame de portafolios:
# 1. Índice: i
# 2. Columnas 1-2: w, 1-w
# 3. Columnas 3-4: E[r], sigma
# 4. Columna 5: Sharpe ratio
portafolios = pd.DataFrame(data={'w': w,
                                 '1-w': 1 - w,
                                 'Media': w * E_EMV + (1 - w) * E_minvar,
                                 'Vol': ((w * s_EMV)**2 + ((1 - w) * s_minvar)**2 + 2 * w * (1 - w) * cov)**0.5                             })
portafolios['RS'] = (portafolios['Media'] - rf) / portafolios['Vol']
portafolios.head()
```

      w
      1-w
      Media
      Vol
      RS

      0
      0.000000
      1.000000
      0.139629
      0.136447
      0.656878

      1
      0.030303
      0.969697
      0.139703
      0.136464
      0.657335

      2
      0.060606
      0.939394
      0.139777
      0.136485
      0.657777

      3
      0.090909
      0.909091
      0.139851
      0.136508
      0.658205

      4
      0.121212
      0.878788
      0.139924
      0.136535
      0.658619

```python
# Importar librerías de gráficos
from matplotlib import pyplot as plt
%matplotlib inline
```

```python
# Gráfica de dispersión de puntos coloreando
# de acuerdo a SR, los activos individuales
# y los portafolios hallados
plt.figure(figsize=(6, 4))
plt.scatter(portafolios['Vol'], portafolios['Media'], c=portafolios['RS'], cmap='RdYlBu', label='Front. Min. Var.')
plt.plot(s_minvar, E_minvar, 'og', ms=7, label='Port. Min. Var.')
plt.plot(s_EMV, E_EMV, 'or', ms=7, label='Port. EMV')
plt.legend(loc='best')
plt.colorbar()
plt.xlabel("Volatilidad $\sigma$")
plt.ylabel("Rendimiento esperado $E[r]$")
plt.grid()
plt.axis([xmin, xmax, ymin, ymax])
```

**A partir de lo anterior, solo restaría construir la LAC y elegir la distribución de capital de acuerdo a las preferencias (aversión al riesgo).**
___

## 3. Comentarios finales

### 3.1. Restricciones adicionales

Los inversionistas pueden tener restricciones adicionales:
1. Restricciones en posiciones cortas.
2. Pueden requerir un rendimiento mínimo.
3. Inversión socialmente responsable: prescinden de inversiones en negocios o paises considerados éticamente o políticamente indeseables.

Todo lo anterior se puede incluir como restricciones en el problema de optimización, y puede ser llevado a cabo a costa de un cociente de Sharpe menor.

### 3.2. Críticas a la optimización media varianza
1. Solo importan medias y varianzas: recordar que la varianza subestima el riesgo en algunos casos.
2. Preferencias media-varianza tratan las ganancias y pérdidas simétricamente: el sentimiento de insatisfacción de una perdida es mayor al sentimiento de satisfacción de una ganancia (aversión a pérdidas).
3. La aversión al riesgo es constante: la actitud frente al riesgo puede cambiar, por ejemplo con el estado de la economía.
4. Horizonte corto (un periodo).
5. Basura entra - basura sale: la optimización media varianza es supremamente sensible a las entradas: estimaciones de rendimientos esperados y varianzas.
___

# Anuncios parroquiales

## 1. Quiz la próxima clase (clases 12, 13, y 14).
## 2. Revisar archivo Tarea 6. Definir fecha.
## 3. Fin de módulo. Abrir Clase0 y revisar objetivos del módulo.
## 4. [Nota interesante](http://yetanothermathprogrammingconsultant.blogspot.com/2016/08/portfolio-optimization-maximize-sharpe.html)

Created with Jupyter by Esteban Jiménez Rodríguez.


Runge-Kutta methods
===================

```
%matplotlib inline
```

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

matplotlib.rcParams.update({'font.size': 14})
```

Considering IVPs of the form

$${\boldsymbol{y}}'(x) = {\boldsymbol{f}}(x, {\boldsymbol{y}}(x)).$$

We looked at multistage (e.g. Runge-Kutta) methods. These

-   self start,

-   compute next iteration by combining estimates of ${\boldsymbol{f}}$

to improve order of accuracy.

Methods constructed by matching terms in Taylor expansion; ensures local
truncation error is a certain order. May improve efficiency by checking
if results are “good enough” at given $h$.

### Comparing errors

Use $y_e(x_0 + h)$ as the *exact* solution to

$$y'(x) = f(x, y(x)), \quad y(x_0) = y_0.$$

Consider numerical solution $y_h(x_0 + h)$ for one step (many stages) of
a multistage method. By construction the Taylor expansion implies

$$| y_e(x_0 + h) - y_h(x_0 + h) | = C h^{s+1}.$$

Know that method has order $s$; to compute error also
need $C$.

Can use Richardson extrapolation variant: compare the error with two
different step sizes.

By assumption taking two steps of size $h/2$ gives

$$| y_e(x_0 + h) - y_{h/2}(x_0 + h) | = 2 C (h/2)^{s+1}.$$

Rearranging gives

$$\text{Local truncation error} = C h^{s+1} = \frac{y_h - y_{h/2}}{1 - 2^{-s}}.$$

### Adaptive stepping

Given a computable estimate of the error, decide whether our result is
sufficiently accurate automatically. As with adaptive quadrature, can
modify $h$ where necessary.

Two key differences between algorithms for quadrature and IVPs.

1.  IVP step depends on previous step: $h_1$ at $x_1$ different from $h_0$ at $x_0$.

    Expect accuracy depends on local properties ($\lambda \sim
    |\partial_y f|$); always use latest $h_1$, not initial $h_0$.

    Must *increase* $h$ when accuracy sufficient.

2.  Richardson extrapolation expensive (3 times original algorithm).

Instead combine estimates in different ways to estimate error
(e.g. RKF45: 6 evaluations, $4^{\text{th}}$ order update and
$5^{\text{th}}$ order error check).

RKF45 seems optimal for number of function evaluations:

| Number of function evaluations | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|:------------------------------:|---|---|---|---|---|---|---|---|
| Maximum order of R-K method    | 1 | 2 | 3 | 4 | 4 | 5 | 6 | 6 |

Multistep Methods
=================

Loosely a multistage method approximates $y'$ in

$$y'(x) = f(x, y(x)), \quad y(x_0) = y_0.$$

Loosely a *multistep* method approximates the quadrature solution

$$y_{n+1} - y_n = \int_{x_n}^{x_{n+1}} f(x, y(x)) \, d x.$$

Difference comes from interpretation and assumption: that the quadrature
can be well approximated using a formula depending on $y_{n-j}$ (and
$x_{n-j}$). This gives the general formula

$$a_k y_{n+1} + a_{k-1} y_n + \dots + a_0 y_{n+1-k} = h \left[ b_k f_{n+1} + b_{k-1} f_n + \dots + b_0 f_{n+1-k} \right].$$

### $k$-step methods

In a *$k$-step method*

$$a_k y_{n+1} + a_{k-1} y_n + \dots + a_0 y_{n+1-k} = h \left[ b_k f_{n+1} + b_{k-1} f_n + \dots + b_0 f_{n+1-k} \right]$$

all terms on the left (except $y_{n+1}$) are known, and all terms on the
right (except $f_{n+1}$) are computable. The coefficients $a_m,
  b_m$ fix the method.

To give $y_{n+1}$ (essential!) must have $a_k \neq 0$.

If $b_k = 0$ can directly compute $y_{n+1}$; the method is *explicit*.

If $b_k \neq 0$ then required $f_{n+1}$ is not computable; the method is
*implicit*. Either

1.  use iteration to solve nonlinear equation, *or*

2.  use predictor-corrector method (usual approach).

Adams-Bashforth methods
-----------------------

The Adams-Bashforth methods are explicit $k$-step methods with formula

$$y_{n+1} - y_n = h \left[ b_{k-1} f_n + b_{k-2} f_{n-1} + \dots \right]$$

based on evenly spaced points $x_j$.

To compute the coefficients, require that the method is “as accurate as
possible”; i.e., that it is exact for as many polynomials as possible.

### Adams-Bashforth methods: example

For example, the 2-step method

$$y_{n+1} - y_n = h \left[ b_1 f_n + b_0 f_{n-1} \right]$$

should be exact for polynomials of degree 0, 1. Approximate

\begin{align}
  y_{n+1} - y_n & = \int_{x_n}^{x_{n+1}} f(x, y(x)) \, dx \\
  & \simeq h \left[ b_1 f_n + b_0 f_{n-1} \right].
\end{align}

Insist the formula holds for $f(x) = p_m(x) = x^m$ for $m = 0, 1$.

Without loss of generality choose $x_n = 0$, $x_{n-1} = -h$:

\begin{align}
  p_0 & = 1: & h & = h \left[ b_1 + b_0 \right] \\
  p_1 & = x: & h^2 / 2 & = h \left[ -h b_0 \right].
\end{align}

Hence $b_0 = -1 / 2$, $b_1 = 3 / 2$.

### Standard example

Apply the 2 step Adams-Bashforth method to

$$y'(x) = - \sin(x), \quad y(0) = 1$$

and integrate to $x = 0.5$. Use Euler predictor-corrector to start
algorithm. Using $h = 0.1$ gives

|$n$|$x_n$|$y_n$|$f(x_n, y_n)$|$\cos(x_n)$|
|---|-----|-----|-------------|-----------|
|0  | 0.0 |1.000| 0.000       | 1.000     |
|1  | 0.1 |0.995| -0.100      | 0.995     |
|2  | 0.2 |0.980| -0.199      | 0.980     |
|3  | 0.3 |0.955| -0.296      | 0.955     |
|4  | 0.4 |0.921| -0.389      | 0.921     |
|5  | 0.5 |0.877|             | 0.878     |

The error is $0.4\%$, just visible at this precision. With $h =
  0.01$ the error is $0.006\%$. Convergence is nearly second order;
result is slightly biased by the starting method.

```
def ab2_method(f, x_end, y0, N):
    """Solve IVP y'=f(x, y(x)) on x \in [0, x_end] with y(0) = y0 using N+1 points, using 2nd order Adams-Bashforth method."""

    h = x_end / N
    x = np.linspace(0.0, x_end, N+1)

    y = np.zeros((N+1, len(y0)))
    fn = np.zeros_like(y)
    y[0, :] = y0

    # Start using Euler predictor-corrector step
    fn[0, :] = f(x[0], y[0, :])
    yp = y[0, :] + h * fn[0, :]
    y[1, :] = y[0, :] + h / 2.0 * (fn[0, :] + f(x[1], yp))

    for n in range(1, N):
        fn[n, :] = f(x[n], y[n, :])
        y[n+1, :] = y[n, :] + h / 2.0 * (3.0 * fn[n, :] - fn[n-1, :])

    return x, y

def fn_simple(x, y):
    """Simple function for example"""

    return -np.sin(x)

# Example given above
x_5, y_5 = ab2_method(fn_simple, 0.5, [1.0], 5)
x_50, y_50 = ab2_method(fn_simple, 0.5, [1.0], 50)

print("Solution at x = 0.5 using h = 0.1 is y = {}.".format(y_5[-1, 0]))
print("Solution at x = 0.5 using h = 0.01 is y = {}.".format(y_50[-1, 0]))
```

    Solution at x = 0.5 using h = 0.1 is y = 0.877193282411.
    Solution at x = 0.5 using h = 0.01 is y = 0.877577581123.

```
Npoints = np.array([2**i for i in range(3, 12)])
h = 0.5 / Npoints
err = np.zeros_like(h)

for i in range(len(Npoints)):
    N = Npoints[i]
    x, y = ab2_method(fn_simple, 0.5, [1.0], N)
    err[i] = np.abs(np.cos(0.5) - y[-1, 0])

# Find the best fit to the data: if err ~ A h^s then the best fit straight line of log(err) vs log(h) has slope s.
p = np.polyfit(np.log(h[1:]), np.log(err[1:]), 1)

fig = plt.figure(figsize = (10,6))
ax = fig.add_subplot(111)
ax.loglog(h, err, 'kx', label = 'Numerical data')
ax.loglog(h, np.exp(p[1])*h**(p[0]), 'b-', label = "Line slope {:.2f}".format(p[0]))
ax.set_xlabel('h')
ax.set_ylabel('$\|$ Error $\|$')
ax.set_title("Convergence of the $2^{nd}$ order Adams-Bashforth method for $y' = -\sin(x)$")
ax.legend(loc = 2)
fig.tight_layout()
plt.show()
```

### Standard Example: 2

Consider the system

$$\left\{
\begin{aligned}
  \dot{x} & = -y \\ \dot{y} & = x
\end{aligned} \right., \quad x(0) = 1, \, \, y(0) = 0.$$

In polar coordinates this is $\dot{r} = 0$, $\dot{\phi} = 1$.

Use Adams-Bashforth 2 step method with $h=0.1$. At
$t=500$ the result matches the correct answer to the eye.
The growth of the radius makes the errors visible.

Use Adams-Bashforth 2 step method with $h=0.01$. At
$t=500$ the result matches the correct answer to the eye.

Looking at the growth of the radius makes the errors visible even now.

```
def fn_circular_motion(x, y):
    """Simple function for Euler's method example"""

    dfdt = np.zeros_like(y)
    dfdt[0] = -y[1]
    dfdt[1] =  y[0]

    return dfdt

# Example given above
y0 = np.array([1.0, 0.0])
t_0_1, y_0_1 = ab2_method(fn_circular_motion, 500.0, y0, 5000)
t_0_01, y_0_01 = ab2_method(fn_circular_motion, 500.0, y0, 50000)

fig = plt.figure(figsize = (10,10))
ax1 = fig.add_subplot(221)
ax1.plot(y_0_1[:, 0], y_0_1[:, 1], 'b-', label = '$h = 0.1$')
ax1.set_xlabel('$x$')
ax1.set_ylabel('$y$')
ax2 = fig.add_subplot(222)
ax2.plot(y_0_01[:, 0], y_0_01[:, 1], 'b-', label = '$h = 0.01$')
ax2.set_xlabel('$x$')
ax2.set_ylabel('$y$')
ax3 = fig.add_subplot(223)
ax3.plot(t_0_1, np.abs(y_0_1[:, 0]**2 + y_0_1[:, 1]**2 - 1.0), 'b-', label = '$h = 0.1$')
ax3.set_xlabel('$t$')
ax3.set_ylabel('Growth in $r$')
ax4 = fig.add_subplot(224)
ax4.plot(t_0_01, np.abs(y_0_01[:, 0]**2 + y_0_01[:, 1]**2 - 1.0), 'b-', label = '$h = 0.01$')
ax4.set_xlabel('$t$')
ax4.set_ylabel('Growth in $r$')
fig.tight_layout()
plt.show()
```

### Higher order Adams-Bashforth

Principle behind higher order Adams-Bashforth methods the same; solve
for the coefficients by integrating

$$y_{n+1} - y_n = h \left[ b_k f_n + b_{k-1} f_{n-1} + \dots b_0
f_{n + 1 - k} \right]$$

over the range, assuming $f(x) = p_s(x)$ for polynomials of degree
$s = 0, \dots k$.

Easiest to choose as a basis for the polynomials

$$p_{s+1}(x) = x (x + h) \dots (x + h s)$$

as $p_{s+1}$ vanishes at $x = 0, -h, \dots, -h s$. Then choose
$x_n = 0, x_{n - 1} = -h$ and so on: the resulting system of equations
is in upper-triangular form – solve by back substitution.

### Higher order Adams-Bashforth example

For example, the Adams-Bashforth method of order five has formula

$$y_{n+1} - y_n = h \left[ b_4 f_n + b_3 f_{n-1} + b_2 f_{n-2} + b_1 f_{n-3} + b_0 f_{n-4} \right] ,$$

and the coefficients follow from

\begin{align}
p_0 & = 1: & h & = h \left[ b_4 + b_3 + b_2 + b_1 + b_0 \right] \\
p_1 & = x: & \frac{h^2}{2} & = h \left[ -h \left( b_3 + 2 b_2 + 3
    b_1 + 4 b_0 \right) \right] \\
p_2 & = x (x + h): & 5 \frac{h^3}{6} & = h \left[ h^2 \left( 2 b_2
    + 6 b_1 + 12 b_0 \right) \right] \\
p_3 & = x (x + h) (x + 2 h): & 9 \frac{h^4}{4} & = h \left[ -h^3
  \left( 6 b_1 + 24 b_0 \right) \right] \\
p_4 & = x (x + h) (x + 2 h) (x + 3 h): & 251 \frac{h^5}{30} & = h
\left[ 24 h^4 b_0 \right].
\end{align}

### Standard example

Apply the 5 step Adams-Bashforth method to

$$y'(x) = - \sin(x), \quad y(0) = 1$$

and integrate to $x = 0.5$. Use RK4 method to start algorithm. Using
$h = 0.05$ gives

|$n$|$x_n$|$y_n$|$f(x_n, y_n)$|$\cos(x_n)$|
|---|-----|-----|-------------|-----------|
|0 | 0.0 | 1.000 | 0.000 | 1.000|
|2 | 0.1 | 0.995 | -0.100 | 0.995|
|4 | 0.2 | 0.980 | -0.199 | 0.980|
|6 | 0.3 | 0.955 | -0.296 | 0.955|
|8 | 0.4 | 0.921 | -0.389 | 0.921|
|10 |0.5 |0.878| |0.878|

The error is $3 \times 10^{-6}\%$, not visible at this precision. With
$h = 0.01$ the error is $2 \times 10^{-9}\%$. Convergence is slightly
worse than fifth order; result is slightly biased by the starting
method.

```
def ab5_method(f, x_end, y0, N):
    """Solve IVP y'=f(x, y(x)) on x \in [0, x_end] with y(0) = y0 using N+1 points, using 5th order Adams-Bashforth method."""

    h = x_end / N
    x = np.linspace(0.0, x_end, N+1)

    y = np.zeros((N+1, len(y0)))
    fn = np.zeros_like(y)
    y[0, :] = y0

    # Start using RK4 steps
    for n in range(4):
        fn[n, :] = f(x[n], y[n, :])
        k1 = h * fn[n, :]
        k2 = h * f(x[n] + h / 2.0, y[n, :] + k1 / 2.0)
        k3 = h * f(x[n] + h / 2.0, y[n, :] + k2 / 2.0)
        k4 = h * f(x[n] + h, y[n, :] + k3)
        y[n+1, :] = y[n, :] +  (k1 + k4 + 2.0 * (k2 + k3)) / 6.0

    # Solve for the coefficients

    coeff_A = np.array([[1.0, 1.0, 1.0, 1.0, 1.0],[0.0, -1.0, -2.0, -3.0, -4.0],[0.0, 0.0, 2.0, 6.0, 12.0],
                        [0.0, 0.0, 0.0, -6.0, -24.0],[0.0, 0.0, 0.0, 0.0, 24.0]])
    coeff_b = np.array([1.0, 1.0 / 2.0, 5.0/6.0, 9.0/4.0, 251.0/30.0])
    b = np.linalg.solve(coeff_A, coeff_b)

    for n in range(4, N):
        fn[n, :] = f(x[n], y[n, :])
        y[n+1, :] = y[n, :] + h * (b[0]*fn[n, :] + b[1]*fn[n-1, :] + b[2]*fn[n-2, :] + b[3]*fn[n-3, :] + b[4]*fn[n-4, :])

    return x, y

def fn_simple(x, y):
    """Simple function for example"""

    return -np.sin(x)

# Example given above
x_5, y_5 = ab5_method(fn_simple, 0.5, [1.0], 5)
x_50, y_50 = ab5_method(fn_simple, 0.5, [1.0], 50)

print("Solution at x = 0.5 using h = 0.1 is y = {}.".format(y_5[-1, 0]))
print("Error at x = 0.5 using h = 0.1 is y = {}.".format(abs(np.cos(0.5) - y_5[-1, 0])))
print("Solution at x = 0.5 using h = 0.01 is y = {}.".format(y_50[-1, 0]))
print("Error at x = 0.5 using h = 0.01 is y = {}.".format(abs(np.cos(0.5) - y_50[-1, 0])))
```

    Solution at x = 0.5 using h = 0.1 is y = 0.877582878483.
    Error at x = 0.5 using h = 0.1 is y = 3.16592701721e-07.
    Solution at x = 0.5 using h = 0.01 is y = 0.877582561905.
    Error at x = 0.5 using h = 0.01 is y = 1.45709000421e-11.

### Standard Example: 2

Consider the system

$$
\left\{
\begin{aligned}
  \dot{x} & = -y \\ \dot{y} & = x
\end{aligned} \right., \quad x(0) = 1, \, \, y(0) = 0.
$$

In polar coordinates this is $\dot{r} = 0$, $\dot{\phi} = 1$.

Use the Adams-Bashforth 5 step method with $h=0.1$. At
$t=500$ the result matches the correct answer to the eye.

Looking at the growth of the radius makes the errors visible.

Use Adams-Bashforth 5 step method with $h=0.01$. At
$t=500$ the result matches the correct answer to the eye.

Looking at the growth of the radius makes the errors visible even now.

```
def fn_circular_motion(x, y):
    """Simple function for circular motion example"""

    dfdt = np.zeros_like(y)
    dfdt[0] = -y[1]
    dfdt[1] =  y[0]

    return dfdt

# Example given above
y0 = np.array([1.0, 0.0])
t_0_1, y_0_1 = ab5_method(fn_circular_motion, 50.0, y0, 500)
t_0_01, y_0_01 = ab5_method(fn_circular_motion, 50.0, y0, 5000)

fig = plt.figure(figsize = (10,6))
ax1 = fig.add_subplot(121)
ax1.plot(y_0_1[:, 0], y_0_1[:, 1], 'b-', label = '$h = 0.1$')
ax1.set_xlabel('$x$')
ax1.set_ylabel('$y$')
ax2 = fig.add_subplot(122)
ax2.plot(y_0_01[:, 0], y_0_01[:, 1], 'b-', label = '$h = 0.01$')
ax2.set_xlabel('$x$')
ax2.set_ylabel('$y$')
fig.tight_layout()
plt.show()
```

```
fig = plt.figure(figsize = (10,6))
ax1 = fig.add_subplot(121)
ax1.plot(t_0_1, np.sqrt(y_0_1[:, 0]**2 + y_0_1[:, 1]**2)-1.0, 'b-', label = '$h = 0.1$')
ax1.set_xlabel('$t$')
ax1.set_ylabel('$r - 1$')
ax2 = fig.add_subplot(122)
ax2.plot(t_0_01, np.sqrt(y_0_01[:, 0]**2 + y_0_01[:, 1]**2)-1.0, 'b-', label = '$h = 0.01$')
ax2.set_xlabel('$t$')
ax2.set_ylabel('$r - 1$')
fig.tight_layout()
plt.show()
```

```
Npoints = np.array([50*2**i for i in range(2, 10)])
h = np.zeros_like(Npoints, float)
h = 50.0 / Npoints
err = np.zeros_like(h)

for i in range(len(Npoints)):
    N = Npoints[i]
    x, y = ab5_method(fn_circular_motion, 50.0, y0, N)
    err[i] = np.abs(y[-1, 0]**2 + y[-1, 1]**2 - 1.0)

# Find the best fit to the data: if err ~ A h^s then the best fit straight line of log(err) vs log(h) has slope s.
p = np.polyfit(np.log(h[1:]), np.log(err[1:]), 1)

fig = plt.figure(figsize = (10,6))
ax = fig.add_subplot(111)
ax.loglog(h, err, 'kx', label = 'Numerical data')
ax.loglog(h, np.exp(p[1])*h**(p[0]), 'b-', label = "Line slope {:.2f}".format(p[0]))
ax.set_xlabel('h')
ax.set_ylabel('$\|$ Error $\|$')
ax.set_title("Convergence of $5^{th}$ order Adams-Bashforth method for circular motion example")
ax.legend(loc = 2)
fig.tight_layout()
plt.show()
```

Summary
=======

-   The Runge-Kutta Fehlberg method, often known as RK45, is a fourth order method that estimates its own error using a fifth order computation.
-   When combined with adaptive step sizes this is a standard method for solving IVPs to moderate accuracy efficiently. The Matlab command is ode45.
-   The multistep methods estimate the quadrature solution of the IVP, not the derivative.
-   Multistep methods use a (given) number of prior values of the solution at each step.
-   Multistep methods require fewer function evaluations than multistage methods such as Runge-Kutta for the same order of accuracy.
-   It is difficult to use adaptive stepping with multistep methods.


```javascript
%%javascript
MathJax.Hub.Config({
    TeX: { equationNumbers: { autoNumber: "AMS" } }
});
```

    <IPython.core.display.Javascript object>

## Exercise 2.8 Conditional independence iff joint factorizes
In the text we said $X\perp Y |Z$ iff

$$
p(x, y|z) = p(x|z)p(y|z)
$$

for all $x$, $y$, $z$ such that $p(z) > 0$. Now prove the following alternative definition: $X \perp Y |Z$ iff there exist function $g$ and $h$ such that

$$
p(x, y|z) = g(x, z)h(y, z)
$$

for all $x$, $y$, $z$ such that $p(z) > 0$.

### Solution
The intuition behind the alternative definition is based on the chain rule:

$$
p(x, y|z) = p(x|z)p(y|x, z).
$$

Note that the first term is just a function of $X$ and $Z$ ($g(x, z)$) and the second term is a function of the three random variables $X$, $Y$ and $Z$. Only if $X\perp Y|Z$ then we can decouple the functions as we wanted.

Furthermore, if we know that $p(x, y |z)$ can be decoupled, then it makes sense that $p(y|x, z) = p(y|z) = h(y, z)$.

Prove the **If** part:

$$
p(x, y|z) = g(x, z)h(y, z) \implies p(x, y|z)=p(x|z)p(y|z)
$$

Let's sum all possible cases of $x$ and $y$ on the pdf. We are assuming discrete random variables.

\begin{equation}
\sum_x\sum_yp(x, y|z) = 1
\end{equation}

At the same time,

\begin{equation}
\sum_x\sum_y p(x, y |z) = \sum_x\sum_y g(x, z)h(y, z) = (\sum_x g(x, z))(\sum_y h(y, z))
\end{equation}

From this we arrive at

\begin{equation}
(\sum_x g(x, z))(\sum_y h(y, z)) = 1
\end{equation}

Let's calculate the marginal distributions of $p(x, y | z)$:

\begin{equation}
p(y|z) = \sum_x p(x, y| z) = \sum_x g(x, z) h(y, z) = h(y, z)\sum_x g(x, z)
\end{equation}

Isolating $\sum_xg(x, z)$, we get

\begin{equation}
\sum_x g(x, z) = \frac{p(y|z)}{h(y, z)}
\end{equation}

Now marginalize $x$

\begin{equation}
p(x|z) = \sum_y p(x, y |z) = \sum_y g(x, z)h(y, z) = g(x, z)\sum_y h(y, z)
\end{equation}

Isolating $\sum_y h(y, z)$, we get

\begin{equation}
\sum_y h(y, z) = \frac{p(x|z)}{g(x, z)}
\end{equation}

Finally, multiplying (5) and (7) and substituting (3) on the result we get:

\begin{equation}
\sum_x g(x, z)\sum_y h(y, z) = \frac{p(y|z)p(x|z)}{h(y, z)g(x, z)} = 1
\end{equation}

Thus $h(y, z) = p(y|z)$ and $g(x, z)=p(x|z)$ and we proved that $p(x, y|z) = g(x, z)h(y,z) \implies p(x,y|z) = p(x|z)p(y|z)$.

**Only if**

$p(x|z)$ is only a function of $x$ and $z$, so $p(x|z) = g(x, z)$. using the same argument for $y$ and $z$, we get $p(y|z) = h(y,z)$.

So
$$
p(x, y|z) = p(x|z)p(y|z) \implies p(x, y|z) = g(x, z)h(y, z)
$$

This result is important because initially we know that conditional independence is achieved if we could decouple the **conditional probabilities** of $X$ and $Y$. With this we know that the decomposition of $p(x, y|z)$ into **any** pair of function $g(x, z)$ and $h(y,z)$ implies conditional probability.


# Vertical Line Test

## 1.1 Create two graphs, one that passes the vertical line test and one that does not.

```
# This graph passes the vertical line test.

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-10,10,100)
y = 2*x+1
plt.plot(x, y, '-r', label='y=2x+1')

plt.xlabel('x')
plt.ylabel('y')
plt.legend(loc='upper left')
plt.grid()
plt.show()

```

```
# This graph does not pass the vertical line test.

t = np.arange(0,2*np.pi, 0.1)
x = 16*np.sin(t)**3
y = 13*np.cos(t)-5*np.cos(2*t)-2*np.cos(3*t)-np.cos(4*t)
plt.plot(x,y)
plt.show()

```

## 1.2 Why are graphs that don't pass the vertical line test not considered "functions?"

Functions are mappings from an input to a single input. Graphs that fail the vertical line test have two values for an input, and are therefore not functions.

# Functions as Relations

## 2.1 Which of the following relations are functions? Why?

\begin{align}
\text{Relation 1: } \{(1, 2), (3, 2), (1, 3)\}
\\
\text{Relation 2: } \{(1, 3), (2, 3), (6, 7)\}
\\
\text{Relation 3: } \{(9, 4), (2, 1), (9, 6)\}
\\
\text{Relation 4: } \{(6, 2), (8, 3), (6, 4)\}
\\
\text{Relation 5: } \{(2, 6), (2, 7), (2, 4)\}
\end{align}

Relation 1 and Relation 2 are functions. Relations 3-5 map a single input to multiple outputs, and therefore are not functions.

# Functions as a mapping between dimensions

## 3.1 for the following functions what is the dimensionality of the domain (input) and codomain (range/output)?

\begin{align}
m(𝑥_1,𝑥_2,𝑥_3)=(x_1+x_2, x_1+x_3, x_2+x_3)
\\
n(𝑥_1,𝑥_2,𝑥_3,𝑥_4)=(x_2^2 + x_3, x_2x_4)
\end{align}

*   The domain of m is 3-dimensional, and the codomain is 3-dimensional.
*   The domain of n is 4-dimensional, and the codomain is 2-dimensional.

## 3.2 Do you think it's possible to create a function that maps from a lower dimensional space to a higher dimensional space? If so, provide an example.

Yes, it is possible to create such a function. For example:
\begin{align}
f(𝑥_1,𝑥_2)=(x_1, x_2, 1)
\\
\end{align}

# Vector Transformations

## 4.1 Plug the corresponding unit vectors into each function. Use the output vectors to create a transformation matrix.

\begin{align}
p(\begin{bmatrix}x_1 \\ x_2 \end{bmatrix}) = \begin{bmatrix} x_1 + 3x_2 \\2 x_2 - x_1 \\  \end{bmatrix}
\\
\\
q(\begin{bmatrix}x_1 \\ x_2 \\ x_3\end{bmatrix}) = \begin{bmatrix} 4x_1 + x_2 + 2x_3 \\2 x_2 - x_1 + 3x_3 \\ 5x_1 - 2x_3 + x_2  \end{bmatrix}
\end{align}

\begin{align}
p(\begin{bmatrix}1 \\ 0 \end{bmatrix}) = \begin{bmatrix} 1 \\ -1 \\  \end{bmatrix}
\\
p(\begin{bmatrix}0 \\ 1 \end{bmatrix}) = \begin{bmatrix} 3 \\ 2 \\  \end{bmatrix}
\\
\end{align}

\begin{align}
\begin{bmatrix} 1  & 3 \\-1 & 2 \\  \end{bmatrix}
\end{align}

\begin{align}
\\
q(\begin{bmatrix}1 \\ 0 \\ 0\end{bmatrix}) = \begin{bmatrix} 4 \\-1 \\ 5  \end{bmatrix}
\\
q(\begin{bmatrix}0 \\ 1 \\ 0\end{bmatrix}) = \begin{bmatrix} 1 \\2 \\ 1  \end{bmatrix}
\\
q(\begin{bmatrix}0 \\ 0 \\ 1\end{bmatrix}) = \begin{bmatrix} 2 \\3 \\ -2  \end{bmatrix}
\end{align}

\begin{align}
\begin{bmatrix} 4 & 1 & 2 \\-1 & 2 & 3 \\ 5 & 1 & -2  \end{bmatrix}
\end{align}

## 4.2 Verify that your transformation matrices are correct by choosing an input matrix and calculating the result both via the traditional functions above and also via vector-matrix multiplication.

```
# Testing p with vector [3,4]. Via the traditional functions:

t1 = np.array([[3],[4]])
p = np.array([
    t1[0]+(3*t1[1]),
    (2*t1[1]-t1[0])
])
p
```

    array([[15],
           [ 5]])

```
# Via vector-matrix multiplication:

tmat_1 = np.array([
    [1,3],
    [-1,2]
])
np.dot(tmat_1, t1)
```

    array([[15],
           [ 5]])

```
# Testing q with vector [2,3,4]. Via the traditional functions:

t2 = np.array([[2],[3],[4]])
q = np.array([
    4*t2[0] + t2[1] + 2*t2[2],
    2*t2[1] - t2[0] + 3*t2[2],
    5*t2[0] - 2*t2[2] + t2[1]
])
q
```

    array([[19],
           [16],
           [ 5]])

```
# Via vector-matrix multiplication:

tmat_2 = np.array([
    [4,1,2],
    [-1,2,3],
    [5,1,-2]
])
np.dot(tmat_2, t2)
```

    array([[19],
           [16],
           [ 5]])

# Eigenvalues and Eigenvectors

## 5.1 In your own words, give an explanation for the intuition behind eigenvalues and eigenvectors.

The eigenvectors for a linear transformation are the vectors that don't get knocked off of their span during the transformation. They are scaled by their eigenvalues.

# The Curse of Dimensionality

## 6.1 What are some of the challenges of working with high dimensional spaces?

*   Each time a feature is added, it exponentially increases the computational workload
*   The volume of the space rapidly increases with each additional feature, meaning that available data will become sparse. This is a problem for various machine learning algorithms that are sensitive to distance.
* A high number of dimensions relative to observations risks overfitting.

## 6.2 What is the rule of thumb for how many observations you should have compared to parameters in your model?

*   The rule of thumb is that you should have 5 times more observations than parameters in your model.

# Principal Component Analysis

## 7.1 Load the UCI Machine Learning Repository's [Iris Dataset](https://gist.githubusercontent.com/curran/a08a1080b88344b0c8a7/raw/d546eaee765268bf2f487608c537c05e22e4b221/iris.csv) and use PCA to isolate the dataset's first and second principal components and plot them on a graph.

```

```

# Stretch Goal

## 1) Do NOT work on the stretch goal until you feel like you have a firm grasp of eigenvectors, eigenvalues, and PCA. Prioritize self-study over the stretch goal if you are not comfortable with those topics yet.

## 2) Explore further the intuition behind eigenvalues and eigenvectors by creating your very own eigenfaces:

You don't necessarily have to use this resource, but this will get you started:
[Eigenface Tutorial](https://sandipanweb.wordpress.com/2018/01/06/eigenfaces-and-a-simple-face-detector-with-pca-svd-in-python/)


# Des dates qui font des nombres premiers ?

Ce petit [notebook Jupyter](https://www.jupyter.org/), écrit en [Python](https://www.python.org/), a pour but de résoudre la question suivante :

> *"En 2017, combien de jours ont leur date qui est un nombre premier ?"*

Par exemple, en 2017, le 23 février donne `23022017` est premier, mais le 24 février donne `24022017` qui ne l'est pas.
On veut trouver toutes les dates en 2017 qui sont des nombres premiers.

----
## 1. Il faut bien poser le problème
- *Q:* Qu'entends-on par *"date qui est un nombre premier"* ?
  - Ça dépend de l'écriture de la date, que ce soit `12012017` pour le 12 janvier ou `01122017`.
  - Dans le doute, la solution présentée ici sera adaptable aux deux cas.
- *Q:* Pourquoi se restreindre à 2017 ?
  - Parce que j'écris ce document en 2017...
  - Parce que c'est un nombre impair, et donc un nombre de la forme `12012017` a une chance d'être premier !
  - Mais dans le doute, la solution présentée ici sera adaptable à n'importe quelle année.
- *Q:* Quelles années ont le plus de dates premières ?
- *Q:* Est-ce qu'on peut inverser la question et chercher quel jour donne le plus de dates premières ?

----
## 2. Une première solution, naïve
- On va d'abord écrire (ou importer) une fonction pour tester si un entier est premier,
- Puis on va écrire une fonction qui transforme une date en son nombre,
- Et enfin une boucle sur les 365 (ou 366) jours de l'année suffira à afficher les jours ayant une date première.

### 2.1. Tester la primalité, version tricheur
[`sympy`](http://www.sympy.org/) propose une fonction [`sympy.isprime`](http://docs.sympy.org/latest/modules/ntheory.html#sympy.ntheory.primetest.isprime).

```python
from sympy import isprime
```

Elle marche très bien, et est très rapide !

```python
[isprime(i) for i in [2, 3, 5, 7, 10, 11, 13, 17, 2017]]
```

    [True, True, True, True, False, True, True, True, True]

Pour des nombres de 8 chiffres (c'est tout petit), elle est vraiment rapide :

```python
from numpy.random import randint
%timeit sum([isprime(i) for i in randint(1e8, 1e9-1, 10**4)])
```

    10 loops, best of 3: 64.8 ms per loop

$\implies$ $65 ~\text{ms}$ pour 10000 nombres à tester, ça me semble assez rapide pour ce qu'on veut en faire !

----
### 2.2. Transformer une date en nombre
On va utiliser le module [`datetime`](https://docs.python.org/3/library/datetime.html) de la bibliothèque standard :

```python
from datetime import datetime
```

```python
today = datetime.today()
YEAR = today.year
print("On va travailler avec l'année", YEAR, "!")
```

    On va travailler avec l'année 2017 !

C'est ensuite facile de transformer une date en nombre, selon les deux formats.
On utilise [le formatage avec `.format()`](https://pyformat.info/#datetime) (en Python 3) :

```python
def date_vers_nombre(date):
    return int("{:%d%m%Y}".format(date))

def date_vers_nombre_2(date):
    return int("{:%m%d%Y}".format(date))
```

```python
date = datetime(YEAR, 1, 12)
print(date_vers_nombre(date))
print(date_vers_nombre_2(date))  # Le 0 initial est ignoré
```

    12012017
    1122017

### 2.3. Tester tous les jours de l'année

On peut partir du 1er janvier de cette année, et ajouter des jours un par un.
On utilise un itérateur (avec le mot clé `yield`), pour pouvoir facilement boucler sur tous les jours de l'année en cours :

```python
from datetime import timedelta

def tous_les_jours(year=YEAR):
    date = datetime(year, 1, 1)
    un_jour = timedelta(days=1)
    for i in range(0, 366):
        yield date
        date += un_jour
        if date.year > year:  # On est allé trop loin
            raise StopIteration
```

On peut vérifier que ça donne ce qu'on voulait :

```python
for date in tous_les_jours():
    print("Le jour {:%d/%m/%Y} donne l'entier {:>8} au format jour-mois-année et {:>8} au format mois-jour-année.".format(date, date_vers_nombre(date), date_vers_nombre_2(date)))
```

    Le jour 01/01/2017 donne l'entier  1012017 au format jour-mois-année et  1012017 au format mois-jour-année.
    Le jour 02/01/2017 donne l'entier  2012017 au format jour-mois-année et  1022017 au format mois-jour-année.
    Le jour 03/01/2017 donne l'entier  3012017 au format jour-mois-année et  1032017 au format mois-jour-année.
    Le jour 04/01/2017 donne l'entier  4012017 au format jour-mois-année et  1042017 au format mois-jour-année.
    Le jour 05/01/2017 donne l'entier  5012017 au format jour-mois-année et  1052017 au format mois-jour-année.
    Le jour 06/01/2017 donne l'entier  6012017 au format jour-mois-année et  1062017 au format mois-jour-année.
    Le jour 07/01/2017 donne l'entier  7012017 au format jour-mois-année et  1072017 au format mois-jour-année.
    Le jour 08/01/2017 donne l'entier  8012017 au format jour-mois-année et  1082017 au format mois-jour-année.
    Le jour 09/01/2017 donne l'entier  9012017 au format jour-mois-année et  1092017 au format mois-jour-année.
    Le jour 10/01/2017 donne l'entier 10012017 au format jour-mois-année et  1102017 au format mois-jour-année.
    Le jour 11/01/2017 donne l'entier 11012017 au format jour-mois-année et  1112017 au format mois-jour-année.
    Le jour 12/01/2017 donne l'entier 12012017 au format jour-mois-année et  1122017 au format mois-jour-année.
    Le jour 13/01/2017 donne l'entier 13012017 au format jour-mois-année et  1132017 au format mois-jour-année.
    Le jour 14/01/2017 donne l'entier 14012017 au format jour-mois-année et  1142017 au format mois-jour-année.
    Le jour 15/01/2017 donne l'entier 15012017 au format jour-mois-année et  1152017 au format mois-jour-année.
    Le jour 16/01/2017 donne l'entier 16012017 au format jour-mois-année et  1162017 au format mois-jour-année.
    Le jour 17/01/2017 donne l'entier 17012017 au format jour-mois-année et  1172017 au format mois-jour-année.
    Le jour 18/01/2017 donne l'entier 18012017 au format jour-mois-année et  1182017 au format mois-jour-année.
    Le jour 19/01/2017 donne l'entier 19012017 au format jour-mois-année et  1192017 au format mois-jour-année.
    Le jour 20/01/2017 donne l'entier 20012017 au format jour-mois-année et  1202017 au format mois-jour-année.
    Le jour 21/01/2017 donne l'entier 21012017 au format jour-mois-année et  1212017 au format mois-jour-année.
    Le jour 22/01/2017 donne l'entier 22012017 au format jour-mois-année et  1222017 au format mois-jour-année.
    Le jour 23/01/2017 donne l'entier 23012017 au format jour-mois-année et  1232017 au format mois-jour-année.
    Le jour 24/01/2017 donne l'entier 24012017 au format jour-mois-année et  1242017 au format mois-jour-année.
    Le jour 25/01/2017 donne l'entier 25012017 au format jour-mois-année et  1252017 au format mois-jour-année.
    Le jour 26/01/2017 donne l'entier 26012017 au format jour-mois-année et  1262017 au format mois-jour-année.
    Le jour 27/01/2017 donne l'entier 27012017 au format jour-mois-année et  1272017 au format mois-jour-année.
    Le jour 28/01/2017 donne l'entier 28012017 au format jour-mois-année et  1282017 au format mois-jour-année.
    Le jour 29/01/2017 donne l'entier 29012017 au format jour-mois-année et  1292017 au format mois-jour-année.
    Le jour 30/01/2017 donne l'entier 30012017 au format jour-mois-année et  1302017 au format mois-jour-année.
    Le jour 31/01/2017 donne l'entier 31012017 au format jour-mois-année et  1312017 au format mois-jour-année.
    Le jour 01/02/2017 donne l'entier  1022017 au format jour-mois-année et  2012017 au format mois-jour-année.
    Le jour 02/02/2017 donne l'entier  2022017 au format jour-mois-année et  2022017 au format mois-jour-année.
    Le jour 03/02/2017 donne l'entier  3022017 au format jour-mois-année et  2032017 au format mois-jour-année.
    Le jour 04/02/2017 donne l'entier  4022017 au format jour-mois-année et  2042017 au format mois-jour-année.
    Le jour 05/02/2017 donne l'entier  5022017 au format jour-mois-année et  2052017 au format mois-jour-année.
    Le jour 06/02/2017 donne l'entier  6022017 au format jour-mois-année et  2062017 au format mois-jour-année.
    Le jour 07/02/2017 donne l'entier  7022017 au format jour-mois-année et  2072017 au format mois-jour-année.
    Le jour 08/02/2017 donne l'entier  8022017 au format jour-mois-année et  2082017 au format mois-jour-année.
    Le jour 09/02/2017 donne l'entier  9022017 au format jour-mois-année et  2092017 au format mois-jour-année.
    Le jour 10/02/2017 donne l'entier 10022017 au format jour-mois-année et  2102017 au format mois-jour-année.
    Le jour 11/02/2017 donne l'entier 11022017 au format jour-mois-année et  2112017 au format mois-jour-année.
    Le jour 12/02/2017 donne l'entier 12022017 au format jour-mois-année et  2122017 au format mois-jour-année.
    Le jour 13/02/2017 donne l'entier 13022017 au format jour-mois-année et  2132017 au format mois-jour-année.
    Le jour 14/02/2017 donne l'entier 14022017 au format jour-mois-année et  2142017 au format mois-jour-année.
    Le jour 15/02/2017 donne l'entier 15022017 au format jour-mois-année et  2152017 au format mois-jour-année.
    Le jour 16/02/2017 donne l'entier 16022017 au format jour-mois-année et  2162017 au format mois-jour-année.
    Le jour 17/02/2017 donne l'entier 17022017 au format jour-mois-année et  2172017 au format mois-jour-année.
    Le jour 18/02/2017 donne l'entier 18022017 au format jour-mois-année et  2182017 au format mois-jour-année.
    Le jour 19/02/2017 donne l'entier 19022017 au format jour-mois-année et  2192017 au format mois-jour-année.
    Le jour 20/02/2017 donne l'entier 20022017 au format jour-mois-année et  2202017 au format mois-jour-année.
    Le jour 21/02/2017 donne l'entier 21022017 au format jour-mois-année et  2212017 au format mois-jour-année.
    Le jour 22/02/2017 donne l'entier 22022017 au format jour-mois-année et  2222017 au format mois-jour-année.
    Le jour 23/02/2017 donne l'entier 23022017 au format jour-mois-année et  2232017 au format mois-jour-année.
    Le jour 24/02/2017 donne l'entier 24022017 au format jour-mois-année et  2242017 au format mois-jour-année.
    Le jour 25/02/2017 donne l'entier 25022017 au format jour-mois-année et  2252017 au format mois-jour-année.
    Le jour 26/02/2017 donne l'entier 26022017 au format jour-mois-année et  2262017 au format mois-jour-année.
    Le jour 27/02/2017 donne l'entier 27022017 au format jour-mois-année et  2272017 au format mois-jour-année.
    Le jour 28/02/2017 donne l'entier 28022017 au format jour-mois-année et  2282017 au format mois-jour-année.
    Le jour 01/03/2017 donne l'entier  1032017 au format jour-mois-année et  3012017 au format mois-jour-année.
    Le jour 02/03/2017 donne l'entier  2032017 au format jour-mois-année et  3022017 au format mois-jour-année.
    Le jour 03/03/2017 donne l'entier  3032017 au format jour-mois-année et  3032017 au format mois-jour-année.
    Le jour 04/03/2017 donne l'entier  4032017 au format jour-mois-année et  3042017 au format mois-jour-année.
    Le jour 05/03/2017 donne l'entier  5032017 au format jour-mois-année et  3052017 au format mois-jour-année.
    Le jour 06/03/2017 donne l'entier  6032017 au format jour-mois-année et  3062017 au format mois-jour-année.
    Le jour 07/03/2017 donne l'entier  7032017 au format jour-mois-année et  3072017 au format mois-jour-année.
    Le jour 08/03/2017 donne l'entier  8032017 au format jour-mois-année et  3082017 au format mois-jour-année.
    Le jour 09/03/2017 donne l'entier  9032017 au format jour-mois-année et  3092017 au format mois-jour-année.
    Le jour 10/03/2017 donne l'entier 10032017 au format jour-mois-année et  3102017 au format mois-jour-année.
    Le jour 11/03/2017 donne l'entier 11032017 au format jour-mois-année et  3112017 au format mois-jour-année.
    Le jour 12/03/2017 donne l'entier 12032017 au format jour-mois-année et  3122017 au format mois-jour-année.
    Le jour 13/03/2017 donne l'entier 13032017 au format jour-mois-année et  3132017 au format mois-jour-année.
    Le jour 14/03/2017 donne l'entier 14032017 au format jour-mois-année et  3142017 au format mois-jour-année.
    Le jour 15/03/2017 donne l'entier 15032017 au format jour-mois-année et  3152017 au format mois-jour-année.
    Le jour 16/03/2017 donne l'entier 16032017 au format jour-mois-année et  3162017 au format mois-jour-année.
    Le jour 17/03/2017 donne l'entier 17032017 au format jour-mois-année et  3172017 au format mois-jour-année.
    Le jour 18/03/2017 donne l'entier 18032017 au format jour-mois-année et  3182017 au format mois-jour-année.
    Le jour 19/03/2017 donne l'entier 19032017 au format jour-mois-année et  3192017 au format mois-jour-année.
    Le jour 20/03/2017 donne l'entier 20032017 au format jour-mois-année et  3202017 au format mois-jour-année.
    Le jour 21/03/2017 donne l'entier 21032017 au format jour-mois-année et  3212017 au format mois-jour-année.
    Le jour 22/03/2017 donne l'entier 22032017 au format jour-mois-année et  3222017 au format mois-jour-année.
    Le jour 23/03/2017 donne l'entier 23032017 au format jour-mois-année et  3232017 au format mois-jour-année.
    Le jour 24/03/2017 donne l'entier 24032017 au format jour-mois-année et  3242017 au format mois-jour-année.
    Le jour 25/03/2017 donne l'entier 25032017 au format jour-mois-année et  3252017 au format mois-jour-année.
    Le jour 26/03/2017 donne l'entier 26032017 au format jour-mois-année et  3262017 au format mois-jour-année.
    Le jour 27/03/2017 donne l'entier 27032017 au format jour-mois-année et  3272017 au format mois-jour-année.
    Le jour 28/03/2017 donne l'entier 28032017 au format jour-mois-année et  3282017 au format mois-jour-année.
    Le jour 29/03/2017 donne l'entier 29032017 au format jour-mois-année et  3292017 au format mois-jour-année.
    Le jour 30/03/2017 donne l'entier 30032017 au format jour-mois-année et  3302017 au format mois-jour-année.
    Le jour 31/03/2017 donne l'entier 31032017 au format jour-mois-année et  3312017 au format mois-jour-année.
    Le jour 01/04/2017 donne l'entier  1042017 au format jour-mois-année et  4012017 au format mois-jour-année.
    Le jour 02/04/2017 donne l'entier  2042017 au format jour-mois-année et  4022017 au format mois-jour-année.
    Le jour 03/04/2017 donne l'entier  3042017 au format jour-mois-a

<b>limit_output extension: Maximum message size of 10000 exceeded with 39420 characters</b>

Maintenant, il suffit de boucler, de tester si l'entier est premier, et de n'afficher que ceux qui le sont :

```python
def date_premieres(conversion=date_vers_nombre, year=YEAR):
    for date in tous_les_jours(year):
        if isprime(conversion(date)):
            yield date
```

On peut aussi facilement trouver la prochaine date qui sera première :

```python
def prochaine_date_premiere(date=datetime.today(), conversion=date_vers_nombre):
    year = date.year
    un_jour = timedelta(days=1)
    for i in range(0, 366):
        if isprime(conversion(date)):
            return date
        date += un_jour
        if date.year > year:  # On est allé trop loin
            return None
    return None
```

```python
date = datetime.today()
prochain = prochaine_date_premiere(date)
print("Pour le jour d'aujourd'hui ({:%x}), le prochain jour ayant une date première dans l'année {} est : {:%x} !".format(date, date.year, prochain))
```

    Pour le jour d'aujourd'hui (02/24/17), le prochain jour ayant une date première dans l'année 2017 est : 03/06/17 !

#### Pour les dates écrites "jour mois année" :

```python
for date in date_premieres(date_vers_nombre):
    print("Le jour {:%d/%m/%Y} donne l'entier {:>8} qui est premier !".format(date, date_vers_nombre(date)))
```

    Le jour 03/01/2017 donne l'entier  3012017 qui est premier !
    Le jour 11/01/2017 donne l'entier 11012017 qui est premier !
    Le jour 12/01/2017 donne l'entier 12012017 qui est premier !
    Le jour 18/01/2017 donne l'entier 18012017 qui est premier !
    Le jour 27/01/2017 donne l'entier 27012017 qui est premier !
    Le jour 29/01/2017 donne l'entier 29012017 qui est premier !
    Le jour 01/02/2017 donne l'entier  1022017 qui est premier !
    Le jour 02/02/2017 donne l'entier  2022017 qui est premier !
    Le jour 04/02/2017 donne l'entier  4022017 qui est premier !
    Le jour 08/02/2017 donne l'entier  8022017 qui est premier !
    Le jour 10/02/2017 donne l'entier 10022017 qui est premier !
    Le jour 17/02/2017 donne l'entier 17022017 qui est premier !
    Le jour 20/02/2017 donne l'entier 20022017 qui est premier !
    Le jour 23/02/2017 donne l'entier 23022017 qui est premier !
    Le jour 06/03/2017 donne l'entier  6032017 qui est premier !
    Le jour 09/03/2017 donne l'entier  9032017 qui est premier !
    Le jour 10/03/2017 donne l'entier 10032017 qui est premier !
    Le jour 13/03/2017 donne l'entier 13032017 qui est premier !
    Le jour 18/03/2017 donne l'entier 18032017 qui est premier !
    Le jour 27/03/2017 donne l'entier 27032017 qui est premier !
    Le jour 31/03/2017 donne l'entier 31032017 qui est premier !
    Le jour 02/04/2017 donne l'entier  2042017 qui est premier !
    Le jour 08/04/2017 donne l'entier  8042017 qui est premier !
    Le jour 17/04/2017 donne l'entier 17042017 qui est premier !
    Le jour 21/04/2017 donne l'entier 21042017 qui est premier !
    Le jour 23/04/2017 donne l'entier 23042017 qui est premier !
    Le jour 24/04/2017 donne l'entier 24042017 qui est premier !
    Le jour 26/04/2017 donne l'entier 26042017 qui est premier !
    Le jour 27/04/2017 donne l'entier 27042017 qui est premier !
    Le jour 08/05/2017 donne l'entier  8052017 qui est premier !
    Le jour 10/05/2017 donne l'entier 10052017 qui est premier !
    Le jour 11/05/2017 donne l'entier 11052017 qui est premier !
    Le jour 31/05/2017 donne l'entier 31052017 qui est premier !
    Le jour 09/06/2017 donne l'entier  9062017 qui est premier !
    Le jour 13/06/2017 donne l'entier 13062017 qui est premier !
    Le jour 21/06/2017 donne l'entier 21062017 qui est premier !
    Le jour 22/06/2017 donne l'entier 22062017 qui est premier !
    Le jour 25/06/2017 donne l'entier 25062017 qui est premier !
    Le jour 28/06/2017 donne l'entier 28062017 qui est premier !
    Le jour 14/07/2017 donne l'entier 14072017 qui est premier !
    Le jour 15/07/2017 donne l'entier 15072017 qui est premier !
    Le jour 17/07/2017 donne l'entier 17072017 qui est premier !
    Le jour 21/07/2017 donne l'entier 21072017 qui est premier !
    Le jour 01/08/2017 donne l'entier  1082017 qui est premier !
    Le jour 07/08/2017 donne l'entier  7082017 qui est premier !
    Le jour 08/08/2017 donne l'entier  8082017 qui est premier !
    Le jour 17/08/2017 donne l'entier 17082017 qui est premier !
    Le jour 19/08/2017 donne l'entier 19082017 qui est premier !
    Le jour 22/08/2017 donne l'entier 22082017 qui est premier !
    Le jour 28/08/2017 donne l'entier 28082017 qui est premier !
    Le jour 31/08/2017 donne l'entier 31082017 qui est premier !
    Le jour 03/09/2017 donne l'entier  3092017 qui est premier !
    Le jour 16/09/2017 donne l'entier 16092017 qui est premier !
    Le jour 21/09/2017 donne l'entier 21092017 qui est premier !
    Le jour 27/09/2017 donne l'entier 27092017 qui est premier !
    Le jour 30/09/2017 donne l'entier 30092017 qui est premier !
    Le jour 03/10/2017 donne l'entier  3102017 qui est premier !
    Le jour 12/10/2017 donne l'entier 12102017 qui est premier !
    Le jour 24/10/2017 donne l'entier 24102017 qui est premier !
    Le jour 26/10/2017 donne l'entier 26102017 qui est premier !
    Le jour 27/10/2017 donne l'entier 27102017 qui est premier !
    Le jour 01/11/2017 donne l'entier  1112017 qui est premier !
    Le jour 02/11/2017 donne l'entier  2112017 qui est premier !
    Le jour 05/11/2017 donne l'entier  5112017 qui est premier !
    Le jour 14/11/2017 donne l'entier 14112017 qui est premier !
    Le jour 16/11/2017 donne l'entier 16112017 qui est premier !
    Le jour 22/11/2017 donne l'entier 22112017 qui est premier !
    Le jour 03/12/2017 donne l'entier  3122017 qui est premier !
    Le jour 22/12/2017 donne l'entier 22122017 qui est premier !
    Le jour 24/12/2017 donne l'entier 24122017 qui est premier !
    Le jour 30/12/2017 donne l'entier 30122017 qui est premier !

#### Pour les dates écrites "mois jour année" :

```python
for date in date_premieres(date_vers_nombre_2):
    print("Le jour {:%d/%m/%Y} donne l'entier {:>8} qui est premier !".format(date, date_vers_nombre_2(date)))
```

    Le jour 02/01/2017 donne l'entier  1022017 qui est premier !
    Le jour 08/01/2017 donne l'entier  1082017 qui est premier !
    Le jour 11/01/2017 donne l'entier  1112017 qui est premier !
    Le jour 14/01/2017 donne l'entier  1142017 qui est premier !
    Le jour 20/01/2017 donne l'entier  1202017 qui est premier !
    Le jour 21/01/2017 donne l'entier  1212017 qui est premier !
    Le jour 26/01/2017 donne l'entier  1262017 qui est premier !
    Le jour 30/01/2017 donne l'entier  1302017 qui est premier !
    Le jour 02/02/2017 donne l'entier  2022017 qui est premier !
    Le jour 04/02/2017 donne l'entier  2042017 qui est premier !
    Le jour 11/02/2017 donne l'entier  2112017 qui est premier !
    Le jour 19/02/2017 donne l'entier  2192017 qui est premier !
    Le jour 23/02/2017 donne l'entier  2232017 qui est premier !
    Le jour 25/02/2017 donne l'entier  2252017 qui est premier !
    Le jour 26/02/2017 donne l'entier  2262017 qui est premier !
    Le jour 28/02/2017 donne l'entier  2282017 qui est premier !
    Le jour 01/03/2017 donne l'entier  3012017 qui est premier !
    Le jour 09/03/2017 donne l'entier  3092017 qui est premier !
    Le jour 10/03/2017 donne l'entier  3102017 qui est premier !
    Le jour 12/03/2017 donne l'entier  3122017 qui est premier !
    Le jour 19/03/2017 donne l'entier  3192017 qui est premier !
    Le jour 24/03/2017 donne l'entier  3242017 qui est premier !
    Le jour 25/03/2017 donne l'entier  3252017 qui est premier !
    Le jour 02/04/2017 donne l'entier  4022017 qui est premier !
    Le jour 20/04/2017 donne l'entier  4202017 qui est premier !
    Le jour 29/04/2017 donne l'entier  4292017 qui est premier !
    Le jour 30/04/2017 donne l'entier  4302017 qui est premier !
    Le jour 11/05/2017 donne l'entier  5112017 qui est premier !
    Le jour 14/05/2017 donne l'entier  5142017 qui est premier !
    Le jour 17/05/2017 donne l'entier  5172017 qui est premier !
    Le jour 25/05/2017 donne l'entier  5252017 qui est premier !
    Le jour 26/05/2017 donne l'entier  5262017 qui est premier !
    Le jour 29/05/2017 donne l'entier  5292017 qui est premier !
    Le jour 03/06/2017 donne l'entier  6032017 qui est premier !
    Le jour 24/06/2017 donne l'entier  6242017 qui est premier !
    Le jour 25/06/2017 donne l'entier  6252017 qui est premier !
    Le jour 27/06/2017 donne l'entier  6272017 qui est premier !
    Le jour 08/07/2017 donne l'entier  7082017 qui est premier !
    Le jour 20/07/2017 donne l'entier  7202017 qui est premier !
    Le jour 02/08/2017 donne l'entier  8022017 qui est premier !
    Le jour 04/08/2017 donne l'entier  8042017 qui est premier !
    Le jour 05/08/2017 donne l'entier  8052017 qui est premier !
    Le jour 08/08/2017 donne l'entier  8082017 qui est premier !
    Le jour 13/08/2017 donne l'entier  8132017 qui est premier !
    Le jour 16/08/2017 donne l'entier  8162017 qui est premier !
    Le jour 23/08/2017 donne l'entier  8232017 qui est premier !
    Le jour 25/08/2017 donne l'entier  8252017 qui est premier !
    Le jour 29/08/2017 donne l'entier  8292017 qui est premier !
    Le jour 03/09/2017 donne l'entier  9032017 qui est premier !
    Le jour 06/09/2017 donne l'entier  9062017 qui est premier !
    Le jour 19/09/2017 donne l'entier  9192017 qui est premier !
    Le jour 24/09/2017 donne l'entier  9242017 qui est premier !
    Le jour 27/09/2017 donne l'entier  9272017 qui est premier !
    Le jour 02/10/2017 donne l'entier 10022017 qui est premier !
    Le jour 03/10/2017 donne l'entier 10032017 qui est premier !
    Le jour 05/10/2017 donne l'entier 10052017 qui est premier !
    Le jour 14/10/2017 donne l'entier 10142017 qui est premier !
    Le jour 15/10/2017 donne l'entier 10152017 qui est premier !
    Le jour 17/10/2017 donne l'entier 10172017 qui est premier !
    Le jour 01/11/2017 donne l'entier 11012017 qui est premier !
    Le jour 05/11/2017 donne l'entier 11052017 qui est premier !
    Le jour 20/11/2017 donne l'entier 11202017 qui est premier !
    Le jour 23/11/2017 donne l'entier 11232017 qui est premier !
    Le jour 28/11/2017 donne l'entier 11282017 qui est premier !
    Le jour 01/12/2017 donne l'entier 12012017 qui est premier !
    Le jour 10/12/2017 donne l'entier 12102017 qui est premier !
    Le jour 19/12/2017 donne l'entier 12192017 qui est premier !
    Le jour 24/12/2017 donne l'entier 12242017 qui est premier !
    Le jour 31/12/2017 donne l'entier 12312017 qui est premier !

#### Comparaison
Il y a 71 jours, en 2017, qui ont une date première si on les écrit "jour mois année", et 69 si on les écrits "mois jour année".

```python
len(list(date_premieres(date_vers_nombre)))
```

    71

```python
len(list(date_premieres(date_vers_nombre_2)))
```

    69

----
## 3. Résolvons le problème pour toutes les années entre 0 AC et 2500 AC

On aimerait afficher une courbe montrant l'évolution du nombre de dates premières au cours des années, selon les deux formats.

```python
def nombres_dates_premieres(year=YEAR):
    if year % 2 == 0:
        return [0, 0]
    else:
        return [len(list(date_premieres(date_vers_nombre, year=year))), len(list(date_premieres(date_vers_nombre_2, year=year)))]
```

```python
nombres_dates_premieres()
```

    [71, 69]

On vérifie que pour les années paires il n'y a pas de dates premières :

```python
len(list(date_premieres(date_vers_nombre, year=2016)))
```

    0

On peut donc récupérer toutes ces nombres, jusqu'à l'année 3000.
(On pourrait gagner du temps en ne considérant que les années impaires, et l'année 1)

```python
import numpy as np

def intervale_nombres_dates_premieres(year1=1, year2=3000):
    nombres = np.zeros((year2 - year1 + 1, 2))
    for i, year in enumerate(range(year1, year2 + 1)):
        nombres[i, :] = nombres_dates_premieres(year)
    return nombres
```

On essaie sur un intervale de 11 ans :

```python
%%time
nombres = intervale_nombres_dates_premieres(year1=2010, year2=2020)
print(nombres)
```

    [[  0.   0.]
     [ 46.  50.]
     [  0.   0.]
     [ 52.  51.]
     [  0.   0.]
     [  0.   0.]
     [  0.   0.]
     [ 71.  69.]
     [  0.   0.]
     [ 50.  58.]
     [  0.   0.]]
    CPU times: user 64 ms, sys: 0 ns, total: 64 ms
    Wall time: 65 ms

```python
%%time
nombres = intervale_nombres_dates_premieres(year1=1, year2=3000)
print(nombres)
```

    [[ 108.  108.]
     [   0.    0.]
     [  98.  104.]
     ...,
     [   0.    0.]
     [  64.   55.]
     [   0.    0.]]
    CPU times: user 15.6 s, sys: 20 ms, total: 15.6 s
    Wall time: 15.6 s

```python
np.shape(nombres)
```

    (3000, 2)

Quelques statistiques :

```python
print("- Au format jour-mois-année, il y a en moyenne {:.3g} jours premiers par an.".format(np.mean(nombres[:, 0])))
print("- Et en moyenne {:.3g} jours premiers par an, en enlevant les années paires.".format(np.mean(nombres[:, 0][nombres[:, 0] > 0])))

print("- Au format mois-jour-année, il y a en moyenne {:.3g} jours premiers par an.".format(np.mean(nombres[:, 1])))
print("- Et en moyenne {:.3g} jours premiers par an, en enlevant les années paires.".format(np.mean(nombres[:, 1][nombres[:, 1] > 0])))
```

    - Au format jour-mois-année, il y a en moyenne 23.9 jours premiers par an.
    - Et en moyenne 59.7 jours premiers par an, en enlevant les années paires.
    - Au format mois-jour-année, il y a en moyenne 25.2 jours premiers par an.
    - Et en moyenne 63 jours premiers par an, en enlevant les années paires.

On va afficher tout ça :

```python
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(context="notebook", style="darkgrid", palette="hls", font="sans-serif", font_scale=1.4)
```

```python
def affiche_nombres_dates_premieres(nombres):
    plt.figure()
    plt.plot(nombres[:, 0], 'b.', label="Format jour-mois-année")
    plt.plot(nombres[:, 1], 'g*', label="Format mois-jour-année")
    plt.title("Nombres de dates premières par an")
    plt.legend(loc='best', numpoints=1, fancybox=True, shadow=True, framealpha=0.8)
    plt.xlabel("Année")
    plt.ylabel("Nombre de dates premières")
```

```python
affiche_nombres_dates_premieres(nombres)
```

Avec un histogramme :

```python
def hist_nombres_dates_premieres(nombres, ind=0):
    nbs = nombres[:, ind]
    nbs = nbs[nbs > 0]
    plt.figure()
    plt.hist(nbs, bins=100, label="Format %s-année" % ('jour-mois' if ind==0 else 'mois-jour'), color='bg'[ind])
    plt.title("Répartition du nombres de dates premières par an")
    plt.legend(loc='best', numpoints=1, fancybox=True, shadow=True, framealpha=0.8)
    plt.xlabel("Nombres de dates premières")
    plt.ylabel("Nombres")
```

```python
hist_nombres_dates_premieres(nombres, 0)
hist_nombres_dates_premieres(nombres, 1)
```

## 4. Quels jours donnent le plus de nombres premiers ?

On peut se poser une autre question : quelle date donne le plus de nombres premiers ?

On a vu que le 23 février donne `23022017` qui est premier, et `24022017` ne l'est pas.
Mais en sur 3000 années successives, est-ce que le 23 février donne plus souvent un nombre premier que le 24 février ?

```python
def meme_jour_toutes_les_annees(days=0, year1=1, year2=3000):
    date = datetime(year1, 1, 1)
    date += timedelta(days=days)
    for i, year in enumerate(range(year1, year2 + 1)):
        yield date.replace(year=year)
```

```python
list(meme_jour_toutes_les_annees(days=31+22, year1=2017, year2=2019))
```

    [datetime.datetime(2017, 2, 23, 0, 0),
     datetime.datetime(2018, 2, 23, 0, 0),
     datetime.datetime(2019, 2, 23, 0, 0)]

```python
def nombre_annees_qui_donnent_date_premiere(days=0, year1=1, year2=3000, conversion=date_vers_nombre):
    for date in meme_jour_toutes_les_annees(days=days, year1=year1, year2=year2):
        if isprime(conversion(date)):
            yield date
```

Par exemple, pour mon anniversaire, entre ma naissance et maintenant, une seule année a donné une date `1201YEAR` qui soit première :

```python
list(nombre_annees_qui_donnent_date_premiere(days=11, year1=1993, year2=2017))
```

    [datetime.datetime(2017, 1, 12, 0, 0)]

Le 23 février a plus de chance :

```python
list(nombre_annees_qui_donnent_date_premiere(days=31+22, year1=1993, year2=2017))
```

    [datetime.datetime(1993, 2, 23, 0, 0),
     datetime.datetime(2001, 2, 23, 0, 0),
     datetime.datetime(2017, 2, 23, 0, 0)]

On peut récupérer ces données pour tous les jours de l'année :

```python
def histogramme_par_jours(year1=1, year2=3000, conversion=date_vers_nombre):
    jours = np.zeros(366)
    for days in range(366):
        jours[days] += len(list(nombre_annees_qui_donnent_date_premiere(days=days, year1=year1, year2=year2, conversion=conversion)))
    return jours
```

```python
%%time
jours = histogramme_par_jours(year1=1993, year2=2017)
jours
```

    CPU times: user 124 ms, sys: 0 ns, total: 124 ms
    Wall time: 126 ms

```python
%%time
jours_format1 = histogramme_par_jours(year1=1, year2=3000, conversion=date_vers_nombre)
jours_format1 /= 3000
```

    CPU times: user 11.9 s, sys: 0 ns, total: 11.9 s
    Wall time: 11.9 s

```python
%%time
jours_format2 = histogramme_par_jours(year1=1, year2=3000, conversion=date_vers_nombre_2)
jours_format2 /= 3000
```

    CPU times: user 11.8 s, sys: 0 ns, total: 11.8 s
    Wall time: 11.8 s

### 4.1. Visualisations, jour par jour

Exploitons ces données :

```python
def hist_nombres_dates_premieres(jours, ind=0):
    plt.figure()
    plt.plot(jours, label="Format %s-année" % ('jour-mois' if ind==0 else 'mois-jour'), color='bg'[ind])
    plt.title("Répartition du nombres de dates premières selon la date\nEntre l'an 1 et l'an 3000")
    plt.legend(loc='best', numpoints=1, fancybox=True, shadow=True, framealpha=0.8)
    plt.xlabel("Jour dans l'année")
    plt.ylabel("Fréquence")
```

```python
hist_nombres_dates_premieres(jours_format1, ind=0)
hist_nombres_dates_premieres(jours_format2, ind=1)
```

```python
def txt_date_de_jour(days):
    date = datetime(year=1, day=1, month=1) + timedelta(days=int(days))
    return "{:%d/%m}".format(date)
```

### 4.2. Quelle jour donne le plus de dates premières ?
Sans trop de surprise, c'est le 1er janvier qui gagne, pour les deux cas :

```python
txt_date_de_jour(np.argmax(jours_format1))
txt_date_de_jour(np.argmax(jours_format2))
```

    '01/01'

    '01/01'

Mais en enlevant le 1er janvier et le dernier jour de l'année, on trouve une différence :

```python
np.argmax(jours_format1[1:-1])
txt_date_de_jour(_)

np.argmax(jours_format2[1:-1])
txt_date_de_jour(_)
```

    242

    '31/08'

    7

    '08/01'

----
## 5. Conclusions
- Pour les années impaires, il y a en moyenne une soixantaine de jours qui ont une date première !s,
- Le nombre est quasiment identique selon les deux formats, `jour-mois` ou `mois-jour`.

Ce n'était pas très dur à calculer, mais intéressant.

- Et en moyenne, un certain jour a entre 6% et 8% de donner une date première, entre l'an 1 et l'an 3000.
- Avec les deux formats, le 1er janvier est le jour qui donne le plus de dates premières, et puis le 31 décembre, puis le 31 août pour le format `jour-mois` et le 8 janvier pour le format `mois-jour`.

> C'est tout pour aujourd'hui les amis, [allez voir d'autres notebooks si vous êtes curieux !](https://github.com/Naereen/notebooks/).

> [See this repository for other Python notebook doing numerical simulations](https://github.com/Naereen/notebooks/tree/master/simus/).


# FITC

```python
from sympy import *
import utils as utils
from SuperMatExpr import Variable, SuperDiagMat
from MVG import MVG
import kernels.kernel as kern
from IPython.display import display, Math, Latex
```

```python
m, n, l = symbols('m n l')
s_y = symbols('\u03c3_y')
K = kern.Kernel()
```

## 1. Induding prior

```python
u = Variable('u',m,1)
p_u = MVG([u],mean=ZeroMatrix(m,1),cov=K(u,u))

print("p_u:")
display(Latex(utils.matLatex(p_u)))
```

    p_u:

\begin{align*}
p\left(\mathbf{u}\right)&= \mathcal{N}\left(\mathbf{m}_{\mathbf{u}},\mathbf{\Sigma}_{\mathbf{u}}\right)\\
\mathbf{m}_{\mathbf{u}} &= \mathbf{0}\\
\mathbf{\Sigma}_{\mathbf{u}} &= \mathbf{K}_{\mathbf{u},\mathbf{u}}\\
\end{align*}

## 2. Training and Test Conditionals

```python
f, fs, y = utils.variables("f fs y",[n, l, n])

q_fgu = MVG([f], mean=K(f,u)*K(u,u).I*u,
                 cov=SuperDiagMat(K(f,f)-K(f,u)*K(u,u).I*K(u,f)),
                 cond_vars=[u],
                 prefix='q_{FITC}')

print("q_fgu:")
display(Latex(utils.matLatex(q_fgu)))
```

    q_fgu:

\begin{align*}
q_{FITC}\left(\mathbf{f}|\mathbf{u}\right)&= \mathcal{N}\left(\mathbf{m}_{\mathbf{f}|\mathbf{u}},\mathbf{\Sigma}_{\mathbf{f}|\mathbf{u}}\right)\\
\mathbf{m}_{\mathbf{f}|\mathbf{u}} &= \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{u}\\
\mathbf{\Sigma}_{\mathbf{f}|\mathbf{u}} &= \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}]\\
\end{align*}

```python
q_fsgu = MVG([fs], mean=K(fs,u)*K(u,u).I*u,
                   cov=K(fs,fs)-K(fs,u)*K(u,u).I*K(u,fs),
                   cond_vars=[u],
                   prefix='q_{FITC}')

print("q_fgu:")
display(Latex(utils.matLatex(q_fsgu)))
```

    q_fgu:

\begin{align*}
q_{FITC}\left(\mathbf{fs}|\mathbf{u}\right)&= \mathcal{N}\left(\mathbf{m}_{\mathbf{fs}|\mathbf{u}},\mathbf{\Sigma}_{\mathbf{fs}|\mathbf{u}}\right)\\
\mathbf{m}_{\mathbf{fs}|\mathbf{u}} &= \mathbf{K}_{\mathbf{fs},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{u}\\
\mathbf{\Sigma}_{\mathbf{fs}|\mathbf{u}} &= \mathbf{K}_{\mathbf{fs},\mathbf{fs}} - \mathbf{K}_{\mathbf{fs},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{fs}}\\
\end{align*}

```python
# q(f,fs|u)
q_f_fs_g_u = q_fgu*q_fsgu

print("q_f_fs_g_u:")
display(Latex(utils.matLatex(q_f_fs_g_u)))
```

    q_f_fs_g_u:

\begin{align*}
q_{FITC}\left(\mathbf{f},\mathbf{fs}|\mathbf{u}\right)&= \mathcal{N}\left(\mathbf{m}_{\mathbf{f},\mathbf{fs}|\mathbf{u}},\mathbf{\Sigma}_{\mathbf{f},\mathbf{fs}|\mathbf{u}}\right)\\
\mathbf{m}_{\mathbf{f},\mathbf{fs}|\mathbf{u}} &= \left[\begin{smallmatrix}\mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{u}\\\mathbf{K}_{\mathbf{fs},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{u}\end{smallmatrix}\right]\\
\mathbf{\Sigma}_{\mathbf{f},\mathbf{fs}|\mathbf{u}} &= \left[\begin{smallmatrix}\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}]&\mathbf{0}\\\mathbf{0}&\mathbf{K}_{\mathbf{fs},\mathbf{fs}} - \mathbf{K}_{\mathbf{fs},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{fs}}\end{smallmatrix}\right]\\
\end{align*}

```python
# q(f,fs,u)
q_f_fs_u = q_f_fs_g_u*p_u

print("q_f_fs_u:")
display(Latex(utils.matLatex(q_f_fs_u)))
```

    q_f_fs_u:

\begin{align*}
q_{FITC}\left(\mathbf{f},\mathbf{fs},\mathbf{u}\right)&= \mathcal{N}\left(\mathbf{m}_{\mathbf{f},\mathbf{fs},\mathbf{u}},\mathbf{\Sigma}_{\mathbf{f},\mathbf{fs},\mathbf{u}}\right)\\
\mathbf{m}_{\mathbf{f},\mathbf{fs},\mathbf{u}} &= \left[\begin{smallmatrix}\mathbf{0}\\\mathbf{0}\\\mathbf{0}\end{smallmatrix}\right]\\
\mathbf{\Sigma}_{\mathbf{f},\mathbf{fs},\mathbf{u}} &= \left[\begin{smallmatrix}\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}&\mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{fs}}&\mathbf{K}_{\mathbf{f},\mathbf{u}}\\\mathbf{K}_{\mathbf{fs},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}&\mathbf{K}_{\mathbf{fs},\mathbf{fs}}&\mathbf{K}_{\mathbf{fs},\mathbf{u}}\\\mathbf{K}_{\mathbf{u},\mathbf{f}}&\mathbf{K}_{\mathbf{u},\mathbf{fs}}&\mathbf{K}_{\mathbf{u},\mathbf{u}}\end{smallmatrix}\right]\\
\end{align*}

```python
# Effective prior: q(f,fs)
q_f_fs = q_f_fs_u.marginalize([u])

print("q_f_fs:")
display(Latex(utils.matLatex(q_f_fs)))
```

    q_f_fs:

\begin{align*}
q_{FITC}\left(\mathbf{f},\mathbf{fs}\right)&= \mathcal{N}\left(\mathbf{m}_{\mathbf{f},\mathbf{fs}},\mathbf{\Sigma}_{\mathbf{f},\mathbf{fs}}\right)\\
\mathbf{m}_{\mathbf{f},\mathbf{fs}} &= \left[\begin{smallmatrix}\mathbf{0}\\\mathbf{0}\end{smallmatrix}\right]\\
\mathbf{\Sigma}_{\mathbf{f},\mathbf{fs}} &= \left[\begin{smallmatrix}\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}&\mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{fs}}\\\mathbf{K}_{\mathbf{fs},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}&\mathbf{K}_{\mathbf{fs},\mathbf{fs}}\end{smallmatrix}\right]\\
\end{align*}

## 3. Likelihood

```python
p_ygf = MVG([y],mean=f,cov=s_y**2*Identity(n),cond_vars=[f])

print("p_ygf:")
display(Latex(utils.matLatex(p_ygf)))
```

    p_ygf:

\begin{align*}
p\left(\mathbf{y}|\mathbf{f}\right)&= \mathcal{N}\left(\mathbf{m}_{\mathbf{y}|\mathbf{f}},\mathbf{\Sigma}_{\mathbf{y}|\mathbf{f}}\right)\\
\mathbf{m}_{\mathbf{y}|\mathbf{f}} &= \mathbf{f}\\
\mathbf{\Sigma}_{\mathbf{y}|\mathbf{f}} &= \sigma_y^{2} \mathbf{I}\\
\end{align*}

## 4. Inference

```python
# q(f,fs,y)
q_f_fs_y = p_ygf*q_f_fs

print("q_f_fs_y:")
display(Latex(utils.matLatex(q_f_fs_y)))
```

    q_f_fs_y:

\begin{align*}
q_{FITC}\left(\mathbf{y},\mathbf{fs},\mathbf{f}\right)&= \mathcal{N}\left(\mathbf{m}_{\mathbf{y},\mathbf{fs},\mathbf{f}},\mathbf{\Sigma}_{\mathbf{y},\mathbf{fs},\mathbf{f}}\right)\\
\mathbf{m}_{\mathbf{y},\mathbf{fs},\mathbf{f}} &= \left[\begin{smallmatrix}\mathbf{0}\\\mathbf{0}\\\mathbf{0}\end{smallmatrix}\right]\\
\mathbf{\Sigma}_{\mathbf{y},\mathbf{fs},\mathbf{f}} &= \left[\begin{smallmatrix}\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}&\mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{fs}}&\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\\\mathbf{K}_{\mathbf{fs},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}&\mathbf{K}_{\mathbf{fs},\mathbf{fs}}&\mathbf{K}_{\mathbf{fs},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\\\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}&\mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{fs}}&\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\end{smallmatrix}\right]\\
\end{align*}

```python
# q(f,fs|y)
q_f_fs_g_y = q_f_fs_y.condition([y])

print("q_f_fs_g_y:")
display(Latex(utils.matLatex(q_f_fs_g_y)))
```

    q_f_fs_g_y:

\begin{align*}
q_{FITC}\left(\mathbf{fs},\mathbf{f}|\mathbf{y}\right)&= \mathcal{N}\left(\mathbf{m}_{\mathbf{fs},\mathbf{f}|\mathbf{y}},\mathbf{\Sigma}_{\mathbf{fs},\mathbf{f}|\mathbf{y}}\right)\\
\mathbf{m}_{\mathbf{fs},\mathbf{f}|\mathbf{y}} &= \left[\begin{smallmatrix}\mathbf{K}_{\mathbf{fs},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}} \left(\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)^{-1} \mathbf{y}\\\left(\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right) \left(\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)^{-1} \mathbf{y}\end{smallmatrix}\right]\\
\mathbf{\Sigma}_{\mathbf{fs},\mathbf{f}|\mathbf{y}} &= \left[\begin{smallmatrix}\mathbf{K}_{\mathbf{fs},\mathbf{fs}} - \mathbf{K}_{\mathbf{fs},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}} \left(\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)^{-1} \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{fs}}&\mathbf{K}_{\mathbf{fs},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}} - \mathbf{K}_{\mathbf{fs},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}} \left(\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)^{-1} \left(\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)\\\mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{fs}} - \left(\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right) \left(\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)^{-1} \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{fs}}&\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}} - \left(\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right) \left(\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)^{-1} \left(\text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)\end{smallmatrix}\right]\\
\end{align*}

```python
# q(fs|y)
q_fs_g_y = q_f_fs_g_y.marginalize([f])

print("q_fs_g_y:")
display(Latex(utils.matLatex(q_fs_g_y)))
```

    q_fs_g_y:

\begin{align*}
q_{FITC}\left(\mathbf{fs}|\mathbf{y}\right)&= \mathcal{N}\left(\mathbf{m}_{\mathbf{fs}|\mathbf{y}},\mathbf{\Sigma}_{\mathbf{fs}|\mathbf{y}}\right)\\
\mathbf{m}_{\mathbf{fs}|\mathbf{y}} &= \mathbf{K}_{\mathbf{fs},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}} \left(\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)^{-1} \mathbf{y}\\
\mathbf{\Sigma}_{\mathbf{fs}|\mathbf{y}} &= \mathbf{K}_{\mathbf{fs},\mathbf{fs}} - \mathbf{K}_{\mathbf{fs},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}} \left(\sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}] + \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{f}}\right)^{-1} \mathbf{K}_{\mathbf{f},\mathbf{u}} \mathbf{K}_{\mathbf{u},\mathbf{u}}^{-1} \mathbf{K}_{\mathbf{u},\mathbf{fs}}\\
\end{align*}

```python
Q = kern.Kernel(name='Q')
repl_dict = {K(fs,u)*K(u,u).I*K(u,f): Q(fs,f),
             K(f,u)*K(u,u).I*K(u,f): Q(f,f),
             K(f,u)*K(u,u).I*K(u,fs): Q(f,fs)}

q_fs_g_y.mean.expanded = utils.replace(q_fs_g_y.mean, repl_dict)
q_fs_g_y.covar.expanded = utils.replace(q_fs_g_y.covar, repl_dict)

print("q_fs_g_y (replaced):")
display(Latex(utils.matLatex(q_fs_g_y)))
```

    Q(fs,f)*(Q(f,f) + σ_y**2*I + SuperDiagMat(K(f,f) + (-1)*Q(f,f)))^-1*y
    K(fs,fs) + (-1)*Q(fs,f)*(Q(f,f) + σ_y**2*I + SuperDiagMat(K(f,f) + (-1)*Q(f,f)))^-1*Q(f,fs)
    q_fs_g_y (replaced):

\begin{align*}
q_{FITC}\left(\mathbf{fs}|\mathbf{y}\right)&= \mathcal{N}\left(\mathbf{m}_{\mathbf{fs}|\mathbf{y}},\mathbf{\Sigma}_{\mathbf{fs}|\mathbf{y}}\right)\\
\mathbf{m}_{\mathbf{fs}|\mathbf{y}} &= \mathbf{Q}_{\mathbf{fs},\mathbf{f}} \left(\mathbf{Q}_{\mathbf{f},\mathbf{f}} + \sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{Q}_{\mathbf{f},\mathbf{f}}]\right)^{-1} \mathbf{y}\\
\mathbf{\Sigma}_{\mathbf{fs}|\mathbf{y}} &= \mathbf{K}_{\mathbf{fs},\mathbf{fs}} - \mathbf{Q}_{\mathbf{fs},\mathbf{f}} \left(\mathbf{Q}_{\mathbf{f},\mathbf{f}} + \sigma_y^{2} \mathbf{I} + \text{diag}[\mathbf{K}_{\mathbf{f},\mathbf{f}} - \mathbf{Q}_{\mathbf{f},\mathbf{f}}]\right)^{-1} \mathbf{Q}_{\mathbf{f},\mathbf{fs}}\\
\end{align*}

```python

```