Repository: networkx/networkx

Issue:
k-components approximation (migrated from Trac #589)
Original ticket https://networkx.lanl.gov/trac/ticket/589
Reported 2011-07-07 by @jtorrents, assigned to @jtorrents.

New version of the algorithm. To run it you'll need the code for vertex connectivity approximation #538, the antigraph data structure #608. For accuracy tests you will also need the flow based connectivity code #625 

A k-component is a maximal subgraph that cannot be disconnected removing less
than k nodes (along with their incident edges). By Merger's theorem, this is 
equivalent to a maximal subgraph in which all pairs of nodes are connected 
by at least k node-independent paths which each run entirely within the 
subgraph. Note that a k-component must be a subset of a (k-1)-component or
be a (k-1)-component itself. Thus, they have an inherent hiearchical structure.
Components of level k can overlap in k-1 nodes.

Following White and Harary (2001) and Moody and White (2003), the cohesive 
structure of a network can be conceptualized as increasingly cohesive groups 
nested inside each other. Those groups can be operacionalized as the 
k-components of the network. A common structural pattern in large networks is 
an hierarchical nesting of increasingly cohesive groups at low connectivity 
levels and non-overlapping highly cohesive groups at higher connectivity levels. 

Those highly cohesive groups play a key role in the diffusion of the 
consequences of social interactions among actors in networks. It is usually 
assumed that the transmission through the network of knowledge, influence 
and resources generated by social interactions is limited to people 2 or 3 
steps away from the initiator of such interactions. However, strongly cohesive 
subsets of nodes allow repetition of information and reinforcement of influence 
because they are characterized by multiple independent pathways that 
compensate the decay effects of the transmission of knowledge, 
influence and resources.

An exact solution for the computation of the complete k-component structure 
(Moody & White, 2003) is impractical for large networks. This approximation 
is based on the proposal of White and Newman (2001) of a fast approximation
for finding node independent paths (implemented in #538). It allows us to 
compute the approximate k-component structure for moderately large networks, 
in a reasonable time frame.

The algorithm consists in 6 steps:
1. k-core computation (time O(E)): Compute the core number for each node. A k-core is a maximal subgraph that contains nodes of degree k or more. The core number of a node is the largest value k of a k-core containing that node. This is a baseline for k-components because \kappa(G) <= \lambda(G) <= \delta(G)
2. bicomponents computation (time O(V+E)): Compute the biconnected components of the graph. (k>2)-components are a subset of (or are themselves also) a bicomponent. Besides, the exact computation is faster than the approximation because usually bicomponents are quite large. Thus we start computing the approximation for k=3. (same for tricomponents but it is not implemented in NetworkX yet)
3. for each k from 3 to the maximum core number of a node of the graph we repeat the following steps:
4. For each bicomponent we create a subgraph with all nodes with core level = k. Compute the connected component of the k-core sugraph (note that the k-core could be disconnected) and then compute the approximation for vertex connectivity (proposed by White and Newman (2001)) between all pairs of nodes. Implemented in ticket #538. 
5. Build an auxiliary graph with all nodes in the subgraph analyzed and edges between two nodes if the local vertex connectivity (ie number of node independent paths between them) is greater or equal than the level k of the main for loop. Note: we actually use a complement graph data structure to save memory because usually a big part of the k-core is actually a k-component.
6. Each connected component of the auxiliary graph is a candidate to be a k-component. But we need to do a final check: we create the induced subgraph of candidate nodes from the input graph and discard any node that, in this induced subgraph, has core number less than k. The remaining nodes are assumed to be a k-component.

References

```
White, Douglas R., and Mark Newman. 2001 
A Fast Algorithm for Node-Independent Paths. 
Santa Fe Institute Working Paper 01-07-035  
http://eclectic.ss.uci.edu/~drwhite/working.pdf

White, D. R., and F. Harary. (2001) 
The cohesiveness of blocks in social networks: Node
connectivity and conditional density,
Sociological Methodology 31:305--59.
http://eclectic.ss.uci.edu/~drwhite/sm-w23.PDF

Moody, James and Douglas R. White. 2003. 
Social Cohesion and Embeddedness. American Sociological Review. 68:103-127
http://www2.asanet.org/journals/ASRFeb03MoodyWhite.pdf
```



Discussion:
Attachment in Trac by @jtorrents, 2011-07-07: [test_1m.adj](https://networkx.lanl.gov/trac/attachment/ticket/589/test_1m.adj)

Attachment in Trac by @jtorrents, 2011-07-07: [test_2m.adj](https://networkx.lanl.gov/trac/attachment/ticket/589/test_2m.adj)

Attachment in Trac by @jtorrents, 2011-07-07: [k_components.py](https://networkx.lanl.gov/trac/attachment/ticket/589/k_components.py)

Comment in Trac by @jtorrents, 2011-07-07

To run the script you need the code for node_independent_paths (#538) and to check the results against the exact computation for bicomponents you also need igraph (maybe we should also implement a biconnected_components function in NetworkX). The output in my machine:

'''
Output of accuracy test removed from comments
'''

Comment in Trac by @jtorrents, 2011-07-07

The main problem with this implementation is the high consumption of RAM for large networks. I don't know how to properly debug RAM usage (other than add prints to the algorithm to know what it is doing and checking the '''top''' command). For instance, this algorithm applied to a gnm_random_graph(7000, 8000) consumes almost 3 Gb of RAM. This must be improved.

Comment in Trac by @jtorrents, 2011-07-17

Following Aric's smart suggestion to avoid the creation of a very dense graph in step 3 (which was what was consuming immensely huge amounts of memory for large graphs) by building instead its complement; and after a fruitful discussion at Scipy sprints, Dan implemented the "anticore number" algorithm (ie to compute the core number of the dense graph working only with its complement).

With this improvements, the new implementation of the algorithm (see attached files) uses a lot less memory, which will hopefully allow to apply it to large networks (I'm hoping to be able to apply it to sparse networks of the order of hundreds of thousands of nodes and edges). The anticore algorithm needs more work to work properly in bipartite networks though (I'll work on this). The new implementation of the approximation is a bit less accurate in bipartite networks because the anticore algo does not take into account the restrictions in terms of complement degree and complement neighbors that bipartite networks have. Despite that, the approximation works quite well for 2-components (see tests of accuracy in k_components_1.py).

I also include the old version that uses the dense graph in the new file uploaded to be able to compare accuracy and memory consumption. I post the results of accuracy and memory tests as attached txt files in this ticked to avoid very long comments.

Salut!

PS: I'm still at Austin ;) It was really great to be able to attend Scipy conference and to work with you all!!

Attachment in Trac by @jtorrents, 2011-07-17: [anticore.py](https://networkx.lanl.gov/trac/attachment/ticket/589/anticore.py)

Attachment in Trac by @jtorrents, 2011-07-17: [k_components_1.py](https://networkx.lanl.gov/trac/attachment/ticket/589/k_components_1.py)

Attachment in Trac by @jtorrents, 2011-07-17: [output_accuracy_tests.txt](https://networkx.lanl.gov/trac/attachment/ticket/589/output_accuracy_tests.txt)

Attachment in Trac by @jtorrents, 2011-07-17: [memtest_k_components.py](https://networkx.lanl.gov/trac/attachment/ticket/589/memtest_k_components.py)

Attachment in Trac by @jtorrents, 2011-07-17: [output_memtest_k_components.txt](https://networkx.lanl.gov/trac/attachment/ticket/589/output_memtest_k_components.txt)

Comment in Trac by @jtorrents, 2011-07-17

The memory tests are quite hackish, but so far is the easiest way that I've been able to found. It works using sys.getsizeof with the string representation of G.adj (ie str(G.adj)) as a principal metric for graph memory usage. It is not accurate but allows us to compare memory footprint between different graphs (see source code for details). The complement graph is 2 orders of magnitude smaller than the dense graph in most of the examples, so the memory footprint is quite smaller.

Comment in Trac by @dschult, 2011-07-20

It looks like now your memory bottleneck is creating K (the dict-of-dict of pairwise vertex connectivity).

If so, I think you can rearrange the code so you don't have to store those values.  Instead, compute each K value and use it immediately.  Something like:

'''
H=nx.Graph()
SG=G.subgraph(candidates)
for u,v in itertools.combinations(SG,2):
    K_value=pairwise_vertex_connectivity(SG,u,v)
    if k_value < k:   # complement graph
        H.add_edge(u,v)
'''

Comment in Trac by @hagberg, 2011-07-21

Also to get this into the code base when it's ready the bicomponents function needs to be implemented.

Comment in Trac by @jtorrents, 2011-07-21

Replying to [comment:5 dschult]:

> It looks like now your memory bottleneck is creating K (the dict-of-dict of pairwise vertex connectivity).
> 
> If so, I think you can rearrange the code so you don't have to store those values.  Instead, compute each K value and use it immediately.  Something like:
> 
> '''
> H=nx.Graph()
> SG=G.subgraph(candidates)
> for u,v in itertools.combinations(SG,2):
>     K_value=pairwise_vertex_connectivity(SG,u,v)
>     if k_value < k:   # complement graph
>         H.add_edge(u,v)
> '''

That's a great idea! Thank you very much. I've implemented this approach in '''k_components_2.py''' and the memory footprint is greatly reduced compared with the implementation that also uses the complement graph.

I have to work more in the creation of the complement graph in step 3 because the approximation using the dense graph is more accurate (see attached file '''output_accuracy_tests.txt''') and I don't understand why yet. First I thought that it was because the anticore algorithm does not take into account the bipartite nature of some networks, but after some thinking and some tests, it seems that the problem is in the creation of the complement graph and not in the anticore algorithm. I'll work on this and will post here my progress.

Attachment in Trac by @jtorrents, 2011-07-21: [k_components_2.py](https://networkx.lanl.gov/trac/attachment/ticket/589/k_components_2.py)

Comment in Trac by @jtorrents, 2011-07-21

Replying to [comment:6 aric]:

> Also to get this into the code base when it's ready the bicomponents function needs to be implemented.

I'll work on the implementation of '''biconnected_components''' function (opening a new ticket). So far I've found two interesting python implementations:
1. Jesus Cerquides' translation of Igraph C implementation at https://bitbucket.org/cerquide/networkx-chordal-approx/src/2423f58039f5/networkx/algorithms/components/biconnected.py (he is the guy that implemented chordal algorithms in NetworkX)
2. David Eppstein's implementation at http://www.ics.uci.edu/~eppstein/PADS/Biconnectivity.py 

I have to look at them more closely. Eppstein's implementation seems more elegant and concise but I do not fully understand it yet. When I have some code that works I'll open a new ticket for biconnected components.

Comment in Trac by @dschult, 2011-07-21

While thinking about why the approximation would be off for the complement graph it occurs to me that the last step (finding the components) might be finding the components of the complement graph instead of the actual graph.  If that's true, then we would have to fiddle with that algorithm too.   I'm still thinking about a complement graph data structure so that we wouldn't have to rewrite algorithms but its tricky.

Comment in Trac by @jtorrents, 2011-07-22

Replying to [comment:9 dschult]:

> While thinking about why the approximation would be off for the complement graph it occurs to me that the last step (finding the components) might be finding the components of the complement graph instead of the actual graph.  If that's true, then we would have to fiddle with that algorithm too.   I'm still thinking about a complement graph data structure so that we wouldn't have to rewrite algorithms but its tricky.

You are right, the problem is in the last step (finding the connected components of k-core subgraph of the auxiliary graph H) because we need the connected components of its complement graph. As you suggest, in order to avoid having to rewrite algorithms to work in the complement graph, I've implemented a first version of complement graph data structure in '''antigraph.py'''. I'm not sure that I did it correctly but it seems to work (now the approximation algorithm yield the same results both in the dense and complement versions). The updated approximation algorithm is in '''k_components_3.py'''. The new data structure inherits (is this the correct term?) form nx.Graph class and some methods ('''**getitem**, neighbors, neighbors_iter, degree, degree_iter''') are overwritten so it works as expected with the algorithms of interest ('''core_number, connected_components'''). I'm not sure if it would be necessary to overwrite other methods in order to make it work for other algorithms. For now the docstrings of the overwritten methods are not updated.

Attachment in Trac by @jtorrents, 2011-07-22: [k_components_3.py](https://networkx.lanl.gov/trac/attachment/ticket/589/k_components_3.py)

Attachment in Trac by @jtorrents, 2011-07-22: [accuracy_tests_antigraph.txt](https://networkx.lanl.gov/trac/attachment/ticket/589/accuracy_tests_antigraph.txt)

Attachment in Trac by @jtorrents, 2011-07-23: [antigraph.py](https://networkx.lanl.gov/trac/attachment/ticket/589/antigraph.py)

Attachment in Trac by @jtorrents, 2011-07-23: [test_antigraph.py](https://networkx.lanl.gov/trac/attachment/ticket/589/test_antigraph.py)

Comment in Trac by @jtorrents, 2011-07-23

I've updated the AntiGraph class and implemented some tests. Using it the complement version of the algorithm obtains the same accuracy than in the dense implementation. Now the memory footprint is quite smaller. I'm not sure if I should open a new ticket for the AntiGraph class; Dan, do you think that is interesting and general enough to be a ticket on its own?

Comment in Trac by @dschult, 2011-07-25

Yes, lets split off the complement graph(antigraph) as a separate ticket.

I think this version of it does very well and is the correct approach (add nodes and edges like a normal graph but report differently).  This version is also sufficient for this problem.  I'm thinking that other methods could be needed for other algorithms though so we should implement them too.  And the docs should be rewritten--maybe with lots of references to to the standard graph class methods.

So, is this footprint small enough to get you to try the big dataset you were thinking of?

Comment in Trac by @jtorrents, 2011-07-26

Replying to [comment:12 dschult]:

> Yes, lets split off the complement graph(antigraph) as a separate ticket.
> 
> I think this version of it does very well and is the correct approach (add nodes and edges like a normal graph but report differently).  This version is also sufficient for this problem.  I'm thinking that other methods could be needed for other algorithms though so we should implement them too.  And the docs should be rewritten--maybe with lots of references to to the standard graph class methods.

I agree, this version is sufficient for this problem but we might have to think about other use cases in order to make it a solid data structure that could be included in NetworkX codebase. I have some ideas of new methods to implement; I'll comment on the new ticket.

> So, is this footprint small enough to get you to try the big dataset you were thinking of?

I did some tests and it seems that the memory footprint is quite small. I did not test it yet with the big datasets because I've found a problem with the approximation algorithm. I'm preparing a paper about this approximation algorithm and, as an example, I plan to include an analysis of Zachary's karate club. Analyzing this network I've found out that the approximation algorithm (as implemented in '''k_components_3.py''') does not detect the 4-component. This is because this implementation does not consider node independent paths between adjacent nodes in the original graph (eg '''parwise_vertex,connectivity(..., strict=True)'''). But in small dense networks it is necessary to do so in order to obtain accurate results. The problem is that if we actually consider node independent paths between adjacent nodes then the results obtained for large sparse networks are quite less accurate. 

As a first approach we could check the density of the k-core analyzed and then decide what to do for adjacent nodes (implemented in '''k_components_4.py'''). But what worries me is that I do not understand why this happens. I've put together more accuracy tests to show the problem in k_components_4.py and I attached the output as a .txt . The tests involving Karate network are at the end.

Attachment in Trac by @jtorrents, 2011-07-26: [k_components_4.py](https://networkx.lanl.gov/trac/attachment/ticket/589/k_components_4.py)

Attachment in Trac by @jtorrents, 2011-07-26: [accuracy_tests_strict.txt](https://networkx.lanl.gov/trac/attachment/ticket/589/accuracy_tests_strict.txt)

Comment in Trac by @jtorrents, 2011-08-24

Attachment in Trac by @jtorrents, 2011-08-24: [k_components_5.py](https://networkx.lanl.gov/trac/attachment/ticket/589/k_components_5.py)

Attachment in Trac by @jtorrents, 2011-08-24: [accuracy_tests_k_components_5.txt](https://networkx.lanl.gov/trac/attachment/ticket/589/accuracy_tests_k_components_5.txt)

Comment in Trac by @jtorrents, 2011-08-24

I've added a new version of the algorithm, a new description, and a new set of accuracy tests that, using the flow based connectivity code #625, checks the actual vertex connectivity for each k-component detected by the algorithm. As you can see, the accuracy tests are quite good, but there are still problems with dense networks with a lot of levels of connectivity. Analyzing the network in the file test_1m.adj (attached to this ticket) we get it right for k < 8 and for k > 12; in the middle the accuracy is quite poor and I think that this is because of the changes that I did in the algorithm:

Now the last step consists in creating the induced subgraph of all nodes that are candidates to be part of a k-component, and remove all nodes with core number < k from this subgraph before considering it a k-component. The problem is that, after removing those nodes, other nodes of the subgraph could have core number < k because we deleted edges when removing nodes with core number < k. Thus, I'll have to improve these last step.

Regarding memory footprint issues, most of the memory was used to approximate k=2 for large networks. Now we only compute the approximation for k >= 3. We compute the exact solution for bicomponents (which is fast, see #609) and take it as a base for the approximation to higher levels of connectivity. This way we get always right the biconnected structure (along with articualtion points) that were difficult to approximate right with the algorithm and also save a lot of memory in large networks with big bicomponents. The same could be done for tricomponents (and should be done) but they are not implemented in NetworkX (yet).


Resolving patch:
diff --git a/examples/subclass/antigraph.py b/examples/subclass/antigraph.py
new file mode 100644
--- /dev/null
+++ b/examples/subclass/antigraph.py
@@ -0,0 +1,207 @@
+""" Complement graph class for small footprint when working on dense graphs.
+
+This class allows you to add the edges that *do not exist* in the dense 
+graph. However, when applying algorithms to this complement graph data 
+structure, it behaves as if it were the dense version. So it can be used
+directly in several NetworkX algorithms.
+
+This subclass has only been tested for k-core, connected_components,
+and biconnected_components algorithms but might also work for other
+algorithms.
+
+"""
+#    Copyright (C) 2015 by 
+#    Jordi Torrents <jtorrents@milnou.net>
+#    All rights reserved.
+#    BSD license.
+import networkx as nx
+from networkx.exception import NetworkXError
+
+
+__author__ = """\n""".join(['Jordi Torrents <jtorrents@milnou.net>'])
+
+__all__ = ['AntiGraph']
+
+
+class AntiGraph(nx.Graph):
+    """
+    Class for complement graphs.
+
+    The main goal is to be able to work with big and dense graphs with
+    a low memory foodprint.
+
+    In this class you add the edges that *do not exist* in the dense graph,
+    the report methods of the class return the neighbors, the edges and 
+    the degree as if it was the dense graph. Thus it's possible to use
+    an instance of this class with some of NetworkX functions. 
+    """
+
+    all_edge_dict = {'weight': 1}
+    def single_edge_dict(self):
+        return self.all_edge_dict
+    edge_attr_dict_factory = single_edge_dict
+
+    def __getitem__(self, n):
+        """Return a dict of neighbors of node n in the dense graph.
+
+        Parameters
+        ----------
+        n : node
+           A node in the graph.
+
+        Returns
+        -------
+        adj_dict : dictionary
+           The adjacency dictionary for nodes connected to n.
+
+        """
+        return dict((node, self.all_edge_dict) for node in
+                    set(self.adj) - set(self.adj[n]) - set([n]))
+
+
+    def neighbors(self, n):
+        """Return a list of the nodes connected to the node n in 
+           the dense graph.
+
+        Parameters
+        ----------
+        n : node
+           A node in the graph
+
+        Returns
+        -------
+        nlist : list
+            A list of nodes that are adjacent to n.
+
+        Raises
+        ------
+        NetworkXError
+            If the node n is not in the graph.
+
+        """
+        try:
+            return list(set(self.adj) - set(self.adj[n]) - set([n]))
+        except KeyError:
+            raise NetworkXError("The node %s is not in the graph."%(n,))
+
+    def neighbors_iter(self, n):
+        """Return an iterator over all neighbors of node n in the 
+           dense graph.
+
+        """
+        try:
+            return iter(set(self.adj) - set(self.adj[n]) - set([n]))
+        except KeyError:
+            raise NetworkXError("The node %s is not in the graph."%(n,))
+
+    def degree(self, nbunch=None, weight=None):
+        """Return the degree of a node or nodes in the dense graph.
+        """
+        if nbunch in self:      # return a single node
+            return next(self.degree_iter(nbunch,weight))[1]
+        else:           # return a dict
+            return dict(self.degree_iter(nbunch,weight))
+
+    def degree_iter(self, nbunch=None, weight=None):
+        """Return an iterator for (node, degree) in the dense graph.
+
+        The node degree is the number of edges adjacent to the node.
+
+        Parameters
+        ----------
+        nbunch : iterable container, optional (default=all nodes)
+            A container of nodes.  The container will be iterated
+            through once.
+
+        weight : string or None, optional (default=None)
+           The edge attribute that holds the numerical value used 
+           as a weight.  If None, then each edge has weight 1.
+           The degree is the sum of the edge weights adjacent to the node.
+
+        Returns
+        -------
+        nd_iter : an iterator
+            The iterator returns two-tuples of (node, degree).
+
+        See Also
+        --------
+        degree
+
+        Examples
+        --------
+        >>> G = nx.Graph()   # or DiGraph, MultiGraph, MultiDiGraph, etc
+        >>> G.add_path([0,1,2,3])
+        >>> list(G.degree_iter(0)) # node 0 with degree 1
+        [(0, 1)]
+        >>> list(G.degree_iter([0,1]))
+        [(0, 1), (1, 2)]
+
+        """
+        if nbunch is None:
+            nodes_nbrs = ((n, {v: self.all_edge_dict for v in
+                            set(self.adj) - set(self.adj[n]) - set([n])})
+                            for n in self.nodes_iter())
+        else:
+            nodes_nbrs= ((n, {v: self.all_edge_dict for v in
+                            set(self.nodes()) - set(self.adj[n]) - set([n])})
+                            for n in self.nbunch_iter(nbunch))
+
+        if weight is None:
+            for n,nbrs in nodes_nbrs:
+                yield (n,len(nbrs)+(n in nbrs)) # return tuple (n,degree)
+        else:
+            # AntiGraph is a ThinGraph so all edges have weight 1
+            for n,nbrs in nodes_nbrs:
+                yield (n, sum((nbrs[nbr].get(weight, 1) for nbr in nbrs)) +
+                              (n in nbrs and nbrs[n].get(weight, 1)))
+
+    def adjacency_iter(self):
+        """Return an iterator of (node, adjacency set) tuples for all nodes
+           in the dense graph.
+
+        This is the fastest way to look at every edge.
+        For directed graphs, only outgoing adjacencies are included.
+
+        Returns
+        -------
+        adj_iter : iterator
+           An iterator of (node, adjacency set) for all nodes in
+           the graph.
+
+        """
+        for n in self.adj:
+            yield (n, set(self.adj) - set(self.adj[n]) - set([n]))
+
+
+if __name__ == '__main__':
+    # Build several pairs of graphs, a regular graph
+    # and the AntiGraph of it's complement, which behaves
+    # as if it were the original graph.
+    Gnp = nx.gnp_random_graph(20,0.8)
+    Anp = AntiGraph(nx.complement(Gnp))
+    Gd = nx.davis_southern_women_graph()
+    Ad = AntiGraph(nx.complement(Gd))
+    Gk = nx.karate_club_graph()
+    Ak = AntiGraph(nx.complement(Gk))
+    pairs = [(Gnp, Anp), (Gd, Ad), (Gk, Ak)]
+    # test connected components
+    for G, A in pairs:
+        gc = [set(c) for c in nx.connected_components(G)]
+        ac = [set(c) for c in nx.connected_components(A)]
+        for comp in ac:
+            assert comp in gc
+    # test biconnected components
+    for G, A in pairs:
+        gc = [set(c) for c in nx.biconnected_components(G)]
+        ac = [set(c) for c in nx.biconnected_components(A)]
+        for comp in ac:
+            assert comp in gc
+    # test degree
+    for G, A in pairs:
+        node = list(G.nodes())[0]
+        nodes = list(G.nodes())[1:4]
+        assert G.degree(node) == A.degree(node)
+        assert sum(G.degree().values()) == sum(A.degree().values())
+        # AntiGraph is a ThinGraph, so all the weights are 1
+        assert sum(A.degree().values()) == sum(A.degree(weight='weight').values())
+        assert sum(G.degree(nodes).values()) == sum(A.degree(nodes).values())
diff --git a/networkx/algorithms/approximation/__init__.py b/networkx/algorithms/approximation/__init__.py
--- a/networkx/algorithms/approximation/__init__.py
+++ b/networkx/algorithms/approximation/__init__.py
@@ -2,6 +2,7 @@
 from networkx.algorithms.approximation.clique import *
 from networkx.algorithms.approximation.connectivity import *
 from networkx.algorithms.approximation.dominating_set import *
+from networkx.algorithms.approximation.kcomponents import *
 from networkx.algorithms.approximation.independent_set import *
 from networkx.algorithms.approximation.matching import *
 from networkx.algorithms.approximation.ramsey import *
diff --git a/networkx/algorithms/approximation/kcomponents.py b/networkx/algorithms/approximation/kcomponents.py
new file mode 100644
--- /dev/null
+++ b/networkx/algorithms/approximation/kcomponents.py
@@ -0,0 +1,350 @@
+""" Fast approximation for k-component structure
+"""
+#    Copyright (C) 2015 by 
+#    Jordi Torrents <jtorrents@milnou.net>
+#    All rights reserved.
+#    BSD license.
+import itertools
+import collections
+
+import networkx as nx
+from networkx.exception import NetworkXError
+from networkx.utils import not_implemented_for
+
+from networkx.algorithms.approximation import local_node_connectivity
+from networkx.algorithms.connectivity import \
+    local_node_connectivity as exact_local_node_connectivity
+from networkx.algorithms.connectivity import build_auxiliary_node_connectivity
+from networkx.algorithms.flow import build_residual_network
+
+
+__author__ = """\n""".join(['Jordi Torrents <jtorrents@milnou.net>'])
+
+__all__ = ['k_components']
+
+
+not_implemented_for('directed')
+def k_components(G, min_density=0.95):
+    r"""Returns the approximate k-component structure of a graph G.
+    
+    A `k`-component is a maximal subgraph of a graph G that has, at least, 
+    node connectivity `k`: we need to remove at least `k` nodes to break it
+    into more components. `k`-components have an inherent hierarchical
+    structure because they are nested in terms of connectivity: a connected 
+    graph can contain several 2-components, each of which can contain 
+    one or more 3-components, and so forth.
+
+    This implementation is based on the fast heuristics to approximate
+    the `k`-component sturcture of a graph [1]_. Which, in turn, it is based on
+    a fast approximation algorithm for finding good lower bounds of the number 
+    of node independent paths between two nodes [2]_.
+  
+    Parameters
+    ----------
+    G : NetworkX graph
+        Undirected graph
+
+    min_density : Float
+        Density relaxation treshold. Default value 0.95
+
+    Returns
+    -------
+    k_components : dict
+        Dictionary with connectivity level `k` as key and a list of
+        sets of nodes that form a k-component of level `k` as values.
+        
+
+    Examples
+    --------
+    >>> # Petersen graph has 10 nodes and it is triconnected, thus all 
+    >>> # nodes are in a single component on all three connectivity levels
+    >>> from networkx.algorithms import approximation as apxa
+    >>> G = nx.petersen_graph()
+    >>> k_components = apxa.k_components(G)
+    
+    Notes
+    -----
+    The logic of the approximation algorithm for computing the `k`-component 
+    structure [1]_ is based on repeatedly applying simple and fast algorithms 
+    for `k`-cores and biconnected components in order to narrow down the 
+    number of pairs of nodes over which we have to compute White and Newman's
+    approximation algorithm for finding node independent paths [2]_. More
+    formally, this algorithm is based on Whitney's theorem, which states 
+    an inclusion relation among node connectivity, edge connectivity, and 
+    minimum degree for any graph G. This theorem implies that every 
+    `k`-component is nested inside a `k`-edge-component, which in turn, 
+    is contained in a `k`-core. Thus, this algorithm computes node independent
+    paths among pairs of nodes in each biconnected part of each `k`-core,
+    and repeats this procedure for each `k` from 3 to the maximal core number 
+    of a node in the input graph.
+
+    Because, in practice, many nodes of the core of level `k` inside a 
+    bicomponent actually are part of a component of level k, the auxiliary 
+    graph needed for the algorithm is likely to be very dense. Thus, we use 
+    a complement graph data structure (see `AntiGraph`) to save memory. 
+    AntiGraph only stores information of the edges that are *not* present 
+    in the actual auxiliary graph. When applying algorithms to this 
+    complement graph data structure, it behaves as if it were the dense 
+    version.
+
+    See also
+    --------
+    k_components
+
+    References
+    ----------
+    .. [1]  Torrents, J. and F. Ferraro (2015) Structural Cohesion: 
+            Visualization and Heuristics for Fast Computation.
+            http://arxiv.org/pdf/1503.04476v1
+
+    .. [2]  White, Douglas R., and Mark Newman (2001) A Fast Algorithm for 
+            Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
+            http://eclectic.ss.uci.edu/~drwhite/working.pdf
+
+    .. [3]  Moody, J. and D. White (2003). Social cohesion and embeddedness: 
+            A hierarchical conception of social groups. 
+            American Sociological Review 68(1), 103--28.
+            http://www2.asanet.org/journals/ASRFeb03MoodyWhite.pdf
+
+    """
+    # Dictionary with connectivity level (k) as keys and a list of
+    # sets of nodes that form a k-component as values
+    k_components = collections.defaultdict(list)
+    # make a few functions local for speed
+    node_connectivity = local_node_connectivity
+    k_core = nx.k_core
+    core_number = nx.core_number
+    biconnected_components = nx.biconnected_components
+    density = nx.density
+    combinations = itertools.combinations
+    # Exact solution for k = {1,2}
+    # There is a linear time algorithm for triconnectivity, if we had an
+    # implementation available we could start from k = 4.
+    for component in  nx.connected_components(G):
+        # isolated nodes have connectivity 0
+        comp = set(component)
+        if len(comp) > 1:
+            k_components[1].append(comp)
+    for bicomponent in  nx.biconnected_components(G):
+        # avoid considering dyads as bicomponents
+        bicomp = set(bicomponent)
+        if len(bicomp) > 2:
+            k_components[2].append(bicomp)
+    # There is no k-component of k > maximum core number
+    # \kappa(G) <= \lambda(G) <= \delta(G)
+    g_cnumber = core_number(G)
+    max_core = max(g_cnumber.values())
+    for k in range(3, max_core + 1):
+        C = k_core(G, k, core_number=g_cnumber)
+        for nodes in biconnected_components(C):
+            # Build a subgraph SG induced by the nodes that are part of
+            # each biconnected component of the k-core subgraph C.
+            if len(nodes) < k:
+                continue
+            SG = G.subgraph(nodes)
+            # Build auxiliary graph
+            H = _AntiGraph()
+            H.add_nodes_from(SG.nodes_iter())
+            for u,v in combinations(SG, 2):
+                K = node_connectivity(SG, u, v, cutoff=k)
+                if k > K:
+                    H.add_edge(u,v)
+            for h_nodes in biconnected_components(H):
+                if len(h_nodes) <= k:
+                    continue
+                SH = H.subgraph(h_nodes)
+                for Gc in _cliques_heuristic(SG, SH, k, min_density):
+                    for k_nodes in biconnected_components(Gc):
+                        Gk = nx.k_core(SG.subgraph(k_nodes), k)
+                        if len(Gk) <= k:
+                            continue
+                        k_components[k].append(set(Gk))
+    return k_components
+
+
+def _cliques_heuristic(G, H, k, min_density):
+    h_cnumber = nx.core_number(H)
+    for i, c_value in enumerate(sorted(set(h_cnumber.values()), reverse=True)):
+        cands = set(n for n, c in h_cnumber.items() if c == c_value)
+        # Skip checking for overlap for the highest core value
+        if i == 0:
+            overlap = False
+        else:
+            overlap = set.intersection(*[
+                        set(x for x in H[n] if x not in cands)
+                        for n in cands])
+        if overlap and len(overlap) < k:
+            SH = H.subgraph(cands | overlap)
+        else:
+            SH = H.subgraph(cands)
+        sh_cnumber = nx.core_number(SH)
+        SG = nx.k_core(G.subgraph(SH), k)
+        while not (_same(sh_cnumber) and nx.density(SH) >= min_density):
+            SH = H.subgraph(SG)
+            if len(SH) <= k:
+                break
+            sh_cnumber = nx.core_number(SH)
+            sh_deg = SH.degree()
+            min_deg = min(sh_deg.values())
+            SH.remove_nodes_from(n for n, d in sh_deg.items() if d == min_deg)
+            SG = nx.k_core(G.subgraph(SH), k)
+        else:
+            yield SG
+
+
+def _same(measure, tol=0):
+    vals = set(measure.values())
+    if (max(vals) - min(vals)) <= tol:
+        return True
+    return False
+
+
+class _AntiGraph(nx.Graph):
+    """
+    Class for complement graphs.
+
+    The main goal is to be able to work with big and dense graphs with
+    a low memory foodprint.
+
+    In this class you add the edges that *do not exist* in the dense graph,
+    the report methods of the class return the neighbors, the edges and 
+    the degree as if it was the dense graph. Thus it's possible to use
+    an instance of this class with some of NetworkX functions. In this
+    case we only use k-core, connected_components, and biconnected_components.
+    """
+
+    all_edge_dict = {'weight': 1}
+    def single_edge_dict(self):
+        return self.all_edge_dict
+    edge_attr_dict_factory = single_edge_dict
+
+    def __getitem__(self, n):
+        """Return a dict of neighbors of node n in the dense graph.
+
+        Parameters
+        ----------
+        n : node
+           A node in the graph.
+
+        Returns
+        -------
+        adj_dict : dictionary
+           The adjacency dictionary for nodes connected to n.
+
+        """
+        all_edge_dict = self.all_edge_dict
+        return dict((node, all_edge_dict) for node in 
+                    set(self.adj) - set(self.adj[n]) - set([n]))
+
+    def neighbors(self, n):
+        """Return a list of the nodes connected to the node n in 
+           the dense graph.
+
+        Parameters
+        ----------
+        n : node
+           A node in the graph
+
+        Returns
+        -------
+        nlist : list
+            A list of nodes that are adjacent to n.
+
+        Raises
+        ------
+        NetworkXError
+            If the node n is not in the graph.
+
+        """
+        try:
+            return list(set(self.adj) - set(self.adj[n]) - set([n]))
+        except KeyError:
+            raise NetworkXError("The node %s is not in the graph."%(n,))
+
+    def neighbors_iter(self, n):
+        """Return an iterator over all neighbors of node n in the 
+           dense graph.
+
+        """
+        try:
+            return iter(set(self.adj) - set(self.adj[n]) - set([n]))
+        except KeyError:
+            raise NetworkXError("The node %s is not in the graph."%(n,))
+
+    def degree(self, nbunch=None, weight=None):
+        """Return the degree of a node or nodes in the dense graph.
+        """
+        if nbunch in self:      # return a single node
+            return next(self.degree_iter(nbunch,weight))[1]
+        else:           # return a dict
+            return dict(self.degree_iter(nbunch,weight))
+
+    def degree_iter(self, nbunch=None, weight=None):
+        """Return an iterator for (node, degree) in the dense graph.
+
+        The node degree is the number of edges adjacent to the node.
+
+        Parameters
+        ----------
+        nbunch : iterable container, optional (default=all nodes)
+            A container of nodes.  The container will be iterated
+            through once.
+
+        weight : string or None, optional (default=None)
+           The edge attribute that holds the numerical value used 
+           as a weight.  If None, then each edge has weight 1.
+           The degree is the sum of the edge weights adjacent to the node.
+
+        Returns
+        -------
+        nd_iter : an iterator
+            The iterator returns two-tuples of (node, degree).
+
+        See Also
+        --------
+        degree
+
+        Examples
+        --------
+        >>> G = nx.Graph()   # or DiGraph, MultiGraph, MultiDiGraph, etc
+        >>> G.add_path([0,1,2,3])
+        >>> list(G.degree_iter(0)) # node 0 with degree 1
+        [(0, 1)]
+        >>> list(G.degree_iter([0,1]))
+        [(0, 1), (1, 2)]
+
+        """
+        if nbunch is None:
+            nodes_nbrs = ((n, {v: self.all_edge_dict for v in
+                            set(self.adj) - set(self.adj[n]) - set([n])})
+                            for n in self.nodes_iter())
+        else:
+            nodes_nbrs = ((n, {v: self.all_edge_dict for v in
+                            set(self.nodes()) - set(self.adj[n]) - set([n])})
+                            for n in self.nbunch_iter(nbunch))
+
+        if weight is None:
+            for n,nbrs in nodes_nbrs:
+                yield (n,len(nbrs)+(n in nbrs)) # return tuple (n,degree)
+        else:
+            # AntiGraph is a ThinGraph so all edges have weight 1
+            for n,nbrs in nodes_nbrs:
+                yield (n, sum((nbrs[nbr].get(weight, 1) for nbr in nbrs)) +
+                              (n in nbrs and nbrs[n].get(weight, 1)))
+
+    def adjacency_iter(self):
+        """Return an iterator of (node, adjacency set) tuples for all nodes
+           in the dense graph.
+
+        This is the fastest way to look at every edge.
+        For directed graphs, only outgoing adjacencies are included.
+
+        Returns
+        -------
+        adj_iter : iterator
+           An iterator of (node, adjacency set) for all nodes in
+           the graph.
+
+        """
+        for n in self.adj:
+            yield (n, set(self.adj) - set(self.adj[n]) - set([n]))


Test patch:
diff --git a/networkx/algorithms/approximation/tests/test_connectivity.py b/networkx/algorithms/approximation/tests/test_connectivity.py
--- a/networkx/algorithms/approximation/tests/test_connectivity.py
+++ b/networkx/algorithms/approximation/tests/test_connectivity.py
@@ -61,10 +61,12 @@ def test_octahedral():
     assert_equal(4, approx.node_connectivity(G))
     assert_equal(4, approx.node_connectivity(G, 0, 5))
 
-def test_icosahedral():
-    G=nx.icosahedral_graph()
-    assert_equal(5, approx.node_connectivity(G))
-    assert_equal(5, approx.node_connectivity(G, 0, 5))
+# Approximation can fail with icosahedral graph depending
+# on iteration order.
+#def test_icosahedral():
+#    G=nx.icosahedral_graph()
+#    assert_equal(5, approx.node_connectivity(G))
+#    assert_equal(5, approx.node_connectivity(G, 0, 5))
 
 def test_only_source():
     G = nx.complete_graph(5)
diff --git a/networkx/algorithms/approximation/tests/test_kcomponents.py b/networkx/algorithms/approximation/tests/test_kcomponents.py
new file mode 100644
--- /dev/null
+++ b/networkx/algorithms/approximation/tests/test_kcomponents.py
@@ -0,0 +1,269 @@
+# Test for approximation to k-components algorithm
+from nose.tools import assert_equal, assert_true, assert_false, assert_raises, raises
+import networkx as nx
+from networkx.algorithms.approximation import k_components
+from networkx.algorithms.approximation.kcomponents import _AntiGraph, _same
+
+
+def build_k_number_dict(k_components):
+    k_num = {}
+    for k, comps in sorted(k_components.items()):
+        for comp in comps:
+            for node in comp:
+                k_num[node] = k
+    return k_num
+
+##
+## Some nice synthetic graphs
+##
+def graph_example_1():
+    G = nx.convert_node_labels_to_integers(nx.grid_graph([5,5]),
+                                            label_attribute='labels')
+    rlabels = nx.get_node_attributes(G, 'labels')
+    labels = dict((v, k) for k, v in rlabels.items())
+
+    for nodes in [(labels[(0,0)], labels[(1,0)]),
+                    (labels[(0,4)], labels[(1,4)]),
+                    (labels[(3,0)], labels[(4,0)]),
+                    (labels[(3,4)], labels[(4,4)]) ]:
+        new_node = G.order()+1
+        # Petersen graph is triconnected
+        P = nx.petersen_graph()
+        G = nx.disjoint_union(G,P)
+        # Add two edges between the grid and P
+        G.add_edge(new_node+1, nodes[0])
+        G.add_edge(new_node, nodes[1])
+        # K5 is 4-connected
+        K = nx.complete_graph(5)
+        G = nx.disjoint_union(G,K)
+        # Add three edges between P and K5
+        G.add_edge(new_node+2,new_node+11)
+        G.add_edge(new_node+3,new_node+12)
+        G.add_edge(new_node+4,new_node+13)
+        # Add another K5 sharing a node
+        G = nx.disjoint_union(G,K)
+        nbrs = G[new_node+10]
+        G.remove_node(new_node+10)
+        for nbr in nbrs:
+            G.add_edge(new_node+17, nbr)
+        G.add_edge(new_node+16, new_node+5)
+
+    G.name = 'Example graph for connectivity'
+    return G
+
+def torrents_and_ferraro_graph():
+    G = nx.convert_node_labels_to_integers(nx.grid_graph([5,5]),
+                                            label_attribute='labels')
+    rlabels = nx.get_node_attributes(G, 'labels')
+    labels = dict((v, k) for k, v in rlabels.items())
+
+    for nodes in [ (labels[(0,4)], labels[(1,4)]),
+                    (labels[(3,4)], labels[(4,4)]) ]:
+        new_node = G.order()+1
+        # Petersen graph is triconnected
+        P = nx.petersen_graph()
+        G = nx.disjoint_union(G,P)
+        # Add two edges between the grid and P
+        G.add_edge(new_node+1, nodes[0])
+        G.add_edge(new_node, nodes[1])
+        # K5 is 4-connected
+        K = nx.complete_graph(5)
+        G = nx.disjoint_union(G,K)
+        # Add three edges between P and K5
+        G.add_edge(new_node+2,new_node+11)
+        G.add_edge(new_node+3,new_node+12)
+        G.add_edge(new_node+4,new_node+13)
+        # Add another K5 sharing a node
+        G = nx.disjoint_union(G,K)
+        nbrs = G[new_node+10]
+        G.remove_node(new_node+10)
+        for nbr in nbrs:
+            G.add_edge(new_node+17, nbr)
+        # Commenting this makes the graph not biconnected !!
+        # This stupid mistake make one reviewer very angry :P
+        G.add_edge(new_node+16, new_node+8)
+
+    for nodes in [(labels[(0,0)], labels[(1,0)]),
+                    (labels[(3,0)], labels[(4,0)])]:
+        new_node = G.order()+1
+        # Petersen graph is triconnected
+        P = nx.petersen_graph()
+        G = nx.disjoint_union(G,P)
+        # Add two edges between the grid and P
+        G.add_edge(new_node+1, nodes[0])
+        G.add_edge(new_node, nodes[1])
+        # K5 is 4-connected
+        K = nx.complete_graph(5)
+        G = nx.disjoint_union(G,K)
+        # Add three edges between P and K5
+        G.add_edge(new_node+2,new_node+11)
+        G.add_edge(new_node+3,new_node+12)
+        G.add_edge(new_node+4,new_node+13)
+        # Add another K5 sharing two nodes
+        G = nx.disjoint_union(G,K)
+        nbrs = G[new_node+10]
+        G.remove_node(new_node+10)
+        for nbr in nbrs:
+            G.add_edge(new_node+17, nbr)
+        nbrs2 = G[new_node+9]
+        G.remove_node(new_node+9)
+        for nbr in nbrs2:
+            G.add_edge(new_node+18, nbr)
+
+    G.name = 'Example graph for connectivity'
+    return G
+
+# Helper function
+def _check_connectivity(G):
+    result = k_components(G)
+    for k, components in result.items():
+        if k < 3:
+            continue
+        for component in components:
+            C = G.subgraph(component)
+            K = nx.node_connectivity(C)
+            assert_true(K >= k)
+
+def test_torrents_and_ferraro_graph():
+    G = torrents_and_ferraro_graph()
+    _check_connectivity(G)
+
+def test_example_1():
+    G = graph_example_1()
+    _check_connectivity(G)
+
+def test_random_gnp():
+    G = nx.gnp_random_graph(50, 0.2)
+    _check_connectivity(G)
+
+def test_shell():
+    constructor=[(20,80,0.8),(80,180,0.6)]
+    G = nx.random_shell_graph(constructor)
+    _check_connectivity(G)
+
+def test_configuration():
+    deg_seq = nx.utils.create_degree_sequence(100,nx.utils.powerlaw_sequence)
+    G = nx.Graph(nx.configuration_model(deg_seq))
+    G.remove_edges_from(G.selfloop_edges())
+    _check_connectivity(G)
+
+def test_karate_0():
+    G = nx.karate_club_graph()
+    _check_connectivity(G)
+
+def test_karate_1():
+    karate_k_num = {0: 4, 1: 4, 2: 4, 3: 4, 4: 3, 5: 3, 6: 3, 7: 4, 8: 4, 9: 2,
+                    10: 3, 11: 1, 12: 2, 13: 4, 14: 2, 15: 2, 16: 2, 17: 2, 18: 2,
+                    19: 3, 20: 2, 21: 2, 22: 2, 23: 3, 24: 3, 25: 3, 26: 2, 27: 3,
+                    28: 3, 29: 3, 30: 4, 31: 3, 32: 4, 33: 4}
+    G = nx.karate_club_graph()
+    k_comps = k_components(G)
+    k_num = build_k_number_dict(k_comps)
+    assert_equal(karate_k_num, k_num)
+
+def test_example_1_detail_3_and_4():
+    solution = {
+        3: [set([40, 41, 42, 43, 39]),
+            set([32, 33, 34, 35, 36, 37, 38, 42, 25, 26, 27, 28, 29, 30, 31]),
+            set([58, 59, 60, 61, 62]),
+            set([44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 61]),
+            set([80, 81, 77, 78, 79]),
+            set([64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 80, 63]),
+            set([97, 98, 99, 100, 101]),
+            set([96, 100, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 94, 95])
+        ],
+        4: [set([40, 41, 42, 43, 39]),
+            set([42, 35, 36, 37, 38]),
+            set([58, 59, 60, 61, 62]),
+            set([56, 57, 61, 54, 55]),
+            set([80, 81, 77, 78, 79]),
+            set([80, 73, 74, 75, 76]),
+            set([97, 98, 99, 100, 101]),
+            set([96, 100, 92, 94, 95])
+        ],
+    }
+    G = graph_example_1()
+    result = k_components(G)
+    for k, components in solution.items():
+        for component in components:
+            assert_true(component in result[k])
+
+@raises(nx.NetworkXNotImplemented)
+def test_directed():
+    G = nx.gnp_random_graph(10, 0.4, directed=True)
+    kc = k_components(G)
+
+def test_same():
+    equal = {'A': 2, 'B': 2, 'C': 2}
+    slightly_different =  {'A': 2, 'B': 1, 'C': 2}
+    different = {'A': 2, 'B': 8, 'C': 18}
+    assert_true(_same(equal))
+    assert_false(_same(slightly_different))
+    assert_true(_same(slightly_different, tol=1))
+    assert_false(_same(different))
+    assert_false(_same(different, tol=4))
+
+
+class TestAntiGraph:
+    def setUp(self):
+        self.Gnp = nx.gnp_random_graph(20,0.8)
+        self.Anp = _AntiGraph(nx.complement(self.Gnp))
+        self.Gd = nx.davis_southern_women_graph()
+        self.Ad = _AntiGraph(nx.complement(self.Gd))
+        self.Gk = nx.karate_club_graph()
+        self.Ak = _AntiGraph(nx.complement(self.Gk))
+        self.GA = [(self.Gnp, self.Anp),
+                    (self.Gd,self.Ad),
+                    (self.Gk, self.Ak)]
+
+    def test_size(self):
+        for G, A in self.GA:
+            n = G.order()
+            s = len(G.edges())+len(A.edges())
+            assert_true(s == (n*(n-1))/2)
+
+    def test_degree(self):
+        for G, A in self.GA:
+            assert_equal(G.degree(), A.degree())
+
+    def test_core_number(self):
+        for G, A in self.GA:
+            assert_equal(nx.core_number(G), nx.core_number(A))
+
+    def test_connected_components(self):
+        for G, A in self.GA:
+            gc = [set(c) for c in nx.connected_components(G)]
+            ac = [set(c) for c in nx.connected_components(A)]
+            for comp in ac:
+                assert_true(comp in gc)
+
+    def test_adjacency_iter(self):
+        for G, A in self.GA:
+            a_adj = list(A.adjacency_iter())
+            for n, nbrs in G.adjacency_iter():
+                assert_true((n, set(nbrs)) in a_adj)
+
+    def test_neighbors(self):
+        for G, A in self.GA:
+            node = list(G.nodes())[0]
+            assert_equal(set(G.neighbors(node)), set(A.neighbors(node)))
+
+    def test_node_not_in_graph(self):
+        for G, A in self.GA:
+            node = 'non_existent_node'
+            assert_raises(nx.NetworkXError, A.neighbors, node)
+            assert_raises(nx.NetworkXError, A.neighbors_iter, node)
+            assert_raises(nx.NetworkXError, G.neighbors, node)
+            assert_raises(nx.NetworkXError, G.neighbors_iter, node)
+
+    def test_degree(self):
+        for G, A in self.GA:
+            node = list(G.nodes())[0]
+            nodes = list(G.nodes())[1:4]
+            assert_equal(G.degree(node), A.degree(node))
+            assert_equal(sum(G.degree().values()), sum(A.degree().values()))
+            # AntiGraph is a ThinGraph, so all the weights are 1
+            assert_equal(sum(A.degree().values()), 
+                         sum(A.degree(weight='weight').values()))
+            assert_equal(sum(G.degree(nodes).values()), 
+                         sum(A.degree(nodes).values()))