fuzzy_infer 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.
@@ -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,10 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
5
+ # development dependencies
6
+ gem 'minitest'
7
+ gem 'minitest-reporters'
8
+ gem 'mysql2'
9
+ gem 'earth'
10
+ gem 'pg'
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Seamus Abshere
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.
@@ -0,0 +1,48 @@
1
+ # FuzzyInfer
2
+
3
+ ## Where it's used
4
+
5
+ * [Brighter Planet CM1 Impact Estimate web service](http://impact.brighterplanet.com)
6
+
7
+ ## setup
8
+
9
+ 1) gem install a bleeding edge earth
10
+
11
+ cd earth
12
+ git pull
13
+ gem build earth.gemspec
14
+ gem install earth-0.11.10.gem --ignore-dependencies --no-rdoc --no-ri
15
+
16
+ 2) create your test database
17
+
18
+ mysql -u root -ppassword -e "create database test_fuzzy_infer charset utf8"
19
+
20
+ 3) load cbecs (just the first time - note that it is hardcoded to ONLY run cbecs data_miner)
21
+
22
+ RUN_DATA_MINER=true rake
23
+
24
+ ## further testing
25
+
26
+ rake
27
+
28
+ ## future plans
29
+
30
+ **in the future the fuzzy inference machine will make TEMPORARY tables, rather than gum up your db**
31
+
32
+ for now, it makes permanent tables so that you can examine them
33
+
34
+ wishlist:
35
+
36
+ * re-use FIM for multiple targets
37
+ * cache #fuzzy_infer
38
+ * randomize names of all added columns
39
+ * use arel to generate sql(?)
40
+
41
+ ## Database compatibility
42
+
43
+ * mysql
44
+ * postgresql
45
+
46
+ ## Copyright
47
+
48
+ Copyright 2012 Brighter Planet, Inc.
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rake'
5
+ require 'rake/testtask'
6
+ Rake::TestTask.new(:test) do |test|
7
+ test.libs << 'lib' << 'test'
8
+ test.pattern = 'test/**/test_*.rb'
9
+ test.verbose = true
10
+ end
11
+
12
+ task :default => :test
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/fuzzy_infer/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Seamus Abshere", "Ian Hough", "Matt Kling"]
6
+ gem.email = ["seamus@abshere.net", 'ijhough@gmail.com', 'mattkling@gmail.com']
7
+ desc = %q{Use fuzzy set analysis to infer missing values. You provide a sigma function, a membership function, and a kernel.}
8
+ gem.description = desc
9
+ gem.summary = desc
10
+ gem.homepage = "https://github.com/seamusabshere/fuzzy_infer"
11
+
12
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
13
+ gem.files = `git ls-files`.split("\n")
14
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ gem.name = "fuzzy_infer"
16
+ gem.require_paths = ["lib"]
17
+ gem.version = FuzzyInfer::VERSION
18
+
19
+ gem.add_runtime_dependency 'activesupport', '>=3'
20
+ gem.add_runtime_dependency 'activerecord', '>=3'
21
+ gem.add_runtime_dependency 'hashie'
22
+ end
@@ -0,0 +1,14 @@
1
+ require 'active_support/core_ext'
2
+
3
+ require "fuzzy_infer/version"
4
+ require 'fuzzy_infer/registry'
5
+ require 'fuzzy_infer/active_record_class_methods'
6
+ require 'fuzzy_infer/active_record_instance_methods'
7
+ require 'fuzzy_infer/fuzzy_inference_machine'
8
+
9
+ module FuzzyInfer
10
+ # Your code goes here...
11
+ end
12
+
13
+ ActiveRecord::Base.send :include, FuzzyInfer::ActiveRecordInstanceMethods
14
+ ActiveRecord::Base.extend FuzzyInfer::ActiveRecordClassMethods
@@ -0,0 +1,15 @@
1
+ require 'hashie/mash'
2
+
3
+ module FuzzyInfer
4
+ module ActiveRecordClassMethods
5
+ # Configure fuzzy inferences
6
+ # see test/helper.rb for an example
7
+ def fuzzy_infer(options = {})
8
+ options = ::Hashie::Mash.new options
9
+ options.target.each do |target|
10
+ Registry.instance[name] ||= {}
11
+ Registry.instance[name][target] = options
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ module FuzzyInfer
2
+ module ActiveRecordInstanceMethods
3
+ # Returns a new FuzzyInferenceMachine instance that can infer this target (field)
4
+ def fuzzy_inference_machine(target)
5
+ target = target.to_sym
6
+ FuzzyInferenceMachine.new self, target, Registry.config_for(self.class.name, target)
7
+ end
8
+
9
+ # Shortcut to creating a FIM and immediately calling it
10
+ def fuzzy_infer(target)
11
+ fuzzy_inference_machine(target).infer
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,126 @@
1
+ module FuzzyInfer
2
+ class FuzzyInferenceMachine
3
+
4
+ attr_reader :kernel
5
+ attr_reader :target
6
+ attr_reader :config
7
+
8
+ def initialize(kernel, target, config)
9
+ @kernel = kernel
10
+ @target = target
11
+ @config = config
12
+ end
13
+
14
+ def infer
15
+ calculate_table!
16
+ retval = select_value(%{SELECT SUM(fuzzy_weighted_value)/SUM(fuzzy_membership) FROM #{table_name}}).to_f
17
+ execute %{DROP TABLE #{table_name}}
18
+ retval
19
+ end
20
+
21
+ # TODO technically I could use this to generate the SQL
22
+ def arel_table
23
+ calculate_table!
24
+ Arel::Table.new table_name
25
+ end
26
+
27
+ def basis
28
+ @basis ||= kernel.attributes.symbolize_keys.slice(*config.basis).reject { |k, v| v.nil? }
29
+ end
30
+
31
+ def sigma
32
+ @sigma ||= basis.inject({}) do |memo, (k, v)|
33
+ memo[k] = select_value(%{SELECT #{sigma_sql(k, v)} FROM #{kernel.class.quoted_table_name} WHERE #{target_not_null_sql} AND #{basis_not_null_sql}}).to_f
34
+ memo
35
+ end
36
+ end
37
+
38
+ def membership
39
+ return @membership if @membership
40
+ sql = kernel.send(config.membership, basis).dup
41
+ basis.keys.each do |k|
42
+ sql.gsub! ":#{k}_n_w", quote_column_name("#{k}_n_w")
43
+ end
44
+ @membership = sql
45
+ end
46
+
47
+ # In case you want to `cache_method :infer` with https://github.com/seamusabshere/cache_method
48
+ def method_cache_hash
49
+ [kernel.class.name, basis, target, config].hash
50
+ end
51
+
52
+ private
53
+
54
+ def calculate_table!
55
+ return if table_exists?(table_name)
56
+ execute %{CREATE TEMPORARY TABLE #{table_name} AS SELECT * FROM #{kernel.class.quoted_table_name} WHERE #{target_not_null_sql} AND #{basis_not_null_sql}}
57
+ execute %{ALTER TABLE #{table_name} #{weight_create_columns_sql}}
58
+ execute %{ALTER TABLE #{table_name} ADD COLUMN fuzzy_membership FLOAT default null}
59
+ execute %{ALTER TABLE #{table_name} ADD COLUMN fuzzy_weighted_value FLOAT default null}
60
+ execute %{UPDATE #{table_name} SET #{weight_calculate_sql}}
61
+ weight_normalize_frags.each do |sql|
62
+ execute sql
63
+ end
64
+ execute %{UPDATE #{table_name} SET fuzzy_membership = #{membership_sql}}
65
+ execute %{UPDATE #{table_name} SET fuzzy_weighted_value = fuzzy_membership * #{quote_column_name(target)}}
66
+ nil
67
+ end
68
+
69
+ def membership_sql
70
+ if config.weight
71
+ "(#{membership}) * #{quote_column_name(config.weight.to_s)}"
72
+ else
73
+ membership
74
+ end
75
+ end
76
+
77
+ def weight_normalize_frags
78
+ basis.keys.map do |k|
79
+ max = select_value("SELECT MAX(#{quote_column_name("#{k}_w")}) FROM #{table_name}").to_f
80
+ "UPDATE #{table_name} SET #{quote_column_name("#{k}_n_w")} = #{quote_column_name("#{k}_w")} / #{max}"
81
+ end
82
+ end
83
+
84
+ def weight_calculate_sql
85
+ basis.keys.map do |k|
86
+ "#{quote_column_name("#{k}_w")} = 1.0 / (#{sigma[k]}*SQRT(2*PI())) * EXP(-(POW(#{quote_column_name(k)} - #{basis[k]},2))/(2*POW(#{sigma[k]},2)))"
87
+ end.join(', ')
88
+ end
89
+
90
+ def sigma_sql(column_name, value)
91
+ sql = config.sigma.dup
92
+ sql.gsub! ':column', quote_column_name(column_name)
93
+ sql.gsub! ':value', value.to_f.to_s
94
+ sql
95
+ end
96
+
97
+ def table_name
98
+ @table_name ||= "fuzzy_infer_#{Time.now.strftime('%Y_%m_%d_%H_%M_%S')}_#{Kernel.rand(1e11)}"
99
+ end
100
+
101
+ def weight_create_columns_sql
102
+ basis.keys.inject([]) do |memo, k|
103
+ memo << "ADD COLUMN #{quote_column_name("#{k}_w")} FLOAT default null"
104
+ memo << "ADD COLUMN #{quote_column_name("#{k}_n_w")} FLOAT default null"
105
+ memo
106
+ end.flatten.join ', '
107
+ end
108
+
109
+ def basis_not_null_sql
110
+ basis.keys.map do |basis|
111
+ "#{quote_column_name(basis)} IS NOT NULL"
112
+ end.join ' AND '
113
+ end
114
+
115
+ def target_not_null_sql
116
+ "#{quote_column_name(target)} IS NOT NULL"
117
+ end
118
+
119
+ def connection
120
+ kernel.connection
121
+ end
122
+
123
+ delegate :execute, :quote_column_name, :select_value, :table_exists?, :to => :connection
124
+ end
125
+ end
126
+
@@ -0,0 +1,15 @@
1
+ require 'singleton'
2
+
3
+ module FuzzyInfer
4
+ class Registry < ::Hash
5
+ class << self
6
+ def config_for(class_name, target)
7
+ raise %{[fuzzy_infer] Zero machines are defined on #{class_name}.} unless instance.has_key?(class_name)
8
+ raise %{[fuzzy_infer] Target #{target.inspect} is not available on #{class_name}.} unless instance[class_name].has_key?(target)
9
+ instance[class_name][target]
10
+ end
11
+ end
12
+
13
+ include ::Singleton
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module FuzzyInfer
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,83 @@
1
+ require 'bundler/setup'
2
+
3
+ require 'active_record'
4
+ case ENV['DB_ADAPTER']
5
+ when 'postgresql'
6
+ adapter = 'postgresql'
7
+ username = ENV['POSTGRES_USERNAME'] || `whoami`.chomp
8
+ password = ENV['POSTGRES_PASSWORD']
9
+ database = ENV['POSTGRES_DATABASE'] || 'test_fuzzy_infer'
10
+ else
11
+ adapter = 'mysql2'
12
+ database = 'test_fuzzy_infer'
13
+ username = 'root'
14
+ password = 'password'
15
+ end
16
+ config = {
17
+ 'encoding' => 'utf8',
18
+ 'adapter' => adapter,
19
+ 'database' => database,
20
+ }
21
+ config['username'] = username if username
22
+ config['password'] = password if password
23
+ ActiveRecord::Base.establish_connection config
24
+ require 'logger'
25
+ ActiveRecord::Base.logger = Logger.new $stderr
26
+ ActiveRecord::Base.logger.level = Logger::DEBUG
27
+
28
+ require 'earth'
29
+ if ENV['RUN_DATA_MINER'] == 'true'
30
+ Earth.init :hospitality, :load_data_miner => true
31
+ ActiveRecord::Base.logger.level = Logger::INFO
32
+ CommercialBuildingEnergyConsumptionSurveyResponse.run_data_miner!
33
+ $stderr.puts "Done!"
34
+ exit
35
+ end
36
+
37
+ Earth.init :hospitality
38
+
39
+ require 'minitest/spec'
40
+ require 'minitest/autorun'
41
+ require 'minitest/reporters'
42
+ MiniTest::Unit.runner = MiniTest::SuiteRunner.new
43
+ MiniTest::Unit.runner.reporters << MiniTest::Reporters::SpecReporter.new
44
+
45
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
46
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
47
+ require 'fuzzy_infer'
48
+ # class MiniTest::Spec
49
+ # end
50
+
51
+ CBECS = CommercialBuildingEnergyConsumptionSurveyResponse
52
+ class CBECS < ActiveRecord::Base
53
+ fuzzy_infer :target => [:electricity_per_room_night, :natural_gas_per_room_night,
54
+ :fuel_oil_per_room_night, :district_heat_per_room_night], # list of columns that this model is designed to infer
55
+ :basis => [:lodging_rooms, :construction_year, :heating_degree_days,
56
+ :cooling_degree_days, :floors, :ac_coverage], # list of columns that are believed to affect energy use (aka MU)
57
+ :sigma => "(STDDEV_SAMP(:column)/5)+(ABS(AVG(:column)-:value)/3)", # empirically determined formula (SQL!) that captures the desired sample size once all the weights are compiled, across the full range of possible mu values
58
+ :membership => :energy_use_membership, # name of instance method to be called on kernel
59
+ :weight => :weighting # (optional) a pre-existing row weighting, if any, provided by the dataset authors
60
+
61
+ # empirically determined formula that minimizes variance between real and predicted energy use
62
+ # SQL! - :heating_degree_days_n_w will be replaced with, for example, `tmp_table_9301293.hdd_normalized_weight`
63
+ def energy_use_membership(basis)
64
+ keys = basis.keys
65
+
66
+ formula = "(POW(:heating_degree_days_n_w, 0.8) + POW(:cooling_degree_days_n_w, 0.8))"
67
+
68
+ formula += if keys.include? :lodging_rooms and keys.include? :floors
69
+ " * (POW(:lodging_rooms_n_w, 0.8) + POW(:floors_n_w, 0.8))"
70
+ elsif keys.include? :lodging_rooms
71
+ " * POW(:lodging_rooms_n_w, 0.8)"
72
+ elsif keys.include? :floors
73
+ " * POW(:floors_n_w, 0.8)"
74
+ else
75
+ ""
76
+ end
77
+
78
+ formula += " * POW(:percent_cooled_n_w, 0.8)" if keys.include? :percent_cooled
79
+ formula += " * POW(:construction_year_n_w, 0.8)" if keys.include? :construction_year
80
+
81
+ formula
82
+ end
83
+ end
@@ -0,0 +1,58 @@
1
+ # encoding: UTF-8
2
+ require 'helper'
3
+
4
+ describe FuzzyInfer do
5
+ describe FuzzyInfer::ActiveRecordClassMethods do
6
+ it 'adds a way to configure a FuzzyInferenceMachine' do
7
+ CBECS.respond_to?(:fuzzy_infer).must_equal true
8
+ end
9
+ end
10
+
11
+ describe FuzzyInfer::ActiveRecordInstanceMethods do
12
+ it 'adds a way to infer a particular target (field)' do
13
+ CBECS.new.respond_to?(:fuzzy_infer).must_equal true
14
+ end
15
+ it 'adds a way to get a FIM object' do
16
+ CBECS.new.respond_to?(:fuzzy_inference_machine).must_equal true
17
+ end
18
+ it "creates a new FIM object" do
19
+ e = CBECS.new.fuzzy_inference_machine(:electricity_per_room_night)
20
+ e.must_be_instance_of FuzzyInfer::FuzzyInferenceMachine
21
+ end
22
+ end
23
+ # CBECS.new(:heating_degree_days => 2778, :lodging_rooms => 20).fuzzy_infer(:electricity_per_room_night)
24
+
25
+ describe FuzzyInfer::FuzzyInferenceMachine do
26
+ before do
27
+ @kernel = CBECS.new(:heating_degree_days => 2778, :cooling_degree_days => 400, :lodging_rooms => 20, :principal_activity => 'Partying')
28
+ @e = @kernel.fuzzy_inference_machine(:electricity_per_room_night)
29
+ end
30
+ describe '#basis' do
31
+ it "is the union of the kernel's attributes with the basis" do
32
+ @e.basis.must_equal :lodging_rooms => 20, :heating_degree_days => 2778.0, :cooling_degree_days => 400.0
33
+ end
34
+ end
35
+ describe "the temp table" do
36
+ it "excludes rows from the original table where basis or target is nil, but includes rows where they are 0" do
37
+ ActiveRecord::Base.connection.select_value(@e.arel_table.project('COUNT(*)').to_sql).to_f.must_equal 192
38
+ end
39
+ end
40
+ describe '#sigma' do
41
+ it "is calculated from the original table, but only those rows that are also in the temp table" do
42
+ @e.sigma[:heating_degree_days].must_be_close_to 411.9, 0.1
43
+ @e.sigma[:cooling_degree_days].must_be_close_to 267.6, 0.1
44
+ @e.sigma[:lodging_rooms].must_be_close_to 55.0, 0.1
45
+ end
46
+ end
47
+ describe '#membership' do
48
+ it 'depends on the kernel' do
49
+ @e.membership.must_match %r{\(POW\(.heating_degree_days_n_w.,\ 0\.8\)\ \+\ POW\(.cooling_degree_days_n_w.,\ 0\.8\)\)\ \*\ POW\(.lodging_rooms_n_w.,\ 0\.8\)}
50
+ end
51
+ end
52
+ describe '#infer' do
53
+ it 'guesses!' do
54
+ @e.infer.must_be_close_to 17.75, 0.01
55
+ end
56
+ end
57
+ end
58
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fuzzy_infer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Seamus Abshere
9
+ - Ian Hough
10
+ - Matt Kling
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2012-02-21 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: activesupport
18
+ requirement: &2153031100 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ! '>='
22
+ - !ruby/object:Gem::Version
23
+ version: '3'
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: *2153031100
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: &2153029920 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ! '>='
33
+ - !ruby/object:Gem::Version
34
+ version: '3'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: *2153029920
38
+ - !ruby/object:Gem::Dependency
39
+ name: hashie
40
+ requirement: &2153029260 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ type: :runtime
47
+ prerelease: false
48
+ version_requirements: *2153029260
49
+ description: Use fuzzy set analysis to infer missing values. You provide a sigma function,
50
+ a membership function, and a kernel.
51
+ email:
52
+ - seamus@abshere.net
53
+ - ijhough@gmail.com
54
+ - mattkling@gmail.com
55
+ executables: []
56
+ extensions: []
57
+ extra_rdoc_files: []
58
+ files:
59
+ - .gitignore
60
+ - Gemfile
61
+ - LICENSE
62
+ - README.markdown
63
+ - Rakefile
64
+ - fuzzy_infer.gemspec
65
+ - lib/fuzzy_infer.rb
66
+ - lib/fuzzy_infer/active_record_class_methods.rb
67
+ - lib/fuzzy_infer/active_record_instance_methods.rb
68
+ - lib/fuzzy_infer/fuzzy_inference_machine.rb
69
+ - lib/fuzzy_infer/registry.rb
70
+ - lib/fuzzy_infer/version.rb
71
+ - test/helper.rb
72
+ - test/test_fuzzy_infer.rb
73
+ homepage: https://github.com/seamusabshere/fuzzy_infer
74
+ licenses: []
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ! '>='
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ! '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubyforge_project:
93
+ rubygems_version: 1.8.15
94
+ signing_key:
95
+ specification_version: 3
96
+ summary: Use fuzzy set analysis to infer missing values. You provide a sigma function,
97
+ a membership function, and a kernel.
98
+ test_files:
99
+ - test/helper.rb
100
+ - test/test_fuzzy_infer.rb
101
+ has_rdoc: