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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +568 -0
- data/README.md +6 -6
- data/carray.gemspec +1 -1
- data/ext/ca_axis_descriptor.h +27 -24
- data/ext/ca_axis_dispatch.c +220 -0
- data/ext/ca_axis_group.c +201 -123
- data/ext/ca_binop_dispatch.c +46 -52
- data/ext/ca_binop_dispatch.h +1 -1
- data/ext/ca_categorical_iterator.c +249 -183
- data/ext/ca_compare.c +7 -12
- data/ext/ca_for_buffer.h +21 -68
- data/ext/ca_group_iter.c +38 -23
- data/ext/ca_kernel_iterator.c +1765 -2076
- data/ext/ca_kernel_iterator.h +584 -566
- data/ext/ca_monop_dispatch.c +86 -63
- data/ext/ca_monop_dispatch.h +5 -0
- data/ext/ca_obj_array.c +223 -56
- data/ext/ca_obj_bincmp.c +172 -138
- data/ext/ca_obj_binop.c +116 -230
- data/ext/ca_obj_bitarray.c +40 -26
- data/ext/ca_obj_bitfield.c +100 -68
- data/ext/ca_obj_block.c +19 -9
- data/ext/ca_obj_byte_swap.c +45 -46
- data/ext/ca_obj_const_string.c +269 -44
- data/ext/ca_obj_face.c +612 -89
- data/ext/ca_obj_face.h +111 -47
- data/ext/ca_obj_fake.c +36 -79
- data/ext/ca_obj_farray.c +7 -3
- data/ext/ca_obj_field.c +33 -8
- data/ext/ca_obj_fixlen_string.c +23 -24
- data/ext/ca_obj_grid.c +541 -210
- data/ext/ca_obj_meld.c +331 -111
- data/ext/ca_obj_moncmp.c +80 -65
- data/ext/ca_obj_monop.c +177 -149
- data/ext/ca_obj_object.c +664 -248
- data/ext/ca_obj_record.c +63 -23
- data/ext/ca_obj_reduce.c +18 -16
- data/ext/ca_obj_refer.c +140 -58
- data/ext/ca_obj_remap.c +85 -24
- data/ext/ca_obj_repeat.c +24 -9
- data/ext/ca_obj_roll.c +121 -16
- data/ext/ca_obj_select.c +61 -58
- data/ext/ca_obj_select_axis.c +73 -107
- data/ext/ca_obj_shift.c +32 -102
- data/ext/ca_obj_stack.c +372 -122
- data/ext/ca_obj_stride.c +348 -140
- data/ext/ca_obj_string.c +88 -22
- data/ext/ca_obj_tile.c +38 -21
- data/ext/ca_obj_time.c +45 -31
- data/ext/ca_obj_timedelta.c +43 -29
- data/ext/ca_obj_transpose.c +17 -6
- data/ext/ca_obj_triop.c +125 -141
- data/ext/ca_obj_window.c +330 -44
- data/ext/ca_op_ipower.c +24 -11
- data/ext/ca_op_powi.h +57 -30
- data/ext/ca_pow5_table.h +656 -0
- data/ext/ca_sort_kernels.h +70 -0
- data/ext/ca_sweep_engine.c +104 -81
- data/ext/ca_sweep_engine.h +7 -20
- data/ext/ca_triop_dispatch.c +4 -1
- data/ext/ca_triop_dispatch.h +5 -2
- data/ext/caframe_csv_reader.c +542 -0
- data/ext/carray.h +241 -92
- data/ext/carray_access.c +608 -276
- data/ext/carray_address_basis.c +494 -97
- data/ext/carray_attribute.c +129 -4
- data/ext/carray_bincount.c +39 -7
- data/ext/carray_broadcast.c +56 -19
- data/ext/carray_build_flags.h +1 -1
- data/ext/carray_call_cfunc.c +108 -28
- data/ext/carray_cast.c +1576 -223
- data/ext/carray_cast_func.rb +77 -24
- data/ext/carray_class.c +4 -3
- data/ext/carray_conversion.c +208 -75
- data/ext/carray_copy.c +82 -24
- data/ext/carray_core.c +646 -162
- data/ext/carray_count.c +171 -88
- data/ext/carray_element.c +94 -60
- data/ext/carray_factorize.c +670 -324
- data/ext/carray_generate.c +260 -83
- data/ext/carray_histogram.c +209 -106
- data/ext/carray_hold.c +14 -16
- data/ext/carray_index_classifier.c +422 -243
- data/ext/carray_internal.h +692 -3
- data/ext/carray_kernels_bincmp.c +7 -0
- data/ext/carray_kernels_binop.c +487 -120
- data/ext/carray_kernels_init.c +653 -50
- data/ext/carray_kernels_moncmp.c +15 -8
- data/ext/carray_kernels_monop.c +99 -43
- data/ext/carray_kernels_reduce_aggregate.c +3925 -6112
- data/ext/carray_kernels_reduce_boolean.c +112 -36
- data/ext/carray_kernels_reduce_cumulative.c +2127 -6903
- data/ext/carray_kernels_reduce_extreme.c +3032 -2892
- data/ext/carray_kernels_reduce_variance.c +439 -500
- data/ext/carray_kernels_scan.c +1097 -488
- data/ext/carray_kernels_search.c +3787 -2176
- data/ext/carray_kernels_sort.c +1200 -920
- data/ext/carray_kernels_triop.c +479 -7
- data/ext/carray_key_windows.c +216 -0
- data/ext/carray_lazy.c +967 -153
- data/ext/carray_loop.c +81 -65
- data/ext/carray_mask.c +910 -324
- data/ext/carray_math_kernel.h +1 -0
- data/ext/carray_median_percentile.c +299 -100
- data/ext/carray_memory_view.c +872 -758
- data/ext/carray_operator.c +1171 -364
- data/ext/carray_order.c +505 -133
- data/ext/carray_partition.c +112 -65
- data/ext/carray_random.c +327 -239
- data/ext/carray_round.c +273 -0
- data/ext/carray_scatter.c +346 -78
- data/ext/carray_segment.c +257 -0
- data/ext/carray_slab.c +249 -86
- data/ext/carray_sort.c +224 -227
- data/ext/carray_sort_kernel.c +8 -1
- data/ext/carray_struct.c +118 -39
- data/ext/carray_test.c +193 -69
- data/ext/carray_time_text.c +1182 -0
- data/ext/carray_undef.c +4 -0
- data/ext/carray_utils.c +579 -113
- data/ext/extconf.rb +27 -2
- data/ext/mk_call_cfunc.rb +19 -4
- data/ext/mk_pow5_table.rb +35 -0
- data/ext/mkkernel.rb +1202 -1014
- data/ext/ruby_carray.c +17 -5
- data/ext/version.h +4 -4
- data/ext/version.rb +4 -2
- data/lib/carray/attribute.rb +160 -17
- data/lib/carray/autoload_carray.rb +23 -27
- data/lib/carray/autoload_method_extension.rb +20 -5
- data/lib/carray/axis_group.rb +360 -63
- data/lib/carray/basics.rb +162 -28
- data/lib/carray/bincount_nd.rb +18 -2
- data/lib/carray/block_iterator.rb +231 -58
- data/lib/carray/boolean_reduce.rb +34 -42
- data/lib/carray/categorical.rb +242 -52
- data/lib/carray/categorical_iterator.rb +437 -827
- data/lib/carray/complex.rb +41 -34
- data/lib/carray/conditional.rb +150 -14
- data/lib/carray/const_string.rb +59 -4
- 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/concat.rb +157 -6
- data/lib/carray/frame/convert.rb +82 -7
- data/lib/carray/frame/csv_parser.rb +688 -87
- data/lib/carray/frame/describe.rb +80 -0
- data/lib/carray/frame/frame.rb +369 -45
- data/lib/carray/frame/group.rb +631 -90
- data/lib/carray/frame/io.rb +506 -79
- data/lib/carray/frame/join.rb +411 -62
- data/lib/carray/frame/records.rb +116 -19
- data/lib/carray/frame/reshape.rb +455 -0
- data/lib/carray/frame/sort.rb +30 -18
- data/lib/carray/frame/verbs.rb +868 -61
- data/lib/carray/frame/windows.rb +117 -0
- data/lib/carray/frame.rb +4 -0
- data/lib/carray/fuse_source.rb +118 -2
- data/lib/carray/fusion.rb +257 -42
- data/lib/carray/histogram.rb +6 -1
- data/lib/carray/inspect.rb +54 -13
- data/lib/carray/iterator.rb +99 -9
- data/lib/carray/key_windows.rb +620 -0
- data/lib/carray/lazy.rb +216 -193
- data/lib/carray/mask_gap_fill.rb +112 -92
- data/lib/carray/math.rb +32 -19
- data/lib/carray/meld_reduce.rb +52 -21
- data/lib/carray/methods/align_addr.rb +8 -4
- data/lib/carray/methods/bin.rb +26 -18
- data/lib/carray/methods/bincount.rb +18 -2
- data/lib/carray/methods/bit_string.rb +15 -4
- data/lib/carray/methods/choose.rb +16 -3
- data/lib/carray/methods/composition.rb +18 -10
- data/lib/carray/methods/factorize.rb +2 -2
- data/lib/carray/methods/gather_nd.rb +25 -5
- data/lib/carray/methods/insert_block.rb +9 -14
- data/lib/carray/methods/is_between.rb +59 -0
- data/lib/carray/methods/is_in.rb +140 -10
- data/lib/carray/methods/join.rb +2 -7
- data/lib/carray/methods/lag.rb +49 -0
- data/lib/carray/methods/locate_addr.rb +182 -45
- data/lib/carray/methods/mode.rb +18 -1
- data/lib/carray/methods/nunique.rb +11 -6
- data/lib/carray/methods/repeat.rb +1 -20
- data/lib/carray/methods/resize.rb +21 -23
- data/lib/carray/methods/snap.rb +34 -12
- data/lib/carray/methods/unique.rb +11 -2
- data/lib/carray/methods/value_counts.rb +4 -11
- data/lib/carray/record_order.rb +208 -0
- data/lib/carray/rng.rb +3 -0
- data/lib/carray/runtime.rb +65 -17
- data/lib/carray/segment_iterator.rb +1221 -0
- data/lib/carray/serialize.rb +214 -30
- data/lib/carray/slab_iterator.rb +101 -27
- data/lib/carray/stack.rb +10 -4
- data/lib/carray/string.rb +105 -5
- data/lib/carray/string_operation_extension.rb +12 -4
- data/lib/carray/struct.rb +137 -58
- data/lib/carray/struct_builder.rb +124 -17
- data/lib/carray/time.rb +1222 -186
- data/lib/carray/window_iterator.rb +242 -81
- data/lib/carray.rb +9 -2
- data/yard-stubs/ca_obj_array.rb +4 -4
- data/yard-stubs/ca_obj_bitarray.rb +7 -0
- data/yard-stubs/ca_obj_bitfield.rb +23 -12
- data/yard-stubs/ca_obj_fake.rb +1 -1
- data/yard-stubs/ca_obj_field.rb +5 -1
- data/yard-stubs/ca_obj_grid.rb +21 -10
- data/yard-stubs/ca_obj_refer.rb +5 -4
- data/yard-stubs/ca_obj_roll.rb +2 -3
- data/yard-stubs/ca_obj_shift.rb +5 -1
- data/yard-stubs/ca_obj_stride.rb +10 -7
- data/yard-stubs/ca_obj_tile.rb +1 -1
- data/yard-stubs/ca_obj_transpose.rb +1 -1
- data/yard-stubs/ca_obj_window.rb +9 -5
- data/yard-stubs/carray_access.rb +11 -5
- data/yard-stubs/carray_attribute.rb +0 -5
- data/yard-stubs/carray_broadcast.rb +1 -1
- data/yard-stubs/carray_cast.rb +88 -43
- data/yard-stubs/carray_conversion.rb +15 -2
- data/yard-stubs/carray_copy.rb +12 -5
- data/yard-stubs/carray_core.rb +3 -3
- data/yard-stubs/carray_count.rb +9 -0
- data/yard-stubs/carray_element.rb +10 -4
- data/yard-stubs/carray_generate.rb +21 -1
- data/yard-stubs/carray_lazy.rb +5 -1
- data/yard-stubs/carray_loop.rb +4 -1
- data/yard-stubs/carray_mask.rb +60 -12
- data/yard-stubs/carray_math.rb +52 -14
- data/yard-stubs/carray_median_percentile.rb +15 -8
- data/yard-stubs/carray_memory_view.rb +40 -23
- data/yard-stubs/carray_order.rb +61 -29
- data/yard-stubs/carray_random.rb +13 -4
- data/yard-stubs/carray_scatter.rb +21 -9
- data/yard-stubs/carray_segment.rb +72 -0
- data/yard-stubs/carray_slab.rb +10 -2
- data/yard-stubs/carray_sort.rb +49 -8
- data/yard-stubs/carray_test.rb +15 -3
- data/yard-stubs/carray_utils.rb +0 -26
- metadata +19 -5
- data/ext/ca_for_each_element.h +0 -239
- data/ext/carray_kernels_map.c +0 -3467
- data/lib/carray/arrow_tensor.rb +0 -401
- data/lib/carray/table.rb +0 -108
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,
|
|
@@ -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
|
|
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`.
|
|
317
|
-
#
|
|
318
|
-
#
|
|
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(
|
|
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(
|
|
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 @
|
|
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
|
-
@
|
|
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
|
|
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 @
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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 @
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
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).
|
|
640
|
-
#
|
|
641
|
-
#
|
|
642
|
-
if labels.nil? &&
|
|
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
|
-
|
|
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
|