binference 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.
- binference/__init__.py +85 -0
- binference/_kernels.py +252 -0
- binference/_quadrature.py +202 -0
- binference/_simplex.py +54 -0
- binference/_utils.py +139 -0
- binference/_version.py +1 -0
- binference/basis.py +314 -0
- binference/bins.py +645 -0
- binference/density.py +323 -0
- binference/diagnostics.py +400 -0
- binference/estimator.py +197 -0
- binference/inference.py +368 -0
- binference/io.py +309 -0
- binference/likelihoods.py +1684 -0
- binference/measure.py +246 -0
- binference/model.py +833 -0
- binference/plotting.py +538 -0
- binference/priors/__init__.py +47 -0
- binference/priors/base.py +248 -0
- binference/priors/dirichlet.py +640 -0
- binference/priors/hyper.py +240 -0
- binference/priors/logistic.py +676 -0
- binference/priors/rates.py +139 -0
- binference/response.py +539 -0
- binference/results.py +2271 -0
- binference/samplers/__init__.py +19 -0
- binference/samplers/_curvature.py +67 -0
- binference/samplers/_nuts_core.py +284 -0
- binference/samplers/base.py +118 -0
- binference/samplers/bootstrap.py +136 -0
- binference/samplers/gibbs.py +287 -0
- binference/samplers/laplace.py +200 -0
- binference/samplers/nuts.py +681 -0
- binference/samplers/optimize.py +488 -0
- binference/samplers/vb.py +118 -0
- binference/selection.py +621 -0
- binference/simulate.py +720 -0
- binference/validation.py +477 -0
- binference/warnings.py +48 -0
- binference-0.1.0.dist-info/METADATA +130 -0
- binference-0.1.0.dist-info/RECORD +43 -0
- binference-0.1.0.dist-info/WHEEL +4 -0
- binference-0.1.0.dist-info/licenses/LICENSE +21 -0
binference/plotting.py
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
"""Plotting helpers (require matplotlib: ``pip install "binference[plot]"``).
|
|
2
|
+
|
|
3
|
+
Colours follow the active matplotlib style. The population (medians, bands, bars,
|
|
4
|
+
histograms) takes the first colour of ``axes.prop_cycle``, or ``color``; what the objects'
|
|
5
|
+
own data say (the stack, an object's own likelihood) takes the second, or
|
|
6
|
+
``sample_color``; reference lines, error bars and the truth (dashed) take ``text.color``.
|
|
7
|
+
2-D medians ramp from a pale tint of the population colour to the colour, and relative
|
|
8
|
+
uncertainties likewise in ``text.color``; masked bins show the axes background.
|
|
9
|
+
``SAMPLE_COLOR`` is the coral the binference docs give single objects, for ``sample_color``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
from ._utils import warn
|
|
20
|
+
from .warnings import InputWarning
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"SAMPLE_COLOR",
|
|
24
|
+
"plot_corner",
|
|
25
|
+
"plot_individual",
|
|
26
|
+
"plot_population",
|
|
27
|
+
"plot_sbc",
|
|
28
|
+
"plot_trace",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
#: The coral of single objects and samples in the binference docs (their brand ``sample``
|
|
32
|
+
#: colour), to pass as ``sample_color``; ``docs/_scripts/brand.py --check`` keeps the two equal.
|
|
33
|
+
SAMPLE_COLOR = "#EC5F3E"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _style_color(name: str, value: Any, slot: int):
|
|
37
|
+
"""``value`` as RGBA (a list of them for per-bar colours), or colour ``slot`` of the
|
|
38
|
+
style's cycle when None.
|
|
39
|
+
|
|
40
|
+
Resolved now, not left as "C0": a figure drawn after leaving a style context would
|
|
41
|
+
otherwise colour its lines from another cycle than its bars and bands.
|
|
42
|
+
"""
|
|
43
|
+
from matplotlib.colors import is_color_like, to_rgba
|
|
44
|
+
|
|
45
|
+
if value is None:
|
|
46
|
+
value = f"C{slot}"
|
|
47
|
+
if is_color_like(value):
|
|
48
|
+
return to_rgba(value)
|
|
49
|
+
if isinstance(value, list | tuple) and value and all(is_color_like(c) for c in value):
|
|
50
|
+
return [to_rgba(c) for c in value]
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"{name} must be a matplotlib colour, a list of them, or None, got {value!r}."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _ramp(ax, color):
|
|
57
|
+
"""A sequential colormap from a pale tint of ``color`` (over the axes background, so
|
|
58
|
+
zero differs from a masked bin) to ``color``."""
|
|
59
|
+
from matplotlib.colors import LinearSegmentedColormap, to_rgba
|
|
60
|
+
|
|
61
|
+
ground, ink = np.array(to_rgba(ax.get_facecolor())), np.array(to_rgba(color))
|
|
62
|
+
return LinearSegmentedColormap.from_list("ramp", [0.85 * ground + 0.15 * ink, ink])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _tidy(ax) -> None:
|
|
66
|
+
"""Recessive axes: no top/right spines."""
|
|
67
|
+
ax.spines["top"].set_visible(False)
|
|
68
|
+
ax.spines["right"].set_visible(False)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _plt():
|
|
72
|
+
try:
|
|
73
|
+
import matplotlib.pyplot as plt
|
|
74
|
+
except ImportError as err: # pragma: no cover - depends on environment
|
|
75
|
+
raise ImportError("Plotting needs matplotlib: pip install 'binference[plot]'.") from err
|
|
76
|
+
return plt
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
#: axis labels of what can be plotted (``which``)
|
|
80
|
+
_YLABEL = {
|
|
81
|
+
"density": "density",
|
|
82
|
+
"fractions": "fraction per bin",
|
|
83
|
+
"rates": "rate",
|
|
84
|
+
"selected": "selected fraction per bin",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _overlay(result: Any, values: Any, which: str, total: float) -> np.ndarray:
|
|
89
|
+
"""Overlay values (fractions or rates) on the scale of the plotted posterior.
|
|
90
|
+
|
|
91
|
+
Overlays are normalised and multiplied by ``total``, the posterior mean total
|
|
92
|
+
when rates are plotted or the model is a rate model (1 otherwise), so only
|
|
93
|
+
their shape is compared; the selected population of an overlay is taken under
|
|
94
|
+
the fit's selection.
|
|
95
|
+
"""
|
|
96
|
+
v = np.asarray(values, dtype=float)
|
|
97
|
+
v = v / v.sum()
|
|
98
|
+
if which == "selected":
|
|
99
|
+
return result._selected_of(v[None, :]).mean(axis=0)
|
|
100
|
+
if which == "fractions":
|
|
101
|
+
return v
|
|
102
|
+
v = v * total
|
|
103
|
+
return v / result.bins.volumes if which == "density" else v
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def plot_population(
|
|
107
|
+
result: Any,
|
|
108
|
+
ax: Any = None,
|
|
109
|
+
*,
|
|
110
|
+
which: str = "density",
|
|
111
|
+
levels: Sequence[float] = (0.68, 0.95),
|
|
112
|
+
truth: Any = None,
|
|
113
|
+
stack: Any = None,
|
|
114
|
+
color: Any = None,
|
|
115
|
+
sample_color: Any = None,
|
|
116
|
+
label: str | None = None,
|
|
117
|
+
alpha: float = 0.25,
|
|
118
|
+
step: bool = True,
|
|
119
|
+
):
|
|
120
|
+
"""Plot a population posterior: bands for 1-D bins, images for 2-D grids, bars for categories.
|
|
121
|
+
|
|
122
|
+
Overlays (``truth``, ``stack``) contribute only their shape: for rates (and rate
|
|
123
|
+
models) they are scaled to the posterior mean total; for the selected population
|
|
124
|
+
they are reweighted by the fit's selection (bins without a basis). 2-D grids show
|
|
125
|
+
the median and the relative uncertainty as images (an array of two axes) and take
|
|
126
|
+
no overlays.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
result : PopulationResult or PointEstimate
|
|
131
|
+
ax : matplotlib Axes, optional
|
|
132
|
+
Axes to draw on; ``None`` makes a new figure.
|
|
133
|
+
which : {"density", "fractions", "rates", "selected"}
|
|
134
|
+
Plot densities per unit volume (n(z)-style), fractions per bin, rates, or the
|
|
135
|
+
selected population per bin (the singular spellings are accepted too).
|
|
136
|
+
levels : sequence of float
|
|
137
|
+
Central credible levels shown as nested bands.
|
|
138
|
+
truth : array_like (K,), optional
|
|
139
|
+
True fractions to overlay.
|
|
140
|
+
stack : array_like (K,), optional
|
|
141
|
+
A naive stacked estimate to overlay (see :meth:`Likelihoods.stack
|
|
142
|
+
<binference.Likelihoods.stack>`).
|
|
143
|
+
color : matplotlib colour, optional
|
|
144
|
+
Colour of the posterior; ``None`` takes the style's first colour.
|
|
145
|
+
sample_color : matplotlib colour, optional
|
|
146
|
+
Colour of the ``stack`` overlay; ``None`` takes the style's second colour.
|
|
147
|
+
label : str, optional
|
|
148
|
+
Legend label of the median (default "posterior median").
|
|
149
|
+
alpha : float
|
|
150
|
+
Opacity of each credible band.
|
|
151
|
+
step : bool
|
|
152
|
+
Draw 1-D bins as steps (True) or as lines through the bin centres.
|
|
153
|
+
"""
|
|
154
|
+
from .results import _central, _which_name
|
|
155
|
+
|
|
156
|
+
plt = _plt()
|
|
157
|
+
color = _style_color("color", color, 0)
|
|
158
|
+
sample_color = _style_color("sample_color", sample_color, 1)
|
|
159
|
+
ink = plt.rcParams["text.color"]
|
|
160
|
+
bins = result.bins
|
|
161
|
+
which = _which_name(which)
|
|
162
|
+
if bins.ndim > 2 and not bins.is_categorical:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
"plot supports 1-D and 2-D grids and categories; for more axes use "
|
|
165
|
+
"result.density() or result.summary() (audit O-9)."
|
|
166
|
+
)
|
|
167
|
+
X = result._which(which)
|
|
168
|
+
med = np.median(X, axis=0)
|
|
169
|
+
# overlays of rates (and of densities of rate models) are scaled to the mean total
|
|
170
|
+
scaled = which == "rates" or result.kind == "rates"
|
|
171
|
+
total = float(np.mean(result.total)) if scaled else 1.0
|
|
172
|
+
if bins.ndim == 2 and not bins.is_categorical:
|
|
173
|
+
if truth is not None or stack is not None or label is not None:
|
|
174
|
+
warn("2-D plots show no truth, stack or label; they are ignored.", InputWarning)
|
|
175
|
+
return _plot_2d(result, X, plt, ax, color)
|
|
176
|
+
if ax is None:
|
|
177
|
+
_, ax = plt.subplots()
|
|
178
|
+
ylabel = _YLABEL[which]
|
|
179
|
+
if bins.is_categorical:
|
|
180
|
+
x = np.arange(bins.K)
|
|
181
|
+
lo, hi = _central(X, levels[0])
|
|
182
|
+
ax.bar(x, med, color=color, alpha=0.6, label=label or "posterior median")
|
|
183
|
+
if X.shape[0] > 1:
|
|
184
|
+
ax.errorbar(x, med, yerr=[med - lo, hi - med], fmt="none", ecolor=ink, capsize=3)
|
|
185
|
+
if truth is not None:
|
|
186
|
+
ax.plot(x, _overlay(result, truth, which, total), "D", color=ink, ms=6, label="truth")
|
|
187
|
+
if stack is not None:
|
|
188
|
+
ax.plot(
|
|
189
|
+
x,
|
|
190
|
+
_overlay(result, stack, which, total),
|
|
191
|
+
"s",
|
|
192
|
+
color=sample_color,
|
|
193
|
+
ms=6,
|
|
194
|
+
label="stacked",
|
|
195
|
+
)
|
|
196
|
+
ax.set_xticks(x, [str(lab) for lab in bins.labels], rotation=45, ha="right")
|
|
197
|
+
else:
|
|
198
|
+
edges = bins.edges[0]
|
|
199
|
+
centers = bins.centers
|
|
200
|
+
band_handles = []
|
|
201
|
+
for n_layers, lev in enumerate(sorted(levels, reverse=True), start=1):
|
|
202
|
+
if X.shape[0] < 2:
|
|
203
|
+
break
|
|
204
|
+
# the region inside this band is covered by n_layers translucent fills
|
|
205
|
+
band_handles.append(
|
|
206
|
+
_band_patch(color, 1.0 - (1.0 - alpha) ** n_layers, f"{lev:.0%} interval")
|
|
207
|
+
)
|
|
208
|
+
lo, hi = _central(X, lev)
|
|
209
|
+
if step:
|
|
210
|
+
ax.fill_between(
|
|
211
|
+
edges,
|
|
212
|
+
_step_y(bins, lo),
|
|
213
|
+
_step_y(bins, hi),
|
|
214
|
+
step="post",
|
|
215
|
+
color=color,
|
|
216
|
+
alpha=alpha,
|
|
217
|
+
lw=0,
|
|
218
|
+
)
|
|
219
|
+
else:
|
|
220
|
+
ax.fill_between(centers, lo, hi, color=color, alpha=alpha, lw=0)
|
|
221
|
+
lab = label or "posterior median"
|
|
222
|
+
if step:
|
|
223
|
+
ax.step(edges, _step_y(bins, med), where="post", color=color, lw=1.5, label=lab)
|
|
224
|
+
else:
|
|
225
|
+
ax.plot(centers, med, color=color, lw=1.5, label=lab)
|
|
226
|
+
if truth is not None:
|
|
227
|
+
t = _overlay(result, truth, which, total)
|
|
228
|
+
ax.step(
|
|
229
|
+
edges, _step_y(bins, t), where="post", color=ink, lw=1.5, ls="--", label="truth"
|
|
230
|
+
)
|
|
231
|
+
if stack is not None:
|
|
232
|
+
s = _overlay(result, stack, which, total)
|
|
233
|
+
ax.step(
|
|
234
|
+
edges,
|
|
235
|
+
_step_y(bins, s),
|
|
236
|
+
where="post",
|
|
237
|
+
color=sample_color,
|
|
238
|
+
lw=1.5,
|
|
239
|
+
ls=":",
|
|
240
|
+
label="stacked",
|
|
241
|
+
)
|
|
242
|
+
ax.set_xlim(edges[0], edges[-1])
|
|
243
|
+
ax.set_xlabel("bin coordinate")
|
|
244
|
+
handles, labels = ax.get_legend_handles_labels()
|
|
245
|
+
# median first, then bands (innermost first), then reference lines
|
|
246
|
+
handles = handles[:1] + band_handles[::-1] + handles[1:]
|
|
247
|
+
labels = labels[:1] + [h.get_label() for h in band_handles[::-1]] + labels[1:]
|
|
248
|
+
ax.set_ylabel(ylabel)
|
|
249
|
+
ax.set_ylim(bottom=0)
|
|
250
|
+
_tidy(ax)
|
|
251
|
+
ax.legend(handles, labels, frameon=False)
|
|
252
|
+
return ax
|
|
253
|
+
ax.set_ylabel(ylabel)
|
|
254
|
+
ax.set_ylim(bottom=0)
|
|
255
|
+
_tidy(ax)
|
|
256
|
+
ax.legend(frameon=False)
|
|
257
|
+
return ax
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _step_y(bins, y):
|
|
261
|
+
# per-bin values on the full 1-D grid (NaN, a gap, in masked bins), padded for step="post"
|
|
262
|
+
full = bins.unflatten(np.asarray(y, dtype=float))
|
|
263
|
+
return np.append(full, full[-1])
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _band_patch(color: str, alpha: float, label: str):
|
|
267
|
+
from matplotlib.patches import Patch
|
|
268
|
+
|
|
269
|
+
return Patch(facecolor=color, alpha=alpha, lw=0, label=label)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _plot_2d(result, X, plt, ax, color):
|
|
273
|
+
bins = result.bins
|
|
274
|
+
med = bins.unflatten(np.median(X, axis=0))
|
|
275
|
+
sd = bins.unflatten(X.std(axis=0)) if X.shape[0] > 1 else None
|
|
276
|
+
ex, ey = bins.edges
|
|
277
|
+
if ax is None:
|
|
278
|
+
n = 2 if sd is not None else 1
|
|
279
|
+
# side by side when each panel, with its colour bar, keeps at least 4 in, else stacked
|
|
280
|
+
shape = (1, n) if plt.rcParams["figure.figsize"][0] / n >= 4 else (n, 1)
|
|
281
|
+
fig, ax = plt.subplots(*shape, figsize=_figsize(plt, *shape, 0.8), squeeze=False)
|
|
282
|
+
ax = ax.ravel()
|
|
283
|
+
else:
|
|
284
|
+
fig = None
|
|
285
|
+
axes = np.atleast_1d(ax)
|
|
286
|
+
im = axes[0].pcolormesh(ex, ey, med.T, shading="flat", cmap=_ramp(axes[0], color))
|
|
287
|
+
plt.colorbar(im, ax=axes[0], label="posterior median")
|
|
288
|
+
axes[0].set_xlabel("axis 0")
|
|
289
|
+
axes[0].set_ylabel("axis 1")
|
|
290
|
+
if sd is not None and len(axes) > 1:
|
|
291
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
292
|
+
rel = np.where(med > 0, sd / med, np.nan)
|
|
293
|
+
grey = _ramp(axes[1], plt.rcParams["text.color"])
|
|
294
|
+
im2 = axes[1].pcolormesh(ex, ey, rel.T, shading="flat", cmap=grey)
|
|
295
|
+
plt.colorbar(im2, ax=axes[1], label="relative uncertainty (sd / median)")
|
|
296
|
+
axes[1].set_xlabel("axis 0")
|
|
297
|
+
if fig is not None:
|
|
298
|
+
_tight(fig)
|
|
299
|
+
return axes
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _figsize(plt, nrow: int, ncol: int, aspect: float) -> tuple:
|
|
303
|
+
"""The style's figure width, and a height of ``nrow`` panels of height ``aspect`` x
|
|
304
|
+
their width, each with room for two lines of text (labels, titles)."""
|
|
305
|
+
width = plt.rcParams["figure.figsize"][0]
|
|
306
|
+
text = 2 * plt.rcParams["font.size"] / 72
|
|
307
|
+
return width, nrow * (aspect * width / ncol + text)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _subplots(plt, fig, nrow: int, ncol: int, aspect: float, **kwargs: Any):
|
|
311
|
+
# a new figure of nrow x ncol axes, or those axes added to fig
|
|
312
|
+
if fig is None:
|
|
313
|
+
figsize = _figsize(plt, nrow, ncol, aspect)
|
|
314
|
+
return plt.subplots(nrow, ncol, figsize=figsize, squeeze=False, **kwargs)
|
|
315
|
+
return fig, np.asarray(fig.subplots(nrow, ncol, squeeze=False, **kwargs))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _tight(fig) -> None:
|
|
319
|
+
# tidy a figure the style left without a layout engine (a docs-style figure has one)
|
|
320
|
+
if fig.get_layout_engine() is None:
|
|
321
|
+
fig.tight_layout()
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _chosen_bins(result: Any, which: str, idx: Any, max_bins: int):
|
|
325
|
+
"""The draws of ``which``, the bins ``idx`` (by default the ``max_bins`` largest by
|
|
326
|
+
mean, in order) and the quantity's symbol."""
|
|
327
|
+
from .results import _SYMBOL, _which_name
|
|
328
|
+
|
|
329
|
+
X = result._which(which)
|
|
330
|
+
if idx is None:
|
|
331
|
+
idx = np.sort(np.argsort(-X.mean(axis=0))[:max_bins])
|
|
332
|
+
return X, list(idx), _SYMBOL[_which_name(which)]
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def plot_corner(
|
|
336
|
+
result: Any,
|
|
337
|
+
idx: Sequence[int] | None = None,
|
|
338
|
+
*,
|
|
339
|
+
max_bins: int = 4,
|
|
340
|
+
n_hist: int = 30,
|
|
341
|
+
color: Any = None,
|
|
342
|
+
fig: Any = None,
|
|
343
|
+
which: str = "fractions",
|
|
344
|
+
):
|
|
345
|
+
"""Corner plot of the fractions (or ``which="rates"``, ...) in selected bins.
|
|
346
|
+
|
|
347
|
+
Parameters
|
|
348
|
+
----------
|
|
349
|
+
result : PopulationResult
|
|
350
|
+
idx : sequence of int, optional
|
|
351
|
+
Bins to show; ``None`` chooses the ``max_bins`` with the largest mean.
|
|
352
|
+
max_bins : int, optional
|
|
353
|
+
Number of bins shown when ``idx`` is ``None``.
|
|
354
|
+
n_hist : int, optional
|
|
355
|
+
Histogram bins per panel.
|
|
356
|
+
color : matplotlib colour, optional
|
|
357
|
+
Colour of the histograms; ``None`` takes the style's first colour.
|
|
358
|
+
fig : matplotlib Figure, optional
|
|
359
|
+
Figure to draw on; ``None`` makes a new one.
|
|
360
|
+
which : {"fractions", "rates", "density", "selected"}, optional
|
|
361
|
+
Quantity shown: fractions, rates, densities or the selected population.
|
|
362
|
+
"""
|
|
363
|
+
plt = _plt()
|
|
364
|
+
color = _style_color("color", color, 0)
|
|
365
|
+
X, idx, name = _chosen_bins(result, which, idx, max_bins)
|
|
366
|
+
n = len(idx)
|
|
367
|
+
fig, axes = _subplots(plt, fig, n, n, 1.0)
|
|
368
|
+
for i, ki in enumerate(idx):
|
|
369
|
+
for j, kj in enumerate(idx):
|
|
370
|
+
a = axes[i, j]
|
|
371
|
+
if j > i:
|
|
372
|
+
a.set_visible(False)
|
|
373
|
+
continue
|
|
374
|
+
if i == j:
|
|
375
|
+
a.hist(X[:, ki], bins=n_hist, color=color, histtype="step")
|
|
376
|
+
a.set_yticks([])
|
|
377
|
+
else:
|
|
378
|
+
a.hist2d(X[:, kj], X[:, ki], bins=n_hist, cmap=_ramp(a, color))
|
|
379
|
+
if i == n - 1:
|
|
380
|
+
a.set_xlabel(f"{name}[{kj}]")
|
|
381
|
+
if j == 0 and i > 0:
|
|
382
|
+
a.set_ylabel(f"{name}[{ki}]")
|
|
383
|
+
_tight(fig)
|
|
384
|
+
return fig
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def plot_trace(
|
|
388
|
+
result: Any,
|
|
389
|
+
idx: Sequence[int] | None = None,
|
|
390
|
+
*,
|
|
391
|
+
max_bins: int = 4,
|
|
392
|
+
fig: Any = None,
|
|
393
|
+
which: str = "fractions",
|
|
394
|
+
):
|
|
395
|
+
"""Trace plots of the fractions (or ``which="rates"``, ...) in selected bins, one
|
|
396
|
+
line per chain.
|
|
397
|
+
|
|
398
|
+
Parameters
|
|
399
|
+
----------
|
|
400
|
+
result : PopulationResult
|
|
401
|
+
idx : sequence of int, optional
|
|
402
|
+
Bins to show; ``None`` chooses the ``max_bins`` with the largest mean.
|
|
403
|
+
max_bins : int, optional
|
|
404
|
+
Number of bins shown when ``idx`` is ``None``.
|
|
405
|
+
fig : matplotlib Figure, optional
|
|
406
|
+
Figure to draw on; ``None`` makes a new one.
|
|
407
|
+
which : {"fractions", "rates", "density", "selected"}, optional
|
|
408
|
+
Quantity shown: fractions, rates, densities or the selected population.
|
|
409
|
+
"""
|
|
410
|
+
X, idx, name = _chosen_bins(result, which, idx, max_bins)
|
|
411
|
+
C = X.reshape(result.n_chains, -1, result.K)
|
|
412
|
+
fig, axes = _subplots(_plt(), fig, len(idx), 1, 0.4, sharex=True)
|
|
413
|
+
for a, k in zip(axes[:, 0], idx, strict=True):
|
|
414
|
+
for c in range(C.shape[0]):
|
|
415
|
+
a.plot(C[c, :, k], lw=0.6, alpha=0.8)
|
|
416
|
+
a.set_ylabel(f"{name}[{k}]")
|
|
417
|
+
axes[-1, 0].set_xlabel("iteration")
|
|
418
|
+
_tight(fig)
|
|
419
|
+
return fig
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def plot_individual(
|
|
423
|
+
result: Any,
|
|
424
|
+
i: int,
|
|
425
|
+
ax: Any = None,
|
|
426
|
+
n_draws: int = 200,
|
|
427
|
+
*,
|
|
428
|
+
color: Any = None,
|
|
429
|
+
sample_color: Any = None,
|
|
430
|
+
):
|
|
431
|
+
"""Compare object ``i``'s own likelihood (flat-prior posterior) with its shrunk posterior.
|
|
432
|
+
|
|
433
|
+
Parameters
|
|
434
|
+
----------
|
|
435
|
+
result : PopulationResult or PointEstimate
|
|
436
|
+
i : int
|
|
437
|
+
Index of the object.
|
|
438
|
+
ax : matplotlib Axes, optional
|
|
439
|
+
Axes to draw on; ``None`` makes a new figure.
|
|
440
|
+
n_draws : int, optional
|
|
441
|
+
Population draws averaged for the shrunk posterior.
|
|
442
|
+
color : matplotlib colour, optional
|
|
443
|
+
Colour of the shrunk posterior; ``None`` takes the style's first colour.
|
|
444
|
+
sample_color : matplotlib colour, optional
|
|
445
|
+
Colour of the object's own likelihood; ``None`` takes the style's second colour.
|
|
446
|
+
"""
|
|
447
|
+
plt = _plt()
|
|
448
|
+
color = _style_color("color", color, 0)
|
|
449
|
+
sample_color = _style_color("sample_color", sample_color, 1)
|
|
450
|
+
model = result._require_model()
|
|
451
|
+
bins = result.bins
|
|
452
|
+
own = model.likelihoods.subset([i]).toarray()[0] * bins.volumes
|
|
453
|
+
own = own / own.sum()
|
|
454
|
+
shrunk = result.assignment_probabilities(idx=[i], n_draws=n_draws)[0]
|
|
455
|
+
if ax is None:
|
|
456
|
+
_, ax = plt.subplots()
|
|
457
|
+
if bins.is_categorical or bins.ndim > 1:
|
|
458
|
+
x = np.arange(bins.K)
|
|
459
|
+
ax.bar(x - 0.2, own, width=0.4, color=sample_color, label="own likelihood (flat prior)")
|
|
460
|
+
ax.bar(x + 0.2, shrunk, width=0.4, color=color, label="hierarchical posterior")
|
|
461
|
+
ax.set_xlabel("bin")
|
|
462
|
+
else:
|
|
463
|
+
edges = bins.edges[0]
|
|
464
|
+
vol = bins.volumes
|
|
465
|
+
ax.step(
|
|
466
|
+
edges,
|
|
467
|
+
_step_y(bins, own / vol),
|
|
468
|
+
where="post",
|
|
469
|
+
color=sample_color,
|
|
470
|
+
lw=1.5,
|
|
471
|
+
ls="--",
|
|
472
|
+
label="own likelihood (flat prior)",
|
|
473
|
+
)
|
|
474
|
+
ax.step(
|
|
475
|
+
edges,
|
|
476
|
+
_step_y(bins, shrunk / vol),
|
|
477
|
+
where="post",
|
|
478
|
+
color=color,
|
|
479
|
+
lw=1.5,
|
|
480
|
+
label="hierarchical posterior",
|
|
481
|
+
)
|
|
482
|
+
ax.set_xlabel("bin coordinate")
|
|
483
|
+
ax.set_ylabel("posterior density")
|
|
484
|
+
_tidy(ax)
|
|
485
|
+
ax.legend(frameon=False)
|
|
486
|
+
return ax
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def plot_sbc(
|
|
490
|
+
sbc_result: Any,
|
|
491
|
+
n_bins: int | None = None,
|
|
492
|
+
max_panels: int = 12,
|
|
493
|
+
fig: Any = None,
|
|
494
|
+
*,
|
|
495
|
+
color: Any = None,
|
|
496
|
+
):
|
|
497
|
+
"""Rank histograms from :func:`binference.validation.sbc` with a 99% uniformity band.
|
|
498
|
+
|
|
499
|
+
Parameters
|
|
500
|
+
----------
|
|
501
|
+
sbc_result : SBCResult
|
|
502
|
+
n_bins : int, optional
|
|
503
|
+
Histogram bins; ``None`` uses the default of :meth:`SBCResult.histogram
|
|
504
|
+
<binference.validation.SBCResult.histogram>`.
|
|
505
|
+
max_panels : int, optional
|
|
506
|
+
Most quantities shown.
|
|
507
|
+
fig : matplotlib Figure, optional
|
|
508
|
+
Figure to draw on; ``None`` makes a new one.
|
|
509
|
+
color : matplotlib colour, optional
|
|
510
|
+
Colour of the histograms; ``None`` takes the style's first colour.
|
|
511
|
+
"""
|
|
512
|
+
from scipy import stats
|
|
513
|
+
|
|
514
|
+
plt = _plt()
|
|
515
|
+
color = _style_color("color", color, 0)
|
|
516
|
+
ink = plt.rcParams["text.color"]
|
|
517
|
+
H = sbc_result.histogram(n_bins)
|
|
518
|
+
names = sbc_result.names
|
|
519
|
+
q = min(len(names), max_panels)
|
|
520
|
+
ncol = min(4, q)
|
|
521
|
+
nrow = int(np.ceil(q / ncol))
|
|
522
|
+
fig, axes = _subplots(plt, fig, nrow, ncol, 1.2)
|
|
523
|
+
n_sims = sbc_result.ranks.shape[0]
|
|
524
|
+
nb = H.shape[1]
|
|
525
|
+
# per bin: bins can hold different numbers of the possible ranks (audit G-1)
|
|
526
|
+
p = sbc_result.expected(n_bins) / n_sims
|
|
527
|
+
lo, hi = stats.binom.interval(0.99, n_sims, p)
|
|
528
|
+
for j in range(nrow * ncol):
|
|
529
|
+
a = axes.flat[j]
|
|
530
|
+
if j >= q:
|
|
531
|
+
a.set_visible(False)
|
|
532
|
+
continue
|
|
533
|
+
a.stairs(hi, np.arange(nb + 1) - 0.5, baseline=lo, fill=True, color=ink, alpha=0.15, lw=0)
|
|
534
|
+
a.bar(np.arange(nb), H[j], width=1.0, color=color, alpha=0.8)
|
|
535
|
+
a.set_title(names[j])
|
|
536
|
+
a.set_yticks([])
|
|
537
|
+
_tight(fig)
|
|
538
|
+
return fig
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Priors on population fractions (the simplex) and rates.
|
|
2
|
+
|
|
3
|
+
Conjugate simplex priors (Gibbs, EM and VB all closed-form):
|
|
4
|
+
|
|
5
|
+
* :class:`Dirichlet` (the default, ``alpha=1``)
|
|
6
|
+
* :class:`GeneralizedDirichlet`
|
|
7
|
+
* :class:`DirichletTree`
|
|
8
|
+
* :class:`PolyaTree`
|
|
9
|
+
|
|
10
|
+
Smooth logistic-normal priors (sampled with NUTS, hyperparameters inferred):
|
|
11
|
+
|
|
12
|
+
* :class:`LogisticNormal`
|
|
13
|
+
* :class:`LogisticRandomWalk`
|
|
14
|
+
* :class:`LogisticGP`
|
|
15
|
+
|
|
16
|
+
Rates (Poisson process, conjugate):
|
|
17
|
+
|
|
18
|
+
* :class:`GammaRates`
|
|
19
|
+
|
|
20
|
+
Hyperpriors for scale parameters: :class:`HalfNormal`, :class:`LogNormal`,
|
|
21
|
+
:class:`InverseGamma`, :class:`Gamma`, :class:`Fixed`.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from .base import BoundPrior, PopulationPrior
|
|
25
|
+
from .dirichlet import Dirichlet, DirichletTree, GeneralizedDirichlet, PolyaTree
|
|
26
|
+
from .hyper import Fixed, Gamma, HalfNormal, Hyperprior, InverseGamma, LogNormal
|
|
27
|
+
from .logistic import LogisticGP, LogisticNormal, LogisticRandomWalk
|
|
28
|
+
from .rates import GammaRates
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"BoundPrior",
|
|
32
|
+
"Dirichlet",
|
|
33
|
+
"DirichletTree",
|
|
34
|
+
"Fixed",
|
|
35
|
+
"Gamma",
|
|
36
|
+
"GammaRates",
|
|
37
|
+
"GeneralizedDirichlet",
|
|
38
|
+
"HalfNormal",
|
|
39
|
+
"Hyperprior",
|
|
40
|
+
"InverseGamma",
|
|
41
|
+
"LogNormal",
|
|
42
|
+
"LogisticGP",
|
|
43
|
+
"LogisticNormal",
|
|
44
|
+
"LogisticRandomWalk",
|
|
45
|
+
"PolyaTree",
|
|
46
|
+
"PopulationPrior",
|
|
47
|
+
]
|