charty 0.2.3 → 0.2.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +56 -23
  3. data/.github/workflows/nmatrix.yml +67 -0
  4. data/.github/workflows/pycall.yml +86 -0
  5. data/Gemfile +18 -0
  6. data/README.md +172 -4
  7. data/Rakefile +4 -5
  8. data/charty.gemspec +10 -6
  9. data/examples/sample_images/hist_gruff.png +0 -0
  10. data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
  11. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  12. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  13. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  14. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  15. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  16. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  17. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  18. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  19. data/lib/charty.rb +8 -1
  20. data/lib/charty/backends/bokeh.rb +2 -2
  21. data/lib/charty/backends/google_charts.rb +1 -1
  22. data/lib/charty/backends/gruff.rb +14 -3
  23. data/lib/charty/backends/plotly.rb +731 -32
  24. data/lib/charty/backends/plotly_helpers/html_renderer.rb +203 -0
  25. data/lib/charty/backends/plotly_helpers/notebook_renderer.rb +87 -0
  26. data/lib/charty/backends/plotly_helpers/plotly_renderer.rb +121 -0
  27. data/lib/charty/backends/pyplot.rb +514 -66
  28. data/lib/charty/backends/rubyplot.rb +1 -1
  29. data/lib/charty/cache_dir.rb +27 -0
  30. data/lib/charty/dash_pattern_generator.rb +57 -0
  31. data/lib/charty/index.rb +213 -0
  32. data/lib/charty/iruby_helper.rb +18 -0
  33. data/lib/charty/linspace.rb +1 -1
  34. data/lib/charty/plot_methods.rb +283 -8
  35. data/lib/charty/plotter.rb +2 -2
  36. data/lib/charty/plotters.rb +11 -0
  37. data/lib/charty/plotters/abstract_plotter.rb +186 -16
  38. data/lib/charty/plotters/bar_plotter.rb +189 -7
  39. data/lib/charty/plotters/box_plotter.rb +64 -11
  40. data/lib/charty/plotters/categorical_plotter.rb +272 -40
  41. data/lib/charty/plotters/count_plotter.rb +7 -0
  42. data/lib/charty/plotters/distribution_plotter.rb +143 -0
  43. data/lib/charty/plotters/estimation_support.rb +84 -0
  44. data/lib/charty/plotters/histogram_plotter.rb +186 -0
  45. data/lib/charty/plotters/line_plotter.rb +300 -0
  46. data/lib/charty/plotters/random_support.rb +25 -0
  47. data/lib/charty/plotters/relational_plotter.rb +635 -0
  48. data/lib/charty/plotters/scatter_plotter.rb +80 -0
  49. data/lib/charty/plotters/vector_plotter.rb +6 -0
  50. data/lib/charty/statistics.rb +96 -2
  51. data/lib/charty/table.rb +160 -15
  52. data/lib/charty/table_adapters.rb +2 -0
  53. data/lib/charty/table_adapters/active_record_adapter.rb +17 -9
  54. data/lib/charty/table_adapters/base_adapter.rb +166 -0
  55. data/lib/charty/table_adapters/daru_adapter.rb +39 -3
  56. data/lib/charty/table_adapters/datasets_adapter.rb +13 -2
  57. data/lib/charty/table_adapters/hash_adapter.rb +141 -16
  58. data/lib/charty/table_adapters/narray_adapter.rb +25 -6
  59. data/lib/charty/table_adapters/nmatrix_adapter.rb +15 -5
  60. data/lib/charty/table_adapters/pandas_adapter.rb +163 -0
  61. data/lib/charty/util.rb +28 -0
  62. data/lib/charty/vector.rb +69 -0
  63. data/lib/charty/vector_adapters.rb +187 -0
  64. data/lib/charty/vector_adapters/array_adapter.rb +101 -0
  65. data/lib/charty/vector_adapters/daru_adapter.rb +163 -0
  66. data/lib/charty/vector_adapters/narray_adapter.rb +182 -0
  67. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  68. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  69. data/lib/charty/vector_adapters/pandas_adapter.rb +199 -0
  70. data/lib/charty/version.rb +1 -1
  71. metadata +92 -25
@@ -1,13 +1,13 @@
1
1
  module Charty
2
2
  module TableAdapters
3
- class NArrayAdapter
3
+ class NArrayAdapter < BaseAdapter
4
4
  TableAdapters.register(:narray, self)
5
5
 
6
6
  def self.supported?(data)
7
7
  defined?(Numo::NArray) && data.is_a?(Numo::NArray) && data.ndim <= 2
8
8
  end
9
9
 
10
- def initialize(data, columns: nil)
10
+ def initialize(data, columns: nil, index: nil)
11
11
  case data.ndim
12
12
  when 1
13
13
  data = data.reshape(data.length, 1)
@@ -17,23 +17,42 @@ module Charty
17
17
  raise ArgumentError, "Unsupported data format"
18
18
  end
19
19
  @data = data
20
- @column_names = generate_column_names(data.shape[1], columns)
20
+ self.columns = Index.new(generate_column_names(data.shape[1], columns))
21
+ self.index = index || RangeIndex.new(0 ... length)
21
22
  end
22
23
 
23
- attr_reader :column_names, :data
24
+ attr_reader :data
25
+
26
+ def length
27
+ data.shape[0]
28
+ end
29
+
30
+ def column_length
31
+ data.shape[1]
32
+ end
33
+
34
+ def compare_data_equality(other)
35
+ case other
36
+ when NArrayAdapter
37
+ data == other.data
38
+ else
39
+ super
40
+ end
41
+ end
24
42
 
25
43
  def [](row, column)
26
44
  if row
27
45
  @data[row, resolve_column_index(column)]
28
46
  else
29
- @data[true, resolve_column_index(column)]
47
+ column_data = @data[true, resolve_column_index(column)]
48
+ Charty::Vector.new(column_data, index: index, name: column)
30
49
  end
31
50
  end
32
51
 
33
52
  private def resolve_column_index(column)
34
53
  case column
35
54
  when String, Symbol
36
- index = column_names.index(column.to_s)
55
+ index = column_names.index(column.to_sym) || column_names.index(column.to_s)
37
56
  return index if index
38
57
  raise IndexError, "invalid column name: #{column}"
39
58
  when Integer
@@ -1,6 +1,6 @@
1
1
  module Charty
2
2
  module TableAdapters
3
- class NMatrixAdapter
3
+ class NMatrixAdapter < BaseAdapter
4
4
  TableAdapters.register(:nmatrix, self)
5
5
 
6
6
  def self.supported?(data)
@@ -17,23 +17,33 @@ module Charty
17
17
  raise ArgumentError, "Unsupported data format"
18
18
  end
19
19
  @data = data
20
- @column_names = generate_column_names(data.shape[1], columns)
20
+ self.columns = Index.new(generate_column_names(data.shape[1], columns))
21
+ self.index = index || RangeIndex.new(0 ... length)
21
22
  end
22
23
 
23
- attr_reader :column_names, :data
24
+ attr_reader :data
25
+
26
+ def length
27
+ data.shape[0]
28
+ end
29
+
30
+ def column_length
31
+ data.shape[1]
32
+ end
24
33
 
25
34
  def [](row, column)
26
35
  if row
27
36
  @data[row, resolve_column_index(column)]
28
37
  else
29
- @data[:*, resolve_column_index(column)].reshape([@data.shape[0]])
38
+ column_data = @data[:*, resolve_column_index(column)].reshape([@data.shape[0]])
39
+ Charty::Vector.new(column_data, index: index, name: column)
30
40
  end
31
41
  end
32
42
 
33
43
  private def resolve_column_index(column)
34
44
  case column
35
45
  when String, Symbol
36
- index = column_names.index(column.to_s)
46
+ index = column_names.index(column.to_sym) || column_names.index(column.to_s)
37
47
  return index if index
38
48
  raise IndexError, "invalid column name: #{column}"
39
49
  when Integer
@@ -0,0 +1,163 @@
1
+ require "forwardable"
2
+
3
+ module Charty
4
+ module TableAdapters
5
+ class PandasDataFrameAdapter < BaseAdapter
6
+ TableAdapters.register(:pandas_data_frame, self)
7
+
8
+ extend Forwardable
9
+ include Enumerable
10
+
11
+ def self.supported?(data)
12
+ defined?(Pandas::DataFrame) && data.is_a?(Pandas::DataFrame)
13
+ end
14
+
15
+ def initialize(data, columns: nil, index: nil)
16
+ @data = check_type(Pandas::DataFrame, data, :data)
17
+
18
+ self.columns = columns unless columns.nil?
19
+ self.index = index unless index.nil?
20
+ end
21
+
22
+ attr_reader :data
23
+
24
+ def length
25
+ data.shape[0]
26
+ end
27
+
28
+ def columns
29
+ PandasIndex.new(data.columns)
30
+ end
31
+
32
+ def columns=(new_columns)
33
+ case new_columns
34
+ when PandasIndex
35
+ data.columns = new_columns.values
36
+ data.columns.name = new_columns.name
37
+ when Index
38
+ data.columns = new_columns.to_a
39
+ data.columns.name = new_columns.name
40
+ else
41
+ data.columns = new_columns
42
+ end
43
+ end
44
+
45
+ def index
46
+ PandasIndex.new(data.index)
47
+ end
48
+
49
+ def index=(new_index)
50
+ case new_index
51
+ when PandasIndex
52
+ data.index = new_index.values
53
+ data.index.name = new_index.name
54
+ when Index
55
+ data.index = new_index.to_a
56
+ data.index.name = new_index.name
57
+ else
58
+ data.index = new_index
59
+ end
60
+ end
61
+
62
+ def column_names
63
+ @data.columns.to_a
64
+ end
65
+
66
+ def compare_data_equality(other)
67
+ case other
68
+ when PandasDataFrameAdapter
69
+ data.equals(other.data)
70
+ else
71
+ super
72
+ end
73
+ end
74
+
75
+ def [](row, column)
76
+ if row
77
+ @data[column][row]
78
+ else
79
+ Vector.new(@data[column])
80
+ end
81
+ end
82
+
83
+ def drop_na
84
+ Charty::Table.new(@data.dropna)
85
+ end
86
+
87
+ def sort_values(by, na_position: :last)
88
+ Charty::Table.new(@data.sort_values(by, na_position: na_position, kind: :mergesort))
89
+ end
90
+
91
+ private def check_type(type, data, name)
92
+ return data if data.is_a?(type)
93
+ raise TypeError, "#{name} must be a #{type}"
94
+ end
95
+
96
+ def group_by(_table, grouper, sort, drop_na)
97
+ GroupBy.new(@data.groupby(by: grouper, sort: sort, dropna: drop_na))
98
+ end
99
+
100
+ def reset_index
101
+ Charty::Table.new(data.reset_index)
102
+ end
103
+
104
+ class GroupBy < Charty::Table::GroupByBase
105
+ def initialize(groupby)
106
+ @groupby = groupby
107
+ end
108
+
109
+ def indices
110
+ @groupby.indices.map { |k, v|
111
+ [k, v.to_a]
112
+ }.to_h
113
+ end
114
+
115
+ def group_keys
116
+ each_group_key.to_a
117
+ end
118
+
119
+ def each_group_key
120
+ return enum_for(__method__) unless block_given?
121
+
122
+ if PyCall.respond_to?(:iterable)
123
+ PyCall.iterable(@groupby).each do |key, index|
124
+ if key.class == PyCall.builtins.tuple
125
+ key = key.to_a
126
+ end
127
+ yield key
128
+ end
129
+ else # TODO: Remove this clause after the new PyCall will be released
130
+ iter = @groupby.__iter__()
131
+ while true
132
+ begin
133
+ key, sub_data = iter.__next__
134
+ if key.class == PyCall.builtins.tuple
135
+ key = key.to_a
136
+ end
137
+ yield key
138
+ rescue PyCall::PyError => error
139
+ if error.type == PyCall.builtins.StopIteration
140
+ break
141
+ else
142
+ raise error
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ def apply(*args, &block)
150
+ res = @groupby.apply(->(data) {
151
+ res = block.call(Charty::Table.new(data), *args)
152
+ Pandas::Series.new(data: res)
153
+ })
154
+ Charty::Table.new(res)
155
+ end
156
+
157
+ def [](key)
158
+ Charty::Table.new(@groupby.get_group(key))
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,28 @@
1
+ module Charty
2
+ module Util
3
+ if [].respond_to?(:filter_map)
4
+ module_function def filter_map(enum, &block)
5
+ enum.filter_map(&block)
6
+ end
7
+ else
8
+ module_function def filter_map(enum, &block)
9
+ enum.inject([]) do |acc, x|
10
+ y = block.call(x)
11
+ if y
12
+ acc.push(y)
13
+ else
14
+ acc
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ module_function def missing?(val)
21
+ val.nil? || nan?(val)
22
+ end
23
+
24
+ module_function def nan?(val)
25
+ val.respond_to?(:nan?) && val.nan?
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,69 @@
1
+ require "forwardable"
2
+
3
+ module Charty
4
+ class Vector
5
+ extend Forwardable
6
+ include Enumerable
7
+
8
+ def self.try_convert(obj)
9
+ case obj
10
+ when self
11
+ obj
12
+ else
13
+ if VectorAdapters.find_adapter_class(obj, exception: false)
14
+ new(obj)
15
+ end
16
+ end
17
+ end
18
+
19
+ def initialize(data, index: nil, name: nil)
20
+ adapter_class = VectorAdapters.find_adapter_class(data)
21
+ @adapter = adapter_class.new(data)
22
+ self.index = index unless index.nil?
23
+ self.name = name unless name.nil?
24
+ end
25
+
26
+ attr_reader :adapter
27
+
28
+ def_delegators :adapter, :data
29
+ def_delegators :adapter, :index, :index=
30
+ def_delegators :adapter, :==, :[], :[]=
31
+
32
+ def_delegators :adapter, :length
33
+ def_delegators :adapter, :name, :name=
34
+
35
+ alias size length
36
+
37
+ def_delegators :adapter, :to_a
38
+ def_delegators :adapter, :each
39
+ def_delegators :adapter, :empty?
40
+
41
+ def_delegators :adapter, :boolean?, :numeric?, :categorical?
42
+ def_delegators :adapter, :categories
43
+ def_delegators :adapter, :unique_values
44
+ def_delegators :adapter, :group_by
45
+ def_delegators :adapter, :drop_na
46
+ def_delegators :adapter, :values_at
47
+
48
+ def_delegators :adapter, :eq, :notnull
49
+
50
+ alias completecases notnull
51
+
52
+ def_delegators :adapter, :mean, :stdev
53
+
54
+ # TODO: write test
55
+ def categorical_order(order=nil)
56
+ if order.nil?
57
+ case
58
+ when categorical?
59
+ order = categories
60
+ else
61
+ order = unique_values.compact
62
+ order.sort! if numeric?
63
+ end
64
+ order.compact!
65
+ end
66
+ order
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,187 @@
1
+ require "forwardable"
2
+
3
+ module Charty
4
+ module VectorAdapters
5
+ class UnsupportedVectorData < StandardError; end
6
+
7
+ @adapters = {}
8
+
9
+ def self.register(name, adapter_class)
10
+ @adapters[name] = adapter_class
11
+ end
12
+
13
+ def self.find_adapter_class(data, exception: true)
14
+ @adapters.each_value do |adapter_class|
15
+ return adapter_class if adapter_class.supported?(data)
16
+ end
17
+ if exception
18
+ raise UnsupportedVectorData, "Unsupported vector data (#{data.class})"
19
+ end
20
+ end
21
+
22
+ class BaseAdapter
23
+ extend Forwardable
24
+ include Enumerable
25
+
26
+ def self.adapter_name
27
+ name[/:?(\w+)Adapter\z/, 1]
28
+ end
29
+
30
+ private def check_data(data)
31
+ return data if self.class.supported?(data)
32
+ raise UnsupportedVectorData, "Unsupported vector data (#{data.class})"
33
+ end
34
+
35
+ attr_reader :data
36
+
37
+ def_delegators :data, :length, :size
38
+
39
+ def ==(other)
40
+ case other.adapter
41
+ when BaseAdapter
42
+ return false if other.index != index
43
+ if respond_to?(:compare_data_equality)
44
+ compare_data_equality(other.adapter)
45
+ elsif other.adapter.respond_to?(:compare_data_equality)
46
+ other.adapter.compare_data_equality(self)
47
+ else
48
+ case other.adapter
49
+ when self.class
50
+ data == other.data
51
+ else
52
+ false
53
+ end
54
+ end
55
+ else
56
+ super
57
+ end
58
+ end
59
+
60
+ def_delegators :data, :[], :[]=
61
+ def_delegators :data, :each, :to_a, :empty?
62
+
63
+ # Take values at the given positional indices (without indexing)
64
+ def values_at(*indices)
65
+ indices.map {|i| data[i] }
66
+ end
67
+
68
+ def where_in_array(mask)
69
+ mask = check_mask_vector(mask)
70
+ masked_data = []
71
+ masked_index = []
72
+ mask.each_with_index do |f, i|
73
+ case f
74
+ when true, 1
75
+ masked_data << data[i]
76
+ masked_index << index[i]
77
+ end
78
+ end
79
+ return masked_data, masked_index
80
+ end
81
+
82
+ private def check_mask_vector(mask)
83
+ # ensure mask is boolean vector
84
+ case mask
85
+ when Charty::Vector
86
+ unless mask.boolean?
87
+ raise ArgumentError, "Unable to lookup items by a nonboolean vector"
88
+ end
89
+ mask
90
+ else
91
+ Charty::Vector.new(mask)
92
+ end
93
+ end
94
+
95
+ def mean
96
+ Statistics.mean(data)
97
+ end
98
+
99
+ def stdev(population: false)
100
+ Statistics.stdev(data, population: population)
101
+ end
102
+ end
103
+
104
+ module NameSupport
105
+ attr_reader :name
106
+
107
+ def name=(value)
108
+ @name = check_name(value)
109
+ end
110
+
111
+ private def check_name(value)
112
+ value = String.try_convert(value) || value
113
+ case value
114
+ when String, Symbol
115
+ value
116
+ else
117
+ raise ArgumentError,
118
+ "name must be a String or a Symbol (#{value.class} is given)"
119
+ end
120
+ end
121
+ end
122
+
123
+ module IndexSupport
124
+ attr_reader :index
125
+
126
+ def [](key)
127
+ case key
128
+ when Charty::Vector
129
+ where(key)
130
+ else
131
+ super(key_to_loc(key))
132
+ end
133
+ end
134
+
135
+ def []=(key, val)
136
+ super(key_to_loc(key), val)
137
+ end
138
+
139
+ private def key_to_loc(key)
140
+ loc = self.index.loc(key)
141
+ if loc.nil?
142
+ if key.respond_to?(:to_int)
143
+ loc = key.to_int
144
+ else
145
+ raise KeyError.new("key not found: %p" % key,
146
+ receiver: __method__, key: key)
147
+ end
148
+ end
149
+ loc
150
+ end
151
+
152
+ def index=(values)
153
+ @index = check_and_convert_index(values, :index, length)
154
+ end
155
+
156
+ private def check_and_convert_index(values, name, expected_length)
157
+ case values
158
+ when Index, Range
159
+ else
160
+ unless (ary = Array.try_convert(values))
161
+ raise ArgumentError, "invalid object for %s: %p" % [name, values]
162
+ end
163
+ values = ary
164
+ end
165
+ if expected_length != values.size
166
+ raise ArgumentError,
167
+ "invalid length for %s (%d for %d)" % [name, values.size, expected_length]
168
+ end
169
+ case values
170
+ when Index
171
+ values
172
+ when Range
173
+ RangeIndex.new(values)
174
+ when Array
175
+ Index.new(values)
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ require_relative "vector_adapters/array_adapter"
183
+ require_relative "vector_adapters/daru_adapter"
184
+ require_relative "vector_adapters/narray_adapter"
185
+ require_relative "vector_adapters/nmatrix_adapter"
186
+ require_relative "vector_adapters/numpy_adapter"
187
+ require_relative "vector_adapters/pandas_adapter"