plot-io 0.12.0__py3-none-win_arm64.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-arm64.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/helpers/tree.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Hierarchical clustering: :func:`linkage` (scipy's algorithms, in plot_io's C library) and
|
|
2
|
+
:class:`Tree`, its merges with the leaf order and flat clusters."""
|
|
3
|
+
|
|
4
|
+
import array
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .. import kernels
|
|
8
|
+
|
|
9
|
+
__all__ = ["Tree", "linkage"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Tree:
|
|
13
|
+
"""A binary tree of ``n`` observations as ``n - 1`` merges in scipy's layout: row ``i``
|
|
14
|
+
joins clusters ``left`` and ``right`` (observations ``0 … n - 1``, merges ``n + j``) at
|
|
15
|
+
``height`` into a cluster of ``size`` observations. ``merges`` is a scipy ``Z`` or any
|
|
16
|
+
table of such rows; ``labels`` names the observations.
|
|
17
|
+
|
|
18
|
+
``merges`` is ``n − 1`` rows ``(left, right, height, size)`` as ``array('d')``; ``n``;
|
|
19
|
+
``labels`` (``None`` when not given); ``leaves``, the observations in drawing order
|
|
20
|
+
(scipy's ``leaves_list``); ``cut(k=None, height=None)``, the flat cluster of each
|
|
21
|
+
observation, numbered from 1, as scipy's ``fcluster`` with ``maxclust`` (at most ``k``
|
|
22
|
+
clusters) or ``distance`` (merges up to ``height``).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__slots__ = ("merges", "n", "labels", "_leaves")
|
|
26
|
+
|
|
27
|
+
def __init__(self, merges: Any, labels: Any = None) -> None:
|
|
28
|
+
g = kernels.grid(merges)
|
|
29
|
+
if g is not None:
|
|
30
|
+
if g[1] != 4 or g[2] != 1:
|
|
31
|
+
raise ValueError("Tree: merges must have 4 columns")
|
|
32
|
+
rows = [g[3][4 * i:4 * i + 4] for i in range(g[0])]
|
|
33
|
+
else:
|
|
34
|
+
rows = [kernels.doubles(r) for r in merges]
|
|
35
|
+
n = len(rows) + 1
|
|
36
|
+
for i, r in enumerate(rows):
|
|
37
|
+
if len(r) != 4:
|
|
38
|
+
raise ValueError("Tree: merges must have 4 columns")
|
|
39
|
+
a, b = r[0], r[1]
|
|
40
|
+
if not (a == int(a) and b == int(b) and 0 <= a < n + i and 0 <= b < n + i
|
|
41
|
+
and a != b):
|
|
42
|
+
raise ValueError(f"Tree: merge {i} joins {a!r} and {b!r}, not two earlier "
|
|
43
|
+
"clusters")
|
|
44
|
+
if labels is not None:
|
|
45
|
+
labels = list(labels)
|
|
46
|
+
if len(labels) != n:
|
|
47
|
+
raise ValueError(f"Tree: {len(labels)} labels for {n} observations")
|
|
48
|
+
#: the merges: rows of ``(left, right, height, size)`` (``array('d')``)
|
|
49
|
+
self.merges = rows
|
|
50
|
+
#: the number of observations
|
|
51
|
+
self.n = n
|
|
52
|
+
#: the names of the observations (``None``: none given)
|
|
53
|
+
self.labels = labels
|
|
54
|
+
self._leaves: list[int] | None = None
|
|
55
|
+
|
|
56
|
+
def __repr__(self) -> str:
|
|
57
|
+
return f"<Tree of {self.n} observations>"
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def leaves(self) -> list[int]:
|
|
61
|
+
"""The observations in the order a dendrogram draws them (scipy's
|
|
62
|
+
``leaves_list``: each merge's left cluster first)."""
|
|
63
|
+
if self._leaves is None:
|
|
64
|
+
n, out = self.n, []
|
|
65
|
+
stack = [2 * n - 2] if n > 1 else [0]
|
|
66
|
+
while stack:
|
|
67
|
+
k = stack.pop()
|
|
68
|
+
if k < n:
|
|
69
|
+
out.append(k)
|
|
70
|
+
else:
|
|
71
|
+
r = self.merges[k - n]
|
|
72
|
+
stack += [int(r[1]), int(r[0])]
|
|
73
|
+
self._leaves = out
|
|
74
|
+
return list(self._leaves)
|
|
75
|
+
|
|
76
|
+
def _max_heights(self) -> list[float]:
|
|
77
|
+
"""The largest height in each merge's subtree."""
|
|
78
|
+
n = self.n
|
|
79
|
+
out: list[float] = []
|
|
80
|
+
for r in self.merges:
|
|
81
|
+
h = r[2]
|
|
82
|
+
for c in (int(r[0]), int(r[1])):
|
|
83
|
+
if c >= n and out[c - n] > h:
|
|
84
|
+
h = out[c - n]
|
|
85
|
+
out.append(h)
|
|
86
|
+
return out
|
|
87
|
+
|
|
88
|
+
def cut(self, k: int | None = None, height: float | None = None) -> list[int]:
|
|
89
|
+
"""Flat clusters, numbered from 1, of each observation (scipy's ``fcluster``): at
|
|
90
|
+
most ``k`` clusters (``criterion="maxclust"``), or the subtrees whose merges are all
|
|
91
|
+
at most ``height`` (``criterion="distance"``)."""
|
|
92
|
+
if (k is None) == (height is None):
|
|
93
|
+
raise ValueError("cut: give k or height")
|
|
94
|
+
n = self.n
|
|
95
|
+
if n < 2:
|
|
96
|
+
return [1] * n
|
|
97
|
+
md = self._max_heights()
|
|
98
|
+
if k is not None:
|
|
99
|
+
if int(k) != k or k < 1:
|
|
100
|
+
raise ValueError(f"cut: k must be a positive integer, not {k!r}")
|
|
101
|
+
ts = [float("-inf")] + sorted(md) # the count falls as the threshold rises
|
|
102
|
+
lo, hi = 0, len(ts) - 1
|
|
103
|
+
while lo < hi:
|
|
104
|
+
mid = (lo + hi) // 2
|
|
105
|
+
if self._count(md, ts[mid]) <= k:
|
|
106
|
+
hi = mid
|
|
107
|
+
else:
|
|
108
|
+
lo = mid + 1
|
|
109
|
+
return self._flat(md, ts[lo])
|
|
110
|
+
return self._flat(md, float(height)) # type: ignore[arg-type]
|
|
111
|
+
|
|
112
|
+
def _count(self, md: list[float], t: float) -> int:
|
|
113
|
+
"""The number of flat clusters at threshold ``t``."""
|
|
114
|
+
n = self.n
|
|
115
|
+
count, stack = 0, [2 * n - 2]
|
|
116
|
+
while stack:
|
|
117
|
+
c = stack.pop()
|
|
118
|
+
if c < n:
|
|
119
|
+
count += 1
|
|
120
|
+
elif md[c - n] <= t:
|
|
121
|
+
count += 1
|
|
122
|
+
else:
|
|
123
|
+
r = self.merges[c - n]
|
|
124
|
+
stack += [int(r[0]), int(r[1])]
|
|
125
|
+
return count
|
|
126
|
+
|
|
127
|
+
def _flat(self, md: list[float], t: float) -> list[int]:
|
|
128
|
+
"""scipy's ``cluster_monocrit``: the labels in its traversal order."""
|
|
129
|
+
n, merges = self.n, self.merges
|
|
130
|
+
out = [0] * n
|
|
131
|
+
visited: set[int] = set()
|
|
132
|
+
nodes, leader, count = [2 * n - 2], -1, 0
|
|
133
|
+
while nodes:
|
|
134
|
+
root = nodes[-1] - n
|
|
135
|
+
lc, rc = int(merges[root][0]), int(merges[root][1])
|
|
136
|
+
if leader == -1 and md[root] <= t:
|
|
137
|
+
leader = root
|
|
138
|
+
count += 1
|
|
139
|
+
if lc >= n and lc not in visited:
|
|
140
|
+
visited.add(lc)
|
|
141
|
+
nodes.append(lc)
|
|
142
|
+
continue
|
|
143
|
+
if rc >= n and rc not in visited:
|
|
144
|
+
visited.add(rc)
|
|
145
|
+
nodes.append(rc)
|
|
146
|
+
continue
|
|
147
|
+
for c in (lc, rc):
|
|
148
|
+
if c < n:
|
|
149
|
+
if leader == -1:
|
|
150
|
+
count += 1
|
|
151
|
+
out[c] = count
|
|
152
|
+
if leader == root:
|
|
153
|
+
leader = -1
|
|
154
|
+
nodes.pop()
|
|
155
|
+
return out
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _frame(data: Any) -> tuple[Any, Any, Any]:
|
|
159
|
+
"""A data frame's values with its row and column names (pandas, polars), else
|
|
160
|
+
``data``."""
|
|
161
|
+
if hasattr(data, "to_numpy") and hasattr(data, "columns"):
|
|
162
|
+
index = getattr(data, "index", None)
|
|
163
|
+
rows = list(index) if index is not None and hasattr(index, "__iter__") else None
|
|
164
|
+
return data.to_numpy(dtype=float), rows, list(data.columns)
|
|
165
|
+
return data, None, None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def linkage(data: Any, method: str = "average", metric: str = "euclidean",
|
|
169
|
+
axis: str = "rows", labels: Any = None) -> Tree:
|
|
170
|
+
"""The :class:`Tree` of a hierarchical clustering of the rows (``axis="cols"``: the
|
|
171
|
+
columns) of ``data``, a 2-D buffer, a sequence of rows or a data frame (its row or
|
|
172
|
+
column names the labels); ``metric="precomputed"``: ``data`` is the condensed distance
|
|
173
|
+
vector. Methods: ``single``, ``complete``, ``average``, ``weighted``, ``ward``,
|
|
174
|
+
``centroid``, ``median``; metrics: ``euclidean``, ``sqeuclidean``, ``cityblock``,
|
|
175
|
+
``chebyshev``, ``cosine``, ``correlation``, ``precomputed`` (ward, centroid and median:
|
|
176
|
+
euclidean or precomputed). The merges equal scipy's ``linkage`` on data without equal
|
|
177
|
+
distances; equal distances merge in a fixed order. At most 16,384 observations.
|
|
178
|
+
|
|
179
|
+
The algorithms are scipy's, in plot_io's C library; ``data`` with ``precomputed`` is
|
|
180
|
+
the condensed distance vector as scipy's ``pdist`` gives. Equal distances merge in a
|
|
181
|
+
fixed order, the same on every run and platform but not always scipy's. NaN or
|
|
182
|
+
infinite values, a constant row with ``correlation``, a zero row with ``cosine`` and
|
|
183
|
+
more than 16,384 observations raise. The :class:`Tree` gives ``merges``, ``n``,
|
|
184
|
+
``labels``, ``leaves`` and ``cut``; :class:`Dendrogram` draws it.
|
|
185
|
+
|
|
186
|
+
Example::
|
|
187
|
+
|
|
188
|
+
rng = random.Random(1)
|
|
189
|
+
rows = [[rng.gauss(c, 1) for _ in range(4)] for c in (0, 0, 0, 4, 4, 8)]
|
|
190
|
+
tree = plt.linkage(rows, method="average")
|
|
191
|
+
clusters = tree.cut(k=3)
|
|
192
|
+
"""
|
|
193
|
+
if axis not in ("rows", "cols"):
|
|
194
|
+
raise ValueError(f"linkage: axis is 'rows' or 'cols', not {axis!r}")
|
|
195
|
+
values, row_names, col_names = _frame(data)
|
|
196
|
+
if labels is None:
|
|
197
|
+
labels = col_names if axis == "cols" else row_names
|
|
198
|
+
if axis == "cols" and metric != "precomputed":
|
|
199
|
+
g = kernels.grid(values)
|
|
200
|
+
if g is not None:
|
|
201
|
+
h, w, flat = g[0], g[1], g[3]
|
|
202
|
+
else:
|
|
203
|
+
rs = [kernels.doubles(r) for r in values]
|
|
204
|
+
h, w = len(rs), len(rs[0]) if rs else 0
|
|
205
|
+
flat = array.array("d")
|
|
206
|
+
for r in rs:
|
|
207
|
+
if len(r) != w:
|
|
208
|
+
raise ValueError("linkage: rows of different lengths")
|
|
209
|
+
flat.extend(r)
|
|
210
|
+
values = [flat[j::w] for j in range(w)] if h else []
|
|
211
|
+
n, z = kernels.linkage(values, method, metric)
|
|
212
|
+
if n < 1:
|
|
213
|
+
raise ValueError("linkage: no observations")
|
|
214
|
+
return Tree([z[4 * i:4 * i + 4] for i in range(n - 1)], labels)
|