carray 3.0.1 → 3.0.3
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 +824 -0
- data/README.md +5 -6
- data/carray.gemspec +1 -1
- data/ext/ca_axis_dispatch.c +258 -4
- data/ext/ca_axis_group.c +368 -184
- data/ext/ca_binop_dispatch.c +54 -32
- data/ext/ca_binop_dispatch.h +1 -0
- data/ext/ca_categorical_iterator.c +90 -41
- data/ext/ca_compare.c +8 -2
- data/ext/ca_for_buffer.h +21 -68
- data/ext/ca_group_iter.c +27 -11
- data/ext/ca_kernel_iterator.c +960 -245
- data/ext/ca_kernel_iterator.h +352 -181
- data/ext/ca_monop_dispatch.c +4 -4
- data/ext/ca_monop_dispatch.h +4 -0
- data/ext/ca_obj_array.c +194 -42
- data/ext/ca_obj_bincmp.c +152 -121
- data/ext/ca_obj_binop.c +107 -210
- data/ext/ca_obj_bitarray.c +37 -24
- data/ext/ca_obj_bitfield.c +95 -62
- data/ext/ca_obj_block.c +5 -4
- data/ext/ca_obj_byte_swap.c +40 -40
- data/ext/ca_obj_const_string.c +180 -50
- data/ext/ca_obj_face.c +428 -62
- data/ext/ca_obj_face.h +107 -47
- data/ext/ca_obj_fake.c +21 -50
- data/ext/ca_obj_farray.c +1 -0
- data/ext/ca_obj_field.c +16 -3
- data/ext/ca_obj_fixlen_string.c +39 -26
- data/ext/ca_obj_grid.c +181 -124
- data/ext/ca_obj_meld.c +235 -55
- data/ext/ca_obj_moncmp.c +61 -61
- data/ext/ca_obj_monop.c +125 -107
- data/ext/ca_obj_object.c +551 -233
- data/ext/ca_obj_record.c +51 -16
- data/ext/ca_obj_reduce.c +5 -8
- data/ext/ca_obj_refer.c +117 -33
- data/ext/ca_obj_remap.c +57 -17
- data/ext/ca_obj_repeat.c +21 -5
- data/ext/ca_obj_roll.c +115 -11
- data/ext/ca_obj_select.c +88 -75
- data/ext/ca_obj_select_axis.c +58 -60
- data/ext/ca_obj_shift.c +22 -102
- data/ext/ca_obj_stack.c +223 -123
- data/ext/ca_obj_stride.c +267 -35
- data/ext/ca_obj_string.c +25 -22
- data/ext/ca_obj_tile.c +32 -16
- data/ext/ca_obj_time.c +23 -25
- data/ext/ca_obj_timedelta.c +21 -23
- data/ext/ca_obj_transpose.c +12 -3
- data/ext/ca_obj_triop.c +110 -117
- data/ext/ca_obj_window.c +157 -29
- data/ext/ca_op_ipower.c +25 -13
- data/ext/ca_op_powi.h +57 -30
- data/ext/ca_pow5_table.h +656 -0
- data/ext/ca_rng_normal.h +42 -0
- data/ext/ca_rng_xoshiro256pp.h +105 -0
- data/ext/ca_sort_kernels.h +70 -0
- data/ext/ca_sweep_engine.c +385 -198
- data/ext/ca_sweep_engine.h +31 -23
- data/ext/ca_triop_dispatch.c +4 -1
- data/ext/ca_triop_dispatch.h +5 -2
- data/ext/caframe_csv_reader.c +463 -0
- data/ext/carray.h +90 -28
- data/ext/carray_access.c +487 -231
- data/ext/carray_address_basis.c +731 -0
- data/ext/carray_attribute.c +45 -3
- data/ext/carray_bincount.c +32 -2
- data/ext/carray_broadcast.c +49 -17
- data/ext/carray_call_cfunc.c +667 -511
- data/ext/carray_cast.c +1556 -247
- data/ext/carray_cast_func.rb +34 -21
- data/ext/carray_conversion.c +197 -73
- data/ext/carray_copy.c +84 -38
- data/ext/carray_core.c +591 -119
- data/ext/carray_count.c +148 -96
- data/ext/carray_element.c +18 -27
- data/ext/carray_factorize.c +604 -291
- data/ext/carray_generate.c +76 -40
- data/ext/carray_histogram.c +152 -94
- data/ext/carray_hold.c +2 -1
- data/ext/carray_index_classifier.c +63 -96
- data/ext/carray_internal.h +400 -1
- data/ext/carray_lazy.c +834 -79
- data/ext/carray_loop.c +88 -66
- data/ext/carray_mask.c +705 -324
- data/ext/carray_math_kernel.h +1 -0
- data/ext/carray_median_percentile.c +263 -63
- data/ext/carray_memory_view.c +254 -248
- data/ext/carray_operator.c +1093 -363
- data/ext/carray_order.c +133 -69
- data/ext/carray_partition.c +102 -56
- data/ext/carray_random.c +582 -204
- data/ext/carray_scatter.c +308 -65
- data/ext/carray_segment.c +257 -0
- data/ext/carray_slab.c +183 -70
- data/ext/carray_sort.c +235 -240
- data/ext/carray_sort_kernel.c +8 -1
- data/ext/carray_struct.c +99 -21
- data/ext/carray_test.c +167 -64
- data/ext/carray_time_text.c +1182 -0
- data/ext/carray_utils.c +363 -98
- data/ext/extconf.rb +27 -2
- data/ext/mk_call_cfunc.rb +103 -118
- data/ext/mk_pow5_table.rb +35 -0
- data/ext/mkkernel.rb +1349 -851
- data/ext/ruby_carray.c +17 -2
- data/ext/version.h +4 -4
- data/ext/version.rb +4 -2
- data/lib/carray/attribute.rb +39 -0
- data/lib/carray/autoload_carray.rb +24 -3
- data/lib/carray/autoload_method_extension.rb +18 -0
- data/lib/carray/axis_group.rb +245 -20
- data/lib/carray/basics.rb +95 -25
- data/lib/carray/bincount_nd.rb +6 -0
- data/lib/carray/block_iterator.rb +200 -54
- data/lib/carray/boolean_reduce.rb +21 -29
- data/lib/carray/categorical.rb +244 -59
- data/lib/carray/categorical_iterator.rb +391 -757
- data/lib/carray/complex.rb +36 -29
- data/lib/carray/conditional.rb +94 -7
- data/lib/carray/const_string.rb +177 -27
- data/lib/carray/construct.rb +40 -0
- data/lib/carray/data_type_extension.rb +3 -0
- data/lib/carray/data_type_limits.rb +91 -0
- data/lib/carray/fixlen_string.rb +1 -1
- data/lib/carray/frame/concat.rb +157 -6
- data/lib/carray/frame/convert.rb +13 -7
- data/lib/carray/frame/csv_parser.rb +590 -76
- data/lib/carray/frame/describe.rb +79 -0
- data/lib/carray/frame/frame.rb +205 -35
- data/lib/carray/frame/group.rb +199 -40
- data/lib/carray/frame/io.rb +452 -60
- data/lib/carray/frame/join.rb +134 -46
- data/lib/carray/frame/records.rb +54 -19
- data/lib/carray/frame/reshape.rb +448 -0
- data/lib/carray/frame/sort.rb +26 -2
- data/lib/carray/frame/verbs.rb +726 -56
- data/lib/carray/frame.rb +2 -0
- data/lib/carray/fuse_source.rb +111 -2
- data/lib/carray/fusion.rb +229 -38
- data/lib/carray/inspect.rb +71 -20
- data/lib/carray/iterator.rb +157 -2
- data/lib/carray/lazy.rb +54 -67
- data/lib/carray/mask_gap_fill.rb +66 -50
- data/lib/carray/math.rb +32 -19
- data/lib/carray/meld_reduce.rb +40 -7
- data/lib/carray/methods/align_addr.rb +8 -4
- data/lib/carray/methods/bincount.rb +2 -2
- data/lib/carray/methods/bit_string.rb +14 -3
- data/lib/carray/methods/composition.rb +16 -10
- data/lib/carray/methods/discovery_along.rb +74 -0
- data/lib/carray/methods/factorize.rb +50 -0
- data/lib/carray/methods/gather_nd.rb +13 -2
- data/lib/carray/methods/insert_block.rb +9 -14
- data/lib/carray/methods/is_in.rb +130 -6
- data/lib/carray/methods/join.rb +1 -1
- data/lib/carray/methods/locate_addr.rb +235 -21
- data/lib/carray/methods/mask_duplicates.rb +35 -1
- data/lib/carray/methods/mode.rb +18 -1
- data/lib/carray/methods/nunique.rb +22 -1
- data/lib/carray/methods/repeat.rb +91 -0
- data/lib/carray/methods/resize.rb +21 -23
- data/lib/carray/methods/snap.rb +34 -12
- data/lib/carray/methods/unique.rb +52 -3
- data/lib/carray/record_order.rb +208 -0
- data/lib/carray/rng.rb +86 -0
- data/lib/carray/runtime.rb +14 -11
- data/lib/carray/segment_iterator.rb +990 -0
- data/lib/carray/serialize.rb +197 -27
- data/lib/carray/slab_iterator.rb +109 -40
- data/lib/carray/stack.rb +10 -4
- data/lib/carray/string_operation_extension.rb +9 -4
- data/lib/carray/struct.rb +142 -51
- data/lib/carray/struct_builder.rb +110 -14
- data/lib/carray/table.rb +2 -2
- data/lib/carray/time.rb +535 -105
- data/lib/carray/window_iterator.rb +259 -82
- data/lib/carray.rb +4 -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_block.rb +2 -7
- data/yard-stubs/ca_obj_fake.rb +1 -1
- data/yard-stubs/ca_obj_field.rb +1 -1
- data/yard-stubs/ca_obj_refer.rb +3 -3
- data/yard-stubs/ca_obj_roll.rb +0 -1
- data/yard-stubs/ca_obj_stride.rb +10 -7
- data/yard-stubs/ca_obj_transpose.rb +1 -1
- data/yard-stubs/ca_obj_window.rb +11 -4
- data/yard-stubs/carray_access.rb +6 -5
- data/yard-stubs/carray_broadcast.rb +2 -2
- data/yard-stubs/carray_cast.rb +78 -43
- data/yard-stubs/carray_conversion.rb +13 -1
- data/yard-stubs/carray_copy.rb +8 -3
- data/yard-stubs/carray_core.rb +3 -83
- data/yard-stubs/carray_count.rb +16 -2
- data/yard-stubs/carray_lazy.rb +210 -1
- data/yard-stubs/carray_loop.rb +4 -1
- data/yard-stubs/carray_mask.rb +34 -10
- data/yard-stubs/carray_math.rb +1499 -4
- data/yard-stubs/carray_median_percentile.rb +26 -8
- data/yard-stubs/carray_order.rb +43 -82
- data/yard-stubs/carray_scatter.rb +10 -3
- data/yard-stubs/carray_segment.rb +72 -0
- data/yard-stubs/carray_slab.rb +11 -8
- data/yard-stubs/carray_sort.rb +50 -8
- data/yard-stubs/carray_test.rb +10 -0
- data/yard-stubs/carray_utils.rb +0 -26
- metadata +19 -19
- data/ext/ca_for_each_element.h +0 -239
- data/ext/carray_build_flags.h +0 -3
- data/ext/carray_config.h +0 -28
- data/ext/carray_kernels_bincmp.c +0 -4446
- data/ext/carray_kernels_binop.c +0 -11001
- data/ext/carray_kernels_init.c +0 -1131
- data/ext/carray_kernels_map.c +0 -3467
- data/ext/carray_kernels_moncmp.c +0 -2097
- data/ext/carray_kernels_monop.c +0 -18313
- data/ext/carray_kernels_reduce_aggregate.c +0 -25837
- data/ext/carray_kernels_reduce_boolean.c +0 -330
- data/ext/carray_kernels_reduce_cumulative.c +0 -14593
- data/ext/carray_kernels_reduce_extreme.c +0 -16948
- data/ext/carray_kernels_reduce_variance.c +0 -3910
- data/ext/carray_kernels_scan.c +0 -3693
- data/ext/carray_kernels_search.c +0 -32138
- data/ext/carray_kernels_sort.c +0 -10626
- data/ext/carray_kernels_triop.c +0 -1392
|
@@ -69,41 +69,33 @@ class CArray
|
|
|
69
69
|
# @return [Boolean, CArray] a scalar, or an array when an axis is given.
|
|
70
70
|
def none (skip_masked: true, **opts)
|
|
71
71
|
return __none_skipna__(**opts) if skip_masked
|
|
72
|
-
|
|
73
|
-
r = __kleene_fold(:any, opts)
|
|
74
|
-
r.is_a?(CArray) ? r.not : (r.equal?(UNDEF) ? UNDEF : !r)
|
|
72
|
+
__kleene_fold(:none, opts)
|
|
75
73
|
end
|
|
76
74
|
|
|
77
75
|
private
|
|
78
76
|
|
|
79
|
-
# Three-valued fold
|
|
80
|
-
#
|
|
77
|
+
# Three-valued fold. `kind` is :any, :all or :none. The available-case
|
|
78
|
+
# reduction answers wherever an unmasked cell settles the fold (a true for
|
|
79
|
+
# any, a false for all, a true for none); elsewhere a masked cell leaves the
|
|
80
|
+
# answer undetermined. Going through that reduction checks the data type,
|
|
81
|
+
# the axes and the keywords exactly as `skip_masked: true` does.
|
|
81
82
|
def __kleene_fold (kind, opts)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
r = case kind
|
|
84
|
+
when :any then __any_skipna__(**opts)
|
|
85
|
+
when :all then __all_skipna__(**opts)
|
|
86
|
+
else __none_skipna__(**opts)
|
|
87
|
+
end
|
|
88
|
+
cm = count_masked(**opts)
|
|
89
|
+
if r.is_a?(CArray)
|
|
90
|
+
settled = ( kind == :any ) ? r : r.not
|
|
91
|
+
out = r.copy
|
|
92
|
+
out[settled.not & (cm > 0)] = UNDEF
|
|
93
|
+
out
|
|
94
|
+
else
|
|
95
|
+
settled = ( kind == :any ) ? r : !r
|
|
96
|
+
return r if settled
|
|
97
|
+
( cm > 0 ) ? UNDEF : r
|
|
91
98
|
end
|
|
92
|
-
|
|
93
|
-
keep = opts[:keep_axis]
|
|
94
|
-
cnt_kw = keep ? { axis: axis, keep_axis: keep } : { axis: axis }
|
|
95
|
-
ck = ( kind == :any ) ? count(true, **cnt_kw) : count(false, **cnt_kw)
|
|
96
|
-
cm = count_masked(axis: axis) # count_masked has no keep_axis
|
|
97
|
-
cm = cm.reshape(*ck.shape) unless cm.shape == ck.shape
|
|
98
|
-
|
|
99
|
-
# A cell is undetermined only where it is masked AND the known side does
|
|
100
|
-
# not settle it (ck counts the settling value: true for OR, false for AND).
|
|
101
|
-
# Masking only those cells avoids leaving an all-zero mask behind.
|
|
102
|
-
out = CArray.boolean(*ck.shape)
|
|
103
|
-
out[] = ( kind == :any ) ? 0 : 1 # OR default false / AND default true
|
|
104
|
-
out[ck.eq(0) & (cm > 0)] = UNDEF # genuinely undetermined
|
|
105
|
-
out[ck > 0] = ( kind == :any ) ? 1 : 0 # known side dominates
|
|
106
|
-
out
|
|
107
99
|
end
|
|
108
100
|
|
|
109
101
|
end
|
data/lib/carray/categorical.rb
CHANGED
|
@@ -17,8 +17,12 @@
|
|
|
17
17
|
# (per-category counts), `cat.codes.count(code)`, etc.
|
|
18
18
|
# - per-cell access = decode the code into its label (`cat[i]` -> category)
|
|
19
19
|
#
|
|
20
|
-
# Exclusion (missing / out-of-vocabulary) is encoded two ways at once
|
|
21
|
-
#
|
|
20
|
+
# Exclusion (missing / out-of-vocabulary) is encoded two ways at once. Both
|
|
21
|
+
# are written when the categorical is built — categorize produces them, and
|
|
22
|
+
# from_codes normalises whatever it receives into them — and the Face is
|
|
23
|
+
# READONLY afterwards, so they stay in step. Both are needed because they have
|
|
24
|
+
# different readers: the axis-group kernel classifies on the code byte, the
|
|
25
|
+
# materialising paths classify on the mask.
|
|
22
26
|
#
|
|
23
27
|
# - the cell is MASKED -> CArray-native idiom: `cat[i]` is
|
|
24
28
|
# UNDEF, `is_masked` / mask-aware
|
|
@@ -76,46 +80,148 @@ class CACategorical < CAObject
|
|
|
76
80
|
CA_UINT64 => 0xFFFFFFFFFFFFFFFF, CA_INT64 => -1,
|
|
77
81
|
}.freeze
|
|
78
82
|
|
|
83
|
+
# The largest vocabulary each codes data type can carry: every valid code in
|
|
84
|
+
# 0...k has to be representable AND distinct from the exclusion sentinel. An
|
|
85
|
+
# unsigned type spends its top value on the sentinel; a signed one spends -1,
|
|
86
|
+
# which was never a valid index, so a signed type carries one more label than
|
|
87
|
+
# the unsigned type of the same width. categorize picks a width by this rule
|
|
88
|
+
# when it builds codes; from_codes has to check it when it receives them.
|
|
89
|
+
MAX_LABELS = {
|
|
90
|
+
CA_UINT8 => 0xFF, CA_INT8 => 0x80,
|
|
91
|
+
CA_UINT16 => 0xFFFF, CA_INT16 => 0x8000,
|
|
92
|
+
CA_UINT32 => 0xFFFFFFFF, CA_INT32 => 0x80000000,
|
|
93
|
+
CA_UINT64 => 0xFFFFFFFFFFFFFFFF, CA_INT64 => 0x8000000000000000,
|
|
94
|
+
}.freeze
|
|
95
|
+
|
|
79
96
|
class << self
|
|
80
97
|
# Wrap already-dense codes + labels with no discovery — the import receiver
|
|
81
|
-
# for a pandas Categorical or an Arrow dictionary.
|
|
82
|
-
# Face's storage parent verbatim
|
|
83
|
-
#
|
|
98
|
+
# for a pandas Categorical or an Arrow dictionary. from_codes takes
|
|
99
|
+
# ownership of `codes`, which becomes the Face's storage parent: verbatim
|
|
100
|
+
# when it is an entity that needs no rewriting, so a wrapped memory view
|
|
101
|
+
# stays a view, and materialised when it is a view over an array the caller
|
|
102
|
+
# still holds, since a view owns no bytes to take ownership of.
|
|
103
|
+
#
|
|
104
|
+
# This is the one door through which an already-built encoding enters, so
|
|
105
|
+
# it validates rather than assumes, and it normalises before handing over:
|
|
106
|
+
# a cell is excluded when it arrives masked OR holds the all-ones sentinel
|
|
107
|
+
# (type-max for unsigned codes, -1 for signed — both the pandas / Arrow
|
|
108
|
+
# missing code), and every excluded cell leaves here holding the sentinel
|
|
109
|
+
# AND masked. Writing both matters because the two encodings have different
|
|
110
|
+
# readers: the axis-group kernel classifies on the code byte, the
|
|
111
|
+
# materialising paths classify on the mask. A categorical that carries only
|
|
112
|
+
# one of them answers membership two ways, silently. An Arrow dictionary
|
|
113
|
+
# carries its missingness in a validity bitmap with arbitrary code bytes —
|
|
114
|
+
# frequently 0 — so arriving masked-only is the normal import, not an edge.
|
|
84
115
|
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
# the caller pre-masked. Only the mask buffer is touched; the code bytes
|
|
89
|
-
# are left intact (so a pandas byte-reinterpret round-trips).
|
|
116
|
+
# #initialize marks the codes read-only, so this is also the last point at
|
|
117
|
+
# which they can be written; a read-only argument is copied rather than
|
|
118
|
+
# refused.
|
|
90
119
|
# @overload from_codes(codes, labels)
|
|
91
120
|
# Returns a {CACategorical} wrapping already-dense integer
|
|
92
121
|
# `codes` with the given `labels`, without discovery. `codes`
|
|
93
|
-
# becomes the Face's storage parent
|
|
94
|
-
#
|
|
122
|
+
# becomes the Face's storage parent, materialised first if it is
|
|
123
|
+
# a view. A cell that is masked or holds the type-max sentinel is
|
|
124
|
+
# excluded, and leaves as both.
|
|
95
125
|
# @param codes [CArray] integer code storage.
|
|
96
126
|
# @param labels [Array, CArray] category vocabulary indexed by
|
|
97
|
-
# code.
|
|
127
|
+
# code. Must be unique, and must fit the codes data type with
|
|
128
|
+
# the sentinel reserved.
|
|
98
129
|
# @return [CACategorical]
|
|
99
|
-
# @raise [ArgumentError] when `codes` is not an integer CArray
|
|
130
|
+
# @raise [ArgumentError] when `codes` is not an integer CArray,
|
|
131
|
+
# when `labels` holds duplicates or is too large for the codes
|
|
132
|
+
# data type, or when an unmasked code is outside `0...labels.size`
|
|
133
|
+
# and is not the sentinel.
|
|
100
134
|
def from_codes(codes, labels)
|
|
101
135
|
unless codes.is_a?(CArray) && SENTINEL.key?(codes.data_type)
|
|
102
136
|
got = codes.is_a?(CArray) ? codes.data_type : codes.class
|
|
103
137
|
raise ArgumentError, "from_codes: codes must be an integer CArray (got #{got})"
|
|
104
138
|
end
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
139
|
+
|
|
140
|
+
labels_arr = labels.respond_to?(:to_a) ? labels.to_a : Array(labels)
|
|
141
|
+
if labels_arr.uniq.size != labels_arr.size
|
|
142
|
+
raise ArgumentError, "from_codes: labels must be unique (got duplicates)"
|
|
143
|
+
end
|
|
144
|
+
k = labels_arr.size
|
|
145
|
+
sentinel = SENTINEL[codes.data_type]
|
|
146
|
+
max = MAX_LABELS[codes.data_type]
|
|
147
|
+
if k > max
|
|
148
|
+
raise ArgumentError,
|
|
149
|
+
"from_codes: #{k} labels do not fit #{CArray.data_type_name(codes.data_type)} " \
|
|
150
|
+
"codes, which carry at most #{max} (the top value is reserved as the " \
|
|
151
|
+
"exclusion sentinel); widen the codes data type"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Classify on the code bytes with the mask set aside (`.value`): a
|
|
155
|
+
# comparison against a masked cell yields UNDEF, which would read as
|
|
156
|
+
# "not excluded" and let the cell through carrying a valid-looking code.
|
|
157
|
+
raw = codes.value
|
|
158
|
+
if sentinel == -1 # signed codes: -1 is the sentinel
|
|
159
|
+
out = raw.lt(0)
|
|
160
|
+
# Only test the upper bound when k is representable in the codes data
|
|
161
|
+
# type. At the very top of the range (k == max) no value can reach k
|
|
162
|
+
# anyway, and comparing against an unrepresentable literal would wrap
|
|
163
|
+
# and flag every cell as out of range.
|
|
164
|
+
out = out | raw.ge(k) if k <= max - 1
|
|
165
|
+
else # unsigned codes: type-max is the sentinel
|
|
166
|
+
out = raw.ge(k) # k <= max here, so always representable
|
|
108
167
|
end
|
|
109
|
-
|
|
168
|
+
masked = codes.has_mask? ? codes.is_masked : nil
|
|
169
|
+
excluded = masked ? (out | masked) : out
|
|
170
|
+
# A masked cell may hold any byte at all — that is the CArray contract —
|
|
171
|
+
# so it is never corrupt, only in need of normalising. An *unmasked* cell
|
|
172
|
+
# holding an out-of-range code that is not the sentinel is neither a
|
|
173
|
+
# category nor missingness; refuse it here, where the input is still in
|
|
174
|
+
# the caller's hands, instead of letting it surface later as an IndexError
|
|
175
|
+
# from the grouping plan or as a wrong label from a decode.
|
|
176
|
+
corrupt = out & raw.ne(sentinel)
|
|
177
|
+
corrupt = corrupt & masked.not if masked
|
|
178
|
+
if corrupt.any
|
|
179
|
+
bad = raw[corrupt].to_a.uniq.sort
|
|
180
|
+
shown = bad.first(4).join(", ") + (bad.size > 4 ? ", ..." : "")
|
|
181
|
+
raise ArgumentError,
|
|
182
|
+
"from_codes: code#{bad.size == 1 ? "" : "s"} #{shown} outside " \
|
|
183
|
+
"0...#{k} for #{k} label#{k == 1 ? "" : "s"} " \
|
|
184
|
+
"(use #{sentinel} to exclude a cell, or mask it)"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Take real ownership, then normalise so the byte reader and the mask
|
|
188
|
+
# reader agree from here on.
|
|
189
|
+
#
|
|
190
|
+
# A view owns no bytes — its root does, and the caller still holds that
|
|
191
|
+
# root. #initialize marks what it is given read-only, which would stop
|
|
192
|
+
# `cat.codes[i] = x` but not `root[i] = x`, so the codes could still
|
|
193
|
+
# change underneath a Face that is supposed to be immutable (and
|
|
194
|
+
# underneath the grouping plan memoised against them). Marking the root
|
|
195
|
+
# instead is worse: it would freeze bytes outside the window the caller
|
|
196
|
+
# handed over. So a view is materialised. Read-only codes are copied for
|
|
197
|
+
# the adjacent reason — the normalising writes need somewhere to land.
|
|
198
|
+
#
|
|
199
|
+
# An entity that needs no rewriting is adopted verbatim, which is what
|
|
200
|
+
# keeps a wrapped memory view a view. That buffer's producer can still
|
|
201
|
+
# write it; that is the borrowed-buffer bargain, and not something this
|
|
202
|
+
# constructor can close.
|
|
203
|
+
needs_mask = masked ? (excluded & masked.not).any : excluded.any
|
|
204
|
+
needs_byte = (excluded & raw.ne(sentinel)).any
|
|
205
|
+
if !codes.entity? || ((needs_mask || needs_byte) && codes.read_only?)
|
|
206
|
+
codes = codes.copy
|
|
207
|
+
end
|
|
208
|
+
if needs_mask || needs_byte
|
|
209
|
+
codes.value[excluded] = sentinel if needs_byte
|
|
210
|
+
codes.mask = excluded
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
new(codes, labels_arr)
|
|
110
214
|
end
|
|
111
215
|
end
|
|
112
216
|
|
|
113
|
-
# codes : integer CArray, the storage parent
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
217
|
+
# codes : integer CArray, the storage parent, already normalised by the
|
|
218
|
+
# caller (categorize builds it that way; from_codes rewrites what it
|
|
219
|
+
# receives). Excluded cells are both masked AND store the type-max
|
|
220
|
+
# sentinel value (= the all-ones bit pattern, which is signed -1
|
|
221
|
+
# byte-for-byte — the pandas / Arrow missing code). Marking the codes
|
|
222
|
+
# read-only below keeps the two in step from here on, so consumers may
|
|
223
|
+
# rely on either: the mask (CArray-native) or the sentinel
|
|
224
|
+
# (axis-group's out-of-range skip, zero-copy export).
|
|
119
225
|
# labels: Array | CArray, the vocabulary; labels[code] = category.
|
|
120
226
|
# @overload initialize(codes, labels)
|
|
121
227
|
# Allocates a READONLY {CACategorical} Face whose storage is
|
|
@@ -143,8 +249,9 @@ class CACategorical < CAObject
|
|
|
143
249
|
# through views/Faces (a reshape of frozen codes is frozen) and would block
|
|
144
250
|
# the grouping cache from memoising. The flag gives the same write protection
|
|
145
251
|
# (mutations raise) while keeping the object non-frozen. One-way: it takes
|
|
146
|
-
# ownership of `codes
|
|
147
|
-
#
|
|
252
|
+
# ownership of `codes`, which from_codes has already validated, materialised
|
|
253
|
+
# if it was a view, and normalised; a caller keeping a mutable entity of its
|
|
254
|
+
# own must pass `.copy`.
|
|
148
255
|
codes.set_read_only_flag
|
|
149
256
|
end
|
|
150
257
|
|
|
@@ -188,12 +295,11 @@ class CACategorical < CAObject
|
|
|
188
295
|
other.is_a?(CACategorical) && @labels == other.labels
|
|
189
296
|
end
|
|
190
297
|
|
|
191
|
-
# Face hook: decode a per-cell code into its category label
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
def storage_to_scalar(raw)
|
|
298
|
+
# Face hook: decode a per-cell code into its category label, through the one
|
|
299
|
+
# decode every code-to-label path shares (see #label_at).
|
|
300
|
+
def storage_to_element(raw)
|
|
195
301
|
code = raw.is_a?(String) ? raw.unpack1(UNPACK_FORMAT.fetch(parent.data_type)) : raw
|
|
196
|
-
(code
|
|
302
|
+
label_at(code)
|
|
197
303
|
end
|
|
198
304
|
|
|
199
305
|
# ---- category-space operations (by label, not code) -------------------
|
|
@@ -211,6 +317,27 @@ class CACategorical < CAObject
|
|
|
211
317
|
# an all-false result.
|
|
212
318
|
# @param label [Object] category to match.
|
|
213
319
|
# @return [CArray]
|
|
320
|
+
# @overload resize(*newdim, fill_value: UNDEF)
|
|
321
|
+
# Returns a categorical resized to `newdim` (see {CArray#resize}).
|
|
322
|
+
# The new cells are excluded (UNDEF) unless `fill_value` names one of
|
|
323
|
+
# the labels, since a zero code would read as the first label.
|
|
324
|
+
# @raise [ArgumentError] when `fill_value` is not a label.
|
|
325
|
+
def resize (*newdim, fill_value: UNDEF)
|
|
326
|
+
CACategorical.from_codes(codes.resize(*newdim, fill_value: code_for_fill(fill_value)),
|
|
327
|
+
labels)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# @overload insert_block(offset, bsize, &block)
|
|
331
|
+
# Returns a categorical with a block inserted (see
|
|
332
|
+
# {CArray#insert_block}). The inserted cells are excluded (UNDEF)
|
|
333
|
+
# unless the block returns one of the labels.
|
|
334
|
+
# @raise [ArgumentError] when the block returns something that is not
|
|
335
|
+
# a label.
|
|
336
|
+
def insert_block (offset, bsize, &block)
|
|
337
|
+
fill = block ? code_for_fill(block.call) : UNDEF
|
|
338
|
+
CACategorical.from_codes(codes.insert_block(offset, bsize) { fill }, labels)
|
|
339
|
+
end
|
|
340
|
+
|
|
214
341
|
def eq(label)
|
|
215
342
|
codes.eq(@labels.index(label) || @labels.size)
|
|
216
343
|
end
|
|
@@ -250,11 +377,11 @@ class CACategorical < CAObject
|
|
|
250
377
|
# so `labels.zip(category_sizes.to_a)` always pairs up.
|
|
251
378
|
# @return [CArray]
|
|
252
379
|
def category_sizes
|
|
253
|
-
return @
|
|
380
|
+
return @category_sizes if @category_sizes
|
|
254
381
|
bc = codes.bincount
|
|
255
|
-
out = CArray.new(bc.data_type, [@labels.size]) # new zero-fills
|
|
382
|
+
out = CArray.new(bc.data_type, [@labels.size], bytes: bc.bytes) # new zero-fills
|
|
256
383
|
out[0...bc.elements] = bc if bc.elements > 0
|
|
257
|
-
@
|
|
384
|
+
@category_sizes = out
|
|
258
385
|
out
|
|
259
386
|
end
|
|
260
387
|
|
|
@@ -350,10 +477,29 @@ class CACategorical < CAObject
|
|
|
350
477
|
|
|
351
478
|
private
|
|
352
479
|
|
|
353
|
-
#
|
|
354
|
-
#
|
|
480
|
+
# The one decode from a code to its label. An out-of-range code — a negative
|
|
481
|
+
# one, or an unmasked sentinel that reached us from outside — has no label,
|
|
482
|
+
# so it decodes to nil. Writing `@labels[code]` instead would read a negative
|
|
483
|
+
# code from the *end* of the vocabulary and hand back a real label for a cell
|
|
484
|
+
# that has none. Every code-to-label path goes through here rather than
|
|
485
|
+
# indexing @labels itself, because two decodes are two chances to disagree,
|
|
486
|
+
# and disagreeing is how #unique came to contradict #to_a on the same cell.
|
|
487
|
+
# The code a fill value stands for: UNDEF stays UNDEF, a label is its code.
|
|
488
|
+
def code_for_fill (value)
|
|
489
|
+
return UNDEF if UNDEF.equal?(value)
|
|
490
|
+
code = labels.index(value)
|
|
491
|
+
raise ArgumentError, "#{value.inspect} is not one of the labels #{labels.inspect}" unless code
|
|
492
|
+
code
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def label_at (code)
|
|
496
|
+
(code < 0 || code >= @labels.size) ? nil : @labels[code]
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Labels for a code array. The discovery kernels skip masked cells, so what
|
|
500
|
+
# reaches here is real codes — but it is decoded like any other code.
|
|
355
501
|
def labels_for (code_array)
|
|
356
|
-
CA_OBJECT(code_array.to_a.map { |c|
|
|
502
|
+
CA_OBJECT(code_array.to_a.map { |c| label_at(c) })
|
|
357
503
|
end
|
|
358
504
|
|
|
359
505
|
# This categorical's cells as their labels; a masked cell stays masked.
|
|
@@ -424,13 +570,11 @@ class CACategorical < CAObject
|
|
|
424
570
|
# addresses that order the cells by category (excluded cells last).
|
|
425
571
|
# @return [CArray]
|
|
426
572
|
def sort_addr
|
|
427
|
-
return @
|
|
573
|
+
return @sort_addr if @sort_addr
|
|
428
574
|
n = elements
|
|
429
575
|
k = @labels.size
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
cur = CArray.int64(k > 0 ? k : 0) # segment starts, consumed as cursor
|
|
433
|
-
cur[1..-1] = cs.cumsum.int64[0..-2] if k > 1
|
|
576
|
+
nvalid = category_sizes.sum
|
|
577
|
+
cur = reduceat_index.copy # segment starts, consumed as cursor
|
|
434
578
|
flat = codes.reshape(n)
|
|
435
579
|
seq = CArray.int64(n).seq! # source indices, scattered as payload
|
|
436
580
|
out = CArray.int64(n)
|
|
@@ -449,7 +593,7 @@ class CACategorical < CAObject
|
|
|
449
593
|
excluded = excluded.or(flat.is_masked) if flat.has_mask?
|
|
450
594
|
out[nvalid..-1] = seq[excluded] # excluded cells, source order
|
|
451
595
|
end
|
|
452
|
-
@
|
|
596
|
+
@sort_addr = out
|
|
453
597
|
out
|
|
454
598
|
end
|
|
455
599
|
|
|
@@ -464,17 +608,26 @@ class CACategorical < CAObject
|
|
|
464
608
|
# offsets aligned to {#labels}. Pair with {#category_sizes} for lengths.
|
|
465
609
|
# @return [CArray]
|
|
466
610
|
def reduceat_index
|
|
467
|
-
return @
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
611
|
+
return @reduceat_index if @reduceat_index
|
|
612
|
+
# Every boundary but the last; the copy makes it an entity the
|
|
613
|
+
# reduceat kernels can read directly.
|
|
614
|
+
@reduceat_index = CArray.segment_offsets(lengths: category_sizes)[0...-1].copy
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
# @overload categorize(labels: nil, sort_labels: false)
|
|
618
|
+
# Returns a {CACategorical} keyed on the labels, as categorizing any
|
|
619
|
+
# key array does: the labels that occur, in first-appearance order (or
|
|
620
|
+
# sorted with `sort_labels:`), or the fixed `labels:` vocabulary. A
|
|
621
|
+
# category of `self` with no cells does not carry over; a masked cell
|
|
622
|
+
# is excluded.
|
|
623
|
+
# @return [CACategorical]
|
|
624
|
+
def categorize (labels: nil, sort_labels: false)
|
|
625
|
+
if labels || sort_labels
|
|
626
|
+
return label_values.categorize(labels: labels, sort_labels: sort_labels)
|
|
475
627
|
end
|
|
476
|
-
|
|
477
|
-
|
|
628
|
+
# Distinctness rides the codes; only the k labels cross the surface.
|
|
629
|
+
recoded = codes.categorize
|
|
630
|
+
CACategorical.from_codes(recoded.codes, recoded.labels.map { |c| label_at(c) })
|
|
478
631
|
end
|
|
479
632
|
|
|
480
633
|
# @overload inspect
|
|
@@ -512,6 +665,24 @@ class CArray
|
|
|
512
665
|
# @return [CACategorical]
|
|
513
666
|
# @raise [ArgumentError] when explicit `labels` contain
|
|
514
667
|
# duplicates.
|
|
668
|
+
# @raise [CArray::DataTypeError] when `sort_labels: true` discovers
|
|
669
|
+
# the labels of a complex array, which have no order.
|
|
670
|
+
# The order sort_labels: puts two labels in: as sort orders the cells of an
|
|
671
|
+
# array, a Float NaN after every other value; two labels <=> cannot order
|
|
672
|
+
# are refused, by name.
|
|
673
|
+
private def label_order(a, b)
|
|
674
|
+
na = a.is_a?(Float) && a.nan?
|
|
675
|
+
nb = b.is_a?(Float) && b.nan?
|
|
676
|
+
return (na ? 1 : 0) - (nb ? 1 : 0) if na || nb
|
|
677
|
+
c = a <=> b
|
|
678
|
+
if c.nil?
|
|
679
|
+
raise ArgumentError,
|
|
680
|
+
"categorize(sort_labels: true): cannot order the labels " \
|
|
681
|
+
"#{a.inspect} and #{b.inspect}"
|
|
682
|
+
end
|
|
683
|
+
c
|
|
684
|
+
end
|
|
685
|
+
|
|
515
686
|
def categorize(labels: nil, sort_labels: false)
|
|
516
687
|
# Automatic appearance-order vocabulary: one linear pass (C
|
|
517
688
|
# __factorize_appearance__) returns both codes and levels directly, over the
|
|
@@ -519,12 +690,26 @@ class CArray
|
|
|
519
690
|
# lane). Distinctness is the hash-key judgement shared with the discovery
|
|
520
691
|
# family: Float NaN collapses to one category and -0.0 == +0.0, while mixed
|
|
521
692
|
# Integer / Float keys stay distinct (eql?, so 1 and 1.0 are separate
|
|
522
|
-
# categories).
|
|
523
|
-
#
|
|
524
|
-
#
|
|
525
|
-
if labels.nil? &&
|
|
693
|
+
# categories). Complex values are distinct by value with the same NaN and
|
|
694
|
+
# -0.0 rules, and have no order, so sort_labels refuses them. The discovery
|
|
695
|
+
# path below is reserved for an explicit labels list.
|
|
696
|
+
if labels.nil? && sort_labels && complex?
|
|
697
|
+
raise CArray::DataTypeError,
|
|
698
|
+
"categorize(sort_labels: true): a #{data_type_name} value has no " \
|
|
699
|
+
"order (complex numbers are not ordered); without sort_labels: the " \
|
|
700
|
+
"labels are in first-appearance order"
|
|
701
|
+
end
|
|
702
|
+
if labels.nil? && (integer? || float? || complex? || object? || fixlen? || boolean?)
|
|
526
703
|
codes, levels = __factorize_appearance__
|
|
527
|
-
|
|
704
|
+
levels = levels.to_a
|
|
705
|
+
return CACategorical.from_codes(codes, levels) unless sort_labels
|
|
706
|
+
# Sort the k levels as Ruby sorts them, then renumber every code through
|
|
707
|
+
# the rank in one gather. The sentinel of an excluded cell is out of the
|
|
708
|
+
# rank's range, so project makes it UNDEF and from_codes writes it back.
|
|
709
|
+
order = (0...levels.size).sort { |i, j| label_order(levels[i], levels[j]) }
|
|
710
|
+
rank = CArray.new(codes.data_type, [levels.size])
|
|
711
|
+
order.each_with_index { |old, new_code| rank[old] = new_code }
|
|
712
|
+
return CACategorical.from_codes(rank.project(codes), order.map { |i| levels[i] })
|
|
528
713
|
end
|
|
529
714
|
|
|
530
715
|
if labels.nil?
|
|
@@ -533,7 +718,7 @@ class CArray
|
|
|
533
718
|
# masked keys stay excluded), so the non-masked cells are the levels.
|
|
534
719
|
# Only the final list is Ruby, since labels are Ruby objects.
|
|
535
720
|
labels_arr = mask_duplicates[:is_not_masked].to_a
|
|
536
|
-
labels_arr.sort! if sort_labels
|
|
721
|
+
labels_arr.sort! { |a, b| label_order(a, b) } if sort_labels
|
|
537
722
|
else
|
|
538
723
|
labels_arr = labels.respond_to?(:to_a) ? labels.to_a : Array(labels)
|
|
539
724
|
if labels_arr.uniq.size != labels_arr.size
|