fuzzy_infer 0.0.1

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