carray 3.0.4 → 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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +438 -0
  3. data/README.md +1 -1
  4. data/carray.gemspec +3 -2
  5. data/ext/ca_array_pool.c +25 -1
  6. data/ext/ca_axis_dispatch.c +19 -20
  7. data/ext/ca_axis_group.c +1 -1
  8. data/ext/ca_categorical_iterator.c +34 -27
  9. data/ext/ca_compare.c +4 -4
  10. data/ext/ca_composite_dispatch.c +9 -9
  11. data/ext/ca_kernel_iterator.c +122 -237
  12. data/ext/ca_kernel_iterator.h +522 -337
  13. data/ext/ca_obj_array.c +69 -19
  14. data/ext/ca_obj_bincmp.c +10 -1
  15. data/ext/ca_obj_binop.c +11 -1
  16. data/ext/ca_obj_bitarray.c +5 -1
  17. data/ext/ca_obj_bitfield.c +60 -43
  18. data/ext/ca_obj_byte_swap.c +4 -1
  19. data/ext/ca_obj_const_string.c +15 -3
  20. data/ext/ca_obj_face.c +44 -1
  21. data/ext/ca_obj_fake.c +3 -1
  22. data/ext/ca_obj_field.c +13 -3
  23. data/ext/ca_obj_grid.c +219 -17
  24. data/ext/ca_obj_meld.c +69 -113
  25. data/ext/ca_obj_moncmp.c +9 -1
  26. data/ext/ca_obj_monop.c +86 -12
  27. data/ext/ca_obj_object.c +118 -32
  28. data/ext/ca_obj_record.c +3 -0
  29. data/ext/ca_obj_reduce.c +19 -1
  30. data/ext/ca_obj_refer.c +80 -26
  31. data/ext/ca_obj_remap.c +69 -10
  32. data/ext/ca_obj_roll.c +27 -6
  33. data/ext/ca_obj_select.c +38 -6
  34. data/ext/ca_obj_select_axis.c +1 -0
  35. data/ext/ca_obj_shift.c +32 -7
  36. data/ext/ca_obj_stack.c +311 -111
  37. data/ext/ca_obj_stride.c +159 -64
  38. data/ext/ca_obj_string.c +27 -2
  39. data/ext/ca_obj_tile.c +28 -7
  40. data/ext/ca_obj_transpose.c +8 -4
  41. data/ext/ca_obj_triop.c +12 -1
  42. data/ext/ca_obj_window.c +80 -50
  43. data/ext/ca_op_ipower.c +12 -12
  44. data/ext/ca_sort_kernels.h +6 -3
  45. data/ext/caframe_csv_reader.c +1 -1
  46. data/ext/carray.h +35 -1
  47. data/ext/carray_access.c +43 -67
  48. data/ext/carray_address_basis.c +134 -0
  49. data/ext/carray_attribute.c +32 -0
  50. data/ext/carray_broadcast.c +3 -4
  51. data/ext/carray_build_flags.h +1 -1
  52. data/ext/carray_cast.c +251 -93
  53. data/ext/carray_cast_func.rb +56 -26
  54. data/ext/carray_conversion.c +11 -7
  55. data/ext/carray_core.c +814 -72
  56. data/ext/carray_count.c +6 -0
  57. data/ext/carray_element.c +22 -13
  58. data/ext/carray_factorize.c +20 -20
  59. data/ext/carray_generate.c +8 -2
  60. data/ext/carray_histogram.c +26 -4
  61. data/ext/carray_index_classifier.c +4 -4
  62. data/ext/carray_internal.h +159 -12
  63. data/ext/carray_kernels_bincmp.c +2174 -170
  64. data/ext/carray_kernels_binop.c +25653 -5487
  65. data/ext/carray_kernels_init.c +8 -8
  66. data/ext/carray_kernels_moncmp.c +96 -96
  67. data/ext/carray_kernels_monop.c +12253 -5235
  68. data/ext/carray_kernels_reduce_aggregate.c +6150 -2423
  69. data/ext/carray_kernels_reduce_boolean.c +1533 -15
  70. data/ext/carray_kernels_reduce_cumulative.c +2442 -882
  71. data/ext/carray_kernels_reduce_extreme.c +5658 -1661
  72. data/ext/carray_kernels_reduce_variance.c +2534 -402
  73. data/ext/carray_kernels_scan.c +368 -368
  74. data/ext/carray_kernels_search.c +4551 -604
  75. data/ext/carray_kernels_sort.c +659 -751
  76. data/ext/carray_kernels_triop.c +114 -114
  77. data/ext/carray_key_windows.c +4 -0
  78. data/ext/carray_lazy.c +47 -0
  79. data/ext/carray_loop.c +1 -1
  80. data/ext/carray_mask.c +175 -161
  81. data/ext/carray_median_percentile.c +4 -4
  82. data/ext/carray_memory_view.c +15 -9
  83. data/ext/carray_operator.c +152 -49
  84. data/ext/carray_order.c +230 -10
  85. data/ext/carray_random.c +22 -19
  86. data/ext/carray_scatter.c +24 -7
  87. data/ext/carray_slab.c +128 -29
  88. data/ext/carray_sort.c +224 -16
  89. data/ext/carray_sort_kernel.c +5 -7
  90. data/ext/carray_struct.c +35 -43
  91. data/ext/carray_test.c +74 -19
  92. data/ext/carray_time_text.c +6 -4
  93. data/ext/carray_utils.c +63 -21
  94. data/ext/extconf.rb +11 -0
  95. data/ext/mkkernel.rb +546 -199
  96. data/ext/ruby_carray.c +4 -1
  97. data/ext/version.h +4 -4
  98. data/lib/carray/attribute.rb +42 -5
  99. data/lib/carray/autoload_carray.rb +3 -3
  100. data/lib/carray/autoload_method_extension.rb +5 -3
  101. data/lib/carray/axis_group.rb +13 -7
  102. data/lib/carray/basics.rb +28 -8
  103. data/lib/carray/block_iterator.rb +17 -3
  104. data/lib/carray/categorical.rb +35 -23
  105. data/lib/carray/categorical_iterator.rb +8 -8
  106. data/lib/carray/complex.rb +2 -2
  107. data/lib/carray/conditional.rb +3 -2
  108. data/lib/carray/const_string.rb +10 -0
  109. data/lib/carray/data_type_extension.rb +8 -1
  110. data/lib/carray/fixlen_string.rb +41 -2
  111. data/lib/carray/frame/column_verbs.rb +48 -16
  112. data/lib/carray/frame/concat.rb +17 -3
  113. data/lib/carray/frame/convert.rb +19 -0
  114. data/lib/carray/frame/csv_parser.rb +32 -32
  115. data/lib/carray/frame/describe.rb +4 -2
  116. data/lib/carray/frame/frame.rb +405 -85
  117. data/lib/carray/frame/group.rb +777 -64
  118. data/lib/carray/frame/io.rb +212 -72
  119. data/lib/carray/frame/join.rb +146 -37
  120. data/lib/carray/frame/records.rb +3 -3
  121. data/lib/carray/frame/reshape.rb +89 -11
  122. data/lib/carray/frame/sort.rb +57 -9
  123. data/lib/carray/frame/verbs.rb +384 -49
  124. data/lib/carray/frame/windows.rb +44 -6
  125. data/lib/carray/fuse_source.rb +31 -2
  126. data/lib/carray/inspect.rb +7 -2
  127. data/lib/carray/iterator.rb +32 -0
  128. data/lib/carray/lazy.rb +52 -8
  129. data/lib/carray/mask_gap_fill.rb +37 -17
  130. data/lib/carray/meld_reduce.rb +1 -1
  131. data/lib/carray/methods/choose.rb +3 -2
  132. data/lib/carray/methods/is_in.rb +8 -1
  133. data/lib/carray/methods/meshgrid.rb +3 -1
  134. data/lib/carray/runtime.rb +47 -8
  135. data/lib/carray/segment_iterator.rb +35 -16
  136. data/lib/carray/serialize.rb +53 -19
  137. data/lib/carray/stack.rb +33 -18
  138. data/lib/carray/string.rb +8 -0
  139. data/lib/carray/string_operation_extension.rb +13 -5
  140. data/lib/carray/struct_builder.rb +5 -3
  141. data/lib/carray/time.rb +112 -68
  142. data/lib/carray/window_iterator.rb +16 -4
  143. data/yard-stubs/ca_obj_array.rb +26 -1
  144. data/yard-stubs/ca_obj_transpose.rb +5 -4
  145. data/yard-stubs/carray_attribute.rb +3 -1
  146. data/yard-stubs/carray_cast.rb +10 -5
  147. data/yard-stubs/carray_conversion.rb +13 -3
  148. data/yard-stubs/carray_element.rb +7 -6
  149. data/yard-stubs/carray_mask.rb +28 -4
  150. data/yard-stubs/carray_math.rb +45 -20
  151. data/yard-stubs/carray_memory_view.rb +7 -0
  152. data/yard-stubs/carray_order.rb +100 -4
  153. data/yard-stubs/carray_scatter.rb +7 -5
  154. data/yard-stubs/carray_test.rb +7 -4
  155. 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
  #
@@ -158,7 +208,7 @@ class CAFrame
158
208
  g_of = nb > 0 ? occupied / nb : occupied
159
209
  b_of = nb > 0 ? occupied % nb : occupied
160
210
  # Each group's by values, from its first row.
161
- order = gcat.sort_addr
211
+ order = gcat.group_addr
162
212
  first = order[CArray.segment_offsets(lengths: gcat.category_sizes)[0...ng]]
163
213
  columns = {}
164
214
  by_names.zip(by_cols).each do |n, col|
@@ -234,15 +284,358 @@ 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).
240
632
  #
241
633
  # Holds the grouping categorical once (grouping is per-key, computed once
242
- # and shared across every column). Three surfaces:
243
- # grp["col"] -> the CArray group iterator (raw exposure)
244
- # aggregate -> declarative per-column reductions into a new frame
245
- # table { |g| }-> cross-column Ruby escape, g is a per-group view-frame
634
+ # and shared across every column). The surfaces:
635
+ # grp["col"], grp[array] -> the CArray group iterator of a column or array
636
+ # aggregate(spec) -> declarative per-column reductions into a new frame
637
+ # aggregate { |grp| } -> columns built from iterators, run once
638
+ # table { |g| } -> Ruby escape, g is a per-group view-frame
246
639
  class GroupedFrame
247
640
  include CAFrame::ColumnVerbs
248
641
  # +key_names+ are the frame columns the grouping was keyed on. They become
@@ -280,15 +673,79 @@ class GroupedFrame
280
673
  @labels.size
281
674
  end
282
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
+
283
710
  # Group key values (one per group), as an Array.
284
711
  def labels
285
712
  @labels.dup
286
713
  end
287
714
 
288
- # Raw exposure: the CArray group iterator for one column (memo §11.6).
289
- def [](name)
715
+ # The CArray group iterator of a column, or of any CArray with one entry
716
+ # per row of the frame: a value computed from columns, or a column with
717
+ # some cells masked to leave them out. Its reductions answer one value
718
+ # per group, and take the arguments a CArray's reductions take.
719
+ #
720
+ # grp["temp"].percentile(90)
721
+ # grp[df["temp"] * df["w"]].sum / grp["w"].sum
722
+ # grp[df["temp"].mask_where(df["qc"].ne(0))].mean # without the flagged rows
723
+ #
724
+ # The array is matched to the rows by position, as a CArray key of
725
+ # +group_by+ is. An N-D one is grouped along its first axis; pass
726
+ # +axis: 0+ to its reductions to keep the trailing shape.
727
+ #
728
+ # @param source [String, CArray] a column name, or a CArray whose first
729
+ # axis has one entry per row.
730
+ # @return [CACategoricalIterator]
731
+ # @raise [KeyError] for a name that is no column.
732
+ # @raise [ArgumentError] when the CArray's first axis is not the rows.
733
+ def [](source)
290
734
  refuse_changed_rows("[]")
291
- @frame[name].group_by_category(@cat)
735
+ values =
736
+ case source
737
+ when CArray
738
+ rows = source.ndim == 0 ? 0 : source.shape[0]
739
+ unless source.ndim >= 1 && rows == @cat.elements
740
+ raise ArgumentError,
741
+ "[]: the array has #{source.ndim == 0 ? "no axis" : "#{rows} rows"} " \
742
+ "for the frame's #{@cat.elements}"
743
+ end
744
+ source
745
+ else
746
+ @frame[@frame.__send__(:column_key, source, "[]")]
747
+ end
748
+ values.group_by_category(@cat)
292
749
  end
293
750
 
294
751
  # Declarative aggregation (memo §11.6). Spec maps an output column name to
@@ -304,12 +761,44 @@ class GroupedFrame
304
761
  # keyword works for +table+ and the reductions below.
305
762
  #
306
763
  # df.group_by("station").aggregate("temp_mean" => ["temp", :mean], keep_rows: true)
307
- def aggregate(spec = nil, keep_rows: false, **named)
764
+ #
765
+ # Given a block instead of a spec, aggregate yields the grouped frame once
766
+ # and the block returns a Hash of output name => a CArray with one entry
767
+ # per group. The block computes the columns with the grouped frame's
768
+ # reductions, +grp[...]+ iterators and CArray arithmetic, so nothing runs
769
+ # per group in Ruby:
770
+ #
771
+ # df.group_by("station").aggregate do |g|
772
+ # {
773
+ # "temp" => g["temp"].mean,
774
+ # "range" => g["temp"].max - g["temp"].min,
775
+ # "wmean" => g[df["temp"] * df["w"]].sum / g["w"].sum,
776
+ # "n" => g["temp"].group_sizes,
777
+ # }
778
+ # end
779
+ #
780
+ # +keep_rows: true+ lays each column over the rows as for a spec.
781
+ #
782
+ # @raise [ArgumentError] when both a spec and a block are given, or the
783
+ # block returns something other than a Hash of per-group CArrays.
784
+ def aggregate(spec = nil, keep_rows: false, **named, &block)
308
785
  refuse_changed_rows("aggregate")
786
+ if block
787
+ unless spec.nil? && named.empty?
788
+ raise ArgumentError, "aggregate: give a spec or a block, not both"
789
+ end
790
+ return result_frame(block_columns(yield(self)), keep_rows)
791
+ end
309
792
  spec = (spec || {}).merge(named)
310
793
  cols = {}
311
794
  spec.each do |out_name, (in_name, reduction, *args)|
312
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
313
802
  @frame.__send__(:column_key, in_name, "aggregate")
314
803
  options = args.last.is_a?(Hash) ? args.pop : {}
315
804
  cols[out] =
@@ -364,23 +853,29 @@ class GroupedFrame
364
853
  # its trailing shape: a (N, L) column gives (groups, L). Each
365
854
  # takes +min_count:+ and +fill_value:+ as a core reduction does: a group
366
855
  # with fewer than min_count present values is UNDEF, and fill_value fills
367
- # the UNDEF cells. +keep_rows: true+ gives each row its group's value
856
+ # the UNDEF cells. One fill_value goes to every column and has to suit each
857
+ # (a number into a string column raises); a Hash of column name => value
858
+ # fills those columns only. +keep_rows: true+ gives each row its group's value
368
859
  # instead of one row per group, as for +aggregate+.
369
860
  #
370
- # Given column names, they reduce those columns only and return them as
371
- # +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+.
372
866
  #
373
867
  # grp.mean # a frame, one row per group
374
- # grp.mean("temp") # CArray, one value per group
868
+ # grp.mean("temp") # a frame of temp, one row per group
375
869
  # df["temp"] - grp.mean("temp", keep_rows: true) # anomaly from the group mean
376
870
  #
377
871
  # @!method sum(*names, min_count: nil, fill_value: nil, keep_rows: false)
378
872
  # Returns a frame of the per-group sum of every column whose type has a
379
873
  # sum, an N-D one per group along the rows. Other columns are left out.
380
- # @return [CAFrame, CArray, Array<CArray>] a frame with one row per
381
- # group, indexed by the group labels, or with keep_rows: true the
382
- # frame's rows and index; given names, the CArray (one) or the Array of
383
- # 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).
384
879
  # @!method mean(*names, min_count: nil, fill_value: nil, keep_rows: false)
385
880
  # Returns a frame of the per-group mean of every column whose type has
386
881
  # a mean, as {#sum} does.
@@ -424,6 +919,43 @@ class GroupedFrame
424
919
  reduce_columns(red, names, keep_rows, min_count: min_count, fill_value: fill_value)
425
920
  end
426
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
+
427
959
  # The same convenience for the reductions that take no min_count: or
428
960
  # fill_value:, as the core ones take none: a value of the group (first,
429
961
  # last), a count (nunique, count) or a boolean fold (all, any).
@@ -463,8 +995,8 @@ class GroupedFrame
463
995
  # group's rows, a running statistic. A row in no group is UNDEF. Without
464
996
  # names they give a frame of every column whose type has the operation
465
997
  # (the key columns left out), on the frame's rows and index; given names,
466
- # the CArray (one) or the Array of them (several), as +df[...]+ returns
467
- # 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.
468
1000
  #
469
1001
  # df["dtemp"] = df["temp"] - grp.lag("temp") # change since the station's previous row
470
1002
  # grp.order("temp", descending: true) # rank within the station
@@ -473,31 +1005,43 @@ class GroupedFrame
473
1005
  # "Previous" is the previous row of the frame: sort the frame first
474
1006
  # (+sort_by_key("time")+) when its rows are not in time order.
475
1007
  #
476
- # @!method lag(*names, n: 1)
1008
+ # @!method lag(*names, n: 1, before: nil, after: nil, on: nil)
477
1009
  # Returns each row's value +n+ rows back in its own group, UNDEF where
478
- # the group has none, as +CArray#lag+; a negative +n+ looks ahead.
479
- # @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]
480
1015
  # @!method order(*names, descending: false, method: :ordinal, kind: :quick)
481
1016
  # Returns each row's rank among its group's rows (int64, 0 the
482
1017
  # smallest), as +CArray#order+ ranks them.
483
- # @return [CAFrame, CArray, Array<CArray>]
1018
+ # @return [CAFrame, CArray]
484
1019
  # @!method cumsum(*names)
485
1020
  # Returns the running sum within each group, as +CArray#cumsum+.
486
- # @return [CAFrame, CArray, Array<CArray>]
1021
+ # @return [CAFrame, CArray]
487
1022
  # @!method cumprod(*names)
488
1023
  # Returns the running product within each group.
489
- # @return [CAFrame, CArray, Array<CArray>]
1024
+ # @return [CAFrame, CArray]
490
1025
  # @!method cummax(*names)
491
1026
  # Returns the running maximum within each group.
492
- # @return [CAFrame, CArray, Array<CArray>]
1027
+ # @return [CAFrame, CArray]
493
1028
  # @!method cummin(*names)
494
1029
  # Returns the running minimum within each group.
495
- # @return [CAFrame, CArray, Array<CArray>]
1030
+ # @return [CAFrame, CArray]
496
1031
  # @!method cumcount(*names)
497
1032
  # Returns the running count of present values within each group (int64).
498
- # @return [CAFrame, CArray, Array<CArray>]
499
- def lag (*names, n: 1)
500
- 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)
501
1045
  end
502
1046
 
503
1047
  def order (*names, descending: false, method: :ordinal, kind: :quick)
@@ -514,22 +1058,47 @@ class GroupedFrame
514
1058
  # Fill the masked cells of a column from the rows of the same group, in
515
1059
  # place, as +CAFrame#fill+ does for the whole column: +:ffill+ carries the
516
1060
  # group's last present value forward in row order, +:bfill+ its next one
517
- # back. A fill never reaches across into another group, so a cell with
518
- # nothing to carry in its group stays masked, and a row whose key is
519
- # masked (in no group) is left as it is. A constant fills every masked
520
- # 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.
521
1068
  #
522
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
523
1074
  #
524
- # Sort the frame first (+sort_by_key("time")+) when its rows are not in
525
- # 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.
526
1078
  #
527
- # @param name [String] the column.
528
- # @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.
529
1081
  # @return [GroupedFrame] self
530
- # @raise [ArgumentError] for +:linear+, which within groups is not
531
- # provided, or another Symbol.
532
- 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
533
1102
  refuse_changed_rows("fill")
534
1103
  case method_or_value
535
1104
  when :ffill, :bfill
@@ -541,10 +1110,15 @@ class GroupedFrame
541
1110
  target = col.is_masked
542
1111
  col[target] = held[target] if target.any
543
1112
  when :linear
544
- raise ArgumentError,
545
- "fill(#{name.inspect}, :linear): an interpolation within each group " \
546
- "is not provided; use :ffill or :bfill, or interpolate one group at a " \
547
- "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
548
1122
  when Symbol
549
1123
  raise ArgumentError,
550
1124
  "fill: unknown method #{method_or_value.inspect} " \
@@ -555,32 +1129,113 @@ class GroupedFrame
555
1129
  self
556
1130
  end
557
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
+
558
1213
  # Keep the rows of the groups the block chooses. The block receives the
559
1214
  # grouped frame and returns a boolean CArray with one value per group,
560
1215
  # built from the group reductions, as +CAFrame#filter+'s block returns one
561
1216
  # per row:
562
1217
  #
563
1218
  # grp.filter { |g| g["temp"].count_not_masked >= 20 } # stations with 20 readings
564
- # grp.filter { |g| g.mean("temp") > 15.0 }
1219
+ # grp.filter { |g| g["temp"].mean > 15.0 }
565
1220
  #
566
1221
  # The rows of a chosen group are kept in the frame's order, as a sub-frame
567
1222
  # sharing the frame's columns. A group whose value is UNDEF is
568
1223
  # undetermined, as is a row whose key is masked (it belongs to no group):
569
- # such rows are dropped, or with +keep_masked: true+ carried with their
570
- # cells masked, as +CAFrame#filter+ does.
1224
+ # such rows are not kept, as +CAFrame#filter+ does not keep them.
571
1225
  #
572
- # @param keep_masked [Boolean] carry the undetermined rows forward masked.
573
1226
  # @yieldparam grouped [GroupedFrame] self.
574
1227
  # @yieldreturn [CArray] boolean, one value per group.
575
1228
  # @return [CAFrame]
576
1229
  # @raise [ArgumentError] when the block does not return a boolean CArray of
577
1230
  # shape [ngroup].
578
- def filter (keep_masked: false)
1231
+ def filter (**options)
1232
+ CAFrame.__send__(:refuse_filter_keywords, options, "filter")
579
1233
  refuse_changed_rows("filter")
580
1234
  chosen = yield(self)
581
1235
  unless chosen.is_a?(CArray) && chosen.data_type == :boolean
582
1236
  raise ArgumentError,
583
- "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)
584
1239
  end
585
1240
  unless chosen.shape == [ngroup]
586
1241
  raise ArgumentError,
@@ -588,7 +1243,7 @@ class GroupedFrame
588
1243
  "expected [#{ngroup}] (one value per group)"
589
1244
  end
590
1245
  rows = spread_to_rows(chosen)
591
- @frame.filter(keep_masked: keep_masked) { rows }
1246
+ @frame.filter(rows)
592
1247
  end
593
1248
 
594
1249
  # Returns the grouped rows as a frame: the frame the grouping was made
@@ -597,7 +1252,7 @@ class GroupedFrame
597
1252
  # rows; adding or dropping a column changes the new frame only.
598
1253
  #
599
1254
  # def warm_stations (grp)
600
- # 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
601
1256
  # end
602
1257
  # grp.to_frame # all of its rows
603
1258
  #
@@ -618,11 +1273,12 @@ class GroupedFrame
618
1273
  # would otherwise leave every column out without a word.
619
1274
  private def transform_columns(op, names, args, keywords)
620
1275
  refuse_changed_rows(op)
1276
+ refuse_key_on_rows(op, names)
621
1277
  CArray.float64(1).public_send(op, *args, **keywords)
622
1278
  out = over_named_columns(op, names, @key_names, args, keywords) do |col|
623
1279
  group_transform(col, op, args, keywords)
624
1280
  end
625
- return out unless names.empty?
1281
+ return out if out.is_a?(CArray)
626
1282
  CAFrame.new(out, axis_name: @frame.axis_name, index: @frame.index)
627
1283
  end
628
1284
 
@@ -639,17 +1295,56 @@ class GroupedFrame
639
1295
  CArray.object(@labels.size) { |i| @labels[i] }
640
1296
  end
641
1297
 
642
- # A key column is the index here, not a result column. Named columns
643
- # come back as df[...] returns them, each on the frame's rows with
644
- # +keep_rows+.
645
- 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
646
1303
  keywords = keywords.compact
647
- out = over_named_columns(reduction, names, @key_names) do |col|
648
- col = group_reduce(col, reduction, **keywords)
649
- keep_rows && !names.empty? ? spread_to_rows(col) : col
1304
+ fills = keywords.delete(:fill_value)
1305
+ if fills.is_a?(Hash)
1306
+ unknown = fills.keys - @frame.column_names
1307
+ unless unknown.empty?
1308
+ raise ArgumentError,
1309
+ "#{reduction}: fill_value names no column #{unknown.map(&:inspect).join(', ')}"
1310
+ end
1311
+ end
1312
+ named = {}
1313
+ out = over_named_columns(reduction, names, @key_names, args) do |col, name|
1314
+ fill = fills.is_a?(Hash) ? fills[name] : fills
1315
+ kw = fill.nil? ? keywords : keywords.merge(fill_value: fill)
1316
+ col = group_reduce(col, reduction, *args, **kw)
1317
+ named[name] = keep_rows && !names.empty? ? spread_to_rows(col) : col
650
1318
  end
651
- return out unless names.empty?
652
- result_frame(out, keep_rows)
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)
1326
+ end
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"
653
1348
  end
654
1349
 
655
1350
  # One row per group, indexed by the labels; or, with +keep_rows+, the
@@ -690,6 +1385,24 @@ class GroupedFrame
690
1385
  end
691
1386
  end
692
1387
 
1388
+ # The columns an aggregate block returned: a Hash of names to CArrays
1389
+ # with one entry per group along their first axis.
1390
+ private def block_columns(out)
1391
+ unless out.is_a?(Hash)
1392
+ raise ArgumentError, "aggregate block must return a Hash (got #{out.class})"
1393
+ end
1394
+ out.each_with_object({}) do |(name, col), cols|
1395
+ name = @frame.__send__(:column_name_arg, name, "aggregate")
1396
+ unless col.is_a?(CArray) && col.ndim >= 1 && col.shape[0] == ngroup
1397
+ got = col.is_a?(CArray) ? "shape #{col.shape.inspect}" : col.class.to_s
1398
+ raise ArgumentError,
1399
+ "aggregate: #{name.inspect} must be a CArray with one value per group " \
1400
+ "(#{ngroup}), got #{got}"
1401
+ end
1402
+ cols[name] = col
1403
+ end
1404
+ end
1405
+
693
1406
  private def per_group_column(in_name, proc)
694
1407
  results = []
695
1408
  each_group_slice(in_name) { |slice| results << proc.call(slice) }
@@ -751,7 +1464,7 @@ class GroupedFrame
751
1464
  end
752
1465
 
753
1466
  private def group_perm
754
- @group_perm ||= @cat.sort_addr
1467
+ @group_perm ||= @cat.group_addr
755
1468
  end
756
1469
 
757
1470
  private def group_bounds