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 +82 -0
- labelarray/__version__.py +1 -0
- labelarray/_compiled.py +101 -0
- labelarray/align.py +40 -0
- labelarray/array.py +474 -0
- labelarray/collection.py +267 -0
- labelarray/contract.py +47 -0
- labelarray/errors.py +21 -0
- labelarray/grouped.py +259 -0
- labelarray/index.py +470 -0
- labelarray/labels.py +244 -0
- labelarray/py.typed +0 -0
- labelarray/sums.py +317 -0
- labelarray/transforms.py +610 -0
- labelarray-0.1.0.dist-info/METADATA +187 -0
- labelarray-0.1.0.dist-info/RECORD +19 -0
- labelarray-0.1.0.dist-info/WHEEL +5 -0
- labelarray-0.1.0.dist-info/licenses/LICENSE +21 -0
- labelarray-0.1.0.dist-info/top_level.txt +1 -0
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"
|
labelarray/_compiled.py
ADDED
|
@@ -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")
|