Repository: graspologic-org/graspologic

Issue:
sample correlated SBM
Example:

require(igraph)
gg <- rg.sample.SBM.correlated(n = 100, B = matrix(c(0.5,0.5,0.2,0.5), nrow = 2), rho = c(0.4,0.6), sigma = 0.2)

summary(gg$adjacency$A)
IGRAPH c77bf6c U--- 100 2424 -- 
summary(gg$adjacency$B)
IGRAPH 3ccfb0c U--- 100 2039 -- 

cor(as.vector(gg$adjacency$A[]), as.vector(gg$adjacency$B[]))
[1] 0.1494246


## Correlated ER
rg.sample.correlated.gnp <- function(P,sigma){
   require(igraph)
   n <-  nrow(P)
   U <- matrix(0, nrow = n, ncol = n)
   U[col(U) > row(U)] <- runif(n*(n-1)/2)
   U <- (U + t(U))
   diag(U) <- runif(n)
   A <- (U < P) + 0 ;
   diag(A) <- 0

   avec <- A[col(A) > row(A)]
   pvec <- P[col(P) > row(P)]
   bvec <- numeric(n*(n-1)/2)

   uvec <- runif(n*(n-1)/2)

   idx1 <- which(avec == 1)
   idx0 <- which(avec == 0)

   bvec[idx1] <- (uvec[idx1] < (sigma + (1 - sigma)*pvec[idx1])) + 0
   bvec[idx0] <- (uvec[idx0] < (1 - sigma)*pvec[idx0]) + 0

   B <- matrix(0, nrow = n, ncol = n)
   B[col(B) > row(B)] <- bvec
   B <- B + t(B)
   diag(B) <- 0

   return(list(A = graph.adjacency(A,"undirected"), B = graph.adjacency(B,"undirected")))
}

# non-igraph version of correlated SBM
#gg <- rg.sample.SBM.correlated(n = 100, B = matrix(c(0.5,0.5,0.2,0.5), nrow = 2), rho = c(0.4,0.6), sigma = 0.2)
#cor(as.vector(gg$adjacency$A[]), as.vector(gg$adjacency$B[]))
rg.sample.SBM.correlated <- function(n, B, rho, sigma, conditional = FALSE){
   if(!conditional){
       tau <- sample(c(1:length(rho)), n, replace = TRUE, prob = rho)
   }
   else{
       tau <- unlist(lapply(1:2,function(k) rep(k, rho[k]*n)))
   }
   P <- B[tau,tau]
   return(list(adjacency=rg.sample.correlated.gnp(P, sigma),tau=tau))
}


Discussion:
@bvarjavand couple things we need to figure out

- [ ] how to verify that the rho-sbm implementation is correct

- [ ] how to generate rho-sbm with different marginal probabilities
correct

On Fri, Feb 1, 2019 at 7:42 PM Junhao Xiong <notifications@github.com>
wrote:

> @bvarjavand <https://github.com/bvarjavand> couple things we need to
> figure out
>
>    -
>
>    how to verify that the rho-sbm implementation is correct
>    -
>
>    how to generate rho-sbm with different marginal probabilities
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/neurodata/graspy/issues/66#issuecomment-459915808>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AACjcn2WtfJFCnxMOEUbDEoD5ryzMR-lks5vJN7fgaJpZM4Yw0Cf>
> .
>


-- 
the glass is all full:  half water, half air.
neurodata.io

@junhaobearxiong Will there be a PR for rhoER and rhoSBM sampling methods?
sure! I can do that once I finish the current set of experiments I'm running.

Resolving patch:
diff --git a/graspy/simulations/__init__.py b/graspy/simulations/__init__.py
--- a/graspy/simulations/__init__.py
+++ b/graspy/simulations/__init__.py
@@ -1 +1,2 @@
 from .simulations import *
+from .simulations_corr import *
diff --git a/graspy/simulations/simulations_corr.py b/graspy/simulations/simulations_corr.py
new file mode 100644
--- /dev/null
+++ b/graspy/simulations/simulations_corr.py
@@ -0,0 +1,322 @@
+# Copyright 2019 NeuroData (http://neurodata.io)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import numpy as np
+from graspy.simulations import sample_edges
+
+
+def check_dirloop(directed, loops):
+    if type(directed) is not bool:
+        raise TypeError("directed is not of type bool.")
+    if type(loops) is not bool:
+        raise TypeError("loops is not of type bool.")
+
+
+def check_r(r):
+    if not np.issubdtype(type(r), np.floating):
+        raise TypeError("r is not of type float.")
+    elif r < -1 or r > 1:
+        msg = "r must between -1 and 1."
+        raise ValueError(msg)
+
+
+def check_rel_er(p, r):
+    if p + r * (1 - p) < 0:
+        msg = "p + r * (1 - p) should be bigger than 0"
+        raise ValueError(msg)
+
+    if p * (1 - r) < 0:
+        msg = "p * (1 - r) should be bigger than 0"
+        raise ValueError(msg)
+
+
+def check_rel_sbm(p, r):
+    for i in range(np.array(p).shape[0]):
+        for j in range(np.array(p).shape[1]):
+            if p[i][j] + r * (1 - p[i][j]) < 0:
+                msg = "p + r * (1 - p) should be bigger than 0"
+                raise ValueError(msg)
+
+            elif p[i][j] * (1 - r) < 0:
+                msg = "p * (1 - r) should be bigger than 0"
+                raise ValueError(msg)
+
+
+def sample_edges_corr(P, R, directed=False, loops=False):
+    """
+    Generate a pair of correlated graphs with Bernoulli distribution.
+    Both G1 and G2 are binary matrices. 
+
+    Parameters
+    ----------
+    P: np.ndarray, shape (n_vertices, n_vertices)
+        Matrix of probabilities (between 0 and 1) for a random graph.
+        
+    R: np.ndarray, shape (n_vertices, n_vertices)
+        Matrix of correlation (between 0 and 1) between graph pairs.
+
+    directed: boolean, optional (default=False)
+        If False, output adjacency matrix will be symmetric. Otherwise, output adjacency
+        matrix will be asymmetric.
+
+    loops: boolean, optional (default=False)
+        If False, no edges will be sampled in the diagonal. Otherwise, edges
+        are sampled in the diagonal.
+
+    References
+    ----------
+    .. [1] Vince Lyzinski, et al. "Seeded Graph Matching for Correlated Erdos-Renyi Graphs", 
+       Journal of Machine Learning Research 15, 2014
+        
+    Returns
+    -------
+    G1: ndarray (n_vertices, n_vertices)
+        Adjacency matrix the same size as P representing a random graph.
+
+    G2: ndarray (n_vertices, n_vertices)
+        Adjacency matrix the same size as P representing a random graph.
+
+    Examples
+    --------
+    >>> np.random.seed(1)
+    >>> p = 0.5
+    >>> r = 0.3
+    >>> R = r * np.ones((5, 5))
+    >>> P = p * np.ones((5, 5))
+
+    To sample a correlated graph pair based on P and R matrices:
+
+    >>> sample_edges_corr(P, R, directed = False, loops = False)
+    (array([[0., 1., 0., 0., 0.],
+            [1., 0., 0., 0., 0.],
+            [0., 0., 0., 0., 1.],
+            [0., 0., 0., 0., 1.],
+            [0., 0., 1., 1., 0.]]), array([[0., 1., 0., 0., 0.],
+            [1., 0., 1., 0., 1.],
+            [0., 1., 0., 1., 1.],
+            [0., 0., 1., 0., 1.],
+            [0., 1., 1., 1., 0.]]))
+    """
+    # test input
+    # check P
+    if type(P) is not np.ndarray:
+        raise TypeError("P must be numpy.ndarray")
+    if len(P.shape) != 2:
+        raise ValueError("P must have dimension 2 (n_vertices, n_vertices)")
+    if P.shape[0] != P.shape[1]:
+        raise ValueError("P must be a square matrix")
+
+    # check R
+    if type(R) is not np.ndarray:
+        raise TypeError("R must be numpy.ndarray")
+    if len(R.shape) != 2:
+        raise ValueError("R must have dimension 2 (n_vertices, n_vertices)")
+    if R.shape[0] != P.shape[1]:
+        raise ValueError("R must be a square matrix")
+
+    # check directed and loops
+    check_dirloop(directed, loops)
+
+    G1 = sample_edges(P, directed=directed, loops=loops)
+    P2 = G1.copy()
+    P2 = np.where(P2 == 1, P + R * (1 - P), P * (1 - R))
+    G2 = sample_edges(P2, directed=directed, loops=loops)
+    return G1, G2
+
+
+def er_corr(n, p, r, directed=False, loops=False):
+    """
+    Generate a pair of correlated graphs with specified edge probability
+    Both G1 and G2 are binary matrices. 
+
+    Parameters
+    ----------
+    n: int
+       Number of vertices
+
+    p: float
+        Probability of an edge existing between two vertices, between 0 and 1.
+    
+    r: float
+        The value of the correlation between the same vertices in two graphs.
+    
+    directed: boolean, optional (default=False)
+        If False, output adjacency matrix will be symmetric. Otherwise, output adjacency
+        matrix will be asymmetric.
+    
+    loops: boolean, optional (default=False)
+        If False, no edges will be sampled in the diagonal. Otherwise, edges
+        are sampled in the diagonal.
+    
+    Returns
+    -------
+    G1: ndarray (n_vertices, n_vertices)
+        Adjacency matrix the same size as P representing a random graph.
+
+    G2: ndarray (n_vertices, n_vertices)
+        Adjacency matrix the same size as P representing a random graph.
+    
+    Examples
+    --------
+    >>> np.random.seed(2)
+    >>> p = 0.5
+    >>> r = 0.3
+    >>> n = 5
+
+    To sample a correlated ER graph pair based on n, p and r:
+
+    >>> er_corr(n, p, r, directed=False, loops=False)
+    (array([[0., 0., 1., 0., 0.],
+        [0., 0., 0., 1., 0.],
+        [1., 0., 0., 1., 1.],
+        [0., 1., 1., 0., 1.],
+        [0., 0., 1., 1., 0.]]), array([[0., 1., 1., 1., 0.],
+        [1., 0., 0., 1., 0.],
+        [1., 0., 0., 1., 1.],
+        [1., 1., 1., 0., 1.],
+        [0., 0., 1., 1., 0.]]))
+    """
+    # test input
+    # check n
+    if not np.issubdtype(type(n), np.integer):
+        raise TypeError("n is not of type int.")
+    elif n <= 0:
+        msg = "n must be > 0."
+        raise ValueError(msg)
+
+    # check p
+    if not np.issubdtype(type(p), np.floating):
+        raise TypeError("r is not of type float.")
+    elif p < 0 or p > 1:
+        msg = "p must between 0 and 1."
+        raise ValueError(msg)
+
+    # check r
+    check_r(r)
+
+    # check the relation between r and p
+    check_rel_er(p, r)
+
+    # check directed and loops
+    check_dirloop(directed, loops)
+
+    P = p * np.ones((n, n))
+    R = r * np.ones((n, n))
+    G1, G2 = sample_edges_corr(P, R, directed=directed, loops=loops)
+    return G1, G2
+
+
+def sbm_corr(n, p, r, directed=False, loops=False):
+    """
+    Generate a pair of correlated graphs with specified edge probability
+    Both G1 and G2 are binary matrices. 
+
+    Parameters
+    ----------
+    n: list of int, shape (n_communities)
+        Number of vertices in each community. Communities are assigned n[0], n[1], ...
+
+    p: array-like, shape (n_communities, n_communities)
+        Probability of an edge between each of the communities, where p[i, j] indicates 
+        the probability of a connection between edges in communities [i, j]. 
+        0 < p[i, j] < 1 for all i, j. 
+
+    r: float
+        Probability of the correlation between the same vertices in two graphs.
+    
+    directed: boolean, optional (default=False)
+        If False, output adjacency matrix will be symmetric. Otherwise, output adjacency
+        matrix will be asymmetric.
+    
+    loops: boolean, optional (default=False)
+        If False, no edges will be sampled in the diagonal. Otherwise, edges
+        are sampled in the diagonal.
+    
+    Returns
+    -------
+    G1: ndarray (n_vertices, n_vertices)
+        Adjacency matrix the same size as P representing a random graph.
+
+    G2: ndarray (n_vertices, n_vertices)
+        Adjacency matrix the same size as P representing a random graph.
+
+    Examples
+    --------
+    >>> np.random.seed(3)
+    >>> n = [3, 3]
+    >>> p = [[0.5, 0.1], [0.1, 0.5]]
+    >>> r = 0.3
+
+    To sample a correlated SBM graph pair based on n, p and r:
+
+    >>> sbm_corr(n, p, r, directed=False, loops=False)
+    (array([[0., 1., 0., 0., 0., 0.],
+        [1., 0., 0., 0., 0., 0.],
+        [0., 0., 0., 0., 0., 0.],
+        [0., 0., 0., 0., 0., 1.],
+        [0., 0., 0., 0., 0., 0.],
+        [0., 0., 0., 1., 0., 0.]]), array([[0., 1., 0., 0., 0., 0.],
+        [1., 0., 0., 1., 1., 0.],
+        [0., 0., 0., 0., 0., 0.],
+        [0., 1., 0., 0., 0., 1.],
+        [0., 1., 0., 0., 0., 0.],
+        [0., 0., 0., 1., 0., 0.]]))
+    """
+    # test input
+    # Check n
+    if not isinstance(n, (list, np.ndarray)):
+        msg = "n must be a list or np.array, not {}.".format(type(n))
+        raise TypeError(msg)
+    else:
+        n = np.array(n)
+        if not np.issubdtype(n.dtype, np.integer):
+            msg = "There are non-integer elements in n"
+            raise ValueError(msg)
+
+    # Check p
+    if not isinstance(p, (list, np.ndarray)):
+        msg = "p must be a list or np.array, not {}.".format(type(p))
+        raise TypeError(msg)
+    else:
+        p = np.array(p)
+        if not np.issubdtype(p.dtype, np.number):
+            msg = "There are non-numeric elements in p"
+            raise ValueError(msg)
+        elif p.shape != (n.size, n.size):
+            msg = "p is must have shape len(n) x len(n), not {}".format(p.shape)
+            raise ValueError(msg)
+        elif np.any(p < 0) or np.any(p > 1):
+            msg = "Values in p must be in between 0 and 1."
+            raise ValueError(msg)
+
+    # check r
+    check_r(r)
+
+    # check the relation between r and p
+    check_rel_sbm(p, r)
+
+    # check directed and loops
+    check_dirloop(directed, loops)
+
+    P = np.zeros((np.sum(n), np.sum(n)))
+    block_indices = np.insert(np.cumsum(np.array(n)), 0, 0)
+    for i in range(np.array(p).shape[0]):  # for each row
+        for j in range(np.array(p).shape[1]):  # for each column
+            P[
+                block_indices[i] : block_indices[i + 1],
+                block_indices[j] : block_indices[j + 1],
+            ] = p[i][j]
+    R = r * np.ones((np.sum(n), np.sum(n)))
+    G1, G2 = sample_edges_corr(P, R, directed=directed, loops=loops)
+    return G1, G2


Test patch:
diff --git a/tests/test_sims_corr.py b/tests/test_sims_corr.py
new file mode 100644
--- /dev/null
+++ b/tests/test_sims_corr.py
@@ -0,0 +1,193 @@
+import unittest
+from graspy.simulations.simulations_corr import sample_edges_corr, er_corr, sbm_corr
+import numpy as np
+import pytest
+import warnings
+
+
+class Test_Sample_Corr(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.n = 500
+        cls.p = 0.5
+        cls.r = 0.3
+        cls.P = cls.p * np.ones((cls.n, cls.n))
+        cls.R = cls.r * np.ones((cls.n, cls.n))
+
+    def test_bad_input(self):
+        with self.assertRaises(TypeError):
+            P = 10
+            sample_edges_corr(P, self.R, directed=False, loops=False)
+
+        with self.assertRaises(TypeError):
+            P = "0.5"
+            sample_edges_corr(P, self.R, directed=False, loops=False)
+
+        with self.assertRaises(TypeError):
+            R = 10
+            sample_edges_corr(self.P, R, directed=False, loops=False)
+
+        with self.assertRaises(TypeError):
+            R = "0.5"
+            sample_edges_corr(self.P, R, directed=False, loops=False)
+
+        with pytest.raises(TypeError):
+            sample_edges_corr(self.P, self.r, directed="hey", loops=False)
+
+        with pytest.raises(TypeError):
+            sample_edges_corr(self.P, self.r, directed=False, loops=6)
+
+    def test_sample_edges_corr(self):
+        # P = self.p * np.ones((self.n, self.n))
+        g1, g2 = sample_edges_corr(self.P, self.R, directed=False, loops=False)
+
+        # check the 1 probability of the output binary matrix
+        # should be near to the input P matrix
+        self.assertTrue(
+            np.isclose(self.p, g2.sum() / (self.n * (self.n - 1)), atol=0.05)
+        )
+
+        # check rho
+        k1 = g1.copy()
+        k2 = g2.copy()
+        k1 = k1[np.where(~np.eye(k1.shape[0], dtype=bool))]
+        k2 = k2[np.where(~np.eye(k2.shape[0], dtype=bool))]
+        output_r = np.corrcoef(k1, k2)[0, 1]
+        self.assertTrue(np.isclose(self.r, output_r, atol=0.06))
+
+        # check the dimension of input P and Rho
+        self.assertTrue(g1.shape == (self.n, self.n))
+        self.assertTrue(g2.shape == (self.n, self.n))
+
+
+class Test_ER_Corr(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.n = 500
+        cls.p = 0.5
+        cls.r = 0.3
+        cls.P = cls.p * np.ones((cls.n, cls.n))
+
+    def test_bad_input(self):
+        with self.assertRaises(TypeError):
+            n = "10"
+            er_corr(n, self.p, self.r, directed=False, loops=False)
+
+        with self.assertRaises(ValueError):
+            n = -1
+            er_corr(n, self.p, self.r, directed=False, loops=False)
+
+        with self.assertRaises(TypeError):
+            p = "1"
+            er_corr(self.n, p, self.r, directed=False, loops=False)
+
+        with self.assertRaises(ValueError):
+            p = -0.5
+            er_corr(self.n, p, self.r, directed=False, loops=False)
+
+        with self.assertRaises(ValueError):
+            p = 5.0
+            er_corr(self.n, p, self.r, directed=False, loops=False)
+
+        with self.assertRaises(ValueError):
+            r = -2.0
+            er_corr(self.n, self.p, r, directed=False, loops=False)
+
+        with self.assertRaises(ValueError):
+            r = 5.0
+            er_corr(self.n, self.p, r, directed=False, loops=False)
+
+    def test_er_corr(self):
+        g1, g2 = er_corr(self.n, self.p, self.r, directed=False, loops=False)
+        # check the 1 probability of the output binary matrix
+        # should be near to the input P matrix
+        self.assertTrue(
+            np.isclose(self.p, g2.sum() / (self.n * (self.n - 1)), atol=0.05)
+        )
+
+        # check rho
+        k1 = g1.copy()
+        k2 = g2.copy()
+        k1 = k1[np.where(~np.eye(k1.shape[0], dtype=bool))]
+        k2 = k2[np.where(~np.eye(k2.shape[0], dtype=bool))]
+        output_r = np.corrcoef(k1, k2)[0, 1]
+        self.assertTrue(np.isclose(self.r, output_r, atol=0.06))
+
+        # check the dimension of input P and Rho
+        self.assertTrue(g1.shape == (self.n, self.n))
+        self.assertTrue(g2.shape == (self.n, self.n))
+
+
+class Test_SBM_Corr(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.n = [100, 100]
+        cls.p = [[0.5, 0.2], [0.2, 0.5]]
+        cls.r = 0.3
+        cls.p1 = np.array(cls.p)
+
+    def test_bad_input(self):
+        with self.assertRaises(TypeError):
+            n = "1"
+            sbm_corr(n, self.p, self.r, directed=False, loops=False)
+
+        with self.assertRaises(ValueError):
+            n = ["1", 10]
+            sbm_corr(n, self.p, self.r, directed=False, loops=False)
+
+        with self.assertRaises(TypeError):
+            p = 0.5
+            sbm_corr(self.n, p, self.r, directed=False, loops=False)
+
+        with self.assertRaises(ValueError):
+            p = [[0.5]]
+            sbm_corr(self.n, p, self.r, directed=False, loops=False)
+
+        with self.assertRaises(ValueError):
+            p = [[5, 5], [4, 4]]
+            sbm_corr(self.n, p, self.r, directed=False, loops=False)
+
+        with self.assertRaises(ValueError):
+            p = ["str"]
+            sbm_corr(self.n, p, self.r, directed=False, loops=False)
+
+        with self.assertRaises(ValueError):
+            r = -2.0
+            sbm_corr(self.n, self.p, r, directed=False, loops=False)
+
+        with self.assertRaises(ValueError):
+            r = 5.0
+            sbm_corr(self.n, self.p, r, directed=False, loops=False)
+
+        with pytest.raises(TypeError):
+            sbm_corr(self.n, self.p, self.r, directed="hey", loops=False)
+
+        with pytest.raises(TypeError):
+            sbm_corr(self.n, self.p, self.r, directed=False, loops=6)
+
+    def test_sbm_corr(self):
+        g1, g2 = sbm_corr(self.n, self.p, self.r, directed=False, loops=False)
+        a1, a2 = g1[0 : self.n[0], 0 : self.n[0]], g1[0 : self.n[0], self.n[0] :]
+        b1, b2 = g2[0 : self.n[0], 0 : self.n[0]], g2[0 : self.n[0], self.n[0] :]
+        pb1, pb2 = (
+            b1.sum() / (self.n[0] * (self.n[0] - 1)),
+            b2.sum() / (self.n[0] * self.n[1]),
+        )
+        # check the 1 probability of the output binary matrix
+        # should be near to the input P matrix
+        self.assertTrue(np.isclose(self.p1[0][0], pb1, atol=0.05))
+        self.assertTrue(np.isclose(self.p1[0][1], pb2, atol=0.05))
+
+        # check rho
+        a1 = a1[np.where(~np.eye(a1.shape[0], dtype=bool))]
+        b1 = b1[np.where(~np.eye(a2.shape[0], dtype=bool))]
+        a2 = a2.flatten()
+        b2 = b2.flatten()
+        m1 = np.corrcoef(a1, b1)[0, 1]
+        m2 = np.corrcoef(a2, b2)[0, 1]
+        avr = (m1 + m2) / 2
+        self.assertTrue(np.isclose(avr, self.r, atol=0.05))
+
+        # check the dimension of input P and Rho
+        self.assertTrue(g1.shape == (np.sum(self.n), np.sum(self.n)))
+        self.assertTrue(g2.shape == (np.sum(self.n), np.sum(self.n)))