object_table 0.1.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.
@@ -0,0 +1,146 @@
1
+ require 'forwardable'
2
+
3
+ module ObjectTable::TableMethods
4
+ extend Forwardable
5
+
6
+ attr_reader :R
7
+ def initialize
8
+ @R = ObjectTable::BasicGrid
9
+ end
10
+
11
+ def ==(other)
12
+ return false unless other.is_a?(ObjectTable::TableMethods)
13
+ return columns == other.columns
14
+ end
15
+ alias_method :eql?, :==
16
+
17
+ def colnames
18
+ columns.keys
19
+ end
20
+
21
+ def nrows
22
+ columns.empty? ? 0 : columns.values.first.shape[-1]
23
+ end
24
+
25
+ def ncols
26
+ columns.keys.length
27
+ end
28
+
29
+ def_delegator :columns, :include?, :has_column?
30
+
31
+ def_delegator :columns, :[], :get_column
32
+ alias_method :[], :get_column
33
+
34
+ def set_column(name, value, *args)
35
+ column = get_column(name)
36
+ value = value.to_a if value.is_a?(Range)
37
+
38
+ if column
39
+ return (column[] = value)
40
+ end
41
+
42
+ if (value.is_a?(Array) or value.is_a?(NArray)) and args.empty?
43
+ value = NArray.to_na(value)
44
+ unless value.shape[-1] == nrows
45
+ raise ArgumentError.new("Expected size of last dimension to be #{nrows}, was #{value.shape[-1]}")
46
+ end
47
+
48
+ args = [value.typecode] + value.shape[0...-1]
49
+ end
50
+
51
+ column = add_column(name, *args)
52
+ begin
53
+ column[] = value
54
+ rescue Exception => e
55
+ pop_column(name)
56
+ raise e
57
+ end
58
+ end
59
+ alias_method :[]=, :set_column
60
+
61
+ def pop_column(name)
62
+ columns.delete name
63
+ end
64
+
65
+ def apply(&block)
66
+ result = instance_eval &block
67
+ if result.is_a? ObjectTable::BasicGrid
68
+ result = ObjectTable.new(result)
69
+ end
70
+ result
71
+ end
72
+
73
+ def where(&block)
74
+ ObjectTable::TempView.new(self, &block)
75
+ end
76
+
77
+ def group(*args, &block)
78
+ ObjectTable::TempGrouped.new(self, *args, &block)
79
+ end
80
+
81
+ def sort_by(*keys)
82
+ sort_index = _get_sort_index(keys)
83
+ cols = ObjectTable::BasicGrid[columns.map{|k, v| [k, v[sort_index]]}]
84
+ ObjectTable.new(cols)
85
+ end
86
+
87
+ def method_missing(meth, *args, &block)
88
+ get_column(meth) or super
89
+ end
90
+
91
+ def respond_to?(meth)
92
+ super or has_column?(meth)
93
+ end
94
+
95
+ def inspect(max_section = 5)
96
+ header = "#{self.class}(#{nrows}, #{ncols})\n"
97
+ printed_columns = []
98
+
99
+ if nrows > max_section * 2
100
+ head = (0...max_section)
101
+ tail = ((nrows - max_section)...nrows)
102
+
103
+ printed_columns.push [''] + (head.to_a + tail.to_a).map{|i| "#{i}: "} + ['']
104
+ printed_columns += columns.map do |name, c|
105
+ c = c.get_rows([head, tail], true)
106
+ strings = c.shape[-1].times.map do |i|
107
+ row = c.get_rows(i)
108
+ row.is_a?(NArray) ? row.inspect.partition("\n")[-1].strip : row.inspect
109
+ end
110
+
111
+ [name.to_s] + strings + [name.to_s]
112
+ end
113
+ else
114
+ max_section = -1
115
+ printed_columns.push [''] + (0...nrows).map{|i| "#{i}: "} + ['']
116
+ printed_columns += columns.map do |name, c|
117
+ [name.to_s] + c.to_a.map(&:inspect) + [name.to_s]
118
+ end
119
+ end
120
+
121
+ widths = printed_columns.map{|col| col.map(&:length).max + 2}
122
+
123
+ header + printed_columns.transpose.each_with_index.map do |row, index|
124
+ row = row.zip(widths).map do |cell, width|
125
+ cell.rjust(width)
126
+ end.join('')
127
+
128
+ if index == max_section
129
+ row += "\n" + '-'*widths.reduce(:+)
130
+ end
131
+ row
132
+ end.join("\n")
133
+ rescue NoMethodError => e
134
+ raise Exception.new(e)
135
+ end
136
+
137
+ def clone
138
+ cols = ObjectTable::BasicGrid[columns.map{|k, v| [k, v.clone]}]
139
+ ObjectTable.new(cols)
140
+ end
141
+
142
+ def _get_sort_index(columns)
143
+ (0...nrows).zip(columns.map(&:to_a).transpose).sort_by(&:last).map(&:first)
144
+ end
145
+
146
+ end
@@ -0,0 +1,43 @@
1
+ require 'forwardable'
2
+ require_relative 'grouped'
3
+
4
+ class ObjectTable::TempGrouped
5
+ extend Forwardable
6
+ def_delegators :make_grouped, :each, :apply
7
+
8
+ def initialize(parent, *names, &grouper)
9
+ @parent = parent
10
+ @grouper = grouper
11
+ @names = names
12
+ end
13
+
14
+ def _groups
15
+ names, keys = _keys()
16
+ groups = (0...@parent.nrows).zip(keys).group_by{|row, key| key}
17
+ groups.each do |k, v|
18
+ groups[k] = NArray.to_na(v.map(&:first))
19
+ end
20
+ [names, groups]
21
+ end
22
+
23
+ def _keys
24
+ if @names.empty?
25
+ keys = @parent.instance_eval(&@grouper)
26
+ raise 'Group keys must be hashes' unless keys.is_a?(Hash)
27
+ keys = ObjectTable::BasicGrid.new.replace keys
28
+ else
29
+ keys = ObjectTable::BasicGrid[@names.map{|n| [n, @parent.get_column(n)]}]
30
+ end
31
+
32
+ keys._ensure_uniform_columns!(@parent.nrows)
33
+ names = keys.keys
34
+ keys = keys.values.map(&:to_a).transpose
35
+ [names, keys]
36
+ end
37
+
38
+ def make_grouped
39
+ names, groups = _groups()
40
+ ObjectTable::Grouped.new(@parent, names, groups)
41
+ end
42
+
43
+ end
@@ -0,0 +1,63 @@
1
+ require 'forwardable'
2
+ require_relative 'view_methods'
3
+ require_relative 'masked_column'
4
+ require_relative 'view'
5
+
6
+ class ObjectTable::TempView
7
+ include ObjectTable::ViewMethods
8
+
9
+ extend Forwardable
10
+ def_delegators :make_view, :group, :apply
11
+
12
+ def initialize(parent, &block)
13
+ super()
14
+ @parent = parent
15
+ @filter = block
16
+ end
17
+
18
+ def_delegators :@parent, :has_column?
19
+
20
+ def get_column(name)
21
+ col = @parent.get_column(name)
22
+ ObjectTable::MaskedColumn.mask(col, indices) if col
23
+ end
24
+ alias_method :[], :get_column
25
+
26
+ def make_view
27
+ ObjectTable::View.new @parent, indices
28
+ end
29
+
30
+ def clone
31
+ cols = ObjectTable::BasicGrid[@parent.columns.map{|k, v| [k, v[indices]]}]
32
+ ObjectTable.new(cols)
33
+ end
34
+
35
+ def inspect(*args)
36
+ cache_columns{ super }
37
+ rescue NoMethodError => e
38
+ raise Exception.new(e)
39
+ end
40
+
41
+ def indices
42
+ @indices or NArray.int(@parent.nrows).indgen![@parent.apply &@filter]
43
+ end
44
+
45
+ def cache_indices(&block)
46
+ @indices = indices
47
+ value = block.call()
48
+ @indices = nil
49
+ value
50
+ end
51
+
52
+ def columns
53
+ @columns or super
54
+ end
55
+
56
+ def cache_columns(&block)
57
+ @columns = columns
58
+ value = block.call()
59
+ @columns = nil
60
+ value
61
+ end
62
+
63
+ end
@@ -0,0 +1,3 @@
1
+ class ObjectTable
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,24 @@
1
+ require_relative 'view_methods'
2
+ require_relative 'basic_grid'
3
+ require_relative 'masked_column'
4
+
5
+ class ObjectTable::View
6
+ include ObjectTable::ViewMethods
7
+ attr_reader :indices
8
+
9
+ def initialize(parent, indices)
10
+ super()
11
+ @parent = parent
12
+ @indices = indices
13
+ columns
14
+ end
15
+
16
+ def columns
17
+ @columns ||= super
18
+ end
19
+
20
+ def add_column(name, *args)
21
+ @columns[name] = super
22
+ end
23
+
24
+ end
@@ -0,0 +1,22 @@
1
+ require 'forwardable'
2
+ require_relative 'table_methods'
3
+
4
+ module ObjectTable::ViewMethods
5
+ extend Forwardable
6
+ include ObjectTable::TableMethods
7
+
8
+ def columns
9
+ ObjectTable::BasicGrid[@parent.columns.map{|k, v| [k, ObjectTable::MaskedColumn.mask(v, indices)]}]
10
+ end
11
+
12
+ def add_column(name, *args)
13
+ col = @parent.add_column(name, *args)
14
+ ObjectTable::MaskedColumn.mask(col, indices)
15
+ end
16
+
17
+ def pop_column(name)
18
+ @parent.pop_column(name)
19
+ super if @columns
20
+ end
21
+
22
+ end
@@ -0,0 +1,86 @@
1
+ require_relative "object_table/version"
2
+ require_relative "object_table/basic_grid"
3
+ require_relative "object_table/table_methods"
4
+ require_relative "object_table/view"
5
+ require_relative "object_table/temp_view"
6
+ require_relative "object_table/column"
7
+ require_relative "object_table/grouped"
8
+ require_relative "object_table/temp_grouped"
9
+
10
+ class ObjectTable
11
+ include TableMethods
12
+
13
+ attr_reader :columns
14
+
15
+ def initialize(columns = {})
16
+ super()
17
+
18
+ unless columns.is_a? BasicGrid
19
+ columns = BasicGrid[columns]
20
+ end
21
+ columns._ensure_uniform_columns!
22
+ @columns = columns
23
+
24
+ @columns.each do |k, v|
25
+ @columns[k] = Column.make(v)
26
+ end
27
+ end
28
+
29
+ def add_column(name, typecode='object', *args)
30
+ col = ObjectTable::Column.new(typecode, *args, nrows)
31
+ columns[name] = col
32
+ end
33
+
34
+ def stack!(*others)
35
+ new_values = Hash.new{ [] }
36
+
37
+ others.each do |x|
38
+ case x
39
+ when ObjectTable::TableMethods
40
+ x = x.columns
41
+ when BasicGrid
42
+ x._ensure_uniform_columns!
43
+ end
44
+
45
+ raise "Don't know how to append a #{x.class}" unless x.is_a?(BasicGrid)
46
+ raise 'Mismatch in column names' unless (colnames | x.keys).length == colnames.length
47
+
48
+ x.each do |k, v|
49
+ v = v.to_a if v.is_a? NArray
50
+ new_values[k] += v
51
+ end
52
+ end
53
+
54
+ return self if new_values.empty?
55
+
56
+ new_values.each do |k, v|
57
+ @columns[k] = Column.make(@columns[k].to_a + v)
58
+ end
59
+ self
60
+ end
61
+
62
+ def self.stack(*values)
63
+ return self.new if values.empty?
64
+ base = values.shift
65
+
66
+ case base
67
+ when BasicGrid
68
+ base = self.new(base.clone)
69
+ when ObjectTable, ObjectTable::View
70
+ base = base.clone
71
+ else
72
+ raise "Don't know how to join a #{base.class}"
73
+ end
74
+ base.stack!(*values)
75
+ end
76
+
77
+ def sort_by!(*keys)
78
+ sort_index = _get_sort_index(keys)
79
+
80
+ columns.each do |k, v|
81
+ columns[k] = v[sort_index]
82
+ end
83
+ self
84
+ end
85
+
86
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'object_table/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "object_table"
8
+ spec.version = ObjectTable::VERSION
9
+ spec.authors = ["Cheney Lin"]
10
+ spec.email = ["lincheney@gmail.com"]
11
+ spec.summary = %q{Simple data table table implementation in ruby}
12
+ spec.description = spec.summary
13
+ spec.homepage = "https://github.com/lincheney/ruby-object-table"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency 'narray', "~> 0.6"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency 'rspec', "~> 3.1"
26
+ end
27
+
@@ -0,0 +1,80 @@
1
+ require 'object_table/basic_grid'
2
+
3
+ describe ObjectTable::BasicGrid do
4
+ # describe '.[]' do
5
+ # subject{ ObjectTable::BasicGrid[] }
6
+ #
7
+ # it 'should ensure the columns have the same number of rows' do
8
+ # expect_any_instance_of(ObjectTable::BasicGrid).to receive(:_ensure_uniform_columns!)
9
+ # subject
10
+ # end
11
+ # end
12
+
13
+ describe '#_ensure_uniform_columns!' do
14
+ let(:grid){ ObjectTable::BasicGrid[columns] }
15
+
16
+ subject{ grid._ensure_uniform_columns! }
17
+
18
+ context 'with rows of the same length' do
19
+ let(:columns){ {col1: [1, 2, 3], col2: [1, 2, 3]} }
20
+ it 'should succeed' do
21
+ subject
22
+ expect(grid[:col1]).to eql columns[:col1]
23
+ expect(grid[:col2]).to eql columns[:col2]
24
+ end
25
+ end
26
+
27
+ context 'with rows of differing length' do
28
+ let(:columns){ {col1: [1, 2, 3], col2: [1, 2, 3, 4]} }
29
+ it 'should fail' do
30
+ expect{subject}.to raise_error
31
+ end
32
+ end
33
+
34
+ context 'with a mix of scalars and rows' do
35
+ let(:columns){ {col1: [1, 2, 3], col2: [1, 2, 3], col3: 6} }
36
+ it 'should recycle the scalar into a full column' do
37
+ subject
38
+ expect(grid[:col3]).to eql [6] * 3
39
+ end
40
+ end
41
+
42
+ context 'with scalars only' do
43
+ let(:columns){ {col1: 1, col2: 2} }
44
+ it 'should assume there is one row' do
45
+ subject
46
+ expect(grid[:col1]).to eql [columns[:col1]]
47
+ expect(grid[:col2]).to eql [columns[:col2]]
48
+ end
49
+ end
50
+
51
+ context 'with ranges' do
52
+ let(:columns){ {col1: 0...3} }
53
+ it 'should succeed' do
54
+ subject
55
+ expect(grid[:col1]).to eql columns[:col1]
56
+ end
57
+ end
58
+
59
+ context 'with rank>2 narrays' do
60
+ context 'with the correct last dimension' do
61
+ let(:columns) { {col1: NArray[[1, 1], [2, 2], [3, 3]], col2: [1, 2, 3]} }
62
+
63
+ it 'should succeed' do
64
+ subject
65
+ expect(grid).to eql columns
66
+ end
67
+ end
68
+
69
+ context 'with an incorrect last dimension' do
70
+ let(:columns) { {col1: NArray[[1, 2, 3]], col2: [1, 2, 3]} }
71
+
72
+ it 'should succeed' do
73
+ expect{subject}.to raise_error
74
+ end
75
+ end
76
+ end
77
+
78
+ end
79
+
80
+ end
@@ -0,0 +1,128 @@
1
+ require 'object_table/column'
2
+
3
+ shared_examples 'a column coercer' do |value|
4
+ subject{ ObjectTable::Column.make(value) }
5
+
6
+ it "should convert #{value.class} into a column" do
7
+ expect(subject).to be_a ObjectTable::Column
8
+ expect(subject.to_a).to eql value.to_a
9
+ end
10
+ end
11
+
12
+ shared_examples 'a NArray' do |operator, options={}|
13
+ unary = options[:unary]
14
+
15
+ let(:x){ ObjectTable::Column.make(0..10) }
16
+ let(:y){ ObjectTable::Column.make(5..15) }
17
+
18
+ let(:x_na){ NArray.to_na((0..10).to_a) }
19
+ let(:y_na){ NArray.to_na((5..15).to_a) }
20
+
21
+ if unary
22
+ subject{ x.send(operator) }
23
+ let(:expected_result){ x_na.send(operator) }
24
+ else
25
+ subject{ x.send(operator, y) }
26
+ let(:expected_result){ x_na.send(operator, y_na) }
27
+ end
28
+
29
+ it "should give the correct result for :#{operator}" do
30
+ expect(subject.to_a).to eql expected_result.to_a
31
+ end
32
+ end
33
+
34
+ shared_examples 'a vectorized operator' do |method|
35
+ it "should vectorize :#{method} over the array" do
36
+ expect(subject.send(method).to_a).to eql subject.to_a.map{|x| x.send(method)}
37
+ end
38
+ end
39
+
40
+ describe ObjectTable::Column do
41
+
42
+ describe '.make' do
43
+ subject{ ObjectTable::Column.make(value) }
44
+
45
+ context 'on a Column' do
46
+ let(:value){ ObjectTable::Column[1, 2, 3] }
47
+
48
+ it 'should return the same column' do
49
+ expect(subject).to be value
50
+ end
51
+ end
52
+
53
+ it_behaves_like "a column coercer", NArray[1, 2, 3]
54
+ it_behaves_like "a column coercer", 0...100
55
+ it_behaves_like "a column coercer", [1, 2, 3]
56
+
57
+ context 'on something unsupported' do
58
+ let(:value){ Object.new }
59
+
60
+ it 'should fail' do
61
+ expect{subject}.to raise_error
62
+ end
63
+ end
64
+
65
+ end
66
+
67
+ describe '#get_rows' do
68
+ let(:value) { NArray.float(50, 50, 50, 50).random! }
69
+ let(:column) { ObjectTable::Column.make(value) }
70
+ let(:index) { 30 }
71
+
72
+ subject{ column.get_rows(index) }
73
+
74
+ it 'should retrieve the row from the last dimension' do
75
+ expect(subject).to eql column[nil, nil, nil, index]
76
+ end
77
+ end
78
+
79
+ describe '#uniq' do
80
+ subject{ ObjectTable::Column.make([1, 1, 2, 2, 3, 1]) }
81
+
82
+ it 'should return a column of unique elements' do
83
+ expect(subject.uniq).to be_a ObjectTable::Column
84
+ expect(subject.uniq.to_a).to eql subject.to_a.uniq
85
+ end
86
+
87
+ end
88
+
89
+ describe 'vectorisation' do
90
+ subject{ ObjectTable::Column.make(Date.today ... (Date.today+100)) }
91
+
92
+ it_behaves_like 'a vectorized operator', 'day'
93
+ it_behaves_like 'a vectorized operator', 'month'
94
+ it_behaves_like 'a vectorized operator', 'year'
95
+ end
96
+
97
+ describe 'operations' do
98
+ it_behaves_like 'a NArray', '*'
99
+ it_behaves_like 'a NArray', '+'
100
+ it_behaves_like 'a NArray', '/'
101
+ it_behaves_like 'a NArray', '-'
102
+ it_behaves_like 'a NArray', '%'
103
+ it_behaves_like 'a NArray', '**'
104
+ it_behaves_like 'a NArray', '&'
105
+ it_behaves_like 'a NArray', '|'
106
+ it_behaves_like 'a NArray', '^'
107
+ it_behaves_like 'a NArray', 'eq'
108
+ it_behaves_like 'a NArray', 'ne'
109
+ it_behaves_like 'a NArray', 'gt'
110
+ it_behaves_like 'a NArray', '>'
111
+ it_behaves_like 'a NArray', 'ge'
112
+ it_behaves_like 'a NArray', '>='
113
+ it_behaves_like 'a NArray', 'lt'
114
+ it_behaves_like 'a NArray', '<'
115
+ it_behaves_like 'a NArray', 'le'
116
+ it_behaves_like 'a NArray', '<='
117
+ it_behaves_like 'a NArray', 'and'
118
+ it_behaves_like 'a NArray', 'or'
119
+ it_behaves_like 'a NArray', 'xor'
120
+ it_behaves_like 'a NArray', 'to_type'
121
+
122
+ it_behaves_like 'a NArray', '~', unary: true
123
+ it_behaves_like 'a NArray', '-@', unary: true
124
+ it_behaves_like 'a NArray', 'abs', unary: true
125
+ it_behaves_like 'a NArray', 'not', unary: true
126
+ end
127
+
128
+ end