carray 3.0.3 → 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 (212) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -0
  3. data/README.md +3 -2
  4. data/carray.gemspec +1 -1
  5. data/ext/ca_axis_descriptor.h +27 -24
  6. data/ext/ca_axis_dispatch.c +1 -6
  7. data/ext/ca_axis_group.c +27 -27
  8. data/ext/ca_binop_dispatch.c +1 -29
  9. data/ext/ca_binop_dispatch.h +0 -1
  10. data/ext/ca_categorical_iterator.c +205 -134
  11. data/ext/ca_compare.c +7 -18
  12. data/ext/ca_group_iter.c +16 -17
  13. data/ext/ca_kernel_iterator.c +1234 -1994
  14. data/ext/ca_kernel_iterator.h +391 -437
  15. data/ext/ca_monop_dispatch.c +82 -59
  16. data/ext/ca_monop_dispatch.h +1 -0
  17. data/ext/ca_obj_array.c +96 -39
  18. data/ext/ca_obj_bincmp.c +24 -21
  19. data/ext/ca_obj_binop.c +15 -26
  20. data/ext/ca_obj_bitarray.c +3 -2
  21. data/ext/ca_obj_bitfield.c +5 -6
  22. data/ext/ca_obj_block.c +18 -9
  23. data/ext/ca_obj_byte_swap.c +5 -6
  24. data/ext/ca_obj_const_string.c +173 -19
  25. data/ext/ca_obj_face.c +238 -57
  26. data/ext/ca_obj_face.h +19 -0
  27. data/ext/ca_obj_fake.c +15 -29
  28. data/ext/ca_obj_farray.c +6 -3
  29. data/ext/ca_obj_field.c +17 -5
  30. data/ext/ca_obj_fixlen_string.c +2 -3
  31. data/ext/ca_obj_grid.c +406 -132
  32. data/ext/ca_obj_meld.c +172 -34
  33. data/ext/ca_obj_moncmp.c +23 -8
  34. data/ext/ca_obj_monop.c +67 -57
  35. data/ext/ca_obj_object.c +157 -51
  36. data/ext/ca_obj_record.c +12 -7
  37. data/ext/ca_obj_reduce.c +13 -8
  38. data/ext/ca_obj_refer.c +25 -27
  39. data/ext/ca_obj_remap.c +30 -9
  40. data/ext/ca_obj_repeat.c +9 -10
  41. data/ext/ca_obj_roll.c +6 -5
  42. data/ext/ca_obj_select.c +14 -9
  43. data/ext/ca_obj_select_axis.c +15 -47
  44. data/ext/ca_obj_shift.c +11 -1
  45. data/ext/ca_obj_stack.c +179 -34
  46. data/ext/ca_obj_stride.c +162 -115
  47. data/ext/ca_obj_string.c +71 -4
  48. data/ext/ca_obj_tile.c +6 -5
  49. data/ext/ca_obj_time.c +22 -6
  50. data/ext/ca_obj_timedelta.c +22 -6
  51. data/ext/ca_obj_transpose.c +5 -3
  52. data/ext/ca_obj_triop.c +23 -32
  53. data/ext/ca_obj_window.c +190 -26
  54. data/ext/ca_sweep_engine.c +2 -2
  55. data/ext/caframe_csv_reader.c +80 -1
  56. data/ext/carray.h +181 -75
  57. data/ext/carray_access.c +184 -96
  58. data/ext/carray_address_basis.c +343 -87
  59. data/ext/carray_attribute.c +89 -6
  60. data/ext/carray_bincount.c +10 -8
  61. data/ext/carray_broadcast.c +13 -8
  62. data/ext/carray_build_flags.h +3 -0
  63. data/ext/carray_call_cfunc.c +108 -0
  64. data/ext/carray_cast.c +142 -24
  65. data/ext/carray_cast_func.rb +46 -6
  66. data/ext/carray_class.c +4 -3
  67. data/ext/carray_config.h +28 -0
  68. data/ext/carray_conversion.c +16 -7
  69. data/ext/carray_copy.c +51 -14
  70. data/ext/carray_core.c +147 -55
  71. data/ext/carray_count.c +50 -20
  72. data/ext/carray_element.c +76 -33
  73. data/ext/carray_factorize.c +118 -64
  74. data/ext/carray_generate.c +191 -50
  75. data/ext/carray_histogram.c +62 -17
  76. data/ext/carray_hold.c +12 -15
  77. data/ext/carray_index_classifier.c +388 -176
  78. data/ext/carray_internal.h +336 -29
  79. data/ext/carray_kernels_bincmp.c +4453 -0
  80. data/ext/carray_kernels_binop.c +11368 -0
  81. data/ext/carray_kernels_init.c +1734 -0
  82. data/ext/carray_kernels_moncmp.c +2104 -0
  83. data/ext/carray_kernels_monop.c +18369 -0
  84. data/ext/carray_kernels_reduce_aggregate.c +23818 -0
  85. data/ext/carray_kernels_reduce_boolean.c +406 -0
  86. data/ext/carray_kernels_reduce_cumulative.c +10086 -0
  87. data/ext/carray_kernels_reduce_extreme.c +17634 -0
  88. data/ext/carray_kernels_reduce_variance.c +3849 -0
  89. data/ext/carray_kernels_scan.c +4302 -0
  90. data/ext/carray_kernels_search.c +33829 -0
  91. data/ext/carray_kernels_sort.c +10906 -0
  92. data/ext/carray_kernels_triop.c +1864 -0
  93. data/ext/carray_key_windows.c +216 -0
  94. data/ext/carray_lazy.c +170 -111
  95. data/ext/carray_mask.c +292 -72
  96. data/ext/carray_median_percentile.c +82 -28
  97. data/ext/carray_memory_view.c +669 -561
  98. data/ext/carray_operator.c +124 -47
  99. data/ext/carray_order.c +384 -76
  100. data/ext/carray_partition.c +28 -27
  101. data/ext/carray_random.c +67 -13
  102. data/ext/carray_round.c +273 -0
  103. data/ext/carray_scatter.c +63 -38
  104. data/ext/carray_segment.c +3 -3
  105. data/ext/carray_slab.c +91 -28
  106. data/ext/carray_sort.c +35 -35
  107. data/ext/carray_struct.c +19 -18
  108. data/ext/carray_test.c +26 -5
  109. data/ext/carray_undef.c +4 -0
  110. data/ext/carray_utils.c +246 -45
  111. data/ext/mk_call_cfunc.rb +19 -2
  112. data/ext/mkkernel.rb +198 -240
  113. data/ext/ruby_carray.c +10 -4
  114. data/ext/version.h +4 -4
  115. data/lib/carray/attribute.rb +126 -22
  116. data/lib/carray/autoload_carray.rb +8 -31
  117. data/lib/carray/autoload_method_extension.rb +14 -5
  118. data/lib/carray/axis_group.rb +255 -106
  119. data/lib/carray/basics.rb +78 -10
  120. data/lib/carray/bincount_nd.rb +12 -2
  121. data/lib/carray/block_iterator.rb +126 -23
  122. data/lib/carray/boolean_reduce.rb +17 -17
  123. data/lib/carray/categorical.rb +165 -43
  124. data/lib/carray/categorical_iterator.rb +130 -27
  125. data/lib/carray/complex.rb +5 -5
  126. data/lib/carray/conditional.rb +67 -18
  127. data/lib/carray/const_string.rb +14 -5
  128. data/lib/carray/construct.rb +15 -11
  129. data/lib/carray/data_type_extension.rb +1 -0
  130. data/lib/carray/distance_limit.rb +77 -0
  131. data/lib/carray/fixlen_string.rb +7 -1
  132. data/lib/carray/frame/column_verbs.rb +70 -0
  133. data/lib/carray/frame/convert.rb +69 -0
  134. data/lib/carray/frame/csv_parser.rb +111 -17
  135. data/lib/carray/frame/describe.rb +3 -2
  136. data/lib/carray/frame/frame.rb +246 -21
  137. data/lib/carray/frame/group.rb +510 -95
  138. data/lib/carray/frame/io.rb +115 -28
  139. data/lib/carray/frame/join.rb +312 -51
  140. data/lib/carray/frame/records.rb +83 -7
  141. data/lib/carray/frame/reshape.rb +42 -35
  142. data/lib/carray/frame/sort.rb +7 -19
  143. data/lib/carray/frame/verbs.rb +188 -48
  144. data/lib/carray/frame/windows.rb +117 -0
  145. data/lib/carray/frame.rb +2 -0
  146. data/lib/carray/fuse_source.rb +11 -4
  147. data/lib/carray/fusion.rb +40 -16
  148. data/lib/carray/histogram.rb +6 -1
  149. data/lib/carray/inspect.rb +31 -8
  150. data/lib/carray/iterator.rb +85 -7
  151. data/lib/carray/key_windows.rb +620 -0
  152. data/lib/carray/lazy.rb +182 -183
  153. data/lib/carray/mask_gap_fill.rb +63 -57
  154. data/lib/carray/meld_reduce.rb +12 -14
  155. data/lib/carray/methods/bin.rb +26 -18
  156. data/lib/carray/methods/bincount.rb +16 -0
  157. data/lib/carray/methods/bit_string.rb +1 -1
  158. data/lib/carray/methods/choose.rb +16 -3
  159. data/lib/carray/methods/composition.rb +3 -1
  160. data/lib/carray/methods/gather_nd.rb +14 -5
  161. data/lib/carray/methods/insert_block.rb +2 -2
  162. data/lib/carray/methods/is_between.rb +59 -0
  163. data/lib/carray/methods/is_in.rb +53 -36
  164. data/lib/carray/methods/join.rb +1 -6
  165. data/lib/carray/methods/lag.rb +49 -0
  166. data/lib/carray/methods/locate_addr.rb +47 -51
  167. data/lib/carray/methods/mode.rb +1 -1
  168. data/lib/carray/methods/nunique.rb +11 -6
  169. data/lib/carray/methods/resize.rb +3 -3
  170. data/lib/carray/methods/value_counts.rb +4 -11
  171. data/lib/carray/record_order.rb +3 -3
  172. data/lib/carray/rng.rb +3 -0
  173. data/lib/carray/runtime.rb +53 -8
  174. data/lib/carray/segment_iterator.rb +313 -82
  175. data/lib/carray/serialize.rb +24 -10
  176. data/lib/carray/slab_iterator.rb +50 -0
  177. data/lib/carray/string.rb +105 -5
  178. data/lib/carray/string_operation_extension.rb +8 -1
  179. data/lib/carray/struct.rb +9 -21
  180. data/lib/carray/struct_builder.rb +19 -8
  181. data/lib/carray/time.rb +726 -104
  182. data/lib/carray/window_iterator.rb +122 -16
  183. data/lib/carray.rb +7 -0
  184. data/yard-stubs/ca_obj_bitfield.rb +1 -1
  185. data/yard-stubs/ca_obj_field.rb +4 -0
  186. data/yard-stubs/ca_obj_grid.rb +21 -10
  187. data/yard-stubs/ca_obj_refer.rb +2 -1
  188. data/yard-stubs/ca_obj_roll.rb +2 -2
  189. data/yard-stubs/ca_obj_shift.rb +5 -1
  190. data/yard-stubs/ca_obj_tile.rb +1 -1
  191. data/yard-stubs/ca_obj_window.rb +8 -3
  192. data/yard-stubs/carray_access.rb +6 -1
  193. data/yard-stubs/carray_attribute.rb +0 -5
  194. data/yard-stubs/carray_cast.rb +10 -0
  195. data/yard-stubs/carray_conversion.rb +2 -1
  196. data/yard-stubs/carray_copy.rb +4 -2
  197. data/yard-stubs/carray_count.rb +4 -4
  198. data/yard-stubs/carray_element.rb +10 -4
  199. data/yard-stubs/carray_generate.rb +21 -1
  200. data/yard-stubs/carray_mask.rb +26 -2
  201. data/yard-stubs/carray_math.rb +27 -1
  202. data/yard-stubs/carray_median_percentile.rb +5 -2
  203. data/yard-stubs/carray_memory_view.rb +40 -23
  204. data/yard-stubs/carray_order.rb +29 -18
  205. data/yard-stubs/carray_random.rb +13 -4
  206. data/yard-stubs/carray_scatter.rb +14 -9
  207. data/yard-stubs/carray_slab.rb +9 -2
  208. data/yard-stubs/carray_sort.rb +6 -5
  209. data/yard-stubs/carray_test.rb +5 -3
  210. metadata +25 -3
  211. data/lib/carray/arrow_tensor.rb +0 -401
  212. data/lib/carray/table.rb +0 -108
@@ -9,18 +9,50 @@ class CAFrame
9
9
  # Add (or replace) a column, returning a new frame — the column set changes,
10
10
  # so the result is a different table (memo §3.8). The column is shared by
11
11
  # reference; the envelope is cheap. Chain or reassign: +df = df.append(...)+.
12
- def append(name, col)
13
- key = name.to_s
12
+ #
13
+ # The column goes at the end, or where a column of that name already is.
14
+ # +before:+ / +after:+ name a column to put it next to instead; a column
15
+ # of that name already in the frame is moved there.
16
+ #
17
+ # df.append("id", ids, before: df.column_names.first) # leftmost
18
+ def append(name, col, before: nil, after: nil)
19
+ key = column_name_arg(name, "append")
14
20
  ca = coerce_column(col)
15
- rebuild(@columns.merge(key => ca))
21
+ unless before.nil? || after.nil?
22
+ raise ArgumentError, "append: give before: or after:, not both"
23
+ end
24
+ # Checked here: the new frame would take N from whichever witness comes
25
+ # first, and blame the frame's own column for the new one's length. A
26
+ # column that replaces the only one is held to N too, as df[name] = is;
27
+ # only a frame with no column and no index takes N from the new column.
28
+ if (@columns.any? || @index) && ca.shape[0] != @nrow
29
+ raise ArgumentError,
30
+ "append: column #{key.inspect} has axis-0 length #{ca.shape[0]}, expected #{@nrow}"
31
+ end
32
+ anchor = before.nil? ? after : before
33
+ return rebuild(@columns.merge(key => ca)) if anchor.nil?
34
+
35
+ anchor = column_key(anchor, "append")
36
+ unless @columns.key?(anchor)
37
+ raise KeyError, "append: no column #{anchor.inspect} to put #{key.inspect} #{before.nil? ? "after" : "before"}"
38
+ end
39
+ return rebuild(@columns.merge(key => ca)) if anchor == key
40
+
41
+ cols = {}
42
+ @columns.each do |k, v|
43
+ next if k == key
44
+ cols[key] = ca if !before.nil? && k == anchor
45
+ cols[k] = v
46
+ cols[key] = ca if !after.nil? && k == anchor
47
+ end
48
+ rebuild(cols)
16
49
  end
17
50
 
18
51
  # Remove one or more columns, returning a new frame (column set changes,
19
52
  # memo §3.8). Column data is untouched and shared with the original.
20
53
  def drop(*names)
21
54
  cols = @columns.dup
22
- names.each do |name|
23
- key = name.to_s
55
+ column_keys(names, "drop").each do |key|
24
56
  raise KeyError, "no column #{key.inspect}" unless cols.key?(key)
25
57
  cols.delete(key)
26
58
  end
@@ -33,8 +65,8 @@ class CAFrame
33
65
  def rename(mapping)
34
66
  norm = {}
35
67
  mapping.each do |old, new|
36
- o = old.to_s
37
- n = new.to_s
68
+ o = column_key(old, "rename")
69
+ n = column_name_arg(new, "rename")
38
70
  raise KeyError, "no column #{o.inspect}" unless @columns.key?(o)
39
71
  if n != o && @columns.key?(n)
40
72
  raise ArgumentError, "rename target #{n.inspect} already exists"
@@ -65,7 +97,7 @@ class CAFrame
65
97
  # @raise [ArgumentError] when the column is not text or 1-D, +into:+ names
66
98
  # fewer than two columns or one twice, or names a column already there.
67
99
  def split_column(name, sep, into:)
68
- key = name.to_s
100
+ key = column_key(name, "split_column")
69
101
  col = @columns.fetch(key) { raise KeyError, "no column #{key.inspect}" }
70
102
  unless string_column?(col) && col.ndim == 1
71
103
  raise ArgumentError, "split_column: #{key.inspect} is not a one-dimensional text column"
@@ -73,11 +105,12 @@ class CAFrame
73
105
  unless sep.is_a?(String) || sep.is_a?(Regexp)
74
106
  raise ArgumentError, "split_column: sep must be a String or a Regexp (got #{sep.inspect})"
75
107
  end
76
- names = Array(into).map(&:to_s)
77
- if names.size < 2 || names.uniq.size != names.size
108
+ names = Array(into).map { |n| column_name_arg(n, "split_column") }
109
+ if names.size < 2
78
110
  raise ArgumentError,
79
- "split_column: into: names two or more columns, each once (got #{names.inspect})"
111
+ "split_column: into: names two or more columns (got #{names.inspect})"
80
112
  end
113
+ CAFrame.__send__(:refuse_repeated_names, names, "split_column", "into: names")
81
114
  taken = names & (@columns.keys - [key])
82
115
  unless taken.empty?
83
116
  raise ArgumentError, "split_column: #{taken.inspect} already a column"
@@ -101,6 +134,85 @@ class CAFrame
101
134
  rebuild(rebuilt)
102
135
  end
103
136
 
137
+ # Replace a categorical column by numeric columns, one per category, for a
138
+ # model that takes numbers (see CACategorical#contrast):
139
+ #
140
+ # df.contrast_columns("weather") # "weather_cloud", "weather_rain"
141
+ # df.contrast_columns("weather", base: "rain")
142
+ # df.contrast_columns("weather", full: true) # every category kept
143
+ #
144
+ # The new columns take the column's place and are named after it and the
145
+ # label, "weather_cloud"; +into:+ names them instead. A column that is not
146
+ # a CACategorical is categorized first with its labels in ascending order,
147
+ # so the base -- the first label unless +base:+ says otherwise -- does not
148
+ # depend on the order of the rows. A masked cell is UNDEF in every new
149
+ # column.
150
+ #
151
+ # @param name [String] the column to expand.
152
+ # @param scheme [Symbol] the coding; only :dummy is defined.
153
+ # @param base [Object] the label whose column is left out.
154
+ # @param full [Boolean] keep every column.
155
+ # @param into [Array<String>, nil] the names of the new columns.
156
+ # @return [CAFrame] a new frame; the other columns are shared.
157
+ # @raise [ArgumentError] when the column is not one-dimensional, +into:+
158
+ # names a different number of columns or one twice, or a new name is
159
+ # already a column; and as CACategorical#contrast raises.
160
+ def contrast_columns(name, scheme = :dummy, base: nil, full: false, into: nil)
161
+ key = column_key(name, "contrast_columns")
162
+ col = @columns.fetch(key) { raise KeyError, "no column #{key.inspect}" }
163
+ unless col.ndim == 1
164
+ raise ArgumentError, "contrast_columns: #{key.inspect} is not a one-dimensional column"
165
+ end
166
+ # The labels are sorted as categorize(sort_labels: true) sorts them;
167
+ # labels of an object column that cannot be put in order are named here.
168
+ if col.object? && !col.is_a?(CACategorical)
169
+ col.unique.to_a.sort do |a, b|
170
+ CArray.__send__(:object_order_cmp, a, b) or
171
+ raise ArgumentError,
172
+ "contrast_columns: #{key.inspect} has the labels #{a.inspect} and " \
173
+ "#{b.inspect}, which cannot be put in order; categorize it with labels: first"
174
+ end
175
+ end
176
+ cat = col.is_a?(CACategorical) ? col : col.categorize(sort_labels: true)
177
+ coded = cat.contrast(scheme, base: base, full: full)
178
+ m = coded.shape[1]
179
+ # The base is found among the labels as contrast finds it.
180
+ dropped = full ? nil : (base.nil? ? 0 : cat.__send__(:label_position, base))
181
+ labels = cat.labels.reject.with_index { |_, i| i == dropped }
182
+ names =
183
+ if into
184
+ Array(into).map { |n| column_name_arg(n, "contrast_columns") }
185
+ else
186
+ labels.map { |label| "#{key}_#{label}" }
187
+ end
188
+ unless names.size == m
189
+ raise ArgumentError,
190
+ "contrast_columns: into: names #{names.size} columns for #{m} (got #{names.inspect})"
191
+ end
192
+ if into
193
+ CAFrame.__send__(:refuse_repeated_names, names, "contrast_columns", "into: names")
194
+ else
195
+ CAFrame.__send__(:refuse_repeated_names, names, "contrast_columns", "column names",
196
+ hint: "name them with into:")
197
+ end
198
+ taken = names & (@columns.keys - [key])
199
+ unless taken.empty?
200
+ message = "contrast_columns: #{taken.inspect} already a column"
201
+ message += "; name them with into:" unless into
202
+ raise ArgumentError, message
203
+ end
204
+
205
+ rebuilt = {}
206
+ @columns.each do |k, v|
207
+ if k == key
208
+ names.each_with_index { |nm, j| rebuilt[nm] = coded[nil, j] }
209
+ else
210
+ rebuilt[k] = v
211
+ end
212
+ end
213
+ rebuild(rebuilt)
214
+ end
215
+
104
216
  # Stack several columns into one N-D column: the columns side by side in a
105
217
  # file -- a value per month, say -- become one column whose trailing axis
106
218
  # (or axes, with +shape:+) runs over them. The columns are given in the
@@ -177,7 +289,7 @@ class CAFrame
177
289
  #
178
290
  # @return [CAFrame] a new frame; the other columns are shared.
179
291
  def unstack_column(name, into: nil)
180
- key = name.to_s
292
+ key = column_key(name, "unstack_column")
181
293
  col = @columns.fetch(key) { raise KeyError, "no column #{key.inspect}" }
182
294
  if col.ndim < 2
183
295
  raise ArgumentError, "unstack_column: #{key.inspect} is one-dimensional; there is nothing to unstack"
@@ -198,10 +310,11 @@ class CAFrame
198
310
  trailing.map { |d| (0...d).to_a }.inject { |a, b| a.product(b).map(&:flatten) }
199
311
  end
200
312
  names = given || positions.map { |pos| "#{key}_#{pos.join('_')}" }
201
- unless names.size == count && names.uniq.size == count
313
+ unless names.size == count
202
314
  raise ArgumentError,
203
- "unstack_column: into: names #{names.size} columns, each once; #{key.inspect} has #{count} positions"
315
+ "unstack_column: into: names #{names.size} columns; #{key.inspect} has #{count} positions"
204
316
  end
317
+ CAFrame.__send__(:refuse_repeated_names, names, "unstack_column", "into: names")
205
318
  taken = names & (@columns.keys - [key])
206
319
  unless taken.empty?
207
320
  raise ArgumentError, "unstack_column: #{taken.inspect} already a column"
@@ -218,12 +331,10 @@ class CAFrame
218
331
  rebuild(rebuilt)
219
332
  end
220
333
 
221
- # A column name given as an argument: a non-empty String, as the frame's
222
- # column keys are.
334
+ # A column name given as an argument for a column to make: a non-empty
335
+ # String, as the frame's column keys are.
223
336
  private def column_name_arg(name, verb)
224
- unless name.is_a?(String)
225
- raise TypeError, "#{verb}: a column name is a String (got #{name.inspect})"
226
- end
337
+ column_key(name, verb)
227
338
  raise ArgumentError, "#{verb}: a column name can not be empty" if name.empty?
228
339
  name
229
340
  end
@@ -246,7 +357,7 @@ class CAFrame
246
357
  end
247
358
  keys[i..(selection.exclude_end? ? j - 1 : j)]
248
359
  when Array
249
- selection.map(&:to_s).each do |n|
360
+ selection.map { |n| column_key(n, verb) }.each do |n|
250
361
  raise KeyError, "#{verb}: no column #{n.inspect}" unless @columns.key?(n)
251
362
  end
252
363
  when Regexp
@@ -258,10 +369,7 @@ class CAFrame
258
369
  if names.empty?
259
370
  raise ArgumentError, "#{verb}: #{selection.inspect} selects no column"
260
371
  end
261
- if names.uniq.size != names.size
262
- raise ArgumentError, "#{verb}: #{selection.inspect} names a column more than once"
263
- end
264
- names
372
+ CAFrame.__send__(:refuse_repeated_names, names, verb)
265
373
  end
266
374
 
267
375
  # Mask cells of a column that equal +value+ (sentinel -> mask, memo §11.4).
@@ -270,7 +378,7 @@ class CAFrame
270
378
  # raises — recode by rebuilding and rebinding instead.
271
379
  def mask_eq(name, value)
272
380
  refuse_if_frozen
273
- self[name][:eq, value] = UNDEF
381
+ self[column_key(name, "mask_eq")][:eq, value] = UNDEF
274
382
  self
275
383
  end
276
384
 
@@ -281,7 +389,10 @@ class CAFrame
281
389
  # column cast to :time is parsed into a CATime column, in the finest unit
282
390
  # its text shows (:D for dates alone, :s with a time of day, :ms / :us /
283
391
  # :ns for fractions of a second); parse_to_time takes a format and a
284
- # unit. Other targets use to_type.
392
+ # unit. A column cast to :categorical becomes a CACategorical of its
393
+ # values, the labels in the order they first appear (categorize); a text
394
+ # column read by from_csv with types: gets there without a String made per
395
+ # cell. Other targets use to_type.
285
396
  # The call shapes are told apart by the fact that column names are always
286
397
  # Strings and types always Symbols (§3.7):
287
398
  #
@@ -304,7 +415,7 @@ class CAFrame
304
415
  # cast(default: :float64, "station" => nil) # every other column to float64
305
416
  #
306
417
  # A name that is not a column raises KeyError, and a Symbol key other than
307
- # +:default+ raises ArgumentError; both before any column is read. Every
418
+ # +:default+ raises TypeError; both before any column is read. Every
308
419
  # column is read before any is rebound, so a raise leaves the frame as it
309
420
  # was. Returns self so calls chain.
310
421
  #
@@ -332,6 +443,13 @@ class CAFrame
332
443
  # keywords next to on_error:.
333
444
  unless map.empty?
334
445
  unless name_or_map.nil?
446
+ # Symbol keys next to a name or a map are a misspelt keyword
447
+ # (on_erorr:), not a second map; :default is a map key.
448
+ if map.keys.all?(Symbol) && !map.key?(:default)
449
+ raise ArgumentError,
450
+ "#{label}: unknown keyword#{map.size > 1 ? 's' : ''}: " \
451
+ "#{map.keys.map(&:inspect).join(', ')} (the keyword is on_error:)"
452
+ end
335
453
  raise ArgumentError, "#{label} takes a name and a type, or a map, not both"
336
454
  end
337
455
  name_or_map = map
@@ -353,11 +471,15 @@ class CAFrame
353
471
  end
354
472
  resolve_cast_map(name_or_map, label)
355
473
  else
356
- { name_or_map.to_s => type }
474
+ { column_key(name_or_map, label) => type }
357
475
  end
358
476
  # Read every column before rebinding any, so a raise leaves the frame as
359
477
  # it was.
360
- casted = types.map { |key, t| [key, cast_column(key, t, on_error)] }
478
+ # A conversion keeps what the column is, so the column's attributes
479
+ # come along whatever the target type.
480
+ casted = types.map do |key, t|
481
+ [key, @columns[key].__send__(:carry_attrs, cast_column(key, t, on_error))]
482
+ end
361
483
  casted.each { |key, col| @columns[key] = col }
362
484
  self
363
485
  end
@@ -372,10 +494,9 @@ class CAFrame
372
494
  if key == :default
373
495
  default = t
374
496
  elsif key.is_a?(Symbol)
375
- raise ArgumentError, "#{label} takes column names (Strings) and :default " \
376
- "as keys (got #{key.inspect})"
497
+ CAFrame.__send__(:column_key, key, label, hint: ":default is the only Symbol key")
377
498
  else
378
- Array(key).each { |name| named[name.to_s] = t }
499
+ Array(key).each { |name| named[column_key(name, label)] = t }
379
500
  end
380
501
  end
381
502
  named.each_key do |name|
@@ -396,6 +517,7 @@ class CAFrame
396
517
  # The targets a text column is read as numbers for.
397
518
  CAST_NUMBER_TYPES = %w[int8 int16 int32 int64 uint8 uint16 uint32 uint64
398
519
  float32 float64 cmplx64 cmplx128].freeze
520
+ Ractor.make_shareable(CAST_NUMBER_TYPES)
399
521
  private_constant :CAST_ON_ERROR, :CAST_NUMBER_TYPES
400
522
 
401
523
  # The types the text columns read as, as a map +cast+ takes:
@@ -470,9 +592,10 @@ class CAFrame
470
592
  # For those, +format+ is a strptime format, :infer to find the one format
471
593
  # the column is written in (see infer_time_format; a cell not in it
472
594
  # raises CAFrame::UnreadableColumn, whatever +on_error+ says), or :mixed
473
- # to guess at each cell (much slower). +unit+ is the storage resolution:
474
- # without a format or with :infer it defaults to the finest the text
475
- # shows, with a format to :s. Masked / nil and unparseable cells become
595
+ # to guess at each cell (much slower). +unit+ is the storage resolution,
596
+ # by default the finest the text shows -- with a format, the finest field
597
+ # the format writes ("%d/%m/%Y" :D, "%H:%M" :s, "%L" :ms), so a fraction
598
+ # of a second is not cut off. Masked / nil and unparseable cells become
476
599
  # UNDEF; +on_error:+ :warn / :raise reports the unparseable ones as +cast+
477
600
  # does. Make it the index with +set_index+ afterward.
478
601
  # +cast(name => :time)+ is the call without a format.
@@ -486,14 +609,14 @@ class CAFrame
486
609
  raise ArgumentError,
487
610
  "on_error: must be :mask, :warn or :raise (got #{on_error.inspect})"
488
611
  end
489
- key = name.to_s
612
+ key = column_key(name, "parse_to_time")
490
613
  col = @columns.fetch(key) { raise KeyError, "no column #{key.inspect}" }
491
614
  unless string_column?(col)
492
615
  raise ArgumentError,
493
616
  "parse_to_time needs a string column (object / CAString / " \
494
617
  "CAConstString / CAFixlenString); #{key.inspect} is #{col.data_type}"
495
618
  end
496
- @columns[key] = parse_time_column(key, col, format, unit, on_error)
619
+ @columns[key] = col.__send__(:carry_attrs, parse_time_column(key, col, format, unit, on_error))
497
620
  self
498
621
  end
499
622
 
@@ -508,7 +631,7 @@ class CAFrame
508
631
  #
509
632
  # df.infer_time_format("date") # => "%d/%m/%Y"
510
633
  def infer_time_format(name)
511
- key = name.to_s
634
+ key = column_key(name, "infer_time_format")
512
635
  col = @columns.fetch(key) { raise KeyError, "no column #{key.inspect}" }
513
636
  unless string_column?(col)
514
637
  raise ArgumentError, "column #{key.inspect} is #{col.data_type}, not text"
@@ -539,12 +662,12 @@ class CAFrame
539
662
  # df.to_time("time", CATime::Grid.parse("days since 1980-01-01 12:00"))
540
663
  def to_time(name, grid = nil, unit: :s, epoch: nil)
541
664
  refuse_if_frozen
542
- key = name.to_s
665
+ key = column_key(name, "to_time")
543
666
  col = @columns.fetch(key) { raise KeyError, "no column #{key.inspect}" }
544
667
  raw = integer_serial_column(col, key)
545
668
  grid = unit if unit.is_a?(CATime::Grid)
546
669
  if grid.is_a?(CATime::Grid)
547
- @columns[key] = grid.at(raw)
670
+ @columns[key] = col.__send__(:carry_attrs, grid.at(raw))
548
671
  return self
549
672
  end
550
673
  unless grid.nil?
@@ -552,9 +675,10 @@ class CAFrame
552
675
  "the positional argument must be a CATime::Grid (got #{grid.class})"
553
676
  end
554
677
  if epoch
555
- raw = raw + CArray.time(epoch, unit: unit).ticks[0]
678
+ # An Integer epoch is an instant in Unix seconds, whatever the unit.
679
+ raw = raw + CATimeLiteral.to_time_array(epoch, CATime::Resolution.parse(unit), nil, :raise).ticks[0]
556
680
  end
557
- @columns[key] = raw.time(unit: unit)
681
+ @columns[key] = col.__send__(:carry_attrs, raw.time(unit: unit))
558
682
  self
559
683
  end
560
684
 
@@ -576,7 +700,7 @@ class CAFrame
576
700
  # `df.append(name, df[name].strip_mask(method: :forward))`.
577
701
  def fill(name, method_or_value)
578
702
  refuse_if_frozen
579
- key = name.to_s
703
+ key = column_key(name, "fill")
580
704
  raise KeyError, "no column #{key.inspect}" unless @columns.key?(key)
581
705
  col = @columns[key]
582
706
  # An N-D column is filled down the rows: each position on its trailing
@@ -599,7 +723,7 @@ class CAFrame
599
723
  private def fill_linear_column(key, col)
600
724
  # A Face column interpolates through its own linear_fetch (a time
601
725
  # column's keeps the Face and its unit), as the core gap fill does.
602
- unless col.__send__(:__interpolable__)
726
+ unless col.__send__(:interpolable?)
603
727
  raise ArgumentError,
604
728
  "fill(#{key.inspect}, :linear): numeric or time column required " \
605
729
  "(got #{col.data_type_name})"
@@ -625,7 +749,7 @@ class CAFrame
625
749
  return
626
750
  end
627
751
  # The same fill as the core, with the index as x.
628
- col[] = col.send(:__linear_fill_along__, @index) # write-through
752
+ col[] = col.send(:linear_fill_along, @index) # write-through
629
753
  end
630
754
 
631
755
  # Text cells are data, not Ruby literals: an object column cast to a
@@ -635,6 +759,9 @@ class CAFrame
635
759
  key = name.to_s
636
760
  col = @columns.fetch(key) { raise KeyError, "no column #{key.inspect}" }
637
761
  return cast_time_column(key, col, on_error) if type == :time
762
+ if type == :categorical
763
+ return col.is_a?(CACategorical) ? col : col.categorize
764
+ end
638
765
  if CArray.data_type_name(type) == "boolean"
639
766
  return read_boolean_text(key, col, on_error) if string_column?(col)
640
767
  if !col.face? && INTEGER_TYPES.include?(col.data_type)
@@ -729,9 +856,12 @@ class CAFrame
729
856
  private def parse_time_column(key, col, format, unit, on_error)
730
857
  case format
731
858
  when nil then read_time_text(key, col, unit, on_error)
732
- when String then parse_time_by_format(key, col, format, unit || :s, on_error)
859
+ when String
860
+ parse_time_by_format(key, col, format, unit || CATimeLiteral.format_unit(format), on_error)
733
861
  when :infer then parse_time_inferred(key, col, unit)
734
- when :mixed then parse_time_by_format(key, col, nil, unit || :s, on_error)
862
+ when :mixed
863
+ unit ||= CATimeLiteral.unit_shown(time_text_of(col), nil)
864
+ parse_time_by_format(key, col, nil, unit, on_error)
735
865
  else
736
866
  raise ArgumentError,
737
867
  "time format must be a strptime String, :infer or :mixed " \
@@ -854,9 +984,18 @@ class CAFrame
854
984
 
855
985
  # nil, UNDEF and a blank String are missing values.
856
986
  # Every present cell is true or false in some case, and one is present.
987
+ # The first present cell settles most columns, so the distinct words are
988
+ # gathered only when it is one.
857
989
  private def boolean_word_column?(col)
990
+ first = nil
991
+ col.each { |v| unless missing_text?(v) then first = v; break end }
992
+ return false unless first && boolean_word?(first)
858
993
  words = time_text_of(col).unique.to_a.reject { |v| missing_text?(v) }
859
- !words.empty? && words.all? { |v| %w[true false].include?(v.to_s.strip.downcase) }
994
+ words.all? { |v| boolean_word?(v) }
995
+ end
996
+
997
+ private def boolean_word?(cell)
998
+ %w[true false].include?(cell.to_s.strip.downcase)
860
999
  end
861
1000
 
862
1001
  private def missing_text?(cell)
@@ -925,6 +1064,7 @@ class CAFrame
925
1064
  @columns.keys.each_with_index do |key, i|
926
1065
  coerced = promoted[i]
927
1066
  next if coerced.equal?(cols[i])
1067
+ # coerce: the type promote_list chose
928
1068
  @columns[key] = cols[i].to_type(coerced.data_type)
929
1069
  end
930
1070
  end
@@ -0,0 +1,117 @@
1
+ # CAFrame windows bounded by a width of a key column.
2
+
3
+ class CAFrame
4
+ # Moving windows bounded by a width of the key column +on+ (a time, a
5
+ # duration or a number) instead of a count of rows: the window of a row
6
+ # whose key is t holds the rows whose key lies between t - before and
7
+ # t + after. +exclude:+ leaves out the lower end (default), the upper end,
8
+ # both or neither, as CArray#is_between does. +by:+ names columns that
9
+ # must match exactly, so each window stays within one station; +interval:+
10
+ # gives the key's grid step for WindowedFrame#is_complete. The rows need
11
+ # not be sorted; the answers come in row order. A row whose key is
12
+ # masked, NaN or infinite is in no window and has no window.
13
+ #
14
+ # w = df.windows(on: "time", before: "1 hour", by: "station")
15
+ # w.mean("temp") # => CArray, one value per row
16
+ # w.mean # => frame of the windowed mean of every column
17
+ # w.elements # => the number of rows in each window
18
+ #
19
+ # Given +at:+, the windows sit at those points of the key instead of at
20
+ # the rows, and every answer is one per point: a frame of the reductions
21
+ # has a row per point, the points as its index and the on: column's name
22
+ # as its row axis name. The points need not be sorted or on the rows'
23
+ # keys. A grid of points gives windows on a grid; a window with no row
24
+ # answers as an empty reduction does (+mean+ UNDEF, +count+ 0). +at:+
25
+ # cannot be given with +by:+.
26
+ #
27
+ # grid = CArray.time_range(t0, t1, unit: "1 hour")
28
+ # df.windows(on: "time", at: grid, after: "3 hours", exclude: :upper).mean
29
+ # # => a frame with a row per hour, the mean over [t, t + 3 hours)
30
+ #
31
+ # @return [WindowedFrame]
32
+ def windows (on:, before: nil, after: nil, exclude: :lower, by: nil, interval: nil, at: nil)
33
+ on_name = column_key(on, "windows")
34
+ key = key_column(on_name, "windows")
35
+ by_names = by.nil? ? [] : column_keys(by.is_a?(Array) ? by : [by], "windows")
36
+ by_cols = by_names.map { |b| key_column(b, "windows") }
37
+ # The points are taken as they are now: a later write to the array
38
+ # given changes neither the windows nor a result's index.
39
+ at = at.copy if at.is_a?(CArray)
40
+ plan = CAKeyWindowIterator.new(CArray.int64(nrow).seq, on: key,
41
+ before: before, after: after, exclude: exclude,
42
+ by: by_cols.empty? ? nil : by_cols,
43
+ interval: interval, at: at)
44
+ WindowedFrame.new(self, plan, [on_name, *by_names], at: at)
45
+ end
46
+ end
47
+
48
+ # The windows of a CAFrame (CAFrame#windows): the reductions over each row's
49
+ # window, one answer per row, and what each window covered.
50
+ class WindowedFrame
51
+ include CAFrame::ColumnVerbs
52
+
53
+ def initialize (frame, plan, key_names, at: nil)
54
+ @frame = frame
55
+ @plan = plan
56
+ @key_names = key_names
57
+ @at = at
58
+ end
59
+
60
+ # @overload [](name)
61
+ # Returns the CAKeyWindowIterator of one column.
62
+ # @return [CAKeyWindowIterator]
63
+ def [] (name)
64
+ @plan.for_value(@frame[@frame.__send__(:column_key, name, "windows")])
65
+ end
66
+
67
+ # What each window covered, one value per row: see CAKeyWindowIterator.
68
+ [:first_addr, :last_addr, :first_key, :last_key, :elements, :is_complete].each do |m|
69
+ define_method(m, &Ractor.make_shareable(proc { @plan.public_send(m) }))
70
+ end
71
+
72
+ # @overload bounds
73
+ # Returns the range each window was asked to cover, as two arrays of the
74
+ # key's kind.
75
+ # @return [Array(CArray, CArray)]
76
+ def bounds
77
+ @plan.bounds
78
+ end
79
+
80
+ # The windowed reductions. Without names, a frame of every column whose
81
+ # type has the reduction (the on: and by: columns left out), on the
82
+ # frame's rows and index; given names, the CArray (one) or the Array of
83
+ # them (several), as +df[...]+ returns columns.
84
+ CArray.send(:define_named_methods, self,
85
+ [:sum, :mean, :min, :max, :prod, :accumulate, :median,
86
+ :variance, :stddev, :variancep, :stddevp], :window_reduction)
87
+
88
+ CArray.send(:define_named_methods, self,
89
+ [:first, :last, :nunique, :count, :all, :any], :window_value)
90
+
91
+ private
92
+
93
+ def window_reduction (reduction, *names, min_count: nil, fill_value: nil)
94
+ window_reduce(reduction, names, min_count: min_count, fill_value: fill_value)
95
+ end
96
+
97
+ def window_value (reduction, *names)
98
+ window_reduce(reduction, names)
99
+ end
100
+
101
+ # An N-D column (each row a profile) is reduced along the rows, each
102
+ # window keeping the trailing shape, as a group reduces it.
103
+ def window_reduce (reduction, names, **keywords)
104
+ keywords = keywords.compact
105
+ out = over_named_columns(reduction, names, @key_names) do |col|
106
+ @plan.for_value(col).public_send(reduction, **keywords)
107
+ end
108
+ return out unless names.empty?
109
+ if @at
110
+ # Each result has an index of its own, as a group's result has.
111
+ key = @frame.__send__(:key_column, @key_names.first, "windows")
112
+ CAFrame.new(out, axis_name: @key_names.first, index: CAFrame.__send__(:key_labels, key, @at.copy))
113
+ else
114
+ CAFrame.new(out, axis_name: @frame.axis_name, index: @frame.index)
115
+ end
116
+ end
117
+ end
data/lib/carray/frame.rb CHANGED
@@ -6,12 +6,14 @@
6
6
  # devel/MEMO_DATAFRAME_ON_CARRAY.md for the design.
7
7
 
8
8
  require 'carray/frame/frame'
9
+ require 'carray/frame/column_verbs'
9
10
  require 'carray/frame/verbs'
10
11
  require 'carray/frame/convert'
11
12
  require 'carray/frame/io'
12
13
  require 'carray/frame/records'
13
14
  require 'carray/frame/group'
14
15
  require 'carray/frame/join'
16
+ require 'carray/frame/windows'
15
17
  require 'carray/frame/concat'
16
18
  require 'carray/frame/sort'
17
19
  require 'carray/frame/reshape'
@@ -44,13 +44,20 @@ class CArray
44
44
  # every time the same block is called and a different one for each
45
45
  # block, even two on one line. Where Ruby has weak keys, a block whose
46
46
  # code is unloaded takes its entry with it.
47
- SITES = defined?(ObjectSpace::WeakKeyMap) ? ObjectSpace::WeakKeyMap.new
48
- : {}.compare_by_identity
47
+ def self.new_sites
48
+ defined?(ObjectSpace::WeakKeyMap) ? ObjectSpace::WeakKeyMap.new
49
+ : {}.compare_by_identity
50
+ end
51
+ SITES = new_sites
49
52
 
53
+ # A Ractor other than the main one keeps its own table, and evaluates
54
+ # every block in its frame: the empty scope a lambda is compiled in is
55
+ # the top of a file, which only the main Ractor has.
50
56
  def self.compiled (block)
51
57
  site = RubyVM::InstructionSequence.of(block)
52
58
  return compile(block) unless site
53
- SITES[site] || (SITES[site] = compile(block))
59
+ sites = Ractor.current == Ractor.main ? SITES : (Ractor.current[:carray_fuse_sites] ||= new_sites)
60
+ sites[site] || (sites[site] = compile(block))
54
61
  end
55
62
 
56
63
  # The block's free locals, handed to a lambda that was compiled without
@@ -88,7 +95,7 @@ class CArray
88
95
  file, line = block.source_location
89
96
  scope = block.binding
90
97
  locals, needs_frame = free_locals(body, scope.local_variables)
91
- return InFrame.new(text, file, line) if needs_frame
98
+ return InFrame.new(text, file, line) if needs_frame || Ractor.current != Ractor.main
92
99
  Compiled.new(locals, compile_lambda(text, locals, scope, file, line))
93
100
  end
94
101