plot-io 0.12.0__py3-none-win_amd64.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.
- plot_io/__init__.py +73 -0
- plot_io/_methods.py +1691 -0
- plot_io/_native/__init__.py +199 -0
- plot_io/_native/__main__.py +197 -0
- plot_io/_native/_api.py +457 -0
- plot_io/_native/bin/windows-x86_64.dll +0 -0
- plot_io/_native/kernels.py +335 -0
- plot_io/_native/manifest.json +49 -0
- plot_io/axis.py +1347 -0
- plot_io/boxes.py +1156 -0
- plot_io/canvas.py +389 -0
- plot_io/color/__init__.py +3 -0
- plot_io/color/data.py +1646 -0
- plot_io/color/maps.py +922 -0
- plot_io/color/values.py +295 -0
- plot_io/color/variants.py +1246 -0
- plot_io/colorbar.py +1426 -0
- plot_io/columns.py +614 -0
- plot_io/decorations.py +920 -0
- plot_io/defaults.py +809 -0
- plot_io/density.py +113 -0
- plot_io/ext.py +68 -0
- plot_io/facets.py +240 -0
- plot_io/figure.py +788 -0
- plot_io/formatters.py +97 -0
- plot_io/genome/__init__.py +17 -0
- plot_io/genome/adapters.py +356 -0
- plot_io/genome/combine.py +123 -0
- plot_io/genome/contacts.py +508 -0
- plot_io/genome/intervals.py +750 -0
- plot_io/genome/lanes.py +191 -0
- plot_io/genome/layout.py +184 -0
- plot_io/genome/links.py +364 -0
- plot_io/genome/pack.py +27 -0
- plot_io/genome/readers.py +145 -0
- plot_io/genome/regions.py +228 -0
- plot_io/genome/scale.py +349 -0
- plot_io/genome/series.py +344 -0
- plot_io/genome/signal.py +621 -0
- plot_io/genome/sites.py +782 -0
- plot_io/genome/sources.py +459 -0
- plot_io/genome/testing.py +161 -0
- plot_io/geometry.py +289 -0
- plot_io/groups.py +308 -0
- plot_io/guides.py +503 -0
- plot_io/helpers/__init__.py +17 -0
- plot_io/helpers/clustermap.py +202 -0
- plot_io/helpers/dendrogram.py +172 -0
- plot_io/helpers/grids.py +304 -0
- plot_io/helpers/qq.py +334 -0
- plot_io/helpers/raincloud.py +249 -0
- plot_io/helpers/tree.py +214 -0
- plot_io/helpers/volcano.py +505 -0
- plot_io/hints.py +125 -0
- plot_io/kernels.py +740 -0
- plot_io/labels.py +566 -0
- plot_io/layers/__init__.py +100 -0
- plot_io/layers/base.py +630 -0
- plot_io/layers/context.py +87 -0
- plot_io/layers/core.py +446 -0
- plot_io/layers/geoms.py +1362 -0
- plot_io/layers/grammar.py +586 -0
- plot_io/layers/paint.py +614 -0
- plot_io/layers/pipeline.py +170 -0
- plot_io/layers/positions.py +431 -0
- plot_io/layers/stats.py +1088 -0
- plot_io/layout.py +1381 -0
- plot_io/legend.py +605 -0
- plot_io/levels.py +509 -0
- plot_io/limits.py +124 -0
- plot_io/lines.py +42 -0
- plot_io/mapped.py +522 -0
- plot_io/markers.py +110 -0
- plot_io/markup.py +128 -0
- plot_io/numbers.py +354 -0
- plot_io/occupancy.py +295 -0
- plot_io/plot.py +597 -0
- plot_io/presets/__init__.py +51 -0
- plot_io/presets/_util.py +153 -0
- plot_io/presets/area.py +145 -0
- plot_io/presets/bar.py +261 -0
- plot_io/presets/box.py +400 -0
- plot_io/presets/catpoints.py +177 -0
- plot_io/presets/contour.py +353 -0
- plot_io/presets/curves.py +30 -0
- plot_io/presets/dots.py +355 -0
- plot_io/presets/ecdf.py +97 -0
- plot_io/presets/errorbar.py +106 -0
- plot_io/presets/errors.py +296 -0
- plot_io/presets/fill.py +74 -0
- plot_io/presets/heatmap.py +322 -0
- plot_io/presets/hexbin.py +198 -0
- plot_io/presets/hist.py +391 -0
- plot_io/presets/hist2d.py +87 -0
- plot_io/presets/kde.py +162 -0
- plot_io/presets/kde2d.py +147 -0
- plot_io/presets/line.py +143 -0
- plot_io/presets/pie.py +502 -0
- plot_io/presets/regression.py +115 -0
- plot_io/presets/scatter.py +194 -0
- plot_io/presets/stem.py +100 -0
- plot_io/presets/violin.py +323 -0
- plot_io/py.typed +0 -0
- plot_io/registry.py +82 -0
- plot_io/render.py +480 -0
- plot_io/rows.py +491 -0
- plot_io/scales.py +103 -0
- plot_io/seams.py +138 -0
- plot_io/settings.py +807 -0
- plot_io/shapes.py +66 -0
- plot_io/sharing.py +232 -0
- plot_io/sizemaps.py +97 -0
- plot_io/spec.py +240 -0
- plot_io/states.py +216 -0
- plot_io/styles/__init__.py +307 -0
- plot_io/styles/library.py +397 -0
- plot_io/tables.py +138 -0
- plot_io/text.py +412 -0
- plot_io/ticks.py +400 -0
- plot_io/times.py +287 -0
- plot_io/titles.py +225 -0
- plot_io/transform.py +114 -0
- plot_io/types.py +13604 -0
- plot_io/units.py +768 -0
- plot_io/validators.py +1029 -0
- plot_io/values.py +190 -0
- plot_io-0.12.0.dist-info/METADATA +215 -0
- plot_io-0.12.0.dist-info/RECORD +130 -0
- plot_io-0.12.0.dist-info/WHEEL +4 -0
- plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
plot_io/kernels.py
ADDED
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
"""Numeric kernels in plot_io's C library (``plot_io/_native``), on arrays.
|
|
2
|
+
|
|
3
|
+
Arguments are sequences of numbers (``None`` for missing values) or any object exporting
|
|
4
|
+
the buffer protocol (``array.array``, ``memoryview``, numpy arrays, ...). Buffers are read
|
|
5
|
+
through :class:`memoryview` and converted in C; numpy is never imported. 1-D intake is
|
|
6
|
+
vec_io's :func:`vec_io.arrays.doubles` (re-exported as :func:`doubles`, with
|
|
7
|
+
:func:`rows`, :func:`grid` and :func:`buffer_shape` for 2-D and 3-D buffers).
|
|
8
|
+
|
|
9
|
+
:func:`scaled` (values through a function and an affine map), :func:`extent`,
|
|
10
|
+
:func:`quantiles`, :func:`box_stats`; binning (:func:`histogram`, :func:`bin_edges`,
|
|
11
|
+
:func:`histogram2d`, :func:`hexbin`, :func:`ecdf`), kernel densities (:func:`kde`,
|
|
12
|
+
:func:`bandwidth`; 2-D: :func:`kde2d_at`, :func:`kde2d_grid`, :func:`kde2d_bandwidth`),
|
|
13
|
+
contours (:func:`contour_lines`, :func:`contour_bands`), :func:`lowess`, :func:`swarm`,
|
|
14
|
+
:func:`argsort` and :func:`take`; hierarchical clustering (:func:`linkage`). Heatmap cells are mapped by :meth:`plot_io.ColorMap.cells`.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import array
|
|
18
|
+
import bisect
|
|
19
|
+
import ctypes
|
|
20
|
+
import math
|
|
21
|
+
import sys
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from vec_io import threads
|
|
26
|
+
from vec_io.arrays import buffer_shape, doubles, grid, rows
|
|
27
|
+
|
|
28
|
+
from ._native import kernels as _k
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = ["doubles", "buffer_shape", "rows", "grid", "scaled", "extent", "quantiles",
|
|
32
|
+
"box_stats", "BoxStats", "histogram", "bin_edges", "histogram2d", "hexbin", "ecdf",
|
|
33
|
+
"kde", "bandwidth", "kde2d_bandwidth", "kde2d_at", "kde2d_grid", "contour_lines",
|
|
34
|
+
"contour_bands", "lowess", "swarm", "argsort", "take", "linkage", "LINKAGE_METHODS",
|
|
35
|
+
"LINKAGE_METRICS"]
|
|
36
|
+
|
|
37
|
+
def scaled(values: Any, scale: float = 1.0, offset: float = 0.0, function: str | None = None,
|
|
38
|
+
low: float | None = None, high: float | None = None, under: float | None = None,
|
|
39
|
+
over: float | None = None, nan: float | None = None,
|
|
40
|
+
linthresh: float = 1.0, below: float | None = None,
|
|
41
|
+
above: float | None = None) -> array.array:
|
|
42
|
+
"""``scale * f(v) + offset`` for every value, as ``array('d')``.
|
|
43
|
+
|
|
44
|
+
``function``: ``None``, ``"log"`` (natural), ``"log2"``, ``"log10"``, ``"sqrt"``,
|
|
45
|
+
``"exp"``, ``"symlog"`` (``sign(v) log10(1 + |v| / linthresh)``) or ``"logit"``
|
|
46
|
+
(``log10(p / (1 - p))``, for 0 < p < 1). Missing values, values outside the function's domain and results that are not
|
|
47
|
+
finite become NaN, or ``nan`` when it is given. Other results below ``low`` become
|
|
48
|
+
``under`` (default ``low``: clamped) and results above ``high`` become ``over`` (default
|
|
49
|
+
``high``); NaN as ``under`` or ``over`` makes those values missing. ``below`` and
|
|
50
|
+
``above``, when given, replace the results of finite values below the function's domain
|
|
51
|
+
(``log``: ≤ 0, ``sqrt``: < 0, ``logit``: ≤ 0) and above it (``logit``: ≥ 1, ``exp``:
|
|
52
|
+
overflowing)."""
|
|
53
|
+
low_ = -math.inf if low is None else float(low)
|
|
54
|
+
high_ = math.inf if high is None else float(high)
|
|
55
|
+
return _k.scaled(doubles(values), float(scale), float(offset), function, low_, high_,
|
|
56
|
+
low_ if under is None else float(under),
|
|
57
|
+
high_ if over is None else float(over),
|
|
58
|
+
None if nan is None else float(nan), float(linthresh),
|
|
59
|
+
None if below is None else float(below),
|
|
60
|
+
None if above is None else float(above))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def extent(values: Any, function: str | None = None) -> tuple[int, float, float]:
|
|
64
|
+
"""``(count, min, max)`` of the finite values in the domain of ``function`` (names as in
|
|
65
|
+
:func:`scaled`); ``(0, nan, nan)`` when there are none."""
|
|
66
|
+
return _k.extent(doubles(values), function)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def quantiles(values: Any, qs: Any, weights: Any = None) -> list[float]:
|
|
70
|
+
"""Quantiles ``qs`` (each in [0, 1]) of the finite values, by linear interpolation between
|
|
71
|
+
closest ranks (numpy's default); NaN for each when there are no finite values.
|
|
72
|
+
|
|
73
|
+
``weights`` count as repeated values (integer weights give the quantiles of the values
|
|
74
|
+
repeated): Hmisc's ``wtd.quantile`` (type ``"quantile"``), positions ``1 + (n - 1) q``
|
|
75
|
+
on the cumulative weights, ``n`` their total."""
|
|
76
|
+
qs = [float(q) for q in qs]
|
|
77
|
+
if weights is None:
|
|
78
|
+
return _k.quantiles(doubles(values), qs)
|
|
79
|
+
if not all(0.0 <= q <= 1.0 for q in qs):
|
|
80
|
+
raise ValueError("each q must be in [0, 1]")
|
|
81
|
+
xs, cw = _weighted_sorted(values, weights)
|
|
82
|
+
return [_weighted_quantile(xs, cw, q) for q in qs]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _weighted_sorted(values: Any, weights: Any) -> tuple[list[float], list[float]]:
|
|
86
|
+
"""The finite values of positive weight, sorted, and their cumulative weights."""
|
|
87
|
+
v = doubles(values)
|
|
88
|
+
w = _weights(weights, len(v))
|
|
89
|
+
assert w is not None
|
|
90
|
+
xs: list[float] = []
|
|
91
|
+
cw: list[float] = []
|
|
92
|
+
total = 0.0
|
|
93
|
+
for i in argsort(v):
|
|
94
|
+
x, wi = v[i], w[i]
|
|
95
|
+
if wi > 0 and math.isfinite(x):
|
|
96
|
+
total += wi
|
|
97
|
+
xs.append(x)
|
|
98
|
+
cw.append(total)
|
|
99
|
+
return xs, cw
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _weighted_quantile(xs: list[float], cw: list[float], q: float) -> float:
|
|
103
|
+
"""Quantile ``q`` of sorted values ``xs`` with cumulative weights ``cw``: the value at
|
|
104
|
+
(1-based) position ``r`` is the first whose cumulative weight reaches ``r``, positions
|
|
105
|
+
clamped to the ends; between the positions ``floor`` and ``floor + 1`` of
|
|
106
|
+
``1 + (n - 1) q``, linear as :func:`quantiles` (integer weights: equal to the values
|
|
107
|
+
repeated)."""
|
|
108
|
+
if not xs:
|
|
109
|
+
return math.nan
|
|
110
|
+
n = cw[-1]
|
|
111
|
+
|
|
112
|
+
def at(r: float) -> float:
|
|
113
|
+
return xs[min(bisect.bisect_left(cw, r), len(xs) - 1)]
|
|
114
|
+
|
|
115
|
+
h = (n - 1.0) * q
|
|
116
|
+
lo = math.floor(h)
|
|
117
|
+
t = h - lo
|
|
118
|
+
if lo < 0: # a weight total below 1
|
|
119
|
+
order = 1.0 + h
|
|
120
|
+
a, b, t = at(1.0), at(min(2.0, n)), order % 1.0
|
|
121
|
+
return (1.0 - t) * a + t * b
|
|
122
|
+
a = at(lo + 1.0)
|
|
123
|
+
if lo + 1.0 >= n or t == 0:
|
|
124
|
+
return a
|
|
125
|
+
b = at(min(lo + 2.0, n))
|
|
126
|
+
d = b - a
|
|
127
|
+
return b - d * (1.0 - t) if t >= 0.5 else a + d * t
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class BoxStats:
|
|
132
|
+
"""Box-plot statistics of the finite values (NaN fields when there are none)."""
|
|
133
|
+
count: float # with weights: their total
|
|
134
|
+
mean: float
|
|
135
|
+
q1: float
|
|
136
|
+
median: float
|
|
137
|
+
q3: float
|
|
138
|
+
low: float # whisker ends
|
|
139
|
+
high: float
|
|
140
|
+
outliers: array.array # values outside [low, high], in data order
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def box_stats(values: Any, whiskers: Any = 1.5, weights: Any = None) -> BoxStats:
|
|
144
|
+
"""Quartiles, median, mean, whisker ends and outliers of the finite values.
|
|
145
|
+
|
|
146
|
+
``whiskers``: a number w (whiskers at the furthest data within w * IQR of the box),
|
|
147
|
+
``"range"`` (the minimum and maximum) or ``(low, high)`` percentiles (0-100).
|
|
148
|
+
``weights`` count as repeated values (quantiles as :func:`quantiles`); values of weight
|
|
149
|
+
0 are left out, and each outlier is listed once."""
|
|
150
|
+
if isinstance(whiskers, str):
|
|
151
|
+
if whiskers != "range":
|
|
152
|
+
raise ValueError(f"whiskers must be a number, 'range' or (low, high), not {whiskers!r}")
|
|
153
|
+
mode, a, b = 1, 0.0, 0.0
|
|
154
|
+
elif isinstance(whiskers, (tuple, list)):
|
|
155
|
+
if len(whiskers) != 2:
|
|
156
|
+
raise ValueError("whisker percentiles must be a (low, high) pair")
|
|
157
|
+
mode, a, b = 2, float(whiskers[0]), float(whiskers[1])
|
|
158
|
+
else:
|
|
159
|
+
mode, a, b = 0, float(whiskers), 0.0
|
|
160
|
+
if weights is not None:
|
|
161
|
+
return _weighted_box_stats(values, weights, mode, a, b)
|
|
162
|
+
count, out, outliers = _k.box_stats(doubles(values), mode, a, b)
|
|
163
|
+
mean, q1, median, q3, low, high = out
|
|
164
|
+
return BoxStats(count, mean, q1, median, q3, low, high, outliers)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
_SPLIT = 134217729.0 # 2^27 + 1
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _two_product(a: float, b: float) -> tuple[float, float]:
|
|
171
|
+
"""``(p, e)`` with ``p + e == a * b`` exactly (Dekker; ``e`` 0 when not finite)."""
|
|
172
|
+
p = a * b
|
|
173
|
+
c = _SPLIT * a
|
|
174
|
+
ah = c - (c - a)
|
|
175
|
+
al = a - ah
|
|
176
|
+
c = _SPLIT * b
|
|
177
|
+
bh = c - (c - b)
|
|
178
|
+
bl = b - bh
|
|
179
|
+
e = ((ah * bh - p) + ah * bl + al * bh) + al * bl
|
|
180
|
+
return p, (e if math.isfinite(e) else 0.0)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _weighted_box_stats(values: Any, weights: Any, mode: int, a: float, b: float) -> BoxStats:
|
|
184
|
+
if mode == 0 and not a >= 0:
|
|
185
|
+
raise ValueError("whiskers must be at least 0 IQR")
|
|
186
|
+
if mode == 2 and not 0 <= a <= b <= 100:
|
|
187
|
+
raise ValueError("whisker percentiles 0 <= low <= high <= 100")
|
|
188
|
+
v = doubles(values)
|
|
189
|
+
w = _weights(weights, len(v))
|
|
190
|
+
assert w is not None
|
|
191
|
+
xs, cw = _weighted_sorted(v, w)
|
|
192
|
+
nan = math.nan
|
|
193
|
+
if not xs:
|
|
194
|
+
return BoxStats(0, nan, nan, nan, nan, nan, nan, array.array("d"))
|
|
195
|
+
total = cw[-1]
|
|
196
|
+
parts: list[float] = [] # w x exactly, as two doubles each
|
|
197
|
+
for x, wi in zip(v, w):
|
|
198
|
+
if wi > 0 and math.isfinite(x):
|
|
199
|
+
parts += _two_product(wi, x)
|
|
200
|
+
mean = math.fsum(parts) / total
|
|
201
|
+
q1, median, q3 = (_weighted_quantile(xs, cw, q) for q in (0.25, 0.5, 0.75))
|
|
202
|
+
if mode == 1:
|
|
203
|
+
low, high = xs[0], xs[-1]
|
|
204
|
+
elif mode == 2:
|
|
205
|
+
low, high = _weighted_quantile(xs, cw, a / 100.0), _weighted_quantile(xs, cw, b / 100.0)
|
|
206
|
+
else:
|
|
207
|
+
iqr = q3 - q1
|
|
208
|
+
lo_fence, hi_fence = q1 - a * iqr, q3 + a * iqr
|
|
209
|
+
i = min(bisect.bisect_left(xs, lo_fence), len(xs) - 1)
|
|
210
|
+
j = max(bisect.bisect_right(xs, hi_fence) - 1, 0)
|
|
211
|
+
low, high = min(xs[i], q1), max(xs[j], q3)
|
|
212
|
+
outliers = array.array("d", (x for x, wi in zip(v, w)
|
|
213
|
+
if wi > 0 and math.isfinite(x) and (x < low or x > high)))
|
|
214
|
+
count = int(total) if total.is_integer() else total
|
|
215
|
+
return BoxStats(count, mean, q1, median, q3, low, high, outliers)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _weights(weights: Any, n: int) -> array.array | None:
|
|
219
|
+
if weights is None:
|
|
220
|
+
return None
|
|
221
|
+
w = doubles(weights)
|
|
222
|
+
if len(w) != n:
|
|
223
|
+
raise ValueError(f"{len(w)} weights for {n} values")
|
|
224
|
+
if n:
|
|
225
|
+
total = sum(w)
|
|
226
|
+
if not math.isfinite(total) or min(w) < 0 or total <= 0:
|
|
227
|
+
raise ValueError("weights must be finite and non-negative, with a positive sum")
|
|
228
|
+
return w
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _edges(edges: Any) -> array.array:
|
|
232
|
+
e = doubles(edges)
|
|
233
|
+
if len(e) < 2:
|
|
234
|
+
raise ValueError("at least 2 bin edges are needed")
|
|
235
|
+
return e
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def histogram(values: Any, edges: Any, weights: Any = None) -> array.array:
|
|
239
|
+
"""Counts (sums of ``weights``) of the values in each bin between increasing ``edges``:
|
|
240
|
+
``edges[k] <= v < edges[k + 1]``, the last bin closed, as numpy's ``histogram``. NaN
|
|
241
|
+
values and values outside the edges are not counted."""
|
|
242
|
+
v = doubles(values)
|
|
243
|
+
return _k.histogram(v, _edges(edges), _weights(weights, len(v)))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
_RULES = ("auto", "fd", "sturges", "scott", "sqrt", "rice", "doane")
|
|
247
|
+
|
|
248
|
+
#: The most bins :func:`bin_edges` makes.
|
|
249
|
+
MAX_BINS = 1_000_000
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def bin_edges(values: Any, rule: Any = "auto", range: Any = None) -> array.array:
|
|
253
|
+
"""Even bin edges for ``values``: ``rule`` is a count of bins or one of numpy's rules
|
|
254
|
+
(``auto``, ``fd``, ``sturges``, ``scott``, ``sqrt``, ``rice``, ``doane``), over
|
|
255
|
+
``range`` (default: the finite values' span). The rules give numpy's results; when every
|
|
256
|
+
finite value is a whole number, a rule's bin width is at least 1. Values outside ``range`` are left out of the rule; NaN values are ignored. A
|
|
257
|
+
rule asking for more than :data:`MAX_BINS` bins raises ``ValueError``, and so does a
|
|
258
|
+
count of bins narrower than the floats of the span can tell apart; a span that is not
|
|
259
|
+
finite gives one bin. Constant values span 0.5 on each side (more where floats are
|
|
260
|
+
coarser)."""
|
|
261
|
+
count_given = isinstance(rule, int) and not isinstance(rule, bool)
|
|
262
|
+
if count_given:
|
|
263
|
+
if not 1 <= rule <= MAX_BINS:
|
|
264
|
+
raise ValueError(f"bins must be between 1 and {MAX_BINS}, not {rule}")
|
|
265
|
+
elif rule not in _RULES:
|
|
266
|
+
raise ValueError(f"unknown bin rule {rule!r} (use {', '.join(_RULES)})")
|
|
267
|
+
v = doubles(values)
|
|
268
|
+
if range is not None:
|
|
269
|
+
first, last = float(range[0]), float(range[1])
|
|
270
|
+
if not (math.isfinite(first) and math.isfinite(last) and first < last):
|
|
271
|
+
raise ValueError(f"range must be finite (low, high) with low < high, not "
|
|
272
|
+
f"{tuple(range)}")
|
|
273
|
+
if not count_given:
|
|
274
|
+
v = scaled(v, 1.0, 0.0, None, first, last, math.nan, math.nan)
|
|
275
|
+
count, lo, hi = _k.extent(v, None)
|
|
276
|
+
if range is None:
|
|
277
|
+
if not count:
|
|
278
|
+
first, last = 0.0, 1.0
|
|
279
|
+
else:
|
|
280
|
+
first, last = lo, hi
|
|
281
|
+
constant = first == last
|
|
282
|
+
if constant: # wide enough for its bins to increase
|
|
283
|
+
half = max(0.5, (rule if count_given else 1) * 2.0 * math.ulp(first))
|
|
284
|
+
first, last = first - half, last + half
|
|
285
|
+
span = last - first
|
|
286
|
+
if not math.isfinite(span):
|
|
287
|
+
return array.array("d", [first, last])
|
|
288
|
+
n_bins = 1
|
|
289
|
+
if count_given:
|
|
290
|
+
n_bins = rule
|
|
291
|
+
elif count and not constant:
|
|
292
|
+
width = _bin_width(v, rule, count, hi - lo)
|
|
293
|
+
if width < 1 and all(map(float.is_integer, filter(math.isfinite, v))):
|
|
294
|
+
width = 1.0 # whole numbers: no bin narrower than 1
|
|
295
|
+
if width: # no bin narrower than the floats there
|
|
296
|
+
width = max(width, 4.0 * math.ulp(max(-first, last)))
|
|
297
|
+
if width and math.isfinite(width):
|
|
298
|
+
bins = math.ceil(span / width)
|
|
299
|
+
if bins > MAX_BINS:
|
|
300
|
+
raise ValueError(f"bins={rule!r} asks for {bins:.3g} bins (at most "
|
|
301
|
+
f"{MAX_BINS}); give a count or another rule")
|
|
302
|
+
n_bins = max(1, int(bins))
|
|
303
|
+
step = span / n_bins
|
|
304
|
+
edges = array.array("d", [first + i * step for i in _range(n_bins + 1)])
|
|
305
|
+
edges[-1] = last
|
|
306
|
+
if step < 4.0 * math.ulp(max(-first, last)) and any(
|
|
307
|
+
a >= b for a, b in zip(edges, edges[1:])):
|
|
308
|
+
raise ValueError(f"{n_bins} bins over the range {first!r} to {last!r} are "
|
|
309
|
+
f"narrower than its floats can tell apart; give fewer bins")
|
|
310
|
+
return edges
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
_range = range
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _bin_width(v: array.array, rule: str, n: int, ptp: float) -> float:
|
|
317
|
+
if rule == "sqrt":
|
|
318
|
+
return ptp / math.sqrt(n)
|
|
319
|
+
if rule == "sturges":
|
|
320
|
+
return ptp / (math.log2(n) + 1.0)
|
|
321
|
+
if rule == "rice":
|
|
322
|
+
return ptp / (2.0 * n ** (1.0 / 3))
|
|
323
|
+
if rule in ("scott", "doane"):
|
|
324
|
+
unit = 1.0
|
|
325
|
+
if ptp and not 2.0 ** -256 < ptp < 2.0 ** 256: # moments in a unit of about ptp
|
|
326
|
+
unit = math.ldexp(1.0, math.frexp(ptp)[1]) # (a power of 2: exact scaling)
|
|
327
|
+
v = scaled(v, 1.0 / unit)
|
|
328
|
+
_, sw, _, mean, m2, m3 = _k.moments(v, None)
|
|
329
|
+
sigma = math.sqrt(m2 / sw) if sw else 0.0
|
|
330
|
+
if rule == "scott":
|
|
331
|
+
return (24.0 * math.pi ** 0.5 / n) ** (1.0 / 3.0) * sigma * unit
|
|
332
|
+
if n > 2 and sigma > 0:
|
|
333
|
+
sg1 = math.sqrt(6.0 * (n - 2) / ((n + 1.0) * (n + 3)))
|
|
334
|
+
g1 = m3 / sw / sigma ** 3
|
|
335
|
+
return ptp / (1.0 + math.log2(n) + math.log2(1.0 + abs(g1) / sg1))
|
|
336
|
+
return 0.0
|
|
337
|
+
q75, q25 = _k.quantiles(v, [0.75, 0.25])
|
|
338
|
+
fd = 2.0 * (q75 - q25) * n ** (-1.0 / 3.0)
|
|
339
|
+
if rule == "fd":
|
|
340
|
+
return fd
|
|
341
|
+
sturges = ptp / (math.log2(n) + 1.0)
|
|
342
|
+
return min(max(fd, ptp / math.sqrt(n) / 2), sturges) # numpy 2: at most ~2 sqrt(n) bins
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def histogram2d(x: Any, y: Any, x_edges: Any, y_edges: Any,
|
|
346
|
+
weights: Any = None) -> tuple[int, int, array.array]:
|
|
347
|
+
"""2-D counts as ``(rows, columns, array('d'))``, rows over the y bins (bins as in
|
|
348
|
+
:func:`histogram`); pairs with a NaN are not counted."""
|
|
349
|
+
xv, yv = doubles(x), doubles(y)
|
|
350
|
+
if len(xv) != len(yv):
|
|
351
|
+
raise ValueError("x and y must have the same length")
|
|
352
|
+
xe, ye = _edges(x_edges), _edges(y_edges)
|
|
353
|
+
out = _k.histogram2d(xv, yv, _weights(weights, len(xv)), xe, ye)
|
|
354
|
+
return len(ye) - 1, len(xe) - 1, out
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def hexbin(x: Any, y: Any, x0: float, y0: float, dx: float, dy: float, nx: int, ny: int,
|
|
358
|
+
weights: Any = None) -> tuple[array.array, array.array, array.array]:
|
|
359
|
+
"""Hexagonal binning: ``(centres_x, centres_y, counts)`` of the non-empty cells.
|
|
360
|
+
|
|
361
|
+
The cells are those of two offset lattices: centres ``(x0 + i dx, y0 + j dy)`` and
|
|
362
|
+
``(x0 + (i + 1/2) dx, y0 + (j + 1/2) dy)`` for ``i < nx``, ``j < ny``; each point goes to
|
|
363
|
+
the nearest centre. With ``dy = sqrt(3) dx`` the cells are regular hexagons of width
|
|
364
|
+
``dx`` (corner to corner ``2 dx / sqrt(3)``), so pass coordinates in page units."""
|
|
365
|
+
xv, yv = doubles(x), doubles(y)
|
|
366
|
+
if len(xv) != len(yv):
|
|
367
|
+
raise ValueError("x and y must have the same length")
|
|
368
|
+
nx, ny = int(nx), int(ny)
|
|
369
|
+
counts = _k.hexbin(xv, yv, _weights(weights, len(xv)), float(x0), float(y0), float(dx),
|
|
370
|
+
float(dy), nx, ny)
|
|
371
|
+
cx, cy, cc = array.array("d"), array.array("d"), array.array("d")
|
|
372
|
+
cells = nx * ny
|
|
373
|
+
for k, c in enumerate(counts):
|
|
374
|
+
if c:
|
|
375
|
+
j, i = divmod(k % cells, nx)
|
|
376
|
+
off = 0.5 if k >= cells else 0.0
|
|
377
|
+
cx.append(x0 + (i + off) * dx)
|
|
378
|
+
cy.append(y0 + (j + off) * dy)
|
|
379
|
+
cc.append(c)
|
|
380
|
+
return cx, cy, cc
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def ecdf(values: Any, weights: Any = None) -> tuple[array.array, array.array]:
|
|
384
|
+
"""``(sorted values, cumulative weights)`` of the finite values (weights default to 1:
|
|
385
|
+
the cumulative counts); divide by the last for proportions."""
|
|
386
|
+
v = doubles(values)
|
|
387
|
+
return _k.ecdf(v, _weights(weights, len(v)))
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def bandwidth(values: Any, rule: str = "scott", weights: Any = None) -> float:
|
|
391
|
+
"""Gaussian kernel bandwidth by Scott's (``n^(-1/5)``) or Silverman's
|
|
392
|
+
(``(3n/4)^(-1/5)``) rule times the unbiased standard deviation, as scipy's
|
|
393
|
+
``gaussian_kde``. Weights count as repeated values: ``n`` is their total. NaN when
|
|
394
|
+
fewer than 2 values (a weight total of at most 1)."""
|
|
395
|
+
if rule not in ("scott", "silverman"):
|
|
396
|
+
raise ValueError(f"unknown bandwidth rule {rule!r} (use 'scott' or 'silverman')")
|
|
397
|
+
v = doubles(values)
|
|
398
|
+
count, sw, _, _, m2, _ = _k.moments(v, _weights(weights, len(v)))
|
|
399
|
+
if count < 2 or not sw > 1:
|
|
400
|
+
return math.nan
|
|
401
|
+
var = m2 / (sw - 1)
|
|
402
|
+
factor = sw ** -0.2 if rule == "scott" else (sw * 0.75) ** -0.2
|
|
403
|
+
return factor * math.sqrt(var)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def kde(values: Any, grid_min: float, grid_max: float, n: int, bandwidth: float,
|
|
407
|
+
weights: Any = None, cumulative: bool = False) -> array.array:
|
|
408
|
+
"""Gaussian kernel density of the finite values at ``n`` points evenly spaced over
|
|
409
|
+
``[grid_min, grid_max]``; it integrates to 1 over the real line. ``cumulative``: the
|
|
410
|
+
integral from minus infinity instead. Each kernel is cut at 5 bandwidths, so the
|
|
411
|
+
density is 0 farther than that from every value. Direct sums when the kernel is narrow
|
|
412
|
+
against the grid spacing or the work is small; otherwise linear binning and convolution
|
|
413
|
+
(relative error below 1e-4 when the bandwidth is at least twice the spacing)."""
|
|
414
|
+
v = doubles(values)
|
|
415
|
+
return _k.kde(v, _weights(weights, len(v)), float(grid_min), float(grid_max), int(n),
|
|
416
|
+
float(bandwidth), cumulative)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
_grid = grid
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _contour_grid(grid: Any, w: int, h: int, xs: Any, ys: Any):
|
|
423
|
+
shape = buffer_shape(grid)
|
|
424
|
+
if shape is not None and len(shape) == 2:
|
|
425
|
+
g = _grid(grid)[3]
|
|
426
|
+
elif not isinstance(grid, array.array) and grid is not None and len(grid) and \
|
|
427
|
+
not isinstance(grid[0], (int, float)) and grid[0] is not None:
|
|
428
|
+
g = array.array("d", [math.nan if v is None else float(v) for row in grid for v in row])
|
|
429
|
+
else:
|
|
430
|
+
g = doubles(grid)
|
|
431
|
+
w, h = int(w), int(h)
|
|
432
|
+
if w < 2 or h < 2 or len(g) != w * h:
|
|
433
|
+
raise ValueError(f"a grid of w x h values is needed (w, h >= 2; got {len(g)} for "
|
|
434
|
+
f"{w} x {h})")
|
|
435
|
+
xv = doubles(xs) if xs is not None else array.array("d", map(float, range(w)))
|
|
436
|
+
yv = doubles(ys) if ys is not None else array.array("d", map(float, range(h)))
|
|
437
|
+
if len(xv) != w or len(yv) != h:
|
|
438
|
+
raise ValueError("xs needs w values and ys h values (the node coordinates)")
|
|
439
|
+
return g, w, h, xv, yv
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def contour_lines(grid: Any, w: int, h: int, xs: Any = None, ys: Any = None,
|
|
443
|
+
level: float = 0.0) -> list[tuple[array.array, array.array, bool]]:
|
|
444
|
+
"""Contour lines of ``level`` on a ``w`` x ``h`` grid (rows over y; a 2-D buffer,
|
|
445
|
+
nested rows or ``w * h`` values) with nodes at ``xs[i]``, ``ys[j]``: a list of
|
|
446
|
+
``(x, y, closed)``.
|
|
447
|
+
|
|
448
|
+
Marching squares, a node being above the level when ``v >= level``; saddles are resolved
|
|
449
|
+
by the cell's mean; cells with a NaN node are skipped (lines stop there)."""
|
|
450
|
+
g, w, h, xv, yv = _contour_grid(grid, w, h, xs, ys)
|
|
451
|
+
pts, starts, closed = _k.contour_lines(g, w, h, xv, yv, float(level))
|
|
452
|
+
lines = []
|
|
453
|
+
for k in range(len(closed)):
|
|
454
|
+
a, b = starts[k], starts[k + 1]
|
|
455
|
+
lines.append((pts[2 * a:2 * b:2], pts[2 * a + 1:2 * b:2], bool(closed[k])))
|
|
456
|
+
return lines
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def contour_bands(grid: Any, w: int, h: int, xs: Any = None, ys: Any = None,
|
|
460
|
+
lo: float = -math.inf,
|
|
461
|
+
hi: float = math.inf) -> tuple[array.array, array.array, array.array]:
|
|
462
|
+
"""The region ``lo <= v < hi`` of a grid (as :func:`contour_lines`) as its outline
|
|
463
|
+
``(xs, ys, ring_starts)`` (``ring_starts`` holds each ring's first index plus the total):
|
|
464
|
+
closed rings with the band on their left, holes included, to fill together in one path
|
|
465
|
+
with the nonzero rule. The rings follow the contour lines of ``lo`` and ``hi`` and the
|
|
466
|
+
border of the data; cells with a NaN node are left out."""
|
|
467
|
+
g, w, h, xv, yv = _contour_grid(grid, w, h, xs, ys)
|
|
468
|
+
pts, starts = _k.contour_bands(g, w, h, xv, yv, float(lo), float(hi))
|
|
469
|
+
return pts[0::2], pts[1::2], starts
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def lowess(x: Any, y: Any, frac: float = 2.0 / 3.0, iterations: int = 3,
|
|
473
|
+
delta: Any = "auto") -> array.array:
|
|
474
|
+
"""LOWESS fit at each x (input order; NaN for pairs with a NaN): local linear fits over
|
|
475
|
+
the ``floor(frac n)`` nearest points (at least 2) of the ``n`` complete pairs, with
|
|
476
|
+
tricube weights as in statsmodels, then ``iterations`` robust passes with bisquare
|
|
477
|
+
weights of the residuals. As statsmodels' ``delta``, points within ``delta`` of the
|
|
478
|
+
last fitted x are interpolated instead of fitted; ``"auto"`` is 0 up to 10,000 pairs
|
|
479
|
+
and 1 % of the x range above."""
|
|
480
|
+
xv, yv = doubles(x), doubles(y)
|
|
481
|
+
if len(xv) != len(yv):
|
|
482
|
+
raise ValueError("x and y must have the same length")
|
|
483
|
+
if delta == "auto":
|
|
484
|
+
finite = [v for v, w in zip(xv, yv) if math.isfinite(v) and math.isfinite(w)]
|
|
485
|
+
delta = 0.01 * (max(finite) - min(finite)) if len(finite) > 10_000 else 0.0
|
|
486
|
+
return _k.lowess(xv, yv, float(frac), int(iterations), float(delta))
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def swarm(positions: Any, size: Any, slot_width: float) -> tuple[array.array, int]:
|
|
490
|
+
"""Swarm layout: offsets across the value axis (input order) for markers of diameter
|
|
491
|
+
``size`` (one value or one per point) centred at ``positions``, so that none overlap,
|
|
492
|
+
and the number of points clamped to half the ``slot_width``. Use page units. NaN
|
|
493
|
+
positions get NaN offsets."""
|
|
494
|
+
p = doubles(positions)
|
|
495
|
+
if isinstance(size, (int, float)):
|
|
496
|
+
sizes, uniform = array.array("d", [float(size)]), True
|
|
497
|
+
else:
|
|
498
|
+
sizes, uniform = doubles(size), False
|
|
499
|
+
if len(sizes) != len(p):
|
|
500
|
+
raise ValueError(f"{len(sizes)} sizes for {len(p)} positions")
|
|
501
|
+
if not len(p):
|
|
502
|
+
return array.array("d"), 0
|
|
503
|
+
if len(p) < 20_000:
|
|
504
|
+
return _k.swarm(p, sizes, uniform, float(slot_width))
|
|
505
|
+
return _interruptible(_k.swarm, p, sizes, uniform, float(slot_width))
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _interruptible(fn: Any, *args: Any) -> Any:
|
|
509
|
+
"""``fn(*args)`` (a native call that can run for seconds) on a thread while this one
|
|
510
|
+
waits, so that Ctrl-C interrupts the wait; the abandoned call ends on its own."""
|
|
511
|
+
import threading
|
|
512
|
+
result: list[Any] = []
|
|
513
|
+
error: list[BaseException] = []
|
|
514
|
+
|
|
515
|
+
def run() -> None:
|
|
516
|
+
try:
|
|
517
|
+
result.append(fn(*args))
|
|
518
|
+
except BaseException as e: # raised again on the waiting thread
|
|
519
|
+
error.append(e)
|
|
520
|
+
|
|
521
|
+
t = threading.Thread(target=run, name="plot_io-native", daemon=True)
|
|
522
|
+
t.start()
|
|
523
|
+
t.join()
|
|
524
|
+
if error:
|
|
525
|
+
raise error[0]
|
|
526
|
+
return result[0]
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _xy_weights(x: Any, y: Any, weights: Any) -> tuple[array.array, array.array,
|
|
530
|
+
array.array | None]:
|
|
531
|
+
xa, ya = doubles(x), doubles(y)
|
|
532
|
+
if len(xa) != len(ya):
|
|
533
|
+
raise ValueError("x and y must have the same length")
|
|
534
|
+
return xa, ya, _weights(weights, len(xa))
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def kde2d_bandwidth(x: Any, y: Any, rule: str = "scott",
|
|
538
|
+
weights: Any = None) -> tuple[float, float, float]:
|
|
539
|
+
"""Kernel covariance ``(h11, h12, h22)`` of a 2-D Gaussian density, as scipy's
|
|
540
|
+
``gaussian_kde``: the unbiased data covariance times the squared factor ``n^(-1/6)``
|
|
541
|
+
(Scott's and Silverman's rules agree in 2-D). Weights count as repeated points: ``n``
|
|
542
|
+
is their total. A singular covariance (collinear points) falls back to an isotropic
|
|
543
|
+
kernel from the larger variance. NaN when fewer than 2 points (a weight total of at
|
|
544
|
+
most 1), or all equal."""
|
|
545
|
+
if rule not in ("scott", "silverman"):
|
|
546
|
+
raise ValueError(f"unknown bandwidth rule {rule!r} (use 'scott' or 'silverman')")
|
|
547
|
+
xa, ya, w = _xy_weights(x, y, weights)
|
|
548
|
+
count, sw, _, _, _, cxx, cyy, cxy = _k.moments2(xa, ya, w)
|
|
549
|
+
nan = (math.nan, math.nan, math.nan)
|
|
550
|
+
if count < 2 or not sw > 1:
|
|
551
|
+
return nan
|
|
552
|
+
d = sw - 1
|
|
553
|
+
vxx, vyy, vxy = cxx / d, cyy / d, cxy / d
|
|
554
|
+
f2 = sw ** (-1.0 / 3.0) # (n^(-1/6))^2
|
|
555
|
+
if not (vxx * vyy - vxy * vxy > 1e-12 * vxx * vyy) or vxx <= 0 or vyy <= 0:
|
|
556
|
+
v = max(vxx, vyy)
|
|
557
|
+
return (v * f2, 0.0, v * f2) if v > 0 else nan
|
|
558
|
+
return vxx * f2, vxy * f2, vyy * f2
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _kde2d_h(bandwidth: Any, x: array.array, y: array.array,
|
|
562
|
+
w: array.array | None) -> tuple[float, float, float]:
|
|
563
|
+
if isinstance(bandwidth, str):
|
|
564
|
+
return kde2d_bandwidth(x, y, bandwidth, w)
|
|
565
|
+
if isinstance(bandwidth, (int, float)):
|
|
566
|
+
s = float(bandwidth)
|
|
567
|
+
return s * s, 0.0, s * s
|
|
568
|
+
h = tuple(float(v) for v in bandwidth)
|
|
569
|
+
if len(h) != 3:
|
|
570
|
+
raise ValueError("bandwidth: a rule name, a standard deviation, or (h11, h12, h22)")
|
|
571
|
+
return h
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
#: Above this many points, ``method="auto"`` evaluates through the lattice.
|
|
575
|
+
KDE2D_EXACT_MAX = 20000
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def kde2d_at(x: Any, y: Any, bandwidth: Any = "scott", method: str = "auto", grid: int = 512,
|
|
579
|
+
weights: Any = None) -> array.array:
|
|
580
|
+
"""Gaussian kernel density of the points ``(x, y)`` at the points themselves, as
|
|
581
|
+
``gaussian_kde(np.array([x, y]))(np.array([x, y]))``.
|
|
582
|
+
|
|
583
|
+
``bandwidth``: a rule (``"scott"``, ``"silverman"``; see :func:`kde2d_bandwidth`), a
|
|
584
|
+
standard deviation (isotropic kernel), or a kernel covariance ``(h11, h12, h22)``.
|
|
585
|
+
``method``: ``"exact"`` (direct sums on the worker threads; terms below ``e^-40`` of
|
|
586
|
+
their weight are left out), ``"grid"`` (a lattice of at least ``grid`` points per side,
|
|
587
|
+
up to 2048 to keep its spacing at most 0.1 kernel widths: relative error about 1e-4,
|
|
588
|
+
at most a few 1e-3) or ``"auto"`` (exact up to 20,000 points). Points with a missing
|
|
589
|
+
coordinate are not sources and get NaN."""
|
|
590
|
+
if method not in ("auto", "exact", "grid"):
|
|
591
|
+
raise ValueError(f"unknown method {method!r} (use 'auto', 'exact' or 'grid')")
|
|
592
|
+
xa, ya, w = _xy_weights(x, y, weights)
|
|
593
|
+
h = _kde2d_h(bandwidth, xa, ya, w)
|
|
594
|
+
if not all(math.isfinite(v) for v in h):
|
|
595
|
+
raise ValueError("kde2d_at: no bandwidth (fewer than 2 distinct points)")
|
|
596
|
+
n = len(xa)
|
|
597
|
+
exact = method == "exact" or (method == "auto" and n <= KDE2D_EXACT_MAX)
|
|
598
|
+
if not exact:
|
|
599
|
+
return _k.kde2d(xa, ya, w, h, xa, ya, int(grid))
|
|
600
|
+
step = 512
|
|
601
|
+
if n <= step:
|
|
602
|
+
return _k.kde2d(xa, ya, w, h, xa, ya)
|
|
603
|
+
parts = threads.map_ordered(
|
|
604
|
+
lambda i: _k.kde2d(xa, ya, w, h, xa[i:i + step], ya[i:i + step]),
|
|
605
|
+
range(0, n, step))
|
|
606
|
+
out = array.array("d")
|
|
607
|
+
for part in parts:
|
|
608
|
+
out.extend(part)
|
|
609
|
+
return out
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def kde2d_grid(x: Any, y: Any, bandwidth: Any, x0: float, x1: float, nx: int, y0: float,
|
|
613
|
+
y1: float, ny: int, weights: Any = None, grid: int = 512) -> array.array:
|
|
614
|
+
"""The density of :func:`kde2d_at` (through the lattice) on a regular ``ny`` x ``nx``
|
|
615
|
+
grid: row ``j`` at ``y0 + j (y1 - y0) / (ny - 1)``, column ``i`` likewise; row-major."""
|
|
616
|
+
xa, ya, w = _xy_weights(x, y, weights)
|
|
617
|
+
h = _kde2d_h(bandwidth, xa, ya, w)
|
|
618
|
+
if not all(math.isfinite(v) for v in h):
|
|
619
|
+
raise ValueError("kde2d_grid: no bandwidth (fewer than 2 distinct points)")
|
|
620
|
+
nx, ny = int(nx), int(ny)
|
|
621
|
+
if nx < 1 or ny < 1:
|
|
622
|
+
raise ValueError("kde2d_grid: nx and ny must be at least 1")
|
|
623
|
+
gx = [x0 + (x1 - x0) * i / (nx - 1) if nx > 1 else x0 for i in range(nx)]
|
|
624
|
+
gy = [y0 + (y1 - y0) * j / (ny - 1) if ny > 1 else y0 for j in range(ny)]
|
|
625
|
+
tx = array.array("d", gx * ny)
|
|
626
|
+
ty = array.array("d", (v for v in gy for _ in range(nx)))
|
|
627
|
+
return _k.kde2d(xa, ya, w, h, tx, ty, int(grid))
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def argsort(values: Any, descending: bool = False) -> array.array:
|
|
631
|
+
"""Indices that sort the values (``array('q')``), stable: ascending with NaN first, or
|
|
632
|
+
descending with NaN last; -0 equals 0."""
|
|
633
|
+
return _k.argsort(doubles(values), bool(descending))
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
_INTEGER_FORMATS = frozenset("bBhHiIlLqQnN")
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _numeric_view(value):
|
|
640
|
+
"""A memoryview of ``value`` when it has a numeric buffer, else ``None``."""
|
|
641
|
+
if buffer_shape(value) is None:
|
|
642
|
+
return None
|
|
643
|
+
return memoryview(value)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _indices(indices: Any) -> array.array:
|
|
647
|
+
"""``indices`` as ``array('q')``; 8-byte integer buffers are read without a copy loop."""
|
|
648
|
+
if isinstance(indices, array.array) and indices.typecode == "q":
|
|
649
|
+
return indices
|
|
650
|
+
mv = _numeric_view(indices)
|
|
651
|
+
if mv is not None:
|
|
652
|
+
if mv.ndim != 1:
|
|
653
|
+
raise ValueError("take: indices must be 1-D")
|
|
654
|
+
fmt = mv.format.lstrip("@=<>!")
|
|
655
|
+
native = mv.format[0] not in "<>!" or (mv.format[0] == "<") == (sys.byteorder == "little")
|
|
656
|
+
if fmt in ("q", "l", "n") and mv.itemsize == 8 and native:
|
|
657
|
+
out = array.array("q")
|
|
658
|
+
out.frombytes(mv.cast("B") if mv.c_contiguous else mv.tobytes())
|
|
659
|
+
return out
|
|
660
|
+
if fmt in _INTEGER_FORMATS:
|
|
661
|
+
return array.array("q", mv.tolist())
|
|
662
|
+
raise TypeError("take: indices must be integers")
|
|
663
|
+
try:
|
|
664
|
+
return array.array("q", indices)
|
|
665
|
+
except TypeError:
|
|
666
|
+
raise TypeError("take: indices must be integers") from None
|
|
667
|
+
except OverflowError:
|
|
668
|
+
raise IndexError("take: index out of range") from None
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def take(values: Any, indices: Any) -> array.array:
|
|
672
|
+
"""``values[indices]`` as ``array('d')``, gathered in C.
|
|
673
|
+
|
|
674
|
+
``values`` is anything :func:`doubles` accepts, or an ``(n, k)`` buffer of rows (e.g.
|
|
675
|
+
RGBA colors per point), in which case the result holds ``len(indices)`` rows of ``k``,
|
|
676
|
+
flat and row-major. ``indices`` are integers in ``[0, n)`` (``array('q')`` from
|
|
677
|
+
:func:`argsort`, numpy integer arrays, lists); others raise ``IndexError``."""
|
|
678
|
+
idx = _indices(indices)
|
|
679
|
+
mv = _numeric_view(values)
|
|
680
|
+
if mv is not None and mv.ndim in (1, 2):
|
|
681
|
+
k = mv.shape[1] if mv.ndim == 2 else 1
|
|
682
|
+
if k < 1:
|
|
683
|
+
raise ValueError("take: rows of at least one value")
|
|
684
|
+
native = mv.format[0] not in "<>!" or (mv.format[0] == "<") == (sys.byteorder == "little")
|
|
685
|
+
if (mv.format.lstrip("@=<>!") == "d" and native and mv.c_contiguous
|
|
686
|
+
and not mv.readonly and mv.nbytes):
|
|
687
|
+
# read in place: no copy of large inputs
|
|
688
|
+
holder = (ctypes.c_char * mv.nbytes).from_buffer(mv)
|
|
689
|
+
return _k.take_rows(None, k, idx, ctypes.addressof(holder), mv.shape[0])
|
|
690
|
+
flat = doubles(values) if mv.ndim == 1 else _grid(values)[3]
|
|
691
|
+
return _k.take_rows(flat, k, idx)
|
|
692
|
+
return _k.take_rows(doubles(values), 1, idx)
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
#: Methods and metrics of :func:`linkage`.
|
|
696
|
+
LINKAGE_METHODS = tuple(_k.LINKAGE_METHODS)
|
|
697
|
+
LINKAGE_METRICS = tuple(_k.LINKAGE_METRICS)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def linkage(data: Any, method: str = "average",
|
|
701
|
+
metric: str = "euclidean") -> tuple[int, array.array]:
|
|
702
|
+
"""``(n, merges)``: scipy's ``linkage`` of the rows of ``data`` (a 2-D buffer or a
|
|
703
|
+
sequence of equal-length rows), or of a condensed distance vector (``metric``
|
|
704
|
+
"precomputed": the ``n(n - 1)/2`` distances ``d(i, j)``, ``i < j``, in row order).
|
|
705
|
+
``merges`` holds ``n - 1`` rows of ``(left, right, height, size)``, flat: row ``i`` makes
|
|
706
|
+
cluster ``n + i``. Methods: :data:`LINKAGE_METHODS`; metrics: :data:`LINKAGE_METRICS`
|
|
707
|
+
(ward, centroid and median take euclidean or precomputed distances). Equal to scipy's
|
|
708
|
+
on input without equal distances; among equal distances the first found merges (scipy's
|
|
709
|
+
loops, lower indices first), the same on every platform. At most 16,384 observations."""
|
|
710
|
+
if method not in _k.LINKAGE_METHODS:
|
|
711
|
+
raise ValueError(f"linkage: unknown method {method!r} (use "
|
|
712
|
+
f"{', '.join(map(repr, LINKAGE_METHODS))})")
|
|
713
|
+
if metric not in _k.LINKAGE_METRICS:
|
|
714
|
+
raise ValueError(f"linkage: unknown metric {metric!r} (use "
|
|
715
|
+
f"{', '.join(map(repr, LINKAGE_METRICS))})")
|
|
716
|
+
if metric == "precomputed":
|
|
717
|
+
flat = doubles(data)
|
|
718
|
+
m = len(flat)
|
|
719
|
+
n = (1 + math.isqrt(1 + 8 * m)) // 2
|
|
720
|
+
if n * (n - 1) // 2 != m:
|
|
721
|
+
raise ValueError(f"linkage: {m} distances is not n(n - 1)/2 for any n")
|
|
722
|
+
d = 0
|
|
723
|
+
else:
|
|
724
|
+
g = _grid(data)
|
|
725
|
+
if g is not None:
|
|
726
|
+
if g[2] != 1:
|
|
727
|
+
raise ValueError("linkage: data must be 2-D (observations x values)")
|
|
728
|
+
n, d, flat = g[0], g[1], g[3]
|
|
729
|
+
else:
|
|
730
|
+
rs = [doubles(r) for r in data]
|
|
731
|
+
n = len(rs)
|
|
732
|
+
d = len(rs[0]) if rs else 0
|
|
733
|
+
if any(len(r) != d for r in rs):
|
|
734
|
+
raise ValueError("linkage: rows of different lengths")
|
|
735
|
+
flat = array.array("d")
|
|
736
|
+
for r in rs:
|
|
737
|
+
flat.extend(r)
|
|
738
|
+
if n > 1 and d < 1:
|
|
739
|
+
raise ValueError("linkage: rows of at least one value")
|
|
740
|
+
return n, _k.linkage(flat, n, max(d, 1), method, metric)
|