carray 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +5 -25
- data/CHANGELOG.md +16 -0
- data/LICENSE +1 -1
- data/NEWS.md +3 -0
- data/README.md +128 -44
- data/carray.gemspec +22 -24
- data/ext/ca_array_pool.c +91 -0
- data/ext/ca_axis_descriptor.h +186 -0
- data/ext/ca_axis_dispatch.c +924 -0
- data/ext/ca_axis_group.c +1208 -0
- data/ext/ca_bincmp_dispatch.c +76 -0
- data/ext/ca_bincmp_dispatch.h +85 -0
- data/ext/ca_binop_dispatch.c +125 -0
- data/ext/ca_binop_dispatch.h +159 -0
- data/ext/ca_categorical_iterator.c +1375 -0
- data/ext/ca_compare.c +94 -0
- data/ext/ca_compare.h +26 -0
- data/ext/ca_composite_dispatch.c +414 -0
- data/ext/ca_composite_dispatch.h +116 -0
- data/ext/ca_for_buffer.h +96 -0
- data/ext/ca_for_each_element.h +241 -0
- data/ext/ca_group_iter.c +304 -0
- data/ext/ca_iter_substrate.h +325 -0
- data/ext/ca_kernel_iterator.c +4321 -0
- data/ext/ca_kernel_iterator.h +2603 -0
- data/ext/ca_moncmp_dispatch.c +37 -0
- data/ext/ca_moncmp_dispatch.h +62 -0
- data/ext/ca_monop_dispatch.c +200 -0
- data/ext/ca_monop_dispatch.h +235 -0
- data/ext/ca_obj_array.c +355 -359
- data/ext/ca_obj_bincmp.c +809 -0
- data/ext/ca_obj_binop.c +892 -0
- data/ext/ca_obj_bitarray.c +369 -164
- data/ext/ca_obj_bitfield.c +294 -234
- data/ext/ca_obj_block.c +189 -711
- data/ext/ca_obj_byte_swap.c +766 -0
- data/ext/ca_obj_const_string.c +965 -0
- data/ext/ca_obj_face.c +670 -0
- data/ext/ca_obj_face.h +247 -0
- data/ext/ca_obj_fake.c +228 -100
- data/ext/ca_obj_farray.c +54 -441
- data/ext/ca_obj_field.c +82 -529
- data/ext/ca_obj_fixlen_string.c +306 -0
- data/ext/ca_obj_grid.c +858 -440
- data/ext/ca_obj_meld.c +1034 -0
- data/ext/ca_obj_moncmp.c +569 -0
- data/ext/ca_obj_monop.c +1111 -0
- data/ext/ca_obj_object.c +774 -298
- data/ext/ca_obj_record.c +468 -0
- data/ext/ca_obj_reduce.c +97 -82
- data/ext/ca_obj_refer.c +569 -459
- data/ext/ca_obj_remap.c +475 -0
- data/ext/ca_obj_repeat.c +92 -477
- data/ext/ca_obj_roll.c +616 -0
- data/ext/ca_obj_select.c +344 -296
- data/ext/ca_obj_select_axis.c +1296 -0
- data/ext/ca_obj_shift.c +230 -792
- data/ext/ca_obj_source.c +78 -0
- data/ext/ca_obj_stack.c +1173 -0
- data/ext/ca_obj_stride.c +2501 -0
- data/ext/ca_obj_string.c +268 -0
- data/ext/ca_obj_tile.c +614 -0
- data/ext/ca_obj_time.c +546 -0
- data/ext/ca_obj_timedelta.c +435 -0
- data/ext/ca_obj_transpose.c +62 -516
- data/ext/ca_obj_triop.c +746 -0
- data/ext/ca_obj_unbound_repeat.c +208 -241
- data/ext/ca_obj_window.c +1131 -563
- data/ext/ca_op_byte_swap.c +175 -0
- data/ext/ca_op_ipower.c +319 -0
- data/ext/ca_op_powi.h +88 -0
- data/ext/ca_sort_kernels.h +132 -0
- data/ext/ca_sweep_engine.c +430 -0
- data/ext/ca_sweep_engine.h +157 -0
- data/ext/ca_transform_common.c +228 -0
- data/ext/ca_triop_dispatch.c +55 -0
- data/ext/ca_triop_dispatch.h +62 -0
- data/ext/carray.h +795 -402
- data/ext/carray_access.c +831 -711
- data/ext/carray_attribute.c +98 -330
- data/ext/carray_bincount.c +255 -0
- data/ext/carray_broadcast.c +283 -0
- data/ext/carray_call_cfunc.c +1360 -828
- data/ext/carray_call_cfunc.h +160 -0
- data/ext/carray_cast.c +1212 -301
- data/ext/carray_cast_func.rb +81 -40
- data/ext/carray_class.c +53 -63
- data/ext/carray_config.h +28 -0
- data/ext/carray_conversion.c +350 -346
- data/ext/carray_copy.c +156 -268
- data/ext/carray_core.c +1342 -199
- data/ext/carray_count.c +312 -0
- data/ext/carray_data_type.c +43 -19
- data/ext/carray_element.c +585 -213
- data/ext/carray_factorize.c +2542 -0
- data/ext/carray_generate.c +230 -559
- data/ext/carray_histogram.c +490 -0
- data/ext/carray_hold.c +228 -0
- data/ext/carray_index_classifier.c +1035 -0
- data/ext/carray_index_classifier.h +27 -0
- data/ext/carray_internal.h +120 -0
- data/ext/carray_kernels_bincmp.c +4445 -0
- data/ext/carray_kernels_binop.c +10979 -0
- data/ext/carray_kernels_init.c +36 -0
- data/ext/carray_kernels_map.c +3466 -0
- data/ext/carray_kernels_moncmp.c +2096 -0
- data/ext/carray_kernels_monop.c +18312 -0
- data/ext/carray_kernels_reduce_aggregate.c +25836 -0
- data/ext/carray_kernels_reduce_boolean.c +329 -0
- data/ext/carray_kernels_reduce_cumulative.c +14592 -0
- data/ext/carray_kernels_reduce_extreme.c +16947 -0
- data/ext/carray_kernels_reduce_variance.c +3909 -0
- data/ext/carray_kernels_scan.c +3692 -0
- data/ext/carray_kernels_search.c +32137 -0
- data/ext/carray_kernels_sort.c +10625 -0
- data/ext/carray_kernels_triop.c +1391 -0
- data/ext/carray_lazy.c +567 -0
- data/ext/carray_loop.c +88 -200
- data/ext/carray_mask.c +848 -154
- data/ext/carray_math_kernel.h +120 -0
- data/ext/carray_mathfunc.c +10 -241
- data/ext/carray_median_percentile.c +1257 -0
- data/ext/carray_memory_view.c +1625 -0
- data/ext/carray_operator.c +1526 -318
- data/ext/carray_order.c +664 -1394
- data/ext/carray_partition.c +416 -0
- data/ext/carray_random.c +518 -0
- data/ext/carray_scatter.c +357 -0
- data/ext/carray_slab.c +1219 -0
- data/ext/carray_slab.h +84 -0
- data/ext/carray_sort.c +829 -0
- data/ext/carray_sort_kernel.c +620 -0
- data/ext/carray_struct.c +695 -0
- data/ext/carray_test.c +343 -229
- data/ext/carray_undef.c +34 -17
- data/ext/carray_utils.c +175 -74
- data/ext/extconf.rb +216 -55
- data/ext/mk_call_cfunc.rb +480 -0
- data/ext/mkkernel.rb +8842 -0
- data/ext/ruby_carray.c +202 -101
- data/ext/version.h +4 -14
- data/ext/version.rb +5 -13
- data/lib/carray/arrow_tensor.rb +401 -0
- data/lib/carray/attribute.rb +166 -0
- data/lib/carray/autoload_carray.rb +220 -0
- data/lib/carray/autoload_method_extension.rb +44 -0
- data/lib/carray/axis_group.rb +711 -0
- data/lib/carray/basics.rb +481 -0
- data/lib/carray/bincount_nd.rb +358 -0
- data/lib/carray/block_iterator.rb +604 -0
- data/lib/carray/boolean_reduce.rb +109 -0
- data/lib/carray/categorical.rb +561 -0
- data/lib/carray/categorical_iterator.rb +1062 -0
- data/lib/carray/complex.rb +150 -0
- data/lib/carray/conditional.rb +216 -0
- data/lib/carray/const_string.rb +228 -0
- data/lib/carray/construct.rb +139 -328
- data/lib/carray/core_extensions.rb +240 -0
- data/lib/carray/data_type_extension.rb +233 -0
- data/lib/carray/fixlen_string.rb +95 -0
- data/lib/carray/frame/concat.rb +132 -0
- data/lib/carray/frame/convert.rb +95 -0
- data/lib/carray/frame/csv_parser.rb +211 -0
- data/lib/carray/frame/frame.rb +649 -0
- data/lib/carray/frame/group.rb +186 -0
- data/lib/carray/frame/io.rb +164 -0
- data/lib/carray/frame/join.rb +248 -0
- data/lib/carray/frame/records.rb +99 -0
- data/lib/carray/frame/sort.rb +113 -0
- data/lib/carray/frame/verbs.rb +299 -0
- data/lib/carray/frame.rb +16 -0
- data/lib/carray/histogram.rb +512 -0
- data/lib/carray/inspect.rb +37 -20
- data/lib/carray/iterator.rb +57 -349
- data/lib/carray/lazy.rb +889 -0
- data/lib/carray/mask_gap_fill.rb +200 -0
- data/lib/carray/math.rb +78 -342
- data/lib/carray/meld_reduce.rb +289 -0
- data/lib/carray/methods/align_addr.rb +116 -0
- data/lib/carray/methods/bin.rb +128 -0
- data/lib/carray/methods/bincount.rb +87 -0
- data/lib/carray/methods/bit_string.rb +92 -0
- data/lib/carray/methods/broadcast.rb +63 -0
- data/lib/carray/methods/choose.rb +39 -0
- data/lib/carray/methods/composition.rb +280 -0
- data/lib/carray/methods/gather_nd.rb +206 -0
- data/lib/carray/methods/index.rb +39 -0
- data/lib/carray/methods/insert_block.rb +99 -0
- data/lib/carray/methods/is_in.rb +141 -0
- data/lib/carray/methods/join.rb +90 -0
- data/lib/carray/methods/locate_addr.rb +47 -0
- data/lib/carray/methods/mask_duplicates.rb +41 -0
- data/lib/carray/methods/meshgrid.rb +91 -0
- data/lib/carray/methods/mode.rb +126 -0
- data/lib/carray/methods/nunique.rb +46 -0
- data/lib/carray/methods/resize.rb +56 -0
- data/lib/carray/methods/snap.rb +156 -0
- data/lib/carray/methods/string_format.rb +57 -0
- data/lib/carray/methods/unique.rb +47 -0
- data/lib/carray/methods/value_counts.rb +71 -0
- data/lib/carray/mkmf.rb +124 -101
- data/lib/carray/runtime.rb +108 -0
- data/lib/carray/serialize.rb +478 -167
- data/lib/carray/slab_iterator.rb +292 -0
- data/lib/carray/stack.rb +291 -0
- data/lib/carray/string.rb +56 -180
- data/lib/carray/string_operation_extension.rb +289 -0
- data/lib/carray/struct.rb +335 -323
- data/lib/carray/struct_builder.rb +697 -0
- data/lib/carray/table.rb +41 -2
- data/lib/carray/time.rb +2255 -38
- data/lib/carray/window_iterator.rb +655 -0
- data/lib/carray.rb +55 -57
- metadata +163 -130
- data/Rakefile +0 -51
- data/TODO.md +0 -18
- data/ext/ca_iter_block.c +0 -257
- data/ext/ca_iter_dimension.c +0 -299
- data/ext/ca_iter_window.c +0 -214
- data/ext/ca_obj_mapping.c +0 -644
- data/ext/carray_iterator.c +0 -641
- data/ext/carray_math.rb +0 -850
- data/ext/carray_numeric.c +0 -259
- data/ext/carray_sort_addr.c +0 -254
- data/ext/carray_stat.c +0 -2100
- data/ext/carray_stat_proc.rb +0 -1999
- data/ext/mkmath.rb +0 -741
- data/ext/ruby_ccomplex.c +0 -509
- data/ext/ruby_float_func.c +0 -86
- data/lib/carray/array.rb +0 -8
- data/lib/carray/autoload/autoload_base.rb +0 -19
- data/lib/carray/autoload/autoload_gem_cairo.rb +0 -9
- data/lib/carray/autoload/autoload_gem_ffi.rb +0 -9
- data/lib/carray/autoload/autoload_gem_gnuplot.rb +0 -2
- data/lib/carray/autoload/autoload_gem_io_csv.rb +0 -14
- data/lib/carray/autoload/autoload_gem_io_pg.rb +0 -6
- data/lib/carray/autoload/autoload_gem_io_sqlite3.rb +0 -12
- data/lib/carray/autoload/autoload_gem_narray.rb +0 -10
- data/lib/carray/autoload/autoload_gem_numo_narray.rb +0 -15
- data/lib/carray/autoload/autoload_gem_opencv.rb +0 -16
- data/lib/carray/autoload/autoload_gem_random.rb +0 -8
- data/lib/carray/autoload/autoload_gem_rmagick.rb +0 -23
- data/lib/carray/autoload/autoload_gem_zimg.rb +0 -3
- data/lib/carray/autoload/autoload_io_imagemagick.rb +0 -6
- data/lib/carray/autoload/autoload_math_histogram.rb +0 -5
- data/lib/carray/autoload/autoload_math_recurrence.rb +0 -6
- data/lib/carray/autoload/autoload_object_iterator.rb +0 -1
- data/lib/carray/autoload/autoload_object_link.rb +0 -1
- data/lib/carray/autoload/autoload_object_pack.rb +0 -2
- data/lib/carray/autoload.rb +0 -141
- data/lib/carray/basic.rb +0 -191
- data/lib/carray/broadcast.rb +0 -101
- data/lib/carray/compose.rb +0 -315
- data/lib/carray/convert.rb +0 -115
- data/lib/carray/info.rb +0 -110
- data/lib/carray/io/imagemagick.rb +0 -235
- data/lib/carray/mask.rb +0 -102
- data/lib/carray/math/histogram.rb +0 -177
- data/lib/carray/math/recurrence.rb +0 -93
- data/lib/carray/object/ca_obj_iterator.rb +0 -50
- data/lib/carray/object/ca_obj_link.rb +0 -50
- data/lib/carray/object/ca_obj_pack.rb +0 -99
- data/lib/carray/obsolete.rb +0 -256
- data/lib/carray/ordering.rb +0 -181
- data/lib/carray/testing.rb +0 -51
- data/lib/carray/transform.rb +0 -109
- data/misc/Methods.ja.md +0 -182
- data/misc/NOTE +0 -51
- data/spec/Classes/CABitfield_spec.rb +0 -58
- data/spec/Classes/CABlockIterator_spec.rb +0 -114
- data/spec/Classes/CABlock_spec.rb +0 -205
- data/spec/Classes/CAField_spec.rb +0 -39
- data/spec/Classes/CAGrid_spec.rb +0 -75
- data/spec/Classes/CAMap_spec.rb +0 -0
- data/spec/Classes/CAMapping_spec.rb +0 -105
- data/spec/Classes/CAObject_attribute_spec.rb +0 -33
- data/spec/Classes/CAObject_spec.rb +0 -33
- data/spec/Classes/CARefer_spec.rb +0 -93
- data/spec/Classes/CARepeat_spec.rb +0 -65
- data/spec/Classes/CASelect_spec.rb +0 -22
- data/spec/Classes/CAShift_spec.rb +0 -16
- data/spec/Classes/CAStruct_spec.rb +0 -71
- data/spec/Classes/CATranspose_spec.rb +0 -60
- data/spec/Classes/CAUnboudRepeat_spec.rb +0 -102
- data/spec/Classes/CAWindow_spec.rb +0 -54
- data/spec/Classes/CAWrap_spec.rb +0 -8
- data/spec/Classes/CArray_spec.rb +0 -184
- data/spec/Classes/CScalar_spec.rb +0 -55
- data/spec/Classes/ex1.rb +0 -46
- data/spec/Features/feature_130_spec.rb +0 -19
- data/spec/Features/feature_attributes_spec.rb +0 -280
- data/spec/Features/feature_boolean_spec.rb +0 -98
- data/spec/Features/feature_broadcast.rb +0 -116
- data/spec/Features/feature_cast_function.rb +0 -19
- data/spec/Features/feature_cast_spec.rb +0 -33
- data/spec/Features/feature_class_spec.rb +0 -84
- data/spec/Features/feature_complex_spec.rb +0 -42
- data/spec/Features/feature_composite_spec.rb +0 -124
- data/spec/Features/feature_convert_spec.rb +0 -46
- data/spec/Features/feature_copy_spec.rb +0 -123
- data/spec/Features/feature_creation_spec.rb +0 -84
- data/spec/Features/feature_element_spec.rb +0 -144
- data/spec/Features/feature_extream_spec.rb +0 -54
- data/spec/Features/feature_generate_spec.rb +0 -74
- data/spec/Features/feature_index_spec.rb +0 -69
- data/spec/Features/feature_mask_spec.rb +0 -580
- data/spec/Features/feature_math_spec.rb +0 -97
- data/spec/Features/feature_order_spec.rb +0 -146
- data/spec/Features/feature_ref_store_spec.rb +0 -209
- data/spec/Features/feature_serialization_spec.rb +0 -125
- data/spec/Features/feature_stat_spec.rb +0 -397
- data/spec/Features/feature_virtual_spec.rb +0 -48
- data/spec/Features/method_eq_spec.rb +0 -81
- data/spec/Features/method_is_nan_spec.rb +0 -12
- data/spec/Features/method_map_spec.rb +0 -54
- data/spec/Features/method_max_with.rb +0 -20
- data/spec/Features/method_min_with.rb +0 -19
- data/spec/Features/method_ne_spec.rb +0 -18
- data/spec/Features/method_project_spec.rb +0 -188
- data/spec/Features/method_ref_spec.rb +0 -27
- data/spec/Features/method_round_spec.rb +0 -11
- data/spec/Features/method_s_linspace_spec.rb +0 -48
- data/spec/Features/method_s_span_spec.rb +0 -14
- data/spec/Features/method_seq_spec.rb +0 -47
- data/spec/Features/method_sort_with.rb +0 -43
- data/spec/Features/method_sorted_with.rb +0 -29
- data/spec/Features/method_span_spec.rb +0 -42
- data/spec/Features/method_wrap_readonly_spec.rb +0 -43
- data/spec/UnitTest/test_CAVirtual.rb +0 -214
- data/spec/spec_all.rb +0 -10
- data/utils/ca_ase.rb +0 -21
- data/utils/ca_methods.rb +0 -15
- data/utils/cast_checker.rb +0 -30
- data/utils/convert_test.rb +0 -73
- data/utils/extract_yard.rb +0 -22
- data/utils/guess_shape.rb +0 -76
- data/utils/monkey_patch_methods.rb +0 -62
- data/utils/remove_resource_fork.sh +0 -5
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# CAFrame group_by (memo §5 spine, §11.6 surface).
|
|
2
|
+
#
|
|
3
|
+
# Grouping is always on the row axis (axis 0). The key is any length-N
|
|
4
|
+
# thing: a column name, several column names (composite key), or an
|
|
5
|
+
# external length-N CArray. Everything routes through categorize -> codes
|
|
6
|
+
# -> group_by_category, so the frame layer only builds the key and hands
|
|
7
|
+
# the group iterator back (exposure, §4.3).
|
|
8
|
+
|
|
9
|
+
class CAFrame
|
|
10
|
+
# Group rows by one or more keys. Each key is a column name (String) or an
|
|
11
|
+
# external length-N CArray. Returns a GroupedFrame.
|
|
12
|
+
def group_by(*keys)
|
|
13
|
+
raise ArgumentError, "group_by needs at least one key" if keys.empty?
|
|
14
|
+
cat = grouping_categorical(keys)
|
|
15
|
+
axis = keys.size == 1 && keys.first.is_a?(String) ? keys.first : "group"
|
|
16
|
+
GroupedFrame.new(self, cat, axis)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Number of rows currently selected — used by group per-group view-frames
|
|
20
|
+
# and elsewhere; already provided by the core (attr_reader :nrow).
|
|
21
|
+
|
|
22
|
+
private def grouping_categorical(keys)
|
|
23
|
+
cols = keys.map { |k| key_column(k) }
|
|
24
|
+
if cols.size == 1
|
|
25
|
+
cols.first.categorize
|
|
26
|
+
else
|
|
27
|
+
# Composite key: one object cell per row holding the tuple of key
|
|
28
|
+
# values, categorized by content (the codes are composed from the
|
|
29
|
+
# per-column keys).
|
|
30
|
+
n = nrow
|
|
31
|
+
CArray.object(n) { |i| cols.map { |c| c[i] } }.categorize
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private def key_column(key)
|
|
36
|
+
case key
|
|
37
|
+
when String
|
|
38
|
+
self[key]
|
|
39
|
+
when CArray
|
|
40
|
+
unless key.shape[0] == nrow
|
|
41
|
+
raise ArgumentError,
|
|
42
|
+
"external group key length #{key.shape[0]} != nrow #{nrow}"
|
|
43
|
+
end
|
|
44
|
+
key
|
|
45
|
+
else
|
|
46
|
+
raise ArgumentError, "group key must be a column name or CArray (got #{key.class})"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# GroupedFrame — the result of +CAFrame#group_by+ (memo §11.6, §16).
|
|
52
|
+
#
|
|
53
|
+
# Holds the grouping categorical once (grouping is per-key, computed once
|
|
54
|
+
# and shared across every column). Three surfaces:
|
|
55
|
+
# grp["col"] -> the CArray group iterator (raw exposure)
|
|
56
|
+
# aggregate -> declarative per-column reductions into a new frame
|
|
57
|
+
# table { |g| }-> cross-column Ruby escape, g is a per-group view-frame
|
|
58
|
+
class GroupedFrame
|
|
59
|
+
def initialize(frame, cat, axis_name)
|
|
60
|
+
@frame = frame
|
|
61
|
+
@cat = cat
|
|
62
|
+
@axis_name = axis_name
|
|
63
|
+
@labels = cat.labels # group values, in code order
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Number of groups.
|
|
67
|
+
def ngroup
|
|
68
|
+
@labels.size
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Group key values (one per group), as an Array.
|
|
72
|
+
def labels
|
|
73
|
+
@labels.dup
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Raw exposure: the CArray group iterator for one column (memo §11.6).
|
|
77
|
+
def [](name)
|
|
78
|
+
@frame[name].group_by_category(@cat)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Declarative aggregation (memo §11.6). Spec maps an output column name to
|
|
82
|
+
# +[input_column, reduction]+, where reduction is a Symbol (vectorized,
|
|
83
|
+
# applied through the group iterator) or a Proc (per-group custom, called
|
|
84
|
+
# with the group's column slice).
|
|
85
|
+
def aggregate(spec)
|
|
86
|
+
cols = {}
|
|
87
|
+
spec.each do |out_name, (in_name, reduction)|
|
|
88
|
+
out = out_name.to_s
|
|
89
|
+
cols[out] =
|
|
90
|
+
case reduction
|
|
91
|
+
when Symbol
|
|
92
|
+
self[in_name].public_send(reduction)
|
|
93
|
+
when Proc
|
|
94
|
+
per_group_column(in_name, reduction)
|
|
95
|
+
else
|
|
96
|
+
raise ArgumentError,
|
|
97
|
+
"reduction must be a Symbol or Proc (got #{reduction.class})"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
CAFrame.new(cols, axis_name: @axis_name, index: label_index)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Cross-column Ruby escape (memo §11.6). The block receives a per-group
|
|
104
|
+
# view-frame and returns a Hash of output-name => value; the values are
|
|
105
|
+
# collected column-wise across groups into a new frame.
|
|
106
|
+
def table
|
|
107
|
+
collected = {}
|
|
108
|
+
order = nil
|
|
109
|
+
each_group_frame do |g|
|
|
110
|
+
out = yield(g)
|
|
111
|
+
unless out.is_a?(Hash)
|
|
112
|
+
raise ArgumentError, "table block must return a Hash (got #{out.class})"
|
|
113
|
+
end
|
|
114
|
+
order ||= out.keys.map(&:to_s)
|
|
115
|
+
out.each { |k, v| (collected[k.to_s] ||= []) << v }
|
|
116
|
+
end
|
|
117
|
+
cols = {}
|
|
118
|
+
(order || []).each do |name|
|
|
119
|
+
vals = collected[name]
|
|
120
|
+
cols[name] = CArray.object(vals.size) { |i| vals[i] }
|
|
121
|
+
end
|
|
122
|
+
CAFrame.new(cols, axis_name: @axis_name, index: label_index)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Convenience reductions over every numeric scalar column (memo §6-4
|
|
126
|
+
# "grp.mean"). Non-numeric / N-D columns are skipped.
|
|
127
|
+
[:sum, :mean, :min, :max].each do |red|
|
|
128
|
+
define_method(red) { reduce_numeric(red) }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @return [String]
|
|
132
|
+
def inspect
|
|
133
|
+
"#<GroupedFrame ngroup=#{ngroup} by=#{@axis_name.inspect}>"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private def label_index
|
|
137
|
+
CArray.object(@labels.size) { |i| @labels[i] }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
NON_NUMERIC = [:object, :boolean, :fixlen].freeze
|
|
141
|
+
private_constant :NON_NUMERIC
|
|
142
|
+
|
|
143
|
+
private def reduce_numeric(reduction)
|
|
144
|
+
cols = {}
|
|
145
|
+
@frame.variable_names.each do |name|
|
|
146
|
+
col = @frame[name]
|
|
147
|
+
next unless col.ndim == 1 && !NON_NUMERIC.include?(col.data_type)
|
|
148
|
+
cols[name] = col.group_by_category(@cat).public_send(reduction)
|
|
149
|
+
end
|
|
150
|
+
CAFrame.new(cols, axis_name: @axis_name, index: label_index)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private def per_group_column(in_name, proc)
|
|
154
|
+
results = []
|
|
155
|
+
each_group_slice(in_name) { |slice| results << proc.call(slice) }
|
|
156
|
+
CArray.object(results.size) { |i| results[i] }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private def each_group_slice(in_name)
|
|
160
|
+
col = @frame[in_name]
|
|
161
|
+
ngroup.times do |k|
|
|
162
|
+
yield col[group_address(k), *([nil] * (col.ndim - 1))]
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private def each_group_frame
|
|
167
|
+
ngroup.times { |k| yield @frame[group_address(k)] }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
private def group_address(k)
|
|
171
|
+
start = group_offsets[k]
|
|
172
|
+
group_perm[start...(start + group_sizes[k])]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private def group_perm
|
|
176
|
+
@group_perm ||= @cat.sort_addr
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private def group_offsets
|
|
180
|
+
@group_offsets ||= @cat.reduceat_index
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private def group_sizes
|
|
184
|
+
@group_sizes ||= @cat.category_sizes
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# CAFrame CSV input/output (memo §11.2, §6-2).
|
|
2
|
+
|
|
3
|
+
require "carray/frame/csv_parser"
|
|
4
|
+
|
|
5
|
+
class CAFrame
|
|
6
|
+
# Read a CSV into a frame. The header row supplies column names (Strings,
|
|
7
|
+
# §3.7); every column is built raw as an object CArray of the cell strings
|
|
8
|
+
# (§4.2 -- read and arrange only, no type-inference engine). Casting is a
|
|
9
|
+
# separate step: pass +types:+ ({ "temp" => :float64 }, or the array-key /
|
|
10
|
+
# reverse forms of +cast+) to cast named columns on load, or call +cast+
|
|
11
|
+
# later. Broken cells fail to_type and become UNDEF automatically
|
|
12
|
+
# (parse-mask, §6-2).
|
|
13
|
+
#
|
|
14
|
+
# Parsing uses the built-in fast tokenizer (CSVParser). Options:
|
|
15
|
+
# sep: field separator (default ",")
|
|
16
|
+
# quote: quote character (default '"')
|
|
17
|
+
# strip: trim spaces from unquoted fields (default false, RFC spacing)
|
|
18
|
+
# +encoding+: IO open-mode encoding (default "bom|utf-8", strips a BOM)
|
|
19
|
+
# parser: a callable path -> [headers, rows] to inject another parser
|
|
20
|
+
# (e.g. the stdlib +csv+, or a typed-table source); when given,
|
|
21
|
+
# sep/quote/strip/encoding and any block are that parser's concern.
|
|
22
|
+
#
|
|
23
|
+
# A block gives reading control for files with preamble lines, a units row,
|
|
24
|
+
# or no header (memo §11.2), using +skip+ / +header+ / +column_names+ /
|
|
25
|
+
# +body+ (see CSVReader). Without a block the default is +header+ then +body+.
|
|
26
|
+
#
|
|
27
|
+
# CAFrame.from_csv("obs.csv") do
|
|
28
|
+
# skip 2; header; skip 1; body
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# Columns are handed to the frame as CABlock views over one backing object
|
|
32
|
+
# array (§3.6 view-by-default); casting a column materializes it, and +copy+
|
|
33
|
+
# gives an independent frame.
|
|
34
|
+
def self.from_csv(path, types: nil,
|
|
35
|
+
sep: ",", quote: '"', strip: false,
|
|
36
|
+
encoding: "bom|utf-8", parser: nil, &block)
|
|
37
|
+
names, rows =
|
|
38
|
+
if parser
|
|
39
|
+
parser.call(path)
|
|
40
|
+
else
|
|
41
|
+
File.open(path, "r:#{encoding}") do |io|
|
|
42
|
+
reader = CSVReader.new(io, sep: sep, quote: quote, strip: strip)
|
|
43
|
+
if block
|
|
44
|
+
block.arity == 1 ? block.call(reader) : reader.instance_exec(&block)
|
|
45
|
+
else
|
|
46
|
+
reader.header
|
|
47
|
+
reader.body
|
|
48
|
+
end
|
|
49
|
+
reader.result
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
frame = build_frame(names, rows)
|
|
54
|
+
frame.cast(types) if types
|
|
55
|
+
frame
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Build a frame from parsed [names, rows]. When names is nil (headerless and
|
|
59
|
+
# no column_names) positional names "c0".."cN" are generated from the widest
|
|
60
|
+
# row. Rows are squared off to the column count (short rows padded with nil,
|
|
61
|
+
# over-long rows raise), one 2-D object array is bulk-filled, and each column
|
|
62
|
+
# is a view into it (§3.6).
|
|
63
|
+
# Write the frame as CSV. CSV is a flat table of scalar cells, so this is the
|
|
64
|
+
# text form of the same all-scalar subset +to_ca+ requires (§11.9): every
|
|
65
|
+
# column must be 1-D. Unlike +to_ca+ it does not promote to a common dtype --
|
|
66
|
+
# each column is formatted to text independently, so mixed dtypes (numbers,
|
|
67
|
+
# strings, datetime / categorical Faces) sit side by side. An N-D column has
|
|
68
|
+
# no flat CSV cell and raises; export it per column, or use +to_records+ +
|
|
69
|
+
# JSON for the structured shape (memo §11.9, the N-D escape).
|
|
70
|
+
#
|
|
71
|
+
# With +path+, writes the file and returns self; without it, returns the CSV
|
|
72
|
+
# String. The index (if any) is written as the first column under +axis_name+
|
|
73
|
+
# unless +index: false+. A masked cell (UNDEF) becomes an empty field, which
|
|
74
|
+
# +from_csv+ reads back as UNDEF (parse-mask, §6-2) -- so mask round-trips. A
|
|
75
|
+
# genuine empty string is written quoted (+""+) to stay distinct from missing,
|
|
76
|
+
# matching the tokenizer's own unquoted-empty vs quoted-empty distinction.
|
|
77
|
+
#
|
|
78
|
+
# df.to_csv("out.csv") # write file
|
|
79
|
+
# csv = df.to_csv # get a String
|
|
80
|
+
#
|
|
81
|
+
# Options: +sep+ / +quote+ mirror +from_csv+; +header+ writes the name row
|
|
82
|
+
# (default true); +index+ writes the index column (default true).
|
|
83
|
+
def to_csv(path = nil, sep: ",", quote: '"', header: true, index: true)
|
|
84
|
+
nd = @columns.find { |_, c| c.ndim != 1 }
|
|
85
|
+
if nd
|
|
86
|
+
raise ArgumentError,
|
|
87
|
+
"to_csv needs all-scalar (1-D) columns; #{nd.first.inspect} is " \
|
|
88
|
+
"#{nd.last.ndim}-D — export it per column or via to_records + JSON"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
names = []
|
|
92
|
+
formatted = []
|
|
93
|
+
if index && @index
|
|
94
|
+
names << @axis_name
|
|
95
|
+
formatted << format_csv_column(@index)
|
|
96
|
+
end
|
|
97
|
+
@columns.each do |name, col|
|
|
98
|
+
names << name
|
|
99
|
+
formatted << format_csv_column(col)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
out = +""
|
|
103
|
+
if header
|
|
104
|
+
out << names.map { |t| quote_csv_field(t, sep, quote) }.join(sep) << "\n"
|
|
105
|
+
end
|
|
106
|
+
@nrow.times do |i|
|
|
107
|
+
out << formatted.map { |fcol| quote_csv_field(fcol[i], sep, quote) }.join(sep) << "\n"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if path
|
|
111
|
+
File.write(path, out)
|
|
112
|
+
self
|
|
113
|
+
else
|
|
114
|
+
out
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private def format_csv_column(col)
|
|
119
|
+
col.to_a.map do |e|
|
|
120
|
+
if UNDEF.equal?(e) || e.nil?
|
|
121
|
+
nil
|
|
122
|
+
elsif e.is_a?(String)
|
|
123
|
+
e
|
|
124
|
+
elsif e.respond_to?(:iso8601)
|
|
125
|
+
e.iso8601
|
|
126
|
+
else
|
|
127
|
+
e.to_s
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private def quote_csv_field(text, sep, quote)
|
|
133
|
+
return "" if text.nil?
|
|
134
|
+
if text.empty? || text.include?(sep) || text.include?(quote) ||
|
|
135
|
+
text.include?("\n") || text.include?("\r")
|
|
136
|
+
quote + text.gsub(quote, quote * 2) + quote
|
|
137
|
+
else
|
|
138
|
+
text
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.build_frame(names, rows)
|
|
143
|
+
ncol = names ? names.size : (rows.map(&:size).max || 0)
|
|
144
|
+
names ||= Array.new(ncol) { |j| "c#{j}" }
|
|
145
|
+
|
|
146
|
+
cols = {}
|
|
147
|
+
if rows.empty?
|
|
148
|
+
names.each { |name| cols[name] = CArray.object(0) }
|
|
149
|
+
return new(cols)
|
|
150
|
+
end
|
|
151
|
+
rows.each_with_index do |r, i|
|
|
152
|
+
if r.size < ncol
|
|
153
|
+
r.concat(Array.new(ncol - r.size))
|
|
154
|
+
elsif r.size > ncol
|
|
155
|
+
raise ArgumentError,
|
|
156
|
+
"row #{i + 1} has #{r.size} fields, expected #{ncol}"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
table = CArray.object(rows.size, ncol) { rows }
|
|
160
|
+
names.each_with_index { |name, j| cols[name] = table[nil, j] }
|
|
161
|
+
new(cols)
|
|
162
|
+
end
|
|
163
|
+
private_class_method :build_frame
|
|
164
|
+
end
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# CAFrame join (memo §5, §11.7, §13.3).
|
|
2
|
+
#
|
|
3
|
+
# Join is a thin delegation to landed addressing primitives: the join key
|
|
4
|
+
# yields an address array (locate_addr for a row-preserving left lookup,
|
|
5
|
+
# align_addr for symmetric set alignment), and each column is gathered by
|
|
6
|
+
# +project+ (length-preserving, miss -> UNDEF, Face-lift, read-only safe).
|
|
7
|
+
# The frame layer just distributes that address to every column.
|
|
8
|
+
|
|
9
|
+
class CAFrame
|
|
10
|
+
# Default disambiguation for non-key columns present on both sides (§12-C):
|
|
11
|
+
# left gets "_left", right gets "_right". +_left+/+_right+ read clearer than
|
|
12
|
+
# pandas' +_x+/+_y+ for the target audience.
|
|
13
|
+
DEFAULT_JOIN_SUFFIXES = ["_left", "_right"].freeze
|
|
14
|
+
private_constant :DEFAULT_JOIN_SUFFIXES
|
|
15
|
+
|
|
16
|
+
# Join +other+ on a shared key column.
|
|
17
|
+
#
|
|
18
|
+
# how: :left (default) — keep all left rows; right columns gathered per
|
|
19
|
+
# left row (locate_addr), misses become UNDEF.
|
|
20
|
+
# how: :inner/:outer/:right — set-align both key sets (align_addr) and
|
|
21
|
+
# gather both sides; the aligned key values form
|
|
22
|
+
# the +on+ column.
|
|
23
|
+
#
|
|
24
|
+
# A non-key column present on both sides collides. The key column (+on+) is
|
|
25
|
+
# kept once, never suffixed. By default the collision is resolved by suffixing
|
|
26
|
+
# both sides (+suffixes: ["_left", "_right"]+); pass a 2-element array to pick
|
|
27
|
+
# meaningful names up front (+["_obs", "_fcst"]+), or +suffixes: false+ to
|
|
28
|
+
# raise instead. Rename afterward with +rename+ if needed (§12-C).
|
|
29
|
+
def join(other, on:, how: :left, suffixes: DEFAULT_JOIN_SUFFIXES)
|
|
30
|
+
on = on.to_s
|
|
31
|
+
lkey = self[on]
|
|
32
|
+
rkey = other[on]
|
|
33
|
+
plan = join_name_plan(other, on, suffixes)
|
|
34
|
+
|
|
35
|
+
case how
|
|
36
|
+
when :left
|
|
37
|
+
join_left(other, on, lkey, rkey, plan)
|
|
38
|
+
when :inner, :outer, :right
|
|
39
|
+
join_align(other, on, lkey, rkey, how, plan)
|
|
40
|
+
else
|
|
41
|
+
raise ArgumentError, "unknown join mode #{how.inspect}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# As-of (nearest-key) left join for irregular series (memo §11.7). Each
|
|
46
|
+
# left row is matched to the nearest +other+ row by +on+ via
|
|
47
|
+
# locate_nearest_addr (the per-row, row-preserving counterpart of
|
|
48
|
+
# align_nearest_addr); +direction:+ follows CArray (:floor = most recent
|
|
49
|
+
# at-or-before, :ceil = next, :round = nearest), and rows with no match
|
|
50
|
+
# in range or beyond +tolerance:+ come back UNDEF. Same wiring as the left
|
|
51
|
+
# join, only the address primitive differs.
|
|
52
|
+
def join_asof(other, on:, direction: :floor, tolerance: nil, suffixes: DEFAULT_JOIN_SUFFIXES)
|
|
53
|
+
on = on.to_s
|
|
54
|
+
plan = join_name_plan(other, on, suffixes)
|
|
55
|
+
addr = self[on].locate_nearest_addr(other[on], direction: direction, tolerance: tolerance)
|
|
56
|
+
join_by_addr(other, on, addr, plan)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Conform every variable to an externally supplied reference key set (the
|
|
60
|
+
# asymmetric sibling of +join+; pandas +reindex+). Given a +reference+ array
|
|
61
|
+
# of key values, each column is gathered onto it by exact key match
|
|
62
|
+
# (+locate_addr+): the aligned key column (or index) becomes the reference
|
|
63
|
+
# itself, and a reference key absent from the source comes back UNDEF in
|
|
64
|
+
# every other column.
|
|
65
|
+
#
|
|
66
|
+
# CAFrame measures no interval and generates nothing -- the caller owns the
|
|
67
|
+
# reference. This is the primitive for reindexing to a caller-built axis
|
|
68
|
+
# (e.g. a complete 10-minute time grid): supply that axis as +reference+ and
|
|
69
|
+
# the gaps fill with UNDEF rows carrying only the reference key.
|
|
70
|
+
#
|
|
71
|
+
# df.align("time", reftime) # reftime = a CArray of reference key values
|
|
72
|
+
#
|
|
73
|
+
# +key+ may be a column name or the index axis name. +reference+ is a CArray
|
|
74
|
+
# (or Array); its keys are matched against the source key by value, so their
|
|
75
|
+
# data types must be comparable (a DateTime object key matches by eql?/hash).
|
|
76
|
+
def align(key, reference)
|
|
77
|
+
key = key.to_s
|
|
78
|
+
ref = reference.is_a?(CArray) ? reference : reference.to_ca
|
|
79
|
+
on_index = !@columns.key?(key) && @axis_name == key && @index
|
|
80
|
+
addr = ref.locate_addr(on_index ? @index : self[key])
|
|
81
|
+
|
|
82
|
+
cols = {}
|
|
83
|
+
variable_names.each { |name| cols[name] = project_rows(self[name], addr) }
|
|
84
|
+
if on_index
|
|
85
|
+
CAFrame.new(cols, axis_name: key, index: ref)
|
|
86
|
+
else
|
|
87
|
+
cols[key] = ref
|
|
88
|
+
CAFrame.new(cols, axis_name: @axis_name,
|
|
89
|
+
index: @index && project_rows(@index, addr))
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Paste +other+'s variables beside this frame's, matched by **row position**
|
|
94
|
+
# (memo §12-C) — a keyless column merge (the column-direction counterpart of
|
|
95
|
+
# the row-stacking +concat+, and the positional counterpart of the key-aligned
|
|
96
|
+
# +join+; named after the UNIX +paste+). Both frames must have the same
|
|
97
|
+
# +nrow+; rows are assumed to already correspond (no key alignment, consistent
|
|
98
|
+
# with the no-implicit-align stance). Colliding column names are disambiguated
|
|
99
|
+
# by the same policy as +join+ (default suffix +_left+/+_right+, +suffixes:+ to
|
|
100
|
+
# override, +suffixes: false+ to raise). This frame's index is kept; +other+'s
|
|
101
|
+
# index, if any, is not carried (only its columns are pasted). Returns a new
|
|
102
|
+
# frame.
|
|
103
|
+
#
|
|
104
|
+
# obs.paste(fcst) # side by side, same rows
|
|
105
|
+
# obs.paste(fcst, suffixes: ["_obs", "_fcst"])
|
|
106
|
+
def paste(other, suffixes: DEFAULT_JOIN_SUFFIXES)
|
|
107
|
+
CAFrame.paste(self, other, suffixes: suffixes)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Paste N frames side by side by row position (symmetric N-ary sibling of
|
|
111
|
+
# the instance +#paste+; see the instance verb's docstring for the pair
|
|
112
|
+
# semantics). All frames must have the same +nrow+. The result's
|
|
113
|
+
# +axis_name+ and +index+ come from the first frame (peers, but a base is
|
|
114
|
+
# needed for the index). Column names that appear in more than one frame
|
|
115
|
+
# are colliding — pass +suffixes:+ as an Array of exactly K strings (one
|
|
116
|
+
# per input frame) to disambiguate them; otherwise the collision raises.
|
|
117
|
+
#
|
|
118
|
+
# CAFrame.paste(obs, fcst)
|
|
119
|
+
# CAFrame.paste(obs, ecmwf, gfs, suffixes: ["_obs", "_ecmwf", "_gfs"])
|
|
120
|
+
# CAFrame.paste([obs, fcst]) # Array is accepted too
|
|
121
|
+
def self.paste(*frames, suffixes: DEFAULT_JOIN_SUFFIXES)
|
|
122
|
+
frames = frames.flatten
|
|
123
|
+
raise ArgumentError, "paste requires at least one frame" if frames.empty?
|
|
124
|
+
unless frames.all? { |f| f.is_a?(CAFrame) }
|
|
125
|
+
raise ArgumentError, "paste expects CAFrame arguments"
|
|
126
|
+
end
|
|
127
|
+
first = frames.first
|
|
128
|
+
nrow = first.nrow
|
|
129
|
+
frames.each_with_index do |f, i|
|
|
130
|
+
next if i.zero?
|
|
131
|
+
unless f.nrow == nrow
|
|
132
|
+
raise ArgumentError,
|
|
133
|
+
"paste: row-count mismatch (frame #{i}: #{f.nrow} vs #{nrow}); " \
|
|
134
|
+
"use join for key alignment"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Detect columns that appear in 2+ frames (collisions across the K inputs).
|
|
139
|
+
name_count = Hash.new(0)
|
|
140
|
+
frames.each { |f| f.variable_names.each { |name| name_count[name] += 1 } }
|
|
141
|
+
collisions = name_count.select { |_, n| n > 1 }.keys
|
|
142
|
+
|
|
143
|
+
if collisions.any?
|
|
144
|
+
if suffixes == false
|
|
145
|
+
raise ArgumentError,
|
|
146
|
+
"paste column name collision: #{collisions.inspect} " \
|
|
147
|
+
"(pass suffixes: [\"_a\", \"_b\", ...] with one entry per frame, " \
|
|
148
|
+
"or rename first)"
|
|
149
|
+
end
|
|
150
|
+
# For the 2-frame case DEFAULT_JOIN_SUFFIXES ("_left", "_right") works
|
|
151
|
+
# as-is; for K > 2 the caller must pass a K-length Array (or use the
|
|
152
|
+
# pair-only default indirectly via the instance method).
|
|
153
|
+
unless suffixes.is_a?(Array) && suffixes.size == frames.size &&
|
|
154
|
+
suffixes.all? { |s| s.is_a?(String) }
|
|
155
|
+
raise ArgumentError,
|
|
156
|
+
"paste: suffixes must be an Array of #{frames.size} Strings " \
|
|
157
|
+
"(one per frame), got #{suffixes.inspect}"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
cols = {}
|
|
162
|
+
frames.each_with_index do |f, i|
|
|
163
|
+
f.variable_names.each do |name|
|
|
164
|
+
key = collisions.include?(name) ? "#{name}#{suffixes[i]}" : name
|
|
165
|
+
if cols.key?(key)
|
|
166
|
+
raise ArgumentError,
|
|
167
|
+
"paste: column name #{key.inspect} produced twice " \
|
|
168
|
+
"(collision after suffix from frame #{i})"
|
|
169
|
+
end
|
|
170
|
+
cols[key] = f[name]
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
new(cols, axis_name: first.axis_name, index: first.index)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private def join_name_plan(other, on, suffixes)
|
|
177
|
+
collisions = (variable_names & other.variable_names) - [on]
|
|
178
|
+
return { collisions: [], lsuf: nil, rsuf: nil } if collisions.empty?
|
|
179
|
+
|
|
180
|
+
if suffixes == false
|
|
181
|
+
raise ArgumentError,
|
|
182
|
+
"join column name collision: #{collisions.inspect} " \
|
|
183
|
+
"(pass suffixes: [\"_l\", \"_r\"] to disambiguate, or rename first)"
|
|
184
|
+
end
|
|
185
|
+
unless suffixes.is_a?(Array) && suffixes.size == 2 && suffixes.all? { |s| s.is_a?(String) }
|
|
186
|
+
raise ArgumentError,
|
|
187
|
+
"suffixes must be false or a 2-element array of Strings, got #{suffixes.inspect}"
|
|
188
|
+
end
|
|
189
|
+
{ collisions: collisions, lsuf: suffixes[0], rsuf: suffixes[1] }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
private def join_name(name, suffix, plan)
|
|
193
|
+
plan[:collisions].include?(name) ? "#{name}#{suffix}" : name
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
private def put_join_column(cols, name, col)
|
|
197
|
+
if cols.key?(name)
|
|
198
|
+
raise ArgumentError,
|
|
199
|
+
"join produces a duplicate column #{name.inspect} after suffixing; " \
|
|
200
|
+
"rename a column first or pass different suffixes"
|
|
201
|
+
end
|
|
202
|
+
cols[name] = col
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
private def join_left(other, on, lkey, rkey, plan)
|
|
206
|
+
join_by_addr(other, on, lkey.locate_addr(rkey), plan)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
private def join_by_addr(other, on, addr, plan)
|
|
210
|
+
cols = {}
|
|
211
|
+
variable_names.each { |name| put_join_column(cols, join_name(name, plan[:lsuf], plan), self[name]) }
|
|
212
|
+
other.variable_names.each do |name|
|
|
213
|
+
next if name == on
|
|
214
|
+
put_join_column(cols, join_name(name, plan[:rsuf], plan), project_rows(other[name], addr))
|
|
215
|
+
end
|
|
216
|
+
CAFrame.new(cols, axis_name: @axis_name, index: @index)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
private def join_align(other, on, lkey, rkey, how, plan)
|
|
220
|
+
common, a_idx, b_idx = CArray.align_addr(lkey, rkey, join: how)
|
|
221
|
+
cols = {}
|
|
222
|
+
variable_names.each do |name|
|
|
223
|
+
if name == on
|
|
224
|
+
put_join_column(cols, on, common)
|
|
225
|
+
else
|
|
226
|
+
put_join_column(cols, join_name(name, plan[:lsuf], plan), project_rows(self[name], a_idx))
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
other.variable_names.each do |name|
|
|
230
|
+
next if name == on
|
|
231
|
+
put_join_column(cols, join_name(name, plan[:rsuf], plan), project_rows(other[name], b_idx))
|
|
232
|
+
end
|
|
233
|
+
# Carry the left index the same way its columns are gathered (by a_idx),
|
|
234
|
+
# so it stays consistent with the left join; rows with no left match
|
|
235
|
+
# (outer/right) become UNDEF.
|
|
236
|
+
new_index = @index && project_rows(@index, a_idx)
|
|
237
|
+
CAFrame.new(cols, axis_name: @axis_name, index: new_index)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
private def project_rows(col, addr)
|
|
241
|
+
return col.project(addr) if col.ndim == 1
|
|
242
|
+
trailing = col.shape[1..]
|
|
243
|
+
t = trailing.inject(1, :*)
|
|
244
|
+
n = addr.shape[0]
|
|
245
|
+
flat = addr.reshape(n, 1) * t + CArray.int64(t).seq.reshape(1, t)
|
|
246
|
+
col.project(flat).reshape(n, *trailing)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# CAFrame record (row-oriented) input (memo §11.2, §11.5).
|
|
2
|
+
#
|
|
3
|
+
# from_records takes an Array of row Hashes -- the shape JSON.parse yields for
|
|
4
|
+
# a JSON array of objects -- and arranges it into columns. Unlike from_csv,
|
|
5
|
+
# the cell values are already typed Ruby objects (Float / Integer / DateTime /
|
|
6
|
+
# String), so a homogeneous column is built at its native leaf type rather than
|
|
7
|
+
# left as strings ("arrange by the value's own type", not string inference --
|
|
8
|
+
# §4.2 stays intact: no date-like string is parsed, mixed columns stay object).
|
|
9
|
+
#
|
|
10
|
+
# An Array-valued cell that is the same length across every record becomes an
|
|
11
|
+
# N-D column (§11.5): { "temp" => [min, mean, max] } over N records is one
|
|
12
|
+
# (N, 3) column. Ragged or non-numeric arrays fall back to an object column.
|
|
13
|
+
#
|
|
14
|
+
# Missing keys and explicit nils become UNDEF for numeric columns (mask, not a
|
|
15
|
+
# float promotion): an int column with a hole stays int + UNDEF.
|
|
16
|
+
|
|
17
|
+
class CAFrame
|
|
18
|
+
# Build a frame from an Array of row Hashes. Column set is the union of keys
|
|
19
|
+
# in first-appearance order; keys are stringified. +types:+ casts named
|
|
20
|
+
# columns afterward (same map / array-key forms as +cast+).
|
|
21
|
+
def self.from_records(records, types: nil)
|
|
22
|
+
unless records.is_a?(Array) && records.all? { |r| r.is_a?(Hash) }
|
|
23
|
+
raise ArgumentError, "from_records expects an Array of Hashes"
|
|
24
|
+
end
|
|
25
|
+
return new if records.empty?
|
|
26
|
+
|
|
27
|
+
keys = record_key_union(records)
|
|
28
|
+
n = records.size
|
|
29
|
+
cols = {}
|
|
30
|
+
keys.each do |key|
|
|
31
|
+
values = records.map { |r| r[key] }
|
|
32
|
+
cols[key.to_s] = build_record_column(values, n)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
frame = new(cols)
|
|
36
|
+
frame.cast(types) if types
|
|
37
|
+
frame
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Union of record keys in first-appearance order (original key objects, so a
|
|
41
|
+
# String- or Symbol-keyed set both work; the column name stringifies later).
|
|
42
|
+
def self.record_key_union(records)
|
|
43
|
+
seen = {}
|
|
44
|
+
keys = []
|
|
45
|
+
records.each do |r|
|
|
46
|
+
r.each_key do |k|
|
|
47
|
+
unless seen.key?(k)
|
|
48
|
+
seen[k] = true
|
|
49
|
+
keys << k
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
keys
|
|
54
|
+
end
|
|
55
|
+
private_class_method :record_key_union
|
|
56
|
+
|
|
57
|
+
# Arrange one column's values (Ruby objects, nil for missing) into a CArray.
|
|
58
|
+
# All-Array cells of equal length -> N-D native column; numeric scalars ->
|
|
59
|
+
# native scalar column (nil -> UNDEF); anything else -> object column.
|
|
60
|
+
def self.build_record_column(values, n)
|
|
61
|
+
present = values.reject(&:nil?)
|
|
62
|
+
return CArray.object(n) { values } if present.empty?
|
|
63
|
+
|
|
64
|
+
if present.all? { |v| v.is_a?(Array) || v.is_a?(CArray) }
|
|
65
|
+
build_nd_column(values, present, n)
|
|
66
|
+
else
|
|
67
|
+
type = numeric_leaf_type(present)
|
|
68
|
+
col = CArray.object(n) { values }
|
|
69
|
+
type ? col.to_type(type) : col
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
private_class_method :build_record_column
|
|
73
|
+
|
|
74
|
+
# Stack equal-length array cells into an (N, L) column via an object 2-D fill
|
|
75
|
+
# + to_type (nil rows -> UNDEF, int/float by leaf). Ragged lengths or
|
|
76
|
+
# non-numeric leaves fall back to a 1-D object column of the raw cells.
|
|
77
|
+
def self.build_nd_column(values, present, n)
|
|
78
|
+
lengths = present.map { |v| v.is_a?(CArray) ? v.shape[0] : v.size }
|
|
79
|
+
len = lengths.first
|
|
80
|
+
return CArray.object(n) { values } unless lengths.all? { |x| x == len }
|
|
81
|
+
|
|
82
|
+
nested = values.map { |v| v.nil? ? Array.new(len) : (v.is_a?(CArray) ? v.to_a : v) }
|
|
83
|
+
type = numeric_leaf_type(nested.flatten.compact)
|
|
84
|
+
table = CArray.object(n, len) { nested }
|
|
85
|
+
type ? table.to_type(type) : table
|
|
86
|
+
end
|
|
87
|
+
private_class_method :build_nd_column
|
|
88
|
+
|
|
89
|
+
# :int64 if every value is an Integer, :float64 if all are Numeric (int/float
|
|
90
|
+
# mix), otherwise nil (strings / DateTime / booleans / mixed -> keep object).
|
|
91
|
+
def self.numeric_leaf_type(values)
|
|
92
|
+
if values.all? { |v| v.is_a?(Integer) }
|
|
93
|
+
:int64
|
|
94
|
+
elsif values.all? { |v| v.is_a?(Numeric) }
|
|
95
|
+
:float64
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
private_class_method :numeric_leaf_type
|
|
99
|
+
end
|