arraykit 1.5.0__tar.gz → 1.6.0__tar.gz
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.
- {arraykit-1.5.0/arraykit.egg-info → arraykit-1.6.0}/PKG-INFO +7 -2
- {arraykit-1.5.0 → arraykit-1.6.0}/README.rst +6 -1
- arraykit-1.6.0/VERSION +2 -0
- {arraykit-1.5.0 → arraykit-1.6.0/arraykit.egg-info}/PKG-INFO +7 -2
- {arraykit-1.5.0 → arraykit-1.6.0}/arraykit.egg-info/SOURCES.txt +1 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/__init__.py +1 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/__init__.pyi +3 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/_arraykit.c +4 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/auto_map.c +400 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/auto_map.h +2 -0
- arraykit-1.6.0/test/test_factorize.py +274 -0
- arraykit-1.5.0/VERSION +0 -2
- {arraykit-1.5.0 → arraykit-1.6.0}/LICENSE.txt +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/MANIFEST.in +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/arraykit.egg-info/dependency_links.txt +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/arraykit.egg-info/requires.txt +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/arraykit.egg-info/top_level.txt +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/pyproject.toml +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/setup.cfg +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/setup.py +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/array_go.c +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/array_go.h +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/array_to_tuple.c +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/array_to_tuple.h +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/block_index.c +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/block_index.h +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/delimited_to_arrays.c +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/delimited_to_arrays.h +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/methods.c +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/methods.h +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/py.typed +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/tri_map.c +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/tri_map.h +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/src/utilities.h +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/test/test_array_go.py +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/test/test_astype_array.py +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/test/test_auto_map.py +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/test/test_auto_map_property.py +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/test/test_block_index.py +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/test/test_delimited_to_arrays.py +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/test/test_delimited_to_arrays_integration.py +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/test/test_delimited_to_arrays_property.py +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/test/test_nonzero_1d.py +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/test/test_nonzero_1d_property.py +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/test/test_objectable.py +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/test/test_pyi.py +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/test/test_split_after_count.py +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/test/test_transition_slices_from_group.py +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/test/test_tri_map.py +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/test/test_type_discovery.py +0 -0
- {arraykit-1.5.0 → arraykit-1.6.0}/test/test_util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arraykit
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Array utilities for StaticFrame
|
|
5
5
|
Author: Christopher Ariza, Brandt Bucher, Charles Burkland
|
|
6
6
|
License: MIT
|
|
@@ -47,7 +47,7 @@ arraykit
|
|
|
47
47
|
|
|
48
48
|
The ArrayKit library provides utilities for creating and transforming NumPy arrays, implementing performance-critical StaticFrame operations as Python C extensions.
|
|
49
49
|
|
|
50
|
-
Code: https://github.com/
|
|
50
|
+
Code: https://github.com/static-frame/arraykit
|
|
51
51
|
|
|
52
52
|
Packages: https://pypi.org/project/arraykit
|
|
53
53
|
|
|
@@ -64,6 +64,11 @@ ArrayKit requires the following:
|
|
|
64
64
|
What is New in ArrayKit
|
|
65
65
|
-------------------------
|
|
66
66
|
|
|
67
|
+
1.6.0
|
|
68
|
+
............
|
|
69
|
+
Added ``factorize``.
|
|
70
|
+
|
|
71
|
+
|
|
67
72
|
1.5.0
|
|
68
73
|
............
|
|
69
74
|
Added ``transition_slices_from_group``.
|
|
@@ -18,7 +18,7 @@ arraykit
|
|
|
18
18
|
|
|
19
19
|
The ArrayKit library provides utilities for creating and transforming NumPy arrays, implementing performance-critical StaticFrame operations as Python C extensions.
|
|
20
20
|
|
|
21
|
-
Code: https://github.com/
|
|
21
|
+
Code: https://github.com/static-frame/arraykit
|
|
22
22
|
|
|
23
23
|
Packages: https://pypi.org/project/arraykit
|
|
24
24
|
|
|
@@ -35,6 +35,11 @@ ArrayKit requires the following:
|
|
|
35
35
|
What is New in ArrayKit
|
|
36
36
|
-------------------------
|
|
37
37
|
|
|
38
|
+
1.6.0
|
|
39
|
+
............
|
|
40
|
+
Added ``factorize``.
|
|
41
|
+
|
|
42
|
+
|
|
38
43
|
1.5.0
|
|
39
44
|
............
|
|
40
45
|
Added ``transition_slices_from_group``.
|
arraykit-1.6.0/VERSION
ADDED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arraykit
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Array utilities for StaticFrame
|
|
5
5
|
Author: Christopher Ariza, Brandt Bucher, Charles Burkland
|
|
6
6
|
License: MIT
|
|
@@ -47,7 +47,7 @@ arraykit
|
|
|
47
47
|
|
|
48
48
|
The ArrayKit library provides utilities for creating and transforming NumPy arrays, implementing performance-critical StaticFrame operations as Python C extensions.
|
|
49
49
|
|
|
50
|
-
Code: https://github.com/
|
|
50
|
+
Code: https://github.com/static-frame/arraykit
|
|
51
51
|
|
|
52
52
|
Packages: https://pypi.org/project/arraykit
|
|
53
53
|
|
|
@@ -64,6 +64,11 @@ ArrayKit requires the following:
|
|
|
64
64
|
What is New in ArrayKit
|
|
65
65
|
-------------------------
|
|
66
66
|
|
|
67
|
+
1.6.0
|
|
68
|
+
............
|
|
69
|
+
Added ``factorize``.
|
|
70
|
+
|
|
71
|
+
|
|
67
72
|
1.5.0
|
|
68
73
|
............
|
|
69
74
|
Added ``transition_slices_from_group``.
|
|
@@ -36,6 +36,7 @@ test/test_block_index.py
|
|
|
36
36
|
test/test_delimited_to_arrays.py
|
|
37
37
|
test/test_delimited_to_arrays_integration.py
|
|
38
38
|
test/test_delimited_to_arrays_property.py
|
|
39
|
+
test/test_factorize.py
|
|
39
40
|
test/test_nonzero_1d.py
|
|
40
41
|
test/test_nonzero_1d_property.py
|
|
41
42
|
test/test_objectable.py
|
|
@@ -25,6 +25,7 @@ from ._arraykit import iterable_str_to_array_1d as iterable_str_to_array_1d
|
|
|
25
25
|
from ._arraykit import split_after_count as split_after_count
|
|
26
26
|
from ._arraykit import get_new_indexers_and_screen as get_new_indexers_and_screen
|
|
27
27
|
from ._arraykit import write_array_to_file as write_array_to_file
|
|
28
|
+
from ._arraykit import factorize as factorize
|
|
28
29
|
from ._arraykit import count_iteration as count_iteration
|
|
29
30
|
from ._arraykit import first_true_1d as first_true_1d
|
|
30
31
|
from ._arraykit import first_true_2d as first_true_2d
|
|
@@ -224,6 +224,9 @@ def write_array_to_file(
|
|
|
224
224
|
fortran_order: bool = False,
|
|
225
225
|
buffersize: int = 8192,
|
|
226
226
|
) -> None: ...
|
|
227
|
+
def factorize(
|
|
228
|
+
array: np.ndarray, *, sort: bool = ...
|
|
229
|
+
) -> tp.Tuple[np.ndarray, np.ndarray]: ...
|
|
227
230
|
def first_true_1d(__array: np.ndarray, *, forward: bool) -> int: ...
|
|
228
231
|
def first_true_2d(__array: np.ndarray, *, forward: bool, axis: int) -> np.ndarray: ...
|
|
229
232
|
def nonzero_1d(__array: np.ndarray, /) -> np.ndarray: ...
|
|
@@ -2520,6 +2520,406 @@ error:
|
|
|
2520
2520
|
# undef INSERT_FLEXIBLE
|
|
2521
2521
|
|
|
2522
2522
|
|
|
2523
|
+
//------------------------------------------------------------------------------
|
|
2524
|
+
// factorize
|
|
2525
|
+
|
|
2526
|
+
// A fast float hash for factorize only. Unlike double_to_hash (CPython-
|
|
2527
|
+
// compatible, so hash(1.0)==hash(1), via frexp + a loop), factorize never needs
|
|
2528
|
+
// cross-type hashing -- it only compares floats to floats from the same array by
|
|
2529
|
+
// `==`. So we canonicalize -0.0 to +0.0 (they compare equal, must hash equal),
|
|
2530
|
+
// reinterpret the bits, and apply a splitmix64 finalizer for good avalanche.
|
|
2531
|
+
// NaN never reaches here (handled before probing); +/-inf hash distinctly, which
|
|
2532
|
+
// is correct since they are distinct values. lookup_hash_double takes the hash as
|
|
2533
|
+
// a parameter, so this stays self-consistent within factorize's scratch table.
|
|
2534
|
+
static inline Py_hash_t
|
|
2535
|
+
factorize_double_to_hash(double v)
|
|
2536
|
+
{
|
|
2537
|
+
if (v == 0.0) {
|
|
2538
|
+
v = 0.0; // collapse -0.0 to +0.0
|
|
2539
|
+
}
|
|
2540
|
+
npy_uint64 x;
|
|
2541
|
+
memcpy(&x, &v, sizeof(x));
|
|
2542
|
+
x ^= x >> 33;
|
|
2543
|
+
x *= 0xff51afd7ed558ccdULL;
|
|
2544
|
+
x ^= x >> 33;
|
|
2545
|
+
x *= 0xc4ceb9fe1a85ec53ULL;
|
|
2546
|
+
x ^= x >> 33;
|
|
2547
|
+
Py_hash_t h = (Py_hash_t)x;
|
|
2548
|
+
return h == -1 ? -2 : h; // -1 marks an empty slot
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
// Detect a Python-level float NaN (a Python float or a NumPy floating scalar) so
|
|
2552
|
+
// that object-dtype arrays collapse all NaN into a single code, matching the
|
|
2553
|
+
// float-dtype behavior. Only real floats are treated as NaN here; None, NaT, and
|
|
2554
|
+
// complex NaN remain ordinary distinct keys. Returns 1 for float NaN, else 0.
|
|
2555
|
+
static inline int
|
|
2556
|
+
factorize_obj_is_float_nan(PyObject* key)
|
|
2557
|
+
{
|
|
2558
|
+
if (PyFloat_Check(key)) {
|
|
2559
|
+
return isnan(PyFloat_AS_DOUBLE(key));
|
|
2560
|
+
}
|
|
2561
|
+
if (PyArray_IsScalar(key, Half)) {
|
|
2562
|
+
return npy_half_isnan(PyArrayScalar_VAL(key, Half));
|
|
2563
|
+
}
|
|
2564
|
+
if (PyArray_IsScalar(key, Float32)) {
|
|
2565
|
+
return isnan(PyArrayScalar_VAL(key, Float32));
|
|
2566
|
+
}
|
|
2567
|
+
if (PyArray_IsScalar(key, Float64)) {
|
|
2568
|
+
return isnan(PyArrayScalar_VAL(key, Float64));
|
|
2569
|
+
}
|
|
2570
|
+
return 0;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
// Given a probe result `pos_expr` and its `hash_val` for the element at index
|
|
2574
|
+
// `i`, either assign a new sequential code (empty slot) or reuse the code of the
|
|
2575
|
+
// already-seen key (occupied slot). Stores the first-occurrence input index in
|
|
2576
|
+
// the table so the reused lookup_hash_* probes compare against the right value.
|
|
2577
|
+
// Requires `scratch`, `codes`, `code_of_index`, `first_index`, `k`, `i` in scope.
|
|
2578
|
+
# define FACTORIZE_RECORD(pos_expr, hash_val) \
|
|
2579
|
+
{ \
|
|
2580
|
+
Py_ssize_t _pos = (pos_expr); \
|
|
2581
|
+
if (_pos < 0) { \
|
|
2582
|
+
goto fail; \
|
|
2583
|
+
} \
|
|
2584
|
+
if (scratch.table[_pos].hash == -1) { \
|
|
2585
|
+
scratch.table[_pos].keys_pos = i; \
|
|
2586
|
+
scratch.table[_pos].hash = (hash_val); \
|
|
2587
|
+
first_index[k] = i; \
|
|
2588
|
+
code_of_index[i] = k; \
|
|
2589
|
+
codes[i] = k; \
|
|
2590
|
+
k++; \
|
|
2591
|
+
} \
|
|
2592
|
+
else { \
|
|
2593
|
+
codes[i] = code_of_index[scratch.table[_pos].keys_pos]; \
|
|
2594
|
+
} \
|
|
2595
|
+
} \
|
|
2596
|
+
|
|
2597
|
+
// Integer/unsigned scalar factorize loop. When the input is C-contiguous, index
|
|
2598
|
+
// a typed pointer (`b[i]`, compile-time itemsize) instead of PyArray_GETPTR1's
|
|
2599
|
+
// runtime stride multiply -- materially faster, matching INSERT_SCALARS.
|
|
2600
|
+
# define FACTORIZE_SCALAR_LOOP(npy_type, value_t, hash_func, lookup_func, kat_lookup) \
|
|
2601
|
+
{ \
|
|
2602
|
+
if (contiguous) { \
|
|
2603
|
+
const npy_type* b = (const npy_type*)PyArray_DATA(a); \
|
|
2604
|
+
const npy_type* b_end = b + n; \
|
|
2605
|
+
npy_intp i = 0; \
|
|
2606
|
+
while (b < b_end) { \
|
|
2607
|
+
value_t v = (value_t)*b; \
|
|
2608
|
+
Py_hash_t hash = hash_func(v); \
|
|
2609
|
+
FACTORIZE_RECORD(lookup_func(&scratch, v, hash, kat_lookup), hash); \
|
|
2610
|
+
b++; \
|
|
2611
|
+
i++; \
|
|
2612
|
+
} \
|
|
2613
|
+
} \
|
|
2614
|
+
else { \
|
|
2615
|
+
for (npy_intp i = 0; i < n; i++) { \
|
|
2616
|
+
value_t v = (value_t)*(const npy_type*)PyArray_GETPTR1(a, i); \
|
|
2617
|
+
Py_hash_t hash = hash_func(v); \
|
|
2618
|
+
FACTORIZE_RECORD(lookup_func(&scratch, v, hash, kat_lookup), hash); \
|
|
2619
|
+
} \
|
|
2620
|
+
} \
|
|
2621
|
+
} \
|
|
2622
|
+
|
|
2623
|
+
# define FACTORIZE_INT(npy_type, kat_lookup) \
|
|
2624
|
+
FACTORIZE_SCALAR_LOOP(npy_type, npy_int64, int_to_hash, lookup_hash_int, kat_lookup)
|
|
2625
|
+
|
|
2626
|
+
# define FACTORIZE_UINT(npy_type, kat_lookup) \
|
|
2627
|
+
FACTORIZE_SCALAR_LOOP(npy_type, npy_uint64, uint_to_hash, lookup_hash_uint, kat_lookup)
|
|
2628
|
+
|
|
2629
|
+
// Per-element float body. All NaN collapse to one code and never enter the table
|
|
2630
|
+
// (the map compares with `==`, so NaN would otherwise never match itself);
|
|
2631
|
+
// +0.0/-0.0 and inf are handled correctly by the normal path.
|
|
2632
|
+
# define FACTORIZE_FLOAT_ELEM(value_expr, kat_lookup) \
|
|
2633
|
+
{ \
|
|
2634
|
+
npy_double v = (value_expr); \
|
|
2635
|
+
if (v != v) { \
|
|
2636
|
+
if (nan_code < 0) { \
|
|
2637
|
+
nan_code = k; \
|
|
2638
|
+
first_index[k] = i; \
|
|
2639
|
+
codes[i] = k; \
|
|
2640
|
+
k++; \
|
|
2641
|
+
} \
|
|
2642
|
+
else { \
|
|
2643
|
+
codes[i] = nan_code; \
|
|
2644
|
+
} \
|
|
2645
|
+
} \
|
|
2646
|
+
else { \
|
|
2647
|
+
Py_hash_t hash = factorize_double_to_hash(v); \
|
|
2648
|
+
FACTORIZE_RECORD(lookup_hash_double(&scratch, v, hash, kat_lookup), hash); \
|
|
2649
|
+
} \
|
|
2650
|
+
} \
|
|
2651
|
+
|
|
2652
|
+
# define FACTORIZE_FLOAT(npy_type, kat_lookup, post_deref) \
|
|
2653
|
+
{ \
|
|
2654
|
+
if (contiguous) { \
|
|
2655
|
+
const npy_type* b = (const npy_type*)PyArray_DATA(a); \
|
|
2656
|
+
const npy_type* b_end = b + n; \
|
|
2657
|
+
npy_intp i = 0; \
|
|
2658
|
+
while (b < b_end) { \
|
|
2659
|
+
FACTORIZE_FLOAT_ELEM(post_deref(*b), kat_lookup) \
|
|
2660
|
+
b++; \
|
|
2661
|
+
i++; \
|
|
2662
|
+
} \
|
|
2663
|
+
} \
|
|
2664
|
+
else { \
|
|
2665
|
+
for (npy_intp i = 0; i < n; i++) { \
|
|
2666
|
+
FACTORIZE_FLOAT_ELEM(post_deref(*(const npy_type*)PyArray_GETPTR1(a, i)), kat_lookup) \
|
|
2667
|
+
} \
|
|
2668
|
+
} \
|
|
2669
|
+
} \
|
|
2670
|
+
|
|
2671
|
+
// Flexible (unicode/string) loop. In the contiguous case step a running pointer
|
|
2672
|
+
// by dt_size (incremental add, no per-element multiply), matching INSERT_FLEXIBLE.
|
|
2673
|
+
# define FACTORIZE_FLEXIBLE(char_type, lookup_func, hash_func, get_end_func, dt_size_expr) \
|
|
2674
|
+
{ \
|
|
2675
|
+
Py_ssize_t dt_size = (dt_size_expr); \
|
|
2676
|
+
if (contiguous) { \
|
|
2677
|
+
char_type* v = (char_type*)PyArray_DATA(a); \
|
|
2678
|
+
for (npy_intp i = 0; i < n; i++) { \
|
|
2679
|
+
Py_ssize_t ksize = get_end_func(v, dt_size) - v; \
|
|
2680
|
+
Py_hash_t hash = hash_func(v, ksize); \
|
|
2681
|
+
FACTORIZE_RECORD(lookup_func(&scratch, v, ksize, hash), hash); \
|
|
2682
|
+
v += dt_size; \
|
|
2683
|
+
} \
|
|
2684
|
+
} \
|
|
2685
|
+
else { \
|
|
2686
|
+
for (npy_intp i = 0; i < n; i++) { \
|
|
2687
|
+
char_type* v = (char_type*)PyArray_GETPTR1(a, i); \
|
|
2688
|
+
Py_ssize_t ksize = get_end_func(v, dt_size) - v; \
|
|
2689
|
+
Py_hash_t hash = hash_func(v, ksize); \
|
|
2690
|
+
FACTORIZE_RECORD(lookup_func(&scratch, v, ksize, hash), hash); \
|
|
2691
|
+
} \
|
|
2692
|
+
} \
|
|
2693
|
+
} \
|
|
2694
|
+
|
|
2695
|
+
// Hash-based factorize: return (uniques, codes) such that
|
|
2696
|
+
// array[i] == uniques[codes[i]], in O(n), reusing the AutoMap hash table.
|
|
2697
|
+
PyObject *
|
|
2698
|
+
factorize(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs)
|
|
2699
|
+
{
|
|
2700
|
+
static char *kwlist[] = {"array", "sort", NULL};
|
|
2701
|
+
PyObject *array_obj = NULL;
|
|
2702
|
+
int sort = 0;
|
|
2703
|
+
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
|
|
2704
|
+
"O|$p:factorize", kwlist,
|
|
2705
|
+
&array_obj,
|
|
2706
|
+
&sort)) {
|
|
2707
|
+
return NULL;
|
|
2708
|
+
}
|
|
2709
|
+
if (!PyArray_Check(array_obj)) {
|
|
2710
|
+
PyErr_Format(PyExc_TypeError,
|
|
2711
|
+
"Expected a NumPy array, not %s.", Py_TYPE(array_obj)->tp_name);
|
|
2712
|
+
return NULL;
|
|
2713
|
+
}
|
|
2714
|
+
PyArrayObject *a = (PyArrayObject *)array_obj;
|
|
2715
|
+
if (PyArray_NDIM(a) != 1) {
|
|
2716
|
+
PyErr_SetString(PyExc_TypeError, "Array must be 1-dimensional");
|
|
2717
|
+
return NULL;
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
npy_intp n = PyArray_SIZE(a);
|
|
2721
|
+
int array_t = PyArray_TYPE(a);
|
|
2722
|
+
KeysArrayType kat = at_to_kat(array_t, a);
|
|
2723
|
+
|
|
2724
|
+
PyObject *codes_arr = NULL;
|
|
2725
|
+
PyObject *idx_arr = NULL;
|
|
2726
|
+
PyObject *uniques = NULL;
|
|
2727
|
+
PyObject *list = NULL; // object/KAT_LIST path only
|
|
2728
|
+
npy_intp *code_of_index = NULL; // input index -> code
|
|
2729
|
+
npy_intp *first_index = NULL; // code -> first-occurrence input index
|
|
2730
|
+
npy_intp *rank = NULL; // sort remap
|
|
2731
|
+
Py_ssize_t k = 0; // running distinct count
|
|
2732
|
+
Py_ssize_t nan_code = -1; // shared code for all NaN, -1 until seen
|
|
2733
|
+
|
|
2734
|
+
// A private, stack-allocated scratch table; never exposed to Python. Only
|
|
2735
|
+
// the fields read by grow_table / lookup_hash_* are set.
|
|
2736
|
+
FAMObject scratch;
|
|
2737
|
+
scratch.table = NULL;
|
|
2738
|
+
scratch.table_size = 0;
|
|
2739
|
+
scratch.keys = NULL;
|
|
2740
|
+
scratch.keys_array_type = kat;
|
|
2741
|
+
scratch.keys_size = n;
|
|
2742
|
+
scratch.key_buffer = NULL; // never needed by the array lookups
|
|
2743
|
+
|
|
2744
|
+
codes_arr = PyArray_EMPTY(1, &n, NPY_INTP, 0);
|
|
2745
|
+
if (!codes_arr) {
|
|
2746
|
+
goto fail;
|
|
2747
|
+
}
|
|
2748
|
+
npy_intp *codes = (npy_intp*)PyArray_DATA((PyArrayObject*)codes_arr);
|
|
2749
|
+
|
|
2750
|
+
if (n > 0) {
|
|
2751
|
+
code_of_index = PyMem_New(npy_intp, n);
|
|
2752
|
+
first_index = PyMem_New(npy_intp, n);
|
|
2753
|
+
if (!code_of_index || !first_index) {
|
|
2754
|
+
PyErr_NoMemory();
|
|
2755
|
+
goto fail;
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
// The lookups dereference scratch.keys at stored input indices. For a usable
|
|
2760
|
+
// array KAT that is the (borrowed) input array; otherwise build a list.
|
|
2761
|
+
if (kat) {
|
|
2762
|
+
scratch.keys = array_obj;
|
|
2763
|
+
}
|
|
2764
|
+
else {
|
|
2765
|
+
if (array_t == NPY_DATETIME || array_t == NPY_TIMEDELTA) {
|
|
2766
|
+
list = PySequence_List(array_obj);
|
|
2767
|
+
}
|
|
2768
|
+
else {
|
|
2769
|
+
list = PyArray_ToList(a);
|
|
2770
|
+
}
|
|
2771
|
+
if (!list) {
|
|
2772
|
+
goto fail;
|
|
2773
|
+
}
|
|
2774
|
+
scratch.keys = list;
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
if (grow_table(&scratch, n)) {
|
|
2778
|
+
goto fail;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
// Enables the typed-pointer fast path in the scalar/flexible loops below.
|
|
2782
|
+
int contiguous = PyArray_IS_C_CONTIGUOUS(a);
|
|
2783
|
+
|
|
2784
|
+
switch (kat) {
|
|
2785
|
+
case KAT_INT8: FACTORIZE_INT(npy_int8, KAT_INT8); break;
|
|
2786
|
+
case KAT_INT16: FACTORIZE_INT(npy_int16, KAT_INT16); break;
|
|
2787
|
+
case KAT_INT32: FACTORIZE_INT(npy_int32, KAT_INT32); break;
|
|
2788
|
+
case KAT_INT64: FACTORIZE_INT(npy_int64, KAT_INT64); break;
|
|
2789
|
+
case KAT_UINT8: FACTORIZE_UINT(npy_uint8, KAT_UINT8); break;
|
|
2790
|
+
case KAT_UINT16: FACTORIZE_UINT(npy_uint16, KAT_UINT16); break;
|
|
2791
|
+
case KAT_UINT32: FACTORIZE_UINT(npy_uint32, KAT_UINT32); break;
|
|
2792
|
+
case KAT_UINT64: FACTORIZE_UINT(npy_uint64, KAT_UINT64); break;
|
|
2793
|
+
case KAT_FLOAT16: FACTORIZE_FLOAT(npy_half, KAT_FLOAT16, npy_half_to_double); break;
|
|
2794
|
+
case KAT_FLOAT32: FACTORIZE_FLOAT(npy_float, KAT_FLOAT32, ); break;
|
|
2795
|
+
case KAT_FLOAT64: FACTORIZE_FLOAT(npy_double, KAT_FLOAT64, ); break;
|
|
2796
|
+
case KAT_UNICODE:
|
|
2797
|
+
FACTORIZE_FLEXIBLE(Py_UCS4, lookup_hash_unicode, unicode_to_hash,
|
|
2798
|
+
ucs4_get_end_p, PyArray_ITEMSIZE(a) / UCS4_SIZE);
|
|
2799
|
+
break;
|
|
2800
|
+
case KAT_STRING:
|
|
2801
|
+
FACTORIZE_FLEXIBLE(char, lookup_hash_string, string_to_hash,
|
|
2802
|
+
char_get_end_p, PyArray_ITEMSIZE(a));
|
|
2803
|
+
break;
|
|
2804
|
+
case KAT_DTY:
|
|
2805
|
+
case KAT_DTM:
|
|
2806
|
+
case KAT_DTW:
|
|
2807
|
+
case KAT_DTD:
|
|
2808
|
+
case KAT_DTh:
|
|
2809
|
+
case KAT_DTm:
|
|
2810
|
+
case KAT_DTs:
|
|
2811
|
+
case KAT_DTms:
|
|
2812
|
+
case KAT_DTus:
|
|
2813
|
+
case KAT_DTns:
|
|
2814
|
+
case KAT_DTps:
|
|
2815
|
+
case KAT_DTfs:
|
|
2816
|
+
case KAT_DTas:
|
|
2817
|
+
// datetime64/timedelta64 store an int64; NaT (INT64_MIN) compares
|
|
2818
|
+
// equal to itself here, so all NaT collapse into one code naturally.
|
|
2819
|
+
FACTORIZE_INT(npy_int64, KAT_INT64);
|
|
2820
|
+
break;
|
|
2821
|
+
default: { // KAT_LIST: object dtype, complex, dt64 without a unit
|
|
2822
|
+
for (npy_intp i = 0; i < n; i++) {
|
|
2823
|
+
PyObject* key = PyList_GET_ITEM(list, i); // borrowed
|
|
2824
|
+
if (factorize_obj_is_float_nan(key)) {
|
|
2825
|
+
if (nan_code < 0) {
|
|
2826
|
+
nan_code = k;
|
|
2827
|
+
first_index[k] = i;
|
|
2828
|
+
codes[i] = k;
|
|
2829
|
+
k++;
|
|
2830
|
+
}
|
|
2831
|
+
else {
|
|
2832
|
+
codes[i] = nan_code;
|
|
2833
|
+
}
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
Py_hash_t hash = PyObject_Hash(key);
|
|
2837
|
+
if (hash == -1) {
|
|
2838
|
+
goto fail;
|
|
2839
|
+
}
|
|
2840
|
+
FACTORIZE_RECORD(lookup_hash_obj(&scratch, key, hash), hash);
|
|
2841
|
+
}
|
|
2842
|
+
break;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
// uniques: take the first-occurrence values out of the input, same dtype.
|
|
2847
|
+
idx_arr = PyArray_EMPTY(1, &k, NPY_INTP, 0);
|
|
2848
|
+
if (!idx_arr) {
|
|
2849
|
+
goto fail;
|
|
2850
|
+
}
|
|
2851
|
+
if (k > 0) {
|
|
2852
|
+
memcpy(PyArray_DATA((PyArrayObject*)idx_arr), first_index, k * sizeof(npy_intp));
|
|
2853
|
+
}
|
|
2854
|
+
uniques = PyArray_TakeFrom(a, idx_arr, 0, NULL, NPY_RAISE);
|
|
2855
|
+
if (!uniques) {
|
|
2856
|
+
goto fail;
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
if (sort && k > 1) {
|
|
2860
|
+
PyObject* order = PyArray_ArgSort((PyArrayObject*)uniques, 0, NPY_QUICKSORT);
|
|
2861
|
+
if (!order) {
|
|
2862
|
+
goto fail;
|
|
2863
|
+
}
|
|
2864
|
+
npy_intp* order_data = (npy_intp*)PyArray_DATA((PyArrayObject*)order);
|
|
2865
|
+
rank = PyMem_New(npy_intp, k);
|
|
2866
|
+
if (!rank) {
|
|
2867
|
+
Py_DECREF(order);
|
|
2868
|
+
PyErr_NoMemory();
|
|
2869
|
+
goto fail;
|
|
2870
|
+
}
|
|
2871
|
+
for (npy_intp j = 0; j < k; j++) {
|
|
2872
|
+
rank[order_data[j]] = j;
|
|
2873
|
+
}
|
|
2874
|
+
for (npy_intp i = 0; i < n; i++) {
|
|
2875
|
+
codes[i] = rank[codes[i]];
|
|
2876
|
+
}
|
|
2877
|
+
PyObject* uniques_sorted = PyArray_TakeFrom(
|
|
2878
|
+
(PyArrayObject*)uniques, order, 0, NULL, NPY_RAISE);
|
|
2879
|
+
Py_DECREF(order);
|
|
2880
|
+
if (!uniques_sorted) {
|
|
2881
|
+
goto fail;
|
|
2882
|
+
}
|
|
2883
|
+
Py_SETREF(uniques, uniques_sorted);
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
PyArray_CLEARFLAGS((PyArrayObject*)codes_arr, NPY_ARRAY_WRITEABLE);
|
|
2887
|
+
PyArray_CLEARFLAGS((PyArrayObject*)uniques, NPY_ARRAY_WRITEABLE);
|
|
2888
|
+
|
|
2889
|
+
PyMem_Free(scratch.table);
|
|
2890
|
+
PyMem_Free(code_of_index);
|
|
2891
|
+
PyMem_Free(first_index);
|
|
2892
|
+
PyMem_Free(rank);
|
|
2893
|
+
Py_DECREF(idx_arr);
|
|
2894
|
+
Py_XDECREF(list);
|
|
2895
|
+
|
|
2896
|
+
PyObject* result = PyTuple_Pack(2, uniques, codes_arr);
|
|
2897
|
+
Py_DECREF(uniques);
|
|
2898
|
+
Py_DECREF(codes_arr);
|
|
2899
|
+
return result;
|
|
2900
|
+
|
|
2901
|
+
fail:
|
|
2902
|
+
PyMem_Free(scratch.table);
|
|
2903
|
+
PyMem_Free(code_of_index);
|
|
2904
|
+
PyMem_Free(first_index);
|
|
2905
|
+
PyMem_Free(rank);
|
|
2906
|
+
Py_XDECREF(idx_arr);
|
|
2907
|
+
Py_XDECREF(list);
|
|
2908
|
+
Py_XDECREF(codes_arr);
|
|
2909
|
+
Py_XDECREF(uniques);
|
|
2910
|
+
return NULL;
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
# undef FACTORIZE_RECORD
|
|
2914
|
+
# undef FACTORIZE_SCALAR_LOOP
|
|
2915
|
+
# undef FACTORIZE_INT
|
|
2916
|
+
# undef FACTORIZE_UINT
|
|
2917
|
+
# undef FACTORIZE_FLOAT_ELEM
|
|
2918
|
+
# undef FACTORIZE_FLOAT
|
|
2919
|
+
# undef FACTORIZE_FLEXIBLE
|
|
2920
|
+
|
|
2921
|
+
//------------------------------------------------------------------------------
|
|
2922
|
+
|
|
2523
2923
|
static PyObject *
|
|
2524
2924
|
fam_repr(FAMObject *self)
|
|
2525
2925
|
{
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from arraykit import factorize
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _eq_elem(x, y):
|
|
9
|
+
# NaN/NaT-aware scalar equality (nan != nan under ==)
|
|
10
|
+
try:
|
|
11
|
+
if x != x and y != y:
|
|
12
|
+
return True
|
|
13
|
+
except (TypeError, ValueError):
|
|
14
|
+
pass
|
|
15
|
+
try:
|
|
16
|
+
return bool(x == y)
|
|
17
|
+
except (TypeError, ValueError):
|
|
18
|
+
return x is y
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def roundtrips(uniques, codes, array):
|
|
22
|
+
reconstructed = uniques[codes]
|
|
23
|
+
if len(array) != len(codes):
|
|
24
|
+
return False
|
|
25
|
+
return all(_eq_elem(reconstructed[i], array[i]) for i in range(len(array)))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestUnit(unittest.TestCase):
|
|
29
|
+
# ------------------------------------------------------------------
|
|
30
|
+
# basic behavior
|
|
31
|
+
|
|
32
|
+
def test_factorize_int_a(self) -> None:
|
|
33
|
+
a = np.array([10, 10, 10, 20, 20, 30])
|
|
34
|
+
uniques, codes = factorize(a)
|
|
35
|
+
self.assertEqual(uniques.tolist(), [10, 20, 30]) # first-appearance order
|
|
36
|
+
self.assertEqual(codes.tolist(), [0, 0, 0, 1, 1, 2])
|
|
37
|
+
self.assertTrue(roundtrips(uniques, codes, a))
|
|
38
|
+
|
|
39
|
+
def test_factorize_first_appearance_order(self) -> None:
|
|
40
|
+
a = np.array([30, 10, 10, 20, 30])
|
|
41
|
+
uniques, codes = factorize(a)
|
|
42
|
+
self.assertEqual(uniques.tolist(), [30, 10, 20])
|
|
43
|
+
self.assertEqual(codes.tolist(), [0, 1, 1, 2, 0])
|
|
44
|
+
|
|
45
|
+
def test_factorize_codes_dtype_intp(self) -> None:
|
|
46
|
+
_, codes = factorize(np.array([1, 2, 3]))
|
|
47
|
+
self.assertEqual(codes.dtype, np.dtype(np.intp))
|
|
48
|
+
|
|
49
|
+
def test_factorize_codes_in_range(self) -> None:
|
|
50
|
+
a = np.arange(50) % 7
|
|
51
|
+
uniques, codes = factorize(a)
|
|
52
|
+
self.assertTrue((codes >= 0).all())
|
|
53
|
+
self.assertTrue((codes < len(uniques)).all())
|
|
54
|
+
|
|
55
|
+
def test_factorize_outputs_immutable(self) -> None:
|
|
56
|
+
uniques, codes = factorize(np.array([1, 1, 2]))
|
|
57
|
+
self.assertFalse(uniques.flags.writeable)
|
|
58
|
+
self.assertFalse(codes.flags.writeable)
|
|
59
|
+
|
|
60
|
+
# ------------------------------------------------------------------
|
|
61
|
+
# dtype coverage (each hash path)
|
|
62
|
+
|
|
63
|
+
def test_factorize_int_widths(self) -> None:
|
|
64
|
+
for dt in (np.int8, np.int16, np.int32, np.int64):
|
|
65
|
+
a = np.array([1, 1, 2, -3, -3, 2], dtype=dt)
|
|
66
|
+
uniques, codes = factorize(a)
|
|
67
|
+
self.assertTrue(roundtrips(uniques, codes, a), dt)
|
|
68
|
+
self.assertEqual(uniques.dtype, np.dtype(dt))
|
|
69
|
+
|
|
70
|
+
def test_factorize_uint_widths(self) -> None:
|
|
71
|
+
for dt in (np.uint8, np.uint16, np.uint32, np.uint64):
|
|
72
|
+
a = np.array([1, 1, 2, 200, 200], dtype=dt)
|
|
73
|
+
uniques, codes = factorize(a)
|
|
74
|
+
self.assertTrue(roundtrips(uniques, codes, a), dt)
|
|
75
|
+
|
|
76
|
+
def test_factorize_float64(self) -> None:
|
|
77
|
+
a = np.array([1.5, 1.5, 2.5, 0.0, -0.0, np.inf, -np.inf, np.inf])
|
|
78
|
+
uniques, codes = factorize(a)
|
|
79
|
+
self.assertTrue(roundtrips(uniques, codes, a))
|
|
80
|
+
# +0.0 and -0.0 are equal -> one group
|
|
81
|
+
self.assertEqual(codes[3], codes[4])
|
|
82
|
+
# +inf and -inf are distinct
|
|
83
|
+
self.assertNotEqual(codes[5], codes[6])
|
|
84
|
+
self.assertEqual(codes[5], codes[7])
|
|
85
|
+
|
|
86
|
+
def test_factorize_float32(self) -> None:
|
|
87
|
+
a = np.array([1, 1, 2, np.nan, np.nan], dtype=np.float32)
|
|
88
|
+
uniques, codes = factorize(a)
|
|
89
|
+
self.assertTrue(roundtrips(uniques, codes, a))
|
|
90
|
+
|
|
91
|
+
def test_factorize_float16(self) -> None:
|
|
92
|
+
a = np.array([1, 1, 2, 2, 3, np.nan, np.nan], dtype=np.float16)
|
|
93
|
+
uniques, codes = factorize(a)
|
|
94
|
+
self.assertTrue(roundtrips(uniques, codes, a))
|
|
95
|
+
self.assertEqual(codes[5], codes[6]) # NaN collapse
|
|
96
|
+
|
|
97
|
+
def test_factorize_unicode(self) -> None:
|
|
98
|
+
a = np.array(['a', 'a', 'bb', 'b', 'b', 'a'])
|
|
99
|
+
uniques, codes = factorize(a)
|
|
100
|
+
self.assertEqual(uniques.tolist(), ['a', 'bb', 'b'])
|
|
101
|
+
self.assertEqual(codes.tolist(), [0, 0, 1, 2, 2, 0])
|
|
102
|
+
self.assertTrue(roundtrips(uniques, codes, a))
|
|
103
|
+
|
|
104
|
+
def test_factorize_bytes(self) -> None:
|
|
105
|
+
a = np.array([b'a', b'a', b'bb', b'b'], dtype='S2')
|
|
106
|
+
uniques, codes = factorize(a)
|
|
107
|
+
self.assertTrue(roundtrips(uniques, codes, a))
|
|
108
|
+
|
|
109
|
+
def test_factorize_datetime64(self) -> None:
|
|
110
|
+
a = np.array(
|
|
111
|
+
['2020-01-01', '2020-01-01', '2021-01-01', '2020-01-01'],
|
|
112
|
+
dtype='datetime64[D]',
|
|
113
|
+
)
|
|
114
|
+
uniques, codes = factorize(a)
|
|
115
|
+
self.assertEqual(codes.tolist(), [0, 0, 1, 0])
|
|
116
|
+
self.assertTrue(roundtrips(uniques, codes, a))
|
|
117
|
+
|
|
118
|
+
def test_factorize_timedelta64(self) -> None:
|
|
119
|
+
a = np.array([5, 5, 10, 10, 5], dtype='timedelta64[D]')
|
|
120
|
+
uniques, codes = factorize(a)
|
|
121
|
+
self.assertTrue(roundtrips(uniques, codes, a))
|
|
122
|
+
|
|
123
|
+
# ------------------------------------------------------------------
|
|
124
|
+
# NaN / NaT semantics
|
|
125
|
+
|
|
126
|
+
def test_factorize_float_nan_one_group(self) -> None:
|
|
127
|
+
a = np.array([1.0, np.nan, np.nan, 2.0, np.nan])
|
|
128
|
+
uniques, codes = factorize(a)
|
|
129
|
+
nan_codes = {codes[1], codes[2], codes[4]}
|
|
130
|
+
self.assertEqual(len(nan_codes), 1) # all NaN share one code
|
|
131
|
+
self.assertEqual(np.isnan(uniques).sum(), 1) # exactly one NaN unique
|
|
132
|
+
self.assertTrue(roundtrips(uniques, codes, a))
|
|
133
|
+
|
|
134
|
+
def test_factorize_datetime_nat_one_group(self) -> None:
|
|
135
|
+
nat = np.datetime64('NaT')
|
|
136
|
+
a = np.array([nat, nat, np.datetime64('2020'), nat], dtype='datetime64[Y]')
|
|
137
|
+
_, codes = factorize(a)
|
|
138
|
+
self.assertEqual(codes[0], codes[1])
|
|
139
|
+
self.assertEqual(codes[0], codes[3])
|
|
140
|
+
self.assertNotEqual(codes[0], codes[2])
|
|
141
|
+
|
|
142
|
+
def test_factorize_object_nan_matches_float(self) -> None:
|
|
143
|
+
a = np.array(
|
|
144
|
+
['a', 'a', float('nan'), float('nan'), None, None, 1, 1],
|
|
145
|
+
dtype=object,
|
|
146
|
+
)
|
|
147
|
+
uniques, codes = factorize(a)
|
|
148
|
+
self.assertTrue(roundtrips(uniques, codes, a))
|
|
149
|
+
self.assertEqual(codes[2], codes[3]) # float('nan') collapse
|
|
150
|
+
self.assertEqual(codes[4], codes[5]) # None is its own group
|
|
151
|
+
self.assertNotEqual(codes[2], codes[4]) # nan != None
|
|
152
|
+
|
|
153
|
+
def test_factorize_object_mixed_types(self) -> None:
|
|
154
|
+
a = np.array([1, '1', 1.0, 2], dtype=object)
|
|
155
|
+
uniques, codes = factorize(a)
|
|
156
|
+
self.assertTrue(roundtrips(uniques, codes, a))
|
|
157
|
+
self.assertNotEqual(codes[0], codes[1]) # 1 (int) != '1' (str)
|
|
158
|
+
self.assertEqual(codes[0], codes[2]) # 1 == 1.0 by value
|
|
159
|
+
|
|
160
|
+
# ------------------------------------------------------------------
|
|
161
|
+
# sort=True
|
|
162
|
+
|
|
163
|
+
def test_factorize_sort_int(self) -> None:
|
|
164
|
+
a = np.array([30, 10, 10, 20, 30])
|
|
165
|
+
uniques, codes = factorize(a, sort=True)
|
|
166
|
+
self.assertEqual(uniques.tolist(), [10, 20, 30])
|
|
167
|
+
self.assertTrue(roundtrips(uniques, codes, a))
|
|
168
|
+
|
|
169
|
+
def test_factorize_sort_reconstruction_matches_unsorted(self) -> None:
|
|
170
|
+
a = np.array([3.0, np.nan, 1.0, np.nan, 2.0, 1.0])
|
|
171
|
+
u0, c0 = factorize(a, sort=False)
|
|
172
|
+
u1, c1 = factorize(a, sort=True)
|
|
173
|
+
# both reconstruct the same original array
|
|
174
|
+
self.assertTrue(roundtrips(u0, c0, a))
|
|
175
|
+
self.assertTrue(roundtrips(u1, c1, a))
|
|
176
|
+
# sorted uniques are ascending (NaN sorts last)
|
|
177
|
+
finite = u1[~np.isnan(u1)]
|
|
178
|
+
self.assertEqual(finite.tolist(), sorted(finite.tolist()))
|
|
179
|
+
|
|
180
|
+
def test_factorize_sort_strings(self) -> None:
|
|
181
|
+
a = np.array(['c', 'a', 'b', 'a', 'c'])
|
|
182
|
+
uniques, codes = factorize(a, sort=True)
|
|
183
|
+
self.assertEqual(uniques.tolist(), ['a', 'b', 'c'])
|
|
184
|
+
self.assertTrue(roundtrips(uniques, codes, a))
|
|
185
|
+
|
|
186
|
+
def test_factorize_sort_is_keyword_only(self) -> None:
|
|
187
|
+
a = np.array([1, 2, 3])
|
|
188
|
+
with self.assertRaises(TypeError):
|
|
189
|
+
factorize(a, True) # type: ignore
|
|
190
|
+
|
|
191
|
+
def test_factorize_sort_unorderable_object_raises(self) -> None:
|
|
192
|
+
a = np.array([1, 'a', 2], dtype=object)
|
|
193
|
+
with self.assertRaises(TypeError):
|
|
194
|
+
factorize(a, sort=True)
|
|
195
|
+
|
|
196
|
+
# ------------------------------------------------------------------
|
|
197
|
+
# edge cases
|
|
198
|
+
|
|
199
|
+
def test_factorize_empty(self) -> None:
|
|
200
|
+
for a in (np.array([], dtype=np.int64), np.array([], dtype=float),
|
|
201
|
+
np.array([], dtype='U4'), np.array([], dtype=object)):
|
|
202
|
+
uniques, codes = factorize(a)
|
|
203
|
+
self.assertEqual(len(uniques), 0)
|
|
204
|
+
self.assertEqual(len(codes), 0)
|
|
205
|
+
self.assertEqual(codes.dtype, np.dtype(np.intp))
|
|
206
|
+
self.assertEqual(uniques.dtype, a.dtype)
|
|
207
|
+
|
|
208
|
+
def test_factorize_single(self) -> None:
|
|
209
|
+
uniques, codes = factorize(np.array([7]))
|
|
210
|
+
self.assertEqual(uniques.tolist(), [7])
|
|
211
|
+
self.assertEqual(codes.tolist(), [0])
|
|
212
|
+
|
|
213
|
+
def test_factorize_all_unique(self) -> None:
|
|
214
|
+
a = np.array([1, 2, 3, 4])
|
|
215
|
+
uniques, codes = factorize(a)
|
|
216
|
+
self.assertEqual(codes.tolist(), [0, 1, 2, 3])
|
|
217
|
+
self.assertEqual(uniques.tolist(), [1, 2, 3, 4])
|
|
218
|
+
|
|
219
|
+
def test_factorize_all_same(self) -> None:
|
|
220
|
+
a = np.array([5, 5, 5, 5])
|
|
221
|
+
uniques, codes = factorize(a)
|
|
222
|
+
self.assertEqual(uniques.tolist(), [5])
|
|
223
|
+
self.assertEqual(codes.tolist(), [0, 0, 0, 0])
|
|
224
|
+
|
|
225
|
+
def test_factorize_non_contiguous(self) -> None:
|
|
226
|
+
# underlying [1,1,2,2,3,3]; [::2] view is [1,2,3]
|
|
227
|
+
a = np.array([1, 1, 2, 2, 3, 3], dtype=np.int64)[::2]
|
|
228
|
+
self.assertFalse(a.flags.c_contiguous)
|
|
229
|
+
uniques, codes = factorize(a)
|
|
230
|
+
self.assertEqual(uniques.tolist(), [1, 2, 3])
|
|
231
|
+
self.assertEqual(codes.tolist(), [0, 1, 2])
|
|
232
|
+
|
|
233
|
+
def test_factorize_mutable_input_accepted(self) -> None:
|
|
234
|
+
a = np.array([1, 1, 2])
|
|
235
|
+
self.assertTrue(a.flags.writeable)
|
|
236
|
+
uniques, codes = factorize(a) # must not require immutable input
|
|
237
|
+
self.assertTrue(roundtrips(uniques, codes, a))
|
|
238
|
+
|
|
239
|
+
# ------------------------------------------------------------------
|
|
240
|
+
# input validation
|
|
241
|
+
|
|
242
|
+
def test_factorize_requires_array(self) -> None:
|
|
243
|
+
with self.assertRaises(TypeError):
|
|
244
|
+
factorize([1, 2, 3]) # type: ignore
|
|
245
|
+
|
|
246
|
+
def test_factorize_requires_1d(self) -> None:
|
|
247
|
+
with self.assertRaises(TypeError):
|
|
248
|
+
factorize(np.arange(4).reshape(2, 2))
|
|
249
|
+
|
|
250
|
+
# ------------------------------------------------------------------
|
|
251
|
+
# parity with numpy sort-based unique/inverse
|
|
252
|
+
|
|
253
|
+
def test_factorize_parity_np_unique(self) -> None:
|
|
254
|
+
rng = np.random.RandomState(0)
|
|
255
|
+
for _ in range(50):
|
|
256
|
+
a = rng.randint(0, 6, size=rng.randint(0, 40)).astype(np.int64)
|
|
257
|
+
uniques, codes = factorize(a, sort=True)
|
|
258
|
+
u_np, inv_np = np.unique(a, return_inverse=True)
|
|
259
|
+
self.assertEqual(uniques.tolist(), u_np.tolist())
|
|
260
|
+
self.assertEqual(codes.tolist(), inv_np.ravel().tolist())
|
|
261
|
+
|
|
262
|
+
def test_factorize_parity_np_unique_strings(self) -> None:
|
|
263
|
+
rng = np.random.RandomState(1)
|
|
264
|
+
pool = np.array(['aa', 'bb', 'cc', 'dd', 'ee'])
|
|
265
|
+
for _ in range(30):
|
|
266
|
+
a = pool[rng.randint(0, 5, size=rng.randint(1, 40))]
|
|
267
|
+
uniques, codes = factorize(a, sort=True)
|
|
268
|
+
u_np, inv_np = np.unique(a, return_inverse=True)
|
|
269
|
+
self.assertEqual(uniques.tolist(), u_np.tolist())
|
|
270
|
+
self.assertEqual(codes.tolist(), inv_np.ravel().tolist())
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == '__main__':
|
|
274
|
+
unittest.main()
|
arraykit-1.5.0/VERSION
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|