fuzzy_infer 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,11 @@
1
+ == 0.0.2 / 2012-04-02
2
+
3
+ * Enhancements
4
+
5
+ * Allow inferring multiple values at once, e.g. CBECS.fuzzy_infer(:foo, :bar, :baz) for speed
6
+ * Combine SQL queries where possible for speed
7
+ * Truly unique column names so we don't step on any toes
8
+
9
+ == 0.0.1 / 2012-02-21
10
+
11
+ * Initial release
data/Gemfile CHANGED
@@ -7,4 +7,4 @@ gem 'minitest'
7
7
  gem 'minitest-reporters'
8
8
  gem 'mysql2'
9
9
  gem 'earth'
10
- gem 'pg'
10
+ gem 'pg'
@@ -4,7 +4,79 @@
4
4
 
5
5
  * [Brighter Planet CM1 Impact Estimate web service](http://impact.brighterplanet.com)
6
6
 
7
- ## setup
7
+ ## What it does
8
+
9
+ FuzzyInfer predicts one or more unknown characteristics of an input case by comparing its known characteristics to a reference dataset whose records contain both the known and unknown characteristics. It weights these records according to how closely they match the input case on known characteristics, and then performs a weighted average of the records to predict the unknown characteristics.
10
+
11
+ ## Tuning your parameters
12
+
13
+ As you're iteratively tinkering with the two equations for Sigma and compound weighting scheme, it may be helpful to monitor the effects of various options on the distribution of membership weights, both for each variable and for the final compound weights. It's important to note that the two equations aren't independent, such that they must be developed in tandem to get the desired results across the range of possible input values.
14
+
15
+ #### Determining Sigma:
16
+
17
+ The value of Sigma determines the width of the membership function curve for a given variable, and hence the number of records that will be taken into account. Tuning Sigma to fit your desired results for your application is a subjective process based on how wide a net you want to cast around your input value.
18
+
19
+ Sigma does NOT have to be the same for all variables in the fuzzy analysis -- you can tweak it independently for each variable.
20
+
21
+ Sigma can be coded as either:
22
+
23
+ * A constant.
24
+ * A function of all X values.
25
+ * In this case a reasonable default is to set Sigma equal to the standard deviation of variable X. This ensures that regardless of the range covered by X, your fuzzy membership function will capture a nice subset of the records.
26
+ * A function both of all X values and of Mu.
27
+
28
+ Remember that if compound fuzzy weighting is being employed to analyze multiple variables (see below), the effect of your weight compounding formula on the width of your final net should be accounted for, as it will either expand or contract the net.
29
+
30
+ ##### Case study
31
+
32
+ Brighter Planet's [lodging model](http://impact.brighterplanet.com/models/lodging) uses compound fuzzy inference on six variables in the US EIA CBECS data set to predict hotel energy use.
33
+
34
+ 1. To keep things simple we developed an equation for Sigma that works for all six variables, but we could have tweaked them individually if we'd wanted.
35
+ 2. We started by setting Sigma at `STDDEV_SAMP(x)`.
36
+ 3. For most Mu values this ended up including too many records for our taste, so we experimented with fractions of the standard deviation of x, like `STDDEV_SAMP(x) / 3`.
37
+ 4. This worked ok for Mu values close to the mean, but for Mu values near or beyond the range limits of the CBECS data set where records were much more sparse, it wasn't capturing enough records for a reasonable sample size.
38
+ 5. We decided to develop a formula that increased the value of Sigma (the width of the net) as Mu got farther away from the mean. This would allow us to cast a wide net when dealing with sparse edge cases but a narrow net when Mu is in the densely-populated center of the data set. We accomplished this by incorporating `ABS(AVG(X) - Mu)` into our equation.
39
+ 6. After some more tinkering--including looking at the number of records that would ultimately be highly-weighted for various input combinations after our particular weight compounding formula was applied--we settled on a final formula to use for our Sigma equation for all of our variables: `STDDEV_SAMP(x) / 5 + (ABS(AVG(x) - Mu) / 3)`.
40
+
41
+ #### Determining compound weighting scheme:
42
+
43
+ The compound weighting formula comes into play when performing fuzzy inferences based on more than one known variable.
44
+
45
+ For each record in your reference data set, it takes its calculated normalized membership value for each variable you're using for inference, and consolidates those weights into a single membership value for that record.
46
+
47
+ Note that these starting weights are all between 0 and 1, and that they've been normalized so that the weight for the heaviest record is exactly 1.
48
+
49
+ How you want to determine your final membership weights depends on a couple key questions:
50
+
51
+ * How exclusive you want your fuzzy analysis to be, i.e. how closely a record must match your input case to be worthy of a relatively high weight
52
+ * The relative importance of the variables that are driving your inference scheme, i.e. whether you give more predictive credence to some than others
53
+
54
+ You could do all kinds of fun formulas, but there are two primary operations for compounding variable weights:
55
+
56
+ ###### Addition
57
+ * This is the more inclusive operation, as all records that resemble the input case in at least one respect will be given some weight.
58
+ * Relative variable weights can be incorporated by multiplying the variable's weight by the record's membership value during addition.
59
+
60
+ ###### Multiplication
61
+ * This is the more exclusive operation, as a record that's dissimilar to the input case in any respect will end up with a low final weight.
62
+ * Relative variable weights can be incorporated by raising the record's membership value to the power of that variable's weight during multiplication.
63
+ * Note that since membership values are between 0 and 1, raising them to higher powers produces lower values, such that more important variables should be raised to lesser powers than unimportant variables.
64
+ * Also note that unlike in additive compounding, with multiplicative compounding the absolute (not just relative) values of the variable do affect the final distribution of weights -- raising all your variables to the power of 2 is NOT the same as raising all your variables to the power of 0.5.
65
+
66
+ These two primary operations can be mixed as needed to meet the needs of your particular application. A combination of addition and multiplication may make sense for many data sets.
67
+
68
+ ##### Case study
69
+
70
+ Brighter Planet's lodging model uses compound fuzzy inference to predict hotel energy use based on six variables in the CBECS data set: number of rooms, number of floors, heating degree days, cooling degree days, construction year, and percent air conditioned.
71
+
72
+ 1. We wanted to perform our inference based only on records that closely matched our input case for most of these variables, so we started with a straight multiplication scheme rather than addition.
73
+ 2. We recognized that number of rooms and number of floors would be highly correlated, as would number of heating and cooling degree days, so we didn't want to treat them independently. For these paired variables, we also wanted records that were similar in one but not both respects to retain some, albeit lesser, weight.
74
+ 3. We wanted to weight climate and hotel size more heavily than construction year and air conditioning.
75
+ 4. We settled on the equation `(POW(rooms, 0.8) + POW(floors, 0.8)) * (POW(hdd, 0.8) + POW(cdd, 0.8)) * POW(year, 0.8) * POW(ac, 0.8)`
76
+ 5. This effectively weights hotel size and climate more heavily than the other variables, because the addition allows weights for these variables to range up to 2 whereas the other two variables have max weights of 1.
77
+ 6. It also allows a record to be dissimilar to the input case on half of a variable pair without being disqualified, provided it's a close match on the other half.
78
+
79
+ ## Setup
8
80
 
9
81
  1) gem install a bleeding edge earth
10
82
 
@@ -21,11 +93,11 @@
21
93
 
22
94
  RUN_DATA_MINER=true rake
23
95
 
24
- ## further testing
96
+ ## Further testing
25
97
 
26
98
  rake
27
99
 
28
- ## future plans
100
+ ## Future plans
29
101
 
30
102
  **in the future the fuzzy inference machine will make TEMPORARY tables, rather than gum up your db**
31
103
 
@@ -6,10 +6,8 @@ module FuzzyInfer
6
6
  # see test/helper.rb for an example
7
7
  def fuzzy_infer(options = {})
8
8
  options = ::Hashie::Mash.new options
9
- options.target.each do |target|
10
- Registry.instance[name] ||= {}
11
- Registry.instance[name][target] = options
12
- end
9
+ Registry.instance[name] ||= {}
10
+ Registry.instance[name][options.target] = options
13
11
  end
14
12
  end
15
13
  end
@@ -1,14 +1,13 @@
1
1
  module FuzzyInfer
2
2
  module ActiveRecordInstanceMethods
3
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)
4
+ def fuzzy_inference_machine(*targets)
5
+ FuzzyInferenceMachine.new self, targets, Registry.config_for(self.class.name, targets)
7
6
  end
8
7
 
9
8
  # Shortcut to creating a FIM and immediately calling it
10
- def fuzzy_infer(target)
11
- fuzzy_inference_machine(target).infer
9
+ def fuzzy_infer(*targets)
10
+ fuzzy_inference_machine(*targets).infer
12
11
  end
13
12
  end
14
13
  end
@@ -1,126 +1,162 @@
1
1
  module FuzzyInfer
2
2
  class FuzzyInferenceMachine
3
-
3
+ MYSQL_ADAPTER_NAME = /mysql/i
4
+
4
5
  attr_reader :kernel
5
- attr_reader :target
6
+ attr_reader :targets
6
7
  attr_reader :config
7
8
 
8
- def initialize(kernel, target, config)
9
+ delegate :execute, :quote_column_name, :to => :connection
10
+
11
+ def initialize(kernel, targets, config)
9
12
  @kernel = kernel
10
- @target = target
13
+ @targets = targets
11
14
  @config = config
12
15
  end
13
-
16
+
14
17
  def infer
15
18
  calculate_table!
16
- retval = select_value(%{SELECT SUM(fuzzy_weighted_value)/SUM(fuzzy_membership) FROM #{table_name}}).to_f
19
+ pieces = targets.map do |target|
20
+ "SUM(#{qc(target, :v)})/SUM(#{qc(:fuzzy_membership)})"
21
+ end
22
+ values = connection.select_rows(%{SELECT #{pieces.join(', ')} FROM #{table_name}}).first.map do |value|
23
+ value.nil? ? nil : value.to_f
24
+ end
17
25
  execute %{DROP TABLE #{table_name}}
18
- retval
26
+ if targets.length == 1
27
+ return values.first
28
+ else
29
+ return *values
30
+ end
19
31
  end
20
-
32
+
21
33
  # TODO technically I could use this to generate the SQL
22
34
  def arel_table
23
35
  calculate_table!
24
36
  Arel::Table.new table_name
25
37
  end
26
-
38
+
27
39
  def basis
28
40
  @basis ||= kernel.attributes.symbolize_keys.slice(*config.basis).reject { |k, v| v.nil? }
29
41
  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
-
42
+
38
43
  def membership
39
44
  return @membership if @membership
40
45
  sql = kernel.send(config.membership, basis).dup
41
46
  basis.keys.each do |k|
42
- sql.gsub! ":#{k}_n_w", quote_column_name("#{k}_n_w")
47
+ sql.gsub! ":#{k}_n_w", qc(k, :n_w)
43
48
  end
44
49
  @membership = sql
45
50
  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
-
51
+
52
52
  private
53
-
53
+
54
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)}}
55
+ return if connection.table_exists?(table_name)
56
+ mysql = connection.adapter_name =~ MYSQL_ADAPTER_NAME
57
+ execute %{CREATE TEMPORARY TABLE #{table_name} #{'ENGINE=MEMORY' if mysql} AS SELECT * FROM #{kernel.class.quoted_table_name} WHERE #{all_targets_not_null_condition} AND #{basis_not_null_condition}}
58
+ execute %{ALTER TABLE #{table_name} #{additional_column_definitions.join(', ')}}
59
+ execute %{ANALYZE #{'TABLE' if mysql} #{table_name}}
60
+ execute %{UPDATE #{table_name} SET #{weight_calculators.join(', ')}}
61
+ execute %{UPDATE #{table_name} SET #{weight_normalizers.join(', ')}}
62
+ execute %{UPDATE #{table_name} SET #{membership_setter}}
63
+ execute %{UPDATE #{table_name} SET #{target_setters.join(', ')}}
66
64
  nil
67
65
  end
68
-
69
- def membership_sql
70
- if config.weight
66
+
67
+ def membership_setter
68
+ right = if config.weight
71
69
  "(#{membership}) * #{quote_column_name(config.weight.to_s)}"
72
70
  else
73
71
  membership
74
72
  end
73
+ "#{qc(:fuzzy_membership)} = #{right}"
75
74
  end
76
-
77
- def weight_normalize_frags
75
+
76
+ def target_setters
77
+ targets.map do |target|
78
+ %{#{qc(target, :v)} = #{qc(:fuzzy_membership)} * #{quote_column_name(target)}}
79
+ end
80
+ end
81
+
82
+ def weight_calculators
78
83
  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}"
84
+ "#{qc(k, :w)} = 1.0 / (#{sigma[k]}*SQRT(2*PI())) * EXP(-(POW(#{quote_column_name(k)} - #{basis[k]},2))/(2*POW(#{sigma[k]},2)))"
81
85
  end
82
86
  end
83
-
84
- def weight_calculate_sql
87
+
88
+ def weight_normalizers
89
+ max_exprs = basis.keys.map do |k|
90
+ "MAX(#{qc(k, :w)}) AS #{qc(k, :w_max)}"
91
+ end
92
+ maxes = connection.select_one("SELECT #{max_exprs.join(', ')} FROM #{table_name}")
85
93
  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(', ')
94
+ "#{qc(k, :n_w)} = #{qc(k, :w)} / #{maxes[c(k, :w_max)]}"
95
+ end
88
96
  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
97
+
98
+ def sigma
99
+ @sigma ||= begin
100
+ exprs = basis.map do |column_name, kernel_value|
101
+ sql = "#{config.sigma} AS #{qc(column_name)}"
102
+ sql.gsub! ':column', quote_column_name(column_name)
103
+ sql.gsub! ':value', kernel_value.to_f.to_s
104
+ sql
105
+ end
106
+ row = connection.select_one(%{SELECT #{exprs.join(', ')} FROM #{kernel.class.quoted_table_name} WHERE #{all_targets_not_null_condition} AND #{basis_not_null_condition}})
107
+ basis.inject({}) do |memo, (column_name, _)|
108
+ memo[column_name] = row[c(column_name)].to_f
109
+ memo
110
+ end
111
+ end
95
112
  end
96
-
113
+
114
+ def randomness
115
+ @randomness ||= [Time.now.strftime('%H%M%S'), Kernel.rand(1e5)].join('_')
116
+ end
117
+
97
118
  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
119
+ @table_name ||= "fuzzy_infer_#{randomness}"
120
+ end
121
+
122
+ def additional_column_definitions
123
+ cols = []
124
+ cols << "ADD COLUMN #{qc(:fuzzy_membership)} FLOAT DEFAULT NULL"
125
+ basis.keys.each do |k|
126
+ cols << "ADD COLUMN #{qc(k, :w)} FLOAT DEFAULT NULL"
127
+ cols << "ADD COLUMN #{qc(k, :n_w)} FLOAT DEFAULT NULL"
128
+ end
129
+ targets.each do |target|
130
+ cols << "ADD COLUMN #{qc(target, :v)} FLOAT DEFAULT NULL"
131
+ end
132
+ cols
133
+ end
134
+
135
+ def basis_not_null_condition
110
136
  basis.keys.map do |basis|
111
137
  "#{quote_column_name(basis)} IS NOT NULL"
112
138
  end.join ' AND '
113
139
  end
114
-
115
- def target_not_null_sql
116
- "#{quote_column_name(target)} IS NOT NULL"
140
+
141
+ def all_targets_not_null_condition
142
+ [config.target].flatten.map do |target|
143
+ "#{quote_column_name(target)} IS NOT NULL"
144
+ end.join ' AND '
117
145
  end
118
-
146
+
119
147
  def connection
120
148
  kernel.connection
121
149
  end
122
-
123
- delegate :execute, :quote_column_name, :select_value, :table_exists?, :to => :connection
150
+
151
+ # quoted version of #c
152
+ def qc(column_name, suffix = nil)
153
+ quote_column_name c(column_name, suffix)
154
+ end
155
+
156
+ # column name that won't step on anybody's toes
157
+ def c(column_name, suffix = nil)
158
+ [column_name, suffix, randomness].compact.join '_'
159
+ end
124
160
  end
125
161
  end
126
162
 
@@ -3,10 +3,12 @@ require 'singleton'
3
3
  module FuzzyInfer
4
4
  class Registry < ::Hash
5
5
  class << self
6
- def config_for(class_name, target)
6
+ def config_for(class_name, targets)
7
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]
8
+ unless k_v = instance[class_name].detect { |k, _| (targets & k) == targets }
9
+ raise %{[fuzzy_infer] Target #{targets.inspect} is not available on #{class_name}.}
10
+ end
11
+ k_v.last
10
12
  end
11
13
  end
12
14
 
@@ -1,3 +1,3 @@
1
1
  module FuzzyInfer
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -1,5 +1,8 @@
1
+ require 'rubygems'
1
2
  require 'bundler/setup'
2
3
 
4
+ require 'benchmark'
5
+
3
6
  require 'active_record'
4
7
  case ENV['DB_ADAPTER']
5
8
  when 'postgresql'
@@ -39,14 +39,14 @@ describe FuzzyInfer do
39
39
  end
40
40
  describe '#sigma' do
41
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
42
+ @e.send(:sigma)[:heating_degree_days].must_be_close_to 411.9, 0.1
43
+ @e.send(:sigma)[:cooling_degree_days].must_be_close_to 267.6, 0.1
44
+ @e.send(:sigma)[:lodging_rooms].must_be_close_to 55.0, 0.1
45
45
  end
46
46
  end
47
47
  describe '#membership' do
48
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\)}
49
+ @e.membership.must_match %r{\(POW\(.heating_degree_days_n_w_\d+_\d+.,\ 0\.8\)\ \+\ POW\(.cooling_degree_days_n_w_\d+_\d+.,\ 0\.8\)\)\ \*\ POW\(.lodging_rooms_n_w_\d+_\d+.,\ 0\.8\)}
50
50
  end
51
51
  end
52
52
  describe '#infer' do
@@ -54,5 +54,27 @@ describe FuzzyInfer do
54
54
  @e.infer.must_be_close_to 17.75, 0.01
55
55
  end
56
56
  end
57
+ describe 'optimizations' do
58
+ it "can run multiple numbers at once" do
59
+ # dry run
60
+ @kernel.fuzzy_infer :electricity_per_room_night
61
+ @kernel.fuzzy_infer :natural_gas_per_room_night
62
+ @kernel.fuzzy_infer :fuel_oil_per_room_night
63
+ # end
64
+ e1 = n1 = f1 = e2 = n2 = f2 = nil
65
+ uncached_time = Benchmark.realtime do
66
+ e1 = @kernel.fuzzy_infer :electricity_per_room_night
67
+ n1 = @kernel.fuzzy_infer :natural_gas_per_room_night
68
+ f1 = @kernel.fuzzy_infer :fuel_oil_per_room_night
69
+ end
70
+ cached_time = Benchmark.realtime do
71
+ e2, n2, f2 = @kernel.fuzzy_infer :electricity_per_room_night, :natural_gas_per_room_night, :fuel_oil_per_room_night
72
+ end
73
+ (uncached_time / cached_time).must_be :>, 2
74
+ e2.must_equal e1
75
+ n2.must_equal n1
76
+ f2.must_equal f1
77
+ end
78
+ end
57
79
  end
58
80
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fuzzy_infer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,11 +11,11 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2012-02-21 00:00:00.000000000 Z
14
+ date: 2012-04-02 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activesupport
18
- requirement: &2153031100 !ruby/object:Gem::Requirement
18
+ requirement: &2153823060 !ruby/object:Gem::Requirement
19
19
  none: false
20
20
  requirements:
21
21
  - - ! '>='
@@ -23,10 +23,10 @@ dependencies:
23
23
  version: '3'
24
24
  type: :runtime
25
25
  prerelease: false
26
- version_requirements: *2153031100
26
+ version_requirements: *2153823060
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activerecord
29
- requirement: &2153029920 !ruby/object:Gem::Requirement
29
+ requirement: &2153822420 !ruby/object:Gem::Requirement
30
30
  none: false
31
31
  requirements:
32
32
  - - ! '>='
@@ -34,10 +34,10 @@ dependencies:
34
34
  version: '3'
35
35
  type: :runtime
36
36
  prerelease: false
37
- version_requirements: *2153029920
37
+ version_requirements: *2153822420
38
38
  - !ruby/object:Gem::Dependency
39
39
  name: hashie
40
- requirement: &2153029260 !ruby/object:Gem::Requirement
40
+ requirement: &2153821960 !ruby/object:Gem::Requirement
41
41
  none: false
42
42
  requirements:
43
43
  - - ! '>='
@@ -45,7 +45,7 @@ dependencies:
45
45
  version: '0'
46
46
  type: :runtime
47
47
  prerelease: false
48
- version_requirements: *2153029260
48
+ version_requirements: *2153821960
49
49
  description: Use fuzzy set analysis to infer missing values. You provide a sigma function,
50
50
  a membership function, and a kernel.
51
51
  email:
@@ -57,6 +57,7 @@ extensions: []
57
57
  extra_rdoc_files: []
58
58
  files:
59
59
  - .gitignore
60
+ - CHANGELOG
60
61
  - Gemfile
61
62
  - LICENSE
62
63
  - README.markdown