#  CS109B Data Science 2: Advanced Topics in Data Science

## Lecture 10 - Bayesian Analysis and Introduction to pyMC3

**Harvard University**

**Spring 2021**

**Instructors:** Pavlos Protopapas, Mark Glickman, and Chris Tanner

**Additional Instructor and content:** Eleni Angelaki Kaxiras

---

```python
## RUN THIS CELL TO PROPERLY HIGHLIGHT THE EXERCISES
import requests
from IPython.core.display import HTML
styles = requests.get("https://raw.githubusercontent.com/Harvard-IACS/2019-CS109B/master/content/styles/cs109.css").text
HTML(styles)
```

```python
import pymc3 as pm
# from pymc3 import summary
import arviz as az
from matplotlib import gridspec
# Ignore a common pymc3 warning that comes from library functions, not our code.
# Pymc3 may throw additional warnings, but other warnings should be manageable
# by following the instructions included within the warning messages.
import warnings
messages=[
    "Using `from_pymc3` without the model will be deprecated in a future release",
]

# or silence all warnings (not recommended)
# warnings.filterwarnings('ignore')

for m in messages:
    warnings.filterwarnings("ignore", message=m)
print(f"Using PyMC3 version: {pm.__version__}")
print(f"Using ArviZ version: {az.__version__}")
```

    Using PyMC3 version: 3.8
    Using ArviZ version: 0.6.1

```python
import pymc3 as pm
```

```python
from scipy import optimize
```

```python
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as stats
import pandas as pd
import seaborn as sns
%matplotlib inline
```

```javascript
%%javascript
IPython.OutputArea.auto_scroll_threshold = 20000;
```

    <IPython.core.display.Javascript object>

```python
# pandas trick
pd.options.display.max_columns = 50  # or =None -> No Restrictions
pd.options.display.max_rows = 200    # or =None -> *Be careful with this*
pd.options.display.max_colwidth = 100
pd.options.display.precision = 3
```

## Learning Objectives

By the end of this lab, you should be able to:
* identify and describe some of the most important probability distributions.
* apply Bayes Rule in calculating probabilities (and would have had fun in the process).
* create probabilistic models in the PyMC3 library.

## Table of Contents
1. [Some common families of distributions (review)](#1)
2. [The Bayesian Way of Thinking](#2)
3. [Bayesian Regression with pyMC3](#3) and [Defining a model in pyMC3](#model)
4. [Bayesian Logistic Regression with pyMC3](#4)

[Appendix](#appe)

## 1. Some common families of distributions

Statistical distributions are characterized by one of more parameters, such as $\mu$ and $\sigma^2$ for a Gaussian distribution.

\begin{equation}
Y \sim  \mathcal{N}(\mu,\,\sigma^{2})
\end{equation}

### Discrete Distributions

The **probability mass function (pmf)** of a discrete random variable $Y$ is equal to the probability that our random variable will take a specific value $y$: $f_y=P(Y=y)$ for all $y$. The variable, most of the times, assumes integer values.  Plots for **pmf**s are better understood as stem plots since we have discrete values for the probabilities and not a curve. Probabilities should add up to 1 for proper distributions.

- **Bernoulli** for a binary outcome, success has probability $\theta$, and we only have $one$ trial:

\begin{equation}
P(Y=k) =  \theta^k(1-\theta)^{1-k}
\end{equation}

- **Binomial** for a binary outcome, success has probability $\theta$, $k$ successes in $n$ trials:

Our random variable is $Y$= number of successes in $n$ trials.

\begin{equation}
P(Y=k|n,\theta) =  {{n}\choose{k}} \cdot \theta^k(1-\theta)^{n-k} \quad k=0,1,2,...,n
\end{equation}

As a reminder ${{n}\choose{k}}$ is "from $n$ choose $k$":
\begin{equation}
{{n}\choose{k}} = \frac{n!}{k!(n-k)!}
\end{equation}

$EY=n\theta$, $VarY = np(1-p)$

**Note** : Binomial (1,$p$) = Bernouli ($p$)

Exercise: Run the code below (that plots the Binomial distribution using stats.binom.pmf) for various values of the probability for a success $\theta\in [0,1]$. Look at the ordinate values to verify that they add up to 1.

```python
# probabity of success
theta = 0.5
n = 5
k = np.arange(0, n+1)
print(k)

# Best to read op on this function (was required before lab)
pmf = stats.binom.pmf(k, n, theta)
plt.style.use('seaborn-darkgrid')
plt.stem(k, pmf, label=r'n = {}, $\theta$ = {}'.format(n, theta))
plt.xlabel('Y', fontsize=14)
plt.ylabel('P(Y)', fontsize=14)
plt.legend()
plt.show()

# Probability should sum to 1
# Used stem to indicate that it's discrete
```

- **Negative Binomial** for a binary outcome, success has probability $\theta$, we have $r$ successes in $x$ trials:

Our random variable is $X$= number of trials to get to $r$ successes.

\begin{equation}
P(X=x|r,\theta) =  {{x-1}\choose{r-1}} \cdot \theta^r(1-\theta)^{x-r} \quad x=r,r+1,...
\end{equation}

- **Poisson** (counts independent events and has a single parameter $\lambda$)

\begin{equation}
P\left( Y=y|\lambda \right) = \frac{{e^{ - \lambda } \lambda ^y }}{{y!}} \quad y = 0,1,2,...
\end{equation}

Exercise: Change the value of $\lambda$ in the Poisson pmf plot below and see how the plot changes. Remember that the y-axis in a discrete probability distribution shows the probability of the random variable having a specific value in the x-axis. We use stats.poisson.pmf(x, $\lambda$), where $\lambda$ is the parameter.

```python
plt.style.use('seaborn-darkgrid')
x = np.arange(0, 10)
lam = 4
pmf = stats.poisson.pmf(x, lam)
plt.stem(x, pmf, label='$\lambda$ = {}'.format(lam))
plt.xlabel('Y', fontsize=12)
plt.ylabel('P(Y)', fontsize=12)
plt.legend(loc=1)
plt.ylim=(-0.1)
plt.show()
```

- **Discrete Uniform** for a random variable $X\in(1,N)$:

\begin{equation}
P(X=x|N) = \frac{1}{N}, \quad x=1,2,...,N
\end{equation}

```python
# boring but useful as a prior
plt.style.use('seaborn-darkgrid')
N = 40
x = np.arange(0, N)
pmf = stats.randint.pmf(x, 0, N)
plt.plot(x, pmf, label='$N$ = {}'.format(N))
fontsize=14
plt.xlabel('X', fontsize=fontsize)
plt.ylabel(f'P(X|{N})', fontsize=fontsize)
plt.legend()
plt.show()

# Usefull for when you don't what the distribution is
```

- **Categorical, or Multinulli** (random variables can take any of K possible categories, each having its own probability; this is a generalization of the Bernoulli distribution for a discrete variable with more than two possible outcomes, such as the roll of a die)

\begin{equation}
f(x_1,x_2,...,x_n) =  \frac{m}{x_1!\cdot x_2!\dotsb x_n!} \cdot p_1^{x_1}\cdot p_2^{x_2}\dotsb p_n^{x_n}
\end{equation}

### Continuous Distributions

The random variable has a **probability density function (pdf)** whose area under the curve equals to 1.
- **Uniform** (variable equally likely to be near each value in interval $(a,b)$)
\begin{equation}
f(x|a,b) = \frac{1}{b - a} \quad x\in [a,b] \quad \text{and 0 elsewhere}.
\end{equation}

Exercise: Change the value of $\mu$ in the Uniform PDF and see how the plot changes.

Remember that the y-axis in a continuous probability distribution does not shows the actual probability of the random variable having a specific value in the x-axis because that probability is zero!. Instead, to see the probability that the variable is within a small margin we look at the integral below the curve of the PDF.

The uniform is often used as a noninformative prior.

```
Uniform - numpy.random.uniform(a=0.0, b=1.0, size)
```

$\alpha$ and $\beta$ are our parameters. `size` is how many tries to perform.
The mean is $\mu = \frac{(a+b)}{2}$

```python
from scipy.stats import uniform

# We need to give it parameters to start
# Start with setting the mean and variance to some values you randomly select
# So we need to keep on trying with different mean and variances
a = 0
b = 1
r = uniform.rvs(loc=a, scale=b-a, size=1000)
pdf = uniform.pdf(r,loc=a, scale=b-a)
plt.plot(r, pdf,'b-', lw=3, alpha=0.6, label='uniform pdf')
plt.hist(r, density=True, histtype='stepfilled', alpha=0.2)
plt.ylabel(r'pdf')
plt.xlabel(f'x')
plt.legend(loc='best', frameon=False)
plt.show()

# NOTE: We really need to run this ourselves many times to really;
# Understand what it happening
# Need to understand what is our random variable (this varies)
# and parameters is something we don't know but we setup a distribution
```

- **Normal** (a.k.a. Gaussian)
\begin{equation}
X \sim  \mathcal{N}(\mu,\,\sigma^{2})
\end{equation}

    A Normal distribution can be parameterized either in terms of precision $\tau$ or variance $\sigma^{2}$. The link between the two is given by
\begin{equation}
\tau = \frac{1}{\sigma^{2}}
\end{equation}
 - Expected value (mean) $\mu$
 - Variance $\frac{1}{\tau}$ or $\sigma^{2}$
 - Parameters: `mu: float`, `sigma: float` or `tau: float`
 - Range of values (-$\infty$, $\infty$)

```python
plt.style.use('seaborn-darkgrid')
x = np.linspace(-5, 5, 1000)
mus = [0., 0., 0., -2.]
sigmas = [0.4, 1., 2., 0.4]
for mu, sigma in zip(mus, sigmas):
    pdf = stats.norm.pdf(x, mu, sigma)
    plt.plot(x, pdf, label=r'$\mu$ = '+ f'{mu},' + r'$\sigma$ = ' + f'{sigma}')
plt.xlabel('random variable', fontsize=12)
plt.ylabel('probability density', fontsize=12)
plt.legend(loc=1)
plt.show()

# The normal distribution
```

- **Beta** (where the variable ($\theta$) takes on values in the interval $(0,1)$, and is parametrized by two positive parameters, $\alpha$ and $\beta$ that control the shape of the distribution. Note that Beta is a good distribution to use for priors (beliefs) because its range is $[0,1]$ which is the natural range for a probability and because we can model a wide range of functions by changing the $\alpha$ and $\beta$ parameters. Its density (pdf) is:

\begin{equation}
\label{eq:beta}
P(\theta|a,b) = \frac{1}{B(\alpha, \beta)} {\theta}^{\alpha - 1} (1 - \theta)^{\beta - 1} \propto {\theta}^{\alpha - 1} (1 - \theta)^{\beta - 1}
\end{equation}

where the normalisation constant, $B$, is a beta function of $\alpha$ and $\beta$,

\begin{equation}
B(\alpha, \beta) = \int_{x=0}^1 x^{\alpha - 1} (1 - x)^{\beta - 1} dx.
\end{equation}
 - 'Nice', unimodal distribution
 - Range of values $[0, 1]$

$EX = \frac{a}{a+b}$,    $VarX = \frac{ab}{(a+b)^2(a+b+1)}$

Exercise: Try out various combinations of $a$ and $b$. We get an amazing set of shapes by tweaking the two parameters $a$ and $b$. Notice that for $a=b=1.$ we get the uniform distribution. As the values increase, we get a curve that looks more and more Gaussian.

```python
from scipy.stats import beta

fontsize = 15

# Play arround with changing the values
# Alphas don't really seem to change anything only when more beta used
alphas = [0.5] #, 0.5, 1., 3., 6.]
alphas = [0.5, 1., 3., 6.]
betas = [0.5] #, 1., 1., 3., 6.]
betas = [0.5, 1., 1., 3., 6.]
x = np.linspace(0, 1, 1000)
colors = ['red', 'green', 'blue', 'black', 'pink']

fig, ax = plt.subplots(figsize=(8, 5))

for a, b, colors in zip(alphas, betas, colors):
    plt.plot(x, beta.pdf(x,a,b), c=colors,
             label=f'a={a}, b={b}')

ax.set_ylim(0, 3)

ax.set_xlabel(r'$\theta$', fontsize=fontsize)
ax.set_ylabel(r'P ($\theta|\alpha,\beta)$', fontsize=fontsize)
ax.set_title('Beta Distribution', fontsize=fontsize*1.2)

ax.legend(loc='best')
fig.show();

# Often used as a prior
# This goes from 0 to 1 compared to infinity;
# that’s why it’s used so often
```

 At home: Prove the formula mentioned in class which gives the probability density for a Beta distribution with parameters $2$ and $5$:

$p(\theta|2,5) = 30 \cdot \theta(1 - \theta)^4$

 #### Code Resources:
 - Statistical Distributions in numpy/scipy: [scipy.stats](https://docs.scipy.org/doc/scipy/reference/stats.html)

 [Top](#top)

## 2. The Bayesian way of Thinking

```
Here is my state of knowledge about the situation. Here is some data, I am now going to revise my state of knowledge.
```

### Bayes Rule

\begin{equation}
\label{eq:bayes}
P(A|\textbf{B}) = \frac{P(\textbf{B} |A) P(A) }{P(\textbf{B})}
\end{equation}

$P(A|\textbf{B})$ is the **posterior** distribution, probability(parameters| data).

$P(\textbf{B} |A)$ is the **likelihood** function, how probable is my data for different values of the parameters.

$P(A)$ is the marginal probability to observe the data, called the **prior**, this captures our belief about the data before observing it.

$P(\textbf{B})$ is the marginal distribution (sometimes called marginal likelihood). This serves a normalization purpose.

### Let's Make a Deal

The problem we are about to solve gained fame as part of a game show "Let's Make A Deal" hosted by Monty Hall, hence its name. It was first raised by Steve Selvin in American Statistician in 1975.

The game is as follows: there are 3 doors behind **one** of which are the keys to a new car. There is a goat behind each of the other two doors. Let's assume your goal is to get the car and not a goat.

You are asked to pick one door, and let's say you pick **Door1**. The host knows where the keys are. Of the two remaining closed doors, he will always open the door that has a goat behind it. He'll say "I will do you a favor and open **Door2**". So he opens Door2 inside which there is, of course, a goat. He now asks you, do you want to open the initial Door1 you chose or change to **Door3**? Generally, in this game, when you are presented with this choice should you swap the doors?

**Hint:**
- Start by defining the `events` of this probabilities game. One definition is:

    - $A_i$: car is behind door $i$

    - $B_i$ host opens door $i$

$i\in[1,2,3]$

- In more math terms, the question is: is the probability of **the price is behind Door 1** higher than the probability of **the price is behind Door2**, given that an event **has occured**?

Breakout Game: Solve the Monty Hall Paradox using Bayes Rule.

### Bayes rule revisited

We have data that we believe come from an underlying distribution of unknown parameters. If we find those parameters, we know everything about the process that generated this data and we can make inferences (create new data).

\begin{equation}
P(\theta|\textbf{D}) = \frac{P(\textbf{D} |\theta) P(\theta) }{P(\textbf{D})}
\end{equation}

#### But what is $\theta \;$?

$\theta$ is an unknown yet fixed set of parameters. In Bayesian inference we express our belief about what $\theta$ might be and instead of trying to guess $\theta$ exactly, we look for its **probability distribution**. What that means is that we are looking for the **parameters** of that distribution. For example, for a Poisson distribution our $\theta$ is only $\lambda$. In a normal distribution, our $\theta$ is often just $\mu$ and $\sigma$.

 [Top](#top)

## 3. Bayesian Regression with `pyMC3`

PyMC3 is a Python library for programming Bayesian analysis, and more specifically, data creation, model definition, model fitting, and posterior analysis. It uses the concept of a `model` which contains assigned parametric statistical distributions to unknown quantities in the model. Within models we define random variables and their distributions. A distribution requires at least a `name` argument, and other `parameters` that define it. You may also use the `logp()` method in the model to build the model log-likelihood function. We define and fit the model.

PyMC3 includes a comprehensive set of pre-defined statistical distributions that can be used as model building blocks. They are not meant to be used outside of a `model`, and you can invoke them by using the prefix `pm`, as in `pm.Normal`.

For more see: [PyMC3 Quickstart](https://docs.pymc.io/notebooks/api_quickstart.html)

 ### Distributions in `PyMC3`:
 - Statistical [Distributions in pyMC3](https://docs.pymc.io/api/distributions.html).

Information about PyMC3 functions including descriptions of distributions, sampling methods, and other functions, is available via the `help` command.

```python
# help(pm.Beta)
```

### Defining a Model in PyMC3

Our problem is the following: we want to perform multiple linear regression to predict an outcome variable $Y$ which depends on variables $\bf{x}_1$ and $\bf{x}_2$.

We will model $Y$ as normally distributed observations with an expected value $mu$ that is a linear function of the two predictor variables, $\bf{x}_1$ and $\bf{x}_2$.

\begin{equation}
Y \sim  \mathcal{N}(\mu,\,\sigma^{2})
\end{equation}

\begin{equation}
\mu = \beta_0 + \beta_1 \bf{x}_1 + \beta_2 x_2
\end{equation}

where $\sigma^2$ represents the measurement error (in this example, we will use $\sigma^2 = 10$)

We also choose the parameters to have normal distributions with those parameters set by us.

\begin{eqnarray}
\beta_i \sim  \mathcal{N}(0,\,10) \\
\sigma^2 \sim  |\mathcal{N}(0,\,10)|
\end{eqnarray}
We will artificially create the data to predict on. We will then see if our model predicts them correctly.

#### Let's create some synthetic data

```python
np.random.seed(123)

# In the next lab the algorithm will find these hidden parameters
###################################
## Hidden true parameter values
sigma = 1
beta0 = 1
beta = [1, 2.5]
####################################

# Size of dataset
size = 100

# Predictor variable
x1 = np.linspace(0, 1., size)
x2 = np.linspace(0,2., size)

# Simulate outcome variable
Y = beta0 + beta[0]*x1 + beta[1]*x2 + np.random.randn(size)*sigma

# We generate data with the actual parameters we want to find
# Now we use this data and check if our model can actually find these params

```

```python
Y.shape, x1.shape, x2.shape
```

    ((100,), (100,), (100,))

```python
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
fontsize=14
labelsize=8
title='Observed Data (created artificially by ' + r'$Y(x_1,x_2)$)'
ax = fig.add_subplot(111, projection='3d')

ax.scatter(x1, x2, Y)
ax.set_xlabel(r'$x_1$', fontsize=fontsize)
ax.set_ylabel(r'$x_2$', fontsize=fontsize)
ax.set_zlabel(r'$Y$', fontsize=fontsize)

ax.tick_params(labelsize=labelsize)

fig.suptitle(title, fontsize=fontsize)
fig.tight_layout(pad=.1, w_pad=10.1, h_pad=2.)
fig.subplots_adjust(); #top=0.5
plt.tight_layout
plt.show()
```

#### Building the model

**Step1:** Formulate the probability model for our data: $Y \sim  \mathcal{N}(\mu,\,\sigma^{2})$

```
Y_obs = pm.Normal('Y_obs', mu=mu, sd=sigma, observed=Y)
```

**Step2:** Choose a prior distribution for our unknown parameters.
```
 beta0 = Normal('beta0', mu=0, sd=10)
 # Note: betas is a vector of two variables, b1 and b2, (denoted by shape=2)
 # so, in array notation, our beta1 = betas[0], and beta2=betas[1]
 betas = Normal('betas', mu=0, sd=10, shape=2)
 sigma = HalfNormal('sigma', sd=1)

```
**Step3:** Construct the likelihood function.

**Step4:** Determine the posterior distribution, **this is our main goal**.

**Step5:** Summarize important features of the posterior and/or plot the parameters.

```python
# HELPFUL FOR THE HW
with pm.Model() as my_linear_model:

    # Priors for unknown model parameters, specifically created stochastic random variables
    # with Normal prior distributions for the regression coefficients,
    # and a half-normal distribution for the standard deviation of the observations.
    # These are our parameters.

    # I need to give the prior some initial values for the parameters
    mu0 = 0
    sd0 = 10
    beta0 = pm.Normal('beta0', mu=mu0, sd=sd0)
    # Note: betas is a vector of two variables, b1 and b2, (denoted by shape=2)
    # so, in array notation, our beta1 = betas[0], and beta2=betas[1]
    mub = 0
    sdb = 10
    betas = pm.Normal('betas', mu=mub, sd=sdb, shape=2)
    sds = 1
    sigma = pm.HalfNormal('sigma', sd=sds)

    # mu is what is called a deterministic random variable, which implies that
    # its value is completely determined by its parents’ values
    # (betas and sigma in our case). There is no uncertainty in the
    # variable beyond that which is inherent in the parents’ values

    mu = beta0 + betas[0]*x1 + betas[1]*x2

    # Likelihood function = how probable is my observed data?
    # This is an observed variable; it is identical to a standard
    # stochastic variable, except that its observed argument,
    # which passes the data to the variable, indicates that the values for this
    # variable were observed, and should not be changed by any
    # fitting algorithm applied to the model.
    # The data can be passed in the form of a numpy.ndarray or pandas.DataFrame object.

    Y_obs = pm.Normal('Y_obs', mu=mu, sd=sigma, observed=Y)
```

```python
my_linear_model.basic_RVs
```

    [beta0, betas, sigma_log__, Y_obs]

```python
my_linear_model.free_RVs
```

    [beta0, betas, sigma_log__]

**Note**: If our problem was a classification for which we would use Logistic regression.

```python
## do not worry if this does not work, it's just a nice graph to have
## you need to install python-graphviz first
# conda install -c conda-forge python-graphviz
# pm.model_to_graphviz(my_linear_model)
```

Now all we need to do is sample our model.

### ... to be continued

## Appendix A: Bayesian Logistic Regression with `pyMC3`

If the problem above was a classification that required a Logistic Regression, we would use the logistic function ( where $\beta_0$ is the intercept, and $\beta_i$ (i=1, 2, 3) determines the shape of the logistic function).

\begin{equation}
Pr(Y=1|X_1,X_2,X3) = {\frac{1}{1 + exp^{-(\beta_0 + \beta_1X_1 + \beta_2X_2 + \beta_3X_3)}}}
\end{equation}

Since both $\beta_0$ and the $\beta_i$s can be any possitive or negative number, we can model them as gaussian random variables.

\begin{eqnarray}
\beta_0 \sim  \mathcal{N}(\mu,\,\sigma^2) \\
\beta_i \sim  \mathcal{N}(\mu_i,\,\sigma_i^2)
\end{eqnarray}

In PyMC3 we can model those as:
```
pm.Normal('beta_0', mu=0, sigma=100)
```
(where $\mu$ and $\sigma^2$ can have some initial values that we assign them, e.g. 0 and 100)

The dererministic variable would be:
```
logitp = beta0 + beta_1 * X_1 + beta_2 * X_2 + beta_3 * X_3
```
To connect this variable (logit-p) with our observed data, we would use a Bernoulli as our likelihood.
```
our_likelihood = pm.Bernoulli('our_likelihood', logit_p=logitp, observed=our_data)
```

Notice that the main difference with Linear Regression is the use of a Bernoulli distribution instead of a Gaussian distribution, and the use of the logistic function instead of the identity function.

Breakout Room Exercise: Write the model above in code. Suppose that your training dataframe (df_train) has the following features:

**numerical**
- df_train['age']
- df_train['weight']

**categorical**
- df_train['hypertension']

```python
# A reminder of what the logistic function looks like.
# Change parameters a and b to see the shape of the curve change
b = 5.
x = np.linspace(-8, 8, 100)
plt.plot(x, 1 / (1 + np.exp(-b*x)))
plt.xlabel('y')
plt.ylabel('y=logistic(x)')
```

## Appendix B: Is this a fair coin?

### Is this a fair coin?

Let's say you visit the casino in **Monte Carlo**. You want to test your theory that casinos are dubious places where coins have been manipulated to have a larger probability for tails. So you will try to estimate how fair a coin is based on a certain amount of  flips.

You have no prior opinion on the coin's fairness (i.e. what $p$ might be), and begin flipping the coin. You get either Heads ($H$) or Tails ($T$) as our observed data and want to see if your posterior probabilities change as you obtain more data, that is, more coin flips. A nice way to visualize this is to plot the posterior probabilities as we observe more flips (data).

We will be using Bayes rule. $\textbf{D}$ is our data.

\begin{equation}
P(\theta|\textbf{D}) = \frac{P(\textbf{D} |\theta) P(\theta) }{P(\textbf{D})}
\end{equation}

We start with a non-informative prior, a Beta distribution with (a=b=1.)

\begin{equation}
P(\theta|\textbf{k=0}) = Beta(1., 1.)
\end{equation}

Then, as we get new data (say, we observe $k$ heads in $n$ tosses), we update our Beta with new a,b as follows:

\begin{equation}
P(\theta|\textbf{k}) = Beta(\alpha + \textbf{k}, \beta + (n - \textbf{k}))
\end{equation}

*(the proof is beyond our scope, if interested, see this [Wikipedia article](https://en.wikipedia.org/wiki/Conjugate_prior#Example))*

we can say that $\alpha$ and $\beta$ play the roles of a "prior number of heads" and "prior number of tails".

```python
# play with the priors - here we manually set them but we could be sampling from a separate Beta
trials = np.array([0, 1, 3, 5, 10, 15, 20, 100, 200, 300])
heads = np.array([0, 1, 2, 4, 8, 10, 10, 50, 180, 150])
x = np.linspace(0, 1, 100)

# for simplicity we set a,b=1
plt.figure(figsize=(10,8))
for k, N in enumerate(trials):
    sx = plt.subplot(len(trials)/2, 2, k+1)
    posterior = stats.beta.pdf(x, 1 + heads[k], 1 + trials[k] - heads[k])
    plt.plot(x, posterior, alpha = 0.5, label=f'{trials[k]} tosses\n {heads[k]} heads');
    plt.fill_between(x, 0, posterior, color="#348ABD", alpha=0.4)
    plt.legend(loc='upper left', fontsize=10)
    plt.legend()
    plt.autoscale(tight=True)

plt.suptitle("Posterior probabilities for coin flips", fontsize=15);
plt.tight_layout()
plt.subplots_adjust(top=0.88)
```

### References

- *Salvatier J, Wiecki TV, Fonnesbeck C. 2016. Probabilistic programming in Python using PyMC3. PeerJ Computer Science 2:e55* [(https://doi.org/10.7717/peerj-cs.55)](https://doi.org/10.7717/peerj-cs.55)
- [Distributions in PyMC3](https://docs.pymc.io/api/distributions.html)
- [More Details on Distributions](https://docs.pymc.io/developer_guide.html)

This linear regression example is from the original paper on PyMC3: *Salvatier J, Wiecki TV, Fonnesbeck C. 2016. Probabilistic programming in Python using PyMC3. PeerJ Computer Science 2:e55 https://doi.org/10.7717/peerj-cs.55*

#### Cool Reading

- How Bayesian Analysis and Lawrence D. Stone found the Wreckage of [Air France Flight AF 447](https://www.jstor.org/stable/pdf/43288452.pdf?refreqid=excelsior%3Ae972f3a7b38b5815e1570b93dad7d269).
- Search for the gold on the sunken [SS Central America](https://www.metsci.com/wp-content/uploads/2019/08/Search-for-the-SS-Central-America-Mathematical-Treasure-Hunting-Interfaces-22-1-Jan.-Feb.-1992.pdf).


# PID Controller
Self-Driving Car Engineer Nanodegree Program

In this project I implemented a PID controller to steer an autonomous driving car around a track in a simulator. The code is written in C++. This work is part of the Self-Driving Car Engineer Nanodegree Program at Udacity.

---

[//]: # (Image References)

[image1]: ./out_images/image2.png

## Dependencies

* cmake >= 3.5
 * All OSes: [click here for installation instructions](https://cmake.org/install/)
* make >= 4.1(mac, linux), 3.81(Windows)
  * Linux: make is installed by default on most Linux distros
  * Mac: [install Xcode command line tools to get make](https://developer.apple.com/xcode/features/)
  * Windows: [Click here for installation instructions](http://gnuwin32.sourceforge.net/packages/make.htm)
* gcc/g++ >= 5.4
  * Linux: gcc / g++ is installed by default on most Linux distros
  * Mac: same deal as make - [install Xcode command line tools]((https://developer.apple.com/xcode/features/)
  * Windows: recommend using [MinGW](http://www.mingw.org/)
* [uWebSockets](https://github.com/uWebSockets/uWebSockets)
  * Run either `./install-mac.sh` or `./install-ubuntu.sh`.
  * If you install from source, checkout to commit `e94b6e1`, i.e.
    ```
    git clone https://github.com/uWebSockets/uWebSockets
    cd uWebSockets
    git checkout e94b6e1
    ```
    Some function signatures have changed in v0.14.x. See [this PR](https://github.com/udacity/CarND-MPC-Project/pull/3) for more details.
* Simulator. You can download these from the [project intro page](https://github.com/udacity/self-driving-car-sim/releases) in the classroom.

Fellow students have put together a guide to Windows set-up for the project [here](https://s3-us-west-1.amazonaws.com/udacity-selfdrivingcar/files/Kidnapped_Vehicle_Windows_Setup.pdf) if the environment you have set up for the Sensor Fusion projects does not work for this project. There's also an experimental patch for windows in this [PR](https://github.com/udacity/CarND-PID-Control-Project/pull/3).

## Basic Build Instructions

1. Clone this repo.
2. Make a build directory: `mkdir build && cd build`
3. Compile: `cmake .. && make`
4. Run it: `./pid`.

Tips for setting up your environment can be found [here](https://classroom.udacity.com/nanodegrees/nd013/parts/40f38239-66b6-46ec-ae68-03afd8a601c8/modules/0949fca6-b379-42af-a919-ee50aa304e6a/lessons/f758c44c-5e40-4e01-93b5-1a82aa4e044f/concepts/23d376c7-0195-4276-bdf0-e02f1f3c665d)

## Implementation
 ### Understanding of PID controller:

 * The overall control function

 \begin{equation}
 u(t)= kp*e(t) + ki* \int_{0}^{t}e(t')dt' +kd* \frac{de(t)}{dt}
\end{equation}

Where in our project, we take the negative value of the cross track error (CTE) as error "e(t)". The proportional part of the control law make the vehicle turns when it is away from the center of the road. The derivative term provides a better damping so it steers gracefully, while the integral term helps to reduce the steady state error of the system. In this project, we need to tune the parameters kp,kd and ki either manually or automatically.  The following data are what I tuned for this project, which worked well.  And also, I impletemented the PID controller for both steering angle and speed of the car in the simulator.  I chose manual method, following the idea of the twiddle algorithm.

|      | steering angle | speed |
| ----------- | ----------- |----------- |
| kp   | 0.12        | 0.1     |
| kd   | 1.0       |0.0    |
| ki   | 0.0001     |0.002  |

* Implementation of the above equation

```cpp
void PID::Init(double Kp, double Ki, double Kd) {
	/*Set Kp, Ki and Kd to initial values passed by controller. These are passed from main*/
	this->Kp = Kp;
	this->Ki = Ki;
	this->Kd = Kd;
	/*Set up inital p, i and d error to zero.*/
	p_error = 0;
	i_error = 0;
	d_error = 0;
}
void PID::UpdateError(double cte) {
	d_error = cte - p_error;
	p_error = cte;
	i_error += cte;
}
double PID::TotalError() {
	return (-(Kp * p_error) - (Ki * i_error) - (Kd * d_error));
}
```

* Implementation in the simulator
![alt text][image1]

* The video sample of this implementation is as follows
Check youtube video [](https://youtu.be/OWmCYxaqbnQ)

## Reflections

In this project, I impleted PID controller for self-driving car, the core part of the code is not difficult to write, but the parameters for the PID controllers were not easy to tune. I have tried many times, and also refer to the resources online, Finally, I was able to find parameters that worked well.

```python

```


이번에는 선형대수에서 매우 중요한 개념인 "Linear Indepencence(선형독립)" 개념에 대해 살펴보겠습니다.

### 1. Linear Combination

먼저, Linear combination(선형조합) 개념에 대해 알아야 합니다. 선형독립의 정의가 선형조합을 바탕으로 하고 있기 때문이죠!

선형조합은 말그대로 Linear하게 조합한 것 입니다. MML에 따르면,

**Linear combination이란, "$V(v, +, \cdot)$ 내 vector(v)들의 Addition, Multiplication by scalar 연산으로 얻어낸 조합"**

라고 합니다!

즉, 아래와 같이 벡터들의 선형결합연산으로 조합해낸 상태를 말하는 것이죠!

$$
\begin{align}
v = c_1 x_1 + c_2 x_2 + \cdots + c_N x_N
\end{align}
$$

여기서, $v$는 $x$벡터들의 선형조합으로 만들어낸 것이라고 볼 수 있습니다. 당연하게도, $x$와 $v$는 같은 벡터공간 안에 있겠죠!?

### 2. Linear Independence

선형독립의 정의는 위에서 본 선형조합의 식을 활용합니다.

$$
\begin{align}
0 = c_1 x_1 + c_2 x_2 + \cdots + c_N x_N
\end{align}
\tag{식 1}
$$

**식(1)과 같은 벡터들의 선형조합(Linear combination)이 zero-vector가**

**계수가 모두 0일 때$("c_{1} = ... = c_{n} = 0")$ "만" 가능하다면, 벡터$(x_{1} ... x_{n})$ 가 서로 Linearly independent 하다고 정의합니다.**

예를 들어, 다음 벡터 $x_1, x_2$는 선형독립입니다.

$$
\begin{align}
x_1 = \begin{bmatrix} 1 \\ 2 \end{bmatrix}, \;\;
x_2 = \begin{bmatrix} 3 \\ 3 \end{bmatrix}
\tag{3.2.4}
\end{align}
$$

벡터 $x_1$은 두 원소의 크기가 다른데, 벡터 $x_2$는 두 원소의 크기가 같기 때문에 어떤 계수를 사용해도 $c_1x_1 + c_2x_2 = 0$을 만들 수 없습니다.

하지만, 다음 벡터 $x_1, x_2, x_3$는 선형종속입니다.

$$
\begin{align}
x_1 = \begin{bmatrix} 1 \\ 2 \end{bmatrix}, \;\;
x_2 = \begin{bmatrix} 3 \\ 3 \end{bmatrix}, \;\;
x_3 = \begin{bmatrix} 5 \\ 7 \end{bmatrix}
\tag{3.2.5}
\end{align}
$$

이는 다음과 같은 식으로 증명할 수 있습니다.

$$
\begin{align}
2x_1  + x_2 - x_3 = 0
\tag{3.2.6}
\end{align}
$$

파이썬으로 계산한 결과도 영벡터입니다.

```python
import numpy as np

x1 = np.array([1, 2])
x2 = np.array([3, 3])
x3 = np.array([10, 14])
2 * x1 + x2 - 0.5 * x3
```

    array([0., 0.])

### 3. Linear dependence

선형 종속인 경우는 도대체 어떤 경우인 걸까요? 선형 종속을 이해하면, 선형 독립인 경우를 이해하는 데 도움이 되지 않을까 합니다.

선형 종속인 경우, 선형 독립이 아니기 때문에 벡터는 다른 벡터들의 선형조합(Linear combination)으로 생성이 가능합니다!

$$
\begin{align}
v = c_1 x_1 + c_2 x_2 + \cdots + c_N x_N
\end{align}
$$

위와 같은 상황이 되는 것이죠!

예를 들어,
$$ x_{1} - 3x_{2} + x_{3} = 0$$
이라는 방정식이 있다면, 이 때의 $x_{1}, x_{2}, x_{3}$ 은 서로 선형 독립일까요?

아닙니다. 선형 종속입니다!

왜냐하면, 위의 식을 조금만 변형해보면,

$$ x_{1} = + 3x_{2} - x_{3}$$

로 변형이 가능하고, $x_{1}$ 벡터는 다른 벡터들의 조합으로 표현이 가능하다는 것이니까요!

```python
%matplotlib inline

plt.figure(figsize=(10,5))
plt.plot(0, 0, 'kP', ms=10)
plt.text(0.1,-0.3,"Origin, zero-vector",fontsize = 15)
w = np.array([1, 2])
x1 = np.array([1, 2]) * 2
plt.annotate('', xy=x1, xytext=(0, 0),arrowprops=dict(facecolor='red'))
plt.annotate('', xy=w, xytext=(0, 0),arrowprops=dict(facecolor='blue'))
plt.text(0.1, 0.9, "$x_{1}$",fontsize = 15)
plt.text(1.5, 2.4, "$2x_{1} = x_{2}$",fontsize = 15)
plt.xticks(np.arange(-3, 15))
plt.yticks(np.arange(-1, 5))
plt.xlim(-1, 4)
plt.ylim(-1, 5)
plt.title("$x_{1}, x_{2}$ is linearly dependent",fontsize=15)
plt.show()
```

위의 그림처럼, $x_{2}$는 $x_{1}$의 2배로 표현이 가능합니다. 2배를 스케일링하면 표현이 가능한 것이죠! 바로 선형결합 연산으로 표현이 가능하다는 바로 그것입니다! 고로, $x_{1}, x{2}$는 서로 선형 종속입니다!

도표에 벡터를 시각화해서 보니... 이해에 도움이 되셨지 않을까 싶습니다! 두 벡터 모두 하나의 선(line)에서 벗어나지 못하고 갇혀(종속)있는 것이죠. 두 벡터를 아무리 더하고 빼고, 스케일링해서 더하고 해줘도... 저 선을 벗어날 수 없습니다. 예를 들면, [3,3] 같은 벡터는 저 2개의 벡터로 표현할 수 없는 것이죠! 저 직선 위의 벡터만 표현이 가능합니다. 말 그대로 2개의 벡터는 선에 '종속'되어버린 것이죠..!

오늘은 간단하게 선형독립과, 선형종속의 의미에 대해 살펴보았습니다. 물론 더 많은 이야기 파생될 수 있는 개념이지만, 일단 오늘은 간단하게 개념에 대해서만 살펴보았습니다. 이후의 포스트에서 더 다양한 개념들을 살펴보면 좋겠네요! :)

1.Study/2. with computer/1.Math_code/5. Mathematics for Machine Learning/2. Linear Algebra_5. Linear Independece.ipynb


# Elementary greenhouse models

This notebook is part of [The Climate Laboratory](https://brian-rose.github.io/ClimateLaboratoryBook) by [Brian E. J. Rose](http://www.atmos.albany.edu/facstaff/brose/index.html), University at Albany.

____________

## 1. A single layer atmosphere
____________

We will make our first attempt at quantifying the greenhouse effect in the simplest possible greenhouse model: a single layer of atmosphere that is able to absorb and emit longwave radiation.

### Assumptions

- Atmosphere is a single layer of air at temperature $T_a$
- Atmosphere is **completely transparent to shortwave** solar radiation.
- The **surface** absorbs shortwave radiation $(1-\alpha) Q$
- Atmosphere is **completely opaque to infrared** radiation
- Both surface and atmosphere emit radiation as **blackbodies** ($\sigma T_s^4, \sigma T_a^4$)
- Atmosphere radiates **equally up and down** ($\sigma T_a^4$)
- There are no other heat transfer mechanisms

We can now use the concept of energy balance to ask what the temperature need to be in order to balance the energy budgets at the surface and the atmosphere, i.e. the **radiative equilibrium temperatures**.

### Energy balance at the surface

$$
\begin{align*}
\text{energy in} &= \text{energy out} \\
(1-\alpha) Q + \sigma T_a^4 &= \sigma T_s^4 \\
\end{align*}
$$

The presence of the atmosphere above means there is an additional source term: downwelling infrared radiation from the atmosphere.

We call this the **back radiation**.

### Energy balance for the atmosphere

$$
\begin{align}
\text{energy in} &= \text{energy out} \\
\sigma T_s^4 &= A\uparrow + A\downarrow = 2 \sigma T_a^4 \\
\end{align}
$$

which means that
$$ T_s = 2^\frac{1}{4} T_a \approx 1.2 T_a $$

So we have just determined that, in order to have a purely **radiative equilibrium**, we must have $T_s > T_a$.

*The surface must be warmer than the atmosphere.*

### Solve for the radiative equilibrium surface temperature

Now plug this into the surface equation to find

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

and use the definition of the emission temperature $T_e$ to write

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

*In fact, in this model, $T_e$ is identical to the atmospheric temperature $T_a$, since all the OLR originates from this layer.*

Solve for the surface temperature:

$$ T_s = 2^\frac{1}{4} T_e $$

From the observed outgoing longwave radiation that we studied last week, $T_e = 255$ K. Thus you can solve for the surface temperature...

```python
## your T_s calculation here
```

This model is one small step closer to reality: surface is warmer than atmosphere, emissions to space generated in the atmosphere, atmosphere heated from below and helping to keep surface warm.

### Why does this model overpredict the surface temperature

Our model now overpredicts the surface temperature by about 15ºC (303 K versus the observed 288 K).

Why is this?  Let's discuss.

- your idea 1
- your idea 2
- classmates' ideas 1
- ...

____________

## 2. Introducing the two-layer grey gas model
____________

Let's generalize the above model just a little bit to build a slighly more realistic model of longwave radiative transfer.

We will address two shortcomings of our single-layer model:
1. No vertical structure
2. 100% longwave opacity

Relaxing these two assumptions gives us what turns out to be a very useful prototype model for **understanding how the greenhouse effect works**.

### Assumptions

- The atmosphere is **transparent to shortwave radiation** (still)
- Divide the atmosphere up into **two layers of equal mass** (the dividing line is thus at 500 hPa pressure level)
- Each layer **absorbs only a fraction $\epsilon$** of whatever longwave radiation is incident upon it.
- We will call the fraction $\epsilon$ the **absorptivity** of the layer.
- Assume $\epsilon$ is the same in each layer

This is called the **grey gas** model, where **grey** here means the emission and absorption have **no spectral dependence** (same at every wavelength).

We can think of this model informally as a "leaky greenhouse".

Note that the assumption that $\epsilon$ is the same in each layer is appropriate if the absorption is actually carried out by a gas that is **well-mixed** in the atmosphere.

Out of our two most important absorbers:

- CO$_2$ is well mixed
- H$_2$O is not (mostly confined to lower troposphere due to strong temperature dependence of the saturation vapor pressure).

But we will ignore this aspect of reality for now.

### Kirchoff's Law

In order to build our model, we need to introduce one additional piece of physics known as **Kirchoff's Law**:

$$ \text{absorptivity} = \text{emissivity} $$

So **if a layer of atmosphere at temperature $T$ absorbs a fraction $\epsilon$** of incident longwave radiation, it must **emit**

$$ \epsilon ~\sigma ~T^4 $$

both up and down.

### A sketch of the radiative fluxes in the 2-layer atmosphere

- Surface temperature is $T_s$
- Atm. temperatures are $T_0, T_1$ where $T_0$ is closest to the surface.
- absorptivity of atm layers is $\epsilon$
- Surface emission is $\sigma T_s^4$
- Atm emission is $\epsilon \sigma T_0^4, \epsilon \sigma T_1^4$ (up and down)
- Absorptivity = emissivity for atmospheric layers
- a fraction $(1-\epsilon)$ of the longwave beam is **transmitted** through each layer

____________
## 3. Tracing the upwelling beam of longwave radiation
____________

Let's think about the upwelling beam of longwave radiation, which we denote $U$.

### Surface to layer 0

We start at the surface. The upward flux **from the surface to layer 0** is

$$U_0 = \sigma T_s^4$$

(just the emission from the suface).

### Layer 0 to layer 1

Now **following this beam upward**, we first recognize that a fraction $\epsilon$ of this beam is **absorbed** in layer 0.

The upward flux from layer 0 to layer 1 consists of the sum of two parts:

1. The **transmitted part** of whatever is incident from below (i.e. the part that is **not absorbed**)
2. **New upward emissions** from layer 0

We can write this upward flux from layer 0 to layer 1 as:

$$U_1 = (1-\epsilon) \sigma T_s^4 + \epsilon \sigma T_0^4$$

### Beyond layer 1

Continuing to follow the same beam, we follow the same logic! A fraction $\epsilon$ of $U_1$ is absorbed in layer 1, and therefore the transmitted part is $(1-\epsilon) U_1$.

Including new emissions from layer 1, the upwelling flux above layer 1 is

$$U_2 = (1-\epsilon) U_1 + \epsilon \sigma T_1^4$$

### Outgoing Longwave Radiation

Since there is **no more atmosphere above layer 1**, this upwelling beam is our OLR for this model:

$$\text{OLR} = U_2 = (1-\epsilon) U_1 + \epsilon \sigma T_1^4$$

which, plugging in the above expression for $U_1$, works out to

$$\text{OLR} = (1-\epsilon)^2 \sigma T_s^4 + \epsilon(1-\epsilon)\sigma T_0^4 + \epsilon \sigma T_1^4$$

Here the three terms represent **contributions to the total OLR** that **originate from each of the three levels**

### Limits of large and small absorptivity/emissivity

Think about the following two questions:

- What happens to this expression if $\epsilon=1$? *What does this represent physically?*
- What about $\epsilon=0$?

By allowing the atmosphere to partially absorb emissions from other levels, we now see that the Outgoing Longwave Radiation to space **includes emissions from every level** - and therefore **affected by temperature at every level**!

____________

## 4. Tuning the grey gas model to observations
____________

In building our new model we have introduced exactly **one parameter**, the absorptivity $\epsilon$. We need to choose a value for $\epsilon$.

We will tune our model so that it **reproduces the observed global mean OLR** given **observed global mean temperatures**.

### Global mean air temperature observations

To get appropriate temperatures for $T_s, T_0, T_1$, let's revisit the global, annual mean lapse rate plot from NCEP Reanalysis data we first encountered in the [Radiation notes](https://brian-rose.github.io/ClimateLaboratoryBook/courseware/radiation.html).

```python
#  This code is used just to create the skew-T plot of global, annual mean air temperature
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import xarray as xr
from metpy.plots import SkewT
ncep_url = "http://www.esrl.noaa.gov/psd/thredds/dodsC/Datasets/ncep.reanalysis.derived/"
ncep_air = xr.open_dataset( ncep_url + "pressure/air.mon.1981-2010.ltm.nc", use_cftime=True)
#  Take global, annual average and convert to Kelvin
coslat = np.cos(np.deg2rad(ncep_air.lat))
weight = coslat / coslat.mean(dim='lat')
Tglobal = (ncep_air.air * weight).mean(dim=('lat','lon','time'))
fig = plt.figure(figsize=(9, 9))
skew = SkewT(fig, rotation=30)
skew.plot(Tglobal.level, Tglobal, color='black', linestyle='-', linewidth=2, label='Observations')
skew.ax.set_ylim(1050, 10)
skew.ax.set_xlim(-75, 45)
# Add the relevant special lines
skew.plot_dry_adiabats(linewidth=0.5)
skew.plot_moist_adiabats(linewidth=0.5)
#skew.plot_mixing_lines()
skew.ax.legend()
skew.ax.set_xlabel(r'Temperature ($^{\circ} C$)')
skew.ax.set_ylabel('Atmos. pressure (hPa)')
skew.ax.set_title('Global, annual mean sounding from NCEP Reanalysis', fontsize = 16);
```

### Target temperatures for our model tuning

First, we set
$$T_s = 288 \text{ K}  $$

From the lapse rate plot, an average temperature for the layer between 1000 and 500 hPa is

$$ T_0 = 275 \text{ K}$$

Defining an average temperature for the layer between 500 and 0 hPa is more ambiguous because of the lapse rate reversal at the tropopause. We will choose

$$ T_1 = 230 \text{ K}$$

From the graph, this is approximately the observed global mean temperature at 275 hPa or about 10 km.

### OLR

From the observed global energy budget (see previous weeks) we set

$$\text{OLR} = 238.5 \text{ W m}^{-2}$$

### Solving for $\epsilon$

We wrote down the expression for OLR as a function of temperatures and absorptivity in our model above.

All we need to do is plug the observed values into the above expression for OLR, and solve for $\epsilon$.

It is a **quadratic equation** for the unknown $\epsilon$. We could work out the exact solution using the quadratic formula.

But let's do it **graphically**, using Python!

### Exercise: graphical solution to find the best fit value of $\epsilon$

The OLR formula for the leaky greenhouse that we derived above is

$$\text{OLR} = (1-\epsilon)^2 \sigma T_s^4 + \epsilon(1-\epsilon)\sigma T_0^4 + \epsilon \sigma T_1^4$$

Do the following:

- Write a Python function that implements this formula
- The function should accept **four input parameters**:
    - The three temperatures $T_s, T_0, T_1$
    - The emissivity $\epsilon$
- Using this function, make a **graph of OLR vs. $\epsilon$** for the observed temperature values $T_s = 288, T_0 = 275, T_1 = 230$
- For the graph, $\epsilon$ should range between 0 and 1.
- From your graph, find the approximate value of $\epsilon$ that gives $OLR = 238.5$

```python
sigma=5.67E-8
def OLR(Ts, T0, T1, epsilon):
    fromsurf = ((1-epsilon)**2)*sigma*(Ts**4)
    fromL0 = epsilon*(1-epsilon)*sigma*(T0**4)
    fromL1 = epsilon*sigma*(T1**4)
    return fromsurf+fromL0+fromL1
```

```python
eps = np.linspace(0,1,num=20)
olrtests = OLR(Ts=288, T0=275, T1=230, epsilon=eps)
```

```python
fig, ax=plt.subplots()
ax.plot(eps, olrtests)
ax.axhline(238.5, color='k', ls=':')
ax.set(xlabel=r'$\epsilon$', ylabel='OLR [W/m2]')
```

Note if you solve the quadratic equation algebraically you will get two solutions:

- $\epsilon \approx 0.586$
- $\epsilon \approx 3.93$

(for details, see [the advanced notes here](https://brian-rose.github.io/ClimateLaboratoryBook/courseware/sympy-greenhouse.html))

**Why is the second solution not physically meaningful?**

Hopefully your graph shows that $\epsilon = 0.586$ gives the correct value of OLR.

This is the absorptivity that guarantees that **our model reproduces the observed OLR given the observed temperatures**.

____________

## 5. Level of emission
____________

### Contributions from each level to the outgoing radiation

Now that we have tuned up our model, we can see exactly how strongly each level contributes to the OLR.

The three components of the OLR are

$$
\begin{align*}
\text{OLR}_s &= (1-\epsilon)^2 \sigma T_s^4  \\
\text{OLR}_0 &= \epsilon(1-\epsilon)\sigma T_0^4 \\
\text{OLR}_1 &= \epsilon \sigma T_1^4
\end{align*}
$$

which of course add up to the total OLR we wrote down above.

### Exercise: calculate contributions to OLR

**Write some simple Python code to calculate each term in the OLR using the observed temperatures and the tuned value $\epsilon = 0.586$. Fill out the list below using your calculated numbers.**

```python

```

**Contributions to the OLR originating from each level, in W/m2:**

- Surface:
- Level 0:
- Level 1:

```python
# now sum up the numbers to verify you get something very close to 238.5

```

Notice that the largest single contribution is coming from the top layer.

*This is in spite of the fact that the emissions from this layer are weak, because it is so cold.*

###  Changing the level of emission by adding absorbers

Adding some **extra greenhouse absorbers** will mean that a **greater fraction** of incident longwave radiation is **absorbed in each layer**.

Thus **$\epsilon$ must increase** as we add greenhouse gases.

Suppose we have $\epsilon$ initially, and the absorptivity increases to $\epsilon_2 = \epsilon + \Delta \epsilon$.

Suppose further that this increase happens **abruptly** so that there is no time for the temperatures to respond to this change. **We hold the temperatures fixed** in the column and ask how the radiative fluxes change.

**Question: Do you expect the OLR to increase or decrease?**

### Calculating the change in level of emission

Let's use our two-layer leaky greenhouse model to investigate the answer.

The components of the OLR before the perturbation are

$$
\begin{align*}
\text{OLR}_s &= (1-\epsilon)^2 \sigma T_s^4  \\
\text{OLR}_0 &= \epsilon(1-\epsilon)\sigma T_0^4 \\
\text{OLR}_1 &= \epsilon \sigma T_1^4
\end{align*}
$$

and after the perturbation we have

$$
\begin{align*}
\text{OLR}_s &= (1-\epsilon - \Delta \epsilon)^2 \sigma T_s^4  \\
\text{OLR}_0 &= (\epsilon + \Delta \epsilon)(1-\epsilon - \Delta \epsilon)\sigma T_0^4 \\
\text{OLR}_1 &= (\epsilon + \Delta \epsilon) \sigma T_1^4
\end{align*}
$$

Let's subtract off the original components to get the contributions to the **change in OLR** from each layer:

$$
\begin{align*}
\Delta \text{OLR}_s &= \left[(1-\epsilon - \Delta \epsilon)^2 - (1-\epsilon)^2\right]\sigma T_s^4  \\
\Delta \text{OLR}_0 &= \left[(\epsilon + \Delta \epsilon)(1-\epsilon - \Delta \epsilon) - \epsilon(1-\epsilon)  \right] \sigma T_0^4 \\
\Delta \text{OLR}_1 &= \left[(\epsilon + \Delta \epsilon) - \epsilon \right] \sigma T_1^4
\end{align*}
$$

Now expand this out, but to make things easier to deal with, neglect term in $\Delta \epsilon^2$ (very small - we will be considering changes of less than 10% in $\epsilon$):

$$
\begin{align*}
\Delta \text{OLR}_s &\approx (\Delta \epsilon) \left[ -2(1-\epsilon) \right] \sigma T_s^4  \\
\Delta \text{OLR}_0 &\approx (\Delta \epsilon) (1 - 2 \epsilon) \sigma T_0^4 \\
\Delta \text{OLR}_1 &\approx (\Delta \epsilon) \sigma T_1^4
\end{align*}
$$

Now look at the **sign** of each term. Recall that $0 < \epsilon < 1$. **Which terms in the OLR go up and which go down?**

**THIS IS VERY IMPORTANT, SO STOP AND THINK ABOUT IT.**

The contribution from the **surface** must **decrease**, while the contribution from the **top layer** must **increase**.

**When we add absorbers, the average level of emission goes up!**

____________

## 6. Radiative forcing in the 2-layer grey gas model
____________

### Definition of Radiative Forcing

We now define a very important quantity:

**"Radiative forcing" is the change in total radiative flux at TOA after adding absorbers**

In this model, **only the longwave flux can change**, so we calculate the radiative forcing as

$$ R = - \Delta \text{OLR} $$

(with the minus sign so that $R$ is **positive when the climate system is gaining extra energy**).

### Connection between radiative forcing and level of emission

We just worked out that whenever we **add some extra absorbers**, the **emissions to space** (on average) will originate from **higher levels** in the atmosphere.

What does this mean for OLR? Will it increase or decrease?

To get the answer, we just have to sum up the three contributions we wrote above:

$$
\begin{align*}
R &= -\Delta \text{OLR}_s - \Delta \text{OLR}_0 - \Delta \text{OLR}_1  \\
  &= -\Delta \epsilon \left[  -2(1-\epsilon) \sigma T_s^4 + (1 - 2 \epsilon) \sigma T_0^4  + \sigma T_1^4 \right]
\end{align*}
$$

Is this a positive or negative number? The key point is this:

**It depends on the temperatures, i.e. on the lapse rate.**

### Greenhouse effect for an isothermal atmosphere

Stop and think about this question:

If the **surface and atmosphere are all at the same temperature**, does the OLR go up or down when $\epsilon$ increases (i.e. we add more absorbers)?

Understanding this question is key to understanding how the greenhouse effect works.

#### Let's solve the isothermal case

We will just set $T_s = T_0 = T_1$ in the above expression for the radiative forcing.

What do you get?

```python
## Work out on paper
```

#### The answer is $R=0$

For an isothermal atmosphere, there is **no change** in OLR when we add extra greenhouse absorbers. Hence, no radiative forcing and no greenhouse effect.

Why?

The level of emission still must go up. But since the temperature at the upper level is the **same** as everywhere else, the emissions are exactly the same.

### The radiative forcing (change in OLR) depends on the lapse rate!

For a more realistic example of radiative forcing due to an increase in greenhouse absorbers, we use our observed temperatures and the tuned value for $\epsilon$.

We'll express the answer in W m$^{-2}$ for a 2% increase in $\epsilon$:

$$ \Delta \epsilon = 0.02 \times 0.58 $$

```python
epsilon = 0.586041150248834
delta_epsilon = 0.02 * epsilon
delta_epsilon
```

Calculate the three components of the radiative forcing:

```python
sigma = 5.67E-8
Ts = 288.
T0 = 275.
T1 = 230.
```

```python
#  Component originating from the surface
Rs = -delta_epsilon * (-2*(1-epsilon)*sigma * Ts**4)
Rs
```

```python
#  Component originating from level 0
R0 = ...## you compute this component from the above expression
print(R0)
```

```python
#  Component originating from level 1
R1 = ## and this one
print(R1)
```

So just add them up to get the total radiative forcing:

```python
R = Rs + R0 + R1
R
```

So in our example, **the OLR decreases by 2.6 W m$^{-2}$**, or equivalently, the **radiative forcing is +2.6 W m$^{-2}$.**

What we have just calculated is this:

**Given the observed lapse rates, a small increase in absorbers will cause a small decrease in OLR.**

The **greenhouse effect** thus gets **stronger**, and energy will begin to accumulate in the system -- which will eventually **cause temperatures to increase** as the system adjusts to a new equilibrium.

____________

## 7. Summary
____________

### Key physical lessons

- Putting a **layer of longwave absorbers** above the surface keeps the **surface substantially warmer**, because of the **backradiation** from the atmosphere (greenhouse effect).
- The **grey gas** model assumes that each layer absorbs and emits a fraction $\epsilon$ of its blackbody value, independent of wavelength.

- With **incomplete absorption** ($\epsilon < 1$), there are contributions to the OLR from every level and the surface (there is no single **level of radiative emission**)
- Adding more absorbers means that **contributions to the OLR** from **upper levels** go **up**, while contributions from the surface go **down**.

- The **radiative forcing** caused by an increase in absorbers **depends on the lapse rate** (decrease in temperature with altitude in the atmosphere).
- For an **isothermal atmosphere** the radiative forcing is zero and there is **no greenhouse effect**
- The radiative forcing is positive for our atmosphere **because tropospheric temperatures tend to decrease with height**.

____________

## Credits

This notebook is based on a chapter 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 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

```