carray 3.0.1 → 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 (229) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +824 -0
  3. data/README.md +5 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +258 -4
  6. data/ext/ca_axis_group.c +368 -184
  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 +90 -41
  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 +960 -245
  14. data/ext/ca_kernel_iterator.h +352 -181
  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 +194 -42
  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 +5 -4
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +180 -50
  25. data/ext/ca_obj_face.c +428 -62
  26. data/ext/ca_obj_face.h +107 -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 +39 -26
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +235 -55
  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 +551 -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 +88 -75
  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 -123
  46. data/ext/ca_obj_stride.c +267 -35
  47. data/ext/ca_obj_string.c +25 -22
  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 +157 -29
  54. data/ext/ca_op_ipower.c +25 -13
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_rng_normal.h +42 -0
  58. data/ext/ca_rng_xoshiro256pp.h +105 -0
  59. data/ext/ca_sort_kernels.h +70 -0
  60. data/ext/ca_sweep_engine.c +385 -198
  61. data/ext/ca_sweep_engine.h +31 -23
  62. data/ext/ca_triop_dispatch.c +4 -1
  63. data/ext/ca_triop_dispatch.h +5 -2
  64. data/ext/caframe_csv_reader.c +463 -0
  65. data/ext/carray.h +90 -28
  66. data/ext/carray_access.c +487 -231
  67. data/ext/carray_address_basis.c +731 -0
  68. data/ext/carray_attribute.c +45 -3
  69. data/ext/carray_bincount.c +32 -2
  70. data/ext/carray_broadcast.c +49 -17
  71. data/ext/carray_call_cfunc.c +667 -511
  72. data/ext/carray_cast.c +1556 -247
  73. data/ext/carray_cast_func.rb +34 -21
  74. data/ext/carray_conversion.c +197 -73
  75. data/ext/carray_copy.c +84 -38
  76. data/ext/carray_core.c +591 -119
  77. data/ext/carray_count.c +148 -96
  78. data/ext/carray_element.c +18 -27
  79. data/ext/carray_factorize.c +604 -291
  80. data/ext/carray_generate.c +76 -40
  81. data/ext/carray_histogram.c +152 -94
  82. data/ext/carray_hold.c +2 -1
  83. data/ext/carray_index_classifier.c +63 -96
  84. data/ext/carray_internal.h +400 -1
  85. data/ext/carray_lazy.c +834 -79
  86. data/ext/carray_loop.c +88 -66
  87. data/ext/carray_mask.c +705 -324
  88. data/ext/carray_math_kernel.h +1 -0
  89. data/ext/carray_median_percentile.c +263 -63
  90. data/ext/carray_memory_view.c +254 -248
  91. data/ext/carray_operator.c +1093 -363
  92. data/ext/carray_order.c +133 -69
  93. data/ext/carray_partition.c +102 -56
  94. data/ext/carray_random.c +582 -204
  95. data/ext/carray_scatter.c +308 -65
  96. data/ext/carray_segment.c +257 -0
  97. data/ext/carray_slab.c +183 -70
  98. data/ext/carray_sort.c +235 -240
  99. data/ext/carray_sort_kernel.c +8 -1
  100. data/ext/carray_struct.c +99 -21
  101. data/ext/carray_test.c +167 -64
  102. data/ext/carray_time_text.c +1182 -0
  103. data/ext/carray_utils.c +363 -98
  104. data/ext/extconf.rb +27 -2
  105. data/ext/mk_call_cfunc.rb +103 -118
  106. data/ext/mk_pow5_table.rb +35 -0
  107. data/ext/mkkernel.rb +1349 -851
  108. data/ext/ruby_carray.c +17 -2
  109. data/ext/version.h +4 -4
  110. data/ext/version.rb +4 -2
  111. data/lib/carray/attribute.rb +39 -0
  112. data/lib/carray/autoload_carray.rb +24 -3
  113. data/lib/carray/autoload_method_extension.rb +18 -0
  114. data/lib/carray/axis_group.rb +245 -20
  115. data/lib/carray/basics.rb +95 -25
  116. data/lib/carray/bincount_nd.rb +6 -0
  117. data/lib/carray/block_iterator.rb +200 -54
  118. data/lib/carray/boolean_reduce.rb +21 -29
  119. data/lib/carray/categorical.rb +244 -59
  120. data/lib/carray/categorical_iterator.rb +391 -757
  121. data/lib/carray/complex.rb +36 -29
  122. data/lib/carray/conditional.rb +94 -7
  123. data/lib/carray/const_string.rb +177 -27
  124. data/lib/carray/construct.rb +40 -0
  125. data/lib/carray/data_type_extension.rb +3 -0
  126. data/lib/carray/data_type_limits.rb +91 -0
  127. data/lib/carray/fixlen_string.rb +1 -1
  128. data/lib/carray/frame/concat.rb +157 -6
  129. data/lib/carray/frame/convert.rb +13 -7
  130. data/lib/carray/frame/csv_parser.rb +590 -76
  131. data/lib/carray/frame/describe.rb +79 -0
  132. data/lib/carray/frame/frame.rb +205 -35
  133. data/lib/carray/frame/group.rb +199 -40
  134. data/lib/carray/frame/io.rb +452 -60
  135. data/lib/carray/frame/join.rb +134 -46
  136. data/lib/carray/frame/records.rb +54 -19
  137. data/lib/carray/frame/reshape.rb +448 -0
  138. data/lib/carray/frame/sort.rb +26 -2
  139. data/lib/carray/frame/verbs.rb +726 -56
  140. data/lib/carray/frame.rb +2 -0
  141. data/lib/carray/fuse_source.rb +111 -2
  142. data/lib/carray/fusion.rb +229 -38
  143. data/lib/carray/inspect.rb +71 -20
  144. data/lib/carray/iterator.rb +157 -2
  145. data/lib/carray/lazy.rb +54 -67
  146. data/lib/carray/mask_gap_fill.rb +66 -50
  147. data/lib/carray/math.rb +32 -19
  148. data/lib/carray/meld_reduce.rb +40 -7
  149. data/lib/carray/methods/align_addr.rb +8 -4
  150. data/lib/carray/methods/bincount.rb +2 -2
  151. data/lib/carray/methods/bit_string.rb +14 -3
  152. data/lib/carray/methods/composition.rb +16 -10
  153. data/lib/carray/methods/discovery_along.rb +74 -0
  154. data/lib/carray/methods/factorize.rb +50 -0
  155. data/lib/carray/methods/gather_nd.rb +13 -2
  156. data/lib/carray/methods/insert_block.rb +9 -14
  157. data/lib/carray/methods/is_in.rb +130 -6
  158. data/lib/carray/methods/join.rb +1 -1
  159. data/lib/carray/methods/locate_addr.rb +235 -21
  160. data/lib/carray/methods/mask_duplicates.rb +35 -1
  161. data/lib/carray/methods/mode.rb +18 -1
  162. data/lib/carray/methods/nunique.rb +22 -1
  163. data/lib/carray/methods/repeat.rb +91 -0
  164. data/lib/carray/methods/resize.rb +21 -23
  165. data/lib/carray/methods/snap.rb +34 -12
  166. data/lib/carray/methods/unique.rb +52 -3
  167. data/lib/carray/record_order.rb +208 -0
  168. data/lib/carray/rng.rb +86 -0
  169. data/lib/carray/runtime.rb +14 -11
  170. data/lib/carray/segment_iterator.rb +990 -0
  171. data/lib/carray/serialize.rb +197 -27
  172. data/lib/carray/slab_iterator.rb +109 -40
  173. data/lib/carray/stack.rb +10 -4
  174. data/lib/carray/string_operation_extension.rb +9 -4
  175. data/lib/carray/struct.rb +142 -51
  176. data/lib/carray/struct_builder.rb +110 -14
  177. data/lib/carray/table.rb +2 -2
  178. data/lib/carray/time.rb +535 -105
  179. data/lib/carray/window_iterator.rb +259 -82
  180. data/lib/carray.rb +4 -2
  181. data/yard-stubs/ca_obj_array.rb +4 -4
  182. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  183. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  184. data/yard-stubs/ca_obj_block.rb +2 -7
  185. data/yard-stubs/ca_obj_fake.rb +1 -1
  186. data/yard-stubs/ca_obj_field.rb +1 -1
  187. data/yard-stubs/ca_obj_refer.rb +3 -3
  188. data/yard-stubs/ca_obj_roll.rb +0 -1
  189. data/yard-stubs/ca_obj_stride.rb +10 -7
  190. data/yard-stubs/ca_obj_transpose.rb +1 -1
  191. data/yard-stubs/ca_obj_window.rb +11 -4
  192. data/yard-stubs/carray_access.rb +6 -5
  193. data/yard-stubs/carray_broadcast.rb +2 -2
  194. data/yard-stubs/carray_cast.rb +78 -43
  195. data/yard-stubs/carray_conversion.rb +13 -1
  196. data/yard-stubs/carray_copy.rb +8 -3
  197. data/yard-stubs/carray_core.rb +3 -83
  198. data/yard-stubs/carray_count.rb +16 -2
  199. data/yard-stubs/carray_lazy.rb +210 -1
  200. data/yard-stubs/carray_loop.rb +4 -1
  201. data/yard-stubs/carray_mask.rb +34 -10
  202. data/yard-stubs/carray_math.rb +1499 -4
  203. data/yard-stubs/carray_median_percentile.rb +26 -8
  204. data/yard-stubs/carray_order.rb +43 -82
  205. data/yard-stubs/carray_scatter.rb +10 -3
  206. data/yard-stubs/carray_segment.rb +72 -0
  207. data/yard-stubs/carray_slab.rb +11 -8
  208. data/yard-stubs/carray_sort.rb +50 -8
  209. data/yard-stubs/carray_test.rb +10 -0
  210. data/yard-stubs/carray_utils.rb +0 -26
  211. metadata +19 -19
  212. data/ext/ca_for_each_element.h +0 -239
  213. data/ext/carray_build_flags.h +0 -3
  214. data/ext/carray_config.h +0 -28
  215. data/ext/carray_kernels_bincmp.c +0 -4446
  216. data/ext/carray_kernels_binop.c +0 -11001
  217. data/ext/carray_kernels_init.c +0 -1131
  218. data/ext/carray_kernels_map.c +0 -3467
  219. data/ext/carray_kernels_moncmp.c +0 -2097
  220. data/ext/carray_kernels_monop.c +0 -18313
  221. data/ext/carray_kernels_reduce_aggregate.c +0 -25837
  222. data/ext/carray_kernels_reduce_boolean.c +0 -330
  223. data/ext/carray_kernels_reduce_cumulative.c +0 -14593
  224. data/ext/carray_kernels_reduce_extreme.c +0 -16948
  225. data/ext/carray_kernels_reduce_variance.c +0 -3910
  226. data/ext/carray_kernels_scan.c +0 -3693
  227. data/ext/carray_kernels_search.c +0 -32138
  228. data/ext/carray_kernels_sort.c +0 -10626
  229. 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)
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,27 +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
- CArray.object(n) { |i| cols.map { |c| c[i] } }.categorize
87
+ composite_categorical(cols)
32
88
  end
33
89
  end
34
90
 
35
- private def key_column(key)
36
- case key
37
- when String
38
- self[key]
39
- when CArray
40
- unless key.shape[0] == nrow
41
- raise ArgumentError,
42
- "external group key length #{key.shape[0]} != nrow #{nrow}"
43
- end
44
- key
45
- else
46
- 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])"
47
137
  end
138
+ col
48
139
  end
49
140
  end
50
141
 
@@ -56,11 +147,24 @@ end
56
147
  # aggregate -> declarative per-column reductions into a new frame
57
148
  # table { |g| }-> cross-column Ruby escape, g is a per-group view-frame
58
149
  class GroupedFrame
59
- def initialize(frame, cat, axis_name)
150
+ # +key_names+ are the frame columns the grouping was keyed on. They become
151
+ # the result's index, so the reduction shortcuts must not also return them as
152
+ # reduced columns; an external CArray key contributes no name.
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)
60
162
  @frame = frame
61
163
  @cat = cat
62
164
  @axis_name = axis_name
165
+ @key_names = key_names
63
166
  @labels = cat.labels # group values, in code order
167
+ @index = index || (key && key.project(group_perm[group_bounds[0...ngroup]]))
64
168
  end
65
169
 
66
170
  # Number of groups.
@@ -81,16 +185,22 @@ class GroupedFrame
81
185
  # Declarative aggregation (memo §11.6). Spec maps an output column name to
82
186
  # +[input_column, reduction]+, where reduction is a Symbol (vectorized,
83
187
  # applied through the group iterator) or a Proc (per-group custom, called
84
- # 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]+.
85
191
  def aggregate(spec)
86
192
  cols = {}
87
- spec.each do |out_name, (in_name, reduction)|
193
+ spec.each do |out_name, (in_name, reduction, *args)|
88
194
  out = out_name.to_s
195
+ options = args.last.is_a?(Hash) ? args.pop : {}
89
196
  cols[out] =
90
197
  case reduction
91
198
  when Symbol
92
- self[in_name].public_send(reduction)
199
+ group_reduce(@frame[in_name], reduction, *args, **options)
93
200
  when Proc
201
+ unless args.empty? && options.empty?
202
+ raise ArgumentError, "aggregate: a Proc reduction takes no arguments (#{out.inspect})"
203
+ end
94
204
  per_group_column(in_name, reduction)
95
205
  else
96
206
  raise ArgumentError,
@@ -122,10 +232,36 @@ class GroupedFrame
122
232
  CAFrame.new(cols, axis_name: @axis_name, index: label_index)
123
233
  end
124
234
 
125
- # Convenience reductions over every numeric scalar column (memo §6-4
126
- # "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.
244
+ #
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.
248
+ # @return [CAFrame] one row per group, indexed by the group labels.
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.
252
+ # @return [CAFrame] one row per group.
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.
256
+ # @return [CAFrame] one row per group.
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.
260
+ # @return [CAFrame] one row per group.
127
261
  [:sum, :mean, :min, :max].each do |red|
128
- 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
129
265
  end
130
266
 
131
267
  # @return [String]
@@ -133,23 +269,47 @@ class GroupedFrame
133
269
  "#<GroupedFrame ngroup=#{ngroup} by=#{@axis_name.inspect}>"
134
270
  end
135
271
 
272
+ # Each result gets its own index.
136
273
  private def label_index
274
+ return @index.copy if @index
137
275
  CArray.object(@labels.size) { |i| @labels[i] }
138
276
  end
139
277
 
140
- NON_NUMERIC = [:object, :boolean, :fixlen].freeze
141
- private_constant :NON_NUMERIC
142
-
143
- private def reduce_numeric(reduction)
278
+ private def reduce_columns(reduction, **keywords)
144
279
  cols = {}
145
- @frame.variable_names.each do |name|
280
+ @frame.column_names.each do |name|
281
+ # A key column is the index here, not a result column.
282
+ next if @key_names.include?(name)
146
283
  col = @frame[name]
147
- next unless col.ndim == 1 && !NON_NUMERIC.include?(col.data_type)
148
- cols[name] = col.group_by_category(@cat).public_send(reduction)
284
+ next unless answers?(col, reduction)
285
+ cols[name] = group_reduce(col, reduction, **keywords)
149
286
  end
150
287
  CAFrame.new(cols, axis_name: @axis_name, index: label_index)
151
288
  end
152
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
+
153
313
  private def per_group_column(in_name, proc)
154
314
  results = []
155
315
  each_group_slice(in_name) { |slice| results << proc.call(slice) }
@@ -168,19 +328,18 @@ class GroupedFrame
168
328
  end
169
329
 
170
330
  private def group_address(k)
171
- start = group_offsets[k]
172
- group_perm[start...(start + group_sizes[k])]
331
+ group_perm[group_bounds[k]...group_bounds[k + 1]]
173
332
  end
174
333
 
175
- private def group_perm
176
- @group_perm ||= @cat.sort_addr
334
+ private def group_codes
335
+ @cat.codes
177
336
  end
178
337
 
179
- private def group_offsets
180
- @group_offsets ||= @cat.reduceat_index
338
+ private def group_perm
339
+ @group_perm ||= @cat.sort_addr
181
340
  end
182
341
 
183
- private def group_sizes
184
- @group_sizes ||= @cat.category_sizes
342
+ private def group_bounds
343
+ @group_bounds ||= CArray.segment_offsets(lengths: @cat.category_sizes)
185
344
  end
186
345
  end