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
         |