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
-
#cumcount ⇒ CArray
Per-category 1-based within-category ordinal (int64), source-shaped.
-
#cummax ⇒ CArray
Per-category inclusive running maximum (value data type), source-shaped.
-
#cummin ⇒ CArray
Per-category inclusive running minimum (value data type), source-shaped.
-
#cumprod ⇒ CArray
Per-category inclusive running product (float64), source-shaped.
-
#cumsum ⇒ CArray
Per-category inclusive running sum (float64), source-shaped.
-
#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. -
#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 144 145 146 147 148 |
# 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 = counts.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; # A no-axis reduce has no grouped buffer to work from. The reason is # recorded here and raised from wherever one is asked for, so what # surfaces names the mismatch instead of being whatever NoMethodError # the nil produced first. mismatch = "group_by_category: value.elements (#{value.elements}) != " \ "cat.elements (#{cat.elements})" if value.ndim == 1 || ! [1, value.ndim - 1, value.ndim].include?(cat.ndim) raise ArgumentError, mismatch + (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 @no_flat = mismatch + ". This iterator answers only the per-fiber form, " \ "`.<reduce>(axis: k)`." end self end |
Instance Attribute Details
#labels ⇒ Array (readonly)
167 168 169 |
# File 'lib/carray/categorical_iterator.rb', line 167 def labels @labels end |
Instance Method Details
#accumulate ⇒ CArray #accumulate(axis:) ⇒ CArray
307 308 309 310 |
# File 'lib/carray/categorical_iterator.rb', line 307 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
449 450 451 452 |
# File 'lib/carray/categorical_iterator.rb', line 449 def all aa = all_any aa ? aa[:all] : per_category(CA_BOOLEAN) { |s| s.all } end |
#any ⇒ CArray
459 460 461 462 |
# File 'lib/carray/categorical_iterator.rb', line 459 def any aa = all_any aa ? aa[:any] : per_category(CA_BOOLEAN) { |s| s.any } end |
#count(v = <none>) ⇒ CArray #count(axis:) ⇒ CArray
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
# File 'lib/carray/categorical_iterator.rb', line 238 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
263 264 265 266 267 268 269 270 271 |
# File 'lib/carray/categorical_iterator.rb', line 263 def count_masked(axis: nil) if axis raise NotImplementedError, "CACategoricalIterator#count_masked(axis:) is not implemented — " \ "call it without axis:." end m = moments m ? counts - m[:count] : per_category(CA_INT64) { |s| s.count_masked } end |
#count_not_masked ⇒ CArray #count_not_masked(axis:) ⇒ CArray
221 222 223 224 225 |
# File 'lib/carray/categorical_iterator.rb', line 221 def count_not_masked(axis: nil) return axis_counts(axis) if axis m = moments m ? m[:count].copy : per_category(CA_INT64) { |s| s.count_not_masked } end |
#cumcount ⇒ CArray
Per-category 1-based within-category ordinal (int64), source-shaped.
715 716 717 |
# File 'lib/carray/categorical_iterator.rb', line 715 [:cumsum, :cumprod, :cummax, :cummin, :cumcount].each do |op| define_method(op) { scan(op) } end |
#cummax ⇒ CArray
Per-category inclusive running maximum (value data type), source-shaped.
715 716 717 |
# File 'lib/carray/categorical_iterator.rb', line 715 [:cumsum, :cumprod, :cummax, :cummin, :cumcount].each do |op| define_method(op) { scan(op) } end |
#cummin ⇒ CArray
Per-category inclusive running minimum (value data type), source-shaped.
715 716 717 |
# File 'lib/carray/categorical_iterator.rb', line 715 [:cumsum, :cumprod, :cummax, :cummin, :cumcount].each do |op| define_method(op) { scan(op) } end |
#cumprod ⇒ CArray
Per-category inclusive running product (float64), source-shaped.
715 716 717 |
# File 'lib/carray/categorical_iterator.rb', line 715 [:cumsum, :cumprod, :cummax, :cummin, :cumcount].each do |op| define_method(op) { scan(op) } end |
#cumsum ⇒ CArray
Per-category inclusive running sum (float64), source-shaped.
715 716 717 |
# File 'lib/carray/categorical_iterator.rb', line 715 [:cumsum, :cumprod, :cummax, :cummin, :cumcount].each do |op| define_method(op) { scan(op) } end |
#each({ |members| ... }) {|members| ... } ⇒ Enumerator, self
158 159 160 161 162 |
# File 'lib/carray/categorical_iterator.rb', line 158 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
183 184 185 186 187 188 189 190 |
# File 'lib/carray/categorical_iterator.rb', line 183 def elements # A copy, like every other member that reads off a memo: the memo is the # iterator's own state, and handing out the array itself lets a caller # write into it and change what the iterator answers from then on. This # one invites it -- the docs point at its prefix sum for splitting a # column apart, which reads as scratch. counts.copy end |
#inspect ⇒ String
201 202 203 204 205 206 207 208 |
# File 'lib/carray/categorical_iterator.rb', line 201 def inspect # An iterator that answers only the per-fiber form has no per-category # counts to show. Saying so beats both raising -- inspect is what you # reach for when something is already puzzling -- and printing an empty # list, which reads as a grouping that classified nothing. tail = @elements ? "elements=#{@elements.to_a.inspect}" : "per-fiber only" "#<#{self.class} ngroups=#{@k} labels=#{@labels.inspect} #{tail}>" end |
#map(data_type: nil) {|members| ... } ⇒ CArray
663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 |
# File 'lib/carray/categorical_iterator.rb', line 663 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
320 321 322 323 324 |
# File 'lib/carray/categorical_iterator.rb', line 320 def max(axis: nil) return axis_moments(axis)[:max] if axis m = moments m ? m[:max].copy : per_category(core_reduce_type(:max)) { |s| s.max } end |
#max_addr ⇒ CArray
550 551 552 |
# File 'lib/carray/categorical_iterator.rb', line 550 def max_addr group_addr(max_index) end |
#max_index ⇒ CArray
532 533 534 535 |
# File 'lib/carray/categorical_iterator.rb', line 532 def max_index am = arg_minmax am ? am[:max].copy : per_category(CA_INT64) { |s| s.max_index } end |
#mean ⇒ CArray #mean(axis:) ⇒ CArray
347 348 349 350 351 352 353 354 355 |
# File 'lib/carray/categorical_iterator.rb', line 347 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
360 361 362 363 |
# File 'lib/carray/categorical_iterator.rb', line 360 def median(axis: nil) axis_order_stat_defer!(:median) if axis percentile(50.0) end |
#min ⇒ CArray #min(axis:) ⇒ CArray
334 335 336 337 338 |
# File 'lib/carray/categorical_iterator.rb', line 334 def min(axis: nil) return axis_moments(axis)[:min] if axis m = moments m ? m[:min].copy : per_category(core_reduce_type(:min)) { |s| s.min } end |
#min_addr ⇒ CArray
543 544 545 |
# File 'lib/carray/categorical_iterator.rb', line 543 def min_addr group_addr(min_index) end |
#min_index ⇒ CArray
524 525 526 527 |
# File 'lib/carray/categorical_iterator.rb', line 524 def min_index am = arg_minmax am ? am[:min].copy : per_category(CA_INT64) { |s| s.min_index } end |
#minmax ⇒ Array<CArray> #minmax(axis:) ⇒ Array<CArray>
477 478 479 480 481 482 483 484 485 486 |
# File 'lib/carray/categorical_iterator.rb', line 477 def minmax(axis: nil) if axis # take both off one pass rather than asking min and max separately, # which would run the kernel twice now that nothing is kept between # calls -- this is the "keep the result" the axis: family expects m = axis_moments(axis) return [m[:min], m[:max]] end [min, max] end |
#ngroups ⇒ Integer
173 174 175 |
# File 'lib/carray/categorical_iterator.rb', line 173 def ngroups @k end |
#percentile(p) ⇒ CArray
372 373 374 375 376 377 378 379 380 |
# File 'lib/carray/categorical_iterator.rb', line 372 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
436 437 438 439 440 441 442 |
# File 'lib/carray/categorical_iterator.rb', line 436 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>
388 389 390 391 392 393 394 395 |
# File 'lib/carray/categorical_iterator.rb', line 388 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
639 640 641 642 643 644 645 646 647 648 649 650 651 652 |
# File 'lib/carray/categorical_iterator.rb', line 639 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
572 573 574 575 576 577 578 579 580 581 582 583 |
# File 'lib/carray/categorical_iterator.rb', line 572 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
419 420 421 422 423 424 |
# File 'lib/carray/categorical_iterator.rb', line 419 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
511 512 513 514 515 516 |
# File 'lib/carray/categorical_iterator.rb', line 511 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
287 288 289 290 291 292 |
# File 'lib/carray/categorical_iterator.rb', line 287 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
403 404 405 406 407 408 409 410 411 412 |
# File 'lib/carray/categorical_iterator.rb', line 403 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
494 495 496 497 498 499 500 501 502 |
# File 'lib/carray/categorical_iterator.rb', line 494 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
621 622 623 624 625 626 |
# File 'lib/carray/categorical_iterator.rb', line 621 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
601 602 603 604 605 606 |
# File 'lib/carray/categorical_iterator.rb', line 601 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 |