ranker 1.0.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 +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 [](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
|