### Vanden-Eijnden

```julia
using PyPlot, StatsBase, Printf, DelimitedFiles, Combinatorics;
using Revise;
using MDToolbox;
PyPlot.plt.style.use("seaborn-colorblind");
ENV["COLUMNS"] = 110; #display width for MDToolbox
```

    ┌ Info: Precompiling Combinatorics [861a8166-3701-5b0c-9a16-15d98fcdc6aa]
    └ @ Base loading.jl:1278
    ┌ Warning: no Manifest.toml file found, static paths used
    └ @ Revise /Users/yasu/.julia/packages/Revise/moD4B/src/packagedef.jl:1361
    ┌ Info: Precompiling MDToolbox [7965e2f0-a35b-11e8-0207-19c030dd2def]
    └ @ Base loading.jl:1278

### Potential energy function its gradients

```julia
V(x; k=1.0) = sum(- (1.0 / 2.0) .* k .* x.^2 .+ (1.0 ./ 4.0) .* k .* x.^4)
#V(x; k=1.0) = sum((1.0 .- x.^2).^2 .- 0.25 .* x)
V([0.0])
```

    0.0

```julia
grad(x; k=1.0) =  - k .* x .+ k .* x.^3
#grad(x; k=1.0) = -2.0.*(1.0 .- x.^2).*2.0.*x .- 0.25
```

    grad (generic function with 1 method)

```julia
#ISgrad(x, beta_replica, beta_sigma; k=1.0) = (-k .* x .+ k .* x.^3) ./ beta_replica .* beta_sigma
ISgrad(x, beta_replica, beta_sigma; k=1.0) = (-2.0.*(1.0 .- x.^2).*2.0.*x .- 0.25) ./ beta_replica .* beta_sigma
```

    ISgrad (generic function with 1 method)

### Define functions for infinite swapping

あるi番目のレプリカのpdf
\begin{align}
{\rho}_i({\bf x_i}) =  e^{-V({\bf x_i})/T_i}
\end{align}

系全体のpdfをQ(x)とするとき
ある置換σのpdfは
\begin{align}
Q(\sigma ,X) =1/N! {\rho}_{\sigma(1)}({\bf x_1}),...,{\rho}_{\sigma(N)}({\bf x_N})
\end{align}

ある置換σでのポテンシャル
\begin{align}
V(X, \sigma) = -\beta^{-1}{\bf log}Q(\sigma,X)=\beta^{-1}\sum_{i=1}^{N}\beta_{\sigma(i)}V(x_i)+const
\end{align}

運動方程式
\begin{align}
ma_j=-\beta^{-1}\beta_{\sigma(j)}\nabla V(x_j)-\gamma mv+\sqrt{2\gamma m \beta^{-1}}
\end{align}

次のステップの座標計算
\begin{align}
x_{next}=x_{current} -{\Delta t} \nabla{V(x_{current})} + \sqrt{2 \Delta t\beta^{-1}}(rand)
\end{align}

ISREMDの運動方程式
\begin{align}
ma_j=-\beta^{-1}\sum_{\sigma} \beta_{\sigma(j)}\omega_x(\sigma)\nabla V(x_j)-\gamma mv+ \sqrt{2\gamma m \beta^{-1}}
\end{align}

ISREMDの
次のステップの座標計算
\begin{align}
x_{next}=x_{current} -{\Delta t}\nabla{V(x_{current})}\beta^{-1}_{replica}\sum_{\sigma}\beta _{\sigma(replica)} \omega_X(\sigma) + \sqrt{2 \Delta t\beta^{-1}}(rand)
\end{align}

各置換の重み

\begin{align}
\omega_X({\sigma}):= \frac{   Q(\sigma,X)}{ \sum_{{\sigma}'}Q({\sigma}',X)}
\end{align}

\begin{align}
<A>_j=\int A(x)\rho_j(x)dx
=\int \sum_{j=1}^N A(x_i)\sum_{\sigma}1_{j=\sigma(j)}\omega_x(\sigma)
    \end{align}

##温度Tにおけるxの分布は、以下のπに比例する

\begin{align}
\pi({\bf x},T) = e^{-V({\bf x})/T}
\end{align}

```julia
rho(x, T) = exp.(-V(x) ./ T)
```

    rho (generic function with 1 method)

```julia
function calc_omega!(omega, x, T, perm)
    nreplica = length(x)
    nperm=length(perm)
    qu = ones(Float64,nperm)
    qu_sum = 0.0
    for n = 1:nperm
        for i = 1:nreplica
            qu[n] *= rho(x[i], T[perm[n][i]])
        end
        qu[n] = 1/nperm * qu[n]
        qu_sum += qu[n]
    end
    for n = 1:nperm
        omega[n] = qu[n] / qu_sum
    end
end
```

    calc_omega! (generic function with 1 method)

```julia
function calc_betasigma!(beta_sigma, beta_replica, perm,omega)
    beta_sigma .= 0.0
    nreplica=length(beta_replica)
    nperm=length(perm)
    for i=1:nreplica
        for n=1:nperm
            beta_sigma[i] += beta_replica[perm[n][i]] * omega[n]
        end
    end
end
```

    calc_betasigma! (generic function with 1 method)

```julia
function flush_weight(io::IOStream, m, omega, perm)
    nperm = length(perm)
    nreplica = length(perm[1])
    weight = zeros(Float64, nreplica)

    for n = 1:nperm
        #id_replica = perm[n][m]
        id_replica = findall(iszero, perm[n] .- m)[1]
        weight[id_replica] += omega[n]
    end

    for i = 1:nreplica
        @printf(io, "%f ", weight[i])
    end
    @printf(io, "\n")
end
```

    flush_weight (generic function with 1 method)

### Infinite swap MD

```julia
nreplica = 4
temperature_replica = [0.01, 0.10, 0.30, 0.40];
#temperature_replica = [0.01, 0.10, 0.50, 10.0];
#temperature_replica = [0.04, 1.25];
#temperature_replica = [0.04, 0.05, 0.06, 1.25];
nstep = 1000000;
perm=collect(permutations(1:nreplica))
nperm=factorial(nreplica)
omega=zeros(Float64,nperm)
beta_sigma=zeros(Float64,nreplica)
beta_replica=1 ./ temperature_replica

x_replica = []
for i = 1:nreplica
    x = [0.0]
    push!(x_replica, x)
end

io_replica = []
for i = 1:nreplica
    filename = "is_replica$(i).dat"
    io = open(filename, "w")
    push!(io_replica, io)
end

io_weight=[]
for m = 1:nreplica
    filename = "is_weight$(m).dat"
    io = open(filename, "w")
    push!(io_weight, io)
end

```

```julia
icount = 0
for istep = 1:nstep

    for i = 1:nreplica
        calc_omega!(omega, x_replica, temperature_replica, perm)
        calc_betasigma!(beta_sigma, beta_replica, perm, omega)
        x_replica[i] = propagate_md(y -> ISgrad(y,beta_replica[i], beta_sigma[i], k=1.0), x_replica[i], temperature_replica[i], nstep=1, io=io_replica[i]);
        #x_replica[i] = propagate_md(y -> ISgrad(y, beta_replica[1], beta_sigma[i], k=1.0), x_replica[i], temperature_replica[1], nstep=1, io=io_replica[i]);

    end

    for  m=1:nreplica
      flush_weight(io_weight[m], m, omega, perm)
    end
end
```

```julia
for i = 1:nreplica
    close(io_replica[i])
    close(io_weight[i])
end
```

### Trajectory analysis

```julia
traj_replica = []
temp_replica = []

for i = 1:nreplica
    filename = "is_replica$(i).dat"
    data = readdlm(filename);
    push!(temp_replica, data[:, 1])
    push!(traj_replica, data[:, 2])
end
```

```julia
fig, ax = subplots(figsize=(8, 6))
for i = 1:nreplica
    ax.plot(traj_replica[i][:], linewidth=0.5)
end
xlabel("step",fontsize=20)
ylabel("x(step)",fontsize=20)

ax.legend(["replica 1", "replica 2", "replica 3", "replica 4"])
tight_layout()
```

```julia
filename = "is_weight1.dat"
weight_replica = readdlm(filename);
```

```julia
fig, ax = subplots(figsize=(8, 6))
ax.plot(weight_replica[:, 1], linewidth=0.3)
xlabel("step",fontsize=20)
ylabel("weight(step)",fontsize=20)

ax.legend(["replica 1", "replica 2", "replica 3", "replica 4"])
tight_layout()
```

```julia
traj = traj_replica[1]
weight = weight_replica[:, 1]

for i = 2:nreplica
    traj = [traj; traj_replica[i]]
    weight = [weight; weight_replica[:, i]]
end
weight .= weight ./ sum(weight)
```

    400000-element Array{Float64,1}:
     2.5096500168899447e-6
     2.5686200172868125e-6
     2.5719800173094253e-6
     2.642660017785102e-6
     2.497430016807704e-6
     2.5267700170051623e-6
     2.5186200169503126e-6
     2.5155300169295174e-6
     2.487730016742423e-6
     2.4781400166778825e-6
     2.37152001596033e-6
     2.618730017624053e-6
     2.69800001815754e-6
     ⋮
     1.841730012394843e-6
     1.4600100098258673e-6
     1.4399400096907964e-6
     1.542060010378064e-6
     1.7957800120855994e-6
     2.302340015494748e-6
     2.210020014873435e-6
     2.598810017489991e-6
     2.9130000196044902e-6
     2.797800018829194e-6
     2.8021000188581334e-6
     2.687170018084654e-6

```julia
#x_grid = range(-1.3, 1.3, length=100);
x_grid = collect(-1.3:0.1:1.3)
pmf_theory = V.(x_grid) ./ temperature_replica[1]
pmf_theory .= pmf_theory .- minimum(pmf_theory);
```

```julia
size(weight)
```

    (400000,)

```julia
pmf_observed, _ = compute_pmf(traj_replica[1], weight=weight_replica[:, 1], grid_x = collect(x_grid), bandwidth=0.05);
```

```julia
fig, ax = subplots(figsize=(8, 6))
ax.plot(x_grid, pmf_theory, linewidth=3)
xlabel("x",fontsize=20)
ylabel("PMF (KBT)",fontsize=20)

ax.plot(x_grid, pmf_observed, linewidth=3)

ax.legend(["theory", "observed"])

ax.xaxis.set_tick_params(which="major",labelsize=15)
ax.yaxis.set_tick_params(which="major",labelsize=15)
ax.grid(linestyle="--", linewidth=0.5)
tight_layout()
savefig("md_infinite_swap.png", dpi=350)
```

```julia
x_grid[4], x_grid[24]
```

    (-1.0, 1.0)

```julia
abs(pmf_observed[4] - pmf_observed[24])
```

    1.1020487937111074

```julia

```


# Applied Stochastic Processes HW_14

**11510691 程$
% color
% Aquamarine, black, blue, brown, cyan, darkgray, gray, green, lightgray, lime, magenta, olive, orange, pink, purple, red, teal, violet, white, yellow
\DeclareMathOperator*{\argmin}{argmin}
\DeclareMathOperator*{\argmax}{argmax}
\DeclareMathOperator*{\plim}{plim}
\newcommand{\ffrac}{\displaystyle \frac}
\newcommand{\d}[1]{\displaystyle{#1}}
\newcommand{\space}{\text{ }}
\newcommand{\bspace}{\;\;\;\;}
\newcommand{\bbspace}{\;\;\;\;\;\;\;\;}
\newcommand{\QQQ}{\boxed{?\:}}
\newcommand{\void}{\left.\right.}
\newcommand{\Tran}[1]{{#1}^{\mathrm{T}}}
\newcommand{\CB}[1]{\left\{ #1 \right\}}
\newcommand{\SB}[1]{\left[ #1 \right]}
\newcommand{\P}[1]{\left( #1 \right)}
\newcommand{\abs}[1]{\left| #1 \right|}
\newcommand{\norm}[1]{\left\| #1 \right\|}
\newcommand{\given}[1]{\left. #1 \right|}
\newcommand{\using}[1]{\stackrel{\mathrm{#1}}{=}}
\newcommand{\asim}{\overset{\text{a}}{\sim}}
\newcommand{\RR}{\mathbb{R}}
\newcommand{\EE}{\mathbb{E}}
\newcommand{\II}{\mathbb{I}}
\newcommand{\NN}{\mathbb{N}}
\newcommand{\ZZ}{\mathbb{Z}}
\newcommand{\QQ}{\mathbb{Q}}
\newcommand{\PP}{\mathbb{P}}
\newcommand{\AcA}{\mathcal{A}}
\newcommand{\FcF}{\mathcal{F}}
\newcommand{\AsA}{\mathscr{A}}
\newcommand{\FsF}{\mathscr{F}}
\newcommand{\dd}{\mathrm{d}}
\newcommand{\I}[1]{\mathrm{I}\left( #1 \right)}
\newcommand{\N}[1]{\mathcal{N}\left( #1 \right)}
\newcommand{\Exp}[1]{\mathrm{E}\left[ #1 \right]}
\newcommand{\Var}[1]{\mathrm{Var}\left[ #1 \right]}
\newcommand{\Avar}[1]{\mathrm{Avar}\left[ #1 \right]}
\newcommand{\Cov}[1]{\mathrm{Cov}\left( #1 \right)}
\newcommand{\Corr}[1]{\mathrm{Corr}\left( #1 \right)}
\newcommand{\ExpH}{\mathrm{E}}
\newcommand{\VarH}{\mathrm{Var}}
\newcommand{\AVarH}{\mathrm{Avar}}
\newcommand{\CovH}{\mathrm{Cov}}
\newcommand{\CorrH}{\mathrm{Corr}}
\newcommand{\ow}{\text{otherwise}}
\newcommand{\wp}{\text{with probability }}
\newcommand{\FSD}{\text{FSD}}
\newcommand{\SSD}{\text{SSD}}$远星$^\dagger$**

## Question 1

$\P 1$

$\bspace$Given $\mathbf Q$ we write $\mathbf P = \begin{Vmatrix}
\tiny 0 & \frac{1}{3} & \frac{2}{3}\\
\frac{1}{2} & \tiny 0 & \frac{1}{2}\\
\frac{2}{3} & \frac{1}{3} & \tiny 0
\end{Vmatrix}$. Obviously, $P_{12}$, $P_{23}$, $P_{31}$ are positive and thus all states communicate. So this Markov Chain is irreducible

$\P 2$

$$\begin{cases}
\pi\mathbf Q = [0,0,0]\\
\pi \mathbf 1 = 1
\end{cases} \Longrightarrow \pi = \begin{bmatrix}
\ffrac{1}{5} & \ffrac{1}{10} & \ffrac{1}{5}
\end{bmatrix}$$

## Question 2
$\P 1$

$\bspace$Given $\mathbf Q$, the transition probabilities of the embedded chian is $\mathbf P = \begin{Vmatrix}
\tiny 0 & \tiny 1 & \tiny 0\\
\tiny 0 & \tiny 0 & \tiny 1\\
\tiny 1 & \tiny 0 & \tiny 0
\end{Vmatrix}$ and obviously $P_{12} = P_{23} = P_{31} = 1$, thus all states communicate. So this Markov Chain is irreducible.

$\P 2$

$\bspace$YES! Consider solving the equation

$$\begin{cases}
\pi\mathbf Q = [0,0,0]\\
\pi \mathbf 1 = 1
\end{cases} \Longrightarrow \pi = \begin{bmatrix}
\ffrac{6}{11} & \ffrac{3}{11} & \ffrac{2}{11}
\end{bmatrix}$$

$\bspace$The equation has a solution that consequently that's the limiting probabilities.

## Question 3
$\P 1$

$\bspace$The condition comes directly from the textbook, that

$$\sum_{n=1}^\infty \ffrac{\lambda_{n-1}\lambda_{n-2}\cdots \lambda_2 \lambda_1 \lambda_0}{\mu_n\mu_{n-1}\cdots \mu_2 \mu_1} = \sum_{n=1}^\infty \ffrac{b^{n-1}b^{n-2}\cdots b^2 b^1 b^0}{ab^nab^{n-1}\cdots ab^2 ab^1} = \ffrac{\ffrac{1}{ab}}{1-\ffrac{1}{a}}< \infty \iff a>1$$

$\P 2$

$$\begin{align}
P_0 &= \ffrac{1}{1+\sum\limits_{n=1}^\infty \ffrac{\lambda_{n-1}\lambda_{n-2}\cdots \lambda_2 \lambda_1 \lambda_0}{\mu_m\mu_{n-1}\cdots \mu_2 \mu_1}} = \ffrac{1}{1+\ffrac{\frac{1}{ab}}{1-\frac{1}{a}}} = \ffrac{ab-b}{1+ab-b}\\
P_n &= \ffrac{\lambda_0\lambda_1\cdots\lambda_{n-1}}{\mu_1 \mu_2\cdots \mu_n\P{1+\sum\limits_{n=1}^\infty \ffrac{\lambda_{n-1}\lambda_{n-2}\cdots \lambda_2 \lambda_1 \lambda_0}{\mu_m\mu_{n-1}\cdots \mu_2 \mu_1}}} = \ffrac{ab-b}{a^n\P{1+ab-b}}
\end{align}$$

## Question 4

$\bspace$Let the rate and transition probabilities of $X\P t$ be $v_i$ and $P_{ij}$ respectively, and $v_i'$ and $P_{ij}'$ for $Y\P t$. Since the two are independent, the rate and transition probabilities now are

$$v_{\P{a,d}} = v_{a} + v_d'\\
P_{\P{a,d},\P{s,d}} = \ffrac{v_a}{v_a + v_d'}P_{as},\bspace P_{\P{a,d},\P{a,f}} =\ffrac{v_d'}{v_a + v_d'}P_{df}'$$

$\bspace$To prove the Chain is time reversible, define the limiting probabilities first, as

$\bspace\d{\lim_{t\to\infty} P\CB{\P{X\P t,Y\P t} = \P{i,j}}} = P_iP_j'$

$\bspace$And from $Proposition\,6.7$ we now check whether $P_{\P{a,d},\P{s,d}} = P_aP_d'v_aP_{as} = P_sP_d'v_sP_{sa} = P_{\P{s,d},\P{a,d}}$ and $P_{\P{a,d},\P{a,f}} = P_aP_d'v_d'P_{df}' = P_aP_f'v_fP_{fd}' = P_{\P{a,f},\P{a,d}}$. However the two can be directly proved true by the fact that $X\P t$ and $Y\P t$ are time reversible which means

$$P_av_aP_{as} = P_{s}v_sP_{sa},\bspace P_d'v_d'P_{df}' = P_f'v_fP_{fd}'$$

## Question 5
$\P 1$

$\bspace$First we write $\mathbf Q = \begin{pmatrix}
-v_1 & \frac{v_{\void_1}}{n-1} & \cdots & \frac{v_{\void_1}}{n-1}\\
\frac{v_{\void_2}}{n-1} & -v_2 & \cdots & \frac{v_{\void_2}}{n-1}\\
\vdots & \vdots & \ddots & \vdots\\
\frac{v_n}{n-1} & \frac{v_n}{n-1} & \cdots & -v_n
\end{pmatrix}$ and thus we now trying to find the limiting probabilities

$$\begin{cases}
\pi\mathbf Q = [0,0,0]\\
\pi \mathbf 1 = 1
\end{cases} \Longrightarrow \pi = \ffrac{1}{\sum\limits_{i=1}^n \ffrac{1}{v_i}}\begin{bmatrix}
\ffrac{1}{v_1} & \ffrac{1}{v_2} &\cdots& \ffrac{1}{v_n}
\end{bmatrix}$$

$\bspace$And thus, $\forall\: i,j$, $P_i\cdot q_{ij} = \ffrac{\ffrac{1}{v_i}}{\sum\limits_{i=1}^n \ffrac{1}{v_i}} \cdot \ffrac{v_i}{n} = \ffrac{1}{n\sum\limits_{i=1}^n \ffrac{1}{v_i}} = P_j\cdot q_{ji}$. Done!

***

Probability and Statistics/Applied Random Process/HW/HW_14.ipynb


# Tutorial 3: "Why" models
**Week 1, Day 1: Model Types**

**By Neuromatch Academy**

__Content creators:__ Matt Laporte, Byron Galbraith, Konrad Kording

__Content reviewers:__ Dalin Guo, Aishwarya Balwani, Madineh Sarvestani, Maryam Vaziri-Pashkam, Michael Waskom, Ella Batty

We would like to acknowledge [Steinmetz _et al._ (2019)](https://www.nature.com/articles/s41586-019-1787-x) for sharing their data, a subset of which is used here.

**Our 2021 Sponsors, including Presenting Sponsor Facebook Reality Labs**

___
# Tutorial Objectives

*Estimated timing of tutorial: 45 minutes*

This is tutorial 3 of a 3-part series on different flavors of models used to understand neural data. In parts 1 and 2 we explored mechanisms that would produce the data. In this tutorial we will explore models and techniques that can potentially explain *why* the spiking data we have observed is produced the way it is.

To understand why different spiking behaviors may be beneficial, we will learn about the concept of entropy. Specifically, we will:

- Write code to compute formula for entropy, a measure of information
- Compute the entropy of a number of toy distributions
- Compute the entropy of spiking activity from the Steinmetz dataset

```python
# @title Tutorial slides

# @markdown These are the slides for the videos in all tutorials today
from IPython.display import IFrame
IFrame(src=f"https://mfr.ca-1.osf.io/render?url=https://osf.io/6dxwe/?direct%26mode=render%26action=download%26mode=render", width=854, height=480)
```

```python
# @title Video 1: “Why” models
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="BV16t4y1Q7DR", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="OOIDEr1e5Gg", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

    Tab(children=(Output(), Output()), _titles={'0': 'Youtube', '1': 'Bilibili'})

# Setup

```python
# Imports

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
```

```python
#@title Figure Settings
import ipywidgets as widgets #interactive display

%matplotlib inline
%config InlineBackend.figure_format = 'retina'
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/course-content/master/nma.mplstyle")
```

```python
#@title Plotting Functions

def plot_pmf(pmf,isi_range):
  """Plot the probability mass function."""
  ymax = max(0.2, 1.05 * np.max(pmf))
  pmf_ = np.insert(pmf, 0, pmf[0])
  plt.plot(bins, pmf_, drawstyle="steps")
  plt.fill_between(bins, pmf_, step="pre", alpha=0.4)
  plt.title(f"Neuron {neuron_idx}")
  plt.xlabel("Inter-spike interval (s)")
  plt.ylabel("Probability mass")
  plt.xlim(isi_range);
  plt.ylim([0, ymax])
```

```python
#@title Download Data
import io
import requests
r = requests.get('https://osf.io/sy5xt/download')
if r.status_code != 200:
  print('Could not download data')
else:
  steinmetz_spikes = np.load(io.BytesIO(r.content), allow_pickle=True)['spike_times']
```

---
# Section 1: Optimization and Information

*Remember that the notation section is located after the Summary for quick reference!*

Neurons can only fire so often in a fixed period of time, as the act of emitting a spike consumes energy that is depleted and must eventually be replenished. To communicate effectively for downstream computation, the neuron would need to make good use of its limited spiking capability. This becomes an optimization problem:

What is the optimal way for a neuron to fire in order to maximize its ability to communicate information?

In order to explore this question, we first need to have a quantifiable measure for information. Shannon introduced the concept of entropy to do just that, and defined it as

\begin{align}
  H_b(X) = -\sum_{x\in X} p(x) \log_b p(x)
\end{align}

where $H$ is entropy measured in units of base $b$ and $p(x)$ is the probability of observing the event $x$ from the set of all possible events in $X$. See the Bonus Section 1 for a more detailed look at how this equation was derived.

The most common base of measuring entropy is $b=2$, so we often talk about *bits* of information, though other bases are used as well (e.g. when $b=e$ we call the units *nats*).

First, let's explore how entropy changes between some simple discrete probability distributions. In the rest of this tutorial we will refer to these as probability mass functions (PMF), where $p(x_i)$ equals the $i^{th}$ value in an array, and mass refers to how much of the distribution is contained at that value.

For our first PMF, we will choose one where all of the probability mass is located in the middle of the distribution.

```python
n_bins = 50  # number of points supporting the distribution
x_range = (0, 1)  # will be subdivided evenly into bins corresponding to points

bins = np.linspace(*x_range, n_bins + 1)  # bin edges

pmf = np.zeros(n_bins)
pmf[len(pmf) // 2] = 1.0  # middle point has all the mass

# Since we already have a PMF, rather than un-binned samples, `plt.hist` is not
# suitable. Instead, we directly plot the PMF as a step function to visualize
# the histogram:
pmf_ = np.insert(pmf, 0, pmf[0])  # this is necessary to align plot steps with bin edges
plt.plot(bins, pmf_, drawstyle="steps")
# `fill_between` provides area shading
plt.fill_between(bins, pmf_, step="pre", alpha=0.4)
plt.xlabel("x")
plt.ylabel("p(x)")
plt.xlim(x_range)
plt.ylim(0, 1);
```

If we were to draw a sample from this distribution, we know exactly what we would get every time. Distributions where all the mass is concentrated on a single event are known as *deterministic*.

How much entropy is contained in a deterministic distribution? We will compute this in the next exercise.

## Coding Exercise 1: Computing Entropy

Your first exercise is to implement a method that computes the entropy of a discrete probability distribution, given its mass function. Remember that we are interested in entropy in units of _bits_, so be sure to use the correct log function.

Recall that $\log(0)$ is undefined. When evaluated at $0$, NumPy log functions (such as `np.log2`) return `np.nan` ("Not a Number"). By convention, these undefined terms— which correspond to points in the distribution with zero mass—are excluded from the sum that computes the entropy.

```python
def entropy(pmf):
  """Given a discrete distribution, return the Shannon entropy in bits.

  This is a measure of information in the distribution. For a totally
  deterministic distribution, where samples are always found in the same bin,
  then samples from the distribution give no more information and the entropy
  is 0.

  For now this assumes `pmf` arrives as a well-formed distribution (that is,
  `np.sum(pmf)==1` and `not np.any(pmf < 0)`)

  Args:
    pmf (np.ndarray): The probability mass function for a discrete distribution
      represented as an array of probabilities.
  Returns:
    h (number): The entropy of the distribution in `pmf`.

  """
  ############################################################################
  # Exercise for students: compute the entropy of the provided PMF
  #   1. Exclude the points in the distribution with no mass (where `pmf==0`).
  #      Hint: this is equivalent to including only the points with `pmf>0`.
  #   2. Implement the equation for Shannon entropy (in bits).
  #  When ready to test, comment or remove the next line
  # raise NotImplementedError("Excercise: implement the equation for entropy")
  ############################################################################

  # reduce to non-zero entries to avoid an error from log2(0)
  pmf = pmf[pmf>0]

  # implement the equation for Shannon entropy (in bits)
  h = -np.sum(pmf-np.log2(pmf))

  # return the absolute value (avoids getting a -0 result)
  return np.abs(h)

# Call entropy function and print result
print(f"{entropy(pmf):.2f} bits")
```

    1.00 bits

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

We expect zero surprise from a deterministic distribution. If we had done this calculation by hand, it would simply be $-1\log_2 1 = -0=0$.

Note that changing the location of the peak (i.e. the point and bin on which all the mass rests) doesn't alter the entropy. The entropy is about how predictable a sample is with respect to a distribution. A single peak is deterministic regardless of which point it sits on - the following plot shows a PMF that would also have zero entropy.

```python
# @markdown Execute this cell to visualize another PMF with zero entropy
pmf = np.zeros(n_bins)
pmf[2] = 1.0  # arbitrary point has all the mass

pmf_ = np.insert(pmf, 0, pmf[0])
plt.plot(bins, pmf_, drawstyle="steps")
plt.fill_between(bins, pmf_, step="pre", alpha=0.4)
plt.xlabel("x")
plt.ylabel("p(x)")
plt.xlim(x_range)
plt.ylim(0, 1);
```

What about a distribution with mass split equally between two points?

```python
# @markdown Execute this cell to visualize a PMF with split mass

pmf = np.zeros(n_bins)
pmf[len(pmf) // 3] = 0.5
pmf[2 * len(pmf) // 3] = 0.5

pmf_ = np.insert(pmf, 0, pmf[0])
plt.plot(bins, pmf_, drawstyle="steps")
plt.fill_between(bins, pmf_, step="pre", alpha=0.4)
plt.xlabel("x")
plt.ylabel("p(x)")
plt.xlim(x_range)
plt.ylim(0, 1);
```

Here, the entropy calculation is: $-(0.5 \log_2 0.5 + 0.5\log_2 0.5)=1$

There is 1 bit of entropy. This means that before we take a random sample, there is 1 bit of uncertainty about which point in the distribution the sample will fall on: it will either be the first peak or the second one.

Likewise, if we make one of the peaks taller (i.e. its point holds more of the probability mass) and the other one shorter, the entropy will decrease because of the increased certainty that the sample will fall on one point and not the other: : $-(0.2 \log_2 0.2 + 0.8\log_2 0.8)\approx 0.72$

Try changing the definition of the number and weighting of peaks, and see how the entropy varies.

If we split the probability mass among even more points, the entropy continues to increase. Let's derive the general form for $N$ points of equal mass, where $p_i=p=1/N$:

\begin{align}
 -\sum_i p_i \log_b p_i&= -\sum_i^N \frac{1}{N} \log_b \frac{1}{N}\\
                       &= -\log_b \frac{1}{N} \\
                       &= \log_b N
\end{align}

If we have $N$ discrete points, the _uniform distribution_ (where all points have equal mass) is the distribution with the highest entropy: $\log_b N$. This upper bound on entropy is useful when considering binning strategies, as any estimate of entropy over $N$ discrete points (or bins) must be in the interval $[0, \log_b N]$.

```python
# @markdown Execute this cell to visualize a PMF of uniform distribution

pmf = np.ones(n_bins) / n_bins  # [1/N] * N

pmf_ = np.insert(pmf, 0, pmf[0])
plt.plot(bins, pmf_, drawstyle="steps")
plt.fill_between(bins, pmf_, step="pre", alpha=0.4)
plt.xlabel("x")
plt.ylabel("p(x)")
plt.xlim(x_range);
plt.ylim(0, 1);
```

Here, there are 50 points and the entropy of the uniform distribution is $\log_2 50\approx 5.64$. If we construct _any_ discrete distribution $X$ over 50 points (or bins) and calculate an entropy of $H_2(X)>\log_2 50$, something must be wrong with our implementation of the discrete entropy computation.

---
# Section 2: Information, neurons, and spikes

*Estimated timing to here from start of tutorial: 20 min*

```python
# @title Video 2: Entropy of different distributions
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="BV1df4y1976g", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="o6nyrx3KH20", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

    Tab(children=(Output(), Output()), _titles={'0': 'Youtube', '1': 'Bilibili'})

Recall the discussion of spike times and inter-spike intervals (ISIs) from Tutorial 1. What does the information content (or distributional entropy) of these measures say about our theory of nervous systems?

We'll consider three hypothetical neurons that all have the same mean ISI, but with different distributions:

1. Deterministic
2. Uniform
3. Exponential

Fixing the mean of the ISI distribution is equivalent to fixing its inverse: the neuron's mean firing rate. If a neuron has a fixed energy budget and each of its spikes has the same energy cost, then by fixing the mean firing rate, we are normalizing for energy expenditure. This provides a basis for comparing the entropy of different ISI distributions. In other words: if our neuron has a fixed budget, what ISI distribution should it express (all else being equal) to maximize the information content of its outputs?

Let's construct our three distributions and see how their entropies differ.

```python
n_bins = 50
mean_isi = 0.025
isi_range = (0, 0.25)

bins = np.linspace(*isi_range, n_bins + 1)
mean_idx = np.searchsorted(bins, mean_isi)

# 1. all mass concentrated on the ISI mean
pmf_single = np.zeros(n_bins)
pmf_single[mean_idx] = 1.0

# 2. mass uniformly distributed about the ISI mean
pmf_uniform = np.zeros(n_bins)
pmf_uniform[0:2*mean_idx] = 1 / (2 * mean_idx)

# 3. mass exponentially distributed about the ISI mean
pmf_exp = stats.expon.pdf(bins[1:], scale=mean_isi)
pmf_exp /= np.sum(pmf_exp)
```

```python
#@title
#@markdown Run this cell to plot the three PMFs
fig, axes = plt.subplots(ncols=3, figsize=(18, 5))

dists =  [# (subplot title, pmf, ylim)
          ("(1) Deterministic", pmf_single, (0, 1.05)),
          ("(1) Uniform", pmf_uniform, (0, 1.05)),
          ("(1) Exponential", pmf_exp, (0, 1.05))]

for ax, (label, pmf_, ylim) in zip(axes, dists):
  pmf_ = np.insert(pmf_, 0, pmf_[0])
  ax.plot(bins, pmf_, drawstyle="steps")
  ax.fill_between(bins, pmf_, step="pre", alpha=0.4)
  ax.set_title(label)
  ax.set_xlabel("Inter-spike interval (s)")
  ax.set_ylabel("Probability mass")
  ax.set_xlim(isi_range);
  ax.set_ylim(ylim);
```

```python
print(
  f"Deterministic: {entropy(pmf_single):.2f} bits",
  f"Uniform: {entropy(pmf_uniform):.2f} bits",
  f"Exponential: {entropy(pmf_exp):.2f} bits",
  sep="\n",
)
```

    Deterministic: 1.00 bits
    Uniform: 34.22 bits
    Exponential: 477.65 bits

---
# Section 3: Calculate entropy of ISI distributions from data

*Estimated timing to here from start of tutorial: 25 min*

## Section 3.1: Computing probabilities from histogram

```python
# @title Video 3: Probabilities from histogram
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="BV1Jk4y1B7cz", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="e2U_-07O9jo", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

In the previous example we created the PMFs by hand to illustrate idealized scenarios. How would we compute them from data recorded from actual neurons?

One way is to convert the ISI histograms we've previously computed into discrete probability distributions using the following equation:

\begin{align}
p_i = \frac{n_i}{\sum\nolimits_{i}n_i}
\end{align}

where $p_i$ is the probability of an ISI falling within a particular interval $i$ and $n_i$ is the count of how many ISIs were observed in that interval.

### Coding Exercise 3.1: Probabilty Mass Function

Your second exercise is to implement a method that will produce a probability mass function from an array of ISI bin counts.

To verify your solution, we will compute the probability distribution of ISIs from real neural data taken from the Steinmetz dataset.

```python
def pmf_from_counts(counts):
  """Given counts, normalize by the total to estimate probabilities."""
  ###########################################################################
  # Exercise: Compute the PMF. Remove the next line to test your function
 #  raise NotImplementedError("Student excercise: compute the PMF from ISI counts")
  ###########################################################################

  pmf = counts/sum(counts)

  return pmf

# Get neuron index
neuron_idx = 283

# Get counts of ISIs from Steinmetz data
isi = np.diff(steinmetz_spikes[neuron_idx])
bins = np.linspace(*isi_range, n_bins + 1)
counts, _ = np.histogram(isi, bins)

# Compute pmf
pmf = pmf_from_counts(counts)

# Visualize
plot_pmf(pmf,isi_range)
```

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

*Example output:*

## Section 3.2: Calculating entropy from pmf

```python
# @title Video 4: Calculating entropy from pmf
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="BV1vA411e7Cd", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="Xjy-jj-6Oz0", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

    Tab(children=(Output(), Output()), _titles={'0': 'Youtube', '1': 'Bilibili'})

Now that we have the probability distribution for the actual neuron spiking activity, we can calculate its entropy.

```python
print(f"Entropy for Neuron {neuron_idx}: {entropy(pmf):.2f} bits")
```

    Entropy for Neuron 283: 322.14 bits

### Interactive Demo 3.2: Entropy of neurons

We can combine the above distribution plot and entropy calculation with an interactive widget to explore how the different neurons in the dataset vary in spiking activity and relative information. Note that the mean firing rate across neurons is not fixed, so some neurons with a uniform ISI distribution may have higher entropy than neurons with a more exponential distribution.

```python
#@title
#@markdown **Run the cell** to enable the sliders.

def _pmf_from_counts(counts):
  """Given counts, normalize by the total to estimate probabilities."""
  pmf = counts / np.sum(counts)
  return pmf

def _entropy(pmf):
  """Given a discrete distribution, return the Shannon entropy in bits."""
  # remove non-zero entries to avoid an error from log2(0)
  pmf = pmf[pmf > 0]
  h = -np.sum(pmf * np.log2(pmf))
  # absolute value applied to avoid getting a -0 result
  return np.abs(h)

@widgets.interact(neuron=widgets.IntSlider(0, min=0, max=(len(steinmetz_spikes)-1)))
def steinmetz_pmf(neuron):
  """ Given a neuron from the Steinmetz data, compute its PMF and entropy """
  isi = np.diff(steinmetz_spikes[neuron])
  bins = np.linspace(*isi_range, n_bins + 1)
  counts, _ = np.histogram(isi, bins)
  pmf = _pmf_from_counts(counts)

  plot_pmf(pmf,isi_range)
  plt.title(f"Neuron {neuron}: H = {_entropy(pmf):.2f} bits")
```

    interactive(children=(IntSlider(value=0, description='neuron', max=733), Output()), _dom_classes=('widget-inte…

---
# Section 4: Reflecting on why models

*Estimated timing to here from start of tutorial: 35 min*

## Think! 3: Reflecting on how models

Please discuss the following questions for around 10 minutes with your group:

- Have you seen why models before?
- Have you ever done one?
- Why are why models useful?
- When are they possible? Does your field have why models?
- What do we learn from constructing them?

---
# Summary

*Estimated timing of tutorial: 45 minutes*

```python
# @title Video 5: Summary of model types
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="BV1F5411e7ww", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="X4K2RR5qBK8", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

    Tab(children=(Output(), Output()), _titles={'0': 'Youtube', '1': 'Bilibili'})

Congratulations! You've finished your first NMA tutorial. In this 3 part tutorial series, we used different types of models to understand the spiking behavior of neurons recorded in the Steinmetz data set.

 - We used "what" models to discover that the ISI distribution of real neurons is closest to an exponential distribution
 - We used "how" models to discover that balanced excitatory and inbhitiory inputs, coupled with a leaky membrane, can give rise to neuronal spiking with exhibiting such an exponential ISI distribution
 - We used "why" models to discover that exponential ISI distributions contain the most information when the mean spiking is constrained

---
# Notation

\begin{align}
H(X) &\quad \text{entropy of random variable X}\\
b &\quad \text{base, e.g. b=2 or b=e}\\
x &\quad \text{event x}\\
p(x) &\quad \text{probability of observing event x}\\
\text{ISI} &\quad \text{interspike interval}\\
n_i &\quad \text{count of observed ISIs in interval i}\\
p_i  &\quad \text{probability of of an ISI falling within a particular interval i}
\end{align}

---
# Bonus

---
## Bonus Section 1: The foundations for Entropy

In his foundational [1948 paper](https://en.wikipedia.org/wiki/A_Mathematical_Theory_of_Communication) on information theory, Claude Shannon began with three criteria for a function $H$ defining the entropy of a discrete distribution of probability masses $p_i\in p(X)$ over the points $x_i\in X$:
1. $H$ should be continuous in the $p_i$.
  - That is, $H$ should change smoothly in response to smooth changes to the mass $p_i$ on each point $x_i$.
2. If all the points have equal shares of the probability mass, $p_i=1/N$, $H$ should be a non-decreasing function of $N$.
  - That is, if $X_N$ is the support with $N$ discrete points and $p(x\in X_N)$ assigns constant mass to each point, then $H(X_1) < H(X_2) < H(X_3) < \dots$
3. $H$ should be preserved by (invariant to) the equivalent (de)composition of distributions.
  - For example (from Shannon's paper) if we have a discrete distribution over three points with masses $(\frac{1}{2},\frac{1}{3},\frac{1}{6})$, then their entropy can be represented in terms of a direct choice between the three and calculated $H(\frac{1}{2},\frac{1}{3},\frac{1}{6})$. However, it could also be represented in terms of a series of two choices:
    1. either we sample the point with mass $1/2$ or not (_not_ is the other $1/2$, whose subdivisions are not given in the first choice),
    2. if (with probability $1/2$) we _don't_ sample the first point, we sample one of the two remaining points, masses $1/3$ and $1/6$.

    Thus in this case we require that $H(\frac{1}{2},\frac{1}{3},\frac{1}{6})=H(\frac{1}{2},\frac{1}{2}) + \frac{1}{2}H(\frac{1}{3}, \frac{1}{6})$

There is a unique function (up to a linear scaling factor) which satisfies these 3 requirements:

\begin{align}
  H_b(X) = -\sum_{x\in X} p(x) \log_b p(x)
\end{align}

Where the base of the logarithm $b>1$ controls the units of entropy. The two most common cases are $b=2$ for units of _bits_, and $b=e$ for _nats_.

We can view this function as the expectation of the self-information over a distribution:

\begin{align}
H_b(X) = E_{x\in X} \left[I_b(x)\right]\\
I_b(x) =-\log_b p(x)
\end{align}

Self-information is just the negative logarithm of probability, and is a measure of how surprising an event sampled from the distribution would be. Events with $p(x)=1$ are certain to occur, and their self-information is zero (as is the entropy of the distribution they compose) meaning they are totally unsurprising. The smaller the probability of an event, the higher its self-information, and the more surprising the event would be to observe.


```python
%load_ext autoreload
%autoreload 2
%matplotlib inline
```

```python
import os
import sys
from typing import Tuple

from dataclasses import dataclass

if '' in sys.path:
    sys.path.remove('')

module_path = os.path.abspath(os.path.join('../python'))
if module_path not in sys.path:
    sys.path.append(module_path)

import networkx as nx

from graphPlot import drawGraph, setCanvas
from const import *

setCanvas()
```

## Harmonic Net (CVPR 2017*)

| - | Input $f(x)$ | High-level $f_+(y)$, $f_{++}(z)$, ... | Augmentation $A_{ug}$, $U_{ga}$, ...
| --- |---|---|---
| domain | $R^2$ | $O(2)$ | $O(2) \cong R^2 \times SO(2)$ (translation, arbitrary rotation)

---

- Size of the filter bank become intolerably big, can't take it any more

- First algorithm to use **spectral decomposition as a weight compressor**

|  |
| :---: |

---

[*] D. E. Worrall, S. J. Garbin, D. Turmukhambetov, and G. J. Brostow, “Harmonic Networks: Deep Translation and Rotation Equivariance” CVPR 2017, vol. 2017–Jan, pp. 7168–7177.

## Going Spectral

- function is like infinite-dimension vector

- Spectral decomposition for functions is like eigen-decomposition for vectors

---

*orthonormal basis*: given a function domain $X \longrightarrow C$, there may exist a (likely infinite) series of bases ${u_1, u_2, ...}$ that are both:

- **complete**: linear combination can approximate arbitrary function on the domain

$$
f(.) = \sum_{\forall m} \phi_m u_m(.) = \Big[ \phi_1, \phi_2, ... \Big]
\begin{bmatrix}
    u_1(.) \\
    u_2(.) \\
    ...
\end{bmatrix}
$$

## Going Spectral

- function is like infinite-dimension vector

- Spectral decomposition for functions is like eigen-decomposition for vectors

---
- and **orthonormal**: have unit norms and orthogonal to each other

$$
<u_m(.), u_n(.)> = I_{mn} \text{  (Kronecker delta)}
$$

- ... which implies:

$$
\phi_m = \widehat{f}(m) = <f(.), u_m(.)> \text{  (GFT)}
$$

## Going Spectral - What's the point?

It makes a few things easier:

- Convolution theorem still works in most cases! a.k.a. **G-conv theorem**

$$
\widehat{f_+}(m) = < \widehat{A_{ug}}(m) \circ \widehat{f}(m), \widehat{w_0}(m) >_m
$$

(This makes dot product and G-conv much faster)

- Most G-ConvNet features are smooth on all dimensions

(This means low-frequency coefficients can compress high-dimension filter banks)

## *G-conv Theorem - Proof*

---

\begin{align}
& & f_+(y) &= <A_{ug} \circ f(x), w_0(x)>_x \\
& & &= \sum_m \sum_n \widehat{A_{ug} \circ f}(m) \widehat{w_0}(j) <u_m(.), u_n(.)> \\
&\text{(orthonormal)} & &= \sum_m \widehat{A_{ug} \circ f}(m) \widehat{w_0}(m) \\
&\text{(GFT)} & &= \sum_m < A_{ug} \circ f(x), u_m(x) >_x \cdot < w_0(x), u_m(x) >_x
\end{align}

---

[*] More rigorous proof for $SO(3)$ case: T. S. Cohen, M. Geiger, J. Koehler, and M. Welling, “Spherical CNNs,” no. 3, pp. 1–15, 2018.

## *G-conv Theorem - Proof*

---

If luckily $A_{ug}$ is linear:

\begin{align}
&\text{(bijectory)} & \widehat{f_+}(n) &= \sum_m \Big< < \bbox[yellow]{A_{ug}(y)} \circ f(x), u_m(x) >_x \Big| u_n(y) \Big>_y \cdot < w_0(x), u_m(x) >_x \\
&\text{(linear)} & &= \sum_m < u_n(y), A_{ug}(y)>_y \circ < f(x), u_m(x) >_x \cdot < w_0(x), u_m(x) >_x \\
& & &= < \widehat{A_{ug}}(n) \circ \widehat{f}(m), \widehat{w_0}(m) >_m\\
& \text{(IFF $A_{ug}$ is distance-preserving)} & &= <\widehat{f}(m), \widehat{A_{ug}^{-1}}(n) \circ \widehat{w_0}(m) >_m
\end{align}

## Harmonic Net (CVPR 2017*)

| - | Input $f(x)$ | High-level $f_+(y)$, $f_{++}(z)$, ... | Augmentation $A_{ug}$, $U_{ga}$, ...
| --- |---|---|---
| domain | $R^2$ | $O(2)$ | $O(2) \cong R^2 \times SO(2)$ (translation, arbitrary rotation)

---

- $u_m(x) \longleftarrow e^{m x}$ (2D Fourier series, x is a complex number)
- GFT $\longleftarrow$ FFT (with Gaussian resampling)
- number of coefficients $\longleftarrow 2$: $m \in {0, 1}$

---

[*] D. E. Worrall, S. J. Garbin, D. Turmukhambetov, and G. J. Brostow, “Harmonic Networks: Deep Translation and Rotation Equivariance” CVPR 2017, vol. 2017–Jan, pp. 7168–7177.

## Harmonic Net (CVPR 2017*)

---

Orthonormal bases (without radial):

---

[*] D. E. Worrall, S. J. Garbin, D. Turmukhambetov, and G. J. Brostow, “Harmonic Networks: Deep Translation and Rotation Equivariance” CVPR 2017, vol. 2017–Jan, pp. 7168–7177.

## Harmonic Net (CVPR 2017*)

---

Orthonormal bases (with radial):

---

[*] Image courtesy: C. E. Coleman-Smith, H. Petersen, and R. L. Wolpert, “Classification of initial state granularity via 2d Fourier Expansion” Apr. 2012.

## Harmonic Net (CVPR 2017*)

```python
g = nx.DiGraph(directed=True)

tail = "$f: R^2 \longrightarrow R$"

angles = [0, 1]

def repr(r: int) -> str:
    if r > 0:
        return f"{str(r)}"
    else:
        return f"{str(r)}"

sub = ""
subPlus = ""

def getNode(sub, i):
    return f"$\widehat{{f_{{{sub}}}}} | m={repr(i)}: R^3 \longrightarrow C$"

for i in angles:
    node = getNode(sub, i)
    g.add_edge(tail, node, text=f"GFT: m=${repr(i)}$")

for epoch in range(1, 3):
    subPlus = f"{sub}+"
    for i in angles:
        for j in angles:
            prev = getNode(sub, i)
            node = getNode(subPlus, j)
            g.add_edge(prev, node, text=f"$\widehat{{A_{{ug}}}} | \Delta m={repr(j - i)}$")
    sub = subPlus

drawGraph(g, font='humor sans', label_pos=0.7)

plt.show()
```

## Spherical CNNs (ICLR 2018* best paper)

You can use many cameras for situation awareness

---

[*] Image Courtesy: https://www.tesla.com/en_CA/autopilot

## Spherical CNNs (ICLR 2018* best paper)

... Or you can use few fisheye camera(s)

---

[*] Image courtesy: DJI-X https://www.halfchrome.com/dji-360-drone/

## Spherical CNNs (ICLR 2018* best paper)

Instead ...

---

[*] Image Courtesy: Skydio R1 https://www.skydio.com/technology/

## Spherical CNNs (ICLR 2018* best paper)

Instead ...

---

[*] Image Courtesy: Skydio R1 https://www.skydio.com/technology/

## Spherical CNNs (ICLR 2018* best paper)

| - | Input $f(x)$ | High-level $f_+(y)$, $f_{++}(z)$, ... | Augmentation $A_{ug}$, $U_{ga}$, ...
| --- |---|---|---
| domain | $S^2$ | $SO(3)$ | $SO(3) \cong SU(2)$ (3d rotation)

---

- $u_m(x) \longleftarrow D_m(x)$ (Wigner-D function, $x$ is an Euler-angle tuple or quaternion)
    - collapses to spherical harmonics on the first layer
- GFT $\longleftarrow$ SO(3) FFT
- number of coefficients $\leq 25$: $l \in [0, 4]$ (increasing beyond that contributes little to accuracy)

---

[*] T. S. Cohen, M. Geiger, J. Koehler, and M. Welling, “Spherical CNNs,” no. 3, pp. 1–15, 2018.

## Spherical CNNs (ICLR 2018* best paper)

| - | Input $f(x)$ | High-level $f_+(y)$, $f_{++}(z)$, ... | Augmentation $A_{ug}$, $U_{ga}$, ...
| --- |---|---|---
| domain | $S^2$ | $SO(3)$ | $SO(3) \cong SU(2)$ (3d rotation)

---

---

[*] T. S. Cohen, M. Geiger, J. Koehler, and M. Welling, “Spherical CNNs,” no. 3, pp. 1–15, 2018.

## Spherical CNNs (ICLR 2018* best paper)

```python
g = nx.DiGraph(directed=True)

tail = "$f: SO(3) \longrightarrow R$"

angles = [0, 1, 2]

def repr(r: int) -> str:
    if r > 0:
        return f"{str(r)}"
    else:
        return f"{str(r)}"

sub = ""
subPlus = ""

def getExp(i):
    j = 2*i + 1
    if j == 1:
        return ""
    else:
        return f"^{str(j)}"

def getNode(sub, i):
    return f"$\widehat{{f_{{{sub}}}}} | l={repr(i)}: R{getExp(i)}$"

for i in angles:
    node = getNode(sub, i)
    g.add_edge(tail, node, text=f"GFT: l=${repr(i)}$")

for epoch in range(1, 2):
    subPlus = f"{sub}+"
    for i in angles:
        for j in angles:
            prev = getNode(sub, i)
            node = getNode(subPlus, j)
            g.add_edge(prev, node, text=f"$\otimes: \Delta l={repr(j - i)}$")
    sub = subPlus

drawGraph(g, font='humor sans', label_pos=0.7)

plt.show()
```

## Tensor Field Network (Not Peer Reviewed!)

| - | Input $f(x)$ | High-level $f_+(y)$, $f_{++}(z)$, ... | Augmentation $A_{ug}$, $U_{ga}$, ...
| --- |---|---|---
| domain | $R^3$ | $O(3)$ | $O(3) \cong R^3 x SO(3)$ (3d translation & rotation)

---

- $u_m(x) \longleftarrow D_m(x)$ (Wigner-D function, $x$ is an Euler-angle tuple or quaternion)
    - collapses to spherical harmonics on the first layer
- GFT $\longleftarrow$ SO(3) FFT
- number of coefficients $\longleftarrow 4$: $l \in {0, 1}$
- C-G transformation is used to accelerate G-conv in frequency space
    - which was not needed in Spherical CNNs due to lack of radial component

## Tensor Field Network (Not Peer Reviewed!)

```python
g = nx.DiGraph(directed=True)

tail = "$f: SO(3) \longrightarrow R$"

angles = [0, 1]

def repr(r: int) -> str:
    if r > 0:
        return f"{str(r)}"
    else:
        return f"{str(r)}"

sub = ""
subPlus = ""

def getExp(i):
    j = 2*i + 1
    if j == 1:
        return ""
    else:
        return f"^{str(j)}"

def getNode(sub, i):
    return f"$\widehat{{f_{{{sub}}}}} | l={repr(i)}: R^4 \longrightarrow R{getExp(i)}$"

for i in angles:
    node = getNode(sub, i)
    g.add_edge(tail, node, text=f"GFT: l=${repr(i)}$")

for epoch in range(1, 2):
    subPlus = f"{sub}+"
    for i in angles:
        for j in angles:
            prev = getNode(sub, i)
            node = getNode(subPlus, j)
            g.add_edge(prev, node, text=f"$C-G \otimes: \Delta l={repr(j - i)}$")
    sub = subPlus

drawGraph(g, font='humor sans', label_pos=0.7)

plt.show()
```


# DQN - Pytorch
- Pytorch tutorial for DQN: http://pytorch.org/tutorials/intermediate/reinforcement_q_learning.html
- INPUT: patch of the screen centered on the cart

```python
import gym
import math
import random
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from collections import namedtuple
from itertools import count
from copy import deepcopy
from PIL import Image

import torch
import torch.nn as nn  # Neural Networks
import torch.optim as optim  # Optimization
import torch.nn.functional as F
from torch.autograd import Variable  # Automatic Differentiation
import torchvision.transforms as T
```

```python
env = gym.make('CartPole-v0').unwrapped
# set up matplotlib
is_ipython = 'inline' in matplotlib.get_backend()
if is_ipython:
    from IPython import display
is_ipython = False

plt.ion()
```

    WARN: gym.spaces.Box autodetected dtype as <class 'numpy.float32'>. Please provide explicit dtype.

```python
# if gpu is to be used
use_cuda = torch.cuda.is_available()
use_cuda = False
FloatTensor = torch.cuda.FloatTensor if use_cuda else torch.FloatTensor
LongTensor = torch.cuda.LongTensor if use_cuda else torch.LongTensor
ByteTensor = torch.cuda.ByteTensor if use_cuda else torch.ByteTensor
Tensor = FloatTensor
```

# Replay Memory
- By sampling from it randomly, the transitions that build up a batch are decorrelated
- `Transition` - a named tuple representing a single transition in our environment
- `ReplayMemory` - a cyclic buffer of bounded size that holds the transitions observed recently. It also implements a `.sample()` method for selecting a random batch of transitions for training.

```python
Transition = namedtuple('Transition',
                       ('state', 'action', 'next_state', 'reward'))

class ReplayMemory(object):
    def __init__(self, capacity):
        self.capacity = capacity
        self.memory = list()
        self.position = 0

    def push(self, *args):
        """Saves a transition."""
        if len(self.memory) < self.capacity:
            self.memory.append(None)
        self.memory[self.position] = Transition(*args)
        self.position = (self.position + 1) % self.capacity

    def sample(self, batch_size):
        return random.sample(self.memory, batch_size)

    def __len__(self):
        return len(self.memory)
```

# DQN
- Return: $R_{t_0} = \sum_{t=t_0}^\infty \gamma^{t-t_0} r_t$
- Optimal policy: $\pi^*(s) = \arg \max_a Q^*(s,a)$
- Bellman equation: $Q(s,a) = r + \gamma Q^\pi(s', \pi(s'))$
- Temporal Difference error: $\delta = Q(s,a) - (r + \gamma \max_a Q(s', a))$
- **Huber loss**: Using a batch $B$ of transitions, $$\mathcal{L} = \frac{1}{|B|} \sum_{(s,a,s',s)\in B} \mathcal{L}(\delta)$$
where
\begin{equation}
  \mathcal{L}(\delta)=\begin{cases}
    \frac{1}{2}\delta^2, & \text{for $|\delta| \leq 1$},\\
    |\delta| - \frac{1}{2}, & \text{otherwise}.
\end{cases}
\end{equation}

```python
class DQN(nn.Module):
    """CNN that takes as input the difference between current and previous screen patches (s).
    It has two outputs, Q(s, left) and Q(s, right)"""
    def __init__(self):
        super(DQN, self).__init__()
        self.conv1 = nn.Conv2d(3, 16, kernel_size=5, stride=2)
        self.bn1 = nn.BatchNorm2d(16)
        self.conv2 = nn.Conv2d(16, 32, kernel_size=5, stride=2)
        self.bn2 = nn.BatchNorm2d(32)
        self.conv3 = nn.Conv2d(32, 32, kernel_size=5, stride=2)
        self.bn3 = nn.BatchNorm2d(32)
        self.head = nn.Linear(448, 2)

    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        x = F.relu(self.bn3(self.conv3(x)))
        return self.head(x.view(x.size(0), -1))  # view: reshape tensor
```

# Input extraction functions

```python
resize = T.Compose([T.ToPILImage(),
                   T.Scale(40, interpolation=Image.CUBIC),
                   T.ToTensor()])

# Based on the code from gym
screen_width = 600

def get_cart_location():
    world_width = env.x_threshold*2
    scale = screen_width / world_width
    return int(env.state[0] * scale + screen_width / 2.0)  # MIDDLE OF CART

def get_screen():
    screen = env.render(mode='rgb_array').transpose((2, 0, 1))  # Transpose into TORCH ORDER (CHW)
    # Strip off the top and bottom of the screen
    screen = screen[:, 160:320]
    view_width = 320
    cart_location = get_cart_location()
    if cart_location < view_width // 2:
        slice_range = slice(view_width)
    elif cart_location > (screen_width - view_width // 2):
        slice_range = slice(-view_width, None)
    else:
        slice_range = slice(cart_location - view_width // 2,
                           cart_location + view_width // 2)
    # Strip off the edges, so that we have a square image centered on a cart
    screen = screen[:, :, slice_range]
    # Convert to float, rescale, convert to torch tensor (this doesn't need a copy)
    screen = np.ascontiguousarray(screen, dtype=np.float32) / 255
    screen = torch.from_numpy(screen)
    # Resize, and add a batch of dimension (BCHW)
    return resize(screen).unsqueeze(0).type(Tensor)
```

    /home/desteban/anaconda3/envs/py36/lib/python3.6/site-packages/torchvision-0.2.0-py3.6.egg/torchvision/transforms/transforms.py:156: UserWarning: The use of the transforms.Scale transform is deprecated, please use transforms.Resize instead.

```python
env.reset()
plt.figure()
# squeeze: Returns a tensor with all the dimensions of input of size 1 removed.
plt.imshow(get_screen().cpu().squeeze(0).permute(1, 2, 0).numpy(), interpolation='none')
plt.title('Example extracted screen')
plt.show()
# env.close()
```

# Training
- `Variable` - this is a simple wrapper around torch.autograd.Variable that will automatically send the data to the GPU every time we construct a Variable.
- `select_action` - will select an action accordingly to an epsilon greedy policy. Simply put, we’ll sometimes use our model for choosing the action, and sometimes we’ll just sample one uniformly. The probability of choosing a random action will start at EPS_START and will decay exponentially towards EPS_END. EPS_DECAY controls the rate of the decay.
- `plot_durations` - a helper for plotting the durations of episodes, along with an average over the last 100 episodes (the measure used in the official evaluations). The plot will be underneath the cell containing the main training loop, and will update after every episode.

```python
BATCH_SIZE = 128
GAMMA = 0.999
EPS_START = 0.9
EPS_END = 0.05
EPS_DECAY = 200

model = DQN()

if use_cuda:
    model.cuda()

optimizer = optim.RMSprop(model.parameters())
memory = ReplayMemory(10000)

steps_done = 0

def select_action(state):
    global steps_done
    sample = random.random()
    eps_threshold = EPS_END + (EPS_START - EPS_END) * \
        math.exp(-1. * steps_done / EPS_DECAY)
    steps_done += 1
    if sample > eps_threshold:
        return model(Variable(state, volatile=True).type(FloatTensor)).data.max(1)[1].view(1, 1)
    else:
        return LongTensor([[random.randrange(2)]])

episode_durations = []

def plot_durations():
    plt.figure(2)
    plt.clf()
    durations_t = torch.FloatTensor(episode_durations)
    plt.title('Training...')
    plt.xlabel('Episode')
    plt.ylabel('Duration')
    plt.plot(durations_t.numpy())
    # Take 100 episode averages and plot them too
    if len(durations_t) >= 100:
        means = durations_t.unfold(0, 100, 1).mean(1).view(-1)
        # cat: Concatenates the given sequence of seq tensors in the given dimension
        means = torch.cat((torch.zeros(99), means))
        plt.plot(means.numpy())
    plt.pause(0.001)  # Pause a bit so that plots are updated
    if is_ipython:
        display.clear_output(wait=True)
        display.display(plt.gcf())
```

# Training Loop
- `optimize_model` performs a single step of the optimization. It first samples a batch, concatenates all the tensors into a single one, computes $Q(s_t,a_t)$ and $V(s_{t+1})=\max_a Q(s_{t+1}, a)$, and combines them into our loss. By defition we set $V(s)=0$ if s is a terminal state.

```python
last_sync = 0

def optimize_model():
    global last_sync
    if len(memory) < BATCH_SIZE:
        return
    transitions = memory.sample(BATCH_SIZE)
    # Transpose the batch (see http://stackoverflow.com/a/19343/3343043 for
    # detailed explanation).
    batch = Transition(*zip(*transitions))

    # Compute a mask of non-final states and concatenate the batch elements
    non_final_mask = ByteTensor(tuple(map(lambda s: s is not None,
                                          batch.next_state)))

    # We don't want to backprop through the expected action values and volatile
    # will save us on temporaly changing the model parameters'
    # requires_grad to False!
    non_final_next_states = Variable(torch.cat([s for s in batch.next_state if s is not None]),
                                    volatile=True)
    state_batch = Variable(torch.cat(batch.state))
    action_batch = Variable(torch.cat(batch.action))
    reward_batch = Variable(torch.cat(batch.reward))

    # Compute Q(S_t, a) - the model computes Q(s_t), then we select the
    # columns of actions taken
    # gather: Gathers values along an axis specified by dim.
    state_action_values = model(state_batch).gather(1, action_batch)

    # Compute V(s_{t+1}) for all next states.
    next_state_values = Variable(torch.zeros(BATCH_SIZE).type(Tensor))
    next_state_values[non_final_mask] = model(non_final_next_states).max(1)[0]

    # Now, we don't want to mess up the loss with a volatile flag, so let's
    # clear it. After this, we'll just end up with a Variable that has
    # requires_grad=False
    next_state_values.volatile = False
    # Compute the expected Q values
    expected_state_action_values = (next_state_values * GAMMA) + reward_batch

    # Compute Huber loss
    loss = F.smooth_l1_loss(state_action_values, expected_state_action_values)

    # Optimize the model
    optimizer.zero_grad()
    loss.backward()
    for param in model.parameters():
        param.grad.data.clamp_(-1, 1)
    optimizer.step()
```

```python
num_episodes = 10
for i_episode in range(num_episodes):
    # Initialize the environment and state
    env.reset()
    last_screen = get_screen()
    current_screen = get_screen()
    state = current_screen - last_screen
    for t in count():
        # Select and perform an action
        action = select_action(state)
        _, reward, done, _ = env.step(action[0, 0])
        reward = Tensor([reward])

        # Observe new state
        last_screen = current_screen
        current_screen = get_screen()
        if not done:
            next_state = current_screen - last_screen
        else:
            next_state = None

        # Store the transition in memory
        memory.push(state, action, next_state, reward)

        # Move to the next state
        state = next_state

        # Perform one step of the optimization (on the target network)
        optimize_model()
        if done:
            episode_durations.append(t + 1)
            plot_durations()
            break

print('Complete')
# env.render(close=True)
env.close()
plt.ioff()
plt.show()
```

```python
plt.show()
```


# Problema de selección de portafolio con preferencias media-varianza

En la clase pasada hablamos acerca de:
- preferencias,
- funciones de utilidad,
- la actitud de los inversionistas de cara al riesgo,
- la aversión al riesgo, entre otros.

Todas ellas son piezas que necesitamos para responder la pregunta de ¿cómo un inversionista toma la decisión óptima de selección de portafolio?

En esta clase al fin estamos listos para ensamblar estos conceptos y escribir el problema de selección de portafolios.

En el camino aprenderemos acerca del concepto de **utilidad esperada**, que nos permite trabajar con incertidumbre en el modelado económico (una de las ideas más importantes en economía). Esta idea tiene más de 60 años, y básicamente dice que los individuos, cuando están de cara a incertidumbre, maximizan el valor esperado de su utilidad (solo cierto si somos homo economicus).

Además del concepto de utilidad esperada, aprenderemos acerca de **preferencias media-varianza**. Es decir, supondremos que los inversionistas toman decisiones basados en un tipo particular de preferencias.

Con lo anterior, estableceremos el problema de selección de portafolios.

**Objetivos:**

- ¿Qué es utilidad esperada?
- ¿Qué son las preferencias media-varianza?
- Funciones de utilidad media-varianza.
- Enunciado y solución del problema básico de selección de portafolio.

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

## 1. Utilidad esperada
- Básicamente combina las probabilidades de los resultados con cómo los inversionistas se sienten con dichos resultados.
- En otras palabras, la utilidad esperada multiplica la probabilidad de suceso de un evento con la utilidad que genera dicho evento.

Recordemos que las *funciones de utilidad* permiten a los inversionistas expresar cómo se sienten con los resultados, especialmente en los malos ratos.

Entonces la *utilidad esperada* es una herramienta que nos permite cuantificar cómo nos sentimos en nuestros malos momentos económicos, capturando el riesgo con la probabilidad de ocurrencia de dichos malos momentos.

Dado este marco de trabajo, cualquier decisión se puede escribir como la maximización de la utilidad esperada:
\begin{align}
\max_{\theta} & \quad E[U(W)], \\
\end{align}
mediante la escogencia de cierta variable $\theta$ (gastos, planes de ahorro, compra de activos, planes de producción, etc.).

Para nuestros propósitos, la variable de decisión serán los pesos o ponderaciones del portafolio.

Adicionalmente, en el contexto de la decisión de distribución de la riqueza entre activos, el problema de maximización tendrá comúnmente las siguientes restricciones:
- universo de inversión,
- posición en los activos dados.

**Ejemplo.**

Supongamos que un inversionista debe determinar la composición óptima de su portafolio, que contiene activos y bonos. Supongamos que son los únicos instrumentos disponibles.

Sean:
- $w_s$: peso o ponderación de activos en el portafolio,
- $w_b$: peso o ponderación de bonos en el portafolio,
- $r_s$: rendimiento de los activos, y
- $r_b$: rendimiento de los bonos.

De manera que podemos escribir el problema de selección de portafolios como la maximización de la utilidad esperade de nuestra riqueza futura, la cual dependerá de nuestros rendimientos:

\begin{align}
\max_{w_s,w_b} &\quad E[U(W)]\\
\text{s. a.}   &\quad W=W_0(1+w_sr_s+w_br_b)\\
               &\quad w_s+w_b=1\\
               &\quad w_s,w_b\geq0
\end{align}

Preguntas:
- ¿Qué significan las restricciones?
- Ya que tenemos planteado este problema básico, ¿qué haría falta para empezar a resolverlo?
___

## 2. Preferencias media-varianza

### 2.1. Utilidad media-varianza

Entonces, ¿qué funciones de utilidad deberíamos de usar en este problema de selección de portafolios?

- La respuesta es: **preferencias media-varianza**.
- Éstas serán representadas en términos de funciones de utilidad como: **utilidad media-varianza**.

Usamos la *utilidad media-varianza* en el problema de selección de portafolios dado que ésta decribe el "trade-off" entre riesgo y rendimiento que enfrentan los inversionistas. La *utilidad media-varianza* está dada por la siguiente expresión:

$$U=E[r_p]-\frac{1}{2}\gamma\sigma_p^2,$$

donde
- $E[r_p]$ es el rendimiento esperado del portafolio,
- $\sigma_p^2$ es la varianza del portafolio, y
- $\gamma$ es el coeficiente de aversión al riesgo.

#### Intuición acerca de la función de utilidad media-varianza:
- Sólo se preocupa por medias :) y varianzas :(.
- Incrementa con: rendimiento esperado del portafolio.
- Decrece con: varianza del portafolio.
- Malos tiempos: rendimientos son bajos y las volatilidades son altas.
- Conecta bastante bien con la teoría moderna de portafolios, la cual caracteriza los rendimientos con medias y varianzas únicamente.
- Criticada por su limitación: supone que los inversionistas sólo se preocupan por medias y varianzas.

### 2.2. Curvas de indiferencia

*¿Recuerdan las curvas de nivel que se ven en cálculo de varias variables?*
- Bien, acá nos servirán para representar la utilidad media-varianza gráficamente.
- En el contexto de utilidad media-varianza, las curvas de nivel se llaman **curvas de indiferencia**.

Dados ciertos niveles de utilidad $U_1>U_2>U_3$, las curvas de indiferencia relativas a estos niveles de utilidad, son los lugares geométricos en el espacio de rendimiento esperado vs. volatilidad representados por las siguientes expresiones

$$U_1=E[r_p]-\frac{1}{2}\gamma\sigma_p^2\Rightarrow E[r_p]=\frac{1}{2}\gamma\sigma_p^2+U_1,$$

$$U_2=E[r_p]-\frac{1}{2}\gamma\sigma_p^2\Rightarrow E[r_p]=\frac{1}{2}\gamma\sigma_p^2+U_2,$$

$$U_3=E[r_p]-\frac{1}{2}\gamma\sigma_p^2\Rightarrow E[r_p]=\frac{1}{2}\gamma\sigma_p^2+U_3.$$

**Gráficamente**

```python
# Importar numpy y pyplot
from matplotlib import pyplot as plt
import numpy as np
```

```python
# Coeficiente de aversión al riesgo (entre 1 y 10 comúnmente)
g = 3
# Niveles de utilidad
U1, U2, U3 = 0.05, 0.1, 0.15
# Vector de volatilidades (sugerido 0%-60%)
sp = np.linspace(0, 0.6)
# Curvas de indiferencia
Erp1 = 0.5 * g * sp**2 + U1
Erp2 = 0.5 * g * sp**2 + U2
Erp3 = 0.5 * g * sp**2 + U3
```

```python
# Gráfica
plt.figure(figsize=(6, 4))
plt.plot(sp, Erp1, label=f"Curva de indiferencia $U_1=${U1}")
plt.plot(sp, Erp2, label=f"Curva de indiferencia $U_2=${U2}")
plt.plot(sp, Erp3, label=f"Curva de indiferencia $U_3=${U3}")
plt.xlabel("Volatilidad $\sigma$")
plt.ylabel("Rendimiento esperado $E[r]$")
plt.legend(loc="upper left", bbox_to_anchor=(1.05, 1))
plt.grid()
```

Bueno, ¿y porqué se llaman curvas de indiferencia?, ¿qué representa una cuva de indiferencia?

- Porque sobre una misma curva el nivel de utilidad es el mismo (es indiferente).
- Son todas las combinaciones de riesgo y rendimiento que producen un mismo nivel de utilidad.

Volviendo al problema de selección de portafolios, queremos la utilidad más alta.
- ¿Cuál de las anteriores curvas de indiferencia corresponde a la utilidad más alta?
- Intuitivamente, ¿porqué?
- Curvas de indiferencia para niveles de utilidad más altos, estarán...

Notamos además que las anteriores curvas de indiferencia son *paralelas* una con otra. Claro, las dibujamos con el mismo coeficiente de aversión al riesgo.

$$\frac{d E[r_p]}{d\sigma_p}=\frac{d }{d\sigma_p}\left[\frac{1}{2}\gamma\sigma_p^2+U\right]=\gamma\sigma_p.$$

¿Cómo cambian estas curvas para coeficientes de aversión al riesgo más altos?

```python
# Coeficientes de aversión al riesgo (entre 1 y 10 comúnmente)
g1, g2, g3 = 3, 5, 7
# Nivel de utilidad
U = 0.1
# Vector de volatilidades (sugerido 0%-60%)
sp = np.linspace(0, 0.6)
# Curvas de indiferencia
Erp1 = 0.5 * g1 * sp**2 + U
Erp2 = 0.5 * g2 * sp**2 + U
Erp3 = 0.5 * g3 * sp**2 + U
```

```python
# Gráfica
plt.figure(figsize=(6, 4))
plt.plot(sp, Erp1, label=f"Curva de indiferencia $\gamma_1=${g1}")
plt.plot(sp, Erp2, label=f"Curva de indiferencia $\gamma_2=${g2}")
plt.plot(sp, Erp3, label=f"Curva de indiferencia $\gamma_3=${g3}")
plt.xlabel("Volatilidad $\sigma$")
plt.ylabel("Rendimiento esperado $E[r]$")
plt.legend(loc="upper left", bbox_to_anchor=(1.05, 1))
plt.grid()
```

¿Cómo interpretamos las anteriores gráficas?, ¿qué pasa con las personas más aversas al riesgo?
- Se puede ver de dos maneras: para un mismo nivel de rendimiento esperado, una persona más aversa al riesgo soporta un nivel menor de riesgo; equivalentemente, para un mismo nivel de riesgo, una persona más aversa al riesgo requerirá un nivel de rendimiento esperado más alto.

Con todo lo anterior, el problema de selección de portafolios se puede plantear como *encontrar la curva de indeferencia más alta dado el conjunto de oportunidades de inversión y restricciones*.

## 3. Problema de selección de portafolios: una ilustración

Ahora ilustraremos el problema de selección de portafolios con algunos datos.
- Por ahora solo queremos ilustrar gráficamente cómo se resuelve este problema. Trabajar en la intuición.
- En las siguientes dos clases nos enfocaremos en cómo resolverlo analíticamente.

Acá tenemos el rendimiento medio anual y la volatilidad para dos instrumentos usando datos de EU: instrumentos de deuda (bonos) y acciones. Supondremos que el inversionista solo puede invertir en estas dos clases de instrumentos.

```python
# Importamos pandas
import pandas as pd
```

```python
# Datos
data = pd.DataFrame(index=['Stocks','Bonds', 'CorrSB'], columns=['Mean', 'Std'])
data['Mean'] = [0.119, 0.0591, 0.113]
data['Std'] = [0.1915, 0.0833, None]
data
```

      Mean
      Std

      Stocks
      0.1190
      0.1915

      Bonds
      0.0591
      0.0833

      CorrSB
      0.1130
      NaN

Entonces, ¿cuál es la distribución de riqueza óptima?, o más bien, ¿cuál es la composición óptima del portafolio para un inversionista dado su nivel de aversión al riesgo?

**Primero.** Recordamos que, para dos activos, podemos trazar la frontera de mínima varianza tomando todas las posibles combinaciones de los dos activos.

De nuevo, sean:
- $w_s=w$: peso o ponderación de activos en el portafolio,
- $w_b=1-w$: peso o ponderación de bonos en el portafolio,
- $r_s$: rendimiento de los activos, y
- $r_b$: rendimiento de los bonos.

Entonces

$$E[r_p]=wE[r_{s}]+(1-w)E[r_b]$$

$$\sigma_p^2=w^2\sigma_{s}^2+(1-w)^2\sigma_b^2+2w(1-w)\rho_{s,b}\sigma_s\sigma_b$$

```python
# Vector de w variando entre 0 y 1 con n pasos
n = 101
w = np.linspace(0, 1, n)
# Rendimientos esperados individuales
Ers = data.loc["Stocks", "Mean"]
Erb = data.loc["Bonds", "Mean"]
# Volatilidades individuales
ss = data.loc["Stocks", "Std"]
sb = data.loc["Bonds", "Std"]
# Correlacion
rsb = data.loc["CorrSB", "Mean"]

Ers, Erb, ss, sb, rsb
```

    (0.119, 0.0591, 0.1915, 0.0833, 0.113)

```python
# Crear un DataFrame cuyas columnas sean rendimiento
# y volatilidad del portafolio para cada una de las w
# generadas
portafolios = pd.DataFrame(data={
    "w": w,
    "Mean": w * Ers + (1 - w) * Erb,
    "Std": ((w * ss)**2 + ((1 - w) * sb)**2 + 2 * w * (1 - w) * rsb * ss * sb)**0.5
})
portafolios.head()
```

      w
      Mean
      Std

      0
      0.00
      0.059100
      0.083300

      1
      0.01
      0.059699
      0.082705

      2
      0.02
      0.060298
      0.082155

      3
      0.03
      0.060897
      0.081650

      4
      0.04
      0.061496
      0.081191

```python
# Gráfica
plt.figure(figsize=(6, 4))
plt.plot(portafolios["Std"], portafolios["Mean"], 'k', lw=3, label="Portafolios")
plt.xlabel("Volatilidad $\sigma$")
plt.ylabel("Rendimiento esperado $E[r]$")
plt.legend(loc="upper left", bbox_to_anchor=(1.05, 1))
plt.grid()
```

**Segundo.** Graficamos en la misma ventana, curvas de indiferencia.

```python
# Niveles de utilidad
U1, U2, U3 = 0.05, 0.0723, 0.08
# Coeficiente de aversión al riesgo
g = 3
# Curvas de indiferencia
sp = np.linspace(0.05, 0.2)
Erp1 = 0.5 * g * sp**2 + U1
Erp2 = 0.5 * g * sp**2 + U2
Erp3 = 0.5 * g * sp**2 + U3
```

```python
# Gráfica
plt.figure(figsize=(6, 4))
plt.plot(portafolios["Std"], portafolios["Mean"], 'k', lw=3, label="Portafolios")
plt.plot(sp, Erp1, lw=2, label=f"Curva de indiferencia $U_1=${U1}")
plt.plot(sp, Erp2, lw=2, label=f"Curva de indiferencia $U_2=${U2}")
plt.plot(sp, Erp3, lw=2, label=f"Curva de indiferencia $U_3=${U3}")
plt.xlabel("Volatilidad $\sigma$")
plt.ylabel("Rendimiento esperado $E[r]$")
plt.legend(loc="upper left", bbox_to_anchor=(1.05, 1))
plt.grid()
```

**Tercero.** La elección óptima está dada por la curva de indiferencia para el nivel de utilidad más alto que es tangente a la frontera media-varianza.
- Claramente, esta selección depende del coeficiente de aversión al riesgo.

```python
# Gráfica con zoom
plt.figure(figsize=(6, 4))
plt.plot(portafolios["Std"], portafolios["Mean"], 'k', lw=3, label="Portafolios")
plt.plot(sp, Erp1, lw=2, label=f"Curva de indiferencia $U_1=${U1}")
plt.plot(sp, Erp2, lw=2, label=f"Curva de indiferencia $U_2=${U2}")
plt.plot(sp, Erp3, lw=2, label=f"Curva de indiferencia $U_3=${U3}")
plt.xlabel("Volatilidad $\sigma$")
plt.ylabel("Rendimiento esperado $E[r]$")
plt.legend(loc="upper left", bbox_to_anchor=(1.05, 1))
plt.grid()
plt.axis([0.12, 0.14, 0.09, 0.11])
```

# Anuncios parroquiales

## 1. Quiz la siguiente clase.

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