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
- #accumulate(axis: nil) ⇒ Object
-
#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 = 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; # 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
#accumulate ⇒ CArray #accumulate(axis:) ⇒ CArray
293 294 295 296 |
# File 'lib/carray/categorical_iterator.rb', line 293 def accumulate(axis: nil) return axis_by_masked_copy(axis, :accumulate, core_reduce_type(:accumulate)) if axis per_category(core_reduce_type(:accumulate)) { |s| s.accumulate } end |
#all ⇒ CArray
435 436 437 438 |
# File 'lib/carray/categorical_iterator.rb', line 435 def all aa = all_any aa ? aa[:all] : per_category(CA_BOOLEAN) { |s| s.all } end |
#any ⇒ CArray
445 446 447 448 |
# File 'lib/carray/categorical_iterator.rb', line 445 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 |
# 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:) is not implemented — " \ "value-equality count is available without 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 data type 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
249 250 251 252 253 254 255 256 257 |
# File 'lib/carray/categorical_iterator.rb', line 249 def count_masked(axis: nil) if axis raise NotImplementedError, "CACategoricalIterator#count_masked(axis:) is not implemented — " \ "call it without axis:." 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
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 |
# File 'lib/carray/categorical_iterator.rb', line 642 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
306 307 308 309 310 |
# File 'lib/carray/categorical_iterator.rb', line 306 def max(axis: nil) return axis_moments(axis)[:max] if axis m = moments m ? m[:max] : per_category(core_reduce_type(:max)) { |s| s.max } end |
#max_addr ⇒ CArray
529 530 531 |
# File 'lib/carray/categorical_iterator.rb', line 529 def max_addr group_addr(max_index) end |
#max_index ⇒ CArray
511 512 513 514 |
# File 'lib/carray/categorical_iterator.rb', line 511 def max_index am = arg_minmax am ? am[:max] : per_category(CA_INT64) { |s| s.max_index } end |
#mean ⇒ CArray #mean(axis:) ⇒ CArray
333 334 335 336 337 338 339 340 341 |
# File 'lib/carray/categorical_iterator.rb', line 333 def mean(axis: nil) return axis_mean(axis) if axis m = moments return per_category(core_reduce_type(:mean)) { |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
346 347 348 349 |
# File 'lib/carray/categorical_iterator.rb', line 346 def median(axis: nil) axis_order_stat_defer!(:median) if axis percentile(50.0) end |
#min ⇒ CArray #min(axis:) ⇒ CArray
320 321 322 323 324 |
# File 'lib/carray/categorical_iterator.rb', line 320 def min(axis: nil) return axis_moments(axis)[:min] if axis m = moments m ? m[:min] : per_category(core_reduce_type(:min)) { |s| s.min } end |
#min_addr ⇒ CArray
522 523 524 |
# File 'lib/carray/categorical_iterator.rb', line 522 def min_addr group_addr(min_index) end |
#min_index ⇒ CArray
503 504 505 506 |
# File 'lib/carray/categorical_iterator.rb', line 503 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>
462 463 464 465 |
# File 'lib/carray/categorical_iterator.rb', line 462 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
694 695 696 |
# File 'lib/carray/categorical_iterator.rb', line 694 [:cumsum, :cumprod, :cummax, :cummin, :cumcount].each do |op| define_method(op) { scan(op) } end |
#percentile(p) ⇒ CArray
358 359 360 361 362 363 364 365 366 |
# File 'lib/carray/categorical_iterator.rb', line 358 def percentile (p, axis: nil) axis_order_stat_defer!(:percentile) if axis unless MONOID_TYPES.include?(@grouped.data_type) return per_category(core_reduce_type(:percentile, p)) { |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
422 423 424 425 426 427 428 |
# File 'lib/carray/categorical_iterator.rb', line 422 def prod(axis: nil) return axis_prod(axis) if axis return per_category(core_reduce_type(:prod)) { |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>
374 375 376 377 378 379 380 381 |
# File 'lib/carray/categorical_iterator.rb', line 374 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
618 619 620 621 622 623 624 625 626 627 628 629 630 631 |
# File 'lib/carray/categorical_iterator.rb', line 618 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
551 552 553 554 555 556 557 558 559 560 561 562 |
# File 'lib/carray/categorical_iterator.rb', line 551 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
405 406 407 408 409 410 |
# File 'lib/carray/categorical_iterator.rb', line 405 def stddev(axis: nil) return axis_by_masked_copy(axis, :stddev) if axis m = moments return per_category(core_reduce_type(:stddev)) { |s| s.stddev } unless m variance.sqrt # sqrt propagates the n=0 mask end |
#stddevp ⇒ CArray #stddevp(axis:) ⇒ CArray
490 491 492 493 494 495 |
# File 'lib/carray/categorical_iterator.rb', line 490 def stddevp(axis: nil) return axis_by_masked_copy(axis, :stddevp) if axis m = moments return per_category(core_reduce_type(:stddevp)) { |s| s.stddevp } unless m variancep.sqrt end |
#sum ⇒ CArray #sum(axis:) ⇒ CArray
273 274 275 276 277 278 |
# File 'lib/carray/categorical_iterator.rb', line 273 def sum(axis: nil) return axis_sum(axis) if axis m = moments return per_category(core_reduce_type(:sum)) { |s| s.sum } unless m m[:sum].copy # the moments sum IS the core fold (empty -> 0.0) end |
#variance ⇒ CArray
389 390 391 392 393 394 395 396 397 398 |
# File 'lib/carray/categorical_iterator.rb', line 389 def variance(axis: nil) return axis_by_masked_copy(axis, :variance) if axis m = moments return per_category(core_reduce_type(:variance)) { |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
473 474 475 476 477 478 479 480 481 |
# File 'lib/carray/categorical_iterator.rb', line 473 def variancep(axis: nil) return axis_by_masked_copy(axis, :variancep) if axis m = moments return per_category(core_reduce_type(:variancep)) { |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
600 601 602 603 604 605 |
# File 'lib/carray/categorical_iterator.rb', line 600 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
580 581 582 583 584 585 |
# File 'lib/carray/categorical_iterator.rb', line 580 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 |