```python
import numpy as np

def f(x):
    return np.sqrt(x + 2)

```

```python
x = np.array([-2, -1, 0, 2, 4, 6])  # Creando el vector de valores de x
y = f(x)
y

```

    array([0.        , 1.        , 1.41421356, 2.        , 2.44948974,
           2.82842712])

```python
import pandas as pd

tabla = pd.DataFrame( list(zip(x, y)), columns=['x', 'f(x)'] )
tabla

```

      x
      f(x)

      0
      -2
      0.000000

      1
      -1
      1.000000

      2
      0
      1.414214

      3
      2
      2.000000

      4
      4
      2.449490

      5
      6
      2.828427

```python
%matplotlib inline

import matplotlib.pyplot as plt

def move_spines():
    """Esta funcion divide pone al eje y en el valor
    0 de x para dividir claramente los valores positivos y
    negativos."""
    fix, ax = plt.subplots()
    for spine in ["left", "bottom"]:
        ax.spines[spine].set_position("zero")

    for spine in ["right", "top"]:
        ax.spines[spine].set_color("none")

    return ax

x = np.linspace(-2, 6, num=30)

ax = move_spines()
ax.grid()
ax.plot(x, f(x))
plt.title(r"Grafico de $f(x)=\sqrt{x + 2}$")
plt.ylabel('f(x)')
plt.xlabel('x')
plt.show()
```

```python

def f(x):
    return x**2 - x + 2

x = np.array([1, 1.5, 1.9, 1.95, 1.99, 1.999, 2.001, 2.05, 2.1, 2.2, 2.5, 3 ])
y = f(x)
tabla = pd.DataFrame(list(zip(x, y)), columns=['x', 'f(x)'])
tabla
```

      x
      f(x)

      0
      1.000
      2.000000

      1
      1.500
      2.750000

      2
      1.900
      3.710000

      3
      1.950
      3.852500

      4
      1.990
      3.970100

      5
      1.999
      3.997001

      6
      2.001
      4.003001

      7
      2.050
      4.152500

      8
      2.100
      4.310000

      9
      2.200
      4.640000

      10
      2.500
      5.750000

      11
      3.000
      8.000000

```python
x = np.linspace(-2, 4, num=30)

ax = move_spines()
ax.grid()
ax.plot(x, f(x))
ax.scatter(2, 4, label="limite cuando x tiende a 2", color='r')
plt.legend()
plt.title(r"Grafico de $f(x)=x^2 -x + 2$")
plt.ylabel('f(x)')
plt.xlabel('x')
plt.show()
```

```python

from sympy.interactive import printing
from sympy import Limit, limit, Symbol, S

# imprimir con notación matemática.
printing.init_printing(use_latex='mathjax')

x = Symbol('x') # Creando el simbolo x.
Limit(x**2 - x + 2, x, 2) # Creando el objeto Limit
```

$\displaystyle \lim_{x \to 2^+}\left(x^{2} - x + 2\right)$

```python

# Resolviendo el Limite con el metodo doit()
Limit(x**2 - x + 2, x, 2).doit()
```

$\displaystyle 4$

```python

# La funcion limit nos da directamente el resultado
limit(x**2 - x + 2, x, 2)
```

$\displaystyle 4$

```python

Limit(1/x, x, S.Infinity).doit()
```

$\displaystyle 0$

```python

from sympy import Derivative, diff, simplify

fx = (2*x + 1)*(x**3 + 2)
dx = Derivative(fx, x).doit()
dx
```

$\displaystyle 2 x^{3} + 3 x^{2} \left(2 x + 1\right) + 4$

```python
simplify(dx)
```

$\displaystyle 8 x^{3} + 3 x^{2} + 4$

```python

# Derivada de segundo orden con el 3er argumento.
Derivative(fx, x, 2).doit()
```

$\displaystyle 6 x \left(4 x + 1\right)$

```python

# Calculando derivada de (3x +1) / (2x)
fx = (3*x + 1) / (2*x)
dx = Derivative(fx, x).doit()
simplify(dx)
```

$\displaystyle - \frac{1}{2 x^{2}}$

```python
simplify(diff(fx, x))
```

$\displaystyle - \frac{1}{2 x^{2}}$

```python
diff(fx, x).subs(x, 1)
```

$\displaystyle - \frac{1}{2}$

```python
from sympy import Integral, integrate

fx = x**3 - 6*x
dx = Integral(fx, x).doit()
dx
```

$\displaystyle \frac{x^{4}}{4} - 3 x^{2}$

```python
integrate(fx, x)
```

$\displaystyle \frac{x^{4}}{4} - 3 x^{2}$

```python
Integral(fx, (x, 0, 3)).doit()
```

$\displaystyle - \frac{27}{4}$

```python
diff(integrate(fx))
```

$\displaystyle x^{3} - 6 x$

```python
integrate(diff(fx))
```

$\displaystyle x^{3} - 6 x$

```python

```


# Modeling the global energy budget

## Introducing the zero-dimensional Energy Balance Model

____________

## 1. Recap of the global energy budget
____________

Let's look again at the observations:

____________

## 2. Tuning radiative fluxes to the observations
____________

### Recap of our simple greenhouse model

Last class we introduced a very simple model for the **OLR** or Outgoing Longwave Radiation to space:

$$ \text{OLR} = \tau \sigma T_s^4 $$

where $\tau$ is the **transmissivity** of the atmosphere, a number less than 1 that represents the greenhouse effect of Earth's atmosphere.

We also tuned this model to the observations by choosing $ \tau \approx 0.61$.

More precisely:

```python
OLR_obs = 238.5  # in W/m2
sigma = 5.67E-8  # S-B constant
Ts_obs = 288.  # global average surface temperature
tau = OLR_obs / sigma / Ts_obs**4  # solve for tuned value of transmissivity
print(tau)
```

    0.6114139923687016

Let's now deal with the shortwave (solar) side of the energy budget.

### Absorbed Shortwave Radiation (ASR) and Planetary Albedo

Let's define a few terms.

#### Global mean insolation

From the observations, the area-averaged incoming solar radiation, or **insolation**, is 341.3 W m$^{-2}$.

Let's denote this quantity by $Q$.

```python
Q = 341.3  # the insolation
```

#### Planetary albedo

Some of the incoming radiation is not absorbed at all but simply reflected back to space. Let's call this quantity $F_{reflected}$

From observations we have:

```python
Freflected = 101.9  #  reflected shortwave flux in W/m2
```

The **planetary albedo** is the fraction of $Q$ that is reflected.

We will denote the planetary albedo by $\alpha$.

From the observations:

```python
alpha = Freflected / Q
print(alpha)
```

    0.29856431292118374

That is, about 30% of the incoming radiation is reflected back to space.

#### Absorbed Shortwave Radiation

The **Absorbed Shortwave Radiation** or ASR is the part of the incoming sunlight that is *not* reflected back to space, i.e. that part that is absorbed somewhere within the Earth system.

Mathematically we write

$$ \text{ASR} = Q - F_{reflected}\\
= Q-\alpha Q \\
= (1-\alpha) Q $$

From the observations:

```python
ASRobserved = Q - Freflected
print(ASRobserved)
```

    239.4

As we noted last time, this number is *just slightly greater* than the observed OLR of 238.5 W m$^{-2}$.

____________

## 3. Equilibrium temperature
____________

*This is one of the central concepts in climate modeling.*

The Earth system is in **energy balance** when energy in = energy out, i.e., when

$$ \text{ASR} = \text{OLR} $$

We want to know:

- What surface temperature do we need to have this balance?
- By how much would the temperature change in response to other changes in Earth system?
    - Changes in greenhouse gases
    - Changes in cloudiness
    - etc.

With our simple greenhouse model, we can get an **exact solution** for the equilibrium temperature.

First, write down our statement of energy balance:

$$ (1-\alpha) Q = \tau \sigma T_s^4 $$

Rearrange to solve for $T_s$:

$$ T_s^4 = \frac{(1-\alpha) Q}{\tau \sigma} $$

and take the fourth root, denoting our **equilibrium temperature** as $T_{eq}$:

$$ T_{eq} = \left( \frac{(1-\alpha) Q}{\tau \sigma} \right)^\frac{1}{4} $$

Plugging the observed values back in, we compute:

```python
#  define a reusable function
def equilibrium_temperature(alpha, Q, tau):
    return ((1-alpha) *Q / (tau * sigma))**(1/4)

# call the function, passing arguments, and assign the return value to a new variable
Teq_obs = equilibrium_temperature(alpha, Q, tau)
print(Teq_obs)
```

    288.27131447889224

And this equilibrium temperature is *just slightly warmer* than 288 K.

____________
## 4. A climate change scenario
____________

Suppose that, due to global warming (changes in atmospheric composition and subsequent changes in cloudiness):

- The longwave transmissitivity decreases to $\tau = 0.57$
- The planetary albedo increases to $\alpha = 0.32$

What is the ***new equilibrium temperature***?

For this very simple model, we can work out the answer exactly:

```python
Teq_new = equilibrium_temperature(0.32, Q, 0.57)
#  an example of formatted print output, limiting to two or one decimal places
print('The new equilibrium temperature is {:.2f} K.'.format(Teq_new))
print('The equilibrium temperature increased by about {:.1f} K.'.format(Teq_new-Teq_obs))
```

    The new equilibrium temperature is 291.10 K.
    The equilibrium temperature increased by about 2.8 K.

Most climate models are more complicated mathematically, and solving directly for the equilibrium temperature will not be possible!

Instead, we will be able to use the model to calculate the terms in the energy budget (ASR and OLR).

### Python exercise

- Write **two** Python functions to calculate ASR and OLR for *arbitrary parameter values*.
- Verify the following:
    - With the new parameter values but the old temperature $T = 288$ K, is ASR greater or lesser than OLR? (Use formatted strings to print your results.)
    - Is the Earth gaining or losing energy?
    - How does your answer change if $T = 295$ K (or any other temperature greater than 291 K)?

```python

```

```python

```

____________

## 5. A time-dependent Energy Balance Model
____________

The above exercise shows us that if some properties of the climate system change in such a way that the **equilibrium temperature goes up**, then the Earth system *receives more energy from the sun than it is losing to space*. The system is **no longer in energy balance**.

The temperature must then increase to get back into balance. The increase will not happen all at once! It will take time for energy to accumulate in the climate system. We want to model this **time-dependent adjustment** of the system.

In fact almost all climate models are **time-dependent**, meaning the model calculates **time derivatives** (rates of change) of climate variables.

### An energy balance **equation**

We will write the **total energy budget** of the Earth system as

\begin{align}
\frac{dE}{dt} &= \text{net energy flux in to system} \\
 &= \text{flux in – flux out} \\
 &= \text{ASR} - \text{OLR}
\end{align}

where $E$ is the **enthalpy** or **heat content** of the total system.

We will express the budget per unit surface area, so each term above has units W m$^{-2}$

Note: any **internal exchanges** of energy between different reservoirs (e.g. between ocean, land, ice, atmosphere) do not appear in this budget – because $E$ is the **sum of all reservoirs**.

Also note: **This is a generically true statement.** We have just defined some terms, and made the (very good) assumption that the only significant energy sources are radiative exchanges with space.

**This equation is the starting point for EVERY CLIMATE MODEL.**

But so far, we don’t actually have a MODEL. We just have a statement of a budget. To use this budget to make a model, we need to relate terms in the budget to state variables of the atmosphere-ocean system.

For now, the state variable we are most interested in is **temperature** – because it is directly connected to the physics of each term above.

### An energy balance **model**

If we now suppose that

$$ E = C T_s $$

where $T_s$ is the global mean surface temperature, and $C$ is a constant – the **effective heat capacity** of the atmosphere-ocean column.

Then our budget equation becomes:

$$ C \frac{dT_s}{dt} = \text{ASR} - \text{OLR} $$

where

- $C$ is the **heat capacity** of Earth system, in units of J m$^{-2}$ K$^{-1}$.
- $\frac{dT_s}{dt}$ is the rate of change of global average surface temperature.

By adopting this equation, we are assuming that the energy content of the Earth system (atmosphere, ocean, ice, etc.) is *proportional to surface temperature*.

Important things to think about:

- Why is this a sensible assumption?
- What determines the heat capacity $C$?
- What are some limitations of this assumption?

For our purposes here we are going to use a value of $C$ equivalent to heating 100 meters of water:

$$C = c_w \rho_w H$$

where

$c_w = 4 \times 10^3$ J kg$^{-1}$ $^\circ$C$^{-1}$ is the specific heat of water,

$\rho_w = 10^3$ kg m$^{-3}$ is the density of water, and

$H$ is an effective depth of water that is heated or cooled.

```python
c_w = 4E3  #  Specific heat of water in J/kg/K
rho_w = 1E3  #  Density of water in kg/m3
H = 100.   #  Depth of water in m
C = c_w * rho_w * H   #  Heat capacity of the model
print('The effective heat capacity is {:.1e} J/m2/K'.format(C))
```

    The effective heat capacity is 4.0e+08 J/m2/K

### Solving the energy balance model

This is a first-order ordinary differential equation (ODE) for $T_s$ as a function of time. It is also **our very first climate model!**

To solve it (i.e. see how $T_s$ evolves from some specified initial condition) we have two choices:

1. Solve it analytically
2. Solve it numerically

Option 1 (analytical) will usually not be possible because the equations will typically be too complex and non-linear. This is why computers are our best friends in the world of climate modeling.

HOWEVER it is often useful and instructive to simplify a model down to something that is analytically solvable when possible. Why? Two reasons:

1. Analysis will often yield a deeper understanding of the behavior of the system
2. It gives us a benchmark against which to test the results of our numerical solutions.

____________

## 6. Representing time derivatives on a computer
____________

Recall that the derivative is the **instantaneous rate of change**. It is defined as

$$ \frac{dT}{dt} = \lim_{\Delta t\rightarrow 0} \frac{\Delta T}{\Delta t}$$

- **On the computer there is no such thing as an instantaneous change.**
- We are always dealing with *discrete quantities*.
- So we approximate the derivative with $\Delta T/ \Delta t$.
- So long as we take the time interval $\Delta t$ "small enough", the approximation is valid and useful.
- (The meaning of "small enough" varies widely in practice. Let's not talk about it now)

So we write our model as

$$ C  \frac{\Delta T}{\Delta t} \approx \text{ASR} - \text{OLR}$$

where $\Delta T$ is the **change in temperature predicted by our model** over a short time interval $\Delta t$.

We can now use this to **make a prediction**:

Given a current temperature $T_1$ at time $t_1$, what is the temperature $T_2$ at a future time $t_2$?

We can write

$$ \Delta T = T_2-T_1 $$
$$ \Delta t = t_2-t_1 $$

and so our model says

$$ C  \frac{T_2-T_1}{\Delta t} = \text{ASR} - \text{OLR} $$

Which we can rearrange to **solve for the future temperature**:

$$ T_2 = T_1 + \frac{\Delta t}{C} \left( \text{ASR} - \text{OLR}(T_1) \right)  $$

We now have a formula with which to make our prediction!

Notice that we have written the OLR as a *function of temperature*. We will use the current temperature $T_1$ to compute the OLR, and use that OLR to determine the future temperature.

____________

## 7. Numerical solution of the Energy Balance Model
____________

The quantity $\Delta t$ is called a **timestep**. It is the smallest time interval represented in our model.

Here we're going to use a timestep of 1 year:

```python
dt = 60. * 60. * 24. * 365.   # one year expressed in seconds
```

```python
# Try a single timestep, assuming we have working functions for ASR and OLR
T1 = 288.
T2 = T1 + dt / C * ( ASR(alpha=0.32) - OLR(T1, tau=0.57) )
# above I am passing arguments using keywords so that I don't have to remember the order
print(T2)
```

    288.7678026614462

What happened? Why?

Try another timestep

```python
T1 = T2
T2 = T1 + dt / C * ( ASR(alpha=0.32) - OLR(T1, tau=0.57) )
print(T2)
```

    289.3479210238739

Warmed up again, but by a smaller amount.

But this is tedious typing. Time to **define a function** to make things easier and more reliable:

```python
def step_forward(T):
    return T + dt / C * ( ASR(alpha=0.32) - OLR(T, tau=0.57) )
```

Try it out with an arbitrary temperature:

```python
step_forward(300.)
```

    297.658459884

Notice that our function calls other functions and variables we have already defined.

***
#### Python tip

Functions can access variables and other functions defined outside of the function.

This is both very useful and occasionally confusing.
***

Now let's really harness the power of the computer by **making a loop** (and storing values in arrays).

### Python "For" Loops

Definite iteration loops are frequently referred to as **for** loops because for is the keyword that is used to introduce them in nearly all programming languages, including Python.

Definite iteration means that the number of repetitions is specified in advance. Later in the course we will introduce indefinite iteration, in which the code block executes until some condition is met.

```python
# Iterate through a list of numbers and execute statements
for n in [0, 1, 2, 3, 4]:
    print(n)

# Note the loop variable n takes on the value of the next element
# in the collection each time through the loop.
```

    0
    1
    2
    3
    4

```python
# Same thing, but use the built-in range() function
# range(<end>) returns an iterable that yields integers
# starting with 0, up to but not including <end>:
for n in range(5):
    print(n)
```

    0
    1
    2
    3
    4

### Numpy arrays

[NumPy](https://numpy.org/) is the fundamental package for scientific computing with Python.

The fundamental data structure of NumPy is an **n-dimensional array**. N-dimensional means it can be 1-dimensional, 2-dimensional, 3-dimensional, and so on.

As your programming skills grow, you will want to avoid for loops and intead use array programming to speed up operation runtime. Let's not worry about that now.

To access numpy, we must first import it.

```python
import numpy as np
```

The `linspace` function creates an array object that is an array of numbers evenly spaced between the start and end points.

```python
np.linspace(230,300,10)
```

    array([230.        , 237.77777778, 245.55555556, 253.33333333,
           261.11111111, 268.88888889, 276.66666667, 284.44444444,
           292.22222222, 300.        ])

An array can be a function argument.

```python
OLR(np.linspace(230,300,10), tau=0.57)
```

    array([ 90.44181279, 103.31014479, 117.50516903, 133.11508224,
           150.23091968, 168.9465551 , 189.35870079, 211.56690754,
           235.67356467, 261.7839    ])

The `zeros` function creates an array object that is an array of zeros of the specified size.

```python
np.zeros(10)
```

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

### Implementing For-loop and Array assignment in the Energy Balance Model

```python
numsteps = 20
Tsteps = np.zeros(numsteps+1)
Years = np.zeros(numsteps+1)
Tsteps[0] = 288.
for n in range(numsteps):
    Years[n+1] = n+1
    #Here we are calling a function inside a loop
    Tsteps[n+1] = step_forward( Tsteps[n] )
print(Tsteps)
```

    [288.         288.76780266 289.34792102 289.78523685 290.11433323
     290.36166675 290.54736768 290.68669049 290.79115953 290.86946109
     290.92813114 290.97208122 291.00499865 291.02964965 291.0481083
     291.06192909 291.07227674 291.08002371 291.08582346 291.09016532
     291.09341571]

What did we just do?

- Created an array of zeros
- Set the initial temperature to 288 K
- Repeated our time step 20 times
- Stored the results of each time step into the array

***
#### Python tip

Use square bracket [ ] to refer to elements of an array or list. Use round parentheses ( ) for function arguments.
***

### Plotting the result

Now let's draw a picture of our result!

```python
# a special instruction for the Jupyter notebook
# Display all plots inline in the notebook
%matplotlib inline
#  import the plotting package
import matplotlib.pyplot as plt
```

```python
plt.plot(Years, Tsteps)
plt.xlabel('Years')
plt.ylabel('Global mean temperature (K)');
```

Note how the temperature *adjusts smoothly toward the equilibrium temperature*, that is, the temperature at which
ASR = OLR.

**If the planetary energy budget is out of balance, the temperature must change so that the OLR gets closer to the ASR!**

The adjustment is actually an *exponential decay* process: The rate of adjustment slows as the temperature approaches equilibrium.

The temperature gets very very close to equilibrium but never reaches it exactly.

***
#### Python tip

We can easily make simple graphs with the function `plt.plot(x,y)`, where `x` and `y` are arrays of the same size. But we must import it first.

This is actually not native Python, but uses a graphics library called [matplotlib](https://matplotlib.org). This is the workhorse of scientific plotting in Python, and we will be using it all the time!

Just about all of our notebooks will start with this:
```
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
```
***

____________
## 8. Summary
____________

- We looked at the flows of energy in and out of the Earth system.
- These are determined by radiation at the top of the Earth's atmosphere.
- Any imbalance between shortwave absorption (ASR) and longwave emission (OLR) drives a change in temperature
- Using this idea, we built a climate model!
- This **Zero-Dimensional Energy Balance Model** solves for the global, annual mean surface temperature $T_s$
- Two key assumptions:
    - Energy content of the Earth system varies proportionally to $T_s$
    - The OLR increases as $\tau \sigma T_s^4$ (our simple greenhouse model)
- Earth (or any planet) has a well-defined **equilibrium temperature** $T_{eq}$ at which ASR = OLR, because of the *temperature dependence of the outgoing longwave radiation*.

- If $T_s < T_{eq}$, the model will warm up.
- We can represent the continous warming process on the computer using discrete timesteps.
- We can plot the result.

____________

## Credits

This notebook is part of [The Climate Laboratory](https://brian-rose.github.io/ClimateLaboratoryBook), an open-source textbook developed and maintained by [Brian E. J. Rose](http://www.atmos.albany.edu/facstaff/brose/index.html), University at Albany. It has been modified by [Nicole Feldl](http://nicolefeldl.com), UC Santa Cruz.

It is licensed for free and open consumption under the
[Creative Commons Attribution 4.0 International (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/) license.

Development of these notes and the [climlab software](https://github.com/brian-rose/climlab) is partially supported by the National Science Foundation under award AGS-1455071 to Brian Rose. Any opinions, findings, conclusions or recommendations expressed here are mine and do not necessarily reflect the views of the National Science Foundation.
____________

```python

```


```python
import sys
sys.path.append('/home/luca/GitRepositories/Brancher')

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

In this tutorial we are going to discuss stochastic processes and their syntax in Brancher. Stochastic processes are a powerful tool for performing inference in models with a potentially infinite set of random variables. For example, we can train a timeseries model on a finite set of past timepoints and use it for predicting arbitrarily far into the future.

## The ABC of stochastic processes
What is a stochastic process? Loosely speaking, a stochastic process $X$ is a collection of multivariate random variables that can be "patched together" consistently. Let's make this handwavy statement more clear with a simple example. Consider the following set of three probability distributions:

$$ \{ p_{12}(x_1, x_2), ~p_1(x_1), ~p_2(x_2) \}$$

This set is a stochastic process if the following marginalization conditions are met:

$$ \int p_{12}(x_1, x_2) dx_2 = p_1(x_1)~~~~~ \& ~~~~~\int p_{12}(x_1, x_2) dx_1 = p_2(x_2)~.$$

For a more concrete example, consider the following three Gaussian variables:

\begin{equation}
X_{12} \sim \mathcal{N}\!\left(
    \begin{bmatrix}
    0 \\
    0
    \end{bmatrix}
    ,
    \begin{bmatrix}
    \sigma_1^2, \sigma_{12} \\
    \sigma_{12}, \sigma_2^2
    \end{bmatrix}
    \right),
\end{equation}

$$ X_{1} \sim \mathcal{N}\!\left(0, a \right), $$
$$ X_{2} \sim \mathcal{N}\!\left(0, b \right)~. $$

These three variables can be "patched together" into a stochastic process if

* a = $\sigma_1^2~,$
* b = $\sigma_2^2~.$

## Stochastic processes as generators of random variables

The examples given in the previous section are easy to undertand but all in all a bit silly as the bivariate distribution $p_{12}(x_1, x_2)$ contains full information about the process and the marginals such as $p_1(x_1)$ are redundant.

However, the real usefulness of the concept of Stochastic processes comes when we are working with an infinite set of random variables where none contain full information about the process. For example, consider a discrete Markov process defined by the following recursion:

\begin{align}
& x_0 \sim \mathcal{N}(0, \sigma_0^2) \\
& x_{t+1} \sim \mathcal{N}(x_t, \sigma^2)~.
\end{align}

Given a range of time points 1:T, the multivariate distribution can be obtained using the chain rule of probability theory:

$$ p(x_0,...,x_T) = p(x_0) \prod_{t = 0}^{T-1} p(x_{t+1}|x_{t}) = \mathcal{N}(0, \sigma_0^2)\prod_{t = 0}^{T-1} \mathcal{N}(x_t, \sigma^2)~.$$
However, none of these multivariate distributions contain all the infinitely many variables of the process and consequently the process is not fully captured by any of its marginal distributions over a finite number of time points.

This suggests an intuitive way of looking at stochastic processes. A stochastic process X can be seen as a function that takes a set of query points $\{t_1, t_2, ..., t_T\}$ as input and outputs the corresponding multivariate variable:

$$ X(\{t_1, t_2, ..., t_T\}) = x_{t_1,...,t_T} \sim p(x_{t_1},...,x_{t_T}) $$

## Stochastic processes in Brancher

Brancher formalizes stochastic processes using the point of view we just outlined. A Brancher StochasticProcess is an object that takes as input query points and outputs either a RandomVariable or a ProbabilisticModel representing the corresponding marginal distribution.

As an example, let's start by constructing the Markov process that we introduced in the previous section. In Brancher, this can be done in two lines of code:

```python
from brancher.stochastic_processes import MarkovProcess
from brancher.standard_variables import NormalVariable as Normal

## Create time series model ##
x0 = Normal(0, 0.1, "x_0")
X = MarkovProcess(x0, lambda t, x: Normal(x, 0.25, "x_{}".format(t)))
```

In this expression, x0 is the initial distribution and the **lambda** expression defines the conditional distribution. We can now call the stochastic process X with a range of contiguous integers as input in order to get a probabilisticModel:

```python
T = 100
x_T = X(range(T)) # This is our probabilistic model modeling the first T timepoints of the process
```

Pretty simple, isn't it? In this case, x_T is an instance of the class TimeSeriesModel, which is a subclass of ProbabilisticModel. Timeseries models have a series of dedicated methods that make our life easier when working with timeseries. For example, we can use the .get_timeseries_sample method for getting samples already formatted as Pandas timeseries and use the standard Pandas plot method for plotting them easily:

```python
x_T.get_timeseries_sample(10).plot(legend=False)
```

Using a similar syntax, we can also define generalized Markov processes where the conditional distribution is a function of K previous time points. For example, let's define a stochastic oscillatory process by discretizing the following stochastic differential equation:

$$ \frac{d^2z}{dt^2} = -\omega^2 z + w(t) $$

where $2 \pi \omega$ is the frequency of the oscillation and $w(t)$ is a white noise input.

```python
## Create time series model ##
T = 100
dt = 0.01
omega = 2*np.pi*5
z0 = Normal(0, 0.1, "x_0")
z1 = Normal(0, 0.1, "x_1")
Z = MarkovProcess((z0, z1),
                  lambda t, z0, z1: Normal((2*z1 - z0) - dt**2*omega**2*z1, 0.25*np.sqrt(dt), "z_{}".format(t)))
z_T = Z(range(T))
z_T.get_timeseries_sample(5).plot(legend=False)
```

Note that in this case we needed to define two initial variables.

Finally, we can insert random parameters in the model very easily. For example, let's create another oscillatory process but with random frequency:

```python
from brancher.standard_variables import LogNormalVariable as LogNormal

f = LogNormal(2,1,"f")
w0 = Normal(0, 0.1, "x_0")
w1 = Normal(0, 0.1, "x_1")
W = MarkovProcess((w0, w1),
                  lambda t, w0, w1: Normal((2*w1 - w0) - dt**2*(2*np.pi*f)**2*w1, 0.25*np.sqrt(dt), "w_{}".format(t)))
w_T = W(range(T))
w_T.get_timeseries_sample(5).plot(legend=False)
```

As you can see, the sampled timeseries now have different frequencies as f is sampled independently from a lognormal distribution.

## Inference with stochastic processes

We can now perform variational Bayesian inference on the parameters of the stochastic process. We can show this in one of the simplest Markov processes: a first order autoregressive process AR(1):

$$ x_{t+1} \sim \mathcal{N}\!(b ~ x_t, \sigma^2) $$

Let's start by constructing the model in Brancher. We are going to use a beta prior for the autoregressive parameter $b$ in order to guarantee the stability of the process (values bigger than 1 leads to explosive behavior).

```python
from brancher.standard_variables import BetaVariable as Beta

## Create time series model ##
b = Beta(1,1, "b")
x0 = Normal(0, 0.1, "x_0")
X = MarkovProcess(x0, lambda t, x: Normal(b*x, 0.25, "x_{}".format(t)))
```

We can now fix the groundtruth autoregressive parameter to 0.9 and generate some data. Note that we do not need to instantiate the random variable like we did before since we can directly use the .get_timeseries_sample method of the stochastic process (this is a method of the DiscreteTimeSeriesProcess subclass):

```python
## Sample ##
num_timepoints = 60
temporal_sample = X.get_timeseries_sample(1, query_points=num_timepoints, input_values={b: 0.8})
temporal_sample.plot()
plt.show()
```

We can now input these observations in the stochastic process. This can be done using the .observe method. Note that, in the case of stochastic processes, this method needs as input both the data and a list of query points. The method constructs an active submodel including all the observed variables (x_0,x_1,...,x_T) and their parents (b in this case).

```python
## Observe the stochastic prosess
query_points = range(num_timepoints)
X.observe(temporal_sample, query_points)
```

We can now define a simple variational distribution for the autoregressive parameter. The simplest choice is to use another beta distribution:

```python
from brancher.variables import ProbabilisticModel

# Variational model
Qb = Beta(1,1, "b", learnable=True)
variational_model = ProbabilisticModel([Qb])
```

We can finally perform the inference. We do not need to instantiate a finite ProbabilisticModel as the perform_inference function can take a StochasticProcess as input model.

```python
from brancher.inference import perform_inference
from brancher.inference import ReverseKL

## Perform ML inference ##
perform_inference(X,
                  posterior_model=variational_model,
                  inference_method=ReverseKL(),
                  number_iterations=1000,
                  number_samples=50,
                  optimizer="Adam",
                  lr=0.01)9
loss_list = X.active_submodel.diagnostics["loss curve"]
plt.plot(loss_list)
plt.show()
```

Finally, we can plot the posterior distribution over the autoregressive coefficient.

```python
from brancher.visualizations import plot_posterior

#Plot posterior
plot_posterior(X.active_submodel, variables=["b"])
plt.show()
```

Let's now predict the future given our observations. We simply need to call the .get_posterior_timeseries_sample method, which will sample the autoregressive coefficient from the posterior and then the future timeseries given the coefficient and the observed timepoints.

```python
num_future_timepoints = 200
X.get_posterior_timeseries_sample(10, query_points=num_future_timepoints).plot(legend=False)
```

### Example 2: Stochastic oscillation

```python
f = Normal(5,1,"f")
w0 = Normal(0, 0.1, "w_0")
w1 = Normal(0, 0.1, "w_1")
W = MarkovProcess((w0, w1),
                  lambda t, w0, w1: Normal((2*w1 - w0) - dt**2*(2*np.pi*f)**2*w1, 0.25*np.sqrt(dt), "w_{}".format(t)))

# Generate data
num_timepoints = 60
temporal_sample = W.get_timeseries_sample(1, query_points=num_timepoints, input_values={f: 8.})

# Observe
query_points = range(num_timepoints)
W.observe(temporal_sample, query_points)

# Variational model
Qf = Normal(5,1,"f", learnable=True)
variational_model = ProbabilisticModel([Qf])

## Perform ML inference ##
perform_inference(W,
                  posterior_model=variational_model,
                  inference_method=ReverseKL(),
                  number_iterations=1000,
                  number_samples=50,
                  optimizer="Adam",
                  lr=0.01)
loss_list = W.active_submodel.diagnostics["loss curve"]

#Plot posterior
plot_posterior(W.active_submodel, variables=["f"])
plt.show()

#num_future_timepoints = 200
#W.get_posterior_timeseries_sample(10, query_points=num_future_timepoints).plot()
```

```python
num_future_timepoints = 200
W.get_posterior_timeseries_sample(10, query_points=num_future_timepoints).plot(legend=False)
```

## Inference with missing datapoints

It is interesting to consider the case in which there are gaps in our observations. In this case, the non-observed time points are additional latent variables that we need to learn by fitting a variational distribution.

let's start by defining a stochastic process. In this example, we will use a discrete random walk:

$$ x_{t+1} \sim \mathcal{N}\!(x_t, \sigma^2) $$

```python
## Create time series model ##
x0 = Normal(0, 2, "x_0")
X = MarkovProcess(x0, lambda t, x: Normal(x, 0.25, "x_{}".format(t)))

## Sample ##
num_timepoints = 40
temporal_sample = X.get_timeseries_sample(1, query_points=num_timepoints)
temporal_sample.plot()
plt.show()
```

We can now observe only the time points between 0 and 10 and between 30 and 40:

```python
## Observe model ##
data = temporal_sample[:10].append(temporal_sample[30:])
query_points = list(range(0, 10)) + list(range(30, 40))
X.observe(data, query_points)
```

We now need to define a variational model for the latent variables. In this case, the latent variables are the values of the random walk in the time points between 10 and 30.

The construction of this variational model is not as trivial as before as the variational posterior itself should be a timeseries model conditioned on the last observed value. We will use the following Brancher model:

```python
# Variational process
Qx10 = Normal(float(temporal_sample[9:10].values), 0.25, "x_10")
QX = [Qx10]
for idx in range(11, 30):
    QX.append(Normal(QX[idx-11], 0.25, "x_{}".format(idx), has_bias=True, learnable=True))
QX = ProbabilisticModel(QX)
```

which correspond to the following distribution:

$$ x_{t+1}^{(q)} \sim \mathcal{N}\!\left(\mu_t^{(q)}, \sigma_t^{(q)}  \right) $$

$$\mu_t^{(q)} =  \lambda_t x_{t}^{(q)}  + (1 - \lambda_t) m_t^{(q)}$$

where $\lambda_t^{(q)}$ and $m_t^{(q)}$ are learnable variational parameters. These parameters are automatically constructed when the flag has_bias is true. This allows to easily define a variational model that behaves like a random walk while being able to be directed towards the value of the observable data. In particular, the variable $\mu_t^{(q)}$ biases the variational process towards its value at time $t$ while $\lambda_t^{(q)}$ regulates the relative contribution of random walk and bias at time $t$.

We can now perform the inference:

```python
## Perform ML inference ##
perform_inference(X,
                  posterior_model=QX,
                  inference_method=ReverseKL(),
                  number_iterations=3000,
                  number_samples=50,
                  optimizer="Adam",
                  lr=0.005)
loss_list = X.active_submodel.diagnostics["loss curve"]
plt.plot(loss_list)
plt.show()
```

Done! We can finally sample the posterior distribution of the process given the observation!

```python
## Sample ##
post_temporal_sample = X.get_posterior_timeseries_sample(200, query_points=100)
post_temporal_sample.plot(alpha=0.25, legend=False)
plt.show()
```

## Deep learning and stochastic processes

Brancher allows to combine all the methods described in this tutorial with deep learning methods and functions. If you need some guidance for using brancher with deep learning models, you can check out our deep learning tutorial: https://colab.research.google.com/drive/1YNwZpJgrsicK3Pz8igAktbdocm-gA9mV .

As an example of a stochastic process that uses deep learning components, let's define a residual convolutional process with ReLu nonlinearities.

In this case, the initial variable is an MNIST image and the expected value of the conditional distribution is obtained by applying a residual transformation with a random convolutional kernel:

```python
import torchvision
from brancher.standard_variables import DeterministicVariable as Deterministic
from brancher import functions as BF

# MNIST images
image_size = 28
train_set = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=None)
input_image = np.reshape(train_set.train_data.numpy()[0,:], newshape=(1, 1, image_size, image_size))

# Convolutional kernel
n_channels = 1
weights = Normal(loc=np.zeros((n_channels, n_channels, 3, 3)),
                 scale=np.ones((n_channels, n_channels, 3, 3)),
                 name="weights") #Gaussian convolutional kernels

# Residual convolutional stochastic process
h0 = Deterministic(input_image, "h_0", is_observed=True)
H = MarkovProcess(h0,
                  lambda t, h: Normal(h + BF.relu(BF.conv2d(h, weights, padding=1)), 0.1, "h_{}".format(t)))
```

We can now sample the first 10 transformations from this residual convolutional stochastic process:

```python
sampled_images = H.get_timeseries_sample(1, range(5))
```

```python
for t in range(5):
    plt.imshow(sampled_images[0].values[t].reshape((28,28)))
    plt.title("Time: {}".format(t))
    plt.show()
```

```python

```

examples/notebooks/Stochastic processes and timeseries analysis.ipynb


#EPIDEMIC MODEL

```python
import sympy as sm
import matplotlib as plt
import numpy as np
import scipy.linalg as la
from sympy.interactive import printing
printing.init_printing(use_latex=True)
```

```python

#Variable definition
S,E,I,A,R= sm.symbols('S,E,I,A,R')
beta,q,mu,eta,p,gamma,alpha,N= sm.symbols('beta,q,mu,eta,p,gamma,alpha,N')

#the system of equations
Sdot = N-beta*S*(I+q*A)-mu*S
display(Sdot)
Edot=beta*S*(I+q*A)-(mu+eta)*E
display(Edot)
Idot = p*eta*E-(mu+gamma)*I
display(Idot)
Adot=(1-p)*eta*E-(mu+alpha)*A
display(Adot)
Rdot=gamma*I+alpha*A-mu*R
display(Rdot)
```

```python
#Equilibrium points
SEq = sm.Eq(Sdot,0)
EEq = sm.Eq(Edot,0)
IEq = sm.Eq(Idot,0)
AEq = sm.Eq(Adot,0)
REq = sm.Eq(Rdot,0)
#hEq = sm.Eq(h,0)
# compute fixed points
equilibriam_points = sm.solve( (SEq,EEq,IEq,AEq,REq), S,E,I,A,R )
display(equilibriam_points)
```

```python
F_i=sm.Matrix([beta*S*(I+q*A),0,0])
F_i
```

```python
V_i=sm.Matrix([(mu+eta)*E,-Idot,-Adot])
V_i
```

```python
Mat=sm.Matrix([E,I,A])
Mat
```

```python
F=F_i.jacobian(Mat)
F
```

```python
V=V_i.jacobian(Mat)
V
```

```python
V_inv=V.inv()
V_inv
```

```python
G=F*V_inv
G
```

```python
EV=G.eigenvals()
EV
```

```python
p=equilibriam_points[0][0]
q=equilibriam_points[0][1]
r=equilibriam_points[0][2]
Jacsub1=G.subs([(S,p),(E,q),(I,r)])
Jacsub1
```

```python
p
```

```python
EV1=Jacsub1.eigenvals()
EV1
```

```python

```

```python

```

```python

```


# Confidence and prediction intervals

> Marcos Duarte
> Laboratory of Biomechanics and Motor Control ([http://demotu.org/](http://demotu.org/))
> Federal University of ABC, Brazil

For a finite univariate random variable with a normal probability distribution, the mean $\mu$ (a measure of central tendency) and variance $\sigma^2$ (a measure of dispersion) of a population are the well known formulas:

\begin{equation}
\mu = \frac{1}{N}\sum_{i=1}^{N} x_i
\label{mu}
\end{equation}

\begin{equation}
\sigma^2 = \frac{1}{N}\sum_{i=1}^{N} (x_i - \mu)^2
\label{sigma2}
\end{equation}

For a more general case, a continuous univariate random variable $x$ with [probability density function](http://en.wikipedia.org/wiki/Probability_density_function) (PDF), $f(x)$, the mean and variance of a population are:

\begin{equation}
\mu = \int_{\infty}^{\infty} x f(x)\: dx
\label{mub}
\end{equation}

\begin{equation}
\sigma^2 = \int_{\infty}^{\infty} (x-\mu)^2 f(x)\: dx
\label{sigma2b}
\end{equation}

The PDF is a function that describes the relative likelihood for the random variable to take on a given value.
Mean and variance are the first and second central moments of a random variable. The standard deviation $\sigma$ of the population is the square root of the variance.

The [normal (or Gaussian) distribution](http://en.wikipedia.org/wiki/Normal_distribution) is a very common and useful distribution, also because of the [central limit theorem](http://en.wikipedia.org/wiki/Central_limit_theorem), which states that for a sufficiently large number of samples (each with many observations) of an independent random variable with an arbitrary probability distribution, the means of the samples will have a normal distribution. That is, even if the underlying probability distribution of a random variable is not normal, if we sample enough this variable, the means of the set of samples will have a normal distribution.

The probability density function of a univariate normal (or Gaussian) distribution is:

\begin{equation}
f(x) = \frac{1}{\sqrt{2\pi\sigma^2}} \exp\Bigl(-\frac{(x-\mu)^2}{2\sigma^2}\Bigr)
\label{pdf}
\end{equation}

The only parameters that define the normal distribution are the mean $\mu$ and the variance $\sigma^2$, because of that a normal distribution is usually described as $N(\mu,\:\sigma^2)$.

Here is a plot of the PDF for the normal distribution:

```python
# import the necessary libraries
import numpy as np
%matplotlib notebook
import matplotlib.pyplot as plt
from IPython.display import display, Latex
from scipy import stats
import sys
sys.path.insert(1, r'./../functions')  # directory of BMC Python functions
```

```python
from pdf_norm_plot import pdf_norm_plot
pdf_norm_plot()
plt.show()
```

    <IPython.core.display.Javascript object>

The horizontal axis above is shown in terms of the number of standard deviations in relation to the mean, which is known as standard score or $z$ score:

\begin{equation}
z = \frac{x - \mu}{\sigma}
\label{z}
\end{equation}

So, instead of specifying raw values in the distribution, we define the PDF in terms of $z$ scores; this conversion process is called standardizing the distribution (and the result is known as standard normal distribution). Note that because $\mu$ and $\sigma$ are known  parameters, $z$ has the same distribution as $x$, in this case, the normal distribution.

The percentage numbers in the plot are the probability (area under the curve) for each interval shown in the horizontal label.
An interval in terms of z score is specified as: $[\mu-z\sigma,\;\mu+z\sigma]$.
The interval $[\mu-1\sigma,\;\mu+1\sigma]$ contains 68.3% of the population and the interval $[\mu-2\sigma,\;\mu+2\sigma]$ contains 95.4% of the population.
These numbers can be calculated using the function `stats.norm.cdf()`, the [cumulative distribution function](http://en.wikipedia.org/wiki/Cumulative_distribution_function) (CDF) of the normal distribution at a given value:

```python
print('Cumulative distribution function (cdf) of the normal distribution:')
for i in range(-3, 4):
    display(Latex(r'%d $\sigma:\;$ %.2f' %(i, stats.norm.cdf(i, loc=0, scale=1)*100) + ' %'))
```

    Cumulative distribution function (cdf) of the normal distribution:

-3 $\sigma:\;$ 0.13 %

-2 $\sigma:\;$ 2.28 %

-1 $\sigma:\;$ 15.87 %

0 $\sigma:\;$ 50.00 %

1 $\sigma:\;$ 84.13 %

2 $\sigma:\;$ 97.72 %

3 $\sigma:\;$ 99.87 %

The parameters `loc` and `scale` are optionals and represent mean and variance of the distribution. The default is `loc=0` and `scale=1`.
A commonly used proportion is 95%. The value that results is this proportion can be found using the function `stats.norm.ppf()`. If we want to find the $\pm$value for the interval that will result in 95% of the population inside, we have to consider that 2.5% of the population will stay out of the interval in each tail of the distribution. Because of that, the number we have to use with the `stats.norm.ppf()` is 0.975:

```python
print('Percent point function (inverse of CDF) of the normal distribution:')
display(Latex(r'ppf(.975) = %.2f' % stats.norm.ppf(.975, loc=0, scale=1)))
```

    Percent point function (inverse of CDF) of the normal distribution:

ppf(.975) = 1.96

Or we can use the function `stats.norm.interval` which already gives the interval:

```python
print('Confidence interval around the mean:')
stats.norm.interval(alpha=0.95, loc=0, scale=1)
```

    Confidence interval around the mean:

    (-1.959963984540054, 1.959963984540054)

So, the interval $[\mu-1.96\sigma,\;\mu+1.96\sigma]$ contains 95% of the population.

Now that we know how the probability density function of a normal distribution looks like, let's demonstrate the central limit theorem for a uniform distribution. For that, we will generate samples of a uniform distribution, calculate the mean across samples, and plot the histogram of the mean  across samples:

```python
rc={'axes.labelsize': 12, 'font.size': 12, 'legend.fontsize': 12, 'axes.titlesize': 12}
plt.rcParams.update(**rc)
fig, ax = plt.subplots(1, 4, sharey=True, squeeze=True, figsize=(9, 3))
x = np.linspace(0, 1, 100)
for i, n in enumerate([1, 2, 3, 10]):
    f = np.mean(np.random.random((1000, n)), 1)
    m, s = np.mean(f), np.std(f, ddof=1)
    fn = (1/(s*np.sqrt(2*np.pi)))*np.exp(-(x-m)**2/(2*s**2))  # normal pdf
    ax[i].hist(f, 20, density=True, color=[0, 0.2, .8, .6], edgecolor='black')
    ax[i].set_title('n=%d' %n, fontsize=12)
    ax[i].plot(x, fn, color=[1, 0, 0, .6], linewidth=2)
    ax[i].xaxis.label.set_size(12)
plt.suptitle('Demonstration of the central limit theorem for a uniform distribution', fontsize=12, y=1.0)
plt.tight_layout()
plt.show()
```

    <IPython.core.display.Javascript object>

**Statistics for a sample of the population**

Parameters (such as mean and variance) are characteristics of a population; statistics are the equivalent for a sample. For a population and a sample with normal or Gaussian distribution, mean and variance is everything we need to completely characterize this population or sample.

The difference between sample and population is fundamental for the understanding of probability and statistics.
In Statistics, a sample is a set of data collected from a population. A population is usually very large and can't be accessed completely; all we have access is a sample (a smaller set) of the population.

If we have only a sample of a finite univariate random variable with a normal distribution, both mean and variance of the population are unknown and they have to be estimated from the sample:

\begin{equation}
\bar{x} = \frac{1}{N}\sum_{i=1}^{N} x_i
\label{mean}
\end{equation}

\begin{equation}
s^2 = \frac{1}{N-1}\sum_{i=1}^{N} (x_i - \bar{x})^2
\label{s2}
\end{equation}

The sample $\bar{x}$ and $s^2$ are only estimations of the unknown true mean and variance of the population, but because of the [law of large numbers](http://en.wikipedia.org/wiki/Law_of_large_numbers), as the size of the sample increases, the sample mean and variance have an increased probability of being close to the population mean and variance.

**Prediction interval around the mean**

For a sample of a univariate random variable, the area in an interval of the probability density function can't be interpreted anymore as the proportion of the sample lying inside the interval. Rather, that area in the interval is a prediction of the probability that a new value from the population added to the sample will be inside the interval. This is called a [prediction interval](http://en.wikipedia.org/wiki/Prediction_interval). However, there is one more thing to correct. We have to adjust the interval limits for the fact that now we have only a sample of the population and the parameters $\mu$ and $\sigma$ are unknown and have to be estimated. This correction will increase the interval for the same probability value of the interval because we are not so certain about the distribution of the population.
To calculate the interval given a desired probability, we have to determine the distribution of the z-score equivalent for the case of a sample with unknown mean and variance:

\begin{equation}
\frac{x_{n+i}-\bar{x}}{s\sqrt{1+1/n}}
\label{zb}
\end{equation}

Where $x_{n+i}$ is the new observation for which we want to calculate the prediction interval.
The distribution of the ratio above is called Student's t-distribution or simply $T$ distribution, with $n-1$ degrees of freedom. A $T$ distribution is symmetric and its pdf tends to that of the
standard normal as $n$ tends to infinity.

Then, the prediction interval around the sample mean for a new observation is:

\begin{equation}
\left[\bar{x} - T_{n-1}\:s\:\sqrt{1+1/n},\quad \bar{x} + T_{n-1}\:s\:\sqrt{1+1/n}\right]
\label{pi}
\end{equation}

Where $T_{n-1}$ is the $100((1+p)/2)^{th}$ percentile of the Student's t-distribution with n−1 degrees of freedom.

For instance, the prediction interval with 95% of probability for a sample ($\bar{x}=0,\;s^2=1$) with size equals to 10 is:

```python
np.asarray(stats.t.interval(alpha=0.95, df=25-1, loc=0, scale=1)) * np.sqrt(1+1/10)
```

    array([-2.16463507,  2.16463507])

For a large sample (e.g., 10000), the interval approaches the one for a normal distribution (according to the [central limit theorem](http://en.wikipedia.org/wiki/Central_limit_theorem)):

```python
np.asarray(stats.t.interval(alpha=0.95, df=10000-1, loc=0, scale=1)) * np.sqrt(1+1/10000)
```

    array([-1.96029927,  1.96029927])

Here is a plot of the pdf for the normal distribution and the pdf for the Student's t-distribution with different number of degrees of freedom (n-1):

```python
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
x = np.linspace(-4, 4, 1000)
f = stats.norm.pdf(x, loc=0, scale=1)
t2 = stats.t.pdf(x, df=2-1)
t10 = stats.t.pdf(x, df=10-1)
t100 = stats.t.pdf(x, df=100-1)
ax.plot(x, f, color='k', linestyle='--', lw=4, label='Normal')
ax.plot(x, t2, color='r', lw=2, label='T (1)')
ax.plot(x, t10, color='g', lw=2, label='T (9)')
ax.plot(x, t100, color='b', lw=2, label='T (99)')
ax.legend(title='Distribution', fontsize=14)
ax.set_title("Normal and Student's t distributions", fontsize=12)
ax.set_xticks(np.linspace(-4, 4, 9))
xtl = [r'%+d$\sigma$' %i for i in range(-4, 5, 1)]
xtl[4] = r'$\mu$'
ax.set_xticklabels(xtl)
ax.set_ylim(-0.01, .41)
plt.grid()
plt.rc('font', size=12)
plt.show()
```

    <IPython.core.display.Javascript object>

It's common to use 1.96 as value for the 95% prediction interval even when dealing with a sample; let's quantify the error of this approximation for different sample sizes:

```python
T = lambda n: stats.t.ppf(0.975, n-1)*np.sqrt(1+1/n)  # T distribution
N = stats.norm.ppf(0.975)                             # Normal distribution
for n in [1000, 100, 10]:
    print('\nApproximation error for n = %d' %n)
    print('Using Normal distribution: %.1f%%' % (100*(N-T(n))/T(n)))
```

    Approximation error for n = 1000
    Using Normal distribution: -0.2%

    Approximation error for n = 100
    Using Normal distribution: -1.7%

    Approximation error for n = 10
    Using Normal distribution: -17.4%

For n=1000, the approximation is good, for n=10 it is bad, and it always underestimates.

**Standard error of the mean**

The [standard error of the mean](http://en.wikipedia.org/wiki/Standard_error) (sem) is the standard deviation of the sample-mean estimate of a population mean and is given by:

\begin{equation}
sem = \frac{s}{\sqrt{n}}
\label{sem}
\end{equation}

**Confidence interval**

In statistics, a [confidence interval](http://en.wikipedia.org/wiki/Confidence_interval) (CI) is a type of interval estimate of a population parameter and is used to indicate the reliability of an estimate ([Wikipedia](http://en.wikipedia.org/wiki/Confidence_interval)). For instance, the 95% confidence interval for the sample-mean estimate of a population mean is:

\begin{equation}
\left[\bar{x} - T_{n-1}\:s/\sqrt{n},\quad \bar{x} + T_{n-1}\:s/\sqrt{n}\right] $$
\label{ci}
\end{equation}

Where $T_{n-1}$ is the $100((1+p)/2)^{th}$ percentile of the Student's t-distribution with n−1 degrees of freedom.
For instance, the confidence interval for the mean with 95% of probability for a sample ($\bar{x}=0,\;s^2=1$) with size equals to 10 is:

```python
stats.t.interval(alpha=0.95, df=10-1, loc=0, scale=1) / np.sqrt(10)
```

    array([-0.71535691,  0.71535691])

The 95% CI means that if we randomly obtain 100 samples of a population and calculate the CI of each sample (i.e., we replicate the experiment 99 times in a independent way), 95% of these CIs should contain the population mean (the true mean). This is different from the prediction interval, which is larger, and gives the probability that a new observation is inside this interval. Note that the confidence interval DOES NOT give the probability that the true mean (the mean of the population) is inside this interval. The true mean is a parameter (fixed) and it is either inside the calculated interval or not; it is not a matter of chance (probability).

Let's simulate samples of a population ~ $N(\mu=0, \sigma^2=1) $ and calculate the confidence interval for the samples' mean:

```python
n = 20                            # number of observations
x = np.random.randn(n, 100)       # 100 samples with n observations
m = np.mean(x, axis=0)            # samples' mean
s = np.std(x, axis=0, ddof=1)     # samples' standard deviation
T = stats.t.ppf(.975, n-1)        # T statistic for 95% and n-1 degrees of freedom
ci = m + np.array([-s*T/np.sqrt(n), s*T/np.sqrt(n)])
out = ci[0, :]*ci[1, :] > 0       # CIs that don't contain the true mean
```

```python
fig, ax = plt.subplots(1, 1, figsize=(9, 5))
ind = np.arange(1, 101)
ax.axhline(y=0, xmin=0, xmax=n+1, color=[0, 0, 0])
ax.plot([ind, ind], ci, color=[0, 0.2, 0.8, 0.8], marker='_', ms=0, linewidth=3)
ax.plot([ind[out], ind[out]], ci[:, out], color=[1, 0, 0, 0.8], marker='_', ms=0, linewidth=3)
ax.plot(ind, m, color=[0, .8, .2, .8], marker='.', ms=10, linestyle='')
ax.set_xlim(0, 101)
ax.set_ylim(-1.1, 1.1)
ax.set_title("Confidence interval for the samples' mean estimate of a population ~ $N(0, 1)$",
             fontsize=14)
ax.set_xlabel('Sample (with %d observations)' %n, fontsize=14)
plt.show()
```

    <IPython.core.display.Javascript object>

Four out of 100 95%-CI's don't contain the population mean, about what we predicted.

And the standard deviation of the samples' mean per definition should be equal to the standard error of the mean:

```python
print("Samples' mean and standard deviation:")
print('m = %.3f   s = %.3f' % (np.mean(m), np.mean(s)))
print("Standard deviation of the samples' mean:")
print('%.3f' % np.std(m, ddof=1))
print("Standard error of the mean:")
print('%.3f' % (np.mean(s)/np.sqrt(20)))
```

    Samples' mean and standard deviation:
    m = 0.010   s = 1.018
    Standard deviation of the samples' mean:
    0.234
    Standard error of the mean:
    0.228

Likewise, it's common to use 1.96 for the 95% confidence interval even when dealing with a sample; let's quantify the error of this approximation for different sample sizes:

```python
T = lambda n: stats.t.ppf(0.975, n-1)  # T distribution
N = stats.norm.ppf(0.975)              # Normal distribution
for n in [1000, 100, 10]:
    print('\nApproximation error for n = %d' %n)
    print('Using Normal distribution: %.1f%%' % (100*(N-T(n))/T(n)))
```

    Approximation error for n = 1000
    Using Normal distribution: -0.1%

    Approximation error for n = 100
    Using Normal distribution: -1.2%

    Approximation error for n = 10
    Using Normal distribution: -13.4%

For n=1000, the approximation is good, for n=10 it is bad, and it always underestimates.

For the case of a multivariate random variable, see [Prediction ellipse and prediction ellipsoid](http://nbviewer.ipython.org/github/demotu/BMC/blob/master/notebooks/PredictionEllipseEllipsoid.ipynb).

### Verification of the relation between standard deviation and standard error of the mean

```python
rc = {'axes.labelsize': 12, 'font.size': 12, 'legend.fontsize': 12, 'axes.titlesize': 12}
plt.rcParams.update(**rc)
fig, ax = plt.subplots(1, 5, sharey=True, squeeze=True, figsize=(9, 3))
print('{0:10} {1:10} {2:5} {3:10}'.format('n', 'm', 's', '1/np.sqrt(n)'))
for i, n in enumerate([1, 2, 3, 10, 100]):
    f = np.mean(np.random.randn(1000, n), 1)
    m, s = np.mean(f), np.std(f, ddof=1)
    x = np.linspace(np.min(f), np.max(f), 100)
    fn = (1/(s*np.sqrt(2*np.pi)))*np.exp(-(x-m)**2/(2*s**2))  # normal pdf
    ax[i].hist(f, 20, density=True, color=[0, 0.2, .8, .6], edgecolor='black')
    ax[i].set_title('n=%d' %n, fontsize=12)
    ax[i].plot(x, fn, color=[1, 0, 0, .6], linewidth=2)
    ax[i].xaxis.label.set_size(12)
    print('{0:3} {1:10.4f} {2:10.4f} {3:10.4f}'.format(n, m, s, 1/np.sqrt(n)))
plt.suptitle('Standard deviation and standard error of the mean', fontsize=12, y=1.0)
plt.tight_layout()
plt.show()
```

    <IPython.core.display.Javascript object>

    n          m          s     1/np.sqrt(n)
      1    -0.0212     0.9835     1.0000
      2    -0.0140     0.7088     0.7071
      3    -0.0251     0.5595     0.5774
     10    -0.0132     0.3289     0.3162
    100     0.0069     0.1015     0.1000

```python
rc = {'axes.labelsize': 12, 'font.size': 12, 'legend.fontsize': 12, 'axes.titlesize': 12}
plt.rcParams.update(**rc)
fig, ax = plt.subplots(1, 5, sharey=True, squeeze=True, figsize=(9, 3))
print('{0:10} {1:10} {2:5} {3:10}'.format('n', 'm', 's', '1/np.sqrt(n)'))
for i, n in enumerate([1, 2, 3, 10, 100]):
    f = np.mean(np.random.randn(100, n), 1)
    m, s = np.mean(f), np.std(f, ddof=1)
    x = np.linspace(np.min(f), np.max(f), 100)
    fn = (1/(s*np.sqrt(2*np.pi)))*np.exp(-(x-m)**2/(2*s**2))  # normal pdf
    ax[i].hist(f, 10, density=True, color=[0, 0.2, .8, .6], edgecolor='black')
    ax[i].set_title('n=%d' %n, fontsize=12)
    ax[i].plot(x, fn, color=[1, 0, 0, .6], linewidth=2)
    ax[i].xaxis.label.set_size(12)
    print('{0:3} {1:10.4f} {2:10.4f} {3:10.4f}'.format(n, m, s, 1/np.sqrt(n)))
plt.suptitle('Standard deviation and standard error of the mean', fontsize=12, y=1.0)
plt.tight_layout()
plt.show()
```

    <IPython.core.display.Javascript object>

    n          m          s     1/np.sqrt(n)
      1     0.1274     0.9783     1.0000
      2    -0.0279     0.6698     0.7071
      3    -0.0232     0.5716     0.5774
     10    -0.0054     0.2846     0.3162
    100    -0.0014     0.0940     0.1000

## References

- Hahn GJ, Meeker WQ (1991) [Statistical Intervals: A Guide for Practitioners](http://books.google.com.br/books?id=ADGuRxqt5z4C). John Wiley & Sons.
- Montgomery (2013) [Applied Statistics and Probability for Engineers](http://books.google.com.br/books?id=_f4KrEcNAfEC). John Wiley & Sons.