carray 3.0.1 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +824 -0
- data/README.md +5 -6
- data/carray.gemspec +1 -1
- data/ext/ca_axis_dispatch.c +258 -4
- data/ext/ca_axis_group.c +368 -184
- data/ext/ca_binop_dispatch.c +54 -32
- data/ext/ca_binop_dispatch.h +1 -0
- data/ext/ca_categorical_iterator.c +90 -41
- data/ext/ca_compare.c +8 -2
- data/ext/ca_for_buffer.h +21 -68
- data/ext/ca_group_iter.c +27 -11
- data/ext/ca_kernel_iterator.c +960 -245
- data/ext/ca_kernel_iterator.h +352 -181
- data/ext/ca_monop_dispatch.c +4 -4
- data/ext/ca_monop_dispatch.h +4 -0
- data/ext/ca_obj_array.c +194 -42
- data/ext/ca_obj_bincmp.c +152 -121
- data/ext/ca_obj_binop.c +107 -210
- data/ext/ca_obj_bitarray.c +37 -24
- data/ext/ca_obj_bitfield.c +95 -62
- data/ext/ca_obj_block.c +5 -4
- data/ext/ca_obj_byte_swap.c +40 -40
- data/ext/ca_obj_const_string.c +180 -50
- data/ext/ca_obj_face.c +428 -62
- data/ext/ca_obj_face.h +107 -47
- data/ext/ca_obj_fake.c +21 -50
- data/ext/ca_obj_farray.c +1 -0
- data/ext/ca_obj_field.c +16 -3
- data/ext/ca_obj_fixlen_string.c +39 -26
- data/ext/ca_obj_grid.c +181 -124
- data/ext/ca_obj_meld.c +235 -55
- data/ext/ca_obj_moncmp.c +61 -61
- data/ext/ca_obj_monop.c +125 -107
- data/ext/ca_obj_object.c +551 -233
- data/ext/ca_obj_record.c +51 -16
- data/ext/ca_obj_reduce.c +5 -8
- data/ext/ca_obj_refer.c +117 -33
- data/ext/ca_obj_remap.c +57 -17
- data/ext/ca_obj_repeat.c +21 -5
- data/ext/ca_obj_roll.c +115 -11
- data/ext/ca_obj_select.c +88 -75
- data/ext/ca_obj_select_axis.c +58 -60
- data/ext/ca_obj_shift.c +22 -102
- data/ext/ca_obj_stack.c +223 -123
- data/ext/ca_obj_stride.c +267 -35
- data/ext/ca_obj_string.c +25 -22
- data/ext/ca_obj_tile.c +32 -16
- data/ext/ca_obj_time.c +23 -25
- data/ext/ca_obj_timedelta.c +21 -23
- data/ext/ca_obj_transpose.c +12 -3
- data/ext/ca_obj_triop.c +110 -117
- data/ext/ca_obj_window.c +157 -29
- data/ext/ca_op_ipower.c +25 -13
- data/ext/ca_op_powi.h +57 -30
- data/ext/ca_pow5_table.h +656 -0
- data/ext/ca_rng_normal.h +42 -0
- data/ext/ca_rng_xoshiro256pp.h +105 -0
- data/ext/ca_sort_kernels.h +70 -0
- data/ext/ca_sweep_engine.c +385 -198
- data/ext/ca_sweep_engine.h +31 -23
- data/ext/ca_triop_dispatch.c +4 -1
- data/ext/ca_triop_dispatch.h +5 -2
- data/ext/caframe_csv_reader.c +463 -0
- data/ext/carray.h +90 -28
- data/ext/carray_access.c +487 -231
- data/ext/carray_address_basis.c +731 -0
- data/ext/carray_attribute.c +45 -3
- data/ext/carray_bincount.c +32 -2
- data/ext/carray_broadcast.c +49 -17
- data/ext/carray_call_cfunc.c +667 -511
- data/ext/carray_cast.c +1556 -247
- data/ext/carray_cast_func.rb +34 -21
- data/ext/carray_conversion.c +197 -73
- data/ext/carray_copy.c +84 -38
- data/ext/carray_core.c +591 -119
- data/ext/carray_count.c +148 -96
- data/ext/carray_element.c +18 -27
- data/ext/carray_factorize.c +604 -291
- data/ext/carray_generate.c +76 -40
- data/ext/carray_histogram.c +152 -94
- data/ext/carray_hold.c +2 -1
- data/ext/carray_index_classifier.c +63 -96
- data/ext/carray_internal.h +400 -1
- data/ext/carray_lazy.c +834 -79
- data/ext/carray_loop.c +88 -66
- data/ext/carray_mask.c +705 -324
- data/ext/carray_math_kernel.h +1 -0
- data/ext/carray_median_percentile.c +263 -63
- data/ext/carray_memory_view.c +254 -248
- data/ext/carray_operator.c +1093 -363
- data/ext/carray_order.c +133 -69
- data/ext/carray_partition.c +102 -56
- data/ext/carray_random.c +582 -204
- data/ext/carray_scatter.c +308 -65
- data/ext/carray_segment.c +257 -0
- data/ext/carray_slab.c +183 -70
- data/ext/carray_sort.c +235 -240
- data/ext/carray_sort_kernel.c +8 -1
- data/ext/carray_struct.c +99 -21
- data/ext/carray_test.c +167 -64
- data/ext/carray_time_text.c +1182 -0
- data/ext/carray_utils.c +363 -98
- data/ext/extconf.rb +27 -2
- data/ext/mk_call_cfunc.rb +103 -118
- data/ext/mk_pow5_table.rb +35 -0
- data/ext/mkkernel.rb +1349 -851
- data/ext/ruby_carray.c +17 -2
- data/ext/version.h +4 -4
- data/ext/version.rb +4 -2
- data/lib/carray/attribute.rb +39 -0
- data/lib/carray/autoload_carray.rb +24 -3
- data/lib/carray/autoload_method_extension.rb +18 -0
- data/lib/carray/axis_group.rb +245 -20
- data/lib/carray/basics.rb +95 -25
- data/lib/carray/bincount_nd.rb +6 -0
- data/lib/carray/block_iterator.rb +200 -54
- data/lib/carray/boolean_reduce.rb +21 -29
- data/lib/carray/categorical.rb +244 -59
- data/lib/carray/categorical_iterator.rb +391 -757
- data/lib/carray/complex.rb +36 -29
- data/lib/carray/conditional.rb +94 -7
- data/lib/carray/const_string.rb +177 -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/concat.rb +157 -6
- data/lib/carray/frame/convert.rb +13 -7
- data/lib/carray/frame/csv_parser.rb +590 -76
- data/lib/carray/frame/describe.rb +79 -0
- data/lib/carray/frame/frame.rb +205 -35
- data/lib/carray/frame/group.rb +199 -40
- data/lib/carray/frame/io.rb +452 -60
- data/lib/carray/frame/join.rb +134 -46
- data/lib/carray/frame/records.rb +54 -19
- data/lib/carray/frame/reshape.rb +448 -0
- data/lib/carray/frame/sort.rb +26 -2
- data/lib/carray/frame/verbs.rb +726 -56
- data/lib/carray/frame.rb +2 -0
- data/lib/carray/fuse_source.rb +111 -2
- data/lib/carray/fusion.rb +229 -38
- data/lib/carray/inspect.rb +71 -20
- data/lib/carray/iterator.rb +157 -2
- data/lib/carray/lazy.rb +54 -67
- data/lib/carray/mask_gap_fill.rb +66 -50
- data/lib/carray/math.rb +32 -19
- data/lib/carray/meld_reduce.rb +40 -7
- data/lib/carray/methods/align_addr.rb +8 -4
- data/lib/carray/methods/bincount.rb +2 -2
- data/lib/carray/methods/bit_string.rb +14 -3
- data/lib/carray/methods/composition.rb +16 -10
- data/lib/carray/methods/discovery_along.rb +74 -0
- data/lib/carray/methods/factorize.rb +50 -0
- data/lib/carray/methods/gather_nd.rb +13 -2
- data/lib/carray/methods/insert_block.rb +9 -14
- data/lib/carray/methods/is_in.rb +130 -6
- data/lib/carray/methods/join.rb +1 -1
- data/lib/carray/methods/locate_addr.rb +235 -21
- data/lib/carray/methods/mask_duplicates.rb +35 -1
- data/lib/carray/methods/mode.rb +18 -1
- data/lib/carray/methods/nunique.rb +22 -1
- data/lib/carray/methods/repeat.rb +91 -0
- data/lib/carray/methods/resize.rb +21 -23
- data/lib/carray/methods/snap.rb +34 -12
- data/lib/carray/methods/unique.rb +52 -3
- data/lib/carray/record_order.rb +208 -0
- data/lib/carray/rng.rb +86 -0
- data/lib/carray/runtime.rb +14 -11
- data/lib/carray/segment_iterator.rb +990 -0
- data/lib/carray/serialize.rb +197 -27
- data/lib/carray/slab_iterator.rb +109 -40
- data/lib/carray/stack.rb +10 -4
- data/lib/carray/string_operation_extension.rb +9 -4
- data/lib/carray/struct.rb +142 -51
- data/lib/carray/struct_builder.rb +110 -14
- data/lib/carray/table.rb +2 -2
- data/lib/carray/time.rb +535 -105
- data/lib/carray/window_iterator.rb +259 -82
- data/lib/carray.rb +4 -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_block.rb +2 -7
- data/yard-stubs/ca_obj_fake.rb +1 -1
- data/yard-stubs/ca_obj_field.rb +1 -1
- data/yard-stubs/ca_obj_refer.rb +3 -3
- data/yard-stubs/ca_obj_roll.rb +0 -1
- data/yard-stubs/ca_obj_stride.rb +10 -7
- data/yard-stubs/ca_obj_transpose.rb +1 -1
- data/yard-stubs/ca_obj_window.rb +11 -4
- data/yard-stubs/carray_access.rb +6 -5
- data/yard-stubs/carray_broadcast.rb +2 -2
- data/yard-stubs/carray_cast.rb +78 -43
- data/yard-stubs/carray_conversion.rb +13 -1
- data/yard-stubs/carray_copy.rb +8 -3
- data/yard-stubs/carray_core.rb +3 -83
- data/yard-stubs/carray_count.rb +16 -2
- data/yard-stubs/carray_lazy.rb +210 -1
- data/yard-stubs/carray_loop.rb +4 -1
- data/yard-stubs/carray_mask.rb +34 -10
- data/yard-stubs/carray_math.rb +1499 -4
- data/yard-stubs/carray_median_percentile.rb +26 -8
- data/yard-stubs/carray_order.rb +43 -82
- data/yard-stubs/carray_scatter.rb +10 -3
- data/yard-stubs/carray_segment.rb +72 -0
- data/yard-stubs/carray_slab.rb +11 -8
- data/yard-stubs/carray_sort.rb +50 -8
- data/yard-stubs/carray_test.rb +10 -0
- data/yard-stubs/carray_utils.rb +0 -26
- metadata +19 -19
- data/ext/ca_for_each_element.h +0 -239
- data/ext/carray_build_flags.h +0 -3
- data/ext/carray_config.h +0 -28
- data/ext/carray_kernels_bincmp.c +0 -4446
- data/ext/carray_kernels_binop.c +0 -11001
- data/ext/carray_kernels_init.c +0 -1131
- data/ext/carray_kernels_map.c +0 -3467
- data/ext/carray_kernels_moncmp.c +0 -2097
- data/ext/carray_kernels_monop.c +0 -18313
- data/ext/carray_kernels_reduce_aggregate.c +0 -25837
- data/ext/carray_kernels_reduce_boolean.c +0 -330
- data/ext/carray_kernels_reduce_cumulative.c +0 -14593
- data/ext/carray_kernels_reduce_extreme.c +0 -16948
- data/ext/carray_kernels_reduce_variance.c +0 -3910
- data/ext/carray_kernels_scan.c +0 -3693
- data/ext/carray_kernels_search.c +0 -32138
- data/ext/carray_kernels_sort.c +0 -10626
- data/ext/carray_kernels_triop.c +0 -1392
|
@@ -0,0 +1,990 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------------
|
|
2
|
+
#
|
|
3
|
+
# carray/segment_iterator.rb
|
|
4
|
+
#
|
|
5
|
+
# CASegmentIterator -- reductions over consecutive segments of a flat
|
|
6
|
+
# sequence. The pieces are contiguous runs of a held, contiguous copy
|
|
7
|
+
# (`grouped`), cut at the boundaries in @bounds (every boundary, k+1) and
|
|
8
|
+
# @offsets (the starts, k, the form the reduceat kernels read). Each
|
|
9
|
+
# reduction is the core reduction lifted to the piece.
|
|
10
|
+
#
|
|
11
|
+
# CACategoricalIterator is this iterator over a category-sorted copy: it
|
|
12
|
+
# builds `grouped` by a counting sort and adds the labels and the
|
|
13
|
+
# per-fiber (axis:) form. A segment iterator has no axis: form; the
|
|
14
|
+
# hooks at the bottom say so where a reduction is asked for one.
|
|
15
|
+
#
|
|
16
|
+
# ----------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
require "carray"
|
|
19
|
+
|
|
20
|
+
# A CAIterator over consecutive segments of a flat sequence, built by
|
|
21
|
+
# {CArray#segments}. Segment `c` is the cells `offsets[c]...offsets[c + 1]`
|
|
22
|
+
# of the value read in flatten order, and each reduction answers one value
|
|
23
|
+
# per segment -- the core reduction of the same name, lifted to the segment,
|
|
24
|
+
# with the core's data types and its answer for an empty or all-masked
|
|
25
|
+
# segment. The pieces never overlap, so {#map} and the running scans are
|
|
26
|
+
# available too.
|
|
27
|
+
#
|
|
28
|
+
# The iterator holds a copy of the covered cells, taken at construction.
|
|
29
|
+
# Cells outside `offsets[0]...offsets[-1]` belong to no segment: they are
|
|
30
|
+
# UNDEF in {#map} and in the scans.
|
|
31
|
+
class CASegmentIterator < CAIterator
|
|
32
|
+
|
|
33
|
+
# value : the CArray whose segments are reduced, read in flatten order.
|
|
34
|
+
# offsets : the k+1 boundaries (segment c spans offsets[c]...offsets[c+1]),
|
|
35
|
+
# lengths : or the k lengths, the segments then starting at cell 0.
|
|
36
|
+
#
|
|
37
|
+
# Takes a copy of the cells the segments cover, so the reductions answer
|
|
38
|
+
# about the values as they were at construction. Cells before offsets[0]
|
|
39
|
+
# or from offsets[-1] on belong to no segment.
|
|
40
|
+
def initialize (value, offsets: nil, lengths: nil)
|
|
41
|
+
kw = {}
|
|
42
|
+
kw[:offsets] = offsets unless offsets.nil?
|
|
43
|
+
kw[:lengths] = lengths unless lengths.nil?
|
|
44
|
+
bounds = CArray.__segment_bounds__("segments", **kw)
|
|
45
|
+
origin, finish = bounds[0], bounds[-1]
|
|
46
|
+
if origin < 0 || finish > value.elements
|
|
47
|
+
raise ArgumentError,
|
|
48
|
+
"segments: the segments span #{origin}...#{finish}, " \
|
|
49
|
+
"outside the #{value.elements} elements of the value"
|
|
50
|
+
end
|
|
51
|
+
@value = value
|
|
52
|
+
@src_shape = value.shape
|
|
53
|
+
@k = bounds.elements - 1
|
|
54
|
+
@ndim = 1
|
|
55
|
+
@shape = [@k]
|
|
56
|
+
@origin = origin
|
|
57
|
+
@bounds = bounds - origin # every boundary, from 0
|
|
58
|
+
@offsets = @bounds[0...-1].copy # the starts, for the kernels
|
|
59
|
+
@elements = @k > 0 ? (@bounds[1..-1] - @bounds[0...-1]) : CArray.int64(0)
|
|
60
|
+
@grouped = finish > origin ? value.reshape(value.elements)[origin...finish].copy
|
|
61
|
+
: CArray.new(value.data_type, [0], bytes: value.bytes)
|
|
62
|
+
@empty = CArray.new(@grouped.data_type, [0], bytes: @grouped.bytes)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @overload inspect
|
|
66
|
+
# Returns a one-line summary: the number of segments and their lengths.
|
|
67
|
+
# @return [String]
|
|
68
|
+
def inspect
|
|
69
|
+
"#<#{self.class} segments=#{@k} elements=#{@elements.to_a.inspect}>"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @overload each { |members| ... }
|
|
73
|
+
# Yields each segment's members in order, as a CArray slice of the held
|
|
74
|
+
# copy; an empty segment yields an empty array. Without a block, returns
|
|
75
|
+
# an Enumerator.
|
|
76
|
+
# @yieldparam members [CArray]
|
|
77
|
+
# @return [Enumerator, self]
|
|
78
|
+
def each
|
|
79
|
+
return to_enum(:each) unless block_given?
|
|
80
|
+
@k.times { |c| yield segment_slice(c) }
|
|
81
|
+
self
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @overload elements
|
|
85
|
+
# Returns the number of cells in each segment, masked or not. The
|
|
86
|
+
# CAIterator count-family member — `CArray#elements` (structural,
|
|
87
|
+
# mask-independent) lifted per segment.
|
|
88
|
+
# @return [CArray]
|
|
89
|
+
def elements
|
|
90
|
+
# A copy, like every other member that reads off a memo: the memo is the
|
|
91
|
+
# iterator's own state, and handing out the array itself lets a caller
|
|
92
|
+
# write into it and change what the iterator answers from then on. This
|
|
93
|
+
# one invites it -- the docs point at its prefix sum for splitting a
|
|
94
|
+
# column apart, which reads as scratch.
|
|
95
|
+
counts.copy
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @overload count_not_masked
|
|
99
|
+
# Returns the per-segment count of present (non-masked) values as int64
|
|
100
|
+
# — the denominator the value reductions actually divide by. Equals
|
|
101
|
+
# {#elements} unless the value carries a mask. A count is always defined,
|
|
102
|
+
# so an empty segment is `0` (never masked).
|
|
103
|
+
# @return [CArray]
|
|
104
|
+
def count_not_masked(axis: nil)
|
|
105
|
+
return axis_counts(axis, :count_not_masked) if axis
|
|
106
|
+
m = moments
|
|
107
|
+
m ? m[:count].copy : per_segment(CA_INT64) { |s| s.count_not_masked }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @overload count(v = <none>)
|
|
111
|
+
# Per-segment count, mirroring `CArray#count` per segment. No argument
|
|
112
|
+
# returns {#count_not_masked} (present cells); `count(UNDEF)` returns
|
|
113
|
+
# {#count_masked}; `count(v)` counts cells whose value equals `v`.
|
|
114
|
+
# @return [CArray] length-k int64
|
|
115
|
+
def count (*args, axis: nil)
|
|
116
|
+
if axis
|
|
117
|
+
return count_not_masked(axis: axis) if args.empty?
|
|
118
|
+
raise NotImplementedError,
|
|
119
|
+
"#{self.class}#count(v, axis:) is not implemented — " \
|
|
120
|
+
"value-equality count is available without axis:."
|
|
121
|
+
end
|
|
122
|
+
return count_not_masked if args.empty?
|
|
123
|
+
# Delegate per segment to CArray#count (handles count(UNDEF) -> masked count and
|
|
124
|
+
# count(v) alike, with core's exact data type equality). The segment slice is a
|
|
125
|
+
# CABlock, whose own #count is the block geometry accessor, so dispatch
|
|
126
|
+
# CArray#count explicitly. (Not fused: a value-equality reduceat would have
|
|
127
|
+
# to reproduce core's cross-type / out-of-range equality exactly.)
|
|
128
|
+
cnt = CArray.instance_method(:count)
|
|
129
|
+
per_segment(CA_INT64) { |s| cnt.bind_call(s, *args) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @overload count_masked
|
|
133
|
+
# Returns the per-segment count of masked (missing) values as int64.
|
|
134
|
+
# Empty segments are `0`.
|
|
135
|
+
# @return [CArray]
|
|
136
|
+
def count_masked(axis: nil)
|
|
137
|
+
if axis
|
|
138
|
+
raise NotImplementedError,
|
|
139
|
+
"#{self.class}#count_masked(axis:) is not implemented — " \
|
|
140
|
+
"call it without axis:."
|
|
141
|
+
end
|
|
142
|
+
m = moments
|
|
143
|
+
m ? counts - m[:count] : per_segment(CA_INT64) { |s| s.count_masked }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# @overload sum
|
|
147
|
+
# Returns per-segment sums in the data type `CArray#sum` promotes the value
|
|
148
|
+
# to (float64 for an integer value). `accumulate` is the same fold kept in
|
|
149
|
+
# the value's own type. An empty or fully-masked segment sums the empty
|
|
150
|
+
# set, which is the additive identity `0` (unmasked) — the same contract as
|
|
151
|
+
# `CArray#sum` on an empty / all-masked array.
|
|
152
|
+
# @return [CArray]
|
|
153
|
+
def sum(axis: nil)
|
|
154
|
+
return axis_sum(axis) if axis
|
|
155
|
+
m = moments
|
|
156
|
+
return per_segment(core_reduce_type(:sum)) { |s| s.sum } unless m
|
|
157
|
+
m[:sum].copy # the moments sum IS the core fold (empty -> 0.0)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# @overload accumulate
|
|
161
|
+
# Returns per-segment sums folded in the value's own data type, wrapping at
|
|
162
|
+
# its width, as the core `accumulate` does. This is the exact in-type fold:
|
|
163
|
+
# `sum` reads its answer off a float64 moment and casts back, so it loses
|
|
164
|
+
# the low bits of a wide integer payload and does not wrap. An empty or
|
|
165
|
+
# fully-masked segment accumulates the empty set, the additive identity `0`
|
|
166
|
+
# (unmasked).
|
|
167
|
+
# @return [CArray]
|
|
168
|
+
def accumulate(axis: nil)
|
|
169
|
+
return axis_by_masked_copy(axis, :accumulate, core_reduce_type(:accumulate)) if axis
|
|
170
|
+
per_segment(core_reduce_type(:accumulate)) { |s| s.accumulate }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# @overload max
|
|
174
|
+
# Returns per-segment maxima in the value data type. Empty segments are
|
|
175
|
+
# MASKED.
|
|
176
|
+
# @return [CArray]
|
|
177
|
+
def max(axis: nil)
|
|
178
|
+
return axis_moments(axis, :max)[:max] if axis
|
|
179
|
+
m = moments
|
|
180
|
+
m ? m[:max].copy : per_segment(core_reduce_type(:max)) { |s| s.max }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# @overload min
|
|
184
|
+
# Returns per-segment minima in the value data type. Empty segments are
|
|
185
|
+
# MASKED.
|
|
186
|
+
# @return [CArray]
|
|
187
|
+
def min(axis: nil)
|
|
188
|
+
return axis_moments(axis, :min)[:min] if axis
|
|
189
|
+
m = moments
|
|
190
|
+
m ? m[:min].copy : per_segment(core_reduce_type(:min)) { |s| s.min }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# @overload mean
|
|
194
|
+
# Returns per-segment means as float64. Empty segments are MASKED.
|
|
195
|
+
# @return [CArray]
|
|
196
|
+
def mean(axis: nil)
|
|
197
|
+
return axis_mean(axis) if axis
|
|
198
|
+
m = moments
|
|
199
|
+
return per_segment(core_reduce_type(:mean)) { |s| s.mean } unless m
|
|
200
|
+
cnt = m[:count]
|
|
201
|
+
out = m[:sum] / cnt.float64 # count 0 -> NaN, masked next
|
|
202
|
+
out[cnt.eq(0)] = UNDEF # empty / all-masked category -> MASKED
|
|
203
|
+
out
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# @overload median
|
|
207
|
+
# Returns per-segment medians as float64. Empty segments are MASKED.
|
|
208
|
+
# @return [CArray]
|
|
209
|
+
def median(axis: nil)
|
|
210
|
+
return axis_order_stat(axis, :median) if axis
|
|
211
|
+
percentile(50.0)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# @overload percentile(p)
|
|
215
|
+
# Returns the per-segment `p`-th percentile as float64 (`p` in 0..100,
|
|
216
|
+
# `:linear` interpolation, matching `CArray#percentile`). Empty segments
|
|
217
|
+
# are MASKED. Order statistics need every value of a segment held together —
|
|
218
|
+
# this is the reduceat that only the eager grouped copy can serve.
|
|
219
|
+
# @param p [Numeric] percentile in 0..100.
|
|
220
|
+
# @return [CArray]
|
|
221
|
+
def percentile (p, axis: nil)
|
|
222
|
+
return axis_order_stat(axis, :percentile, p) if axis
|
|
223
|
+
unless MONOID_TYPES.include?(grouped.data_type)
|
|
224
|
+
return per_segment(core_reduce_type(:percentile, p)) { |s| s.percentile(p) }
|
|
225
|
+
end
|
|
226
|
+
out = CArray.float64(@k)
|
|
227
|
+
grouped.send(:__reduceat_percentile__, @offsets, p.to_f, out)
|
|
228
|
+
out
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# @overload quantile
|
|
232
|
+
# Returns the per-segment five-number summary `[min, Q1, median, Q3, max]`
|
|
233
|
+
# as five length-k float64 CArrays (matching `CArray#quantile`): the
|
|
234
|
+
# percentiles at 0 / 25 / 50 / 75 / 100. Empty / all-masked segments are
|
|
235
|
+
# MASKED. For a single fraction q in 0..1 use `percentile(q * 100)`.
|
|
236
|
+
# @return [Array<CArray>]
|
|
237
|
+
def quantile
|
|
238
|
+
unless MONOID_TYPES.include?(grouped.data_type)
|
|
239
|
+
return CArray.__answer_as__(:quantile) { [0, 25, 50, 75, 100].map { |p| percentile(p) } }
|
|
240
|
+
end
|
|
241
|
+
outs = Array.new(5) { CArray.float64(@k) }
|
|
242
|
+
grouped.send(:__reduceat_quantile__, @offsets, *outs)
|
|
243
|
+
outs
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# @overload variance
|
|
247
|
+
# Returns per-segment SAMPLE variance (ddof=1) as float64. Matches
|
|
248
|
+
# `CArray#variance` per segment: a segment with fewer than two present
|
|
249
|
+
# values is MASKED, n>=2 is the sample variance.
|
|
250
|
+
# @return [CArray]
|
|
251
|
+
def variance(axis: nil)
|
|
252
|
+
return axis_by_masked_copy(axis, :variance) if axis
|
|
253
|
+
m = moments
|
|
254
|
+
return per_segment(core_reduce_type(:variance)) { |s| s.variance } unless m
|
|
255
|
+
cnt = m[:count]
|
|
256
|
+
means = m[:sum] / cnt.float64 # per-segment mean (garbage where count 0/1,
|
|
257
|
+
out = CArray.float64(@k) # ignored by the kernel's n<2 guards)
|
|
258
|
+
grouped.send(:__reduceat_variance__, @offsets, means, cnt, out)
|
|
259
|
+
out
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# @overload stddev
|
|
263
|
+
# Returns per-segment SAMPLE standard deviation (ddof=1) as float64.
|
|
264
|
+
# Matches `CArray#stddev` per segment (fewer than two present values
|
|
265
|
+
# MASKED).
|
|
266
|
+
# @return [CArray]
|
|
267
|
+
def stddev(axis: nil)
|
|
268
|
+
return axis_by_masked_copy(axis, :stddev) if axis
|
|
269
|
+
m = moments
|
|
270
|
+
return per_segment(core_reduce_type(:stddev)) { |s| s.stddev } unless m
|
|
271
|
+
variance.sqrt # sqrt propagates the n<2 mask
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# @overload prod
|
|
275
|
+
# Returns per-segment products as float64 (matching `CArray#prod`). An
|
|
276
|
+
# empty / fully-masked segment is `1.0` (the multiplicative identity).
|
|
277
|
+
# Single-pass reduceat for numeric values; per-segment fallback otherwise.
|
|
278
|
+
# @return [CArray]
|
|
279
|
+
def prod(axis: nil)
|
|
280
|
+
return axis_prod(axis) if axis
|
|
281
|
+
return per_segment(core_reduce_type(:prod)) { |s| s.prod } unless MONOID_TYPES.include?(grouped.data_type)
|
|
282
|
+
out = CArray.float64(@k)
|
|
283
|
+
grouped.send(:__reduceat_prod__, @offsets, out)
|
|
284
|
+
out
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# @overload all
|
|
288
|
+
# Returns the per-segment `all` as boolean (matching `CArray#all`): true
|
|
289
|
+
# iff every present value is truthy (empty segment -> true, vacuously).
|
|
290
|
+
# The value data type must be boolean, as for `CArray#all`.
|
|
291
|
+
# @return [CArray]
|
|
292
|
+
def all
|
|
293
|
+
aa = all_any
|
|
294
|
+
aa ? aa[:all] : per_segment(CA_BOOLEAN) { |s| s.all }
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# @overload any
|
|
298
|
+
# Returns the per-segment `any` as boolean (matching `CArray#any`): true
|
|
299
|
+
# iff some present value is truthy (empty segment -> false). The value
|
|
300
|
+
# data type must be boolean, as for `CArray#any`.
|
|
301
|
+
# @return [CArray]
|
|
302
|
+
def any
|
|
303
|
+
aa = all_any
|
|
304
|
+
aa ? aa[:any] : per_segment(CA_BOOLEAN) { |s| s.any }
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# ---- tier 2 (fused / population / position) ------------------------------
|
|
308
|
+
|
|
309
|
+
# @overload minmax
|
|
310
|
+
# Returns the per-segment `[min, max]` pair (each a length-k CArray in the
|
|
311
|
+
# value data type; empty segments MASKED), matching `CArray#minmax`. Both come
|
|
312
|
+
# from one moments pass.
|
|
313
|
+
# @return [Array<CArray>]
|
|
314
|
+
def minmax(axis: nil)
|
|
315
|
+
if axis
|
|
316
|
+
# take both off one pass rather than asking min and max separately,
|
|
317
|
+
# which would run the kernel twice now that nothing is kept between
|
|
318
|
+
# calls -- this is the "keep the result" the axis: family expects
|
|
319
|
+
m = axis_moments(axis, :minmax)
|
|
320
|
+
return [m[:min], m[:max]]
|
|
321
|
+
end
|
|
322
|
+
[min, max]
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# @overload variancep
|
|
326
|
+
# Per-segment POPULATION variance (ddof=0) as float64, matching
|
|
327
|
+
# `CArray#variancep`: empty / all-masked -> MASKED, single value -> 0.0.
|
|
328
|
+
# Derived from the sample variance (variancep = variance * (n-1) / n), so it
|
|
329
|
+
# reuses the centred two-pass kernel with no extra walk.
|
|
330
|
+
# @return [CArray]
|
|
331
|
+
def variancep(axis: nil)
|
|
332
|
+
return axis_by_masked_copy(axis, :variancep) if axis
|
|
333
|
+
m = moments
|
|
334
|
+
return per_segment(core_reduce_type(:variancep)) { |s| s.variancep } unless m
|
|
335
|
+
cnt = m[:count]
|
|
336
|
+
vp = variance * (cnt - 1).float64 / cnt.float64
|
|
337
|
+
vp[cnt.eq(1)] = 0.0 # one value: defined, unlike variance
|
|
338
|
+
vp
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# @overload stddevp
|
|
342
|
+
# Per-segment POPULATION standard deviation (ddof=0) as float64.
|
|
343
|
+
# @return [CArray]
|
|
344
|
+
def stddevp(axis: nil)
|
|
345
|
+
return axis_by_masked_copy(axis, :stddevp) if axis
|
|
346
|
+
m = moments
|
|
347
|
+
return per_segment(core_reduce_type(:stddevp)) { |s| s.stddevp } unless m
|
|
348
|
+
variancep.sqrt
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# @overload min_index
|
|
352
|
+
# Per-segment index of the minimum — the position within the
|
|
353
|
+
# segment's members (source order) — matching `CArray#min_index` per segment.
|
|
354
|
+
# Empty / all-masked segments are MASKED. Single-pass fused reduceat for
|
|
355
|
+
# numeric values; per-segment fallback otherwise.
|
|
356
|
+
# @return [CArray] length-k int64
|
|
357
|
+
def min_index
|
|
358
|
+
am = arg_minmax
|
|
359
|
+
am ? am[:min].copy : per_segment(CA_INT64) { |s| s.min_index }
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# @overload max_index
|
|
363
|
+
# Per-segment index of the maximum. See {#min_index}.
|
|
364
|
+
# @return [CArray] length-k int64
|
|
365
|
+
def max_index
|
|
366
|
+
am = arg_minmax
|
|
367
|
+
am ? am[:max].copy : per_segment(CA_INT64) { |s| s.max_index }
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# @overload min_addr
|
|
371
|
+
# Per-segment flat source address of the minimum — which cell of the source
|
|
372
|
+
# value holds it, matching `CArray#min_addr` per segment. Unlike {#min_index}
|
|
373
|
+
# (the position within the segment) this indexes back into the original array
|
|
374
|
+
# (`value.reshape(value.elements)[grp.min_addr]`). Empty segments MASKED.
|
|
375
|
+
# @return [CArray] length-k int64
|
|
376
|
+
def min_addr
|
|
377
|
+
segment_addr(min_index)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# @overload max_addr
|
|
381
|
+
# Per-segment flat source address of the maximum. See {#min_addr}.
|
|
382
|
+
# @return [CArray] length-k int64
|
|
383
|
+
def max_addr
|
|
384
|
+
segment_addr(max_index)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# @overload sort_addr
|
|
388
|
+
# Per-segment sort by flat source address. Returns a length-nvalid
|
|
389
|
+
# (= `elements.sum`) int64 CArray of the flat SOURCE addresses that sort each
|
|
390
|
+
# segment's members, segment by segment: part `c` holds segment `c`'s
|
|
391
|
+
# source addresses in ascending-value order, parts concatenated in
|
|
392
|
+
# order. So `value.reshape(value.elements)[grp.sort_addr]` yields
|
|
393
|
+
# the values segment by segment, sorted within each, and splitting by the
|
|
394
|
+
# {#elements} prefix sum gives per-segment. Excluded cells (in no segment) are
|
|
395
|
+
# omitted. A masked value sorts to the tail of its segment (as `CArray#sort`
|
|
396
|
+
# sends masked cells to the end), so with a mask the first address is the
|
|
397
|
+
# minimum but the last is the masked cell, not the maximum.
|
|
398
|
+
#
|
|
399
|
+
# Unlike {#min_index} / {#max_index} (positions within a segment), this
|
|
400
|
+
# indexes back into the original array; only the source-address form is
|
|
401
|
+
# offered, mirroring {#min_addr}.
|
|
402
|
+
# @return [CArray] length-nvalid int64
|
|
403
|
+
def sort_addr
|
|
404
|
+
out = CArray.int64(grouped.elements)
|
|
405
|
+
@k.times do |c|
|
|
406
|
+
lo, hi = @bounds[c], @bounds[c + 1]
|
|
407
|
+
next unless hi > lo
|
|
408
|
+
# View-local sort order of the segment (0..size-1), lifted to grouped
|
|
409
|
+
# slots, then mapped back to source addresses via perm.
|
|
410
|
+
out[lo...hi] = perm[grouped[lo...hi].sort_addr + lo]
|
|
411
|
+
end
|
|
412
|
+
out
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# @overload wsum(weights)
|
|
416
|
+
# Per-segment weighted sum as float64, matching `CArray#wsum`. `weights` is
|
|
417
|
+
# a per-cell weight CArray in the source order (same elements as the value).
|
|
418
|
+
# Empty / all-masked segment -> 0.0 (the additive identity). A cell is
|
|
419
|
+
# skipped iff its value OR its weight is masked (core's contract).
|
|
420
|
+
# @param weights [CArray]
|
|
421
|
+
# @return [CArray]
|
|
422
|
+
def wsum (weights, axis: nil)
|
|
423
|
+
return axis_wsum_wmean(weights, axis, :wsum)[0] if axis
|
|
424
|
+
kernel = MONOID_TYPES.include?(grouped.data_type)
|
|
425
|
+
wg = scatter_weights(weights, kernel)
|
|
426
|
+
return kernel_weighted(wg)[0] if kernel
|
|
427
|
+
fold_weighted(:wsum, wg, 0.0) { |v, ws| v.wsum(ws) }
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# @overload wmean(weights)
|
|
431
|
+
# Per-segment weighted mean as float64, matching `CArray#wmean`. Empty
|
|
432
|
+
# segment -> MASKED; a present segment whose weights sum to zero -> NaN
|
|
433
|
+
# (core's 0/0 contract).
|
|
434
|
+
# @param weights [CArray]
|
|
435
|
+
# @return [CArray]
|
|
436
|
+
def wmean (weights, axis: nil)
|
|
437
|
+
return axis_wsum_wmean(weights, axis, :wmean)[1] if axis
|
|
438
|
+
kernel = MONOID_TYPES.include?(grouped.data_type)
|
|
439
|
+
wg = scatter_weights(weights, kernel)
|
|
440
|
+
return kernel_weighted(wg)[1] if kernel
|
|
441
|
+
fold_weighted(:wmean, wg, UNDEF) { |v, ws| v.wmean(ws) }
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# @overload reduce { |members| ... }
|
|
445
|
+
# Custom per-segment reduction (the escape hatch for statistics not in the
|
|
446
|
+
# named surface), mirroring `CArray#reduce_slab`. The block receives each
|
|
447
|
+
# segment's members (a CArray) and returns one value per segment.
|
|
448
|
+
# @yieldparam members [CArray]
|
|
449
|
+
# @return [CArray] length-k
|
|
450
|
+
# @overload reduce(init) { |acc, elem| ... }
|
|
451
|
+
# Per-segment fold: each segment's members are folded element by
|
|
452
|
+
# element starting from `init`.
|
|
453
|
+
# @param init [Object] initial accumulator.
|
|
454
|
+
# @return [CArray] length-k
|
|
455
|
+
def reduce (*args, data_type: nil, &blk)
|
|
456
|
+
raise LocalJumpError, "no block given (yield)" unless blk
|
|
457
|
+
dt = data_type || CA_OBJECT
|
|
458
|
+
if args.empty?
|
|
459
|
+
per_segment(dt) { |s| blk.call(s) }
|
|
460
|
+
else
|
|
461
|
+
init = args[0]
|
|
462
|
+
per_segment(dt) { |s|
|
|
463
|
+
acc = init
|
|
464
|
+
s.each { |e| acc = blk.call(acc, e) }
|
|
465
|
+
acc
|
|
466
|
+
}
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# @overload map(data_type: nil) { |members| ... }
|
|
471
|
+
# Segment-wise element-wise transform, mirroring `CArray#map_slab`. The block
|
|
472
|
+
# receives each segment's members and returns either a same-length CArray
|
|
473
|
+
# (scattered back cell for cell) or a scalar (broadcast over the segment's
|
|
474
|
+
# cells). Returns a NEW CArray shaped like the source `value`; the original
|
|
475
|
+
# is not modified (`value[] = grp.map { ... }` for in-place). Excluded cells
|
|
476
|
+
# (in no segment) are UNDEF in the result.
|
|
477
|
+
#
|
|
478
|
+
# The result has the value's data type, or `data_type:` when it is given.
|
|
479
|
+
# A Face value gives a result of the same Face; a read-only Face (a
|
|
480
|
+
# categorical, a const string), which cannot be written cell by cell,
|
|
481
|
+
# gives an object array of its surface values.
|
|
482
|
+
#
|
|
483
|
+
# `axis:` is for a categorical group-by whose categorical classifies the
|
|
484
|
+
# positions along that axis: the block receives each category's positions
|
|
485
|
+
# along it, every other axis kept. A segment iterator has no axis: form.
|
|
486
|
+
# @yieldparam members [CArray]
|
|
487
|
+
# @return [CArray] shaped like the source value
|
|
488
|
+
def map (data_type: nil, axis: nil, &block)
|
|
489
|
+
raise LocalJumpError, "no block given (yield)" unless block
|
|
490
|
+
return axis_map(axis, data_type, &block) if axis
|
|
491
|
+
# Apply the block per segment, assembled in grouped (segment-contiguous)
|
|
492
|
+
# order: a same-length result scatters cell for cell, a scalar broadcasts.
|
|
493
|
+
scatter_segments(->(shape) { blank_output(shape, data_type) }, &block)
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# ---- segment scan: within-segment running statistics ------------------
|
|
497
|
+
#
|
|
498
|
+
# The per-element-emit siblings of the reductions: unlike a reduction (which
|
|
499
|
+
# collapses each segment to one value) a scan preserves the source shape,
|
|
500
|
+
# each cell holding its segment's running statistic up to and including that
|
|
501
|
+
# cell, in source (row-major) order. A segment is a partition (each cell is
|
|
502
|
+
# in at most one segment), so the running value is single-valued.
|
|
503
|
+
# A cell in no segment is UNDEF. A masked cell inside a segment holds the
|
|
504
|
+
# running value, as `CArray#cumsum` does.
|
|
505
|
+
#
|
|
506
|
+
# Each answers as the core scan of the same name does over the segment: the
|
|
507
|
+
# data type is the core's (`cumsum` of a boolean is uint64, of an integer
|
|
508
|
+
# float64), a Face keeps its Face (`cummin` of a time column is a time), and
|
|
509
|
+
# a scan the core refuses (`cumsum` of a time column) raises in the core's
|
|
510
|
+
# words. When the fused C kernel __axis_group_scan__ answers in that data
|
|
511
|
+
# type, it runs with the whole source as a single grouped axis and the
|
|
512
|
+
# segment of each cell (#codes) as the single bundle, emitting in source
|
|
513
|
+
# order; otherwise each segment is scanned by the core and scattered back.
|
|
514
|
+
|
|
515
|
+
# @!method cumsum
|
|
516
|
+
# Per-segment inclusive running sum, source-shaped.
|
|
517
|
+
# @return [CArray]
|
|
518
|
+
# @!method cumprod
|
|
519
|
+
# Per-segment inclusive running product, source-shaped.
|
|
520
|
+
# @return [CArray]
|
|
521
|
+
# @!method cummax
|
|
522
|
+
# Per-segment inclusive running maximum, source-shaped.
|
|
523
|
+
# @return [CArray]
|
|
524
|
+
# @!method cummin
|
|
525
|
+
# Per-segment inclusive running minimum, source-shaped.
|
|
526
|
+
# @return [CArray]
|
|
527
|
+
# @!method cumcount
|
|
528
|
+
# Per-segment 1-based within-segment ordinal (int64), source-shaped.
|
|
529
|
+
# @return [CArray]
|
|
530
|
+
[:cumsum, :cumprod, :cummax, :cummin, :cumcount].each do |op|
|
|
531
|
+
define_method(op) { |axis: nil| axis ? axis_scan(op, axis) : scan(op) }
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
private
|
|
535
|
+
|
|
536
|
+
# The contiguous copy every reduction works from. On a CACategoricalIterator
|
|
537
|
+
# it does not exist when the classifier does not line up cell-for-cell with the value;
|
|
538
|
+
# such an iterator answers the per-fiber form only, and says so here rather
|
|
539
|
+
# than letting a nil surface as whatever NoMethodError it reaches first.
|
|
540
|
+
def grouped
|
|
541
|
+
@grouped || raise(ArgumentError, @no_flat)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# Per-segment cell counts, alongside #grouped and unavailable for the same
|
|
545
|
+
# reason.
|
|
546
|
+
def counts
|
|
547
|
+
@elements || raise(ArgumentError, @no_flat)
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# A segment scan. When the axis-group scan kernel answers in the core's data
|
|
551
|
+
# type, it runs with the whole value as one grouped axis and the flat codes
|
|
552
|
+
# as the single bundle; it emits in source order, so the flat result
|
|
553
|
+
# reshapes straight back to the source shape. Otherwise each segment is
|
|
554
|
+
# scanned by the core and scattered back.
|
|
555
|
+
#
|
|
556
|
+
# With no segment at all the kernel has no group to size, so it is given
|
|
557
|
+
# one that no cell belongs to: every cell comes back UNDEF, as it does when
|
|
558
|
+
# there are segments and a cell is in none of them.
|
|
559
|
+
def scan (op)
|
|
560
|
+
CArray.__answer_as__(op) { scan_answered(op) }
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def scan_answered (op)
|
|
564
|
+
# An empty Face has no cell to ask the core about, and nothing to scan.
|
|
565
|
+
return blank_output(@src_shape) if @value.face? && @value.elements.zero?
|
|
566
|
+
answer = core_scan_answer(op)
|
|
567
|
+
unless kernel_scans?(op, answer)
|
|
568
|
+
return scatter_segments(->(shape) { blank_like(answer, shape) }) { |s| s.public_send(op) }
|
|
569
|
+
end
|
|
570
|
+
bundle = @k > 0 ? [codes, @k, [0]]
|
|
571
|
+
: [CArray.int64(@value.elements).fill(-1), 1, [0]]
|
|
572
|
+
scan_source.reshape(@value.elements)
|
|
573
|
+
.__axis_group_scan__([0], [bundle], op)
|
|
574
|
+
.reshape(*@src_shape)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# The core's scan of one cell of the value: the scan answers in its data
|
|
578
|
+
# type and Face. A scan the core refuses raises here, in the core's words.
|
|
579
|
+
def core_scan_answer (op)
|
|
580
|
+
(@core_scan_answer ||= {})[op] ||= core_probe.public_send(op)
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# Whether the axis-group scan kernel gives the core's answer. It reads
|
|
584
|
+
# an integer, float, boolean or object payload (not a complex, not a Face),
|
|
585
|
+
# and answers cumsum / cumprod in float64 (an
|
|
586
|
+
# object stays an object), cummax / cummin in the value's data type and
|
|
587
|
+
# cumcount in int64 -- which is not the core's answer for every data type
|
|
588
|
+
# (the core counts a boolean's running sum in uint64).
|
|
589
|
+
def kernel_scans? (op, answer)
|
|
590
|
+
return false if @value.face? || answer.face? || @value.complex?
|
|
591
|
+
kernel_type =
|
|
592
|
+
case op
|
|
593
|
+
when :cumcount then CA_INT64
|
|
594
|
+
when :cumsum, :cumprod then @value.data_type == CA_OBJECT ? CA_OBJECT : CA_FLOAT64
|
|
595
|
+
else @value.data_type
|
|
596
|
+
end
|
|
597
|
+
answer.data_type == kernel_type
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
# Applies the block to each segment's members in grouped order and scatters
|
|
601
|
+
# the answers back to the source cells; a cell in no segment is UNDEF.
|
|
602
|
+
# +blank+ builds an empty array of a given shape that can hold the answers.
|
|
603
|
+
def scatter_segments (blank)
|
|
604
|
+
out = blank.call(@src_shape)
|
|
605
|
+
out[] = UNDEF
|
|
606
|
+
return out if grouped.elements.zero?
|
|
607
|
+
transformed = blank.call([grouped.elements])
|
|
608
|
+
@k.times do |c|
|
|
609
|
+
lo, hi = @bounds[c], @bounds[c + 1]
|
|
610
|
+
transformed[lo...hi] = surface_for(transformed, yield(grouped[lo...hi])) if hi > lo
|
|
611
|
+
end
|
|
612
|
+
out.reshape(@value.elements)[perm] = transformed
|
|
613
|
+
out
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
# A blank array for #map: of +data_type+ when one is given, otherwise of the
|
|
617
|
+
# same kind as the value.
|
|
618
|
+
def blank_output (shape, data_type = nil)
|
|
619
|
+
data_type ? CArray.new(data_type, shape) : blank_like(@value, shape)
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
# An answer as +out+ can hold it. A Face goes into an object array as its
|
|
623
|
+
# surface values; stored as it is, the object array would receive its
|
|
624
|
+
# storage bytes.
|
|
625
|
+
def surface_for (out, answer)
|
|
626
|
+
return answer unless out.data_type == CA_OBJECT && answer.is_a?(CArray) && answer.face?
|
|
627
|
+
answer.to_a
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
# A blank array of +shape+ of the same kind as +model+: its data type, or its
|
|
631
|
+
# Face. A read-only Face cannot be written cell by cell, so its values are
|
|
632
|
+
# held as the surface objects they are.
|
|
633
|
+
def blank_like (model, shape)
|
|
634
|
+
if model.face? && model.read_only?
|
|
635
|
+
CArray.new(CA_OBJECT, shape)
|
|
636
|
+
else
|
|
637
|
+
model.__send__(:__blank_like__, shape)
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
# The values as they were when the iterator was built, in source order.
|
|
642
|
+
# Every no-axis reduction works from the contiguous copy taken then; the
|
|
643
|
+
# scans read @value, so a write through the source between two calls used to
|
|
644
|
+
# be visible to a cumsum and not to a sum, off one iterator.
|
|
645
|
+
#
|
|
646
|
+
# Rebuilt rather than copied a second time: @perm is exactly the classified
|
|
647
|
+
# cells and @grouped holds their values and their masks, which is everything
|
|
648
|
+
# a scan reads -- a cell classified by nothing is skipped on its code, before
|
|
649
|
+
# its value is looked at. So this costs nothing until a scan asks for it, and
|
|
650
|
+
# nothing at all for an iterator that never scans.
|
|
651
|
+
def scan_source
|
|
652
|
+
@scan_source ||=
|
|
653
|
+
begin
|
|
654
|
+
snap = @value.template
|
|
655
|
+
snap.reshape(snap.elements)[perm] = grouped
|
|
656
|
+
snap
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
# Permutation: perm[slot] = the source index whose value sits at that grouped
|
|
662
|
+
# slot, read by #map / #sort_addr / the *_addr reductions. A segment's copy
|
|
663
|
+
# is the source range itself, so this is a run of consecutive indices, built
|
|
664
|
+
# the first time it is asked for. (A CACategoricalIterator sets it at
|
|
665
|
+
# construction: the valid prefix of the categorical's cached sort_addr.)
|
|
666
|
+
def perm
|
|
667
|
+
@perm ||= CArray.int64(grouped.elements).seq!(@origin)
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
# The segment each source cell belongs to, -1 for a cell outside every
|
|
671
|
+
# segment. Read by #map, the scans and the weighted reductions; built the
|
|
672
|
+
# first time one of them asks, since the reductions do not need it.
|
|
673
|
+
def codes
|
|
674
|
+
@codes ||= begin
|
|
675
|
+
c = CArray.int64(@value.elements).fill(-1)
|
|
676
|
+
if grouped.elements > 0
|
|
677
|
+
c[@origin...(@origin + grouped.elements)] =
|
|
678
|
+
CArray.segment_index(offsets: @bounds)
|
|
679
|
+
end
|
|
680
|
+
c
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
# Lay a per-cell weight array out in segment-contiguous order (same layout as
|
|
685
|
+
# @grouped), so wsum / wmean can pair each segment's values with its weights.
|
|
686
|
+
# Weights are coerced to float64; the same counting-sort scatter propagates
|
|
687
|
+
# the weight mask and skips excluded cells, so wg lines up with @grouped.
|
|
688
|
+
# The weighted kernel reads float64 weights; the per-segment fallback hands
|
|
689
|
+
# them to the core as they are, so an object weight stays exact.
|
|
690
|
+
def scatter_weights (weights, as_float64 = true)
|
|
691
|
+
unless weights.elements == codes.elements
|
|
692
|
+
raise ArgumentError,
|
|
693
|
+
"wsum/wmean: weights.elements (#{weights.elements}) != " \
|
|
694
|
+
"value.elements (#{codes.elements})"
|
|
695
|
+
end
|
|
696
|
+
wf = as_float64 ? weights.float64 : weights
|
|
697
|
+
wg = CArray.new(wf.data_type, [grouped.elements], bytes: wf.bytes)
|
|
698
|
+
codes.send(:__categorical_scatter__, wf.reshape(wf.elements),
|
|
699
|
+
@offsets.copy, wg, @k)
|
|
700
|
+
wg
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
# Map a per-segment index to the flat source address via the
|
|
704
|
+
# permutation (grouped slot -> source index). The min/max sits at grouped slot
|
|
705
|
+
# offsets[c] + local_index[c]; perm carries it back to the source. Empty
|
|
706
|
+
# segments (masked local index) stay masked.
|
|
707
|
+
def segment_addr (local_index)
|
|
708
|
+
out = CArray.int64(@k)
|
|
709
|
+
@k.times do |c|
|
|
710
|
+
out[c] = local_index.is_masked[c] ? UNDEF
|
|
711
|
+
: perm[@offsets[c] + local_index[c]]
|
|
712
|
+
end
|
|
713
|
+
out
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
# Fused per-segment weighted sum + weighted mean (one C pass over the grouped
|
|
717
|
+
# copy, weights in segment order). Returns [wsum, wmean]; wmean is masked where a
|
|
718
|
+
# segment has no present (value AND weight) pair. Numeric value data types only.
|
|
719
|
+
def kernel_weighted (wg)
|
|
720
|
+
ws = CArray.float64(@k)
|
|
721
|
+
wm = CArray.float64(@k)
|
|
722
|
+
grouped.send(:__reduceat_wsum_wmean__, @offsets, wg, ws, wm)
|
|
723
|
+
[ws, wm]
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
# Per-segment weighted fallback for the value data types the kernel does not
|
|
727
|
+
# read (object): delegate each segment to CArray#wsum / #wmean, answering in
|
|
728
|
+
# the data type the core answers in. Empty segments take the given identity.
|
|
729
|
+
def fold_weighted (op, wg, empty)
|
|
730
|
+
out = CArray.new(core_reduce_type(op, CA_FLOAT64([[1.0]])), [@k])
|
|
731
|
+
@k.times do |c|
|
|
732
|
+
lo, hi = @bounds[c], @bounds[c + 1]
|
|
733
|
+
out[c] = hi > lo ? yield(grouped[lo...hi], wg[lo...hi]) : empty
|
|
734
|
+
end
|
|
735
|
+
out
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
# The members of segment `c` as a CArray slice of the grouped copy. An empty
|
|
739
|
+
# segment (zero-width segment) yields the shared empty array — a zero-length
|
|
740
|
+
# slice cannot be taken directly, and an empty array carries the same reduction
|
|
741
|
+
# contract we want (identity for sum, UNDEF for ratios).
|
|
742
|
+
def segment_slice (c)
|
|
743
|
+
lo, hi = @bounds[c], @bounds[c + 1]
|
|
744
|
+
hi > lo ? grouped[lo...hi] : @empty
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
# Single-pass reduceat moments (count / sum / min / max per segment), computed
|
|
748
|
+
# once over the grouped copy and cached — the whole point of the eager copy is
|
|
749
|
+
# that one scatter is followed by cheap single-pass reductions with no
|
|
750
|
+
# per-segment views. Nil for a non-numeric value data type (complex / object /
|
|
751
|
+
# bool), where the monoid reductions fall back to per_segment.
|
|
752
|
+
# numeric value data types the C moments kernel handles (int8..float64); bool /
|
|
753
|
+
# complex / object fall back to per_segment.
|
|
754
|
+
MONOID_TYPES = %i[int8 uint8 int16 uint16 int32 uint32
|
|
755
|
+
int64 uint64 float32 float64].freeze
|
|
756
|
+
|
|
757
|
+
def moments
|
|
758
|
+
return @moments if defined?(@moments)
|
|
759
|
+
@moments =
|
|
760
|
+
if MONOID_TYPES.include?(grouped.data_type)
|
|
761
|
+
dt = grouped.data_type
|
|
762
|
+
counts = CArray.int64(@k)
|
|
763
|
+
sums = CArray.float64(@k)
|
|
764
|
+
mins = CArray.new(dt, [@k])
|
|
765
|
+
maxs = CArray.new(dt, [@k])
|
|
766
|
+
grouped.send(:__reduceat_moments__, @offsets, counts, sums, mins, maxs)
|
|
767
|
+
{ count: counts, sum: sums, min: mins, max: maxs }
|
|
768
|
+
end
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
# Single-pass fused per-segment argmin / argmax (min_index / max_index),
|
|
772
|
+
# cached. Nil for a non-numeric value data type (fall back to per_segment).
|
|
773
|
+
def arg_minmax
|
|
774
|
+
return @arg_minmax if defined?(@arg_minmax)
|
|
775
|
+
@arg_minmax =
|
|
776
|
+
if MONOID_TYPES.include?(grouped.data_type)
|
|
777
|
+
mn = CArray.int64(@k)
|
|
778
|
+
mx = CArray.int64(@k)
|
|
779
|
+
grouped.send(:__reduceat_argminmax__, @offsets, mn, mx)
|
|
780
|
+
{ min: mn, max: mx }
|
|
781
|
+
end
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
# Single-pass fused per-segment boolean all / any, cached. Nil unless the
|
|
785
|
+
# value data type is boolean (fall back to per_segment, which raises like
|
|
786
|
+
# CArray#all on a non-boolean).
|
|
787
|
+
def all_any
|
|
788
|
+
return @all_any if defined?(@all_any)
|
|
789
|
+
@all_any =
|
|
790
|
+
if grouped.data_type == CA_BOOLEAN
|
|
791
|
+
a = CArray.boolean(@k)
|
|
792
|
+
o = CArray.boolean(@k)
|
|
793
|
+
grouped.send(:__reduceat_all_any__, @offsets, a, o)
|
|
794
|
+
{ all: a, any: o }
|
|
795
|
+
end
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
# The data type the core reduction `op` promotes this value to. Asked of the
|
|
799
|
+
# core itself -- a one-cell reduction of the value's type -- rather than
|
|
800
|
+
# restated here, so a per-segment answer cannot drift from `CArray#<op>`
|
|
801
|
+
# (`sum` on an integer promotes, `accumulate` stays, `min` / `max` keep the
|
|
802
|
+
# type but a boolean widens, `prod` on an object stays an object). A payload
|
|
803
|
+
# the core refuses to fold this way raises here, with the core's own error.
|
|
804
|
+
# Probes the core with a one-cell array of the value's data type and takes
|
|
805
|
+
# the answer's. Asks @value rather than the grouped copy, which has the same
|
|
806
|
+
# data type but does not exist on an iterator that answers only the
|
|
807
|
+
# per-fiber form -- and `accumulate(axis:)`, the one axis: member that needs
|
|
808
|
+
# this, is exactly the case that would have found it missing.
|
|
809
|
+
def core_reduce_type (op, *args)
|
|
810
|
+
(@core_reduce_type ||= {})[[op, args]] ||=
|
|
811
|
+
@value.face? && @value.elements.zero? ?
|
|
812
|
+
CA_OBJECT : core_probe.public_send(op, *args, axis: 1).data_type
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
# A one-cell array of the same kind as the values, so the core answers about
|
|
816
|
+
# the same thing the segment slices will hand back. For a Face that is a view
|
|
817
|
+
# of the values themselves rather than a blank: a Face cannot be allocated
|
|
818
|
+
# from its surface data type alone, and a blank storage array is not a valid
|
|
819
|
+
# Face for every one of them -- a const string's record indexes a shared
|
|
820
|
+
# pool, so a zeroed record points nowhere.
|
|
821
|
+
def core_probe
|
|
822
|
+
return CArray.new(@value.data_type, [1, 1], bytes: @value.bytes) unless @value.face?
|
|
823
|
+
@value.reshape(@value.elements)[[0]].reshape(1, 1)
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
# Whether an output can be built by lifting one. A Face is filled by writing
|
|
827
|
+
# surface values into storage, so this needs a Face that can be written
|
|
828
|
+
# into; a read-only one -- a const string's records index a shared pool, a
|
|
829
|
+
# categorical's codes index a vocabulary -- has no blank form to fill.
|
|
830
|
+
def face_output?
|
|
831
|
+
@value.face? && ! @value.read_only?
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
# Build a length-k typed output by folding each segment's members with the
|
|
835
|
+
# given reduction block. Fallback path (order statistics, and monoids on a
|
|
836
|
+
# non-numeric value data type): each segment is delegated to the same CArray
|
|
837
|
+
# reduction, so the per-segment result matches `CArray#<reduction>` over that
|
|
838
|
+
# segment's members — the mask carries the "insufficient present data" contract
|
|
839
|
+
# for free (an all-masked segment reduces like an empty one; identity-bearing
|
|
840
|
+
# reductions return their identity, ratios return UNDEF).
|
|
841
|
+
def per_segment (data_type)
|
|
842
|
+
out = if ! @value.face? || data_type != @value.data_type
|
|
843
|
+
bytes = (data_type == @value.data_type) ? @value.bytes : 0
|
|
844
|
+
CArray.new(data_type, [@k], bytes: bytes)
|
|
845
|
+
elsif face_output?
|
|
846
|
+
# the core answered in the values' own Face, so the output is one
|
|
847
|
+
# too: CATime#min hands back a CATime::Element, which only a
|
|
848
|
+
# CATime has anywhere to put
|
|
849
|
+
@value.__send__(:__blank_like__, [@k])
|
|
850
|
+
else
|
|
851
|
+
# a read-only Face cannot be filled, so its answers are collected
|
|
852
|
+
# as the surface objects they already are
|
|
853
|
+
CArray.new(CA_OBJECT, [@k])
|
|
854
|
+
end
|
|
855
|
+
# A member the core refuses for this Face still refuses: the segment slice is
|
|
856
|
+
# the Face, so the refusal comes from there, in the core's own words.
|
|
857
|
+
@k.times { |c| out[c] = yield(segment_slice(c)) }
|
|
858
|
+
out
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
# ---- no axis: form -------------------------------------------------------
|
|
862
|
+
|
|
863
|
+
def axis_by_masked_copy (*)
|
|
864
|
+
raise NotImplementedError, "#{self.class} has no axis: form"
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
def axis_counts (*)
|
|
868
|
+
raise NotImplementedError, "#{self.class} has no axis: form"
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
def axis_mean (*)
|
|
872
|
+
raise NotImplementedError, "#{self.class} has no axis: form"
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
def axis_moments (*)
|
|
876
|
+
raise NotImplementedError, "#{self.class} has no axis: form"
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
def axis_order_stat (*)
|
|
880
|
+
raise NotImplementedError, "#{self.class} has no axis: form"
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
def axis_scan (*)
|
|
884
|
+
raise NotImplementedError, "#{self.class} has no axis: form"
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
def axis_map (*)
|
|
888
|
+
raise NotImplementedError, "#{self.class} has no axis: form"
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
def axis_prod (*)
|
|
892
|
+
raise NotImplementedError, "#{self.class} has no axis: form"
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
def axis_sum (*)
|
|
896
|
+
raise NotImplementedError, "#{self.class} has no axis: form"
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
def axis_wsum_wmean (*)
|
|
900
|
+
raise NotImplementedError, "#{self.class} has no axis: form"
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
# The present cells per segment a weighted reduction counts: those whose
|
|
904
|
+
# value and weight are both present, as CArray#wsum / #wmean count them.
|
|
905
|
+
def present_pair_count (weights, axis)
|
|
906
|
+
if axis
|
|
907
|
+
paired = @value.copy
|
|
908
|
+
paired[weights.is_masked] = UNDEF if weights.has_mask?
|
|
909
|
+
return paired.group_by_category(@cat).count_not_masked(axis: axis)
|
|
910
|
+
end
|
|
911
|
+
both = grouped.is_not_masked & scatter_weights(weights).is_not_masked
|
|
912
|
+
both.int32.segments(offsets: @bounds).sum # one pass, unlike count(true)
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
# min_count: and fill_value: on a value reduction, as a core reduction
|
|
916
|
+
# takes them: a segment with fewer than min_count present values is
|
|
917
|
+
# UNDEF, and fill_value then fills the UNDEF cells, those min_count made
|
|
918
|
+
# and those the reduction answers for an empty segment. Each reduction
|
|
919
|
+
# is left to answer as it does; this only masks and fills its result.
|
|
920
|
+
# A weighted reduction counts the cells whose value and weight are both
|
|
921
|
+
# present.
|
|
922
|
+
module ReductionCountAndFill
|
|
923
|
+
REDUCTIONS = %i[sum accumulate max min minmax mean median percentile
|
|
924
|
+
variance stddev variancep stddevp prod wsum wmean
|
|
925
|
+
min_index max_index min_addr max_addr
|
|
926
|
+
count count_not_masked count_masked].freeze
|
|
927
|
+
|
|
928
|
+
# A count of the mask reads every cell of the segment, as the core's does.
|
|
929
|
+
def self.mask_count? (op, args)
|
|
930
|
+
op == :count_not_masked || op == :count_masked ||
|
|
931
|
+
(op == :count && (args.empty? || args[0].equal?(UNDEF)))
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
REDUCTIONS.each do |op|
|
|
935
|
+
define_method(op) do |*args, min_count: nil, fill_value: nil, **kw|
|
|
936
|
+
# The segment reduction already leaves an empty segment as the core
|
|
937
|
+
# does, so only a min_count asking for more is applied here.
|
|
938
|
+
need = CArray.__min_count_need__(op, min_count, 0)
|
|
939
|
+
# an error from the core reductions a member is made of names the member
|
|
940
|
+
result = CArray.__answer_as__(op) { super(*args, **kw) }
|
|
941
|
+
return result if need == 0 && fill_value.nil?
|
|
942
|
+
if need > 0
|
|
943
|
+
present = if op == :wsum || op == :wmean
|
|
944
|
+
present_pair_count(args[0], kw[:axis])
|
|
945
|
+
elsif ReductionCountAndFill.mask_count?(op, args)
|
|
946
|
+
if kw[:axis]
|
|
947
|
+
raise NotImplementedError,
|
|
948
|
+
"#{self.class}##{op}(axis:, min_count:) is not implemented"
|
|
949
|
+
end
|
|
950
|
+
elements
|
|
951
|
+
else
|
|
952
|
+
count_not_masked(**kw)
|
|
953
|
+
end
|
|
954
|
+
short = present.lt(need)
|
|
955
|
+
end
|
|
956
|
+
if short
|
|
957
|
+
(result.is_a?(Array) ? result : [result]).each { |r| r[short] = UNDEF }
|
|
958
|
+
end
|
|
959
|
+
CArray.__reduce_fill__(result, fill_value)
|
|
960
|
+
end
|
|
961
|
+
end
|
|
962
|
+
end
|
|
963
|
+
prepend ReductionCountAndFill
|
|
964
|
+
|
|
965
|
+
end
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
class CArray
|
|
969
|
+
# @overload segments(offsets:)
|
|
970
|
+
# Returns a {CASegmentIterator} that reduces `self` segment by segment,
|
|
971
|
+
# segment `c` being the cells `offsets[c]...offsets[c + 1]` in flatten
|
|
972
|
+
# order. Cells outside `offsets[0]...offsets[-1]` belong to no segment.
|
|
973
|
+
#
|
|
974
|
+
# data.segments(offsets: [0, 2, 2, 5]).sum # one sum per segment
|
|
975
|
+
#
|
|
976
|
+
# @param offsets [CArray, Array<Integer>] `k + 1` non-decreasing
|
|
977
|
+
# boundaries within `0..elements`.
|
|
978
|
+
# @return [CASegmentIterator]
|
|
979
|
+
# @overload segments(lengths:)
|
|
980
|
+
# The same from the `k` segment lengths, the first segment starting at
|
|
981
|
+
# cell 0.
|
|
982
|
+
# @param lengths [CArray, Array<Integer>] one count per segment.
|
|
983
|
+
# @return [CASegmentIterator]
|
|
984
|
+
# @raise [ArgumentError] for decreasing or empty offsets, a negative or
|
|
985
|
+
# masked length, segments reaching outside `self`, or unless exactly one
|
|
986
|
+
# of `offsets:` and `lengths:` is given.
|
|
987
|
+
def segments (offsets: nil, lengths: nil)
|
|
988
|
+
CASegmentIterator.new(self, offsets: offsets, lengths: lengths)
|
|
989
|
+
end
|
|
990
|
+
end
|