weighted_list_rank 0.5.2 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c194a4b55fa9efecf1ad39fd54b8d22c2a9fd6766cb562c265c1f881deb08c4
4
- data.tar.gz: 50dba99dde929b5bd3feed2709877c48d87bc45f6f9fcd745fd96d1b14749d7d
3
+ metadata.gz: 34f4b87da4046f63fd0771a985c63fab24e30dc51445ce9ceabcce4bde5b3f45
4
+ data.tar.gz: d857a6cb9a7e9114d2157a05a373876f08c52661130cf50126fa6258b4beaa5b
5
5
  SHA512:
6
- metadata.gz: fafab0c49212017e8730cec2b78101e6adea7a85eedc938a0250d76643af7e4dea7fb6d1c49c8ed035675774bc4bf18f2e94eca46546ff8627a02b10d43fb1a7
7
- data.tar.gz: 15e032d6eb71f6b65a825847e6850a8d4e7b248853d4a4480ab8f4ef7c276998dfd1ac0a3edea8f0a0fd19f788c571d84acac1e3d4fa1e8a1089aadd2e9cbab1
6
+ metadata.gz: ea9c40e27791fafe638694e34a440c4df6e8e8c20770b2f9b1da6ca8e8d37d755ae5eaaeeb89fe2ca43b569805b9fde43c1b2616eb0a135bbdea37b02ae5bf4a
7
+ data.tar.gz: 7ad51b9288339b0728841a30d91ee9b0b5824f101801a090e7533a93f090a40b8ef0ea8143fea32c533fc333e0a96037ee3cba684efa9eaa8dfc9388edc20889
data/CHANGELOG.md CHANGED
@@ -1,4 +1,14 @@
1
- ## [Unreleased]
1
+ ## [0.6.0] - 2025-06-29
2
+ - Added `list_count_penalties` feature to `RankingContext` to allow penalizing items based on the number of lists they appear on. This is useful for de-emphasizing items that are not widely represented across your data.
3
+ - The feature accepts a hash mapping list counts to penalty percentages (e.g., `{1 => 0.50, 2 => 0.25}` to penalize items on 1 list by 50% and items on 2 lists by 25%).
4
+ - Penalties are applied to the total score after all list scores are aggregated but before sorting.
5
+ - This feature is fully backwards compatible and stacks with individual item penalties.
6
+ - Updated all dependencies to their latest versions.
7
+
8
+ ## [0.5.3] - 2024-08-22
9
+ - Fixed an issue in the `Exponential` strategy where items with positions higher than the total number of items in a list could cause errors. Now, such items are treated as if they were in the last position, and a warning is logged.
10
+ - Added a new test case to verify the handling of items with positions exceeding the list size.
11
+ - Updated all dependencies to their latest versions.
2
12
 
3
13
  ## [0.5.2] - 2024-08-21
4
14
  - Fixed an issue in the `Exponential` strategy where `score_penalty` was not being applied to unranked items. Now, `score_penalty` is correctly applied to both ranked and unranked items, ensuring consistent scoring behavior.
data/Gemfile.lock CHANGED
@@ -1,64 +1,69 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- weighted_list_rank (0.4.2)
4
+ weighted_list_rank (0.5.3)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- ast (2.4.2)
10
- json (2.7.2)
11
- language_server-protocol (3.17.0.3)
9
+ ast (2.4.3)
10
+ json (2.12.2)
11
+ language_server-protocol (3.17.0.5)
12
12
  lint_roller (1.1.0)
13
- minitest (5.25.1)
14
- parallel (1.26.3)
15
- parser (3.3.4.2)
13
+ minitest (5.25.5)
14
+ parallel (1.27.0)
15
+ parser (3.3.8.0)
16
16
  ast (~> 2.4.1)
17
17
  racc
18
+ prism (1.4.0)
18
19
  racc (1.8.1)
19
20
  rainbow (3.1.1)
20
- rake (13.2.1)
21
- regexp_parser (2.9.2)
22
- rexml (3.3.5)
23
- strscan
24
- rubocop (1.65.1)
21
+ rake (13.3.0)
22
+ regexp_parser (2.10.0)
23
+ rubocop (1.75.8)
25
24
  json (~> 2.3)
26
- language_server-protocol (>= 3.17.0)
25
+ language_server-protocol (~> 3.17.0.2)
26
+ lint_roller (~> 1.1.0)
27
27
  parallel (~> 1.10)
28
28
  parser (>= 3.3.0.2)
29
29
  rainbow (>= 2.2.2, < 4.0)
30
- regexp_parser (>= 2.4, < 3.0)
31
- rexml (>= 3.2.5, < 4.0)
32
- rubocop-ast (>= 1.31.1, < 2.0)
30
+ regexp_parser (>= 2.9.3, < 3.0)
31
+ rubocop-ast (>= 1.44.0, < 2.0)
33
32
  ruby-progressbar (~> 1.7)
34
- unicode-display_width (>= 2.4.0, < 3.0)
35
- rubocop-ast (1.32.1)
36
- parser (>= 3.3.1.0)
37
- rubocop-minitest (0.35.1)
38
- rubocop (>= 1.61, < 2.0)
39
- rubocop-ast (>= 1.31.1, < 2.0)
40
- rubocop-performance (1.21.1)
41
- rubocop (>= 1.48.1, < 2.0)
42
- rubocop-ast (>= 1.31.1, < 2.0)
43
- rubocop-rake (0.6.0)
44
- rubocop (~> 1.0)
33
+ unicode-display_width (>= 2.4.0, < 4.0)
34
+ rubocop-ast (1.45.1)
35
+ parser (>= 3.3.7.2)
36
+ prism (~> 1.4)
37
+ rubocop-minitest (0.38.1)
38
+ lint_roller (~> 1.1)
39
+ rubocop (>= 1.75.0, < 2.0)
40
+ rubocop-ast (>= 1.38.0, < 2.0)
41
+ rubocop-performance (1.25.0)
42
+ lint_roller (~> 1.1)
43
+ rubocop (>= 1.75.0, < 2.0)
44
+ rubocop-ast (>= 1.38.0, < 2.0)
45
+ rubocop-rake (0.7.1)
46
+ lint_roller (~> 1.1)
47
+ rubocop (>= 1.72.1)
45
48
  ruby-progressbar (1.13.0)
46
- standard (1.40.0)
49
+ standard (1.50.0)
47
50
  language_server-protocol (~> 3.17.0.2)
48
51
  lint_roller (~> 1.0)
49
- rubocop (~> 1.65.0)
52
+ rubocop (~> 1.75.5)
50
53
  standard-custom (~> 1.0.0)
51
- standard-performance (~> 1.4)
54
+ standard-performance (~> 1.8)
52
55
  standard-custom (1.0.2)
53
56
  lint_roller (~> 1.0)
54
57
  rubocop (~> 1.50)
55
- standard-performance (1.4.0)
58
+ standard-performance (1.8.0)
56
59
  lint_roller (~> 1.1)
57
- rubocop-performance (~> 1.21.0)
58
- strscan (3.1.0)
59
- unicode-display_width (2.5.0)
60
+ rubocop-performance (~> 1.25.0)
61
+ unicode-display_width (3.1.4)
62
+ unicode-emoji (~> 4.0, >= 4.0.4)
63
+ unicode-emoji (4.0.4)
60
64
 
61
65
  PLATFORMS
66
+ arm64-darwin-24
62
67
  x86_64-linux
63
68
 
64
69
  DEPENDENCIES
@@ -67,7 +72,7 @@ DEPENDENCIES
67
72
  rubocop (~> 1.21)
68
73
  rubocop-minitest (~> 0.3)
69
74
  rubocop-rake (~> 0.6)
70
- standard (~> 1.0)
75
+ standard (>= 1.35.1)
71
76
  weighted_list_rank!
72
77
 
73
78
  BUNDLED WITH
data/README.md CHANGED
@@ -204,6 +204,31 @@ ranked_items.each do |item|
204
204
  end
205
205
  ```
206
206
 
207
+ ### Penalizing Items by Number of Lists
208
+ You can optionally penalize items that appear on only a small number of lists. This is useful for de-emphasizing items that are not widely represented across your data. To use this feature, pass a `list_count_penalties` hash to the `RankingContext` constructor, where the keys are the number of lists and the values are the penalty percentages (as a float between 0 and 1).
209
+
210
+ For example, to penalize items that appear on only 1 list by 50%, and items on 2 lists by 25%:
211
+
212
+ ```ruby
213
+ list_count_penalties = {1 => 0.50, 2 => 0.25}
214
+ ranking_context = WeightedListRank::RankingContext.new(
215
+ WeightedListRank::Strategies::Exponential.new,
216
+ list_count_penalties: list_count_penalties
217
+ )
218
+
219
+ ranked_items = ranking_context.rank([list1, list2, list3])
220
+ ```
221
+
222
+ - Items that appear on only 1 list will have their total score multiplied by 0.5 (50% penalty).
223
+ - Items that appear on only 2 lists will have their total score multiplied by 0.75 (25% penalty).
224
+ - Items that appear on more lists will not be penalized unless specified in the hash.
225
+
226
+ #### How it works
227
+ - The penalty is applied to the total score of each item after all list scores are aggregated, but before sorting.
228
+ - If an item appears on a number of lists not present in the hash, no penalty is applied.
229
+ - This feature is fully backwards compatible: if you do not provide `list_count_penalties`, no penalties are applied.
230
+ - This penalty stacks with individual item penalties (e.g., `score_penalty` on an item).
231
+
207
232
  ## Development
208
233
 
209
234
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -3,12 +3,15 @@ module WeightedListRank
3
3
  # It aggregates scores for each item across all lists, based on the provided strategy.
4
4
  class RankingContext
5
5
  # @strategy: The strategy used for calculating scores.
6
- attr_reader :strategy
6
+ # @list_count_penalties: Hash mapping list counts to penalty percentages (e.g., {1 => 0.50, 2 => 0.25})
7
+ attr_reader :strategy, :list_count_penalties
7
8
 
8
- # Initializes a new RankingContext with an optional ranking strategy.
9
+ # Initializes a new RankingContext with an optional ranking strategy and list count penalties.
9
10
  # @param strategy [Strategy] the strategy to use for ranking items, defaults to Strategies::Exponential.
10
- def initialize(strategy = Strategies::Exponential.new)
11
+ # @param list_count_penalties [Hash] hash mapping list counts to penalty percentages, defaults to empty hash.
12
+ def initialize(strategy = Strategies::Exponential.new, list_count_penalties: {})
11
13
  @strategy = strategy
14
+ @list_count_penalties = list_count_penalties
12
15
  end
13
16
 
14
17
  # Ranks items across multiple lists according to the strategy's score calculation.
@@ -30,8 +33,8 @@ module WeightedListRank
30
33
  end
31
34
  end
32
35
 
33
- # Convert hash to a sorted array
34
- sorted_items = items.map do |id, details|
36
+ # Convert hash to a formatted array
37
+ formatted_items = items.map do |id, details|
35
38
  {
36
39
  id: id,
37
40
  # Sort the score_details array by score in descending order before including it
@@ -40,8 +43,18 @@ module WeightedListRank
40
43
  }
41
44
  end
42
45
 
46
+ # Apply list count penalties if configured
47
+ if !list_count_penalties.empty?
48
+ formatted_items.each do |item|
49
+ list_count = item[:score_details].length
50
+ if (penalty = list_count_penalties[list_count])
51
+ item[:total_score] *= (1 - penalty)
52
+ end
53
+ end
54
+ end
55
+
43
56
  # Sort the array by total_score in descending order
44
- sorted_items.sort_by { |item| -item[:total_score] }
57
+ formatted_items.sort_by { |item| -item[:total_score] }
45
58
  end
46
59
  end
47
60
  end
@@ -64,8 +64,16 @@ module WeightedListRank
64
64
  score += unranked_bonus
65
65
  end
66
66
  else
67
+ # Check if the item's position is higher than the total number of items
68
+ if item.position > total_items
69
+ puts "Warning: Item position (#{item.position}) is higher than the total number of items (#{total_items}) in the list. Using total items as position."
70
+ item_position = total_items
71
+ else
72
+ item_position = item.position
73
+ end
74
+
67
75
  # Ranked items receive a bonus calculated using the exponential formula
68
- exponential_factor = (total_items + 1 - item.position)**exponent
76
+ exponential_factor = (total_items + 1 - item_position)**exponent
69
77
  total_exponential_factor = (1..total_items).sum { |pos| (total_items + 1 - pos)**exponent }
70
78
 
71
79
  # Allocate a portion of the adjusted bonus pool based on the item's exponential factor
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WeightedListRank
4
- VERSION = "0.5.2"
4
+ VERSION = "0.6.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: weighted_list_rank
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shane Sherman
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-08-21 00:00:00.000000000 Z
10
+ date: 2025-06-29 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rubocop
@@ -28,16 +27,16 @@ dependencies:
28
27
  name: standard
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
- - - "~>"
30
+ - - ">="
32
31
  - !ruby/object:Gem::Version
33
- version: '1.0'
32
+ version: 1.35.1
34
33
  type: :development
35
34
  prerelease: false
36
35
  version_requirements: !ruby/object:Gem::Requirement
37
36
  requirements:
38
- - - "~>"
37
+ - - ">="
39
38
  - !ruby/object:Gem::Version
40
- version: '1.0'
39
+ version: 1.35.1
41
40
  - !ruby/object:Gem::Dependency
42
41
  name: rubocop-minitest
43
42
  requirement: !ruby/object:Gem::Requirement
@@ -89,7 +88,6 @@ files:
89
88
  - lib/weighted_list_rank/strategy.rb
90
89
  - lib/weighted_list_rank/version.rb
91
90
  - sig/weighted_list_rank.rbs
92
- - weighted_list_rank.gemspec
93
91
  homepage: https://github.com/ssherman/weighted_list_rank
94
92
  licenses:
95
93
  - MIT
@@ -97,7 +95,6 @@ metadata:
97
95
  homepage_uri: https://github.com/ssherman/weighted_list_rank
98
96
  source_code_uri: https://github.com/ssherman/weighted_list_rank
99
97
  changelog_uri: https://github.com/ssherman/weighted_list_rank/CHANGELOG.md
100
- post_install_message:
101
98
  rdoc_options: []
102
99
  require_paths:
103
100
  - lib
@@ -112,8 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
109
  - !ruby/object:Gem::Version
113
110
  version: '0'
114
111
  requirements: []
115
- rubygems_version: 3.5.11
116
- signing_key:
112
+ rubygems_version: 3.6.2
117
113
  specification_version: 4
118
114
  summary: generate ranks of items from weighted lists
119
115
  test_files: []
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/weighted_list_rank/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "weighted_list_rank"
7
- spec.version = WeightedListRank::VERSION
8
- spec.authors = ["Shane Sherman"]
9
- spec.email = ["shane.sherman@gmail.com"]
10
-
11
- spec.summary = "generate ranks of items from weighted lists"
12
- spec.description = "generate ranks of items from weighted lists"
13
- spec.homepage = "https://github.com/ssherman/weighted_list_rank"
14
- spec.license = "MIT"
15
- spec.required_ruby_version = ">= 2.6.0"
16
-
17
- # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
18
-
19
- spec.metadata["homepage_uri"] = spec.homepage
20
- spec.metadata["source_code_uri"] = "https://github.com/ssherman/weighted_list_rank"
21
- spec.metadata["changelog_uri"] = "https://github.com/ssherman/weighted_list_rank/CHANGELOG.md"
22
-
23
- # Specify which files should be added to the gem when it is released.
24
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
- spec.files = Dir.chdir(__dir__) do
26
- `git ls-files -z`.split("\x0").reject do |f|
27
- (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
28
- end
29
- end
30
- spec.bindir = "exe"
31
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
- spec.require_paths = ["lib"]
33
-
34
- # Uncomment to register a new dependency of your gem
35
- # spec.add_dependency "example-gem", "~> 1.0"
36
- spec.add_development_dependency "rubocop", "~> 1.0"
37
- spec.add_development_dependency "standard", "~> 1.0"
38
- spec.add_development_dependency "rubocop-minitest", "~> 0.3"
39
- spec.add_development_dependency "rubocop-rake", "~> 0.6"
40
- # For more information and examples about making a new gem, check out our
41
- # guide at: https://bundler.io/guides/creating_gem.html
42
- end