carray 3.0.1 → 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/CHANGELOG.md +520 -0
- data/README.md +2 -2
- data/carray.gemspec +1 -1
- data/ext/ca_axis_dispatch.c +33 -4
- data/ext/ca_axis_group.c +202 -96
- data/ext/ca_categorical_iterator.c +108 -54
- data/ext/ca_kernel_iterator.c +317 -51
- data/ext/ca_kernel_iterator.h +142 -35
- data/ext/ca_obj_array.c +62 -20
- data/ext/ca_obj_block.c +4 -4
- data/ext/ca_obj_const_string.c +85 -26
- data/ext/ca_obj_face.c +24 -0
- data/ext/ca_obj_face.h +15 -0
- data/ext/ca_obj_fixlen_string.c +18 -5
- data/ext/ca_obj_meld.c +123 -25
- data/ext/ca_obj_object.c +8 -0
- data/ext/ca_obj_select.c +49 -34
- data/ext/ca_obj_stack.c +3 -8
- data/ext/ca_obj_stride.c +72 -1
- data/ext/ca_obj_string.c +8 -4
- data/ext/ca_obj_window.c +8 -2
- data/ext/ca_op_ipower.c +1 -2
- data/ext/ca_rng_normal.h +42 -0
- data/ext/ca_rng_xoshiro256pp.h +105 -0
- data/ext/ca_sweep_engine.c +307 -143
- data/ext/ca_sweep_engine.h +26 -5
- data/ext/carray.h +21 -2
- data/ext/carray_access.c +32 -20
- data/ext/carray_address_basis.c +590 -0
- data/ext/carray_broadcast.c +3 -3
- data/ext/carray_call_cfunc.c +667 -483
- data/ext/carray_cast.c +115 -41
- data/ext/carray_copy.c +55 -30
- data/ext/carray_core.c +83 -3
- data/ext/carray_count.c +9 -10
- data/ext/carray_factorize.c +46 -25
- data/ext/carray_internal.h +17 -0
- data/ext/carray_kernels_reduce_aggregate.c +168 -0
- data/ext/carray_kernels_reduce_cumulative.c +270 -1
- data/ext/carray_kernels_reduce_extreme.c +554 -8
- data/ext/carray_kernels_scan.c +4 -4
- data/ext/carray_kernels_search.c +94 -14
- data/ext/carray_loop.c +7 -1
- data/ext/carray_mask.c +23 -8
- data/ext/carray_median_percentile.c +55 -0
- data/ext/carray_operator.c +4 -4
- data/ext/carray_order.c +1 -1
- data/ext/carray_random.c +384 -40
- data/ext/carray_slab.c +13 -0
- data/ext/carray_sort.c +20 -22
- data/ext/mk_call_cfunc.rb +103 -116
- data/ext/mkkernel.rb +297 -29
- data/ext/ruby_carray.c +10 -1
- data/ext/version.h +4 -4
- data/lib/carray/autoload_carray.rb +5 -3
- data/lib/carray/autoload_method_extension.rb +12 -0
- data/lib/carray/axis_group.rb +77 -0
- data/lib/carray/basics.rb +4 -0
- data/lib/carray/block_iterator.rb +92 -16
- data/lib/carray/categorical.rb +150 -33
- data/lib/carray/categorical_iterator.rb +207 -80
- data/lib/carray/const_string.rb +131 -27
- data/lib/carray/construct.rb +40 -0
- data/lib/carray/data_type_extension.rb +3 -0
- data/lib/carray/data_type_limits.rb +91 -0
- data/lib/carray/fixlen_string.rb +1 -1
- data/lib/carray/frame/csv_parser.rb +11 -4
- data/lib/carray/frame/frame.rb +81 -10
- data/lib/carray/frame/group.rb +36 -3
- data/lib/carray/frame/io.rb +67 -15
- data/lib/carray/frame/records.rb +18 -4
- data/lib/carray/frame/verbs.rb +14 -11
- data/lib/carray/inspect.rb +42 -9
- data/lib/carray/iterator.rb +143 -0
- data/lib/carray/lazy.rb +0 -37
- data/lib/carray/mask_gap_fill.rb +3 -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 +13 -2
- data/lib/carray/methods/locate_addr.rb +75 -2
- data/lib/carray/methods/mask_duplicates.rb +35 -1
- data/lib/carray/methods/nunique.rb +22 -1
- data/lib/carray/methods/repeat.rb +110 -0
- data/lib/carray/methods/unique.rb +41 -1
- data/lib/carray/rng.rb +86 -0
- data/lib/carray/slab_iterator.rb +58 -13
- data/lib/carray/string_operation_extension.rb +5 -1
- data/lib/carray/time.rb +18 -2
- data/lib/carray/window_iterator.rb +142 -20
- data/lib/carray.rb +2 -0
- data/yard-stubs/ca_obj_block.rb +2 -7
- data/yard-stubs/ca_obj_window.rb +10 -2
- data/yard-stubs/carray_access.rb +1 -1
- data/yard-stubs/carray_broadcast.rb +1 -1
- data/yard-stubs/carray_core.rb +0 -80
- data/yard-stubs/carray_count.rb +7 -2
- data/yard-stubs/carray_lazy.rb +205 -0
- data/yard-stubs/carray_math.rb +1486 -3
- data/yard-stubs/carray_median_percentile.rb +16 -2
- data/yard-stubs/carray_order.rb +9 -69
- data/yard-stubs/carray_slab.rb +9 -7
- data/yard-stubs/carray_sort.rb +7 -5
- metadata +9 -1
|
@@ -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
128
|
# one of the 3 axis: cases;
|
|
129
|
-
#
|
|
130
|
-
#
|
|
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>)
|
|
@@ -253,7 +267,7 @@ class CACategoricalIterator < CAIterator
|
|
|
253
267
|
"call it without axis:."
|
|
254
268
|
end
|
|
255
269
|
m = moments
|
|
256
|
-
m ?
|
|
270
|
+
m ? counts - m[:count] : per_category(CA_INT64) { |s| s.count_masked }
|
|
257
271
|
end
|
|
258
272
|
|
|
259
273
|
# @overload sum
|
|
@@ -306,7 +320,7 @@ class CACategoricalIterator < CAIterator
|
|
|
306
320
|
def max(axis: nil)
|
|
307
321
|
return axis_moments(axis)[:max] if axis
|
|
308
322
|
m = moments
|
|
309
|
-
m ? m[:max] : per_category(core_reduce_type(:max)) { |s| s.max }
|
|
323
|
+
m ? m[:max].copy : per_category(core_reduce_type(:max)) { |s| s.max }
|
|
310
324
|
end
|
|
311
325
|
|
|
312
326
|
# @overload min
|
|
@@ -320,7 +334,7 @@ class CACategoricalIterator < CAIterator
|
|
|
320
334
|
def min(axis: nil)
|
|
321
335
|
return axis_moments(axis)[:min] if axis
|
|
322
336
|
m = moments
|
|
323
|
-
m ? m[:min] : per_category(core_reduce_type(:min)) { |s| s.min }
|
|
337
|
+
m ? m[:min].copy : per_category(core_reduce_type(:min)) { |s| s.min }
|
|
324
338
|
end
|
|
325
339
|
|
|
326
340
|
# @overload mean
|
|
@@ -357,11 +371,11 @@ class CACategoricalIterator < CAIterator
|
|
|
357
371
|
# @return [CArray]
|
|
358
372
|
def percentile (p, axis: nil)
|
|
359
373
|
axis_order_stat_defer!(:percentile) if axis
|
|
360
|
-
unless MONOID_TYPES.include?(
|
|
374
|
+
unless MONOID_TYPES.include?(grouped.data_type)
|
|
361
375
|
return per_category(core_reduce_type(:percentile, p)) { |s| s.percentile(p) }
|
|
362
376
|
end
|
|
363
377
|
out = CArray.float64(@k)
|
|
364
|
-
|
|
378
|
+
grouped.send(:__reduceat_percentile__, @offsets, p.to_f, out)
|
|
365
379
|
out
|
|
366
380
|
end
|
|
367
381
|
|
|
@@ -372,11 +386,11 @@ class CACategoricalIterator < CAIterator
|
|
|
372
386
|
# MASKED. For a single fraction q in 0..1 use `percentile(q * 100)`.
|
|
373
387
|
# @return [Array<CArray>]
|
|
374
388
|
def quantile
|
|
375
|
-
unless MONOID_TYPES.include?(
|
|
389
|
+
unless MONOID_TYPES.include?(grouped.data_type)
|
|
376
390
|
return [0, 25, 50, 75, 100].map { |p| percentile(p) }
|
|
377
391
|
end
|
|
378
392
|
outs = Array.new(5) { CArray.float64(@k) }
|
|
379
|
-
|
|
393
|
+
grouped.send(:__reduceat_quantile__, @offsets, *outs)
|
|
380
394
|
outs
|
|
381
395
|
end
|
|
382
396
|
|
|
@@ -393,7 +407,7 @@ class CACategoricalIterator < CAIterator
|
|
|
393
407
|
cnt = m[:count]
|
|
394
408
|
means = m[:sum] / cnt.float64 # per-segment mean (garbage where count 0/1,
|
|
395
409
|
out = CArray.float64(@k) # ignored by the kernel's n<2 guards)
|
|
396
|
-
|
|
410
|
+
grouped.send(:__reduceat_variance__, @offsets, means, cnt, out)
|
|
397
411
|
out
|
|
398
412
|
end
|
|
399
413
|
|
|
@@ -421,9 +435,9 @@ class CACategoricalIterator < CAIterator
|
|
|
421
435
|
# @return [CArray]
|
|
422
436
|
def prod(axis: nil)
|
|
423
437
|
return axis_prod(axis) if axis
|
|
424
|
-
return per_category(core_reduce_type(:prod)) { |s| s.prod } unless MONOID_TYPES.include?(
|
|
438
|
+
return per_category(core_reduce_type(:prod)) { |s| s.prod } unless MONOID_TYPES.include?(grouped.data_type)
|
|
425
439
|
out = CArray.float64(@k)
|
|
426
|
-
|
|
440
|
+
grouped.send(:__reduceat_prod__, @offsets, out)
|
|
427
441
|
out
|
|
428
442
|
end
|
|
429
443
|
|
|
@@ -452,15 +466,22 @@ class CACategoricalIterator < CAIterator
|
|
|
452
466
|
# @overload minmax
|
|
453
467
|
# Returns the per-category `[min, max]` pair (each a length-k CArray in the
|
|
454
468
|
# value data type; empty categories MASKED), matching `CArray#minmax`. Both come
|
|
455
|
-
# from
|
|
469
|
+
# from one moments pass.
|
|
456
470
|
# @return [Array<CArray>]
|
|
457
471
|
# @overload minmax(axis:)
|
|
458
472
|
# Per-fiber `[min_ca, max_ca]` along `axis` (each shape [K, ...band], h's data type,
|
|
459
473
|
# empty group cells MASKED). Ruby Array of two CArrays, not stacked.
|
|
474
|
+
# Both come from one kernel run.
|
|
460
475
|
# @param axis [Integer]
|
|
461
476
|
# @return [Array<CArray>]
|
|
462
477
|
def minmax(axis: nil)
|
|
463
|
-
|
|
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
|
|
464
485
|
[min, max]
|
|
465
486
|
end
|
|
466
487
|
|
|
@@ -502,7 +523,7 @@ class CACategoricalIterator < CAIterator
|
|
|
502
523
|
# @return [CArray] length-k int64
|
|
503
524
|
def min_index
|
|
504
525
|
am = arg_minmax
|
|
505
|
-
am ? am[:min] : per_category(CA_INT64) { |s| s.min_index }
|
|
526
|
+
am ? am[:min].copy : per_category(CA_INT64) { |s| s.min_index }
|
|
506
527
|
end
|
|
507
528
|
|
|
508
529
|
# @overload max_index
|
|
@@ -510,7 +531,7 @@ class CACategoricalIterator < CAIterator
|
|
|
510
531
|
# @return [CArray] length-k int64
|
|
511
532
|
def max_index
|
|
512
533
|
am = arg_minmax
|
|
513
|
-
am ? am[:max] : per_category(CA_INT64) { |s| s.max_index }
|
|
534
|
+
am ? am[:max].copy : per_category(CA_INT64) { |s| s.max_index }
|
|
514
535
|
end
|
|
515
536
|
|
|
516
537
|
# @overload min_addr
|
|
@@ -549,14 +570,14 @@ class CACategoricalIterator < CAIterator
|
|
|
549
570
|
# {#min_addr} vs the skipped group-local min_index-into-source.
|
|
550
571
|
# @return [CArray] length-nvalid int64
|
|
551
572
|
def sort_addr
|
|
552
|
-
out = CArray.int64(
|
|
573
|
+
out = CArray.int64(grouped.elements)
|
|
553
574
|
@k.times do |c|
|
|
554
575
|
lo = @offsets[c]
|
|
555
|
-
hi = (c + 1 < @k) ? @offsets[c + 1] :
|
|
576
|
+
hi = (c + 1 < @k) ? @offsets[c + 1] : grouped.elements
|
|
556
577
|
next unless hi > lo
|
|
557
578
|
# View-local sort order of the segment (0..size-1), lifted to grouped
|
|
558
579
|
# slots, then mapped back to source addresses via perm.
|
|
559
|
-
out[lo...hi] = perm[
|
|
580
|
+
out[lo...hi] = perm[grouped[lo...hi].sort_addr + lo]
|
|
560
581
|
end
|
|
561
582
|
out
|
|
562
583
|
end
|
|
@@ -580,7 +601,7 @@ class CACategoricalIterator < CAIterator
|
|
|
580
601
|
def wsum (weights, axis: nil)
|
|
581
602
|
return axis_wsum_wmean(weights, axis)[0] if axis
|
|
582
603
|
wg = scatter_weights(weights)
|
|
583
|
-
return kernel_weighted(wg)[0] if MONOID_TYPES.include?(
|
|
604
|
+
return kernel_weighted(wg)[0] if MONOID_TYPES.include?(grouped.data_type)
|
|
584
605
|
fold_weighted(wg, 0.0) { |v, ws| v.wsum(ws) }
|
|
585
606
|
end
|
|
586
607
|
|
|
@@ -600,7 +621,7 @@ class CACategoricalIterator < CAIterator
|
|
|
600
621
|
def wmean (weights, axis: nil)
|
|
601
622
|
return axis_wsum_wmean(weights, axis)[1] if axis
|
|
602
623
|
wg = scatter_weights(weights)
|
|
603
|
-
return kernel_weighted(wg)[1] if MONOID_TYPES.include?(
|
|
624
|
+
return kernel_weighted(wg)[1] if MONOID_TYPES.include?(grouped.data_type)
|
|
604
625
|
fold_weighted(wg, UNDEF) { |v, ws| v.wmean(ws) }
|
|
605
626
|
end
|
|
606
627
|
|
|
@@ -641,14 +662,14 @@ class CACategoricalIterator < CAIterator
|
|
|
641
662
|
# @return [CArray] shaped like the source value
|
|
642
663
|
def map (data_type: nil)
|
|
643
664
|
raise LocalJumpError, "no block given (yield)" unless block_given?
|
|
644
|
-
dt = data_type ||
|
|
665
|
+
dt = data_type || grouped.data_type
|
|
645
666
|
# Apply the block per category, assembled in grouped (category-contiguous)
|
|
646
667
|
# order: a same-length result scatters cell for cell, a scalar broadcasts.
|
|
647
|
-
transformed = CArray.new(dt, [
|
|
668
|
+
transformed = CArray.new(dt, [grouped.elements])
|
|
648
669
|
@k.times do |c|
|
|
649
670
|
lo = @offsets[c]
|
|
650
|
-
hi = (c + 1 < @k) ? @offsets[c + 1] :
|
|
651
|
-
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
|
|
652
673
|
end
|
|
653
674
|
# Scatter back to source positions via the permutation (grouped-order source
|
|
654
675
|
# indices). Excluded cells are absent from perm and stay UNDEF.
|
|
@@ -676,19 +697,19 @@ class CACategoricalIterator < CAIterator
|
|
|
676
697
|
# preserve the value data type, cumcount -> int64 (1-based within-category
|
|
677
698
|
# ordinal); an object value data type is carried by the kernel's object branch.
|
|
678
699
|
|
|
679
|
-
#
|
|
700
|
+
# @!method cumsum
|
|
680
701
|
# Per-category inclusive running sum (float64), source-shaped.
|
|
681
702
|
# @return [CArray]
|
|
682
|
-
#
|
|
703
|
+
# @!method cumprod
|
|
683
704
|
# Per-category inclusive running product (float64), source-shaped.
|
|
684
705
|
# @return [CArray]
|
|
685
|
-
#
|
|
706
|
+
# @!method cummax
|
|
686
707
|
# Per-category inclusive running maximum (value data type), source-shaped.
|
|
687
708
|
# @return [CArray]
|
|
688
|
-
#
|
|
709
|
+
# @!method cummin
|
|
689
710
|
# Per-category inclusive running minimum (value data type), source-shaped.
|
|
690
711
|
# @return [CArray]
|
|
691
|
-
#
|
|
712
|
+
# @!method cumcount
|
|
692
713
|
# Per-category 1-based within-category ordinal (int64), source-shaped.
|
|
693
714
|
# @return [CArray]
|
|
694
715
|
[:cumsum, :cumprod, :cummax, :cummin, :cumcount].each do |op|
|
|
@@ -697,16 +718,63 @@ class CACategoricalIterator < CAIterator
|
|
|
697
718
|
|
|
698
719
|
private
|
|
699
720
|
|
|
700
|
-
#
|
|
701
|
-
# the
|
|
702
|
-
#
|
|
703
|
-
#
|
|
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
|
|
704
765
|
# `{count: <int64>, sum: <float64>, min: <h's type, masked>, max: <h's type, masked>}`,
|
|
705
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.
|
|
706
777
|
def axis_moments (axis)
|
|
707
|
-
@axis_moments_cache ||= {}
|
|
708
|
-
cached = @axis_moments_cache[axis]
|
|
709
|
-
return cached if cached
|
|
710
778
|
h = @value
|
|
711
779
|
unless axis.is_a?(Integer) && axis >= 0 && axis < h.ndim
|
|
712
780
|
raise ArgumentError,
|
|
@@ -722,13 +790,13 @@ class CACategoricalIterator < CAIterator
|
|
|
722
790
|
maxs = CArray.new(h.data_type, out_shape)
|
|
723
791
|
h.__send__(:__fiber_scatter_moments__, codes_h_shape, axis, @k,
|
|
724
792
|
counts, sums, mins, maxs)
|
|
725
|
-
|
|
793
|
+
{count: counts, sum: sums, min: mins, max: maxs}
|
|
726
794
|
end
|
|
727
795
|
|
|
728
796
|
# Axis-aware sum: the moments sum is already the core fold in the core's own
|
|
729
797
|
# type, so it is handed back as is (an empty group cell carries identity 0.0).
|
|
730
798
|
def axis_sum (axis)
|
|
731
|
-
axis_moments(axis)[:sum]
|
|
799
|
+
axis_moments(axis)[:sum]
|
|
732
800
|
end
|
|
733
801
|
|
|
734
802
|
# Axis-aware mean: sums / counts (float64); empty group cells (count=0) MASKED.
|
|
@@ -831,22 +899,20 @@ class CACategoricalIterator < CAIterator
|
|
|
831
899
|
def resolve_axis_codes (codes, h_shape, axis)
|
|
832
900
|
ndim = h_shape.size
|
|
833
901
|
band = h_shape.dup; band.delete_at(axis)
|
|
834
|
-
case
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
|
839
911
|
view_shape = Array.new(ndim, 1); view_shape[axis] = h_shape[axis]
|
|
840
912
|
codes.reshape(*view_shape).broadcast_to(*h_shape)
|
|
841
|
-
when
|
|
842
|
-
unless codes.shape == h_shape
|
|
843
|
-
axis_shape_mismatch!(codes.shape, h_shape, axis, band)
|
|
844
|
-
end
|
|
913
|
+
when codes.shape == h_shape # case B
|
|
845
914
|
codes
|
|
846
|
-
when
|
|
847
|
-
unless codes.shape == band
|
|
848
|
-
axis_shape_mismatch!(codes.shape, h_shape, axis, band)
|
|
849
|
-
end
|
|
915
|
+
when codes.shape == band # band-only
|
|
850
916
|
view_shape = h_shape.dup; view_shape[axis] = 1
|
|
851
917
|
codes.reshape(*view_shape).broadcast_to(*h_shape)
|
|
852
918
|
else
|
|
@@ -855,8 +921,11 @@ class CACategoricalIterator < CAIterator
|
|
|
855
921
|
end
|
|
856
922
|
|
|
857
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.
|
|
858
927
|
raise ArgumentError,
|
|
859
|
-
"group_by_category
|
|
928
|
+
"group_by_category (axis: #{axis}): cat.shape=#{cat_shape.inspect} " \
|
|
860
929
|
"does not fit any of the 3 accepted forms for h.shape=#{h_shape.inspect}: " \
|
|
861
930
|
"case A cat.shape=[#{h_shape[axis]}], " \
|
|
862
931
|
"case B cat.shape=#{h_shape.inspect}, " \
|
|
@@ -875,9 +944,28 @@ class CACategoricalIterator < CAIterator
|
|
|
875
944
|
# one grouped axis, the flat codes as the single bundle. The kernel emits in
|
|
876
945
|
# source order, so the flat result reshapes straight back to the source shape.
|
|
877
946
|
def scan (op)
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
|
881
969
|
end
|
|
882
970
|
|
|
883
971
|
|
|
@@ -900,7 +988,7 @@ class CACategoricalIterator < CAIterator
|
|
|
900
988
|
"value.elements (#{@codes.elements})"
|
|
901
989
|
end
|
|
902
990
|
wf = weights.float64
|
|
903
|
-
wg = CArray.float64(
|
|
991
|
+
wg = CArray.float64(grouped.elements)
|
|
904
992
|
@codes.send(:__categorical_scatter__, wf.reshape(wf.elements),
|
|
905
993
|
@offsets.copy, wg, @k)
|
|
906
994
|
wg
|
|
@@ -925,7 +1013,7 @@ class CACategoricalIterator < CAIterator
|
|
|
925
1013
|
def kernel_weighted (wg)
|
|
926
1014
|
ws = CArray.float64(@k)
|
|
927
1015
|
wm = CArray.float64(@k)
|
|
928
|
-
|
|
1016
|
+
grouped.send(:__reduceat_wsum_wmean__, @offsets, wg, ws, wm)
|
|
929
1017
|
[ws, wm]
|
|
930
1018
|
end
|
|
931
1019
|
|
|
@@ -935,8 +1023,8 @@ class CACategoricalIterator < CAIterator
|
|
|
935
1023
|
out = CArray.float64(@k)
|
|
936
1024
|
@k.times do |c|
|
|
937
1025
|
lo = @offsets[c]
|
|
938
|
-
hi = (c + 1 < @k) ? @offsets[c + 1] :
|
|
939
|
-
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
|
|
940
1028
|
end
|
|
941
1029
|
out
|
|
942
1030
|
end
|
|
@@ -947,8 +1035,8 @@ class CACategoricalIterator < CAIterator
|
|
|
947
1035
|
# contract we want (identity for sum, UNDEF for ratios).
|
|
948
1036
|
def group_slice (c)
|
|
949
1037
|
lo = @offsets[c]
|
|
950
|
-
hi = (c + 1 < @k) ? @offsets[c + 1] :
|
|
951
|
-
hi > lo ?
|
|
1038
|
+
hi = (c + 1 < @k) ? @offsets[c + 1] : grouped.elements
|
|
1039
|
+
hi > lo ? grouped[lo...hi] : @empty
|
|
952
1040
|
end
|
|
953
1041
|
|
|
954
1042
|
# Single-pass reduceat moments (count / sum / min / max per category), computed
|
|
@@ -964,13 +1052,13 @@ class CACategoricalIterator < CAIterator
|
|
|
964
1052
|
def moments
|
|
965
1053
|
return @moments if defined?(@moments)
|
|
966
1054
|
@moments =
|
|
967
|
-
if MONOID_TYPES.include?(
|
|
968
|
-
dt =
|
|
1055
|
+
if MONOID_TYPES.include?(grouped.data_type)
|
|
1056
|
+
dt = grouped.data_type
|
|
969
1057
|
counts = CArray.int64(@k)
|
|
970
1058
|
sums = CArray.float64(@k)
|
|
971
1059
|
mins = CArray.new(dt, [@k])
|
|
972
1060
|
maxs = CArray.new(dt, [@k])
|
|
973
|
-
|
|
1061
|
+
grouped.send(:__reduceat_moments__, @offsets, counts, sums, mins, maxs)
|
|
974
1062
|
{ count: counts, sum: sums, min: mins, max: maxs }
|
|
975
1063
|
end
|
|
976
1064
|
end
|
|
@@ -980,10 +1068,10 @@ class CACategoricalIterator < CAIterator
|
|
|
980
1068
|
def arg_minmax
|
|
981
1069
|
return @arg_minmax if defined?(@arg_minmax)
|
|
982
1070
|
@arg_minmax =
|
|
983
|
-
if MONOID_TYPES.include?(
|
|
1071
|
+
if MONOID_TYPES.include?(grouped.data_type)
|
|
984
1072
|
mn = CArray.int64(@k)
|
|
985
1073
|
mx = CArray.int64(@k)
|
|
986
|
-
|
|
1074
|
+
grouped.send(:__reduceat_argminmax__, @offsets, mn, mx)
|
|
987
1075
|
{ min: mn, max: mx }
|
|
988
1076
|
end
|
|
989
1077
|
end
|
|
@@ -994,10 +1082,10 @@ class CACategoricalIterator < CAIterator
|
|
|
994
1082
|
def all_any
|
|
995
1083
|
return @all_any if defined?(@all_any)
|
|
996
1084
|
@all_any =
|
|
997
|
-
if
|
|
1085
|
+
if grouped.data_type == CA_BOOLEAN
|
|
998
1086
|
a = CArray.boolean(@k)
|
|
999
1087
|
o = CArray.boolean(@k)
|
|
1000
|
-
|
|
1088
|
+
grouped.send(:__reduceat_all_any__, @offsets, a, o)
|
|
1001
1089
|
{ all: a, any: o }
|
|
1002
1090
|
end
|
|
1003
1091
|
end
|
|
@@ -1015,13 +1103,52 @@ class CACategoricalIterator < CAIterator
|
|
|
1015
1103
|
# (`sum` on an integer promotes, `accumulate` stays, `min` / `max` keep the
|
|
1016
1104
|
# type but a boolean widens, `prod` on an object stays an object). A payload
|
|
1017
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.
|
|
1018
1111
|
def core_reduce_type (op, *args)
|
|
1019
1112
|
(@core_reduce_type ||= {})[[op, args]] ||=
|
|
1020
|
-
|
|
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?
|
|
1021
1134
|
end
|
|
1022
1135
|
|
|
1023
1136
|
def per_category (data_type)
|
|
1024
|
-
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.
|
|
1025
1152
|
@k.times { |c| out[c] = yield(group_slice(c)) }
|
|
1026
1153
|
out
|
|
1027
1154
|
end
|