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.
Files changed (54) hide show
  1. {arraykit-1.10.0/arraykit.egg-info → arraykit-1.11.0}/PKG-INFO +7 -1
  2. {arraykit-1.10.0 → arraykit-1.11.0}/README.rst +6 -0
  3. arraykit-1.11.0/VERSION +2 -0
  4. {arraykit-1.10.0 → arraykit-1.11.0/arraykit.egg-info}/PKG-INFO +7 -1
  5. {arraykit-1.10.0 → arraykit-1.11.0}/arraykit.egg-info/SOURCES.txt +1 -0
  6. {arraykit-1.10.0 → arraykit-1.11.0}/src/__init__.py +1 -0
  7. {arraykit-1.10.0 → arraykit-1.11.0}/src/__init__.pyi +3 -0
  8. {arraykit-1.10.0 → arraykit-1.11.0}/src/_arraykit.c +4 -0
  9. {arraykit-1.10.0 → arraykit-1.11.0}/src/methods.c +275 -0
  10. {arraykit-1.10.0 → arraykit-1.11.0}/src/methods.h +3 -0
  11. arraykit-1.11.0/test/test_group_reduce.py +217 -0
  12. arraykit-1.10.0/VERSION +0 -2
  13. {arraykit-1.10.0 → arraykit-1.11.0}/LICENSE.txt +0 -0
  14. {arraykit-1.10.0 → arraykit-1.11.0}/MANIFEST.in +0 -0
  15. {arraykit-1.10.0 → arraykit-1.11.0}/arraykit.egg-info/dependency_links.txt +0 -0
  16. {arraykit-1.10.0 → arraykit-1.11.0}/arraykit.egg-info/requires.txt +0 -0
  17. {arraykit-1.10.0 → arraykit-1.11.0}/arraykit.egg-info/top_level.txt +0 -0
  18. {arraykit-1.10.0 → arraykit-1.11.0}/pyproject.toml +0 -0
  19. {arraykit-1.10.0 → arraykit-1.11.0}/setup.cfg +0 -0
  20. {arraykit-1.10.0 → arraykit-1.11.0}/setup.py +0 -0
  21. {arraykit-1.10.0 → arraykit-1.11.0}/src/array_go.c +0 -0
  22. {arraykit-1.10.0 → arraykit-1.11.0}/src/array_go.h +0 -0
  23. {arraykit-1.10.0 → arraykit-1.11.0}/src/array_to_tuple.c +0 -0
  24. {arraykit-1.10.0 → arraykit-1.11.0}/src/array_to_tuple.h +0 -0
  25. {arraykit-1.10.0 → arraykit-1.11.0}/src/auto_map.c +0 -0
  26. {arraykit-1.10.0 → arraykit-1.11.0}/src/auto_map.h +0 -0
  27. {arraykit-1.10.0 → arraykit-1.11.0}/src/block_index.c +0 -0
  28. {arraykit-1.10.0 → arraykit-1.11.0}/src/block_index.h +0 -0
  29. {arraykit-1.10.0 → arraykit-1.11.0}/src/delimited_to_arrays.c +0 -0
  30. {arraykit-1.10.0 → arraykit-1.11.0}/src/delimited_to_arrays.h +0 -0
  31. {arraykit-1.10.0 → arraykit-1.11.0}/src/py.typed +0 -0
  32. {arraykit-1.10.0 → arraykit-1.11.0}/src/tri_map.c +0 -0
  33. {arraykit-1.10.0 → arraykit-1.11.0}/src/tri_map.h +0 -0
  34. {arraykit-1.10.0 → arraykit-1.11.0}/src/utilities.h +0 -0
  35. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_array_go.py +0 -0
  36. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_astype_array.py +0 -0
  37. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_auto_map.py +0 -0
  38. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_auto_map_property.py +0 -0
  39. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_block_index.py +0 -0
  40. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_delimited_to_arrays.py +0 -0
  41. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_delimited_to_arrays_integration.py +0 -0
  42. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_delimited_to_arrays_property.py +0 -0
  43. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_factorize.py +0 -0
  44. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_fill_directional.py +0 -0
  45. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_group_ordering.py +0 -0
  46. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_nonzero_1d.py +0 -0
  47. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_nonzero_1d_property.py +0 -0
  48. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_objectable.py +0 -0
  49. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_pyi.py +0 -0
  50. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_split_after_count.py +0 -0
  51. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_transition_slices_from_group.py +0 -0
  52. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_tri_map.py +0 -0
  53. {arraykit-1.10.0 → arraykit-1.11.0}/test/test_type_discovery.py +0 -0
  54. {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.10.0
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
 
@@ -35,6 +35,12 @@ ArrayKit requires the following:
35
35
  What is New in ArrayKit
36
36
  -------------------------
37
37
 
38
+ 1.11.0
39
+ ............
40
+
41
+ Added ``group_reduce()``.
42
+
43
+
38
44
  1.10.0
39
45
  ............
40
46
 
@@ -0,0 +1,2 @@
1
+ 1.11.0
2
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arraykit
3
- Version: 1.10.0
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
 
@@ -39,6 +39,7 @@ test/test_delimited_to_arrays_property.py
39
39
  test/test_factorize.py
40
40
  test/test_fill_directional.py
41
41
  test/test_group_ordering.py
42
+ test/test_group_reduce.py
42
43
  test/test_nonzero_1d.py
43
44
  test/test_nonzero_1d_property.py
44
45
  test/test_objectable.py
@@ -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
@@ -1,2 +0,0 @@
1
- 1.10.0
2
-
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