carray 3.0.2 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +568 -0
  3. data/README.md +6 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_descriptor.h +27 -24
  6. data/ext/ca_axis_dispatch.c +220 -0
  7. data/ext/ca_axis_group.c +201 -123
  8. data/ext/ca_binop_dispatch.c +46 -52
  9. data/ext/ca_binop_dispatch.h +1 -1
  10. data/ext/ca_categorical_iterator.c +249 -183
  11. data/ext/ca_compare.c +7 -12
  12. data/ext/ca_for_buffer.h +21 -68
  13. data/ext/ca_group_iter.c +38 -23
  14. data/ext/ca_kernel_iterator.c +1765 -2076
  15. data/ext/ca_kernel_iterator.h +584 -566
  16. data/ext/ca_monop_dispatch.c +86 -63
  17. data/ext/ca_monop_dispatch.h +5 -0
  18. data/ext/ca_obj_array.c +223 -56
  19. data/ext/ca_obj_bincmp.c +172 -138
  20. data/ext/ca_obj_binop.c +116 -230
  21. data/ext/ca_obj_bitarray.c +40 -26
  22. data/ext/ca_obj_bitfield.c +100 -68
  23. data/ext/ca_obj_block.c +19 -9
  24. data/ext/ca_obj_byte_swap.c +45 -46
  25. data/ext/ca_obj_const_string.c +269 -44
  26. data/ext/ca_obj_face.c +612 -89
  27. data/ext/ca_obj_face.h +111 -47
  28. data/ext/ca_obj_fake.c +36 -79
  29. data/ext/ca_obj_farray.c +7 -3
  30. data/ext/ca_obj_field.c +33 -8
  31. data/ext/ca_obj_fixlen_string.c +23 -24
  32. data/ext/ca_obj_grid.c +541 -210
  33. data/ext/ca_obj_meld.c +331 -111
  34. data/ext/ca_obj_moncmp.c +80 -65
  35. data/ext/ca_obj_monop.c +177 -149
  36. data/ext/ca_obj_object.c +664 -248
  37. data/ext/ca_obj_record.c +63 -23
  38. data/ext/ca_obj_reduce.c +18 -16
  39. data/ext/ca_obj_refer.c +140 -58
  40. data/ext/ca_obj_remap.c +85 -24
  41. data/ext/ca_obj_repeat.c +24 -9
  42. data/ext/ca_obj_roll.c +121 -16
  43. data/ext/ca_obj_select.c +61 -58
  44. data/ext/ca_obj_select_axis.c +73 -107
  45. data/ext/ca_obj_shift.c +32 -102
  46. data/ext/ca_obj_stack.c +372 -122
  47. data/ext/ca_obj_stride.c +348 -140
  48. data/ext/ca_obj_string.c +88 -22
  49. data/ext/ca_obj_tile.c +38 -21
  50. data/ext/ca_obj_time.c +45 -31
  51. data/ext/ca_obj_timedelta.c +43 -29
  52. data/ext/ca_obj_transpose.c +17 -6
  53. data/ext/ca_obj_triop.c +125 -141
  54. data/ext/ca_obj_window.c +330 -44
  55. data/ext/ca_op_ipower.c +24 -11
  56. data/ext/ca_op_powi.h +57 -30
  57. data/ext/ca_pow5_table.h +656 -0
  58. data/ext/ca_sort_kernels.h +70 -0
  59. data/ext/ca_sweep_engine.c +104 -81
  60. data/ext/ca_sweep_engine.h +7 -20
  61. data/ext/ca_triop_dispatch.c +4 -1
  62. data/ext/ca_triop_dispatch.h +5 -2
  63. data/ext/caframe_csv_reader.c +542 -0
  64. data/ext/carray.h +241 -92
  65. data/ext/carray_access.c +608 -276
  66. data/ext/carray_address_basis.c +494 -97
  67. data/ext/carray_attribute.c +129 -4
  68. data/ext/carray_bincount.c +39 -7
  69. data/ext/carray_broadcast.c +56 -19
  70. data/ext/carray_build_flags.h +1 -1
  71. data/ext/carray_call_cfunc.c +108 -28
  72. data/ext/carray_cast.c +1576 -223
  73. data/ext/carray_cast_func.rb +77 -24
  74. data/ext/carray_class.c +4 -3
  75. data/ext/carray_conversion.c +208 -75
  76. data/ext/carray_copy.c +82 -24
  77. data/ext/carray_core.c +646 -162
  78. data/ext/carray_count.c +171 -88
  79. data/ext/carray_element.c +94 -60
  80. data/ext/carray_factorize.c +670 -324
  81. data/ext/carray_generate.c +260 -83
  82. data/ext/carray_histogram.c +209 -106
  83. data/ext/carray_hold.c +14 -16
  84. data/ext/carray_index_classifier.c +422 -243
  85. data/ext/carray_internal.h +692 -3
  86. data/ext/carray_kernels_bincmp.c +7 -0
  87. data/ext/carray_kernels_binop.c +487 -120
  88. data/ext/carray_kernels_init.c +653 -50
  89. data/ext/carray_kernels_moncmp.c +15 -8
  90. data/ext/carray_kernels_monop.c +99 -43
  91. data/ext/carray_kernels_reduce_aggregate.c +3925 -6112
  92. data/ext/carray_kernels_reduce_boolean.c +112 -36
  93. data/ext/carray_kernels_reduce_cumulative.c +2127 -6903
  94. data/ext/carray_kernels_reduce_extreme.c +3032 -2892
  95. data/ext/carray_kernels_reduce_variance.c +439 -500
  96. data/ext/carray_kernels_scan.c +1097 -488
  97. data/ext/carray_kernels_search.c +3787 -2176
  98. data/ext/carray_kernels_sort.c +1200 -920
  99. data/ext/carray_kernels_triop.c +479 -7
  100. data/ext/carray_key_windows.c +216 -0
  101. data/ext/carray_lazy.c +967 -153
  102. data/ext/carray_loop.c +81 -65
  103. data/ext/carray_mask.c +910 -324
  104. data/ext/carray_math_kernel.h +1 -0
  105. data/ext/carray_median_percentile.c +299 -100
  106. data/ext/carray_memory_view.c +872 -758
  107. data/ext/carray_operator.c +1171 -364
  108. data/ext/carray_order.c +505 -133
  109. data/ext/carray_partition.c +112 -65
  110. data/ext/carray_random.c +327 -239
  111. data/ext/carray_round.c +273 -0
  112. data/ext/carray_scatter.c +346 -78
  113. data/ext/carray_segment.c +257 -0
  114. data/ext/carray_slab.c +249 -86
  115. data/ext/carray_sort.c +224 -227
  116. data/ext/carray_sort_kernel.c +8 -1
  117. data/ext/carray_struct.c +118 -39
  118. data/ext/carray_test.c +193 -69
  119. data/ext/carray_time_text.c +1182 -0
  120. data/ext/carray_undef.c +4 -0
  121. data/ext/carray_utils.c +579 -113
  122. data/ext/extconf.rb +27 -2
  123. data/ext/mk_call_cfunc.rb +19 -4
  124. data/ext/mk_pow5_table.rb +35 -0
  125. data/ext/mkkernel.rb +1202 -1014
  126. data/ext/ruby_carray.c +17 -5
  127. data/ext/version.h +4 -4
  128. data/ext/version.rb +4 -2
  129. data/lib/carray/attribute.rb +160 -17
  130. data/lib/carray/autoload_carray.rb +23 -27
  131. data/lib/carray/autoload_method_extension.rb +20 -5
  132. data/lib/carray/axis_group.rb +360 -63
  133. data/lib/carray/basics.rb +162 -28
  134. data/lib/carray/bincount_nd.rb +18 -2
  135. data/lib/carray/block_iterator.rb +231 -58
  136. data/lib/carray/boolean_reduce.rb +34 -42
  137. data/lib/carray/categorical.rb +242 -52
  138. data/lib/carray/categorical_iterator.rb +437 -827
  139. data/lib/carray/complex.rb +41 -34
  140. data/lib/carray/conditional.rb +150 -14
  141. data/lib/carray/const_string.rb +59 -4
  142. data/lib/carray/construct.rb +15 -11
  143. data/lib/carray/data_type_extension.rb +1 -0
  144. data/lib/carray/distance_limit.rb +77 -0
  145. data/lib/carray/fixlen_string.rb +7 -1
  146. data/lib/carray/frame/column_verbs.rb +70 -0
  147. data/lib/carray/frame/concat.rb +157 -6
  148. data/lib/carray/frame/convert.rb +82 -7
  149. data/lib/carray/frame/csv_parser.rb +688 -87
  150. data/lib/carray/frame/describe.rb +80 -0
  151. data/lib/carray/frame/frame.rb +369 -45
  152. data/lib/carray/frame/group.rb +631 -90
  153. data/lib/carray/frame/io.rb +506 -79
  154. data/lib/carray/frame/join.rb +411 -62
  155. data/lib/carray/frame/records.rb +116 -19
  156. data/lib/carray/frame/reshape.rb +455 -0
  157. data/lib/carray/frame/sort.rb +30 -18
  158. data/lib/carray/frame/verbs.rb +868 -61
  159. data/lib/carray/frame/windows.rb +117 -0
  160. data/lib/carray/frame.rb +4 -0
  161. data/lib/carray/fuse_source.rb +118 -2
  162. data/lib/carray/fusion.rb +257 -42
  163. data/lib/carray/histogram.rb +6 -1
  164. data/lib/carray/inspect.rb +54 -13
  165. data/lib/carray/iterator.rb +99 -9
  166. data/lib/carray/key_windows.rb +620 -0
  167. data/lib/carray/lazy.rb +216 -193
  168. data/lib/carray/mask_gap_fill.rb +112 -92
  169. data/lib/carray/math.rb +32 -19
  170. data/lib/carray/meld_reduce.rb +52 -21
  171. data/lib/carray/methods/align_addr.rb +8 -4
  172. data/lib/carray/methods/bin.rb +26 -18
  173. data/lib/carray/methods/bincount.rb +18 -2
  174. data/lib/carray/methods/bit_string.rb +15 -4
  175. data/lib/carray/methods/choose.rb +16 -3
  176. data/lib/carray/methods/composition.rb +18 -10
  177. data/lib/carray/methods/factorize.rb +2 -2
  178. data/lib/carray/methods/gather_nd.rb +25 -5
  179. data/lib/carray/methods/insert_block.rb +9 -14
  180. data/lib/carray/methods/is_between.rb +59 -0
  181. data/lib/carray/methods/is_in.rb +140 -10
  182. data/lib/carray/methods/join.rb +2 -7
  183. data/lib/carray/methods/lag.rb +49 -0
  184. data/lib/carray/methods/locate_addr.rb +182 -45
  185. data/lib/carray/methods/mode.rb +18 -1
  186. data/lib/carray/methods/nunique.rb +11 -6
  187. data/lib/carray/methods/repeat.rb +1 -20
  188. data/lib/carray/methods/resize.rb +21 -23
  189. data/lib/carray/methods/snap.rb +34 -12
  190. data/lib/carray/methods/unique.rb +11 -2
  191. data/lib/carray/methods/value_counts.rb +4 -11
  192. data/lib/carray/record_order.rb +208 -0
  193. data/lib/carray/rng.rb +3 -0
  194. data/lib/carray/runtime.rb +65 -17
  195. data/lib/carray/segment_iterator.rb +1221 -0
  196. data/lib/carray/serialize.rb +214 -30
  197. data/lib/carray/slab_iterator.rb +101 -27
  198. data/lib/carray/stack.rb +10 -4
  199. data/lib/carray/string.rb +105 -5
  200. data/lib/carray/string_operation_extension.rb +12 -4
  201. data/lib/carray/struct.rb +137 -58
  202. data/lib/carray/struct_builder.rb +124 -17
  203. data/lib/carray/time.rb +1222 -186
  204. data/lib/carray/window_iterator.rb +242 -81
  205. data/lib/carray.rb +9 -2
  206. data/yard-stubs/ca_obj_array.rb +4 -4
  207. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  208. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  209. data/yard-stubs/ca_obj_fake.rb +1 -1
  210. data/yard-stubs/ca_obj_field.rb +5 -1
  211. data/yard-stubs/ca_obj_grid.rb +21 -10
  212. data/yard-stubs/ca_obj_refer.rb +5 -4
  213. data/yard-stubs/ca_obj_roll.rb +2 -3
  214. data/yard-stubs/ca_obj_shift.rb +5 -1
  215. data/yard-stubs/ca_obj_stride.rb +10 -7
  216. data/yard-stubs/ca_obj_tile.rb +1 -1
  217. data/yard-stubs/ca_obj_transpose.rb +1 -1
  218. data/yard-stubs/ca_obj_window.rb +9 -5
  219. data/yard-stubs/carray_access.rb +11 -5
  220. data/yard-stubs/carray_attribute.rb +0 -5
  221. data/yard-stubs/carray_broadcast.rb +1 -1
  222. data/yard-stubs/carray_cast.rb +88 -43
  223. data/yard-stubs/carray_conversion.rb +15 -2
  224. data/yard-stubs/carray_copy.rb +12 -5
  225. data/yard-stubs/carray_core.rb +3 -3
  226. data/yard-stubs/carray_count.rb +9 -0
  227. data/yard-stubs/carray_element.rb +10 -4
  228. data/yard-stubs/carray_generate.rb +21 -1
  229. data/yard-stubs/carray_lazy.rb +5 -1
  230. data/yard-stubs/carray_loop.rb +4 -1
  231. data/yard-stubs/carray_mask.rb +60 -12
  232. data/yard-stubs/carray_math.rb +52 -14
  233. data/yard-stubs/carray_median_percentile.rb +15 -8
  234. data/yard-stubs/carray_memory_view.rb +40 -23
  235. data/yard-stubs/carray_order.rb +61 -29
  236. data/yard-stubs/carray_random.rb +13 -4
  237. data/yard-stubs/carray_scatter.rb +21 -9
  238. data/yard-stubs/carray_segment.rb +72 -0
  239. data/yard-stubs/carray_slab.rb +10 -2
  240. data/yard-stubs/carray_sort.rb +49 -8
  241. data/yard-stubs/carray_test.rb +15 -3
  242. data/yard-stubs/carray_utils.rb +0 -26
  243. metadata +19 -5
  244. data/ext/ca_for_each_element.h +0 -239
  245. data/ext/carray_kernels_map.c +0 -3467
  246. data/lib/carray/arrow_tensor.rb +0 -401
  247. data/lib/carray/table.rb +0 -108
@@ -0,0 +1,1221 @@
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_boundaries_from_lengths_or_offsets__("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
+ : value.reshape(value.elements)[0...0].copy
62
+ @empty = @grouped[0...0] # an empty segment is the values' own kind
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_kind(: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_kind(: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_kind(: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_kind(: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_kind(: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 kernel_reads?
224
+ return per_segment(core_reduce_kind(: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 kernel_reads?
239
+ return CArray.__name_errors_after_method__(: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_kind(: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_kind(: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_kind(:prod)) { |s| s.prod } unless kernel_reads?
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
+ # @overload first
308
+ # Returns the first present value of each segment, in the value's data
309
+ # type (matching `CArray#first`). A segment with no present value is
310
+ # UNDEF.
311
+ # @return [CArray]
312
+ def first (axis: nil)
313
+ return axis_by_core(axis, :first) if axis
314
+ edge_values(:first)
315
+ end
316
+
317
+ # @overload last
318
+ # Returns the last present value of each segment, in the value's data
319
+ # type (matching `CArray#last`). A segment with no present value is
320
+ # UNDEF.
321
+ # @return [CArray]
322
+ def last (axis: nil)
323
+ return axis_by_core(axis, :last) if axis
324
+ edge_values(:last)
325
+ end
326
+
327
+ # @overload nunique
328
+ # Returns the number of distinct present values in each segment as int64,
329
+ # with the distinctness of `CArray#nunique` (every NaN is one value,
330
+ # -0.0 is 0.0). A segment with no present value counts 0.
331
+ # @return [CArray]
332
+ def nunique (axis: nil)
333
+ return axis_by_core(axis, :nunique) if axis
334
+ return per_segment(CA_INT64) { |s| s.nunique } unless CArray.__fast_paths_enabled__
335
+ out = CArray.int64(@k)
336
+ return out if grouped.elements.zero?
337
+ # Each cell's value becomes a code, so a distinct (segment, value) pair is
338
+ # a distinct int64 key; the keys' segments, counted, are the answer.
339
+ values = grouped.categorize
340
+ width = [values.labels.size, 1].max
341
+ keys = CArray.segment_index(offsets: @bounds) * width + values.codes.int64
342
+ owners = keys.unique / width
343
+ return out if owners.elements.zero?
344
+ counts = owners.bincount
345
+ out[0...counts.elements] = counts
346
+ out
347
+ end
348
+
349
+ # ---- tier 2 (fused / population / position) ------------------------------
350
+
351
+ # @overload minmax
352
+ # Returns the per-segment `[min, max]` pair (each a length-k CArray in the
353
+ # value data type; empty segments MASKED), matching `CArray#minmax`. Both come
354
+ # from one moments pass.
355
+ # @return [Array<CArray>]
356
+ def minmax(axis: nil)
357
+ if axis
358
+ # take both off one pass rather than asking min and max separately,
359
+ # which would run the kernel twice now that nothing is kept between
360
+ # calls -- this is the "keep the result" the axis: family expects
361
+ m = axis_moments(axis, :minmax)
362
+ return [m[:min], m[:max]]
363
+ end
364
+ [min, max]
365
+ end
366
+
367
+ # @overload variancep
368
+ # Per-segment POPULATION variance (ddof=0) as float64, matching
369
+ # `CArray#variancep`: empty / all-masked -> MASKED, single value -> 0.0
370
+ # (NaN when that value is a NaN or an infinity).
371
+ # Derived from the sample variance (variancep = variance * (n-1) / n), so it
372
+ # reuses the centred two-pass kernel with no extra walk.
373
+ # @return [CArray]
374
+ def variancep(axis: nil)
375
+ return axis_by_masked_copy(axis, :variancep) if axis
376
+ m = moments
377
+ return per_segment(core_reduce_kind(:variancep)) { |s| s.variancep } unless m
378
+ cnt = m[:count]
379
+ vp = variance * (cnt - 1).float64 / cnt.float64
380
+ one = cnt.eq(1) # one value: defined, unlike variance --
381
+ vp[one] = (m[:sum] - m[:sum])[one] # 0.0, or NaN for a NaN or an infinity
382
+ vp
383
+ end
384
+
385
+ # @overload stddevp
386
+ # Per-segment POPULATION standard deviation (ddof=0) as float64.
387
+ # @return [CArray]
388
+ def stddevp(axis: nil)
389
+ return axis_by_masked_copy(axis, :stddevp) if axis
390
+ m = moments
391
+ return per_segment(core_reduce_kind(:stddevp)) { |s| s.stddevp } unless m
392
+ variancep.sqrt
393
+ end
394
+
395
+ # @overload min_index
396
+ # Per-segment index of the minimum — the position within the
397
+ # segment's members (source order) — matching `CArray#min_index` per segment.
398
+ # Empty / all-masked segments are MASKED. Single-pass fused reduceat for
399
+ # numeric values; per-segment fallback otherwise.
400
+ # @return [CArray] length-k int64
401
+ def min_index
402
+ am = arg_minmax
403
+ am ? am[:min].copy : per_segment(CA_INT64) { |s| s.min_index }
404
+ end
405
+
406
+ # @overload max_index
407
+ # Per-segment index of the maximum. See {#min_index}.
408
+ # @return [CArray] length-k int64
409
+ def max_index
410
+ am = arg_minmax
411
+ am ? am[:max].copy : per_segment(CA_INT64) { |s| s.max_index }
412
+ end
413
+
414
+ # @overload min_addr
415
+ # Per-segment flat source address of the minimum — which cell of the source
416
+ # value holds it, matching `CArray#min_addr` per segment. Unlike {#min_index}
417
+ # (the position within the segment) this indexes back into the original array
418
+ # (`value.reshape(value.elements)[grp.min_addr]`). Empty segments MASKED.
419
+ # @return [CArray] length-k int64
420
+ def min_addr
421
+ segment_addr(min_index)
422
+ end
423
+
424
+ # @overload max_addr
425
+ # Per-segment flat source address of the maximum. See {#min_addr}.
426
+ # @return [CArray] length-k int64
427
+ def max_addr
428
+ segment_addr(max_index)
429
+ end
430
+
431
+ # @overload sort_addr
432
+ # Per-segment sort by flat source address. Returns a length-nvalid
433
+ # (= `elements.sum`) int64 CArray of the flat SOURCE addresses that sort each
434
+ # segment's members, segment by segment: part `c` holds segment `c`'s
435
+ # source addresses in ascending-value order, parts concatenated in
436
+ # order. So `value.reshape(value.elements)[grp.sort_addr]` yields
437
+ # the values segment by segment, sorted within each, and splitting by the
438
+ # {#elements} prefix sum gives per-segment. Excluded cells (in no segment) are
439
+ # omitted. A masked value sorts to the tail of its segment (as `CArray#sort`
440
+ # sends masked cells to the end), so with a mask the first address is the
441
+ # minimum but the last is the masked cell, not the maximum.
442
+ #
443
+ # Unlike {#min_index} / {#max_index} (positions within a segment), this
444
+ # indexes back into the original array; only the source-address form is
445
+ # offered, mirroring {#min_addr}.
446
+ # @return [CArray] length-nvalid int64
447
+ def sort_addr
448
+ out = CArray.int64(grouped.elements)
449
+ @k.times do |c|
450
+ lo, hi = @bounds[c], @bounds[c + 1]
451
+ next unless hi > lo
452
+ # View-local sort order of the segment (0..size-1), lifted to grouped
453
+ # slots, then mapped back to source addresses via perm.
454
+ out[lo...hi] = perm[grouped[lo...hi].sort_addr + lo]
455
+ end
456
+ out
457
+ end
458
+
459
+ # @overload wsum(weights)
460
+ # Per-segment weighted sum as float64, matching `CArray#wsum`. `weights` is
461
+ # a per-cell weight CArray in the source order (same elements as the value).
462
+ # Empty / all-masked segment -> 0.0 (the additive identity). A cell is
463
+ # skipped iff its value OR its weight is masked (core's contract).
464
+ # @param weights [CArray]
465
+ # @return [CArray]
466
+ def wsum (weights, axis: nil)
467
+ return axis_wsum_wmean(weights, axis, :wsum)[0] if axis
468
+ kernel = kernel_reads?
469
+ wg = scatter_weights(weights, kernel)
470
+ return kernel_weighted(wg)[0] if kernel
471
+ fold_weighted(:wsum, wg, 0.0) { |v, ws| v.wsum(ws) }
472
+ end
473
+
474
+ # @overload wmean(weights)
475
+ # Per-segment weighted mean as float64, matching `CArray#wmean`. Empty
476
+ # segment -> MASKED; a present segment whose weights sum to zero -> NaN
477
+ # (core's 0/0 contract).
478
+ # @param weights [CArray]
479
+ # @return [CArray]
480
+ def wmean (weights, axis: nil)
481
+ return axis_wsum_wmean(weights, axis, :wmean)[1] if axis
482
+ kernel = kernel_reads?
483
+ wg = scatter_weights(weights, kernel)
484
+ return kernel_weighted(wg)[1] if kernel
485
+ fold_weighted(:wmean, wg, UNDEF) { |v, ws| v.wmean(ws) }
486
+ end
487
+
488
+ # @overload reduce { |members| ... }
489
+ # Custom per-segment reduction (the escape hatch for statistics not in the
490
+ # named surface), mirroring `CArray#reduce_slab`. The block receives each
491
+ # segment's members (a CArray) and returns one value per segment.
492
+ # @yieldparam members [CArray]
493
+ # @return [CArray] length-k
494
+ # @overload reduce(init) { |acc, elem| ... }
495
+ # Per-segment fold: each segment's members are folded element by
496
+ # element starting from `init`.
497
+ # @param init [Object] initial accumulator.
498
+ # @return [CArray] length-k
499
+ def reduce (*args, data_type: nil, &blk)
500
+ raise LocalJumpError, "no block given (yield)" unless blk
501
+ dt = data_type || CA_OBJECT
502
+ if args.empty?
503
+ per_segment(dt) { |s| blk.call(s) }
504
+ else
505
+ init = args[0]
506
+ per_segment(dt) { |s|
507
+ acc = init
508
+ s.each { |e| acc = blk.call(acc, e) }
509
+ acc
510
+ }
511
+ end
512
+ end
513
+
514
+ # @overload map(data_type: nil) { |members| ... }
515
+ # Segment-wise element-wise transform, mirroring `CArray#map_slab`. The block
516
+ # receives each segment's members and returns either a same-length CArray
517
+ # (scattered back cell for cell) or a scalar (broadcast over the segment's
518
+ # cells). Returns a NEW CArray shaped like the source `value`; the original
519
+ # is not modified (`value[] = grp.map { ... }` for in-place). Excluded cells
520
+ # (in no segment) are UNDEF in the result.
521
+ #
522
+ # The result has the value's data type, or `data_type:` when it is given.
523
+ # A Face value gives a result of the same Face; a read-only Face (a
524
+ # categorical, a const string), which cannot be written cell by cell,
525
+ # gives an object array of its surface values.
526
+ #
527
+ # `axis:` is for a categorical group-by whose categorical classifies the
528
+ # positions along that axis: the block receives each category's positions
529
+ # along it, every other axis kept. A segment iterator has no axis: form.
530
+ # @yieldparam members [CArray]
531
+ # @return [CArray] shaped like the source value
532
+ def map (data_type: nil, axis: nil, &block)
533
+ raise LocalJumpError, "no block given (yield)" unless block
534
+ return axis_map(axis, data_type, &block) if axis
535
+ # Apply the block per segment, assembled in grouped (segment-contiguous)
536
+ # order: a same-length result scatters cell for cell, a scalar broadcasts.
537
+ scatter_segments(->(shape) { blank_output(shape, data_type) }, &block)
538
+ end
539
+
540
+ # ---- segment scan: within-segment running statistics ------------------
541
+ #
542
+ # The per-element-emit siblings of the reductions: unlike a reduction (which
543
+ # collapses each segment to one value) a scan preserves the source shape,
544
+ # each cell holding its segment's running statistic up to and including that
545
+ # cell, in source (row-major) order. A segment is a partition (each cell is
546
+ # in at most one segment), so the running value is single-valued.
547
+ # A cell in no segment is UNDEF. A masked cell inside a segment holds the
548
+ # running value, as `CArray#cumsum` does.
549
+ #
550
+ # Each answers as the core scan of the same name does over the segment: the
551
+ # data type is the core's (`cumsum` of a boolean is uint64, of an integer
552
+ # float64), a Face keeps its Face (`cummin` of a time column is a time), and
553
+ # a scan the core refuses (`cumsum` of a time column) raises in the core's
554
+ # words. When the fused C kernel __scan_by_axis_groups__ answers in that data
555
+ # type, it runs with the whole source as a single grouped axis and the
556
+ # segment of each cell (#codes) as the single bundle, emitting in source
557
+ # order; otherwise each segment is scanned by the core and scattered back.
558
+
559
+ # @!method cumsum
560
+ # Per-segment inclusive running sum, source-shaped.
561
+ # @return [CArray]
562
+ # @!method cumprod
563
+ # Per-segment inclusive running product, source-shaped.
564
+ # @return [CArray]
565
+ # @!method cummax
566
+ # Per-segment inclusive running maximum, source-shaped.
567
+ # @return [CArray]
568
+ # @!method cummin
569
+ # Per-segment inclusive running minimum, source-shaped.
570
+ # @return [CArray]
571
+ # @!method cumcount
572
+ # Per-segment 1-based within-segment ordinal (int64), source-shaped.
573
+ # @return [CArray]
574
+ CArray.send(:define_named_methods, self,
575
+ [:cumsum, :cumprod, :cummax, :cummin, :cumcount], :segment_scan)
576
+
577
+ private def segment_scan (op, axis: nil)
578
+ axis ? axis_scan(op, axis) : scan(op)
579
+ end
580
+
581
+ # @overload lag(n = 1)
582
+ # Returns the value `n` steps earlier within each segment, source-shaped,
583
+ # as `CArray#lag` does along an axis. A cell whose segment has no value
584
+ # `n` steps back is UNDEF, so a lag never reaches into the segment before;
585
+ # a cell in no segment is UNDEF. A negative `n` looks ahead.
586
+ # @param n [Integer]
587
+ # @return [CArray]
588
+ def lag (n = 1, axis: nil)
589
+ unless n.is_a?(Integer)
590
+ raise TypeError, "lag: n must be an Integer (got #{n.class})"
591
+ end
592
+ return axis_lag(n, axis) if axis
593
+ out = blank_like(@value, @src_shape)
594
+ out[] = UNDEF
595
+ m = grouped.elements
596
+ return out if m.zero?
597
+ # The copy is in segment order, so one shift moves every segment; the
598
+ # cells it brought across a segment boundary are then cut.
599
+ lagged = grouped.lag(n)
600
+ lagged = out.data_type == CA_OBJECT && lagged.face? ?
601
+ CArray.object(m) { lagged.to_a } : lagged.copy
602
+ unless n.zero?
603
+ seg = CArray.segment_index(offsets: @bounds)
604
+ start = @bounds[seg]
605
+ pos = CArray.int64(m).seq! - start
606
+ lagged[n > 0 ? pos.lt(n) : pos.ge(@bounds[seg + 1] - start + n)] = UNDEF
607
+ end
608
+ out.reshape(@value.elements)[perm] = lagged
609
+ out
610
+ end
611
+
612
+ # @overload strip_mask(method:)
613
+ # Returns the values with each masked cell filled from its segment, as
614
+ # `CArray#strip_mask(method:)` fills a 1-D array: `:forward` (`:ffill`)
615
+ # carries the segment's last present value forward, `:backward`
616
+ # (`:bfill`) its next one back. A fill never reaches across into another
617
+ # segment, so a masked cell with nothing to carry in its segment stays
618
+ # masked; a cell in no segment is UNDEF. The value is not modified.
619
+ # @param method [Symbol] `:forward`, `:ffill`, `:backward` or `:bfill`.
620
+ # @return [CArray] source-shaped
621
+ def strip_mask (method:, axis: nil)
622
+ backward = CAIterator.hold_backward?(method)
623
+ return axis_hold(method, axis) if axis
624
+ if @value.face? && ! face_output?
625
+ return scatter_segments(->(shape) { blank_like(@value, shape) }) { |s| s.strip_mask(method: method) }
626
+ end
627
+ out = blank_like(@value, @src_shape)
628
+ out[] = UNDEF
629
+ m = grouped.elements
630
+ return out if m.zero?
631
+ # For each cell, the present cell its value comes from: the nearest one at
632
+ # or before it (forward) or at or after it (backward), kept when it lies
633
+ # in the cell's own segment.
634
+ slot = CArray.int64(m).seq!
635
+ slot[grouped.is_masked] = backward ? m : -1 if grouped.has_mask?
636
+ seg = CArray.segment_index(offsets: @bounds)
637
+ if backward
638
+ source = slot.reverse.cummin.reverse
639
+ found = source.lt(@bounds[seg + 1])
640
+ else
641
+ source = slot.cummax
642
+ found = source.ge(@bounds[seg])
643
+ end
644
+ filled = grouped[source.pmin(m - 1).pmax(0)].copy
645
+ filled[found.not] = UNDEF
646
+ out.reshape(@value.elements)[perm] = filled
647
+ out
648
+ end
649
+
650
+ # @overload order(descending: false, method: :ordinal, kind: :quick)
651
+ # Returns each cell's rank among the members of its segment, source-shaped
652
+ # int64, as `CArray#order` ranks a 1-D array: `0` is the smallest, ties
653
+ # are broken by position (`:ordinal`) or share a rank (`:dense`), and
654
+ # `descending:` reverses the order. A masked cell, and a cell in no
655
+ # segment, is UNDEF.
656
+ # @return [CArray]
657
+ def order (axis: nil, descending: false, method: :ordinal, kind: :quick)
658
+ return axis_order(axis, descending: descending, method: method, kind: kind) if axis
659
+ out = CArray.int64(*@src_shape)
660
+ out[] = UNDEF
661
+ # The keywords are checked by the core, on no cells, so a segment
662
+ # iterator refuses what CArray#order refuses.
663
+ CArray.int64(0).order(descending: descending, method: method, kind: kind)
664
+ # The core ranks every value once, so the order and the ties are its own.
665
+ # Ranked in the asked direction, so tied cells keep their order either way.
666
+ ranks = grouped.order(method: :dense, kind: kind, descending: descending)
667
+ m = grouped.elements
668
+ return out if m.zero?
669
+ seg = CArray.segment_index(offsets: @bounds)
670
+ present = ranks.is_not_masked
671
+ # One key per cell, segment first and a masked cell last in its segment;
672
+ # a stable sort keeps tied cells in their order.
673
+ key = seg * (m + 1) + ranks.strip_mask(m)
674
+ sorted = key.sort_index(kind: :stable)
675
+ first = @bounds[seg[sorted]] # where each sorted cell's segment starts
676
+ local = CArray.int64(m)
677
+ if method == :dense
678
+ skey = key[sorted]
679
+ step = CArray.int64(m)
680
+ step[] = 1
681
+ step[1..-1] = skey[1..-1].ne(skey[0...-1]).int64 if m > 1
682
+ run = step.cumsum.int64
683
+ local[sorted] = run - run[first]
684
+ else
685
+ local[sorted] = CArray.int64(m).seq! - first
686
+ end
687
+ local[present.not] = UNDEF
688
+ out.reshape(@value.elements)[perm] = local
689
+ out
690
+ end
691
+
692
+ private
693
+
694
+ # The contiguous copy every reduction works from. On a CACategoricalIterator
695
+ # it does not exist when the classifier does not line up cell-for-cell with the value;
696
+ # such an iterator answers the per-fiber form only, and says so here rather
697
+ # than letting a nil surface as whatever NoMethodError it reaches first.
698
+ def grouped
699
+ @grouped || raise(ArgumentError, @no_flat)
700
+ end
701
+
702
+ # Per-segment cell counts, alongside #grouped and unavailable for the same
703
+ # reason.
704
+ def counts
705
+ @elements || raise(ArgumentError, @no_flat)
706
+ end
707
+
708
+ # A segment scan. When the axis-group scan kernel answers in the core's data
709
+ # type, it runs with the whole value as one grouped axis and the flat codes
710
+ # as the single bundle; it emits in source order, so the flat result
711
+ # reshapes straight back to the source shape. Otherwise each segment is
712
+ # scanned by the core and scattered back.
713
+ #
714
+ # With no segment at all the kernel has no group to size, so it is given
715
+ # one that no cell belongs to: every cell comes back UNDEF, as it does when
716
+ # there are segments and a cell is in none of them.
717
+ def scan (op)
718
+ CArray.__name_errors_after_method__(op) { scan_answered(op) }
719
+ end
720
+
721
+ def scan_answered (op)
722
+ # An empty Face has no cell to ask the core about, and nothing to scan.
723
+ return blank_output(@src_shape) if @value.face? && @value.elements.zero?
724
+ answer = core_scan_answer(op)
725
+ unless kernel_scans?(op, answer)
726
+ return scatter_segments(->(shape) { blank_like(answer, shape) }) { |s| s.public_send(op) }
727
+ end
728
+ bundle = @k > 0 ? [codes, @k, [0]]
729
+ : [CArray.int64(@value.elements).fill(-1), 1, [0]]
730
+ scan_source.reshape(@value.elements)
731
+ .__scan_by_axis_groups__([0], [bundle], op)
732
+ .reshape(*@src_shape)
733
+ end
734
+
735
+ # The core's scan of one cell of the value: the scan answers in its data
736
+ # type and Face. A scan the core refuses raises here, in the core's words.
737
+ def core_scan_answer (op)
738
+ (@core_scan_answer ||= {})[op] ||= CAIterator.core_probe(@value).public_send(op)
739
+ end
740
+
741
+ # Whether the axis-group scan kernel gives the core's answer. It reads
742
+ # an integer, float, boolean or object payload (not a complex, not a Face),
743
+ # and answers cumsum / cumprod in float64 (an
744
+ # object stays an object), cummax / cummin in the value's data type and
745
+ # cumcount in int64 -- which is not the core's answer for every data type
746
+ # (the core counts a boolean's running sum in uint64).
747
+ def kernel_scans? (op, answer)
748
+ return false unless CArray.__fast_paths_enabled__
749
+ return false if @value.face? || answer.face? || @value.complex?
750
+ kernel_type =
751
+ case op
752
+ when :cumcount then CA_INT64
753
+ when :cumsum, :cumprod then @value.data_type == CA_OBJECT ? CA_OBJECT : CA_FLOAT64
754
+ else @value.data_type
755
+ end
756
+ answer.data_type == kernel_type
757
+ end
758
+
759
+ # Applies the block to each segment's members in grouped order and scatters
760
+ # the answers back to the source cells; a cell in no segment is UNDEF.
761
+ # +blank+ builds an empty array of a given shape that can hold the answers.
762
+ def scatter_segments (blank)
763
+ out = blank.call(@src_shape)
764
+ out[] = UNDEF
765
+ return out if grouped.elements.zero?
766
+ transformed = blank.call([grouped.elements])
767
+ @k.times do |c|
768
+ lo, hi = @bounds[c], @bounds[c + 1]
769
+ transformed[lo...hi] = surface_for(transformed, yield(grouped[lo...hi])) if hi > lo
770
+ end
771
+ out.reshape(@value.elements)[perm] = transformed
772
+ out
773
+ end
774
+
775
+ # A blank array for #map: of +data_type+ when one is given, otherwise of the
776
+ # same kind as the value.
777
+ def blank_output (shape, data_type = nil)
778
+ data_type ? CArray.new(data_type, shape) : blank_like(@value, shape)
779
+ end
780
+
781
+ # An answer as +out+ can hold it. A Face goes into an object array as its
782
+ # surface values; stored as it is, the object array would receive its
783
+ # storage bytes.
784
+ def surface_for (out, answer)
785
+ return answer unless out.data_type == CA_OBJECT && answer.is_a?(CArray) && answer.face?
786
+ answer.to_a
787
+ end
788
+
789
+ # A blank array of +shape+ of the same kind as +model+: its data type, or its
790
+ # Face. A read-only Face cannot be written cell by cell, so its values are
791
+ # held as the surface objects they are.
792
+ def blank_like (model, shape)
793
+ if model.face? && model.read_only?
794
+ CArray.new(CA_OBJECT, shape)
795
+ else
796
+ model.__send__(:blank_like, shape)
797
+ end
798
+ end
799
+
800
+ # The values as they were when the iterator was built, in source order.
801
+ # Every no-axis reduction works from the contiguous copy taken then; the
802
+ # scans read @value, so a write through the source between two calls used to
803
+ # be visible to a cumsum and not to a sum, off one iterator.
804
+ #
805
+ # Rebuilt rather than copied a second time: @perm is exactly the classified
806
+ # cells and @grouped holds their values and their masks, which is everything
807
+ # a scan reads -- a cell classified by nothing is skipped on its code, before
808
+ # its value is looked at. So this costs nothing until a scan asks for it, and
809
+ # nothing at all for an iterator that never scans.
810
+ def scan_source
811
+ @scan_source ||=
812
+ begin
813
+ snap = @value.template
814
+ snap.reshape(snap.elements)[perm] = grouped
815
+ snap
816
+ end
817
+ end
818
+
819
+
820
+ # Permutation: perm[slot] = the source index whose value sits at that grouped
821
+ # slot, read by #map / #sort_addr / the *_addr reductions. A segment's copy
822
+ # is the source range itself, so this is a run of consecutive indices, built
823
+ # the first time it is asked for. (A CACategoricalIterator sets it at
824
+ # construction: the valid prefix of the categorical's cached sort_addr.)
825
+ def perm
826
+ @perm ||= CArray.int64(grouped.elements).seq!(@origin)
827
+ end
828
+
829
+ # The segment each source cell belongs to, -1 for a cell outside every
830
+ # segment. Read by #map, the scans and the weighted reductions; built the
831
+ # first time one of them asks, since the reductions do not need it.
832
+ def codes
833
+ @codes ||= begin
834
+ c = CArray.int64(@value.elements).fill(-1)
835
+ if grouped.elements > 0
836
+ c[@origin...(@origin + grouped.elements)] =
837
+ CArray.segment_index(offsets: @bounds)
838
+ end
839
+ c
840
+ end
841
+ end
842
+
843
+ # Lay a per-cell weight array out in segment-contiguous order (same layout as
844
+ # @grouped), so wsum / wmean can pair each segment's values with its weights.
845
+ # Weights are coerced to float64 and gathered through the same permutation
846
+ # as the values, which carries the weight mask and leaves out the excluded
847
+ # cells, so wg lines up with @grouped.
848
+ # The weighted kernel reads float64 weights; the per-segment fallback hands
849
+ # them to the core as they are, so an object weight stays exact.
850
+ def scatter_weights (weights, as_float64 = true)
851
+ unless weights.elements == codes.elements
852
+ raise ArgumentError,
853
+ "wsum/wmean: weights.elements (#{weights.elements}) != " \
854
+ "value.elements (#{codes.elements})"
855
+ end
856
+ wf = as_float64 ? weights.float64 : weights
857
+ wf = wf.reshape(wf.elements)
858
+ # An empty index into an empty source is out of range, so take the empty
859
+ # layout directly.
860
+ grouped.elements > 0 ? wf[perm].copy : wf[0...0].copy
861
+ end
862
+
863
+ # Map a per-segment index to the flat source address via the
864
+ # permutation (grouped slot -> source index). The min/max sits at grouped slot
865
+ # offsets[c] + local_index[c]; perm carries it back to the source. Empty
866
+ # segments (masked local index) stay masked.
867
+ def segment_addr (local_index)
868
+ out = CArray.int64(@k)
869
+ @k.times do |c|
870
+ out[c] = local_index.is_masked[c] ? UNDEF
871
+ : perm[@offsets[c] + local_index[c]]
872
+ end
873
+ out
874
+ end
875
+
876
+ # Fused per-segment weighted sum + weighted mean (one C pass over the grouped
877
+ # copy, weights in segment order). Returns [wsum, wmean]; wmean is masked where a
878
+ # segment has no present (value AND weight) pair. Numeric value data types only.
879
+ def kernel_weighted (wg)
880
+ ws = CArray.float64(@k)
881
+ wm = CArray.float64(@k)
882
+ grouped.send(:__reduceat_wsum_wmean__, @offsets, wg, ws, wm)
883
+ [ws, wm]
884
+ end
885
+
886
+ # Per-segment weighted fallback for the value data types the kernel does not
887
+ # read (object): delegate each segment to CArray#wsum / #wmean, answering in
888
+ # the data type the core answers in. Empty segments take the given identity.
889
+ def fold_weighted (op, wg, empty)
890
+ out = CArray.new(core_reduce_type(op, CA_FLOAT64([[1.0]])), [@k])
891
+ @k.times do |c|
892
+ lo, hi = @bounds[c], @bounds[c + 1]
893
+ out[c] = hi > lo ? yield(grouped[lo...hi], wg[lo...hi]) : empty
894
+ end
895
+ out
896
+ end
897
+
898
+ # The value at the first or last present cell of each segment, UNDEF for a
899
+ # segment with none. Every segment's position is found at once: for each
900
+ # grouped slot, the nearest present slot at or after it (a running minimum
901
+ # taken from the end) or at or before it (a running maximum), read at the
902
+ # segment's first or last slot and kept when it falls inside the segment.
903
+ # A read-only Face cannot take UNDEF cell by cell, so its answers are
904
+ # collected segment by segment, as its other reductions are.
905
+ def edge_values (op)
906
+ n = grouped.elements
907
+ if n.zero? || (@value.face? && ! face_output?) || ! CArray.__fast_paths_enabled__
908
+ return per_segment(core_reduce_kind(op)) { |s| s.public_send(op) }
909
+ end
910
+ slot = CArray.int64(n).seq!
911
+ if grouped.has_mask?
912
+ slot[grouped.is_masked] = (op == :first) ? n : -1
913
+ end
914
+ lo, hi = @bounds[0...-1], @bounds[1..-1]
915
+ if op == :first
916
+ nearest = slot.reverse.cummin.reverse
917
+ at = nearest[lo.pmin(n - 1)]
918
+ else
919
+ nearest = slot.cummax
920
+ at = nearest[(hi - 1).pmax(0)]
921
+ end
922
+ found = at.ge(lo) & at.lt(hi)
923
+ out = grouped[at.pmin(n - 1).pmax(0)].copy
924
+ out[found.not] = UNDEF
925
+ # A reduction carries no attributes, a picked value included.
926
+ grouped.__send__(:drop_carried_attrs, out)
927
+ end
928
+
929
+ # The members of segment `c` as a CArray slice of the grouped copy. An empty
930
+ # segment (zero-width segment) yields the shared empty array — a zero-length
931
+ # slice cannot be taken directly, and an empty array carries the same reduction
932
+ # contract we want (identity for sum, UNDEF for ratios).
933
+ def segment_slice (c)
934
+ lo, hi = @bounds[c], @bounds[c + 1]
935
+ hi > lo ? grouped[lo...hi] : @empty
936
+ end
937
+
938
+ # Single-pass reduceat moments (count / sum / min / max per segment), computed
939
+ # once over the grouped copy and cached — the whole point of the eager copy is
940
+ # that one scatter is followed by cheap single-pass reductions with no
941
+ # per-segment views. Nil for a non-numeric value data type (complex / object /
942
+ # bool), where the monoid reductions fall back to per_segment.
943
+ # numeric value data types the C moments kernel handles (int8..float64); bool /
944
+ # complex / object fall back to per_segment.
945
+ MONOID_TYPES = %i[int8 uint8 int16 uint16 int32 uint32
946
+ int64 uint64 float32 float64].freeze
947
+
948
+ # Whether the C reduceat and scatter kernels read a value of +data_type+:
949
+ # a numeric one, with the fast paths on. They are a faster way to the
950
+ # core's answer for each group; a development build run with
951
+ # CARRAY_GENERAL_PATH set reduces every group by the core instead, which
952
+ # is how the two are compared.
953
+ def kernel_reads? (data_type = grouped.data_type)
954
+ MONOID_TYPES.include?(data_type) && CArray.__fast_paths_enabled__
955
+ end
956
+
957
+ def moments
958
+ return @moments if defined?(@moments)
959
+ @moments =
960
+ if kernel_reads?
961
+ dt = grouped.data_type
962
+ counts = CArray.int64(@k)
963
+ sums = CArray.float64(@k)
964
+ mins = CArray.new(dt, [@k])
965
+ maxs = CArray.new(dt, [@k])
966
+ grouped.send(:__reduceat_moments__, @offsets, counts, sums, mins, maxs)
967
+ { count: counts, sum: sums, min: mins, max: maxs }
968
+ end
969
+ end
970
+
971
+ # Single-pass fused per-segment argmin / argmax (min_index / max_index),
972
+ # cached. Nil for a non-numeric value data type (fall back to per_segment).
973
+ def arg_minmax
974
+ return @arg_minmax if defined?(@arg_minmax)
975
+ @arg_minmax =
976
+ if kernel_reads?
977
+ mn = CArray.int64(@k)
978
+ mx = CArray.int64(@k)
979
+ grouped.send(:__reduceat_argminmax__, @offsets, mn, mx)
980
+ { min: mn, max: mx }
981
+ end
982
+ end
983
+
984
+ # Single-pass fused per-segment boolean all / any, cached. Nil unless the
985
+ # value data type is boolean (fall back to per_segment, which raises like
986
+ # CArray#all on a non-boolean).
987
+ def all_any
988
+ return @all_any if defined?(@all_any)
989
+ @all_any =
990
+ if grouped.data_type == CA_BOOLEAN && CArray.__fast_paths_enabled__
991
+ a = CArray.boolean(@k)
992
+ o = CArray.boolean(@k)
993
+ grouped.send(:__reduceat_all_any__, @offsets, a, o)
994
+ { all: a, any: o }
995
+ end
996
+ end
997
+
998
+ # The data type the core reduction `op` promotes this value to. Asked of the
999
+ # core itself -- a one-cell reduction of the value's type -- rather than
1000
+ # restated here, so a per-segment answer cannot drift from `CArray#<op>`
1001
+ # (`sum` on an integer promotes, `accumulate` stays, `min` / `max` keep the
1002
+ # type but a boolean widens, `prod` on an object stays an object). A payload
1003
+ # the core refuses to fold this way raises here, with the core's own error.
1004
+ # Probes the core with a one-cell array of the value's data type and takes
1005
+ # the answer's. Asks @value rather than the grouped copy, which has the same
1006
+ # data type but does not exist on an iterator that answers only the
1007
+ # per-fiber form -- and `accumulate(axis:)`, the one axis: member that needs
1008
+ # this, is exactly the case that would have found it missing.
1009
+ def core_reduce_type (op, *args)
1010
+ kind = core_reduce_kind(op, *args)
1011
+ kind ? kind.data_type : CA_OBJECT
1012
+ end
1013
+
1014
+ # The core's answer itself for the one-cell probe, or nil for a Face with no
1015
+ # cell to probe. Its class is the one the per-segment answers come in, which
1016
+ # for a Face need not be the values' own (a CATime's spread is a CATimedelta).
1017
+ def core_reduce_kind (op, *args)
1018
+ memo = (@core_reduce_kind ||= {})
1019
+ return memo[[op, args]] if memo.key?([op, args])
1020
+ probe = CAIterator.core_probe(@value)
1021
+ memo[[op, args]] = probe && probe.public_send(op, *args, axis: 1)
1022
+ end
1023
+
1024
+ # Whether an output can be built by lifting one. A Face is filled by writing
1025
+ # surface values into storage, so this needs a Face that can be written
1026
+ # into; a read-only one -- a const string's records index a shared pool, a
1027
+ # categorical's codes index a vocabulary -- has no blank form to fill.
1028
+ def face_output?
1029
+ @value.face? && ! @value.read_only?
1030
+ end
1031
+
1032
+ # Build a length-k typed output by folding each segment's members with the
1033
+ # given reduction block. Fallback path (order statistics, and monoids on a
1034
+ # non-numeric value data type): each segment is delegated to the same CArray
1035
+ # reduction, so the per-segment result matches `CArray#<reduction>` over that
1036
+ # segment's members — the mask carries the "insufficient present data" contract
1037
+ # for free (an all-masked segment reduces like an empty one; identity-bearing
1038
+ # reductions return their identity, ratios return UNDEF).
1039
+ def per_segment (kind)
1040
+ # a data type the values' own Face answers in names that Face
1041
+ kind = @value if ! kind.is_a?(CArray) && @value.face? && kind == @value.data_type
1042
+ data_type = kind.is_a?(CArray) ? kind.data_type : kind
1043
+ out = if kind.nil?
1044
+ CArray.new(CA_OBJECT, [@k])
1045
+ elsif ! (kind.is_a?(CArray) && kind.face?)
1046
+ bytes = if kind.is_a?(CArray) then kind.bytes
1047
+ elsif data_type == @value.data_type then @value.bytes
1048
+ else 0 end
1049
+ CArray.new(data_type, [@k], bytes: bytes)
1050
+ elsif ! kind.read_only?
1051
+ # the core answered in a Face, so the output is one too:
1052
+ # CATime#min hands back a CATime::Element, which only a CATime
1053
+ # has anywhere to put
1054
+ kind.__send__(:blank_like, [@k])
1055
+ else
1056
+ # a read-only Face cannot be filled, so its answers are collected
1057
+ # as the surface objects they already are
1058
+ CArray.new(CA_OBJECT, [@k])
1059
+ end
1060
+ # A member the core refuses for this Face still refuses: the segment slice is
1061
+ # the Face, so the refusal comes from there, in the core's own words.
1062
+ @k.times { |c| out[c] = yield(segment_slice(c)) }
1063
+ out
1064
+ end
1065
+
1066
+ # ---- no axis: form -------------------------------------------------------
1067
+
1068
+ def axis_by_core (*)
1069
+ raise NotImplementedError, "#{self.class} has no axis: form"
1070
+ end
1071
+
1072
+ def axis_by_masked_copy (*)
1073
+ raise NotImplementedError, "#{self.class} has no axis: form"
1074
+ end
1075
+
1076
+ def axis_counts (*)
1077
+ raise NotImplementedError, "#{self.class} has no axis: form"
1078
+ end
1079
+
1080
+ def axis_mean (*)
1081
+ raise NotImplementedError, "#{self.class} has no axis: form"
1082
+ end
1083
+
1084
+ def axis_moments (*)
1085
+ raise NotImplementedError, "#{self.class} has no axis: form"
1086
+ end
1087
+
1088
+ def axis_order_stat (*)
1089
+ raise NotImplementedError, "#{self.class} has no axis: form"
1090
+ end
1091
+
1092
+ def axis_scan (*)
1093
+ raise NotImplementedError, "#{self.class} has no axis: form"
1094
+ end
1095
+
1096
+ def axis_lag (*)
1097
+ raise NotImplementedError, "#{self.class} has no axis: form"
1098
+ end
1099
+
1100
+ def axis_order (*)
1101
+ raise NotImplementedError, "#{self.class} has no axis: form"
1102
+ end
1103
+
1104
+ def axis_hold (*)
1105
+ raise NotImplementedError, "#{self.class} has no axis: form"
1106
+ end
1107
+
1108
+ def axis_map (*)
1109
+ raise NotImplementedError, "#{self.class} has no axis: form"
1110
+ end
1111
+
1112
+ def axis_prod (*)
1113
+ raise NotImplementedError, "#{self.class} has no axis: form"
1114
+ end
1115
+
1116
+ def axis_sum (*)
1117
+ raise NotImplementedError, "#{self.class} has no axis: form"
1118
+ end
1119
+
1120
+ def axis_wsum_wmean (*)
1121
+ raise NotImplementedError, "#{self.class} has no axis: form"
1122
+ end
1123
+
1124
+ # The present cells per segment a weighted reduction counts: those whose
1125
+ # value and weight are both present, as CArray#wsum / #wmean count them.
1126
+ def present_pair_count (weights, axis)
1127
+ if axis
1128
+ paired = @value.copy
1129
+ paired[weights.is_masked] = UNDEF if weights.has_mask?
1130
+ return paired.group_by_category(@cat).count_not_masked(axis: axis)
1131
+ end
1132
+ both = grouped.is_not_masked & scatter_weights(weights).is_not_masked
1133
+ both.int32.segments(offsets: @bounds).sum # one pass, unlike count(true)
1134
+ end
1135
+
1136
+ # min_count: and fill_value: on a value reduction, as a core reduction
1137
+ # takes them: a segment with fewer than min_count present values is
1138
+ # UNDEF, and fill_value then fills the UNDEF cells, those min_count made
1139
+ # and those the reduction answers for an empty segment. Each reduction
1140
+ # is left to answer as it does; this only masks and fills its result.
1141
+ # A weighted reduction counts the cells whose value and weight are both
1142
+ # present.
1143
+ module ReductionCountAndFill
1144
+ REDUCTIONS = %i[sum accumulate max min minmax mean median percentile
1145
+ variance stddev variancep stddevp prod wsum wmean
1146
+ min_index max_index min_addr max_addr
1147
+ count count_not_masked count_masked].freeze
1148
+
1149
+ # A count of the mask reads every cell of the segment, as the core's does.
1150
+ def self.mask_count? (op, args)
1151
+ op == :count_not_masked || op == :count_masked ||
1152
+ (op == :count && (args.empty? || args[0].equal?(UNDEF)))
1153
+ end
1154
+
1155
+ CArray.send(:define_named_methods, self, REDUCTIONS, :reduce_counted, super_block: true)
1156
+
1157
+ # The segment reduction already leaves an empty segment as the core does,
1158
+ # so only a min_count asking for more is applied here. The reduction the
1159
+ # member is made of is the block (super).
1160
+ private def reduce_counted (op, args, kw)
1161
+ min_count = kw.delete(:min_count)
1162
+ fill_value = kw.delete(:fill_value)
1163
+ need = CArray.__present_cells_needed_per_result_cell__(op, min_count, 0)
1164
+ result = yield(*args, **kw)
1165
+ return result if need == 0 && fill_value.nil?
1166
+ if need > 0
1167
+ present = if op == :wsum || op == :wmean
1168
+ present_pair_count(args[0], kw[:axis])
1169
+ elsif ReductionCountAndFill.mask_count?(op, args)
1170
+ if kw[:axis]
1171
+ raise NotImplementedError,
1172
+ "#{self.class}##{op}(axis:, min_count:) is not implemented"
1173
+ end
1174
+ elements
1175
+ else
1176
+ count_not_masked(**kw)
1177
+ end
1178
+ short = present.lt(need)
1179
+ end
1180
+ if short
1181
+ (result.is_a?(Array) ? result : [result]).each { |r| r[short] = UNDEF }
1182
+ end
1183
+ CArray.__fill_undefined_result_cells__(result, fill_value)
1184
+ end
1185
+ end
1186
+ prepend ReductionCountAndFill
1187
+
1188
+
1189
+ # lag and strip_mask(method:) give the source's values moved or held in
1190
+ # place, so the result shows the source's attributes, as CArray#lag and
1191
+ # CArray#strip_mask(method:) do.
1192
+ prepend(Module.new do
1193
+ def lag (*args, **keywords) = @value.__send__(:carry_attrs, super)
1194
+ def strip_mask (**keywords) = @value.__send__(:carry_attrs, super)
1195
+ end)
1196
+ end
1197
+
1198
+
1199
+ class CArray
1200
+ # @overload segments(offsets:)
1201
+ # Returns a {CASegmentIterator} that reduces `self` segment by segment,
1202
+ # segment `c` being the cells `offsets[c]...offsets[c + 1]` in flatten
1203
+ # order. Cells outside `offsets[0]...offsets[-1]` belong to no segment.
1204
+ #
1205
+ # data.segments(offsets: [0, 2, 2, 5]).sum # one sum per segment
1206
+ #
1207
+ # @param offsets [CArray, Array<Integer>] `k + 1` non-decreasing
1208
+ # boundaries within `0..elements`.
1209
+ # @return [CASegmentIterator]
1210
+ # @overload segments(lengths:)
1211
+ # The same from the `k` segment lengths, the first segment starting at
1212
+ # cell 0.
1213
+ # @param lengths [CArray, Array<Integer>] one count per segment.
1214
+ # @return [CASegmentIterator]
1215
+ # @raise [ArgumentError] for decreasing or empty offsets, a negative or
1216
+ # masked length, segments reaching outside `self`, or unless exactly one
1217
+ # of `offsets:` and `lengths:` is given.
1218
+ def segments (offsets: nil, lengths: nil)
1219
+ CASegmentIterator.new(self, offsets: offsets, lengths: lengths)
1220
+ end
1221
+ end