carray 3.0.3 → 3.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +264 -0
- data/README.md +3 -2
- data/carray.gemspec +1 -1
- data/ext/ca_axis_descriptor.h +27 -24
- data/ext/ca_axis_dispatch.c +1 -6
- 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 +205 -134
- data/ext/ca_compare.c +7 -18
- data/ext/ca_group_iter.c +16 -17
- data/ext/ca_kernel_iterator.c +1234 -1994
- data/ext/ca_kernel_iterator.h +391 -437
- data/ext/ca_monop_dispatch.c +82 -59
- data/ext/ca_monop_dispatch.h +1 -0
- data/ext/ca_obj_array.c +96 -39
- data/ext/ca_obj_bincmp.c +24 -21
- data/ext/ca_obj_binop.c +15 -26
- data/ext/ca_obj_bitarray.c +3 -2
- data/ext/ca_obj_bitfield.c +5 -6
- data/ext/ca_obj_block.c +18 -9
- data/ext/ca_obj_byte_swap.c +5 -6
- data/ext/ca_obj_const_string.c +173 -19
- data/ext/ca_obj_face.c +238 -57
- 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 +17 -5
- data/ext/ca_obj_fixlen_string.c +2 -3
- data/ext/ca_obj_grid.c +406 -132
- data/ext/ca_obj_meld.c +172 -34
- data/ext/ca_obj_moncmp.c +23 -8
- data/ext/ca_obj_monop.c +67 -57
- data/ext/ca_obj_object.c +157 -51
- data/ext/ca_obj_record.c +12 -7
- data/ext/ca_obj_reduce.c +13 -8
- data/ext/ca_obj_refer.c +25 -27
- data/ext/ca_obj_remap.c +30 -9
- data/ext/ca_obj_repeat.c +9 -10
- data/ext/ca_obj_roll.c +6 -5
- data/ext/ca_obj_select.c +14 -9
- data/ext/ca_obj_select_axis.c +15 -47
- data/ext/ca_obj_shift.c +11 -1
- data/ext/ca_obj_stack.c +179 -34
- data/ext/ca_obj_stride.c +162 -115
- data/ext/ca_obj_string.c +71 -4
- data/ext/ca_obj_tile.c +6 -5
- data/ext/ca_obj_time.c +22 -6
- data/ext/ca_obj_timedelta.c +22 -6
- data/ext/ca_obj_transpose.c +5 -3
- data/ext/ca_obj_triop.c +23 -32
- data/ext/ca_obj_window.c +190 -26
- data/ext/ca_sweep_engine.c +2 -2
- data/ext/caframe_csv_reader.c +80 -1
- data/ext/carray.h +181 -75
- data/ext/carray_access.c +184 -96
- data/ext/carray_address_basis.c +343 -87
- data/ext/carray_attribute.c +89 -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 +142 -24
- data/ext/carray_cast_func.rb +46 -6
- data/ext/carray_class.c +4 -3
- data/ext/carray_config.h +28 -0
- data/ext/carray_conversion.c +16 -7
- data/ext/carray_copy.c +51 -14
- data/ext/carray_core.c +147 -55
- data/ext/carray_count.c +50 -20
- data/ext/carray_element.c +76 -33
- data/ext/carray_factorize.c +118 -64
- data/ext/carray_generate.c +191 -50
- data/ext/carray_histogram.c +62 -17
- data/ext/carray_hold.c +12 -15
- data/ext/carray_index_classifier.c +388 -176
- data/ext/carray_internal.h +336 -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 +10906 -0
- data/ext/carray_kernels_triop.c +1864 -0
- data/ext/carray_key_windows.c +216 -0
- data/ext/carray_lazy.c +170 -111
- data/ext/carray_mask.c +292 -72
- data/ext/carray_median_percentile.c +82 -28
- data/ext/carray_memory_view.c +669 -561
- data/ext/carray_operator.c +124 -47
- data/ext/carray_order.c +384 -76
- data/ext/carray_partition.c +28 -27
- data/ext/carray_random.c +67 -13
- data/ext/carray_round.c +273 -0
- data/ext/carray_scatter.c +63 -38
- data/ext/carray_segment.c +3 -3
- data/ext/carray_slab.c +91 -28
- data/ext/carray_sort.c +35 -35
- data/ext/carray_struct.c +19 -18
- data/ext/carray_test.c +26 -5
- data/ext/carray_undef.c +4 -0
- data/ext/carray_utils.c +246 -45
- data/ext/mk_call_cfunc.rb +19 -2
- data/ext/mkkernel.rb +198 -240
- data/ext/ruby_carray.c +10 -4
- data/ext/version.h +4 -4
- data/lib/carray/attribute.rb +126 -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 +78 -10
- 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 +165 -43
- data/lib/carray/categorical_iterator.rb +130 -27
- data/lib/carray/complex.rb +5 -5
- data/lib/carray/conditional.rb +67 -18
- data/lib/carray/const_string.rb +14 -5
- data/lib/carray/construct.rb +15 -11
- data/lib/carray/data_type_extension.rb +1 -0
- data/lib/carray/distance_limit.rb +77 -0
- data/lib/carray/fixlen_string.rb +7 -1
- data/lib/carray/frame/column_verbs.rb +70 -0
- data/lib/carray/frame/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 +246 -21
- data/lib/carray/frame/group.rb +510 -95
- data/lib/carray/frame/io.rb +115 -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 +11 -4
- 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 +182 -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 +53 -36
- 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 +313 -82
- data/lib/carray/serialize.rb +24 -10
- data/lib/carray/slab_iterator.rb +50 -0
- data/lib/carray/string.rb +105 -5
- data/lib/carray/string_operation_extension.rb +8 -1
- data/lib/carray/struct.rb +9 -21
- data/lib/carray/struct_builder.rb +19 -8
- data/lib/carray/time.rb +726 -104
- 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_window.rb +8 -3
- data/yard-stubs/carray_access.rb +6 -1
- data/yard-stubs/carray_attribute.rb +0 -5
- data/yard-stubs/carray_cast.rb +10 -0
- data/yard-stubs/carray_conversion.rb +2 -1
- data/yard-stubs/carray_copy.rb +4 -2
- data/yard-stubs/carray_count.rb +4 -4
- data/yard-stubs/carray_element.rb +10 -4
- data/yard-stubs/carray_generate.rb +21 -1
- data/yard-stubs/carray_mask.rb +26 -2
- data/yard-stubs/carray_math.rb +27 -1
- data/yard-stubs/carray_median_percentile.rb +5 -2
- data/yard-stubs/carray_memory_view.rb +40 -23
- data/yard-stubs/carray_order.rb +29 -18
- data/yard-stubs/carray_random.rb +13 -4
- data/yard-stubs/carray_scatter.rb +14 -9
- 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
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CArray
|
|
4
|
+
|
|
5
|
+
# The length a match by distance is limited by -- the +tolerance:+ of
|
|
6
|
+
# locate_nearest_addr (and of align_nearest_addr and CAFrame#join_asof
|
|
7
|
+
# through it), the +before:+, +after:+ and +interval:+ of a key window --
|
|
8
|
+
# read the same way wherever it is given: a length of the key's kind,
|
|
9
|
+
# not below 0.
|
|
10
|
+
#
|
|
11
|
+
# - For a number key it is a real number; NaN, which no distance is
|
|
12
|
+
# within or beyond, and a negative length are refused, as is a String.
|
|
13
|
+
# - For a time or duration key it is a duration: a String such as
|
|
14
|
+
# "10 minutes" (and "0 minutes") or a CATimedelta value, not negative.
|
|
15
|
+
# A bare number is refused, since its length would depend on the key's
|
|
16
|
+
# unit.
|
|
17
|
+
#
|
|
18
|
+
# Every refusal is ArgumentError naming +label+, the keyword as the
|
|
19
|
+
# caller says it ("tolerance:", "windows: before:").
|
|
20
|
+
module DistanceLimit
|
|
21
|
+
|
|
22
|
+
ZERO_DURATION = /\A\s*0+\s+([A-Za-z]+)\s*\z/
|
|
23
|
+
|
|
24
|
+
# +width+ as a Numeric (number key) or a CATimedelta::Element (time or
|
|
25
|
+
# duration key); +nil+ passes through.
|
|
26
|
+
def self.read (width, key, label)
|
|
27
|
+
return nil if width.nil?
|
|
28
|
+
if key.is_a?(CATime) || key.is_a?(CATimedelta)
|
|
29
|
+
duration(width, label)
|
|
30
|
+
else
|
|
31
|
+
number(width, key, label)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.number (width, key, label)
|
|
36
|
+
if width.is_a?(String) || width.is_a?(CATimedelta::Element)
|
|
37
|
+
raise ArgumentError,
|
|
38
|
+
"#{label} #{width.inspect} is a duration, but the key is " \
|
|
39
|
+
"#{CArray::Inspector.type_label(key)}; give a number"
|
|
40
|
+
end
|
|
41
|
+
unless width.is_a?(Numeric) && width.real? && !(width.is_a?(Float) && width.nan?) && width >= 0
|
|
42
|
+
raise ArgumentError, "#{label} is a number not below 0 (got #{width.inspect})"
|
|
43
|
+
end
|
|
44
|
+
width
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.duration (width, label)
|
|
48
|
+
element =
|
|
49
|
+
case width
|
|
50
|
+
when CATimedelta::Element
|
|
51
|
+
width
|
|
52
|
+
when String
|
|
53
|
+
if (zero = width.match(ZERO_DURATION))
|
|
54
|
+
CATimedelta::Element.new(0, CATime::Resolution.parse(zero[1]).base)
|
|
55
|
+
else
|
|
56
|
+
res = begin
|
|
57
|
+
CATime::Resolution.parse(width)
|
|
58
|
+
rescue ArgumentError
|
|
59
|
+
raise ArgumentError,
|
|
60
|
+
"#{label} #{width.inspect} is not a duration " \
|
|
61
|
+
"(use e.g. \"10 minutes\" / \"1 hour\")"
|
|
62
|
+
end
|
|
63
|
+
CATimedelta::Element.new(res.count, res.base)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
unless element
|
|
67
|
+
raise ArgumentError,
|
|
68
|
+
"#{label} a time key needs a duration such as \"10 minutes\" " \
|
|
69
|
+
"or a CATimedelta value (got #{width.inspect})"
|
|
70
|
+
end
|
|
71
|
+
if element.value < 0
|
|
72
|
+
raise ArgumentError, "#{label} is a duration not below 0 (got #{width.inspect})"
|
|
73
|
+
end
|
|
74
|
+
element
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/carray/fixlen_string.rb
CHANGED
|
@@ -46,7 +46,7 @@ class CArray
|
|
|
46
46
|
# @return [CAFixlenString]
|
|
47
47
|
def self.fixlen_string (arg, bytes: nil, truncate: :error, &block)
|
|
48
48
|
if arg.is_a?(CArray)
|
|
49
|
-
return string_face_of(arg).to_fixlen_string(bytes: bytes, truncate: truncate)
|
|
49
|
+
return string_face_of(arg, "CArray.fixlen_string").to_fixlen_string(bytes: bytes, truncate: truncate)
|
|
50
50
|
end
|
|
51
51
|
unless [:error, :silent].include?(truncate)
|
|
52
52
|
raise ArgumentError, "truncate: must be :error or :silent (got #{truncate.inspect})"
|
|
@@ -90,6 +90,12 @@ end
|
|
|
90
90
|
# Mutable fixed-width String Face: L1 ops + in-place variants. In-place
|
|
91
91
|
# transforms re-store through the native fixlen path (silent truncate).
|
|
92
92
|
class CAFixlenString
|
|
93
|
+
# inspect's name for the array: the class and the cell width, which is the
|
|
94
|
+
# longest string a cell holds.
|
|
95
|
+
private def inspect_label
|
|
96
|
+
"#{self.class}[#{bytes}]"
|
|
97
|
+
end
|
|
98
|
+
|
|
93
99
|
include CArray::StringOperationMixin
|
|
94
100
|
include CArray::StringOperationMixin::Mutable
|
|
95
101
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CAFrame
|
|
4
|
+
|
|
5
|
+
# The one rule of a frame-wide verb over the frame's columns -- the group
|
|
6
|
+
# reductions and transforms, the windowed reductions -- kept in one place
|
|
7
|
+
# so that every verb answers in the same shape:
|
|
8
|
+
#
|
|
9
|
+
# - given names, the CArray (one) or the Array of them (several), as
|
|
10
|
+
# +df[...]+ returns columns; a named column whose type has no such
|
|
11
|
+
# operation is refused with CArray::DataTypeError, as the core refuses
|
|
12
|
+
# it, since it was asked for;
|
|
13
|
+
# - given none, every column whose type has it, the key columns left out,
|
|
14
|
+
# as a Hash of name to result that the caller makes a frame of.
|
|
15
|
+
#
|
|
16
|
+
# Whether a type has the operation is asked of the core on two copies of
|
|
17
|
+
# the column's first row (two blank cells of its kind when it has no
|
|
18
|
+
# rows), so the answer is about the type and not the data: a refusal
|
|
19
|
+
# there means the operation does not apply, while an error in the verb
|
|
20
|
+
# itself is left to raise. An N-D column (each row a profile) is asked,
|
|
21
|
+
# and computed, along the rows.
|
|
22
|
+
module ColumnVerbs
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def over_named_columns (verb, names, key_names, args = [], keywords = {})
|
|
27
|
+
if names.empty?
|
|
28
|
+
cols = {}
|
|
29
|
+
@frame.column_names.each do |name|
|
|
30
|
+
next if key_names.include?(name)
|
|
31
|
+
col = @frame[name]
|
|
32
|
+
next unless column_has?(col, verb, args, keywords)
|
|
33
|
+
cols[name] = yield(col)
|
|
34
|
+
end
|
|
35
|
+
return cols
|
|
36
|
+
end
|
|
37
|
+
results = names.map do |name|
|
|
38
|
+
col = @frame[@frame.__send__(:column_key, name, verb)]
|
|
39
|
+
unless column_has?(col, verb, args, keywords)
|
|
40
|
+
raise CArray::DataTypeError,
|
|
41
|
+
"#{verb}: column #{name.inspect} " \
|
|
42
|
+
"(#{CArray::Inspector.type_label(col)}) has no #{verb}"
|
|
43
|
+
end
|
|
44
|
+
yield(col)
|
|
45
|
+
end
|
|
46
|
+
results.size == 1 ? results.first : results
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# count with no value counts the present cells, which every type has;
|
|
50
|
+
# a Face's own count takes a value (CACategorical#count(label)), so the
|
|
51
|
+
# type is asked about count_not_masked instead.
|
|
52
|
+
def column_has? (col, verb, args, keywords)
|
|
53
|
+
verb = :count_not_masked if verb == :count && args.empty?
|
|
54
|
+
# Two cells: one cell does not reach what combines two (a String's
|
|
55
|
+
# median interpolates, its sum adds), and an empty column answers
|
|
56
|
+
# with the identity without asking the type at all.
|
|
57
|
+
tail = [nil] * (col.ndim - 1)
|
|
58
|
+
probe = if col.shape[0] > 0
|
|
59
|
+
col[CA_INT64([0, 0]), *tail]
|
|
60
|
+
else
|
|
61
|
+
col.resize(2, *col.shape[1..])
|
|
62
|
+
end
|
|
63
|
+
keywords = keywords.merge(axis: 0) if col.ndim > 1
|
|
64
|
+
probe.public_send(verb, *args, **keywords)
|
|
65
|
+
true
|
|
66
|
+
rescue CArray::DataTypeError, TypeError, ArgumentError
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/carray/frame/convert.rb
CHANGED
|
@@ -99,3 +99,72 @@ class CAFrame
|
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
101
|
end
|
|
102
|
+
|
|
103
|
+
class CArray
|
|
104
|
+
# @overload to_frame(*names, axis: -1)
|
|
105
|
+
# Shows this array as a table: a CAFrame whose rows run along axis 0 and
|
|
106
|
+
# whose columns are the positions along `axis`. Column `j` is the view of
|
|
107
|
+
# `self` with `j` at `axis` and every other axis whole, so its shape is
|
|
108
|
+
# `self`'s shape without `axis`. A 2-D array gives 1-D columns, an array
|
|
109
|
+
# of more dimensions gives N-D columns:
|
|
110
|
+
#
|
|
111
|
+
# ```
|
|
112
|
+
# a = CArray.float64(3, 2).seq!
|
|
113
|
+
# df = a.to_frame("x", "y")
|
|
114
|
+
# df["y"] # => a[nil, 1], shape [3]
|
|
115
|
+
#
|
|
116
|
+
# img = CArray.uint8(480, 640, 3)
|
|
117
|
+
# img.to_frame("r", "g", "b")["r"].shape # => [480, 640]
|
|
118
|
+
# ```
|
|
119
|
+
#
|
|
120
|
+
# Nothing is copied: each column is a view, so writes through the frame
|
|
121
|
+
# reach `self` and writes to `self` show in the frame, values and mask
|
|
122
|
+
# alike. Use `copy` on the frame for an independent one.
|
|
123
|
+
#
|
|
124
|
+
# Without names the columns are called `"c0"`, `"c1"`, ... A 1-D array
|
|
125
|
+
# has no axis to take columns from; give it one with `a[nil, :_]`.
|
|
126
|
+
#
|
|
127
|
+
# @param names [Array<String>] column names, one per position along `axis`.
|
|
128
|
+
# @param axis [Integer] the axis the columns are taken along; it cannot
|
|
129
|
+
# be axis 0, which holds the rows.
|
|
130
|
+
# @return [CAFrame]
|
|
131
|
+
# @raise [TypeError] when a name is not a String or `axis` is not an
|
|
132
|
+
# Integer.
|
|
133
|
+
# @raise [ArgumentError] when `axis` is out of range or is the row axis,
|
|
134
|
+
# when the number of names differs from the length of `axis`, or when
|
|
135
|
+
# a name repeats.
|
|
136
|
+
def to_frame (*names, axis: -1)
|
|
137
|
+
axis = -1 if axis.nil?
|
|
138
|
+
if ndim < 2
|
|
139
|
+
raise ArgumentError,
|
|
140
|
+
"to_frame: a #{ndim}-D array has no axis to take columns from " \
|
|
141
|
+
"besides the row axis (axis 0); give it one with a[nil, :_]"
|
|
142
|
+
end
|
|
143
|
+
ax = normalize_axis(axis, "to_frame")
|
|
144
|
+
if ax == 0
|
|
145
|
+
raise ArgumentError,
|
|
146
|
+
"to_frame: axis #{axis} is the row axis (axis 0); columns are " \
|
|
147
|
+
"taken along another axis"
|
|
148
|
+
end
|
|
149
|
+
ncol = shape[ax]
|
|
150
|
+
if names.empty?
|
|
151
|
+
names = Array.new(ncol) { |j| "c#{j}" }
|
|
152
|
+
else
|
|
153
|
+
names.each { |name| CAFrame.__send__(:column_key, name, "to_frame") }
|
|
154
|
+
raise ArgumentError, "to_frame: a column name can not be empty" if names.include?("")
|
|
155
|
+
unless names.size == ncol
|
|
156
|
+
raise ArgumentError,
|
|
157
|
+
"to_frame: #{names.size} names for #{ncol} columns " \
|
|
158
|
+
"(axis #{axis} has length #{ncol})"
|
|
159
|
+
end
|
|
160
|
+
CAFrame.__send__(:refuse_repeated_names, names, "to_frame")
|
|
161
|
+
end
|
|
162
|
+
index = Array.new(ndim, nil)
|
|
163
|
+
columns = {}
|
|
164
|
+
names.each_with_index do |name, j|
|
|
165
|
+
index[ax] = j
|
|
166
|
+
columns[name] = self[*index]
|
|
167
|
+
end
|
|
168
|
+
CAFrame.new(columns)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -354,11 +354,18 @@ class CAFrame
|
|
|
354
354
|
end
|
|
355
355
|
|
|
356
356
|
# Read one record. With no argument it becomes the column names; with a
|
|
357
|
-
# name it is a secondary header (e.g. units)
|
|
358
|
-
#
|
|
357
|
+
# name it is a secondary header (e.g. units): its field j becomes the
|
|
358
|
+
# attribute +name+ of column j (an empty field gives none). Returns the
|
|
359
|
+
# record's fields either way.
|
|
359
360
|
def header(name = nil)
|
|
360
|
-
|
|
361
|
+
# A named header of a file with at most one column may be an empty
|
|
362
|
+
# line: that is how to_csv writes a column without the attribute, so
|
|
363
|
+
# the line is the header's, as an empty line is a row in the data.
|
|
364
|
+
blank_is_row = !name.nil? && !@names.nil? && @names.size <= 1
|
|
365
|
+
fields = @tok.read(@io, blank_is_row: blank_is_row)
|
|
361
366
|
if fields.nil?
|
|
367
|
+
# A file with no columns has nothing to give a named header.
|
|
368
|
+
return [] if !name.nil? && @names&.empty?
|
|
362
369
|
# An input with no record at all is a table with no columns: that is
|
|
363
370
|
# what to_csv writes for a frame without columns or index. Once lines
|
|
364
371
|
# have been skipped to reach the header, a missing header is an error.
|
|
@@ -372,11 +379,18 @@ class CAFrame
|
|
|
372
379
|
lineno: @tok.lineno + 1)
|
|
373
380
|
end
|
|
374
381
|
if name.nil?
|
|
375
|
-
|
|
382
|
+
# An empty field names no column: the column takes the name a file
|
|
383
|
+
# without a header gives it, from its place in the line.
|
|
384
|
+
@names = fields.each_with_index.map { |f, j| f.to_s.empty? ? "c#{j}" : f.to_s }
|
|
376
385
|
@header_line = @tok.record_line
|
|
377
386
|
@names_to_check = nil
|
|
378
387
|
else
|
|
379
|
-
|
|
388
|
+
key = name.to_s
|
|
389
|
+
@named_headers ||= {}
|
|
390
|
+
if @named_headers.key?(key)
|
|
391
|
+
raise ArgumentError, "from_csv: header(#{name.inspect}) is read twice"
|
|
392
|
+
end
|
|
393
|
+
@named_headers[key] = [fields, @tok.record_line, name]
|
|
380
394
|
end
|
|
381
395
|
fields
|
|
382
396
|
end
|
|
@@ -388,6 +402,9 @@ class CAFrame
|
|
|
388
402
|
# one.
|
|
389
403
|
def column_names(*names)
|
|
390
404
|
names = names.flatten.map(&:to_s)
|
|
405
|
+
if names.include?("")
|
|
406
|
+
raise ArgumentError, "from_csv: column_names: a column name can not be empty"
|
|
407
|
+
end
|
|
391
408
|
if @header_line
|
|
392
409
|
check_column_names(names, @names.size, @header_line)
|
|
393
410
|
else
|
|
@@ -401,13 +418,24 @@ class CAFrame
|
|
|
401
418
|
# indexes of the file's columns from 0 (Integers or Ranges), in the order
|
|
402
419
|
# the frame is to have them. The fields of the others are read past, so
|
|
403
420
|
# a few columns of a wide file cost little more than a narrow file.
|
|
421
|
+
#
|
|
422
|
+
# A Hash selects by its keys, as above (a name or an index, not a Range),
|
|
423
|
+
# and names the columns read with its values:
|
|
424
|
+
#
|
|
425
|
+
# r.columns(2 => "code", "c7" => "area1")
|
|
404
426
|
def columns(*selection)
|
|
427
|
+
@renames = nil
|
|
428
|
+
if selection.size == 1 && selection[0].is_a?(Hash)
|
|
429
|
+
@renames = column_renames(selection[0])
|
|
430
|
+
selection = selection[0].keys
|
|
431
|
+
end
|
|
405
432
|
selection = selection.flatten
|
|
406
433
|
if selection.empty?
|
|
407
434
|
raise ArgumentError, "from_csv: columns: names no column"
|
|
408
435
|
end
|
|
409
436
|
selection.each do |c|
|
|
410
437
|
next if c.is_a?(String)
|
|
438
|
+
CAFrame.__send__(:column_key, c, "from_csv", "columns: a column name") if c.is_a?(Symbol)
|
|
411
439
|
next if c.is_a?(Integer) && c >= 0
|
|
412
440
|
next if c.is_a?(Range) && (c.begin.nil? || (c.begin.is_a?(Integer) && c.begin >= 0)) &&
|
|
413
441
|
(c.end.nil? || c.end.is_a?(Integer))
|
|
@@ -418,6 +446,24 @@ class CAFrame
|
|
|
418
446
|
self
|
|
419
447
|
end
|
|
420
448
|
|
|
449
|
+
# The new names a Hash given to +columns+ holds, checked.
|
|
450
|
+
private def column_renames(map)
|
|
451
|
+
map.each do |key, name|
|
|
452
|
+
if key.is_a?(Range)
|
|
453
|
+
raise ArgumentError, "from_csv: columns: a Hash selects one column per key, " \
|
|
454
|
+
"not a Range (got #{key.inspect})"
|
|
455
|
+
end
|
|
456
|
+
CAFrame.__send__(:column_key, name, "from_csv", "columns: a column name",
|
|
457
|
+
hint: "given for #{key.inspect}")
|
|
458
|
+
if name.empty?
|
|
459
|
+
raise ArgumentError, "from_csv: columns: a column name can not be empty " \
|
|
460
|
+
"(given for #{key.inspect})"
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
CAFrame.__send__(:refuse_repeated_names, map.values, "from_csv", "columns: names")
|
|
464
|
+
map.values
|
|
465
|
+
end
|
|
466
|
+
|
|
421
467
|
# The indexes of the selected columns, in the order asked for, among
|
|
422
468
|
# ncol columns named +names+ (nil: by index only). nil when ncol is not
|
|
423
469
|
# known yet and the selection needs it.
|
|
@@ -425,8 +471,12 @@ class CAFrame
|
|
|
425
471
|
indexes = @selection.flat_map do |c|
|
|
426
472
|
case c
|
|
427
473
|
when String
|
|
428
|
-
|
|
429
|
-
|
|
474
|
+
# Without a header the columns are named c0, c1, ... by position.
|
|
475
|
+
j = names ? names.index(c) : c[/\Ac(0|[1-9]\d*)\z/, 1]&.to_i
|
|
476
|
+
if j.nil?
|
|
477
|
+
raise KeyError, "from_csv: columns: names no column #{c.inspect}" +
|
|
478
|
+
(names ? "" : " (with no header the columns are c0, c1, ...)")
|
|
479
|
+
end
|
|
430
480
|
j
|
|
431
481
|
when Integer then c
|
|
432
482
|
else
|
|
@@ -438,10 +488,7 @@ class CAFrame
|
|
|
438
488
|
(first..last).to_a
|
|
439
489
|
end
|
|
440
490
|
end
|
|
441
|
-
|
|
442
|
-
unless dup.empty?
|
|
443
|
-
raise ArgumentError, "from_csv: columns: selects column #{dup.first} more than once"
|
|
444
|
-
end
|
|
491
|
+
CAFrame.__send__(:refuse_repeated_names, indexes, "from_csv", "columns: selects column")
|
|
445
492
|
if ncol && (bad = indexes.find { |j| j >= ncol })
|
|
446
493
|
raise ArgumentError, "from_csv: columns: #{bad} is past the #{ncol} columns of the file"
|
|
447
494
|
end
|
|
@@ -484,22 +531,29 @@ class CAFrame
|
|
|
484
531
|
# or to none (false) when column_names: is given; data: is the first line
|
|
485
532
|
# of the data or a Range of lines, by default from the line after the
|
|
486
533
|
# header. A record that starts within data: is read whole.
|
|
487
|
-
def layout(header: nil, data: nil, column_names: nil, columns: nil)
|
|
534
|
+
def layout(header: nil, data: nil, column_names: nil, columns: nil, attrs: nil)
|
|
488
535
|
header = column_names ? false : 0 if header.nil?
|
|
489
536
|
unless header == false || (header.is_a?(Integer) && header >= 0)
|
|
490
537
|
raise ArgumentError, "from_csv: header: takes the index of a line, from 0, " \
|
|
491
538
|
"or false (got #{header.inspect})"
|
|
492
539
|
end
|
|
540
|
+
attr_keys = attrs.nil? ? [] : CAFrame.send(:csv_attr_keys, attrs, "from_csv")
|
|
541
|
+
if !header && !attr_keys.empty?
|
|
542
|
+
raise ArgumentError, "from_csv: attrs: reads the lines below the header, " \
|
|
543
|
+
"so it needs a header line"
|
|
544
|
+
end
|
|
493
545
|
first, last = CSVReader.data_lines(data)
|
|
494
546
|
if header
|
|
495
547
|
skip(header - @tok.lineno) if header > @tok.lineno
|
|
496
548
|
self.header
|
|
549
|
+
attr_keys.each { |key| self.header(key.to_sym) }
|
|
497
550
|
end
|
|
498
551
|
column_names(*column_names) if column_names
|
|
499
552
|
self.columns(*(columns.is_a?(Array) ? columns : [columns])) if columns
|
|
500
553
|
first ||= @tok.lineno
|
|
501
554
|
if first < @tok.lineno
|
|
502
|
-
raise ArgumentError, "from_csv: data: #{first} does not come after header: #{header}"
|
|
555
|
+
raise ArgumentError, "from_csv: data: #{first} does not come after header: #{header}" +
|
|
556
|
+
(attr_keys.empty? ? "" : " and the #{attr_keys.size} attrs: lines")
|
|
503
557
|
end
|
|
504
558
|
skip(first - @tok.lineno)
|
|
505
559
|
read_data(last && last + 1)
|
|
@@ -575,7 +629,7 @@ class CAFrame
|
|
|
575
629
|
private def read_body_into_const_string_columns
|
|
576
630
|
ncol = @names && !@names_to_check ? @names.size : 0
|
|
577
631
|
keep = columns_to_read
|
|
578
|
-
text =
|
|
632
|
+
text = read_rest
|
|
579
633
|
n, buffer, pairs, records = CArray.__csv_read_body_as_const_string_columns__(text, @tok.sep, @tok.quote, ncol, keep)
|
|
580
634
|
unless buffer
|
|
581
635
|
@rows = []
|
|
@@ -609,7 +663,7 @@ class CAFrame
|
|
|
609
663
|
loop do
|
|
610
664
|
text = if chunked then next_chunk(chunk_bytes)
|
|
611
665
|
elsif last then lines_through(last)
|
|
612
|
-
else
|
|
666
|
+
else read_rest
|
|
613
667
|
end
|
|
614
668
|
break if text.nil?
|
|
615
669
|
n, flat, records = CArray.__csv_read_body_as_string_cells__(text, @tok.sep, @tok.quote, ncol, keep)
|
|
@@ -675,6 +729,20 @@ class CAFrame
|
|
|
675
729
|
buf
|
|
676
730
|
end
|
|
677
731
|
|
|
732
|
+
# The rest of the input, in the IO's encoding. Read in pieces when the IO
|
|
733
|
+
# does not transcode: IO#read without a length checks the text's encoding
|
|
734
|
+
# as it reads, and the C reader checks it again faster.
|
|
735
|
+
private def read_rest
|
|
736
|
+
unless @io.respond_to?(:internal_encoding) && @io.internal_encoding.nil?
|
|
737
|
+
return @io.read || ""
|
|
738
|
+
end
|
|
739
|
+
buf = +""
|
|
740
|
+
while (piece = @io.read(CHUNK_BYTES))
|
|
741
|
+
buf << piece
|
|
742
|
+
end
|
|
743
|
+
buf.force_encoding(@io.external_encoding || Encoding.default_external)
|
|
744
|
+
end
|
|
745
|
+
|
|
678
746
|
# The next chunk_bytes of the input, read on to the end of its line and
|
|
679
747
|
# past any quoted field still open, in the IO's encoding; nil at EOF.
|
|
680
748
|
private def next_chunk(chunk_bytes)
|
|
@@ -690,6 +758,32 @@ class CAFrame
|
|
|
690
758
|
buf
|
|
691
759
|
end
|
|
692
760
|
|
|
761
|
+
# The attributes the named headers give the columns +result+ returns, as
|
|
762
|
+
# [{key => value}, ...] in the same order. A column of the file the
|
|
763
|
+
# selection left out takes no part.
|
|
764
|
+
def column_attrs (ncol)
|
|
765
|
+
return nil unless @named_headers
|
|
766
|
+
indexes = @selection ? selected_indexes(@names, ncol) : (0...ncol).to_a
|
|
767
|
+
attrs = indexes.map { {} }
|
|
768
|
+
@named_headers.each do |key, (fields, line, name)|
|
|
769
|
+
if fields.size > ncol
|
|
770
|
+
raise ArgumentError, "from_csv: header(#{name.inspect}) on line #{line} " \
|
|
771
|
+
"has #{fields.size} fields for the #{ncol} columns"
|
|
772
|
+
end
|
|
773
|
+
indexes.each_with_index do |j, k|
|
|
774
|
+
value = fields[j]
|
|
775
|
+
attrs[k][key] = value unless value.nil?
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
attrs
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
# The number of columns of the file, once the data is read.
|
|
782
|
+
def file_ncol
|
|
783
|
+
return @table_ncol if @table
|
|
784
|
+
@names ? @names.size : ((@rows || []).map(&:size).max || 0)
|
|
785
|
+
end
|
|
786
|
+
|
|
693
787
|
# [names_or_nil, rows] for CAFrame.from_csv to build from. names is nil when
|
|
694
788
|
# neither header nor column_names ran (positional names are generated).
|
|
695
789
|
def result
|
|
@@ -703,7 +797,7 @@ class CAFrame
|
|
|
703
797
|
return [@names, rows] unless @selection
|
|
704
798
|
ncol = @names ? @names.size : (rows.map(&:size).max || 0)
|
|
705
799
|
indexes = selected_indexes(@names, ncol)
|
|
706
|
-
names = indexes.map { |j| @names ? @names[j] : "c#{j}" }
|
|
800
|
+
names = @renames || indexes.map { |j| @names ? @names[j] : "c#{j}" }
|
|
707
801
|
[names, rows.map { |r| r.values_at(*indexes) }]
|
|
708
802
|
end
|
|
709
803
|
|
|
@@ -711,7 +805,7 @@ class CAFrame
|
|
|
711
805
|
private def table_result
|
|
712
806
|
return [@names, @table] unless @selection
|
|
713
807
|
indexes = selected_indexes(@names, @table_ncol)
|
|
714
|
-
names = indexes.map { |j| @names ? @names[j] : "c#{j}" }
|
|
808
|
+
names = @renames || indexes.map { |j| @names ? @names[j] : "c#{j}" }
|
|
715
809
|
kept = @table_keep || (0...@table_ncol).to_a
|
|
716
810
|
order = indexes.map { |j| kept.index(j) }
|
|
717
811
|
table =
|
|
@@ -33,7 +33,7 @@ class CAFrame
|
|
|
33
33
|
# @param names [Array<String>] the columns to summarize; default all.
|
|
34
34
|
# @return [CAFrame] a new frame with one row per column.
|
|
35
35
|
def describe(*names)
|
|
36
|
-
names = names.empty? ? column_names : names
|
|
36
|
+
names = names.empty? ? column_names : column_keys(names, "describe")
|
|
37
37
|
rows = names.map { |name| describe_column(self[name]) }
|
|
38
38
|
stats = %w[type count masked unique min max mean stddev]
|
|
39
39
|
cols = stats.to_h do |stat|
|
|
@@ -51,6 +51,7 @@ class CAFrame
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
DESCRIBE_TIME_FACES = %w[CATime CATimedelta].freeze
|
|
54
|
+
Ractor.make_shareable(DESCRIBE_TIME_FACES)
|
|
54
55
|
private_constant :DESCRIBE_TIME_FACES
|
|
55
56
|
|
|
56
57
|
private def describe_column(col)
|
|
@@ -64,7 +65,7 @@ class CAFrame
|
|
|
64
65
|
else
|
|
65
66
|
:other
|
|
66
67
|
end
|
|
67
|
-
type =
|
|
68
|
+
type = CArray::Inspector.type_label(col)
|
|
68
69
|
type += col.shape[1..].inspect if col.ndim > 1
|
|
69
70
|
row = { "type" => type,
|
|
70
71
|
"count" => col.count_not_masked, "masked" => col.count_masked,
|