networkx 0.1.0 → 0.2.0
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.
- 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
|