carray 3.0.0 → 3.0.2
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/.yardopts +2 -1
- data/CHANGELOG.md +845 -3
- data/{NEWS.md → CHANGELOG.v1.md} +1 -1
- data/README.md +33 -33
- data/carray.gemspec +12 -8
- data/ext/ca_axis_dispatch.c +33 -4
- data/ext/ca_axis_group.c +256 -114
- data/ext/ca_binop_dispatch.c +5 -6
- data/ext/ca_binop_dispatch.h +0 -7
- data/ext/ca_categorical_iterator.c +121 -67
- data/ext/ca_for_each_element.h +6 -8
- data/ext/ca_group_iter.c +1 -1
- data/ext/ca_kernel_iterator.c +386 -74
- data/ext/ca_kernel_iterator.h +145 -45
- data/ext/ca_obj_array.c +62 -20
- data/ext/ca_obj_bincmp.c +57 -27
- data/ext/ca_obj_binop.c +86 -30
- data/ext/ca_obj_block.c +4 -4
- data/ext/ca_obj_const_string.c +88 -27
- data/ext/ca_obj_face.c +104 -0
- data/ext/ca_obj_face.h +48 -1
- data/ext/ca_obj_fake.c +11 -0
- data/ext/ca_obj_fixlen_string.c +21 -6
- data/ext/ca_obj_grid.c +11 -3
- data/ext/ca_obj_meld.c +125 -22
- data/ext/ca_obj_moncmp.c +33 -14
- data/ext/ca_obj_monop.c +19 -7
- data/ext/ca_obj_object.c +107 -4
- data/ext/ca_obj_record.c +3 -1
- data/ext/ca_obj_refer.c +30 -6
- data/ext/ca_obj_roll.c +13 -5
- data/ext/ca_obj_select.c +49 -34
- data/ext/ca_obj_select_axis.c +16 -6
- data/ext/ca_obj_shift.c +3 -3
- data/ext/ca_obj_stack.c +3 -8
- data/ext/ca_obj_stride.c +178 -24
- data/ext/ca_obj_string.c +11 -5
- data/ext/ca_obj_tile.c +13 -5
- data/ext/ca_obj_time.c +3 -1
- data/ext/ca_obj_timedelta.c +3 -1
- data/ext/ca_obj_transpose.c +1 -1
- data/ext/ca_obj_triop.c +62 -23
- data/ext/ca_obj_window.c +94 -19
- data/ext/ca_op_cmplx64.h +123 -0
- data/ext/ca_op_ipower.c +1 -5
- data/ext/ca_rng_normal.h +42 -0
- data/ext/ca_rng_xoshiro256pp.h +105 -0
- data/ext/ca_sort_kernels.h +5 -5
- data/ext/ca_sweep_engine.c +362 -155
- data/ext/ca_sweep_engine.h +39 -9
- data/ext/ca_transform_common.c +7 -0
- data/ext/carray.h +148 -132
- data/ext/carray_access.c +83 -49
- data/ext/carray_address_basis.c +590 -0
- data/ext/carray_bincount.c +8 -8
- data/ext/carray_broadcast.c +102 -9
- data/ext/carray_build_flags.h +3 -0
- data/ext/carray_call_cfunc.c +2010 -335
- data/ext/carray_call_cfunc.h +153 -0
- data/ext/carray_cast.c +184 -72
- data/ext/carray_conversion.c +22 -22
- data/ext/carray_copy.c +67 -32
- data/ext/carray_core.c +140 -13
- data/ext/carray_count.c +10 -11
- data/ext/carray_element.c +7 -7
- data/ext/carray_factorize.c +72 -51
- data/ext/carray_hold.c +1 -1
- data/ext/carray_index_classifier.c +7 -21
- data/ext/carray_internal.h +36 -3
- data/ext/carray_kernels_bincmp.c +1 -0
- data/ext/carray_kernels_binop.c +320 -298
- data/ext/carray_kernels_init.c +1095 -0
- data/ext/carray_kernels_map.c +1 -0
- data/ext/carray_kernels_moncmp.c +1 -0
- data/ext/carray_kernels_monop.c +412 -411
- data/ext/carray_kernels_reduce_aggregate.c +256 -87
- data/ext/carray_kernels_reduce_boolean.c +1 -0
- data/ext/carray_kernels_reduce_cumulative.c +324 -54
- data/ext/carray_kernels_reduce_extreme.c +605 -58
- data/ext/carray_kernels_reduce_variance.c +1 -0
- data/ext/carray_kernels_scan.c +5 -4
- data/ext/carray_kernels_search.c +187 -106
- data/ext/carray_kernels_sort.c +1 -0
- data/ext/carray_kernels_triop.c +1 -0
- data/ext/carray_lazy.c +175 -5
- data/ext/carray_loop.c +7 -1
- data/ext/carray_mask.c +29 -13
- data/ext/carray_median_percentile.c +56 -1
- data/ext/carray_memory_view.c +46 -21
- data/ext/carray_operator.c +50 -53
- data/ext/carray_order.c +1 -1
- data/ext/carray_partition.c +4 -4
- data/ext/carray_random.c +391 -47
- data/ext/carray_scatter.c +1 -1
- data/ext/carray_slab.c +13 -0
- data/ext/carray_sort.c +22 -24
- data/ext/carray_sort_kernel.c +10 -10
- data/ext/carray_test.c +1 -1
- data/ext/extconf.rb +18 -0
- data/ext/mk_call_cfunc.rb +323 -145
- data/ext/mkkernel.rb +651 -129
- data/ext/ruby_carray.c +28 -17
- data/ext/version.h +4 -4
- data/lib/carray/autoload_carray.rb +24 -3
- data/lib/carray/autoload_method_extension.rb +14 -1
- data/lib/carray/axis_group.rb +84 -7
- data/lib/carray/basics.rb +63 -59
- data/lib/carray/bincount_nd.rb +12 -12
- data/lib/carray/block_iterator.rb +111 -25
- data/lib/carray/categorical.rb +154 -37
- data/lib/carray/categorical_iterator.rb +300 -151
- data/lib/carray/conditional.rb +14 -14
- data/lib/carray/const_string.rb +131 -27
- data/lib/carray/construct.rb +61 -0
- data/lib/carray/core_extensions.rb +62 -5
- data/lib/carray/data_type_extension.rb +21 -1
- data/lib/carray/data_type_limits.rb +91 -0
- data/lib/carray/fixlen_string.rb +1 -1
- data/lib/carray/frame/concat.rb +3 -3
- data/lib/carray/frame/convert.rb +1 -1
- data/lib/carray/frame/csv_parser.rb +11 -4
- data/lib/carray/frame/frame.rb +85 -21
- data/lib/carray/frame/group.rb +36 -3
- data/lib/carray/frame/io.rb +224 -15
- data/lib/carray/frame/records.rb +18 -4
- data/lib/carray/frame/sort.rb +1 -1
- data/lib/carray/frame/verbs.rb +32 -12
- data/lib/carray/fuse_source.rb +123 -0
- data/lib/carray/fusion.rb +218 -0
- data/lib/carray/histogram.rb +16 -16
- data/lib/carray/inspect.rb +43 -16
- data/lib/carray/iterator.rb +147 -3
- data/lib/carray/lazy.rb +88 -73
- data/lib/carray/mask_gap_fill.rb +3 -1
- data/lib/carray/meld_reduce.rb +2 -2
- data/lib/carray/methods/align_addr.rb +1 -1
- data/lib/carray/methods/composition.rb +1 -1
- data/lib/carray/methods/discovery_along.rb +74 -0
- data/lib/carray/methods/factorize.rb +50 -0
- data/lib/carray/methods/is_in.rb +25 -14
- data/lib/carray/methods/locate_addr.rb +81 -3
- data/lib/carray/methods/mask_duplicates.rb +36 -2
- data/lib/carray/methods/meshgrid.rb +4 -5
- data/lib/carray/methods/mode.rb +2 -2
- data/lib/carray/methods/nunique.rb +23 -2
- data/lib/carray/methods/repeat.rb +110 -0
- data/lib/carray/methods/snap.rb +7 -2
- data/lib/carray/methods/unique.rb +44 -4
- data/lib/carray/methods/value_counts.rb +2 -2
- data/lib/carray/rng.rb +86 -0
- data/lib/carray/runtime.rb +0 -19
- data/lib/carray/slab_iterator.rb +77 -19
- data/lib/carray/string_operation_extension.rb +10 -6
- data/lib/carray/time.rb +906 -491
- data/lib/carray/window_iterator.rb +421 -27
- data/lib/carray.rb +7 -5
- data/yard-stubs/ca_obj_array.rb +385 -0
- data/yard-stubs/ca_obj_bitarray.rb +38 -0
- data/yard-stubs/ca_obj_bitfield.rb +43 -0
- data/yard-stubs/ca_obj_block.rb +68 -0
- data/yard-stubs/ca_obj_byte_swap.rb +56 -0
- data/yard-stubs/ca_obj_fake.rb +31 -0
- data/yard-stubs/ca_obj_farray.rb +32 -0
- data/yard-stubs/ca_obj_field.rb +45 -0
- data/yard-stubs/ca_obj_grid.rb +35 -0
- data/yard-stubs/ca_obj_refer.rb +72 -0
- data/yard-stubs/ca_obj_roll.rb +45 -0
- data/yard-stubs/ca_obj_shift.rb +43 -0
- data/yard-stubs/ca_obj_stride.rb +181 -0
- data/yard-stubs/ca_obj_tile.rb +29 -0
- data/yard-stubs/ca_obj_transpose.rb +40 -0
- data/yard-stubs/ca_obj_window.rb +57 -0
- data/yard-stubs/carray_access.rb +131 -0
- data/yard-stubs/carray_attribute.rb +246 -0
- data/yard-stubs/carray_broadcast.rb +37 -0
- data/yard-stubs/carray_cast.rb +489 -0
- data/yard-stubs/carray_class.rb +65 -0
- data/yard-stubs/carray_conversion.rb +76 -0
- data/yard-stubs/carray_copy.rb +79 -0
- data/yard-stubs/carray_core.rb +34 -0
- data/yard-stubs/carray_count.rb +84 -0
- data/yard-stubs/carray_element.rb +108 -0
- data/yard-stubs/carray_generate.rb +66 -0
- data/yard-stubs/carray_lazy.rb +228 -0
- data/yard-stubs/carray_loop.rb +140 -0
- data/yard-stubs/carray_mask.rb +259 -0
- data/yard-stubs/carray_math.rb +1615 -0
- data/yard-stubs/carray_mathfunc.rb +45 -0
- data/yard-stubs/carray_median_percentile.rb +103 -0
- data/yard-stubs/carray_memory_view.rb +163 -0
- data/yard-stubs/carray_order.rb +252 -0
- data/yard-stubs/carray_random.rb +89 -0
- data/yard-stubs/carray_scatter.rb +106 -0
- data/yard-stubs/carray_slab.rb +59 -0
- data/yard-stubs/carray_sort.rb +165 -0
- data/yard-stubs/carray_test.rb +85 -0
- data/yard-stubs/carray_undef.rb +64 -0
- data/yard-stubs/carray_utils.rb +97 -0
- data/yard-stubs/ruby_carray.rb +193 -0
- metadata +67 -10
- data/ext/ca_obj_unbound_repeat.c +0 -496
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# What each numeric data type can hold, as constants on the typed
|
|
2
|
+
# classes.
|
|
3
|
+
#
|
|
4
|
+
# CArray::Int32::MAX # => 2147483647
|
|
5
|
+
# CArray::Float32::EPSILON # => 1.1920928955078125e-07
|
|
6
|
+
#
|
|
7
|
+
# The names follow NumPy's iinfo / finfo rather than Ruby's Float, so
|
|
8
|
+
# that MIN and MAX bracket the range for every numeric type alike:
|
|
9
|
+
#
|
|
10
|
+
# CArray::Float64::MIN # => -1.7976931348623157e+308
|
|
11
|
+
# Float::MIN # => 2.2250738585072014e-308
|
|
12
|
+
#
|
|
13
|
+
# Those are not the same number and not the same question. Ruby's
|
|
14
|
+
# Float::MIN is the smallest positive normal, which is TINY here; MIN
|
|
15
|
+
# is the bottom of the range, which for an integer type is the only
|
|
16
|
+
# thing MIN could mean. Code that has to bracket a type without
|
|
17
|
+
# knowing whether it is integer or float reads MIN and MAX and is
|
|
18
|
+
# right either way, and that is the reason for the choice.
|
|
19
|
+
#
|
|
20
|
+
# Not every data type has limits. boolean, fixlen and object have no
|
|
21
|
+
# numeric range, so they carry none of these constants -- asking gives
|
|
22
|
+
# a NameError rather than an answer that would have to be invented.
|
|
23
|
+
|
|
24
|
+
class CArray
|
|
25
|
+
|
|
26
|
+
# The integer widths come from the extension rather than from a table
|
|
27
|
+
# written here, so a platform where a type is not the usual width is
|
|
28
|
+
# described correctly instead of confidently mis-described.
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
Int8 => true, Int16 => true, Int32 => true, Int64 => true,
|
|
32
|
+
UInt8 => false, UInt16 => false, UInt32 => false, UInt64 => false,
|
|
33
|
+
}.each do |klass, signed|
|
|
34
|
+
bits = 8 * CArray.sizeof(klass::TypeSymbol)
|
|
35
|
+
if signed
|
|
36
|
+
klass.const_set(:MIN, -(2 ** (bits - 1)))
|
|
37
|
+
klass.const_set(:MAX, 2 ** (bits - 1) - 1)
|
|
38
|
+
else
|
|
39
|
+
klass.const_set(:MIN, 0)
|
|
40
|
+
klass.const_set(:MAX, 2 ** bits - 1)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# float32 and float64 are IEEE-754 binary32 and binary64, and the four
|
|
45
|
+
# values below follow from the format: with `p` significand bits (the
|
|
46
|
+
# implicit one included) and `emax` the exponent of the largest finite
|
|
47
|
+
# value,
|
|
48
|
+
#
|
|
49
|
+
# EPSILON = 2 ** (1 - p) the step from 1.0 to the next float
|
|
50
|
+
# MAX = (2 - EPSILON) * 2 ** emax
|
|
51
|
+
# MIN = -MAX every format here is symmetric
|
|
52
|
+
# TINY = 2 ** (1 - emax) the smallest positive normal
|
|
53
|
+
#
|
|
54
|
+
# All four are exactly representable as a Ruby Float, binary32
|
|
55
|
+
# included, so nothing is rounded on the way in. The width is
|
|
56
|
+
# checked rather than assumed: on a platform where C float or double
|
|
57
|
+
# is not one of these formats the arithmetic below would be wrong,
|
|
58
|
+
# and a wrong limit is worse than a missing one.
|
|
59
|
+
|
|
60
|
+
{
|
|
61
|
+
Float32 => [4, 24, 127],
|
|
62
|
+
Float64 => [8, 53, 1023],
|
|
63
|
+
}.each do |klass, (bytes, precision, max_exponent)|
|
|
64
|
+
actual = CArray.sizeof(klass::TypeSymbol)
|
|
65
|
+
unless actual == bytes
|
|
66
|
+
raise "#{klass} is #{actual} bytes wide, not the #{bytes * 8}-bit IEEE-754 " \
|
|
67
|
+
"format its limits are derived from"
|
|
68
|
+
end
|
|
69
|
+
epsilon = 2.0 ** (1 - precision)
|
|
70
|
+
max = (2.0 - epsilon) * 2.0 ** max_exponent
|
|
71
|
+
klass.const_set(:EPSILON, epsilon)
|
|
72
|
+
klass.const_set(:MAX, max)
|
|
73
|
+
klass.const_set(:MIN, -max)
|
|
74
|
+
klass.const_set(:TINY, 2.0 ** (1 - max_exponent))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A complex type is a pair of floats, so its limits are its
|
|
78
|
+
# component's -- MIN and MAX bound the real and the imaginary part
|
|
79
|
+
# separately, not any magnitude of the pair. This is what
|
|
80
|
+
# np.finfo(np.complex64) answers too.
|
|
81
|
+
|
|
82
|
+
{
|
|
83
|
+
Complex64 => Float32,
|
|
84
|
+
Complex128 => Float64,
|
|
85
|
+
}.each do |klass, component|
|
|
86
|
+
[:MIN, :MAX, :TINY, :EPSILON].each do |name|
|
|
87
|
+
klass.const_set(name, component.const_get(name))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
end
|
data/lib/carray/fixlen_string.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# CAFixlenString high-level construction surface.
|
|
4
4
|
#
|
|
5
5
|
# CAFixlenString itself (the string interpretation of CA_FIXLEN storage) lives
|
|
6
|
-
# in ext/
|
|
6
|
+
# in ext/ca_obj_fixlen_string.c. This file provides the ergonomic builder that packs
|
|
7
7
|
# Ruby Strings into a fixed-width CA_FIXLEN entity and wraps it via
|
|
8
8
|
# CAFixlenString.wrap.
|
|
9
9
|
#
|
data/lib/carray/frame/concat.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Two entry points, mirroring the CArray-level taxonomy:
|
|
4
4
|
#
|
|
5
|
-
# +CAFrame.meld+ view frame, strict same
|
|
5
|
+
# +CAFrame.meld+ view frame, strict same data type per column,
|
|
6
6
|
# each column is a CAMeld view over the inputs
|
|
7
7
|
# +CAFrame.concatenate+ eager frame, auto-casts per column,
|
|
8
8
|
# each column is a materialised entity
|
|
@@ -48,10 +48,10 @@ class CAFrame
|
|
|
48
48
|
|
|
49
49
|
# Concatenate frames along the row axis, eagerly. Each output column is
|
|
50
50
|
# +CArray.concatenate+ of that column across the input frames, so per-column
|
|
51
|
-
#
|
|
51
|
+
# data types auto-promote to a common type. The result is a fresh, independent
|
|
52
52
|
# frame — writes to it do not propagate back to the input frames.
|
|
53
53
|
#
|
|
54
|
-
# For a view frame that shares storage with the inputs (strict same
|
|
54
|
+
# For a view frame that shares storage with the inputs (strict same data type
|
|
55
55
|
# per column, chain composability preserved) use {CAFrame.meld}.
|
|
56
56
|
#
|
|
57
57
|
# Column matching, index handling, and column-set / index-mix rules
|
data/lib/carray/frame/convert.rb
CHANGED
|
@@ -31,7 +31,7 @@ class CAFrame
|
|
|
31
31
|
#
|
|
32
32
|
# Only same-shape scalar (1-D) columns qualify. An N-D column has no single
|
|
33
33
|
# matrix form and raises — escape it per column with +df["name"]+. A mixed
|
|
34
|
-
#
|
|
34
|
+
# data type set is promoted to a common type (+result_type+, §12-F) through
|
|
35
35
|
# lazy cast lanes, so the promotion costs no buffer either.
|
|
36
36
|
#
|
|
37
37
|
# +writable: true+ demands a result whose writes reach this frame's own
|
|
@@ -91,13 +91,19 @@ class CAFrame
|
|
|
91
91
|
@recno = 0
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
-
# Fields of the next
|
|
95
|
-
|
|
94
|
+
# Fields of the next record, or nil at EOF. A blank line carries no
|
|
95
|
+
# separator, so it cannot be a row of a file with more than one column
|
|
96
|
+
# and is skipped as noise between records. In a single-column file it
|
|
97
|
+
# is the only spelling a missing single field has -- which is what
|
|
98
|
+
# to_csv writes for a masked cell -- so the caller passes
|
|
99
|
+
# +blank_is_row: true+ once the column count is known to be one, and
|
|
100
|
+
# the empty record becomes a row of no fields for build_frame to pad.
|
|
101
|
+
def read(io, blank_is_row: false)
|
|
96
102
|
loop do
|
|
97
103
|
rec = CSVParser.read_record(io, @quote)
|
|
98
104
|
return nil if rec.nil?
|
|
99
105
|
@recno += 1
|
|
100
|
-
next if rec.empty?
|
|
106
|
+
next if rec.empty? && !blank_is_row
|
|
101
107
|
return rec.count(@quote).zero? ? simple(rec) : scan(rec)
|
|
102
108
|
end
|
|
103
109
|
end
|
|
@@ -195,7 +201,8 @@ class CAFrame
|
|
|
195
201
|
# Consume the remaining records as data rows.
|
|
196
202
|
def body
|
|
197
203
|
rows = []
|
|
198
|
-
|
|
204
|
+
blank_is_row = @names && @names.size == 1
|
|
205
|
+
while (fields = @tok.read(@io, blank_is_row: blank_is_row))
|
|
199
206
|
rows << fields
|
|
200
207
|
end
|
|
201
208
|
@rows = rows
|
data/lib/carray/frame/frame.rb
CHANGED
|
@@ -45,14 +45,21 @@ class CAFrame
|
|
|
45
45
|
@columns = {}
|
|
46
46
|
@axis_name = axis_name || DEFAULT_AXIS_NAME
|
|
47
47
|
@index = nil
|
|
48
|
+
# The row axis name this frame had before set_index promoted a column over
|
|
49
|
+
# it, so reset_index can put it back. A frame built with an index never
|
|
50
|
+
# had one, and neither does a frame derived from an indexed one: both are
|
|
51
|
+
# born indexed, so there is nothing to restore and the default stands.
|
|
52
|
+
@axis_name_before_index = nil
|
|
48
53
|
|
|
49
54
|
n = nil
|
|
55
|
+
n_from = nil
|
|
50
56
|
columns.each do |name, col|
|
|
51
57
|
key = name.to_s
|
|
52
58
|
ca = coerce_column(col)
|
|
53
59
|
len = ca.shape[0]
|
|
54
60
|
if n.nil?
|
|
55
|
-
n
|
|
61
|
+
n = len
|
|
62
|
+
n_from = key
|
|
56
63
|
elsif len != n
|
|
57
64
|
raise ArgumentError,
|
|
58
65
|
"column #{key.inspect} has axis-0 length #{len}, expected #{n}"
|
|
@@ -67,8 +74,12 @@ class CAFrame
|
|
|
67
74
|
raise ArgumentError, "index must be a 1-D column (got ndim #{idx.ndim})"
|
|
68
75
|
end
|
|
69
76
|
if n && idx.shape[0] != n
|
|
77
|
+
# Name the column as well as the index: when the index is the frame's
|
|
78
|
+
# existing one and a column is the new arrival, blaming the index alone
|
|
79
|
+
# points at the side the caller cannot change.
|
|
70
80
|
raise ArgumentError,
|
|
71
|
-
"
|
|
81
|
+
"column #{n_from.inspect} has axis-0 length #{n}, " \
|
|
82
|
+
"but the index has length #{idx.shape[0]}"
|
|
72
83
|
end
|
|
73
84
|
@index = idx
|
|
74
85
|
@nrow = idx.shape[0] if n.nil?
|
|
@@ -234,9 +245,10 @@ class CAFrame
|
|
|
234
245
|
end
|
|
235
246
|
end
|
|
236
247
|
|
|
237
|
-
# Bind +key+ to +column+, adding the name when it is new. This is
|
|
238
|
-
#
|
|
239
|
-
#
|
|
248
|
+
# Bind +key+ to +column+, adding the name when it is new. This is where a
|
|
249
|
+
# column arriving from outside the frame has its axis-0 length checked
|
|
250
|
+
# (§12-A); the conversions in verbs.rb rebind +@columns+ directly and rely on
|
|
251
|
+
# being length-preserving instead. It is a replacement rather than an edit,
|
|
240
252
|
# which is what sets it apart from the rest: +fill+ / +mask_eq+ /
|
|
241
253
|
# +df[rows] = UNDEF+ write to the shared column and are therefore visible
|
|
242
254
|
# wherever it is held, while this binds the name to a different column and
|
|
@@ -307,6 +319,12 @@ class CAFrame
|
|
|
307
319
|
# value kept, so the row stays identifiable), leaving a later, better-informed
|
|
308
320
|
# pass to re-judge them. Definitely-true rows carry their values through
|
|
309
321
|
# unchanged in both modes.
|
|
322
|
+
#
|
|
323
|
+
# The default is a view-frame. +keep_masked: true+ returns a **materialized**
|
|
324
|
+
# frame instead -- carrying the UNDEF forward means writing it into the
|
|
325
|
+
# result, which a view cannot do without masking the parent's rows. That holds
|
|
326
|
+
# whether or not the selector actually carries a masked cell, so the same call
|
|
327
|
+
# site does not switch between sharing and copying with the data.
|
|
310
328
|
def filter(keep_masked: false)
|
|
311
329
|
mask = yield(self)
|
|
312
330
|
unless mask.is_a?(CArray) && mask.data_type == :boolean
|
|
@@ -330,7 +348,18 @@ class CAFrame
|
|
|
330
348
|
unless idx.ndim == 1
|
|
331
349
|
raise ArgumentError, "index must be a 1-D column (got ndim #{idx.ndim})"
|
|
332
350
|
end
|
|
351
|
+
# Only the name held before the frame had an index: a second set_index must
|
|
352
|
+
# not overwrite it with the first index's name.
|
|
353
|
+
@axis_name_before_index = @axis_name if @index.nil?
|
|
333
354
|
@columns.delete(key)
|
|
355
|
+
if @index
|
|
356
|
+
# Re-indexing replaces the index, so the one being replaced goes back to
|
|
357
|
+
# being a column -- the same demotion reset_index performs, in the same
|
|
358
|
+
# position, which makes set_index over an existing index equivalent to
|
|
359
|
+
# reset_index followed by set_index. Dropping it would lose the column it
|
|
360
|
+
# was made from.
|
|
361
|
+
@columns = { @axis_name => @index }.merge(@columns)
|
|
362
|
+
end
|
|
334
363
|
@index = idx
|
|
335
364
|
@axis_name = key
|
|
336
365
|
@nrow = idx.shape[0]
|
|
@@ -344,7 +373,8 @@ class CAFrame
|
|
|
344
373
|
return self unless @index
|
|
345
374
|
@columns = { @axis_name => @index }.merge(@columns)
|
|
346
375
|
@index = nil
|
|
347
|
-
@axis_name = DEFAULT_AXIS_NAME
|
|
376
|
+
@axis_name = @axis_name_before_index || DEFAULT_AXIS_NAME
|
|
377
|
+
@axis_name_before_index = nil
|
|
348
378
|
self
|
|
349
379
|
end
|
|
350
380
|
|
|
@@ -370,6 +400,17 @@ class CAFrame
|
|
|
370
400
|
# multi-row, frame-returning path). Positional access is +df[i]+.
|
|
371
401
|
def at(label)
|
|
372
402
|
raise ArgumentError, "at requires an index (set one with set_index)" unless @index
|
|
403
|
+
# An index may hold a masked cell -- an outer join and align both build one
|
|
404
|
+
# -- but an undefined label identifies no row, and two undefined labels are
|
|
405
|
+
# not the same label. The addressing primitives already say this (a masked
|
|
406
|
+
# key matches nothing, not another masked key), so at says it too rather
|
|
407
|
+
# than answering through eq(UNDEF), which asks about the mask and not about
|
|
408
|
+
# the label at all.
|
|
409
|
+
if UNDEF.equal?(label)
|
|
410
|
+
raise ArgumentError,
|
|
411
|
+
"at(UNDEF): an undefined label identifies no row; " \
|
|
412
|
+
"use filter { |f| f.index.is_masked } for the rows with no label"
|
|
413
|
+
end
|
|
373
414
|
pos = @index.eq(label).where
|
|
374
415
|
case pos.elements
|
|
375
416
|
when 0
|
|
@@ -436,13 +477,6 @@ class CAFrame
|
|
|
436
477
|
h
|
|
437
478
|
end
|
|
438
479
|
|
|
439
|
-
# @return [String]
|
|
440
|
-
def inspect
|
|
441
|
-
parts = @columns.map { |k, v| "#{k}:#{v.data_type}#{v.ndim > 1 ? v.shape[1..].inspect : ''}" }
|
|
442
|
-
idx = @index ? " index=#{@axis_name.inspect}" : ""
|
|
443
|
-
"#<CAFrame nrow=#{@nrow} vars=[#{parts.join(', ')}]#{idx}>"
|
|
444
|
-
end
|
|
445
|
-
|
|
446
480
|
private def rebuild(cols)
|
|
447
481
|
CAFrame.new(cols, axis_name: @axis_name, index: @index)
|
|
448
482
|
end
|
|
@@ -490,6 +524,19 @@ class CAFrame
|
|
|
490
524
|
# and index are untouched; the selected cells of every column go to UNDEF.
|
|
491
525
|
# The selector is forwarded to the column indexer, which classifies it.
|
|
492
526
|
private def mask_rows(selector)
|
|
527
|
+
# Decide before changing anything. A read-only column (a categorical, whose
|
|
528
|
+
# codes are read-only) refuses the write, and refusing half way through
|
|
529
|
+
# would leave the frame masked in whichever columns happened to come first.
|
|
530
|
+
refusing = @columns.find { |_, col| col.read_only? }
|
|
531
|
+
if refusing
|
|
532
|
+
raise "can not modify read-only array: column #{refusing[0].inspect} " \
|
|
533
|
+
"refuses the write, so no column was masked"
|
|
534
|
+
end
|
|
535
|
+
# The selector is classified by the column indexer, which is also what
|
|
536
|
+
# bound-checks it -- so with no column to forward it to, nothing would.
|
|
537
|
+
# Build the row mask purely to have the frame's own row axis refuse an
|
|
538
|
+
# out-of-range row, as every other row form does.
|
|
539
|
+
selected_row_mask(selector) if @columns.empty?
|
|
493
540
|
@columns.each_value do |col|
|
|
494
541
|
col[selector, *([nil] * (col.ndim - 1))] = UNDEF
|
|
495
542
|
end
|
|
@@ -511,7 +558,7 @@ class CAFrame
|
|
|
511
558
|
end
|
|
512
559
|
|
|
513
560
|
# df[sel] = other : replace the selected contiguous span with other's rows
|
|
514
|
-
# (any length). Columns are concatenated head + other + tail, so
|
|
561
|
+
# (any length). Columns are concatenated head + other + tail, so data types
|
|
515
562
|
# promote and the row count shifts by other.nrow - span.
|
|
516
563
|
private def splice_rows(selector, other)
|
|
517
564
|
lo, hi = contiguous_span(selector)
|
|
@@ -536,9 +583,9 @@ class CAFrame
|
|
|
536
583
|
case pieces.size
|
|
537
584
|
when 0 then col[CArray.int32(0), *tail] # replaced every row with none
|
|
538
585
|
when 1 then pieces.first
|
|
539
|
-
else CArray.meld(pieces, axis: 0) # CAMeld view;
|
|
586
|
+
else CArray.meld(pieces, axis: 0) # CAMeld view; data type mismatch
|
|
540
587
|
end # across pieces raises.
|
|
541
|
-
# For
|
|
588
|
+
# For data type conversion, cast
|
|
542
589
|
# the incoming +other+'s
|
|
543
590
|
# column beforehand — silent
|
|
544
591
|
# promotion in a splice would
|
|
@@ -547,7 +594,16 @@ class CAFrame
|
|
|
547
594
|
new_index = splice_index(other, lo, hi)
|
|
548
595
|
@columns = new_cols
|
|
549
596
|
@index = new_index
|
|
550
|
-
|
|
597
|
+
# Read the new count off something the frame now holds, rather than
|
|
598
|
+
# computing it. For every column the pieces welded above are head (lo) +
|
|
599
|
+
# other's rows + tail (nrow - hi), so the arithmetic and the objects agree
|
|
600
|
+
# -- except when there is nothing to weld: a frame with no columns and no
|
|
601
|
+
# index would otherwise come away claiming rows that nothing backs.
|
|
602
|
+
@nrow = if (witness = new_cols.each_value.first || new_index)
|
|
603
|
+
witness.shape[0]
|
|
604
|
+
else
|
|
605
|
+
0
|
|
606
|
+
end
|
|
551
607
|
self
|
|
552
608
|
end
|
|
553
609
|
|
|
@@ -566,7 +622,7 @@ class CAFrame
|
|
|
566
622
|
case pieces.size
|
|
567
623
|
when 0 then @index[CArray.int32(0)]
|
|
568
624
|
when 1 then pieces.first
|
|
569
|
-
else CArray.meld(pieces, axis: 0) # index
|
|
625
|
+
else CArray.meld(pieces, axis: 0) # index data type must match across frames
|
|
570
626
|
end
|
|
571
627
|
end
|
|
572
628
|
|
|
@@ -616,8 +672,11 @@ class CAFrame
|
|
|
616
672
|
end
|
|
617
673
|
|
|
618
674
|
private def select_rows(selector, keep_masked: false)
|
|
619
|
-
if keep_masked && selector.is_a?(CArray) &&
|
|
620
|
-
|
|
675
|
+
if keep_masked && selector.is_a?(CArray) && selector.data_type == :boolean
|
|
676
|
+
# Not gated on selector.has_mask?: gating there would make the same call
|
|
677
|
+
# site return a view-frame or a materialized one depending on whether that
|
|
678
|
+
# run's data happened to produce an undetermined cell (the reason splice
|
|
679
|
+
# always snapshots -- see CAFrame.md section 3).
|
|
621
680
|
return select_rows_keep_masked(selector)
|
|
622
681
|
end
|
|
623
682
|
cols = {}
|
|
@@ -638,7 +697,12 @@ class CAFrame
|
|
|
638
697
|
g[undet_kept, *tail] = UNDEF
|
|
639
698
|
cols[name] = g
|
|
640
699
|
end
|
|
641
|
-
|
|
700
|
+
# The index is copied along with the columns. Its values are carried over
|
|
701
|
+
# unmasked (an undetermined row keeps its label, as mask_rows leaves the
|
|
702
|
+
# index alone), but a materialized frame has to be materialized whole: an
|
|
703
|
+
# aliased index would write through to the parent from a frame whose columns
|
|
704
|
+
# do not.
|
|
705
|
+
new_index = @index ? @index[keep].copy : nil
|
|
642
706
|
CAFrame.new(cols, axis_name: @axis_name, index: new_index)
|
|
643
707
|
end
|
|
644
708
|
|
data/lib/carray/frame/group.rb
CHANGED
|
@@ -13,7 +13,7 @@ class CAFrame
|
|
|
13
13
|
raise ArgumentError, "group_by needs at least one key" if keys.empty?
|
|
14
14
|
cat = grouping_categorical(keys)
|
|
15
15
|
axis = keys.size == 1 && keys.first.is_a?(String) ? keys.first : "group"
|
|
16
|
-
GroupedFrame.new(self, cat, axis)
|
|
16
|
+
GroupedFrame.new(self, cat, axis, keys.grep(String))
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
# Number of rows currently selected — used by group per-group view-frames
|
|
@@ -28,7 +28,14 @@ class CAFrame
|
|
|
28
28
|
# values, categorized by content (the codes are composed from the
|
|
29
29
|
# per-column keys).
|
|
30
30
|
n = nrow
|
|
31
|
-
CArray.object(n) { |i| cols.map { |c| c[i] } }
|
|
31
|
+
key = CArray.object(n) { |i| cols.map { |c| c[i] } }
|
|
32
|
+
# One undetermined component makes the whole tuple undetermined, the same
|
|
33
|
+
# answer a single masked key cell gets. Left as a value, the UNDEF inside
|
|
34
|
+
# the tuple would intern as an ordinary distinct key and the row would
|
|
35
|
+
# form a group of its own.
|
|
36
|
+
undetermined = CArray.boolean(n) { |i| key[i].any? { |v| UNDEF.equal?(v) } }
|
|
37
|
+
key[undetermined] = UNDEF if undetermined.any
|
|
38
|
+
key.categorize
|
|
32
39
|
end
|
|
33
40
|
end
|
|
34
41
|
|
|
@@ -56,10 +63,14 @@ end
|
|
|
56
63
|
# aggregate -> declarative per-column reductions into a new frame
|
|
57
64
|
# table { |g| }-> cross-column Ruby escape, g is a per-group view-frame
|
|
58
65
|
class GroupedFrame
|
|
59
|
-
|
|
66
|
+
# +key_names+ are the frame columns the grouping was keyed on. They become
|
|
67
|
+
# the result's index, so the reduction shortcuts must not also return them as
|
|
68
|
+
# reduced columns; an external CArray key contributes no name.
|
|
69
|
+
def initialize(frame, cat, axis_name, key_names = [])
|
|
60
70
|
@frame = frame
|
|
61
71
|
@cat = cat
|
|
62
72
|
@axis_name = axis_name
|
|
73
|
+
@key_names = key_names
|
|
63
74
|
@labels = cat.labels # group values, in code order
|
|
64
75
|
end
|
|
65
76
|
|
|
@@ -124,6 +135,23 @@ class GroupedFrame
|
|
|
124
135
|
|
|
125
136
|
# Convenience reductions over every numeric scalar column (memo §6-4
|
|
126
137
|
# "grp.mean"). Non-numeric / N-D columns are skipped.
|
|
138
|
+
#
|
|
139
|
+
# @!method sum
|
|
140
|
+
# Returns a frame of the per-group sum of every numeric one-dimensional
|
|
141
|
+
# column. Non-numeric and multi-dimensional columns are left out.
|
|
142
|
+
# @return [CAFrame] one row per group, indexed by the group labels.
|
|
143
|
+
# @!method mean
|
|
144
|
+
# Returns a frame of the per-group arithmetic mean of every numeric
|
|
145
|
+
# one-dimensional column, as {#sum} does.
|
|
146
|
+
# @return [CAFrame] one row per group.
|
|
147
|
+
# @!method min
|
|
148
|
+
# Returns a frame of the per-group minimum of every numeric
|
|
149
|
+
# one-dimensional column, as {#sum} does.
|
|
150
|
+
# @return [CAFrame] one row per group.
|
|
151
|
+
# @!method max
|
|
152
|
+
# Returns a frame of the per-group maximum of every numeric
|
|
153
|
+
# one-dimensional column, as {#sum} does.
|
|
154
|
+
# @return [CAFrame] one row per group.
|
|
127
155
|
[:sum, :mean, :min, :max].each do |red|
|
|
128
156
|
define_method(red) { reduce_numeric(red) }
|
|
129
157
|
end
|
|
@@ -143,6 +171,11 @@ class GroupedFrame
|
|
|
143
171
|
private def reduce_numeric(reduction)
|
|
144
172
|
cols = {}
|
|
145
173
|
@frame.variable_names.each do |name|
|
|
174
|
+
# A key column is the index here, not a result column. Filtering it out
|
|
175
|
+
# by name rather than by data type is what makes a numeric key behave
|
|
176
|
+
# like a string one: NON_NUMERIC is about which columns a reduction can
|
|
177
|
+
# apply to, which happened to cover string keys and nothing else.
|
|
178
|
+
next if @key_names.include?(name)
|
|
146
179
|
col = @frame[name]
|
|
147
180
|
next unless col.ndim == 1 && !NON_NUMERIC.include?(col.data_type)
|
|
148
181
|
cols[name] = col.group_by_category(@cat).public_send(reduction)
|