carray 3.0.1 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +824 -0
  3. data/README.md +5 -6
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_dispatch.c +258 -4
  6. data/ext/ca_axis_group.c +368 -184
  7. data/ext/ca_binop_dispatch.c +54 -32
  8. data/ext/ca_binop_dispatch.h +1 -0
  9. data/ext/ca_categorical_iterator.c +90 -41
  10. data/ext/ca_compare.c +8 -2
  11. data/ext/ca_for_buffer.h +21 -68
  12. data/ext/ca_group_iter.c +27 -11
  13. data/ext/ca_kernel_iterator.c +960 -245
  14. data/ext/ca_kernel_iterator.h +352 -181
  15. data/ext/ca_monop_dispatch.c +4 -4
  16. data/ext/ca_monop_dispatch.h +4 -0
  17. data/ext/ca_obj_array.c +194 -42
  18. data/ext/ca_obj_bincmp.c +152 -121
  19. data/ext/ca_obj_binop.c +107 -210
  20. data/ext/ca_obj_bitarray.c +37 -24
  21. data/ext/ca_obj_bitfield.c +95 -62
  22. data/ext/ca_obj_block.c +5 -4
  23. data/ext/ca_obj_byte_swap.c +40 -40
  24. data/ext/ca_obj_const_string.c +180 -50
  25. data/ext/ca_obj_face.c +428 -62
  26. data/ext/ca_obj_face.h +107 -47
  27. data/ext/ca_obj_fake.c +21 -50
  28. data/ext/ca_obj_farray.c +1 -0
  29. data/ext/ca_obj_field.c +16 -3
  30. data/ext/ca_obj_fixlen_string.c +39 -26
  31. data/ext/ca_obj_grid.c +181 -124
  32. data/ext/ca_obj_meld.c +235 -55
  33. data/ext/ca_obj_moncmp.c +61 -61
  34. data/ext/ca_obj_monop.c +125 -107
  35. data/ext/ca_obj_object.c +551 -233
  36. data/ext/ca_obj_record.c +51 -16
  37. data/ext/ca_obj_reduce.c +5 -8
  38. data/ext/ca_obj_refer.c +117 -33
  39. data/ext/ca_obj_remap.c +57 -17
  40. data/ext/ca_obj_repeat.c +21 -5
  41. data/ext/ca_obj_roll.c +115 -11
  42. data/ext/ca_obj_select.c +88 -75
  43. data/ext/ca_obj_select_axis.c +58 -60
  44. data/ext/ca_obj_shift.c +22 -102
  45. data/ext/ca_obj_stack.c +223 -123
  46. data/ext/ca_obj_stride.c +267 -35
  47. data/ext/ca_obj_string.c +25 -22
  48. data/ext/ca_obj_tile.c +32 -16
  49. data/ext/ca_obj_time.c +23 -25
  50. data/ext/ca_obj_timedelta.c +21 -23
  51. data/ext/ca_obj_transpose.c +12 -3
  52. data/ext/ca_obj_triop.c +110 -117
  53. data/ext/ca_obj_window.c +157 -29
  54. data/ext/ca_op_ipower.c +25 -13
  55. data/ext/ca_op_powi.h +57 -30
  56. data/ext/ca_pow5_table.h +656 -0
  57. data/ext/ca_rng_normal.h +42 -0
  58. data/ext/ca_rng_xoshiro256pp.h +105 -0
  59. data/ext/ca_sort_kernels.h +70 -0
  60. data/ext/ca_sweep_engine.c +385 -198
  61. data/ext/ca_sweep_engine.h +31 -23
  62. data/ext/ca_triop_dispatch.c +4 -1
  63. data/ext/ca_triop_dispatch.h +5 -2
  64. data/ext/caframe_csv_reader.c +463 -0
  65. data/ext/carray.h +90 -28
  66. data/ext/carray_access.c +487 -231
  67. data/ext/carray_address_basis.c +731 -0
  68. data/ext/carray_attribute.c +45 -3
  69. data/ext/carray_bincount.c +32 -2
  70. data/ext/carray_broadcast.c +49 -17
  71. data/ext/carray_call_cfunc.c +667 -511
  72. data/ext/carray_cast.c +1556 -247
  73. data/ext/carray_cast_func.rb +34 -21
  74. data/ext/carray_conversion.c +197 -73
  75. data/ext/carray_copy.c +84 -38
  76. data/ext/carray_core.c +591 -119
  77. data/ext/carray_count.c +148 -96
  78. data/ext/carray_element.c +18 -27
  79. data/ext/carray_factorize.c +604 -291
  80. data/ext/carray_generate.c +76 -40
  81. data/ext/carray_histogram.c +152 -94
  82. data/ext/carray_hold.c +2 -1
  83. data/ext/carray_index_classifier.c +63 -96
  84. data/ext/carray_internal.h +400 -1
  85. data/ext/carray_lazy.c +834 -79
  86. data/ext/carray_loop.c +88 -66
  87. data/ext/carray_mask.c +705 -324
  88. data/ext/carray_math_kernel.h +1 -0
  89. data/ext/carray_median_percentile.c +263 -63
  90. data/ext/carray_memory_view.c +254 -248
  91. data/ext/carray_operator.c +1093 -363
  92. data/ext/carray_order.c +133 -69
  93. data/ext/carray_partition.c +102 -56
  94. data/ext/carray_random.c +582 -204
  95. data/ext/carray_scatter.c +308 -65
  96. data/ext/carray_segment.c +257 -0
  97. data/ext/carray_slab.c +183 -70
  98. data/ext/carray_sort.c +235 -240
  99. data/ext/carray_sort_kernel.c +8 -1
  100. data/ext/carray_struct.c +99 -21
  101. data/ext/carray_test.c +167 -64
  102. data/ext/carray_time_text.c +1182 -0
  103. data/ext/carray_utils.c +363 -98
  104. data/ext/extconf.rb +27 -2
  105. data/ext/mk_call_cfunc.rb +103 -118
  106. data/ext/mk_pow5_table.rb +35 -0
  107. data/ext/mkkernel.rb +1349 -851
  108. data/ext/ruby_carray.c +17 -2
  109. data/ext/version.h +4 -4
  110. data/ext/version.rb +4 -2
  111. data/lib/carray/attribute.rb +39 -0
  112. data/lib/carray/autoload_carray.rb +24 -3
  113. data/lib/carray/autoload_method_extension.rb +18 -0
  114. data/lib/carray/axis_group.rb +245 -20
  115. data/lib/carray/basics.rb +95 -25
  116. data/lib/carray/bincount_nd.rb +6 -0
  117. data/lib/carray/block_iterator.rb +200 -54
  118. data/lib/carray/boolean_reduce.rb +21 -29
  119. data/lib/carray/categorical.rb +244 -59
  120. data/lib/carray/categorical_iterator.rb +391 -757
  121. data/lib/carray/complex.rb +36 -29
  122. data/lib/carray/conditional.rb +94 -7
  123. data/lib/carray/const_string.rb +177 -27
  124. data/lib/carray/construct.rb +40 -0
  125. data/lib/carray/data_type_extension.rb +3 -0
  126. data/lib/carray/data_type_limits.rb +91 -0
  127. data/lib/carray/fixlen_string.rb +1 -1
  128. data/lib/carray/frame/concat.rb +157 -6
  129. data/lib/carray/frame/convert.rb +13 -7
  130. data/lib/carray/frame/csv_parser.rb +590 -76
  131. data/lib/carray/frame/describe.rb +79 -0
  132. data/lib/carray/frame/frame.rb +205 -35
  133. data/lib/carray/frame/group.rb +199 -40
  134. data/lib/carray/frame/io.rb +452 -60
  135. data/lib/carray/frame/join.rb +134 -46
  136. data/lib/carray/frame/records.rb +54 -19
  137. data/lib/carray/frame/reshape.rb +448 -0
  138. data/lib/carray/frame/sort.rb +26 -2
  139. data/lib/carray/frame/verbs.rb +726 -56
  140. data/lib/carray/frame.rb +2 -0
  141. data/lib/carray/fuse_source.rb +111 -2
  142. data/lib/carray/fusion.rb +229 -38
  143. data/lib/carray/inspect.rb +71 -20
  144. data/lib/carray/iterator.rb +157 -2
  145. data/lib/carray/lazy.rb +54 -67
  146. data/lib/carray/mask_gap_fill.rb +66 -50
  147. data/lib/carray/math.rb +32 -19
  148. data/lib/carray/meld_reduce.rb +40 -7
  149. data/lib/carray/methods/align_addr.rb +8 -4
  150. data/lib/carray/methods/bincount.rb +2 -2
  151. data/lib/carray/methods/bit_string.rb +14 -3
  152. data/lib/carray/methods/composition.rb +16 -10
  153. data/lib/carray/methods/discovery_along.rb +74 -0
  154. data/lib/carray/methods/factorize.rb +50 -0
  155. data/lib/carray/methods/gather_nd.rb +13 -2
  156. data/lib/carray/methods/insert_block.rb +9 -14
  157. data/lib/carray/methods/is_in.rb +130 -6
  158. data/lib/carray/methods/join.rb +1 -1
  159. data/lib/carray/methods/locate_addr.rb +235 -21
  160. data/lib/carray/methods/mask_duplicates.rb +35 -1
  161. data/lib/carray/methods/mode.rb +18 -1
  162. data/lib/carray/methods/nunique.rb +22 -1
  163. data/lib/carray/methods/repeat.rb +91 -0
  164. data/lib/carray/methods/resize.rb +21 -23
  165. data/lib/carray/methods/snap.rb +34 -12
  166. data/lib/carray/methods/unique.rb +52 -3
  167. data/lib/carray/record_order.rb +208 -0
  168. data/lib/carray/rng.rb +86 -0
  169. data/lib/carray/runtime.rb +14 -11
  170. data/lib/carray/segment_iterator.rb +990 -0
  171. data/lib/carray/serialize.rb +197 -27
  172. data/lib/carray/slab_iterator.rb +109 -40
  173. data/lib/carray/stack.rb +10 -4
  174. data/lib/carray/string_operation_extension.rb +9 -4
  175. data/lib/carray/struct.rb +142 -51
  176. data/lib/carray/struct_builder.rb +110 -14
  177. data/lib/carray/table.rb +2 -2
  178. data/lib/carray/time.rb +535 -105
  179. data/lib/carray/window_iterator.rb +259 -82
  180. data/lib/carray.rb +4 -2
  181. data/yard-stubs/ca_obj_array.rb +4 -4
  182. data/yard-stubs/ca_obj_bitarray.rb +7 -0
  183. data/yard-stubs/ca_obj_bitfield.rb +23 -12
  184. data/yard-stubs/ca_obj_block.rb +2 -7
  185. data/yard-stubs/ca_obj_fake.rb +1 -1
  186. data/yard-stubs/ca_obj_field.rb +1 -1
  187. data/yard-stubs/ca_obj_refer.rb +3 -3
  188. data/yard-stubs/ca_obj_roll.rb +0 -1
  189. data/yard-stubs/ca_obj_stride.rb +10 -7
  190. data/yard-stubs/ca_obj_transpose.rb +1 -1
  191. data/yard-stubs/ca_obj_window.rb +11 -4
  192. data/yard-stubs/carray_access.rb +6 -5
  193. data/yard-stubs/carray_broadcast.rb +2 -2
  194. data/yard-stubs/carray_cast.rb +78 -43
  195. data/yard-stubs/carray_conversion.rb +13 -1
  196. data/yard-stubs/carray_copy.rb +8 -3
  197. data/yard-stubs/carray_core.rb +3 -83
  198. data/yard-stubs/carray_count.rb +16 -2
  199. data/yard-stubs/carray_lazy.rb +210 -1
  200. data/yard-stubs/carray_loop.rb +4 -1
  201. data/yard-stubs/carray_mask.rb +34 -10
  202. data/yard-stubs/carray_math.rb +1499 -4
  203. data/yard-stubs/carray_median_percentile.rb +26 -8
  204. data/yard-stubs/carray_order.rb +43 -82
  205. data/yard-stubs/carray_scatter.rb +10 -3
  206. data/yard-stubs/carray_segment.rb +72 -0
  207. data/yard-stubs/carray_slab.rb +11 -8
  208. data/yard-stubs/carray_sort.rb +50 -8
  209. data/yard-stubs/carray_test.rb +10 -0
  210. data/yard-stubs/carray_utils.rb +0 -26
  211. metadata +19 -19
  212. data/ext/ca_for_each_element.h +0 -239
  213. data/ext/carray_build_flags.h +0 -3
  214. data/ext/carray_config.h +0 -28
  215. data/ext/carray_kernels_bincmp.c +0 -4446
  216. data/ext/carray_kernels_binop.c +0 -11001
  217. data/ext/carray_kernels_init.c +0 -1131
  218. data/ext/carray_kernels_map.c +0 -3467
  219. data/ext/carray_kernels_moncmp.c +0 -2097
  220. data/ext/carray_kernels_monop.c +0 -18313
  221. data/ext/carray_kernels_reduce_aggregate.c +0 -25837
  222. data/ext/carray_kernels_reduce_boolean.c +0 -330
  223. data/ext/carray_kernels_reduce_cumulative.c +0 -14593
  224. data/ext/carray_kernels_reduce_extreme.c +0 -16948
  225. data/ext/carray_kernels_reduce_variance.c +0 -3910
  226. data/ext/carray_kernels_scan.c +0 -3693
  227. data/ext/carray_kernels_search.c +0 -32138
  228. data/ext/carray_kernels_sort.c +0 -10626
  229. data/ext/carray_kernels_triop.c +0 -1392
@@ -0,0 +1,448 @@
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(index.to_s, columns.to_s, aggregate, "pivot")
66
+ labels = Array.new(plan[:nc]) { |j| plan[:clab][j].to_s }
67
+ value_names = values.is_a?(Array) ? values.map(&:to_s) : nil
68
+ names =
69
+ if value_names
70
+ value_names.flat_map { |v| labels.map { |l| "#{v}_#{l}" } }
71
+ else
72
+ labels
73
+ end
74
+ dup = names.tally.select { |_, count| count > 1 }.keys
75
+ unless dup.empty?
76
+ raise ArgumentError, "pivot: output columns would share the names #{dup.inspect}"
77
+ end
78
+
79
+ source = plan[:source].reshape(plan[:nr], plan[:nc])
80
+ cols = {}
81
+ (value_names || [values.to_s]).each_with_index do |value, k|
82
+ val = pivot_value(plan, value)
83
+ plan[:nc].times do |j|
84
+ cols[names[k * plan[:nc] + j]] = project_rows(val, source[nil, j])
85
+ end
86
+ end
87
+ CAFrame.new(cols, axis_name: plan[:index], index: plan[:rlab])
88
+ end
89
+
90
+ # The cells of {#pivot} as one CArray: rows are the sorted +index+ key,
91
+ # columns the sorted +columns+ key, and any trailing dimensions of the
92
+ # value column follow. Returns the grid together with the two key arrays
93
+ # that label its axes, so it destructures:
94
+ #
95
+ # temp, times, stations = long.pivot_grid(index: "time", columns: "station",
96
+ # values: "temp")
97
+ # temp.shape # => [times.elements, stations.elements]
98
+ # temp.mean(axis: 0) # mean per station
99
+ #
100
+ # Missing pairs, masked keys, repeats and +aggregate:+ behave as in
101
+ # {#pivot}. The grid keeps the value column's data type, Face included.
102
+ #
103
+ # @param index [String] key whose values label axis 0.
104
+ # @param columns [String] key whose values label axis 1.
105
+ # @param values [String] column whose cells fill the grid.
106
+ # @param aggregate [Symbol, nil] reduction applied to the rows of each pair.
107
+ # @return [Array(CArray, CArray, CArray)] the grid, the axis-0 key values
108
+ # and the axis-1 key values; the grid is new, not a view of +self+.
109
+ # @raise [ArgumentError] as {#pivot}.
110
+ def pivot_grid(index:, columns:, values:, aggregate: nil)
111
+ plan = pivot_plan(index.to_s, columns.to_s, aggregate, "pivot_grid")
112
+ val = pivot_value(plan, values.to_s)
113
+ grid = project_rows(val, plan[:source]).reshape(plan[:nr], plan[:nc], *val.shape[1..])
114
+ [grid, plan[:rlab], plan[:clab]]
115
+ end
116
+
117
+ # The distinct values of a pivot key, ascending. Keys that cannot be put
118
+ # in order (nil beside numbers, numbers beside strings) are refused,
119
+ # naming the key.
120
+ private def pivot_sorted_keys(key, kw, name, verb)
121
+ key.unique(sort: true)
122
+ rescue ArgumentError => e
123
+ raise unless e.message.start_with?("sort:")
124
+ raise ArgumentError,
125
+ "#{verb}: #{kw}: #{name.inspect} holds values that cannot be put in order " \
126
+ "(#{e.message.sub(/\Asort: /, "")}); its keys are sorted to make the " \
127
+ "#{kw == "index" ? "rows" : "columns"}"
128
+ end
129
+
130
+ # Everything pivot needs that does not depend on the value column: the
131
+ # sorted keys and, for each of the nr * nc cells, the address of the row
132
+ # (or with +aggregate:+, of the reduced pair) that fills it.
133
+ private def pivot_plan(index, columns, aggregate, verb)
134
+ rkey = column_or_index(index)
135
+ ckey = column_or_index(columns)
136
+ { "index" => [index, rkey], "columns" => [columns, ckey] }.each do |kw, (name, key)|
137
+ next if key.ndim == 1
138
+ raise ArgumentError,
139
+ "#{verb}: #{kw}: #{name.inspect} has shape #{key.shape.inspect}, not one value per row"
140
+ end
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
+ placed = r.is_not_masked & c.is_not_masked
149
+ cell = r[placed] * nc + c[placed]
150
+ source = CArray.int64(nr * nc)
151
+ source[] = UNDEF
152
+ plan = { index: index, rlab: rlab, clab: clab, nr: nr, nc: nc,
153
+ placed: placed, aggregate: aggregate, source: source }
154
+ if aggregate
155
+ unless aggregate.is_a?(Symbol)
156
+ raise ArgumentError, "pivot: aggregate: takes a reduction name (got #{aggregate.inspect})"
157
+ end
158
+ pairs = cell.categorize(sort_labels: true)
159
+ plan[:pairs] = pairs
160
+ source[CA_INT64(pairs.labels)] = CArray.int64(pairs.labels.size).seq
161
+ else
162
+ if cell.nunique != cell.elements
163
+ first = cell.to_a.tally.find { |_, count| count > 1 }.first
164
+ raise ArgumentError,
165
+ "pivot: more than one row for #{index}=#{rlab[first / nc].inspect}, " \
166
+ "#{columns}=#{clab[first % nc].inspect}; pass aggregate: to combine them"
167
+ end
168
+ source[cell] = CArray.int64(nrow).seq[placed]
169
+ end
170
+ plan
171
+ end
172
+
173
+ # The column the plan's addresses point into: the value column itself, or
174
+ # with +aggregate:+ one reduced value per occupied pair.
175
+ private def pivot_value(plan, name)
176
+ val = self[name]
177
+ return val unless plan[:aggregate]
178
+ unless val.ndim == 1
179
+ raise ArgumentError,
180
+ "pivot: aggregate: needs one-dimensional value columns " \
181
+ "(#{name.inspect} has shape #{val.shape.inspect})"
182
+ end
183
+ groups = val[plan[:placed]].group_by_category(plan[:pairs])
184
+ unless groups.respond_to?(plan[:aggregate])
185
+ raise ArgumentError, "pivot: unknown reduction #{plan[:aggregate].inspect} for aggregate:"
186
+ end
187
+ groups.public_send(plan[:aggregate])
188
+ end
189
+
190
+ # Gather a wide frame into a long one. The +value_columns+ are stacked one
191
+ # after another into a single +value_name+ column, a +var_name+ column says
192
+ # which of them each row came from, and every +id+ column repeats once per
193
+ # value column.
194
+ #
195
+ # wide.melt(id: "time") # time / variable / value
196
+ #
197
+ # With no +value_columns+, every column that is not an id is melted. When
198
+ # the frame has an index it is carried as an id column named after the row
199
+ # axis. Masked cells stay masked.
200
+ #
201
+ # The result is a view frame: the value column is +CArray.meld+ of the
202
+ # melted columns and each id column is +CArray.meld+ of itself, so a write
203
+ # to the result lands in +self+. The value columns must therefore share
204
+ # one data type and one trailing shape; cast first when they do not.
205
+ #
206
+ # @param id [String, Array<String>] columns repeated alongside each value.
207
+ # @param value_columns [Array<String>, nil] columns to stack; default all
208
+ # non-id columns.
209
+ # @param var_name [String] name of the column holding the source names.
210
+ # @param value_name [String] name of the stacked value column.
211
+ # @return [CAFrame] a view frame of +nrow * value_columns.size+ rows.
212
+ # @raise [ArgumentError] when there is nothing to melt, or the value
213
+ # columns differ in data type.
214
+ def melt(id: [], value_columns: nil, var_name: "variable", value_name: "value")
215
+ ids = Array(id).map(&:to_s)
216
+ ids.each { |name| self[name] }
217
+ vars = (value_columns || column_names - ids).map(&:to_s)
218
+ raise ArgumentError, "melt: no columns to melt" if vars.empty?
219
+ overlap = vars & ids
220
+ unless overlap.empty?
221
+ raise ArgumentError, "melt: #{overlap.inspect} named both as id and value column"
222
+ end
223
+ pieces = vars.map { |name| self[name] }
224
+ types = pieces.map(&:data_type).uniq
225
+ unless types.size == 1
226
+ raise ArgumentError,
227
+ "melt: value columns differ in data type (#{vars.zip(pieces.map(&:data_type)).to_h}); " \
228
+ "cast them to one type first"
229
+ end
230
+ shapes = pieces.map { |c| c.shape.drop(1) }.uniq
231
+ unless shapes.size == 1
232
+ raise ArgumentError,
233
+ "melt: value columns differ in shape after the row axis " \
234
+ "(#{vars.zip(pieces.map { |c| c.shape.drop(1) }).to_h}); " \
235
+ "they are stacked into one column, so they need one shape"
236
+ end
237
+
238
+ k = vars.size
239
+ out = {}
240
+ out[@axis_name] = CArray.meld(Array.new(k, @index)) if @index
241
+ ids.each { |name| out[name] = CArray.meld(Array.new(k, self[name])) }
242
+ [var_name, value_name].each do |name|
243
+ if out.key?(name)
244
+ raise ArgumentError, "melt: #{name.inspect} is already an id column"
245
+ end
246
+ end
247
+ n = nrow
248
+ out[var_name] = CArray.object(n * k) { |i| vars[i / n] }
249
+ out[value_name] = CArray.meld(pieces)
250
+ CAFrame.new(out)
251
+ end
252
+
253
+ # Stack each group's rows into one row: the rows sharing a value of +by+
254
+ # become one row, and every other column becomes an N-D column whose new
255
+ # axis runs over the group's rows, in the order they are in the frame. A
256
+ # long table of one observation per row -- a station's readings at several
257
+ # levels -- becomes one row per station:
258
+ #
259
+ # long.stack_rows(by: "station")
260
+ # # station level temp
261
+ # # tokyo [1000, 850, 500] [15.0, 5.0, -20.0]
262
+ #
263
+ # The key becomes the index, as in +group_by+. A column that tells the rows
264
+ # apart (+level+ here) is stacked like any other, so what position k means
265
+ # stays in the frame as a column of its own; the new axis carries no label.
266
+ # Sort first (+sort_by+) when the rows are to be stacked in another order.
267
+ #
268
+ # Without +on:+ the rows are stacked by position, so every group has to
269
+ # have the same number of rows: filling a short group with UNDEF at its end
270
+ # would put its values at the wrong positions (a station missing one level
271
+ # would have the next level's value in its place). With +on:+ a column
272
+ # says which rows go together: position k is the k-th value of that column
273
+ # in the order it first appears in the frame, a group without a row for it
274
+ # has UNDEF there, as +pivot+ leaves a missing cell, and a group with two
275
+ # rows for it is refused. The +on:+ column is stacked too, the same values
276
+ # in every row. A row whose key or +on:+ value is masked belongs nowhere
277
+ # and is refused. The frame's own index, if it has one, is stacked as a
278
+ # column named after the row axis. A frame with no column besides the key
279
+ # (and no index) has nothing to stack and is refused. +unstack_rows+ goes
280
+ # the other way; +stack_rows+ on its result, by the same keys, gives this
281
+ # frame back.
282
+ #
283
+ # Without +on:+ the result is a view, and writes reach this frame. With
284
+ # +on:+ it is a new frame, since the missing cells are new.
285
+ #
286
+ # long.stack_rows(by: "station", on: "level")
287
+ #
288
+ # @param by [String, CArray, Array] the keys, as for +group_by+: column
289
+ # names, or a CArray of one value per row (a day computed from a time
290
+ # column, say), or several of them.
291
+ # @param on [String, nil] the column whose values line the rows up.
292
+ # @return [CAFrame] one row per group, indexed by the group labels.
293
+ def stack_rows(by:, on: nil)
294
+ keys = (by.is_a?(Array) ? by : [by]).map { |k| k.is_a?(Symbol) ? k.to_s : k }
295
+ raise ArgumentError, "stack_rows: by: names no key" if keys.empty?
296
+ frame = @index ? CAFrame.new({ @axis_name => @index }.merge(@columns)) : self
297
+ keys.each { |k| frame.__send__(:key_column, k, "stack_rows") }
298
+ # With nothing but the key there is no column to stack, and the result
299
+ # would keep no record of how many rows each group had.
300
+ if (frame.column_names - keys).empty?
301
+ raise ArgumentError, "stack_rows: the frame has no column besides the key to stack"
302
+ end
303
+ if keys.size != 1 || !keys.first.is_a?(String)
304
+ if frame.column_names.include?("group") && !keys.include?("group")
305
+ raise ArgumentError,
306
+ "stack_rows: the result's rows are named \"group\" when the key is not " \
307
+ "one column, and \"group\" is a column; rename it first"
308
+ end
309
+ end
310
+ if on
311
+ on = column_name_arg(on, "stack_rows")
312
+ raise KeyError, "stack_rows: no column #{on.inspect}" unless frame.column_names.include?(on)
313
+ return stack_rows_on(frame, keys, on)
314
+ end
315
+ grouped = frame.group_by(*keys)
316
+ perm = grouped.__send__(:group_perm)
317
+ bounds = grouped.__send__(:group_bounds).to_a
318
+ sizes = bounds.each_cons(2).map { |a, b| b - a }
319
+ stacked = sizes.sum
320
+ if stacked != frame.nrow
321
+ raise ArgumentError,
322
+ "stack_rows: every row has to belong to a group; " \
323
+ "the key has no value for #{frame.nrow - stacked} of #{frame.nrow} rows"
324
+ end
325
+ unless sizes.uniq.size <= 1
326
+ # One group of each size, so the message says where to look.
327
+ examples = sizes.each_with_index.uniq { |n, _| n }.map do |n, g|
328
+ "#{group_key_desc(keys, grouped.labels[g])} has #{n}"
329
+ end
330
+ raise ArgumentError,
331
+ "stack_rows: the groups have different numbers of rows " \
332
+ "(#{examples.join(', ')}); stacked rows need the same number in every group"
333
+ end
334
+ count = sizes.first || 0
335
+ perm = stacked.zero? ? CArray.int64(0) : perm[0...stacked]
336
+ cols = {}
337
+ frame.columns.zip(frame.column_names).each do |col, name|
338
+ next if keys.include?(name)
339
+ rows = col[perm, *([nil] * (col.ndim - 1))]
340
+ cols[name] = rows.reshape(sizes.size, count, *col.shape[1..])
341
+ end
342
+ axis = keys.size == 1 && keys.first.is_a?(String) ? keys.first : "group"
343
+ CAFrame.new(cols, axis_name: axis, index: grouped.__send__(:label_index))
344
+ end
345
+
346
+ # A group's key written as name=value, one pair per key column (a key
347
+ # given as a CArray has no name and is written "key").
348
+ private def group_key_desc(keys, label)
349
+ values = keys.size == 1 ? [label] : label
350
+ keys.zip(values).map { |k, v| "#{k.is_a?(String) ? k : "key"}=#{v.inspect}" }.join(", ")
351
+ end
352
+
353
+ private def stack_rows_on(frame, keys, on)
354
+ if keys.include?(on)
355
+ raise ArgumentError, "stack_rows: #{on.inspect} is a key; on: names another column"
356
+ end
357
+ place = frame[on]
358
+ unless place.ndim == 1
359
+ raise ArgumentError, "stack_rows: on: #{on.inspect} has shape #{place.shape.inspect}, not one value per row"
360
+ end
361
+ grouped = frame.group_by(*keys)
362
+ bounds = grouped.__send__(:group_bounds).to_a
363
+ groups = bounds.size - 1
364
+ if bounds.last != frame.nrow
365
+ raise ArgumentError,
366
+ "stack_rows: every row has to belong to a group; " \
367
+ "the key has no value for #{frame.nrow - bounds.last} of #{frame.nrow} rows"
368
+ end
369
+ if place.has_mask? && place.count_masked > 0
370
+ raise ArgumentError,
371
+ "stack_rows: every row has to have a place; " \
372
+ "#{on.inspect} has no value for #{place.count_masked} of #{frame.nrow} rows"
373
+ end
374
+ positions = place.categorize # labels in order of first appearance
375
+ count = positions.labels.size
376
+ # A row's group is its code in the grouping (the groups run in code order).
377
+ group_of = grouped.__send__(:group_codes).int64
378
+ slot = group_of * count + positions.codes.int64
379
+ rows = CArray.int64(groups * count)
380
+ rows[] = UNDEF
381
+ seen = CArray.int64(groups * count)
382
+ seen.scatter_add!(slot, 1) if frame.nrow > 0
383
+ if frame.nrow > 0 && seen.max > 1
384
+ at = seen.gt(1).where[0]
385
+ raise ArgumentError,
386
+ "stack_rows: more than one row for #{group_key_desc(keys, grouped.labels[at / count])}, " \
387
+ "#{on}=#{positions.labels[at % count].inspect}"
388
+ end
389
+ rows[slot] = CArray.int64(frame.nrow).seq if frame.nrow > 0
390
+ # The row where each value of on: first appears.
391
+ first = CArray.int64(count)
392
+ first[] = frame.nrow
393
+ first.scatter_min!(positions.codes.int64, CArray.int64(frame.nrow).seq) if frame.nrow > 0
394
+ cols = {}
395
+ frame.column_names.each do |name|
396
+ next if keys.include?(name)
397
+ col = frame[name]
398
+ src = name == on ? (CArray.int64(groups * count).seq % count).then { |k| first[k] } : rows
399
+ cols[name] = gather_rows(col, src).reshape(groups, count, *col.shape[1..])
400
+ end
401
+ axis = keys.size == 1 && keys.first.is_a?(String) ? keys.first : "group"
402
+ CAFrame.new(cols, axis_name: axis, index: grouped.__send__(:label_index))
403
+ end
404
+
405
+ # The rows +rows+ names of +col+, a masked entry giving a masked row.
406
+ private def gather_rows(col, rows)
407
+ inner = col.shape[1..].inject(1, :*)
408
+ return col.project(rows) if inner == 1
409
+ addr = (rows[nil, :_] * inner + CArray.int64(inner).seq[:_, nil]).reshape(rows.elements * inner)
410
+ col.project(addr)
411
+ end
412
+
413
+ # Unstack the rows +stack_rows+ stacked: every N-D column is spread back
414
+ # into rows along its first trailing axis, and every other column, and the
415
+ # index, repeats once per position. The N-D columns have to agree on the
416
+ # length of that axis. The index stays the index; +reset_index+ turns it
417
+ # back into a column.
418
+ #
419
+ # @return [CAFrame] a frame of +nrow * length+ rows.
420
+ def unstack_rows
421
+ stacked = @columns.select { |_, c| c.ndim >= 2 }
422
+ if stacked.empty?
423
+ raise ArgumentError, "unstack_rows: the frame has no N-D column to unstack"
424
+ end
425
+ lengths = stacked.transform_values { |c| c.shape[1] }
426
+ unless lengths.values.uniq.size == 1
427
+ raise ArgumentError,
428
+ "unstack_rows: the N-D columns differ in length along their first axis #{lengths}"
429
+ end
430
+ count = lengths.values.first
431
+ repeat = CArray.int64(nrow * count).seq / count
432
+ cols = @columns.to_h do |name, col|
433
+ if col.ndim >= 2
434
+ [name, col.reshape(nrow * count, *col.shape[2..])]
435
+ else
436
+ [name, col[repeat]]
437
+ end
438
+ end
439
+ CAFrame.new(cols, axis_name: @axis_name, index: @index && @index[repeat])
440
+ end
441
+
442
+ # A column, or the index when +name+ is the row axis name (pivot and resample
443
+ # keys may be either).
444
+ private def column_or_index(name)
445
+ return @index if @index && !@columns.key?(name) && @axis_name == name
446
+ self[name]
447
+ end
448
+ end
@@ -38,12 +38,36 @@ class CAFrame
38
38
  def sort_by_key(*specs, order: :asc, masked_position: :last)
39
39
  raise ArgumentError, "sort_by_key requires at least one key" if specs.empty?
40
40
  validate_sort_order(order)
41
- keys = specs.map do |spec|
41
+ named = specs.map do |spec|
42
42
  name, dir = parse_sort_key_spec(spec, order)
43
- col = sort_key_column(name)
43
+ [name.to_s, sort_key_column(name), dir]
44
+ end
45
+ keys = named.map do |name, col, dir|
44
46
  dir == :desc ? col.order(descending: true, method: :dense) : col
45
47
  end
46
48
  select_rows(CArray.sort_addr(*keys, masked_position: masked_position))
49
+ rescue ArgumentError, CArray::DataTypeError => e
50
+ raise unless named
51
+ name, col, = named.find { |_, c, _| !sortable_key?(c) }
52
+ raise unless name
53
+ reason = e.message.sub(/\A\w+: /, "")
54
+ if col.face?
55
+ # The core names the conversion as an array method; in a frame it is
56
+ # written with sort_by.
57
+ reason = "#{reason.split(";").first}; sort by its values with " \
58
+ "sort_by { |f| f[#{name.inspect}].to_type(:object) }, or by its " \
59
+ "storage with sort_by { |f| f[#{name.inspect}].parent }"
60
+ end
61
+ raise e.class, "sort_by_key: the column #{name.inspect} cannot be sorted: #{reason}"
62
+ end
63
+
64
+ # Whether +col+ can be a sort key, asked of an empty slice of it so that
65
+ # nothing is sorted: used only to name the column a refusal was about.
66
+ private def sortable_key?(col)
67
+ CArray.sort_addr(col[0...0])
68
+ true
69
+ rescue ArgumentError, CArray::DataTypeError
70
+ false
47
71
  end
48
72
 
49
73
  # Sort rows by key columns the block builds (memo §4.3 escape). The block