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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +824 -0
- data/README.md +5 -6
- data/carray.gemspec +1 -1
- data/ext/ca_axis_dispatch.c +258 -4
- data/ext/ca_axis_group.c +368 -184
- data/ext/ca_binop_dispatch.c +54 -32
- data/ext/ca_binop_dispatch.h +1 -0
- data/ext/ca_categorical_iterator.c +90 -41
- data/ext/ca_compare.c +8 -2
- data/ext/ca_for_buffer.h +21 -68
- data/ext/ca_group_iter.c +27 -11
- data/ext/ca_kernel_iterator.c +960 -245
- data/ext/ca_kernel_iterator.h +352 -181
- data/ext/ca_monop_dispatch.c +4 -4
- data/ext/ca_monop_dispatch.h +4 -0
- data/ext/ca_obj_array.c +194 -42
- data/ext/ca_obj_bincmp.c +152 -121
- data/ext/ca_obj_binop.c +107 -210
- data/ext/ca_obj_bitarray.c +37 -24
- data/ext/ca_obj_bitfield.c +95 -62
- data/ext/ca_obj_block.c +5 -4
- data/ext/ca_obj_byte_swap.c +40 -40
- data/ext/ca_obj_const_string.c +180 -50
- data/ext/ca_obj_face.c +428 -62
- data/ext/ca_obj_face.h +107 -47
- data/ext/ca_obj_fake.c +21 -50
- data/ext/ca_obj_farray.c +1 -0
- data/ext/ca_obj_field.c +16 -3
- data/ext/ca_obj_fixlen_string.c +39 -26
- data/ext/ca_obj_grid.c +181 -124
- data/ext/ca_obj_meld.c +235 -55
- data/ext/ca_obj_moncmp.c +61 -61
- data/ext/ca_obj_monop.c +125 -107
- data/ext/ca_obj_object.c +551 -233
- data/ext/ca_obj_record.c +51 -16
- data/ext/ca_obj_reduce.c +5 -8
- data/ext/ca_obj_refer.c +117 -33
- data/ext/ca_obj_remap.c +57 -17
- data/ext/ca_obj_repeat.c +21 -5
- data/ext/ca_obj_roll.c +115 -11
- data/ext/ca_obj_select.c +88 -75
- data/ext/ca_obj_select_axis.c +58 -60
- data/ext/ca_obj_shift.c +22 -102
- data/ext/ca_obj_stack.c +223 -123
- data/ext/ca_obj_stride.c +267 -35
- data/ext/ca_obj_string.c +25 -22
- data/ext/ca_obj_tile.c +32 -16
- data/ext/ca_obj_time.c +23 -25
- data/ext/ca_obj_timedelta.c +21 -23
- data/ext/ca_obj_transpose.c +12 -3
- data/ext/ca_obj_triop.c +110 -117
- data/ext/ca_obj_window.c +157 -29
- data/ext/ca_op_ipower.c +25 -13
- data/ext/ca_op_powi.h +57 -30
- data/ext/ca_pow5_table.h +656 -0
- data/ext/ca_rng_normal.h +42 -0
- data/ext/ca_rng_xoshiro256pp.h +105 -0
- data/ext/ca_sort_kernels.h +70 -0
- data/ext/ca_sweep_engine.c +385 -198
- data/ext/ca_sweep_engine.h +31 -23
- data/ext/ca_triop_dispatch.c +4 -1
- data/ext/ca_triop_dispatch.h +5 -2
- data/ext/caframe_csv_reader.c +463 -0
- data/ext/carray.h +90 -28
- data/ext/carray_access.c +487 -231
- data/ext/carray_address_basis.c +731 -0
- data/ext/carray_attribute.c +45 -3
- data/ext/carray_bincount.c +32 -2
- data/ext/carray_broadcast.c +49 -17
- data/ext/carray_call_cfunc.c +667 -511
- data/ext/carray_cast.c +1556 -247
- data/ext/carray_cast_func.rb +34 -21
- data/ext/carray_conversion.c +197 -73
- data/ext/carray_copy.c +84 -38
- data/ext/carray_core.c +591 -119
- data/ext/carray_count.c +148 -96
- data/ext/carray_element.c +18 -27
- data/ext/carray_factorize.c +604 -291
- data/ext/carray_generate.c +76 -40
- data/ext/carray_histogram.c +152 -94
- data/ext/carray_hold.c +2 -1
- data/ext/carray_index_classifier.c +63 -96
- data/ext/carray_internal.h +400 -1
- data/ext/carray_lazy.c +834 -79
- data/ext/carray_loop.c +88 -66
- data/ext/carray_mask.c +705 -324
- data/ext/carray_math_kernel.h +1 -0
- data/ext/carray_median_percentile.c +263 -63
- data/ext/carray_memory_view.c +254 -248
- data/ext/carray_operator.c +1093 -363
- data/ext/carray_order.c +133 -69
- data/ext/carray_partition.c +102 -56
- data/ext/carray_random.c +582 -204
- data/ext/carray_scatter.c +308 -65
- data/ext/carray_segment.c +257 -0
- data/ext/carray_slab.c +183 -70
- data/ext/carray_sort.c +235 -240
- data/ext/carray_sort_kernel.c +8 -1
- data/ext/carray_struct.c +99 -21
- data/ext/carray_test.c +167 -64
- data/ext/carray_time_text.c +1182 -0
- data/ext/carray_utils.c +363 -98
- data/ext/extconf.rb +27 -2
- data/ext/mk_call_cfunc.rb +103 -118
- data/ext/mk_pow5_table.rb +35 -0
- data/ext/mkkernel.rb +1349 -851
- data/ext/ruby_carray.c +17 -2
- data/ext/version.h +4 -4
- data/ext/version.rb +4 -2
- data/lib/carray/attribute.rb +39 -0
- data/lib/carray/autoload_carray.rb +24 -3
- data/lib/carray/autoload_method_extension.rb +18 -0
- data/lib/carray/axis_group.rb +245 -20
- data/lib/carray/basics.rb +95 -25
- data/lib/carray/bincount_nd.rb +6 -0
- data/lib/carray/block_iterator.rb +200 -54
- data/lib/carray/boolean_reduce.rb +21 -29
- data/lib/carray/categorical.rb +244 -59
- data/lib/carray/categorical_iterator.rb +391 -757
- data/lib/carray/complex.rb +36 -29
- data/lib/carray/conditional.rb +94 -7
- data/lib/carray/const_string.rb +177 -27
- data/lib/carray/construct.rb +40 -0
- data/lib/carray/data_type_extension.rb +3 -0
- data/lib/carray/data_type_limits.rb +91 -0
- data/lib/carray/fixlen_string.rb +1 -1
- data/lib/carray/frame/concat.rb +157 -6
- data/lib/carray/frame/convert.rb +13 -7
- data/lib/carray/frame/csv_parser.rb +590 -76
- data/lib/carray/frame/describe.rb +79 -0
- data/lib/carray/frame/frame.rb +205 -35
- data/lib/carray/frame/group.rb +199 -40
- data/lib/carray/frame/io.rb +452 -60
- data/lib/carray/frame/join.rb +134 -46
- data/lib/carray/frame/records.rb +54 -19
- data/lib/carray/frame/reshape.rb +448 -0
- data/lib/carray/frame/sort.rb +26 -2
- data/lib/carray/frame/verbs.rb +726 -56
- data/lib/carray/frame.rb +2 -0
- data/lib/carray/fuse_source.rb +111 -2
- data/lib/carray/fusion.rb +229 -38
- data/lib/carray/inspect.rb +71 -20
- data/lib/carray/iterator.rb +157 -2
- data/lib/carray/lazy.rb +54 -67
- data/lib/carray/mask_gap_fill.rb +66 -50
- data/lib/carray/math.rb +32 -19
- data/lib/carray/meld_reduce.rb +40 -7
- data/lib/carray/methods/align_addr.rb +8 -4
- data/lib/carray/methods/bincount.rb +2 -2
- data/lib/carray/methods/bit_string.rb +14 -3
- data/lib/carray/methods/composition.rb +16 -10
- data/lib/carray/methods/discovery_along.rb +74 -0
- data/lib/carray/methods/factorize.rb +50 -0
- data/lib/carray/methods/gather_nd.rb +13 -2
- data/lib/carray/methods/insert_block.rb +9 -14
- data/lib/carray/methods/is_in.rb +130 -6
- data/lib/carray/methods/join.rb +1 -1
- data/lib/carray/methods/locate_addr.rb +235 -21
- data/lib/carray/methods/mask_duplicates.rb +35 -1
- data/lib/carray/methods/mode.rb +18 -1
- data/lib/carray/methods/nunique.rb +22 -1
- data/lib/carray/methods/repeat.rb +91 -0
- data/lib/carray/methods/resize.rb +21 -23
- data/lib/carray/methods/snap.rb +34 -12
- data/lib/carray/methods/unique.rb +52 -3
- data/lib/carray/record_order.rb +208 -0
- data/lib/carray/rng.rb +86 -0
- data/lib/carray/runtime.rb +14 -11
- data/lib/carray/segment_iterator.rb +990 -0
- data/lib/carray/serialize.rb +197 -27
- data/lib/carray/slab_iterator.rb +109 -40
- data/lib/carray/stack.rb +10 -4
- data/lib/carray/string_operation_extension.rb +9 -4
- data/lib/carray/struct.rb +142 -51
- data/lib/carray/struct_builder.rb +110 -14
- data/lib/carray/table.rb +2 -2
- data/lib/carray/time.rb +535 -105
- data/lib/carray/window_iterator.rb +259 -82
- data/lib/carray.rb +4 -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_block.rb +2 -7
- data/yard-stubs/ca_obj_fake.rb +1 -1
- data/yard-stubs/ca_obj_field.rb +1 -1
- data/yard-stubs/ca_obj_refer.rb +3 -3
- data/yard-stubs/ca_obj_roll.rb +0 -1
- data/yard-stubs/ca_obj_stride.rb +10 -7
- data/yard-stubs/ca_obj_transpose.rb +1 -1
- data/yard-stubs/ca_obj_window.rb +11 -4
- data/yard-stubs/carray_access.rb +6 -5
- data/yard-stubs/carray_broadcast.rb +2 -2
- data/yard-stubs/carray_cast.rb +78 -43
- data/yard-stubs/carray_conversion.rb +13 -1
- data/yard-stubs/carray_copy.rb +8 -3
- data/yard-stubs/carray_core.rb +3 -83
- data/yard-stubs/carray_count.rb +16 -2
- data/yard-stubs/carray_lazy.rb +210 -1
- data/yard-stubs/carray_loop.rb +4 -1
- data/yard-stubs/carray_mask.rb +34 -10
- data/yard-stubs/carray_math.rb +1499 -4
- data/yard-stubs/carray_median_percentile.rb +26 -8
- data/yard-stubs/carray_order.rb +43 -82
- data/yard-stubs/carray_scatter.rb +10 -3
- data/yard-stubs/carray_segment.rb +72 -0
- data/yard-stubs/carray_slab.rb +11 -8
- data/yard-stubs/carray_sort.rb +50 -8
- data/yard-stubs/carray_test.rb +10 -0
- data/yard-stubs/carray_utils.rb +0 -26
- metadata +19 -19
- data/ext/ca_for_each_element.h +0 -239
- data/ext/carray_build_flags.h +0 -3
- data/ext/carray_config.h +0 -28
- data/ext/carray_kernels_bincmp.c +0 -4446
- data/ext/carray_kernels_binop.c +0 -11001
- data/ext/carray_kernels_init.c +0 -1131
- data/ext/carray_kernels_map.c +0 -3467
- data/ext/carray_kernels_moncmp.c +0 -2097
- data/ext/carray_kernels_monop.c +0 -18313
- data/ext/carray_kernels_reduce_aggregate.c +0 -25837
- data/ext/carray_kernels_reduce_boolean.c +0 -330
- data/ext/carray_kernels_reduce_cumulative.c +0 -14593
- data/ext/carray_kernels_reduce_extreme.c +0 -16948
- data/ext/carray_kernels_reduce_variance.c +0 -3910
- data/ext/carray_kernels_scan.c +0 -3693
- data/ext/carray_kernels_search.c +0 -32138
- data/ext/carray_kernels_sort.c +0 -10626
- 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
|
data/lib/carray/frame/sort.rb
CHANGED
|
@@ -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
|
-
|
|
41
|
+
named = specs.map do |spec|
|
|
42
42
|
name, dir = parse_sort_key_spec(spec, order)
|
|
43
|
-
|
|
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
|