carray 3.0.2 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +568 -0
  3. data/README.md +6 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_descriptor.h +27 -24
  6. data/ext/ca_axis_dispatch.c +220 -0
  7. data/ext/ca_axis_group.c +201 -123
  8. data/ext/ca_binop_dispatch.c +46 -52
  9. data/ext/ca_binop_dispatch.h +1 -1
  10. data/ext/ca_categorical_iterator.c +249 -183
  11. data/ext/ca_compare.c +7 -12
  12. data/ext/ca_for_buffer.h +21 -68
  13. data/ext/ca_group_iter.c +38 -23
  14. data/ext/ca_kernel_iterator.c +1765 -2076
  15. data/ext/ca_kernel_iterator.h +584 -566
  16. data/ext/ca_monop_dispatch.c +86 -63
  17. data/ext/ca_monop_dispatch.h +5 -0
  18. data/ext/ca_obj_array.c +223 -56
  19. data/ext/ca_obj_bincmp.c +172 -138
  20. data/ext/ca_obj_binop.c +116 -230
  21. data/ext/ca_obj_bitarray.c +40 -26
  22. data/ext/ca_obj_bitfield.c +100 -68
  23. data/ext/ca_obj_block.c +19 -9
  24. data/ext/ca_obj_byte_swap.c +45 -46
  25. data/ext/ca_obj_const_string.c +269 -44
  26. data/ext/ca_obj_face.c +612 -89
  27. data/ext/ca_obj_face.h +111 -47
  28. data/ext/ca_obj_fake.c +36 -79
  29. data/ext/ca_obj_farray.c +7 -3
  30. data/ext/ca_obj_field.c +33 -8
  31. data/ext/ca_obj_fixlen_string.c +23 -24
  32. data/ext/ca_obj_grid.c +541 -210
  33. data/ext/ca_obj_meld.c +331 -111
  34. data/ext/ca_obj_moncmp.c +80 -65
  35. data/ext/ca_obj_monop.c +177 -149
  36. data/ext/ca_obj_object.c +664 -248
  37. data/ext/ca_obj_record.c +63 -23
  38. data/ext/ca_obj_reduce.c +18 -16
  39. data/ext/ca_obj_refer.c +140 -58
  40. data/ext/ca_obj_remap.c +85 -24
  41. data/ext/ca_obj_repeat.c +24 -9
  42. data/ext/ca_obj_roll.c +121 -16
  43. data/ext/ca_obj_select.c +61 -58
  44. data/ext/ca_obj_select_axis.c +73 -107
  45. data/ext/ca_obj_shift.c +32 -102
  46. data/ext/ca_obj_stack.c +372 -122
  47. data/ext/ca_obj_stride.c +348 -140
  48. data/ext/ca_obj_string.c +88 -22
  49. data/ext/ca_obj_tile.c +38 -21
  50. data/ext/ca_obj_time.c +45 -31
  51. data/ext/ca_obj_timedelta.c +43 -29
  52. data/ext/ca_obj_transpose.c +17 -6
  53. data/ext/ca_obj_triop.c +125 -141
  54. data/ext/ca_obj_window.c +330 -44
  55. data/ext/ca_op_ipower.c +24 -11
  56. data/ext/ca_op_powi.h +57 -30
  57. data/ext/ca_pow5_table.h +656 -0
  58. data/ext/ca_sort_kernels.h +70 -0
  59. data/ext/ca_sweep_engine.c +104 -81
  60. data/ext/ca_sweep_engine.h +7 -20
  61. data/ext/ca_triop_dispatch.c +4 -1
  62. data/ext/ca_triop_dispatch.h +5 -2
  63. data/ext/caframe_csv_reader.c +542 -0
  64. data/ext/carray.h +241 -92
  65. data/ext/carray_access.c +608 -276
  66. data/ext/carray_address_basis.c +494 -97
  67. data/ext/carray_attribute.c +129 -4
  68. data/ext/carray_bincount.c +39 -7
  69. data/ext/carray_broadcast.c +56 -19
  70. data/ext/carray_build_flags.h +1 -1
  71. data/ext/carray_call_cfunc.c +108 -28
  72. data/ext/carray_cast.c +1576 -223
  73. data/ext/carray_cast_func.rb +77 -24
  74. data/ext/carray_class.c +4 -3
  75. data/ext/carray_conversion.c +208 -75
  76. data/ext/carray_copy.c +82 -24
  77. data/ext/carray_core.c +646 -162
  78. data/ext/carray_count.c +171 -88
  79. data/ext/carray_element.c +94 -60
  80. data/ext/carray_factorize.c +670 -324
  81. data/ext/carray_generate.c +260 -83
  82. data/ext/carray_histogram.c +209 -106
  83. data/ext/carray_hold.c +14 -16
  84. data/ext/carray_index_classifier.c +422 -243
  85. data/ext/carray_internal.h +692 -3
  86. data/ext/carray_kernels_bincmp.c +7 -0
  87. data/ext/carray_kernels_binop.c +487 -120
  88. data/ext/carray_kernels_init.c +653 -50
  89. data/ext/carray_kernels_moncmp.c +15 -8
  90. data/ext/carray_kernels_monop.c +99 -43
  91. data/ext/carray_kernels_reduce_aggregate.c +3925 -6112
  92. data/ext/carray_kernels_reduce_boolean.c +112 -36
  93. data/ext/carray_kernels_reduce_cumulative.c +2127 -6903
  94. data/ext/carray_kernels_reduce_extreme.c +3032 -2892
  95. data/ext/carray_kernels_reduce_variance.c +439 -500
  96. data/ext/carray_kernels_scan.c +1097 -488
  97. data/ext/carray_kernels_search.c +3787 -2176
  98. data/ext/carray_kernels_sort.c +1200 -920
  99. data/ext/carray_kernels_triop.c +479 -7
  100. data/ext/carray_key_windows.c +216 -0
  101. data/ext/carray_lazy.c +967 -153
  102. data/ext/carray_loop.c +81 -65
  103. data/ext/carray_mask.c +910 -324
  104. data/ext/carray_math_kernel.h +1 -0
  105. data/ext/carray_median_percentile.c +299 -100
  106. data/ext/carray_memory_view.c +872 -758
  107. data/ext/carray_operator.c +1171 -364
  108. data/ext/carray_order.c +505 -133
  109. data/ext/carray_partition.c +112 -65
  110. data/ext/carray_random.c +327 -239
  111. data/ext/carray_round.c +273 -0
  112. data/ext/carray_scatter.c +346 -78
  113. data/ext/carray_segment.c +257 -0
  114. data/ext/carray_slab.c +249 -86
  115. data/ext/carray_sort.c +224 -227
  116. data/ext/carray_sort_kernel.c +8 -1
  117. data/ext/carray_struct.c +118 -39
  118. data/ext/carray_test.c +193 -69
  119. data/ext/carray_time_text.c +1182 -0
  120. data/ext/carray_undef.c +4 -0
  121. data/ext/carray_utils.c +579 -113
  122. data/ext/extconf.rb +27 -2
  123. data/ext/mk_call_cfunc.rb +19 -4
  124. data/ext/mk_pow5_table.rb +35 -0
  125. data/ext/mkkernel.rb +1202 -1014
  126. data/ext/ruby_carray.c +17 -5
  127. data/ext/version.h +4 -4
  128. data/ext/version.rb +4 -2
  129. data/lib/carray/attribute.rb +160 -17
  130. data/lib/carray/autoload_carray.rb +23 -27
  131. data/lib/carray/autoload_method_extension.rb +20 -5
  132. data/lib/carray/axis_group.rb +360 -63
  133. data/lib/carray/basics.rb +162 -28
  134. data/lib/carray/bincount_nd.rb +18 -2
  135. data/lib/carray/block_iterator.rb +231 -58
  136. data/lib/carray/boolean_reduce.rb +34 -42
  137. data/lib/carray/categorical.rb +242 -52
  138. data/lib/carray/categorical_iterator.rb +437 -827
  139. data/lib/carray/complex.rb +41 -34
  140. data/lib/carray/conditional.rb +150 -14
  141. data/lib/carray/const_string.rb +59 -4
  142. data/lib/carray/construct.rb +15 -11
  143. data/lib/carray/data_type_extension.rb +1 -0
  144. data/lib/carray/distance_limit.rb +77 -0
  145. data/lib/carray/fixlen_string.rb +7 -1
  146. data/lib/carray/frame/column_verbs.rb +70 -0
  147. data/lib/carray/frame/concat.rb +157 -6
  148. data/lib/carray/frame/convert.rb +82 -7
  149. data/lib/carray/frame/csv_parser.rb +688 -87
  150. data/lib/carray/frame/describe.rb +80 -0
  151. data/lib/carray/frame/frame.rb +369 -45
  152. data/lib/carray/frame/group.rb +631 -90
  153. data/lib/carray/frame/io.rb +506 -79
  154. data/lib/carray/frame/join.rb +411 -62
  155. data/lib/carray/frame/records.rb +116 -19
  156. data/lib/carray/frame/reshape.rb +455 -0
  157. data/lib/carray/frame/sort.rb +30 -18
  158. data/lib/carray/frame/verbs.rb +868 -61
  159. data/lib/carray/frame/windows.rb +117 -0
  160. data/lib/carray/frame.rb +4 -0
  161. data/lib/carray/fuse_source.rb +118 -2
  162. data/lib/carray/fusion.rb +257 -42
  163. data/lib/carray/histogram.rb +6 -1
  164. data/lib/carray/inspect.rb +54 -13
  165. data/lib/carray/iterator.rb +99 -9
  166. data/lib/carray/key_windows.rb +620 -0
  167. data/lib/carray/lazy.rb +216 -193
  168. data/lib/carray/mask_gap_fill.rb +112 -92
  169. data/lib/carray/math.rb +32 -19
  170. data/lib/carray/meld_reduce.rb +52 -21
  171. data/lib/carray/methods/align_addr.rb +8 -4
  172. data/lib/carray/methods/bin.rb +26 -18
  173. data/lib/carray/methods/bincount.rb +18 -2
  174. data/lib/carray/methods/bit_string.rb +15 -4
  175. data/lib/carray/methods/choose.rb +16 -3
  176. data/lib/carray/methods/composition.rb +18 -10
  177. data/lib/carray/methods/factorize.rb +2 -2
  178. data/lib/carray/methods/gather_nd.rb +25 -5
  179. data/lib/carray/methods/insert_block.rb +9 -14
  180. data/lib/carray/methods/is_between.rb +59 -0
  181. data/lib/carray/methods/is_in.rb +140 -10
  182. data/lib/carray/methods/join.rb +2 -7
  183. data/lib/carray/methods/lag.rb +49 -0
  184. data/lib/carray/methods/locate_addr.rb +182 -45
  185. data/lib/carray/methods/mode.rb +18 -1
  186. data/lib/carray/methods/nunique.rb +11 -6
  187. data/lib/carray/methods/repeat.rb +1 -20
  188. data/lib/carray/methods/resize.rb +21 -23
  189. data/lib/carray/methods/snap.rb +34 -12
  190. data/lib/carray/methods/unique.rb +11 -2
  191. data/lib/carray/methods/value_counts.rb +4 -11
  192. data/lib/carray/record_order.rb +208 -0
  193. data/lib/carray/rng.rb +3 -0
  194. data/lib/carray/runtime.rb +65 -17
  195. data/lib/carray/segment_iterator.rb +1221 -0
  196. data/lib/carray/serialize.rb +214 -30
  197. data/lib/carray/slab_iterator.rb +101 -27
  198. data/lib/carray/stack.rb +10 -4
  199. data/lib/carray/string.rb +105 -5
  200. data/lib/carray/string_operation_extension.rb +12 -4
  201. data/lib/carray/struct.rb +137 -58
  202. data/lib/carray/struct_builder.rb +124 -17
  203. data/lib/carray/time.rb +1222 -186
  204. data/lib/carray/window_iterator.rb +242 -81
  205. data/lib/carray.rb +9 -2
  206. data/yard-stubs/ca_obj_array.rb +4 -4
  207. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  208. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  209. data/yard-stubs/ca_obj_fake.rb +1 -1
  210. data/yard-stubs/ca_obj_field.rb +5 -1
  211. data/yard-stubs/ca_obj_grid.rb +21 -10
  212. data/yard-stubs/ca_obj_refer.rb +5 -4
  213. data/yard-stubs/ca_obj_roll.rb +2 -3
  214. data/yard-stubs/ca_obj_shift.rb +5 -1
  215. data/yard-stubs/ca_obj_stride.rb +10 -7
  216. data/yard-stubs/ca_obj_tile.rb +1 -1
  217. data/yard-stubs/ca_obj_transpose.rb +1 -1
  218. data/yard-stubs/ca_obj_window.rb +9 -5
  219. data/yard-stubs/carray_access.rb +11 -5
  220. data/yard-stubs/carray_attribute.rb +0 -5
  221. data/yard-stubs/carray_broadcast.rb +1 -1
  222. data/yard-stubs/carray_cast.rb +88 -43
  223. data/yard-stubs/carray_conversion.rb +15 -2
  224. data/yard-stubs/carray_copy.rb +12 -5
  225. data/yard-stubs/carray_core.rb +3 -3
  226. data/yard-stubs/carray_count.rb +9 -0
  227. data/yard-stubs/carray_element.rb +10 -4
  228. data/yard-stubs/carray_generate.rb +21 -1
  229. data/yard-stubs/carray_lazy.rb +5 -1
  230. data/yard-stubs/carray_loop.rb +4 -1
  231. data/yard-stubs/carray_mask.rb +60 -12
  232. data/yard-stubs/carray_math.rb +52 -14
  233. data/yard-stubs/carray_median_percentile.rb +15 -8
  234. data/yard-stubs/carray_memory_view.rb +40 -23
  235. data/yard-stubs/carray_order.rb +61 -29
  236. data/yard-stubs/carray_random.rb +13 -4
  237. data/yard-stubs/carray_scatter.rb +21 -9
  238. data/yard-stubs/carray_segment.rb +72 -0
  239. data/yard-stubs/carray_slab.rb +10 -2
  240. data/yard-stubs/carray_sort.rb +49 -8
  241. data/yard-stubs/carray_test.rb +15 -3
  242. data/yard-stubs/carray_utils.rb +0 -26
  243. metadata +19 -5
  244. data/ext/ca_for_each_element.h +0 -239
  245. data/ext/carray_kernels_map.c +0 -3467
  246. data/lib/carray/arrow_tensor.rb +0 -401
  247. data/lib/carray/table.rb +0 -108
@@ -48,6 +48,7 @@
48
48
  # ----------------------------------------------------------------------------
49
49
 
50
50
  require "carray"
51
+ require "carray/methods/is_in" # refuse_range_set, for the set-valued verbs
51
52
 
52
53
  # Categorical column: dense integer codes plus a label vocabulary, so each
53
54
  # element is an index into a small set of categories. Structurally the same
@@ -68,6 +69,7 @@ class CACategorical < CAObject
68
69
  CA_INT32 => "l", CA_UINT32 => "L",
69
70
  CA_INT64 => "q", CA_UINT64 => "Q",
70
71
  }.freeze
72
+ Ractor.make_shareable(UNPACK_FORMAT)
71
73
 
72
74
  # The exclusion sentinel per codes data_type: the all-ones bit pattern, read
73
75
  # as type-max for an unsigned data type and as -1 for a signed one. Either way it
@@ -214,6 +216,34 @@ class CACategorical < CAObject
214
216
  end
215
217
  end
216
218
 
219
+ # The rows' categorical for key columns of one length (CAFrame#group_by,
220
+ # #is_duplicate, windows(on:, by:)). Each column is categorized on its
221
+ # own, so a component compares as a single key does (NaN is one value,
222
+ # -0.0 is 0.0, a masked cell matches nothing); with several columns a
223
+ # row's category is the tuple of its component codes, labelled by the
224
+ # tuple of the component labels. One masked component leaves the row in
225
+ # no category, as a masked single key does.
226
+ private_class_method def self.from_keys (cols)
227
+ return cols.first.categorize if cols.size == 1
228
+ nrow = cols.first.elements
229
+ cats = cols.map(&:categorize)
230
+ sizes = cats.map { |c| c.labels.size }
231
+ tuple = if sizes.inject(1, :*) <= 2**62
232
+ cats.drop(1).inject(cats[0].codes.int64) { |acc, c| acc * c.labels.size + c.codes.int64 }
233
+ else
234
+ codes = cats.map(&:codes)
235
+ t = CArray.object(nrow) { |i| codes.map { |c| c[i] } }
236
+ nrow.times { |i| t[i] = UNDEF if t[i].any? { |x| UNDEF.equal?(x) } }
237
+ t
238
+ end
239
+ rows = tuple.categorize
240
+ labels = rows.labels.map do |code|
241
+ parts = code.is_a?(Array) ? code : sizes.reverse.map { |n| code, d = code.divmod(n); d }.reverse
242
+ parts.each_with_index.map { |d, j| cats[j].labels[d] }
243
+ end
244
+ from_codes(rows.codes, labels)
245
+ end
246
+
217
247
  # codes : integer CArray, the storage parent, already normalised by the
218
248
  # caller (categorize builds it that way; from_codes rewrites what it
219
249
  # receives). Excluded cells are both masked AND store the type-max
@@ -235,7 +265,7 @@ class CACategorical < CAObject
235
265
  # copy first so a caller's array is never frozen as a side effect; the
236
266
  # label objects themselves are left untouched (container-level freeze).
237
267
  @labels = (labels.respond_to?(:to_a) ? labels.to_a : Array(labels)).dup.freeze
238
- super(CA_FIXLEN, codes.dim,
268
+ super(CA_FIXLEN, codes.shape,
239
269
  bytes: codes.bytes,
240
270
  storage: codes.data_type,
241
271
  parent: codes,
@@ -297,7 +327,7 @@ class CACategorical < CAObject
297
327
 
298
328
  # Face hook: decode a per-cell code into its category label, through the one
299
329
  # decode every code-to-label path shares (see #label_at).
300
- def storage_to_scalar(raw)
330
+ def storage_to_element(raw)
301
331
  code = raw.is_a?(String) ? raw.unpack1(UNPACK_FORMAT.fetch(parent.data_type)) : raw
302
332
  label_at(code)
303
333
  end
@@ -309,24 +339,49 @@ class CACategorical < CAObject
309
339
  # the integer code. The raw codes stay reachable via #codes for code-space
310
340
  # work (interop, ML features), but they are not the everyday surface.
311
341
 
342
+ # @overload resize(*newdim, fill_value: UNDEF)
343
+ # Returns a categorical resized to `newdim` (see {CArray#resize}).
344
+ # The new cells are excluded (UNDEF) unless `fill_value` names one of
345
+ # the labels, since a zero code would read as the first label.
346
+ # @raise [ArgumentError] when `fill_value` is not a label.
347
+ def resize (*newdim, fill_value: UNDEF)
348
+ CACategorical.from_codes(codes.resize(*newdim, fill_value: code_for_fill(fill_value)),
349
+ labels)
350
+ end
351
+
352
+ # @overload insert_block(offset, bsize, &block)
353
+ # Returns a categorical with a block inserted (see
354
+ # {CArray#insert_block}). The inserted cells are excluded (UNDEF)
355
+ # unless the block returns one of the labels.
356
+ # @raise [ArgumentError] when the block returns something that is not
357
+ # a label.
358
+ def insert_block (offset, bsize, &block)
359
+ fill = block ? code_for_fill(block.call) : UNDEF
360
+ CACategorical.from_codes(codes.insert_block(offset, bsize) { fill }, labels)
361
+ end
362
+
312
363
  # Boolean mask of cells whose category == label. Excluded cells stay UNDEF
313
364
  # (their category is unknown); an unknown label yields an all-false mask.
314
365
  # @overload eq(label)
315
366
  # Returns a boolean CArray marking cells whose category equals
316
- # `label`. Excluded cells stay masked; an unknown label yields
317
- # an all-false result.
318
- # @param label [Object] category to match.
367
+ # `label`. A CArray is compared cell by cell, by label: another
368
+ # categorical is matched whatever order its labels are in, and a
369
+ # masked cell on either side gives UNDEF. Anything else, a Ruby Array
370
+ # included, is one label, as for {CArray#eq}. An unknown label matches
371
+ # nothing.
372
+ # @param label [Object, CArray] category to match, or one per cell.
319
373
  # @return [CArray]
320
374
  def eq(label)
321
- codes.eq(@labels.index(label) || @labels.size)
375
+ codes.eq(codes_of(label))
322
376
  end
323
377
 
324
378
  # @overload ne(label)
325
379
  # Returns the complement of {#eq}.
326
- # @param label [Object] category to compare against.
380
+ # @param label [Object, CArray] category to compare against, or one per
381
+ # cell.
327
382
  # @return [CArray]
328
383
  def ne(label)
329
- codes.ne(@labels.index(label) || @labels.size)
384
+ codes.ne(codes_of(label))
330
385
  end
331
386
 
332
387
  # @overload count(label)
@@ -356,11 +411,11 @@ class CACategorical < CAObject
356
411
  # so `labels.zip(category_sizes.to_a)` always pairs up.
357
412
  # @return [CArray]
358
413
  def category_sizes
359
- return @_category_sizes if @_category_sizes
414
+ return @category_sizes if @category_sizes
360
415
  bc = codes.bincount
361
- out = CArray.new(bc.data_type, [@labels.size]) # new zero-fills
416
+ out = CArray.new(bc.data_type, [@labels.size], bytes: bc.bytes) # new zero-fills
362
417
  out[0...bc.elements] = bc if bc.elements > 0
363
- @_category_sizes = out
418
+ @category_sizes = out
364
419
  out
365
420
  end
366
421
 
@@ -425,6 +480,7 @@ class CACategorical < CAObject
425
480
  # @param values [Array, CArray, CACategorical] labels to test against.
426
481
  # @return [CArray]
427
482
  def is_in (values)
483
+ refuse_range_set(values, "is_in")
428
484
  wanted = label_list(values).filter_map { |l| @labels.index(l) }
429
485
  codes.is_in(CArray.int64(wanted.size) { |i| wanted[i] })
430
486
  end
@@ -432,18 +488,21 @@ class CACategorical < CAObject
432
488
  # @overload intersection(other, sort: false)
433
489
  # @return [CArray] object CArray of the labels present in both.
434
490
  def intersection (other, sort: false)
491
+ refuse_range_set(other, "intersection")
435
492
  unique.intersection(label_array(other), sort: sort)
436
493
  end
437
494
 
438
495
  # @overload difference(other, sort: false)
439
496
  # @return [CArray] object CArray of the labels only `self` has.
440
497
  def difference (other, sort: false)
498
+ refuse_range_set(other, "difference")
441
499
  unique.difference(label_array(other), sort: sort)
442
500
  end
443
501
 
444
502
  # @overload union(other, sort: false)
445
503
  # @return [CArray] object CArray of the labels of either side.
446
504
  def union (other, sort: false)
505
+ refuse_range_set(other, "union")
447
506
  unique.union(label_array(other), sort: sort)
448
507
  end
449
508
 
@@ -463,6 +522,32 @@ class CACategorical < CAObject
463
522
  # that has none. Every code-to-label path goes through here rather than
464
523
  # indexing @labels itself, because two decodes are two chances to disagree,
465
524
  # and disagreeing is how #unique came to contradict #to_a on the same cell.
525
+ # An operand of #eq / #ne as codes of this vocabulary. A value that is not
526
+ # one of the labels gets labels.size, which no cell holds. A CArray is read
527
+ # cell by cell, a masked cell staying masked; a categorical goes through its
528
+ # own codes, so the two vocabularies need not agree. Anything else is one
529
+ # label, as CArray#eq takes it.
530
+ def codes_of (other)
531
+ return @labels.index(other) || @labels.size unless other.is_a?(CArray)
532
+ if other.is_a?(CACategorical)
533
+ table = other.labels.map { |l| @labels.index(l) || @labels.size }
534
+ return CA_INT64(table)[other.codes] unless table.empty?
535
+ end
536
+ memo = {}
537
+ list = other.flatten.to_a.map { |v|
538
+ UNDEF.equal?(v) ? v : memo.fetch(v) { memo[v] = @labels.index(v) || @labels.size }
539
+ }
540
+ CA_INT64(list).reshape(*other.shape)
541
+ end
542
+
543
+ # The code a fill value stands for: UNDEF stays UNDEF, a label is its code.
544
+ def code_for_fill (value)
545
+ return UNDEF if UNDEF.equal?(value)
546
+ code = labels.index(value)
547
+ raise ArgumentError, "#{value.inspect} is not one of the labels #{labels.inspect}" unless code
548
+ code
549
+ end
550
+
466
551
  def label_at (code)
467
552
  (code < 0 || code >= @labels.size) ? nil : @labels[code]
468
553
  end
@@ -535,38 +620,18 @@ class CACategorical < CAObject
535
620
  # `value.reshape(elements)[cat.sort_addr]` produces the contiguous blocks.
536
621
  # Memoised (see the grouping-plan note above): the counting sort is the
537
622
  # dominant grouping cost, so it is computed once and shared by every payload
538
- # column and by CACategoricalIterator. Do not mutate the returned array.
623
+ # column and by CACategoricalIterator, and kept for the categorical's
624
+ # lifetime (an int64 per cell). Do not mutate the returned array.
539
625
  # @overload sort_addr
540
626
  # Returns a length-{#elements} integer CArray of flat storage
541
627
  # addresses that order the cells by category (excluded cells last).
542
628
  # @return [CArray]
543
629
  def sort_addr
544
- return @_sort_addr if @_sort_addr
545
- n = elements
546
- k = @labels.size
547
- cs = category_sizes # per-category counts
548
- nvalid = cs.sum
549
- cur = CArray.int64(k > 0 ? k : 0) # segment starts, consumed as cursor
550
- cur[1..-1] = cs.cumsum.int64[0..-2] if k > 1
551
- flat = codes.reshape(n)
552
- seq = CArray.int64(n).seq! # source indices, scattered as payload
553
- out = CArray.int64(n)
554
- if nvalid == n
555
- flat.send(:__categorical_scatter__, seq, cur, out, k) # all valid: scatter straight in
556
- else
557
- if nvalid > 0
558
- valid = CArray.int64(nvalid) # scatter target must be an entity, not a view
559
- flat.send(:__categorical_scatter__, seq, cur, valid, k)
560
- out[0...nvalid] = valid
561
- end
562
- # Excluded = every cell the scatter skips: code out of [0, k), OR masked
563
- # (a from_codes pre-masked cell keeps a valid code but is excluded). Both
564
- # must be caught or the tail slot count would not add up to n - nvalid.
565
- excluded = flat.value.ge(k).or(flat.value.lt(0))
566
- excluded = excluded.or(flat.is_masked) if flat.has_mask?
567
- out[nvalid..-1] = seq[excluded] # excluded cells, source order
568
- end
569
- @_sort_addr = out
630
+ return @sort_addr if @sort_addr
631
+ out = codes.reshape(elements).__send__(:addresses_by_category,
632
+ reduceat_index.copy, @labels.size,
633
+ category_sizes.sum)
634
+ @sort_addr = out
570
635
  out
571
636
  end
572
637
 
@@ -581,16 +646,94 @@ class CACategorical < CAObject
581
646
  # offsets aligned to {#labels}. Pair with {#category_sizes} for lengths.
582
647
  # @return [CArray]
583
648
  def reduceat_index
584
- return @_reduceat_index if @_reduceat_index
649
+ return @reduceat_index if @reduceat_index
650
+ # Every boundary but the last; the copy makes it an entity the
651
+ # reduceat kernels can read directly.
652
+ @reduceat_index = CArray.segment_offsets(lengths: category_sizes)[0...-1].copy
653
+ end
654
+
655
+ # @overload categorize(labels: nil, sort_labels: false)
656
+ # Returns a {CACategorical} keyed on the labels, as categorizing any
657
+ # key array does: the labels that occur, in first-appearance order (or
658
+ # sorted with `sort_labels:`), or the fixed `labels:` vocabulary. A
659
+ # category of `self` with no cells does not carry over; a masked cell
660
+ # is excluded.
661
+ # @return [CACategorical]
662
+ def categorize (labels: nil, sort_labels: false)
663
+ if labels || sort_labels
664
+ return label_values.categorize(labels: labels, sort_labels: sort_labels)
665
+ end
666
+ # Distinctness rides the codes; only the k labels cross the surface.
667
+ recoded = codes.categorize
668
+ CACategorical.from_codes(recoded.codes, recoded.labels.map { |c| label_at(c) })
669
+ end
670
+
671
+ # The position of +label+ among the labels, found by ==, a NaN label by a
672
+ # NaN, as join and at match NaN keys; nil when it is not one of them.
673
+ private def label_position (label)
674
+ return @labels.index(label) unless label.is_a?(Float) && label.nan?
675
+ @labels.index { |l| l.is_a?(Float) && l.nan? }
676
+ end
677
+
678
+ # The categorical as numeric columns, one per category, for a model that
679
+ # takes numbers: row i holds 1 in the column of its category and 0
680
+ # elsewhere. With an intercept in the model the k columns would sum to the
681
+ # intercept's column, so by default the column of the base category is
682
+ # left out and k - 1 remain; each coefficient then reads as that category
683
+ # minus the base. +full: true+ keeps all k (a model without an intercept,
684
+ # or an input to a learner).
685
+ #
686
+ # The columns follow the labels, not the data: a category with no cells
687
+ # gives a column of zeros, so two categoricals with the same labels give
688
+ # the same columns. A masked cell gives a masked row.
689
+ #
690
+ # @overload contrast(scheme = :dummy, base: nil, full: false)
691
+ # Returns an int8 CArray of shape (n, k - 1), or (n, k) with
692
+ # `full: true`, whose columns are the labels in order with the base
693
+ # left out.
694
+ # @param scheme [Symbol] the coding; only `:dummy` is defined.
695
+ # @param base [Object] the label whose column is left out; the first
696
+ # label when omitted.
697
+ # @param full [Boolean] keep every column.
698
+ # @return [CArray]
699
+ # @raise [ArgumentError] when `scheme` is not `:dummy`, when `base` is
700
+ # not a label, when `base:` and `full: true` are both given, when
701
+ # there are no labels to leave one out of, or when the categorical is
702
+ # not one-dimensional.
703
+ def contrast (scheme = :dummy, base: nil, full: false)
704
+ unless scheme == :dummy
705
+ raise ArgumentError, "contrast: unknown scheme #{scheme.inspect} (only :dummy is defined)"
706
+ end
707
+ unless ndim == 1
708
+ raise ArgumentError, "contrast: the categorical is #{ndim}-dimensional, not one-dimensional"
709
+ end
585
710
  k = @labels.size
586
- out = CArray.int64(k > 0 ? k : 0)
587
- if k > 1
588
- out[0] = 0
589
- out[1..-1] = category_sizes.cumsum.int64[0..-2]
590
- elsif k == 1
591
- out[0] = 0
711
+ if full
712
+ raise ArgumentError, "contrast: base: and full: true contradict (full keeps every column)" unless base.nil?
713
+ kept = (0...k).to_a
714
+ else
715
+ raise ArgumentError, "contrast: there are no labels, so no base column to leave out" if k == 0
716
+ dropped = base.nil? ? 0 : label_position(base)
717
+ raise ArgumentError, "contrast: #{base.inspect} is not one of the labels #{@labels.inspect}" unless dropped
718
+ kept = (0...k).to_a - [dropped]
592
719
  end
593
- @_reduceat_index = out
720
+ n = elements
721
+ m = kept.size
722
+ out = CArray.int8(n, m).zero
723
+ return out if n == 0 || m == 0
724
+
725
+ # One pass: each cell's code picks its column through colmap (-1 for the
726
+ # base), and a 1 is written at row * m + column.
727
+ colmap = CArray.int64(k) { -1 }
728
+ kept.each_with_index { |code, j| colmap[code] = j }
729
+ excluded = codes.has_mask? ? codes.is_masked : CArray.boolean(n) { false }
730
+ code = codes.value.to_type(CA_INT64)
731
+ code[excluded] = 0 if excluded.any
732
+ col = colmap[code]
733
+ hit = col.ge(0) & excluded.not
734
+ addr = CArray.int64(n).seq! * m + col
735
+ out.reshape(n * m)[addr[hit]] = 1 if hit.any
736
+ out[excluded, nil] = UNDEF if excluded.any
594
737
  out
595
738
  end
596
739
 
@@ -629,6 +772,21 @@ class CArray
629
772
  # @return [CACategorical]
630
773
  # @raise [ArgumentError] when explicit `labels` contain
631
774
  # duplicates.
775
+ # @raise [CArray::DataTypeError] when `sort_labels: true` discovers
776
+ # the labels of a complex array, which have no order.
777
+ # The order sort_labels: puts two labels in: the order sort puts the cells
778
+ # of an array in (object_order_cmp); two labels <=> cannot order are
779
+ # refused, by name.
780
+ private def label_order(a, b)
781
+ c = CArray.send(:object_order_cmp, a, b)
782
+ if c.nil?
783
+ raise ArgumentError,
784
+ "categorize(sort_labels: true): cannot order the labels " \
785
+ "#{a.inspect} and #{b.inspect}"
786
+ end
787
+ c
788
+ end
789
+
632
790
  def categorize(labels: nil, sort_labels: false)
633
791
  # Automatic appearance-order vocabulary: one linear pass (C
634
792
  # __factorize_appearance__) returns both codes and levels directly, over the
@@ -636,12 +794,32 @@ class CArray
636
794
  # lane). Distinctness is the hash-key judgement shared with the discovery
637
795
  # family: Float NaN collapses to one category and -0.0 == +0.0, while mixed
638
796
  # Integer / Float keys stay distinct (eql?, so 1 and 1.0 are separate
639
- # categories). The discovery path below is reserved for sort_labels (which
640
- # reorders the vocabulary, desyncing the appearance-order codes), an explicit
641
- # labels list, and the data types the factorize kernel does not take (complex).
642
- if labels.nil? && !sort_labels && (integer? || float? || object? || fixlen? || boolean?)
797
+ # categories). Complex values are distinct by value with the same NaN and
798
+ # -0.0 rules, and have no order, so sort_labels refuses them. The discovery
799
+ # path below is reserved for an explicit labels list.
800
+ if labels.nil? && sort_labels && complex?
801
+ raise CArray::DataTypeError,
802
+ "categorize(sort_labels: true): a #{data_type_name} value has no " \
803
+ "order (complex numbers are not ordered); without sort_labels: the " \
804
+ "labels are in first-appearance order"
805
+ end
806
+ if labels.nil? && (integer? || float? || complex? || object? || fixlen? || boolean?)
643
807
  codes, levels = __factorize_appearance__
644
- return CACategorical.from_codes(codes, levels.to_a)
808
+ levels = levels.to_a
809
+ return CACategorical.from_codes(codes, levels) unless sort_labels
810
+ # Sort the k levels as Ruby sorts them, then renumber every code through
811
+ # the rank in one gather. The sentinel of an excluded cell is out of the
812
+ # rank's range, so project makes it UNDEF and from_codes writes it back.
813
+ # A boolean label reads as true or false, and sorts as its storage,
814
+ # false before true, as sort and unique(sort: true) put the cells.
815
+ order = if boolean?
816
+ (0...levels.size).sort_by { |i| levels[i] ? 1 : 0 }
817
+ else
818
+ (0...levels.size).sort { |i, j| label_order(levels[i], levels[j]) }
819
+ end
820
+ rank = CArray.new(codes.data_type, [levels.size])
821
+ order.each_with_index { |old, new_code| rank[old] = new_code }
822
+ return CACategorical.from_codes(rank.project(codes), order.map { |i| levels[i] })
645
823
  end
646
824
 
647
825
  if labels.nil?
@@ -650,7 +828,7 @@ class CArray
650
828
  # masked keys stay excluded), so the non-masked cells are the levels.
651
829
  # Only the final list is Ruby, since labels are Ruby objects.
652
830
  labels_arr = mask_duplicates[:is_not_masked].to_a
653
- labels_arr.sort! if sort_labels
831
+ labels_arr.sort! { |a, b| label_order(a, b) } if sort_labels
654
832
  else
655
833
  labels_arr = labels.respond_to?(:to_a) ? labels.to_a : Array(labels)
656
834
  if labels_arr.uniq.size != labels_arr.size
@@ -676,3 +854,15 @@ class CArray
676
854
  CACategorical.from_codes(codes, labels_arr)
677
855
  end
678
856
  end
857
+
858
+ # categorize is a conversion: the categories are the values in another
859
+ # representation, so they show the attributes the values show. Each of the
860
+ # three implementations returns from several places, so the attributes are
861
+ # carried here, around all of them.
862
+ [CArray, CACategorical, CAConstString].each do |klass|
863
+ klass.prepend(Module.new do
864
+ def categorize (labels: nil, sort_labels: false)
865
+ carry_attrs(super)
866
+ end
867
+ end)
868
+ end