Class: CACategoricalIterator

Inherits:
CAIterator show all
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

Attributes inherited from CAIterator

#ndim, #shape

Instance Method Summary collapse

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)

Returns the category vocabulary the results are aligned to.

Returns:



167
168
169
# File 'lib/carray/categorical_iterator.rb', line 167

def labels
  @labels
end

Instance Method Details

#accumulate ⇒ CArray #accumulate(axis:) ⇒ CArray

Overloads:

  • #accumulate ⇒ CArray

    Returns per-category sums folded in the value's own data type, wrapping at its width, as the core accumulate does. This is the exact in-type fold: sum reads its answer off a float64 moment and casts back, so it loses the low bits of a wide integer payload and does not wrap. An empty or fully-masked category accumulates the empty set, the additive identity 0 (unmasked).

    Returns:

  • #accumulate(axis:) ⇒ CArray

    Per-fiber per-category in-type sums along axis. Output shape = [K, ...source.shape without axis].

    Parameters:

    • axis (Integer) —

      reduce axis of the source value.

    Returns:



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

Returns the per-category all as boolean (matching CArray#all): true iff every present value is truthy (empty category -> true, vacuously). The value data type must be boolean, as for CArray#all.

Returns:



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

Returns the per-category any as boolean (matching CArray#any): true iff some present value is truthy (empty category -> false). The value data type must be boolean, as for CArray#any.

Returns:



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

Overloads:

  • #count(v = <none>) ⇒ CArray

    Per-category count, mirroring CArray#count per group. No argument returns #count_not_masked (present cells); count(UNDEF) returns #count_masked; count(v) counts cells whose value equals v.

    Returns:

  • #count(axis:) ⇒ CArray

    No-arg + axis: = per-fiber per-category count_not_masked (shape [K, ...band]). count(v, axis:) (value equality) and count(UNDEF, axis:) are not implemented; use them without axis:.

    Parameters:

    • axis (Integer)

    Returns:



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

Overloads:

  • #count_masked ⇒ CArray

    Returns the per-category count of masked (missing) values as int64. Empty categories are 0.

    Returns:

  • #count_masked(axis:) ⇒ CArray

    Not implemented; call it without axis:.

    Parameters:

    • axis (Integer)

    Returns:



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

Overloads:

  • #count_not_masked ⇒ CArray

    Returns the per-category count of present (non-masked) values as int64 — the denominator the value reductions actually divide by. Equals #elements unless the value carries a mask. A count is always defined, so an empty category is 0 (never masked).

    Returns:

  • #count_not_masked(axis:) ⇒ CArray

    Per-fiber per-category count of present (non-masked) values along axis (int64, shape [K, ...band]). Empty cells are 0.

    Parameters:

    • axis (Integer)

    Returns:



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.

Returns:



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.

Returns:



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.

Returns:



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.

Returns:



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.

Returns:



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

Yields each category's members (a CArray slice of the grouped copy, in #labels order; an empty category yields an empty array). Without a block, returns an Enumerator. This is the own iteration that drives the inherited Enumerable methods (map / count / to_a / ...); it does not use the CAIterator base each / kernel_at_addr path.

Yield Parameters:

Returns:

  • (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

Returns per-group cell counts (classified cells, including value-masked ones; = cat.category_sizes), a length-ngroups CArray aligned to #labels. The CAIterator count-family member — CArray#elements (structural, mask-independent) lifted per group.

Returns:



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

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.

Returns:

  • (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

Group-wise element-wise transform, mirroring CArray#map_slab. The block receives each category's members and returns either a same-length CArray (scattered back cell for cell) or a scalar (broadcast over the group's cells). Returns a NEW CArray shaped like the source value; the original is not modified (value[] = grp.map { ... } for in-place). Excluded cells (in no category) are UNDEF in the result.

Yield Parameters:

Returns:

  • (CArray) —

    shaped like the source value

Raises:

  • (LocalJumpError)


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

Overloads:

  • #max ⇒ CArray

    Returns per-category maxima in the value data type. Empty categories are MASKED.

    Returns:

  • #max(axis:) ⇒ CArray

    Per-fiber per-category maxima along axis (h's data type, masked where empty).

    Parameters:

    • axis (Integer)

    Returns:



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

Per-category flat source address of the maximum. See #min_addr.

Returns:



550
551
552
# File 'lib/carray/categorical_iterator.rb', line 550

def max_addr
  group_addr(max_index)
end

#max_index ⇒ CArray

Per-category group-local index of the maximum. See #min_index.

Returns:



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

Overloads:

  • #mean ⇒ CArray

    Returns per-category means as float64. Empty categories are MASKED.

    Returns:

  • #mean(axis:) ⇒ CArray

    Per-fiber per-category means (float64, empty group cells MASKED).

    Parameters:

    • axis (Integer)

    Returns:



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

Returns per-category medians as float64. Empty categories are MASKED.

Returns:



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

Overloads:

  • #min ⇒ CArray

    Returns per-category minima in the value data type. Empty categories are MASKED.

    Returns:

  • #min(axis:) ⇒ CArray

    Per-fiber per-category minima along axis (h's data type, masked where empty).

    Parameters:

    • axis (Integer)

    Returns:



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

Per-category flat source address of the minimum — which cell of the source value holds it, matching CArray#min_addr per group. Unlike #min_index (the group-local rank) this indexes back into the original array (value.reshape(value.elements)[grp.min_addr]). Empty categories MASKED.

Returns:



543
544
545
# File 'lib/carray/categorical_iterator.rb', line 543

def min_addr
  group_addr(min_index)
end

#min_index ⇒ CArray

Per-category group-local index of the minimum — the position within the category's members (source order) — matching CArray#min_index per group. Empty / all-masked categories are MASKED. Single-pass fused reduceat for numeric values; per-group fallback otherwise.

Returns:



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>

Overloads:

  • #minmax ⇒ Array<CArray>

    Returns the per-category [min, max] pair (each a length-k CArray in the value data type; empty categories MASKED), matching CArray#minmax. Both come from one moments pass.

    Returns:

  • #minmax(axis:) ⇒ Array<CArray>

    Per-fiber [min_ca, max_ca] along axis (each shape [K, ...band], h's data type, empty group cells MASKED). Ruby Array of two CArrays, not stacked. Both come from one kernel run.

    Parameters:

    • axis (Integer)

    Returns:



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

Returns the number of groups (= labels.size); the length of every per-group result CArray the reductions return.

Returns:

  • (Integer)


173
174
175
# File 'lib/carray/categorical_iterator.rb', line 173

def ngroups
  @k
end

#percentile(p) ⇒ CArray

Returns the per-category p-th percentile as float64 (p in 0..100, :linear interpolation, matching CArray#percentile). Empty categories are MASKED. Order statistics need every value of a group held together — this is the reduceat that only the eager grouped copy can serve.

Parameters:

  • p (Numeric) —

    percentile in 0..100.

Returns:



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

Overloads:

  • #prod ⇒ CArray

    Returns per-category products as float64 (matching CArray#prod). An empty / fully-masked category is 1.0 (the multiplicative identity). Single-pass reduceat for numeric values; per-group fallback otherwise.

    Returns:

  • #prod(axis:) ⇒ CArray

    Per-fiber per-category products (float64, shape [K, ...band]). Empty group cells 1.0 (identity).

    Parameters:

    • axis (Integer)

    Returns:



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>

Returns the per-category five-number summary [min, Q1, median, Q3, max] as five length-k float64 CArrays (matching CArray#quantile): the percentiles at 0 / 25 / 50 / 75 / 100. Empty / all-masked categories are MASKED. For a single fraction q in 0..1 use percentile(q * 100).

Returns:



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

Overloads:

  • #reduce({ |members| ... }) {|members| ... } ⇒ CArray

    Custom per-category reduction (the escape hatch for statistics not in the named surface), mirroring CArray#reduce_slab. The block receives each category's members (a CArray) and returns one value per category.

    Yield Parameters:

    Returns:

  • #reduce(init) ⇒ CArray

    Per-category fiber fold: each category's members are folded element by element starting from init.

    Parameters:

    • init (Object) —

      initial accumulator.

    Returns:

Raises:

  • (LocalJumpError)


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

Per-category sort by flat source address. Returns a length-nvalid (= elements.sum) int64 CArray of the flat SOURCE addresses that sort each category's members, in group-major order: segment c holds category c's source addresses in ascending-value order, segments concatenated in #labels order. So value.reshape(value.elements)[grp.sort_addr] yields the values grouped and sorted within each group, and splitting by the #elements prefix sum gives per-group. Excluded cells (in no category) are omitted. A masked value sorts to the tail of its segment (as CArray#sort sends masked cells to the end), so with a mask the first address is the minimum but the last is the masked cell, not the maximum.

Unlike #min_index / #max_index (group-local rank), this indexes back into the original array. There is no group-local sort surface: a group-local rank order is weak (the grouped copy is already category-contiguous), so only the source-address form is offered, mirroring #min_addr vs the skipped group-local min_index-into-source.

Returns:

  • (CArray) —

    length-nvalid int64



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

Returns per-category SAMPLE standard deviation (ddof=1) as float64. Matches CArray#stddev per group (empty / all-masked MASKED, single-value 0.0).

Returns:



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

Overloads:

  • #stddevp ⇒ CArray

    Per-category POPULATION standard deviation (ddof=0) as float64.

    Returns:

  • #stddevp(axis:) ⇒ CArray

    Per-fiber per-category population stddev (float64, empty group cells MASKED).

    Parameters:

    • axis (Integer)

    Returns:



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

Overloads:

  • #sum ⇒ CArray

    Returns per-category sums in the data type CArray#sum promotes the value to (float64 for an integer value). accumulate is the same fold kept in the value's own type. An empty or fully-masked category sums the empty set, which is the additive identity 0 (unmasked) — the same contract as CArray#sum on an empty / all-masked array.

    Returns:

  • #sum(axis:) ⇒ CArray

    Returns per-category sums per fiber along axis. Cat may be 1-D (case A, broadcasts across band axes), same rank as source (case B, per-fiber independent classifier), or one rank less (band-only, constant along reduce axis). Output shape = [K, ...source.shape without axis].

    Parameters:

    • axis (Integer) —

      reduce axis of the source value.

    Returns:



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

Returns per-category SAMPLE variance (ddof=1) as float64. Matches CArray#variance per group: an empty or fully-masked category is MASKED, a single-value category is 0.0 (CArray's n=1 contract), n>=2 is the sample variance.

Returns:



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

Per-category POPULATION variance (ddof=0) as float64, matching CArray#variancep: empty / all-masked -> MASKED, single value -> 0.0. Derived from the sample variance (variancep = variance * (n-1) / n), so it reuses the centred two-pass kernel with no extra walk.

Returns:



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

Overloads:

  • #wmean(weights) ⇒ CArray

    Per-category weighted mean as float64, matching CArray#wmean. Empty category -> MASKED; a present category whose weights sum to zero -> NaN (core's 0/0 contract).

    Parameters:

    Returns:

  • #wmean(weights, axis:) ⇒ CArray

    Per-fiber per-category weighted mean along axis. Same weights-shape contract as #wsum (weights.shape == source.shape). Empty cell → MASKED; a present cell whose weights sum to zero → NaN (0/0 core contract).

    Parameters:

    • weights (CArray)
    • axis (Integer)

    Returns:



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

Overloads:

  • #wsum(weights) ⇒ CArray

    Per-category weighted sum as float64, matching CArray#wsum. weights is a per-cell weight CArray in the source order (same elements as the value). Empty / all-masked category -> 0.0 (the additive identity). A cell is skipped iff its value OR its weight is masked (core's contract).

    Parameters:

    Returns:

  • #wsum(weights, axis:) ⇒ CArray

    Per-fiber per-category weighted sum along axis. weights must have shape == source.shape (rev3 requires explicit broadcast; wrap 1-D or band-shape weights via .broadcast_to(*source.shape) at the call site). Empty group cell → 0.0 (identity). Mask contract: cell contributes iff value AND weight are present.

    Parameters:

    • weights (CArray)
    • axis (Integer)

    Returns:



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