carray 3.0.5 → 3.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +233 -0
  3. data/README.md +1 -1
  4. data/carray.gemspec +3 -2
  5. data/ext/ca_axis_dispatch.c +6 -0
  6. data/ext/ca_axis_group.c +1 -1
  7. data/ext/ca_categorical_iterator.c +7 -7
  8. data/ext/ca_kernel_iterator.c +116 -231
  9. data/ext/ca_kernel_iterator.h +410 -254
  10. data/ext/ca_obj_bincmp.c +9 -1
  11. data/ext/ca_obj_binop.c +10 -1
  12. data/ext/ca_obj_bitfield.c +2 -0
  13. data/ext/ca_obj_byte_swap.c +2 -0
  14. data/ext/ca_obj_face.c +2 -0
  15. data/ext/ca_obj_fake.c +3 -1
  16. data/ext/ca_obj_grid.c +167 -12
  17. data/ext/ca_obj_meld.c +69 -113
  18. data/ext/ca_obj_moncmp.c +9 -1
  19. data/ext/ca_obj_monop.c +40 -2
  20. data/ext/ca_obj_object.c +54 -20
  21. data/ext/ca_obj_reduce.c +15 -1
  22. data/ext/ca_obj_refer.c +65 -20
  23. data/ext/ca_obj_remap.c +69 -10
  24. data/ext/ca_obj_roll.c +1 -0
  25. data/ext/ca_obj_select.c +4 -1
  26. data/ext/ca_obj_select_axis.c +1 -0
  27. data/ext/ca_obj_shift.c +1 -0
  28. data/ext/ca_obj_stack.c +310 -111
  29. data/ext/ca_obj_stride.c +131 -45
  30. data/ext/ca_obj_string.c +18 -2
  31. data/ext/ca_obj_tile.c +1 -0
  32. data/ext/ca_obj_triop.c +9 -1
  33. data/ext/ca_obj_window.c +1 -0
  34. data/ext/ca_op_ipower.c +8 -8
  35. data/ext/caframe_csv_reader.c +1 -1
  36. data/ext/carray.h +2 -1
  37. data/ext/carray_access.c +27 -64
  38. data/ext/carray_address_basis.c +134 -0
  39. data/ext/carray_attribute.c +8 -0
  40. data/ext/carray_broadcast.c +3 -4
  41. data/ext/carray_build_flags.h +1 -1
  42. data/ext/carray_cast.c +8 -11
  43. data/ext/carray_conversion.c +9 -5
  44. data/ext/carray_core.c +811 -72
  45. data/ext/carray_element.c +13 -9
  46. data/ext/carray_histogram.c +25 -3
  47. data/ext/carray_internal.h +147 -12
  48. data/ext/carray_kernels_bincmp.c +2007 -3
  49. data/ext/carray_kernels_binop.c +25365 -5199
  50. data/ext/carray_kernels_init.c +8 -8
  51. data/ext/carray_kernels_monop.c +10696 -3678
  52. data/ext/carray_kernels_reduce_aggregate.c +4429 -702
  53. data/ext/carray_kernels_reduce_boolean.c +1533 -15
  54. data/ext/carray_kernels_reduce_cumulative.c +1808 -248
  55. data/ext/carray_kernels_reduce_extreme.c +4508 -511
  56. data/ext/carray_kernels_reduce_variance.c +2284 -152
  57. data/ext/carray_kernels_scan.c +240 -240
  58. data/ext/carray_kernels_search.c +4211 -264
  59. data/ext/carray_kernels_sort.c +367 -450
  60. data/ext/carray_lazy.c +47 -0
  61. data/ext/carray_loop.c +1 -1
  62. data/ext/carray_mask.c +130 -139
  63. data/ext/carray_median_percentile.c +4 -4
  64. data/ext/carray_memory_view.c +13 -7
  65. data/ext/carray_operator.c +147 -44
  66. data/ext/carray_order.c +3 -2
  67. data/ext/carray_random.c +4 -1
  68. data/ext/carray_slab.c +97 -21
  69. data/ext/carray_sort.c +1 -1
  70. data/ext/carray_test.c +74 -19
  71. data/ext/carray_time_text.c +2 -2
  72. data/ext/extconf.rb +11 -0
  73. data/ext/mkkernel.rb +346 -94
  74. data/ext/ruby_carray.c +4 -1
  75. data/ext/version.h +4 -4
  76. data/lib/carray/attribute.rb +25 -1
  77. data/lib/carray/autoload_carray.rb +3 -3
  78. data/lib/carray/autoload_method_extension.rb +5 -3
  79. data/lib/carray/axis_group.rb +13 -7
  80. data/lib/carray/basics.rb +1 -1
  81. data/lib/carray/block_iterator.rb +17 -3
  82. data/lib/carray/categorical.rb +6 -8
  83. data/lib/carray/categorical_iterator.rb +2 -2
  84. data/lib/carray/complex.rb +2 -2
  85. data/lib/carray/conditional.rb +3 -2
  86. data/lib/carray/frame/column_verbs.rb +45 -14
  87. data/lib/carray/frame/convert.rb +19 -0
  88. data/lib/carray/frame/csv_parser.rb +32 -32
  89. data/lib/carray/frame/describe.rb +4 -2
  90. data/lib/carray/frame/frame.rb +399 -78
  91. data/lib/carray/frame/group.rb +679 -53
  92. data/lib/carray/frame/io.rb +209 -71
  93. data/lib/carray/frame/join.rb +146 -37
  94. data/lib/carray/frame/records.rb +3 -3
  95. data/lib/carray/frame/reshape.rb +89 -11
  96. data/lib/carray/frame/sort.rb +57 -9
  97. data/lib/carray/frame/verbs.rb +384 -49
  98. data/lib/carray/frame/windows.rb +44 -6
  99. data/lib/carray/inspect.rb +7 -2
  100. data/lib/carray/iterator.rb +32 -0
  101. data/lib/carray/lazy.rb +17 -8
  102. data/lib/carray/mask_gap_fill.rb +37 -17
  103. data/lib/carray/meld_reduce.rb +1 -1
  104. data/lib/carray/methods/choose.rb +3 -2
  105. data/lib/carray/methods/meshgrid.rb +3 -1
  106. data/lib/carray/runtime.rb +47 -8
  107. data/lib/carray/segment_iterator.rb +23 -8
  108. data/lib/carray/serialize.rb +34 -17
  109. data/lib/carray/stack.rb +18 -6
  110. data/lib/carray/time.rb +5 -0
  111. data/lib/carray/window_iterator.rb +16 -4
  112. data/yard-stubs/ca_obj_array.rb +26 -1
  113. data/yard-stubs/carray_cast.rb +8 -5
  114. data/yard-stubs/carray_conversion.rb +8 -2
  115. data/yard-stubs/carray_mask.rb +20 -1
  116. data/yard-stubs/carray_math.rb +31 -16
  117. data/yard-stubs/carray_memory_view.rb +7 -0
  118. data/yard-stubs/carray_test.rb +7 -4
  119. metadata +18 -4
@@ -6,7 +6,57 @@
6
6
  # -> group_by_category, so the frame layer only builds the key and hands
7
7
  # the group iterator back (exposure, §4.3).
8
8
 
9
+ require "carray/distance_limit"
10
+
9
11
  class CAFrame
12
+ # How many rows hold each value of a column, or each combination of the
13
+ # values of several (every column by default), as a frame: the values as
14
+ # its index (one column) or its first columns (several), and their counts
15
+ # in a column +name:+ ("count"). +sort:+ orders the rows by +:count+, the
16
+ # most first (the default; ties in the order the values first appear), by
17
+ # +:value+, or not at all (+false+, the order the values first appear). A
18
+ # row with a masked value is not counted, as +group_by+ leaves it out.
19
+ #
20
+ # df.value_counts("station")
21
+ # df.value_counts("station", "weather", sort: :value)
22
+ #
23
+ # The columns are given as +select+ takes them. +group_by(...).size+ is the
24
+ # same count in the order of the groups.
25
+ #
26
+ # @return [CAFrame]
27
+ # @raise [ArgumentError] for another +sort:+, or a +name:+ a key has.
28
+ def value_counts (*names, sort: :count, name: "count")
29
+ unless [:count, :value, false].include?(sort)
30
+ raise ArgumentError, "value_counts: sort: is :count, :value or false (got #{sort.inspect})"
31
+ end
32
+ keys = names.empty? ? column_names : names_selected(names, "value_counts")
33
+ if keys.empty?
34
+ hint = @index ? "; name the index (#{@axis_name.inspect}) to count its values" : ""
35
+ raise ArgumentError, "value_counts: the frame has no columns#{hint}"
36
+ end
37
+ name = column_name_arg(name, "value_counts")
38
+ if keys.include?(name)
39
+ raise ArgumentError,
40
+ "value_counts: a counted column is named #{name.inspect}; name the count " \
41
+ "otherwise, as in value_counts(..., name: \"n\")"
42
+ end
43
+ out = group_by(*keys).size(name: name)
44
+ case sort
45
+ when :count then out.sort_by_key(name, descending: true)
46
+ when :value
47
+ # A categorical key is ordered by its labels, as CArray#value_counts
48
+ # orders one; its codes are in the order labels were met.
49
+ cols = keys.map do |key|
50
+ col = out.__send__(:key_column, key, "value_counts")
51
+ col.is_a?(CACategorical) ? col.to_type(:object) : col
52
+ end
53
+ out.__send__(:select_rows, CArray.sort_addr(*cols))
54
+ else out
55
+ end
56
+ rescue ArgumentError, KeyError, TypeError, CArray::DataTypeError => e
57
+ raise e.class, e.message.sub(/\A(group_by|size|sort_by_key):/, "value_counts:"), e.backtrace
58
+ end
59
+
10
60
  # Group rows by one or more keys. Each key is a column name (String) or an
11
61
  # external length-N CArray. Returns a GroupedFrame.
12
62
  #
@@ -234,6 +284,348 @@ class CAFrame
234
284
  end
235
285
  col
236
286
  end
287
+
288
+ # Each column reduced over all the rows, as a frame of one row: the
289
+ # reductions of +GroupedFrame+ with every row in one group. Given names, the
290
+ # frame of those columns; without, of every column whose type has the
291
+ # reduction. A column keeps its type where the reduction keeps it (the
292
+ # earliest time of a time column is a time), and +min_count:+ /
293
+ # +fill_value:+ / +keep_rows:+ work as they do for a group. A frame with no
294
+ # rows still gives its one row: a sum of nothing is 0, a mean UNDEF.
295
+ #
296
+ # df.mean # => one row, the mean of every numeric column
297
+ # df.max("tmax", "tmin") # => one row, the two maxima
298
+ # df["tmax"] - df.mean("tmax", keep_rows: true) # the anomaly on each row
299
+ #
300
+ # For one column's value as a Ruby value, reduce the column: +df["tmax"].mean+.
301
+ #
302
+ # @!method sum(*names, min_count: nil, fill_value: nil, keep_rows: false)
303
+ # @return [CAFrame] one row.
304
+ # @!method mean(*names, min_count: nil, fill_value: nil, keep_rows: false)
305
+ # @return [CAFrame] one row.
306
+ # @!method min(*names, min_count: nil, fill_value: nil, keep_rows: false)
307
+ # @return [CAFrame] one row.
308
+ # @!method max(*names, min_count: nil, fill_value: nil, keep_rows: false)
309
+ # @return [CAFrame] one row.
310
+ # @!method prod(*names, min_count: nil, fill_value: nil, keep_rows: false)
311
+ # @return [CAFrame] one row.
312
+ # @!method accumulate(*names, min_count: nil, fill_value: nil, keep_rows: false)
313
+ # @return [CAFrame] one row.
314
+ # @!method median(*names, min_count: nil, fill_value: nil, keep_rows: false)
315
+ # @return [CAFrame] one row.
316
+ # @!method variance(*names, min_count: nil, fill_value: nil, keep_rows: false)
317
+ # @return [CAFrame] one row.
318
+ # @!method stddev(*names, min_count: nil, fill_value: nil, keep_rows: false)
319
+ # @return [CAFrame] one row.
320
+ # @!method variancep(*names, min_count: nil, fill_value: nil, keep_rows: false)
321
+ # @return [CAFrame] one row.
322
+ # @!method stddevp(*names, min_count: nil, fill_value: nil, keep_rows: false)
323
+ # @return [CAFrame] one row.
324
+ # @!method first(*names, keep_rows: false)
325
+ # @return [CAFrame] one row: each column's first present value.
326
+ # @!method last(*names, keep_rows: false)
327
+ # @return [CAFrame] one row: each column's last present value.
328
+ # @!method nunique(*names, keep_rows: false)
329
+ # @return [CAFrame] one row: each column's number of distinct present values.
330
+ # @!method count(*names, keep_rows: false)
331
+ # @return [CAFrame] one row: each column's number of present values.
332
+ # @!method all(*names, keep_rows: false)
333
+ # @return [CAFrame] one row, for the boolean columns.
334
+ # @!method any(*names, keep_rows: false)
335
+ # @return [CAFrame] one row, for the boolean columns.
336
+ CArray.send(:define_named_methods, self,
337
+ [:sum, :mean, :min, :max, :prod, :accumulate, :median,
338
+ :variance, :stddev, :variancep, :stddevp], :reduce_all_rows)
339
+
340
+ CArray.send(:define_named_methods, self,
341
+ [:first, :last, :nunique, :count, :all, :any], :reduce_all_rows_to_a_value)
342
+
343
+ # Each column's +p+-th percentile (0 to 100) over all the rows, as a frame
344
+ # of one row, as {#median} gives the 50th. The percentage comes first, then
345
+ # the names.
346
+ #
347
+ # df.percentile(90, "revenue")
348
+ #
349
+ # @param p [Numeric] the percentage.
350
+ # @param names [Array<String>] the columns; default every column that has
351
+ # a percentile.
352
+ # @return [CAFrame, CArray] one row; with +keep_rows: true+ the frame's
353
+ # rows, one name giving the CArray.
354
+ def percentile (p, *names, min_count: nil, fill_value: nil, keep_rows: false)
355
+ CAFrame.__send__(:check_percentage, p)
356
+ reduce_all_rows(:percentile, *names, min_count: min_count, fill_value: fill_value,
357
+ keep_rows: keep_rows, args: [p])
358
+ end
359
+
360
+ # Each column's five quartile percentiles over all the rows, as
361
+ # +CArray#quantile+ gives them, as a frame of one row: a column "x" gives
362
+ # the five columns "x_p0", "x_p25", "x_p50", "x_p75" and "x_p100". With
363
+ # +keep_rows: true+ every row holds them.
364
+ #
365
+ # df.quantile("temp")
366
+ #
367
+ # @param names [Array<String>] the columns; default every column that has
368
+ # a quantile.
369
+ # @return [CAFrame]
370
+ def quantile (*names, keep_rows: false)
371
+ named = {}
372
+ ColumnWalk.new(self).over(:quantile, names) do |col, name|
373
+ named[name] = col.quantile(axis: 0, keep_axis: true).map do |q|
374
+ keep_rows ? q[CArray.int64(@nrow).fill(0), *([nil] * (q.ndim - 1))].copy : q
375
+ end
376
+ end
377
+ cols = CAFrame.__send__(:quantile_columns, named)
378
+ return CAFrame.new(cols) unless keep_rows
379
+ return CAFrame.new(cols, axis_name: @axis_name, index: @index)
380
+ end
381
+
382
+ # The five columns a quantile gives a column, by name.
383
+ def self.quantile_columns(named)
384
+ named.each_with_object({}) do |(name, quartiles), cols|
385
+ QUANTILE_SUFFIXES.zip(quartiles) { |suffix, q| cols["#{name}_#{suffix}"] = q }
386
+ end
387
+ end
388
+ private_class_method :quantile_columns
389
+
390
+ QUANTILE_SUFFIXES = %w[p0 p25 p50 p75 p100].freeze
391
+ private_constant :QUANTILE_SUFFIXES
392
+
393
+ private def reduce_all_rows (reduction, *names, min_count: nil, fill_value: nil, keep_rows: false, args: [])
394
+ if fill_value.is_a?(Hash)
395
+ unknown = fill_value.keys - column_names
396
+ unless unknown.empty?
397
+ raise ArgumentError,
398
+ "#{reduction}: fill_value names no column #{unknown.map(&:inspect).join(', ')}"
399
+ end
400
+ end
401
+ return reduce_down_the_rows(reduction, names, keep_rows, args) do |name|
402
+ fill = fill_value.is_a?(Hash) ? fill_value[name] : fill_value
403
+ { min_count: min_count, fill_value: fill }.compact
404
+ end
405
+ end
406
+
407
+ private def reduce_all_rows_to_a_value (reduction, *names, keep_rows: false)
408
+ return reduce_down_the_rows(reduction, names, keep_rows) { |_| {} }
409
+ end
410
+
411
+ # Each named column, or every column whose type has the reduction,
412
+ # reduced by the column itself along the rows to one row (keep_axis), so
413
+ # it keeps its type as the column's reduction keeps it. The block gives a
414
+ # column's keywords. A frame of one row with no index; with +keep_rows+,
415
+ # each row holding the result, as a group's +keep_rows+ gives it.
416
+ private def reduce_down_the_rows (reduction, names, keep_rows, args = [])
417
+ meth = reduction == :count ? :count_not_masked : reduction
418
+ named = {}
419
+ out = ColumnWalk.new(self).over(reduction, names, [], args) do |col, name|
420
+ result = col.public_send(meth, *args, axis: 0, keep_axis: true, **yield(name))
421
+ if keep_rows
422
+ result = result[CArray.int64(@nrow).fill(0), *([nil] * (result.ndim - 1))].copy
423
+ end
424
+ named[name] = result
425
+ end
426
+ return CAFrame.new(named) unless keep_rows
427
+ return out if out.is_a?(CArray)
428
+ return CAFrame.new(named, axis_name: @axis_name, index: @index)
429
+ end
430
+
431
+ # Values computed down the rows, one per row, as +GroupedFrame+ computes
432
+ # them within each group, with every row in one group: each row's rank
433
+ # (+order+, int64, 0 the smallest) and the running values up to and
434
+ # including it. Given names, one name gives the CArray and several a frame
435
+ # of those columns; without names, a frame of every column whose type has
436
+ # the operation. A frame is on the frame's rows and index.
437
+ #
438
+ # df["total"] = df.cumsum("rain")
439
+ # df["rank"] = df.order("temp", descending: true)
440
+ #
441
+ # @!method order(*names, descending: false, method: :ordinal, kind: :quick)
442
+ # @return [CAFrame, CArray]
443
+ # @!method cumsum(*names)
444
+ # @return [CAFrame, CArray]
445
+ # @!method cumprod(*names)
446
+ # @return [CAFrame, CArray]
447
+ # @!method cummax(*names)
448
+ # @return [CAFrame, CArray]
449
+ # @!method cummin(*names)
450
+ # @return [CAFrame, CArray]
451
+ # @!method cumcount(*names)
452
+ # @return [CAFrame, CArray]
453
+ CArray.send(:define_named_methods, self,
454
+ [:order, :cumsum, :cumprod, :cummax, :cummin, :cumcount], :run_down_all_rows)
455
+
456
+ private def run_down_all_rows (operation, *names, **keywords)
457
+ # The keywords are the core's to check, before any column is read.
458
+ CArray.float64(1).public_send(operation, **keywords)
459
+ out = ColumnWalk.new(self).over(operation, names, [], [], keywords) do |col, _|
460
+ col.public_send(operation, **(col.ndim > 1 ? keywords.merge(axis: 0) : keywords))
461
+ end
462
+ return out if out.is_a?(CArray)
463
+ return CAFrame.new(out, axis_name: @axis_name, index: @index)
464
+ end
465
+
466
+ # The frame's own walk over its columns: the one rule of a frame-wide verb
467
+ # (ColumnVerbs) -- which columns, and the shape of the answer -- for the
468
+ # verbs that run down all the rows, each column computed by itself.
469
+ class ColumnWalk
470
+ include CAFrame::ColumnVerbs
471
+
472
+ def initialize (frame)
473
+ @frame = frame
474
+ end
475
+
476
+ def over (verb, names, key_names = [], args = [], keywords = {}, &block)
477
+ return over_named_columns(verb, names, key_names, args, keywords, &block)
478
+ end
479
+ end
480
+ private_constant :ColumnWalk
481
+
482
+ # The values of the columns moved along the rows: +n:+ rows back (a
483
+ # negative +n+ looks ahead), or the value at an exact distance of the key
484
+ # -- +before: "1 hour"+ gives each row the value of the row whose key is
485
+ # one hour earlier, +after:+ one hour later. The key is the index, or the
486
+ # column +on:+ names; it is a time, a duration or a number, and the
487
+ # distance is a duration such as "1 hour" (or a CATimedelta value) or a
488
+ # number accordingly. A row with no row at that key gets UNDEF; so does a
489
+ # row whose key is masked. The rows need not be in order. The result is
490
+ # new arrays in every form, so writing to it, or filling it once it is
491
+ # stored as a column, does not reach the column it came from.
492
+ #
493
+ # df["dtemp"] = df["temp"] - df.lag("temp", before: "1 hour")
494
+ # df.lag("temp", n: 1) # the previous row
495
+ #
496
+ # +GroupedFrame#lag+ does the same within each group. Given names, one
497
+ # name gives the CArray and several a frame of those columns; without
498
+ # names, a frame of every column (the +on:+ column left out). A frame is
499
+ # on the frame's rows and index. A result shows the column's attributes, as +CArray#lag+'s
500
+ # does.
501
+ #
502
+ # @param names [Array<String>] the columns.
503
+ # @param n [Integer] rows back.
504
+ # @param before [String, CATimedelta::Element, Numeric] the distance back.
505
+ # @param after [String, CATimedelta::Element, Numeric] the distance ahead.
506
+ # @param on [String] the key column, with +before:+ or +after:+ (default the
507
+ # index).
508
+ # @return [CArray, CAFrame]
509
+ # @raise [ArgumentError] when +n:+, +before:+ and +after:+ are combined,
510
+ # when +on:+ comes without a distance, when there is no key, or when the
511
+ # key repeats (the row at a key would not be one row).
512
+ def lag (*names, n: nil, before: nil, after: nil, on: nil)
513
+ distance = lag_distance("lag", n, before, after, on)
514
+ walk = ColumnWalk.new(self)
515
+ if distance.nil?
516
+ step = n.nil? ? 1 : n
517
+ cols = walk.over(:lag, names, [], [step]) do |col, _|
518
+ (col.ndim > 1 ? col.lag(step, axis: 0) : col.lag(step)).copy
519
+ end
520
+ else
521
+ addr = lag_addresses("lag", [CArray.int64(nrow).seq], *distance)
522
+ cols = walk.over(:lag, names, [distance.first], [1]) do |col, _|
523
+ col[addr, *([nil] * (col.ndim - 1))].copy
524
+ end
525
+ end
526
+ return cols if cols.is_a?(CArray)
527
+ CAFrame.new(cols, axis_name: @axis_name, index: @index)
528
+ end
529
+
530
+ # [key name, key, sign, width] for a lag by distance; nil for one by rows.
531
+ private def lag_distance (verb, n, before, after, on)
532
+ if before.nil? && after.nil?
533
+ raise ArgumentError, "#{verb}: on: goes with before: or after:" unless on.nil?
534
+ return nil
535
+ end
536
+ unless n.nil?
537
+ raise ArgumentError, "#{verb}: give n: (rows) or before: / after: (a distance of the key), not both"
538
+ end
539
+ unless before.nil? || after.nil?
540
+ raise ArgumentError, "#{verb}: give before: or after:, not both"
541
+ end
542
+ if on.nil?
543
+ unless @index
544
+ raise ArgumentError, "#{verb}: the frame has no index; name the key column with on:"
545
+ end
546
+ on_name = @axis_name
547
+ else
548
+ on_name = column_key(on, verb)
549
+ end
550
+ key = key_column(on_name, verb)
551
+ unless key.is_a?(CATime) || key.is_a?(CATimedelta) || (!key.face? && key.numeric?)
552
+ raise ArgumentError,
553
+ "#{verb}: the key #{on_name.inspect} is #{CArray::Inspector.type_label(key)}; " \
554
+ "a distance is taken along a time, a duration or a number"
555
+ end
556
+ label = before.nil? ? "#{verb}: after:" : "#{verb}: before:"
557
+ width = CArray::DistanceLimit.read(before || after, key, label)
558
+ return [on_name, key, before.nil? ? :+ : :-, width]
559
+ end
560
+
561
+ # For each row, the address of the row of its row set whose key is its own
562
+ # key moved by the width; masked where there is none. A row in no set is
563
+ # masked too, and so is a row whose key is NaN or infinite, which is no
564
+ # distance from any key (as in windows).
565
+ private def lag_addresses (verb, row_sets, _on_name, key, sign, width)
566
+ key = key.mask_invalid if key.float?
567
+ addr = CArray.int64(nrow)
568
+ addr[] = UNDEF
569
+ row_sets.each do |rows|
570
+ keys = key[rows]
571
+ next if keys.count_not_masked == 0
572
+ if keys.nunique < keys.count_not_masked
573
+ raise ArgumentError,
574
+ "#{verb}: the key repeats#{row_sets.size > 1 ? " within a group" : ""}, " \
575
+ "so the row at a key is not one row"
576
+ end
577
+ addr[rows] = rows[lag_target(keys, sign, width).locate_addr(keys)]
578
+ end
579
+ return addr
580
+ end
581
+
582
+ # The keys moved by the width. A time is moved on the finer of its unit and
583
+ # the width's, so that an hour back from a day is not rounded onto the day.
584
+ private def lag_target (keys, sign, width)
585
+ if width.is_a?(CATimedelta::Element) &&
586
+ CATimeUnitAlgebra.same_group?(keys.unit.base, width.unit.base)
587
+ keys = keys.to_unit(CATimeUnitAlgebra.finer(keys.unit, width.unit))
588
+ end
589
+ return lag_integer_target(keys, sign, width) if keys.integer?
590
+ return keys.public_send(sign, width)
591
+ end
592
+
593
+ LAG_UNSIGNED = [CA_UINT8, CA_UINT16, CA_UINT32, CA_UINT64].freeze
594
+ private_constant :LAG_UNSIGNED
595
+
596
+ # Integer keys moved by the width exactly, in int64 (or as Ruby integers
597
+ # for a 64-bit key a step does not fit in). A target outside the key's
598
+ # range is masked, so it matches no row rather than wrapping onto one; a
599
+ # width that is not a whole number matches no row either.
600
+ private def lag_integer_target (keys, sign, width)
601
+ bits = keys.bytes * 8
602
+ if LAG_UNSIGNED.include?(keys.data_type)
603
+ lo, hi = 0, 2**bits - 1
604
+ else
605
+ lo, hi = -2**(bits - 1), 2**(bits - 1) - 1
606
+ end
607
+ step = width.is_a?(Integer) ? width : nil
608
+ if step.nil? && width.respond_to?(:finite?) && width.finite? && width == width.floor
609
+ step = width.to_i
610
+ end
611
+ if step.nil? || step > hi - lo
612
+ out = CArray.int64(*keys.shape)
613
+ out[] = UNDEF
614
+ return out
615
+ end
616
+ if bits < 64
617
+ return keys.to_type(CA_INT64).public_send(sign, step)
618
+ end
619
+ if step > 2**63 - 1
620
+ moved = keys.to_type(CA_OBJECT).public_send(sign, step)
621
+ moved[moved.lt(lo).or(moved.gt(hi))] = UNDEF
622
+ return moved
623
+ end
624
+ out = keys.copy
625
+ out[sign == :- ? keys.lt(lo + step) : keys.gt(hi - step)] = UNDEF
626
+ return out.public_send(sign, step)
627
+ end
628
+
237
629
  end
238
630
 
239
631
  # GroupedFrame — the result of +CAFrame#group_by+ (memo §11.6, §16).
@@ -281,6 +673,40 @@ class GroupedFrame
281
673
  @labels.size
282
674
  end
283
675
 
676
+ # The number of rows in each group, as a frame of one column, "size"
677
+ # unless +name:+ says otherwise, labelled as a reduction's result is. A row
678
+ # whose key is masked is in no group, so the sizes need not add up to the
679
+ # frame's nrow.
680
+ #
681
+ # @param name [String] the result column's name.
682
+ # @return [CAFrame]
683
+ # @raise [ArgumentError] when a key has that name.
684
+ def size (name: "size")
685
+ refuse_changed_rows("size")
686
+ name = @frame.__send__(:column_name_arg, name, "size")
687
+ if @key_names.include?(name) || @axis_name == name
688
+ raise ArgumentError,
689
+ "size: a key is named #{name.inspect}; name the result otherwise, as in size(name: \"n\")"
690
+ end
691
+ return result_frame({ name => @cat.category_sizes.int64 }, false)
692
+ end
693
+
694
+ # Each group's label (an Array of the key values for several keys) and its
695
+ # rows as a sub-frame sharing the frame's columns, in the order of
696
+ # +labels+. Without a block, an Enumerator.
697
+ #
698
+ # df.group_by("station").each { |station, rows| puts station, rows.nrow }
699
+ #
700
+ # @yieldparam label [Object, Array] the group's key value(s).
701
+ # @yieldparam rows [CAFrame] the group's rows.
702
+ # @return [GroupedFrame] self
703
+ def each
704
+ return enum_for(:each) { ngroup } unless block_given?
705
+ refuse_changed_rows("each")
706
+ ngroup.times { |k| yield @labels[k], @frame[group_address(k)] }
707
+ return self
708
+ end
709
+
284
710
  # Group key values (one per group), as an Array.
285
711
  def labels
286
712
  @labels.dup
@@ -367,6 +793,12 @@ class GroupedFrame
367
793
  cols = {}
368
794
  spec.each do |out_name, (in_name, reduction, *args)|
369
795
  out = @frame.__send__(:column_name_arg, out_name, "aggregate")
796
+ if in_name.is_a?(Symbol)
797
+ raise ArgumentError,
798
+ "aggregate: each entry is result name => [column, reduction] " \
799
+ "(got #{out_name.inspect} => #{spec[out_name].inspect}); " \
800
+ "as in aggregate(\"mean_#{out_name}\" => [#{out_name.inspect}, :mean])"
801
+ end
370
802
  @frame.__send__(:column_key, in_name, "aggregate")
371
803
  options = args.last.is_a?(Hash) ? args.pop : {}
372
804
  cols[out] =
@@ -426,20 +858,24 @@ class GroupedFrame
426
858
  # fills those columns only. +keep_rows: true+ gives each row its group's value
427
859
  # instead of one row per group, as for +aggregate+.
428
860
  #
429
- # Given column names, they reduce those columns only and return them as
430
- # +df[...]+ does: one name gives the CArray, several an Array of them.
861
+ # Given column names, they reduce those columns only, and the result is
862
+ # the frame of those columns, one row per group, labelled as above; with
863
+ # +keep_rows: true+ one comes back as +df[name]+ returns it, a CArray on
864
+ # the frame's rows, and several as a frame on those rows. For a group's
865
+ # values alone, reduce the column's group iterator: +grp["temp"].mean+.
431
866
  #
432
867
  # grp.mean # a frame, one row per group
433
- # grp.mean("temp") # CArray, one value per group
868
+ # grp.mean("temp") # a frame of temp, one row per group
434
869
  # df["temp"] - grp.mean("temp", keep_rows: true) # anomaly from the group mean
435
870
  #
436
871
  # @!method sum(*names, min_count: nil, fill_value: nil, keep_rows: false)
437
872
  # Returns a frame of the per-group sum of every column whose type has a
438
873
  # sum, an N-D one per group along the rows. Other columns are left out.
439
- # @return [CAFrame, CArray, Array<CArray>] a frame with one row per
440
- # group, indexed by the group labels, or with keep_rows: true the
441
- # frame's rows and index; given names, the CArray (one) or the Array of
442
- # them (several).
874
+ # @return [CAFrame, CArray] a frame with one row per
875
+ # group, indexed by the group labels (of the named columns when names
876
+ # are given), or with keep_rows: true the frame's rows and index; given
877
+ # names and keep_rows: true, the CArray (one) or a frame of them on the
878
+ # frame's rows (several).
443
879
  # @!method mean(*names, min_count: nil, fill_value: nil, keep_rows: false)
444
880
  # Returns a frame of the per-group mean of every column whose type has
445
881
  # a mean, as {#sum} does.
@@ -483,6 +919,43 @@ class GroupedFrame
483
919
  reduce_columns(red, names, keep_rows, min_count: min_count, fill_value: fill_value)
484
920
  end
485
921
 
922
+ # Each group's +p+-th percentile (0 to 100), as {#median} gives the 50th
923
+ # and {#sum} gives its sums. The percentage comes first, then the names.
924
+ #
925
+ # grp.percentile(90, "revenue")
926
+ #
927
+ # @param p [Numeric] the percentage.
928
+ # @param names [Array<String>] the columns; default every column that has
929
+ # a percentile.
930
+ # @return [CAFrame, CArray] one row per group, as {#sum} returns.
931
+ def percentile (p, *names, min_count: nil, fill_value: nil, keep_rows: false)
932
+ CAFrame.__send__(:check_percentage, p)
933
+ refuse_changed_rows(:percentile)
934
+ reduce_columns(:percentile, names, keep_rows, args: [p], min_count: min_count, fill_value: fill_value)
935
+ end
936
+
937
+ # Each group's five quartile percentiles, as +CAFrame#quantile+ gives them
938
+ # for all the rows: a column "x" gives "x_p0", "x_p25", "x_p50", "x_p75"
939
+ # and "x_p100", one row per group, labelled as {#sum} labels its result;
940
+ # with +keep_rows: true+ on the frame's rows.
941
+ #
942
+ # grp.quantile("temp")
943
+ #
944
+ # @param names [Array<String>] the columns; default every column that has
945
+ # a quantile.
946
+ # @return [CAFrame]
947
+ def quantile (*names, keep_rows: false)
948
+ refuse_changed_rows(:quantile)
949
+ refuse_key_as_result(:quantile, names) unless keep_rows
950
+ named = {}
951
+ over_named_columns(:quantile, names, @key_names) do |col, name|
952
+ named[name] = group_reduce(col, :quantile).map { |q| keep_rows ? spread_to_rows(q) : q }
953
+ end
954
+ cols = CAFrame.__send__(:quantile_columns, named)
955
+ return result_frame(cols, false) unless keep_rows
956
+ return CAFrame.new(cols, axis_name: @frame.axis_name, index: @frame.index)
957
+ end
958
+
486
959
  # The same convenience for the reductions that take no min_count: or
487
960
  # fill_value:, as the core ones take none: a value of the group (first,
488
961
  # last), a count (nunique, count) or a boolean fold (all, any).
@@ -522,8 +995,8 @@ class GroupedFrame
522
995
  # group's rows, a running statistic. A row in no group is UNDEF. Without
523
996
  # names they give a frame of every column whose type has the operation
524
997
  # (the key columns left out), on the frame's rows and index; given names,
525
- # the CArray (one) or the Array of them (several), as +df[...]+ returns
526
- # columns. An N-D column runs along the rows.
998
+ # the CArray (one), as +df[name]+ returns a column, or a frame of those
999
+ # columns (several) on the frame's rows and index. An N-D column runs along the rows.
527
1000
  #
528
1001
  # df["dtemp"] = df["temp"] - grp.lag("temp") # change since the station's previous row
529
1002
  # grp.order("temp", descending: true) # rank within the station
@@ -532,31 +1005,43 @@ class GroupedFrame
532
1005
  # "Previous" is the previous row of the frame: sort the frame first
533
1006
  # (+sort_by_key("time")+) when its rows are not in time order.
534
1007
  #
535
- # @!method lag(*names, n: 1)
1008
+ # @!method lag(*names, n: 1, before: nil, after: nil, on: nil)
536
1009
  # Returns each row's value +n+ rows back in its own group, UNDEF where
537
- # the group has none, as +CArray#lag+; a negative +n+ looks ahead.
538
- # @return [CAFrame, CArray, Array<CArray>]
1010
+ # the group has none, as +CArray#lag+; a negative +n+ looks ahead. With
1011
+ # +before:+ (or +after:+) instead, the value of the group's row whose key
1012
+ # (the index, or the column +on:+ names) is that distance earlier (or
1013
+ # later), as +CAFrame#lag+ gives it.
1014
+ # @return [CAFrame, CArray]
539
1015
  # @!method order(*names, descending: false, method: :ordinal, kind: :quick)
540
1016
  # Returns each row's rank among its group's rows (int64, 0 the
541
1017
  # smallest), as +CArray#order+ ranks them.
542
- # @return [CAFrame, CArray, Array<CArray>]
1018
+ # @return [CAFrame, CArray]
543
1019
  # @!method cumsum(*names)
544
1020
  # Returns the running sum within each group, as +CArray#cumsum+.
545
- # @return [CAFrame, CArray, Array<CArray>]
1021
+ # @return [CAFrame, CArray]
546
1022
  # @!method cumprod(*names)
547
1023
  # Returns the running product within each group.
548
- # @return [CAFrame, CArray, Array<CArray>]
1024
+ # @return [CAFrame, CArray]
549
1025
  # @!method cummax(*names)
550
1026
  # Returns the running maximum within each group.
551
- # @return [CAFrame, CArray, Array<CArray>]
1027
+ # @return [CAFrame, CArray]
552
1028
  # @!method cummin(*names)
553
1029
  # Returns the running minimum within each group.
554
- # @return [CAFrame, CArray, Array<CArray>]
1030
+ # @return [CAFrame, CArray]
555
1031
  # @!method cumcount(*names)
556
1032
  # Returns the running count of present values within each group (int64).
557
- # @return [CAFrame, CArray, Array<CArray>]
558
- def lag (*names, n: 1)
559
- transform_columns(:lag, names, [n], {})
1033
+ # @return [CAFrame, CArray]
1034
+ def lag (*names, n: nil, before: nil, after: nil, on: nil)
1035
+ distance = @frame.__send__(:lag_distance, "lag", n, before, after, on)
1036
+ return transform_columns(:lag, names, [n.nil? ? 1 : n], {}) if distance.nil?
1037
+ refuse_changed_rows("lag")
1038
+ refuse_key_on_rows(:lag, names)
1039
+ addr = @frame.__send__(:lag_addresses, "lag", (0...ngroup).map { |k| group_address(k) }, *distance)
1040
+ out = over_named_columns(:lag, names, @key_names | [distance.first], [1], {}) do |col|
1041
+ col[addr, *([nil] * (col.ndim - 1))].copy
1042
+ end
1043
+ return out if out.is_a?(CArray)
1044
+ CAFrame.new(out, axis_name: @frame.axis_name, index: @frame.index)
560
1045
  end
561
1046
 
562
1047
  def order (*names, descending: false, method: :ordinal, kind: :quick)
@@ -573,22 +1058,47 @@ class GroupedFrame
573
1058
  # Fill the masked cells of a column from the rows of the same group, in
574
1059
  # place, as +CAFrame#fill+ does for the whole column: +:ffill+ carries the
575
1060
  # group's last present value forward in row order, +:bfill+ its next one
576
- # back. A fill never reaches across into another group, so a cell with
577
- # nothing to carry in its group stays masked, and a row whose key is
578
- # masked (in no group) is left as it is. A constant fills every masked
579
- # cell, as +CAFrame#fill+ does. Returns self.
1061
+ # back, and +:linear+ interpolates between the group's present values
1062
+ # against the frame's index (against the row position within the group
1063
+ # when the frame has no index). A fill never reaches across into another
1064
+ # group, so a cell with nothing to carry or interpolate from in its group
1065
+ # stays masked, and a row whose key is masked (in no group) is left as it
1066
+ # is. A constant fills every masked cell, as +CAFrame#fill+ does. Returns
1067
+ # self.
580
1068
  #
581
1069
  # df.group_by("station").fill("temp", :ffill)
1070
+ # df.group_by("station").fill("temp", :linear)
1071
+ # df.group_by("station").fill("temp" => :linear, "rain" => 0.0)
1072
+ # df.group_by("station").fill("temp", "rain", :linear)
1073
+ # df.group_by("station").fill(:ffill) # every column with a masked cell
582
1074
  #
583
- # Sort the frame first (+sort_by_key("time")+) when its rows are not in
584
- # time order.
1075
+ # +:ffill+ and +:bfill+ follow the row order: sort the frame first
1076
+ # (+sort_by_key("time")+) when its rows are not in time order. +:linear+
1077
+ # reads each group in the order of the index, as +CAFrame#fill+ does.
585
1078
  #
586
- # @param name [String] the column.
587
- # @param method_or_value [Symbol, Object] +:ffill+, +:bfill+, or a constant.
1079
+ # @param args [Array] the columns, as +CAFrame#fill+ takes them, then
1080
+ # +:ffill+, +:bfill+, +:linear+, or a constant; or one Hash of them.
588
1081
  # @return [GroupedFrame] self
589
- # @raise [ArgumentError] for +:linear+, which within groups is not
590
- # provided, or another Symbol.
591
- def fill (name, method_or_value)
1082
+ # @raise [ArgumentError] for another Symbol, or for +:linear+ on a column
1083
+ # or index +CAFrame#fill+ cannot interpolate.
1084
+ def fill (*args)
1085
+ plan = @frame.__send__(:fill_arguments, args, @key_names)
1086
+ return self if plan.nil?
1087
+ refuse_key_on_rows(:fill, plan.is_a?(Hash) ? plan.keys : [plan.first])
1088
+ if plan.is_a?(Hash)
1089
+ @frame.__send__(:fill_by_map, plan, "fill") do |frame, key, how|
1090
+ grouped = if frame.equal?(@frame)
1091
+ self
1092
+ else
1093
+ # key-labels: the groups' index as it is, on a copy of the frame
1094
+ GroupedFrame.new(frame, @cat, @axis_name, @key_names, index: @index,
1095
+ label_columns: @label_columns)
1096
+ end
1097
+ grouped.fill(key, how)
1098
+ end
1099
+ return self
1100
+ end
1101
+ name, method_or_value = plan
592
1102
  refuse_changed_rows("fill")
593
1103
  case method_or_value
594
1104
  when :ffill, :bfill
@@ -600,10 +1110,15 @@ class GroupedFrame
600
1110
  target = col.is_masked
601
1111
  col[target] = held[target] if target.any
602
1112
  when :linear
603
- raise ArgumentError,
604
- "fill(#{name.inspect}, :linear): an interpolation within each group " \
605
- "is not provided; use :ffill or :bfill, or interpolate one group at a " \
606
- "time with CAFrame#fill"
1113
+ @frame.__send__(:refuse_if_frozen)
1114
+ key = @frame.__send__(:column_key, name, "fill")
1115
+ # The column and the index are checked as CAFrame#fill checks them,
1116
+ # on no rows, so a call without groups is refused as one with groups.
1117
+ @frame.__send__(:select_rows, CA_INT64([])).fill(key, :linear)
1118
+ # Each group's rows as a frame of views, so its fill writes through.
1119
+ ngroup.times do |k|
1120
+ @frame.__send__(:select_rows, group_address(k)).fill(key, :linear)
1121
+ end
607
1122
  when Symbol
608
1123
  raise ArgumentError,
609
1124
  "fill: unknown method #{method_or_value.inspect} " \
@@ -614,32 +1129,113 @@ class GroupedFrame
614
1129
  self
615
1130
  end
616
1131
 
1132
+ # The first +n+ rows of each group, in row order, as a view-frame: the
1133
+ # rows stay in the frame's order, as +filter+ keeps them. A group with
1134
+ # fewer than +n+ rows gives all of them; a row whose group key is masked
1135
+ # is in no group.
1136
+ #
1137
+ # df.group_by("station").head(1) # each station's first row
1138
+ #
1139
+ # @param n [Integer] how many rows per group, 0 or more (default 5).
1140
+ # @return [CAFrame]
1141
+ def head (n = 5)
1142
+ return rows_at_the_ends_of_groups("head", n) { |addr| addr.first(n) }
1143
+ end
1144
+
1145
+ # The last +n+ rows of each group, in row order, as +head+ gives the first.
1146
+ #
1147
+ # @param n [Integer] how many rows per group, 0 or more (default 5).
1148
+ # @return [CAFrame]
1149
+ def tail (n = 5)
1150
+ return rows_at_the_ends_of_groups("tail", n) { |addr| addr.last(n) }
1151
+ end
1152
+
1153
+ private def rows_at_the_ends_of_groups (verb, n)
1154
+ refuse_changed_rows(verb)
1155
+ unless n.is_a?(Integer)
1156
+ raise TypeError, "#{verb}: the count is an Integer (got #{n.inspect})"
1157
+ end
1158
+ raise ArgumentError, "#{verb}: the count must be 0 or more (got #{n})" if n < 0
1159
+ picked = (0...ngroup).flat_map { |k| yield(group_address(k).to_a) }.sort
1160
+ return @frame[CA_INT64(picked)]
1161
+ end
1162
+
1163
+ # The +n+ rows of each group with the largest values of the key columns,
1164
+ # as a view-frame: the groups in the order of +labels+, each group's rows
1165
+ # largest first, as +CAFrame#nlargest+ orders them (several keys break
1166
+ # ties in turn, a NaN or masked key loses to every value, rows that tie on
1167
+ # every key keep their order). A group with fewer than +n+ rows gives all
1168
+ # of them; a row whose group key is masked is in no group.
1169
+ #
1170
+ # df.group_by("station").nlargest(3, "temp") # each station's 3 warmest rows
1171
+ #
1172
+ # @param n [Integer] how many rows per group, 0 or more.
1173
+ # @param keys [Array<String>] the key columns (or the index axis name).
1174
+ # @return [CAFrame]
1175
+ # @raise [ArgumentError] when +n+ is negative or no key is given.
1176
+ def nlargest (n, *keys)
1177
+ return largest_rows_of_groups("nlargest", n, keys, true)
1178
+ end
1179
+
1180
+ # The +n+ rows of each group with the smallest values of the key columns,
1181
+ # as +nlargest+ gives the largest.
1182
+ #
1183
+ # @param n [Integer] how many rows per group, 0 or more.
1184
+ # @param keys [Array<String>] the key columns (or the index axis name).
1185
+ # @return [CAFrame]
1186
+ # @raise [ArgumentError] when +n+ is negative or no key is given.
1187
+ def nsmallest (n, *keys)
1188
+ return largest_rows_of_groups("nsmallest", n, keys, false)
1189
+ end
1190
+
1191
+ private def largest_rows_of_groups (verb, n, keys, descending)
1192
+ refuse_changed_rows(verb)
1193
+ unless n.is_a?(Integer)
1194
+ raise TypeError, "#{verb}: the count is an Integer (got #{n.inspect})"
1195
+ end
1196
+ raise ArgumentError, "#{verb}: the count must be 0 or more (got #{n})" if n < 0
1197
+ raise ArgumentError, "#{verb}: give at least one key column" if keys.empty?
1198
+ cols = keys.map do |key|
1199
+ @frame.__send__(:key_column, @frame.__send__(:column_key, key, verb), verb)
1200
+ end
1201
+ sort_keys = cols.map { |col| descending ? col.order(descending: true, method: :dense) : col }
1202
+ # Rows sorted by group, then by the keys; a row in no group comes last.
1203
+ addr = CArray.sort_addr(group_codes, *sort_keys, masked_position: :last).to_a
1204
+ picked = []
1205
+ start = 0
1206
+ @cat.category_sizes.to_a.each do |size|
1207
+ picked.concat(addr[start, [n, size].min]) if n > 0
1208
+ start += size
1209
+ end
1210
+ return @frame[CA_INT64(picked)]
1211
+ end
1212
+
617
1213
  # Keep the rows of the groups the block chooses. The block receives the
618
1214
  # grouped frame and returns a boolean CArray with one value per group,
619
1215
  # built from the group reductions, as +CAFrame#filter+'s block returns one
620
1216
  # per row:
621
1217
  #
622
1218
  # grp.filter { |g| g["temp"].count_not_masked >= 20 } # stations with 20 readings
623
- # grp.filter { |g| g.mean("temp") > 15.0 }
1219
+ # grp.filter { |g| g["temp"].mean > 15.0 }
624
1220
  #
625
1221
  # The rows of a chosen group are kept in the frame's order, as a sub-frame
626
1222
  # sharing the frame's columns. A group whose value is UNDEF is
627
1223
  # undetermined, as is a row whose key is masked (it belongs to no group):
628
- # such rows are dropped, or with +keep_masked: true+ carried with their
629
- # cells masked, as +CAFrame#filter+ does.
1224
+ # such rows are not kept, as +CAFrame#filter+ does not keep them.
630
1225
  #
631
- # @param keep_masked [Boolean] carry the undetermined rows forward masked.
632
1226
  # @yieldparam grouped [GroupedFrame] self.
633
1227
  # @yieldreturn [CArray] boolean, one value per group.
634
1228
  # @return [CAFrame]
635
1229
  # @raise [ArgumentError] when the block does not return a boolean CArray of
636
1230
  # shape [ngroup].
637
- def filter (keep_masked: false)
1231
+ def filter (**options)
1232
+ CAFrame.__send__(:refuse_filter_keywords, options, "filter")
638
1233
  refuse_changed_rows("filter")
639
1234
  chosen = yield(self)
640
1235
  unless chosen.is_a?(CArray) && chosen.data_type == :boolean
641
1236
  raise ArgumentError,
642
- "filter block must return a boolean CArray (got #{chosen.is_a?(CArray) ? "a #{CArray::Inspector.type_label(chosen)} CArray" : chosen.class})"
1237
+ "filter block must return a boolean CArray (got #{chosen.is_a?(CArray) ? "a #{CArray::Inspector.type_label(chosen)} CArray" : chosen.class})" +
1238
+ CAFrame.__send__(:equality_hint, chosen)
643
1239
  end
644
1240
  unless chosen.shape == [ngroup]
645
1241
  raise ArgumentError,
@@ -647,7 +1243,7 @@ class GroupedFrame
647
1243
  "expected [#{ngroup}] (one value per group)"
648
1244
  end
649
1245
  rows = spread_to_rows(chosen)
650
- @frame.filter(keep_masked: keep_masked) { rows }
1246
+ @frame.filter(rows)
651
1247
  end
652
1248
 
653
1249
  # Returns the grouped rows as a frame: the frame the grouping was made
@@ -656,7 +1252,7 @@ class GroupedFrame
656
1252
  # rows; adding or dropping a column changes the new frame only.
657
1253
  #
658
1254
  # def warm_stations (grp)
659
- # grp.filter { |g| g.mean("temp") > 15.0 } # the rows of some groups
1255
+ # grp.filter { |g| g["temp"].mean > 15.0 } # the rows of some groups
660
1256
  # end
661
1257
  # grp.to_frame # all of its rows
662
1258
  #
@@ -677,11 +1273,12 @@ class GroupedFrame
677
1273
  # would otherwise leave every column out without a word.
678
1274
  private def transform_columns(op, names, args, keywords)
679
1275
  refuse_changed_rows(op)
1276
+ refuse_key_on_rows(op, names)
680
1277
  CArray.float64(1).public_send(op, *args, **keywords)
681
1278
  out = over_named_columns(op, names, @key_names, args, keywords) do |col|
682
1279
  group_transform(col, op, args, keywords)
683
1280
  end
684
- return out unless names.empty?
1281
+ return out if out.is_a?(CArray)
685
1282
  CAFrame.new(out, axis_name: @frame.axis_name, index: @frame.index)
686
1283
  end
687
1284
 
@@ -698,10 +1295,11 @@ class GroupedFrame
698
1295
  CArray.object(@labels.size) { |i| @labels[i] }
699
1296
  end
700
1297
 
701
- # A key column is the index here, not a result column. Named columns
702
- # come back as df[...] returns them, each on the frame's rows with
703
- # +keep_rows+.
704
- private def reduce_columns(reduction, names, keep_rows, **keywords)
1298
+ # A key column is the index here, not a result column. Named columns give
1299
+ # the frame of those columns only, one row per group; with +keep_rows+
1300
+ # they come back as df[...] returns them, each on the frame's rows.
1301
+ private def reduce_columns(reduction, names, keep_rows, args: [], **keywords)
1302
+ refuse_key_as_result(reduction, names) unless keep_rows
705
1303
  keywords = keywords.compact
706
1304
  fills = keywords.delete(:fill_value)
707
1305
  if fills.is_a?(Hash)
@@ -711,14 +1309,42 @@ class GroupedFrame
711
1309
  "#{reduction}: fill_value names no column #{unknown.map(&:inspect).join(', ')}"
712
1310
  end
713
1311
  end
714
- out = over_named_columns(reduction, names, @key_names) do |col, name|
1312
+ named = {}
1313
+ out = over_named_columns(reduction, names, @key_names, args) do |col, name|
715
1314
  fill = fills.is_a?(Hash) ? fills[name] : fills
716
1315
  kw = fill.nil? ? keywords : keywords.merge(fill_value: fill)
717
- col = group_reduce(col, reduction, **kw)
718
- keep_rows && !names.empty? ? spread_to_rows(col) : col
1316
+ col = group_reduce(col, reduction, *args, **kw)
1317
+ named[name] = keep_rows && !names.empty? ? spread_to_rows(col) : col
1318
+ end
1319
+ return result_frame(out, keep_rows) if names.empty?
1320
+ # One named column on the frame's rows comes back as df[name] returns it,
1321
+ # several as a frame on those rows; one value per group needs the group
1322
+ # labels beside it.
1323
+ if keep_rows
1324
+ return out if out.is_a?(CArray)
1325
+ return CAFrame.new(out, axis_name: @frame.axis_name, index: @frame.index)
719
1326
  end
720
- return out unless names.empty?
721
- result_frame(out, keep_rows)
1327
+ result_frame(named, false)
1328
+ end
1329
+
1330
+ # A key column named in a reduction would be a result column beside the
1331
+ # key it is the group of: the index, or a key column of the result.
1332
+ private def refuse_key_as_result(reduction, names)
1333
+ named = names.map { |n| n.is_a?(String) ? n : nil }.compact & @key_names
1334
+ return if named.empty?
1335
+ hint = reduction == :count ? "; the number of rows of each group is size" : ""
1336
+ raise ArgumentError,
1337
+ "#{reduction}: #{named.first.inspect} is the group key, already in the result#{hint}"
1338
+ end
1339
+
1340
+ # A key column named in a verb computed within each group: the key holds
1341
+ # one value in a group, so there is nothing to compute along it, and a
1342
+ # fill would write to what the groups were made from.
1343
+ private def refuse_key_on_rows(verb, names)
1344
+ named = names.map { |n| n.is_a?(String) ? n : nil }.compact & @key_names
1345
+ return if named.empty?
1346
+ raise ArgumentError,
1347
+ "#{verb}: #{named.first.inspect} is the group key, one value within each group"
722
1348
  end
723
1349
 
724
1350
  # One row per group, indexed by the labels; or, with +keep_rows+, the