carray 3.0.3 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +264 -0
- data/README.md +3 -2
- data/carray.gemspec +1 -1
- data/ext/ca_axis_descriptor.h +27 -24
- data/ext/ca_axis_dispatch.c +1 -6
- data/ext/ca_axis_group.c +27 -27
- data/ext/ca_binop_dispatch.c +1 -29
- data/ext/ca_binop_dispatch.h +0 -1
- data/ext/ca_categorical_iterator.c +205 -134
- data/ext/ca_compare.c +7 -18
- data/ext/ca_group_iter.c +16 -17
- data/ext/ca_kernel_iterator.c +1234 -1994
- data/ext/ca_kernel_iterator.h +391 -437
- data/ext/ca_monop_dispatch.c +82 -59
- data/ext/ca_monop_dispatch.h +1 -0
- data/ext/ca_obj_array.c +96 -39
- data/ext/ca_obj_bincmp.c +24 -21
- data/ext/ca_obj_binop.c +15 -26
- data/ext/ca_obj_bitarray.c +3 -2
- data/ext/ca_obj_bitfield.c +5 -6
- data/ext/ca_obj_block.c +18 -9
- data/ext/ca_obj_byte_swap.c +5 -6
- data/ext/ca_obj_const_string.c +173 -19
- data/ext/ca_obj_face.c +238 -57
- data/ext/ca_obj_face.h +19 -0
- data/ext/ca_obj_fake.c +15 -29
- data/ext/ca_obj_farray.c +6 -3
- data/ext/ca_obj_field.c +17 -5
- data/ext/ca_obj_fixlen_string.c +2 -3
- data/ext/ca_obj_grid.c +406 -132
- data/ext/ca_obj_meld.c +172 -34
- data/ext/ca_obj_moncmp.c +23 -8
- data/ext/ca_obj_monop.c +67 -57
- data/ext/ca_obj_object.c +157 -51
- data/ext/ca_obj_record.c +12 -7
- data/ext/ca_obj_reduce.c +13 -8
- data/ext/ca_obj_refer.c +25 -27
- data/ext/ca_obj_remap.c +30 -9
- data/ext/ca_obj_repeat.c +9 -10
- data/ext/ca_obj_roll.c +6 -5
- data/ext/ca_obj_select.c +14 -9
- data/ext/ca_obj_select_axis.c +15 -47
- data/ext/ca_obj_shift.c +11 -1
- data/ext/ca_obj_stack.c +179 -34
- data/ext/ca_obj_stride.c +162 -115
- data/ext/ca_obj_string.c +71 -4
- data/ext/ca_obj_tile.c +6 -5
- data/ext/ca_obj_time.c +22 -6
- data/ext/ca_obj_timedelta.c +22 -6
- data/ext/ca_obj_transpose.c +5 -3
- data/ext/ca_obj_triop.c +23 -32
- data/ext/ca_obj_window.c +190 -26
- data/ext/ca_sweep_engine.c +2 -2
- data/ext/caframe_csv_reader.c +80 -1
- data/ext/carray.h +181 -75
- data/ext/carray_access.c +184 -96
- data/ext/carray_address_basis.c +343 -87
- data/ext/carray_attribute.c +89 -6
- data/ext/carray_bincount.c +10 -8
- data/ext/carray_broadcast.c +13 -8
- data/ext/carray_build_flags.h +3 -0
- data/ext/carray_call_cfunc.c +108 -0
- data/ext/carray_cast.c +142 -24
- data/ext/carray_cast_func.rb +46 -6
- data/ext/carray_class.c +4 -3
- data/ext/carray_config.h +28 -0
- data/ext/carray_conversion.c +16 -7
- data/ext/carray_copy.c +51 -14
- data/ext/carray_core.c +147 -55
- data/ext/carray_count.c +50 -20
- data/ext/carray_element.c +76 -33
- data/ext/carray_factorize.c +118 -64
- data/ext/carray_generate.c +191 -50
- data/ext/carray_histogram.c +62 -17
- data/ext/carray_hold.c +12 -15
- data/ext/carray_index_classifier.c +388 -176
- data/ext/carray_internal.h +336 -29
- data/ext/carray_kernels_bincmp.c +4453 -0
- data/ext/carray_kernels_binop.c +11368 -0
- data/ext/carray_kernels_init.c +1734 -0
- data/ext/carray_kernels_moncmp.c +2104 -0
- data/ext/carray_kernels_monop.c +18369 -0
- data/ext/carray_kernels_reduce_aggregate.c +23818 -0
- data/ext/carray_kernels_reduce_boolean.c +406 -0
- data/ext/carray_kernels_reduce_cumulative.c +10086 -0
- data/ext/carray_kernels_reduce_extreme.c +17634 -0
- data/ext/carray_kernels_reduce_variance.c +3849 -0
- data/ext/carray_kernels_scan.c +4302 -0
- data/ext/carray_kernels_search.c +33829 -0
- data/ext/carray_kernels_sort.c +10906 -0
- data/ext/carray_kernels_triop.c +1864 -0
- data/ext/carray_key_windows.c +216 -0
- data/ext/carray_lazy.c +170 -111
- data/ext/carray_mask.c +292 -72
- data/ext/carray_median_percentile.c +82 -28
- data/ext/carray_memory_view.c +669 -561
- data/ext/carray_operator.c +124 -47
- data/ext/carray_order.c +384 -76
- data/ext/carray_partition.c +28 -27
- data/ext/carray_random.c +67 -13
- data/ext/carray_round.c +273 -0
- data/ext/carray_scatter.c +63 -38
- data/ext/carray_segment.c +3 -3
- data/ext/carray_slab.c +91 -28
- data/ext/carray_sort.c +35 -35
- data/ext/carray_struct.c +19 -18
- data/ext/carray_test.c +26 -5
- data/ext/carray_undef.c +4 -0
- data/ext/carray_utils.c +246 -45
- data/ext/mk_call_cfunc.rb +19 -2
- data/ext/mkkernel.rb +198 -240
- data/ext/ruby_carray.c +10 -4
- data/ext/version.h +4 -4
- data/lib/carray/attribute.rb +126 -22
- data/lib/carray/autoload_carray.rb +8 -31
- data/lib/carray/autoload_method_extension.rb +14 -5
- data/lib/carray/axis_group.rb +255 -106
- data/lib/carray/basics.rb +78 -10
- data/lib/carray/bincount_nd.rb +12 -2
- data/lib/carray/block_iterator.rb +126 -23
- data/lib/carray/boolean_reduce.rb +17 -17
- data/lib/carray/categorical.rb +165 -43
- data/lib/carray/categorical_iterator.rb +130 -27
- data/lib/carray/complex.rb +5 -5
- data/lib/carray/conditional.rb +67 -18
- data/lib/carray/const_string.rb +14 -5
- data/lib/carray/construct.rb +15 -11
- data/lib/carray/data_type_extension.rb +1 -0
- data/lib/carray/distance_limit.rb +77 -0
- data/lib/carray/fixlen_string.rb +7 -1
- data/lib/carray/frame/column_verbs.rb +70 -0
- data/lib/carray/frame/convert.rb +69 -0
- data/lib/carray/frame/csv_parser.rb +111 -17
- data/lib/carray/frame/describe.rb +3 -2
- data/lib/carray/frame/frame.rb +246 -21
- data/lib/carray/frame/group.rb +510 -95
- data/lib/carray/frame/io.rb +115 -28
- data/lib/carray/frame/join.rb +312 -51
- data/lib/carray/frame/records.rb +83 -7
- data/lib/carray/frame/reshape.rb +42 -35
- data/lib/carray/frame/sort.rb +7 -19
- data/lib/carray/frame/verbs.rb +188 -48
- data/lib/carray/frame/windows.rb +117 -0
- data/lib/carray/frame.rb +2 -0
- data/lib/carray/fuse_source.rb +11 -4
- data/lib/carray/fusion.rb +40 -16
- data/lib/carray/histogram.rb +6 -1
- data/lib/carray/inspect.rb +31 -8
- data/lib/carray/iterator.rb +85 -7
- data/lib/carray/key_windows.rb +620 -0
- data/lib/carray/lazy.rb +182 -183
- data/lib/carray/mask_gap_fill.rb +63 -57
- data/lib/carray/meld_reduce.rb +12 -14
- data/lib/carray/methods/bin.rb +26 -18
- data/lib/carray/methods/bincount.rb +16 -0
- data/lib/carray/methods/bit_string.rb +1 -1
- data/lib/carray/methods/choose.rb +16 -3
- data/lib/carray/methods/composition.rb +3 -1
- data/lib/carray/methods/gather_nd.rb +14 -5
- data/lib/carray/methods/insert_block.rb +2 -2
- data/lib/carray/methods/is_between.rb +59 -0
- data/lib/carray/methods/is_in.rb +53 -36
- data/lib/carray/methods/join.rb +1 -6
- data/lib/carray/methods/lag.rb +49 -0
- data/lib/carray/methods/locate_addr.rb +47 -51
- data/lib/carray/methods/mode.rb +1 -1
- data/lib/carray/methods/nunique.rb +11 -6
- data/lib/carray/methods/resize.rb +3 -3
- data/lib/carray/methods/value_counts.rb +4 -11
- data/lib/carray/record_order.rb +3 -3
- data/lib/carray/rng.rb +3 -0
- data/lib/carray/runtime.rb +53 -8
- data/lib/carray/segment_iterator.rb +313 -82
- data/lib/carray/serialize.rb +24 -10
- data/lib/carray/slab_iterator.rb +50 -0
- data/lib/carray/string.rb +105 -5
- data/lib/carray/string_operation_extension.rb +8 -1
- data/lib/carray/struct.rb +9 -21
- data/lib/carray/struct_builder.rb +19 -8
- data/lib/carray/time.rb +726 -104
- data/lib/carray/window_iterator.rb +122 -16
- data/lib/carray.rb +7 -0
- data/yard-stubs/ca_obj_bitfield.rb +1 -1
- data/yard-stubs/ca_obj_field.rb +4 -0
- data/yard-stubs/ca_obj_grid.rb +21 -10
- data/yard-stubs/ca_obj_refer.rb +2 -1
- data/yard-stubs/ca_obj_roll.rb +2 -2
- data/yard-stubs/ca_obj_shift.rb +5 -1
- data/yard-stubs/ca_obj_tile.rb +1 -1
- data/yard-stubs/ca_obj_window.rb +8 -3
- data/yard-stubs/carray_access.rb +6 -1
- data/yard-stubs/carray_attribute.rb +0 -5
- data/yard-stubs/carray_cast.rb +10 -0
- data/yard-stubs/carray_conversion.rb +2 -1
- data/yard-stubs/carray_copy.rb +4 -2
- data/yard-stubs/carray_count.rb +4 -4
- data/yard-stubs/carray_element.rb +10 -4
- data/yard-stubs/carray_generate.rb +21 -1
- data/yard-stubs/carray_mask.rb +26 -2
- data/yard-stubs/carray_math.rb +27 -1
- data/yard-stubs/carray_median_percentile.rb +5 -2
- data/yard-stubs/carray_memory_view.rb +40 -23
- data/yard-stubs/carray_order.rb +29 -18
- data/yard-stubs/carray_random.rb +13 -4
- data/yard-stubs/carray_scatter.rb +14 -9
- data/yard-stubs/carray_slab.rb +9 -2
- data/yard-stubs/carray_sort.rb +6 -5
- data/yard-stubs/carray_test.rb +5 -3
- metadata +25 -3
- data/lib/carray/arrow_tensor.rb +0 -401
- data/lib/carray/table.rb +0 -108
data/lib/carray/bincount_nd.rb
CHANGED
|
@@ -78,8 +78,13 @@ class CArray
|
|
|
78
78
|
# accumulators; `nil` for pure counts (int64).
|
|
79
79
|
# @return [BincountND]
|
|
80
80
|
def initialize (lengths:, fiber_shape: [], weights_data_type: nil)
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
raise ArgumentError, "lengths must be a non-empty list" if lengths.empty?
|
|
82
|
+
lengths.each_with_index do |l, k|
|
|
83
|
+
unless l.is_a?(Integer)
|
|
84
|
+
raise TypeError, "lengths[#{k}] must be an Integer (got #{l.inspect})"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
@lengths = lengths.dup
|
|
83
88
|
@lengths.each_with_index do |l, k|
|
|
84
89
|
raise ArgumentError, "lengths[#{k}] must be >= 1" if l < 1
|
|
85
90
|
end
|
|
@@ -209,6 +214,7 @@ class CArray
|
|
|
209
214
|
|
|
210
215
|
if weights
|
|
211
216
|
raise ArgumentError, "weights given but accumulator is unweighted" unless @weighted
|
|
217
|
+
# coerce: the accumulator's type, declared by weights_data_type:
|
|
212
218
|
weights = CArray.wrap_readonly(weights, @counts_data_type)
|
|
213
219
|
expected_w_shape = chunk.shape.dup
|
|
214
220
|
expected_w_shape.delete_at(channel_ax)
|
|
@@ -344,6 +350,10 @@ class CArray
|
|
|
344
350
|
# @return [BincountND]
|
|
345
351
|
def bincount_nd (lengths:, axis: [-2, -1], weights: nil)
|
|
346
352
|
raise ArgumentError, "lengths must be an Array of per-dim extents" unless lengths.is_a?(Array)
|
|
353
|
+
axis = [-2, -1] if axis.nil?
|
|
354
|
+
unless axis.is_a?(Array) && axis.size == 2
|
|
355
|
+
raise ArgumentError, "bincount_nd: axis: is a [sample, channel] pair (got #{axis.inspect})"
|
|
356
|
+
end
|
|
347
357
|
sample_ax = normalize_axis(axis[0], "bincount_nd sample axis")
|
|
348
358
|
channel_ax = normalize_axis(axis[1], "bincount_nd channel axis")
|
|
349
359
|
fiber_shape = shape.dup
|
|
@@ -302,28 +302,47 @@ class CABlockIterator < CAIterator
|
|
|
302
302
|
# @param fill_value [Object, nil] value to put in place of a masked
|
|
303
303
|
# result instead of leaving it masked.
|
|
304
304
|
# @return [CArray] tile-grid shaped
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
kw[:min_count] = min_count unless min_count.nil?
|
|
313
|
-
kw[:fill_value] = fill_value unless fill_value.nil?
|
|
314
|
-
fold(op, **kw)
|
|
305
|
+
CArray.send(:define_named_methods, self,
|
|
306
|
+
[:sum, :accumulate, :prod, :mean, :min, :max, :variance, :stddev,
|
|
307
|
+
:variancep, :stddevp], :fold_reduction)
|
|
308
|
+
|
|
309
|
+
private def fold_reduction (op, min_count: nil, fill_value: nil)
|
|
310
|
+
if FACE_WINNER_INDEX.key?(op) && @source.face?
|
|
311
|
+
return face_winner(FACE_WINNER_INDEX[op], min_count, fill_value)
|
|
315
312
|
end
|
|
313
|
+
kw = {}
|
|
314
|
+
kw[:min_count] = min_count unless min_count.nil?
|
|
315
|
+
kw[:fill_value] = fill_value unless fill_value.nil?
|
|
316
|
+
fold(op, **kw)
|
|
316
317
|
end
|
|
317
318
|
|
|
318
|
-
[:all, :any]
|
|
319
|
-
|
|
319
|
+
CArray.send(:define_named_methods, self, [:all, :any], :fold_with_keywords)
|
|
320
|
+
|
|
321
|
+
private def fold_with_keywords (op, **kw)
|
|
322
|
+
fold(op, **CAIterator.reduce_keywords(op, kw))
|
|
320
323
|
end
|
|
321
324
|
|
|
325
|
+
# @!method first
|
|
326
|
+
# Per-tile first present value, in row-major order within the tile;
|
|
327
|
+
# UNDEF for a tile with none.
|
|
328
|
+
# @return [CArray] tile-grid shaped
|
|
329
|
+
# @!method last
|
|
330
|
+
# Per-tile last present value; UNDEF for a tile with none.
|
|
331
|
+
# @return [CArray] tile-grid shaped
|
|
332
|
+
# @!method nunique
|
|
333
|
+
# Per-tile number of distinct present values (int64); 0 for a tile with
|
|
334
|
+
# none.
|
|
335
|
+
# @return [CArray] tile-grid shaped
|
|
336
|
+
#
|
|
337
|
+
# These take no `min_count:` or `fill_value:`, as the core ones do not.
|
|
338
|
+
CArray.send(:define_named_methods, self, [:first, :last, :nunique], :fold_with_keywords)
|
|
339
|
+
|
|
322
340
|
# A Face's minimum and maximum are cells of the Face, so they are gathered
|
|
323
341
|
# from the source by the winner's address. Assembling the per-region results
|
|
324
342
|
# instead copies the Face's storage into an array that no longer knows it is
|
|
325
343
|
# one: a CATime came back as 8-byte strings.
|
|
326
344
|
FACE_WINNER_INDEX = { min: :min_index, max: :max_index }.freeze
|
|
345
|
+
Ractor.make_shareable(FACE_WINNER_INDEX)
|
|
327
346
|
private_constant :FACE_WINNER_INDEX
|
|
328
347
|
|
|
329
348
|
# ---- count family and elements ----------------------------------------
|
|
@@ -362,11 +381,11 @@ class CABlockIterator < CAIterator
|
|
|
362
381
|
# @return [CArray]
|
|
363
382
|
def count_masked (**kw)
|
|
364
383
|
kw = CAIterator.reduce_keywords(:count_masked, kw)
|
|
365
|
-
need = CArray.
|
|
384
|
+
need = CArray.__present_cells_needed_per_result_cell__(:count_masked, kw[:min_count], 0)
|
|
366
385
|
out = elements - count_not_masked
|
|
367
386
|
# A count of the mask reads every cell of the tile.
|
|
368
387
|
out[elements.lt(need)] = UNDEF if need > 0
|
|
369
|
-
CArray.
|
|
388
|
+
CArray.__fill_undefined_result_cells__(out, kw[:fill_value])
|
|
370
389
|
end
|
|
371
390
|
|
|
372
391
|
# @overload elements
|
|
@@ -434,13 +453,14 @@ class CABlockIterator < CAIterator
|
|
|
434
453
|
# and with `min_count` a tile with too few present cells, is a masked cell.
|
|
435
454
|
def face_winner (idx_op, min_count, fill_value)
|
|
436
455
|
addr = winner_addr(idx_op)
|
|
437
|
-
need = CArray.
|
|
456
|
+
need = CArray.__present_cells_needed_per_result_cell__(idx_op == :min_index ? :min : :max, min_count, 0)
|
|
438
457
|
addr[count_not_masked.lt(need)] = UNDEF if need > 0
|
|
439
458
|
src = @source.reshape(@source.elements)
|
|
440
459
|
empty = addr.is_masked
|
|
441
460
|
return src[addr].copy unless empty.any
|
|
442
|
-
# A masked index
|
|
443
|
-
#
|
|
461
|
+
# A masked index would read as masked, but gathering by one gives the
|
|
462
|
+
# source a mask, so an empty tile takes a placeholder address instead.
|
|
463
|
+
# Pointing it at a masked cell of the source
|
|
444
464
|
# carries the mask through the gather, which a read-only Face (whose copy
|
|
445
465
|
# cannot be written) needs. Otherwise mask the placeholder afterwards.
|
|
446
466
|
if fill_value.nil? && src.has_mask? && (hole = src.is_masked.where).elements > 0
|
|
@@ -474,7 +494,7 @@ class CABlockIterator < CAIterator
|
|
|
474
494
|
out ||= CArray.int64(*@shape)
|
|
475
495
|
out[*out_ranges] = picked
|
|
476
496
|
end
|
|
477
|
-
CArray.
|
|
497
|
+
CArray.__fill_undefined_result_cells__(out || CArray.int64(*@shape), kw[:fill_value])
|
|
478
498
|
end
|
|
479
499
|
|
|
480
500
|
public
|
|
@@ -691,24 +711,81 @@ class CABlockIterator < CAIterator
|
|
|
691
711
|
# @!method cumcount
|
|
692
712
|
# Per-tile running count of present cells (int64), source-shaped.
|
|
693
713
|
# @return [CArray]
|
|
694
|
-
|
|
695
|
-
|
|
714
|
+
CArray.send(:define_named_methods, self,
|
|
715
|
+
[:cumsum, :cumprod, :cummax, :cummin, :cumcount], :scan_tiles)
|
|
716
|
+
|
|
717
|
+
# A scan takes no arguments here.
|
|
718
|
+
private def scan_tiles (op, *args)
|
|
719
|
+
unless args.empty?
|
|
720
|
+
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0)"
|
|
721
|
+
end
|
|
722
|
+
block_scan(op)
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
# @overload lag(n = 1)
|
|
726
|
+
# Per-tile value `n` steps earlier, counting the tile's cells row-major;
|
|
727
|
+
# UNDEF where the tile has none, so a lag never reaches into another
|
|
728
|
+
# tile. A partial edge tile counts only the cells it has.
|
|
729
|
+
# @return [CArray] source-shaped
|
|
730
|
+
def lag (n = 1)
|
|
731
|
+
unless n.is_a?(Integer)
|
|
732
|
+
raise TypeError, "lag: n must be an Integer (got #{n.class})"
|
|
733
|
+
end
|
|
734
|
+
tile_wise { |rows, axis| rows.lag(n, axis: axis) }
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# @overload strip_mask(method:)
|
|
738
|
+
# Per-tile fill of the masked cells, counting the tile's cells row-major:
|
|
739
|
+
# `:forward` / `:ffill` or `:backward` / `:bfill`. A fill never reaches
|
|
740
|
+
# into another tile, so a masked cell with nothing to carry in its tile
|
|
741
|
+
# stays masked.
|
|
742
|
+
# @return [CArray] source-shaped
|
|
743
|
+
def strip_mask (method:)
|
|
744
|
+
CAIterator.hold_backward?(method)
|
|
745
|
+
tile_wise { |rows, axis| rows.strip_mask(method: method, axis: axis) }
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
# @overload order(descending: false, method: :ordinal, kind: :quick)
|
|
749
|
+
# Per-tile rank of each cell among the tile's cells (int64, `0` the
|
|
750
|
+
# smallest), as `CArray#order` ranks them; a partial edge tile ranks only
|
|
751
|
+
# the cells it has. The keywords are those of `CArray#order`.
|
|
752
|
+
# @return [CArray] source-shaped
|
|
753
|
+
def order (**kw)
|
|
754
|
+
tile_wise { |rows, axis| rows.order(axis: axis, **kw) }
|
|
696
755
|
end
|
|
697
756
|
|
|
698
757
|
private
|
|
699
758
|
|
|
759
|
+
# A source-shaped result built tile by tile: the block gets a region's
|
|
760
|
+
# tiles as rows (one tile's cells, row-major, along +axis+) and gives back
|
|
761
|
+
# rows of the same shape. Each region has whole tiles of one size, so a
|
|
762
|
+
# partial edge tile is its own cells rather than cells and padding.
|
|
763
|
+
def tile_wise
|
|
764
|
+
out = nil
|
|
765
|
+
each_region do |strip_ranges, tiles, _out_ranges|
|
|
766
|
+
view = @source[*strip_ranges].block_view(*tiles)
|
|
767
|
+
grid = view.shape[0, @sndim]
|
|
768
|
+
found = yield(view.copy.reshape(*grid, tiles.inject(1, :*)), @sndim)
|
|
769
|
+
.reshape(*grid, *tiles)
|
|
770
|
+
out ||= tile_output(found)
|
|
771
|
+
out[*strip_ranges].block_view(*tiles)[] = tile_values(out, found)
|
|
772
|
+
end
|
|
773
|
+
# a source with no cells has no region; its one-cell rows give the type
|
|
774
|
+
out || yield(@source.copy.reshape(*@source.shape, 1), @sndim).reshape(*@source.shape)
|
|
775
|
+
end
|
|
776
|
+
|
|
700
777
|
# Drive a within-tile segment scan: scan each tile flattened row-major and
|
|
701
778
|
# scatter back to a source-shaped result. The output data type is taken from the
|
|
702
779
|
# first tile's scan (all tiles share it); the padded margin is UNDEF and the
|
|
703
780
|
# OOB cells of a partial edge tile are cropped from the result.
|
|
704
|
-
def block_scan (op)
|
|
781
|
+
def block_scan (op, *args)
|
|
705
782
|
tgv = tile_grid_view
|
|
706
783
|
nils = Array.new(@sndim, nil)
|
|
707
784
|
pout = nil
|
|
708
785
|
pgv = nil
|
|
709
786
|
CArray.each_index(*@shape) do |*g|
|
|
710
787
|
tile = tgv[*g, *nils]
|
|
711
|
-
scanned = tile.copy.reshape(tile.elements).public_send(op).reshape(*@sizes)
|
|
788
|
+
scanned = tile.copy.reshape(tile.elements).public_send(op, *args).reshape(*@sizes)
|
|
712
789
|
unless pout
|
|
713
790
|
pout = CArray.new(scanned.data_type, padded_source.shape, bytes: scanned.bytes)
|
|
714
791
|
pout[] = UNDEF
|
|
@@ -719,6 +796,24 @@ class CABlockIterator < CAIterator
|
|
|
719
796
|
pout[*@sndim.times.map { |i| 0...@source.shape[i] }].copy
|
|
720
797
|
end
|
|
721
798
|
|
|
799
|
+
# A source-shaped array that can hold what #tile_wise gives: of its kind,
|
|
800
|
+
# or an object array for a read-only Face, which cannot be written cell by
|
|
801
|
+
# cell.
|
|
802
|
+
def tile_output (lagged)
|
|
803
|
+
if lagged.face? && lagged.read_only?
|
|
804
|
+
CArray.object(*@source.shape)
|
|
805
|
+
else
|
|
806
|
+
lagged.__send__(:blank_like, @source.shape)
|
|
807
|
+
end
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
# The values as +out+ can hold them: a Face going into an object array
|
|
811
|
+
# goes as its surface values, not its storage.
|
|
812
|
+
def tile_values (out, lagged)
|
|
813
|
+
return lagged unless out.data_type == CA_OBJECT && lagged.face?
|
|
814
|
+
CArray.object(*lagged.shape) { lagged.to_a }
|
|
815
|
+
end
|
|
816
|
+
|
|
722
817
|
# A padded entity whose interior holds the source and whose partial-edge
|
|
723
818
|
# margin is masked (UNDEF), sized to exactly cover the ceil tile grid. When
|
|
724
819
|
# the source already divides evenly this is the source itself (no copy), so
|
|
@@ -729,7 +824,7 @@ class CABlockIterator < CAIterator
|
|
|
729
824
|
if pshape == @source.shape.to_a
|
|
730
825
|
@source
|
|
731
826
|
else
|
|
732
|
-
pad =
|
|
827
|
+
pad = @source.__send__(:blank_like, pshape) # a Face stays one
|
|
733
828
|
pad[] = UNDEF
|
|
734
829
|
pad[*@sndim.times.map { |i| 0...@source.shape[i] }] = @source
|
|
735
830
|
pad
|
|
@@ -742,6 +837,14 @@ class CABlockIterator < CAIterator
|
|
|
742
837
|
def tile_grid_view
|
|
743
838
|
@tile_grid_view ||= padded_source.block_view(*@sizes)
|
|
744
839
|
end
|
|
840
|
+
|
|
841
|
+
# lag and strip_mask(method:) give the source's values moved or held in
|
|
842
|
+
# place, so the result shows the source's attributes, as CArray#lag and
|
|
843
|
+
# CArray#strip_mask(method:) do.
|
|
844
|
+
prepend(Module.new do
|
|
845
|
+
def lag (*args, **keywords) = @source.__send__(:carry_attrs, super)
|
|
846
|
+
def strip_mask (**keywords) = @source.__send__(:carry_attrs, super)
|
|
847
|
+
end)
|
|
745
848
|
end
|
|
746
849
|
|
|
747
850
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Boolean folds all / any / none gain a `skip_masked:` keyword.
|
|
2
2
|
#
|
|
3
|
-
# skip_masked: true (default) -- available-case
|
|
3
|
+
# skip_masked: true (default) -- available-case: masked cells are
|
|
4
4
|
# ignored, the result is always true/false.
|
|
5
5
|
# This is the existing behavior; every other
|
|
6
6
|
# keyword (axis:, keep_axis:, ...) forwards
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
# false < unknown < true).
|
|
15
15
|
#
|
|
16
16
|
# The Kleene fold composes from count(true) / count(false) / count_masked, so
|
|
17
|
-
# there is no dedicated kernel; the C reductions
|
|
18
|
-
#
|
|
17
|
+
# there is no dedicated kernel; the C reductions that skip masked cells stay
|
|
18
|
+
# reachable as the private `*_skipping_masked` aliases.
|
|
19
19
|
|
|
20
20
|
class CArray
|
|
21
21
|
|
|
22
|
-
alias_method :
|
|
23
|
-
alias_method :
|
|
24
|
-
alias_method :
|
|
25
|
-
private :
|
|
22
|
+
alias_method :any_skipping_masked, :any
|
|
23
|
+
alias_method :all_skipping_masked, :all
|
|
24
|
+
alias_method :none_skipping_masked, :none
|
|
25
|
+
private :any_skipping_masked, :all_skipping_masked, :none_skipping_masked
|
|
26
26
|
|
|
27
27
|
# Whether any cell is true.
|
|
28
28
|
#
|
|
@@ -36,8 +36,8 @@ class CArray
|
|
|
36
36
|
# `keep_axis:`, ...).
|
|
37
37
|
# @return [Boolean, CArray] a scalar, or an array when an axis is given.
|
|
38
38
|
def any (skip_masked: true, **opts)
|
|
39
|
-
return
|
|
40
|
-
|
|
39
|
+
return any_skipping_masked(**opts) if skip_masked
|
|
40
|
+
kleene_fold(:any, opts)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# Whether every cell is true.
|
|
@@ -52,8 +52,8 @@ class CArray
|
|
|
52
52
|
# `keep_axis:`, ...).
|
|
53
53
|
# @return [Boolean, CArray] a scalar, or an array when an axis is given.
|
|
54
54
|
def all (skip_masked: true, **opts)
|
|
55
|
-
return
|
|
56
|
-
|
|
55
|
+
return all_skipping_masked(**opts) if skip_masked
|
|
56
|
+
kleene_fold(:all, opts)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
# Whether no cell is true.
|
|
@@ -68,8 +68,8 @@ class CArray
|
|
|
68
68
|
# `keep_axis:`, ...).
|
|
69
69
|
# @return [Boolean, CArray] a scalar, or an array when an axis is given.
|
|
70
70
|
def none (skip_masked: true, **opts)
|
|
71
|
-
return
|
|
72
|
-
|
|
71
|
+
return none_skipping_masked(**opts) if skip_masked
|
|
72
|
+
kleene_fold(:none, opts)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
private
|
|
@@ -79,11 +79,11 @@ class CArray
|
|
|
79
79
|
# any, a false for all, a true for none); elsewhere a masked cell leaves the
|
|
80
80
|
# answer undetermined. Going through that reduction checks the data type,
|
|
81
81
|
# the axes and the keywords exactly as `skip_masked: true` does.
|
|
82
|
-
def
|
|
82
|
+
def kleene_fold (kind, opts)
|
|
83
83
|
r = case kind
|
|
84
|
-
when :any then
|
|
85
|
-
when :all then
|
|
86
|
-
else
|
|
84
|
+
when :any then any_skipping_masked(**opts)
|
|
85
|
+
when :all then all_skipping_masked(**opts)
|
|
86
|
+
else none_skipping_masked(**opts)
|
|
87
87
|
end
|
|
88
88
|
cm = count_masked(**opts)
|
|
89
89
|
if r.is_a?(CArray)
|
data/lib/carray/categorical.rb
CHANGED
|
@@ -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.
|
|
268
|
+
super(CA_FIXLEN, codes.shape,
|
|
239
269
|
bytes: codes.bytes,
|
|
240
270
|
storage: codes.data_type,
|
|
241
271
|
parent: codes,
|
|
@@ -309,14 +339,6 @@ 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
|
|
|
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
342
|
# @overload resize(*newdim, fill_value: UNDEF)
|
|
321
343
|
# Returns a categorical resized to `newdim` (see {CArray#resize}).
|
|
322
344
|
# The new cells are excluded (UNDEF) unless `fill_value` names one of
|
|
@@ -338,16 +360,28 @@ class CACategorical < CAObject
|
|
|
338
360
|
CACategorical.from_codes(codes.insert_block(offset, bsize) { fill }, labels)
|
|
339
361
|
end
|
|
340
362
|
|
|
363
|
+
# Boolean mask of cells whose category == label. Excluded cells stay UNDEF
|
|
364
|
+
# (their category is unknown); an unknown label yields an all-false mask.
|
|
365
|
+
# @overload eq(label)
|
|
366
|
+
# Returns a boolean CArray marking cells whose category equals
|
|
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.
|
|
373
|
+
# @return [CArray]
|
|
341
374
|
def eq(label)
|
|
342
|
-
codes.eq(
|
|
375
|
+
codes.eq(codes_of(label))
|
|
343
376
|
end
|
|
344
377
|
|
|
345
378
|
# @overload ne(label)
|
|
346
379
|
# Returns the complement of {#eq}.
|
|
347
|
-
# @param label [Object] category to compare against
|
|
380
|
+
# @param label [Object, CArray] category to compare against, or one per
|
|
381
|
+
# cell.
|
|
348
382
|
# @return [CArray]
|
|
349
383
|
def ne(label)
|
|
350
|
-
codes.ne(
|
|
384
|
+
codes.ne(codes_of(label))
|
|
351
385
|
end
|
|
352
386
|
|
|
353
387
|
# @overload count(label)
|
|
@@ -446,6 +480,7 @@ class CACategorical < CAObject
|
|
|
446
480
|
# @param values [Array, CArray, CACategorical] labels to test against.
|
|
447
481
|
# @return [CArray]
|
|
448
482
|
def is_in (values)
|
|
483
|
+
refuse_range_set(values, "is_in")
|
|
449
484
|
wanted = label_list(values).filter_map { |l| @labels.index(l) }
|
|
450
485
|
codes.is_in(CArray.int64(wanted.size) { |i| wanted[i] })
|
|
451
486
|
end
|
|
@@ -453,18 +488,21 @@ class CACategorical < CAObject
|
|
|
453
488
|
# @overload intersection(other, sort: false)
|
|
454
489
|
# @return [CArray] object CArray of the labels present in both.
|
|
455
490
|
def intersection (other, sort: false)
|
|
491
|
+
refuse_range_set(other, "intersection")
|
|
456
492
|
unique.intersection(label_array(other), sort: sort)
|
|
457
493
|
end
|
|
458
494
|
|
|
459
495
|
# @overload difference(other, sort: false)
|
|
460
496
|
# @return [CArray] object CArray of the labels only `self` has.
|
|
461
497
|
def difference (other, sort: false)
|
|
498
|
+
refuse_range_set(other, "difference")
|
|
462
499
|
unique.difference(label_array(other), sort: sort)
|
|
463
500
|
end
|
|
464
501
|
|
|
465
502
|
# @overload union(other, sort: false)
|
|
466
503
|
# @return [CArray] object CArray of the labels of either side.
|
|
467
504
|
def union (other, sort: false)
|
|
505
|
+
refuse_range_set(other, "union")
|
|
468
506
|
unique.union(label_array(other), sort: sort)
|
|
469
507
|
end
|
|
470
508
|
|
|
@@ -484,6 +522,24 @@ class CACategorical < CAObject
|
|
|
484
522
|
# that has none. Every code-to-label path goes through here rather than
|
|
485
523
|
# indexing @labels itself, because two decodes are two chances to disagree,
|
|
486
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
|
+
|
|
487
543
|
# The code a fill value stands for: UNDEF stays UNDEF, a label is its code.
|
|
488
544
|
def code_for_fill (value)
|
|
489
545
|
return UNDEF if UNDEF.equal?(value)
|
|
@@ -564,35 +620,17 @@ class CACategorical < CAObject
|
|
|
564
620
|
# `value.reshape(elements)[cat.sort_addr]` produces the contiguous blocks.
|
|
565
621
|
# Memoised (see the grouping-plan note above): the counting sort is the
|
|
566
622
|
# dominant grouping cost, so it is computed once and shared by every payload
|
|
567
|
-
# column and by CACategoricalIterator
|
|
623
|
+
# column and by CACategoricalIterator, and kept for the categorical's
|
|
624
|
+
# lifetime (an int64 per cell). Do not mutate the returned array.
|
|
568
625
|
# @overload sort_addr
|
|
569
626
|
# Returns a length-{#elements} integer CArray of flat storage
|
|
570
627
|
# addresses that order the cells by category (excluded cells last).
|
|
571
628
|
# @return [CArray]
|
|
572
629
|
def sort_addr
|
|
573
630
|
return @sort_addr if @sort_addr
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
|
631
|
+
out = codes.reshape(elements).__send__(:addresses_by_category,
|
|
632
|
+
reduceat_index.copy, @labels.size,
|
|
633
|
+
category_sizes.sum)
|
|
596
634
|
@sort_addr = out
|
|
597
635
|
out
|
|
598
636
|
end
|
|
@@ -630,6 +668,75 @@ class CACategorical < CAObject
|
|
|
630
668
|
CACategorical.from_codes(recoded.codes, recoded.labels.map { |c| label_at(c) })
|
|
631
669
|
end
|
|
632
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
|
|
710
|
+
k = @labels.size
|
|
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]
|
|
719
|
+
end
|
|
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
|
|
737
|
+
out
|
|
738
|
+
end
|
|
739
|
+
|
|
633
740
|
# @overload inspect
|
|
634
741
|
# Returns a short summary showing element count, category count,
|
|
635
742
|
# and vocabulary.
|
|
@@ -667,14 +774,11 @@ class CArray
|
|
|
667
774
|
# duplicates.
|
|
668
775
|
# @raise [CArray::DataTypeError] when `sort_labels: true` discovers
|
|
669
776
|
# the labels of a complex array, which have no order.
|
|
670
|
-
# The order sort_labels: puts two labels in:
|
|
671
|
-
#
|
|
672
|
-
#
|
|
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.
|
|
673
780
|
private def label_order(a, b)
|
|
674
|
-
|
|
675
|
-
nb = b.is_a?(Float) && b.nan?
|
|
676
|
-
return (na ? 1 : 0) - (nb ? 1 : 0) if na || nb
|
|
677
|
-
c = a <=> b
|
|
781
|
+
c = CArray.send(:object_order_cmp, a, b)
|
|
678
782
|
if c.nil?
|
|
679
783
|
raise ArgumentError,
|
|
680
784
|
"categorize(sort_labels: true): cannot order the labels " \
|
|
@@ -706,7 +810,13 @@ class CArray
|
|
|
706
810
|
# Sort the k levels as Ruby sorts them, then renumber every code through
|
|
707
811
|
# the rank in one gather. The sentinel of an excluded cell is out of the
|
|
708
812
|
# rank's range, so project makes it UNDEF and from_codes writes it back.
|
|
709
|
-
|
|
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
|
|
710
820
|
rank = CArray.new(codes.data_type, [levels.size])
|
|
711
821
|
order.each_with_index { |old, new_code| rank[old] = new_code }
|
|
712
822
|
return CACategorical.from_codes(rank.project(codes), order.map { |i| levels[i] })
|
|
@@ -744,3 +854,15 @@ class CArray
|
|
|
744
854
|
CACategorical.from_codes(codes, labels_arr)
|
|
745
855
|
end
|
|
746
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
|