**Notas para contenedor de docker:**

Comando de docker para ejecución de la nota de forma local:

nota: cambiar `dir_montar` por la ruta de directorio que se desea mapear a `/datos` dentro del contenedor de docker.

```
dir_montar=<ruta completa de mi máquina a mi directorio>#aquí colocar la ruta al directorio a montar, por ejemplo:
#dir_montar=/Users/erick/midirectorio.
```

Ejecutar:

```
$docker run --rm -v $dir_montar:/datos --name jupyterlab_prope_r_kernel_tidyverse -p 8888:8888 -d palmoreck/jupyterlab_prope_r_kernel_tidyverse:3.0.16

```

Ir a `localhost:8888` y escribir el password para jupyterlab: `qwerty`

Detener el contenedor de docker:

```
docker stop jupyterlab_prope_r_kernel_tidyverse
```

Documentación de la imagen de docker `palmoreck/jupyterlab_prope_r_kernel_tidyverse:3.0.16` en [liga](https://github.com/palmoreck/dockerfiles/tree/master/jupyterlab/prope_r_kernel_tidyverse).

---

Para ejecución de la nota usar:

[docker](https://www.docker.com/) (instalación de forma **local** con [Get docker](https://docs.docker.com/install/)) y ejecutar comandos que están al inicio de la nota de forma **local**.

O bien dar click en alguno de los botones siguientes:

[](https://mybinder.org/v2/gh/palmoreck/dockerfiles-for-binder/jupyterlab_prope_r_kernel_tidyerse?urlpath=lab/tree/Propedeutico/Python/clases/2_calculo_DeI/1_aproximacion_a_derivadas_e_integrales.ipynb) esta opción crea una máquina individual en un servidor de Google, clona el repositorio y permite la ejecución de los notebooks de jupyter.

[](https://repl.it/languages/python3) esta opción no clona el repositorio, no ejecuta los notebooks de jupyter pero permite ejecución de instrucciones de Python de forma colaborativa con [repl.it](https://repl.it/). Al dar click se crearán nuevos ***repl*** debajo de sus users de ***repl.it***.

## Se sugiere apoyar esta nota con la lectura de los capítulos 5 y 6 del libro de texto de J. Kiusalaas "Numerical Methods in Engineering with Python 3".

# Función

Una función, $f$, es una regla de correspondencia entre un conjunto nombrado dominio, $D_f$ y otro conjunto nombrado codominio, $C_f$.

Notación: $f: A \rightarrow B$ es una función de un conjunto $\text{dom}f \subseteq A$ en un conjunto $B$.

---

**Observación**

$\text{dom}f$ (el dominio de $f$) podría ser un subconjunto propio de $A$, esto es, algunos elementos de $A$ y otros no, son mapeados a elementos de $B$.

---

**Ejemplos**

* La regla de correspondencia que asocia a cada estudiante su clave única.

* La regla de correspondencia que asocia a cada persona una casilla para votar en elecciones.

* $f: \mathbb{R} \rightarrow \mathbb{R}$ con $f(x) = x^2$.

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

```python
x = np.linspace(-1,1,100) #100 puntos equidistantes entre -1,1
```

```python
y = lambda x: x**2
```

```python
y_eval = y(x)
```

```python
plt.plot(x,y_eval)
plt.title('y=x^2')
plt.show()
```

# Derivada de una función

Consideremos en lo que sigue $f: \mathbb{R} \rightarrow \mathbb{R}$.

$f$ es diferenciable en $x_0 \in (a,b)$ si $\displaystyle \lim_{x \rightarrow x_0} \frac{f(x)-f(x_0)}{x-x_0}$ existe y escribimos:

$$f^{(1)}(x_0) = \displaystyle \lim_{x \rightarrow x_0} \frac{f(x)-f(x_0)}{x-x_0}.$$

$f$ es diferenciable en $[a,b]$ si es diferenciable en cada punto de $[a,b]$. Análogamente definiendo la variable $h=x-x_0$ se tiene:

$f^{(1)}(x_0) = \displaystyle \lim_{h \rightarrow 0} \frac{f(x_0+h)-f(x_0)}{h}$ que típicamente se escribe como:

$$f^{(1)}(x) = \displaystyle \lim_{h \rightarrow 0} \frac{f(x+h)-f(x)}{h}.$$

---

**Comentario**

Si $f$ es diferenciable en $x_0$ entonces $f(x) \approx f(x_0) + f^{(1)}(x_0)(x-x_0)$. Gráficamente:

---

**Notación:** $\mathcal{C}^n([a,b])=\{\text{funciones } f:\mathbb{R} \rightarrow \mathbb{R} \text{ con } n \text{ derivadas continuas en el intervalo [a,b]}\}$.

---

**Observación**

En la definición anterior se calculan límites los cuales pueden calcularse con el paquete *SymPy*.

```python
import sympy
```

**Límite de $\frac{\sin(x)}{x}$ para $x \rightarrow 0$:**

```python
x = sympy.Symbol("x")
```

```python
quotient = sympy.sin(x)/x
```

```python
sympy.limit(quotient,x,0)
```

$\displaystyle 1$

---

**Límite de $\frac{\cos(x+h) - \cos(x)}{h}$ para $h \rightarrow 0$:**

```python
x, h = sympy.symbols("x, h")
```

```python
quotient = (sympy.cos(x+h) - sympy.cos(x))/h
```

```python
sympy.limit(quotient, h, 0)
```

$\displaystyle - \sin{\left(x \right)}$

Lo anterior corresponde a la **derivada de $\cos(x)$**:

```python
x = sympy.Symbol("x")
```

```python
sympy.cos(x).diff(x)
```

$\displaystyle - \sin{\left(x \right)}$

**Si queremos evaluar la derivada podemos usar:**

```python
sympy.cos(x).diff(x).subs(x,sympy.pi/2)
```

$\displaystyle -1$

**Otra forma:**

```python
sympy.Derivative(sympy.cos(x), x)
```

$\displaystyle \frac{d}{d x} \cos{\left(x \right)}$

```python
sympy.Derivative(sympy.cos(x), x).doit_numerically(sympy.pi/2)
```

$\displaystyle -1.0$

# Errores absolutos y relativos de una aproximación

Si `aprox` es mi cantidad con la que aproximo a mi objetivo `obj` entonces el error absoluto de `aprox` y el error relativo de `aprox` es:

$$ErrAbs(\text{aprox}) = |\text{aprox} - \text{obj}|.$$

$$ErrRel(\text{aprox}) = \frac{ErrAbs(\text{aprox})}{|\text{obj}|}.$$

---

**Observación**

* Obsérvese que `obj` debe ser distinto de cero para que el error relativo esté bien definido.

* si $ErrRel(aprox) \approx 10^{-k}$ se dice que `aprox` aproxima a `obj` con alrededor de $k$ dígitos correctos. Por ejemplo si $k=3$ entonces la cantidad `aprox` aproxima a la cantidad `obj` con alrededor de $3$ dígitos de precisión.

---

# Aproximación a una función por el teorema de Taylor

Las fórmulas de aproximación a las derivadas por diferencias finitas y a integrales definidas en un intervalo por las reglas de cuadratura Newton-Cotes pueden obtenerse con los **polinomios de Taylor** presentes en el teorema del mismo autor, el cual, bajo ciertas hipótesis nos proporciona una expansión de una función alrededor de un punto. Otras opciones son con polinomios de Lagrange, ver [Lagrange_polynomial](https://en.wikipedia.org/wiki/Lagrange_polynomial). El teorema de Taylor es el siguiente:

Sea $f \in \mathcal{C}^n([a,b])$, $f^{(n+1)}$ existe en [a,b]. Si $x_0 \in [a,b]$ entonces $\forall x \in [a,b]$ se tiene: $f(x) = P_n(x) + R_n(x)$ donde:

$$P_n(x) = \displaystyle \sum_{k=0}^n \frac{f^{(k)}(x_0)(x-x_0)^k}{k!} \quad (f^{(0)} = f)$$ y $$R_n(x) = \frac{f^{(n+1)}(\xi_x)(x-x_0)^{(n+1)}}{(n+1)!}$$ con $\xi_x$ entre $x_0, x$ y $x_0$ se llama centro.

## Ejemplo

Aproximemos a la función $\frac{1}{x}$ en el intervalo $[1,2]$ con polinomios de Taylor de orden $n$ con $n \in \{0,1,2\}$ con centro en $x_0=1.5$. Los polinomios de Taylor son:

$$P_0(x) = f(x_0) = \frac{2}{3} \quad \text{(constante)}$$

$$P_1(x) = f(x_0) + f^{(1)}(x_0)(x-x_0) = \frac{2}{3} - \frac{1}{x_0^2}(x-x_0) =\frac{2}{3} - \frac{1}{1.5^2}(x-1.5) \quad \text{(lineal)}$$

$$P_2(x) = f(x_0) + f^{(1)}(x_0)(x-x_0) + \frac{f^{(2)}(x_0)(x-x_0)^2}{2} = \frac{2}{3} - \frac{1}{x_0^2}(x-x_0) + \frac{1}{x_0^3}(x-x_0)^2 = \frac{2}{3} -\frac{1}{1.5^2}(x-1.5) + \frac{1}{1.5^3}(x-1.5)^2 \quad \text{(cuadrático)}$$

---

**Ejercicio**

Graficar la función y los polinomios constante, lineal y cuadrático en una sola gráfica con `matplotlib` en el intervalo [1,2]. ¿Cuánto es la aproximación de los polinomios en x=1.9? Calcula el error relativo de tus aproximaciones.

---

**Comentario**

Otras aproximaciones a una función se pueden realizar con:

* Interpoladores polinomiales (representación por Vandermonde, Newton, Lagrange).

---

# Diferenciación numérica por diferencias finitas

Las fórmulas de diferencias finitas pueden obtenerse con el teorema de Taylor. Por ejemplo:

Sea $f \in \mathcal{C}^1([a,b])$ y $f^{(2)}$ existe y está acotada $\forall x \in [a,b]$ entonces, si $x+h \in [a,b]$ con $h>0$ por el teorema de Taylor se tiene:

$$f(x+h) = f(x) + f^{(1)}(x)h + f^{(2)}(\xi_{x+h})\frac{h^2}{2}$$ con $\xi_{x+h} \in [x,x+h]$

Y al despejar $f^{(1)}(x)$ se tiene la **aproximación por diferencias hacia delante a la primera derivada de $f$**:

$$f^{(1)}(x) = \frac{f(x+h)-f(x)}{h} - f^{(2)}(\xi_{x+h})\frac{h}{2}$$

---

**Observación**

La aproximación por diferencias finitas a la primer derivada de la función tiene un error de orden $\mathcal{O}(h)$ por lo que una elección de $h$ igual a $.1 = 10^{-1}$ generará aproximaciones con alrededor de un dígito correcto.

---

Así también pueden obtenerse la versión centrada y aproximaciones a la segunda derivada de $f$:

**Aproximación por diferencias hacia delante para la segunda derivada**

$$\frac{d^2f(x)}{dx} \approx \frac{f(x+2h)-2f(x+h)+f(x)}{h^2}$$

**Aproximación por diferencias centradas a la primer y segunda derivada**

$$ \frac{df(x)}{dx} \approx \frac{f(x+h)-f(x-h)}{2h}$$

$$ \frac{d^2f(x)}{dx} \approx \frac{f(x+h)-2f(x)+f(x-h)}{h^2}$$

**Interpretación geométrica de aproximación por diferencias centradas a la primer derivada de $f$:**

---

**Ejercicio**

Aproximar la primera y segunda derivadas de la función `arctan` con diferencias finitas centradas en el punto $x=0.5$

```python
def central_approx_diff(f,x,h=0.0001): #el parámetro h tiene un valor default
    df =(f(x+h) - f(x-h))/(2.0*h) #primera derivada
    ddf =(f(x+h) - 2.0*f(x) + f(x-h))/h**2 #segunda derivada
    return df,ddf
```

```python
import math
```

```python
#Ejemplo de llamada a función utilizando el parámetro de default de h=0.0001
x = 0.5 #punto donde se realizará la aproximación
df, ddf = central_approx_diff(math.atan, x)
print('Primera derivada:', df)
print('Segunda derivada:', ddf)
```

    Primera derivada: 0.7999999995730867
    Segunda derivada: -0.6399999918915711

```python
#Ejemplo de llamada a función utilizando h=1e-6
h = 1e-6
x = 0.5
df, ddf = central_approx_diff(math.atan, 0.5,h)
print('Primera derivada:', df)
print('Segunda derivada:', ddf)
```

    Primera derivada: 0.799999999995249
    Segunda derivada: -0.639877040242709

```python
#derivadas analíticas:
d = 1/(1+x**2)
dd = (-2*x)/(1+x**2)**2
print(d)
print(dd)
```

    0.8
    -0.64

```python
def relative_absolute_error(aprox, obj):
    if(np.abs(obj) > 0):
        return np.abs(aprox-obj)/np.abs(obj)
    else:
        return np.abs(aprox-obj)
```

```python
rel_err_df = relative_absolute_error(df, d)
```

```python
rel_err_df
```

    5.938860514476119e-12

```python
rel_err_ddf = relative_absolute_error(ddf, dd)
```

```python
rel_err_ddf
```

    0.00019212462076725195

---

**Comentario**

Otra forma de evaluar las aproximaciones realizadas es con módulos o paquetes de Python creados para este propósito en lugar de crear nuestras funciones como la de `relative_absolute_error`. En la siguiente celda instalamos el paquete [pytest](https://docs.pytest.org/en/latest/) y mostramos cómo evaluar la calidad de la aproximación con la función [approx](https://docs.pytest.org/en/latest/reference.html#pytest-approx) de este paquete.

---

```python
!pip3 install -q --user pytest
```

      WARNING: The scripts py.test and pytest are installed in '/home/propeuser/.local/bin' which is not on PATH.
      Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.

```python
from pytest import approx
```

```python
df == approx(d)
```

    True

```python
ddf == approx(dd)
```

    False

Y podemos usar un valor definido de tolerancia definido para hacer la prueba (por default se tiene una tolerancia de $10^{-6}$).

```python
ddf == approx(dd, rel=1e-3,abs=1e-3)
```

    True

---

**Ejercicios**

La diferenciación numérica por diferencias finitas **no es un proceso con una alta exactitud** pues los problemas del redondeo de la aritmética en la máquina se hacen presentes en el mismo. Como ejemplo de esta situación hágase el siguiente ejercicio 1.

1) **(Tarea) Realizar una gráfica de log(error relativo) vs log(h) (h en el eje horizontal) para aproximar la segunda derivada de $f(x)=e^{-x}$ en $x=1$ con $h \in \{10^{-16}, 10^{-14}, \dots , 10^{-1}\}$ y diferencias hacia delante. Valor a aproximar: $f^{(2)}(1) = e^{-1}$. Usar:**

$$\frac{d^2f(x)}{dx} \approx \frac{f(x+2h)-2f(x+h)+f(x)}{h^2}$$

2) **Crear un módulo con nombre `central_finite_derivative.py` en el que se tengan dos funciones de Python que aproximen la primera y segunda derivada de una función en un punto `x`. Ambas funciones reciben `fun`, `x` y `h` donde: `fun` es la función a calcularse su primera y segunda derivadas, `x` es el punto donde se realiza la aproximación y `h` es el parámetro de espaciado entre `x` y `x+h` igual a $h=10^{-6}$. La salida de cada función es un `float`.
Función de prueba: `math.atan` y `x=0.9`.**.

**Los nombres de las funciones y sus salidas son:**

| central_finite_derivative.py   | parámetros de entrada |salida|
|:---:|:---:|:---:|
| approx_first_derivative  | fun (function), x (float) ,h (float) | float|
| approx_second_derivative | fun (function), x (float), h (float)| float|

**3) (Tarea) Mismo ejercicio que 2) pero función de prueba: `math.asin` y `x=0.5`.**

---

## Diferenciación numérica en más dimensiones

La anterior aproximación por diferencias finitas también puede utilizarse para aproximar el gradiente de una función $f: \mathbb{R}^n \rightarrow \mathbb{R}$ considerando que:

$$\nabla f(x) =
\begin{array}{l}
\left[ \begin{array}{c}
\frac{\partial f(x)}{\partial x_1}\\
\vdots\\
\frac{\partial f(x)}{\partial x_n}
\end{array}
\right] = \left[
\begin{array}{c}
\displaystyle \lim_{h \rightarrow 0} \frac{f(x+he_1) - f(x)}{h}\\
\vdots\\
\displaystyle \lim_{h \rightarrow 0} \frac{f(x+he_n) - f(x)}{h}
\end{array}
\right]
\end{array} \in \mathbb{R}^n$$

con $e_i$ vectores canónicos (poseen 1 en la posición $i$ y cero en las restantes) para $i=1, \dots, n$.

---

**Observación**

El gradiente de una función como se definió arriba también es una función, de hecho: $\nabla f: \mathbb{R}^n \rightarrow \mathbb{R}^n$.

---

En este contexto el teorema de Taylor para el polinomio de grado 2 se puede escribir como: $$P_2(x) = f(x_0) + \nabla f(x_0)^T(x-x_0) + \frac{1}{2}(x-x_0)^T\nabla^2f(x_0)(x-x_0) $$

# Integración numérica

Las reglas o métodos por cuadratura nos ayudan a aproximar integrales con sumas de la forma:

$$\displaystyle \int_a^bf(x)dx \approx \displaystyle \sum_{i=0}^nw_if(x_i)$$

donde: $w_i$ es el peso para el nodo $x_i$. Los valores $f(x_i)$ se asumen conocidos.

Todas las reglas o métodos por cuadratura se obtienen con interpoladores polinomiales del integrando (por ejemplo usando la representación de Lagrange) o también con el teorema Taylor.

Se realizan aproximaciones numéricas por:
* Desconocimiento de la función en todo el intervalo $[a,b]$ y sólo se conoce en los nodos su valor.
* Inexistencia de antiderivada o primitiva del integrando. Por ejemplo:

$$\displaystyle \int_a^be^{-\frac{x^2}{2}}dx$$ con $a,b$ números reales.

Dependiendo de la ubicación de los nodos y pesos es el método de cuadratura que resulta:

* Newton-Cotes si los nodos y pesos son equidistantes como la regla del rectángulo, trapecio y Simpson (con el teorema de Taylor es posible obtener tales fórmulas).
* Cuadratura Gaussiana si se desea obtener reglas o fórmulas que tengan la mayor exactitud posible. Ejemplos de este tipo de cuadratura se tiene la regla por cuadratura Gauss-Legendre en [-1,1] o Gauss-Hermite para el caso de integrales en $[-\infty, \infty]$ con integrando $e^{-x^2}f(x)$.

---

**Observación**

Con *SymPy* también es posible calcular integrales definidas o indefinidas.

**Integral indefinida de $\sin(x)$:**

```python
x = sympy.Symbol('x')
```

```python
sympy.integrate(sympy.sin(x))
```

$\displaystyle - \cos{\left(x \right)}$

**Integral definida de: $\displaystyle \int_0^\infty e^{-x}dx$:**

```python
sympy.integrate(sympy.exp(-x), (x, 0, sympy.oo))
```

$\displaystyle 1$

**otra forma:**

```python
sympy.Integral(sympy.exp(-x), (x, 0, sympy.oo))
```

$\displaystyle \int\limits_{0}^{\infty} e^{- x}\, dx$

```python
sympy.Integral(sympy.exp(-x), (x, 0, sympy.oo)).doit()
```

$\displaystyle 1$

---

## Newton-Cotes

## Regla simple del rectángulo o del punto medio

Denotaremos a esta regla como $Rf$. En este caso se aproxima el integrando $f$ por un polinomio de grado **cero** con nodo en $x_1 = \frac{a+b}{2}$. Entonces:

$$\displaystyle \int_a^bf(x)dx \approx \int_a^bf(x_1)dx = (b-a)f(x_1)=(b-a)f\left( \frac{a+b}{2} \right ) = hf(x_1)$$

con $h=b-a, x_1=\frac{a+b}{2}$.

### Ejemplo de implementación de regla simple de rectángulo

Utilizar la regla simple del rectángulo para aproximar la integral $\displaystyle \int_0^1e^{-x^2}dx \approx 0.7468241328124271$.

```python
f=lambda x: math.exp(-x**2) #integrand function
```

```python
def Rf(f,a,b):
    node=(a+b)/2 #middle point
    h=b-a
    return h*f(node) #polynomial of zero degree
```

```python
Rf(f,0,1)
```

    0.7788007830714049

Para contrastar con nuestra implementación usamos la función de `quad` dentro del paquete `scipy`. Ver [liga1 a quad](https://docs.scipy.org/doc/scipy/reference/tutorial/integrate.html), [liga 2 a quad](https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.quad.html#scipy.integrate.quad) y [liga 3 a quad](https://github.com/scipy/scipy/blob/v1.4.1/scipy/integrate/quadpack.py#L44-L432) para referencias sobre `quad`.

```python
from scipy.integrate import quad
```

```python
obj,err = quad(f, 0, 1)
```

```python
obj
```

    0.7468241328124271

```python
err
```

    8.291413475940725e-15

```python
relative_absolute_error(Rf(f,0,1), obj )
```

    0.04281684114646715

```python
Rf(f,0,1) == approx(obj)
```

    False

```python
Rf(f,0,1) == approx(obj, abs=1e-1, rel=1e-1)
```

    True

## Regla compuesta del rectángulo

En cada subintervalo construído como $[a_{i-1},a_i]$ con $i=1,\dots,n$ se aplica la regla simple $Rf$, esto es:

$$\displaystyle \int_{a_{i-1}}^{a_i}f(x)dx \approx R_i(f) \forall i=1,\dots,n.$$

De forma sencilla se puede ver que la regla compuesta del rectángulo $R_c(f)$ se escribe:

$$R_c(f) = \displaystyle \sum_{i=1}^n(a_i-a_{i-1})f\left( \frac{a_i+a_{i-1}}{2}\right) = \frac{h}{n}\sum_{i=1}^nf\left( \frac{a_i+a_{i-1}}{2}\right)$$

con $h=b-a$ y $n$ número de subintervalos.

---

**Nota**

Los nodos para el caso del rectángulo se obtienen con la fórmula: $x_i = a +(i+\frac{1}{2})\hat{h}, \forall i=0,\dots,n-1, \hat{h}=\frac{h}{n}$. Por ejemplo si $a=1, b=2$ y $\hat{h}=\frac{1}{4}$ (por tanto $n=4$ subintervalos) entonces:

Los subintervalos que tenemos son: $\left[1,\frac{5}{4}\right], \left[\frac{5}{4}, \frac{6}{4}\right], \left[\frac{6}{4}, \frac{7}{4}\right]$ y $\left[\frac{7}{4}, 2\right]$.

Los nodos están dados por:

$$x_0 = 1 + \left(0 + \frac{1}{2} \right)\frac{1}{4} = 1 + \frac{1}{8} = \frac{9}{8}$$

$$x_1 = 1 + \left(1 + \frac{1}{2}\right)\frac{1}{4} = 1 + \frac{3}{2}\cdot \frac{1}{4} = \frac{11}{8}$$

$$x_2 = 1 + \left(2 + \frac{1}{2}\right)\frac{1}{4} = 1 + \frac{5}{8}\cdot \frac{1}{4} = \frac{13}{8}$$

$$x_3 = 1 + \left(3 + \frac{1}{2}\right)\frac{1}{4} = 1 + \frac{7}{2}\cdot \frac{1}{4} = \frac{15}{8}$$

---

### Ejemplo de implementación de regla compuesta de rectángulo

Utilizar la regla compuesta del rectángulo para aproximar la integral $\int_0^1e^{-x^2}dx \approx 0.7468241328124271$.

```python
f=lambda x: np.exp(-x**2)
```

```python
def Rcf(f,a,b,n): #Rcf: composite rectangle method
    """
    Compute numerical approximation using rectangle or mid-point
    method in an interval.
    Nodes are generated via formula: x_i = a+(i+1/2)h_hat for
    i=0,1,...,n-1 and h_hat=(b-a)/n
    Args:

        f (float): function expression of integrand.

        a (float): left point of interval.

        b (float): right point of interval.

        n (int): number of subintervals.

    Returns:

        sum_res (float): numerical approximation to integral
            of f in the interval a,b
    """
    h_hat = (b-a)/n
    sum_res = 0
    for i in range(n):
        x = a+(i+1/2)*h_hat
        sum_res += f(x)
    return h_hat*sum_res
```

```python
a = 0
b = 1
```

```python
aprox_1=Rcf(f,a,b,1) #1 subinterval
aprox_1
```

    0.7788007830714049

```python
aprox_2=Rcf(f,a,b,2) #2 subintervals
aprox_2
```

    0.7545979437721995

```python
aprox_3=Rcf(f,a,b,10**3)#1000 subintervals
aprox_3
```

    0.746824163469049

Y se puede evaluar el error de aproximación con el error relativo:

```python
obj, err = quad(f, a, b)
(relative_absolute_error(aprox_1,obj), relative_absolute_error(aprox_2,obj), relative_absolute_error(aprox_3,obj))
```

    (0.04281684114646715, 0.010409158754012628, 4.1049318789768585e-08)

```python
obj
```

    0.7468241328124271

## Regla del trapecio

De forma sencilla se puede ver que la regla compuesta del trapecio $T_c(f)$ se escribe como:

$$T_c(f) = \displaystyle \frac{h}{2n}\left[f(x_0)+f(x_n)+2\displaystyle\sum_{i=1}^{n-1}f(x_i)\right]$$

con $h=b-a$ y $n$ número de subintervalos.

---

**Nota**

Los nodos para el caso del trapecio se obtienen con la fórmula: $x_i = a +i\hat{h}, \forall i=0,\dots,n, \hat{h}=\frac{h}{n}$.

---

**(Tarea) Ejercicio:**

**En un módulo con nombre `numerical_integration.py` aproximar el valor de la integral $\displaystyle \int_0^{\pi}sin(x)dx = 2$ con regla compuesta del trapecio con $n=10^4$ subintervalos. Para este caso utilizar la función:**

```
def Tcf(f,a,b,n): #Tcf: composite trapezoidal method for f
    """
    Compute numerical approximation using trapezoidal method in
    an interval.
    Nodes are generated via formula: x_i = a+ih_hat for i=0,1,...,n and h_hat=(b-a)/n
    Args:
        f (function): function expression of integrand
        a (float): left point of interval
        b (float): right point of interval
        n (float): number of subintervals
    Returns:
        sum_res (float): numerical approximation to integral of f in the interval a,b
    """
```

---

## Regla de Simpson

**Revisar sección 6.2 del libro de texto de J. Kiusalaas Numerical Methods in Engineering with Python 3 para la fórmula de Simpson.**

## Cuadratura Gaussiana

Una distinción con la cuadratura por Newton-Cotes es que en este caso los pesos y nodos se eligen para tener una regla o fórmula que integre de forma exacta a los polinomios de grado menor o igual que $2n+1$ con $n \in \{0,1,2,\dots\}$. Esto es:

$$\displaystyle \int_a^b w(x)Q_m(x)dx = \sum_{i=0}^nw_iQ_m(x_i)$$ con $Q_m$ polinomio de grado $m \leq 2n+1$ y $w(x)$ función de ponderación.

## Gauss-Legendre

Si se elige la base canónica de polinomios $\{1, x, x^2, \dots x^{2n+1}\}$, $w(x)=1$ (no ponderación), el intervalo $[-1,1]$ y la solución del siguiente sistema de ecuaciones podemos obtener la regla por cuadratura conocida con el nombre de Gauss-Legendre.

---

**Comentario**

Otra forma de obtener esta regla es vía los polinomios de Legendre, ver [Legendre polynomials](https://en.wikipedia.org/wiki/Legendre_polynomials)

---

Teniendo el objetivo de integrar de forma exacta los polinomios de la base canónica para el caso de $n=1$, resultan las siguientes ecuaciones:

$$2=\displaystyle \int_{-1}^{1}1dx = w_0 \cdot 1 + w_1\cdot1$$

$$0 = \displaystyle \int_{-1}^1xdx = w_0x_0 + w_1x_1$$

$$\frac{2}{3} = \displaystyle \int_{-1}^1x^2dx = w_0x_0^2 + w_1x_1^2$$

$$0 = \displaystyle \int_{-1}^1x^3dx = w_0x_0^3 + w_1x_1^3$$

el cual es un sistema de 4 ecuaciones no lineales con 4 incógnitas: $(w_0, x_0), (w_1, x_1)$ cuya solución es: $w_0 = w_1 = 1$ y $x_0 =-\sqrt{\frac{1}{3}} \approx -0.57735, x_1 = \sqrt{\frac{1}{3}} \approx 0.57735$.

Ya se han calculado los pesos y nodos para diferentes valores de los grados de los polinomios. A continuación se tiene la siguiente tabla para $n \in \{0,1,2,3,4\}$ y una integral definida en el intervalo $[-1,1]$:

|n  |grado|# (num de nodos + num de pesos)|pesos: $w_i, w_{i+1}$    |nodos: $x_i, x_{i+1}$              |
|---|:----:|:---:|:-------:|:-----------------:|
|0  |1|2|2|0|
|1  |3|4|1,1|-$\sqrt{\frac{1}{3}}$,$\sqrt{\frac{1}{3}}$|
|2  |5|6|$\frac{5}{9}$, $\frac{8}{9}$, $\frac{5}{9}$ |$-\sqrt{\frac{3}{5}}$, 0, $\sqrt{\frac{3}{5}}$  |
|3  |7|8|0.347855, 0.652145, 0.652145, 0.347855|-0.861136,-0.339981,0.339981,0.861136|
|4  |9|10|0.236927, 0.478629, 0.568889, 0.478629, 0.236927 | -0.90618, -0.538469, 0, 0.538469, 0.90618|

Y para una integral en $[a,b]$ se utiliza la fórmula de cambio de variable:

$$\displaystyle \int_{a}^{b}f(t)dt \approx \frac{(b-a)}{2} \displaystyle \sum_{i=0}^nw_if \left (\frac{1}{2}[(b-a)x_i+a+b] \right )$$

con los pesos definidos para el intervalo $[-1,1]$

### Ejemplo con la cuadratura de Gauss-Legendre utilizando dos nodos aproximar la integral $\int_0^1e^{-t^2}dt \approx 0.7468241328124271$

**Solución:** Utilizamos $f(t) = e^{-t^2}$ en:

$$\displaystyle \int_{a}^{b}f(t)dt \approx \frac{(b-a)}{2} \displaystyle \sum_{i=0}^nw_if \left (\frac{1}{2}[(b-a)x_i+a+b] \right )$$

por lo que se tiene:

$$
\begin{eqnarray}
\int_0^1e^{-t^2}dt &=& \frac{(1-0)}{2} \displaystyle \sum_{i=0}^2w_i \cdot \exp\left[-\left ({\frac{1}{2}[(1-0)x_i + 0 + 1]} \right) ^2 \right] \nonumber \\
&=& \frac{1}{2} \left ( 1\cdot \exp \left[ -\left ( \frac{1}{2} \left[-\sqrt{\frac{1}{3}}+1 \right] \right)^2 \right] + 1\cdot \exp \left[ -\left ( \frac{1}{2} \left[\sqrt{\frac{1}{3}}+1 \right] \right)^2 \right] \right) \nonumber \\
\end{eqnarray}
$$

y haciendo los cálculos en Python:

```python
cte0 = -(1/2*(-math.sqrt(1/3)+1))**2
cte1 = -(1/2*(math.sqrt(1/3)+1))**2
w0 = 1
w1 = 1
approx_GL = 1/2*(w0*math.exp(cte0) + w1*math.exp(cte1))
```

```python
approx_GL
```

    0.7465946882828597

```python
obj
```

    0.7468241328124271

```python
relative_absolute_error(approx_GL, obj)
```

    0.00030722698890749486

---

**(Tarea) Ejercicio: aproximar la integral de:**

$$\displaystyle \int_0^1e^{-\frac{t^2}{2}}dt \approx .855624391892149$$

**con cuadratura Gauss-Legendre**

**1) En el módulo `numerical_integration.py` crear la función:**

```
def GLf(f,a,b,n): #GLf: Gauss-Legendre quadrature for f
    """
    Compute numerical approximation using quadrature Gauss-Legendre.
    Weights and nodes are obtained with table for n=0,1,2,3,4
    Args:
        f (function): function expression of integrand
        a (float): left point of interval
        b (float): right point of interval
        n (float): number of subintervals
    Returns:
        sum_res (float): numerical approximation to integral of f in the interval a,b
    """
```

**2) Realizar una gráfica de la forma error relativo vs $n$ ($n$ en el eje horizontal).**

---

## Otras reglas de cuadratura Gaussiana

Las reglas de cuadratura Gaussiana como se escribió en la sección anterior buscan tener la mayor exactitud posible para integrar polinomios de grado menor o igual $2n+1$. Diferentes elecciones de polinomios resultan en distintas reglas. Entre las más populares se encuentran:

* Gauss-Chebyshev.
* Gauss-Laguerre.
* Gauss-Hermite.
* Cuadratura Gaussiana con singularidad logarítmica.

Y se puede probar que los nodos en la regla de cuadratura de cada una de las reglas anteriores son las raíces de los polinomios que las definen.

**Revisar sección 6.4 el libro de texto de J. Kiusalaas Numerical Methods in Engineering with Python 3 para la expresiones de las reglas anteriores y los valores de los pesos y nodos de cada regla anterior para diferentes números de nodos. Como ayuda está el documento: [Gauss-Hermite-2-nodos.pdf](https://drive.google.com/file/d/1w7fGm0oOAoYlVeL_S61O1IGAdpaMBkJM/view?usp=sharing) para su consulta.**

---

**Ejercicio**

**(Tarea) Aproximar las integrales: $$(2\pi\sigma^2)^{-\frac{1}{2}}\displaystyle \int_{-\infty}^\infty te^{\frac{-(t-\mu)^2}{2\sigma^2}}dt$$**

$$(2\pi\sigma^2)^{-\frac{1}{2}}\displaystyle \int_{-\infty}^\infty t^2e^{\frac{-(t-\mu)^2}{2\sigma^2}}dt$$

**donde: $\sigma=0.25, \mu=0.15$ cuyos valores respectivamente son: $0.15, 0.085$ con cuadratura de Gauss-Hermite y $n=5$. Para lo anterior, realizar cambio de variable $x=\frac{t-\mu}{\sqrt{2\sigma^2}}, dt=\sqrt{2\sigma^2}dx$. En el módulo de `numerical_integration.py` crear una función:**

```
def GHf(f,mu, sigma): #GHf: Gauss-Hermite quadrature for f
    """
    Compute numerical approximation using quadrature Gauss-Hermite.
    Weights and nodes are obtained with table in Kiusalaas for n=6
    Args:
        f (function): function expression of integrand
        mu (float): mean
        sigma (float): standard deviation
    Returns:
        sum_res (float): numerical approximation to integral of f in the interval a,b
    """

```

---

## Integración numérica en más dimensiones

**Revisar sección 6.5 del libro de texto de J. Kiusalaas Numerical Methods in Engineering with Python 3 integrales múltiples hasta el ejemplo 6.14.**

### The curse of dimensionality

Como puede observarse en el desarrollo de la sección 6.5, la aproximación por integración numérica a integrales múltiples por los métodos por Newton-Cotes o cuadratura Gaussiana implican sustituir la integral $\int$ sobre una región por una $\sum$ y evaluaciones del integrando en un conjunto de nodos multiplicados por pesos (suma ponderada). Esto para dimensiones igual a dos o tres es viable pero para dimensiones altas no es computacionalmente práctico.

La razón de lo anterior tiene que ver con la cantidad de nodos y finalmente evaluaciones del integrando que se tienen que realizar para tener una aproximación con una exactitud aceptable. Por ejemplo, la regla del rectángulo o del trapecio tienen un error de orden $\mathcal{O}(\hat{h}^2)$ independientemente de si se está aproximando integrales de una o más dimensiones.

Supóngase que se utilizan $n$ nodos para tener un valor de espaciado igual a $\hat{h}$ en una dimensión, entonces para $\mathcal{D}$ dimensiones se requerirían $N=n^\mathcal{D}$ evaluaciones del integrando, o bien, si se tiene un valor de $N$ igual a $10, 000$ y $\mathcal{D}=4$ dimensiones el error sería del orden $\mathcal{O}(N^{-2/\mathcal{D}})$ lo que implicaría un valor de $\hat{h}=.1$ para aproximadamente sólo **dos dígitos** correctos en la aproximación (para el enunciado anterior recuérdese que $\hat{h}$ es proporcional a $n^{-1}$ y $n$ = $N^{1/\mathcal{D}}$). Este esfuerzo enorme de evaluar $N$ veces el integrando para una exactitud pequeña se debe al problema de generar puntos para *llenar* un espacio $\mathcal{D}$-dimensional y se conoce con el nombre de la maldición de la dimensionalidad, [***the curse of dimensionality***](https://en.wikipedia.org/wiki/Curse_of_dimensionality).

Como alternativa a los métodos por cuadratura anteriores para las integrales de más dimensiones se tienen los métodos de integración por el método Monte Carlo que generan aproximaciones con una exactitud moderada (del orden de $\mathcal{O}(n^{-1/2})$) para un número de puntos moderado independiente de la dimensión. Tales métodos de integración son similares a los métodos por cuadratura en el sentido que se eligen puntos en los que se evaluará el integrando para sumar sus valores pero la diferencia con estos métodos, es que en el método de integración por Monte Carlo los puntos son seleccionados de una forma **aleatoria** (de hecho es pseudo-aleatoria pues se generan con un programa de computadora) en lugar de generarse con una fórmula.

Los métodos por integración por Monte Carlo requieren el concepto de **variables aleatorias**.

## Referencias

* [SymPy](https://www.sympy.org/en/index.html) y [Numerical Python by Robert Johansson, Apress](https://www.apress.com/gp/book/9781484242452)


```python
%matplotlib inline
```

Audio manipulation with torchaudio
==================================

``torchaudio`` provides powerful audio I/O functions, preprocessing
transforms and dataset.

In this tutorial, we will look into how to prepare audio data and
extract features that can be fed to NN models.

```python
# When running this tutorial in Google Colab, install the required packages
# with the following.
# !pip install torchaudio librosa boto3

import torch
import torchaudio
import torchaudio.functional as F
import torchaudio.transforms as T

print(torch.__version__)
print(torchaudio.__version__)
```

Preparing data and utility functions (skip this section)
--------------------------------------------------------

```python
#@title Prepare data and utility functions. {display-mode: "form"}
#@markdown
#@markdown You do not need to look into this cell.
#@markdown Just execute once and you are good to go.
#@markdown
#@markdown In this tutorial, we will use a speech data from [VOiCES dataset](https://iqtlabs.github.io/voices/), which is licensed under Creative Commos BY 4.0.

#-------------------------------------------------------------------------------
# Preparation of data and helper functions.
#-------------------------------------------------------------------------------
import io
import os
import math
import tarfile
import multiprocessing

import scipy
import librosa
import boto3
from botocore import UNSIGNED
from botocore.config import Config
import requests
import matplotlib
import matplotlib.pyplot as plt
import pandas as pd
import time
from IPython.display import Audio, display

[width, height] = matplotlib.rcParams['figure.figsize']
if width < 10:
  matplotlib.rcParams['figure.figsize'] = [width * 2.5, height]

_SAMPLE_DIR = "_sample_data"
SAMPLE_WAV_URL = "https://pytorch-tutorial-assets.s3.amazonaws.com/steam-train-whistle-daniel_simon.wav"
SAMPLE_WAV_PATH = os.path.join(_SAMPLE_DIR, "steam.wav")

SAMPLE_WAV_SPEECH_URL = "https://pytorch-tutorial-assets.s3.amazonaws.com/VOiCES_devkit/source-16k/train/sp0307/Lab41-SRI-VOiCES-src-sp0307-ch127535-sg0042.wav"
SAMPLE_WAV_SPEECH_PATH = os.path.join(_SAMPLE_DIR, "speech.wav")

SAMPLE_RIR_URL = "https://pytorch-tutorial-assets.s3.amazonaws.com/VOiCES_devkit/distant-16k/room-response/rm1/impulse/Lab41-SRI-VOiCES-rm1-impulse-mc01-stu-clo.wav"
SAMPLE_RIR_PATH = os.path.join(_SAMPLE_DIR, "rir.wav")

SAMPLE_NOISE_URL = "https://pytorch-tutorial-assets.s3.amazonaws.com/VOiCES_devkit/distant-16k/distractors/rm1/babb/Lab41-SRI-VOiCES-rm1-babb-mc01-stu-clo.wav"
SAMPLE_NOISE_PATH = os.path.join(_SAMPLE_DIR, "bg.wav")

SAMPLE_MP3_URL = "https://pytorch-tutorial-assets.s3.amazonaws.com/steam-train-whistle-daniel_simon.mp3"
SAMPLE_MP3_PATH = os.path.join(_SAMPLE_DIR, "steam.mp3")

SAMPLE_GSM_URL = "https://pytorch-tutorial-assets.s3.amazonaws.com/steam-train-whistle-daniel_simon.gsm"
SAMPLE_GSM_PATH = os.path.join(_SAMPLE_DIR, "steam.gsm")

SAMPLE_TAR_URL = "https://pytorch-tutorial-assets.s3.amazonaws.com/VOiCES_devkit.tar.gz"
SAMPLE_TAR_PATH = os.path.join(_SAMPLE_DIR, "sample.tar.gz")
SAMPLE_TAR_ITEM = "VOiCES_devkit/source-16k/train/sp0307/Lab41-SRI-VOiCES-src-sp0307-ch127535-sg0042.wav"

S3_BUCKET = "pytorch-tutorial-assets"
S3_KEY = "VOiCES_devkit/source-16k/train/sp0307/Lab41-SRI-VOiCES-src-sp0307-ch127535-sg0042.wav"

YESNO_DATASET_PATH = os.path.join(_SAMPLE_DIR, "yes_no")
os.makedirs(YESNO_DATASET_PATH, exist_ok=True)
os.makedirs(_SAMPLE_DIR, exist_ok=True)

def _fetch_data():
  uri = [
    (SAMPLE_WAV_URL, SAMPLE_WAV_PATH),
    (SAMPLE_WAV_SPEECH_URL, SAMPLE_WAV_SPEECH_PATH),
    (SAMPLE_RIR_URL, SAMPLE_RIR_PATH),
    (SAMPLE_NOISE_URL, SAMPLE_NOISE_PATH),
    (SAMPLE_MP3_URL, SAMPLE_MP3_PATH),
    (SAMPLE_GSM_URL, SAMPLE_GSM_PATH),
    (SAMPLE_TAR_URL, SAMPLE_TAR_PATH),
  ]
  for url, path in uri:
    with open(path, 'wb') as file_:
      file_.write(requests.get(url).content)

_fetch_data()

def _download_yesno():
  if os.path.exists(os.path.join(YESNO_DATASET_PATH, "waves_yesno.tar.gz")):
    return
  torchaudio.datasets.YESNO(root=YESNO_DATASET_PATH, download=True)

YESNO_DOWNLOAD_PROCESS = multiprocessing.Process(target=_download_yesno)
YESNO_DOWNLOAD_PROCESS.start()

def _get_sample(path, resample=None):
  effects = [
    ["remix", "1"]
  ]
  if resample:
    effects.extend([
      ["lowpass", f"{resample // 2}"],
      ["rate", f'{resample}'],
    ])
  return torchaudio.sox_effects.apply_effects_file(path, effects=effects)

def get_speech_sample(*, resample=None):
  return _get_sample(SAMPLE_WAV_SPEECH_PATH, resample=resample)

def get_sample(*, resample=None):
  return _get_sample(SAMPLE_WAV_PATH, resample=resample)

def get_rir_sample(*, resample=None, processed=False):
  rir_raw, sample_rate = _get_sample(SAMPLE_RIR_PATH, resample=resample)
  if not processed:
    return rir_raw, sample_rate
  rir = rir_raw[:, int(sample_rate*1.01):int(sample_rate*1.3)]
  rir = rir / torch.norm(rir, p=2)
  rir = torch.flip(rir, [1])
  return rir, sample_rate

def get_noise_sample(*, resample=None):
  return _get_sample(SAMPLE_NOISE_PATH, resample=resample)

def print_stats(waveform, sample_rate=None, src=None):
  if src:
    print("-" * 10)
    print("Source:", src)
    print("-" * 10)
  if sample_rate:
    print("Sample Rate:", sample_rate)
  print("Shape:", tuple(waveform.shape))
  print("Dtype:", waveform.dtype)
  print(f" - Max:     {waveform.max().item():6.3f}")
  print(f" - Min:     {waveform.min().item():6.3f}")
  print(f" - Mean:    {waveform.mean().item():6.3f}")
  print(f" - Std Dev: {waveform.std().item():6.3f}")
  print()
  print(waveform)
  print()

def plot_waveform(waveform, sample_rate, title="Waveform", xlim=None, ylim=None):
  waveform = waveform.numpy()

  num_channels, num_frames = waveform.shape
  time_axis = torch.arange(0, num_frames) / sample_rate

  figure, axes = plt.subplots(num_channels, 1)
  if num_channels == 1:
    axes = [axes]
  for c in range(num_channels):
    axes[c].plot(time_axis, waveform[c], linewidth=1)
    axes[c].grid(True)
    if num_channels > 1:
      axes[c].set_ylabel(f'Channel {c+1}')
    if xlim:
      axes[c].set_xlim(xlim)
    if ylim:
      axes[c].set_ylim(ylim)
  figure.suptitle(title)
  plt.show(block=False)

def plot_specgram(waveform, sample_rate, title="Spectrogram", xlim=None):
  waveform = waveform.numpy()

  num_channels, num_frames = waveform.shape
  time_axis = torch.arange(0, num_frames) / sample_rate

  figure, axes = plt.subplots(num_channels, 1)
  if num_channels == 1:
    axes = [axes]
  for c in range(num_channels):
    axes[c].specgram(waveform[c], Fs=sample_rate)
    if num_channels > 1:
      axes[c].set_ylabel(f'Channel {c+1}')
    if xlim:
      axes[c].set_xlim(xlim)
  figure.suptitle(title)
  plt.show(block=False)

def play_audio(waveform, sample_rate):
  waveform = waveform.numpy()

  num_channels, num_frames = waveform.shape
  if num_channels == 1:
    display(Audio(waveform[0], rate=sample_rate))
  elif num_channels == 2:
    display(Audio((waveform[0], waveform[1]), rate=sample_rate))
  else:
    raise ValueError("Waveform with more than 2 channels are not supported.")

def inspect_file(path):
  print("-" * 10)
  print("Source:", path)
  print("-" * 10)
  print(f" - File size: {os.path.getsize(path)} bytes")
  print(f" - {torchaudio.info(path)}")

def plot_spectrogram(spec, title=None, ylabel='freq_bin', aspect='auto', xmax=None):
  fig, axs = plt.subplots(1, 1)
  axs.set_title(title or 'Spectrogram (db)')
  axs.set_ylabel(ylabel)
  axs.set_xlabel('frame')
  im = axs.imshow(librosa.power_to_db(spec), origin='lower', aspect=aspect)
  if xmax:
    axs.set_xlim((0, xmax))
  fig.colorbar(im, ax=axs)
  plt.show(block=False)

def plot_mel_fbank(fbank, title=None):
  fig, axs = plt.subplots(1, 1)
  axs.set_title(title or 'Filter bank')
  axs.imshow(fbank, aspect='auto')
  axs.set_ylabel('frequency bin')
  axs.set_xlabel('mel bin')
  plt.show(block=False)

def get_spectrogram(
    n_fft = 400,
    win_len = None,
    hop_len = None,
    power = 2.0,
):
  waveform, _ = get_speech_sample()
  spectrogram = T.Spectrogram(
      n_fft=n_fft,
      win_length=win_len,
      hop_length=hop_len,
      center=True,
      pad_mode="reflect",
      power=power,
  )
  return spectrogram(waveform)

def plot_pitch(waveform, sample_rate, pitch):
  figure, axis = plt.subplots(1, 1)
  axis.set_title("Pitch Feature")
  axis.grid(True)

  end_time = waveform.shape[1] / sample_rate
  time_axis = torch.linspace(0, end_time,  waveform.shape[1])
  axis.plot(time_axis, waveform[0], linewidth=1, color='gray', alpha=0.3)

  axis2 = axis.twinx()
  time_axis = torch.linspace(0, end_time, pitch.shape[1])
  ln2 = axis2.plot(
      time_axis, pitch[0], linewidth=2, label='Pitch', color='green')

  axis2.legend(loc=0)
  plt.show(block=False)

def plot_kaldi_pitch(waveform, sample_rate, pitch, nfcc):
  figure, axis = plt.subplots(1, 1)
  axis.set_title("Kaldi Pitch Feature")
  axis.grid(True)

  end_time = waveform.shape[1] / sample_rate
  time_axis = torch.linspace(0, end_time,  waveform.shape[1])
  axis.plot(time_axis, waveform[0], linewidth=1, color='gray', alpha=0.3)

  time_axis = torch.linspace(0, end_time, pitch.shape[1])
  ln1 = axis.plot(time_axis, pitch[0], linewidth=2, label='Pitch', color='green')
  axis.set_ylim((-1.3, 1.3))

  axis2 = axis.twinx()
  time_axis = torch.linspace(0, end_time, nfcc.shape[1])
  ln2 = axis2.plot(
      time_axis, nfcc[0], linewidth=2, label='NFCC', color='blue', linestyle='--')

  lns = ln1 + ln2
  labels = [l.get_label() for l in lns]
  axis.legend(lns, labels, loc=0)
  plt.show(block=False)

DEFAULT_OFFSET = 201
SWEEP_MAX_SAMPLE_RATE = 48000
DEFAULT_LOWPASS_FILTER_WIDTH = 6
DEFAULT_ROLLOFF = 0.99
DEFAULT_RESAMPLING_METHOD = 'sinc_interpolation'

def _get_log_freq(sample_rate, max_sweep_rate, offset):
  """Get freqs evenly spaced out in log-scale, between [0, max_sweep_rate // 2]

  offset is used to avoid negative infinity `log(offset + x)`.

  """
  half = sample_rate // 2
  start, stop = math.log(offset), math.log(offset + max_sweep_rate // 2)
  return torch.exp(torch.linspace(start, stop, sample_rate, dtype=torch.double)) - offset

def _get_inverse_log_freq(freq, sample_rate, offset):
  """Find the time where the given frequency is given by _get_log_freq"""
  half = sample_rate // 2
  return sample_rate * (math.log(1 + freq / offset) / math.log(1 + half / offset))

def _get_freq_ticks(sample_rate, offset, f_max):
  # Given the original sample rate used for generating the sweep,
  # find the x-axis value where the log-scale major frequency values fall in
  time, freq = [], []
  for exp in range(2, 5):
    for v in range(1, 10):
      f = v * 10 ** exp
      if f < sample_rate // 2:
        t = _get_inverse_log_freq(f, sample_rate, offset) / sample_rate
        time.append(t)
        freq.append(f)
  t_max = _get_inverse_log_freq(f_max, sample_rate, offset) / sample_rate
  time.append(t_max)
  freq.append(f_max)
  return time, freq

def plot_sweep(waveform, sample_rate, title, max_sweep_rate=SWEEP_MAX_SAMPLE_RATE, offset=DEFAULT_OFFSET):
  x_ticks = [100, 500, 1000, 5000, 10000, 20000, max_sweep_rate // 2]
  y_ticks = [1000, 5000, 10000, 20000, sample_rate//2]

  time, freq = _get_freq_ticks(max_sweep_rate, offset, sample_rate // 2)
  freq_x = [f if f in x_ticks and f <= max_sweep_rate // 2 else None for f in freq]
  freq_y = [f for f in freq if f >= 1000 and f in y_ticks and f <= sample_rate // 2]

  figure, axis = plt.subplots(1, 1)
  axis.specgram(waveform[0].numpy(), Fs=sample_rate)
  plt.xticks(time, freq_x)
  plt.yticks(freq_y, freq_y)
  axis.set_xlabel('Original Signal Frequency (Hz, log scale)')
  axis.set_ylabel('Waveform Frequency (Hz)')
  axis.xaxis.grid(True, alpha=0.67)
  axis.yaxis.grid(True, alpha=0.67)
  figure.suptitle(f'{title} (sample rate: {sample_rate} Hz)')
  plt.show(block=True)

def get_sine_sweep(sample_rate, offset=DEFAULT_OFFSET):
    max_sweep_rate = sample_rate
    freq = _get_log_freq(sample_rate, max_sweep_rate, offset)
    delta = 2 * math.pi * freq / sample_rate
    cummulative = torch.cumsum(delta, dim=0)
    signal = torch.sin(cummulative).unsqueeze(dim=0)
    return signal

def benchmark_resample(
    method,
    waveform,
    sample_rate,
    resample_rate,
    lowpass_filter_width=DEFAULT_LOWPASS_FILTER_WIDTH,
    rolloff=DEFAULT_ROLLOFF,
    resampling_method=DEFAULT_RESAMPLING_METHOD,
    beta=None,
    librosa_type=None,
    iters=5
):
  if method == "functional":
    begin = time.time()
    for _ in range(iters):
      F.resample(waveform, sample_rate, resample_rate, lowpass_filter_width=lowpass_filter_width,
                 rolloff=rolloff, resampling_method=resampling_method)
    elapsed = time.time() - begin
    return elapsed / iters
  elif method == "transforms":
    resampler = T.Resample(sample_rate, resample_rate, lowpass_filter_width=lowpass_filter_width,
                           rolloff=rolloff, resampling_method=resampling_method, dtype=waveform.dtype)
    begin = time.time()
    for _ in range(iters):
      resampler(waveform)
    elapsed = time.time() - begin
    return elapsed / iters
  elif method == "librosa":
    waveform_np = waveform.squeeze().numpy()
    begin = time.time()
    for _ in range(iters):
      librosa.resample(waveform_np, sample_rate, resample_rate, res_type=librosa_type)
    elapsed = time.time() - begin
    return elapsed / iters
```

Audio I/O
=========

torchaudio integrates ``libsox`` and provides a rich set of audio I/O.

Quering audio metadata
----------------------

``torchaudio.info`` function fetches metadata of audio. You can provide
a path-like object or file-like object.

```python
metadata = torchaudio.info(SAMPLE_WAV_PATH)
print(metadata)
```

Where

-  ``sample_rate`` is the sampling rate of the audio
-  ``num_channels`` is the number of channels
-  ``num_frames`` is the number of frames per channel
-  ``bits_per_sample`` is bit depth
-  ``encoding`` is the sample coding format

The values ``encoding`` can take are one of the following

-  ``"PCM_S"``: Signed integer linear PCM
-  ``"PCM_U"``: Unsigned integer linear PCM
-  ``"PCM_F"``: Floating point linear PCM
-  ``"FLAC"``: Flac, `Free Lossless Audio
   Codec <https://xiph.org/flac/>`__
-  ``"ULAW"``: Mu-law,
   [`wikipedia <https://en.wikipedia.org/wiki/%CE%9C-law_algorithm>`__]
-  ``"ALAW"``: A-law
   [`wikipedia <https://en.wikipedia.org/wiki/A-law_algorithm>`__]
-  ``"MP3"`` : MP3, MPEG-1 Audio Layer III
-  ``"VORBIS"``: OGG Vorbis [`xiph.org <https://xiph.org/vorbis/>`__]
-  ``"AMR_NB"``: Adaptive Multi-Rate
   [`wikipedia <https://en.wikipedia.org/wiki/Adaptive_Multi-Rate_audio_codec>`__]
-  ``"AMR_WB"``: Adaptive Multi-Rate Wideband
   [`wikipedia <https://en.wikipedia.org/wiki/Adaptive_Multi-Rate_Wideband>`__]
-  ``"OPUS"``: Opus [`opus-codec.org <https://opus-codec.org/>`__]
-  ``"GSM"``: GSM-FR
   [`wikipedia <https://en.wikipedia.org/wiki/Full_Rate>`__]
-  ``"UNKNOWN"`` None of avobe

**Note**

-  ``bits_per_sample`` can be ``0`` for formats with compression and/or
   variable bit rate. (such as mp3)
-  ``num_frames`` can be ``0`` for GSM-FR format.

```python
metadata = torchaudio.info(SAMPLE_MP3_PATH)
print(metadata)

metadata = torchaudio.info(SAMPLE_GSM_PATH)
print(metadata)
```

Querying file-like object
~~~~~~~~~~~~~~~~~~~~~~~~~

``info`` function works on file-like object as well.

```python
print("Source:", SAMPLE_WAV_URL)
with requests.get(SAMPLE_WAV_URL, stream=True) as response:
  metadata = torchaudio.info(response.raw)
print(metadata)
```

**Note** When passing file-like object, ``info`` function does not read
all the data, instead it only reads the beginning portion of data.
Therefore, depending on the audio format, it cannot get the correct
metadata, including the format itself. The following example illustrates
this.

-  Use ``format`` argument to tell what audio format it is.
-  The returned metadata has ``num_frames = 0``

```python
print("Source:", SAMPLE_MP3_URL)
with requests.get(SAMPLE_MP3_URL, stream=True) as response:
  metadata = torchaudio.info(response.raw, format="mp3")

  print(f"Fetched {response.raw.tell()} bytes.")
print(metadata)
```

Loading audio data into Tensor
------------------------------

To load audio data, you can use ``torchaudio.load``.

This function accepts path-like object and file-like object.

The returned value is a tuple of waveform (``Tensor``) and sample rate
(``int``).

By default, the resulting tensor object has ``dtype=torch.float32`` and
its value range is normalized within ``[-1.0, 1.0]``.

For the list of supported format, please refer to `the torchaudio
documentation <https://pytorch.org/audio>`__.

```python
waveform, sample_rate = torchaudio.load(SAMPLE_WAV_SPEECH_PATH)

print_stats(waveform, sample_rate=sample_rate)
plot_waveform(waveform, sample_rate)
plot_specgram(waveform, sample_rate)
play_audio(waveform, sample_rate)
```

Loading from file-like object
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``torchaudio``\ ’s I/O functions now support file-like object. This
allows to fetch audio data and decode at the same time from the location
other than local file system. The following examples illustrates this.

```python
# Load audio data as HTTP request
with requests.get(SAMPLE_WAV_SPEECH_URL, stream=True) as response:
  waveform, sample_rate = torchaudio.load(response.raw)
plot_specgram(waveform, sample_rate, title="HTTP datasource")

# Load audio from tar file
with tarfile.open(SAMPLE_TAR_PATH, mode='r') as tarfile_:
  fileobj = tarfile_.extractfile(SAMPLE_TAR_ITEM)
  waveform, sample_rate = torchaudio.load(fileobj)
plot_specgram(waveform, sample_rate, title="TAR file")

# Load audio from S3
client = boto3.client('s3', config=Config(signature_version=UNSIGNED))
response = client.get_object(Bucket=S3_BUCKET, Key=S3_KEY)
waveform, sample_rate = torchaudio.load(response['Body'])
plot_specgram(waveform, sample_rate, title="From S3")
```

Tips on slicing
~~~~~~~~~~~~~~~

Providing ``num_frames`` and ``frame_offset`` arguments will slice the
resulting Tensor object while decoding.

The same result can be achieved using the regular Tensor slicing,
(i.e. ``waveform[:, frame_offset:frame_offset+num_frames]``) however,
providing ``num_frames`` and ``frame_offset`` arguments is more
efficient.

This is because the function will stop data acquisition and decoding
once it finishes decoding the requested frames. This is advantageous
when the audio data are transfered via network as the data transfer will
stop as soon as the necessary amount of data is fetched.

The following example illustrates this;

```python
# Illustration of two different decoding methods.
# The first one will fetch all the data and decode them, while
# the second one will stop fetching data once it completes decoding.
# The resulting waveforms are identical.

frame_offset, num_frames = 16000, 16000  # Fetch and decode the 1 - 2 seconds

print("Fetching all the data...")
with requests.get(SAMPLE_WAV_SPEECH_URL, stream=True) as response:
  waveform1, sample_rate1 = torchaudio.load(response.raw)
  waveform1 = waveform1[:, frame_offset:frame_offset+num_frames]
  print(f" - Fetched {response.raw.tell()} bytes")

print("Fetching until the requested frames are available...")
with requests.get(SAMPLE_WAV_SPEECH_URL, stream=True) as response:
  waveform2, sample_rate2 = torchaudio.load(
      response.raw, frame_offset=frame_offset, num_frames=num_frames)
  print(f" - Fetched {response.raw.tell()} bytes")

print("Checking the resulting waveform ... ", end="")
assert (waveform1 == waveform2).all()
print("matched!")
```

Saving audio to file
--------------------

To save audio data in the formats intepretable by common applications,
you can use ``torchaudio.save``.

This function accepts path-like object and file-like object.

When passing file-like object, you also need to provide ``format``
argument so that the function knows which format it should be using. In
case of path-like object, the function will detemine the format based on
the extension. If you are saving to a file without extension, you need
to provide ``format`` argument.

When saving as WAV format, the default encoding for ``float32`` Tensor
is 32-bit floating-point PCM. You can provide ``encoding`` and
``bits_per_sample`` argument to change this. For example, to save data
in 16 bit signed integer PCM, you can do the following.

**Note** Saving data in encodings with lower bit depth reduces the
resulting file size but loses precision.

```python
waveform, sample_rate = get_sample()
print_stats(waveform, sample_rate=sample_rate)

# Save without any encoding option.
# The function will pick up the encoding which
# the provided data fit
path = "save_example_default.wav"
torchaudio.save(path, waveform, sample_rate)
inspect_file(path)

# Save as 16-bit signed integer Linear PCM
# The resulting file occupies half the storage but loses precision
path = "save_example_PCM_S16.wav"
torchaudio.save(
    path, waveform, sample_rate,
    encoding="PCM_S", bits_per_sample=16)
inspect_file(path)
```

``torchaudio.save`` can also handle other formats. To name a few;

```python
waveform, sample_rate = get_sample(resample=8000)

formats = [
  "mp3",
  "flac",
  "vorbis",
  "sph",
  "amb",
  "amr-nb",
  "gsm",
]

for format in formats:
  path = f"save_example.{format}"
  torchaudio.save(path, waveform, sample_rate, format=format)
  inspect_file(path)
```

Saving to file-like object
~~~~~~~~~~~~~~~~~~~~~~~~~~

Similar to the other I/O functions, you can save audio into file-like
object. When saving to file-like object, ``format`` argument is
required.

```python
waveform, sample_rate = get_sample()

# Saving to Bytes buffer
buffer_ = io.BytesIO()
torchaudio.save(buffer_, waveform, sample_rate, format="wav")

buffer_.seek(0)
print(buffer_.read(16))
```

Resampling
==========

To resample an audio waveform from one freqeuncy to another, you can use
``transforms.Resample`` or ``functional.resample``.
``transforms.Resample`` precomputes and caches the kernel used for
resampling, while ``functional.resample`` computes it on the fly, so
using ``transforms.Resample`` will result in a speedup if resampling
multiple waveforms using the same parameters (see Benchmarking section).

Both resampling methods use `bandlimited sinc
interpolation <https://ccrma.stanford.edu/~jos/resample/>`__ to compute
signal values at arbitrary time steps. The implementation involves
convolution, so we can take advantage of GPU / multithreading for
performance improvements. When using resampling in multiple
subprocesses, such as data loading with multiple worker processes, your
application might create more threads than your system can handle
efficiently. Setting ``torch.set_num_threads(1)`` might help in this
case.

Because a finite number of samples can only represent a finite number of
frequencies, resampling does not produce perfect results, and a variety
of parameters can be used to control for its quality and computational
speed. We demonstrate these properties through resampling a logarithmic
sine sweep, which is a sine wave that increases exponentially in
frequency over time.

The spectrograms below show the frequency representation of the signal,
where the x-axis labels correspond to the frequency of the original
waveform (in log scale), the y-axis corresponds to the frequency of the
plotted waveform, and the color intensity refers to amplitude.

```python
sample_rate = 48000
resample_rate = 32000

waveform = get_sine_sweep(sample_rate)
plot_sweep(waveform, sample_rate, title="Original Waveform")
play_audio(waveform, sample_rate)

resampler = T.Resample(sample_rate, resample_rate, dtype=waveform.dtype)
resampled_waveform = resampler(waveform)
plot_sweep(resampled_waveform, resample_rate, title="Resampled Waveform")
play_audio(waveform, sample_rate)
```

Controling resampling quality with parameters
---------------------------------------------

Lowpass filter width
~~~~~~~~~~~~~~~~~~~~

Because the filter used for interpolation extends infinitely, the
``lowpass_filter_width`` parameter is used to control for the width of
the filter to use to window the interpolation. It is also referred to as
the number of zero crossings, since the interpolation passes through
zero at every time unit. Using a larger ``lowpass_filter_width``
provides a sharper, more precise filter, but is more computationally
expensive.

```python
sample_rate = 48000
resample_rate = 32000

resampled_waveform = F.resample(waveform, sample_rate, resample_rate, lowpass_filter_width=6)
plot_sweep(resampled_waveform, resample_rate, title="lowpass_filter_width=6")

resampled_waveform = F.resample(waveform, sample_rate, resample_rate, lowpass_filter_width=128)
plot_sweep(resampled_waveform, resample_rate, title="lowpass_filter_width=128")
```

Rolloff
~~~~~~~

The ``rolloff`` parameter is represented as a fraction of the Nyquist
frequency, which is the maximal frequency representable by a given
finite sample rate. ``rolloff`` determines the lowpass filter cutoff and
controls the degree of aliasing, which takes place when frequencies
higher than the Nyquist are mapped to lower frequencies. A lower rolloff
will therefore reduce the amount of aliasing, but it will also reduce
some of the higher frequencies.

```python
sample_rate = 48000
resample_rate = 32000

resampled_waveform = F.resample(waveform, sample_rate, resample_rate, rolloff=0.99)
plot_sweep(resampled_waveform, resample_rate, title="rolloff=0.99")

resampled_waveform = F.resample(waveform, sample_rate, resample_rate, rolloff=0.8)
plot_sweep(resampled_waveform, resample_rate, title="rolloff=0.8")
```

Window function
~~~~~~~~~~~~~~~

By default, torchaudio’s resample uses the Hann window filter, which is
a weighted cosine function. It additionally supports the Kaiser window,
which is a near optimal window function that contains an additional
``beta`` parameter that allows for the design of the smoothness of the
filter and width of impulse. This can be controlled using the
``resampling_method`` parameter.

```python
sample_rate = 48000
resample_rate = 32000

resampled_waveform = F.resample(waveform, sample_rate, resample_rate, resampling_method="sinc_interpolation")
plot_sweep(resampled_waveform, resample_rate, title="Hann Window Default")

resampled_waveform = F.resample(waveform, sample_rate, resample_rate, resampling_method="kaiser_window")
plot_sweep(resampled_waveform, resample_rate, title="Kaiser Window Default")
```

Comparison against librosa
--------------------------

torchaudio’s resample function can be used to produce results similar to
that of librosa (resampy)’s kaiser window resampling, with some noise

```python
sample_rate = 48000
resample_rate = 32000

### kaiser_best
resampled_waveform = F.resample(
    waveform,
    sample_rate,
    resample_rate,
    lowpass_filter_width=64,
    rolloff=0.9475937167399596,
    resampling_method="kaiser_window",
    beta=14.769656459379492
)
plot_sweep(resampled_waveform, resample_rate, title="Kaiser Window Best (torchaudio)")

librosa_resampled_waveform = torch.from_numpy(
    librosa.resample(waveform.squeeze().numpy(), sample_rate, resample_rate, res_type='kaiser_best')).unsqueeze(0)
plot_sweep(librosa_resampled_waveform, resample_rate, title="Kaiser Window Best (librosa)")

mse = torch.square(resampled_waveform - librosa_resampled_waveform).mean().item()
print("torchaudio and librosa kaiser best MSE:", mse)

### kaiser_fast
resampled_waveform = F.resample(
    waveform,
    sample_rate,
    resample_rate,
    lowpass_filter_width=16,
    rolloff=0.85,
    resampling_method="kaiser_window",
    beta=8.555504641634386
)
plot_specgram(resampled_waveform, resample_rate, title="Kaiser Window Fast (torchaudio)")

librosa_resampled_waveform = torch.from_numpy(
    librosa.resample(waveform.squeeze().numpy(), sample_rate, resample_rate, res_type='kaiser_fast')).unsqueeze(0)
plot_sweep(librosa_resampled_waveform, resample_rate, title="Kaiser Window Fast (librosa)")

mse = torch.square(resampled_waveform - librosa_resampled_waveform).mean().item()
print("torchaudio and librosa kaiser fast MSE:", mse)
```

Performance Benchmarking
------------------------

Below are benchmarks for downsampling and upsampling waveforms between
two pairs of sampling rates. We demonstrate the performance implications
that the ``lowpass_filter_wdith``, window type, and sample rates can
have. Additionally, we provide a comparison against ``librosa``\ ’s
``kaiser_best`` and ``kaiser_fast`` using their corresponding parameters
in ``torchaudio``.

To elaborate on the results:
- a larger ``lowpass_filter_width`` results in a larger resampling kernel,
  and therefore increases computation time for both the kernel computation
  and convolution
- using ``kaiser_window`` results in longer computation times than the default
  ``sinc_interpolation`` because it is more complex to compute the intermediate
  window values - a large GCD between the sample and resample rate will result
  in a simplification that allows for a smaller kernel and faster kernel computation.

```python
configs = {
    "downsample (48 -> 44.1 kHz)": [48000, 44100],
    "downsample (16 -> 8 kHz)": [16000, 8000],
    "upsample (44.1 -> 48 kHz)": [44100, 48000],
    "upsample (8 -> 16 kHz)": [8000, 1600],
}

for label in configs:
  times, rows = [], []
  sample_rate = configs[label][0]
  resample_rate = configs[label][1]
  waveform = get_sine_sweep(sample_rate)

  # sinc 64 zero-crossings
  f_time = benchmark_resample("functional", waveform, sample_rate, resample_rate, lowpass_filter_width=64)
  t_time = benchmark_resample("transforms", waveform, sample_rate, resample_rate, lowpass_filter_width=64)
  times.append([None, 1000 * f_time, 1000 * t_time])
  rows.append(f"sinc (width 64)")

  # sinc 6 zero-crossings
  f_time = benchmark_resample("functional", waveform, sample_rate, resample_rate, lowpass_filter_width=16)
  t_time = benchmark_resample("transforms", waveform, sample_rate, resample_rate, lowpass_filter_width=16)
  times.append([None, 1000 * f_time, 1000 * t_time])
  rows.append(f"sinc (width 16)")

  # kaiser best
  lib_time = benchmark_resample("librosa", waveform, sample_rate, resample_rate, librosa_type="kaiser_best")
  f_time = benchmark_resample(
      "functional",
      waveform,
      sample_rate,
      resample_rate,
      lowpass_filter_width=64,
      rolloff=0.9475937167399596,
      resampling_method="kaiser_window",
      beta=14.769656459379492)
  t_time = benchmark_resample(
      "transforms",
      waveform,
      sample_rate,
      resample_rate,
      lowpass_filter_width=64,
      rolloff=0.9475937167399596,
      resampling_method="kaiser_window",
      beta=14.769656459379492)
  times.append([1000 * lib_time, 1000 * f_time, 1000 * t_time])
  rows.append(f"kaiser_best")

  # kaiser fast
  lib_time = benchmark_resample("librosa", waveform, sample_rate, resample_rate, librosa_type="kaiser_fast")
  f_time = benchmark_resample(
      "functional",
      waveform,
      sample_rate,
      resample_rate,
      lowpass_filter_width=16,
      rolloff=0.85,
      resampling_method="kaiser_window",
      beta=8.555504641634386)
  t_time = benchmark_resample(
      "transforms",
      waveform,
      sample_rate,
      resample_rate,
      lowpass_filter_width=16,
      rolloff=0.85,
      resampling_method="kaiser_window",
      beta=8.555504641634386)
  times.append([1000 * lib_time, 1000 * f_time, 1000 * t_time])
  rows.append(f"kaiser_fast")

  df = pd.DataFrame(times,
                    columns=["librosa", "functional", "transforms"],
                    index=rows)
  df.columns = pd.MultiIndex.from_product([[f"{label} time (ms)"],df.columns])
  display(df.round(2))
```

Data Augmentation
=================

``torchaudio`` provides a variety of ways to augment audio data.

Applying effects and filtering
------------------------------

``torchaudio.sox_effects`` module provides ways to apply filiters like
``sox`` command on Tensor objects and file-object audio sources
directly.

There are two functions for this;

-  ``torchaudio.sox_effects.apply_effects_tensor`` for applying effects
   on Tensor
-  ``torchaudio.sox_effects.apply_effects_file`` for applying effects on
   other audio source

Both function takes effects in the form of ``List[List[str]]``. This
mostly corresponds to how ``sox`` command works, but one caveat is that
``sox`` command adds some effects automatically, but torchaudio’s
implementation does not do that.

For the list of available effects, please refer to `the sox
documentation <http://sox.sourceforge.net/sox.html>`__.

**Tip** If you need to load and resample your audio data on-the-fly,
then you can use ``torchaudio.sox_effects.apply_effects_file`` with
``"rate"`` effect.

**Note** ``apply_effects_file`` accepts file-like object or path-like
object. Similar to ``torchaudio.load``, when the audio format cannot be
detected from either file extension or header, you can provide
``format`` argument to tell what format the audio source is.

**Note** This process is not differentiable.

```python
# Load the data
waveform1, sample_rate1 = get_sample(resample=16000)

# Define effects
effects = [
  ["lowpass", "-1", "300"], # apply single-pole lowpass filter
  ["speed", "0.8"],  # reduce the speed
                     # This only changes sample rate, so it is necessary to
                     # add `rate` effect with original sample rate after this.
  ["rate", f"{sample_rate1}"],
  ["reverb", "-w"],  # Reverbration gives some dramatic feeling
]

# Apply effects
waveform2, sample_rate2 = torchaudio.sox_effects.apply_effects_tensor(
    waveform1, sample_rate1, effects)

plot_waveform(waveform1, sample_rate1, title="Original", xlim=(-.1, 3.2))
plot_waveform(waveform2, sample_rate2, title="Effects Applied", xlim=(-.1, 3.2))
print_stats(waveform1, sample_rate=sample_rate1, src="Original")
print_stats(waveform2, sample_rate=sample_rate2, src="Effects Applied")
```

Note that the number of frames and number of channels are different from
the original after the effects. Let’s listen to the audio. Doesn’t it
sound more dramatic?

```python
plot_specgram(waveform1, sample_rate1, title="Original", xlim=(0, 3.04))
play_audio(waveform1, sample_rate1)
plot_specgram(waveform2, sample_rate2, title="Effects Applied", xlim=(0, 3.04))
play_audio(waveform2, sample_rate2)
```

Simulating room reverbration
----------------------------

`Convolution
reverb <https://en.wikipedia.org/wiki/Convolution_reverb>`__ is a
technique used to make a clean audio data sound like in a different
environment.

Using Room Impulse Response (RIR), we can make a clean speech sound like
uttered in a conference room.

For this process, we need RIR data. The following data are from VOiCES
dataset, but you can record one by your self. Just turn on microphone
and clap you hands.

```python
sample_rate = 8000

rir_raw, _ = get_rir_sample(resample=sample_rate)

plot_waveform(rir_raw, sample_rate, title="Room Impulse Response (raw)", ylim=None)
plot_specgram(rir_raw, sample_rate, title="Room Impulse Response (raw)")
play_audio(rir_raw, sample_rate)
```

First, we need to clean up the RIR. We extract the main impulse,
normalize the signal power, then flip the time axis.

```python
rir = rir_raw[:, int(sample_rate*1.01):int(sample_rate*1.3)]
rir = rir / torch.norm(rir, p=2)
rir = torch.flip(rir, [1])

print_stats(rir)
plot_waveform(rir, sample_rate, title="Room Impulse Response", ylim=None)
```

Then we convolve the speech signal with the RIR filter.

```python
speech, _ = get_speech_sample(resample=sample_rate)

speech_ = torch.nn.functional.pad(speech, (rir.shape[1]-1, 0))
augmented = torch.nn.functional.conv1d(speech_[None, ...], rir[None, ...])[0]

plot_waveform(speech, sample_rate, title="Original", ylim=None)
plot_waveform(augmented, sample_rate, title="RIR Applied", ylim=None)

plot_specgram(speech, sample_rate, title="Original")
play_audio(speech, sample_rate)

plot_specgram(augmented, sample_rate, title="RIR Applied")
play_audio(augmented, sample_rate)
```

Adding background noise
-----------------------

To add background noise to audio data, you can simply add audio Tensor
and noise Tensor. A commonly way to adjust the intensity of noise is to
change Signal-to-Noise Ratio (SNR).
[`wikipedia <https://en.wikipedia.org/wiki/Signal-to-noise_ratio>`__]

\begin{align}\mathrm{SNR} = \frac{P_\mathrm{signal}}{P_\mathrm{noise}}\end{align}

\begin{align}{\mathrm  {SNR_{{dB}}}}=10\log _{{10}}\left({\mathrm  {SNR}}\right)\end{align}

```python
sample_rate = 8000
speech, _ = get_speech_sample(resample=sample_rate)
noise, _ = get_noise_sample(resample=sample_rate)
noise = noise[:, :speech.shape[1]]

plot_waveform(noise, sample_rate, title="Background noise")
plot_specgram(noise, sample_rate, title="Background noise")
play_audio(noise, sample_rate)

speech_power = speech.norm(p=2)
noise_power = noise.norm(p=2)

for snr_db in [20, 10, 3]:
  snr = math.exp(snr_db / 10)
  scale = snr * noise_power / speech_power
  noisy_speech = (scale * speech + noise) / 2

  plot_waveform(noisy_speech, sample_rate, title=f"SNR: {snr_db} [dB]")
  plot_specgram(noisy_speech, sample_rate, title=f"SNR: {snr_db} [dB]")
  play_audio(noisy_speech, sample_rate)
```

Applying codec to Tensor object
-------------------------------

``torchaudio.functional.apply_codec`` can apply codecs to Tensor object.

**Note** This process is not differentiable.

```python
waveform, sample_rate = get_speech_sample(resample=8000)

plot_specgram(waveform, sample_rate, title="Original")
play_audio(waveform, sample_rate)

configs = [
    ({"format": "wav", "encoding": 'ULAW', "bits_per_sample": 8}, "8 bit mu-law"),
    ({"format": "gsm"}, "GSM-FR"),
    ({"format": "mp3", "compression": -9}, "MP3"),
    ({"format": "vorbis", "compression": -1}, "Vorbis"),
]
for param, title in configs:
  augmented = F.apply_codec(waveform, sample_rate, **param)
  plot_specgram(augmented, sample_rate, title=title)
  play_audio(augmented, sample_rate)
```

Simulating a phone recoding
---------------------------

Combining the previous techniques, we can simulate audio that sounds
like a person talking over a phone in a echoey room with people talking
in the background.

```python
sample_rate = 16000
speech, _ = get_speech_sample(resample=sample_rate)

plot_specgram(speech, sample_rate, title="Original")
play_audio(speech, sample_rate)

# Apply RIR
rir, _ = get_rir_sample(resample=sample_rate, processed=True)
speech_ = torch.nn.functional.pad(speech, (rir.shape[1]-1, 0))
speech = torch.nn.functional.conv1d(speech_[None, ...], rir[None, ...])[0]

plot_specgram(speech, sample_rate, title="RIR Applied")
play_audio(speech, sample_rate)

# Add background noise
# Because the noise is recorded in the actual environment, we consider that
# the noise contains the acoustic feature of the environment. Therefore, we add
# the noise after RIR application.
noise, _ = get_noise_sample(resample=sample_rate)
noise = noise[:, :speech.shape[1]]

snr_db = 8
scale = math.exp(snr_db / 10) * noise.norm(p=2) / speech.norm(p=2)
speech = (scale * speech + noise) / 2

plot_specgram(speech, sample_rate, title="BG noise added")
play_audio(speech, sample_rate)

# Apply filtering and change sample rate
speech, sample_rate = torchaudio.sox_effects.apply_effects_tensor(
  speech,
  sample_rate,
  effects=[
      ["lowpass", "4000"],
      ["compand", "0.02,0.05", "-60,-60,-30,-10,-20,-8,-5,-8,-2,-8", "-8", "-7", "0.05"],
      ["rate", "8000"],
  ],
)

plot_specgram(speech, sample_rate, title="Filtered")
play_audio(speech, sample_rate)

# Apply telephony codec
speech = F.apply_codec(speech, sample_rate, format="gsm")

plot_specgram(speech, sample_rate, title="GSM Codec Applied")
play_audio(speech, sample_rate)
```

Feature Extractions
===================

``torchaudio`` implements feature extractions commonly used in audio
domain. They are available in ``torchaudio.functional`` and
``torchaudio.transforms``.

``functional`` module implements features as a stand alone functions.
They are stateless.

``transforms`` module implements features in object-oriented manner,
using implementations from ``functional`` and ``torch.nn.Module``.

Because all the transforms are subclass of ``torch.nn.Module``, they can
be serialized using TorchScript.

For the complete list of available features, please refer to the
documentation. In this tutorial, we will look into conversion between
time domain and frequency domain (``Spectrogram``, ``GriffinLim``,
``MelSpectrogram``) and augmentation technique called SpecAugment.

Spectrogram
-----------

To get the frequency representation of audio signal, you can use
``Spectrogram`` transform.

```python
waveform, sample_rate = get_speech_sample()

n_fft = 1024
win_length = None
hop_length = 512

# define transformation
spectrogram = T.Spectrogram(
    n_fft=n_fft,
    win_length=win_length,
    hop_length=hop_length,
    center=True,
    pad_mode="reflect",
    power=2.0,
)
# Perform transformation
spec = spectrogram(waveform)

print_stats(spec)
plot_spectrogram(spec[0], title='torchaudio')
```

GriffinLim
----------

To recover a waveform from spectrogram, you can use ``GriffinLim``.

```python
torch.random.manual_seed(0)
waveform, sample_rate = get_speech_sample()
plot_waveform(waveform, sample_rate, title="Original")
play_audio(waveform, sample_rate)

n_fft = 1024
win_length = None
hop_length = 512

spec = T.Spectrogram(
    n_fft=n_fft,
    win_length=win_length,
    hop_length=hop_length,
)(waveform)

griffin_lim = T.GriffinLim(
    n_fft=n_fft,
    win_length=win_length,
    hop_length=hop_length,
)
waveform = griffin_lim(spec)

plot_waveform(waveform, sample_rate, title="Reconstructed")
play_audio(waveform, sample_rate)
```

Mel Filter Bank
---------------

``torchaudio.functional.create_fb_matrix`` can generate the filter bank
to convert frequency bins to Mel-scale bins.

Since this function does not require input audio/features, there is no
equivalent transform in ``torchaudio.transforms``.

```python
n_fft = 256
n_mels = 64
sample_rate = 6000

mel_filters = F.create_fb_matrix(
    int(n_fft // 2 + 1),
    n_mels=n_mels,
    f_min=0.,
    f_max=sample_rate/2.,
    sample_rate=sample_rate,
    norm='slaney'
)
plot_mel_fbank(mel_filters, "Mel Filter Bank - torchaudio")
```

Comparison against librosa
~~~~~~~~~~~~~~~~~~~~~~~~~~

As a comparison, here is the equivalent way to get the mel filter bank
with ``librosa``.

```python
mel_filters_librosa = librosa.filters.mel(
    sample_rate,
    n_fft,
    n_mels=n_mels,
    fmin=0.,
    fmax=sample_rate/2.,
    norm='slaney',
    htk=True,
).T

plot_mel_fbank(mel_filters_librosa, "Mel Filter Bank - librosa")

mse = torch.square(mel_filters - mel_filters_librosa).mean().item()
print('Mean Square Difference: ', mse)
```

MelSpectrogram
--------------

Mel-scale spectrogram is a combination of Spectrogram and mel scale
conversion. In ``torchaudio``, there is a transform ``MelSpectrogram``
which is composed of ``Spectrogram`` and ``MelScale``.

```python
waveform, sample_rate = get_speech_sample()

n_fft = 1024
win_length = None
hop_length = 512
n_mels = 128

mel_spectrogram = T.MelSpectrogram(
    sample_rate=sample_rate,
    n_fft=n_fft,
    win_length=win_length,
    hop_length=hop_length,
    center=True,
    pad_mode="reflect",
    power=2.0,
    norm='slaney',
    onesided=True,
    n_mels=n_mels,
    mel_scale="htk",
)

melspec = mel_spectrogram(waveform)
plot_spectrogram(
    melspec[0], title="MelSpectrogram - torchaudio", ylabel='mel freq')
```

Comparison against librosa
~~~~~~~~~~~~~~~~~~~~~~~~~~

As a comparison, here is the equivalent way to get Mel-scale spectrogram
with ``librosa``.

```python
melspec_librosa = librosa.feature.melspectrogram(
    waveform.numpy()[0],
    sr=sample_rate,
    n_fft=n_fft,
    hop_length=hop_length,
    win_length=win_length,
    center=True,
    pad_mode="reflect",
    power=2.0,
    n_mels=n_mels,
    norm='slaney',
    htk=True,
)
plot_spectrogram(
    melspec_librosa, title="MelSpectrogram - librosa", ylabel='mel freq')

mse = torch.square(melspec - melspec_librosa).mean().item()
print('Mean Square Difference: ', mse)
```

MFCC
----

```python
waveform, sample_rate = get_speech_sample()

n_fft = 2048
win_length = None
hop_length = 512
n_mels = 256
n_mfcc = 256

mfcc_transform = T.MFCC(
    sample_rate=sample_rate,
    n_mfcc=n_mfcc,
    melkwargs={
      'n_fft': n_fft,
      'n_mels': n_mels,
      'hop_length': hop_length,
      'mel_scale': 'htk',
    }
)

mfcc = mfcc_transform(waveform)

plot_spectrogram(mfcc[0])
```

Comparing against librosa
~~~~~~~~~~~~~~~~~~~~~~~~~

```python
melspec = librosa.feature.melspectrogram(
  y=waveform.numpy()[0], sr=sample_rate, n_fft=n_fft,
  win_length=win_length, hop_length=hop_length,
  n_mels=n_mels, htk=True, norm=None)

mfcc_librosa = librosa.feature.mfcc(
  S=librosa.core.spectrum.power_to_db(melspec),
  n_mfcc=n_mfcc, dct_type=2, norm='ortho')

plot_spectrogram(mfcc_librosa)

mse = torch.square(mfcc - mfcc_librosa).mean().item()
print('Mean Square Difference: ', mse)
```

Pitch
-----

```python
waveform, sample_rate = get_speech_sample()

pitch = F.detect_pitch_frequency(waveform, sample_rate)
plot_pitch(waveform, sample_rate, pitch)
play_audio(waveform, sample_rate)
```

Kaldi Pitch (beta)
------------------

Kaldi Pitch feature [1] is pitch detection mechanism tuned for ASR
application. This is a beta feature in torchaudio, and only
``functional`` form is available.

1. A pitch extraction algorithm tuned for automatic speech recognition

   Ghahremani, B. BabaAli, D. Povey, K. Riedhammer, J. Trmal and S.
   Khudanpur

   2014 IEEE International Conference on Acoustics, Speech and Signal
   Processing (ICASSP), Florence, 2014, pp. 2494-2498, doi:
   10.1109/ICASSP.2014.6854049.
   [`abstract <https://ieeexplore.ieee.org/document/6854049>`__],
   [`paper <https://danielpovey.com/files/2014_icassp_pitch.pdf>`__]

```python
waveform, sample_rate = get_speech_sample(resample=16000)

pitch_feature = F.compute_kaldi_pitch(waveform, sample_rate)
pitch, nfcc = pitch_feature[..., 0], pitch_feature[..., 1]

plot_kaldi_pitch(waveform, sample_rate, pitch, nfcc)
play_audio(waveform, sample_rate)
```

Feature Augmentation
====================

SpecAugment
-----------

`SpecAugment <https://ai.googleblog.com/2019/04/specaugment-new-data-augmentation.html>`__
is a popular augmentation technique applied on spectrogram.

``torchaudio`` implements ``TimeStrech``, ``TimeMasking`` and
``FrequencyMasking``.

TimeStrech
~~~~~~~~~~

```python
spec = get_spectrogram(power=None)
strech = T.TimeStretch()

rate = 1.2
spec_ = strech(spec, rate)
plot_spectrogram(F.complex_norm(spec_[0]), title=f"Stretched x{rate}", aspect='equal', xmax=304)

plot_spectrogram(F.complex_norm(spec[0]), title="Original", aspect='equal', xmax=304)

rate = 0.9
spec_ = strech(spec, rate)
plot_spectrogram(F.complex_norm(spec_[0]), title=f"Stretched x{rate}", aspect='equal', xmax=304)
```

TimeMasking
~~~~~~~~~~~

```python
torch.random.manual_seed(4)

spec = get_spectrogram()
plot_spectrogram(spec[0], title="Original")

masking = T.TimeMasking(time_mask_param=80)
spec = masking(spec)

plot_spectrogram(spec[0], title="Masked along time axis")
```

FrequencyMasking
~~~~~~~~~~~~~~~~

```python
torch.random.manual_seed(4)

spec = get_spectrogram()
plot_spectrogram(spec[0], title="Original")

masking = T.FrequencyMasking(freq_mask_param=80)
spec = masking(spec)

plot_spectrogram(spec[0], title="Masked along frequency axis")
```

Datasets
========

``torchaudio`` provides easy access to common, publicly accessible
datasets. Please checkout the official documentation for the list of
available datasets.

Here, we take ``YESNO`` dataset and look into how to use it.

```python
YESNO_DOWNLOAD_PROCESS.join()

dataset = torchaudio.datasets.YESNO(YESNO_DATASET_PATH, download=True)

for i in [1, 3, 5]:
  waveform, sample_rate, label = dataset[i]
  plot_specgram(waveform, sample_rate, title=f"Sample {i}: {label}")
  play_audio(waveform, sample_rate)
```


# Funciones de utilidad y aversión al riesgo

Antes de comenzar la clase, por favor entrar [este enlace](http://cursos.iteso.mx/course/view.php?id=1480)

En el módulo anterior aprendimos
- qué es un portafolio, cómo medir su rendimiento esperado y su volatilidad;
- un portafolio de activos riesgosos tiene menos riesgo que la suma ponderada de los riesgos individuales,
- y que esto se logra mediante el concepto de diversificación;
- la diversificación elimina el riesgo idiosincrático, que es el que afecta a cada compañía en particular,
- sin embargo, el riesgo de mercado no se puede eliminar porque afecta a todos por igual.
- Finalmente, aprendimos conceptos importantes como frontera de mínima varianza, portafolios eficientes y el portafolio de mínima varianza, que son claves en el problema de selección óptima de portafolios.

Muy bien, sin embargo, para plantear el problema de selección óptima de portafolios necesitamos definir la función que vamos a optimizar: función de utilidad.

**Objetivos:**
- ¿Cómo tomamos decisiones según los economistas?
- ¿Cómo toman decisiones los inversionistas?
- ¿Qué son las funciones de utilidad?

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

## 1. Introducción

La teoría económica comienza con una suposición muy importante:
- **cada individuo actúa para obtener el mayor beneficio posible con los recursos disponibles**.
- En otras palabras, **maximizan su propia utilidad**

¿Qué es utilidad?
- Es un concepto relacionado con la felicidad, pero más amplio.
- Por ejemplo, yo obtengo utilidad de lavar mis dientes o comer sano. Ninguna de las dos me brindan felicidad, pero lo primero mantendrá mis dientes sanos y en el largo plazo, lo segundo probablemente contribuirá a una buena vejez.

Los economistas no se preocupan en realidad por lo que nos da utilidad, sino simplemente que cada uno de nosotros tiene sus propias preferencias.
- Por ejemplo, a mi me gusta el café, el fútbol, los perros, la academia, viajar, entre otros.
- Ustedes tienen sus propias preferencias también.

La vida es compleja y con demasiada incertidumbre. Debemos tomar decisiones a cada momento, y estas decisiones involucran ciertos "trade-off".
- Por ejemplo, normalmente tenemos una compensación entre utilidad hoy contra utilidad en el futuro.
- Debemos balancear nuestro consumo hoy contra nuestro consumo luego.
- Por ejemplo, ustedes gastan cerca de cuatro horas a la semana viniendo a clases de portafolios, porque esperan que esto contribuya a mejorar su nivel de vida en el futuro.

De manera que los economistas dicen que cada individuo se comporta como el siguiente optimizador:

\begin{align}
\max & \quad\text{Utilidad}\\
\text{s. a.} & \quad\text{Recursos disponibles}
\end{align}

¿Qué tiene que ver todo esto con el curso?
- En este módulo desarrollaremos herramientas para describir las preferencias de los inversionistas cuando se encuentran con decisiones de riesgo y rendimiento.
- Veremos como podemos medir la actitud frente al riesgo, ¿cuánto te gusta o disgusta el riesgo?
- Finalmente, veremos como podemos formular el problema de maximizar la utilidad de un inversionista para tomar la decisión de inversión óptima.
___

## 2. Funciones de utilidad.

¿Cómo tomamos decisiones?
Por ejemplo:
- Ustedes tienen que decidir si venir a clase o quedarse en su casa viendo Netflix, o ir al gimnasio.
- Tienen que decidir entre irse de fiesta cada fin, o ahorrar para salir de vacaciones.

En el caso de un portafolio, la decisión que se debe tomar es **¿cuáto riesgo estás dispuesto a tomar por qué cantidad de rendimiento?**

**¿Cómo evaluarías el "trade-off" entre tener cetes contra una estrategia muy riesgosa con un posible altísimo rendimiento?**

De manera que veremos como tomamos decisiones cuando tenemos distintas posibilidades. Específicamente, hablaremos acerca de las **preferencias**, como los economistas usan dichas preferencias para explicar las decisiones y los "trade-offs" en dichas decisiones.

Usamos las **preferencias** para describir las decisiones que tomamos. Las preferencias nos dicen cómo un individuo evalúa los "trade-offs" entre distintas elecciones.

Por definición, las preferencias son únicas para cada individuo. En el problema de selección de portafolios:
- las preferencias que dictan cuánto riesgo estás dispuesto a asumir por cuánto rendimiento, son específicas para cada uno de ustedes.
- Sus respuestas a esa pregunta pueden ser muy distintas, porque tenemos distintas preferencias.

Ahora, nosotros no podemos *cuantificar* dichas preferencias.
- Por esto usamos el concepto de utilidad, para medir qué tan satisfecho está un individuo con sus elecciones.
- Así que podemos pensar en la utilidad como un indicador numérico que describe las preferencias,
- o un índice que nos ayuda a clasificar diferentes decisiones.
- En términos simples, **la utilidad nos ayuda a transmitir a números la noción de cómo te sientes**;
- mientras más utilidad, mejor te sientes.

**Función de utilidad**: manera sistemática de asignar una medida o indicador numérico para clasificar diferentes escogencias.

El número que da una función de utilidad no tiene significado alguno. Simplemente es una manera de clasificar diferentes decisiones.

**Ejemplo.**

Podemos escribir la utilidad de un inversionista como función de la riqueza,

$$U(W).$$

- $U(W)$ nos da una medida de qué tan satisfechos estamos con el nivel de riqueza que tenemos.
- $U(W)$ no es la riqueza como tal, sino que la función de utilidad traduce la cantidad de riqueza en un índice numérico subjetivo.

¿Cómo luciría gráficamente una función de utilidad de riqueza $U(W)$?

 Ver en el tablero
- ¿Qué caracteristicas debe tener?
- ¿Cómo es su primera derivada?
- ¿Cómo es su segunda derivada?
- Tiempos buenos: riqueza alta (¿cómo es la primera derivada acá?)
- Tiempos malos: poca riqueza (¿cómo es la primera derivada acá?)

```python
from matplotlib import pyplot as plt
%matplotlib inline
import numpy as np
```

```python
W = np.linspace(0, 1.5, 100)
U1 = W
U2 = W**2
U3 = W**0.5
```

```python
plt.figure(figsize=(6, 4))
plt.plot(W, U1, label=r'$U_1(W)=W$')
plt.plot(W, U2, label=r'$U_2(W)=W^2$')
plt.plot(W, U3, label=r'$U_3(W)=\sqrt{W}$')
plt.grid()
plt.legend(loc='best')
plt.xlabel('Wealth ($W$)')
plt.ylabel('Utility ($U$)')
#plt.axis([0, 0.4, 0, 0.8])
#plt.axis([1, 1.2, 1, 1.2])
```

## 3. Aversión al riesgo

Una dimensión importante en la toma de decisiones en finanzas y economía es la **incertidumbre**. Probablemente no hay ninguna decisión en economía que no involucre riesgo.

- A la mayoría de las personas no les gusta mucho el riesgo.
- De hecho, estudios del comportamiento humano de cara al riesgo, sugieren fuertemente que los seres humanos somos aversos al riesgo.
- Por ejemplo, la mayoría de hogares poseen seguros para sus activos.
- Así, cuando planteamos el problema de selección óptima de portafolios, suponemos que el inversionista es averso al riesgo.

¿Qué significa esto en términos de preferencias? ¿Cómo lo medimos?

- Como seres humanos, todos tenemos diferentes genes y preferencias, y esto aplica también a la actitud frente al riesgo.
- Por tanto, la aversión al riesgo es clave en cómo describimos las preferencias de un inversinista.
- Individuos con un alto grado de aversión al riesgo valorarán la seguridad a un alto precio, mientras otros no tanto.
- De manera que alguien con alta aversión al riesgo, no querrá enfrentarse a una situación con resultado incierto y querrá pagar una gran prima de seguro para eliminar dicho riesgo.
- O equivalentemente, una persona con alta aversión al riesgo requerirá una compensación alta si se decide a asumir ese riesgo.

El **grado de aversión al riesgo** mide qué tanto un inversionista prefiere un resultado seguro a un resultado incierto.

Lo opuesto a aversión al riesgo es **tolerancia al riesgo**.

 Ver en el tablero gráficamente, cómo se explica la aversión al riesgo desde las funciones de utilidad.

**Conclusión:** la concavidad en la función de utilidad dicta qué tan averso al riesgo es el individuo.

```python
def straight(x1, y1, x2, y2, x):
    m = (y2 - y1) / (x2 - x1)
    return m * (x - x1) + y1
```

```python
plt.figure(figsize=(6, 4))
plt.plot(W, U3, label=r'$U_3(W)=\sqrt{W}$')
x1, x2 = 0.1, 1.0
y1, y2 = x1**0.5, x2**0.5
plt.axvline(x=x1, ls='--', color='grey')
plt.axvline(x=x2, ls='--', color='grey')
plt.axhline(y=y1, ls='--', color='grey')
plt.axhline(y=y2, ls='--', color='grey')
plt.plot(x1, y1, '*b', ms=5)
plt.plot(x2, y2, '*g', ms=5)
plt.plot(0.5 * x1 + 0.5 * x2, 0.5 * y1 + 0.5 * y2, 'or', ms=5)
plt.plot(0.5 * x1 + 0.5 * x2, (0.5 * x1 + 0.5 * x2)**0.5, 'om', ms=5)
plt.plot(W, straight(x1, y1, x2, y2, W))
plt.grid()
plt.legend(loc='best')
plt.xlabel('Wealth ($W$)')
plt.ylabel('Utility ($U$)')
```

```python
0.5 * 0.1 + 0.5 * 1.0
```

    0.55

```python
0.5 * y1 + 0.5 * y2
```

    0.658113883008419

### ¿Cómo medimos el grado de aversión al riesgo de un individuo?

¿Saben cuál es su coeficiente de aversión al riesgo? Podemos estimarlo.

Suponga que se puede participar en la siguiente lotería:
- usted puede ganar $\$1000$ con $50\%$ de probabilidad, o
- puede ganar $\$500$ con $50\%$ de probabilidad.

Es decir, de entrada usted tendrá $\$500$ seguros pero también tiene la posibilidad de ganar $\$1000$.

¿Cuánto estarías dispuesto a pagar por esta oportunidad?

Bien, podemos relacionar tu respuesta con tu coeficiente de aversión al riesgo.

| Coeficiente de aversión al riesgo | Cantidad que pagarías |
| --------------------------------- | --------------------- |
| 0                                 | 750                   |
| 0.5                               | 729                   |
| 1                                 | 707                   |
| 2                                 | 667                   |
| 3                                 | 632                   |
| 4                                 | 606                   |
| 5                                 | 586                   |
| 10                                | 540                   |
| 15                                | 525                   |
| 20                                | 519                   |
| 50                                | 507                   |

La mayoría de la gente está dispuesta a pagar entre $\$540$ (10) y $\$707$ (1). Es muy raro encontrar coeficientes de aversión al riesgo menores a 1. Esto está soportado por una gran cantidad de encuestas.

- En el mundo financiero, los consultores financieros utilizan cuestionarios para medir el coeficiente de aversión al riesgo.

**Ejemplo.** Describir en términos de aversión al riesgo las siguientes funciones de utilidad que dibujaré en el tablero.
___

# Anuncios

## 1. Quiz la siguiente clase (pendientes del chat).
## 2. Tarea 4 entrega 2 para hoy, viernes 13 de marzo.
## 3. Tarea 5 entrega 2 para martes 17 de marzo.

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