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,109 @@
|
|
|
1
|
+
# Boolean folds all / any / none gain a `skip_masked:` keyword.
|
|
2
|
+
#
|
|
3
|
+
# skip_masked: true (default) -- available-case (skipna): masked cells are
|
|
4
|
+
# ignored, the result is always true/false.
|
|
5
|
+
# This is the existing behavior; every other
|
|
6
|
+
# keyword (axis:, keep_axis:, ...) forwards
|
|
7
|
+
# unchanged to the C reduction.
|
|
8
|
+
# skip_masked: false -- three-valued (Kleene) fold: the result is
|
|
9
|
+
# UNDEF when a masked cell could change it.
|
|
10
|
+
# OR is undetermined without a known true;
|
|
11
|
+
# AND without a known false. Matches the
|
|
12
|
+
# element-wise `|` / `&` Kleene semantics as
|
|
13
|
+
# an n-ary fold (|=max, &=min over
|
|
14
|
+
# false < unknown < true).
|
|
15
|
+
#
|
|
16
|
+
# The Kleene fold composes from count(true) / count(false) / count_masked, so
|
|
17
|
+
# there is no dedicated kernel; the C reductions keep the skipna semantics
|
|
18
|
+
# under the `__*_skipna__` aliases.
|
|
19
|
+
|
|
20
|
+
class CArray
|
|
21
|
+
|
|
22
|
+
alias_method :__any_skipna__, :any
|
|
23
|
+
alias_method :__all_skipna__, :all
|
|
24
|
+
alias_method :__none_skipna__, :none
|
|
25
|
+
private :__any_skipna__, :__all_skipna__, :__none_skipna__
|
|
26
|
+
|
|
27
|
+
# Whether any cell is true.
|
|
28
|
+
#
|
|
29
|
+
# With `skip_masked: true` (the default) masked cells are simply ignored and
|
|
30
|
+
# the result is always `true` / `false`. With `skip_masked: false` the fold
|
|
31
|
+
# is three-valued: the result is `UNDEF` when a masked cell could change it,
|
|
32
|
+
# matching the element-wise Kleene semantics of `|` / `&`.
|
|
33
|
+
#
|
|
34
|
+
# @param skip_masked [Boolean] ignore masked cells, or fold them three-valued.
|
|
35
|
+
# @param opts [Hash] forwarded to the underlying reduction (`axis:`,
|
|
36
|
+
# `keep_axis:`, ...).
|
|
37
|
+
# @return [Boolean, CArray] a scalar, or an array when an axis is given.
|
|
38
|
+
def any (skip_masked: true, **opts)
|
|
39
|
+
return __any_skipna__(**opts) if skip_masked
|
|
40
|
+
__kleene_fold(:any, opts)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Whether every cell is true.
|
|
44
|
+
#
|
|
45
|
+
# With `skip_masked: true` (the default) masked cells are simply ignored and
|
|
46
|
+
# the result is always `true` / `false`. With `skip_masked: false` the fold
|
|
47
|
+
# is three-valued: the result is `UNDEF` when a masked cell could change it,
|
|
48
|
+
# matching the element-wise Kleene semantics of `|` / `&`.
|
|
49
|
+
#
|
|
50
|
+
# @param skip_masked [Boolean] ignore masked cells, or fold them three-valued.
|
|
51
|
+
# @param opts [Hash] forwarded to the underlying reduction (`axis:`,
|
|
52
|
+
# `keep_axis:`, ...).
|
|
53
|
+
# @return [Boolean, CArray] a scalar, or an array when an axis is given.
|
|
54
|
+
def all (skip_masked: true, **opts)
|
|
55
|
+
return __all_skipna__(**opts) if skip_masked
|
|
56
|
+
__kleene_fold(:all, opts)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Whether no cell is true.
|
|
60
|
+
#
|
|
61
|
+
# With `skip_masked: true` (the default) masked cells are simply ignored and
|
|
62
|
+
# the result is always `true` / `false`. With `skip_masked: false` the fold
|
|
63
|
+
# is three-valued: the result is `UNDEF` when a masked cell could change it,
|
|
64
|
+
# matching the element-wise Kleene semantics of `|` / `&`.
|
|
65
|
+
#
|
|
66
|
+
# @param skip_masked [Boolean] ignore masked cells, or fold them three-valued.
|
|
67
|
+
# @param opts [Hash] forwarded to the underlying reduction (`axis:`,
|
|
68
|
+
# `keep_axis:`, ...).
|
|
69
|
+
# @return [Boolean, CArray] a scalar, or an array when an axis is given.
|
|
70
|
+
def none (skip_masked: true, **opts)
|
|
71
|
+
return __none_skipna__(**opts) if skip_masked
|
|
72
|
+
# none = not any (Kleene): not(true)=false, not(false)=true, not(UNDEF)=UNDEF
|
|
73
|
+
r = __kleene_fold(:any, opts)
|
|
74
|
+
r.is_a?(CArray) ? r.not : (r.equal?(UNDEF) ? UNDEF : !r)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Three-valued fold via counts. `kind` is :any (OR) or :all (AND).
|
|
80
|
+
# `opts` carries axis: / keep_axis: (same surface as the skipna reduction).
|
|
81
|
+
def __kleene_fold (kind, opts)
|
|
82
|
+
axis = opts[:axis]
|
|
83
|
+
if axis.nil?
|
|
84
|
+
if kind == :any
|
|
85
|
+
return true if count(true) > 0 # a known TRUE settles OR
|
|
86
|
+
else
|
|
87
|
+
return false if count(false) > 0 # a known FALSE settles AND
|
|
88
|
+
end
|
|
89
|
+
return UNDEF if count_masked > 0 # undetermined
|
|
90
|
+
return( kind == :any ? false : true )
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
keep = opts[:keep_axis]
|
|
94
|
+
cnt_kw = keep ? { axis: axis, keep_axis: keep } : { axis: axis }
|
|
95
|
+
ck = ( kind == :any ) ? count(true, **cnt_kw) : count(false, **cnt_kw)
|
|
96
|
+
cm = count_masked(axis: axis) # count_masked has no keep_axis
|
|
97
|
+
cm = cm.reshape(*ck.shape) unless cm.shape == ck.shape
|
|
98
|
+
|
|
99
|
+
# A cell is undetermined only where it is masked AND the known side does
|
|
100
|
+
# not settle it (ck counts the settling value: true for OR, false for AND).
|
|
101
|
+
# Masking only those cells avoids leaving an all-zero mask behind.
|
|
102
|
+
out = CArray.boolean(*ck.shape)
|
|
103
|
+
out[] = ( kind == :any ) ? 0 : 1 # OR default false / AND default true
|
|
104
|
+
out[ck.eq(0) & (cm > 0)] = UNDEF # genuinely undetermined
|
|
105
|
+
out[ck > 0] = ( kind == :any ) ? 1 : 0 # known side dominates
|
|
106
|
+
out
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
end
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------------
|
|
2
|
+
#
|
|
3
|
+
# carray/categorical.rb
|
|
4
|
+
#
|
|
5
|
+
# CACategorical — a categorical dtype: dense integer codes (the storage) plus
|
|
6
|
+
# a label vocabulary. Structurally the same as a pandas Categorical or an
|
|
7
|
+
# Arrow dictionary: each element is an index into a small set of categories.
|
|
8
|
+
#
|
|
9
|
+
# Implemented as a READONLY NonNumeric Face (see docs/CAFace.md) over
|
|
10
|
+
# the codes array:
|
|
11
|
+
#
|
|
12
|
+
# - storage (parent) = the integer codes
|
|
13
|
+
# - surface = CA_FIXLEN, so raw numeric kernels are gated off
|
|
14
|
+
# (`cat + 1` raises — arithmetic on category codes is
|
|
15
|
+
# nonsense). The meaningful operations come back for
|
|
16
|
+
# free on the codes parent: `cat.codes.bincount`
|
|
17
|
+
# (per-category counts), `cat.codes.count(code)`, etc.
|
|
18
|
+
# - per-cell access = decode the code into its label (`cat[i]` -> category)
|
|
19
|
+
#
|
|
20
|
+
# Exclusion (missing / out-of-vocabulary) is encoded two ways at once, and
|
|
21
|
+
# because the Face is READONLY they can never desync:
|
|
22
|
+
#
|
|
23
|
+
# - the cell is MASKED -> CArray-native idiom: `cat[i]` is
|
|
24
|
+
# UNDEF, `is_masked` / mask-aware
|
|
25
|
+
# reductions / category_sizes all work
|
|
26
|
+
# - the cell STORES the all-ones value -> the type-max sentinel, which is
|
|
27
|
+
# signed -1 byte-for-byte. The
|
|
28
|
+
# axis-group kernel skips it by its
|
|
29
|
+
# [0, k) range check (no kernel
|
|
30
|
+
# change); the pandas / Arrow bridge
|
|
31
|
+
# reads it as -1 by byte-reinterpret
|
|
32
|
+
# (zero-copy, no conversion)
|
|
33
|
+
#
|
|
34
|
+
# Because the codes ARE the storage parent, every view-creating operation
|
|
35
|
+
# (slice / reshape / transpose / mask / …) carries the codes along the view
|
|
36
|
+
# chain automatically; only the label vocabulary is carried via copy_state.
|
|
37
|
+
#
|
|
38
|
+
# Construction:
|
|
39
|
+
# keys.categorize # discover levels (first-appearance order)
|
|
40
|
+
# keys.categorize(labels: set) # fixed vocabulary; off-set keys excluded
|
|
41
|
+
# CACategorical.from_codes(c, lab) # wrap already-dense codes + labels
|
|
42
|
+
# # (= the pandas / Arrow import receiver)
|
|
43
|
+
#
|
|
44
|
+
# ----------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
require "carray"
|
|
47
|
+
|
|
48
|
+
# Categorical column: dense integer codes plus a label vocabulary, so each
|
|
49
|
+
# element is an index into a small set of categories. Structurally the same
|
|
50
|
+
# idea as a pandas Categorical or an Arrow dictionary array.
|
|
51
|
+
#
|
|
52
|
+
# Implemented as a read-only non-numeric Face over the codes array — the
|
|
53
|
+
# storage is the integer codes, while the surface is `CA_FIXLEN` so numeric
|
|
54
|
+
# kernels are gated off (`cat + 1` raises; arithmetic on category codes is
|
|
55
|
+
# not meaningful).
|
|
56
|
+
class CACategorical < CAObject
|
|
57
|
+
|
|
58
|
+
# raw-byte unpack format per storage (codes) data_type, native endian.
|
|
59
|
+
# The FIXLEN surface delivers a per-cell fetch as an N-byte String; this
|
|
60
|
+
# decodes it back into the integer code.
|
|
61
|
+
UNPACK_FORMAT = {
|
|
62
|
+
CA_INT8 => "c", CA_UINT8 => "C",
|
|
63
|
+
CA_INT16 => "s", CA_UINT16 => "S",
|
|
64
|
+
CA_INT32 => "l", CA_UINT32 => "L",
|
|
65
|
+
CA_INT64 => "q", CA_UINT64 => "Q",
|
|
66
|
+
}.freeze
|
|
67
|
+
|
|
68
|
+
# The exclusion sentinel per codes data_type: the all-ones bit pattern, read
|
|
69
|
+
# as type-max for an unsigned dtype and as -1 for a signed one. Either way it
|
|
70
|
+
# is out of every valid [0, k) range and byte-identical to a pandas / Arrow
|
|
71
|
+
# missing code.
|
|
72
|
+
SENTINEL = {
|
|
73
|
+
CA_UINT8 => 0xFF, CA_INT8 => -1,
|
|
74
|
+
CA_UINT16 => 0xFFFF, CA_INT16 => -1,
|
|
75
|
+
CA_UINT32 => 0xFFFFFFFF, CA_INT32 => -1,
|
|
76
|
+
CA_UINT64 => 0xFFFFFFFFFFFFFFFF, CA_INT64 => -1,
|
|
77
|
+
}.freeze
|
|
78
|
+
|
|
79
|
+
class << self
|
|
80
|
+
# Wrap already-dense codes + labels with no discovery — the import receiver
|
|
81
|
+
# for a pandas Categorical or an Arrow dictionary. `codes` becomes the
|
|
82
|
+
# Face's storage parent verbatim (zero-copy when it is a wrapped memory
|
|
83
|
+
# view), and from_codes takes ownership of it.
|
|
84
|
+
#
|
|
85
|
+
# Excluded cells are identified by the all-ones sentinel value (type-max
|
|
86
|
+
# for unsigned codes, -1 for signed — both the pandas / Arrow missing code)
|
|
87
|
+
# and masked here, so the categorical is well-formed regardless of whether
|
|
88
|
+
# the caller pre-masked. Only the mask buffer is touched; the code bytes
|
|
89
|
+
# are left intact (so a pandas byte-reinterpret round-trips).
|
|
90
|
+
# @overload from_codes(codes, labels)
|
|
91
|
+
# Returns a {CACategorical} wrapping already-dense integer
|
|
92
|
+
# `codes` with the given `labels`, without discovery. `codes`
|
|
93
|
+
# becomes the Face's storage parent; excluded cells (identified
|
|
94
|
+
# by the type-max sentinel value) are masked automatically.
|
|
95
|
+
# @param codes [CArray] integer code storage.
|
|
96
|
+
# @param labels [Array, CArray] category vocabulary indexed by
|
|
97
|
+
# code.
|
|
98
|
+
# @return [CACategorical]
|
|
99
|
+
# @raise [ArgumentError] when `codes` is not an integer CArray.
|
|
100
|
+
def from_codes(codes, labels)
|
|
101
|
+
unless codes.is_a?(CArray) && SENTINEL.key?(codes.data_type)
|
|
102
|
+
got = codes.is_a?(CArray) ? codes.data_type : codes.class
|
|
103
|
+
raise ArgumentError, "from_codes: codes must be an integer CArray (got #{got})"
|
|
104
|
+
end
|
|
105
|
+
excluded = codes.eq(SENTINEL[codes.data_type])
|
|
106
|
+
if excluded.count(true) > 0
|
|
107
|
+
codes.mask = codes.has_mask? ? (codes.mask | excluded) : excluded
|
|
108
|
+
end
|
|
109
|
+
new(codes, labels)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# codes : integer CArray, the storage parent. Excluded cells are both
|
|
114
|
+
# masked AND store the type-max sentinel value (= the all-ones bit
|
|
115
|
+
# pattern, which is signed -1 byte-for-byte — the pandas / Arrow
|
|
116
|
+
# missing code). Because the Face is READONLY the two never desync,
|
|
117
|
+
# so consumers may rely on either: the mask (CArray-native) or the
|
|
118
|
+
# sentinel (axis-group's out-of-range skip, zero-copy export).
|
|
119
|
+
# labels: Array | CArray, the vocabulary; labels[code] = category.
|
|
120
|
+
# @overload initialize(codes, labels)
|
|
121
|
+
# Allocates a READONLY {CACategorical} Face whose storage is
|
|
122
|
+
# `codes` and whose vocabulary is `labels`. The label list is
|
|
123
|
+
# copied and frozen so codes always index a stable vocabulary.
|
|
124
|
+
# @param codes [CArray] integer code storage.
|
|
125
|
+
# @param labels [Array, CArray] category vocabulary.
|
|
126
|
+
def initialize(codes, labels)
|
|
127
|
+
# Own a frozen copy of the vocabulary: the categorical is READONLY and its
|
|
128
|
+
# codes index into labels, so the label list must not change under it. We
|
|
129
|
+
# copy first so a caller's array is never frozen as a side effect; the
|
|
130
|
+
# label objects themselves are left untouched (container-level freeze).
|
|
131
|
+
@labels = (labels.respond_to?(:to_a) ? labels.to_a : Array(labels)).dup.freeze
|
|
132
|
+
super(CA_FIXLEN, codes.dim,
|
|
133
|
+
bytes: codes.bytes,
|
|
134
|
+
storage: codes.data_type,
|
|
135
|
+
parent: codes,
|
|
136
|
+
read_only: true,
|
|
137
|
+
face: true)
|
|
138
|
+
# Mark the codes storage read-only so the READONLY guarantee holds at the
|
|
139
|
+
# root, not just on the Face. Without this the Face is read_only but its
|
|
140
|
+
# parent is writable, so `cat.codes[i] = x` silently mutates the categorical
|
|
141
|
+
# (and any grouping cache derived from it). We set the CA_FLAG_READ_ONLY flag
|
|
142
|
+
# rather than #freeze: freeze also freezes the Ruby object, which propagates
|
|
143
|
+
# through views/Faces (a reshape of frozen codes is frozen) and would block
|
|
144
|
+
# the grouping cache from memoising. The flag gives the same write protection
|
|
145
|
+
# (mutations raise) while keeping the object non-frozen. One-way: it takes
|
|
146
|
+
# ownership of `codes` (categorize / from_codes build or receive it, mask
|
|
147
|
+
# already derived above); a caller keeping a mutable array must pass `.copy`.
|
|
148
|
+
codes.set_read_only_flag
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
attr_reader :labels
|
|
152
|
+
|
|
153
|
+
# The raw integer codes (= the storage parent). On a derived view this is
|
|
154
|
+
# the correspondingly sliced/reshaped codes, since codes ride the chain.
|
|
155
|
+
# Excluded cells are masked and store the type-max sentinel; the same array
|
|
156
|
+
# serves the axis-group kernel (out-of-range skip) and the pandas / Arrow
|
|
157
|
+
# bridge (byte-reinterpret to signed -1) with no conversion.
|
|
158
|
+
# @overload codes
|
|
159
|
+
# Returns the raw integer code CArray backing `self`. Excluded
|
|
160
|
+
# cells are masked and store the type-max sentinel. The array is
|
|
161
|
+
# read-only — the categorical owns immutable codes, so
|
|
162
|
+
# `codes[i] = x` raises; use `codes.copy` for a mutable copy.
|
|
163
|
+
# @return [CArray]
|
|
164
|
+
def codes
|
|
165
|
+
parent
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Face hook: carry the vocabulary across lifted views (slice / reshape / …).
|
|
169
|
+
# The codes ride along automatically as the Face's parent.
|
|
170
|
+
def copy_state(src)
|
|
171
|
+
@labels = src.labels
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Face hook: the homogeneity gate for multi-parent constructions
|
|
175
|
+
# (CArray.promote_list / CArray.stack / anything that Face-lifts a list).
|
|
176
|
+
# A code only means anything against the vocabulary it was assigned from, so
|
|
177
|
+
# two categoricals may share one lifted Face only when they index the same
|
|
178
|
+
# labels in the same code order.
|
|
179
|
+
#
|
|
180
|
+
# Same labels in a *different* code order is refused rather than re-coded,
|
|
181
|
+
# for the same reason CATime refuses a unit mismatch it knows how to convert:
|
|
182
|
+
# this is a predicate consulted after the parents are assembled, with no
|
|
183
|
+
# channel to rewrite storage — and the codes are read-only by construction,
|
|
184
|
+
# so agreeing would mean silently materialising fresh codes inside what the
|
|
185
|
+
# caller asked for as a view. Build the shared vocabulary up front instead:
|
|
186
|
+
# `keys.categorize(labels: shared)`.
|
|
187
|
+
def face_state_compatible?(other)
|
|
188
|
+
other.is_a?(CACategorical) && @labels == other.labels
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Face hook: decode a per-cell code into its category label. An out-of-range
|
|
192
|
+
# code (e.g. an unmasked external sentinel) decodes to nil rather than a
|
|
193
|
+
# wrong category via Ruby negative indexing.
|
|
194
|
+
def storage_to_scalar(raw)
|
|
195
|
+
code = raw.is_a?(String) ? raw.unpack1(UNPACK_FORMAT.fetch(parent.data_type)) : raw
|
|
196
|
+
(code < 0 || code >= @labels.size) ? nil : @labels[code]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# ---- category-space operations (by label, not code) -------------------
|
|
200
|
+
#
|
|
201
|
+
# The element value of a categorical IS its category, so these specialise
|
|
202
|
+
# CArray's value operations into label space — the user never has to know
|
|
203
|
+
# the integer code. The raw codes stay reachable via #codes for code-space
|
|
204
|
+
# work (interop, ML features), but they are not the everyday surface.
|
|
205
|
+
|
|
206
|
+
# Boolean mask of cells whose category == label. Excluded cells stay UNDEF
|
|
207
|
+
# (their category is unknown); an unknown label yields an all-false mask.
|
|
208
|
+
# @overload eq(label)
|
|
209
|
+
# Returns a boolean CArray marking cells whose category equals
|
|
210
|
+
# `label`. Excluded cells stay masked; an unknown label yields
|
|
211
|
+
# an all-false result.
|
|
212
|
+
# @param label [Object] category to match.
|
|
213
|
+
# @return [CArray]
|
|
214
|
+
def eq(label)
|
|
215
|
+
codes.eq(@labels.index(label) || @labels.size)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# @overload ne(label)
|
|
219
|
+
# Returns the complement of {#eq}.
|
|
220
|
+
# @param label [Object] category to compare against.
|
|
221
|
+
# @return [CArray]
|
|
222
|
+
def ne(label)
|
|
223
|
+
codes.ne(@labels.index(label) || @labels.size)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# @overload count(label)
|
|
227
|
+
# Returns the number of cells whose category equals `label`
|
|
228
|
+
# (0 for an unknown label).
|
|
229
|
+
# @param label [Object] category to count.
|
|
230
|
+
# @return [Integer]
|
|
231
|
+
def count(label)
|
|
232
|
+
code = @labels.index(label)
|
|
233
|
+
code ? codes.count(code) : 0
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Per-category counts as a length-k array aligned to #labels. Trailing empty
|
|
237
|
+
# categories are kept as 0 (unlike codes.bincount, which truncates them), so
|
|
238
|
+
# `labels.zip(category_sizes.to_a)` always pairs up.
|
|
239
|
+
#
|
|
240
|
+
# Memoised: the codes are read-only (immutable storage), so the counts are a
|
|
241
|
+
# pure function of the categorical and stay valid for its lifetime. The same
|
|
242
|
+
# cached array backs {#reduceat_index} and {#sort_addr}, so a wide aggregate
|
|
243
|
+
# over the same categorical pays the count once. (Do not mutate the returned
|
|
244
|
+
# array — it is shared; take `.copy` for a scratch buffer.) A derived-view /
|
|
245
|
+
# composite categorical caches too: read-only rides from the codes as a flag,
|
|
246
|
+
# not a Ruby freeze, so the object stays non-frozen and the memo ivar sticks.
|
|
247
|
+
# @overload category_sizes
|
|
248
|
+
# Returns per-category counts as a CArray with one entry per {#labels}
|
|
249
|
+
# aligned to {#labels}. Trailing empty categories are kept as 0
|
|
250
|
+
# so `labels.zip(category_sizes.to_a)` always pairs up.
|
|
251
|
+
# @return [CArray]
|
|
252
|
+
def category_sizes
|
|
253
|
+
return @_category_sizes if @_category_sizes
|
|
254
|
+
bc = codes.bincount
|
|
255
|
+
out = CArray.new(bc.data_type, [@labels.size]) # new zero-fills
|
|
256
|
+
out[0...bc.elements] = bc if bc.elements > 0
|
|
257
|
+
@_category_sizes = out
|
|
258
|
+
out
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Back-compatible alias; {#category_sizes} is the canonical name (a histogram
|
|
262
|
+
# `bincount` reading is misleading for per-category cell counts).
|
|
263
|
+
alias bincount category_sizes
|
|
264
|
+
|
|
265
|
+
# ---- value-hash discovery family --------------------------------------
|
|
266
|
+
#
|
|
267
|
+
# The codes are storage and the labels are the values, so this family has to
|
|
268
|
+
# answer in label space. It cannot ride the family's Face gate: a categorical
|
|
269
|
+
# is not ORDERABLE (code order is the vocabulary's order, not the labels'),
|
|
270
|
+
# and lifting a code array back would still be codes. Without these overrides
|
|
271
|
+
# `unique` handed back the raw code bytes and `is_in` compared labels against
|
|
272
|
+
# codes, so it was false everywhere.
|
|
273
|
+
#
|
|
274
|
+
# Distinctness itself still rides the codes, where it is a uint8 pass; only the
|
|
275
|
+
# values crossing the surface are translated. {#count} and {#category_sizes}
|
|
276
|
+
# above are the same idea for a single label / the whole vocabulary.
|
|
277
|
+
|
|
278
|
+
# @overload unique(sort: false)
|
|
279
|
+
# Returns the labels that occur, in first-appearance order. A category
|
|
280
|
+
# with no cells is not included (use {#labels} for the vocabulary).
|
|
281
|
+
# @param sort [Boolean] when true, sort the labels ascending.
|
|
282
|
+
# @return [CArray] object CArray of labels.
|
|
283
|
+
def unique (sort: false)
|
|
284
|
+
u = labels_for(codes.unique)
|
|
285
|
+
sort ? u.sort : u
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# @overload value_counts(sort: false)
|
|
289
|
+
# Returns `[labels, counts]` for the categories that occur. {#category_sizes}
|
|
290
|
+
# is the aligned-to-{#labels} counterpart, which keeps the empty ones.
|
|
291
|
+
# @param sort [false, :count, :value] pair ordering.
|
|
292
|
+
# @return [Array(CArray, CArray)]
|
|
293
|
+
def value_counts (sort: false)
|
|
294
|
+
unless [false, :count, :value].include?(sort)
|
|
295
|
+
raise ArgumentError, "value_counts: sort must be false, :count, or :value"
|
|
296
|
+
end
|
|
297
|
+
code_values, counts = codes.value_counts(sort: sort == :count ? :count : false)
|
|
298
|
+
values = labels_for(code_values)
|
|
299
|
+
return [values, counts] unless sort == :value
|
|
300
|
+
# Ascending *label*: code order is the vocabulary's, so reorder here.
|
|
301
|
+
la = values.to_a
|
|
302
|
+
order = (0...la.size).sort_by { |i| [la[i], i] }
|
|
303
|
+
idx = CArray.int64(order.size) { |i| order[i] }
|
|
304
|
+
[values[idx], counts[idx]]
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# @overload mode(axis: nil)
|
|
308
|
+
# Returns the most frequent label(s). Runs in label space because the
|
|
309
|
+
# modal values come back sorted ascending, which for labels is not the
|
|
310
|
+
# code order.
|
|
311
|
+
# @return [CArray, Array<CArray>]
|
|
312
|
+
def mode (axis: nil)
|
|
313
|
+
label_values.mode(axis: axis)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# @overload is_in(values)
|
|
317
|
+
# Returns a boolean CArray, true where the cell's label is in `values`.
|
|
318
|
+
# An unknown label matches nothing. Masked cells stay masked.
|
|
319
|
+
# @param values [Array, CArray, CACategorical] labels to test against.
|
|
320
|
+
# @return [CArray]
|
|
321
|
+
def is_in (values)
|
|
322
|
+
wanted = label_list(values).filter_map { |l| @labels.index(l) }
|
|
323
|
+
codes.is_in(CArray.int64(wanted.size) { |i| wanted[i] })
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# @overload intersection(other, sort: false)
|
|
327
|
+
# @return [CArray] object CArray of the labels present in both.
|
|
328
|
+
def intersection (other, sort: false)
|
|
329
|
+
unique.intersection(label_array(other), sort: sort)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# @overload difference(other, sort: false)
|
|
333
|
+
# @return [CArray] object CArray of the labels only `self` has.
|
|
334
|
+
def difference (other, sort: false)
|
|
335
|
+
unique.difference(label_array(other), sort: sort)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# @overload union(other, sort: false)
|
|
339
|
+
# @return [CArray] object CArray of the labels of either side.
|
|
340
|
+
def union (other, sort: false)
|
|
341
|
+
unique.union(label_array(other), sort: sort)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# @overload locate_addr(ref)
|
|
345
|
+
# @return [CArray] for each cell, its label's address in `ref` (UNDEF when
|
|
346
|
+
# the label does not occur there).
|
|
347
|
+
def locate_addr (ref)
|
|
348
|
+
label_values.locate_addr(label_array(ref))
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
private
|
|
352
|
+
|
|
353
|
+
# Labels for a code array (the discovery kernels skip masked cells, so the
|
|
354
|
+
# code arrays reaching here hold real codes only).
|
|
355
|
+
def labels_for (code_array)
|
|
356
|
+
CA_OBJECT(code_array.to_a.map { |c| @labels[c] })
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# This categorical's cells as their labels; a masked cell stays masked.
|
|
360
|
+
def label_values
|
|
361
|
+
CA_OBJECT(to_a)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# An operand's labels as a plain Array (a masked cell comes through as UNDEF,
|
|
365
|
+
# which matches no label and so drops out of the set).
|
|
366
|
+
def label_list (other)
|
|
367
|
+
other.respond_to?(:to_a) ? other.to_a : Array(other)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def label_array (other)
|
|
371
|
+
CA_OBJECT(label_list(other))
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
public
|
|
375
|
+
|
|
376
|
+
# ---- reduceat / sort-based grouping foundation ------------------------
|
|
377
|
+
#
|
|
378
|
+
# The scatter path (axis_group) handles the monoid reductions (sum / mean /
|
|
379
|
+
# min / max / variance) in one pass. Order statistics (median / percentile)
|
|
380
|
+
# cannot be scattered: they need every value of a group held together. These
|
|
381
|
+
# two accessors are the building blocks for that other engine — lay the data
|
|
382
|
+
# out as category-contiguous blocks, then select per block.
|
|
383
|
+
#
|
|
384
|
+
# The grouping plan — sort_addr (the counting sort, the dominant cost) plus
|
|
385
|
+
# reduceat_index and category_sizes — depends only on the codes, which are
|
|
386
|
+
# read-only. It is therefore memoised (a pure function of the categorical): the
|
|
387
|
+
# first grouping access builds it and every later one reuses it. This is what
|
|
388
|
+
# lets a wide aggregate (many payload columns over one classifier) or a
|
|
389
|
+
# CACategoricalIterator run the counting sort once, not once per column /
|
|
390
|
+
# iterator. Only the plan is cached; the payload-dependent grouped copy
|
|
391
|
+
# (value.reshape(n)[sort_addr]) is rebuilt per column (it varies with value).
|
|
392
|
+
|
|
393
|
+
# Force-build (and cache) the whole grouping plan up front — sort_addr,
|
|
394
|
+
# reduceat_index, category_sizes — for prepare-ahead use before a batch of
|
|
395
|
+
# groupings. Lazy building already covers correctness; this is the explicit
|
|
396
|
+
# "pay the counting sort now" handle (e.g. right after `df.group_by(col)`).
|
|
397
|
+
# Returns self so it chains.
|
|
398
|
+
# @overload build_grouping
|
|
399
|
+
# Eagerly builds and caches the grouping plan (sort_addr / reduceat_index
|
|
400
|
+
# / category_sizes). Optional — the plan is built lazily on first grouping
|
|
401
|
+
# access — but useful to pay the counting sort once ahead of a batch.
|
|
402
|
+
# @return [self]
|
|
403
|
+
def build_grouping
|
|
404
|
+
sort_addr # pulls category_sizes; reduceat_index shares category_sizes
|
|
405
|
+
reduceat_index
|
|
406
|
+
self
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Flat addresses that gather self into category-contiguous order: every cell
|
|
410
|
+
# of category 0 first, then 1, ..., then k-1, with excluded cells last. Built
|
|
411
|
+
# by a counting sort over the codes (O(n + k), stable): the segment starts
|
|
412
|
+
# (an exclusive prefix scan of {#category_sizes}) drive a scatter that places
|
|
413
|
+
# each source index into its category's block in source order; excluded cells
|
|
414
|
+
# (masked, or code out of range 0...k) are appended at the tail in source
|
|
415
|
+
# order. The first {#category_sizes}.sum addresses are exactly the classified
|
|
416
|
+
# cells in category order — CACategoricalIterator's permutation. For an N-D
|
|
417
|
+
# categorical the addresses are into the raveled storage, so
|
|
418
|
+
# `value.reshape(elements)[cat.sort_addr]` produces the contiguous blocks.
|
|
419
|
+
# Memoised (see the grouping-plan note above): the counting sort is the
|
|
420
|
+
# dominant grouping cost, so it is computed once and shared by every payload
|
|
421
|
+
# column and by CACategoricalIterator. Do not mutate the returned array.
|
|
422
|
+
# @overload sort_addr
|
|
423
|
+
# Returns a length-{#elements} integer CArray of flat storage
|
|
424
|
+
# addresses that order the cells by category (excluded cells last).
|
|
425
|
+
# @return [CArray]
|
|
426
|
+
def sort_addr
|
|
427
|
+
return @_sort_addr if @_sort_addr
|
|
428
|
+
n = elements
|
|
429
|
+
k = @labels.size
|
|
430
|
+
cs = category_sizes # per-category counts
|
|
431
|
+
nvalid = cs.sum
|
|
432
|
+
cur = CArray.int64(k > 0 ? k : 0) # segment starts, consumed as cursor
|
|
433
|
+
cur[1..-1] = cs.cumsum.int64[0..-2] if k > 1
|
|
434
|
+
flat = codes.reshape(n)
|
|
435
|
+
seq = CArray.int64(n).seq! # source indices, scattered as payload
|
|
436
|
+
out = CArray.int64(n)
|
|
437
|
+
if nvalid == n
|
|
438
|
+
flat.send(:__categorical_scatter__, seq, cur, out, k) # all valid: scatter straight in
|
|
439
|
+
else
|
|
440
|
+
if nvalid > 0
|
|
441
|
+
valid = CArray.int64(nvalid) # scatter target must be an entity, not a view
|
|
442
|
+
flat.send(:__categorical_scatter__, seq, cur, valid, k)
|
|
443
|
+
out[0...nvalid] = valid
|
|
444
|
+
end
|
|
445
|
+
# Excluded = every cell the scatter skips: code out of [0, k), OR masked
|
|
446
|
+
# (a from_codes pre-masked cell keeps a valid code but is excluded). Both
|
|
447
|
+
# must be caught or the tail slot count would not add up to n - nvalid.
|
|
448
|
+
excluded = flat.value.ge(k).or(flat.value.lt(0))
|
|
449
|
+
excluded = excluded.or(flat.is_masked) if flat.has_mask?
|
|
450
|
+
out[nvalid..-1] = seq[excluded] # excluded cells, source order
|
|
451
|
+
end
|
|
452
|
+
@_sort_addr = out
|
|
453
|
+
out
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Segment start offsets into the category-contiguous layout produced by
|
|
457
|
+
# {#sort_addr}: reduceat_index[c] is where category c's block begins, so
|
|
458
|
+
# block c spans `reduceat_index[c] ... reduceat_index[c] + category_sizes[c]`.
|
|
459
|
+
# Empty categories repeat the following start (a zero-width block). Length k,
|
|
460
|
+
# aligned to #labels; pair with {#category_sizes} for the block lengths. On
|
|
461
|
+
# data already laid out in category order the offsets index it directly.
|
|
462
|
+
# @overload reduceat_index
|
|
463
|
+
# Returns an int64 CArray, one entry per {#labels}, of segment start
|
|
464
|
+
# offsets aligned to {#labels}. Pair with {#category_sizes} for lengths.
|
|
465
|
+
# @return [CArray]
|
|
466
|
+
def reduceat_index
|
|
467
|
+
return @_reduceat_index if @_reduceat_index
|
|
468
|
+
k = @labels.size
|
|
469
|
+
out = CArray.int64(k > 0 ? k : 0)
|
|
470
|
+
if k > 1
|
|
471
|
+
out[0] = 0
|
|
472
|
+
out[1..-1] = category_sizes.cumsum.int64[0..-2]
|
|
473
|
+
elsif k == 1
|
|
474
|
+
out[0] = 0
|
|
475
|
+
end
|
|
476
|
+
@_reduceat_index = out
|
|
477
|
+
out
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# @overload inspect
|
|
481
|
+
# Returns a short summary showing element count, category count,
|
|
482
|
+
# and vocabulary.
|
|
483
|
+
# @return [String]
|
|
484
|
+
def inspect
|
|
485
|
+
"#<CACategorical n=#{elements} categories=#{@labels.size} labels=#{@labels.inspect}>"
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class CArray
|
|
491
|
+
# Build a CACategorical from self read as category keys (= the values whose
|
|
492
|
+
# distinct levels become the categories). Codes are dense 0-based in the
|
|
493
|
+
# order labels appear (first-appearance by default, or ascending sorted
|
|
494
|
+
# when `sort_labels: true`); masked keys become masked (excluded) codes.
|
|
495
|
+
#
|
|
496
|
+
# labels: nil -> discover, first-appearance order
|
|
497
|
+
# labels: nil, sort_labels: true -> discover, then sort ascending
|
|
498
|
+
# labels: set -> fixed vocabulary (must be unique);
|
|
499
|
+
# keys outside it are excluded (masked)
|
|
500
|
+
# @overload categorize(labels: nil, sort_labels: false)
|
|
501
|
+
# Returns a {CACategorical} built from `self` read as category
|
|
502
|
+
# keys. With `labels: nil` distinct levels are discovered in
|
|
503
|
+
# first-appearance order (or ascending sorted when
|
|
504
|
+
# `sort_labels: true`); with an explicit `labels` list the
|
|
505
|
+
# vocabulary is fixed and keys outside it become masked
|
|
506
|
+
# (excluded). `sort_labels:` is ignored when an explicit `labels`
|
|
507
|
+
# is given (the caller has already chosen the order).
|
|
508
|
+
# @param labels [Array, CArray, nil] fixed vocabulary; `nil`
|
|
509
|
+
# enables discovery.
|
|
510
|
+
# @param sort_labels [Boolean] when discovering (labels: nil), sort
|
|
511
|
+
# the discovered vocabulary ascending after collecting it.
|
|
512
|
+
# @return [CACategorical]
|
|
513
|
+
# @raise [ArgumentError] when explicit `labels` contain
|
|
514
|
+
# duplicates.
|
|
515
|
+
def categorize(labels: nil, sort_labels: false)
|
|
516
|
+
# Automatic appearance-order vocabulary: one linear pass (C
|
|
517
|
+
# __factorize_appearance__) returns both codes and levels directly, over the
|
|
518
|
+
# integer / float / object / fixlen / boolean lanes (boolean rides the uint8
|
|
519
|
+
# lane). Distinctness is the hash-key judgement shared with the discovery
|
|
520
|
+
# family: Float NaN collapses to one category and -0.0 == +0.0, while mixed
|
|
521
|
+
# Integer / Float keys stay distinct (eql?, so 1 and 1.0 are separate
|
|
522
|
+
# categories). The discovery path below is reserved for sort_labels (which
|
|
523
|
+
# reorders the vocabulary, desyncing the appearance-order codes), an explicit
|
|
524
|
+
# labels list, and the dtypes the factorize kernel does not take (complex).
|
|
525
|
+
if labels.nil? && !sort_labels && (integer? || float? || object? || fixlen? || boolean?)
|
|
526
|
+
codes, levels = __factorize_appearance__
|
|
527
|
+
return CACategorical.from_codes(codes, levels.to_a)
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
if labels.nil?
|
|
531
|
+
# Discover the levels in first-appearance order: mask_duplicates keeps the
|
|
532
|
+
# first occurrence of each distinct value and masks the rest (already-
|
|
533
|
+
# masked keys stay excluded), so the non-masked cells are the levels.
|
|
534
|
+
# Only the final list is Ruby, since labels are Ruby objects.
|
|
535
|
+
labels_arr = mask_duplicates[:is_not_masked].to_a
|
|
536
|
+
labels_arr.sort! if sort_labels
|
|
537
|
+
else
|
|
538
|
+
labels_arr = labels.respond_to?(:to_a) ? labels.to_a : Array(labels)
|
|
539
|
+
if labels_arr.uniq.size != labels_arr.size
|
|
540
|
+
raise ArgumentError, "categorize: labels: must be unique (got duplicates)"
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# Choose a narrow unsigned code dtype, reserving its top value as the
|
|
545
|
+
# exclusion sentinel so it never collides with a real code 0..k-1.
|
|
546
|
+
k = labels_arr.size
|
|
547
|
+
code_type, sentinel =
|
|
548
|
+
if k <= 0xFF then [CA_UINT8, 0xFF]
|
|
549
|
+
elsif k <= 0xFFFF then [CA_UINT16, 0xFFFF]
|
|
550
|
+
else [CA_UINT32, 0xFFFFFFFF]
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# One vectorized masked write per category. Cells matching no category
|
|
554
|
+
# (out-of-vocabulary) and masked cells (eq yields UNDEF, skipped) keep the
|
|
555
|
+
# sentinel; from_codes then derives the mask from it.
|
|
556
|
+
codes = CArray.new(code_type, shape).fill(sentinel)
|
|
557
|
+
labels_arr.each_with_index { |label, c| codes[eq(label)] = c }
|
|
558
|
+
|
|
559
|
+
CACategorical.from_codes(codes, labels_arr)
|
|
560
|
+
end
|
|
561
|
+
end
|