carray 3.0.3 → 3.0.5

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 (221) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +469 -0
  3. data/README.md +3 -2
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_array_pool.c +25 -1
  6. data/ext/ca_axis_descriptor.h +27 -24
  7. data/ext/ca_axis_dispatch.c +14 -26
  8. data/ext/ca_axis_group.c +27 -27
  9. data/ext/ca_binop_dispatch.c +1 -29
  10. data/ext/ca_binop_dispatch.h +0 -1
  11. data/ext/ca_categorical_iterator.c +231 -153
  12. data/ext/ca_compare.c +9 -20
  13. data/ext/ca_composite_dispatch.c +9 -9
  14. data/ext/ca_group_iter.c +16 -17
  15. data/ext/ca_kernel_iterator.c +1240 -2000
  16. data/ext/ca_kernel_iterator.h +502 -519
  17. data/ext/ca_monop_dispatch.c +82 -59
  18. data/ext/ca_monop_dispatch.h +1 -0
  19. data/ext/ca_obj_array.c +152 -45
  20. data/ext/ca_obj_bincmp.c +25 -21
  21. data/ext/ca_obj_binop.c +16 -26
  22. data/ext/ca_obj_bitarray.c +8 -3
  23. data/ext/ca_obj_bitfield.c +63 -49
  24. data/ext/ca_obj_block.c +18 -9
  25. data/ext/ca_obj_byte_swap.c +7 -7
  26. data/ext/ca_obj_const_string.c +188 -22
  27. data/ext/ca_obj_face.c +280 -58
  28. data/ext/ca_obj_face.h +19 -0
  29. data/ext/ca_obj_fake.c +15 -29
  30. data/ext/ca_obj_farray.c +6 -3
  31. data/ext/ca_obj_field.c +30 -8
  32. data/ext/ca_obj_fixlen_string.c +2 -3
  33. data/ext/ca_obj_grid.c +455 -134
  34. data/ext/ca_obj_meld.c +172 -34
  35. data/ext/ca_obj_moncmp.c +23 -8
  36. data/ext/ca_obj_monop.c +113 -67
  37. data/ext/ca_obj_object.c +210 -52
  38. data/ext/ca_obj_record.c +15 -7
  39. data/ext/ca_obj_reduce.c +17 -8
  40. data/ext/ca_obj_refer.c +42 -35
  41. data/ext/ca_obj_remap.c +30 -9
  42. data/ext/ca_obj_repeat.c +9 -10
  43. data/ext/ca_obj_roll.c +31 -10
  44. data/ext/ca_obj_select.c +48 -14
  45. data/ext/ca_obj_select_axis.c +15 -47
  46. data/ext/ca_obj_shift.c +41 -7
  47. data/ext/ca_obj_stack.c +180 -34
  48. data/ext/ca_obj_stride.c +190 -134
  49. data/ext/ca_obj_string.c +80 -4
  50. data/ext/ca_obj_tile.c +32 -11
  51. data/ext/ca_obj_time.c +22 -6
  52. data/ext/ca_obj_timedelta.c +22 -6
  53. data/ext/ca_obj_transpose.c +13 -7
  54. data/ext/ca_obj_triop.c +26 -32
  55. data/ext/ca_obj_window.c +267 -74
  56. data/ext/ca_op_ipower.c +4 -4
  57. data/ext/ca_sort_kernels.h +6 -3
  58. data/ext/ca_sweep_engine.c +2 -2
  59. data/ext/caframe_csv_reader.c +80 -1
  60. data/ext/carray.h +214 -75
  61. data/ext/carray_access.c +200 -99
  62. data/ext/carray_address_basis.c +343 -87
  63. data/ext/carray_attribute.c +113 -6
  64. data/ext/carray_bincount.c +10 -8
  65. data/ext/carray_broadcast.c +13 -8
  66. data/ext/carray_build_flags.h +3 -0
  67. data/ext/carray_call_cfunc.c +108 -0
  68. data/ext/carray_cast.c +375 -96
  69. data/ext/carray_cast_func.rb +90 -20
  70. data/ext/carray_class.c +4 -3
  71. data/ext/carray_config.h +28 -0
  72. data/ext/carray_conversion.c +18 -9
  73. data/ext/carray_copy.c +51 -14
  74. data/ext/carray_core.c +150 -55
  75. data/ext/carray_count.c +56 -20
  76. data/ext/carray_element.c +81 -33
  77. data/ext/carray_factorize.c +138 -84
  78. data/ext/carray_generate.c +199 -52
  79. data/ext/carray_histogram.c +63 -18
  80. data/ext/carray_hold.c +12 -15
  81. data/ext/carray_index_classifier.c +388 -176
  82. data/ext/carray_internal.h +348 -29
  83. data/ext/carray_kernels_bincmp.c +4453 -0
  84. data/ext/carray_kernels_binop.c +11368 -0
  85. data/ext/carray_kernels_init.c +1734 -0
  86. data/ext/carray_kernels_moncmp.c +2104 -0
  87. data/ext/carray_kernels_monop.c +18369 -0
  88. data/ext/carray_kernels_reduce_aggregate.c +23818 -0
  89. data/ext/carray_kernels_reduce_boolean.c +406 -0
  90. data/ext/carray_kernels_reduce_cumulative.c +10086 -0
  91. data/ext/carray_kernels_reduce_extreme.c +17634 -0
  92. data/ext/carray_kernels_reduce_variance.c +3849 -0
  93. data/ext/carray_kernels_scan.c +4302 -0
  94. data/ext/carray_kernels_search.c +33829 -0
  95. data/ext/carray_kernels_sort.c +10897 -0
  96. data/ext/carray_kernels_triop.c +1864 -0
  97. data/ext/carray_key_windows.c +220 -0
  98. data/ext/carray_lazy.c +170 -111
  99. data/ext/carray_mask.c +337 -94
  100. data/ext/carray_median_percentile.c +82 -28
  101. data/ext/carray_memory_view.c +669 -561
  102. data/ext/carray_operator.c +129 -52
  103. data/ext/carray_order.c +604 -77
  104. data/ext/carray_partition.c +28 -27
  105. data/ext/carray_random.c +85 -31
  106. data/ext/carray_round.c +273 -0
  107. data/ext/carray_scatter.c +87 -45
  108. data/ext/carray_segment.c +3 -3
  109. data/ext/carray_slab.c +123 -37
  110. data/ext/carray_sort.c +259 -51
  111. data/ext/carray_sort_kernel.c +5 -7
  112. data/ext/carray_struct.c +54 -61
  113. data/ext/carray_test.c +26 -5
  114. data/ext/carray_time_text.c +4 -2
  115. data/ext/carray_undef.c +4 -0
  116. data/ext/carray_utils.c +309 -66
  117. data/ext/mk_call_cfunc.rb +19 -2
  118. data/ext/mkkernel.rb +403 -350
  119. data/ext/ruby_carray.c +10 -4
  120. data/ext/version.h +4 -4
  121. data/lib/carray/attribute.rb +139 -22
  122. data/lib/carray/autoload_carray.rb +8 -31
  123. data/lib/carray/autoload_method_extension.rb +14 -5
  124. data/lib/carray/axis_group.rb +255 -106
  125. data/lib/carray/basics.rb +105 -17
  126. data/lib/carray/bincount_nd.rb +12 -2
  127. data/lib/carray/block_iterator.rb +126 -23
  128. data/lib/carray/boolean_reduce.rb +17 -17
  129. data/lib/carray/categorical.rb +195 -59
  130. data/lib/carray/categorical_iterator.rb +132 -29
  131. data/lib/carray/complex.rb +5 -5
  132. data/lib/carray/conditional.rb +67 -18
  133. data/lib/carray/const_string.rb +24 -5
  134. data/lib/carray/construct.rb +15 -11
  135. data/lib/carray/data_type_extension.rb +9 -1
  136. data/lib/carray/distance_limit.rb +77 -0
  137. data/lib/carray/fixlen_string.rb +48 -3
  138. data/lib/carray/frame/column_verbs.rb +71 -0
  139. data/lib/carray/frame/concat.rb +17 -3
  140. data/lib/carray/frame/convert.rb +69 -0
  141. data/lib/carray/frame/csv_parser.rb +111 -17
  142. data/lib/carray/frame/describe.rb +3 -2
  143. data/lib/carray/frame/frame.rb +252 -28
  144. data/lib/carray/frame/group.rb +603 -101
  145. data/lib/carray/frame/io.rb +117 -28
  146. data/lib/carray/frame/join.rb +312 -51
  147. data/lib/carray/frame/records.rb +83 -7
  148. data/lib/carray/frame/reshape.rb +42 -35
  149. data/lib/carray/frame/sort.rb +7 -19
  150. data/lib/carray/frame/verbs.rb +188 -48
  151. data/lib/carray/frame/windows.rb +117 -0
  152. data/lib/carray/frame.rb +2 -0
  153. data/lib/carray/fuse_source.rb +42 -6
  154. data/lib/carray/fusion.rb +40 -16
  155. data/lib/carray/histogram.rb +6 -1
  156. data/lib/carray/inspect.rb +31 -8
  157. data/lib/carray/iterator.rb +85 -7
  158. data/lib/carray/key_windows.rb +620 -0
  159. data/lib/carray/lazy.rb +217 -183
  160. data/lib/carray/mask_gap_fill.rb +63 -57
  161. data/lib/carray/meld_reduce.rb +12 -14
  162. data/lib/carray/methods/bin.rb +26 -18
  163. data/lib/carray/methods/bincount.rb +16 -0
  164. data/lib/carray/methods/bit_string.rb +1 -1
  165. data/lib/carray/methods/choose.rb +16 -3
  166. data/lib/carray/methods/composition.rb +3 -1
  167. data/lib/carray/methods/gather_nd.rb +14 -5
  168. data/lib/carray/methods/insert_block.rb +2 -2
  169. data/lib/carray/methods/is_between.rb +59 -0
  170. data/lib/carray/methods/is_in.rb +61 -37
  171. data/lib/carray/methods/join.rb +1 -6
  172. data/lib/carray/methods/lag.rb +49 -0
  173. data/lib/carray/methods/locate_addr.rb +47 -51
  174. data/lib/carray/methods/mode.rb +1 -1
  175. data/lib/carray/methods/nunique.rb +11 -6
  176. data/lib/carray/methods/resize.rb +3 -3
  177. data/lib/carray/methods/value_counts.rb +4 -11
  178. data/lib/carray/record_order.rb +3 -3
  179. data/lib/carray/rng.rb +3 -0
  180. data/lib/carray/runtime.rb +53 -8
  181. data/lib/carray/segment_iterator.rb +323 -88
  182. data/lib/carray/serialize.rb +45 -14
  183. data/lib/carray/slab_iterator.rb +50 -0
  184. data/lib/carray/stack.rb +15 -12
  185. data/lib/carray/string.rb +113 -5
  186. data/lib/carray/string_operation_extension.rb +21 -6
  187. data/lib/carray/struct.rb +9 -21
  188. data/lib/carray/struct_builder.rb +24 -11
  189. data/lib/carray/time.rb +819 -158
  190. data/lib/carray/window_iterator.rb +122 -16
  191. data/lib/carray.rb +7 -0
  192. data/yard-stubs/ca_obj_bitfield.rb +1 -1
  193. data/yard-stubs/ca_obj_field.rb +4 -0
  194. data/yard-stubs/ca_obj_grid.rb +21 -10
  195. data/yard-stubs/ca_obj_refer.rb +2 -1
  196. data/yard-stubs/ca_obj_roll.rb +2 -2
  197. data/yard-stubs/ca_obj_shift.rb +5 -1
  198. data/yard-stubs/ca_obj_tile.rb +1 -1
  199. data/yard-stubs/ca_obj_transpose.rb +5 -4
  200. data/yard-stubs/ca_obj_window.rb +8 -3
  201. data/yard-stubs/carray_access.rb +6 -1
  202. data/yard-stubs/carray_attribute.rb +3 -6
  203. data/yard-stubs/carray_cast.rb +12 -0
  204. data/yard-stubs/carray_conversion.rb +7 -2
  205. data/yard-stubs/carray_copy.rb +4 -2
  206. data/yard-stubs/carray_count.rb +4 -4
  207. data/yard-stubs/carray_element.rb +17 -10
  208. data/yard-stubs/carray_generate.rb +21 -1
  209. data/yard-stubs/carray_mask.rb +32 -3
  210. data/yard-stubs/carray_math.rb +41 -5
  211. data/yard-stubs/carray_median_percentile.rb +5 -2
  212. data/yard-stubs/carray_memory_view.rb +40 -23
  213. data/yard-stubs/carray_order.rb +129 -22
  214. data/yard-stubs/carray_random.rb +13 -4
  215. data/yard-stubs/carray_scatter.rb +17 -10
  216. data/yard-stubs/carray_slab.rb +9 -2
  217. data/yard-stubs/carray_sort.rb +6 -5
  218. data/yard-stubs/carray_test.rb +5 -3
  219. metadata +25 -3
  220. data/lib/carray/arrow_tensor.rb +0 -401
  221. 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,
@@ -269,7 +299,7 @@ class CACategorical < CAObject
269
299
  # `codes[i] = x` raises; use `codes.copy` for a mutable copy.
270
300
  # @return [CArray]
271
301
  def codes
272
- parent
302
+ face_storage
273
303
  end
274
304
 
275
305
  # Face hook: carry the vocabulary across lifted views (slice / reshape / …).
@@ -298,10 +328,17 @@ class CACategorical < CAObject
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
330
  def storage_to_element(raw)
301
- code = raw.is_a?(String) ? raw.unpack1(UNPACK_FORMAT.fetch(parent.data_type)) : raw
331
+ code = raw.is_a?(String) ? raw.unpack1(UNPACK_FORMAT.fetch(face_storage.data_type)) : raw
302
332
  label_at(code)
303
333
  end
304
334
 
335
+ # Face hook: a value written into a cell (a fill_value of shift and
336
+ # window) is a label, and the cell holds its code.
337
+ # @raise [ArgumentError] if the value is not one of the labels.
338
+ def element_to_storage (value)
339
+ code_for_fill(value)
340
+ end
341
+
305
342
  # ---- category-space operations (by label, not code) -------------------
306
343
  #
307
344
  # The element value of a categorical IS its category, so these specialise
@@ -309,14 +346,6 @@ class CACategorical < CAObject
309
346
  # the integer code. The raw codes stay reachable via #codes for code-space
310
347
  # work (interop, ML features), but they are not the everyday surface.
311
348
 
312
- # Boolean mask of cells whose category == label. Excluded cells stay UNDEF
313
- # (their category is unknown); an unknown label yields an all-false mask.
314
- # @overload eq(label)
315
- # 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.
319
- # @return [CArray]
320
349
  # @overload resize(*newdim, fill_value: UNDEF)
321
350
  # Returns a categorical resized to `newdim` (see {CArray#resize}).
322
351
  # The new cells are excluded (UNDEF) unless `fill_value` names one of
@@ -338,16 +367,28 @@ class CACategorical < CAObject
338
367
  CACategorical.from_codes(codes.insert_block(offset, bsize) { fill }, labels)
339
368
  end
340
369
 
370
+ # Boolean mask of cells whose category == label. Excluded cells stay UNDEF
371
+ # (their category is unknown); an unknown label yields an all-false mask.
372
+ # @overload eq(label)
373
+ # Returns a boolean CArray marking cells whose category equals
374
+ # `label`. A CArray is compared cell by cell, by label: another
375
+ # categorical is matched whatever order its labels are in, and a
376
+ # masked cell on either side gives UNDEF. Anything else, a Ruby Array
377
+ # included, is one label, as for {CArray#eq}. An unknown label matches
378
+ # nothing.
379
+ # @param label [Object, CArray] category to match, or one per cell.
380
+ # @return [CArray]
341
381
  def eq(label)
342
- codes.eq(@labels.index(label) || @labels.size)
382
+ codes.eq(codes_of(label))
343
383
  end
344
384
 
345
385
  # @overload ne(label)
346
386
  # Returns the complement of {#eq}.
347
- # @param label [Object] category to compare against.
387
+ # @param label [Object, CArray] category to compare against, or one per
388
+ # cell.
348
389
  # @return [CArray]
349
390
  def ne(label)
350
- codes.ne(@labels.index(label) || @labels.size)
391
+ codes.ne(codes_of(label))
351
392
  end
352
393
 
353
394
  # @overload count(label)
@@ -355,7 +396,14 @@ class CACategorical < CAObject
355
396
  # (0 for an unknown label).
356
397
  # @param label [Object] category to count.
357
398
  # @return [Integer]
358
- def count(label)
399
+ # @overload count
400
+ # Returns the number of cells in some category (not masked), as
401
+ # {CArray#count} with no value does.
402
+ # @return [Integer]
403
+ def count(*args)
404
+ return count_not_masked if args.empty?
405
+ raise ArgumentError, "count: wrong number of arguments (given #{args.size}, expected 0..1)" if args.size > 1
406
+ label = args[0]
359
407
  code = @labels.index(label)
360
408
  code ? codes.count(code) : 0
361
409
  end
@@ -366,7 +414,7 @@ class CACategorical < CAObject
366
414
  #
367
415
  # Memoised: the codes are read-only (immutable storage), so the counts are a
368
416
  # pure function of the categorical and stay valid for its lifetime. The same
369
- # cached array backs {#reduceat_index} and {#sort_addr}, so a wide aggregate
417
+ # cached array backs {#reduceat_index} and {#group_addr}, so a wide aggregate
370
418
  # over the same categorical pays the count once. (Do not mutate the returned
371
419
  # array — it is shared; take `.copy` for a scratch buffer.) A derived-view /
372
420
  # composite categorical caches too: read-only rides from the codes as a flag,
@@ -446,6 +494,7 @@ class CACategorical < CAObject
446
494
  # @param values [Array, CArray, CACategorical] labels to test against.
447
495
  # @return [CArray]
448
496
  def is_in (values)
497
+ refuse_range_set(values, "is_in")
449
498
  wanted = label_list(values).filter_map { |l| @labels.index(l) }
450
499
  codes.is_in(CArray.int64(wanted.size) { |i| wanted[i] })
451
500
  end
@@ -453,18 +502,21 @@ class CACategorical < CAObject
453
502
  # @overload intersection(other, sort: false)
454
503
  # @return [CArray] object CArray of the labels present in both.
455
504
  def intersection (other, sort: false)
505
+ refuse_range_set(other, "intersection")
456
506
  unique.intersection(label_array(other), sort: sort)
457
507
  end
458
508
 
459
509
  # @overload difference(other, sort: false)
460
510
  # @return [CArray] object CArray of the labels only `self` has.
461
511
  def difference (other, sort: false)
512
+ refuse_range_set(other, "difference")
462
513
  unique.difference(label_array(other), sort: sort)
463
514
  end
464
515
 
465
516
  # @overload union(other, sort: false)
466
517
  # @return [CArray] object CArray of the labels of either side.
467
518
  def union (other, sort: false)
519
+ refuse_range_set(other, "union")
468
520
  unique.union(label_array(other), sort: sort)
469
521
  end
470
522
 
@@ -484,10 +536,28 @@ class CACategorical < CAObject
484
536
  # that has none. Every code-to-label path goes through here rather than
485
537
  # indexing @labels itself, because two decodes are two chances to disagree,
486
538
  # and disagreeing is how #unique came to contradict #to_a on the same cell.
539
+ # An operand of #eq / #ne as codes of this vocabulary. A value that is not
540
+ # one of the labels gets labels.size, which no cell holds. A CArray is read
541
+ # cell by cell, a masked cell staying masked; a categorical goes through its
542
+ # own codes, so the two vocabularies need not agree. Anything else is one
543
+ # label, as CArray#eq takes it.
544
+ def codes_of (other)
545
+ return @labels.index(other) || @labels.size unless other.is_a?(CArray)
546
+ if other.is_a?(CACategorical)
547
+ table = other.labels.map { |l| @labels.index(l) || @labels.size }
548
+ return CA_INT64(table)[other.codes] unless table.empty?
549
+ end
550
+ memo = {}
551
+ list = other.flatten.to_a.map { |v|
552
+ UNDEF.equal?(v) ? v : memo.fetch(v) { memo[v] = @labels.index(v) || @labels.size }
553
+ }
554
+ CA_INT64(list).reshape(*other.shape)
555
+ end
556
+
487
557
  # The code a fill value stands for: UNDEF stays UNDEF, a label is its code.
488
558
  def code_for_fill (value)
489
559
  return UNDEF if UNDEF.equal?(value)
490
- code = labels.index(value)
560
+ code = label_position(value)
491
561
  raise ArgumentError, "#{value.inspect} is not one of the labels #{labels.inspect}" unless code
492
562
  code
493
563
  end
@@ -527,27 +597,27 @@ class CACategorical < CAObject
527
597
  # two accessors are the building blocks for that other engine — lay the data
528
598
  # out as category-contiguous blocks, then select per block.
529
599
  #
530
- # The grouping plan — sort_addr (the counting sort, the dominant cost) plus
600
+ # The grouping plan — group_addr (the counting sort, the dominant cost) plus
531
601
  # reduceat_index and category_sizes — depends only on the codes, which are
532
602
  # read-only. It is therefore memoised (a pure function of the categorical): the
533
603
  # first grouping access builds it and every later one reuses it. This is what
534
604
  # lets a wide aggregate (many payload columns over one classifier) or a
535
605
  # CACategoricalIterator run the counting sort once, not once per column /
536
606
  # iterator. Only the plan is cached; the payload-dependent grouped copy
537
- # (value.reshape(n)[sort_addr]) is rebuilt per column (it varies with value).
607
+ # (value.reshape(n)[group_addr]) is rebuilt per column (it varies with value).
538
608
 
539
- # Force-build (and cache) the whole grouping plan up front — sort_addr,
609
+ # Force-build (and cache) the whole grouping plan up front — group_addr,
540
610
  # reduceat_index, category_sizes — for prepare-ahead use before a batch of
541
611
  # groupings. Lazy building already covers correctness; this is the explicit
542
612
  # "pay the counting sort now" handle (e.g. right after `df.group_by(col)`).
543
613
  # Returns self so it chains.
544
614
  # @overload build_grouping
545
- # Eagerly builds and caches the grouping plan (sort_addr / reduceat_index
615
+ # Eagerly builds and caches the grouping plan (group_addr / reduceat_index
546
616
  # / category_sizes). Optional — the plan is built lazily on first grouping
547
617
  # access — but useful to pay the counting sort once ahead of a batch.
548
618
  # @return [self]
549
619
  def build_grouping
550
- sort_addr # pulls category_sizes; reduceat_index shares category_sizes
620
+ group_addr # pulls category_sizes; reduceat_index shares category_sizes
551
621
  reduceat_index
552
622
  self
553
623
  end
@@ -561,44 +631,26 @@ class CACategorical < CAObject
561
631
  # order. The first {#category_sizes}.sum addresses are exactly the classified
562
632
  # cells in category order — CACategoricalIterator's permutation. For an N-D
563
633
  # categorical the addresses are into the raveled storage, so
564
- # `value.reshape(elements)[cat.sort_addr]` produces the contiguous blocks.
634
+ # `value.reshape(elements)[cat.group_addr]` produces the contiguous blocks.
565
635
  # Memoised (see the grouping-plan note above): the counting sort is the
566
636
  # dominant grouping cost, so it is computed once and shared by every payload
567
- # column and by CACategoricalIterator. Do not mutate the returned array.
568
- # @overload sort_addr
637
+ # column and by CACategoricalIterator, and kept for the categorical's
638
+ # lifetime (an int64 per cell). Do not mutate the returned array.
639
+ # @overload group_addr
569
640
  # Returns a length-{#elements} integer CArray of flat storage
570
641
  # addresses that order the cells by category (excluded cells last).
571
642
  # @return [CArray]
572
- def sort_addr
573
- return @sort_addr if @sort_addr
574
- n = elements
575
- k = @labels.size
576
- nvalid = category_sizes.sum
577
- cur = reduceat_index.copy # segment starts, consumed as cursor
578
- flat = codes.reshape(n)
579
- seq = CArray.int64(n).seq! # source indices, scattered as payload
580
- out = CArray.int64(n)
581
- if nvalid == n
582
- flat.send(:__categorical_scatter__, seq, cur, out, k) # all valid: scatter straight in
583
- else
584
- if nvalid > 0
585
- valid = CArray.int64(nvalid) # scatter target must be an entity, not a view
586
- flat.send(:__categorical_scatter__, seq, cur, valid, k)
587
- out[0...nvalid] = valid
588
- end
589
- # Excluded = every cell the scatter skips: code out of [0, k), OR masked
590
- # (a from_codes pre-masked cell keeps a valid code but is excluded). Both
591
- # must be caught or the tail slot count would not add up to n - nvalid.
592
- excluded = flat.value.ge(k).or(flat.value.lt(0))
593
- excluded = excluded.or(flat.is_masked) if flat.has_mask?
594
- out[nvalid..-1] = seq[excluded] # excluded cells, source order
595
- end
596
- @sort_addr = out
643
+ def group_addr
644
+ return @group_addr if @group_addr
645
+ out = codes.reshape(elements).__send__(:addresses_by_category,
646
+ reduceat_index.copy, @labels.size,
647
+ category_sizes.sum)
648
+ @group_addr = out
597
649
  out
598
650
  end
599
651
 
600
652
  # Segment start offsets into the category-contiguous layout produced by
601
- # {#sort_addr}: reduceat_index[c] is where category c's block begins, so
653
+ # {#group_addr}: reduceat_index[c] is where category c's block begins, so
602
654
  # block c spans `reduceat_index[c] ... reduceat_index[c] + category_sizes[c]`.
603
655
  # Empty categories repeat the following start (a zero-width block). Length k,
604
656
  # aligned to #labels; pair with {#category_sizes} for the block lengths. On
@@ -630,6 +682,75 @@ class CACategorical < CAObject
630
682
  CACategorical.from_codes(recoded.codes, recoded.labels.map { |c| label_at(c) })
631
683
  end
632
684
 
685
+ # The position of +label+ among the labels, found by ==, a NaN label by a
686
+ # NaN, as join and at match NaN keys; nil when it is not one of them.
687
+ private def label_position (label)
688
+ return @labels.index(label) unless label.is_a?(Float) && label.nan?
689
+ @labels.index { |l| l.is_a?(Float) && l.nan? }
690
+ end
691
+
692
+ # The categorical as numeric columns, one per category, for a model that
693
+ # takes numbers: row i holds 1 in the column of its category and 0
694
+ # elsewhere. With an intercept in the model the k columns would sum to the
695
+ # intercept's column, so by default the column of the base category is
696
+ # left out and k - 1 remain; each coefficient then reads as that category
697
+ # minus the base. +full: true+ keeps all k (a model without an intercept,
698
+ # or an input to a learner).
699
+ #
700
+ # The columns follow the labels, not the data: a category with no cells
701
+ # gives a column of zeros, so two categoricals with the same labels give
702
+ # the same columns. A masked cell gives a masked row.
703
+ #
704
+ # @overload contrast(scheme = :dummy, base: nil, full: false)
705
+ # Returns an int8 CArray of shape (n, k - 1), or (n, k) with
706
+ # `full: true`, whose columns are the labels in order with the base
707
+ # left out.
708
+ # @param scheme [Symbol] the coding; only `:dummy` is defined.
709
+ # @param base [Object] the label whose column is left out; the first
710
+ # label when omitted.
711
+ # @param full [Boolean] keep every column.
712
+ # @return [CArray]
713
+ # @raise [ArgumentError] when `scheme` is not `:dummy`, when `base` is
714
+ # not a label, when `base:` and `full: true` are both given, when
715
+ # there are no labels to leave one out of, or when the categorical is
716
+ # not one-dimensional.
717
+ def contrast (scheme = :dummy, base: nil, full: false)
718
+ unless scheme == :dummy
719
+ raise ArgumentError, "contrast: unknown scheme #{scheme.inspect} (only :dummy is defined)"
720
+ end
721
+ unless ndim == 1
722
+ raise ArgumentError, "contrast: the categorical is #{ndim}-dimensional, not one-dimensional"
723
+ end
724
+ k = @labels.size
725
+ if full
726
+ raise ArgumentError, "contrast: base: and full: true contradict (full keeps every column)" unless base.nil?
727
+ kept = (0...k).to_a
728
+ else
729
+ raise ArgumentError, "contrast: there are no labels, so no base column to leave out" if k == 0
730
+ dropped = base.nil? ? 0 : label_position(base)
731
+ raise ArgumentError, "contrast: #{base.inspect} is not one of the labels #{@labels.inspect}" unless dropped
732
+ kept = (0...k).to_a - [dropped]
733
+ end
734
+ n = elements
735
+ m = kept.size
736
+ out = CArray.int8(n, m).zero
737
+ return out if n == 0 || m == 0
738
+
739
+ # One pass: each cell's code picks its column through colmap (-1 for the
740
+ # base), and a 1 is written at row * m + column.
741
+ colmap = CArray.int64(k) { -1 }
742
+ kept.each_with_index { |code, j| colmap[code] = j }
743
+ excluded = codes.has_mask? ? codes.is_masked : CArray.boolean(n) { false }
744
+ code = codes.value.to_type(CA_INT64)
745
+ code[excluded] = 0 if excluded.any
746
+ col = colmap[code]
747
+ hit = col.ge(0) & excluded.not
748
+ addr = CArray.int64(n).seq! * m + col
749
+ out.reshape(n * m)[addr[hit]] = 1 if hit.any
750
+ out[excluded, nil] = UNDEF if excluded.any
751
+ out
752
+ end
753
+
633
754
  # @overload inspect
634
755
  # Returns a short summary showing element count, category count,
635
756
  # and vocabulary.
@@ -667,14 +788,11 @@ class CArray
667
788
  # duplicates.
668
789
  # @raise [CArray::DataTypeError] when `sort_labels: true` discovers
669
790
  # the labels of a complex array, which have no order.
670
- # The order sort_labels: puts two labels in: as sort orders the cells of an
671
- # array, a Float NaN after every other value; two labels <=> cannot order
672
- # are refused, by name.
791
+ # The order sort_labels: puts two labels in: the order sort puts the cells
792
+ # of an array in (object_order_cmp); two labels <=> cannot order are
793
+ # refused, by name.
673
794
  private def label_order(a, b)
674
- na = a.is_a?(Float) && a.nan?
675
- nb = b.is_a?(Float) && b.nan?
676
- return (na ? 1 : 0) - (nb ? 1 : 0) if na || nb
677
- c = a <=> b
795
+ c = CArray.send(:object_order_cmp, a, b)
678
796
  if c.nil?
679
797
  raise ArgumentError,
680
798
  "categorize(sort_labels: true): cannot order the labels " \
@@ -706,7 +824,13 @@ class CArray
706
824
  # Sort the k levels as Ruby sorts them, then renumber every code through
707
825
  # the rank in one gather. The sentinel of an excluded cell is out of the
708
826
  # rank's range, so project makes it UNDEF and from_codes writes it back.
709
- order = (0...levels.size).sort { |i, j| label_order(levels[i], levels[j]) }
827
+ # A boolean label reads as true or false, and sorts as its storage,
828
+ # false before true, as sort and unique(sort: true) put the cells.
829
+ order = if boolean?
830
+ (0...levels.size).sort_by { |i| levels[i] ? 1 : 0 }
831
+ else
832
+ (0...levels.size).sort { |i, j| label_order(levels[i], levels[j]) }
833
+ end
710
834
  rank = CArray.new(codes.data_type, [levels.size])
711
835
  order.each_with_index { |old, new_code| rank[old] = new_code }
712
836
  return CACategorical.from_codes(rank.project(codes), order.map { |i| levels[i] })
@@ -744,3 +868,15 @@ class CArray
744
868
  CACategorical.from_codes(codes, labels_arr)
745
869
  end
746
870
  end
871
+
872
+ # categorize is a conversion: the categories are the values in another
873
+ # representation, so they show the attributes the values show. Each of the
874
+ # three implementations returns from several places, so the attributes are
875
+ # carried here, around all of them.
876
+ [CArray, CACategorical, CAConstString].each do |klass|
877
+ klass.prepend(Module.new do
878
+ def categorize (labels: nil, sort_labels: false)
879
+ carry_attrs(super)
880
+ end
881
+ end)
882
+ end