Class: CACategorical
- Defined in:
- lib/carray/categorical.rb
Overview
Categorical column: dense integer codes plus a label vocabulary, so each element is an index into a small set of categories. Structurally the same idea as a pandas Categorical or an Arrow dictionary array.
Implemented as a read-only non-numeric Face over the codes array — the
storage is the integer codes, while the surface is CA_FIXLEN so numeric
kernels are gated off (cat + 1 raises; arithmetic on category codes is
not meaningful).
Constant Summary collapse
- UNPACK_FORMAT =
raw-byte unpack format per storage (codes) data_type, native endian. The FIXLEN surface delivers a per-cell fetch as an N-byte String; this decodes it back into the integer code.
{ CA_INT8 => "c", CA_UINT8 => "C", CA_INT16 => "s", CA_UINT16 => "S", CA_INT32 => "l", CA_UINT32 => "L", CA_INT64 => "q", CA_UINT64 => "Q", }.freeze
- SENTINEL =
The exclusion sentinel per codes data_type: the all-ones bit pattern, read as type-max for an unsigned data type and as -1 for a signed one. Either way it is out of every valid [0, k) range and byte-identical to a pandas / Arrow missing code.
{ CA_UINT8 => 0xFF, CA_INT8 => -1, CA_UINT16 => 0xFFFF, CA_INT16 => -1, CA_UINT32 => 0xFFFFFFFF, CA_INT32 => -1, CA_UINT64 => 0xFFFFFFFFFFFFFFFF, CA_INT64 => -1, }.freeze
- MAX_LABELS =
The largest vocabulary each codes data type can carry: every valid code in 0...k has to be representable AND distinct from the exclusion sentinel. An unsigned type spends its top value on the sentinel; a signed one spends -1, which was never a valid index, so a signed type carries one more label than the unsigned type of the same width. categorize picks a width by this rule when it builds codes; from_codes has to check it when it receives them.
{ CA_UINT8 => 0xFF, CA_INT8 => 0x80, CA_UINT16 => 0xFFFF, CA_INT16 => 0x8000, CA_UINT32 => 0xFFFFFFFF, CA_INT32 => 0x80000000, CA_UINT64 => 0xFFFFFFFFFFFFFFFF, CA_INT64 => 0x8000000000000000, }.freeze
Constants inherited from CArray
CArray::HAVE_COMPLEX, CArray::LAZY_BINCMP_OP_IDS, CArray::LAZY_BINCMP_TOL_OP_IDS, CArray::LAZY_BINOP_OP_IDS, CArray::LAZY_MONOP_OP_IDS, CArray::LAZY_TRIOP_OP_IDS, CArray::UNSPECIFIED, CArray::VERSION
Instance Attribute Summary collapse
-
#labels ⇒ Object
readonly
Returns the value of attribute labels.
Class Method Summary collapse
-
.from_codes(codes, labels) ⇒ CACategorical
Wrap already-dense codes + labels with no discovery — the import receiver for a pandas Categorical or an Arrow dictionary.
Instance Method Summary collapse
-
#build_grouping ⇒ self
Force-build (and cache) the whole grouping plan up front — sort_addr, reduceat_index, category_sizes — for prepare-ahead use before a batch of groupings.
-
#category_sizes ⇒ CArray
(also: #bincount)
Per-category counts as a length-k array aligned to #labels.
-
#codes ⇒ CArray
The raw integer codes (= the storage parent).
-
#copy_state(src) ⇒ Object
Face hook: carry the vocabulary across lifted views (slice / reshape / …).
-
#count(label) ⇒ Integer
Returns the number of cells whose category equals
label(0 for an unknown label). - #difference(other, sort: false) ⇒ CArray
-
#eq(label) ⇒ CArray
Boolean mask of cells whose category == label.
-
#face_state_compatible?(other) ⇒ Boolean
Face hook: the homogeneity gate for multi-parent constructions (CArray.promote_list / CArray.stack / anything that Face-lifts a list).
-
#initialize(codes, labels) ⇒ CACategorical
constructor
codes : integer CArray, the storage parent, already normalised by the caller (categorize builds it that way; from_codes rewrites what it receives).
-
#inspect ⇒ String
Returns a short summary showing element count, category count, and vocabulary.
- #intersection(other, sort: false) ⇒ CArray
-
#is_in(values) ⇒ CArray
Returns a boolean CArray, true where the cell's label is in
values. - #locate_addr(ref) ⇒ CArray
-
#mode(axis: nil) ⇒ CArray+
Returns the most frequent label(s).
-
#ne(label) ⇒ CArray
Returns the complement of #eq.
-
#reduceat_index ⇒ CArray
Segment start offsets into the category-contiguous layout produced by #sort_addr: reduceat_index is where category c's block begins, so block c spans `reduceat_index ...
-
#sort_addr ⇒ CArray
Flat addresses that gather self into category-contiguous order: every cell of category 0 first, then 1, ..., then k-1, with excluded cells last.
-
#storage_to_scalar(raw) ⇒ Object
Face hook: decode a per-cell code into its category label, through the one decode every code-to-label path shares (see #label_at).
- #union(other, sort: false) ⇒ CArray
-
#unique(sort: false) ⇒ CArray
Returns the labels that occur, in first-appearance order.
-
#value_counts(sort: false) ⇒ Array(CArray, CArray)
Returns
[labels, counts]for the categories that occur.
Methods inherited from CArray
#%, #&, #*, #**, #+, #-, #-@, #/, #<, #<<, #<=, #<=>, #==, #>, #>=, #>>, #T, #[], #[]=, #^, _scan_float, _scan_int, #abs, #abs!, #abs_i, #abs_i!, #acos, #acos!, #acosh, #acosh!, #add, #add!, addr2index, #addr2index, #address, align_addr, align_nearest_addr, #all, #all_masked?, #ancestors, #and, #and!, #any, #any_masked?, #arg, #arg!, #as_boolean, #as_byte, #as_cmplx128, #as_cmplx256, #as_cmplx64, #as_complex, #as_dcomplex, #as_double, #as_fixlen, #as_float, #as_float128, #as_float32, #as_float64, #as_int, #as_int16, #as_int32, #as_int64, #as_int8, #as_object, #as_short, #as_strided, #as_type, #as_uint16, #as_uint32, #as_uint64, #as_uint8, #asin, #asin!, #asinh, #asinh!, #atan, #atan!, #atan2, #atan2!, #atanh, #atanh!, #attached?, #attr, #attrs, #axis2addr, #axis_group, big_endian?, #bin, #bin_to, #bincount_nd, #bit_and, #bit_lshift, #bit_lshift!, #bit_neg, #bit_neg!, #bit_or, #bit_rshift, #bit_rshift!, #bit_xor, #bitarray, #bitfield, #bits, #block_view, #blocks, #boolean, boolean, #boolean?, #broadcast_to, #bsearch, #bsearch_addr, byte, #byte, #bytes, cast, #cast_with, #categorize, #ceil, #ceil!, #choose, #clip, #clip_int16, #clip_int32, #clip_int64, #clip_int8, #clip_uint16, #clip_uint32, #clip_uint64, #clip_uint8, cmplx128, #cmplx128, cmplx64, #cmplx64, #coerce, #collect!, #collect_addr!, #collect_index!, #collect_with_addr!, #collect_with_index!, #complex, complex, #complex?, concatenate, #concatenate, #conditional, #conj, #conj!, const_string, #convert, #copy, #copysign, #copysign!, #cos, #cos!, #cosh, #cosh!, #count_masked, #count_not_masked, #crop, #data_class, #data_class=, #data_type, data_type_code, #data_type_name, data_type_name, dcomplex, #dcomplex, #deg, #deg!, #deg_180, #deg_180!, #deg_360, #deg_360!, #delete_block, #diagonal, #dim, #dim0, #dim1, #dim2, #dim3, #dim_view, #div, #div!, #divmod, #double, double, #drop_axis, dump, #dump_binary, #each, #each_addr, each_index, #each_index, #each_slab, #each_with_addr, #each_with_index, #elem_copy, #elem_decr, #elem_fetch, #elem_incr, #elem_mask, #elem_masked?, #elem_max, #elem_min, #elem_store, #elem_swap, #elem_unmask, #elements, empty, #empty?, #endian, endian, #entity?, #eql?, #exp, #exp!, #exp10, #exp10!, #exp2, #exp2!, #expm1, #expm1!, #face?, #factorize, #fake, #false, #farray, #feq, #field, #fields, #fields_at, #fill, #fill_copy, #first, #fixlen, fixlen, #fixlen?, fixlen_string, #flags, #flatten, #flip, #float, float, #float32, float32, #float64, float64, #float?, #floor, #floor!, #fma, #fma!, #fmax, #fmin, #fmod, #fmod!, #fms, #fms!, format, #format, #frac, #freeze, from_bit_string, #from_bit_string, from_memory_view, fuse, #gather_nd, #ge, #grid, #group_by_category, #group_by_run, #gt, guard_undef, guess_type_and_bytes, #has_attr?, #has_data_class?, #has_mask?, #hash, #histogram, #histogram1d, #histogram2d, #hypot, #hypot!, #imag, #imag=, #imag_i, #imag_i!, #index, index2addr, #index2addr, #indices, #inherit_mask, #inherit_mask_replace, #initialize_copy, #insert_axis, #insert_block, #inspect_full, int, #int, #int16, int16, int32, #int32, #int64, int64, #int8, int8, #integer?, #invert_mask, #is_close, #is_equiv, #is_finite, #is_inf, #is_invalid, #is_masked, #is_mode, #is_nan, #is_not_masked, #is_real, #join, #last, #lazy, #le, #length, #linear_fetch, #linear_section, little_endian?, load, load_arrow_tensor, #load_binary, #locate_nearest_addr, #log, #log!, #log10, #log10!, #log1p, #log1p!, #log2, #log2!, #logaddexp, #logaddexp!, #logb, #logb!, #lookup, #lt, #map!, #map_addr!, #map_index!, #map_slab, #map_with_addr!, #map_with_index!, #marshal_dump, #marshal_load, #mask, #mask=, #mask_array?, #mask_duplicates, #mask_eq, #mask_invalid, #mask_where, #maximum, #maximum!, #median, meld, #meld, #members, memory_view_available?, memory_view_reject_reason, meshgrid, #minimum, #minimum!, #mod, #mod!, montage, mosaic, #mul, #mul!, #ndim, #neg, #neg!, #nextafter, #nextafter!, #none, #normalize_axes, #normalize_axis, normalize_axis, #normalize_index, #not, #not!, #numeric?, #nunique, #obj_type, #object, object, #object?, #one, #one!, #or, #or!, #order, #pack_bits, #parent, #partition, #partition_copy, #paste, #percentile, #pmax, #pmax!, #pmin, #pmin!, #pow, #power, #power!, #project, promote_list, #put_nd, #quantile, #rad, #rad!, #rad_2pi, #rad_2pi!, #rad_pi, #rad_pi!, #random, #random!, #randomn, #randomn!, #rank, #rcp, #rcp!, #rcp_mul, #rcp_mul!, #read_only?, #real, #real=, #real?, #reduce_slab, #refer, #repeat, #replace_where, #reshape, #resize, result_type, #roll, #root_array, #round, #rsqrt, #rsqrt!, #same_shape?, save, #save_arrow_tensor, #scalar?, #scale, #scale!, #scatter_add!, #scatter_max!, #scatter_min!, #scatter_mul!, #scatter_replace!, #scatter_sub!, #search, #search_addr, #search_nearest, #search_nearest_addr, select, #seq, #seq!, #set_attr, #set_read_only_flag, #shape, #shift, short, #short, #shuffle, #shuffle!, #sign, #sign!, #signbit, #sin, #sin!, #sinh, #sinh!, #size, sizeof, #sliding_windows, #snap, #snap_to, #sort, sort_addr, #sort_copy, #source_code, #span, #span!, #split, #sqrt, #sqrt!, #square, #square!, #st, #stack, stack, string, #strip_mask, struct, #sub, #sub!, #swap_bytes, tabulate, #tan, #tan!, #tanh, #tanh!, #template, #then_else, #tile, #time, time, time_range, time_series, #timedelta, #to_a, #to_bit_string, #to_ca, #to_s, #to_type, #transpose, #true, #trunc, #trunc!, uint16, #uint16, uint32, #uint32, uint64, #uint64, uint8, #uint8, #unfold, union, #unmask, #valid_addr?, #valid_index?, #validity_bits, #value, #value_array?, #virtual?, #where, #window, #windows, wrap, wrap_memory_view, wrap_readonly, wrap_writable, #xor, #xor!, #zero, #zero!, #|, #~
Methods included from CArray::DataTypeExtension
#arange, #empty, #eye, #full, #identity, #linspace, #ones, #zeros
Constructor Details
#initialize(codes, labels) ⇒ CACategorical
codes : integer CArray, the storage parent, already normalised by the caller (categorize builds it that way; from_codes rewrites what it receives). Excluded cells are both masked AND store the type-max sentinel value (= the all-ones bit pattern, which is signed -1 byte-for-byte — the pandas / Arrow missing code). Marking the codes read-only below keeps the two in step from here on, so consumers may rely on either: the mask (CArray-native) or the sentinel (axis-group's out-of-range skip, zero-copy export). labels: Array | CArray, the vocabulary; labels = category.
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
# File 'lib/carray/categorical.rb', line 232 def initialize(codes, labels) # Own a frozen copy of the vocabulary: the categorical is READONLY and its # codes index into labels, so the label list must not change under it. We # copy first so a caller's array is never frozen as a side effect; the # label objects themselves are left untouched (container-level freeze). @labels = (labels.respond_to?(:to_a) ? labels.to_a : Array(labels)).dup.freeze super(CA_FIXLEN, codes.dim, bytes: codes.bytes, storage: codes.data_type, parent: codes, read_only: true, face: true) # Mark the codes storage read-only so the READONLY guarantee holds at the # root, not just on the Face. Without this the Face is read_only but its # parent is writable, so `cat.codes[i] = x` silently mutates the categorical # (and any grouping cache derived from it). We set the CA_FLAG_READ_ONLY flag # rather than #freeze: freeze also freezes the Ruby object, which propagates # through views/Faces (a reshape of frozen codes is frozen) and would block # the grouping cache from memoising. The flag gives the same write protection # (mutations raise) while keeping the object non-frozen. One-way: it takes # ownership of `codes`, which from_codes has already validated, materialised # if it was a view, and normalised; a caller keeping a mutable entity of its # own must pass `.copy`. codes.set_read_only_flag end |
Instance Attribute Details
#labels ⇒ Object (readonly)
Returns the value of attribute labels.
258 259 260 |
# File 'lib/carray/categorical.rb', line 258 def labels @labels end |
Class Method Details
.from_codes(codes, labels) ⇒ CACategorical
Wrap already-dense codes + labels with no discovery — the import receiver
for a pandas Categorical or an Arrow dictionary. from_codes takes
ownership of codes, which becomes the Face's storage parent: verbatim
when it is an entity that needs no rewriting, so a wrapped memory view
stays a view, and materialised when it is a view over an array the caller
still holds, since a view owns no bytes to take ownership of.
This is the one door through which an already-built encoding enters, so it validates rather than assumes, and it normalises before handing over: a cell is excluded when it arrives masked OR holds the all-ones sentinel (type-max for unsigned codes, -1 for signed — both the pandas / Arrow missing code), and every excluded cell leaves here holding the sentinel AND masked. Writing both matters because the two encodings have different readers: the axis-group kernel classifies on the code byte, the materialising paths classify on the mask. A categorical that carries only one of them answers membership two ways, silently. An Arrow dictionary carries its missingness in a validity bitmap with arbitrary code bytes — frequently 0 — so arriving masked-only is the normal import, not an edge.
#initialize marks the codes read-only, so this is also the last point at which they can be written; a read-only argument is copied rather than refused.
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/carray/categorical.rb', line 134 def from_codes(codes, labels) unless codes.is_a?(CArray) && SENTINEL.key?(codes.data_type) got = codes.is_a?(CArray) ? codes.data_type : codes.class raise ArgumentError, "from_codes: codes must be an integer CArray (got #{got})" end labels_arr = labels.respond_to?(:to_a) ? labels.to_a : Array(labels) if labels_arr.uniq.size != labels_arr.size raise ArgumentError, "from_codes: labels must be unique (got duplicates)" end k = labels_arr.size sentinel = SENTINEL[codes.data_type] max = MAX_LABELS[codes.data_type] if k > max raise ArgumentError, "from_codes: #{k} labels do not fit #{CArray.data_type_name(codes.data_type)} " \ "codes, which carry at most #{max} (the top value is reserved as the " \ "exclusion sentinel); widen the codes data type" end # Classify on the code bytes with the mask set aside (`.value`): a # comparison against a masked cell yields UNDEF, which would read as # "not excluded" and let the cell through carrying a valid-looking code. raw = codes.value if sentinel == -1 # signed codes: -1 is the sentinel out = raw.lt(0) # Only test the upper bound when k is representable in the codes data # type. At the very top of the range (k == max) no value can reach k # anyway, and comparing against an unrepresentable literal would wrap # and flag every cell as out of range. out = out | raw.ge(k) if k <= max - 1 else # unsigned codes: type-max is the sentinel out = raw.ge(k) # k <= max here, so always representable end masked = codes.has_mask? ? codes.is_masked : nil excluded = masked ? (out | masked) : out # A masked cell may hold any byte at all — that is the CArray contract — # so it is never corrupt, only in need of normalising. An *unmasked* cell # holding an out-of-range code that is not the sentinel is neither a # category nor missingness; refuse it here, where the input is still in # the caller's hands, instead of letting it surface later as an IndexError # from the grouping plan or as a wrong label from a decode. corrupt = out & raw.ne(sentinel) corrupt = corrupt & masked.not if masked if corrupt.any bad = raw[corrupt].to_a.uniq.sort shown = bad.first(4).join(", ") + (bad.size > 4 ? ", ..." : "") raise ArgumentError, "from_codes: code#{bad.size == 1 ? "" : "s"} #{shown} outside " \ "0...#{k} for #{k} label#{k == 1 ? "" : "s"} " \ "(use #{sentinel} to exclude a cell, or mask it)" end # Take real ownership, then normalise so the byte reader and the mask # reader agree from here on. # # A view owns no bytes — its root does, and the caller still holds that # root. #initialize marks what it is given read-only, which would stop # `cat.codes[i] = x` but not `root[i] = x`, so the codes could still # change underneath a Face that is supposed to be immutable (and # underneath the grouping plan memoised against them). Marking the root # instead is worse: it would freeze bytes outside the window the caller # handed over. So a view is materialised. Read-only codes are copied for # the adjacent reason — the normalising writes need somewhere to land. # # An entity that needs no rewriting is adopted verbatim, which is what # keeps a wrapped memory view a view. That buffer's producer can still # write it; that is the borrowed-buffer bargain, and not something this # constructor can close. needs_mask = masked ? (excluded & masked.not).any : excluded.any needs_byte = (excluded & raw.ne(sentinel)).any if !codes.entity? || ((needs_mask || needs_byte) && codes.read_only?) codes = codes.copy end if needs_mask || needs_byte codes.value[excluded] = sentinel if needs_byte codes.mask = excluded end new(codes, labels_arr) end |
Instance Method Details
#build_grouping ⇒ self
Force-build (and cache) the whole grouping plan up front — sort_addr,
reduceat_index, category_sizes — for prepare-ahead use before a batch of
groupings. Lazy building already covers correctness; this is the explicit
"pay the counting sort now" handle (e.g. right after df.group_by(col)).
Returns self so it chains.
520 521 522 523 524 |
# File 'lib/carray/categorical.rb', line 520 def build_grouping sort_addr # pulls category_sizes; reduceat_index shares category_sizes reduceat_index self end |
#category_sizes ⇒ CArray Also known as: bincount
Per-category counts as a length-k array aligned to #labels. Trailing empty
categories are kept as 0 (unlike codes.bincount, which truncates them), so
labels.zip(category_sizes.to_a) always pairs up.
Memoised: the codes are read-only (immutable storage), so the counts are a
pure function of the categorical and stay valid for its lifetime. The same
cached array backs #reduceat_index and #sort_addr, so a wide aggregate
over the same categorical pays the count once. (Do not mutate the returned
array — it is shared; take .copy for a scratch buffer.) A derived-view /
composite categorical caches too: read-only rides from the codes as a flag,
not a Ruby freeze, so the object stays non-frozen and the memo ivar sticks.
358 359 360 361 362 363 364 365 |
# File 'lib/carray/categorical.rb', line 358 def category_sizes return @_category_sizes if @_category_sizes bc = codes.bincount out = CArray.new(bc.data_type, [@labels.size]) # new zero-fills out[0...bc.elements] = bc if bc.elements > 0 @_category_sizes = out out end |
#codes ⇒ CArray
The raw integer codes (= the storage parent). On a derived view this is the correspondingly sliced/reshaped codes, since codes ride the chain. Excluded cells are masked and store the type-max sentinel; the same array serves the axis-group kernel (out-of-range skip) and the pandas / Arrow bridge (byte-reinterpret to signed -1) with no conversion.
271 272 273 |
# File 'lib/carray/categorical.rb', line 271 def codes parent end |
#copy_state(src) ⇒ Object
Face hook: carry the vocabulary across lifted views (slice / reshape / …). The codes ride along automatically as the Face's parent.
277 278 279 |
# File 'lib/carray/categorical.rb', line 277 def copy_state(src) @labels = src.labels end |
#count(label) ⇒ Integer
337 338 339 340 |
# File 'lib/carray/categorical.rb', line 337 def count(label) code = @labels.index(label) code ? codes.count(code) : 0 end |
#difference(other, sort: false) ⇒ CArray
440 441 442 |
# File 'lib/carray/categorical.rb', line 440 def difference (other, sort: false) unique.difference(label_array(other), sort: sort) end |
#eq(label) ⇒ CArray
Boolean mask of cells whose category == label. Excluded cells stay UNDEF (their category is unknown); an unknown label yields an all-false mask.
320 321 322 |
# File 'lib/carray/categorical.rb', line 320 def eq(label) codes.eq(@labels.index(label) || @labels.size) end |
#face_state_compatible?(other) ⇒ Boolean
Face hook: the homogeneity gate for multi-parent constructions (CArray.promote_list / CArray.stack / anything that Face-lifts a list). A code only means anything against the vocabulary it was assigned from, so two categoricals may share one lifted Face only when they index the same labels in the same code order.
Same labels in a different code order is refused rather than re-coded,
for the same reason CATime refuses a unit mismatch it knows how to convert:
this is a predicate consulted after the parents are assembled, with no
channel to rewrite storage — and the codes are read-only by construction,
so agreeing would mean silently materialising fresh codes inside what the
caller asked for as a view. Build the shared vocabulary up front instead:
keys.categorize(labels: shared).
294 295 296 |
# File 'lib/carray/categorical.rb', line 294 def face_state_compatible?(other) other.is_a?(CACategorical) && @labels == other.labels end |
#inspect ⇒ String
601 602 603 |
# File 'lib/carray/categorical.rb', line 601 def inspect "#<CACategorical n=#{elements} categories=#{@labels.size} labels=#{@labels.inspect}>" end |
#intersection(other, sort: false) ⇒ CArray
434 435 436 |
# File 'lib/carray/categorical.rb', line 434 def intersection (other, sort: false) unique.intersection(label_array(other), sort: sort) end |
#is_in(values) ⇒ CArray
427 428 429 430 |
# File 'lib/carray/categorical.rb', line 427 def is_in (values) wanted = label_list(values).filter_map { |l| @labels.index(l) } codes.is_in(CArray.int64(wanted.size) { |i| wanted[i] }) end |
#locate_addr(ref) ⇒ CArray
453 454 455 |
# File 'lib/carray/categorical.rb', line 453 def locate_addr (ref) label_values.locate_addr(label_array(ref)) end |
#mode(axis: nil) ⇒ CArray+
418 419 420 |
# File 'lib/carray/categorical.rb', line 418 def mode (axis: nil) label_values.mode(axis: axis) end |
#ne(label) ⇒ CArray
328 329 330 |
# File 'lib/carray/categorical.rb', line 328 def ne(label) codes.ne(@labels.index(label) || @labels.size) end |
#reduceat_index ⇒ CArray
Segment start offsets into the category-contiguous layout produced by
#sort_addr: reduceat_index is where category c's block begins, so
block c spans reduceat_index[c] ... reduceat_index[c] + category_sizes[c].
Empty categories repeat the following start (a zero-width block). Length k,
aligned to #labels; pair with #category_sizes for the block lengths. On
data already laid out in category order the offsets index it directly.
583 584 585 586 587 588 589 590 591 592 593 594 595 |
# File 'lib/carray/categorical.rb', line 583 def reduceat_index return @_reduceat_index if @_reduceat_index k = @labels.size out = CArray.int64(k > 0 ? k : 0) if k > 1 out[0] = 0 out[1..-1] = category_sizes.cumsum.int64[0..-2] elsif k == 1 out[0] = 0 end @_reduceat_index = out out end |
#sort_addr ⇒ CArray
Flat addresses that gather self into category-contiguous order: every cell
of category 0 first, then 1, ..., then k-1, with excluded cells last. Built
by a counting sort over the codes (O(n + k), stable): the segment starts
(an exclusive prefix scan of #category_sizes) drive a scatter that places
each source index into its category's block in source order; excluded cells
(masked, or code out of range 0...k) are appended at the tail in source
order. The first #category_sizes.sum addresses are exactly the classified
cells in category order — CACategoricalIterator's permutation. For an N-D
categorical the addresses are into the raveled storage, so
value.reshape(elements)[cat.sort_addr] produces the contiguous blocks.
Memoised (see the grouping-plan note above): the counting sort is the
dominant grouping cost, so it is computed once and shared by every payload
column and by CACategoricalIterator. Do not mutate the returned array.
543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 |
# File 'lib/carray/categorical.rb', line 543 def sort_addr return @_sort_addr if @_sort_addr n = elements k = @labels.size cs = category_sizes # per-category counts nvalid = cs.sum cur = CArray.int64(k > 0 ? k : 0) # segment starts, consumed as cursor cur[1..-1] = cs.cumsum.int64[0..-2] if k > 1 flat = codes.reshape(n) seq = CArray.int64(n).seq! # source indices, scattered as payload out = CArray.int64(n) if nvalid == n flat.send(:__categorical_scatter__, seq, cur, out, k) # all valid: scatter straight in else if nvalid > 0 valid = CArray.int64(nvalid) # scatter target must be an entity, not a view flat.send(:__categorical_scatter__, seq, cur, valid, k) out[0...nvalid] = valid end # Excluded = every cell the scatter skips: code out of [0, k), OR masked # (a from_codes pre-masked cell keeps a valid code but is excluded). Both # must be caught or the tail slot count would not add up to n - nvalid. excluded = flat.value.ge(k).or(flat.value.lt(0)) excluded = excluded.or(flat.is_masked) if flat.has_mask? out[nvalid..-1] = seq[excluded] # excluded cells, source order end @_sort_addr = out out end |
#storage_to_scalar(raw) ⇒ Object
Face hook: decode a per-cell code into its category label, through the one decode every code-to-label path shares (see #label_at).
300 301 302 303 |
# File 'lib/carray/categorical.rb', line 300 def storage_to_scalar(raw) code = raw.is_a?(String) ? raw.unpack1(UNPACK_FORMAT.fetch(parent.data_type)) : raw label_at(code) end |
#union(other, sort: false) ⇒ CArray
446 447 448 |
# File 'lib/carray/categorical.rb', line 446 def union (other, sort: false) unique.union(label_array(other), sort: sort) end |
#unique(sort: false) ⇒ CArray
389 390 391 392 |
# File 'lib/carray/categorical.rb', line 389 def unique (sort: false) u = labels_for(codes.unique) sort ? u.sort : u end |
#value_counts(sort: false) ⇒ Array(CArray, CArray)
399 400 401 402 403 404 405 406 407 408 409 410 411 |
# File 'lib/carray/categorical.rb', line 399 def value_counts (sort: false) unless [false, :count, :value].include?(sort) raise ArgumentError, "value_counts: sort must be false, :count, or :value" end code_values, counts = codes.value_counts(sort: sort == :count ? :count : false) values = labels_for(code_values) return [values, counts] unless sort == :value # Ascending *label*: code order is the vocabulary's, so reorder here. la = values.to_a order = (0...la.size).sort_by { |i| [la[i], i] } idx = CArray.int64(order.size) { |i| order[i] } [values[idx], counts[idx]] end |