networkx 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/{CODE_OF_CONDUCT.md → .github/CODE_OF_CONDUCT.md} +0 -0
- data/{CONTRIBUTING.md → .github/CONTRIBUTING.md} +21 -11
- data/.github/ISSUE_TEMPLATE.md +15 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +10 -0
- data/.github/workflows/ci.yml +17 -0
- data/.github/workflows/doc.yml +23 -0
- data/.github/workflows/gem-push.yml +45 -0
- data/.rspec +0 -1
- data/.rubocop.yml +56 -63
- data/.yardopts +0 -1
- data/README.md +27 -27
- data/Rakefile +2 -3
- data/lib/networkx/auxillary_functions/cliques.rb +62 -0
- data/lib/networkx/auxillary_functions/cycles.rb +114 -0
- data/lib/networkx/auxillary_functions/dag.rb +59 -0
- data/lib/networkx/auxillary_functions/eccentricity.rb +37 -0
- data/lib/networkx/auxillary_functions/mis.rb +23 -0
- data/lib/networkx/auxillary_functions/mst.rb +33 -0
- data/lib/networkx/auxillary_functions/union_find.rb +104 -0
- data/lib/networkx/auxillary_functions/vitality.rb +13 -0
- data/lib/networkx/auxillary_functions/wiener.rb +13 -0
- data/lib/networkx/converters/to_csv.rb +45 -0
- data/lib/networkx/converters/to_json.rb +37 -0
- data/lib/networkx/digraph.rb +234 -0
- data/lib/networkx/flow/capacityscaling.rb +249 -0
- data/lib/networkx/flow/edmondskarp.rb +115 -0
- data/lib/networkx/flow/preflowpush.rb +249 -0
- data/lib/networkx/flow/shortestaugmentingpath.rb +154 -0
- data/lib/networkx/flow/utils.rb +139 -0
- data/lib/networkx/graph.rb +448 -0
- data/lib/networkx/link_analysis/hits.rb +59 -0
- data/lib/networkx/link_analysis/pagerank.rb +89 -0
- data/lib/networkx/multidigraph.rb +249 -0
- data/lib/networkx/multigraph.rb +199 -0
- data/lib/networkx/operators/all.rb +65 -0
- data/lib/networkx/operators/binary.rb +222 -0
- data/lib/networkx/operators/product.rb +201 -0
- data/lib/networkx/operators/unary.rb +17 -0
- data/lib/networkx/others/bridges.rb +30 -0
- data/lib/networkx/others/generators.rb +237 -0
- data/lib/networkx/others/grid_2d_graph.rb +38 -0
- data/lib/networkx/others/info.rb +11 -0
- data/lib/networkx/others/number_connected_components.rb +17 -0
- data/lib/networkx/others/reads.rb +52 -0
- data/lib/networkx/shortest_path/astar.rb +73 -0
- data/lib/networkx/shortest_path/dense.rb +29 -0
- data/lib/networkx/shortest_path/unweighted.rb +136 -0
- data/lib/networkx/shortest_path/weighted.rb +417 -0
- data/lib/networkx/to_matrix.rb +51 -0
- data/lib/networkx/traversals/bfs.rb +110 -0
- data/lib/networkx/traversals/dfs.rb +135 -0
- data/lib/networkx/traversals/edge_dfs.rb +114 -0
- data/lib/networkx/version.rb +1 -1
- data/lib/networkx.rb +43 -1
- data/networkx.gemspec +14 -12
- metadata +118 -62
- data/.rspec_formatter.rb +0 -24
- data/.travis.yml +0 -18
- data/Guardfile +0 -7
@@ -0,0 +1,448 @@
|
|
1
|
+
module NetworkX
|
2
|
+
# Describes the class for making Undirected Graphs
|
3
|
+
#
|
4
|
+
# @attr_reader adj [Hash{ Object => Hash{ Object => Hash{ Object => Object } } }]
|
5
|
+
# Stores the edges and their attributes in an adjencency list form
|
6
|
+
# @attr_reader graph [Hash{ Object => Object }] Stores the attributes of the graph
|
7
|
+
class Graph
|
8
|
+
attr_reader :adj, :graph
|
9
|
+
|
10
|
+
# Constructor for initializing graph
|
11
|
+
#
|
12
|
+
# @example Initialize a graph with attributes 'type' and 'name'
|
13
|
+
# graph = NetworkX::Graph.new(name: "Social Network", type: "undirected")
|
14
|
+
#
|
15
|
+
# @param graph_attrs [Hash{ Object => Object }] the graph attributes in a hash format
|
16
|
+
def initialize(**graph_attrs)
|
17
|
+
@nodes = {}
|
18
|
+
@adj = {}
|
19
|
+
@graph = graph_attrs
|
20
|
+
end
|
21
|
+
|
22
|
+
# Adds the respective edges
|
23
|
+
#
|
24
|
+
# @example Add an edge with attribute name
|
25
|
+
# graph.add_edge(node1, node2, name: "Edge1")
|
26
|
+
#
|
27
|
+
# @example Add an edge with no attribute
|
28
|
+
# graph.add_edge("Bangalore", "Chennai")
|
29
|
+
#
|
30
|
+
# @param node1 [Object] the first node of the edge
|
31
|
+
# @param node2 [Object] the second node of the edge
|
32
|
+
# @param edge_attrs [Hash{ Object => Object }] the hash of the edge attributes
|
33
|
+
def add_edge(node1, node2, **edge_attrs)
|
34
|
+
add_node(node1)
|
35
|
+
add_node(node2)
|
36
|
+
|
37
|
+
edge_attrs = (@adj[node1][node2] || {}).merge(edge_attrs)
|
38
|
+
@adj[node1][node2] = edge_attrs
|
39
|
+
@adj[node2][node1] = edge_attrs
|
40
|
+
end
|
41
|
+
|
42
|
+
# Adds multiple edges from an array
|
43
|
+
#
|
44
|
+
# @example Add multiple edges without any attributes
|
45
|
+
# graph.add_edges([['Nagpur', 'Kgp'], ['Noida', 'Kgp']])
|
46
|
+
# @param edges [Array<Object, Object>]
|
47
|
+
def add_edges(edges)
|
48
|
+
case edges
|
49
|
+
when Array
|
50
|
+
edges.each { |node1, node2, attrs| add_edge(node1, node2, **(attrs || {})) }
|
51
|
+
else
|
52
|
+
raise ArgumentError, 'Expected argument to be an Array of edges, ' \
|
53
|
+
"received #{edges.class.name} instead."
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_edges_from(rng)
|
58
|
+
rng.each { |node| add_edge(*node) }
|
59
|
+
end
|
60
|
+
|
61
|
+
# Adds a node and its attributes to the graph
|
62
|
+
#
|
63
|
+
# @example Add a node with attribute 'type'
|
64
|
+
# graph.add_node("Noida", type: "city")
|
65
|
+
#
|
66
|
+
# @param node [Object] the node object
|
67
|
+
# @param node_attrs [Hash{ Object => Object }] the hash of the attributes of the node
|
68
|
+
def add_node(node, **node_attrs)
|
69
|
+
if @nodes.has_key?(node)
|
70
|
+
@nodes[node].merge!(node_attrs)
|
71
|
+
else
|
72
|
+
@adj[node] = {}
|
73
|
+
@nodes[node] = node_attrs
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Adds multiple nodes to the graph
|
78
|
+
#
|
79
|
+
# @example Adds multiple nodes with attribute 'type'
|
80
|
+
# graph.add_nodes([["Noida", type: "city"], ["Kgp", type: "town"]])
|
81
|
+
#
|
82
|
+
# @param nodes [Array<Object, Hash{ Object => Object }>] the Array of pair containing nodes and its attributes
|
83
|
+
def add_nodes(nodes)
|
84
|
+
case nodes
|
85
|
+
when Set, Array
|
86
|
+
nodes.each { |node, node_attrs| add_node(node, **(node_attrs || {})) }
|
87
|
+
when Range
|
88
|
+
nodes.each { |node| add_node(node) }
|
89
|
+
else
|
90
|
+
raise ArgumentError, 'Expected argument to be an Array/Set/Range of nodes, ' \
|
91
|
+
"received #{nodes.class.name} instead."
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# [TODO][EXPERIMENTAL]
|
96
|
+
#
|
97
|
+
# @param nodes_for_adding [Array | Range | String] nodes
|
98
|
+
def add_nodes_from(nodes_for_adding)
|
99
|
+
case nodes_for_adding
|
100
|
+
when String
|
101
|
+
nodes_for_adding.each_char { |node| add_node(node) }
|
102
|
+
else
|
103
|
+
nodes_for_adding.each { |node| add_node(node) }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def add_path(paths)
|
108
|
+
paths.each_cons(2){|x, y| add_edge(x, y) }
|
109
|
+
end
|
110
|
+
|
111
|
+
# Removes node from the graph
|
112
|
+
#
|
113
|
+
# @example
|
114
|
+
# graph.remove_node("Noida")
|
115
|
+
#
|
116
|
+
# @param node [Object] the node to be removed
|
117
|
+
def remove_node(node)
|
118
|
+
raise KeyError, "Error in deleting node #{node} from Graph." unless @nodes.has_key?(node)
|
119
|
+
|
120
|
+
@adj[node].each_key { |k| @adj[k].delete(node) }
|
121
|
+
@adj.delete(node)
|
122
|
+
@nodes.delete(node)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Removes multiple nodes from the graph
|
126
|
+
#
|
127
|
+
# @example
|
128
|
+
# graph.remove_nodes(["Noida", "Bangalore"])
|
129
|
+
#
|
130
|
+
# @param nodes [Array<Object>] the array of nodes to be removed
|
131
|
+
def remove_nodes(nodes)
|
132
|
+
case nodes
|
133
|
+
when Set, Array
|
134
|
+
nodes.each { |node| remove_node(node) }
|
135
|
+
else
|
136
|
+
raise ArgumentError, 'Expected argument to be an Array or Set of nodes, ' \
|
137
|
+
"received #{nodes.class.name} instead."
|
138
|
+
end
|
139
|
+
end
|
140
|
+
alias remove_nodes_from remove_nodes
|
141
|
+
|
142
|
+
# Removes edge from the graph
|
143
|
+
#
|
144
|
+
# @example
|
145
|
+
# graph.remove_edge('Noida', 'Bangalore')
|
146
|
+
#
|
147
|
+
# @param node1 [Object] the first node of the edge
|
148
|
+
# @param node2 [Object] the second node of the edge
|
149
|
+
def remove_edge(node1, node2)
|
150
|
+
raise KeyError, "#{node1} is not a valid node." unless @nodes.has_key?(node1)
|
151
|
+
raise KeyError, "#{node2} is not a valid node" unless @nodes.has_key?(node2)
|
152
|
+
raise KeyError, 'The given edge is not a valid one.' unless @adj[node1].has_key?(node2)
|
153
|
+
|
154
|
+
@adj[node1].delete(node2)
|
155
|
+
@adj[node2].delete(node1) if node1 != node2
|
156
|
+
end
|
157
|
+
|
158
|
+
# Removes multiple edges from the graph
|
159
|
+
#
|
160
|
+
# @example
|
161
|
+
# graph.remove_edges([%w[Noida Bangalore], %w[Bangalore Chennai]])
|
162
|
+
#
|
163
|
+
# @param edges [Array<Object>] the array of edges to be removed
|
164
|
+
def remove_edges(edges)
|
165
|
+
case edges
|
166
|
+
when Array, Set
|
167
|
+
edges.each { |node1, node2| remove_edge(node1, node2) }
|
168
|
+
else
|
169
|
+
raise ArgumentError, 'Expected Arguement to be Array or Set of edges, ' \
|
170
|
+
"received #{edges.class.name} instead."
|
171
|
+
end
|
172
|
+
end
|
173
|
+
alias remove_edges_from remove_edges
|
174
|
+
|
175
|
+
# Adds weighted edge
|
176
|
+
#
|
177
|
+
# @example
|
178
|
+
# graph.add_weighted_edge('Noida', 'Bangalore', 1000)
|
179
|
+
#
|
180
|
+
# @param node1 [Object] the first node of the edge
|
181
|
+
# @param node2 [Object] the second node of the edge
|
182
|
+
# @param weight [Integer] the weight value
|
183
|
+
def add_weighted_edge(node1, node2, weight)
|
184
|
+
add_edge(node1, node2, weight: weight)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Adds multiple weighted edges
|
188
|
+
#
|
189
|
+
# @example
|
190
|
+
# graph.add_weighted_edges([['Noida', 'Bangalore'],
|
191
|
+
# ['Noida', 'Nagpur']], [1000, 2000])
|
192
|
+
#
|
193
|
+
# @param edges [Array<Object, Object>] the array of edges
|
194
|
+
# @param weights [Array<Integer>] the array of weights
|
195
|
+
def add_weighted_edges(edges, weights)
|
196
|
+
raise ArgumentError, 'edges and weights array must have equal number of elements.' \
|
197
|
+
unless edges.size == weights.size
|
198
|
+
raise ArgumentError, 'edges and weight must be given in an Array.' \
|
199
|
+
unless edges.is_a?(Array) && weights.is_a?(Array)
|
200
|
+
|
201
|
+
(edges.transpose << weights).transpose.each do |node1, node2, weight|
|
202
|
+
add_weighted_edge(node1, node2, weight)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# [TODO][EXPERIMENTAL]
|
207
|
+
#
|
208
|
+
# @param edges [[Object, Object, Integer|Float]] the weight of edge
|
209
|
+
# @param weight [Symbol] weight name key. default key is `:weight``
|
210
|
+
def add_weighted_edges_from(edges, weight: :weight)
|
211
|
+
edges.each do |s, t, w|
|
212
|
+
add_edge(s, t, **{weight => w})
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# [TODO] Current default of `data` is true.
|
217
|
+
# [Alert] Change the default in the futher. Please specify the `data`.
|
218
|
+
#
|
219
|
+
# @param data [bool] true if you want data of each edge
|
220
|
+
#
|
221
|
+
# @return [Hash | Array] if data is true, it returns hash including data.
|
222
|
+
# otherwise, simple nodes array.
|
223
|
+
def nodes(data: true)
|
224
|
+
if data
|
225
|
+
@nodes
|
226
|
+
else
|
227
|
+
@nodes.keys
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def each_node(data: false, &block)
|
232
|
+
return enum_for(:each_node, data: data) unless block_given?
|
233
|
+
|
234
|
+
data ? @nodes.each(&block) : @nodes.each_key(&block)
|
235
|
+
end
|
236
|
+
|
237
|
+
# [TODO][EXPERIMENTAL]
|
238
|
+
#
|
239
|
+
# @param data [bool] true if you want data of each edge
|
240
|
+
#
|
241
|
+
# @return [Array[[Object, Object]]] edges array
|
242
|
+
def edges(data: false)
|
243
|
+
each_edge(data: data).to_a
|
244
|
+
end
|
245
|
+
|
246
|
+
# [TODO][EXPERIMENTAL]
|
247
|
+
#
|
248
|
+
# @param data [bool] true if you want data of each edge
|
249
|
+
def each_edge(data: false)
|
250
|
+
return enum_for(:each_edge, data: data) unless block_given?
|
251
|
+
|
252
|
+
h = {}
|
253
|
+
@adj.each do |v, ws|
|
254
|
+
ws.each do |w, info|
|
255
|
+
next if v > w
|
256
|
+
|
257
|
+
h[[v, w, info]] = true
|
258
|
+
end
|
259
|
+
end
|
260
|
+
if data
|
261
|
+
h.each { |(v, w, info), _true| yield(v, w, info) }
|
262
|
+
else
|
263
|
+
h.each { |(v, w, _info), _true| yield(v, w) }
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# Clears the graph
|
268
|
+
#
|
269
|
+
# @example
|
270
|
+
# graph.clear
|
271
|
+
def clear
|
272
|
+
@adj.clear
|
273
|
+
@nodes.clear
|
274
|
+
@graph.clear
|
275
|
+
end
|
276
|
+
|
277
|
+
# Checks if a node is present in the graph
|
278
|
+
#
|
279
|
+
# @example
|
280
|
+
# graph.node?(node1)
|
281
|
+
#
|
282
|
+
# @param node [Object] the node to be checked
|
283
|
+
def node?(node)
|
284
|
+
@nodes.has_key?(node)
|
285
|
+
end
|
286
|
+
alias has_node? node?
|
287
|
+
|
288
|
+
# Checks if the the edge consisting of two nodes is present in the graph
|
289
|
+
#
|
290
|
+
# @example
|
291
|
+
# graph.edge?(node1, node2)
|
292
|
+
#
|
293
|
+
# @param node1 [Object] the first node of the edge to be checked
|
294
|
+
# @param node2 [Object] the second node of the edge to be checked
|
295
|
+
def edge?(node1, node2)
|
296
|
+
node?(node1) && @adj[node1].has_key?(node2)
|
297
|
+
end
|
298
|
+
alias has_edge? edge?
|
299
|
+
|
300
|
+
# Gets the node data
|
301
|
+
#
|
302
|
+
# @example
|
303
|
+
# graph.get_node_data(node)
|
304
|
+
#
|
305
|
+
# @param node [Object] the node whose data is to be fetched
|
306
|
+
def get_node_data(node)
|
307
|
+
raise ArgumentError, 'No such node exists!' unless node?(node)
|
308
|
+
|
309
|
+
@nodes[node]
|
310
|
+
end
|
311
|
+
|
312
|
+
# Gets the edge data
|
313
|
+
#
|
314
|
+
# @example
|
315
|
+
# graph.get_edge_data(node1, node2)
|
316
|
+
#
|
317
|
+
# @param node1 [Object] the first node of the edge
|
318
|
+
# @param node2 [Object] the second node of the edge
|
319
|
+
def get_edge_data(node1, node2)
|
320
|
+
raise KeyError, 'No such edge exists!' unless node?(node1) && node?(node2)
|
321
|
+
|
322
|
+
@adj[node1][node2]
|
323
|
+
end
|
324
|
+
|
325
|
+
# Retus a hash of neighbours of a node
|
326
|
+
#
|
327
|
+
# @example
|
328
|
+
# graph.neighbours(node)
|
329
|
+
#
|
330
|
+
# @param node [Object] the node whose neighbours are to be fetched
|
331
|
+
def neighbours(node)
|
332
|
+
raise KeyError, 'No such node exists!' unless node?(node)
|
333
|
+
|
334
|
+
@adj[node]
|
335
|
+
end
|
336
|
+
|
337
|
+
# Returns number of nodes
|
338
|
+
#
|
339
|
+
# @example
|
340
|
+
# graph.number_of_nodes
|
341
|
+
def number_of_nodes
|
342
|
+
@nodes.length
|
343
|
+
end
|
344
|
+
|
345
|
+
# Returns number of edges
|
346
|
+
#
|
347
|
+
# @example
|
348
|
+
# graph.number_of_edges
|
349
|
+
def number_of_edges
|
350
|
+
@adj.values.map(&:length).sum / 2
|
351
|
+
end
|
352
|
+
|
353
|
+
# Returns the size of the graph
|
354
|
+
#
|
355
|
+
# @example
|
356
|
+
# graph.size(true)
|
357
|
+
#
|
358
|
+
# @param is_weighted [Bool] if true, method returns sum of weights of all edges
|
359
|
+
# else returns number of edges
|
360
|
+
def size(is_weighted = false)
|
361
|
+
if is_weighted
|
362
|
+
graph_size = 0
|
363
|
+
@adj.each do |_, hash_val|
|
364
|
+
hash_val.each { |_, v| graph_size += v[:weight] if v.has_key?(:weight) }
|
365
|
+
end
|
366
|
+
return graph_size / 2
|
367
|
+
end
|
368
|
+
number_of_edges
|
369
|
+
end
|
370
|
+
|
371
|
+
# Returns subgraph consisting of given array of nodes
|
372
|
+
#
|
373
|
+
# @example
|
374
|
+
# graph.subgraph(%w[Mumbai Nagpur])
|
375
|
+
#
|
376
|
+
# @param nodes [Array<Object>] the nodes to be included in the subgraph
|
377
|
+
def subgraph(nodes)
|
378
|
+
case nodes
|
379
|
+
when Array, Set
|
380
|
+
sub_graph = NetworkX::Graph.new(**@graph)
|
381
|
+
nodes.each do |u, _|
|
382
|
+
raise KeyError, "#{u} does not exist in the current graph!" unless @nodes.has_key?(u)
|
383
|
+
|
384
|
+
sub_graph.add_node(u, **@nodes[u])
|
385
|
+
@adj[u].each do |v, edge_val|
|
386
|
+
sub_graph.add_edge(u, v, **edge_val) if @adj[u].has_key?(v) && nodes.include?(v)
|
387
|
+
end
|
388
|
+
end
|
389
|
+
sub_graph
|
390
|
+
else
|
391
|
+
raise ArgumentError, 'Expected Argument to be Array or Set of nodes, ' \
|
392
|
+
"received #{nodes.class.name} instead."
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
# Returns subgraph conisting of given edges
|
397
|
+
#
|
398
|
+
# @example
|
399
|
+
# graph.edge_subgraph([%w[Nagpur Wardha], %w[Nagpur Mumbai]])
|
400
|
+
#
|
401
|
+
# @param edges [Array<Object, Object>] the edges to be included in the subraph
|
402
|
+
def edge_subgraph(edges)
|
403
|
+
case edges
|
404
|
+
when Array, Set
|
405
|
+
sub_graph = NetworkX::Graph.new(**@graph)
|
406
|
+
edges.each do |u, v|
|
407
|
+
raise KeyError, "Edge between #{u} and #{v} does not exist in the graph!" unless @nodes.has_key?(u) \
|
408
|
+
&& @adj[u].has_key?(v)
|
409
|
+
|
410
|
+
sub_graph.add_node(u, **@nodes[u])
|
411
|
+
sub_graph.add_node(v, **@nodes[v])
|
412
|
+
sub_graph.add_edge(u, v, **@adj[u][v])
|
413
|
+
end
|
414
|
+
sub_graph
|
415
|
+
else
|
416
|
+
raise ArgumentError, 'Expected Argument to be Array or Set of edges, ' \
|
417
|
+
"received #{edges.class.name} instead."
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
# [EXPERIMENTAL]
|
422
|
+
def degree(nodes = nil)
|
423
|
+
if nodes.nil?
|
424
|
+
@adj.transform_values(&:size)
|
425
|
+
else
|
426
|
+
res = {}
|
427
|
+
nodes.each { |node| res[node] = @adj[node].size }
|
428
|
+
res
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
def info
|
433
|
+
info = ''
|
434
|
+
info << "Type: #{self.class}\n"
|
435
|
+
info << "Number of nodes: #{number_of_nodes}\n"
|
436
|
+
info << "Number of edges: #{number_of_edges}\n"
|
437
|
+
info
|
438
|
+
end
|
439
|
+
|
440
|
+
def multigraph?
|
441
|
+
['NetworkX::MultiGraph', 'NetworkX::MultiDiGraph'].include?(self.class.name)
|
442
|
+
end
|
443
|
+
|
444
|
+
def directed?
|
445
|
+
['NetworkX::DiGraph', 'NetworkX::MultiDiGraph'].include?(self.class.name)
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module NetworkX
|
2
|
+
# Computes hits and authority scores for all the graphs
|
3
|
+
#
|
4
|
+
# @param graph [Graph, DiGraph] a graph
|
5
|
+
# @param max_iter [Integer] max iterations to run the hits algorithm
|
6
|
+
# @param tol [Numeric] tolerences to cut off the loop
|
7
|
+
# @param nstart [Array<Numeric>] starting hub values for the nodes
|
8
|
+
#
|
9
|
+
# @return [Array<Numeric, Numeric>] hits and authority scores
|
10
|
+
def self.hits(graph, max_iter = 100, tol = 1e-8, nstart)
|
11
|
+
return [{}, {}] if graph.nodes.empty?
|
12
|
+
|
13
|
+
h = nstart
|
14
|
+
sum = h.values.sum
|
15
|
+
h.each_key { |k| h[k] /= (sum * 1.0) }
|
16
|
+
i = 0
|
17
|
+
a = {}
|
18
|
+
|
19
|
+
loop do
|
20
|
+
hlast = Marshal.load(Marshal.dump(h))
|
21
|
+
h, a = {}, {}
|
22
|
+
hlast.each do |k, _v|
|
23
|
+
h[k] = 0
|
24
|
+
a[k] = 0
|
25
|
+
end
|
26
|
+
h.each_key { |k| graph.adj[k].each { |nbr, attrs| a[k] += hlast[nbr] * (attrs[:weight] || 1) } }
|
27
|
+
h.each_key { |k| graph.adj[k].each { |nbr, attrs| h[k] += a[nbr] * (attrs[:weight] || 1) } }
|
28
|
+
smax = h.values.max
|
29
|
+
h.each_key { |k| h[k] /= smax }
|
30
|
+
smax = a.values.max
|
31
|
+
a.each_key { |k| a[k] /= smax }
|
32
|
+
break if h.keys.map { |k| (h[k] - hlast[k]).abs }.sum < tol
|
33
|
+
raise ArgumentError, 'Power Iteration failed to converge!' if i > max_iter
|
34
|
+
|
35
|
+
i += 1
|
36
|
+
end
|
37
|
+
[h, a]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Computes authority matrix for the graph
|
41
|
+
#
|
42
|
+
# @param graph [Graph, DiGraph] a graph
|
43
|
+
#
|
44
|
+
# @return [Matrix] authority matrix for the graph
|
45
|
+
def self.authority_matrix(graph)
|
46
|
+
matrix, = to_matrix(graph, 0)
|
47
|
+
matrix.transpose * matrix
|
48
|
+
end
|
49
|
+
|
50
|
+
# Computes hub matrix for the graph
|
51
|
+
#
|
52
|
+
# @param graph [Graph, DiGraph] a graph
|
53
|
+
#
|
54
|
+
# @return [Matrix] hub matrix for the graph
|
55
|
+
def self.hub_matrix(graph)
|
56
|
+
matrix, = to_matrix(graph, 0)
|
57
|
+
matrix * matrix.transpose
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module NetworkX
|
2
|
+
# Computes pagerank values for the graph
|
3
|
+
#
|
4
|
+
# @param graph [Graph] a graph
|
5
|
+
# @param init [Array<Numeric>] initial pagerank values for the nodes
|
6
|
+
# @param alpha [Numeric] the alpha value to compute the pagerank
|
7
|
+
# @param eps [Numeric] tolerence to check for convergence
|
8
|
+
# @param max_iter [Integer] max iterations for the pagerank algorithm to run
|
9
|
+
#
|
10
|
+
# @return [Array<Numeric>] pagerank values of the graph
|
11
|
+
def self.pagerank(graph, init = nil, alpha = 0.85, eps = 1e-4, max_iter = 100)
|
12
|
+
dim = graph.nodes.length
|
13
|
+
if init.nil?
|
14
|
+
init = graph.nodes(data: false).to_h{ |i| [i, 1.0 / dim] }
|
15
|
+
else
|
16
|
+
s = init.values.sum.to_f
|
17
|
+
init = init.transform_values { |v| v / s }
|
18
|
+
end
|
19
|
+
raise ArgumentError, 'Init array needs to have same length as number of graph nodes!' \
|
20
|
+
unless dim == init.length
|
21
|
+
|
22
|
+
matrix = []
|
23
|
+
elem_ind = {}
|
24
|
+
p = []
|
25
|
+
curr = init.values
|
26
|
+
init.keys.each_with_index { |n, i| elem_ind[n] = i }
|
27
|
+
graph.adj.each do |_u, u_edges|
|
28
|
+
adj_arr = Array.new(dim, 0)
|
29
|
+
u_edges.each do |v, _|
|
30
|
+
adj_arr[elem_ind[v]] = 1
|
31
|
+
end
|
32
|
+
matrix << adj_arr
|
33
|
+
end
|
34
|
+
(0..(dim - 1)).each do |i|
|
35
|
+
p[i] = []
|
36
|
+
(0..(dim - 1)).each { |j| p[i][j] = matrix[i][j] / (matrix[i].sum * 1.0) }
|
37
|
+
end
|
38
|
+
|
39
|
+
max_iter.times do |_|
|
40
|
+
prev = curr.clone
|
41
|
+
dim.times do |i|
|
42
|
+
ip = 0
|
43
|
+
dim.times { |j| ip += p.transpose[i][j] * prev[j] }
|
44
|
+
curr[i] = (alpha * ip) + ((1 - alpha) / (dim * 1.0))
|
45
|
+
end
|
46
|
+
err = 0
|
47
|
+
dim.times { |i| err += (prev[i] - curr[i]).abs }
|
48
|
+
return curr if err < eps
|
49
|
+
end
|
50
|
+
raise ArgumentError, 'PageRank failed to converge!'
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.pagerank2(graph, alpha: 0.85, personalization: nil, eps: 1e-6, max_iter: 100)
|
54
|
+
n = graph.number_of_nodes
|
55
|
+
|
56
|
+
matrix, index_to_node = NetworkX.to_matrix(graph, 0)
|
57
|
+
|
58
|
+
# index_to_node = {0=>0, 1=>1, 2=>2, 3=>3}
|
59
|
+
|
60
|
+
index_from_node = index_to_node.invert
|
61
|
+
|
62
|
+
probabilities = Array.new(n) do |i|
|
63
|
+
total = matrix.row(i).sum
|
64
|
+
(matrix.row(i) / total.to_f).to_a
|
65
|
+
end
|
66
|
+
|
67
|
+
curr = personalization
|
68
|
+
unless curr
|
69
|
+
curr = Array.new(n)
|
70
|
+
graph.each_node{|node| curr[index_from_node[node]] = 1.0 / n }
|
71
|
+
end
|
72
|
+
|
73
|
+
max_iter.times do
|
74
|
+
prev = curr.clone
|
75
|
+
|
76
|
+
n.times do |i|
|
77
|
+
ip = 0.0
|
78
|
+
n.times do |j|
|
79
|
+
ip += probabilities[j][i] * prev[j]
|
80
|
+
end
|
81
|
+
curr[i] = (alpha * ip) + ((1.0 - alpha) / n * 1.0)
|
82
|
+
end
|
83
|
+
|
84
|
+
err = (0...n).map{|i| (prev[i] - curr[i]).abs }.sum
|
85
|
+
return (0...n).map{|i| [index_to_node[i], curr[i]] }.sort.to_h if err < eps
|
86
|
+
end
|
87
|
+
(0...n).map{|i| [index_to_node[i], curr[i]] }.sort.to_h
|
88
|
+
end
|
89
|
+
end
|