carray 3.0.1 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +824 -0
- data/README.md +5 -6
- data/carray.gemspec +1 -1
- data/ext/ca_axis_dispatch.c +258 -4
- data/ext/ca_axis_group.c +368 -184
- data/ext/ca_binop_dispatch.c +54 -32
- data/ext/ca_binop_dispatch.h +1 -0
- data/ext/ca_categorical_iterator.c +90 -41
- data/ext/ca_compare.c +8 -2
- data/ext/ca_for_buffer.h +21 -68
- data/ext/ca_group_iter.c +27 -11
- data/ext/ca_kernel_iterator.c +960 -245
- data/ext/ca_kernel_iterator.h +352 -181
- data/ext/ca_monop_dispatch.c +4 -4
- data/ext/ca_monop_dispatch.h +4 -0
- data/ext/ca_obj_array.c +194 -42
- data/ext/ca_obj_bincmp.c +152 -121
- data/ext/ca_obj_binop.c +107 -210
- data/ext/ca_obj_bitarray.c +37 -24
- data/ext/ca_obj_bitfield.c +95 -62
- data/ext/ca_obj_block.c +5 -4
- data/ext/ca_obj_byte_swap.c +40 -40
- data/ext/ca_obj_const_string.c +180 -50
- data/ext/ca_obj_face.c +428 -62
- data/ext/ca_obj_face.h +107 -47
- data/ext/ca_obj_fake.c +21 -50
- data/ext/ca_obj_farray.c +1 -0
- data/ext/ca_obj_field.c +16 -3
- data/ext/ca_obj_fixlen_string.c +39 -26
- data/ext/ca_obj_grid.c +181 -124
- data/ext/ca_obj_meld.c +235 -55
- data/ext/ca_obj_moncmp.c +61 -61
- data/ext/ca_obj_monop.c +125 -107
- data/ext/ca_obj_object.c +551 -233
- data/ext/ca_obj_record.c +51 -16
- data/ext/ca_obj_reduce.c +5 -8
- data/ext/ca_obj_refer.c +117 -33
- data/ext/ca_obj_remap.c +57 -17
- data/ext/ca_obj_repeat.c +21 -5
- data/ext/ca_obj_roll.c +115 -11
- data/ext/ca_obj_select.c +88 -75
- data/ext/ca_obj_select_axis.c +58 -60
- data/ext/ca_obj_shift.c +22 -102
- data/ext/ca_obj_stack.c +223 -123
- data/ext/ca_obj_stride.c +267 -35
- data/ext/ca_obj_string.c +25 -22
- data/ext/ca_obj_tile.c +32 -16
- data/ext/ca_obj_time.c +23 -25
- data/ext/ca_obj_timedelta.c +21 -23
- data/ext/ca_obj_transpose.c +12 -3
- data/ext/ca_obj_triop.c +110 -117
- data/ext/ca_obj_window.c +157 -29
- data/ext/ca_op_ipower.c +25 -13
- data/ext/ca_op_powi.h +57 -30
- data/ext/ca_pow5_table.h +656 -0
- data/ext/ca_rng_normal.h +42 -0
- data/ext/ca_rng_xoshiro256pp.h +105 -0
- data/ext/ca_sort_kernels.h +70 -0
- data/ext/ca_sweep_engine.c +385 -198
- data/ext/ca_sweep_engine.h +31 -23
- data/ext/ca_triop_dispatch.c +4 -1
- data/ext/ca_triop_dispatch.h +5 -2
- data/ext/caframe_csv_reader.c +463 -0
- data/ext/carray.h +90 -28
- data/ext/carray_access.c +487 -231
- data/ext/carray_address_basis.c +731 -0
- data/ext/carray_attribute.c +45 -3
- data/ext/carray_bincount.c +32 -2
- data/ext/carray_broadcast.c +49 -17
- data/ext/carray_call_cfunc.c +667 -511
- data/ext/carray_cast.c +1556 -247
- data/ext/carray_cast_func.rb +34 -21
- data/ext/carray_conversion.c +197 -73
- data/ext/carray_copy.c +84 -38
- data/ext/carray_core.c +591 -119
- data/ext/carray_count.c +148 -96
- data/ext/carray_element.c +18 -27
- data/ext/carray_factorize.c +604 -291
- data/ext/carray_generate.c +76 -40
- data/ext/carray_histogram.c +152 -94
- data/ext/carray_hold.c +2 -1
- data/ext/carray_index_classifier.c +63 -96
- data/ext/carray_internal.h +400 -1
- data/ext/carray_lazy.c +834 -79
- data/ext/carray_loop.c +88 -66
- data/ext/carray_mask.c +705 -324
- data/ext/carray_math_kernel.h +1 -0
- data/ext/carray_median_percentile.c +263 -63
- data/ext/carray_memory_view.c +254 -248
- data/ext/carray_operator.c +1093 -363
- data/ext/carray_order.c +133 -69
- data/ext/carray_partition.c +102 -56
- data/ext/carray_random.c +582 -204
- data/ext/carray_scatter.c +308 -65
- data/ext/carray_segment.c +257 -0
- data/ext/carray_slab.c +183 -70
- data/ext/carray_sort.c +235 -240
- data/ext/carray_sort_kernel.c +8 -1
- data/ext/carray_struct.c +99 -21
- data/ext/carray_test.c +167 -64
- data/ext/carray_time_text.c +1182 -0
- data/ext/carray_utils.c +363 -98
- data/ext/extconf.rb +27 -2
- data/ext/mk_call_cfunc.rb +103 -118
- data/ext/mk_pow5_table.rb +35 -0
- data/ext/mkkernel.rb +1349 -851
- data/ext/ruby_carray.c +17 -2
- data/ext/version.h +4 -4
- data/ext/version.rb +4 -2
- data/lib/carray/attribute.rb +39 -0
- data/lib/carray/autoload_carray.rb +24 -3
- data/lib/carray/autoload_method_extension.rb +18 -0
- data/lib/carray/axis_group.rb +245 -20
- data/lib/carray/basics.rb +95 -25
- data/lib/carray/bincount_nd.rb +6 -0
- data/lib/carray/block_iterator.rb +200 -54
- data/lib/carray/boolean_reduce.rb +21 -29
- data/lib/carray/categorical.rb +244 -59
- data/lib/carray/categorical_iterator.rb +391 -757
- data/lib/carray/complex.rb +36 -29
- data/lib/carray/conditional.rb +94 -7
- data/lib/carray/const_string.rb +177 -27
- data/lib/carray/construct.rb +40 -0
- data/lib/carray/data_type_extension.rb +3 -0
- data/lib/carray/data_type_limits.rb +91 -0
- data/lib/carray/fixlen_string.rb +1 -1
- data/lib/carray/frame/concat.rb +157 -6
- data/lib/carray/frame/convert.rb +13 -7
- data/lib/carray/frame/csv_parser.rb +590 -76
- data/lib/carray/frame/describe.rb +79 -0
- data/lib/carray/frame/frame.rb +205 -35
- data/lib/carray/frame/group.rb +199 -40
- data/lib/carray/frame/io.rb +452 -60
- data/lib/carray/frame/join.rb +134 -46
- data/lib/carray/frame/records.rb +54 -19
- data/lib/carray/frame/reshape.rb +448 -0
- data/lib/carray/frame/sort.rb +26 -2
- data/lib/carray/frame/verbs.rb +726 -56
- data/lib/carray/frame.rb +2 -0
- data/lib/carray/fuse_source.rb +111 -2
- data/lib/carray/fusion.rb +229 -38
- data/lib/carray/inspect.rb +71 -20
- data/lib/carray/iterator.rb +157 -2
- data/lib/carray/lazy.rb +54 -67
- data/lib/carray/mask_gap_fill.rb +66 -50
- data/lib/carray/math.rb +32 -19
- data/lib/carray/meld_reduce.rb +40 -7
- data/lib/carray/methods/align_addr.rb +8 -4
- data/lib/carray/methods/bincount.rb +2 -2
- data/lib/carray/methods/bit_string.rb +14 -3
- data/lib/carray/methods/composition.rb +16 -10
- data/lib/carray/methods/discovery_along.rb +74 -0
- data/lib/carray/methods/factorize.rb +50 -0
- data/lib/carray/methods/gather_nd.rb +13 -2
- data/lib/carray/methods/insert_block.rb +9 -14
- data/lib/carray/methods/is_in.rb +130 -6
- data/lib/carray/methods/join.rb +1 -1
- data/lib/carray/methods/locate_addr.rb +235 -21
- data/lib/carray/methods/mask_duplicates.rb +35 -1
- data/lib/carray/methods/mode.rb +18 -1
- data/lib/carray/methods/nunique.rb +22 -1
- data/lib/carray/methods/repeat.rb +91 -0
- data/lib/carray/methods/resize.rb +21 -23
- data/lib/carray/methods/snap.rb +34 -12
- data/lib/carray/methods/unique.rb +52 -3
- data/lib/carray/record_order.rb +208 -0
- data/lib/carray/rng.rb +86 -0
- data/lib/carray/runtime.rb +14 -11
- data/lib/carray/segment_iterator.rb +990 -0
- data/lib/carray/serialize.rb +197 -27
- data/lib/carray/slab_iterator.rb +109 -40
- data/lib/carray/stack.rb +10 -4
- data/lib/carray/string_operation_extension.rb +9 -4
- data/lib/carray/struct.rb +142 -51
- data/lib/carray/struct_builder.rb +110 -14
- data/lib/carray/table.rb +2 -2
- data/lib/carray/time.rb +535 -105
- data/lib/carray/window_iterator.rb +259 -82
- data/lib/carray.rb +4 -2
- data/yard-stubs/ca_obj_array.rb +4 -4
- data/yard-stubs/ca_obj_bitarray.rb +7 -0
- data/yard-stubs/ca_obj_bitfield.rb +23 -12
- data/yard-stubs/ca_obj_block.rb +2 -7
- data/yard-stubs/ca_obj_fake.rb +1 -1
- data/yard-stubs/ca_obj_field.rb +1 -1
- data/yard-stubs/ca_obj_refer.rb +3 -3
- data/yard-stubs/ca_obj_roll.rb +0 -1
- data/yard-stubs/ca_obj_stride.rb +10 -7
- data/yard-stubs/ca_obj_transpose.rb +1 -1
- data/yard-stubs/ca_obj_window.rb +11 -4
- data/yard-stubs/carray_access.rb +6 -5
- data/yard-stubs/carray_broadcast.rb +2 -2
- data/yard-stubs/carray_cast.rb +78 -43
- data/yard-stubs/carray_conversion.rb +13 -1
- data/yard-stubs/carray_copy.rb +8 -3
- data/yard-stubs/carray_core.rb +3 -83
- data/yard-stubs/carray_count.rb +16 -2
- data/yard-stubs/carray_lazy.rb +210 -1
- data/yard-stubs/carray_loop.rb +4 -1
- data/yard-stubs/carray_mask.rb +34 -10
- data/yard-stubs/carray_math.rb +1499 -4
- data/yard-stubs/carray_median_percentile.rb +26 -8
- data/yard-stubs/carray_order.rb +43 -82
- data/yard-stubs/carray_scatter.rb +10 -3
- data/yard-stubs/carray_segment.rb +72 -0
- data/yard-stubs/carray_slab.rb +11 -8
- data/yard-stubs/carray_sort.rb +50 -8
- data/yard-stubs/carray_test.rb +10 -0
- data/yard-stubs/carray_utils.rb +0 -26
- metadata +19 -19
- data/ext/ca_for_each_element.h +0 -239
- data/ext/carray_build_flags.h +0 -3
- data/ext/carray_config.h +0 -28
- data/ext/carray_kernels_bincmp.c +0 -4446
- data/ext/carray_kernels_binop.c +0 -11001
- data/ext/carray_kernels_init.c +0 -1131
- data/ext/carray_kernels_map.c +0 -3467
- data/ext/carray_kernels_moncmp.c +0 -2097
- data/ext/carray_kernels_monop.c +0 -18313
- data/ext/carray_kernels_reduce_aggregate.c +0 -25837
- data/ext/carray_kernels_reduce_boolean.c +0 -330
- data/ext/carray_kernels_reduce_cumulative.c +0 -14593
- data/ext/carray_kernels_reduce_extreme.c +0 -16948
- data/ext/carray_kernels_reduce_variance.c +0 -3910
- data/ext/carray_kernels_scan.c +0 -3693
- data/ext/carray_kernels_search.c +0 -32138
- data/ext/carray_kernels_sort.c +0 -10626
- data/ext/carray_kernels_triop.c +0 -1392
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# CAFrame#describe: one row of statistics per column, for a first look at a
|
|
2
|
+
# table that has just been read.
|
|
3
|
+
|
|
4
|
+
class CAFrame
|
|
5
|
+
# Summarize the columns, one row each, as a new frame indexed by column
|
|
6
|
+
# name:
|
|
7
|
+
#
|
|
8
|
+
# puts df.describe.to_table
|
|
9
|
+
#
|
|
10
|
+
# column type count masked unique min max mean stddev
|
|
11
|
+
# ------- ------- ----- ------ ------ ---------- ---------- ---------- --------
|
|
12
|
+
# station object 3 0 2 _ _ _ _
|
|
13
|
+
# temp float64 2 1 2 19.0 22.1 20.55 2.192031
|
|
14
|
+
# time CATime 3 0 3 2024-01-01 2024-01-03 2024-01-02 1D
|
|
15
|
+
#
|
|
16
|
+
# +type+ is the data type, or the Face's class for a Face column, with the
|
|
17
|
+
# trailing shape of an N-D column in brackets (+float64[3]+). +count+ and
|
|
18
|
+
# +masked+ count cells, so an N-D column counts every cell of every row;
|
|
19
|
+
# +unique+ is the number of distinct values among the present cells.
|
|
20
|
+
#
|
|
21
|
+
# The other statistics apply only where the column's kind gives them a
|
|
22
|
+
# meaning, and are UNDEF elsewhere:
|
|
23
|
+
#
|
|
24
|
+
# real numbers, boolean min max mean stddev
|
|
25
|
+
# complex mean stddev (no order)
|
|
26
|
+
# CATime, CATimedelta min max mean stddev (as times / durations)
|
|
27
|
+
# anything else (text, categorical, records ...) unique only
|
|
28
|
+
#
|
|
29
|
+
# A boolean column counts true as 1, so its mean is the share of trues. A
|
|
30
|
+
# column with no present cell has UNDEF statistics. The index is not a
|
|
31
|
+
# column and is not summarized.
|
|
32
|
+
#
|
|
33
|
+
# @param names [Array<String>] the columns to summarize; default all.
|
|
34
|
+
# @return [CAFrame] a new frame with one row per column.
|
|
35
|
+
def describe(*names)
|
|
36
|
+
names = names.empty? ? column_names : names.map(&:to_s)
|
|
37
|
+
rows = names.map { |name| describe_column(self[name]) }
|
|
38
|
+
stats = %w[type count masked unique min max mean stddev]
|
|
39
|
+
cols = stats.to_h do |stat|
|
|
40
|
+
values = rows.map { |row| row[stat] }
|
|
41
|
+
column =
|
|
42
|
+
case stat
|
|
43
|
+
when "count", "masked", "unique" then CArray.int64(values.size)
|
|
44
|
+
else CArray.object(values.size)
|
|
45
|
+
end
|
|
46
|
+
column[] = UNDEF
|
|
47
|
+
values.each_with_index { |v, i| column[i] = v unless UNDEF.equal?(v) }
|
|
48
|
+
[stat, column]
|
|
49
|
+
end
|
|
50
|
+
CAFrame.new(cols, axis_name: "column", index: CA_OBJECT(names))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
DESCRIBE_TIME_FACES = %w[CATime CATimedelta].freeze
|
|
54
|
+
private_constant :DESCRIBE_TIME_FACES
|
|
55
|
+
|
|
56
|
+
private def describe_column(col)
|
|
57
|
+
kind =
|
|
58
|
+
if col.face?
|
|
59
|
+
DESCRIBE_TIME_FACES.include?(col.class.name) ? :ordered : :other
|
|
60
|
+
elsif col.complex?
|
|
61
|
+
:complex
|
|
62
|
+
elsif col.numeric? || col.boolean?
|
|
63
|
+
:ordered
|
|
64
|
+
else
|
|
65
|
+
:other
|
|
66
|
+
end
|
|
67
|
+
type = col.face? ? col.class.name : col.data_type_name
|
|
68
|
+
type += col.shape[1..].inspect if col.ndim > 1
|
|
69
|
+
row = { "type" => type,
|
|
70
|
+
"count" => col.count_not_masked, "masked" => col.count_masked,
|
|
71
|
+
"unique" => col.nunique }
|
|
72
|
+
ordered = kind == :ordered
|
|
73
|
+
row["min"] = ordered ? col.min : UNDEF
|
|
74
|
+
row["max"] = ordered ? col.max : UNDEF
|
|
75
|
+
row["mean"] = kind == :other ? UNDEF : col.mean
|
|
76
|
+
row["stddev"] = kind == :other ? UNDEF : col.stddev
|
|
77
|
+
row
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/carray/frame/frame.rb
CHANGED
|
@@ -11,6 +11,29 @@
|
|
|
11
11
|
# storage, and +copy+ is the way to an independent frame.
|
|
12
12
|
|
|
13
13
|
class CAFrame
|
|
14
|
+
# Raised when the contents of a column cannot be read as asked: a cell
|
|
15
|
+
# that does not read as the type a +cast+ (or +from_csv+'s +types:+) asks
|
|
16
|
+
# for under on_error: :raise, a cell +parse_to_time+ cannot read, a column
|
|
17
|
+
# whose time format +infer_time_format+ / parse_to_time(name, :infer)
|
|
18
|
+
# cannot find, or a float column +to_time+ cannot read as whole counts.
|
|
19
|
+
# The message names the column and the cell. The arguments were fine; the
|
|
20
|
+
# column's contents are what did not read.
|
|
21
|
+
class UnreadableColumn < StandardError; end
|
|
22
|
+
|
|
23
|
+
# Raised when +infer_time_format+ / parse_to_time(name, :infer) finds more
|
|
24
|
+
# than one format every cell fits ("01/02/2024" and "03/04/2024" are both
|
|
25
|
+
# day-first and month-first). The text reads in each of them, so it is
|
|
26
|
+
# not an UnreadableColumn; +formats+ gives the candidates, to choose one and
|
|
27
|
+
# pass it as the format.
|
|
28
|
+
class AmbiguousTimeFormat < StandardError
|
|
29
|
+
attr_reader :formats
|
|
30
|
+
|
|
31
|
+
def initialize(message = nil, formats: [])
|
|
32
|
+
@formats = formats
|
|
33
|
+
super(message)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
14
37
|
# Integer data_types that select rows positionally when used as a df[] key.
|
|
15
38
|
INTEGER_TYPES = [:int8, :int16, :int32, :int64,
|
|
16
39
|
:uint8, :uint16, :uint32, :uint64].freeze
|
|
@@ -45,14 +68,21 @@ class CAFrame
|
|
|
45
68
|
@columns = {}
|
|
46
69
|
@axis_name = axis_name || DEFAULT_AXIS_NAME
|
|
47
70
|
@index = nil
|
|
71
|
+
# The row axis name this frame had before set_index promoted a column over
|
|
72
|
+
# it, so reset_index can put it back. A frame built with an index never
|
|
73
|
+
# had one, and neither does a frame derived from an indexed one: both are
|
|
74
|
+
# born indexed, so there is nothing to restore and the default stands.
|
|
75
|
+
@axis_name_before_index = nil
|
|
48
76
|
|
|
49
77
|
n = nil
|
|
78
|
+
n_from = nil
|
|
50
79
|
columns.each do |name, col|
|
|
51
80
|
key = name.to_s
|
|
52
81
|
ca = coerce_column(col)
|
|
53
82
|
len = ca.shape[0]
|
|
54
83
|
if n.nil?
|
|
55
|
-
n
|
|
84
|
+
n = len
|
|
85
|
+
n_from = key
|
|
56
86
|
elsif len != n
|
|
57
87
|
raise ArgumentError,
|
|
58
88
|
"column #{key.inspect} has axis-0 length #{len}, expected #{n}"
|
|
@@ -67,8 +97,12 @@ class CAFrame
|
|
|
67
97
|
raise ArgumentError, "index must be a 1-D column (got ndim #{idx.ndim})"
|
|
68
98
|
end
|
|
69
99
|
if n && idx.shape[0] != n
|
|
100
|
+
# Name the column as well as the index: when the index is the frame's
|
|
101
|
+
# existing one and a column is the new arrival, blaming the index alone
|
|
102
|
+
# points at the side the caller cannot change.
|
|
70
103
|
raise ArgumentError,
|
|
71
|
-
"
|
|
104
|
+
"column #{n_from.inspect} has axis-0 length #{n}, " \
|
|
105
|
+
"but the index has length #{idx.shape[0]}"
|
|
72
106
|
end
|
|
73
107
|
@index = idx
|
|
74
108
|
@nrow = idx.shape[0] if n.nil?
|
|
@@ -101,7 +135,7 @@ class CAFrame
|
|
|
101
135
|
if keys.size > 1
|
|
102
136
|
unless keys.all? { |k| k.is_a?(String) }
|
|
103
137
|
raise ArgumentError,
|
|
104
|
-
"
|
|
138
|
+
"df[...] with several keys returns columns; every key must be a String " \
|
|
105
139
|
"(use df.select(...) for a subset frame)"
|
|
106
140
|
end
|
|
107
141
|
return keys.map { |k| @columns.fetch(k) { raise KeyError, "no column #{k.inspect}" } }
|
|
@@ -131,7 +165,7 @@ class CAFrame
|
|
|
131
165
|
"CArray df[] key must be boolean or integer (got #{key.data_type})"
|
|
132
166
|
end
|
|
133
167
|
when Symbol
|
|
134
|
-
raise
|
|
168
|
+
raise ArgumentError, "df[symbol] is reserved for predicate keys"
|
|
135
169
|
else
|
|
136
170
|
raise ArgumentError, "unsupported df[] key: #{key.class}"
|
|
137
171
|
end
|
|
@@ -186,6 +220,7 @@ class CAFrame
|
|
|
186
220
|
# write path always propagates to derived views (unchanged from
|
|
187
221
|
# previous behaviour).
|
|
188
222
|
def []=(*keys, value)
|
|
223
|
+
refuse_if_frozen
|
|
189
224
|
if keys.size != 1
|
|
190
225
|
raise ArgumentError,
|
|
191
226
|
"df[...] = takes one key (a column name or a row selector); " \
|
|
@@ -211,6 +246,52 @@ class CAFrame
|
|
|
211
246
|
value
|
|
212
247
|
end
|
|
213
248
|
|
|
249
|
+
# Freezing a frame fixes its columns, rows and index; the columns' cells are
|
|
250
|
+
# their own arrays' to guard, as an Array's elements are. The check comes
|
|
251
|
+
# first so a refused edit leaves the frame as it was: the edits change the
|
|
252
|
+
# column table before any instance variable.
|
|
253
|
+
private def refuse_if_frozen
|
|
254
|
+
raise FrozenError.new("can't modify frozen CAFrame: #{inspect}", receiver: self) if frozen?
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Make this frame read-only and return it. It is +freeze+ extended to the
|
|
258
|
+
# cells: the column set, the rows and the index are fixed as by +freeze+,
|
|
259
|
+
# and the cells and masks of every column and of the index refuse writes
|
|
260
|
+
# made through this frame.
|
|
261
|
+
#
|
|
262
|
+
# Each column (and the index) is replaced by a read-only view of itself, so
|
|
263
|
+
# the arrays the frame was built from are left as they were: whoever holds
|
|
264
|
+
# them can still write, and the writes show through this frame. Frames
|
|
265
|
+
# derived from a protected frame by views (+select+, +filter+, a row slice)
|
|
266
|
+
# read through those views and refuse cell writes too, but are not frozen.
|
|
267
|
+
# +copy+ gives a frame that can be written again.
|
|
268
|
+
#
|
|
269
|
+
# Calling it on a protected frame returns the frame unchanged. A frame
|
|
270
|
+
# frozen with +freeze+ cannot be protected afterwards, because its columns
|
|
271
|
+
# can no longer be replaced: raises +FrozenError+.
|
|
272
|
+
def protect
|
|
273
|
+
return self if protected?
|
|
274
|
+
refuse_if_frozen
|
|
275
|
+
@columns = @columns.transform_values { |col| read_only_view(col) }
|
|
276
|
+
@index = read_only_view(@index) if @index
|
|
277
|
+
@protected = true
|
|
278
|
+
freeze
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# True if this frame was made read-only by +protect+. A +dup+ is not frozen
|
|
282
|
+
# and so not protected, though its columns stay the read-only views it
|
|
283
|
+
# shares; a +clone+ keeps both.
|
|
284
|
+
def protected?
|
|
285
|
+
frozen? && @protected == true
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
private def read_only_view(col)
|
|
289
|
+
return col if col.read_only?
|
|
290
|
+
view = col.refer
|
|
291
|
+
view.set_read_only_flag
|
|
292
|
+
view
|
|
293
|
+
end
|
|
294
|
+
|
|
214
295
|
# df["name"] = ... : the column forms of []=. Unlike the verbs, +[]=+ can
|
|
215
296
|
# only ever mutate the receiver -- Ruby hands the right-hand side back as
|
|
216
297
|
# the value of an assignment, so there is no way to return a new frame.
|
|
@@ -227,16 +308,17 @@ class CAFrame
|
|
|
227
308
|
mask_column(key)
|
|
228
309
|
elsif value.is_a?(CAFrame)
|
|
229
310
|
raise ArgumentError,
|
|
230
|
-
"df[name] = takes a column, not a CAFrame;
|
|
311
|
+
"df[name] = takes a column, not a CAFrame; take one with " \
|
|
231
312
|
"other[\"name\"], or splice rows with df[rows] = other"
|
|
232
313
|
else
|
|
233
314
|
rebind_column(key, value)
|
|
234
315
|
end
|
|
235
316
|
end
|
|
236
317
|
|
|
237
|
-
# Bind +key+ to +column+, adding the name when it is new. This is
|
|
238
|
-
#
|
|
239
|
-
#
|
|
318
|
+
# Bind +key+ to +column+, adding the name when it is new. This is where a
|
|
319
|
+
# column arriving from outside the frame has its axis-0 length checked
|
|
320
|
+
# (§12-A); the conversions in verbs.rb rebind +@columns+ directly and rely on
|
|
321
|
+
# being length-preserving instead. It is a replacement rather than an edit,
|
|
240
322
|
# which is what sets it apart from the rest: +fill+ / +mask_eq+ /
|
|
241
323
|
# +df[rows] = UNDEF+ write to the shared column and are therefore visible
|
|
242
324
|
# wherever it is held, while this binds the name to a different column and
|
|
@@ -307,15 +389,21 @@ class CAFrame
|
|
|
307
389
|
# value kept, so the row stays identifiable), leaving a later, better-informed
|
|
308
390
|
# pass to re-judge them. Definitely-true rows carry their values through
|
|
309
391
|
# unchanged in both modes.
|
|
392
|
+
#
|
|
393
|
+
# The default is a view-frame. +keep_masked: true+ returns a **materialized**
|
|
394
|
+
# frame instead -- carrying the UNDEF forward means writing it into the
|
|
395
|
+
# result, which a view cannot do without masking the parent's rows. That holds
|
|
396
|
+
# whether or not the selector actually carries a masked cell, so the same call
|
|
397
|
+
# site does not switch between sharing and copying with the data.
|
|
310
398
|
def filter(keep_masked: false)
|
|
311
399
|
mask = yield(self)
|
|
312
400
|
unless mask.is_a?(CArray) && mask.data_type == :boolean
|
|
313
401
|
raise ArgumentError,
|
|
314
402
|
"filter block must return a boolean CArray (got #{mask.class})"
|
|
315
403
|
end
|
|
316
|
-
unless mask.shape
|
|
404
|
+
unless mask.shape == [@nrow]
|
|
317
405
|
raise ArgumentError,
|
|
318
|
-
"filter mask has
|
|
406
|
+
"filter mask has shape #{mask.shape.inspect}, expected [#{@nrow}] (one value per row)"
|
|
319
407
|
end
|
|
320
408
|
select_rows(mask, keep_masked: keep_masked)
|
|
321
409
|
end
|
|
@@ -324,13 +412,25 @@ class CAFrame
|
|
|
324
412
|
# An index-role change: the data is unchanged, so this mutates self and
|
|
325
413
|
# returns it (memo §3.8). Any existing index is replaced.
|
|
326
414
|
def set_index(name)
|
|
415
|
+
refuse_if_frozen
|
|
327
416
|
key = name.to_s
|
|
328
417
|
raise KeyError, "no column #{key.inspect}" unless @columns.key?(key)
|
|
329
418
|
idx = @columns[key]
|
|
330
419
|
unless idx.ndim == 1
|
|
331
420
|
raise ArgumentError, "index must be a 1-D column (got ndim #{idx.ndim})"
|
|
332
421
|
end
|
|
422
|
+
# Only the name held before the frame had an index: a second set_index must
|
|
423
|
+
# not overwrite it with the first index's name.
|
|
424
|
+
@axis_name_before_index = @axis_name if @index.nil?
|
|
333
425
|
@columns.delete(key)
|
|
426
|
+
if @index
|
|
427
|
+
# Re-indexing replaces the index, so the one being replaced goes back to
|
|
428
|
+
# being a column -- the same demotion reset_index performs, in the same
|
|
429
|
+
# position, which makes set_index over an existing index equivalent to
|
|
430
|
+
# reset_index followed by set_index. Dropping it would lose the column it
|
|
431
|
+
# was made from.
|
|
432
|
+
@columns = { @axis_name => @index }.merge(@columns)
|
|
433
|
+
end
|
|
334
434
|
@index = idx
|
|
335
435
|
@axis_name = key
|
|
336
436
|
@nrow = idx.shape[0]
|
|
@@ -341,10 +441,12 @@ class CAFrame
|
|
|
341
441
|
# Also an index-role change, so it mutates self (memo §3.8). The former
|
|
342
442
|
# index becomes the first column, named after the row axis.
|
|
343
443
|
def reset_index
|
|
444
|
+
refuse_if_frozen
|
|
344
445
|
return self unless @index
|
|
345
446
|
@columns = { @axis_name => @index }.merge(@columns)
|
|
346
447
|
@index = nil
|
|
347
|
-
@axis_name = DEFAULT_AXIS_NAME
|
|
448
|
+
@axis_name = @axis_name_before_index || DEFAULT_AXIS_NAME
|
|
449
|
+
@axis_name_before_index = nil
|
|
348
450
|
self
|
|
349
451
|
end
|
|
350
452
|
|
|
@@ -364,22 +466,50 @@ class CAFrame
|
|
|
364
466
|
|
|
365
467
|
# The single row whose index label equals +label+, as a Ruby Hash (memo
|
|
366
468
|
# §13.2b). +label+ is matched exactly against the index (+index.eq+), so any
|
|
367
|
-
# orderable / object / datetime / categorical index works
|
|
469
|
+
# orderable / object / datetime / categorical index works, and +label+ is
|
|
470
|
+
# whatever +index.eq+ takes -- for a time index, a Time, Date or a String
|
|
471
|
+
# such as "2024-01-02". A NaN label finds the NaN label, as join and
|
|
472
|
+
# group_by match NaN keys. A numeric index takes a number; a String or
|
|
473
|
+
# true / false raises. The return type is
|
|
368
474
|
# a row Hash and stays that way: zero matches raise KeyError, and duplicate
|
|
369
475
|
# labels raise (go through +filter { |f| f.index.eq(label) }+ for the
|
|
370
476
|
# multi-row, frame-returning path). Positional access is +df[i]+.
|
|
371
477
|
def at(label)
|
|
372
478
|
raise ArgumentError, "at requires an index (set one with set_index)" unless @index
|
|
373
|
-
|
|
479
|
+
# An index may hold a masked cell -- an outer join and align both build one
|
|
480
|
+
# -- but an undefined label identifies no row, and two undefined labels are
|
|
481
|
+
# not the same label. The addressing primitives already say this (a masked
|
|
482
|
+
# key matches nothing, not another masked key), so at says it too rather
|
|
483
|
+
# than answering through eq(UNDEF), which asks about the mask and not about
|
|
484
|
+
# the label at all.
|
|
485
|
+
if UNDEF.equal?(label)
|
|
486
|
+
raise ArgumentError,
|
|
487
|
+
"at(UNDEF): an undefined label identifies no row; " \
|
|
488
|
+
"use filter { |f| f.index.is_masked } for the rows with no label"
|
|
489
|
+
end
|
|
490
|
+
# A NaN label is the NaN value, as join and group_by take it: eq answers
|
|
491
|
+
# false for NaN against anything, itself included.
|
|
492
|
+
# A numeric index is looked up with a number: a String or true/false would
|
|
493
|
+
# be read differently by each numeric type, or not at all.
|
|
494
|
+
if !@index.face? && (@index.integer? || @index.float? || @index.complex?) &&
|
|
495
|
+
!label.is_a?(Numeric)
|
|
496
|
+
raise ArgumentError,
|
|
497
|
+
"at: the index is #{@index.data_type}, so the label must be a number " \
|
|
498
|
+
"(got #{label.inspect}); convert it first, e.g. at(Float(s)) or at(Integer(s))"
|
|
499
|
+
end
|
|
500
|
+
nan = label.is_a?(Float) && label.nan?
|
|
501
|
+
hit = nan ? @index.is_in([label]) : @index.eq(label)
|
|
502
|
+
pos = hit.where
|
|
374
503
|
case pos.elements
|
|
375
504
|
when 0
|
|
376
505
|
raise KeyError, "no row with index label #{label.inspect}"
|
|
377
506
|
when 1
|
|
378
507
|
row(pos[0])
|
|
379
508
|
else
|
|
509
|
+
test = nan ? "f.index.is_nan" : "f.index.eq(label)"
|
|
380
510
|
raise ArgumentError,
|
|
381
511
|
"index label #{label.inspect} matches #{pos.elements} rows; " \
|
|
382
|
-
"use filter { |f|
|
|
512
|
+
"use filter { |f| #{test} } for duplicate labels"
|
|
383
513
|
end
|
|
384
514
|
end
|
|
385
515
|
|
|
@@ -403,20 +533,20 @@ class CAFrame
|
|
|
403
533
|
# --- metadata readers (memo §13.2) ------------------------------------
|
|
404
534
|
# Each returns a fresh object; the live columns Hash is never exposed.
|
|
405
535
|
|
|
406
|
-
#
|
|
407
|
-
def
|
|
536
|
+
# Column names in column order. Returns +Array<String>+.
|
|
537
|
+
def column_names
|
|
408
538
|
@columns.keys
|
|
409
539
|
end
|
|
410
540
|
|
|
411
|
-
#
|
|
412
|
-
#
|
|
413
|
-
#
|
|
414
|
-
def
|
|
541
|
+
# The columns (raw CArrays) in column order. Returns +Array<CArray>+.
|
|
542
|
+
# Equivalent to +df[*column_names]+ but built directly from the columns
|
|
543
|
+
# Hash.
|
|
544
|
+
def columns
|
|
415
545
|
@columns.values
|
|
416
546
|
end
|
|
417
547
|
|
|
418
|
-
# Number of
|
|
419
|
-
def
|
|
548
|
+
# Number of columns.
|
|
549
|
+
def ncol
|
|
420
550
|
@columns.size
|
|
421
551
|
end
|
|
422
552
|
|
|
@@ -444,7 +574,7 @@ class CAFrame
|
|
|
444
574
|
if col.is_a?(CAFrame)
|
|
445
575
|
raise ArgumentError,
|
|
446
576
|
"a CAFrame cannot be a column (it answers to_ca as a 2-D matrix); " \
|
|
447
|
-
"
|
|
577
|
+
"take a column with df[\"name\"] or df.to_ca first"
|
|
448
578
|
end
|
|
449
579
|
unless col.respond_to?(:to_ca)
|
|
450
580
|
raise ArgumentError, "column must be a CArray or Array (got #{col.class})"
|
|
@@ -457,10 +587,11 @@ class CAFrame
|
|
|
457
587
|
end
|
|
458
588
|
|
|
459
589
|
private def row(i)
|
|
460
|
-
i
|
|
461
|
-
unless
|
|
590
|
+
pos = i < 0 ? i + @nrow : i
|
|
591
|
+
unless pos >= 0 && pos < @nrow
|
|
462
592
|
raise IndexError, "row index #{i} out of range (nrow=#{@nrow})"
|
|
463
593
|
end
|
|
594
|
+
i = pos
|
|
464
595
|
h = {}
|
|
465
596
|
h[@axis_name] = elem_at(@index, i) if @index
|
|
466
597
|
@columns.each { |name, col| h[name] = elem_at(col, i) }
|
|
@@ -483,6 +614,19 @@ class CAFrame
|
|
|
483
614
|
# and index are untouched; the selected cells of every column go to UNDEF.
|
|
484
615
|
# The selector is forwarded to the column indexer, which classifies it.
|
|
485
616
|
private def mask_rows(selector)
|
|
617
|
+
# Decide before changing anything. A read-only column (a categorical, whose
|
|
618
|
+
# codes are read-only) refuses the write, and refusing half way through
|
|
619
|
+
# would leave the frame masked in whichever columns happened to come first.
|
|
620
|
+
refusing = @columns.find { |_, col| col.read_only? }
|
|
621
|
+
if refusing
|
|
622
|
+
raise "can not modify read-only array: column #{refusing[0].inspect} " \
|
|
623
|
+
"refuses the write, so no column was masked"
|
|
624
|
+
end
|
|
625
|
+
# The selector is classified by the column indexer, which is also what
|
|
626
|
+
# bound-checks it -- so with no column to forward it to, nothing would.
|
|
627
|
+
# Build the row mask purely to have the frame's own row axis refuse an
|
|
628
|
+
# out-of-range row, as every other row form does.
|
|
629
|
+
selected_row_mask(selector) if @columns.empty?
|
|
486
630
|
@columns.each_value do |col|
|
|
487
631
|
col[selector, *([nil] * (col.ndim - 1))] = UNDEF
|
|
488
632
|
end
|
|
@@ -508,10 +652,10 @@ class CAFrame
|
|
|
508
652
|
# promote and the row count shifts by other.nrow - span.
|
|
509
653
|
private def splice_rows(selector, other)
|
|
510
654
|
lo, hi = contiguous_span(selector)
|
|
511
|
-
unless other.
|
|
655
|
+
unless other.column_names.sort == column_names.sort
|
|
512
656
|
raise ArgumentError,
|
|
513
|
-
"splice frame has columns #{other.
|
|
514
|
-
"expected the same set as #{
|
|
657
|
+
"splice frame has columns #{other.column_names.inspect}, " \
|
|
658
|
+
"expected the same set as #{column_names.inspect}"
|
|
515
659
|
end
|
|
516
660
|
new_cols = {}
|
|
517
661
|
@columns.each do |name, col|
|
|
@@ -540,7 +684,16 @@ class CAFrame
|
|
|
540
684
|
new_index = splice_index(other, lo, hi)
|
|
541
685
|
@columns = new_cols
|
|
542
686
|
@index = new_index
|
|
543
|
-
|
|
687
|
+
# Read the new count off something the frame now holds, rather than
|
|
688
|
+
# computing it. For every column the pieces welded above are head (lo) +
|
|
689
|
+
# other's rows + tail (nrow - hi), so the arithmetic and the objects agree
|
|
690
|
+
# -- except when there is nothing to weld: a frame with no columns and no
|
|
691
|
+
# index would otherwise come away claiming rows that nothing backs.
|
|
692
|
+
@nrow = if (witness = new_cols.each_value.first || new_index)
|
|
693
|
+
witness.shape[0]
|
|
694
|
+
else
|
|
695
|
+
0
|
|
696
|
+
end
|
|
544
697
|
self
|
|
545
698
|
end
|
|
546
699
|
|
|
@@ -609,8 +762,11 @@ class CAFrame
|
|
|
609
762
|
end
|
|
610
763
|
|
|
611
764
|
private def select_rows(selector, keep_masked: false)
|
|
612
|
-
if keep_masked && selector.is_a?(CArray) &&
|
|
613
|
-
|
|
765
|
+
if keep_masked && selector.is_a?(CArray) && selector.data_type == :boolean
|
|
766
|
+
# Not gated on selector.has_mask?: gating there would make the same call
|
|
767
|
+
# site return a view-frame or a materialized one depending on whether that
|
|
768
|
+
# run's data happened to produce an undetermined cell (the reason splice
|
|
769
|
+
# always snapshots -- see CAFrame.md section 3).
|
|
614
770
|
return select_rows_keep_masked(selector)
|
|
615
771
|
end
|
|
616
772
|
cols = {}
|
|
@@ -625,13 +781,27 @@ class CAFrame
|
|
|
625
781
|
keep = selector.strip_mask(1) # true where selected OR undetermined
|
|
626
782
|
undet_kept = selector.is_masked[keep] # among kept rows, which were undetermined
|
|
627
783
|
cols = {}
|
|
784
|
+
any_undet = undet_kept.any
|
|
628
785
|
@columns.each do |name, col|
|
|
629
786
|
tail = [nil] * (col.ndim - 1)
|
|
630
|
-
g = col[keep, *tail]
|
|
631
|
-
|
|
632
|
-
|
|
787
|
+
g = col[keep, *tail]
|
|
788
|
+
if any_undet
|
|
789
|
+
# mask_where copies and masks in one step, so a read-only Face
|
|
790
|
+
# (categorical, CAConstString) takes the mask as any column does.
|
|
791
|
+
cells = CArray.boolean(*g.shape)
|
|
792
|
+
cells[] = false
|
|
793
|
+
cells[undet_kept, *tail] = true
|
|
794
|
+
cols[name] = g.mask_where(cells)
|
|
795
|
+
else
|
|
796
|
+
cols[name] = g.copy
|
|
797
|
+
end
|
|
633
798
|
end
|
|
634
|
-
|
|
799
|
+
# The index is copied along with the columns. Its values are carried over
|
|
800
|
+
# unmasked (an undetermined row keeps its label, as mask_rows leaves the
|
|
801
|
+
# index alone), but a materialized frame has to be materialized whole: an
|
|
802
|
+
# aliased index would write through to the parent from a frame whose columns
|
|
803
|
+
# do not.
|
|
804
|
+
new_index = @index ? @index[keep].copy : nil
|
|
635
805
|
CAFrame.new(cols, axis_name: @axis_name, index: new_index)
|
|
636
806
|
end
|
|
637
807
|
|