# Mock exam paper - solutions

## Question 1

### (a) **definition of a Normal form game**:

An \\(N\\) player normal form game consists of:

- A finite set of $N$ players
- Strategy spaces for the players: $\{S_1,S_2,S_3,\dots,S_N\}$;
- Payoff functions for the players: $u_i:S_1\times S_2\dots\times S_N\to\mathbb{R}$

[2]

### (b) **utilities of strategies**

(i) $$\sigma_r A \sigma_c = 1\qquad \sigma_r B \sigma_c = 1$$ [1]

(ii) $$\sigma_r A \sigma_c = 3/2\qquad \sigma_r B \sigma_c = 5/3$$ [1]

(iii) $$\sigma_r A \sigma_c = 7/4\qquad \sigma_r B \sigma_c = 5/2$$ [1]

Some code to verify:

```python
import numpy as np
A, B = np.array([[3, 1], [0, 2]]), np.array([[2, 1], [0, 3]])
strategies = [(np.array([1, 0]), np.array([0, 1])),
              (np.array([1/2, 1/2]), np.array([1/3, 2/3])),
              (np.array([1/4, 3/4]), np.array([0, 1]))]
for sigma_r, sigma_c in strategies:
    print(np.dot(sigma_r, np.dot(A, sigma_c)), np.dot(sigma_r, np.dot(B, sigma_c)))
```

    1 1
    1.5 1.6666666666666665
    1.75 2.5

### (c)  **define LH**

For a nondegenerate 2 player game $(A, B)\in{\mathbb{R}^{m\times n}_{>0}}^2$ the following algorithm returns all nash equilibria:

1. Start at the artificial equilibrium: $(0, 0)$ [1]
2. Choose a label to drop. [1]
3. Remove this label from the corresponding vertex by traversing an edge of the corresponding polytope to another vertex. [1]
4. The new vertex will now have a duplicate label in the other polytope. Remove this label from the vertex of the other polytope and traverse an edge of that polytope to another vertex. [1]
5. Repeat step 4 until the pair of vertices is fully labelled. [1]

### (d) **obtain best response polytopes**

We start by scaling $A, B$:

$$
A \to A + 1 = \begin{pmatrix}4&2\\1&3\end{pmatrix} \qquad B \to B + 1 =\begin{pmatrix}3&2\\1&4\end{pmatrix}
$$

The row player best response polytope $\mathcal{P}$ is defined by $x\geq0, xB\leq 1$:

$$
x_1 \geq 0\\
x_2 \geq 0\\
3x_1+x_2 \leq 1\\
2x_1+4x_2 \leq 1
$$

which corresponds to:

$$
x_1 \geq 0\\
x_2 \geq 0\\
x_2 \leq 1 - 3x_1\\
x_2 \leq 1/4-1/2x_1
$$

[1]

The vertices (and their corresponding labels) are then given by:

- $a=(0, 0)$ with labels: $\{0, 1\}$
- $b=(1/3, 0)$ with labels: $\{1, 2\}$
- $c=(0, 1/4)$ with labels: $\{0, 3\}$
- $d=(3/10, 1/10)$ with labels: $\{2, 3\}$

[1]

The column player best response polytope $\mathcal{Q}$ is defined by $Ax\leq 1, x\geq0$:

$$
4x_1+2x_2 \leq 1\\
x_1+3x_2 \leq 1\\
x_1 \geq 0\\
x_2 \geq 0\\
$$

which corresponds to:

$$
x_2 \leq 1/2 - 2x_1\\
x_2 \leq 1/3-1/3x_1\\
x_1 \geq 0\\
x_2 \geq 0
$$

[1]

The vertices (and their corresponding labels) are then given by:

- $w=(0, 0)$ with labels: $\{2, 3\}$
- $x=(1/4, 0)$ with labels: $\{0, 3\}$
- $y=(0, 1/3)$ with labels: $\{1, 2\}$
- $z=(1/10, 3/10)$ with labels: $\{0, 1\}$

[1]

### (e) Drawing the best response polytope

[1] mark for each polytope.

```python
%matplotlib inline

import matplotlib.pyplot as plt
import numpy as np
import scipy.spatial

V = [np.array([0, 0]), np.array([1 / 3, 0]), np.array([0, 1 / 4]), np.array([3/10, 1/10])]
P = scipy.spatial.ConvexHull(V)
scipy.spatial.convex_hull_plot_2d(P)
plt.title("$\mathcal{P}$")
plt.text(0.001, .1, "Label: 0")
plt.text(0.15, .005, "Label: 1")
plt.text(0.15, .18, "Label: 3")
plt.text(0.26, .05, "Label: 2")
for v, s in zip(V, "abcd"):
    plt.text(v[0] + 0.001, v[1] + 0.001, s);
```

```python
V = [np.array([0, 0]), np.array([1 / 4, 0]), np.array([0, 1 / 3]), np.array([1/10, 3/10])]
Q = scipy.spatial.ConvexHull(V)
scipy.spatial.convex_hull_plot_2d(Q)
plt.title("$\mathcal{Q}$")
plt.text(0.001, .15, "Label: 2")
plt.text(0.10, .005, "Label: 3")
plt.text(0.05, .32, "Label: 1")
plt.text(0.2, .1, "Label: 0")
for v, s in zip(V, "wxyz"):
    plt.text(v[0] + 0.001, v[1] + 0.001, s);
```

### (f) Carrying out the LH algorithm

Using the plot we can carry out the Lemke-Howson algorithm:

- Dropping label 0:

  - $(a, w)$ have labels $\{0, 1\}, \{2, 3\}$. Drop 0.
  - $\to (b, w)$ have labels $\{1, 2\}, \{2, 3\}$. In $\mathcal{Q}$ drop 2.
  - $\to (b, x)$ have labels $\{1, 2\}, \{0, 3\}$. Fully labeled vertex pair.

- Dropping label 1:

  - $(a, w)$ have labels $\{0, 1\}, \{2, 3\}$. Drop 1.
  - $\to (c, w)$ have labels $\{0, 3\}, \{2, 3\}$. In $\mathcal{Q}$ drop 3.
  - $\to (c, y)$ have labels $\{0, 3\}, \{1, 2\}$. Fully labeled vertex pair.

- Dropping label 2:

  - $(a, w)$ have labels $\{0, 1\}, \{2, 3\}$. Drop 2.
  - $\to (a, x)$ have labels $\{0, 1\}, \{0, 3\}$. In $\mathcal{P}$ drop 0.
  - $\to (b, x)$ have labels $\{1, 2\}, \{0, 3\}$. Fully labeled vertex pair.

- Dropping label 3:

  - $(a, w)$ have labels $\{0, 1\}, \{2, 3\}$. Drop 3.
  - $\to (a, y)$ have labels $\{0, 1\}, \{1, 2\}$. In $\mathcal{P}$ drop 1.
  - $\to (c, y)$ have labels $\{0, 3\}, \{1, 2\}$. Fully labeled vertex pair.

  [2]

We see that we have obtained two equilibria:

$$(b, x) = ((1/3, 0), (1/4, 0))$$

$$(c, y) = ((0, 1/4), (0, 1/3))$$

which gives the following two Nash equilibria:

$$((1, 0), (1, 0))$$

$$((0, 1), (0, 1))$$

[2]

Some code to verify:

```python
import nashpy as nash
A, B = np.array([[3, 1], [0, 2]]), np.array([[2, 1], [0, 3]])
game = nash.Game(A, B)
for label, eq in enumerate(game.lemke_howson_enumeration()):
    print(label, eq)
```

    0 (array([1., 0.]), array([1., 0.]))
    1 (array([0., 1.]), array([0., 1.]))
    2 (array([1., 0.]), array([1., 0.]))
    3 (array([0., 1.]), array([0., 1.]))

### (g)  **Using a different initial vertex pair**

- $(b, x)$ has labels $\{1, 2\}, \{0, 3\}$. Drop 3.
- $\to (b, z)$ have labels $\{1, 2\}, \{0, 1\}$. In $\mathcal{P}$ drop 1.
- $\to (d, y)$ have labels $\{2, 3\}, \{0, 1\}$. Fully labeled vertex pair.

[1]

This gives the Nash equilibria:

$$
((3/4, 1/4), (1/4, 3/4))
$$

some code to verify the result:

```python
list(game.vertex_enumeration())
```

    [(array([1., 0.]), array([1., 0.])),
     (array([0., 1.]), array([0., 1.])),
     (array([0.75, 0.25]), array([0.25, 0.75]))]

### (h) Sketch of proof

- We know that there is a path between $(0, 0)$ and a fully labelled vertex pair. [1]
- Similarly, from a fully labelled vertex pair we can assume that it's possible to drop another fully labelled vertex pairs. [1]
- We can construct a graph of pairs of fully labelled vertex pairs. [1]
- As we have pairs this corresponds to an even number of fully labelled vertex pairs. Removing $(0, 0)$ this implies there is an odd number of Nash equilibria. [1]

## Question 2

### (a) Definition of a Prisoner's Dilemma (bookwork)

$$
A =
\begin{pmatrix}
    R & S\\
    T & P
\end{pmatrix}\qquad
B =
\begin{pmatrix}
    R & T\\
    S & P
\end{pmatrix}
$$

with the following constraints:

$$T > R > P > S$$
$$2R > T + S$$

[2]

### (b) Finding valid Prisoner's Dilemmas

(i)

For $A, B$ to be valid we need:

$$
\begin{pmatrix}
    3 & S\\
    5 & 1
\end{pmatrix}
=
\begin{pmatrix}
    R & S\\
    T & P
\end{pmatrix}
$$

which gives: $R=3, T=5, P=1$

further more:

$$
\begin{pmatrix}
    3 & T\\
    -1 & 1
\end{pmatrix}
=
\begin{pmatrix}
    R & T\\
    S & P
\end{pmatrix}
$$

[2]

which gives: $R=3, S=-1, P=1$

Thus we have (R, S, P, T) = (3, -1, 1, 5) which also follows the two required inequalities:

$$T > R > P > S \Leftrightarrow 5 > 3 > 1 > -1$$
$$2R > T + S \Leftrightarrow 6 > 4$$

[2]

(ii)

For $A, B$ to be valid we need:

$$
\begin{pmatrix}
    2 & S\\
    -2 & 1
\end{pmatrix}
=
\begin{pmatrix}
    R & S\\
    T & P
\end{pmatrix}
$$

which gives: $R=2, T=-2, P=1$

We immediately see that $R > T$ so this cannot be a Prisoner's Dilemma.

[4]

### (c) Markov chain representation of a reactive player match

$$
M = \begin{pmatrix}
3/10 & 3/10 & 1/5 & 1/5\\
3/8 & 3/8 & 1/8 & 1/8\\
3/20 & 9/20 & 1/10 & 3/10\\
3/16 & 9/16 & 1/16 & 3/16\\
\end{pmatrix}
$$

### (d) Expected utility (bookwork)

The first player:

$$s_1s_2\times R +  s1(1-s_2) \times S +  (1-s_1)s_2 \times T + (1-s_1)(1-s_2)\times P$$

The second player:

$$s_1s_2\times R +  s1(1-s_2) \times T +  (1-s_1)s_2 \times S + (1-s_1)(1-s_2)\times P$$

where:

$$
s_1 = \frac{q_2r_1+p_2}{1-r_1r_2}\qquad s_2 = \frac{p_2r_2+q_2}{1-r_1r_2}
$$

for:

$$r_1=p_1-p_2\qquad r_2=q_1-q_2$$

### (e) Expected utility for specific type of player

We have:

$$r_1=x-x/2=x/2\qquad r_2=1/2-1/4=1/4$$

thus

$$
s_1 = \frac{1/4x/2+x/2}{1-x/8}=\frac{5x}{8-x}\qquad s_2 = \frac{1/4x/2+1/4}{1-x/8}=\frac{x+2}{8-x}
$$

Direct substitution gives:

$$\frac{5x(x+2)}{(8-x)^2}\times R
  +  \frac{5x(6-2x)}{(8-x)^2} \times S
  +  \frac{(8-6x)(x+2)}{(8-x)^2} \times T
  +  \frac{(8-6x)(6-2x)}{(8-x)^2}\times P$$

expanding:

$$\frac{R\times(5x^2+10x) + S\times (35x-10x^2) + T\times (-6x^2+16-4x) + P\times (48-52x+12x^2)}{(8-x)^2}$$

substituting $(R, S, T, P)=(3, 0, 4, 1)$:

$$\frac{3x^2 - 38x + 112}{(8-x)^2}$$

factorising the $(8-x)$ term gives:

$$\frac{(8-x)(14-3x)}{(8-x)^2}=
\frac{\left(3 x-14\right)}{\left(x - 8\right)}
$$

Some code to verify the calculations:

```python
import sympy as sym
x, R, S, T, P = sym.symbols("x, R, S, T, P")

expr = R * (5 * x ** 2 + 10 * x) + S * (35 * x - 10 * x ** 2) + T * (-6 * x ** 2 + 16 - 4 * x) + P *(48 - 52 * x + 12 * x ** 2)
```

```python
expr.subs({R:3, S:0, T:4, P:1}).factor()
```

    (x - 8)*(3*x - 14)

```python
import numpy as np

def theoretic_steady_state(p, q):
    r_1 = p[0] - p[1]
    r_2 = q[0] - q[1]
    s_1 = (q[1] * r_1 + p[1]) / (1 - r_1 * r_2)
    s_2 = (p[1] * r_2 + q[1]) / (1 - r_1 * r_2)
    return np.array([s_1 * s_2, s_1 * (1 - s_2), (1 - s_1) * s_2, (1 - s_1) * (1 - s_2)])

def theoretic_utility(p, q, rstp=np.array([3, 0, 5, 1])):
    pi = theoretic_steady_state(p, q)
    return np.dot(pi, rstp)
```

```python
p = np.array([x, x / 2])
q = np.array([sym.S(1) / 2, sym.S(1) / 4])
expr = theoretic_utility(p=p, q=q, rstp=np.array([3, 0, 4, 1]))
sym.factor(expr)
```

    (3*x - 14)/(x - 8)

Here is a plot of the utility (calculated both ways)

**Note that this is not requested in the question: just here to help understanding.**

```python
xs = np.linspace(0, 1, 25)
ys = [theoretic_utility(p=[x, x / 2], q=q, rstp=np.array([3, 0, 4, 1])) for x in xs]
simplified_ys = [(3 * x - 14) / (x - 8) for x in xs]
plt.plot(xs, ys, label="$u(x)$")
plt.scatter(xs, simplified_ys, label="$\\frac{3x-14}{x-8}$")
plt.legend();
```

### (f) Identifying the optimal behaviour

Direct differentiating gives:

$$
\frac{3x - 24 - (3x - 14)}{\left(x - 8\right)^2}=\frac{-10}{\left(x - 8\right)^{2}}
$$

[2]

simplifying gives the required result.

We see that our function is decreasing for all values of $x$ (it has negative derivative). Thus, the optimal value of $x$ is $0$.

[2]

```python
sym.factor(expr.diff(x))
```

    -10/(x - 8)**2

## Question 3

### (a) Defining equations (bookwork)

The matrix $A$ correspods to the utility of a row player in a game where the row player is a given individual and the column player is the population.

This gives:

$$f_1=ax_1+bx_2\qquad f_2=cx_1+dx_2$$

[1]

or equivalently:

$$f=Ax\qquad \phi=fx$$

thus we have the same equation as before but in matrix notation:

$$\frac{dx}{dt}=x(f-\phi)$$

[1]

### (b) Defining mutated population (bookwork)

Given a strategy vector $x=(x_1, x_2)$, some $\epsilon>0$ and another strategy $y=(y_1, y_1)$, the post entry population $x_{\epsilon}$ is given by:

$$
x_{\epsilon} = (x_1 + \epsilon(y_1 - x_1), x_2 + \epsilon(y_2 - x_2))
$$

[2]

### (c) Defining an evolutionary stable strategy (bookwork)

Given a stable population distribution, $x$ it represents an **Evolutionary Stable Strategy** (ESS) if and only if there exists $\bar\epsilon>0$:

$$u(x, x_{\epsilon})>u(y, x_{\epsilon})\text{ for all }0<\epsilon<\bar\epsilon, y$$

[1]

where $u(x, y)$ corresponds to the fitness of strategy $x$ in population $y$ which is given by:

$$xAy^T$$

[1]

### (d) Proof of result for ESS (bookwork)

**Theorem:**

If $x$ is an ESS, then for all $y\ne x$, either:

1. $u(x,x)>u(y,x)$
2. $u(x,x)=u(y,x)$ and $u(x,y)>u(y,y)$
---

Conversely, if either (1) or (2) holds for all $y\ne x$ then $x$ is an ESS.

[2]

---

**Proof:**

---

If $x$ is an ESS, then by definition:

$$u(x,x_{\epsilon})>u(y,x_{\epsilon})$$

which corresponds to:

$$(1-\epsilon)u(x,x)+\epsilon u(x,y)>(1-\epsilon)u(y,x)+\epsilon u(y,y)$$

- If condition 1 of the theorem holds then the above inequality can be satisfied for \\(\epsilon\\) sufficiently small. If condition 2 holds then the inequality is satisfied.
- Conversely:

    - If $u(x,x) < u(y,x)$ then we can find $\epsilon$ sufficiently small such that the inequality is violated. [1]

    - If $u(x, x) = u(y,x)$ and $u(x,y) \leq u(y,y)$ then the inequality is violated. [1]

### (e) Obtain all ESS for a 2 by 2 game

First step is to identify the Nash equilibria. Identify best responses for the associated two player game $(A, A^T)$:

$$
A=\begin{pmatrix}
1&\underline{4}\\
\underline{2}&1
\end{pmatrix}\qquad
A^T=\begin{pmatrix}
1&\underline{2}\\
\underline{4}&1
\end{pmatrix}
$$

[1]

This immediately gives 2 pure Nash equilibria, however they are not symmetrix so are not:

$$
(\sigma_r, \sigma_c) \in \{((1, 0), (0, 1)), ((0, 1), (1, 0))\}
$$

To find the remaining Nash equilibria, we use the support enumeration algorithm which gives:

$${\sigma_r}_1  + 4{\sigma_r}_2 = 2{\sigma_r}_1  + {\sigma_r}_2$$
$${\sigma_c}_1  + 4{\sigma_c}_2 = 2{\sigma_c}_1  + {\sigma_c}_2$$

This gives:

$$3{\sigma_r}_2  = {\sigma_r}_1$$

which gives, a final Nash equilibrium of:

$$x = (3/4, 1/4)$$

[2]

some code to verify this:

```python
A = np.array([[1, 4], [2, 1]])
game = nash.Game(A, A.transpose())
list(game.support_enumeration())
```

    [(array([1., 0.]), array([0., 1.])),
     (array([0., 1.]), array([1., 0.])),
     (array([0.75, 0.25]), array([0.75, 0.25]))]

Considering this final equilibria, we have:

$$u(x, x)=u(y, x)\text{ for all }y$$

Applying the theorem we thus consider:

$$
u(x, y)=(5/4)y_1 + (13/4)y_2=(5/4)y_1 + (13/4)(1-y_1)=-2y_1+13/4
$$

\begin{align}
u(y, y)&=y_1^2+4y_1y_2+2y_1y_2+y_2^2\\
       &=y_1^2+6y_1-6y_1^2+1 - 2y_1 + y_1^2\\
       &=1+4y_1-4y_1^2
\end{align}

Thus:

$$u(x, y) - u(y, y) = 4y_1^2 - 6y_1+9/4 = 4(y_1 - 3/4)^2$$

[2]

however $y_1\ne3/4$ thus $x=(3/4, 1/4)$ is an ESS.

### (f) Discussing the research paper

- (i) This paper looks at GT and cancer: it is a proof of concept sitting in literature that already exists on the subject. The main result is confirming intuitive understanding of proliferation/motility. Increased nutrients implies less motility. This is done in two ways: a theoretic game and a simulation. [3]
- (ii) The game matrix should in fact be: $\begin{pmatrix}b/2 & b - c\\ b & b - c / 2\end{pmatrix}$ [2]
- (iii) The theorem is used in the paper, although it is not done explicitly it is used to implictely describe the stability of the equilibria. [2]
- (iv) Another approach would be to use a Moran process. The main difference with the approach in the paper would be to use a Moran process. This would correspond to considering a finite population size as opposed to the infinite population model used. This would correspond more closely to the simulation model used in the paper. [3]

## Question 4

### (a) Definition of a Moran process on a game

Consider a matrix $A\in\mathbb{R}^{m\times n}$ representing a game with two strategies.

$$
A=
\begin{pmatrix}
    a & b\\
    c & d
\end{pmatrix}
$$

The Moran process is as follows:

- At a given time step: all individuals play all other individuals.
- Obtain their fitness as given by the game.
- Randomly select an individual proportional to their fitness as an individual to be reproduced
- Uniformly select an individual to be replaced
- Proceed to the next time step.
- The process terminates when there is only one type of individual in the population.

### (b) Theorem

**Theorem: Fixation probabilities for the birth death process**

Given a birth death process as defined above, the fixation probability $x_i$ is given by:

$$x_i=\frac{1+\sum_{j=1}^{i-1}\prod_{k=1}^j\gamma_k}{1+\sum_{j=1}^{N-1}\prod_{k=1}^j\gamma_k}$$

where:

$$
\gamma_k = \frac{p_{k,k-1}}{p_{k,k+1}}
$$

**Proof**

We have:

\begin{align}
    p_{i,i+1}x_{i+1} & = -p_{i,i-1}x_{i-1} + x_i(1 - p_{ii}) \\
    p_{i,i+1}x_{i+1} & = p_{i,i-1}(x_{i} - x_{i-1}) + x_ip_{i,i+1} \\
    x_{i+1} - x_i    & = \frac{p_{i, i-1}}{p_{i, i+1}}(x_i-x_{i-1})=\gamma_i(x_i-x_{i-1})
\end{align}

We observe that:

\begin{align}
    x_2 - x_1 &= \gamma_1(x_1-x_{0})=\gamma_1x_1\\
    x_3 - x_2 &= \gamma_2(x_2-x_1)=\gamma_2\gamma_1x_1\\
    x_4 - x_3 &= \gamma_3(x_3-x_2)=\gamma_3\gamma_2\gamma_1x_1\\
              &\; \vdots & \\
    x_{i+1} - x_i &= \gamma_i(x_i-x_{i-1})=\prod_{k=1}^i\gamma_kx_1\\
               &\; \vdots & \\
    x_{N} - x_{N-1} &= \gamma_{N-1}(x_{N-1}-x_{N-2})=\prod_{k=1}^{N-1}\gamma_kx_1\\
\end{align}

thus we have:

$$x_i=\sum_{j=0}^{i-1}x_{j+1}-x_j=\left(1+\sum_{j=1}^{i-1}\prod_{k=1}^j\gamma_k\right)x_1$$

we complete the proof by solving the following equation to obtain $x_1$:

$$x_N=1=\left(1+\sum_{j=1}^{N-1}\prod_{k=1}^j\gamma_k\right)x_1$$

### (c) Moran process for the game

Assuming $i$ individuals of the first type, for this game we have $N=5$ and $(a, b, c, d)=(4, 1, 6, 2)$ the fitness of both types is given respectively by:

$$f_{1i}=\frac{a(i-1)+b(N-i)}{N-1}=\frac{4i-4+5-i}{4}=\frac{3i+1}{4}$$
$$f_{2i}=\frac{c(i)+d(N-i-1)}{N-1}=\frac{6i+2(4-i)}{4}=\frac{4i+8}{4}=i$$

which gives:

$$\gamma_i=\frac{f_{2i}}{f_{1i}}=\frac{4i+8}{3i+1}$$

thus:

$$
x_1=\frac{1}{1+\sum_{j=1}^{4}\prod_{k=1}^j\frac{4k+8}{3k+1}}=\frac{1}{1+3+3(16/7)+3(16/7)2+3(16/7)2(24/13)}=\frac{91}{4540}
$$

$$
x_{4}=\frac{1+\sum_{j=1}^{3}\prod_{k=1}^j\frac{4k+8}{3k+1}}{1+\sum_{j=1}^{4}\prod_{k=1}^j\frac{4k+8}{3k+1}}=\frac{1+3+3(16/7)+3(16/7)2}{1+3+3(16/7)+3(16/7)2+3(16/7)2(24/13)}=\frac{559}{1135}
$$

Thus the two fixation probabilities $x_1$ are:

- Cooperation: $\frac{91}{4540}\approx 0.02$
- Defection $1-\frac{559}{1135}=\frac{576}{1135}\approx 0.51$

Some code to verify the result:

```python
def theoretic_fixation(N, game, i=1):
    """
    Calculate x_i as given by the above formula
    """
    f_ones = np.array([(game[0, 0] * (i - 1) + game[0, 1] * (N - i)) / (N - 1) for i in range(1, N)])
    f_twos = np.array([(game[1, 0] * i + game[1, 1] * (N - i - 1)) / (N - 1) for i in range(1, N)])
    gammas = f_twos / f_ones
    return (1 + np.sum(np.cumprod(gammas[:i-1]))) / (1 + np.sum(np.cumprod(gammas)))
```

```python
import sympy as sym
game = np.array([[sym.S(4), sym.S(1)], [sym.S(6), sym.S(2)]])
theoretic_fixation(N=5, game=game), 1- theoretic_fixation(N=5, game=game, i=4)
```

    (91/4540, 576/1135)

```python
[float(x) for x in _]
```

    [0.020044052863436124, 0.507488986784141]

### (d) Game for TfT vs Alternator

The history of 5 turns would give between TfT and Alternator

1. TfT: C Alternator: C with scores: (4, 4)
2. TfT: C Alternator: D with scores: (1, 6)
3. TfT: D Alternator: C with scores: (6, 1)
4. TfT: C Alternator: D with scores: (1, 6)
5. TfT: D Alternator: C with scores: (6, 1)

The history of 5 turns would give between TfT and TfT

1. TfT: C TfT: C with scores: (4, 4)
2. TfT: C TfT: C with scores: (4, 4)
3. TfT: C TfT: C with scores: (4, 4)
4. TfT: C TfT: C with scores: (4, 4)
5. TfT: C TfT: C with scores: (4, 4)

The history of 5 turns between Alternator and Alternator:

1. Alternator: C Alternator: C with scores: (4, 4)
2. Alternator: D Alternator: D with scores: (2, 2)
3. Alternator: C Alternator: C with scores: (4, 4)
4. Alternator: D Alternator: D with scores: (2, 2)
5. Alternator: C Alternator: C with scores: (4, 4)

Summing these, the payoff matrix is thus given by:

$$
\begin{pmatrix}
20 & 18 \\
18 & 16\\
\end{pmatrix}
$$

### (e) Moran calculations

Repeating the previous calculations:

$$f_{1i}=\frac{a(i-1)+b(N-i)}{N-1}=\frac{20(i - 1) + 18(5-i)}{4}=\frac{2i+70}{4}$$
$$f_{2i}=\frac{c(i)+d(N-i-1)}{N-1}=\frac{18i+16(4-i)}{4}=\frac{2i+64}{4}=i$$

which gives:

$$\gamma_i=\frac{f_{2i}}{f_{1i}}=\frac{2i+64}{2i+70}$$

thus:

$$
x_1=\frac{1}{1+\sum_{j=1}^{4}\prod_{k=1}^j\frac{2k+64}{2k+70}}=\frac{1}{1+11/12+(11/12)(34/37)+(11/12)(34/37)(35/38)+(11/12)(34/37)(35/38)(12/13)}=\frac{247}{1050}
$$

$$
x_4=\frac{1+\sum_{j=1}^{3}\prod_{k=1}^j\frac{2k+64}{2k+70}}{1+\sum_{j=1}^{4}\prod_{k=1}^j\frac{2k+64}{2k+70}}=\frac{1+11/12+(11/12)(34/37)+(11/12)(34/37)(35/38)}{1+11/12+(11/12)(34/37)+(11/12)(34/37)(35/38)+(11/12)(34/37)(35/38)(12/13)}=\frac{923}{1110}
$$

Thus the two fixation probabilities $x_1$ are:

- TfT: $\frac{247}{1050}\approx 0.24$
- Alternator $1-\frac{923}{1110}=\frac{187}{1110}\approx 0.17$

Some code to verify the result:

```python
game = np.array([[sym.S(20), sym.S(18)], [sym.S(18), sym.S(16)]])
theoretic_fixation(N=5, game=game), 1 - theoretic_fixation(N=5, game=game, i=4)
```

    (247/1050, 187/1110)

```python
[float(x) for x in _]
```

    [0.23523809523809525, 0.16846846846846847]


# BUILDING AN EVALUATION TESTBED

This notebook illustrates how to construct closed loop systems for ``NonlinearIOSystem`` elements,
including noise and disturbances.

# Preliminaries

```python
!pip install -q control
!pip install -q tellurium
!pip install -q controlSBML

import collections
import control
import controlSBML as ctl
from IPython.display import HTML, Math
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import sympy
import tellurium as te
```

```python
TIMES = ctl.makeSimulationTimes(0, 5, 500)
```

# Antimony Model

```python
# Constants
CONSTANT_DCT = {"k1": 1, "k2": 2, "k3": 3, "k4": 4}
s = sympy.Symbol("s")
REF = 10
```

```python
MODEL = """
$S1 -> S2; k1*$S1
J1: S2 -> S3; k2*S2
J2: S3 -> S2; k3*S3
J3: S2 -> ; k4*S2

k1 = 1
k2 = 2
k3 = 3
k4 = 4
$S1 = 10
S2 = 0
S3 = 0
S4 = 0
"""
RR = te.loada(MODEL)
RR.simulate()
RR.plot()
```

# Helpers

```python
def plotStep(sys, is_plot=True):
    """
    Plots a transfer function step response

    Parameters
    ----------
    sys: control.TransferFunction/control.NonlinearIOSystem

    Returns
    control.Timeresponse
    """
    if "TransferFunction" in str(type(sys)):
        result = control.forced_response(sys, T=TIMES, U=1)
    elif "NonlinearIOSystem" in str(type(sys)):
        result = control.input_output_response(sys, T=TIMES, U=1)
    if is_plot:
        plt.plot(TIMES, result.outputs.flatten())
    return result

# TESTS
Gs = control.TransferFunction([2], [1, 3])
_ = plotStep(Gs, is_plot=True)
ctlsb = ctl.ControlSBML(MODEL, input_names=["S2"], output_names=["S3"])
ctlsb.set(CONSTANT_DCT)
sys = ctlsb.makeNonlinearIOSystem("sys")
result = plotStep(sys, is_plot=True)
print("OK!")
```

```python
def pp(obj):
    """
    Does a LaTex rendering of an object with a latex representation.

    Parameters
    ----------
    obj: object
    """
    display(Math(str(obj)))

# Tests
pp(r"e^{\alpha}")
```

$\displaystyle e^{\alpha}$

# Closed Loop Transfer Functions

**Transfer Functions**
\begin{eqnarray}
H_{RY}(s) & = & \frac{Y(s)}{R(s)} & = & \frac{C(s) G(s)}{1 + C(s) G(s) F(s)} \\
H_{RE}(s) & = & \frac{E(s)}{R(s)} & = & \frac{1}{1 + C(s) G(s) F(s)} \\
H_{NY}(s) & = & \frac{Y(s)}{N(s)} & = & -\frac{ F(s)}{1 + C(s) G(s) F(s)} \\
H_{DY}(s) & = & \frac{Y(s)}{D(s)} & = & \frac{ C(s)}{1 + C(s) G(s) F(s)} \\
\end{eqnarray}

# Elements of an Evaluation Testbed

## System Under Control

```python
ctlsb = ctl.ControlSBML(MODEL, input_names=["S2"], output_names=["S3"])
SYSTEM = ctlsb.makeNonlinearIOSystem("system")
```

```python
result = control.input_output_response(SYSTEM, T=TIMES, U=1)
plt.plot(result.t, result.y.flatten())
```

## Noise and Disturbanes

```python
def makeSinusoidSystem(name, amp, frequency):
    """
    Construct a NonlinearIOSystem for a sinusoid.
    The system has output "out".

    Parameters
    ----------
    name: str
    amp: float
    frequency: float

    Returns
    -------
    NonlinearIOSystem
    """
    def outfcn(time, _, __, ___):
        """
        Creates a sine wave at the frequency specified.

        Parameters
        ----------
        time: float
        """
        return amp*np.sin(time*frequency)
    #
    return control.NonlinearIOSystem(
        None, outfcn, outputs=['out'], inputs=[],
        name=name)

# Tests
sys = makeSinusoidSystem("sine", 10, 20)
result = control.input_output_response(sys, T=TIMES)
assert(len(result.y) > 0)
print("OK!")
```

    OK!

```python
noise = makeSinusoidSystem("noise", 1.5, 7)
result = control.input_output_response(noise, T=TIMES)
plt.plot(result.t, result.y.flatten())
```

## Controller

```python
def makeController(name, kp=2):
    """
    Creates a proportional controller as a NonlinearIOSystem with input "in" and output "out".

    Parameters
    ----------
    name: str
        Name of the system
    kp: float

    Returns
    -------
    control.NonlinearIOSystem
    """
    def outfcn(_, __, u, ___):
        # u: float (error signal)
        if not "len" in dir(u):
            u = [u]
        return kp*(u[0])
    #
    return control.NonlinearIOSystem(
        None, outfcn, inputs=['in'], outputs=['out'],
        name=name)

# Tests
kp = 2
controller = makeController("controller", kp=kp)
times = [0, 1, 2, 3, 4]
result = control.input_output_response(controller, T=times, U=times)
trues = [r == kp*( t) for t, r in zip(result.t, result.outputs)]
assert(all(trues))
print("OK!")
```

    OK!

## Using ``IOSystemFactor``

``ControlSBML`` has capabilities for constructing various ``IOSystem`` objects in ``IOSystemFactory``.

```python
help(ctl.IOSystemFactory)
```

    Help on class IOSystemFactory in module controlSBML.iosystem_factory:

    class IOSystemFactory(builtins.object)
     |  Methods defined here:
     |
     |  __init__(self)
     |      Initialize self.  See help(type(self)) for accurate signature.
     |
     |  makeAdder(self, name, num_input=2)
     |      Inputs two or more elements. Outputs their sum. Name is "sum".
     |      The inputs are "in1", "in2", ...
     |      The output is "out".
     |
     |      Parameters
     |      ----------
     |      name: str
     |
     |      Returns
     |      -------
     |      NonlinearIOSystem
     |
     |  makeConstant(self, name, constant)
     |      Outputs a constant value.
     |
     |      Parameters
     |      ----------
     |      name: str
     |      constant: float
     |
     |      Returns
     |      -------
     |      NonlinearIOSystem
     |
     |  makeFilter(self, name, constant)
     |      Construct a NonlinearIOSystem for x' = a*x + u, where u is the
     |      filter input.
     |      The system has input "in" and output "out".
     |      The output is normalized so that the DC gain of the filter is 1.
     |
     |      Parameters
     |      ----------
     |      name: str
     |      constant: float
     |          e**expo_constant*time
     |
     |      Returns
     |      -------
     |      NonlinearIOSystem
     |
     |  makeMultiplier(self, name, factor)
     |      Outputs a multiple of the input signal.
     |
     |      Parameters
     |      ----------
     |      name: str
     |      factor: float
     |
     |      Returns
     |      -------
     |      NonlinearIOSystem
     |
     |  makePIDController(self, name, kp=2, ki=0, kd=0)
     |      Creates a PID controller.
     |      NonlinearIOSystem with input "in" and output "out".
     |      States are:
     |          culmulative_err: cumulative control error
     |          last_err: last control error
     |
     |      Parameters
     |      ----------
     |      name: str
     |          Name of the system
     |      kp: float
     |         proportional control constant
     |      ki: float
     |         integral control constant
     |      kd: float
     |         differential control constant.
     |
     |      Returns
     |      -------
     |      control.NonlinearIOSystem
     |
     |  makePassthru(self, name)
     |      Makes a pass through system that outputs its input.
     |
     |      Parameters
     |      ----------
     |      name: str
     |
     |      Returns
     |      -------
     |      NonlinearIOSystem
     |
     |  makeSinusoid(self, name, amp, frequency)
     |      Construct a NonlinearIOSystem that outputs a sinusoid.
     |      The system has output "out".
     |
     |      Parameters
     |      ----------
     |      name: str
     |      amp: float
     |      frequency: float
     |
     |      Returns
     |      -------
     |      NonlinearIOSystem
     |
     |  ----------------------------------------------------------------------
     |  Data descriptors defined here:
     |
     |  __dict__
     |      dictionary for instance variables (if defined)
     |
     |  __weakref__
     |      list of weak references to the object (if defined)

**Factories**

  - makeFilter: creates an exponential filter
  - makeConstant: outputs a constant value
  - makeAdder: creates an IOSystem that outputs the sum of the inputs
  - makePassthur: creates an IOSystem that outputs the inpu
  - makePIDController: creates a PID controller
  - makeSinusoid: creates an IOSystem that outputs a sinusoid

```python
FACTORY = ctl.IOSystemFactory()
controller1 = FACTORY.makePIDController("controller", kp=2)
times = [0, 1, 2, 3, 4]
result1 = control.input_output_response(controller1, T=times, U=times)
plt.scatter(result.y.flatten(), result1.y.flatten())
```

# Building a Testbed

## Create the IOSystem Elements

* $R(s)$: Reference input
* $D(s)$: disturbance
* $N(s)$: noise signal

* $C(s)$: controller
* $G(s)$: system
* $F(s)$: filter

* Summantion of $R(s)$, filter output
* Summantion of $U(s)$, $D(s)$
* Summation of $N(s)$, $Y(s)$

### Solution

```python
ref = 3

# Create the elements of the feedback loop
reference = FACTORY.makeMultiplier("reference", ref)
noise = FACTORY.makeSinusoid("noise", 0.01, 20)
disturbance = FACTORY.makeSinusoid("disturbance", 5, 2)

ctlsb = ctl.ControlSBML(MODEL, input_names=["S2"], output_names=["S3"])
system = ctlsb.makeNonlinearIOSystem("system")
controller = FACTORY.makePIDController("controller", kp=30)
fltr = FACTORY.makeFilter("fltr", -100)

sum_Y_N = FACTORY.makeAdder("sum_Y_N")
sum_U_D = FACTORY.makeAdder("sum_U_D")
sum_R_F = FACTORY.makeAdder("sum_R_F")
```

## Connect the Elements

```python
# Create the closed loop system
closed_loop = control.interconnect(
  [reference, noise, disturbance, sum_Y_N, sum_R_F, sum_U_D, system, fltr, controller ],
  connections=[
    ['controller.in', 'sum_R_F.out'],    # e(t)
    ['sum_U_D.in1', 'controller.out'],   # u(t)
    ['sum_U_D.in2', 'disturbance.out'],  # d(t)

  ],
  inplist=["reference.in"],
  outlist = [?],
)
```

### Solution

```python
# Create the closed loop system
closed_loop = control.interconnect(
  [reference, noise, disturbance, sum_Y_N, sum_R_F, sum_U_D, system, fltr, controller ],
  connections=[
    ['controller.in', 'sum_R_F.out'],    # e(t)
    ['sum_U_D.in1', 'controller.out'],   # u(t)
    ['sum_U_D.in2', 'disturbance.out'],  # d(t)
    ['system.S2',   'sum_U_D.out'],
    ['sum_Y_N.in1', 'system.S3'],        # y(t)
    ['sum_Y_N.in2', 'noise.out'],        # n(t)
    ['fltr.in',     'sum_Y_N.out'],
    ['sum_R_F.in1', '-fltr.out'],
    ['sum_R_F.in2', 'reference.out'],
  ],
  inplist=["reference.in"],
  outlist=["disturbance.out", "sum_Y_N.out", 'sum_R_F.out', 'system.S3'],
)
```

```python
type(closed_loop)
```

    control.iosys.InterconnectedSystem

## Simulate

Plot the response of the closed loop system to a unit step response.

### Solution

```python
times = ctl.makeSimulationTimes(0, 20, 500)
result = control.input_output_response(closed_loop, T=times, U=1)
plt.plot(result.t, result.outputs[0].flatten())
plt.plot(result.t, result.outputs[1].flatten())
plt.ylim([-5, 5])
legends = ["disturbance", "output"]
plt.legend(legends)
```

The output is biased! We wanted $S_3$ to be 10, but it's 7.6.


Lambda School Data Science

*Unit 2, Sprint 1, Module 2*

---

# Regression 2
- Do train/test split
- Use scikit-learn to fit a multiple regression
- Understand how ordinary least squares regression minimizes the sum of squared errors
- Define overfitting/underfitting and the bias/variance tradeoff

### Setup

Run the code cell below. You can work locally (follow the [local setup instructions](https://lambdaschool.github.io/ds/unit2/local/)) or on Colab.

Libraries:
- matplotlib
- numpy
- pandas
- plotly
- scikit-learn

```python
# Importing libraries
import pandas as pd
import numpy as np
import sys
import warnings
from sklearn.metrics import mean_absolute_error
import matplotlib.pyplot as plt
```

```python

# If you're on Colab:
if 'google.colab' in sys.modules:
    DATA_PATH = 'https://raw.githubusercontent.com/LambdaSchool/DS-Unit-2-Applied-Modeling/master/data/'

# If you're working locally:
else:
    DATA_PATH = '../data/'

# Ignore this Numpy warning when using Plotly Express:
# FutureWarning: Method .ptp is deprecated and will be removed in a future version. Use numpy.ptp instead.
warnings.filterwarnings(action='ignore', category=FutureWarning, module='numpy')
```

```python
#Clean up the data

def wrangle(filepath):
  columns = ['year','incumbent','challenger','income','fatalities','incumbent_vote_share']
  df = pd.read_csv(filepath,names = columns,header = 0,index_col='year',) #skiprows = 1
  df.drop(columns = ['incumbent','challenger'],inplace=True)
  return df

df = wrangle(DATA_PATH + 'elections/bread_peace_voting.csv')

df.head()
```

      income
      fatalities
      incumbent_vote_share

      year

      1952
      2.40
      190
      44.60

      1956
      2.89
      0
      57.76

      1960
      0.85
      0
      49.91

      1964
      4.21
      1
      61.34

      1968
      3.02
      146
      49.60

# Do train/test split

```python
target = 'incumbent_vote_share'
y = df[target]
X = df.drop(columns = target)
```

```python
print('X shape:',X.shape,'\ny shape:',y.shape)
```

    X shape: (17, 2)
    y shape: (17,)

Your model should 'GENERALIZE' well

## Overview

### Predict Elections! 🇺🇸🗳

How could we try to predict the 2020 US Presidential election?

According to Douglas Hibbs, a political science and economics professor, you can [explain elections with just two features, "Bread and Peace":](https://douglas-hibbs.com/background-information-on-bread-and-peace-voting-in-us-presidential-elections/)

> Aggregate two-party vote shares going to candidates of the party holding the presidency during the postwar era are well explained by just two fundamental determinants:
>
> (1) Positively by weighted-average growth of per capita real disposable personal income over the term.
> (2) Negatively by cumulative US military fatalities (scaled to population) owing to unprovoked, hostile deployments of American armed forces in foreign wars.

Let's look at the data that Hibbs collected and analyzed:

Data Sources & Definitions

- 1952-2012: Douglas Hibbs, [2014 lecture at Deakin University Melbourne](http://www.douglas-hibbs.com/HibbsArticles/HIBBS-PRESVOTE-SLIDES-MELBOURNE-Part1-2014-02-26.pdf), Slide 40
- 2016, Vote Share: [The American Presidency Project](https://www.presidency.ucsb.edu/statistics/elections)
- 2016, Recent Growth in Personal Incomes: [The 2016 election economy: the "Bread and Peace" model final forecast](https://angrybearblog.com/2016/11/the-2016-election-economy-the-bread-and-peace-model-final-forecast.html)
- 2016, US Military Fatalities: Assumption that Afghanistan War fatalities in 2012-16 occured at the same rate as 2008-12

> Fatalities denotes the cumulative number of American military fatalities per millions of US population the in Korea, Vietnam, Iraq and Afghanistan wars during the presidential terms preceding the 1952, 1964, 1968, 1976 and 2004, 2008 and 2012 elections. —[Hibbs](http://www.douglas-hibbs.com/HibbsArticles/HIBBS-PRESVOTE-SLIDES-MELBOURNE-Part1-2014-02-26.pdf), Slide 33

Here we have data from the 1952-2016 elections. We could make a model to predict 1952-2016 election outcomes — but do we really care about that?

No, not really. We already know what happened, we don't need to predict it.

This is explained in [_An Introduction to Statistical Learning_](http://faculty.marshall.usc.edu/gareth-james/ISL/), Chapter 2.2, Assessing Model Accuracy:

> In general, we do not really care how well the method works training on the training data. Rather, _we are interested in the accuracy of the predictions that we obtain when we apply our method to previously unseen test data._ Why is this what we care about?
>
> Suppose that we are interested in developing an algorithm to predict a stock’s price based on previous stock returns. We can train the method using stock returns from the past 6 months. But we don’t really care how well our method predicts last week’s stock price. We instead care about how well it will predict tomorrow’s price or next month’s price.
>
> On a similar note, suppose that we have clinical measurements (e.g. weight, blood pressure, height, age, family history of disease) for a number of patients, as well as information about whether each patient has diabetes. We can use these patients to train a statistical learning method to predict risk of diabetes based on clinical measurements. In practice, we want this method to accurately predict diabetes risk for _future patients_ based on their clinical measurements. We are not very interested in whether or not the method accurately predicts diabetes risk for patients used to train the model, since we already know which of those patients have diabetes.

So, we're really interested in the 2020 election — but we probably don't want to wait until then to evaluate our model.

There is a way we can estimate now how well our model will generalize in the future. We can't fast-forward time, but we can rewind it...

We can split our data in **two sets.** For example:
1. **Train** a model on elections before 2008.
2. **Test** the model on 2008, 2012, 2016.

This "backtesting" helps us estimate how well the model will predict the next elections going forward, starting in 2020.

This is explained in [_Forecasting,_ Chapter 3.4,](https://otexts.com/fpp2/accuracy.html) Evaluating forecast accuracy:

> The accuracy of forecasts can only be determined by considering how well a model performs on new data that were not used when fitting the model.
>
>When choosing models, it is common practice to separate the available data into two portions, training and test data, where the training data is used to estimate any parameters of a forecasting method and the test data is used to evaluate its accuracy. Because the test data is not used in determining the forecasts, it should provide a reliable indication of how well the model is likely to forecast on new data.
>
>
>
>The size of the test set is typically about 20% of the total sample, although this value depends on how long the sample is and how far ahead you want to forecast. The following points should be noted.
>
>- A model which fits the training data well will not necessarily forecast well.
>- A perfect fit can always be obtained by using a model with enough parameters.
>- Over-fitting a model to data is just as bad as failing to identify a systematic pattern in the data.
>
>Some references describe the test set as the “hold-out set” because these data are “held out” of the data used for fitting. Other references call the training set the “in-sample data” and the test set the “out-of-sample data”. We prefer to use “training data” and “test data” in this book.

**How should we split: Randomly? Before/after a given date?**

I recommend you all read a great blog post, [How (and why) to create a good validation set](https://www.fast.ai/2017/11/13/validation-sets/), by fast.ai cofounder Rachel Thomas.

She gives great examples to answer the question “When is a random subset not good enough?” I’m not as opposed to random splits as Rachel Thomas seems to be. But it’s worth thinking about the trade-offs!

Time-based and random splits can both be useful, and you’ll get repeated hands-on practice with both during this unit! (She also talks about the distinction between validation & test sets, which we’ll introduce in the last lesson of this Sprint.)

#Mikayla's Notes--------------------------

Training data and testing data

80% reserved for training
20% reserved for testing

For this specific issue we need to use the 80% of years and then the last 20% of years

For other data sets we need to consider seasonality

## Follow Along

Split the data in two sets:
1. Train on elections before 2008.
2. Test on 2008 and after.

**First** we need to split our **target vector** from our **feature matrix**.

target vector aka - y-variable, response variable, dependent variable

feature matrix - matrix of x-variables

**Second** we need to split our dataset into **training** and **test** sets.

Two strategies:

- Random train-test split using [`train_test_split`](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html). Generally we use 80% of the data for training, and 20% of the data for testing.
- If you have **timeseries**, then you need to do a "cutoff" split.

```python
cuttoff = X.index <2008

y_train = y.loc[cuttoff]
y_test = y.loc[~cuttoff]

X_train = X.loc[cuttoff]
X_test = X.loc[~cuttoff]

print(X_train)
```

          income  fatalities
    year
    1952    2.40         190
    1956    2.89           0
    1960    0.85           0
    1964    4.21           1
    1968    3.02         146
    1972    3.62           0
    1976    1.08           2
    1980   -0.39           0
    1984    3.86           0
    1988    2.27           0
    1992    0.38           0
    1996    1.04           0
    2000    2.36           0
    2004    1.72           4

Note that this volume of data is at least two orders of magnitude smaller than we usually want to work with for predictive modeling.

There are other validation techniques that could be used here, such as [time series cross-validation](https://scikit-learn.org/stable/modules/cross_validation.html#time-series-split), or [leave-one-out cross validation](https://scikit-learn.org/stable/modules/cross_validation.html#leave-one-out-loo) for small datasets. However, for this module, let's start simpler, with train/test split.

Using a tiny dataset is intentional here. It's good for learning because we can see all the data at once.

## Challenge

In your assignment, you will do train/test split, based on date.

# Use scikit-learn to fit a multiple regression

## Overview

We've done train/test split, and we're ready to fit a model.

We'll proceed in 3 steps. The first 2 are review from the previous module. The 3rd is new.

- Begin with baselines (0 features)
- Simple regression (1 feature)
- Multiple regression (2 features)

## Follow Along

### Begin with baselines (0 features)

What was the average Incumbent Party Vote Share, in the 1952-2004 elections?

```python
baseline_pred = y_train.mean()
```

What if we guessed this number for every election? How far off would this be on average?

```python
y_base_pred = [baseline_pred] * len(y_train)
baseline_mae = mean_absolute_error(y_train,y_base_pred)
baseline_mae
```

    4.846938775510204

```python
y_test_pred = [baseline_pred]*len(y_test)
baseline_mae_test = mean_absolute_error(y_test,y_test_pred)
baseline_mae_test
```

    3.6285714285714215

### Simple regression (1 feature)

Make a scatterplot of the relationship between 1 feature and the target.

We'll use an economic feature: Average Recent Growth in Personal Incomes. ("Bread")

1952 & 1968 are outliers: The incumbent party got fewer votes than predicted by the regression. What do you think could explain those years? We'll come back to this soon, but first...

Use scikit-learn to fit the simple regression with one feature.

Follow the [5 step process](https://jakevdp.github.io/PythonDataScienceHandbook/05.02-introducing-scikit-learn.html#Basics-of-the-API), and refer to [Scikit-Learn LinearRegression documentation](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html).

How does the error compare to the baseline?

```python
plt.scatter(df['fatalities'], df['incumbent_vote_share'])
plt.xlabel('Military Fatalities [per Million]')
plt.ylabel('Incumbent Party Vote Share [%]')
```

```python
plt.scatter(df['income'], df['incumbent_vote_share'])
plt.xlabel('Military Fatalities [per Million]')
plt.ylabel('Incumbent Party Vote Share [%]')
```

```python
from sklearn.linear_model import LinearRegression
model_1 = LinearRegression()
X1_train = X_train[['income']]
X1_test = X_test[['income']]
model_1.fit(X1_train,y_train)
y1_pred = model_1.predict(X1_train)
```

```python
mae_model1 = mean_absolute_error(y_train,y1_pred)
mae_model1
```

    2.645479454315596

```python
y1_pred_test = model_1.predict(X1_test)
mae_model1_test = mean_absolute_error(y_test,y1_pred_test)
mae_model1_test
```

    1.7998717748375863

### Multiple regression (2 features)

Make a scatterplot of the relationship between 2 features and the target.

We'll add another feature: US Military Fatalities per Million. ("Peace" or the lack thereof.)

Rotate the scatterplot to explore the data. What's different about 1952 & 1968?

```python
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = Axes3D(fig)

x = df['income']
z = df['incumbent_vote_share']
y = df['fatalities']

ax.set_xlabel("Income")
ax.set_ylabel("incumbent vote share")
ax.set_zlabel("fatalities")

ax.scatter(x, y, z)

plt.show()
```

Use scikit-learn to fit a multiple regression with two features.

```python
X2_train = X_train[['income','fatalities']]
X2_test = X_test[['income','fatalities']]
model_2 = LinearRegression()
model_2.fit(X2_train,y_train)
y2_pred = model_2.predict(X2_train)
```

```python
mae_model2 = mean_absolute_error(y_train,y2_pred)
mae_model2
```

    1.3303221006874268

```python
y2_pred_test = model_2.predict(X2_test)
mae_model2_test = mean_absolute_error(y_test, y2_pred_test)
mae_model2_test
```

    1.6341722692537293

How does the error compare to the prior model?

### Plot the plane of best fit

For a regression with 1 feature, we plotted the line of best fit in 2D.

(There are many ways to do this. Plotly Express's `scatter` function makes it convenient with its `trendline='ols'` parameter.)

For a regression with 2 features, we can plot the plane of best fit in 3D!

(Plotly Express has a `scatter_3d` function but it won't plot the plane of best fit for us. But, we can write our own function, with the same "function signature" as the Plotly Express API.)

Where are 1952 & 1968 in relation to the plane? Which elections are the biggest outliers now?

Roll over points on the plane to see predicted incumbent party vote share (z axis), dependent on personal income growth (x axis) and military fatatlies per capita (y axis).

### Get and interpret coefficients

During the previous module, we got the simple regression's coefficient and intercept. We plugged these numbers into an equation for the line of best fit, in slope-intercept form: $y = mx + b$

Let's review this objective, but now for multiple regression.

What's the equation for the plane of best fit?

$y = \beta_0 + \beta_1x_1 + \beta_2x_2$

Can you relate the intercept and coefficients to what you see in the plot above?

## Challenge

In your assignment, you'll fit a Linear Regression with at least 2 features.

# Understand how ordinary least squares regression minimizes the sum of squared errors

## Overview

So far, we've evaluated our models by their absolute error. It's an intuitive metric for regression problems.

However, ordinary least squares doesn't directly minimize absolute error. Instead, it minimizes squared error.

In this section, we'll introduce two new regression metrics:

- Squared error
- $R^2$

We'll demostrate two possible methods to minimize squared error:

- Guess & check
- Linear Algebra

## Follow Along

Notice that $R^2$ is exactly zero.

[$R^2$ represents the proportion of the variance for a dependent variable that is explained by the independent variable(s).](https://en.wikipedia.org/wiki/Coefficient_of_determination)

The mean baseline uses zero independent variables and explains none of the variance in the dependent variable, so its $R^2$ score is zero.

The highest possible $R^2$ score is 1. The lowest possible *Train* $R^2$ score with ordinary least squares regression is 0.

In this demo, it's possible to get a negative Train $R^2$, if you manually set values of m & b that are worse than the mean baseline. But that wouldn't happen in the real world.

However, in the real world, it _is_ possible to get a negative *Test/Validation* $R^2$. It means that your *Test/Validation* predictions are worse than if you'd constantly predicted the mean of the *Test/Validation* set.

---

Now that we've visualized the squared errors for the mean baseline, let's guess & check some better values for the m & b parameters:

You can run the function repeatedly, with different values for m & b.

How do you interpret each metric you see?

- Mean Squared Error
- Root Mean Squared Error
- Mean Absolute Error
- $R^2$

Does guess & check really get used in machine learning? Sometimes! Some complex functions are hard to minimize, so we use a sophisticated form of guess & check called "gradient descent", which you'll learn about in Unit 4.

Fortunately, we don't need to use guess & check for ordinary least squares regression. We have a solution, using linear algebra!

### Linear Algebra

The same result that is found by minimizing the sum of the squared errors can be also found through a linear algebra process known as the "Least Squares Solution:"

\begin{align}
\hat{\beta} = (X^{T}X)^{-1}X^{T}y
\end{align}

Before we can work with this equation in its linear algebra form we have to understand how to set up the matrices that are involved in this equation.

#### The $\beta$ vector

The $\beta$ vector represents all the parameters that we are trying to estimate, our $y$ vector and $X$ matrix values are full of data from our dataset. The $\beta$ vector holds the variables that we are solving for: $\beta_0$ and $\beta_1$

Now that we have all of the necessary parts we can set them up in the following equation:

\begin{align}
y = X \beta + \epsilon
\end{align}

Since our $\epsilon$ value represents **random** error we can assume that it will equal zero on average.

\begin{align}
y = X \beta
\end{align}

The objective now is to isolate the $\beta$ matrix. We can do this by pre-multiplying both sides by "X transpose" $X^{T}$.

\begin{align}
X^{T}y =  X^{T}X \beta
\end{align}

Since anything times its transpose will result in a square matrix, if that matrix is then an invertible matrix, then we should be able to multiply both sides by its inverse to remove it from the right hand side. (We'll talk tomorrow about situations that could lead to $X^{T}X$ not being invertible.)

\begin{align}
(X^{T}X)^{-1}X^{T}y =  (X^{T}X)^{-1}X^{T}X \beta
\end{align}

Since any matrix multiplied by its inverse results in the identity matrix, and anything multiplied by the identity matrix is itself, we are left with only $\beta$ on the right hand side:

\begin{align}
(X^{T}X)^{-1}X^{T}y = \hat{\beta}
\end{align}

We will now call it "beta hat" $\hat{\beta}$ because it now represents our estimated values for $\beta_0$ and $\beta_1$

#### Lets calculate our $\beta$ parameters with numpy!

# Define overfitting/underfitting and the bias/variance tradeoff

## Overview

Read [_Python Data Science Handbook,_ Chapter 5.3](https://jakevdp.github.io/PythonDataScienceHandbook/05.03-hyperparameters-and-model-validation.html#The-Bias-variance-trade-off). Jake VanderPlas explains overfitting & underfitting:

> Fundamentally, the question of "the best model" is about finding a sweet spot in the tradeoff between bias and variance. Consider the following figure, which presents two regression fits to the same dataset:
>
>
>
> The model on the left attempts to find a straight-line fit through the data. Because the data are intrinsically more complicated than a straight line, the straight-line model will never be able to describe this dataset well. Such a model is said to _underfit_ the data: that is, it does not have enough model flexibility to suitably account for all the features in the data; another way of saying this is that the model has high _bias_.
>
> The model on the right attempts to fit a high-order polynomial through the data. Here the model fit has enough flexibility to nearly perfectly account for the fine features in the data, but even though it very accurately describes the training data, its precise form seems to be more reflective of the particular noise properties of the data rather than the intrinsic properties of whatever process generated that data. Such a model is said to _overfit_ the data: that is, it has so much model flexibility that the model ends up accounting for random errors as well as the underlying data distribution; another way of saying this is that the model has high _variance_.

VanderPlas goes on to connect these concepts to the "bias/variance tradeoff":

> From the scores associated with these two models, we can make an observation that holds more generally:
>
>- For high-bias models, the performance of the model on the validation set is similar to the performance on the training set.
>
>- For high-variance models, the performance of the model on the validation set is far worse than the performance on the training set.
>
> If we imagine that we have some ability to tune the model complexity, we would expect the training score and validation score to behave as illustrated in the following figure:
>
>
>
> The diagram shown here is often called a validation curve, and we see the following essential features:
>
>- The training score is everywhere higher than the validation score. This is generally the case: the model will be a better fit to data it has seen than to data it has not seen.
>- For very low model complexity (a high-bias model), the training data is under-fit, which means that the model is a poor predictor both for the training data and for any previously unseen data.
>- For very high model complexity (a high-variance model), the training data is over-fit, which means that the model predicts the training data very well, but fails for any previously unseen data.
>- For some intermediate value, the validation curve has a maximum. This level of complexity indicates a suitable trade-off between bias and variance.
>
>The means of tuning the model complexity varies from model to model.

So far, our only "means of tuning the model complexity" has been selecting one feature or two features for our linear regression models. But we'll quickly start to select more features, and more complex models, with more "hyperparameters."

This is just a first introduction to underfitting & overfitting. We'll continue to learn about this topic all throughout this unit.

## Follow Along

Let's make our own Validation Curve, by tuning a new type of model complexity: polynomial degrees in a linear regression.

Go back to the the NYC Tribeca condo sales data

```python
# Read NYC Tribeca condo sales data, from first 4 months of 2019.
# Dataset has 90 rows, 9 columns.
df = pd.read_csv(DATA_PATH+'condos/tribeca.csv')
assert df.shape == (90, 9)

# Arrange X features matrix & y target vector
features = ['GROSS_SQUARE_FEET']
target = 'SALE_PRICE'
X = df[features]
y = df[target]
```

Do random [train/test split](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html)

```python
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=11)
```

Repeatedly fit increasingly complex models, and keep track of the scores

```python
from IPython.display import display, HTML
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures
import matplotlib.pyplot as plt
import numpy as np

# Credit for PolynomialRegression: Jake VanderPlas, Python Data Science Handbook, Chapter 5.3
# https://jakevdp.github.io/PythonDataScienceHandbook/05.03-hyperparameters-and-model-validation.html#Validation-curves-in-Scikit-Learn
def PolynomialRegression(degree=2, **kwargs):
    return make_pipeline(PolynomialFeatures(degree),
                         LinearRegression(**kwargs))

polynomial_degrees = range(1, 10, 2)
train_r2s = []
test_r2s = []

for degree in polynomial_degrees:
    model = PolynomialRegression(degree)
    display(HTML(f'Polynomial degree={degree}'))

    model.fit(X_train, y_train)
    train_r2 = model.score(X_train, y_train)
    test_r2 = model.score(X_test, y_test)
    display(HTML(f'<b style="color: blue">Train R2 {train_r2:.2f}</b>'))
    display(HTML(f'<b style="color: red">Test R2 {test_r2:.2f}</b>'))

    plt.scatter(X_train, y_train, color='blue', alpha=0.5)
    plt.scatter(X_test, y_test, color='red', alpha=0.5)
    plt.xlabel(features)
    plt.ylabel(target)

    x_domain = np.linspace(X.min(), X.max())
    curve = model.predict(x_domain)
    plt.plot(x_domain, curve, color='blue')
    plt.show()
    display(HTML('<hr/>'))

    train_r2s.append(train_r2)
    test_r2s.append(test_r2)

display(HTML('Validation Curve'))
plt.plot(polynomial_degrees, train_r2s, color='blue', label='Train')
plt.plot(polynomial_degrees, test_r2s, color='red', label='Test')
plt.xlabel('Model Complexity (Polynomial Degree)')
plt.ylabel('R^2 Score')
plt.legend()
plt.show()
```

As model complexity increases, what happens to Train $R^2$ and Test $R^2$?

# Review

In your assignment, you'll continue to **predict how much it costs to rent an apartment in NYC,** using the dataset from renthop.com.

- Do train/test split. Use data from April & May 2016 to train. Use data from June 2016 to test.
- Engineer at least two new features.
- Fit a linear regression model with at least two features.
- Get the model's coefficients and intercept.
- Get regression metrics RMSE, MAE, and $R^2$, for both the train and test sets.

You've been provided with a separate notebook for your assignment, which has all the instructions and stretch goals. What's the best test MAE you can get? Share your score and features used with your cohort on Slack!

# Sources

#### Train/Test Split
- James, Witten, Hastie, Tibshirani, [_An Introduction to Statistical Learning_](http://faculty.marshall.usc.edu/gareth-james/ISL/), Chapter 2.2, Assessing Model Accuracy
- Hyndman, Athanasopoulos, [_Forecasting,_ Chapter 3.4,](https://otexts.com/fpp2/accuracy.html) Evaluating forecast accuracy
- Rachel Thomas, [How (and why) to create a good validation set](https://www.fast.ai/2017/11/13/validation-sets/)

#### Bias-Variance Tradeoff
- Jake VanderPlas, [_Python Data Science Handbook,_ Chapter 5.3](https://jakevdp.github.io/PythonDataScienceHandbook/05.03-hyperparameters-and-model-validation.html#The-Bias-variance-trade-off), Hyperparameters and Model Validation
- StatQuest, [Machine Learning Fundamentals: Bias and Variance](https://youtu.be/EuBBz3bI-aA) (6.5 minutes)

#### "Bread and Peace" Background
- Douglas Hibbs, [Background Information on the ‘Bread and Peace’ Model of Voting in Postwar US Presidential Elections](https://douglas-hibbs.com/background-information-on-bread-and-peace-voting-in-us-presidential-elections/)
- Nate Silver, [What Do Economic Models Really Tell Us About Elections?](https://fivethirtyeight.com/features/what-do-economic-models-really-tell-us-about-elections/)

#### "Bread and Peace" Data Sources & Definitions
- 1952-2012: Douglas Hibbs, [2014 lecture at Deakin University Melbourne](http://www.douglas-hibbs.com/HibbsArticles/HIBBS-PRESVOTE-SLIDES-MELBOURNE-Part1-2014-02-26.pdf), Slide 40
- 2016, Vote Share: [The American Presidency Project](https://www.presidency.ucsb.edu/statistics/elections)
- 2016, Recent Growth in Personal Incomes: [The 2016 election economy: the "Bread and Peace" model final forecast](https://angrybearblog.com/2016/11/the-2016-election-economy-the-bread-and-peace-model-final-forecast.html)
- 2016, US Military Fatalities: Assumption that Afghanistan War fatalities in 2012-16 occured at the same rate as 2008-12

> Fatalities denotes the cumulative number of American military fatalities per millions of US population the in Korea, Vietnam, Iraq and Afghanistan wars during the presidential terms preceding the 1952, 1964, 1968, 1976 and 2004, 2008 and 2012 elections. —[Hibbs](http://www.douglas-hibbs.com/HibbsArticles/HIBBS-PRESVOTE-SLIDES-MELBOURNE-Part1-2014-02-26.pdf), Slide 33