carray 3.0.0 → 3.0.2
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/.yardopts +2 -1
- data/CHANGELOG.md +845 -3
- data/{NEWS.md → CHANGELOG.v1.md} +1 -1
- data/README.md +33 -33
- data/carray.gemspec +12 -8
- data/ext/ca_axis_dispatch.c +33 -4
- data/ext/ca_axis_group.c +256 -114
- data/ext/ca_binop_dispatch.c +5 -6
- data/ext/ca_binop_dispatch.h +0 -7
- data/ext/ca_categorical_iterator.c +121 -67
- data/ext/ca_for_each_element.h +6 -8
- data/ext/ca_group_iter.c +1 -1
- data/ext/ca_kernel_iterator.c +386 -74
- data/ext/ca_kernel_iterator.h +145 -45
- data/ext/ca_obj_array.c +62 -20
- data/ext/ca_obj_bincmp.c +57 -27
- data/ext/ca_obj_binop.c +86 -30
- data/ext/ca_obj_block.c +4 -4
- data/ext/ca_obj_const_string.c +88 -27
- data/ext/ca_obj_face.c +104 -0
- data/ext/ca_obj_face.h +48 -1
- data/ext/ca_obj_fake.c +11 -0
- data/ext/ca_obj_fixlen_string.c +21 -6
- data/ext/ca_obj_grid.c +11 -3
- data/ext/ca_obj_meld.c +125 -22
- data/ext/ca_obj_moncmp.c +33 -14
- data/ext/ca_obj_monop.c +19 -7
- data/ext/ca_obj_object.c +107 -4
- data/ext/ca_obj_record.c +3 -1
- data/ext/ca_obj_refer.c +30 -6
- data/ext/ca_obj_roll.c +13 -5
- data/ext/ca_obj_select.c +49 -34
- data/ext/ca_obj_select_axis.c +16 -6
- data/ext/ca_obj_shift.c +3 -3
- data/ext/ca_obj_stack.c +3 -8
- data/ext/ca_obj_stride.c +178 -24
- data/ext/ca_obj_string.c +11 -5
- data/ext/ca_obj_tile.c +13 -5
- data/ext/ca_obj_time.c +3 -1
- data/ext/ca_obj_timedelta.c +3 -1
- data/ext/ca_obj_transpose.c +1 -1
- data/ext/ca_obj_triop.c +62 -23
- data/ext/ca_obj_window.c +94 -19
- data/ext/ca_op_cmplx64.h +123 -0
- data/ext/ca_op_ipower.c +1 -5
- data/ext/ca_rng_normal.h +42 -0
- data/ext/ca_rng_xoshiro256pp.h +105 -0
- data/ext/ca_sort_kernels.h +5 -5
- data/ext/ca_sweep_engine.c +362 -155
- data/ext/ca_sweep_engine.h +39 -9
- data/ext/ca_transform_common.c +7 -0
- data/ext/carray.h +148 -132
- data/ext/carray_access.c +83 -49
- data/ext/carray_address_basis.c +590 -0
- data/ext/carray_bincount.c +8 -8
- data/ext/carray_broadcast.c +102 -9
- data/ext/carray_build_flags.h +3 -0
- data/ext/carray_call_cfunc.c +2010 -335
- data/ext/carray_call_cfunc.h +153 -0
- data/ext/carray_cast.c +184 -72
- data/ext/carray_conversion.c +22 -22
- data/ext/carray_copy.c +67 -32
- data/ext/carray_core.c +140 -13
- data/ext/carray_count.c +10 -11
- data/ext/carray_element.c +7 -7
- data/ext/carray_factorize.c +72 -51
- data/ext/carray_hold.c +1 -1
- data/ext/carray_index_classifier.c +7 -21
- data/ext/carray_internal.h +36 -3
- data/ext/carray_kernels_bincmp.c +1 -0
- data/ext/carray_kernels_binop.c +320 -298
- data/ext/carray_kernels_init.c +1095 -0
- data/ext/carray_kernels_map.c +1 -0
- data/ext/carray_kernels_moncmp.c +1 -0
- data/ext/carray_kernels_monop.c +412 -411
- data/ext/carray_kernels_reduce_aggregate.c +256 -87
- data/ext/carray_kernels_reduce_boolean.c +1 -0
- data/ext/carray_kernels_reduce_cumulative.c +324 -54
- data/ext/carray_kernels_reduce_extreme.c +605 -58
- data/ext/carray_kernels_reduce_variance.c +1 -0
- data/ext/carray_kernels_scan.c +5 -4
- data/ext/carray_kernels_search.c +187 -106
- data/ext/carray_kernels_sort.c +1 -0
- data/ext/carray_kernels_triop.c +1 -0
- data/ext/carray_lazy.c +175 -5
- data/ext/carray_loop.c +7 -1
- data/ext/carray_mask.c +29 -13
- data/ext/carray_median_percentile.c +56 -1
- data/ext/carray_memory_view.c +46 -21
- data/ext/carray_operator.c +50 -53
- data/ext/carray_order.c +1 -1
- data/ext/carray_partition.c +4 -4
- data/ext/carray_random.c +391 -47
- data/ext/carray_scatter.c +1 -1
- data/ext/carray_slab.c +13 -0
- data/ext/carray_sort.c +22 -24
- data/ext/carray_sort_kernel.c +10 -10
- data/ext/carray_test.c +1 -1
- data/ext/extconf.rb +18 -0
- data/ext/mk_call_cfunc.rb +323 -145
- data/ext/mkkernel.rb +651 -129
- data/ext/ruby_carray.c +28 -17
- data/ext/version.h +4 -4
- data/lib/carray/autoload_carray.rb +24 -3
- data/lib/carray/autoload_method_extension.rb +14 -1
- data/lib/carray/axis_group.rb +84 -7
- data/lib/carray/basics.rb +63 -59
- data/lib/carray/bincount_nd.rb +12 -12
- data/lib/carray/block_iterator.rb +111 -25
- data/lib/carray/categorical.rb +154 -37
- data/lib/carray/categorical_iterator.rb +300 -151
- data/lib/carray/conditional.rb +14 -14
- data/lib/carray/const_string.rb +131 -27
- data/lib/carray/construct.rb +61 -0
- data/lib/carray/core_extensions.rb +62 -5
- data/lib/carray/data_type_extension.rb +21 -1
- data/lib/carray/data_type_limits.rb +91 -0
- data/lib/carray/fixlen_string.rb +1 -1
- data/lib/carray/frame/concat.rb +3 -3
- data/lib/carray/frame/convert.rb +1 -1
- data/lib/carray/frame/csv_parser.rb +11 -4
- data/lib/carray/frame/frame.rb +85 -21
- data/lib/carray/frame/group.rb +36 -3
- data/lib/carray/frame/io.rb +224 -15
- data/lib/carray/frame/records.rb +18 -4
- data/lib/carray/frame/sort.rb +1 -1
- data/lib/carray/frame/verbs.rb +32 -12
- data/lib/carray/fuse_source.rb +123 -0
- data/lib/carray/fusion.rb +218 -0
- data/lib/carray/histogram.rb +16 -16
- data/lib/carray/inspect.rb +43 -16
- data/lib/carray/iterator.rb +147 -3
- data/lib/carray/lazy.rb +88 -73
- data/lib/carray/mask_gap_fill.rb +3 -1
- data/lib/carray/meld_reduce.rb +2 -2
- data/lib/carray/methods/align_addr.rb +1 -1
- data/lib/carray/methods/composition.rb +1 -1
- data/lib/carray/methods/discovery_along.rb +74 -0
- data/lib/carray/methods/factorize.rb +50 -0
- data/lib/carray/methods/is_in.rb +25 -14
- data/lib/carray/methods/locate_addr.rb +81 -3
- data/lib/carray/methods/mask_duplicates.rb +36 -2
- data/lib/carray/methods/meshgrid.rb +4 -5
- data/lib/carray/methods/mode.rb +2 -2
- data/lib/carray/methods/nunique.rb +23 -2
- data/lib/carray/methods/repeat.rb +110 -0
- data/lib/carray/methods/snap.rb +7 -2
- data/lib/carray/methods/unique.rb +44 -4
- data/lib/carray/methods/value_counts.rb +2 -2
- data/lib/carray/rng.rb +86 -0
- data/lib/carray/runtime.rb +0 -19
- data/lib/carray/slab_iterator.rb +77 -19
- data/lib/carray/string_operation_extension.rb +10 -6
- data/lib/carray/time.rb +906 -491
- data/lib/carray/window_iterator.rb +421 -27
- data/lib/carray.rb +7 -5
- data/yard-stubs/ca_obj_array.rb +385 -0
- data/yard-stubs/ca_obj_bitarray.rb +38 -0
- data/yard-stubs/ca_obj_bitfield.rb +43 -0
- data/yard-stubs/ca_obj_block.rb +68 -0
- data/yard-stubs/ca_obj_byte_swap.rb +56 -0
- data/yard-stubs/ca_obj_fake.rb +31 -0
- data/yard-stubs/ca_obj_farray.rb +32 -0
- data/yard-stubs/ca_obj_field.rb +45 -0
- data/yard-stubs/ca_obj_grid.rb +35 -0
- data/yard-stubs/ca_obj_refer.rb +72 -0
- data/yard-stubs/ca_obj_roll.rb +45 -0
- data/yard-stubs/ca_obj_shift.rb +43 -0
- data/yard-stubs/ca_obj_stride.rb +181 -0
- data/yard-stubs/ca_obj_tile.rb +29 -0
- data/yard-stubs/ca_obj_transpose.rb +40 -0
- data/yard-stubs/ca_obj_window.rb +57 -0
- data/yard-stubs/carray_access.rb +131 -0
- data/yard-stubs/carray_attribute.rb +246 -0
- data/yard-stubs/carray_broadcast.rb +37 -0
- data/yard-stubs/carray_cast.rb +489 -0
- data/yard-stubs/carray_class.rb +65 -0
- data/yard-stubs/carray_conversion.rb +76 -0
- data/yard-stubs/carray_copy.rb +79 -0
- data/yard-stubs/carray_core.rb +34 -0
- data/yard-stubs/carray_count.rb +84 -0
- data/yard-stubs/carray_element.rb +108 -0
- data/yard-stubs/carray_generate.rb +66 -0
- data/yard-stubs/carray_lazy.rb +228 -0
- data/yard-stubs/carray_loop.rb +140 -0
- data/yard-stubs/carray_mask.rb +259 -0
- data/yard-stubs/carray_math.rb +1615 -0
- data/yard-stubs/carray_mathfunc.rb +45 -0
- data/yard-stubs/carray_median_percentile.rb +103 -0
- data/yard-stubs/carray_memory_view.rb +163 -0
- data/yard-stubs/carray_order.rb +252 -0
- data/yard-stubs/carray_random.rb +89 -0
- data/yard-stubs/carray_scatter.rb +106 -0
- data/yard-stubs/carray_slab.rb +59 -0
- data/yard-stubs/carray_sort.rb +165 -0
- data/yard-stubs/carray_test.rb +85 -0
- data/yard-stubs/carray_undef.rb +64 -0
- data/yard-stubs/carray_utils.rb +97 -0
- data/yard-stubs/ruby_carray.rb +193 -0
- metadata +67 -10
- data/ext/ca_obj_unbound_repeat.c +0 -496
|
@@ -26,18 +26,18 @@
|
|
|
26
26
|
# the mask contract (empty / all-masked -> identity for sum/prod, UNDEF for
|
|
27
27
|
# ratios) carries through unchanged. Results are length-k CArrays aligned to
|
|
28
28
|
# `cat.labels`; undefined slots are MASKED cells (never magic floats). Output
|
|
29
|
-
#
|
|
29
|
+
# data type and the empty / all-masked answer per method:
|
|
30
30
|
#
|
|
31
31
|
# elements -> int64, classified cells (incl. masked)
|
|
32
32
|
# count / count_not_masked / count_masked / count(v) -> int64
|
|
33
|
-
# sum -> value
|
|
33
|
+
# sum -> value data type, empty/all-masked = 0 (identity)
|
|
34
34
|
# prod -> float64, empty/all-masked = 1 (identity)
|
|
35
|
-
# max / min -> value
|
|
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
38
|
# variance / stddev (sample, ddof=1) -> float64, empty/all-masked = MASKED,
|
|
39
39
|
# single value = 0.0 (n=1 contract)
|
|
40
|
-
# all / any -> boolean (boolean value
|
|
40
|
+
# all / any -> boolean (boolean value data type only)
|
|
41
41
|
# labels -> cat.labels
|
|
42
42
|
#
|
|
43
43
|
# Generic iteration (the escape hatch for statistics not in the named surface),
|
|
@@ -103,7 +103,7 @@ class CACategoricalIterator < CAIterator
|
|
|
103
103
|
# index): offsets[c] = sum of counts[0...c]. Both come off the shared
|
|
104
104
|
# plan, so the counting sort is not repeated here.
|
|
105
105
|
@elements = cat.category_sizes.int64
|
|
106
|
-
nvalid =
|
|
106
|
+
nvalid = counts.sum
|
|
107
107
|
@offsets = cat.reduceat_index # cached segment STARTS (int64[k])
|
|
108
108
|
# Group-major source indices = the valid prefix of the cached sort_addr.
|
|
109
109
|
# With no classified cell the prefix is empty (and slicing a length-0
|
|
@@ -119,25 +119,30 @@ class CACategoricalIterator < CAIterator
|
|
|
119
119
|
# range — so build the empty grouped buffer directly.
|
|
120
120
|
@grouped = nvalid > 0 ? value.reshape(value.elements)[@perm].copy
|
|
121
121
|
: CArray.new(value.data_type, [0])
|
|
122
|
-
@empty = CArray.new(
|
|
122
|
+
@empty = CArray.new(grouped.data_type, [0])
|
|
123
123
|
else
|
|
124
124
|
# Shape mismatch: only per-fiber axis: dispatch could still work. With a
|
|
125
125
|
# 1-D value there is no fiber structure to broadcast into, so a mismatch
|
|
126
126
|
# is unrecoverable (preserves the old strict check). For higher-rank
|
|
127
127
|
# value, defer validation to reduce time — check only that cat.ndim fits
|
|
128
|
-
# one of the 3 axis: cases
|
|
129
|
-
#
|
|
130
|
-
#
|
|
128
|
+
# one of the 3 axis: cases;
|
|
129
|
+
# A no-axis reduce has no grouped buffer to work from. The reason is
|
|
130
|
+
# recorded here and raised from wherever one is asked for, so what
|
|
131
|
+
# surfaces names the mismatch instead of being whatever NoMethodError
|
|
132
|
+
# the nil produced first.
|
|
133
|
+
mismatch = "group_by_category: value.elements (#{value.elements}) != " \
|
|
134
|
+
"cat.elements (#{cat.elements})"
|
|
131
135
|
if value.ndim == 1 ||
|
|
132
136
|
! [1, value.ndim - 1, value.ndim].include?(cat.ndim)
|
|
133
137
|
raise ArgumentError,
|
|
134
|
-
|
|
135
|
-
"cat.elements (#{cat.elements})" +
|
|
138
|
+
mismatch +
|
|
136
139
|
(value.ndim == 1 ? "" :
|
|
137
140
|
". For per-fiber reduce use `.sum(axis: k)`; cat.ndim=" \
|
|
138
141
|
"#{cat.ndim} must be 1 (case A), #{value.ndim} (case B), " \
|
|
139
142
|
"or #{value.ndim - 1} (band-only) for h.ndim=#{value.ndim}.")
|
|
140
143
|
end
|
|
144
|
+
@no_flat = mismatch + ". This iterator answers only the per-fiber form, " \
|
|
145
|
+
"`.<reduce>(axis: k)`."
|
|
141
146
|
end
|
|
142
147
|
self
|
|
143
148
|
end
|
|
@@ -176,7 +181,12 @@ class CACategoricalIterator < CAIterator
|
|
|
176
181
|
# (structural, mask-independent) lifted per group.
|
|
177
182
|
# @return [CArray]
|
|
178
183
|
def elements
|
|
179
|
-
|
|
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
|
|
180
190
|
end
|
|
181
191
|
|
|
182
192
|
# Group-vocabulary alias for {#elements}; reads naturally next to
|
|
@@ -189,8 +199,12 @@ class CACategoricalIterator < CAIterator
|
|
|
189
199
|
# internal grouped/value/codes buffers.
|
|
190
200
|
# @return [String]
|
|
191
201
|
def inspect
|
|
192
|
-
|
|
193
|
-
|
|
202
|
+
# An iterator that answers only the per-fiber form has no per-category
|
|
203
|
+
# counts to show. Saying so beats both raising -- inspect is what you
|
|
204
|
+
# reach for when something is already puzzling -- and printing an empty
|
|
205
|
+
# list, which reads as a grouping that classified nothing.
|
|
206
|
+
tail = @elements ? "elements=#{@elements.to_a.inspect}" : "per-fiber only"
|
|
207
|
+
"#<#{self.class} ngroups=#{@k} labels=#{@labels.inspect} #{tail}>"
|
|
194
208
|
end
|
|
195
209
|
|
|
196
210
|
# @overload count_not_masked
|
|
@@ -205,9 +219,9 @@ class CACategoricalIterator < CAIterator
|
|
|
205
219
|
# @param axis [Integer]
|
|
206
220
|
# @return [CArray]
|
|
207
221
|
def count_not_masked(axis: nil)
|
|
208
|
-
return
|
|
222
|
+
return axis_counts(axis) if axis
|
|
209
223
|
m = moments
|
|
210
|
-
m ? m[:count] : per_category(CA_INT64) { |s| s.count_not_masked }
|
|
224
|
+
m ? m[:count].copy : per_category(CA_INT64) { |s| s.count_not_masked }
|
|
211
225
|
end
|
|
212
226
|
|
|
213
227
|
# @overload count(v = <none>)
|
|
@@ -217,21 +231,20 @@ class CACategoricalIterator < CAIterator
|
|
|
217
231
|
# @return [CArray] length-k int64, aligned to {#labels}
|
|
218
232
|
# @overload count(axis:)
|
|
219
233
|
# No-arg + axis: = per-fiber per-category count_not_masked (shape [K, ...band]).
|
|
220
|
-
# `count(v, axis:)` (value equality) and `count(UNDEF, axis:)` are
|
|
221
|
-
#
|
|
234
|
+
# `count(v, axis:)` (value equality) and `count(UNDEF, axis:)` are not
|
|
235
|
+
# implemented; use them without `axis:`.
|
|
222
236
|
# @param axis [Integer]
|
|
223
237
|
# @return [CArray]
|
|
224
238
|
def count (*args, axis: nil)
|
|
225
239
|
if axis
|
|
226
240
|
return count_not_masked(axis: axis) if args.empty?
|
|
227
241
|
raise NotImplementedError,
|
|
228
|
-
"CACategoricalIterator#count(v, axis:) not
|
|
229
|
-
"value-equality count
|
|
230
|
-
"PROPOSAL_CATEGORICAL_REDUCE_AXIS."
|
|
242
|
+
"CACategoricalIterator#count(v, axis:) is not implemented — " \
|
|
243
|
+
"value-equality count is available without axis:."
|
|
231
244
|
end
|
|
232
245
|
return count_not_masked if args.empty?
|
|
233
246
|
# Delegate per group to CArray#count (handles count(UNDEF) -> masked count and
|
|
234
|
-
# count(v) alike, with core's exact
|
|
247
|
+
# count(v) alike, with core's exact data type equality). The group slice is a
|
|
235
248
|
# CABlock, whose own #count is the block geometry accessor, so dispatch
|
|
236
249
|
# CArray#count explicitly. (Not fused: a value-equality reduceat would have
|
|
237
250
|
# to reproduce core's cross-type / out-of-range equality exactly.)
|
|
@@ -244,70 +257,84 @@ class CACategoricalIterator < CAIterator
|
|
|
244
257
|
# Empty categories are `0`.
|
|
245
258
|
# @return [CArray]
|
|
246
259
|
# @overload count_masked(axis:)
|
|
247
|
-
#
|
|
248
|
-
# "assigned count" scatter that counts cells regardless of value mask).
|
|
260
|
+
# Not implemented; call it without `axis:`.
|
|
249
261
|
# @param axis [Integer]
|
|
250
262
|
# @return [CArray]
|
|
251
263
|
def count_masked(axis: nil)
|
|
252
264
|
if axis
|
|
253
265
|
raise NotImplementedError,
|
|
254
|
-
"CACategoricalIterator#count_masked(axis:) not
|
|
255
|
-
"
|
|
256
|
-
"(needs a separate value-mask-only scatter kernel)."
|
|
266
|
+
"CACategoricalIterator#count_masked(axis:) is not implemented — " \
|
|
267
|
+
"call it without axis:."
|
|
257
268
|
end
|
|
258
269
|
m = moments
|
|
259
|
-
m ?
|
|
270
|
+
m ? counts - m[:count] : per_category(CA_INT64) { |s| s.count_masked }
|
|
260
271
|
end
|
|
261
272
|
|
|
262
273
|
# @overload sum
|
|
263
|
-
# Returns per-category sums in the
|
|
264
|
-
#
|
|
265
|
-
#
|
|
266
|
-
#
|
|
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.
|
|
267
279
|
# @return [CArray]
|
|
268
280
|
# @overload sum(axis:)
|
|
269
281
|
# Returns per-category sums per fiber along `axis`. Cat may be 1-D (case
|
|
270
282
|
# A, broadcasts across band axes), same rank as source (case B, per-fiber
|
|
271
283
|
# independent classifier), or one rank less (band-only, constant along
|
|
272
|
-
# reduce axis). Output shape = `[K, ...source.shape without axis]`.
|
|
273
|
-
# PROPOSAL_CATEGORICAL_REDUCE_AXIS.
|
|
284
|
+
# reduce axis). Output shape = `[K, ...source.shape without axis]`.
|
|
274
285
|
# @param axis [Integer] reduce axis of the source value.
|
|
275
286
|
# @return [CArray]
|
|
276
287
|
def sum(axis: nil)
|
|
277
288
|
return axis_sum(axis) if axis
|
|
278
289
|
m = moments
|
|
279
|
-
return per_category(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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 }
|
|
283
310
|
end
|
|
284
311
|
|
|
285
312
|
# @overload max
|
|
286
|
-
# Returns per-category maxima in the value
|
|
313
|
+
# Returns per-category maxima in the value data type. Empty categories are
|
|
287
314
|
# MASKED.
|
|
288
315
|
# @return [CArray]
|
|
289
316
|
# @overload max(axis:)
|
|
290
|
-
# Per-fiber per-category maxima along `axis` (h
|
|
317
|
+
# Per-fiber per-category maxima along `axis` (h's data type, masked where empty).
|
|
291
318
|
# @param axis [Integer]
|
|
292
319
|
# @return [CArray]
|
|
293
320
|
def max(axis: nil)
|
|
294
321
|
return axis_moments(axis)[:max] if axis
|
|
295
322
|
m = moments
|
|
296
|
-
m ? m[:max] : per_category(
|
|
323
|
+
m ? m[:max].copy : per_category(core_reduce_type(:max)) { |s| s.max }
|
|
297
324
|
end
|
|
298
325
|
|
|
299
326
|
# @overload min
|
|
300
|
-
# Returns per-category minima in the value
|
|
327
|
+
# Returns per-category minima in the value data type. Empty categories are
|
|
301
328
|
# MASKED.
|
|
302
329
|
# @return [CArray]
|
|
303
330
|
# @overload min(axis:)
|
|
304
|
-
# Per-fiber per-category minima along `axis` (h
|
|
331
|
+
# Per-fiber per-category minima along `axis` (h's data type, masked where empty).
|
|
305
332
|
# @param axis [Integer]
|
|
306
333
|
# @return [CArray]
|
|
307
334
|
def min(axis: nil)
|
|
308
335
|
return axis_moments(axis)[:min] if axis
|
|
309
336
|
m = moments
|
|
310
|
-
m ? m[:min] : per_category(
|
|
337
|
+
m ? m[:min].copy : per_category(core_reduce_type(:min)) { |s| s.min }
|
|
311
338
|
end
|
|
312
339
|
|
|
313
340
|
# @overload mean
|
|
@@ -320,7 +347,7 @@ class CACategoricalIterator < CAIterator
|
|
|
320
347
|
def mean(axis: nil)
|
|
321
348
|
return axis_mean(axis) if axis
|
|
322
349
|
m = moments
|
|
323
|
-
return per_category(
|
|
350
|
+
return per_category(core_reduce_type(:mean)) { |s| s.mean } unless m
|
|
324
351
|
cnt = m[:count]
|
|
325
352
|
out = m[:sum] / cnt.float64 # count 0 -> NaN, masked next
|
|
326
353
|
out[cnt.eq(0)] = UNDEF # empty / all-masked category -> MASKED
|
|
@@ -344,11 +371,11 @@ class CACategoricalIterator < CAIterator
|
|
|
344
371
|
# @return [CArray]
|
|
345
372
|
def percentile (p, axis: nil)
|
|
346
373
|
axis_order_stat_defer!(:percentile) if axis
|
|
347
|
-
unless MONOID_TYPES.include?(
|
|
348
|
-
return per_category(
|
|
374
|
+
unless MONOID_TYPES.include?(grouped.data_type)
|
|
375
|
+
return per_category(core_reduce_type(:percentile, p)) { |s| s.percentile(p) }
|
|
349
376
|
end
|
|
350
377
|
out = CArray.float64(@k)
|
|
351
|
-
|
|
378
|
+
grouped.send(:__reduceat_percentile__, @offsets, p.to_f, out)
|
|
352
379
|
out
|
|
353
380
|
end
|
|
354
381
|
|
|
@@ -359,11 +386,11 @@ class CACategoricalIterator < CAIterator
|
|
|
359
386
|
# MASKED. For a single fraction q in 0..1 use `percentile(q * 100)`.
|
|
360
387
|
# @return [Array<CArray>]
|
|
361
388
|
def quantile
|
|
362
|
-
unless MONOID_TYPES.include?(
|
|
389
|
+
unless MONOID_TYPES.include?(grouped.data_type)
|
|
363
390
|
return [0, 25, 50, 75, 100].map { |p| percentile(p) }
|
|
364
391
|
end
|
|
365
392
|
outs = Array.new(5) { CArray.float64(@k) }
|
|
366
|
-
|
|
393
|
+
grouped.send(:__reduceat_quantile__, @offsets, *outs)
|
|
367
394
|
outs
|
|
368
395
|
end
|
|
369
396
|
|
|
@@ -374,13 +401,13 @@ class CACategoricalIterator < CAIterator
|
|
|
374
401
|
# sample variance.
|
|
375
402
|
# @return [CArray]
|
|
376
403
|
def variance(axis: nil)
|
|
377
|
-
return
|
|
404
|
+
return axis_by_masked_copy(axis, :variance) if axis
|
|
378
405
|
m = moments
|
|
379
|
-
return per_category(
|
|
406
|
+
return per_category(core_reduce_type(:variance)) { |s| s.variance } unless m
|
|
380
407
|
cnt = m[:count]
|
|
381
408
|
means = m[:sum] / cnt.float64 # per-segment mean (garbage where count 0/1,
|
|
382
409
|
out = CArray.float64(@k) # ignored by the kernel's n<2 guards)
|
|
383
|
-
|
|
410
|
+
grouped.send(:__reduceat_variance__, @offsets, means, cnt, out)
|
|
384
411
|
out
|
|
385
412
|
end
|
|
386
413
|
|
|
@@ -390,9 +417,9 @@ class CACategoricalIterator < CAIterator
|
|
|
390
417
|
# single-value `0.0`).
|
|
391
418
|
# @return [CArray]
|
|
392
419
|
def stddev(axis: nil)
|
|
393
|
-
return
|
|
420
|
+
return axis_by_masked_copy(axis, :stddev) if axis
|
|
394
421
|
m = moments
|
|
395
|
-
return per_category(
|
|
422
|
+
return per_category(core_reduce_type(:stddev)) { |s| s.stddev } unless m
|
|
396
423
|
variance.sqrt # sqrt propagates the n=0 mask
|
|
397
424
|
end
|
|
398
425
|
|
|
@@ -408,16 +435,16 @@ class CACategoricalIterator < CAIterator
|
|
|
408
435
|
# @return [CArray]
|
|
409
436
|
def prod(axis: nil)
|
|
410
437
|
return axis_prod(axis) if axis
|
|
411
|
-
return per_category(
|
|
438
|
+
return per_category(core_reduce_type(:prod)) { |s| s.prod } unless MONOID_TYPES.include?(grouped.data_type)
|
|
412
439
|
out = CArray.float64(@k)
|
|
413
|
-
|
|
440
|
+
grouped.send(:__reduceat_prod__, @offsets, out)
|
|
414
441
|
out
|
|
415
442
|
end
|
|
416
443
|
|
|
417
444
|
# @overload all
|
|
418
445
|
# Returns the per-category `all` as boolean (matching `CArray#all`): true
|
|
419
446
|
# iff every present value is truthy (empty category -> true, vacuously).
|
|
420
|
-
# The value
|
|
447
|
+
# The value data type must be boolean, as for `CArray#all`.
|
|
421
448
|
# @return [CArray]
|
|
422
449
|
def all
|
|
423
450
|
aa = all_any
|
|
@@ -427,7 +454,7 @@ class CACategoricalIterator < CAIterator
|
|
|
427
454
|
# @overload any
|
|
428
455
|
# Returns the per-category `any` as boolean (matching `CArray#any`): true
|
|
429
456
|
# iff some present value is truthy (empty category -> false). The value
|
|
430
|
-
#
|
|
457
|
+
# data type must be boolean, as for `CArray#any`.
|
|
431
458
|
# @return [CArray]
|
|
432
459
|
def any
|
|
433
460
|
aa = all_any
|
|
@@ -438,16 +465,23 @@ class CACategoricalIterator < CAIterator
|
|
|
438
465
|
|
|
439
466
|
# @overload minmax
|
|
440
467
|
# Returns the per-category `[min, max]` pair (each a length-k CArray in the
|
|
441
|
-
# value
|
|
442
|
-
# from
|
|
468
|
+
# value data type; empty categories MASKED), matching `CArray#minmax`. Both come
|
|
469
|
+
# from one moments pass.
|
|
443
470
|
# @return [Array<CArray>]
|
|
444
471
|
# @overload minmax(axis:)
|
|
445
|
-
# Per-fiber `[min_ca, max_ca]` along `axis` (each shape [K, ...band], h
|
|
472
|
+
# Per-fiber `[min_ca, max_ca]` along `axis` (each shape [K, ...band], h's data type,
|
|
446
473
|
# empty group cells MASKED). Ruby Array of two CArrays, not stacked.
|
|
474
|
+
# Both come from one kernel run.
|
|
447
475
|
# @param axis [Integer]
|
|
448
476
|
# @return [Array<CArray>]
|
|
449
477
|
def minmax(axis: nil)
|
|
450
|
-
|
|
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
|
|
451
485
|
[min, max]
|
|
452
486
|
end
|
|
453
487
|
|
|
@@ -458,9 +492,9 @@ class CACategoricalIterator < CAIterator
|
|
|
458
492
|
# reuses the centred two-pass kernel with no extra walk.
|
|
459
493
|
# @return [CArray]
|
|
460
494
|
def variancep(axis: nil)
|
|
461
|
-
return
|
|
495
|
+
return axis_by_masked_copy(axis, :variancep) if axis
|
|
462
496
|
m = moments
|
|
463
|
-
return per_category(
|
|
497
|
+
return per_category(core_reduce_type(:variancep)) { |s| s.variancep } unless m
|
|
464
498
|
cnt = m[:count]
|
|
465
499
|
vp = variance * (cnt - 1).float64 / cnt.float64
|
|
466
500
|
vp[cnt.eq(0)] = UNDEF # empty / all-masked stays masked
|
|
@@ -475,9 +509,9 @@ class CACategoricalIterator < CAIterator
|
|
|
475
509
|
# @param axis [Integer]
|
|
476
510
|
# @return [CArray]
|
|
477
511
|
def stddevp(axis: nil)
|
|
478
|
-
return
|
|
512
|
+
return axis_by_masked_copy(axis, :stddevp) if axis
|
|
479
513
|
m = moments
|
|
480
|
-
return per_category(
|
|
514
|
+
return per_category(core_reduce_type(:stddevp)) { |s| s.stddevp } unless m
|
|
481
515
|
variancep.sqrt
|
|
482
516
|
end
|
|
483
517
|
|
|
@@ -489,7 +523,7 @@ class CACategoricalIterator < CAIterator
|
|
|
489
523
|
# @return [CArray] length-k int64
|
|
490
524
|
def min_index
|
|
491
525
|
am = arg_minmax
|
|
492
|
-
am ? am[:min] : per_category(CA_INT64) { |s| s.min_index }
|
|
526
|
+
am ? am[:min].copy : per_category(CA_INT64) { |s| s.min_index }
|
|
493
527
|
end
|
|
494
528
|
|
|
495
529
|
# @overload max_index
|
|
@@ -497,7 +531,7 @@ class CACategoricalIterator < CAIterator
|
|
|
497
531
|
# @return [CArray] length-k int64
|
|
498
532
|
def max_index
|
|
499
533
|
am = arg_minmax
|
|
500
|
-
am ? am[:max] : per_category(CA_INT64) { |s| s.max_index }
|
|
534
|
+
am ? am[:max].copy : per_category(CA_INT64) { |s| s.max_index }
|
|
501
535
|
end
|
|
502
536
|
|
|
503
537
|
# @overload min_addr
|
|
@@ -536,14 +570,14 @@ class CACategoricalIterator < CAIterator
|
|
|
536
570
|
# {#min_addr} vs the skipped group-local min_index-into-source.
|
|
537
571
|
# @return [CArray] length-nvalid int64
|
|
538
572
|
def sort_addr
|
|
539
|
-
out = CArray.int64(
|
|
573
|
+
out = CArray.int64(grouped.elements)
|
|
540
574
|
@k.times do |c|
|
|
541
575
|
lo = @offsets[c]
|
|
542
|
-
hi = (c + 1 < @k) ? @offsets[c + 1] :
|
|
576
|
+
hi = (c + 1 < @k) ? @offsets[c + 1] : grouped.elements
|
|
543
577
|
next unless hi > lo
|
|
544
578
|
# View-local sort order of the segment (0..size-1), lifted to grouped
|
|
545
579
|
# slots, then mapped back to source addresses via perm.
|
|
546
|
-
out[lo...hi] = perm[
|
|
580
|
+
out[lo...hi] = perm[grouped[lo...hi].sort_addr + lo]
|
|
547
581
|
end
|
|
548
582
|
out
|
|
549
583
|
end
|
|
@@ -567,7 +601,7 @@ class CACategoricalIterator < CAIterator
|
|
|
567
601
|
def wsum (weights, axis: nil)
|
|
568
602
|
return axis_wsum_wmean(weights, axis)[0] if axis
|
|
569
603
|
wg = scatter_weights(weights)
|
|
570
|
-
return kernel_weighted(wg)[0] if MONOID_TYPES.include?(
|
|
604
|
+
return kernel_weighted(wg)[0] if MONOID_TYPES.include?(grouped.data_type)
|
|
571
605
|
fold_weighted(wg, 0.0) { |v, ws| v.wsum(ws) }
|
|
572
606
|
end
|
|
573
607
|
|
|
@@ -587,7 +621,7 @@ class CACategoricalIterator < CAIterator
|
|
|
587
621
|
def wmean (weights, axis: nil)
|
|
588
622
|
return axis_wsum_wmean(weights, axis)[1] if axis
|
|
589
623
|
wg = scatter_weights(weights)
|
|
590
|
-
return kernel_weighted(wg)[1] if MONOID_TYPES.include?(
|
|
624
|
+
return kernel_weighted(wg)[1] if MONOID_TYPES.include?(grouped.data_type)
|
|
591
625
|
fold_weighted(wg, UNDEF) { |v, ws| v.wmean(ws) }
|
|
592
626
|
end
|
|
593
627
|
|
|
@@ -628,14 +662,14 @@ class CACategoricalIterator < CAIterator
|
|
|
628
662
|
# @return [CArray] shaped like the source value
|
|
629
663
|
def map (data_type: nil)
|
|
630
664
|
raise LocalJumpError, "no block given (yield)" unless block_given?
|
|
631
|
-
dt = data_type ||
|
|
665
|
+
dt = data_type || grouped.data_type
|
|
632
666
|
# Apply the block per category, assembled in grouped (category-contiguous)
|
|
633
667
|
# order: a same-length result scatters cell for cell, a scalar broadcasts.
|
|
634
|
-
transformed = CArray.new(dt, [
|
|
668
|
+
transformed = CArray.new(dt, [grouped.elements])
|
|
635
669
|
@k.times do |c|
|
|
636
670
|
lo = @offsets[c]
|
|
637
|
-
hi = (c + 1 < @k) ? @offsets[c + 1] :
|
|
638
|
-
transformed[lo...hi] = yield(
|
|
671
|
+
hi = (c + 1 < @k) ? @offsets[c + 1] : grouped.elements
|
|
672
|
+
transformed[lo...hi] = yield(grouped[lo...hi]) if hi > lo
|
|
639
673
|
end
|
|
640
674
|
# Scatter back to source positions via the permutation (grouped-order source
|
|
641
675
|
# indices). Excluded cells are absent from perm and stay UNDEF.
|
|
@@ -660,22 +694,22 @@ class CACategoricalIterator < CAIterator
|
|
|
660
694
|
# Excluded (out-of-vocabulary / masked-code) and source-masked cells join no
|
|
661
695
|
# running total and are UNDEF. Mirroring the reductions (sum / mean), a scan
|
|
662
696
|
# takes no axis argument. cumsum / cumprod -> float64, cummax / cummin
|
|
663
|
-
# preserve the value
|
|
664
|
-
# ordinal); an object value
|
|
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.
|
|
665
699
|
|
|
666
|
-
#
|
|
700
|
+
# @!method cumsum
|
|
667
701
|
# Per-category inclusive running sum (float64), source-shaped.
|
|
668
702
|
# @return [CArray]
|
|
669
|
-
#
|
|
703
|
+
# @!method cumprod
|
|
670
704
|
# Per-category inclusive running product (float64), source-shaped.
|
|
671
705
|
# @return [CArray]
|
|
672
|
-
#
|
|
673
|
-
# Per-category inclusive running maximum (value
|
|
706
|
+
# @!method cummax
|
|
707
|
+
# Per-category inclusive running maximum (value data type), source-shaped.
|
|
674
708
|
# @return [CArray]
|
|
675
|
-
#
|
|
676
|
-
# Per-category inclusive running minimum (value
|
|
709
|
+
# @!method cummin
|
|
710
|
+
# Per-category inclusive running minimum (value data type), source-shaped.
|
|
677
711
|
# @return [CArray]
|
|
678
|
-
#
|
|
712
|
+
# @!method cumcount
|
|
679
713
|
# Per-category 1-based within-category ordinal (int64), source-shaped.
|
|
680
714
|
# @return [CArray]
|
|
681
715
|
[:cumsum, :cumprod, :cummax, :cummin, :cumcount].each do |op|
|
|
@@ -684,16 +718,63 @@ class CACategoricalIterator < CAIterator
|
|
|
684
718
|
|
|
685
719
|
private
|
|
686
720
|
|
|
687
|
-
#
|
|
688
|
-
# the
|
|
689
|
-
#
|
|
690
|
-
#
|
|
691
|
-
|
|
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)
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
# Per-category cell counts, alongside #grouped and unavailable for the same
|
|
730
|
+
# reason.
|
|
731
|
+
def counts
|
|
732
|
+
@elements || raise(ArgumentError, @no_flat)
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
# Axis-aware count of present cells. Counting how many cells fall in a
|
|
736
|
+
# group does not look at what is in them, so it is taken from the codes and
|
|
737
|
+
# the value's mask rather than from the fused moments kernel, which is
|
|
738
|
+
# numeric-only and refused a complex, boolean or object payload for an
|
|
739
|
+
# answer that never depended on the payload.
|
|
740
|
+
def axis_counts (axis)
|
|
741
|
+
h = @value
|
|
742
|
+
unless axis.is_a?(Integer) && axis >= 0 && axis < h.ndim
|
|
743
|
+
raise ArgumentError,
|
|
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
|
|
747
|
+
full_c = resolve_axis_codes(@cat.codes, h.shape, axis)
|
|
748
|
+
band = h.shape.dup; band.delete_at(axis)
|
|
749
|
+
out = CArray.int64(*([@k] + band))
|
|
750
|
+
present = h.has_mask? ? h.is_not_masked : nil
|
|
751
|
+
slot = [nil] * (band.size + 1)
|
|
752
|
+
@k.times do |c|
|
|
753
|
+
# a masked code belongs to no group, and eq yields UNDEF there
|
|
754
|
+
belongs = full_c.eq(c)
|
|
755
|
+
belongs = belongs.strip_mask(false) if belongs.has_mask?
|
|
756
|
+
belongs = belongs & present if present
|
|
757
|
+
slot[0] = c
|
|
758
|
+
out[*slot] = belongs.int64.sum(axis: axis)
|
|
759
|
+
end
|
|
760
|
+
out
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
# Axis-aware moments (count / sum / min / max) via the fused per-fiber
|
|
764
|
+
# scatter-reduce C kernel. Returns
|
|
765
|
+
# `{count: <int64>, sum: <float64>, min: <h's type, masked>, max: <h's type, masked>}`,
|
|
692
766
|
# all shape [K, ...band].
|
|
767
|
+
#
|
|
768
|
+
# Read fresh on every call, deliberately. It used to be kept per axis, and
|
|
769
|
+
# since the rest of the axis: family (prod, the variance family, wsum /
|
|
770
|
+
# wmean) reads the source when asked, half of the family answered about the
|
|
771
|
+
# values as they were and half about the values as they are. Writing
|
|
772
|
+
# through another view of the source between two calls got you a mean of 2.0
|
|
773
|
+
# beside a variance of 4704.5 for the same cell, which is not a pair any
|
|
774
|
+
# data can produce. The axis: path materialises nothing else, so holding
|
|
775
|
+
# this one thing was the odd choice; a caller who wants a fused kernel's
|
|
776
|
+
# four answers shares them by keeping the result.
|
|
693
777
|
def axis_moments (axis)
|
|
694
|
-
@axis_moments_cache ||= {}
|
|
695
|
-
cached = @axis_moments_cache[axis]
|
|
696
|
-
return cached if cached
|
|
697
778
|
h = @value
|
|
698
779
|
unless axis.is_a?(Integer) && axis >= 0 && axis < h.ndim
|
|
699
780
|
raise ArgumentError,
|
|
@@ -709,16 +790,13 @@ class CACategoricalIterator < CAIterator
|
|
|
709
790
|
maxs = CArray.new(h.data_type, out_shape)
|
|
710
791
|
h.__send__(:__fiber_scatter_moments__, codes_h_shape, axis, @k,
|
|
711
792
|
counts, sums, mins, maxs)
|
|
712
|
-
|
|
793
|
+
{count: counts, sum: sums, min: mins, max: maxs}
|
|
713
794
|
end
|
|
714
795
|
|
|
715
|
-
# Axis-aware sum:
|
|
716
|
-
#
|
|
796
|
+
# Axis-aware sum: the moments sum is already the core fold in the core's own
|
|
797
|
+
# type, so it is handed back as is (an empty group cell carries identity 0.0).
|
|
717
798
|
def axis_sum (axis)
|
|
718
|
-
|
|
719
|
-
out = CArray.new(@value.data_type, m[:sum].shape)
|
|
720
|
-
out[] = m[:sum]
|
|
721
|
-
out
|
|
799
|
+
axis_moments(axis)[:sum]
|
|
722
800
|
end
|
|
723
801
|
|
|
724
802
|
# Axis-aware mean: sums / counts (float64); empty group cells (count=0) MASKED.
|
|
@@ -731,17 +809,19 @@ class CACategoricalIterator < CAIterator
|
|
|
731
809
|
out
|
|
732
810
|
end
|
|
733
811
|
|
|
734
|
-
# Axis-aware
|
|
735
|
-
#
|
|
736
|
-
#
|
|
737
|
-
#
|
|
738
|
-
#
|
|
739
|
-
#
|
|
812
|
+
# Axis-aware reduction by masked copy — Ruby-level per-c mask, then delegate
|
|
813
|
+
# to the source's own axis-aware kernel, so the core contract for `op` rides
|
|
814
|
+
# unchanged. Used by the variance family (a centred two-pass numeric
|
|
815
|
+
# aggregate, hitting the same ε-close kernel per (group, axis) that
|
|
816
|
+
# CArray#variance uses) and by `accumulate` (whose in-type wrapping fold has
|
|
817
|
+
# no float64 moment to read it off). Order (median / percentile / quantile)
|
|
818
|
+
# is genuinely order-statistical (needs a sort per group) and remains
|
|
819
|
+
# deferred.
|
|
740
820
|
#
|
|
741
821
|
# Cost: K axis-reductions over an h-shaped local (most cells masked away for
|
|
742
822
|
# each c) — bounded by K, typically small. A fused per-fiber variance
|
|
743
823
|
# kernel is a natural follow-on if bench demands it.
|
|
744
|
-
def
|
|
824
|
+
def axis_by_masked_copy (axis, op, out_data_type = CA_FLOAT64)
|
|
745
825
|
h = @value
|
|
746
826
|
unless axis.is_a?(Integer) && axis >= 0 && axis < h.ndim
|
|
747
827
|
raise ArgumentError,
|
|
@@ -750,7 +830,7 @@ class CACategoricalIterator < CAIterator
|
|
|
750
830
|
end
|
|
751
831
|
full_c = resolve_axis_codes(@cat.codes, h.shape, axis)
|
|
752
832
|
band = h.shape.dup; band.delete_at(axis)
|
|
753
|
-
out = CArray.
|
|
833
|
+
out = CArray.new(out_data_type, [@k] + band)
|
|
754
834
|
slot_idx = [nil] + [nil] * band.size # placeholder; c fills slot 0
|
|
755
835
|
codes_bad = full_c.has_mask? ? full_c.is_masked : nil
|
|
756
836
|
@k.times do |c|
|
|
@@ -819,22 +899,20 @@ class CACategoricalIterator < CAIterator
|
|
|
819
899
|
def resolve_axis_codes (codes, h_shape, axis)
|
|
820
900
|
ndim = h_shape.size
|
|
821
901
|
band = h_shape.dup; band.delete_at(axis)
|
|
822
|
-
case
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
902
|
+
# Chosen by shape, not by rank. For a 2-D source the case A shape and the
|
|
903
|
+
# band-only shape are both rank 1, so choosing by rank took case A every
|
|
904
|
+
# time and band-only could never be reached there -- while the refusal
|
|
905
|
+
# went on to list the very shape it was refusing among the ones it
|
|
906
|
+
# accepts. When both fit, which a square source makes possible, case A
|
|
907
|
+
# wins: classifying along the reduce axis is the reading that holds at
|
|
908
|
+
# every rank.
|
|
909
|
+
case
|
|
910
|
+
when codes.shape == [h_shape[axis]] # case A
|
|
827
911
|
view_shape = Array.new(ndim, 1); view_shape[axis] = h_shape[axis]
|
|
828
912
|
codes.reshape(*view_shape).broadcast_to(*h_shape)
|
|
829
|
-
when
|
|
830
|
-
unless codes.shape == h_shape
|
|
831
|
-
axis_shape_mismatch!(codes.shape, h_shape, axis, band)
|
|
832
|
-
end
|
|
913
|
+
when codes.shape == h_shape # case B
|
|
833
914
|
codes
|
|
834
|
-
when
|
|
835
|
-
unless codes.shape == band
|
|
836
|
-
axis_shape_mismatch!(codes.shape, h_shape, axis, band)
|
|
837
|
-
end
|
|
915
|
+
when codes.shape == band # band-only
|
|
838
916
|
view_shape = h_shape.dup; view_shape[axis] = 1
|
|
839
917
|
codes.reshape(*view_shape).broadcast_to(*h_shape)
|
|
840
918
|
else
|
|
@@ -843,8 +921,11 @@ class CACategoricalIterator < CAIterator
|
|
|
843
921
|
end
|
|
844
922
|
|
|
845
923
|
def axis_shape_mismatch! (cat_shape, h_shape, axis, band)
|
|
924
|
+
# No method name: the one place that resolves this serves sum, mean, min,
|
|
925
|
+
# max, count and the rest alike, and naming one of them would be wrong for
|
|
926
|
+
# the others. The backtrace says which was called.
|
|
846
927
|
raise ArgumentError,
|
|
847
|
-
"group_by_category
|
|
928
|
+
"group_by_category (axis: #{axis}): cat.shape=#{cat_shape.inspect} " \
|
|
848
929
|
"does not fit any of the 3 accepted forms for h.shape=#{h_shape.inspect}: " \
|
|
849
930
|
"case A cat.shape=[#{h_shape[axis]}], " \
|
|
850
931
|
"case B cat.shape=#{h_shape.inspect}, " \
|
|
@@ -855,18 +936,36 @@ class CACategoricalIterator < CAIterator
|
|
|
855
936
|
# Called from median / percentile / variance / stddev when axis: is given.
|
|
856
937
|
def axis_order_stat_defer! (op)
|
|
857
938
|
raise NotImplementedError,
|
|
858
|
-
"CACategoricalIterator##{op}(axis:) not
|
|
859
|
-
"statistics
|
|
860
|
-
"(needs per-fiber counting-sort C kernel, tracked separately)."
|
|
939
|
+
"CACategoricalIterator##{op}(axis:) is not implemented — order " \
|
|
940
|
+
"statistics are available without axis:."
|
|
861
941
|
end
|
|
862
942
|
|
|
863
943
|
# Drive a segment scan through the axis-group scan kernel: the whole value as
|
|
864
944
|
# one grouped axis, the flat codes as the single bundle. The kernel emits in
|
|
865
945
|
# source order, so the flat result reshapes straight back to the source shape.
|
|
866
946
|
def scan (op)
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
|
870
969
|
end
|
|
871
970
|
|
|
872
971
|
|
|
@@ -889,7 +988,7 @@ class CACategoricalIterator < CAIterator
|
|
|
889
988
|
"value.elements (#{@codes.elements})"
|
|
890
989
|
end
|
|
891
990
|
wf = weights.float64
|
|
892
|
-
wg = CArray.float64(
|
|
991
|
+
wg = CArray.float64(grouped.elements)
|
|
893
992
|
@codes.send(:__categorical_scatter__, wf.reshape(wf.elements),
|
|
894
993
|
@offsets.copy, wg, @k)
|
|
895
994
|
wg
|
|
@@ -910,22 +1009,22 @@ class CACategoricalIterator < CAIterator
|
|
|
910
1009
|
|
|
911
1010
|
# Fused per-segment weighted sum + weighted mean (one C pass over the grouped
|
|
912
1011
|
# copy, weights in group order). Returns [wsum, wmean]; wmean is masked where a
|
|
913
|
-
# segment has no present (value AND weight) pair. Numeric value
|
|
1012
|
+
# segment has no present (value AND weight) pair. Numeric value data types only.
|
|
914
1013
|
def kernel_weighted (wg)
|
|
915
1014
|
ws = CArray.float64(@k)
|
|
916
1015
|
wm = CArray.float64(@k)
|
|
917
|
-
|
|
1016
|
+
grouped.send(:__reduceat_wsum_wmean__, @offsets, wg, ws, wm)
|
|
918
1017
|
[ws, wm]
|
|
919
1018
|
end
|
|
920
1019
|
|
|
921
|
-
# Per-group weighted fallback for non-numeric value
|
|
1020
|
+
# Per-group weighted fallback for non-numeric value data types (complex): delegate
|
|
922
1021
|
# each group to CArray#wsum / #wmean. Empty segments take the given identity.
|
|
923
1022
|
def fold_weighted (wg, empty)
|
|
924
1023
|
out = CArray.float64(@k)
|
|
925
1024
|
@k.times do |c|
|
|
926
1025
|
lo = @offsets[c]
|
|
927
|
-
hi = (c + 1 < @k) ? @offsets[c + 1] :
|
|
928
|
-
out[c] = hi > lo ? yield(
|
|
1026
|
+
hi = (c + 1 < @k) ? @offsets[c + 1] : grouped.elements
|
|
1027
|
+
out[c] = hi > lo ? yield(grouped[lo...hi], wg[lo...hi]) : empty
|
|
929
1028
|
end
|
|
930
1029
|
out
|
|
931
1030
|
end
|
|
@@ -936,16 +1035,16 @@ class CACategoricalIterator < CAIterator
|
|
|
936
1035
|
# contract we want (identity for sum, UNDEF for ratios).
|
|
937
1036
|
def group_slice (c)
|
|
938
1037
|
lo = @offsets[c]
|
|
939
|
-
hi = (c + 1 < @k) ? @offsets[c + 1] :
|
|
940
|
-
hi > lo ?
|
|
1038
|
+
hi = (c + 1 < @k) ? @offsets[c + 1] : grouped.elements
|
|
1039
|
+
hi > lo ? grouped[lo...hi] : @empty
|
|
941
1040
|
end
|
|
942
1041
|
|
|
943
1042
|
# Single-pass reduceat moments (count / sum / min / max per category), computed
|
|
944
1043
|
# once over the grouped copy and cached — the whole point of the eager copy is
|
|
945
1044
|
# that one scatter is followed by cheap single-pass reductions with no
|
|
946
|
-
# per-segment views. Nil for a non-numeric value
|
|
1045
|
+
# per-segment views. Nil for a non-numeric value data type (complex / object /
|
|
947
1046
|
# bool), where the monoid reductions fall back to per_category.
|
|
948
|
-
# numeric value
|
|
1047
|
+
# numeric value data types the C moments kernel handles (int8..float64); bool /
|
|
949
1048
|
# complex / object fall back to per_category.
|
|
950
1049
|
MONOID_TYPES = %i[int8 uint8 int16 uint16 int32 uint32
|
|
951
1050
|
int64 uint64 float32 float64].freeze
|
|
@@ -953,53 +1052,103 @@ class CACategoricalIterator < CAIterator
|
|
|
953
1052
|
def moments
|
|
954
1053
|
return @moments if defined?(@moments)
|
|
955
1054
|
@moments =
|
|
956
|
-
if MONOID_TYPES.include?(
|
|
957
|
-
dt =
|
|
1055
|
+
if MONOID_TYPES.include?(grouped.data_type)
|
|
1056
|
+
dt = grouped.data_type
|
|
958
1057
|
counts = CArray.int64(@k)
|
|
959
1058
|
sums = CArray.float64(@k)
|
|
960
1059
|
mins = CArray.new(dt, [@k])
|
|
961
1060
|
maxs = CArray.new(dt, [@k])
|
|
962
|
-
|
|
1061
|
+
grouped.send(:__reduceat_moments__, @offsets, counts, sums, mins, maxs)
|
|
963
1062
|
{ count: counts, sum: sums, min: mins, max: maxs }
|
|
964
1063
|
end
|
|
965
1064
|
end
|
|
966
1065
|
|
|
967
1066
|
# Single-pass fused group-local argmin / argmax (min_index / max_index),
|
|
968
|
-
# cached. Nil for a non-numeric value
|
|
1067
|
+
# cached. Nil for a non-numeric value data type (fall back to per_category).
|
|
969
1068
|
def arg_minmax
|
|
970
1069
|
return @arg_minmax if defined?(@arg_minmax)
|
|
971
1070
|
@arg_minmax =
|
|
972
|
-
if MONOID_TYPES.include?(
|
|
1071
|
+
if MONOID_TYPES.include?(grouped.data_type)
|
|
973
1072
|
mn = CArray.int64(@k)
|
|
974
1073
|
mx = CArray.int64(@k)
|
|
975
|
-
|
|
1074
|
+
grouped.send(:__reduceat_argminmax__, @offsets, mn, mx)
|
|
976
1075
|
{ min: mn, max: mx }
|
|
977
1076
|
end
|
|
978
1077
|
end
|
|
979
1078
|
|
|
980
1079
|
# Single-pass fused per-category boolean all / any, cached. Nil unless the
|
|
981
|
-
# value
|
|
1080
|
+
# value data type is boolean (fall back to per_category, which raises like
|
|
982
1081
|
# CArray#all on a non-boolean).
|
|
983
1082
|
def all_any
|
|
984
1083
|
return @all_any if defined?(@all_any)
|
|
985
1084
|
@all_any =
|
|
986
|
-
if
|
|
1085
|
+
if grouped.data_type == CA_BOOLEAN
|
|
987
1086
|
a = CArray.boolean(@k)
|
|
988
1087
|
o = CArray.boolean(@k)
|
|
989
|
-
|
|
1088
|
+
grouped.send(:__reduceat_all_any__, @offsets, a, o)
|
|
990
1089
|
{ all: a, any: o }
|
|
991
1090
|
end
|
|
992
1091
|
end
|
|
993
1092
|
|
|
994
1093
|
# Build a length-k typed output by folding each category's members with the
|
|
995
1094
|
# given reduction block. Fallback path (order statistics, and monoids on a
|
|
996
|
-
# non-numeric value
|
|
1095
|
+
# non-numeric value data type): each group is delegated to the same CArray
|
|
997
1096
|
# reduction, so the per-group result matches `CArray#<reduction>` over that
|
|
998
1097
|
# group's members — the mask carries the "insufficient present data" contract
|
|
999
1098
|
# for free (an all-masked group reduces like an empty one; identity-bearing
|
|
1000
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
|
+
|
|
1001
1136
|
def per_category (data_type)
|
|
1002
|
-
out =
|
|
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.
|
|
1003
1152
|
@k.times { |c| out[c] = yield(group_slice(c)) }
|
|
1004
1153
|
out
|
|
1005
1154
|
end
|