charty 0.1.5.dev → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) 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 +176 -9
  8. data/Rakefile +4 -5
  9. data/charty.gemspec +10 -1
  10. data/examples/Gemfile +1 -0
  11. data/examples/active_record.ipynb +1 -1
  12. data/examples/daru.ipynb +1 -1
  13. data/examples/iris_dataset.ipynb +1 -1
  14. data/examples/nmatrix.ipynb +1 -1
  15. data/examples/{numo-narray.ipynb → numo_narray.ipynb} +1 -1
  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_images/bar_bokeh.html +85 -0
  21. data/examples/sample_images/barh_bokeh.html +85 -0
  22. data/examples/sample_images/box_plot_bokeh.html +85 -0
  23. data/examples/sample_images/curve_bokeh.html +85 -0
  24. data/examples/sample_images/curve_with_function_bokeh.html +85 -0
  25. data/examples/sample_images/hist_gruff.png +0 -0
  26. data/examples/sample_images/scatter_bokeh.html +85 -0
  27. data/examples/sample_pyplot.ipynb +40 -38
  28. data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
  29. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  30. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  31. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  32. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  33. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  34. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  35. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  36. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  37. data/lib/charty.rb +14 -1
  38. data/lib/charty/backend_methods.rb +8 -0
  39. data/lib/charty/backends.rb +80 -0
  40. data/lib/charty/backends/bokeh.rb +32 -26
  41. data/lib/charty/backends/google_charts.rb +267 -0
  42. data/lib/charty/backends/gruff.rb +102 -83
  43. data/lib/charty/backends/plotly.rb +685 -0
  44. data/lib/charty/backends/pyplot.rb +586 -92
  45. data/lib/charty/backends/rubyplot.rb +82 -74
  46. data/lib/charty/backends/unicode_plot.rb +79 -0
  47. data/lib/charty/index.rb +213 -0
  48. data/lib/charty/linspace.rb +1 -1
  49. data/lib/charty/missing_value_support.rb +14 -0
  50. data/lib/charty/plot_methods.rb +184 -0
  51. data/lib/charty/plotter.rb +48 -40
  52. data/lib/charty/plotters.rb +11 -0
  53. data/lib/charty/plotters/abstract_plotter.rb +183 -0
  54. data/lib/charty/plotters/bar_plotter.rb +201 -0
  55. data/lib/charty/plotters/box_plotter.rb +79 -0
  56. data/lib/charty/plotters/categorical_plotter.rb +380 -0
  57. data/lib/charty/plotters/count_plotter.rb +7 -0
  58. data/lib/charty/plotters/estimation_support.rb +84 -0
  59. data/lib/charty/plotters/random_support.rb +25 -0
  60. data/lib/charty/plotters/relational_plotter.rb +518 -0
  61. data/lib/charty/plotters/scatter_plotter.rb +104 -0
  62. data/lib/charty/plotters/vector_plotter.rb +6 -0
  63. data/lib/charty/statistics.rb +114 -0
  64. data/lib/charty/table.rb +80 -3
  65. data/lib/charty/table_adapters.rb +25 -0
  66. data/lib/charty/table_adapters/active_record_adapter.rb +63 -0
  67. data/lib/charty/table_adapters/base_adapter.rb +69 -0
  68. data/lib/charty/table_adapters/daru_adapter.rb +70 -0
  69. data/lib/charty/table_adapters/datasets_adapter.rb +49 -0
  70. data/lib/charty/table_adapters/hash_adapter.rb +224 -0
  71. data/lib/charty/table_adapters/narray_adapter.rb +76 -0
  72. data/lib/charty/table_adapters/nmatrix_adapter.rb +67 -0
  73. data/lib/charty/table_adapters/pandas_adapter.rb +81 -0
  74. data/lib/charty/util.rb +20 -0
  75. data/lib/charty/vector.rb +69 -0
  76. data/lib/charty/vector_adapters.rb +183 -0
  77. data/lib/charty/vector_adapters/array_adapter.rb +109 -0
  78. data/lib/charty/vector_adapters/daru_adapter.rb +171 -0
  79. data/lib/charty/vector_adapters/narray_adapter.rb +187 -0
  80. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  81. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  82. data/lib/charty/vector_adapters/pandas_adapter.rb +200 -0
  83. data/lib/charty/version.rb +1 -1
  84. metadata +179 -10
  85. data/.travis.yml +0 -11
  86. data/lib/charty/backends/google_chart.rb +0 -167
  87. data/lib/charty/plotter_adapter.rb +0 -17
@@ -0,0 +1,104 @@
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
+ private def render_plot(backend, **)
51
+ draw_points(backend)
52
+ annotate_axes(backend)
53
+ end
54
+
55
+ private def draw_points(backend)
56
+ map_color(palette: palette, order: color_order, norm: color_norm)
57
+ map_size(sizes: sizes, order: size_order, norm: size_norm)
58
+ map_style(markers: markers, order: marker_order)
59
+
60
+ data = @plot_data.drop_na
61
+
62
+ # TODO: shold pass key_color to backend's scatter method.
63
+ # In pyplot backend, it is passed as color parameter.
64
+
65
+ x = data[:x]
66
+ y = data[:y]
67
+ color = data[:color] if @variables.key?(:color)
68
+ style = data[:style] if @variables.key?(:style)
69
+ size = data[:size] if @variables.key?(:size)
70
+
71
+ # TODO: key_color
72
+ backend.scatter(
73
+ x, y, @variables,
74
+ color: color, color_mapper: @color_mapper,
75
+ style: style, style_mapper: @style_mapper,
76
+ size: size, size_mapper: @size_mapper,
77
+ legend: legend
78
+ )
79
+ end
80
+
81
+ private def annotate_axes(backend)
82
+ xlabel = self.variables[:x]
83
+ ylabel = self.variables[:y]
84
+ backend.set_xlabel(xlabel) unless xlabel.nil?
85
+ backend.set_ylabel(ylabel) unless ylabel.nil?
86
+
87
+ if legend
88
+ add_legend_data(backend)
89
+ end
90
+ end
91
+
92
+ private def add_legend_data(backend)
93
+ # TODO: Legend Support
94
+ verbosity = legend
95
+ verbosity = :auto if verbosity == true
96
+
97
+ titles = Util.filter_map([:color, :size, :style]) do |v|
98
+ variables[v] if variables.key?(v)
99
+ end
100
+ legend_title = titles.length == 1 ? titles[0] : ""
101
+ end
102
+ end
103
+ end
104
+ 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
@@ -39,5 +97,24 @@ module Charty
39
97
  i += 1
40
98
  end
41
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
42
119
  end
43
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