Repository: networkx/networkx

Issue:
sparse matrix todense changes diag
I was adding in code for community detection, and all my tests started failing.

Found this behaviour when roundtripping from_numpy_matrix, and adj_matrix:

In [63]: jnk = np.eye(10)
array([[ 1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.]])

In [64]: graph = networkx.from_numpy_matrix(jnk)

In [65]: mat = networkx.adjacency_matrix(graph).todense()

In [66]: mat
Out[66]: 
matrix([[ 2.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
        [ 0.,  2.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
        [ 0.,  0.,  2.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
        [ 0.,  0.,  0.,  2.,  0.,  0.,  0.,  0.,  0.,  0.],
        [ 0.,  0.,  0.,  0.,  2.,  0.,  0.,  0.,  0.,  0.],
        [ 0.,  0.,  0.,  0.,  0.,  2.,  0.,  0.,  0.,  0.],
        [ 0.,  0.,  0.,  0.,  0.,  0.,  2.,  0.,  0.,  0.],
        [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  2.,  0.,  0.],
        [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  2.,  0.],
        [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  2.]])

In [67]: graph = networkx.from_numpy_matrix(mat)

In [68]: mat2 = networkx.adjacency_matrix(graph).todense()

In [69]: mat2
Out[69]: 
matrix([[ 4.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
        [ 0.,  4.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
        [ 0.,  0.,  4.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
        [ 0.,  0.,  0.,  4.,  0.,  0.,  0.,  0.,  0.,  0.],
        [ 0.,  0.,  0.,  0.,  4.,  0.,  0.,  0.,  0.,  0.],
        [ 0.,  0.,  0.,  0.,  0.,  4.,  0.,  0.,  0.,  0.],
        [ 0.,  0.,  0.,  0.,  0.,  0.,  4.,  0.,  0.,  0.],
        [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  4.,  0.,  0.],
        [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  4.,  0.],
        [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  4.]])

Thoughts?



Discussion:
Looks like self loops are getting handled incorrectly (doubled).
Thanks for reporting this.


Resolving patch:
diff --git a/networkx/convert_matrix.py b/networkx/convert_matrix.py
--- a/networkx/convert_matrix.py
+++ b/networkx/convert_matrix.py
@@ -1,27 +1,26 @@
-"""Functions to convert NetworkX graphs to and from other formats.
+"""Functions to convert NetworkX graphs to and from numpy/scipy matrices.
 
 The preferred way of converting data to a NetworkX graph is through the
 graph constuctor.  The constructor calls the to_networkx_graph() function
 which attempts to guess the input type and convert it automatically.
 
 Examples
-
-
+--------
 Create a 10 node random graph from a numpy matrix
 
 >>> import numpy
->>> a=numpy.reshape(numpy.random.random_integers(0,1,size=100),(10,10))
->>> D=nx.DiGraph(a)
+>>> a = numpy.reshape(numpy.random.random_integers(0,1,size=100),(10,10))
+>>> D = nx.DiGraph(a)
 
 or equivalently
 
->>> D=nx.to_networkx_graph(a,create_using=nx.DiGraph())
+>>> D = nx.to_networkx_graph(a,create_using=nx.DiGraph())
 
 See Also
 --------
 nx_pygraphviz, nx_pydot
 """
-#    Copyright (C) 2006-2013 by
+#    Copyright (C) 2006-2014 by
 #    Aric Hagberg <hagberg@lanl.gov>
 #    Dan Schult <dschult@colgate.edu>
 #    Pieter Swart <swart@lanl.gov>
@@ -80,7 +79,7 @@ def to_numpy_matrix(G, nodelist=None, dtype=None, order=None,
     Returns
     -------
     M : NumPy matrix
-       Graph adjacency matrix.
+       Graph adjacency matrix
 
     See Also
     --------
@@ -88,14 +87,30 @@ def to_numpy_matrix(G, nodelist=None, dtype=None, order=None,
 
     Notes
     -----
-    The matrix entries are assigned with weight edge attribute. When
-    an edge does not have the weight attribute, the value of the entry is 1.
-    For multiple edges, the values of the entries are the sums of the edge
-    attributes for each edge.
+    The matrix entries are assigned to the weight edge attribute. When
+    an edge does not have a weight attribute, the value of the entry is set to
+    the number 1.  For multiple (parallel) edges, the values of the entries
+    are determined by the 'multigraph_weight' paramter.  The default is to
+    sum the weight attributes for each of the parallel edges.
 
     When `nodelist` does not contain every node in `G`, the matrix is built
     from the subgraph of `G` that is induced by the nodes in `nodelist`.
 
+    The convention used for self-loop edges in graphs is to assign the
+    diagonal matrix entry value to the weight attributr of the edge
+    (or the number 1 if the edge has no weight attribute).  If the
+    alternate convention of doubling the edge weight is desired the
+    resulting Numpy matrix can be modified as follows:
+
+    >>> import numpy as np
+    >>> G = nx.Graph([(1,1)])
+    >>> A = nx.to_numpy_matrix(G)
+    >>> A
+    matrix([[ 1.]])
+    >>> A.A[np.diag_indices_from(A)] *= 2
+    >>> A
+    matrix([[ 2.]])
+
     Examples
     --------
     >>> G = nx.MultiDiGraph()
@@ -394,6 +409,21 @@ def to_scipy_sparse_matrix(G, nodelist=None, dtype=None,
     Uses coo_matrix format. To convert to other formats specify the
     format= keyword.
 
+    The convention used for self-loop edges in graphs is to assign the
+    diagonal matrix entry value to the weight attribute of the edge
+    (or the number 1 if the edge has no weight attribute).  If the
+    alternate convention of doubling the edge weight is desired the
+    resulting Scipy sparse matrix can be modified as follows:
+
+    >>> import scipy as sp
+    >>> G = nx.Graph([(1,1)])
+    >>> A = nx.to_scipy_sparse_matrix(G)
+    >>> print(A.todense())
+    [[1]]
+    >>> A.setdiag(A.diagonal()*2)
+    >>> print(A.todense())
+    [[2]]
+
     Examples
     --------
     >>> G = nx.MultiDiGraph()
@@ -435,8 +465,20 @@ def to_scipy_sparse_matrix(G, nodelist=None, dtype=None,
                               shape=(nlen,nlen), dtype=dtype)
     else:
         # symmetrize matrix
-        M = sparse.coo_matrix((data+data, (row+col,col+row)),
-                              shape=(nlen,nlen), dtype=dtype)
+        d = data + data
+        r = row + col
+        c = col + row
+        # selfloop entries get double counted when symmetrizing
+        # so we subtract the data on the diagonal
+        selfloops = G.selfloop_edges(data=True)
+        if selfloops:
+            diag_index,diag_data = zip(*((index[u],-d.get(weight,1))
+                                         for u,v,d in selfloops
+                                         if u in index and v in index))
+            d += diag_data
+            r += diag_index
+            c += diag_index
+        M = sparse.coo_matrix((d, (r, c)), shape=(nlen,nlen), dtype=dtype)
     try:
         return M.asformat(format)
     except AttributeError:
diff --git a/networkx/linalg/graphmatrix.py b/networkx/linalg/graphmatrix.py
--- a/networkx/linalg/graphmatrix.py
+++ b/networkx/linalg/graphmatrix.py
@@ -131,12 +131,28 @@ def adjacency_matrix(G, nodelist=None, weight='weight'):
     dictionary-of-dictionaries format that can be addressed as a
     sparse matrix.
 
-    For MultiGraph/MultiDiGraph, the edges weights are summed.
+    For MultiGraph/MultiDiGraph with parallel edges the weights are summed.
     See to_numpy_matrix for other options.
 
+    The convention used for self-loop edges in graphs is to assign the
+    diagonal matrix entry value to the edge weight attribute
+    (or the number 1 if the edge has no weight attribute).  If the
+    alternate convention of doubling the edge weight is desired the
+    resulting Scipy sparse matrix can be modified as follows:
+
+    >>> import scipy as sp
+    >>> G = nx.Graph([(1,1)])
+    >>> A = nx.adjacency_matrix(G)
+    >>> print(A.todense())
+    [[1]]
+    >>> A.setdiag(A.diagonal()*2)
+    >>> print(A.todense())
+    [[2]]
+
     See Also
     --------
     to_numpy_matrix
+    to_scipy_sparse_matrix
     to_dict_of_dicts
     """
     return nx.to_scipy_sparse_matrix(G,nodelist=nodelist,weight=weight)
@@ -147,6 +163,6 @@ def adjacency_matrix(G, nodelist=None, weight='weight'):
 def setup_module(module):
     from nose import SkipTest
     try:
-        import numpy
+        import scipy
     except:
-        raise SkipTest("NumPy not available")
+        raise SkipTest("SciPy not available")
diff --git a/networkx/linalg/laplacianmatrix.py b/networkx/linalg/laplacianmatrix.py
--- a/networkx/linalg/laplacianmatrix.py
+++ b/networkx/linalg/laplacianmatrix.py
@@ -117,10 +117,6 @@ def normalized_laplacian_matrix(G, nodelist=None, weight='weight'):
         nodelist = G.nodes()
     A = nx.to_scipy_sparse_matrix(G, nodelist=nodelist, weight=weight,
                                   format='csr')
-    # the convention for normalized Laplacian is to not count self loops
-    # twice in the diagonal.  So we remove one here.
-    for n,_ in G.selfloop_edges():
-        A[n,n] -= 1
     n,m = A.shape
     diags = A.sum(axis=1).flatten()
     D = scipy.sparse.spdiags(diags, [0], m, n, format='csr')


Test patch:
diff --git a/networkx/tests/test_convert_scipy.py b/networkx/tests/test_convert_scipy.py
--- a/networkx/tests/test_convert_scipy.py
+++ b/networkx/tests/test_convert_scipy.py
@@ -177,3 +177,13 @@ def test_ordering(self):
         G.add_edge(3,1)
         M = nx.to_scipy_sparse_matrix(G,nodelist=[3,2,1])
         np_assert_equal(M.todense(), np.matrix([[0,0,1],[1,0,0],[0,1,0]]))
+
+    def test_selfloop_graph(self):
+        G = nx.Graph([(1,1)])
+        M = nx.to_scipy_sparse_matrix(G)
+        np_assert_equal(M.todense(), np.matrix([[1]]))
+
+    def test_selfloop_digraph(self):
+        G = nx.DiGraph([(1,1)])
+        M = nx.to_scipy_sparse_matrix(G)
+        np_assert_equal(M.todense(), np.matrix([[1]]))