carray 3.0.4 → 3.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +438 -0
  3. data/README.md +1 -1
  4. data/carray.gemspec +3 -2
  5. data/ext/ca_array_pool.c +25 -1
  6. data/ext/ca_axis_dispatch.c +19 -20
  7. data/ext/ca_axis_group.c +1 -1
  8. data/ext/ca_categorical_iterator.c +34 -27
  9. data/ext/ca_compare.c +4 -4
  10. data/ext/ca_composite_dispatch.c +9 -9
  11. data/ext/ca_kernel_iterator.c +122 -237
  12. data/ext/ca_kernel_iterator.h +522 -337
  13. data/ext/ca_obj_array.c +69 -19
  14. data/ext/ca_obj_bincmp.c +10 -1
  15. data/ext/ca_obj_binop.c +11 -1
  16. data/ext/ca_obj_bitarray.c +5 -1
  17. data/ext/ca_obj_bitfield.c +60 -43
  18. data/ext/ca_obj_byte_swap.c +4 -1
  19. data/ext/ca_obj_const_string.c +15 -3
  20. data/ext/ca_obj_face.c +44 -1
  21. data/ext/ca_obj_fake.c +3 -1
  22. data/ext/ca_obj_field.c +13 -3
  23. data/ext/ca_obj_grid.c +219 -17
  24. data/ext/ca_obj_meld.c +69 -113
  25. data/ext/ca_obj_moncmp.c +9 -1
  26. data/ext/ca_obj_monop.c +86 -12
  27. data/ext/ca_obj_object.c +118 -32
  28. data/ext/ca_obj_record.c +3 -0
  29. data/ext/ca_obj_reduce.c +19 -1
  30. data/ext/ca_obj_refer.c +80 -26
  31. data/ext/ca_obj_remap.c +69 -10
  32. data/ext/ca_obj_roll.c +27 -6
  33. data/ext/ca_obj_select.c +38 -6
  34. data/ext/ca_obj_select_axis.c +1 -0
  35. data/ext/ca_obj_shift.c +32 -7
  36. data/ext/ca_obj_stack.c +311 -111
  37. data/ext/ca_obj_stride.c +159 -64
  38. data/ext/ca_obj_string.c +27 -2
  39. data/ext/ca_obj_tile.c +28 -7
  40. data/ext/ca_obj_transpose.c +8 -4
  41. data/ext/ca_obj_triop.c +12 -1
  42. data/ext/ca_obj_window.c +80 -50
  43. data/ext/ca_op_ipower.c +12 -12
  44. data/ext/ca_sort_kernels.h +6 -3
  45. data/ext/caframe_csv_reader.c +1 -1
  46. data/ext/carray.h +35 -1
  47. data/ext/carray_access.c +43 -67
  48. data/ext/carray_address_basis.c +134 -0
  49. data/ext/carray_attribute.c +32 -0
  50. data/ext/carray_broadcast.c +3 -4
  51. data/ext/carray_build_flags.h +1 -1
  52. data/ext/carray_cast.c +251 -93
  53. data/ext/carray_cast_func.rb +56 -26
  54. data/ext/carray_conversion.c +11 -7
  55. data/ext/carray_core.c +814 -72
  56. data/ext/carray_count.c +6 -0
  57. data/ext/carray_element.c +22 -13
  58. data/ext/carray_factorize.c +20 -20
  59. data/ext/carray_generate.c +8 -2
  60. data/ext/carray_histogram.c +26 -4
  61. data/ext/carray_index_classifier.c +4 -4
  62. data/ext/carray_internal.h +159 -12
  63. data/ext/carray_kernels_bincmp.c +2174 -170
  64. data/ext/carray_kernels_binop.c +25653 -5487
  65. data/ext/carray_kernels_init.c +8 -8
  66. data/ext/carray_kernels_moncmp.c +96 -96
  67. data/ext/carray_kernels_monop.c +12253 -5235
  68. data/ext/carray_kernels_reduce_aggregate.c +6150 -2423
  69. data/ext/carray_kernels_reduce_boolean.c +1533 -15
  70. data/ext/carray_kernels_reduce_cumulative.c +2442 -882
  71. data/ext/carray_kernels_reduce_extreme.c +5658 -1661
  72. data/ext/carray_kernels_reduce_variance.c +2534 -402
  73. data/ext/carray_kernels_scan.c +368 -368
  74. data/ext/carray_kernels_search.c +4551 -604
  75. data/ext/carray_kernels_sort.c +659 -751
  76. data/ext/carray_kernels_triop.c +114 -114
  77. data/ext/carray_key_windows.c +4 -0
  78. data/ext/carray_lazy.c +47 -0
  79. data/ext/carray_loop.c +1 -1
  80. data/ext/carray_mask.c +175 -161
  81. data/ext/carray_median_percentile.c +4 -4
  82. data/ext/carray_memory_view.c +15 -9
  83. data/ext/carray_operator.c +152 -49
  84. data/ext/carray_order.c +230 -10
  85. data/ext/carray_random.c +22 -19
  86. data/ext/carray_scatter.c +24 -7
  87. data/ext/carray_slab.c +128 -29
  88. data/ext/carray_sort.c +224 -16
  89. data/ext/carray_sort_kernel.c +5 -7
  90. data/ext/carray_struct.c +35 -43
  91. data/ext/carray_test.c +74 -19
  92. data/ext/carray_time_text.c +6 -4
  93. data/ext/carray_utils.c +63 -21
  94. data/ext/extconf.rb +11 -0
  95. data/ext/mkkernel.rb +546 -199
  96. data/ext/ruby_carray.c +4 -1
  97. data/ext/version.h +4 -4
  98. data/lib/carray/attribute.rb +42 -5
  99. data/lib/carray/autoload_carray.rb +3 -3
  100. data/lib/carray/autoload_method_extension.rb +5 -3
  101. data/lib/carray/axis_group.rb +13 -7
  102. data/lib/carray/basics.rb +28 -8
  103. data/lib/carray/block_iterator.rb +17 -3
  104. data/lib/carray/categorical.rb +35 -23
  105. data/lib/carray/categorical_iterator.rb +8 -8
  106. data/lib/carray/complex.rb +2 -2
  107. data/lib/carray/conditional.rb +3 -2
  108. data/lib/carray/const_string.rb +10 -0
  109. data/lib/carray/data_type_extension.rb +8 -1
  110. data/lib/carray/fixlen_string.rb +41 -2
  111. data/lib/carray/frame/column_verbs.rb +48 -16
  112. data/lib/carray/frame/concat.rb +17 -3
  113. data/lib/carray/frame/convert.rb +19 -0
  114. data/lib/carray/frame/csv_parser.rb +32 -32
  115. data/lib/carray/frame/describe.rb +4 -2
  116. data/lib/carray/frame/frame.rb +405 -85
  117. data/lib/carray/frame/group.rb +777 -64
  118. data/lib/carray/frame/io.rb +212 -72
  119. data/lib/carray/frame/join.rb +146 -37
  120. data/lib/carray/frame/records.rb +3 -3
  121. data/lib/carray/frame/reshape.rb +89 -11
  122. data/lib/carray/frame/sort.rb +57 -9
  123. data/lib/carray/frame/verbs.rb +384 -49
  124. data/lib/carray/frame/windows.rb +44 -6
  125. data/lib/carray/fuse_source.rb +31 -2
  126. data/lib/carray/inspect.rb +7 -2
  127. data/lib/carray/iterator.rb +32 -0
  128. data/lib/carray/lazy.rb +52 -8
  129. data/lib/carray/mask_gap_fill.rb +37 -17
  130. data/lib/carray/meld_reduce.rb +1 -1
  131. data/lib/carray/methods/choose.rb +3 -2
  132. data/lib/carray/methods/is_in.rb +8 -1
  133. data/lib/carray/methods/meshgrid.rb +3 -1
  134. data/lib/carray/runtime.rb +47 -8
  135. data/lib/carray/segment_iterator.rb +35 -16
  136. data/lib/carray/serialize.rb +53 -19
  137. data/lib/carray/stack.rb +33 -18
  138. data/lib/carray/string.rb +8 -0
  139. data/lib/carray/string_operation_extension.rb +13 -5
  140. data/lib/carray/struct_builder.rb +5 -3
  141. data/lib/carray/time.rb +112 -68
  142. data/lib/carray/window_iterator.rb +16 -4
  143. data/yard-stubs/ca_obj_array.rb +26 -1
  144. data/yard-stubs/ca_obj_transpose.rb +5 -4
  145. data/yard-stubs/carray_attribute.rb +3 -1
  146. data/yard-stubs/carray_cast.rb +10 -5
  147. data/yard-stubs/carray_conversion.rb +13 -3
  148. data/yard-stubs/carray_element.rb +7 -6
  149. data/yard-stubs/carray_mask.rb +28 -4
  150. data/yard-stubs/carray_math.rb +45 -20
  151. data/yard-stubs/carray_memory_view.rb +7 -0
  152. data/yard-stubs/carray_order.rb +100 -4
  153. data/yard-stubs/carray_scatter.rb +7 -5
  154. data/yard-stubs/carray_test.rb +7 -4
  155. metadata +18 -4
@@ -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 +from_csv+'s +types:+) asks
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
- cast(types, on_error: on_error, option_name: :types) unless types.nil?
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) { raise KeyError, "no column #{k.inspect}" } }
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) { raise KeyError, "no column #{key.inspect}" }
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, "df[symbol] is reserved for predicate keys"
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 / [start, count, step]), a boolean CArray
209
- # (SELECT), an integer CArray (GRID), or an Integer (a single row). Mask and
210
- # delete take any of them and are forwarded to the column indexer, so their
211
- # errors match the rest of CArray. Splice reuses the same classifier
212
- # (CArray.scan_index) but needs a contiguous slice, so it takes an Integer or
213
- # a step-1 BLOCK (Range, step-1 ArithmeticSequence, or [start, count]); a
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
- raise KeyError, "no column #{key.inspect}" unless @columns.key?(key)
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) { raise KeyError, "no column #{key.inspect}" }
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
- column_keys(names, "select").each do |key|
395
- raise KeyError, "no column #{key.inspect}" unless @columns.key?(key)
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
- # Filter rows with a block that receives the frame and returns a boolean
402
- # column (memo §15). The block builds the mask from +f["col"]+ / +f.index+.
403
- # Keep the rows where the block's boolean selector is true.
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
- # A masked (UNDEF) selector cell means the row's membership is *genuinely
406
- # undetermined* (e.g. the predicate read a masked input). By default such
407
- # rows are silently dropped, exactly as a false cell would be. Pass
408
- # +keep_masked: true+ to carry the UNDEF forward instead: the undetermined
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
- # The default is a view-frame. +keep_masked: true+ returns a **materialized**
415
- # frame instead -- carrying the UNDEF forward means writing it into the
416
- # result, which a view cannot do without masking the parent's rows. That holds
417
- # whether or not the selector actually carries a masked cell, so the same call
418
- # site does not switch between sharing and copying with the data.
419
- def filter(keep_masked: false)
420
- mask = yield(self)
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
- "filter block must return a boolean CArray (got #{mask.is_a?(CArray) ? "a #{CArray::Inspector.type_label(mask)} CArray" : mask.class})"
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, keep_masked: keep_masked)
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
- # Move a column to the index and name the row axis after it (memo §3).
490
- # An index-role change: the data is unchanged, so this mutates self and
491
- # returns it (memo §3.8). Any existing index is replaced.
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
- raise KeyError, "no column #{key.inspect}" unless @columns.key?(key)
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
- # Drop the index back into an ordinary column (the inverse of set_index).
519
- # Also an index-role change, so it mutates self (memo §3.8). The former
520
- # index becomes the first column, named after the row axis.
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
- ca
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 CArray.meld(pieces, axis: 0) # CAMeld view; data type mismatch
844
- end # across pieces raises.
845
- # For data type conversion, cast
846
- # the incoming +other+'s
847
- # column beforehand — silent
848
- # promotion in a splice would
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, keep_masked: false)
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
- private def select_rows_keep_masked(selector)
957
- keep = selector.strip_mask(1) # true where selected OR undetermined
958
- undet_kept = selector.is_masked[keep] # among kept rows, which were undetermined
959
- cols = {}
960
- any_undet = undet_kept.any
961
- @columns.each do |name, col|
962
- tail = [nil] * (col.ndim - 1)
963
- g = col[keep, *tail]
964
- if any_undet
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
- # The index is copied along with the columns. Its values are carried over
976
- # unmasked (an undetermined row keeps its label, as mask_rows leaves the
977
- # index alone), but a materialized frame has to be materialized whole: an
978
- # aliased index would write through to the parent from a frame whose columns
979
- # do not.
980
- new_index = @index ? @index[keep].copy : nil
981
- CAFrame.new(cols, axis_name: @axis_name, index: new_index, attrs: @attrs)
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