carray 3.0.2 → 3.0.3

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 (218) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +304 -0
  3. data/README.md +3 -4
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +225 -0
  6. data/ext/ca_axis_group.c +177 -99
  7. data/ext/ca_binop_dispatch.c +54 -32
  8. data/ext/ca_binop_dispatch.h +1 -0
  9. data/ext/ca_categorical_iterator.c +66 -71
  10. data/ext/ca_compare.c +8 -2
  11. data/ext/ca_for_buffer.h +21 -68
  12. data/ext/ca_group_iter.c +27 -11
  13. data/ext/ca_kernel_iterator.c +663 -214
  14. data/ext/ca_kernel_iterator.h +231 -167
  15. data/ext/ca_monop_dispatch.c +4 -4
  16. data/ext/ca_monop_dispatch.h +4 -0
  17. data/ext/ca_obj_array.c +132 -22
  18. data/ext/ca_obj_bincmp.c +152 -121
  19. data/ext/ca_obj_binop.c +107 -210
  20. data/ext/ca_obj_bitarray.c +37 -24
  21. data/ext/ca_obj_bitfield.c +95 -62
  22. data/ext/ca_obj_block.c +1 -0
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +96 -25
  25. data/ext/ca_obj_face.c +406 -64
  26. data/ext/ca_obj_face.h +92 -47
  27. data/ext/ca_obj_fake.c +21 -50
  28. data/ext/ca_obj_farray.c +1 -0
  29. data/ext/ca_obj_field.c +16 -3
  30. data/ext/ca_obj_fixlen_string.c +21 -21
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +189 -107
  33. data/ext/ca_obj_moncmp.c +61 -61
  34. data/ext/ca_obj_monop.c +125 -107
  35. data/ext/ca_obj_object.c +543 -233
  36. data/ext/ca_obj_record.c +51 -16
  37. data/ext/ca_obj_reduce.c +5 -8
  38. data/ext/ca_obj_refer.c +117 -33
  39. data/ext/ca_obj_remap.c +57 -17
  40. data/ext/ca_obj_repeat.c +21 -5
  41. data/ext/ca_obj_roll.c +115 -11
  42. data/ext/ca_obj_select.c +47 -49
  43. data/ext/ca_obj_select_axis.c +58 -60
  44. data/ext/ca_obj_shift.c +22 -102
  45. data/ext/ca_obj_stack.c +223 -118
  46. data/ext/ca_obj_stride.c +195 -34
  47. data/ext/ca_obj_string.c +17 -18
  48. data/ext/ca_obj_tile.c +32 -16
  49. data/ext/ca_obj_time.c +23 -25
  50. data/ext/ca_obj_timedelta.c +21 -23
  51. data/ext/ca_obj_transpose.c +12 -3
  52. data/ext/ca_obj_triop.c +110 -117
  53. data/ext/ca_obj_window.c +149 -27
  54. data/ext/ca_op_ipower.c +24 -11
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_sort_kernels.h +70 -0
  58. data/ext/ca_sweep_engine.c +102 -79
  59. data/ext/ca_sweep_engine.h +7 -20
  60. data/ext/ca_triop_dispatch.c +4 -1
  61. data/ext/ca_triop_dispatch.h +5 -2
  62. data/ext/caframe_csv_reader.c +463 -0
  63. data/ext/carray.h +70 -27
  64. data/ext/carray_access.c +455 -211
  65. data/ext/carray_address_basis.c +177 -36
  66. data/ext/carray_attribute.c +45 -3
  67. data/ext/carray_bincount.c +32 -2
  68. data/ext/carray_broadcast.c +46 -14
  69. data/ext/carray_call_cfunc.c +0 -28
  70. data/ext/carray_cast.c +1444 -209
  71. data/ext/carray_cast_func.rb +34 -21
  72. data/ext/carray_conversion.c +197 -73
  73. data/ext/carray_copy.c +32 -11
  74. data/ext/carray_core.c +514 -122
  75. data/ext/carray_count.c +140 -87
  76. data/ext/carray_element.c +18 -27
  77. data/ext/carray_factorize.c +569 -277
  78. data/ext/carray_generate.c +76 -40
  79. data/ext/carray_histogram.c +152 -94
  80. data/ext/carray_hold.c +2 -1
  81. data/ext/carray_index_classifier.c +63 -96
  82. data/ext/carray_internal.h +385 -3
  83. data/ext/carray_lazy.c +834 -79
  84. data/ext/carray_loop.c +81 -65
  85. data/ext/carray_mask.c +682 -316
  86. data/ext/carray_math_kernel.h +1 -0
  87. data/ext/carray_median_percentile.c +221 -76
  88. data/ext/carray_memory_view.c +254 -248
  89. data/ext/carray_operator.c +1089 -359
  90. data/ext/carray_order.c +133 -69
  91. data/ext/carray_partition.c +102 -56
  92. data/ext/carray_random.c +273 -239
  93. data/ext/carray_scatter.c +308 -65
  94. data/ext/carray_segment.c +257 -0
  95. data/ext/carray_slab.c +170 -70
  96. data/ext/carray_sort.c +215 -218
  97. data/ext/carray_sort_kernel.c +8 -1
  98. data/ext/carray_struct.c +99 -21
  99. data/ext/carray_test.c +167 -64
  100. data/ext/carray_time_text.c +1182 -0
  101. data/ext/carray_utils.c +363 -98
  102. data/ext/extconf.rb +27 -2
  103. data/ext/mk_call_cfunc.rb +0 -2
  104. data/ext/mk_pow5_table.rb +35 -0
  105. data/ext/mkkernel.rb +1076 -846
  106. data/ext/ruby_carray.c +7 -1
  107. data/ext/version.h +4 -4
  108. data/ext/version.rb +4 -2
  109. data/lib/carray/attribute.rb +39 -0
  110. data/lib/carray/autoload_carray.rb +19 -0
  111. data/lib/carray/autoload_method_extension.rb +6 -0
  112. data/lib/carray/axis_group.rb +174 -26
  113. data/lib/carray/basics.rb +91 -25
  114. data/lib/carray/bincount_nd.rb +6 -0
  115. data/lib/carray/block_iterator.rb +118 -48
  116. data/lib/carray/boolean_reduce.rb +21 -29
  117. data/lib/carray/categorical.rb +94 -26
  118. data/lib/carray/categorical_iterator.rb +333 -826
  119. data/lib/carray/complex.rb +36 -29
  120. data/lib/carray/conditional.rb +94 -7
  121. data/lib/carray/const_string.rb +46 -0
  122. data/lib/carray/frame/concat.rb +157 -6
  123. data/lib/carray/frame/convert.rb +13 -7
  124. data/lib/carray/frame/csv_parser.rb +589 -82
  125. data/lib/carray/frame/describe.rb +79 -0
  126. data/lib/carray/frame/frame.rb +124 -25
  127. data/lib/carray/frame/group.rb +189 -63
  128. data/lib/carray/frame/io.rb +412 -72
  129. data/lib/carray/frame/join.rb +134 -46
  130. data/lib/carray/frame/records.rb +37 -16
  131. data/lib/carray/frame/reshape.rb +448 -0
  132. data/lib/carray/frame/sort.rb +26 -2
  133. data/lib/carray/frame/verbs.rb +715 -48
  134. data/lib/carray/frame.rb +2 -0
  135. data/lib/carray/fuse_source.rb +111 -2
  136. data/lib/carray/fusion.rb +229 -38
  137. data/lib/carray/inspect.rb +29 -11
  138. data/lib/carray/iterator.rb +14 -2
  139. data/lib/carray/lazy.rb +54 -30
  140. data/lib/carray/mask_gap_fill.rb +66 -52
  141. data/lib/carray/math.rb +32 -19
  142. data/lib/carray/meld_reduce.rb +40 -7
  143. data/lib/carray/methods/align_addr.rb +8 -4
  144. data/lib/carray/methods/bincount.rb +2 -2
  145. data/lib/carray/methods/bit_string.rb +14 -3
  146. data/lib/carray/methods/composition.rb +16 -10
  147. data/lib/carray/methods/factorize.rb +2 -2
  148. data/lib/carray/methods/gather_nd.rb +13 -2
  149. data/lib/carray/methods/insert_block.rb +9 -14
  150. data/lib/carray/methods/is_in.rb +117 -4
  151. data/lib/carray/methods/join.rb +1 -1
  152. data/lib/carray/methods/locate_addr.rb +177 -36
  153. data/lib/carray/methods/mode.rb +18 -1
  154. data/lib/carray/methods/repeat.rb +1 -20
  155. data/lib/carray/methods/resize.rb +21 -23
  156. data/lib/carray/methods/snap.rb +34 -12
  157. data/lib/carray/methods/unique.rb +11 -2
  158. data/lib/carray/record_order.rb +208 -0
  159. data/lib/carray/runtime.rb +14 -11
  160. data/lib/carray/segment_iterator.rb +990 -0
  161. data/lib/carray/serialize.rb +197 -27
  162. data/lib/carray/slab_iterator.rb +51 -27
  163. data/lib/carray/stack.rb +10 -4
  164. data/lib/carray/string_operation_extension.rb +4 -3
  165. data/lib/carray/struct.rb +142 -51
  166. data/lib/carray/struct_builder.rb +110 -14
  167. data/lib/carray/table.rb +2 -2
  168. data/lib/carray/time.rb +517 -103
  169. data/lib/carray/window_iterator.rb +127 -72
  170. data/lib/carray.rb +2 -2
  171. data/yard-stubs/ca_obj_array.rb +4 -4
  172. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  173. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  174. data/yard-stubs/ca_obj_fake.rb +1 -1
  175. data/yard-stubs/ca_obj_field.rb +1 -1
  176. data/yard-stubs/ca_obj_refer.rb +3 -3
  177. data/yard-stubs/ca_obj_roll.rb +0 -1
  178. data/yard-stubs/ca_obj_stride.rb +10 -7
  179. data/yard-stubs/ca_obj_transpose.rb +1 -1
  180. data/yard-stubs/ca_obj_window.rb +1 -2
  181. data/yard-stubs/carray_access.rb +5 -4
  182. data/yard-stubs/carray_broadcast.rb +1 -1
  183. data/yard-stubs/carray_cast.rb +78 -43
  184. data/yard-stubs/carray_conversion.rb +13 -1
  185. data/yard-stubs/carray_copy.rb +8 -3
  186. data/yard-stubs/carray_core.rb +3 -3
  187. data/yard-stubs/carray_count.rb +9 -0
  188. data/yard-stubs/carray_lazy.rb +5 -1
  189. data/yard-stubs/carray_loop.rb +4 -1
  190. data/yard-stubs/carray_mask.rb +34 -10
  191. data/yard-stubs/carray_math.rb +25 -13
  192. data/yard-stubs/carray_median_percentile.rb +10 -6
  193. data/yard-stubs/carray_order.rb +34 -13
  194. data/yard-stubs/carray_scatter.rb +10 -3
  195. data/yard-stubs/carray_segment.rb +72 -0
  196. data/yard-stubs/carray_slab.rb +2 -1
  197. data/yard-stubs/carray_sort.rb +43 -3
  198. data/yard-stubs/carray_test.rb +10 -0
  199. data/yard-stubs/carray_utils.rb +0 -26
  200. metadata +11 -19
  201. data/ext/ca_for_each_element.h +0 -239
  202. data/ext/carray_build_flags.h +0 -3
  203. data/ext/carray_config.h +0 -28
  204. data/ext/carray_kernels_bincmp.c +0 -4446
  205. data/ext/carray_kernels_binop.c +0 -11001
  206. data/ext/carray_kernels_init.c +0 -1131
  207. data/ext/carray_kernels_map.c +0 -3467
  208. data/ext/carray_kernels_moncmp.c +0 -2097
  209. data/ext/carray_kernels_monop.c +0 -18313
  210. data/ext/carray_kernels_reduce_aggregate.c +0 -26005
  211. data/ext/carray_kernels_reduce_boolean.c +0 -330
  212. data/ext/carray_kernels_reduce_cumulative.c +0 -14862
  213. data/ext/carray_kernels_reduce_extreme.c +0 -17494
  214. data/ext/carray_kernels_reduce_variance.c +0 -3910
  215. data/ext/carray_kernels_scan.c +0 -3693
  216. data/ext/carray_kernels_search.c +0 -32218
  217. data/ext/carray_kernels_sort.c +0 -10626
  218. data/ext/carray_kernels_triop.c +0 -1392
@@ -13,7 +13,67 @@ class CAFrame
13
13
  raise ArgumentError, "group_by needs at least one key" if keys.empty?
14
14
  cat = grouping_categorical(keys)
15
15
  axis = keys.size == 1 && keys.first.is_a?(String) ? keys.first : "group"
16
- GroupedFrame.new(self, cat, axis, keys.grep(String))
16
+ key = keys.size == 1 ? key_column(keys.first) : nil
17
+ GroupedFrame.new(self, cat, axis, keys.grep(String), key: key)
18
+ end
19
+
20
+ # Group rows into time bins of length +unit+ along the time column (or the
21
+ # time index) +name+, for the reductions GroupedFrame offers:
22
+ #
23
+ # df.resample("time", "1 hour").mean
24
+ # df.resample("time", "1 day", fill: true).aggregate("rain" => ["rain", :sum])
25
+ #
26
+ # The result's index is the bin labels as a CATime, in time order, and its
27
+ # row axis is named after +name+.
28
+ #
29
+ # +label: :left+ (the default) makes each bin start at its label and hold
30
+ # the times at or after it: [00:00, 01:00) is labelled 00:00. +label: :right+
31
+ # makes it end at its label and hold the times up to and including it:
32
+ # (00:00, 01:00] is labelled 01:00, the convention for a value that
33
+ # describes the hour before it. +origin:+ shifts the bins as +CATime#floor+
34
+ # does ("1 hour" with origin 00:30 gives 00:30, 01:30, ...).
35
+ #
36
+ # Without +fill:+, a bin with no rows does not appear. With +fill: true+
37
+ # every bin from the first to the last is a row, and an empty one reduces
38
+ # as an empty reduction does: UNDEF for +mean+, 0 for +count+ and +sum+.
39
+ # A row whose time is masked belongs to no bin.
40
+ #
41
+ # @param name [String] the time column, or the index's axis name.
42
+ # @param unit [String, Symbol, CATime::Resolution] the bin length.
43
+ # @param origin [nil, Time, String, CATime::Element] where bins start.
44
+ # @param label [:left, :right] which end of a bin names it and is closed.
45
+ # @param fill [Boolean] whether bins with no rows are rows of the result.
46
+ # @return [GroupedFrame]
47
+ # @raise [ArgumentError] when the column is not a CATime, or +label:+ is
48
+ # neither :left nor :right.
49
+ def resample(name, unit, origin: nil, label: :left, fill: false)
50
+ name = name.to_s
51
+ time = column_or_index(name)
52
+ unless time.is_a?(CATime)
53
+ raise ArgumentError, "resample: #{name.inspect} is not a time column (got #{time.class})"
54
+ end
55
+ bins =
56
+ case label
57
+ when :left then time.floor(unit: unit, origin: origin)
58
+ when :right then time.ceil(unit: unit, origin: origin)
59
+ else
60
+ raise ArgumentError, "resample: label: must be :left or :right (got #{label.inspect})"
61
+ end
62
+ grid = fill && bins.count_not_masked > 0 ? resample_grid(bins, unit) : bins.unique(sort: true)
63
+ cat = CACategorical.from_codes(bins.locate_addr(grid), grid.to_a)
64
+ GroupedFrame.new(self, cat, name, @columns.key?(name) ? [name] : [], index: grid)
65
+ end
66
+
67
+ # Every bin from the first occupied one to the last. A fixed-length bin
68
+ # steps from the first bin, so an origin's phase is kept; a month or a year
69
+ # cannot be stepped on a finer grid and is laid on its own unit, where
70
+ # bins start on the calendar boundary anyway.
71
+ private def resample_grid(bins, unit)
72
+ if %i[Y M].include?(CATime::Resolution.parse(unit).base)
73
+ CArray.time_range(bins.min, bins.max, unit: unit)
74
+ else
75
+ CArray.time_range(bins.min, bins.max, unit: bins.unit, step: unit)
76
+ end
17
77
  end
18
78
 
19
79
  # Number of rows currently selected — used by group per-group view-frames
@@ -24,34 +84,58 @@ class CAFrame
24
84
  if cols.size == 1
25
85
  cols.first.categorize
26
86
  else
27
- # Composite key: one object cell per row holding the tuple of key
28
- # values, categorized by content (the codes are composed from the
29
- # per-column keys).
30
- n = nrow
31
- key = CArray.object(n) { |i| cols.map { |c| c[i] } }
32
- # One undetermined component makes the whole tuple undetermined, the same
33
- # answer a single masked key cell gets. Left as a value, the UNDEF inside
34
- # the tuple would intern as an ordinary distinct key and the row would
35
- # form a group of its own.
36
- undetermined = CArray.boolean(n) { |i| key[i].any? { |v| UNDEF.equal?(v) } }
37
- key[undetermined] = UNDEF if undetermined.any
38
- key.categorize
87
+ composite_categorical(cols)
39
88
  end
40
89
  end
41
90
 
42
- private def key_column(key)
43
- case key
44
- when String
45
- self[key]
46
- when CArray
47
- unless key.shape[0] == nrow
48
- raise ArgumentError,
49
- "external group key length #{key.shape[0]} != nrow #{nrow}"
50
- end
51
- key
52
- else
53
- raise ArgumentError, "group key must be a column name or CArray (got #{key.class})"
91
+ # A composite key. Each column is categorized on its own, so a component
92
+ # compares exactly as a single key does (NaN is one value, -0.0 is 0.0, a
93
+ # masked cell matches nothing); a row's group is then the tuple of its
94
+ # component codes, and a group's label the tuple of the component labels.
95
+ # One masked component leaves the row in no group, as a masked single key.
96
+ private def composite_categorical(cols)
97
+ cats = cols.map(&:categorize)
98
+ sizes = cats.map { |c| c.labels.size }
99
+ tuple = if sizes.inject(1, :*) <= 2**62
100
+ cats.drop(1).inject(cats[0].codes.int64) { |acc, c| acc * c.labels.size + c.codes.int64 }
101
+ else
102
+ codes = cats.map(&:codes)
103
+ t = CArray.object(nrow) { |i| codes.map { |c| c[i] } }
104
+ nrow.times { |i| t[i] = UNDEF if t[i].any? { |x| UNDEF.equal?(x) } }
105
+ t
106
+ end
107
+ rows = tuple.categorize
108
+ labels = rows.labels.map do |code|
109
+ parts = code.is_a?(Array) ? code : sizes.reverse.map { |n| code, d = code.divmod(n); d }.reverse
110
+ parts.each_with_index.map { |d, j| cats[j].labels[d] }
111
+ end
112
+ CACategorical.from_codes(rows.codes, labels)
113
+ end
114
+
115
+ # The column a group key names, checked: +verb+ is the method the caller
116
+ # called, for the messages.
117
+ private def key_column(key, verb = "group_by")
118
+ col = case key
119
+ when String
120
+ @columns.fetch(key) { raise KeyError, "#{verb}: no column #{key.inspect}" }
121
+ when CArray
122
+ unless key.shape[0] == nrow
123
+ raise ArgumentError,
124
+ "#{verb}: the key has #{key.shape[0]} values for #{nrow} rows"
125
+ end
126
+ key
127
+ else
128
+ raise ArgumentError, "#{verb}: a group key is a column name or a CArray (got #{key.class})"
129
+ end
130
+ # A key gives each row one value. An N-D one would be categorized cell
131
+ # by cell, making groups of cells rather than of rows.
132
+ unless col.ndim == 1
133
+ name = key.is_a?(String) ? "#{key.inspect} " : ""
134
+ raise ArgumentError,
135
+ "#{verb}: the key #{name}has shape #{col.shape.inspect}, not one value per row; " \
136
+ "group by one of its components (e.g. df[name][nil, 0])"
54
137
  end
138
+ col
55
139
  end
56
140
  end
57
141
 
@@ -66,12 +150,21 @@ class GroupedFrame
66
150
  # +key_names+ are the frame columns the grouping was keyed on. They become
67
151
  # the result's index, so the reduction shortcuts must not also return them as
68
152
  # reduced columns; an external CArray key contributes no name.
69
- def initialize(frame, cat, axis_name, key_names = [])
153
+ #
154
+ # The result's index is +index:+ when given (one entry per group, in code
155
+ # order); otherwise, for a single +key:+, the key's value at each group's
156
+ # first row, so it keeps the key's data type and Face; otherwise an object
157
+ # array of the labels (a composite key's tuples).
158
+ #
159
+ # The grouping and the index are taken here, so a later change to the key
160
+ # does not rename the groups; the values are read when a reduction is.
161
+ def initialize(frame, cat, axis_name, key_names = [], index: nil, key: nil)
70
162
  @frame = frame
71
163
  @cat = cat
72
164
  @axis_name = axis_name
73
165
  @key_names = key_names
74
166
  @labels = cat.labels # group values, in code order
167
+ @index = index || (key && key.project(group_perm[group_bounds[0...ngroup]]))
75
168
  end
76
169
 
77
170
  # Number of groups.
@@ -92,16 +185,22 @@ class GroupedFrame
92
185
  # Declarative aggregation (memo §11.6). Spec maps an output column name to
93
186
  # +[input_column, reduction]+, where reduction is a Symbol (vectorized,
94
187
  # applied through the group iterator) or a Proc (per-group custom, called
95
- # with the group's column slice).
188
+ # with the group's column slice). A Symbol reduction takes its arguments
189
+ # after it, as in a call: +["temp", :percentile, 90.0]+, and its keywords
190
+ # last: +["temp", :mean, min_count: 6]+.
96
191
  def aggregate(spec)
97
192
  cols = {}
98
- spec.each do |out_name, (in_name, reduction)|
193
+ spec.each do |out_name, (in_name, reduction, *args)|
99
194
  out = out_name.to_s
195
+ options = args.last.is_a?(Hash) ? args.pop : {}
100
196
  cols[out] =
101
197
  case reduction
102
198
  when Symbol
103
- self[in_name].public_send(reduction)
199
+ group_reduce(@frame[in_name], reduction, *args, **options)
104
200
  when Proc
201
+ unless args.empty? && options.empty?
202
+ raise ArgumentError, "aggregate: a Proc reduction takes no arguments (#{out.inspect})"
203
+ end
105
204
  per_group_column(in_name, reduction)
106
205
  else
107
206
  raise ArgumentError,
@@ -133,27 +232,36 @@ class GroupedFrame
133
232
  CAFrame.new(cols, axis_name: @axis_name, index: label_index)
134
233
  end
135
234
 
136
- # Convenience reductions over every numeric scalar column (memo §6-4
137
- # "grp.mean"). Non-numeric / N-D columns are skipped.
235
+ # Convenience reductions over every column that defines the reduction. A
236
+ # column is in the result when its type answers the reduction -- a time
237
+ # column has a mean, a minimum and a maximum but no sum, a string column a
238
+ # minimum and a maximum only -- and left out when it does not, as are the
239
+ # key columns. An N-D column is reduced along the rows, so each group keeps
240
+ # its trailing shape: a (N, L) column gives (groups, L). Each
241
+ # takes +min_count:+ and +fill_value:+ as a core reduction does: a group
242
+ # with fewer than min_count present values is UNDEF, and fill_value fills
243
+ # the UNDEF cells.
138
244
  #
139
- # @!method sum
140
- # Returns a frame of the per-group sum of every numeric one-dimensional
141
- # column. Non-numeric and multi-dimensional columns are left out.
245
+ # @!method sum(min_count: nil, fill_value: nil)
246
+ # Returns a frame of the per-group sum of every column whose type has a
247
+ # sum, an N-D one per group along the rows. Other columns are left out.
142
248
  # @return [CAFrame] one row per group, indexed by the group labels.
143
- # @!method mean
144
- # Returns a frame of the per-group arithmetic mean of every numeric
145
- # one-dimensional column, as {#sum} does.
249
+ # @!method mean(min_count: nil, fill_value: nil)
250
+ # Returns a frame of the per-group mean of every column whose type has
251
+ # a mean, as {#sum} does.
146
252
  # @return [CAFrame] one row per group.
147
- # @!method min
148
- # Returns a frame of the per-group minimum of every numeric
149
- # one-dimensional column, as {#sum} does.
253
+ # @!method min(min_count: nil, fill_value: nil)
254
+ # Returns a frame of the per-group minimum of every column whose type has
255
+ # a minimum, as {#sum} does.
150
256
  # @return [CAFrame] one row per group.
151
- # @!method max
152
- # Returns a frame of the per-group maximum of every numeric
153
- # one-dimensional column, as {#sum} does.
257
+ # @!method max(min_count: nil, fill_value: nil)
258
+ # Returns a frame of the per-group maximum of every column whose type has
259
+ # a maximum, as {#sum} does.
154
260
  # @return [CAFrame] one row per group.
155
261
  [:sum, :mean, :min, :max].each do |red|
156
- define_method(red) { reduce_numeric(red) }
262
+ define_method(red) do |min_count: nil, fill_value: nil|
263
+ reduce_columns(red, min_count: min_count, fill_value: fill_value)
264
+ end
157
265
  end
158
266
 
159
267
  # @return [String]
@@ -161,28 +269,47 @@ class GroupedFrame
161
269
  "#<GroupedFrame ngroup=#{ngroup} by=#{@axis_name.inspect}>"
162
270
  end
163
271
 
272
+ # Each result gets its own index.
164
273
  private def label_index
274
+ return @index.copy if @index
165
275
  CArray.object(@labels.size) { |i| @labels[i] }
166
276
  end
167
277
 
168
- NON_NUMERIC = [:object, :boolean, :fixlen].freeze
169
- private_constant :NON_NUMERIC
170
-
171
- private def reduce_numeric(reduction)
278
+ private def reduce_columns(reduction, **keywords)
172
279
  cols = {}
173
- @frame.variable_names.each do |name|
174
- # A key column is the index here, not a result column. Filtering it out
175
- # by name rather than by data type is what makes a numeric key behave
176
- # like a string one: NON_NUMERIC is about which columns a reduction can
177
- # apply to, which happened to cover string keys and nothing else.
280
+ @frame.column_names.each do |name|
281
+ # A key column is the index here, not a result column.
178
282
  next if @key_names.include?(name)
179
283
  col = @frame[name]
180
- next unless col.ndim == 1 && !NON_NUMERIC.include?(col.data_type)
181
- cols[name] = col.group_by_category(@cat).public_send(reduction)
284
+ next unless answers?(col, reduction)
285
+ cols[name] = group_reduce(col, reduction, **keywords)
182
286
  end
183
287
  CAFrame.new(cols, axis_name: @axis_name, index: label_index)
184
288
  end
185
289
 
290
+ # A column's group reduction. An N-D column is reduced along the rows, so
291
+ # each group keeps the trailing shape: a profile column (N, L) gives (G, L).
292
+ private def group_reduce(col, reduction, *args, **keywords)
293
+ it = col.group_by_category(@cat)
294
+ if col.ndim > 1 && !keywords.key?(:axis)
295
+ it.public_send(reduction, *args, axis: 0, **keywords)
296
+ else
297
+ it.public_send(reduction, *args, **keywords)
298
+ end
299
+ end
300
+
301
+ # Whether the column's type defines the reduction, asked of the core on one
302
+ # cell of it so that the answer is about the type and not the data: a
303
+ # refusal there is the core saying the reduction does not apply, while an
304
+ # error in the grouped reduction itself is left to raise. A column with no
305
+ # rows is asked as it is: the refusal does not depend on the cells.
306
+ private def answers?(col, reduction)
307
+ (col.elements > 0 ? col[0..0] : col).public_send(reduction)
308
+ true
309
+ rescue CArray::DataTypeError, TypeError, ArgumentError
310
+ false
311
+ end
312
+
186
313
  private def per_group_column(in_name, proc)
187
314
  results = []
188
315
  each_group_slice(in_name) { |slice| results << proc.call(slice) }
@@ -201,19 +328,18 @@ class GroupedFrame
201
328
  end
202
329
 
203
330
  private def group_address(k)
204
- start = group_offsets[k]
205
- group_perm[start...(start + group_sizes[k])]
331
+ group_perm[group_bounds[k]...group_bounds[k + 1]]
206
332
  end
207
333
 
208
- private def group_perm
209
- @group_perm ||= @cat.sort_addr
334
+ private def group_codes
335
+ @cat.codes
210
336
  end
211
337
 
212
- private def group_offsets
213
- @group_offsets ||= @cat.reduceat_index
338
+ private def group_perm
339
+ @group_perm ||= @cat.sort_addr
214
340
  end
215
341
 
216
- private def group_sizes
217
- @group_sizes ||= @cat.category_sizes
342
+ private def group_bounds
343
+ @group_bounds ||= CArray.segment_offsets(lengths: @cat.category_sizes)
218
344
  end
219
345
  end