```python
import numpy as np
import pandas as pd
import arviz as az
import scipy.stats as stats
from matplotlib import pyplot as plt
from ipywidgets import interact
import ipywidgets as ipyw
```

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

## Objetivos de este capítulo

* Comprender el teorema de Bayes
* Comprender que implica hacer estadística Bayesiana
* Aprender cómo se interpretan las probabilidades en estadística Bayesiana
* Entender las distribuciones a priori, a posteriori y a posteriori predictiva

# El universo Bayesiano

> Probability theory is nothing but common sense reduced to calculation. -Pierre-Simon Laplace

En este curso aprenderemos sobre una forma de hacer estadística llamada usualmente estadística Bayesiana. El nombre se debe a Thomas Bayes (1702-1761) un ministro presbiteriano, y matemático aficionado, quien derivó por primera vez lo que ahora conocemos como el **teorema de Bayes**, el cual fue publicado (postumanente) en 1763. Sin embargo una de las primeras personas en realmente desarrollar métodos Bayesianos, fue Pierre-Simon Laplace (1749-1827), por lo que tal vez sería un poco más correcto hablar de _Estadística Laplaciana_ y no Bayesiana.

Existe otro paradigma estadístico llamado estadística clásica o frecuentista. Si ustedes han tenido un curso de estadística (ya sea en el grado o posgrado) es casi seguro que dicho curso fue sobre métodos frecuentistas (aun cuando esto no haya sido explicitado). Es interesante notar que mientras los orígenes de las estadística Bayesiana se remontan al siglo XVIII. Los métodos "_clásicos_" (o frecuentistas) fueron desarrollados principalmente durante el siglo XX! De hecho una de las motivaciones para desarrollar métodos frecuentistas fue un _sentimiento_ e ideología anti-bayesiano. A lo largo del curso nos centraremos en los métodos Bayesianos.

Hay dos ideas centrales que hacen que un método sea Bayesiano:

* Toda cantidad desconocida es modelada utilizando una distribución de probabilidad de algún tipo.
* El teorema de Bayes es usado para actualizar dicha distribución a la luz de los datos.

En el universo Bayesiano las cantidades conocidas son consideradas fijas y usualmente les llamamos **datos**. Por el contrario toda cantidad desconocida es considerada como una variable aleatoria y modelada usando una distribución de probabilidad.

## Teorema de Bayes

El teorema de Bayes es una consecuencia directa de la regla del producto, veamos.

$$
p(\theta, y) = p(\theta \mid y) p(y) \\
p(\theta, y) = p(y \mid \theta) p(\theta)
$$
Dado que los dos términos a la derecha de la igualdad son iguales entre si podemos escribir que:

$$
p(\theta \mid y) p(y) = p(y \mid \theta) p(\theta)
$$

Reordenando llegamos al Teorema de Bayes!

$$
p(\theta \mid y) = \frac{p(y \mid \theta) p(\theta)}{p(y)}
$$

El cual también suele ser escrito de la siguiente forma:

$$
p(\theta \mid y) = \frac{p(y \mid \theta) p(\theta)}{\int_{\theta} p(y \mid \theta) p(\theta) \text{d}\theta}
$$

El denominador es una integral sobre todos los valores de $\theta$ definidos por  el _a priori_. La integral es reemplazada por una sumatoria en el caso que estemos hablando de valores discretos y no continuos.

Cada término del teorema de Bayes tiene un nombre específico:

* $p(\theta \mid y)$: _a posteriori_
* $p(y \mid \theta)$: _likelihood_ (_verosimilitud_)
* $p(\theta)$: _a priori_
* $p(y)$: _likelihood marginal_

El **_a priori_** es la forma de introducir conocimiento previo sobre los valores que pueden tomar los parámetros. A veces cuando no sabemos demasiado se suelen usar _a prioris_ que asignan igual probabilidad a todos los valores de los parámetros, otras veces se puede elegir _a prioris_ que restrijan los valores de los parámetros a rangos razonables, algo que se conoce como regularización, por ejemplo solo valores positivos. Muchas veces contamos con información mucho más precisa como medidas experimentales previas o límites impuesto por alguna teoría.

El **_likelihood_** es la forma de incluir nuestros datos en el análisis. Es una expresión matemática que especifica la plausibilidad de los datos. El _likelihood_ es central tanto en estadística Bayesiana como en estadística no-Bayesiana. A medida que la cantidad de datos aumenta el _likelihood_ tiene cada vez más peso en los resultados, esto explica el porqué a veces los resultados de la estadística Bayesiana y frecuentista coinciden cuando la muestra es _grande_.

El **_a posteriori_** es la distribución de probabilidad para los parámetros. Es la consecuencia lógica de haber usado un conjunto de datos, un _likelihood_ y un _a priori_. Se lo suele pensar como la versión actualizada del _a priori_. De hecho un _a posteriori_ puede ser un _a priori_ de un análisis a futuro.

La **_likelihood marginal_** (también llamado _evidencia_) es la probabilidad de observar los datos $y$ promediado sobre todas los posibles hipótesis (o conjunto de parámetros) $\theta$. Si la oración anterior no es muy clara, no hay problema ya veremos ejemplos que harán más claro este concepto. En general, la _evidencia_ puede ser vista como una simple constante de normalización que en la mayoría de los problemas prácticos puede (y suele) omitirse sin perdida de generalidad. Por lo que el teorema de Bayes suele aparecer escrito como:

$$
p(\theta \mid y) \propto p(y \mid \theta) p(\theta)
$$

El rol de todos estos términos irá quedando más claro a medida que avancemos.

## El _a posteriori_ como único estimador

El _a posteriori_ representa todo lo que sabemos de un problema, dado un modelo y un conjunto de datos. Y por lo tanto todas las inferencias estadísticas pueden deducirse a partir de él. Tipicamente esto toma la forma de integrales como la siguiente.

$$
J = \int \varphi(\theta) \ \ p(\theta \mid y) d\theta
$$

Por ejemplo, para calcular la media de $\theta$ deberíamos reemplazar $\varphi(\theta)$, por $\theta$:

$$
\bar \theta = \int \theta \ \ p(\theta \mid y) d\theta
$$

Esto no es más que la definición de un promedio pesado, donde cada valor de $\theta$ es _pesado_ según la probabilidad asignada por el _a posteriori_.

En la práctica y al resolver problemas computacionales, estas integrales se convierten en simples sumatorias.

## Estadística Bayesiana en tres pasos

El teorema de Bayes es el único estimador usado en estadística Bayesiana. Por lo que conceptualmente la estadística Bayesiana resulta muy simple. Según George Box y [Andrew Gelman et al. (2013)](https://www.crcpress.com/Bayesian-Data-Analysis-Third-Edition/Gelman-Carlin-Stern-Dunson-Vehtari-Rubin/p/book/9781439840955) la estadística Bayesiana se reduce a tres pasos:

1. **Crear un modelo probabilístico**. Los modelos probabilísticos son _historias_ que dan cuenta de como se generan los datos observados (o por observar). Los modelos se expresan usando distribuciones de probabilidad.

2. **Condicionar el modelo a los datos observados a fin de obtener el _a posteriori_**. Usando el teorema de Bayes se actualizan las probabilidades asignadas _a priori_ de acuerdo a los datos observados obteniéndose las probabilidades _a posteriori_.

3. **Criticar el ajuste del modelo generado a los datos y evaluar las consecuencias del modelo**. Se puede demostrar que dada la información previa y los datos observados no existe otro mecanismo capaz de generar una _mejor_ inferencia que la estadística Bayesiana. Esto parece maravilloso, pero hay un problema, solo es cierto si se asumen que los datos y el modelo son correctos. En la práctica, los datos pueden contener errores y los modelos son _a duras penas_ aproximaciones de fenómenos _reales_. Por lo tanto es necesario realizar varias evaluaciones, incluyendo si las predicciones generadas por el modelo se ajustan a los datos observados, si las conclusiones obtenidas tienen sentido dado el marco conceptual en el que uno trabaja, la sensibilidad de los resultados a los _detalles_ del modelo (sobre todo a detalles para los cuales no tenemos demasiada información), etc.

# Inferencia Bayesiana

En la práctica la mayoría de los modelos tendrán más de un parámetro, pero al usar software como PyMC3 modelar 1 o 1000 parámetros es más o menos lo mismo. Sin embargo, esos modelos pueden distraernos de los conceptos esenciales, por lo que considero importante comenzar por el caso más sencillo.

## El problema del diagnóstico

Veamos nuestro primer problema:

* En una población una de cada mil personas tiene una enfermedad.
* Una persona tomada al azar de esa población da positivo en un test para dicha enfermedad.
* El test da positivo el 99 % de las veces que una persona tiene la enfermedad.
* Dicho test tiene además una taza de falsos positivos del 5 %.

¿Cuál es la probabilidad de que dicha persona tenga la enfermedad? Antes de seguir leyendo tomate un tiempo y contesta a esta pregunta de forma intuitiva.

A continuación se ve cómo se aplica el teorema de Bayes para dar respuesta a esta pregunta.

$$p(\theta \mid y) = \frac{p(y \mid \theta) p(\theta)}{p(y)}$$

$$p(\theta \mid y) = \frac{p(y \mid \theta) p(\theta)}{p(y \mid \theta)p(\theta) + p(y \mid \theta^c)p(\theta^c)}$$

$$p(\theta \mid y) = \frac{0.99 \times 0.001}{0.99 \times 0.001 + 0.05 \times (1−0.001)}$$

$$p(\theta \mid y) = \frac{0.00099}{0.00099 + 0.04995}$$

$$p(\theta \mid y) \approx 0.02 \approx 2\%$$

Si es la primer vez que te encontrás con este problema, las chances son altas de que hayas dado como respuesta un número mucho más alto y cercano a 99%. La mayoría de las personas se dan cuenta que dado que la taza de falsos positivos es del 5% la respuesta tiene que ser menor de 99%, pero fallan en incluir la información a priori, solo 1 de cada mil personas tiene la enfermedad. La importancia del a priori queda claro si uno considera dos casos extremos:

* Toda la población tiene la enfermedad, en este caso no interesan las propiedades del test, la probabilidad de que el paciente tenga la enfermedad es 1.
* En el otro extremo estamos seguros que nadie en la población tiene la enfermedad, en este caso tampoco importa lo que dé el test, la respuesta siempre será 0.

Ésto nos muestra dos cosas, el a priori no puede ser dejado de lado y en general no es buena idea asignar una probabilidad de 1 o 0 a un priori, ya que sin importar lo que nos digan los datos jamás cambiaremos de parecer.

```python
p_apriori_enfermo = 0.001
p_pos_enfermo = 0.99
p_pos_sano = 0.05

(p_pos_enfermo * p_apriori_enfermo) / \
(p_pos_enfermo * p_apriori_enfermo + p_pos_sano * (1-p_apriori_enfermo))
```

    0.019434628975265017

En el ejemplo anterior el a priori es un número (0.001) en estadística Bayesiana esto equivale a decir que tenemos certeza absoluta sobre el valor del a priori, vale exactamente 0.001 ni un poco más ni un poco menos. Este nivel de precisión es imposible para cualquier problema real, a lo sumo podemos encontrar casos donde el grado de error con el que conocemos una cantidad es muy pequeño en comparación con otros términos de nuestro problema y por lo tanto aproximamos esa cantidad como una constante, aproximación que será válida si no tiene efectos prácticos en nuestras conclusiones.

El ejemplo anterior es un caso sencillo donde la aplicación del teorema de Bayes nos lleva a la respuesta correcta, y donde no hace falta ningún grado de sofisticación matemática para aplicarlo. Pero este ejemplo es apenas un precalentamiento que no hace justicia a la Estadística Bayesiana. Un ejemplo que logra capturar mucho mejor las ideas centrales en estadística Bayesiana es el que veremos a continuación y usaremos el resto de este capítulo y todo el próximo.

## El problema de la moneda

A juzgar por la cantidad de ejemplos sobre monedas arrojadas al aires en libros de estadística y probabilidad, pareciera que las monedas son uno de los objetos de estudio centrales de estas disciplinas.

Una de las razones detrás de la ubiquidad de este ejemplo es que las monedas son objetos familiares que facilitan discutir conceptos que de otra forma podrían sonar demasiado abstractos. De todas formas quizá la razón más importante sea que el problema puede ser modelado de forma simple y que muchos problemas _reales_ son conceptualmente similares, de hecho cualquier problema en donde obtengamos resultados binarios (0/1, enfermo/sano, spam/no-spam, etc) puede ser pensado como si estuviéramos hablando de monedas. En definitiva el modelo que veremos a continuación (ejemplificado con monedas) sirve para cualquier situación en la cual los datos observados solo pueden tomar dos valores mutuamente excluyentes. Debido a que estos valores son nominales y son dos, a este modelo se le llama binomial.

En el siguiente ejemplo trataremos de determinar el grado en que una moneda está sesgada. En general cuando se habla de sesgo se hace referencia a la desviación de algún valor (por ejemplo, igual proporción de caras y cecas), pero aquí usaremos el termino _sesgo_ de forma más general. Diremos que el sesgo es un valor en el intervalo [0, 1], siendo 0 para una moneda que siempre cae ceca y 1 para una moneda que siempre cae cara y lo representaremos con la variable $\theta$. A fin de cuantificar $\theta$ arrojaremos una moneda al aire repetidas veces, por practicidad arrojaremos la moneda de forma computacional (¡pero nada nos impide hacerlo manualmente!). Llevaremos registro del resultado en la variable $y$. Siendo $y$ la cantidad de caras obtenidas en un experimento.

Habiendo definido nuestro problema debemos expresarlo en términos del teorema de Bayes,

$$
p(\theta \mid y) \propto p(y \mid  \theta) p(\theta)
$$

Donde, como dijimos $\theta = 1$ quiere decir 100% cara y $\theta = 0$ 100% ceca.

Ahora solo restar reemplazar los dos términos a la derecha de la igualdad, el _a priori_ y el _likelihood_,  por distribuciones de probabilidad _adecuadas_ y luego multiplicarlas para obtener el término a la izquierda, el _a posteriori_. Como es la primera vez que haremos ésto, lo haremos paso a paso y analíticamente. En el próximo capítulo veremos cómo hacerlo computacionalmente.

#### Definiendo el _a priori_

El _a priori_ lo modelaremos usando una distribución beta, que es una distribución muy usada en estadística Bayesiana. La $pdf$ de esta distribución es:

$$
p(\theta)= \frac{\Gamma(\alpha+\beta)}{\Gamma(\alpha)\Gamma(\beta)}\, \theta^{\alpha-1}(1-\theta)^{\beta-1}
$$

El primer término es una constante de normalización. Por suerte para nuestro problema nos basta con establecer una proporcionalidad, por lo que podemos simplificar esta expresión y escribir la distribución beta de la siguiente forma.

$$
p(\theta) \propto  \theta^{\alpha-1}(1-\theta)^{\beta-1}
$$

Hay varias razones para usar una distribución beta para este y otros problemas:

* La distribución beta varía entre 0 y 1, de igual forma que lo hace $\theta$ en nuestro modelo.
* Esta distribución combinada con la que elegiremos como _likelihood_ (ver más adelante), nos permitirá resolver el problema de forma analítica.
* Es una distribución versátil para expresar distintas situaciones.

Respecto al último punto, veamos un ejemplo. Supongamos que el experimento de la moneda es realizado por tres personas. Una de ellas dice no saber nada de la moneda por lo tanto _a priori_ todos los valores de $\theta$ son igualmente probables. La segunda persona desconfía de la moneda, ya que sospecha que es una moneda trucada, por lo tanto considera que está sesgada, pero no sabe si hacia cara o hacia ceca. Por último, la tercer persona asegura que lo más probable es que $\theta$ tome un valor alrededor de 0.5 ya que según su experiencia así es como se comportan las monedas. Todas estas situaciones pueden ser modeladas por la distribución beta, como se ve a continuación.

```python
plt.figure(figsize=(10, 3))
x = np.linspace(0, 1, 100)

for ind, (a, b) in enumerate([(1, 1), (0.5, 0.5), (20, 20)]):
    y = stats.beta.pdf(x, a, b)
    plt.subplot(1, 3, ind+1)
    plt.plot(x, y, label='a = %s\nb = %s' % (a, b))
    plt.legend(fontsize=12)
plt.tight_layout();
```

```python
def beta(a, b):
    x = np.linspace(0, 1, 130)
    beta = stats.beta(a, b)
    plt.plot(x, beta.pdf(x))
    plt.yticks([])
    plt.ylim(0, 6)

interact(beta,
         a=ipyw.FloatSlider(min=0.5, max=7, step=0.5, value=2),
         b=ipyw.FloatSlider(min=0.5, max=7, step=0.5, value=2));
```

    interactive(children=(FloatSlider(value=2.0, description='a', max=7.0, min=0.5, step=0.5), FloatSlider(value=2…

####  Definiendo el _likelihood_

Habiendo definido el _a priori_ veamos ahora el likelihood. Asumiendo que el resultado obtenido al arrojar una moneda no influye en el resultado de posteriores experimentos (es decir los experimentos son independientes entre sí) es razonable utilizar como likelihood la distribución binomial.

$$
p(y \mid \theta) = \frac{N!}{y!(N-y)!} \theta^y (1 - \theta)^{N−y}
$$

Donde N es la cantidad total de experimentos (monedas arrojadas al aire) e $y$ es la cantidad de caras obtenidas. A los fines prácticos podríamos simplificar la igualdad anterior y convertirla en una proporcionalidad, eliminando el término $\frac{N!}{y!(N-y)!}$ ya que ese término no depende de $\theta$ que es lo que nos interesa averiguar. Por lo que podríamos establecer que:

$$
p(y \mid \theta) \propto \theta^y (1 - \theta)^{N−y}
$$

La elección de esta distribución para modelar nuestro problema es razonable ya que $\theta$ es la chance de obtener una cara al arrojar una moneda y ese hecho ha ocurrido $y$ veces, de la misma forma $1-\theta$ es la chance de obtener ceca lo cual ha sido observado $N-y$ veces.

```python
def binomial(n, θ):
    bino = stats.binom(n, θ)
    plt.bar(range(n+1), bino.pmf(range(n+1)))
    plt.xticks(range(n+1))
    plt.ylim(0, 1);

interact(binomial, n=ipyw.IntSlider(min=1, max=10, value=1), θ=ipyw.FloatSlider(min=0, max=1, step=0.05, value=0.5));
```

    interactive(children=(IntSlider(value=1, description='n', max=10, min=1), FloatSlider(value=0.5, description='…

#### Obteniendo el _a posteriori_

Se puede demostrar que siempre que usemos como _a priori_ una función beta y como _likelihood_ una distribución binomial obtendremos como resultado un _a posteriori_ que será nuevamente una distribución beta con los siguientes parámetros:

$$
p(\theta \mid y) \propto \operatorname{Beta}(\alpha_{a priori} + y, \beta_{a priori} + N - y)
$$

Veamos de donde surge este resultado, según el teorema de Bayes el _a posteriori_ es el producto del _likelihood_ y el _a priori_.

$$
p(\theta \mid y) \propto p(y \mid \theta) p(\theta)
$$

Por lo tanto, en nuestro caso tendremos que:

$$
p(\theta \mid y) \propto \theta^y (1 - \theta)^{N−y} \theta^{\alpha-1}(1-\theta)^{\beta-1}
$$

Reordenando, obtenemos que el _a posteriori_ es:

$$
p(\theta \mid y) \propto \theta^{\alpha-1+y}(1-\theta)^{\beta-1+N−y}
$$

Esto es una distribución Beta (sin considerar la constante de normalización).

Cuando se cumple que para un cierto _likelihood_ la forma funcional del _a priori_ y la del _a posteriori_ coinciden se dice que el _a priori_ es conjugado con el _likelihood_. Historicamente los problemas en estadística Bayesiana estuvieron restringidos al uso de _a prioris_ conjugados, ya que estos garantizan la tratabilidad matemática del problema, es decir garantizan que es posible obtener una expresión analítica para nuestro problema. En el próximo capítulo veremos técnicas computacionales modernas que permiten obtener _a posterioris_ incluso cuando no se usan _a prioris_ conjugados, lo que ha permitido el resurgimiento de la estadística Bayesiana en las últimas décadas.

#### Notación y visualización de modelos Bayesianos

Para representar modelos en estadística Bayesiana (y en probabilidad en general) se suele utilizar la siguiente notación

$$
\theta \sim \operatorname{Beta}(\alpha, \beta) \\
y \sim \operatorname{Bin}(n=1, p=\theta)
$$

El símbolo $\sim$ indica que la variable a la izquierda se distribuye según la distribución a la derecha. Entonces podríamos decir que $\mathbf{\theta}$ es una variable aleatoria con distribución beta, y que beta está definida por los parámetros $\alpha$ y $\beta$, este es nuestro _a priori_. En la siguiente linea tenemos el _likelihood_ el cual está definido por una distribución binomial con parámetros $n=1$ y $p=\theta$. Nótese que tanto $\theta$ como $y$ son vectores, en algunos textos se usa una notación alternativa usando escalares y subíndices como por ejemplo $y_i$.

Gráficamente esto se puede representar usando los diagramas de Kruschke:

En el primer nivel (de arriba hacia abajo) se observa el _a priori_, luego el likelihood, y por último los datos. Las flechas indican la vinculación entre las partes del modelo y el signo $\sim$ la naturaleza estocástica de las variables.

####  Obteniendo los datos

Bien, ahora que sabemos cómo calcular el *a posteriori*, lo único que resta es conseguir los datos. En este ejemplo los datos son sintéticos, es decir los obtuve computacionalemnte mediante un generador de números (pseudo)aleatorios, pero bien podrían haber surgido de un experimento con una moneda *real*.

#### Calculando el _a posteriori_

En el próximo capítulo veremos cómo usar métodos computacionales para computar un _a posteriori_ sin necesidad de derivarlo analíticamente. Esto es lo que haremos para resolver el resto de los problemas del curso. Pero dado que ya nos tomamos el trabajo de derivar analíticamente la expresión para el _a posteriori_ vamos a usar esa expresión. Si miran el código de la siguiente celda verán que la mayoría de las lineas se encargan de dibujar los resultados y no de calcularlos. El cálculo del _a posteriori_ ocurre en las lineas 18, 22 y 26. Cada una de estas lineas computa el _a posteriori_ para cada uno de los _a prioris_ que vimos antes. El cálculo es simple, tan solo se computa el valor del _a posteriori_ (usando la función _pdf_ de la distribución beta provista por SciPy) para 100 puntos igualmente espaciados entre 0 y 1 (linea 7). El loop que empieza en la linea 9 se debe a que exploraremos cómo cambian las distribuciones _a posteriori_ para distinta cantidad de datos (*n_intentos*). Con una linea negra punteada y vertical se indica el valor real de $\theta$, valor que por supuesto es desconocido en una situación real, pero conocido para mí, ya que los datos son sintéticos.

```python
plt.figure(figsize=(12, 9))

n_trials = [0, 1, 2, 3, 4, 8, 16, 32, 50, 150]
data = [0, 1, 1, 1, 1, 4, 6, 9, 13, 48]
theta_real = 0.35

beta_params = [(1, 1), (0.5, 0.5), (20, 20)]
dist = stats.beta
x = np.linspace(0, 1, 2000)

for idx, N in enumerate(n_trials):
    if idx == 0:
        plt.subplot(4, 3, 2)
        plt.xlabel('θ')
    else:
        plt.subplot(4, 3, idx+3)
        plt.xticks([])
    y = data[idx]
    for (a_prior, b_prior) in beta_params:
        p_theta_given_y = dist.pdf(x, a_prior + y, b_prior + N - y)
        plt.fill_between(x, 0, p_theta_given_y, alpha=0.7)

    plt.plot(theta_real, 0, ms=9, marker='o', mec='w', mfc='k')
    plt.plot(0, 0, label=f'{N:4d} experimentos\n{y:4d} caras', alpha=0)
    plt.xlim(0, 1)
    plt.ylim(0, 12)
    plt.legend()
    plt.yticks([])
plt.tight_layout()
```

#### Analizando los resultados

La primer figura del panel muestra los *a priori*, nuestra estimación de $\theta$ dado que no hemos realizado ningún experimento. Las sucesivas nueve figuras muestran las distribuciones *a posteriori* y se indica la cantidad de experimentos y de caras obtenidas. Además se puede ver una linea negra vertical en 0.35, la cual representa el valor verdadero de $\theta$. Por supuesto que en problemas reales este valor es desconocido.

Este ejemplo es realmente ilustrativo en varios aspectos.

* El resultado de un análisis Bayesiano NO es un solo valor, si no una distribución (_a posteriori_) de los valores plausibles de los parámetros (dado los datos y el modelo).
* La dispersión o ancho de las curvas es una medida de la incertidumbre sobre los valores.
* El valor más probable viene dado por la moda de la distribución (el _pico_ de la distribución).
* Aún cuando $\frac{2}{1} = \frac{8}{4}$ son numéricamente iguales tenemos menor incertidumbre en un resultado cuando el número de experimentos es mayor.
* Dada una cantidad _suficiente_ de datos los resultados tienden a converger sin importar el _a priori_ usado.
* La rapidez con la que los resultados convergen varía. En este ejemplo las curvas azul y anaranjada parecen converger con tan solo 8 experimentos, pero se necesitan más de 50 experimentos para que las tres curvas se muestren similares. Aún con 150 experimentos se observan ligeras diferencias.
* Partiendo de los _a priori_ uniforme (azul) o _sesgado_ (anaranjado) y habiendo realizado un solo experimento y observado una sola cara, lo más razonable es pensar que estamos frente a una moneda con dos caras!
* La situación cambia drásticamente al ver por primera vez una moneda caer ceca. Ahora lo más probable (dado cualquiera de los tres *a prioris*) es inferir que $\theta=0.5$. Los valores de $\theta$ exactamente 0 o 1 se vuelven imposibles.
* El _a priori_ no sesgado (verde) es más informativo que los otros dos (la distribución esta más concentrada), por ello se requiere de un número mas grande de experimentos para "moverlo".
* El _a priori_ uniforme (azul) es lo que se conoce como no informativo. El resultado de un análisis Bayesiano usando un _a priori_ no-informativos en general coinciden con los resultados de análisis frecuentistas (en este caso el valor esperado de $\theta = \frac{y}{N}$).

##  Influencia y elección del _a priori_

De los ejemplos anteriores debería quedar claro que los _a priori_ influencian los resultados de nuestros cálculos. Esto tiene total sentido si no fuese así no haría falta incluirlos en el análisis y todo sería más simple (aunque nos perderíamos la oportunidad de usar información previa). De los ejemplos anteriores también debería quedar claro que a medida que aumentan los datos (como las tiradas de monedas) los resultados son cada vez menos sensibles al _a priori_. De hecho, para una cantidad infinita de datos el _a priori_ no tiene ningún efecto. Exactamente cuantos datos son necesarios para que el efecto del _a priori_ sea despreciable varía según el problema y los modelos usados. En el ejemplo de la moneda se puede ver que 50 experimentos bastan para hacer que dos de los resultados sean prácticamente indistinguibles, pero hacen falta más de 150 experimentos para que los 3 resultados se vuelvan _practicamente_ independientes del _a priori_. Esto es así por que los dos primeros _a prioris_ son relativamente _planos_, mientras que el tercer _a priori_ concentra casi toda la probabilidad en una región relativamente pequeña. El tercer a priori no solo considera que el valor más probable de $\theta$ es 0.5, si no que considera que la mayoría de los otros valores son muy poco probables. ¿Cómo cambiarían los resultados si hubiéramos usado como _a priori_ $\operatorname{Beta}(\alpha=2, \beta=2)$?

La elección de los _a priori_ puede poner nervioso a quienes se inician en el análisis Bayesiano (o a los detractores de este paradigma). ¡El temor es que los _a prioris_ censuren a los datos y no les permitan _hablar por sí mismos_! Eso está muy bien, pero el punto es que los datos no saben hablar, con suerte murmuran. Los datos solo tienen sentido a la luz de los modelos (matemáticos y mentales) usados para interpretarlos, y los _a prioris_ son parte de esos modelos.

Hay quienes prefieren usar _a priori_ no-informativos (también conocidos como _a priori_ planos,
vagos, o difusos). Estos _a priori_ aportan la menor cantidad posible de información y por lo tanto tienen el menor impacto posible en el análisis. Si bien es posible usarlos, en general hay razones prácticas para no preferirlos. En este curso usaremos _a priori ligeramente informativos_ siguendo las recomendaciones de Gelman, McElreath, Kruschke, y otros. En muchos problemas sabemos al menos algo de los valores posibles que pueden tomar nuestros parámetros, por ejemplo que solo pueden ser positivos, o que están restringidos a sumar 1 o el rango aproximado, etc. En esos casos podemos usar _a prioris_ que introduzcan esta _ligera_ información. En estos casos podemos pensar que la función del _a priori_ es la de mantener las inferencias dentro de límites razonables. Estos _a priori_ se suelen llamar regularizadores.

Por supuesto que también es posible usar _a prioris informativos_ (o _fuertes_). Hacer esto es razonable solo si contamos con información previa confiable. Esto puede ser ventajoso en casos en que los datos contengan poca información sobre el problema. Si la información no viene por el _likelihood_ (datos), entonces puede venir por el _a priori_. A modo de ejemplo, en bioinformática estructural es común usar toda la información previa posible (de forma Bayesiana y no-Bayesiana) para resolver problemas. Esto es posible por la existencia de bases de datos que almacenan los resultados de cientos o miles experimentos realizados a lo largo de décadas de esfuerzo (¡No usar esta información sería casi absurdo!). En resumen, si contás con información confiable no hay razón para descartarla, menos si el _argumento_ es algo relacionado con pretender ser _objetivo_ (¡No hay objetividad en negar lo que se sabe!).

Hasta ahora hemos visto que es posible clasificar, aunque sea de forma vaga o aproximada, a los _a priori_ en función de la información que contienen. Pero saber esta clasificación no necesariamente hace las cosas más simples a la hora de elegir un _a priori_. ¿Acaso no sería mejor eliminar los _a prioris_ de nuestro análisis? Eso haría el asunto mucho mas simple. Bueno, el punto es que desde una perspectiva Bayesiana todos los modelos tienen _a prioris_, aun cuando no sean explícitos. De hecho muchos resultados de la estadística frecuentista pueden considerarse casos especiales de modelos Bayesianos usando _a prioris planos_. Volviendo a la figura anterior se puede ver que la moda del _a posteriori_ para la curva azul. Coincide con la estimación (puntual) frecuentista para el valor de $\theta$

$$
\hat \theta = {{y} \over {N}}
$$

Notar que $\hat \theta$ es una estimación puntual (un número) y no una distribución.

Este ejemplo nos muestra que no es posible hacer análisis estadísticos y sacarse los _a prioris_ de encima. Un posible corolario es que es más flexible y transparente especificar los _a prioris_ de forma explícita que esconderlos bajo la cama. Al hacerlo ganamos mayor control sobre nuestro modelo, mayor transparencia y por el mismo precio la estimación de la incertidumbre con la que se estima cada parámetro.

Por último, hay que recordar que el modelado estadístico (como otras formas de modelado) es un proceso iterativo e interactivo. Nada nos impide usar más de un _a priori_ (o un likelihood) si así lo quisiéramos. Una parte importante del modelado es la de cuestionar los supuestos y los _a prioris_ son simplemente un tipo de supuestos (como lo son los _likelihoods_). Si tuvieramos más de un _a priori_ razonable podríamos realizar un _análisis de sensibilidad_, es decir evaluar como cambian los resultados con los _a prioris_, podríamos llegar a la conclusión que para un rango amplio de _a prioris_ ¡los resultados no varían! Más adelante veremos varias herramientas para comparar distintos modelos.

Dado que los _a prioris_ tienen un papel central en la estadística Bayesiana, seguiremos discutiéndolos a medida que vayamos viendo problemas concretos. Por lo que si esta discusión no ha aclarado todas tus dudas y seguís algo confundido, mejor mantener la calma y no preocuparse demasiado, este tema ha sido motivo de discusión y confusión durante décadas ¡y la discusión todavía continua!

## Cuantificando el peso del _a priori_

En general la distribución más familiar para la mayoría de las personas es la distribución Gaussiana, como esta distribución está definida por dos parámetros, la media y la dispersión de ese valor medio, suele resultarnos _natural_ pensar las distribuciones en esos términos. Si queremos expresar la distribución beta en función de la media y la dispersión podemos hacerlo de la siguiente forma:

\begin{align}
\alpha &= \mu \kappa \\
\beta &= (1 - \mu) \kappa
\end{align}

donde $\mu$ es la media y $\kappa$ es un parámetro llamado concentración. Por ejemplo si $\mu=0.5$ y $\kappa=40$, tenemos que:

\begin{align}
\alpha = 0.5 \times 40 &= 20 \\
\beta = (1-0.5) \times 40 &= 20
\end{align}

$\kappa$ se puede interpretar como la cantidad de experimentos si/no que realizamos dándonos como resultado la media $\mu$. Es decir el _a priori_ no sesgado (verde) equivale a haber arrojado una moneda 40 veces y haber obtenido como media 0.5. Es decir que si usamos ese _a priori_ recién al observar 40 experimentos si/no, los datos tendrán el mismo peso relativo que el _a priori_, por debajo de este número el _a priori_ contribuye más que los datos al resultado final y por encima menos. El _a priori_ azul (uniforme) equivale a haber observado a la moneda caer una vez cara y otra vez ceca ($\kappa = 2$). Cuando $\kappa < 2$, la cosa se pone un poco extraña, por ejemplo el _a priori_ sesgado (anaranjado) equivale a haber observado una sola moneda ($\kappa = 1$) pero en una especie de (a falta de mejor analogía) ¡_superposición cuántica de estados_!

## Resumiendo el  _a posteriori_

El resultado de un análisis Bayesiano es siempre una distribución de probabilidad. En el caso de la moneda esto es evidente, y en el caso del diagnostico es menos claro ya que la distribución es discreta y solo puede tomar dos valores.

A la hora de comunicar los resultados de un análisis Bayesiano, lo más informativo es reportar la distribución completa, aunque esto no siempre es posible o deseable, por ejemplo el _a posteriori_ de una distribución multidimensional es imposible de dibujar en papel. En general, se suele recurrir a distintas medidas que resumen el _a priori_, por ejemplo reportando la media de la distribución _a posteriori_. Algo un poco más informativo es reportar además un intervalo de credibilidad. Existen varios criterios para definir intervalos de credibilidad, el que usaremos en este curso (y que también es ampliamente usado en la literatura) es lo que se conoce como intervalo de más alta densidad y nos referiremos a él por su sigla en ingles, HPD (_Highest Posterior Density interval_). Un HPD es el intervalo, más corto, que contiene una porción fija de la densidad de probabilidad, generalmente el 95% (aunque otros valores como 90% o 50% son comunes). Cualquier punto dentro de este intervalo tiene mayor densidad que cualquier punto fuera del intervalo. Para una distribución unimodal, el HPD 95 es simplemente el intervalo entre los percentiles 2,5 y 97,5.

ArviZ es un paquete de Python para análisis exploratorio de modelos Bayesianos. ArviZ provee de funciones que facilitan el resumir el _a posteriori_. Por ejemplo `plot

## Resumiendo el  _a posteriori_

El resultado de un análisis Bayesiano es siempre una distribución de probabilidad. En el caso de la moneda esto es evidente, y en el caso del diagnostico es menos claro ya que la distribución es discreta y solo puede tomar dos valores.

A la hora de comunicar los resultados de un análisis Bayesiano, lo más informativo es reportar la distribución completa, aunque esto no siempre es posible o deseable, por ejemplo el _a posteriori_ de una distribución multidimensional es imposible de dibujar en papel. En general, se suele recurrir a distintas medidas que resumen el _a priori_, por ejemplo reportando la media de la distribución _a posteriori_. Algo un poco más informativo es reportar además un intervalo de credibilidad. Existen varios criterios para definir intervalos de credibilidad, el que usaremos en este curso (y que también es ampliamente usado en la literatura) es lo que se conoce como intervalo de más alta densidad y nos referiremos a él por su sigla en ingles, HPD (_Highest Posterior Density interval_). Un HPD es el intervalo, más corto, que contiene una porción fija de la densidad de probabilidad, generalmente el 95% (aunque otros valores como 90% o 50% son comunes). Cualquier punto dentro de este intervalo tiene mayor densidad que cualquier punto fuera del intervalo. Para una distribución unimodal, el HPD 95 es simplemente el intervalo entre los percentiles 2,5 y 97,5.

ArviZ es un paquete de Python para análisis exploratorio de modelos Bayesianos. ArviZ provee de funciones que facilitan el resumir el _a posteriori_. Por ejemplo `plot_posterior` puede ser usado para generar un gráfico con la media y HPD. En el siguiente ejemplo en vez de un _a posteriori_ de un ejemplo real estamos usando datos generados al azar según una distribución beta.

```python
mock_posterior = stats.beta.rvs(5, 11, size=1000)
az.plot_posterior(mock_posterior, figsize=(8, 4));
```

Ahora que estamos aprendiendo que es un HPD por primera vez y antes de que automaticemos el concepto conviene aclarar un par de puntos.

1. **La elección automática de 95% (o cualquier otro valor) es totalmente arbitraria**. En principio no hay ninguna razón para pensar que describir el _a posteriori_ con un HPD 95 sea mejor que describirlo con un HPD 98 o que no podamos usar valores como 87% o 66%. El valor de 95% es tan solo un accidente histórico. Como un sutil recordatorio de esto ArviZ usa por defecto el intervalo de 94%.

2. **Un intervalo de credibilidad (que es Bayesiano) no es lo mismo que un intervalo de confianza (que es frecuentista)**. Un intervalo de confianza es un intervalo que se define según un nivel de confianza, en general del 95%. Un intervalo de confianza se construye de tal forma que si repitiéramos infinitas veces un experimento obtendríamos que la proporción de intervalos que contienen el valor _verdadero_ del parámetro que nos interesa coincide con el nivel de confianza estipulado. Contra-intuitivamente esto no es lo mismo que decir que un intervalo en particular tiene una probabilidad $x$ de contener el parámetro (esto sería la definición de un intervalo de credibilidad, que es Bayesiano). De hecho, un intervalo de confianza en particular contiene o no contiene al valor, la teoría frecuentista no nos deja hablar de probabilidades de los parámetros, ya que estos tienen valores fijos. Si no queda clara la diferencia no te hagas problema, la diferencia entre estos dos conceptos suele ser tan difícil de entender que en la práctica estudiantes y científicos por igual interpretan los intervalos de confianza (frecuentistas) como intervalos de credibilidad (Bayesianos).

> Si bien desde la perspectiva Bayesiana podemos afirmar que un intervalo de credibilidad nos permite asegurar que la probabilidad de un parámetro está acotado en cierto rango. Siempre hay que tener presente que dicha afirmación es correcta SOLO en sentido teórico. Es decir, solo si todos los supuestos contenidos en el modelo son ciertos. Una inferencia es siempre dependiente de los datos y modelos usados.

### Pruebas predictivas _a posteriori_

Las pruebas predictivas _a posteriori_ son una forma de evaluar el modelo. Una vez obtenido el modelo Bayesiano se usa el _a posteriori_ para generar datos $\tilde{y}$, es decir datos predichos condicionados por los valores estimados de $\theta$ y por los datos ya observados $y$.

$$
p(\tilde{y}  \mid  y) = \int p(\tilde{y} \mid \theta) p(\theta \mid y) d\theta
$$

Los datos generados son predictivos ya que son los datos que se esperaría ver por ejemplo en un futuro experimento, es decir son variables no observadas pero potencialmente observables. La prueba consiste en comparar los datos observados con los datos predichos a partir del _a posteriori_.

Las pruebas predictivas _a posteriori_ son pruebas de auto-consistencia. Este ejercicio nos permite evaluar si el modelo es razonable, la idea general no es determinar si un modelo es _correcto_ o no ya que como dijo George Box _"todos los modelos están equivocados, pero algunos son útiles"_. El grado de confianza en la _verosimilitud_ de los modelos ciertamente es distinta entre practicantes de distintas disciplinas científicas, en disciplinas como física cuando se estudian sistemas relativamente simples bajo condiciones experimentales extremadamente controladas y haciendo uso de _teorías fuertes_, es probable que se le asigne un alto grado de confianza a ciertos modelos. Pero esto no suele ser cierto en disciplinas como ciencias sociales o biología (aunque sospecho que la variabilidad encontrada en biología ¡es muy alta!). En el caso de contar _a prioris_ muy informativos la evaluación de un modelo también puede ser usado para evaluar si los propios datos son razonables, indicando que tal vez sea necesario conseguir nuevos datos o revisar como se obtuvieron los datos o como se procesaron.

En definitiva la principal utilidad de las pruebas predictivas _a posteriori_ debería ser el permitirnos dar una segunda mirada, crítica, al modelo y tratar de entender la razón de discrepancias sistemáticas (si las hubiera), estas discrepancias nos pueden llevar a entender mejor los límites del modelo, abandonar el modelo por completo o tal vez mejorarlo.

Si bien se han desarrollado métodos _formales_ o _cuantitativos_ para realizar pruebas predictivas _a posteriori_, una aproximación que suele ser más informativa y simple de interpretar es realizar gráficas, que es lo que iremos viendo en los próximos capítulos.

## Resumen

Empezamos este capítulo con una breve discusión sobre el modelado estadístico y la teoría de la probabilidad y teorema de Bayes que se deriva de ella. Luego utilizamos el problema de la moneda como una excusa para introducir aspectos básicos del modelado Bayesiano y el análisis de datos. Utilizamos este ejemplo clásico para transmitir algunas de las ideas más importantes de las estadística Bayesiana, fundamentalmente el uso de distribuciones de probabilidad para construir modelos y representar la incertidumbre. Tratamos de desmitificar el uso de los _a prioris_ dándoles el mismo estatus epistemológico-metodológico que otros elementos que forman parte del proceso de modelado e inferencia, como el _likelihood_ o incluso meta-preguntas, como por qué estamos tratando de resolver un problema en particular. Concluimos el capítulo con una breve descripción de cómo interpretar y comunicar los resultados de un análisis bayesiano.

La siguiente figura, inspirada en una figura de [Sumio Watanabe](https://www.crcpress.com/Mathematical-Foundations-of-Bayesian-Statistics/Watanabe/p/book/9781482238068) resume el _flujo de trabajo Bayesiano_ tal cual se describió en este capítulo.

1. Suponemos que existe una distribución _verdadera_ que, en general, es desconocida (ya sea en la práctica o intrínsecamente). De esta distribucción se obtiene una muestra finita, ya sea haciendo un experimento, una encuesta, una observación, una simulación, etc.
2. A partir de la muestra realizamos una inferencia Bayesiana obteniendo una distribución _a posteriori_. Esta distribución es el objeto central de la estadística Bayesiana ya que contiene toda la información sobre un problema (dado el modelo y los datos).
3. Una cantidad que podemos derivar del _a posteriori_ es la distribución predictiva _a posteriori_, es decir predicciones. Una forma de evaluar un modelo es comparar la distribución predictiva _a posteriori_ con la muestra finita que obtuvimos en primer lugar.

Es importante notar que mientras la distribución _a posteriori_ es una distribución **sobre los parámetros en un modelo**, la distribución predictiva _a posteriori_ es una distribución **sobre los datos** (predichos).

## Ejercicios

1. El estadístico Bruno de Finetti declaró que "Las probabilidades no existen", queriendo indicar que las probabildiades son solo una herramienta para cuantificar la incerteza y que no tienen existencia objetiva en sí mismas. Edwin Jaynes, físico, declaró que la teoría de probabilidad es la lógica de la ciencia. Discuta estos enunciados a la luz de lo expuesto en este y el anterior capítulo.

2. Supongamos que tenemos dos monedas una que cae la mitad de veces cara y la mitad ceca y una moneda trucada que cae siempre cara. Si tomamos una de las monedas al azar y obtenemos cara, cual es la probabilidad que esa moneda sea la trucada.

3. En el ejemplo del diagnóstico. Si realizamos el mismo análisis una segunda vez sobre el mismo paciente y obtenemos nuevamente positivo ¿Cuál seria la probabilidad que el paciente esté enfermo? ¿Y si diera negativo? ¿Para el segundo análisis el _a priori_ es el mismo que para el primer caso?

4. Estás en un programa de concursos: Hay tres puertas, detrás de una de ellas un 0km, detrás de las otras dos, una cupón para tomar el té con Eduardo Feinmann. Sin saber cuál puerta esconde cada "premio" se te pide que elijas una de ella. Una vez elegida el conductor del programa (que si sabe que hay detrás de cada puerta), abre una de las puertas que contiene el cupón para pasar un rato con Eduardo Feinmann. En ese momento el conductor te advierte que tienes la posiblidad de cambiar de puerta o quedarte con la puerta que elegiste inicialmente. ¿Cuál es la mejor opción?

5. En la cola del supermercado una mujer les cuenta que es madre de dos niñes. Asumiendo que la probabilidad _a priori_ de niña es la misma que la de niño y es igual a 1/2.
    * Ustedes le preguntan si tiene algún varón y la mujer dice que sí ¿Cuál es la probabilidad que une de les niñes sea niña?
    * Supongamos que en vez del escenario anterior, sucede otra cosa mientras están conversando _aparece_ su hijo varón y la abraza. ¿Cuál es la probabilidad que la mujer tenga una hija?

7. En una escena del crimen se encuentra sangre. La sangre es de un tipo que solo está presente en el 1% de la población.
    * El fiscal enuncia: "Si el acusado fuese inocente, la probabilidad que tuviese el mismo tipo de sangre encontrado en la escena del crimen sería de 1%, y de 99% si fuese culpable, por lo tanto ¡lo más probable es que sea culpable!". Este razonamiento es incorrecto, explique el porqué.
    * El abogado defensor enuncia: "El crimen ocurrió en una ciudad de 500000 habitantes por lo que 5000 personas tienen ese tipo de sangre, por lo tanto el acusado sólo tiene una probabiliad de 1/5000 de ser el responsable". Este razonamiento también es incorrecto, explique el porqué.

8. Use la siguiente función para explorar diversas combinaciones de _priors_ y _likelihoods_. Enuncie las conclusiones que considere más relevantes.

```python
def a_posteriori_grilla(grilla=10, a=1, b=1, caras=6, tiradas=9):
    grid = np.linspace(0, 1, grilla)
    prior = stats.beta(a, b).pdf(grid)
    likelihood = stats.binom.pmf(caras, tiradas, grid)
    posterior = likelihood * prior
    posterior /= posterior.sum()
    _, ax = plt.subplots(1, 3, sharex=True, figsize=(16, 4))
    ax[0].set_title('caras = {}\ntiradas = {}'.format(caras, tiradas))
    for i, (e, e_n) in enumerate(zip([prior, likelihood, posterior], ['a priori', 'likelihood', 'a posteriori'])):
        ax[i].set_yticks([])
        ax[i].plot(grid, e, 'o-', label=e_n)
        ax[i].legend(fontsize=14)

interact(a_posteriori_grilla, grilla=ipyw.IntSlider(min=2, max=100, step=1, value=15), a=ipyw.FloatSlider(min=1, max=7, step=1, value=1), b=ipyw.FloatSlider(
    min=1, max=7, step=1, value=1), caras=ipyw.IntSlider(min=0, max=20, step=1, value=6), tiradas=ipyw.IntSlider(min=0, max=20, step=1, value=9));
```

    interactive(children=(IntSlider(value=15, description='grilla', min=2), FloatSlider(value=1.0, description='a'…


#  CS207 Systems Development for Computational Science
## Final Project

**Harvard University**

**Fall 2019**

**Instructor**: David Sondak

**Team #14**: Fantastic Four

**Students**: Daniel Cox, Anna Davydova, Stephen Moon, Valentina Toll Villagra

**Git Repository**:https://github.com/IACS-CS-207-FantasticFour/cs207-FinalProject

*“Nothing takes place in the world whose meaning is not that of some maximum or minimum.”*
― Leonhard Euler

# INTRODUCTION:

This project aims to deliver an Automatic Differentiation Software DeltaPi that will calculate derivatives efficiently and accurately, making it useful across a wide spectrum of applications. In this package, we will execute the forward mode of Automatic Differentiation.  We will also provide an extension package HedgeDeltaPi that will calculate delta for European Call and Put options, allowing for efficient delta hedging. We are especially excited about the applications of this software on Wall Street, where Automatic Differentiation has been adopted fairly recently and has already shown significant improvements in efficiency and/or accuracy vs. the alternative methods (Hedin, 2019) .

Derivatives are powerful and ubiquitous. Their use spans from gradients and Hessians in machine learning applications to hedge sensitivities in financial markets. However, of the four existing methods for derivative calculation only Automatic  Differentiation (AD) combines interpretability, efficiency and accuracy. Manual differentiation is inefficient and susceptible to error.(Baydin et. al., 2018). Numerical differentiation, while easy and fairly quick in its implementation, is also inaccurate, prone to rounding and truncation errors (Jerrell, 1997).  It does not scale well, which makes it a poor choice for machine learning models. Symbolic differentiation, while accurate, can become incredibly complex facing "expression swell" issues (Corliss, 1988). AD overcomes these issues, via its application of the chain rule and a step by step approach to differentiation, and accurately computes derivatives with asymptotic efficiency. (Baydin et.al, 2018).

Our software DeltaPi will implement AD methods, allowing the end user to benefit from its accuracy and efficiency.  Our goal is to produce a package that can handle a wide variety of uses beyond simple scalar functions and an extension package to compute deltas for financial options on Wall Street.

# BACKGROUND:

Generally speaking, in any computer program a function can be broken down into its elementary function components (unary or binary) such as addition, subtraction  log, sin, sqrt etc (Heath, 2018).  Since the value of partial derivatives of these elementary functions can be easily calculated, then the value of the entire function can be calculated via the application of the chain rule.

Let us walk through this process in more detail. Fundamentally, Chain Rule is the key pillar to the AD process. Recall that:

$$\dfrac{\partial h}{\partial t} = \dfrac{\partial h}{\partial u}\dfrac{\partial u}{\partial t}$$

We can extend the definition of the Chain Rule to a function that contains to functions as follows: h(u(t),v(t)) (Sondak, 2019).:

\begin{align}
  \displaystyle
  \frac{\partial h}{\partial t} = \frac{\partial h}{\partial u}\frac{\partial u}{\partial t} + \frac{\partial h}{\partial v}\frac{\partial v}{\partial t}.
\end{align}

This result, from a perspetive of a gradient, leads us to this general rule:

\begin{align}
  \nabla_{x}h = \sum_{i=1}^{n}{\frac{\partial h}{\partial y_{i}}\nabla y_{i}\left(x\right)}.
\end{align}

Thus, in a nutshell, the AD process consist of breaking down the function into its elementary components and carrying out the differentiation process in sequential order of these operations while multiplying through with the chain rule. The output of this process is a dual number containing the value of the function along with its derivative.  We can visualize this process with a computational graph and a table that contains the trace of the calculations.

For example, for a simple function (*adapted from class exercises*): $$f\left(x,y\right) = (sin(x)-cos(y))^2.$$

The forward mode AD graph looks as follows:

The corresponding computational table for values $x=\frac{\pi}{2}$ and $y=\frac{\pi}{3}$looks as follows:

| Trace    | Elementary Operation    | Current Function Value           | Derivative         | Partial Derivative w.r.t. x                            | Partial Derivative w.r.t. y                       |evaluate f(x) and its derivative: (f(x), f'(x))                   |
| :------: | :-------------------------------------:| :--------------------------------------------------------:|:-------------------------------------: | :-------------------------------------------------:|:----------------------------------------------:|:---------------------------------------:
| $x_{1}$  | $x_{1}$                                |          $\frac{\pi}{2}$                                              |$\dot {x_1}$                                     |      $1$                                           |      $0$                                       |$\left(\frac{\pi}{2}, 1\right)$                                                                  |
| $x_{2}$  | $x_{2}$                                |          $\frac{\pi}{3}$                                              |$\dot {x_2}$                                     |      $0$                                           |      $1$                                       |$\left(\frac{\pi}{3},1 \right)$                                                                  |
| $x_{3}$  | $sin(x_1)$                             |          $1$                                          |$cos(x_1)\dot{x_1}$                          |      $0$                                          |      $0$                                       |$\left(1,0)\right)$                                                        |
| $x_{4}$  | $cos(x_2)$                         |               $\frac{1}{2}$                                            |$-sin(x_2)\dot{x_2}$                     |      $0$                                          |    $-\frac{\sqrt{3}}{2}$                             |$\left(\frac{1}{2},-\frac{\sqrt{3}}{2}\right )$                                                         |
| $x_{5}$  | $x_3-x_4$                          |          $\frac{1}{2}$                                        |$\dot{x_3}-\dot{x_5}$           |     $0$                                        |   $\frac{\sqrt{3}}{2}$                            |$\left(\frac{1}{2},\frac{\sqrt{3}}{2}\right)$                                                         |
| $x_{6}$  | $x_5^2$                             |    $\frac{1}{4}$                                     |  $2x_5\dot{x_5}$                         |                        $0$                            |       $\frac{\sqrt{3}}{2}$ |           $\left(\frac{1}{4},\frac{\sqrt{3}}{2}\right)$      |

We note not only the simplicity of differentiation process on elementary functions carried along by the chain rule but relatively interpretable process that could be useful to our end-users.

AD process generalizes nicely to calculate a function's  Jacobian where the function:

$f: \mathbb{R}^n \rightarrow \mathbb{R}^m $ has n independent (input) variables $x_i$ and m dependent (output) variables $y_j$ (Baydin et.al, 2018). Here, for each forward pass only one of of the variables $\dot{x_i}$ is initialized at $\dot{x_i}=1$, while the rest are set at 0.  Every time we evaluate the derivate for a specified value of x such as $x=a$, we are computing:

$$\left.\dot{y}_j=\frac{\partial{y_j}}{\partial{x_i}}\right\vert_{x=a}, j=1, . . . , m$$

This gives us one column from a Jacobian $mxn$ matrix:

    $$\left. J_f=
    \begin{bmatrix}
    \frac{\partial{y_1}}{\partial{x_1}} & . & . & . & \frac{\partial{y_1}}{\partial{x_n}} \\
    . & . &  &  & .\\
    . &   & . & & .\\
    . &  & & .& . \\
    \frac{\partial{y_m}}{\partial{x_1}} & . & . & . & \frac{\partial{y_m}}{\partial{x_n}}
    \end{bmatrix}\right\vert_{x=a}$$

When we finish all of our evaluations for each value of $x_i, . . . , x_n$ we end up with the full Jacobian matrix. What is particularly useful about AD is that it provides a more efficient/matrix free way for calculating Jacobian-vector products (Baydin et.al, 2018). Consider the following Jacobian-vector product:

$$ J_fr=
    \begin{bmatrix}
    \frac{\partial{y_1}}{\partial{x_1}} & . & . & . & \frac{\partial{y_1}}{\partial{x_n}} \\
    . & . &  &  & .\\
    . &   & . & & .\\
    . &  & &. & . \\
    \frac{\partial{y_m}}{\partial{x_1}} & . & . & . & \frac{\partial{y_m}}{\partial{x_n}}
    \end{bmatrix}
    \begin{bmatrix}
    r_1\\
    .\\
    .\\
    .\\
    r_n
    \end{bmatrix}$$

All we have to do here is initialize $\dot{x}=r$ and we will be able to compute the entire Jacobian-vector product in one forward pass. In general, AD forward mode is very efficients for $f: \mathbb{R}\rightarrow \mathbb{R}^m $ with all of the  derivatives computed in one forward pass.

***Please Note:*** AD forward mode is not always efficient! In the case where we have  $f: \mathbb{R}^n \rightarrow \mathbb{R}$  AD forward mode performs n evaluations, outputting one column at a time for 1xn Jacobian matrix. Using reverse mode here would be significantly more efficient as we would just need one pass. It is also important to keep in mind that for situations where $f: \mathbb{R^n}\rightarrow \mathbb{R}^m $ where n>>m, using reverse mode would be more efficient. For a function $f: \mathbb{R^n}\rightarrow \mathbb{R}^m $ where $ops(f)$ is the number of operations contained within the function, the time it would take AD forward mode can be approximated as $n*c*ops(f)$ while for AD reverse mode the time is $m*c*ops(f)$ where c is some constant c < 6, usually in the [2, 3] range (Griewank and Walther, 2008). For more information on AD reverse mode please see [Automatic Differentiation in Machine Learning: A Survey by Baydin et al](https://arxiv.org/pdf/1502.05767.pdf).

# HOW TO USE OUR PACKAGE:

## HOW TO INSTALL

To install without creating a virtual environment, refer to Setep 4 only.

### 1. Check Conda is Installed and Updated
* In the command line of a terminal window, check you have conda installed by entering:
        conda -V
* If conda is installed you will see:
        conda -V
        conda x.x.x
* To check conda is updated, type:
        conda update conda

### 2. Create a Virtual Environment
* In the command line, enter:
        conda create --name env_name python
  where `env_name` is your desired name for the environment

* To specify the python version, use:
        conda create --name env_name python=x.x

### 3. Activate the Virtual Environment
* To activate or switch into your new virtual environment, type:
        source activate env_name

### 4. Install

#### PyPI
* Installable at: https://pypi.org/project/DeltaPI-FantasticFour/

#### GitHub
* You can clone or download the Github repository from here:

        HTTP: https://github.com/IACS-CS-207-FantasticFour/cs207-FinalProject.git

        SSH: git@github.com:IACS-CS-207-FantasticFour/cs207-FinalProject.git

### 5. Activate and Use Package
* Follow the instruction in the following section, 'HOW TO USE' for step by step details and examples on how to use.

### 6. Deactivating and Removing the Virtual Environment
* Once you have finished using the AutiDiff paackage, you can deactivate the virtual environment by enterting:
        source deactivate
* To remove the virtual environment completely, type:
        conda remove --name env_name --all

### HOW TO USE

### To use the AutoDiff Class
* Be `x,y,z,...` input variables
* Be `f(x,y,z,...)` the output of function f at (x,y,z,...)
* Be `AutoDiff` the class

#### 0. Import AutoDiff class (for using in script)
        from AutoDiff import AutoDiff as AutoDiff

#### 1. Create an instance of the AutoDiff class
        x = AutoDiff(val, derv)
where:
* `val` = the value of x at the desired point
* `derv` = the initial value of the derivative of x
    * 1 for calculating the partial derivative df/dx
    * 0 for calculating the partial derivative df/dy,df/dz,...

#### 2. Repeat the process of creating an instance of the AutoDiff class for each input variable
        y = AutoDiff(val, derv)
        z = AutoDiff(val, derv)
        ...
where:
* `val` = the value of the instance variable (y,z,...) at the desired point
* `derv` = the initial value of the derivative of the instance variable (y,z,...)
    * 1 for calculating the partial derivative of the instance variable (df/dy for y, df/dz for z,...)
    * 0 for calculating the partial derivative of another variable that is not the instance one (df/dx,df/dz for y, df/dx,df/dy for z,...)

#### 3. Enter the function of interest
        f = function of x,y,z,...
where `function` can contain any of the operations listed in Table 2 in the Implementation section

#### 4. Print f.val to get the calculated value of f at the specified point (x,y,z,...)
        print(f.val)

#### 5. Print f.derv to get the calculated value of df/dx or df/dy or df/dz or ... at the specified point (x,y,z,...)
        print(f.derv)

### Examples

* For values:
        x = 3
        y = 4
        f = x + 2 * y

#### Be the goal to calculate the partial derivatives df/dx at (x,y)=(3,4)

0. Import AutoDiff class
        from AutoDiff import AutoDiff as AutoDiff

1. Create an instance of the AutoDiff class
        x = AutoDiff(3, 1)

2. Repeat the process of creating an instance of the AutoDiff class for y
        y = AutoDiff(4, 0)

3. Enter the function of interest
        f = x + 2 * y

4. Print f.val to get the calculated value of f at (3,4)
        print(f.val)

5. Print f.derv to get the calculated value of df/dx at (3,4)
        print(f.derv)

#### Be the goal to calculate the partial derivatives df/dy at the same (x,y)=(3,4)

0. Import AutoDiff class
        from AutoDiff import AutoDiff as AutoDiff

1. Create an instance of the AutoDiff class
        x = AutoDiff(3, 0)

2. Repeat the process of creating an instance of the AutoDiff class for y
        y = AutoDiff(4, 1)

3. Enter the function of interest
        f = x + 2 * y

4. Print f.val to get the calculated value of f at (3,4) - will be the same from df/dx
        print(f.val)

5. Print f.derv to get the calculated value of df/dy at (3,4)
        print(f.derv)

### To use the all_derivatives() function

* Be `x,y,z,...` input variables
* Be `func(x,y,z,...)` the output of function f at (x,y,z,...)
* Be `all_derivatives` the function that uses `AutoDiff` class

#### 0. Import all_derivatives function
        from All_derivatives import all_derivatives as all_derivatives

#### 1. Create a vector containing the input variables
        in_vars = [x,y,z,...]
where:
* `x,y,z,...` are variable names

#### 2. Create a vector containing the variable values
        in_vals = [val_x,val_y,val_z,...]
where:
* `val_x` = the value of x at which to evaluate the function
* `val_y` = the value of y at which to evaluate the function
* `val_z` = the value of z at which to evaluate the function

### 3. Enter the function to be evaluated
        func = function of x,y,z,...
where `function` can contain any of the operations listed in Table 2 in the Implementation section

#### 4. Run all_derivatives()
        result = allderivatives(func, in_vars, in_vals)

#### 5. Print result.val to get the calculated value of func at (in_vars)
        print(result.val)

#### 6. Print result.derv_vals to get vector of calculated partial derivative values, one for each variable in in_vars
        print(result.derv_vals)

### Example

* For values:
        x = 3
        y = 4
        z = 5
        func = x + 2 * y - z

#### Be the goal to calculate the all partial derivatives at once
0. Import all_derivatives function
        from All_derivatives import all_derivatives as all_derivatives

1. Create a vector containing the input variables
        in_vars = [x,y,z]

2. Create a vector containing the variable values
        in_vals = [3,4,5]

3. Enter the function to be evaluated
        func =  x + 2 * y - z

4. Run all_derivatives()
        result = all_derivatives(func, in_vars, in_vals)

5. Print result.val to get the calculated value of func at (in_vars)
        print(result.val)

6. Print result.derv_vals to get vector of calculated partial derivative values, one for each variable in in_vars
        print(result.derv_vals)

### To use multi_func_all_derivatives() function
* Be `x,y,z,...` input variables
* Be `func(x,y,z,...`) the output of function f at (x,y,z,...)
* Be `multi_func_all_derivatives` the function that uses `AutoDiff` class

#### 0. Import multi_func_all_derivatives function
        from All_derivatives import multi_func_all_derivatives as multi_func_all_derivatives

#### 1. Create a vector containing the input variables
        in_vars = [x,y,z,...]
where:
* `x,y,z,...` are variable names

#### 2. Create a vector containing the variable values
    in_vals = [val_x,val_y,val_z,...]
where:
* `val_x` = the value of x at which to evaluate the function
* `val_y` = the value of y at which to evaluate the function
* `val_z` = the value of z at which to evaluate the function

#### 3. Create a vector containing the functions to be evaluated
        funcs = [func_1,func_2,func_3,...]
where:
* `func_n` = function of x,y,z,...

where `func_n` can contain any of the operations listed in Table 2 in the Implementation section

#### 4. Run multi_func_all_derivatives()
        result = multi_func_all_derivatives(funcs, in_vars, in_vals)

#### 5. Print result.out_vals to get vector of calculated values of all funcs each at (in_vars)
        print(result.out_vals)

#### 6. Print result.out_derv_matrix to get the Hessian matrix of partial derivatives evaluated at (in_vars)
        print(result.out_derv_matrix)

### Example
* For values:
        x = 3
        y = 4
        z = 5
        func_1 = x + 2 * y - z
        func_2 = x - exp(y) + z

#### Be the goal to calculate the partial all the derivatives of more than one function at once
0. Import multi_func_all_derivatives function
        from All_derivatives import multi_func_all_derivatives as multi_func_all_derivatives

1. Create a vector containing the input variables
        in_vars = [x,y,z]

2. Create a vector containing the variable values
        in_vals = [3,4,5]

3. Create a vector containing the functions to be evaluated
        func_1 = x + 2 * y - z
        func_2 = x - exp(y) + z
        funcs = [func_1,func_2]

4. Run multi_func_all_derivatives()
        result = multi_func_all_derivatives(funcs, in_vars, in_vals)

5. Print result.out_vals to get vector of calculated values of all funcs each at (in_vars)
        print(result.out_vals)

6. Print result.out_derv_matrix to get the Hessian matrix of partial derivatives evaluated at (in_vars)
        print(result.out_derv_matrix

# SOFTWARE ORGANIZATION:

Our directory structure is very similar to the one we outlined in milestone 1. Our package has two subpackages - AutoDiff/ and Extension/. The entirety of our forward mode implementation is contained in two modules, AutoDiff.py and All_derivatives.py. AutoDiff.py defines a class for AutoDiff objects which are used in the All_derivatives to calculate the values of both multiple functions and all of their partial derivatives. Additionally, the AutoDiff subpackage also has a test subpackage that includes tests for both the AutoDiff and All_derivatives modules. The tests included in our test module are automatically run by TravisCI upon each commit to the remote repository. We didn’t experienced any issues with this testing method.

Our second subpackage, Extension, contains our application of autodifferentiation to option pricing. Importing from its sibling directory - AutoDiff, DeltaHedgePi.py uses the AutoDiff class and its overloaded functions to calculate deltas for European stock options. We use an html scraper to get live stock price data and then calculate the desired qualities using our own functions and methods.

Finally, all of our documentation is contained in a directory called docs. We decided to distribute our package on PyPI using a traditional setup.py script. We have also included a requirements.txt file in the package as our extension has quite a lot of dependencies. Generally speaking, we didn't stray to far from how we initially planned to structure our package.

# IMPEMENTATION:

Our DeltaPi software provides the user with a package he or she can use to evaluate functions of multiple variables at a specified point while at the same time evaluating the partial derivatives of the function at that same point. A class is provided to perform simple calculations, and two functions are provided to handle vector inputs and outputs.
DeltaPi performs automatic differentiation via the algorithim known as forward mode.

Our strategy has been to create input-variable objects in python that once defined may be used in python calculations as if they were standard python variables of type float. However, when calculations are done with these AutDiff objects, a partial derivative with respect to one of the input variables is calculated and carried along. The object representing the result then contains an attribute that contains the value of the function at the specified point (val) and an attribute that contains a partial derivative with respect to one of the input variables (derv). Which partial derivative is calculated is specified by the user by intiating the derv attribute of one input variable to 1 and the derv attributes of the other input variables to 0.  The calculation can then be repeated to get partial derivatives with respect to other input variables by changing which input variable’s derv attribute is intiated to 1. A function *all_derivatives( )* is supplied to automate this process.

*The AutDiff( ) class*

Most of the functionality of our DeltaPi package is implemented in a single class called AutoDiff( ). Code for this class can be found in the module 'AutoDiff.py' in the *code* directory. It has two attributes, self.val and self.derv, that respectively hold the value of the function being evaluated and the partial derivative of the function being evaluated up to some point in the calculation.

Autodiff() has many methods. Each is short and designed to replace a standard mathematical operation. Thus, for example there is a method \_\_add\_\_ that will overload the standard addition operator (+) and thus specify how objects of type AutoDiff will be added to scalar values or to each other. And similar methods are included to overload the other standard mathematical operators (- * / ^ +). Also, methods are included to replace many
numpy elemenatry functions so that these functions may be used as well in any calculations involving AutoDiff objects. Two functions are included separately outside the AutoDiff class. They are LogN and Logist, as these
functions are not implemented in numpy, they had to overloaded with a diferent approach. A Table of the functions
and operations overloaded in the AutoDiff module is shown below:

| Arithmetic     | Power    | Log     | Urnary  | Trig    | Hyper   | Special  | Comparison |
| :------------  | :------- | :------ | :------ | :------ | :------ | :------- | :--------: |
| Addition       | $x^{a}$  | log     | neg     | sin     | sinh    | logistic |    >       |
| subtraction    | $a^{x}$  | log2    | pos     | cos     | cosh    |          |    <       |
| Multiplication | $y^{x}$  | log10   |         | tan     | tanh    |          |    =       |
| Division       | $e^{x}$  | logN    |         | arcsin  | arcsinh |          |    <=      |
|                |          |         |         | arccos  | acrcosh |          |    >=      |
|                |          |         |         | arctan  | acrtanh |          |    !=      |

Table 2: AutoDiff class supported functions

Central to all Autodiff( ) methods is the principle that each method returns a new AutDiff object that will represent the next stage in the function’s evaluation. For example, if x is an AutDiff object, and it is multiplied by the scalar 2 (x\*2), then x’s \_\_mul\_\_ method is called, and it returns a new object that will represent x\*2. The AutoDiff.\_\_mul\_\_ method performs this calculation as follows. It assumes that 2 is another object of type AutoDiff, and trys to multiply the two AutoDiff objects together. It finds that 2 is not an AutoDiff object: it falls to its except block, and then it makes two calculations: 2 * x.val and 2 * x.derv and stores these values in the returned object’s val and derv attributes. If the 2 had been to the right of x (2*x), then the \_\_rmul\_\_ method would have been called, and it would have executed the same operation.

More subtle is the case where two AutoDiff objects, say x and y, are multiplied together x\*y. Here x’s \_\_mul\_\_ method is called. It finds that the two objects are instances of AutoDiff, and it 1) multiplies x.val and y.val together and stores the result as the returned object’s val attribute, and 2) calculates (x.derv\*y.val + y.derv\*x.val) and store this value as the returned object’s derv attribute. Notice in the second calculation the derivatives from both objects x and y are carried along as a sum. Recall, however, that the derv value of either x or y will have been initiated to 0, so one term in the sum (x.derv\*y.val + y.derv\*x.val) will always evaluate to 0. In this way, the algorithm can automatically choose the proper derivative to carry along for subsequent calculations (here either that of x or that of y) when two Autodiff objects collide. This same approach has been used generally to  overload the division and power operators as well.

An example of a elementary mathematical function implemented in AutoDiff( ) is the exponential function. This works as follows. For a given AutoDiff object, say x, when exp(x) is executed, x’s exp() function is called on itself. It uses numpy’s exp() function to evaluate np.exp(x.val) and store this value in the return object’s val attribute. It then evaluates the derivative of exp(x.value) —which in this case is just exp(x.value)— and stores this value in the return object’s derv attribute. This approach has been used generally for all the elementary functions.

Autdiff objects are fully functional replacements for python float variables such that once the AutoDiff class is defined, these objects can be used to perform forward automatic differentiation on any multi-input, single output function at any specified point.

*The all_derivatives() function*

This function allows the user to calculate all partial derivatives of a given function at once without having to initiate the derv attributes of the input variables several times manually. It operates by evaluating the desired function once for each input variable and during each run altering which variable's derv attribute is set to 1, while the other's derv attributes are set to 0. It takes as input: the function to be evaluated, a list of independent variables, and the values of these variables at the point desired. It returns  the value of the input function at the desired point, and an array of partial derivative one for each inpedendent variable. Since the code for this function is not long it is listed below. It takes advantage of the AutoDiff( ) class.

    def all_derivatives(func, in_vars, point):
        '''
        Function that calculates the value of the input function at the input point and all its partial derivatives

        :params func: (function) function to evaluate whose input is a list of variables
                in_vars: (list)  list of variables to use when calculating the values of the input function
                point: (list)    list of values of each input variable at the input point

        :returns: out_val (scalar) the value of the input function at the input point
                  out_dervs (numpy array , 1 dimensional) an array of the 1st derivative values of each
                  input variable at the input point

        :dependencies: requires class AutoDiff from module AutoDiff, package numpy imported ad np

        Example:

        f(x,y) = x^2 + y^2

        def func(vars):
            return vars[0]**2 + vars[1]**2

        a, b = all_derivatives(func, ['x','y'], [3,2])
        print(a, b)
        13.0, [6.0, 4.0]

        '''
        auto_diff_vars = []   # list to hold AutoDiff objects, one for each var in in_vars
        out_dervs =[]         # list to hold derivative values, one for each var in in_vars

        # Make AutoDiff objects
        for var, pt in zip(in_vars, point):
            var = AutoDiff(pt, 0)
            auto_diff_vars.append(var)

        # Calculate the value of func at the input point
        f = func(auto_diff_vars)
        out_val = f.val

        # Calculate the partial derivatives of func at the input point
        # Go through AutoDiff objects in auto_diff_vars
        # and during each pass change the derv value of 1 variable to 1, while others are held at 0
        # and calculate the partial derivative
        for i in range(len(auto_diff_vars)):
            auto_diff_vars[i].derv = 1
            f = func(auto_diff_vars)
            out_dervs.append(f.derv)
            auto_diff_vars[i].derv = 0

        return out_val, np.array(out_dervs)

*The multi_func_all_derivatives() function*

This function handles vector inputs and outputs. It allows the user to specify: a vector of functions, a vector of input variables, and a vectors of values defining a specific point. It the returns: a vector of output values (one for each function evaluated at the specified point), and a Jacobian matrix of partial derivatives containing partial derivatives of all functions evaluated at the specified point. Consider the equation below as an example:

\begin{equation}
\begin{pmatrix} f(x,y) \\ g(x,y)\end{pmatrix} =
\begin{pmatrix} 2xy \\ x+y^2 \end{pmatrix} at \begin{pmatrix} x=a \\ y=b \end{pmatrix}
\end{equation}

Here the multi_func_all_derivatives() function would return:
1) out_vals, a vector of the values of each function after evaluation $(f(a,b), g(a,b))$
2) out_derv_matrix, a matrix containing the partial derivatives evaluated at the point (a, b)

Since the code for this function is not long it is listed below. It takes advantage of the
AutoDiff( ) class and the all_derivatives( ) function:

    def multi_func_all_derivatives(functions, in_vars, point):
        '''
        Function that calculates the value of all input functions at the input point and all their
        partial derivatives producing a numpy array of output values and a numpy 2D array (matrix)
        of partial derivatives, the Jacobian

        :params func: (functions) list of function to evaluate whose input is a list of variables
                in_vars: (list)   list of variables to use when calculating the values of the input functions
                point: (list)     list of values of each input variable at the input point

        :returns: out_vals (numpy array 1 dimensional) the values of the input functions at the input point
                  out_dervs_matrix (numpy array 2 dimensional) a matrix of the 1st derivative values of each
                  input variable at the input point, the Jacobian.

        :dependencies: requires class AutoDiff from module AutoDiff, function all_derivatives from this module,
                                All_derivatives, and the package numpy imported as np

        Example:

        f(x,y) = x^2 + y^2

        (x,y) = x^2*y^2

        def func1(vars):
            return vars[0]**2 + vars[1]**2

        def func2(vars):
            return vars[0]**2*vars[1]**2

        a, b = multi_func_all_derivatives([func1,func2] , ['x','y'], [3,2])
        print(a, b)
        [13. 36.] [[ 6.  4.], [24. 36.]]

        '''
        out_vals = []
        out_dervs_matrix =[]

        # for each function in functions list
        # run all_derivatives and store output
        for func in functions:
            val, dervs = all_derivatives(func, in_vars, point)
            out_vals.append(val)
            out_dervs_matrix.append(dervs)

        return np.array(out_vals), np.array(out_dervs_matrix)

*Testing*

We have implemented a series of tests to test the AutoDiff( ) class and the all_derivatives and multi_func_all_derivatives functions. There are 67 tests in all. They are designed to be run by *pytest* and to provide full code coverage. They can be found in the modules 'test_AutoDiff.py and 'test_all_derivatives.py'. Travis CI and codecov inidicate that all tests are passed by our code, and our code coverage is very high.

# EXTENSION: OPTIONS HEDGING

In the financial markets, large institutions (e.g. banks, asset managers) hold complex portfolios with a variety of financial instruments. While calculating the value of such portfolios can be done fairly easily with simulations and aggregation of market pricing, the sensitivities of these portfolios to changes in price, volatility and interest rates in the market can be a lot more difficult to calculate and come with an accuracy vs. efficiency trade off.

Given the time and the scope of this project we chose to focus on a subset of the financial instruments and potential end users.  Specifically, we aim to extend our DeltaPi package into a second package HedgeDeltaPi that would be useful to equity options traders as they estimate sensitivities  of their options portfolios and purchase underlying assets (delta-hedge) to minimize their volatility. Accurate and efficient estimate of these sensitivities would allow options traders to buy the right amounts of options to hedge their stock positions from big swings in the asset prices (more on this later).

Here we will focus on vanilla, European, non-dividend paying stock options that can only be exercised on specified date of their expiration as opposed to American options that can be exercised during any time up until the expiration date.

### What are Financial Options?

A call option is a right but not an obligation to buy an underlying asset (in our case stock) at a given price (strike price) on a given date (expiration date).  Note from the plot below that the option has no intrinsic value below the strike price of USD100.  However, as the stock reaches that value and climbs higher, the intrinsic value of the option is the difference between USD100 and the current price (in the >100 region). That makes sense because we can buy the stock at a 100 and sell it at a price that is now >100, netting a profit that is now the intrinsic value of the option. When the stock price crosses the strike value like this, the option goes from being out-of-the-money to being in-the-money.

$$\text{Value of European Call Option}$$

$$\text{source: www.quantopia.net/wp-content/uploads/2013/03/EuroCallRates.png}$$

The plot above illustrates the value progression of a European call option. Essentially, call value (blue curve) is made up of two components: 1) intrinsic value and 2) time value. The green line is its intrinsic value, that goes from 0 when the option is out of the money to climbing with the stock price once it crosses over the strike value at USD100.  The orange curve is the time value of the option that takes into account how far we are from the expiration date (since we cannot exercise the option until it gets there)  and how close we are to the strike price, the discount rate (risk free rate, usually a 10 year Treasury) and the overall volatility of the share price. Black Scholes equation for pricing options takes all of these variables into account (more on this later).

A put option is a right but not an obligation to sell an underlying asset (in our case stock) at a given price (strike) on a given date (expiration).  The plot below shows that the put option has no intrinsic value until the stock price drops below the strike. Then the intrinsic value of the option is the profit one would net by buying the stock <100 and selling it at a strike price of USD100. Similarly to the call option, the total value of the put option is a combination of intrinsic value and time value that takes into account how far the stock is from the strike, time to expiration and stock volatility discounted by the risk free rate.

$$\text{Value of European Put Option}$$

$$\text{source: www.quantopia.net/wp-content/uploads/2013/03/EuroPutRates.png}$$

The classical approach to valuing options is with Black-Scholes Equation that describes the option price overtime as follows:

$$\frac{\partial V}{\partial t}+\frac{1}{2}\sigma^2 S^2\frac{\partial^2 V}{\partial S^2}+r S\frac{\partial V}{\partial S} -r V = 0$$

where $V$ is the value of the option, $t$ is time, $\sigma$ is volatility of the underlying asset, $S$ is the price of the underlying asset and $r$ is the risk free rate (Black and Scholes, 1973).

Further, Black-Sholes-Merton formula for estimating price of the European Call option is as follows:

$$C(S_0,t)=S_0N(d_1)-Ke^{-r(T-t)}N(d_2)$$

and similarly the put option value is as follows:

$$P(S_0,t)=Xe^{-r(T-t)}N(-d_2)-S_tN(-d_1)$$

where $S_0$ is the stock price, $C(S_0,t)$ is a price of the call option and $P(S_0,t)$ is price of a put option as a function of stock price and time, $K$ is the strike prices, $(T-t)$ is time to maturity represented in years and $N(d_1)$ and $N(d_2)$ are  cumulative distribution functions for a standard normal distribution (Merton, 1973).

$$d_1=\frac{ln(\frac{S_t}{X})+(r+\frac{\sigma^2_s}{2})(T-t)}{\sigma_s\sqrt{(T-t)}}$$

$$d_2=\frac{ln(\frac{S_t}{X})+(r-\frac{\sigma^2_s}{2})(T-t)}{\sigma_s\sqrt{(T-t)}}=d_1-\sigma_s\sqrt{(T-t)}$$

$$N(d_1)=\int_{-\infty} ^{d_1}f(u)du=\int_{-\infty}^{d_1}\frac{1}{\sqrt{2\pi}}e^{\frac{-u^2}{2}}du$$

$$\dot{N(d_1)}=\frac{\partial N(d_1)}{\partial d_1}=\frac{1}{\sqrt{2\pi}}e^{-\frac{d_1^2}{2}}$$

We can solve for both put and call deltas and obtain a closed form solution in the form.

$$\Delta_{call}=\frac{\partial C_t}{\partial S_t}=N(d_1)+S_t\frac{\partial N(d_1)}{\partial S_t}-Xe^{-r(T-t)}\frac{\partial N(d_2)}{\partial S_t}= N(d_1)$$

$$\Delta_{put}=\frac{\partial P_t}{\partial S_t}=Xe^{-r(T-t)}\frac{\partial N(-d_2)}{\partial S_t}-N(-d_1)-S_t\frac {\partial N(-d_1)}{\partial S_t}=N(d_1)-1$$

Note, that delta is positive for call options and negative for put options, which makes sense.  As the price of the stock goes up so does the value of call option (i.e. right to buy at potentially lower price).  As the price of the stock goes up, the value of the put option goes down (i.e. right to sell at potentially lower price is not valuable). The plot below illustrates this by plotting call/put deltas vs. the 'moneyness' of the option (OTM=Out of the Money, ATM = At the Money and ITM = In the Money)

$$\text{source: https://www.optiontradingtips.com/images/delta-vs-moneyness.png}$$

We can also obtain closed form solution for first order partial derivative w.r.t. volatility - Vega (note its the same for both put and call options):

$$v=\frac{\partial{C_t}}{\partial\sigma}=\frac{\partial{P_t}}{\partial\sigma}=S_t\sqrt{T-t} N(d_1)$$

However, it is important to keep in mind that the  𝜎  (volatility) of the option is not static overtime (i.e. the volatility surface is not flat). Thus, rather then focusing on simple delta above, our package will take this into account and calculate minimum variance delta that minimizes volatility of a delta neutral portfolio (i.e. more accurate than static delta). For local volatility models, for options that are closer to being in the money, several papers ( Derman et al (1995), and Coleman et al (2001)) have shown that:

$$\Delta_{MV}=\Delta_{BS}+v_{BS}\frac{\partial{\sigma_{imp}}}{\partial{K}}$$

where $\Delta_{BS}$ $v_{BS}$ are Black Scholes delta and vega respectively, and $\sigma_{imp}$ is the value of implied volatility.  To estimation equations of implied volatility have proven to be most accurate (Isengildina-Massa, et. al 2007).  We will consider them in this package.

1) Implied volatility equation proposed by Bharadia et al. (1996):

$$\sigma_{imp}\approx\sqrt{\frac{2\pi}{(T-t)}}\frac{C-(S-K)/2}{S-(S-K)/2}$$

2) Implied volatility equation  from Corrado et. al., (1996):

$$\sigma_{imp}\approx\sqrt{\frac{2\pi}{(T-t)}} \frac{1}{S+K}(C-\frac{S-K}{2}+\sqrt{(C-\frac{S-K}{2})^2-\frac{(S-K)^2}{\pi}})$$

DeltaHedgePi will leverage DeltaPi package to calculate the first derivative of $\sigma_{imp}$ and will calculate $\Delta_{BS}$ and $v_{BS}$ using market data inputed by the traders and checking it against the currently available live market data. The package will output the following:

1) Three delta values:

      a. classic Black Scholes

      b.  Black Scholes + Bharadia et al.

      c.  Black Scholes + Corrado et al.

2) Depending on the trader's position and type of option, the package will output what action the trader needs to take in order to delta hedge their position.

3) Two visualization for volatility surfaces for the non static approaches.

   a.  Black Scholes + Bharadia et al.

   b.  Black Scholes + Corrado et al.

The goal of this package is to provide the traders with a more accurate delta estimations for near the money options and make the process of delta hedging easier.

### What is Delta Hedging?

In the simple case of vanilla, non dividend paying, European call/put options above, delta hedging refers to buying or selling specific amounts of underlying stock to neutralize the impact of the stock price moves on the options portfolio.  Let us consider the following example:

Consider the following example. Let's supposed we have a stock XYZ that currently has a price of USD100 and the bank sold call options (a while ago before the stock price run up) for 1000 shares with strike USD10.  Thus, the buyer of our option has the right but not obligation to buy 1000 shares of XYZ at just USD10 on expiration date (right now the option is deep in the money). If the delta of this call option is 0.5, this means that for every USD1 increase in stock price, the value of the call option goes up USD0.50 (to the holder).  To execute a delta hedge, the bank should buy 1000*.5=500 shares of XYZ. Now if the stock price goes down by USD1, the bank will lose USD500 on the stock position but will gain 0.5x1000=USD500 on its short call option position. Thus, the impact is neutral.

Keep in mind that delta is not static as the slope of option price vs. stock price is usually non linear. This is why accurate and frequent estimates of delta are so important (Lee, 2008).

In summary, HedgeDeltaPi will apply automatic differentiation from DeltaPi to compute deltas using 3 approaches described above: 1) Pure Black Scholes, 2) Black Scholes + Implied Vol Simple, 3) Black Scholes + Implied Vol More Complex. The package will then produce a buy/sell recommendation and the quantity of shares in the underlying assets required to successfully delta hedge the traders' position.  The package will also produce a volatility surface visualizations using the latter 2 approaches (classic Black Scholes volatility surface is flat).

**Note:** The package is pulling live options data from yahoo finance to confirm current market value of call and put options respectively and realistic expiration dates and strike values. Thus, **the package assumes that the traders are analyzing real options that would practically trade in the market** (i.e. not wildly mis-priced/unrealistically structured options that wouldn't occur in real life). If the user would like to run the package on real options for practice here are some of the valid options to try:

### How to Use the Delta Hedge Pi Extension

#### 0. Initialize extension in the installed directory
        pythonw DeltaHedgePi.py

#### 1. Enter your option type or exit by selecting 1, 2 or 3 (number)
        Please Select Type of Option to Evaluate
        1) Exit
        2) Puts
        3) Calls

#### 2. Enter the ticker for the stock you want to delta hedge (string)
        Please Enter Ticker

#### 3. Enter the strike price (number)
        Please Enter Strike

#### 4. Enter the expiration year (number)
        Please Enter Expiration Year

#### 5. Enter the expiration month (number)
        Please Enter Expiration Month

#### 5. Enter the expiration day (number)
        Please Enter Expiration Day

#### 6. Confirm you want to delta hedge entering 'y'
        Would You Like to Delta Hedge Your Position: y/n?

#### 6. Enter the number of stocks you have long/short (positive number for long, negative number for short)
        How many units? Enter + values for long and - values for short

### Example

Scenario:
* Company: Apple
* Ticker: AAPL
* Expiration: 1/24/2020
* Option type: Puts
* Strike: 300
* Ownershio: 10,000 units

        [INPUT]     pythonw DeltaHedgePi.py
        [OUTPUT]    Please Select Type of Option to Evaluate
                    1) Exit
                    2) Puts
                    3) Calls
        [INPUT]     2
        [OUTPUT]    Please Enter Ticker
        [INPUT]     aapl
        [OUTPUT]    Please Enter Strike
        [INPUT]     300
        [OUTPUT]    Please Enter Expiration Year
        [INPUT]     2020
        [OUTPUT]    Please Enter Expiration Month
        [INPUT]     1
        [OUTPUT]    Please Enter Expiration Day
        [INPUT]     24
        [OUTPUT]    Black Scholes Delta:  0.20314624495527905
                    negative number has been input to the sqaure root function
                    Bharadia delta:  0.20519111437373902
                    Corrado delta:  0.20416867966450905
                    Would You Like to Delta Hedge Your Position: y/n?
        [INPUT]     y
        [OUTPUT]    How many units? Enter + values for long and - values for short
        [INPUT]     10000
        [OUTPUT]    According to Black Scholes you need to  sell short  2031  shares of  aapl
                    According to Bharadia apporach you need to  sell short  2051  shares of  aapl
                    Accoding to Corrado approach you need to  sell short  2041  shares of  aapl

```python
#Possible Imputs for Practice:
#Run during market Open (weekdays 9:30-4)

#inputs:
#demo tickers:
#ticker='GE'
#exp_year=2020
#exp_month=1
#exp_day=17
#exp_date=str(str(exp_month)+'/'+str(exp_day)+'/'+str(exp_year))
#op_type='puts'
#strike=8

#ticker='aapl'
#exp_year=2020
#exp_month=1
#exp_day=24
#exp_date=str(str(exp_month)+'/'+str(exp_day)+'/'+str(exp_year))
#op_type='puts'
#strike=300

#ticker='nflx'
#exp_year=2021
#exp_month=1
#exp_day=15
#exp_date=str(str(exp_month)+'/'+str(exp_day)+'/'+str(exp_year))
#op_type='calls'
#strike=350
```

Since we are pulling live data form yahoo finance, the package is dependent on yahoo finance API working properly. **If it is yahoo finance is not working, the package would not work.** This is why, for more practical application our package should be linked to more reliable platforms like Bloomberg, which is what most of the traders use.  Since we cannot afford Bloomberg, we built the package with free yahoo finance but it is less reliable.

# **REFERENCES:**

Atilim Baydin. Automatic Differentiation in Machine Learning: a Survey. Journal of Machine Learning Research, 2018.

Fischer Black, & Myron Scholes, M. The Pricing of Options and Corporate Liabilities. Retrieved November 1, 2019, from http://www.jstor.org/stable/1831029

George F. Corliss. Application of differentiation arithmetic, volume 19 of Perspectives in Computing, pages 127–48. Academic Press, Boston, 1988.

Andreas Griewank and Andrea Walther. Evaluating Derivatives: Principles and Techniques of Algorithmic Differentiation. Humboldt-Universität zu Berlin, Berlin, Germany. Second Edition, 2008.

Michael Heath. Scientific Computing: An Introductory Survey. Society for Industrial and Applied Mathematics. 8(6):367, 2018.

Christina Homescu. Adjoints and automatic (algorithmic) differentiation in computational finance. arXiv:1107.1831v1 10 Jul 2011.

Olga Isengildina-Massa,Charles E. Curtis, Jr., William C. Bridges, Jr. and Minhuan Nian. Accuracy of Implied Volatility Approximations Using “Nearest-to-the-Money” Option Premiums. 2007.  Retrieved on November 20, 2019, from  https://ageconsearch.umn.edu/record/34927/files/sp07is02.pdf.

Max E. Jerrell. Automatic differentiation and interval arithmetic for estimation of disequilibrium models. Computational Economics, 10(3):295–316, 1997.

Cheng-Few Lee, 'Handbook on Quantitative Finance and Risk Management.' Rutgers University. 2008.

Robert Merton. Theory of Rational Option Pricing. Retrieved on November 1, 2019, from http://www.jstor.org/stable/3003143

David Sondak. Lectures 10 - 11. Harvard University. CS207 Fall 2019.

Eric Sunnegardh and Ludvig Lamm. "Efficient Sensitivity Analysis Using Algorithmic Diferentation in Financial Applications". 2015.