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
@@ -29,9 +29,27 @@ class CAFrame
29
29
  # right row.
30
30
  #
31
31
  # Rows come in left-row order (right-row order for :right), the pairs of
32
- # one row in the other side's order. The left columns are views of this
33
- # frame's, picked by row, wherever every row comes from the left (:left,
34
- # :inner); the right columns are gathered into new arrays.
32
+ # one row in the other side's order. A side whose row every result row
33
+ # has gives views of its columns, picked by row: this frame's for :left
34
+ # and :inner, +other+'s for :inner and :right (for :right, the key column
35
+ # too). A
36
+ # write to the result then reaches that frame, and reading the result
37
+ # gathers through the views each time; call +copy+ on the result for
38
+ # columns of its own, when it will be written or read many times. The
39
+ # side that gets UNDEF for an unmatched row is gathered into new arrays.
40
+ #
41
+ # The views are for cleaning data through the join, on frames read in as
42
+ # working copies just before an analysis: a condition built from both
43
+ # frames' columns masks or corrects the cells it marks, and the write
44
+ # lands in the frame they came from. +view: :full+ (experimental)
45
+ # makes the other side views too, so a write reaches either frame, with a
46
+ # hole where a row has no match: the hole reads UNDEF and a write to it is
47
+ # dropped. Every result column then reaches its frame, at a cost on each
48
+ # read (a column with holes is gathered whole: a reduction is about
49
+ # three times slower than on a column of its own, a +copy+ five to ten). The key of an outer join welds the two key
50
+ # columns (+CArray.meld+), so it needs keys of one kind on both sides, the
51
+ # same type and, for a Face, the same state (time unit, labels); a key of
52
+ # +CAConstString+ cannot be welded.
35
53
  #
36
54
  # A non-key column present on both sides collides. The key column (+on+) is
37
55
  # kept once, never suffixed. By default the collision is resolved by suffixing
@@ -41,8 +59,12 @@ class CAFrame
41
59
  #
42
60
  # +on:+ may name several columns, as an Array: rows then match when every
43
61
  # key column matches, and a row with any key cell masked matches nothing.
62
+ # When the key has different names on the two sides, give a Hash of this
63
+ # frame's name to +other+'s; the key column keeps this frame's name, and
64
+ # +other+'s key column is not added.
44
65
  #
45
66
  # obs.join(fcst, on: ["station", "date"])
67
+ # obs.join(stations, on: { "station" => "code" })
46
68
  #
47
69
  # Two modes filter this frame's rows by +other+ instead of pairing them:
48
70
  #
@@ -55,21 +77,25 @@ class CAFrame
55
77
  # The result shares this frame's columns and index, as +filter+'s does.
56
78
  #
57
79
  # obs.join(stations, on: "station", how: :anti) # stations not listed
58
- def join(other, on:, how: :left, suffixes: DEFAULT_JOIN_SUFFIXES)
80
+ def join(other, on:, how: :left, suffixes: DEFAULT_JOIN_SUFFIXES, view: nil)
59
81
  unless %i[left inner outer right semi anti].include?(how)
60
82
  raise ArgumentError, "unknown join mode #{how.inspect}"
61
83
  end
84
+ refuse_unknown_view(view, "join")
85
+ if (how == :semi || how == :anti) && view
86
+ raise ArgumentError, "join: how: #{how.inspect} adds no column of the other frame, so view: does not apply"
87
+ end
62
88
  if (how == :semi || how == :anti) && !suffixes.equal?(DEFAULT_JOIN_SUFFIXES)
63
89
  raise ArgumentError, "join: how: #{how.inspect} adds no column of the other frame, so suffixes: does not apply"
64
90
  end
65
- names = join_key_names(on, "join", "on:")
91
+ names, rnames = join_key_pairs(on, "join", "on:")
66
92
  lkeys = names.map { |name| key_column(name, "join") }
67
- rkeys = names.map { |name| other.__send__(:key_column, name, "join") }
93
+ rkeys = rnames.map { |name| other.__send__(:key_column, name, "join") }
68
94
  join_check_key_widths(names, lkeys, rkeys, "join")
69
95
  return join_filter(lkeys, rkeys, how) if how == :semi || how == :anti
70
96
 
71
- plan = join_name_plan(other, names, suffixes)
72
- join_pairs(other, names, lkeys, rkeys, how, plan)
97
+ plan = join_name_plan(other, names, rnames, suffixes)
98
+ join_pairs(other, names, rnames, lkeys, rkeys, how, plan, view)
73
99
  end
74
100
 
75
101
  # As-of (nearest-key) left join for irregular series (memo §11.7). Each
@@ -80,22 +106,31 @@ class CAFrame
80
106
  # in that direction or beyond +tolerance:+ come back UNDEF. +tolerance:+
81
107
  # is a number for a numeric key and a duration for a time key
82
108
  # ("10 minutes", or a CATimedelta value). Same wiring as the left join,
83
- # only the address primitive differs.
109
+ # only the address primitive differs; +view: :full+ makes +other+'s
110
+ # columns views with holes, as +join+'s does.
84
111
  #
85
112
  # +by:+ names columns that must match exactly, one name or an Array: each
86
113
  # left row is matched to the nearest row among the rows of +other+ with
87
114
  # the same +by+ values. A row with a masked +by+ cell matches nothing.
115
+ # +on:+ and +by:+ take a Hash of this frame's name to +other+'s for a key
116
+ # named differently on the two sides, as +join+'s +on:+ does.
88
117
  #
89
118
  # obs.join_asof(radar, on: "time", by: "station", tolerance: "10 minutes")
90
- def join_asof(other, on:, by: nil, direction: :floor, tolerance: nil, suffixes: DEFAULT_JOIN_SUFFIXES)
91
- on = column_key(on, "join_asof")
92
- exact = by.nil? ? [] : join_key_names(by, "join_asof", "by:")
93
- if exact.include?(on)
94
- raise ArgumentError, "join_asof: #{on.inspect} is the on: key; by: names the others"
119
+ def join_asof(other, on:, by: nil, direction: :floor, tolerance: nil, suffixes: DEFAULT_JOIN_SUFFIXES, view: nil)
120
+ refuse_unknown_view(view, "join_asof")
121
+ ons, ron = join_key_pairs(on, "join_asof", "on:")
122
+ if ons.size != 1
123
+ raise ArgumentError, "join_asof: on: names one key column (got #{ons.size}); by: names the others"
124
+ end
125
+ on, ron = ons.first, ron.first
126
+ exact, rexact_names = by.nil? ? [[], []] : join_key_pairs(by, "join_asof", "by:")
127
+ if exact.include?(on) || rexact_names.include?(ron)
128
+ name = exact.include?(on) ? on : ron
129
+ raise ArgumentError, "join_asof: #{name.inspect} is the on: key; by: names the others"
95
130
  end
96
- plan = join_name_plan(other, [on, *exact], suffixes)
131
+ plan = join_name_plan(other, [on, *exact], [ron, *rexact_names], suffixes)
97
132
  lkey = key_column(on, "join_asof")
98
- rkey = other.__send__(:key_column, on, "join_asof")
133
+ rkey = other.__send__(:key_column, ron, "join_asof")
99
134
  [lkey, rkey].each do |key|
100
135
  key.__send__(:nearest_refuse_bytes, key, "join_asof: the on: key #{on.inspect}",
101
136
  "join on it exactly with join")
@@ -104,12 +139,12 @@ class CAFrame
104
139
  lkey.locate_nearest_addr(rkey, direction: direction, tolerance: tolerance)
105
140
  else
106
141
  lexact = exact.map { |name| key_column(name, "join_asof") }
107
- rexact = exact.map { |name| other.__send__(:key_column, name, "join_asof") }
142
+ rexact = rexact_names.map { |name| other.__send__(:key_column, name, "join_asof") }
108
143
  join_check_key_widths(exact, lexact, rexact, "join_asof")
109
144
  _, lcode, rcode, k = join_codes(lexact, rexact)
110
145
  join_asof_by(lkey, rkey, lcode, rcode, k, direction, tolerance)
111
146
  end
112
- join_by_addr(other, [on, *exact], addr, plan)
147
+ join_by_addr(other, [on, *exact], [ron, *rexact_names], addr, plan, view)
113
148
  end
114
149
 
115
150
  # Conform every column to an externally supplied reference key set (the
@@ -259,12 +294,21 @@ class CAFrame
259
294
  end
260
295
  end
261
296
 
262
- # The key column names of a join: one name, or an Array of names.
263
- private def join_key_names(on, verb, option)
264
- names = on.is_a?(Array) ? on : [on]
265
- raise ArgumentError, "#{verb}: #{option} names no key column" if names.empty?
266
- CAFrame.__send__(:refuse_repeated_names, names.map { |name| column_key(name, verb) },
267
- verb, "#{option} names")
297
+ # The key column names of a join, this frame's and the other's: one name
298
+ # or an Array of names (the same on both sides), or a Hash of this frame's
299
+ # name to the other's.
300
+ private def join_key_pairs(on, verb, option)
301
+ pairs = case on
302
+ when Hash then on.to_a
303
+ when Array then on.map { |name| [name, name] }
304
+ else [[on, on]]
305
+ end
306
+ raise ArgumentError, "#{verb}: #{option} names no key column" if pairs.empty?
307
+ lnames = pairs.map { |name, _| column_key(name, verb) }
308
+ rnames = pairs.map { |_, name| column_key(name, verb) }
309
+ CAFrame.__send__(:refuse_repeated_names, lnames, verb, "#{option} names")
310
+ CAFrame.__send__(:refuse_repeated_names, rnames, verb, "#{option} names")
311
+ return [lnames, rnames]
268
312
  end
269
313
 
270
314
  # Codes for the rows of both sides of a join on +lkeys+ / +rkeys+ (one
@@ -396,9 +440,11 @@ class CAFrame
396
440
  # The left index keeps its name as the result's row axis, so a right
397
441
  # column of that name collides with it as with a left column: it takes
398
442
  # the right suffix, and there is no left column to take the left one.
399
- private def join_name_plan(other, names, suffixes)
443
+ # The names of +other+'s columns that the join adds (its key columns
444
+ # +rnames+ are not added) and that this frame already has collide.
445
+ private def join_name_plan(other, names, rnames, suffixes)
400
446
  taken = @index ? column_names | [@axis_name] : column_names
401
- collisions = (taken & other.column_names) - names
447
+ collisions = taken & (other.column_names - rnames)
402
448
  return { collisions: [], lsuf: nil, rsuf: nil } if collisions.empty?
403
449
 
404
450
  if suffixes == false
@@ -427,19 +473,24 @@ class CAFrame
427
473
  cols[name] = col
428
474
  end
429
475
 
430
- private def join_by_addr(other, names, addr, plan)
476
+ # The left key columns keep their names, as in join_pairs; a right column
477
+ # of the same name takes the right suffix.
478
+ private def join_by_addr(other, names, rnames, addr, plan, view)
431
479
  cols = {}
432
- column_names.each { |name| put_join_column(cols, join_name(name, plan[:lsuf], plan), self[name]) }
480
+ column_names.each do |name|
481
+ name_out = names.include?(name) ? name : join_name(name, plan[:lsuf], plan)
482
+ put_join_column(cols, name_out, self[name])
483
+ end
433
484
  other.column_names.each do |name|
434
- next if names.include?(name)
435
- put_join_column(cols, join_name(name, plan[:rsuf], plan), project_rows(other[name], addr))
485
+ next if rnames.include?(name)
486
+ put_join_column(cols, join_name(name, plan[:rsuf], plan), join_rows(other[name], addr, view))
436
487
  end
437
488
  CAFrame.new(cols, axis_name: @axis_name, index: @index)
438
489
  end
439
490
 
440
491
  # The rows of a key join: li / ri are the left / right row of each result
441
492
  # row, UNDEF on the side a row has no match on.
442
- private def join_pairs(other, names, lkeys, rkeys, how, plan)
493
+ private def join_pairs(other, names, rnames, lkeys, rkeys, how, plan, view)
443
494
  parts, lcode, rcode, k = join_codes(lkeys, rkeys)
444
495
  extra = nil
445
496
  if how == :right
@@ -463,21 +514,33 @@ class CAFrame
463
514
  every_left = how == :left || how == :inner
464
515
  identity = every_left && li.elements == nrow &&
465
516
  (nrow == 0 || li.eq(CArray.int64(nrow).seq).all)
466
- rows = { li: li, ri: ri, extra: extra, how: how, every_left: every_left, identity: identity }
517
+ # Every result row of an inner or right join has a row of the other
518
+ # frame, so its columns are views of those rows, as the left's are in
519
+ # an inner or left join.
520
+ every_right = how == :right || how == :inner
521
+ right_identity = every_right && ri.elements == other.nrow &&
522
+ (other.nrow == 0 || ri.eq(CArray.int64(other.nrow).seq).all)
523
+ rows = { li: li, ri: ri, extra: extra, how: how, every_left: every_left, identity: identity,
524
+ view: view }
467
525
  cols = {}
468
526
  column_names.each do |name|
469
527
  j = names.index(name)
470
528
  col = if j then joined_key(j, parts, lkeys, rkeys, rows)
471
529
  elsif identity then self[name]
472
530
  elsif every_left then self[name][li, *([nil] * (self[name].ndim - 1))]
473
- else project_rows(self[name], li)
531
+ else join_rows(self[name], li, view)
474
532
  end
475
533
  name = join_name(name, plan[:lsuf], plan) unless j
476
534
  put_join_column(cols, name, col)
477
535
  end
478
536
  other.column_names.each do |name|
479
- next if names.include?(name)
480
- put_join_column(cols, join_name(name, plan[:rsuf], plan), project_rows(other[name], ri))
537
+ next if rnames.include?(name)
538
+ col = other[name]
539
+ col = if right_identity then col
540
+ elsif every_right then col[ri, *([nil] * (col.ndim - 1))]
541
+ else join_rows(col, ri, view)
542
+ end
543
+ put_join_column(cols, join_name(name, plan[:rsuf], plan), col)
481
544
  end
482
545
  # A key that is the left index is the result's index, as a key column
483
546
  # is a column of it.
@@ -486,7 +549,7 @@ class CAFrame
486
549
  elsif ij then joined_key(ij, parts, lkeys, rkeys, rows)
487
550
  elsif identity then @index
488
551
  elsif every_left then @index[li]
489
- else project_rows(@index, li)
552
+ else join_rows(@index, li, view)
490
553
  end
491
554
  CAFrame.new(cols, axis_name: @axis_name, index: new_index)
492
555
  end
@@ -498,7 +561,8 @@ class CAFrame
498
561
  li, ri = rows[:li], rows[:ri]
499
562
  return lkeys[j] if rows[:identity]
500
563
  return lkeys[j][li] if rows[:every_left]
501
- return project_rows(rkeys[j], ri) if rows[:how] == :right
564
+ return rkeys[j][ri] if rows[:how] == :right
565
+ return joined_key_view(lkeys[j], rkeys[j], li, rows[:extra]) if rows[:view]
502
566
  common, lc, rc = parts[j]
503
567
  extra = rows[:extra]
504
568
  code = if extra
@@ -586,6 +650,51 @@ class CAFrame
586
650
  end
587
651
  end
588
652
 
653
+ # The key of an outer join as a view: the two key columns welded
654
+ # (+CArray.meld+), picked at the left rows and then at the right rows no
655
+ # left row matched. Welding the whole columns, whatever rows the join
656
+ # gives, makes the call raise or pass the same way with any data, and
657
+ # makes the key show the attributes both keys have alike, as the default
658
+ # key does.
659
+ private def joined_key_view(lkey, rkey, li, extra)
660
+ # A Face's class, not the view class a filtered key has.
661
+ lface = lkey.class if lkey.face?
662
+ rface = rkey.class if rkey.face?
663
+ reason = if lface != rface || lkey.data_type != rkey.data_type
664
+ "#{CArray::Inspector.type_label(lkey)} and #{CArray::Inspector.type_label(rkey)}"
665
+ end
666
+ unless reason
667
+ begin
668
+ both = CArray.meld([lkey, rkey], axis: 0)
669
+ rescue ArgumentError => e # a Face's state, such as a time unit
670
+ reason = e.message
671
+ end
672
+ end
673
+ if reason
674
+ raise ArgumentError,
675
+ "join: view: :full welds the two key columns of an outer join, which needs " \
676
+ "one kind of key (#{reason}); cast one side first, or leave view: out"
677
+ end
678
+ return both[li] unless extra
679
+ nleft = li.elements - extra.elements
680
+ both[CArray.concatenate([li[0...nleft], extra + lkey.elements])]
681
+ end
682
+
683
+ # view: is nil (views where every row has a row of the side, new columns
684
+ # where some do not) or :full (views throughout, UNDEF for a row with no
685
+ # row of the side).
686
+ private def refuse_unknown_view(view, verb)
687
+ return if view.nil? || view == :full
688
+ raise ArgumentError, "#{verb}: view: takes :full (got #{view.inspect})"
689
+ end
690
+
691
+ # The rows +addr+ picks from +col+, UNDEF where +addr+ is masked: a view
692
+ # with view: :full, else a new column.
693
+ private def join_rows(col, addr, view)
694
+ return project_rows(col, addr) unless view == :full
695
+ col[addr, *([nil] * (col.ndim - 1))]
696
+ end
697
+
589
698
  private def project_rows(col, addr)
590
699
  return col.project(addr) if col.ndim == 1
591
700
  trailing = col.shape[1..]
@@ -1,7 +1,7 @@
1
1
  # CAFrame record (row-oriented) input (memo §11.2, §11.5).
2
2
  #
3
3
  # from_records takes an Array of row Hashes -- the shape JSON.parse yields for
4
- # a JSON array of objects -- and arranges it into columns. Unlike from_csv,
4
+ # a JSON array of objects -- and arranges it into columns. Unlike read_csv,
5
5
  # the cell values are already typed Ruby objects (Float / Integer / DateTime /
6
6
  # String), so a homogeneous column is built at its native leaf type rather than
7
7
  # left as strings ("arrange by the value's own type", not string inference --
@@ -41,7 +41,7 @@ class CAFrame
41
41
  end
42
42
 
43
43
  frame = new(cols)
44
- frame.cast(types, on_error: on_error, option_name: :types) unless types.nil?
44
+ frame.__send__(:cast_in_place, types, on_error: on_error, option_name: :types) unless types.nil?
45
45
  frame
46
46
  end
47
47
 
@@ -67,7 +67,7 @@ class CAFrame
67
67
  # them (CAFrame.new and df[name]= given an Array, from_records, the values
68
68
  # a group's block or Proc gives). All-Array cells of equal length -> N-D
69
69
  # native column; numeric scalars -> native scalar column (missing ->
70
- # UNDEF); Strings -> a text column (CAString), as from_csv reads text;
70
+ # UNDEF); Strings -> a text column (CAString), as read_csv reads text;
71
71
  # time and duration elements -> a CATime / CATimedelta column in the finest
72
72
  # resolution among them; anything else -> object column.
73
73
  def self.build_record_column(values, n)
@@ -165,8 +165,8 @@ class CAFrame
165
165
  # names-check: two rows for one cell of the grid
166
166
  first = cell.to_a.tally.find { |_, count| count > 1 }.first
167
167
  raise ArgumentError,
168
- "pivot: more than one row for #{index}=#{rlab[first / nc].inspect}, " \
169
- "#{columns}=#{clab[first % nc].inspect}; pass aggregate: to combine them"
168
+ "pivot: more than one row for #{index}=#{CAFrame.__send__(:key_text, rlab[first / nc])}, " \
169
+ "#{columns}=#{CAFrame.__send__(:key_text, clab[first % nc])}; pass aggregate: to combine them"
170
170
  end
171
171
  source[cell] = CArray.int64(nrow).seq[placed]
172
172
  end
@@ -199,6 +199,61 @@ class CAFrame
199
199
  groups.public_send(plan[:aggregate])
200
200
  end
201
201
 
202
+ # The bits of each integer type and whether it is signed; the significand
203
+ # bits of each floating type (a complex type by its parts).
204
+ INTEGER_BITS = {
205
+ "int8" => [8, true], "int16" => [16, true], "int32" => [32, true], "int64" => [64, true],
206
+ "uint8" => [8, false], "uint16" => [16, false], "uint32" => [32, false], "uint64" => [64, false],
207
+ }.freeze
208
+ SIGNIFICAND_BITS = { "float32" => 24, "float64" => 53, "cmplx64" => 24, "cmplx128" => 53 }.freeze
209
+ Ractor.make_shareable(INTEGER_BITS)
210
+ Ractor.make_shareable(SIGNIFICAND_BITS)
211
+ private_constant :INTEGER_BITS, :SIGNIFICAND_BITS
212
+
213
+ # Whether every value of data type +from+ is exactly a value of +to+, for
214
+ # numbers and booleans: a boolean in any number, an integer in a wide
215
+ # enough integer (a signed one never in an unsigned) or in a float whose
216
+ # significand holds it, a float in a wider float or complex.
217
+ def self.holds_every_value (from, to)
218
+ f = CArray.data_type_name(from)
219
+ t = CArray.data_type_name(to)
220
+ return true if f == t || f == "boolean"
221
+ if (fbits, fsigned = INTEGER_BITS[f])
222
+ if (tbits, tsigned = INTEGER_BITS[t])
223
+ return fsigned ? (tsigned && tbits >= fbits) : (tsigned ? tbits > fbits : tbits >= fbits)
224
+ end
225
+ significand = SIGNIFICAND_BITS[t]
226
+ return !significand.nil? && (fsigned ? fbits - 1 : fbits) <= significand
227
+ end
228
+ fs = SIGNIFICAND_BITS[f]
229
+ ts = SIGNIFICAND_BITS[t]
230
+ return false if fs.nil? || ts.nil? || ts < fs
231
+ return !(f.start_with?("cmplx") && t.start_with?("float"))
232
+ end
233
+ private_class_method :holds_every_value
234
+
235
+ # The types melt may bring number columns to, narrowest first within each
236
+ # kind, so the first that holds every column's type is the common one.
237
+ MELT_TYPES = %w[int8 uint8 int16 uint16 int32 uint32 int64 uint64
238
+ float32 float64 cmplx64 cmplx128].freeze
239
+ Ractor.make_shareable(MELT_TYPES)
240
+ private_constant :MELT_TYPES
241
+
242
+ # The common type of number (and boolean) columns of different types: the
243
+ # one CArray.result_type picks when it holds every value of every column's
244
+ # type, else the narrowest that does; nil when no type does. Decided by
245
+ # the types alone, so a value written to a column later is one the common
246
+ # type holds too.
247
+ def self.melt_common_type (pieces)
248
+ types = pieces.map(&:data_type)
249
+ holds = ->(to) { types.all? { |t| holds_every_value(t, to) } }
250
+ first = CArray.result_type(*pieces)
251
+ return first if holds.(first)
252
+ name = MELT_TYPES.find { |t| holds.(t) }
253
+ name&.to_sym
254
+ end
255
+ private_class_method :melt_common_type
256
+
202
257
  # Gather a wide frame into a long one. The +value_columns+ are stacked one
203
258
  # after another into a single +value_name+ column, a +var_name+ column says
204
259
  # which of them each row came from (a text column), and every +id+ column
@@ -212,8 +267,12 @@ class CAFrame
212
267
  #
213
268
  # The result is a view frame: the value column is +CArray.meld+ of the
214
269
  # melted columns and each id column is +CArray.meld+ of itself, so a write
215
- # to the result lands in +self+. The value columns must therefore share
216
- # one data type and one trailing shape; cast first when they do not.
270
+ # to the result lands in +self+. Number columns of different types meet
271
+ # at a type that holds every value of each column's type (int8 and uint8
272
+ # at int16, int32 and float64 at float64), so a value written later reads
273
+ # back unchanged; a mixture no type holds (int64 and float64) raises, and
274
+ # so do other mixtures of types. The value columns share one trailing
275
+ # shape; cast first when they do not.
217
276
  #
218
277
  # @param id [String, Array<String>] columns repeated alongside each value.
219
278
  # @param value_columns [Array<String>, nil] columns to stack; default all
@@ -222,7 +281,7 @@ class CAFrame
222
281
  # @param value_name [String] name of the stacked value column.
223
282
  # @return [CAFrame] a view frame of +nrow * value_columns.size+ rows.
224
283
  # @raise [ArgumentError] when there is nothing to melt, or the value
225
- # columns differ in data type.
284
+ # columns differ in data type and no type holds every value of each.
226
285
  def melt(id: [], value_columns: nil, var_name: "variable", value_name: "value")
227
286
  ids = column_keys(Array(id), "melt")
228
287
  ids.each { |name| self[name] }
@@ -235,9 +294,20 @@ class CAFrame
235
294
  pieces = vars.map { |name| self[name] }
236
295
  types = pieces.map(&:data_type).uniq
237
296
  unless types.size == 1
238
- raise ArgumentError,
239
- "melt: value columns differ in data type (#{vars.zip(pieces.map(&:data_type)).to_h}); " \
240
- "cast them to one type first"
297
+ # Numbers of different types meet at a type that holds every value of
298
+ # every column's type, each through a view of its own column (as_type),
299
+ # so the value column still reads and writes the columns it was made
300
+ # from, whatever is written to them later.
301
+ common = nil
302
+ if pieces.all? { |c| !c.face? && (c.numeric? || c.data_type == :boolean) }
303
+ common = CAFrame.__send__(:melt_common_type, pieces)
304
+ end
305
+ if common.nil?
306
+ raise ArgumentError,
307
+ "melt: value columns differ in data type (#{vars.zip(pieces.map(&:data_type)).to_h}) " \
308
+ "and no type holds every value of each; cast them to one type first"
309
+ end
310
+ pieces = pieces.map { |c| c.data_type == common ? c : c.as_type(common) }
241
311
  end
242
312
  shapes = pieces.map { |c| c.shape.drop(1) }.uniq
243
313
  unless shapes.size == 1
@@ -259,7 +329,15 @@ class CAFrame
259
329
  end
260
330
  n = nrow
261
331
  out[var_name] = CArray.string(Array.new(n * k) { |i| vars[i / n] })
262
- out[value_name] = CArray.meld(pieces)
332
+ begin
333
+ out[value_name] = CArray.meld(pieces)
334
+ rescue ArgumentError => e
335
+ raise unless e.message.start_with?("promote_list:")
336
+ labels = vars.zip(pieces.map { |c| CArray::Inspector.type_label(c) }).to_h
337
+ raise ArgumentError,
338
+ "melt: value columns #{labels} do not meet as one column " \
339
+ "(#{e.message.delete_prefix("promote_list: ")}); cast them to one type first"
340
+ end
263
341
  CAFrame.new(out)
264
342
  end
265
343
 
@@ -323,7 +401,7 @@ class CAFrame
323
401
  end
324
402
  if on
325
403
  on = column_name_arg(on, "stack_rows")
326
- raise KeyError, "stack_rows: no column #{on.inspect}" unless frame.column_names.include?(on)
404
+ frame.__send__(:refuse_missing_column, on, "stack_rows") unless frame.column_names.include?(on)
327
405
  return stack_rows_on(frame, keys, on)
328
406
  end
329
407
  grouped = frame.group_by(*keys)
@@ -362,7 +440,7 @@ class CAFrame
362
440
  # given as a CArray has no name and is written "key").
363
441
  private def group_key_desc(keys, label)
364
442
  values = keys.size == 1 ? [label] : label
365
- keys.zip(values).map { |k, v| "#{k.is_a?(String) ? k : "key"}=#{v.inspect}" }.join(", ")
443
+ keys.zip(values).map { |k, v| "#{k.is_a?(String) ? k : "key"}=#{CAFrame.__send__(:key_text, v)}" }.join(", ")
366
444
  end
367
445
 
368
446
  private def stack_rows_on(frame, keys, on)
@@ -5,7 +5,7 @@
5
5
  # the single-key instance CArray#sort_index) -- and gathering every column and
6
6
  # the index by that address into a view-frame in key order:
7
7
  #
8
- # sort_by_key(*keys, order:, masked_position:) -- sort by named columns, with
8
+ # sort_by_key(*keys, descending:, masked_position:) -- sort by named columns, with
9
9
  # per-key or blanket ascending/descending.
10
10
  # sort_by { |f| ... } -- sort by key CArrays the block builds (the escape,
11
11
  # §4.3): derived or composite keys are ordinary column
@@ -25,19 +25,20 @@ class CAFrame
25
25
  # row permutation and gathers every column and the index by it.
26
26
  #
27
27
  # Each key is a **column name** (or the index axis name), or a
28
- # **`[name, :asc | :desc]`** pair for a per-key direction. The +order:+ keyword
29
- # is the direction for bare-name keys (default +:asc+). +masked_position:+
28
+ # **`[name, :asc | :desc]`** pair for a per-key direction. +descending: true+
29
+ # sorts the bare-name keys from largest to smallest (default ascending), as
30
+ # +CArray#order+ takes it. +masked_position:+
30
31
  # sends masked key rows to the +:last+ (default) or +:first+ end.
31
32
  #
32
33
  # df.sort_by_key("temp") # one column, ascending
33
34
  # df.sort_by_key("station", "temp") # lexicographic
34
- # df.sort_by_key("temp", order: :desc) # all keys descending
35
+ # df.sort_by_key("temp", descending: true) # all keys descending
35
36
  # df.sort_by_key(["station", :asc], ["temp", :desc]) # per-key direction
36
37
  # df.sort_by_key("temp", masked_position: :first) # masked rows first
37
38
  # df.sort_by_key("time") # by the index
38
- def sort_by_key(*specs, order: :asc, masked_position: :last)
39
+ def sort_by_key(*specs, descending: false, masked_position: :last)
39
40
  raise ArgumentError, "sort_by_key requires at least one key" if specs.empty?
40
- validate_sort_order(order)
41
+ order = descending ? :desc : :asc
41
42
  named = specs.map do |spec|
42
43
  name, dir = parse_sort_key_spec(spec, order)
43
44
  [column_key(name, "sort_by_key"), key_column(column_key(name, "sort_by_key"), "sort_by_key"), dir]
@@ -74,6 +75,50 @@ class CAFrame
74
75
  false
75
76
  end
76
77
 
78
+ # The +n+ rows with the largest values of the key columns, largest first,
79
+ # as a view-frame: +sort_by_key(*keys, descending: true).head(n)+. Several
80
+ # keys break ties in turn, as +sort_by_key+ takes them; rows that tie on
81
+ # every key keep their order. A NaN and a masked key lose to every value,
82
+ # as +CArray#nlargest+ has them, so they come only when fewer than +n+
83
+ # rows have a value.
84
+ #
85
+ # df.nlargest(3, "revenue")
86
+ # df.nsmallest(2, "rank", "time")
87
+ #
88
+ # @param n [Integer] how many rows, 0 or more.
89
+ # @param keys [Array<String>] the key columns (or the index axis name).
90
+ # @return [CAFrame]
91
+ # @raise [ArgumentError] when +n+ is negative or no key is given.
92
+ def nlargest (n, *keys)
93
+ return largest_or_smallest("nlargest", n, keys, true)
94
+ end
95
+
96
+ # The +n+ rows with the smallest values of the key columns, smallest first,
97
+ # as +nlargest+ gives the largest.
98
+ #
99
+ # @param n [Integer] how many rows, 0 or more.
100
+ # @param keys [Array<String>] the key columns (or the index axis name).
101
+ # @return [CAFrame]
102
+ # @raise [ArgumentError] when +n+ is negative or no key is given.
103
+ def nsmallest (n, *keys)
104
+ return largest_or_smallest("nsmallest", n, keys, false)
105
+ end
106
+
107
+ private def largest_or_smallest (verb, n, keys, descending)
108
+ unless n.is_a?(Integer)
109
+ raise TypeError, "#{verb}: the count is an Integer (got #{n.inspect})"
110
+ end
111
+ raise ArgumentError, "#{verb}: the count must be 0 or more (got #{n})" if n < 0
112
+ raise ArgumentError, "#{verb}: give at least one key column" if keys.empty?
113
+ keys.each { |k| column_key(k, verb) }
114
+ begin
115
+ return sort_by_key(*keys, descending: descending).head(n)
116
+ rescue ArgumentError, KeyError, TypeError, CArray::DataTypeError => e
117
+ # The sort is this verb's own, so its refusal names this verb.
118
+ raise e.class, e.message.sub(/\Asort_by_key:/, "#{verb}:"), e.backtrace
119
+ end
120
+ end
121
+
77
122
  # Sort rows by key columns the block builds (memo §4.3 escape). The block
78
123
  # receives the frame and returns a key CArray, or an Array of them for a
79
124
  # multi-key lexicographic sort; rows are sorted ascending by +CArray.sort_addr+
@@ -81,11 +126,13 @@ class CAFrame
81
126
  # +filter+: where +sort_by_key+ takes plain column names, the block form lets
82
127
  # you build any key with CArray ops -- a descending key is its dense
83
128
  # descending rank, derived or composite keys are ordinary column math.
84
- # +masked_position:+ places masked key rows first or last (default last).
129
+ # +descending: true+ sorts every key from largest to smallest, as
130
+ # +sort_by_key+ takes it; +masked_position:+ places masked key rows first or
131
+ # last (default last).
85
132
  #
86
133
  # df.sort_by { |f| (f["temp"] - target).abs } # nearest-to-target first
87
- # df.sort_by { |f| f["temp"].order(descending: true, method: :dense) }
88
- def sort_by(masked_position: :last)
134
+ # df.sort_by(descending: true) { |f| f["name"].length }
135
+ def sort_by(descending: false, masked_position: :last)
89
136
  raise ArgumentError, "sort_by requires a block" unless block_given?
90
137
  keys = yield(self)
91
138
  keys = [keys] unless keys.is_a?(Array)
@@ -99,6 +146,7 @@ class CAFrame
99
146
  "sort_by key #{i} must be a 1-D column of length #{nrow} (got shape #{k.shape.inspect})"
100
147
  end
101
148
  end
149
+ keys = keys.map { |k| k.order(descending: true, method: :dense) } if descending
102
150
  select_rows(CArray.sort_addr(*keys, masked_position: masked_position))
103
151
  end
104
152