arraykit 1.10.0__tar.gz → 1.11.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.10.0/arraykit.egg-info → arraykit-1.11.0}/PKG-INFO +7 -1
- {arraykit-1.10.0 → arraykit-1.11.0}/README.rst +6 -0
- arraykit-1.11.0/VERSION +2 -0
- {arraykit-1.10.0 → arraykit-1.11.0/arraykit.egg-info}/PKG-INFO +7 -1
- {arraykit-1.10.0 → arraykit-1.11.0}/arraykit.egg-info/SOURCES.txt +1 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/__init__.py +1 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/__init__.pyi +3 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/_arraykit.c +4 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/methods.c +275 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/methods.h +3 -0
- arraykit-1.11.0/test/test_group_reduce.py +217 -0
- arraykit-1.10.0/VERSION +0 -2
- {arraykit-1.10.0 → arraykit-1.11.0}/LICENSE.txt +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/MANIFEST.in +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/arraykit.egg-info/dependency_links.txt +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/arraykit.egg-info/requires.txt +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/arraykit.egg-info/top_level.txt +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/pyproject.toml +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/setup.cfg +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/setup.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/array_go.c +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/array_go.h +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/array_to_tuple.c +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/array_to_tuple.h +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/auto_map.c +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/auto_map.h +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/block_index.c +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/block_index.h +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/delimited_to_arrays.c +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/delimited_to_arrays.h +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/py.typed +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/tri_map.c +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/tri_map.h +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/src/utilities.h +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_array_go.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_astype_array.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_auto_map.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_auto_map_property.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_block_index.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_delimited_to_arrays.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_delimited_to_arrays_integration.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_delimited_to_arrays_property.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_factorize.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_fill_directional.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_group_ordering.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_nonzero_1d.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_nonzero_1d_property.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_objectable.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_pyi.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_split_after_count.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_transition_slices_from_group.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_tri_map.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.0}/test/test_type_discovery.py +0 -0
- {arraykit-1.10.0 → arraykit-1.11.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.11.0
|
|
4
4
|
Summary: Array utilities for StaticFrame
|
|
5
5
|
Author: Christopher Ariza, Brandt Bucher, Charles Burkland
|
|
6
6
|
License: MIT
|
|
@@ -64,6 +64,12 @@ ArrayKit requires the following:
|
|
|
64
64
|
What is New in ArrayKit
|
|
65
65
|
-------------------------
|
|
66
66
|
|
|
67
|
+
1.11.0
|
|
68
|
+
............
|
|
69
|
+
|
|
70
|
+
Added ``group_reduce()``.
|
|
71
|
+
|
|
72
|
+
|
|
67
73
|
1.10.0
|
|
68
74
|
............
|
|
69
75
|
|
arraykit-1.11.0/VERSION
ADDED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arraykit
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.11.0
|
|
4
4
|
Summary: Array utilities for StaticFrame
|
|
5
5
|
Author: Christopher Ariza, Brandt Bucher, Charles Burkland
|
|
6
6
|
License: MIT
|
|
@@ -64,6 +64,12 @@ ArrayKit requires the following:
|
|
|
64
64
|
What is New in ArrayKit
|
|
65
65
|
-------------------------
|
|
66
66
|
|
|
67
|
+
1.11.0
|
|
68
|
+
............
|
|
69
|
+
|
|
70
|
+
Added ``group_reduce()``.
|
|
71
|
+
|
|
72
|
+
|
|
67
73
|
1.10.0
|
|
68
74
|
............
|
|
69
75
|
|
|
@@ -27,6 +27,7 @@ from ._arraykit import get_new_indexers_and_screen as get_new_indexers_and_scree
|
|
|
27
27
|
from ._arraykit import write_array_to_file as write_array_to_file
|
|
28
28
|
from ._arraykit import factorize as factorize
|
|
29
29
|
from ._arraykit import group_ordering as group_ordering
|
|
30
|
+
from ._arraykit import group_reduce as group_reduce
|
|
30
31
|
from ._arraykit import fill_directional as fill_directional
|
|
31
32
|
from ._arraykit import count_iteration as count_iteration
|
|
32
33
|
from ._arraykit import first_true_1d as first_true_1d
|
|
@@ -233,6 +233,9 @@ def factorize(
|
|
|
233
233
|
def group_ordering(
|
|
234
234
|
codes: np.ndarray, *, size: tp.Optional[int] = ...
|
|
235
235
|
) -> tp.Tuple[np.ndarray, np.ndarray]: ...
|
|
236
|
+
def group_reduce(
|
|
237
|
+
codes: np.ndarray, size: int, values: np.ndarray, op: str
|
|
238
|
+
) -> np.ndarray: ...
|
|
236
239
|
def fill_directional(
|
|
237
240
|
array: np.ndarray,
|
|
238
241
|
target: np.ndarray,
|
|
@@ -78,6 +78,10 @@ static PyMethodDef arraykit_methods[] = {
|
|
|
78
78
|
(PyCFunction)group_ordering,
|
|
79
79
|
METH_VARARGS | METH_KEYWORDS,
|
|
80
80
|
NULL},
|
|
81
|
+
{"group_reduce",
|
|
82
|
+
(PyCFunction)group_reduce,
|
|
83
|
+
METH_VARARGS | METH_KEYWORDS,
|
|
84
|
+
NULL},
|
|
81
85
|
{"fill_directional",
|
|
82
86
|
(PyCFunction)fill_directional,
|
|
83
87
|
METH_VARARGS | METH_KEYWORDS,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
# include "numpy/arrayscalars.h"
|
|
9
9
|
# include "numpy/halffloat.h"
|
|
10
10
|
# include <string.h>
|
|
11
|
+
# include <math.h>
|
|
11
12
|
|
|
12
13
|
# ifdef _WIN32
|
|
13
14
|
# include <io.h>
|
|
@@ -1128,6 +1129,280 @@ fail:
|
|
|
1128
1129
|
return NULL;
|
|
1129
1130
|
}
|
|
1130
1131
|
|
|
1132
|
+
typedef enum {
|
|
1133
|
+
GR_SUM,
|
|
1134
|
+
GR_PROD,
|
|
1135
|
+
GR_MIN,
|
|
1136
|
+
GR_MAX,
|
|
1137
|
+
GR_COUNT,
|
|
1138
|
+
} AK_GroupReduceOp;
|
|
1139
|
+
|
|
1140
|
+
static int
|
|
1141
|
+
AK_group_reduce_op_from_str(const char *op, AK_GroupReduceOp *out) {
|
|
1142
|
+
if (strcmp(op, "sum") == 0) { *out = GR_SUM; return 0; }
|
|
1143
|
+
if (strcmp(op, "prod") == 0) { *out = GR_PROD; return 0; }
|
|
1144
|
+
if (strcmp(op, "min") == 0) { *out = GR_MIN; return 0; }
|
|
1145
|
+
if (strcmp(op, "max") == 0) { *out = GR_MAX; return 0; }
|
|
1146
|
+
if (strcmp(op, "count") == 0) { *out = GR_COUNT; return 0; }
|
|
1147
|
+
PyErr_Format(PyExc_ValueError,
|
|
1148
|
+
"unknown op '%s'; expected one of sum, prod, min, max, count", op);
|
|
1149
|
+
return -1;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Accumulate `n` float64 into `out[size]` per group. NaN propagates for min/max
|
|
1153
|
+
// (matching np.min/np.max, not the nan-skipping variants).
|
|
1154
|
+
static void
|
|
1155
|
+
AK_group_reduce_f64(
|
|
1156
|
+
const npy_float64 *v,
|
|
1157
|
+
const npy_intp *codes,
|
|
1158
|
+
npy_intp n,
|
|
1159
|
+
npy_float64 *out,
|
|
1160
|
+
npy_intp size,
|
|
1161
|
+
AK_GroupReduceOp op) {
|
|
1162
|
+
npy_float64 init;
|
|
1163
|
+
switch (op) {
|
|
1164
|
+
case GR_PROD: init = 1.0; break;
|
|
1165
|
+
case GR_MIN: init = NPY_INFINITY; break;
|
|
1166
|
+
case GR_MAX: init = -NPY_INFINITY; break;
|
|
1167
|
+
default: init = 0.0; break; // GR_SUM
|
|
1168
|
+
}
|
|
1169
|
+
for (npy_intp g = 0; g < size; g++) {
|
|
1170
|
+
out[g] = init;
|
|
1171
|
+
}
|
|
1172
|
+
for (npy_intp i = 0; i < n; i++) {
|
|
1173
|
+
npy_intp g = codes[i];
|
|
1174
|
+
npy_float64 x = v[i];
|
|
1175
|
+
switch (op) {
|
|
1176
|
+
case GR_SUM: out[g] += x; break;
|
|
1177
|
+
case GR_PROD: out[g] *= x; break;
|
|
1178
|
+
case GR_MIN: if (isnan(x) || x < out[g]) out[g] = x; break;
|
|
1179
|
+
case GR_MAX: if (isnan(x) || x > out[g]) out[g] = x; break;
|
|
1180
|
+
default: break;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Accumulate `n` int64 into `out[size]` per group.
|
|
1186
|
+
static void
|
|
1187
|
+
AK_group_reduce_i64(
|
|
1188
|
+
const npy_int64 *v,
|
|
1189
|
+
const npy_intp *codes,
|
|
1190
|
+
npy_intp n,
|
|
1191
|
+
npy_int64 *out,
|
|
1192
|
+
npy_intp size,
|
|
1193
|
+
AK_GroupReduceOp op) {
|
|
1194
|
+
npy_int64 init;
|
|
1195
|
+
switch (op) {
|
|
1196
|
+
case GR_PROD: init = 1; break;
|
|
1197
|
+
case GR_MIN: init = NPY_MAX_INT64; break;
|
|
1198
|
+
case GR_MAX: init = NPY_MIN_INT64; break;
|
|
1199
|
+
default: init = 0; break; // GR_SUM
|
|
1200
|
+
}
|
|
1201
|
+
for (npy_intp g = 0; g < size; g++) {
|
|
1202
|
+
out[g] = init;
|
|
1203
|
+
}
|
|
1204
|
+
for (npy_intp i = 0; i < n; i++) {
|
|
1205
|
+
npy_intp g = codes[i];
|
|
1206
|
+
npy_int64 x = v[i];
|
|
1207
|
+
switch (op) {
|
|
1208
|
+
case GR_SUM: out[g] += x; break;
|
|
1209
|
+
case GR_PROD: out[g] *= x; break;
|
|
1210
|
+
case GR_MIN: if (x < out[g]) out[g] = x; break;
|
|
1211
|
+
case GR_MAX: if (x > out[g]) out[g] = x; break;
|
|
1212
|
+
default: break;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Accumulate `n` uint64 into `out[size]` per group.
|
|
1218
|
+
static void
|
|
1219
|
+
AK_group_reduce_u64(
|
|
1220
|
+
const npy_uint64 *v,
|
|
1221
|
+
const npy_intp *codes,
|
|
1222
|
+
npy_intp n,
|
|
1223
|
+
npy_uint64 *out,
|
|
1224
|
+
npy_intp size,
|
|
1225
|
+
AK_GroupReduceOp op) {
|
|
1226
|
+
npy_uint64 init;
|
|
1227
|
+
switch (op) {
|
|
1228
|
+
case GR_PROD: init = 1; break;
|
|
1229
|
+
case GR_MIN: init = NPY_MAX_UINT64; break;
|
|
1230
|
+
case GR_MAX: init = 0; break;
|
|
1231
|
+
default: init = 0; break; // GR_SUM
|
|
1232
|
+
}
|
|
1233
|
+
for (npy_intp g = 0; g < size; g++) {
|
|
1234
|
+
out[g] = init;
|
|
1235
|
+
}
|
|
1236
|
+
for (npy_intp i = 0; i < n; i++) {
|
|
1237
|
+
npy_intp g = codes[i];
|
|
1238
|
+
npy_uint64 x = v[i];
|
|
1239
|
+
switch (op) {
|
|
1240
|
+
case GR_SUM: out[g] += x; break;
|
|
1241
|
+
case GR_PROD: out[g] *= x; break;
|
|
1242
|
+
case GR_MIN: if (x < out[g]) out[g] = x; break;
|
|
1243
|
+
case GR_MAX: if (x > out[g]) out[g] = x; break;
|
|
1244
|
+
default: break;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
static char *group_reduce_kwarg_names[] = {
|
|
1250
|
+
"codes",
|
|
1251
|
+
"size",
|
|
1252
|
+
"values",
|
|
1253
|
+
"op",
|
|
1254
|
+
NULL
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
// Grouped reduction. Given dense group `codes` in [0, size), a 1D `values` array, and
|
|
1258
|
+
// an `op` ('sum'/'prod'/'min'/'max'/'count'), return a length-`size` array of per-group
|
|
1259
|
+
// results in code order. Accumulates directly by code in an O(n) pass after validating
|
|
1260
|
+
// codes (no sort, no reorder). Values are accumulated at a 64-bit width by kind: signed
|
|
1261
|
+
// integers -> int64, unsigned integers -> uint64, floats -> float64; the result uses
|
|
1262
|
+
// that accumulator dtype (the caller casts to a narrower output as needed -- min/max are
|
|
1263
|
+
// selected elements so such a cast is lossless). Integer overflow wraps, matching numpy.
|
|
1264
|
+
// float16/float32 sum/prod are rejected: accumulating them at float64 would not match
|
|
1265
|
+
// numpy's native-width result, so the caller should fall back for those. 'count' returns
|
|
1266
|
+
// int64 group sizes and ignores the values dtype. This is the vectorized replacement for
|
|
1267
|
+
// a per-group Python reduction loop.
|
|
1268
|
+
PyObject *
|
|
1269
|
+
group_reduce(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs)
|
|
1270
|
+
{
|
|
1271
|
+
PyArrayObject *codes = NULL;
|
|
1272
|
+
Py_ssize_t size = 0;
|
|
1273
|
+
PyArrayObject *values = NULL;
|
|
1274
|
+
const char *op_name = NULL;
|
|
1275
|
+
|
|
1276
|
+
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
|
|
1277
|
+
"O!nO!s:group_reduce",
|
|
1278
|
+
group_reduce_kwarg_names,
|
|
1279
|
+
&PyArray_Type, &codes,
|
|
1280
|
+
&size,
|
|
1281
|
+
&PyArray_Type, &values,
|
|
1282
|
+
&op_name
|
|
1283
|
+
)) {
|
|
1284
|
+
return NULL;
|
|
1285
|
+
}
|
|
1286
|
+
AK_GroupReduceOp op;
|
|
1287
|
+
if (AK_group_reduce_op_from_str(op_name, &op)) {
|
|
1288
|
+
return NULL;
|
|
1289
|
+
}
|
|
1290
|
+
if (size < 0) {
|
|
1291
|
+
PyErr_SetString(PyExc_ValueError, "size must be non-negative");
|
|
1292
|
+
return NULL;
|
|
1293
|
+
}
|
|
1294
|
+
if (PyArray_NDIM(codes) != 1 || PyArray_NDIM(values) != 1) {
|
|
1295
|
+
PyErr_SetString(PyExc_ValueError, "Arrays must be 1-dimensional");
|
|
1296
|
+
return NULL;
|
|
1297
|
+
}
|
|
1298
|
+
if (PyArray_TYPE(codes) != NPY_INTP) {
|
|
1299
|
+
PyErr_SetString(PyExc_ValueError, "codes must be of type intp");
|
|
1300
|
+
return NULL;
|
|
1301
|
+
}
|
|
1302
|
+
if (!PyArray_IS_C_CONTIGUOUS(codes) || !PyArray_IS_C_CONTIGUOUS(values)) {
|
|
1303
|
+
PyErr_SetString(PyExc_ValueError, "Arrays must be contiguous");
|
|
1304
|
+
return NULL;
|
|
1305
|
+
}
|
|
1306
|
+
npy_intp n = PyArray_SIZE(codes);
|
|
1307
|
+
if (PyArray_SIZE(values) != n) {
|
|
1308
|
+
PyErr_SetString(PyExc_ValueError,
|
|
1309
|
+
"codes and values must be the same length");
|
|
1310
|
+
return NULL;
|
|
1311
|
+
}
|
|
1312
|
+
const npy_intp *codes_buffer = (npy_intp*)PyArray_DATA(codes);
|
|
1313
|
+
// validate codes are in range before any indexed writes into the output
|
|
1314
|
+
for (npy_intp i = 0; i < n; i++) {
|
|
1315
|
+
npy_intp c = codes_buffer[i];
|
|
1316
|
+
if (c < 0 || c >= size) {
|
|
1317
|
+
PyErr_Format(PyExc_ValueError,
|
|
1318
|
+
"code %zd out of range [0, %zd)",
|
|
1319
|
+
(Py_ssize_t)c, (Py_ssize_t)size);
|
|
1320
|
+
return NULL;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
npy_intp dims[1] = {size};
|
|
1325
|
+
int vtype = PyArray_TYPE(values);
|
|
1326
|
+
|
|
1327
|
+
if (op == GR_COUNT) {
|
|
1328
|
+
PyObject *out_arr = PyArray_ZEROS(1, dims, NPY_INT64, 0);
|
|
1329
|
+
if (!out_arr) {
|
|
1330
|
+
return NULL;
|
|
1331
|
+
}
|
|
1332
|
+
npy_int64 *out = (npy_int64*)PyArray_DATA((PyArrayObject*)out_arr);
|
|
1333
|
+
for (npy_intp i = 0; i < n; i++) {
|
|
1334
|
+
out[codes_buffer[i]]++;
|
|
1335
|
+
}
|
|
1336
|
+
PyArray_CLEARFLAGS((PyArrayObject*)out_arr, NPY_ARRAY_WRITEABLE);
|
|
1337
|
+
return out_arr;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// Choose a 64-bit accumulator by input kind. Integers accumulate exactly at their
|
|
1341
|
+
// signed/unsigned 64-bit width (wrapping on overflow, like numpy); floats at float64.
|
|
1342
|
+
int acc_type;
|
|
1343
|
+
if (PyArray_ISSIGNED(values)) {
|
|
1344
|
+
acc_type = NPY_INT64;
|
|
1345
|
+
}
|
|
1346
|
+
else if (PyArray_ISUNSIGNED(values)) {
|
|
1347
|
+
acc_type = NPY_UINT64;
|
|
1348
|
+
}
|
|
1349
|
+
else if (vtype == NPY_HALF || vtype == NPY_FLOAT || vtype == NPY_DOUBLE) {
|
|
1350
|
+
acc_type = NPY_DOUBLE;
|
|
1351
|
+
}
|
|
1352
|
+
else {
|
|
1353
|
+
PyErr_SetString(PyExc_ValueError,
|
|
1354
|
+
"values must be an integer or float (float16/32/64) dtype");
|
|
1355
|
+
return NULL;
|
|
1356
|
+
}
|
|
1357
|
+
// float16/float32 sum/prod cannot be accumulated at float64 without diverging from
|
|
1358
|
+
// numpy's native-width result; the caller falls back to a per-group reduction.
|
|
1359
|
+
if ((op == GR_SUM || op == GR_PROD)
|
|
1360
|
+
&& acc_type == NPY_DOUBLE && vtype != NPY_DOUBLE) {
|
|
1361
|
+
PyErr_SetString(PyExc_ValueError,
|
|
1362
|
+
"float16/float32 sum/prod is not supported; cast to float64");
|
|
1363
|
+
return NULL;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// upcast the input to the accumulator dtype (no copy when already 64-bit wide)
|
|
1367
|
+
PyArrayObject *v64;
|
|
1368
|
+
if (vtype == acc_type) {
|
|
1369
|
+
Py_INCREF(values);
|
|
1370
|
+
v64 = values;
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
v64 = (PyArrayObject*)PyArray_Cast(values, acc_type);
|
|
1374
|
+
if (v64 == NULL) {
|
|
1375
|
+
return NULL;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
PyObject *out_arr = PyArray_EMPTY(1, dims, acc_type, 0);
|
|
1380
|
+
if (!out_arr) {
|
|
1381
|
+
Py_DECREF(v64);
|
|
1382
|
+
return NULL;
|
|
1383
|
+
}
|
|
1384
|
+
switch (acc_type) {
|
|
1385
|
+
case NPY_DOUBLE:
|
|
1386
|
+
AK_group_reduce_f64(
|
|
1387
|
+
(npy_float64*)PyArray_DATA(v64), codes_buffer, n,
|
|
1388
|
+
(npy_float64*)PyArray_DATA((PyArrayObject*)out_arr), size, op);
|
|
1389
|
+
break;
|
|
1390
|
+
case NPY_UINT64:
|
|
1391
|
+
AK_group_reduce_u64(
|
|
1392
|
+
(npy_uint64*)PyArray_DATA(v64), codes_buffer, n,
|
|
1393
|
+
(npy_uint64*)PyArray_DATA((PyArrayObject*)out_arr), size, op);
|
|
1394
|
+
break;
|
|
1395
|
+
default: // NPY_INT64
|
|
1396
|
+
AK_group_reduce_i64(
|
|
1397
|
+
(npy_int64*)PyArray_DATA(v64), codes_buffer, n,
|
|
1398
|
+
(npy_int64*)PyArray_DATA((PyArrayObject*)out_arr), size, op);
|
|
1399
|
+
break;
|
|
1400
|
+
}
|
|
1401
|
+
Py_DECREF(v64);
|
|
1402
|
+
PyArray_CLEARFLAGS((PyArrayObject*)out_arr, NPY_ARRAY_WRITEABLE);
|
|
1403
|
+
return out_arr;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1131
1406
|
// Fill one strided lane in place: walk positions in the fill direction, carrying
|
|
1132
1407
|
// the most recent non-target value into each target position (subject to `limit`
|
|
1133
1408
|
// consecutive fills per run). `elem_base`/`elem_stride` address elements in bytes;
|
|
@@ -72,6 +72,9 @@ first_true_2d(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs);
|
|
|
72
72
|
PyObject *
|
|
73
73
|
group_ordering(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs);
|
|
74
74
|
|
|
75
|
+
PyObject *
|
|
76
|
+
group_reduce(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs);
|
|
77
|
+
|
|
75
78
|
PyObject *
|
|
76
79
|
fill_directional(PyObject *Py_UNUSED(m), PyObject *args, PyObject *kwargs);
|
|
77
80
|
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from arraykit import factorize, group_reduce
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestUnit(unittest.TestCase):
|
|
8
|
+
# ------------------------------------------------------------------
|
|
9
|
+
# basic behavior
|
|
10
|
+
|
|
11
|
+
def test_group_reduce_sum_f64(self) -> None:
|
|
12
|
+
codes = np.array([0, 1, 0, 2, 1, 0], dtype=np.intp)
|
|
13
|
+
values = np.array([1.0, 10.0, 2.0, 100.0, 20.0, 3.0])
|
|
14
|
+
post = group_reduce(codes, 3, values, 'sum')
|
|
15
|
+
self.assertEqual(post.tolist(), [6.0, 30.0, 100.0])
|
|
16
|
+
self.assertEqual(post.dtype, np.dtype(np.float64))
|
|
17
|
+
|
|
18
|
+
def test_group_reduce_all_ops_f64(self) -> None:
|
|
19
|
+
codes = np.array([0, 1, 0, 2, 1, 0], dtype=np.intp)
|
|
20
|
+
values = np.array([1.0, 10.0, 2.0, 100.0, 20.0, 3.0])
|
|
21
|
+
self.assertEqual(group_reduce(codes, 3, values, 'sum').tolist(), [6.0, 30.0, 100.0])
|
|
22
|
+
self.assertEqual(group_reduce(codes, 3, values, 'prod').tolist(), [6.0, 200.0, 100.0])
|
|
23
|
+
self.assertEqual(group_reduce(codes, 3, values, 'min').tolist(), [1.0, 10.0, 100.0])
|
|
24
|
+
self.assertEqual(group_reduce(codes, 3, values, 'max').tolist(), [3.0, 20.0, 100.0])
|
|
25
|
+
|
|
26
|
+
def test_group_reduce_all_ops_i64(self) -> None:
|
|
27
|
+
codes = np.array([0, 1, 0, 2, 1, 0], dtype=np.intp)
|
|
28
|
+
values = np.array([1, 10, 2, 100, 20, 3], dtype=np.int64)
|
|
29
|
+
for op in ('sum', 'prod', 'min', 'max'):
|
|
30
|
+
post = group_reduce(codes, 3, values, op)
|
|
31
|
+
self.assertEqual(post.dtype, np.dtype(np.int64))
|
|
32
|
+
self.assertEqual(group_reduce(codes, 3, values, 'sum').tolist(), [6, 30, 100])
|
|
33
|
+
self.assertEqual(group_reduce(codes, 3, values, 'prod').tolist(), [6, 200, 100])
|
|
34
|
+
self.assertEqual(group_reduce(codes, 3, values, 'min').tolist(), [1, 10, 100])
|
|
35
|
+
self.assertEqual(group_reduce(codes, 3, values, 'max').tolist(), [3, 20, 100])
|
|
36
|
+
|
|
37
|
+
def test_group_reduce_all_int_widths(self) -> None:
|
|
38
|
+
# every signed width accumulates at int64; every unsigned width at uint64.
|
|
39
|
+
# values are exact (selected elements / small sums), only the dtype widens.
|
|
40
|
+
codes = np.array([0, 1, 0, 2, 1, 0], dtype=np.intp)
|
|
41
|
+
raw = [1, 10, 2, 100, 20, 3]
|
|
42
|
+
for dt in ('int8', 'int16', 'int32', 'int64'):
|
|
43
|
+
values = np.array(raw, dtype=dt)
|
|
44
|
+
for op, exp in (
|
|
45
|
+
('sum', [6, 30, 100]),
|
|
46
|
+
('min', [1, 10, 100]),
|
|
47
|
+
('max', [3, 20, 100]),
|
|
48
|
+
):
|
|
49
|
+
post = group_reduce(codes, 3, values, op)
|
|
50
|
+
self.assertEqual(post.dtype, np.dtype(np.int64), (dt, op))
|
|
51
|
+
self.assertEqual(post.tolist(), exp, (dt, op))
|
|
52
|
+
for dt in ('uint8', 'uint16', 'uint32', 'uint64'):
|
|
53
|
+
values = np.array(raw, dtype=dt)
|
|
54
|
+
for op, exp in (
|
|
55
|
+
('sum', [6, 30, 100]),
|
|
56
|
+
('min', [1, 10, 100]),
|
|
57
|
+
('max', [3, 20, 100]),
|
|
58
|
+
):
|
|
59
|
+
post = group_reduce(codes, 3, values, op)
|
|
60
|
+
self.assertEqual(post.dtype, np.dtype(np.uint64), (dt, op))
|
|
61
|
+
self.assertEqual(post.tolist(), exp, (dt, op))
|
|
62
|
+
|
|
63
|
+
def test_group_reduce_float_widths(self) -> None:
|
|
64
|
+
# min/max/count work for every float width (result is float64); the
|
|
65
|
+
# selected element is exact
|
|
66
|
+
codes = np.array([0, 1, 0, 2, 1, 0], dtype=np.intp)
|
|
67
|
+
raw = [1.0, 10.0, 2.0, 100.0, 20.0, 3.0]
|
|
68
|
+
for dt in ('float16', 'float32', 'float64'):
|
|
69
|
+
values = np.array(raw, dtype=dt)
|
|
70
|
+
for op, exp in (('min', [1.0, 10.0, 100.0]), ('max', [3.0, 20.0, 100.0])):
|
|
71
|
+
post = group_reduce(codes, 3, values, op)
|
|
72
|
+
self.assertEqual(post.dtype, np.dtype(np.float64), (dt, op))
|
|
73
|
+
self.assertEqual(post.tolist(), exp, (dt, op))
|
|
74
|
+
|
|
75
|
+
def test_group_reduce_narrow_float_sum_rejected(self) -> None:
|
|
76
|
+
# float16/float32 sum/prod cannot match numpy's native-width result at
|
|
77
|
+
# float64, so they are rejected (caller falls back); float64 is fine
|
|
78
|
+
codes = np.array([0, 1], dtype=np.intp)
|
|
79
|
+
for dt in ('float16', 'float32'):
|
|
80
|
+
values = np.array([1.0, 2.0], dtype=dt)
|
|
81
|
+
for op in ('sum', 'prod'):
|
|
82
|
+
with self.assertRaises(ValueError):
|
|
83
|
+
group_reduce(codes, 2, values, op)
|
|
84
|
+
# float64 sum/prod is supported
|
|
85
|
+
self.assertEqual(
|
|
86
|
+
group_reduce(codes, 2, np.array([1.0, 2.0]), 'sum').tolist(), [1.0, 2.0]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def test_group_reduce_integer_overflow_wraps(self) -> None:
|
|
90
|
+
# integer sum overflow wraps silently, matching numpy (not raising)
|
|
91
|
+
codes = np.array([0, 0], dtype=np.intp)
|
|
92
|
+
big = np.full(2, np.iinfo(np.int64).max, dtype=np.int64)
|
|
93
|
+
self.assertEqual(
|
|
94
|
+
group_reduce(codes, 1, big, 'sum').tolist(), [int(np.sum(big))]
|
|
95
|
+
)
|
|
96
|
+
ubig = np.full(2, np.iinfo(np.uint64).max, dtype=np.uint64)
|
|
97
|
+
self.assertEqual(
|
|
98
|
+
group_reduce(codes, 1, ubig, 'sum').tolist(), [int(np.sum(ubig))]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def test_group_reduce_count(self) -> None:
|
|
102
|
+
# count returns int64 group sizes regardless of values dtype
|
|
103
|
+
codes = np.array([0, 1, 0, 2, 1, 0], dtype=np.intp)
|
|
104
|
+
post = group_reduce(codes, 3, np.array([1.0, 2, 3, 4, 5, 6]), 'count')
|
|
105
|
+
self.assertEqual(post.tolist(), [3, 2, 1])
|
|
106
|
+
self.assertEqual(post.dtype, np.dtype(np.int64))
|
|
107
|
+
# count ignores the values dtype entirely
|
|
108
|
+
post = group_reduce(codes, 3, np.array([1, 2, 3, 4, 5, 6], dtype=np.int64), 'count')
|
|
109
|
+
self.assertEqual(post.tolist(), [3, 2, 1])
|
|
110
|
+
|
|
111
|
+
def test_group_reduce_nan_propagates(self) -> None:
|
|
112
|
+
# min/max propagate NaN, matching np.min/np.max (not the nan-skipping variants)
|
|
113
|
+
codes = np.array([0, 1, 0, 2, 1, 0], dtype=np.intp)
|
|
114
|
+
values = np.array([1.0, np.nan, 2.0, 5.0, np.nan, 3.0])
|
|
115
|
+
mx = group_reduce(codes, 3, values, 'max')
|
|
116
|
+
mn = group_reduce(codes, 3, values, 'min')
|
|
117
|
+
self.assertEqual(mx[0], 3.0)
|
|
118
|
+
self.assertTrue(np.isnan(mx[1]))
|
|
119
|
+
self.assertEqual(mx[2], 5.0)
|
|
120
|
+
self.assertEqual(mn[0], 1.0)
|
|
121
|
+
self.assertTrue(np.isnan(mn[1]))
|
|
122
|
+
# sum also propagates NaN
|
|
123
|
+
s = group_reduce(codes, 3, values, 'sum')
|
|
124
|
+
self.assertTrue(np.isnan(s[1]))
|
|
125
|
+
|
|
126
|
+
def test_group_reduce_single_group(self) -> None:
|
|
127
|
+
codes = np.array([0, 0, 0], dtype=np.intp)
|
|
128
|
+
values = np.array([1.0, 2.0, 3.0])
|
|
129
|
+
self.assertEqual(group_reduce(codes, 1, values, 'sum').tolist(), [6.0])
|
|
130
|
+
|
|
131
|
+
def test_group_reduce_empty(self) -> None:
|
|
132
|
+
codes = np.array([], dtype=np.intp)
|
|
133
|
+
values = np.array([], dtype=np.float64)
|
|
134
|
+
self.assertEqual(group_reduce(codes, 0, values, 'sum').tolist(), [])
|
|
135
|
+
|
|
136
|
+
def test_group_reduce_outputs_immutable(self) -> None:
|
|
137
|
+
codes = np.array([0, 1, 0], dtype=np.intp)
|
|
138
|
+
values = np.array([1.0, 2.0, 3.0])
|
|
139
|
+
for op in ('sum', 'prod', 'min', 'max', 'count'):
|
|
140
|
+
post = group_reduce(codes, 2, values, op)
|
|
141
|
+
self.assertFalse(post.flags.writeable)
|
|
142
|
+
|
|
143
|
+
# ------------------------------------------------------------------
|
|
144
|
+
# equivalence to a per-group numpy reduction
|
|
145
|
+
|
|
146
|
+
def test_group_reduce_equivalence_f64(self) -> None:
|
|
147
|
+
rng = np.random.RandomState(0)
|
|
148
|
+
for _ in range(20):
|
|
149
|
+
size = int(rng.randint(1, 40))
|
|
150
|
+
n = int(rng.randint(size, size + 500))
|
|
151
|
+
codes = rng.randint(0, size, n).astype(np.intp)
|
|
152
|
+
values = rng.rand(n) * 100
|
|
153
|
+
for op, npf in (('sum', np.sum), ('min', np.min), ('max', np.max)):
|
|
154
|
+
got = group_reduce(codes, size, values, op)
|
|
155
|
+
for g in range(size):
|
|
156
|
+
mask = codes == g
|
|
157
|
+
if np.any(mask): # real usage (factorize) has no empty groups
|
|
158
|
+
self.assertTrue(np.isclose(got[g], npf(values[mask])), op)
|
|
159
|
+
|
|
160
|
+
def test_group_reduce_equivalence_i64(self) -> None:
|
|
161
|
+
rng = np.random.RandomState(1)
|
|
162
|
+
for _ in range(20):
|
|
163
|
+
size = int(rng.randint(1, 40))
|
|
164
|
+
n = int(rng.randint(size, size + 500))
|
|
165
|
+
codes = rng.randint(0, size, n).astype(np.intp)
|
|
166
|
+
values = rng.randint(-1000, 1000, n).astype(np.int64)
|
|
167
|
+
for op, npf in (('sum', np.sum), ('min', np.min), ('max', np.max)):
|
|
168
|
+
got = group_reduce(codes, size, values, op)
|
|
169
|
+
for g in range(size):
|
|
170
|
+
mask = codes == g
|
|
171
|
+
if np.any(mask):
|
|
172
|
+
self.assertEqual(got[g], npf(values[mask]), op)
|
|
173
|
+
|
|
174
|
+
def test_group_reduce_with_factorize(self) -> None:
|
|
175
|
+
# the intended pipeline: factorize(sort=True) -> group_reduce
|
|
176
|
+
key = np.array([30, 10, 20, 10, 30, 20, 10])
|
|
177
|
+
values = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0])
|
|
178
|
+
uniques, codes = factorize(key, sort=True)
|
|
179
|
+
self.assertEqual(uniques.tolist(), [10, 20, 30])
|
|
180
|
+
post = group_reduce(codes, len(uniques), values, 'sum')
|
|
181
|
+
# group 10 -> 2+4+7=13; group 20 -> 3+6=9; group 30 -> 1+5=6
|
|
182
|
+
self.assertEqual(post.tolist(), [13.0, 9.0, 6.0])
|
|
183
|
+
|
|
184
|
+
# ------------------------------------------------------------------
|
|
185
|
+
# errors
|
|
186
|
+
|
|
187
|
+
def test_group_reduce_errors(self) -> None:
|
|
188
|
+
codes = np.array([0, 1, 0], dtype=np.intp)
|
|
189
|
+
values = np.array([1.0, 2.0, 3.0])
|
|
190
|
+
with self.assertRaises(ValueError): # unknown op
|
|
191
|
+
group_reduce(codes, 2, values, 'median')
|
|
192
|
+
with self.assertRaises(ValueError): # length mismatch
|
|
193
|
+
group_reduce(codes, 2, np.array([1.0, 2.0]), 'sum')
|
|
194
|
+
with self.assertRaises(ValueError): # code out of range
|
|
195
|
+
group_reduce(np.array([0, 5], dtype=np.intp), 2, np.array([1.0, 2.0]), 'sum')
|
|
196
|
+
with self.assertRaises(ValueError): # negative code
|
|
197
|
+
group_reduce(np.array([0, -1], dtype=np.intp), 2, np.array([1.0, 2.0]), 'sum')
|
|
198
|
+
with self.assertRaises(ValueError): # codes wrong dtype (int8 is never intp)
|
|
199
|
+
group_reduce(np.array([0, 1], dtype=np.int8), 2, np.array([1.0, 2.0]), 'sum')
|
|
200
|
+
with self.assertRaises(ValueError): # values unsupported dtype (complex)
|
|
201
|
+
group_reduce(codes, 2, np.array([1, 2, 3], dtype=np.complex128), 'sum')
|
|
202
|
+
with self.assertRaises(ValueError): # values unsupported dtype (datetime)
|
|
203
|
+
group_reduce(
|
|
204
|
+
codes, 2, np.array([1, 2, 3], dtype='datetime64[s]'), 'max'
|
|
205
|
+
)
|
|
206
|
+
with self.assertRaises(ValueError): # negative size
|
|
207
|
+
group_reduce(codes, -1, values, 'sum')
|
|
208
|
+
with self.assertRaises(ValueError): # 2d codes
|
|
209
|
+
group_reduce(
|
|
210
|
+
np.array([[0, 1]], dtype=np.intp), 2, np.array([[1.0, 2.0]]), 'sum'
|
|
211
|
+
)
|
|
212
|
+
with self.assertRaises(TypeError): # not an array
|
|
213
|
+
group_reduce([0, 1, 0], 2, values, 'sum')
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
if __name__ == '__main__':
|
|
217
|
+
unittest.main()
|
arraykit-1.10.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|