# Clustering Agreements and Distances

Consider a graph represented with adjacency matrix $A$ where its nodes . Here, $A$ is a $d\times d$ matrix where $a_{ij}$ represents the association between node $i$ and $j$, e.g. the existance of an edge.
And $U$ is a $d \times k$ matrix, where  $u_{ik}$ denotes the memberships of node $i$ in the $k^{th}$ cluster of $U$, for example it is $1$ if node $i$ belongs to cluster $k$ in $U$ and $0$ otherwise. With this representation, a simple matrix outer product and transepose can give us interesting information. More specifically, two matrices of
$$ M= (UU^T)_{d\times d}\quad \quad N = (U^TU)_{k \times k}$$
denote respectively the co-memberships of pairs of nodes in clusters of $U$, and overlaps of pairs of clusters in $U$.
So that $M_{ij}$ denotes in how many clusters node $i$ and $j$ appeared together.
And $N_{ij}$ denotes how many nodes clusters $i$ and $j$ have in common.

We can also compute $A A^T$ or $A^TA$, which compute the number of common (in/out-)neighbors for every pair of nodes in A: a.k.a. co-citaion and biblographic coupling matrices in case of directed graphs. For undirected graphs, $A$ is symmetric and $AA^T = A^TA = A^2$.

We use these to define network clustering distances. But first lets visualize a clustered network for having illustrated examples.

## Visualizing a Clustered Graph

```
import math
import logging
import numpy as np
import numpy.linalg as LA
import networkx as nx
import igraph as ig
import networkx.linalg.graphmatrix as gm
from sympy.printing import latex
from sympy.matrices import *
import sympy as sy
import matplotlib
import matplotlib.pyplot as plt
from pylab import *

FORMAT = "[%(lineno)s:%(funcName)20s()]\n %(message)s"
logging.basicConfig(format=FORMAT, level=logging.ERROR)

init_printing()
%matplotlib inline

def printlatex(A):
    print latex(Matrix(A),mode='inline')

def draw_clustered_graph(G,U, pos, draw_edge_wights = False, draw_graph = True):
    """
    Visualizes the given clustering of a graph.

    Parameters
    ----------
    G: A networkx graph
    U: numpy matrix
        A nxk matrix representing a clustering, where n is the number of data points and k is the number of clusters in U,
        so that U_{ik} shows if the ith data-point belongs to the kth cluster in U.

    pos: A Python dictionary (optional)
        A dictionary of positions for graph nodes keyed by node

    Returns
    -------
    None

    Examples
    -------
    >>> import numpy as np
    >>> import networkx as nx
    >>> from pylab import *

   >>> np.matrix(  [[0,1,1,0,0,0,0,0,0,0],
                    [1,0,0,1,0,0,0,0,0,0],
                    [1,0,0,1,0,0,0,1,0,0],
                    [0,1,1,0,1,1,0,0,0,0],
                    [0,0,0,1,0,1,1,0,0,0],
                    [0,0,0,1,1,0,1,0,0,0],
                    [0,0,0,0,1,1,0,0,0,0],
                    [0,0,1,0,0,0,0,0,1,1],
                    [0,0,0,0,0,0,0,1,0,1],
                    [0,0,0,0,0,0,0,1,1,0]])

    >>> G = nx.from_numpy_matrix(A)
    >>> U = np.matrix([[1,0],[1,0],[1,0],[0,1],[0,1],[0,1],[0,1],[1,0],[1,0],[1,0]])
    >>> figure(figsize=(10, 4))
    >>> draw_clustered_graph(G,U, nx.spring_layout(G))
    >>> show()

    """

    color_list = ['b','g','r','m','y','c']
    color_list =['#9999FF', '#FF9999', '#66FF99','#FF99FF', '#FFFF99', '#E0E0E0','#FF9933','#FF3333','#33FF99','#33FFFF','#3333FF']

    if pos==None:
        pos = nx.spring_layout(G)#, nlist, dim, scale)spring_layout(G)
        print '----- positions to repeat the layout:: \n ',pos

    maxW = 0.0
    for (i,j, w) in G.edges(data=True):
        maxW = max(maxW,w['weight'] )
    if U is not None:
        UG = nx.from_numpy_matrix( U.dot(U.T))
        maxU = U.max()
        # draw clusters
        t =6.2
        for c in range(U.shape[1]):
            nsizes = [ int(t**4 * U[i,c]/maxU) for i in range(U.shape[0])]
            esizes = [ ((U[i,c]*U[j,c])*t**2.0/maxU**2) for (i,j, w) in UG.edges(data=True)]

            try:
                nx.draw(UG, pos = pos, node_size=nsizes, style ='solid' ,\
                        #labels ='.',\
                        node_color = color_list[c],edge_color =color_list[c],\
                        linewidths=0, width= esizes, alpha =.5)#, alpha =1
            except:
                pass
    # draw graph
#     if draw_graph:
    try:
        f= lambda w: np.log(w/maxW +1)*w*3/maxW +1  if draw_graph else 0
        nx.draw(G, pos = pos, node_color = 'w', alpha =1 ,  linewidths=1, width= [f(w['weight']) for (i,j, w) in G.edges(data=True)])
    except:
        pass

    if draw_edge_wights:
        edge_lbls = dict([((u,v,),d['weight']) for u,v,d in G.edges(data=True)])
        nx.draw_networkx_edge_labels(G, pos = pos, label_pos=0.5 ,edge_labels =edge_lbls)

    return pos

```

### Example: A visualized clustered network

```
A = np.array([[0,1,1,0,0,0,0,0,0,0],
            [1,0,0,1,0,0,0,0,0,0],
            [1,0,0,1,0,0,0,1,0,0],
            [0,1,1,0,1,1,0,0,0,0],
            [0,0,0,1,0,1,1,0,0,0],
            [0,0,0,1,1,0,1,0,0,0],
            [0,0,0,0,1,1,0,0,0,0],
            [0,0,1,0,0,0,0,0,1,1],
            [0,0,0,0,0,0,0,1,0,1],
            [0,0,0,0,0,0,0,1,1,0]])

G = nx.from_numpy_matrix(A)
U = np.array([[1,0],[1,0],[1,0],[0,1],[0,1],[0,1],[0,1],[1,0],[1,0],[1,0]])
figure(figsize=(4, 2))
fix_pos = draw_clustered_graph(G,U, nx.spring_layout(G))
show()

```

## Computing the Agreement of Two Clusters

Consider two alternative clustering of $d$ data-points, called $U$ and $V$. Where, $U$ is a $d \times k$ matrix, where  $u_{ik}$ denotes the memberships of node $i$ in the $k^{th}$ cluster of $U$, for example it is $1$ if node $i$ belongs to cluster $k$ in $U$ and $0$ otherwise. With this representation, a simple matrix outer product and transepose can give us interesting information.
More specifically, two matrices of $ M= (UU^T)_{d\times d}$ and $ N = (U^TU)_{k \times k}$
denote respectively the co-memberships of pairs of nodes in clusters of $U$, and overlaps of pairs of clusters in $U$.
In other words, $M_{ij}$ denotes in how many clusters node $i$ and $j$ appeared together; and $N_{ij}$ denotes how many nodes clusters $i$ and $j$ have in common.

### Agreement from Cluster Overlaps i.e. Contingency Table

 Now for computing the overlaps between two different clustering $U$ and $V$, we can simply compute: $N= (U^TV)_{k \times r} $ which we write $N$ for short. The original rand index, is then defined in terms of this contingency matrix. More specifically assume $\varphi(x)= x(x-1)/2$, then consider these four terms:
$$\sum_{ij} \varphi(n_{ij}) = sum(\varphi(N))  = \mathbb{1}_{1\times k} \times \varphi(N) \times \mathbb{1}_{r \times 1} ,\; and \quad \sum_{i} \varphi(\sum_{j} n_{ij}) = sum(\varphi(sum(N,1)))  = \mathbb{1}_{1\times k} \times \varphi(N \times \mathbb{1}_{r \times 1} )$$
$$\sum_{j} \varphi(\sum_{i} n_{ij}) = sum(\varphi(sum(N,0)))  = \varphi(\mathbb{1}_{1\times k} \times N) \times \mathbb{1}_{r \times 1} ,\; and \quad \varphi(\sum_{ij}  n_{ij}) = \varphi(sum(N))  = \varphi(\mathbb{1}_{1\times k} \times N \times \mathbb{1}_{r \times 1}) $$
where $\mathbb{1}$ is a identity vector with appropiate shape, written afterward simply as $\mathbb{1}$.
The rand index which is originally defined as:
$RI =1 + [2\sum_{i=1}^k\sum_{j=1}^r n_{ij}(n_{ij}-1) - \sum_{i=1}^k n_{i.}(n_{i.}-1)- \sum_{j=1}^r n_{.j}(n_{.j}-1))]/[n(n-1)]$; can be re-written as:
$RI =1 +
[\mathbb{1} \varphi(N) \mathbb{1}  - \mathbb{1} \varphi(N \mathbb{1} ) ]/[\varphi(\mathbb{1} N \mathbb{1}) ] + [\mathbb{1}  \varphi(N) \mathbb{1}  - \varphi(\mathbb{1} N) \mathbb{1} ]/[\varphi(\mathbb{1} N \mathbb{1}) ] $.
We can similarly derive the adjusted for chance version of rand index. The generalized forms are:
            $$ \mathcal{G}_{\varphi} = \frac{\mathbf{1} \varphi(N) \mathbf{1}^T  - \mathbf{1} \varphi(N \mathbf{1}^T )   } {\varphi(\mathbf{1} N \mathbf{1}^T) } + \frac{\mathbf{1} \varphi(N) \mathbf{1}^T - \varphi(\mathbf{1} N) \mathbf{1}^T } {\varphi(\mathbf{1} N \mathbf{1}^T) }  $$$$ A\mathcal{G}_{\varphi} =\frac{\mathbf{1}  \varphi(N)  \mathbf{1}^T  -  E}{M-E} ,\quad M = \frac{ \mathbf{1} \varphi(N \mathbf{1}^T ) +  \varphi(\mathbf{1} N) \mathbf{1}^T  }{2 } ,\; E = \frac{ (\mathbf{1}  \varphi(N  \mathbf{1}^T ))  ( \varphi(\mathbf{1} N ) \mathbf{1}^T  )}{\varphi(\mathbf{1}  N \mathbf{1}^T) } $$

```
def agreement_from_overlaps(U,V, f = lambda x: x*(x-1)*0.5, exact=False):
    """
    Computes the agreement between two disjoint clusterings based on the overlap of their clusters.
    Parameters
    ----------
    U: numpy matrix
        A nxk matrix representing a clustering,
        where n is the number of data points and k is the number of clusters in U,
        so that U_{ik} shows if the ith data-point belongs to the kth cluster in U.
    V: numpy matrix
       A nxr matrix representing a clustering similar to U
    f: function
        A Python function (non-linear) to apply on the overlap sizes to compute the divergence,
        f = lambda x: x*(x-1)*0.5 (default) will result in Rand Index and Adjusted Rand Index,
        f = lambda x: 0 if x==0 else (x * math.log(x)) derives  normalized VI.

    Returns
    -------
        G: float
            agreement between input clusterings from
            $$\GAM_{\varphi} = \frac{\mathbf{1} \varphi(N) \mathbf{1}^T  - \mathbf{1} \varphi(N \mathbf{1}^T )   } {\varphi(\mathbf{1} N \mathbf{1}^T) } + \frac{\mathbf{1} \varphi(N) \mathbf{1}^T - \varphi(\mathbf{1} N) \mathbf{1}^T } {\varphi(\mathbf{1} N \mathbf{1}^T) }  $$
        AG: float
            agreement between input clusterings from
            $$\AG_{\varphi} =\frac{\mathbf{1}  \varphi(N)  \mathbf{1}^T  -  E}{M-E} ,\quad M = \frac{ \mathbf{1} \varphi(N \mathbf{1}^T ) +  \varphi(\mathbf{1} N) \mathbf{1}^T  }{2 } ,\; E = \frac{ (\mathbf{1}  \varphi(N  \mathbf{1}^T ))  ( \varphi(\mathbf{1} N ) \mathbf{1}^T  )}{\varphi(\mathbf{1}  N \mathbf{1}^T) } $$
    """
    return 1- distance_from_overlaps(U, V, f, exact)

def distance_from_overlaps(U,V, f = lambda x: x*(x-1)*0.5, exact=False):
    f = np.vectorize(f, otypes=[np.float])
    N = np.dot(U.T,V) # the overlaps, a.k.a contingency table

    b2 = np.ones((N.shape[1],1),dtype='float')
    b1 = np.ones((1, N.shape[0]),dtype='float')

    m1=b1.dot(N)
    m2=N.dot(b2)
    m = b1.dot(N).dot(b2)
    s1 = (b1.dot(f(m2)))
    s2 = (f(m1).dot(b2))
    n = f(m)
    I = (b1.dot(f(N)).dot(b2))

    # or
    # s1 = f(N.sum(axis=1)).sum()
    # s2 = f(N.sum(axis=0)).sum()
    # n =  f(N.sum())
    # I =  f(N).sum()

    G =  float ((s1+s2- 2* I  )/( n ) )
    if exact:
        E = (b1.dot(f(m2).dot(f(m1))/f(m))).dot(b2)
    else:
        E = (b1.dot(f(m2.dot(m1)/m))).dot(b2)
    AG = float ((s1+s2- 2* I  )/( (s1+s2) - 2 *E ))
    return  np.array([G, AG])
```

#### Example

```
V = np.array([[1,0],[1,0],[1,0],[0,1],[0,1],[0,1],[0,1],[1,0],[1,0],[1,0]])
U = np.array([[1,0,0],[1,0,0],[1,0,0],[1,0,0],[0,1,0],[0,1,0],[0,1,0],[0,0,1],[0,0,1],[0,0,1]])
figure(figsize=(9, 9))
nr = 2
nf = 2
subplot(nr,nf,1)#.set_title('$U$')
draw_clustered_graph(G,V, fix_pos)
subplot(nr,nf,2)#.set_title('$V1$')
draw_clustered_graph(G,U, fix_pos)

print  'RI = %0.3f, ARI = %0.3f'% tuple(agreement_from_overlaps(U,V, exact=True))
print  'VI = %0.3f, NMI (_sum, VM in sklearn) = %0.3f'% tuple(agreement_from_overlaps(U,V, f = lambda x: 0 if x==0 else x*log(x), exact=False))
print  'RI\' = %0.3f, ARI\' = %0.3f'% tuple(agreement_from_overlaps(U,V,f = lambda x: x**2, exact=False))

```

##### Compare it with scikit

```

def convert_cluster_to_labels(U):
    return np.nonzero(U)[1]
def sklearn_measures(U, V):
    #     http://scikit-learn.org/stable/modules/classes.html#clustering-metrics
    import sklearn.metrics.cluster as sym
    U_labels = np.nonzero(U)[1]
    V_labels = np.nonzero(V)[1]
   # print U_labels, V_labels

    print  'From sklean:: ARI = %0.3f'% sym.adjusted_rand_score(U_labels, V_labels),
    print  'NMI = %0.3f'% sym.normalized_mutual_info_score(U_labels, V_labels),
    print  'AMI = %0.3f'% sym.adjusted_mutual_info_score(U_labels, V_labels),
    print  'VM = %0.3f'% sym.v_measure_score(U_labels, V_labels)

sklearn_measures(U,V)
```

    From sklean:: ARI = 0.312 NMI = 0.523 AMI = 0.327 VM = 0.509

#### NMI

We can also derive NMI directly from our matrix representation

```
def nmi(U,V):
    N = np.dot(U.T,V) # the overlaps, a.k.a contingency table

    b2 = np.ones((N.shape[1],1),dtype='float')
    b1 = np.ones((1, N.shape[0]),dtype='float')

    n = b1.dot(N).dot(b2)
    nv= b1.dot(N)
    nu= N.dot(b2)

    f = np.vectorize(lambda x: x*math.log(x) if x >0 else 0, otypes=[np.float])
    Hu = -(b1.dot(f(nu/n)))
    Hv = -(f(nv/n).dot(b2))
    Huv = -(b1.dot(f(N/n)).dot(b2))
    Iuv = Hu+Hv-Huv

    vi = float((2*Huv-(Hu+Hv) )/math.log(n))
    nmi_sum = float(2*Iuv/(Hu+Hv)) # VM in sklearn
    nmi_sqrt = float(Iuv/math.sqrt(Hu*Hv)) # NMI in sklearn
    # 1-VI to make it into a agreement measure
    return  np.array([1-vi, nmi_sum, nmi_sqrt])
```

```
print '1-VI = %0.3f, NMI_sum = %0.3f, NMI_sqrt = %0.3f'% tuple(nmi(U,V))

```

    1-VI = 0.624, NMI_sum = 0.509, NMI_sqrt = 0.523

### Overlap from Co-memberships

The two square matrices of $UU^T$ and $VV^T$ show the co-membership of pair of nodes in clusterings $U$ and $V$ respectively. Therefore the difference of these two will give us the disagreement between $U$ and $V$, i.e. $\Delta = UU^T - VV^T $. We can show that the $RI$ and $ARI$, can be derived from $\Delta$ as:
$$(A)RI = 1 - \frac{|| UU^T - VV^T ||^2_F}{MAX}\quad where$$
$$ MAX_{RI}=   max(max(UU^T),max(VV^T)) n^2    \;\quad  and \quad\;
   MAX_{ARI}=||UU^T||^2_F+||VV^T||^2_F-2 |UU^T||VV^T| / (n^2)$$

```

def agreement_from_co_memberships(U, V, same_nodes = True):
    return 1-distance_from_co_memberships(U, V, same_nodes)

def distance_from_co_memberships(U, V, same_nodes = True):

    """
    Computes the agreement between two clusterings based on the co_memberships of data points in their clusters.
    Parameters
    ----------
    U: Matrix
        A nxk matrix representing a clustering,
        where upper is the number of data points and k is the number of clusters in U,
        so that U_{ik} shows if the ith data-point belongs to the kth cluster in U.
    V: Matrix
        A nxr matrix representing a clustering similar to U

    same_nodes: Boolean
        determines whether the formula should be exact same as the original RI formula or the approximate forms

    Returns
    -------
        G: float
            agreement between input clusterings from
            $$\GAM_{\varphi} = 1 - \frac{|| UU^T - VV^T ||}{MAX}$$
            where MAX is $$MAX= \mathfrak{m} upper^2$$ and  $$ \mathfrak{m} = max(max(UU^T),max(VV^T))$$
        AG: float
            agreement between input clusterings from same formula as G but where MAX is
            $$MAX=||UU^T||+||VV^T||-2 ||UU^T||||VV^T|| / (\mathfrak{m} upper^2)$$.
    """

    CU = U.dot(U.T).astype('float')
    CV = V.dot(V.T).astype('float')
    #print CU
    sf = lambda A: np.multiply(A, A).sum() #squared frob norm
    norm = sf
    if not same_nodes:
        np.fill_diagonal(CU,0)
        np.fill_diagonal(CV,0)

    pairCount =  CU.shape[0]**2 - (0 if same_nodes else CU.shape[0])

    sUV = norm( CU-CV  )
    sU = norm(CU)
    sV = norm(CV)
    m = max(np.max(CU),np.max(CV))

    GI =  sUV /(pairCount*m**2)
    AGI = sUV / ((sU+sV) - 2*(np.sum(CU)*np.sum(CV))/(pairCount))
    return  np.array([GI, AGI])

```

```
print  'RI = %0.3f, ARI = %0.3f'% tuple(agreement_from_co_memberships(U,V, False))
print  'RI\' = %0.3f, ARI\' = %0.3f'% tuple(agreement_from_co_memberships(U,V, True))

```

    RI = 0.667, ARI = 0.312
    RI' = 0.700, ARI' = 0.408

#### The advantage to this formulation is that it extends to overlapping clusters.

## OMEGA

Collins and Dent (1988) proposed the Omega index as a generalization of the (adjusted) rand index for overlapping clusters with crisp memberships. The Omega Index($\omega$) derives from our formulation if we define $\Delta = [UU^T == VV^T]$. Since in $\omega$, each pair of datapoints is considered as an agreement if they appear in  exactly the same number of clusters in the two clusterings,
 i.e. $\Delta_{ij} =1$ if $(UU^T)_{ij} == (VV^T)_{ij}$ and zero otherwise.

```
def agreement_from_omega(U, V, same_nodes = False):
    CU = U.dot(U.T).astype('float')
    CV = V.dot(V.T).astype('float')
    if not same_nodes:
        np.fill_diagonal(CU,0)
        np.fill_diagonal(CV,0)
    pairCount =  CU.shape[0]**2 - (0 if same_nodes else CU.shape[0])
    pair_counts = np.zeros((int(np.max(CU))+1,int(np.max(CV))+1))
    for i in range(0, int(np.max(CU)+1)):
        for j in range(0, int(np.max(CV)+1)):
            tmp = (CU==i)*(CV==j)
            tmp = np.tril(tmp) if same_nodes else np.tril(tmp,-1)
            pair_counts[i,j] = np.sum(tmp)

    w=  np.array( CU==CV, dtype='float')
    w = np.sum(w)- w.trace()
    w = w*1.0/ pairCount

    tU = np.histogram(CU, bins=np.max(CU)+1)[0]
    tV = np.histogram(CV, bins=np.max(CV)+1)[0]

    tU[0]= tU[0] - (0 if same_nodes else CU.shape[0])
    tV[0]= tV[0] - (0 if same_nodes else CU.shape[0])

    m =  min(tU.shape[0], tV.shape[0])
    ew = [tU[j]*tV[j] for j in range(0 ,m)]
    ew = np.sum(ew)*1.0 / (pairCount**2)

    aw = (w-ew)/(1-ew)

    return  np.array([w, aw])

```

```
print  'w = %0.3f, Aw = %0.3f'% tuple(agreement_from_omega(U,V, False))

```

    w = 0.667, Aw = 0.312

### Alternative Forms:

#### Alternative Formulation From Matrix Norm:

```
def agreement_from_matrix_norms(U,V, norm = lambda A: math.sqrt(np.multiply(A, A).sum()) ):
    return 1-distance_from_matrix_norms(U, V, norm)

def distance_from_matrix_norms(U,V, norm = lambda A: math.sqrt(np.multiply(A, A).sum()) ):
#     L21_norm  = lambda A: np.sqrt(np.multiply(A, A).sum(axis=1)).sum()
#     frobenius_norm = lambda A: math.sqrt(np.multiply(A, A).sum())
#     log_norm = lambda A: np.multiply(A, np.log(A.clip(1)).sum(axis=1)).sum()
    CU = U.dot(U.T).astype('float')
    CV = V.dot(V.T).astype('float')
    return np.array([norm(CU-CV)/(norm(CU)+norm(CV)) ])

print  'I_norm = %0.3f'% tuple(agreement_from_matrix_norms(U,V))
```

    I_norm = 0.580

#### Alternative Formulation From Matrix Trace:

```
def distance_alternative_forms(U,V):
    return 1-agreement_alternative_forms(U, V)

def agreement_alternative_forms(U, V):
    CU = U.dot(U.T).astype('float')
    CV = V.dot(V.T).astype('float')

    CU2 = CU.dot(CU)
    CV2 = CV.dot(CV)

    Q = CU.dot(CV)
    # Cauchy-Schwarz inequality ==> alt1<1
    alt1 =   float(Q.trace()) / math.sqrt( (CU2).trace() * (CV2).trace() )
    alt2 =  float(Q.trace() / (CU.trace() * CV.trace()))
    return  np.array([alt1, alt2])

print  'I_sqrt(tr) = %0.3f, I_tr = %0.3f'% tuple(agreement_alternative_forms(U,V))

```

    I_sqrt(tr) = 0.666, I_tr = 0.280

### Summary of all measures up to here

```
def get_all_overlapping_agreement_variations(U, V):
    dists=  get_all_overlapping_distance_variations(U,V)
    return [dists[0], 1-dists[1]]

def get_all_overlapping_distance_variations(U, V):
    res= ["RI","ARI","RI'","ARI'", "nF", "sqtr","tr" ] ,\
        np.hstack((distance_from_co_memberships(U,V, False),\
            distance_from_co_memberships(U,V),\
            distance_from_matrix_norms(U,V) ,\
            distance_alternative_forms(U, V)
                   ))
    return res

from tabulate import tabulate
Res = get_all_overlapping_agreement_variations(U, V)
headrs = np.hstack( ([" - "], Res[0]))
vals = [ tuple(["(V,U)"]+Res[1].tolist()) ]
vals = np.asarray(vals, dtype=dict(names = headrs, formats=["a18"]+["float32"]*len(Res[1]) ))
print tabulate(vals, headers = "keys",  floatfmt=".3f")

```

     -        RI    ARI    RI'    ARI'     nF    sqtr     tr
    -----  -----  -----  -----  ------  -----  ------  -----
    (V,U)  0.667  0.312  0.700   0.408  0.580   0.666  0.280

## Structure Based Measures

All the agreement measures presented so far only consider memberships of datapoints, and ignore any relations between them. Here we define structure dependent clustering distances which incorporate the
underlying structure of the graph.

### Incident Matrix of the Graph to Represent Underlying Structure

```
def get_incident_matrix(n, edges, directed =False):
    N = np.zeros((n, len(edges)), dtype =np.float)
    for j,e in enumerate(edges):
        w = 1 if len(e)<3 else np.sqrt(e[2])
        N[e[0]][j] = w
        N[e[1]][j] = w * (-1 if directed else 1)
    return N

def get_incident_matrix_from_graph(G, directed =False):
    #N = gm.incidence_matrix(G)
    edges = [(i,j, w['weight']) for (i,j, w) in G.edges(data=True)]
    return get_incident_matrix(G.number_of_nodes(), edges,directed)

def get_incident_matrix_from_adjecency(A, directed =False):
    edges =  np.nonzero(A if directed else np.tril(A))
    weighted_edges = np.vstack((edges, A[edges]))
    return get_incident_matrix(A.shape[0], np.transpose(weighted_edges), directed)

```

### Distance of a Clustering From Structure

```

def distance_with_structure(G,U):
    N = get_incident_matrix_from_graph(G)
    return get_all_distance_variations(N, U)

def agreement_with_structure(G,U):
    dists=  distance_with_structure(G,U)
    return [dists[0], 1-dists[1]]

```

### Distance of Two Clustering Given the Structure

```
def agreement_based_on_structure(G, U, V, method = "trans"):
    dists=  distance_based_on_structure(G, U, V, method)
    return [dists[0], 1-dists[1]]

def distance_based_on_structure(G, U, V, method = "trans"):
#     A = gm.adj_matrix(G)
    N = get_incident_matrix_from_graph(G)
    # avg dis
    if method=="trans":
        return get_all_distance_variations(N.T.dot(U), N.T.dot(V))
    else:
        names, d = get_all_distance_variations(U,V)
        d1 = get_all_distance_variations(U,N)[1]
        d2 = get_all_distance_variations(V,N)[1]
        if method == "sum":
            alpha =0.5
            return [d1[0], alpha*d+ (1-alpha)*np.absolute(d1-d2) ]
        elif method == "prod":
            return [d1[0], 1./3*(np.sqrt(d*d1)+np.sqrt(d*d2)+np.sqrt(d1*d2))]

    return float( (U.dot(U.T).dot(N.dot(N.T)).dot(V.dot(V.T))).trace()  /\
                  math.sqrt(   (U.dot(U.T).dot(N.dot(N.T)).dot(U.dot(U.T))).trace() *\
                            (V.dot(V.T).dot(N.dot(N.T)).dot(V.dot(V.T))).trace()  )\
                 )

```

#### The trans method for structured based meaures, in picture

```
def transformation():
    figure(figsize=(10, 5))
    A = np.zeros((9,9),dtype='float')
    edges = [(0,1),(0,5),(0,6),
              (1,0),(1,2),(1,5),
              (2,1),(2,3),(2,5),
              (3,2),(3,5),(3,4),
              (4,3),(4,5),
              (5,0),(5,1),(5,2),(5,3),(5,4),(5,6),(5,8),
              (6,0),(6,5),(6,7),(6,8),
              (7,6),(7,8),
              (8,6),(8,7),(8,5),
              ]
    for e in edges:
        A[e]=1

    fix_pos = {0: np.array([ 0.20385061-0.1,  0.23977043-0.1]),
               1: np.array([ 0.43693314,  0.        ]),
               2: np.array([ 0.75900616,  0.01462725]),
               3: np.array([ 0.97478493,  0.24577521-0.1]),
               4: np.array([ 1.        ,  0.53455287-0.1-0.1]),
               5: np.array([ 0.56256428+0.05,  0.38383768 +0.1]),
               6: np.array([ 0.16250501-0.1,  0.59711573]),
               7: np.array([ 0.        ,  0.96007752]),
               8: np.array([ 0.32060991,  0.8106598 ])}

    edges =  transpose(np.nonzero(np.tril(A)))
    edg_pos ={}
    for j,e in enumerate(edges):
        edg_pos[j]= 0.5*(fix_pos[e[0]]+fix_pos[e[1]])
  #  print edg_pos
    U = np.array([[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[0,1],[0,1],[0,1]])
    V1 = np.array([[0,1],[1,0],[1,0],[1,0],[1,0],[1,0],[0,1],[0,1],[0,1]])
    V2 = np.array([[1,0],[1,0],[1,0],[1,0],[1,0],[0,1],[0,1],[0,1],[0,1]])

    N = get_incident_matrix(A.shape[0],edges)

    G=nx.from_numpy_matrix(A)#=N.dot(N.T)
    LG=nx.from_numpy_matrix(N.T.dot(N)) #Line graph

    T = N.T.dot(U)
    TRA = T.dot(T.T)
#     nx.write_dot(LG,'graph.dot')

    subplot(2,4,1)
    draw_clustered_graph(G ,U, pos= fix_pos,draw_graph = True)
    subplot(2,4,2)
    draw_clustered_graph(G ,V1, pos= fix_pos,draw_graph = True)
    subplot(2,4,3)
    draw_clustered_graph(G ,V2, pos= fix_pos,draw_graph = True)

    subplot(2,4,6)
    draw_clustered_graph(nx.from_numpy_matrix((N.T.dot(U)).dot(U.T.dot(N))) ,N.T.dot(U), pos= edg_pos,draw_graph = False)
    subplot(2,4,7)
    draw_clustered_graph(nx.from_numpy_matrix((N.T.dot(V1)).dot(V1.T.dot(N))) ,N.T.dot(V1), pos= edg_pos,draw_graph = False)
    subplot(2,4,8)
    draw_clustered_graph(nx.from_numpy_matrix((N.T.dot(V2)).dot(V2.T.dot(N))) ,N.T.dot(V2), pos= edg_pos,draw_graph = False)
    show()

transformation()
```

## Examples

```
def experiment(A,V,U,U2=None, fix_pos=None):
    G = nx.from_numpy_matrix(A)
    plotTable = False
    nr = 2 if plotTable else 1
    nf = 2 #numFigs
    if not U2==None:
        nf =3
    if fix_pos==None:
        fix_pos = nx.spring_layout(G)#, nlist, dim, scale)spring_layout(G)
        print '----- positions to repeat the layout:: \n ',fix_pos

    figure(figsize=(9, 4))
    subplot(nr,nf,1)#.set_title('$U$')
    draw_clustered_graph(G,V, fix_pos)
    subplot(nr,nf,2)#.set_title('$V1$')
    draw_clustered_graph(G,U, fix_pos)
    if not U2 == None:
        subplot(nr,nf,3)#.set_title('$V2$')
        draw_clustered_graph(G,U2, fix_pos)
    show()

    Res = get_all_agreement_variations(V, U)
    headrs = np.hstack( ([" - "], Res[0]))
    vals = [ tuple(["(V,U_1)"]+Res[1].tolist()) ]
    if U2 is not None:
        vals = vals + [ tuple(["(V,U_2)"]+get_all_agreement_variations(V, U2)[1].tolist()) ]

    vals = vals + [ tuple(["(N,V)"]+agreement_with_structure(G, V)[1].tolist()) ]
    vals = vals + [ tuple(["(N,U_1)"]+agreement_with_structure(G, U)[1].tolist()) ]
    if not U2==None:
        vals = vals + [ tuple(["(N,U_2)"]+agreement_with_structure(G, U2)[1].tolist()) ]

    for m in ["trans","sum"]:
        vals = vals + [ tuple(["(V,U_1|G)"+m[0]]+agreement_based_on_structure(G, V,U, method=m)[1].tolist()) ]
        if not U2==None:
            vals = vals + [ tuple(["(V,U_2|G)"+m[0]]+agreement_based_on_structure(G, V,U2, method=m)[1].tolist()) ]

    vals = np.asarray(vals, dtype=dict(names = headrs, formats=["a18"]+["float32"]*len(Res[1]) ))
    print tabulate(vals, headers = "keys", tablefmt="grid",  floatfmt=".3f")

    if plotTable:
        args = { "tablefmt":"plain",  "floatfmt":".3f", "numalign":"right"}
        ax = plt.subplot2grid((2,nf), (1,0), colspan=nf)
        plt.axis('off')
        plt.text(0,0, tabulate(vals, headers = "keys", **args))#,backgroundcolor = 'y',alpha=1)
    show()
```

### Disjoint Example

```

def basic_example():
    A = np.array([[0,1,1,0,0,0,0,0,0,0],
            [1,0,0,1,0,0,0,0,0,0],
            [1,0,0,1,0,0,0,1,0,0],
            [0,1,1,0,1,1,0,0,0,0],
            [0,0,0,1,0,1,1,0,0,0],
            [0,0,0,1,1,0,1,0,0,0],
            [0,0,0,0,1,1,0,0,0,0],
            [0,0,1,0,0,0,0,0,1,1],
            [0,0,0,0,0,0,0,1,0,1],
            [0,0,0,0,0,0,0,1,1,0]])
    fix_pos_GraphExample = {0: array([ 0.05,  .35]),
                            1: array([ 0.02,  0.62]),
                            2: array([ 0.25,  0.4]),
                            3: array([ 0.25,  0.73]),
                            4: array([ .5,  0.9]),
                            5: array([ 0,  0.9]),
                            6: array([ 0.25,  1 ]),
                            7: array([ 0.25,  0.1]),
                            8: array([ 0,  0.   ]),
                            9: array([ .5, 0])}

    return A, fix_pos_GraphExample

A, fix_pos_GraphExample = basic_example()
V = np.array([[1,0],[1,0],[1,0],[0,1],[0,1],[0,1],[0,1],[1,0],[1,0],[1,0]])
U = np.array([[1,0,0],[1,0,0],[1,0,0],[1,0,0],[0,1,0],[0,1,0],[0,1,0],[0,0,1],[0,0,1],[0,0,1]])
experiment(A,U,V, fix_pos=fix_pos_GraphExample)
```

### Overlap Example

```
A, fix_pos_GraphExample = basic_example()
U = np.array([[1,0,0],[1,0,0],[1,0,0],[1,1,0],[0,1,0],[0,1,0],[0,1,0],[0,0,1],[0,0,1],[0,0,1]])
V = np.array([[1,0,0],[1,0,0],[1,0,0],[.6,.4,0],[0,1,0],[0,1,0],[0,1,0],[0,0,1],[0,0,1],[0,0,1]])
V2 = np.array([[2,0,0],[2,0,0],[1,0,0],[1,1,0],[0,2,0],[0,2,0],[0,3,0],[0,0,2],[0,0,2],[0,0,2]])
experiment(A,U,V,V2, fix_pos=fix_pos_GraphExample)
```

### Matching Example

```
A = np.zeros((11,11),dtype='float')
for e in [(0,1),(0,3),(0,4),
          (1,2),(1,3),(1,4),
          (2,3),
          (3,4),
          (5,6),(5,7),
          (6,7),
          (7,8),
          (8,9),(8,10),
          (9,10)
          ]:
    A[e]=A[(e[1],e[0])]=1

fix_pos_GraphExample =   {0: array([ 0.5/2 ,  0.95]),
                          1: array([ 1./2  ,  0.59]),
                          2: array([ 0.81/2,  0.  ]),
                          3: array([ 0.19/2,  0.  ]),
                          4: array([ 0.  ,  0.59]),
                          5: array([ 0.74,  1.  ]),
                          6: array([ 1,  1.  ]),
                          7: array([ 0.88,  0.69]),
                          8: array([ 0.88,  0.35]),
                          9: array([ 0.74,  0]),
                          10: array([ 1,  0])}

np.set_printoptions(precision=3,suppress=True)

U = np.array([[1,0,0],[1,0,0],[1,0,0],[1,0,0],[1,0,0],\
               [0,1,0],[0,1,0],[0,1,0],[0,0,1],[0,0,1],[0,0,1]])
V1 = np.array([[1,0],[1,0],[1,0],[1,0],[1,0],\
               [0,1],[0,1],[0,1],[0,1],[0,1],[0,1]])
V2 = np.array([[1,0,0],[0,0,1],[0,0,1],[1,0,0],[1,0,0],\
               [0,1,0],[0,1,0],[0,1,0],[0,0,1],[0,0,1],[0,0,1]])
sklearn_measures(U, V1)
sklearn_measures(U, V2)

print '---', nmi(U,V1)
print '---', nmi(U,V2)

experiment(A,U,V1,V2,  fix_pos=fix_pos_GraphExample)

```

### Graph Example

```
A = np.zeros((9,9),dtype='float')
for e in [(0,1),(0,5),(0,6),
          (1,0),(1,2),(1,5),
          (2,1),(2,3),(2,5),
          (3,2),(3,5),(3,4),
          (4,3),(4,5),
          (5,0),(5,1),(5,2),(5,3),(5,4),(5,6),(5,8),
          (6,0),(6,5),(6,7),(6,8),
          (7,6),(7,8),
          (8,6),(8,7),(8,5),
          ]:
    A[e]=1

fix_pos_GraphExample = {0: np.array([ 0.20385061-0.1,  0.23977043-0.1]),
           1: np.array([ 0.43693314,  0.        ]),
           2: np.array([ 0.75900616,  0.01462725]),
           3: np.array([ 0.97478493,  0.24577521-0.1]),
           4: np.array([ 1.        ,  0.53455287-0.1-0.1]),
           5: np.array([ 0.56256428+0.05,  0.38383768 +0.1]),
           6: np.array([ 0.16250501-0.1,  0.59711573]),
           7: np.array([ 0.        ,  0.96007752]),
           8: np.array([ 0.32060991,  0.8106598 ])}

U = np.array([[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[0,1],[0,1],[0,1]])
V1 = np.array([[0,1],[1,0],[1,0],[1,0],[1,0],[1,0],[0,1],[0,1],[0,1]])
V2 = np.array([[1,0],[1,0],[1,0],[1,0],[1,0],[0,1],[0,1],[0,1],[0,1]])

experiment(A,U,V1,V2,  fix_pos=fix_pos_GraphExample)
```

### Omega Example

```
A = np.zeros((5,5),dtype='float')
for e in [(0,1),(0,4),
          (1,0),(1,2),
          (2,1),(2,3),
          (3,2),(3,4),
          (4,3),(4,0),
          ]:
    A[e]=1

fix_pos_GraphExample =   {0: array([ 0.49682607,  0.95151713]),
                          1: array([ 1.        ,  0.59183855]),
                          2: array([  8.12052312e-01,   3.19389103e-05]),
                          3: array([ 0.19258776,  0.        ]),
                          4: array([ 0.        ,  0.58628989])}

U = np.array([[1,0,0],[0,1,0],[0,1,1],[1,1,1],[1,0,1]])
V1 = np.array([[1,0,0],[0,1,0],[0,0,1],[1,0,0],[1,0,0]])
V2 = np.array([[1,0,0],[0,1,0],[0,0,1],[1,0,1],[1,0,0]])
print 'V_1', agreement_from_omega(U, V1)
print 'V_2', agreement_from_omega(U, V2)
experiment(A,U,V1,V2,  fix_pos=fix_pos_GraphExample)

```


```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.style.use('classic')
%matplotlib inline
```

# Class 7: Deterministic Time Series Models I

Time series models are at the foundatation of dynamic macroeconomic theory. A time series model is an equation or system of equations that describes how the variables in the model change with time. Here, we examine some theory about **deterministic**, i.e., non-random, time series models and we explore methods for simulating them. Later, we'll examine the properties of **stochastic** time series models by introducing random variables to the discrete time models covered below.

## Discrete Versus Continuous Time

To begin, suppose that we are interested in a variable $y$ that takes on the value $y_t$ at date $t$. The date index $t$ is a real number. We'll say that $y_t$ is a **discrete time** variable if $t$ takes on values from a countable sequence; e.g. $t = 1, 2, 3 \ldots$ and so on. Otherwise, if $t$ takes on values from an uncountable sequence; e.g. $t\in \mathbb{R}^+$, then we'll say that $y_t$ is a **continuous time** variable. Discrete and continuous time models both have important places in macroeconomic theory, but we're going to focus on understanding discrete time models.

## First-Order Difference Equations

Now, suppose that the variable $y_t$ is determined by a linear function of $y_{t-1}$ and some other exogenously given variable $w_t$

\begin{align}
    y_{t} & =  \rho y_{t-1} + w_t,  \tag{1}\\
\end{align}

where $\rho$ is a constant. Equation (1) is an example of a **linear first-order difference equation**. As a *difference equation*, it specifies how $y_t$ is related to past values of $y$. The equation is a *first-order* difference equation because it specifies $y_t$ as a function only of $y_{t-1}$ and not $y_{t-2}$ or $y_{t-3}$ and so on. As simple as the equation looks, it's used in a wide range of modeling applications.

### Example: Compounding Interest

Suppose that you have an initial balance of $b_0$ dollars in a savings account that pays an interest rate $i$ per compounding period. Then, after the first compounding, your account will have $b_1 = (1+i)b_0$ dollars in it. Assuming that you never withdraw funds from the account, then your account balance in any subsequent period $t$ is given by the following difference equation:

\begin{align}
b_{t} & =  \left(1+i\right) b_{t-1}. \tag{2}
\end{align}

Equation (2) is linear first-order difference equation in the same form as Equation (1). You can see this by setting $y_t = b_t$, $\rho=1+i$, and $w_t=0$ in Equation (1).

### Example: Capital Accumulation

Let $K_t$ denote the amount of physical capital in a country at date $t$, let $\delta$ denote the rate at which the capital stock depreciates each period, and let $I_t$ denote the country's investment in new capital in date $t$. Then the law of motion for the stock of physical capital is:

\begin{align}
K_{t+1} & =  I_t + (1-\delta)K_t. \tag{3}
\end{align}

This standard expression for the law of motion for the capital stock is a linear first-order difference equation. To reconcile Equation (3) with Equation (1), set $y_t = K_{t+1}$, $\rho=1-\delta$, and $w_t=I_t$.

*Note*: There is a potentially confusing way in which we identified the $t+1$-dated variable $K_{t+1}$ with the $t$-dated variable $y_t$ in this example. We can do this because the value of $K_{t+1}$ truly is determined at date $t$ even though the capital isn't used for production until the next period.

## Computation

From Equation (1), it's easy to compute the value of $y_t$ as long as you know the value of the constant $\rho$ and the variables $y_{t-1}$ and $w_t$. To begin, let's suppose that $\rho=0.5$. Then Equation (1) in our example looks like this:

\begin{align}
y_{t} & =  0.5 y_{t-1} + w_t. \tag{4}\\
\end{align}

Now, suppose that the initial value of $y$ is $y_0=0$ and that $w$ is equal to 1 in the first period and equal to zero in subsequent periods. That is: $w_1=1$ and $w_2=w_3=\cdots =0$. Now, with what we have, we can compute $y_1$. Here's how:

```python
# Initialize values for rho, y0, and w1

# Compute the value of y1

# Print the value of y1

```

The variable `y1` in the preceding example stores the computed value for $y_1$. We can continue to *iterate* on Equation (4) to compute $y_2$, $y_3$, and so on. For example:

```python
# Initialize value for w2

# Compute the value of y2

# Print the result

```

We can do this as many times as necessary to reach the desired value of $t$. Note that iteration is necesary. There is no *vectorized* way to compute an array of values $\left[y_1, y_2, y_3, \ldots, y_{T}\right]$ from a difference equation simultaneously. The linear first-order difference equation is an example of a *recursive* model and in general iteration is necessary for computing recursive models.

Of course, there are smart ways to write code to simulate models. Rather than creating a bunch of variables `y1`, `y2`, etc., let's define a function called `diff1_example()`that takes as arguments $\rho$, $y_0$m and an array of values for $w$, and returns an array of simulated $y$ values . The function should:

1. Define a variable named `T` equal to the length of  `w`
2. Define an array of of length called `y` that is filled with zeros
3. Set the first vale of `y` to $y_0$
4. Fill-in the remaining values of `y` by iterating over:
        y[t+1]=rho*y[t]+w[t+1]
   for `t` in $[0,1,\ldots, T-1]$
5. Return `y`

```python
# Define a function that returns an array of y-values given rho, y0, and an array of w values.

```

### Example: Temporary Shock to $w_t$ with $\rho=0.5$

Now, use the function `diff1_example()` to simulate $y$ for 11 periods (i.e., for $t = 0, 1, \ldots 10$) with
1. $\rho=0.5$
2. $w_1 = 1$ and $w_0 = w_2 = \ldots = w_{10} = 0$
3. $y_0 = 0$

```python
# Initialize the variable 'T' to be equal to the number of periods to simulate

# Create new variable 'rho' equal to the value for rho

# Create new variable 'y0' equal to the initial value for y

# Initialize variable 'w' and set initial value

# Simulate y

# Construct figure with two line graphs plotting the trajectories of y and w with:
#    1. Plot for w in left panel and plot for y in right panel
#    2. Clear title, x-, and y-axis labels
#    3. y-axis limits [0,1.2]

# Create figure

# w trajectory

# y trajectory

# Use fig.tight_layout() to manage whitespace in the figure

```

In period 1, the *shock* to $w_t$ creates an exogenous increase in the process $y_t$. Since $y_{t+1}$ depends on the value of $y_{t}$, the transient or temporary shock to $w_t$ has a persistent effect on the series $y_t$. We call plots like those in the previous figure **impulse responses** because they depict the *responses* of variables to a one-time shock or *impulse* in the exogenous variable.

### Example: Temporary Shock to $w_t$ for Different Values of $\rho$

Next, use the function `diff1_example()` to simulate $y$ for 11 periods with:
1. $\rho=0.1,0.2, 0.3, \ldots, 0.9$
2. $w_1 = 1$ and $w_0 = w_2 = \ldots = w_{10} = 0$
3. $y_0 = 0$

```python
# Note: Since T, w, and y0 are already defined, there's no need to re-define them

# Construct a line graph plotting the trajectories of y with:
#    1. Clear title, x-, and y-axis labels
#    2. Y-axis limits [0,1.2]
#    3. Add a legend outside of the plot with arguments: loc='center left', bbox_to_anchor=(1, 0.5)

# Iterate over rho in [0.1, 0.2, 0.3, ... , 0.9]. Simulate y for each rho and plot. Set plot label equal to rho value

# ax.set_ylim([0,1.2])

```

Apparently increasing $\rho$ also increases the degree of persistence in the process $y$ following the shock to $w$. By $t=10$, the effects of the one-time increase in $w$ on $y$ have essentially dissipated for the simulations with $\rho= 0.1, 0.2, \ldots, 0.7$. But for $\rho=0.8$, the value of the $y$ is still above $0.15$ in period $10$ and for $\rho=0.9$, the process $y$ is nearly at $0.4$; i.e., nearly $40$ percent of the shock still remains. But regardless, in all cases it appears that $y$ approaches zero in the limit.

### Example: Temporary Shock to $w_t$ for $\rho=-0.5$

Use the function `diff1_example()` to simulate $y$ for 11 periods with:
1. $\rho=-0.5$
2. $w_1 = 1$ and $w_0 = w_2 = \ldots = w_{10} = 0$
3. $y_0 = 0$

```python
# Note: Since T, w, and y0 are already defined, there's no need to re-define them

# Create new variable 'rho' equal to the value for rho

# Simulate y

# Construct a line graph plotting the trajectory of y with:
#    1. Plot of y
#    2. Clear title, x-, and y-axis labels
#    3. Y-axis limits [-0.8,1.2]

# Create figure

# y trajectory

```

With $\rho=-0.5$, the sign of the pricess $y$ oscilates between positive and negative values but the absolute value of $y$ smoothly converges to zero in the limit.

### Example: Temporary Shock to $w_t$ for $\rho=1.5$ and $\rho=-1.5$

So far all of the values that we hav used for $\rho$ have been less than 1 in absolute value and in all cases, the process $y$ has returned to zero in the limit. But this may not always be the case. Let's consider the cases where $\rho=1.5$ and $\rho=-1.5$.

Use the function `diff1_example()` to simulate $y$ for 11 periods with:
1. $\rho=1.5$ (left panel) and $\rho=-1.5$ (right panel)
2. $w_1 = 1$ and $w_0 = w_2 = \ldots = w_{10} = 0$
3. $y_0 = 0$

```python
# Note: Since T, w, and y0 are already defined, there's no need to re-define them

# Create figure

# Set rho=1.5

# Simulate y with rho = 1.5

# plot trajectory of y with rho = 1.5

# Set rho=-1.5

# Simulate y with rho = -1.5

# plot trajectory of y with rho = -1.5

```

Unlike the earlier simulations, the ones in the previous example do not return to zero and instead either explode toward infinity ($\rho = 1.5$) or oscillate towards positive and negative infinity. These two simulations are examples of *explosive processes* while the ones prior were examples of *stable processes*. In the next section, I elaborate on the notion of stability.

### Example: Permanent Shock to $w_t$ with $\rho=0.5$

So far we hav eonly considered temporary shocks to to $wt$. But sometimes we want to know what effect does a permanent change in $w_t$ have on $y_t$. To answer this, we set $w_0=0$ and $w_1 = w_2 = \cdots = 1$.

Now, use the function `diff1_example()` to simulate $y$ for 11 periods (i.e., for $t = 0, 1, \ldots 10$) with
1. $\rho=0.5$
2. $w_1 = 0$ and $w_1 = w_2 = \ldots = w_{10} = 1$
3. $y_0 = 0$

```python
# Initialize the variable 'T' to be equal to the number of periods to simulate

# Create new variable 'rho' equal to the value for rho

# Create new variable 'y0' equal to the initial value for y

# Initialize variable 'w' as arrays of ones

# Simulate y

# Construct figure with two line graphs plotting the trajectories of y and w with:
#    1. Plot for w in left panel and plot for y in right panel
#    2. Clear title, x-, and y-axis labels
#    3. y-axis limits [0,2.5]

# Create figure

# w trajectory

# y trajectory

# Use fig.tight_layout() to manage whitespace in the figure

```

The cumulative effect on $y$ in period $T$ of the permanent change in $w_t$ is:

\begin{align}
y_T & = \rho^{T-1} + \rho^{T-2} + \rho^{T-3} + \cdots + \rho + 1\\
& = \frac{1}{1-\rho},
\end{align}

which, for the example above, is $2$ and that is confirmed by the simulation.

## Stability of First-Order Difference Equations

You might already see that the value of $\rho$ is important in determining the stability of the process $y$, but we can see clearly why. Suppose that $\mu=0$ (this just makes the argument more clear) and start with the equation for determining $y_1$:

\begin{align}
y_1 & = \rho y_0 + w_1
\end{align}

The value for $y_2$ is determined by an analogous equation:

\begin{align}
y_2 & = \rho y_1 + w_2
\end{align}

Then use the equation for $y_1$ to eliminate $y_1$ from the equation for $y_2$:

\begin{align}
y_2 & = \rho^2 y_0 + \rho w_1 + w_2
\end{align}

Continue this process to arrive at a formula that specifies the value of $y$ in period $T$ as a function of just $\rho$, $y_0$, and the values of $w$: $w_1, w_2, \ldots, w_T$:

\begin{align}
y_T & = \rho^T y_0 + \rho^{T-1} w_1 + \rho^{T-2} w_2 + \cdots + \rho w_{T-1} + w_T\\
\end{align}

Then, you can see that the marginal effect of $w_1$ on $y_T$ is:

\begin{align}
\frac{\partial y_T}{\partial w_1} & = \rho^{T-1}
\end{align}

From this, there are three facts:

1. If $|\rho|<1$, then the process $y$ is **stable** because $|\rho^{T-1}|\rightarrow 0$ as $T\rightarrow \infty$.
2. If $|\rho|>1$, then the process $y$ is **explosive** because $|\rho^{T-1}|\rightarrow \infty$ as $T\rightarrow \infty$.
2. If $|\rho|=1$, then the process $y$ is neither stable nor explosive because $|\rho^{T-1}|\rightarrow 1$ as $T\rightarrow \infty$.

The final case, with $|\rho^T|=1$, is closely related to the *random walk* stochastic process that is often used to describe finacial asset returns over short time horizons. We will encounter this process later.

Stability is a fundamental concept in economics and there are important roles for models with stable and explosive properties.

## Higher-order Difference Equations (Optional)

The $p$th-order linear difference equation is given by the following expression:

\begin{align}
y_t & = \rho_1 y_{t-1} + \rho_2 y_{t-2} + \cdots + \rho_p y_{t-p} + w_t,
\end{align}

where $\rho_1, \rho_2, \ldots, \rho_p$ are constants. In principle, computing the $p$th-order difference equation is the same as computing the first-order difference equation. The only difference real difference is that you need $p$ initial values of $y$ and not just 1. That is, in order to compute the period 1 value $y_1$, you need values for $y_0, y_{-1}, \ldots, y_{-(p-1)}$.

The function in the following cell simulates $p$-order difference equations so we can use it for the examples below.

```python
# Define a function that returns a DataFrame of y and w values given coefficients, y0, and an array of w values. CELL PROVIDED
def pth_order_diff(rho,y0,w):
    '''Function for computing the first-order difference equation:

            y[t] = C + rho[0]*y[t-1] + rho[1]*y[t-2] + ... + rho[p]*y[t-p] + w[t]

    Args:
        rho (NumPy ndarray or list):   Autoregressive coefficients on y[t-1], y[t-2], ... y[t-1]
        y0 (NumPy ndarray or list):    Initial value of the process (must be same length as rho)
        w (NumPy ndarray or list):     Array of exogenous values for t=0,..,T+1 where T is the end date

     Returns:
         NumPy ndarray with columns 'w' and 'y' and index: -(p-1), -(p-2), ..., T
     '''

    # Set order of the process. If rho is a list or array, set p to the length. Otherwise, p=1.
    try:
        p = len(rho)
    except:
        p = 1

    # Length of simulation
    try:
        T = len(w)
    except:
        T = 1

    # Construct series for y
    y = np.zeros(p+T-1)
    y[0:p] = y0

    # Construct series for w
    empty = np.empty(p-1)
    empty[:] = np.nan
    w = np.concatenate([empty,w[:T+1]])

    # Date index
    index = np.arange(-(p-1),T)

    # Initialize DataFrame
    df = pd.DataFrame({'w':w,'y':y},index=index)

    # Compute y
    for i in df.index[p:]:
        df.loc[i,'y'] = np.dot(rho,np.flip(df.loc[i-p:i-1,'y'])) + df.loc[i,'w']

    return df
```

### Example: A Second-Order Difference Equation

Consider the second order difference equation:

\begin{align}
y_t & = \rho_1 y_{t-1} + \rho_2 y_{t-2} + w_t,
\end{align}

As usual, take $w_1=1$ and $w_0=w_2=w_3 = \cdots$ and $y_0 = y_{-1} = 0$. Let's simulate the model with two different combinations of $\rho_1$ and $\rho_2$:

\begin{align}
y_t & = 0.5 y_{t-1} + 0.2 y_{t-2} + w_t,
\end{align}

and:

\begin{align}
y_t & = 0.6 y_{t-1} + -0.9 y_{t-2} + w_t,
\end{align}

Simulate the processes for 24 periods and, instead of using a line plot this time, use barcharts. Stack the axes for each vertically in a single figure.

```python
# Create figure that has dimensions 12x8

# Initial y values

# End period of simulation

# Create w array and set period 1 value

# Specify coefficients

# Simulate y and remove zeros from the simulated data

# Remove zeros from the simulated data

# Construct barchart of y values

# Specify coefficients

# Simulate y

# Remove zeros from the simulated data

# Construct barchart of y values

```

Apparently under both parameterizations the second-order difference equation is stable. But change the sign of $\rho_2$ in the last simulation so that the model becomes:

\begin{align}
y_t & = 0.6 y_{t-1} + 0.9 y_{t-2} + w_t,
\end{align}

```python
# Create figure that has dimensions 12x4

# Initial y values

# End period of simulation

# Create w array and set period 1 value

# Specify coefficients

# Simulate y and remove zeros from the simulated data

# Remove zeros from the simulated data

# Construct barchart of y values

```

Evidently reversing the sign on the second coefficient has caused the model to become explosive. Determining the stability of second-order and higher-order difference equations is not trivial and requires math that is beyond what most students in the course will have seen and I won't go into the topic here. See Chapter 1 of James Hamilton's book [*Time Series Analysis*](https://press.princeton.edu/titles/5386.html]).

However, as the examples suggest, simulating a process can give strong clues about whether a process is stable.

### A Fifth-Order Difference Equation

Now consider the following fifth-order difference equation difference equation:

\begin{align}
y_t & =  -0.3948 y_{t-1} - 0.3719 y_{t-2} - 0.4126 y_{t-3} - 0.3845y_{t-4} +  0.2477 y_{t-5} + w_t
\end{align}

The coefficients were actually chosen randomly using NumPy as you can see in the code below.

```python
# Create figure that has dimensions 12x4

# Initial y values

# End period of simulation

# Create w array and set period 1 value

# Specify coefficients

# rho = [0.5,0.2]

# Simulate y and remove zeros from the simulated data

# Remove zeros from the simulated data

# Construct barchart of y values

```


```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.sparse import spdiags
from scipy.sparse.linalg import spsolve

%matplotlib notebook
```

```python
from sympy import *
init_printing()
```

```python
M0,x=symbols("M_0 x",real=True)
D=Symbol('D',positive=True)
t=Symbol('t',positive=True)
```

```python
Csoln=M0/sqrt(4*pi*D*t) * exp(-x**2 / (4*D*t))
Csoln
```

```python
mom0=integrate(Csoln,(x,-oo,oo))
mom0
```

```python
mom1=integrate(Csoln*x,(x,-oo,oo))
mom1
```

```python
mom2=integrate(Csoln*x**2,(x,-oo,oo))
mom2
```

Centroid is $m_1/m_0$:

```python
mom1/mom0
```

Variance is $m_2/m_0$, with the variance calculated relative to x=centroid

```python
mom2/mom0
```

```python
term1=integrate( exp(-x**2), (x,t,oo))
term1
```

```python
term1.expand().factor()
```

```python
from scipy.special import erf, erfc

y=np.linspace(-5,5,200)

plt.figure()
plt.plot(y,erf(y),label='erf')
plt.plot(y,erfc(y),label='erfc')

```

    <IPython.core.display.Javascript object>

    [<matplotlib.lines.Line2D at 0x7f3b2f4b8df0>]

```python
xp=Symbol("x'")
xp
```

```python
c_exp=M0/sqrt(4*pi*D*t)* exp(-(x-xp)**2/(4*D*t))
c_exp
```

```python
integrate( c_exp, (xp,-oo,0))
```

```python
# Continuous release
# naive approach: integrate the point release in time out to
# infinity, representing overlapping releases.
#
u=Symbol('u',real=True,positive=True)
xpos=Symbol('x',real=True,positive=True)

soln=integrate( M0/sqrt(4*pi*D*t)* exp(-(xpos-u*t)**2/(4*D*t)), (t,0,oo))
soln
```

```python
eq1.subs(u,0.05)
```

```python
mdot=4.0 # g/m2 /s
Dnum=0.05 # m2/s

eq1=soln.subs(u,0.05).subs(D,0.05).subs(M0,4)

y=np.linspace(-10,10,100)

c_y=[ eq1.subs(xpos,val).evalf() for val in y]

plt.figure()
plt.plot(y,c_y)
```

    <IPython.core.display.Javascript object>

    [<matplotlib.lines.Line2D at 0x7f3b811b7ee0>]

```python
N=1001
dt=0.4
mdot=4.0 # g/m2 /s
Dnum=0.05 # m2/s
unum=0.05 # m/s

C0=mdot/unum # This is the correct value, and there is a bug below.

x=np.linspace(-8,10,N)
Cnum=np.zeros_like(x)

dx=x[1]-x[0]
fig,ax=plt.subplots()

l=ax.plot(x,Cnum)[0]

ax.axis(xmin=x[0],xmax=x[-1],ymin=-0.5,ymax=1.2*C0) ;
```

    <IPython.core.display.Javascript object>

```python
ff=Dnum*dt/dx**2
d0=(1+2*ff)*np.ones(N)
dp1=-ff*np.ones(N)
dp1[1]*=2
dm1=-ff*np.ones(N)
dm1[-2:]*=2
mat_imp=spdiags( [dm1,d0,dp1], [-1,0,1], N, N).tocsr()

xedge=np.r_[x-dx/2,x[-1]+dx/2]

for _ in range(1000):
    # Advection:
    Cnum=np.interp(x,x+dt*unum,Cnum,left=0,right=0)
    # Source w/advection
    # add Csrc=mdot/u for the interval [0,u*dt]
    # integrate to get the cumulative distribution, then interpolate
    # and diff to get discrete distribution:
    # for x<0, cdf is 0.
    # over [0,u*dt] this should ramp up to mdot*dt
    # Make an x coord array for the edge of the control volumes
    # The cumulative mass over [-inf,x_i] for each x_i in xedge,
    # for mass released over [0,dt]
    cmdf_x=np.interp( xedge, [0,unum*dt], [0,mdot*dt])
    mdf_x=np.diff(cmdf_x) # mass per CV
    Cnum[:]+=mdf_x/dx # concentratino per CV

    # Diffusion:
    Cnum=sparse.linalg.spsolve(mat_imp,Cnum)

    l.set_ydata(Cnum)

```

```python
sel=x<=0
ax.plot(x[sel],C0*np.exp(unum*x[sel]/Dnum),'k--')
```

Reasoning:
---

At steady state, for $x \neq 0$,

$$ u \frac{dC}{dx} = D \frac{d^2 C}{dx^2}$$

Integrate both sides...

$$ uC = D\frac{dC}{dx} + A $$

Can't integrate again, as $C=C(x)$.
One solution is that $C$ is constant, $dC/dx=0$, and we solve for $A$. This works for the +x side.
One the -x side, we can assume that as $x\rightarrow-\infty$, $C\rightarrow 0$. If $C$ goes to zero, then advective flux goes to zero and the balancing

Consider a control volume.  A control volume goes arbitrarily far out to -x, and stops at x<0. This box has no flux in on the left, no internal sources, and a steady mass.  So the flux on the right side of the box must be 0. So for any point x<0, advective flux is to the right ($J_A >= 0$) and it must be exactly balanced by diffusive flux ($J_D=-J_A<=0$)

For $x<0$:
$$ uC=D \frac{dC}{dx}$$

We can solve this and find that $C(x)=C(0) e^{ux/D}$

If the right side of the CV is at $x>0$, then the box includes a source, but still no flux from the left. So the net flux out the right side must be equal the source.

$$uC -D \frac{dC}{dx} = \dot{m}/a$$

One solution is that $C$ is constant, the flux is entirely advective.

This is a first order ODE. So we need 1 BC.

Could say that as $x\rightarrow \infty$, advection dominates, and if it's just a point source and advection then we can solve directly for the right BC. That feels a bit weak, but probably okay.

Can do a bit better than that:
$$C - \frac{\dot{m}}{au} = \frac{D}{u} \frac{dC}{dx} $$

$$ \int dx = \frac{D}{u} \int \frac{dC}{C- \dot{m}/au}$$

$$ x + B = \frac{D}{u} \ln [ C-\dot{m}/au ]$$

$$ C = B' e^\frac{ux}{D} + \dot{m}/au $$

We have a growing exponential and a constant. Knowing that as $x$ goes to $\infty$ $C$ is finite and nonzero, then $B'=0$

```python

```


# POL280 Bayesian Modelling Memo & Codes

(Contents are originally provided by Christopher Hare, annotated and revised by Gento Kato)

***THIS IS A MEMO, NOTE THAT THERE CAN BE ERRORS AND MISUNDERSTANDINGS***

## Topic 1: Introduction (04/13/2017)

### Why Bayes (than Frequentist) ?

* Model **assumptions are clear**.
* Better way to make **probability statements** on quantitities of interest.
* Can **update** statements as new information is received.

### Bayes Theorem (Bayes Rule)

$$P(A|B) = \frac{P(B|A) P(A)}{P(B)}$$

*Example: Test of Disease*

* 1% of the population has the disease: $P(A) = 0.01$
* A test is 95% accurate to detect the disease: $P(B|A) = 0.95$
* Probability that you have the disease when you've tested positive: $P(A|B)$?

\begin{align}
P(A|B) = \frac{P(B|A) P(A)}{P(B)} &= \frac{P(B|A) P(A)}{P(A)P(B|A)+P(!A)P(B|!A)}\\
&= \frac{.95 \times .01}{(.01 \times .95) + (.99 \times .05)} \\
&\approx 0.16
\end{align}

### Intuition: The Monty Hall Problem

Suppose you're on a game show and you're given teh choice of three doors: Behind one door is a car; behind the otheres, goats. You pick a door, say No.1, and the host, who knows what's behind the doors, opens another door, say No.3, which has a goat. He then says to you "Do you wan to pick door No.2?" Is it to your advantage to switch your choice?

Let's pick door 1, and Monty then opens door 2 to show us a goat.

Let A represent the outcome that the car is behind door 1. Let B represent the event that we're shown a goat behind door 2:

$$P(A|B) = \frac{P(B|A) P(A)}{P(B)}$$

Let's Simulate by R!

```R
## Monty Hall Problem Simulation ##
doors <- c("1", "2", "3") # Three Doors
sim_montyhall <- function(door){
car <- sample(doors, 1) # define the door with car
pick <- doors[door] # your pick of the door
open <- sample(doors[which(doors!=car & doors!=pick)], 1) # define the door opened
return(pick==car) # return if the car is behind your picked door.
}
sims <- replicate(10000, sim_montyhall(1) ) #repeat the process many times
mean(sims) # The probability
```

0.3304

###  Monte Carlo Simulation

```R
## Monte Carlo Simulation ##

#install.packages("plotrix")
library(plotrix)
library(grid)

options(repr.plot.width=5, repr.plot.height=5)
## Plot Rectangle and Circle
plot(c(-1, 1), c(-1, 1), type = "n", asp = 1)
rect(-1, -1, 1, 1)
draw.circle(0, 0, 1)

## Conduct Simulation
nsamp <- 10000 # The size of sample
inside <- NA # Storage for those values inside the circle
for(i in 1:nsamp){
  x <- runif(2, -1, 1) ## Draw two values from uniform distribution (-1, 1)
  if (sqrt(x[1] * x[1] + x[2] * x[2]) < 1) {
    inside[i] <- 1; points(x[1], x[2], col = "orange")
  }
  if (sqrt(x[1] * x[1] + x[2] * x[2]) > 1) {
    inside[i] <- 0; points(x[1], x[2], col = "black")
  }
}
```

```R
table(inside)
4 * (7848 / 10000)
```

    inside
       0    1
    2176 7824

3.1392


# Lecture 33: Markov chains (cont.), Google PageRank as a Markov chain

## Stat 110, Prof. Joe Blitzstein, Harvard University

----

## Generalizing Reversible Markov Chains

Now let's take what we learned about undirected graphs and reversible Markov chains, and extend that just a bit more: let's assume that the transition probabilities are not uniform for a node, that some edges are more likely to be traversed than others (but still keeping to an undirected graph).

Recall that the degree for node is the number of edges it has. We proved last time that the stationary probability for any node is proportional to its degree.

Now we will generalize that to deal with edge weights: $w_{ij} \ge 0, w_{ij} = 0$ if there is no edge $\{ij\}$.

Since the graph in question is undirected, assume $w_{ij} = w_{ji}$.

### Random walk

From state $i$, go to $j$ with transition probability $\propto w_{ij}$. If $\{ij\}$ is an edge, then $q_{ij} = \frac{w_{ij}}{\sum_k w_{ik}}$, assuming $w_{ij} \ge 0$. In the case of no weights, just think of all the weights as being 1; hence, the transition probabilities are proportional to degree.

** Proof **

Proving reversibility, we have:

\begin{align}
  \left( \sum_k w_{ik} \right) \, q_{ij} &= w_{ij} &\text{ from assumption above } \\
  &= w_{ji} &\text{ since undirected graph } \\
  &= \left( \sum_k w_{jk} \right) \, q_{ji} \\
  \\\\
  \Rightarrow s_i &\propto \sum_k w_{ik}
\end{align}

And so you see, here we have a _complete_ generalization of a reversible Markov chain.

### Examining the converse

Conversely, **any** reversible Markov chain can be represented by a graph like the above.

Assuming we have a Markov chain (we have $q_{ij}$; and assuming the Markov chain is reversible, we have:

\begin{align}
  \text{Let } w_{ij} &= s_{i} \, q_{ij} \\
  &= s_{i} \, q_{ji} \\
  &= w_{ji} \\
  \\\\
  \rightarrow P (X_{n+1} = j | X_n = i) &= \frac{w_{ij}}{\sum_k w_{ik}} \\
  &= \frac{s_i \, q_{ij}}{\sum_k w_{ik}} \\
  &= \frac{s_i \, q_{ij}}{\sum_k s_i \, q_{ik}} \\
  &= \frac{q_{ij}}{\sum_k q_{ik}} \\
  &= \frac{q_{ij}}{1} \\
  &= q_{ij}
\end{align}

And so this chain we constructed has the desired transition probabilities: this is the _quintessential_ example of a reversible Markov chain.

## Non-reversible Example: Google PageRank

Now let's generalize this just a bit more, and look at the case of **non-reversible** Markov chains.

In this example, consider nodes to be pages in the world-wide web, and the links between the pages as directed edges. In this way, you can see that the Internet is an example of a non-reversible Markov chain. We will consider a very tiny, toy representation of the web as a 4-node directed graph as below.

In the case of web search, the results should be returned in ranked form, with the most important pages with relation to your search query ranked the highest. But how to measure _importance_?

Google's original approach, known as PageRank, measured the importance of a page in terms of pages linking to it and also their importance. This relation suggests a sort of eigenvalue/eigenvector relation, which motivates the key concept of modelling the web as a non-reversible Markov chain.

### Intuition & Motivation

Let a web page's score be $s_j = \sum_i s_i \, q_{ij}$. Now all the links a page might have should be "diluted" equally, and so that is what the $q_{ij}$ is doing the previous equation.

So given the simple, non-reversible Markov chain above, our matrix $Q$ would be:

\begin{align}
  Q &= \begin{pmatrix}
    0 & \frac{1}{2} & \frac{1}{2} & 0 \\
    \frac{1}{2} & 0 & \frac{1}{2} & 0 \\
    0 & 0 & 0 & 1 \\
    \frac{1}{4} & \frac{1}{4} & \frac{1}{4} & \frac{1}{4} \\
    \end{pmatrix}
\end{align}

Note that with regards to page 4, since we want to represent the web with a Markov chain, we assume that you can jump to any other page from page 4 with equal probability. After all, if you navigated to some page in the Internet that had no outgoing links, you would just open up another tab or window and jump elsewhere.

So from

\begin{align}
  s_j &= \sum_i s_i \, q_{ij} \\
  \\
  \Rightarrow \vec{s} &= \vec{s} \, Q &\text{ if } \vec{s} \text{ is normalized, all elements} \in \mathbb{Z}_{\ge 0}
\end{align}

Hence we can see that in our model, $\vec{s}$ is the stationary distribution of the random web-surfing chain. Intuitively, this stationary distribution gives us the long-run probabilities of being in certain states (pages). Therefore, all we want to do is solve that linear system and normalize it to gain $\vec{s}$.

### Why model with a Markov chain?

Using a Markov chain for our model makes sense from the intuition that the stationary distribution $\vec{s}$ is the amount of time one would spend on certain pages.

But there are other other reasons that motivate the use of a Markov chain with regards to _computation_.

In the real world, there are billions of web pages, and the transition matrix $Q$ will have an equivalent rank. Algorithms such as Gaussian Elimination have cubic (polynomial) complexity, and if matrix $Q$ has rank in the billions, you can see that the problem could be intractable.

Here is the original PageRank formula:

\begin{align}
  G &= \alpha \, Q + (1 - \alpha) \, \frac{J}{M}
\end{align}

where

* $M$ is the # pages in the web
* $J$ is an $m \times m$ matrix of $1$s
* $0 \lt \alpha \lt 1$

There are now _two_ transition matrices: $Q$ for clicking links to pages on the web; and $\frac{J}{M}$ matrix for "teleporting" to another page. So, with probability $\alpha$ the web surfer will follow a link from the present page to another, and with probability $1-\alpha$ the surfer will jump to another page (with all pages equally probable).

Note that with the above formula irreducibility is guaranteed, as all transition probabilities are greater than 0. And since this is an irreducible Markov chain, a unique stationary distribution does exist.

Eigen computations on a matrix with a rank of this magnitude could very well be computationally unfeasible, but if we use a Markov model and agree that we do _not_ need the exact stationary distribution but only an _approximation_, then we can use property of convergence to stationarity to find a reasonable answer!

We can start with _any_ initial distribution and run the chain for until we reach a level of convergence that we deem acceptable.

Let $\vec{t}$ be our initial probability vector.

\begin{align}
  \vec{t} \, G &= \alpha \, \vec{t} \, Q + (1 - \alpha) \, \vec{t} \, \frac{J}{M}
\end{align}

* $\vec{t} \, Q$ is a very _sparse_ matrix, and there are many algorithms and data structures for dealing with such in an optimal manner
* $\vec{t}$ is $\mathbb{R}^{m}$ and $J$ is a square matrix $1$s with rank $m$, so $\vec{t} \, J$ is just a $1 \times m$ vector of $1$s
* $\Rightarrow (\vec{t} \, G)G = \vec{t} \, G^2$
* $\Rightarrow (\vec{t} \, G^2)G = \vec{t} \, G^3$
* $\cdots$
* $\Rightarrow (\vec{t} \, G^{n-1})G = \vec{t} \, G^n$
* and as $n \rightarrow \infty$ we get the stationary distribution for matrix $G$!

----

View [Lecture 33: Markov Chains Continued Further | Statistics 110](http://bit.ly/2wRyU5u) on YouTube.


# Lecture 1: Introduction to Python 3.6 and Jupyter Notebook
By Dr. Anders S. Christensen
`anders.christensen @ unibas.ch`

Today we go through the basics of Python3 and also how to use Jupyter Notebooks (via Google Colab).

Try to run the code block below by clicking the play-button (or pressing Ctrl+Enter).

```
print("Hello Python!")
```

## Section 1.1: Basic math operations

```
print(7 + 17)
```

```
print(7 * 17)
```

```
print(7 - 17)
```

```
print(7 / 17)
```

```
print(7**17)
```

### Exercise 1.1.1: Write your own example here:

```

```

## Section 1.2: Variables and naming

```
x = 7
y = 17.0

z = x * y

print(z)
```

Strings can also be variables:

```
greeting = "Hello World!"

print(greeting)
```

Different data types?

```
x = 1
print(type(x))
```

```
y = 5.0
print(type(y))
```

```
w = x * y
print(type(w))
```

Some variables don't mix:

```
x = "Hello"
y = 42

z = x + y

print("z is", z)
```

Variable names can be really long! Normally we use lower-case and _ in our variables in Python

```
lecturer_name = "Anders Christensen"
students_asleep = 0
days_until_xmas = 200.5
```

Some examples of legal and illegal names:

```
#Legal variable names:
myvar = "John"
my_var = "John"
_my_var = "John"
myVar = "John"
MYVAR = "John"
myvar2 = "John"
```

### Exercise 1.2.1:
The following will not work. Lets fix them!

```
# Illegal variable names:
myvar2 = "John"
my-var = "John"
my var = "John"
```

## Section 1.3: ```for```-loops

We have seen how to write variables and do simple calculations, but often we want to do more than just *one* thing!

In order to do a series of things, we can use the so-called `for`-loop, which is often invoked using the `range()` funtions.

`range(n)` will give you all the numbers from $0$ to $n-1$

(Because Python counting starts from $0$)

For example:

```
# Print all numbers from 0 to 19
for i in range(20):
  print(i)

print("Does this get repeated?")
```

Note how everything that is indented gets repeated by the `for` loop!

**Question:** What is `range()`??

```
# Convert the range to a list
print( list(range(10)) )
```

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Note how `range()` "skips" the final number!

---

Normally `range()` starts from 0, but you can start from any number, by using *two* arguments, for example:

```
# Print from 10 to 19
print( list(range(10, 20)) )
```

    [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

We can for example use the loop to calculate a sum:
\begin{equation}
k = \sum_{n=0}^{19} i
\end{equation}

```
k = 0
for i in range(20):
  # print(i)
  k = k + i

print("k is", k)
```

### Section 1.4: Lists
A fundamental data structure of Python is so-called "lists". A list contains a list of items, for example numbers.

An example of a list with 7 items is given below:

\begin{array}{lrrrrrrr}
\texttt{i}  & 0  & 1 & 2 & 3 & 4 & 5 & 6 \\ \hline
\texttt{my_list} & 3.5 & 2.1 & -2.5 & 1.2 & 6.2 & 5.2 & -8.1
\end{array}

In Python this would be:

```
 my_list = [3.5, 2.1, -2.5, 1.2, 6.2, 5.2, -8.1]
 print(my_list)
```

Note: The first item in the list has index $0$, and the last has index $n-1$. You count starting from 0, and NOT 1!

For example:

```
# The first item in the list
print(my_list[0])

# The "third" item in the list
print(my_list[2])

# The last item in the list
print(my_list[6])
```

```
# List contatining some numbers
some_numbers = [5, 10, 15, 20, 25, 30]

# print length of that list
print(len(some_numbers))
```

```
# Example 1: Print some_numbers
for i in range(len(some_numbers)):
  print(some_numbers[i])
```

```
# Example 2: Print some_numbers
for n in some_numbers:
  print(n)
```

### Debugging Exercise 1.4.1: What happens if you ask for something that isn't there?

```
my_list = [3.5, 2.1, -2.5, 1.2, 6.2, 5.2, -8.1]

# Only up to 6 allowed!
print(my_list[7])

```

### Section 1.5: Sum over items in list:

This is an example of summation:

\begin{equation}
y = \sum_{i=0}^n x_i
\end{equation}

```
my_sum = 0

for i in range(7):

  print(i, my_list[i])
  my_sum = my_sum + my_list[i]

print("my_sum is", my_sum)
```

## Coding Example: The Quantum Harmonic Oscillator

The behavior of a di-atomic molecule can be approximated by a quantum harmonic oscillator.

In this example we calculate the internal energy, $U$, and the heat capacity $C_v$ of a quantum harmonic oscillator.

The energy $\epsilon_n$ for a state of the harmonic oscillator is given by
\begin{equation}
\epsilon_n = n+\frac{1}{2},
\end{equation}
in units of $\hbar\omega$. We also assume that there are no degenerate states!

The probability for observing the state $n$ at a given temperature $\tau$ is given by:
\begin{equation}
p(n) = \frac{\exp{\left(-\epsilon_n / \tau \right)}}{Z},
\end{equation}

where $Z$ is the partition function (*Zustandssumme*)
\begin{equation}
Z = \sum_{n=0}^{\infty} \exp{\left(-\epsilon_n / \tau\right)}.
\end{equation}

The total *internal* energy $U$ is then given by:
\begin{equation}
U = \langle \epsilon \rangle = \sum_n \epsilon_n p(n)
\end{equation}

---

Another interesting property of this system is the *heat capactity* $C_V$ given by
\begin{align}
C_V &= \frac{\partial U}{\partial \tau} \biggl|_V \\
&= \frac{\langle \epsilon^2 \rangle - \langle \epsilon \rangle^2}{\tau^2}.
\end{align}

The expectation value $\langle \epsilon \rangle$ is given via the energy $U$:
\begin{align}
\langle \epsilon \rangle &= \sum_n \epsilon_n p(n) \\
&= \sum_n \left(n+\frac{1}{2}\right) p(n)
\end{align}

And $\langle \epsilon^2 \rangle$ is the second moment of the energy given by:
\begin{align}
\langle \epsilon^2 \rangle &= \sum_n \epsilon^2_n p(n) \\
&= \sum_n \left(n+\frac{1}{2}\right)^2 p(n)
\end{align}

### Example 1: Print the energy $\epsilon_n$ for each state up to $n=3$?

The energy $\epsilon_n$ for a state of the harmonic oscillator is given by:
\begin{equation}
\epsilon_n = n+\frac{1}{2}
\end{equation}

```
for n in range(3+1):

  epsilon = n + 0.5

  print(n, epsilon)
```

#### Example 2: What is the partition function $Z$

for $\tau$ = 2.0?

Recall that the partition function is given by:

\begin{equation}
Z = \sum_{n=0}^{\infty} \exp{\left(-\frac{\left(n+\frac{1}{2}\right)}{\tau}\right)}
\end{equation}

Before we start, we have to import the function to evaluate the exponential function from a package called NumPy.

```
# Import functionality for exponential function: np.exp()
import numpy as np

# np.exp() = "e^x"

print(np.exp(0.0))
print(np.exp(1.0))
```

```
tau = 2.0
n_states = 10000

Z = 0.0

for n in range(n_states+1):

  Z = Z + np.exp(-(n + 0.5) / tau)

print(Z)
```

#### Example 3: What is the internal energy $U$ for $\tau$ = 2.0?

\begin{equation}
U = \langle \epsilon \rangle = \sum_n \epsilon_n p(n)
\end{equation}

Also recall that:

\begin{equation}
p(n) = \frac{\exp{\left(-\epsilon_n / \tau \right)}}{Z},
\end{equation}

```
U = 0

for n in range(n_states+1):

  epsilon = n + 0.5
  prob = np.exp(-epsilon/tau) / Z

  U = U + epsilon * prob

  # print(n, epsilon, prob)

print("Internal energy:", U)
```

#### Example 4: Second moment of the energy and the heat capacity

What is the heat capacity $C_V$ for $\tau = 2.0$?
\begin{equation}
C_V = \frac{\langle \epsilon^2 \rangle - \langle \epsilon \rangle^2}{\tau^2}.
\end{equation}

With the second moment of the energy:
\begin{equation}
\langle \epsilon^2 \rangle = \sum_n \left(n+\frac{1}{2}\right)^2 p(n)
\end{equation}

```
# Second moment of the energy:

U2 = 0

for n in range(n_states+1):

  epsilon = n + 0.5
  prob = np.exp(-epsilon/tau) / Z

  U2 = U2 + epsilon**2 * prob

  # print(n, epsilon, prob)

print("Second moment of the energy:", U2)
```

Finally, the heat capacity is:

```
Cv = (U2 - U**2) / tau**2

print("Heat capacity:", Cv)
```

## Extra: Calculating $C_V$ as a function of $\tau$

```
# 7 temperatures
temperatures = [0.1, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]
n_states = 10

for tau in temperatures:

  # Calculate the partition function for tau
  Z = 0.0
  for n in range(n_states+1):
    Z = Z + np.exp(-(n + 0.5) / tau)

  # First moment of the energy
  U = 0.0

  # Second moment of the energy
  U2 = 0.0

  for n in range(n_states+1):

    epsilon = n + 0.5
    prob = np.exp(-epsilon/tau) / Z

    U = U + epsilon * prob
    U2 = U2 + epsilon**2 * prob

  Cv = (U2 - U**2) / tau

  print(tau, Cv)
```


Table of Contents

Non Linear Regression Analysis

Importing required libraries

Quadratic

Exponential

Logarithmic

Sigmoidal/Logistic

Non-Linear Regression example

Plotting the Dataset

Choosing a model

Building The Model

# Non Linear Regression Analysis

If the data shows a curvy trend, then linear regression will not produce very accurate results when compared to a non-linear regression because, as the name implies, linear regression presumes that the data is linear.
Let's learn about non linear regressions and apply an example on python. In this notebook, we fit a non-linear model to the datapoints corrensponding to China's GDP from 1960 to 2014.

### Importing required libraries

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

Though Linear regression is very good to solve many problems, it cannot be used for all datasets. First recall how linear regression, could model a dataset. It models a linear relation between a dependent variable y and independent variable x. It had a simple equation, of degree 1, for example y = 2*(x) + 3.

```python
x = np.arange(-5.0, 5.0, 0.1)

##You can adjust the slope and intercept to verify the changes in the graph
y = 2*(x) + 3
y_noise = 2 * np.random.normal(size=x.size)
ydata = y + y_noise
#plt.figure(figsize=(8,6))
plt.plot(x, ydata,  'bo')
plt.plot(x,y, 'r')
plt.ylabel('Dependent Variable')
plt.xlabel('Indepdendent Variable')
plt.show()
```

Non-linear regressions are a relationship between independent variables $x$ and a dependent variable $y$ which result in a non-linear function modeled data. Essentially any relationship that is not linear can be termed as non-linear, and is usually represented by the polynomial of $k$ degrees (maximum power of $x$).

$$ \ y = a x^3 + b x^2 + c x + d \ $$

Non-linear functions can have elements like exponentials, logarithms, fractions, and others. For example: $$ y = \log(x)$$

Or even, more complicated such as :
$$ y = \log(a x^3 + b x^2 + c x + d)$$

Let's take a look at a cubic function's graph.

```python
x = np.arange(-5.0, 5.0, 0.1)

##You can adjust the slope and intercept to verify the changes in the graph
y = 1*(x**3) + 1*(x**2) + 1*x + 3
y_noise = 20 * np.random.normal(size=x.size)
ydata = y + y_noise
plt.plot(x, ydata,  'bo')
plt.plot(x,y, 'r')
plt.ylabel('Dependent Variable')
plt.xlabel('Indepdendent Variable')
plt.show()
```

As you can see, this function has $x^3$ and $x^2$ as independent variables. Also, the graphic of this function is not a straight line over the 2D plane. So this is a non-linear function.

Some other types of non-linear functions are:

### Quadratic

$$ Y = X^2 $$

```python
x = np.arange(-5.0, 5.0, 0.1)

##You can adjust the slope and intercept to verify the changes in the graph

y = np.power(x,2)
y_noise = 2 * np.random.normal(size=x.size)
ydata = y + y_noise
plt.plot(x, ydata,  'bo')
plt.plot(x,y, 'r')
plt.ylabel('Dependent Variable')
plt.xlabel('Indepdendent Variable')
plt.show()
```

### Exponential

An exponential function with base c is defined by $$ Y = a + b c^X$$ where b ≠0, c > 0 , c ≠1, and x is any real number. The base, c, is constant and the exponent, x, is a variable.

```python
X = np.arange(-5.0, 5.0, 0.1)

##You can adjust the slope and intercept to verify the changes in the graph

Y= np.exp(X)

plt.plot(X,Y)
plt.ylabel('Dependent Variable')
plt.xlabel('Indepdendent Variable')
plt.show()
```

### Logarithmic

The response $y$ is a results of applying logarithmic map from input $x$'s to output variable $y$. It is one of the simplest form of __log()__: i.e. $$ y = \log(x)$$

Please consider that instead of $x$, we can use $X$, which can be polynomial representation of the $x$'s. In general form it would be written as
\begin{equation}
y = \log(X)
\end{equation}

```python
X = np.arange(-5.0, 5.0, 0.1)

Y = np.log(X)

plt.plot(X,Y)
plt.ylabel('Dependent Variable')
plt.xlabel('Indepdendent Variable')
plt.show()
```

### Sigmoidal/Logistic

$$ Y = a + \frac{b}{1+ c^{(X-d)}}$$

```python
X = np.arange(-5.0, 5.0, 0.1)

Y = 1-4/(1+np.power(3, X-2))

plt.plot(X,Y)
plt.ylabel('Dependent Variable')
plt.xlabel('Indepdendent Variable')
plt.show()
```

# Non-Linear Regression example

For an example, we're going to try and fit a non-linear model to the datapoints corrensponding to China's GDP from 1960 to 2014. We download a dataset with two columns, the first, a year between 1960 and 2014, the second, China's corresponding annual gross domestic income in US dollars for that year.

```python
import numpy as np
import pandas as pd
```

```python
# downloading dataset
# !wget -nv -O china_gdp.csv https://s3-api.us-geo.objectstorage.softlayer.net/cf-courses-data/CognitiveClass/ML0101ENv3/labs/china_gdp.csv -P ./Data/
```

```python

df = pd.read_csv("./Data/china_gdp.csv")
df.head(10)
```

    /bin/sh: wget: command not found

      Year
      Value

      0
      1960
      5.918412e+10

      1
      1961
      4.955705e+10

      2
      1962
      4.668518e+10

      3
      1963
      5.009730e+10

      4
      1964
      5.906225e+10

      5
      1965
      6.970915e+10

      6
      1966
      7.587943e+10

      7
      1967
      7.205703e+10

      8
      1968
      6.999350e+10

      9
      1969
      7.871882e+10

__Did you know?__ When it comes to Machine Learning, you will likely be working with large datasets. As a business, where can you host your data? IBM is offering a unique opportunity for businesses, with 10 Tb of IBM Cloud Object Storage: [Sign up now for free](http://cocl.us/ML0101EN-IBM-Offer-CC)

### Plotting the Dataset ###
This is what the datapoints look like. It kind of looks like an either logistic or exponential function. The growth starts off slow, then from 2005 on forward, the growth is very significant. And finally, it deaccelerates slightly in the 2010s.

```python
plt.figure(figsize=(8,5))
x_data, y_data = (df["Year"].values, df["Value"].values)
plt.plot(x_data, y_data, 'ro')
plt.ylabel('GDP')
plt.xlabel('Year')
plt.show()
```

### Choosing a model

From an initial look at the plot, we determine that the logistic function could be a good approximation,
since it has the property of starting with a slow growth, increasing growth in the middle, and then decreasing again at the end; as illustrated below:

```python
X = np.arange(-5.0, 5.0, 0.1)
Y = 1.0 / (1.0 + np.exp(-X))

plt.plot(X,Y)
plt.ylabel('Dependent Variable')
plt.xlabel('Indepdendent Variable')
plt.show()
```

The formula for the logistic function is the following:

$$ \hat{Y} = \frac1{1+e^{\beta_1(X-\beta_2)}}$$

$\beta_1$: Controls the curve's steepness,

$\beta_2$: Slides the curve on the x-axis.

### Building The Model
Now, let's build our regression model and initialize its parameters.

```python
def sigmoid(x, Beta_1, Beta_2):
    y = 1 / (1 + np.exp(-Beta_1*(x-Beta_2)))
    return y
```

Lets look at a sample sigmoid line that might fit with the data:

```python
beta_1 = 0.10
beta_2 = 1990.0

#logistic function
Y_pred = sigmoid(x_data, beta_1 , beta_2)

#plot initial prediction against datapoints
plt.plot(x_data, Y_pred*15000000000000.)
plt.plot(x_data, y_data, 'ro')
```

Our task here is to find the best parameters for our model. Lets first normalize our x and y:

```python
# Lets normalize our data
xdata =x_data/max(x_data)
ydata =y_data/max(y_data)
```

#### How we find the best parameters for our fit line?
we can use __curve_fit__ which uses non-linear least squares to fit our sigmoid function, to data. Optimal values for the parameters so that the sum of the squared residuals of sigmoid(xdata, *popt) - ydata is minimized.

popt are our optimized parameters.

```python
from scipy.optimize import curve_fit
popt, pcov = curve_fit(sigmoid, xdata, ydata)
# print the final parameters
print(" beta_1 = %f, beta_2 = %f" % (popt[0], popt[1]))
```

    beta_1 = 690.453018, beta_2 = 0.997207

Now we plot our resulting regresssion model.

```python
x = np.linspace(1960, 2015, 55)
x = x/max(x)
plt.figure(figsize=(8,5))
y = sigmoid(x, *popt)
plt.plot(xdata, ydata, 'ro', label='data')
plt.plot(x,y, linewidth=3.0, label='fit')
plt.legend(loc='best')
plt.ylabel('GDP')
plt.xlabel('Year')
plt.show()
```