forget_table 0.0.1
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/lib/forget_table/decay.rb +44 -0
- data/lib/forget_table/decrementer.rb +54 -0
- data/lib/forget_table/distribution.rb +104 -0
- data/lib/forget_table/distribution_decrementer.rb +32 -0
- data/lib/forget_table/distribution_keys.rb +18 -0
- data/lib/forget_table/poisson.rb +40 -0
- data/lib/forget_table/weighted_distribution.rb +23 -0
- data/lib/forget_table.rb +9 -0
- data/spec/forget_table/decay_spec.rb +37 -0
- data/spec/forget_table/decrementer_spec.rb +70 -0
- data/spec/forget_table/distribution_decrementer_spec.rb +50 -0
- data/spec/forget_table/distribution_spec.rb +89 -0
- data/spec/forget_table/poisson_spec.rb +29 -0
- data/spec/forget_table/weighted_distribution_spec.rb +28 -0
- data/spec/integration/integration_spec.rb +81 -0
- data/spec/spec_helper.rb +3 -0
- metadata +117 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 51b34b5b1d37ac5d704a4d7e48c6b635fb8de7b0
|
4
|
+
data.tar.gz: 276ca5952d1e6bdcca27c1c257c8b1fb4b3054d7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d28a09e53676e1b908a8bdcefded130c241b71bdebf5610c48d0e2b25c39d063293f1efc1c82483c8c7552630efc3def2844a103fa98ed0833bb924ad9b03129
|
7
|
+
data.tar.gz: 8abb4e1332f2aeac8f2423d09b47fdfd081ae8d86c853915b503662b9d2d9687e3da7865d753faf46dbe89304784dc0a9c1797aad5f7973a901f6058ce54c5cf
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module ForgetTable
|
2
|
+
|
3
|
+
class Decay
|
4
|
+
|
5
|
+
DEFAULT_DECAY_RATE = 0.05
|
6
|
+
|
7
|
+
# - last_updated: timestamp of the last update
|
8
|
+
# - rate: the rate of the decay (optional)
|
9
|
+
def initialize(last_updated, rate = DEFAULT_DECAY_RATE)
|
10
|
+
@last_updated = last_updated
|
11
|
+
@rate = rate
|
12
|
+
end
|
13
|
+
|
14
|
+
def decay_value(value)
|
15
|
+
decayed_value = value - decay_for(value)
|
16
|
+
decayed_value > 0 ? decayed_value : 1
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :last_updated, :rate
|
22
|
+
|
23
|
+
def decay_for(value)
|
24
|
+
poisson(decay_factor * value)
|
25
|
+
end
|
26
|
+
|
27
|
+
def decay_factor
|
28
|
+
rate * tau
|
29
|
+
end
|
30
|
+
|
31
|
+
# Time since last update
|
32
|
+
def tau
|
33
|
+
[current_time - last_updated, 1].max
|
34
|
+
end
|
35
|
+
|
36
|
+
def poisson(value)
|
37
|
+
Poisson.new(value).sample
|
38
|
+
end
|
39
|
+
|
40
|
+
def current_time
|
41
|
+
@timestamp ||= Time.now.to_i
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'forget_table/distribution_decrementer'
|
2
|
+
|
3
|
+
module ForgetTable
|
4
|
+
|
5
|
+
class Decrementer
|
6
|
+
|
7
|
+
def initialize(redis, weighted_distribution)
|
8
|
+
@redis = redis
|
9
|
+
@weighted_distribution = weighted_distribution
|
10
|
+
end
|
11
|
+
|
12
|
+
def run!
|
13
|
+
decremented_distribution = distribution_decrementer.decremented_distribution
|
14
|
+
updated_redis(decremented_distribution)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :redis, :weighted_distribution
|
20
|
+
|
21
|
+
# Updates:
|
22
|
+
# 1. the weighted distribution with the new distribution
|
23
|
+
# 2. the total number of hits for the distribution with the new count
|
24
|
+
# 3. the last_updated_at value with the current time
|
25
|
+
|
26
|
+
def updated_redis(distribution)
|
27
|
+
redis.pipelined do
|
28
|
+
redis.zadd(distribution.name, distribution.bins.to_a.map(&:reverse))
|
29
|
+
redis.set(hits_count_key, distribution.hits_count)
|
30
|
+
redis.set(last_updated_at_key, Time.now.to_i)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def distribution_decrementer
|
35
|
+
last_updated_at = Integer(@redis.get(last_updated_at_key))
|
36
|
+
distribution_decrementer = DistributionDecrementer.new(
|
37
|
+
weighted_distribution: weighted_distribution,
|
38
|
+
last_updated_at: last_updated_at
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def distribution_keys
|
43
|
+
DistributionKeys.new(weighted_distribution.name)
|
44
|
+
end
|
45
|
+
|
46
|
+
def hits_count_key
|
47
|
+
distribution_keys.hits_count
|
48
|
+
end
|
49
|
+
|
50
|
+
def last_updated_at_key
|
51
|
+
distribution_keys.last_updated_at
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'forget_table/decrementer'
|
2
|
+
require 'forget_table/weighted_distribution'
|
3
|
+
require 'forget_table/distribution_keys'
|
4
|
+
|
5
|
+
module ForgetTable
|
6
|
+
|
7
|
+
# Represents a categorical distribution composed by weighted bins.
|
8
|
+
#
|
9
|
+
# A distribution is instantiated with the following parameters:
|
10
|
+
# - name: the name of the distribution
|
11
|
+
# - redis: the redis client that will host the distribution
|
12
|
+
#
|
13
|
+
# Example of an instance:
|
14
|
+
# distribution: "guitars"
|
15
|
+
# bins: "fender" => 10, "gibson" => 20, "epi" => "30
|
16
|
+
|
17
|
+
class Distribution
|
18
|
+
attr_reader :name
|
19
|
+
|
20
|
+
def initialize(name:, redis:)
|
21
|
+
@name = name
|
22
|
+
@redis = redis
|
23
|
+
end
|
24
|
+
|
25
|
+
# Increments the bin score by the given amount.
|
26
|
+
# params:
|
27
|
+
# - bin
|
28
|
+
# - amount
|
29
|
+
def increment(bin:, amount: 1)
|
30
|
+
redis.zincrby(name, amount, bin)
|
31
|
+
|
32
|
+
# Increment the total number of hits
|
33
|
+
stored_bins = redis.incrby(hits_count_key, 1)
|
34
|
+
|
35
|
+
if stored_bins == 1
|
36
|
+
# Set the initial timestamp if never set
|
37
|
+
redis.set(last_updated_at_key, Time.now.to_i)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the list of bins in the distribution.
|
42
|
+
# Params:
|
43
|
+
# - number_of_bins
|
44
|
+
# - options
|
45
|
+
def distribution(number_of_bins: -1, with_scores: false)
|
46
|
+
begin
|
47
|
+
decrement!
|
48
|
+
|
49
|
+
stop_bin = (number_of_bins == -1) ? -1 : (number_of_bins - 1)
|
50
|
+
redis.zrevrange(name, 0, stop_bin, with_scores: with_scores)
|
51
|
+
rescue RuntimeError
|
52
|
+
[[]]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns the score for the given bin
|
57
|
+
def score_for_bin(bin)
|
58
|
+
decrement!
|
59
|
+
|
60
|
+
redis.zscore(name, bin)
|
61
|
+
end
|
62
|
+
|
63
|
+
def last_updated
|
64
|
+
redis.get(last_updated_at_key)
|
65
|
+
end
|
66
|
+
|
67
|
+
def hits_count
|
68
|
+
redis.get(hits_count_key).to_i
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
attr_reader :redis
|
73
|
+
|
74
|
+
def hits_count_key
|
75
|
+
distribution_keys.hits_count
|
76
|
+
end
|
77
|
+
|
78
|
+
def last_updated_at_key
|
79
|
+
distribution_keys.last_updated_at
|
80
|
+
end
|
81
|
+
|
82
|
+
def decrement!
|
83
|
+
raise "Cannot find distribution #{name}" unless redis.exists(name)
|
84
|
+
|
85
|
+
decrementer.run!
|
86
|
+
end
|
87
|
+
|
88
|
+
def decrementer
|
89
|
+
Decrementer.new(redis, weighted_distribution)
|
90
|
+
end
|
91
|
+
|
92
|
+
def weighted_distribution
|
93
|
+
bins = Hash[*redis.zrevrange(name, 0, -1, with_scores: true).flatten]
|
94
|
+
WeightedDistribution.new(
|
95
|
+
name: name,
|
96
|
+
bins: bins,
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
def distribution_keys
|
101
|
+
DistributionKeys.new(name)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module ForgetTable
|
2
|
+
|
3
|
+
class DistributionDecrementer
|
4
|
+
|
5
|
+
def initialize(weighted_distribution:, last_updated_at:)
|
6
|
+
@weighted_distribution = weighted_distribution
|
7
|
+
@last_updated_at = last_updated_at
|
8
|
+
end
|
9
|
+
|
10
|
+
def decremented_distribution
|
11
|
+
WeightedDistribution.new(
|
12
|
+
name: weighted_distribution.name,
|
13
|
+
bins: decremented_bins
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :weighted_distribution, :last_updated_at
|
20
|
+
|
21
|
+
def decremented_bins
|
22
|
+
decremented_values = decrement(weighted_distribution.values)
|
23
|
+
bin_names = weighted_distribution.bin_names
|
24
|
+
Hash[*bin_names.zip(decremented_values).flatten]
|
25
|
+
end
|
26
|
+
|
27
|
+
def decrement(values)
|
28
|
+
decay = Decay.new(last_updated_at)
|
29
|
+
values.map { |value| decay.decay_value(value) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ForgetTable
|
2
|
+
|
3
|
+
class DistributionKeys
|
4
|
+
attr_reader :distribution_name
|
5
|
+
|
6
|
+
def initialize(distribution_name)
|
7
|
+
@distribution_name = distribution_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def last_updated_at
|
11
|
+
"#{distribution_name}/last_updated_at"
|
12
|
+
end
|
13
|
+
|
14
|
+
def hits_count
|
15
|
+
"#{distribution_name}/hits_count"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ForgetTable
|
2
|
+
|
3
|
+
class Poisson
|
4
|
+
attr_reader :average
|
5
|
+
|
6
|
+
def initialize(average)
|
7
|
+
raise ArgumentError, "average must be > 0 , #{average} given" if average <= 0
|
8
|
+
@average = average
|
9
|
+
end
|
10
|
+
|
11
|
+
def sample
|
12
|
+
@sample ||= extract_sample
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# Returns an Integer extracted from a Poisson
|
18
|
+
# distribution with average `average`.
|
19
|
+
# Implemented according to the Knuth algorithm.
|
20
|
+
def extract_sample
|
21
|
+
l = Math.exp(-average)
|
22
|
+
k = 0
|
23
|
+
p = 1
|
24
|
+
while p > l do
|
25
|
+
k += 1
|
26
|
+
p *= random_in_0_1
|
27
|
+
end
|
28
|
+
k - 1
|
29
|
+
end
|
30
|
+
|
31
|
+
def random_in_0_1
|
32
|
+
random.rand(1.0)
|
33
|
+
end
|
34
|
+
|
35
|
+
def random
|
36
|
+
@@rand ||= Random.new
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ForgetTable
|
2
|
+
|
3
|
+
class WeightedDistribution
|
4
|
+
attr_reader :name, :bins
|
5
|
+
|
6
|
+
def initialize(name:, bins:)
|
7
|
+
@name = name
|
8
|
+
@bins = bins
|
9
|
+
end
|
10
|
+
|
11
|
+
def values
|
12
|
+
bins.values
|
13
|
+
end
|
14
|
+
|
15
|
+
def bin_names
|
16
|
+
bins.keys
|
17
|
+
end
|
18
|
+
|
19
|
+
def hits_count
|
20
|
+
values.inject(:+).to_i
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/forget_table.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
require 'forget_table/decay'
|
4
|
+
require 'forget_table/decrementer'
|
5
|
+
require 'forget_table/distribution'
|
6
|
+
require 'forget_table/distribution_decrementer'
|
7
|
+
require 'forget_table/distribution_keys'
|
8
|
+
require 'forget_table/poisson'
|
9
|
+
require 'forget_table/weighted_distribution'
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ForgetTable::Decay do
|
4
|
+
|
5
|
+
class FakePoisson
|
6
|
+
attr_reader :sample
|
7
|
+
def initialize(sample)
|
8
|
+
@sample = sample
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:last_updated) { 1_000 }
|
13
|
+
|
14
|
+
before do
|
15
|
+
allow(ForgetTable::Poisson).to receive(:new) { |arg| FakePoisson.new(arg) }
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#decay" do
|
19
|
+
let(:decay) { ForgetTable::Decay.new(last_updated, 0.01) }
|
20
|
+
|
21
|
+
it "returns new decayed value" do
|
22
|
+
set_current_time(1_010)
|
23
|
+
|
24
|
+
expect(decay.decay_value(10)).to eq(9)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "replaces negative decayed values with 1" do
|
28
|
+
set_current_time(10_000)
|
29
|
+
|
30
|
+
expect(decay.decay_value(20)).to eq(1)
|
31
|
+
end
|
32
|
+
|
33
|
+
def set_current_time(current_time)
|
34
|
+
allow(Time).to receive_message_chain(:now, :to_i).and_return(current_time)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'fakeredis'
|
3
|
+
|
4
|
+
describe ForgetTable::Decrementer do
|
5
|
+
|
6
|
+
let(:redis) { Redis.new(port: 10000) }
|
7
|
+
let(:distribution_name) { "guitars" }
|
8
|
+
let(:distribution_keys) { ForgetTable::DistributionKeys.new("guitars") }
|
9
|
+
|
10
|
+
let(:weighted_distribution) do
|
11
|
+
double(:weighted_distribution, name: distribution_name)
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:distribution_decrementer) do
|
15
|
+
double(:distribution_decrementer, decremented_distribution: decremented_distribution)
|
16
|
+
end
|
17
|
+
|
18
|
+
let(:decremented_distribution) do
|
19
|
+
double(:decremented_distribution,
|
20
|
+
name: distribution_name,
|
21
|
+
bins: { "fender" => 10.0, "gibson" => 20.0 },
|
22
|
+
hits_count: 30.0,
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:decrementer) do
|
27
|
+
ForgetTable::Decrementer.new(
|
28
|
+
redis,
|
29
|
+
weighted_distribution
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
before(:each) do
|
34
|
+
redis.flushall
|
35
|
+
redis.set(distribution_keys.last_updated_at, 123)
|
36
|
+
|
37
|
+
allow(ForgetTable::DistributionDecrementer).to receive(:new).
|
38
|
+
with(
|
39
|
+
weighted_distribution: weighted_distribution,
|
40
|
+
last_updated_at: 123
|
41
|
+
) { distribution_decrementer }
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#run!" do
|
45
|
+
it "updates the `hits_count` key after decrementing" do
|
46
|
+
decrementer.run!
|
47
|
+
|
48
|
+
expect(redis.get(distribution_keys.hits_count).to_i).to eq(30)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "updates the timestamp" do
|
52
|
+
allow(Time).to receive_message_chain(:now, :to_i) { 37 }
|
53
|
+
|
54
|
+
decrementer.run!
|
55
|
+
|
56
|
+
expect(redis.get(distribution_keys.last_updated_at).to_i).to eq(37)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "stores the new values for the distribution" do
|
60
|
+
decrementer.run!
|
61
|
+
|
62
|
+
distribution = redis.zrevrange(distribution_name, 0, -1, with_scores: true)
|
63
|
+
expect(distribution).to match_array(
|
64
|
+
[
|
65
|
+
["fender", 10.0],
|
66
|
+
["gibson", 20.0],
|
67
|
+
])
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'forget_table/distribution_decrementer'
|
3
|
+
|
4
|
+
describe ForgetTable::DistributionDecrementer do
|
5
|
+
|
6
|
+
let(:last_updated_at) { "updated_at" }
|
7
|
+
let(:weighted_distribution) do
|
8
|
+
double(:dist,
|
9
|
+
name: "foo",
|
10
|
+
values: [13, 17],
|
11
|
+
bin_names: %w(fender gibson)
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:dist_decrementer) do
|
16
|
+
ForgetTable::DistributionDecrementer.new(
|
17
|
+
weighted_distribution: weighted_distribution,
|
18
|
+
last_updated_at: last_updated_at
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
before do
|
23
|
+
allow(ForgetTable::Decay).to receive(:new).with(last_updated_at) { |arg| FakeDecay.new(arg) }
|
24
|
+
end
|
25
|
+
|
26
|
+
class FakeDecay
|
27
|
+
def initialize(*); end
|
28
|
+
|
29
|
+
# Just decrement by 1
|
30
|
+
def decay_value(value)
|
31
|
+
value - 1
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#decremented_distribution" do
|
36
|
+
it "returns a new distribution with the same name" do
|
37
|
+
expect(dist_decrementer.decremented_distribution.name).to eq("foo")
|
38
|
+
end
|
39
|
+
|
40
|
+
it "returns a new distribution with decremented bins" do
|
41
|
+
decremented_distribution = double
|
42
|
+
allow(ForgetTable::WeightedDistribution).to receive(:new).with(
|
43
|
+
name: "foo",
|
44
|
+
bins: { "fender" => 12, "gibson" => 16 }
|
45
|
+
).and_return(decremented_distribution)
|
46
|
+
|
47
|
+
expect(dist_decrementer.decremented_distribution).to eq(decremented_distribution)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'fakeredis'
|
3
|
+
|
4
|
+
describe ForgetTable::Distribution do
|
5
|
+
|
6
|
+
# Test with a real redis server
|
7
|
+
let(:redis) { Redis.new(port: 10000) }
|
8
|
+
let(:decrementer) { double(:decrementer) }
|
9
|
+
let(:distribution) { ForgetTable::Distribution.new(name: "guitars", redis: redis) }
|
10
|
+
|
11
|
+
before(:each) do
|
12
|
+
redis.flushall
|
13
|
+
allow(ForgetTable::Decrementer).to receive(:new).and_return(decrementer)
|
14
|
+
allow(decrementer).to receive(:run!)
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#name" do
|
18
|
+
it "reads the correct distribution_name" do
|
19
|
+
expect(distribution.name).to eq("guitars")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#increment" do
|
24
|
+
|
25
|
+
it "insert the value if it was not existing before" do
|
26
|
+
distribution.increment(bin: "fender", amount: 10)
|
27
|
+
|
28
|
+
expect(distribution.distribution).to eq(["fender"])
|
29
|
+
end
|
30
|
+
|
31
|
+
it "sets the initial value if the item was not there before" do
|
32
|
+
distribution.increment(bin: "fender", amount: 10)
|
33
|
+
|
34
|
+
expect(distribution.score_for_bin("fender")).to eq(10)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "increments the existing value" do
|
38
|
+
distribution.increment(bin: "fender", amount: 10)
|
39
|
+
distribution.increment(bin: "fender", amount: 1)
|
40
|
+
|
41
|
+
expect(distribution.score_for_bin("fender")).to eq(11)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#distribution" do
|
46
|
+
before do
|
47
|
+
distribution.increment(bin: "epiphone", amount: 10)
|
48
|
+
distribution.increment(bin: "gibson", amount: 20)
|
49
|
+
distribution.increment(bin: "fender", amount: 30)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "returns the list of all stored items" do
|
53
|
+
expect(distribution.distribution).to match_array(["epiphone", "gibson", "fender"])
|
54
|
+
end
|
55
|
+
|
56
|
+
it "returns the list of the top n stored item" do
|
57
|
+
expect(distribution.distribution(number_of_bins: 2)).to match_array(["gibson", "fender"])
|
58
|
+
end
|
59
|
+
|
60
|
+
it "returns at most the stored elements even if asked for more" do
|
61
|
+
expect(distribution.distribution(number_of_bins: 42)).to match_array(["epiphone", "gibson", "fender"])
|
62
|
+
end
|
63
|
+
|
64
|
+
it "accepts a with_scores parameter" do
|
65
|
+
expect(distribution.distribution(number_of_bins: 3, with_scores: true)).
|
66
|
+
to match_array([["epiphone", 10.0], ["gibson", 20.0], ["fender", 30.0]])
|
67
|
+
end
|
68
|
+
|
69
|
+
it "returns and empty hash if the distribution is not stored in redis" do
|
70
|
+
distribution = ForgetTable::Distribution.new(name: "foo", redis: redis)
|
71
|
+
|
72
|
+
expect(distribution.distribution).to eq([[]])
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "#score_for_bin" do
|
77
|
+
it "returns the score for a given stored item" do
|
78
|
+
distribution.increment(bin: "ibanez", amount: 37)
|
79
|
+
|
80
|
+
expect(distribution.score_for_bin("ibanez")).to eq(37)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "raises an exception if the distribution is not stored in redis" do
|
84
|
+
distribution = ForgetTable::Distribution.new(name: "foo", redis: redis)
|
85
|
+
|
86
|
+
expect{ distribution.score_for_bin("yolo") }.to raise_error
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ForgetTable::Poisson do
|
4
|
+
|
5
|
+
context ".sample" do
|
6
|
+
|
7
|
+
REPETITIONS = 1000
|
8
|
+
|
9
|
+
it "raises an exception if average is negative" do
|
10
|
+
expect { ForgetTable::Poisson.new(-1) }.to raise_error(ArgumentError)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should always generate an integer" do
|
14
|
+
REPETITIONS.times do
|
15
|
+
expect(ForgetTable::Poisson.new(37).sample).to be_kind_of(Integer)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should always be within the acceptance range" do
|
20
|
+
sum = 0
|
21
|
+
REPETITIONS.times do
|
22
|
+
sum += ForgetTable::Poisson.new(37).sample
|
23
|
+
end
|
24
|
+
sum /= REPETITIONS.to_f
|
25
|
+
|
26
|
+
expect(sum).to be_within(1).of(37)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'forget_table/weighted_distribution'
|
3
|
+
|
4
|
+
describe ForgetTable::WeightedDistribution do
|
5
|
+
|
6
|
+
let(:distribution) do
|
7
|
+
ForgetTable::WeightedDistribution.new(
|
8
|
+
name: "foo",
|
9
|
+
bins: { "fender" => 10, "gibson" => 20 }
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "returns the distribution name" do
|
14
|
+
expect(distribution.name).to eq("foo")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "returns the array of values" do
|
18
|
+
expect(distribution.values).to eq([10, 20])
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns the array of bin names" do
|
22
|
+
expect(distribution.bin_names).to eq(["fender", "gibson"])
|
23
|
+
end
|
24
|
+
|
25
|
+
it "returns the total number of hits" do
|
26
|
+
expect(distribution.hits_count).to eq(30)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'fakeredis'
|
3
|
+
|
4
|
+
describe "One bin distribution" do
|
5
|
+
|
6
|
+
let(:redis) { Redis.new(port: 10000) }
|
7
|
+
let(:distribution) { ForgetTable::Distribution.new(name: "guitars", redis: redis) }
|
8
|
+
|
9
|
+
before do
|
10
|
+
redis.flushall
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "single bin" do
|
14
|
+
context "with a single increment" do
|
15
|
+
before do
|
16
|
+
distribution.increment(bin: "fender", amount: 10)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "returns the only bin" do
|
20
|
+
expect(distribution.distribution).to eq(["fender"])
|
21
|
+
end
|
22
|
+
|
23
|
+
it "returns a positive score for the bin" do
|
24
|
+
expect(distribution.score_for_bin("fender")).to be > 0
|
25
|
+
end
|
26
|
+
|
27
|
+
it "returns a positive number of hits" do
|
28
|
+
expect(distribution.hits_count).to be > 0
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "with multiple increment" do
|
33
|
+
before do
|
34
|
+
distribution.increment(bin: "fender", amount: 10)
|
35
|
+
distribution.increment(bin: "fender", amount: 20)
|
36
|
+
distribution.increment(bin: "fender", amount: 30)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "returns the only bin" do
|
40
|
+
expect(distribution.distribution).to eq(["fender"])
|
41
|
+
end
|
42
|
+
|
43
|
+
it "returns a positive score for the bin" do
|
44
|
+
expect(distribution.score_for_bin("fender")).to be > 0
|
45
|
+
end
|
46
|
+
|
47
|
+
it "returns a positive number of hits" do
|
48
|
+
expect(distribution.hits_count).to be > 0
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "multiple bins" do
|
54
|
+
context "with single increment" do
|
55
|
+
before do
|
56
|
+
distribution.increment(bin: "fender", amount: 10)
|
57
|
+
distribution.increment(bin: "gibson", amount: 20)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "returns both bins" do
|
61
|
+
expect(distribution.distribution).to match_array(["fender", "gibson"])
|
62
|
+
end
|
63
|
+
|
64
|
+
it "returns a positive score for the bins" do
|
65
|
+
expect(distribution.score_for_bin("fender")).to be > 0
|
66
|
+
expect(distribution.score_for_bin("gibson")).to be > 0
|
67
|
+
end
|
68
|
+
|
69
|
+
it "respects the scoring order" do
|
70
|
+
fender_score = distribution.score_for_bin("fender")
|
71
|
+
gibson_score = distribution.score_for_bin("gibson")
|
72
|
+
|
73
|
+
expect(gibson_score).to be >= fender_score
|
74
|
+
end
|
75
|
+
|
76
|
+
it "returns a positive number of hits" do
|
77
|
+
expect(distribution.hits_count).to be > 0
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: forget_table
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nicolò Calcavecchia
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-11-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.3'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: fakeredis
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: An implementation of http://word.bitly.com/post/41284219720/forget-table
|
70
|
+
in ruby
|
71
|
+
email: calcavecchia@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- lib/forget_table.rb
|
77
|
+
- lib/forget_table/decay.rb
|
78
|
+
- lib/forget_table/decrementer.rb
|
79
|
+
- lib/forget_table/distribution.rb
|
80
|
+
- lib/forget_table/distribution_decrementer.rb
|
81
|
+
- lib/forget_table/distribution_keys.rb
|
82
|
+
- lib/forget_table/poisson.rb
|
83
|
+
- lib/forget_table/weighted_distribution.rb
|
84
|
+
- spec/forget_table/decay_spec.rb
|
85
|
+
- spec/forget_table/decrementer_spec.rb
|
86
|
+
- spec/forget_table/distribution_decrementer_spec.rb
|
87
|
+
- spec/forget_table/distribution_spec.rb
|
88
|
+
- spec/forget_table/poisson_spec.rb
|
89
|
+
- spec/forget_table/weighted_distribution_spec.rb
|
90
|
+
- spec/integration/integration_spec.rb
|
91
|
+
- spec/spec_helper.rb
|
92
|
+
homepage: https://github.com/ncalca/forgettable
|
93
|
+
licenses:
|
94
|
+
- MIT
|
95
|
+
metadata: {}
|
96
|
+
post_install_message:
|
97
|
+
rdoc_options: []
|
98
|
+
require_paths:
|
99
|
+
- lib
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
requirements: []
|
111
|
+
rubyforge_project:
|
112
|
+
rubygems_version: 2.2.2
|
113
|
+
signing_key:
|
114
|
+
specification_version: 4
|
115
|
+
summary: Keep track of dynamically changing categorical distribution
|
116
|
+
test_files: []
|
117
|
+
has_rdoc:
|