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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +233 -0
- data/README.md +1 -1
- data/carray.gemspec +3 -2
- data/ext/ca_axis_dispatch.c +6 -0
- data/ext/ca_axis_group.c +1 -1
- data/ext/ca_categorical_iterator.c +7 -7
- data/ext/ca_kernel_iterator.c +116 -231
- data/ext/ca_kernel_iterator.h +410 -254
- data/ext/ca_obj_bincmp.c +9 -1
- data/ext/ca_obj_binop.c +10 -1
- data/ext/ca_obj_bitfield.c +2 -0
- data/ext/ca_obj_byte_swap.c +2 -0
- data/ext/ca_obj_face.c +2 -0
- data/ext/ca_obj_fake.c +3 -1
- data/ext/ca_obj_grid.c +167 -12
- data/ext/ca_obj_meld.c +69 -113
- data/ext/ca_obj_moncmp.c +9 -1
- data/ext/ca_obj_monop.c +40 -2
- data/ext/ca_obj_object.c +54 -20
- data/ext/ca_obj_reduce.c +15 -1
- data/ext/ca_obj_refer.c +65 -20
- data/ext/ca_obj_remap.c +69 -10
- data/ext/ca_obj_roll.c +1 -0
- data/ext/ca_obj_select.c +4 -1
- data/ext/ca_obj_select_axis.c +1 -0
- data/ext/ca_obj_shift.c +1 -0
- data/ext/ca_obj_stack.c +310 -111
- data/ext/ca_obj_stride.c +131 -45
- data/ext/ca_obj_string.c +18 -2
- data/ext/ca_obj_tile.c +1 -0
- data/ext/ca_obj_triop.c +9 -1
- data/ext/ca_obj_window.c +1 -0
- data/ext/ca_op_ipower.c +8 -8
- data/ext/caframe_csv_reader.c +1 -1
- data/ext/carray.h +2 -1
- data/ext/carray_access.c +27 -64
- data/ext/carray_address_basis.c +134 -0
- data/ext/carray_attribute.c +8 -0
- data/ext/carray_broadcast.c +3 -4
- data/ext/carray_build_flags.h +1 -1
- data/ext/carray_cast.c +8 -11
- data/ext/carray_conversion.c +9 -5
- data/ext/carray_core.c +811 -72
- data/ext/carray_element.c +13 -9
- data/ext/carray_histogram.c +25 -3
- data/ext/carray_internal.h +147 -12
- data/ext/carray_kernels_bincmp.c +2007 -3
- data/ext/carray_kernels_binop.c +25365 -5199
- data/ext/carray_kernels_init.c +8 -8
- data/ext/carray_kernels_monop.c +10696 -3678
- data/ext/carray_kernels_reduce_aggregate.c +4429 -702
- data/ext/carray_kernels_reduce_boolean.c +1533 -15
- data/ext/carray_kernels_reduce_cumulative.c +1808 -248
- data/ext/carray_kernels_reduce_extreme.c +4508 -511
- data/ext/carray_kernels_reduce_variance.c +2284 -152
- data/ext/carray_kernels_scan.c +240 -240
- data/ext/carray_kernels_search.c +4211 -264
- data/ext/carray_kernels_sort.c +367 -450
- data/ext/carray_lazy.c +47 -0
- data/ext/carray_loop.c +1 -1
- data/ext/carray_mask.c +130 -139
- data/ext/carray_median_percentile.c +4 -4
- data/ext/carray_memory_view.c +13 -7
- data/ext/carray_operator.c +147 -44
- data/ext/carray_order.c +3 -2
- data/ext/carray_random.c +4 -1
- data/ext/carray_slab.c +97 -21
- data/ext/carray_sort.c +1 -1
- data/ext/carray_test.c +74 -19
- data/ext/carray_time_text.c +2 -2
- data/ext/extconf.rb +11 -0
- data/ext/mkkernel.rb +346 -94
- data/ext/ruby_carray.c +4 -1
- data/ext/version.h +4 -4
- data/lib/carray/attribute.rb +25 -1
- data/lib/carray/autoload_carray.rb +3 -3
- data/lib/carray/autoload_method_extension.rb +5 -3
- data/lib/carray/axis_group.rb +13 -7
- data/lib/carray/basics.rb +1 -1
- data/lib/carray/block_iterator.rb +17 -3
- data/lib/carray/categorical.rb +6 -8
- data/lib/carray/categorical_iterator.rb +2 -2
- data/lib/carray/complex.rb +2 -2
- data/lib/carray/conditional.rb +3 -2
- data/lib/carray/frame/column_verbs.rb +45 -14
- data/lib/carray/frame/convert.rb +19 -0
- data/lib/carray/frame/csv_parser.rb +32 -32
- data/lib/carray/frame/describe.rb +4 -2
- data/lib/carray/frame/frame.rb +399 -78
- data/lib/carray/frame/group.rb +679 -53
- data/lib/carray/frame/io.rb +209 -71
- data/lib/carray/frame/join.rb +146 -37
- data/lib/carray/frame/records.rb +3 -3
- data/lib/carray/frame/reshape.rb +89 -11
- data/lib/carray/frame/sort.rb +57 -9
- data/lib/carray/frame/verbs.rb +384 -49
- data/lib/carray/frame/windows.rb +44 -6
- data/lib/carray/inspect.rb +7 -2
- data/lib/carray/iterator.rb +32 -0
- data/lib/carray/lazy.rb +17 -8
- data/lib/carray/mask_gap_fill.rb +37 -17
- data/lib/carray/meld_reduce.rb +1 -1
- data/lib/carray/methods/choose.rb +3 -2
- data/lib/carray/methods/meshgrid.rb +3 -1
- data/lib/carray/runtime.rb +47 -8
- data/lib/carray/segment_iterator.rb +23 -8
- data/lib/carray/serialize.rb +34 -17
- data/lib/carray/stack.rb +18 -6
- data/lib/carray/time.rb +5 -0
- data/lib/carray/window_iterator.rb +16 -4
- data/yard-stubs/ca_obj_array.rb +26 -1
- data/yard-stubs/carray_cast.rb +8 -5
- data/yard-stubs/carray_conversion.rb +8 -2
- data/yard-stubs/carray_mask.rb +20 -1
- data/yard-stubs/carray_math.rb +31 -16
- data/yard-stubs/carray_memory_view.rb +7 -0
- data/yard-stubs/carray_test.rb +7 -4
- metadata +18 -4
data/lib/carray/frame/join.rb
CHANGED
|
@@ -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.
|
|
33
|
-
#
|
|
34
|
-
# :inner
|
|
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 =
|
|
91
|
+
names, rnames = join_key_pairs(on, "join", "on:")
|
|
66
92
|
lkeys = names.map { |name| key_column(name, "join") }
|
|
67
|
-
rkeys =
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
if
|
|
94
|
-
raise ArgumentError, "join_asof:
|
|
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,
|
|
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 =
|
|
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
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
435
|
-
put_join_column(cols, join_name(name, plan[:rsuf], plan),
|
|
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
|
-
|
|
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
|
|
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
|
|
480
|
-
|
|
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
|
|
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
|
|
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..]
|
data/lib/carray/frame/records.rb
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
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)
|
data/lib/carray/frame/reshape.rb
CHANGED
|
@@ -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]
|
|
169
|
-
"#{columns}=#{clab[first % nc]
|
|
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+.
|
|
216
|
-
#
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
data/lib/carray/frame/sort.rb
CHANGED
|
@@ -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,
|
|
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.
|
|
29
|
-
#
|
|
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",
|
|
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,
|
|
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
|
-
|
|
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
|
-
# +
|
|
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["
|
|
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
|
|