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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +438 -0
- data/README.md +1 -1
- data/carray.gemspec +3 -2
- data/ext/ca_array_pool.c +25 -1
- data/ext/ca_axis_dispatch.c +19 -20
- data/ext/ca_axis_group.c +1 -1
- data/ext/ca_categorical_iterator.c +34 -27
- data/ext/ca_compare.c +4 -4
- data/ext/ca_composite_dispatch.c +9 -9
- data/ext/ca_kernel_iterator.c +122 -237
- data/ext/ca_kernel_iterator.h +522 -337
- data/ext/ca_obj_array.c +69 -19
- data/ext/ca_obj_bincmp.c +10 -1
- data/ext/ca_obj_binop.c +11 -1
- data/ext/ca_obj_bitarray.c +5 -1
- data/ext/ca_obj_bitfield.c +60 -43
- data/ext/ca_obj_byte_swap.c +4 -1
- data/ext/ca_obj_const_string.c +15 -3
- data/ext/ca_obj_face.c +44 -1
- data/ext/ca_obj_fake.c +3 -1
- data/ext/ca_obj_field.c +13 -3
- data/ext/ca_obj_grid.c +219 -17
- data/ext/ca_obj_meld.c +69 -113
- data/ext/ca_obj_moncmp.c +9 -1
- data/ext/ca_obj_monop.c +86 -12
- data/ext/ca_obj_object.c +118 -32
- data/ext/ca_obj_record.c +3 -0
- data/ext/ca_obj_reduce.c +19 -1
- data/ext/ca_obj_refer.c +80 -26
- data/ext/ca_obj_remap.c +69 -10
- data/ext/ca_obj_roll.c +27 -6
- data/ext/ca_obj_select.c +38 -6
- data/ext/ca_obj_select_axis.c +1 -0
- data/ext/ca_obj_shift.c +32 -7
- data/ext/ca_obj_stack.c +311 -111
- data/ext/ca_obj_stride.c +159 -64
- data/ext/ca_obj_string.c +27 -2
- data/ext/ca_obj_tile.c +28 -7
- data/ext/ca_obj_transpose.c +8 -4
- data/ext/ca_obj_triop.c +12 -1
- data/ext/ca_obj_window.c +80 -50
- data/ext/ca_op_ipower.c +12 -12
- data/ext/ca_sort_kernels.h +6 -3
- data/ext/caframe_csv_reader.c +1 -1
- data/ext/carray.h +35 -1
- data/ext/carray_access.c +43 -67
- data/ext/carray_address_basis.c +134 -0
- data/ext/carray_attribute.c +32 -0
- data/ext/carray_broadcast.c +3 -4
- data/ext/carray_build_flags.h +1 -1
- data/ext/carray_cast.c +251 -93
- data/ext/carray_cast_func.rb +56 -26
- data/ext/carray_conversion.c +11 -7
- data/ext/carray_core.c +814 -72
- data/ext/carray_count.c +6 -0
- data/ext/carray_element.c +22 -13
- data/ext/carray_factorize.c +20 -20
- data/ext/carray_generate.c +8 -2
- data/ext/carray_histogram.c +26 -4
- data/ext/carray_index_classifier.c +4 -4
- data/ext/carray_internal.h +159 -12
- data/ext/carray_kernels_bincmp.c +2174 -170
- data/ext/carray_kernels_binop.c +25653 -5487
- data/ext/carray_kernels_init.c +8 -8
- data/ext/carray_kernels_moncmp.c +96 -96
- data/ext/carray_kernels_monop.c +12253 -5235
- data/ext/carray_kernels_reduce_aggregate.c +6150 -2423
- data/ext/carray_kernels_reduce_boolean.c +1533 -15
- data/ext/carray_kernels_reduce_cumulative.c +2442 -882
- data/ext/carray_kernels_reduce_extreme.c +5658 -1661
- data/ext/carray_kernels_reduce_variance.c +2534 -402
- data/ext/carray_kernels_scan.c +368 -368
- data/ext/carray_kernels_search.c +4551 -604
- data/ext/carray_kernels_sort.c +659 -751
- data/ext/carray_kernels_triop.c +114 -114
- data/ext/carray_key_windows.c +4 -0
- data/ext/carray_lazy.c +47 -0
- data/ext/carray_loop.c +1 -1
- data/ext/carray_mask.c +175 -161
- data/ext/carray_median_percentile.c +4 -4
- data/ext/carray_memory_view.c +15 -9
- data/ext/carray_operator.c +152 -49
- data/ext/carray_order.c +230 -10
- data/ext/carray_random.c +22 -19
- data/ext/carray_scatter.c +24 -7
- data/ext/carray_slab.c +128 -29
- data/ext/carray_sort.c +224 -16
- data/ext/carray_sort_kernel.c +5 -7
- data/ext/carray_struct.c +35 -43
- data/ext/carray_test.c +74 -19
- data/ext/carray_time_text.c +6 -4
- data/ext/carray_utils.c +63 -21
- data/ext/extconf.rb +11 -0
- data/ext/mkkernel.rb +546 -199
- data/ext/ruby_carray.c +4 -1
- data/ext/version.h +4 -4
- data/lib/carray/attribute.rb +42 -5
- 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 +28 -8
- data/lib/carray/block_iterator.rb +17 -3
- data/lib/carray/categorical.rb +35 -23
- data/lib/carray/categorical_iterator.rb +8 -8
- data/lib/carray/complex.rb +2 -2
- data/lib/carray/conditional.rb +3 -2
- data/lib/carray/const_string.rb +10 -0
- data/lib/carray/data_type_extension.rb +8 -1
- data/lib/carray/fixlen_string.rb +41 -2
- data/lib/carray/frame/column_verbs.rb +48 -16
- data/lib/carray/frame/concat.rb +17 -3
- 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 +405 -85
- data/lib/carray/frame/group.rb +777 -64
- data/lib/carray/frame/io.rb +212 -72
- 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/fuse_source.rb +31 -2
- data/lib/carray/inspect.rb +7 -2
- data/lib/carray/iterator.rb +32 -0
- data/lib/carray/lazy.rb +52 -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/is_in.rb +8 -1
- data/lib/carray/methods/meshgrid.rb +3 -1
- data/lib/carray/runtime.rb +47 -8
- data/lib/carray/segment_iterator.rb +35 -16
- data/lib/carray/serialize.rb +53 -19
- data/lib/carray/stack.rb +33 -18
- data/lib/carray/string.rb +8 -0
- data/lib/carray/string_operation_extension.rb +13 -5
- data/lib/carray/struct_builder.rb +5 -3
- data/lib/carray/time.rb +112 -68
- data/lib/carray/window_iterator.rb +16 -4
- data/yard-stubs/ca_obj_array.rb +26 -1
- data/yard-stubs/ca_obj_transpose.rb +5 -4
- data/yard-stubs/carray_attribute.rb +3 -1
- data/yard-stubs/carray_cast.rb +10 -5
- data/yard-stubs/carray_conversion.rb +13 -3
- data/yard-stubs/carray_element.rb +7 -6
- data/yard-stubs/carray_mask.rb +28 -4
- data/yard-stubs/carray_math.rb +45 -20
- data/yard-stubs/carray_memory_view.rb +7 -0
- data/yard-stubs/carray_order.rb +100 -4
- data/yard-stubs/carray_scatter.rb +7 -5
- data/yard-stubs/carray_test.rb +7 -4
- metadata +18 -4
data/lib/carray/frame/frame.rb
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
class CAFrame
|
|
14
14
|
# Raised when the contents of a column cannot be read as asked: a cell
|
|
15
|
-
# that does not read as the type a +cast+ (or +
|
|
15
|
+
# that does not read as the type a +cast+ (or +read_csv+'s +types:+) asks
|
|
16
16
|
# for under on_error: :raise, a cell +parse_to_time+ cannot read, a column
|
|
17
17
|
# whose time format +infer_time_format+ / parse_to_time(name, :infer)
|
|
18
18
|
# cannot find, or a float column +to_time+ cannot read as whole counts.
|
|
@@ -131,7 +131,7 @@ class CAFrame
|
|
|
131
131
|
"axis_name #{@axis_name.inspect} collides with a column of the same name"
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
cast_in_place(types, on_error: on_error, option_name: :types) unless types.nil?
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
# --- df[...] : key type decides the axis (memo §13.2) ------------------
|
|
@@ -155,13 +155,13 @@ class CAFrame
|
|
|
155
155
|
"df[...] with several keys returns columns; every key must be a String " \
|
|
156
156
|
"(use df.select(...) for a subset frame)"
|
|
157
157
|
end
|
|
158
|
-
return keys.map { |k| @columns.fetch(k) {
|
|
158
|
+
return keys.map { |k| @columns.fetch(k) { refuse_missing_column(k) } }
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
key = keys.first
|
|
162
162
|
case key
|
|
163
163
|
when String
|
|
164
|
-
@columns.fetch(key) {
|
|
164
|
+
@columns.fetch(key) { refuse_missing_column(key) }
|
|
165
165
|
when Integer
|
|
166
166
|
row(key)
|
|
167
167
|
when Range
|
|
@@ -182,12 +182,35 @@ class CAFrame
|
|
|
182
182
|
"CArray df[] key must be boolean or integer (got #{key.data_type})"
|
|
183
183
|
end
|
|
184
184
|
when Symbol
|
|
185
|
-
raise ArgumentError,
|
|
185
|
+
raise ArgumentError,
|
|
186
|
+
"df[symbol] is reserved for predicate keys; name a column with a String: " \
|
|
187
|
+
"df[#{key.to_s.inspect}]"
|
|
188
|
+
when Array
|
|
189
|
+
refuse_array_key(key)
|
|
186
190
|
else
|
|
187
191
|
raise ArgumentError, "unsupported df[] key: #{key.class}"
|
|
188
192
|
end
|
|
189
193
|
end
|
|
190
194
|
|
|
195
|
+
# A Ruby Array is no df[] key, read or written: CArray reads one as
|
|
196
|
+
# [start, count], where [0, 2] would be rows 0 and 1 rather than rows 0
|
|
197
|
+
# and 2. Says what to write for what the Array holds.
|
|
198
|
+
private def refuse_array_key(key, verb = "df[]")
|
|
199
|
+
strings = !key.empty? && key.all? { |k| k.is_a?(String) }
|
|
200
|
+
hint = if strings && verb == "df[]"
|
|
201
|
+
"for columns, df[#{key.map(&:inspect).join(', ')}] or " \
|
|
202
|
+
"df.select(#{key.map(&:inspect).join(', ')})"
|
|
203
|
+
elsif strings
|
|
204
|
+
"a column is named by one String, df[#{key.first.inspect}] = ..."
|
|
205
|
+
elsif !key.empty? && key.all? { |k| k == true || k == false }
|
|
206
|
+
"for rows, a boolean CArray: df[CA_BOOLEAN(#{key.inspect})]"
|
|
207
|
+
else
|
|
208
|
+
"for rows, an integer CArray: df[CA_INT64(#{key.inspect})] " \
|
|
209
|
+
"(a Range picks a run of rows: df[0..2])"
|
|
210
|
+
end
|
|
211
|
+
raise ArgumentError, "#{verb} takes no Ruby Array; #{hint}"
|
|
212
|
+
end
|
|
213
|
+
|
|
191
214
|
# --- df[...] = value : row-level assignment ---------------------------
|
|
192
215
|
#
|
|
193
216
|
# The RHS value decides the operation; the key selects the rows:
|
|
@@ -205,12 +228,13 @@ class CAFrame
|
|
|
205
228
|
#
|
|
206
229
|
# +sel+ is a row-axis indexer key, classified exactly as CArray classifies a
|
|
207
230
|
# 1-D column key (docs/topics/Indexer_decision_tree.md): a slice (BLOCK —
|
|
208
|
-
# Range / ArithmeticSequence
|
|
209
|
-
# (
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
-
#
|
|
231
|
+
# Range / ArithmeticSequence), a boolean CArray (SELECT), an integer CArray
|
|
232
|
+
# (GRID), or an Integer (a single row). A Ruby Array, which the column
|
|
233
|
+
# indexer would read as [start, count], is refused, as df[] refuses it.
|
|
234
|
+
# Mask and delete take the others and are forwarded to the column indexer,
|
|
235
|
+
# so their errors match the rest of CArray. Splice reuses the same
|
|
236
|
+
# classifier (CArray.scan_index) but needs a contiguous slice, so it takes
|
|
237
|
+
# an Integer or a step-1 BLOCK (Range, step-1 ArithmeticSequence); a
|
|
214
238
|
# strided or scattered selector raises.
|
|
215
239
|
#
|
|
216
240
|
# Delete and splice rebuild the column set, so an alias previously taken
|
|
@@ -249,6 +273,8 @@ class CAFrame
|
|
|
249
273
|
# String names a column, anything else selects rows.
|
|
250
274
|
if selector.is_a?(String)
|
|
251
275
|
column_assign(selector, value)
|
|
276
|
+
elsif selector.is_a?(Array)
|
|
277
|
+
refuse_array_key(selector, "df[] =")
|
|
252
278
|
elsif UNDEF.equal?(value)
|
|
253
279
|
mask_rows(selector)
|
|
254
280
|
elsif value.nil?
|
|
@@ -320,6 +346,8 @@ class CAFrame
|
|
|
320
346
|
# untouched.
|
|
321
347
|
#
|
|
322
348
|
# df["temp"] = col -> bind the name to that column (a new name is added)
|
|
349
|
+
# df["unit"] = "degC" -> a column holding that one value on every row,
|
|
350
|
+
# typed as a record's value is (from_records)
|
|
323
351
|
# df["temp"] = nil -> remove the column
|
|
324
352
|
# df["temp"] = UNDEF -> mask every cell of the column, in place
|
|
325
353
|
private def column_assign(key, value)
|
|
@@ -331,11 +359,27 @@ class CAFrame
|
|
|
331
359
|
raise ArgumentError,
|
|
332
360
|
"df[name] = takes a column, not a CAFrame; take one with " \
|
|
333
361
|
"other[\"name\"], or splice rows with df[rows] = other"
|
|
362
|
+
elsif one_value?(value)
|
|
363
|
+
if @columns.empty? && @index.nil?
|
|
364
|
+
raise ArgumentError,
|
|
365
|
+
"df[#{key.inspect}] = #{value.inspect}: the frame has no rows to repeat " \
|
|
366
|
+
"the value on yet; give a column (a CArray or an Array)"
|
|
367
|
+
end
|
|
368
|
+
# Typed from the one value, so a frame with no rows gets the type a
|
|
369
|
+
# frame with rows gets.
|
|
370
|
+
one = CAFrame.__send__(:build_record_column, [value], 1)
|
|
371
|
+
rebind_column(key, one[CArray.int64(@nrow).fill(0), *([nil] * (one.ndim - 1))].copy)
|
|
334
372
|
else
|
|
335
373
|
rebind_column(key, value)
|
|
336
374
|
end
|
|
337
375
|
end
|
|
338
376
|
|
|
377
|
+
# A value that is not a column: neither an Array nor anything that turns
|
|
378
|
+
# into a CArray.
|
|
379
|
+
private def one_value?(value)
|
|
380
|
+
return !value.is_a?(Array) && !value.is_a?(CArray) && !value.respond_to?(:to_ca)
|
|
381
|
+
end
|
|
382
|
+
|
|
339
383
|
# Bind +key+ to +column+, adding the name when it is new. This is where a
|
|
340
384
|
# column arriving from outside the frame has its axis-0 length checked
|
|
341
385
|
# (§12-A); the conversions in verbs.rb rebind +@columns+ directly and rely on
|
|
@@ -366,7 +410,7 @@ class CAFrame
|
|
|
366
410
|
# Remove a column. +drop+ is the same edit as a new frame (§3.8); this is
|
|
367
411
|
# the in-place form, which is all an assignment can be.
|
|
368
412
|
private def delete_column(key)
|
|
369
|
-
|
|
413
|
+
refuse_missing_column(key) unless @columns.key?(key)
|
|
370
414
|
@columns.delete(key)
|
|
371
415
|
@nrow = 0 if @columns.empty? && @index.nil?
|
|
372
416
|
self
|
|
@@ -377,56 +421,179 @@ class CAFrame
|
|
|
377
421
|
# counterpart, and like it the write goes through to the stored column and
|
|
378
422
|
# is visible through every alias.
|
|
379
423
|
private def mask_column(key)
|
|
380
|
-
col = @columns.fetch(key) {
|
|
424
|
+
col = @columns.fetch(key) { refuse_missing_column(key) }
|
|
381
425
|
col[] = UNDEF
|
|
382
426
|
self
|
|
383
427
|
end
|
|
384
428
|
|
|
429
|
+
# The names that the arguments of select / drop / transform pick: a name,
|
|
430
|
+
# a Range or Regexp expanded as stack_columns expands it, or a Symbol for
|
|
431
|
+
# the columns of a kind (columns_of_kind); none twice.
|
|
432
|
+
private def names_selected(args, verb)
|
|
433
|
+
names = args.flat_map do |a|
|
|
434
|
+
case a
|
|
435
|
+
when Range, Regexp then columns_selected(a, verb)
|
|
436
|
+
when Symbol then columns_of_kind(a, verb)
|
|
437
|
+
when Array then names_selected(a, verb)
|
|
438
|
+
else [a]
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
return column_keys(names, verb)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# The names of the columns of a kind, in the frame's order:
|
|
445
|
+
# :numeric numbers (CArray#numeric?: integers, floats, complex)
|
|
446
|
+
# :float floats of any width (CArray#float?)
|
|
447
|
+
# :integer integers of any width, signed or not (CArray#integer?)
|
|
448
|
+
# :complex complex numbers of any width (CArray#complex?)
|
|
449
|
+
# :string text: CAString, CAConstString, CAFixlenString, or an
|
|
450
|
+
# object column whose present cells are all Strings
|
|
451
|
+
# :time CATime
|
|
452
|
+
# :timedelta CATimedelta
|
|
453
|
+
# :categorical CACategorical
|
|
454
|
+
# or a data type as cast names it (:float64, :int32, :boolean, ...), which
|
|
455
|
+
# picks the columns of that type that are not a Face. :float, :integer and
|
|
456
|
+
# :complex are the kinds, not the C type names cast reads them as.
|
|
457
|
+
private def columns_of_kind(kind, verb)
|
|
458
|
+
matches =
|
|
459
|
+
case kind
|
|
460
|
+
when :numeric then ->(col) { col.numeric? }
|
|
461
|
+
when :float then ->(col) { !col.face? && col.float? }
|
|
462
|
+
when :integer then ->(col) { !col.face? && col.integer? }
|
|
463
|
+
when :complex then ->(col) { !col.face? && col.complex? }
|
|
464
|
+
when :string then ->(col) { string_kind_column?(col) }
|
|
465
|
+
when :time then ->(col) { col.is_a?(CATime) }
|
|
466
|
+
when :timedelta then ->(col) { col.is_a?(CATimedelta) }
|
|
467
|
+
when :categorical then ->(col) { col.is_a?(CACategorical) }
|
|
468
|
+
else
|
|
469
|
+
# A Symbol that is neither is a column name written as a Symbol.
|
|
470
|
+
name = begin
|
|
471
|
+
CArray.data_type_name(kind)
|
|
472
|
+
rescue ArgumentError, TypeError, CArray::DataTypeError
|
|
473
|
+
CAFrame.__send__(:column_key, kind, verb,
|
|
474
|
+
hint: "a Symbol names a kind of column (:numeric, :float, " \
|
|
475
|
+
":integer, :complex, :string, :time, :timedelta, " \
|
|
476
|
+
":categorical) or a data type")
|
|
477
|
+
end
|
|
478
|
+
->(col) { !col.face? && CArray.data_type_name(col.data_type) == name }
|
|
479
|
+
end
|
|
480
|
+
names = @columns.keys.select { |key| matches.(@columns[key]) }
|
|
481
|
+
raise ArgumentError, "#{verb}: #{kind.inspect} selects no column" if names.empty?
|
|
482
|
+
return names
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# A column of text: a string Face, or an object column whose present
|
|
486
|
+
# cells are all Strings (one with none present is not).
|
|
487
|
+
private def string_kind_column?(col)
|
|
488
|
+
return true if col.is_a?(CArray::StringOperationMixin)
|
|
489
|
+
return false if col.face? || col.data_type != :object
|
|
490
|
+
found = false
|
|
491
|
+
col.each do |v|
|
|
492
|
+
next if UNDEF.equal?(v)
|
|
493
|
+
return false unless v.is_a?(String)
|
|
494
|
+
found = true
|
|
495
|
+
end
|
|
496
|
+
return found
|
|
497
|
+
end
|
|
498
|
+
|
|
385
499
|
# Column projection (memo §13.2): a view-frame holding the named columns as
|
|
386
500
|
# aliases (zero-copy, sharing storage with the parent frame). Distinct from
|
|
387
501
|
# +df[...]+, which escapes columns to raw CArrays, and from +filter+, which
|
|
388
502
|
# selects rows. +select+ takes no block — row conditions go through +filter+
|
|
389
503
|
# (old carray-dataframe fused the two into +select(cols) { cond }+; here they
|
|
390
504
|
# are orthogonal and compose by chaining: +df.select(...).filter { ... }+).
|
|
505
|
+
# Each argument is a name, a Range of names (the columns from one to the
|
|
506
|
+
# other, in the frame's order) or a Regexp (the names it matches), as
|
|
507
|
+
# +stack_columns+ takes them.
|
|
508
|
+
#
|
|
509
|
+
# df.select("station", "tmax".."rain")
|
|
510
|
+
# df.select(/^t/)
|
|
391
511
|
def select(*names)
|
|
392
512
|
raise ArgumentError, "select requires at least one column name" if names.empty?
|
|
393
513
|
cols = {}
|
|
394
|
-
|
|
395
|
-
|
|
514
|
+
names_selected(names, "select").each do |key|
|
|
515
|
+
refuse_missing_column(key) unless @columns.key?(key)
|
|
396
516
|
cols[key] = @columns[key]
|
|
397
517
|
end
|
|
398
518
|
rebuild(cols)
|
|
399
519
|
end
|
|
400
520
|
|
|
401
|
-
#
|
|
402
|
-
#
|
|
403
|
-
#
|
|
521
|
+
# Keep the rows where a boolean selector is true. The selector is given
|
|
522
|
+
# directly (+df.filter(df["temp"] > 0)+), or built by a block that receives
|
|
523
|
+
# the frame (+df.filter { |f| f["temp"] > 0 }+), from +f["col"]+ /
|
|
524
|
+
# +f.index+. Give one or the other, not both.
|
|
404
525
|
#
|
|
405
|
-
#
|
|
406
|
-
# undetermined
|
|
407
|
-
#
|
|
408
|
-
#
|
|
409
|
-
# rows survive into the result with their data cells masked (and their index
|
|
410
|
-
# value kept, so the row stays identifiable), leaving a later, better-informed
|
|
411
|
-
# pass to re-judge them. Definitely-true rows carry their values through
|
|
412
|
-
# unchanged in both modes.
|
|
526
|
+
# Combine conditions with +&+ / +|+ (or +.and+ / +.or+, which leave a row
|
|
527
|
+
# undetermined when either side is masked) and negate with +.not+. Ruby's
|
|
528
|
+
# +&&+ / +||+ look only at whether the left array exists, so +a && b+ is +b+
|
|
529
|
+
# and the first condition is silently dropped.
|
|
413
530
|
#
|
|
414
|
-
#
|
|
415
|
-
#
|
|
416
|
-
#
|
|
417
|
-
#
|
|
418
|
-
#
|
|
419
|
-
|
|
420
|
-
|
|
531
|
+
# A masked (UNDEF) selector cell means the row's membership is undetermined
|
|
532
|
+
# (e.g. the predicate read a masked input). Such a row is not kept, as a
|
|
533
|
+
# false cell is not, as +ca[selector]+ does. Settle it in the selector to
|
|
534
|
+
# choose otherwise: +s.is_masked+ selects the undetermined rows,
|
|
535
|
+
# +s.strip_mask(true)+ keeps them along with the true ones.
|
|
536
|
+
#
|
|
537
|
+
# The result is a sub-frame of views sharing the frame's cells.
|
|
538
|
+
def filter(*selector, **options)
|
|
539
|
+
CAFrame.__send__(:refuse_filter_keywords, options, "filter")
|
|
540
|
+
if selector.size > 1
|
|
541
|
+
raise ArgumentError, "filter takes one selector (got #{selector.size})"
|
|
542
|
+
end
|
|
543
|
+
if selector.empty?
|
|
544
|
+
unless block_given?
|
|
545
|
+
raise ArgumentError, "filter needs a boolean selector or a block"
|
|
546
|
+
end
|
|
547
|
+
mask = yield(self)
|
|
548
|
+
given = "filter block must return"
|
|
549
|
+
else
|
|
550
|
+
if block_given?
|
|
551
|
+
raise ArgumentError, "filter takes a selector or a block, not both"
|
|
552
|
+
end
|
|
553
|
+
mask = selector.first
|
|
554
|
+
given = "filter selector must be"
|
|
555
|
+
end
|
|
421
556
|
unless mask.is_a?(CArray) && mask.data_type == :boolean
|
|
422
557
|
raise ArgumentError,
|
|
423
|
-
"
|
|
558
|
+
"#{given} a boolean CArray (got #{mask.is_a?(CArray) ? "a #{CArray::Inspector.type_label(mask)} CArray" : mask.class})" +
|
|
559
|
+
CAFrame.__send__(:equality_hint, mask)
|
|
424
560
|
end
|
|
425
561
|
unless mask.shape == [@nrow]
|
|
426
562
|
raise ArgumentError,
|
|
427
563
|
"filter mask has shape #{mask.shape.inspect}, expected [#{@nrow}] (one value per row)"
|
|
428
564
|
end
|
|
429
|
-
select_rows(mask
|
|
565
|
+
select_rows(mask)
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
# Keep the rows that have no masked cell in the named columns (every
|
|
569
|
+
# column when none is named), as a sub-frame of views, as +filter+ returns.
|
|
570
|
+
# A cell of an N-D column masks its row. The index is a column here only
|
|
571
|
+
# when named, by the row axis name. A NaN is a value, not a masked cell:
|
|
572
|
+
# mask it first (+df["x"][:is_invalid] = UNDEF+) to drop its row too.
|
|
573
|
+
#
|
|
574
|
+
# df.drop_masked # rows complete in every column
|
|
575
|
+
# df.drop_masked("tmax", "tmin") # rows with both temperatures
|
|
576
|
+
#
|
|
577
|
+
# @param names [Array<String, Range, Regexp, Symbol>] the columns to look
|
|
578
|
+
# at, picked as +select+ picks them.
|
|
579
|
+
# @return [CAFrame]
|
|
580
|
+
# @raise [KeyError] for a name that is no column.
|
|
581
|
+
def drop_masked (*names)
|
|
582
|
+
keys = names.empty? ? @columns.keys : names_selected(names, "drop_masked")
|
|
583
|
+
holes = CArray.boolean(@nrow).fill(false)
|
|
584
|
+
keys.each do |key|
|
|
585
|
+
col = if @columns.key?(key)
|
|
586
|
+
@columns[key]
|
|
587
|
+
elsif @index && key == @axis_name
|
|
588
|
+
@index
|
|
589
|
+
else
|
|
590
|
+
refuse_missing_column(key)
|
|
591
|
+
end
|
|
592
|
+
masked = col.is_masked
|
|
593
|
+
masked = masked.any(axis: (1...masked.ndim).to_a) if masked.ndim > 1
|
|
594
|
+
holes |= masked
|
|
595
|
+
end
|
|
596
|
+
return select_rows(holes.not)
|
|
430
597
|
end
|
|
431
598
|
|
|
432
599
|
# Judge every row against named rules, and return the verdicts as a frame:
|
|
@@ -472,7 +639,8 @@ class CAFrame
|
|
|
472
639
|
unless verdict.is_a?(CArray) && verdict.data_type == :boolean
|
|
473
640
|
raise ArgumentError,
|
|
474
641
|
"verify: rule #{key.inspect} must return a boolean CArray (got " \
|
|
475
|
-
"#{verdict.is_a?(CArray) ? "a #{verdict.data_type} CArray" : verdict.class})"
|
|
642
|
+
"#{verdict.is_a?(CArray) ? "a #{verdict.data_type} CArray" : verdict.class})" +
|
|
643
|
+
CAFrame.__send__(:equality_hint, verdict)
|
|
476
644
|
end
|
|
477
645
|
unless verdict.shape[0] == @nrow
|
|
478
646
|
raise ArgumentError,
|
|
@@ -486,13 +654,17 @@ class CAFrame
|
|
|
486
654
|
CAFrame.new(verdicts, axis_name: @axis_name, index: @index)
|
|
487
655
|
end
|
|
488
656
|
|
|
489
|
-
#
|
|
490
|
-
#
|
|
491
|
-
#
|
|
657
|
+
# A new frame with a column moved to the index and the row axis named
|
|
658
|
+
# after it (memo §3); this frame is left as it is, and the two share their
|
|
659
|
+
# columns. Any existing index is replaced, going back to being a column.
|
|
492
660
|
def set_index(name)
|
|
661
|
+
return dup.__send__(:set_index_in_place, name)
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
private def set_index_in_place(name)
|
|
493
665
|
refuse_if_frozen
|
|
494
666
|
key = column_key(name, "set_index")
|
|
495
|
-
|
|
667
|
+
refuse_missing_column(key) unless @columns.key?(key)
|
|
496
668
|
idx = @columns[key]
|
|
497
669
|
unless idx.ndim == 1
|
|
498
670
|
raise ArgumentError, "index must be a 1-D column (got ndim #{idx.ndim})"
|
|
@@ -515,10 +687,14 @@ class CAFrame
|
|
|
515
687
|
self
|
|
516
688
|
end
|
|
517
689
|
|
|
518
|
-
#
|
|
519
|
-
#
|
|
520
|
-
#
|
|
690
|
+
# A new frame with the index back as an ordinary column (the inverse of
|
|
691
|
+
# set_index); this frame is left as it is. The former index becomes the
|
|
692
|
+
# first column, named after the row axis.
|
|
521
693
|
def reset_index
|
|
694
|
+
return dup.__send__(:reset_index_in_place)
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
private def reset_index_in_place
|
|
522
698
|
refuse_if_frozen
|
|
523
699
|
return self unless @index
|
|
524
700
|
@columns = { @axis_name => @index }.merge(@columns)
|
|
@@ -599,6 +775,35 @@ class CAFrame
|
|
|
599
775
|
attrs: @attrs)
|
|
600
776
|
end
|
|
601
777
|
|
|
778
|
+
# Whether +other+ is a frame holding the same table: the same column names
|
|
779
|
+
# in the same order, each column equal as CArray#== has it (data type,
|
|
780
|
+
# shape, the present values and where the cells are masked), and the same
|
|
781
|
+
# index and row axis name. Attributes are not compared.
|
|
782
|
+
#
|
|
783
|
+
# @param other [Object]
|
|
784
|
+
# @return [Boolean]
|
|
785
|
+
def == (other)
|
|
786
|
+
return true if equal?(other)
|
|
787
|
+
return false unless other.is_a?(CAFrame)
|
|
788
|
+
return false unless column_names == other.column_names && @axis_name == other.axis_name
|
|
789
|
+
return false unless @nrow == other.nrow
|
|
790
|
+
if @index.nil? || other.index.nil?
|
|
791
|
+
return false unless @index.nil? && other.index.nil?
|
|
792
|
+
else
|
|
793
|
+
return false unless @index == other.index
|
|
794
|
+
end
|
|
795
|
+
return @columns.all? { |name, col| col.shape == other[name].shape && col == other[name] }
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
# A key value as an error message shows it: a String quoted, an element of
|
|
799
|
+
# a Face (a time, say) as it reads.
|
|
800
|
+
def self.key_text (value)
|
|
801
|
+
return value.inspect if value.is_a?(String) || value.is_a?(Numeric) || value.nil?
|
|
802
|
+
return value.to_s if value.class.name.to_s.end_with?("::Element")
|
|
803
|
+
return value.inspect
|
|
804
|
+
end
|
|
805
|
+
private_class_method :key_text
|
|
806
|
+
|
|
602
807
|
# +dup+ / +clone+ share the column data -- that is the CArray dup contract
|
|
603
808
|
# (§3.6) -- but must not share the columns Hash itself: Ruby's shallow copy
|
|
604
809
|
# hands over the same Hash object, so a membership edit (+append+ / +drop+ /
|
|
@@ -608,13 +813,14 @@ class CAFrame
|
|
|
608
813
|
super
|
|
609
814
|
@columns = @columns.dup
|
|
610
815
|
@attrs = @attrs.dup if @attrs
|
|
816
|
+
@name_views = nil
|
|
611
817
|
end
|
|
612
818
|
|
|
613
819
|
# Marshal writes the instance variables as they are. Loading stores the
|
|
614
820
|
# attribute values again as set_attr does, as frozen copies: Marshal
|
|
615
821
|
# brings back a value thawed.
|
|
616
822
|
private def marshal_dump
|
|
617
|
-
instance_variables.to_h { |name| [name, instance_variable_get(name)] }
|
|
823
|
+
(instance_variables - [:@name_views]).to_h { |name| [name, instance_variable_get(name)] }
|
|
618
824
|
end
|
|
619
825
|
|
|
620
826
|
private def marshal_load(ivars)
|
|
@@ -681,6 +887,94 @@ class CAFrame
|
|
|
681
887
|
(@attrs ? @attrs.dup : {}).freeze
|
|
682
888
|
end
|
|
683
889
|
|
|
890
|
+
# The frame's attributes with Symbol keys, frozen. The keys of Hashes
|
|
891
|
+
# nested in the values become Symbols as well.
|
|
892
|
+
def attrs_symkey
|
|
893
|
+
CArray.__send__(:attr_symbolize_keys, attrs)
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
# Set attributes on columns and return self. The columns are the ones
|
|
897
|
+
# named, or every column when none is named. With attributes, they are
|
|
898
|
+
# set on those columns; with none, each column gets its own name as the
|
|
899
|
+
# attribute "name", which is what the frame knows of a column and a column
|
|
900
|
+
# taken out of it does not:
|
|
901
|
+
#
|
|
902
|
+
# df.set_column_attrs # "name" on every column
|
|
903
|
+
# df.set_column_attrs(period: "I") # period on every column
|
|
904
|
+
# df.set_column_attrs(df.attrs) # the frame's attributes
|
|
905
|
+
# df.set_column_attrs("hr", long_name: "Hour") # long_name on "hr"
|
|
906
|
+
# df.set_column_attrs("hr", "cnt", station: "A") # station on both
|
|
907
|
+
#
|
|
908
|
+
# The attributes come as keywords, as a Hash given last, or both; an empty
|
|
909
|
+
# Hash sets nothing (df.set_column_attrs(df.attrs) on a frame with no
|
|
910
|
+
# attributes does not set "name"). Keys and values follow
|
|
911
|
+
# CArray#set_attrs. A column's other attributes are kept, and the index
|
|
912
|
+
# is not a column. A column is named once. Every name, key, value and
|
|
913
|
+
# column is checked before any is written. The attributes belong to the
|
|
914
|
+
# columns, not the table, so a frozen or protected frame takes them; a
|
|
915
|
+
# frozen column refuses them.
|
|
916
|
+
#
|
|
917
|
+
# Attributes given are set on the column arrays themselves, so frames that
|
|
918
|
+
# share a column (select, rename, append, drop, paste and dup share them)
|
|
919
|
+
# all see them, as they all see its cells. A name is not about the data
|
|
920
|
+
# but about this frame, so it is not set on the shared array: the column
|
|
921
|
+
# is replaced by a view of it (+refer+, the same data, Face and mask) that
|
|
922
|
+
# carries the name, and the array's other attributes still show through
|
|
923
|
+
# it. Each frame names its own view, whatever order frames are named in,
|
|
924
|
+
# and a frame that names nothing shows no name. Giving names replaces
|
|
925
|
+
# columns, so a frozen frame refuses it unless every column named already
|
|
926
|
+
# is this frame's own named view.
|
|
927
|
+
def set_column_attrs(*names, **keywords)
|
|
928
|
+
given = names.last.is_a?(Hash) || !keywords.empty?
|
|
929
|
+
attrs = names.last.is_a?(Hash) ? names.pop.merge(keywords) : keywords
|
|
930
|
+
targets = names.empty? ? @columns.keys : column_keys(names, "set_column_attrs")
|
|
931
|
+
columns = targets.map { |name| @columns.fetch(name) { refuse_missing_column(name, "set_column_attrs") } }
|
|
932
|
+
attrs.each do |key, value|
|
|
933
|
+
CArray.__send__(:attr_normalize_key, key)
|
|
934
|
+
CArray.__send__(:attr_validate_value, value)
|
|
935
|
+
end
|
|
936
|
+
columns.each { |col| col.__send__(:attr_check_writable) }
|
|
937
|
+
if given
|
|
938
|
+
# On the arrays the names were put in front of, so every frame
|
|
939
|
+
# sharing the data sees them.
|
|
940
|
+
columns.each { |col| CAFrame.__send__(:named_source, col).set_attrs(attrs) }
|
|
941
|
+
return self
|
|
942
|
+
end
|
|
943
|
+
own = @name_views || {}
|
|
944
|
+
reuse = targets.zip(columns).map do |name, col|
|
|
945
|
+
own.key?(col) && @columns.count { |_, c| c.equal?(col) } == 1
|
|
946
|
+
end
|
|
947
|
+
if frozen? && !reuse.all?
|
|
948
|
+
raise FrozenError.new("set_column_attrs: a name goes on a view of the column that " \
|
|
949
|
+
"replaces it in this frame, and a frozen CAFrame keeps its " \
|
|
950
|
+
"columns; name the columns before freezing, or name a copy",
|
|
951
|
+
receiver: self)
|
|
952
|
+
end
|
|
953
|
+
targets.zip(columns, reuse).each do |name, col, same|
|
|
954
|
+
view = same ? col : CAFrame.__send__(:name_view, col)
|
|
955
|
+
(@name_views ||= {}.compare_by_identity)[view] = true
|
|
956
|
+
@columns[name] = view
|
|
957
|
+
view.set_attr("name", name)
|
|
958
|
+
end
|
|
959
|
+
self
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
# A view of +col+ to carry a frame's name for it: the same data, Face and
|
|
963
|
+
# mask, with the column's other attributes showing through. It remembers
|
|
964
|
+
# the column it was made from (named_source).
|
|
965
|
+
def self.name_view(col)
|
|
966
|
+
view = col.refer
|
|
967
|
+
view.instance_variable_set(:@named_source, named_source(col))
|
|
968
|
+
view
|
|
969
|
+
end
|
|
970
|
+
private_class_method :name_view
|
|
971
|
+
|
|
972
|
+
# The array a name view was made from, or +col+ itself.
|
|
973
|
+
def self.named_source(col)
|
|
974
|
+
col.instance_variable_get(:@named_source) || col
|
|
975
|
+
end
|
|
976
|
+
private_class_method :named_source
|
|
977
|
+
|
|
684
978
|
# Whether the frame has any attribute, or the attribute +key+.
|
|
685
979
|
def has_attr?(key = nil)
|
|
686
980
|
return !(@attrs.nil? || @attrs.empty?) if key.nil?
|
|
@@ -707,6 +1001,16 @@ class CAFrame
|
|
|
707
1001
|
@columns.size
|
|
708
1002
|
end
|
|
709
1003
|
|
|
1004
|
+
# [nrow, ncol]. The index is not a column, so it is not counted.
|
|
1005
|
+
def shape
|
|
1006
|
+
return [@nrow, @columns.size]
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
# Whether the frame has no rows.
|
|
1010
|
+
def empty?
|
|
1011
|
+
return @nrow == 0
|
|
1012
|
+
end
|
|
1013
|
+
|
|
710
1014
|
# Number of rows (axis-0 length N).
|
|
711
1015
|
attr_reader :nrow
|
|
712
1016
|
|
|
@@ -750,7 +1054,9 @@ class CAFrame
|
|
|
750
1054
|
unless ca.is_a?(CArray)
|
|
751
1055
|
raise ArgumentError, "column did not convert to a CArray (got #{ca.class})"
|
|
752
1056
|
end
|
|
753
|
-
|
|
1057
|
+
# A CScalar takes no index but [] and [0]; a column is selected by rows,
|
|
1058
|
+
# so the scalar enters as the one-cell array it views.
|
|
1059
|
+
ca.scalar? ? ca.flatten : ca
|
|
754
1060
|
end
|
|
755
1061
|
|
|
756
1062
|
private def row(i)
|
|
@@ -840,13 +1146,12 @@ class CAFrame
|
|
|
840
1146
|
case pieces.size
|
|
841
1147
|
when 0 then col[CArray.int32(0), *tail] # replaced every row with none
|
|
842
1148
|
when 1 then pieces.first
|
|
843
|
-
else
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
# hide schema drift.
|
|
1149
|
+
else
|
|
1150
|
+
# A splice does not promote the column (it would hide schema
|
|
1151
|
+
# drift); cast the incoming column beforehand.
|
|
1152
|
+
CAFrame.send(:check_same_column_type, pieces, name, verb: "splice")
|
|
1153
|
+
CArray.meld(pieces, axis: 0)
|
|
1154
|
+
end
|
|
850
1155
|
splice_keep_attrs(new_cols[name], col)
|
|
851
1156
|
end
|
|
852
1157
|
new_index = splice_index(other, lo, hi)
|
|
@@ -937,14 +1242,7 @@ class CAFrame
|
|
|
937
1242
|
end
|
|
938
1243
|
end
|
|
939
1244
|
|
|
940
|
-
private def select_rows(selector
|
|
941
|
-
if keep_masked && selector.is_a?(CArray) && selector.data_type == :boolean
|
|
942
|
-
# Not gated on selector.has_mask?: gating there would make the same call
|
|
943
|
-
# site return a view-frame or a materialized one depending on whether that
|
|
944
|
-
# run's data happened to produce an undetermined cell (the reason splice
|
|
945
|
-
# always snapshots -- see CAFrame.md section 3).
|
|
946
|
-
return select_rows_keep_masked(selector)
|
|
947
|
-
end
|
|
1245
|
+
private def select_rows(selector)
|
|
948
1246
|
cols = {}
|
|
949
1247
|
@columns.each do |name, col|
|
|
950
1248
|
cols[name] = col[selector, *([nil] * (col.ndim - 1))]
|
|
@@ -953,33 +1251,32 @@ class CAFrame
|
|
|
953
1251
|
CAFrame.new(cols, axis_name: @axis_name, index: new_index, attrs: @attrs)
|
|
954
1252
|
end
|
|
955
1253
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
@
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
# mask_where copies and masks in one step, so a read-only Face
|
|
966
|
-
# (categorical, CAConstString) takes the mask as any column does.
|
|
967
|
-
cells = CArray.boolean(*g.shape)
|
|
968
|
-
cells[] = false
|
|
969
|
-
cells[undet_kept, *tail] = true
|
|
970
|
-
cols[name] = g.mask_where(cells)
|
|
971
|
-
else
|
|
972
|
-
cols[name] = g.copy
|
|
973
|
-
end
|
|
1254
|
+
# df["name"] for a name that is no column. The index is not a column, so
|
|
1255
|
+
# its name is refused too, pointing at df.index. +verb+, when given, leads
|
|
1256
|
+
# the message.
|
|
1257
|
+
private def refuse_missing_column(key, verb = nil)
|
|
1258
|
+
lead = verb ? "#{verb}: " : ""
|
|
1259
|
+
if @index && key == @axis_name
|
|
1260
|
+
raise KeyError,
|
|
1261
|
+
"#{lead}no column #{key.inspect}: it is the index, read it with df.index " \
|
|
1262
|
+
"(reset_index makes it a column again)"
|
|
974
1263
|
end
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1264
|
+
raise KeyError, "#{lead}no column #{key.inspect}"
|
|
1265
|
+
end
|
|
1266
|
+
|
|
1267
|
+
# filter takes no keyword. keep_masked:, which it once took, is named
|
|
1268
|
+
# with where its work went: the selector settles an undetermined row.
|
|
1269
|
+
def self.refuse_filter_keywords(options, verb)
|
|
1270
|
+
return if options.empty?
|
|
1271
|
+
names = options.keys.map(&:inspect).join(", ")
|
|
1272
|
+
message = "#{verb}: unknown keyword#{options.size > 1 ? "s" : ""} #{names}"
|
|
1273
|
+
if options.key?(:keep_masked)
|
|
1274
|
+
message += "; to keep the rows whose condition is undetermined, select them " \
|
|
1275
|
+
"in the selector (s.strip_mask(true))"
|
|
1276
|
+
end
|
|
1277
|
+
raise ArgumentError, message
|
|
982
1278
|
end
|
|
1279
|
+
private_class_method :refuse_filter_keywords
|
|
983
1280
|
|
|
984
1281
|
# A column name given as an argument: a String, as the frame's column
|
|
985
1282
|
# keys are. Symbols are the frame's options and types, never names.
|
|
@@ -1021,6 +1318,29 @@ class CAFrame
|
|
|
1021
1318
|
end
|
|
1022
1319
|
private_class_method :refuse_repeated_names
|
|
1023
1320
|
|
|
1321
|
+
# A condition that gave true or false rather than a boolean CArray has, as
|
|
1322
|
+
# likely as not, compared two arrays whole with == or !=.
|
|
1323
|
+
def self.equality_hint(value)
|
|
1324
|
+
return "" unless value == true || value == false
|
|
1325
|
+
"; == and != compare whole arrays, eq and ne compare cell by cell " \
|
|
1326
|
+
"(f[\"x\"].eq(\"a\"))"
|
|
1327
|
+
end
|
|
1328
|
+
private_class_method :equality_hint
|
|
1329
|
+
|
|
1330
|
+
# percentile(p, *names): the percentage comes first. A name there is the
|
|
1331
|
+
# order of median's arguments carried over, said so; a percentage out of
|
|
1332
|
+
# range is refused by the core here, before any column is asked whether
|
|
1333
|
+
# it has a percentile.
|
|
1334
|
+
def self.check_percentage(p)
|
|
1335
|
+
unless p.is_a?(Numeric)
|
|
1336
|
+
raise ArgumentError,
|
|
1337
|
+
"percentile: give the percentage first, then the column names " \
|
|
1338
|
+
"(got #{p.inspect}; percentile(90, \"x\"))"
|
|
1339
|
+
end
|
|
1340
|
+
CArray.float64(1).percentile(p)
|
|
1341
|
+
end
|
|
1342
|
+
private_class_method :check_percentage
|
|
1343
|
+
|
|
1024
1344
|
# The labels an index is made of from a key -- a group's first values,
|
|
1025
1345
|
# the resample bins, the sorted pivot keys -- as the key column shows
|
|
1026
1346
|
# them: +labels+, already of the key's data type, Face and unit, given
|