carray 2.0.1 → 3.0.0

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 (339) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +5 -25
  3. data/CHANGELOG.md +16 -0
  4. data/LICENSE +1 -1
  5. data/NEWS.md +3 -0
  6. data/README.md +128 -44
  7. data/carray.gemspec +22 -24
  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 +1208 -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 +125 -0
  15. data/ext/ca_binop_dispatch.h +159 -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 +241 -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 +4321 -0
  26. data/ext/ca_kernel_iterator.h +2603 -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 +809 -0
  33. data/ext/ca_obj_binop.c +892 -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 +965 -0
  39. data/ext/ca_obj_face.c +670 -0
  40. data/ext/ca_obj_face.h +247 -0
  41. data/ext/ca_obj_fake.c +228 -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 +306 -0
  45. data/ext/ca_obj_grid.c +858 -440
  46. data/ext/ca_obj_meld.c +1034 -0
  47. data/ext/ca_obj_moncmp.c +569 -0
  48. data/ext/ca_obj_monop.c +1111 -0
  49. data/ext/ca_obj_object.c +772 -297
  50. data/ext/ca_obj_record.c +468 -0
  51. data/ext/ca_obj_reduce.c +97 -82
  52. data/ext/ca_obj_refer.c +569 -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 +616 -0
  56. data/ext/ca_obj_select.c +344 -296
  57. data/ext/ca_obj_select_axis.c +1296 -0
  58. data/ext/ca_obj_shift.c +229 -791
  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 +2501 -0
  62. data/ext/ca_obj_string.c +268 -0
  63. data/ext/ca_obj_tile.c +614 -0
  64. data/ext/ca_obj_time.c +546 -0
  65. data/ext/ca_obj_timedelta.c +435 -0
  66. data/ext/ca_obj_transpose.c +62 -516
  67. data/ext/ca_obj_triop.c +746 -0
  68. data/ext/ca_obj_unbound_repeat.c +207 -240
  69. data/ext/ca_obj_window.c +1131 -563
  70. data/ext/ca_op_byte_swap.c +175 -0
  71. data/ext/ca_op_ipower.c +319 -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 +430 -0
  75. data/ext/ca_sweep_engine.h +157 -0
  76. data/ext/ca_transform_common.c +228 -0
  77. data/ext/ca_triop_dispatch.c +55 -0
  78. data/ext/ca_triop_dispatch.h +62 -0
  79. data/ext/carray.h +795 -402
  80. data/ext/carray_access.c +831 -711
  81. data/ext/carray_attribute.c +98 -329
  82. data/ext/carray_bincount.c +255 -0
  83. data/ext/carray_broadcast.c +283 -0
  84. data/ext/carray_call_cfunc.c +1360 -828
  85. data/ext/carray_call_cfunc.h +160 -0
  86. data/ext/carray_cast.c +1212 -301
  87. data/ext/carray_cast_func.rb +81 -40
  88. data/ext/carray_class.c +53 -63
  89. data/ext/carray_config.h +28 -0
  90. data/ext/carray_conversion.c +350 -346
  91. data/ext/carray_copy.c +156 -268
  92. data/ext/carray_core.c +1341 -198
  93. data/ext/carray_count.c +312 -0
  94. data/ext/carray_data_type.c +43 -19
  95. data/ext/carray_element.c +585 -213
  96. data/ext/carray_factorize.c +2542 -0
  97. data/ext/carray_generate.c +230 -559
  98. data/ext/carray_histogram.c +490 -0
  99. data/ext/carray_hold.c +228 -0
  100. data/ext/carray_index_classifier.c +1035 -0
  101. data/ext/carray_index_classifier.h +27 -0
  102. data/ext/carray_internal.h +120 -0
  103. data/ext/carray_kernels_bincmp.c +4445 -0
  104. data/ext/carray_kernels_binop.c +10979 -0
  105. data/ext/carray_kernels_init.c +36 -0
  106. data/ext/carray_kernels_map.c +3466 -0
  107. data/ext/carray_kernels_moncmp.c +2096 -0
  108. data/ext/carray_kernels_monop.c +18312 -0
  109. data/ext/carray_kernels_reduce_aggregate.c +25836 -0
  110. data/ext/carray_kernels_reduce_boolean.c +329 -0
  111. data/ext/carray_kernels_reduce_cumulative.c +14592 -0
  112. data/ext/carray_kernels_reduce_extreme.c +16947 -0
  113. data/ext/carray_kernels_reduce_variance.c +3909 -0
  114. data/ext/carray_kernels_scan.c +3692 -0
  115. data/ext/carray_kernels_search.c +32137 -0
  116. data/ext/carray_kernels_sort.c +10625 -0
  117. data/ext/carray_kernels_triop.c +1391 -0
  118. data/ext/carray_lazy.c +567 -0
  119. data/ext/carray_loop.c +88 -200
  120. data/ext/carray_mask.c +848 -154
  121. data/ext/carray_math_kernel.h +120 -0
  122. data/ext/carray_mathfunc.c +10 -241
  123. data/ext/carray_median_percentile.c +1257 -0
  124. data/ext/carray_memory_view.c +1625 -0
  125. data/ext/carray_operator.c +1526 -318
  126. data/ext/carray_order.c +664 -1394
  127. data/ext/carray_partition.c +416 -0
  128. data/ext/carray_random.c +518 -0
  129. data/ext/carray_scatter.c +357 -0
  130. data/ext/carray_slab.c +1219 -0
  131. data/ext/carray_slab.h +84 -0
  132. data/ext/carray_sort.c +829 -0
  133. data/ext/carray_sort_kernel.c +620 -0
  134. data/ext/carray_struct.c +695 -0
  135. data/ext/carray_test.c +343 -229
  136. data/ext/carray_undef.c +34 -17
  137. data/ext/carray_utils.c +175 -74
  138. data/ext/extconf.rb +216 -55
  139. data/ext/mk_call_cfunc.rb +480 -0
  140. data/ext/mkkernel.rb +8842 -0
  141. data/ext/ruby_carray.c +202 -101
  142. data/ext/version.h +5 -15
  143. data/ext/version.rb +5 -13
  144. data/lib/carray/arrow_tensor.rb +401 -0
  145. data/lib/carray/attribute.rb +166 -0
  146. data/lib/carray/autoload_carray.rb +220 -0
  147. data/lib/carray/autoload_method_extension.rb +44 -0
  148. data/lib/carray/axis_group.rb +711 -0
  149. data/lib/carray/basics.rb +481 -0
  150. data/lib/carray/bincount_nd.rb +358 -0
  151. data/lib/carray/block_iterator.rb +604 -0
  152. data/lib/carray/boolean_reduce.rb +109 -0
  153. data/lib/carray/categorical.rb +561 -0
  154. data/lib/carray/categorical_iterator.rb +1062 -0
  155. data/lib/carray/complex.rb +150 -0
  156. data/lib/carray/conditional.rb +216 -0
  157. data/lib/carray/const_string.rb +228 -0
  158. data/lib/carray/construct.rb +139 -328
  159. data/lib/carray/core_extensions.rb +240 -0
  160. data/lib/carray/data_type_extension.rb +233 -0
  161. data/lib/carray/fixlen_string.rb +95 -0
  162. data/lib/carray/frame/concat.rb +132 -0
  163. data/lib/carray/frame/convert.rb +95 -0
  164. data/lib/carray/frame/csv_parser.rb +211 -0
  165. data/lib/carray/frame/frame.rb +649 -0
  166. data/lib/carray/frame/group.rb +186 -0
  167. data/lib/carray/frame/io.rb +164 -0
  168. data/lib/carray/frame/join.rb +248 -0
  169. data/lib/carray/frame/records.rb +99 -0
  170. data/lib/carray/frame/sort.rb +113 -0
  171. data/lib/carray/frame/verbs.rb +299 -0
  172. data/lib/carray/frame.rb +16 -0
  173. data/lib/carray/histogram.rb +512 -0
  174. data/lib/carray/inspect.rb +37 -20
  175. data/lib/carray/iterator.rb +57 -349
  176. data/lib/carray/lazy.rb +889 -0
  177. data/lib/carray/mask_gap_fill.rb +200 -0
  178. data/lib/carray/math.rb +78 -342
  179. data/lib/carray/meld_reduce.rb +289 -0
  180. data/lib/carray/methods/align_addr.rb +116 -0
  181. data/lib/carray/methods/bin.rb +128 -0
  182. data/lib/carray/methods/bincount.rb +87 -0
  183. data/lib/carray/methods/bit_string.rb +92 -0
  184. data/lib/carray/methods/broadcast.rb +63 -0
  185. data/lib/carray/methods/choose.rb +39 -0
  186. data/lib/carray/methods/composition.rb +280 -0
  187. data/lib/carray/methods/gather_nd.rb +206 -0
  188. data/lib/carray/methods/index.rb +39 -0
  189. data/lib/carray/methods/insert_block.rb +99 -0
  190. data/lib/carray/methods/is_in.rb +141 -0
  191. data/lib/carray/methods/join.rb +90 -0
  192. data/lib/carray/methods/locate_addr.rb +47 -0
  193. data/lib/carray/methods/mask_duplicates.rb +41 -0
  194. data/lib/carray/methods/meshgrid.rb +91 -0
  195. data/lib/carray/methods/mode.rb +126 -0
  196. data/lib/carray/methods/nunique.rb +46 -0
  197. data/lib/carray/methods/resize.rb +56 -0
  198. data/lib/carray/methods/snap.rb +156 -0
  199. data/lib/carray/methods/string_format.rb +57 -0
  200. data/lib/carray/methods/unique.rb +47 -0
  201. data/lib/carray/methods/value_counts.rb +71 -0
  202. data/lib/carray/mkmf.rb +124 -101
  203. data/lib/carray/runtime.rb +108 -0
  204. data/lib/carray/serialize.rb +478 -167
  205. data/lib/carray/slab_iterator.rb +292 -0
  206. data/lib/carray/stack.rb +291 -0
  207. data/lib/carray/string.rb +56 -180
  208. data/lib/carray/string_operation_extension.rb +289 -0
  209. data/lib/carray/struct.rb +335 -323
  210. data/lib/carray/struct_builder.rb +697 -0
  211. data/lib/carray/table.rb +41 -2
  212. data/lib/carray/time.rb +2255 -38
  213. data/lib/carray/window_iterator.rb +655 -0
  214. data/lib/carray.rb +55 -57
  215. metadata +163 -130
  216. data/Rakefile +0 -51
  217. data/TODO.md +0 -18
  218. data/ext/ca_iter_block.c +0 -257
  219. data/ext/ca_iter_dimension.c +0 -299
  220. data/ext/ca_iter_window.c +0 -214
  221. data/ext/ca_obj_mapping.c +0 -644
  222. data/ext/carray_iterator.c +0 -641
  223. data/ext/carray_math.rb +0 -850
  224. data/ext/carray_numeric.c +0 -259
  225. data/ext/carray_sort_addr.c +0 -254
  226. data/ext/carray_stat.c +0 -2100
  227. data/ext/carray_stat_proc.rb +0 -1999
  228. data/ext/mkmath.rb +0 -741
  229. data/ext/ruby_ccomplex.c +0 -509
  230. data/ext/ruby_float_func.c +0 -86
  231. data/lib/carray/array.rb +0 -8
  232. data/lib/carray/autoload/autoload_base.rb +0 -19
  233. data/lib/carray/autoload/autoload_gem_cairo.rb +0 -9
  234. data/lib/carray/autoload/autoload_gem_ffi.rb +0 -9
  235. data/lib/carray/autoload/autoload_gem_gnuplot.rb +0 -2
  236. data/lib/carray/autoload/autoload_gem_io_csv.rb +0 -14
  237. data/lib/carray/autoload/autoload_gem_io_pg.rb +0 -6
  238. data/lib/carray/autoload/autoload_gem_io_sqlite3.rb +0 -12
  239. data/lib/carray/autoload/autoload_gem_narray.rb +0 -10
  240. data/lib/carray/autoload/autoload_gem_numo_narray.rb +0 -15
  241. data/lib/carray/autoload/autoload_gem_opencv.rb +0 -16
  242. data/lib/carray/autoload/autoload_gem_random.rb +0 -8
  243. data/lib/carray/autoload/autoload_gem_rmagick.rb +0 -23
  244. data/lib/carray/autoload/autoload_gem_zimg.rb +0 -3
  245. data/lib/carray/autoload/autoload_io_imagemagick.rb +0 -6
  246. data/lib/carray/autoload/autoload_math_histogram.rb +0 -5
  247. data/lib/carray/autoload/autoload_math_recurrence.rb +0 -6
  248. data/lib/carray/autoload/autoload_object_iterator.rb +0 -1
  249. data/lib/carray/autoload/autoload_object_link.rb +0 -1
  250. data/lib/carray/autoload/autoload_object_pack.rb +0 -2
  251. data/lib/carray/autoload.rb +0 -141
  252. data/lib/carray/basic.rb +0 -191
  253. data/lib/carray/broadcast.rb +0 -101
  254. data/lib/carray/compose.rb +0 -315
  255. data/lib/carray/convert.rb +0 -115
  256. data/lib/carray/info.rb +0 -110
  257. data/lib/carray/io/imagemagick.rb +0 -235
  258. data/lib/carray/mask.rb +0 -102
  259. data/lib/carray/math/histogram.rb +0 -177
  260. data/lib/carray/math/recurrence.rb +0 -93
  261. data/lib/carray/object/ca_obj_iterator.rb +0 -50
  262. data/lib/carray/object/ca_obj_link.rb +0 -50
  263. data/lib/carray/object/ca_obj_pack.rb +0 -99
  264. data/lib/carray/obsolete.rb +0 -256
  265. data/lib/carray/ordering.rb +0 -181
  266. data/lib/carray/testing.rb +0 -51
  267. data/lib/carray/transform.rb +0 -109
  268. data/mailmap +0 -1
  269. data/misc/Methods.ja.md +0 -182
  270. data/misc/NOTE +0 -51
  271. data/spec/Classes/CABitfield_spec.rb +0 -58
  272. data/spec/Classes/CABlockIterator_spec.rb +0 -114
  273. data/spec/Classes/CABlock_spec.rb +0 -205
  274. data/spec/Classes/CAField_spec.rb +0 -39
  275. data/spec/Classes/CAGrid_spec.rb +0 -75
  276. data/spec/Classes/CAMap_spec.rb +0 -0
  277. data/spec/Classes/CAMapping_spec.rb +0 -105
  278. data/spec/Classes/CAObject_attribute_spec.rb +0 -33
  279. data/spec/Classes/CAObject_spec.rb +0 -33
  280. data/spec/Classes/CARefer_spec.rb +0 -93
  281. data/spec/Classes/CARepeat_spec.rb +0 -65
  282. data/spec/Classes/CASelect_spec.rb +0 -22
  283. data/spec/Classes/CAShift_spec.rb +0 -16
  284. data/spec/Classes/CAStruct_spec.rb +0 -71
  285. data/spec/Classes/CATranspose_spec.rb +0 -60
  286. data/spec/Classes/CAUnboudRepeat_spec.rb +0 -102
  287. data/spec/Classes/CAWindow_spec.rb +0 -54
  288. data/spec/Classes/CAWrap_spec.rb +0 -8
  289. data/spec/Classes/CArray_spec.rb +0 -184
  290. data/spec/Classes/CScalar_spec.rb +0 -55
  291. data/spec/Features/feature_130_spec.rb +0 -19
  292. data/spec/Features/feature_attributes_spec.rb +0 -280
  293. data/spec/Features/feature_boolean_spec.rb +0 -98
  294. data/spec/Features/feature_broadcast.rb +0 -116
  295. data/spec/Features/feature_cast_function.rb +0 -19
  296. data/spec/Features/feature_cast_spec.rb +0 -33
  297. data/spec/Features/feature_class_spec.rb +0 -84
  298. data/spec/Features/feature_complex_spec.rb +0 -42
  299. data/spec/Features/feature_composite_spec.rb +0 -124
  300. data/spec/Features/feature_convert_spec.rb +0 -46
  301. data/spec/Features/feature_copy_spec.rb +0 -123
  302. data/spec/Features/feature_creation_spec.rb +0 -84
  303. data/spec/Features/feature_element_spec.rb +0 -144
  304. data/spec/Features/feature_extream_spec.rb +0 -54
  305. data/spec/Features/feature_generate_spec.rb +0 -74
  306. data/spec/Features/feature_index_spec.rb +0 -69
  307. data/spec/Features/feature_mask_spec.rb +0 -580
  308. data/spec/Features/feature_math_spec.rb +0 -97
  309. data/spec/Features/feature_order_spec.rb +0 -146
  310. data/spec/Features/feature_ref_store_spec.rb +0 -209
  311. data/spec/Features/feature_serialization_spec.rb +0 -125
  312. data/spec/Features/feature_stat_spec.rb +0 -397
  313. data/spec/Features/feature_virtual_spec.rb +0 -48
  314. data/spec/Features/method_eq_spec.rb +0 -81
  315. data/spec/Features/method_is_nan_spec.rb +0 -12
  316. data/spec/Features/method_map_spec.rb +0 -54
  317. data/spec/Features/method_max_with.rb +0 -20
  318. data/spec/Features/method_min_with.rb +0 -19
  319. data/spec/Features/method_ne_spec.rb +0 -18
  320. data/spec/Features/method_project_spec.rb +0 -188
  321. data/spec/Features/method_ref_spec.rb +0 -27
  322. data/spec/Features/method_round_spec.rb +0 -11
  323. data/spec/Features/method_s_linspace_spec.rb +0 -48
  324. data/spec/Features/method_s_span_spec.rb +0 -14
  325. data/spec/Features/method_seq_spec.rb +0 -47
  326. data/spec/Features/method_sort_with.rb +0 -43
  327. data/spec/Features/method_sorted_with.rb +0 -29
  328. data/spec/Features/method_span_spec.rb +0 -42
  329. data/spec/Features/method_wrap_readonly_spec.rb +0 -43
  330. data/spec/UnitTest/test_CAVirtual.rb +0 -214
  331. data/spec/spec_all.rb +0 -10
  332. data/utils/ca_ase.rb +0 -21
  333. data/utils/ca_methods.rb +0 -15
  334. data/utils/cast_checker.rb +0 -30
  335. data/utils/convert_test.rb +0 -73
  336. data/utils/extract_yard.rb +0 -22
  337. data/utils/guess_shape.rb +0 -76
  338. data/utils/monkey_patch_methods.rb +0 -62
  339. data/utils/remove_resource_fork.sh +0 -5
@@ -0,0 +1,109 @@
1
+ # Boolean folds all / any / none gain a `skip_masked:` keyword.
2
+ #
3
+ # skip_masked: true (default) -- available-case (skipna): masked cells are
4
+ # ignored, the result is always true/false.
5
+ # This is the existing behavior; every other
6
+ # keyword (axis:, keep_axis:, ...) forwards
7
+ # unchanged to the C reduction.
8
+ # skip_masked: false -- three-valued (Kleene) fold: the result is
9
+ # UNDEF when a masked cell could change it.
10
+ # OR is undetermined without a known true;
11
+ # AND without a known false. Matches the
12
+ # element-wise `|` / `&` Kleene semantics as
13
+ # an n-ary fold (|=max, &=min over
14
+ # false < unknown < true).
15
+ #
16
+ # The Kleene fold composes from count(true) / count(false) / count_masked, so
17
+ # there is no dedicated kernel; the C reductions keep the skipna semantics
18
+ # under the `__*_skipna__` aliases.
19
+
20
+ class CArray
21
+
22
+ alias_method :__any_skipna__, :any
23
+ alias_method :__all_skipna__, :all
24
+ alias_method :__none_skipna__, :none
25
+ private :__any_skipna__, :__all_skipna__, :__none_skipna__
26
+
27
+ # Whether any cell is true.
28
+ #
29
+ # With `skip_masked: true` (the default) masked cells are simply ignored and
30
+ # the result is always `true` / `false`. With `skip_masked: false` the fold
31
+ # is three-valued: the result is `UNDEF` when a masked cell could change it,
32
+ # matching the element-wise Kleene semantics of `|` / `&`.
33
+ #
34
+ # @param skip_masked [Boolean] ignore masked cells, or fold them three-valued.
35
+ # @param opts [Hash] forwarded to the underlying reduction (`axis:`,
36
+ # `keep_axis:`, ...).
37
+ # @return [Boolean, CArray] a scalar, or an array when an axis is given.
38
+ def any (skip_masked: true, **opts)
39
+ return __any_skipna__(**opts) if skip_masked
40
+ __kleene_fold(:any, opts)
41
+ end
42
+
43
+ # Whether every cell is true.
44
+ #
45
+ # With `skip_masked: true` (the default) masked cells are simply ignored and
46
+ # the result is always `true` / `false`. With `skip_masked: false` the fold
47
+ # is three-valued: the result is `UNDEF` when a masked cell could change it,
48
+ # matching the element-wise Kleene semantics of `|` / `&`.
49
+ #
50
+ # @param skip_masked [Boolean] ignore masked cells, or fold them three-valued.
51
+ # @param opts [Hash] forwarded to the underlying reduction (`axis:`,
52
+ # `keep_axis:`, ...).
53
+ # @return [Boolean, CArray] a scalar, or an array when an axis is given.
54
+ def all (skip_masked: true, **opts)
55
+ return __all_skipna__(**opts) if skip_masked
56
+ __kleene_fold(:all, opts)
57
+ end
58
+
59
+ # Whether no cell is true.
60
+ #
61
+ # With `skip_masked: true` (the default) masked cells are simply ignored and
62
+ # the result is always `true` / `false`. With `skip_masked: false` the fold
63
+ # is three-valued: the result is `UNDEF` when a masked cell could change it,
64
+ # matching the element-wise Kleene semantics of `|` / `&`.
65
+ #
66
+ # @param skip_masked [Boolean] ignore masked cells, or fold them three-valued.
67
+ # @param opts [Hash] forwarded to the underlying reduction (`axis:`,
68
+ # `keep_axis:`, ...).
69
+ # @return [Boolean, CArray] a scalar, or an array when an axis is given.
70
+ def none (skip_masked: true, **opts)
71
+ return __none_skipna__(**opts) if skip_masked
72
+ # none = not any (Kleene): not(true)=false, not(false)=true, not(UNDEF)=UNDEF
73
+ r = __kleene_fold(:any, opts)
74
+ r.is_a?(CArray) ? r.not : (r.equal?(UNDEF) ? UNDEF : !r)
75
+ end
76
+
77
+ private
78
+
79
+ # Three-valued fold via counts. `kind` is :any (OR) or :all (AND).
80
+ # `opts` carries axis: / keep_axis: (same surface as the skipna reduction).
81
+ def __kleene_fold (kind, opts)
82
+ axis = opts[:axis]
83
+ if axis.nil?
84
+ if kind == :any
85
+ return true if count(true) > 0 # a known TRUE settles OR
86
+ else
87
+ return false if count(false) > 0 # a known FALSE settles AND
88
+ end
89
+ return UNDEF if count_masked > 0 # undetermined
90
+ return( kind == :any ? false : true )
91
+ end
92
+
93
+ keep = opts[:keep_axis]
94
+ cnt_kw = keep ? { axis: axis, keep_axis: keep } : { axis: axis }
95
+ ck = ( kind == :any ) ? count(true, **cnt_kw) : count(false, **cnt_kw)
96
+ cm = count_masked(axis: axis) # count_masked has no keep_axis
97
+ cm = cm.reshape(*ck.shape) unless cm.shape == ck.shape
98
+
99
+ # A cell is undetermined only where it is masked AND the known side does
100
+ # not settle it (ck counts the settling value: true for OR, false for AND).
101
+ # Masking only those cells avoids leaving an all-zero mask behind.
102
+ out = CArray.boolean(*ck.shape)
103
+ out[] = ( kind == :any ) ? 0 : 1 # OR default false / AND default true
104
+ out[ck.eq(0) & (cm > 0)] = UNDEF # genuinely undetermined
105
+ out[ck > 0] = ( kind == :any ) ? 1 : 0 # known side dominates
106
+ out
107
+ end
108
+
109
+ end
@@ -0,0 +1,561 @@
1
+ # ----------------------------------------------------------------------------
2
+ #
3
+ # carray/categorical.rb
4
+ #
5
+ # CACategorical — a categorical dtype: dense integer codes (the storage) plus
6
+ # a label vocabulary. Structurally the same as a pandas Categorical or an
7
+ # Arrow dictionary: each element is an index into a small set of categories.
8
+ #
9
+ # Implemented as a READONLY NonNumeric Face (see docs/CAFace.md) over
10
+ # the codes array:
11
+ #
12
+ # - storage (parent) = the integer codes
13
+ # - surface = CA_FIXLEN, so raw numeric kernels are gated off
14
+ # (`cat + 1` raises — arithmetic on category codes is
15
+ # nonsense). The meaningful operations come back for
16
+ # free on the codes parent: `cat.codes.bincount`
17
+ # (per-category counts), `cat.codes.count(code)`, etc.
18
+ # - per-cell access = decode the code into its label (`cat[i]` -> category)
19
+ #
20
+ # Exclusion (missing / out-of-vocabulary) is encoded two ways at once, and
21
+ # because the Face is READONLY they can never desync:
22
+ #
23
+ # - the cell is MASKED -> CArray-native idiom: `cat[i]` is
24
+ # UNDEF, `is_masked` / mask-aware
25
+ # reductions / category_sizes all work
26
+ # - the cell STORES the all-ones value -> the type-max sentinel, which is
27
+ # signed -1 byte-for-byte. The
28
+ # axis-group kernel skips it by its
29
+ # [0, k) range check (no kernel
30
+ # change); the pandas / Arrow bridge
31
+ # reads it as -1 by byte-reinterpret
32
+ # (zero-copy, no conversion)
33
+ #
34
+ # Because the codes ARE the storage parent, every view-creating operation
35
+ # (slice / reshape / transpose / mask / …) carries the codes along the view
36
+ # chain automatically; only the label vocabulary is carried via copy_state.
37
+ #
38
+ # Construction:
39
+ # keys.categorize # discover levels (first-appearance order)
40
+ # keys.categorize(labels: set) # fixed vocabulary; off-set keys excluded
41
+ # CACategorical.from_codes(c, lab) # wrap already-dense codes + labels
42
+ # # (= the pandas / Arrow import receiver)
43
+ #
44
+ # ----------------------------------------------------------------------------
45
+
46
+ require "carray"
47
+
48
+ # Categorical column: dense integer codes plus a label vocabulary, so each
49
+ # element is an index into a small set of categories. Structurally the same
50
+ # idea as a pandas Categorical or an Arrow dictionary array.
51
+ #
52
+ # Implemented as a read-only non-numeric Face over the codes array — the
53
+ # storage is the integer codes, while the surface is `CA_FIXLEN` so numeric
54
+ # kernels are gated off (`cat + 1` raises; arithmetic on category codes is
55
+ # not meaningful).
56
+ class CACategorical < CAObject
57
+
58
+ # raw-byte unpack format per storage (codes) data_type, native endian.
59
+ # The FIXLEN surface delivers a per-cell fetch as an N-byte String; this
60
+ # decodes it back into the integer code.
61
+ UNPACK_FORMAT = {
62
+ CA_INT8 => "c", CA_UINT8 => "C",
63
+ CA_INT16 => "s", CA_UINT16 => "S",
64
+ CA_INT32 => "l", CA_UINT32 => "L",
65
+ CA_INT64 => "q", CA_UINT64 => "Q",
66
+ }.freeze
67
+
68
+ # The exclusion sentinel per codes data_type: the all-ones bit pattern, read
69
+ # as type-max for an unsigned dtype and as -1 for a signed one. Either way it
70
+ # is out of every valid [0, k) range and byte-identical to a pandas / Arrow
71
+ # missing code.
72
+ SENTINEL = {
73
+ CA_UINT8 => 0xFF, CA_INT8 => -1,
74
+ CA_UINT16 => 0xFFFF, CA_INT16 => -1,
75
+ CA_UINT32 => 0xFFFFFFFF, CA_INT32 => -1,
76
+ CA_UINT64 => 0xFFFFFFFFFFFFFFFF, CA_INT64 => -1,
77
+ }.freeze
78
+
79
+ class << self
80
+ # Wrap already-dense codes + labels with no discovery — the import receiver
81
+ # for a pandas Categorical or an Arrow dictionary. `codes` becomes the
82
+ # Face's storage parent verbatim (zero-copy when it is a wrapped memory
83
+ # view), and from_codes takes ownership of it.
84
+ #
85
+ # Excluded cells are identified by the all-ones sentinel value (type-max
86
+ # for unsigned codes, -1 for signed — both the pandas / Arrow missing code)
87
+ # and masked here, so the categorical is well-formed regardless of whether
88
+ # the caller pre-masked. Only the mask buffer is touched; the code bytes
89
+ # are left intact (so a pandas byte-reinterpret round-trips).
90
+ # @overload from_codes(codes, labels)
91
+ # Returns a {CACategorical} wrapping already-dense integer
92
+ # `codes` with the given `labels`, without discovery. `codes`
93
+ # becomes the Face's storage parent; excluded cells (identified
94
+ # by the type-max sentinel value) are masked automatically.
95
+ # @param codes [CArray] integer code storage.
96
+ # @param labels [Array, CArray] category vocabulary indexed by
97
+ # code.
98
+ # @return [CACategorical]
99
+ # @raise [ArgumentError] when `codes` is not an integer CArray.
100
+ def from_codes(codes, labels)
101
+ unless codes.is_a?(CArray) && SENTINEL.key?(codes.data_type)
102
+ got = codes.is_a?(CArray) ? codes.data_type : codes.class
103
+ raise ArgumentError, "from_codes: codes must be an integer CArray (got #{got})"
104
+ end
105
+ excluded = codes.eq(SENTINEL[codes.data_type])
106
+ if excluded.count(true) > 0
107
+ codes.mask = codes.has_mask? ? (codes.mask | excluded) : excluded
108
+ end
109
+ new(codes, labels)
110
+ end
111
+ end
112
+
113
+ # codes : integer CArray, the storage parent. Excluded cells are both
114
+ # masked AND store the type-max sentinel value (= the all-ones bit
115
+ # pattern, which is signed -1 byte-for-byte — the pandas / Arrow
116
+ # missing code). Because the Face is READONLY the two never desync,
117
+ # so consumers may rely on either: the mask (CArray-native) or the
118
+ # sentinel (axis-group's out-of-range skip, zero-copy export).
119
+ # labels: Array | CArray, the vocabulary; labels[code] = category.
120
+ # @overload initialize(codes, labels)
121
+ # Allocates a READONLY {CACategorical} Face whose storage is
122
+ # `codes` and whose vocabulary is `labels`. The label list is
123
+ # copied and frozen so codes always index a stable vocabulary.
124
+ # @param codes [CArray] integer code storage.
125
+ # @param labels [Array, CArray] category vocabulary.
126
+ def initialize(codes, labels)
127
+ # Own a frozen copy of the vocabulary: the categorical is READONLY and its
128
+ # codes index into labels, so the label list must not change under it. We
129
+ # copy first so a caller's array is never frozen as a side effect; the
130
+ # label objects themselves are left untouched (container-level freeze).
131
+ @labels = (labels.respond_to?(:to_a) ? labels.to_a : Array(labels)).dup.freeze
132
+ super(CA_FIXLEN, codes.dim,
133
+ bytes: codes.bytes,
134
+ storage: codes.data_type,
135
+ parent: codes,
136
+ read_only: true,
137
+ face: true)
138
+ # Mark the codes storage read-only so the READONLY guarantee holds at the
139
+ # root, not just on the Face. Without this the Face is read_only but its
140
+ # parent is writable, so `cat.codes[i] = x` silently mutates the categorical
141
+ # (and any grouping cache derived from it). We set the CA_FLAG_READ_ONLY flag
142
+ # rather than #freeze: freeze also freezes the Ruby object, which propagates
143
+ # through views/Faces (a reshape of frozen codes is frozen) and would block
144
+ # the grouping cache from memoising. The flag gives the same write protection
145
+ # (mutations raise) while keeping the object non-frozen. One-way: it takes
146
+ # ownership of `codes` (categorize / from_codes build or receive it, mask
147
+ # already derived above); a caller keeping a mutable array must pass `.copy`.
148
+ codes.set_read_only_flag
149
+ end
150
+
151
+ attr_reader :labels
152
+
153
+ # The raw integer codes (= the storage parent). On a derived view this is
154
+ # the correspondingly sliced/reshaped codes, since codes ride the chain.
155
+ # Excluded cells are masked and store the type-max sentinel; the same array
156
+ # serves the axis-group kernel (out-of-range skip) and the pandas / Arrow
157
+ # bridge (byte-reinterpret to signed -1) with no conversion.
158
+ # @overload codes
159
+ # Returns the raw integer code CArray backing `self`. Excluded
160
+ # cells are masked and store the type-max sentinel. The array is
161
+ # read-only — the categorical owns immutable codes, so
162
+ # `codes[i] = x` raises; use `codes.copy` for a mutable copy.
163
+ # @return [CArray]
164
+ def codes
165
+ parent
166
+ end
167
+
168
+ # Face hook: carry the vocabulary across lifted views (slice / reshape / …).
169
+ # The codes ride along automatically as the Face's parent.
170
+ def copy_state(src)
171
+ @labels = src.labels
172
+ end
173
+
174
+ # Face hook: the homogeneity gate for multi-parent constructions
175
+ # (CArray.promote_list / CArray.stack / anything that Face-lifts a list).
176
+ # A code only means anything against the vocabulary it was assigned from, so
177
+ # two categoricals may share one lifted Face only when they index the same
178
+ # labels in the same code order.
179
+ #
180
+ # Same labels in a *different* code order is refused rather than re-coded,
181
+ # for the same reason CATime refuses a unit mismatch it knows how to convert:
182
+ # this is a predicate consulted after the parents are assembled, with no
183
+ # channel to rewrite storage — and the codes are read-only by construction,
184
+ # so agreeing would mean silently materialising fresh codes inside what the
185
+ # caller asked for as a view. Build the shared vocabulary up front instead:
186
+ # `keys.categorize(labels: shared)`.
187
+ def face_state_compatible?(other)
188
+ other.is_a?(CACategorical) && @labels == other.labels
189
+ end
190
+
191
+ # Face hook: decode a per-cell code into its category label. An out-of-range
192
+ # code (e.g. an unmasked external sentinel) decodes to nil rather than a
193
+ # wrong category via Ruby negative indexing.
194
+ def storage_to_scalar(raw)
195
+ code = raw.is_a?(String) ? raw.unpack1(UNPACK_FORMAT.fetch(parent.data_type)) : raw
196
+ (code < 0 || code >= @labels.size) ? nil : @labels[code]
197
+ end
198
+
199
+ # ---- category-space operations (by label, not code) -------------------
200
+ #
201
+ # The element value of a categorical IS its category, so these specialise
202
+ # CArray's value operations into label space — the user never has to know
203
+ # the integer code. The raw codes stay reachable via #codes for code-space
204
+ # work (interop, ML features), but they are not the everyday surface.
205
+
206
+ # Boolean mask of cells whose category == label. Excluded cells stay UNDEF
207
+ # (their category is unknown); an unknown label yields an all-false mask.
208
+ # @overload eq(label)
209
+ # Returns a boolean CArray marking cells whose category equals
210
+ # `label`. Excluded cells stay masked; an unknown label yields
211
+ # an all-false result.
212
+ # @param label [Object] category to match.
213
+ # @return [CArray]
214
+ def eq(label)
215
+ codes.eq(@labels.index(label) || @labels.size)
216
+ end
217
+
218
+ # @overload ne(label)
219
+ # Returns the complement of {#eq}.
220
+ # @param label [Object] category to compare against.
221
+ # @return [CArray]
222
+ def ne(label)
223
+ codes.ne(@labels.index(label) || @labels.size)
224
+ end
225
+
226
+ # @overload count(label)
227
+ # Returns the number of cells whose category equals `label`
228
+ # (0 for an unknown label).
229
+ # @param label [Object] category to count.
230
+ # @return [Integer]
231
+ def count(label)
232
+ code = @labels.index(label)
233
+ code ? codes.count(code) : 0
234
+ end
235
+
236
+ # Per-category counts as a length-k array aligned to #labels. Trailing empty
237
+ # categories are kept as 0 (unlike codes.bincount, which truncates them), so
238
+ # `labels.zip(category_sizes.to_a)` always pairs up.
239
+ #
240
+ # Memoised: the codes are read-only (immutable storage), so the counts are a
241
+ # pure function of the categorical and stay valid for its lifetime. The same
242
+ # cached array backs {#reduceat_index} and {#sort_addr}, so a wide aggregate
243
+ # over the same categorical pays the count once. (Do not mutate the returned
244
+ # array — it is shared; take `.copy` for a scratch buffer.) A derived-view /
245
+ # composite categorical caches too: read-only rides from the codes as a flag,
246
+ # not a Ruby freeze, so the object stays non-frozen and the memo ivar sticks.
247
+ # @overload category_sizes
248
+ # Returns per-category counts as a CArray with one entry per {#labels}
249
+ # aligned to {#labels}. Trailing empty categories are kept as 0
250
+ # so `labels.zip(category_sizes.to_a)` always pairs up.
251
+ # @return [CArray]
252
+ def category_sizes
253
+ return @_category_sizes if @_category_sizes
254
+ bc = codes.bincount
255
+ out = CArray.new(bc.data_type, [@labels.size]) # new zero-fills
256
+ out[0...bc.elements] = bc if bc.elements > 0
257
+ @_category_sizes = out
258
+ out
259
+ end
260
+
261
+ # Back-compatible alias; {#category_sizes} is the canonical name (a histogram
262
+ # `bincount` reading is misleading for per-category cell counts).
263
+ alias bincount category_sizes
264
+
265
+ # ---- value-hash discovery family --------------------------------------
266
+ #
267
+ # The codes are storage and the labels are the values, so this family has to
268
+ # answer in label space. It cannot ride the family's Face gate: a categorical
269
+ # is not ORDERABLE (code order is the vocabulary's order, not the labels'),
270
+ # and lifting a code array back would still be codes. Without these overrides
271
+ # `unique` handed back the raw code bytes and `is_in` compared labels against
272
+ # codes, so it was false everywhere.
273
+ #
274
+ # Distinctness itself still rides the codes, where it is a uint8 pass; only the
275
+ # values crossing the surface are translated. {#count} and {#category_sizes}
276
+ # above are the same idea for a single label / the whole vocabulary.
277
+
278
+ # @overload unique(sort: false)
279
+ # Returns the labels that occur, in first-appearance order. A category
280
+ # with no cells is not included (use {#labels} for the vocabulary).
281
+ # @param sort [Boolean] when true, sort the labels ascending.
282
+ # @return [CArray] object CArray of labels.
283
+ def unique (sort: false)
284
+ u = labels_for(codes.unique)
285
+ sort ? u.sort : u
286
+ end
287
+
288
+ # @overload value_counts(sort: false)
289
+ # Returns `[labels, counts]` for the categories that occur. {#category_sizes}
290
+ # is the aligned-to-{#labels} counterpart, which keeps the empty ones.
291
+ # @param sort [false, :count, :value] pair ordering.
292
+ # @return [Array(CArray, CArray)]
293
+ def value_counts (sort: false)
294
+ unless [false, :count, :value].include?(sort)
295
+ raise ArgumentError, "value_counts: sort must be false, :count, or :value"
296
+ end
297
+ code_values, counts = codes.value_counts(sort: sort == :count ? :count : false)
298
+ values = labels_for(code_values)
299
+ return [values, counts] unless sort == :value
300
+ # Ascending *label*: code order is the vocabulary's, so reorder here.
301
+ la = values.to_a
302
+ order = (0...la.size).sort_by { |i| [la[i], i] }
303
+ idx = CArray.int64(order.size) { |i| order[i] }
304
+ [values[idx], counts[idx]]
305
+ end
306
+
307
+ # @overload mode(axis: nil)
308
+ # Returns the most frequent label(s). Runs in label space because the
309
+ # modal values come back sorted ascending, which for labels is not the
310
+ # code order.
311
+ # @return [CArray, Array<CArray>]
312
+ def mode (axis: nil)
313
+ label_values.mode(axis: axis)
314
+ end
315
+
316
+ # @overload is_in(values)
317
+ # Returns a boolean CArray, true where the cell's label is in `values`.
318
+ # An unknown label matches nothing. Masked cells stay masked.
319
+ # @param values [Array, CArray, CACategorical] labels to test against.
320
+ # @return [CArray]
321
+ def is_in (values)
322
+ wanted = label_list(values).filter_map { |l| @labels.index(l) }
323
+ codes.is_in(CArray.int64(wanted.size) { |i| wanted[i] })
324
+ end
325
+
326
+ # @overload intersection(other, sort: false)
327
+ # @return [CArray] object CArray of the labels present in both.
328
+ def intersection (other, sort: false)
329
+ unique.intersection(label_array(other), sort: sort)
330
+ end
331
+
332
+ # @overload difference(other, sort: false)
333
+ # @return [CArray] object CArray of the labels only `self` has.
334
+ def difference (other, sort: false)
335
+ unique.difference(label_array(other), sort: sort)
336
+ end
337
+
338
+ # @overload union(other, sort: false)
339
+ # @return [CArray] object CArray of the labels of either side.
340
+ def union (other, sort: false)
341
+ unique.union(label_array(other), sort: sort)
342
+ end
343
+
344
+ # @overload locate_addr(ref)
345
+ # @return [CArray] for each cell, its label's address in `ref` (UNDEF when
346
+ # the label does not occur there).
347
+ def locate_addr (ref)
348
+ label_values.locate_addr(label_array(ref))
349
+ end
350
+
351
+ private
352
+
353
+ # Labels for a code array (the discovery kernels skip masked cells, so the
354
+ # code arrays reaching here hold real codes only).
355
+ def labels_for (code_array)
356
+ CA_OBJECT(code_array.to_a.map { |c| @labels[c] })
357
+ end
358
+
359
+ # This categorical's cells as their labels; a masked cell stays masked.
360
+ def label_values
361
+ CA_OBJECT(to_a)
362
+ end
363
+
364
+ # An operand's labels as a plain Array (a masked cell comes through as UNDEF,
365
+ # which matches no label and so drops out of the set).
366
+ def label_list (other)
367
+ other.respond_to?(:to_a) ? other.to_a : Array(other)
368
+ end
369
+
370
+ def label_array (other)
371
+ CA_OBJECT(label_list(other))
372
+ end
373
+
374
+ public
375
+
376
+ # ---- reduceat / sort-based grouping foundation ------------------------
377
+ #
378
+ # The scatter path (axis_group) handles the monoid reductions (sum / mean /
379
+ # min / max / variance) in one pass. Order statistics (median / percentile)
380
+ # cannot be scattered: they need every value of a group held together. These
381
+ # two accessors are the building blocks for that other engine — lay the data
382
+ # out as category-contiguous blocks, then select per block.
383
+ #
384
+ # The grouping plan — sort_addr (the counting sort, the dominant cost) plus
385
+ # reduceat_index and category_sizes — depends only on the codes, which are
386
+ # read-only. It is therefore memoised (a pure function of the categorical): the
387
+ # first grouping access builds it and every later one reuses it. This is what
388
+ # lets a wide aggregate (many payload columns over one classifier) or a
389
+ # CACategoricalIterator run the counting sort once, not once per column /
390
+ # iterator. Only the plan is cached; the payload-dependent grouped copy
391
+ # (value.reshape(n)[sort_addr]) is rebuilt per column (it varies with value).
392
+
393
+ # Force-build (and cache) the whole grouping plan up front — sort_addr,
394
+ # reduceat_index, category_sizes — for prepare-ahead use before a batch of
395
+ # groupings. Lazy building already covers correctness; this is the explicit
396
+ # "pay the counting sort now" handle (e.g. right after `df.group_by(col)`).
397
+ # Returns self so it chains.
398
+ # @overload build_grouping
399
+ # Eagerly builds and caches the grouping plan (sort_addr / reduceat_index
400
+ # / category_sizes). Optional — the plan is built lazily on first grouping
401
+ # access — but useful to pay the counting sort once ahead of a batch.
402
+ # @return [self]
403
+ def build_grouping
404
+ sort_addr # pulls category_sizes; reduceat_index shares category_sizes
405
+ reduceat_index
406
+ self
407
+ end
408
+
409
+ # Flat addresses that gather self into category-contiguous order: every cell
410
+ # of category 0 first, then 1, ..., then k-1, with excluded cells last. Built
411
+ # by a counting sort over the codes (O(n + k), stable): the segment starts
412
+ # (an exclusive prefix scan of {#category_sizes}) drive a scatter that places
413
+ # each source index into its category's block in source order; excluded cells
414
+ # (masked, or code out of range 0...k) are appended at the tail in source
415
+ # order. The first {#category_sizes}.sum addresses are exactly the classified
416
+ # cells in category order — CACategoricalIterator's permutation. For an N-D
417
+ # categorical the addresses are into the raveled storage, so
418
+ # `value.reshape(elements)[cat.sort_addr]` produces the contiguous blocks.
419
+ # Memoised (see the grouping-plan note above): the counting sort is the
420
+ # dominant grouping cost, so it is computed once and shared by every payload
421
+ # column and by CACategoricalIterator. Do not mutate the returned array.
422
+ # @overload sort_addr
423
+ # Returns a length-{#elements} integer CArray of flat storage
424
+ # addresses that order the cells by category (excluded cells last).
425
+ # @return [CArray]
426
+ def sort_addr
427
+ return @_sort_addr if @_sort_addr
428
+ n = elements
429
+ k = @labels.size
430
+ cs = category_sizes # per-category counts
431
+ nvalid = cs.sum
432
+ cur = CArray.int64(k > 0 ? k : 0) # segment starts, consumed as cursor
433
+ cur[1..-1] = cs.cumsum.int64[0..-2] if k > 1
434
+ flat = codes.reshape(n)
435
+ seq = CArray.int64(n).seq! # source indices, scattered as payload
436
+ out = CArray.int64(n)
437
+ if nvalid == n
438
+ flat.send(:__categorical_scatter__, seq, cur, out, k) # all valid: scatter straight in
439
+ else
440
+ if nvalid > 0
441
+ valid = CArray.int64(nvalid) # scatter target must be an entity, not a view
442
+ flat.send(:__categorical_scatter__, seq, cur, valid, k)
443
+ out[0...nvalid] = valid
444
+ end
445
+ # Excluded = every cell the scatter skips: code out of [0, k), OR masked
446
+ # (a from_codes pre-masked cell keeps a valid code but is excluded). Both
447
+ # must be caught or the tail slot count would not add up to n - nvalid.
448
+ excluded = flat.value.ge(k).or(flat.value.lt(0))
449
+ excluded = excluded.or(flat.is_masked) if flat.has_mask?
450
+ out[nvalid..-1] = seq[excluded] # excluded cells, source order
451
+ end
452
+ @_sort_addr = out
453
+ out
454
+ end
455
+
456
+ # Segment start offsets into the category-contiguous layout produced by
457
+ # {#sort_addr}: reduceat_index[c] is where category c's block begins, so
458
+ # block c spans `reduceat_index[c] ... reduceat_index[c] + category_sizes[c]`.
459
+ # Empty categories repeat the following start (a zero-width block). Length k,
460
+ # aligned to #labels; pair with {#category_sizes} for the block lengths. On
461
+ # data already laid out in category order the offsets index it directly.
462
+ # @overload reduceat_index
463
+ # Returns an int64 CArray, one entry per {#labels}, of segment start
464
+ # offsets aligned to {#labels}. Pair with {#category_sizes} for lengths.
465
+ # @return [CArray]
466
+ def reduceat_index
467
+ return @_reduceat_index if @_reduceat_index
468
+ k = @labels.size
469
+ out = CArray.int64(k > 0 ? k : 0)
470
+ if k > 1
471
+ out[0] = 0
472
+ out[1..-1] = category_sizes.cumsum.int64[0..-2]
473
+ elsif k == 1
474
+ out[0] = 0
475
+ end
476
+ @_reduceat_index = out
477
+ out
478
+ end
479
+
480
+ # @overload inspect
481
+ # Returns a short summary showing element count, category count,
482
+ # and vocabulary.
483
+ # @return [String]
484
+ def inspect
485
+ "#<CACategorical n=#{elements} categories=#{@labels.size} labels=#{@labels.inspect}>"
486
+ end
487
+ end
488
+
489
+
490
+ class CArray
491
+ # Build a CACategorical from self read as category keys (= the values whose
492
+ # distinct levels become the categories). Codes are dense 0-based in the
493
+ # order labels appear (first-appearance by default, or ascending sorted
494
+ # when `sort_labels: true`); masked keys become masked (excluded) codes.
495
+ #
496
+ # labels: nil -> discover, first-appearance order
497
+ # labels: nil, sort_labels: true -> discover, then sort ascending
498
+ # labels: set -> fixed vocabulary (must be unique);
499
+ # keys outside it are excluded (masked)
500
+ # @overload categorize(labels: nil, sort_labels: false)
501
+ # Returns a {CACategorical} built from `self` read as category
502
+ # keys. With `labels: nil` distinct levels are discovered in
503
+ # first-appearance order (or ascending sorted when
504
+ # `sort_labels: true`); with an explicit `labels` list the
505
+ # vocabulary is fixed and keys outside it become masked
506
+ # (excluded). `sort_labels:` is ignored when an explicit `labels`
507
+ # is given (the caller has already chosen the order).
508
+ # @param labels [Array, CArray, nil] fixed vocabulary; `nil`
509
+ # enables discovery.
510
+ # @param sort_labels [Boolean] when discovering (labels: nil), sort
511
+ # the discovered vocabulary ascending after collecting it.
512
+ # @return [CACategorical]
513
+ # @raise [ArgumentError] when explicit `labels` contain
514
+ # duplicates.
515
+ def categorize(labels: nil, sort_labels: false)
516
+ # Automatic appearance-order vocabulary: one linear pass (C
517
+ # __factorize_appearance__) returns both codes and levels directly, over the
518
+ # integer / float / object / fixlen / boolean lanes (boolean rides the uint8
519
+ # lane). Distinctness is the hash-key judgement shared with the discovery
520
+ # family: Float NaN collapses to one category and -0.0 == +0.0, while mixed
521
+ # Integer / Float keys stay distinct (eql?, so 1 and 1.0 are separate
522
+ # categories). The discovery path below is reserved for sort_labels (which
523
+ # reorders the vocabulary, desyncing the appearance-order codes), an explicit
524
+ # labels list, and the dtypes the factorize kernel does not take (complex).
525
+ if labels.nil? && !sort_labels && (integer? || float? || object? || fixlen? || boolean?)
526
+ codes, levels = __factorize_appearance__
527
+ return CACategorical.from_codes(codes, levels.to_a)
528
+ end
529
+
530
+ if labels.nil?
531
+ # Discover the levels in first-appearance order: mask_duplicates keeps the
532
+ # first occurrence of each distinct value and masks the rest (already-
533
+ # masked keys stay excluded), so the non-masked cells are the levels.
534
+ # Only the final list is Ruby, since labels are Ruby objects.
535
+ labels_arr = mask_duplicates[:is_not_masked].to_a
536
+ labels_arr.sort! if sort_labels
537
+ else
538
+ labels_arr = labels.respond_to?(:to_a) ? labels.to_a : Array(labels)
539
+ if labels_arr.uniq.size != labels_arr.size
540
+ raise ArgumentError, "categorize: labels: must be unique (got duplicates)"
541
+ end
542
+ end
543
+
544
+ # Choose a narrow unsigned code dtype, reserving its top value as the
545
+ # exclusion sentinel so it never collides with a real code 0..k-1.
546
+ k = labels_arr.size
547
+ code_type, sentinel =
548
+ if k <= 0xFF then [CA_UINT8, 0xFF]
549
+ elsif k <= 0xFFFF then [CA_UINT16, 0xFFFF]
550
+ else [CA_UINT32, 0xFFFFFFFF]
551
+ end
552
+
553
+ # One vectorized masked write per category. Cells matching no category
554
+ # (out-of-vocabulary) and masked cells (eq yields UNDEF, skipped) keep the
555
+ # sentinel; from_codes then derives the mask from it.
556
+ codes = CArray.new(code_type, shape).fill(sentinel)
557
+ labels_arr.each_with_index { |label, c| codes[eq(label)] = c }
558
+
559
+ CACategorical.from_codes(codes, labels_arr)
560
+ end
561
+ end