```python
#Exercise 3:

! pip install sympy
! pip install numpy
! pip install scipy
! pip install matplotlib
! pip install random
! pip install pyswarm
import matplotlib.pyplot as plt
from scipy.optimize import minimize
import numpy as np
import time
import random as r
import sympy as sp
from sympy import symbols
from pyswarm import pso
```

    Requirement already satisfied: sympy in /opt/miniconda3/envs/conda_env/lib/python3.7/site-packages (1.6.2)
    Requirement already satisfied: mpmath>=0.19 in /opt/miniconda3/envs/conda_env/lib/python3.7/site-packages (from sympy) (1.1.0)
    Requirement already satisfied: numpy in /opt/miniconda3/envs/conda_env/lib/python3.7/site-packages (1.19.2)
    Requirement already satisfied: scipy in /opt/miniconda3/envs/conda_env/lib/python3.7/site-packages (1.5.2)
    Requirement already satisfied: numpy>=1.14.5 in /opt/miniconda3/envs/conda_env/lib/python3.7/site-packages (from scipy) (1.19.2)
    Requirement already satisfied: matplotlib in /opt/miniconda3/envs/conda_env/lib/python3.7/site-packages (3.3.2)
    Requirement already satisfied: kiwisolver>=1.0.1 in /opt/miniconda3/envs/conda_env/lib/python3.7/site-packages (from matplotlib) (1.2.0)
    Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.3 in /opt/miniconda3/envs/conda_env/lib/python3.7/site-packages (from matplotlib) (2.4.7)
    Requirement already satisfied: python-dateutil>=2.1 in /opt/miniconda3/envs/conda_env/lib/python3.7/site-packages (from matplotlib) (2.8.1)
    Requirement already satisfied: pillow>=6.2.0 in /opt/miniconda3/envs/conda_env/lib/python3.7/site-packages (from matplotlib) (8.0.0)
    Requirement already satisfied: numpy>=1.15 in /opt/miniconda3/envs/conda_env/lib/python3.7/site-packages (from matplotlib) (1.19.2)
    Requirement already satisfied: certifi>=2020.06.20 in /opt/miniconda3/envs/conda_env/lib/python3.7/site-packages (from matplotlib) (2020.6.20)
    Requirement already satisfied: cycler>=0.10 in /opt/miniconda3/envs/conda_env/lib/python3.7/site-packages (from matplotlib) (0.10.0)
    Requirement already satisfied: six>=1.5 in /opt/miniconda3/envs/conda_env/lib/python3.7/site-packages (from python-dateutil>=2.1->matplotlib) (1.15.0)
    ERROR: Could not find a version that satisfies the requirement random (from versions: none)
    ERROR: No matching distribution found for random
    Collecting pyswarm
      Downloading pyswarm-0.6.tar.gz (4.3 kB)
    Requirement already satisfied: numpy in /opt/miniconda3/envs/conda_env/lib/python3.7/site-packages (from pyswarm) (1.19.2)
    Building wheels for collected packages: pyswarm
      Building wheel for pyswarm (setup.py) ... done
      Created wheel for pyswarm: filename=pyswarm-0.6-py3-none-any.whl size=4480 sha256=25566f7fc3138ead70b748b0615324e749f6b5cf493f08d389b01be651cfbf3f
      Stored in directory: /home/scorpion_178/.cache/pip/wheels/ac/69/65/926e9c51b9fa99757cb43e8f6b74c5e6bb8b41a038b35c2db1
    Successfully built pyswarm
    Installing collected packages: pyswarm
    Successfully installed pyswarm-0.6

```python
#Case of Rosenbrock

save_rosenbrock=[]
def rosenbrock(X):  #cost function
    global save_rosenbrock
    global new
    Rosen=100*(X[1]-X[0]**2)**2 + (1-X[0])**2
    if not save_rosenbrock :
        save_rosenbrock.append(Rosen)
    if Rosen< save_rosenbrock[-1]:
        save_rosenbrock.append(Rosen)
    return Rosen

#Rosenbrock function is not constrained in this case but has bounds.

rose_ub=5*np.ones(2)
rose_lb=-rose_ub

#calling pso  and plotting results
#Setting parameters for plot

fig=plt.figure(figsize=(13,5))
ax1=fig.add_subplot(121)
ax1.set_title("Rosenbrock Optimization with PSO",fontweight="bold")
ax1.set_ylabel("values")
ax1.set_xlabel("Iterations")

for i in range(10):
    save_rosenbrock=[]
    new=[]
    start_time=time.time()
    xrosenbrock, frosenbrock = pso(rosenbrock, rose_lb, rose_ub, f_ieqcons=None,swarmsize=40,maxiter=100)
    print("Run",i +1)
    print(f"--- time used for function call in run {i+1} is %s seconds ---" % (time.time() - start_time))
    print("The optimal  point is:",xrosenbrock)
    print("The final Solution",frosenbrock)
    print("\n")

    #plot of Rosenbrock Optimisation
    ax1.plot(save_rosenbrock,marker='', label=f"Run {i+1}")
    ax1.plot(0,save_rosenbrock[0],marker='.', color=plt.gca().lines[-1].get_color())
    ax1.plot(len(save_rosenbrock)-1,save_rosenbrock[-1],marker='.',color=plt.gca().lines[-1].get_color())
    ax1.grid(color='lightgray', alpha=0.7)

    #plot of Rosenbrock Optimisation with log function
    ax2=fig.add_subplot(122)
    ax2.plot(save_rosenbrock,marker='', label=f"Run {i+1}")
    ax2.plot(0,save_rosenbrock[0],marker='.', color=plt.gca().lines[-1].get_color())
    ax2.plot(len(save_rosenbrock)-1,save_rosenbrock[-1],marker='.',color=plt.gca().lines[-1].get_color())
    ax2.grid(color='lightgray', alpha=0.5)
    ax2.set_title("Rosenbrock Optimization with PSO(logged)",fontweight="bold")
    ax2.set_ylabel(" logged Function values")
    ax2.set_xlabel("iterations")
    ax2.set_yscale('log')
    ax2.legend(loc="lower right",bbox_to_anchor=(1.35,-0.0),borderaxespad=0., ncol=1, title="Runs")

```

```python

```

```python

```

```python
#Case of Eggcrate

save_eggcrate=[]
def eggcrate(X:float):
    global save_eggcrate
    egg=X[0]**2 + X[1]**2 + 25*(np.sin(X[0])**2 + np.sin(X[1])**2)
    if not save_eggcrate :
        save_eggcrate.append(egg)
    if egg< save_eggcrate[-1]:
        save_eggcrate.append(egg)
    return egg
#Eggcrate function is not constrained in this case but has bounds.
egg_ub=2*np.pi*(np.ones(2))
egg_lb=-egg_ub

#calling pso  and plotting results
#Setting parameters for plot
fig=plt.figure(figsize=(13,5))

for i in range(10):
    save_eggcrate=[]
    start_time=time.time()
    xeggcrate,feggcrate=pso(eggcrate, egg_lb, egg_ub, f_ieqcons=None,swarmsize=30,minfunc=1e-8)
    print("Run",i +1)
    print(f"--- time used for function call in run {i+1} is %s seconds ---" % (time.time() - start_time))
    print("The optimal  point is:",xeggcrate)
    print("The final solution",feggcrate)
    print("\n")
    ax1=fig.add_subplot(121)
    ax1.plot(save_eggcrate,marker='', label=f"Run {i+1}")
    ax1.plot(0,save_eggcrate[0],marker='', color=plt.gca().lines[-1].get_color())
    ax1.plot(len(save_eggcrate)-1,save_eggcrate[-1],marker='.',color=plt.gca().lines[-1].get_color())
    ax1.grid(color='lightgray', alpha=0.5)
    ax1.set_title("Eggcrate Optimization with PSO",fontweight="bold")
    ax1.set_ylabel("values")
    ax1.set_xlabel("iterations")

   #plot of Eggcrate Optimisation with log function
    ax2=fig.add_subplot(122)
    ax2.plot(save_eggcrate,marker='', label=f"Run {i+1}")
    ax2.plot(0,save_eggcrate[0],marker='.', color=plt.gca().lines[-1].get_color())
    ax2.plot(len(save_eggcrate)-1,save_eggcrate[-1],marker='.',color=plt.gca().lines[-1].get_color())
    ax2.grid(color='lightgray', alpha=0.5)
    ax2.set_title("Eggcrate Optimization with PSO(log)",fontweight="bold")
    ax2.set_ylabel("values")
    ax2.set_xlabel("iterations")
    ax2.set_yscale('log')
    ax2.legend(loc="lower right",bbox_to_anchor=(1.40,-0.0),borderaxespad=0., ncol=1, title="Runs")

```

```python

```

```python

```

```python
#Case of Golinski

save_golinski=[]
def Golinski(X):
    Golin=0.7854*X[0]*X[1]**2*(3.3333*X[2]**2+14.9334*X[2]-43.0934)-1.5079*X[0]*(X[5]**2+X[6]**2)+7.477*(X[5]**3+X[6]**3)+0.7854*(X[3]*X[5]**2+X[4]*X[6]**2)
    global save_golinski
    if not save_golinski:
        save_golinski.append(Golin)
    if (Golin<save_golinski[-1] and np.all(np.array([cons1(X),cons2(X),cons3(X),cons4(X),cons5(X),cons6(X),cons7(X), cons8(X),cons9(X),cons10(X),cons11(X)]))>=0):
        save_golinski.append(Golin)
    if save_golinski[-1]<2994.355: #this will avoid recording values below optimal value found by PSO
        del save_golinski[-1]
    return Golin

#the following functions cons1 to cons11 are the constraints
def cons1(X):
    return 1-(27/(X[0]*X[1]**2*X[2]))
def cons2(X):
    return 1-(397.5/(X[0]*X[1]**2*X[2]**2))
def cons3(X):
    return 1-(1.93*X[3]**3/(X[1]*X[2]*X[5]**4))
def cons4(X):
    return 1-(1.93*X[4]**3/(X[1]*X[2]*X[6]**4))
def cons5(X):
    return 1-(np.sqrt((745*X[3]/(X[1]*X[2]))**2+16.9*10**6)/(110.0*X[5]**3))
def cons6(X):
    return 1-(np.sqrt((745*X[4]/(X[1]*X[2]))**2+157.5*10**6)/(85.0*X[6]**3))
def cons7(X):
    return 1-(X[1]*X[2]/40)
def cons8(X):
    return 1-(5*X[1]/X[0])
def cons9(X):
    return 1-(X[0]/(12*X[1]))
def cons10(X):
    return 1-((1.5*X[5]+1.9)/X[3])
def cons11(X):
    return 1-((1.1*X[6]+1.9)/X[4])
cons=({'type':'ineq',  'fun':  c1},
      {'type':'ineq',  'fun':  c2},
      {'type':'ineq',  'fun':  c3},
      {'type':'ineq',  'fun':  c4},
      {'type':'ineq',  'fun':  c5},
      {'type':'ineq',  'fun':  c6},
      {'type':'ineq',  'fun':  c7},
      {'type':'ineq',  'fun':  c8},
      {'type':'ineq',  'fun':  c9},
      {'type':'ineq',  'fun':  c10},
      {'type':'ineq',  'fun':  c11})

#the bounds for pso algorithm

golinski_ub=[3.6,0.8,28.0,8.3,8.3,3.9,5.9]
golinski_lb=[2.6,0.7,17.0,7.3,7.3,2.9,5.0]
fig=plt.figure(figsize=(16,7))
for i in range(10):
    save_golinski=[]
    print("Run",i +1)
    start_time=time.time()
    xgolinski,fgolinski=pso(Golinski,golinski_lb,golinski_ub,ieqcons=[c1,c2,c3,c4,c5,c6,c7,c8,c9,c10,c11],swarmsize=450,omega=0.6, phip=1.52,phig=1.52,maxiter=200,debug=False)
    print(f"--- time used for function call in run {i+1} is %s seconds ---" % (time.time() - start_time))
    print("The optimal  point is:",xgolinski)
    print(fgolinski)
    ax1=fig.add_subplot(121)
    ax1.plot(save_golinski,marker='', label=f"Run No{i+1}")
    ax1.plot(0,save_golinski[0],marker='', color=plt.gca().lines[-1].get_color())
    ax1.plot(len(save_golinski)-1,save_golinski[-1],marker='.',color=plt.gca().lines[-1].get_color())
    ax1.grid(color='lightgray', alpha=0.7)
    ax1.set_xlabel("Iterations")
    ax1.set_ylabel("Function Values")
    ax1.set_title("Golinski Function Optimisation with PSO",fontweight="bold")
    ax1.legend(loc="lower right",bbox_to_anchor=(1.5,-0.0),borderaxespad=0., ncol=1)

    ax2=fig.add_subplot(122)
    ax2.plot(save_golinski,marker='', label=f"Run {i+1}")
    ax2.plot(0,save_golinski[0],marker='.', color=plt.gca().lines[-1].get_color())
    ax2.plot(len(save_golinski)-1,save_golinski[-1],marker='.',color=plt.gca().lines[-1].get_color())
    ax2.grid(color='lightgray', alpha=0.5)
    ax2.set_title("Golinski Optimization with PSO(log)",fontweight="bold")
    ax2.set_ylabel(" logged Function values")
    ax2.set_xlabel("swarm evolution")
    ax2.set_yscale('log')
    ax2.legend(loc="lower right",bbox_to_anchor=(1.40,-0.0),borderaxespad=0., ncol=1, title="Runs")

```

```python

```


# Lithophane Library
written by Dirk Colbry

[Link to slides](https://docs.google.com/presentation/d/1s_8gcGfFDEHnqS7U-TkC4xp9T49fblb2_EWRpsd-v_I/edit#slide=id.g7d81a7112a_0_68)

In this notebook we will use some code that makes lithophanes out of images.

Lithophans work by changing the thickness of a material to correspond with how bright or dark a pixel is in an image. Dark pixels are thicker and light pixels are thiner.  This notebook describes steps to build a lithophane using python:

* [Step 1: Installing numpy-stl](#Installing_numpy-stl)
* [Step 2: Reading an image into python](#Reading_an_image_into_python)
* [Step 3: Flat Lithophane](#Flat_Lithophane)
* [Step 4: Cylinder Lithophane](#Cylinder_Lithophane)

---

# Step 1: Installing numpy-stl

First thing we need to do is install some functions that I have written to convert an image into an stl file.  I have stored these functions in a file named ```lithophane.py``` which should be in the same directory as this notebokk.   These functions also use a module called [numpy-stl](https://pypi.org/project/numpy-stl/) which can be installed using pip.

✅ **DO THIS:** Uncomment the following command (Delete the #) and run the cell using "shift-enter".   your own image and change the following code to display your image. Show the instructor when you get it working.

```python
!pip install numpy-stl
```

```python
import urllib
imageurl = 'https://insideindiana.images.worldnow.com/images/9914370_G.jpg?auto=webp&disable=upscale&height=560&fit=bounds'
urllib.request.urlretrieve('https://raw.githubusercontent.com/colbrydi/Lithophane/master/lithophane.py', 'lithophane.py')
urllib.request.urlretrieve(imageurl, 'image.jpg')
```

```python
import lithophane as li
```

**Note** The above import may give a warning.  This can be ignored for now.

---

# Step 2: Reading image data into python

There are many python libraries that can read an image.  In this example we will use a very common one called ```matplotlib```.  Run the following code by clicking on the cell and hitting "Shift and Enter" at the same time.

✅ **DO THIS:** Upload your own image and change the following code to display your image. UShow the instructor when you get it working.

```python
%matplotlib inline
import matplotlib.pylab as plt
import matplotlib.image as img
imagefile = './image.jpg'
im = img.imread(imagefile)
plt.imshow(im);
```

```python
from IPython.display import YouTubeVideo
YouTubeVideo("15aqFQQVBWU",width=640,height=360)
```

```python
%matplotlib inline
import matplotlib.pylab as plt
from ipywidgets import interact, fixed
import numpy as np

def showcolor(red,green,blue):
    plt.scatter(1,1, color=(red/255,green/255,blue/255), s=10000 );
    plt.axis('off');
    plt.show();

interact(showcolor, red=(0,255), green=(0,255), blue=(0,255));
```

We use the following fomula to convert the colors to a grayscale value.  An average would work but these look more "realistic":

$$gray = 0.2989r + 0.5870g + 0.1140b$$

```python
gray = li.rgb2gray(im)
plt.imshow(gray, cmap='gray');
plt. colorbar()
```

---

# Step 3: Generate Flat Lithophane

First we will start by creating a "point cloud" of three matrixes. The following function takes in a python image object (the one we created above is called ```im```) and returns the point cloud scaled to a width in millimeters.  The aspect ratio of the image will be maintained.

$$z = h\left(1 - \frac{p}{255}\right) + d$$

$$
\begin{align}
z &- \text{depth value for each pixel} \\
h &- \text{hight of the lithophan (thickness)} \\
p &- \text{pixel value (0-255)} \\
d &- \text{default depth}
\end{align}
$$

```python
#Generate x,y and z values for each pixel
width = 102 #Width in mm
x,y,z = li.jpg2stl(gray, width=width, h=3, d=0.6, show=False)
```

```python
plt.figure(figsize=(25,5))
plt.subplot(1,3,1)
plt.imshow(x)
plt.axis('off')
plt.title('x distances (mm)')
plt.colorbar()
plt.subplot(1,3,2)
plt.imshow(y)
plt.axis('off')
plt.title('y distances (mm)')
plt.colorbar()
plt.subplot(1,3,3)
plt.imshow(z)
plt.axis('off');
plt.title('z distances (mm)');
plt.colorbar()
```

```python
#plot a cross section to try and visualize data
plt.plot(x[550,0:100],z[550,0:100])
plt.axis('equal');

plt.ylabel('Lithophane Depth (z direction mm)')
plt.title('Lithophane cross-seciton');
```

The following takes our 3D points and creates a mesh model and saves the model as an STL file. A model is just a list of points (x,y,z) and a list of triangles which are just lists of points.

```python
model = li.makemesh(x,y,z);
filename=imagefile[:-4] + '_Flat.stl'
model.save(filename)
```

We can use the following function to visualize the stl file (note the z axis is _**NOT**_ to scale:

```python
#note z axis is not same scale as x and y axes.
li.showstl(x,y,z)
```

---

# Step 4: Cylinder Lithophane

Since we understand math, there is nothing that requires us to make lithophanes flat.  Consider the following example:

```python
from IPython.display import YouTubeVideo
YouTubeVideo("4I3ItcZOAjM",height=320, cc_load_policy=True)
```

To make lithophans cylinderical we just need to modify the x,y and z values.  The following code wraps x and z axis around the y axis and maintains the pixel depth described above.

```python
cx,cy,cz = li.makeCylinder(x,y,z)
li.showstl(cx,cy,cz)
```

We can save the new file using the same ```makemesh``` function from above.

```python
model = li.makemesh(cx,cy,cz);
filename=imagefile[:-4] + '_Cylinder.stl'
model.save(filename)
```

Here is a timelaps of a printed cylindrical stl file.

```python
from IPython.display import YouTubeVideo
YouTubeVideo("-h8pF6psdp4",height=320, cc_load_policy=True)
```

© Copyright 2019,  Dirk Colbry


# Deterministic Optimal Economic Growth Model

**Randall Romero Aguilar, PhD**

This demo is based on the original Matlab demo accompanying the  Computational Economics and Finance 2001 textbook by Mario Miranda and Paul Fackler.

Original (Matlab) CompEcon file: **demdp06.m**

Running this file requires the Python version of CompEcon. This can be installed with pip by running

    !pip install compecon --upgrade

Last updated: 2021-Oct-01

## About

Welfare maximizing social planner must decide how much society should consume and invest.  Model is of special interest because it has a known closed-form solution.

- States
    -     s       stock of wealth
- Actions
    -     k       capital investment
- Parameters
    -     beta    capital production elasticity
    -     delta   discount factor

```python
import numpy as np
import matplotlib.pyplot as plt
from compecon import BasisChebyshev, DPmodel, DPoptions, qnwnorm, demo
```

### Model parameters

Assuming that the marginal productivity of capital is $\beta=0.5$ and the discount factor is $\delta=0.9$

```python
β, δ = 0.5, 0.9
```

## Analytic results

The steady-state values for this model are

```python
sstar = (β * δ) ** (β / (1 - β))   # steady-state wealth
kstar = β * δ * sstar                    # steady-state capital investment
vstar = np.log(sstar - kstar) / (1 - δ)     # steady-state value
pstar = 1 / (sstar * (1 - β * δ))        # steady-state shadow price
b = 1 / (1 - δ * β)

print('\n\nSteady-State')
for var, value in zip(['Wealth','Investment','Value','Shadow price'], [sstar,kstar,vstar,pstar]):
    print(f'\t{var:12s} = {value:8.4f}')
```

    Steady-State
    	Wealth       =   0.4500
    	Investment   =   0.2025
    	Value        = -13.9634
    	Shadow price =   4.0404

The true value function is
\begin{equation}
V(s) = v^* + \frac{1}{1-\delta\beta}\left(\log(s) -\log(s^*)\right)
\end{equation}

```python
def vtrue(wealth): # analytic value function
    return vstar + b * (np.log(wealth) - np.log(sstar))
```

The true policy function is
\begin{equation}
k(s) = \delta\beta s
\end{equation}

```python
def ktrue(wealth): #analytic policy function
    return δ*β*wealth
```

## Numeric results

### State space
The state variable is s="Wealth", which we restrict to $0\in[0.2, 1.0]$.

Here, we represent it with a Chebyshev basis, with $n=15$ nodes.

```python
n, smin, smax = 15, 0.2, 1.0
basis = BasisChebyshev(n, smin, smax, labels=['Wealth'])
```

### Action space
The choice variable k="Investment" must be nonnegative.

```python
def bounds(s, i=None, j=None):
    return np.zeros_like(s), s[:]
```

### Reward function
The reward function is the utility of consumption=$s-k$.

```python
def reward(s, k, i=None, j=None):
    sk = s - k
    u = np.log(sk)
    ux= - sk ** -1
    uxx = - sk ** -2
    return u, ux, uxx
```

### State transition function
Next period, wealth will be equal to production from available initial capital $k$, that is $s' = k^\beta$

```python
def transition(s, k, i=None, j=None, in_=None, e=None):
    g = k ** β
    gx = β * k **(β - 1)
    gxx = (β - 1) * β * k ** (β - 2)
    return g, gx, gxx
```

### Model structure
The value of wealth $s$ satisfies the Bellman equation
\begin{equation*}
V(s) = \max_k\left\{\log(s-k) + \delta V(k^\beta)  \right\}
\end{equation*}

To solve and simulate this model,use the CompEcon class `DPmodel`

```python
growth_model = DPmodel(basis, reward, transition, bounds,
                       x=['Investment'],
                       discount=δ)
```

### Solving the model

Solving the growth model by collocation, using *Newton* algorithm and a maximum of 20 iterations

```python
options = dict(show=True,
               algorithm='newton',
               maxit=20)

snodes = growth_model.Value.nodes
S = growth_model.solve(vtrue(snodes), ktrue(snodes), **options)
```

    Solving infinite-horizon model collocation equation by Newton's method
    iter change       time
    ------------------------------
       0       4.7e-07    0.0050
       1       1.6e-12    0.0080
    Elapsed Time =    0.01 Seconds

`DPmodel.solve` returns a pandas `DataFrame` with the following data:

```python
S.head()
```

      Wealth
      value
      resid
      Investment

      Wealth

      0.200000
      0.200000
      -15.437865
      2.766030e-07
      0.090000

      0.205369
      0.205369
      -15.389699
      -2.099164e-07
      0.092416

      0.210738
      0.210738
      -15.342776
      -2.488242e-07
      0.094832

      0.216107
      0.216107
      -15.297033
      -1.102768e-07
      0.097248

      0.221477
      0.221477
      -15.252412
      5.644805e-08
      0.099664

We are also interested in the shadow price of wealth (the first derivative of the value function) and the approximation error.

To analyze the dynamics of the model, it also helps to compute the optimal change of wealth.

```python
S['shadow price'] = growth_model.Value(S['Wealth'],1)
S['error'] = S['value'] - vtrue(S['Wealth'])
S['D.Wealth'] = transition(S['Wealth'], S['Investment'])[0] - S['Wealth']
S.head()
```

      Wealth
      value
      resid
      Investment
      shadow price
      error
      D.Wealth

      Wealth

      0.200000
      0.200000
      -15.437865
      2.766030e-07
      0.090000
      9.090758
      4.241867e-07
      0.100000

      0.205369
      0.205369
      -15.389699
      -2.099164e-07
      0.092416
      8.853206
      -3.099038e-08
      0.098631

      0.210738
      0.210738
      -15.342776
      -2.488242e-07
      0.094832
      8.627699
      -4.010325e-08
      0.097210

      0.216107
      0.216107
      -15.297033
      -1.102768e-07
      0.097248
      8.413362
      1.253504e-07
      0.095739

      0.221477
      0.221477
      -15.252412
      5.644805e-08
      0.099664
      8.209399
      3.151235e-07
      0.094220

### Solving the model by Linear-Quadratic Approximation

The `DPmodel.lqapprox` solves the linear-quadratic approximation, in this case arround the steady-state. It returns a LQmodel which works similar to the DPmodel object.

We also compute the shadow price and the approximation error to compare these results to the collocation results.

```python
growth_lq = growth_model.lqapprox(sstar, kstar)
L = growth_lq.solution(basis.nodes)
L['shadow price'] = L['value_Wealth']
L['error'] = L['value'] - vtrue(L['Wealth'])
L['D.Wealth'] = L['Wealth_next']- L['Wealth']
L.head()
```

      Wealth
      Investment
      value
      value_Wealth
      Wealth_next
      shadow price
      error
      D.Wealth

      Wealth

      0.202191
      0.202191
      -0.020528
      -15.014820
      4.444953
      0.202192
      4.444953
      0.403234
      2.977733e-07

      0.219577
      0.219577
      -0.004880
      -14.937786
      4.416570
      0.219578
      4.416570
      0.330284
      2.769166e-07

      0.253590
      0.253590
      0.025731
      -14.788512
      4.361045
      0.253590
      4.361045
      0.217716
      2.361148e-07

      0.302742
      0.302742
      0.069968
      -14.576129
      4.280804
      0.302742
      4.280804
      0.107984
      1.771510e-07

      0.364886
      0.364886
      0.125897
      -14.313256
      4.179353
      0.364886
      4.179353
      0.031397
      1.026024e-07

```python
growth_lq.G
```

    array([[1.]])

## Plotting the results

### Optimal Policy

```python
fig1 = demo.figure('Optimal Investment Policy', 'Wealth', 'Investment')
plt.plot(S.Investment, label='Chebychev Collocation')
plt.plot(L.Investment, label='L-Q Approximation')
demo.annotate(sstar, kstar,'$s^*$ = %.2f\n$V^*$ = %.2f' % (sstar, kstar),'bo', (10, -17),ms=10)
plt.legend(loc= 'upper left')
```

### Value Function

```python
fig2 = demo.figure('Value Function', 'Wealth', 'Value')
plt.plot(S.value, label='Chebychev Collocation')
plt.plot(L.value, label='L-Q Approximation')

demo.annotate(sstar, vstar, f'$s^* = {sstar:.2f}$\n$V^* = {vstar:.2f}$', 'bo', (10, -17),ms=10)
plt.legend(loc= 'upper left')
```

### Shadow Price Function

```python
fig3 = demo.figure('Shadow Price Function', 'Wealth', 'Shadow Price')
plt.plot(S['shadow price'], label='Chebychev Collocation')
plt.plot(L['shadow price'], label='L-Q Approximation')
demo.annotate(sstar, pstar,f'$s^* = {sstar:.2f}$\n$\lambda^* = {pstar:.2f}$', 'bo', (10, 17),ms=10)
plt.legend(loc= 'upper right')
```

### Chebychev Collocation Residual and Approximation Error vs. Linear-Quadratic Approximation Error

```python
fig4 = plt.figure(figsize=[12, 6])
demo.subplot(1, 2, 1, 'Chebychev Collocation Residual\nand Approximation Error', 'Wealth', 'Residual/Error')
plt.hlines(0,smin,smax,'k',linestyles='--')
plt.plot(S[['resid', 'error']])
plt.legend(['Residual','Error'], loc='lower right')
plt.ticklabel_format(style='sci', axis='y', scilimits=(-1,1))

demo.subplot(1, 2, 2, 'Linear-Quadratic Approximation Error', 'Wealth', 'Error')
plt.hlines(0,smin,smax,'k',linestyles='--')
plt.plot(L['error'], label='Error')
plt.legend(loc='upper left')
plt.ticklabel_format(style='sci', axis='y', scilimits=(-1,1))
```

### Wealth dynamics

Notice how the steady-state is stable in the Chebyshev collocation solution, but unstable in the linear-quadratic approximation. In particular, simulated paths of wealth in the L-Q approximation will converge to zero, unless the initial states is within a small neighborhood of the steady-state.

```python
fig5 = demo.figure('Wealth dynamics', 'Wealth', 'Wealth change', figsize=[8,5])
plt.plot(S['D.Wealth'], label='Chebychev Collocation')
plt.plot(L['D.Wealth'], label='L-Q Approximation')
plt.hlines(0,smin,smax,linestyles=':')

demo.annotate(sstar, 0, f'$s^* = {sstar:.2f}$\n$\Delta s^* = {0:.2f}$', 'bo', (10, 10),ms=10,fs=11)
plt.legend(loc= 'lower left')
```

## Simulating the model

We simulate 20 periods of the model starting from $s=s_{\min}$

```python
T = 20
data = growth_model.simulate(T, smin)
```

### Simulated State and Policy Paths

```python
opts = dict(spec='r*', offset=(-5, -5), fs=11, ha='right')

fig6 = demo.figure('State and Policy Paths','Period', 'Wealth/Investment',[0, T + 0.5])
plt.plot(data[['Wealth', 'Investment']])
demo.annotate(T, sstar, 'steady-state wealth\n = %.2f' % sstar, **opts)
demo.annotate(T, kstar, 'steady-state investment\n = %.2f' % kstar, **opts)
```

```python
#demo.savefig([fig1,fig2,fig3,fig4,fig5,fig6])
```

_build/jupyter_execute/notebooks/dp/06 Deterministic Optimal Economic Growth Model.ipynb


Table of Contents

1  Softmax Regression

1.1  Gradient Descent

1.2  More Gradient Descent ...

1.3  Softmax Regression Code

2  Reference

```python
# code for loading the format for the notebook
import os

# path : store the current path to convert back to it later
path = os.getcwd()
os.chdir(os.path.join('..', 'notebook_format'))

from formats import load_style
load_style(plot_style = False)
```

```python
os.chdir(path)

# 1. magic for inline plot
# 2. magic to print version
# 3. magic so that the notebook will reload external python modules
# 4. magic to enable retina (high resolution) plots
# https://gist.github.com/minrk/3301035
%matplotlib inline
%load_ext watermark
%load_ext autoreload
%autoreload 2
%config InlineBackend.figure_format = 'retina'

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.metrics import accuracy_score
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

%watermark -a 'Ethen' -d -t -v -p numpy,pandas,matplotlib,sklearn
```

    Ethen 2018-09-15 15:15:16

    CPython 3.6.4
    IPython 6.4.0

    numpy 1.14.1
    pandas 0.23.0
    matplotlib 2.2.2
    sklearn 0.19.1

# Softmax Regression

**Softmax Regression** is a generalization of logistic regression that we can use for multi-class classification. If we want to assign probabilities to an object being one of several different things, softmax is the thing to do. Even later on, when we start training neural network models, the final step will be a layer of softmax.

A softmax regression has two steps: first we add up the evidence of our input being in certain classes, and then we convert that evidence into probabilities.

In **Softmax Regression**, we replace the sigmoid logistic function by the so-called *softmax* function $\phi(\cdot)$.

$$P(y=j \mid z^{(i)}) = \phi(z^{(i)}) = \frac{e^{z^{(i)}}}{\sum_{j=1}^{k} e^{z_{j}^{(i)}}}$$

where we define the net input *z* as

$$z = w_1x_1 + ... + w_mx_m  + b= \sum_{l=1}^{m} w_l x_l + b= \mathbf{w}^T\mathbf{x} + b$$

(**w** is the weight vector, $\mathbf{x}$ is the feature vector of 1 training sample. Each $w$ corresponds to a feature $x$ and there're $m$ of them in total. $b$ is the bias unit. $k$ denotes the total number of classes.)

Now, this softmax function computes the probability that the $i_{th}$ training sample $\mathbf{x}^{(i)}$ belongs to class $l$ given the weight and net input $z^{(i)}$. So given the obtained weight $w$, we're basically compute the probability, $p(y = j \mid \mathbf{x^{(i)}; w}_j)$, the probability of the training sample belonging to class $j$ for each class label in $j = 1, \ldots, k$. Note the normalization term in the denominator which causes these class probabilities to sum up to one.

We can picture our softmax regression as looking something like the following, although with a lot more $x_s$. For each output, we compute a weighted sum of the $x_s$, add a bias, and then apply softmax.

If we write that out as equations, we get:

We can "vectorize" this procedure, turning it into a matrix multiplication and vector addition. This is helpful for computational efficiency. (It's also a useful way to think.)

To illustrate the concept of softmax, let us walk through a concrete example. Suppose we have a training set consisting of 4 samples from 3 different classes (0, 1, and 2)

- $x_0 \rightarrow \text{class }0$
- $x_1 \rightarrow \text{class }1$
- $x_2 \rightarrow \text{class }2$
- $x_3 \rightarrow \text{class }2$

First, we apply one-hot encoding to encode the class labels into a format that we can more easily work with.

```python
y = np.array([0, 1, 2, 2])

def one_hot_encode(y):
    n_class = np.unique(y).shape[0]
    y_encode = np.zeros((y.shape[0], n_class))
    for idx, val in enumerate(y):
        y_encode[idx, val] = 1.0

    return y_encode

y_encode = one_hot_encode(y)
y_encode
```

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

A sample that belongs to class 0 (the first row) has a 1 in the first cell, a sample that belongs to class 1 has a 1 in the second cell of its row, and so forth.

Next, let us define the feature matrix of our 4 training samples. Here, we assume that our dataset consists of 2 features; thus, we create a 4x2 dimensional matrix of our samples and features.
Similarly, we create a 2x3 dimensional weight matrix (one row per feature and one column for each class).

```python
X = np.array([[0.1, 0.5],
              [1.1, 2.3],
              [-1.1, -2.3],
              [-1.5, -2.5]])

W = np.array([[0.1, 0.2, 0.3],
              [0.1, 0.2, 0.3]])

bias = np.array([0.01, 0.1, 0.1])

print('Inputs X:\n', X)
print('\nWeights W:\n', W)
print('\nbias:\n', bias)
```

    Inputs X:
     [[ 0.1  0.5]
     [ 1.1  2.3]
     [-1.1 -2.3]
     [-1.5 -2.5]]

    Weights W:
     [[0.1 0.2 0.3]
     [0.1 0.2 0.3]]

    bias:
     [0.01 0.1  0.1 ]

To compute the net input, we multiply the 4x2 matrix feature matrix `X` with the 2x3 (n_features x n_classes) weight matrix `W`, which yields a 4x3 output matrix (n_samples x n_classes) to which we then add the bias unit:

$$\mathbf{Z} = \mathbf{X}\mathbf{W} + \mathbf{b}$$

```python
def net_input(X, W, b):
    return X.dot(W) + b

net_in = net_input(X, W, bias)
print('net input:\n', net_in)
```

    net input:
     [[ 0.07  0.22  0.28]
     [ 0.35  0.78  1.12]
     [-0.33 -0.58 -0.92]
     [-0.39 -0.7  -1.1 ]]

Now, it's time to compute the softmax activation that we discussed earlier:

$$P(y=j \mid z^{(i)}) = \phi_{softmax}(z^{(i)}) = \frac{e^{z^{(i)}}}{\sum_{j=1}^{k} e^{z_{j}^{(i)}}}$$

```python
def softmax(z):
    return np.exp(z) / np.sum(np.exp(z), axis = 1, keepdims = True)

smax = softmax(net_in)
print('softmax:\n', smax)
```

    softmax:
     [[0.29450637 0.34216758 0.36332605]
     [0.21290077 0.32728332 0.45981591]
     [0.42860913 0.33380113 0.23758974]
     [0.44941979 0.32962558 0.22095463]]

As we can see, the values for each sample (row) nicely sum up to 1 now. E.g., we can say that the first sample  `[ 0.29450637  0.34216758  0.36332605]` has a 29.45% probability to belong to class 0. Now, in order to turn these probabilities back into class labels, we could simply take the argmax-index position of each row:

[[ 0.29450637  0.34216758  **0.36332605**] -> 2
[ 0.21290077  0.32728332  **0.45981591**]  -> 2
[ **0.42860913**  0.33380113  0.23758974]  -> 0
[ **0.44941979**  0.32962558  0.22095463]] -> 0

```python
def to_classlabel(z):
    return z.argmax(axis = 1)

print('predicted class labels: ', to_classlabel(smax))
```

    predicted class labels:  [2 2 0 0]

As we can see, our predictions are terribly wrong, since the correct class labels are `[0, 1, 2, 2]`. Now, in order to train our model we need to measuring how inefficient our predictions are for describing the truth and then optimize on it. To do so we first need to define a loss/cost function $J(\cdot)$ that we want to minimize. One very common function is "cross-entropy":

$$J(\mathbf{W}; \mathbf{b}) = \frac{1}{n} \sum_{i=1}^{n} H( T^{(i)}, O^{(i)} )$$

which is the average of all cross-entropies $H$ over our $n$ training samples. The cross-entropy  function is defined as:

$$H( T^{(i)}, O^{(i)} ) = -\sum_k T^{(i)} \cdot log(O^{(i)})$$

Where:

- $T$ stands for "target" (i.e., the *true* class labels)
- $O$ stands for output -- the computed *probability* via softmax; **not** the predicted class label.
- $\sum_k$ denotes adding up the difference between the target and the output for all classes.

```python
def cross_entropy_cost(y_target, output):
    return np.mean(-np.sum(y_target * np.log(output), axis = 1))

cost = cross_entropy_cost(y_target = y_encode, output = smax)
print('Cross Entropy Cost:', cost)
```

    Cross Entropy Cost: 1.3215978715930938

## Gradient Descent

Our objective in training a neural network is to find a set of weights that gives us the lowest error when we run it against our training data. There are many ways to find these weights and simplest one is so called **gradient descent**. It does this by giving us directions (using derivatives) on how to "shift" our weights to an optimum. It tells us whether we should increase or decrease the value of a specific weight in order to lower the error function.

Let's imagine we have a function $f(x) = x^4 - 3x^3 + 2$ and we want to find the minimum of this function using gradient descent. Here's a graph of that function:

```python
from sympy.plotting import plot
from sympy import symbols, init_printing

# change default figure and font size
plt.rcParams['figure.figsize'] = 8, 6
plt.rcParams['font.size'] = 12

# plotting f(x) = x^4 - 3x^3 + 2, showing -2 < x <4
init_printing()
x = symbols('x')
fx = x ** 4 - 3 * x ** 3 + 2
p1 = plot(fx, (x, -2, 4), ylim = (-10, 50))
```

As you can see, there appears to be a minimum around ~2.3 or so. Gradient descent answers this question: If we were to start with a random value of x, which direction should we go if we want to get to the lowest point on this function? Let's imagine we pick a random x value, say x = 4, which would be somewhere way up on the right side of the graph. We obviously need to start going to the left if we want to get to the bottom. This is obvious when the function is an easily visualizable 2d plot, but when dealing with functions of multiple variables, we need to rely on the raw mathematics.

Calculus tells us that the derivative of a function at a particular point is the rate of change/slope of the tangent to that part of the function. So let's use derivatives to help us get to the bottom of this function. The derivative of $f(x) = x^4 - 3x^3 + 2$ is $f'(x) = 4x^3 - 9x^2$. So if we plug in our random point from above (x=4) into the first derivative of $f(x)$ we get $f'(4) = 4(4)^3 - 9(4)^2 = 112$. So how does 112 tell us where to go? Well, first of all, it's positive. If we were to compute $f'(-1)$ we get a negative number (-13). So it looks like we can say that whenever the $f'(x)$ for a particular $x$ is positive, we should move to the left (decrease x) and whenever it's negative, we should move to the right (increase x).

We'll now formalize this: When we start with a random x and compute it's deriative $f'(x)$, our new x should then be proportional to $x - f'(x)$. The word proportional is there because we wish to control to what degree we move at each step, for example when we compute $f'(4)=112$, do we really want our new $x$ to be $x - 112 = -108$? No, if we jump all the way to -108, we're even farther from the minimum than we were before. Instead, we want to take relatively small steps toward the minimum.

Let's say that for any random $x$, we want to take a step (change $x$ a little bit) such that our new $x$ $ = x - \alpha*f'(x)$. We'll call $\alpha$ (alpha) our learning rate or step size because it determines how big of a step we take. $\alpha$ is something we will just have to play around with to find a good value. Some functions might require bigger steps, others smaller steps.

Suppose we've set our $\alpha$ to be 0.001. This means, if we randomly started at $f'(4)=112$ then our new $x$ will be $ = 4 - (0.001 * 112) = 3.888$. So we moved to the left a little bit, toward the optimum. Let's do it again. $x_{new} = x - \alpha*f'(3.888) = 3.888 - (0.001 * 99.0436) = 3.79$. Nice, we're indeed moving to the left, closer to the minimum of $f(x)$, little by little. And we'll keep on doing this until we've reached convergence. By convergence, we mean that if the absolute value of the difference between the updated $x$ and the old $x$ is smaller than some randomly small number that we set, denoted as $\epsilon$ (epsilon).

```python
x_old = 0
x_new = 4 # The algorithm starts at x = 4
alpha = 0.01 # step size
epsilon = 0.00001

def f_derivative(x):
    return 4 * x ** 3 - 9 * x ** 2

while abs(x_new - x_old) > epsilon:
    x_old = x_new
    x_new = x_old - alpha * f_derivative(x_old)

print("Local minimum occurs at", x_new)
```

    Local minimum occurs at 2.2500325268933734

The script above says that if the absolute difference of $x$ between the two iterations is not changing by more than 0.00001, then we're probably at the bottom of the "bowl" because our slope is approaching 0, and therefore we should stop and call it a day. Now, if you remember some calculus and algebra, you could have solved for this minimum analytically, and you should get 2.25. Very close to what our gradient descent algorithm above found.

## More Gradient Descent ...

As you might imagine, when we use gradient descent for a neural network, things get a lot more complicated. Not because gradient descent gets more complicated, it still ends up just being a matter of taking small steps downhill, it's that we need that pesky derivative in order to use gradient descent, and the derivative of a neural network cost function (with respect to its weights) is pretty intense. It's not a matter of just analytically solving $f(x)=x^2, f'(x)=2x$ , because the output of a neural net has many nested or "inner" functions.

Also unlike our toy math problem above, a neural network may have many weights. We need to find the optimal value for each individual weight to lower the cost for our entire neural net output. This requires taking the partial derivative of the cost/error function with respect to a single weight, and then running gradient descent for each individual weight. Thus, for any individual weight $W_j$, we'll compute the following:

$$ W_j^{(t + 1)} = W_j^{(t)} - \alpha * \frac{\partial L}{\partial W_j}$$

Where:

- $L$ denotes the loss function that we've defined.
- $W_j^{(t)}$ denotes the weight of the $j_{th}$ feature at iteration $t$.

And as before, we do this iteratively for each weight, many times, until the whole network's cost function is minimized.

In order to learn the weight for our softmax model via gradient descent, we then need to compute the gradient of our cost function for each class $j \in \{0, 1, ..., k\}$.

$$\nabla \mathbf{w}_j \, J(\mathbf{W}; \mathbf{b})$$

We won't be going through the tedious details here, but this cost's gradient turns out to be simply:

$$\nabla \mathbf{w}_j \, J(\mathbf{W}; \mathbf{b}) = \frac{1}{n} \sum^{n}_{i=0} \big[\mathbf{x}^{(i)}_j\ \big( O^{(i)} - T^{(i)} \big) \big]$$

We can then use the cost derivate to update the weights in opposite direction of the cost gradient with learning rate $\eta$:

$$\mathbf{w}_j := \mathbf{w}_j - \eta \nabla \mathbf{w}_j \, J(\mathbf{W}; \mathbf{b})$$

(note that $\mathbf{w}_j$ is the weight vector for the class $y=j$), and we update the bias units using:

$$
\mathbf{b}_j := \mathbf{b}_j   - \eta \bigg[ \frac{1}{n} \sum^{n}_{i=0} \big( O^{(i)} - T^{(i)} \big) \bigg]
$$

As a penalty against complexity, an approach to reduce the variance of our model and decrease the degree of overfitting by adding additional bias, we can further add a regularization term such as the L2 term with the regularization parameter $\lambda$:

$$\frac{\lambda}{2} ||\mathbf{w}||_{2}^{2}$$

where $||\mathbf{w}||_{2}^{2}$ simply means adding up the squared weights across all the features and classes.

$$||\mathbf{w}||_{2}^{2} = \sum^{m}_{l=0} \sum^{k}_{j=0} w_{l, j}^2$$

so that our cost function becomes

$$
J(\mathbf{W}; \mathbf{b}) = \frac{1}{n} \sum_{i=1}^{n} H( T^{(i)}, O^{(i)} ) + \frac{\lambda}{2} ||\mathbf{w}||_{2}^{2}
$$

and we define the "regularized" weight update as

$$
\mathbf{w}_j := \mathbf{w}_j -  \eta \big[\nabla \mathbf{w}_j \, J(\mathbf{W}) + \lambda \mathbf{w}_j \big]
$$

Note that we don't regularize the bias term, thus the update function for it stays the same.

## Softmax Regression Code

Bringing the concepts together, we could come up with an implementation as follows: Note that for the weight and bias parameter, we'll have initialize a value for it. Here we'll simply draw the weights from a normal distribution and set the bias as zero. The code can be obtained [here](https://github.com/ethen8181/machine-learning/blob/master/deep_learning/softmax.py).

```python
# import some data to play with
iris = datasets.load_iris()
X = iris.data
y = iris.target

# standardize the input features
scaler = StandardScaler()
X_std = scaler.fit_transform(X)
```

```python
from softmax import SoftmaxRegression

# train the softmax using batch gradient descent,
# eta: learning rate, epochs : number of iterations, minibatches, number of
# training data to use for training at each iteration
softmax_reg = SoftmaxRegression(eta = 0.1, epochs = 10, minibatches = y.shape[0])
softmax_reg.fit(X_std, y)

# print the training accuracy
y_pred = softmax_reg.predict(X_std)
accuracy = np.sum(y_pred == y) / y.shape[0]
print('accuracy: ', accuracy)

# use a library to ensure comparable results
log_reg = LogisticRegression()
log_reg.fit(X_std, y)
y_pred = log_reg.predict(X_std)
print('accuracy library: ', accuracy_score(y_true = y, y_pred = y_pred))
```

    accuracy:  0.94
    accuracy library:  0.9266666666666666

# Reference

- [Blog: Softmax Regression](http://nbviewer.jupyter.org/github/rasbt/python-machine-learning-book/blob/master/code/bonus/softmax-regression.ipynb)
- [Blog: Gradient Descent with Backpropagation](http://outlace.com/Beginner-Tutorial-Backpropagation/)
- [TensorFlow Documentation: MNIST For ML Beginners](https://www.tensorflow.org/get_started/mnist/beginners)


```python
import random

%matplotlib inline
import networkx as nx
```

# Chapter 6 Tutorial

Contents:

1. Partitions
2. Modularity
3. Zachary's Karate Club
4. Girvan-Newman clustering algorithm

## 1. Partitions

A **partition** of a graph is a separation of its nodes into disjoint groups. Consider the following graph:

```python
G = nx.Graph()
G.add_cycle([0, 1, 2, 3])
G.add_cycle([4, 5, 6, 7])
G.add_edge(0, 7)

nx.draw(G, with_labels=True)
```

The following is an example of a partition of these nodes:

```python
partition = [
    {1, 2, 3},
    {4, 5, 6},
    {0, 7},
]
```

Observe that every node in the graph is in exactly one of the sets in the partition. Formally, a partition is a list of sets such that every node is in exactly one set. NetworkX can verify that our partition is valid:

```python
nx.community.is_partition(G, partition)
```

    True

When developing community detection algorithms, we often make use of a *partition map*, which is a dictionary mapping node names to a partition index. This is useful for quickly comparing if two nodes are in the same cluster in the partition:

```python
partition_map = {}
for idx, cluster_nodes in enumerate(partition):
    for node in cluster_nodes:
        partition_map[node] = idx

partition_map
```

    {1: 0, 2: 0, 3: 0, 4: 1, 5: 1, 6: 1, 0: 2, 7: 2}

In this dictionary, the keys are the node names and two nodes will have the same value if they are in the same partition:

```python
partition_map[0] == partition_map[7]
```

    True

We can visualize our partition by drawing the graph with nodes colored by their partition membership:

```python
node_colors = [partition_map[n] for n in G.nodes]

nx.draw(G, node_color=node_colors, with_labels=True)
```

There are two trivial partitions:

1. The partition with one set containing every node;
2. The partition with N sets, each containing a single node.

A valid partition thus contains between 1 and N sets.

Feel free to experiment by changing the partition above and running the subsequent cells.

## 2. Modularity

At a high level, network community detection consists of finding a partition that achieves good separation between the groups of nodes. Before we get into how to find good partitions of a graph, we need an objective -- a way to measure how good the partition is. Modularity is one such objective function.

The modularity of a graph partition compares the number of intra-group edges with a random baseline. Higher modularity scores correspond to a higher proportion of intra-group edges, therefore fewer inter-group edges and better separation of groups.

For weighted undirected networks, as described in the text, we have
\begin{equation}
    Q_w=\frac{1}{W}\sum_C \left(W_C-\frac{s_C^2}{4W}\right),
    \label{eq:wmodul}
\end{equation}
where
* $W$ is the total weight of the links of the network,
* $W_C$ the total weight of the internal links of cluster $C$, and
* $s_C$ the total strength of the nodes of $C$.

The total weight $W$ is half the total strength for the same reason that the number of edges $L$ is half the total degree. While this formula may look a bit complicated, it's straightforward to write code to compute the sum:

```python
def modularity(G, partition):
    W = sum(G.edges[v, w].get('weight', 1) for v, w in G.edges)
    summation = 0
    for cluster_nodes in partition:
        s_c = sum(G.degree(n, weight='weight') for n in cluster_nodes)
        # Use subgraph to count only internal links
        C = G.subgraph(cluster_nodes)
        W_c = sum(C.edges[v, w].get('weight', 1) for v, w in C.edges)
        summation += W_c - s_c ** 2 / (4 * W)

    return summation / W
```

```python
modularity(G, partition)
```

    0.2222222222222222

Let's compare this to a partition we would suspect to have higher modularity:

```python
partition_2 = [
    {0, 1, 2, 3},
    {4, 5, 6, 7},
]
modularity(G, partition_2)
```

    0.38888888888888884

### NetworkX function

NetworkX provides a modularity function that is more efficient than ours:

```python
nx.community.quality.modularity(G, partition_2)
```

    0.3888888888888889

## 3. Zachary's Karate Club

When writing and testing community-detection algorithms, it helps to make use of benchmark networks: graphs with a known, "natural" community structure. Perhaps the most famous benchmark graph is Zachary's Karate Club. It contains 34 nodes, representing members of a karate club whose interactions were monitored over a period of three years by researchers. Links in this graph connect individuals interacting outside club activities, a proxy for social ties.

During the course of the study, a conflict between the instructor Mr. Hi (node 0) and the president, or Officer (node 33) led to a split of the club into separate groups led by Mr. Hi and Officer. In this case we know whom each member of the group followed after the split, providing empirical community labels: those members who followed Mr. Hi are said to be one community and those following the Officer make up the other.

For this graph, we assume that the post-split group composition was largely driven by the social ties: members of the same friend groups would want to be part of the same club after the split. We thus expect a good community-detection algorithm to predict the post-split group composition with high accuracy.

Zachary's karate club is such a popular benchmark graph that it has its own function in NetworkX:

```python
K = nx.karate_club_graph()
nx.draw(K, with_labels=True)
```

Each node in a NetworkX graph has a dictionary of *attributes* associated with it. This dictionary can hold arbitrary data about a node. We can get the attributes for a single node by giving the node name to the `nodes` object.

Each node in this graph has a `'club'` attribute, indicating whether the member followed the instructor or the president after the split:

```python
K.nodes[0]
```

    {'club': 'Mr. Hi'}

```python
K.node[9]
```

    {'club': 'Officer'}

We can visualize these labels by coloring each node according to its `'club'` attribute:

```python
K = nx.karate_club_graph()
club_color = {
    'Mr. Hi': 'orange',
    'Officer': 'lightblue',
}
node_colors = [club_color[K.node[n]['club']] for n in K.nodes]
nx.draw(K, node_color=node_colors, with_labels=True)
```

This separation looks good, in that there are relatively few inter-community links as opposed to intra-community links. Let's create a graph partition based on these labels and measure its modularity.

We can do this by creating a dictionary of two sets, one for each value of the nodes' `'club'` attribute, then assigning the nodes to the corresponding set.

```python
groups = {
    'Mr. Hi': set(),
    'Officer': set(),
}

for n in K.nodes:
    club = K.node[n]['club']
    groups[club].add(n)

groups
```

    {'Mr. Hi': {0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 16, 17, 19, 21},
     'Officer': {9, 14, 15, 18, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33}}

By using the dictionary's `.values()` method, we can get a list of sets that define our partition:

```python
empirical_partition = list(groups.values())
empirical_partition
```

    [{0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 16, 17, 19, 21},
     {9, 14, 15, 18, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33}]

```python
nx.community.is_partition(K, empirical_partition)
```

    True

Since our partition is indeed a valid partition, we can get the modularity of this partition:

```python
nx.community.quality.modularity(K, empirical_partition)
```

    0.35823471400394263

This is a relatively high modularity, which is what we expect.

### Comparison to a random partition

For the sake of comparison, let's generate a random partition of this network and check its modularity. We would expect a modularity close to zero in this case.

First we generate a sample of 17 nodes, half the total number of nodes, and assign them to one community. Our second community then includes the nodes in the graph not in the first community. We can use some set arithmetic to do this concisely:

```python
random_nodes = random.sample(K.nodes, 17)
random_partition = [set(random_nodes),
                    set(K.nodes) - set(random_nodes)]
random_partition
```

    [{0, 1, 4, 5, 7, 9, 12, 15, 16, 17, 21, 24, 26, 27, 28, 31, 32},
     {2, 3, 6, 8, 10, 11, 13, 14, 18, 19, 20, 22, 23, 25, 29, 30, 33}]

We can visualize this partition and observe that the communities are much less natural-looking, as we would expect from a random assignment.

```python
random_node_colors = ['orange' if n in random_nodes else 'lightblue' for n in K.nodes]
nx.draw(K, node_color=random_node_colors)
```

And finally we can test the modularity of this partition:

```python
nx.community.quality.modularity(K, random_partition)
```

    -0.06410256410256385

Since this is a random process the modularity won't be exactly zero, but it should be fairly close. Go ahead and repeat the process of generating a random partition and testing its modularity -- it will fluctuate around its mean value of zero.

# 4. Girvan-Newman clustering

Our task in this part will be to implement the Girvan-Newman clustering algorithm. Since NetworkX can do the heavy lifting for us -- computing betweenness centrality -- the code part of the task is relatively straightforward. Most of our effort here is spent interpreting and explaining intermediate results.

Recall from the text the Girvan-Newman clustering algorithm:

1. Create a partition sequence
  1. Calculate the betweenness centrality for all links.
  2. Remove the link with largest betweenness and create a partition using connected components.
  3. Recalculate the betweenness centrality of the links of the resulting graph.
  4. Repeat from step B until no links remain.
2. Evaluate each partition in the sequence and choose the one with the highest modularity.

During this process, the number of connected components in the graph will increase monotonically as clusters are broken up. Since we are removing one link at a time, the number of connected components can increase by at most one between steps in the sequence -- it's not possible for a single edge to connect more than two nodes, and thus components.

We hope that the resulting partition of the graph will approximate its underlying community structure. We'll use the Karate Club graph here because we know the ground-truth community labels and can compare the result obtained from the algorithm.

```python
G = nx.karate_club_graph()
nx.draw(G)
```

## 4.1 Create a partition sequence
### A. Calculate the betweenness centrality for all links

NetworkX does the heavy lifting here. All we need to do is understand the output.

```python
nx.edge_betweenness_centrality(G)
```

    {(0, 1): 0.025252525252525245,
     (0, 2): 0.0777876807288572,
     (0, 3): 0.02049910873440285,
     (0, 4): 0.0522875816993464,
     (0, 5): 0.07813428401663694,
     (0, 6): 0.07813428401663695,
     (0, 7): 0.0228206434088787,
     (0, 8): 0.07423959482783014,
     (0, 10): 0.0522875816993464,
     (0, 11): 0.058823529411764705,
     (0, 12): 0.04652406417112298,
     (0, 13): 0.04237189825425121,
     (0, 17): 0.04012392835922248,
     (0, 19): 0.045936960642843,
     (0, 21): 0.040123928359222474,
     (0, 31): 0.1272599949070537,
     (1, 2): 0.023232323232323233,
     (1, 3): 0.0077243018419489,
     (1, 7): 0.007422969187675069,
     (1, 13): 0.01240556828792123,
     (1, 17): 0.01869960105254222,
     (1, 19): 0.014633732280791102,
     (1, 21): 0.01869960105254222,
     (1, 30): 0.032280791104320514,
     (2, 3): 0.022430184194890075,
     (2, 7): 0.025214328155504617,
     (2, 8): 0.009175791528732704,
     (2, 9): 0.030803836686189627,
     (2, 13): 0.007630931160342923,
     (2, 27): 0.04119203236850296,
     (2, 28): 0.02278244631185807,
     (2, 32): 0.06898678663384543,
     (3, 7): 0.003365588659706307,
     (3, 12): 0.012299465240641705,
     (3, 13): 0.01492233256939139,
     (4, 6): 0.0047534165181224,
     (4, 10): 0.0029708853238265,
     (5, 6): 0.0029708853238265003,
     (5, 10): 0.0047534165181224,
     (5, 16): 0.029411764705882353,
     (6, 16): 0.029411764705882353,
     (8, 30): 0.00980392156862745,
     (8, 32): 0.0304416716181422,
     (8, 33): 0.04043657867187279,
     (9, 33): 0.029615482556659026,
     (13, 33): 0.06782389723566191,
     (14, 32): 0.024083977025153497,
     (14, 33): 0.03473955238661121,
     (15, 32): 0.024083977025153497,
     (15, 33): 0.03473955238661121,
     (18, 32): 0.024083977025153497,
     (18, 33): 0.03473955238661121,
     (19, 33): 0.05938233879410351,
     (20, 32): 0.024083977025153497,
     (20, 33): 0.03473955238661121,
     (22, 32): 0.024083977025153493,
     (22, 33): 0.03473955238661121,
     (23, 25): 0.019776193305605066,
     (23, 27): 0.010536739948504653,
     (23, 29): 0.00665478312537136,
     (23, 32): 0.022341057635175278,
     (23, 33): 0.03266983561101209,
     (24, 25): 0.0042186571598336305,
     (24, 27): 0.018657159833630418,
     (24, 31): 0.040106951871657755,
     (25, 31): 0.04205783323430383,
     (26, 29): 0.004532722179781003,
     (26, 33): 0.0542908072319837,
     (27, 33): 0.030477039300568713,
     (28, 31): 0.0148544266191325,
     (28, 33): 0.024564977506153975,
     (29, 32): 0.023328523328523323,
     (29, 33): 0.029807882749059215,
     (30, 32): 0.01705288175876411,
     (30, 33): 0.02681436210847975,
     (31, 32): 0.04143394731630026,
     (31, 33): 0.05339388280564752,
     (32, 33): 0.008225108225108224}

The resulting dictionary has edge tuples as the keys, and each associated value is the betweenness centrality of that edge. The algorithm to compute the edge betweenness of all edges in a graph costs about the same as calculating it for a single edge, so we'll make use of this dictionary with the computed values for every edge.

Once computed for all edges, we can easily get the associated betweenness for a single edge. For example, to get the edge betweenness of the edge between nodes 0 and 1:

```python
my_edge_betweenness = nx.edge_betweenness_centrality(G)
my_edge_betweenness[0, 1]
```

    0.025252525252525245

Recall that dictionaries also have the `.get` method. This will be used in the next step.

```python
my_edge_betweenness.get((0, 1))
```

    0.025252525252525245

### B. Remove the link with largest betweenness...

Given this dictionary of betweenness values for each edge, we can make use of Python's builtin `max` function to give us the key in this dictionary with the greatest value. Since there is a key in the dictionary for each edge in the graph, the following two expressions are equivalent, but the second one is probably more explicit as to what we're doing with this statement.

I'm using the name `my_edge_betweenness` to make clear that this is a dictionary we've named and not a NetworkX function.

```python
max(my_edge_betweenness, key=my_edge_betweenness.get)
```

    (0, 31)

```python
max(G.edges(), key=my_edge_betweenness.get)
```

    (0, 31)

This is then the edge we want to remove at this step in the process:

```python
my_edge_betweenness = nx.edge_betweenness_centrality(G)
most_valuable_edge = max(G.edges(), key=my_edge_betweenness.get)
G.remove_edge(*most_valuable_edge)
```

The "splat" in the last statement above `G.remove_edge(*most_valuable_edge)` performs tuple unpacking into the arguments of a function. For example, if our most valuable edge is `(0, 31)`,

    G.remove_edge(*most_valuable_edge)
is the same as

    G.remove_edge(most_valuable_edge[0], most_valuable_edge[1])
or

    G.remove_edge(0, 31)

### B. (cont'd) ...and create a partition using connected components

This one is almost a gimme because the `nx.connected_components()` function gives us almost exactly what we want:

```python
nx.connected_components(G)
```

    <generator object connected_components at 0x7f2e1ad741a8>

We just have to remember to ask for it in a list:

```python
list(nx.connected_components(G))
```

    [{0,
      1,
      2,
      3,
      4,
      5,
      6,
      7,
      8,
      9,
      10,
      11,
      12,
      13,
      14,
      15,
      16,
      17,
      18,
      19,
      20,
      21,
      22,
      23,
      24,
      25,
      26,
      27,
      28,
      29,
      30,
      31,
      32,
      33}]

Remember: a partition is a list of sets where every node is in exactly one of these sets. This is just what we have here, although it's a bit boring since we've only removed one edge and so there is still one connected component. If you like, you can try running the previous two cells a few times until you have more than one connected component so you can see what that looks like.

Note that this feature whereby the connected components correspond exactly to our putative community labels is particular to the Girvan-Newman algorithm: other clustering algorithms may use different ways of generating their partitions.

### C. Recalculate the betweenness centrality of the links of the resulting graph.
### D. Repeat from step B until no links remain.

This implies that we need a loop to repeat this process $L$ times, once for each edge, and that we should keep track of the partitions generated. Straightforward stuff. We'll start with a fresh Karate Club graph since we removed some edges above:

```python
G = nx.karate_club_graph()
partition_sequence = []
for _ in range(G.number_of_edges()):
    my_edge_betweenness = nx.edge_betweenness_centrality(G)
    most_valuable_edge = max(G.edges(), key=my_edge_betweenness.get)
    G.remove_edge(*most_valuable_edge)
    my_partition = list(nx.connected_components(G))
    partition_sequence.append(my_partition)
```

Note the idiomatic construction of this `for` loop. Using `_` as the name for the loop variable tells the reader that we don't expect to do anything with the loop variable -- we just want to perform a task a specific number of times. One might be tempted to use a `while` loop here, but that way lie dragons: a mistake in a `while` loop can lead to infinite loops which are a headache.

If we've done this right, we should have a partition for each step of the process, *i.e.* one for each edge in the graph:

```python
len(partition_sequence), nx.karate_club_graph().number_of_edges()
```

    (78, 78)

Since we started with a connected graph, removing one edge probably doesn't disconnect the graph, so our first partition probably only has one community:

```python
len(partition_sequence[0])
```

    1

...and the last partition should also be trivial, with each node in its own community:

```python
len(partition_sequence[-1]), nx.karate_club_graph().number_of_nodes()
```

    (34, 34)

## 4.2 Evaluate the modularity of each partition in the sequence

We now have a sequence of partitions and a function to calculate the modularity of a partition. This is a great time to use a list comprehension!

```python
G = nx.karate_club_graph()
modularity_sequence = [modularity(G, p) for p in partition_sequence]
modularity_sequence
```

    [0.0,
     0.0,
     0.0,
     0.0,
     0.0,
     0.0,
     0.0,
     0.0,
     0.0,
     0.0,
     0.3599605522682446,
     0.3599605522682446,
     0.3599605522682446,
     0.348783694937541,
     0.348783694937541,
     0.348783694937541,
     0.348783694937541,
     0.3632478632478632,
     0.3632478632478632,
     0.3632478632478632,
     0.3632478632478632,
     0.3632478632478632,
     0.3632478632478632,
     0.4012984878369493,
     0.3925049309664694,
     0.3925049309664694,
     0.3762327416173569,
     0.3762327416173569,
     0.35831689677843515,
     0.35831689677843515,
     0.34171597633136086,
     0.34171597633136086,
     0.3247863247863247,
     0.3247863247863247,
     0.3159105851413542,
     0.3159105851413542,
     0.2986522024983562,
     0.2986522024983562,
     0.28040762656147256,
     0.28040762656147256,
     0.26282051282051266,
     0.26282051282051266,
     0.24753451676528584,
     0.24753451676528584,
     0.22682445759368833,
     0.22682445759368833,
     0.20890861275476655,
     0.20890861275476655,
     0.1898422090729783,
     0.18129520052596976,
     0.18129520052596976,
     0.18129520052596976,
     0.1600920447074293,
     0.1600920447074293,
     0.1469428007889546,
     0.1469428007889546,
     0.1469428007889546,
     0.12031558185404337,
     0.12031558185404337,
     0.12031558185404337,
     0.1081525312294543,
     0.1081525312294543,
     0.09064760026298488,
     0.08029257067718606,
     0.06993754109138725,
     0.057856673241288625,
     0.057856673241288625,
     0.03418803418803419,
     0.0220249835634451,
     0.0220249835634451,
     0.0220249835634451,
     -0.002876397107166334,
     -0.002876397107166334,
     -0.026298487836949366,
     -0.03763971071663378,
     -0.03763971071663378,
     -0.053747534516765276,
     -0.04980276134122286]

This sequence is then the modularity of the partition at each step in the algorithm. The first several entries in this sequence are effectively zero while there is only one community/component, then it jumps up once there is more than one community. We can use pyplot to visualize this relationship:

```python
import matplotlib.pyplot as plt
plt.plot(modularity_sequence)
plt.ylabel('Modularity')
plt.xlabel('Algorithm step')
```

### Get the partition with highest modularity

Visually, we see a peak in the modularity sequence. This is the partition that maximizes modularity, and thus the output of the algorithm. We can use the `max` function to get the partition with highest modularity. Ideally we want to write the following:

```python
best_partition = max(partition_sequence, key=nx.community.quality.modularity)
```

...but we receive an error. Recall that a key function must take exactly one argument, the item in the sequence being evaluated, but the modularity function takes two arguments: the graph and the partition. We can fix this by writing a single-argument function to use as the key:

```python
def my_modularity(partition):
    return nx.community.quality.modularity(G, partition)
best_partition = max(partition_sequence, key=my_modularity)
```

Advanced Pythonauts will see a differet solution to this using the `zip` function to align the previously-generated partition & modularity sequences, but this solution is more explicit.

So after all that work, what is the best partition?

```python
best_partition
```

    [{0, 1, 3, 7, 11, 12, 13, 17, 19, 21},
     {2, 24, 25, 27, 28, 31},
     {4, 5, 6, 10, 16},
     {8, 14, 15, 18, 20, 22, 23, 26, 29, 30, 32, 33},
     {9}]

Interesting! The partition of the karate club graph with highest modularity actually has five components! Let's visualize them, using our code for partition maps we wrote back at the beginning of this tutorial:

```python
def create_partition_map(partition):
    partition_map = {}
    for idx, cluster_nodes in enumerate(partition):
        for node in cluster_nodes:
            partition_map[node] = idx
    return partition_map
```

```python
best_partition_map = create_partition_map(best_partition)

node_colors = [best_partition_map[n] for n in G.nodes()]
nx.draw(G, with_labels=True, node_color=node_colors)
```

Exactly how good is this five-community clustering?

```python
nx.community.quality.modularity(G, best_partition)
```

    0.40129848783694877

It's higher than the "ground truth" communities we evaluated in section 3, which is a good sign, but for the specific problem of trying to predict the post-split community membership, a clustering into five groups is useless to us.

### Get the best partition with a given number of communities

One of the most useful parts of the Girvan-Newman algorithm is that it is also useful when we have a specific number of clusters we want. In this case, we know the karate club split into two groups, so let's get the partition in the sequence with two components:

```python
for partition in partition_sequence:
    if len(partition) == 2:
        two_cluster_partition = partition
        break

two_cluster_partition
```

    [{0, 1, 3, 4, 5, 6, 7, 10, 11, 12, 13, 16, 17, 19, 21},
     {2, 8, 9, 14, 15, 18, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33}]

```python
two_cluster_partition_map = create_partition_map(two_cluster_partition)

node_colors = [two_cluster_partition_map[n] for n in G.nodes()]
nx.draw(G, with_labels=True, node_color=node_colors)
```

How good is this partition? We can get its modularity:

```python
nx.community.quality.modularity(G, two_cluster_partition)
```

    0.35996055226824225

Pretty good -- comparable to the ground truth community labels. Let's compare these side-by-side:

```python
import matplotlib.pyplot as plt

pos = nx.layout.spring_layout(G)
fig = plt.figure(figsize=(15, 6))

plt.subplot(1, 2, 1)
two_cluster_partition_map = create_partition_map(two_cluster_partition)
node_colors = [two_cluster_partition_map[n] for n in G.nodes()]
nx.draw(G, with_labels=True, node_color=node_colors, pos=pos)
plt.title('Predicted communities')

plt.subplot(1, 2, 2)
node_colors = [G.nodes[n]['club'] == 'Officer' for n in G.nodes()]
nx.draw(G, with_labels=True, node_color=node_colors, pos=pos)
plt.title('Actual communities')
```

We can see that the predicted community labels are pretty accurate, only differing on a couple nodes that, visually, seem like they could plausibly beling to either group. Zachary's original paper even explains the practical considerations of one of these mispredicted nodes: student 8 was very near receiving his black belt from Mr. Hi and thus did not want to leave the group even though several of his friends did.

```python
G.nodes[8]
```

    {'club': 'Mr. Hi'}

#### Aside

The astute reader might note that there may be several two-cluster partitions in the partition sequence we generated. We assert the following to be true:

1. For every integer 1 to N, the number of nodes, there is a partition in the sequence with that number of clusters
2. Every partition in the sequence with the same number of clusters is the same

Proving these is left as an exercise to the reader, but as a consequence of these being true, optimized implementations of Girvan-Newman clustering will only store one partition for each number of clusters. This is how the implementation in NetworkX works, only providing one partition for each number of communities greater than one.

## NetworkX Function

`nx.community.girvan_newman(G)` will generate a sequence containing one partition of each size greater than one. Here we can see the first several are the same as those we generated:

```python
list(nx.community.girvan_newman(G))[:5]
```

    [({0, 1, 3, 4, 5, 6, 7, 10, 11, 12, 13, 16, 17, 19, 21},
      {2, 8, 9, 14, 15, 18, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33}),
     ({0, 1, 3, 4, 5, 6, 7, 10, 11, 12, 13, 16, 17, 19, 21},
      {2, 8, 14, 15, 18, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33},
      {9}),
     ({0, 1, 3, 7, 11, 12, 13, 17, 19, 21},
      {2, 8, 14, 15, 18, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33},
      {4, 5, 6, 10, 16},
      {9}),
     ({0, 1, 3, 7, 11, 12, 13, 17, 19, 21},
      {2, 24, 25, 27, 28, 31},
      {4, 5, 6, 10, 16},
      {8, 14, 15, 18, 20, 22, 23, 26, 29, 30, 32, 33},
      {9}),
     ({0, 1, 3, 7, 12, 13, 17, 19, 21},
      {2, 24, 25, 27, 28, 31},
      {4, 5, 6, 10, 16},
      {8, 14, 15, 18, 20, 22, 23, 26, 29, 30, 32, 33},
      {9},
      {11})]

```python

```