Repository: networkx/networkx
Pull request: refactor: modifications and additional tests for weighted distance
Fixes #5257

Additional tests have been added for weighted graph distance computations.

Problem statement:
Distance_measures.py functions should work for weighted graphs too.
Many of the functions in `distance_measures.py` work for unweighted graphs, but have straightforward definitions for weighted graphs too.  We should go through this module and upgrade each function to work for weighted graphs.  It should be a straightforward fix with a keyword "weight" input which defaults to the current unweighted treatment.  The `shortest_path` functions provide a nice interface for that to use as an example.

Hints from issue discussion:
To be clear, we mean `distance_measures.py`, right? Or is `distance_measurements.py` somewhere and I'm just not seeing it?
Thanks -- Yes -- I mean `distance_measures.py`.  I'll correct it by editing my statement.  
Can i work on this issue

Hey @dschult 
I would like to work on it.
Thanks.
Hi All, I would also be interested in contributing to this.

It looks like there are handful of functions to address - perhaps we can split the work among ourselves?
> Hi All, I would also be interested in contributing to this.
> 
> It looks like there are handful of functions to address - perhaps we can split the work among ourselves?

sure lets do it

Yeah @nishant42491 @lucasmccabe :)
Hi guys,

Are you still working actively on this? Otherwise I can also take over some work.
Hello, if you guys are still working on this and need an extra helping hand, do add me too!
Hello all! It's great to see so much interest to work on the project :)

You can read more about the development processes on our website:

- [New Contributor FAQ](https://networkx.org/documentation/latest/developer/new_contributor_faq.html)
- [Contributor Guide](https://networkx.org/documentation/latest/developer/contribute.html)

For the point about assigning issues we don't usually assign issues to anyone, feel free to open a PR when you are ready.
If you are worried it may conflict with what other people are working on please put in the method you are working on in this issue and open a PR.

Even if you are stuck please do open a PR (you can keep it as a draft) as we can help you review and improve the code in the PR :)

Commit message: docs: correct formatting of references

impacted: resistance_distance and __extrema_bounding

Patch to review:
diff --git a/networkx/algorithms/distance_measures.py b/networkx/algorithms/distance_measures.py
index 5edd3962152..5b70fe6cc88 100644
--- a/networkx/algorithms/distance_measures.py
+++ b/networkx/algorithms/distance_measures.py
@@ -14,7 +14,7 @@
 ]
 
 
-def _extrema_bounding(G, compute="diameter"):
+def _extrema_bounding(G, compute="diameter", weight="weight"):
     """Compute requested extreme distance metric of undirected graph G
 
     Computation is based on smart lower and upper bounds, and in practice
@@ -33,6 +33,24 @@ def _extrema_bounding(G, compute="diameter"):
        "center" for the set of nodes with eccentricity equal to the radius,
        "eccentricities" for the maximum distance from each node to all other nodes in G
 
+    weight : string or function
+        If this is a string, then edge weights will be accessed via the
+        edge attribute with this key (that is, the weight of the edge
+        joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
+        such edge attribute exists, the weight of the edge is assumed to
+        be one.
+
+        If this is a function, the weight of an edge is the value
+        returned by the function. The function must accept exactly three
+        positional arguments: the two endpoints of an edge and the
+        dictionary of edge attributes for that edge. The function must
+        return a number.
+
+        Weights stored as floating point values can lead to small round-off
+        errors in distances. Use integer weights to avoid this.
+
+        Weights should be positive, since they are distances.
+
     Returns
     -------
     value : value of the requested metric
@@ -46,25 +64,26 @@ def _extrema_bounding(G, compute="diameter"):
         If the graph consists of multiple components
     ValueError
         If `compute` is not one of "diameter", "radius", "periphery", "center", or "eccentricities".
+
     Notes
     -----
-    This algorithm was proposed in the following papers:
+    This algorithm was proposed in [1]_ and discussed further in [2]_ and [3]_.
 
-    F.W. Takes and W.A. Kosters, Determining the Diameter of Small World
-    Networks, in Proceedings of the 20th ACM International Conference on
-    Information and Knowledge Management (CIKM 2011), pp. 1191-1196, 2011.
-    doi: https://doi.org/10.1145/2063576.2063748
-
-    F.W. Takes and W.A. Kosters, Computing the Eccentricity Distribution of
-    Large Graphs, Algorithms 6(1): 100-118, 2013.
-    doi: https://doi.org/10.3390/a6010100
-
-    M. Borassi, P. Crescenzi, M. Habib, W.A. Kosters, A. Marino and F.W. Takes,
-    Fast Graph Diameter and Radius BFS-Based Computation in (Weakly Connected)
-    Real-World Graphs, Theoretical Computer Science 586: 59-80, 2015.
-    doi: https://doi.org/10.1016/j.tcs.2015.02.033
+    References
+    ----------
+    .. [1] F. W. Takes, W. A. Kosters,
+       "Determining the diameter of small world networks."
+       Proceedings of the 20th ACM international conference on Information and knowledge management, 2011
+       https://dl.acm.org/doi/abs/10.1145/2063576.2063748
+    .. [2] F. W. Takes, W. A. Kosters,
+       "Computing the Eccentricity Distribution of Large Graphs."
+       Algorithms, 2013
+       https://www.mdpi.com/1999-4893/6/1/100
+    .. [3] M. Borassi, P. Crescenzi, M. Habib, W. A. Kosters, A. Marino, F. W. Takes,
+       "Fast diameter and radius BFS-based computation in (weakly connected) real-world graphs: With an application to the six degrees of separation games. "
+       Theoretical Computer Science, 2015
+       https://www.sciencedirect.com/science/article/pii/S0304397515001644
     """
-
     # init variables
     degrees = dict(G.degree())  # start with the highest degree node
     minlowernode = max(degrees, key=degrees.get)
@@ -91,7 +110,8 @@ def _extrema_bounding(G, compute="diameter"):
         high = not high
 
         # get distances from/to current node and derive eccentricity
-        dist = dict(nx.single_source_shortest_path_length(G, current))
+        dist = nx.shortest_path_length(G=G, source=current, weight=weight)
+
         if len(dist) != N:
             msg = "Cannot compute metric because graph is not connected."
             raise nx.NetworkXError(msg)
@@ -200,20 +220,20 @@ def _extrema_bounding(G, compute="diameter"):
     # return the correct value of the requested metric
     if compute == "diameter":
         return maxlower
-    elif compute == "radius":
+    if compute == "radius":
         return minupper
-    elif compute == "periphery":
+    if compute == "periphery":
         p = [v for v in G if ecc_lower[v] == maxlower]
         return p
-    elif compute == "center":
+    if compute == "center":
         c = [v for v in G if ecc_upper[v] == minupper]
         return c
-    elif compute == "eccentricities":
+    if compute == "eccentricities":
         return ecc_lower
     return None
 
 
-def eccentricity(G, v=None, sp=None):
+def eccentricity(G, v=None, sp=None, weight="weight"):
     """Returns the eccentricity of nodes in G.
 
     The eccentricity of a node v is the maximum distance from v to
@@ -230,6 +250,24 @@ def eccentricity(G, v=None, sp=None):
     sp : dict of dicts, optional
        All pairs shortest path lengths as a dictionary of dictionaries
 
+    weight : string or function (default="weight")
+        If this is a string, then edge weights will be accessed via the
+        edge attribute with this key (that is, the weight of the edge
+        joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
+        such edge attribute exists, the weight of the edge is assumed to
+        be one.
+
+        If this is a function, the weight of an edge is the value
+        returned by the function. The function must accept exactly three
+        positional arguments: the two endpoints of an edge and the
+        dictionary of edge attributes for that edge. The function must
+        return a number.
+
+        Weights stored as floating point values can lead to small round-off
+        errors in distances. Use integer weights to avoid this.
+
+        Weights should be positive, since they are distances.
+
     Returns
     -------
     ecc : dictionary
@@ -252,11 +290,11 @@ def eccentricity(G, v=None, sp=None):
     #    else:                      # assume v is a container of nodes
     #        nodes=v
     order = G.order()
-
     e = {}
     for n in G.nbunch_iter(v):
         if sp is None:
-            length = nx.single_source_shortest_path_length(G, n)
+            length = nx.shortest_path_length(G=G, source=n, weight=weight)
+
             L = len(length)
         else:
             try:
@@ -278,11 +316,10 @@ def eccentricity(G, v=None, sp=None):
 
     if v in G:
         return e[v]  # return single value
-    else:
-        return e
+    return e
 
 
-def diameter(G, e=None, usebounds=False):
+def diameter(G, e=None, usebounds=False, weight="weight"):
     """Returns the diameter of the graph G.
 
     The diameter is the maximum eccentricity.
@@ -295,6 +332,24 @@ def diameter(G, e=None, usebounds=False):
     e : eccentricity dictionary, optional
       A precomputed dictionary of eccentricities.
 
+    weight : string or function
+        If this is a string, then edge weights will be accessed via the
+        edge attribute with this key (that is, the weight of the edge
+        joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
+        such edge attribute exists, the weight of the edge is assumed to
+        be one.
+
+        If this is a function, the weight of an edge is the value
+        returned by the function. The function must accept exactly three
+        positional arguments: the two endpoints of an edge and the
+        dictionary of edge attributes for that edge. The function must
+        return a number.
+
+        Weights stored as floating point values can lead to small round-off
+        errors in distances. Use integer weights to avoid this.
+
+        Weights should be positive, since they are distances.
+
     Returns
     -------
     d : integer
@@ -311,13 +366,13 @@ def diameter(G, e=None, usebounds=False):
     eccentricity
     """
     if usebounds is True and e is None and not G.is_directed():
-        return _extrema_bounding(G, compute="diameter")
+        return _extrema_bounding(G=G, compute="diameter", weight=weight)
     if e is None:
-        e = eccentricity(G)
+        e = eccentricity(G, weight=weight)
     return max(e.values())
 
 
-def periphery(G, e=None, usebounds=False):
+def periphery(G, e=None, usebounds=False, weight="weight"):
     """Returns the periphery of the graph G.
 
     The periphery is the set of nodes with eccentricity equal to the diameter.
@@ -330,6 +385,24 @@ def periphery(G, e=None, usebounds=False):
     e : eccentricity dictionary, optional
       A precomputed dictionary of eccentricities.
 
+    weight : string or function
+        If this is a string, then edge weights will be accessed via the
+        edge attribute with this key (that is, the weight of the edge
+        joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
+        such edge attribute exists, the weight of the edge is assumed to
+        be one.
+
+        If this is a function, the weight of an edge is the value
+        returned by the function. The function must accept exactly three
+        positional arguments: the two endpoints of an edge and the
+        dictionary of edge attributes for that edge. The function must
+        return a number.
+
+        Weights stored as floating point values can lead to small round-off
+        errors in distances. Use integer weights to avoid this.
+
+        Weights should be positive, since they are distances.
+
     Returns
     -------
     p : list
@@ -347,15 +420,15 @@ def periphery(G, e=None, usebounds=False):
     center
     """
     if usebounds is True and e is None and not G.is_directed():
-        return _extrema_bounding(G, compute="periphery")
+        return _extrema_bounding(G=G, compute="periphery", weight=weight)
     if e is None:
-        e = eccentricity(G)
+        e = eccentricity(G, weight=weight)
     diameter = max(e.values())
     p = [v for v in e if e[v] == diameter]
     return p
 
 
-def radius(G, e=None, usebounds=False):
+def radius(G, e=None, usebounds=False, weight="weight"):
     """Returns the radius of the graph G.
 
     The radius is the minimum eccentricity.
@@ -368,6 +441,24 @@ def radius(G, e=None, usebounds=False):
     e : eccentricity dictionary, optional
       A precomputed dictionary of eccentricities.
 
+    weight : string or function
+        If this is a string, then edge weights will be accessed via the
+        edge attribute with this key (that is, the weight of the edge
+        joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
+        such edge attribute exists, the weight of the edge is assumed to
+        be one.
+
+        If this is a function, the weight of an edge is the value
+        returned by the function. The function must accept exactly three
+        positional arguments: the two endpoints of an edge and the
+        dictionary of edge attributes for that edge. The function must
+        return a number.
+
+        Weights stored as floating point values can lead to small round-off
+        errors in distances. Use integer weights to avoid this.
+
+        Weights should be positive, since they are distances.
+
     Returns
     -------
     r : integer
@@ -381,13 +472,13 @@ def radius(G, e=None, usebounds=False):
 
     """
     if usebounds is True and e is None and not G.is_directed():
-        return _extrema_bounding(G, compute="radius")
+        return _extrema_bounding(G=G, compute="radius", weight=weight)
     if e is None:
-        e = eccentricity(G)
+        e = eccentricity(G, weight=weight)
     return min(e.values())
 
 
-def center(G, e=None, usebounds=False):
+def center(G, e=None, usebounds=False, weight="weight"):
     """Returns the center of the graph G.
 
     The center is the set of nodes with eccentricity equal to radius.
@@ -400,6 +491,24 @@ def center(G, e=None, usebounds=False):
     e : eccentricity dictionary, optional
       A precomputed dictionary of eccentricities.
 
+    weight : string or function
+        If this is a string, then edge weights will be accessed via the
+        edge attribute with this key (that is, the weight of the edge
+        joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
+        such edge attribute exists, the weight of the edge is assumed to
+        be one.
+
+        If this is a function, the weight of an edge is the value
+        returned by the function. The function must accept exactly three
+        positional arguments: the two endpoints of an edge and the
+        dictionary of edge attributes for that edge. The function must
+        return a number.
+
+        Weights stored as floating point values can lead to small round-off
+        errors in distances. Use integer weights to avoid this.
+
+        Weights should be positive, since they are distances.
+
     Returns
     -------
     c : list
@@ -417,9 +526,9 @@ def center(G, e=None, usebounds=False):
     periphery
     """
     if usebounds is True and e is None and not G.is_directed():
-        return _extrema_bounding(G, compute="center")
+        return _extrema_bounding(G=G, compute="center", weight=weight)
     if e is None:
-        e = eccentricity(G)
+        e = eccentricity(G, weight=weight)
     radius = min(e.values())
     p = [v for v in e if e[v] == radius]
     return p
@@ -560,14 +669,23 @@ def resistance_distance(G, nodeA, nodeB, weight=None, invert_weight=True):
 
     Notes
     -----
-    Overview discussion:
-    * https://en.wikipedia.org/wiki/Resistance_distance
-    * http://mathworld.wolfram.com/ResistanceDistance.html
-
-    Additional details:
-    Vaya Sapobi Samui Vos, “Methods for determining the effective resistance,” M.S.,
-    Mathematisch Instituut, Universiteit Leiden, Leiden, Netherlands, 2016
-    Available: `Link to thesis <https://www.universiteitleiden.nl/binaries/content/assets/science/mi/scripties/master/vos_vaya_master.pdf>`_
+    Overviews are provided in [1]_ and [2]_. Additional details on computational
+    methods, proofs of properties, and corresponding MATLAB codes are provided
+    in [3]_.
+
+    References
+    ----------
+    .. [1] Wikipedia
+       "Resistance distance."
+       https://en.wikipedia.org/wiki/Resistance_distance
+    .. [2] E. W. Weisstein
+       "Resistance Distance."
+       MathWorld--A Wolfram Web Resource
+       https://mathworld.wolfram.com/ResistanceDistance.html
+    .. [3] V. S. S. Vos,
+       "Methods for determining the effective resistance."
+       Mestrado, Mathematisch Instituut Universiteit Leiden, 2016
+       https://www.universiteitleiden.nl/binaries/content/assets/science/mi/scripties/master/vos_vaya_master.pdf
     """
     import numpy as np
     import scipy as sp
diff --git a/networkx/algorithms/tests/test_distance_measures.py b/networkx/algorithms/tests/test_distance_measures.py
index 72f23fc50e3..33c1f013f82 100644
--- a/networkx/algorithms/tests/test_distance_measures.py
+++ b/networkx/algorithms/tests/test_distance_measures.py
@@ -97,6 +97,62 @@ def test_eccentricity_directed_weakly_connected(self):
             nx.eccentricity(DG)
 
 
+class TestWeightedDistance:
+    def setup_method(self):
+        G = nx.Graph()
+        G.add_edge(0, 1, weight=0.6)
+        G.add_edge(0, 2, weight=0.2)
+        G.add_edge(2, 3, weight=0.1)
+        G.add_edge(2, 4, weight=0.7)
+        G.add_edge(2, 5, weight=0.9)
+        G.add_edge(1, 5, weight=0.3)
+        self.G = G
+
+    def test_eccentricity(self):
+        assert nx.eccentricity(self.G, 1) == 1.5
+        e = nx.eccentricity(self.G)
+        assert e[1] == 1.5
+
+        e = nx.eccentricity(self.G, v=1)
+        assert e == 1.5
+
+        # This behavior changed in version 1.8 (ticket #739)
+        e = nx.eccentricity(self.G, v=[1, 1])
+        assert e[1] == 1.5
+        e = nx.eccentricity(self.G, v=[1, 2])
+        assert e[1] == 1.5
+
+    def test_diameter(self):
+        assert nx.diameter(self.G) == 1.6
+
+    def test_radius(self):
+        assert nx.radius(self.G) != 0.9
+        assert pytest.approx(nx.radius(self.G)) == 0.9
+
+    def test_periphery(self):
+        for v in set(nx.periphery(self.G)):
+            assert nx.eccentricity(self.G, v) == nx.diameter(self.G)
+
+    def test_center(self):
+        for v in set(nx.center(self.G)):
+            assert nx.eccentricity(self.G, v) == nx.radius(self.G)
+
+    def test_bound_diameter(self):
+        assert nx.diameter(self.G, usebounds=True) == 1.6
+
+    def test_bound_radius(self):
+        assert nx.radius(self.G, usebounds=True) != 0.9
+        assert pytest.approx(nx.radius(self.G, usebounds=True)) == 0.9
+
+    def test_bound_periphery(self):
+        result = {4, 5}
+        assert set(nx.periphery(self.G, usebounds=True)) == result
+
+    def test_bound_center(self):
+        result = {0}
+        assert set(nx.center(self.G, usebounds=True)) == result
+
+
 class TestResistanceDistance:
     @classmethod
     def setup_class(cls):


Code review comments:
diff_hunk: @@ -14,7 +14,7 @@
 ]
 
 
-def _extrema_bounding(G, compute="diameter"):
+def _extrema_bounding(G, compute="diameter", weight="weight"):
path: networkx/algorithms/distance_measures.py
text: @user1:
API question: do we want the default to be "weight"? This seems like it might silently change some user code as "weight" is a very common edge attribute name, so any existing code that calls e.g. `nx.eccentricity(G)` may get a different result than previous versions of networkx if the edges of `G` have a "weight" attribute!

I think the correct thing to do would be to match the default value for `weight=` in `shortest_path_lengths`, which is `weight=None`.

@author:
> I think the correct thing to do would be to match the default value for weight= in shortest_path_lengths, which is weight=None.

I agree, and this is how I would do it normally, but here I'm following the pattern of [shortest_paths/weighted](https://github.com/networkx/networkx/blob/main/networkx/algorithms/shortest_paths/weighted.py), which has default `weight="weight"`.

This seems like an API inconsistency - if you and @user2 agree that `weight=None` is preferable, I will update here and maybe we should open a separate issue for refactoring in this direction elsewhere (e.g. in [shortest_paths/weighted](https://github.com/networkx/networkx/blob/main/networkx/algorithms/shortest_paths/weighted.py)). Thoughts?

@user2:
Yes -- it should default to `weight=None`.  

The `weighted.py` module does use `weight="weight"`, but we shouldn't use the `weighted.py` functions unless a `weight` argument is provided. That's what `shortest_path_lengths` does for us (which is in the `generic.py` module). It chooses whether to call the weighted functions, or stick with the unweighted functions with the default choice being unweighted (unless a weight value is provided).

@author:
> I think the correct thing to do would be to match the default value for weight= in shortest_path_lengths, which is weight=None.

@user1 The default value for weight in `shortest_path_lengths` is None, but this sets all weights to 1, which is not what we're looking for. 

> That's what shortest_path_lengths does for us (which is in the generic.py module). It chooses whether to call the weighted functions, or stick with the unweighted functions with the default choice being unweighted (unless a weight value is provided).

@user2 I'm not sure I agree. We are trying to distinguish three cases:
1. Weights are via a provided weight function
2. Weights are via edge attributes ( weight="weight")
3. Weights are all 1 ("unweighted" case)

When weight="weight", we're not defaulting to the unweighted case, we're using the edge attributes as weights. If we pass weight=None, we are telling `shortest_path_length` to set weights to 1, not use the edge attributes.

Constructively, three paths forward come to mind:
1. Leave  weight="weight" as the default in all `distance_measures`. Consider refactoring `distance_measures` and `generic` at a later time.
2. In `_extrema_bounding`, do something like:
```
if not weight:
      dist = nx.shortest_path_length(G=G, source=current, weight="weight")
else:
      dist = nx.shortest_path_length(G=G, source=current, weight=weight)
```
3. refactor `generic` as part of this PR.

My view is refactoring `generic` is out of scope, and option 2 seems clunky, but I am happy to do it if you think that best. Do you see a preferable alternative?


@user2:
We want the unweighted values when the user does not provide the `weight` argument to the function.
I think all the code is just fine except for the default value of weight, which should be None just as it is for `shortest_path_length`.  Then for each use case the right thing happens:

1) Weight=function: The user specifies a weight function, so `weight` is not None and `shortest_path_length` uses the weight provided weight function -- all is good.
2) Weights=edge attribute name.  For example, weight="weight" or weight="distance" or weight="cost" or any other attribute name.  Again, weight is not None, so `shortest_path_length` uses the edge attribute specified to find the weights.
3) Unweighted case:  This is the old/existing case. The user doesn't specify any `weight` and so `weight=None` is passed into `shortest_path_length` which calls the unweighted path length function.



@user1:
@author consider the following.

Let's say a user has the following code:

```python
>>> import networkx as nx
>>> G = nx.Graph([(0, 1, {"weight": 10})])
>>> nx.diameter(G)
```

In all previous versions of networkx, this would give `1`. However, if we were to make `weight="weight"` the default, then this would give `10` instead; i.e. we'd be silently changing the results of existing user code without warning. This is something that we must avoid, otherwise we'd potentially be introducing bugs (and hard-to-track-down ones, at that) all over existing user code!

I certainly understand the logic of wanting a weighted result by default if the graph is weighted. Even without backwards-compatibility constraints, choosing an explicit default for an edge attribute doesn't mesh well with the generality of the library. Consider another example: let's say someone has a graph where the nodes represent towns and the edges represent roads between them (as I gather, this is a pretty common example in geospatial applications). The edges of this graph might have multiple attributes, say one called `"distance"` that represents the physical distance between nodes along a given edge, and another called `"weight"` which represents the number of travelers / hour that travel along that edge. In this case, if a user is calling one of the `distance_measures` functions on the graph, chances are they actually want the result to be weighted by the `"distance"` edge attr, but we'd be giving them a different (and harder to understand for this example) value *by default*. In this case too, I'd argue it's much better to have an unweighted default and force the user to be intentional about which weight they want.

Anyways, just some high-level pontification. The real blocker for me with going with `weight="weight"` as the default is the backward-incompatibility concern!

@author:
@user2  @user1  Thank you both for being patient and explaining your reasoning here; I now understand what you mean.

I thought you wanted to make `weight=None` do what `weight="weight"` does, and drop `weight="weight"` as an option altogether. My mistake!

diff_hunk: @@ -97,6 +97,62 @@ def test_eccentricity_directed_weakly_connected(self):
             nx.eccentricity(DG)
 
 
+class TestWeightedDistance:
+    def setup_method(self):
+        G = nx.Graph()
+        G.add_edge(0, 1, weight=0.6)
+        G.add_edge(0, 2, weight=0.2)
+        G.add_edge(2, 3, weight=0.1)
+        G.add_edge(2, 4, weight=0.7)
+        G.add_edge(2, 5, weight=0.9)
+        G.add_edge(1, 5, weight=0.3)
+        self.G = G
+
+    def test_eccentricity(self):
+        assert nx.eccentricity(self.G, 1) == 1.5
+        e = nx.eccentricity(self.G)
+        assert e[1] == 1.5
+
+        e = nx.eccentricity(self.G, v=1)
+        assert e == 1.5
+
+        # This behavior changed in version 1.8 (ticket #739)
+        e = nx.eccentricity(self.G, v=[1, 1])
+        assert e[1] == 1.5
+        e = nx.eccentricity(self.G, v=[1, 2])
+        assert e[1] == 1.5
+
+    def test_diameter(self):
+        assert nx.diameter(self.G) == 1.6
+
+    def test_radius(self):
+        assert nx.radius(self.G) != 0.9
path: networkx/algorithms/tests/test_distance_measures.py
text: @user1:
I don't think this assertion is a good idea... the pytest.approx call on the next line actually tests the behavior we care about.
```suggestion
```

@user2:
OK... that's my mistake.  I was thinking we should check that we understand what is happening by testing that round-off occurs. But I agree that this is not the answer we care about. 

Should we make all the comparisons use the `pytest.approx` idiom? or only the ones where round-off messes up the comparison?

@user1:
> Should we make all the comparisons use the pytest.approx idiom? or only the ones where round-off messes up the comparison?

It's probably safest to use `pytest.approx` everywhere for testing "equality" with floating point values, but it's also not super critical. I doubt it will cause failures except *maybe* on systems that use 32-bit floats. 

@user2:
Would it be useful to know (and test for) cases where round-off makes a difference. In these examples, 2 have round-off and the rest don't. Do we care if that changes? probably not. :}

diff_hunk: @@ -97,6 +97,62 @@ def test_eccentricity_directed_weakly_connected(self):
             nx.eccentricity(DG)
 
 
+class TestWeightedDistance:
+    def setup_method(self):
+        G = nx.Graph()
+        G.add_edge(0, 1, weight=0.6)
+        G.add_edge(0, 2, weight=0.2)
+        G.add_edge(2, 3, weight=0.1)
+        G.add_edge(2, 4, weight=0.7)
+        G.add_edge(2, 5, weight=0.9)
+        G.add_edge(1, 5, weight=0.3)
+        self.G = G
+
+    def test_eccentricity(self):
+        assert nx.eccentricity(self.G, 1) == 1.5
+        e = nx.eccentricity(self.G)
+        assert e[1] == 1.5
+
+        e = nx.eccentricity(self.G, v=1)
+        assert e == 1.5
+
+        # This behavior changed in version 1.8 (ticket #739)
+        e = nx.eccentricity(self.G, v=[1, 1])
+        assert e[1] == 1.5
+        e = nx.eccentricity(self.G, v=[1, 2])
+        assert e[1] == 1.5
path: networkx/algorithms/tests/test_distance_measures.py
text: @user1:
Hm, this behavior seems inconsistent with the docstring, which indicates that `v` is expected to be a node. I'm not sure we want to enshrine this in the test suite, at least not without first deciding that this is indeed the desired behavior.

@user2:
Interesting... these tests seem to be testing the distinction between returning a value and returning a dict. When the input is an iterable/iterator of nodes, the output should be a dict -- even when there is only one value computed. When the input is a node, then the output should be a value.

The blame on the code (and the commented out code at the top of the function) indicate that this treatment of the input `v` was indeed intended to handle input `v` that are lists. This behavior goes all the way back to the hg(pre-git) repository. So, we built in handling of list inputs without advertising it. Of course, the original docs didn't say anything about the input or output parameters. That was added later (still in the hg days) and didn't discuss inputs other than a node or None, even though the code handled it.

So, I think it's fair to say that the behavior has been enshrined -- which does not mean it is the right choice for API. What would be the right choice?

@author:
Re: the question of whether to support `v` being a list - I think the answer should be "yes," but I will defer to @user2  and @user1. I could just add to the docstring to indicate that `v` can be a list, as well.

That being said, I do feel like this API decision may be out of scope for this ticket. As it stands, `v` can be a list, so I test this, just as it is tested in the unweighted case.