carray 3.0.5 → 3.0.6
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 +233 -0
- data/README.md +1 -1
- data/carray.gemspec +3 -2
- data/ext/ca_axis_dispatch.c +6 -0
- data/ext/ca_axis_group.c +1 -1
- data/ext/ca_categorical_iterator.c +7 -7
- data/ext/ca_kernel_iterator.c +116 -231
- data/ext/ca_kernel_iterator.h +410 -254
- data/ext/ca_obj_bincmp.c +9 -1
- data/ext/ca_obj_binop.c +10 -1
- data/ext/ca_obj_bitfield.c +2 -0
- data/ext/ca_obj_byte_swap.c +2 -0
- data/ext/ca_obj_face.c +2 -0
- data/ext/ca_obj_fake.c +3 -1
- data/ext/ca_obj_grid.c +167 -12
- data/ext/ca_obj_meld.c +69 -113
- data/ext/ca_obj_moncmp.c +9 -1
- data/ext/ca_obj_monop.c +40 -2
- data/ext/ca_obj_object.c +54 -20
- data/ext/ca_obj_reduce.c +15 -1
- data/ext/ca_obj_refer.c +65 -20
- data/ext/ca_obj_remap.c +69 -10
- data/ext/ca_obj_roll.c +1 -0
- data/ext/ca_obj_select.c +4 -1
- data/ext/ca_obj_select_axis.c +1 -0
- data/ext/ca_obj_shift.c +1 -0
- data/ext/ca_obj_stack.c +310 -111
- data/ext/ca_obj_stride.c +131 -45
- data/ext/ca_obj_string.c +18 -2
- data/ext/ca_obj_tile.c +1 -0
- data/ext/ca_obj_triop.c +9 -1
- data/ext/ca_obj_window.c +1 -0
- data/ext/ca_op_ipower.c +8 -8
- data/ext/caframe_csv_reader.c +1 -1
- data/ext/carray.h +2 -1
- data/ext/carray_access.c +27 -64
- data/ext/carray_address_basis.c +134 -0
- data/ext/carray_attribute.c +8 -0
- data/ext/carray_broadcast.c +3 -4
- data/ext/carray_build_flags.h +1 -1
- data/ext/carray_cast.c +8 -11
- data/ext/carray_conversion.c +9 -5
- data/ext/carray_core.c +811 -72
- data/ext/carray_element.c +13 -9
- data/ext/carray_histogram.c +25 -3
- data/ext/carray_internal.h +147 -12
- data/ext/carray_kernels_bincmp.c +2007 -3
- data/ext/carray_kernels_binop.c +25365 -5199
- data/ext/carray_kernels_init.c +8 -8
- data/ext/carray_kernels_monop.c +10696 -3678
- data/ext/carray_kernels_reduce_aggregate.c +4429 -702
- data/ext/carray_kernels_reduce_boolean.c +1533 -15
- data/ext/carray_kernels_reduce_cumulative.c +1808 -248
- data/ext/carray_kernels_reduce_extreme.c +4508 -511
- data/ext/carray_kernels_reduce_variance.c +2284 -152
- data/ext/carray_kernels_scan.c +240 -240
- data/ext/carray_kernels_search.c +4211 -264
- data/ext/carray_kernels_sort.c +367 -450
- data/ext/carray_lazy.c +47 -0
- data/ext/carray_loop.c +1 -1
- data/ext/carray_mask.c +130 -139
- data/ext/carray_median_percentile.c +4 -4
- data/ext/carray_memory_view.c +13 -7
- data/ext/carray_operator.c +147 -44
- data/ext/carray_order.c +3 -2
- data/ext/carray_random.c +4 -1
- data/ext/carray_slab.c +97 -21
- data/ext/carray_sort.c +1 -1
- data/ext/carray_test.c +74 -19
- data/ext/carray_time_text.c +2 -2
- data/ext/extconf.rb +11 -0
- data/ext/mkkernel.rb +346 -94
- data/ext/ruby_carray.c +4 -1
- data/ext/version.h +4 -4
- data/lib/carray/attribute.rb +25 -1
- data/lib/carray/autoload_carray.rb +3 -3
- data/lib/carray/autoload_method_extension.rb +5 -3
- data/lib/carray/axis_group.rb +13 -7
- data/lib/carray/basics.rb +1 -1
- data/lib/carray/block_iterator.rb +17 -3
- data/lib/carray/categorical.rb +6 -8
- data/lib/carray/categorical_iterator.rb +2 -2
- data/lib/carray/complex.rb +2 -2
- data/lib/carray/conditional.rb +3 -2
- data/lib/carray/frame/column_verbs.rb +45 -14
- data/lib/carray/frame/convert.rb +19 -0
- data/lib/carray/frame/csv_parser.rb +32 -32
- data/lib/carray/frame/describe.rb +4 -2
- data/lib/carray/frame/frame.rb +399 -78
- data/lib/carray/frame/group.rb +679 -53
- data/lib/carray/frame/io.rb +209 -71
- data/lib/carray/frame/join.rb +146 -37
- data/lib/carray/frame/records.rb +3 -3
- data/lib/carray/frame/reshape.rb +89 -11
- data/lib/carray/frame/sort.rb +57 -9
- data/lib/carray/frame/verbs.rb +384 -49
- data/lib/carray/frame/windows.rb +44 -6
- data/lib/carray/inspect.rb +7 -2
- data/lib/carray/iterator.rb +32 -0
- data/lib/carray/lazy.rb +17 -8
- data/lib/carray/mask_gap_fill.rb +37 -17
- data/lib/carray/meld_reduce.rb +1 -1
- data/lib/carray/methods/choose.rb +3 -2
- data/lib/carray/methods/meshgrid.rb +3 -1
- data/lib/carray/runtime.rb +47 -8
- data/lib/carray/segment_iterator.rb +23 -8
- data/lib/carray/serialize.rb +34 -17
- data/lib/carray/stack.rb +18 -6
- data/lib/carray/time.rb +5 -0
- data/lib/carray/window_iterator.rb +16 -4
- data/yard-stubs/ca_obj_array.rb +26 -1
- data/yard-stubs/carray_cast.rb +8 -5
- data/yard-stubs/carray_conversion.rb +8 -2
- data/yard-stubs/carray_mask.rb +20 -1
- data/yard-stubs/carray_math.rb +31 -16
- data/yard-stubs/carray_memory_view.rb +7 -0
- data/yard-stubs/carray_test.rb +7 -4
- metadata +18 -4
data/lib/carray/frame/group.rb
CHANGED
|
@@ -6,7 +6,57 @@
|
|
|
6
6
|
# -> group_by_category, so the frame layer only builds the key and hands
|
|
7
7
|
# the group iterator back (exposure, §4.3).
|
|
8
8
|
|
|
9
|
+
require "carray/distance_limit"
|
|
10
|
+
|
|
9
11
|
class CAFrame
|
|
12
|
+
# How many rows hold each value of a column, or each combination of the
|
|
13
|
+
# values of several (every column by default), as a frame: the values as
|
|
14
|
+
# its index (one column) or its first columns (several), and their counts
|
|
15
|
+
# in a column +name:+ ("count"). +sort:+ orders the rows by +:count+, the
|
|
16
|
+
# most first (the default; ties in the order the values first appear), by
|
|
17
|
+
# +:value+, or not at all (+false+, the order the values first appear). A
|
|
18
|
+
# row with a masked value is not counted, as +group_by+ leaves it out.
|
|
19
|
+
#
|
|
20
|
+
# df.value_counts("station")
|
|
21
|
+
# df.value_counts("station", "weather", sort: :value)
|
|
22
|
+
#
|
|
23
|
+
# The columns are given as +select+ takes them. +group_by(...).size+ is the
|
|
24
|
+
# same count in the order of the groups.
|
|
25
|
+
#
|
|
26
|
+
# @return [CAFrame]
|
|
27
|
+
# @raise [ArgumentError] for another +sort:+, or a +name:+ a key has.
|
|
28
|
+
def value_counts (*names, sort: :count, name: "count")
|
|
29
|
+
unless [:count, :value, false].include?(sort)
|
|
30
|
+
raise ArgumentError, "value_counts: sort: is :count, :value or false (got #{sort.inspect})"
|
|
31
|
+
end
|
|
32
|
+
keys = names.empty? ? column_names : names_selected(names, "value_counts")
|
|
33
|
+
if keys.empty?
|
|
34
|
+
hint = @index ? "; name the index (#{@axis_name.inspect}) to count its values" : ""
|
|
35
|
+
raise ArgumentError, "value_counts: the frame has no columns#{hint}"
|
|
36
|
+
end
|
|
37
|
+
name = column_name_arg(name, "value_counts")
|
|
38
|
+
if keys.include?(name)
|
|
39
|
+
raise ArgumentError,
|
|
40
|
+
"value_counts: a counted column is named #{name.inspect}; name the count " \
|
|
41
|
+
"otherwise, as in value_counts(..., name: \"n\")"
|
|
42
|
+
end
|
|
43
|
+
out = group_by(*keys).size(name: name)
|
|
44
|
+
case sort
|
|
45
|
+
when :count then out.sort_by_key(name, descending: true)
|
|
46
|
+
when :value
|
|
47
|
+
# A categorical key is ordered by its labels, as CArray#value_counts
|
|
48
|
+
# orders one; its codes are in the order labels were met.
|
|
49
|
+
cols = keys.map do |key|
|
|
50
|
+
col = out.__send__(:key_column, key, "value_counts")
|
|
51
|
+
col.is_a?(CACategorical) ? col.to_type(:object) : col
|
|
52
|
+
end
|
|
53
|
+
out.__send__(:select_rows, CArray.sort_addr(*cols))
|
|
54
|
+
else out
|
|
55
|
+
end
|
|
56
|
+
rescue ArgumentError, KeyError, TypeError, CArray::DataTypeError => e
|
|
57
|
+
raise e.class, e.message.sub(/\A(group_by|size|sort_by_key):/, "value_counts:"), e.backtrace
|
|
58
|
+
end
|
|
59
|
+
|
|
10
60
|
# Group rows by one or more keys. Each key is a column name (String) or an
|
|
11
61
|
# external length-N CArray. Returns a GroupedFrame.
|
|
12
62
|
#
|
|
@@ -234,6 +284,348 @@ class CAFrame
|
|
|
234
284
|
end
|
|
235
285
|
col
|
|
236
286
|
end
|
|
287
|
+
|
|
288
|
+
# Each column reduced over all the rows, as a frame of one row: the
|
|
289
|
+
# reductions of +GroupedFrame+ with every row in one group. Given names, the
|
|
290
|
+
# frame of those columns; without, of every column whose type has the
|
|
291
|
+
# reduction. A column keeps its type where the reduction keeps it (the
|
|
292
|
+
# earliest time of a time column is a time), and +min_count:+ /
|
|
293
|
+
# +fill_value:+ / +keep_rows:+ work as they do for a group. A frame with no
|
|
294
|
+
# rows still gives its one row: a sum of nothing is 0, a mean UNDEF.
|
|
295
|
+
#
|
|
296
|
+
# df.mean # => one row, the mean of every numeric column
|
|
297
|
+
# df.max("tmax", "tmin") # => one row, the two maxima
|
|
298
|
+
# df["tmax"] - df.mean("tmax", keep_rows: true) # the anomaly on each row
|
|
299
|
+
#
|
|
300
|
+
# For one column's value as a Ruby value, reduce the column: +df["tmax"].mean+.
|
|
301
|
+
#
|
|
302
|
+
# @!method sum(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
303
|
+
# @return [CAFrame] one row.
|
|
304
|
+
# @!method mean(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
305
|
+
# @return [CAFrame] one row.
|
|
306
|
+
# @!method min(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
307
|
+
# @return [CAFrame] one row.
|
|
308
|
+
# @!method max(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
309
|
+
# @return [CAFrame] one row.
|
|
310
|
+
# @!method prod(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
311
|
+
# @return [CAFrame] one row.
|
|
312
|
+
# @!method accumulate(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
313
|
+
# @return [CAFrame] one row.
|
|
314
|
+
# @!method median(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
315
|
+
# @return [CAFrame] one row.
|
|
316
|
+
# @!method variance(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
317
|
+
# @return [CAFrame] one row.
|
|
318
|
+
# @!method stddev(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
319
|
+
# @return [CAFrame] one row.
|
|
320
|
+
# @!method variancep(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
321
|
+
# @return [CAFrame] one row.
|
|
322
|
+
# @!method stddevp(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
323
|
+
# @return [CAFrame] one row.
|
|
324
|
+
# @!method first(*names, keep_rows: false)
|
|
325
|
+
# @return [CAFrame] one row: each column's first present value.
|
|
326
|
+
# @!method last(*names, keep_rows: false)
|
|
327
|
+
# @return [CAFrame] one row: each column's last present value.
|
|
328
|
+
# @!method nunique(*names, keep_rows: false)
|
|
329
|
+
# @return [CAFrame] one row: each column's number of distinct present values.
|
|
330
|
+
# @!method count(*names, keep_rows: false)
|
|
331
|
+
# @return [CAFrame] one row: each column's number of present values.
|
|
332
|
+
# @!method all(*names, keep_rows: false)
|
|
333
|
+
# @return [CAFrame] one row, for the boolean columns.
|
|
334
|
+
# @!method any(*names, keep_rows: false)
|
|
335
|
+
# @return [CAFrame] one row, for the boolean columns.
|
|
336
|
+
CArray.send(:define_named_methods, self,
|
|
337
|
+
[:sum, :mean, :min, :max, :prod, :accumulate, :median,
|
|
338
|
+
:variance, :stddev, :variancep, :stddevp], :reduce_all_rows)
|
|
339
|
+
|
|
340
|
+
CArray.send(:define_named_methods, self,
|
|
341
|
+
[:first, :last, :nunique, :count, :all, :any], :reduce_all_rows_to_a_value)
|
|
342
|
+
|
|
343
|
+
# Each column's +p+-th percentile (0 to 100) over all the rows, as a frame
|
|
344
|
+
# of one row, as {#median} gives the 50th. The percentage comes first, then
|
|
345
|
+
# the names.
|
|
346
|
+
#
|
|
347
|
+
# df.percentile(90, "revenue")
|
|
348
|
+
#
|
|
349
|
+
# @param p [Numeric] the percentage.
|
|
350
|
+
# @param names [Array<String>] the columns; default every column that has
|
|
351
|
+
# a percentile.
|
|
352
|
+
# @return [CAFrame, CArray] one row; with +keep_rows: true+ the frame's
|
|
353
|
+
# rows, one name giving the CArray.
|
|
354
|
+
def percentile (p, *names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
355
|
+
CAFrame.__send__(:check_percentage, p)
|
|
356
|
+
reduce_all_rows(:percentile, *names, min_count: min_count, fill_value: fill_value,
|
|
357
|
+
keep_rows: keep_rows, args: [p])
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Each column's five quartile percentiles over all the rows, as
|
|
361
|
+
# +CArray#quantile+ gives them, as a frame of one row: a column "x" gives
|
|
362
|
+
# the five columns "x_p0", "x_p25", "x_p50", "x_p75" and "x_p100". With
|
|
363
|
+
# +keep_rows: true+ every row holds them.
|
|
364
|
+
#
|
|
365
|
+
# df.quantile("temp")
|
|
366
|
+
#
|
|
367
|
+
# @param names [Array<String>] the columns; default every column that has
|
|
368
|
+
# a quantile.
|
|
369
|
+
# @return [CAFrame]
|
|
370
|
+
def quantile (*names, keep_rows: false)
|
|
371
|
+
named = {}
|
|
372
|
+
ColumnWalk.new(self).over(:quantile, names) do |col, name|
|
|
373
|
+
named[name] = col.quantile(axis: 0, keep_axis: true).map do |q|
|
|
374
|
+
keep_rows ? q[CArray.int64(@nrow).fill(0), *([nil] * (q.ndim - 1))].copy : q
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
cols = CAFrame.__send__(:quantile_columns, named)
|
|
378
|
+
return CAFrame.new(cols) unless keep_rows
|
|
379
|
+
return CAFrame.new(cols, axis_name: @axis_name, index: @index)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# The five columns a quantile gives a column, by name.
|
|
383
|
+
def self.quantile_columns(named)
|
|
384
|
+
named.each_with_object({}) do |(name, quartiles), cols|
|
|
385
|
+
QUANTILE_SUFFIXES.zip(quartiles) { |suffix, q| cols["#{name}_#{suffix}"] = q }
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
private_class_method :quantile_columns
|
|
389
|
+
|
|
390
|
+
QUANTILE_SUFFIXES = %w[p0 p25 p50 p75 p100].freeze
|
|
391
|
+
private_constant :QUANTILE_SUFFIXES
|
|
392
|
+
|
|
393
|
+
private def reduce_all_rows (reduction, *names, min_count: nil, fill_value: nil, keep_rows: false, args: [])
|
|
394
|
+
if fill_value.is_a?(Hash)
|
|
395
|
+
unknown = fill_value.keys - column_names
|
|
396
|
+
unless unknown.empty?
|
|
397
|
+
raise ArgumentError,
|
|
398
|
+
"#{reduction}: fill_value names no column #{unknown.map(&:inspect).join(', ')}"
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
return reduce_down_the_rows(reduction, names, keep_rows, args) do |name|
|
|
402
|
+
fill = fill_value.is_a?(Hash) ? fill_value[name] : fill_value
|
|
403
|
+
{ min_count: min_count, fill_value: fill }.compact
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
private def reduce_all_rows_to_a_value (reduction, *names, keep_rows: false)
|
|
408
|
+
return reduce_down_the_rows(reduction, names, keep_rows) { |_| {} }
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Each named column, or every column whose type has the reduction,
|
|
412
|
+
# reduced by the column itself along the rows to one row (keep_axis), so
|
|
413
|
+
# it keeps its type as the column's reduction keeps it. The block gives a
|
|
414
|
+
# column's keywords. A frame of one row with no index; with +keep_rows+,
|
|
415
|
+
# each row holding the result, as a group's +keep_rows+ gives it.
|
|
416
|
+
private def reduce_down_the_rows (reduction, names, keep_rows, args = [])
|
|
417
|
+
meth = reduction == :count ? :count_not_masked : reduction
|
|
418
|
+
named = {}
|
|
419
|
+
out = ColumnWalk.new(self).over(reduction, names, [], args) do |col, name|
|
|
420
|
+
result = col.public_send(meth, *args, axis: 0, keep_axis: true, **yield(name))
|
|
421
|
+
if keep_rows
|
|
422
|
+
result = result[CArray.int64(@nrow).fill(0), *([nil] * (result.ndim - 1))].copy
|
|
423
|
+
end
|
|
424
|
+
named[name] = result
|
|
425
|
+
end
|
|
426
|
+
return CAFrame.new(named) unless keep_rows
|
|
427
|
+
return out if out.is_a?(CArray)
|
|
428
|
+
return CAFrame.new(named, axis_name: @axis_name, index: @index)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Values computed down the rows, one per row, as +GroupedFrame+ computes
|
|
432
|
+
# them within each group, with every row in one group: each row's rank
|
|
433
|
+
# (+order+, int64, 0 the smallest) and the running values up to and
|
|
434
|
+
# including it. Given names, one name gives the CArray and several a frame
|
|
435
|
+
# of those columns; without names, a frame of every column whose type has
|
|
436
|
+
# the operation. A frame is on the frame's rows and index.
|
|
437
|
+
#
|
|
438
|
+
# df["total"] = df.cumsum("rain")
|
|
439
|
+
# df["rank"] = df.order("temp", descending: true)
|
|
440
|
+
#
|
|
441
|
+
# @!method order(*names, descending: false, method: :ordinal, kind: :quick)
|
|
442
|
+
# @return [CAFrame, CArray]
|
|
443
|
+
# @!method cumsum(*names)
|
|
444
|
+
# @return [CAFrame, CArray]
|
|
445
|
+
# @!method cumprod(*names)
|
|
446
|
+
# @return [CAFrame, CArray]
|
|
447
|
+
# @!method cummax(*names)
|
|
448
|
+
# @return [CAFrame, CArray]
|
|
449
|
+
# @!method cummin(*names)
|
|
450
|
+
# @return [CAFrame, CArray]
|
|
451
|
+
# @!method cumcount(*names)
|
|
452
|
+
# @return [CAFrame, CArray]
|
|
453
|
+
CArray.send(:define_named_methods, self,
|
|
454
|
+
[:order, :cumsum, :cumprod, :cummax, :cummin, :cumcount], :run_down_all_rows)
|
|
455
|
+
|
|
456
|
+
private def run_down_all_rows (operation, *names, **keywords)
|
|
457
|
+
# The keywords are the core's to check, before any column is read.
|
|
458
|
+
CArray.float64(1).public_send(operation, **keywords)
|
|
459
|
+
out = ColumnWalk.new(self).over(operation, names, [], [], keywords) do |col, _|
|
|
460
|
+
col.public_send(operation, **(col.ndim > 1 ? keywords.merge(axis: 0) : keywords))
|
|
461
|
+
end
|
|
462
|
+
return out if out.is_a?(CArray)
|
|
463
|
+
return CAFrame.new(out, axis_name: @axis_name, index: @index)
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# The frame's own walk over its columns: the one rule of a frame-wide verb
|
|
467
|
+
# (ColumnVerbs) -- which columns, and the shape of the answer -- for the
|
|
468
|
+
# verbs that run down all the rows, each column computed by itself.
|
|
469
|
+
class ColumnWalk
|
|
470
|
+
include CAFrame::ColumnVerbs
|
|
471
|
+
|
|
472
|
+
def initialize (frame)
|
|
473
|
+
@frame = frame
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def over (verb, names, key_names = [], args = [], keywords = {}, &block)
|
|
477
|
+
return over_named_columns(verb, names, key_names, args, keywords, &block)
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
private_constant :ColumnWalk
|
|
481
|
+
|
|
482
|
+
# The values of the columns moved along the rows: +n:+ rows back (a
|
|
483
|
+
# negative +n+ looks ahead), or the value at an exact distance of the key
|
|
484
|
+
# -- +before: "1 hour"+ gives each row the value of the row whose key is
|
|
485
|
+
# one hour earlier, +after:+ one hour later. The key is the index, or the
|
|
486
|
+
# column +on:+ names; it is a time, a duration or a number, and the
|
|
487
|
+
# distance is a duration such as "1 hour" (or a CATimedelta value) or a
|
|
488
|
+
# number accordingly. A row with no row at that key gets UNDEF; so does a
|
|
489
|
+
# row whose key is masked. The rows need not be in order. The result is
|
|
490
|
+
# new arrays in every form, so writing to it, or filling it once it is
|
|
491
|
+
# stored as a column, does not reach the column it came from.
|
|
492
|
+
#
|
|
493
|
+
# df["dtemp"] = df["temp"] - df.lag("temp", before: "1 hour")
|
|
494
|
+
# df.lag("temp", n: 1) # the previous row
|
|
495
|
+
#
|
|
496
|
+
# +GroupedFrame#lag+ does the same within each group. Given names, one
|
|
497
|
+
# name gives the CArray and several a frame of those columns; without
|
|
498
|
+
# names, a frame of every column (the +on:+ column left out). A frame is
|
|
499
|
+
# on the frame's rows and index. A result shows the column's attributes, as +CArray#lag+'s
|
|
500
|
+
# does.
|
|
501
|
+
#
|
|
502
|
+
# @param names [Array<String>] the columns.
|
|
503
|
+
# @param n [Integer] rows back.
|
|
504
|
+
# @param before [String, CATimedelta::Element, Numeric] the distance back.
|
|
505
|
+
# @param after [String, CATimedelta::Element, Numeric] the distance ahead.
|
|
506
|
+
# @param on [String] the key column, with +before:+ or +after:+ (default the
|
|
507
|
+
# index).
|
|
508
|
+
# @return [CArray, CAFrame]
|
|
509
|
+
# @raise [ArgumentError] when +n:+, +before:+ and +after:+ are combined,
|
|
510
|
+
# when +on:+ comes without a distance, when there is no key, or when the
|
|
511
|
+
# key repeats (the row at a key would not be one row).
|
|
512
|
+
def lag (*names, n: nil, before: nil, after: nil, on: nil)
|
|
513
|
+
distance = lag_distance("lag", n, before, after, on)
|
|
514
|
+
walk = ColumnWalk.new(self)
|
|
515
|
+
if distance.nil?
|
|
516
|
+
step = n.nil? ? 1 : n
|
|
517
|
+
cols = walk.over(:lag, names, [], [step]) do |col, _|
|
|
518
|
+
(col.ndim > 1 ? col.lag(step, axis: 0) : col.lag(step)).copy
|
|
519
|
+
end
|
|
520
|
+
else
|
|
521
|
+
addr = lag_addresses("lag", [CArray.int64(nrow).seq], *distance)
|
|
522
|
+
cols = walk.over(:lag, names, [distance.first], [1]) do |col, _|
|
|
523
|
+
col[addr, *([nil] * (col.ndim - 1))].copy
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
return cols if cols.is_a?(CArray)
|
|
527
|
+
CAFrame.new(cols, axis_name: @axis_name, index: @index)
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# [key name, key, sign, width] for a lag by distance; nil for one by rows.
|
|
531
|
+
private def lag_distance (verb, n, before, after, on)
|
|
532
|
+
if before.nil? && after.nil?
|
|
533
|
+
raise ArgumentError, "#{verb}: on: goes with before: or after:" unless on.nil?
|
|
534
|
+
return nil
|
|
535
|
+
end
|
|
536
|
+
unless n.nil?
|
|
537
|
+
raise ArgumentError, "#{verb}: give n: (rows) or before: / after: (a distance of the key), not both"
|
|
538
|
+
end
|
|
539
|
+
unless before.nil? || after.nil?
|
|
540
|
+
raise ArgumentError, "#{verb}: give before: or after:, not both"
|
|
541
|
+
end
|
|
542
|
+
if on.nil?
|
|
543
|
+
unless @index
|
|
544
|
+
raise ArgumentError, "#{verb}: the frame has no index; name the key column with on:"
|
|
545
|
+
end
|
|
546
|
+
on_name = @axis_name
|
|
547
|
+
else
|
|
548
|
+
on_name = column_key(on, verb)
|
|
549
|
+
end
|
|
550
|
+
key = key_column(on_name, verb)
|
|
551
|
+
unless key.is_a?(CATime) || key.is_a?(CATimedelta) || (!key.face? && key.numeric?)
|
|
552
|
+
raise ArgumentError,
|
|
553
|
+
"#{verb}: the key #{on_name.inspect} is #{CArray::Inspector.type_label(key)}; " \
|
|
554
|
+
"a distance is taken along a time, a duration or a number"
|
|
555
|
+
end
|
|
556
|
+
label = before.nil? ? "#{verb}: after:" : "#{verb}: before:"
|
|
557
|
+
width = CArray::DistanceLimit.read(before || after, key, label)
|
|
558
|
+
return [on_name, key, before.nil? ? :+ : :-, width]
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# For each row, the address of the row of its row set whose key is its own
|
|
562
|
+
# key moved by the width; masked where there is none. A row in no set is
|
|
563
|
+
# masked too, and so is a row whose key is NaN or infinite, which is no
|
|
564
|
+
# distance from any key (as in windows).
|
|
565
|
+
private def lag_addresses (verb, row_sets, _on_name, key, sign, width)
|
|
566
|
+
key = key.mask_invalid if key.float?
|
|
567
|
+
addr = CArray.int64(nrow)
|
|
568
|
+
addr[] = UNDEF
|
|
569
|
+
row_sets.each do |rows|
|
|
570
|
+
keys = key[rows]
|
|
571
|
+
next if keys.count_not_masked == 0
|
|
572
|
+
if keys.nunique < keys.count_not_masked
|
|
573
|
+
raise ArgumentError,
|
|
574
|
+
"#{verb}: the key repeats#{row_sets.size > 1 ? " within a group" : ""}, " \
|
|
575
|
+
"so the row at a key is not one row"
|
|
576
|
+
end
|
|
577
|
+
addr[rows] = rows[lag_target(keys, sign, width).locate_addr(keys)]
|
|
578
|
+
end
|
|
579
|
+
return addr
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
# The keys moved by the width. A time is moved on the finer of its unit and
|
|
583
|
+
# the width's, so that an hour back from a day is not rounded onto the day.
|
|
584
|
+
private def lag_target (keys, sign, width)
|
|
585
|
+
if width.is_a?(CATimedelta::Element) &&
|
|
586
|
+
CATimeUnitAlgebra.same_group?(keys.unit.base, width.unit.base)
|
|
587
|
+
keys = keys.to_unit(CATimeUnitAlgebra.finer(keys.unit, width.unit))
|
|
588
|
+
end
|
|
589
|
+
return lag_integer_target(keys, sign, width) if keys.integer?
|
|
590
|
+
return keys.public_send(sign, width)
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
LAG_UNSIGNED = [CA_UINT8, CA_UINT16, CA_UINT32, CA_UINT64].freeze
|
|
594
|
+
private_constant :LAG_UNSIGNED
|
|
595
|
+
|
|
596
|
+
# Integer keys moved by the width exactly, in int64 (or as Ruby integers
|
|
597
|
+
# for a 64-bit key a step does not fit in). A target outside the key's
|
|
598
|
+
# range is masked, so it matches no row rather than wrapping onto one; a
|
|
599
|
+
# width that is not a whole number matches no row either.
|
|
600
|
+
private def lag_integer_target (keys, sign, width)
|
|
601
|
+
bits = keys.bytes * 8
|
|
602
|
+
if LAG_UNSIGNED.include?(keys.data_type)
|
|
603
|
+
lo, hi = 0, 2**bits - 1
|
|
604
|
+
else
|
|
605
|
+
lo, hi = -2**(bits - 1), 2**(bits - 1) - 1
|
|
606
|
+
end
|
|
607
|
+
step = width.is_a?(Integer) ? width : nil
|
|
608
|
+
if step.nil? && width.respond_to?(:finite?) && width.finite? && width == width.floor
|
|
609
|
+
step = width.to_i
|
|
610
|
+
end
|
|
611
|
+
if step.nil? || step > hi - lo
|
|
612
|
+
out = CArray.int64(*keys.shape)
|
|
613
|
+
out[] = UNDEF
|
|
614
|
+
return out
|
|
615
|
+
end
|
|
616
|
+
if bits < 64
|
|
617
|
+
return keys.to_type(CA_INT64).public_send(sign, step)
|
|
618
|
+
end
|
|
619
|
+
if step > 2**63 - 1
|
|
620
|
+
moved = keys.to_type(CA_OBJECT).public_send(sign, step)
|
|
621
|
+
moved[moved.lt(lo).or(moved.gt(hi))] = UNDEF
|
|
622
|
+
return moved
|
|
623
|
+
end
|
|
624
|
+
out = keys.copy
|
|
625
|
+
out[sign == :- ? keys.lt(lo + step) : keys.gt(hi - step)] = UNDEF
|
|
626
|
+
return out.public_send(sign, step)
|
|
627
|
+
end
|
|
628
|
+
|
|
237
629
|
end
|
|
238
630
|
|
|
239
631
|
# GroupedFrame — the result of +CAFrame#group_by+ (memo §11.6, §16).
|
|
@@ -281,6 +673,40 @@ class GroupedFrame
|
|
|
281
673
|
@labels.size
|
|
282
674
|
end
|
|
283
675
|
|
|
676
|
+
# The number of rows in each group, as a frame of one column, "size"
|
|
677
|
+
# unless +name:+ says otherwise, labelled as a reduction's result is. A row
|
|
678
|
+
# whose key is masked is in no group, so the sizes need not add up to the
|
|
679
|
+
# frame's nrow.
|
|
680
|
+
#
|
|
681
|
+
# @param name [String] the result column's name.
|
|
682
|
+
# @return [CAFrame]
|
|
683
|
+
# @raise [ArgumentError] when a key has that name.
|
|
684
|
+
def size (name: "size")
|
|
685
|
+
refuse_changed_rows("size")
|
|
686
|
+
name = @frame.__send__(:column_name_arg, name, "size")
|
|
687
|
+
if @key_names.include?(name) || @axis_name == name
|
|
688
|
+
raise ArgumentError,
|
|
689
|
+
"size: a key is named #{name.inspect}; name the result otherwise, as in size(name: \"n\")"
|
|
690
|
+
end
|
|
691
|
+
return result_frame({ name => @cat.category_sizes.int64 }, false)
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
# Each group's label (an Array of the key values for several keys) and its
|
|
695
|
+
# rows as a sub-frame sharing the frame's columns, in the order of
|
|
696
|
+
# +labels+. Without a block, an Enumerator.
|
|
697
|
+
#
|
|
698
|
+
# df.group_by("station").each { |station, rows| puts station, rows.nrow }
|
|
699
|
+
#
|
|
700
|
+
# @yieldparam label [Object, Array] the group's key value(s).
|
|
701
|
+
# @yieldparam rows [CAFrame] the group's rows.
|
|
702
|
+
# @return [GroupedFrame] self
|
|
703
|
+
def each
|
|
704
|
+
return enum_for(:each) { ngroup } unless block_given?
|
|
705
|
+
refuse_changed_rows("each")
|
|
706
|
+
ngroup.times { |k| yield @labels[k], @frame[group_address(k)] }
|
|
707
|
+
return self
|
|
708
|
+
end
|
|
709
|
+
|
|
284
710
|
# Group key values (one per group), as an Array.
|
|
285
711
|
def labels
|
|
286
712
|
@labels.dup
|
|
@@ -367,6 +793,12 @@ class GroupedFrame
|
|
|
367
793
|
cols = {}
|
|
368
794
|
spec.each do |out_name, (in_name, reduction, *args)|
|
|
369
795
|
out = @frame.__send__(:column_name_arg, out_name, "aggregate")
|
|
796
|
+
if in_name.is_a?(Symbol)
|
|
797
|
+
raise ArgumentError,
|
|
798
|
+
"aggregate: each entry is result name => [column, reduction] " \
|
|
799
|
+
"(got #{out_name.inspect} => #{spec[out_name].inspect}); " \
|
|
800
|
+
"as in aggregate(\"mean_#{out_name}\" => [#{out_name.inspect}, :mean])"
|
|
801
|
+
end
|
|
370
802
|
@frame.__send__(:column_key, in_name, "aggregate")
|
|
371
803
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
372
804
|
cols[out] =
|
|
@@ -426,20 +858,24 @@ class GroupedFrame
|
|
|
426
858
|
# fills those columns only. +keep_rows: true+ gives each row its group's value
|
|
427
859
|
# instead of one row per group, as for +aggregate+.
|
|
428
860
|
#
|
|
429
|
-
# Given column names, they reduce those columns only and
|
|
430
|
-
#
|
|
861
|
+
# Given column names, they reduce those columns only, and the result is
|
|
862
|
+
# the frame of those columns, one row per group, labelled as above; with
|
|
863
|
+
# +keep_rows: true+ one comes back as +df[name]+ returns it, a CArray on
|
|
864
|
+
# the frame's rows, and several as a frame on those rows. For a group's
|
|
865
|
+
# values alone, reduce the column's group iterator: +grp["temp"].mean+.
|
|
431
866
|
#
|
|
432
867
|
# grp.mean # a frame, one row per group
|
|
433
|
-
# grp.mean("temp") #
|
|
868
|
+
# grp.mean("temp") # a frame of temp, one row per group
|
|
434
869
|
# df["temp"] - grp.mean("temp", keep_rows: true) # anomaly from the group mean
|
|
435
870
|
#
|
|
436
871
|
# @!method sum(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
437
872
|
# Returns a frame of the per-group sum of every column whose type has a
|
|
438
873
|
# sum, an N-D one per group along the rows. Other columns are left out.
|
|
439
|
-
# @return [CAFrame, CArray
|
|
440
|
-
# group, indexed by the group labels
|
|
441
|
-
# frame's rows and index; given
|
|
442
|
-
#
|
|
874
|
+
# @return [CAFrame, CArray] a frame with one row per
|
|
875
|
+
# group, indexed by the group labels (of the named columns when names
|
|
876
|
+
# are given), or with keep_rows: true the frame's rows and index; given
|
|
877
|
+
# names and keep_rows: true, the CArray (one) or a frame of them on the
|
|
878
|
+
# frame's rows (several).
|
|
443
879
|
# @!method mean(*names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
444
880
|
# Returns a frame of the per-group mean of every column whose type has
|
|
445
881
|
# a mean, as {#sum} does.
|
|
@@ -483,6 +919,43 @@ class GroupedFrame
|
|
|
483
919
|
reduce_columns(red, names, keep_rows, min_count: min_count, fill_value: fill_value)
|
|
484
920
|
end
|
|
485
921
|
|
|
922
|
+
# Each group's +p+-th percentile (0 to 100), as {#median} gives the 50th
|
|
923
|
+
# and {#sum} gives its sums. The percentage comes first, then the names.
|
|
924
|
+
#
|
|
925
|
+
# grp.percentile(90, "revenue")
|
|
926
|
+
#
|
|
927
|
+
# @param p [Numeric] the percentage.
|
|
928
|
+
# @param names [Array<String>] the columns; default every column that has
|
|
929
|
+
# a percentile.
|
|
930
|
+
# @return [CAFrame, CArray] one row per group, as {#sum} returns.
|
|
931
|
+
def percentile (p, *names, min_count: nil, fill_value: nil, keep_rows: false)
|
|
932
|
+
CAFrame.__send__(:check_percentage, p)
|
|
933
|
+
refuse_changed_rows(:percentile)
|
|
934
|
+
reduce_columns(:percentile, names, keep_rows, args: [p], min_count: min_count, fill_value: fill_value)
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
# Each group's five quartile percentiles, as +CAFrame#quantile+ gives them
|
|
938
|
+
# for all the rows: a column "x" gives "x_p0", "x_p25", "x_p50", "x_p75"
|
|
939
|
+
# and "x_p100", one row per group, labelled as {#sum} labels its result;
|
|
940
|
+
# with +keep_rows: true+ on the frame's rows.
|
|
941
|
+
#
|
|
942
|
+
# grp.quantile("temp")
|
|
943
|
+
#
|
|
944
|
+
# @param names [Array<String>] the columns; default every column that has
|
|
945
|
+
# a quantile.
|
|
946
|
+
# @return [CAFrame]
|
|
947
|
+
def quantile (*names, keep_rows: false)
|
|
948
|
+
refuse_changed_rows(:quantile)
|
|
949
|
+
refuse_key_as_result(:quantile, names) unless keep_rows
|
|
950
|
+
named = {}
|
|
951
|
+
over_named_columns(:quantile, names, @key_names) do |col, name|
|
|
952
|
+
named[name] = group_reduce(col, :quantile).map { |q| keep_rows ? spread_to_rows(q) : q }
|
|
953
|
+
end
|
|
954
|
+
cols = CAFrame.__send__(:quantile_columns, named)
|
|
955
|
+
return result_frame(cols, false) unless keep_rows
|
|
956
|
+
return CAFrame.new(cols, axis_name: @frame.axis_name, index: @frame.index)
|
|
957
|
+
end
|
|
958
|
+
|
|
486
959
|
# The same convenience for the reductions that take no min_count: or
|
|
487
960
|
# fill_value:, as the core ones take none: a value of the group (first,
|
|
488
961
|
# last), a count (nunique, count) or a boolean fold (all, any).
|
|
@@ -522,8 +995,8 @@ class GroupedFrame
|
|
|
522
995
|
# group's rows, a running statistic. A row in no group is UNDEF. Without
|
|
523
996
|
# names they give a frame of every column whose type has the operation
|
|
524
997
|
# (the key columns left out), on the frame's rows and index; given names,
|
|
525
|
-
# the CArray (one)
|
|
526
|
-
# columns. An N-D column runs along the rows.
|
|
998
|
+
# the CArray (one), as +df[name]+ returns a column, or a frame of those
|
|
999
|
+
# columns (several) on the frame's rows and index. An N-D column runs along the rows.
|
|
527
1000
|
#
|
|
528
1001
|
# df["dtemp"] = df["temp"] - grp.lag("temp") # change since the station's previous row
|
|
529
1002
|
# grp.order("temp", descending: true) # rank within the station
|
|
@@ -532,31 +1005,43 @@ class GroupedFrame
|
|
|
532
1005
|
# "Previous" is the previous row of the frame: sort the frame first
|
|
533
1006
|
# (+sort_by_key("time")+) when its rows are not in time order.
|
|
534
1007
|
#
|
|
535
|
-
# @!method lag(*names, n: 1)
|
|
1008
|
+
# @!method lag(*names, n: 1, before: nil, after: nil, on: nil)
|
|
536
1009
|
# 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
|
-
#
|
|
1010
|
+
# the group has none, as +CArray#lag+; a negative +n+ looks ahead. With
|
|
1011
|
+
# +before:+ (or +after:+) instead, the value of the group's row whose key
|
|
1012
|
+
# (the index, or the column +on:+ names) is that distance earlier (or
|
|
1013
|
+
# later), as +CAFrame#lag+ gives it.
|
|
1014
|
+
# @return [CAFrame, CArray]
|
|
539
1015
|
# @!method order(*names, descending: false, method: :ordinal, kind: :quick)
|
|
540
1016
|
# Returns each row's rank among its group's rows (int64, 0 the
|
|
541
1017
|
# smallest), as +CArray#order+ ranks them.
|
|
542
|
-
# @return [CAFrame, CArray
|
|
1018
|
+
# @return [CAFrame, CArray]
|
|
543
1019
|
# @!method cumsum(*names)
|
|
544
1020
|
# Returns the running sum within each group, as +CArray#cumsum+.
|
|
545
|
-
# @return [CAFrame, CArray
|
|
1021
|
+
# @return [CAFrame, CArray]
|
|
546
1022
|
# @!method cumprod(*names)
|
|
547
1023
|
# Returns the running product within each group.
|
|
548
|
-
# @return [CAFrame, CArray
|
|
1024
|
+
# @return [CAFrame, CArray]
|
|
549
1025
|
# @!method cummax(*names)
|
|
550
1026
|
# Returns the running maximum within each group.
|
|
551
|
-
# @return [CAFrame, CArray
|
|
1027
|
+
# @return [CAFrame, CArray]
|
|
552
1028
|
# @!method cummin(*names)
|
|
553
1029
|
# Returns the running minimum within each group.
|
|
554
|
-
# @return [CAFrame, CArray
|
|
1030
|
+
# @return [CAFrame, CArray]
|
|
555
1031
|
# @!method cumcount(*names)
|
|
556
1032
|
# Returns the running count of present values within each group (int64).
|
|
557
|
-
# @return [CAFrame, CArray
|
|
558
|
-
def lag (*names, n:
|
|
559
|
-
|
|
1033
|
+
# @return [CAFrame, CArray]
|
|
1034
|
+
def lag (*names, n: nil, before: nil, after: nil, on: nil)
|
|
1035
|
+
distance = @frame.__send__(:lag_distance, "lag", n, before, after, on)
|
|
1036
|
+
return transform_columns(:lag, names, [n.nil? ? 1 : n], {}) if distance.nil?
|
|
1037
|
+
refuse_changed_rows("lag")
|
|
1038
|
+
refuse_key_on_rows(:lag, names)
|
|
1039
|
+
addr = @frame.__send__(:lag_addresses, "lag", (0...ngroup).map { |k| group_address(k) }, *distance)
|
|
1040
|
+
out = over_named_columns(:lag, names, @key_names | [distance.first], [1], {}) do |col|
|
|
1041
|
+
col[addr, *([nil] * (col.ndim - 1))].copy
|
|
1042
|
+
end
|
|
1043
|
+
return out if out.is_a?(CArray)
|
|
1044
|
+
CAFrame.new(out, axis_name: @frame.axis_name, index: @frame.index)
|
|
560
1045
|
end
|
|
561
1046
|
|
|
562
1047
|
def order (*names, descending: false, method: :ordinal, kind: :quick)
|
|
@@ -573,22 +1058,47 @@ class GroupedFrame
|
|
|
573
1058
|
# Fill the masked cells of a column from the rows of the same group, in
|
|
574
1059
|
# place, as +CAFrame#fill+ does for the whole column: +:ffill+ carries the
|
|
575
1060
|
# group's last present value forward in row order, +:bfill+ its next one
|
|
576
|
-
# back
|
|
577
|
-
#
|
|
578
|
-
#
|
|
579
|
-
#
|
|
1061
|
+
# back, and +:linear+ interpolates between the group's present values
|
|
1062
|
+
# against the frame's index (against the row position within the group
|
|
1063
|
+
# when the frame has no index). A fill never reaches across into another
|
|
1064
|
+
# group, so a cell with nothing to carry or interpolate from in its group
|
|
1065
|
+
# stays masked, and a row whose key is masked (in no group) is left as it
|
|
1066
|
+
# is. A constant fills every masked cell, as +CAFrame#fill+ does. Returns
|
|
1067
|
+
# self.
|
|
580
1068
|
#
|
|
581
1069
|
# df.group_by("station").fill("temp", :ffill)
|
|
1070
|
+
# df.group_by("station").fill("temp", :linear)
|
|
1071
|
+
# df.group_by("station").fill("temp" => :linear, "rain" => 0.0)
|
|
1072
|
+
# df.group_by("station").fill("temp", "rain", :linear)
|
|
1073
|
+
# df.group_by("station").fill(:ffill) # every column with a masked cell
|
|
582
1074
|
#
|
|
583
|
-
#
|
|
584
|
-
# time order.
|
|
1075
|
+
# +:ffill+ and +:bfill+ follow the row order: sort the frame first
|
|
1076
|
+
# (+sort_by_key("time")+) when its rows are not in time order. +:linear+
|
|
1077
|
+
# reads each group in the order of the index, as +CAFrame#fill+ does.
|
|
585
1078
|
#
|
|
586
|
-
# @param
|
|
587
|
-
#
|
|
1079
|
+
# @param args [Array] the columns, as +CAFrame#fill+ takes them, then
|
|
1080
|
+
# +:ffill+, +:bfill+, +:linear+, or a constant; or one Hash of them.
|
|
588
1081
|
# @return [GroupedFrame] self
|
|
589
|
-
# @raise [ArgumentError] for +:linear
|
|
590
|
-
#
|
|
591
|
-
def fill (
|
|
1082
|
+
# @raise [ArgumentError] for another Symbol, or for +:linear+ on a column
|
|
1083
|
+
# or index +CAFrame#fill+ cannot interpolate.
|
|
1084
|
+
def fill (*args)
|
|
1085
|
+
plan = @frame.__send__(:fill_arguments, args, @key_names)
|
|
1086
|
+
return self if plan.nil?
|
|
1087
|
+
refuse_key_on_rows(:fill, plan.is_a?(Hash) ? plan.keys : [plan.first])
|
|
1088
|
+
if plan.is_a?(Hash)
|
|
1089
|
+
@frame.__send__(:fill_by_map, plan, "fill") do |frame, key, how|
|
|
1090
|
+
grouped = if frame.equal?(@frame)
|
|
1091
|
+
self
|
|
1092
|
+
else
|
|
1093
|
+
# key-labels: the groups' index as it is, on a copy of the frame
|
|
1094
|
+
GroupedFrame.new(frame, @cat, @axis_name, @key_names, index: @index,
|
|
1095
|
+
label_columns: @label_columns)
|
|
1096
|
+
end
|
|
1097
|
+
grouped.fill(key, how)
|
|
1098
|
+
end
|
|
1099
|
+
return self
|
|
1100
|
+
end
|
|
1101
|
+
name, method_or_value = plan
|
|
592
1102
|
refuse_changed_rows("fill")
|
|
593
1103
|
case method_or_value
|
|
594
1104
|
when :ffill, :bfill
|
|
@@ -600,10 +1110,15 @@ class GroupedFrame
|
|
|
600
1110
|
target = col.is_masked
|
|
601
1111
|
col[target] = held[target] if target.any
|
|
602
1112
|
when :linear
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
1113
|
+
@frame.__send__(:refuse_if_frozen)
|
|
1114
|
+
key = @frame.__send__(:column_key, name, "fill")
|
|
1115
|
+
# The column and the index are checked as CAFrame#fill checks them,
|
|
1116
|
+
# on no rows, so a call without groups is refused as one with groups.
|
|
1117
|
+
@frame.__send__(:select_rows, CA_INT64([])).fill(key, :linear)
|
|
1118
|
+
# Each group's rows as a frame of views, so its fill writes through.
|
|
1119
|
+
ngroup.times do |k|
|
|
1120
|
+
@frame.__send__(:select_rows, group_address(k)).fill(key, :linear)
|
|
1121
|
+
end
|
|
607
1122
|
when Symbol
|
|
608
1123
|
raise ArgumentError,
|
|
609
1124
|
"fill: unknown method #{method_or_value.inspect} " \
|
|
@@ -614,32 +1129,113 @@ class GroupedFrame
|
|
|
614
1129
|
self
|
|
615
1130
|
end
|
|
616
1131
|
|
|
1132
|
+
# The first +n+ rows of each group, in row order, as a view-frame: the
|
|
1133
|
+
# rows stay in the frame's order, as +filter+ keeps them. A group with
|
|
1134
|
+
# fewer than +n+ rows gives all of them; a row whose group key is masked
|
|
1135
|
+
# is in no group.
|
|
1136
|
+
#
|
|
1137
|
+
# df.group_by("station").head(1) # each station's first row
|
|
1138
|
+
#
|
|
1139
|
+
# @param n [Integer] how many rows per group, 0 or more (default 5).
|
|
1140
|
+
# @return [CAFrame]
|
|
1141
|
+
def head (n = 5)
|
|
1142
|
+
return rows_at_the_ends_of_groups("head", n) { |addr| addr.first(n) }
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
# The last +n+ rows of each group, in row order, as +head+ gives the first.
|
|
1146
|
+
#
|
|
1147
|
+
# @param n [Integer] how many rows per group, 0 or more (default 5).
|
|
1148
|
+
# @return [CAFrame]
|
|
1149
|
+
def tail (n = 5)
|
|
1150
|
+
return rows_at_the_ends_of_groups("tail", n) { |addr| addr.last(n) }
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1153
|
+
private def rows_at_the_ends_of_groups (verb, n)
|
|
1154
|
+
refuse_changed_rows(verb)
|
|
1155
|
+
unless n.is_a?(Integer)
|
|
1156
|
+
raise TypeError, "#{verb}: the count is an Integer (got #{n.inspect})"
|
|
1157
|
+
end
|
|
1158
|
+
raise ArgumentError, "#{verb}: the count must be 0 or more (got #{n})" if n < 0
|
|
1159
|
+
picked = (0...ngroup).flat_map { |k| yield(group_address(k).to_a) }.sort
|
|
1160
|
+
return @frame[CA_INT64(picked)]
|
|
1161
|
+
end
|
|
1162
|
+
|
|
1163
|
+
# The +n+ rows of each group with the largest values of the key columns,
|
|
1164
|
+
# as a view-frame: the groups in the order of +labels+, each group's rows
|
|
1165
|
+
# largest first, as +CAFrame#nlargest+ orders them (several keys break
|
|
1166
|
+
# ties in turn, a NaN or masked key loses to every value, rows that tie on
|
|
1167
|
+
# every key keep their order). A group with fewer than +n+ rows gives all
|
|
1168
|
+
# of them; a row whose group key is masked is in no group.
|
|
1169
|
+
#
|
|
1170
|
+
# df.group_by("station").nlargest(3, "temp") # each station's 3 warmest rows
|
|
1171
|
+
#
|
|
1172
|
+
# @param n [Integer] how many rows per group, 0 or more.
|
|
1173
|
+
# @param keys [Array<String>] the key columns (or the index axis name).
|
|
1174
|
+
# @return [CAFrame]
|
|
1175
|
+
# @raise [ArgumentError] when +n+ is negative or no key is given.
|
|
1176
|
+
def nlargest (n, *keys)
|
|
1177
|
+
return largest_rows_of_groups("nlargest", n, keys, true)
|
|
1178
|
+
end
|
|
1179
|
+
|
|
1180
|
+
# The +n+ rows of each group with the smallest values of the key columns,
|
|
1181
|
+
# as +nlargest+ gives the largest.
|
|
1182
|
+
#
|
|
1183
|
+
# @param n [Integer] how many rows per group, 0 or more.
|
|
1184
|
+
# @param keys [Array<String>] the key columns (or the index axis name).
|
|
1185
|
+
# @return [CAFrame]
|
|
1186
|
+
# @raise [ArgumentError] when +n+ is negative or no key is given.
|
|
1187
|
+
def nsmallest (n, *keys)
|
|
1188
|
+
return largest_rows_of_groups("nsmallest", n, keys, false)
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
private def largest_rows_of_groups (verb, n, keys, descending)
|
|
1192
|
+
refuse_changed_rows(verb)
|
|
1193
|
+
unless n.is_a?(Integer)
|
|
1194
|
+
raise TypeError, "#{verb}: the count is an Integer (got #{n.inspect})"
|
|
1195
|
+
end
|
|
1196
|
+
raise ArgumentError, "#{verb}: the count must be 0 or more (got #{n})" if n < 0
|
|
1197
|
+
raise ArgumentError, "#{verb}: give at least one key column" if keys.empty?
|
|
1198
|
+
cols = keys.map do |key|
|
|
1199
|
+
@frame.__send__(:key_column, @frame.__send__(:column_key, key, verb), verb)
|
|
1200
|
+
end
|
|
1201
|
+
sort_keys = cols.map { |col| descending ? col.order(descending: true, method: :dense) : col }
|
|
1202
|
+
# Rows sorted by group, then by the keys; a row in no group comes last.
|
|
1203
|
+
addr = CArray.sort_addr(group_codes, *sort_keys, masked_position: :last).to_a
|
|
1204
|
+
picked = []
|
|
1205
|
+
start = 0
|
|
1206
|
+
@cat.category_sizes.to_a.each do |size|
|
|
1207
|
+
picked.concat(addr[start, [n, size].min]) if n > 0
|
|
1208
|
+
start += size
|
|
1209
|
+
end
|
|
1210
|
+
return @frame[CA_INT64(picked)]
|
|
1211
|
+
end
|
|
1212
|
+
|
|
617
1213
|
# Keep the rows of the groups the block chooses. The block receives the
|
|
618
1214
|
# grouped frame and returns a boolean CArray with one value per group,
|
|
619
1215
|
# built from the group reductions, as +CAFrame#filter+'s block returns one
|
|
620
1216
|
# per row:
|
|
621
1217
|
#
|
|
622
1218
|
# grp.filter { |g| g["temp"].count_not_masked >= 20 } # stations with 20 readings
|
|
623
|
-
# grp.filter { |g| g
|
|
1219
|
+
# grp.filter { |g| g["temp"].mean > 15.0 }
|
|
624
1220
|
#
|
|
625
1221
|
# The rows of a chosen group are kept in the frame's order, as a sub-frame
|
|
626
1222
|
# sharing the frame's columns. A group whose value is UNDEF is
|
|
627
1223
|
# undetermined, as is a row whose key is masked (it belongs to no group):
|
|
628
|
-
# such rows are
|
|
629
|
-
# cells masked, as +CAFrame#filter+ does.
|
|
1224
|
+
# such rows are not kept, as +CAFrame#filter+ does not keep them.
|
|
630
1225
|
#
|
|
631
|
-
# @param keep_masked [Boolean] carry the undetermined rows forward masked.
|
|
632
1226
|
# @yieldparam grouped [GroupedFrame] self.
|
|
633
1227
|
# @yieldreturn [CArray] boolean, one value per group.
|
|
634
1228
|
# @return [CAFrame]
|
|
635
1229
|
# @raise [ArgumentError] when the block does not return a boolean CArray of
|
|
636
1230
|
# shape [ngroup].
|
|
637
|
-
def filter (
|
|
1231
|
+
def filter (**options)
|
|
1232
|
+
CAFrame.__send__(:refuse_filter_keywords, options, "filter")
|
|
638
1233
|
refuse_changed_rows("filter")
|
|
639
1234
|
chosen = yield(self)
|
|
640
1235
|
unless chosen.is_a?(CArray) && chosen.data_type == :boolean
|
|
641
1236
|
raise ArgumentError,
|
|
642
|
-
"filter block must return a boolean CArray (got #{chosen.is_a?(CArray) ? "a #{CArray::Inspector.type_label(chosen)} CArray" : chosen.class})"
|
|
1237
|
+
"filter block must return a boolean CArray (got #{chosen.is_a?(CArray) ? "a #{CArray::Inspector.type_label(chosen)} CArray" : chosen.class})" +
|
|
1238
|
+
CAFrame.__send__(:equality_hint, chosen)
|
|
643
1239
|
end
|
|
644
1240
|
unless chosen.shape == [ngroup]
|
|
645
1241
|
raise ArgumentError,
|
|
@@ -647,7 +1243,7 @@ class GroupedFrame
|
|
|
647
1243
|
"expected [#{ngroup}] (one value per group)"
|
|
648
1244
|
end
|
|
649
1245
|
rows = spread_to_rows(chosen)
|
|
650
|
-
@frame.filter(
|
|
1246
|
+
@frame.filter(rows)
|
|
651
1247
|
end
|
|
652
1248
|
|
|
653
1249
|
# Returns the grouped rows as a frame: the frame the grouping was made
|
|
@@ -656,7 +1252,7 @@ class GroupedFrame
|
|
|
656
1252
|
# rows; adding or dropping a column changes the new frame only.
|
|
657
1253
|
#
|
|
658
1254
|
# def warm_stations (grp)
|
|
659
|
-
# grp.filter { |g| g
|
|
1255
|
+
# grp.filter { |g| g["temp"].mean > 15.0 } # the rows of some groups
|
|
660
1256
|
# end
|
|
661
1257
|
# grp.to_frame # all of its rows
|
|
662
1258
|
#
|
|
@@ -677,11 +1273,12 @@ class GroupedFrame
|
|
|
677
1273
|
# would otherwise leave every column out without a word.
|
|
678
1274
|
private def transform_columns(op, names, args, keywords)
|
|
679
1275
|
refuse_changed_rows(op)
|
|
1276
|
+
refuse_key_on_rows(op, names)
|
|
680
1277
|
CArray.float64(1).public_send(op, *args, **keywords)
|
|
681
1278
|
out = over_named_columns(op, names, @key_names, args, keywords) do |col|
|
|
682
1279
|
group_transform(col, op, args, keywords)
|
|
683
1280
|
end
|
|
684
|
-
return out
|
|
1281
|
+
return out if out.is_a?(CArray)
|
|
685
1282
|
CAFrame.new(out, axis_name: @frame.axis_name, index: @frame.index)
|
|
686
1283
|
end
|
|
687
1284
|
|
|
@@ -698,10 +1295,11 @@ class GroupedFrame
|
|
|
698
1295
|
CArray.object(@labels.size) { |i| @labels[i] }
|
|
699
1296
|
end
|
|
700
1297
|
|
|
701
|
-
# A key column is the index here, not a result column. Named columns
|
|
702
|
-
#
|
|
703
|
-
#
|
|
704
|
-
private def reduce_columns(reduction, names, keep_rows, **keywords)
|
|
1298
|
+
# A key column is the index here, not a result column. Named columns give
|
|
1299
|
+
# the frame of those columns only, one row per group; with +keep_rows+
|
|
1300
|
+
# they come back as df[...] returns them, each on the frame's rows.
|
|
1301
|
+
private def reduce_columns(reduction, names, keep_rows, args: [], **keywords)
|
|
1302
|
+
refuse_key_as_result(reduction, names) unless keep_rows
|
|
705
1303
|
keywords = keywords.compact
|
|
706
1304
|
fills = keywords.delete(:fill_value)
|
|
707
1305
|
if fills.is_a?(Hash)
|
|
@@ -711,14 +1309,42 @@ class GroupedFrame
|
|
|
711
1309
|
"#{reduction}: fill_value names no column #{unknown.map(&:inspect).join(', ')}"
|
|
712
1310
|
end
|
|
713
1311
|
end
|
|
714
|
-
|
|
1312
|
+
named = {}
|
|
1313
|
+
out = over_named_columns(reduction, names, @key_names, args) do |col, name|
|
|
715
1314
|
fill = fills.is_a?(Hash) ? fills[name] : fills
|
|
716
1315
|
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
|
|
1316
|
+
col = group_reduce(col, reduction, *args, **kw)
|
|
1317
|
+
named[name] = keep_rows && !names.empty? ? spread_to_rows(col) : col
|
|
1318
|
+
end
|
|
1319
|
+
return result_frame(out, keep_rows) if names.empty?
|
|
1320
|
+
# One named column on the frame's rows comes back as df[name] returns it,
|
|
1321
|
+
# several as a frame on those rows; one value per group needs the group
|
|
1322
|
+
# labels beside it.
|
|
1323
|
+
if keep_rows
|
|
1324
|
+
return out if out.is_a?(CArray)
|
|
1325
|
+
return CAFrame.new(out, axis_name: @frame.axis_name, index: @frame.index)
|
|
719
1326
|
end
|
|
720
|
-
|
|
721
|
-
|
|
1327
|
+
result_frame(named, false)
|
|
1328
|
+
end
|
|
1329
|
+
|
|
1330
|
+
# A key column named in a reduction would be a result column beside the
|
|
1331
|
+
# key it is the group of: the index, or a key column of the result.
|
|
1332
|
+
private def refuse_key_as_result(reduction, names)
|
|
1333
|
+
named = names.map { |n| n.is_a?(String) ? n : nil }.compact & @key_names
|
|
1334
|
+
return if named.empty?
|
|
1335
|
+
hint = reduction == :count ? "; the number of rows of each group is size" : ""
|
|
1336
|
+
raise ArgumentError,
|
|
1337
|
+
"#{reduction}: #{named.first.inspect} is the group key, already in the result#{hint}"
|
|
1338
|
+
end
|
|
1339
|
+
|
|
1340
|
+
# A key column named in a verb computed within each group: the key holds
|
|
1341
|
+
# one value in a group, so there is nothing to compute along it, and a
|
|
1342
|
+
# fill would write to what the groups were made from.
|
|
1343
|
+
private def refuse_key_on_rows(verb, names)
|
|
1344
|
+
named = names.map { |n| n.is_a?(String) ? n : nil }.compact & @key_names
|
|
1345
|
+
return if named.empty?
|
|
1346
|
+
raise ArgumentError,
|
|
1347
|
+
"#{verb}: #{named.first.inspect} is the group key, one value within each group"
|
|
722
1348
|
end
|
|
723
1349
|
|
|
724
1350
|
# One row per group, indexed by the labels; or, with +keep_rows+, the
|