labelarray 0.1.0__py3-none-any.whl

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.
labelarray/__init__.py ADDED
@@ -0,0 +1,82 @@
1
+ """labelarray: labelled N-D arrays in NumPy, with exact labels and reproducible reductions.
2
+
3
+ The public API:
4
+
5
+ - ``Array``: a NumPy array with named axes, exact labels, attributes, and a
6
+ name; NumPy's ufuncs, ``np.where``, and Python's operators broadcast by axis
7
+ name. ``Collection``: arrays sharing axes, one label array per axis.
8
+ ``concat``, ``align``, and ``broadcast`` work on several arrays (``array``,
9
+ ``collection``).
10
+ - The exact label rule: ``kind``, ``equal``, ``compare``, ``strict_subset``,
11
+ ``exact_cast``, ``lossy_join_type`` (``labels``); label indexes and their
12
+ factory, ``index_for`` and ``register_index`` (``index``).
13
+ - Grouping: ``Grouping``, with ``reduce``, ``counts``, and ``broadcast``
14
+ applying it to an array (``grouped``); ``factorize`` and the key transforms
15
+ ``Bins``, ``Duration``, ``Round``, and ``GridCells`` (``transforms``).
16
+ - Numbers: float32 sums and means correctly rounded (``sums``) and the
17
+ contraction behind ``Array.dot`` (``contract``).
18
+ - Errors: ``LabelArrayError`` and its ``LabelError``, ``AlignError``,
19
+ ``GroupError``, and ``ShapeError`` (``errors``).
20
+ """
21
+
22
+ from labelarray.__version__ import __version__
23
+ from labelarray.array import Array, align, broadcast, concat, label_index
24
+ from labelarray.collection import Collection
25
+ from labelarray.errors import (
26
+ AlignError,
27
+ GroupError,
28
+ LabelArrayError,
29
+ LabelError,
30
+ ShapeError,
31
+ )
32
+ from labelarray.grouped import Grouping
33
+ from labelarray.index import (
34
+ GridIndex,
35
+ LabelIndex,
36
+ SortedIndex,
37
+ SortOnceIndex,
38
+ index_for,
39
+ register_index,
40
+ )
41
+ from labelarray.labels import (
42
+ compare,
43
+ equal,
44
+ exact_cast,
45
+ kind,
46
+ lossy_join_type,
47
+ strict_subset,
48
+ )
49
+ from labelarray.transforms import Bins, Duration, GridCells, Round, factorize
50
+
51
+ __all__ = [
52
+ "AlignError",
53
+ "Array",
54
+ "Bins",
55
+ "Collection",
56
+ "Duration",
57
+ "GridCells",
58
+ "GridIndex",
59
+ "GroupError",
60
+ "Grouping",
61
+ "LabelArrayError",
62
+ "LabelError",
63
+ "LabelIndex",
64
+ "Round",
65
+ "ShapeError",
66
+ "SortOnceIndex",
67
+ "SortedIndex",
68
+ "__version__",
69
+ "align",
70
+ "broadcast",
71
+ "compare",
72
+ "concat",
73
+ "equal",
74
+ "exact_cast",
75
+ "factorize",
76
+ "index_for",
77
+ "kind",
78
+ "label_index",
79
+ "lossy_join_type",
80
+ "register_index",
81
+ "strict_subset",
82
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,101 @@
1
+ """The compiled grouped estimate for ``sums``, used when Numba is installed (``labelarray[fast]``).
2
+
3
+ One parallel pass over the values computes, per group and column, a
4
+ compensated float64 sum (Neumaier's improvement of Kahan's method), the sum
5
+ of magnitudes, and the values present. Each thread adds its share of the
6
+ positions; the threads' compensated sums are then added with compensation
7
+ too. The result is within (6u + 4 (n u)^2) * sum|x| of the exact sum, so
8
+ ``sums._certify`` settles nearly every result even under heavy cancellation,
9
+ and the results are the same float32 values the NumPy path gives.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import numba as nb
15
+ import numpy as np
16
+
17
+
18
+ @nb.njit(parallel=True, cache=True, nogil=True)
19
+ def _estimate(table, rows, groups, na_rm, threads): # pragma: no cover - compiled
20
+ positions, width = table.shape
21
+ share = (positions + threads - 1) // threads
22
+ sums = np.zeros((threads, groups, width))
23
+ carries = np.zeros((threads, groups, width))
24
+ magnitudes = np.zeros((threads, groups, width))
25
+ present = np.zeros((threads, groups, width), dtype=np.intp)
26
+ for thread in nb.prange(threads):
27
+ for position in range(thread * share, min(positions, (thread + 1) * share)):
28
+ group = rows[position]
29
+ for column in range(width):
30
+ value = np.float64(table[position, column])
31
+ if np.isnan(value): # skipped with na_rm, otherwise it makes the sum NaN
32
+ if na_rm:
33
+ continue
34
+ else:
35
+ present[thread, group, column] += 1
36
+ total = sums[thread, group, column]
37
+ added = total + value
38
+ if abs(total) >= abs(value):
39
+ carries[thread, group, column] += (total - added) + value
40
+ else:
41
+ carries[thread, group, column] += (value - added) + total
42
+ sums[thread, group, column] = added
43
+ magnitudes[thread, group, column] += abs(value)
44
+ total = np.zeros((groups, width))
45
+ carry = np.zeros((groups, width))
46
+ magnitude = np.zeros((groups, width))
47
+ counted = np.zeros((groups, width), dtype=np.intp)
48
+ for thread in range(threads):
49
+ for group in range(groups):
50
+ for column in range(width):
51
+ for value in (sums[thread, group, column], carries[thread, group, column]):
52
+ running = total[group, column]
53
+ added = running + value
54
+ if abs(running) >= abs(value):
55
+ carry[group, column] += (running - added) + value
56
+ else:
57
+ carry[group, column] += (value - added) + running
58
+ total[group, column] = added
59
+ magnitude[group, column] += magnitudes[thread, group, column]
60
+ counted[group, column] += present[thread, group, column]
61
+ return total + carry, magnitude, counted
62
+
63
+
64
+ def grouped_estimate(table: np.ndarray, rows: np.ndarray, groups: int, na_rm: bool):
65
+ """Compensated float64 sums, sums of magnitudes, and values present, per group and column."""
66
+ # The thread count is passed in: reading it inside the compiled function would stop Numba caching it.
67
+ return _estimate(table, rows, groups, na_rm, nb.get_num_threads())
68
+
69
+
70
+ @nb.njit(parallel=True, cache=True, nogil=True, error_model="numpy")
71
+ def _in_order(table, mean, na_rm): # pragma: no cover - compiled
72
+ positions, width = table.shape
73
+ out = np.empty(width, dtype=np.float32)
74
+ present = np.zeros(width, dtype=np.intp)
75
+ for column in nb.prange(width):
76
+ total = 0.0
77
+ counted = 0
78
+ for position in range(positions):
79
+ value = np.float64(table[position, column])
80
+ if np.isnan(value):
81
+ if na_rm:
82
+ value = 0.0
83
+ else:
84
+ counted += 1
85
+ total += value
86
+ if mean:
87
+ total = total / (counted if na_rm else positions)
88
+ out[column] = np.float32(total)
89
+ present[column] = counted
90
+ return out, present
91
+
92
+
93
+ def in_order(table: np.ndarray, mean: bool, na_rm: bool):
94
+ """Few values per result, as ``sums._in_order`` adds them, in one parallel pass.
95
+
96
+ Each result adds the values of its column first to last in float64,
97
+ starting from 0.0, with NaN as 0.0 under ``na_rm``; a mean divides in
98
+ float64; the result is rounded once to float32. These are the operations
99
+ and the order of the NumPy path, so the bits are the same.
100
+ """
101
+ return _in_order(table, mean, na_rm)
labelarray/align.py ADDED
@@ -0,0 +1,40 @@
1
+ """Exact alignment of one axis's labels (exact is the only mode).
2
+
3
+ Labels on a shared axis must match exactly. The same labels in another
4
+ order are matched by label; different labels, or repeated labels in another
5
+ order, cannot be paired and are an ``AlignError``. There is no looser mode
6
+ (xarray's outer join): a mode can be added later, a changed default cannot.
7
+
8
+ This module needs NumPy only.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Callable, Hashable
14
+ from typing import Any
15
+
16
+ import numpy as np
17
+
18
+ from labelarray.errors import AlignError
19
+ from labelarray.index import LabelIndex, index_for
20
+ from labelarray.labels import compare
21
+
22
+
23
+ def order_like(reference: Any, labels: Any, dim: Hashable = None, *,
24
+ index: Callable[..., LabelIndex] = index_for) -> np.ndarray | None:
25
+ """The positions that put *labels* in *reference*'s order, or None when they are already the same.
26
+
27
+ *index* gives a label array's index; a caller that keeps indexes on its
28
+ label arrays passes its own, so they are built once.
29
+ """
30
+ state, only_reference, only_labels = compare(reference, labels, "align", dim)
31
+ if state == "same":
32
+ return None
33
+ if state == "reordered":
34
+ ours = index(labels, "align", dim)
35
+ if not (ours.unique and index(reference, "align", dim).unique):
36
+ raise AlignError(f"{dim} has repeated labels in a different order on two operands; "
37
+ "repeated labels cannot be matched one to one")
38
+ return ours.get_indexer(reference)
39
+ raise AlignError(f"{dim} labels differ between operands: {only_labels:,} only in one, "
40
+ f"{only_reference:,} only in the other; join them first")