charty 0.2.4 → 0.2.9
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.
- checksums.yaml +4 -4
- data/README.md +64 -15
- data/charty.gemspec +10 -3
- data/lib/charty.rb +5 -2
- data/lib/charty/backends/bokeh.rb +2 -2
- data/lib/charty/backends/google_charts.rb +1 -1
- data/lib/charty/backends/gruff.rb +1 -1
- data/lib/charty/backends/plotly.rb +434 -32
- data/lib/charty/backends/plotly_helpers/html_renderer.rb +203 -0
- data/lib/charty/backends/plotly_helpers/notebook_renderer.rb +87 -0
- data/lib/charty/backends/plotly_helpers/plotly_renderer.rb +121 -0
- data/lib/charty/backends/pyplot.rb +187 -48
- data/lib/charty/backends/rubyplot.rb +1 -1
- data/lib/charty/cache_dir.rb +27 -0
- data/lib/charty/dash_pattern_generator.rb +57 -0
- data/lib/charty/index.rb +1 -1
- data/lib/charty/iruby_helper.rb +18 -0
- data/lib/charty/plot_methods.rb +115 -3
- data/lib/charty/plotter.rb +2 -2
- data/lib/charty/plotters.rb +4 -0
- data/lib/charty/plotters/abstract_plotter.rb +106 -11
- data/lib/charty/plotters/bar_plotter.rb +1 -16
- data/lib/charty/plotters/box_plotter.rb +1 -16
- data/lib/charty/plotters/distribution_plotter.rb +150 -0
- data/lib/charty/plotters/histogram_plotter.rb +242 -0
- data/lib/charty/plotters/line_plotter.rb +300 -0
- data/lib/charty/plotters/relational_plotter.rb +213 -96
- data/lib/charty/plotters/scatter_plotter.rb +8 -43
- data/lib/charty/statistics.rb +11 -2
- data/lib/charty/table.rb +124 -14
- data/lib/charty/table_adapters/base_adapter.rb +97 -0
- data/lib/charty/table_adapters/daru_adapter.rb +2 -0
- data/lib/charty/table_adapters/datasets_adapter.rb +7 -0
- data/lib/charty/table_adapters/hash_adapter.rb +19 -3
- data/lib/charty/table_adapters/pandas_adapter.rb +82 -0
- data/lib/charty/util.rb +28 -0
- data/lib/charty/vector_adapters.rb +5 -1
- data/lib/charty/vector_adapters/array_adapter.rb +2 -10
- data/lib/charty/vector_adapters/daru_adapter.rb +3 -11
- data/lib/charty/vector_adapters/narray_adapter.rb +1 -6
- data/lib/charty/vector_adapters/numpy_adapter.rb +1 -1
- data/lib/charty/vector_adapters/pandas_adapter.rb +0 -1
- data/lib/charty/version.rb +1 -1
- metadata +104 -11
- data/lib/charty/missing_value_support.rb +0 -14
@@ -42,25 +42,10 @@ module Charty
|
|
42
42
|
@cap_size = check_number(cap_size, :cap_size, allow_nil: true)
|
43
43
|
end
|
44
44
|
|
45
|
-
def
|
46
|
-
backend = Backends.current
|
47
|
-
backend.begin_figure
|
45
|
+
private def render_plot(backend, **)
|
48
46
|
draw_bars(backend)
|
49
47
|
annotate_axes(backend)
|
50
48
|
backend.invert_yaxis if orient == :h
|
51
|
-
backend.show
|
52
|
-
end
|
53
|
-
|
54
|
-
# TODO:
|
55
|
-
# - Should infer mime type from file's extname
|
56
|
-
# - Should check backend's supported mime type before begin_figure
|
57
|
-
def save(filename, **opts)
|
58
|
-
backend = Backends.current
|
59
|
-
backend.begin_figure
|
60
|
-
draw_bars(backend)
|
61
|
-
annotate_axes(backend)
|
62
|
-
backend.invert_yaxis if orient == :h
|
63
|
-
backend.save(filename, **opts)
|
64
49
|
end
|
65
50
|
|
66
51
|
private def draw_bars(backend)
|
@@ -27,25 +27,10 @@ module Charty
|
|
27
27
|
@whisker = check_number(val, :whisker, allow_nil: true)
|
28
28
|
end
|
29
29
|
|
30
|
-
def
|
31
|
-
backend = Backends.current
|
32
|
-
backend.begin_figure
|
30
|
+
private def render_plot(backend, **)
|
33
31
|
draw_box_plot(backend)
|
34
32
|
annotate_axes(backend)
|
35
33
|
backend.invert_yaxis if orient == :h
|
36
|
-
backend.show
|
37
|
-
end
|
38
|
-
|
39
|
-
# TODO:
|
40
|
-
# - Should infer mime type from file's extname
|
41
|
-
# - Should check backend's supported mime type before begin_figure
|
42
|
-
def save(filename, **opts)
|
43
|
-
backend = Backends.current
|
44
|
-
backend.begin_figure
|
45
|
-
draw_box_plot(backend)
|
46
|
-
annotate_axes(backend)
|
47
|
-
backend.invert_yaxis if orient == :h
|
48
|
-
backend.save(filename, **opts)
|
49
34
|
end
|
50
35
|
|
51
36
|
private def draw_box_plot(backend)
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module Charty
|
2
|
+
module Plotters
|
3
|
+
class DistributionPlotter < AbstractPlotter
|
4
|
+
def flat_structure
|
5
|
+
{
|
6
|
+
x: :values
|
7
|
+
}
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(data:, variables:, **options, &block)
|
11
|
+
x, y, color = variables.values_at(:x, :y, :color)
|
12
|
+
super(x, y, color, data: data, **options, &block)
|
13
|
+
|
14
|
+
setup_variables
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :weights
|
18
|
+
|
19
|
+
def weights=(val)
|
20
|
+
@weights = check_dimension(val, :weights)
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :variables
|
24
|
+
|
25
|
+
attr_reader :color_norm
|
26
|
+
|
27
|
+
def color_norm=(val)
|
28
|
+
unless val.nil?
|
29
|
+
raise NotImplementedError,
|
30
|
+
"Specifying color_norm is not supported yet"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :legend
|
35
|
+
|
36
|
+
def legend=(val)
|
37
|
+
@legend = check_legend(val)
|
38
|
+
end
|
39
|
+
|
40
|
+
private def check_legend(val)
|
41
|
+
check_boolean(val, :legend)
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :input_format, :plot_data, :variables, :var_types
|
45
|
+
|
46
|
+
# This should be the same as one in RelationalPlotter
|
47
|
+
# TODO: move this to AbstractPlotter and refactor with CategoricalPlotter
|
48
|
+
private def setup_variables
|
49
|
+
if x.nil? && y.nil?
|
50
|
+
@input_format = :wide
|
51
|
+
setup_variables_with_wide_form_dataset
|
52
|
+
else
|
53
|
+
@input_format = :long
|
54
|
+
setup_variables_with_long_form_dataset
|
55
|
+
end
|
56
|
+
|
57
|
+
@var_types = @plot_data.columns.map { |k|
|
58
|
+
[k, variable_type(@plot_data[k], :categorical)]
|
59
|
+
}.to_h
|
60
|
+
end
|
61
|
+
|
62
|
+
private def setup_variables_with_wide_form_dataset
|
63
|
+
unless color.nil?
|
64
|
+
raise ArgumentError,
|
65
|
+
"Unable to assign the following variables in wide-form data: color"
|
66
|
+
end
|
67
|
+
|
68
|
+
if data.nil? || data.empty?
|
69
|
+
@plot_data = Charty::Table.new({})
|
70
|
+
@variables = {}
|
71
|
+
return
|
72
|
+
end
|
73
|
+
|
74
|
+
# TODO: detect flat data
|
75
|
+
flat = data.is_a?(Charty::Vector)
|
76
|
+
if flat
|
77
|
+
@plot_data = {}
|
78
|
+
@variables = {}
|
79
|
+
|
80
|
+
[:x, :y].each do |var|
|
81
|
+
case self.flat_structure[var]
|
82
|
+
when :index
|
83
|
+
@plot_data[var] = data.index.to_a
|
84
|
+
@variables[var] = data.index.name
|
85
|
+
when :values
|
86
|
+
@plot_data[var] = data.to_a
|
87
|
+
@variables[var] = data.name
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
@plot_data = Charty::Table.new(@plot_data)
|
92
|
+
else
|
93
|
+
raise NotImplementedError,
|
94
|
+
"wide-form input is not supported"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private def setup_variables_with_long_form_dataset
|
99
|
+
if data.nil? || data.empty?
|
100
|
+
@plot_data = Charty::Table.new({})
|
101
|
+
@variables = {}
|
102
|
+
return
|
103
|
+
end
|
104
|
+
|
105
|
+
plot_data = {}
|
106
|
+
variables = {}
|
107
|
+
|
108
|
+
{
|
109
|
+
x: self.x,
|
110
|
+
y: self.y,
|
111
|
+
color: self.color,
|
112
|
+
weights: self.weights
|
113
|
+
}.each do |key, val|
|
114
|
+
next if val.nil?
|
115
|
+
|
116
|
+
if data.column?(val)
|
117
|
+
plot_data[key] = data[val]
|
118
|
+
variables[key] = val
|
119
|
+
else
|
120
|
+
case val
|
121
|
+
when Charty::Vector
|
122
|
+
plot_data[key] = val
|
123
|
+
variables[key] = val.name
|
124
|
+
else
|
125
|
+
raise ArgumentError,
|
126
|
+
"Could not interpret value %p for parameter %p" % [val, key]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
@plot_data = Charty::Table.new(plot_data)
|
132
|
+
@variables = variables.select do |var, name|
|
133
|
+
@plot_data[var].notnull.any?
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
private def map_color(palette: nil, order: nil, norm: nil)
|
138
|
+
@color_mapper = ColorMapper.new(self, palette, order, norm)
|
139
|
+
end
|
140
|
+
|
141
|
+
private def map_size(sizes: nil, order: nil, norm: nil)
|
142
|
+
@size_mapper = SizeMapper.new(self, sizes, order, norm)
|
143
|
+
end
|
144
|
+
|
145
|
+
private def map_style(markers: nil, dashes: nil, order: nil)
|
146
|
+
@style_mapper = StyleMapper.new(self, markers, dashes, order)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,242 @@
|
|
1
|
+
module Charty
|
2
|
+
module Plotters
|
3
|
+
class HistogramPlotter < DistributionPlotter
|
4
|
+
def univariate?
|
5
|
+
self.variables.key?(:x) != self.variables.key?(:y)
|
6
|
+
end
|
7
|
+
|
8
|
+
def univariate_variable
|
9
|
+
unless univariate?
|
10
|
+
raise TypeError, "This is not a univariate plot"
|
11
|
+
end
|
12
|
+
([:x, :y] & self.variables.keys)[0]
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :stat
|
16
|
+
|
17
|
+
def stat=(val)
|
18
|
+
@stat = check_stat(val)
|
19
|
+
end
|
20
|
+
|
21
|
+
private def check_stat(val)
|
22
|
+
case val
|
23
|
+
when :count, "count"
|
24
|
+
val.to_sym
|
25
|
+
when :frequency, "frequency",
|
26
|
+
:density, "density",
|
27
|
+
:probability, "probability"
|
28
|
+
raise ArgumentError,
|
29
|
+
"%p for `stat` is not supported yet" % val,
|
30
|
+
caller
|
31
|
+
else
|
32
|
+
raise ArgumentError,
|
33
|
+
"Invalid value for `stat` (%p)" % val,
|
34
|
+
caller
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :bins
|
39
|
+
|
40
|
+
def bins=(val)
|
41
|
+
@bins = check_bins(val)
|
42
|
+
end
|
43
|
+
|
44
|
+
private def check_bins(val)
|
45
|
+
case val
|
46
|
+
when :auto, "auto"
|
47
|
+
val.to_sym
|
48
|
+
when Integer
|
49
|
+
val
|
50
|
+
else
|
51
|
+
raise ArgumentError,
|
52
|
+
"Invalid value for `bins` (%p)" % val,
|
53
|
+
caller
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# TODO: bin_width
|
58
|
+
|
59
|
+
attr_reader :bin_range
|
60
|
+
|
61
|
+
def bin_range=(val)
|
62
|
+
@bin_range = check_bin_range(val)
|
63
|
+
end
|
64
|
+
|
65
|
+
private def check_bin_range(val)
|
66
|
+
case val
|
67
|
+
when nil, Range
|
68
|
+
return val
|
69
|
+
when Array
|
70
|
+
if val.length == 2
|
71
|
+
val.each_with_index do |v, i|
|
72
|
+
check_number(v, "bin_range[#{i}]")
|
73
|
+
end
|
74
|
+
return val
|
75
|
+
else
|
76
|
+
amount = val.length < 2 ? "few" : "many"
|
77
|
+
raise ArgumentError,
|
78
|
+
"Too #{amount} items in `bin_range` array (%p for 2)" % val.length
|
79
|
+
end
|
80
|
+
else
|
81
|
+
raise ArgumentError,
|
82
|
+
"Invalid value for `bin_range` " +
|
83
|
+
"(%p for a range or a pair of numbers)" % val
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# TODO: discrete
|
88
|
+
# TODO: cumulative
|
89
|
+
|
90
|
+
attr_reader :common_bins
|
91
|
+
|
92
|
+
def common_bins=(val)
|
93
|
+
@common_bins = check_boolean(val, :common_bins)
|
94
|
+
end
|
95
|
+
|
96
|
+
# TODO: common_norm
|
97
|
+
|
98
|
+
attr_reader :multiple
|
99
|
+
|
100
|
+
def multiple=(val)
|
101
|
+
@multiple = check_multiple(val)
|
102
|
+
end
|
103
|
+
|
104
|
+
private def check_multiple(val)
|
105
|
+
case val
|
106
|
+
when :layer, "layer"
|
107
|
+
val.to_sym
|
108
|
+
when :dodge, "dodge",
|
109
|
+
:stack, "stack",
|
110
|
+
:fill, "fill"
|
111
|
+
val = val.to_sym
|
112
|
+
raise NotImplementedError,
|
113
|
+
"%p for `multiple` is not supported yet" % val,
|
114
|
+
caller
|
115
|
+
else
|
116
|
+
raise ArgumentError,
|
117
|
+
"Invalid value for `multiple` (%p)" % val,
|
118
|
+
caller
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# TODO: element
|
123
|
+
# TODO: fill
|
124
|
+
# TODO: shrink
|
125
|
+
|
126
|
+
attr_reader :kde
|
127
|
+
|
128
|
+
def kde=(val)
|
129
|
+
raise NotImplementedError, "kde is not supported yet"
|
130
|
+
end
|
131
|
+
|
132
|
+
attr_reader :kde_params
|
133
|
+
|
134
|
+
def kde_params=(val)
|
135
|
+
raise NotImplementedError, "kde_params is not supported yet"
|
136
|
+
end
|
137
|
+
|
138
|
+
# TODO: thresh
|
139
|
+
# TODO: pthresh
|
140
|
+
# TODO: pmax
|
141
|
+
# TODO: cbar
|
142
|
+
# TODO: cbar_params
|
143
|
+
# TODO: x_log_scale
|
144
|
+
# TODO: y_log_scale
|
145
|
+
|
146
|
+
private def render_plot(backend, **)
|
147
|
+
draw_univariate_histogram(backend)
|
148
|
+
annotate_axes(backend)
|
149
|
+
end
|
150
|
+
|
151
|
+
private def draw_univariate_histogram(backend)
|
152
|
+
map_color(palette: palette, order: color_order, norm: color_norm)
|
153
|
+
|
154
|
+
key_color = self.key_color
|
155
|
+
if key_color.nil? && !self.variables.key?(:color)
|
156
|
+
palette = case self.palette
|
157
|
+
when Palette
|
158
|
+
self.palette
|
159
|
+
when nil
|
160
|
+
Palette.default
|
161
|
+
else
|
162
|
+
Palette[self.palette]
|
163
|
+
end
|
164
|
+
key_color = palette[0]
|
165
|
+
end
|
166
|
+
|
167
|
+
# TODO: calculate histogram here and use bar plot to visualize
|
168
|
+
data_variable = self.univariate_variable
|
169
|
+
|
170
|
+
if common_bins
|
171
|
+
all_data = processed_data.drop_na
|
172
|
+
all_observations = all_data[data_variable].to_a
|
173
|
+
|
174
|
+
bins = self.bins
|
175
|
+
bins = 10 if self.variables.key?(:color) && bins == :auto
|
176
|
+
|
177
|
+
case bins
|
178
|
+
when Integer
|
179
|
+
case bin_range
|
180
|
+
when Range
|
181
|
+
start = bin_range.begin
|
182
|
+
stop = bin_range.end
|
183
|
+
when Array
|
184
|
+
start, stop = bin_range.minmax
|
185
|
+
end
|
186
|
+
data_range = all_observations.minmax
|
187
|
+
start ||= data_range[0]
|
188
|
+
stop ||= data_range[1]
|
189
|
+
if start == stop
|
190
|
+
start -= 0.5
|
191
|
+
stop += 0.5
|
192
|
+
end
|
193
|
+
common_bin_edges = Linspace.new(start .. stop, bins + 1).map(&:to_f)
|
194
|
+
else
|
195
|
+
params = {}
|
196
|
+
params[:weights] = all_data[:weights].to_a if all_data.column?(:weights)
|
197
|
+
h = Statistics.histogram(all_observations, bins, **params)
|
198
|
+
common_bin_edges = h.edges
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
if self.variables.key?(:color)
|
203
|
+
alpha = 0.5
|
204
|
+
else
|
205
|
+
alpha = 0.75
|
206
|
+
end
|
207
|
+
|
208
|
+
each_subset([:color], processed: true) do |sub_vars, sub_data|
|
209
|
+
observations = sub_data[data_variable].drop_na.to_a
|
210
|
+
params = {}
|
211
|
+
params[:weights] = sub_data[:weights].to_a if sub_data.column?(:weights)
|
212
|
+
params[:edges] = common_bin_edges if common_bin_edges
|
213
|
+
hist = Statistics.histogram(observations, bins, **params)
|
214
|
+
|
215
|
+
name = sub_vars[:color]
|
216
|
+
backend.univariate_histogram(hist, name, data_variable, stat,
|
217
|
+
alpha, name, key_color, @color_mapper,
|
218
|
+
multiple, :bars, true, 1r)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
private def annotate_axes(backend)
|
223
|
+
if univariate?
|
224
|
+
xlabel = self.variables[:x]
|
225
|
+
ylabel = self.variables[:y]
|
226
|
+
case self.univariate_variable
|
227
|
+
when :x
|
228
|
+
ylabel = self.stat.to_s.capitalize
|
229
|
+
else
|
230
|
+
xlabel = self.stat.to_s.capitalize
|
231
|
+
end
|
232
|
+
backend.set_ylabel(ylabel) if ylabel
|
233
|
+
backend.set_xlabel(xlabel) if xlabel
|
234
|
+
|
235
|
+
if self.variables.key?(:color)
|
236
|
+
backend.legend(loc: :best, title: self.variables[:color])
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,300 @@
|
|
1
|
+
module Charty
|
2
|
+
module Plotters
|
3
|
+
class EstimateAggregator
|
4
|
+
def initialize(estimator, error_bar, n_boot, random)
|
5
|
+
@estimator = estimator
|
6
|
+
@method, @level = error_bar
|
7
|
+
@n_boot = n_boot
|
8
|
+
@random = random
|
9
|
+
end
|
10
|
+
|
11
|
+
# Perform aggregation
|
12
|
+
#
|
13
|
+
# @param data [Hash<Any, Charty::Table>]
|
14
|
+
# @param var_name [Symbol, String] A column name to be aggregated
|
15
|
+
def aggregate(data, var_name)
|
16
|
+
values = data[var_name]
|
17
|
+
estimation = case @estimator
|
18
|
+
when :count
|
19
|
+
values.length
|
20
|
+
when :mean
|
21
|
+
values.mean
|
22
|
+
end
|
23
|
+
|
24
|
+
n = values.length
|
25
|
+
case
|
26
|
+
# No error bars
|
27
|
+
when @method.nil?
|
28
|
+
err_min = err_max = Float::NAN
|
29
|
+
when n <= 1
|
30
|
+
err_min = err_max = Float::NAN
|
31
|
+
|
32
|
+
# User-defined method
|
33
|
+
when @method.respond_to?(:call)
|
34
|
+
err_min, err_max = @method.call(values)
|
35
|
+
|
36
|
+
# Parametric
|
37
|
+
when @method == :sd
|
38
|
+
err_radius = values.stdev * @level
|
39
|
+
err_min = estimation - err_radius
|
40
|
+
err_max = estimation + err_radius
|
41
|
+
when @method == :se
|
42
|
+
err_radius = values.stdev / Math.sqrt(n)
|
43
|
+
err_min = estimation - err_radius
|
44
|
+
err_max = estimation + err_radius
|
45
|
+
|
46
|
+
# Nonparametric
|
47
|
+
when @method == :pi
|
48
|
+
err_min, err_max = percentile_interval(values, @level)
|
49
|
+
when @method == :ci
|
50
|
+
# TODO: Support units
|
51
|
+
err_min, err_max =
|
52
|
+
Statistics.bootstrap_ci(values, @level, units: nil, func: @estimator,
|
53
|
+
n_boot: @n_boot, random: @random)
|
54
|
+
end
|
55
|
+
|
56
|
+
{
|
57
|
+
var_name => estimation,
|
58
|
+
"#{var_name}_min" => err_min,
|
59
|
+
"#{var_name}_max" => err_max
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def percentile_interval(values, width)
|
64
|
+
q = [50 - width / 2, 50 + width / 2]
|
65
|
+
Statistics.percentile(values, q)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class LinePlotter < RelationalPlotter
|
70
|
+
def initialize(data: nil, variables: {}, **options, &block)
|
71
|
+
x, y, color, style, size = variables.values_at(:x, :y, :color, :style, :size)
|
72
|
+
super(x, y, color, style, size, data: data, **options, &block)
|
73
|
+
|
74
|
+
@comp_data = nil
|
75
|
+
end
|
76
|
+
|
77
|
+
attr_reader :estimator
|
78
|
+
|
79
|
+
def estimator=(estimator)
|
80
|
+
@estimator = check_estimator(estimator)
|
81
|
+
end
|
82
|
+
|
83
|
+
private def check_estimator(value)
|
84
|
+
case value
|
85
|
+
when nil, false
|
86
|
+
nil
|
87
|
+
when :count, "count"
|
88
|
+
:count
|
89
|
+
when :mean, "mean"
|
90
|
+
:mean
|
91
|
+
when :median
|
92
|
+
raise NotImplementedError,
|
93
|
+
"median estimator has not been supported yet"
|
94
|
+
when Proc
|
95
|
+
raise NotImplementedError,
|
96
|
+
"a callable estimator has not been supported yet"
|
97
|
+
else
|
98
|
+
raise ArgumentError,
|
99
|
+
"invalid value for estimator (%p for :mean)" % value
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
attr_reader :n_boot
|
104
|
+
|
105
|
+
def n_boot=(n_boot)
|
106
|
+
@n_boot = check_n_boot(n_boot)
|
107
|
+
end
|
108
|
+
|
109
|
+
private def check_n_boot(value)
|
110
|
+
case value
|
111
|
+
when Integer
|
112
|
+
if value <= 0
|
113
|
+
raise ArgumentError,
|
114
|
+
"n_boot must be larger than zero, but %p is given" % value
|
115
|
+
end
|
116
|
+
value
|
117
|
+
else
|
118
|
+
raise ArgumentError,
|
119
|
+
"invalid value for n_boot (%p for an integer > 0)" % value
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
include RandomSupport
|
124
|
+
|
125
|
+
attr_reader :sort, :err_style, :err_kws, :error_bar, :x_scale, :y_scale
|
126
|
+
|
127
|
+
def sort=(val)
|
128
|
+
@sort = check_boolean(val, :sort)
|
129
|
+
end
|
130
|
+
|
131
|
+
def err_style=(val)
|
132
|
+
@err_style = check_err_style(val)
|
133
|
+
end
|
134
|
+
|
135
|
+
private def check_err_style(val)
|
136
|
+
case val
|
137
|
+
when :bars, "bars", :band, "band"
|
138
|
+
val.to_sym
|
139
|
+
else
|
140
|
+
raise ArgumentError,
|
141
|
+
"Invalid value for err_style (%p for :band or :bars)" % val
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# parameters to draw error bars/bands
|
146
|
+
def err_params=(val)
|
147
|
+
unless val.nil?
|
148
|
+
raise NotImplementedError,
|
149
|
+
"Specifying `err_params` is not supported"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# The method and level to calculate error bars/bands
|
154
|
+
def error_bar=(val)
|
155
|
+
@error_bar = check_error_bar(val)
|
156
|
+
end
|
157
|
+
|
158
|
+
DEFAULT_ERROR_BAR_LEVELS = {
|
159
|
+
ci: 95,
|
160
|
+
pi: 95,
|
161
|
+
se: 1,
|
162
|
+
sd: 1
|
163
|
+
}.freeze
|
164
|
+
|
165
|
+
VALID_ERROR_BAR_METHODS = DEFAULT_ERROR_BAR_LEVELS.keys
|
166
|
+
VALID_ERROR_BAR_METHODS.concat(VALID_ERROR_BAR_METHODS.map(&:to_s))
|
167
|
+
VALID_ERROR_BAR_METHODS.freeze
|
168
|
+
|
169
|
+
private def check_error_bar(val)
|
170
|
+
case val
|
171
|
+
when nil
|
172
|
+
return [nil, nil]
|
173
|
+
when ->(x) { x.respond_to?(:call) }
|
174
|
+
return [val, nil]
|
175
|
+
when *VALID_ERROR_BAR_METHODS
|
176
|
+
method = val.to_sym
|
177
|
+
level = nil
|
178
|
+
when Array
|
179
|
+
if val.length != 2
|
180
|
+
raise ArgumentError,
|
181
|
+
"The `error_bar` array has the wrong number of items " +
|
182
|
+
"(%d for 2)" % val.length
|
183
|
+
end
|
184
|
+
method, level = *val
|
185
|
+
else
|
186
|
+
raise ArgumentError,
|
187
|
+
"Unable to recognize the value for `error_bar`: %p" % val
|
188
|
+
end
|
189
|
+
|
190
|
+
case method
|
191
|
+
when *VALID_ERROR_BAR_METHODS
|
192
|
+
method = method.to_sym
|
193
|
+
else
|
194
|
+
error_message = "The value for method in `error_bar` array must be in %p, but %p was passed" % [
|
195
|
+
DEFAULT_ERROR_BAR_LEVELS.keys,
|
196
|
+
method
|
197
|
+
]
|
198
|
+
raise ArgumentError, error_message
|
199
|
+
end
|
200
|
+
|
201
|
+
case level
|
202
|
+
when Numeric
|
203
|
+
# nothing to do
|
204
|
+
when nil
|
205
|
+
level = DEFAULT_ERROR_BAR_LEVELS[method]
|
206
|
+
else
|
207
|
+
raise ArgumentError,
|
208
|
+
"The value of level in `error_bar` array must be a number "
|
209
|
+
end
|
210
|
+
|
211
|
+
[method, level]
|
212
|
+
end
|
213
|
+
|
214
|
+
def x_scale=(val)
|
215
|
+
@x_scale = check_axis_scale(val, :x)
|
216
|
+
end
|
217
|
+
|
218
|
+
def y_scale=(val)
|
219
|
+
@y_scale = check_axis_scale(val, :y)
|
220
|
+
end
|
221
|
+
|
222
|
+
private def check_axis_scale(val, axis)
|
223
|
+
case val
|
224
|
+
when :linear, "linear", :log10, "log10"
|
225
|
+
val.to_sym
|
226
|
+
else
|
227
|
+
raise ArgumentError,
|
228
|
+
"The value of `#{axis}_scale` is worng: %p" % val,
|
229
|
+
caller
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
private def render_plot(backend, **)
|
234
|
+
draw_lines(backend)
|
235
|
+
annotate_axes(backend)
|
236
|
+
end
|
237
|
+
|
238
|
+
private def draw_lines(backend)
|
239
|
+
map_color(palette: palette, order: color_order, norm: color_norm)
|
240
|
+
map_size(sizes: sizes, order: size_order, norm: size_norm)
|
241
|
+
map_style(markers: markers, dashes: dashes, order: style_order)
|
242
|
+
|
243
|
+
aggregator = EstimateAggregator.new(estimator, error_bar, n_boot, random)
|
244
|
+
|
245
|
+
agg_var = :y
|
246
|
+
grouper = :x
|
247
|
+
grouping_vars = [:color, :size, :style]
|
248
|
+
|
249
|
+
each_subset(grouping_vars, processed: true) do |sub_vars, sub_data|
|
250
|
+
if self.sort
|
251
|
+
sort_cols = [:units, :x, :y] & self.variables.keys
|
252
|
+
sub_data = sub_data.sort_values(sort_cols)
|
253
|
+
end
|
254
|
+
|
255
|
+
unless estimator.nil?
|
256
|
+
if self.variables.include?(:units)
|
257
|
+
raise "`estimator` is must be nil when specifying `units`"
|
258
|
+
end
|
259
|
+
|
260
|
+
grouped = sub_data.group_by(grouper, sort: self.sort)
|
261
|
+
sub_data = grouped.apply(agg_var, &aggregator.method(:aggregate)).reset_index
|
262
|
+
end
|
263
|
+
|
264
|
+
# TODO: perform inverse conversion of axis scaling before plot
|
265
|
+
|
266
|
+
unit_grouping = if self.variables.include?(:units)
|
267
|
+
sub_data.group_by(:units).each_group
|
268
|
+
else
|
269
|
+
{ nil => sub_data }
|
270
|
+
end
|
271
|
+
unit_grouping.each do |_unit_value, unit_data|
|
272
|
+
ci_params = unless self.estimator.nil? || self.error_bar.nil?
|
273
|
+
{
|
274
|
+
style: self.err_style,
|
275
|
+
y_min: sub_data[:y_min],
|
276
|
+
y_max: sub_data[:y_max]
|
277
|
+
}
|
278
|
+
end
|
279
|
+
backend.line(unit_data[:x], unit_data[:y], self.variables,
|
280
|
+
color: sub_vars[:color], color_mapper: @color_mapper,
|
281
|
+
size: sub_vars[:size], size_mapper: @size_mapper,
|
282
|
+
style: sub_vars[:style], style_mapper: @style_mapper,
|
283
|
+
ci_params: ci_params)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
if legend
|
288
|
+
backend.add_line_plot_legend(@variables, @color_mapper, @size_mapper, @style_mapper, legend)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
private def annotate_axes(backend)
|
293
|
+
xlabel = self.variables[:x]
|
294
|
+
ylabel = self.variables[:y]
|
295
|
+
backend.set_xlabel(xlabel) unless xlabel.nil?
|
296
|
+
backend.set_ylabel(ylabel) unless ylabel.nil?
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|