carray 3.0.2 → 3.0.4

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 (247) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +568 -0
  3. data/README.md +6 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_descriptor.h +27 -24
  6. data/ext/ca_axis_dispatch.c +220 -0
  7. data/ext/ca_axis_group.c +201 -123
  8. data/ext/ca_binop_dispatch.c +46 -52
  9. data/ext/ca_binop_dispatch.h +1 -1
  10. data/ext/ca_categorical_iterator.c +249 -183
  11. data/ext/ca_compare.c +7 -12
  12. data/ext/ca_for_buffer.h +21 -68
  13. data/ext/ca_group_iter.c +38 -23
  14. data/ext/ca_kernel_iterator.c +1765 -2076
  15. data/ext/ca_kernel_iterator.h +584 -566
  16. data/ext/ca_monop_dispatch.c +86 -63
  17. data/ext/ca_monop_dispatch.h +5 -0
  18. data/ext/ca_obj_array.c +223 -56
  19. data/ext/ca_obj_bincmp.c +172 -138
  20. data/ext/ca_obj_binop.c +116 -230
  21. data/ext/ca_obj_bitarray.c +40 -26
  22. data/ext/ca_obj_bitfield.c +100 -68
  23. data/ext/ca_obj_block.c +19 -9
  24. data/ext/ca_obj_byte_swap.c +45 -46
  25. data/ext/ca_obj_const_string.c +269 -44
  26. data/ext/ca_obj_face.c +612 -89
  27. data/ext/ca_obj_face.h +111 -47
  28. data/ext/ca_obj_fake.c +36 -79
  29. data/ext/ca_obj_farray.c +7 -3
  30. data/ext/ca_obj_field.c +33 -8
  31. data/ext/ca_obj_fixlen_string.c +23 -24
  32. data/ext/ca_obj_grid.c +541 -210
  33. data/ext/ca_obj_meld.c +331 -111
  34. data/ext/ca_obj_moncmp.c +80 -65
  35. data/ext/ca_obj_monop.c +177 -149
  36. data/ext/ca_obj_object.c +664 -248
  37. data/ext/ca_obj_record.c +63 -23
  38. data/ext/ca_obj_reduce.c +18 -16
  39. data/ext/ca_obj_refer.c +140 -58
  40. data/ext/ca_obj_remap.c +85 -24
  41. data/ext/ca_obj_repeat.c +24 -9
  42. data/ext/ca_obj_roll.c +121 -16
  43. data/ext/ca_obj_select.c +61 -58
  44. data/ext/ca_obj_select_axis.c +73 -107
  45. data/ext/ca_obj_shift.c +32 -102
  46. data/ext/ca_obj_stack.c +372 -122
  47. data/ext/ca_obj_stride.c +348 -140
  48. data/ext/ca_obj_string.c +88 -22
  49. data/ext/ca_obj_tile.c +38 -21
  50. data/ext/ca_obj_time.c +45 -31
  51. data/ext/ca_obj_timedelta.c +43 -29
  52. data/ext/ca_obj_transpose.c +17 -6
  53. data/ext/ca_obj_triop.c +125 -141
  54. data/ext/ca_obj_window.c +330 -44
  55. data/ext/ca_op_ipower.c +24 -11
  56. data/ext/ca_op_powi.h +57 -30
  57. data/ext/ca_pow5_table.h +656 -0
  58. data/ext/ca_sort_kernels.h +70 -0
  59. data/ext/ca_sweep_engine.c +104 -81
  60. data/ext/ca_sweep_engine.h +7 -20
  61. data/ext/ca_triop_dispatch.c +4 -1
  62. data/ext/ca_triop_dispatch.h +5 -2
  63. data/ext/caframe_csv_reader.c +542 -0
  64. data/ext/carray.h +241 -92
  65. data/ext/carray_access.c +608 -276
  66. data/ext/carray_address_basis.c +494 -97
  67. data/ext/carray_attribute.c +129 -4
  68. data/ext/carray_bincount.c +39 -7
  69. data/ext/carray_broadcast.c +56 -19
  70. data/ext/carray_build_flags.h +1 -1
  71. data/ext/carray_call_cfunc.c +108 -28
  72. data/ext/carray_cast.c +1576 -223
  73. data/ext/carray_cast_func.rb +77 -24
  74. data/ext/carray_class.c +4 -3
  75. data/ext/carray_conversion.c +208 -75
  76. data/ext/carray_copy.c +82 -24
  77. data/ext/carray_core.c +646 -162
  78. data/ext/carray_count.c +171 -88
  79. data/ext/carray_element.c +94 -60
  80. data/ext/carray_factorize.c +670 -324
  81. data/ext/carray_generate.c +260 -83
  82. data/ext/carray_histogram.c +209 -106
  83. data/ext/carray_hold.c +14 -16
  84. data/ext/carray_index_classifier.c +422 -243
  85. data/ext/carray_internal.h +692 -3
  86. data/ext/carray_kernels_bincmp.c +7 -0
  87. data/ext/carray_kernels_binop.c +487 -120
  88. data/ext/carray_kernels_init.c +653 -50
  89. data/ext/carray_kernels_moncmp.c +15 -8
  90. data/ext/carray_kernels_monop.c +99 -43
  91. data/ext/carray_kernels_reduce_aggregate.c +3925 -6112
  92. data/ext/carray_kernels_reduce_boolean.c +112 -36
  93. data/ext/carray_kernels_reduce_cumulative.c +2127 -6903
  94. data/ext/carray_kernels_reduce_extreme.c +3032 -2892
  95. data/ext/carray_kernels_reduce_variance.c +439 -500
  96. data/ext/carray_kernels_scan.c +1097 -488
  97. data/ext/carray_kernels_search.c +3787 -2176
  98. data/ext/carray_kernels_sort.c +1200 -920
  99. data/ext/carray_kernels_triop.c +479 -7
  100. data/ext/carray_key_windows.c +216 -0
  101. data/ext/carray_lazy.c +967 -153
  102. data/ext/carray_loop.c +81 -65
  103. data/ext/carray_mask.c +910 -324
  104. data/ext/carray_math_kernel.h +1 -0
  105. data/ext/carray_median_percentile.c +299 -100
  106. data/ext/carray_memory_view.c +872 -758
  107. data/ext/carray_operator.c +1171 -364
  108. data/ext/carray_order.c +505 -133
  109. data/ext/carray_partition.c +112 -65
  110. data/ext/carray_random.c +327 -239
  111. data/ext/carray_round.c +273 -0
  112. data/ext/carray_scatter.c +346 -78
  113. data/ext/carray_segment.c +257 -0
  114. data/ext/carray_slab.c +249 -86
  115. data/ext/carray_sort.c +224 -227
  116. data/ext/carray_sort_kernel.c +8 -1
  117. data/ext/carray_struct.c +118 -39
  118. data/ext/carray_test.c +193 -69
  119. data/ext/carray_time_text.c +1182 -0
  120. data/ext/carray_undef.c +4 -0
  121. data/ext/carray_utils.c +579 -113
  122. data/ext/extconf.rb +27 -2
  123. data/ext/mk_call_cfunc.rb +19 -4
  124. data/ext/mk_pow5_table.rb +35 -0
  125. data/ext/mkkernel.rb +1202 -1014
  126. data/ext/ruby_carray.c +17 -5
  127. data/ext/version.h +4 -4
  128. data/ext/version.rb +4 -2
  129. data/lib/carray/attribute.rb +160 -17
  130. data/lib/carray/autoload_carray.rb +23 -27
  131. data/lib/carray/autoload_method_extension.rb +20 -5
  132. data/lib/carray/axis_group.rb +360 -63
  133. data/lib/carray/basics.rb +162 -28
  134. data/lib/carray/bincount_nd.rb +18 -2
  135. data/lib/carray/block_iterator.rb +231 -58
  136. data/lib/carray/boolean_reduce.rb +34 -42
  137. data/lib/carray/categorical.rb +242 -52
  138. data/lib/carray/categorical_iterator.rb +437 -827
  139. data/lib/carray/complex.rb +41 -34
  140. data/lib/carray/conditional.rb +150 -14
  141. data/lib/carray/const_string.rb +59 -4
  142. data/lib/carray/construct.rb +15 -11
  143. data/lib/carray/data_type_extension.rb +1 -0
  144. data/lib/carray/distance_limit.rb +77 -0
  145. data/lib/carray/fixlen_string.rb +7 -1
  146. data/lib/carray/frame/column_verbs.rb +70 -0
  147. data/lib/carray/frame/concat.rb +157 -6
  148. data/lib/carray/frame/convert.rb +82 -7
  149. data/lib/carray/frame/csv_parser.rb +688 -87
  150. data/lib/carray/frame/describe.rb +80 -0
  151. data/lib/carray/frame/frame.rb +369 -45
  152. data/lib/carray/frame/group.rb +631 -90
  153. data/lib/carray/frame/io.rb +506 -79
  154. data/lib/carray/frame/join.rb +411 -62
  155. data/lib/carray/frame/records.rb +116 -19
  156. data/lib/carray/frame/reshape.rb +455 -0
  157. data/lib/carray/frame/sort.rb +30 -18
  158. data/lib/carray/frame/verbs.rb +868 -61
  159. data/lib/carray/frame/windows.rb +117 -0
  160. data/lib/carray/frame.rb +4 -0
  161. data/lib/carray/fuse_source.rb +118 -2
  162. data/lib/carray/fusion.rb +257 -42
  163. data/lib/carray/histogram.rb +6 -1
  164. data/lib/carray/inspect.rb +54 -13
  165. data/lib/carray/iterator.rb +99 -9
  166. data/lib/carray/key_windows.rb +620 -0
  167. data/lib/carray/lazy.rb +216 -193
  168. data/lib/carray/mask_gap_fill.rb +112 -92
  169. data/lib/carray/math.rb +32 -19
  170. data/lib/carray/meld_reduce.rb +52 -21
  171. data/lib/carray/methods/align_addr.rb +8 -4
  172. data/lib/carray/methods/bin.rb +26 -18
  173. data/lib/carray/methods/bincount.rb +18 -2
  174. data/lib/carray/methods/bit_string.rb +15 -4
  175. data/lib/carray/methods/choose.rb +16 -3
  176. data/lib/carray/methods/composition.rb +18 -10
  177. data/lib/carray/methods/factorize.rb +2 -2
  178. data/lib/carray/methods/gather_nd.rb +25 -5
  179. data/lib/carray/methods/insert_block.rb +9 -14
  180. data/lib/carray/methods/is_between.rb +59 -0
  181. data/lib/carray/methods/is_in.rb +140 -10
  182. data/lib/carray/methods/join.rb +2 -7
  183. data/lib/carray/methods/lag.rb +49 -0
  184. data/lib/carray/methods/locate_addr.rb +182 -45
  185. data/lib/carray/methods/mode.rb +18 -1
  186. data/lib/carray/methods/nunique.rb +11 -6
  187. data/lib/carray/methods/repeat.rb +1 -20
  188. data/lib/carray/methods/resize.rb +21 -23
  189. data/lib/carray/methods/snap.rb +34 -12
  190. data/lib/carray/methods/unique.rb +11 -2
  191. data/lib/carray/methods/value_counts.rb +4 -11
  192. data/lib/carray/record_order.rb +208 -0
  193. data/lib/carray/rng.rb +3 -0
  194. data/lib/carray/runtime.rb +65 -17
  195. data/lib/carray/segment_iterator.rb +1221 -0
  196. data/lib/carray/serialize.rb +214 -30
  197. data/lib/carray/slab_iterator.rb +101 -27
  198. data/lib/carray/stack.rb +10 -4
  199. data/lib/carray/string.rb +105 -5
  200. data/lib/carray/string_operation_extension.rb +12 -4
  201. data/lib/carray/struct.rb +137 -58
  202. data/lib/carray/struct_builder.rb +124 -17
  203. data/lib/carray/time.rb +1222 -186
  204. data/lib/carray/window_iterator.rb +242 -81
  205. data/lib/carray.rb +9 -2
  206. data/yard-stubs/ca_obj_array.rb +4 -4
  207. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  208. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  209. data/yard-stubs/ca_obj_fake.rb +1 -1
  210. data/yard-stubs/ca_obj_field.rb +5 -1
  211. data/yard-stubs/ca_obj_grid.rb +21 -10
  212. data/yard-stubs/ca_obj_refer.rb +5 -4
  213. data/yard-stubs/ca_obj_roll.rb +2 -3
  214. data/yard-stubs/ca_obj_shift.rb +5 -1
  215. data/yard-stubs/ca_obj_stride.rb +10 -7
  216. data/yard-stubs/ca_obj_tile.rb +1 -1
  217. data/yard-stubs/ca_obj_transpose.rb +1 -1
  218. data/yard-stubs/ca_obj_window.rb +9 -5
  219. data/yard-stubs/carray_access.rb +11 -5
  220. data/yard-stubs/carray_attribute.rb +0 -5
  221. data/yard-stubs/carray_broadcast.rb +1 -1
  222. data/yard-stubs/carray_cast.rb +88 -43
  223. data/yard-stubs/carray_conversion.rb +15 -2
  224. data/yard-stubs/carray_copy.rb +12 -5
  225. data/yard-stubs/carray_core.rb +3 -3
  226. data/yard-stubs/carray_count.rb +9 -0
  227. data/yard-stubs/carray_element.rb +10 -4
  228. data/yard-stubs/carray_generate.rb +21 -1
  229. data/yard-stubs/carray_lazy.rb +5 -1
  230. data/yard-stubs/carray_loop.rb +4 -1
  231. data/yard-stubs/carray_mask.rb +60 -12
  232. data/yard-stubs/carray_math.rb +52 -14
  233. data/yard-stubs/carray_median_percentile.rb +15 -8
  234. data/yard-stubs/carray_memory_view.rb +40 -23
  235. data/yard-stubs/carray_order.rb +61 -29
  236. data/yard-stubs/carray_random.rb +13 -4
  237. data/yard-stubs/carray_scatter.rb +21 -9
  238. data/yard-stubs/carray_segment.rb +72 -0
  239. data/yard-stubs/carray_slab.rb +10 -2
  240. data/yard-stubs/carray_sort.rb +49 -8
  241. data/yard-stubs/carray_test.rb +15 -3
  242. data/yard-stubs/carray_utils.rb +0 -26
  243. metadata +19 -5
  244. data/ext/ca_for_each_element.h +0 -239
  245. data/ext/carray_kernels_map.c +0 -3467
  246. data/lib/carray/arrow_tensor.rb +0 -401
  247. data/lib/carray/table.rb +0 -108
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CAFrame
4
+
5
+ # The one rule of a frame-wide verb over the frame's columns -- the group
6
+ # reductions and transforms, the windowed reductions -- kept in one place
7
+ # so that every verb answers in the same shape:
8
+ #
9
+ # - given names, the CArray (one) or the Array of them (several), as
10
+ # +df[...]+ returns columns; a named column whose type has no such
11
+ # operation is refused with CArray::DataTypeError, as the core refuses
12
+ # it, since it was asked for;
13
+ # - given none, every column whose type has it, the key columns left out,
14
+ # as a Hash of name to result that the caller makes a frame of.
15
+ #
16
+ # Whether a type has the operation is asked of the core on two copies of
17
+ # the column's first row (two blank cells of its kind when it has no
18
+ # rows), so the answer is about the type and not the data: a refusal
19
+ # there means the operation does not apply, while an error in the verb
20
+ # itself is left to raise. An N-D column (each row a profile) is asked,
21
+ # and computed, along the rows.
22
+ module ColumnVerbs
23
+
24
+ private
25
+
26
+ def over_named_columns (verb, names, key_names, args = [], keywords = {})
27
+ if names.empty?
28
+ cols = {}
29
+ @frame.column_names.each do |name|
30
+ next if key_names.include?(name)
31
+ col = @frame[name]
32
+ next unless column_has?(col, verb, args, keywords)
33
+ cols[name] = yield(col)
34
+ end
35
+ return cols
36
+ end
37
+ results = names.map do |name|
38
+ col = @frame[@frame.__send__(:column_key, name, verb)]
39
+ unless column_has?(col, verb, args, keywords)
40
+ raise CArray::DataTypeError,
41
+ "#{verb}: column #{name.inspect} " \
42
+ "(#{CArray::Inspector.type_label(col)}) has no #{verb}"
43
+ end
44
+ yield(col)
45
+ end
46
+ results.size == 1 ? results.first : results
47
+ end
48
+
49
+ # count with no value counts the present cells, which every type has;
50
+ # a Face's own count takes a value (CACategorical#count(label)), so the
51
+ # type is asked about count_not_masked instead.
52
+ def column_has? (col, verb, args, keywords)
53
+ verb = :count_not_masked if verb == :count && args.empty?
54
+ # Two cells: one cell does not reach what combines two (a String's
55
+ # median interpolates, its sum adds), and an empty column answers
56
+ # with the identity without asking the type at all.
57
+ tail = [nil] * (col.ndim - 1)
58
+ probe = if col.shape[0] > 0
59
+ col[CA_INT64([0, 0]), *tail]
60
+ else
61
+ col.resize(2, *col.shape[1..])
62
+ end
63
+ keywords = keywords.merge(axis: 0) if col.ndim > 1
64
+ probe.public_send(verb, *args, **keywords)
65
+ true
66
+ rescue CArray::DataTypeError, TypeError, ArgumentError
67
+ false
68
+ end
69
+ end
70
+ end
@@ -9,6 +9,9 @@
9
9
  #
10
10
  # Both are class methods (symmetric N-ary; no frame is privileged), like
11
11
  # +CArray.meld+ / +CArray.concatenate+, not instance verbs.
12
+ #
13
+ # +CAFrame.stack+ and +CAFrame#unstack+ add and take away a layer axis
14
+ # instead of rows.
12
15
 
13
16
  class CAFrame
14
17
  # Weld frames along the row axis, view-style. Each output column is
@@ -28,6 +31,13 @@ class CAFrame
28
31
  #
29
32
  # The index is welded too when every frame has one (their +axis_name+
30
33
  # must agree); if none do, the result has no index; a mix raises.
34
+ #
35
+ # A frame with no rows adds no values, so it takes no part in the result
36
+ # past these checks: its column set, its N-D columns' trailing dimensions
37
+ # and its index still have to agree, but the
38
+ # data types are the other frames' (a header-only CSV reads as object
39
+ # columns, which would otherwise refuse or demote the rest). When every
40
+ # frame is empty, the first one stands for them.
31
41
  # Column-set mismatch raises — a union-with-UNDEF mode is a possible
32
42
  # future opt-in, kept out here to stay explicit (memo §4.2).
33
43
  #
@@ -37,8 +47,11 @@ class CAFrame
37
47
  frames = frames.flatten
38
48
  check_concat_inputs(frames, verb: "meld")
39
49
  first = frames.first
40
- names = first.variable_names
50
+ names = first.column_names
41
51
  check_column_sets(frames, names, verb: "meld")
52
+ index_pieces(frames, verb: "meld")
53
+ check_trailing_shapes(frames, names, verb: "meld")
54
+ frames = frames_with_rows(frames)
42
55
  cols = {}
43
56
  names.each do |name|
44
57
  cols[name] = CArray.meld(frames.map { |f| f[name] }, axis: 0)
@@ -54,8 +67,9 @@ class CAFrame
54
67
  # For a view frame that shares storage with the inputs (strict same data type
55
68
  # per column, chain composability preserved) use {CAFrame.meld}.
56
69
  #
57
- # Column matching, index handling, and column-set / index-mix rules
58
- # match {CAFrame.meld}.
70
+ # Column matching, index handling, column-set / index-mix rules and the
71
+ # treatment of a frame with no rows match {CAFrame.meld}: an empty frame's
72
+ # data types take no part in the common type.
59
73
  #
60
74
  # CAFrame.concatenate(jan, feb, mar) # eager, independent result
61
75
  # CAFrame.concatenate([jan, feb, mar]) # an Array is accepted too
@@ -63,8 +77,11 @@ class CAFrame
63
77
  frames = frames.flatten
64
78
  check_concat_inputs(frames, verb: "concatenate")
65
79
  first = frames.first
66
- names = first.variable_names
80
+ names = first.column_names
67
81
  check_column_sets(frames, names, verb: "concatenate")
82
+ index_pieces(frames, verb: "concatenate")
83
+ check_trailing_shapes(frames, names, verb: "concatenate")
84
+ frames = frames_with_rows(frames)
68
85
  cols = {}
69
86
  names.each do |name|
70
87
  cols[name] = CArray.concatenate(frames.map { |f| f[name] })
@@ -72,6 +89,108 @@ class CAFrame
72
89
  new(cols, axis_name: first.axis_name, index: concatenate_index(frames))
73
90
  end
74
91
 
92
+ # Stack frames of the same shape as layers. Each output column is
93
+ # +CArray.stack+ of that column across the frames along +axis+, counted in
94
+ # the column's own axes (0 is the row axis, so +axis+ starts at 1): a
95
+ # scalar column of K frames becomes a column of shape (nrow, K). The rows
96
+ # stay as they are, so row verbs work on the stack unchanged and a layer is
97
+ # a column axis -- +s["temp"][nil, k]+ is frame k's column, and
98
+ # +s["temp"].mean(axis: 1)+ reduces across the layers.
99
+ #
100
+ # The result is a view frame: its columns are CAStack views over the
101
+ # frames' columns, and writes reach them. Call +copy+ for an independent
102
+ # frame. The layers carry no labels; keep them alongside (the dates of the
103
+ # files, say) as an array of your own.
104
+ #
105
+ # Every frame must have the same columns, the same number of rows and the
106
+ # same index (by value) under the same axis name, or none; the result
107
+ # takes the first frame's. +unstack+ is the inverse:
108
+ #
109
+ # s = CAFrame.stack(jan1, jan2, jan3) # temp:float64 -> temp:float64[3]
110
+ # CAFrame.stack(*s.unstack(axis: 1)) # the same frame again
111
+ def self.stack(*frames, axis: 1)
112
+ frames = frames.flatten
113
+ check_concat_inputs(frames, verb: "stack")
114
+ check_layer_axis(axis, "stack")
115
+ first = frames.first
116
+ names = first.column_names
117
+ check_column_sets(frames, names, verb: "stack")
118
+ frames.each_with_index do |f, i|
119
+ next if i.zero?
120
+ unless f.nrow == first.nrow
121
+ raise ArgumentError,
122
+ "stack: frame #{i} has #{f.nrow} rows, frame 0 has #{first.nrow}"
123
+ end
124
+ end
125
+ indexes, = index_pieces(frames, verb: "stack")
126
+ if indexes && ! indexes.all? { |x| same_index_values?(x, indexes.first) }
127
+ raise ArgumentError, "stack: the frames' indexes differ; stacked rows have to be the same rows"
128
+ end
129
+ cols = {}
130
+ names.each do |name|
131
+ parts = frames.map { |f| f[name] }
132
+ if axis > parts.first.ndim
133
+ raise ArgumentError,
134
+ "stack: axis #{axis} out of range for column #{name.inspect} " \
135
+ "(a #{parts.first.ndim}-D column takes 1 to #{parts.first.ndim})"
136
+ end
137
+ cols[name] = CArray.stack(parts, axis: axis)
138
+ end
139
+ new(cols, axis_name: first.axis_name, index: first.index)
140
+ end
141
+
142
+ # Split the frame along a column axis into an Array of frames, one per
143
+ # position on that axis -- the inverse of {CAFrame.stack}. +axis+ counts
144
+ # the columns' own axes (0 is the row axis, so it starts at 1). Every
145
+ # column must have that axis, and the same length along it: a column
146
+ # without it has no layer to give each frame. Each frame's columns are
147
+ # views of this frame's, and each has this frame's index.
148
+ #
149
+ # s.unstack(axis: 1) # => [frame of layer 0, frame of layer 1, ...]
150
+ def unstack(axis:)
151
+ self.class.send(:check_layer_axis, axis, "unstack")
152
+ if @columns.empty?
153
+ raise ArgumentError, "unstack: the frame has no columns to unstack"
154
+ end
155
+ length = nil
156
+ @columns.each do |name, col|
157
+ unless axis < col.ndim
158
+ raise ArgumentError,
159
+ "unstack: column #{name.inspect} is #{col.ndim}-D and has no axis #{axis}"
160
+ end
161
+ length ||= col.shape[axis]
162
+ unless col.shape[axis] == length
163
+ raise ArgumentError,
164
+ "unstack: column #{name.inspect} has #{col.shape[axis]} along axis #{axis}, " \
165
+ "the others #{length}"
166
+ end
167
+ end
168
+ (0...length).map do |k|
169
+ cols = @columns.to_h do |name, col|
170
+ idx = [nil] * col.ndim
171
+ idx[axis] = k
172
+ [name, col[*idx]]
173
+ end
174
+ CAFrame.new(cols, axis_name: @axis_name, index: @index)
175
+ end
176
+ end
177
+
178
+ def self.check_layer_axis(axis, verb)
179
+ unless axis.is_a?(Integer)
180
+ raise TypeError, "#{verb}: axis must be an Integer (got #{axis.inspect})"
181
+ end
182
+ if axis == 0
183
+ raise ArgumentError,
184
+ "#{verb}: axis 0 is the row axis; rows are joined by meld or concatenate"
185
+ end
186
+ if axis < 0
187
+ raise ArgumentError,
188
+ "#{verb}: axis counts a column's axes from 1 (got #{axis}); " \
189
+ "a negative axis would mean a different axis in columns of different ndim"
190
+ end
191
+ end
192
+ private_class_method :check_layer_axis
193
+
75
194
  # ---- shared validators -------------------------------------------------
76
195
 
77
196
  def self.check_concat_inputs(frames, verb:)
@@ -86,15 +205,47 @@ class CAFrame
86
205
  expected = names.sort
87
206
  frames.each_with_index do |f, i|
88
207
  next if i.zero?
89
- if f.variable_names.sort != expected
208
+ if f.column_names.sort != expected
90
209
  raise ArgumentError,
91
- "#{verb}: frame #{i} has columns #{f.variable_names.inspect}, " \
210
+ "#{verb}: frame #{i} has columns #{f.column_names.inspect}, " \
92
211
  "expected the same set as #{names.inspect}"
93
212
  end
94
213
  end
95
214
  end
96
215
  private_class_method :check_column_sets
97
216
 
217
+ # A column's trailing dimensions are its shape, not a type the other
218
+ # frames can settle, so a frame with no rows has to agree on them too.
219
+ def self.check_trailing_shapes(frames, names, verb:)
220
+ names.each do |name|
221
+ trailing = frames.first[name].shape[1..]
222
+ frames.each_with_index do |f, i|
223
+ next if i.zero? || f[name].shape[1..] == trailing
224
+ raise ArgumentError,
225
+ "#{verb}: column #{name.inspect} has trailing dimensions " \
226
+ "#{f[name].shape[1..].inspect} in frame #{i}, #{trailing.inspect} in frame 0"
227
+ end
228
+ end
229
+ end
230
+ private_class_method :check_trailing_shapes
231
+
232
+ # Two indexes hold the same rows when their values agree cell by cell; a
233
+ # NaN label matches a NaN label, and a masked cell a masked cell.
234
+ def self.same_index_values?(a, b)
235
+ return false unless a.shape == b.shape
236
+ a.to_a.zip(b.to_a).all? do |x, y|
237
+ x.equal?(y) || x == y || (x.is_a?(Float) && y.is_a?(Float) && x.nan? && y.nan?)
238
+ end
239
+ end
240
+ private_class_method :same_index_values?
241
+
242
+ # The frames that add rows, or the first frame when none does.
243
+ def self.frames_with_rows(frames)
244
+ with_rows = frames.reject { |f| f.nrow.zero? }
245
+ with_rows.empty? ? [frames.first] : with_rows
246
+ end
247
+ private_class_method :frames_with_rows
248
+
98
249
  # ---- index helpers -----------------------------------------------------
99
250
 
100
251
  # Index welded via CArray.meld (view). Every frame must agree: all
@@ -16,21 +16,25 @@ class CAFrame
16
16
  # column name => value; a scalar cell is a Ruby value, an N-D cell is a Ruby
17
17
  # Array, and a masked cell (UNDEF) is +nil+. Normalizing UNDEF -> nil and
18
18
  # CArray -> Array (unlike +each_row+, which yields the raw view with UNDEF /
19
- # CArray slices) is what makes it round-trip: +from_records(df.to_records)+
20
- # rebuilds the same typing, and the result is JSON-serializable.
19
+ # CArray slices) makes the result JSON-serializable and lets
20
+ # +from_records(df.to_records)+ give back the column names, the values and
21
+ # the mask. The rest is not in a record: +from_records+ rebuilds each type
22
+ # from the values (an int32 comes back int64, a time column as an object
23
+ # column of its elements), the index comes back as an ordinary first
24
+ # column, and a frame with no rows gives +[]+, which has no column names.
21
25
  def to_records
22
26
  each_row.map { |row| row.transform_values { |v| record_value(v) } }
23
27
  end
24
28
 
25
29
  # Hand the frame over as a CArray with the minimum work (memo §11.9): a
26
- # 2-D **view** of shape (nrow, nvar), one column per variable in column
30
+ # 2-D **view** of shape (nrow, ncol), one per column in column
27
31
  # order (§12-C). Nothing is materialised — the result is a +CAStack+ over
28
32
  # the stored columns, so reads gather from them and writes flow back
29
33
  # (§3.6). Call +copy+ on it for an independent, owned matrix; +CArray.tabulate+
30
34
  # is the eager sibling that builds one directly.
31
35
  #
32
36
  # Only same-shape scalar (1-D) columns qualify. An N-D column has no single
33
- # matrix form and raises — escape it per column with +df["name"]+. A mixed
37
+ # matrix form and raises — take it per column with +df["name"]+. A mixed
34
38
  # data type set is promoted to a common type (+result_type+, §12-F) through
35
39
  # lazy cast lanes, so the promotion costs no buffer either.
36
40
  #
@@ -48,12 +52,12 @@ class CAFrame
48
52
  if nd
49
53
  raise ArgumentError,
50
54
  "to_ca needs all-scalar (1-D) columns; #{nd.first.inspect} is " \
51
- "#{nd.last.ndim}-D — escape per column with df[name]"
55
+ "#{nd.last.ndim}-D — take it per column with df[name]"
52
56
  end
53
57
  refuse_unshared_columns(cols) if writable
54
58
  begin
55
59
  CArray.stack(cols, axis: 1)
56
- rescue ArgumentError, RuntimeError => e
60
+ rescue ArgumentError, TypeError, CArray::DataTypeError => e
57
61
  # The columns have no common type (a text or Face column beside a
58
62
  # numeric one). Point at the frame-level verb that gives them one.
59
63
  raise e.class,
@@ -87,9 +91,80 @@ class CAFrame
87
91
  if UNDEF.equal?(v)
88
92
  nil
89
93
  elsif v.is_a?(CArray)
90
- v.to_a.map { |e| UNDEF.equal?(e) ? nil : e }
94
+ record_value(v.to_a)
95
+ elsif v.is_a?(Array)
96
+ v.map { |e| record_value(e) }
91
97
  else
92
98
  v
93
99
  end
94
100
  end
95
101
  end
102
+
103
+ class CArray
104
+ # @overload to_frame(*names, axis: -1)
105
+ # Shows this array as a table: a CAFrame whose rows run along axis 0 and
106
+ # whose columns are the positions along `axis`. Column `j` is the view of
107
+ # `self` with `j` at `axis` and every other axis whole, so its shape is
108
+ # `self`'s shape without `axis`. A 2-D array gives 1-D columns, an array
109
+ # of more dimensions gives N-D columns:
110
+ #
111
+ # ```
112
+ # a = CArray.float64(3, 2).seq!
113
+ # df = a.to_frame("x", "y")
114
+ # df["y"] # => a[nil, 1], shape [3]
115
+ #
116
+ # img = CArray.uint8(480, 640, 3)
117
+ # img.to_frame("r", "g", "b")["r"].shape # => [480, 640]
118
+ # ```
119
+ #
120
+ # Nothing is copied: each column is a view, so writes through the frame
121
+ # reach `self` and writes to `self` show in the frame, values and mask
122
+ # alike. Use `copy` on the frame for an independent one.
123
+ #
124
+ # Without names the columns are called `"c0"`, `"c1"`, ... A 1-D array
125
+ # has no axis to take columns from; give it one with `a[nil, :_]`.
126
+ #
127
+ # @param names [Array<String>] column names, one per position along `axis`.
128
+ # @param axis [Integer] the axis the columns are taken along; it cannot
129
+ # be axis 0, which holds the rows.
130
+ # @return [CAFrame]
131
+ # @raise [TypeError] when a name is not a String or `axis` is not an
132
+ # Integer.
133
+ # @raise [ArgumentError] when `axis` is out of range or is the row axis,
134
+ # when the number of names differs from the length of `axis`, or when
135
+ # a name repeats.
136
+ def to_frame (*names, axis: -1)
137
+ axis = -1 if axis.nil?
138
+ if ndim < 2
139
+ raise ArgumentError,
140
+ "to_frame: a #{ndim}-D array has no axis to take columns from " \
141
+ "besides the row axis (axis 0); give it one with a[nil, :_]"
142
+ end
143
+ ax = normalize_axis(axis, "to_frame")
144
+ if ax == 0
145
+ raise ArgumentError,
146
+ "to_frame: axis #{axis} is the row axis (axis 0); columns are " \
147
+ "taken along another axis"
148
+ end
149
+ ncol = shape[ax]
150
+ if names.empty?
151
+ names = Array.new(ncol) { |j| "c#{j}" }
152
+ else
153
+ names.each { |name| CAFrame.__send__(:column_key, name, "to_frame") }
154
+ raise ArgumentError, "to_frame: a column name can not be empty" if names.include?("")
155
+ unless names.size == ncol
156
+ raise ArgumentError,
157
+ "to_frame: #{names.size} names for #{ncol} columns " \
158
+ "(axis #{axis} has length #{ncol})"
159
+ end
160
+ CAFrame.__send__(:refuse_repeated_names, names, "to_frame")
161
+ end
162
+ index = Array.new(ndim, nil)
163
+ columns = {}
164
+ names.each_with_index do |name, j|
165
+ index[ax] = j
166
+ columns[name] = self[*index]
167
+ end
168
+ CAFrame.new(columns)
169
+ end
170
+ end