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
@@ -16,15 +16,23 @@
16
16
 
17
17
  class CAFrame
18
18
  # Build a frame from an Array of row Hashes. Column set is the union of keys
19
- # in first-appearance order; keys are stringified. +types:+ casts named
20
- # columns afterward (same map / array-key forms as +cast+).
21
- def self.from_records(records, types: nil)
19
+ # in first-appearance order; keys are stringified, and two keys that
20
+ # stringify to one name (+:temp+ and +"temp"+) raise. +types:+ takes
21
+ # anything +cast+ takes as a map (a name => type map, :infer, or a map with
22
+ # +default:+) and casts the built frame with it, with +on_error:+ as for
23
+ # +cast+.
24
+ def self.from_records(records, types: nil, on_error: :mask)
22
25
  unless records.is_a?(Array) && records.all? { |r| r.is_a?(Hash) }
23
26
  raise ArgumentError, "from_records expects an Array of Hashes"
24
27
  end
25
28
  return new if records.empty?
26
29
 
27
30
  keys = record_key_union(records)
31
+ if keys.any? { |k| k.to_s.empty? }
32
+ raise ArgumentError, "from_records: a key names a column, and a column name can not be empty"
33
+ end
34
+ refuse_repeated_names(keys.map(&:to_s), "from_records", "the keys name the column",
35
+ hint: "a String key and a Symbol key spell it both ways; use one spelling")
28
36
  n = records.size
29
37
  cols = {}
30
38
  keys.each do |key|
@@ -33,7 +41,7 @@ class CAFrame
33
41
  end
34
42
 
35
43
  frame = new(cols)
36
- frame.cast(types) if types
44
+ frame.cast(types, on_error: on_error, option_name: :types) unless types.nil?
37
45
  frame
38
46
  end
39
47
 
@@ -54,15 +62,27 @@ class CAFrame
54
62
  end
55
63
  private_class_method :record_key_union
56
64
 
57
- # Arrange one column's values (Ruby objects, nil for missing) into a CArray.
58
- # All-Array cells of equal length -> N-D native column; numeric scalars ->
59
- # native scalar column (nil -> UNDEF); anything else -> object column.
65
+ # Arrange one column's values (Ruby objects, nil or UNDEF for missing) into
66
+ # a CArray: the one rule a frame types Ruby values by, wherever it takes
67
+ # them (CAFrame.new and df[name]= given an Array, from_records, the values
68
+ # a group's block or Proc gives). All-Array cells of equal length -> N-D
69
+ # native column; numeric scalars -> native scalar column (missing ->
70
+ # UNDEF); Strings -> a text column (CAString), as from_csv reads text;
71
+ # time and duration elements -> a CATime / CATimedelta column in the finest
72
+ # resolution among them; anything else -> object column.
60
73
  def self.build_record_column(values, n)
74
+ values = values.map { |v| UNDEF.equal?(v) ? nil : v }
61
75
  present = values.reject(&:nil?)
62
76
  return mask_missing(CArray.object(n) { values }) if present.empty?
63
77
 
64
78
  if present.all? { |v| v.is_a?(Array) || v.is_a?(CArray) }
65
79
  build_nd_column(values, present, n)
80
+ elsif present.all? { |v| v.is_a?(String) }
81
+ CArray.string(values)
82
+ elsif present.all? { |v| v.is_a?(CATime::Element) }
83
+ CArray.time(values)
84
+ elsif present.all? { |v| v.is_a?(CATimedelta::Element) }
85
+ build_duration_column(values, present, n)
66
86
  else
67
87
  type = numeric_leaf_type(present)
68
88
  col = CArray.object(n) { values }
@@ -71,6 +91,27 @@ class CAFrame
71
91
  end
72
92
  private_class_method :build_record_column
73
93
 
94
+ # A CATimedelta column of duration elements, in the finest of their
95
+ # resolutions. Durations that have no common resolution (a month beside a
96
+ # second) stay an object column of the elements.
97
+ def self.build_duration_column(values, present, n)
98
+ unit = begin
99
+ present.map(&:unit).reduce { |a, b| CATimeUnitAlgebra.finer(a, b) }
100
+ rescue ArgumentError
101
+ return mask_missing(CArray.object(n) { values })
102
+ end
103
+ ticks = CArray.int64(n)
104
+ values.each_with_index do |v, i|
105
+ ticks[i] =
106
+ if v.nil? then UNDEF
107
+ elsif v.unit == unit then v.value
108
+ else CA_INT64([v.value]).timedelta(unit: v.unit).to_unit(unit).ticks[0]
109
+ end
110
+ end
111
+ ticks.timedelta(unit: unit)
112
+ end
113
+ private_class_method :build_duration_column
114
+
74
115
  # A missing cell is UNDEF, not a Ruby nil sitting in a cell. to_records
75
116
  # writes a masked cell as nil, so nil on the way back in is the only
76
117
  # spelling a missing cell has; to_type does this conversion for a numeric
@@ -83,30 +124,86 @@ class CAFrame
83
124
  end
84
125
  private_class_method :mask_missing
85
126
 
86
- # Stack equal-length array cells into an (N, L) column via an object 2-D fill
87
- # + to_type (nil rows -> UNDEF, int/float by leaf). Ragged lengths or
88
- # non-numeric leaves fall back to a 1-D object column of the raw cells.
127
+ # Stack array cells of one shape into an (N, *shape) column via an object
128
+ # fill + to_type (nil rows -> UNDEF, int/float by leaf); a cell may itself
129
+ # be nested, an (N, L, M) column from [[..], [..]] cells. Cells of
130
+ # different shapes, nesting that is not rectangular, or non-numeric leaves
131
+ # fall back to a 1-D object column of the raw cells.
89
132
  def self.build_nd_column(values, present, n)
90
- lengths = present.map { |v| v.is_a?(CArray) ? v.shape[0] : v.size }
91
- len = lengths.first
92
- unless lengths.all? { |x| x == len }
133
+ shapes = present.map { |v| v.is_a?(CArray) ? v.shape : nested_shape(v) }
134
+ shape = shapes.first
135
+ unless shape && shapes.all? { |x| x == shape }
93
136
  return mask_missing(CArray.object(n) { values })
94
137
  end
95
138
 
96
- nested = values.map { |v| v.nil? ? Array.new(len) : (v.is_a?(CArray) ? v.to_a : v) }
97
- type = numeric_leaf_type(nested.flatten.compact)
98
- table = CArray.object(n, len) { nested }
139
+ blank = shape.reverse.inject(nil) { |cell, d| Array.new(d) { cell } }
140
+ # A missing leaf is nil or UNDEF (a masked cell of a CArray cell reads
141
+ # as UNDEF), the same two spellings a scalar cell has.
142
+ nested = values.map { |v| v.nil? ? blank : undef_to_nil(v.is_a?(CArray) ? v.to_a : v) }
143
+ leaves = nested.flatten.compact
144
+ # Cells given as numeric CArrays keep their data type, the one the
145
+ # cells have in common, when that type holds every cell's values;
146
+ # otherwise (int64 beside uint64, a large int64 beside float32), and
147
+ # for Ruby values, the column is typed by its leaves.
148
+ common = if present.all? { |v| v.is_a?(CArray) && !v.face? && v.numeric? }
149
+ CArray.result_type(*present)
150
+ end
151
+ type = if common && present.all? { |v| holds_values?(v, common) }
152
+ common
153
+ elsif leaves.empty?
154
+ nil
155
+ else
156
+ numeric_leaf_type(leaves)
157
+ end
158
+ table = CArray.object(n, *shape) { nested }
99
159
  type ? table.to_type(type) : mask_missing(table)
100
160
  end
101
161
  private_class_method :build_nd_column
102
162
 
103
- # :int64 if every value is an Integer, :float64 if all are Numeric (int/float
104
- # mix), otherwise nil (strings / DateTime / booleans / mixed -> keep object).
163
+ def self.undef_to_nil(v)
164
+ return v.map { |e| undef_to_nil(e) } if v.is_a?(Array)
165
+ UNDEF.equal?(v) ? nil : v
166
+ end
167
+ private_class_method :undef_to_nil
168
+
169
+ # The shape of a rectangular nested Array, or nil when it is not
170
+ # rectangular. A leaf (anything but an Array) has shape [].
171
+ def self.nested_shape(v)
172
+ return [] unless v.is_a?(Array)
173
+ inner = v.map { |e| nested_shape(e) }
174
+ return [v.size] if v.empty?
175
+ return nil unless inner.first && inner.all? { |x| x == inner.first }
176
+ [v.size, *inner.first]
177
+ end
178
+ private_class_method :nested_shape
179
+
180
+ # :int64 if every value is an Integer, :float64 for Integers and Floats,
181
+ # :cmplx128 when Complex values join them, otherwise nil (keep object).
182
+ # Other Numerics (Rational, BigDecimal) stay object: a float column would
183
+ # round them, and a Complex one has room for them only as floats too. So
184
+ # does an Integer an int64 cannot hold: it is kept as it is, not refused.
185
+ INT64_RANGE = (-2**63..2**63 - 1)
186
+ private_constant :INT64_RANGE
187
+
188
+ # Whether converting the numeric CArray +cell+ to +data_type+ keeps every
189
+ # value (a NaN stays a NaN).
190
+ def self.holds_values?(cell, data_type)
191
+ return true if cell.data_type == data_type
192
+ # coerce: the type CArray.result_type chose for the cells, tried on each
193
+ cell.to_type(data_type).to_a.flatten.zip(cell.to_a.flatten).all? do |a, b|
194
+ a == b || (a.respond_to?(:nan?) && b.respond_to?(:nan?) && a.nan? && b.nan?)
195
+ end
196
+ end
197
+ private_class_method :holds_values?
198
+
105
199
  def self.numeric_leaf_type(values)
200
+ return nil if values.any? { |v| v.is_a?(Integer) && !INT64_RANGE.cover?(v) }
106
201
  if values.all? { |v| v.is_a?(Integer) }
107
202
  :int64
108
- elsif values.all? { |v| v.is_a?(Numeric) }
203
+ elsif values.all? { |v| v.is_a?(Integer) || v.is_a?(Float) }
109
204
  :float64
205
+ elsif values.all? { |v| v.is_a?(Integer) || v.is_a?(Float) || v.is_a?(Complex) }
206
+ :cmplx128
110
207
  end
111
208
  end
112
209
  private_class_method :numeric_leaf_type
@@ -0,0 +1,455 @@
1
+ # CAFrame long <-> wide reshaping: +pivot+ and +melt+.
2
+ #
3
+ # Both are compositions of landed addressing primitives, like join:
4
+ #
5
+ # pivot the distinct keys come from +unique(sort: true)+, each row's
6
+ # position on them from +locate_addr+, and each output column is a
7
+ # +project+ of the value column through an inverse address (a cell
8
+ # no row reached stays UNDEF). The result is a new frame;
9
+ # +pivot_grid+ gathers the same cells into one 2-D CArray.
10
+ # melt the value columns are welded with +CArray.meld+ and each id column
11
+ # is welded with itself, so the result is a view frame: a write to
12
+ # its value column lands in the wide frame it came from.
13
+
14
+ class CAFrame
15
+ # Spread a long frame into a wide one. Each distinct value of the +index+
16
+ # key becomes a row, each distinct value of the +columns+ key becomes a
17
+ # column, and the cell at their crossing holds +values+ from the row that
18
+ # carried that pair.
19
+ #
20
+ # long = CAFrame.new("time" => t, "station" => s, "temp" => x)
21
+ # wide = long.pivot(index: "time", columns: "station", values: "temp")
22
+ # wide["tokyo"] # temperature at tokyo, one cell per time
23
+ #
24
+ # Both keys are sorted ascending. The +index+ key becomes the result's
25
+ # index and names its row axis; the +columns+ key values become column
26
+ # names through +to_s+. A pair no row carries is UNDEF, and so is a pair
27
+ # whose value cell is masked. A row whose +index+ or +columns+ key is
28
+ # masked belongs to no cell and is left out.
29
+ #
30
+ # +index:+ and +columns:+ are column names; +index:+ may also name the
31
+ # frame's index (its row axis name). A value column may have trailing
32
+ # dimensions, which each output column keeps.
33
+ #
34
+ # +values:+ may be an Array of column names. Every one of them is spread
35
+ # over the same rows and columns, and the output columns are named
36
+ # +"<value>_<label>"+, all of the first value's columns before the next's:
37
+ #
38
+ # long.pivot(index: "time", columns: "station", values: ["temp", "rh"])
39
+ # # temp_osaka, temp_tokyo, rh_osaka, rh_tokyo
40
+ #
41
+ # Without +aggregate:+, two rows with the same pair raise: pivot places
42
+ # values, it does not combine them. With +aggregate:+ (a reduction name
43
+ # such as +:mean+, +:sum+, +:max+ or +:count+) every pair holds that
44
+ # reduction over the rows that carry it, computed by +group_by_category+;
45
+ # masked values are left out of it as in any reduction, so a pair whose
46
+ # values are all masked is UNDEF. A pair no row carries stays UNDEF
47
+ # whatever the reduction, +:count+ included. +aggregate:+ needs
48
+ # one-dimensional value columns.
49
+ #
50
+ # long.pivot(index: "day", columns: "station", values: "temp", aggregate: :mean)
51
+ #
52
+ # For the same cells as one CArray rather than named columns, see
53
+ # {#pivot_grid}.
54
+ #
55
+ # @param index [String] key whose values become the rows.
56
+ # @param columns [String] key whose values become the columns.
57
+ # @param values [String, Array<String>] column(s) whose cells fill the
58
+ # result.
59
+ # @param aggregate [Symbol, nil] reduction applied to the rows of each pair.
60
+ # @return [CAFrame] a new frame, not a view of +self+.
61
+ # @raise [ArgumentError] on a repeated pair without +aggregate:+, on an
62
+ # unknown reduction or a multi-dimensional value column with it, or when
63
+ # two output columns would share a name.
64
+ def pivot(index:, columns:, values:, aggregate: nil)
65
+ plan = pivot_plan(column_key(index, "pivot"), column_key(columns, "pivot"), aggregate, "pivot")
66
+ labels = Array.new(plan[:nc]) { |j| plan[:clab][j].to_s }
67
+ if labels.include?("") && !values.is_a?(Array)
68
+ raise ArgumentError,
69
+ "pivot: a value of columns: #{columns.inspect} is the empty string, " \
70
+ "which cannot name a column; recode it first"
71
+ end
72
+ value_names = values.is_a?(Array) ? column_keys(values, "pivot") : nil
73
+ names =
74
+ if value_names
75
+ value_names.flat_map { |v| labels.map { |l| "#{v}_#{l}" } }
76
+ else
77
+ labels
78
+ end
79
+ CAFrame.__send__(:refuse_repeated_names, names, "pivot", "the output columns would name")
80
+
81
+ source = plan[:source].reshape(plan[:nr], plan[:nc])
82
+ cols = {}
83
+ (value_names || [column_key(values, "pivot")]).each_with_index do |value, k|
84
+ val = pivot_value(plan, value)
85
+ plan[:nc].times do |j|
86
+ cols[names[k * plan[:nc] + j]] = project_rows(val, source[nil, j])
87
+ end
88
+ end
89
+ CAFrame.new(cols, axis_name: plan[:index], index: plan[:rlab])
90
+ end
91
+
92
+ # The cells of {#pivot} as one CArray: rows are the sorted +index+ key,
93
+ # columns the sorted +columns+ key, and any trailing dimensions of the
94
+ # value column follow. Returns the grid together with the two key arrays
95
+ # that label its axes, so it destructures:
96
+ #
97
+ # temp, times, stations = long.pivot_grid(index: "time", columns: "station",
98
+ # values: "temp")
99
+ # temp.shape # => [times.elements, stations.elements]
100
+ # temp.mean(axis: 0) # mean per station
101
+ #
102
+ # Missing pairs, masked keys, repeats and +aggregate:+ behave as in
103
+ # {#pivot}. The grid keeps the value column's data type, Face included.
104
+ #
105
+ # @param index [String] key whose values label axis 0.
106
+ # @param columns [String] key whose values label axis 1.
107
+ # @param values [String] column whose cells fill the grid.
108
+ # @param aggregate [Symbol, nil] reduction applied to the rows of each pair.
109
+ # @return [Array(CArray, CArray, CArray)] the grid, the axis-0 key values
110
+ # and the axis-1 key values; the grid is new, not a view of +self+.
111
+ # @raise [ArgumentError] as {#pivot}.
112
+ def pivot_grid(index:, columns:, values:, aggregate: nil)
113
+ plan = pivot_plan(column_key(index, "pivot_grid"), column_key(columns, "pivot_grid"), aggregate, "pivot_grid")
114
+ val = pivot_value(plan, column_key(values, "pivot_grid"))
115
+ grid = project_rows(val, plan[:source]).reshape(plan[:nr], plan[:nc], *val.shape[1..])
116
+ [grid, plan[:rlab], plan[:clab]]
117
+ end
118
+
119
+ # The distinct values of a pivot key, ascending. Keys that cannot be put
120
+ # in order (nil beside numbers, numbers beside strings) are refused,
121
+ # naming the key.
122
+ private def pivot_sorted_keys(key, kw, name, verb)
123
+ # The keys label rows or columns, so they show the key column's
124
+ # attributes, as a group index does.
125
+ CAFrame.__send__(:key_labels, key, key.unique(sort: true))
126
+ rescue ArgumentError => e
127
+ why = e.message[/\A[^:]+: (comparison of .+ failed)\z/, 1] or raise
128
+ raise ArgumentError,
129
+ "#{verb}: #{kw}: #{name.inspect} holds values that cannot be put in order " \
130
+ "(#{why}); its keys are sorted to make the " \
131
+ "#{kw == "index" ? "rows" : "columns"}"
132
+ end
133
+
134
+ # Everything pivot needs that does not depend on the value column: the
135
+ # sorted keys and, for each of the nr * nc cells, the address of the row
136
+ # (or with +aggregate:+, of the reduced pair) that fills it.
137
+ private def pivot_plan(index, columns, aggregate, verb)
138
+ CAFrame.__send__(:refuse_repeated_names, [index, columns], verb, "index: and columns: name")
139
+ rkey = key_column(index, verb)
140
+ ckey = key_column(columns, verb)
141
+ rlab = pivot_sorted_keys(rkey, "index", index, verb)
142
+ clab = pivot_sorted_keys(ckey, "columns", columns, verb)
143
+ nr = rlab.elements
144
+ nc = clab.elements
145
+
146
+ r = rkey.locate_addr(rlab)
147
+ c = ckey.locate_addr(clab)
148
+ rlab = pivot_key_kind(rkey, rlab)
149
+ clab = pivot_key_kind(ckey, clab)
150
+ placed = r.is_not_masked & c.is_not_masked
151
+ cell = r[placed] * nc + c[placed]
152
+ source = CArray.int64(nr * nc)
153
+ source[] = UNDEF
154
+ plan = { index: index, rlab: rlab, clab: clab, nr: nr, nc: nc,
155
+ placed: placed, aggregate: aggregate, source: source }
156
+ if aggregate
157
+ unless aggregate.is_a?(Symbol)
158
+ raise ArgumentError, "pivot: aggregate: takes a reduction name (got #{aggregate.inspect})"
159
+ end
160
+ pairs = cell.categorize(sort_labels: true)
161
+ plan[:pairs] = pairs
162
+ source[CA_INT64(pairs.labels)] = CArray.int64(pairs.labels.size).seq
163
+ else
164
+ if cell.nunique != cell.elements
165
+ # names-check: two rows for one cell of the grid
166
+ first = cell.to_a.tally.find { |_, count| count > 1 }.first
167
+ raise ArgumentError,
168
+ "pivot: more than one row for #{index}=#{rlab[first / nc].inspect}, " \
169
+ "#{columns}=#{clab[first % nc].inspect}; pass aggregate: to combine them"
170
+ end
171
+ source[cell] = CArray.int64(nrow).seq[placed]
172
+ end
173
+ plan
174
+ end
175
+
176
+ # The distinct values of a categorical key come back as its label values;
177
+ # the keys that label the result are a categorical again, with the key's
178
+ # labels, as a group index keeps a categorical key.
179
+ private def pivot_key_kind(key, sorted)
180
+ return sorted unless key.is_a?(CACategorical)
181
+ CAFrame.__send__(:key_labels, key,
182
+ sorted.to_type(:object).categorize(labels: key.labels))
183
+ end
184
+
185
+ # The column the plan's addresses point into: the value column itself, or
186
+ # with +aggregate:+ one reduced value per occupied pair.
187
+ private def pivot_value(plan, name)
188
+ val = self[name]
189
+ return val unless plan[:aggregate]
190
+ unless val.ndim == 1
191
+ raise ArgumentError,
192
+ "pivot: aggregate: needs one-dimensional value columns " \
193
+ "(#{name.inspect} has shape #{val.shape.inspect})"
194
+ end
195
+ groups = val[plan[:placed]].group_by_category(plan[:pairs])
196
+ unless groups.respond_to?(plan[:aggregate])
197
+ raise ArgumentError, "pivot: unknown reduction #{plan[:aggregate].inspect} for aggregate:"
198
+ end
199
+ groups.public_send(plan[:aggregate])
200
+ end
201
+
202
+ # Gather a wide frame into a long one. The +value_columns+ are stacked one
203
+ # after another into a single +value_name+ column, a +var_name+ column says
204
+ # which of them each row came from (a text column), and every +id+ column
205
+ # repeats once per value column.
206
+ #
207
+ # wide.melt(id: "time") # time / variable / value
208
+ #
209
+ # With no +value_columns+, every column that is not an id is melted. When
210
+ # the frame has an index it is carried as an id column named after the row
211
+ # axis. Masked cells stay masked.
212
+ #
213
+ # The result is a view frame: the value column is +CArray.meld+ of the
214
+ # melted columns and each id column is +CArray.meld+ of itself, so a write
215
+ # to the result lands in +self+. The value columns must therefore share
216
+ # one data type and one trailing shape; cast first when they do not.
217
+ #
218
+ # @param id [String, Array<String>] columns repeated alongside each value.
219
+ # @param value_columns [Array<String>, nil] columns to stack; default all
220
+ # non-id columns.
221
+ # @param var_name [String] name of the column holding the source names.
222
+ # @param value_name [String] name of the stacked value column.
223
+ # @return [CAFrame] a view frame of +nrow * value_columns.size+ rows.
224
+ # @raise [ArgumentError] when there is nothing to melt, or the value
225
+ # columns differ in data type.
226
+ def melt(id: [], value_columns: nil, var_name: "variable", value_name: "value")
227
+ ids = column_keys(Array(id), "melt")
228
+ ids.each { |name| self[name] }
229
+ vars = column_keys(value_columns || column_names - ids, "melt")
230
+ raise ArgumentError, "melt: no columns to melt" if vars.empty?
231
+ overlap = vars & ids
232
+ unless overlap.empty?
233
+ raise ArgumentError, "melt: #{overlap.inspect} named both as id and value column"
234
+ end
235
+ pieces = vars.map { |name| self[name] }
236
+ types = pieces.map(&:data_type).uniq
237
+ unless types.size == 1
238
+ raise ArgumentError,
239
+ "melt: value columns differ in data type (#{vars.zip(pieces.map(&:data_type)).to_h}); " \
240
+ "cast them to one type first"
241
+ end
242
+ shapes = pieces.map { |c| c.shape.drop(1) }.uniq
243
+ unless shapes.size == 1
244
+ raise ArgumentError,
245
+ "melt: value columns differ in shape after the row axis " \
246
+ "(#{vars.zip(pieces.map { |c| c.shape.drop(1) }).to_h}); " \
247
+ "they are stacked into one column, so they need one shape"
248
+ end
249
+
250
+ k = vars.size
251
+ out = {}
252
+ out[@axis_name] = CArray.meld(Array.new(k, @index)) if @index
253
+ ids.each { |name| out[name] = CArray.meld(Array.new(k, self[name])) }
254
+ [var_name, value_name].each do |name|
255
+ column_name_arg(name, "melt")
256
+ if out.key?(name)
257
+ raise ArgumentError, "melt: #{name.inspect} is already an id column"
258
+ end
259
+ end
260
+ n = nrow
261
+ out[var_name] = CArray.string(Array.new(n * k) { |i| vars[i / n] })
262
+ out[value_name] = CArray.meld(pieces)
263
+ CAFrame.new(out)
264
+ end
265
+
266
+ # Stack each group's rows into one row: the rows sharing a value of +by+
267
+ # become one row, and every other column becomes an N-D column whose new
268
+ # axis runs over the group's rows, in the order they are in the frame. A
269
+ # long table of one observation per row -- a station's readings at several
270
+ # levels -- becomes one row per station:
271
+ #
272
+ # long.stack_rows(by: "station")
273
+ # # station level temp
274
+ # # tokyo [1000, 850, 500] [15.0, 5.0, -20.0]
275
+ #
276
+ # The key becomes the index, as in +group_by+. A column that tells the rows
277
+ # apart (+level+ here) is stacked like any other, so what position k means
278
+ # stays in the frame as a column of its own; the new axis carries no label.
279
+ # Sort first (+sort_by+) when the rows are to be stacked in another order.
280
+ #
281
+ # Without +on:+ the rows are stacked by position, so every group has to
282
+ # have the same number of rows: filling a short group with UNDEF at its end
283
+ # would put its values at the wrong positions (a station missing one level
284
+ # would have the next level's value in its place). With +on:+ a column
285
+ # says which rows go together: position k is the k-th value of that column
286
+ # in the order it first appears in the frame, a group without a row for it
287
+ # has UNDEF there, as +pivot+ leaves a missing cell, and a group with two
288
+ # rows for it is refused. The +on:+ column is stacked too, the same values
289
+ # in every row. A row whose key or +on:+ value is masked belongs nowhere
290
+ # and is refused. The frame's own index, if it has one, is stacked as a
291
+ # column named after the row axis. A frame with no column besides the key
292
+ # (and no index) has nothing to stack and is refused. +unstack_rows+ goes
293
+ # the other way; +stack_rows+ on its result, by the same keys, gives this
294
+ # frame back.
295
+ #
296
+ # Without +on:+ the result is a view, and writes reach this frame. With
297
+ # +on:+ it is a new frame, since the missing cells are new.
298
+ #
299
+ # long.stack_rows(by: "station", on: "level")
300
+ #
301
+ # @param by [String, CArray, Array] the keys, as for +group_by+: column
302
+ # names, or a CArray of one value per row (a day computed from a time
303
+ # column, say), or several of them.
304
+ # @param on [String, nil] the column whose values line the rows up.
305
+ # @return [CAFrame] one row per group, indexed by the group labels.
306
+ def stack_rows(by:, on: nil)
307
+ keys = (by.is_a?(Array) ? by : [by]).map { |k| k.is_a?(Symbol) ? column_key(k, "stack_rows") : k }
308
+ raise ArgumentError, "stack_rows: by: names no key" if keys.empty?
309
+ CAFrame.__send__(:refuse_repeated_names, keys.grep(String), "stack_rows", "by: names")
310
+ frame = @index ? CAFrame.new({ @axis_name => @index }.merge(@columns)) : self
311
+ keys.each { |k| frame.__send__(:key_column, k, "stack_rows") }
312
+ # With nothing but the key there is no column to stack, and the result
313
+ # would keep no record of how many rows each group had.
314
+ if (frame.column_names - keys).empty?
315
+ raise ArgumentError, "stack_rows: the frame has no column besides the key to stack"
316
+ end
317
+ if keys.size != 1 || !keys.first.is_a?(String)
318
+ if frame.column_names.include?("group") && !keys.include?("group")
319
+ raise ArgumentError,
320
+ "stack_rows: the result's rows are named \"group\" when the key is not " \
321
+ "one column, and \"group\" is a column; rename it first"
322
+ end
323
+ end
324
+ if on
325
+ on = column_name_arg(on, "stack_rows")
326
+ raise KeyError, "stack_rows: no column #{on.inspect}" unless frame.column_names.include?(on)
327
+ return stack_rows_on(frame, keys, on)
328
+ end
329
+ grouped = frame.group_by(*keys)
330
+ perm = grouped.__send__(:group_perm)
331
+ bounds = grouped.__send__(:group_bounds).to_a
332
+ sizes = bounds.each_cons(2).map { |a, b| b - a }
333
+ stacked = sizes.sum
334
+ if stacked != frame.nrow
335
+ raise ArgumentError,
336
+ "stack_rows: every row has to belong to a group; " \
337
+ "the key has no value for #{frame.nrow - stacked} of #{frame.nrow} rows"
338
+ end
339
+ # names-check: equal sizes, not names
340
+ unless sizes.uniq.size <= 1
341
+ # One group of each size, so the message says where to look.
342
+ examples = sizes.each_with_index.uniq { |n, _| n }.map do |n, g|
343
+ "#{group_key_desc(keys, grouped.labels[g])} has #{n}"
344
+ end
345
+ raise ArgumentError,
346
+ "stack_rows: the groups have different numbers of rows " \
347
+ "(#{examples.join(', ')}); stacked rows need the same number in every group"
348
+ end
349
+ count = sizes.first || 0
350
+ perm = stacked.zero? ? CArray.int64(0) : perm[0...stacked]
351
+ cols = {}
352
+ frame.columns.zip(frame.column_names).each do |col, name|
353
+ next if keys.include?(name)
354
+ rows = col[perm, *([nil] * (col.ndim - 1))]
355
+ cols[name] = rows.reshape(sizes.size, count, *col.shape[1..])
356
+ end
357
+ axis = keys.size == 1 && keys.first.is_a?(String) ? keys.first : "group"
358
+ CAFrame.new(cols, axis_name: axis, index: grouped.__send__(:label_index))
359
+ end
360
+
361
+ # A group's key written as name=value, one pair per key column (a key
362
+ # given as a CArray has no name and is written "key").
363
+ private def group_key_desc(keys, label)
364
+ values = keys.size == 1 ? [label] : label
365
+ keys.zip(values).map { |k, v| "#{k.is_a?(String) ? k : "key"}=#{v.inspect}" }.join(", ")
366
+ end
367
+
368
+ private def stack_rows_on(frame, keys, on)
369
+ if keys.include?(on)
370
+ raise ArgumentError, "stack_rows: #{on.inspect} is a key; on: names another column"
371
+ end
372
+ place = frame.__send__(:key_column, on, "stack_rows")
373
+ grouped = frame.group_by(*keys)
374
+ bounds = grouped.__send__(:group_bounds).to_a
375
+ groups = bounds.size - 1
376
+ if bounds.last != frame.nrow
377
+ raise ArgumentError,
378
+ "stack_rows: every row has to belong to a group; " \
379
+ "the key has no value for #{frame.nrow - bounds.last} of #{frame.nrow} rows"
380
+ end
381
+ if place.has_mask? && place.count_masked > 0
382
+ raise ArgumentError,
383
+ "stack_rows: every row has to have a place; " \
384
+ "#{on.inspect} has no value for #{place.count_masked} of #{frame.nrow} rows"
385
+ end
386
+ positions = place.categorize # labels in order of first appearance
387
+ count = positions.labels.size
388
+ # A row's group is its code in the grouping (the groups run in code order).
389
+ group_of = grouped.__send__(:group_codes).int64
390
+ slot = group_of * count + positions.codes.int64
391
+ rows = CArray.int64(groups * count)
392
+ rows[] = UNDEF
393
+ seen = CArray.int64(groups * count)
394
+ seen.scatter_add!(slot, 1) if frame.nrow > 0
395
+ if frame.nrow > 0 && seen.max > 1
396
+ at = seen.gt(1).where[0]
397
+ raise ArgumentError,
398
+ "stack_rows: more than one row for #{group_key_desc(keys, grouped.labels[at / count])}, " \
399
+ "#{on}=#{positions.labels[at % count].inspect}"
400
+ end
401
+ rows[slot] = CArray.int64(frame.nrow).seq if frame.nrow > 0
402
+ # The row where each value of on: first appears.
403
+ first = CArray.int64(count)
404
+ first[] = frame.nrow
405
+ first.scatter_min!(positions.codes.int64, CArray.int64(frame.nrow).seq) if frame.nrow > 0
406
+ cols = {}
407
+ frame.column_names.each do |name|
408
+ next if keys.include?(name)
409
+ col = frame[name]
410
+ src = name == on ? (CArray.int64(groups * count).seq % count).then { |k| first[k] } : rows
411
+ cols[name] = gather_rows(col, src).reshape(groups, count, *col.shape[1..])
412
+ end
413
+ axis = keys.size == 1 && keys.first.is_a?(String) ? keys.first : "group"
414
+ CAFrame.new(cols, axis_name: axis, index: grouped.__send__(:label_index))
415
+ end
416
+
417
+ # The rows +rows+ names of +col+, a masked entry giving a masked row.
418
+ private def gather_rows(col, rows)
419
+ inner = col.shape[1..].inject(1, :*)
420
+ return col.project(rows) if inner == 1
421
+ addr = (rows[nil, :_] * inner + CArray.int64(inner).seq[:_, nil]).reshape(rows.elements * inner)
422
+ col.project(addr)
423
+ end
424
+
425
+ # Unstack the rows +stack_rows+ stacked: every N-D column is spread back
426
+ # into rows along its first trailing axis, and every other column, and the
427
+ # index, repeats once per position. The N-D columns have to agree on the
428
+ # length of that axis. The index stays the index; +reset_index+ turns it
429
+ # back into a column.
430
+ #
431
+ # @return [CAFrame] a frame of +nrow * length+ rows.
432
+ def unstack_rows
433
+ stacked = @columns.select { |_, c| c.ndim >= 2 }
434
+ if stacked.empty?
435
+ raise ArgumentError, "unstack_rows: the frame has no N-D column to unstack"
436
+ end
437
+ lengths = stacked.transform_values { |c| c.shape[1] }
438
+ # names-check: equal lengths, not names
439
+ unless lengths.values.uniq.size == 1
440
+ raise ArgumentError,
441
+ "unstack_rows: the N-D columns differ in length along their first axis #{lengths}"
442
+ end
443
+ count = lengths.values.first
444
+ repeat = CArray.int64(nrow * count).seq / count
445
+ cols = @columns.to_h do |name, col|
446
+ if col.ndim >= 2
447
+ [name, col.reshape(nrow * count, *col.shape[2..])]
448
+ else
449
+ [name, col[repeat]]
450
+ end
451
+ end
452
+ CAFrame.new(cols, axis_name: @axis_name, index: @index && @index[repeat])
453
+ end
454
+
455
+ end