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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +469 -0
- data/README.md +3 -2
- data/carray.gemspec +1 -1
- data/ext/ca_array_pool.c +25 -1
- data/ext/ca_axis_descriptor.h +27 -24
- data/ext/ca_axis_dispatch.c +14 -26
- 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 +231 -153
- data/ext/ca_compare.c +9 -20
- data/ext/ca_composite_dispatch.c +9 -9
- data/ext/ca_group_iter.c +16 -17
- data/ext/ca_kernel_iterator.c +1240 -2000
- data/ext/ca_kernel_iterator.h +502 -519
- data/ext/ca_monop_dispatch.c +82 -59
- data/ext/ca_monop_dispatch.h +1 -0
- data/ext/ca_obj_array.c +152 -45
- data/ext/ca_obj_bincmp.c +25 -21
- data/ext/ca_obj_binop.c +16 -26
- data/ext/ca_obj_bitarray.c +8 -3
- data/ext/ca_obj_bitfield.c +63 -49
- data/ext/ca_obj_block.c +18 -9
- data/ext/ca_obj_byte_swap.c +7 -7
- data/ext/ca_obj_const_string.c +188 -22
- data/ext/ca_obj_face.c +280 -58
- 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 +30 -8
- data/ext/ca_obj_fixlen_string.c +2 -3
- data/ext/ca_obj_grid.c +455 -134
- data/ext/ca_obj_meld.c +172 -34
- data/ext/ca_obj_moncmp.c +23 -8
- data/ext/ca_obj_monop.c +113 -67
- data/ext/ca_obj_object.c +210 -52
- data/ext/ca_obj_record.c +15 -7
- data/ext/ca_obj_reduce.c +17 -8
- data/ext/ca_obj_refer.c +42 -35
- data/ext/ca_obj_remap.c +30 -9
- data/ext/ca_obj_repeat.c +9 -10
- data/ext/ca_obj_roll.c +31 -10
- data/ext/ca_obj_select.c +48 -14
- data/ext/ca_obj_select_axis.c +15 -47
- data/ext/ca_obj_shift.c +41 -7
- data/ext/ca_obj_stack.c +180 -34
- data/ext/ca_obj_stride.c +190 -134
- data/ext/ca_obj_string.c +80 -4
- data/ext/ca_obj_tile.c +32 -11
- data/ext/ca_obj_time.c +22 -6
- data/ext/ca_obj_timedelta.c +22 -6
- data/ext/ca_obj_transpose.c +13 -7
- data/ext/ca_obj_triop.c +26 -32
- data/ext/ca_obj_window.c +267 -74
- data/ext/ca_op_ipower.c +4 -4
- data/ext/ca_sort_kernels.h +6 -3
- data/ext/ca_sweep_engine.c +2 -2
- data/ext/caframe_csv_reader.c +80 -1
- data/ext/carray.h +214 -75
- data/ext/carray_access.c +200 -99
- data/ext/carray_address_basis.c +343 -87
- data/ext/carray_attribute.c +113 -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 +375 -96
- data/ext/carray_cast_func.rb +90 -20
- data/ext/carray_class.c +4 -3
- data/ext/carray_config.h +28 -0
- data/ext/carray_conversion.c +18 -9
- data/ext/carray_copy.c +51 -14
- data/ext/carray_core.c +150 -55
- data/ext/carray_count.c +56 -20
- data/ext/carray_element.c +81 -33
- data/ext/carray_factorize.c +138 -84
- data/ext/carray_generate.c +199 -52
- data/ext/carray_histogram.c +63 -18
- data/ext/carray_hold.c +12 -15
- data/ext/carray_index_classifier.c +388 -176
- data/ext/carray_internal.h +348 -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 +10897 -0
- data/ext/carray_kernels_triop.c +1864 -0
- data/ext/carray_key_windows.c +220 -0
- data/ext/carray_lazy.c +170 -111
- data/ext/carray_mask.c +337 -94
- data/ext/carray_median_percentile.c +82 -28
- data/ext/carray_memory_view.c +669 -561
- data/ext/carray_operator.c +129 -52
- data/ext/carray_order.c +604 -77
- data/ext/carray_partition.c +28 -27
- data/ext/carray_random.c +85 -31
- data/ext/carray_round.c +273 -0
- data/ext/carray_scatter.c +87 -45
- data/ext/carray_segment.c +3 -3
- data/ext/carray_slab.c +123 -37
- data/ext/carray_sort.c +259 -51
- data/ext/carray_sort_kernel.c +5 -7
- data/ext/carray_struct.c +54 -61
- data/ext/carray_test.c +26 -5
- data/ext/carray_time_text.c +4 -2
- data/ext/carray_undef.c +4 -0
- data/ext/carray_utils.c +309 -66
- data/ext/mk_call_cfunc.rb +19 -2
- data/ext/mkkernel.rb +403 -350
- data/ext/ruby_carray.c +10 -4
- data/ext/version.h +4 -4
- data/lib/carray/attribute.rb +139 -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 +105 -17
- 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 +195 -59
- data/lib/carray/categorical_iterator.rb +132 -29
- data/lib/carray/complex.rb +5 -5
- data/lib/carray/conditional.rb +67 -18
- data/lib/carray/const_string.rb +24 -5
- data/lib/carray/construct.rb +15 -11
- data/lib/carray/data_type_extension.rb +9 -1
- data/lib/carray/distance_limit.rb +77 -0
- data/lib/carray/fixlen_string.rb +48 -3
- data/lib/carray/frame/column_verbs.rb +71 -0
- data/lib/carray/frame/concat.rb +17 -3
- 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 +252 -28
- data/lib/carray/frame/group.rb +603 -101
- data/lib/carray/frame/io.rb +117 -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 +42 -6
- 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 +217 -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 +61 -37
- 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 +323 -88
- data/lib/carray/serialize.rb +45 -14
- data/lib/carray/slab_iterator.rb +50 -0
- data/lib/carray/stack.rb +15 -12
- data/lib/carray/string.rb +113 -5
- data/lib/carray/string_operation_extension.rb +21 -6
- data/lib/carray/struct.rb +9 -21
- data/lib/carray/struct_builder.rb +24 -11
- data/lib/carray/time.rb +819 -158
- 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_transpose.rb +5 -4
- data/yard-stubs/ca_obj_window.rb +8 -3
- data/yard-stubs/carray_access.rb +6 -1
- data/yard-stubs/carray_attribute.rb +3 -6
- data/yard-stubs/carray_cast.rb +12 -0
- data/yard-stubs/carray_conversion.rb +7 -2
- data/yard-stubs/carray_copy.rb +4 -2
- data/yard-stubs/carray_count.rb +4 -4
- data/yard-stubs/carray_element.rb +17 -10
- data/yard-stubs/carray_generate.rb +21 -1
- data/yard-stubs/carray_mask.rb +32 -3
- data/yard-stubs/carray_math.rb +41 -5
- data/yard-stubs/carray_median_percentile.rb +5 -2
- data/yard-stubs/carray_memory_view.rb +40 -23
- data/yard-stubs/carray_order.rb +129 -22
- data/yard-stubs/carray_random.rb +13 -4
- data/yard-stubs/carray_scatter.rb +17 -10
- 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/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,
|
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 {#
|
|
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 =
|
|
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 —
|
|
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)[
|
|
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 —
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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
|
|
568
|
-
#
|
|
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
|
|
573
|
-
return @
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
# {#
|
|
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:
|
|
671
|
-
#
|
|
672
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|