inferx 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
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