rgraph 0.0.9 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/rgraph/graph.rb +66 -4
- data/lib/rgraph/link.rb +3 -2
- data/lib/rgraph/version.rb +1 -1
- data/spec/rgraph/graph_spec.rb +65 -3
- data/spec/rgraph/link_spec.rb +16 -9
- metadata +2 -2
data/lib/rgraph/graph.rb
CHANGED
@@ -6,24 +6,32 @@ require_relative '../../lib/rgraph/node'
|
|
6
6
|
class Graph
|
7
7
|
attr_accessor :nodes, :links, :infinity
|
8
8
|
|
9
|
+
def self.new_from_string(string)
|
10
|
+
csv = CSV.new(string, headers: true)
|
11
|
+
new(csv)
|
12
|
+
end
|
13
|
+
|
9
14
|
def initialize(csv)
|
10
15
|
@nodes = []
|
11
16
|
@links = []
|
12
17
|
@distance = nil
|
13
18
|
@infinity = 100_000
|
14
|
-
raise Exception.new("the file must be a .csv") unless File.extname(csv) == ".csv"
|
15
19
|
|
16
|
-
CSV.
|
20
|
+
csv = CSV.new(File.open(csv), headers: true) if csv.is_a?(String)
|
21
|
+
|
22
|
+
csv.each do |row|
|
17
23
|
#last because CSV#delete returns [column,value]
|
18
24
|
source_id = row.delete('source').last
|
19
25
|
target_id = row.delete('target').last
|
26
|
+
type = row.delete('type').last || 'undirected'
|
27
|
+
weight = row.delete('weight').last || 1
|
20
28
|
|
21
29
|
source = get_node_by_id(source_id) || Node.new(id: source_id)
|
22
30
|
target = get_node_by_id(target_id) || Node.new(id: target_id)
|
23
31
|
|
24
32
|
maybe_add_to_nodes(source, target)
|
25
33
|
|
26
|
-
@links << Link.new(source: source, target: target, weight:
|
34
|
+
@links << Link.new(source: source, target: target, weight: weight, type: type)
|
27
35
|
end
|
28
36
|
end
|
29
37
|
|
@@ -84,6 +92,13 @@ class Graph
|
|
84
92
|
@distance
|
85
93
|
end
|
86
94
|
|
95
|
+
def mdistance
|
96
|
+
distances unless @distance
|
97
|
+
|
98
|
+
fdistance = @distance.flatten.select{|d| d != @infinity}
|
99
|
+
fdistance.inject(:+) / fdistance.count.to_f
|
100
|
+
end
|
101
|
+
|
87
102
|
def shortest_paths
|
88
103
|
tmp = []
|
89
104
|
|
@@ -127,7 +142,7 @@ class Graph
|
|
127
142
|
|
128
143
|
def betweenness(args = {})
|
129
144
|
cumulative = args[:cumulative] || false
|
130
|
-
|
145
|
+
|
131
146
|
#If we ask cumulative, normalized must be true
|
132
147
|
normalized = args[:normalized] || cumulative || false
|
133
148
|
|
@@ -153,8 +168,50 @@ class Graph
|
|
153
168
|
(distances.flatten - [@infinity]).max
|
154
169
|
end
|
155
170
|
|
171
|
+
def single_clustering(node)
|
172
|
+
possible = possible_connections(node)
|
173
|
+
return 0 if possible == 0
|
174
|
+
|
175
|
+
existent = node.neighbours.combination(2).select{ |t| t[0].neighbours.include?(t[1]) }.count
|
176
|
+
existent / possible.to_f
|
177
|
+
end
|
178
|
+
|
179
|
+
def clustering
|
180
|
+
nodes.map{ |node| single_clustering(node) }.inject(:+) / nodes.size.to_f
|
181
|
+
end
|
182
|
+
|
183
|
+
def page_rank(d = 0.85, e = 0.01)
|
184
|
+
n = @nodes.count
|
185
|
+
|
186
|
+
#Initial values
|
187
|
+
@page_rank = Array.new(n, 1 / n.to_f)
|
188
|
+
|
189
|
+
while true
|
190
|
+
return @page_rank if @page_rank.inject(:+) - step_page_rank(d, e).inject(:+) < e
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
156
194
|
private
|
157
195
|
|
196
|
+
def step_page_rank(d = 0.85, e = 0.01)
|
197
|
+
n = @nodes.count
|
198
|
+
tmp = Array.new(n, 0)
|
199
|
+
|
200
|
+
@nodes.each_with_index do |node,i|
|
201
|
+
#How much 'node' will give to its neighbours
|
202
|
+
to_neighbours = @page_rank[i] / node.neighbours.count
|
203
|
+
|
204
|
+
#Give 'to_neighbours' to each neighbour
|
205
|
+
node.neighbours.each do |ne|
|
206
|
+
j = @nodes.index(ne)
|
207
|
+
tmp[j] += to_neighbours
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
#Calculates the final value
|
212
|
+
@page_rank = tmp.map{|t| ((1 - d) / n) + t * d}
|
213
|
+
end
|
214
|
+
|
158
215
|
def get_node_by_id(node_id)
|
159
216
|
@nodes.select{|n| n.id == node_id}.first
|
160
217
|
end
|
@@ -164,4 +221,9 @@ class Graph
|
|
164
221
|
@nodes << node unless get_node_by_id(node.id)
|
165
222
|
end
|
166
223
|
end
|
224
|
+
|
225
|
+
def possible_connections(node)
|
226
|
+
total = node.neighbours.count
|
227
|
+
total * (total - 1) / 2
|
228
|
+
end
|
167
229
|
end
|
data/lib/rgraph/link.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
class Link
|
2
|
-
attr_accessor :source, :target
|
2
|
+
attr_accessor :source, :target, :type
|
3
3
|
|
4
4
|
def initialize(arg)
|
5
5
|
@args = arg
|
6
6
|
@source = @args.delete(:source)
|
7
7
|
@target = @args.delete(:target)
|
8
|
+
@type = @args.delete(:type) || 'undirected'
|
8
9
|
|
9
10
|
raise Exception.new("source cant be nil") unless @source
|
10
11
|
raise Exception.new("target cant be nil") unless @target
|
@@ -14,7 +15,7 @@ class Link
|
|
14
15
|
@args[:weight] ||= 1
|
15
16
|
|
16
17
|
@source.neighbours << @target
|
17
|
-
@target.neighbours << @source
|
18
|
+
@target.neighbours << @source unless @type == 'directed'
|
18
19
|
end
|
19
20
|
|
20
21
|
def method_missing(name, *args)
|
data/lib/rgraph/version.rb
CHANGED
data/spec/rgraph/graph_spec.rb
CHANGED
@@ -39,8 +39,17 @@ describe Graph do
|
|
39
39
|
expect(Graph.new('spec/fixtures/2005.csv').nodes.size).to eq(445)
|
40
40
|
end
|
41
41
|
|
42
|
-
it "creates a
|
43
|
-
|
42
|
+
it "creates a graph with string as input" do
|
43
|
+
graph = Graph.new_from_string("source,target\n1,2")
|
44
|
+
expect(graph.nodes.count).to eq(2)
|
45
|
+
expect(graph.links.count).to eq(1)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "understands directed graph" do
|
49
|
+
graph = Graph.new_from_string("source,target,type\n1,2,directed")
|
50
|
+
expect(graph.nodes.first.neighbours.count).to eq(1)
|
51
|
+
expect(graph.nodes.last.neighbours.count).to eq(0)
|
52
|
+
expect(graph.links.count).to eq(1)
|
44
53
|
end
|
45
54
|
end
|
46
55
|
|
@@ -76,7 +85,7 @@ describe Graph do
|
|
76
85
|
[k,k,k,1,k,0]])
|
77
86
|
end
|
78
87
|
|
79
|
-
it "
|
88
|
+
it "calculates distances" do
|
80
89
|
expect(subject.distances).to eq(
|
81
90
|
[[0,1,2,2,1,3],
|
82
91
|
[1,0,1,2,1,3],
|
@@ -86,6 +95,17 @@ describe Graph do
|
|
86
95
|
[3,3,2,1,2,0]])
|
87
96
|
end
|
88
97
|
|
98
|
+
it "calculates the mean distance" do
|
99
|
+
mdistance = subject.distances.flatten
|
100
|
+
mdistance = mdistance.inject(:+) / mdistance.count.to_f
|
101
|
+
expect(subject.mdistance).to eq(mdistance)
|
102
|
+
end
|
103
|
+
|
104
|
+
it "calculates the mean distance ignoring 'infinity'" do
|
105
|
+
graph = Graph.new('spec/fixtures/two_links_with_hole.csv')
|
106
|
+
expect(graph.mdistance).to eq(10.0/13.0)
|
107
|
+
end
|
108
|
+
|
89
109
|
it "understands holes on the graph" do
|
90
110
|
graph = Graph.new('spec/fixtures/two_links_with_hole.csv')
|
91
111
|
k = graph.infinity
|
@@ -149,5 +169,47 @@ describe Graph do
|
|
149
169
|
it "calculates cumulative betweenness" do
|
150
170
|
expect(subject.betweenness(cumulative: true)).to eq([[6, 0.0], [4, 0.5], [1, 1.0]])
|
151
171
|
end
|
172
|
+
it "calculates clustering of a single node" do
|
173
|
+
nodes = subject.nodes.sort{|a,b| a.id <=> b.id}
|
174
|
+
|
175
|
+
expect(subject.single_clustering(nodes[0])).to eq(1.0/1.0)
|
176
|
+
expect(subject.single_clustering(nodes[1])).to eq(1.0/3.0)
|
177
|
+
expect(subject.single_clustering(nodes[2])).to eq(0.0/1.0)
|
178
|
+
expect(subject.single_clustering(nodes[3])).to eq(0.0/3.0)
|
179
|
+
expect(subject.single_clustering(nodes[4])).to eq(1.0/3.0)
|
180
|
+
expect(subject.single_clustering(nodes[5])).to eq(0.0/1.0)
|
181
|
+
end
|
182
|
+
it "calculates clustering of all nodes" do
|
183
|
+
expect(subject.clustering).to eq((1.0/1.0 + 2.0/3.0) / 6.0)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
describe "PageRank" do
|
187
|
+
it "calculates with two nodes connected" do
|
188
|
+
pr = Graph.new_from_string("source,target,type\n1,2,directed").page_rank
|
189
|
+
expect(pr.first).to be < pr.last
|
190
|
+
end
|
191
|
+
it "calculates with three nodes within a line" do
|
192
|
+
pr = Graph.new_from_string("source,target,type\n1,2,directed\n2,3,directed").page_rank
|
193
|
+
expect(pr[0]).to be < pr[1]
|
194
|
+
expect(pr[1]).to be < pr[2]
|
195
|
+
end
|
196
|
+
it "calculates with two nodes pointing to the same" do
|
197
|
+
pr = Graph.new_from_string("source,target,type\n1,2,directed\n3,2,directed").page_rank
|
198
|
+
expect(pr[0]).to be < pr[1]
|
199
|
+
expect(pr[2]).to be < pr[1]
|
200
|
+
expect(pr[0]).to eq(pr[2])
|
201
|
+
end
|
202
|
+
|
203
|
+
it "calculates with a cycle" do
|
204
|
+
pr = Graph.new_from_string("source,target,type\n1,2,directed\n2,3,directed\n3,1,directed").page_rank
|
205
|
+
expect(pr[0]).to eq(pr[1])
|
206
|
+
expect(pr[1]).to eq(pr[2])
|
207
|
+
expect(pr.inject(:+)).to eq(1)
|
208
|
+
end
|
209
|
+
it "calculates with two subgraphs" do
|
210
|
+
pr = Graph.new_from_string("source,target,type\n1,2,directed\n3,4,directed\n3,1,directed").page_rank
|
211
|
+
expect(pr[0]).to be < pr[1]
|
212
|
+
expect(pr[2]).to be < pr[3]
|
213
|
+
end
|
152
214
|
end
|
153
215
|
end
|
data/spec/rgraph/link_spec.rb
CHANGED
@@ -4,7 +4,7 @@ require_relative '../../lib/rgraph/node'
|
|
4
4
|
|
5
5
|
describe Link do
|
6
6
|
describe "creates a link without a weight" do
|
7
|
-
subject { Link.new(source: Node.new(id:
|
7
|
+
subject { Link.new(source: Node.new(id: 1), target: Node.new(id: 2), years: [2011, 2012]) }
|
8
8
|
its(:source) { should be_kind_of Node }
|
9
9
|
its(:target) { should be_kind_of Node }
|
10
10
|
its(:weight) { should == 1 }
|
@@ -15,31 +15,38 @@ describe Link do
|
|
15
15
|
expect(subject.target.neighbours).to eq([subject.source])
|
16
16
|
end
|
17
17
|
|
18
|
+
it "checks link's direction" do
|
19
|
+
link = Link.new(source: Node.new(id: 1), target: Node.new(id: 2), type: 'directed')
|
20
|
+
expect(link.source.neighbours.count).to eq(1)
|
21
|
+
expect(link.target.neighbours.count).to eq(0)
|
22
|
+
end
|
23
|
+
|
18
24
|
end
|
19
25
|
|
20
26
|
describe "creates a link passing a weight" do
|
21
27
|
subject { Link.new(source: Node.new(id: 00001), target: Node.new(id: 00002), weight: 4, years: [2011, 2012]) }
|
22
28
|
its(:weight) { should == 4 }
|
23
29
|
|
24
|
-
it "checks the creation of neighbours" do
|
25
|
-
expect(subject.source.neighbours).to eq([subject.target])
|
26
|
-
expect(subject.target.neighbours).to eq([subject.source])
|
27
|
-
end
|
28
30
|
end
|
29
31
|
|
30
|
-
it "
|
32
|
+
it "doesn't create a link without source" do
|
31
33
|
expect { Link.new(target: Node.new(id: 00001)) }.to raise_exception("source cant be nil")
|
32
34
|
end
|
33
35
|
|
34
|
-
it "
|
36
|
+
it "doesn't create a link without target" do
|
35
37
|
expect { Link.new(source: Node.new(id: 00001)) }.to raise_exception("target cant be nil")
|
36
38
|
end
|
37
39
|
|
38
|
-
it "
|
40
|
+
it "doesn't create a link with wrong source type" do
|
39
41
|
expect { Link.new(source: "Lucas", target: Node.new(id: 1)) }.to raise_exception("source must be of type Node")
|
40
42
|
end
|
41
43
|
|
42
|
-
it "
|
44
|
+
it "doesn't create a link with wrong target type" do
|
43
45
|
expect { Link.new(source: Node.new(id: 1), target: "Lucas") }.to raise_exception("target must be of type Node")
|
44
46
|
end
|
47
|
+
|
48
|
+
it "sets default type as 'undirected'" do
|
49
|
+
expect(Link.new(source: Node.new(id: 1), target: Node.new(id: 2)).type).to eq('undirected')
|
50
|
+
end
|
51
|
+
|
45
52
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rgraph
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.10
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2013-
|
13
|
+
date: 2013-09-12 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: bundler
|