Class: CACategoricalIterator
- Inherits:
-
CAIterator
- Object
- CAIterator
- CACategoricalIterator
- Defined in:
- lib/carray/categorical_iterator.rb
Overview
A CAIterator over the categories of a CACategorical. CAIterator is the
family base (the built-in iterators like CAWindowIterator / CABlockIterator
are defined in C); a Ruby Foo < CAIterator supplies its own behaviour and
does not lean on the base machinery. Like CASlabIterator, this class defines
its own each (over the k categories, yielding each category's member slice),
which drives the inherited Enumerable surface; the reduction methods (sum /
mean / median / ...) aggregate the groups into length-k arrays. The kernels
are per-category slices of an eager, category-contiguous grouped copy. This
supersedes the older CAClassIterator.
Instance Attribute Summary collapse
-
#labels ⇒ Array
readonly
Returns the category vocabulary the results are aligned to.
Attributes inherited from CAIterator
Instance Method Summary collapse
-
#all ⇒ CArray
Returns the per-category
allas boolean (matchingCArray#all): true iff every present value is truthy (empty category -> true, vacuously). -
#any ⇒ CArray
Returns the per-category
anyas boolean (matchingCArray#any): true iff some present value is truthy (empty category -> false). - #count(*args, axis: nil) ⇒ Object
- #count_masked(axis: nil) ⇒ Object
- #count_not_masked(axis: nil) ⇒ Object
-
#each({ |members| ... }) {|members| ... } ⇒ Enumerator, self
Yields each category's members (a CArray slice of the grouped copy, in #labels order; an empty category yields an empty array).
-
#elements ⇒ CArray
(also: #group_sizes)
Returns per-group cell counts (classified cells, including value-masked ones; =
cat.category_sizes), a length-ngroups CArray aligned to #labels. -
#initialize(value, cat) ⇒ CACategoricalIterator
constructor
value : the payload CArray to reduce, one cell per categorical cell.
-
#inspect ⇒ String
Returns a compact one-line summary — the group count, the label vocabulary, and the per-group cell counts — instead of dumping the internal grouped/value/codes buffers.
-
#map(data_type: nil) {|members| ... } ⇒ CArray
Group-wise element-wise transform, mirroring
CArray#map_slab. - #max(axis: nil) ⇒ Object
-
#max_addr ⇒ CArray
Per-category flat source address of the maximum.
-
#max_index ⇒ CArray
Per-category group-local index of the maximum.
- #mean(axis: nil) ⇒ Object
-
#median ⇒ CArray
Returns per-category medians as float64.
- #min(axis: nil) ⇒ Object
-
#min_addr ⇒ CArray
Per-category flat source address of the minimum — which cell of the source value holds it, matching
CArray#min_addrper group. -
#min_index ⇒ CArray
Per-category group-local index of the minimum — the position within the category's members (source order) — matching
CArray#min_indexper group. - #minmax(axis: nil) ⇒ Object
-
#ngroups ⇒ Integer
Returns the number of groups (=
labels.size); the length of every per-group result CArray the reductions return. - #op ⇒ Object
-
#percentile(p) ⇒ CArray
Returns the per-category
p-th percentile as float64 (pin 0..100,:linearinterpolation, matchingCArray#percentile). - #prod(axis: nil) ⇒ Object
-
#quantile ⇒ Array<CArray>
Returns the per-category five-number summary
[min, Q1, median, Q3, max]as five length-k float64 CArrays (matchingCArray#quantile): the percentiles at 0 / 25 / 50 / 75 / 100. - #reduce(*args, data_type: nil, &blk) ⇒ Object
-
#sort_addr ⇒ CArray
Per-category sort by flat source address.
-
#stddev ⇒ CArray
Returns per-category SAMPLE standard deviation (ddof=1) as float64.
- #stddevp(axis: nil) ⇒ Object
- #sum(axis: nil) ⇒ Object
-
#variance ⇒ CArray
Returns per-category SAMPLE variance (ddof=1) as float64.
-
#variancep ⇒ CArray
Per-category POPULATION variance (ddof=0) as float64, matching
CArray#variancep: empty / all-masked -> MASKED, single value -> 0.0. - #wmean(weights, axis: nil) ⇒ Object
- #wsum(weights, axis: nil) ⇒ Object
Constructor Details
#initialize(value, cat) ⇒ CACategoricalIterator
value : the payload CArray to reduce, one cell per categorical cell. cat : the CACategorical carrying the classification.
Lays the value out category-contiguous by GATHERING it through the
categorical's cached grouping plan (the counting sort lives on cat, built
once and shared by every payload column and iterator — see
CACategorical's grouping-plan note). The plan gives the segment STARTS
(reduceat_index) and the group-major permutation (perm[slot] = source index
at that grouped slot, = the valid prefix of sort_addr); gathering value
through perm is the only per-column work. Excluded cells (masked or
out-of-vocabulary code) are absent from perm, so they never join a group;
the value mask rides the gather into the grouped copy.
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/carray/categorical_iterator.rb', line 87 def initialize (value, cat) @cat = cat @labels = cat.labels @k = cat.labels.size @value = value # source, kept for #cumsum etc. @src_shape = value.shape # output shape for #map @ndim = 1 # 1-D iterator over k categories @shape = [@k] if value.elements == cat.elements # Flat classifier path (backward compat): cat classifies every cell of # value one-to-one, so eager counting-sort gather is meaningful. This is # what all no-axis reductions consume — case C (all cells collapse into k # buckets) plus case B interpreted flatly. # category_sizes IS the per-group cell counts (what #elements returns); # the segment STARTS are its cached exclusive prefix scan (cat.reduceat_ # index): offsets[c] = sum of counts[0...c]. Both come off the shared # plan, so the counting sort is not repeated here. @elements = cat.category_sizes.int64 nvalid = @elements.sum @offsets = cat.reduceat_index # cached segment STARTS (int64[k]) # Group-major source indices = the valid prefix of the cached sort_addr. # With no classified cell the prefix is empty (and slicing a length-0 # sort_addr would be out of range), so take the empty permutation directly. @perm = nvalid > 0 ? cat.sort_addr[0...nvalid] : CArray.int64(0) @codes = cat.codes.reshape(cat.elements) # flat codes view (map re-walk / weights) # Gather value into category-contiguous order via the cached permutation # and materialise (the reduceat kernels read the grouped buffer's raw ptr, # so it must be a contiguous entity, not the selection view); the value # mask rides the gather. Payload-dependent, so this is the only part # rebuilt per column. With no classified cell (empty / all-excluded) there # is nothing to gather — an empty index into an empty source is out of # range — so build the empty grouped buffer directly. @grouped = nvalid > 0 ? value.reshape(value.elements)[@perm].copy : CArray.new(value.data_type, [0]) @empty = CArray.new(@grouped.data_type, [0]) else # Shape mismatch: only per-fiber axis: dispatch could still work. With a # 1-D value there is no fiber structure to broadcast into, so a mismatch # is unrecoverable (preserves the old strict check). For higher-rank # value, defer validation to reduce time — check only that cat.ndim fits # one of the 3 axis: cases (§2.2 of PROPOSAL_CATEGORICAL_REDUCE_AXIS); # any no-axis reduce called on this iterator will surface the mismatch # because @grouped stays undefined. if value.ndim == 1 || ! [1, value.ndim - 1, value.ndim].include?(cat.ndim) raise ArgumentError, "group_by_category: value.elements (#{value.elements}) != " \ "cat.elements (#{cat.elements})" + (value.ndim == 1 ? "" : ". For per-fiber reduce use `.sum(axis: k)`; cat.ndim=" \ "#{cat.ndim} must be 1 (case A), #{value.ndim} (case B), " \ "or #{value.ndim - 1} (band-only) for h.ndim=#{value.ndim}.") end end self end |
Instance Attribute Details
#labels ⇒ Array (readonly)
162 163 164 |
# File 'lib/carray/categorical_iterator.rb', line 162 def labels @labels end |
Instance Method Details
#all ⇒ CArray
422 423 424 425 |
# File 'lib/carray/categorical_iterator.rb', line 422 def all aa = all_any aa ? aa[:all] : per_category(CA_BOOLEAN) { |s| s.all } end |
#any ⇒ CArray
432 433 434 435 |
# File 'lib/carray/categorical_iterator.rb', line 432 def any aa = all_any aa ? aa[:any] : per_category(CA_BOOLEAN) { |s| s.any } end |
#count(v = <none>) ⇒ CArray #count(axis:) ⇒ CArray
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
# File 'lib/carray/categorical_iterator.rb', line 224 def count (*args, axis: nil) if axis return count_not_masked(axis: axis) if args.empty? raise NotImplementedError, "CACategoricalIterator#count(v, axis:) not yet implemented — " \ "value-equality count with axis: deferred to Phase 3 of " \ "PROPOSAL_CATEGORICAL_REDUCE_AXIS." end return count_not_masked if args.empty? # Delegate per group to CArray#count (handles count(UNDEF) -> masked count and # count(v) alike, with core's exact dtype equality). The group slice is a # CABlock, whose own #count is the block geometry accessor, so dispatch # CArray#count explicitly. (Not fused: a value-equality reduceat would have # to reproduce core's cross-type / out-of-range equality exactly.) cnt = CArray.instance_method(:count) per_category(CA_INT64) { |s| cnt.bind_call(s, *args) } end |
#count_masked ⇒ CArray #count_masked(axis:) ⇒ CArray
251 252 253 254 255 256 257 258 259 260 |
# File 'lib/carray/categorical_iterator.rb', line 251 def count_masked(axis: nil) if axis raise NotImplementedError, "CACategoricalIterator#count_masked(axis:) not yet implemented — " \ "deferred to Phase 3 of PROPOSAL_CATEGORICAL_REDUCE_AXIS " \ "(needs a separate value-mask-only scatter kernel)." end m = moments m ? @elements - m[:count] : per_category(CA_INT64) { |s| s.count_masked } end |
#count_not_masked ⇒ CArray #count_not_masked(axis:) ⇒ CArray
207 208 209 210 211 |
# File 'lib/carray/categorical_iterator.rb', line 207 def count_not_masked(axis: nil) return axis_moments(axis)[:count] if axis m = moments m ? m[:count] : per_category(CA_INT64) { |s| s.count_not_masked } end |
#each({ |members| ... }) {|members| ... } ⇒ Enumerator, self
153 154 155 156 157 |
# File 'lib/carray/categorical_iterator.rb', line 153 def each return to_enum(:each) unless block_given? @k.times { |c| yield group_slice(c) } self end |
#elements ⇒ CArray Also known as: group_sizes
178 179 180 |
# File 'lib/carray/categorical_iterator.rb', line 178 def elements @elements end |
#inspect ⇒ String
191 192 193 194 |
# File 'lib/carray/categorical_iterator.rb', line 191 def inspect "#<#{self.class} ngroups=#{@k} labels=#{@labels.inspect} " \ "elements=#{@elements.to_a.inspect}>" end |
#map(data_type: nil) {|members| ... } ⇒ CArray
629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 |
# File 'lib/carray/categorical_iterator.rb', line 629 def map (data_type: nil) raise LocalJumpError, "no block given (yield)" unless block_given? dt = data_type || @grouped.data_type # Apply the block per category, assembled in grouped (category-contiguous) # order: a same-length result scatters cell for cell, a scalar broadcasts. transformed = CArray.new(dt, [@grouped.elements]) @k.times do |c| lo = @offsets[c] hi = (c + 1 < @k) ? @offsets[c + 1] : @grouped.elements transformed[lo...hi] = yield(@grouped[lo...hi]) if hi > lo end # Scatter back to source positions via the permutation (grouped-order source # indices). Excluded cells are absent from perm and stay UNDEF. out = CArray.new(dt, @src_shape) out[] = UNDEF out.reshape(@codes.elements)[perm] = transformed out end |
#max ⇒ CArray #max(axis:) ⇒ CArray
293 294 295 296 297 |
# File 'lib/carray/categorical_iterator.rb', line 293 def max(axis: nil) return axis_moments(axis)[:max] if axis m = moments m ? m[:max] : per_category(@grouped.data_type) { |s| s.max } end |
#max_addr ⇒ CArray
516 517 518 |
# File 'lib/carray/categorical_iterator.rb', line 516 def max_addr group_addr(max_index) end |
#max_index ⇒ CArray
498 499 500 501 |
# File 'lib/carray/categorical_iterator.rb', line 498 def max_index am = arg_minmax am ? am[:max] : per_category(CA_INT64) { |s| s.max_index } end |
#mean ⇒ CArray #mean(axis:) ⇒ CArray
320 321 322 323 324 325 326 327 328 |
# File 'lib/carray/categorical_iterator.rb', line 320 def mean(axis: nil) return axis_mean(axis) if axis m = moments return per_category(CA_FLOAT64) { |s| s.mean } unless m cnt = m[:count] out = m[:sum] / cnt.float64 # count 0 -> NaN, masked next out[cnt.eq(0)] = UNDEF # empty / all-masked category -> MASKED out end |
#median ⇒ CArray
333 334 335 336 |
# File 'lib/carray/categorical_iterator.rb', line 333 def median(axis: nil) axis_order_stat_defer!(:median) if axis percentile(50.0) end |
#min ⇒ CArray #min(axis:) ⇒ CArray
307 308 309 310 311 |
# File 'lib/carray/categorical_iterator.rb', line 307 def min(axis: nil) return axis_moments(axis)[:min] if axis m = moments m ? m[:min] : per_category(@grouped.data_type) { |s| s.min } end |
#min_addr ⇒ CArray
509 510 511 |
# File 'lib/carray/categorical_iterator.rb', line 509 def min_addr group_addr(min_index) end |
#min_index ⇒ CArray
490 491 492 493 |
# File 'lib/carray/categorical_iterator.rb', line 490 def min_index am = arg_minmax am ? am[:min] : per_category(CA_INT64) { |s| s.min_index } end |
#minmax ⇒ Array<CArray> #minmax(axis:) ⇒ Array<CArray>
449 450 451 452 |
# File 'lib/carray/categorical_iterator.rb', line 449 def minmax(axis: nil) return [min(axis: axis), max(axis: axis)] if axis [min, max] end |
#ngroups ⇒ Integer
168 169 170 |
# File 'lib/carray/categorical_iterator.rb', line 168 def ngroups @k end |
#cumsum ⇒ CArray #cumprod ⇒ CArray #cummax ⇒ CArray #cummin ⇒ CArray #cumcount ⇒ CArray
681 682 683 |
# File 'lib/carray/categorical_iterator.rb', line 681 [:cumsum, :cumprod, :cummax, :cummin, :cumcount].each do |op| define_method(op) { scan(op) } end |
#percentile(p) ⇒ CArray
345 346 347 348 349 350 351 352 353 |
# File 'lib/carray/categorical_iterator.rb', line 345 def percentile (p, axis: nil) axis_order_stat_defer!(:percentile) if axis unless MONOID_TYPES.include?(@grouped.data_type) return per_category(CA_FLOAT64) { |s| s.percentile(p) } end out = CArray.float64(@k) @grouped.send(:__reduceat_percentile__, @offsets, p.to_f, out) out end |
#prod ⇒ CArray #prod(axis:) ⇒ CArray
409 410 411 412 413 414 415 |
# File 'lib/carray/categorical_iterator.rb', line 409 def prod(axis: nil) return axis_prod(axis) if axis return per_category(CA_FLOAT64) { |s| s.prod } unless MONOID_TYPES.include?(@grouped.data_type) out = CArray.float64(@k) @grouped.send(:__reduceat_prod__, @offsets, out) out end |
#quantile ⇒ Array<CArray>
361 362 363 364 365 366 367 368 |
# File 'lib/carray/categorical_iterator.rb', line 361 def quantile unless MONOID_TYPES.include?(@grouped.data_type) return [0, 25, 50, 75, 100].map { |p| percentile(p) } end outs = Array.new(5) { CArray.float64(@k) } @grouped.send(:__reduceat_quantile__, @offsets, *outs) outs end |
#reduce({ |members| ... }) {|members| ... } ⇒ CArray #reduce(init) ⇒ CArray
605 606 607 608 609 610 611 612 613 614 615 616 617 618 |
# File 'lib/carray/categorical_iterator.rb', line 605 def reduce (*args, data_type: nil, &blk) raise LocalJumpError, "no block given (yield)" unless blk dt = data_type || CA_OBJECT if args.empty? per_category(dt) { |s| blk.call(s) } else init = args[0] per_category(dt) { |s| acc = init s.each { |e| acc = blk.call(acc, e) } acc } end end |
#sort_addr ⇒ CArray
538 539 540 541 542 543 544 545 546 547 548 549 |
# File 'lib/carray/categorical_iterator.rb', line 538 def sort_addr out = CArray.int64(@grouped.elements) @k.times do |c| lo = @offsets[c] hi = (c + 1 < @k) ? @offsets[c + 1] : @grouped.elements next unless hi > lo # View-local sort order of the segment (0..size-1), lifted to grouped # slots, then mapped back to source addresses via perm. out[lo...hi] = perm[@grouped[lo...hi].sort_addr + lo] end out end |
#stddev ⇒ CArray
392 393 394 395 396 397 |
# File 'lib/carray/categorical_iterator.rb', line 392 def stddev(axis: nil) return axis_variance_family(axis, :stddev) if axis m = moments return per_category(CA_FLOAT64) { |s| s.stddev } unless m variance.sqrt # sqrt propagates the n=0 mask end |
#stddevp ⇒ CArray #stddevp(axis:) ⇒ CArray
477 478 479 480 481 482 |
# File 'lib/carray/categorical_iterator.rb', line 477 def stddevp(axis: nil) return axis_variance_family(axis, :stddevp) if axis m = moments return per_category(CA_FLOAT64) { |s| s.stddevp } unless m variancep.sqrt end |
#sum ⇒ CArray #sum(axis:) ⇒ CArray
276 277 278 279 280 281 282 283 |
# File 'lib/carray/categorical_iterator.rb', line 276 def sum(axis: nil) return axis_sum(axis) if axis m = moments return per_category(@grouped.data_type) { |s| s.sum } unless m out = CArray.new(@grouped.data_type, [@k]) out[] = m[:sum] # cast float64 sums -> value dtype (empty -> 0) out end |
#variance ⇒ CArray
376 377 378 379 380 381 382 383 384 385 |
# File 'lib/carray/categorical_iterator.rb', line 376 def variance(axis: nil) return axis_variance_family(axis, :variance) if axis m = moments return per_category(CA_FLOAT64) { |s| s.variance } unless m cnt = m[:count] means = m[:sum] / cnt.float64 # per-segment mean (garbage where count 0/1, out = CArray.float64(@k) # ignored by the kernel's n<2 guards) @grouped.send(:__reduceat_variance__, @offsets, means, cnt, out) out end |
#variancep ⇒ CArray
460 461 462 463 464 465 466 467 468 |
# File 'lib/carray/categorical_iterator.rb', line 460 def variancep(axis: nil) return axis_variance_family(axis, :variancep) if axis m = moments return per_category(CA_FLOAT64) { |s| s.variancep } unless m cnt = m[:count] vp = variance * (cnt - 1).float64 / cnt.float64 vp[cnt.eq(0)] = UNDEF # empty / all-masked stays masked vp end |
#wmean(weights) ⇒ CArray #wmean(weights, axis:) ⇒ CArray
587 588 589 590 591 592 |
# File 'lib/carray/categorical_iterator.rb', line 587 def wmean (weights, axis: nil) return axis_wsum_wmean(weights, axis)[1] if axis wg = scatter_weights(weights) return kernel_weighted(wg)[1] if MONOID_TYPES.include?(@grouped.data_type) fold_weighted(wg, UNDEF) { |v, ws| v.wmean(ws) } end |
#wsum(weights) ⇒ CArray #wsum(weights, axis:) ⇒ CArray
567 568 569 570 571 572 |
# File 'lib/carray/categorical_iterator.rb', line 567 def wsum (weights, axis: nil) return axis_wsum_wmean(weights, axis)[0] if axis wg = scatter_weights(weights) return kernel_weighted(wg)[0] if MONOID_TYPES.include?(@grouped.data_type) fold_weighted(wg, 0.0) { |v, ws| v.wsum(ws) } end |