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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +304 -0
- data/README.md +3 -4
- data/carray.gemspec +1 -1
- data/ext/ca_axis_dispatch.c +225 -0
- data/ext/ca_axis_group.c +177 -99
- data/ext/ca_binop_dispatch.c +54 -32
- data/ext/ca_binop_dispatch.h +1 -0
- data/ext/ca_categorical_iterator.c +66 -71
- data/ext/ca_compare.c +8 -2
- data/ext/ca_for_buffer.h +21 -68
- data/ext/ca_group_iter.c +27 -11
- data/ext/ca_kernel_iterator.c +663 -214
- data/ext/ca_kernel_iterator.h +231 -167
- data/ext/ca_monop_dispatch.c +4 -4
- data/ext/ca_monop_dispatch.h +4 -0
- data/ext/ca_obj_array.c +132 -22
- data/ext/ca_obj_bincmp.c +152 -121
- data/ext/ca_obj_binop.c +107 -210
- data/ext/ca_obj_bitarray.c +37 -24
- data/ext/ca_obj_bitfield.c +95 -62
- data/ext/ca_obj_block.c +1 -0
- data/ext/ca_obj_byte_swap.c +40 -40
- data/ext/ca_obj_const_string.c +96 -25
- data/ext/ca_obj_face.c +406 -64
- data/ext/ca_obj_face.h +92 -47
- data/ext/ca_obj_fake.c +21 -50
- data/ext/ca_obj_farray.c +1 -0
- data/ext/ca_obj_field.c +16 -3
- data/ext/ca_obj_fixlen_string.c +21 -21
- data/ext/ca_obj_grid.c +181 -124
- data/ext/ca_obj_meld.c +189 -107
- data/ext/ca_obj_moncmp.c +61 -61
- data/ext/ca_obj_monop.c +125 -107
- data/ext/ca_obj_object.c +543 -233
- data/ext/ca_obj_record.c +51 -16
- data/ext/ca_obj_reduce.c +5 -8
- data/ext/ca_obj_refer.c +117 -33
- data/ext/ca_obj_remap.c +57 -17
- data/ext/ca_obj_repeat.c +21 -5
- data/ext/ca_obj_roll.c +115 -11
- data/ext/ca_obj_select.c +47 -49
- data/ext/ca_obj_select_axis.c +58 -60
- data/ext/ca_obj_shift.c +22 -102
- data/ext/ca_obj_stack.c +223 -118
- data/ext/ca_obj_stride.c +195 -34
- data/ext/ca_obj_string.c +17 -18
- data/ext/ca_obj_tile.c +32 -16
- data/ext/ca_obj_time.c +23 -25
- data/ext/ca_obj_timedelta.c +21 -23
- data/ext/ca_obj_transpose.c +12 -3
- data/ext/ca_obj_triop.c +110 -117
- data/ext/ca_obj_window.c +149 -27
- data/ext/ca_op_ipower.c +24 -11
- data/ext/ca_op_powi.h +57 -30
- data/ext/ca_pow5_table.h +656 -0
- data/ext/ca_sort_kernels.h +70 -0
- data/ext/ca_sweep_engine.c +102 -79
- data/ext/ca_sweep_engine.h +7 -20
- data/ext/ca_triop_dispatch.c +4 -1
- data/ext/ca_triop_dispatch.h +5 -2
- data/ext/caframe_csv_reader.c +463 -0
- data/ext/carray.h +70 -27
- data/ext/carray_access.c +455 -211
- data/ext/carray_address_basis.c +177 -36
- data/ext/carray_attribute.c +45 -3
- data/ext/carray_bincount.c +32 -2
- data/ext/carray_broadcast.c +46 -14
- data/ext/carray_call_cfunc.c +0 -28
- data/ext/carray_cast.c +1444 -209
- data/ext/carray_cast_func.rb +34 -21
- data/ext/carray_conversion.c +197 -73
- data/ext/carray_copy.c +32 -11
- data/ext/carray_core.c +514 -122
- data/ext/carray_count.c +140 -87
- data/ext/carray_element.c +18 -27
- data/ext/carray_factorize.c +569 -277
- data/ext/carray_generate.c +76 -40
- data/ext/carray_histogram.c +152 -94
- data/ext/carray_hold.c +2 -1
- data/ext/carray_index_classifier.c +63 -96
- data/ext/carray_internal.h +385 -3
- data/ext/carray_lazy.c +834 -79
- data/ext/carray_loop.c +81 -65
- data/ext/carray_mask.c +682 -316
- data/ext/carray_math_kernel.h +1 -0
- data/ext/carray_median_percentile.c +221 -76
- data/ext/carray_memory_view.c +254 -248
- data/ext/carray_operator.c +1089 -359
- data/ext/carray_order.c +133 -69
- data/ext/carray_partition.c +102 -56
- data/ext/carray_random.c +273 -239
- data/ext/carray_scatter.c +308 -65
- data/ext/carray_segment.c +257 -0
- data/ext/carray_slab.c +170 -70
- data/ext/carray_sort.c +215 -218
- data/ext/carray_sort_kernel.c +8 -1
- data/ext/carray_struct.c +99 -21
- data/ext/carray_test.c +167 -64
- data/ext/carray_time_text.c +1182 -0
- data/ext/carray_utils.c +363 -98
- data/ext/extconf.rb +27 -2
- data/ext/mk_call_cfunc.rb +0 -2
- data/ext/mk_pow5_table.rb +35 -0
- data/ext/mkkernel.rb +1076 -846
- data/ext/ruby_carray.c +7 -1
- data/ext/version.h +4 -4
- data/ext/version.rb +4 -2
- data/lib/carray/attribute.rb +39 -0
- data/lib/carray/autoload_carray.rb +19 -0
- data/lib/carray/autoload_method_extension.rb +6 -0
- data/lib/carray/axis_group.rb +174 -26
- data/lib/carray/basics.rb +91 -25
- data/lib/carray/bincount_nd.rb +6 -0
- data/lib/carray/block_iterator.rb +118 -48
- data/lib/carray/boolean_reduce.rb +21 -29
- data/lib/carray/categorical.rb +94 -26
- data/lib/carray/categorical_iterator.rb +333 -826
- data/lib/carray/complex.rb +36 -29
- data/lib/carray/conditional.rb +94 -7
- data/lib/carray/const_string.rb +46 -0
- data/lib/carray/frame/concat.rb +157 -6
- data/lib/carray/frame/convert.rb +13 -7
- data/lib/carray/frame/csv_parser.rb +589 -82
- data/lib/carray/frame/describe.rb +79 -0
- data/lib/carray/frame/frame.rb +124 -25
- data/lib/carray/frame/group.rb +189 -63
- data/lib/carray/frame/io.rb +412 -72
- data/lib/carray/frame/join.rb +134 -46
- data/lib/carray/frame/records.rb +37 -16
- data/lib/carray/frame/reshape.rb +448 -0
- data/lib/carray/frame/sort.rb +26 -2
- data/lib/carray/frame/verbs.rb +715 -48
- data/lib/carray/frame.rb +2 -0
- data/lib/carray/fuse_source.rb +111 -2
- data/lib/carray/fusion.rb +229 -38
- data/lib/carray/inspect.rb +29 -11
- data/lib/carray/iterator.rb +14 -2
- data/lib/carray/lazy.rb +54 -30
- data/lib/carray/mask_gap_fill.rb +66 -52
- data/lib/carray/math.rb +32 -19
- data/lib/carray/meld_reduce.rb +40 -7
- data/lib/carray/methods/align_addr.rb +8 -4
- data/lib/carray/methods/bincount.rb +2 -2
- data/lib/carray/methods/bit_string.rb +14 -3
- data/lib/carray/methods/composition.rb +16 -10
- data/lib/carray/methods/factorize.rb +2 -2
- data/lib/carray/methods/gather_nd.rb +13 -2
- data/lib/carray/methods/insert_block.rb +9 -14
- data/lib/carray/methods/is_in.rb +117 -4
- data/lib/carray/methods/join.rb +1 -1
- data/lib/carray/methods/locate_addr.rb +177 -36
- data/lib/carray/methods/mode.rb +18 -1
- data/lib/carray/methods/repeat.rb +1 -20
- data/lib/carray/methods/resize.rb +21 -23
- data/lib/carray/methods/snap.rb +34 -12
- data/lib/carray/methods/unique.rb +11 -2
- data/lib/carray/record_order.rb +208 -0
- data/lib/carray/runtime.rb +14 -11
- data/lib/carray/segment_iterator.rb +990 -0
- data/lib/carray/serialize.rb +197 -27
- data/lib/carray/slab_iterator.rb +51 -27
- data/lib/carray/stack.rb +10 -4
- data/lib/carray/string_operation_extension.rb +4 -3
- data/lib/carray/struct.rb +142 -51
- data/lib/carray/struct_builder.rb +110 -14
- data/lib/carray/table.rb +2 -2
- data/lib/carray/time.rb +517 -103
- data/lib/carray/window_iterator.rb +127 -72
- data/lib/carray.rb +2 -2
- data/yard-stubs/ca_obj_array.rb +4 -4
- data/yard-stubs/ca_obj_bitarray.rb +7 -0
- data/yard-stubs/ca_obj_bitfield.rb +23 -12
- data/yard-stubs/ca_obj_fake.rb +1 -1
- data/yard-stubs/ca_obj_field.rb +1 -1
- data/yard-stubs/ca_obj_refer.rb +3 -3
- data/yard-stubs/ca_obj_roll.rb +0 -1
- data/yard-stubs/ca_obj_stride.rb +10 -7
- data/yard-stubs/ca_obj_transpose.rb +1 -1
- data/yard-stubs/ca_obj_window.rb +1 -2
- data/yard-stubs/carray_access.rb +5 -4
- data/yard-stubs/carray_broadcast.rb +1 -1
- data/yard-stubs/carray_cast.rb +78 -43
- data/yard-stubs/carray_conversion.rb +13 -1
- data/yard-stubs/carray_copy.rb +8 -3
- data/yard-stubs/carray_core.rb +3 -3
- data/yard-stubs/carray_count.rb +9 -0
- data/yard-stubs/carray_lazy.rb +5 -1
- data/yard-stubs/carray_loop.rb +4 -1
- data/yard-stubs/carray_mask.rb +34 -10
- data/yard-stubs/carray_math.rb +25 -13
- data/yard-stubs/carray_median_percentile.rb +10 -6
- data/yard-stubs/carray_order.rb +34 -13
- data/yard-stubs/carray_scatter.rb +10 -3
- data/yard-stubs/carray_segment.rb +72 -0
- data/yard-stubs/carray_slab.rb +2 -1
- data/yard-stubs/carray_sort.rb +43 -3
- data/yard-stubs/carray_test.rb +10 -0
- data/yard-stubs/carray_utils.rb +0 -26
- metadata +11 -19
- data/ext/ca_for_each_element.h +0 -239
- data/ext/carray_build_flags.h +0 -3
- data/ext/carray_config.h +0 -28
- data/ext/carray_kernels_bincmp.c +0 -4446
- data/ext/carray_kernels_binop.c +0 -11001
- data/ext/carray_kernels_init.c +0 -1131
- data/ext/carray_kernels_map.c +0 -3467
- data/ext/carray_kernels_moncmp.c +0 -2097
- data/ext/carray_kernels_monop.c +0 -18313
- data/ext/carray_kernels_reduce_aggregate.c +0 -26005
- data/ext/carray_kernels_reduce_boolean.c +0 -330
- data/ext/carray_kernels_reduce_cumulative.c +0 -14862
- data/ext/carray_kernels_reduce_extreme.c +0 -17494
- data/ext/carray_kernels_reduce_variance.c +0 -3910
- data/ext/carray_kernels_scan.c +0 -3693
- data/ext/carray_kernels_search.c +0 -32218
- data/ext/carray_kernels_sort.c +0 -10626
- data/ext/carray_kernels_triop.c +0 -1392
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
# max / min -> value data type, empty/all-masked = MASKED
|
|
36
36
|
# mean -> float64, empty/all-masked = MASKED
|
|
37
37
|
# median / percentile -> float64, empty/all-masked = MASKED
|
|
38
|
-
# variance / stddev (sample, ddof=1) -> float64,
|
|
39
|
-
#
|
|
38
|
+
# variance / stddev (sample, ddof=1) -> float64, fewer than two present
|
|
39
|
+
# values = MASKED
|
|
40
40
|
# all / any -> boolean (boolean value data type only)
|
|
41
41
|
# labels -> cat.labels
|
|
42
42
|
#
|
|
@@ -60,17 +60,17 @@
|
|
|
60
60
|
# ----------------------------------------------------------------------------
|
|
61
61
|
|
|
62
62
|
require "carray"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
class CACategoricalIterator <
|
|
63
|
+
require "carray/segment_iterator"
|
|
64
|
+
|
|
65
|
+
# A CAIterator over the categories of a CACategorical. It is a
|
|
66
|
+
# CASegmentIterator over a category-sorted copy of the value: construction
|
|
67
|
+
# gathers the value through the categorical's cached grouping plan, so each
|
|
68
|
+
# category is one contiguous segment, and every reduction without `axis:` is
|
|
69
|
+
# the segment reduction over that copy (one value per category, in {#labels}
|
|
70
|
+
# order). This class adds the construction, the labels, and the per-fiber
|
|
71
|
+
# form (`axis:`), which classifies each fiber along an axis and keeps the
|
|
72
|
+
# other axes.
|
|
73
|
+
class CACategoricalIterator < CASegmentIterator
|
|
74
74
|
|
|
75
75
|
# value : the payload CArray to reduce, one cell per categorical cell.
|
|
76
76
|
# cat : the CACategorical carrying the classification.
|
|
@@ -105,6 +105,7 @@ class CACategoricalIterator < CAIterator
|
|
|
105
105
|
@elements = cat.category_sizes.int64
|
|
106
106
|
nvalid = counts.sum
|
|
107
107
|
@offsets = cat.reduceat_index # cached segment STARTS (int64[k])
|
|
108
|
+
@bounds = CArray.segment_offsets(lengths: @elements) # every boundary (int64[k+1])
|
|
108
109
|
# Group-major source indices = the valid prefix of the cached sort_addr.
|
|
109
110
|
# With no classified cell the prefix is empty (and slicing a length-0
|
|
110
111
|
# sort_addr would be out of range), so take the empty permutation directly.
|
|
@@ -118,8 +119,8 @@ class CACategoricalIterator < CAIterator
|
|
|
118
119
|
# is nothing to gather — an empty index into an empty source is out of
|
|
119
120
|
# range — so build the empty grouped buffer directly.
|
|
120
121
|
@grouped = nvalid > 0 ? value.reshape(value.elements)[@perm].copy
|
|
121
|
-
: CArray.new(value.data_type, [0])
|
|
122
|
-
@empty = CArray.new(grouped.data_type, [0])
|
|
122
|
+
: CArray.new(value.data_type, [0], bytes: value.bytes)
|
|
123
|
+
@empty = CArray.new(grouped.data_type, [0], bytes: grouped.bytes)
|
|
123
124
|
else
|
|
124
125
|
# Shape mismatch: only per-fiber axis: dispatch could still work. With a
|
|
125
126
|
# 1-D value there is no fiber structure to broadcast into, so a mismatch
|
|
@@ -147,20 +148,6 @@ class CACategoricalIterator < CAIterator
|
|
|
147
148
|
self
|
|
148
149
|
end
|
|
149
150
|
|
|
150
|
-
# @overload each { |members| ... }
|
|
151
|
-
# Yields each category's members (a CArray slice of the grouped copy, in
|
|
152
|
-
# {#labels} order; an empty category yields an empty array). Without a
|
|
153
|
-
# block, returns an Enumerator. This is the own iteration that drives the
|
|
154
|
-
# inherited Enumerable methods (map / count / to_a / ...); it does not use
|
|
155
|
-
# the CAIterator base each / kernel_at_addr path.
|
|
156
|
-
# @yieldparam members [CArray]
|
|
157
|
-
# @return [Enumerator, self]
|
|
158
|
-
def each
|
|
159
|
-
return to_enum(:each) unless block_given?
|
|
160
|
-
@k.times { |c| yield group_slice(c) }
|
|
161
|
-
self
|
|
162
|
-
end
|
|
163
|
-
|
|
164
151
|
# @overload labels
|
|
165
152
|
# Returns the category vocabulary the results are aligned to.
|
|
166
153
|
# @return [Array]
|
|
@@ -174,21 +161,6 @@ class CACategoricalIterator < CAIterator
|
|
|
174
161
|
@k
|
|
175
162
|
end
|
|
176
163
|
|
|
177
|
-
# @overload elements
|
|
178
|
-
# Returns per-group cell counts (classified cells, including value-masked
|
|
179
|
-
# ones; = `cat.category_sizes`), a length-ngroups CArray aligned to
|
|
180
|
-
# {#labels}. The CAIterator count-family member — `CArray#elements`
|
|
181
|
-
# (structural, mask-independent) lifted per group.
|
|
182
|
-
# @return [CArray]
|
|
183
|
-
def elements
|
|
184
|
-
# A copy, like every other member that reads off a memo: the memo is the
|
|
185
|
-
# iterator's own state, and handing out the array itself lets a caller
|
|
186
|
-
# write into it and change what the iterator answers from then on. This
|
|
187
|
-
# one invites it -- the docs point at its prefix sum for splitting a
|
|
188
|
-
# column apart, which reads as scratch.
|
|
189
|
-
counts.copy
|
|
190
|
-
end
|
|
191
|
-
|
|
192
164
|
# Group-vocabulary alias for {#elements}; reads naturally next to
|
|
193
165
|
# {#ngroups} and mirrors `CACategorical#category_sizes`.
|
|
194
166
|
alias group_sizes elements
|
|
@@ -207,543 +179,144 @@ class CACategoricalIterator < CAIterator
|
|
|
207
179
|
"#<#{self.class} ngroups=#{@k} labels=#{@labels.inspect} #{tail}>"
|
|
208
180
|
end
|
|
209
181
|
|
|
210
|
-
#
|
|
211
|
-
# Returns the per-category count of present (non-masked) values as int64
|
|
212
|
-
# — the denominator the value reductions actually divide by. Equals
|
|
213
|
-
# {#elements} unless the value carries a mask. A count is always defined,
|
|
214
|
-
# so an empty category is `0` (never masked).
|
|
215
|
-
# @return [CArray]
|
|
216
|
-
# @overload count_not_masked(axis:)
|
|
217
|
-
# Per-fiber per-category count of present (non-masked) values along `axis`
|
|
218
|
-
# (int64, shape [K, ...band]). Empty cells are `0`.
|
|
219
|
-
# @param axis [Integer]
|
|
220
|
-
# @return [CArray]
|
|
221
|
-
def count_not_masked(axis: nil)
|
|
222
|
-
return axis_counts(axis) if axis
|
|
223
|
-
m = moments
|
|
224
|
-
m ? m[:count].copy : per_category(CA_INT64) { |s| s.count_not_masked }
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
# @overload count(v = <none>)
|
|
228
|
-
# Per-category count, mirroring `CArray#count` per group. No argument
|
|
229
|
-
# returns {#count_not_masked} (present cells); `count(UNDEF)` returns
|
|
230
|
-
# {#count_masked}; `count(v)` counts cells whose value equals `v`.
|
|
231
|
-
# @return [CArray] length-k int64, aligned to {#labels}
|
|
232
|
-
# @overload count(axis:)
|
|
233
|
-
# No-arg + axis: = per-fiber per-category count_not_masked (shape [K, ...band]).
|
|
234
|
-
# `count(v, axis:)` (value equality) and `count(UNDEF, axis:)` are not
|
|
235
|
-
# implemented; use them without `axis:`.
|
|
236
|
-
# @param axis [Integer]
|
|
237
|
-
# @return [CArray]
|
|
238
|
-
def count (*args, axis: nil)
|
|
239
|
-
if axis
|
|
240
|
-
return count_not_masked(axis: axis) if args.empty?
|
|
241
|
-
raise NotImplementedError,
|
|
242
|
-
"CACategoricalIterator#count(v, axis:) is not implemented — " \
|
|
243
|
-
"value-equality count is available without axis:."
|
|
244
|
-
end
|
|
245
|
-
return count_not_masked if args.empty?
|
|
246
|
-
# Delegate per group to CArray#count (handles count(UNDEF) -> masked count and
|
|
247
|
-
# count(v) alike, with core's exact data type equality). The group slice is a
|
|
248
|
-
# CABlock, whose own #count is the block geometry accessor, so dispatch
|
|
249
|
-
# CArray#count explicitly. (Not fused: a value-equality reduceat would have
|
|
250
|
-
# to reproduce core's cross-type / out-of-range equality exactly.)
|
|
251
|
-
cnt = CArray.instance_method(:count)
|
|
252
|
-
per_category(CA_INT64) { |s| cnt.bind_call(s, *args) }
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
# @overload count_masked
|
|
256
|
-
# Returns the per-category count of masked (missing) values as int64.
|
|
257
|
-
# Empty categories are `0`.
|
|
258
|
-
# @return [CArray]
|
|
259
|
-
# @overload count_masked(axis:)
|
|
260
|
-
# Not implemented; call it without `axis:`.
|
|
261
|
-
# @param axis [Integer]
|
|
262
|
-
# @return [CArray]
|
|
263
|
-
def count_masked(axis: nil)
|
|
264
|
-
if axis
|
|
265
|
-
raise NotImplementedError,
|
|
266
|
-
"CACategoricalIterator#count_masked(axis:) is not implemented — " \
|
|
267
|
-
"call it without axis:."
|
|
268
|
-
end
|
|
269
|
-
m = moments
|
|
270
|
-
m ? counts - m[:count] : per_category(CA_INT64) { |s| s.count_masked }
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
# @overload sum
|
|
274
|
-
# Returns per-category sums in the data type `CArray#sum` promotes the value
|
|
275
|
-
# to (float64 for an integer value). `accumulate` is the same fold kept in
|
|
276
|
-
# the value's own type. An empty or fully-masked category sums the empty
|
|
277
|
-
# set, which is the additive identity `0` (unmasked) — the same contract as
|
|
278
|
-
# `CArray#sum` on an empty / all-masked array.
|
|
279
|
-
# @return [CArray]
|
|
280
|
-
# @overload sum(axis:)
|
|
281
|
-
# Returns per-category sums per fiber along `axis`. Cat may be 1-D (case
|
|
282
|
-
# A, broadcasts across band axes), same rank as source (case B, per-fiber
|
|
283
|
-
# independent classifier), or one rank less (band-only, constant along
|
|
284
|
-
# reduce axis). Output shape = `[K, ...source.shape without axis]`.
|
|
285
|
-
# @param axis [Integer] reduce axis of the source value.
|
|
286
|
-
# @return [CArray]
|
|
287
|
-
def sum(axis: nil)
|
|
288
|
-
return axis_sum(axis) if axis
|
|
289
|
-
m = moments
|
|
290
|
-
return per_category(core_reduce_type(:sum)) { |s| s.sum } unless m
|
|
291
|
-
m[:sum].copy # the moments sum IS the core fold (empty -> 0.0)
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
# @overload accumulate
|
|
295
|
-
# Returns per-category sums folded in the value's own data type, wrapping at
|
|
296
|
-
# its width, as the core `accumulate` does. This is the exact in-type fold:
|
|
297
|
-
# `sum` reads its answer off a float64 moment and casts back, so it loses
|
|
298
|
-
# the low bits of a wide integer payload and does not wrap. An empty or
|
|
299
|
-
# fully-masked category accumulates the empty set, the additive identity `0`
|
|
300
|
-
# (unmasked).
|
|
301
|
-
# @return [CArray]
|
|
302
|
-
# @overload accumulate(axis:)
|
|
303
|
-
# Per-fiber per-category in-type sums along `axis`. Output shape =
|
|
304
|
-
# `[K, ...source.shape without axis]`.
|
|
305
|
-
# @param axis [Integer] reduce axis of the source value.
|
|
306
|
-
# @return [CArray]
|
|
307
|
-
def accumulate(axis: nil)
|
|
308
|
-
return axis_by_masked_copy(axis, :accumulate, core_reduce_type(:accumulate)) if axis
|
|
309
|
-
per_category(core_reduce_type(:accumulate)) { |s| s.accumulate }
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
# @overload max
|
|
313
|
-
# Returns per-category maxima in the value data type. Empty categories are
|
|
314
|
-
# MASKED.
|
|
315
|
-
# @return [CArray]
|
|
316
|
-
# @overload max(axis:)
|
|
317
|
-
# Per-fiber per-category maxima along `axis` (h's data type, masked where empty).
|
|
318
|
-
# @param axis [Integer]
|
|
319
|
-
# @return [CArray]
|
|
320
|
-
def max(axis: nil)
|
|
321
|
-
return axis_moments(axis)[:max] if axis
|
|
322
|
-
m = moments
|
|
323
|
-
m ? m[:max].copy : per_category(core_reduce_type(:max)) { |s| s.max }
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
# @overload min
|
|
327
|
-
# Returns per-category minima in the value data type. Empty categories are
|
|
328
|
-
# MASKED.
|
|
329
|
-
# @return [CArray]
|
|
330
|
-
# @overload min(axis:)
|
|
331
|
-
# Per-fiber per-category minima along `axis` (h's data type, masked where empty).
|
|
332
|
-
# @param axis [Integer]
|
|
333
|
-
# @return [CArray]
|
|
334
|
-
def min(axis: nil)
|
|
335
|
-
return axis_moments(axis)[:min] if axis
|
|
336
|
-
m = moments
|
|
337
|
-
m ? m[:min].copy : per_category(core_reduce_type(:min)) { |s| s.min }
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
# @overload mean
|
|
341
|
-
# Returns per-category means as float64. Empty categories are MASKED.
|
|
342
|
-
# @return [CArray]
|
|
343
|
-
# @overload mean(axis:)
|
|
344
|
-
# Per-fiber per-category means (float64, empty group cells MASKED).
|
|
345
|
-
# @param axis [Integer]
|
|
346
|
-
# @return [CArray]
|
|
347
|
-
def mean(axis: nil)
|
|
348
|
-
return axis_mean(axis) if axis
|
|
349
|
-
m = moments
|
|
350
|
-
return per_category(core_reduce_type(:mean)) { |s| s.mean } unless m
|
|
351
|
-
cnt = m[:count]
|
|
352
|
-
out = m[:sum] / cnt.float64 # count 0 -> NaN, masked next
|
|
353
|
-
out[cnt.eq(0)] = UNDEF # empty / all-masked category -> MASKED
|
|
354
|
-
out
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
# @overload median
|
|
358
|
-
# Returns per-category medians as float64. Empty categories are MASKED.
|
|
359
|
-
# @return [CArray]
|
|
360
|
-
def median(axis: nil)
|
|
361
|
-
axis_order_stat_defer!(:median) if axis
|
|
362
|
-
percentile(50.0)
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
# @overload percentile(p)
|
|
366
|
-
# Returns the per-category `p`-th percentile as float64 (`p` in 0..100,
|
|
367
|
-
# `:linear` interpolation, matching `CArray#percentile`). Empty categories
|
|
368
|
-
# are MASKED. Order statistics need every value of a group held together —
|
|
369
|
-
# this is the reduceat that only the eager grouped copy can serve.
|
|
370
|
-
# @param p [Numeric] percentile in 0..100.
|
|
371
|
-
# @return [CArray]
|
|
372
|
-
def percentile (p, axis: nil)
|
|
373
|
-
axis_order_stat_defer!(:percentile) if axis
|
|
374
|
-
unless MONOID_TYPES.include?(grouped.data_type)
|
|
375
|
-
return per_category(core_reduce_type(:percentile, p)) { |s| s.percentile(p) }
|
|
376
|
-
end
|
|
377
|
-
out = CArray.float64(@k)
|
|
378
|
-
grouped.send(:__reduceat_percentile__, @offsets, p.to_f, out)
|
|
379
|
-
out
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
# @overload quantile
|
|
383
|
-
# Returns the per-category five-number summary `[min, Q1, median, Q3, max]`
|
|
384
|
-
# as five length-k float64 CArrays (matching `CArray#quantile`): the
|
|
385
|
-
# percentiles at 0 / 25 / 50 / 75 / 100. Empty / all-masked categories are
|
|
386
|
-
# MASKED. For a single fraction q in 0..1 use `percentile(q * 100)`.
|
|
387
|
-
# @return [Array<CArray>]
|
|
388
|
-
def quantile
|
|
389
|
-
unless MONOID_TYPES.include?(grouped.data_type)
|
|
390
|
-
return [0, 25, 50, 75, 100].map { |p| percentile(p) }
|
|
391
|
-
end
|
|
392
|
-
outs = Array.new(5) { CArray.float64(@k) }
|
|
393
|
-
grouped.send(:__reduceat_quantile__, @offsets, *outs)
|
|
394
|
-
outs
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
# @overload variance
|
|
398
|
-
# Returns per-category SAMPLE variance (ddof=1) as float64. Matches
|
|
399
|
-
# `CArray#variance` per group: an empty or fully-masked category is MASKED,
|
|
400
|
-
# a single-value category is `0.0` (CArray's n=1 contract), n>=2 is the
|
|
401
|
-
# sample variance.
|
|
402
|
-
# @return [CArray]
|
|
403
|
-
def variance(axis: nil)
|
|
404
|
-
return axis_by_masked_copy(axis, :variance) if axis
|
|
405
|
-
m = moments
|
|
406
|
-
return per_category(core_reduce_type(:variance)) { |s| s.variance } unless m
|
|
407
|
-
cnt = m[:count]
|
|
408
|
-
means = m[:sum] / cnt.float64 # per-segment mean (garbage where count 0/1,
|
|
409
|
-
out = CArray.float64(@k) # ignored by the kernel's n<2 guards)
|
|
410
|
-
grouped.send(:__reduceat_variance__, @offsets, means, cnt, out)
|
|
411
|
-
out
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
# @overload stddev
|
|
415
|
-
# Returns per-category SAMPLE standard deviation (ddof=1) as float64.
|
|
416
|
-
# Matches `CArray#stddev` per group (empty / all-masked MASKED,
|
|
417
|
-
# single-value `0.0`).
|
|
418
|
-
# @return [CArray]
|
|
419
|
-
def stddev(axis: nil)
|
|
420
|
-
return axis_by_masked_copy(axis, :stddev) if axis
|
|
421
|
-
m = moments
|
|
422
|
-
return per_category(core_reduce_type(:stddev)) { |s| s.stddev } unless m
|
|
423
|
-
variance.sqrt # sqrt propagates the n=0 mask
|
|
424
|
-
end
|
|
425
|
-
|
|
426
|
-
# @overload prod
|
|
427
|
-
# Returns per-category products as float64 (matching `CArray#prod`). An
|
|
428
|
-
# empty / fully-masked category is `1.0` (the multiplicative identity).
|
|
429
|
-
# Single-pass reduceat for numeric values; per-group fallback otherwise.
|
|
430
|
-
# @return [CArray]
|
|
431
|
-
# @overload prod(axis:)
|
|
432
|
-
# Per-fiber per-category products (float64, shape [K, ...band]). Empty
|
|
433
|
-
# group cells `1.0` (identity).
|
|
434
|
-
# @param axis [Integer]
|
|
435
|
-
# @return [CArray]
|
|
436
|
-
def prod(axis: nil)
|
|
437
|
-
return axis_prod(axis) if axis
|
|
438
|
-
return per_category(core_reduce_type(:prod)) { |s| s.prod } unless MONOID_TYPES.include?(grouped.data_type)
|
|
439
|
-
out = CArray.float64(@k)
|
|
440
|
-
grouped.send(:__reduceat_prod__, @offsets, out)
|
|
441
|
-
out
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
# @overload all
|
|
445
|
-
# Returns the per-category `all` as boolean (matching `CArray#all`): true
|
|
446
|
-
# iff every present value is truthy (empty category -> true, vacuously).
|
|
447
|
-
# The value data type must be boolean, as for `CArray#all`.
|
|
448
|
-
# @return [CArray]
|
|
449
|
-
def all
|
|
450
|
-
aa = all_any
|
|
451
|
-
aa ? aa[:all] : per_category(CA_BOOLEAN) { |s| s.all }
|
|
452
|
-
end
|
|
453
|
-
|
|
454
|
-
# @overload any
|
|
455
|
-
# Returns the per-category `any` as boolean (matching `CArray#any`): true
|
|
456
|
-
# iff some present value is truthy (empty category -> false). The value
|
|
457
|
-
# data type must be boolean, as for `CArray#any`.
|
|
458
|
-
# @return [CArray]
|
|
459
|
-
def any
|
|
460
|
-
aa = all_any
|
|
461
|
-
aa ? aa[:any] : per_category(CA_BOOLEAN) { |s| s.any }
|
|
462
|
-
end
|
|
463
|
-
|
|
464
|
-
# ---- tier 2 (fused / population / position) ------------------------------
|
|
465
|
-
|
|
466
|
-
# @overload minmax
|
|
467
|
-
# Returns the per-category `[min, max]` pair (each a length-k CArray in the
|
|
468
|
-
# value data type; empty categories MASKED), matching `CArray#minmax`. Both come
|
|
469
|
-
# from one moments pass.
|
|
470
|
-
# @return [Array<CArray>]
|
|
471
|
-
# @overload minmax(axis:)
|
|
472
|
-
# Per-fiber `[min_ca, max_ca]` along `axis` (each shape [K, ...band], h's data type,
|
|
473
|
-
# empty group cells MASKED). Ruby Array of two CArrays, not stacked.
|
|
474
|
-
# Both come from one kernel run.
|
|
475
|
-
# @param axis [Integer]
|
|
476
|
-
# @return [Array<CArray>]
|
|
477
|
-
def minmax(axis: nil)
|
|
478
|
-
if axis
|
|
479
|
-
# take both off one pass rather than asking min and max separately,
|
|
480
|
-
# which would run the kernel twice now that nothing is kept between
|
|
481
|
-
# calls -- this is the "keep the result" the axis: family expects
|
|
482
|
-
m = axis_moments(axis)
|
|
483
|
-
return [m[:min], m[:max]]
|
|
484
|
-
end
|
|
485
|
-
[min, max]
|
|
486
|
-
end
|
|
487
|
-
|
|
488
|
-
# @overload variancep
|
|
489
|
-
# Per-category POPULATION variance (ddof=0) as float64, matching
|
|
490
|
-
# `CArray#variancep`: empty / all-masked -> MASKED, single value -> 0.0.
|
|
491
|
-
# Derived from the sample variance (variancep = variance * (n-1) / n), so it
|
|
492
|
-
# reuses the centred two-pass kernel with no extra walk.
|
|
493
|
-
# @return [CArray]
|
|
494
|
-
def variancep(axis: nil)
|
|
495
|
-
return axis_by_masked_copy(axis, :variancep) if axis
|
|
496
|
-
m = moments
|
|
497
|
-
return per_category(core_reduce_type(:variancep)) { |s| s.variancep } unless m
|
|
498
|
-
cnt = m[:count]
|
|
499
|
-
vp = variance * (cnt - 1).float64 / cnt.float64
|
|
500
|
-
vp[cnt.eq(0)] = UNDEF # empty / all-masked stays masked
|
|
501
|
-
vp
|
|
502
|
-
end
|
|
503
|
-
|
|
504
|
-
# @overload stddevp
|
|
505
|
-
# Per-category POPULATION standard deviation (ddof=0) as float64.
|
|
506
|
-
# @return [CArray]
|
|
507
|
-
# @overload stddevp(axis:)
|
|
508
|
-
# Per-fiber per-category population stddev (float64, empty group cells MASKED).
|
|
509
|
-
# @param axis [Integer]
|
|
510
|
-
# @return [CArray]
|
|
511
|
-
def stddevp(axis: nil)
|
|
512
|
-
return axis_by_masked_copy(axis, :stddevp) if axis
|
|
513
|
-
m = moments
|
|
514
|
-
return per_category(core_reduce_type(:stddevp)) { |s| s.stddevp } unless m
|
|
515
|
-
variancep.sqrt
|
|
516
|
-
end
|
|
517
|
-
|
|
518
|
-
# @overload min_index
|
|
519
|
-
# Per-category group-local index of the minimum — the position within the
|
|
520
|
-
# category's members (source order) — matching `CArray#min_index` per group.
|
|
521
|
-
# Empty / all-masked categories are MASKED. Single-pass fused reduceat for
|
|
522
|
-
# numeric values; per-group fallback otherwise.
|
|
523
|
-
# @return [CArray] length-k int64
|
|
524
|
-
def min_index
|
|
525
|
-
am = arg_minmax
|
|
526
|
-
am ? am[:min].copy : per_category(CA_INT64) { |s| s.min_index }
|
|
527
|
-
end
|
|
528
|
-
|
|
529
|
-
# @overload max_index
|
|
530
|
-
# Per-category group-local index of the maximum. See {#min_index}.
|
|
531
|
-
# @return [CArray] length-k int64
|
|
532
|
-
def max_index
|
|
533
|
-
am = arg_minmax
|
|
534
|
-
am ? am[:max].copy : per_category(CA_INT64) { |s| s.max_index }
|
|
535
|
-
end
|
|
536
|
-
|
|
537
|
-
# @overload min_addr
|
|
538
|
-
# Per-category flat source address of the minimum — which cell of the source
|
|
539
|
-
# value holds it, matching `CArray#min_addr` per group. Unlike {#min_index}
|
|
540
|
-
# (the group-local rank) this indexes back into the original array
|
|
541
|
-
# (`value.reshape(value.elements)[grp.min_addr]`). Empty categories MASKED.
|
|
542
|
-
# @return [CArray] length-k int64
|
|
543
|
-
def min_addr
|
|
544
|
-
group_addr(min_index)
|
|
545
|
-
end
|
|
546
|
-
|
|
547
|
-
# @overload max_addr
|
|
548
|
-
# Per-category flat source address of the maximum. See {#min_addr}.
|
|
549
|
-
# @return [CArray] length-k int64
|
|
550
|
-
def max_addr
|
|
551
|
-
group_addr(max_index)
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
# @overload sort_addr
|
|
555
|
-
# Per-category sort by flat source address. Returns a length-nvalid
|
|
556
|
-
# (= `elements.sum`) int64 CArray of the flat SOURCE addresses that sort each
|
|
557
|
-
# category's members, in group-major order: segment `c` holds category `c`'s
|
|
558
|
-
# source addresses in ascending-value order, segments concatenated in
|
|
559
|
-
# {#labels} order. So `value.reshape(value.elements)[grp.sort_addr]` yields
|
|
560
|
-
# the values grouped and sorted within each group, and splitting by the
|
|
561
|
-
# {#elements} prefix sum gives per-group. Excluded cells (in no category) are
|
|
562
|
-
# omitted. A masked value sorts to the tail of its segment (as `CArray#sort`
|
|
563
|
-
# sends masked cells to the end), so with a mask the first address is the
|
|
564
|
-
# minimum but the last is the masked cell, not the maximum.
|
|
182
|
+
# ---- the per-fiber form ---------------------------------------------------
|
|
565
183
|
#
|
|
566
|
-
#
|
|
567
|
-
#
|
|
568
|
-
#
|
|
569
|
-
# category-contiguous), so only the source-address form is offered, mirroring
|
|
570
|
-
# {#min_addr} vs the skipped group-local min_index-into-source.
|
|
571
|
-
# @return [CArray] length-nvalid int64
|
|
572
|
-
def sort_addr
|
|
573
|
-
out = CArray.int64(grouped.elements)
|
|
574
|
-
@k.times do |c|
|
|
575
|
-
lo = @offsets[c]
|
|
576
|
-
hi = (c + 1 < @k) ? @offsets[c + 1] : grouped.elements
|
|
577
|
-
next unless hi > lo
|
|
578
|
-
# View-local sort order of the segment (0..size-1), lifted to grouped
|
|
579
|
-
# slots, then mapped back to source addresses via perm.
|
|
580
|
-
out[lo...hi] = perm[grouped[lo...hi].sort_addr + lo]
|
|
581
|
-
end
|
|
582
|
-
out
|
|
583
|
-
end
|
|
584
|
-
|
|
585
|
-
# @overload wsum(weights)
|
|
586
|
-
# Per-category weighted sum as float64, matching `CArray#wsum`. `weights` is
|
|
587
|
-
# a per-cell weight CArray in the source order (same elements as the value).
|
|
588
|
-
# Empty / all-masked category -> 0.0 (the additive identity). A cell is
|
|
589
|
-
# skipped iff its value OR its weight is masked (core's contract).
|
|
590
|
-
# @param weights [CArray]
|
|
591
|
-
# @return [CArray]
|
|
592
|
-
# @overload wsum(weights, axis:)
|
|
593
|
-
# Per-fiber per-category weighted sum along `axis`. `weights` must have
|
|
594
|
-
# shape == source.shape (rev3 requires explicit broadcast; wrap 1-D or
|
|
595
|
-
# band-shape weights via `.broadcast_to(*source.shape)` at the call site).
|
|
596
|
-
# Empty group cell → `0.0` (identity). Mask contract: cell contributes iff
|
|
597
|
-
# value AND weight are present.
|
|
598
|
-
# @param weights [CArray]
|
|
599
|
-
# @param axis [Integer]
|
|
600
|
-
# @return [CArray]
|
|
601
|
-
def wsum (weights, axis: nil)
|
|
602
|
-
return axis_wsum_wmean(weights, axis)[0] if axis
|
|
603
|
-
wg = scatter_weights(weights)
|
|
604
|
-
return kernel_weighted(wg)[0] if MONOID_TYPES.include?(grouped.data_type)
|
|
605
|
-
fold_weighted(wg, 0.0) { |v, ws| v.wsum(ws) }
|
|
606
|
-
end
|
|
607
|
-
|
|
608
|
-
# @overload wmean(weights)
|
|
609
|
-
# Per-category weighted mean as float64, matching `CArray#wmean`. Empty
|
|
610
|
-
# category -> MASKED; a present category whose weights sum to zero -> NaN
|
|
611
|
-
# (core's 0/0 contract).
|
|
612
|
-
# @param weights [CArray]
|
|
613
|
-
# @return [CArray]
|
|
614
|
-
# @overload wmean(weights, axis:)
|
|
615
|
-
# Per-fiber per-category weighted mean along `axis`. Same weights-shape
|
|
616
|
-
# contract as {#wsum} (weights.shape == source.shape). Empty cell → MASKED;
|
|
617
|
-
# a present cell whose weights sum to zero → NaN (0/0 core contract).
|
|
618
|
-
# @param weights [CArray]
|
|
619
|
-
# @param axis [Integer]
|
|
620
|
-
# @return [CArray]
|
|
621
|
-
def wmean (weights, axis: nil)
|
|
622
|
-
return axis_wsum_wmean(weights, axis)[1] if axis
|
|
623
|
-
wg = scatter_weights(weights)
|
|
624
|
-
return kernel_weighted(wg)[1] if MONOID_TYPES.include?(grouped.data_type)
|
|
625
|
-
fold_weighted(wg, UNDEF) { |v, ws| v.wmean(ws) }
|
|
626
|
-
end
|
|
627
|
-
|
|
628
|
-
# @overload reduce { |members| ... }
|
|
629
|
-
# Custom per-category reduction (the escape hatch for statistics not in the
|
|
630
|
-
# named surface), mirroring `CArray#reduce_slab`. The block receives each
|
|
631
|
-
# category's members (a CArray) and returns one value per category.
|
|
632
|
-
# @yieldparam members [CArray]
|
|
633
|
-
# @return [CArray] length-k, aligned to {#labels}
|
|
634
|
-
# @overload reduce(init) { |acc, elem| ... }
|
|
635
|
-
# Per-category fiber fold: each category's members are folded element by
|
|
636
|
-
# element starting from `init`.
|
|
637
|
-
# @param init [Object] initial accumulator.
|
|
638
|
-
# @return [CArray] length-k
|
|
639
|
-
def reduce (*args, data_type: nil, &blk)
|
|
640
|
-
raise LocalJumpError, "no block given (yield)" unless blk
|
|
641
|
-
dt = data_type || CA_OBJECT
|
|
642
|
-
if args.empty?
|
|
643
|
-
per_category(dt) { |s| blk.call(s) }
|
|
644
|
-
else
|
|
645
|
-
init = args[0]
|
|
646
|
-
per_category(dt) { |s|
|
|
647
|
-
acc = init
|
|
648
|
-
s.each { |e| acc = blk.call(acc, e) }
|
|
649
|
-
acc
|
|
650
|
-
}
|
|
651
|
-
end
|
|
652
|
-
end
|
|
653
|
-
|
|
654
|
-
# @overload map(data_type: nil) { |members| ... }
|
|
655
|
-
# Group-wise element-wise transform, mirroring `CArray#map_slab`. The block
|
|
656
|
-
# receives each category's members and returns either a same-length CArray
|
|
657
|
-
# (scattered back cell for cell) or a scalar (broadcast over the group's
|
|
658
|
-
# cells). Returns a NEW CArray shaped like the source `value`; the original
|
|
659
|
-
# is not modified (`value[] = grp.map { ... }` for in-place). Excluded cells
|
|
660
|
-
# (in no category) are UNDEF in the result.
|
|
661
|
-
# @yieldparam members [CArray]
|
|
662
|
-
# @return [CArray] shaped like the source value
|
|
663
|
-
def map (data_type: nil)
|
|
664
|
-
raise LocalJumpError, "no block given (yield)" unless block_given?
|
|
665
|
-
dt = data_type || grouped.data_type
|
|
666
|
-
# Apply the block per category, assembled in grouped (category-contiguous)
|
|
667
|
-
# order: a same-length result scatters cell for cell, a scalar broadcasts.
|
|
668
|
-
transformed = CArray.new(dt, [grouped.elements])
|
|
669
|
-
@k.times do |c|
|
|
670
|
-
lo = @offsets[c]
|
|
671
|
-
hi = (c + 1 < @k) ? @offsets[c + 1] : grouped.elements
|
|
672
|
-
transformed[lo...hi] = yield(grouped[lo...hi]) if hi > lo
|
|
673
|
-
end
|
|
674
|
-
# Scatter back to source positions via the permutation (grouped-order source
|
|
675
|
-
# indices). Excluded cells are absent from perm and stay UNDEF.
|
|
676
|
-
out = CArray.new(dt, @src_shape)
|
|
677
|
-
out[] = UNDEF
|
|
678
|
-
out.reshape(@codes.elements)[perm] = transformed
|
|
679
|
-
out
|
|
680
|
-
end
|
|
681
|
-
|
|
682
|
-
# ---- segment scan: within-category running statistics ------------------
|
|
184
|
+
# These reductions also take `axis:` on a categorical. Without it each is
|
|
185
|
+
# the CASegmentIterator reduction of the same name, over the category-sorted
|
|
186
|
+
# copy: one value per category, in {#labels} order.
|
|
683
187
|
#
|
|
684
|
-
#
|
|
685
|
-
#
|
|
686
|
-
#
|
|
687
|
-
#
|
|
688
|
-
#
|
|
689
|
-
#
|
|
690
|
-
#
|
|
691
|
-
#
|
|
692
|
-
#
|
|
693
|
-
#
|
|
694
|
-
#
|
|
695
|
-
#
|
|
696
|
-
#
|
|
697
|
-
#
|
|
698
|
-
#
|
|
699
|
-
|
|
700
|
-
#
|
|
701
|
-
#
|
|
702
|
-
# @
|
|
703
|
-
#
|
|
704
|
-
#
|
|
705
|
-
#
|
|
706
|
-
#
|
|
707
|
-
#
|
|
708
|
-
#
|
|
709
|
-
#
|
|
710
|
-
#
|
|
711
|
-
# @
|
|
712
|
-
#
|
|
713
|
-
#
|
|
714
|
-
#
|
|
715
|
-
[
|
|
716
|
-
|
|
717
|
-
|
|
188
|
+
# @!method accumulate(axis: nil)
|
|
189
|
+
# @overload accumulate
|
|
190
|
+
# The per-category form: {CASegmentIterator#accumulate}, one value per category.
|
|
191
|
+
# @overload accumulate(axis:)
|
|
192
|
+
# Per-fiber per-category in-type sums along `axis`. Output shape =
|
|
193
|
+
# `[K, ...source.shape without axis]`.
|
|
194
|
+
# @param axis [Integer] reduce axis of the source value.
|
|
195
|
+
# @return [CArray]
|
|
196
|
+
# @!method count(*args, axis: nil)
|
|
197
|
+
# @overload count
|
|
198
|
+
# The per-category form: {CASegmentIterator#count}, one value per category.
|
|
199
|
+
# @overload count(axis:)
|
|
200
|
+
# No-arg + axis: = per-fiber per-category count_not_masked (shape [K, ...band]).
|
|
201
|
+
# `count(v, axis:)` (value equality) and `count(UNDEF, axis:)` are not
|
|
202
|
+
# implemented; use them without `axis:`.
|
|
203
|
+
# @param axis [Integer]
|
|
204
|
+
# @return [CArray]
|
|
205
|
+
# @!method count_masked(axis: nil)
|
|
206
|
+
# @overload count_masked
|
|
207
|
+
# The per-category form: {CASegmentIterator#count_masked}, one value per category.
|
|
208
|
+
# @overload count_masked(axis:)
|
|
209
|
+
# Not implemented; call it without `axis:`.
|
|
210
|
+
# @param axis [Integer]
|
|
211
|
+
# @return [CArray]
|
|
212
|
+
# @!method count_not_masked(axis: nil)
|
|
213
|
+
# @overload count_not_masked
|
|
214
|
+
# The per-category form: {CASegmentIterator#count_not_masked}, one value per category.
|
|
215
|
+
# @overload count_not_masked(axis:)
|
|
216
|
+
# Per-fiber per-category count of present (non-masked) values along `axis`
|
|
217
|
+
# (int64, shape [K, ...band]). Empty cells are `0`.
|
|
218
|
+
# @param axis [Integer]
|
|
219
|
+
# @return [CArray]
|
|
220
|
+
# @!method max(axis: nil)
|
|
221
|
+
# @overload max
|
|
222
|
+
# The per-category form: {CASegmentIterator#max}, one value per category.
|
|
223
|
+
# @overload max(axis:)
|
|
224
|
+
# Per-fiber per-category maxima along `axis` (h's data type, masked where empty).
|
|
225
|
+
# @param axis [Integer]
|
|
226
|
+
# @return [CArray]
|
|
227
|
+
# @!method mean(axis: nil)
|
|
228
|
+
# @overload mean
|
|
229
|
+
# The per-category form: {CASegmentIterator#mean}, one value per category.
|
|
230
|
+
# @overload mean(axis:)
|
|
231
|
+
# Per-fiber per-category means (float64, empty group cells MASKED).
|
|
232
|
+
# @param axis [Integer]
|
|
233
|
+
# @return [CArray]
|
|
234
|
+
# @!method min(axis: nil)
|
|
235
|
+
# @overload min
|
|
236
|
+
# The per-category form: {CASegmentIterator#min}, one value per category.
|
|
237
|
+
# @overload min(axis:)
|
|
238
|
+
# Per-fiber per-category minima along `axis` (h's data type, masked where empty).
|
|
239
|
+
# @param axis [Integer]
|
|
240
|
+
# @return [CArray]
|
|
241
|
+
# @!method minmax(axis: nil)
|
|
242
|
+
# @overload minmax
|
|
243
|
+
# The per-category form: {CASegmentIterator#minmax}, one value per category.
|
|
244
|
+
# @overload minmax(axis:)
|
|
245
|
+
# Per-fiber `[min_ca, max_ca]` along `axis` (each shape [K, ...band], h's data type,
|
|
246
|
+
# empty group cells MASKED). Ruby Array of two CArrays, not stacked.
|
|
247
|
+
# Both come from one kernel run.
|
|
248
|
+
# @param axis [Integer]
|
|
249
|
+
# @return [Array<CArray>]
|
|
250
|
+
# @!method prod(axis: nil)
|
|
251
|
+
# @overload prod
|
|
252
|
+
# The per-category form: {CASegmentIterator#prod}, one value per category.
|
|
253
|
+
# @overload prod(axis:)
|
|
254
|
+
# Per-fiber per-category products (float64, shape [K, ...band]). Empty
|
|
255
|
+
# group cells `1.0` (identity).
|
|
256
|
+
# @param axis [Integer]
|
|
257
|
+
# @return [CArray]
|
|
258
|
+
# @!method stddevp(axis: nil)
|
|
259
|
+
# @overload stddevp
|
|
260
|
+
# The per-category form: {CASegmentIterator#stddevp}, one value per category.
|
|
261
|
+
# @overload stddevp(axis:)
|
|
262
|
+
# Per-fiber per-category population stddev (float64, empty group cells MASKED).
|
|
263
|
+
# @param axis [Integer]
|
|
264
|
+
# @return [CArray]
|
|
265
|
+
# @!method sum(axis: nil)
|
|
266
|
+
# @overload sum
|
|
267
|
+
# The per-category form: {CASegmentIterator#sum}, one value per category.
|
|
268
|
+
# @overload sum(axis:)
|
|
269
|
+
# Returns per-category sums per fiber along `axis`. Cat may be 1-D (case
|
|
270
|
+
# A, broadcasts across band axes), same rank as source (case B, per-fiber
|
|
271
|
+
# independent classifier), or one rank less (band-only, constant along
|
|
272
|
+
# reduce axis). Output shape = `[K, ...source.shape without axis]`.
|
|
273
|
+
# @param axis [Integer] reduce axis of the source value.
|
|
274
|
+
# @return [CArray]
|
|
275
|
+
# @!method wmean(weights, axis: nil)
|
|
276
|
+
# @overload wmean
|
|
277
|
+
# The per-category form: {CASegmentIterator#wmean}, one value per category.
|
|
278
|
+
# @overload wmean(weights, axis:)
|
|
279
|
+
# Per-fiber per-category weighted mean along `axis`. Same weights-shape
|
|
280
|
+
# contract as {#wsum} (weights.shape == source.shape). Empty cell → MASKED;
|
|
281
|
+
# a present cell whose weights sum to zero → NaN (0/0 core contract).
|
|
282
|
+
# @param weights [CArray]
|
|
283
|
+
# @param axis [Integer]
|
|
284
|
+
# @return [CArray]
|
|
285
|
+
# @!method wsum(weights, axis: nil)
|
|
286
|
+
# @overload wsum
|
|
287
|
+
# The per-category form: {CASegmentIterator#wsum}, one value per category.
|
|
288
|
+
# @overload wsum(weights, axis:)
|
|
289
|
+
# Per-fiber per-category weighted sum along `axis`. `weights` must have
|
|
290
|
+
# shape == source.shape (rev3 requires explicit broadcast; wrap 1-D or
|
|
291
|
+
# band-shape weights via `.broadcast_to(*source.shape)` at the call site).
|
|
292
|
+
# Empty group cell → `0.0` (identity). Mask contract: cell contributes iff
|
|
293
|
+
# value AND weight are present.
|
|
294
|
+
# @param weights [CArray]
|
|
295
|
+
# @param axis [Integer]
|
|
296
|
+
# @return [CArray]
|
|
718
297
|
|
|
719
298
|
private
|
|
720
299
|
|
|
721
|
-
#
|
|
722
|
-
#
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
def grouped
|
|
726
|
-
@grouped || raise(ArgumentError, @no_flat)
|
|
300
|
+
# Both are built with the grouping plan, and only on the flat path; the
|
|
301
|
+
# per-fiber form has neither and must not build a segment's.
|
|
302
|
+
def perm
|
|
303
|
+
@perm
|
|
727
304
|
end
|
|
728
305
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
def counts
|
|
732
|
-
@elements || raise(ArgumentError, @no_flat)
|
|
306
|
+
def codes
|
|
307
|
+
@codes
|
|
733
308
|
end
|
|
734
309
|
|
|
735
310
|
# Axis-aware count of present cells. Counting how many cells fall in a
|
|
736
|
-
# group does not look at what is in them
|
|
737
|
-
#
|
|
738
|
-
#
|
|
739
|
-
# answer that never depended on the payload
|
|
740
|
-
|
|
311
|
+
# group does not look at what is in them. An integer or float payload goes
|
|
312
|
+
# through the fused moments kernel, which counts in the same pass that
|
|
313
|
+
# sums. Any other payload -- complex, boolean, object, a Face -- the kernel
|
|
314
|
+
# refuses, for an answer that never depended on the payload, so it is
|
|
315
|
+
# counted from the codes and the value's mask, one group at a time.
|
|
316
|
+
def axis_counts (axis, op)
|
|
741
317
|
h = @value
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
"group_by_category.count(axis: #{axis.inspect}): axis must be an " \
|
|
745
|
-
"Integer in [0, #{h.ndim}) for source h with shape #{h.shape}"
|
|
746
|
-
end
|
|
318
|
+
return axis_moments(axis, op)[:count] if h.integer? || h.float?
|
|
319
|
+
axis = checked_axis(axis, op)
|
|
747
320
|
full_c = resolve_axis_codes(@cat.codes, h.shape, axis)
|
|
748
321
|
band = h.shape.dup; band.delete_at(axis)
|
|
749
322
|
out = CArray.int64(*([@k] + band))
|
|
@@ -755,7 +328,8 @@ class CACategoricalIterator < CAIterator
|
|
|
755
328
|
belongs = belongs.strip_mask(false) if belongs.has_mask?
|
|
756
329
|
belongs = belongs & present if present
|
|
757
330
|
slot[0] = c
|
|
758
|
-
|
|
331
|
+
# a boolean sums to a count without widening the whole array first
|
|
332
|
+
out[*slot] = belongs.sum(axis: axis)
|
|
759
333
|
end
|
|
760
334
|
out
|
|
761
335
|
end
|
|
@@ -774,35 +348,79 @@ class CACategoricalIterator < CAIterator
|
|
|
774
348
|
# data can produce. The axis: path materialises nothing else, so holding
|
|
775
349
|
# this one thing was the odd choice; a caller who wants a fused kernel's
|
|
776
350
|
# four answers shares them by keeping the result.
|
|
777
|
-
def axis_moments (axis)
|
|
351
|
+
def axis_moments (axis, op)
|
|
352
|
+
return axis_moments_by_core(axis, op) unless MONOID_TYPES.include?(@value.data_type)
|
|
778
353
|
h = @value
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
"Integer in [0, #{h.ndim}) for source h with shape #{h.shape}"
|
|
783
|
-
end
|
|
354
|
+
axis = checked_axis(axis, op)
|
|
355
|
+
band = h.shape.dup; band.delete_at(axis)
|
|
356
|
+
return band_only_moments(h, axis, band, op) if band_only?(@cat.codes, h.shape, axis)
|
|
784
357
|
codes_h_shape = resolve_axis_codes(@cat.codes, h.shape, axis)
|
|
785
|
-
band = h.shape.dup; band.delete_at(axis)
|
|
786
358
|
out_shape = [@k] + band
|
|
787
359
|
counts = CArray.int64(*out_shape)
|
|
788
360
|
sums = CArray.float64(*out_shape)
|
|
789
|
-
mins = CArray.new(h.data_type, out_shape)
|
|
790
|
-
maxs = CArray.new(h.data_type, out_shape)
|
|
361
|
+
mins = CArray.new(h.data_type, out_shape, bytes: h.bytes)
|
|
362
|
+
maxs = CArray.new(h.data_type, out_shape, bytes: h.bytes)
|
|
791
363
|
h.__send__(:__fiber_scatter_moments__, codes_h_shape, axis, @k,
|
|
792
364
|
counts, sums, mins, maxs)
|
|
793
365
|
{count: counts, sum: sums, min: mins, max: maxs}
|
|
794
366
|
end
|
|
795
367
|
|
|
368
|
+
# The classifier is band-only, as resolve_axis_codes decides it (case A
|
|
369
|
+
# wins when both shapes fit).
|
|
370
|
+
def band_only? (codes, h_shape, axis)
|
|
371
|
+
band = h_shape.dup; band.delete_at(axis)
|
|
372
|
+
!band.empty? && codes.shape == band && codes.shape != [h_shape[axis]]
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Moments for a band-only classifier. Every cell of a fiber has the same
|
|
376
|
+
# code, so a fiber falls into one group whole: the core's own reductions
|
|
377
|
+
# along the axis answer for it, and each answer is placed in its code's
|
|
378
|
+
# row. A fiber whose code is masked belongs to no group. A group no fiber
|
|
379
|
+
# falls in, or one whose fibers are all masked, has count 0, sum 0.0 and
|
|
380
|
+
# masked min and max, as the fused kernel leaves it. Only the answers the
|
|
381
|
+
# caller reads are computed; each is a pass of its own here, where the
|
|
382
|
+
# kernel had all four from one.
|
|
383
|
+
BAND_ONLY_KEYS = { sum: [:sum], mean: [:sum, :count], min: [:min],
|
|
384
|
+
max: [:max], minmax: [:min, :max] }.freeze
|
|
385
|
+
private_constant :BAND_ONLY_KEYS
|
|
386
|
+
|
|
387
|
+
def band_only_moments (h, axis, band, op)
|
|
388
|
+
nb = band.inject(1, :*)
|
|
389
|
+
shape = [@k] + band
|
|
390
|
+
codes = @cat.codes
|
|
391
|
+
valid = codes.lt(@k).flatten
|
|
392
|
+
valid = valid.strip_mask(false) if valid.has_mask?
|
|
393
|
+
addr = (codes.value.int64.flatten * nb + CArray.int64(nb).seq)[valid]
|
|
394
|
+
out = {}
|
|
395
|
+
BAND_ONLY_KEYS.fetch(op, [:count]).each do |key|
|
|
396
|
+
res = case key
|
|
397
|
+
when :count then CArray.int64(*shape)
|
|
398
|
+
when :sum then CArray.float64(*shape)
|
|
399
|
+
else CArray.new(h.data_type, shape, bytes: h.bytes).tap { |a| a[] = UNDEF }
|
|
400
|
+
end
|
|
401
|
+
per_fiber = case key
|
|
402
|
+
when :count then h.count_not_masked(axis: axis)
|
|
403
|
+
when :sum then h.sum(axis: axis)
|
|
404
|
+
when :min then h.min(axis: axis)
|
|
405
|
+
when :max then h.max(axis: axis)
|
|
406
|
+
end
|
|
407
|
+
res.reshape(@k * nb)[addr] = per_fiber.flatten[valid]
|
|
408
|
+
out[key] = res
|
|
409
|
+
end
|
|
410
|
+
out
|
|
411
|
+
end
|
|
412
|
+
|
|
796
413
|
# Axis-aware sum: the moments sum is already the core fold in the core's own
|
|
797
414
|
# type, so it is handed back as is (an empty group cell carries identity 0.0).
|
|
798
415
|
def axis_sum (axis)
|
|
799
|
-
axis_moments(axis)[:sum]
|
|
416
|
+
axis_moments(axis, :sum)[:sum]
|
|
800
417
|
end
|
|
801
418
|
|
|
802
419
|
# Axis-aware mean: sums / counts (float64); empty group cells (count=0) MASKED.
|
|
803
420
|
# Matches flat #mean per fiber.
|
|
804
421
|
def axis_mean (axis)
|
|
805
|
-
|
|
422
|
+
return axis_by_core(axis, :mean) unless MONOID_TYPES.include?(@value.data_type)
|
|
423
|
+
m = axis_moments(axis, :mean)
|
|
806
424
|
cnt = m[:count]
|
|
807
425
|
out = m[:sum] / cnt.float64 # count 0 -> NaN
|
|
808
426
|
out[cnt.eq(0)] = UNDEF # empty / all-masked -> MASKED
|
|
@@ -821,13 +439,9 @@ class CACategoricalIterator < CAIterator
|
|
|
821
439
|
# Cost: K axis-reductions over an h-shaped local (most cells masked away for
|
|
822
440
|
# each c) — bounded by K, typically small. A fused per-fiber variance
|
|
823
441
|
# kernel is a natural follow-on if bench demands it.
|
|
824
|
-
def axis_by_masked_copy (axis, op, out_data_type =
|
|
442
|
+
def axis_by_masked_copy (axis, op, out_data_type = core_reduce_type(op))
|
|
825
443
|
h = @value
|
|
826
|
-
|
|
827
|
-
raise ArgumentError,
|
|
828
|
-
"group_by_category.#{op}(axis: #{axis.inspect}): axis must be an " \
|
|
829
|
-
"Integer in [0, #{h.ndim}) for source h with shape #{h.shape}"
|
|
830
|
-
end
|
|
444
|
+
axis = checked_axis(axis, op)
|
|
831
445
|
full_c = resolve_axis_codes(@cat.codes, h.shape, axis)
|
|
832
446
|
band = h.shape.dup; band.delete_at(axis)
|
|
833
447
|
out = CArray.new(out_data_type, [@k] + band)
|
|
@@ -842,27 +456,128 @@ class CACategoricalIterator < CAIterator
|
|
|
842
456
|
exclude = in_c.not
|
|
843
457
|
exclude = exclude | codes_bad if codes_bad
|
|
844
458
|
h_local[exclude] = UNDEF
|
|
845
|
-
slice = h_local.__send__(op, axis: axis) #
|
|
459
|
+
slice = h_local.__send__(op, axis: axis) # band shape, mask carries n<contract
|
|
846
460
|
slot_idx[0] = c
|
|
847
461
|
out[*slot_idx] = slice
|
|
848
462
|
end
|
|
849
463
|
out
|
|
850
464
|
end
|
|
851
465
|
|
|
466
|
+
# The axis: reductions for a value the fused kernels cannot read (boolean,
|
|
467
|
+
# object, a Face): each group's cells are reduced by the core's own
|
|
468
|
+
# reduction along the axis and the answers stacked, so the data type, the
|
|
469
|
+
# refusals and the empty-group answer are the core's -- a time column's mean
|
|
470
|
+
# is a time, its sum is refused, an empty group's sum is the identity.
|
|
471
|
+
# Classified along the axis (case A), a group is its positions on the axis,
|
|
472
|
+
# taken by index; otherwise its cells are kept by masking the rest of a copy.
|
|
473
|
+
# A read-only Face's answers (a const string's records index a pool of
|
|
474
|
+
# their own) cannot be put together, so they are collected as the surface
|
|
475
|
+
# values they are, as the flat form does.
|
|
476
|
+
def axis_by_core (axis, op, *args)
|
|
477
|
+
h = @value
|
|
478
|
+
axis = checked_axis(axis, op)
|
|
479
|
+
groups = (0...[@k, 1].max).map { |c| group_cells(h, axis, c, @k.zero?) }
|
|
480
|
+
answers = groups.map { |g| g.__send__(op, *args, axis: axis) }
|
|
481
|
+
if answers.first.face? && answers.first.read_only?
|
|
482
|
+
answers = answers.map { |a| CArray.object(*a.shape) { a.to_a } }
|
|
483
|
+
end
|
|
484
|
+
out = CArray.stack(answers, axis: 0).copy
|
|
485
|
+
@k.zero? ? out[0...0, *([nil] * (out.ndim - 1))].copy : out
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def axis_moments_by_core (axis, op)
|
|
489
|
+
BAND_ONLY_KEYS.fetch(op, [:count]).to_h do |key|
|
|
490
|
+
[key, axis_by_core(axis, key == :count ? :count_not_masked : key)]
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def group_cells (h, axis, c, none)
|
|
495
|
+
codes = @cat.codes
|
|
496
|
+
if codes.shape == [h.shape[axis]]
|
|
497
|
+
sel = none ? CArray.boolean(codes.elements) { false } : codes.eq(c)
|
|
498
|
+
sel = sel.strip_mask(false) if sel.has_mask?
|
|
499
|
+
idx = [nil] * h.ndim
|
|
500
|
+
idx[axis] = sel.where
|
|
501
|
+
h[*idx]
|
|
502
|
+
else
|
|
503
|
+
full_c = resolve_axis_codes(codes, h.shape, axis)
|
|
504
|
+
exclude = none ? CArray.boolean(*h.shape) { true } : full_c.eq(c).not
|
|
505
|
+
exclude = exclude | full_c.is_masked if full_c.has_mask?
|
|
506
|
+
local = h.copy
|
|
507
|
+
local[exclude] = UNDEF
|
|
508
|
+
local
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
private :axis_by_core, :axis_moments_by_core, :group_cells
|
|
512
|
+
|
|
513
|
+
# A running statistic along an axis, for a categorical that classifies the
|
|
514
|
+
# positions along it (case A): each cell holds its group's running value up
|
|
515
|
+
# to it along the axis, per fiber. The answer is the core's, as the flat
|
|
516
|
+
# form's is; the kernel runs when it answers in the core's data type, and
|
|
517
|
+
# otherwise each group's positions are scanned by the core along the axis.
|
|
518
|
+
# Read when called, as the other axis: members are.
|
|
519
|
+
def axis_scan (op, axis)
|
|
520
|
+
h = @value
|
|
521
|
+
axis = checked_axis(axis, op)
|
|
522
|
+
classified_along!(op, axis)
|
|
523
|
+
answer = core_scan_answer(op)
|
|
524
|
+
if kernel_scans?(op, answer)
|
|
525
|
+
bundle = @k > 0 ? [@cat.codes, @k, [axis]]
|
|
526
|
+
: [CArray.int64(h.shape[axis]).fill(-1), 1, [axis]]
|
|
527
|
+
return h.__axis_group_scan__([axis], [bundle], op)
|
|
528
|
+
end
|
|
529
|
+
each_group_along(axis, ->(shape) { blank_like(answer, shape) }) do |g|
|
|
530
|
+
g.public_send(op, axis: axis)
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# #map along an axis (case A): the block receives each group's positions
|
|
535
|
+
# along the axis, every other axis kept, and its answer goes back to them.
|
|
536
|
+
def axis_map (axis, data_type, &block)
|
|
537
|
+
axis = checked_axis(axis, :map)
|
|
538
|
+
classified_along!(:map, axis)
|
|
539
|
+
each_group_along(axis, ->(shape) { blank_output(shape, data_type) }, &block)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# Yields each group's positions along +axis+ as a view of the value and
|
|
543
|
+
# writes the block's answer back to them; a position in no group is UNDEF.
|
|
544
|
+
def each_group_along (axis, blank)
|
|
545
|
+
h = @value
|
|
546
|
+
out = blank.call(h.shape)
|
|
547
|
+
out[] = UNDEF
|
|
548
|
+
@k.times do |c|
|
|
549
|
+
sel = @cat.codes.eq(c)
|
|
550
|
+
sel = sel.strip_mask(false) if sel.has_mask?
|
|
551
|
+
next if sel.count(true).zero?
|
|
552
|
+
idx = [nil] * h.ndim
|
|
553
|
+
idx[axis] = sel.where
|
|
554
|
+
out[*idx] = surface_for(out, yield(h[*idx]))
|
|
555
|
+
end
|
|
556
|
+
out
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# A running value or a transform along an axis follows the positions along
|
|
560
|
+
# it, so the categorical has to classify those positions.
|
|
561
|
+
def classified_along! (op, axis)
|
|
562
|
+
n = @value.shape[axis]
|
|
563
|
+
return if @cat.codes.shape == [n]
|
|
564
|
+
raise ArgumentError,
|
|
565
|
+
"#{op}(axis: #{axis}): needs a categorical that classifies the " \
|
|
566
|
+
"positions along axis #{axis} (shape [#{n}]); got shape " \
|
|
567
|
+
"#{@cat.codes.shape.inspect}"
|
|
568
|
+
end
|
|
569
|
+
private :axis_scan, :axis_map, :each_group_along, :classified_along!
|
|
570
|
+
|
|
852
571
|
# Axis-aware wsum + wmean fused (single kernel call, both outputs). Returns
|
|
853
572
|
# [wsum_ca, wmean_ca]. Weights must match source shape exactly (explicit
|
|
854
573
|
# broadcast on the call site for 1-D or band-shape weights). A cell
|
|
855
574
|
# contributes iff its value AND its weight are present.
|
|
856
|
-
def axis_wsum_wmean (weights, axis)
|
|
575
|
+
def axis_wsum_wmean (weights, axis, op)
|
|
857
576
|
h = @value
|
|
858
|
-
|
|
859
|
-
raise ArgumentError,
|
|
860
|
-
"group_by_category.wsum/wmean(axis: #{axis.inspect}): axis must " \
|
|
861
|
-
"be an Integer in [0, #{h.ndim}) for source h with shape #{h.shape}"
|
|
862
|
-
end
|
|
577
|
+
axis = checked_axis(axis, op)
|
|
863
578
|
unless weights.is_a?(CArray) && weights.shape == h.shape
|
|
864
579
|
raise ArgumentError,
|
|
865
|
-
"
|
|
580
|
+
"#{op}(axis: #{axis}): weights.shape " \
|
|
866
581
|
"#{weights.respond_to?(:shape) ? weights.shape.inspect : weights.class} " \
|
|
867
582
|
"must equal source.shape #{h.shape.inspect}. Wrap 1-D / band-shape " \
|
|
868
583
|
"weights via `.broadcast_to(*source.shape)` before passing."
|
|
@@ -880,12 +595,9 @@ class CACategoricalIterator < CAIterator
|
|
|
880
595
|
# Axis-aware prod: dedicated kernel (identity 1.0, separate from moments to
|
|
881
596
|
# avoid conflating with sum's zero-identity memset).
|
|
882
597
|
def axis_prod (axis)
|
|
598
|
+
return axis_by_core(axis, :prod) unless MONOID_TYPES.include?(@value.data_type)
|
|
883
599
|
h = @value
|
|
884
|
-
|
|
885
|
-
raise ArgumentError,
|
|
886
|
-
"group_by_category.prod(axis: #{axis.inspect}): axis must be an " \
|
|
887
|
-
"Integer in [0, #{h.ndim}) for source h with shape #{h.shape}"
|
|
888
|
-
end
|
|
600
|
+
axis = checked_axis(axis, :prod)
|
|
889
601
|
codes_h_shape = resolve_axis_codes(@cat.codes, h.shape, axis)
|
|
890
602
|
band = h.shape.dup; band.delete_at(axis)
|
|
891
603
|
out = CArray.float64(*([@k] + band))
|
|
@@ -896,6 +608,12 @@ class CACategoricalIterator < CAIterator
|
|
|
896
608
|
# Broadcast `codes` to `h_shape` per PROPOSAL §2.2 3-case positional rule.
|
|
897
609
|
# Returns a broadcast view of codes at h_shape (or codes itself for case B).
|
|
898
610
|
# Raises ArgumentError with a message that enumerates all 3 accepted shapes.
|
|
611
|
+
# The axis of an axis: reduction, normalised against the source: a
|
|
612
|
+
# negative axis counts from the end, as everywhere else.
|
|
613
|
+
def checked_axis (axis, op)
|
|
614
|
+
@value.normalize_axis(axis, op.to_s)
|
|
615
|
+
end
|
|
616
|
+
|
|
899
617
|
def resolve_axis_codes (codes, h_shape, axis)
|
|
900
618
|
ndim = h_shape.size
|
|
901
619
|
band = h_shape.dup; band.delete_at(axis)
|
|
@@ -932,225 +650,10 @@ class CACategoricalIterator < CAIterator
|
|
|
932
650
|
"band-only cat.shape=#{band.inspect}."
|
|
933
651
|
end
|
|
934
652
|
|
|
935
|
-
#
|
|
936
|
-
#
|
|
937
|
-
def
|
|
938
|
-
|
|
939
|
-
"CACategoricalIterator##{op}(axis:) is not implemented — order " \
|
|
940
|
-
"statistics are available without axis:."
|
|
941
|
-
end
|
|
942
|
-
|
|
943
|
-
# Drive a segment scan through the axis-group scan kernel: the whole value as
|
|
944
|
-
# one grouped axis, the flat codes as the single bundle. The kernel emits in
|
|
945
|
-
# source order, so the flat result reshapes straight back to the source shape.
|
|
946
|
-
def scan (op)
|
|
947
|
-
scan_source.reshape(@value.elements)
|
|
948
|
-
.__axis_group_scan__([0], [[@codes, @k, [0]]], op)
|
|
949
|
-
.reshape(*@src_shape)
|
|
950
|
-
end
|
|
951
|
-
|
|
952
|
-
# The values as they were when the iterator was built, in source order.
|
|
953
|
-
# Every no-axis reduction works from the category-major copy taken then; the
|
|
954
|
-
# scans read @value, so a write through the source between two calls used to
|
|
955
|
-
# be visible to a cumsum and not to a sum, off one iterator.
|
|
956
|
-
#
|
|
957
|
-
# Rebuilt rather than copied a second time: @perm is exactly the classified
|
|
958
|
-
# cells and @grouped holds their values and their masks, which is everything
|
|
959
|
-
# a scan reads -- a cell classified by nothing is skipped on its code, before
|
|
960
|
-
# its value is looked at. So this costs nothing until a scan asks for it, and
|
|
961
|
-
# nothing at all for an iterator that never scans.
|
|
962
|
-
def scan_source
|
|
963
|
-
@scan_source ||=
|
|
964
|
-
begin
|
|
965
|
-
snap = @value.template
|
|
966
|
-
snap.reshape(snap.elements)[perm] = grouped
|
|
967
|
-
snap
|
|
968
|
-
end
|
|
969
|
-
end
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
# Permutation: perm[slot] = the source index whose value sits at that grouped
|
|
973
|
-
# slot. This is the valid prefix of the categorical's cached sort_addr, sliced
|
|
974
|
-
# at construction (the same counting sort that lays out @grouped), so #map /
|
|
975
|
-
# #sort_addr / the *_addr reductions read it for free.
|
|
976
|
-
def perm
|
|
977
|
-
@perm
|
|
978
|
-
end
|
|
979
|
-
|
|
980
|
-
# Lay a per-cell weight array out in category-contiguous order (same layout as
|
|
981
|
-
# @grouped), so wsum / wmean can pair each group's values with its weights.
|
|
982
|
-
# Weights are coerced to float64; the same counting-sort scatter propagates
|
|
983
|
-
# the weight mask and skips excluded cells, so wg lines up with @grouped.
|
|
984
|
-
def scatter_weights (weights)
|
|
985
|
-
unless weights.elements == @codes.elements
|
|
986
|
-
raise ArgumentError,
|
|
987
|
-
"wsum/wmean: weights.elements (#{weights.elements}) != " \
|
|
988
|
-
"value.elements (#{@codes.elements})"
|
|
989
|
-
end
|
|
990
|
-
wf = weights.float64
|
|
991
|
-
wg = CArray.float64(grouped.elements)
|
|
992
|
-
@codes.send(:__categorical_scatter__, wf.reshape(wf.elements),
|
|
993
|
-
@offsets.copy, wg, @k)
|
|
994
|
-
wg
|
|
995
|
-
end
|
|
996
|
-
|
|
997
|
-
# Map a per-category group-local index to the flat source address via the
|
|
998
|
-
# permutation (grouped slot -> source index). The min/max sits at grouped slot
|
|
999
|
-
# offsets[c] + local_index[c]; perm carries it back to the source. Empty
|
|
1000
|
-
# categories (masked local index) stay masked.
|
|
1001
|
-
def group_addr (local_index)
|
|
1002
|
-
out = CArray.int64(@k)
|
|
1003
|
-
@k.times do |c|
|
|
1004
|
-
out[c] = local_index.is_masked[c] ? UNDEF
|
|
1005
|
-
: perm[@offsets[c] + local_index[c]]
|
|
1006
|
-
end
|
|
1007
|
-
out
|
|
1008
|
-
end
|
|
1009
|
-
|
|
1010
|
-
# Fused per-segment weighted sum + weighted mean (one C pass over the grouped
|
|
1011
|
-
# copy, weights in group order). Returns [wsum, wmean]; wmean is masked where a
|
|
1012
|
-
# segment has no present (value AND weight) pair. Numeric value data types only.
|
|
1013
|
-
def kernel_weighted (wg)
|
|
1014
|
-
ws = CArray.float64(@k)
|
|
1015
|
-
wm = CArray.float64(@k)
|
|
1016
|
-
grouped.send(:__reduceat_wsum_wmean__, @offsets, wg, ws, wm)
|
|
1017
|
-
[ws, wm]
|
|
1018
|
-
end
|
|
1019
|
-
|
|
1020
|
-
# Per-group weighted fallback for non-numeric value data types (complex): delegate
|
|
1021
|
-
# each group to CArray#wsum / #wmean. Empty segments take the given identity.
|
|
1022
|
-
def fold_weighted (wg, empty)
|
|
1023
|
-
out = CArray.float64(@k)
|
|
1024
|
-
@k.times do |c|
|
|
1025
|
-
lo = @offsets[c]
|
|
1026
|
-
hi = (c + 1 < @k) ? @offsets[c + 1] : grouped.elements
|
|
1027
|
-
out[c] = hi > lo ? yield(grouped[lo...hi], wg[lo...hi]) : empty
|
|
1028
|
-
end
|
|
1029
|
-
out
|
|
1030
|
-
end
|
|
1031
|
-
|
|
1032
|
-
# The members of category `c` as a CArray slice of the grouped copy. An empty
|
|
1033
|
-
# category (zero-width segment) yields the shared empty array — a zero-length
|
|
1034
|
-
# slice cannot be taken directly, and an empty array carries the same reduction
|
|
1035
|
-
# contract we want (identity for sum, UNDEF for ratios).
|
|
1036
|
-
def group_slice (c)
|
|
1037
|
-
lo = @offsets[c]
|
|
1038
|
-
hi = (c + 1 < @k) ? @offsets[c + 1] : grouped.elements
|
|
1039
|
-
hi > lo ? grouped[lo...hi] : @empty
|
|
1040
|
-
end
|
|
1041
|
-
|
|
1042
|
-
# Single-pass reduceat moments (count / sum / min / max per category), computed
|
|
1043
|
-
# once over the grouped copy and cached — the whole point of the eager copy is
|
|
1044
|
-
# that one scatter is followed by cheap single-pass reductions with no
|
|
1045
|
-
# per-segment views. Nil for a non-numeric value data type (complex / object /
|
|
1046
|
-
# bool), where the monoid reductions fall back to per_category.
|
|
1047
|
-
# numeric value data types the C moments kernel handles (int8..float64); bool /
|
|
1048
|
-
# complex / object fall back to per_category.
|
|
1049
|
-
MONOID_TYPES = %i[int8 uint8 int16 uint16 int32 uint32
|
|
1050
|
-
int64 uint64 float32 float64].freeze
|
|
1051
|
-
|
|
1052
|
-
def moments
|
|
1053
|
-
return @moments if defined?(@moments)
|
|
1054
|
-
@moments =
|
|
1055
|
-
if MONOID_TYPES.include?(grouped.data_type)
|
|
1056
|
-
dt = grouped.data_type
|
|
1057
|
-
counts = CArray.int64(@k)
|
|
1058
|
-
sums = CArray.float64(@k)
|
|
1059
|
-
mins = CArray.new(dt, [@k])
|
|
1060
|
-
maxs = CArray.new(dt, [@k])
|
|
1061
|
-
grouped.send(:__reduceat_moments__, @offsets, counts, sums, mins, maxs)
|
|
1062
|
-
{ count: counts, sum: sums, min: mins, max: maxs }
|
|
1063
|
-
end
|
|
1064
|
-
end
|
|
1065
|
-
|
|
1066
|
-
# Single-pass fused group-local argmin / argmax (min_index / max_index),
|
|
1067
|
-
# cached. Nil for a non-numeric value data type (fall back to per_category).
|
|
1068
|
-
def arg_minmax
|
|
1069
|
-
return @arg_minmax if defined?(@arg_minmax)
|
|
1070
|
-
@arg_minmax =
|
|
1071
|
-
if MONOID_TYPES.include?(grouped.data_type)
|
|
1072
|
-
mn = CArray.int64(@k)
|
|
1073
|
-
mx = CArray.int64(@k)
|
|
1074
|
-
grouped.send(:__reduceat_argminmax__, @offsets, mn, mx)
|
|
1075
|
-
{ min: mn, max: mx }
|
|
1076
|
-
end
|
|
1077
|
-
end
|
|
1078
|
-
|
|
1079
|
-
# Single-pass fused per-category boolean all / any, cached. Nil unless the
|
|
1080
|
-
# value data type is boolean (fall back to per_category, which raises like
|
|
1081
|
-
# CArray#all on a non-boolean).
|
|
1082
|
-
def all_any
|
|
1083
|
-
return @all_any if defined?(@all_any)
|
|
1084
|
-
@all_any =
|
|
1085
|
-
if grouped.data_type == CA_BOOLEAN
|
|
1086
|
-
a = CArray.boolean(@k)
|
|
1087
|
-
o = CArray.boolean(@k)
|
|
1088
|
-
grouped.send(:__reduceat_all_any__, @offsets, a, o)
|
|
1089
|
-
{ all: a, any: o }
|
|
1090
|
-
end
|
|
1091
|
-
end
|
|
1092
|
-
|
|
1093
|
-
# Build a length-k typed output by folding each category's members with the
|
|
1094
|
-
# given reduction block. Fallback path (order statistics, and monoids on a
|
|
1095
|
-
# non-numeric value data type): each group is delegated to the same CArray
|
|
1096
|
-
# reduction, so the per-group result matches `CArray#<reduction>` over that
|
|
1097
|
-
# group's members — the mask carries the "insufficient present data" contract
|
|
1098
|
-
# for free (an all-masked group reduces like an empty one; identity-bearing
|
|
1099
|
-
# reductions return their identity, ratios return UNDEF; see ext ERI).
|
|
1100
|
-
# The data type the core reduction `op` promotes this value to. Asked of the
|
|
1101
|
-
# core itself -- a one-cell reduction of the value's type -- rather than
|
|
1102
|
-
# restated here, so a per-category answer cannot drift from `CArray#<op>`
|
|
1103
|
-
# (`sum` on an integer promotes, `accumulate` stays, `min` / `max` keep the
|
|
1104
|
-
# type but a boolean widens, `prod` on an object stays an object). A payload
|
|
1105
|
-
# the core refuses to fold this way raises here, with the core's own error.
|
|
1106
|
-
# Probes the core with a one-cell array of the value's data type and takes
|
|
1107
|
-
# the answer's. Asks @value rather than the grouped copy, which has the same
|
|
1108
|
-
# data type but does not exist on an iterator that answers only the
|
|
1109
|
-
# per-fiber form -- and `accumulate(axis:)`, the one axis: member that needs
|
|
1110
|
-
# this, is exactly the case that would have found it missing.
|
|
1111
|
-
def core_reduce_type (op, *args)
|
|
1112
|
-
(@core_reduce_type ||= {})[[op, args]] ||=
|
|
1113
|
-
@value.face? && @value.elements.zero? ?
|
|
1114
|
-
CA_OBJECT : core_probe.public_send(op, *args, axis: 1).data_type
|
|
1115
|
-
end
|
|
1116
|
-
|
|
1117
|
-
# A one-cell array of the same kind as the values, so the core answers about
|
|
1118
|
-
# the same thing the group slices will hand back. For a Face that is a view
|
|
1119
|
-
# of the values themselves rather than a blank: a Face cannot be allocated
|
|
1120
|
-
# from its surface data type alone, and a blank storage array is not a valid
|
|
1121
|
-
# Face for every one of them -- a const string's record indexes a shared
|
|
1122
|
-
# pool, so a zeroed record points nowhere.
|
|
1123
|
-
def core_probe
|
|
1124
|
-
return CArray.new(@value.data_type, [1, 1]) unless @value.face?
|
|
1125
|
-
@value.reshape(@value.elements)[[0]].reshape(1, 1)
|
|
1126
|
-
end
|
|
1127
|
-
|
|
1128
|
-
# Whether an output can be built by lifting one. A Face is filled by writing
|
|
1129
|
-
# surface values into storage, so this needs a Face that can be written
|
|
1130
|
-
# into; a read-only one -- a const string's records index a shared pool, a
|
|
1131
|
-
# categorical's codes index a vocabulary -- has no blank form to fill.
|
|
1132
|
-
def face_output?
|
|
1133
|
-
@value.face? && ! @value.read_only?
|
|
1134
|
-
end
|
|
1135
|
-
|
|
1136
|
-
def per_category (data_type)
|
|
1137
|
-
out = if ! @value.face? || data_type != @value.data_type
|
|
1138
|
-
CArray.new(data_type, [@k])
|
|
1139
|
-
elsif face_output?
|
|
1140
|
-
# the core answered in the values' own Face, so the output is one
|
|
1141
|
-
# too: CATime#min hands back a CATime::Element, which only a
|
|
1142
|
-
# CATime has anywhere to put
|
|
1143
|
-
CArray.new(@value.parent.data_type, [@k],
|
|
1144
|
-
bytes: @value.parent.bytes).face_lift(@value)
|
|
1145
|
-
else
|
|
1146
|
-
# a read-only Face cannot be filled, so its answers are collected
|
|
1147
|
-
# as the surface objects they already are
|
|
1148
|
-
CArray.new(CA_OBJECT, [@k])
|
|
1149
|
-
end
|
|
1150
|
-
# A member the core refuses for this Face still refuses: the group slice is
|
|
1151
|
-
# the Face, so the refusal comes from there, in the core's own words.
|
|
1152
|
-
@k.times { |c| out[c] = yield(group_slice(c)) }
|
|
1153
|
-
out
|
|
653
|
+
# median / percentile along an axis: the core's own order statistic of each
|
|
654
|
+
# group's cells, stacked -- the axis_by_core path, one core call per group.
|
|
655
|
+
def axis_order_stat (axis, op, *args)
|
|
656
|
+
axis_by_core(axis, op, *args)
|
|
1154
657
|
end
|
|
1155
658
|
end
|
|
1156
659
|
|
|
@@ -1161,8 +664,14 @@ class CArray
|
|
|
1161
664
|
# per category of `cat`. Requires `self.elements == cat.elements`.
|
|
1162
665
|
# @param cat [CACategorical] the classifier.
|
|
1163
666
|
# @return [CACategoricalIterator]
|
|
667
|
+
# @raise [TypeError] when `cat` is not a CACategorical.
|
|
1164
668
|
# @raise [ArgumentError] when element counts differ.
|
|
1165
669
|
def group_by_category (cat)
|
|
670
|
+
unless cat.is_a?(CACategorical)
|
|
671
|
+
raise TypeError,
|
|
672
|
+
"group_by_category: expects a CACategorical (got #{cat.class}); " \
|
|
673
|
+
"classify the values first with .categorize"
|
|
674
|
+
end
|
|
1166
675
|
CACategoricalIterator.new(self, cat)
|
|
1167
676
|
end
|
|
1168
677
|
|
|
@@ -1187,18 +696,16 @@ class CArray
|
|
|
1187
696
|
# `grp.labels` is `[0, 1, ...]` in position order.
|
|
1188
697
|
#
|
|
1189
698
|
# @return [CACategoricalIterator] one category per run, in order.
|
|
1190
|
-
# @raise [
|
|
699
|
+
# @raise [ArgumentError] when `self` is not 1-D.
|
|
1191
700
|
def group_by_run
|
|
1192
|
-
raise "group_by_run: 1-D only (got #{ndim}-D)" unless ndim == 1
|
|
701
|
+
raise ArgumentError, "group_by_run: 1-D only (got #{ndim}-D)" unless ndim == 1
|
|
1193
702
|
if elements == 0
|
|
1194
703
|
code = CArray.int64(0)
|
|
1195
704
|
else
|
|
1196
705
|
present = is_not_masked
|
|
1197
706
|
edge = present & present.shift(1).not # rising edge = run start
|
|
1198
|
-
#
|
|
1199
|
-
|
|
1200
|
-
# running count never overflows int8.
|
|
1201
|
-
code = edge.refer(:int8).cumsum.int64 - 1 # 0-based run index per cell
|
|
707
|
+
# a boolean cumsum counts in uint64, exactly
|
|
708
|
+
code = edge.cumsum.int64 - 1 # 0-based run index per cell
|
|
1202
709
|
code[present.not] = UNDEF # masked cells join no run
|
|
1203
710
|
end
|
|
1204
711
|
# categorize turns the dense run indices into the run categories: it derives
|