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[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

#labelsArray (readonly)

Returns the category vocabulary the results are aligned to.

Returns:



162
163
164
# File 'lib/carray/categorical_iterator.rb', line 162

def labels
  @labels
end

Instance Method Details

#allCArray

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

Returns:



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

#anyCArray

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

Returns:



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

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 deferred to Phase 3 of PROPOSAL_CATEGORICAL_REDUCE_AXIS.

    Parameters:

    • axis (Integer)

    Returns:



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_maskedCArray #count_masked(axis:) ⇒ CArray

Overloads:

  • #count_maskedCArray

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

    Returns:

  • #count_masked(axis:) ⇒ CArray

    Deferred to Phase 3 of PROPOSAL_CATEGORICAL_REDUCE_AXIS (needs a separate "assigned count" scatter that counts cells regardless of value mask).

    Parameters:

    • axis (Integer)

    Returns:



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_maskedCArray #count_not_masked(axis:) ⇒ CArray

Overloads:

  • #count_not_maskedCArray

    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:



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

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)


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

#elementsCArray 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:



178
179
180
# File 'lib/carray/categorical_iterator.rb', line 178

def elements
  @elements
end

#inspectString

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)


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

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)


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

#maxCArray #max(axis:) ⇒ CArray

Overloads:

  • #maxCArray

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

    Returns:

  • #max(axis:) ⇒ CArray

    Per-fiber per-category maxima along axis (h dtype, masked where empty).

    Parameters:

    • axis (Integer)

    Returns:



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_addrCArray

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

Returns:



516
517
518
# File 'lib/carray/categorical_iterator.rb', line 516

def max_addr
  group_addr(max_index)
end

#max_indexCArray

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

Returns:



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

#meanCArray #mean(axis:) ⇒ CArray

Overloads:

  • #meanCArray

    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:



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

#medianCArray

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

Returns:



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

#minCArray #min(axis:) ⇒ CArray

Overloads:

  • #minCArray

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

    Returns:

  • #min(axis:) ⇒ CArray

    Per-fiber per-category minima along axis (h dtype, masked where empty).

    Parameters:

    • axis (Integer)

    Returns:



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_addrCArray

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:



509
510
511
# File 'lib/carray/categorical_iterator.rb', line 509

def min_addr
  group_addr(min_index)
end

#min_indexCArray

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:



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

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

Overloads:

  • #minmaxArray<CArray>

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

    Returns:

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

    Per-fiber [min_ca, max_ca] along axis (each shape [K, ...band], h dtype, empty group cells MASKED). Ruby Array of two CArrays, not stacked.

    Parameters:

    • axis (Integer)

    Returns:



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

#ngroupsInteger

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

Returns:

  • (Integer)


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

def ngroups
  @k
end

#cumsumCArray #cumprodCArray #cummaxCArray #cumminCArray #cumcountCArray

Overloads:

  • #cumsumCArray

    Per-category inclusive running sum (float64), source-shaped.

    Returns:

  • #cumprodCArray

    Per-category inclusive running product (float64), source-shaped.

    Returns:

  • #cummaxCArray

    Per-category inclusive running maximum (value dtype), source-shaped.

    Returns:

  • #cumminCArray

    Per-category inclusive running minimum (value dtype), source-shaped.

    Returns:

  • #cumcountCArray

    Per-category 1-based within-category ordinal (int64), source-shaped.

    Returns:



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

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:



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

#prodCArray #prod(axis:) ⇒ CArray

Overloads:

  • #prodCArray

    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:



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

#quantileArray<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:



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

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)


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_addrCArray

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



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

#stddevCArray

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

Returns:



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

#stddevpCArray #stddevp(axis:) ⇒ CArray

Overloads:

  • #stddevpCArray

    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:



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

#sumCArray #sum(axis:) ⇒ CArray

Overloads:

  • #sumCArray

    Returns per-category sums in the value dtype. 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]. See PROPOSAL_CATEGORICAL_REDUCE_AXIS.

    Parameters:

    • axis (Integer)

      reduce axis of the source value.

    Returns:



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

#varianceCArray

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:



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

#variancepCArray

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:



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

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:



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

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:



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