Repository: networkx/networkx

Issue:
numeric assortativity coefficient only works for nonnegative numeric attributes
<!-- If you have a general question about NetworkX, please use the discussions tab to create a new discussion -->

<!--- Provide a general summary of the issue in the Title above -->


### Current Behavior
<!--- Tell us what happens instead of the expected behavior -->

If the range of numeric attributes ranges from negative to positive (and not all negative), `nx.numeric_mixing_matrix` effectively disregards it since it is generated using `mapping = {x: x for x in range(m + 1)}`.

### Expected Behavior
<!--- Tell us what should happen -->
The negative integers should be included in generating the matrix. Alternatively:
1. just use the Pearson correlation coefficient as given in Peel et al, eq. S4-S5 (Leto Peel, Jean-Charles Delvenne, Renaud Lambiotte, Multiscale mixing patterns in networks, PNAS Apr 2018, 115 (16) 4057-4062; DOI: 10.1073/pnas.1713019115). This has the added benefit that the scalar quantities need not be integers.
2. require that the integers be nonnegative
3. increase the size of the matrix so it includes the negative integers as well.



### Steps to Reproduce
<!--- Provide a minimal example that reproduces the bug -->

```
G = nx.Graph()
G.add_nodes_from([1,2], a=-1)
G.add_nodes_from([3], a=1)
G.add_nodes_from([4], a=2)
G.add_edges_from([(3,4),(1,2),(1,3)])
# numeric_assortativity_coefficient gives -1.0
print(nx.numeric_assortativity_coefficient(G, 'a'))
```

### Environment
<!--- Please provide details about your local environment -->
Python version: 3.8
NetworkX version: 2.5


### Additional context
<!--- Add any other context about the problem here, screenshots, etc. -->


Discussion:

Patch:
diff --git a/doc/release/migration_guide_from_2.x_to_3.0.rst b/doc/release/migration_guide_from_2.x_to_3.0.rst
new file mode 100644
index 000000000..d02ac76c8
--- /dev/null
+++ b/doc/release/migration_guide_from_2.x_to_3.0.rst
@@ -0,0 +1,44 @@
+:orphan:
+
+*****************************
+Preparing for the 3.0 release
+*****************************
+
+.. note::
+   Much of the work leading to the NetworkX 3.0 release will be included
+   in the NetworkX 2.6 and 2.7 releases.  For example, we are changing our
+   default dependencies in the 2.6 release and we are deprecating a lot
+   of old code in the 2.6 and 2.7 releases.  This guide will discuss this
+   ongoing work and will help you understand what changes you can make now
+   to minimize the disruption caused by the move to 3.0.
+
+This is a guide for people moving from NetworkX 2.X to NetworkX 3.0
+
+Any issues with these can be discussed on the `mailing list
+<https://groups.google.com/forum/#!forum/networkx-discuss>`_.
+
+The focus of 3.0 release is on addressing years of technical debt, modernizing our codebase,
+improving performance, and making it easier to contribute.
+We plan to release 2.7 near the end of summer and 3.0 near the end of the year.
+
+Default dependencies
+--------------------
+
+We no longer depend on the "decorator" library.
+
+NumPy, SciPy, Matplotlib, and Pandas are now default dependencies due to their
+central position in the scientific Python ecosystem and the fact that they
+provide self-contained, easy to install wheels for pip.
+While we've made these core libraries of the scientific Python ecosystem default dependencies,
+much of NetworkX still works without these libraries and we expect that to continue.
+Be assured that we test NetworkX with and without the dependencies,
+so if you do **not** use functions that depend on external libraries
+you can install NetworkX using pip with the `--no-deps` flag::
+
+    $ pip install networkx --no-deps
+
+Deprecated code
+---------------
+
+The 2.6 release deprecates over 30 functions.
+See :ref:`NetworkX 2.6`.
diff --git a/doc/release/release_dev.rst b/doc/release/release_dev.rst
index 07db238b2..46a5c08ef 100644
--- a/doc/release/release_dev.rst
+++ b/doc/release/release_dev.rst
@@ -1,3 +1,5 @@
+.. _NetworkX 2.6:
+
 Next Release
 ============
 
@@ -5,6 +7,8 @@ Release date: TBD
 
 Supports Python 3.7, 3.8, and 3.9.
 
+See :doc:`migration_guide_from_2.x_to_3.0`.
+
 NetworkX is a Python package for the creation, manipulation, and study of the
 structure, dynamics, and functions of complex networks.
 
diff --git a/examples/algorithms/plot_parallel_betweenness.py b/examples/algorithms/plot_parallel_betweenness.py
index 7a27abaaf..aa88652b6 100644
--- a/examples/algorithms/plot_parallel_betweenness.py
+++ b/examples/algorithms/plot_parallel_betweenness.py
@@ -10,8 +10,12 @@ The function betweenness centrality accepts a bunch of nodes and computes
 the contribution of those nodes to the betweenness centrality of the whole
 network. Here we divide the network in chunks of nodes and we compute their
 contribution to the betweenness centrality of the whole network.
-"""
 
+Note: The example output below shows that the non-parallel implementation is
+faster. This is a limitation of our CI/CD pipeline running on a single core.
+
+Depending on your setup, you will likely observe a speedup.
+"""
 from multiprocessing import Pool
 import time
 import itertools
diff --git a/networkx/algorithms/assortativity/correlation.py b/networkx/algorithms/assortativity/correlation.py
index b885a22ee..8a244fc7d 100644
--- a/networkx/algorithms/assortativity/correlation.py
+++ b/networkx/algorithms/assortativity/correlation.py
@@ -73,8 +73,12 @@ def degree_assortativity_coefficient(G, x="out", y="in", weight=None, nodes=None
     .. [2] Foster, J.G., Foster, D.V., Grassberger, P. & Paczuski, M.
        Edge direction and the structure of networks, PNAS 107, 10815-20 (2010).
     """
-    M = degree_mixing_matrix(G, x=x, y=y, nodes=nodes, weight=weight)
-    return numeric_ac(M)
+    if nodes is None:
+        nodes = G.nodes
+    degrees = set([d for n, d in G.degree(nodes, weight=weight)])
+    mapping = {d: i for i, d, in enumerate(degrees)}
+    M = degree_mixing_matrix(G, x=x, y=y, nodes=nodes, weight=weight, mapping=mapping)
+    return numeric_ac(M, mapping=mapping)
 
 
 def degree_pearson_correlation_coefficient(G, x="out", y="in", weight=None, nodes=None):
@@ -223,8 +227,12 @@ def numeric_assortativity_coefficient(G, attribute, nodes=None):
     .. [1] M. E. J. Newman, Mixing patterns in networks
            Physical Review E, 67 026126, 2003
     """
-    a = numeric_mixing_matrix(G, attribute, nodes)
-    return numeric_ac(a)
+    if nodes is None:
+        nodes = G.nodes
+    vals = set(G.nodes[n][attribute] for n in nodes)
+    mapping = {d: i for i, d, in enumerate(vals)}
+    M = attribute_mixing_matrix(G, attribute, nodes, mapping)
+    return numeric_ac(M, mapping)
 
 
 def attribute_ac(M):
@@ -254,7 +262,7 @@ def attribute_ac(M):
     return r
 
 
-def numeric_ac(M):
+def numeric_ac(M, mapping):
     # M is a numpy matrix or array
     # numeric assortativity coefficient, pearsonr
     import numpy as np
@@ -262,12 +270,13 @@ def numeric_ac(M):
     if M.sum() != 1.0:
         M = M / float(M.sum())
     nx, ny = M.shape  # nx=ny
-    x = np.arange(nx)
-    y = np.arange(ny)
+    x = np.array(list(mapping.keys()))
+    y = x  # x and y have the same support
+    idx = list(mapping.values())
     a = M.sum(axis=0)
     b = M.sum(axis=1)
-    vara = (a * x ** 2).sum() - ((a * x).sum()) ** 2
-    varb = (b * x ** 2).sum() - ((b * x).sum()) ** 2
+    vara = (a[idx] * x ** 2).sum() - ((a[idx] * x).sum()) ** 2
+    varb = (b[idx] * y ** 2).sum() - ((b[idx] * y).sum()) ** 2
     xy = np.outer(x, y)
-    ab = np.outer(a, b)
+    ab = np.outer(a[idx], b[idx])
     return (xy * (M - ab)).sum() / np.sqrt(vara * varb)
diff --git a/networkx/algorithms/centrality/katz.py b/networkx/algorithms/centrality/katz.py
index c528bafc5..a113ab0ee 100644
--- a/networkx/algorithms/centrality/katz.py
+++ b/networkx/algorithms/centrality/katz.py
@@ -298,7 +298,7 @@ def katz_centrality_numpy(G, alpha=0.1, beta=1.0, normalized=True, weight=None):
     ----------
     .. [1] Mark E. J. Newman:
        Networks: An Introduction.
-       Oxford University Press, USA, 2010, p. 720.
+       Oxford University Press, USA, 2010, p. 173.
     .. [2] Leo Katz:
        A New Status Index Derived from Sociometric Index.
        Psychometrika 18(1):39–43, 1953
diff --git a/networkx/algorithms/community/quality.py b/networkx/algorithms/community/quality.py
index ba972277c..9f374435f 100644
--- a/networkx/algorithms/community/quality.py
+++ b/networkx/algorithms/community/quality.py
@@ -3,8 +3,7 @@ communities).
 
 """
 
-from functools import wraps
-from itertools import product, combinations
+from itertools import combinations
 
 import networkx as nx
 from networkx import NetworkXError
@@ -78,7 +77,7 @@ def intra_community_edges(G, partition):
 
 
 def inter_community_edges(G, partition):
-    """Returns the number of inter-community edges for a prtition of `G`.
+    """Returns the number of inter-community edges for a partition of `G`.
     according to the given
     partition of the nodes of `G`.
 
@@ -112,9 +111,12 @@ def inter_community_non_edges(G, partition):
     """Returns the number of inter-community non-edges according to the
     given partition of the nodes of `G`.
 
-    `G` must be a NetworkX graph.
+    Parameters
+    ----------
+    G : NetworkX graph.
 
-    `partition` must be a partition of the nodes of `G`.
+    partition : iterable of sets of nodes
+        This must be a partition of the nodes of `G`.
 
     A *non-edge* is a pair of nodes (undirected if `G` is undirected)
     that are not adjacent in `G`. The *inter-community non-edges* are
@@ -145,8 +147,8 @@ def performance(G, partition):
     .. deprecated:: 2.6
        Use `partition_quality` instead.
 
-    The *performance* of a partition is the ratio of the number of
-    intra-community edges plus inter-community non-edges with the total
+    The *performance* of a partition is the number of
+    intra-community edges plus inter-community non-edges divided by the total
     number of potential edges.
 
     Parameters
@@ -358,8 +360,8 @@ def partition_quality(G, partition):
     The *coverage* of a partition is the ratio of the number of
     intra-community edges to the total number of edges in the graph.
 
-    The *performance* of a partition is the ratio of the number of
-    intra-community edges plus inter-community non-edges with the total
+    The *performance* of a partition is the number of
+    intra-community edges plus inter-community non-edges divided by the total
     number of potential edges.
 
     This algorithm has complexity $O(C^2 + L)$ where C is the number of communities and L is the number of links.
diff --git a/networkx/algorithms/minors/contraction.py b/networkx/algorithms/minors/contraction.py
index a879b9b01..199c22726 100644
--- a/networkx/algorithms/minors/contraction.py
+++ b/networkx/algorithms/minors/contraction.py
@@ -51,6 +51,12 @@ def equivalence_classes(iterable, relation):
         Duplicate elements will be ignored so it makes the most sense for
         `iterable` to be a :class:`set`.
 
+    Notes
+    -----
+    This function does not check that `relation` represents an equivalence
+    relation. You can check that your equivalence classes provide a partition
+    using `is_partition`.
+
     Examples
     --------
     Let `X` be the set of integers from `0` to `9`, and consider an equivalence
@@ -128,10 +134,9 @@ def quotient_graph(
 
     edge_relation : Boolean function with two arguments
         This function must represent an edge relation on the *blocks* of
-        `G` in the partition induced by `node_relation`. It must
-        take two arguments, *B* and *C*, each one a set of nodes, and
-        return True exactly when there should be an edge joining
-        block *B* to block *C* in the returned graph.
+        the `partition` of `G`. It must take two arguments, *B* and *C*,
+        each one a set of nodes, and return True exactly when there should be
+        an edge joining block *B* to block *C* in the returned graph.
 
         If `edge_relation` is not specified, it is assumed to be the
         following relation. Block *B* is related to block *C* if and
@@ -296,6 +301,10 @@ def quotient_graph(
     if callable(partition):
         # equivalence_classes always return partition of whole G.
         partition = equivalence_classes(G, partition)
+        if not nx.community.is_partition(G, partition):
+            raise nx.NetworkXException(
+                "Input `partition` is not an equivalence relation for nodes of G"
+            )
         return _quotient_graph(
             G, partition, edge_relation, node_data, edge_data, relabel, create_using
         )
@@ -311,6 +320,9 @@ def quotient_graph(
     partition_nodes = set().union(*partition)
     if len(partition_nodes) != len(G):
         G = G.subgraph(partition_nodes)
+    # Each node in the graph/subgraph must be in exactly one block.
+    if not nx.community.is_partition(G, partition):
+        raise NetworkXException("each node must be in exactly one part of `partition`")
     return _quotient_graph(
         G, partition, edge_relation, node_data, edge_data, relabel, create_using
     )
@@ -325,9 +337,7 @@ def _quotient_graph(
     relabel=False,
     create_using=None,
 ):
-    # Each node in the graph must be in exactly one block.
-    if any(sum(1 for b in partition if v in b) != 1 for v in G):
-        raise NetworkXException("each node must be in exactly one block")
+    """Construct the quotient graph assuming input has been checked"""
     if create_using is None:
         H = G.__class__()
     else:
diff --git a/networkx/readwrite/gml.py b/networkx/readwrite/gml.py
index a2f5a0b50..153e6a0e2 100644
--- a/networkx/readwrite/gml.py
+++ b/networkx/readwrite/gml.py
@@ -163,8 +163,20 @@ def read_gml(path, label="label", destringizer=None):
     --------
     >>> G = nx.path_graph(4)
     >>> nx.write_gml(G, "test.gml")
+
+    GML values are interpreted as strings by default:
+
     >>> H = nx.read_gml("test.gml")
-    >>> I = nx.read_gml("test.gml", destringizer=int)
+    >>> H.nodes
+    NodeView(('0', '1', '2', '3'))
+
+    When a `destringizer` is provided, GML values are converted to the provided type.
+    For example, integer nodes can be recovered as shown below:
+
+    >>> J = nx.read_gml("test.gml", destringizer=int)
+    >>> J.nodes
+    NodeView((0, 1, 2, 3))
+
     """
 
     def filter_lines(lines):


Test patch:
diff --git a/networkx/algorithms/assortativity/tests/base_test.py b/networkx/algorithms/assortativity/tests/base_test.py
index 5b3712310..ee5b126c0 100644
--- a/networkx/algorithms/assortativity/tests/base_test.py
+++ b/networkx/algorithms/assortativity/tests/base_test.py
@@ -52,6 +52,10 @@ class BaseTestDegreeMixing:
         cls.W = nx.Graph()
         cls.W.add_edges_from([(0, 3), (1, 3), (2, 3)], weight=0.5)
         cls.W.add_edge(0, 2, weight=1)
+        S1 = nx.star_graph(4)
+        S2 = nx.star_graph(4)
+        cls.DS = nx.disjoint_union(S1, S2)
+        cls.DS.add_edge(4, 5)
 
 
 class BaseTestNumericMixing:
@@ -70,3 +74,10 @@ class BaseTestNumericMixing:
         F.add_edge(0, 2, weight=1)
         nx.set_node_attributes(F, dict(F.degree(weight="weight")), "margin")
         cls.F = F
+
+        M = nx.Graph()
+        M.add_nodes_from([1, 2], margin=-1)
+        M.add_nodes_from([3], margin=1)
+        M.add_nodes_from([4], margin=2)
+        M.add_edges_from([(3, 4), (1, 2), (1, 3)])
+        cls.M = M
diff --git a/networkx/algorithms/assortativity/tests/test_correlation.py b/networkx/algorithms/assortativity/tests/test_correlation.py
index 763f0a415..8da72652f 100644
--- a/networkx/algorithms/assortativity/tests/test_correlation.py
+++ b/networkx/algorithms/assortativity/tests/test_correlation.py
@@ -42,6 +42,10 @@ class TestDegreeMixingCorrelation(BaseTestDegreeMixing):
         r = nx.degree_assortativity_coefficient(self.W, weight="weight")
         np.testing.assert_almost_equal(r, -0.1429, decimal=4)
 
+    def test_degree_assortativity_double_star(self):
+        r = nx.degree_assortativity_coefficient(self.DS)
+        np.testing.assert_almost_equal(r, -0.9339, decimal=4)
+
 
 class TestAttributeMixingCorrelation(BaseTestAttributeMixing):
     def test_attribute_assortativity_undirected(self):
@@ -91,3 +95,7 @@ class TestNumericMixingCorrelation(BaseTestNumericMixing):
     def test_numeric_assortativity_float(self):
         r = nx.numeric_assortativity_coefficient(self.F, "margin")
         np.testing.assert_almost_equal(r, -0.1429, decimal=4)
+
+    def test_numeric_assortativity_mixed(self):
+        r = nx.numeric_assortativity_coefficient(self.M, "margin")
+        np.testing.assert_almost_equal(r, 0.4340, decimal=4)