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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +568 -0
- data/README.md +6 -6
- data/carray.gemspec +1 -1
- data/ext/ca_axis_descriptor.h +27 -24
- data/ext/ca_axis_dispatch.c +220 -0
- data/ext/ca_axis_group.c +201 -123
- data/ext/ca_binop_dispatch.c +46 -52
- data/ext/ca_binop_dispatch.h +1 -1
- data/ext/ca_categorical_iterator.c +249 -183
- data/ext/ca_compare.c +7 -12
- data/ext/ca_for_buffer.h +21 -68
- data/ext/ca_group_iter.c +38 -23
- data/ext/ca_kernel_iterator.c +1765 -2076
- data/ext/ca_kernel_iterator.h +584 -566
- data/ext/ca_monop_dispatch.c +86 -63
- data/ext/ca_monop_dispatch.h +5 -0
- data/ext/ca_obj_array.c +223 -56
- data/ext/ca_obj_bincmp.c +172 -138
- data/ext/ca_obj_binop.c +116 -230
- data/ext/ca_obj_bitarray.c +40 -26
- data/ext/ca_obj_bitfield.c +100 -68
- data/ext/ca_obj_block.c +19 -9
- data/ext/ca_obj_byte_swap.c +45 -46
- data/ext/ca_obj_const_string.c +269 -44
- data/ext/ca_obj_face.c +612 -89
- data/ext/ca_obj_face.h +111 -47
- data/ext/ca_obj_fake.c +36 -79
- data/ext/ca_obj_farray.c +7 -3
- data/ext/ca_obj_field.c +33 -8
- data/ext/ca_obj_fixlen_string.c +23 -24
- data/ext/ca_obj_grid.c +541 -210
- data/ext/ca_obj_meld.c +331 -111
- data/ext/ca_obj_moncmp.c +80 -65
- data/ext/ca_obj_monop.c +177 -149
- data/ext/ca_obj_object.c +664 -248
- data/ext/ca_obj_record.c +63 -23
- data/ext/ca_obj_reduce.c +18 -16
- data/ext/ca_obj_refer.c +140 -58
- data/ext/ca_obj_remap.c +85 -24
- data/ext/ca_obj_repeat.c +24 -9
- data/ext/ca_obj_roll.c +121 -16
- data/ext/ca_obj_select.c +61 -58
- data/ext/ca_obj_select_axis.c +73 -107
- data/ext/ca_obj_shift.c +32 -102
- data/ext/ca_obj_stack.c +372 -122
- data/ext/ca_obj_stride.c +348 -140
- data/ext/ca_obj_string.c +88 -22
- data/ext/ca_obj_tile.c +38 -21
- data/ext/ca_obj_time.c +45 -31
- data/ext/ca_obj_timedelta.c +43 -29
- data/ext/ca_obj_transpose.c +17 -6
- data/ext/ca_obj_triop.c +125 -141
- data/ext/ca_obj_window.c +330 -44
- data/ext/ca_op_ipower.c +24 -11
- data/ext/ca_op_powi.h +57 -30
- data/ext/ca_pow5_table.h +656 -0
- data/ext/ca_sort_kernels.h +70 -0
- data/ext/ca_sweep_engine.c +104 -81
- data/ext/ca_sweep_engine.h +7 -20
- data/ext/ca_triop_dispatch.c +4 -1
- data/ext/ca_triop_dispatch.h +5 -2
- data/ext/caframe_csv_reader.c +542 -0
- data/ext/carray.h +241 -92
- data/ext/carray_access.c +608 -276
- data/ext/carray_address_basis.c +494 -97
- data/ext/carray_attribute.c +129 -4
- data/ext/carray_bincount.c +39 -7
- data/ext/carray_broadcast.c +56 -19
- data/ext/carray_build_flags.h +1 -1
- data/ext/carray_call_cfunc.c +108 -28
- data/ext/carray_cast.c +1576 -223
- data/ext/carray_cast_func.rb +77 -24
- data/ext/carray_class.c +4 -3
- data/ext/carray_conversion.c +208 -75
- data/ext/carray_copy.c +82 -24
- data/ext/carray_core.c +646 -162
- data/ext/carray_count.c +171 -88
- data/ext/carray_element.c +94 -60
- data/ext/carray_factorize.c +670 -324
- data/ext/carray_generate.c +260 -83
- data/ext/carray_histogram.c +209 -106
- data/ext/carray_hold.c +14 -16
- data/ext/carray_index_classifier.c +422 -243
- data/ext/carray_internal.h +692 -3
- data/ext/carray_kernels_bincmp.c +7 -0
- data/ext/carray_kernels_binop.c +487 -120
- data/ext/carray_kernels_init.c +653 -50
- data/ext/carray_kernels_moncmp.c +15 -8
- data/ext/carray_kernels_monop.c +99 -43
- data/ext/carray_kernels_reduce_aggregate.c +3925 -6112
- data/ext/carray_kernels_reduce_boolean.c +112 -36
- data/ext/carray_kernels_reduce_cumulative.c +2127 -6903
- data/ext/carray_kernels_reduce_extreme.c +3032 -2892
- data/ext/carray_kernels_reduce_variance.c +439 -500
- data/ext/carray_kernels_scan.c +1097 -488
- data/ext/carray_kernels_search.c +3787 -2176
- data/ext/carray_kernels_sort.c +1200 -920
- data/ext/carray_kernels_triop.c +479 -7
- data/ext/carray_key_windows.c +216 -0
- data/ext/carray_lazy.c +967 -153
- data/ext/carray_loop.c +81 -65
- data/ext/carray_mask.c +910 -324
- data/ext/carray_math_kernel.h +1 -0
- data/ext/carray_median_percentile.c +299 -100
- data/ext/carray_memory_view.c +872 -758
- data/ext/carray_operator.c +1171 -364
- data/ext/carray_order.c +505 -133
- data/ext/carray_partition.c +112 -65
- data/ext/carray_random.c +327 -239
- data/ext/carray_round.c +273 -0
- data/ext/carray_scatter.c +346 -78
- data/ext/carray_segment.c +257 -0
- data/ext/carray_slab.c +249 -86
- data/ext/carray_sort.c +224 -227
- data/ext/carray_sort_kernel.c +8 -1
- data/ext/carray_struct.c +118 -39
- data/ext/carray_test.c +193 -69
- data/ext/carray_time_text.c +1182 -0
- data/ext/carray_undef.c +4 -0
- data/ext/carray_utils.c +579 -113
- data/ext/extconf.rb +27 -2
- data/ext/mk_call_cfunc.rb +19 -4
- data/ext/mk_pow5_table.rb +35 -0
- data/ext/mkkernel.rb +1202 -1014
- data/ext/ruby_carray.c +17 -5
- data/ext/version.h +4 -4
- data/ext/version.rb +4 -2
- data/lib/carray/attribute.rb +160 -17
- data/lib/carray/autoload_carray.rb +23 -27
- data/lib/carray/autoload_method_extension.rb +20 -5
- data/lib/carray/axis_group.rb +360 -63
- data/lib/carray/basics.rb +162 -28
- data/lib/carray/bincount_nd.rb +18 -2
- data/lib/carray/block_iterator.rb +231 -58
- data/lib/carray/boolean_reduce.rb +34 -42
- data/lib/carray/categorical.rb +242 -52
- data/lib/carray/categorical_iterator.rb +437 -827
- data/lib/carray/complex.rb +41 -34
- data/lib/carray/conditional.rb +150 -14
- data/lib/carray/const_string.rb +59 -4
- data/lib/carray/construct.rb +15 -11
- data/lib/carray/data_type_extension.rb +1 -0
- data/lib/carray/distance_limit.rb +77 -0
- data/lib/carray/fixlen_string.rb +7 -1
- data/lib/carray/frame/column_verbs.rb +70 -0
- data/lib/carray/frame/concat.rb +157 -6
- data/lib/carray/frame/convert.rb +82 -7
- data/lib/carray/frame/csv_parser.rb +688 -87
- data/lib/carray/frame/describe.rb +80 -0
- data/lib/carray/frame/frame.rb +369 -45
- data/lib/carray/frame/group.rb +631 -90
- data/lib/carray/frame/io.rb +506 -79
- data/lib/carray/frame/join.rb +411 -62
- data/lib/carray/frame/records.rb +116 -19
- data/lib/carray/frame/reshape.rb +455 -0
- data/lib/carray/frame/sort.rb +30 -18
- data/lib/carray/frame/verbs.rb +868 -61
- data/lib/carray/frame/windows.rb +117 -0
- data/lib/carray/frame.rb +4 -0
- data/lib/carray/fuse_source.rb +118 -2
- data/lib/carray/fusion.rb +257 -42
- data/lib/carray/histogram.rb +6 -1
- data/lib/carray/inspect.rb +54 -13
- data/lib/carray/iterator.rb +99 -9
- data/lib/carray/key_windows.rb +620 -0
- data/lib/carray/lazy.rb +216 -193
- data/lib/carray/mask_gap_fill.rb +112 -92
- data/lib/carray/math.rb +32 -19
- data/lib/carray/meld_reduce.rb +52 -21
- data/lib/carray/methods/align_addr.rb +8 -4
- data/lib/carray/methods/bin.rb +26 -18
- data/lib/carray/methods/bincount.rb +18 -2
- data/lib/carray/methods/bit_string.rb +15 -4
- data/lib/carray/methods/choose.rb +16 -3
- data/lib/carray/methods/composition.rb +18 -10
- data/lib/carray/methods/factorize.rb +2 -2
- data/lib/carray/methods/gather_nd.rb +25 -5
- data/lib/carray/methods/insert_block.rb +9 -14
- data/lib/carray/methods/is_between.rb +59 -0
- data/lib/carray/methods/is_in.rb +140 -10
- data/lib/carray/methods/join.rb +2 -7
- data/lib/carray/methods/lag.rb +49 -0
- data/lib/carray/methods/locate_addr.rb +182 -45
- data/lib/carray/methods/mode.rb +18 -1
- data/lib/carray/methods/nunique.rb +11 -6
- data/lib/carray/methods/repeat.rb +1 -20
- data/lib/carray/methods/resize.rb +21 -23
- data/lib/carray/methods/snap.rb +34 -12
- data/lib/carray/methods/unique.rb +11 -2
- data/lib/carray/methods/value_counts.rb +4 -11
- data/lib/carray/record_order.rb +208 -0
- data/lib/carray/rng.rb +3 -0
- data/lib/carray/runtime.rb +65 -17
- data/lib/carray/segment_iterator.rb +1221 -0
- data/lib/carray/serialize.rb +214 -30
- data/lib/carray/slab_iterator.rb +101 -27
- data/lib/carray/stack.rb +10 -4
- data/lib/carray/string.rb +105 -5
- data/lib/carray/string_operation_extension.rb +12 -4
- data/lib/carray/struct.rb +137 -58
- data/lib/carray/struct_builder.rb +124 -17
- data/lib/carray/time.rb +1222 -186
- data/lib/carray/window_iterator.rb +242 -81
- data/lib/carray.rb +9 -2
- data/yard-stubs/ca_obj_array.rb +4 -4
- data/yard-stubs/ca_obj_bitarray.rb +7 -0
- data/yard-stubs/ca_obj_bitfield.rb +23 -12
- data/yard-stubs/ca_obj_fake.rb +1 -1
- data/yard-stubs/ca_obj_field.rb +5 -1
- data/yard-stubs/ca_obj_grid.rb +21 -10
- data/yard-stubs/ca_obj_refer.rb +5 -4
- data/yard-stubs/ca_obj_roll.rb +2 -3
- data/yard-stubs/ca_obj_shift.rb +5 -1
- data/yard-stubs/ca_obj_stride.rb +10 -7
- data/yard-stubs/ca_obj_tile.rb +1 -1
- data/yard-stubs/ca_obj_transpose.rb +1 -1
- data/yard-stubs/ca_obj_window.rb +9 -5
- data/yard-stubs/carray_access.rb +11 -5
- data/yard-stubs/carray_attribute.rb +0 -5
- data/yard-stubs/carray_broadcast.rb +1 -1
- data/yard-stubs/carray_cast.rb +88 -43
- data/yard-stubs/carray_conversion.rb +15 -2
- data/yard-stubs/carray_copy.rb +12 -5
- data/yard-stubs/carray_core.rb +3 -3
- data/yard-stubs/carray_count.rb +9 -0
- data/yard-stubs/carray_element.rb +10 -4
- data/yard-stubs/carray_generate.rb +21 -1
- data/yard-stubs/carray_lazy.rb +5 -1
- data/yard-stubs/carray_loop.rb +4 -1
- data/yard-stubs/carray_mask.rb +60 -12
- data/yard-stubs/carray_math.rb +52 -14
- data/yard-stubs/carray_median_percentile.rb +15 -8
- data/yard-stubs/carray_memory_view.rb +40 -23
- data/yard-stubs/carray_order.rb +61 -29
- data/yard-stubs/carray_random.rb +13 -4
- data/yard-stubs/carray_scatter.rb +21 -9
- data/yard-stubs/carray_segment.rb +72 -0
- data/yard-stubs/carray_slab.rb +10 -2
- data/yard-stubs/carray_sort.rb +49 -8
- data/yard-stubs/carray_test.rb +15 -3
- data/yard-stubs/carray_utils.rb +0 -26
- metadata +19 -5
- data/ext/ca_for_each_element.h +0 -239
- data/ext/carray_kernels_map.c +0 -3467
- data/lib/carray/arrow_tensor.rb +0 -401
- data/lib/carray/table.rb +0 -108
data/lib/carray/frame/records.rb
CHANGED
|
@@ -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
|
|
20
|
-
#
|
|
21
|
-
|
|
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)
|
|
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
|
|
58
|
-
#
|
|
59
|
-
#
|
|
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
|
|
87
|
-
# + to_type (nil rows -> UNDEF, int/float by leaf)
|
|
88
|
-
#
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
unless
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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?(
|
|
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
|