ranker 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +135 -0
- data/lib/ranker.rb +72 -0
- data/lib/ranker/ranking.rb +44 -0
- data/lib/ranker/rankings.rb +56 -0
- data/lib/ranker/strategies.rb +9 -0
- data/lib/ranker/strategies/dense.rb +20 -0
- data/lib/ranker/strategies/modified_competition.rb +26 -0
- data/lib/ranker/strategies/ordinal.rb +23 -0
- data/lib/ranker/strategies/standard_competition.rb +21 -0
- data/lib/ranker/strategies/strategy.rb +82 -0
- data/lib/ranker/version.rb +3 -0
- data/spec/lib/ranker/ranking_spec.rb +91 -0
- data/spec/lib/ranker/rankings_spec.rb +59 -0
- data/spec/lib/ranker/strategies/dense_spec.rb +100 -0
- data/spec/lib/ranker/strategies/modified_competition_spec.rb +100 -0
- data/spec/lib/ranker/strategies/ordinal_spec.rb +86 -0
- data/spec/lib/ranker/strategies/standard_competition_spec.rb +139 -0
- data/spec/lib/ranker/strategies/strategy_spec.rb +28 -0
- data/spec/ranker_spec.rb +144 -0
- data/spec/spec_helper.rb +3 -0
- metadata +111 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
data.tar.gz: 00ce8440e7681886fd6ae2c93f4db5af94c5a655
|
4
|
+
metadata.gz: 691b75e03742cc6ca452f8fc9a45cec01a1fa9f2
|
5
|
+
SHA512:
|
6
|
+
data.tar.gz: 458a61a409e17288fb97b90dea58417b99426d627f61c9aaa5d9417e88a27944b6137b2dbed40390bd0e0d281c5b6277ca329dc0bdafa5067cd4964ca8d6ee8f
|
7
|
+
metadata.gz: 77454afb9e3c29d72a8e3c223f9a0d47df41d7841e00a933e9c7f3de0adeffa9c882a173c432178d1fc621fe45dae2e37b6cff2f950b8c6339d0a36f173355f2
|
data/README.md
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
Ranker [![Build Status](https://travis-ci.org/quidproquo/ranker.png?branch=master)](http://travis-ci.org/quidproquo/ranker)
|
2
|
+
======
|
3
|
+
|
4
|
+
A Ruby library for ranking scorable types using various ranking strategies.
|
5
|
+
|
6
|
+
Compatibility
|
7
|
+
-------------
|
8
|
+
|
9
|
+
Ranker is tested against MRI (1.8.7+) and JRuby (1.9.0+).
|
10
|
+
|
11
|
+
Installation
|
12
|
+
------------
|
13
|
+
|
14
|
+
With bundler, add the `ranker` gem to your `Gemfile`.
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
gem "ranker", "~> 1.0"
|
18
|
+
```
|
19
|
+
|
20
|
+
Require the `ranker` gem in your application.
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
require "ranker"
|
24
|
+
```
|
25
|
+
|
26
|
+
Usage
|
27
|
+
-----
|
28
|
+
|
29
|
+
### Default Ranking
|
30
|
+
|
31
|
+
Default ranking will assume values are numeric and rank them in descending order.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
scores = [1, 1, 2, 3, 3, 1, 4, 4, 5, 6, 8, 1, 0]
|
35
|
+
rankings = Ranker.rank(scores)
|
36
|
+
rankings.count #=> 8
|
37
|
+
ranking_1 = rankings[0]
|
38
|
+
ranking_1.rank #=> 1
|
39
|
+
ranking_1.score #=> 8
|
40
|
+
```
|
41
|
+
|
42
|
+
### Custom Ranking
|
43
|
+
|
44
|
+
Custom ranking allows for ranking of objects by using a symbol or a lambda.
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
class Player
|
48
|
+
attr_accesor :score
|
49
|
+
|
50
|
+
def initalize(score)
|
51
|
+
@score = score
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
players = [Player.new(0), Player.new(100), Player.new(1000), Player.new(25)]
|
56
|
+
rankings = Ranker.rank(players, :by => lambda { |player| player.score })
|
57
|
+
# or
|
58
|
+
rankings = Ranker.rank(players, :by => :score)
|
59
|
+
```
|
60
|
+
|
61
|
+
In some cases objects need to be ranked by score in ascending order, for example, if you were ranking golf players.
|
62
|
+
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
class GolfPlayer < Player
|
66
|
+
end
|
67
|
+
|
68
|
+
players = [GolfPlayer.new(72), GolfPlayer.new(100), GolfPlayer.new(138), GolfPlayer.new(54)]
|
69
|
+
rankings = Ranker.rank(players, :by => :score, :desc => false)
|
70
|
+
```
|
71
|
+
|
72
|
+
### Ranking Strategies
|
73
|
+
|
74
|
+
Ranker has a number of ranking strategies available to use, mostly based on the Wikipedia entry on [ranking](http://en.wikipedia.org/wiki/Ranking). Strategies can be passed in as an option to the rank method.
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
rankings = Ranker.rank(players, :by => :score, :strategy => :ordinal)
|
78
|
+
```
|
79
|
+
|
80
|
+
#### Standard Competition Ranking ("1224" ranking)
|
81
|
+
|
82
|
+
This is the default ranking strategy. For more info, see the Wikipedia entry on [Standard Competition Ranking](http://en.wikipedia.org/wiki/Ranking#Standard_competition_ranking_.28.221224.22_ranking.29).
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
rankings = Ranker.rank(players, :by => :score, :strategy => :standard_competition)
|
86
|
+
```
|
87
|
+
|
88
|
+
#### Modified Competition Ranking ("1334" ranking)
|
89
|
+
|
90
|
+
For more info, see the Wikipedia entry on [Modified Competition Ranking](http://en.wikipedia.org/wiki/Ranking#Modified_competition_ranking_.28.221334.22_ranking.29).
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
rankings = Ranker.rank(players, :by => :score, :strategy => :modified_competition)
|
94
|
+
```
|
95
|
+
|
96
|
+
#### Dense Ranking ("1223" ranking)
|
97
|
+
|
98
|
+
For more info, see the Wikipedia entry on [Dense Ranking](http://en.wikipedia.org/wiki/Ranking#Dense_ranking_.28.221223.22_ranking.29).
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
rankings = Ranker.rank(players, :by => :score, :strategy => :dense)
|
102
|
+
```
|
103
|
+
|
104
|
+
#### Ordinal Ranking ("1234" ranking)
|
105
|
+
|
106
|
+
For more info, see the Wikipedia entry on [Ordinal Ranking](http://en.wikipedia.org/wiki/Ranking#Ordinal_ranking_.28.221234.22_ranking.29).
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
rankings = Ranker.rank(players, :by => :score, :strategy => :ordinal)
|
110
|
+
```
|
111
|
+
|
112
|
+
#### Custom Ranking
|
113
|
+
|
114
|
+
If you find the current strategies not to your liking, you can write your own and pass the class into the rank method.
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
class MyCustomStrategy < Ranker::Strategies::Strategy
|
118
|
+
|
119
|
+
def execute
|
120
|
+
# My code here
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
rankings = Ranker.rank(players, :by => :score, :strategy => MyCustomStrategy)
|
126
|
+
```
|
127
|
+
|
128
|
+
|
129
|
+
Copyright
|
130
|
+
---------
|
131
|
+
|
132
|
+
Copyright © 2013 Ilya Scharrenbroich. Released under the MIT License.
|
133
|
+
|
134
|
+
|
135
|
+
|
data/lib/ranker.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'ranker/ranking'
|
2
|
+
require 'ranker/rankings'
|
3
|
+
require 'ranker/strategies'
|
4
|
+
require 'ranker/version'
|
5
|
+
|
6
|
+
##
|
7
|
+
# Ranks are based on: http://en.wikipedia.org/wiki/Ranking
|
8
|
+
#
|
9
|
+
module Ranker
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
# Properties:
|
14
|
+
|
15
|
+
def strategies
|
16
|
+
@strategies ||= {
|
17
|
+
:standard_competition => Ranker::Strategies::StandardCompetition,
|
18
|
+
:modified_competition => Ranker::Strategies::ModifiedCompetition,
|
19
|
+
:dense => Ranker::Strategies::Dense,
|
20
|
+
:ordinal => Ranker::Strategies::Ordinal
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
# Methods:
|
25
|
+
|
26
|
+
def rank(rankables, *args)
|
27
|
+
options = args.pop
|
28
|
+
if options && options.kind_of?(Hash)
|
29
|
+
options = default_options.merge(options)
|
30
|
+
else
|
31
|
+
options = default_options
|
32
|
+
end
|
33
|
+
strategy = get_strategy(rankables, options)
|
34
|
+
strategy.rank
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
# Properties:
|
41
|
+
|
42
|
+
def default_options
|
43
|
+
{
|
44
|
+
:by => lambda { |rankable| rankable },
|
45
|
+
:desc => true,
|
46
|
+
:strategy => :standard_competition
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
# Methods:
|
51
|
+
|
52
|
+
def get_strategy(rankables, options)
|
53
|
+
strategy_class = get_strategy_class(options[:strategy])
|
54
|
+
strategy_class.new(rankables, options)
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_strategy_class(strategy)
|
58
|
+
if strategy.kind_of?(Class)
|
59
|
+
strategy
|
60
|
+
else
|
61
|
+
if strategy = strategies[strategy]
|
62
|
+
strategy
|
63
|
+
else
|
64
|
+
raise ArgumentError.new("Unknown strategy: #{strategy}")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end # class methods
|
70
|
+
|
71
|
+
end
|
72
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Ranker
|
2
|
+
|
3
|
+
class Ranking
|
4
|
+
|
5
|
+
attr_reader :rankings, :index, :rank, :score, :rankables
|
6
|
+
|
7
|
+
def initialize(rankings, index, rank, score, rankables)
|
8
|
+
@rankings = rankings
|
9
|
+
@index = index
|
10
|
+
@rank = rank
|
11
|
+
@score = score
|
12
|
+
@rankables = rankables
|
13
|
+
end
|
14
|
+
|
15
|
+
# Properties:
|
16
|
+
|
17
|
+
def num_rankables
|
18
|
+
rankables.count
|
19
|
+
end
|
20
|
+
|
21
|
+
def percentile
|
22
|
+
@percentile ||= (num_scores_at_or_below.to_f / rankings.num_scores) * 100
|
23
|
+
end
|
24
|
+
|
25
|
+
def z_score
|
26
|
+
@z_score ||= if rankings.standard_deviation == 0
|
27
|
+
0
|
28
|
+
else
|
29
|
+
(score - rankings.mean) / rankings.standard_deviation
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def num_scores_at_or_below
|
36
|
+
@scores_at_or_below ||= rankings[index..rankings.num_scores].reduce(0) { |sum, ranking|
|
37
|
+
sum + ranking.num_rankables
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
end # class
|
42
|
+
|
43
|
+
end # module
|
44
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Ranker
|
2
|
+
|
3
|
+
class Rankings < Array
|
4
|
+
|
5
|
+
attr_reader :strategy, :scores
|
6
|
+
|
7
|
+
def initialize(strategy)
|
8
|
+
@strategy = strategy
|
9
|
+
@scores = []
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
# Properties:
|
14
|
+
|
15
|
+
def mean
|
16
|
+
@mean ||= total.to_f / num_scores
|
17
|
+
end
|
18
|
+
|
19
|
+
def standard_deviation
|
20
|
+
@standard_deviation ||= if variance.nan?
|
21
|
+
# For ruby-1.8.7 compatibility
|
22
|
+
variance
|
23
|
+
else
|
24
|
+
Math.sqrt(variance)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def num_scores
|
29
|
+
scores.size
|
30
|
+
end
|
31
|
+
|
32
|
+
def variance
|
33
|
+
@variance ||= total_difference.to_f / num_scores
|
34
|
+
end
|
35
|
+
|
36
|
+
def total
|
37
|
+
@total ||= scores.reduce(:+)
|
38
|
+
end
|
39
|
+
|
40
|
+
def total_difference
|
41
|
+
@total_difference ||= scores.reduce(0) { |sum, score|
|
42
|
+
sum + (score - mean) ** 2
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
# Methods:
|
48
|
+
|
49
|
+
def create(rank, score, rankables)
|
50
|
+
scores.concat(Array.new(rankables.count, score))
|
51
|
+
self << Ranking.new(self, self.count, rank, score, rankables)
|
52
|
+
end
|
53
|
+
|
54
|
+
end # class
|
55
|
+
|
56
|
+
end # module
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'ranker/strategies/strategy.rb'
|
2
|
+
require 'ranker/strategies/dense.rb'
|
3
|
+
require 'ranker/strategies/modified_competition.rb'
|
4
|
+
require 'ranker/strategies/standard_competition.rb'
|
5
|
+
require 'ranker/strategies/ordinal.rb'
|
6
|
+
|
7
|
+
module Ranker::Strategies
|
8
|
+
|
9
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Ranker::Strategies
|
2
|
+
|
3
|
+
##
|
4
|
+
# Ranks rankables according to: http://en.wikipedia.org/wiki/Ranking#Dense_ranking_.28.221223.22_ranking.29
|
5
|
+
#
|
6
|
+
class Dense < Strategy
|
7
|
+
|
8
|
+
# Methods:
|
9
|
+
|
10
|
+
def execute
|
11
|
+
scores_unique_sorted.each_with_index { |score, index|
|
12
|
+
rank = index + 1
|
13
|
+
rankables_for_score = rankables_for_score(score)
|
14
|
+
create_ranking(rank, score, rankables_for_score)
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
end # class
|
19
|
+
|
20
|
+
end # module
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Ranker::Strategies
|
2
|
+
|
3
|
+
##
|
4
|
+
# Ranks rankables according to: http://en.wikipedia.org/wiki/Ranking#Modified_competition_ranking_.28.221334.22_ranking.29
|
5
|
+
#
|
6
|
+
class ModifiedCompetition < Strategy
|
7
|
+
|
8
|
+
# Methods:
|
9
|
+
|
10
|
+
def execute
|
11
|
+
rank = 0
|
12
|
+
scores_unique_sorted.each_with_index { |score, index|
|
13
|
+
rankables_for_score = rankables_for_score(score)
|
14
|
+
if rank == 0
|
15
|
+
create_ranking(1, score, rankables_for_score)
|
16
|
+
rank += rankables_for_score.count
|
17
|
+
else
|
18
|
+
rank += rankables_for_score.count
|
19
|
+
create_ranking(rank, score, rankables_for_score)
|
20
|
+
end
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
end # class
|
25
|
+
|
26
|
+
end # module
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Ranker::Strategies
|
2
|
+
|
3
|
+
##
|
4
|
+
# Ranks rankables according to: http://en.wikipedia.org/wiki/Ranking#Ordinal_ranking_.28.221234.22_ranking.29
|
5
|
+
#
|
6
|
+
class Ordinal < Strategy
|
7
|
+
|
8
|
+
# Methods:
|
9
|
+
|
10
|
+
def execute
|
11
|
+
rank = 1
|
12
|
+
scores_unique_sorted.each_with_index { |score, index|
|
13
|
+
rankables_for_score = rankables_for_score(score)
|
14
|
+
rankables_for_score.each { |value|
|
15
|
+
create_ranking(rank, score, [value])
|
16
|
+
rank += 1
|
17
|
+
}
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
end # class
|
22
|
+
|
23
|
+
end # module
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Ranker::Strategies
|
2
|
+
|
3
|
+
##
|
4
|
+
# Ranks rankables according to: http://en.wikipedia.org/wiki/Ranking#Standard_competition_ranking_.28.221224.22_ranking.29
|
5
|
+
#
|
6
|
+
class StandardCompetition < Strategy
|
7
|
+
|
8
|
+
# Methods:
|
9
|
+
|
10
|
+
def execute
|
11
|
+
rank = 1
|
12
|
+
scores_unique_sorted.each_with_index { |score, index|
|
13
|
+
rankables_for_score = rankables_for_score(score)
|
14
|
+
create_ranking(rank, score, rankables_for_score)
|
15
|
+
rank += rankables_for_score.count
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
end # class
|
20
|
+
|
21
|
+
end # module
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Ranker::Strategies
|
2
|
+
|
3
|
+
class Strategy
|
4
|
+
|
5
|
+
attr_reader :rankables, :options
|
6
|
+
|
7
|
+
def initialize(rankables, *args)
|
8
|
+
@rankables = rankables
|
9
|
+
options = args.pop
|
10
|
+
if options && options.kind_of?(Hash)
|
11
|
+
@options = default_options.merge(options)
|
12
|
+
else
|
13
|
+
@options = default_options
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
# Properties:
|
19
|
+
|
20
|
+
def rankings
|
21
|
+
@rankings ||= Ranker::Rankings.new(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
# Methods:
|
26
|
+
|
27
|
+
|
28
|
+
def rank
|
29
|
+
execute
|
30
|
+
rankings
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
# Properties:
|
37
|
+
|
38
|
+
def default_options
|
39
|
+
{
|
40
|
+
:by => lambda { |rankable| rankable },
|
41
|
+
:desc => true
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def score
|
46
|
+
options[:by]
|
47
|
+
end
|
48
|
+
|
49
|
+
def sort_desc?
|
50
|
+
options[:desc]
|
51
|
+
end
|
52
|
+
|
53
|
+
def rankables_grouped_by_score
|
54
|
+
@rankables_grouped_by_score ||= rankables.group_by(&score)
|
55
|
+
end
|
56
|
+
|
57
|
+
def scores_unique_sorted
|
58
|
+
@scores_unique_sorted ||= unless sort_desc?
|
59
|
+
rankables_grouped_by_score.keys.sort!
|
60
|
+
else
|
61
|
+
rankables_grouped_by_score.keys.sort!.reverse!
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
# Methods:
|
67
|
+
|
68
|
+
def create_ranking(rank, score, rankables)
|
69
|
+
rankings.create(rank, score, rankables)
|
70
|
+
end
|
71
|
+
|
72
|
+
def execute
|
73
|
+
raise NotImplementedError.new('You must implement the execute method.')
|
74
|
+
end
|
75
|
+
|
76
|
+
def rankables_for_score(score)
|
77
|
+
rankables_grouped_by_score[score]
|
78
|
+
end
|
79
|
+
|
80
|
+
end # class
|
81
|
+
|
82
|
+
end # module
|