# Neuromatch Academy: Week 3, Day 2, Tutorial 1
# Neuronal Network Dynamics: Neural Rate Models

__Content creators:__ Qinglong Gu, Songtin Li, Arvind Kumar, John Murray, Julijana Gjorgjieva

__Content reviewers:__ Spiros Chavlis, Lorenzo Fontolan, Richard Gao, Maryam Vaziri-Pashkam,Michael Waskom

---
# Tutorial Objectives

The brain is a complex system, not because it is composed of a large number of diverse types of neurons, but mainly because of how neurons are connected to each other. The brain is indeed a network of highly specialized neuronal networks.

The activity of a neural network constantly evolves in time. For this reason, neurons can be modeled as dynamical systems. The dynamical system approach is only one of the many modeling approaches that computational neuroscientists have developed (other points of view include information processing,  statistical models, etc.).

How the dynamics of neuronal networks affect the representation and processing of information in the brain is an open question. However, signatures of altered brain dynamics present in many brain diseases (e.g., in epilepsy or Parkinson's disease) tell us that it is crucial to study network activity dynamics if we want to understand the brain.

In this tutorial, we will simulate and study one of the simplest models of biological neuronal networks. Instead of modeling and simulating individual excitatory neurons (e.g., LIF models that you implemented yesterday), we will treat them as a single homogeneous population and approximate their dynamics using a single one-dimensional equation describing the evolution of their average spiking rate in time.

In this tutorial, we will learn how to build a firing rate model of a single population of excitatory neurons.

**Steps:**
- Write the equation for the firing rate dynamics of a 1D excitatory population.
- Visualize the response of the population as a function of parameters such as threshold level and gain, using the frequency-current (F-I) curve.
- Numerically simulate the dynamics of the excitatory population and find the fixed points of the system.
- Investigate the stability of the fixed points by linearizing the dynamics around them.

---
# Setup

```python
# Imports
import matplotlib.pyplot as plt
import numpy as np
import scipy.optimize as opt       # root-finding algorithm
```

```python
# @title Figure Settings
import ipywidgets as widgets       # interactive display
%config InlineBackend.figure_format = 'retina'
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/course-content/master/nma.mplstyle")
```

```python
# @title Helper functions

def plot_fI(x, f):
  plt.figure(figsize=(6, 4))  # plot the figure
  plt.plot(x, f, 'k')
  plt.xlabel('x (a.u.)', fontsize=14)
  plt.ylabel('F(x)', fontsize=14)
  plt.show()

def plot_dr_r(r, drdt):
  plt.figure()
  plt.plot(r, drdt, 'k')
  plt.plot(r, 0. * r, 'k--')
  plt.xlabel(r'$r$')
  plt.ylabel(r'$\frac{dr}{dt}$', fontsize=20)
  plt.ylim(-0.1, 0.1)

def plot_dFdt(x, dFdt):
  plt.figure()
  plt.plot(x, dFdt, 'r')
  plt.xlabel('x (a.u.)', fontsize=14)
  plt.ylabel('dF(x)', fontsize=14)
  plt.show()
```

# Section 1: Neuronal network dynamics

```python
# @title Video 1: Dynamic networks
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="p848349hPyw", width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

    Video available at https://youtube.com/watch?v=p848349hPyw

## Section 1.1: Dynamics of a single excitatory population

Individual neurons respond by spiking. When we average the spikes of neurons in a population, we can define the average firing activity of the population. In this model, we are interested in how the population-averaged firing varies as a function of time and network parameters. Mathematically, we can describe the firing rate dynamic as:

\begin{align}
\tau \frac{dr}{dt} &= -r + F(w\cdot r + I_{\text{ext}})  \quad\qquad (1)
\end{align}

$r(t)$ represents the average firing rate of the excitatory population at time $t$, $\tau$ controls the timescale of the evolution of the average firing rate, $w$ denotes the strength (synaptic weight) of the recurrent input to the population, $I_{\text{ext}}$ represents the external input, and the transfer function $F(\cdot)$ (which can be related to f-I curve of individual neurons described in the next sections) represents the population activation function in response to all received inputs.

To start building the model, please execute the cell below to initialize the simulation parameters.

```python
# @title Default parameters for a single excitatory population model

def default_pars_single(**kwargs):
  pars = {}

  # Excitatory parameters
  pars['tau'] = 1.     # Timescale of the E population [ms]
  pars['a'] = 1.2      # Gain of the E population
  pars['theta'] = 2.8  # Threshold of the E population

  # Connection strength
  pars['w'] = 0.  # E to E, we first set it to 0

  # External input
  pars['I_ext'] = 0.

  # simulation parameters
  pars['T'] = 20.       # Total duration of simulation [ms]
  pars['dt'] = .1       # Simulation time step [ms]
  pars['r_init'] = 0.2  # Initial value of E

  # External parameters if any
  for k in kwargs:
    pars[k] = kwargs[k]

  # Vector of discretized time points [ms]
  pars['range_t'] = np.arange(0, pars['T'], pars['dt'])

  return pars

```

You can use:
- `pars = default_pars_single()` to get all the parameters, and then you can execute `print(pars)` to check these parameters.
- `pars = default_pars_single(T=T_sim, dt=time_step)` to set new simulation time and time step
- After `pars = default_pars_single()`, use `pars['New_para'] = value` to add an new parameter with its value

## Section 1.2: F-I curves
In electrophysiology, a neuron is often characterized by its spike rate output in response to input currents. This is often called the **F-I** curve, denoting the output spike frequency (**F**) in response to different injected currents (**I**). We estimated this for an LIF neuron in yesterday's tutorial.

The transfer function $F(\cdot)$ in Equation $1$ represents the gain of the population as a function of the total input. The gain is often modeled as a sigmoidal function, i.e., more input drive leads to a nonlinear increase in the population firing rate. The output firing rate will eventually saturate for high input values.

A sigmoidal $F(\cdot)$ is parameterized by its gain $a$ and threshold $\theta$.

$$ F(x;a,\theta) = \frac{1}{1+\text{e}^{-a(x-\theta)}} - \frac{1}{1+\text{e}^{a\theta}}  \quad(2)$$

The argument $x$ represents the input to the population. Note that the second term is chosen so that $F(0;a,\theta)=0$.

Many other transfer functions (generally monotonic) can be also used. Examples are the rectified linear function $ReLU(x)$ or the hyperbolic tangent $tanh(x)$.

### Exercise 1: Implement F-I curve

Let's first investigate the activation functions before simulating the dynamics of the entire population.

In this exercise, you will implement a sigmoidal **F-I** curve or transfer function $F(x)$, with gain $a$ and threshold level $\theta$ as parameters.

```python
def F(x, a, theta):
  """
  Population activation function.

  Args:
    x (float): the population input
    a (float): the gain of the function
    theta (float): the threshold of the function

  Returns:
    float: the population activation response F(x) for input x
  """
  #################################################
  ## TODO for students: compute f = F(x) ##
  # Fill out function and remove
  raise NotImplementedError("Student excercise: implement the f-I function")
  #################################################
  # add the expression of f = F(x)
  f = ...

  return f

pars = default_pars_single()  # get default parameters
x = np.arange(0, 10, .1)      # set the range of input

# Uncomment below to test your function
# f = F(x, pars['a'], pars['theta'])

# plot_fI(x, f)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W3D2_DynamicNetworks/solutions/W3D2_Tutorial1_Solution_37924c14.py)

*Example output:*

### Interactive Demo: Parameter exploration of F-I curve
Here's an interactive demo that shows how the F-I curve is changing for different values of the gain and threshold parameters.

**Remember to enable the demo by running the cell.**

```python
# @title

# @markdown Make sure you execute this cell to enable the widget!

def interactive_plot_FI(a, theta):
  """
  Population activation function.

  Expecxts:
    a     : the gain of the function
    theta : the threshold of the function

  Returns:
    plot the F-I curve with give parameters
  """

  # set the range of input
  x = np.arange(0, 10, .1)
  plt.figure()
  plt.plot(x, F(x, a, theta), 'k')
  plt.xlabel('x (a.u.)', fontsize=14)
  plt.ylabel('F(x)', fontsize=14)
  plt.show()

_ = widgets.interact(interactive_plot_FI, a=(0.3, 3, 0.3), theta=(2, 4, 0.2))
```

## Section 1.3: Simulation scheme of E dynamics

Because $F(\cdot)$ is a nonlinear function, the exact solution of Equation $1$ can not be determined via analytical methods. Therefore, numerical methods must be used to find the solution. In practice, the derivative on the left-hand side of Equation $1$ can be approximated using the Euler method on a time-grid of stepsize $\Delta t$:

\begin{align}
&\frac{dr}{dt} \approx \frac{r[k+1]-r[k]}{\Delta t}
\end{align}
where $r[k] = r(k\Delta t)$.

Thus,

$$\Delta r[k] = \frac{\Delta t}{\tau}[-r[k] + F(w\cdot r[k] + I_{\text{ext}}(k;a,\theta))]$$

Hence, Equation (1) is updated at each time step by:

$$r[k+1] = r[k] + \Delta r[k]$$

**_Please execute the following cell to enable the WC simulator_**

```python
# @title Single population rate model simulator: `simulate_single`

def simulate_single(pars):
  """
  Simulate an excitatory population of neurons

  Args:
    pars   : Parameter dictionary

  Returns:
    rE     : Activity of excitatory population (array)

  Example:
    pars = default_pars_single()
    r = simulate_single(pars)
  """

  # Set parameters
  tau, a, theta = pars['tau'], pars['a'], pars['theta']
  w = pars['w']
  I_ext = pars['I_ext']
  r_init = pars['r_init']
  dt, range_t = pars['dt'], pars['range_t']
  Lt = range_t.size

  # Initialize activity
  r = np.zeros(Lt)
  r[0] = r_init
  I_ext = I_ext * np.ones(Lt)

  # Update the E activity
  for k in range(Lt - 1):
      dr = dt / tau * (-r[k] + F(w * r[k] + I_ext[k], a, theta))
      r[k+1] = r[k] + dr

  return r

print(help(simulate_single))
```

    Help on function simulate_single in module __main__:

    simulate_single(pars)
        Simulate an excitatory population of neurons

        Args:
          pars   : Parameter dictionary

        Returns:
          rE     : Activity of excitatory population (array)

        Example:
          pars = default_pars_single()
          r = simulate_single(pars)

    None

### Interactive Demo: Parameter Exploration of single population dynamics

Note that $w=0$, as in the default setting, means no recurrent input to the neuron population in Equation (1). Hence, the dynamics is entirely determined by the external input $I_{\text{ext}}$. Try to explore how $r_{\text{sim}}(t)$ changes with different $I_{\text{ext}}$ and $\tau$ parameter values, and investigate the relationship between $F(I_{\text{ext}}; a, \theta)$ and the steady value of $r(t)$. Note that, $r_{\rm ana}(t)$ denotes the analytical solution.

```python
# @title

# @markdown Make sure you execute this cell to enable the widget!

# get default parameters
pars = default_pars_single(T=20.)

def Myplot_E_diffI_difftau(I_ext, tau):
  # set external input and time constant
  pars['I_ext'] = I_ext
  pars['tau'] = tau

  # simulation
  r = simulate_single(pars)

  # Analytical Solution
  r_ana = (pars['r_init']
           + (F(I_ext, pars['a'], pars['theta'])
           - pars['r_init']) * (1. - np.exp(-pars['range_t'] / pars['tau'])))

  # plot
  plt.figure()
  plt.plot(pars['range_t'], r, 'b', label=r'$r_{\mathrm{sim}}$(t)', alpha=0.5,
           zorder=1)
  plt.plot(pars['range_t'], r_ana, 'b--', lw=5, dashes=(2, 2),
           label=r'$r_{\mathrm{ana}}$(t)', zorder=2)
  plt.plot(pars['range_t'],
           F(I_ext, pars['a'], pars['theta']) * np.ones(pars['range_t'].size),
           'k--', label=r'$F(I_{\mathrm{ext}})$')
  plt.xlabel('t (ms)', fontsize=16.)
  plt.ylabel('Activity r(t)', fontsize=16.)
  plt.legend(loc='best', fontsize=14.)
  plt.show()

_ = widgets.interact(Myplot_E_diffI_difftau, I_ext=(0.0, 10., 1.),
                     tau=(1., 5., 0.2))
```

## Think!
Above, we have numerically solved a system driven by a positive input and that, if $w_{EE} \neq 0$, receives an excitatory recurrent input (**try changing the value of $w_{EE}$ to a positive number**). Yet, $r_E(t)$ either decays to zero or reaches a fixed non-zero value.
- Why doesn't the solution of the system "explode" in a finite time? In other words, what guarantees that $r_E$(t) stays finite?
- Which parameter would you change in order to increase the maximum value of the response?

# Section 2: Fixed points of the single population system

```python
# @title Video 2: Fixed point
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="Ox3ELd1UFyo", width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

    Video available at https://youtube.com/watch?v=Ox3ELd1UFyo

As you varied the two parameters in the last Interactive Demo, you noticed that, while at first the system output quickly changes, with time, it reaches its maximum/minimum value and does not change anymore. The value eventually reached by the system is called the **steady state** of the system, or the **fixed point**. Essentially, in the steady states the derivative with respect to time of the activity ($r$) is zero, i.e. $\displaystyle \frac{dr}{dt}=0$.

We can find that the steady state of the Equation. (1) by setting $\displaystyle{\frac{dr}{dt}=0}$ and solve for $r$:

$$-r_{\text{steady}} + F(w\cdot r_{\text{steady}} + I_{\text{ext}};a,\theta) = 0, \qquad (3)$$

When it exists, the solution of Equation. (3) defines a **fixed point** of the dynamical system Equation. (1). Note that if $F(x)$ is nonlinear, it is not always possible to find an analytical solution, but the solution can be found via numerical simulations, as we will do later.

From the Interactive Demo, one could also notice that the value of $\tau$ influences how quickly the activity will converge to the steady state from its initial value.

In the specific case of $w=0$, we can also analytically compute  the solution of Equation (1) (i.e., the thick blue dashed line) and deduce the role of $\tau$ in determining the convergence to the fixed point:

$$\displaystyle{r(t) = \big{[}F(I_{\text{ext}};a,\theta) -r(t=0)\big{]} (1-\text{e}^{-\frac{t}{\tau}})} + r(t=0)$$ \\

We can now numerically calculate the fixed point with the `scipy.optimize.root` function.

_(Recall that at the very beginning, we `import scipy.optimize as opt` )_.

\\

Please execute the cell below to define the functions `my_fp_single` and `check_fp_single`

```python
# @title Function of calculating the fixed point

# @markdown Make sure you execute this cell to enable the function!

def my_fp_single(pars, r_init):
  """
  Calculate the fixed point through drE/dt=0

  Args:
    pars    : Parameter dictionary
    r_init  : Initial value used for scipy.optimize function

  Returns:
    x_fp    : value of fixed point
  """

  # get the parameters
  a, theta = pars['a'], pars['theta']
  w = pars['w']
  I_ext = pars['I_ext']

  # define the right hand of E dynamics
  def my_WCr(x):
    r = x
    drdt = (-r + F(w * r + I_ext, a, theta))
    y = np.array(drdt)

    return y

  x0 = np.array(r_init)
  x_fp = opt.root(my_WCr, x0).x

  return x_fp

print(help(my_fp_single))

def check_fp_single(pars, x_fp, mytol=1e-4):
  """
   Verify |dr/dt| < mytol

  Args:
    pars    : Parameter dictionary
    fp      : value of fixed point
    mytol   : tolerance, default as 10^{-4}

  Returns :
    Whether it is a correct fixed point: True/False
  """

  a, theta = pars['a'], pars['theta']
  w = pars['w']
  I_ext = pars['I_ext']

  # calculate Equation(3)
  y = x_fp - F(w * x_fp + I_ext, a, theta)

  # Here we set tolerance as 10^{-4}
  return np.abs(y) < mytol

print(help(check_fp_single))
```

    Help on function my_fp_single in module __main__:

    my_fp_single(pars, r_init)
        Calculate the fixed point through drE/dt=0

        Args:
          pars    : Parameter dictionary
          r_init  : Initial value used for scipy.optimize function

        Returns:
          x_fp    : value of fixed point

    None
    Help on function check_fp_single in module __main__:

    check_fp_single(pars, x_fp, mytol=0.0001)
         Verify |dr/dt| < mytol

        Args:
          pars    : Parameter dictionary
          fp      : value of fixed point
          mytol   : tolerance, default as 10^{-4}

        Returns :
          Whether it is a correct fixed point: True/False

    None

## Exercise 2: Visualization of the fixed point

When it is not possible to find the solution for Equation (3) analytically, a graphical approach can be taken. To that end, it is useful to plot $\displaystyle{\frac{dr}{dt}=0}$ as a function of $r$. The values of $r$ for which the plotted function crosses zero on the y axis correspond to fixed points.

Here, let us, for example, set $w=5.0$ and $I^{\text{ext}}=0.5$. From Equation (1), you can obtain

$$\frac{dr}{dt} = [-r + F(w\cdot r + I^{\text{ext}})]/\tau $$

Then, plot the $dr/dt$ as a function of $r$, and check for the presence of fixed points.

Finally, try to find the fixed points using the previously defined function `my_fp_single(pars, r_init)` with proper initial values ($r_{\text{init}}$). You can use the previously defined function `check_fp_single(pars, x_fp)` to verify that the values of $r{\rm fp}$ for which $\displaystyle{\frac{dr}{dt}} = 0$ are the true fixed points. From the line $\displaystyle{\frac{dr}{dt}}$ plotted above, the proper initial values can be chosen as the values close to where the line crosses zero on the y axis (real fixed point).

```python
pars = default_pars_single()  # get default parameters

# set your external input and wEE
pars['I_ext'] = 0.5
pars['w'] = 5.0

r = np.linspace(0, 1, 1000)  # give the values of r

# Calculate drEdt
# drdt = ...

# Uncomment this to plot the drdt across r
# plot_dr_r(r, drdt)

################################################################
# TODO for students:
# Find the values close to the intersections of drdt and y=0
# as your initial values
# Calculate the fixed point with your initial value, verify them,
# and plot the corret ones
# check if x_fp is the intersection of the lines with the given function
# check_fpE(pars, x_fp)
# vary different initial values to find the correct fixed point (Should be 3)
# Use blue, red and yellow colors, respectively ('b', 'r', 'y' codenames)
################################################################

# Calculate the first fixed point with your initial value
# x_fp_1 = my_fp_single(pars, ...)
# if check_fp_single(pars, x_fp_1):
#   plt.plot(x_fp_1, 0, 'bo', ms=8)

# Calculate the second fixed point with your initial value
# x_fp_2 = my_fp_single(pars, ...)
# if check_fp_single(pars, x_fp_2):
#   plt.plot(x_fp_2, 0, 'ro', ms=8)

# Calculate the third fixed point with your initial value
# x_fp_3 = my_fp_single(pars, ...)
# if check_fp_single(pars, x_fp_3):
#   plt.plot(x_fp_3, 0, 'yo', ms=8)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W3D2_DynamicNetworks/solutions/W3D2_Tutorial1_Solution_37eb2320.py)

*Example output:*

## Interactive Demo: fixed points as a function of recurrent and external inputs.

You can now explore how the previous plot changes when the recurrent coupling $w$ and the external input $I_{\text{ext}}$ take different values.

```python
# @title

# @markdown Make sure you execute this cell to enable the widget!

def plot_intersection_single(w, I_ext):
  # set your parameters
  pars['w'] = w
  pars['I_ext'] = I_ext

  # note that wEE!=0
  if w > 0:
    # find fixed point
    x_fp_1 = my_fp_single(pars, 0.)
    x_fp_2 = my_fp_single(pars, 0.4)
    x_fp_3 = my_fp_single(pars, 0.9)

    plt.figure()
    r = np.linspace(0, 1., 1000)
    drdt = (-r + F(w * r + I_ext, pars['a'], pars['theta'])) / pars['tau']

    plt.plot(r, drdt, 'k')
    plt.plot(r, 0. * r, 'k--')

    if check_fp_single(pars, x_fp_1):
        plt.plot(x_fp_1, 0, 'bo', ms=8)
    if check_fp_single(pars, x_fp_2):
        plt.plot(x_fp_2, 0, 'ro', ms=8)
    if check_fp_single(pars, x_fp_3):
        plt.plot(x_fp_3, 0, 'yo', ms=8)

    plt.xlabel(r'$r$', fontsize=14.)
    plt.ylabel(r'$\frac{dr}{dt}$', fontsize=20.)

    plt.show()

_ = widgets.interact(plot_intersection_single, w=(1, 7, 0.2),
                     I_ext=(0, 3, 0.1))
```

---
# Summary

In this tutorial, we have investigated the dynamics of a rate-based single population of neurons.

We learned about:
- The effect of the input parameters and the time constant of the network on the dynamics of the population.
- How to find the fixed point(s) of the system.

Next, we have two Bonus, but important concepts in dynamical system analysis and simulation. If you have time left, watch the next video and proceed to solve the exercises. You will learn:

- How to determine the stability of a fixed point by linearizing the system.
- How to add realistic inputs to our model.

---
# Bonus 1: Stability of a fixed point

```python
# @title Video 3: Stability of fixed points
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="KKMlWWU83Jg", width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

    Video available at https://youtube.com/watch?v=KKMlWWU83Jg

#### Initial values and trajectories

Here, let us first set $w=5.0$ and $I_{\text{ext}}=0.5$, and investigate the dynamics of $r(t)$ starting with different initial values $r(0) \equiv r_{\text{init}}$. We will plot the trajectories of $r(t)$ with $r_{\text{init}} = 0.0, 0.1, 0.2,..., 0.9$.

```python
# @title Initial values

# @markdown Make sure you execute this cell to see the trajectories!

pars = default_pars_single()
pars['w'] = 5.0
pars['I_ext'] = 0.5

plt.figure(figsize=(8, 5))
for ie in range(10):
  pars['r_init'] = 0.1 * ie  # set the initial value
  r = simulate_single(pars)  # run the simulation

  # plot the activity with given initial
  plt.plot(pars['range_t'], r, 'b', alpha=0.1 + 0.1 * ie,
           label=r'r$_{\mathrm{init}}$=%.1f' % (0.1 * ie))

plt.xlabel('t (ms)')
plt.title('Two steady states?')
plt.ylabel(r'$r$(t)')
plt.legend(loc=[1.01, -0.06], fontsize=14)
plt.show()
```

## Interactive Demo: dynamics as a function of the initial value

Let's now set $r_{\rm init}$ to a value of your choice in this demo. How does the solution change? What do you observe?

```python
# @title

# @markdown Make sure you execute this cell to enable the widget!

pars = default_pars_single()
pars['w'] = 5.0
pars['I_ext'] = 0.5

def plot_single_diffEinit(r_init):
  pars['r_init'] = r_init
  r = simulate_single(pars)

  plt.figure()
  plt.plot(pars['range_t'], r, 'b', zorder=1)
  plt.plot(0, r[0], 'bo', alpha=0.7, zorder=2)
  plt.xlabel('t (ms)', fontsize=16)
  plt.ylabel(r'$r(t)$', fontsize=16)
  plt.ylim(0, 1.0)
  plt.show()

_ = widgets.interact(plot_single_diffEinit, r_init=(0, 1, 0.02))
```

### Stability analysis via linearization of the dynamics

Just like Equation $1$ in the case ($w=0$) discussed above, a generic linear system
$$\frac{dx}{dt} = \lambda (x - b),$$
has a fixed point for $x=b$. The analytical solution of such a system can be found to be:
$$x(t) = b + \big{(} x(0) - b \big{)} \text{e}^{\lambda t}.$$
Now consider a small perturbation of the activity around the fixed point: $x(0) = b+ \epsilon$, where $|\epsilon| \ll 1$. Will the perturbation $\epsilon(t)$ grow with time or will it decay to the fixed point? The evolution of the perturbation with time can be written, using the analytical solution for $x(t)$, as:
 $$\epsilon (t) = x(t) - b = \epsilon \text{e}^{\lambda t}$$

- if $\lambda < 0$, $\epsilon(t)$ decays to zero, $x(t)$ will still converge to $b$ and the fixed point is "**stable**".

- if $\lambda > 0$, $\epsilon(t)$ grows with time, $x(t)$ will leave the fixed point $b$ exponentially, and the fixed point is, therefore, "**unstable**" .

### Compute the stability of Equation $1$

Similar to what we did in the linear system above, in order to determine the stability of a fixed point $r^{*}$ of the excitatory population dynamics, we perturb Equation (1) around $r^{*}$ by $\epsilon$, i.e. $r = r^{*} + \epsilon$. We can plug in Equation (1) and obtain the equation determining the time evolution of the perturbation $\epsilon(t)$:

\begin{align}
\tau \frac{d\epsilon}{dt} \approx -\epsilon + w F'(w\cdot r^{*} + I_{\text{ext}};a,\theta) \epsilon
\end{align}

where $F'(\cdot)$ is the derivative of the transfer function $F(\cdot)$. We can rewrite the above equation as:

\begin{align}
\frac{d\epsilon}{dt} \approx \frac{\epsilon}{\tau }[-1 + w F'(w\cdot r^* + I_{\text{ext}};a,\theta)]
\end{align}

That is, as in the linear system above, the value of

$$\lambda = [-1+ wF'(w\cdot r^* + I_{\text{ext}};a,\theta)]/\tau \qquad (4)$$

determines whether the perturbation will grow or decay to zero, i.e., $\lambda$ defines the stability of the fixed point. This value is called the **eigenvalue** of the dynamical system.

## Exercise 3: Compute $dF$ and Eigenvalue

The derivative of the sigmoid transfer function is:
\begin{align}
\frac{dF}{dx} & = \frac{d}{dx} (1+\exp\{-a(x-\theta)\})^{-1}  \\
& = a\exp\{-a(x-\theta)\} (1+\exp\{-a(x-\theta)\})^{-2}. \qquad (5)
\end{align}

Let's now find the expression for the derivative $\displaystyle{\frac{dF}{dx}}$ in the following cell and plot it.

```python
def dF(x, a, theta):
  """
  Population activation function.

  Args:
    x     : the population input
    a     : the gain of the function
    theta : the threshold of the function

  Returns:
    dFdx  : the population activation response F(x) for input x
  """

  #################################################
  # TODO for students: compute dFdx ##
  raise NotImplementedError("Student excercise: compute the deravitive of F")
  #################################################
  # Calculate the population activation
  dFdx = ...

  return dFdx

pars = default_pars_single()  # get default parameters
x = np.arange(0, 10, .1)      # set the range of input

# Uncomment below to test your function
# df = dF(x, pars['a'], pars['theta'])

# plot_dFdt(x, df)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W3D2_DynamicNetworks/solutions/W3D2_Tutorial1_Solution_ce2e3bc5.py)

*Example output:*

## Exercise 4: Compute eigenvalues

As discussed above, for the case with $w=5.0$ and $I_{\text{ext}}=0.5$, the system displays **three** fixed points. However, when we simulated the dynamics and varied the initial conditions $r_{\rm init}$, we could only obtain **two** steady states. In this exercise, we will now check the stability of each of the three fixed points by calculating the corresponding eigenvalues with the function `eig_single`. Check the sign of each eigenvalue (i.e., stability of each fixed point). How many of the fixed points are stable?

Note that the expression of the eigenvalue at fixed point $r^*$
$$\lambda = [-1+ wF'(w\cdot r^* + I_{\text{ext}};a,\theta)]/\tau$$

```python
def eig_single(pars, fp):
  """
  Args:
    pars : Parameter dictionary
    fp   : fixed point r_fp

  Returns:
    eig : eigevalue of the linearized system
  """

  # get the parameters
  tau, a, theta = pars['tau'], pars['a'], pars['theta']
  w, I_ext = pars['w'], pars['I_ext']
  print(tau, a, theta, w, I_ext)

  #################################################
  ## TODO for students: compute eigenvalue ##
  raise NotImplementedError("Student excercise: compute the eigenvalue")
  #################################################
  # Compute the eigenvalue
  eig = ...

  return eig

pars = default_pars_single()
pars['w'] = 5.0
pars['I_ext'] = 0.5

# Uncomment below lines after completing the eig_single function.
# Find the eigenvalues for all fixed points of Exercise 2

# x_fp_1 = my_fp_single(pars, 0.).item()
# eig_fp_1 = eig_single(pars, x_fp_1).item()
# print(f'Fixed point1 at {x_fp_1:.3f} with Eigenvalue={eig_fp_1:.3f}')

# x_fp_2 = my_fp_single(pars, 0.4).item()
# eig_fp_2 = eig_single(pars, x_fp_2).item()
# print(f'Fixed point2 at {x_fp_2:.3f} with Eigenvalue={eig_fp_2:.3f}')

# x_fp_3 = my_fp_single(pars, 0.9).item()
# eig_fp_3 = eig_single(pars, x_fp_3).item()
# print(f'Fixed point3 at {x_fp_3:.3f} with Eigenvalue={eig_fp_3:.3f}')
```

**SAMPLE OUTPUT**

```
Fixed point1 at 0.042 with Eigenvalue=-0.583
Fixed point2 at 0.447 with Eigenvalue=0.498
Fixed point3 at 0.900 with Eigenvalue=-0.626
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W3D2_DynamicNetworks/solutions/W3D2_Tutorial1_Solution_4bd389b3.py)

## Think!
Throughout the tutorial, we have assumed $w> 0 $, i.e., we considered a single population of **excitatory** neurons. What do you think will be the behavior of a population of inhibitory neurons, i.e., where $w> 0$ is replaced by $w< 0$?

---
# Bonus 2: Noisy input drives the transition between two stable states

## Ornstein-Uhlenbeck (OU) process

As discussed in several previous tutorials, the OU process is usually used to generate a noisy input into the neuron. The OU input $\eta(t)$ follows:

$$\tau_\eta \frac{d}{dt}\eta(t) = -\eta (t) + \sigma_\eta\sqrt{2\tau_\eta}\xi(t)$$

Execute the following function `my_OU(pars, sig, myseed=False)` to generate an OU process.

```python
# @title OU process `my_OU(pars, sig, myseed=False)`

# @markdown Make sure you execute this cell to visualize the noise!

def my_OU(pars, sig, myseed=False):
  """
  A functions that generates Ornstein-Uhlenback process

  Args:
    pars       : parameter dictionary
    sig        : noise amplitute
    myseed     : random seed. int or boolean

  Returns:
    I          : Ornstein-Uhlenbeck input current
  """

  # Retrieve simulation parameters
  dt, range_t = pars['dt'], pars['range_t']
  Lt = range_t.size
  tau_ou = pars['tau_ou']  # [ms]

  # set random seed
  if myseed:
      np.random.seed(seed=myseed)
  else:
      np.random.seed()

  # Initialize
  noise = np.random.randn(Lt)
  I_ou = np.zeros(Lt)
  I_ou[0] = noise[0] * sig

  # generate OU
  for it in range(Lt - 1):
    I_ou[it + 1] = (I_ou[it]
                    + dt / tau_ou * (0. - I_ou[it])
                    + np.sqrt(2 * dt / tau_ou) * sig * noise[it + 1])

  return I_ou

pars = default_pars_single(T=100)
pars['tau_ou'] = 1.  # [ms]
sig_ou = 0.1
I_ou = my_OU(pars, sig=sig_ou, myseed=2020)
plt.figure(figsize=(10, 4))
plt.plot(pars['range_t'], I_ou, 'r')
plt.xlabel('t (ms)')
plt.ylabel(r'$I_{\mathrm{OU}}$')
plt.show()
```

## Example: Up-Down transition

In the presence of two or more fixed points, noisy inputs can drive a transition between the fixed points! Here, we stimulate an E population for 1,000 ms applying OU inputs.

```python
# @title Simulation of an E population with OU inputs

# @markdown Make sure you execute this cell to spot the Up-Down states!

pars = default_pars_single(T=1000)
pars['w'] = 5.0
sig_ou = 0.7
pars['tau_ou'] = 1.  # [ms]
pars['I_ext'] = 0.56 + my_OU(pars, sig=sig_ou, myseed=2020)

r = simulate_single(pars)

plt.figure(figsize=(10, 4))
plt.plot(pars['range_t'], r, 'b', alpha=0.8)
plt.xlabel('t (ms)')
plt.ylabel(r'$r(t)$')
plt.show()
```


Empecemos probando un Árbol de Decisión, con nuestro famoso dataset de **Iris** 😀

```python
#Importamos la librerias
import pandas as pd
import numpy as np

import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.datasets import load_iris
```

```python
#Convertimos a nuestro objeto en un dataframe
iris = load_iris()
data_frame = pd.DataFrame(data= np.c_[iris['data'], iris['target']],
                     columns= iris['feature_names'] + ['target'])
```

```python
data_frame
```

      sepal length (cm)
      sepal width (cm)
      petal length (cm)
      petal width (cm)
      target

      0
      5.1
      3.5
      1.4
      0.2
      0.0

      1
      4.9
      3.0
      1.4
      0.2
      0.0

      2
      4.7
      3.2
      1.3
      0.2
      0.0

      3
      4.6
      3.1
      1.5
      0.2
      0.0

      4
      5.0
      3.6
      1.4
      0.2
      0.0

      ...
      ...
      ...
      ...
      ...
      ...

      145
      6.7
      3.0
      5.2
      2.3
      2.0

      146
      6.3
      2.5
      5.0
      1.9
      2.0

      147
      6.5
      3.0
      5.2
      2.0
      2.0

      148
      6.2
      3.4
      5.4
      2.3
      2.0

      149
      5.9
      3.0
      5.1
      1.8
      2.0

150 rows × 5 columns

```python
data_frame.target.unique()
```

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

```python
#Veamos el shape
print(data_frame.shape)
```

    (150, 5)

```python
X = data_frame.drop("target", axis=1) #Elimino de mi dataset la variable a predecir
X
```

      sepal length (cm)
      sepal width (cm)
      petal length (cm)
      petal width (cm)

      0
      5.1
      3.5
      1.4
      0.2

      1
      4.9
      3.0
      1.4
      0.2

      2
      4.7
      3.2
      1.3
      0.2

      3
      4.6
      3.1
      1.5
      0.2

      4
      5.0
      3.6
      1.4
      0.2

      ...
      ...
      ...
      ...
      ...

      145
      6.7
      3.0
      5.2
      2.3

      146
      6.3
      2.5
      5.0
      1.9

      147
      6.5
      3.0
      5.2
      2.0

      148
      6.2
      3.4
      5.4
      2.3

      149
      5.9
      3.0
      5.1
      1.8

150 rows × 4 columns

```python
y = data_frame.target #Defino el Target
y
```

    0      0.0
    1      0.0
    2      0.0
    3      0.0
    4      0.0
          ...
    145    2.0
    146    2.0
    147    2.0
    148    2.0
    149    2.0
    Name: target, Length: 150, dtype: float64

```python
#Importamos la libreria para separar en train y test
from sklearn.model_selection import train_test_split

#Me quedo con 30% para test y 70% para train
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30, random_state=42)
```

```python
X_train
```

      sepal length (cm)
      sepal width (cm)
      petal length (cm)
      petal width (cm)

      81
      5.5
      2.4
      3.7
      1.0

      133
      6.3
      2.8
      5.1
      1.5

      137
      6.4
      3.1
      5.5
      1.8

      75
      6.6
      3.0
      4.4
      1.4

      109
      7.2
      3.6
      6.1
      2.5

      ...
      ...
      ...
      ...
      ...

      71
      6.1
      2.8
      4.0
      1.3

      106
      4.9
      2.5
      4.5
      1.7

      14
      5.8
      4.0
      1.2
      0.2

      92
      5.8
      2.6
      4.0
      1.2

      102
      7.1
      3.0
      5.9
      2.1

105 rows × 4 columns

```python
X_test.shape
```

    (45, 4)

```python
from sklearn.tree import DecisionTreeClassifier #Importamos el modelo
tree = DecisionTreeClassifier(max_depth=3, random_state = 42,criterion="gini") #Creamos el modelo
```

```python
#Entrenamos el modelo
tree.fit(X_train,y_train)
```

    DecisionTreeClassifier(ccp_alpha=0.0, class_weight=None, criterion='gini',
                           max_depth=3, max_features=None, max_leaf_nodes=None,
                           min_impurity_decrease=0.0, min_impurity_split=None,
                           min_samples_leaf=1, min_samples_split=2,
                           min_weight_fraction_leaf=0.0, presort='deprecated',
                           random_state=42, splitter='best')

```python
X_train
```

      sepal length (cm)
      sepal width (cm)
      petal length (cm)
      petal width (cm)

      81
      5.5
      2.4
      3.7
      1.0

      133
      6.3
      2.8
      5.1
      1.5

      137
      6.4
      3.1
      5.5
      1.8

      75
      6.6
      3.0
      4.4
      1.4

      109
      7.2
      3.6
      6.1
      2.5

      ...
      ...
      ...
      ...
      ...

      71
      6.1
      2.8
      4.0
      1.3

      106
      4.9
      2.5
      4.5
      1.7

      14
      5.8
      4.0
      1.2
      0.2

      92
      5.8
      2.6
      4.0
      1.2

      102
      7.1
      3.0
      5.9
      2.1

105 rows × 4 columns

```python
y_train_pred = tree.predict(X_train) #Prediccion en Train
y_train_pred
```

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

```python
X_test.head()
```

      sepal length (cm)
      sepal width (cm)
      petal length (cm)
      petal width (cm)

      73
      6.1
      2.8
      4.7
      1.2

      18
      5.7
      3.8
      1.7
      0.3

      118
      7.7
      2.6
      6.9
      2.3

      78
      6.0
      2.9
      4.5
      1.5

      76
      6.8
      2.8
      4.8
      1.4

```python
y_test_pred = tree.predict(X_test) #Prediccion en Test
y_test_pred
```

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

```python
from sklearn.metrics import accuracy_score

#Calculo el accuracy en Train
train_accuracy = accuracy_score(y_train, y_train_pred)

#Calculo el accuracy en Test
test_accuracy = accuracy_score(y_test, y_test_pred)

print('% de aciertos sobre el set de entrenamiento:', train_accuracy)
print('% de aciertos sobre el set de evaluación:',test_accuracy)
```

    % de aciertos sobre el set de entrenamiento: 0.9523809523809523
    % de aciertos sobre el set de evaluación: 1.0

```python
from sklearn.metrics import confusion_matrix

confusion_matrix(y_test,y_test_pred)
```

    array([[19,  0,  0],
           [ 0, 13,  0],
           [ 0,  0, 13]])

```python
confusion_matrix(y_train,y_train_pred)
```

    array([[31,  0,  0],
           [ 0, 36,  1],
           [ 0,  4, 33]])

```python
import pandas as pd
import pydotplus
import numpy  as np
import matplotlib.pyplot as plt
import seaborn as sns
import statsmodels.api as sm

from io import StringIO
from IPython.display import Image, SVG
from graphviz import Source
from matplotlib import cm
from matplotlib.colors import ListedColormap, LinearSegmentedColormap
from matplotlib.patches import Patch
from scipy.stats import chi2_contingency, norm
from sklearn.tree import export_graphviz
from sklearn import tree
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_curve, auc, accuracy_score
from sklearn.model_selection import StratifiedKFold, train_test_split
from statsmodels.formula.api import ols
from sympy import var, plot_implicit, Eq
```

    /usr/local/lib/python3.7/dist-packages/statsmodels/tools/_testing.py:19: FutureWarning: pandas.util.testing is deprecated. Use the functions in the public API at pandas.testing instead.
      import pandas.util.testing as tm

```python
data_frame
```

      sepal length (cm)
      sepal width (cm)
      petal length (cm)
      petal width (cm)
      target

      0
      5.1
      3.5
      1.4
      0.2
      0.0

      1
      4.9
      3.0
      1.4
      0.2
      0.0

      2
      4.7
      3.2
      1.3
      0.2
      0.0

      3
      4.6
      3.1
      1.5
      0.2
      0.0

      4
      5.0
      3.6
      1.4
      0.2
      0.0

      ...
      ...
      ...
      ...
      ...
      ...

      145
      6.7
      3.0
      5.2
      2.3
      2.0

      146
      6.3
      2.5
      5.0
      1.9
      2.0

      147
      6.5
      3.0
      5.2
      2.0
      2.0

      148
      6.2
      3.4
      5.4
      2.3
      2.0

      149
      5.9
      3.0
      5.1
      1.8
      2.0

150 rows × 5 columns

```python
clf = DecisionTreeClassifier(max_depth = 3,criterion='gini')

# Train Decision Tree Classifer
clf = clf.fit(X_train, y_train)
dot_data = StringIO()

export_graphviz(clf, out_file=dot_data, #feature_names = covariates,
                #class_names=['Charged Off', 'Fully Paid'],
                #filled=True, rounded=True,
                #special_characters=True,
                impurity='gini', proportion=True)
graph = pydotplus.graph_from_dot_data(dot_data.getvalue())
Image(graph.create_png())
```


```python
from sympy import symbols
from sympy import integrate, diff, log, sqrt, exp, erf
from sympy import oo, pi as π
```

```python
class Simplify:
    _SIMPLIFY = 'simplify'

    def __init__(self, lazy=False):
        self._lazy = lazy

    def __ror__(self, value):
        if self._lazy:
            return value
        _simplify = getattr(value, self._SIMPLIFY, None)
        if _simplify is not None:
            try:
                return _simplify()
            finally:
                print('Formula simplified.')

    def __call__(self, value):
        return self.__ror__(value)

simplify = Simplify(lazy=False)
```

```python
S, E, r, σ, T, t = symbols('S, E, r, σ, T, t')
_ = symbols('_')

d1 = (log(S/E) + r*(T-t) + σ**2*(T-t)/2) / (σ*sqrt(T-t))
d2 = d1 - σ*sqrt(T-t)

N = lambda x: (erf(sqrt(2)*x/2)+1)/2
```

```python
C = S*N(d1) - E*exp(-r*(T-t)*N(d2)) | simplify
Δ = diff(C, S)
ν = diff(C, σ)
θ = diff(C, t)
γ = diff(C, S, 2)
```

    Formula simplified.

```python
C
```

$\displaystyle - E e^{- \frac{r \left(T - t\right) \left(\operatorname{erf}{\left(\frac{\sqrt{2} \left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)}{4 σ \sqrt{T - t}} \right)} + 1\right)}{2}} + \frac{S \left(\operatorname{erf}{\left(\frac{\sqrt{2} \left(2 r \left(T - t\right) + σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)}{4 σ \sqrt{T - t}} \right)} + 1\right)}{2}$

```python
Δ | simplify
```

    Formula simplified.

$\displaystyle \frac{\sqrt{2} E r \sqrt{T - t} e^{- \frac{r \left(T - t\right) \left(\operatorname{erf}{\left(\frac{\sqrt{2} \left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)}{4 σ \sqrt{T - t}} \right)} + 1\right)}{2}} e^{- \frac{\left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)^{2}}{8 σ^{2} \left(T - t\right)}}}{2 \sqrt{\pi} S σ} + \frac{\operatorname{erf}{\left(\frac{\sqrt{2} \left(2 r \left(T - t\right) + σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)}{4 σ \sqrt{T - t}} \right)}}{2} + \frac{1}{2} + \frac{\sqrt{2} e^{- \frac{\left(2 r \left(T - t\right) + σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)^{2}}{8 σ^{2} \left(T - t\right)}}}{2 \sqrt{\pi} σ \sqrt{T - t}}$

```python
ν | simplify
```

    Formula simplified.

$\displaystyle \frac{\sqrt{2} \left(- E r \left(T - t\right) \left(2 r \left(T - t\right) + σ^{2} \left(- T + t\right) + 2 σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right) e^{\frac{\left(2 r \left(T - t\right) + σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)^{2}}{8 σ^{2} \left(T - t\right)}} + S \left(- 2 r \left(T - t\right) + σ^{2} \left(- T + t\right) + 2 σ^{2} \left(T - t\right) - 2 \log{\left(\frac{S}{E} \right)}\right) e^{\frac{4 r σ^{2} \left(T - t\right)^{2} \left(\operatorname{erf}{\left(\frac{\sqrt{2} \left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)}{4 σ \sqrt{T - t}} \right)} + 1\right) + \left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)^{2}}{8 σ^{2} \left(T - t\right)}}\right) e^{- \frac{4 r σ^{2} \left(T - t\right)^{2} \left(\operatorname{erf}{\left(\frac{\sqrt{2} \left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)}{4 σ \sqrt{T - t}} \right)} + 1\right) + \left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)^{2} + \left(2 r \left(T - t\right) + σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)^{2}}{8 σ^{2} \left(T - t\right)}}}{4 \sqrt{\pi} σ^{2} \sqrt{T - t}}$

```python
θ | simplify
```

    Formula simplified.

$\displaystyle \frac{\left(E r \left(T - t\right)^{2} \left(- 4 \sqrt{\pi} σ \left(T - t\right) \left(\operatorname{erf}{\left(\frac{\sqrt{2} \left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)}{4 σ \sqrt{T - t}} \right)} + 1\right) e^{\frac{\left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)^{2}}{8 σ^{2} \left(T - t\right)}} + \sqrt{2} \sqrt{T - t} \left(2 r \left(T - t\right) + σ^{2} \left(- T + t\right) - 2 \left(T - t\right) \left(2 r - σ^{2}\right) + 2 \log{\left(\frac{S}{E} \right)}\right)\right) e^{\frac{\left(2 r \left(T - t\right) + σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)^{2}}{8 σ^{2} \left(T - t\right)}} - \sqrt{2} S \left(T - t\right)^{\frac{3}{2}} \left(- 2 r \left(T - t\right) + σ^{2} \left(- T + t\right) + 2 \left(T - t\right) \left(2 r + σ^{2}\right) - 2 \log{\left(\frac{S}{E} \right)}\right) e^{\frac{4 r σ^{2} \left(T - t\right)^{2} \left(\operatorname{erf}{\left(\frac{\sqrt{2} \left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)}{4 σ \sqrt{T - t}} \right)} + 1\right) + \left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)^{2}}{8 σ^{2} \left(T - t\right)}}\right) e^{- \frac{4 r σ^{2} \left(T - t\right)^{2} \left(\operatorname{erf}{\left(\frac{\sqrt{2} \left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)}{4 σ \sqrt{T - t}} \right)} + 1\right) + \left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)^{2} + \left(2 r \left(T - t\right) + σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)^{2}}{8 σ^{2} \left(T - t\right)}}}{8 \sqrt{\pi} σ \left(T - t\right)^{3}}$

```python
γ | simplify
```

    Formula simplified.

$\displaystyle - \frac{\frac{2 E r^{2} \left(T - t\right) e^{- \frac{r \left(T - t\right) \left(\operatorname{erf}{\left(\frac{\sqrt{2} \left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)}{4 σ \sqrt{T - t}} \right)} + 1\right)}{2}} e^{- \frac{\left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)^{2}}{4 σ^{2} \left(T - t\right)}}}{\pi S σ} + \frac{2 \sqrt{2} E r \sqrt{T - t} e^{- \frac{r \left(T - t\right) \left(\operatorname{erf}{\left(\frac{\sqrt{2} \left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)}{4 σ \sqrt{T - t}} \right)} + 1\right)}{2}} e^{- \frac{\left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)^{2}}{8 σ^{2} \left(T - t\right)}}}{\sqrt{\pi} S} + \frac{\sqrt{2} E r \left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right) e^{- \frac{r \left(T - t\right) \left(\operatorname{erf}{\left(\frac{\sqrt{2} \left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)}{4 σ \sqrt{T - t}} \right)} + 1\right)}{2}} e^{- \frac{\left(2 r \left(T - t\right) - σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)^{2}}{8 σ^{2} \left(T - t\right)}}}{\sqrt{\pi} S σ^{2} \sqrt{T - t}} - \frac{2 \sqrt{2} e^{- \frac{\left(2 r \left(T - t\right) + σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)^{2}}{8 σ^{2} \left(T - t\right)}}}{\sqrt{\pi} \sqrt{T - t}} + \frac{\sqrt{2} \left(2 r \left(T - t\right) + σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right) e^{- \frac{\left(2 r \left(T - t\right) + σ^{2} \left(T - t\right) + 2 \log{\left(\frac{S}{E} \right)}\right)^{2}}{8 σ^{2} \left(T - t\right)}}}{\sqrt{\pi} σ^{2} \left(T - t\right)^{\frac{3}{2}}}}{4 S σ}$

```python

```


Table of Contents

1  Introduction

1.1  Simulation

1.1.1  The Mesh

1.1.2  The Elements and DofHandlers

1.1.3  The Gaussian Field

1.1.4  Assembly

1.1.5  Solver

1.1.6  Quantity of Interest

2  Multiresolution Approximation

# Optimal Upscaling

```python
# Add src folder to path
import os
import sys

sys.path.insert(0,'../../src/')
```

## Introduction

Suppose $u$ satisfies the elliptic partial differential equation

\begin{align*}
-\nabla \cdot (\exp(q) \nabla u) = 1, \ \ &x \in D,\\
u = 0, \ \ &x \in \partial D
\end{align*}

where $D=[0,1]^2$ is a physical domain and $q(x,\omega)$ is a Gaussian random field. Moreover, let

\begin{equation}
J = \iint_{R}  u(x,\omega) dx, \ \ \text{where } R = [0.75,1]^2,
\end{equation}

be a physical quantity of interest for which we want to compute statistics using Monte Carlo sampling or whatever.

### Simulation

First, we show how to generate a single sample of $J$. Specifically, we will

1. Construct the computational mesh and define the elements
2. Generate samples of the random parameter $q$
3. Assemble the finite element system
4. Solve the system for $u$
5. Compute the associated $J$

We will use the ```quadmesh``` package, obtainable [here](https://github.com/hvanwyk/quadmesh).

#### The Mesh

The ```mesh``` module contains methods for generating and working with meshes. In this work, we use quadrilateral meshes, since they can be readily refined. On our mesh, we need to mark the boundary $\partial D$ and the region of integration $R$. To do this, we use marking functions that check whether a point lies in the given region.

```python
from mesh import QuadMesh
from plot import Plot

plot = Plot()

# Define the Quadrilateral Mesh
mesh = QuadMesh(resolution=(16,16))

# Mark boundary
bnd_fn = lambda x,y: abs(x)<1e-6 or abs(1-x)<1e-6 or abs(y)<1e-6 or abs(1-y)<1e-6
mesh.mark_region('bnd', bnd_fn, entity_type='half_edge', on_boundary=True)

# Mark averaging region
dmn_fn = lambda x,y: x>=0.75 and x<=1 and y>=0.75 and y<=1
mesh.mark_region('dmn', dmn_fn, entity_type='cell', strict_containment=True, on_boundary=False)
#cells = mesh.get_region(flag='dmn', entity_type='cell', on_boundary=False, subforest_flag=None)
plot.mesh(mesh, regions=[('bnd','edge'),('dmn','cell')])
```

#### The Elements and DofHandlers

Next we need to define the approximation space, i.e. the elements. This is done using the ```QuadFE``` class.
- Here we use piecewise constant elements (```DQ0```) to represent the parameter and piecewise linear elements (```Q1```) for the solution.
- Since we would like to have multiple different elements associated with the same mesh, we handle the element degrees of freedom by means of a **degrees-of-freedom-handler** or ```DofHandler```, which contains information of the ```mesh``` and the ```element```.
- For the assembly and function definitions, we may also require ```Basis``` functions. These also encode derivative information.

```python
from fem import QuadFE, DofHandler, Basis

#
# Elements
#
Q0 = QuadFE(mesh.dim(), 'DQ0')  # "Discontinuous Quadratic of Degree 0", for parameter
Q1 = QuadFE(mesh.dim(), 'Q1')  # "Continuous Linear" for output

#
# DofHandlers
#
dQ0 = DofHandler(mesh,Q0)
dQ1 = DofHandler(mesh,Q1)

#
# Distribute DOFs, i.e. store the dof-indices in the dofhandler
#
dQ0.distribute_dofs()
dQ1.distribute_dofs()

#
# Basis functions for assembly and definition of Nodal functions
#
phi_0 = Basis(dQ0)  # piecewise constant

phi_1 = Basis(dQ1)  # piecewise linear
phix_1 = Basis(dQ1,'ux')  # piecewise linear first partial w.r.t. x
phiy_1 = Basis(dQ1,'uy')  # piecewise linear first partial w.r.t. y
```

#### The Gaussian Field

The ```gmrf``` module contains routines to generate a variety of Gaussian random fields.
1. First, the covariance matrix is assembled from a bivariate kernel function using the ```Covariance``` class. The assembly is dependent on the mesh and approximation space (element) associated with the field.
2. Next we compute its eigendecomposition.
3. Finally we define a Gaussian random field using the ```GaussianField``` class, which allows us to sample, condition, etc.
4. Samples from the Gaussian field are given as vectors but we can use these to define ```Nodal``` interpolants on the given mesh.

```python
from function import Nodal
from gmrf import Covariance
from gmrf import GaussianField
import numpy as np

#
# Approximate Covariance Matrix
#
cov = Covariance(dQ0, name='gaussian', parameters={'l':0.01})
cov.compute_eig_decomp()

#
# Define the Gaussian field
#
q = GaussianField(dQ0.n_dofs(), K=cov)

# Sample Random field and store all samples in a Nodal DQ0 finite element function
n_samples = 100
eq = Nodal(basis=phi_0, data=np.exp(q.sample(n_samples)))

#
# Plot a single sample
#
plot.contour(eq, n_sample=20)
```

#### Assembly

The weak form of the elliptic PDE is given by

\begin{equation}\label{eq:weak_form}
\iint_D \exp(q) \nabla u \cdot \nabla \phi dx = \iint_D 1 \phi dx,
\end{equation}

for any test function $\phi$. Approximating $u = \sum_j c_j \phi_j$ in the Galerkin framework, we get $A c = b $, where

\begin{equation}\label{eq:bilinear}
A_{ij} = \iint_D \exp(q) \nabla \phi_j \cdot \nabla \phi_i dx = \iint_D \exp(q) \frac{\partial \phi_j}{\partial x} \frac{\partial \phi_i}{\partial x} + \exp(q)  \frac{\partial \phi_j}{\partial u}\frac{\partial \phi_i}{\partial y} dx
\end{equation}

and

\begin{equation}\label{eq:linear}
b_i = \iint_D \phi_i dx.
\end{equation}

The assembly therefore requires a bilinear form $A$ and a linear form $b$. We define each form with the ```Form``` class in the ```assembler``` module.

Each form requires a ```Kernel``` function, possibly a ```test``` function, and possibly a ```trial``` function (both of which should be ```Basis``` functions).

> For example, the linear form \ref{eq:linear} is defined by ```Form(1, test=phi_1)```, while the bilinear form $\iint_D \exp(q) \frac{\partial \phi_j}{\partial x} \frac{\partial \phi_i}{\partial x}dx$ is stored as ```Form(eq, test=phix_1, trial=phix_1)```.

1. The object ```Form(kernel)``` is assembled into a scalar.
2. The linear form ```Form(kernel, test=phi_1)``` is assembled into a vector.
3. The bilinear form ```Form(kernel, test=phix_1, trial=phix_1)``` is assembled into a sparse matrix.

Some forms, such as $\iint_R \phi_i dx$ should only be assembled over a subdomain of the mesh. To do this, use ```Form(1, test=phi_1, flag='dmn')```, where ```dmn``` was the flag marking the region $R$ from earlier.

> __REMARK:__ When the kernel is a function with multiple samples (such as our function ```eq```), then the relevant form is assembled for all samples.

The ```Assembler``` class handles finite element assembly. For each problem you want to assemble, e.g. the elliptic problem, or the problem of computing $J$, you list all relevant bilinear, linear, and constant forms you need. The assembler will automatically add up the contributions of all bilinear forms into a sparse matrix, those of all the linear forms into a vector, and those of all the constant forms into a scalar. To assemble multiple problems, simply form a ```list``` of all relevant problems.

```python
from assembler import Assembler
from assembler import Kernel
from assembler import Form

# Define weak form of elliptic problem
problem1 = [Form(eq, test=phix_1, trial=phix_1),
            Form(eq, test=phiy_1, trial=phiy_1),
            Form(1, test=phi_1)]

# Define integration operator
problem2 = [Form(1,test=phi_1,flag='dmn')]

# Put them together in a list
state = [problem1,problem2]

# Assemble system
assembler = Assembler(state,mesh)  # initialize the assembler
assembler.add_dirichlet('bnd', dir_fn=0, i_problem=0)  # add homogeneous Dirichlet conditions for problem 0
assembler.assemble()  # assemble

```

The vector representing the integration operator $\iint_R \phi_i dx$ can be obtained by

```python
J = assembler.get_vector(i_problem=1)
```

#### Solver

To solve the elliptic equation, you simply specify the problem (default=0), the matrix sample (default=0), and the vector sample for the right hand side (default=0) you want to use, i.e.

```python
# Solve system
u_vec = assembler.solve(i_problem=0, i_matrix=20)

# Define a finite element function using the solution vector
u = Nodal(basis=phi_1, data=u_vec)

# Plot
plot.contour(u)
```

#### Quantity of Interest

We can now compute $J = \iint_R u dx$ by taking dot product

```python
J_sample = J.dot(u_vec)

print(J_sample)
```

    0.0007864274342134218

## Multiresolution Approximation

We would like to represent our spatially varying input parameter $q$ in a mult-resolution decomposition. Specifically, if $\tilde{\mathbb{T}}$ is a sub-mesh of $\mathbb{T}$ we would like to construct the projection of our piecewise constant function $q$ onto subspace spanned by the piecewise constant basis functions on the coarse mesh $\tilde{\mathbb{T}}$. To this end, we define a projection matrix.

```python
from scipy import sparse as sp

def projection_matrix(dofhandler, fine_flag, coarse_flag):
    """
    Project a piecewise constant function, defined on a fine scale mesh onto
    a coarse scale mesh.

    Inputs:

        dofhandler: DofHandler, for discontinuous piecewise constant elements

        fine_flag: str/int, mesh-flag for fine mesh

        coarse_flag: str/int, mesh-flag for coarse mesh

    Outputs:

        P: double, sparse (n_dofs_coars, n_dofs_fine) matrix representation of
            the projection,

    """
    assert dofhandler.element.element_type()=='DQ0', \
        'Only piecewise constant approximations supported.'

    mesh = dofhandler.mesh
    rows, cols, vals = [], [], []
    for leaf in mesh.cells.get_leaves(subforest_flag=fine_flag):
        # Iterate over fine mesh

        # Add leaf dof to list of columns
        cols.extend(dofhandler.get_cell_dofs(leaf))

        # Search for nearest ancestor in coarse grid
        ancestor = leaf
        while not ancestor.is_marked(coarse_flag):
            ancestor = ancestor.get_parent()

        # Record coarse cell dof
        rows.extend(dofhandler.get_cell_dofs(ancestor))

        # Determine the ratio in areas
        if mesh.dim()==1:
            # One-dimensional interval
            multiplier = leaf.length()/ancestor.length()
        elif mesh.dim()==2:
            # Two-dimensional cell
            multiplier = leaf.area()/ancestor.area()

        # Store the value
        vals.append(multiplier)

    #
    # Re-index rows and columns
    #

    # Compute unique dofs
    col_dofs = list(set(cols))
    row_dofs = list(set(rows))

    # Re-index using unique dofs
    rows = [row_dofs.index(i) for i in rows]
    cols = [col_dofs.index(i) for i in cols]

    #
    # Define sparse projection matrix
    #
    n_rows = len(row_dofs)
    n_cols = len(col_dofs)

    P = sp.coo_matrix((vals,(rows,cols)), shape=(n_rows,n_cols))

    return P

```

Let's test it out. Take $q(x_1,x_2) = \sin(\pi x_1)\sin(\pi x_2)$ on $[0,1]^2$ and project it onto a lower dimensional mesh.

```python
import matplotlib.pyplot as plt

# Define initial coarse mesh
mesh = QuadMesh(resolution=(4,4))

# Label the coarse mesh
mesh.record('coarse')

# Refine the mesh
for i in range(3):
    mesh.cells.refine()

# Label the fine mesh
mesh.record('fine')

# Plot the meshes
fig, axes = plt.subplots(1,2,figsize=(8,4))
plot = Plot(quickview=False)
axes[0] = plot.mesh(mesh, axis=axes[0], subforest_flag='coarse')
axes[0].set_title('Coarse Mesh')

axes[1] = plot.mesh(mesh, axis=axes[1], subforest_flag='fine')
axes[1].set_title('Fine Mesh')

plt.show()
```

```python
#
# Define Piecewise Constant Elements
#
Q0 = QuadFE(2,'DQ0')

# DofHandler
dQ0 = DofHandler(mesh,Q0)
dQ0.distribute_dofs()

# Basis on the fine mesh
phi_fine = Basis(dQ0)

# Define function on the fine mesh (note how x is treated as a two column array)
q_fine = Nodal(f=lambda x: np.sin(np.pi*x[:,0])*np.sin(np.pi*x[:,1]), basis=phi_fine)

# Compute the projection matrix
P = projection_matrix(dQ0,'fine','coarse')

# Basis function on the coarse mesh
phi_coarse = Basis(dQ0, subforest_flag='coarse')

# Define the projection of q onto the coarse basis
q_coarse = Nodal(data = P.dot(q_fine.data()), basis=phi_coarse)

#
# Plot Results
#
fig, axes = plt.subplots(1,2, figsize=(12,5))

axes[0] = plot.contour(q_fine,axis=axes[0])
axes[0].set_title('Original Function')

axes[1] = plot.contour(q_coarse, axis=axes[1])
axes[1].set_title('Projection')

plt.show()
```


```python
from sympy import symbols, solve, log, diff
from scipy.optimize import minimize_scalar, newton, minimize
from scipy.integrate import quad
from scipy.stats import norm
import numpy as np
import pandas as pd
from numpy.linalg import inv
import matplotlib.pyplot as plt
from numpy.random import dirichlet
```

```python
%matplotlib inline
plt.style.use('fivethirtyeight')
np.random.seed(42)
```

## The optimal size of a bet

```python
share, odds, probability = symbols('share odds probability')
Value = probability * log(1 + odds * share) + (1 - probability) * log(1 - share)
solve(diff(Value, share), share)
```

    [(odds*probability + probability - 1)/odds]

```python
f, p = symbols('f p')
y = p * log(1 + f) + (1 - p) * log(1 - f)
solve(diff(y, f), f)
```

    [2*p - 1]

## Get S&P 500 Data

```python
with pd.HDFStore('../../data/assets.h5') as store:
    sp500 = store['sp500/prices'].close
```

### Compute Returns & Standard Deviation

```python
annual_returns = sp500.resample('A').last().pct_change().to_frame('sp500')
```

```python
return_params = annual_returns.sp500.rolling(25).agg(['mean', 'std']).dropna()
```

```python
return_ci = (return_params[['mean']]
                .assign(lower=return_params['mean'].sub(return_params['std'].mul(2)))
                .assign(upper=return_params['mean'].add(return_params['std'].mul(2))))
```

```python
return_ci.plot(lw=2, figsize=(14, 8));
```

### Kelly Rule for Index Returns

```python
def norm_integral(f, mean, std):
    val, er = quad(lambda s: np.log(1 + f * s) * norm.pdf(s, mean, std),
                               mean - 3 * std,
                               mean + 3 * std)
    return -val
```

```python
def norm_dev_integral(f, mean, std):
    val, er = quad(lambda s: (s / (1 + f * s)) * norm.pdf(s, mean, std), m-3*std, mean+3*std)
    return val
```

```python
def get_kelly_share(data):
    solution = minimize_scalar(norm_integral,
                        args=(data['mean'], data['std']),
                        bounds=[0, 2],
                        method='bounded')
    return solution.x
```

```python
annual_returns['f'] = return_params.apply(get_kelly_share, axis=1)
```

    /home/stefan/.pyenv/versions/at-3.6/lib/python3.6/site-packages/ipykernel_launcher.py:2: RuntimeWarning: invalid value encountered in log

    /home/stefan/.pyenv/versions/at-3.6/lib/python3.6/site-packages/scipy/integrate/quadpack.py:385: IntegrationWarning: The occurrence of roundoff error is detected, which prevents
      the requested tolerance from being achieved.  The error may be
      underestimated.
      warnings.warn(msg, IntegrationWarning)
    /home/stefan/.pyenv/versions/at-3.6/lib/python3.6/site-packages/scipy/integrate/quadpack.py:385: IntegrationWarning: The occurrence of roundoff error is detected, which prevents
      the requested tolerance from being achieved.  The error may be
      underestimated.
      warnings.warn(msg, IntegrationWarning)
    /home/stefan/.pyenv/versions/at-3.6/lib/python3.6/site-packages/scipy/integrate/quadpack.py:385: IntegrationWarning: The occurrence of roundoff error is detected, which prevents
      the requested tolerance from being achieved.  The error may be
      underestimated.
      warnings.warn(msg, IntegrationWarning)
    /home/stefan/.pyenv/versions/at-3.6/lib/python3.6/site-packages/scipy/integrate/quadpack.py:385: IntegrationWarning: The occurrence of roundoff error is detected, which prevents
      the requested tolerance from being achieved.  The error may be
      underestimated.
      warnings.warn(msg, IntegrationWarning)
    /home/stefan/.pyenv/versions/at-3.6/lib/python3.6/site-packages/scipy/integrate/quadpack.py:385: IntegrationWarning: The occurrence of roundoff error is detected, which prevents
      the requested tolerance from being achieved.  The error may be
      underestimated.
      warnings.warn(msg, IntegrationWarning)
    /home/stefan/.pyenv/versions/at-3.6/lib/python3.6/site-packages/scipy/integrate/quadpack.py:385: IntegrationWarning: The occurrence of roundoff error is detected, which prevents
      the requested tolerance from being achieved.  The error may be
      underestimated.
      warnings.warn(msg, IntegrationWarning)
    /home/stefan/.pyenv/versions/at-3.6/lib/python3.6/site-packages/scipy/integrate/quadpack.py:385: IntegrationWarning: The occurrence of roundoff error is detected, which prevents
      the requested tolerance from being achieved.  The error may be
      underestimated.
      warnings.warn(msg, IntegrationWarning)

```python
return_params.plot(subplots=True, lw=2, figsize=(14, 8));
```

```python
annual_returns.tail()
```

      sp500
      f

      date

      2014-12-31
      0.113906
      1.999996

      2015-12-31
      -0.007266
      1.999996

      2016-12-31
      0.095350
      1.999996

      2017-12-31
      0.194200
      1.999996

      2018-12-31
      0.014815
      1.999996

### Performance Evaluation

```python
(annual_returns[['sp500']]
 .assign(kelly=annual_returns.sp500.mul(annual_returns.f.shift()))
 .dropna()
 .loc['1900':]
 .add(1)
 .cumprod()
 .sub(1)
 .plot(lw=2));

```

```python
annual_returns.f.describe()
```

    count    205.000000
    mean       1.303795
    std        0.585540
    min        0.000003
    25%        0.830339
    50%        1.227349
    75%        1.999996
    max        1.999996
    Name: f, dtype: float64

```python
return_ci.head()
```

      mean
      lower
      upper

      date

      1814-12-31
      0.074059
      -0.426468
      0.574586

      1815-12-31
      0.070621
      -0.429199
      0.570442

      1816-12-31
      0.064872
      -0.436591
      0.566336

      1817-12-31
      0.075920
      -0.418596
      0.570436

      1818-12-31
      0.074465
      -0.419380
      0.568309

### Compute Kelly Fraction

```python
m = .058
s = .216
```

```python
# Option 1: minimize the expectation integral
sol = minimize_scalar(norm_integral, args=(m, s), bounds=[0., 2.], method='bounded')
print('Optimal Kelly fraction: {:.4f}'.format(sol.x))
```

    Optimal Kelly fraction: 1.1974

```python
# Option 2: take the derivative of the expectation and make it null
x0 = newton(norm_dev_integral, .1, args=(m, s))
print('Optimal Kelly fraction: {:.4f}'.format(x0))
```

    Optimal Kelly fraction: 1.1974

## Kelly Rule for Multiple Assets

```python
with pd.HDFStore('../../data/assets.h5') as store:
    sp500_stocks = store['sp500/stocks'].index
    prices = store['quandl/wiki/prices'].adj_close.unstack('ticker').filter(sp500_stocks)
```

```python
prices.info()
```

    <class 'pandas.core.frame.DataFrame'>
    DatetimeIndex: 14277 entries, 1962-01-02 to 2018-03-27
    Columns: 501 entries, MMM to ZTS
    dtypes: float64(501)
    memory usage: 54.7 MB

```python
monthly_returns = prices.loc['1988':'2017'].resample('M').last().pct_change().dropna(how='all').dropna(axis=1)
stocks = monthly_returns.columns
monthly_returns.info()
```

    <class 'pandas.core.frame.DataFrame'>
    DatetimeIndex: 359 entries, 1988-02-29 to 2017-12-31
    Freq: M
    Columns: 217 entries, MMM to XRX
    dtypes: float64(217)
    memory usage: 611.4 KB

```python
cov = monthly_returns.cov()
inv_cov = pd.DataFrame(inv(cov), index=stocks, columns=stocks)
```

```python
kelly_allocation = monthly_returns.mean().dot(inv_cov)
```

```python
kelly_allocation.describe()
```

    count    217.000000
    mean       0.213979
    std        3.493734
    min      -10.840354
    25%       -1.845360
    50%        0.002192
    75%        1.990041
    max       11.316506
    dtype: float64

```python
kelly_allocation.sum()
```

    46.43347236762427

```python
TRADING_DAYS = 12
```

```python
def pf_vol(weights, cov):
    return np.sqrt(weights.T @ cov @ weights)

def pf_ret(weights, mean_ret):
    return weights @ mean_ret.values

def pf_performance(weights, mean_ret, cov):
    r = pf_ret(weights, mean_ret)
    sd = pf_vol(weights, cov)
    return r, sd
```

```python
n_assets = len(stocks)  # number of assets to allocate

x0 = np.full(n_assets, 1 / n_assets)
mean_asset_ret = monthly_returns.mean()
asset_cov = monthly_returns.cov()
```

```python
def simulate_pf(mean_ret, cov):
    perf, weights = [], []
    for i in range(N_PORTFOLIOS):
        if i % 50000 == 0:
            print(i)
        weights = dirichlet([.08] * n_assets)
        weights /= np.sum(weights)

        r, sd = pf_performance(weights, mean_ret, cov)
        perf.append([r, sd, (r - RF_RATE) / sd])
    perf_df = pd.DataFrame(perf, columns=['ret', 'vol', 'sharpe'])
    return perf_df, weights
```

```python
RF_RATE = 0
def neg_sharpe_ratio(weights, mean_ret, cov):
    r, sd = pf_performance(weights, mean_ret, cov)
    return -(r / sd)

def max_sharpe_ratio(mean_ret, cov):
    args = (mean_ret, cov)
    constraints = {'type': 'eq', 'fun': lambda x: np.sum(x) - 1}
    bounds = ((-1, 1),) * n_assets
    return minimize(fun=neg_sharpe_ratio,
                    x0=x0,
                    args=args,
                    method='SLSQP',
                    bounds=bounds,
                    constraints=constraints)
```

```python
res = max_sharpe_ratio(mean_asset_ret, asset_cov)
```

```python
(res.x / kelly_allocation).sort_values().plot.bar(figsize=(15, 5));
```

```python
def pf_volatility(w, r, c):
    return pf_performance(w, r, c)[1]

def efficient_return(mean_ret, cov, target):
    args = (mean_ret, cov)

    def ret_(weights):
        return pf_ret(weights, mean_ret)

    constraints = [{'type': 'eq', 'fun': lambda x: ret_(x) - target},
                   {'type': 'eq', 'fun': lambda x: np.sum(x) - 1}]

    bounds = ((0.0, 1.0),) * n_assets

    # noinspection PyTypeChecker
    return minimize(pf_volatility,
                    x0=x0,
                    args=args, method='SLSQP',
                    bounds=bounds,
                    constraints=constraints)

def efficient_frontier(mean_ret, cov, ret_range):
    efficient_pf = []
    for ret in ret_range:
        efficient_pf.append(efficient_return(mean_ret, cov, ret))
    return efficient_pf
```

```python
def calculate_efficient_frontier(mean_ret, cov):
    perf, wt = simulate_pf(mean_ret, cov)

    max_sharpe = max_sharpe_ratio(mean_ret, cov)
    max_sharpe_perf = pf_performance(max_sharpe.x, mean_ret, cov)
    wmax = max_sharpe.x
    print(np.sum(wmax))

    min_vol = min_variance(mean_ret, cov)
    min_vol_perf = pf_performance(min_vol['x'], mean_ret, cov)

    pf = ['Max Sharpe', 'Min Vol']
    alloc = pd.DataFrame(dict(zip(pf, [max_sharpe.x, min_vol.x])), index=assets)
    selected_pf = pd.DataFrame(dict(zip(pf, [max_sharpe_perf, min_vol_perf])),
                               index=['ret', 'vol'])

    print(selected_pf)
    print(perf.describe())

    perf.plot.scatter(x='vol', y='ret', c='sharpe',
                      cmap='YlGnBu', marker='o', s=10,
                      alpha=0.3, figsize=(10, 7), colorbar=True,
                      title='PF Simulation')

    r, sd = selected_pf['Max Sharpe'].values
    plt.scatter(sd, r, marker='*', color='r', s=500, label='Max Sharpe Ratio')
    r, sd = selected_pf['Min Vol'].values
    plt.scatter(sd, r, marker='*', color='g', s=500, label='Min volatility')
    plt.xlabel('Annualised Volatility')
    plt.ylabel('Annualised Returns')
    plt.legend(labelspacing=0.8)

    rmin = selected_pf.loc['ret', 'Min Vol']
    rmax = returns.add(1).prod().pow(1 / len(returns)).pow(TRADING_DAYS).sub(1).max()
    ret_range = np.linspace(rmin, rmax, 50)
    # ret_range = np.linspace(rmin, .22, 50)
    efficient_portfolios = efficient_frontier(mean_asset_ret, cov, ret_range)

    plt.plot([p['fun'] for p in efficient_portfolios], ret_range, linestyle='-.', color='black',
             label='efficient frontier')
    plt.title('Calculated Portfolio Optimization based on Efficient Frontier')
    plt.xlabel('annualised volatility')
    plt.ylabel('annualised returns')
    plt.legend(labelspacing=0.8)
    plt.tight_layout()
    plt.savefig('Calculated EF.png')

```

```python
def min_variance(mean_ret, cov):
    args = (mean_ret, cov)
    constraints = {'type': 'eq', 'fun': lambda x: np.sum(x) - 1}
    bounds = ((0, 1),) * n_assets

    return minimize(fun=pf_volatility,
                    x0=x0,
                    args=args,
                    method='SLSQP',
                    bounds=bounds,
                    constraints=constraints)
```

```python
res = min_variance(mean_asset_ret, asset_cov)
```

```python

```