geoptima 0.1.3 → 0.1.4

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.
data/bin/show_geoptima CHANGED
@@ -6,8 +6,9 @@ $: << '../lib'
6
6
 
7
7
  require 'date'
8
8
  require 'geoptima'
9
+ require 'geoptima/options'
9
10
 
10
- Geoptima::assert_version("0.1.3")
11
+ Geoptima::assert_version("0.1.4")
11
12
 
12
13
  $debug=false
13
14
 
@@ -24,29 +25,89 @@ $files = Geoptima::Options.process_args do |option|
24
25
  option.m {$map_headers = true}
25
26
  option.a {$combine_all = true}
26
27
  option.l {$more_headers = true}
28
+ option.P {$export_prefix = ARGV.shift}
27
29
  option.E {$event_names += ARGV.shift.split(/[\,\;\:\.]+/)}
28
- option.T do
29
- $time_range = Geoptima::DateRange.from ARGV.shift
30
- end
30
+ option.T {$time_range = Geoptima::DateRange.from ARGV.shift}
31
31
  option.L {$print_limit = [1,ARGV.shift.to_i].max}
32
-
33
- option.t {$time_split = true}
34
- option.D {$export_dir = ARGV.shift}
35
- option.N {$merged_name = ARGV.shift}
36
- option.S {$specfile = ARGV.shift}
37
- option.P {$diversity = ARGV.shift.to_f}
38
- option.W {$chart_width = ARGV.shift.to_i}
39
- option.T do
40
- $time_range = Geoptima::DateRange.from ARGV.shift
41
- end
32
+ option.M {$mapfile = ARGV.shift}
42
33
  end.map do |file|
43
34
  File.exist?(file) ? file : puts("No such file: #{file}")
44
35
  end.compact
45
36
 
37
+ class HeaderMap
38
+ attr_reader :prefix, :name, :event
39
+ attr_accessor :columns
40
+ def initialize(prefix,name,event)
41
+ @prefix = prefix
42
+ @name = name
43
+ @event = event
44
+ @columns = []
45
+ end
46
+ def mk_known(header)
47
+ puts "Creating column mappings for headers: #{header}" if($debug)
48
+ @col_indices = {}
49
+ columns.each do |col|
50
+ c = (col[1] && col[1].gsub(/\?/,'')).to_s
51
+ if c.length>0
52
+ @col_indices[c] = header.index(c)
53
+ puts "\tMade column mapping: #{c} --> #{header.index(c)}" if($debug)
54
+ end
55
+ end
56
+ end
57
+ def map_fields(header,fields)
58
+ @scenario_counter ||= 0
59
+ mk_known(header) unless @col_indices
60
+ @columns.map do |column|
61
+ if column[1] =~ /SCENARIO_COUNTER/
62
+ @scenario_counter += 1
63
+ else
64
+ index = @col_indices[column[1]]
65
+ puts "Found mapping #{column} -> #{index} -> #{index && fields[index]}" if($debug)
66
+ index && fields[index]
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ if $mapfile
73
+ $header_maps = []
74
+ current_map = nil
75
+ prefix = $mapfile.split(/\./)[0]
76
+ File.open($mapfile).each do |line|
77
+ line.chomp!
78
+ next if line =~ /^\s*#/
79
+ next if line.length < 2
80
+ if line =~ /^\[(\w+)\]\t(\w+)/
81
+ current_map = HeaderMap.new(prefix,$1,$2)
82
+ $header_maps << current_map
83
+ elsif current_map
84
+ current_map.columns << line.chomp.split(/\t/)[0..1]
85
+ else
86
+ puts "Invalid header map line: #{line}"
87
+ end
88
+ end
89
+ end
90
+
91
+ def show_header_maps
92
+ if $header_maps
93
+ puts "Using #{$header_maps.length} header maps:"
94
+ $header_maps.each do |hm|
95
+ puts "\t[#{hm.name}] (#{hm.event})"
96
+ if $debug
97
+ hm.columns.each do |hc|
98
+ puts "\t\t#{hc.map{|c| (c+' '*30)[0..30]}.join("\t-->\t")}"
99
+ end
100
+ else
101
+ puts "\t\t#{hm.columns.map{|hc| hc[0]}.join(', ')}"
102
+ end
103
+ end
104
+ end
105
+ end
106
+
46
107
  $help = true if($files.length < 1)
47
108
  if $help
48
109
  puts <<EOHELP
49
- Usage: show_geoptima <-dpvxomlsah> <-L limit> <-E types> <-T min,max> file <files>
110
+ Usage: show_geoptima <-dpvxomlsah> <-L limit> <-E types> <-T min,max> <-M mapfile> file <files>
50
111
  -d debug mode (output more context during processing) #{cw $debug}
51
112
  -p print mode (print out final results to console) #{cw $print}
52
113
  -v verbose mode (output extra information to console) #{cw $verbose}
@@ -57,14 +118,19 @@ Usage: show_geoptima <-dpvxomlsah> <-L limit> <-E types> <-T min,max> file <file
57
118
  -s seperate the export files by event type #{cw $seperate}
58
119
  -a combine all IMEI's into a single dataset #{cw $combine_all}
59
120
  -h show this help
121
+ -P prefix for exported files (default: ''; current: #{$export_prefix})
60
122
  -E comma-seperated list of event types to show and export (default: all; current: #{$event_names.join(',')})
61
123
  -T time range to limit results to (default: all; current: #{$time_range})
62
124
  -L limit verbose output to specific number of lines #{cw $print_limit}
125
+ -M mapfile of normal->altered header names: #{$mapfile}
63
126
  EOHELP
127
+ show_header_maps
64
128
  exit 0
65
129
  end
66
130
 
67
131
  $verbose = $verbose || $debug
132
+ show_header_maps if($verbose)
133
+
68
134
  $datasets = Geoptima::Dataset.make_datasets($files, :locate => true, :time_range => $time_range, :combine_all => $combine_all)
69
135
 
70
136
  class Export
@@ -74,13 +140,18 @@ class Export
74
140
  @imei = imei
75
141
  @names = names
76
142
  if $export
77
- if $seperate
143
+ if $header_maps
144
+ @files = $header_maps.inject({}) do |a,hm|
145
+ a[hm.event] = File.open("#{$export_prefix}#{imei}_#{hm.prefix}_#{hm.name}.csv",'w')
146
+ a
147
+ end
148
+ elsif $seperate
78
149
  @files = names.inject({}) do |a,name|
79
- a[name] = File.open("#{imei}_#{name}.csv",'w')
150
+ a[name] = File.open("#{$export_prefix}#{imei}_#{name}.csv",'w')
80
151
  a
81
152
  end
82
153
  else
83
- @files={nil => File.open("#{imei}.csv",'w')}
154
+ @files={nil => File.open("#{$export_prefix}#{imei}.csv",'w')}
84
155
  end
85
156
  end
86
157
  @headers = names.inject({}) do |a,name|
@@ -91,7 +162,11 @@ class Export
91
162
  end
92
163
  @headers[nil] = @headers.values.flatten.sort
93
164
  files && files.each do |key,file|
94
- file.puts map_headers(base_headers+more_headers+header(key)).join("\t")
165
+ if $header_maps
166
+ file.puts $header_maps.find{|hm| hm.event == key}.columns.map{|c| c[0]}.join("\t")
167
+ else
168
+ file.puts map_headers(base_headers+more_headers+header(key)).join("\t")
169
+ end
95
170
  end
96
171
  if $debug || $verbose
97
172
  @headers.each do |name,head|
@@ -108,7 +183,7 @@ class Export
108
183
  end
109
184
  def more_headers
110
185
  $more_headers ?
111
- ['IMSI','MSISDN','MCC','MNC','LAC','CI','LAC-CI','RSSI','Platform','Model','OS','Operator'] :
186
+ ['IMSI','MSISDN','MCC','MNC','LAC','CI','LAC-CI','RSSI','Platform','Model','OS','Operator','Battery'] :
112
187
  []
113
188
  end
114
189
  def base_fields(event)
@@ -127,15 +202,17 @@ class Export
127
202
  when 'LAC-CI'
128
203
  "#{dataset.recent(event,'service.lac')}-#{dataset.recent(event,'service.cell_id')}"
129
204
  when 'MCC'
130
- dataset[h] || dataset.recent(event,'service.mcc')
205
+ event.file[h] || dataset.recent(event,'service.mcc')
131
206
  when 'MNC'
132
- dataset[h] || dataset.recent(event,'service.mnc')
207
+ event.file[h] || dataset.recent(event,'service.mnc')
208
+ when 'Battery'
209
+ dataset.recent(event,'batteryState.state',600)
133
210
  when 'Operator'
134
- dataset['carrierName']
211
+ event.file['carrierName']
135
212
  when 'IMSI'
136
- dataset.imsi
213
+ event.file['imsi']
137
214
  else
138
- dataset[h]
215
+ event.file[h]
139
216
  end
140
217
  end
141
218
  end
@@ -164,10 +241,10 @@ class Export
164
241
  end || hnames
165
242
  end
166
243
  def export_stats(stats)
167
- File.open("#{imei}_stats.csv",'w') do |out|
244
+ File.open("#{$export_prefix}#{imei}_stats.csv",'w') do |out|
168
245
  stats.keys.sort.each do |header|
169
246
  out.puts header
170
- values = stats[header].keys.sort
247
+ values = stats[header].keys.sort{|a,b| b.to_s<=>a.to_s}
171
248
  out.puts values.join("\t")
172
249
  out.puts values.map{|v| stats[header][v]}.join("\t")
173
250
  out.puts
@@ -178,7 +255,7 @@ class Export
178
255
  @headers[name]
179
256
  end
180
257
  def puts_to(line,name)
181
- name = nil unless($seperate)
258
+ name = nil unless($seperate || $header_maps)
182
259
  files[name].puts(line) if($export && files[name])
183
260
  end
184
261
  def puts_to_all(line)
@@ -222,13 +299,29 @@ $datasets.keys.sort.each do |imei|
222
299
  names = dataset.events_names if(names.length<1)
223
300
  export = Export.new(imei,names,dataset)
224
301
  export.export_stats(dataset.stats) if($export_stats)
225
- events.each do |event|
226
- names.each do |name|
227
- if event.name === name
228
- fields = export.header($seperate ? name : nil).map{|h| event[h]}
229
- b_fields = export.base_fields(event) + export.more_fields(event,dataset)
230
- export.puts_to "#{b_fields.join("\t")}\t#{fields.join("\t")}", name
231
- if_le{puts "#{b_fields.join("\t")}\t#{event.fields.inspect}"}
302
+ if $header_maps && $header_maps.length > 0
303
+ $header_maps.each do |hm|
304
+ puts "Searching for events for header_maps '#{hm.event}'"
305
+ events.each do |event|
306
+ if event.name == hm.event
307
+ header = export.header(event.name)
308
+ fields = header.map{|h| event[h]}
309
+ b_header = export.base_headers + export.more_headers
310
+ b_fields = export.base_fields(event) + export.more_fields(event,dataset)
311
+ all_fields = hm.map_fields(b_header + header, b_fields + fields)
312
+ export.puts_to all_fields.join("\t"), event.name
313
+ end
314
+ end
315
+ end
316
+ else
317
+ events.each do |event|
318
+ names.each do |name|
319
+ if event.name === name
320
+ fields = export.header($seperate ? name : nil).map{|h| event[h]}
321
+ b_fields = export.base_fields(event) + export.more_fields(event,dataset)
322
+ export.puts_to "#{b_fields.join("\t")}\t#{fields.join("\t")}", name
323
+ if_le{puts "#{b_fields.join("\t")}\t#{event.fields.inspect}"}
324
+ end
232
325
  end
233
326
  end
234
327
  end
@@ -10,7 +10,7 @@ require 'geoptima/options'
10
10
  require 'fileutils'
11
11
  require 'geoptima/daterange'
12
12
 
13
- Geoptima::assert_version("0.1.3")
13
+ Geoptima::assert_version("0.1.4")
14
14
  Geoptima::Chart.available? || puts("No charting libraries available") || exit(-1)
15
15
 
16
16
  $export_dir = '.'
@@ -26,9 +26,7 @@ $files = Geoptima::Options.process_args do |option|
26
26
  option.S {$specfile = ARGV.shift}
27
27
  option.P {$diversity = ARGV.shift.to_f}
28
28
  option.W {$chart_width = ARGV.shift.to_i}
29
- option.T do
30
- $time_range = Geoptima::DateRange.from ARGV.shift
31
- end
29
+ option.T {$time_range = Geoptima::DateRange.from ARGV.shift}
32
30
  end
33
31
 
34
32
  FileUtils.mkdir_p $export_dir
@@ -148,8 +146,10 @@ class StatsManager
148
146
  end
149
147
 
150
148
  module Geoptima
149
+
150
+ # Class for original stats approach of creating a new 'column' from simple combinations of other columns
151
151
  class StatSpec
152
- attr_reader :header, :event, :index, :indices, :fields, :options, :proc, :groups
152
+ attr_reader :header, :event, :index, :indices, :fields, :options, :proc, :groups, :values
153
153
  def initialize(header,*fields,&block)
154
154
  @header = header
155
155
  @fields = fields
@@ -184,13 +184,13 @@ module Geoptima
184
184
  key = @group.call(time)
185
185
  ghead = "#{header} #{key}"
186
186
  @groups[key] = ghead
187
- stats_manager.add(map(fields),ghead,nil)
187
+ stats_manager.add(map_fields(fields),ghead,nil)
188
188
  end
189
189
  rescue ArgumentError
190
190
  puts "Error: Unable to process time field[#{time}]: #{$!}"
191
191
  end
192
192
  end
193
- stats_manager.add(map(fields),header,index)
193
+ stats_manager.add(map_fields(fields),header,index)
194
194
  end
195
195
  def div
196
196
  unless @div
@@ -224,18 +224,30 @@ module Geoptima
224
224
  val
225
225
  end
226
226
  end
227
- def map(values,filter=nil)
227
+ def prepare_values(values)
228
+ @values = []
228
229
  if @indices
229
230
  puts "StatSpec[#{self}]: #{options.inspect}" if($debug)
230
- vals = @indices.map{|i| values[i]}
231
- puts "\tVALUES: #{vals.inspect}" if($debug)
232
- (options[:filter] || {}).each do |field,expected|
231
+ @values = @indices.map{|i| values[i]}
232
+ puts "\tVALUES: #{values.inspect}" if($debug)
233
+ end
234
+ @values
235
+ end
236
+ def vals_for(values,filter={})
237
+ if @indices
238
+ prepare_values(values)
239
+ (options[:filter] || filter).each do |field,expected|
233
240
  puts "\t\tChecking if field #{field} is #{expected}" if($debug)
234
241
  puts "\t\tLooking for #{field} or #{event}.#{field} in #{@fields.inspect}" if($debug)
235
242
  hi = @fields.index(field.to_s) || @fields.index("#{event}.#{field}")
236
- puts "\t\t#{field} -> #{hi} -> #{hi && vals[hi]}" if($debug)
237
- return nil unless(hi && vals[hi] && (expected === vals[hi].downcase || vals[hi].downcase === expected.to_s.downcase))
243
+ puts "\t\t#{field} -> #{hi} -> #{hi && values[hi]}" if($debug)
244
+ return nil unless(hi && values[hi] && (expected === values[hi].downcase || values[hi].downcase === expected.to_s.downcase))
238
245
  end
246
+ values
247
+ end
248
+ end
249
+ def map_fields(values,filter={})
250
+ if vals = vals_for(values,filter)
239
251
  val = proc.nil? ? vals[0] : proc.call(*vals)
240
252
  puts "\tBLOCK MAP: #{vals.inspect} --> #{val.inspect}" if($debug)
241
253
  if options[:div]
@@ -266,6 +278,128 @@ module Geoptima
266
278
  "#{header}[#{index}]<-#{fields.inspect}(#{indices && indices.join(',')})"
267
279
  end
268
280
  end
281
+
282
+ class Group
283
+ attr_reader :name, :options, :proc, :is_time, :index
284
+ def initialize(name,options={},&block)
285
+ @name = name
286
+ @options = options
287
+ @proc = block
288
+ @is_time = options[:is_time]
289
+ end
290
+ def index= (ind)
291
+ puts "Set group header index=#{ind} for group '#{name}'"
292
+ @index = ind
293
+ end
294
+ def call(time,values)
295
+ is_time && @proc.call(time) || @proc.call(values[index])
296
+ end
297
+ end
298
+
299
+ # The KPI class allows for complex statistics called 'Key Performance Indicators'.
300
+ # These are specified using four functions:
301
+ # filter: how to choose rows to include in the statistics (default is '!map.nil?')
302
+ # map: how to convert a row into the internal stats (default is input columns)
303
+ # aggregate: how to aggregate internal stats to higher levels (eg. daily, default is count)
304
+ # reduce: how to extract presentable stats from internal stats (eg. avg=total/count, default is internal stats)
305
+ #
306
+ # The KPI is defined with a name and set of columns to use, followed by the block
307
+ # defining the four functions above. For example:
308
+ #
309
+ # kpi 'DNS Success', 'dnsLookup.address', 'dnsLookup.error', 'dnsLookup.interface' do |f|
310
+ # f.filter {|addr,err,int| addr =~/\w/}
311
+ # f.map {|addr,err,int| err.length==0 ? [1,1] : [1,0]}
312
+ # f.aggregate {|a,v| a[0]+=v[0];a[1]+=v[1];a}
313
+ # f.reduce {|a| 100.0*a[1].to_f/a[0].to_f}
314
+ # end
315
+ #
316
+ # Currently this class extends StatSpec for access to the prepare_indices method.
317
+ # We should consider moving that to a mixin, or depreciating the StatSpec class
318
+ # entirely since KPISpec should provide a superset of features.
319
+ class KPISpec < StatSpec
320
+ def initialize(header,*fields,&block)
321
+ @header = header
322
+ @fields = fields
323
+ @event = @fields[0].split(/\./)[0]
324
+ block.call self unless(block.nil?)
325
+ if @fields[-1].is_a?(Hash)
326
+ @options = @fields.pop
327
+ else
328
+ @options = {}
329
+ end
330
+ @group_procs = []
331
+ @groups = {}
332
+ if @options[:group]
333
+ [@options[:group]].flatten.compact.sort.uniq.each do |group_name|
334
+ gname = group_name.to_s.intern
335
+ case gname
336
+ when :months
337
+ group_by(gname,true) {|t| t.strftime("%Y-%m")}
338
+ when :weeks
339
+ group_by(gname,true) {|t| t.strftime("%Y w%W")}
340
+ when :days
341
+ group_by(gname,true) {|t| t.strftime("%Y-%m-%d")}
342
+ when :hours
343
+ group_by(gname,true) {|t| t.strftime("%Y-%m-%d %H")}
344
+ else
345
+ group_by(gname) {|f| f}
346
+ end
347
+ end
348
+ end
349
+ puts "Created StatSpec: #{self}"
350
+ end
351
+ def group_by(field,is_time=false,&block)
352
+ @group_procs = Group.new(field,:is_time => is_time,&block)
353
+ end
354
+ def filter(&block)
355
+ @filter_proc = block
356
+ end
357
+ def map(&block)
358
+ @map_proc = block
359
+ end
360
+ def aggregate(&block)
361
+ @aggregate_proc = block
362
+ end
363
+ def reduce(&block)
364
+ @reduce_proc = block
365
+ end
366
+ def add(stats_manager,values)
367
+ prepare_values(values)
368
+ if @group_procs.length > 0
369
+ begin
370
+ time = DateTime.parse(values[stats_manager.time_index])
371
+ if $time_range.nil? || $time_range.include?(time)
372
+ key = @group_procs.inject(header) do |ghead,group|
373
+ key = @group.call(time,values)
374
+ ghead += " #{key}"
375
+ end
376
+ @groups[key] = ghead
377
+ stats_manager.add(map_fields(fields),ghead,nil)
378
+ end
379
+ rescue ArgumentError
380
+ puts "Error: Unable to process time field[#{time}]: #{$!}"
381
+ end
382
+ end
383
+ stats_manager.add(map_fields(fields),header,index)
384
+ end
385
+ def map_fields(values,filter=nil)
386
+ if values
387
+ if @filter_proc.nil? || @filter_proc.call(*values)
388
+ val = @map_proc && @map_proc.call(*values) || values[0]
389
+ puts "\tBLOCK MAP: #{values.inspect} --> #{values.inspect}" if($debug)
390
+ end
391
+ val
392
+ end
393
+ end
394
+ def prepare_indices(stats_manager,headers)
395
+ super(stats_manager,headers)
396
+ @group_procs.each do |g|
397
+ g.index = fields.index(g.name)
398
+ end
399
+ end
400
+ end
401
+
402
+ # Class for specifications of individual charts
269
403
  class ChartSpec
270
404
  attr_reader :chart_type, :header, :options
271
405
  def initialize(header,options={})
@@ -328,14 +462,15 @@ module Geoptima
328
462
  g.write("#{$export_dir}/Chart_#{stats_manager.name}_#{header}_#{chart_type}_distribution.png")
329
463
  end
330
464
  def to_s
331
- "#{chart_type.upcase}-#{header}"
465
+ "#{chart_type.to_s.upcase}-#{header}"
332
466
  end
333
467
  end
334
468
  class StatsSpecs
335
- attr_reader :chart_specs, :stat_specs
469
+ attr_reader :chart_specs, :stat_specs, :kpi_specs
336
470
  def initialize(specfile)
337
471
  @chart_specs = []
338
472
  @stat_specs = []
473
+ @kpi_specs = []
339
474
  instance_eval(File.open(specfile).read)
340
475
  end
341
476
  def category_chart(header,options={})
@@ -353,10 +488,16 @@ module Geoptima
353
488
  def stats(header,*fields,&block)
354
489
  @stat_specs << StatSpec.new(header,*fields,&block)
355
490
  end
491
+ def kpi(header,*fields,&block)
492
+ @kpi_specs << KPISpec.new(header,*fields,&block)
493
+ end
356
494
  def add_stats(stats_manager,headers)
357
495
  stat_specs.each do |stat_spec|
358
496
  stat_spec.prepare_indices(stats_manager,headers)
359
497
  end
498
+ kpi_specs.each do |kpi_spec|
499
+ kpi_spec.prepare_indices(stats_manager,headers)
500
+ end
360
501
  end
361
502
  def add_fields(stats_manager,fields)
362
503
  puts "Adding fields to #{stat_specs.length} StatSpec's" if($debug)
@@ -364,9 +505,14 @@ module Geoptima
364
505
  puts "Adding fields to StatSpec: #{stat_spec}" if($debug)
365
506
  stat_spec.add(stats_manager,fields)
366
507
  end
508
+ puts "Adding fields to #{kpi_specs.length} KPISpec's" if($debug)
509
+ kpi_specs.each do |kpi_spec|
510
+ puts "Adding fields to KPISpec: #{kpi_spec}" if($debug)
511
+ kpi_spec.add(stats_manager,fields)
512
+ end
367
513
  end
368
514
  def to_s
369
- "Stats[#{@stat_specs.join(', ')}] AND Charts[#{@chart_specs.join(', ')}]"
515
+ "Stats[#{@stat_specs.join(', ')}] AND KPIs[#{@kpi_specs.join(', ')}] AND Charts[#{@chart_specs.join(', ')}]"
370
516
  end
371
517
  end
372
518
  end
@@ -468,7 +614,11 @@ end
468
614
  $stats_managers.each do |name,stats_manager|
469
615
  if $specs
470
616
  $specs.chart_specs.each do |chart_spec|
471
- chart_spec.process(stats_manager)
617
+ begin
618
+ chart_spec.process(stats_manager)
619
+ rescue NoMethodError
620
+ puts "Failed to process chart '#{chart_spec}': #{$!}"
621
+ end
472
622
  end
473
623
  end
474
624
  if $create_all