carray 3.0.2 → 3.0.3

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 (218) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +304 -0
  3. data/README.md +3 -4
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +225 -0
  6. data/ext/ca_axis_group.c +177 -99
  7. data/ext/ca_binop_dispatch.c +54 -32
  8. data/ext/ca_binop_dispatch.h +1 -0
  9. data/ext/ca_categorical_iterator.c +66 -71
  10. data/ext/ca_compare.c +8 -2
  11. data/ext/ca_for_buffer.h +21 -68
  12. data/ext/ca_group_iter.c +27 -11
  13. data/ext/ca_kernel_iterator.c +663 -214
  14. data/ext/ca_kernel_iterator.h +231 -167
  15. data/ext/ca_monop_dispatch.c +4 -4
  16. data/ext/ca_monop_dispatch.h +4 -0
  17. data/ext/ca_obj_array.c +132 -22
  18. data/ext/ca_obj_bincmp.c +152 -121
  19. data/ext/ca_obj_binop.c +107 -210
  20. data/ext/ca_obj_bitarray.c +37 -24
  21. data/ext/ca_obj_bitfield.c +95 -62
  22. data/ext/ca_obj_block.c +1 -0
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +96 -25
  25. data/ext/ca_obj_face.c +406 -64
  26. data/ext/ca_obj_face.h +92 -47
  27. data/ext/ca_obj_fake.c +21 -50
  28. data/ext/ca_obj_farray.c +1 -0
  29. data/ext/ca_obj_field.c +16 -3
  30. data/ext/ca_obj_fixlen_string.c +21 -21
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +189 -107
  33. data/ext/ca_obj_moncmp.c +61 -61
  34. data/ext/ca_obj_monop.c +125 -107
  35. data/ext/ca_obj_object.c +543 -233
  36. data/ext/ca_obj_record.c +51 -16
  37. data/ext/ca_obj_reduce.c +5 -8
  38. data/ext/ca_obj_refer.c +117 -33
  39. data/ext/ca_obj_remap.c +57 -17
  40. data/ext/ca_obj_repeat.c +21 -5
  41. data/ext/ca_obj_roll.c +115 -11
  42. data/ext/ca_obj_select.c +47 -49
  43. data/ext/ca_obj_select_axis.c +58 -60
  44. data/ext/ca_obj_shift.c +22 -102
  45. data/ext/ca_obj_stack.c +223 -118
  46. data/ext/ca_obj_stride.c +195 -34
  47. data/ext/ca_obj_string.c +17 -18
  48. data/ext/ca_obj_tile.c +32 -16
  49. data/ext/ca_obj_time.c +23 -25
  50. data/ext/ca_obj_timedelta.c +21 -23
  51. data/ext/ca_obj_transpose.c +12 -3
  52. data/ext/ca_obj_triop.c +110 -117
  53. data/ext/ca_obj_window.c +149 -27
  54. data/ext/ca_op_ipower.c +24 -11
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_sort_kernels.h +70 -0
  58. data/ext/ca_sweep_engine.c +102 -79
  59. data/ext/ca_sweep_engine.h +7 -20
  60. data/ext/ca_triop_dispatch.c +4 -1
  61. data/ext/ca_triop_dispatch.h +5 -2
  62. data/ext/caframe_csv_reader.c +463 -0
  63. data/ext/carray.h +70 -27
  64. data/ext/carray_access.c +455 -211
  65. data/ext/carray_address_basis.c +177 -36
  66. data/ext/carray_attribute.c +45 -3
  67. data/ext/carray_bincount.c +32 -2
  68. data/ext/carray_broadcast.c +46 -14
  69. data/ext/carray_call_cfunc.c +0 -28
  70. data/ext/carray_cast.c +1444 -209
  71. data/ext/carray_cast_func.rb +34 -21
  72. data/ext/carray_conversion.c +197 -73
  73. data/ext/carray_copy.c +32 -11
  74. data/ext/carray_core.c +514 -122
  75. data/ext/carray_count.c +140 -87
  76. data/ext/carray_element.c +18 -27
  77. data/ext/carray_factorize.c +569 -277
  78. data/ext/carray_generate.c +76 -40
  79. data/ext/carray_histogram.c +152 -94
  80. data/ext/carray_hold.c +2 -1
  81. data/ext/carray_index_classifier.c +63 -96
  82. data/ext/carray_internal.h +385 -3
  83. data/ext/carray_lazy.c +834 -79
  84. data/ext/carray_loop.c +81 -65
  85. data/ext/carray_mask.c +682 -316
  86. data/ext/carray_math_kernel.h +1 -0
  87. data/ext/carray_median_percentile.c +221 -76
  88. data/ext/carray_memory_view.c +254 -248
  89. data/ext/carray_operator.c +1089 -359
  90. data/ext/carray_order.c +133 -69
  91. data/ext/carray_partition.c +102 -56
  92. data/ext/carray_random.c +273 -239
  93. data/ext/carray_scatter.c +308 -65
  94. data/ext/carray_segment.c +257 -0
  95. data/ext/carray_slab.c +170 -70
  96. data/ext/carray_sort.c +215 -218
  97. data/ext/carray_sort_kernel.c +8 -1
  98. data/ext/carray_struct.c +99 -21
  99. data/ext/carray_test.c +167 -64
  100. data/ext/carray_time_text.c +1182 -0
  101. data/ext/carray_utils.c +363 -98
  102. data/ext/extconf.rb +27 -2
  103. data/ext/mk_call_cfunc.rb +0 -2
  104. data/ext/mk_pow5_table.rb +35 -0
  105. data/ext/mkkernel.rb +1076 -846
  106. data/ext/ruby_carray.c +7 -1
  107. data/ext/version.h +4 -4
  108. data/ext/version.rb +4 -2
  109. data/lib/carray/attribute.rb +39 -0
  110. data/lib/carray/autoload_carray.rb +19 -0
  111. data/lib/carray/autoload_method_extension.rb +6 -0
  112. data/lib/carray/axis_group.rb +174 -26
  113. data/lib/carray/basics.rb +91 -25
  114. data/lib/carray/bincount_nd.rb +6 -0
  115. data/lib/carray/block_iterator.rb +118 -48
  116. data/lib/carray/boolean_reduce.rb +21 -29
  117. data/lib/carray/categorical.rb +94 -26
  118. data/lib/carray/categorical_iterator.rb +333 -826
  119. data/lib/carray/complex.rb +36 -29
  120. data/lib/carray/conditional.rb +94 -7
  121. data/lib/carray/const_string.rb +46 -0
  122. data/lib/carray/frame/concat.rb +157 -6
  123. data/lib/carray/frame/convert.rb +13 -7
  124. data/lib/carray/frame/csv_parser.rb +589 -82
  125. data/lib/carray/frame/describe.rb +79 -0
  126. data/lib/carray/frame/frame.rb +124 -25
  127. data/lib/carray/frame/group.rb +189 -63
  128. data/lib/carray/frame/io.rb +412 -72
  129. data/lib/carray/frame/join.rb +134 -46
  130. data/lib/carray/frame/records.rb +37 -16
  131. data/lib/carray/frame/reshape.rb +448 -0
  132. data/lib/carray/frame/sort.rb +26 -2
  133. data/lib/carray/frame/verbs.rb +715 -48
  134. data/lib/carray/frame.rb +2 -0
  135. data/lib/carray/fuse_source.rb +111 -2
  136. data/lib/carray/fusion.rb +229 -38
  137. data/lib/carray/inspect.rb +29 -11
  138. data/lib/carray/iterator.rb +14 -2
  139. data/lib/carray/lazy.rb +54 -30
  140. data/lib/carray/mask_gap_fill.rb +66 -52
  141. data/lib/carray/math.rb +32 -19
  142. data/lib/carray/meld_reduce.rb +40 -7
  143. data/lib/carray/methods/align_addr.rb +8 -4
  144. data/lib/carray/methods/bincount.rb +2 -2
  145. data/lib/carray/methods/bit_string.rb +14 -3
  146. data/lib/carray/methods/composition.rb +16 -10
  147. data/lib/carray/methods/factorize.rb +2 -2
  148. data/lib/carray/methods/gather_nd.rb +13 -2
  149. data/lib/carray/methods/insert_block.rb +9 -14
  150. data/lib/carray/methods/is_in.rb +117 -4
  151. data/lib/carray/methods/join.rb +1 -1
  152. data/lib/carray/methods/locate_addr.rb +177 -36
  153. data/lib/carray/methods/mode.rb +18 -1
  154. data/lib/carray/methods/repeat.rb +1 -20
  155. data/lib/carray/methods/resize.rb +21 -23
  156. data/lib/carray/methods/snap.rb +34 -12
  157. data/lib/carray/methods/unique.rb +11 -2
  158. data/lib/carray/record_order.rb +208 -0
  159. data/lib/carray/runtime.rb +14 -11
  160. data/lib/carray/segment_iterator.rb +990 -0
  161. data/lib/carray/serialize.rb +197 -27
  162. data/lib/carray/slab_iterator.rb +51 -27
  163. data/lib/carray/stack.rb +10 -4
  164. data/lib/carray/string_operation_extension.rb +4 -3
  165. data/lib/carray/struct.rb +142 -51
  166. data/lib/carray/struct_builder.rb +110 -14
  167. data/lib/carray/table.rb +2 -2
  168. data/lib/carray/time.rb +517 -103
  169. data/lib/carray/window_iterator.rb +127 -72
  170. data/lib/carray.rb +2 -2
  171. data/yard-stubs/ca_obj_array.rb +4 -4
  172. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  173. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  174. data/yard-stubs/ca_obj_fake.rb +1 -1
  175. data/yard-stubs/ca_obj_field.rb +1 -1
  176. data/yard-stubs/ca_obj_refer.rb +3 -3
  177. data/yard-stubs/ca_obj_roll.rb +0 -1
  178. data/yard-stubs/ca_obj_stride.rb +10 -7
  179. data/yard-stubs/ca_obj_transpose.rb +1 -1
  180. data/yard-stubs/ca_obj_window.rb +1 -2
  181. data/yard-stubs/carray_access.rb +5 -4
  182. data/yard-stubs/carray_broadcast.rb +1 -1
  183. data/yard-stubs/carray_cast.rb +78 -43
  184. data/yard-stubs/carray_conversion.rb +13 -1
  185. data/yard-stubs/carray_copy.rb +8 -3
  186. data/yard-stubs/carray_core.rb +3 -3
  187. data/yard-stubs/carray_count.rb +9 -0
  188. data/yard-stubs/carray_lazy.rb +5 -1
  189. data/yard-stubs/carray_loop.rb +4 -1
  190. data/yard-stubs/carray_mask.rb +34 -10
  191. data/yard-stubs/carray_math.rb +25 -13
  192. data/yard-stubs/carray_median_percentile.rb +10 -6
  193. data/yard-stubs/carray_order.rb +34 -13
  194. data/yard-stubs/carray_scatter.rb +10 -3
  195. data/yard-stubs/carray_segment.rb +72 -0
  196. data/yard-stubs/carray_slab.rb +2 -1
  197. data/yard-stubs/carray_sort.rb +43 -3
  198. data/yard-stubs/carray_test.rb +10 -0
  199. data/yard-stubs/carray_utils.rb +0 -26
  200. metadata +11 -19
  201. data/ext/ca_for_each_element.h +0 -239
  202. data/ext/carray_build_flags.h +0 -3
  203. data/ext/carray_config.h +0 -28
  204. data/ext/carray_kernels_bincmp.c +0 -4446
  205. data/ext/carray_kernels_binop.c +0 -11001
  206. data/ext/carray_kernels_init.c +0 -1131
  207. data/ext/carray_kernels_map.c +0 -3467
  208. data/ext/carray_kernels_moncmp.c +0 -2097
  209. data/ext/carray_kernels_monop.c +0 -18313
  210. data/ext/carray_kernels_reduce_aggregate.c +0 -26005
  211. data/ext/carray_kernels_reduce_boolean.c +0 -330
  212. data/ext/carray_kernels_reduce_cumulative.c +0 -14862
  213. data/ext/carray_kernels_reduce_extreme.c +0 -17494
  214. data/ext/carray_kernels_reduce_variance.c +0 -3910
  215. data/ext/carray_kernels_scan.c +0 -3693
  216. data/ext/carray_kernels_search.c +0 -32218
  217. data/ext/carray_kernels_sort.c +0 -10626
  218. data/ext/carray_kernels_triop.c +0 -1392
@@ -0,0 +1,79 @@
1
+ # CAFrame#describe: one row of statistics per column, for a first look at a
2
+ # table that has just been read.
3
+
4
+ class CAFrame
5
+ # Summarize the columns, one row each, as a new frame indexed by column
6
+ # name:
7
+ #
8
+ # puts df.describe.to_table
9
+ #
10
+ # column type count masked unique min max mean stddev
11
+ # ------- ------- ----- ------ ------ ---------- ---------- ---------- --------
12
+ # station object 3 0 2 _ _ _ _
13
+ # temp float64 2 1 2 19.0 22.1 20.55 2.192031
14
+ # time CATime 3 0 3 2024-01-01 2024-01-03 2024-01-02 1D
15
+ #
16
+ # +type+ is the data type, or the Face's class for a Face column, with the
17
+ # trailing shape of an N-D column in brackets (+float64[3]+). +count+ and
18
+ # +masked+ count cells, so an N-D column counts every cell of every row;
19
+ # +unique+ is the number of distinct values among the present cells.
20
+ #
21
+ # The other statistics apply only where the column's kind gives them a
22
+ # meaning, and are UNDEF elsewhere:
23
+ #
24
+ # real numbers, boolean min max mean stddev
25
+ # complex mean stddev (no order)
26
+ # CATime, CATimedelta min max mean stddev (as times / durations)
27
+ # anything else (text, categorical, records ...) unique only
28
+ #
29
+ # A boolean column counts true as 1, so its mean is the share of trues. A
30
+ # column with no present cell has UNDEF statistics. The index is not a
31
+ # column and is not summarized.
32
+ #
33
+ # @param names [Array<String>] the columns to summarize; default all.
34
+ # @return [CAFrame] a new frame with one row per column.
35
+ def describe(*names)
36
+ names = names.empty? ? column_names : names.map(&:to_s)
37
+ rows = names.map { |name| describe_column(self[name]) }
38
+ stats = %w[type count masked unique min max mean stddev]
39
+ cols = stats.to_h do |stat|
40
+ values = rows.map { |row| row[stat] }
41
+ column =
42
+ case stat
43
+ when "count", "masked", "unique" then CArray.int64(values.size)
44
+ else CArray.object(values.size)
45
+ end
46
+ column[] = UNDEF
47
+ values.each_with_index { |v, i| column[i] = v unless UNDEF.equal?(v) }
48
+ [stat, column]
49
+ end
50
+ CAFrame.new(cols, axis_name: "column", index: CA_OBJECT(names))
51
+ end
52
+
53
+ DESCRIBE_TIME_FACES = %w[CATime CATimedelta].freeze
54
+ private_constant :DESCRIBE_TIME_FACES
55
+
56
+ private def describe_column(col)
57
+ kind =
58
+ if col.face?
59
+ DESCRIBE_TIME_FACES.include?(col.class.name) ? :ordered : :other
60
+ elsif col.complex?
61
+ :complex
62
+ elsif col.numeric? || col.boolean?
63
+ :ordered
64
+ else
65
+ :other
66
+ end
67
+ type = col.face? ? col.class.name : col.data_type_name
68
+ type += col.shape[1..].inspect if col.ndim > 1
69
+ row = { "type" => type,
70
+ "count" => col.count_not_masked, "masked" => col.count_masked,
71
+ "unique" => col.nunique }
72
+ ordered = kind == :ordered
73
+ row["min"] = ordered ? col.min : UNDEF
74
+ row["max"] = ordered ? col.max : UNDEF
75
+ row["mean"] = kind == :other ? UNDEF : col.mean
76
+ row["stddev"] = kind == :other ? UNDEF : col.stddev
77
+ row
78
+ end
79
+ end
@@ -11,6 +11,29 @@
11
11
  # storage, and +copy+ is the way to an independent frame.
12
12
 
13
13
  class CAFrame
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
16
+ # for under on_error: :raise, a cell +parse_to_time+ cannot read, a column
17
+ # whose time format +infer_time_format+ / parse_to_time(name, :infer)
18
+ # cannot find, or a float column +to_time+ cannot read as whole counts.
19
+ # The message names the column and the cell. The arguments were fine; the
20
+ # column's contents are what did not read.
21
+ class UnreadableColumn < StandardError; end
22
+
23
+ # Raised when +infer_time_format+ / parse_to_time(name, :infer) finds more
24
+ # than one format every cell fits ("01/02/2024" and "03/04/2024" are both
25
+ # day-first and month-first). The text reads in each of them, so it is
26
+ # not an UnreadableColumn; +formats+ gives the candidates, to choose one and
27
+ # pass it as the format.
28
+ class AmbiguousTimeFormat < StandardError
29
+ attr_reader :formats
30
+
31
+ def initialize(message = nil, formats: [])
32
+ @formats = formats
33
+ super(message)
34
+ end
35
+ end
36
+
14
37
  # Integer data_types that select rows positionally when used as a df[] key.
15
38
  INTEGER_TYPES = [:int8, :int16, :int32, :int64,
16
39
  :uint8, :uint16, :uint32, :uint64].freeze
@@ -112,7 +135,7 @@ class CAFrame
112
135
  if keys.size > 1
113
136
  unless keys.all? { |k| k.is_a?(String) }
114
137
  raise ArgumentError,
115
- "multi-key df[...] escapes columns; every key must be a String " \
138
+ "df[...] with several keys returns columns; every key must be a String " \
116
139
  "(use df.select(...) for a subset frame)"
117
140
  end
118
141
  return keys.map { |k| @columns.fetch(k) { raise KeyError, "no column #{k.inspect}" } }
@@ -142,7 +165,7 @@ class CAFrame
142
165
  "CArray df[] key must be boolean or integer (got #{key.data_type})"
143
166
  end
144
167
  when Symbol
145
- raise NotImplementedError, "df[symbol] is reserved for predicate keys"
168
+ raise ArgumentError, "df[symbol] is reserved for predicate keys"
146
169
  else
147
170
  raise ArgumentError, "unsupported df[] key: #{key.class}"
148
171
  end
@@ -197,6 +220,7 @@ class CAFrame
197
220
  # write path always propagates to derived views (unchanged from
198
221
  # previous behaviour).
199
222
  def []=(*keys, value)
223
+ refuse_if_frozen
200
224
  if keys.size != 1
201
225
  raise ArgumentError,
202
226
  "df[...] = takes one key (a column name or a row selector); " \
@@ -222,6 +246,52 @@ class CAFrame
222
246
  value
223
247
  end
224
248
 
249
+ # Freezing a frame fixes its columns, rows and index; the columns' cells are
250
+ # their own arrays' to guard, as an Array's elements are. The check comes
251
+ # first so a refused edit leaves the frame as it was: the edits change the
252
+ # column table before any instance variable.
253
+ private def refuse_if_frozen
254
+ raise FrozenError.new("can't modify frozen CAFrame: #{inspect}", receiver: self) if frozen?
255
+ end
256
+
257
+ # Make this frame read-only and return it. It is +freeze+ extended to the
258
+ # cells: the column set, the rows and the index are fixed as by +freeze+,
259
+ # and the cells and masks of every column and of the index refuse writes
260
+ # made through this frame.
261
+ #
262
+ # Each column (and the index) is replaced by a read-only view of itself, so
263
+ # the arrays the frame was built from are left as they were: whoever holds
264
+ # them can still write, and the writes show through this frame. Frames
265
+ # derived from a protected frame by views (+select+, +filter+, a row slice)
266
+ # read through those views and refuse cell writes too, but are not frozen.
267
+ # +copy+ gives a frame that can be written again.
268
+ #
269
+ # Calling it on a protected frame returns the frame unchanged. A frame
270
+ # frozen with +freeze+ cannot be protected afterwards, because its columns
271
+ # can no longer be replaced: raises +FrozenError+.
272
+ def protect
273
+ return self if protected?
274
+ refuse_if_frozen
275
+ @columns = @columns.transform_values { |col| read_only_view(col) }
276
+ @index = read_only_view(@index) if @index
277
+ @protected = true
278
+ freeze
279
+ end
280
+
281
+ # True if this frame was made read-only by +protect+. A +dup+ is not frozen
282
+ # and so not protected, though its columns stay the read-only views it
283
+ # shares; a +clone+ keeps both.
284
+ def protected?
285
+ frozen? && @protected == true
286
+ end
287
+
288
+ private def read_only_view(col)
289
+ return col if col.read_only?
290
+ view = col.refer
291
+ view.set_read_only_flag
292
+ view
293
+ end
294
+
225
295
  # df["name"] = ... : the column forms of []=. Unlike the verbs, +[]=+ can
226
296
  # only ever mutate the receiver -- Ruby hands the right-hand side back as
227
297
  # the value of an assignment, so there is no way to return a new frame.
@@ -238,7 +308,7 @@ class CAFrame
238
308
  mask_column(key)
239
309
  elsif value.is_a?(CAFrame)
240
310
  raise ArgumentError,
241
- "df[name] = takes a column, not a CAFrame; escape one with " \
311
+ "df[name] = takes a column, not a CAFrame; take one with " \
242
312
  "other[\"name\"], or splice rows with df[rows] = other"
243
313
  else
244
314
  rebind_column(key, value)
@@ -331,9 +401,9 @@ class CAFrame
331
401
  raise ArgumentError,
332
402
  "filter block must return a boolean CArray (got #{mask.class})"
333
403
  end
334
- unless mask.shape[0] == @nrow
404
+ unless mask.shape == [@nrow]
335
405
  raise ArgumentError,
336
- "filter mask has axis-0 length #{mask.shape[0]}, expected nrow #{@nrow}"
406
+ "filter mask has shape #{mask.shape.inspect}, expected [#{@nrow}] (one value per row)"
337
407
  end
338
408
  select_rows(mask, keep_masked: keep_masked)
339
409
  end
@@ -342,6 +412,7 @@ class CAFrame
342
412
  # An index-role change: the data is unchanged, so this mutates self and
343
413
  # returns it (memo §3.8). Any existing index is replaced.
344
414
  def set_index(name)
415
+ refuse_if_frozen
345
416
  key = name.to_s
346
417
  raise KeyError, "no column #{key.inspect}" unless @columns.key?(key)
347
418
  idx = @columns[key]
@@ -370,6 +441,7 @@ class CAFrame
370
441
  # Also an index-role change, so it mutates self (memo §3.8). The former
371
442
  # index becomes the first column, named after the row axis.
372
443
  def reset_index
444
+ refuse_if_frozen
373
445
  return self unless @index
374
446
  @columns = { @axis_name => @index }.merge(@columns)
375
447
  @index = nil
@@ -394,7 +466,11 @@ class CAFrame
394
466
 
395
467
  # The single row whose index label equals +label+, as a Ruby Hash (memo
396
468
  # §13.2b). +label+ is matched exactly against the index (+index.eq+), so any
397
- # orderable / object / datetime / categorical index works. The return type is
469
+ # orderable / object / datetime / categorical index works, and +label+ is
470
+ # whatever +index.eq+ takes -- for a time index, a Time, Date or a String
471
+ # such as "2024-01-02". A NaN label finds the NaN label, as join and
472
+ # group_by match NaN keys. A numeric index takes a number; a String or
473
+ # true / false raises. The return type is
398
474
  # a row Hash and stays that way: zero matches raise KeyError, and duplicate
399
475
  # labels raise (go through +filter { |f| f.index.eq(label) }+ for the
400
476
  # multi-row, frame-returning path). Positional access is +df[i]+.
@@ -411,16 +487,29 @@ class CAFrame
411
487
  "at(UNDEF): an undefined label identifies no row; " \
412
488
  "use filter { |f| f.index.is_masked } for the rows with no label"
413
489
  end
414
- pos = @index.eq(label).where
490
+ # A NaN label is the NaN value, as join and group_by take it: eq answers
491
+ # false for NaN against anything, itself included.
492
+ # A numeric index is looked up with a number: a String or true/false would
493
+ # be read differently by each numeric type, or not at all.
494
+ if !@index.face? && (@index.integer? || @index.float? || @index.complex?) &&
495
+ !label.is_a?(Numeric)
496
+ raise ArgumentError,
497
+ "at: the index is #{@index.data_type}, so the label must be a number " \
498
+ "(got #{label.inspect}); convert it first, e.g. at(Float(s)) or at(Integer(s))"
499
+ end
500
+ nan = label.is_a?(Float) && label.nan?
501
+ hit = nan ? @index.is_in([label]) : @index.eq(label)
502
+ pos = hit.where
415
503
  case pos.elements
416
504
  when 0
417
505
  raise KeyError, "no row with index label #{label.inspect}"
418
506
  when 1
419
507
  row(pos[0])
420
508
  else
509
+ test = nan ? "f.index.is_nan" : "f.index.eq(label)"
421
510
  raise ArgumentError,
422
511
  "index label #{label.inspect} matches #{pos.elements} rows; " \
423
- "use filter { |f| f.index.eq(label) } for duplicate labels"
512
+ "use filter { |f| #{test} } for duplicate labels"
424
513
  end
425
514
  end
426
515
 
@@ -444,20 +533,20 @@ class CAFrame
444
533
  # --- metadata readers (memo §13.2) ------------------------------------
445
534
  # Each returns a fresh object; the live columns Hash is never exposed.
446
535
 
447
- # Variable (column) names in column order. Returns +Array<String>+.
448
- def variable_names
536
+ # Column names in column order. Returns +Array<String>+.
537
+ def column_names
449
538
  @columns.keys
450
539
  end
451
540
 
452
- # Variables (raw column CArrays) in column order. Returns
453
- # +Array<CArray>+. Equivalent to +df[*variable_names]+ but built
454
- # directly from the columns Hash.
455
- def variables
541
+ # The columns (raw CArrays) in column order. Returns +Array<CArray>+.
542
+ # Equivalent to +df[*column_names]+ but built directly from the columns
543
+ # Hash.
544
+ def columns
456
545
  @columns.values
457
546
  end
458
547
 
459
- # Number of variables (columns).
460
- def nvar
548
+ # Number of columns.
549
+ def ncol
461
550
  @columns.size
462
551
  end
463
552
 
@@ -485,7 +574,7 @@ class CAFrame
485
574
  if col.is_a?(CAFrame)
486
575
  raise ArgumentError,
487
576
  "a CAFrame cannot be a column (it answers to_ca as a 2-D matrix); " \
488
- "escape a column with df[\"name\"] or df.to_ca first"
577
+ "take a column with df[\"name\"] or df.to_ca first"
489
578
  end
490
579
  unless col.respond_to?(:to_ca)
491
580
  raise ArgumentError, "column must be a CArray or Array (got #{col.class})"
@@ -498,10 +587,11 @@ class CAFrame
498
587
  end
499
588
 
500
589
  private def row(i)
501
- i += @nrow if i < 0
502
- unless i >= 0 && i < @nrow
590
+ pos = i < 0 ? i + @nrow : i
591
+ unless pos >= 0 && pos < @nrow
503
592
  raise IndexError, "row index #{i} out of range (nrow=#{@nrow})"
504
593
  end
594
+ i = pos
505
595
  h = {}
506
596
  h[@axis_name] = elem_at(@index, i) if @index
507
597
  @columns.each { |name, col| h[name] = elem_at(col, i) }
@@ -562,10 +652,10 @@ class CAFrame
562
652
  # promote and the row count shifts by other.nrow - span.
563
653
  private def splice_rows(selector, other)
564
654
  lo, hi = contiguous_span(selector)
565
- unless other.variable_names.sort == variable_names.sort
655
+ unless other.column_names.sort == column_names.sort
566
656
  raise ArgumentError,
567
- "splice frame has columns #{other.variable_names.inspect}, " \
568
- "expected the same set as #{variable_names.inspect}"
657
+ "splice frame has columns #{other.column_names.inspect}, " \
658
+ "expected the same set as #{column_names.inspect}"
569
659
  end
570
660
  new_cols = {}
571
661
  @columns.each do |name, col|
@@ -691,11 +781,20 @@ class CAFrame
691
781
  keep = selector.strip_mask(1) # true where selected OR undetermined
692
782
  undet_kept = selector.is_masked[keep] # among kept rows, which were undetermined
693
783
  cols = {}
784
+ any_undet = undet_kept.any
694
785
  @columns.each do |name, col|
695
786
  tail = [nil] * (col.ndim - 1)
696
- g = col[keep, *tail].copy
697
- g[undet_kept, *tail] = UNDEF
698
- cols[name] = g
787
+ g = col[keep, *tail]
788
+ if any_undet
789
+ # mask_where copies and masks in one step, so a read-only Face
790
+ # (categorical, CAConstString) takes the mask as any column does.
791
+ cells = CArray.boolean(*g.shape)
792
+ cells[] = false
793
+ cells[undet_kept, *tail] = true
794
+ cols[name] = g.mask_where(cells)
795
+ else
796
+ cols[name] = g.copy
797
+ end
699
798
  end
700
799
  # The index is copied along with the columns. Its values are carried over
701
800
  # unmasked (an undetermined row keeps its label, as mask_rows leaves the