carray 3.0.0 → 3.0.2

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 (202) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -1
  3. data/CHANGELOG.md +845 -3
  4. data/{NEWS.md → CHANGELOG.v1.md} +1 -1
  5. data/README.md +33 -33
  6. data/carray.gemspec +12 -8
  7. data/ext/ca_axis_dispatch.c +33 -4
  8. data/ext/ca_axis_group.c +256 -114
  9. data/ext/ca_binop_dispatch.c +5 -6
  10. data/ext/ca_binop_dispatch.h +0 -7
  11. data/ext/ca_categorical_iterator.c +121 -67
  12. data/ext/ca_for_each_element.h +6 -8
  13. data/ext/ca_group_iter.c +1 -1
  14. data/ext/ca_kernel_iterator.c +386 -74
  15. data/ext/ca_kernel_iterator.h +145 -45
  16. data/ext/ca_obj_array.c +62 -20
  17. data/ext/ca_obj_bincmp.c +57 -27
  18. data/ext/ca_obj_binop.c +86 -30
  19. data/ext/ca_obj_block.c +4 -4
  20. data/ext/ca_obj_const_string.c +88 -27
  21. data/ext/ca_obj_face.c +104 -0
  22. data/ext/ca_obj_face.h +48 -1
  23. data/ext/ca_obj_fake.c +11 -0
  24. data/ext/ca_obj_fixlen_string.c +21 -6
  25. data/ext/ca_obj_grid.c +11 -3
  26. data/ext/ca_obj_meld.c +125 -22
  27. data/ext/ca_obj_moncmp.c +33 -14
  28. data/ext/ca_obj_monop.c +19 -7
  29. data/ext/ca_obj_object.c +107 -4
  30. data/ext/ca_obj_record.c +3 -1
  31. data/ext/ca_obj_refer.c +30 -6
  32. data/ext/ca_obj_roll.c +13 -5
  33. data/ext/ca_obj_select.c +49 -34
  34. data/ext/ca_obj_select_axis.c +16 -6
  35. data/ext/ca_obj_shift.c +3 -3
  36. data/ext/ca_obj_stack.c +3 -8
  37. data/ext/ca_obj_stride.c +178 -24
  38. data/ext/ca_obj_string.c +11 -5
  39. data/ext/ca_obj_tile.c +13 -5
  40. data/ext/ca_obj_time.c +3 -1
  41. data/ext/ca_obj_timedelta.c +3 -1
  42. data/ext/ca_obj_transpose.c +1 -1
  43. data/ext/ca_obj_triop.c +62 -23
  44. data/ext/ca_obj_window.c +94 -19
  45. data/ext/ca_op_cmplx64.h +123 -0
  46. data/ext/ca_op_ipower.c +1 -5
  47. data/ext/ca_rng_normal.h +42 -0
  48. data/ext/ca_rng_xoshiro256pp.h +105 -0
  49. data/ext/ca_sort_kernels.h +5 -5
  50. data/ext/ca_sweep_engine.c +362 -155
  51. data/ext/ca_sweep_engine.h +39 -9
  52. data/ext/ca_transform_common.c +7 -0
  53. data/ext/carray.h +148 -132
  54. data/ext/carray_access.c +83 -49
  55. data/ext/carray_address_basis.c +590 -0
  56. data/ext/carray_bincount.c +8 -8
  57. data/ext/carray_broadcast.c +102 -9
  58. data/ext/carray_build_flags.h +3 -0
  59. data/ext/carray_call_cfunc.c +2010 -335
  60. data/ext/carray_call_cfunc.h +153 -0
  61. data/ext/carray_cast.c +184 -72
  62. data/ext/carray_conversion.c +22 -22
  63. data/ext/carray_copy.c +67 -32
  64. data/ext/carray_core.c +140 -13
  65. data/ext/carray_count.c +10 -11
  66. data/ext/carray_element.c +7 -7
  67. data/ext/carray_factorize.c +72 -51
  68. data/ext/carray_hold.c +1 -1
  69. data/ext/carray_index_classifier.c +7 -21
  70. data/ext/carray_internal.h +36 -3
  71. data/ext/carray_kernels_bincmp.c +1 -0
  72. data/ext/carray_kernels_binop.c +320 -298
  73. data/ext/carray_kernels_init.c +1095 -0
  74. data/ext/carray_kernels_map.c +1 -0
  75. data/ext/carray_kernels_moncmp.c +1 -0
  76. data/ext/carray_kernels_monop.c +412 -411
  77. data/ext/carray_kernels_reduce_aggregate.c +256 -87
  78. data/ext/carray_kernels_reduce_boolean.c +1 -0
  79. data/ext/carray_kernels_reduce_cumulative.c +324 -54
  80. data/ext/carray_kernels_reduce_extreme.c +605 -58
  81. data/ext/carray_kernels_reduce_variance.c +1 -0
  82. data/ext/carray_kernels_scan.c +5 -4
  83. data/ext/carray_kernels_search.c +187 -106
  84. data/ext/carray_kernels_sort.c +1 -0
  85. data/ext/carray_kernels_triop.c +1 -0
  86. data/ext/carray_lazy.c +175 -5
  87. data/ext/carray_loop.c +7 -1
  88. data/ext/carray_mask.c +29 -13
  89. data/ext/carray_median_percentile.c +56 -1
  90. data/ext/carray_memory_view.c +46 -21
  91. data/ext/carray_operator.c +50 -53
  92. data/ext/carray_order.c +1 -1
  93. data/ext/carray_partition.c +4 -4
  94. data/ext/carray_random.c +391 -47
  95. data/ext/carray_scatter.c +1 -1
  96. data/ext/carray_slab.c +13 -0
  97. data/ext/carray_sort.c +22 -24
  98. data/ext/carray_sort_kernel.c +10 -10
  99. data/ext/carray_test.c +1 -1
  100. data/ext/extconf.rb +18 -0
  101. data/ext/mk_call_cfunc.rb +323 -145
  102. data/ext/mkkernel.rb +651 -129
  103. data/ext/ruby_carray.c +28 -17
  104. data/ext/version.h +4 -4
  105. data/lib/carray/autoload_carray.rb +24 -3
  106. data/lib/carray/autoload_method_extension.rb +14 -1
  107. data/lib/carray/axis_group.rb +84 -7
  108. data/lib/carray/basics.rb +63 -59
  109. data/lib/carray/bincount_nd.rb +12 -12
  110. data/lib/carray/block_iterator.rb +111 -25
  111. data/lib/carray/categorical.rb +154 -37
  112. data/lib/carray/categorical_iterator.rb +300 -151
  113. data/lib/carray/conditional.rb +14 -14
  114. data/lib/carray/const_string.rb +131 -27
  115. data/lib/carray/construct.rb +61 -0
  116. data/lib/carray/core_extensions.rb +62 -5
  117. data/lib/carray/data_type_extension.rb +21 -1
  118. data/lib/carray/data_type_limits.rb +91 -0
  119. data/lib/carray/fixlen_string.rb +1 -1
  120. data/lib/carray/frame/concat.rb +3 -3
  121. data/lib/carray/frame/convert.rb +1 -1
  122. data/lib/carray/frame/csv_parser.rb +11 -4
  123. data/lib/carray/frame/frame.rb +85 -21
  124. data/lib/carray/frame/group.rb +36 -3
  125. data/lib/carray/frame/io.rb +224 -15
  126. data/lib/carray/frame/records.rb +18 -4
  127. data/lib/carray/frame/sort.rb +1 -1
  128. data/lib/carray/frame/verbs.rb +32 -12
  129. data/lib/carray/fuse_source.rb +123 -0
  130. data/lib/carray/fusion.rb +218 -0
  131. data/lib/carray/histogram.rb +16 -16
  132. data/lib/carray/inspect.rb +43 -16
  133. data/lib/carray/iterator.rb +147 -3
  134. data/lib/carray/lazy.rb +88 -73
  135. data/lib/carray/mask_gap_fill.rb +3 -1
  136. data/lib/carray/meld_reduce.rb +2 -2
  137. data/lib/carray/methods/align_addr.rb +1 -1
  138. data/lib/carray/methods/composition.rb +1 -1
  139. data/lib/carray/methods/discovery_along.rb +74 -0
  140. data/lib/carray/methods/factorize.rb +50 -0
  141. data/lib/carray/methods/is_in.rb +25 -14
  142. data/lib/carray/methods/locate_addr.rb +81 -3
  143. data/lib/carray/methods/mask_duplicates.rb +36 -2
  144. data/lib/carray/methods/meshgrid.rb +4 -5
  145. data/lib/carray/methods/mode.rb +2 -2
  146. data/lib/carray/methods/nunique.rb +23 -2
  147. data/lib/carray/methods/repeat.rb +110 -0
  148. data/lib/carray/methods/snap.rb +7 -2
  149. data/lib/carray/methods/unique.rb +44 -4
  150. data/lib/carray/methods/value_counts.rb +2 -2
  151. data/lib/carray/rng.rb +86 -0
  152. data/lib/carray/runtime.rb +0 -19
  153. data/lib/carray/slab_iterator.rb +77 -19
  154. data/lib/carray/string_operation_extension.rb +10 -6
  155. data/lib/carray/time.rb +906 -491
  156. data/lib/carray/window_iterator.rb +421 -27
  157. data/lib/carray.rb +7 -5
  158. data/yard-stubs/ca_obj_array.rb +385 -0
  159. data/yard-stubs/ca_obj_bitarray.rb +38 -0
  160. data/yard-stubs/ca_obj_bitfield.rb +43 -0
  161. data/yard-stubs/ca_obj_block.rb +68 -0
  162. data/yard-stubs/ca_obj_byte_swap.rb +56 -0
  163. data/yard-stubs/ca_obj_fake.rb +31 -0
  164. data/yard-stubs/ca_obj_farray.rb +32 -0
  165. data/yard-stubs/ca_obj_field.rb +45 -0
  166. data/yard-stubs/ca_obj_grid.rb +35 -0
  167. data/yard-stubs/ca_obj_refer.rb +72 -0
  168. data/yard-stubs/ca_obj_roll.rb +45 -0
  169. data/yard-stubs/ca_obj_shift.rb +43 -0
  170. data/yard-stubs/ca_obj_stride.rb +181 -0
  171. data/yard-stubs/ca_obj_tile.rb +29 -0
  172. data/yard-stubs/ca_obj_transpose.rb +40 -0
  173. data/yard-stubs/ca_obj_window.rb +57 -0
  174. data/yard-stubs/carray_access.rb +131 -0
  175. data/yard-stubs/carray_attribute.rb +246 -0
  176. data/yard-stubs/carray_broadcast.rb +37 -0
  177. data/yard-stubs/carray_cast.rb +489 -0
  178. data/yard-stubs/carray_class.rb +65 -0
  179. data/yard-stubs/carray_conversion.rb +76 -0
  180. data/yard-stubs/carray_copy.rb +79 -0
  181. data/yard-stubs/carray_core.rb +34 -0
  182. data/yard-stubs/carray_count.rb +84 -0
  183. data/yard-stubs/carray_element.rb +108 -0
  184. data/yard-stubs/carray_generate.rb +66 -0
  185. data/yard-stubs/carray_lazy.rb +228 -0
  186. data/yard-stubs/carray_loop.rb +140 -0
  187. data/yard-stubs/carray_mask.rb +259 -0
  188. data/yard-stubs/carray_math.rb +1615 -0
  189. data/yard-stubs/carray_mathfunc.rb +45 -0
  190. data/yard-stubs/carray_median_percentile.rb +103 -0
  191. data/yard-stubs/carray_memory_view.rb +163 -0
  192. data/yard-stubs/carray_order.rb +252 -0
  193. data/yard-stubs/carray_random.rb +89 -0
  194. data/yard-stubs/carray_scatter.rb +106 -0
  195. data/yard-stubs/carray_slab.rb +59 -0
  196. data/yard-stubs/carray_sort.rb +165 -0
  197. data/yard-stubs/carray_test.rb +85 -0
  198. data/yard-stubs/carray_undef.rb +64 -0
  199. data/yard-stubs/carray_utils.rb +97 -0
  200. data/yard-stubs/ruby_carray.rb +193 -0
  201. metadata +67 -10
  202. data/ext/ca_obj_unbound_repeat.c +0 -496
@@ -0,0 +1,91 @@
1
+ # What each numeric data type can hold, as constants on the typed
2
+ # classes.
3
+ #
4
+ # CArray::Int32::MAX # => 2147483647
5
+ # CArray::Float32::EPSILON # => 1.1920928955078125e-07
6
+ #
7
+ # The names follow NumPy's iinfo / finfo rather than Ruby's Float, so
8
+ # that MIN and MAX bracket the range for every numeric type alike:
9
+ #
10
+ # CArray::Float64::MIN # => -1.7976931348623157e+308
11
+ # Float::MIN # => 2.2250738585072014e-308
12
+ #
13
+ # Those are not the same number and not the same question. Ruby's
14
+ # Float::MIN is the smallest positive normal, which is TINY here; MIN
15
+ # is the bottom of the range, which for an integer type is the only
16
+ # thing MIN could mean. Code that has to bracket a type without
17
+ # knowing whether it is integer or float reads MIN and MAX and is
18
+ # right either way, and that is the reason for the choice.
19
+ #
20
+ # Not every data type has limits. boolean, fixlen and object have no
21
+ # numeric range, so they carry none of these constants -- asking gives
22
+ # a NameError rather than an answer that would have to be invented.
23
+
24
+ class CArray
25
+
26
+ # The integer widths come from the extension rather than from a table
27
+ # written here, so a platform where a type is not the usual width is
28
+ # described correctly instead of confidently mis-described.
29
+
30
+ {
31
+ Int8 => true, Int16 => true, Int32 => true, Int64 => true,
32
+ UInt8 => false, UInt16 => false, UInt32 => false, UInt64 => false,
33
+ }.each do |klass, signed|
34
+ bits = 8 * CArray.sizeof(klass::TypeSymbol)
35
+ if signed
36
+ klass.const_set(:MIN, -(2 ** (bits - 1)))
37
+ klass.const_set(:MAX, 2 ** (bits - 1) - 1)
38
+ else
39
+ klass.const_set(:MIN, 0)
40
+ klass.const_set(:MAX, 2 ** bits - 1)
41
+ end
42
+ end
43
+
44
+ # float32 and float64 are IEEE-754 binary32 and binary64, and the four
45
+ # values below follow from the format: with `p` significand bits (the
46
+ # implicit one included) and `emax` the exponent of the largest finite
47
+ # value,
48
+ #
49
+ # EPSILON = 2 ** (1 - p) the step from 1.0 to the next float
50
+ # MAX = (2 - EPSILON) * 2 ** emax
51
+ # MIN = -MAX every format here is symmetric
52
+ # TINY = 2 ** (1 - emax) the smallest positive normal
53
+ #
54
+ # All four are exactly representable as a Ruby Float, binary32
55
+ # included, so nothing is rounded on the way in. The width is
56
+ # checked rather than assumed: on a platform where C float or double
57
+ # is not one of these formats the arithmetic below would be wrong,
58
+ # and a wrong limit is worse than a missing one.
59
+
60
+ {
61
+ Float32 => [4, 24, 127],
62
+ Float64 => [8, 53, 1023],
63
+ }.each do |klass, (bytes, precision, max_exponent)|
64
+ actual = CArray.sizeof(klass::TypeSymbol)
65
+ unless actual == bytes
66
+ raise "#{klass} is #{actual} bytes wide, not the #{bytes * 8}-bit IEEE-754 " \
67
+ "format its limits are derived from"
68
+ end
69
+ epsilon = 2.0 ** (1 - precision)
70
+ max = (2.0 - epsilon) * 2.0 ** max_exponent
71
+ klass.const_set(:EPSILON, epsilon)
72
+ klass.const_set(:MAX, max)
73
+ klass.const_set(:MIN, -max)
74
+ klass.const_set(:TINY, 2.0 ** (1 - max_exponent))
75
+ end
76
+
77
+ # A complex type is a pair of floats, so its limits are its
78
+ # component's -- MIN and MAX bound the real and the imaginary part
79
+ # separately, not any magnitude of the pair. This is what
80
+ # np.finfo(np.complex64) answers too.
81
+
82
+ {
83
+ Complex64 => Float32,
84
+ Complex128 => Float64,
85
+ }.each do |klass, component|
86
+ [:MIN, :MAX, :TINY, :EPSILON].each do |name|
87
+ klass.const_set(name, component.const_get(name))
88
+ end
89
+ end
90
+
91
+ end
@@ -3,7 +3,7 @@
3
3
  # CAFixlenString high-level construction surface.
4
4
  #
5
5
  # CAFixlenString itself (the string interpretation of CA_FIXLEN storage) lives
6
- # in ext/ca_obj_string.c. This file provides the ergonomic builder that packs
6
+ # in ext/ca_obj_fixlen_string.c. This file provides the ergonomic builder that packs
7
7
  # Ruby Strings into a fixed-width CA_FIXLEN entity and wraps it via
8
8
  # CAFixlenString.wrap.
9
9
  #
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # Two entry points, mirroring the CArray-level taxonomy:
4
4
  #
5
- # +CAFrame.meld+ view frame, strict same-dtype per column,
5
+ # +CAFrame.meld+ view frame, strict same data type per column,
6
6
  # each column is a CAMeld view over the inputs
7
7
  # +CAFrame.concatenate+ eager frame, auto-casts per column,
8
8
  # each column is a materialised entity
@@ -48,10 +48,10 @@ class CAFrame
48
48
 
49
49
  # Concatenate frames along the row axis, eagerly. Each output column is
50
50
  # +CArray.concatenate+ of that column across the input frames, so per-column
51
- # dtypes auto-promote to a common type. The result is a fresh, independent
51
+ # data types auto-promote to a common type. The result is a fresh, independent
52
52
  # frame — writes to it do not propagate back to the input frames.
53
53
  #
54
- # For a view frame that shares storage with the inputs (strict same dtype
54
+ # For a view frame that shares storage with the inputs (strict same data type
55
55
  # per column, chain composability preserved) use {CAFrame.meld}.
56
56
  #
57
57
  # Column matching, index handling, and column-set / index-mix rules
@@ -31,7 +31,7 @@ class CAFrame
31
31
  #
32
32
  # Only same-shape scalar (1-D) columns qualify. An N-D column has no single
33
33
  # matrix form and raises — escape it per column with +df["name"]+. A mixed
34
- # dtype set is promoted to a common type (+result_type+, §12-F) through
34
+ # data type set is promoted to a common type (+result_type+, §12-F) through
35
35
  # lazy cast lanes, so the promotion costs no buffer either.
36
36
  #
37
37
  # +writable: true+ demands a result whose writes reach this frame's own
@@ -91,13 +91,19 @@ class CAFrame
91
91
  @recno = 0
92
92
  end
93
93
 
94
- # Fields of the next non-blank record, or nil at EOF.
95
- def read(io)
94
+ # Fields of the next record, or nil at EOF. A blank line carries no
95
+ # separator, so it cannot be a row of a file with more than one column
96
+ # and is skipped as noise between records. In a single-column file it
97
+ # is the only spelling a missing single field has -- which is what
98
+ # to_csv writes for a masked cell -- so the caller passes
99
+ # +blank_is_row: true+ once the column count is known to be one, and
100
+ # the empty record becomes a row of no fields for build_frame to pad.
101
+ def read(io, blank_is_row: false)
96
102
  loop do
97
103
  rec = CSVParser.read_record(io, @quote)
98
104
  return nil if rec.nil?
99
105
  @recno += 1
100
- next if rec.empty?
106
+ next if rec.empty? && !blank_is_row
101
107
  return rec.count(@quote).zero? ? simple(rec) : scan(rec)
102
108
  end
103
109
  end
@@ -195,7 +201,8 @@ class CAFrame
195
201
  # Consume the remaining records as data rows.
196
202
  def body
197
203
  rows = []
198
- while (fields = @tok.read(@io))
204
+ blank_is_row = @names && @names.size == 1
205
+ while (fields = @tok.read(@io, blank_is_row: blank_is_row))
199
206
  rows << fields
200
207
  end
201
208
  @rows = rows
@@ -45,14 +45,21 @@ class CAFrame
45
45
  @columns = {}
46
46
  @axis_name = axis_name || DEFAULT_AXIS_NAME
47
47
  @index = nil
48
+ # The row axis name this frame had before set_index promoted a column over
49
+ # it, so reset_index can put it back. A frame built with an index never
50
+ # had one, and neither does a frame derived from an indexed one: both are
51
+ # born indexed, so there is nothing to restore and the default stands.
52
+ @axis_name_before_index = nil
48
53
 
49
54
  n = nil
55
+ n_from = nil
50
56
  columns.each do |name, col|
51
57
  key = name.to_s
52
58
  ca = coerce_column(col)
53
59
  len = ca.shape[0]
54
60
  if n.nil?
55
- n = len
61
+ n = len
62
+ n_from = key
56
63
  elsif len != n
57
64
  raise ArgumentError,
58
65
  "column #{key.inspect} has axis-0 length #{len}, expected #{n}"
@@ -67,8 +74,12 @@ class CAFrame
67
74
  raise ArgumentError, "index must be a 1-D column (got ndim #{idx.ndim})"
68
75
  end
69
76
  if n && idx.shape[0] != n
77
+ # Name the column as well as the index: when the index is the frame's
78
+ # existing one and a column is the new arrival, blaming the index alone
79
+ # points at the side the caller cannot change.
70
80
  raise ArgumentError,
71
- "index length #{idx.shape[0]} does not match nrow #{n}"
81
+ "column #{n_from.inspect} has axis-0 length #{n}, " \
82
+ "but the index has length #{idx.shape[0]}"
72
83
  end
73
84
  @index = idx
74
85
  @nrow = idx.shape[0] if n.nil?
@@ -234,9 +245,10 @@ class CAFrame
234
245
  end
235
246
  end
236
247
 
237
- # Bind +key+ to +column+, adding the name when it is new. This is the one
238
- # place a column enters an existing frame, so it is where the axis-0 length
239
- # invariant is enforced (§12-A). It is a replacement rather than an edit,
248
+ # Bind +key+ to +column+, adding the name when it is new. This is where a
249
+ # column arriving from outside the frame has its axis-0 length checked
250
+ # (§12-A); the conversions in verbs.rb rebind +@columns+ directly and rely on
251
+ # being length-preserving instead. It is a replacement rather than an edit,
240
252
  # which is what sets it apart from the rest: +fill+ / +mask_eq+ /
241
253
  # +df[rows] = UNDEF+ write to the shared column and are therefore visible
242
254
  # wherever it is held, while this binds the name to a different column and
@@ -307,6 +319,12 @@ class CAFrame
307
319
  # value kept, so the row stays identifiable), leaving a later, better-informed
308
320
  # pass to re-judge them. Definitely-true rows carry their values through
309
321
  # unchanged in both modes.
322
+ #
323
+ # The default is a view-frame. +keep_masked: true+ returns a **materialized**
324
+ # frame instead -- carrying the UNDEF forward means writing it into the
325
+ # result, which a view cannot do without masking the parent's rows. That holds
326
+ # whether or not the selector actually carries a masked cell, so the same call
327
+ # site does not switch between sharing and copying with the data.
310
328
  def filter(keep_masked: false)
311
329
  mask = yield(self)
312
330
  unless mask.is_a?(CArray) && mask.data_type == :boolean
@@ -330,7 +348,18 @@ class CAFrame
330
348
  unless idx.ndim == 1
331
349
  raise ArgumentError, "index must be a 1-D column (got ndim #{idx.ndim})"
332
350
  end
351
+ # Only the name held before the frame had an index: a second set_index must
352
+ # not overwrite it with the first index's name.
353
+ @axis_name_before_index = @axis_name if @index.nil?
333
354
  @columns.delete(key)
355
+ if @index
356
+ # Re-indexing replaces the index, so the one being replaced goes back to
357
+ # being a column -- the same demotion reset_index performs, in the same
358
+ # position, which makes set_index over an existing index equivalent to
359
+ # reset_index followed by set_index. Dropping it would lose the column it
360
+ # was made from.
361
+ @columns = { @axis_name => @index }.merge(@columns)
362
+ end
334
363
  @index = idx
335
364
  @axis_name = key
336
365
  @nrow = idx.shape[0]
@@ -344,7 +373,8 @@ class CAFrame
344
373
  return self unless @index
345
374
  @columns = { @axis_name => @index }.merge(@columns)
346
375
  @index = nil
347
- @axis_name = DEFAULT_AXIS_NAME
376
+ @axis_name = @axis_name_before_index || DEFAULT_AXIS_NAME
377
+ @axis_name_before_index = nil
348
378
  self
349
379
  end
350
380
 
@@ -370,6 +400,17 @@ class CAFrame
370
400
  # multi-row, frame-returning path). Positional access is +df[i]+.
371
401
  def at(label)
372
402
  raise ArgumentError, "at requires an index (set one with set_index)" unless @index
403
+ # An index may hold a masked cell -- an outer join and align both build one
404
+ # -- but an undefined label identifies no row, and two undefined labels are
405
+ # not the same label. The addressing primitives already say this (a masked
406
+ # key matches nothing, not another masked key), so at says it too rather
407
+ # than answering through eq(UNDEF), which asks about the mask and not about
408
+ # the label at all.
409
+ if UNDEF.equal?(label)
410
+ raise ArgumentError,
411
+ "at(UNDEF): an undefined label identifies no row; " \
412
+ "use filter { |f| f.index.is_masked } for the rows with no label"
413
+ end
373
414
  pos = @index.eq(label).where
374
415
  case pos.elements
375
416
  when 0
@@ -436,13 +477,6 @@ class CAFrame
436
477
  h
437
478
  end
438
479
 
439
- # @return [String]
440
- def inspect
441
- parts = @columns.map { |k, v| "#{k}:#{v.data_type}#{v.ndim > 1 ? v.shape[1..].inspect : ''}" }
442
- idx = @index ? " index=#{@axis_name.inspect}" : ""
443
- "#<CAFrame nrow=#{@nrow} vars=[#{parts.join(', ')}]#{idx}>"
444
- end
445
-
446
480
  private def rebuild(cols)
447
481
  CAFrame.new(cols, axis_name: @axis_name, index: @index)
448
482
  end
@@ -490,6 +524,19 @@ class CAFrame
490
524
  # and index are untouched; the selected cells of every column go to UNDEF.
491
525
  # The selector is forwarded to the column indexer, which classifies it.
492
526
  private def mask_rows(selector)
527
+ # Decide before changing anything. A read-only column (a categorical, whose
528
+ # codes are read-only) refuses the write, and refusing half way through
529
+ # would leave the frame masked in whichever columns happened to come first.
530
+ refusing = @columns.find { |_, col| col.read_only? }
531
+ if refusing
532
+ raise "can not modify read-only array: column #{refusing[0].inspect} " \
533
+ "refuses the write, so no column was masked"
534
+ end
535
+ # The selector is classified by the column indexer, which is also what
536
+ # bound-checks it -- so with no column to forward it to, nothing would.
537
+ # Build the row mask purely to have the frame's own row axis refuse an
538
+ # out-of-range row, as every other row form does.
539
+ selected_row_mask(selector) if @columns.empty?
493
540
  @columns.each_value do |col|
494
541
  col[selector, *([nil] * (col.ndim - 1))] = UNDEF
495
542
  end
@@ -511,7 +558,7 @@ class CAFrame
511
558
  end
512
559
 
513
560
  # df[sel] = other : replace the selected contiguous span with other's rows
514
- # (any length). Columns are concatenated head + other + tail, so dtypes
561
+ # (any length). Columns are concatenated head + other + tail, so data types
515
562
  # promote and the row count shifts by other.nrow - span.
516
563
  private def splice_rows(selector, other)
517
564
  lo, hi = contiguous_span(selector)
@@ -536,9 +583,9 @@ class CAFrame
536
583
  case pieces.size
537
584
  when 0 then col[CArray.int32(0), *tail] # replaced every row with none
538
585
  when 1 then pieces.first
539
- else CArray.meld(pieces, axis: 0) # CAMeld view; dtype mismatch
586
+ else CArray.meld(pieces, axis: 0) # CAMeld view; data type mismatch
540
587
  end # across pieces raises.
541
- # For dtype conversion, cast
588
+ # For data type conversion, cast
542
589
  # the incoming +other+'s
543
590
  # column beforehand — silent
544
591
  # promotion in a splice would
@@ -547,7 +594,16 @@ class CAFrame
547
594
  new_index = splice_index(other, lo, hi)
548
595
  @columns = new_cols
549
596
  @index = new_index
550
- @nrow = @nrow - (hi - lo) + other.nrow
597
+ # Read the new count off something the frame now holds, rather than
598
+ # computing it. For every column the pieces welded above are head (lo) +
599
+ # other's rows + tail (nrow - hi), so the arithmetic and the objects agree
600
+ # -- except when there is nothing to weld: a frame with no columns and no
601
+ # index would otherwise come away claiming rows that nothing backs.
602
+ @nrow = if (witness = new_cols.each_value.first || new_index)
603
+ witness.shape[0]
604
+ else
605
+ 0
606
+ end
551
607
  self
552
608
  end
553
609
 
@@ -566,7 +622,7 @@ class CAFrame
566
622
  case pieces.size
567
623
  when 0 then @index[CArray.int32(0)]
568
624
  when 1 then pieces.first
569
- else CArray.meld(pieces, axis: 0) # index dtype must match across frames
625
+ else CArray.meld(pieces, axis: 0) # index data type must match across frames
570
626
  end
571
627
  end
572
628
 
@@ -616,8 +672,11 @@ class CAFrame
616
672
  end
617
673
 
618
674
  private def select_rows(selector, keep_masked: false)
619
- if keep_masked && selector.is_a?(CArray) &&
620
- selector.data_type == :boolean && selector.has_mask?
675
+ if keep_masked && selector.is_a?(CArray) && selector.data_type == :boolean
676
+ # Not gated on selector.has_mask?: gating there would make the same call
677
+ # site return a view-frame or a materialized one depending on whether that
678
+ # run's data happened to produce an undetermined cell (the reason splice
679
+ # always snapshots -- see CAFrame.md section 3).
621
680
  return select_rows_keep_masked(selector)
622
681
  end
623
682
  cols = {}
@@ -638,7 +697,12 @@ class CAFrame
638
697
  g[undet_kept, *tail] = UNDEF
639
698
  cols[name] = g
640
699
  end
641
- new_index = @index ? @index[keep] : nil
700
+ # The index is copied along with the columns. Its values are carried over
701
+ # unmasked (an undetermined row keeps its label, as mask_rows leaves the
702
+ # index alone), but a materialized frame has to be materialized whole: an
703
+ # aliased index would write through to the parent from a frame whose columns
704
+ # do not.
705
+ new_index = @index ? @index[keep].copy : nil
642
706
  CAFrame.new(cols, axis_name: @axis_name, index: new_index)
643
707
  end
644
708
 
@@ -13,7 +13,7 @@ class CAFrame
13
13
  raise ArgumentError, "group_by needs at least one key" if keys.empty?
14
14
  cat = grouping_categorical(keys)
15
15
  axis = keys.size == 1 && keys.first.is_a?(String) ? keys.first : "group"
16
- GroupedFrame.new(self, cat, axis)
16
+ GroupedFrame.new(self, cat, axis, keys.grep(String))
17
17
  end
18
18
 
19
19
  # Number of rows currently selected — used by group per-group view-frames
@@ -28,7 +28,14 @@ class CAFrame
28
28
  # values, categorized by content (the codes are composed from the
29
29
  # per-column keys).
30
30
  n = nrow
31
- CArray.object(n) { |i| cols.map { |c| c[i] } }.categorize
31
+ key = CArray.object(n) { |i| cols.map { |c| c[i] } }
32
+ # One undetermined component makes the whole tuple undetermined, the same
33
+ # answer a single masked key cell gets. Left as a value, the UNDEF inside
34
+ # the tuple would intern as an ordinary distinct key and the row would
35
+ # form a group of its own.
36
+ undetermined = CArray.boolean(n) { |i| key[i].any? { |v| UNDEF.equal?(v) } }
37
+ key[undetermined] = UNDEF if undetermined.any
38
+ key.categorize
32
39
  end
33
40
  end
34
41
 
@@ -56,10 +63,14 @@ end
56
63
  # aggregate -> declarative per-column reductions into a new frame
57
64
  # table { |g| }-> cross-column Ruby escape, g is a per-group view-frame
58
65
  class GroupedFrame
59
- def initialize(frame, cat, axis_name)
66
+ # +key_names+ are the frame columns the grouping was keyed on. They become
67
+ # the result's index, so the reduction shortcuts must not also return them as
68
+ # reduced columns; an external CArray key contributes no name.
69
+ def initialize(frame, cat, axis_name, key_names = [])
60
70
  @frame = frame
61
71
  @cat = cat
62
72
  @axis_name = axis_name
73
+ @key_names = key_names
63
74
  @labels = cat.labels # group values, in code order
64
75
  end
65
76
 
@@ -124,6 +135,23 @@ class GroupedFrame
124
135
 
125
136
  # Convenience reductions over every numeric scalar column (memo §6-4
126
137
  # "grp.mean"). Non-numeric / N-D columns are skipped.
138
+ #
139
+ # @!method sum
140
+ # Returns a frame of the per-group sum of every numeric one-dimensional
141
+ # column. Non-numeric and multi-dimensional columns are left out.
142
+ # @return [CAFrame] one row per group, indexed by the group labels.
143
+ # @!method mean
144
+ # Returns a frame of the per-group arithmetic mean of every numeric
145
+ # one-dimensional column, as {#sum} does.
146
+ # @return [CAFrame] one row per group.
147
+ # @!method min
148
+ # Returns a frame of the per-group minimum of every numeric
149
+ # one-dimensional column, as {#sum} does.
150
+ # @return [CAFrame] one row per group.
151
+ # @!method max
152
+ # Returns a frame of the per-group maximum of every numeric
153
+ # one-dimensional column, as {#sum} does.
154
+ # @return [CAFrame] one row per group.
127
155
  [:sum, :mean, :min, :max].each do |red|
128
156
  define_method(red) { reduce_numeric(red) }
129
157
  end
@@ -143,6 +171,11 @@ class GroupedFrame
143
171
  private def reduce_numeric(reduction)
144
172
  cols = {}
145
173
  @frame.variable_names.each do |name|
174
+ # A key column is the index here, not a result column. Filtering it out
175
+ # by name rather than by data type is what makes a numeric key behave
176
+ # like a string one: NON_NUMERIC is about which columns a reduction can
177
+ # apply to, which happened to cover string keys and nothing else.
178
+ next if @key_names.include?(name)
146
179
  col = @frame[name]
147
180
  next unless col.ndim == 1 && !NON_NUMERIC.include?(col.data_type)
148
181
  cols[name] = col.group_by_category(@cat).public_send(reduction)