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 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 &copy; 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