inferx 0.1.5

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.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in inferx.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Takahiro Kondo
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ What is inferx
2
+ ==============
3
+
4
+ It is Naive Bayes classifier, and the training data is kept always by Redis.
5
+
6
+ Installation
7
+ ------------
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'inferx'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install inferx
20
+
21
+ Usage
22
+ -----
23
+
24
+ require 'inferx'
25
+
26
+ inferx = Inferx.new
27
+ inferx.categories.add(:red, :green, :blue)
28
+
29
+ inferx.categories[:red].train(%w(
30
+ he
31
+ buy
32
+ apple
33
+ its
34
+ apple
35
+ fresh
36
+ ))
37
+
38
+ inferx.categories[:green].train(%w(grasses ...))
39
+ inferx.categories[:blue].train(%w(sea ...))
40
+
41
+ puts inferx.classify(%w(apple ...)) # => red
42
+
43
+ Contributing
44
+ ------------
45
+
46
+ 1. Fork it
47
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
48
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
49
+ 4. Push to the branch (`git push origin my-new-feature`)
50
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/inferx.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/inferx/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Takahiro Kondo"]
6
+ gem.email = ["kondo@atedesign.net"]
7
+ gem.description = %q{It is Naive Bayes classifier, and the training data is kept always by Redis.}
8
+ gem.summary = %q{Naive Bayes classifier, the training data on Redis}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "inferx"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Inferx::VERSION
17
+
18
+ gem.add_runtime_dependency 'redis'
19
+
20
+ gem.add_development_dependency 'rspec'
21
+ end
@@ -0,0 +1,52 @@
1
+ class Inferx
2
+ class Adapter
3
+
4
+ # @param [Redis] an instance of Redis
5
+ # @param [String] namespace of keys to be used to Redis
6
+ def initialize(redis, namespace = nil)
7
+ @redis = redis
8
+ @namespace = namespace
9
+ end
10
+
11
+ # Get the key for access to categories.
12
+ #
13
+ # @return [String] the key
14
+ def categories_key
15
+ @categories_key ||= make_categories_key
16
+ end
17
+
18
+ # Make the key for access to categories.
19
+ #
20
+ # @return [String] the key
21
+ def make_categories_key
22
+ parts = %w(inferx categories)
23
+ parts.insert(1, @namespace) if @namespace
24
+ parts.join(':')
25
+ end
26
+
27
+ # Make the key for access to scores stored each by word.
28
+ #
29
+ # @param [Symbol] a category name
30
+ # @return [String] the key
31
+ def make_category_key(category_name)
32
+ "#{categories_key}:#{category_name}"
33
+ end
34
+
35
+ # Spawn an instance of any class.
36
+ #
37
+ # @param [Class] any class, constructor takes the instance of Redis to
38
+ # first argument, and takes the namespace to last argument
39
+ # @return [Object] a instance of the class
40
+ def spawn(klass, *args)
41
+ klass.new(@redis, *args, @namespace)
42
+ end
43
+
44
+ protected
45
+
46
+ %w(hdel hexists hget hincrby hkeys hsetnx).each do |command|
47
+ define_method(command) do |*args|
48
+ @redis.__send__(command, categories_key, *args)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,53 @@
1
+ require 'inferx/adapter'
2
+ require 'inferx/category'
3
+
4
+ class Inferx
5
+ class Categories < Adapter
6
+
7
+ # Get all category names.
8
+ #
9
+ # @return [Array<Symbol>] category names
10
+ def all
11
+ (hkeys || []).map(&:to_sym)
12
+ end
13
+
14
+ # Get a category according name.
15
+ #
16
+ # @param [Symbol] category name
17
+ # @return [Inferx::Category] category
18
+ def get(category_name)
19
+ raise ArgumentError, "'#{category_name}' is missing" unless hexists(category_name)
20
+ spawn(Category, category_name)
21
+ end
22
+ alias [] get
23
+
24
+ # Add categories.
25
+ #
26
+ # @param [Array<Symbol>] category names
27
+ def add(*category_names)
28
+ @redis.pipelined do
29
+ category_names.each { |category_name| hsetnx(category_name, 0) }
30
+ end
31
+ end
32
+
33
+ # Remove categories.
34
+ #
35
+ # @param [Array<Symbol>] category names
36
+ def remove(*category_names)
37
+ @redis.pipelined do
38
+ category_names.each { |category_name| hdel(category_name) }
39
+ @redis.del(*category_names.map(&method(:make_category_key)))
40
+ end
41
+ end
42
+
43
+ include Enumerable
44
+
45
+ # Apply process for each category.
46
+ #
47
+ # @yield a block to be called for every category
48
+ # @yieldparam [Inferx::Category] category
49
+ def each
50
+ all.each { |category_name| yield spawn(Category, category_name) }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,139 @@
1
+ require 'inferx/adapter'
2
+
3
+ class Inferx
4
+ class Category < Adapter
5
+
6
+ # @param [Redis] an instance of Redis
7
+ # @param [Symbol] a category name
8
+ # @param [String] namespace of keys to be used to Redis
9
+ def initialize(redis, name, namespace = nil)
10
+ super(redis, namespace)
11
+ @name = name
12
+ end
13
+
14
+ attr_reader :name
15
+
16
+ # Get words with scores in the category.
17
+ #
18
+ # @param [Hash] options
19
+ # - `:score => Integer`: lower limit for getting by score
20
+ # - `:rank => Integer`: upper limit for getting by rank
21
+ #
22
+ # @return [Hash<String, Integer>] words with scores
23
+ def all(options = {})
24
+ words_with_scores = if score = options[:score]
25
+ zrevrangebyscore('+inf', score, :withscores => true)
26
+ else
27
+ rank = options[:rank] || -1
28
+ zrevrange(0, rank, :withscores => true)
29
+ end
30
+
31
+ size = words_with_scores.size
32
+ index = 1
33
+
34
+ while index < size
35
+ words_with_scores[index] = words_with_scores[index].to_i
36
+ index += 2
37
+ end
38
+
39
+ Hash[*words_with_scores]
40
+ end
41
+
42
+ # Get score of a word.
43
+ #
44
+ # @param [String] a word
45
+ # @return [Integer, nil]
46
+ # - when the word is member, score of the word
47
+ # - when the word is not member, nil
48
+ def get(word)
49
+ score = zscore(word)
50
+ score ? score.to_i : nil
51
+ end
52
+ alias [] get
53
+
54
+ # Enhance the training data giving words.
55
+ #
56
+ # @param [Array<String>] words
57
+ def train(words)
58
+ @redis.pipelined do
59
+ increase = collect(words).inject(0) do |count, pair|
60
+ zincrby(pair[1], pair[0])
61
+ count + pair[1]
62
+ end
63
+
64
+ hincrby(name, increase) if increase > 0
65
+ end
66
+ end
67
+
68
+ # Attenuate the training data giving words.
69
+ #
70
+ # @param [Array<String>] words
71
+ def untrain(words)
72
+ decrease = 0
73
+
74
+ values = @redis.pipelined do
75
+ decrease = collect(words).inject(0) do |count, pair|
76
+ zincrby(-pair[1], pair[0])
77
+ count + pair[1]
78
+ end
79
+
80
+ zremrangebyscore('-inf', 0)
81
+ end
82
+
83
+ values[0..-2].each do |score|
84
+ score = score.to_i
85
+ decrease += score if score < 0
86
+ end
87
+
88
+ hincrby(name, -decrease) if decrease > 0
89
+ end
90
+
91
+ # Get total of scores.
92
+ #
93
+ # @return [Integer] total of scores
94
+ def size
95
+ (hget(name) || 0).to_i
96
+ end
97
+
98
+ # Get effectively scores for each word.
99
+ #
100
+ # @param [Array<String>] words
101
+ # @param [Hash<String, Integer>] words with scores prepared in advance for
102
+ # reduce access to Redis
103
+ # @return [Array<Integer>] scores for each word
104
+ def scores(words, words_with_scores = {})
105
+ scores = @redis.pipelined do
106
+ words.each do |word|
107
+ zscore(word) unless words_with_scores[word]
108
+ end
109
+ end
110
+
111
+ index = 0
112
+
113
+ next_score = lambda do
114
+ score = scores[index]
115
+ index += 1
116
+ score ? score.to_i : nil
117
+ end
118
+
119
+ words.map { |word| words_with_scores[word] || next_score[] }
120
+ end
121
+
122
+ private
123
+
124
+ %w(zrevrange zrevrangebyscore zscore zincrby zremrangebyscore).each do |command|
125
+ define_method(command) do |*args|
126
+ @category_key ||= make_category_key(@name)
127
+ @redis.__send__(command, @category_key, *args)
128
+ end
129
+ end
130
+
131
+ def collect(words)
132
+ words.inject({}) do |hash, word|
133
+ hash[word] ||= 0
134
+ hash[word] += 1
135
+ hash
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,3 @@
1
+ class Inferx
2
+ VERSION = '0.1.5'
3
+ end
data/lib/inferx.rb ADDED
@@ -0,0 +1,56 @@
1
+ require 'redis'
2
+
3
+ require 'inferx/version'
4
+ require 'inferx/categories'
5
+
6
+ class Inferx
7
+
8
+ # @param [Hash] options
9
+ # - `:namespace => String`: namespace of keys to be used to Redis
10
+ #
11
+ # Other options are passed to Redis#initialize in:
12
+ # https://github.com/redis/redis-rb
13
+ def initialize(options = {})
14
+ namespace = options.delete(:namespace)
15
+ redis = Redis.new(options)
16
+ @categories = Categories.new(redis, namespace)
17
+ end
18
+
19
+ attr_reader :categories
20
+
21
+ # Get a score of a category according to a set of words.
22
+ #
23
+ # @param [Inferx::Category] a category for scoring
24
+ # @param [Array<String>] a set of words
25
+ # @return [Float] a score of the category
26
+ def score(category, words)
27
+ size = category.size.to_f
28
+ return -Float::INFINITY unless size > 0
29
+ words_with_scores = category.all(:rank => 500)
30
+ scores = category.scores(words, words_with_scores)
31
+ scores.inject(0) { |s, score| s + Math.log((score || 0.1) / size) }
32
+ end
33
+
34
+ # Get a score for each category according to a set of words.
35
+ #
36
+ # @param [Array<String>] a set of words
37
+ # @return [Hash<Symbol, Float>] scores to key a category
38
+ #
39
+ # @see #score
40
+ def classifications(words)
41
+ words = words.uniq
42
+ Hash[@categories.map { |category| [category.name, score(category, words)] }]
43
+ end
44
+
45
+ # Classify words to any one category
46
+ #
47
+ # @param [Array<String>] a set of words
48
+ # @return [Symbol] most high-scoring category name
49
+ #
50
+ # @see #score
51
+ # @see #classifications
52
+ def classify(words)
53
+ category = classifications(words).max_by { |score| score[1] }
54
+ category ? category[0] : nil
55
+ end
56
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+ require 'inferx/adapter'
3
+
4
+ describe Inferx::Adapter, '#initialize' do
5
+ it 'sets the instance of Redis to @redis' do
6
+ redis = redis_stub
7
+ adapter = described_class.new(redis)
8
+ adapter.instance_eval { @redis }.should == redis
9
+ end
10
+
11
+ context 'with a namespace' do
12
+ it 'sets the namespace to @namespace' do
13
+ adapter = described_class.new(redis_stub, 'example')
14
+ adapter.instance_eval { @namespace }.should == 'example'
15
+ end
16
+ end
17
+ end
18
+
19
+ describe Inferx::Adapter, '#categories_key' do
20
+ it "calls #{described_class}#make_category_key" do
21
+ adapter = described_class.new(redis_stub)
22
+ adapter.should_receive(:make_categories_key)
23
+ adapter.categories_key
24
+ end
25
+
26
+ it "calls #{described_class}#make_category_key once in same instance" do
27
+ adapter = described_class.new(redis_stub)
28
+ adapter.should_receive(:make_categories_key).and_return('inferx:categories')
29
+ adapter.categories_key
30
+ adapter.categories_key
31
+ end
32
+ end
33
+
34
+ describe Inferx::Adapter, '#make_categories_key' do
35
+ it 'returns the key for access to categories' do
36
+ adapter = described_class.new(redis_stub)
37
+ adapter.categories_key.should == 'inferx:categories'
38
+ end
39
+
40
+ context 'with a namespace' do
41
+ it 'returns the key included the namespace' do
42
+ adapter = described_class.new(redis_stub, 'example')
43
+ adapter.categories_key.should == 'inferx:example:categories'
44
+ end
45
+ end
46
+ end
47
+
48
+ describe Inferx::Adapter, '#make_category_key' do
49
+ it 'returns the key for access to to scores stored each by word' do
50
+ adapter = described_class.new(redis_stub)
51
+ adapter.make_category_key(:red).should == 'inferx:categories:red'
52
+ end
53
+
54
+ context 'with a namespace' do
55
+ it 'returns the key included the namespace' do
56
+ adapter = described_class.new(redis_stub, 'example')
57
+ adapter.make_category_key(:red).should == 'inferx:example:categories:red'
58
+ end
59
+ end
60
+ end
61
+
62
+ describe Inferx::Adapter, '#spawn' do
63
+ it 'calls constructor of the class with the instance variables and the arguments' do
64
+ redis = redis_stub
65
+ adapter = described_class.new(redis, 'example')
66
+ klass = mock.tap { |m| m.should_receive(:new).with(redis, 'arg1', 'arg2', 'example') }
67
+ adapter.spawn(klass, 'arg1', 'arg2')
68
+ end
69
+
70
+ it 'returns an instance of the class' do
71
+ adapter = described_class.new(redis_stub)
72
+ klass = stub.tap { |s| s.stub!(:new).and_return('klass') }
73
+ adapter.spawn(klass).should == 'klass'
74
+ end
75
+ end
@@ -0,0 +1,151 @@
1
+ require 'spec_helper'
2
+ require 'inferx/categories'
3
+
4
+ describe Inferx::Categories do
5
+ it 'includes Enumerable' do
6
+ described_class.should be_include(Enumerable)
7
+ end
8
+ end
9
+
10
+ describe Inferx::Categories, '#initialize' do
11
+ it 'calls Inferx::Adapter#initialize' do
12
+ redis = redis_stub
13
+ categories = described_class.new(redis, 'example')
14
+ categories.instance_eval { @redis }.should == redis
15
+ categories.instance_eval { @namespace }.should == 'example'
16
+ end
17
+ end
18
+
19
+ describe Inferx::Categories, '#all' do
20
+ it 'calls Redis#hkeys' do
21
+ redis = redis_stub do |s|
22
+ s.should_receive(:hkeys).with('inferx:categories')
23
+ end
24
+
25
+ categories = described_class.new(redis)
26
+ categories.all
27
+ end
28
+
29
+ it 'returns the all categories as Symbol' do
30
+ redis = redis_stub do |s|
31
+ s.stub!(:hkeys).and_return(%w(red green blue))
32
+ end
33
+
34
+ categories = described_class.new(redis)
35
+ categories.all.should == [:red, :green, :blue]
36
+ end
37
+
38
+ it 'returns an empty array if the key is missing' do
39
+ redis = redis_stub do |s|
40
+ s.stub!(:hkeys).and_return([])
41
+ end
42
+
43
+ categories = described_class.new(redis)
44
+ categories.all.should be_empty
45
+ end
46
+ end
47
+
48
+ describe Inferx::Categories, '#get' do
49
+ it 'calls Redis#hexists' do
50
+ redis = redis_stub do |s|
51
+ s.should_receive(:hexists).with('inferx:categories', :red).and_return(true)
52
+ end
53
+
54
+ categories = described_class.new(redis)
55
+ categories.get(:red)
56
+ end
57
+
58
+ it 'calles Inferx::Category.new with the instance of Redis, the category name and the namepsace' do
59
+ redis = redis_stub do |s|
60
+ s.stub!(:hexists).and_return(true)
61
+ end
62
+
63
+ Inferx::Category.should_receive(:new).with(redis, :red, 'example')
64
+ categories = described_class.new(redis, 'example')
65
+ categories.get(:red)
66
+ end
67
+
68
+ it 'returns an instance of Inferx::Category' do
69
+ redis = redis_stub do |s|
70
+ s.stub!(:hexists).and_return(true)
71
+ end
72
+
73
+ categories = described_class.new(redis)
74
+ categories.get(:red).should be_an(Inferx::Category)
75
+ end
76
+
77
+ context 'with a missing category' do
78
+ it 'raises ArgumentError' do
79
+ redis = redis_stub do |s|
80
+ s.stub!(:hexists).and_return(false)
81
+ end
82
+
83
+ categories = described_class.new(redis)
84
+ lambda { categories.get(:red) }.should raise_error(ArgumentError, /'red' is missing/)
85
+ end
86
+ end
87
+ end
88
+
89
+ describe Inferx::Categories, '#add' do
90
+ it 'calls Redis#hsetnx' do
91
+ redis = redis_stub do |s|
92
+ s.should_receive(:hsetnx).with('inferx:categories', :red, 0)
93
+ end
94
+
95
+ categories = described_class.new(redis)
96
+ categories.add(:red)
97
+ end
98
+
99
+ it 'calls Redis#hsetnx according to the number of the categories' do
100
+ redis = redis_stub do |s|
101
+ s.should_receive(:hsetnx).with('inferx:categories', :red, 0)
102
+ s.should_receive(:hsetnx).with('inferx:categories', :green, 0)
103
+ end
104
+
105
+ categories = described_class.new(redis)
106
+ categories.add(:red, :green)
107
+ end
108
+ end
109
+
110
+ describe Inferx::Categories, '#remove' do
111
+ it 'calls Redis#hdel and Redis#del' do
112
+ redis = redis_stub do |s|
113
+ s.should_receive(:hdel).with('inferx:categories', :red)
114
+ s.should_receive(:del).with('inferx:categories:red')
115
+ end
116
+
117
+ categories = described_class.new(redis)
118
+ categories.remove(:red)
119
+ end
120
+
121
+ it 'calls Redis#hdel according to the number of the categories' do
122
+ redis = redis_stub do |s|
123
+ s.should_receive(:hdel).with('inferx:categories', :red)
124
+ s.should_receive(:hdel).with('inferx:categories', :green)
125
+ s.should_receive(:del).with('inferx:categories:red', 'inferx:categories:green')
126
+ end
127
+
128
+ categories = described_class.new(redis)
129
+ categories.remove(:red, :green)
130
+ end
131
+ end
132
+
133
+ describe Inferx::Categories, '#each' do
134
+ before do
135
+ @redis = redis_stub do |s|
136
+ s.stub!(:hkeys).and_return(%w(red green blue))
137
+ end
138
+ end
139
+
140
+ it 'passes an instance of Inferx::Category to the block' do
141
+ categories = described_class.new(@redis)
142
+ categories.each { |category| category.should be_an(Inferx::Category) }
143
+ end
144
+
145
+ it 'calls the block according to the number of the categories' do
146
+ n = 0
147
+ categories = described_class.new(@redis)
148
+ categories.each { |category| n += 1 }
149
+ n.should == 3
150
+ end
151
+ end
@@ -0,0 +1,206 @@
1
+ require 'spec_helper'
2
+ require 'inferx/category'
3
+
4
+ describe Inferx::Category, '#initialize' do
5
+ it 'calls Inferx::Adapter#initialize' do
6
+ redis = redis_stub
7
+ category = described_class.new(redis, :red, 'example')
8
+ category.instance_eval { @redis }.should == redis
9
+ category.instance_eval { @namespace }.should == 'example'
10
+ end
11
+
12
+ it 'sets the category name to the name attribute' do
13
+ category = described_class.new(redis_stub, :red)
14
+ category.name.should == :red
15
+ end
16
+ end
17
+
18
+ describe Inferx::Category, '#all' do
19
+ it 'calls Redis#revrange' do
20
+ redis = redis_stub do |s|
21
+ s.should_receive(:zrevrange).with('inferx:categories:red', 0, -1, :withscores => true).and_return([])
22
+ end
23
+
24
+ category = described_class.new(redis, :red)
25
+ category.all
26
+ end
27
+
28
+ it 'returns the words with the score' do
29
+ redis = redis_stub do |s|
30
+ s.stub!(:zrevrange).with('inferx:categories:red', 0, -1, :withscores => true).and_return(%w(apple 2 strawberry 3))
31
+ end
32
+
33
+ category = described_class.new(redis, :red)
34
+ category.all.should == {'apple' => 2, 'strawberry' => 3}
35
+ end
36
+
37
+ context 'with the rank option' do
38
+ it 'calls Redis#zrevrange' do
39
+ redis = redis_stub do |s|
40
+ s.should_receive(:zrevrange).with('inferx:categories:red', 0, 1000, :withscores => true).and_return([])
41
+ end
42
+
43
+ category = described_class.new(redis, :red)
44
+ category.all(:rank => 1000)
45
+ end
46
+ end
47
+
48
+ context 'with the score option' do
49
+ it 'calls Redis#zrevrangebyscore' do
50
+ redis = redis_stub do |s|
51
+ s.should_receive(:zrevrangebyscore).with('inferx:categories:red', '+inf', 2, :withscores => true).and_return([])
52
+ end
53
+
54
+ category = described_class.new(redis, :red)
55
+ category.all(:score => 2)
56
+ end
57
+ end
58
+ end
59
+
60
+ describe Inferx::Category, '#get' do
61
+ it 'calls Redis#zscore' do
62
+ redis = redis_stub do |s|
63
+ s.should_receive(:zscore).with('inferx:categories:red', 'apple')
64
+ end
65
+
66
+ category = described_class.new(redis, :red)
67
+ category.get('apple')
68
+ end
69
+
70
+ it 'returns the score as Integer' do
71
+ redis = redis_stub do |s|
72
+ s.stub!(:zscore).with('inferx:categories:red', 'apple').and_return('1')
73
+ end
74
+
75
+ category = described_class.new(redis, :red)
76
+ category.get('apple').should == 1
77
+ end
78
+
79
+ context 'with a missing word' do
80
+ it 'returns nil' do
81
+ redis = redis_stub do |s|
82
+ s.stub!(:zscore).with('inferx:categories:red', 'strawberry').and_return(nil)
83
+ end
84
+
85
+ category = described_class.new(redis, :red)
86
+ category.get('strawberry').should be_nil
87
+ end
88
+ end
89
+ end
90
+
91
+ describe Inferx::Category, '#train' do
92
+ it 'calls Redis#zincrby and Redis#hincrby' do
93
+ redis = redis_stub do |s|
94
+ s.should_receive(:zincrby).with('inferx:categories:red', 2, 'apple')
95
+ s.should_receive(:zincrby).with('inferx:categories:red', 3, 'strawberry')
96
+ s.should_receive(:hincrby).with('inferx:categories', :red, 5)
97
+ end
98
+
99
+ category = described_class.new(redis, :red)
100
+ category.train(%w(apple strawberry apple strawberry strawberry))
101
+ end
102
+
103
+ context 'with no update' do
104
+ it 'does not call Redis#hincrby' do
105
+ redis = redis_stub do |s|
106
+ s.should_not_receive(:hincrby)
107
+ end
108
+
109
+ category = described_class.new(redis, :red)
110
+ category.train(%w())
111
+ end
112
+ end
113
+ end
114
+
115
+ describe Inferx::Category, '#untrain' do
116
+ it 'calls Redis#zincrby, Redis#zremrangebyscore and Redis#hincrby' do
117
+ redis = redis_stub do |s|
118
+ s.should_receive(:zincrby).with('inferx:categories:red', -2, 'apple')
119
+ s.should_receive(:zincrby).with('inferx:categories:red', -3, 'strawberry')
120
+ s.should_receive(:zremrangebyscore).with('inferx:categories:red', '-inf', 0).and_return(%w(3 -2 1))
121
+ s.should_receive(:hincrby).with('inferx:categories', :red, -3)
122
+ end
123
+
124
+ category = described_class.new(redis, :red)
125
+ category.untrain(%w(apple strawberry apple strawberry strawberry))
126
+ end
127
+
128
+ context 'with no update' do
129
+ it 'does not call Redis#hincrby' do
130
+ redis = redis_stub do |s|
131
+ s.stub!(:zincrby)
132
+ s.stub!(:zremrangebyscore).and_return(%w(-2 -3 2))
133
+ s.should_not_receive(:hincrby)
134
+ end
135
+
136
+ category = described_class.new(redis, :red)
137
+ category.untrain(%w(apple strawberry apple strawberry strawberry))
138
+ end
139
+ end
140
+ end
141
+
142
+ describe Inferx::Category, '#size' do
143
+ it 'calls Redis#hget' do
144
+ redis = redis_stub do |s|
145
+ s.should_receive(:hget).with('inferx:categories', :red)
146
+ end
147
+
148
+ category = described_class.new(redis, :red)
149
+ category.size
150
+ end
151
+
152
+ it 'returns total of the score of the words as Integer' do
153
+ redis = redis_stub do |s|
154
+ s.stub!(:hget).and_return('1')
155
+ end
156
+
157
+ category = described_class.new(redis, :red)
158
+ category.size.should == 1
159
+ end
160
+
161
+ context 'with the missing key' do
162
+ it 'returns 0' do
163
+ redis = redis_stub do |s|
164
+ s.stub!(:hget).and_return(nil)
165
+ end
166
+
167
+ category = described_class.new(redis, :red)
168
+ category.size.should == 0
169
+ end
170
+ end
171
+ end
172
+
173
+ describe Inferx::Category, '#scores' do
174
+ it 'calls Redis#zscore' do
175
+ redis = redis_stub do |s|
176
+ s.should_receive(:zscore).with('inferx:categories:red', 'apple')
177
+ s.should_receive(:zscore).with('inferx:categories:red', 'strawberry')
178
+ end
179
+
180
+ category = described_class.new(redis, :red)
181
+ category.scores(%w(apple strawberry))
182
+ end
183
+
184
+ it 'returns the scores' do
185
+ redis = redis_stub do |s|
186
+ s.stub!(:pipelined).and_return(%w(2 3))
187
+ end
188
+
189
+ category = described_class.new(redis, :red)
190
+ scores = category.scores(%w(apple strawberry))
191
+ scores.should == [2, 3]
192
+ end
193
+
194
+ context 'with words with scores' do
195
+ it 'returns the scores to use the cache' do
196
+ redis = redis_stub do |s|
197
+ s.should_not_receive(:zscore).with('inferx:categories:red', 'strawberry')
198
+ s.stub!(:pipelined).and_return { |&block| block.call; [2] }
199
+ end
200
+
201
+ category = described_class.new(redis, :red)
202
+ scores = category.scores(%w(apple strawberry), 'strawberry' => 3, 'hoge' => 1)
203
+ scores.should == [2, 3]
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,137 @@
1
+ require 'spec_helper'
2
+ require 'inferx'
3
+
4
+ describe Inferx do
5
+ it 'is an instance of Class' do
6
+ described_class.should be_an_instance_of(Class)
7
+ end
8
+
9
+ it 'has VERSION constant' do
10
+ described_class.should be_const_defined(:VERSION)
11
+ end
12
+ end
13
+
14
+ describe Inferx, '#initialize' do
15
+ it "calls #{described_class}::Categories.new with a connection of Redis and the namespace option" do
16
+ redis = redis_stub
17
+ Inferx::Categories.should_receive(:new).with(redis, 'example')
18
+ described_class.new(:namespace => 'example')
19
+ end
20
+
21
+ it "sets an instance of #{described_class}::Categories to the categories attribute" do
22
+ redis_stub
23
+ inferx = described_class.new
24
+ inferx.categories.should be_an(Inferx::Categories)
25
+ end
26
+ end
27
+
28
+ describe Inferx, '#score' do
29
+ before do
30
+ @inferx = described_class.new
31
+ end
32
+
33
+ it "calls #{described_class}::Categories#size, #{described_class}::Category#all and #{described_class}::Category#scores" do
34
+ category = mock.tap do |m|
35
+ m.should_receive(:size).and_return(5)
36
+ m.should_receive(:all).with(:rank => 500).and_return('apple' => 2)
37
+ m.should_receive(:scores).with(%w(apple), 'apple' => 2).and_return([2])
38
+ end
39
+
40
+ @inferx.score(category, %w(apple))
41
+ end
42
+
43
+ it 'returns an expected score' do
44
+ {
45
+ [%w(apple), 2, [2]] => 0.0,
46
+ [%w(apple), 2, [nil]] => -2.995732273553991,
47
+ [%w(apple), 3, [nil]] => -3.4011973816621555
48
+ }.each do |args, expected|
49
+ words, size, scores = args
50
+
51
+ category = stub.tap do |s|
52
+ s.stub!(:size).and_return(size)
53
+ s.stub!(:scores).and_return(scores)
54
+ s.stub!(:all)
55
+ end
56
+
57
+ @inferx.score(category, words).should == expected
58
+ end
59
+ end
60
+
61
+ it 'returns a negative infinity number if the category does not have words' do
62
+ category = stub.tap do |s|
63
+ s.stub!(:size).and_return(0)
64
+ end
65
+
66
+ score = @inferx.score(category, %w(apple))
67
+ score.should be_infinite
68
+ score.should < 0
69
+ end
70
+ end
71
+
72
+ describe Inferx, '#classifications' do
73
+ before do
74
+ categories = [:red, :green, :blue].map do |category_name|
75
+ stub.tap { |s| s.stub!(:name).and_return(category_name) }
76
+ end
77
+
78
+ @inferx = described_class.new.tap do |s|
79
+ s.instance_eval { @categories = categories }
80
+ s.stub!(:score).and_return { |category, words| "score of #{category.name}" }
81
+ end
82
+ end
83
+
84
+ it "calls #{described_class}#score" do
85
+ @inferx.tap do |m|
86
+ m.should_receive(:score).with(@inferx.categories[0], %w(apple))
87
+ m.should_receive(:score).with(@inferx.categories[1], %w(apple))
88
+ m.should_receive(:score).with(@inferx.categories[2], %w(apple))
89
+ end
90
+
91
+ @inferx.classifications(%w(apple))
92
+ end
93
+
94
+ it 'calls uniq method of the words' do
95
+ words = %w(apple).tap do |m|
96
+ m.should_receive(:uniq).and_return(m)
97
+ end
98
+
99
+ @inferx.classifications(words)
100
+ end
101
+
102
+ it 'returns the scores to key the category name' do
103
+ @inferx.classifications(%w(apple)).should == {
104
+ :red => 'score of red',
105
+ :green => 'score of green',
106
+ :blue => 'score of blue'
107
+ }
108
+ end
109
+ end
110
+
111
+ describe Inferx, '#classify' do
112
+ before do
113
+ @inferx = described_class.new.tap do |s|
114
+ s.stub!(:classifications).and_return(:red => -2, :green => -1, :blue => -3)
115
+ end
116
+ end
117
+
118
+ it "calls #{described_class}#classifications" do
119
+ @inferx.tap do |m|
120
+ m.should_receive(:classifications).with(%w(apple)).and_return(:red => -2)
121
+ end
122
+
123
+ @inferx.classify(%w(apple))
124
+ end
125
+
126
+ it 'returns the most high-scoring category' do
127
+ @inferx.classify(%w(apple)).should == :green
128
+ end
129
+
130
+ it 'returns nil if the categories is nothing' do
131
+ @inferx.tap do |s|
132
+ s.stub!(:classifications).and_return({})
133
+ end
134
+
135
+ @inferx.classify(%w(apple)).should be_nil
136
+ end
137
+ end
@@ -0,0 +1,7 @@
1
+ def redis_stub
2
+ stub.tap do |s|
3
+ s.stub!(:pipelined).and_return { |&block| block.call }
4
+ yield s if block_given?
5
+ Redis.stub!(:new).and_return(s) if defined? Redis
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: inferx
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.5
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Takahiro Kondo
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-04-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: It is Naive Bayes classifier, and the training data is kept always by
47
+ Redis.
48
+ email:
49
+ - kondo@atedesign.net
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - .gitignore
55
+ - Gemfile
56
+ - LICENSE
57
+ - README.md
58
+ - Rakefile
59
+ - inferx.gemspec
60
+ - lib/inferx.rb
61
+ - lib/inferx/adapter.rb
62
+ - lib/inferx/categories.rb
63
+ - lib/inferx/category.rb
64
+ - lib/inferx/version.rb
65
+ - spec/inferx/adapter_spec.rb
66
+ - spec/inferx/categories_spec.rb
67
+ - spec/inferx/category_spec.rb
68
+ - spec/inferx_spec.rb
69
+ - spec/spec_helper.rb
70
+ homepage: ''
71
+ licenses: []
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubyforge_project:
90
+ rubygems_version: 1.8.21
91
+ signing_key:
92
+ specification_version: 3
93
+ summary: Naive Bayes classifier, the training data on Redis
94
+ test_files:
95
+ - spec/inferx/adapter_spec.rb
96
+ - spec/inferx/categories_spec.rb
97
+ - spec/inferx/category_spec.rb
98
+ - spec/inferx_spec.rb
99
+ - spec/spec_helper.rb