carray 3.0.3 → 3.0.5
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 +469 -0
- data/README.md +3 -2
- data/carray.gemspec +1 -1
- data/ext/ca_array_pool.c +25 -1
- data/ext/ca_axis_descriptor.h +27 -24
- data/ext/ca_axis_dispatch.c +14 -26
- data/ext/ca_axis_group.c +27 -27
- data/ext/ca_binop_dispatch.c +1 -29
- data/ext/ca_binop_dispatch.h +0 -1
- data/ext/ca_categorical_iterator.c +231 -153
- data/ext/ca_compare.c +9 -20
- data/ext/ca_composite_dispatch.c +9 -9
- data/ext/ca_group_iter.c +16 -17
- data/ext/ca_kernel_iterator.c +1240 -2000
- data/ext/ca_kernel_iterator.h +502 -519
- data/ext/ca_monop_dispatch.c +82 -59
- data/ext/ca_monop_dispatch.h +1 -0
- data/ext/ca_obj_array.c +152 -45
- data/ext/ca_obj_bincmp.c +25 -21
- data/ext/ca_obj_binop.c +16 -26
- data/ext/ca_obj_bitarray.c +8 -3
- data/ext/ca_obj_bitfield.c +63 -49
- data/ext/ca_obj_block.c +18 -9
- data/ext/ca_obj_byte_swap.c +7 -7
- data/ext/ca_obj_const_string.c +188 -22
- data/ext/ca_obj_face.c +280 -58
- data/ext/ca_obj_face.h +19 -0
- data/ext/ca_obj_fake.c +15 -29
- data/ext/ca_obj_farray.c +6 -3
- data/ext/ca_obj_field.c +30 -8
- data/ext/ca_obj_fixlen_string.c +2 -3
- data/ext/ca_obj_grid.c +455 -134
- data/ext/ca_obj_meld.c +172 -34
- data/ext/ca_obj_moncmp.c +23 -8
- data/ext/ca_obj_monop.c +113 -67
- data/ext/ca_obj_object.c +210 -52
- data/ext/ca_obj_record.c +15 -7
- data/ext/ca_obj_reduce.c +17 -8
- data/ext/ca_obj_refer.c +42 -35
- data/ext/ca_obj_remap.c +30 -9
- data/ext/ca_obj_repeat.c +9 -10
- data/ext/ca_obj_roll.c +31 -10
- data/ext/ca_obj_select.c +48 -14
- data/ext/ca_obj_select_axis.c +15 -47
- data/ext/ca_obj_shift.c +41 -7
- data/ext/ca_obj_stack.c +180 -34
- data/ext/ca_obj_stride.c +190 -134
- data/ext/ca_obj_string.c +80 -4
- data/ext/ca_obj_tile.c +32 -11
- data/ext/ca_obj_time.c +22 -6
- data/ext/ca_obj_timedelta.c +22 -6
- data/ext/ca_obj_transpose.c +13 -7
- data/ext/ca_obj_triop.c +26 -32
- data/ext/ca_obj_window.c +267 -74
- data/ext/ca_op_ipower.c +4 -4
- data/ext/ca_sort_kernels.h +6 -3
- data/ext/ca_sweep_engine.c +2 -2
- data/ext/caframe_csv_reader.c +80 -1
- data/ext/carray.h +214 -75
- data/ext/carray_access.c +200 -99
- data/ext/carray_address_basis.c +343 -87
- data/ext/carray_attribute.c +113 -6
- data/ext/carray_bincount.c +10 -8
- data/ext/carray_broadcast.c +13 -8
- data/ext/carray_build_flags.h +3 -0
- data/ext/carray_call_cfunc.c +108 -0
- data/ext/carray_cast.c +375 -96
- data/ext/carray_cast_func.rb +90 -20
- data/ext/carray_class.c +4 -3
- data/ext/carray_config.h +28 -0
- data/ext/carray_conversion.c +18 -9
- data/ext/carray_copy.c +51 -14
- data/ext/carray_core.c +150 -55
- data/ext/carray_count.c +56 -20
- data/ext/carray_element.c +81 -33
- data/ext/carray_factorize.c +138 -84
- data/ext/carray_generate.c +199 -52
- data/ext/carray_histogram.c +63 -18
- data/ext/carray_hold.c +12 -15
- data/ext/carray_index_classifier.c +388 -176
- data/ext/carray_internal.h +348 -29
- data/ext/carray_kernels_bincmp.c +4453 -0
- data/ext/carray_kernels_binop.c +11368 -0
- data/ext/carray_kernels_init.c +1734 -0
- data/ext/carray_kernels_moncmp.c +2104 -0
- data/ext/carray_kernels_monop.c +18369 -0
- data/ext/carray_kernels_reduce_aggregate.c +23818 -0
- data/ext/carray_kernels_reduce_boolean.c +406 -0
- data/ext/carray_kernels_reduce_cumulative.c +10086 -0
- data/ext/carray_kernels_reduce_extreme.c +17634 -0
- data/ext/carray_kernels_reduce_variance.c +3849 -0
- data/ext/carray_kernels_scan.c +4302 -0
- data/ext/carray_kernels_search.c +33829 -0
- data/ext/carray_kernels_sort.c +10897 -0
- data/ext/carray_kernels_triop.c +1864 -0
- data/ext/carray_key_windows.c +220 -0
- data/ext/carray_lazy.c +170 -111
- data/ext/carray_mask.c +337 -94
- data/ext/carray_median_percentile.c +82 -28
- data/ext/carray_memory_view.c +669 -561
- data/ext/carray_operator.c +129 -52
- data/ext/carray_order.c +604 -77
- data/ext/carray_partition.c +28 -27
- data/ext/carray_random.c +85 -31
- data/ext/carray_round.c +273 -0
- data/ext/carray_scatter.c +87 -45
- data/ext/carray_segment.c +3 -3
- data/ext/carray_slab.c +123 -37
- data/ext/carray_sort.c +259 -51
- data/ext/carray_sort_kernel.c +5 -7
- data/ext/carray_struct.c +54 -61
- data/ext/carray_test.c +26 -5
- data/ext/carray_time_text.c +4 -2
- data/ext/carray_undef.c +4 -0
- data/ext/carray_utils.c +309 -66
- data/ext/mk_call_cfunc.rb +19 -2
- data/ext/mkkernel.rb +403 -350
- data/ext/ruby_carray.c +10 -4
- data/ext/version.h +4 -4
- data/lib/carray/attribute.rb +139 -22
- data/lib/carray/autoload_carray.rb +8 -31
- data/lib/carray/autoload_method_extension.rb +14 -5
- data/lib/carray/axis_group.rb +255 -106
- data/lib/carray/basics.rb +105 -17
- data/lib/carray/bincount_nd.rb +12 -2
- data/lib/carray/block_iterator.rb +126 -23
- data/lib/carray/boolean_reduce.rb +17 -17
- data/lib/carray/categorical.rb +195 -59
- data/lib/carray/categorical_iterator.rb +132 -29
- data/lib/carray/complex.rb +5 -5
- data/lib/carray/conditional.rb +67 -18
- data/lib/carray/const_string.rb +24 -5
- data/lib/carray/construct.rb +15 -11
- data/lib/carray/data_type_extension.rb +9 -1
- data/lib/carray/distance_limit.rb +77 -0
- data/lib/carray/fixlen_string.rb +48 -3
- data/lib/carray/frame/column_verbs.rb +71 -0
- data/lib/carray/frame/concat.rb +17 -3
- data/lib/carray/frame/convert.rb +69 -0
- data/lib/carray/frame/csv_parser.rb +111 -17
- data/lib/carray/frame/describe.rb +3 -2
- data/lib/carray/frame/frame.rb +252 -28
- data/lib/carray/frame/group.rb +603 -101
- data/lib/carray/frame/io.rb +117 -28
- data/lib/carray/frame/join.rb +312 -51
- data/lib/carray/frame/records.rb +83 -7
- data/lib/carray/frame/reshape.rb +42 -35
- data/lib/carray/frame/sort.rb +7 -19
- data/lib/carray/frame/verbs.rb +188 -48
- data/lib/carray/frame/windows.rb +117 -0
- data/lib/carray/frame.rb +2 -0
- data/lib/carray/fuse_source.rb +42 -6
- data/lib/carray/fusion.rb +40 -16
- data/lib/carray/histogram.rb +6 -1
- data/lib/carray/inspect.rb +31 -8
- data/lib/carray/iterator.rb +85 -7
- data/lib/carray/key_windows.rb +620 -0
- data/lib/carray/lazy.rb +217 -183
- data/lib/carray/mask_gap_fill.rb +63 -57
- data/lib/carray/meld_reduce.rb +12 -14
- data/lib/carray/methods/bin.rb +26 -18
- data/lib/carray/methods/bincount.rb +16 -0
- data/lib/carray/methods/bit_string.rb +1 -1
- data/lib/carray/methods/choose.rb +16 -3
- data/lib/carray/methods/composition.rb +3 -1
- data/lib/carray/methods/gather_nd.rb +14 -5
- data/lib/carray/methods/insert_block.rb +2 -2
- data/lib/carray/methods/is_between.rb +59 -0
- data/lib/carray/methods/is_in.rb +61 -37
- data/lib/carray/methods/join.rb +1 -6
- data/lib/carray/methods/lag.rb +49 -0
- data/lib/carray/methods/locate_addr.rb +47 -51
- data/lib/carray/methods/mode.rb +1 -1
- data/lib/carray/methods/nunique.rb +11 -6
- data/lib/carray/methods/resize.rb +3 -3
- data/lib/carray/methods/value_counts.rb +4 -11
- data/lib/carray/record_order.rb +3 -3
- data/lib/carray/rng.rb +3 -0
- data/lib/carray/runtime.rb +53 -8
- data/lib/carray/segment_iterator.rb +323 -88
- data/lib/carray/serialize.rb +45 -14
- data/lib/carray/slab_iterator.rb +50 -0
- data/lib/carray/stack.rb +15 -12
- data/lib/carray/string.rb +113 -5
- data/lib/carray/string_operation_extension.rb +21 -6
- data/lib/carray/struct.rb +9 -21
- data/lib/carray/struct_builder.rb +24 -11
- data/lib/carray/time.rb +819 -158
- data/lib/carray/window_iterator.rb +122 -16
- data/lib/carray.rb +7 -0
- data/yard-stubs/ca_obj_bitfield.rb +1 -1
- data/yard-stubs/ca_obj_field.rb +4 -0
- data/yard-stubs/ca_obj_grid.rb +21 -10
- data/yard-stubs/ca_obj_refer.rb +2 -1
- data/yard-stubs/ca_obj_roll.rb +2 -2
- data/yard-stubs/ca_obj_shift.rb +5 -1
- data/yard-stubs/ca_obj_tile.rb +1 -1
- data/yard-stubs/ca_obj_transpose.rb +5 -4
- data/yard-stubs/ca_obj_window.rb +8 -3
- data/yard-stubs/carray_access.rb +6 -1
- data/yard-stubs/carray_attribute.rb +3 -6
- data/yard-stubs/carray_cast.rb +12 -0
- data/yard-stubs/carray_conversion.rb +7 -2
- data/yard-stubs/carray_copy.rb +4 -2
- data/yard-stubs/carray_count.rb +4 -4
- data/yard-stubs/carray_element.rb +17 -10
- data/yard-stubs/carray_generate.rb +21 -1
- data/yard-stubs/carray_mask.rb +32 -3
- data/yard-stubs/carray_math.rb +41 -5
- data/yard-stubs/carray_median_percentile.rb +5 -2
- data/yard-stubs/carray_memory_view.rb +40 -23
- data/yard-stubs/carray_order.rb +129 -22
- data/yard-stubs/carray_random.rb +13 -4
- data/yard-stubs/carray_scatter.rb +17 -10
- data/yard-stubs/carray_slab.rb +9 -2
- data/yard-stubs/carray_sort.rb +6 -5
- data/yard-stubs/carray_test.rb +5 -3
- metadata +25 -3
- data/lib/carray/arrow_tensor.rb +0 -401
- data/lib/carray/table.rb +0 -108
data/lib/carray/frame/group.rb
CHANGED
|
@@ -9,12 +9,72 @@
|
|
|
9
9
|
class CAFrame
|
|
10
10
|
# Group rows by one or more keys. Each key is a column name (String) or an
|
|
11
11
|
# external length-N CArray. Returns a GroupedFrame.
|
|
12
|
+
#
|
|
13
|
+
# With one key, a summary's index is the key's value of each group. With
|
|
14
|
+
# several, each key is a column of a summary, holding its part of each
|
|
15
|
+
# group's key, and the summary has no index; a CArray key among several
|
|
16
|
+
# names its column by its position, "key0", "key1", ....
|
|
12
17
|
def group_by(*keys)
|
|
13
18
|
raise ArgumentError, "group_by needs at least one key" if keys.empty?
|
|
19
|
+
CAFrame.__send__(:refuse_repeated_names, keys.grep(String), "group_by")
|
|
14
20
|
cat = grouping_categorical(keys)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
if keys.size == 1
|
|
22
|
+
axis = keys.first.is_a?(String) ? keys.first : "group"
|
|
23
|
+
return GroupedFrame.new(self, cat, axis, keys.grep(String), key: key_column(keys.first))
|
|
24
|
+
end
|
|
25
|
+
names = keys.each_with_index.map { |k, i| k.is_a?(String) ? k : "key#{i}" }
|
|
26
|
+
CAFrame.__send__(:refuse_repeated_names, names, "group_by")
|
|
27
|
+
GroupedFrame.new(self, cat, "group", keys.grep(String),
|
|
28
|
+
key_columns: names.zip(keys.map { |k| key_column(k) }).to_h)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Whether each row repeats another row: a boolean column, true for the rows
|
|
32
|
+
# whose values in the named columns (every column when none are named)
|
|
33
|
+
# appear on more than one row, except the one +except:+ leaves out --
|
|
34
|
+
#
|
|
35
|
+
# except: :first every occurrence but the first (the default)
|
|
36
|
+
# except: :last every occurrence but the last
|
|
37
|
+
# except: :none every occurrence
|
|
38
|
+
#
|
|
39
|
+
# so +df[df.is_duplicate.not]+ keeps one row of each, and
|
|
40
|
+
# +is_duplicate(except: :none).not+ is the rows that appear only once.
|
|
41
|
+
# Values compare as group_by compares keys (NaN is one value, -0.0 is 0.0),
|
|
42
|
+
# and a row with a masked cell in those columns is never a duplicate, as a
|
|
43
|
+
# masked key is in no group. The index is compared only when named
|
|
44
|
+
# (by the row axis name, as group_by takes it).
|
|
45
|
+
#
|
|
46
|
+
# df[df.is_duplicate.not] # the rows without repeats
|
|
47
|
+
# df.is_duplicate("station", "time") # repeats of the pair
|
|
48
|
+
# df.is_duplicate(except: :none).not # the rows that appear once
|
|
49
|
+
#
|
|
50
|
+
# @param names [Array<String>] the columns to compare; default every column.
|
|
51
|
+
# @param except [Symbol] the occurrence left unmarked: :first, :last or :none.
|
|
52
|
+
# @return [CArray] boolean, one value per row.
|
|
53
|
+
# @raise [KeyError] for a name that is no column.
|
|
54
|
+
# @raise [ArgumentError] when a compared column is N-D, there is no column
|
|
55
|
+
# to compare, or +except:+ is not one of the three.
|
|
56
|
+
def is_duplicate(*names, except: :first)
|
|
57
|
+
unless %i[first last none].include?(except)
|
|
58
|
+
raise ArgumentError,
|
|
59
|
+
"is_duplicate: except: must be :first, :last or :none (got #{except.inspect})"
|
|
60
|
+
end
|
|
61
|
+
names = column_keys(names, "is_duplicate")
|
|
62
|
+
names = column_names if names.empty?
|
|
63
|
+
raise ArgumentError, "is_duplicate: the frame has no column to compare" if names.empty?
|
|
64
|
+
cols = names.map { |n| key_column(n, "is_duplicate") }
|
|
65
|
+
cat = CACategorical.send(:from_keys, cols)
|
|
66
|
+
codes = cat.codes
|
|
67
|
+
out =
|
|
68
|
+
case except
|
|
69
|
+
when :first then codes.mask_duplicates.is_masked
|
|
70
|
+
when :last then codes.flip(0).mask_duplicates.is_masked.flip(0).copy
|
|
71
|
+
when :none
|
|
72
|
+
index = codes.value.int64
|
|
73
|
+
index[codes.is_masked] = 0 if codes.has_mask?
|
|
74
|
+
sizes = cat.category_sizes
|
|
75
|
+
sizes.elements > 0 ? sizes[index].gt(1) : CArray.boolean(nrow) { false }
|
|
76
|
+
end
|
|
77
|
+
codes.has_mask? ? out & codes.is_not_masked : out
|
|
18
78
|
end
|
|
19
79
|
|
|
20
80
|
# Group rows into time bins of length +unit+ along the time column (or the
|
|
@@ -38,17 +98,28 @@ class CAFrame
|
|
|
38
98
|
# as an empty reduction does: UNDEF for +mean+, 0 for +count+ and +sum+.
|
|
39
99
|
# A row whose time is masked belongs to no bin.
|
|
40
100
|
#
|
|
101
|
+
# +by:+ (a column name or an Array of them) bins each group of rows on its
|
|
102
|
+
# own: a group is the rows sharing the +by:+ values and a bin. The result
|
|
103
|
+
# then has no index: the +by:+ columns and the bin times (named +name+)
|
|
104
|
+
# are its first columns, the groups in the order their +by:+ values first
|
|
105
|
+
# appear and the bins of each in time order. With +fill: true+ every group
|
|
106
|
+
# has every bin from the first to the last of the whole frame. A row whose
|
|
107
|
+
# +by:+ value is masked belongs to no group.
|
|
108
|
+
#
|
|
109
|
+
# df.resample("date", "1 week", by: "shop").sum
|
|
110
|
+
#
|
|
41
111
|
# @param name [String] the time column, or the index's axis name.
|
|
42
112
|
# @param unit [String, Symbol, CATime::Resolution] the bin length.
|
|
43
113
|
# @param origin [nil, Time, String, CATime::Element] where bins start.
|
|
44
114
|
# @param label [:left, :right] which end of a bin names it and is closed.
|
|
45
115
|
# @param fill [Boolean] whether bins with no rows are rows of the result.
|
|
116
|
+
# @param by [nil, String, Array<String>] columns whose groups are binned apart.
|
|
46
117
|
# @return [GroupedFrame]
|
|
47
118
|
# @raise [ArgumentError] when the column is not a CATime, or +label:+ is
|
|
48
|
-
# neither :left nor :right.
|
|
49
|
-
def resample(name, unit, origin: nil, label: :left, fill: false)
|
|
50
|
-
name = name
|
|
51
|
-
time =
|
|
119
|
+
# neither :left nor :right, or +by:+ names the time column.
|
|
120
|
+
def resample(name, unit, origin: nil, label: :left, fill: false, by: nil)
|
|
121
|
+
name = column_key(name, "resample")
|
|
122
|
+
time = key_column(name, "resample")
|
|
52
123
|
unless time.is_a?(CATime)
|
|
53
124
|
raise ArgumentError, "resample: #{name.inspect} is not a time column (got #{time.class})"
|
|
54
125
|
end
|
|
@@ -59,18 +130,62 @@ class CAFrame
|
|
|
59
130
|
else
|
|
60
131
|
raise ArgumentError, "resample: label: must be :left or :right (got #{label.inspect})"
|
|
61
132
|
end
|
|
133
|
+
# key-labels: the bins are the index, given the column's attributes below.
|
|
62
134
|
grid = fill && bins.count_not_masked > 0 ? resample_grid(bins, unit) : bins.unique(sort: true)
|
|
135
|
+
return resample_by(name, time, bins, grid, by, fill) unless by.nil?
|
|
63
136
|
cat = CACategorical.from_codes(bins.locate_addr(grid), grid.to_a)
|
|
64
|
-
GroupedFrame.new(self, cat, name, @columns.key?(name) ? [name] : [],
|
|
137
|
+
GroupedFrame.new(self, cat, name, @columns.key?(name) ? [name] : [],
|
|
138
|
+
index: CAFrame.__send__(:key_labels, time, grid))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# resample with by:. A group is a (by values, bin) pair, coded as
|
|
142
|
+
# group * bins + bin, so the codes run through the groups in the order their
|
|
143
|
+
# by values first appear and through each group's bins in time order.
|
|
144
|
+
private def resample_by(name, time, bins, grid, by, fill)
|
|
145
|
+
by_names = column_keys(Array(by), "resample")
|
|
146
|
+
raise ArgumentError, "resample: by: names no column" if by_names.empty?
|
|
147
|
+
if by_names.include?(name)
|
|
148
|
+
raise ArgumentError, "resample: by: names the time column #{name.inspect}"
|
|
149
|
+
end
|
|
150
|
+
by_cols = by_names.map { |n| key_column(n, "resample") }
|
|
151
|
+
gcat = CACategorical.send(:from_keys, by_cols)
|
|
152
|
+
ng = gcat.labels.size
|
|
153
|
+
nb = grid.elements
|
|
154
|
+
comb = gcat.codes.int64 * nb + bins.locate_addr(grid).int64
|
|
155
|
+
# key-labels: the used (group, bin) codes, not labels; those go through key_labels below
|
|
156
|
+
occupied = fill ? CArray.int64(ng * nb).seq! : comb.unique(sort: true)
|
|
157
|
+
codes = fill ? comb : comb.locate_addr(occupied)
|
|
158
|
+
g_of = nb > 0 ? occupied / nb : occupied
|
|
159
|
+
b_of = nb > 0 ? occupied % nb : occupied
|
|
160
|
+
# Each group's by values, from its first row.
|
|
161
|
+
order = gcat.group_addr
|
|
162
|
+
first = order[CArray.segment_offsets(lengths: gcat.category_sizes)[0...ng]]
|
|
163
|
+
columns = {}
|
|
164
|
+
by_names.zip(by_cols).each do |n, col|
|
|
165
|
+
columns[n] = CAFrame.__send__(:key_labels, col, col.project(first))[g_of].copy
|
|
166
|
+
end
|
|
167
|
+
columns[name] = CAFrame.__send__(:key_labels, time, grid[b_of].copy)
|
|
168
|
+
glabels = gcat.labels
|
|
169
|
+
blabels = grid.to_a
|
|
170
|
+
labels = occupied.to_a.map do |c|
|
|
171
|
+
g, b = c.divmod(nb)
|
|
172
|
+
parts = by_names.size == 1 ? [glabels[g]] : glabels[g]
|
|
173
|
+
parts + [blabels[b]]
|
|
174
|
+
end
|
|
175
|
+
cat = CACategorical.from_codes(codes, labels)
|
|
176
|
+
key_names = by_names + (@columns.key?(name) ? [name] : [])
|
|
177
|
+
GroupedFrame.new(self, cat, "group", key_names, label_columns: columns)
|
|
65
178
|
end
|
|
66
179
|
|
|
67
|
-
# Every bin from the first occupied one to the last
|
|
68
|
-
#
|
|
69
|
-
# cannot be stepped on a finer grid
|
|
70
|
-
# bins
|
|
180
|
+
# Every bin from the first occupied one to the last, stepped from the
|
|
181
|
+
# first bin so an origin's phase (or label: :right) is kept. A month or a
|
|
182
|
+
# year cannot be stepped on a finer grid, so it is stepped on the one-month
|
|
183
|
+
# or one-year grid and then given the bins' unit, as the labels without
|
|
184
|
+
# fill: are.
|
|
71
185
|
private def resample_grid(bins, unit)
|
|
72
|
-
|
|
73
|
-
|
|
186
|
+
base = CATime::Resolution.parse(unit).base
|
|
187
|
+
if %i[Y M].include?(base)
|
|
188
|
+
CArray.time_range(bins.min, bins.max, unit: base, step: unit).to_unit(bins.unit)
|
|
74
189
|
else
|
|
75
190
|
CArray.time_range(bins.min, bins.max, unit: bins.unit, step: unit)
|
|
76
191
|
end
|
|
@@ -80,50 +195,32 @@ class CAFrame
|
|
|
80
195
|
# and elsewhere; already provided by the core (attr_reader :nrow).
|
|
81
196
|
|
|
82
197
|
private def grouping_categorical(keys)
|
|
83
|
-
|
|
84
|
-
if cols.size == 1
|
|
85
|
-
cols.first.categorize
|
|
86
|
-
else
|
|
87
|
-
composite_categorical(cols)
|
|
88
|
-
end
|
|
198
|
+
CACategorical.send(:from_keys, keys.map { |k| key_column(k) })
|
|
89
199
|
end
|
|
90
200
|
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
cats = cols.map(&:categorize)
|
|
98
|
-
sizes = cats.map { |c| c.labels.size }
|
|
99
|
-
tuple = if sizes.inject(1, :*) <= 2**62
|
|
100
|
-
cats.drop(1).inject(cats[0].codes.int64) { |acc, c| acc * c.labels.size + c.codes.int64 }
|
|
101
|
-
else
|
|
102
|
-
codes = cats.map(&:codes)
|
|
103
|
-
t = CArray.object(nrow) { |i| codes.map { |c| c[i] } }
|
|
104
|
-
nrow.times { |i| t[i] = UNDEF if t[i].any? { |x| UNDEF.equal?(x) } }
|
|
105
|
-
t
|
|
106
|
-
end
|
|
107
|
-
rows = tuple.categorize
|
|
108
|
-
labels = rows.labels.map do |code|
|
|
109
|
-
parts = code.is_a?(Array) ? code : sizes.reverse.map { |n| code, d = code.divmod(n); d }.reverse
|
|
110
|
-
parts.each_with_index.map { |d, j| cats[j].labels[d] }
|
|
111
|
-
end
|
|
112
|
-
CACategorical.from_codes(rows.codes, labels)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# The column a group key names, checked: +verb+ is the method the caller
|
|
116
|
-
# called, for the messages.
|
|
201
|
+
# The column a key names, checked -- the one reading of a key every verb
|
|
202
|
+
# that takes one goes through (group_by, is_duplicate, sort_by_key, join,
|
|
203
|
+
# join_asof, align, pivot, resample, windows): a column of that name, or
|
|
204
|
+
# the index by the row axis name; or a CArray of one value per row. It
|
|
205
|
+
# gives each row one value. +verb+ is the method the caller called, for
|
|
206
|
+
# the messages.
|
|
117
207
|
private def key_column(key, verb = "group_by")
|
|
118
208
|
col = case key
|
|
119
209
|
when String
|
|
120
|
-
|
|
210
|
+
# The index names the rows, so it is a key as a column is, by the
|
|
211
|
+
# row axis name, as resample and pivot take it.
|
|
212
|
+
@columns.fetch(key) do
|
|
213
|
+
next @index if @index && @axis_name == key
|
|
214
|
+
raise KeyError, "#{verb}: no column #{key.inspect}"
|
|
215
|
+
end
|
|
121
216
|
when CArray
|
|
122
217
|
unless key.shape[0] == nrow
|
|
123
218
|
raise ArgumentError,
|
|
124
219
|
"#{verb}: the key has #{key.shape[0]} values for #{nrow} rows"
|
|
125
220
|
end
|
|
126
221
|
key
|
|
222
|
+
when Symbol
|
|
223
|
+
column_key(key, verb)
|
|
127
224
|
else
|
|
128
225
|
raise ArgumentError, "#{verb}: a group key is a column name or a CArray (got #{key.class})"
|
|
129
226
|
end
|
|
@@ -133,7 +230,7 @@ class CAFrame
|
|
|
133
230
|
name = key.is_a?(String) ? "#{key.inspect} " : ""
|
|
134
231
|
raise ArgumentError,
|
|
135
232
|
"#{verb}: the key #{name}has shape #{col.shape.inspect}, not one value per row; " \
|
|
136
|
-
"
|
|
233
|
+
"use one of its components (e.g. df[name][nil, 0])"
|
|
137
234
|
end
|
|
138
235
|
col
|
|
139
236
|
end
|
|
@@ -142,11 +239,13 @@ end
|
|
|
142
239
|
# GroupedFrame — the result of +CAFrame#group_by+ (memo §11.6, §16).
|
|
143
240
|
#
|
|
144
241
|
# Holds the grouping categorical once (grouping is per-key, computed once
|
|
145
|
-
# and shared across every column).
|
|
146
|
-
# grp["col"]
|
|
147
|
-
# aggregate
|
|
148
|
-
#
|
|
242
|
+
# and shared across every column). The surfaces:
|
|
243
|
+
# grp["col"], grp[array] -> the CArray group iterator of a column or array
|
|
244
|
+
# aggregate(spec) -> declarative per-column reductions into a new frame
|
|
245
|
+
# aggregate { |grp| } -> columns built from iterators, run once
|
|
246
|
+
# table { |g| } -> Ruby escape, g is a per-group view-frame
|
|
149
247
|
class GroupedFrame
|
|
248
|
+
include CAFrame::ColumnVerbs
|
|
150
249
|
# +key_names+ are the frame columns the grouping was keyed on. They become
|
|
151
250
|
# the result's index, so the reduction shortcuts must not also return them as
|
|
152
251
|
# reduced columns; an external CArray key contributes no name.
|
|
@@ -154,17 +253,27 @@ class GroupedFrame
|
|
|
154
253
|
# The result's index is +index:+ when given (one entry per group, in code
|
|
155
254
|
# order); otherwise, for a single +key:+, the key's value at each group's
|
|
156
255
|
# first row, so it keeps the key's data type and Face; otherwise an object
|
|
157
|
-
# array of the labels
|
|
256
|
+
# array of the labels.
|
|
257
|
+
#
|
|
258
|
+
# With several keys the result has no index: +key_columns:+ (name => the
|
|
259
|
+
# key, one value per row) become its first columns, each holding the key's
|
|
260
|
+
# value at each group's first row; +label_columns:+ (name => one value per
|
|
261
|
+
# group, in code order) give them directly, for groups that may have no
|
|
262
|
+
# rows (resample with fill: true).
|
|
158
263
|
#
|
|
159
264
|
# The grouping and the index are taken here, so a later change to the key
|
|
160
265
|
# does not rename the groups; the values are read when a reduction is.
|
|
161
|
-
def initialize(frame, cat, axis_name, key_names = [], index: nil, key: nil
|
|
266
|
+
def initialize(frame, cat, axis_name, key_names = [], index: nil, key: nil,
|
|
267
|
+
key_columns: nil, label_columns: nil)
|
|
162
268
|
@frame = frame
|
|
163
269
|
@cat = cat
|
|
164
270
|
@axis_name = axis_name
|
|
165
271
|
@key_names = key_names
|
|
166
272
|
@labels = cat.labels # group values, in code order
|
|
167
|
-
@index = index ||
|
|
273
|
+
@index = index ||
|
|
274
|
+
(key && CAFrame.__send__(:key_labels, key, key.project(first_rows)))
|
|
275
|
+
@label_columns = label_columns ||
|
|
276
|
+
key_columns&.transform_values { |col| CAFrame.__send__(:key_labels, col, col.project(first_rows)) }
|
|
168
277
|
end
|
|
169
278
|
|
|
170
279
|
# Number of groups.
|
|
@@ -177,9 +286,40 @@ class GroupedFrame
|
|
|
177
286
|
@labels.dup
|
|
178
287
|
end
|
|
179
288
|
|
|
180
|
-
#
|
|
181
|
-
|
|
182
|
-
|
|
289
|
+
# The CArray group iterator of a column, or of any CArray with one entry
|
|
290
|
+
# per row of the frame: a value computed from columns, or a column with
|
|
291
|
+
# some cells masked to leave them out. Its reductions answer one value
|
|
292
|
+
# per group, and take the arguments a CArray's reductions take.
|
|
293
|
+
#
|
|
294
|
+
# grp["temp"].percentile(90)
|
|
295
|
+
# grp[df["temp"] * df["w"]].sum / grp["w"].sum
|
|
296
|
+
# grp[df["temp"].mask_where(df["qc"].ne(0))].mean # without the flagged rows
|
|
297
|
+
#
|
|
298
|
+
# The array is matched to the rows by position, as a CArray key of
|
|
299
|
+
# +group_by+ is. An N-D one is grouped along its first axis; pass
|
|
300
|
+
# +axis: 0+ to its reductions to keep the trailing shape.
|
|
301
|
+
#
|
|
302
|
+
# @param source [String, CArray] a column name, or a CArray whose first
|
|
303
|
+
# axis has one entry per row.
|
|
304
|
+
# @return [CACategoricalIterator]
|
|
305
|
+
# @raise [KeyError] for a name that is no column.
|
|
306
|
+
# @raise [ArgumentError] when the CArray's first axis is not the rows.
|
|
307
|
+
def [](source)
|
|
308
|
+
refuse_changed_rows("[]")
|
|
309
|
+
values =
|
|
310
|
+
case source
|
|
311
|
+
when CArray
|
|
312
|
+
rows = source.ndim == 0 ? 0 : source.shape[0]
|
|
313
|
+
unless source.ndim >= 1 && rows == @cat.elements
|
|
314
|
+
raise ArgumentError,
|
|
315
|
+
"[]: the array has #{source.ndim == 0 ? "no axis" : "#{rows} rows"} " \
|
|
316
|
+
"for the frame's #{@cat.elements}"
|
|
317
|
+
end
|
|
318
|
+
source
|
|
319
|
+
else
|
|
320
|
+
@frame[@frame.__send__(:column_key, source, "[]")]
|
|
321
|
+
end
|
|
322
|
+
values.group_by_category(@cat)
|
|
183
323
|
end
|
|
184
324
|
|
|
185
325
|
# Declarative aggregation (memo §11.6). Spec maps an output column name to
|
|
@@ -188,10 +328,46 @@ class GroupedFrame
|
|
|
188
328
|
# with the group's column slice). A Symbol reduction takes its arguments
|
|
189
329
|
# after it, as in a call: +["temp", :percentile, 90.0]+, and its keywords
|
|
190
330
|
# last: +["temp", :mean, min_count: 6]+.
|
|
191
|
-
|
|
331
|
+
#
|
|
332
|
+
# With +keep_rows: true+ the rows are not collapsed: each row of the frame
|
|
333
|
+
# gets the result of its own group, so the result has the frame's rows and
|
|
334
|
+
# index, and a row whose key is masked (in no group) is UNDEF. The same
|
|
335
|
+
# keyword works for +table+ and the reductions below.
|
|
336
|
+
#
|
|
337
|
+
# df.group_by("station").aggregate("temp_mean" => ["temp", :mean], keep_rows: true)
|
|
338
|
+
#
|
|
339
|
+
# Given a block instead of a spec, aggregate yields the grouped frame once
|
|
340
|
+
# and the block returns a Hash of output name => a CArray with one entry
|
|
341
|
+
# per group. The block computes the columns with the grouped frame's
|
|
342
|
+
# reductions, +grp[...]+ iterators and CArray arithmetic, so nothing runs
|
|
343
|
+
# per group in Ruby:
|
|
344
|
+
#
|
|
345
|
+
# df.group_by("station").aggregate do |g|
|
|
346
|
+
# {
|
|
347
|
+
# "temp" => g["temp"].mean,
|
|
348
|
+
# "range" => g["temp"].max - g["temp"].min,
|
|
349
|
+
# "wmean" => g[df["temp"] * df["w"]].sum / g["w"].sum,
|
|
350
|
+
# "n" => g["temp"].group_sizes,
|
|
351
|
+
# }
|
|
352
|
+
# end
|
|
353
|
+
#
|
|
354
|
+
# +keep_rows: true+ lays each column over the rows as for a spec.
|
|
355
|
+
#
|
|
356
|
+
# @raise [ArgumentError] when both a spec and a block are given, or the
|
|
357
|
+
# block returns something other than a Hash of per-group CArrays.
|
|
358
|
+
def aggregate(spec = nil, keep_rows: false, **named, &block)
|
|
359
|
+
refuse_changed_rows("aggregate")
|
|
360
|
+
if block
|
|
361
|
+
unless spec.nil? && named.empty?
|
|
362
|
+
raise ArgumentError, "aggregate: give a spec or a block, not both"
|
|
363
|
+
end
|
|
364
|
+
return result_frame(block_columns(yield(self)), keep_rows)
|
|
365
|
+
end
|
|
366
|
+
spec = (spec || {}).merge(named)
|
|
192
367
|
cols = {}
|
|
193
368
|
spec.each do |out_name, (in_name, reduction, *args)|
|
|
194
|
-
out = out_name
|
|
369
|
+
out = @frame.__send__(:column_name_arg, out_name, "aggregate")
|
|
370
|
+
@frame.__send__(:column_key, in_name, "aggregate")
|
|
195
371
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
196
372
|
cols[out] =
|
|
197
373
|
case reduction
|
|
@@ -207,29 +383,34 @@ class GroupedFrame
|
|
|
207
383
|
"reduction must be a Symbol or Proc (got #{reduction.class})"
|
|
208
384
|
end
|
|
209
385
|
end
|
|
210
|
-
|
|
386
|
+
result_frame(cols, keep_rows)
|
|
211
387
|
end
|
|
212
388
|
|
|
213
389
|
# Cross-column Ruby escape (memo §11.6). The block receives a per-group
|
|
214
390
|
# view-frame and returns a Hash of output-name => value; the values are
|
|
215
|
-
# collected column-wise across groups into a new frame.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
391
|
+
# collected column-wise across groups into a new frame. +keep_rows: true+
|
|
392
|
+
# gives each row its group's values, as for +aggregate+.
|
|
393
|
+
#
|
|
394
|
+
# The columns are the names any group's Hash has, in the order they
|
|
395
|
+
# first appear; a group whose Hash lacks a name is UNDEF there, as
|
|
396
|
+
# +from_records+ reads a record that lacks a key.
|
|
397
|
+
def table(keep_rows: false)
|
|
398
|
+
refuse_changed_rows("table")
|
|
399
|
+
outs = []
|
|
219
400
|
each_group_frame do |g|
|
|
220
401
|
out = yield(g)
|
|
221
402
|
unless out.is_a?(Hash)
|
|
222
403
|
raise ArgumentError, "table block must return a Hash (got #{out.class})"
|
|
223
404
|
end
|
|
224
|
-
|
|
225
|
-
|
|
405
|
+
out.each_key { |k| @frame.__send__(:column_name_arg, k, "table") }
|
|
406
|
+
outs << out
|
|
226
407
|
end
|
|
408
|
+
names = CAFrame.__send__(:record_key_union, outs)
|
|
227
409
|
cols = {}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
cols[name] = CArray.object(vals.size) { |i| vals[i] }
|
|
410
|
+
names.each do |name|
|
|
411
|
+
cols[name] = column_of_values(outs.map { |out| out.fetch(name, nil) })
|
|
231
412
|
end
|
|
232
|
-
|
|
413
|
+
result_frame(cols, keep_rows)
|
|
233
414
|
end
|
|
234
415
|
|
|
235
416
|
# Convenience reductions over every column that defines the reduction. A
|
|
@@ -240,28 +421,249 @@ class GroupedFrame
|
|
|
240
421
|
# its trailing shape: a (N, L) column gives (groups, L). Each
|
|
241
422
|
# takes +min_count:+ and +fill_value:+ as a core reduction does: a group
|
|
242
423
|
# with fewer than min_count present values is UNDEF, and fill_value fills
|
|
243
|
-
# the UNDEF cells.
|
|
424
|
+
# the UNDEF cells. One fill_value goes to every column and has to suit each
|
|
425
|
+
# (a number into a string column raises); a Hash of column name => value
|
|
426
|
+
# fills those columns only. +keep_rows: true+ gives each row its group's value
|
|
427
|
+
# instead of one row per group, as for +aggregate+.
|
|
244
428
|
#
|
|
245
|
-
#
|
|
429
|
+
# Given column names, they reduce those columns only and return them as
|
|
430
|
+
# +df[...]+ does: one name gives the CArray, several an Array of them.
|
|
431
|
+
#
|
|
432
|
+
# grp.mean # a frame, one row per group
|
|
433
|
+
# grp.mean("temp") # CArray, one value per group
|
|
434
|
+
# df["temp"] - grp.mean("temp", keep_rows: true) # anomaly from the group mean
|
|
435
|
+
#
|
|
436
|
+
# @!method sum(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
246
437
|
# Returns a frame of the per-group sum of every column whose type has a
|
|
247
438
|
# sum, an N-D one per group along the rows. Other columns are left out.
|
|
248
|
-
# @return [CAFrame
|
|
249
|
-
#
|
|
439
|
+
# @return [CAFrame, CArray, Array<CArray>] a frame with one row per
|
|
440
|
+
# group, indexed by the group labels, or with keep_rows: true the
|
|
441
|
+
# frame's rows and index; given names, the CArray (one) or the Array of
|
|
442
|
+
# them (several).
|
|
443
|
+
# @!method mean(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
250
444
|
# Returns a frame of the per-group mean of every column whose type has
|
|
251
445
|
# a mean, as {#sum} does.
|
|
252
446
|
# @return [CAFrame] one row per group.
|
|
253
|
-
# @!method min(min_count: nil, fill_value: nil)
|
|
447
|
+
# @!method min(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
254
448
|
# Returns a frame of the per-group minimum of every column whose type has
|
|
255
449
|
# a minimum, as {#sum} does.
|
|
256
450
|
# @return [CAFrame] one row per group.
|
|
257
|
-
# @!method max(min_count: nil, fill_value: nil)
|
|
451
|
+
# @!method max(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
258
452
|
# Returns a frame of the per-group maximum of every column whose type has
|
|
259
453
|
# a maximum, as {#sum} does.
|
|
260
454
|
# @return [CAFrame] one row per group.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
455
|
+
# @!method prod(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
456
|
+
# Returns the per-group product, as {#sum} does.
|
|
457
|
+
# @return [CAFrame] one row per group.
|
|
458
|
+
# @!method accumulate(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
459
|
+
# Returns the per-group sum kept in each column's own data type, as
|
|
460
|
+
# +CArray#accumulate+, as {#sum} does.
|
|
461
|
+
# @return [CAFrame] one row per group.
|
|
462
|
+
# @!method median(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
463
|
+
# Returns the per-group median, as {#sum} does.
|
|
464
|
+
# @return [CAFrame] one row per group.
|
|
465
|
+
# @!method variance(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
466
|
+
# Returns the per-group sample variance (divisor n - 1), as {#sum} does.
|
|
467
|
+
# @return [CAFrame] one row per group.
|
|
468
|
+
# @!method stddev(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
469
|
+
# Returns the per-group sample standard deviation, as {#sum} does.
|
|
470
|
+
# @return [CAFrame] one row per group.
|
|
471
|
+
# @!method variancep(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
472
|
+
# Returns the per-group population variance (divisor n), as {#sum} does.
|
|
473
|
+
# @return [CAFrame] one row per group.
|
|
474
|
+
# @!method stddevp(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
475
|
+
# Returns the per-group population standard deviation, as {#sum} does.
|
|
476
|
+
# @return [CAFrame] one row per group.
|
|
477
|
+
CArray.send(:define_named_methods, self,
|
|
478
|
+
[:sum, :mean, :min, :max, :prod, :accumulate, :median,
|
|
479
|
+
:variance, :stddev, :variancep, :stddevp], :group_reduction)
|
|
480
|
+
|
|
481
|
+
private def group_reduction (red, *names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
482
|
+
refuse_changed_rows(red)
|
|
483
|
+
reduce_columns(red, names, keep_rows, min_count: min_count, fill_value: fill_value)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# The same convenience for the reductions that take no min_count: or
|
|
487
|
+
# fill_value:, as the core ones take none: a value of the group (first,
|
|
488
|
+
# last), a count (nunique, count) or a boolean fold (all, any).
|
|
489
|
+
#
|
|
490
|
+
# @!method first(*names, keep_rows: false)
|
|
491
|
+
# Returns each group's first present value in row order, UNDEF for a
|
|
492
|
+
# group with none, as {#sum} returns its sums.
|
|
493
|
+
# @return [CAFrame] one row per group.
|
|
494
|
+
# @!method last(*names, keep_rows: false)
|
|
495
|
+
# Returns each group's last present value in row order.
|
|
496
|
+
# @return [CAFrame] one row per group.
|
|
497
|
+
# @!method nunique(*names, keep_rows: false)
|
|
498
|
+
# Returns each group's number of distinct present values (int64).
|
|
499
|
+
# @return [CAFrame] one row per group.
|
|
500
|
+
# @!method count(*names, keep_rows: false)
|
|
501
|
+
# Returns each group's number of present values (int64), a masked cell
|
|
502
|
+
# left out.
|
|
503
|
+
# @return [CAFrame] one row per group.
|
|
504
|
+
# @!method all(*names, keep_rows: false)
|
|
505
|
+
# Returns whether every present value of the group is true, for the
|
|
506
|
+
# boolean columns.
|
|
507
|
+
# @return [CAFrame] one row per group.
|
|
508
|
+
# @!method any(*names, keep_rows: false)
|
|
509
|
+
# Returns whether any present value of the group is true, for the
|
|
510
|
+
# boolean columns.
|
|
511
|
+
# @return [CAFrame] one row per group.
|
|
512
|
+
CArray.send(:define_named_methods, self,
|
|
513
|
+
[:first, :last, :nunique, :count, :all, :any], :group_value)
|
|
514
|
+
|
|
515
|
+
private def group_value (red, *names, keep_rows: false)
|
|
516
|
+
refuse_changed_rows(red)
|
|
517
|
+
reduce_columns(red, names, keep_rows)
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Values computed within each group and kept on the frame's rows, in row
|
|
521
|
+
# order: the value some rows back in the same group, a rank among the
|
|
522
|
+
# group's rows, a running statistic. A row in no group is UNDEF. Without
|
|
523
|
+
# names they give a frame of every column whose type has the operation
|
|
524
|
+
# (the key columns left out), on the frame's rows and index; given names,
|
|
525
|
+
# the CArray (one) or the Array of them (several), as +df[...]+ returns
|
|
526
|
+
# columns. An N-D column runs along the rows.
|
|
527
|
+
#
|
|
528
|
+
# df["dtemp"] = df["temp"] - grp.lag("temp") # change since the station's previous row
|
|
529
|
+
# grp.order("temp", descending: true) # rank within the station
|
|
530
|
+
# grp.cumsum("rain") # running total per station
|
|
531
|
+
#
|
|
532
|
+
# "Previous" is the previous row of the frame: sort the frame first
|
|
533
|
+
# (+sort_by_key("time")+) when its rows are not in time order.
|
|
534
|
+
#
|
|
535
|
+
# @!method lag(*names, n: 1)
|
|
536
|
+
# Returns each row's value +n+ rows back in its own group, UNDEF where
|
|
537
|
+
# the group has none, as +CArray#lag+; a negative +n+ looks ahead.
|
|
538
|
+
# @return [CAFrame, CArray, Array<CArray>]
|
|
539
|
+
# @!method order(*names, descending: false, method: :ordinal, kind: :quick)
|
|
540
|
+
# Returns each row's rank among its group's rows (int64, 0 the
|
|
541
|
+
# smallest), as +CArray#order+ ranks them.
|
|
542
|
+
# @return [CAFrame, CArray, Array<CArray>]
|
|
543
|
+
# @!method cumsum(*names)
|
|
544
|
+
# Returns the running sum within each group, as +CArray#cumsum+.
|
|
545
|
+
# @return [CAFrame, CArray, Array<CArray>]
|
|
546
|
+
# @!method cumprod(*names)
|
|
547
|
+
# Returns the running product within each group.
|
|
548
|
+
# @return [CAFrame, CArray, Array<CArray>]
|
|
549
|
+
# @!method cummax(*names)
|
|
550
|
+
# Returns the running maximum within each group.
|
|
551
|
+
# @return [CAFrame, CArray, Array<CArray>]
|
|
552
|
+
# @!method cummin(*names)
|
|
553
|
+
# Returns the running minimum within each group.
|
|
554
|
+
# @return [CAFrame, CArray, Array<CArray>]
|
|
555
|
+
# @!method cumcount(*names)
|
|
556
|
+
# Returns the running count of present values within each group (int64).
|
|
557
|
+
# @return [CAFrame, CArray, Array<CArray>]
|
|
558
|
+
def lag (*names, n: 1)
|
|
559
|
+
transform_columns(:lag, names, [n], {})
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def order (*names, descending: false, method: :ordinal, kind: :quick)
|
|
563
|
+
transform_columns(:order, names, [], { descending: descending, method: method, kind: kind })
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
CArray.send(:define_named_methods, self,
|
|
567
|
+
[:cumsum, :cumprod, :cummax, :cummin, :cumcount], :group_scan)
|
|
568
|
+
|
|
569
|
+
private def group_scan (op, *names)
|
|
570
|
+
transform_columns(op, names, [], {})
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# Fill the masked cells of a column from the rows of the same group, in
|
|
574
|
+
# place, as +CAFrame#fill+ does for the whole column: +:ffill+ carries the
|
|
575
|
+
# group's last present value forward in row order, +:bfill+ its next one
|
|
576
|
+
# back. A fill never reaches across into another group, so a cell with
|
|
577
|
+
# nothing to carry in its group stays masked, and a row whose key is
|
|
578
|
+
# masked (in no group) is left as it is. A constant fills every masked
|
|
579
|
+
# cell, as +CAFrame#fill+ does. Returns self.
|
|
580
|
+
#
|
|
581
|
+
# df.group_by("station").fill("temp", :ffill)
|
|
582
|
+
#
|
|
583
|
+
# Sort the frame first (+sort_by_key("time")+) when its rows are not in
|
|
584
|
+
# time order.
|
|
585
|
+
#
|
|
586
|
+
# @param name [String] the column.
|
|
587
|
+
# @param method_or_value [Symbol, Object] +:ffill+, +:bfill+, or a constant.
|
|
588
|
+
# @return [GroupedFrame] self
|
|
589
|
+
# @raise [ArgumentError] for +:linear+, which within groups is not
|
|
590
|
+
# provided, or another Symbol.
|
|
591
|
+
def fill (name, method_or_value)
|
|
592
|
+
refuse_changed_rows("fill")
|
|
593
|
+
case method_or_value
|
|
594
|
+
when :ffill, :bfill
|
|
595
|
+
@frame.__send__(:refuse_if_frozen)
|
|
596
|
+
key = @frame.__send__(:column_key, name, "fill")
|
|
597
|
+
col = @frame[key]
|
|
598
|
+
# A row in no group is UNDEF in the fill, so writing it changes nothing.
|
|
599
|
+
held = group_transform(col, :strip_mask, [], { method: method_or_value })
|
|
600
|
+
target = col.is_masked
|
|
601
|
+
col[target] = held[target] if target.any
|
|
602
|
+
when :linear
|
|
603
|
+
raise ArgumentError,
|
|
604
|
+
"fill(#{name.inspect}, :linear): an interpolation within each group " \
|
|
605
|
+
"is not provided; use :ffill or :bfill, or interpolate one group at a " \
|
|
606
|
+
"time with CAFrame#fill"
|
|
607
|
+
when Symbol
|
|
608
|
+
raise ArgumentError,
|
|
609
|
+
"fill: unknown method #{method_or_value.inspect} " \
|
|
610
|
+
"(:ffill | :bfill, or a constant fill value)"
|
|
611
|
+
else
|
|
612
|
+
@frame.fill(name, method_or_value)
|
|
613
|
+
end
|
|
614
|
+
self
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
# Keep the rows of the groups the block chooses. The block receives the
|
|
618
|
+
# grouped frame and returns a boolean CArray with one value per group,
|
|
619
|
+
# built from the group reductions, as +CAFrame#filter+'s block returns one
|
|
620
|
+
# per row:
|
|
621
|
+
#
|
|
622
|
+
# grp.filter { |g| g["temp"].count_not_masked >= 20 } # stations with 20 readings
|
|
623
|
+
# grp.filter { |g| g.mean("temp") > 15.0 }
|
|
624
|
+
#
|
|
625
|
+
# The rows of a chosen group are kept in the frame's order, as a sub-frame
|
|
626
|
+
# sharing the frame's columns. A group whose value is UNDEF is
|
|
627
|
+
# undetermined, as is a row whose key is masked (it belongs to no group):
|
|
628
|
+
# such rows are dropped, or with +keep_masked: true+ carried with their
|
|
629
|
+
# cells masked, as +CAFrame#filter+ does.
|
|
630
|
+
#
|
|
631
|
+
# @param keep_masked [Boolean] carry the undetermined rows forward masked.
|
|
632
|
+
# @yieldparam grouped [GroupedFrame] self.
|
|
633
|
+
# @yieldreturn [CArray] boolean, one value per group.
|
|
634
|
+
# @return [CAFrame]
|
|
635
|
+
# @raise [ArgumentError] when the block does not return a boolean CArray of
|
|
636
|
+
# shape [ngroup].
|
|
637
|
+
def filter (keep_masked: false)
|
|
638
|
+
refuse_changed_rows("filter")
|
|
639
|
+
chosen = yield(self)
|
|
640
|
+
unless chosen.is_a?(CArray) && chosen.data_type == :boolean
|
|
641
|
+
raise ArgumentError,
|
|
642
|
+
"filter block must return a boolean CArray (got #{chosen.is_a?(CArray) ? "a #{CArray::Inspector.type_label(chosen)} CArray" : chosen.class})"
|
|
643
|
+
end
|
|
644
|
+
unless chosen.shape == [ngroup]
|
|
645
|
+
raise ArgumentError,
|
|
646
|
+
"filter mask has shape #{chosen.shape.inspect}, " \
|
|
647
|
+
"expected [#{ngroup}] (one value per group)"
|
|
264
648
|
end
|
|
649
|
+
rows = spread_to_rows(chosen)
|
|
650
|
+
@frame.filter(keep_masked: keep_masked) { rows }
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# Returns the grouped rows as a frame: the frame the grouping was made
|
|
654
|
+
# from, its rows, columns and index, as a new frame sharing its columns.
|
|
655
|
+
# Nothing is copied, so a write to a cell reaches the grouped frame's
|
|
656
|
+
# rows; adding or dropping a column changes the new frame only.
|
|
657
|
+
#
|
|
658
|
+
# def warm_stations (grp)
|
|
659
|
+
# grp.filter { |g| g.mean("temp") > 15.0 } # the rows of some groups
|
|
660
|
+
# end
|
|
661
|
+
# grp.to_frame # all of its rows
|
|
662
|
+
#
|
|
663
|
+
# @return [CAFrame]
|
|
664
|
+
def to_frame
|
|
665
|
+
refuse_changed_rows("to_frame")
|
|
666
|
+
@frame.__send__(:rebuild, @frame.column_names.to_h { |name| [name, @frame[name]] })
|
|
265
667
|
end
|
|
266
668
|
|
|
267
669
|
# @return [String]
|
|
@@ -269,22 +671,81 @@ class GroupedFrame
|
|
|
269
671
|
"#<GroupedFrame ngroup=#{ngroup} by=#{@axis_name.inspect}>"
|
|
270
672
|
end
|
|
271
673
|
|
|
674
|
+
# A within-group transform of the named columns, or of every column whose
|
|
675
|
+
# type has it. The arguments are checked by the core first: below, a
|
|
676
|
+
# refusal reads as "this type has no such operation", and a wrong keyword
|
|
677
|
+
# would otherwise leave every column out without a word.
|
|
678
|
+
private def transform_columns(op, names, args, keywords)
|
|
679
|
+
refuse_changed_rows(op)
|
|
680
|
+
CArray.float64(1).public_send(op, *args, **keywords)
|
|
681
|
+
out = over_named_columns(op, names, @key_names, args, keywords) do |col|
|
|
682
|
+
group_transform(col, op, args, keywords)
|
|
683
|
+
end
|
|
684
|
+
return out unless names.empty?
|
|
685
|
+
CAFrame.new(out, axis_name: @frame.axis_name, index: @frame.index)
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
# A column's within-group transform: an N-D column runs along the rows.
|
|
689
|
+
private def group_transform(col, op, args, keywords)
|
|
690
|
+
it = col.group_by_category(@cat)
|
|
691
|
+
keywords = keywords.merge(axis: 0) if col.ndim > 1
|
|
692
|
+
it.public_send(op, *args, **keywords)
|
|
693
|
+
end
|
|
694
|
+
|
|
272
695
|
# Each result gets its own index.
|
|
273
696
|
private def label_index
|
|
274
697
|
return @index.copy if @index
|
|
275
698
|
CArray.object(@labels.size) { |i| @labels[i] }
|
|
276
699
|
end
|
|
277
700
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
701
|
+
# A key column is the index here, not a result column. Named columns
|
|
702
|
+
# come back as df[...] returns them, each on the frame's rows with
|
|
703
|
+
# +keep_rows+.
|
|
704
|
+
private def reduce_columns(reduction, names, keep_rows, **keywords)
|
|
705
|
+
keywords = keywords.compact
|
|
706
|
+
fills = keywords.delete(:fill_value)
|
|
707
|
+
if fills.is_a?(Hash)
|
|
708
|
+
unknown = fills.keys - @frame.column_names
|
|
709
|
+
unless unknown.empty?
|
|
710
|
+
raise ArgumentError,
|
|
711
|
+
"#{reduction}: fill_value names no column #{unknown.map(&:inspect).join(', ')}"
|
|
712
|
+
end
|
|
286
713
|
end
|
|
287
|
-
|
|
714
|
+
out = over_named_columns(reduction, names, @key_names) do |col, name|
|
|
715
|
+
fill = fills.is_a?(Hash) ? fills[name] : fills
|
|
716
|
+
kw = fill.nil? ? keywords : keywords.merge(fill_value: fill)
|
|
717
|
+
col = group_reduce(col, reduction, **kw)
|
|
718
|
+
keep_rows && !names.empty? ? spread_to_rows(col) : col
|
|
719
|
+
end
|
|
720
|
+
return out unless names.empty?
|
|
721
|
+
result_frame(out, keep_rows)
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
# One row per group, indexed by the labels; or, with +keep_rows+, the
|
|
725
|
+
# frame's rows, each holding its group's result.
|
|
726
|
+
private def result_frame(cols, keep_rows)
|
|
727
|
+
unless keep_rows
|
|
728
|
+
return CAFrame.new(cols, axis_name: @axis_name, index: label_index) unless @label_columns
|
|
729
|
+
clash = cols.keys & @label_columns.keys
|
|
730
|
+
unless clash.empty?
|
|
731
|
+
raise ArgumentError,
|
|
732
|
+
"a result column has the name of a key column: #{clash.map(&:inspect).join(', ')}"
|
|
733
|
+
end
|
|
734
|
+
return CAFrame.new(@label_columns.transform_values(&:copy).merge(cols))
|
|
735
|
+
end
|
|
736
|
+
CAFrame.new(cols.transform_values { |col| spread_to_rows(col) },
|
|
737
|
+
axis_name: @frame.axis_name, index: @frame.index)
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
# A per-group result (one entry per group along axis 0) laid back over the
|
|
741
|
+
# frame's rows through the group codes. A row in no group is UNDEF.
|
|
742
|
+
private def spread_to_rows(result)
|
|
743
|
+
# A row in no group has a masked index cell, which x[i] answers as a
|
|
744
|
+
# hole: so even with no group at all, the result keeps its type.
|
|
745
|
+
codes = group_codes
|
|
746
|
+
index = codes.value.int64
|
|
747
|
+
index[codes.is_masked] = UNDEF
|
|
748
|
+
result[index, *[nil] * (result.ndim - 1)].copy
|
|
288
749
|
end
|
|
289
750
|
|
|
290
751
|
# A column's group reduction. An N-D column is reduced along the rows, so
|
|
@@ -298,22 +759,46 @@ class GroupedFrame
|
|
|
298
759
|
end
|
|
299
760
|
end
|
|
300
761
|
|
|
301
|
-
#
|
|
302
|
-
#
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
762
|
+
# The columns an aggregate block returned: a Hash of names to CArrays
|
|
763
|
+
# with one entry per group along their first axis.
|
|
764
|
+
private def block_columns(out)
|
|
765
|
+
unless out.is_a?(Hash)
|
|
766
|
+
raise ArgumentError, "aggregate block must return a Hash (got #{out.class})"
|
|
767
|
+
end
|
|
768
|
+
out.each_with_object({}) do |(name, col), cols|
|
|
769
|
+
name = @frame.__send__(:column_name_arg, name, "aggregate")
|
|
770
|
+
unless col.is_a?(CArray) && col.ndim >= 1 && col.shape[0] == ngroup
|
|
771
|
+
got = col.is_a?(CArray) ? "shape #{col.shape.inspect}" : col.class.to_s
|
|
772
|
+
raise ArgumentError,
|
|
773
|
+
"aggregate: #{name.inspect} must be a CArray with one value per group " \
|
|
774
|
+
"(#{ngroup}), got #{got}"
|
|
775
|
+
end
|
|
776
|
+
cols[name] = col
|
|
777
|
+
end
|
|
311
778
|
end
|
|
312
779
|
|
|
313
780
|
private def per_group_column(in_name, proc)
|
|
314
781
|
results = []
|
|
315
782
|
each_group_slice(in_name) { |slice| results << proc.call(slice) }
|
|
316
|
-
|
|
783
|
+
column_of_values(results)
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
# The grouping holds one code per row of the frame as it was when grouped.
|
|
787
|
+
# A frame whose row count changed since (a splice, a deletion) has rows
|
|
788
|
+
# the grouping does not know, so every verb refuses it.
|
|
789
|
+
private def refuse_changed_rows(verb)
|
|
790
|
+
return if @frame.nrow == @cat.elements
|
|
791
|
+
raise ArgumentError,
|
|
792
|
+
"#{verb}: the frame has #{@frame.nrow} rows, #{@cat.elements} when it " \
|
|
793
|
+
"was grouped; group it again"
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
# A column of the Ruby values the groups gave, typed as from_records types
|
|
797
|
+
# a record's values: Integers int64, numbers float64, Strings text, time
|
|
798
|
+
# elements a time column, arrays of one shape an N-D column, anything else
|
|
799
|
+
# object. UNDEF and nil are missing.
|
|
800
|
+
private def column_of_values(values)
|
|
801
|
+
CAFrame.__send__(:build_record_column, values, values.size)
|
|
317
802
|
end
|
|
318
803
|
|
|
319
804
|
private def each_group_slice(in_name)
|
|
@@ -327,6 +812,23 @@ class GroupedFrame
|
|
|
327
812
|
ngroup.times { |k| yield @frame[group_address(k)] }
|
|
328
813
|
end
|
|
329
814
|
|
|
815
|
+
# The first row of each group, in code order.
|
|
816
|
+
# With few groups each row's number is folded into its group's slot by the
|
|
817
|
+
# smallest, which needs no sort of the rows (a numeric column then reduces
|
|
818
|
+
# without one too); a row in no group (masked code) is in no slot, and a
|
|
819
|
+
# group without rows keeps no row (UNDEF). With more groups the columns
|
|
820
|
+
# are gathered through the sort anyway, so the first rows are read off it.
|
|
821
|
+
private def first_rows
|
|
822
|
+
if ngroup > CACategoricalIterator::SCATTER_CATEGORIES_MAX
|
|
823
|
+
return group_perm[group_bounds[0...ngroup]]
|
|
824
|
+
end
|
|
825
|
+
n = @cat.elements
|
|
826
|
+
first = CArray.int64(ngroup).fill(n)
|
|
827
|
+
first.scatter_min!(@cat.codes, CArray.int64(n).seq!)
|
|
828
|
+
first[first.eq(n)] = UNDEF if ngroup > 0
|
|
829
|
+
first
|
|
830
|
+
end
|
|
831
|
+
|
|
330
832
|
private def group_address(k)
|
|
331
833
|
group_perm[group_bounds[k]...group_bounds[k + 1]]
|
|
332
834
|
end
|
|
@@ -336,7 +838,7 @@ class GroupedFrame
|
|
|
336
838
|
end
|
|
337
839
|
|
|
338
840
|
private def group_perm
|
|
339
|
-
@group_perm ||= @cat.
|
|
841
|
+
@group_perm ||= @cat.group_addr
|
|
340
842
|
end
|
|
341
843
|
|
|
342
844
|
private def group_bounds
|