charty 0.1.4.dev → 0.2.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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +71 -0
  3. data/.github/workflows/nmatrix.yml +67 -0
  4. data/.github/workflows/pycall.yml +86 -0
  5. data/Dockerfile.dev +9 -1
  6. data/Gemfile +18 -0
  7. data/README.md +128 -9
  8. data/Rakefile +4 -5
  9. data/charty.gemspec +7 -2
  10. data/examples/Gemfile +1 -0
  11. data/examples/active_record.ipynb +34 -34
  12. data/examples/daru.ipynb +71 -29
  13. data/examples/iris_dataset.ipynb +12 -5
  14. data/examples/nmatrix.ipynb +30 -30
  15. data/examples/numo_narray.ipynb +245 -0
  16. data/examples/palette.rb +71 -0
  17. data/examples/sample.png +0 -0
  18. data/examples/sample_bokeh.ipynb +156 -0
  19. data/examples/sample_google_chart.ipynb +229 -68
  20. data/examples/sample_gruff.ipynb +148 -133
  21. data/examples/sample_images/bar_bokeh.html +85 -0
  22. data/examples/sample_images/barh_bokeh.html +85 -0
  23. data/examples/sample_images/barh_gruff.png +0 -0
  24. data/examples/sample_images/box_plot_bokeh.html +85 -0
  25. data/examples/sample_images/{boxplot_pyplot.png → box_plot_pyplot.png} +0 -0
  26. data/examples/sample_images/curve_bokeh.html +85 -0
  27. data/examples/sample_images/curve_with_function_bokeh.html +85 -0
  28. data/examples/sample_images/{errorbar_pyplot.png → error_bar_pyplot.png} +0 -0
  29. data/examples/sample_images/hist_gruff.png +0 -0
  30. data/examples/sample_images/scatter_bokeh.html +85 -0
  31. data/examples/sample_pyplot.ipynb +37 -35
  32. data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
  33. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  34. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  35. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  36. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  37. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  38. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  39. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  40. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  41. data/lib/charty.rb +13 -7
  42. data/lib/charty/backend_methods.rb +8 -0
  43. data/lib/charty/backends.rb +80 -0
  44. data/lib/charty/backends/bokeh.rb +80 -0
  45. data/lib/charty/backends/google_charts.rb +267 -0
  46. data/lib/charty/backends/gruff.rb +104 -67
  47. data/lib/charty/backends/plotly.rb +549 -0
  48. data/lib/charty/backends/pyplot.rb +584 -86
  49. data/lib/charty/backends/rubyplot.rb +82 -74
  50. data/lib/charty/backends/unicode_plot.rb +79 -0
  51. data/lib/charty/index.rb +213 -0
  52. data/lib/charty/linspace.rb +1 -1
  53. data/lib/charty/missing_value_support.rb +14 -0
  54. data/lib/charty/plot_methods.rb +184 -0
  55. data/lib/charty/plotter.rb +57 -41
  56. data/lib/charty/plotters.rb +11 -0
  57. data/lib/charty/plotters/abstract_plotter.rb +156 -0
  58. data/lib/charty/plotters/bar_plotter.rb +216 -0
  59. data/lib/charty/plotters/box_plotter.rb +94 -0
  60. data/lib/charty/plotters/categorical_plotter.rb +380 -0
  61. data/lib/charty/plotters/count_plotter.rb +7 -0
  62. data/lib/charty/plotters/estimation_support.rb +84 -0
  63. data/lib/charty/plotters/random_support.rb +25 -0
  64. data/lib/charty/plotters/relational_plotter.rb +518 -0
  65. data/lib/charty/plotters/scatter_plotter.rb +115 -0
  66. data/lib/charty/plotters/vector_plotter.rb +6 -0
  67. data/lib/charty/statistics.rb +114 -0
  68. data/lib/charty/table.rb +82 -3
  69. data/lib/charty/table_adapters.rb +25 -0
  70. data/lib/charty/table_adapters/active_record_adapter.rb +63 -0
  71. data/lib/charty/table_adapters/base_adapter.rb +69 -0
  72. data/lib/charty/table_adapters/daru_adapter.rb +70 -0
  73. data/lib/charty/table_adapters/datasets_adapter.rb +49 -0
  74. data/lib/charty/table_adapters/hash_adapter.rb +224 -0
  75. data/lib/charty/table_adapters/narray_adapter.rb +76 -0
  76. data/lib/charty/table_adapters/nmatrix_adapter.rb +67 -0
  77. data/lib/charty/table_adapters/pandas_adapter.rb +81 -0
  78. data/lib/charty/vector.rb +69 -0
  79. data/lib/charty/vector_adapters.rb +183 -0
  80. data/lib/charty/vector_adapters/array_adapter.rb +109 -0
  81. data/lib/charty/vector_adapters/daru_adapter.rb +171 -0
  82. data/lib/charty/vector_adapters/narray_adapter.rb +187 -0
  83. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  84. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  85. data/lib/charty/vector_adapters/pandas_adapter.rb +200 -0
  86. data/lib/charty/version.rb +1 -1
  87. metadata +127 -13
  88. data/.travis.yml +0 -11
  89. data/examples/numo-narray.ipynb +0 -234
  90. data/lib/charty/backends/google_chart.rb +0 -167
  91. data/lib/charty/plotter_adapter.rb +0 -17
@@ -0,0 +1,115 @@
1
+ module Charty
2
+ module Plotters
3
+ class ScatterPlotter < RelationalPlotter
4
+ def initialize(data: nil, variables: {}, **options, &block)
5
+ x, y, color, style, size = variables.values_at(:x, :y, :color, :style, :size)
6
+ super(x, y, color, style, size, data: data, **options, &block)
7
+ end
8
+
9
+ attr_reader :alpha, :legend
10
+
11
+ def alpha=(val)
12
+ case val
13
+ when nil, :auto, 0..1
14
+ @alpha = val
15
+ when "auto"
16
+ @alpha = val.to_sym
17
+ when Numeric
18
+ raise ArgumentError,
19
+ "the given alpha is out of bounds " +
20
+ "(%p for nil, :auto, or number 0..1)" % val
21
+ else
22
+ raise ArgumentError,
23
+ "invalid value of alpha " +
24
+ "(%p for nil, :auto, or number in 0..1)" % val
25
+ end
26
+ end
27
+
28
+ def legend=(val)
29
+ case val
30
+ when :auto, :brief, :full, false
31
+ @legend = val
32
+ when "auto", "brief", "full"
33
+ @legend = val.to_sym
34
+ else
35
+ raise ArgumentError,
36
+ "invalid value of legend (%p for :auto, :brief, :full, or false)" % val
37
+ end
38
+ end
39
+
40
+ attr_reader :line_width, :edge_color
41
+
42
+ def line_width=(val)
43
+ @line_width = check_number(val, :line_width, allow_nil: true)
44
+ end
45
+
46
+ def edge_color=(val)
47
+ @line_width = check_color(val, :edge_color, allow_nil: true)
48
+ end
49
+
50
+ def render
51
+ backend = Backends.current
52
+ backend.begin_figure
53
+ draw_points(backend)
54
+ annotate_axes(backend)
55
+ backend.show
56
+ end
57
+
58
+ def save(filename, **opts)
59
+ backend = Backends.current
60
+ backend.begin_figure
61
+ draw_points(backend)
62
+ annotate_axes(backend)
63
+ backend.save(filename, **opts)
64
+ end
65
+
66
+ private def draw_points(backend)
67
+ map_color(palette: palette, order: color_order, norm: color_norm)
68
+ map_size(sizes: sizes, order: size_order, norm: size_norm)
69
+ map_style(markers: markers, order: marker_order)
70
+
71
+ data = @plot_data.drop_na
72
+
73
+ # TODO: shold pass key_color to backend's scatter method.
74
+ # In pyplot backend, it is passed as color parameter.
75
+
76
+ x = data[:x]
77
+ y = data[:y]
78
+ color = data[:color] if @variables.key?(:color)
79
+ style = data[:style] if @variables.key?(:style)
80
+ size = data[:size] if @variables.key?(:size)
81
+
82
+ # TODO: key_color
83
+ backend.scatter(
84
+ x, y, @variables,
85
+ color: color, color_mapper: @color_mapper,
86
+ style: style, style_mapper: @style_mapper,
87
+ size: size, size_mapper: @size_mapper,
88
+ legend: legend
89
+ )
90
+ end
91
+
92
+ private def annotate_axes(backend)
93
+ xlabel = self.variables[:x]
94
+ ylabel = self.variables[:y]
95
+ backend.set_xlabel(xlabel) unless xlabel.nil?
96
+ backend.set_ylabel(ylabel) unless ylabel.nil?
97
+
98
+ if legend
99
+ add_legend_data(backend)
100
+ end
101
+ end
102
+
103
+ private def add_legend_data(backend)
104
+ # TODO: Legend Support
105
+ verbosity = legend
106
+ verbosity = :auto if verbosity == true
107
+
108
+ titles = [:color, :size, :style].filter_map do |v|
109
+ variables[v] if variables.key?(v)
110
+ end
111
+ legend_title = titles.length == 1 ? titles[0] : ""
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,6 @@
1
+ module Charty
2
+ module Plotters
3
+ class VectorPlotter < AbstractPlotter
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,114 @@
1
+ module Charty
2
+ module Statistics
3
+ begin
4
+ require "enumerable/statistics"
5
+
6
+ def self.mean(enum)
7
+ enum.mean
8
+ end
9
+
10
+ def self.stdev(enum, population: false)
11
+ enum.stdev(population: population)
12
+ end
13
+ rescue LoadError
14
+ def self.mean(enum)
15
+ xs = enum.to_a
16
+ xs.sum / xs.length.to_f
17
+ end
18
+
19
+ def self.stdev(enum, population: false)
20
+ xs = enum.to_a
21
+ n = xs.length
22
+ mean = xs.sum.to_f / n
23
+ ddof = population ? 0 : 1
24
+ var = xs.map {|x| (x - mean)**2 }.sum / (n - ddof)
25
+ Math.sqrt(var)
26
+ end
27
+ end
28
+
29
+ def self.bootstrap(vector, n_boot: 2000, func: :mean, units: nil, random: nil)
30
+ n = vector.size
31
+ random = Charty::Plotters::RandomSupport.check_random(random)
32
+ func = Charty::Plotters::EstimationSupport.check_estimator(func)
33
+
34
+ if units
35
+ return structured_bootstrap(vector, n_boot, units, func, random)
36
+ end
37
+
38
+ if defined?(Pandas::Series) || defined?(Numpy::NDArray)
39
+ boot_dist = bootstrap_optimized_for_pycall(vector, n_boot, random, func)
40
+ return boot_dist if boot_dist
41
+ end
42
+
43
+ boot_dist = Array.new(n_boot) do |i|
44
+ resampler = Array.new(n) { random.rand(n) }
45
+
46
+ w ||= vector.values_at(*resampler)
47
+
48
+ case func
49
+ when :mean
50
+ mean(w)
51
+ end
52
+ end
53
+
54
+ boot_dist
55
+ end
56
+
57
+ private_class_method def self.bootstrap_optimized_for_pycall(vector, n_boot, random, func)
58
+ case
59
+ when vector.is_a?(Charty::Vector)
60
+ bootstrap_optimized_for_pycall(vector.data, n_boot, random, func)
61
+
62
+ when defined?(Pandas::Series) && vector.is_a?(Pandas::Series) || vector.is_a?(Numpy::NDArray)
63
+ # numpy is also available when pandas is available
64
+ n = vector.size
65
+ resampler = Numpy.empty(n, dtype: Numpy.intp)
66
+ Array.new(n_boot) do |i|
67
+ # TODO: Use Numo and MemoryView to reduce execution time
68
+ # resampler = Numo::Int64.new(n).rand(n)
69
+ # w = Numpy.take(vector, resampler)
70
+ n.times {|i| resampler[i] = random.rand(n) }
71
+ w = vector.take(resampler)
72
+
73
+ case func
74
+ when :mean
75
+ w.mean
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ private_class_method def self.structured_bootstrap(vector, n_boot, units, func, random)
82
+ raise NotImplementedError,
83
+ "structured bootstrapping has not been supported yet"
84
+ end
85
+
86
+ def self.bootstrap_ci(*vectors, which, n_boot: 2000, func: :mean, units: nil, random: nil)
87
+ boot = bootstrap(*vectors, n_boot: n_boot, func: func, units: units, random: random)
88
+ q = [50 - which / 2, 50 + which / 2]
89
+ if boot.respond_to?(:percentile)
90
+ boot.percentile(q)
91
+ else
92
+ percentile(boot, q)
93
+ end
94
+ end
95
+
96
+ # TODO: optimize with introselect algorithm
97
+ def self.percentile(a, q)
98
+ return mean(a) if a.size == 0
99
+
100
+ a = a.sort
101
+ n = a.size
102
+ q.map do |x|
103
+ x = n * (x / 100.0)
104
+ i = x.floor
105
+ if i == n-1
106
+ a[i]
107
+ else
108
+ t = x - i
109
+ (1-t)*a[i] + t*a[i+1]
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
data/lib/charty/table.rb CHANGED
@@ -1,11 +1,69 @@
1
+ require 'forwardable'
1
2
 
2
3
  module Charty
4
+ class ColumnAccessor
5
+ def initialize(adapter)
6
+ @adapter = adapter
7
+ end
8
+
9
+ def [](column_name)
10
+ @adapter[nil, column_name]
11
+ end
12
+ end
13
+
3
14
  class Table
4
- def initialize(table)
5
- @table = table
15
+ extend Forwardable
16
+ include MissingValueSupport
17
+
18
+ def initialize(data, **kwargs)
19
+ adapter_class = TableAdapters.find_adapter_class(data)
20
+ if kwargs.empty?
21
+ @adapter = adapter_class.new(data)
22
+ else
23
+ @adapter = adapter_class.new(data, **kwargs)
24
+ end
25
+
26
+ @column_cache = {}
6
27
  end
7
28
 
8
- attr_reader :table
29
+ attr_reader :adapter
30
+
31
+ def_delegators :adapter, :length, :column_length
32
+
33
+ def_delegators :adapter, :columns, :columns=
34
+ def_delegators :adapter, :index, :index=
35
+
36
+ def_delegator :@adapter, :column_names
37
+ def_delegator :@adapter, :data, :raw_data
38
+
39
+ def ==(other)
40
+ return true if equal?(other)
41
+
42
+ case other
43
+ when Charty::Table
44
+ adapter == other.adapter
45
+ else
46
+ super
47
+ end
48
+ end
49
+
50
+ def empty?
51
+ length == 0
52
+ end
53
+
54
+ def [](key)
55
+ key = case key
56
+ when Symbol
57
+ key
58
+ else
59
+ String.try_convert(key).to_sym
60
+ end
61
+ if @column_cache.key?(key)
62
+ @column_cache[key]
63
+ else
64
+ @column_cache[key] = @adapter[nil, key]
65
+ end
66
+ end
9
67
 
10
68
  def to_a(x=nil, y=nil, z=nil)
11
69
  case
@@ -23,6 +81,8 @@ module Charty
23
81
  else
24
82
  raise ArgumentError, "column_names is required to convert to_a from ActiveRecord::Relation"
25
83
  end
84
+ when table.kind_of?(Array)
85
+ table
26
86
  else
27
87
  raise ArgumentError, "unsupported object: #{table.inspect}"
28
88
  end
@@ -37,5 +97,24 @@ module Charty
37
97
  i += 1
38
98
  end
39
99
  end
100
+
101
+ def drop_na
102
+ # TODO: Must implement this method in each adapter
103
+ missing_index = index.select do |i|
104
+ column_names.any? do |key|
105
+ missing_value?(self[key][i])
106
+ end
107
+ end
108
+ if missing_index.empty?
109
+ self
110
+ else
111
+ select_index = index.to_a - missing_index
112
+ new_data = column_names.map { |key|
113
+ vals = select_index.map {|i| self[key][i] }
114
+ [key, vals]
115
+ }.to_h
116
+ Charty::Table.new(new_data, index: select_index)
117
+ end
118
+ end
40
119
  end
41
120
  end
@@ -0,0 +1,25 @@
1
+ module Charty
2
+ module TableAdapters
3
+ @adapters = {}
4
+
5
+ def self.register(name, adapter_class)
6
+ @adapters[name] = adapter_class
7
+ end
8
+
9
+ def self.find_adapter_class(data)
10
+ @adapters.each_value do |adapter_class|
11
+ return adapter_class if adapter_class.supported?(data)
12
+ end
13
+ raise ArgumentError, "Unsupported data class: #{data.class}"
14
+ end
15
+ end
16
+ end
17
+
18
+ require_relative 'table_adapters/base_adapter'
19
+ require_relative 'table_adapters/hash_adapter'
20
+ require_relative 'table_adapters/narray_adapter'
21
+ require_relative 'table_adapters/datasets_adapter'
22
+ require_relative 'table_adapters/daru_adapter'
23
+ require_relative 'table_adapters/active_record_adapter'
24
+ require_relative 'table_adapters/nmatrix_adapter'
25
+ require_relative 'table_adapters/pandas_adapter'
@@ -0,0 +1,63 @@
1
+ module Charty
2
+ module TableAdapters
3
+ class ActiveRecordAdapter < BaseAdapter
4
+ TableAdapters.register(:active_record, self)
5
+
6
+ def self.supported?(data)
7
+ defined?(ActiveRecord::Relation) && data.is_a?(ActiveRecord::Relation)
8
+ end
9
+
10
+ def initialize(data)
11
+ @data = check_type(ActiveRecord::Relation, data, :data)
12
+ @column_names = @data.column_names.freeze
13
+ self.columns = Index.new(@column_names)
14
+ self.index = RangeIndex.new(0 ... length)
15
+ end
16
+
17
+ attr_reader :data, :column_names
18
+
19
+ def_delegators :data, :size
20
+
21
+ alias length size
22
+
23
+ def column_length
24
+ column_names.length
25
+ end
26
+
27
+ def [](row, column)
28
+ fetch_records unless @columns_cache
29
+ if row
30
+ @columns_cache[resolve_column_index(column)][row]
31
+ else
32
+ column_data = @columns_cache[resolve_column_index(column)]
33
+ Vector.new(column_data, index: index, name: column)
34
+ end
35
+ end
36
+
37
+ private def resolve_column_index(column)
38
+ case column
39
+ when String, Symbol
40
+ index = column_names.index(column.to_s)
41
+ unless index
42
+ raise IndexError, "invalid column name: #{column.inspect}"
43
+ end
44
+ index
45
+ when Integer
46
+ column
47
+ else
48
+ message = "column must be String or Integer: #{column.inspect}"
49
+ raise ArgumentError, message
50
+ end
51
+ end
52
+
53
+ private def fetch_records
54
+ @columns_cache = @data.pluck(*column_names).transpose
55
+ end
56
+
57
+ private def check_type(type, data, name)
58
+ return data if data.is_a?(type)
59
+ raise TypeError, "#{name} must be a #{type}"
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,69 @@
1
+ require "forwardable"
2
+
3
+ module Charty
4
+ module TableAdapters
5
+ class BaseAdapter
6
+ extend Forwardable
7
+ include Enumerable
8
+
9
+ attr_reader :columns
10
+
11
+ def columns=(values)
12
+ @columns = check_and_convert_index(values, :columns, column_length)
13
+ end
14
+
15
+ def column_names
16
+ columns.to_a
17
+ end
18
+
19
+ attr_reader :index
20
+
21
+ def index=(values)
22
+ @index = check_and_convert_index(values, :index, length)
23
+ end
24
+
25
+ def ==(other)
26
+ case other
27
+ when BaseAdapter
28
+ return false if columns != other.columns
29
+ return false if index != other.index
30
+ compare_data_equality(other)
31
+ else
32
+ false
33
+ end
34
+ end
35
+
36
+ def compare_data_equality(other)
37
+ columns.each do |name|
38
+ if self[nil, name] != other[nil, name]
39
+ return false
40
+ end
41
+ end
42
+ true
43
+ end
44
+
45
+ private def check_and_convert_index(values, name, expected_length)
46
+ case values
47
+ when Index, Range
48
+ else
49
+ unless (ary = Array.try_convert(values))
50
+ raise ArgumentError, "invalid object for %s: %p" % [name, values]
51
+ end
52
+ values = ary
53
+ end
54
+ if expected_length != values.size
55
+ raise ArgumentError,
56
+ "invalid length for %s (%d for %d)" % [name, values.size, expected_length]
57
+ end
58
+ case values
59
+ when Index
60
+ values
61
+ when Range
62
+ RangeIndex.new(values)
63
+ when Array
64
+ Index.new(values)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end