carray 2.0.1 → 3.0.1

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 (386) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -25
  3. data/CHANGELOG.md +338 -0
  4. data/{NEWS.md → CHANGELOG.v1.md} +3 -0
  5. data/LICENSE +1 -1
  6. data/README.md +120 -36
  7. data/carray.gemspec +32 -30
  8. data/ext/ca_array_pool.c +91 -0
  9. data/ext/ca_axis_descriptor.h +186 -0
  10. data/ext/ca_axis_dispatch.c +924 -0
  11. data/ext/ca_axis_group.c +1244 -0
  12. data/ext/ca_bincmp_dispatch.c +76 -0
  13. data/ext/ca_bincmp_dispatch.h +85 -0
  14. data/ext/ca_binop_dispatch.c +124 -0
  15. data/ext/ca_binop_dispatch.h +152 -0
  16. data/ext/ca_categorical_iterator.c +1375 -0
  17. data/ext/ca_compare.c +94 -0
  18. data/ext/ca_compare.h +26 -0
  19. data/ext/ca_composite_dispatch.c +414 -0
  20. data/ext/ca_composite_dispatch.h +116 -0
  21. data/ext/ca_for_buffer.h +96 -0
  22. data/ext/ca_for_each_element.h +239 -0
  23. data/ext/ca_group_iter.c +304 -0
  24. data/ext/ca_iter_substrate.h +325 -0
  25. data/ext/ca_kernel_iterator.c +4367 -0
  26. data/ext/ca_kernel_iterator.h +2596 -0
  27. data/ext/ca_moncmp_dispatch.c +37 -0
  28. data/ext/ca_moncmp_dispatch.h +62 -0
  29. data/ext/ca_monop_dispatch.c +200 -0
  30. data/ext/ca_monop_dispatch.h +235 -0
  31. data/ext/ca_obj_array.c +355 -359
  32. data/ext/ca_obj_bincmp.c +839 -0
  33. data/ext/ca_obj_binop.c +948 -0
  34. data/ext/ca_obj_bitarray.c +369 -164
  35. data/ext/ca_obj_bitfield.c +294 -234
  36. data/ext/ca_obj_block.c +189 -711
  37. data/ext/ca_obj_byte_swap.c +766 -0
  38. data/ext/ca_obj_const_string.c +967 -0
  39. data/ext/ca_obj_face.c +750 -0
  40. data/ext/ca_obj_face.h +279 -0
  41. data/ext/ca_obj_fake.c +239 -100
  42. data/ext/ca_obj_farray.c +54 -441
  43. data/ext/ca_obj_field.c +82 -529
  44. data/ext/ca_obj_fixlen_string.c +308 -0
  45. data/ext/ca_obj_grid.c +866 -440
  46. data/ext/ca_obj_meld.c +1039 -0
  47. data/ext/ca_obj_moncmp.c +588 -0
  48. data/ext/ca_obj_monop.c +1123 -0
  49. data/ext/ca_obj_object.c +866 -296
  50. data/ext/ca_obj_record.c +470 -0
  51. data/ext/ca_obj_reduce.c +97 -82
  52. data/ext/ca_obj_refer.c +593 -459
  53. data/ext/ca_obj_remap.c +475 -0
  54. data/ext/ca_obj_repeat.c +92 -477
  55. data/ext/ca_obj_roll.c +624 -0
  56. data/ext/ca_obj_select.c +344 -296
  57. data/ext/ca_obj_select_axis.c +1306 -0
  58. data/ext/ca_obj_shift.c +231 -793
  59. data/ext/ca_obj_source.c +78 -0
  60. data/ext/ca_obj_stack.c +1173 -0
  61. data/ext/ca_obj_stride.c +2584 -0
  62. data/ext/ca_obj_string.c +270 -0
  63. data/ext/ca_obj_tile.c +622 -0
  64. data/ext/ca_obj_time.c +548 -0
  65. data/ext/ca_obj_timedelta.c +437 -0
  66. data/ext/ca_obj_transpose.c +62 -516
  67. data/ext/ca_obj_triop.c +785 -0
  68. data/ext/ca_obj_window.c +1202 -565
  69. data/ext/ca_op_byte_swap.c +175 -0
  70. data/ext/ca_op_cmplx64.h +123 -0
  71. data/ext/ca_op_ipower.c +316 -0
  72. data/ext/ca_op_powi.h +88 -0
  73. data/ext/ca_sort_kernels.h +132 -0
  74. data/ext/ca_sweep_engine.c +473 -0
  75. data/ext/ca_sweep_engine.h +166 -0
  76. data/ext/ca_transform_common.c +235 -0
  77. data/ext/ca_triop_dispatch.c +55 -0
  78. data/ext/ca_triop_dispatch.h +62 -0
  79. data/ext/carray.h +810 -420
  80. data/ext/carray_access.c +873 -731
  81. data/ext/carray_attribute.c +98 -329
  82. data/ext/carray_bincount.c +255 -0
  83. data/ext/carray_broadcast.c +376 -0
  84. data/ext/carray_build_flags.h +3 -0
  85. data/ext/carray_call_cfunc.c +2897 -874
  86. data/ext/carray_call_cfunc.h +313 -0
  87. data/ext/carray_cast.c +1264 -315
  88. data/ext/carray_cast_func.rb +81 -40
  89. data/ext/carray_class.c +53 -63
  90. data/ext/carray_config.h +28 -0
  91. data/ext/carray_conversion.c +350 -346
  92. data/ext/carray_copy.c +168 -270
  93. data/ext/carray_core.c +1396 -206
  94. data/ext/carray_count.c +312 -0
  95. data/ext/carray_data_type.c +43 -19
  96. data/ext/carray_element.c +585 -213
  97. data/ext/carray_factorize.c +2542 -0
  98. data/ext/carray_generate.c +230 -559
  99. data/ext/carray_histogram.c +490 -0
  100. data/ext/carray_hold.c +228 -0
  101. data/ext/carray_index_classifier.c +1021 -0
  102. data/ext/carray_index_classifier.h +27 -0
  103. data/ext/carray_internal.h +136 -0
  104. data/ext/carray_kernels_bincmp.c +4446 -0
  105. data/ext/carray_kernels_binop.c +11001 -0
  106. data/ext/carray_kernels_init.c +1131 -0
  107. data/ext/carray_kernels_map.c +3467 -0
  108. data/ext/carray_kernels_moncmp.c +2097 -0
  109. data/ext/carray_kernels_monop.c +18313 -0
  110. data/ext/carray_kernels_reduce_aggregate.c +25837 -0
  111. data/ext/carray_kernels_reduce_boolean.c +330 -0
  112. data/ext/carray_kernels_reduce_cumulative.c +14593 -0
  113. data/ext/carray_kernels_reduce_extreme.c +16948 -0
  114. data/ext/carray_kernels_reduce_variance.c +3910 -0
  115. data/ext/carray_kernels_scan.c +3693 -0
  116. data/ext/carray_kernels_search.c +32138 -0
  117. data/ext/carray_kernels_sort.c +10626 -0
  118. data/ext/carray_kernels_triop.c +1392 -0
  119. data/ext/carray_lazy.c +737 -0
  120. data/ext/carray_loop.c +88 -200
  121. data/ext/carray_mask.c +853 -158
  122. data/ext/carray_math_kernel.h +120 -0
  123. data/ext/carray_mathfunc.c +10 -241
  124. data/ext/carray_median_percentile.c +1257 -0
  125. data/ext/carray_memory_view.c +1650 -0
  126. data/ext/carray_operator.c +1525 -320
  127. data/ext/carray_order.c +664 -1394
  128. data/ext/carray_partition.c +416 -0
  129. data/ext/carray_random.c +518 -0
  130. data/ext/carray_scatter.c +357 -0
  131. data/ext/carray_slab.c +1219 -0
  132. data/ext/carray_slab.h +84 -0
  133. data/ext/carray_sort.c +829 -0
  134. data/ext/carray_sort_kernel.c +620 -0
  135. data/ext/carray_struct.c +695 -0
  136. data/ext/carray_test.c +343 -229
  137. data/ext/carray_undef.c +34 -17
  138. data/ext/carray_utils.c +175 -74
  139. data/ext/extconf.rb +234 -55
  140. data/ext/mk_call_cfunc.rb +671 -0
  141. data/ext/mkkernel.rb +9096 -0
  142. data/ext/ruby_carray.c +211 -108
  143. data/ext/version.h +4 -14
  144. data/ext/version.rb +5 -13
  145. data/lib/carray/arrow_tensor.rb +401 -0
  146. data/lib/carray/attribute.rb +166 -0
  147. data/lib/carray/autoload_carray.rb +239 -0
  148. data/lib/carray/autoload_method_extension.rb +45 -0
  149. data/lib/carray/axis_group.rb +711 -0
  150. data/lib/carray/basics.rb +481 -0
  151. data/lib/carray/bincount_nd.rb +358 -0
  152. data/lib/carray/block_iterator.rb +614 -0
  153. data/lib/carray/boolean_reduce.rb +109 -0
  154. data/lib/carray/categorical.rb +561 -0
  155. data/lib/carray/categorical_iterator.rb +1084 -0
  156. data/lib/carray/complex.rb +150 -0
  157. data/lib/carray/conditional.rb +216 -0
  158. data/lib/carray/const_string.rb +228 -0
  159. data/lib/carray/construct.rb +160 -328
  160. data/lib/carray/core_extensions.rb +297 -0
  161. data/lib/carray/data_type_extension.rb +250 -0
  162. data/lib/carray/fixlen_string.rb +95 -0
  163. data/lib/carray/frame/concat.rb +132 -0
  164. data/lib/carray/frame/convert.rb +95 -0
  165. data/lib/carray/frame/csv_parser.rb +211 -0
  166. data/lib/carray/frame/frame.rb +642 -0
  167. data/lib/carray/frame/group.rb +186 -0
  168. data/lib/carray/frame/io.rb +321 -0
  169. data/lib/carray/frame/join.rb +248 -0
  170. data/lib/carray/frame/records.rb +99 -0
  171. data/lib/carray/frame/sort.rb +113 -0
  172. data/lib/carray/frame/verbs.rb +316 -0
  173. data/lib/carray/frame.rb +16 -0
  174. data/lib/carray/fuse_source.rb +123 -0
  175. data/lib/carray/fusion.rb +218 -0
  176. data/lib/carray/histogram.rb +512 -0
  177. data/lib/carray/inspect.rb +37 -26
  178. data/lib/carray/iterator.rb +58 -349
  179. data/lib/carray/lazy.rb +941 -0
  180. data/lib/carray/mask_gap_fill.rb +200 -0
  181. data/lib/carray/math.rb +78 -342
  182. data/lib/carray/meld_reduce.rb +289 -0
  183. data/lib/carray/methods/align_addr.rb +116 -0
  184. data/lib/carray/methods/bin.rb +128 -0
  185. data/lib/carray/methods/bincount.rb +87 -0
  186. data/lib/carray/methods/bit_string.rb +92 -0
  187. data/lib/carray/methods/broadcast.rb +63 -0
  188. data/lib/carray/methods/choose.rb +39 -0
  189. data/lib/carray/methods/composition.rb +280 -0
  190. data/lib/carray/methods/gather_nd.rb +206 -0
  191. data/lib/carray/methods/index.rb +39 -0
  192. data/lib/carray/methods/insert_block.rb +99 -0
  193. data/lib/carray/methods/is_in.rb +141 -0
  194. data/lib/carray/methods/join.rb +90 -0
  195. data/lib/carray/methods/locate_addr.rb +52 -0
  196. data/lib/carray/methods/mask_duplicates.rb +41 -0
  197. data/lib/carray/methods/meshgrid.rb +90 -0
  198. data/lib/carray/methods/mode.rb +126 -0
  199. data/lib/carray/methods/nunique.rb +46 -0
  200. data/lib/carray/methods/resize.rb +56 -0
  201. data/lib/carray/methods/snap.rb +161 -0
  202. data/lib/carray/methods/string_format.rb +57 -0
  203. data/lib/carray/methods/unique.rb +47 -0
  204. data/lib/carray/methods/value_counts.rb +71 -0
  205. data/lib/carray/mkmf.rb +124 -101
  206. data/lib/carray/runtime.rb +89 -0
  207. data/lib/carray/serialize.rb +478 -167
  208. data/lib/carray/slab_iterator.rb +305 -0
  209. data/lib/carray/stack.rb +291 -0
  210. data/lib/carray/string.rb +56 -180
  211. data/lib/carray/string_operation_extension.rb +289 -0
  212. data/lib/carray/struct.rb +335 -323
  213. data/lib/carray/struct_builder.rb +697 -0
  214. data/lib/carray/table.rb +41 -2
  215. data/lib/carray/time.rb +2654 -38
  216. data/lib/carray/window_iterator.rb +927 -0
  217. data/lib/carray.rb +55 -57
  218. data/yard-stubs/ca_obj_array.rb +385 -0
  219. data/yard-stubs/ca_obj_bitarray.rb +38 -0
  220. data/yard-stubs/ca_obj_bitfield.rb +43 -0
  221. data/yard-stubs/ca_obj_block.rb +73 -0
  222. data/yard-stubs/ca_obj_byte_swap.rb +56 -0
  223. data/yard-stubs/ca_obj_fake.rb +31 -0
  224. data/yard-stubs/ca_obj_farray.rb +32 -0
  225. data/yard-stubs/ca_obj_field.rb +45 -0
  226. data/yard-stubs/ca_obj_grid.rb +35 -0
  227. data/yard-stubs/ca_obj_refer.rb +72 -0
  228. data/yard-stubs/ca_obj_roll.rb +45 -0
  229. data/yard-stubs/ca_obj_shift.rb +43 -0
  230. data/yard-stubs/ca_obj_stride.rb +181 -0
  231. data/yard-stubs/ca_obj_tile.rb +29 -0
  232. data/yard-stubs/ca_obj_transpose.rb +40 -0
  233. data/yard-stubs/ca_obj_window.rb +49 -0
  234. data/yard-stubs/carray_access.rb +131 -0
  235. data/yard-stubs/carray_attribute.rb +246 -0
  236. data/yard-stubs/carray_broadcast.rb +37 -0
  237. data/yard-stubs/carray_cast.rb +489 -0
  238. data/yard-stubs/carray_class.rb +65 -0
  239. data/yard-stubs/carray_conversion.rb +76 -0
  240. data/yard-stubs/carray_copy.rb +79 -0
  241. data/yard-stubs/carray_core.rb +114 -0
  242. data/yard-stubs/carray_count.rb +79 -0
  243. data/yard-stubs/carray_element.rb +108 -0
  244. data/yard-stubs/carray_generate.rb +66 -0
  245. data/yard-stubs/carray_lazy.rb +23 -0
  246. data/yard-stubs/carray_loop.rb +140 -0
  247. data/yard-stubs/carray_mask.rb +259 -0
  248. data/yard-stubs/carray_math.rb +132 -0
  249. data/yard-stubs/carray_mathfunc.rb +45 -0
  250. data/yard-stubs/carray_median_percentile.rb +89 -0
  251. data/yard-stubs/carray_memory_view.rb +163 -0
  252. data/yard-stubs/carray_order.rb +312 -0
  253. data/yard-stubs/carray_random.rb +89 -0
  254. data/yard-stubs/carray_scatter.rb +106 -0
  255. data/yard-stubs/carray_slab.rb +57 -0
  256. data/yard-stubs/carray_sort.rb +163 -0
  257. data/yard-stubs/carray_test.rb +85 -0
  258. data/yard-stubs/carray_undef.rb +64 -0
  259. data/yard-stubs/carray_utils.rb +97 -0
  260. data/yard-stubs/ruby_carray.rb +193 -0
  261. metadata +220 -138
  262. data/Rakefile +0 -51
  263. data/TODO.md +0 -18
  264. data/ext/ca_iter_block.c +0 -257
  265. data/ext/ca_iter_dimension.c +0 -299
  266. data/ext/ca_iter_window.c +0 -214
  267. data/ext/ca_obj_mapping.c +0 -644
  268. data/ext/ca_obj_unbound_repeat.c +0 -529
  269. data/ext/carray_iterator.c +0 -641
  270. data/ext/carray_math.rb +0 -850
  271. data/ext/carray_numeric.c +0 -259
  272. data/ext/carray_sort_addr.c +0 -254
  273. data/ext/carray_stat.c +0 -2100
  274. data/ext/carray_stat_proc.rb +0 -1999
  275. data/ext/mkmath.rb +0 -741
  276. data/ext/ruby_ccomplex.c +0 -509
  277. data/ext/ruby_float_func.c +0 -86
  278. data/lib/carray/array.rb +0 -8
  279. data/lib/carray/autoload/autoload_base.rb +0 -19
  280. data/lib/carray/autoload/autoload_gem_cairo.rb +0 -9
  281. data/lib/carray/autoload/autoload_gem_ffi.rb +0 -9
  282. data/lib/carray/autoload/autoload_gem_gnuplot.rb +0 -2
  283. data/lib/carray/autoload/autoload_gem_io_csv.rb +0 -14
  284. data/lib/carray/autoload/autoload_gem_io_pg.rb +0 -6
  285. data/lib/carray/autoload/autoload_gem_io_sqlite3.rb +0 -12
  286. data/lib/carray/autoload/autoload_gem_narray.rb +0 -10
  287. data/lib/carray/autoload/autoload_gem_numo_narray.rb +0 -15
  288. data/lib/carray/autoload/autoload_gem_opencv.rb +0 -16
  289. data/lib/carray/autoload/autoload_gem_random.rb +0 -8
  290. data/lib/carray/autoload/autoload_gem_rmagick.rb +0 -23
  291. data/lib/carray/autoload/autoload_gem_zimg.rb +0 -3
  292. data/lib/carray/autoload/autoload_io_imagemagick.rb +0 -6
  293. data/lib/carray/autoload/autoload_math_histogram.rb +0 -5
  294. data/lib/carray/autoload/autoload_math_recurrence.rb +0 -6
  295. data/lib/carray/autoload/autoload_object_iterator.rb +0 -1
  296. data/lib/carray/autoload/autoload_object_link.rb +0 -1
  297. data/lib/carray/autoload/autoload_object_pack.rb +0 -2
  298. data/lib/carray/autoload.rb +0 -141
  299. data/lib/carray/basic.rb +0 -191
  300. data/lib/carray/broadcast.rb +0 -101
  301. data/lib/carray/compose.rb +0 -315
  302. data/lib/carray/convert.rb +0 -115
  303. data/lib/carray/info.rb +0 -110
  304. data/lib/carray/io/imagemagick.rb +0 -235
  305. data/lib/carray/mask.rb +0 -102
  306. data/lib/carray/math/histogram.rb +0 -177
  307. data/lib/carray/math/recurrence.rb +0 -93
  308. data/lib/carray/object/ca_obj_iterator.rb +0 -50
  309. data/lib/carray/object/ca_obj_link.rb +0 -50
  310. data/lib/carray/object/ca_obj_pack.rb +0 -99
  311. data/lib/carray/obsolete.rb +0 -256
  312. data/lib/carray/ordering.rb +0 -181
  313. data/lib/carray/testing.rb +0 -51
  314. data/lib/carray/transform.rb +0 -109
  315. data/mailmap +0 -1
  316. data/misc/Methods.ja.md +0 -182
  317. data/misc/NOTE +0 -51
  318. data/spec/Classes/CABitfield_spec.rb +0 -58
  319. data/spec/Classes/CABlockIterator_spec.rb +0 -114
  320. data/spec/Classes/CABlock_spec.rb +0 -205
  321. data/spec/Classes/CAField_spec.rb +0 -39
  322. data/spec/Classes/CAGrid_spec.rb +0 -75
  323. data/spec/Classes/CAMap_spec.rb +0 -0
  324. data/spec/Classes/CAMapping_spec.rb +0 -105
  325. data/spec/Classes/CAObject_attribute_spec.rb +0 -33
  326. data/spec/Classes/CAObject_spec.rb +0 -33
  327. data/spec/Classes/CARefer_spec.rb +0 -93
  328. data/spec/Classes/CARepeat_spec.rb +0 -65
  329. data/spec/Classes/CASelect_spec.rb +0 -22
  330. data/spec/Classes/CAShift_spec.rb +0 -16
  331. data/spec/Classes/CAStruct_spec.rb +0 -71
  332. data/spec/Classes/CATranspose_spec.rb +0 -60
  333. data/spec/Classes/CAUnboudRepeat_spec.rb +0 -102
  334. data/spec/Classes/CAWindow_spec.rb +0 -54
  335. data/spec/Classes/CAWrap_spec.rb +0 -8
  336. data/spec/Classes/CArray_spec.rb +0 -184
  337. data/spec/Classes/CScalar_spec.rb +0 -55
  338. data/spec/Features/feature_130_spec.rb +0 -19
  339. data/spec/Features/feature_attributes_spec.rb +0 -280
  340. data/spec/Features/feature_boolean_spec.rb +0 -98
  341. data/spec/Features/feature_broadcast.rb +0 -116
  342. data/spec/Features/feature_cast_function.rb +0 -19
  343. data/spec/Features/feature_cast_spec.rb +0 -33
  344. data/spec/Features/feature_class_spec.rb +0 -84
  345. data/spec/Features/feature_complex_spec.rb +0 -42
  346. data/spec/Features/feature_composite_spec.rb +0 -124
  347. data/spec/Features/feature_convert_spec.rb +0 -46
  348. data/spec/Features/feature_copy_spec.rb +0 -123
  349. data/spec/Features/feature_creation_spec.rb +0 -84
  350. data/spec/Features/feature_element_spec.rb +0 -144
  351. data/spec/Features/feature_extream_spec.rb +0 -54
  352. data/spec/Features/feature_generate_spec.rb +0 -74
  353. data/spec/Features/feature_index_spec.rb +0 -69
  354. data/spec/Features/feature_mask_spec.rb +0 -580
  355. data/spec/Features/feature_math_spec.rb +0 -97
  356. data/spec/Features/feature_order_spec.rb +0 -146
  357. data/spec/Features/feature_ref_store_spec.rb +0 -209
  358. data/spec/Features/feature_serialization_spec.rb +0 -125
  359. data/spec/Features/feature_stat_spec.rb +0 -397
  360. data/spec/Features/feature_virtual_spec.rb +0 -48
  361. data/spec/Features/method_eq_spec.rb +0 -81
  362. data/spec/Features/method_is_nan_spec.rb +0 -12
  363. data/spec/Features/method_map_spec.rb +0 -54
  364. data/spec/Features/method_max_with.rb +0 -20
  365. data/spec/Features/method_min_with.rb +0 -19
  366. data/spec/Features/method_ne_spec.rb +0 -18
  367. data/spec/Features/method_project_spec.rb +0 -188
  368. data/spec/Features/method_ref_spec.rb +0 -27
  369. data/spec/Features/method_round_spec.rb +0 -11
  370. data/spec/Features/method_s_linspace_spec.rb +0 -48
  371. data/spec/Features/method_s_span_spec.rb +0 -14
  372. data/spec/Features/method_seq_spec.rb +0 -47
  373. data/spec/Features/method_sort_with.rb +0 -43
  374. data/spec/Features/method_sorted_with.rb +0 -29
  375. data/spec/Features/method_span_spec.rb +0 -42
  376. data/spec/Features/method_wrap_readonly_spec.rb +0 -43
  377. data/spec/UnitTest/test_CAVirtual.rb +0 -214
  378. data/spec/spec_all.rb +0 -10
  379. data/utils/ca_ase.rb +0 -21
  380. data/utils/ca_methods.rb +0 -15
  381. data/utils/cast_checker.rb +0 -30
  382. data/utils/convert_test.rb +0 -73
  383. data/utils/extract_yard.rb +0 -22
  384. data/utils/guess_shape.rb +0 -76
  385. data/utils/monkey_patch_methods.rb +0 -62
  386. data/utils/remove_resource_fork.sh +0 -5
@@ -0,0 +1,1084 @@
1
+ # ----------------------------------------------------------------------------
2
+ #
3
+ # carray/categorical_iterator.rb
4
+ #
5
+ # CACategoricalIterator — a per-category reduction dispatcher. `cat` (a
6
+ # CACategorical) brings the equivalence-class classification; `value` is the
7
+ # payload. `value.group_by_category(cat)` lays `value` out as a
8
+ # category-contiguous eager copy by gathering it through `cat`'s cached
9
+ # grouping plan (the counting sort lives on the categorical, built once and
10
+ # shared across every payload column) and offers per-category reductions off
11
+ # `cat.reduceat_index` as segment boundaries:
12
+ #
13
+ # grp = value.group_by_category(cat)
14
+ # grp.max # per-category max
15
+ # grp.median # order statistics share the same surface
16
+ # grp.stddev
17
+ #
18
+ # This is the consumer of the sort_addr / reduceat_index foundation on
19
+ # CACategorical. Order statistics (median / percentile / quantile) cannot be
20
+ # scattered — they need every value of a group held together — so the values
21
+ # are materialized into contiguous blocks once, and every reduction (monoid or
22
+ # order-stat) then folds those held blocks.
23
+ #
24
+ # Each group is delegated to the same-named CArray reduction over that group's
25
+ # members, so a group result equals `CArray#<reduction>` for those members and
26
+ # the mask contract (empty / all-masked -> identity for sum/prod, UNDEF for
27
+ # ratios) carries through unchanged. Results are length-k CArrays aligned to
28
+ # `cat.labels`; undefined slots are MASKED cells (never magic floats). Output
29
+ # data type and the empty / all-masked answer per method:
30
+ #
31
+ # elements -> int64, classified cells (incl. masked)
32
+ # count / count_not_masked / count_masked / count(v) -> int64
33
+ # sum -> value data type, empty/all-masked = 0 (identity)
34
+ # prod -> float64, empty/all-masked = 1 (identity)
35
+ # max / min -> value data type, empty/all-masked = MASKED
36
+ # mean -> float64, empty/all-masked = MASKED
37
+ # median / percentile -> float64, empty/all-masked = MASKED
38
+ # variance / stddev (sample, ddof=1) -> float64, empty/all-masked = MASKED,
39
+ # single value = 0.0 (n=1 contract)
40
+ # all / any -> boolean (boolean value data type only)
41
+ # labels -> cat.labels
42
+ #
43
+ # Generic iteration (the escape hatch for statistics not in the named surface),
44
+ # matching each_slab / reduce_slab: each { |members| ... } yields per category,
45
+ # reduce { |members| ... } / reduce(init) { |acc, e| ... } folds each category
46
+ # to one value (length-k). map (an element-wise group-wise transform back to
47
+ # the source shape) is a later pass -- use reduce for aggregation.
48
+ #
49
+ # prod / all / any / count(v) are per-group fallbacks (they delegate to the
50
+ # CArray reduction per category); a fused reduceat for them is a later pass.
51
+ #
52
+ # Names (group_by_category / CACategoricalIterator) are provisional; the
53
+ # contract (grouped copy + offsets + labels + per-category reduction) is the
54
+ # ground truth.
55
+ #
56
+ # The per-column gather and the segmented reduction both run in Ruby; the
57
+ # counting sort itself (the dominant cost) is a C kernel cached on the
58
+ # categorical, so a wide aggregate pays it once.
59
+ #
60
+ # ----------------------------------------------------------------------------
61
+
62
+ require "carray"
63
+
64
+ # A CAIterator over the categories of a CACategorical. CAIterator is the
65
+ # family base (the built-in iterators like CAWindowIterator / CABlockIterator
66
+ # are defined in C); a Ruby `Foo < CAIterator` supplies its own behaviour and
67
+ # does not lean on the base machinery. Like CASlabIterator, this class defines
68
+ # its own `each` (over the k categories, yielding each category's member slice),
69
+ # which drives the inherited Enumerable surface; the reduction methods (sum /
70
+ # mean / median / ...) aggregate the groups into length-k arrays. The kernels
71
+ # are per-category slices of an eager, category-contiguous grouped copy. This
72
+ # supersedes the older CAClassIterator.
73
+ class CACategoricalIterator < CAIterator
74
+
75
+ # value : the payload CArray to reduce, one cell per categorical cell.
76
+ # cat : the CACategorical carrying the classification.
77
+ #
78
+ # Lays the value out category-contiguous by GATHERING it through the
79
+ # categorical's cached grouping plan (the counting sort lives on `cat`, built
80
+ # once and shared by every payload column and iterator — see
81
+ # CACategorical's grouping-plan note). The plan gives the segment STARTS
82
+ # (reduceat_index) and the group-major permutation (perm[slot] = source index
83
+ # at that grouped slot, = the valid prefix of sort_addr); gathering value
84
+ # through perm is the only per-column work. Excluded cells (masked or
85
+ # out-of-vocabulary code) are absent from perm, so they never join a group;
86
+ # the value mask rides the gather into the grouped copy.
87
+ def initialize (value, cat)
88
+ @cat = cat
89
+ @labels = cat.labels
90
+ @k = cat.labels.size
91
+ @value = value # source, kept for #cumsum etc.
92
+ @src_shape = value.shape # output shape for #map
93
+ @ndim = 1 # 1-D iterator over k categories
94
+ @shape = [@k]
95
+
96
+ if value.elements == cat.elements
97
+ # Flat classifier path (backward compat): cat classifies every cell of
98
+ # value one-to-one, so eager counting-sort gather is meaningful. This is
99
+ # what all no-axis reductions consume — case C (all cells collapse into k
100
+ # buckets) plus case B interpreted flatly.
101
+ # category_sizes IS the per-group cell counts (what #elements returns);
102
+ # the segment STARTS are its cached exclusive prefix scan (cat.reduceat_
103
+ # index): offsets[c] = sum of counts[0...c]. Both come off the shared
104
+ # plan, so the counting sort is not repeated here.
105
+ @elements = cat.category_sizes.int64
106
+ nvalid = @elements.sum
107
+ @offsets = cat.reduceat_index # cached segment STARTS (int64[k])
108
+ # Group-major source indices = the valid prefix of the cached sort_addr.
109
+ # With no classified cell the prefix is empty (and slicing a length-0
110
+ # sort_addr would be out of range), so take the empty permutation directly.
111
+ @perm = nvalid > 0 ? cat.sort_addr[0...nvalid] : CArray.int64(0)
112
+ @codes = cat.codes.reshape(cat.elements) # flat codes view (map re-walk / weights)
113
+ # Gather value into category-contiguous order via the cached permutation
114
+ # and materialise (the reduceat kernels read the grouped buffer's raw ptr,
115
+ # so it must be a contiguous entity, not the selection view); the value
116
+ # mask rides the gather. Payload-dependent, so this is the only part
117
+ # rebuilt per column. With no classified cell (empty / all-excluded) there
118
+ # is nothing to gather — an empty index into an empty source is out of
119
+ # range — so build the empty grouped buffer directly.
120
+ @grouped = nvalid > 0 ? value.reshape(value.elements)[@perm].copy
121
+ : CArray.new(value.data_type, [0])
122
+ @empty = CArray.new(@grouped.data_type, [0])
123
+ else
124
+ # Shape mismatch: only per-fiber axis: dispatch could still work. With a
125
+ # 1-D value there is no fiber structure to broadcast into, so a mismatch
126
+ # is unrecoverable (preserves the old strict check). For higher-rank
127
+ # value, defer validation to reduce time — check only that cat.ndim fits
128
+ # one of the 3 axis: cases;
129
+ # any no-axis reduce called on this iterator will surface the mismatch
130
+ # because @grouped stays undefined.
131
+ if value.ndim == 1 ||
132
+ ! [1, value.ndim - 1, value.ndim].include?(cat.ndim)
133
+ raise ArgumentError,
134
+ "group_by_category: value.elements (#{value.elements}) != " \
135
+ "cat.elements (#{cat.elements})" +
136
+ (value.ndim == 1 ? "" :
137
+ ". For per-fiber reduce use `.sum(axis: k)`; cat.ndim=" \
138
+ "#{cat.ndim} must be 1 (case A), #{value.ndim} (case B), " \
139
+ "or #{value.ndim - 1} (band-only) for h.ndim=#{value.ndim}.")
140
+ end
141
+ end
142
+ self
143
+ end
144
+
145
+ # @overload each { |members| ... }
146
+ # Yields each category's members (a CArray slice of the grouped copy, in
147
+ # {#labels} order; an empty category yields an empty array). Without a
148
+ # block, returns an Enumerator. This is the own iteration that drives the
149
+ # inherited Enumerable methods (map / count / to_a / ...); it does not use
150
+ # the CAIterator base each / kernel_at_addr path.
151
+ # @yieldparam members [CArray]
152
+ # @return [Enumerator, self]
153
+ def each
154
+ return to_enum(:each) unless block_given?
155
+ @k.times { |c| yield group_slice(c) }
156
+ self
157
+ end
158
+
159
+ # @overload labels
160
+ # Returns the category vocabulary the results are aligned to.
161
+ # @return [Array]
162
+ attr_reader :labels
163
+
164
+ # @overload ngroups
165
+ # Returns the number of groups (= `labels.size`); the length of every
166
+ # per-group result CArray the reductions return.
167
+ # @return [Integer]
168
+ def ngroups
169
+ @k
170
+ end
171
+
172
+ # @overload elements
173
+ # Returns per-group cell counts (classified cells, including value-masked
174
+ # ones; = `cat.category_sizes`), a length-ngroups CArray aligned to
175
+ # {#labels}. The CAIterator count-family member — `CArray#elements`
176
+ # (structural, mask-independent) lifted per group.
177
+ # @return [CArray]
178
+ def elements
179
+ @elements
180
+ end
181
+
182
+ # Group-vocabulary alias for {#elements}; reads naturally next to
183
+ # {#ngroups} and mirrors `CACategorical#category_sizes`.
184
+ alias group_sizes elements
185
+
186
+ # @overload inspect
187
+ # Returns a compact one-line summary — the group count, the label
188
+ # vocabulary, and the per-group cell counts — instead of dumping the
189
+ # internal grouped/value/codes buffers.
190
+ # @return [String]
191
+ def inspect
192
+ "#<#{self.class} ngroups=#{@k} labels=#{@labels.inspect} " \
193
+ "elements=#{@elements.to_a.inspect}>"
194
+ end
195
+
196
+ # @overload count_not_masked
197
+ # Returns the per-category count of present (non-masked) values as int64
198
+ # — the denominator the value reductions actually divide by. Equals
199
+ # {#elements} unless the value carries a mask. A count is always defined,
200
+ # so an empty category is `0` (never masked).
201
+ # @return [CArray]
202
+ # @overload count_not_masked(axis:)
203
+ # Per-fiber per-category count of present (non-masked) values along `axis`
204
+ # (int64, shape [K, ...band]). Empty cells are `0`.
205
+ # @param axis [Integer]
206
+ # @return [CArray]
207
+ def count_not_masked(axis: nil)
208
+ return axis_moments(axis)[:count] if axis
209
+ m = moments
210
+ m ? m[:count] : per_category(CA_INT64) { |s| s.count_not_masked }
211
+ end
212
+
213
+ # @overload count(v = <none>)
214
+ # Per-category count, mirroring `CArray#count` per group. No argument
215
+ # returns {#count_not_masked} (present cells); `count(UNDEF)` returns
216
+ # {#count_masked}; `count(v)` counts cells whose value equals `v`.
217
+ # @return [CArray] length-k int64, aligned to {#labels}
218
+ # @overload count(axis:)
219
+ # No-arg + axis: = per-fiber per-category count_not_masked (shape [K, ...band]).
220
+ # `count(v, axis:)` (value equality) and `count(UNDEF, axis:)` are not
221
+ # implemented; use them without `axis:`.
222
+ # @param axis [Integer]
223
+ # @return [CArray]
224
+ def count (*args, axis: nil)
225
+ if axis
226
+ return count_not_masked(axis: axis) if args.empty?
227
+ raise NotImplementedError,
228
+ "CACategoricalIterator#count(v, axis:) is not implemented — " \
229
+ "value-equality count is available without axis:."
230
+ end
231
+ return count_not_masked if args.empty?
232
+ # Delegate per group to CArray#count (handles count(UNDEF) -> masked count and
233
+ # count(v) alike, with core's exact data type equality). The group slice is a
234
+ # CABlock, whose own #count is the block geometry accessor, so dispatch
235
+ # CArray#count explicitly. (Not fused: a value-equality reduceat would have
236
+ # to reproduce core's cross-type / out-of-range equality exactly.)
237
+ cnt = CArray.instance_method(:count)
238
+ per_category(CA_INT64) { |s| cnt.bind_call(s, *args) }
239
+ end
240
+
241
+ # @overload count_masked
242
+ # Returns the per-category count of masked (missing) values as int64.
243
+ # Empty categories are `0`.
244
+ # @return [CArray]
245
+ # @overload count_masked(axis:)
246
+ # Not implemented; call it without `axis:`.
247
+ # @param axis [Integer]
248
+ # @return [CArray]
249
+ def count_masked(axis: nil)
250
+ if axis
251
+ raise NotImplementedError,
252
+ "CACategoricalIterator#count_masked(axis:) is not implemented — " \
253
+ "call it without axis:."
254
+ end
255
+ m = moments
256
+ m ? @elements - m[:count] : per_category(CA_INT64) { |s| s.count_masked }
257
+ end
258
+
259
+ # @overload sum
260
+ # Returns per-category sums in the data type `CArray#sum` promotes the value
261
+ # to (float64 for an integer value). `accumulate` is the same fold kept in
262
+ # the value's own type. An empty or fully-masked category sums the empty
263
+ # set, which is the additive identity `0` (unmasked) — the same contract as
264
+ # `CArray#sum` on an empty / all-masked array.
265
+ # @return [CArray]
266
+ # @overload sum(axis:)
267
+ # Returns per-category sums per fiber along `axis`. Cat may be 1-D (case
268
+ # A, broadcasts across band axes), same rank as source (case B, per-fiber
269
+ # independent classifier), or one rank less (band-only, constant along
270
+ # reduce axis). Output shape = `[K, ...source.shape without axis]`.
271
+ # @param axis [Integer] reduce axis of the source value.
272
+ # @return [CArray]
273
+ def sum(axis: nil)
274
+ return axis_sum(axis) if axis
275
+ m = moments
276
+ return per_category(core_reduce_type(:sum)) { |s| s.sum } unless m
277
+ m[:sum].copy # the moments sum IS the core fold (empty -> 0.0)
278
+ end
279
+
280
+ # @overload accumulate
281
+ # Returns per-category sums folded in the value's own data type, wrapping at
282
+ # its width, as the core `accumulate` does. This is the exact in-type fold:
283
+ # `sum` reads its answer off a float64 moment and casts back, so it loses
284
+ # the low bits of a wide integer payload and does not wrap. An empty or
285
+ # fully-masked category accumulates the empty set, the additive identity `0`
286
+ # (unmasked).
287
+ # @return [CArray]
288
+ # @overload accumulate(axis:)
289
+ # Per-fiber per-category in-type sums along `axis`. Output shape =
290
+ # `[K, ...source.shape without axis]`.
291
+ # @param axis [Integer] reduce axis of the source value.
292
+ # @return [CArray]
293
+ def accumulate(axis: nil)
294
+ return axis_by_masked_copy(axis, :accumulate, core_reduce_type(:accumulate)) if axis
295
+ per_category(core_reduce_type(:accumulate)) { |s| s.accumulate }
296
+ end
297
+
298
+ # @overload max
299
+ # Returns per-category maxima in the value data type. Empty categories are
300
+ # MASKED.
301
+ # @return [CArray]
302
+ # @overload max(axis:)
303
+ # Per-fiber per-category maxima along `axis` (h's data type, masked where empty).
304
+ # @param axis [Integer]
305
+ # @return [CArray]
306
+ def max(axis: nil)
307
+ return axis_moments(axis)[:max] if axis
308
+ m = moments
309
+ m ? m[:max] : per_category(core_reduce_type(:max)) { |s| s.max }
310
+ end
311
+
312
+ # @overload min
313
+ # Returns per-category minima in the value data type. Empty categories are
314
+ # MASKED.
315
+ # @return [CArray]
316
+ # @overload min(axis:)
317
+ # Per-fiber per-category minima along `axis` (h's data type, masked where empty).
318
+ # @param axis [Integer]
319
+ # @return [CArray]
320
+ def min(axis: nil)
321
+ return axis_moments(axis)[:min] if axis
322
+ m = moments
323
+ m ? m[:min] : per_category(core_reduce_type(:min)) { |s| s.min }
324
+ end
325
+
326
+ # @overload mean
327
+ # Returns per-category means as float64. Empty categories are MASKED.
328
+ # @return [CArray]
329
+ # @overload mean(axis:)
330
+ # Per-fiber per-category means (float64, empty group cells MASKED).
331
+ # @param axis [Integer]
332
+ # @return [CArray]
333
+ def mean(axis: nil)
334
+ return axis_mean(axis) if axis
335
+ m = moments
336
+ return per_category(core_reduce_type(:mean)) { |s| s.mean } unless m
337
+ cnt = m[:count]
338
+ out = m[:sum] / cnt.float64 # count 0 -> NaN, masked next
339
+ out[cnt.eq(0)] = UNDEF # empty / all-masked category -> MASKED
340
+ out
341
+ end
342
+
343
+ # @overload median
344
+ # Returns per-category medians as float64. Empty categories are MASKED.
345
+ # @return [CArray]
346
+ def median(axis: nil)
347
+ axis_order_stat_defer!(:median) if axis
348
+ percentile(50.0)
349
+ end
350
+
351
+ # @overload percentile(p)
352
+ # Returns the per-category `p`-th percentile as float64 (`p` in 0..100,
353
+ # `:linear` interpolation, matching `CArray#percentile`). Empty categories
354
+ # are MASKED. Order statistics need every value of a group held together —
355
+ # this is the reduceat that only the eager grouped copy can serve.
356
+ # @param p [Numeric] percentile in 0..100.
357
+ # @return [CArray]
358
+ def percentile (p, axis: nil)
359
+ axis_order_stat_defer!(:percentile) if axis
360
+ unless MONOID_TYPES.include?(@grouped.data_type)
361
+ return per_category(core_reduce_type(:percentile, p)) { |s| s.percentile(p) }
362
+ end
363
+ out = CArray.float64(@k)
364
+ @grouped.send(:__reduceat_percentile__, @offsets, p.to_f, out)
365
+ out
366
+ end
367
+
368
+ # @overload quantile
369
+ # Returns the per-category five-number summary `[min, Q1, median, Q3, max]`
370
+ # as five length-k float64 CArrays (matching `CArray#quantile`): the
371
+ # percentiles at 0 / 25 / 50 / 75 / 100. Empty / all-masked categories are
372
+ # MASKED. For a single fraction q in 0..1 use `percentile(q * 100)`.
373
+ # @return [Array<CArray>]
374
+ def quantile
375
+ unless MONOID_TYPES.include?(@grouped.data_type)
376
+ return [0, 25, 50, 75, 100].map { |p| percentile(p) }
377
+ end
378
+ outs = Array.new(5) { CArray.float64(@k) }
379
+ @grouped.send(:__reduceat_quantile__, @offsets, *outs)
380
+ outs
381
+ end
382
+
383
+ # @overload variance
384
+ # Returns per-category SAMPLE variance (ddof=1) as float64. Matches
385
+ # `CArray#variance` per group: an empty or fully-masked category is MASKED,
386
+ # a single-value category is `0.0` (CArray's n=1 contract), n>=2 is the
387
+ # sample variance.
388
+ # @return [CArray]
389
+ def variance(axis: nil)
390
+ return axis_by_masked_copy(axis, :variance) if axis
391
+ m = moments
392
+ return per_category(core_reduce_type(:variance)) { |s| s.variance } unless m
393
+ cnt = m[:count]
394
+ means = m[:sum] / cnt.float64 # per-segment mean (garbage where count 0/1,
395
+ out = CArray.float64(@k) # ignored by the kernel's n<2 guards)
396
+ @grouped.send(:__reduceat_variance__, @offsets, means, cnt, out)
397
+ out
398
+ end
399
+
400
+ # @overload stddev
401
+ # Returns per-category SAMPLE standard deviation (ddof=1) as float64.
402
+ # Matches `CArray#stddev` per group (empty / all-masked MASKED,
403
+ # single-value `0.0`).
404
+ # @return [CArray]
405
+ def stddev(axis: nil)
406
+ return axis_by_masked_copy(axis, :stddev) if axis
407
+ m = moments
408
+ return per_category(core_reduce_type(:stddev)) { |s| s.stddev } unless m
409
+ variance.sqrt # sqrt propagates the n=0 mask
410
+ end
411
+
412
+ # @overload prod
413
+ # Returns per-category products as float64 (matching `CArray#prod`). An
414
+ # empty / fully-masked category is `1.0` (the multiplicative identity).
415
+ # Single-pass reduceat for numeric values; per-group fallback otherwise.
416
+ # @return [CArray]
417
+ # @overload prod(axis:)
418
+ # Per-fiber per-category products (float64, shape [K, ...band]). Empty
419
+ # group cells `1.0` (identity).
420
+ # @param axis [Integer]
421
+ # @return [CArray]
422
+ def prod(axis: nil)
423
+ return axis_prod(axis) if axis
424
+ return per_category(core_reduce_type(:prod)) { |s| s.prod } unless MONOID_TYPES.include?(@grouped.data_type)
425
+ out = CArray.float64(@k)
426
+ @grouped.send(:__reduceat_prod__, @offsets, out)
427
+ out
428
+ end
429
+
430
+ # @overload all
431
+ # Returns the per-category `all` as boolean (matching `CArray#all`): true
432
+ # iff every present value is truthy (empty category -> true, vacuously).
433
+ # The value data type must be boolean, as for `CArray#all`.
434
+ # @return [CArray]
435
+ def all
436
+ aa = all_any
437
+ aa ? aa[:all] : per_category(CA_BOOLEAN) { |s| s.all }
438
+ end
439
+
440
+ # @overload any
441
+ # Returns the per-category `any` as boolean (matching `CArray#any`): true
442
+ # iff some present value is truthy (empty category -> false). The value
443
+ # data type must be boolean, as for `CArray#any`.
444
+ # @return [CArray]
445
+ def any
446
+ aa = all_any
447
+ aa ? aa[:any] : per_category(CA_BOOLEAN) { |s| s.any }
448
+ end
449
+
450
+ # ---- tier 2 (fused / population / position) ------------------------------
451
+
452
+ # @overload minmax
453
+ # Returns the per-category `[min, max]` pair (each a length-k CArray in the
454
+ # value data type; empty categories MASKED), matching `CArray#minmax`. Both come
455
+ # from the single cached moments pass.
456
+ # @return [Array<CArray>]
457
+ # @overload minmax(axis:)
458
+ # Per-fiber `[min_ca, max_ca]` along `axis` (each shape [K, ...band], h's data type,
459
+ # empty group cells MASKED). Ruby Array of two CArrays, not stacked.
460
+ # @param axis [Integer]
461
+ # @return [Array<CArray>]
462
+ def minmax(axis: nil)
463
+ return [min(axis: axis), max(axis: axis)] if axis
464
+ [min, max]
465
+ end
466
+
467
+ # @overload variancep
468
+ # Per-category POPULATION variance (ddof=0) as float64, matching
469
+ # `CArray#variancep`: empty / all-masked -> MASKED, single value -> 0.0.
470
+ # Derived from the sample variance (variancep = variance * (n-1) / n), so it
471
+ # reuses the centred two-pass kernel with no extra walk.
472
+ # @return [CArray]
473
+ def variancep(axis: nil)
474
+ return axis_by_masked_copy(axis, :variancep) if axis
475
+ m = moments
476
+ return per_category(core_reduce_type(:variancep)) { |s| s.variancep } unless m
477
+ cnt = m[:count]
478
+ vp = variance * (cnt - 1).float64 / cnt.float64
479
+ vp[cnt.eq(0)] = UNDEF # empty / all-masked stays masked
480
+ vp
481
+ end
482
+
483
+ # @overload stddevp
484
+ # Per-category POPULATION standard deviation (ddof=0) as float64.
485
+ # @return [CArray]
486
+ # @overload stddevp(axis:)
487
+ # Per-fiber per-category population stddev (float64, empty group cells MASKED).
488
+ # @param axis [Integer]
489
+ # @return [CArray]
490
+ def stddevp(axis: nil)
491
+ return axis_by_masked_copy(axis, :stddevp) if axis
492
+ m = moments
493
+ return per_category(core_reduce_type(:stddevp)) { |s| s.stddevp } unless m
494
+ variancep.sqrt
495
+ end
496
+
497
+ # @overload min_index
498
+ # Per-category group-local index of the minimum — the position within the
499
+ # category's members (source order) — matching `CArray#min_index` per group.
500
+ # Empty / all-masked categories are MASKED. Single-pass fused reduceat for
501
+ # numeric values; per-group fallback otherwise.
502
+ # @return [CArray] length-k int64
503
+ def min_index
504
+ am = arg_minmax
505
+ am ? am[:min] : per_category(CA_INT64) { |s| s.min_index }
506
+ end
507
+
508
+ # @overload max_index
509
+ # Per-category group-local index of the maximum. See {#min_index}.
510
+ # @return [CArray] length-k int64
511
+ def max_index
512
+ am = arg_minmax
513
+ am ? am[:max] : per_category(CA_INT64) { |s| s.max_index }
514
+ end
515
+
516
+ # @overload min_addr
517
+ # Per-category flat source address of the minimum — which cell of the source
518
+ # value holds it, matching `CArray#min_addr` per group. Unlike {#min_index}
519
+ # (the group-local rank) this indexes back into the original array
520
+ # (`value.reshape(value.elements)[grp.min_addr]`). Empty categories MASKED.
521
+ # @return [CArray] length-k int64
522
+ def min_addr
523
+ group_addr(min_index)
524
+ end
525
+
526
+ # @overload max_addr
527
+ # Per-category flat source address of the maximum. See {#min_addr}.
528
+ # @return [CArray] length-k int64
529
+ def max_addr
530
+ group_addr(max_index)
531
+ end
532
+
533
+ # @overload sort_addr
534
+ # Per-category sort by flat source address. Returns a length-nvalid
535
+ # (= `elements.sum`) int64 CArray of the flat SOURCE addresses that sort each
536
+ # category's members, in group-major order: segment `c` holds category `c`'s
537
+ # source addresses in ascending-value order, segments concatenated in
538
+ # {#labels} order. So `value.reshape(value.elements)[grp.sort_addr]` yields
539
+ # the values grouped and sorted within each group, and splitting by the
540
+ # {#elements} prefix sum gives per-group. Excluded cells (in no category) are
541
+ # omitted. A masked value sorts to the tail of its segment (as `CArray#sort`
542
+ # sends masked cells to the end), so with a mask the first address is the
543
+ # minimum but the last is the masked cell, not the maximum.
544
+ #
545
+ # Unlike {#min_index} / {#max_index} (group-local rank), this indexes back
546
+ # into the original array. There is no group-local sort surface: a
547
+ # group-local rank order is weak (the grouped copy is already
548
+ # category-contiguous), so only the source-address form is offered, mirroring
549
+ # {#min_addr} vs the skipped group-local min_index-into-source.
550
+ # @return [CArray] length-nvalid int64
551
+ def sort_addr
552
+ out = CArray.int64(@grouped.elements)
553
+ @k.times do |c|
554
+ lo = @offsets[c]
555
+ hi = (c + 1 < @k) ? @offsets[c + 1] : @grouped.elements
556
+ next unless hi > lo
557
+ # View-local sort order of the segment (0..size-1), lifted to grouped
558
+ # slots, then mapped back to source addresses via perm.
559
+ out[lo...hi] = perm[@grouped[lo...hi].sort_addr + lo]
560
+ end
561
+ out
562
+ end
563
+
564
+ # @overload wsum(weights)
565
+ # Per-category weighted sum as float64, matching `CArray#wsum`. `weights` is
566
+ # a per-cell weight CArray in the source order (same elements as the value).
567
+ # Empty / all-masked category -> 0.0 (the additive identity). A cell is
568
+ # skipped iff its value OR its weight is masked (core's contract).
569
+ # @param weights [CArray]
570
+ # @return [CArray]
571
+ # @overload wsum(weights, axis:)
572
+ # Per-fiber per-category weighted sum along `axis`. `weights` must have
573
+ # shape == source.shape (rev3 requires explicit broadcast; wrap 1-D or
574
+ # band-shape weights via `.broadcast_to(*source.shape)` at the call site).
575
+ # Empty group cell → `0.0` (identity). Mask contract: cell contributes iff
576
+ # value AND weight are present.
577
+ # @param weights [CArray]
578
+ # @param axis [Integer]
579
+ # @return [CArray]
580
+ def wsum (weights, axis: nil)
581
+ return axis_wsum_wmean(weights, axis)[0] if axis
582
+ wg = scatter_weights(weights)
583
+ return kernel_weighted(wg)[0] if MONOID_TYPES.include?(@grouped.data_type)
584
+ fold_weighted(wg, 0.0) { |v, ws| v.wsum(ws) }
585
+ end
586
+
587
+ # @overload wmean(weights)
588
+ # Per-category weighted mean as float64, matching `CArray#wmean`. Empty
589
+ # category -> MASKED; a present category whose weights sum to zero -> NaN
590
+ # (core's 0/0 contract).
591
+ # @param weights [CArray]
592
+ # @return [CArray]
593
+ # @overload wmean(weights, axis:)
594
+ # Per-fiber per-category weighted mean along `axis`. Same weights-shape
595
+ # contract as {#wsum} (weights.shape == source.shape). Empty cell → MASKED;
596
+ # a present cell whose weights sum to zero → NaN (0/0 core contract).
597
+ # @param weights [CArray]
598
+ # @param axis [Integer]
599
+ # @return [CArray]
600
+ def wmean (weights, axis: nil)
601
+ return axis_wsum_wmean(weights, axis)[1] if axis
602
+ wg = scatter_weights(weights)
603
+ return kernel_weighted(wg)[1] if MONOID_TYPES.include?(@grouped.data_type)
604
+ fold_weighted(wg, UNDEF) { |v, ws| v.wmean(ws) }
605
+ end
606
+
607
+ # @overload reduce { |members| ... }
608
+ # Custom per-category reduction (the escape hatch for statistics not in the
609
+ # named surface), mirroring `CArray#reduce_slab`. The block receives each
610
+ # category's members (a CArray) and returns one value per category.
611
+ # @yieldparam members [CArray]
612
+ # @return [CArray] length-k, aligned to {#labels}
613
+ # @overload reduce(init) { |acc, elem| ... }
614
+ # Per-category fiber fold: each category's members are folded element by
615
+ # element starting from `init`.
616
+ # @param init [Object] initial accumulator.
617
+ # @return [CArray] length-k
618
+ def reduce (*args, data_type: nil, &blk)
619
+ raise LocalJumpError, "no block given (yield)" unless blk
620
+ dt = data_type || CA_OBJECT
621
+ if args.empty?
622
+ per_category(dt) { |s| blk.call(s) }
623
+ else
624
+ init = args[0]
625
+ per_category(dt) { |s|
626
+ acc = init
627
+ s.each { |e| acc = blk.call(acc, e) }
628
+ acc
629
+ }
630
+ end
631
+ end
632
+
633
+ # @overload map(data_type: nil) { |members| ... }
634
+ # Group-wise element-wise transform, mirroring `CArray#map_slab`. The block
635
+ # receives each category's members and returns either a same-length CArray
636
+ # (scattered back cell for cell) or a scalar (broadcast over the group's
637
+ # cells). Returns a NEW CArray shaped like the source `value`; the original
638
+ # is not modified (`value[] = grp.map { ... }` for in-place). Excluded cells
639
+ # (in no category) are UNDEF in the result.
640
+ # @yieldparam members [CArray]
641
+ # @return [CArray] shaped like the source value
642
+ def map (data_type: nil)
643
+ raise LocalJumpError, "no block given (yield)" unless block_given?
644
+ dt = data_type || @grouped.data_type
645
+ # Apply the block per category, assembled in grouped (category-contiguous)
646
+ # order: a same-length result scatters cell for cell, a scalar broadcasts.
647
+ transformed = CArray.new(dt, [@grouped.elements])
648
+ @k.times do |c|
649
+ lo = @offsets[c]
650
+ hi = (c + 1 < @k) ? @offsets[c + 1] : @grouped.elements
651
+ transformed[lo...hi] = yield(@grouped[lo...hi]) if hi > lo
652
+ end
653
+ # Scatter back to source positions via the permutation (grouped-order source
654
+ # indices). Excluded cells are absent from perm and stay UNDEF.
655
+ out = CArray.new(dt, @src_shape)
656
+ out[] = UNDEF
657
+ out.reshape(@codes.elements)[perm] = transformed
658
+ out
659
+ end
660
+
661
+ # ---- segment scan: within-category running statistics ------------------
662
+ #
663
+ # The per-element-emit siblings of the reductions: unlike a reduction (which
664
+ # collapses each category to one value) a scan preserves the source shape,
665
+ # each cell holding its category's running statistic up to and including that
666
+ # cell, in source (row-major) order. A category is a partition (each cell is
667
+ # in exactly one category), so the running value is single-valued. The flat
668
+ # categorical grouping is the one-band case of the axis-group scan, so each
669
+ # routes straight through the fused C kernel __axis_group_scan__ (the same one
670
+ # CAGroupIterator drives) with the whole source as a single grouped axis and
671
+ # the categorical's codes as the single bundle -- which yields SOURCE-ORDER
672
+ # output directly, so no counting-sort inverse permutation is needed.
673
+ # Excluded (out-of-vocabulary / masked-code) and source-masked cells join no
674
+ # running total and are UNDEF. Mirroring the reductions (sum / mean), a scan
675
+ # takes no axis argument. cumsum / cumprod -> float64, cummax / cummin
676
+ # preserve the value data type, cumcount -> int64 (1-based within-category
677
+ # ordinal); an object value data type is carried by the kernel's object branch.
678
+
679
+ # @overload cumsum
680
+ # Per-category inclusive running sum (float64), source-shaped.
681
+ # @return [CArray]
682
+ # @overload cumprod
683
+ # Per-category inclusive running product (float64), source-shaped.
684
+ # @return [CArray]
685
+ # @overload cummax
686
+ # Per-category inclusive running maximum (value data type), source-shaped.
687
+ # @return [CArray]
688
+ # @overload cummin
689
+ # Per-category inclusive running minimum (value data type), source-shaped.
690
+ # @return [CArray]
691
+ # @overload cumcount
692
+ # Per-category 1-based within-category ordinal (int64), source-shaped.
693
+ # @return [CArray]
694
+ [:cumsum, :cumprod, :cummax, :cummin, :cumcount].each do |op|
695
+ define_method(op) { scan(op) }
696
+ end
697
+
698
+ private
699
+
700
+ # Axis-aware moments (count / sum / min / max) — computed once per axis via
701
+ # the fused per-fiber scatter-reduce C kernel and cached (matches the flat
702
+ # #moments caching in spirit: pay one kernel per {iterator, axis} pair, share
703
+ # across sum / mean / min / max / minmax / count* consumers). Returns
704
+ # `{count: <int64>, sum: <float64>, min: <h's type, masked>, max: <h's type, masked>}`,
705
+ # all shape [K, ...band].
706
+ def axis_moments (axis)
707
+ @axis_moments_cache ||= {}
708
+ cached = @axis_moments_cache[axis]
709
+ return cached if cached
710
+ h = @value
711
+ unless axis.is_a?(Integer) && axis >= 0 && axis < h.ndim
712
+ raise ArgumentError,
713
+ "group_by_category.<reduce>(axis: #{axis.inspect}): axis must be an " \
714
+ "Integer in [0, #{h.ndim}) for source h with shape #{h.shape}"
715
+ end
716
+ codes_h_shape = resolve_axis_codes(@cat.codes, h.shape, axis)
717
+ band = h.shape.dup; band.delete_at(axis)
718
+ out_shape = [@k] + band
719
+ counts = CArray.int64(*out_shape)
720
+ sums = CArray.float64(*out_shape)
721
+ mins = CArray.new(h.data_type, out_shape)
722
+ maxs = CArray.new(h.data_type, out_shape)
723
+ h.__send__(:__fiber_scatter_moments__, codes_h_shape, axis, @k,
724
+ counts, sums, mins, maxs)
725
+ @axis_moments_cache[axis] = {count: counts, sum: sums, min: mins, max: maxs}
726
+ end
727
+
728
+ # Axis-aware sum: the moments sum is already the core fold in the core's own
729
+ # type, so it is handed back as is (an empty group cell carries identity 0.0).
730
+ def axis_sum (axis)
731
+ axis_moments(axis)[:sum].copy
732
+ end
733
+
734
+ # Axis-aware mean: sums / counts (float64); empty group cells (count=0) MASKED.
735
+ # Matches flat #mean per fiber.
736
+ def axis_mean (axis)
737
+ m = axis_moments(axis)
738
+ cnt = m[:count]
739
+ out = m[:sum] / cnt.float64 # count 0 -> NaN
740
+ out[cnt.eq(0)] = UNDEF # empty / all-masked -> MASKED
741
+ out
742
+ end
743
+
744
+ # Axis-aware reduction by masked copy — Ruby-level per-c mask, then delegate
745
+ # to the source's own axis-aware kernel, so the core contract for `op` rides
746
+ # unchanged. Used by the variance family (a centred two-pass numeric
747
+ # aggregate, hitting the same ε-close kernel per (group, axis) that
748
+ # CArray#variance uses) and by `accumulate` (whose in-type wrapping fold has
749
+ # no float64 moment to read it off). Order (median / percentile / quantile)
750
+ # is genuinely order-statistical (needs a sort per group) and remains
751
+ # deferred.
752
+ #
753
+ # Cost: K axis-reductions over an h-shaped local (most cells masked away for
754
+ # each c) — bounded by K, typically small. A fused per-fiber variance
755
+ # kernel is a natural follow-on if bench demands it.
756
+ def axis_by_masked_copy (axis, op, out_data_type = CA_FLOAT64)
757
+ h = @value
758
+ unless axis.is_a?(Integer) && axis >= 0 && axis < h.ndim
759
+ raise ArgumentError,
760
+ "group_by_category.#{op}(axis: #{axis.inspect}): axis must be an " \
761
+ "Integer in [0, #{h.ndim}) for source h with shape #{h.shape}"
762
+ end
763
+ full_c = resolve_axis_codes(@cat.codes, h.shape, axis)
764
+ band = h.shape.dup; band.delete_at(axis)
765
+ out = CArray.new(out_data_type, [@k] + band)
766
+ slot_idx = [nil] + [nil] * band.size # placeholder; c fills slot 0
767
+ codes_bad = full_c.has_mask? ? full_c.is_masked : nil
768
+ @k.times do |c|
769
+ h_local = h.copy
770
+ # Boolean of cells that DO belong to group c (with codes present). On
771
+ # any masked codes cell the codes.eq(c) result carries UNDEF, which
772
+ # naturally reads as "not in group c" for our exclusion purpose.
773
+ in_c = full_c.eq(c)
774
+ exclude = in_c.not
775
+ exclude = exclude | codes_bad if codes_bad
776
+ h_local[exclude] = UNDEF
777
+ slice = h_local.__send__(op, axis: axis) # float64, band shape, mask carries n<contract
778
+ slot_idx[0] = c
779
+ out[*slot_idx] = slice
780
+ end
781
+ out
782
+ end
783
+
784
+ # Axis-aware wsum + wmean fused (single kernel call, both outputs). Returns
785
+ # [wsum_ca, wmean_ca]. Weights must match source shape exactly (explicit
786
+ # broadcast on the call site for 1-D or band-shape weights). A cell
787
+ # contributes iff its value AND its weight are present.
788
+ def axis_wsum_wmean (weights, axis)
789
+ h = @value
790
+ unless axis.is_a?(Integer) && axis >= 0 && axis < h.ndim
791
+ raise ArgumentError,
792
+ "group_by_category.wsum/wmean(axis: #{axis.inspect}): axis must " \
793
+ "be an Integer in [0, #{h.ndim}) for source h with shape #{h.shape}"
794
+ end
795
+ unless weights.is_a?(CArray) && weights.shape == h.shape
796
+ raise ArgumentError,
797
+ "group_by_category.wsum/wmean(axis: #{axis}): weights.shape " \
798
+ "#{weights.respond_to?(:shape) ? weights.shape.inspect : weights.class} " \
799
+ "must equal source.shape #{h.shape.inspect}. Wrap 1-D / band-shape " \
800
+ "weights via `.broadcast_to(*source.shape)` before passing."
801
+ end
802
+ codes_h_shape = resolve_axis_codes(@cat.codes, h.shape, axis)
803
+ weights_f64 = weights.data_type == CA_FLOAT64 ? weights : weights.float64
804
+ band = h.shape.dup; band.delete_at(axis)
805
+ ws_out = CArray.float64(*([@k] + band))
806
+ wm_out = CArray.float64(*([@k] + band))
807
+ h.__send__(:__fiber_scatter_wsum_wmean__, codes_h_shape, weights_f64,
808
+ axis, @k, ws_out, wm_out)
809
+ [ws_out, wm_out]
810
+ end
811
+
812
+ # Axis-aware prod: dedicated kernel (identity 1.0, separate from moments to
813
+ # avoid conflating with sum's zero-identity memset).
814
+ def axis_prod (axis)
815
+ h = @value
816
+ unless axis.is_a?(Integer) && axis >= 0 && axis < h.ndim
817
+ raise ArgumentError,
818
+ "group_by_category.prod(axis: #{axis.inspect}): axis must be an " \
819
+ "Integer in [0, #{h.ndim}) for source h with shape #{h.shape}"
820
+ end
821
+ codes_h_shape = resolve_axis_codes(@cat.codes, h.shape, axis)
822
+ band = h.shape.dup; band.delete_at(axis)
823
+ out = CArray.float64(*([@k] + band))
824
+ h.__send__(:__fiber_scatter_prod__, codes_h_shape, axis, @k, out)
825
+ out
826
+ end
827
+
828
+ # Broadcast `codes` to `h_shape` per PROPOSAL §2.2 3-case positional rule.
829
+ # Returns a broadcast view of codes at h_shape (or codes itself for case B).
830
+ # Raises ArgumentError with a message that enumerates all 3 accepted shapes.
831
+ def resolve_axis_codes (codes, h_shape, axis)
832
+ ndim = h_shape.size
833
+ band = h_shape.dup; band.delete_at(axis)
834
+ case codes.ndim
835
+ when 1
836
+ unless codes.shape == [h_shape[axis]]
837
+ axis_shape_mismatch!(codes.shape, h_shape, axis, band)
838
+ end
839
+ view_shape = Array.new(ndim, 1); view_shape[axis] = h_shape[axis]
840
+ codes.reshape(*view_shape).broadcast_to(*h_shape)
841
+ when ndim
842
+ unless codes.shape == h_shape
843
+ axis_shape_mismatch!(codes.shape, h_shape, axis, band)
844
+ end
845
+ codes
846
+ when ndim - 1
847
+ unless codes.shape == band
848
+ axis_shape_mismatch!(codes.shape, h_shape, axis, band)
849
+ end
850
+ view_shape = h_shape.dup; view_shape[axis] = 1
851
+ codes.reshape(*view_shape).broadcast_to(*h_shape)
852
+ else
853
+ axis_shape_mismatch!(codes.shape, h_shape, axis, band)
854
+ end
855
+ end
856
+
857
+ def axis_shape_mismatch! (cat_shape, h_shape, axis, band)
858
+ raise ArgumentError,
859
+ "group_by_category.sum(axis: #{axis}): cat.shape=#{cat_shape.inspect} " \
860
+ "does not fit any of the 3 accepted forms for h.shape=#{h_shape.inspect}: " \
861
+ "case A cat.shape=[#{h_shape[axis]}], " \
862
+ "case B cat.shape=#{h_shape.inspect}, " \
863
+ "band-only cat.shape=#{band.inspect}."
864
+ end
865
+
866
+ # Order-stat axis: is deferred to Phase 4 (per-fiber counting-sort C kernel).
867
+ # Called from median / percentile / variance / stddev when axis: is given.
868
+ def axis_order_stat_defer! (op)
869
+ raise NotImplementedError,
870
+ "CACategoricalIterator##{op}(axis:) is not implemented — order " \
871
+ "statistics are available without axis:."
872
+ end
873
+
874
+ # Drive a segment scan through the axis-group scan kernel: the whole value as
875
+ # one grouped axis, the flat codes as the single bundle. The kernel emits in
876
+ # source order, so the flat result reshapes straight back to the source shape.
877
+ def scan (op)
878
+ @value.reshape(@value.elements)
879
+ .__axis_group_scan__([0], [[@codes, @k, [0]]], op)
880
+ .reshape(*@src_shape)
881
+ end
882
+
883
+
884
+ # Permutation: perm[slot] = the source index whose value sits at that grouped
885
+ # slot. This is the valid prefix of the categorical's cached sort_addr, sliced
886
+ # at construction (the same counting sort that lays out @grouped), so #map /
887
+ # #sort_addr / the *_addr reductions read it for free.
888
+ def perm
889
+ @perm
890
+ end
891
+
892
+ # Lay a per-cell weight array out in category-contiguous order (same layout as
893
+ # @grouped), so wsum / wmean can pair each group's values with its weights.
894
+ # Weights are coerced to float64; the same counting-sort scatter propagates
895
+ # the weight mask and skips excluded cells, so wg lines up with @grouped.
896
+ def scatter_weights (weights)
897
+ unless weights.elements == @codes.elements
898
+ raise ArgumentError,
899
+ "wsum/wmean: weights.elements (#{weights.elements}) != " \
900
+ "value.elements (#{@codes.elements})"
901
+ end
902
+ wf = weights.float64
903
+ wg = CArray.float64(@grouped.elements)
904
+ @codes.send(:__categorical_scatter__, wf.reshape(wf.elements),
905
+ @offsets.copy, wg, @k)
906
+ wg
907
+ end
908
+
909
+ # Map a per-category group-local index to the flat source address via the
910
+ # permutation (grouped slot -> source index). The min/max sits at grouped slot
911
+ # offsets[c] + local_index[c]; perm carries it back to the source. Empty
912
+ # categories (masked local index) stay masked.
913
+ def group_addr (local_index)
914
+ out = CArray.int64(@k)
915
+ @k.times do |c|
916
+ out[c] = local_index.is_masked[c] ? UNDEF
917
+ : perm[@offsets[c] + local_index[c]]
918
+ end
919
+ out
920
+ end
921
+
922
+ # Fused per-segment weighted sum + weighted mean (one C pass over the grouped
923
+ # copy, weights in group order). Returns [wsum, wmean]; wmean is masked where a
924
+ # segment has no present (value AND weight) pair. Numeric value data types only.
925
+ def kernel_weighted (wg)
926
+ ws = CArray.float64(@k)
927
+ wm = CArray.float64(@k)
928
+ @grouped.send(:__reduceat_wsum_wmean__, @offsets, wg, ws, wm)
929
+ [ws, wm]
930
+ end
931
+
932
+ # Per-group weighted fallback for non-numeric value data types (complex): delegate
933
+ # each group to CArray#wsum / #wmean. Empty segments take the given identity.
934
+ def fold_weighted (wg, empty)
935
+ out = CArray.float64(@k)
936
+ @k.times do |c|
937
+ lo = @offsets[c]
938
+ hi = (c + 1 < @k) ? @offsets[c + 1] : @grouped.elements
939
+ out[c] = hi > lo ? yield(@grouped[lo...hi], wg[lo...hi]) : empty
940
+ end
941
+ out
942
+ end
943
+
944
+ # The members of category `c` as a CArray slice of the grouped copy. An empty
945
+ # category (zero-width segment) yields the shared empty array — a zero-length
946
+ # slice cannot be taken directly, and an empty array carries the same reduction
947
+ # contract we want (identity for sum, UNDEF for ratios).
948
+ def group_slice (c)
949
+ lo = @offsets[c]
950
+ hi = (c + 1 < @k) ? @offsets[c + 1] : @grouped.elements
951
+ hi > lo ? @grouped[lo...hi] : @empty
952
+ end
953
+
954
+ # Single-pass reduceat moments (count / sum / min / max per category), computed
955
+ # once over the grouped copy and cached — the whole point of the eager copy is
956
+ # that one scatter is followed by cheap single-pass reductions with no
957
+ # per-segment views. Nil for a non-numeric value data type (complex / object /
958
+ # bool), where the monoid reductions fall back to per_category.
959
+ # numeric value data types the C moments kernel handles (int8..float64); bool /
960
+ # complex / object fall back to per_category.
961
+ MONOID_TYPES = %i[int8 uint8 int16 uint16 int32 uint32
962
+ int64 uint64 float32 float64].freeze
963
+
964
+ def moments
965
+ return @moments if defined?(@moments)
966
+ @moments =
967
+ if MONOID_TYPES.include?(@grouped.data_type)
968
+ dt = @grouped.data_type
969
+ counts = CArray.int64(@k)
970
+ sums = CArray.float64(@k)
971
+ mins = CArray.new(dt, [@k])
972
+ maxs = CArray.new(dt, [@k])
973
+ @grouped.send(:__reduceat_moments__, @offsets, counts, sums, mins, maxs)
974
+ { count: counts, sum: sums, min: mins, max: maxs }
975
+ end
976
+ end
977
+
978
+ # Single-pass fused group-local argmin / argmax (min_index / max_index),
979
+ # cached. Nil for a non-numeric value data type (fall back to per_category).
980
+ def arg_minmax
981
+ return @arg_minmax if defined?(@arg_minmax)
982
+ @arg_minmax =
983
+ if MONOID_TYPES.include?(@grouped.data_type)
984
+ mn = CArray.int64(@k)
985
+ mx = CArray.int64(@k)
986
+ @grouped.send(:__reduceat_argminmax__, @offsets, mn, mx)
987
+ { min: mn, max: mx }
988
+ end
989
+ end
990
+
991
+ # Single-pass fused per-category boolean all / any, cached. Nil unless the
992
+ # value data type is boolean (fall back to per_category, which raises like
993
+ # CArray#all on a non-boolean).
994
+ def all_any
995
+ return @all_any if defined?(@all_any)
996
+ @all_any =
997
+ if @grouped.data_type == CA_BOOLEAN
998
+ a = CArray.boolean(@k)
999
+ o = CArray.boolean(@k)
1000
+ @grouped.send(:__reduceat_all_any__, @offsets, a, o)
1001
+ { all: a, any: o }
1002
+ end
1003
+ end
1004
+
1005
+ # Build a length-k typed output by folding each category's members with the
1006
+ # given reduction block. Fallback path (order statistics, and monoids on a
1007
+ # non-numeric value data type): each group is delegated to the same CArray
1008
+ # reduction, so the per-group result matches `CArray#<reduction>` over that
1009
+ # group's members — the mask carries the "insufficient present data" contract
1010
+ # for free (an all-masked group reduces like an empty one; identity-bearing
1011
+ # reductions return their identity, ratios return UNDEF; see ext ERI).
1012
+ # The data type the core reduction `op` promotes this value to. Asked of the
1013
+ # core itself -- a one-cell reduction of the value's type -- rather than
1014
+ # restated here, so a per-category answer cannot drift from `CArray#<op>`
1015
+ # (`sum` on an integer promotes, `accumulate` stays, `min` / `max` keep the
1016
+ # type but a boolean widens, `prod` on an object stays an object). A payload
1017
+ # the core refuses to fold this way raises here, with the core's own error.
1018
+ def core_reduce_type (op, *args)
1019
+ (@core_reduce_type ||= {})[[op, args]] ||=
1020
+ CArray.new(@grouped.data_type, [1, 1]).public_send(op, *args, axis: 1).data_type
1021
+ end
1022
+
1023
+ def per_category (data_type)
1024
+ out = CArray.new(data_type, [@k])
1025
+ @k.times { |c| out[c] = yield(group_slice(c)) }
1026
+ out
1027
+ end
1028
+ end
1029
+
1030
+
1031
+ class CArray
1032
+ # @overload group_by_category(cat)
1033
+ # Returns a {CACategoricalIterator} that reduces `self` (the payload)
1034
+ # per category of `cat`. Requires `self.elements == cat.elements`.
1035
+ # @param cat [CACategorical] the classifier.
1036
+ # @return [CACategoricalIterator]
1037
+ # @raise [ArgumentError] when element counts differ.
1038
+ def group_by_category (cat)
1039
+ CACategoricalIterator.new(self, cat)
1040
+ end
1041
+
1042
+ # @overload group_by_run
1043
+ # Segments `self` into maximal runs of consecutive non-masked cells and
1044
+ # returns a {CACategoricalIterator} that reduces each run as one category,
1045
+ # ordered by position. The run boundary is the mask: a masked cell belongs
1046
+ # to no run and breaks any run across it. State what separates runs (the
1047
+ # "background") by masking before the call — e.g. `ca.mask_where(:le, 0)`
1048
+ # makes non-positive cells background without mutating `ca`. A series with
1049
+ # no present cell yields zero groups rather than raising. 1-D only.
1050
+ #
1051
+ # ```ruby
1052
+ # prec = CA_DOUBLE([1,2,2,2,0,0,0,2,1,2,0,0,0,3,2,3,2,1,0,0,0])
1053
+ # grp = prec.mask_where(:le, 0).group_by_run
1054
+ # grp.sum # => [7.0, 5.0, 11.0] per-run accumulation
1055
+ # grp.count # => [4, 3, 5] per-run length
1056
+ # grp.each { |members| ... } # each run as a CArray
1057
+ # ```
1058
+ #
1059
+ # The run categories are labelled by their 0-based run index, so
1060
+ # `grp.labels` is `[0, 1, ...]` in position order.
1061
+ #
1062
+ # @return [CACategoricalIterator] one category per run, in order.
1063
+ # @raise [RuntimeError] when `self` is not 1-D.
1064
+ def group_by_run
1065
+ raise "group_by_run: 1-D only (got #{ndim}-D)" unless ndim == 1
1066
+ if elements == 0
1067
+ code = CArray.int64(0)
1068
+ else
1069
+ present = is_not_masked
1070
+ edge = present & present.shift(1).not # rising edge = run start
1071
+ # feed cumsum via a zero-copy int8 reinterpret of the 1-byte booleans
1072
+ # rather than widening to int64; cumsum promotes to float64, so the
1073
+ # running count never overflows int8.
1074
+ code = edge.refer(:int8).cumsum.int64 - 1 # 0-based run index per cell
1075
+ code[present.not] = UNDEF # masked cells join no run
1076
+ end
1077
+ # categorize turns the dense run indices into the run categories: it derives
1078
+ # the label vocabulary and folds an all-masked (dry) series to zero groups
1079
+ # on its own, so no explicit run count is needed here. code is monotonic (a
1080
+ # cumsum), so categorize's first-appearance order is already run order and
1081
+ # sort_labels would be a no-op.
1082
+ group_by_category(code.categorize)
1083
+ end
1084
+ end