Skip to content

Commit

Permalink
graph/community: add k-clique community function
Browse files Browse the repository at this point in the history
  • Loading branch information
kortschak committed Sep 8, 2017
1 parent a1136ab commit 64750a0
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 0 deletions.
98 changes: 98 additions & 0 deletions graph/community/k_communities.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright ©2017 The gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package community

import (
"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/graph/internal/set"
"gonum.org/v1/gonum/graph/simple"
"gonum.org/v1/gonum/graph/topo"
"gonum.org/v1/gonum/graph/traverse"
)

// KCliqueCommunities returns the k-clique communties of the undirected graph g for
// k greater than zero. The returned communities are identified by linkage via k-clique
// adjacency, where adjacency is defined as having k-1 common nodes. KCliqueCommunities
// returns a single component including the full set of nodes of g when k is 1,
// and the classical connected components of g when k is 2. Note that k-clique
// communities may contain common nodes from g.
//
// k-clique communities are described in Palla et al. doi:10.1038/nature03607.
func KCliqueCommunities(k int, g graph.Undirected) [][]graph.Node {
if k < 1 {
panic("community: invalid k for k-clique communities")
}
switch k {
case 1:
return [][]graph.Node{g.Nodes()}
case 2:
return topo.ConnectedComponents(g)
default:
cg := simple.NewUndirectedGraph()
topo.CliqueGraph(cg, g)
cc := kConnectedComponents(k, cg)

// Extract the nodes in g from cg,
// removing duplicates and separating
// cliques smaller than k into separate
// single nodes.
var kcc [][]graph.Node
single := make(set.Nodes)
inCommunity := make(set.Nodes)
for _, c := range cc {
nodes := make(set.Nodes, len(c))
for _, cn := range c {
for _, n := range cn.(topo.Clique).Nodes() {
nodes.Add(n)
}
}
if len(nodes) < k {
for _, n := range nodes {
single.Add(n)
}
continue
}
var kc []graph.Node
for _, n := range nodes {
inCommunity.Add(n)
kc = append(kc, n)
}
kcc = append(kcc, kc)
}
for _, n := range single {
if !inCommunity.Has(n) {
kcc = append(kcc, []graph.Node{n})
}
}

return kcc
}
}

// kConnectedComponents returns the connected components of topo.Clique nodes that
// are joined by k-1 underlying shared nodes in the graph that created the clique
// graph cg.
func kConnectedComponents(k int, cg graph.Undirected) [][]graph.Node {
var (
c []graph.Node
cc [][]graph.Node
)
during := func(n graph.Node) {
c = append(c, n)
}
after := func() {
cc = append(cc, []graph.Node(nil))
cc[len(cc)-1] = append(cc[len(cc)-1], c...)
c = c[:0]
}
w := traverse.DepthFirst{
EdgeFilter: func(e graph.Edge) bool {
return len(e.(topo.CliqueGraphEdge).Nodes()) >= k-1
},
}
w.WalkAll(cg, nil, after, during)

return cc
}
132 changes: 132 additions & 0 deletions graph/community/k_communities_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright ©2017 The gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package community

import (
"reflect"
"sort"
"testing"

"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/graph/internal/ordered"
"gonum.org/v1/gonum/graph/simple"
)

// batageljZaversnikGraph is the example graph from
// figure 1 of http://arxiv.org/abs/cs/0310049v1
var batageljZaversnikGraph = []intset{
0: nil,

1: linksTo(2, 3),
2: linksTo(4),
3: linksTo(4),
4: linksTo(5),
5: nil,

6: linksTo(7, 8, 14),
7: linksTo(8, 11, 12, 14),
8: linksTo(14),
9: linksTo(11),
10: linksTo(11),
11: linksTo(12),
12: linksTo(18),
13: linksTo(14, 15),
14: linksTo(15, 17),
15: linksTo(16, 17),
16: nil,
17: linksTo(18, 19, 20),
18: linksTo(19, 20),
19: linksTo(20),
20: nil,
}

var kCliqueCommunitiesTests = []struct {
g []intset
k int
want [][]graph.Node
}{
{
g: []intset{
0: linksTo(1, 2, 4, 6),
1: linksTo(2, 4, 6),
2: linksTo(3, 6),
3: linksTo(4, 5),
4: linksTo(6),
5: nil,
6: nil,
},
k: 3,
want: [][]graph.Node{
{simple.Node(0), simple.Node(1), simple.Node(2), simple.Node(4), simple.Node(6)},
{simple.Node(3)},
{simple.Node(5)},
},
},
{
g: batageljZaversnikGraph,
k: 3,
want: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
{simple.Node(5)},
{simple.Node(6), simple.Node(7), simple.Node(8), simple.Node(14)},
{simple.Node(7), simple.Node(11), simple.Node(12)},
{simple.Node(9)},
{simple.Node(10)},
{simple.Node(13), simple.Node(14), simple.Node(15), simple.Node(17)},
{simple.Node(16)},
{simple.Node(17), simple.Node(18), simple.Node(19), simple.Node(20)},
},
},
{
g: batageljZaversnikGraph,
k: 4,
want: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
{simple.Node(5)},
{simple.Node(6), simple.Node(7), simple.Node(8), simple.Node(14)},
{simple.Node(9)},
{simple.Node(10)},
{simple.Node(11)},
{simple.Node(12)},
{simple.Node(13)},
{simple.Node(15)},
{simple.Node(16)},
{simple.Node(17), simple.Node(18), simple.Node(19), simple.Node(20)},
},
},
}

func TestKCliqueCommunities(t *testing.T) {
for _, test := range kCliqueCommunitiesTests {
g := simple.NewUndirectedGraph()
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)})
}
}
got := KCliqueCommunities(test.k, g)

for _, c := range got {
sort.Sort(ordered.ByID(c))
}
sort.Sort(ordered.BySliceIDs(got))

if !reflect.DeepEqual(got, test.want) {
t.Errorf("unexpected k-connected components:\ngot: %v\nwant:%v", got, test.want)
}
}
}

0 comments on commit 64750a0

Please sign in to comment.