carray 3.0.2 → 3.0.4
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 +568 -0
- data/README.md +6 -6
- data/carray.gemspec +1 -1
- data/ext/ca_axis_descriptor.h +27 -24
- data/ext/ca_axis_dispatch.c +220 -0
- data/ext/ca_axis_group.c +201 -123
- data/ext/ca_binop_dispatch.c +46 -52
- data/ext/ca_binop_dispatch.h +1 -1
- data/ext/ca_categorical_iterator.c +249 -183
- data/ext/ca_compare.c +7 -12
- data/ext/ca_for_buffer.h +21 -68
- data/ext/ca_group_iter.c +38 -23
- data/ext/ca_kernel_iterator.c +1765 -2076
- data/ext/ca_kernel_iterator.h +584 -566
- data/ext/ca_monop_dispatch.c +86 -63
- data/ext/ca_monop_dispatch.h +5 -0
- data/ext/ca_obj_array.c +223 -56
- data/ext/ca_obj_bincmp.c +172 -138
- data/ext/ca_obj_binop.c +116 -230
- data/ext/ca_obj_bitarray.c +40 -26
- data/ext/ca_obj_bitfield.c +100 -68
- data/ext/ca_obj_block.c +19 -9
- data/ext/ca_obj_byte_swap.c +45 -46
- data/ext/ca_obj_const_string.c +269 -44
- data/ext/ca_obj_face.c +612 -89
- data/ext/ca_obj_face.h +111 -47
- data/ext/ca_obj_fake.c +36 -79
- data/ext/ca_obj_farray.c +7 -3
- data/ext/ca_obj_field.c +33 -8
- data/ext/ca_obj_fixlen_string.c +23 -24
- data/ext/ca_obj_grid.c +541 -210
- data/ext/ca_obj_meld.c +331 -111
- data/ext/ca_obj_moncmp.c +80 -65
- data/ext/ca_obj_monop.c +177 -149
- data/ext/ca_obj_object.c +664 -248
- data/ext/ca_obj_record.c +63 -23
- data/ext/ca_obj_reduce.c +18 -16
- data/ext/ca_obj_refer.c +140 -58
- data/ext/ca_obj_remap.c +85 -24
- data/ext/ca_obj_repeat.c +24 -9
- data/ext/ca_obj_roll.c +121 -16
- data/ext/ca_obj_select.c +61 -58
- data/ext/ca_obj_select_axis.c +73 -107
- data/ext/ca_obj_shift.c +32 -102
- data/ext/ca_obj_stack.c +372 -122
- data/ext/ca_obj_stride.c +348 -140
- data/ext/ca_obj_string.c +88 -22
- data/ext/ca_obj_tile.c +38 -21
- data/ext/ca_obj_time.c +45 -31
- data/ext/ca_obj_timedelta.c +43 -29
- data/ext/ca_obj_transpose.c +17 -6
- data/ext/ca_obj_triop.c +125 -141
- data/ext/ca_obj_window.c +330 -44
- 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 +104 -81
- 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 +542 -0
- data/ext/carray.h +241 -92
- data/ext/carray_access.c +608 -276
- data/ext/carray_address_basis.c +494 -97
- data/ext/carray_attribute.c +129 -4
- data/ext/carray_bincount.c +39 -7
- data/ext/carray_broadcast.c +56 -19
- data/ext/carray_build_flags.h +1 -1
- data/ext/carray_call_cfunc.c +108 -28
- data/ext/carray_cast.c +1576 -223
- data/ext/carray_cast_func.rb +77 -24
- data/ext/carray_class.c +4 -3
- data/ext/carray_conversion.c +208 -75
- data/ext/carray_copy.c +82 -24
- data/ext/carray_core.c +646 -162
- data/ext/carray_count.c +171 -88
- data/ext/carray_element.c +94 -60
- data/ext/carray_factorize.c +670 -324
- data/ext/carray_generate.c +260 -83
- data/ext/carray_histogram.c +209 -106
- data/ext/carray_hold.c +14 -16
- data/ext/carray_index_classifier.c +422 -243
- data/ext/carray_internal.h +692 -3
- data/ext/carray_kernels_bincmp.c +7 -0
- data/ext/carray_kernels_binop.c +487 -120
- data/ext/carray_kernels_init.c +653 -50
- data/ext/carray_kernels_moncmp.c +15 -8
- data/ext/carray_kernels_monop.c +99 -43
- data/ext/carray_kernels_reduce_aggregate.c +3925 -6112
- data/ext/carray_kernels_reduce_boolean.c +112 -36
- data/ext/carray_kernels_reduce_cumulative.c +2127 -6903
- data/ext/carray_kernels_reduce_extreme.c +3032 -2892
- data/ext/carray_kernels_reduce_variance.c +439 -500
- data/ext/carray_kernels_scan.c +1097 -488
- data/ext/carray_kernels_search.c +3787 -2176
- data/ext/carray_kernels_sort.c +1200 -920
- data/ext/carray_kernels_triop.c +479 -7
- data/ext/carray_key_windows.c +216 -0
- data/ext/carray_lazy.c +967 -153
- data/ext/carray_loop.c +81 -65
- data/ext/carray_mask.c +910 -324
- data/ext/carray_math_kernel.h +1 -0
- data/ext/carray_median_percentile.c +299 -100
- data/ext/carray_memory_view.c +872 -758
- data/ext/carray_operator.c +1171 -364
- data/ext/carray_order.c +505 -133
- data/ext/carray_partition.c +112 -65
- data/ext/carray_random.c +327 -239
- data/ext/carray_round.c +273 -0
- data/ext/carray_scatter.c +346 -78
- data/ext/carray_segment.c +257 -0
- data/ext/carray_slab.c +249 -86
- data/ext/carray_sort.c +224 -227
- data/ext/carray_sort_kernel.c +8 -1
- data/ext/carray_struct.c +118 -39
- data/ext/carray_test.c +193 -69
- data/ext/carray_time_text.c +1182 -0
- data/ext/carray_undef.c +4 -0
- data/ext/carray_utils.c +579 -113
- data/ext/extconf.rb +27 -2
- data/ext/mk_call_cfunc.rb +19 -4
- data/ext/mk_pow5_table.rb +35 -0
- data/ext/mkkernel.rb +1202 -1014
- data/ext/ruby_carray.c +17 -5
- data/ext/version.h +4 -4
- data/ext/version.rb +4 -2
- data/lib/carray/attribute.rb +160 -17
- data/lib/carray/autoload_carray.rb +23 -27
- data/lib/carray/autoload_method_extension.rb +20 -5
- data/lib/carray/axis_group.rb +360 -63
- data/lib/carray/basics.rb +162 -28
- data/lib/carray/bincount_nd.rb +18 -2
- data/lib/carray/block_iterator.rb +231 -58
- data/lib/carray/boolean_reduce.rb +34 -42
- data/lib/carray/categorical.rb +242 -52
- data/lib/carray/categorical_iterator.rb +437 -827
- data/lib/carray/complex.rb +41 -34
- data/lib/carray/conditional.rb +150 -14
- data/lib/carray/const_string.rb +59 -4
- data/lib/carray/construct.rb +15 -11
- data/lib/carray/data_type_extension.rb +1 -0
- data/lib/carray/distance_limit.rb +77 -0
- data/lib/carray/fixlen_string.rb +7 -1
- data/lib/carray/frame/column_verbs.rb +70 -0
- data/lib/carray/frame/concat.rb +157 -6
- data/lib/carray/frame/convert.rb +82 -7
- data/lib/carray/frame/csv_parser.rb +688 -87
- data/lib/carray/frame/describe.rb +80 -0
- data/lib/carray/frame/frame.rb +369 -45
- data/lib/carray/frame/group.rb +631 -90
- data/lib/carray/frame/io.rb +506 -79
- data/lib/carray/frame/join.rb +411 -62
- data/lib/carray/frame/records.rb +116 -19
- data/lib/carray/frame/reshape.rb +455 -0
- data/lib/carray/frame/sort.rb +30 -18
- data/lib/carray/frame/verbs.rb +868 -61
- data/lib/carray/frame/windows.rb +117 -0
- data/lib/carray/frame.rb +4 -0
- data/lib/carray/fuse_source.rb +118 -2
- data/lib/carray/fusion.rb +257 -42
- data/lib/carray/histogram.rb +6 -1
- data/lib/carray/inspect.rb +54 -13
- data/lib/carray/iterator.rb +99 -9
- data/lib/carray/key_windows.rb +620 -0
- data/lib/carray/lazy.rb +216 -193
- data/lib/carray/mask_gap_fill.rb +112 -92
- data/lib/carray/math.rb +32 -19
- data/lib/carray/meld_reduce.rb +52 -21
- data/lib/carray/methods/align_addr.rb +8 -4
- data/lib/carray/methods/bin.rb +26 -18
- data/lib/carray/methods/bincount.rb +18 -2
- data/lib/carray/methods/bit_string.rb +15 -4
- data/lib/carray/methods/choose.rb +16 -3
- data/lib/carray/methods/composition.rb +18 -10
- data/lib/carray/methods/factorize.rb +2 -2
- data/lib/carray/methods/gather_nd.rb +25 -5
- data/lib/carray/methods/insert_block.rb +9 -14
- data/lib/carray/methods/is_between.rb +59 -0
- data/lib/carray/methods/is_in.rb +140 -10
- data/lib/carray/methods/join.rb +2 -7
- data/lib/carray/methods/lag.rb +49 -0
- data/lib/carray/methods/locate_addr.rb +182 -45
- data/lib/carray/methods/mode.rb +18 -1
- data/lib/carray/methods/nunique.rb +11 -6
- 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/methods/value_counts.rb +4 -11
- data/lib/carray/record_order.rb +208 -0
- data/lib/carray/rng.rb +3 -0
- data/lib/carray/runtime.rb +65 -17
- data/lib/carray/segment_iterator.rb +1221 -0
- data/lib/carray/serialize.rb +214 -30
- data/lib/carray/slab_iterator.rb +101 -27
- data/lib/carray/stack.rb +10 -4
- data/lib/carray/string.rb +105 -5
- data/lib/carray/string_operation_extension.rb +12 -4
- data/lib/carray/struct.rb +137 -58
- data/lib/carray/struct_builder.rb +124 -17
- data/lib/carray/time.rb +1222 -186
- data/lib/carray/window_iterator.rb +242 -81
- data/lib/carray.rb +9 -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 +5 -1
- data/yard-stubs/ca_obj_grid.rb +21 -10
- data/yard-stubs/ca_obj_refer.rb +5 -4
- data/yard-stubs/ca_obj_roll.rb +2 -3
- data/yard-stubs/ca_obj_shift.rb +5 -1
- data/yard-stubs/ca_obj_stride.rb +10 -7
- data/yard-stubs/ca_obj_tile.rb +1 -1
- data/yard-stubs/ca_obj_transpose.rb +1 -1
- data/yard-stubs/ca_obj_window.rb +9 -5
- data/yard-stubs/carray_access.rb +11 -5
- data/yard-stubs/carray_attribute.rb +0 -5
- data/yard-stubs/carray_broadcast.rb +1 -1
- data/yard-stubs/carray_cast.rb +88 -43
- data/yard-stubs/carray_conversion.rb +15 -2
- data/yard-stubs/carray_copy.rb +12 -5
- data/yard-stubs/carray_core.rb +3 -3
- data/yard-stubs/carray_count.rb +9 -0
- data/yard-stubs/carray_element.rb +10 -4
- data/yard-stubs/carray_generate.rb +21 -1
- data/yard-stubs/carray_lazy.rb +5 -1
- data/yard-stubs/carray_loop.rb +4 -1
- data/yard-stubs/carray_mask.rb +60 -12
- data/yard-stubs/carray_math.rb +52 -14
- data/yard-stubs/carray_median_percentile.rb +15 -8
- data/yard-stubs/carray_memory_view.rb +40 -23
- data/yard-stubs/carray_order.rb +61 -29
- data/yard-stubs/carray_random.rb +13 -4
- data/yard-stubs/carray_scatter.rb +21 -9
- data/yard-stubs/carray_segment.rb +72 -0
- data/yard-stubs/carray_slab.rb +10 -2
- data/yard-stubs/carray_sort.rb +49 -8
- data/yard-stubs/carray_test.rb +15 -3
- data/yard-stubs/carray_utils.rb +0 -26
- metadata +19 -5
- data/ext/ca_for_each_element.h +0 -239
- data/ext/carray_kernels_map.c +0 -3467
- data/lib/carray/arrow_tensor.rb +0 -401
- data/lib/carray/table.rb +0 -108
|
@@ -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,19 @@
|
|
|
60
60
|
# ----------------------------------------------------------------------------
|
|
61
61
|
|
|
62
62
|
require "carray"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
# are
|
|
72
|
-
#
|
|
73
|
-
|
|
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: the value is
|
|
67
|
+
# gathered through the categorical's cached grouping plan, so each category
|
|
68
|
+
# is one contiguous segment, and every reduction without `axis:` is the
|
|
69
|
+
# segment reduction over that copy (one value per category, in {#labels}
|
|
70
|
+
# order). With few categories and a numeric value, the count / sum / min /
|
|
71
|
+
# max are read from the values by code instead, and the copy is gathered
|
|
72
|
+
# only for the reductions that need it. This class adds the construction, the labels, and the per-fiber
|
|
73
|
+
# form (`axis:`), which classifies each fiber along an axis and keeps the
|
|
74
|
+
# other axes.
|
|
75
|
+
class CACategoricalIterator < CASegmentIterator
|
|
74
76
|
|
|
75
77
|
# value : the payload CArray to reduce, one cell per categorical cell.
|
|
76
78
|
# cat : the CACategorical carrying the classification.
|
|
@@ -105,10 +107,10 @@ class CACategoricalIterator < CAIterator
|
|
|
105
107
|
@elements = cat.category_sizes.int64
|
|
106
108
|
nvalid = counts.sum
|
|
107
109
|
@offsets = cat.reduceat_index # cached segment STARTS (int64[k])
|
|
108
|
-
|
|
109
|
-
#
|
|
110
|
-
# sort_addr
|
|
111
|
-
@
|
|
110
|
+
@bounds = CArray.segment_offsets(lengths: @elements) # every boundary (int64[k+1])
|
|
111
|
+
# The group-major source indices (#perm) are taken from the cached
|
|
112
|
+
# sort_addr when first asked for.
|
|
113
|
+
@nvalid = nvalid
|
|
112
114
|
@codes = cat.codes.reshape(cat.elements) # flat codes view (map re-walk / weights)
|
|
113
115
|
# Gather value into category-contiguous order via the cached permutation
|
|
114
116
|
# and materialise (the reduceat kernels read the grouped buffer's raw ptr,
|
|
@@ -117,9 +119,21 @@ class CACategoricalIterator < CAIterator
|
|
|
117
119
|
# rebuilt per column. With no classified cell (empty / all-excluded) there
|
|
118
120
|
# is nothing to gather — an empty index into an empty source is out of
|
|
119
121
|
# range — so build the empty grouped buffer directly.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
#
|
|
123
|
+
# With few categories, a numeric value is instead copied as it lies:
|
|
124
|
+
# the count / sum / min / max read it in one pass by code (scattering
|
|
125
|
+
# into one slot per category beats gathering every cell first while
|
|
126
|
+
# the slots stay in cache), and the grouped copy is gathered from it
|
|
127
|
+
# only when a reduction needs the categories contiguous. Either way
|
|
128
|
+
# the iterator holds the values as they were when it was built.
|
|
129
|
+
flat = value.reshape(value.elements)
|
|
130
|
+
if @k <= SCATTER_CATEGORIES_MAX && kernel_reads?(value.data_type)
|
|
131
|
+
@as_built = flat.copy
|
|
132
|
+
@empty = @as_built[0...0]
|
|
133
|
+
else
|
|
134
|
+
@grouped = nvalid > 0 ? flat[perm].copy : flat[0...0].copy
|
|
135
|
+
@empty = @grouped[0...0] # an empty category is the values' own kind
|
|
136
|
+
end
|
|
123
137
|
else
|
|
124
138
|
# Shape mismatch: only per-fiber axis: dispatch could still work. With a
|
|
125
139
|
# 1-D value there is no fiber structure to broadcast into, so a mismatch
|
|
@@ -147,19 +161,9 @@ class CACategoricalIterator < CAIterator
|
|
|
147
161
|
self
|
|
148
162
|
end
|
|
149
163
|
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
|
|
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
|
|
164
|
+
# The most categories for which the count / sum / min / max are read by
|
|
165
|
+
# scattering the values by code rather than through the grouped copy.
|
|
166
|
+
SCATTER_CATEGORIES_MAX = 4096
|
|
163
167
|
|
|
164
168
|
# @overload labels
|
|
165
169
|
# Returns the category vocabulary the results are aligned to.
|
|
@@ -174,21 +178,6 @@ class CACategoricalIterator < CAIterator
|
|
|
174
178
|
@k
|
|
175
179
|
end
|
|
176
180
|
|
|
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
181
|
# Group-vocabulary alias for {#elements}; reads naturally next to
|
|
193
182
|
# {#ngroups} and mirrors `CACategorical#category_sizes`.
|
|
194
183
|
alias group_sizes elements
|
|
@@ -207,543 +196,178 @@ class CACategoricalIterator < CAIterator
|
|
|
207
196
|
"#<#{self.class} ngroups=#{@k} labels=#{@labels.inspect} #{tail}>"
|
|
208
197
|
end
|
|
209
198
|
|
|
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.
|
|
199
|
+
# ---- the per-fiber form ---------------------------------------------------
|
|
565
200
|
#
|
|
566
|
-
#
|
|
567
|
-
#
|
|
568
|
-
#
|
|
569
|
-
#
|
|
570
|
-
#
|
|
571
|
-
# @
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
#
|
|
586
|
-
#
|
|
587
|
-
#
|
|
588
|
-
#
|
|
589
|
-
#
|
|
590
|
-
# @
|
|
591
|
-
#
|
|
592
|
-
#
|
|
593
|
-
#
|
|
594
|
-
#
|
|
595
|
-
#
|
|
596
|
-
#
|
|
597
|
-
#
|
|
598
|
-
#
|
|
599
|
-
#
|
|
600
|
-
#
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
201
|
+
# These reductions also take `axis:` on a categorical. Without it each is
|
|
202
|
+
# the CASegmentIterator reduction of the same name, over the category-sorted
|
|
203
|
+
# copy: one value per category, in {#labels} order.
|
|
204
|
+
#
|
|
205
|
+
# @!method accumulate(axis: nil)
|
|
206
|
+
# @overload accumulate
|
|
207
|
+
# The per-category form: {CASegmentIterator#accumulate}, one value per category.
|
|
208
|
+
# @overload accumulate(axis:)
|
|
209
|
+
# Per-fiber per-category in-type sums along `axis`. Output shape =
|
|
210
|
+
# `[K, ...source.shape without axis]`.
|
|
211
|
+
# @param axis [Integer] reduce axis of the source value.
|
|
212
|
+
# @return [CArray]
|
|
213
|
+
# @!method count(*args, axis: nil)
|
|
214
|
+
# @overload count
|
|
215
|
+
# The per-category form: {CASegmentIterator#count}, one value per category.
|
|
216
|
+
# @overload count(axis:)
|
|
217
|
+
# No-arg + axis: = per-fiber per-category count_not_masked (shape [K, ...band]).
|
|
218
|
+
# `count(v, axis:)` (value equality) and `count(UNDEF, axis:)` are not
|
|
219
|
+
# implemented; use them without `axis:`.
|
|
220
|
+
# @param axis [Integer]
|
|
221
|
+
# @return [CArray]
|
|
222
|
+
# @!method count_masked(axis: nil)
|
|
223
|
+
# @overload count_masked
|
|
224
|
+
# The per-category form: {CASegmentIterator#count_masked}, one value per category.
|
|
225
|
+
# @overload count_masked(axis:)
|
|
226
|
+
# Not implemented; call it without `axis:`.
|
|
227
|
+
# @param axis [Integer]
|
|
228
|
+
# @return [CArray]
|
|
229
|
+
# @!method count_not_masked(axis: nil)
|
|
230
|
+
# @overload count_not_masked
|
|
231
|
+
# The per-category form: {CASegmentIterator#count_not_masked}, one value per category.
|
|
232
|
+
# @overload count_not_masked(axis:)
|
|
233
|
+
# Per-fiber per-category count of present (non-masked) values along `axis`
|
|
234
|
+
# (int64, shape [K, ...band]). Empty cells are `0`.
|
|
235
|
+
# @param axis [Integer]
|
|
236
|
+
# @return [CArray]
|
|
237
|
+
# @!method max(axis: nil)
|
|
238
|
+
# @overload max
|
|
239
|
+
# The per-category form: {CASegmentIterator#max}, one value per category.
|
|
240
|
+
# @overload max(axis:)
|
|
241
|
+
# Per-fiber per-category maxima along `axis` (h's data type, masked where empty).
|
|
242
|
+
# @param axis [Integer]
|
|
243
|
+
# @return [CArray]
|
|
244
|
+
# @!method mean(axis: nil)
|
|
245
|
+
# @overload mean
|
|
246
|
+
# The per-category form: {CASegmentIterator#mean}, one value per category.
|
|
247
|
+
# @overload mean(axis:)
|
|
248
|
+
# Per-fiber per-category means (float64, empty group cells MASKED).
|
|
249
|
+
# @param axis [Integer]
|
|
250
|
+
# @return [CArray]
|
|
251
|
+
# @!method min(axis: nil)
|
|
252
|
+
# @overload min
|
|
253
|
+
# The per-category form: {CASegmentIterator#min}, one value per category.
|
|
254
|
+
# @overload min(axis:)
|
|
255
|
+
# Per-fiber per-category minima along `axis` (h's data type, masked where empty).
|
|
256
|
+
# @param axis [Integer]
|
|
257
|
+
# @return [CArray]
|
|
258
|
+
# @!method minmax(axis: nil)
|
|
259
|
+
# @overload minmax
|
|
260
|
+
# The per-category form: {CASegmentIterator#minmax}, one value per category.
|
|
261
|
+
# @overload minmax(axis:)
|
|
262
|
+
# Per-fiber `[min_ca, max_ca]` along `axis` (each shape [K, ...band], h's data type,
|
|
263
|
+
# empty group cells MASKED). Ruby Array of two CArrays, not stacked.
|
|
264
|
+
# Both come from one kernel run.
|
|
265
|
+
# @param axis [Integer]
|
|
266
|
+
# @return [Array<CArray>]
|
|
267
|
+
# @!method prod(axis: nil)
|
|
268
|
+
# @overload prod
|
|
269
|
+
# The per-category form: {CASegmentIterator#prod}, one value per category.
|
|
270
|
+
# @overload prod(axis:)
|
|
271
|
+
# Per-fiber per-category products (float64, shape [K, ...band]). Empty
|
|
272
|
+
# group cells `1.0` (identity).
|
|
273
|
+
# @param axis [Integer]
|
|
274
|
+
# @return [CArray]
|
|
275
|
+
# @!method stddevp(axis: nil)
|
|
276
|
+
# @overload stddevp
|
|
277
|
+
# The per-category form: {CASegmentIterator#stddevp}, one value per category.
|
|
278
|
+
# @overload stddevp(axis:)
|
|
279
|
+
# Per-fiber per-category population stddev (float64, empty group cells MASKED).
|
|
280
|
+
# @param axis [Integer]
|
|
281
|
+
# @return [CArray]
|
|
282
|
+
# @!method sum(axis: nil)
|
|
283
|
+
# @overload sum
|
|
284
|
+
# The per-category form: {CASegmentIterator#sum}, one value per category.
|
|
285
|
+
# @overload sum(axis:)
|
|
286
|
+
# Returns per-category sums per fiber along `axis`. Cat may be 1-D (case
|
|
287
|
+
# A, broadcasts across band axes), same rank as source (case B, per-fiber
|
|
288
|
+
# independent classifier), or one rank less (band-only, constant along
|
|
289
|
+
# reduce axis). Output shape = `[K, ...source.shape without axis]`.
|
|
290
|
+
# @param axis [Integer] reduce axis of the source value.
|
|
291
|
+
# @return [CArray]
|
|
292
|
+
# @!method wmean(weights, axis: nil)
|
|
293
|
+
# @overload wmean
|
|
294
|
+
# The per-category form: {CASegmentIterator#wmean}, one value per category.
|
|
295
|
+
# @overload wmean(weights, axis:)
|
|
296
|
+
# Per-fiber per-category weighted mean along `axis`. Same weights-shape
|
|
297
|
+
# contract as {#wsum} (weights.shape == source.shape). Empty cell → MASKED;
|
|
298
|
+
# a present cell whose weights sum to zero → NaN (0/0 core contract).
|
|
299
|
+
# @param weights [CArray]
|
|
300
|
+
# @param axis [Integer]
|
|
301
|
+
# @return [CArray]
|
|
302
|
+
# @!method wsum(weights, axis: nil)
|
|
303
|
+
# @overload wsum
|
|
304
|
+
# The per-category form: {CASegmentIterator#wsum}, one value per category.
|
|
305
|
+
# @overload wsum(weights, axis:)
|
|
306
|
+
# Per-fiber per-category weighted sum along `axis`. `weights` must have
|
|
307
|
+
# shape == source.shape (rev3 requires explicit broadcast; wrap 1-D or
|
|
308
|
+
# band-shape weights via `.broadcast_to(*source.shape)` at the call site).
|
|
309
|
+
# Empty group cell → `0.0` (identity). Mask contract: cell contributes iff
|
|
310
|
+
# value AND weight are present.
|
|
311
|
+
# @param weights [CArray]
|
|
312
|
+
# @param axis [Integer]
|
|
313
|
+
# @return [CArray]
|
|
607
314
|
|
|
608
|
-
|
|
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
|
|
315
|
+
private
|
|
627
316
|
|
|
628
|
-
#
|
|
629
|
-
#
|
|
630
|
-
#
|
|
631
|
-
#
|
|
632
|
-
#
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
}
|
|
317
|
+
# Both are built with the grouping plan, and only on the flat path; the
|
|
318
|
+
# per-fiber form has neither and must not build a segment's.
|
|
319
|
+
# Group-major source indices = the valid prefix of the cached sort_addr.
|
|
320
|
+
# With no classified cell the prefix is empty (and slicing a length-0
|
|
321
|
+
# sort_addr would be out of range), so take the empty permutation directly.
|
|
322
|
+
def perm
|
|
323
|
+
if @nvalid && ! @perm
|
|
324
|
+
@perm = @nvalid > 0 ? @cat.sort_addr[0...@nvalid] : CArray.int64(0)
|
|
651
325
|
end
|
|
326
|
+
@perm
|
|
652
327
|
end
|
|
653
328
|
|
|
654
|
-
|
|
655
|
-
|
|
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
|
|
329
|
+
def codes
|
|
330
|
+
@codes
|
|
680
331
|
end
|
|
681
332
|
|
|
682
|
-
#
|
|
683
|
-
#
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
# categorical grouping is the one-band case of the axis-group scan, so each
|
|
690
|
-
# routes straight through the fused C kernel __axis_group_scan__ (the same one
|
|
691
|
-
# CAGroupIterator drives) with the whole source as a single grouped axis and
|
|
692
|
-
# the categorical's codes as the single bundle -- which yields SOURCE-ORDER
|
|
693
|
-
# output directly, so no counting-sort inverse permutation is needed.
|
|
694
|
-
# Excluded (out-of-vocabulary / masked-code) and source-masked cells join no
|
|
695
|
-
# running total and are UNDEF. Mirroring the reductions (sum / mean), a scan
|
|
696
|
-
# takes no axis argument. cumsum / cumprod -> float64, cummax / cummin
|
|
697
|
-
# preserve the value data type, cumcount -> int64 (1-based within-category
|
|
698
|
-
# ordinal); an object value data type is carried by the kernel's object branch.
|
|
699
|
-
|
|
700
|
-
# @!method cumsum
|
|
701
|
-
# Per-category inclusive running sum (float64), source-shaped.
|
|
702
|
-
# @return [CArray]
|
|
703
|
-
# @!method cumprod
|
|
704
|
-
# Per-category inclusive running product (float64), source-shaped.
|
|
705
|
-
# @return [CArray]
|
|
706
|
-
# @!method cummax
|
|
707
|
-
# Per-category inclusive running maximum (value data type), source-shaped.
|
|
708
|
-
# @return [CArray]
|
|
709
|
-
# @!method cummin
|
|
710
|
-
# Per-category inclusive running minimum (value data type), source-shaped.
|
|
711
|
-
# @return [CArray]
|
|
712
|
-
# @!method cumcount
|
|
713
|
-
# Per-category 1-based within-category ordinal (int64), source-shaped.
|
|
714
|
-
# @return [CArray]
|
|
715
|
-
[:cumsum, :cumprod, :cummax, :cummin, :cumcount].each do |op|
|
|
716
|
-
define_method(op) { scan(op) }
|
|
333
|
+
# With few categories the grouped copy is gathered from the values as
|
|
334
|
+
# built, the first time a reduction asks for it.
|
|
335
|
+
def grouped
|
|
336
|
+
if @as_built && ! @grouped
|
|
337
|
+
@grouped = @nvalid > 0 ? @as_built[perm].copy : @as_built[0...0].copy
|
|
338
|
+
end
|
|
339
|
+
super
|
|
717
340
|
end
|
|
718
341
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
# The category-major copy every no-axis reduction works from. It does not
|
|
722
|
-
# exist when the classifier does not line up cell-for-cell with the value;
|
|
723
|
-
# such an iterator answers the per-fiber form only, and says so here rather
|
|
724
|
-
# than letting a nil surface as whatever NoMethodError it reaches first.
|
|
725
|
-
def grouped
|
|
726
|
-
@grouped || raise(ArgumentError, @no_flat)
|
|
342
|
+
def kernel_reads? (data_type = (@as_built || grouped).data_type)
|
|
343
|
+
super(data_type)
|
|
727
344
|
end
|
|
728
345
|
|
|
729
|
-
#
|
|
730
|
-
#
|
|
731
|
-
|
|
732
|
-
|
|
346
|
+
# With few categories the count / sum / min / max come from one pass over
|
|
347
|
+
# the values as built, each cell added to the slot of its code; a cell
|
|
348
|
+
# whose value or code is masked, or whose code names no category, is in
|
|
349
|
+
# no slot, as it is in no segment of the grouped copy.
|
|
350
|
+
def moments
|
|
351
|
+
return @moments if defined?(@moments)
|
|
352
|
+
return super unless @as_built && kernel_reads?
|
|
353
|
+
counts = CArray.int64(@k)
|
|
354
|
+
sums = CArray.float64(@k)
|
|
355
|
+
mins = CArray.new(@as_built.data_type, [@k])
|
|
356
|
+
maxs = CArray.new(@as_built.data_type, [@k])
|
|
357
|
+
@as_built.__send__(:__fiber_scatter_moments__, @codes, 0, @k, counts, sums, mins, maxs)
|
|
358
|
+
@moments = { count: counts, sum: sums, min: mins, max: maxs }
|
|
733
359
|
end
|
|
734
360
|
|
|
735
361
|
# 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
|
-
|
|
362
|
+
# group does not look at what is in them. An integer or float payload goes
|
|
363
|
+
# through the fused moments kernel, which counts in the same pass that
|
|
364
|
+
# sums. Any other payload -- complex, boolean, object, a Face -- the kernel
|
|
365
|
+
# refuses, for an answer that never depended on the payload, so it is
|
|
366
|
+
# counted from the codes and the value's mask, one group at a time.
|
|
367
|
+
def axis_counts (axis, op)
|
|
741
368
|
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
|
|
369
|
+
return axis_moments(axis, op)[:count] if kernel_reads?(h.data_type)
|
|
370
|
+
axis = checked_axis(axis, op)
|
|
747
371
|
full_c = resolve_axis_codes(@cat.codes, h.shape, axis)
|
|
748
372
|
band = h.shape.dup; band.delete_at(axis)
|
|
749
373
|
out = CArray.int64(*([@k] + band))
|
|
@@ -755,7 +379,8 @@ class CACategoricalIterator < CAIterator
|
|
|
755
379
|
belongs = belongs.strip_mask(false) if belongs.has_mask?
|
|
756
380
|
belongs = belongs & present if present
|
|
757
381
|
slot[0] = c
|
|
758
|
-
|
|
382
|
+
# a boolean sums to a count without widening the whole array first
|
|
383
|
+
out[*slot] = belongs.sum(axis: axis)
|
|
759
384
|
end
|
|
760
385
|
out
|
|
761
386
|
end
|
|
@@ -774,35 +399,80 @@ class CACategoricalIterator < CAIterator
|
|
|
774
399
|
# data can produce. The axis: path materialises nothing else, so holding
|
|
775
400
|
# this one thing was the odd choice; a caller who wants a fused kernel's
|
|
776
401
|
# four answers shares them by keeping the result.
|
|
777
|
-
def axis_moments (axis)
|
|
402
|
+
def axis_moments (axis, op)
|
|
403
|
+
return axis_moments_by_core(axis, op) unless kernel_reads?(@value.data_type)
|
|
778
404
|
h = @value
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
"Integer in [0, #{h.ndim}) for source h with shape #{h.shape}"
|
|
783
|
-
end
|
|
405
|
+
axis = checked_axis(axis, op)
|
|
406
|
+
band = h.shape.dup; band.delete_at(axis)
|
|
407
|
+
return band_only_moments(h, axis, band, op) if band_only?(@cat.codes, h.shape, axis)
|
|
784
408
|
codes_h_shape = resolve_axis_codes(@cat.codes, h.shape, axis)
|
|
785
|
-
band = h.shape.dup; band.delete_at(axis)
|
|
786
409
|
out_shape = [@k] + band
|
|
787
410
|
counts = CArray.int64(*out_shape)
|
|
788
411
|
sums = CArray.float64(*out_shape)
|
|
789
|
-
mins = CArray.new(h.data_type, out_shape)
|
|
790
|
-
maxs = CArray.new(h.data_type, out_shape)
|
|
412
|
+
mins = CArray.new(h.data_type, out_shape, bytes: h.bytes)
|
|
413
|
+
maxs = CArray.new(h.data_type, out_shape, bytes: h.bytes)
|
|
791
414
|
h.__send__(:__fiber_scatter_moments__, codes_h_shape, axis, @k,
|
|
792
415
|
counts, sums, mins, maxs)
|
|
793
416
|
{count: counts, sum: sums, min: mins, max: maxs}
|
|
794
417
|
end
|
|
795
418
|
|
|
419
|
+
# The classifier is band-only, as resolve_axis_codes decides it (case A
|
|
420
|
+
# wins when both shapes fit).
|
|
421
|
+
def band_only? (codes, h_shape, axis)
|
|
422
|
+
band = h_shape.dup; band.delete_at(axis)
|
|
423
|
+
!band.empty? && codes.shape == band && codes.shape != [h_shape[axis]]
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Moments for a band-only classifier. Every cell of a fiber has the same
|
|
427
|
+
# code, so a fiber falls into one group whole: the core's own reductions
|
|
428
|
+
# along the axis answer for it, and each answer is placed in its code's
|
|
429
|
+
# row. A fiber whose code is masked belongs to no group. A group no fiber
|
|
430
|
+
# falls in, or one whose fibers are all masked, has count 0, sum 0.0 and
|
|
431
|
+
# masked min and max, as the fused kernel leaves it. Only the answers the
|
|
432
|
+
# caller reads are computed; each is a pass of its own here, where the
|
|
433
|
+
# kernel had all four from one.
|
|
434
|
+
BAND_ONLY_KEYS = { sum: [:sum], mean: [:sum, :count], min: [:min],
|
|
435
|
+
max: [:max], minmax: [:min, :max] }.freeze
|
|
436
|
+
Ractor.make_shareable(BAND_ONLY_KEYS)
|
|
437
|
+
private_constant :BAND_ONLY_KEYS
|
|
438
|
+
|
|
439
|
+
def band_only_moments (h, axis, band, op)
|
|
440
|
+
nb = band.inject(1, :*)
|
|
441
|
+
shape = [@k] + band
|
|
442
|
+
codes = @cat.codes
|
|
443
|
+
valid = codes.lt(@k).flatten
|
|
444
|
+
valid = valid.strip_mask(false) if valid.has_mask?
|
|
445
|
+
addr = (codes.value.int64.flatten * nb + CArray.int64(nb).seq)[valid]
|
|
446
|
+
out = {}
|
|
447
|
+
BAND_ONLY_KEYS.fetch(op, [:count]).each do |key|
|
|
448
|
+
res = case key
|
|
449
|
+
when :count then CArray.int64(*shape)
|
|
450
|
+
when :sum then CArray.float64(*shape)
|
|
451
|
+
else CArray.new(h.data_type, shape, bytes: h.bytes).tap { |a| a[] = UNDEF }
|
|
452
|
+
end
|
|
453
|
+
per_fiber = case key
|
|
454
|
+
when :count then h.count_not_masked(axis: axis)
|
|
455
|
+
when :sum then h.sum(axis: axis)
|
|
456
|
+
when :min then h.min(axis: axis)
|
|
457
|
+
when :max then h.max(axis: axis)
|
|
458
|
+
end
|
|
459
|
+
res.reshape(@k * nb)[addr] = per_fiber.flatten[valid]
|
|
460
|
+
out[key] = res
|
|
461
|
+
end
|
|
462
|
+
out
|
|
463
|
+
end
|
|
464
|
+
|
|
796
465
|
# Axis-aware sum: the moments sum is already the core fold in the core's own
|
|
797
466
|
# type, so it is handed back as is (an empty group cell carries identity 0.0).
|
|
798
467
|
def axis_sum (axis)
|
|
799
|
-
axis_moments(axis)[:sum]
|
|
468
|
+
axis_moments(axis, :sum)[:sum]
|
|
800
469
|
end
|
|
801
470
|
|
|
802
471
|
# Axis-aware mean: sums / counts (float64); empty group cells (count=0) MASKED.
|
|
803
472
|
# Matches flat #mean per fiber.
|
|
804
473
|
def axis_mean (axis)
|
|
805
|
-
|
|
474
|
+
return axis_by_core(axis, :mean) unless kernel_reads?(@value.data_type)
|
|
475
|
+
m = axis_moments(axis, :mean)
|
|
806
476
|
cnt = m[:count]
|
|
807
477
|
out = m[:sum] / cnt.float64 # count 0 -> NaN
|
|
808
478
|
out[cnt.eq(0)] = UNDEF # empty / all-masked -> MASKED
|
|
@@ -821,52 +491,185 @@ class CACategoricalIterator < CAIterator
|
|
|
821
491
|
# Cost: K axis-reductions over an h-shaped local (most cells masked away for
|
|
822
492
|
# each c) — bounded by K, typically small. A fused per-fiber variance
|
|
823
493
|
# kernel is a natural follow-on if bench demands it.
|
|
824
|
-
def axis_by_masked_copy (axis, op, out_data_type =
|
|
494
|
+
def axis_by_masked_copy (axis, op, out_data_type = core_reduce_type(op))
|
|
825
495
|
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
|
|
496
|
+
axis = checked_axis(axis, op)
|
|
831
497
|
full_c = resolve_axis_codes(@cat.codes, h.shape, axis)
|
|
832
498
|
band = h.shape.dup; band.delete_at(axis)
|
|
833
499
|
out = CArray.new(out_data_type, [@k] + band)
|
|
834
500
|
slot_idx = [nil] + [nil] * band.size # placeholder; c fills slot 0
|
|
835
501
|
codes_bad = full_c.has_mask? ? full_c.is_masked : nil
|
|
836
502
|
@k.times do |c|
|
|
837
|
-
h_local = h.copy
|
|
838
503
|
# Boolean of cells that DO belong to group c (with codes present). On
|
|
839
504
|
# any masked codes cell the codes.eq(c) result carries UNDEF, which
|
|
840
505
|
# naturally reads as "not in group c" for our exclusion purpose.
|
|
841
506
|
in_c = full_c.eq(c)
|
|
842
507
|
exclude = in_c.not
|
|
843
508
|
exclude = exclude | codes_bad if codes_bad
|
|
844
|
-
h_local
|
|
845
|
-
slice = h_local.__send__(op, axis: axis) #
|
|
509
|
+
h_local = h.mask_where(exclude)
|
|
510
|
+
slice = h_local.__send__(op, axis: axis) # band shape, mask carries n<contract
|
|
846
511
|
slot_idx[0] = c
|
|
847
512
|
out[*slot_idx] = slice
|
|
848
513
|
end
|
|
849
514
|
out
|
|
850
515
|
end
|
|
851
516
|
|
|
517
|
+
# The axis: reductions for a value the fused kernels cannot read (boolean,
|
|
518
|
+
# object, a Face): each group's cells are reduced by the core's own
|
|
519
|
+
# reduction along the axis and the answers stacked, so the data type, the
|
|
520
|
+
# refusals and the empty-group answer are the core's -- a time column's mean
|
|
521
|
+
# is a time, its sum is refused, an empty group's sum is the identity.
|
|
522
|
+
# Classified along the axis (case A), a group is its positions on the axis,
|
|
523
|
+
# taken by index; otherwise its cells are kept by masking the rest of a copy.
|
|
524
|
+
# A read-only Face's answers (a const string's records index a pool of
|
|
525
|
+
# their own) cannot be put together, so they are collected as the surface
|
|
526
|
+
# values they are, as the flat form does. Over a 1-D value each group
|
|
527
|
+
# reduces to one cell, kept as a 1-element array so the answers stack.
|
|
528
|
+
def axis_by_core (axis, op, *args)
|
|
529
|
+
h = @value
|
|
530
|
+
axis = checked_axis(axis, op)
|
|
531
|
+
flat = (h.ndim == 1)
|
|
532
|
+
groups = (0...[@k, 1].max).map { |c| group_cells(h, axis, c, @k.zero?) }
|
|
533
|
+
answers = groups.map do |g|
|
|
534
|
+
flat ? g.__send__(op, *args, axis: axis, keep_axis: true)
|
|
535
|
+
: g.__send__(op, *args, axis: axis)
|
|
536
|
+
end
|
|
537
|
+
if answers.first.face? && answers.first.read_only?
|
|
538
|
+
answers = answers.map { |a| CArray.object(*a.shape) { a.to_a } }
|
|
539
|
+
end
|
|
540
|
+
out = flat ? CArray.concatenate(answers) : CArray.stack(answers, axis: 0).copy
|
|
541
|
+
@k.zero? ? out[0...0, *([nil] * (out.ndim - 1))].copy : out
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def axis_moments_by_core (axis, op)
|
|
545
|
+
BAND_ONLY_KEYS.fetch(op, [:count]).to_h do |key|
|
|
546
|
+
[key, axis_by_core(axis, key == :count ? :count_not_masked : key)]
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def group_cells (h, axis, c, none)
|
|
551
|
+
codes = @cat.codes
|
|
552
|
+
# Classified along the axis, a group is taken by index; the general way,
|
|
553
|
+
# masking the rest of a copy, gives the same cells.
|
|
554
|
+
if codes.shape == [h.shape[axis]] && CArray.__fast_paths_enabled__
|
|
555
|
+
sel = none ? CArray.boolean(codes.elements) { false } : codes.eq(c)
|
|
556
|
+
sel = sel.strip_mask(false) if sel.has_mask?
|
|
557
|
+
idx = [nil] * h.ndim
|
|
558
|
+
idx[axis] = sel.where
|
|
559
|
+
h[*idx]
|
|
560
|
+
else
|
|
561
|
+
full_c = resolve_axis_codes(codes, h.shape, axis)
|
|
562
|
+
exclude = none ? CArray.boolean(*h.shape) { true } : full_c.eq(c).not
|
|
563
|
+
exclude = exclude | full_c.is_masked if full_c.has_mask?
|
|
564
|
+
# a copy with the rest masked, which a read-only Face also gives
|
|
565
|
+
h.mask_where(exclude)
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
private :axis_by_core, :axis_moments_by_core, :group_cells
|
|
569
|
+
|
|
570
|
+
# A running statistic along an axis, for a categorical that classifies the
|
|
571
|
+
# positions along it (case A): each cell holds its group's running value up
|
|
572
|
+
# to it along the axis, per fiber. The answer is the core's, as the flat
|
|
573
|
+
# form's is; the kernel runs when it answers in the core's data type, and
|
|
574
|
+
# otherwise each group's positions are scanned by the core along the axis.
|
|
575
|
+
# Read when called, as the other axis: members are.
|
|
576
|
+
def axis_scan (op, axis)
|
|
577
|
+
h = @value
|
|
578
|
+
axis = checked_axis(axis, op)
|
|
579
|
+
classified_along!(op, axis)
|
|
580
|
+
answer = core_scan_answer(op)
|
|
581
|
+
if kernel_scans?(op, answer)
|
|
582
|
+
bundle = @k > 0 ? [@cat.codes, @k, [axis]]
|
|
583
|
+
: [CArray.int64(h.shape[axis]).fill(-1), 1, [axis]]
|
|
584
|
+
return h.__scan_by_axis_groups__([axis], [bundle], op)
|
|
585
|
+
end
|
|
586
|
+
each_group_along(axis, ->(shape) { blank_like(answer, shape) }) do |g|
|
|
587
|
+
g.public_send(op, axis: axis)
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
# #lag along an axis (case A): each group's positions along the axis, every
|
|
592
|
+
# other axis kept, lagged along it, so a lag never reaches another group.
|
|
593
|
+
def axis_lag (n, axis)
|
|
594
|
+
axis = checked_axis(axis, :lag)
|
|
595
|
+
classified_along!(:lag, axis)
|
|
596
|
+
each_group_along(axis, ->(shape) { blank_like(@value, shape) }) do |g|
|
|
597
|
+
g.lag(n, axis: axis)
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# #strip_mask(method:) along an axis (case A): each group's positions along
|
|
602
|
+
# the axis filled along it, fiber by fiber.
|
|
603
|
+
def axis_hold (method, axis)
|
|
604
|
+
axis = checked_axis(axis, :strip_mask)
|
|
605
|
+
classified_along!(:strip_mask, axis)
|
|
606
|
+
each_group_along(axis, ->(shape) { blank_like(@value, shape) }) do |g|
|
|
607
|
+
g.strip_mask(method: method, axis: axis)
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
# #order along an axis (case A): each group's positions along the axis
|
|
612
|
+
# ranked along it, fiber by fiber.
|
|
613
|
+
def axis_order (axis, **kw)
|
|
614
|
+
axis = checked_axis(axis, :order)
|
|
615
|
+
classified_along!(:order, axis)
|
|
616
|
+
each_group_along(axis, ->(shape) { CArray.int64(*shape) }) do |g|
|
|
617
|
+
g.order(axis: axis, **kw)
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
# #map along an axis (case A): the block receives each group's positions
|
|
622
|
+
# along the axis, every other axis kept, and its answer goes back to them.
|
|
623
|
+
def axis_map (axis, data_type, &block)
|
|
624
|
+
axis = checked_axis(axis, :map)
|
|
625
|
+
classified_along!(:map, axis)
|
|
626
|
+
each_group_along(axis, ->(shape) { blank_output(shape, data_type) }, &block)
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# Yields each group's positions along +axis+ as a view of the value and
|
|
630
|
+
# writes the block's answer back to them; a position in no group is UNDEF.
|
|
631
|
+
def each_group_along (axis, blank)
|
|
632
|
+
h = @value
|
|
633
|
+
out = blank.call(h.shape)
|
|
634
|
+
out[] = UNDEF
|
|
635
|
+
@k.times do |c|
|
|
636
|
+
sel = @cat.codes.eq(c)
|
|
637
|
+
sel = sel.strip_mask(false) if sel.has_mask?
|
|
638
|
+
next if sel.count(true).zero?
|
|
639
|
+
idx = [nil] * h.ndim
|
|
640
|
+
idx[axis] = sel.where
|
|
641
|
+
out[*idx] = surface_for(out, yield(h[*idx]))
|
|
642
|
+
end
|
|
643
|
+
out
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
# A running value or a transform along an axis follows the positions along
|
|
647
|
+
# it, so the categorical has to classify those positions.
|
|
648
|
+
def classified_along! (op, axis)
|
|
649
|
+
n = @value.shape[axis]
|
|
650
|
+
return if @cat.codes.shape == [n]
|
|
651
|
+
raise ArgumentError,
|
|
652
|
+
"#{op}(axis: #{axis}): needs a categorical that classifies the " \
|
|
653
|
+
"positions along axis #{axis} (shape [#{n}]); got shape " \
|
|
654
|
+
"#{@cat.codes.shape.inspect}"
|
|
655
|
+
end
|
|
656
|
+
private :axis_scan, :axis_lag, :axis_order, :axis_hold, :axis_map, :each_group_along, :classified_along!
|
|
657
|
+
|
|
852
658
|
# Axis-aware wsum + wmean fused (single kernel call, both outputs). Returns
|
|
853
659
|
# [wsum_ca, wmean_ca]. Weights must match source shape exactly (explicit
|
|
854
660
|
# broadcast on the call site for 1-D or band-shape weights). A cell
|
|
855
661
|
# contributes iff its value AND its weight are present.
|
|
856
|
-
def axis_wsum_wmean (weights, axis)
|
|
662
|
+
def axis_wsum_wmean (weights, axis, op)
|
|
857
663
|
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
|
|
664
|
+
axis = checked_axis(axis, op)
|
|
863
665
|
unless weights.is_a?(CArray) && weights.shape == h.shape
|
|
864
666
|
raise ArgumentError,
|
|
865
|
-
"
|
|
667
|
+
"#{op}(axis: #{axis}): weights.shape " \
|
|
866
668
|
"#{weights.respond_to?(:shape) ? weights.shape.inspect : weights.class} " \
|
|
867
669
|
"must equal source.shape #{h.shape.inspect}. Wrap 1-D / band-shape " \
|
|
868
670
|
"weights via `.broadcast_to(*source.shape)` before passing."
|
|
869
671
|
end
|
|
672
|
+
return weighted_by_core(weights, axis) unless kernel_reads?(h.data_type)
|
|
870
673
|
codes_h_shape = resolve_axis_codes(@cat.codes, h.shape, axis)
|
|
871
674
|
weights_f64 = weights.data_type == CA_FLOAT64 ? weights : weights.float64
|
|
872
675
|
band = h.shape.dup; band.delete_at(axis)
|
|
@@ -877,15 +680,27 @@ class CACategoricalIterator < CAIterator
|
|
|
877
680
|
[ws_out, wm_out]
|
|
878
681
|
end
|
|
879
682
|
|
|
683
|
+
# wsum and wmean of each group by the core: the group's cells of the value
|
|
684
|
+
# and of the weights, each other cell masked, reduced along the axis.
|
|
685
|
+
def weighted_by_core (weights, axis)
|
|
686
|
+
h = @value
|
|
687
|
+
%i[wsum wmean].map do |op|
|
|
688
|
+
answers = (0...[@k, 1].max).map do |c|
|
|
689
|
+
group_cells(h, axis, c, @k.zero?)
|
|
690
|
+
.public_send(op, group_cells(weights, axis, c, @k.zero?), axis: axis)
|
|
691
|
+
end
|
|
692
|
+
out = CArray.stack(answers, axis: 0).copy
|
|
693
|
+
@k.zero? ? out[0...0, *([nil] * (out.ndim - 1))].copy : out
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
private :weighted_by_core
|
|
697
|
+
|
|
880
698
|
# Axis-aware prod: dedicated kernel (identity 1.0, separate from moments to
|
|
881
699
|
# avoid conflating with sum's zero-identity memset).
|
|
882
700
|
def axis_prod (axis)
|
|
701
|
+
return axis_by_core(axis, :prod) unless kernel_reads?(@value.data_type)
|
|
883
702
|
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
|
|
703
|
+
axis = checked_axis(axis, :prod)
|
|
889
704
|
codes_h_shape = resolve_axis_codes(@cat.codes, h.shape, axis)
|
|
890
705
|
band = h.shape.dup; band.delete_at(axis)
|
|
891
706
|
out = CArray.float64(*([@k] + band))
|
|
@@ -896,6 +711,12 @@ class CACategoricalIterator < CAIterator
|
|
|
896
711
|
# Broadcast `codes` to `h_shape` per PROPOSAL §2.2 3-case positional rule.
|
|
897
712
|
# Returns a broadcast view of codes at h_shape (or codes itself for case B).
|
|
898
713
|
# Raises ArgumentError with a message that enumerates all 3 accepted shapes.
|
|
714
|
+
# The axis of an axis: reduction, normalised against the source: a
|
|
715
|
+
# negative axis counts from the end, as everywhere else.
|
|
716
|
+
def checked_axis (axis, op)
|
|
717
|
+
@value.normalize_axis(axis, op.to_s)
|
|
718
|
+
end
|
|
719
|
+
|
|
899
720
|
def resolve_axis_codes (codes, h_shape, axis)
|
|
900
721
|
ndim = h_shape.size
|
|
901
722
|
band = h_shape.dup; band.delete_at(axis)
|
|
@@ -932,225 +753,10 @@ class CACategoricalIterator < CAIterator
|
|
|
932
753
|
"band-only cat.shape=#{band.inspect}."
|
|
933
754
|
end
|
|
934
755
|
|
|
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
|
|
756
|
+
# median / percentile along an axis: the core's own order statistic of each
|
|
757
|
+
# group's cells, stacked -- the axis_by_core path, one core call per group.
|
|
758
|
+
def axis_order_stat (axis, op, *args)
|
|
759
|
+
axis_by_core(axis, op, *args)
|
|
1154
760
|
end
|
|
1155
761
|
end
|
|
1156
762
|
|
|
@@ -1161,8 +767,14 @@ class CArray
|
|
|
1161
767
|
# per category of `cat`. Requires `self.elements == cat.elements`.
|
|
1162
768
|
# @param cat [CACategorical] the classifier.
|
|
1163
769
|
# @return [CACategoricalIterator]
|
|
770
|
+
# @raise [TypeError] when `cat` is not a CACategorical.
|
|
1164
771
|
# @raise [ArgumentError] when element counts differ.
|
|
1165
772
|
def group_by_category (cat)
|
|
773
|
+
unless cat.is_a?(CACategorical)
|
|
774
|
+
raise TypeError,
|
|
775
|
+
"group_by_category: expects a CACategorical (got #{cat.class}); " \
|
|
776
|
+
"classify the values first with .categorize"
|
|
777
|
+
end
|
|
1166
778
|
CACategoricalIterator.new(self, cat)
|
|
1167
779
|
end
|
|
1168
780
|
|
|
@@ -1187,18 +799,16 @@ class CArray
|
|
|
1187
799
|
# `grp.labels` is `[0, 1, ...]` in position order.
|
|
1188
800
|
#
|
|
1189
801
|
# @return [CACategoricalIterator] one category per run, in order.
|
|
1190
|
-
# @raise [
|
|
802
|
+
# @raise [ArgumentError] when `self` is not 1-D.
|
|
1191
803
|
def group_by_run
|
|
1192
|
-
raise "group_by_run: 1-D only (got #{ndim}-D)" unless ndim == 1
|
|
804
|
+
raise ArgumentError, "group_by_run: 1-D only (got #{ndim}-D)" unless ndim == 1
|
|
1193
805
|
if elements == 0
|
|
1194
806
|
code = CArray.int64(0)
|
|
1195
807
|
else
|
|
1196
808
|
present = is_not_masked
|
|
1197
809
|
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
|
|
810
|
+
# a boolean cumsum counts in uint64, exactly
|
|
811
|
+
code = edge.cumsum.int64 - 1 # 0-based run index per cell
|
|
1202
812
|
code[present.not] = UNDEF # masked cells join no run
|
|
1203
813
|
end
|
|
1204
814
|
# categorize turns the dense run indices into the run categories: it derives
|