carray 3.0.1 → 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/CHANGELOG.md +520 -0
- data/README.md +2 -2
- data/carray.gemspec +1 -1
- data/ext/ca_axis_dispatch.c +33 -4
- data/ext/ca_axis_group.c +202 -96
- data/ext/ca_categorical_iterator.c +108 -54
- data/ext/ca_kernel_iterator.c +317 -51
- data/ext/ca_kernel_iterator.h +142 -35
- data/ext/ca_obj_array.c +62 -20
- data/ext/ca_obj_block.c +4 -4
- data/ext/ca_obj_const_string.c +85 -26
- data/ext/ca_obj_face.c +24 -0
- data/ext/ca_obj_face.h +15 -0
- data/ext/ca_obj_fixlen_string.c +18 -5
- data/ext/ca_obj_meld.c +123 -25
- data/ext/ca_obj_object.c +8 -0
- data/ext/ca_obj_select.c +49 -34
- data/ext/ca_obj_stack.c +3 -8
- data/ext/ca_obj_stride.c +72 -1
- data/ext/ca_obj_string.c +8 -4
- data/ext/ca_obj_window.c +8 -2
- data/ext/ca_op_ipower.c +1 -2
- data/ext/ca_rng_normal.h +42 -0
- data/ext/ca_rng_xoshiro256pp.h +105 -0
- data/ext/ca_sweep_engine.c +307 -143
- data/ext/ca_sweep_engine.h +26 -5
- data/ext/carray.h +21 -2
- data/ext/carray_access.c +32 -20
- data/ext/carray_address_basis.c +590 -0
- data/ext/carray_broadcast.c +3 -3
- data/ext/carray_call_cfunc.c +667 -483
- data/ext/carray_cast.c +115 -41
- data/ext/carray_copy.c +55 -30
- data/ext/carray_core.c +83 -3
- data/ext/carray_count.c +9 -10
- data/ext/carray_factorize.c +46 -25
- data/ext/carray_internal.h +17 -0
- data/ext/carray_kernels_reduce_aggregate.c +168 -0
- data/ext/carray_kernels_reduce_cumulative.c +270 -1
- data/ext/carray_kernels_reduce_extreme.c +554 -8
- data/ext/carray_kernels_scan.c +4 -4
- data/ext/carray_kernels_search.c +94 -14
- data/ext/carray_loop.c +7 -1
- data/ext/carray_mask.c +23 -8
- data/ext/carray_median_percentile.c +55 -0
- data/ext/carray_operator.c +4 -4
- data/ext/carray_order.c +1 -1
- data/ext/carray_random.c +384 -40
- data/ext/carray_slab.c +13 -0
- data/ext/carray_sort.c +20 -22
- data/ext/mk_call_cfunc.rb +103 -116
- data/ext/mkkernel.rb +297 -29
- data/ext/ruby_carray.c +10 -1
- data/ext/version.h +4 -4
- data/lib/carray/autoload_carray.rb +5 -3
- data/lib/carray/autoload_method_extension.rb +12 -0
- data/lib/carray/axis_group.rb +77 -0
- data/lib/carray/basics.rb +4 -0
- data/lib/carray/block_iterator.rb +92 -16
- data/lib/carray/categorical.rb +150 -33
- data/lib/carray/categorical_iterator.rb +207 -80
- data/lib/carray/const_string.rb +131 -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/csv_parser.rb +11 -4
- data/lib/carray/frame/frame.rb +81 -10
- data/lib/carray/frame/group.rb +36 -3
- data/lib/carray/frame/io.rb +67 -15
- data/lib/carray/frame/records.rb +18 -4
- data/lib/carray/frame/verbs.rb +14 -11
- data/lib/carray/inspect.rb +42 -9
- data/lib/carray/iterator.rb +143 -0
- data/lib/carray/lazy.rb +0 -37
- data/lib/carray/mask_gap_fill.rb +3 -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 +13 -2
- data/lib/carray/methods/locate_addr.rb +75 -2
- data/lib/carray/methods/mask_duplicates.rb +35 -1
- data/lib/carray/methods/nunique.rb +22 -1
- data/lib/carray/methods/repeat.rb +110 -0
- data/lib/carray/methods/unique.rb +41 -1
- data/lib/carray/rng.rb +86 -0
- data/lib/carray/slab_iterator.rb +58 -13
- data/lib/carray/string_operation_extension.rb +5 -1
- data/lib/carray/time.rb +18 -2
- data/lib/carray/window_iterator.rb +142 -20
- data/lib/carray.rb +2 -0
- data/yard-stubs/ca_obj_block.rb +2 -7
- data/yard-stubs/ca_obj_window.rb +10 -2
- data/yard-stubs/carray_access.rb +1 -1
- data/yard-stubs/carray_broadcast.rb +1 -1
- data/yard-stubs/carray_core.rb +0 -80
- data/yard-stubs/carray_count.rb +7 -2
- data/yard-stubs/carray_lazy.rb +205 -0
- data/yard-stubs/carray_math.rb +1486 -3
- data/yard-stubs/carray_median_percentile.rb +16 -2
- data/yard-stubs/carray_order.rb +9 -69
- data/yard-stubs/carray_slab.rb +9 -7
- data/yard-stubs/carray_sort.rb +7 -5
- metadata +9 -1
data/lib/carray/const_string.rb
CHANGED
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
# CAConstString high-level construction + conversion surface.
|
|
6
6
|
#
|
|
7
7
|
# CAConstString itself (the Face + tail buffer + fetch decode + numeric gate) lives
|
|
8
|
-
# in ext/ca_obj_const_string.c. This file provides the ergonomic builders that
|
|
9
|
-
# Ruby Strings into the
|
|
10
|
-
#
|
|
8
|
+
# in ext/ca_obj_const_string.c. This file provides the ergonomic builders that
|
|
9
|
+
# pack Ruby Strings into the shared buffer + (start,end) pair entity, via the
|
|
10
|
+
# CAConstString.__build__ C primitive.
|
|
11
11
|
#
|
|
12
|
-
# Internal buffer format: concatenation of
|
|
13
|
-
#
|
|
12
|
+
# Internal buffer format: a pure concatenation of the element bytes, with no
|
|
13
|
+
# per-record length prefix (= the Arrow values buffer). Each element carries
|
|
14
|
+
# its own `(start, end)` byte range in the storage, so it is self-describing
|
|
15
|
+
# and a gather / sort / select view over the pairs decodes correctly with no
|
|
16
|
+
# buffer copy.
|
|
14
17
|
#
|
|
15
18
|
# ----------------------------------------------------------------------------
|
|
16
19
|
|
|
@@ -89,21 +92,118 @@ class CAConstString
|
|
|
89
92
|
# override the L1 generics where present.
|
|
90
93
|
include CArray::StringOperationMixin
|
|
91
94
|
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
95
|
+
# ---- ordering family ---------------------------------------------------
|
|
96
|
+
#
|
|
97
|
+
# A storage cell is a `(start, end)` byte range into the shared buffer, so
|
|
98
|
+
# storage order is the order the strings were packed in, not the order they
|
|
99
|
+
# compare in. Without these overrides the family sorted the offsets: the
|
|
100
|
+
# answers came back well-formed and wrong -- `%w[pear apple].sort_addr` gave
|
|
101
|
+
# the identity, and `partition_copy` gave NUL bytes.
|
|
102
|
+
#
|
|
103
|
+
# Every member therefore reads the bytes. The flat forms do it natively
|
|
104
|
+
# (a byte-memcmp scan over the packed buffer, an order of magnitude faster
|
|
105
|
+
# than decoding a Ruby String per cell), the per-axis forms through
|
|
106
|
+
# {#to_string}, where a cell is the string and CArray's own kernels apply.
|
|
107
|
+
# Arguments are forwarded verbatim, so `kind:` / `masked_position:` /
|
|
108
|
+
# `keep_axis:` mean here what they mean on CArray.
|
|
109
|
+
#
|
|
110
|
+
# Anything whose cells are strings comes back a CAConstString, gathered
|
|
111
|
+
# over the same buffer where it can be (sort / sort_copy) and rebuilt where
|
|
112
|
+
# it cannot. Indices, addresses and counts need no conversion.
|
|
113
|
+
|
|
114
|
+
# @overload sort(axis: nil, kind: :quick, masked_position: :last)
|
|
115
|
+
# Returns a sorted {CAConstString} view, gathered over the same buffer
|
|
116
|
+
# and offsets. With no `axis:` the array is flattened first, as
|
|
117
|
+
# `CArray#sort` does.
|
|
96
118
|
# @return [CAConstString]
|
|
97
|
-
def sort
|
|
98
|
-
|
|
119
|
+
def sort (*args, **kw)
|
|
120
|
+
addr = sort_addr(*args, **kw)
|
|
121
|
+
kw[:axis].nil? ? self[addr.flatten] : self[addr]
|
|
99
122
|
end
|
|
100
123
|
|
|
101
|
-
# @overload sort_copy
|
|
102
|
-
# Returns an owned sorted {CAConstString}; the materialised
|
|
103
|
-
#
|
|
124
|
+
# @overload sort_copy(axis: nil, kind: :quick, masked_position: :last)
|
|
125
|
+
# Returns an owned sorted {CAConstString}; the materialised counterpart
|
|
126
|
+
# to {#sort}.
|
|
104
127
|
# @return [CAConstString]
|
|
105
|
-
def sort_copy
|
|
106
|
-
|
|
128
|
+
def sort_copy (*args, **kw)
|
|
129
|
+
sort(*args, **kw).copy
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @overload sort_addr(axis: nil, kind: :quick, masked_position: :last)
|
|
133
|
+
# Returns the view-flat addresses that index a sort by string order.
|
|
134
|
+
# @return [CArray] `:int64` addresses.
|
|
135
|
+
def sort_addr (*args, **kw)
|
|
136
|
+
# The native scan has no notion of an incomparable sentinel, so a masked
|
|
137
|
+
# column goes the same way a per-axis one does.
|
|
138
|
+
return __sort_addr_bytes__ if args.empty? && kw.empty? && ! has_mask?
|
|
139
|
+
to_string.sort_addr(*args, **kw)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# @overload sort_index(axis: nil, kind: :quick, masked_position: :last)
|
|
143
|
+
# Returns the per-fiber indices that index a sort by string order.
|
|
144
|
+
# @return [CArray] `:int64` indices.
|
|
145
|
+
def sort_index (*args, **kw)
|
|
146
|
+
to_string.sort_index(*args, **kw)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @overload rank_index(axis: nil)
|
|
150
|
+
# @return [CArray] each cell's rank in string order.
|
|
151
|
+
def rank_index (*args, **kw)
|
|
152
|
+
to_string.rank_index(*args, **kw)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @overload order(*args)
|
|
156
|
+
# @return [CArray] the ordering of the cells by string order.
|
|
157
|
+
def order (*args, **kw)
|
|
158
|
+
to_string.order(*args, **kw)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @overload min(axis: nil, keep_axis: false)
|
|
162
|
+
# Returns the byte-smallest string, skipping masked cells; UNDEF when
|
|
163
|
+
# every cell is masked or the array is empty.
|
|
164
|
+
# @return [String, CAConstString]
|
|
165
|
+
def min (*args, **kw)
|
|
166
|
+
return __min_bytes__ || UNDEF if args.empty? && kw.empty?
|
|
167
|
+
const_string_lift(to_string.min(*args, **kw))
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# @overload max(axis: nil, keep_axis: false)
|
|
171
|
+
# Byte-largest counterpart of {#min}.
|
|
172
|
+
# @return [String, CAConstString]
|
|
173
|
+
def max (*args, **kw)
|
|
174
|
+
return __max_bytes__ || UNDEF if args.empty? && kw.empty?
|
|
175
|
+
const_string_lift(to_string.max(*args, **kw))
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# @overload minmax(axis: nil, keep_axis: false)
|
|
179
|
+
# @return [Array] `[min, max]`.
|
|
180
|
+
def minmax (*args, **kw)
|
|
181
|
+
return [min, max] if args.empty? && kw.empty?
|
|
182
|
+
to_string.minmax(*args, **kw).map { |r| const_string_lift(r) }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# @overload min_index(axis: nil)
|
|
186
|
+
# @return [Integer, CArray] where the byte-smallest string sits.
|
|
187
|
+
def min_index (*args, **kw)
|
|
188
|
+
to_string.min_index(*args, **kw)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# @overload max_index(axis: nil)
|
|
192
|
+
# @return [Integer, CArray] where the byte-largest string sits.
|
|
193
|
+
def max_index (*args, **kw)
|
|
194
|
+
to_string.max_index(*args, **kw)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# @overload partition_copy(kth, axis: nil)
|
|
198
|
+
# @return [CAConstString] partitioned about the `kth` string in order.
|
|
199
|
+
def partition_copy (*args, **kw)
|
|
200
|
+
const_string_lift(to_string.partition_copy(*args, **kw))
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# @overload partition_index(kth, axis: nil)
|
|
204
|
+
# @return [CArray] the indices that partition about the `kth` string.
|
|
205
|
+
def partition_index (*args, **kw)
|
|
206
|
+
to_string.partition_index(*args, **kw)
|
|
107
207
|
end
|
|
108
208
|
|
|
109
209
|
# CAConstString is the one Face with three separate entities -- storage
|
|
@@ -144,14 +244,14 @@ class CAConstString
|
|
|
144
244
|
# @overload unique(sort: false)
|
|
145
245
|
# @return [CAConstString] the distinct strings.
|
|
146
246
|
def unique (sort: false)
|
|
147
|
-
to_string.unique(sort: sort)
|
|
247
|
+
const_string_lift(to_string.unique(sort: sort))
|
|
148
248
|
end
|
|
149
249
|
|
|
150
250
|
# @overload value_counts(sort: false)
|
|
151
251
|
# @return [Array(CAConstString, CArray)] `[values, counts]`.
|
|
152
252
|
def value_counts (sort: false)
|
|
153
253
|
values, counts = to_string.value_counts(sort: sort)
|
|
154
|
-
[values
|
|
254
|
+
[const_string_lift(values), counts]
|
|
155
255
|
end
|
|
156
256
|
|
|
157
257
|
# @overload nunique(axis: nil, keep_axis: false)
|
|
@@ -164,11 +264,7 @@ class CAConstString
|
|
|
164
264
|
# @return [CAConstString, Array] the most frequent string(s).
|
|
165
265
|
def mode (axis: nil)
|
|
166
266
|
r = to_string.mode(axis: axis)
|
|
167
|
-
|
|
168
|
-
when CArray then r.to_const_string
|
|
169
|
-
when Array then r.map { |c| c.is_a?(CArray) ? c.to_const_string : c }
|
|
170
|
-
else r
|
|
171
|
-
end
|
|
267
|
+
r.is_a?(Array) ? r.map { |c| const_string_lift(c) } : const_string_lift(r)
|
|
172
268
|
end
|
|
173
269
|
|
|
174
270
|
# @overload is_mode(axis: nil)
|
|
@@ -180,7 +276,7 @@ class CAConstString
|
|
|
180
276
|
# @overload mask_duplicates(axis: nil)
|
|
181
277
|
# @return [CAConstString] a copy with every repeat occurrence masked.
|
|
182
278
|
def mask_duplicates (axis: nil)
|
|
183
|
-
to_string.mask_duplicates(axis: axis)
|
|
279
|
+
const_string_lift(to_string.mask_duplicates(axis: axis))
|
|
184
280
|
end
|
|
185
281
|
|
|
186
282
|
# @overload is_in(values)
|
|
@@ -192,19 +288,19 @@ class CAConstString
|
|
|
192
288
|
# @overload intersection(other, sort: false)
|
|
193
289
|
# @return [CAConstString] the distinct strings present in both.
|
|
194
290
|
def intersection (other, sort: false)
|
|
195
|
-
to_string.intersection(string_operand(other), sort: sort)
|
|
291
|
+
const_string_lift(to_string.intersection(string_operand(other), sort: sort))
|
|
196
292
|
end
|
|
197
293
|
|
|
198
294
|
# @overload difference(other, sort: false)
|
|
199
295
|
# @return [CAConstString] the distinct strings only `self` has.
|
|
200
296
|
def difference (other, sort: false)
|
|
201
|
-
to_string.difference(string_operand(other), sort: sort)
|
|
297
|
+
const_string_lift(to_string.difference(string_operand(other), sort: sort))
|
|
202
298
|
end
|
|
203
299
|
|
|
204
300
|
# @overload union(other, sort: false)
|
|
205
301
|
# @return [CAConstString] the distinct strings of either side.
|
|
206
302
|
def union (other, sort: false)
|
|
207
|
-
to_string.union(string_operand(other), sort: sort)
|
|
303
|
+
const_string_lift(to_string.union(string_operand(other), sort: sort))
|
|
208
304
|
end
|
|
209
305
|
|
|
210
306
|
# @overload locate_addr(ref)
|
|
@@ -225,4 +321,12 @@ class CAConstString
|
|
|
225
321
|
other.is_a?(CAConstString) ? other.to_string : other
|
|
226
322
|
end
|
|
227
323
|
|
|
324
|
+
# Pack a string-bearing result back into a column of this one's encoding.
|
|
325
|
+
# The encoding has to be carried: the builder checks each element against
|
|
326
|
+
# the column's, so a Shift_JIS column rebuilt as the UTF-8 default raised
|
|
327
|
+
# rather than coming back.
|
|
328
|
+
private def const_string_lift (x)
|
|
329
|
+
x.is_a?(CArray) ? CArray.const_string(x, encoding: encoding) : x
|
|
330
|
+
end
|
|
331
|
+
|
|
228
332
|
end
|
data/lib/carray/construct.rb
CHANGED
|
@@ -256,6 +256,46 @@ class CArray
|
|
|
256
256
|
TypeSymbol = nil
|
|
257
257
|
# @!visibility private
|
|
258
258
|
DataType = nil
|
|
259
|
+
|
|
260
|
+
# @overload empty(data_type, dim, bytes: nil)
|
|
261
|
+
# Returns a new CArray of `data_type` with the shape `dim`, whose
|
|
262
|
+
# contents are **undefined**. This is {CArray.new} without the fill:
|
|
263
|
+
# the arguments are read the same way, and the cost of zeroing the
|
|
264
|
+
# buffer is not paid. Use it when every cell is written before it is
|
|
265
|
+
# read; otherwise use `CArray.new`, which zero-fills.
|
|
266
|
+
#
|
|
267
|
+
# `CA_OBJECT` is the exception: its cells are zero-initialised
|
|
268
|
+
# anyway, because the garbage collector walks them.
|
|
269
|
+
#
|
|
270
|
+
# @param data_type [Symbol, String, Integer] element type.
|
|
271
|
+
# @param dim [Array<Integer>] shape of the new CArray.
|
|
272
|
+
# @param bytes [Integer, nil] element width, for `:fixlen` only.
|
|
273
|
+
# @return [CArray]
|
|
274
|
+
# @raise [ArgumentError] if a block is given -- filling an array is
|
|
275
|
+
# what `CArray.new` is for.
|
|
276
|
+
# @overload empty(*shape)
|
|
277
|
+
# (Numo / NumPy compatibility) Returns an uninitialised CArray of
|
|
278
|
+
# the given shape. The element type comes from the receiver, so
|
|
279
|
+
# `CArray` itself gives `CA_FLOAT64`; see
|
|
280
|
+
# {DataTypeExtension#empty}.
|
|
281
|
+
# @param shape [Array<Integer>, Array<Array<Integer>>] shape.
|
|
282
|
+
# @return [CArray]
|
|
283
|
+
def self.empty (*args, **options, &block)
|
|
284
|
+
if block
|
|
285
|
+
raise ArgumentError,
|
|
286
|
+
"CArray.empty does not take a block " \
|
|
287
|
+
"(its contents are left undefined); use CArray.new to fill."
|
|
288
|
+
end
|
|
289
|
+
if args.size == 2 and args[1].is_a?(Array)
|
|
290
|
+
return __empty__(*args, **options)
|
|
291
|
+
end
|
|
292
|
+
unless options.empty?
|
|
293
|
+
raise ArgumentError,
|
|
294
|
+
"CArray.empty(*shape) takes no options; bytes: belongs to " \
|
|
295
|
+
"CArray.empty(data_type, dim)"
|
|
296
|
+
end
|
|
297
|
+
super(*args)
|
|
298
|
+
end
|
|
259
299
|
end
|
|
260
300
|
|
|
261
301
|
class CArray
|
|
@@ -237,6 +237,9 @@ class CArray
|
|
|
237
237
|
# **uninitialised**. The caller must overwrite the array
|
|
238
238
|
# before reading from it. `CA_OBJECT` silently falls back to
|
|
239
239
|
# a zero-VALUE init required for GC safety.
|
|
240
|
+
#
|
|
241
|
+
# carray's own spelling is `CArray.empty(data_type, dim)`, which
|
|
242
|
+
# names the element type instead of taking it from the receiver.
|
|
240
243
|
# @param shape [Array<Integer>, Array<Array<Integer>>]
|
|
241
244
|
# @return [CArray]
|
|
242
245
|
def empty (*args)
|
|
@@ -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
|
#
|
|
@@ -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
|
|
@@ -483,6 +524,19 @@ class CAFrame
|
|
|
483
524
|
# and index are untouched; the selected cells of every column go to UNDEF.
|
|
484
525
|
# The selector is forwarded to the column indexer, which classifies it.
|
|
485
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?
|
|
486
540
|
@columns.each_value do |col|
|
|
487
541
|
col[selector, *([nil] * (col.ndim - 1))] = UNDEF
|
|
488
542
|
end
|
|
@@ -540,7 +594,16 @@ class CAFrame
|
|
|
540
594
|
new_index = splice_index(other, lo, hi)
|
|
541
595
|
@columns = new_cols
|
|
542
596
|
@index = new_index
|
|
543
|
-
|
|
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
|
|
544
607
|
self
|
|
545
608
|
end
|
|
546
609
|
|
|
@@ -609,8 +672,11 @@ class CAFrame
|
|
|
609
672
|
end
|
|
610
673
|
|
|
611
674
|
private def select_rows(selector, keep_masked: false)
|
|
612
|
-
if keep_masked && selector.is_a?(CArray) &&
|
|
613
|
-
|
|
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).
|
|
614
680
|
return select_rows_keep_masked(selector)
|
|
615
681
|
end
|
|
616
682
|
cols = {}
|
|
@@ -631,7 +697,12 @@ class CAFrame
|
|
|
631
697
|
g[undet_kept, *tail] = UNDEF
|
|
632
698
|
cols[name] = g
|
|
633
699
|
end
|
|
634
|
-
|
|
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
|
|
635
706
|
CAFrame.new(cols, axis_name: @axis_name, index: new_index)
|
|
636
707
|
end
|
|
637
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)
|