evoc 3.5.0

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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +21 -0
  7. data/Makefile +4 -0
  8. data/README.md +61 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/evoc +3 -0
  12. data/bin/setup +7 -0
  13. data/evoc.gemspec +30 -0
  14. data/lib/evoc/algorithm.rb +147 -0
  15. data/lib/evoc/algorithms/top_k.rb +86 -0
  16. data/lib/evoc/analyze.rb +395 -0
  17. data/lib/evoc/array.rb +43 -0
  18. data/lib/evoc/evaluate.rb +109 -0
  19. data/lib/evoc/exceptions/aggregation_error.rb +6 -0
  20. data/lib/evoc/exceptions/expectedoutcome_nil_or_empty.rb +6 -0
  21. data/lib/evoc/exceptions/measure_calculation_error.rb +6 -0
  22. data/lib/evoc/exceptions/no_changed_items_in_changes.rb +6 -0
  23. data/lib/evoc/exceptions/no_changes_in_json_object.rb +6 -0
  24. data/lib/evoc/exceptions/no_date_in_json_object.rb +6 -0
  25. data/lib/evoc/exceptions/no_result.rb +6 -0
  26. data/lib/evoc/exceptions/non_finite.rb +8 -0
  27. data/lib/evoc/exceptions/non_numeric.rb +8 -0
  28. data/lib/evoc/exceptions/not_a_query.rb +6 -0
  29. data/lib/evoc/exceptions/not_a_result.rb +6 -0
  30. data/lib/evoc/exceptions/not_a_transaction.rb +6 -0
  31. data/lib/evoc/exceptions/not_initialized.rb +6 -0
  32. data/lib/evoc/exceptions/only_nil_in_changes.rb +6 -0
  33. data/lib/evoc/exceptions/query_nil_or_empty.rb +6 -0
  34. data/lib/evoc/exceptions/unable_to_convert_json_to_tx.rb +6 -0
  35. data/lib/evoc/experiment.rb +239 -0
  36. data/lib/evoc/hash.rb +56 -0
  37. data/lib/evoc/history_store.rb +53 -0
  38. data/lib/evoc/hyper_rule.rb +53 -0
  39. data/lib/evoc/interestingness_measure.rb +77 -0
  40. data/lib/evoc/interestingness_measure_aggregator.rb +147 -0
  41. data/lib/evoc/interestingness_measures.rb +882 -0
  42. data/lib/evoc/logger.rb +34 -0
  43. data/lib/evoc/memory_profiler.rb +43 -0
  44. data/lib/evoc/recommendation_cache.rb +152 -0
  45. data/lib/evoc/rule.rb +32 -0
  46. data/lib/evoc/rule_store.rb +340 -0
  47. data/lib/evoc/scenario.rb +303 -0
  48. data/lib/evoc/svd.rb +124 -0
  49. data/lib/evoc/tx.rb +34 -0
  50. data/lib/evoc/tx_store.rb +379 -0
  51. data/lib/evoc/version.rb +3 -0
  52. data/lib/evoc.rb +4 -0
  53. data/lib/evoc_cli/analyze.rb +198 -0
  54. data/lib/evoc_cli/cli_helper.rb +1 -0
  55. data/lib/evoc_cli/experiment.rb +78 -0
  56. data/lib/evoc_cli/info.rb +22 -0
  57. data/lib/evoc_cli/main.rb +29 -0
  58. data/lib/evoc_cli/util.rb +36 -0
  59. data/lib/evoc_helper.rb +40 -0
  60. data/mem_profiler/Gemfile.lock +39 -0
  61. data/mem_profiler/README.md +126 -0
  62. data/mem_profiler/createdb.rb +4 -0
  63. data/mem_profiler/db.rb +82 -0
  64. data/mem_profiler/gemfile +6 -0
  65. data/mem_profiler/gencsv.rb +64 -0
  66. data/mem_profiler/genimport.sh +8 -0
  67. data/mem_profiler/graph.rb +91 -0
  68. metadata +251 -0
@@ -0,0 +1,34 @@
1
+ # enable logging in classes through 'include Logging'
2
+ module Logging
3
+ def logger
4
+ @logger ||= Logging.logger_for(self.class.name)
5
+ end
6
+
7
+ # Use a hash class-ivar to cache a unique Logger per class:
8
+ @loggers = {}
9
+ @logger_level = 'info'
10
+
11
+ class << self
12
+ def logger_for(classname)
13
+ @loggers[classname] ||= configure_logger_for(classname)
14
+ end
15
+
16
+ def configure_logger_for(classname)
17
+ logger = Logger.new('evoc.log','daily')
18
+ logger.progname = classname
19
+ logger.level = const_get('Logger::'+@logger_level.upcase)
20
+ logger
21
+ end
22
+
23
+ def set_level(level)
24
+ possible_levels = %w(debug info warn error info)
25
+ if possible_levels.include?(level)
26
+ STDERR.puts "Logging level has been set to '#{level}' for output to evoc.log"
27
+ @loggers.each {|l| l.level = const_get('Logger::'+level.upcase)}
28
+ @logger_level = level
29
+ else
30
+ STDERR.puts "Unable to set logger level to #{level}, possible values are #{possible_levels}. Defaulting to 'info'."
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ require 'objspace'
2
+
3
+ module Kernel
4
+ def tick_every sec, &pr
5
+ Thread.new do loop do
6
+ pr.call
7
+ t = Time.now.to_f
8
+ frac = t.modulo(sec.to_f)
9
+ sleep(sec - frac)
10
+ end end
11
+ end
12
+ end
13
+
14
+ module MemoryProfiler
15
+
16
+ @@thread = nil
17
+
18
+ ##
19
+ # @param [string] tag tag to add to dumped files
20
+ # @oaram [int] n dump every n seconds
21
+ def self.start(tag,n)
22
+ dump_dir = 'mem_dumps'
23
+ index = "#{dump_dir}/#{tag}-index.txt"
24
+ ObjectSpace.trace_object_allocations_start
25
+ if !Dir.exists?(dump_dir)
26
+ Dir.mkdir dump_dir
27
+ end
28
+ File.open(index,"w")
29
+ @@thread = tick_every(n) do
30
+ GC.start
31
+ i = Time.now.strftime('%Y-%m-%dT%H:%M:%S')
32
+ dump = "#{tag}-#{i}.dump"
33
+ dump_path = "#{dump_dir}/#{dump}"
34
+ ObjectSpace.dump_all(output: open(dump_path, "w"))
35
+ File.open(index,'a') {|index| index.puts "#{dump_path},#{i}" }
36
+ end
37
+ end
38
+
39
+ def self.stop
40
+ @@thread.kill
41
+ end
42
+
43
+ end
@@ -0,0 +1,152 @@
1
+ module Evoc
2
+ class RecommendationCache
3
+ extend Logging
4
+
5
+ # create accessors for class level instance variables
6
+ #
7
+ # tag: the string representation of the currently cached recommendation
8
+ # recommendation: the currently cached recommendation
9
+ # time: the time it took to generate the currently cached recommendation
10
+ # model_size: the number of transactions used when generating the currently cached recommendation
11
+ class << self
12
+ attr_accessor :tag, :base_recommendation, :last_recommendation, :time_rulegeneration, :time_measurecalculation, :time_aggregation, :time_evaluation, :filtered_model_size, :evaluation
13
+ end
14
+
15
+ def self.recommendation_cached?(algorithm:,
16
+ query:,
17
+ model_start:,
18
+ model_end:,
19
+ max_size: nil)
20
+ return self.tag == [algorithm,query,model_start,model_end,max_size].hash
21
+ end
22
+
23
+ def self.get_recommendation(algorithm:,
24
+ query:,
25
+ model_start:,
26
+ model_end:,
27
+ max_size: nil,
28
+ aggregator: nil,
29
+ measures: [])
30
+ # check if a new base recommendation needs to be generated
31
+ tag = [algorithm,query,model_start,model_end,max_size].hash
32
+ if self.tag != tag
33
+ # clear out any evaluation done
34
+ self.evaluation = Hash.new
35
+ # new recommendation
36
+ logger.debug "Caching new recommendation: algorithm: #{algorithm}, query: #{query}, model_start/end: #{model_start} - #{model_end}, maxsize: #{max_size}"
37
+ self.tag = tag
38
+ tx_store = Evoc::HistoryStore.get_history(model_start,
39
+ model_end,
40
+ max_size)
41
+ self.filtered_model_size = tx_store.size
42
+
43
+ t1 = Time.new
44
+ self.base_recommendation = Evoc::Algorithm.execute(tx_store: tx_store,
45
+ query: query,
46
+ algorithm: algorithm)
47
+ self.last_recommendation = self.base_recommendation
48
+ t2 = Time.new
49
+ self.time_rulegeneration = TimeDifference.between(t1,t2).in_seconds.round(8)
50
+ end
51
+
52
+ # calculate measures on rules
53
+ t1 = Time.new
54
+ self.base_recommendation.calculate_measures(measures)
55
+ t2 = Time.new
56
+ self.time_measurecalculation = TimeDifference.between(t1,t2).in_seconds.round(8)
57
+
58
+ # perform aggregation
59
+ if !aggregator.nil?
60
+ t1 = Time.new
61
+ self.last_recommendation = self.base_recommendation.aggregate_by(aggregator: aggregator.to_sym,measures: measures) {|r| r.rhs}
62
+ t2 = Time.new
63
+ self.time_aggregation = TimeDifference.between(t1,t2).in_seconds.round(8)
64
+ else
65
+ self.last_recommendation = self.base_recommendation
66
+ end
67
+ return self.last_recommendation
68
+ end
69
+
70
+ ##
71
+ # Evaluate the currently cached recommendation
72
+ #
73
+ # @param [Array<String>] evaluators the evaluators to apply
74
+ # @param [Array<String>] expected_outcome the expected outcome to use in evaluations
75
+ # @param [Array<String>] measure_combinations the list of measures to use when sorting a recommendation before evaluating
76
+ #
77
+ # @return [Hash[aggregator][evaluator][result]] the hash of results
78
+ def self.evaluate(evaluators: ,expected_outcome:,measure_combination: )
79
+ if !self.last_recommendation.nil?
80
+ t1 = Time.new
81
+ evaluators.each do |evaluator|
82
+ self.evaluation[evaluator] = self.last_recommendation.evaluate_with(evaluator: evaluator,expected_outcome: expected_outcome,measure_combination: measure_combination)
83
+ end
84
+ t2 = Time.new
85
+ self.time_evaluation = TimeDifference.between(t1,t2).in_seconds.round(8)
86
+ else
87
+ STDERR.puts "TAG = #{self.tag}No recommendation to evaluate"
88
+ end
89
+ end
90
+
91
+ ##
92
+ # format:
93
+ # {
94
+ # time: 'execution time',
95
+ # filtered_model_size:
96
+ # number_of_rules :
97
+ # evaluation: {
98
+ # average_precision: ..,
99
+ # ..next evaluator..
100
+ # }
101
+ # rules: [
102
+ # {
103
+ # lhs: [lhs]
104
+ # rhs: [rhs],
105
+ # measures: {
106
+ # measure_1: value,
107
+ # measure_n: value
108
+ # }
109
+ # },
110
+ # ..next rule..
111
+ # ]
112
+ # }
113
+ #
114
+ # measures: the interestingness measures that you want to output in the hash
115
+ def self.to_h(measures: Evoc::Rule.measures)
116
+ recommendation_hash = Hash.new
117
+ recommendation_hash[:recommendation_tag] = self.tag
118
+ recommendation_hash[:time_rulegeneration] = self.time_rulegeneration
119
+ recommendation_hash[:time_measurecalculation] = self.time_measurecalculation
120
+ recommendation_hash[:time_aggregation] = self.time_aggregation
121
+ recommendation_hash[:time_evaluation] = self.time_evaluation
122
+ recommendation_hash[:filtered_model_size] = self.filtered_model_size
123
+ recommendation_hash[:number_of_baserules] = self.base_recommendation.size
124
+ recommendation_hash[:number_of_rules] = self.last_recommendation.size
125
+ recommendation_hash[:aggregator] = self.last_recommendation.aggregator
126
+ recommendation_hash[:number_of_hyper_rules] = self.last_recommendation.number_of_hyper_rules
127
+ recommendation_hash[:mean_hyper_coefficient] = self.last_recommendation.inject(0.0){ |sum, r|
128
+ sum + r.get_measure('m_hyper_coefficient').value } / self.last_recommendation.size
129
+ recommendation_hash[:largest_antecedent] = self.last_recommendation.largest_antecedent
130
+ if !self.evaluation.nil?
131
+ self.evaluation.each do |evaluator,value|
132
+ recommendation_hash[evaluator.to_sym] = value[:value]
133
+ recommendation_hash[:unique_consequents] = value[:unique_consequents]
134
+ end
135
+ end
136
+ recommendation_hash[:rules] = []
137
+ self.last_recommendation.each do |rule|
138
+ rule_hash = Hash.new
139
+ rule_hash[:lhs] = rule.lhs.is_a?(String) ? rule.lhs : rule.lhs.join(',')
140
+ rule_hash[:rhs] = rule.rhs.is_a?(String) ? rule.rhs : rule.rhs.join(',')
141
+ rule_hash[:measures] = Hash.new
142
+ measures.each do |m|
143
+ if rule.measure_instantiated?(m)
144
+ rule_hash[:measures][m] = rule.get_measure(m).value
145
+ end
146
+ end
147
+ recommendation_hash[:rules] << rule_hash
148
+ end
149
+ return recommendation_hash
150
+ end
151
+ end
152
+ end
data/lib/evoc/rule.rb ADDED
@@ -0,0 +1,32 @@
1
+ module Evoc
2
+ class Rule
3
+ include InterestingnessMeasures, Logging, Comparable
4
+ attr_accessor :lhs, :rhs, :name, :tx_store
5
+
6
+ def initialize(lhs:,rhs:,tx_store: nil,**measures)
7
+ self.lhs = lhs.is_a?(Array) ? lhs.sort : [lhs]
8
+ self.rhs = rhs.is_a?(Array) ? rhs.sort : [rhs]
9
+ self.name = "#{self.lhs.join(",")} -> #{self.rhs.join(",")}"
10
+ self.tx_store = tx_store
11
+ measures.each do |measure,value|
12
+ set_measure(measure,value)
13
+ end
14
+ end
15
+
16
+ def <=> other
17
+ other.name <=> self.name
18
+ end
19
+
20
+ def to_s
21
+ name
22
+ end
23
+
24
+ def lhs=input
25
+ input.is_a?(Array) ? @lhs = input : @lhs = [input]
26
+ end
27
+
28
+ def rhs=input
29
+ input.is_a?(Array) ? @rhs = input : @rhs = [input]
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,340 @@
1
+ module Evoc
2
+ class RuleStore
3
+ include Enumerable, Logging
4
+ attr_accessor :query, :rules, :aggregator
5
+
6
+ def initialize(rules = [],query: nil,aggregator: nil)
7
+ self.rules = rules
8
+ self.query = query
9
+ self.aggregator = aggregator
10
+ end
11
+
12
+ ##
13
+ # CLASS METHODS
14
+ ##
15
+
16
+ def self.parse_file(path_to_rules)
17
+ rule_store = Evoc::RuleStore.new
18
+ CSV.foreach(path_to_rules, :headers => true) do |row|
19
+ params = row.to_h.symbolize_keys.convert_values(except: [:lhs,:rhs], converter: Evoc::InterestingnessMeasures::VALUE_TYPE)
20
+ rule = Evoc::Rule.new(params)
21
+ rule_store << rule
22
+ end
23
+ rule_store
24
+ end
25
+
26
+ def self.parse_string(string)
27
+ rule_store = Evoc::RuleStore.new
28
+ CSV.parse(string, :headers => true) do |row|
29
+ params = row.to_h.symbolize_keys.convert_values(except: [:lhs,:rhs], converter: Evoc::InterestingnessMeasures::VALUE_TYPE)
30
+ rule = Evoc::Rule.new(params)
31
+ rule_store << rule
32
+ end
33
+ rule_store
34
+ end
35
+
36
+ ##
37
+ # \CLASS METHODS
38
+ ##
39
+
40
+ # required by Enumerable
41
+ def each &block
42
+ @rules.each do |rule|
43
+ if block_given?
44
+ block.call rule
45
+ else
46
+ yield rule
47
+ end
48
+ end
49
+ end
50
+
51
+ def [] index
52
+ @rules[index]
53
+ end
54
+
55
+ def []=(index,value)
56
+ @rules[index] = value
57
+ end
58
+
59
+ def group_by(&block)
60
+ res = Hash.new { |hash, key| hash[key] = [] }
61
+ each do |e|
62
+ res[block.call(e)] << e
63
+ end
64
+ res
65
+ end
66
+
67
+ ##
68
+ # Calculates the requested measures on the current rule set
69
+ # @param measures [Array<Symbol>] the set of measures to calculate
70
+ def calculate_measures(measures)
71
+ if measures.nil?
72
+ raise ArgumentError.new, "Tried to calculate measures, but list of measures was 'nil'"
73
+ else
74
+ self.each do |rule|
75
+ measures.each do |m|
76
+ rule.get_measure(m)
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ ##
83
+ # Aggregates the current set of rules using the given aggregator over the rule clusters specified by the given block
84
+ #
85
+ # @param: [Symbol] aggregator the name of a defined aggregator function
86
+ # @param: [Array<String>] measures the measures to aggregate
87
+ # @param: [block] define the rules clusters which should be aggregated
88
+ def aggregate_by(aggregator: ,measures:,&block)
89
+ rule_clusters = group_by(&block)
90
+ # remove clusters with only one item
91
+ #aggregatable_rules = rule_clusters.select {|g,cluster| cluster.size > 1}
92
+ #non_aggregatable_rules = rule_clusters.select {|g,cluster| cluster.size == 1}
93
+ aggregation = Evoc::RuleStore.new(query: self.query, aggregator: aggregator)
94
+ # aggregate the rules in each cluster
95
+ rule_clusters.each do |_,cluster|
96
+ # we create one aggregated rule from each rule cluster
97
+ if cluster.size > 1
98
+ aggregation << Evoc::HyperRule.new(cluster,aggregator,measures)
99
+ else
100
+ aggregation << cluster.first
101
+ end
102
+ end
103
+ return aggregation
104
+ end
105
+
106
+ ##
107
+ # @return the number of hyper rules in this store
108
+ def number_of_hyper_rules
109
+ self.hyper_rules.size
110
+ end
111
+
112
+ ##
113
+ # @return the hyper rules in the store
114
+ def hyper_rules
115
+ self.select {|r| r.is_a?(Evoc::HyperRule)}
116
+ end
117
+
118
+
119
+ ##
120
+ # Evaluate this recommendation using the given evaluator
121
+ #
122
+ # Note that the hyper coefficient is added as a last tie breaker for
123
+ # aggregation functions called with '*aggregator*_hc'
124
+ # Not pretty, sorry..
125
+ #
126
+ # @param [String] evaluator the method to use for evaluating
127
+ # @param [Array] expected_outcome the list of items to evaluate against
128
+ # @param [Array] measure_combination the list of measures used to first sort the recommendation
129
+ def evaluate_with(evaluator: :average_precision,expected_outcome:,measure_combination: )
130
+ if measure_combination.empty? then raise ArgumentError, "Cannot evalute a recommendation without specifying which measures to rank on" end
131
+ result = Hash.new
132
+ logger.debug "#{__method__} params: evaluator: #{evaluator}, measure_combination: #{measure_combination}"
133
+ # sort the rules on each combination and evaluate
134
+ sorted_rules = []
135
+ if self.aggregator =~ /_hc\z/
136
+ sorted_rules = self.sort_on(measures: measure_combination+['m_hyper_coefficient'])
137
+ elsif !self.aggregator.nil?
138
+ sorted_rules = self.sort_on(measures: measure_combination)
139
+ else
140
+ # not an aggregation
141
+ # get the strongest unique rules
142
+ unique_rules = self.unique_by(measure_combination.first)
143
+ sorted_rules = self.sort_on(rules: unique_rules,measures: measure_combination)
144
+ end
145
+ # get the recommended items
146
+ recommendation = sorted_rules.map(&:rhs)
147
+ # evaluate the sorted list against the expected outcome
148
+ result[:value] = Evoc::Evaluate.execute(recommendation,expected_outcome,evaluator)
149
+ return result
150
+ end
151
+
152
+ ##
153
+ # Sort rules on one or more measures
154
+ # If a measure is undefined/nil for a rule, we treat it as -infinity
155
+ # for purposes of sorting
156
+ # @param: [Array<String>] measures the list of measures to sort by
157
+ def sort_on(rules: self, measures:)
158
+ rules.sort_by {|r| measures.map {|m| r.get_measure(m).value.nil? ? Float::INFINITY : -r.get_measure(m)}}
159
+ end
160
+
161
+ ##
162
+ # returns the set of unique consequents
163
+ # where each consequent is the strongest given by the input measure
164
+ #
165
+ # @param: [String] measure the measure used to find the strongest rules
166
+ def unique_by(measure)
167
+ selected_rules = Evoc::Env::GOOGLE_HASH ? GoogleHashSparseIntToRuby.new : Hash.new
168
+ self.each do |rule|
169
+ if !rule.get_measure(measure).value.nil?
170
+ key = rule.rhs.first
171
+ if selected_rules[key].nil?
172
+ selected_rules[key] = Evoc::Env::GOOGLE_HASH ? GoogleHashSparseRubyToRuby.new : Hash.new
173
+ selected_rules[key][:value] = rule.get_measure(measure).value
174
+ selected_rules[key][:rule] = rule
175
+ end
176
+ if rule.get_measure(measure).value > selected_rules[key][:value]
177
+ selected_rules[key][:value] = rule.get_measure(measure).value
178
+ selected_rules[key][:rule] = rule
179
+ end
180
+ end
181
+ end
182
+ return selected_rules.values.map {|k,v| k[:rule]}
183
+ end
184
+
185
+ ##
186
+ # @return [Integer] the size of the largest rule, measures by antecedent size
187
+ def largest_antecedent
188
+ self.map {|r| r.lhs.size}.max
189
+ end
190
+
191
+ ##
192
+ # @return [True/False/Nil] if the lhs of one of the rules is equal to the query
193
+ def exact_match
194
+ match = nil
195
+ if !self.query.nil?
196
+ match = false
197
+ self.each do |rule|
198
+ if (rule.lhs.sort == self.query.sort)
199
+ match = true
200
+ break
201
+ end
202
+ end
203
+ else
204
+ logger.debug "Tried to calculate exact match, but query was nil "
205
+ end
206
+ return match
207
+ end
208
+
209
+
210
+ def << rule
211
+ self.rules << rule
212
+ end
213
+
214
+ def pretty_print
215
+ CSV.generate do |csv|
216
+ # write header
217
+ defined_measures = []
218
+ if aggregator.nil?
219
+ defined_measures = self.map {|r| r.instantiated_measures}.array_union
220
+ else
221
+ defined_measures = self.hyper_rules.map {|r| r.instantiated_measures}.array_union
222
+ end
223
+ csv << ['rule'] + defined_measures
224
+ self.each do |rule|
225
+ row = CSV::Row.new([],[],false)
226
+ row << rule.name
227
+ defined_measures.each do |m|
228
+ row << rule.get_measure(m).value
229
+ end
230
+ csv << row
231
+ end
232
+ end
233
+ end
234
+
235
+ def to_s
236
+ CSV.generate do |csv|
237
+ # write header
238
+ csv << ['lhs','rhs'] + Evoc::Rule.measures
239
+ self.each do |rule|
240
+ row = CSV::Row.new([],[],false)
241
+ row << (rule.lhs.respond_to?(:join) ? rule.lhs.join(',') : rule.lhs)
242
+ row << (rule.rhs.respond_to?(:join) ? rule.rhs.join(',') : rule.rhs)
243
+ Evoc::Rule.measures.each do |m|
244
+ row << (rule.measure_instantiated?(m) ? rule.get_measure(m).value : nil)
245
+ end
246
+ csv << row
247
+ end
248
+ end
249
+ end
250
+
251
+ def print(measures = Evoc::Rule.measures)
252
+ CSV {|row| row << ['lhs','rhs'] + measures}
253
+ if self.size > 0
254
+ name_mapping = self.first.tx_store.int_2_name
255
+ self.sort_on(measures: measures).each do |rule|
256
+ row = CSV::Row.new([],[],false)
257
+ row << rule.lhs.map{|i| name_mapping[i]}.join(',')
258
+ row << rule.rhs.map{|i| name_mapping[i]}.join(',')
259
+ measures.each do |m|
260
+ row << (rule.measure_instantiated?(m) ? rule.get_measure(m).value : nil)
261
+ end
262
+ CSV {|r| r << row}
263
+ end
264
+ end
265
+ nil
266
+ end
267
+
268
+
269
+ ##
270
+ # Print the current rule set to a csv file
271
+ # @param measures [Array<String>] the measures to include in output. Default is all measures.
272
+ # @param file [String] the file to write to.
273
+ def print_to_file(measures: Evoc::Rule.measures, file:)
274
+ CSV.open(file, "wb") do |csv|
275
+ # write header
276
+ csv << ['lhs','rhs'] + measures
277
+ self.each do |rule|
278
+ row = CSV::Row.new([],[],false)
279
+ row << rule.lhs.join(',')
280
+ row << rule.rhs.join(',')
281
+ measures.each do |m|
282
+ row << (rule.measure_instantiated?(m) ? rule.get_measure(m).value : nil)
283
+ end
284
+ csv << row
285
+ end
286
+ end
287
+ end
288
+
289
+ def ==other
290
+ ( self.map(&:lhs) == other.map(&:lhs) ) &
291
+ ( self.map(&:rhs) == other.map(&:rhs) ) &
292
+ Evoc::Rule.measures.each do |m|
293
+ self.map {|r| r.get_measure(m)} == other.map {|r| r.get_measure(m)}
294
+ end
295
+ end
296
+
297
+ def size
298
+ self.rules.size
299
+ end
300
+
301
+ def empty?
302
+ self.rules.empty?
303
+ end
304
+
305
+ def clear
306
+ self.rules.clear
307
+ end
308
+
309
+ def to_h
310
+ if self.rules.nil?
311
+ {}
312
+ else
313
+ self.rules.map {|r|
314
+ h = Hash.new
315
+ h[:lhs] = r.lhs
316
+ h[:rhs] = r.rhs
317
+ r.instantiated_measures.each {|m| h[m] = r.get_measure(m).value.to_r}
318
+ h}
319
+ end
320
+ end
321
+
322
+ def instance_values_for_csv
323
+ dont_include = ['rules']
324
+ self.instance_values.delete_if {|k,v| dont_include.include?(k)}
325
+ end
326
+
327
+ ##
328
+ # generate an array suitable for a csv header
329
+ def csv_header
330
+ self.instance_values_for_csv.keys
331
+ end
332
+
333
+ ##
334
+ # generate an array of the current values of <self>
335
+ # converts any array values to a comma separated string representation
336
+ def to_csv_row
337
+ self.instance_values_for_csv.values.map {|val| val.is_a?(Array) ? val.join(',') : val}
338
+ end
339
+ end
340
+ end