catii 1.0.5.dev1__tar.gz → 1.0.6.dev1__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.
- {catii-1.0.5.dev1/src/catii.egg-info → catii-1.0.6.dev1}/PKG-INFO +1 -1
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/setup.py +1 -1
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/src/catii/iindexes.py +3 -1
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/src/catii/xfuncs.py +519 -2
- {catii-1.0.5.dev1 → catii-1.0.6.dev1/src/catii.egg-info}/PKG-INFO +1 -1
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/LICENSE +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/README.md +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/pyproject.toml +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/setup.cfg +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/src/catii/__init__.py +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/src/catii/ccubes.py +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/src/catii/ffuncs.py +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/src/catii/indxio.py +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/src/catii/set_operations.pyx +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/src/catii/xcubes.py +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/src/catii.egg-info/SOURCES.txt +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/src/catii.egg-info/dependency_links.txt +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/src/catii.egg-info/requires.txt +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/src/catii.egg-info/top_level.txt +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/tests/test_ccubes.py +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/tests/test_iindexes.py +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/tests/test_iindexes_nonint.py +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/tests/test_indxio.py +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/tests/test_set_operations.py +0 -0
- {catii-1.0.5.dev1 → catii-1.0.6.dev1}/tests/test_xcubes.py +0 -0
|
@@ -773,7 +773,9 @@ class iindex(dict):
|
|
|
773
773
|
# this is much faster than heapq, and slightly faster than
|
|
774
774
|
# sortednp.kway_merge (or even custom specializations of it).
|
|
775
775
|
rowids = numpy.concatenate(rowid_lists)
|
|
776
|
-
|
|
776
|
+
# Each list is sorted; a stable sort (timsort) merges such
|
|
777
|
+
# runs instead of sorting from scratch.
|
|
778
|
+
rowids.sort(kind="stable")
|
|
777
779
|
if not assume_unique:
|
|
778
780
|
mask = numpy.empty(rowids.shape, dtype=bool)
|
|
779
781
|
mask[:1] = True
|
|
@@ -50,6 +50,8 @@ replace NaN values in the `values` array with 0 in that case. If you prefer
|
|
|
50
50
|
`sum([])` to return 0, for example, without a second "validity" array,
|
|
51
51
|
pass `return_missing_as=0`.
|
|
52
52
|
"""
|
|
53
|
+
import functools
|
|
54
|
+
import operator
|
|
53
55
|
import warnings
|
|
54
56
|
|
|
55
57
|
import numpy
|
|
@@ -1105,14 +1107,14 @@ class xfunc_quantile(xfunc):
|
|
|
1105
1107
|
if self.weights.shape:
|
|
1106
1108
|
w = self.weights[rowmask]
|
|
1107
1109
|
else:
|
|
1108
|
-
w = numpy.repeat(self.weights, len(
|
|
1110
|
+
w = numpy.repeat(self.weights, len(rowmask))
|
|
1109
1111
|
qs[i] = self.weighted_quantile(seg, self.probability, w)
|
|
1110
1112
|
|
|
1111
1113
|
def weighted_quantile(self, arr, probability, weights):
|
|
1112
1114
|
def weighted_quantile_1d(a):
|
|
1113
1115
|
w = weights
|
|
1114
1116
|
|
|
1115
|
-
ind =
|
|
1117
|
+
ind = a.argsort()
|
|
1116
1118
|
a = a[ind]
|
|
1117
1119
|
w = w[ind]
|
|
1118
1120
|
|
|
@@ -1490,3 +1492,518 @@ class xfunc_covariance(xfunc):
|
|
|
1490
1492
|
return covs, ~missings
|
|
1491
1493
|
else:
|
|
1492
1494
|
return covs
|
|
1495
|
+
|
|
1496
|
+
|
|
1497
|
+
# Default quantile probabilities
|
|
1498
|
+
DEFAULT_QUANTILES = [0.0, 0.25, 0.5, 0.75, 1.0]
|
|
1499
|
+
|
|
1500
|
+
|
|
1501
|
+
class xfunc_numeric_summary(xfunc):
|
|
1502
|
+
"""Calculate multiple numeric statistics in a single pass over the data.
|
|
1503
|
+
|
|
1504
|
+
This xfunc computes any combination of:
|
|
1505
|
+
- mean: weighted arithmetic mean
|
|
1506
|
+
- stddev: weighted standard deviation (ddof=1)
|
|
1507
|
+
- valid_count: unweighted count of non-missing values
|
|
1508
|
+
- sum: weighted sum
|
|
1509
|
+
- quantiles: weighted quantiles at specified probabilities
|
|
1510
|
+
|
|
1511
|
+
The goal is to reduce both the number of cube calls and the number of
|
|
1512
|
+
calculations by sharing intermediate computations.
|
|
1513
|
+
|
|
1514
|
+
Parameters
|
|
1515
|
+
----------
|
|
1516
|
+
arr : array or (values, validity) tuple
|
|
1517
|
+
NumPy array of numeric values, or a tuple of (values, validity) arrays.
|
|
1518
|
+
Missing values are represented by NaN or validity=False.
|
|
1519
|
+
|
|
1520
|
+
weights : array or (weights, validity) tuple, optional
|
|
1521
|
+
NumPy array of weight values.
|
|
1522
|
+
|
|
1523
|
+
ignore_missing : bool, default False
|
|
1524
|
+
If False, any missing value in a bin causes that bin's result to be
|
|
1525
|
+
missing. If True, missing values are skipped.
|
|
1526
|
+
|
|
1527
|
+
return_missing_as : scalar or tuple, default NaN
|
|
1528
|
+
If NaN, missing results are returned as NaN. If a tuple like (0, False),
|
|
1529
|
+
results are returned as (values, validity) tuples.
|
|
1530
|
+
|
|
1531
|
+
include : dict, optional
|
|
1532
|
+
Specifies which statistics to compute. Keys are statistic names,
|
|
1533
|
+
values are True or (for quantiles) a list of probabilities.
|
|
1534
|
+
Default: all statistics with quantiles=[0, 0.25, 0.5, 0.75, 1.0]
|
|
1535
|
+
|
|
1536
|
+
Examples:
|
|
1537
|
+
include={"mean": True, "stddev": True}
|
|
1538
|
+
include={"quantiles": [0.1, 0.5, 0.9]}
|
|
1539
|
+
include={"mean": True, "quantiles": [0.5]}
|
|
1540
|
+
|
|
1541
|
+
Returns
|
|
1542
|
+
-------
|
|
1543
|
+
dict
|
|
1544
|
+
A dict with requested statistics as keys.
|
|
1545
|
+
For dimensioned cubes, values are NumPy arrays shaped like the cube.
|
|
1546
|
+
The "quantiles" value is itself a dict mapping probability -> array.
|
|
1547
|
+
"""
|
|
1548
|
+
|
|
1549
|
+
def __init__(
|
|
1550
|
+
self,
|
|
1551
|
+
arr,
|
|
1552
|
+
weights=None,
|
|
1553
|
+
ignore_missing=False,
|
|
1554
|
+
return_missing_as=NaN,
|
|
1555
|
+
include=None,
|
|
1556
|
+
):
|
|
1557
|
+
# Parse include configuration
|
|
1558
|
+
if include is None:
|
|
1559
|
+
include = {
|
|
1560
|
+
"mean": True,
|
|
1561
|
+
"stddev": True,
|
|
1562
|
+
"valid_count": True,
|
|
1563
|
+
"sum": True,
|
|
1564
|
+
"quantiles": DEFAULT_QUANTILES,
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
if not include:
|
|
1568
|
+
raise ValueError("include must specify at least one statistic")
|
|
1569
|
+
|
|
1570
|
+
self.include_mean = include.get("mean", False)
|
|
1571
|
+
self.include_stddev = include.get("stddev", False)
|
|
1572
|
+
self.include_valid_count = include.get("valid_count", False)
|
|
1573
|
+
self.include_sum = include.get("sum", False)
|
|
1574
|
+
self.include_quantiles = include.get("quantiles", False)
|
|
1575
|
+
|
|
1576
|
+
if self.include_quantiles:
|
|
1577
|
+
if self.include_quantiles is True:
|
|
1578
|
+
self.quantile_probs = numpy.array(DEFAULT_QUANTILES, dtype=float)
|
|
1579
|
+
else:
|
|
1580
|
+
self.quantile_probs = numpy.array(self.include_quantiles, dtype=float)
|
|
1581
|
+
else:
|
|
1582
|
+
self.quantile_probs = None
|
|
1583
|
+
|
|
1584
|
+
# Process input array
|
|
1585
|
+
arr, validity = as_separate_validity(arr)
|
|
1586
|
+
self.shape = arr.shape[1:]
|
|
1587
|
+
self.size = numpy.prod(self.shape, 0)
|
|
1588
|
+
|
|
1589
|
+
# Process weights
|
|
1590
|
+
if weights is not None:
|
|
1591
|
+
weights, weights_validity = as_separate_validity(weights)
|
|
1592
|
+
validity = (validity.T & weights_validity).T
|
|
1593
|
+
# Set negative weights to 0, like SAS EXCLNPWGT option
|
|
1594
|
+
with numpy.errstate(invalid="ignore"):
|
|
1595
|
+
neg_weights = weights < 0
|
|
1596
|
+
if neg_weights.any():
|
|
1597
|
+
weights = weights.copy()
|
|
1598
|
+
weights[neg_weights] = 0
|
|
1599
|
+
|
|
1600
|
+
self.weights = weights
|
|
1601
|
+
self.validity = validity
|
|
1602
|
+
self.ignore_missing = ignore_missing
|
|
1603
|
+
self.return_missing_as = return_missing_as
|
|
1604
|
+
|
|
1605
|
+
if isinstance(self.return_missing_as, tuple):
|
|
1606
|
+
self.null = self.return_missing_as[0]
|
|
1607
|
+
else:
|
|
1608
|
+
self.null = self.return_missing_as
|
|
1609
|
+
|
|
1610
|
+
# Prepare data arrays for different computations
|
|
1611
|
+
# For quantiles, keep original values with NaN for missing
|
|
1612
|
+
self.arr = arr.astype(float).copy()
|
|
1613
|
+
self.arr[~validity] = NaN
|
|
1614
|
+
|
|
1615
|
+
# For mean/sum, prepare weighted summables
|
|
1616
|
+
if self.include_mean or self.include_sum or self.include_stddev:
|
|
1617
|
+
summables = arr.astype(float).copy()
|
|
1618
|
+
if weights is not None:
|
|
1619
|
+
self.wsummables = (summables.T * weights).T
|
|
1620
|
+
else:
|
|
1621
|
+
self.wsummables = summables
|
|
1622
|
+
self.wsummables[~validity] = 0
|
|
1623
|
+
|
|
1624
|
+
# For valid_count computation (weighted counts for mean denominator)
|
|
1625
|
+
if weights is not None:
|
|
1626
|
+
self.countables = (validity.astype(float).T * weights).T
|
|
1627
|
+
else:
|
|
1628
|
+
self.countables = validity.astype(float)
|
|
1629
|
+
self.countables[~validity] = 0
|
|
1630
|
+
|
|
1631
|
+
# Unweighted summables for stddev variance calculation
|
|
1632
|
+
self.summables = arr.astype(float).copy()
|
|
1633
|
+
self.summables[~validity] = 0
|
|
1634
|
+
|
|
1635
|
+
def get_initial_regions(self, cube):
|
|
1636
|
+
"""Return empty NumPy arrays to fill."""
|
|
1637
|
+
shape = cube.shape + self.shape
|
|
1638
|
+
if not shape:
|
|
1639
|
+
shape = (1,)
|
|
1640
|
+
|
|
1641
|
+
regions = {}
|
|
1642
|
+
|
|
1643
|
+
# valid_count is needed for mean denominator and stddev, so always allocate
|
|
1644
|
+
regions["valid_count"] = numpy.zeros(shape, dtype=int)
|
|
1645
|
+
|
|
1646
|
+
# For weighted mean, we need weighted counts separately
|
|
1647
|
+
if self.include_mean and self.weights is not None:
|
|
1648
|
+
regions["weighted_counts"] = numpy.zeros(shape, dtype=float)
|
|
1649
|
+
|
|
1650
|
+
if self.include_mean or self.include_sum:
|
|
1651
|
+
regions["sums"] = numpy.zeros(shape, dtype=float)
|
|
1652
|
+
|
|
1653
|
+
if self.include_stddev:
|
|
1654
|
+
regions["stddev_values"] = numpy.full(shape, NaN, dtype=float)
|
|
1655
|
+
|
|
1656
|
+
if self.include_quantiles:
|
|
1657
|
+
regions["quantiles"] = {
|
|
1658
|
+
p: numpy.full(shape, NaN, dtype=float) for p in self.quantile_probs
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
# Track missing for output
|
|
1662
|
+
if not self.ignore_missing:
|
|
1663
|
+
regions["missing_counts"] = numpy.zeros(shape, dtype=int)
|
|
1664
|
+
|
|
1665
|
+
return regions
|
|
1666
|
+
|
|
1667
|
+
def calculate(self, cube):
|
|
1668
|
+
"""Return a dict with requested statistics.
|
|
1669
|
+
|
|
1670
|
+
This overrides the base calculate to properly handle the regions dict
|
|
1671
|
+
format and compute coordinates from the cube.
|
|
1672
|
+
"""
|
|
1673
|
+
regions = self.get_initial_regions(cube)
|
|
1674
|
+
|
|
1675
|
+
# Compute coordinates from cube dimensions (same logic as xcube.calculate)
|
|
1676
|
+
strided_dims = cube.strided_dims()
|
|
1677
|
+
coordinates = (
|
|
1678
|
+
functools.reduce(operator.add, strided_dims) if strided_dims else None
|
|
1679
|
+
)
|
|
1680
|
+
|
|
1681
|
+
self.fill(coordinates, regions)
|
|
1682
|
+
return self.reduce(cube, regions)
|
|
1683
|
+
|
|
1684
|
+
def fill(self, coordinates, regions):
|
|
1685
|
+
"""Fill regions with statistics computed in a single pass."""
|
|
1686
|
+
if coordinates is None:
|
|
1687
|
+
self._fill_no_coordinates(regions)
|
|
1688
|
+
else:
|
|
1689
|
+
self._fill_with_coordinates(coordinates, regions)
|
|
1690
|
+
|
|
1691
|
+
def _fill_no_coordinates(self, regions):
|
|
1692
|
+
"""Fill for scalar cube (no dimensions)."""
|
|
1693
|
+
valid_count = numpy.count_nonzero(self.validity, axis=0)
|
|
1694
|
+
regions["valid_count"][:] = valid_count
|
|
1695
|
+
|
|
1696
|
+
if not self.ignore_missing:
|
|
1697
|
+
regions["missing_counts"][:] = numpy.count_nonzero(~self.validity, axis=0)
|
|
1698
|
+
|
|
1699
|
+
# Mean and sum share weighted sums
|
|
1700
|
+
if self.include_mean or self.include_sum:
|
|
1701
|
+
value_sums = numpy.nansum(self.wsummables, axis=0)
|
|
1702
|
+
regions["sums"][:] = value_sums
|
|
1703
|
+
|
|
1704
|
+
if self.include_mean and self.weights is not None:
|
|
1705
|
+
regions["weighted_counts"][:] = numpy.sum(self.countables, axis=0)
|
|
1706
|
+
|
|
1707
|
+
if self.include_stddev:
|
|
1708
|
+
# Compute stddev
|
|
1709
|
+
if self.weights is not None:
|
|
1710
|
+
weighted_counts = numpy.sum(self.countables, axis=0)
|
|
1711
|
+
value_sums = numpy.nansum(self.wsummables, axis=0)
|
|
1712
|
+
with numpy.errstate(divide="ignore", invalid="ignore"):
|
|
1713
|
+
means = value_sums / weighted_counts
|
|
1714
|
+
else:
|
|
1715
|
+
value_sums = numpy.nansum(self.wsummables, axis=0)
|
|
1716
|
+
with numpy.errstate(divide="ignore", invalid="ignore"):
|
|
1717
|
+
means = value_sums / valid_count
|
|
1718
|
+
|
|
1719
|
+
squared_variances = (self.summables - means) ** 2
|
|
1720
|
+
if self.weights is not None:
|
|
1721
|
+
squared_variances = (squared_variances.T * self.weights).T
|
|
1722
|
+
squared_variances[~self.validity] = 0
|
|
1723
|
+
varsums = numpy.sum(squared_variances, axis=0)
|
|
1724
|
+
|
|
1725
|
+
with numpy.errstate(divide="ignore", invalid="ignore"):
|
|
1726
|
+
if self.weights is None:
|
|
1727
|
+
regions["stddev_values"][:] = numpy.sqrt(varsums / (valid_count - 1))
|
|
1728
|
+
else:
|
|
1729
|
+
weight_sums = numpy.sum(
|
|
1730
|
+
numpy.where(self.validity, self.weights, 0), axis=0
|
|
1731
|
+
)
|
|
1732
|
+
regions["stddev_values"][:] = numpy.sqrt(
|
|
1733
|
+
(varsums / weight_sums) * (valid_count / (valid_count - 1))
|
|
1734
|
+
)
|
|
1735
|
+
|
|
1736
|
+
if self.include_quantiles:
|
|
1737
|
+
if self.weights is None:
|
|
1738
|
+
# Unweighted: numpy handles multiple quantiles efficiently
|
|
1739
|
+
for p in self.quantile_probs:
|
|
1740
|
+
if len(self.arr) and not numpy.all(numpy.isnan(self.arr)):
|
|
1741
|
+
if self.ignore_missing:
|
|
1742
|
+
regions["quantiles"][p][:] = numpy.nanquantile(
|
|
1743
|
+
self.arr, p, axis=0
|
|
1744
|
+
)
|
|
1745
|
+
else:
|
|
1746
|
+
regions["quantiles"][p][:] = numpy.quantile(
|
|
1747
|
+
self.arr, p, axis=0
|
|
1748
|
+
)
|
|
1749
|
+
else:
|
|
1750
|
+
# Weighted: sort once and compute all quantiles
|
|
1751
|
+
results = self._weighted_quantiles_all(
|
|
1752
|
+
self.arr, self.quantile_probs, self.weights
|
|
1753
|
+
)
|
|
1754
|
+
for p, val in zip(self.quantile_probs, results):
|
|
1755
|
+
regions["quantiles"][p][:] = val
|
|
1756
|
+
|
|
1757
|
+
|
|
1758
|
+
def _fill_with_coordinates(self, coordinates, regions):
|
|
1759
|
+
"""Fill for cube with dimensions."""
|
|
1760
|
+
# Get size from valid_count region
|
|
1761
|
+
size = regions["valid_count"].size
|
|
1762
|
+
|
|
1763
|
+
# valid_count is the unweighted count of non-missing values
|
|
1764
|
+
valid_count = numpy.bincount(
|
|
1765
|
+
coordinates, weights=self.validity.astype(float), minlength=size
|
|
1766
|
+
)[:size].astype(int)
|
|
1767
|
+
regions["valid_count"].flat[:] = valid_count
|
|
1768
|
+
|
|
1769
|
+
if not self.ignore_missing:
|
|
1770
|
+
missing = numpy.bincount(
|
|
1771
|
+
coordinates, weights=(~self.validity).astype(float), minlength=size
|
|
1772
|
+
)[:size].astype(int)
|
|
1773
|
+
regions["missing_counts"].flat[:] = missing
|
|
1774
|
+
|
|
1775
|
+
if self.include_mean or self.include_sum or self.include_stddev:
|
|
1776
|
+
value_sums = numpy.bincount(
|
|
1777
|
+
coordinates, weights=self.wsummables, minlength=size
|
|
1778
|
+
)[:size]
|
|
1779
|
+
if self.include_mean or self.include_sum:
|
|
1780
|
+
regions["sums"].flat[:] = value_sums
|
|
1781
|
+
|
|
1782
|
+
if self.include_mean and self.weights is not None:
|
|
1783
|
+
weighted_counts = numpy.bincount(
|
|
1784
|
+
coordinates, weights=self.countables, minlength=size
|
|
1785
|
+
)[:size]
|
|
1786
|
+
regions["weighted_counts"].flat[:] = weighted_counts
|
|
1787
|
+
|
|
1788
|
+
if self.include_stddev:
|
|
1789
|
+
if self.weights is not None:
|
|
1790
|
+
weighted_counts = numpy.bincount(
|
|
1791
|
+
coordinates, weights=self.countables, minlength=size
|
|
1792
|
+
)[:size]
|
|
1793
|
+
with numpy.errstate(divide="ignore", invalid="ignore"):
|
|
1794
|
+
means = value_sums / weighted_counts
|
|
1795
|
+
else:
|
|
1796
|
+
with numpy.errstate(divide="ignore", invalid="ignore"):
|
|
1797
|
+
means = value_sums / valid_count
|
|
1798
|
+
|
|
1799
|
+
# Compute variance sums
|
|
1800
|
+
squared_variances = (self.summables - means[coordinates]) ** 2
|
|
1801
|
+
if self.weights is not None:
|
|
1802
|
+
squared_variances = squared_variances * self.weights
|
|
1803
|
+
squared_variances[~self.validity] = 0
|
|
1804
|
+
varsums = numpy.bincount(
|
|
1805
|
+
coordinates, weights=squared_variances, minlength=size
|
|
1806
|
+
)[:size]
|
|
1807
|
+
|
|
1808
|
+
with numpy.errstate(divide="ignore", invalid="ignore"):
|
|
1809
|
+
if self.weights is None:
|
|
1810
|
+
regions["stddev_values"].flat[:] = numpy.sqrt(
|
|
1811
|
+
varsums / (valid_count - 1)
|
|
1812
|
+
)
|
|
1813
|
+
else:
|
|
1814
|
+
weight_sums = numpy.bincount(
|
|
1815
|
+
coordinates,
|
|
1816
|
+
weights=numpy.where(self.validity, self.weights, 0),
|
|
1817
|
+
minlength=size,
|
|
1818
|
+
)[:size]
|
|
1819
|
+
regions["stddev_values"].flat[:] = numpy.sqrt(
|
|
1820
|
+
(varsums / weight_sums) * (valid_count / (valid_count - 1))
|
|
1821
|
+
)
|
|
1822
|
+
|
|
1823
|
+
if self.include_quantiles:
|
|
1824
|
+
# Quantiles require per-bin iteration (sorting-based)
|
|
1825
|
+
for i, rowmask in self.bins(coordinates, size):
|
|
1826
|
+
seg = self.arr[rowmask]
|
|
1827
|
+
if len(seg) == 0 or numpy.all(numpy.isnan(seg)):
|
|
1828
|
+
continue
|
|
1829
|
+
|
|
1830
|
+
if self.weights is None:
|
|
1831
|
+
for p in self.quantile_probs:
|
|
1832
|
+
if self.ignore_missing:
|
|
1833
|
+
regions["quantiles"][p].flat[i] = numpy.nanquantile(
|
|
1834
|
+
seg, p, axis=0
|
|
1835
|
+
)
|
|
1836
|
+
else:
|
|
1837
|
+
regions["quantiles"][p].flat[i] = numpy.quantile(
|
|
1838
|
+
seg, p, axis=0
|
|
1839
|
+
)
|
|
1840
|
+
else:
|
|
1841
|
+
w = self.weights[rowmask]
|
|
1842
|
+
for p in self.quantile_probs:
|
|
1843
|
+
regions["quantiles"][p].flat[i] = self._weighted_quantile(
|
|
1844
|
+
seg, p, w
|
|
1845
|
+
)
|
|
1846
|
+
|
|
1847
|
+
def _weighted_quantiles_all(self, arr, probabilities, weights):
|
|
1848
|
+
"""Compute multiple weighted quantiles with a single sort.
|
|
1849
|
+
|
|
1850
|
+
This is much faster than calling _weighted_quantile for each probability
|
|
1851
|
+
because sorting is O(n log n) and dominates the computation.
|
|
1852
|
+
"""
|
|
1853
|
+
|
|
1854
|
+
def weighted_quantiles_1d(a):
|
|
1855
|
+
w = weights
|
|
1856
|
+
|
|
1857
|
+
# Sort once
|
|
1858
|
+
ind = a.argsort()
|
|
1859
|
+
a = a[ind]
|
|
1860
|
+
w = w[ind]
|
|
1861
|
+
|
|
1862
|
+
if self.ignore_missing:
|
|
1863
|
+
missing = numpy.isnan(a) | numpy.isnan(w)
|
|
1864
|
+
if numpy.any(missing):
|
|
1865
|
+
valid = ~missing
|
|
1866
|
+
a = a[valid]
|
|
1867
|
+
w = w[valid]
|
|
1868
|
+
|
|
1869
|
+
N = len(w)
|
|
1870
|
+
if N == 0:
|
|
1871
|
+
return [NaN] * len(probabilities)
|
|
1872
|
+
|
|
1873
|
+
cs = w.cumsum(axis=0)
|
|
1874
|
+
xdiff = numpy.diff(a, append=[0], axis=0)
|
|
1875
|
+
|
|
1876
|
+
# Compute all quantiles from the same sorted data
|
|
1877
|
+
results = []
|
|
1878
|
+
for prob_val in probabilities:
|
|
1879
|
+
prob = prob_val * cs[-1]
|
|
1880
|
+
right = numpy.digitize(prob, cs)
|
|
1881
|
+
left = (right - 1).clip(min=0)
|
|
1882
|
+
with numpy.errstate(divide="ignore", invalid="ignore"):
|
|
1883
|
+
frac = (prob - cs[left]).clip(min=0) / w[right.clip(max=len(w) - 1)]
|
|
1884
|
+
results.append(a[left] + frac * xdiff[left])
|
|
1885
|
+
return results
|
|
1886
|
+
|
|
1887
|
+
if arr.ndim == 1:
|
|
1888
|
+
return weighted_quantiles_1d(arr)
|
|
1889
|
+
|
|
1890
|
+
# For 2D arrays, apply along axis 0
|
|
1891
|
+
n_probs = len(probabilities)
|
|
1892
|
+
result_shape = arr.shape[1:] if arr.ndim > 1 else (1,)
|
|
1893
|
+
results = [numpy.full(result_shape, NaN, dtype=float) for _ in range(n_probs)]
|
|
1894
|
+
|
|
1895
|
+
for col_idx in range(arr.shape[1] if arr.ndim > 1 else 1):
|
|
1896
|
+
col = arr[:, col_idx] if arr.ndim > 1 else arr
|
|
1897
|
+
col_results = weighted_quantiles_1d(col)
|
|
1898
|
+
for i, val in enumerate(col_results):
|
|
1899
|
+
if arr.ndim > 1:
|
|
1900
|
+
results[i][col_idx] = val
|
|
1901
|
+
else:
|
|
1902
|
+
results[i][:] = val
|
|
1903
|
+
|
|
1904
|
+
return results
|
|
1905
|
+
|
|
1906
|
+
def _weighted_quantile(self, arr, probability, weights):
|
|
1907
|
+
"""Compute weighted quantile for 1D array."""
|
|
1908
|
+
|
|
1909
|
+
def weighted_quantile_1d(a):
|
|
1910
|
+
w = weights
|
|
1911
|
+
|
|
1912
|
+
ind = a.argsort()
|
|
1913
|
+
a = a[ind]
|
|
1914
|
+
w = w[ind]
|
|
1915
|
+
|
|
1916
|
+
if self.ignore_missing:
|
|
1917
|
+
missing = numpy.isnan(a) | numpy.isnan(w)
|
|
1918
|
+
if numpy.any(missing):
|
|
1919
|
+
valid = ~missing
|
|
1920
|
+
a = a[valid]
|
|
1921
|
+
w = w[valid]
|
|
1922
|
+
|
|
1923
|
+
N = len(w)
|
|
1924
|
+
if N == 0:
|
|
1925
|
+
return NaN
|
|
1926
|
+
|
|
1927
|
+
cs = w.cumsum(axis=0)
|
|
1928
|
+
prob = probability * cs[-1]
|
|
1929
|
+
right = numpy.digitize(prob, cs)
|
|
1930
|
+
left = (right - 1).clip(min=0)
|
|
1931
|
+
xdiff = numpy.diff(a, append=[0], axis=0)
|
|
1932
|
+
with numpy.errstate(divide="ignore", invalid="ignore"):
|
|
1933
|
+
frac = (prob - cs[left]).clip(min=0) / w[right.clip(max=len(w) - 1)]
|
|
1934
|
+
return a[left] + frac * xdiff[left]
|
|
1935
|
+
|
|
1936
|
+
if arr.ndim == 1:
|
|
1937
|
+
return weighted_quantile_1d(arr)
|
|
1938
|
+
return numpy.apply_along_axis(weighted_quantile_1d, 0, arr)
|
|
1939
|
+
|
|
1940
|
+
def reduce(self, cube, regions):
|
|
1941
|
+
"""Reduce regions to final output."""
|
|
1942
|
+
result = {}
|
|
1943
|
+
|
|
1944
|
+
valid_count = regions["valid_count"]
|
|
1945
|
+
if not self.ignore_missing:
|
|
1946
|
+
missing_counts = regions["missing_counts"]
|
|
1947
|
+
has_missing = missing_counts != 0
|
|
1948
|
+
else:
|
|
1949
|
+
has_missing = numpy.zeros_like(valid_count, dtype=bool)
|
|
1950
|
+
|
|
1951
|
+
no_data = valid_count == 0
|
|
1952
|
+
|
|
1953
|
+
if self.include_mean:
|
|
1954
|
+
if self.weights is not None:
|
|
1955
|
+
denominator = regions["weighted_counts"]
|
|
1956
|
+
else:
|
|
1957
|
+
denominator = valid_count
|
|
1958
|
+
with numpy.errstate(divide="ignore", invalid="ignore"):
|
|
1959
|
+
mean = regions["sums"] / denominator
|
|
1960
|
+
output_is_missing = no_data | has_missing
|
|
1961
|
+
mean = self.adjust_zeros(mean, self.null, condition=output_is_missing)
|
|
1962
|
+
if isinstance(self.return_missing_as, tuple):
|
|
1963
|
+
result["mean"] = (mean, ~output_is_missing)
|
|
1964
|
+
else:
|
|
1965
|
+
result["mean"] = mean
|
|
1966
|
+
|
|
1967
|
+
if self.include_stddev:
|
|
1968
|
+
stddev = regions["stddev_values"]
|
|
1969
|
+
# stddev needs at least 2 values
|
|
1970
|
+
output_is_missing = (valid_count < 2) | has_missing
|
|
1971
|
+
stddev = self.adjust_zeros(stddev, self.null, condition=output_is_missing)
|
|
1972
|
+
if isinstance(self.return_missing_as, tuple):
|
|
1973
|
+
result["stddev"] = (stddev, ~output_is_missing)
|
|
1974
|
+
else:
|
|
1975
|
+
result["stddev"] = stddev
|
|
1976
|
+
|
|
1977
|
+
if self.include_valid_count:
|
|
1978
|
+
# valid_count is always returned, even for empty bins
|
|
1979
|
+
if isinstance(self.return_missing_as, tuple):
|
|
1980
|
+
result["valid_count"] = (valid_count, ~no_data)
|
|
1981
|
+
else:
|
|
1982
|
+
result["valid_count"] = valid_count
|
|
1983
|
+
|
|
1984
|
+
if self.include_sum:
|
|
1985
|
+
sum_values = regions["sums"]
|
|
1986
|
+
output_is_missing = no_data | has_missing
|
|
1987
|
+
sum_values = self.adjust_zeros(
|
|
1988
|
+
sum_values, self.null, condition=output_is_missing
|
|
1989
|
+
)
|
|
1990
|
+
if isinstance(self.return_missing_as, tuple):
|
|
1991
|
+
result["sum"] = (sum_values, ~output_is_missing)
|
|
1992
|
+
else:
|
|
1993
|
+
result["sum"] = sum_values
|
|
1994
|
+
|
|
1995
|
+
if self.include_quantiles:
|
|
1996
|
+
quantiles_result = {}
|
|
1997
|
+
output_is_missing = no_data | has_missing
|
|
1998
|
+
for p in self.quantile_probs:
|
|
1999
|
+
q = regions["quantiles"][p]
|
|
2000
|
+
if isinstance(self.return_missing_as, tuple):
|
|
2001
|
+
missings = numpy.isnan(q) | output_is_missing
|
|
2002
|
+
q = q.copy()
|
|
2003
|
+
q[missings] = self.null
|
|
2004
|
+
quantiles_result[p] = (q, ~missings)
|
|
2005
|
+
else:
|
|
2006
|
+
quantiles_result[p] = q
|
|
2007
|
+
result["quantiles"] = quantiles_result
|
|
2008
|
+
|
|
2009
|
+
return result
|
|
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
|