kernelmix 0.3.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.
- kernelmix/__init__.py +36 -0
- kernelmix/_basis.py +299 -0
- kernelmix/_clusters.py +78 -0
- kernelmix/_generalized.py +488 -0
- kernelmix/_input.py +120 -0
- kernelmix/_kernel.py +132 -0
- kernelmix/_likelihoods.py +151 -0
- kernelmix/_prediction.py +43 -0
- kernelmix/_structure.py +326 -0
- kernelmix/_structured.py +886 -0
- kernelmix/_survival.py +358 -0
- kernelmix/diagnostics.py +77 -0
- kernelmix/effects.py +332 -0
- kernelmix/model.py +677 -0
- kernelmix/plots.py +205 -0
- kernelmix/priors.py +100 -0
- kernelmix/result.py +785 -0
- kernelmix/structured_result.py +1054 -0
- kernelmix-0.3.0.dist-info/METADATA +102 -0
- kernelmix-0.3.0.dist-info/RECORD +24 -0
- kernelmix-0.3.0.dist-info/WHEEL +5 -0
- kernelmix-0.3.0.dist-info/licenses/LICENSE +339 -0
- kernelmix-0.3.0.dist-info/licenses/NOTICE +23 -0
- kernelmix-0.3.0.dist-info/top_level.txt +1 -0
kernelmix/_structured.py
ADDED
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
"""Noncentered inference for structured residuals, survival histories and GP bases."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import time
|
|
5
|
+
import warnings
|
|
6
|
+
from dataclasses import asdict
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from scipy.linalg import solve_triangular
|
|
10
|
+
from scipy.special import log_ndtr, ndtr
|
|
11
|
+
|
|
12
|
+
from ._basis import basis_matrix, prepare_basis
|
|
13
|
+
from ._clusters import canonical_label, cluster_mask
|
|
14
|
+
from ._generalized import _count, _log_proposal, _positive, elliptical_slice
|
|
15
|
+
from ._input import matrix, prepare_data
|
|
16
|
+
from ._likelihoods import negative_binomial_loglik, poisson_loglik
|
|
17
|
+
from ._structure import _standardized_residual, correlation_blocks, gaussian_loglik
|
|
18
|
+
from ._survival import extended_survival_loglik, prepare_extended_survival
|
|
19
|
+
from .priors import Prior, gamma_logpdf
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _original(value, n, name, *, two_dimensional=False, infinity=False):
|
|
23
|
+
a = np.asarray(value, dtype=float)
|
|
24
|
+
if two_dimensional and a.ndim == 1:
|
|
25
|
+
a = a[:, None]
|
|
26
|
+
shape_ok = a.ndim == 2 and len(a) == n if two_dimensional else a.shape == (n,)
|
|
27
|
+
if not shape_ok or (not infinity and np.isinf(a).any()) or np.isneginf(a).any():
|
|
28
|
+
raise ValueError(f"{name} must have one numeric row per original observation")
|
|
29
|
+
return a.copy()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _rank(a, name, *, no_constant=False):
|
|
33
|
+
if not a.shape[1]:
|
|
34
|
+
return
|
|
35
|
+
check = np.column_stack((np.ones(len(a)), a)) if no_constant else a
|
|
36
|
+
magnitude = np.max(np.abs(check), axis=0)
|
|
37
|
+
if (magnitude == 0).any() or np.linalg.matrix_rank(check / magnitude) != check.shape[1]:
|
|
38
|
+
suffix = " and must not contain an intercept in its column span" if no_constant else ""
|
|
39
|
+
raise ValueError(f"{name} columns must be linearly independent{suffix}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _prepare(
|
|
43
|
+
y,
|
|
44
|
+
Z,
|
|
45
|
+
X,
|
|
46
|
+
*,
|
|
47
|
+
family,
|
|
48
|
+
standardize,
|
|
49
|
+
intercept,
|
|
50
|
+
missing,
|
|
51
|
+
exposure_names,
|
|
52
|
+
covariate_names,
|
|
53
|
+
groups,
|
|
54
|
+
cluster,
|
|
55
|
+
offset,
|
|
56
|
+
event,
|
|
57
|
+
baseline_breaks,
|
|
58
|
+
random_slopes,
|
|
59
|
+
variance_design,
|
|
60
|
+
correlation,
|
|
61
|
+
observation_time,
|
|
62
|
+
entry,
|
|
63
|
+
upper,
|
|
64
|
+
subject,
|
|
65
|
+
n_causes,
|
|
66
|
+
):
|
|
67
|
+
outcome = np.asarray(y, dtype=float)
|
|
68
|
+
if outcome.ndim != 1:
|
|
69
|
+
raise ValueError("y must be a one-dimensional vector")
|
|
70
|
+
n = len(outcome)
|
|
71
|
+
z = matrix(Z, "Z")
|
|
72
|
+
x = np.empty((n, 0)) if X is None else matrix(X, "X")
|
|
73
|
+
if len(z) != n or len(x) != n or np.isinf(outcome).any():
|
|
74
|
+
raise ValueError("outcome, exposures and covariates must have matching finite rows")
|
|
75
|
+
extra = {}
|
|
76
|
+
for name, value, is_matrix, infinity in (
|
|
77
|
+
("random_slopes", random_slopes, True, False),
|
|
78
|
+
("variance_design", variance_design, True, False),
|
|
79
|
+
("offset", offset, False, False),
|
|
80
|
+
("event", event, False, False),
|
|
81
|
+
("entry", entry, False, False),
|
|
82
|
+
("upper", upper, False, True),
|
|
83
|
+
("observation_time", observation_time, False, False),
|
|
84
|
+
):
|
|
85
|
+
if value is not None:
|
|
86
|
+
extra[name] = _original(value, n, name, two_dimensional=is_matrix, infinity=infinity)
|
|
87
|
+
if family != "survival" and any(
|
|
88
|
+
v is not None for v in (entry, upper, subject, n_causes, event, baseline_breaks)
|
|
89
|
+
):
|
|
90
|
+
raise ValueError('survival inputs require family="survival"')
|
|
91
|
+
if family not in ("poisson", "negative_binomial") and offset is not None:
|
|
92
|
+
raise ValueError("offset applies only to count models")
|
|
93
|
+
if family != "gaussian" and (
|
|
94
|
+
variance_design is not None or correlation is not None or observation_time is not None
|
|
95
|
+
):
|
|
96
|
+
raise ValueError("variance_design and residual correlation apply only to Gaussian outcomes")
|
|
97
|
+
if random_slopes is not None and cluster is None:
|
|
98
|
+
raise ValueError("random_slopes requires cluster labels")
|
|
99
|
+
if correlation is not None and cluster is None:
|
|
100
|
+
raise ValueError("residual correlation requires cluster labels")
|
|
101
|
+
if correlation is None and observation_time is not None:
|
|
102
|
+
raise ValueError("observation_time requires a residual correlation model")
|
|
103
|
+
keep = np.isfinite(outcome) & np.isfinite(z).all(axis=1) & np.isfinite(x).all(axis=1)
|
|
104
|
+
keep &= cluster_mask(cluster, n)
|
|
105
|
+
for a in extra.values():
|
|
106
|
+
available = ~np.isnan(a)
|
|
107
|
+
keep &= available.all(axis=1) if a.ndim == 2 else available
|
|
108
|
+
dropped_subjects = []
|
|
109
|
+
if subject is not None:
|
|
110
|
+
labels = np.asarray(subject, dtype=object)
|
|
111
|
+
keep &= cluster_mask(labels, n)
|
|
112
|
+
by_subject = {}
|
|
113
|
+
for i, label in enumerate(labels):
|
|
114
|
+
if label is None or isinstance(label, (float, np.floating)) and np.isnan(label):
|
|
115
|
+
continue
|
|
116
|
+
by_subject.setdefault(canonical_label(label), []).append(i)
|
|
117
|
+
for identity, rows in by_subject.items():
|
|
118
|
+
if not keep[rows].all():
|
|
119
|
+
keep[rows] = False
|
|
120
|
+
dropped_subjects.append(identity[1])
|
|
121
|
+
if missing not in ("raise", "drop"):
|
|
122
|
+
raise ValueError("missing must be raise or drop")
|
|
123
|
+
if missing == "raise" and not keep.all():
|
|
124
|
+
raise ValueError(
|
|
125
|
+
'missing input values; missing="drop" removes whole incomplete subject histories'
|
|
126
|
+
)
|
|
127
|
+
masked = outcome.copy()
|
|
128
|
+
masked[~keep] = np.nan
|
|
129
|
+
data = prepare_data(
|
|
130
|
+
masked,
|
|
131
|
+
Z,
|
|
132
|
+
X,
|
|
133
|
+
family="gaussian",
|
|
134
|
+
standardize=standardize,
|
|
135
|
+
intercept=intercept,
|
|
136
|
+
missing=missing,
|
|
137
|
+
exposure_names=exposure_names,
|
|
138
|
+
covariate_names=covariate_names,
|
|
139
|
+
groups=groups,
|
|
140
|
+
cluster=cluster,
|
|
141
|
+
)
|
|
142
|
+
rows = data["rows"]
|
|
143
|
+
data["y"] = outcome[rows].copy()
|
|
144
|
+
extra = {key: a[rows].copy() for key, a in extra.items()}
|
|
145
|
+
data["dropped_subject_levels"] = dropped_subjects
|
|
146
|
+
data["variance_design"] = extra.get("variance_design", np.empty((len(rows), 0)))
|
|
147
|
+
_rank(data["variance_design"], "variance_design", no_constant=True)
|
|
148
|
+
slopes = extra.get("random_slopes", np.empty((len(rows), 0)))
|
|
149
|
+
data["random_design"] = (
|
|
150
|
+
np.column_stack((np.ones(len(rows)), slopes))
|
|
151
|
+
if cluster is not None
|
|
152
|
+
else np.empty((len(rows), 0))
|
|
153
|
+
)
|
|
154
|
+
_rank(data["random_design"], "random coefficient design")
|
|
155
|
+
if family == "binomial" and not np.isin(data["y"], [0, 1]).all():
|
|
156
|
+
raise ValueError("binomial outcomes must be zero or one")
|
|
157
|
+
if family in ("poisson", "negative_binomial"):
|
|
158
|
+
if (data["y"] < 0).any() or (data["y"] != np.floor(data["y"])).any():
|
|
159
|
+
raise ValueError("count outcomes must be nonnegative integers")
|
|
160
|
+
data["offset"] = extra.get("offset", np.zeros(len(rows)))
|
|
161
|
+
if family == "survival":
|
|
162
|
+
if event is None:
|
|
163
|
+
raise ValueError("survival outcomes require event indicators or cause codes")
|
|
164
|
+
labels = None if subject is None else np.asarray(subject, dtype=object)[rows]
|
|
165
|
+
data.update(
|
|
166
|
+
prepare_extended_survival(
|
|
167
|
+
data["y"],
|
|
168
|
+
extra["event"],
|
|
169
|
+
entry=extra.get("entry"),
|
|
170
|
+
upper=extra.get("upper"),
|
|
171
|
+
subject=labels,
|
|
172
|
+
n_causes=n_causes,
|
|
173
|
+
baseline_breaks=baseline_breaks,
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
data["observation_time"] = extra.get("observation_time")
|
|
177
|
+
if correlation is None:
|
|
178
|
+
data["correlation_matrix"], kind = None, "independent"
|
|
179
|
+
elif isinstance(correlation, str):
|
|
180
|
+
if correlation not in ("ar1", "exponential"):
|
|
181
|
+
raise ValueError("correlation must be ar1, exponential, a matrix, or None")
|
|
182
|
+
data["correlation_matrix"], kind = None, correlation
|
|
183
|
+
if observation_time is None:
|
|
184
|
+
raise ValueError("parametric residual correlation requires observation_time")
|
|
185
|
+
else:
|
|
186
|
+
corr = np.asarray(correlation, dtype=float)
|
|
187
|
+
if corr.shape != (n, n) or not np.isfinite(corr).all():
|
|
188
|
+
raise ValueError("fixed correlation must be a finite original-row square matrix")
|
|
189
|
+
data["correlation_matrix"], kind = corr[np.ix_(rows, rows)].copy(), "fixed"
|
|
190
|
+
return data, kind
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _random_factor(scale_raw, correlation_raw, bound):
|
|
194
|
+
"""Proper induced correlation prior and bounded uniform standard deviations."""
|
|
195
|
+
k, q = scale_raw.shape
|
|
196
|
+
result = np.zeros((k, q, q))
|
|
197
|
+
for cause in range(k):
|
|
198
|
+
lower = np.eye(q)
|
|
199
|
+
lower[np.tril_indices(q, -1)] = correlation_raw[cause]
|
|
200
|
+
lower /= np.linalg.norm(lower, axis=1)[:, None]
|
|
201
|
+
sd = bound * ndtr(scale_raw[cause])
|
|
202
|
+
if not np.isfinite(sd).all() or (sd <= 0).any():
|
|
203
|
+
raise FloatingPointError("random-effect scales reached a floating-point boundary")
|
|
204
|
+
result[cause] = sd[:, None] * lower
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class _Layout:
|
|
209
|
+
def __init__(self):
|
|
210
|
+
self.parts = {}
|
|
211
|
+
self.size = 0
|
|
212
|
+
|
|
213
|
+
def add(self, name, shape):
|
|
214
|
+
size = math.prod(shape)
|
|
215
|
+
self.parts[name] = (slice(self.size, self.size + size), tuple(shape))
|
|
216
|
+
self.size += size
|
|
217
|
+
|
|
218
|
+
def get(self, white, name):
|
|
219
|
+
sl, shape = self.parts[name]
|
|
220
|
+
return white[sl].reshape(shape)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def fit_structured(
|
|
224
|
+
y,
|
|
225
|
+
Z,
|
|
226
|
+
X=None,
|
|
227
|
+
*,
|
|
228
|
+
family="gaussian",
|
|
229
|
+
draws=1000,
|
|
230
|
+
warmup=1000,
|
|
231
|
+
chains=4,
|
|
232
|
+
thin=1,
|
|
233
|
+
seed=None,
|
|
234
|
+
standardize=True,
|
|
235
|
+
intercept=True,
|
|
236
|
+
selection="none",
|
|
237
|
+
groups=None,
|
|
238
|
+
prior=None,
|
|
239
|
+
r_fixed=None,
|
|
240
|
+
missing="raise",
|
|
241
|
+
exposure_names=None,
|
|
242
|
+
covariate_names=None,
|
|
243
|
+
initial=None,
|
|
244
|
+
proposal_scales=None,
|
|
245
|
+
cluster=None,
|
|
246
|
+
offset=None,
|
|
247
|
+
event=None,
|
|
248
|
+
baseline_breaks=None,
|
|
249
|
+
kernel="gaussian",
|
|
250
|
+
kernel_alpha=1,
|
|
251
|
+
approximation="dense",
|
|
252
|
+
n_components=32,
|
|
253
|
+
knots=None,
|
|
254
|
+
random_slopes=None,
|
|
255
|
+
random_covariance=None,
|
|
256
|
+
random_scale=2,
|
|
257
|
+
correlation=None,
|
|
258
|
+
observation_time=None,
|
|
259
|
+
correlation_parameter=None,
|
|
260
|
+
variance_design=None,
|
|
261
|
+
entry=None,
|
|
262
|
+
upper=None,
|
|
263
|
+
subject=None,
|
|
264
|
+
n_causes=None,
|
|
265
|
+
):
|
|
266
|
+
"""Fit the documented structured model with a dense or finite-basis GP."""
|
|
267
|
+
started = time.perf_counter()
|
|
268
|
+
if family not in ("gaussian", "binomial", "poisson", "negative_binomial", "survival"):
|
|
269
|
+
raise ValueError("unknown outcome family")
|
|
270
|
+
draws, warmup = _count(draws, "draws", 2), _count(warmup, "warmup", 0)
|
|
271
|
+
chains, thin = _count(chains, "chains", 1), _count(thin, "thin", 1)
|
|
272
|
+
if seed is not None:
|
|
273
|
+
seed = _count(seed, "seed", 0)
|
|
274
|
+
if selection not in ("none", "component", "group") or (
|
|
275
|
+
(selection == "group") != (groups is not None)
|
|
276
|
+
):
|
|
277
|
+
raise ValueError(
|
|
278
|
+
"selection must be none/component/group; groups are required only for group selection"
|
|
279
|
+
)
|
|
280
|
+
defaults = dict(
|
|
281
|
+
lambda_mean=1.0,
|
|
282
|
+
lambda_sd=0.5,
|
|
283
|
+
cluster_lambda_mean=1.0,
|
|
284
|
+
cluster_lambda_sd=0.5,
|
|
285
|
+
sigma2_shape=2.0,
|
|
286
|
+
sigma2_rate=1.0,
|
|
287
|
+
)
|
|
288
|
+
if prior is None:
|
|
289
|
+
prior = Prior(**defaults)
|
|
290
|
+
elif isinstance(prior, dict):
|
|
291
|
+
prior = Prior(**{**defaults, **prior})
|
|
292
|
+
if not isinstance(prior, Prior):
|
|
293
|
+
raise ValueError("prior must be Prior, a dictionary or None")
|
|
294
|
+
data, corr_kind = _prepare(
|
|
295
|
+
y,
|
|
296
|
+
Z,
|
|
297
|
+
X,
|
|
298
|
+
family=family,
|
|
299
|
+
standardize=standardize,
|
|
300
|
+
intercept=intercept,
|
|
301
|
+
missing=missing,
|
|
302
|
+
exposure_names=exposure_names,
|
|
303
|
+
covariate_names=covariate_names,
|
|
304
|
+
groups=groups,
|
|
305
|
+
cluster=cluster,
|
|
306
|
+
offset=offset,
|
|
307
|
+
event=event,
|
|
308
|
+
baseline_breaks=baseline_breaks,
|
|
309
|
+
random_slopes=random_slopes,
|
|
310
|
+
variance_design=variance_design,
|
|
311
|
+
correlation=correlation,
|
|
312
|
+
observation_time=observation_time,
|
|
313
|
+
entry=entry,
|
|
314
|
+
upper=upper,
|
|
315
|
+
subject=subject,
|
|
316
|
+
n_causes=n_causes,
|
|
317
|
+
)
|
|
318
|
+
n, m = data["Z"].shape
|
|
319
|
+
p = data["X"].shape[1]
|
|
320
|
+
k = data.get("n_causes", 1)
|
|
321
|
+
g = len(data["cluster_levels"])
|
|
322
|
+
q = data["random_design"].shape[1]
|
|
323
|
+
random_scale = _positive(random_scale, "random_scale")
|
|
324
|
+
if random_covariance is not None and not g:
|
|
325
|
+
raise ValueError("random_covariance requires cluster labels")
|
|
326
|
+
fixed_random = None
|
|
327
|
+
if random_covariance is not None:
|
|
328
|
+
fixed_random = np.asarray(random_covariance, dtype=float)
|
|
329
|
+
if (
|
|
330
|
+
fixed_random.shape != (q, q)
|
|
331
|
+
or not np.isfinite(fixed_random).all()
|
|
332
|
+
or not np.allclose(fixed_random, fixed_random.T, rtol=0, atol=1e-12)
|
|
333
|
+
):
|
|
334
|
+
raise ValueError(
|
|
335
|
+
"random_covariance must be a finite symmetric matrix matching the random design"
|
|
336
|
+
)
|
|
337
|
+
try:
|
|
338
|
+
np.linalg.cholesky(fixed_random)
|
|
339
|
+
except np.linalg.LinAlgError as exc:
|
|
340
|
+
raise ValueError("random_covariance must be positive definite") from exc
|
|
341
|
+
data["fixed_random_covariance"] = None if fixed_random is None else fixed_random.copy()
|
|
342
|
+
corr_inferred = corr_kind in ("ar1", "exponential") and correlation_parameter is None
|
|
343
|
+
if corr_kind in ("independent", "fixed") and correlation_parameter is not None:
|
|
344
|
+
raise ValueError("correlation_parameter applies only to ar1/exponential correlation")
|
|
345
|
+
initial_corr = 0.0 if corr_kind == "ar1" else 1.0 if corr_kind == "exponential" else None
|
|
346
|
+
if correlation_parameter is not None:
|
|
347
|
+
if (
|
|
348
|
+
isinstance(correlation_parameter, (bool, np.bool_))
|
|
349
|
+
or not np.isscalar(correlation_parameter)
|
|
350
|
+
or not np.isfinite(correlation_parameter)
|
|
351
|
+
):
|
|
352
|
+
raise ValueError("correlation_parameter must be a finite scalar")
|
|
353
|
+
initial_corr = float(correlation_parameter)
|
|
354
|
+
if corr_kind != "independent":
|
|
355
|
+
correlation_blocks(
|
|
356
|
+
data["cluster_codes"],
|
|
357
|
+
data["observation_time"],
|
|
358
|
+
corr_kind,
|
|
359
|
+
initial_corr,
|
|
360
|
+
data["correlation_matrix"],
|
|
361
|
+
)
|
|
362
|
+
sequence = np.random.SeedSequence(seed)
|
|
363
|
+
streams = sequence.spawn(chains + 1)
|
|
364
|
+
transformed_knots = None
|
|
365
|
+
if knots is not None:
|
|
366
|
+
raw = np.asarray(knots, dtype=float)
|
|
367
|
+
if raw.ndim != 2 or raw.shape[1] != m or not np.isfinite(raw).all():
|
|
368
|
+
raise ValueError("knots must have finite rows in original exposure units")
|
|
369
|
+
transformed_knots = (raw - data["z_center"]) / data["z_scale"]
|
|
370
|
+
basis = prepare_basis(
|
|
371
|
+
data["Z"],
|
|
372
|
+
approximation=approximation,
|
|
373
|
+
kernel=kernel,
|
|
374
|
+
alpha=kernel_alpha,
|
|
375
|
+
n_components=n_components,
|
|
376
|
+
knots=transformed_knots,
|
|
377
|
+
rng=np.random.default_rng(streams[0]),
|
|
378
|
+
)
|
|
379
|
+
b = basis["n_features"]
|
|
380
|
+
mode = basis["approximation"]
|
|
381
|
+
data["basis_meta"] = {
|
|
382
|
+
name: value for name, value in basis.items() if not isinstance(value, np.ndarray)
|
|
383
|
+
}
|
|
384
|
+
for name, value in basis.items():
|
|
385
|
+
if isinstance(value, np.ndarray):
|
|
386
|
+
data["basis_" + name] = value.copy()
|
|
387
|
+
anchor_rows = n if mode == "dense" else len(basis["knots"]) if mode == "pp" else 0
|
|
388
|
+
layout = _Layout()
|
|
389
|
+
layout.add("gp", (k, b))
|
|
390
|
+
layout.add("beta", (k, p))
|
|
391
|
+
layout.add("random", (k, g, q))
|
|
392
|
+
layout.add("gamma", (data["variance_design"].shape[1],))
|
|
393
|
+
if g and fixed_random is None:
|
|
394
|
+
layout.add("random_scale", (k, q))
|
|
395
|
+
layout.add("random_correlation", (k, q * (q - 1) // 2))
|
|
396
|
+
if corr_inferred:
|
|
397
|
+
layout.add("correlation", (1,))
|
|
398
|
+
shapes = {
|
|
399
|
+
"beta": (k, p),
|
|
400
|
+
"r": (m,),
|
|
401
|
+
"lambda": (k,),
|
|
402
|
+
"sigma2": (),
|
|
403
|
+
"delta": (m,),
|
|
404
|
+
"pi": (),
|
|
405
|
+
"gp_weight": (k, b),
|
|
406
|
+
}
|
|
407
|
+
if anchor_rows:
|
|
408
|
+
shapes["gp_anchor"] = (k, anchor_rows)
|
|
409
|
+
if g:
|
|
410
|
+
shapes.update(random_coeff=(k, g, q), random_covariance=(k, q, q))
|
|
411
|
+
if data["variance_design"].shape[1]:
|
|
412
|
+
shapes["gamma"] = (data["variance_design"].shape[1],)
|
|
413
|
+
if corr_kind in ("ar1", "exponential"):
|
|
414
|
+
shapes["correlation_parameter"] = ()
|
|
415
|
+
if family == "negative_binomial":
|
|
416
|
+
shapes["dispersion"] = ()
|
|
417
|
+
if family == "survival":
|
|
418
|
+
shapes["baseline_hazard"] = (k, len(data["baseline_breaks"]) + 1)
|
|
419
|
+
saved_elements = chains * draws * sum(math.prod(shape) for shape in shapes.values())
|
|
420
|
+
if 8 * (saved_elements + 8 * n * b + 4 * layout.size) > 512 * 1024**2:
|
|
421
|
+
raise ValueError(
|
|
422
|
+
"structured fit exceeds the 512 MiB working-memory limit; reduce saved states or basis size"
|
|
423
|
+
)
|
|
424
|
+
samples = {
|
|
425
|
+
name: np.empty((chains, draws) + shape, dtype=bool if name == "delta" else float)
|
|
426
|
+
for name, shape in shapes.items()
|
|
427
|
+
}
|
|
428
|
+
fixed = None if r_fixed is None else np.asarray(r_fixed, dtype=float)
|
|
429
|
+
if fixed is not None and (
|
|
430
|
+
fixed.shape != (m,)
|
|
431
|
+
or not np.isfinite(fixed).all()
|
|
432
|
+
or (fixed < 0).any()
|
|
433
|
+
or selection != "none"
|
|
434
|
+
):
|
|
435
|
+
raise ValueError(
|
|
436
|
+
"r_fixed must be nonnegative with one entry per exposure and disallows selection"
|
|
437
|
+
)
|
|
438
|
+
units = (
|
|
439
|
+
[np.array([j]) for j in range(m)]
|
|
440
|
+
if selection == "component"
|
|
441
|
+
else [
|
|
442
|
+
np.flatnonzero(np.asarray(data["groups"]) == label)
|
|
443
|
+
for label in dict.fromkeys(data["groups"])
|
|
444
|
+
]
|
|
445
|
+
if selection == "group"
|
|
446
|
+
else []
|
|
447
|
+
)
|
|
448
|
+
scales = {"r": 0.3, "lambda": 0.3}
|
|
449
|
+
if family == "negative_binomial":
|
|
450
|
+
scales["dispersion"] = 0.3
|
|
451
|
+
if family == "survival":
|
|
452
|
+
scales["baseline_hazard"] = 0.3
|
|
453
|
+
if proposal_scales is not None:
|
|
454
|
+
if not isinstance(proposal_scales, dict) or set(proposal_scales) - set(scales):
|
|
455
|
+
raise ValueError(f"proposal_scales accepts only {list(scales)} for this model")
|
|
456
|
+
scales.update(proposal_scales)
|
|
457
|
+
scales = {name: _positive(value, f"proposal_scales[{name}]") for name, value in scales.items()}
|
|
458
|
+
if initial is None:
|
|
459
|
+
initial = {}
|
|
460
|
+
allowed = {"beta", "gp_weight", "r", "delta", "pi", "lambda", "sigma2"}
|
|
461
|
+
if family == "negative_binomial":
|
|
462
|
+
allowed.add("dispersion")
|
|
463
|
+
if family == "survival":
|
|
464
|
+
allowed.add("baseline_hazard")
|
|
465
|
+
if not isinstance(initial, dict) or set(initial) - allowed:
|
|
466
|
+
raise ValueError(f"initial accepts only {sorted(allowed)} for this model")
|
|
467
|
+
messages = []
|
|
468
|
+
if prior.lambda_mean / prior.lambda_sd**2 <= 2:
|
|
469
|
+
messages.append(
|
|
470
|
+
"The supplied GP amplitude prior does not guarantee finite second exp-Gaussian moments; finite-chain means do not prove moment existence."
|
|
471
|
+
)
|
|
472
|
+
if family == "gaussian" and prior.sigma2_shape <= 1:
|
|
473
|
+
messages.append("The supplied inverse-gamma residual prior has no finite mean.")
|
|
474
|
+
if family == "survival":
|
|
475
|
+
if intercept:
|
|
476
|
+
messages.append(
|
|
477
|
+
"Survival intercept and baseline scale are separated by their priors; consider intercept=False."
|
|
478
|
+
)
|
|
479
|
+
if not (data["event"] != 0).any():
|
|
480
|
+
messages.append("No observed events; survival estimates depend strongly on the priors.")
|
|
481
|
+
if (data["entry"] > 0).any():
|
|
482
|
+
messages.append(
|
|
483
|
+
"Latent priors describe the included entrant cohort, not an inception-population frailty distribution."
|
|
484
|
+
)
|
|
485
|
+
if g and (g < q + 1 or all(np.sum(data["cluster_codes"] == j) == 1 for j in range(g))):
|
|
486
|
+
messages.append(
|
|
487
|
+
"Few repeated clusters for random covariance estimation; variance components may be weakly identified."
|
|
488
|
+
)
|
|
489
|
+
if mode != "dense":
|
|
490
|
+
messages.append(
|
|
491
|
+
"Inference conditions on the selected finite GP basis; it is not exact dense-GP inference."
|
|
492
|
+
)
|
|
493
|
+
for message in messages:
|
|
494
|
+
warnings.warn(message, RuntimeWarning, stacklevel=2)
|
|
495
|
+
chain_info = []
|
|
496
|
+
for chain, stream in enumerate(streams[1:]):
|
|
497
|
+
rng = np.random.default_rng(stream)
|
|
498
|
+
delta = np.ones(m, dtype=bool)
|
|
499
|
+
if selection != "none":
|
|
500
|
+
delta[:] = False
|
|
501
|
+
for members in units:
|
|
502
|
+
if rng.random() < 0.5:
|
|
503
|
+
delta[rng.choice(members)] = True
|
|
504
|
+
if "delta" in initial:
|
|
505
|
+
supplied = np.asarray(initial["delta"])
|
|
506
|
+
if supplied.shape != (m,) or not np.isin(supplied, [0, 1]).all():
|
|
507
|
+
raise ValueError("initial delta must be zero/one per exposure")
|
|
508
|
+
delta = supplied.astype(bool)
|
|
509
|
+
if fixed is not None:
|
|
510
|
+
if "delta" in initial and not np.array_equal(delta, fixed > 0):
|
|
511
|
+
raise ValueError("initial delta conflicts with r_fixed")
|
|
512
|
+
delta = fixed > 0
|
|
513
|
+
if selection == "none" and fixed is None and not delta.all():
|
|
514
|
+
raise ValueError("all exposures must be active without selection")
|
|
515
|
+
if selection == "group" and any(delta[u].sum() > 1 for u in units):
|
|
516
|
+
raise ValueError("at most one exposure per group may be active")
|
|
517
|
+
r = (
|
|
518
|
+
fixed.copy()
|
|
519
|
+
if fixed is not None
|
|
520
|
+
else np.array([prior.sample_r(rng) if active else 0.0 for active in delta])
|
|
521
|
+
)
|
|
522
|
+
if "r" in initial:
|
|
523
|
+
supplied = np.asarray(initial["r"], dtype=float)
|
|
524
|
+
if fixed is not None and not np.array_equal(supplied, fixed):
|
|
525
|
+
raise ValueError("initial r conflicts with r_fixed")
|
|
526
|
+
r = supplied.copy()
|
|
527
|
+
if (
|
|
528
|
+
r.shape != (m,)
|
|
529
|
+
or not np.isfinite(r).all()
|
|
530
|
+
or (r[~delta] != 0).any()
|
|
531
|
+
or (fixed is None and any(not np.isfinite(prior.log_r(v)) for v in r[delta]))
|
|
532
|
+
):
|
|
533
|
+
raise ValueError("initial relevance is inconsistent with selection or its prior")
|
|
534
|
+
white = np.zeros(layout.size)
|
|
535
|
+
for name, target, scale in [("beta", "beta", prior.beta_sd), ("gp_weight", "gp", 1.0)]:
|
|
536
|
+
if name in initial:
|
|
537
|
+
a = np.asarray(initial[name], dtype=float)
|
|
538
|
+
sl, shape = layout.parts[target]
|
|
539
|
+
if k == 1 and a.shape == shape[1:]:
|
|
540
|
+
a = a[None, :]
|
|
541
|
+
if a.shape != shape or not np.isfinite(a).all():
|
|
542
|
+
raise ValueError(f"initial {name} has an invalid shape")
|
|
543
|
+
white[sl] = a.ravel() / scale
|
|
544
|
+
amplitude = np.asarray(initial.get("lambda", np.full(k, prior.lambda_mean)), dtype=float)
|
|
545
|
+
if amplitude.ndim == 0:
|
|
546
|
+
amplitude = np.full(k, float(amplitude))
|
|
547
|
+
if amplitude.shape != (k,) or not np.isfinite(amplitude).all() or (amplitude <= 0).any():
|
|
548
|
+
raise ValueError("initial lambda must be positive per cause")
|
|
549
|
+
sigma2 = _positive(
|
|
550
|
+
initial.get(
|
|
551
|
+
"sigma2", max(float(np.var(data["y"])), 0.1) if family == "gaussian" else 1.0
|
|
552
|
+
),
|
|
553
|
+
"initial sigma2",
|
|
554
|
+
)
|
|
555
|
+
if family != "gaussian" and sigma2 != 1:
|
|
556
|
+
raise ValueError("non-Gaussian sigma2 is fixed at one")
|
|
557
|
+
phi = (
|
|
558
|
+
_positive(
|
|
559
|
+
initial.get("dispersion", prior.dispersion_shape / prior.dispersion_rate),
|
|
560
|
+
"initial dispersion",
|
|
561
|
+
)
|
|
562
|
+
if family == "negative_binomial"
|
|
563
|
+
else None
|
|
564
|
+
)
|
|
565
|
+
baseline = np.asarray(
|
|
566
|
+
initial.get(
|
|
567
|
+
"baseline_hazard",
|
|
568
|
+
np.full(
|
|
569
|
+
shapes.get("baseline_hazard", (0, 0)),
|
|
570
|
+
prior.baseline_shape / prior.baseline_rate,
|
|
571
|
+
),
|
|
572
|
+
),
|
|
573
|
+
dtype=float,
|
|
574
|
+
)
|
|
575
|
+
if family == "survival" and (
|
|
576
|
+
baseline.shape != shapes["baseline_hazard"]
|
|
577
|
+
or not np.isfinite(baseline).all()
|
|
578
|
+
or (baseline <= 0).any()
|
|
579
|
+
):
|
|
580
|
+
raise ValueError(
|
|
581
|
+
"initial baseline_hazard must be positive with shape (causes, intervals)"
|
|
582
|
+
)
|
|
583
|
+
pi = float(initial.get("pi", 1.0 if selection == "none" else 0.5))
|
|
584
|
+
if (
|
|
585
|
+
not np.isfinite(pi)
|
|
586
|
+
or (selection == "none" and pi != 1)
|
|
587
|
+
or (selection != "none" and not 0 < pi < 1)
|
|
588
|
+
):
|
|
589
|
+
raise ValueError("initial pi is inconsistent with selection")
|
|
590
|
+
factor = basis_matrix(data["Z"], r, basis)
|
|
591
|
+
|
|
592
|
+
def components(w, f=None, amp=None):
|
|
593
|
+
f = factor if f is None else f
|
|
594
|
+
amp = amplitude if amp is None else amp
|
|
595
|
+
gp = (f @ layout.get(w, "gp").T) * np.sqrt(amp)[None, :]
|
|
596
|
+
beta = layout.get(w, "beta") * prior.beta_sd
|
|
597
|
+
eta = gp + data["X"] @ beta.T
|
|
598
|
+
cov = np.empty((k, 0, 0))
|
|
599
|
+
coeff = np.empty((k, 0, 0))
|
|
600
|
+
if g:
|
|
601
|
+
lower = (
|
|
602
|
+
np.repeat(np.linalg.cholesky(fixed_random)[None], k, axis=0)
|
|
603
|
+
if fixed_random is not None
|
|
604
|
+
else _random_factor(
|
|
605
|
+
layout.get(w, "random_scale"),
|
|
606
|
+
layout.get(w, "random_correlation"),
|
|
607
|
+
random_scale,
|
|
608
|
+
)
|
|
609
|
+
)
|
|
610
|
+
coeff = np.einsum("kgq,krq->kgr", layout.get(w, "random"), lower)
|
|
611
|
+
cov = lower @ lower.transpose(0, 2, 1)
|
|
612
|
+
eta += np.einsum(
|
|
613
|
+
"nkq,nq->nk",
|
|
614
|
+
coeff[:, data["cluster_codes"], :].transpose(1, 0, 2),
|
|
615
|
+
data["random_design"],
|
|
616
|
+
)
|
|
617
|
+
gamma = layout.get(w, "gamma")
|
|
618
|
+
logvar = data["variance_design"] @ gamma
|
|
619
|
+
corr = (
|
|
620
|
+
(
|
|
621
|
+
float(np.tanh(layout.get(w, "correlation")[0]))
|
|
622
|
+
if corr_kind == "ar1"
|
|
623
|
+
else math.exp(float(layout.get(w, "correlation")[0]))
|
|
624
|
+
)
|
|
625
|
+
if corr_inferred
|
|
626
|
+
else initial_corr
|
|
627
|
+
)
|
|
628
|
+
blocks = (
|
|
629
|
+
correlation_blocks(
|
|
630
|
+
data["cluster_codes"],
|
|
631
|
+
data["observation_time"],
|
|
632
|
+
corr_kind,
|
|
633
|
+
corr,
|
|
634
|
+
data["correlation_matrix"],
|
|
635
|
+
)
|
|
636
|
+
if corr_kind != "independent"
|
|
637
|
+
else []
|
|
638
|
+
)
|
|
639
|
+
return gp, beta, eta, coeff, cov, gamma, logvar, corr, blocks
|
|
640
|
+
|
|
641
|
+
def likelihood(w, f=None, amp=None):
|
|
642
|
+
try:
|
|
643
|
+
_, _, eta, _, _, _, logvar, _, blocks = components(w, f, amp)
|
|
644
|
+
if not np.isfinite(eta).all() or not np.isfinite(logvar).all():
|
|
645
|
+
return -np.inf
|
|
646
|
+
if family == "gaussian":
|
|
647
|
+
return gaussian_loglik(data["y"], eta[:, 0], sigma2, logvar, blocks)
|
|
648
|
+
if family == "binomial":
|
|
649
|
+
return float(log_ndtr((2 * data["y"] - 1) * eta[:, 0]).sum())
|
|
650
|
+
if family == "poisson":
|
|
651
|
+
return poisson_loglik(data["y"], eta[:, 0] + data["offset"])
|
|
652
|
+
if family == "negative_binomial":
|
|
653
|
+
return negative_binomial_loglik(data["y"], eta[:, 0] + data["offset"], phi)
|
|
654
|
+
return extended_survival_loglik(eta, baseline, data)
|
|
655
|
+
except (FloatingPointError, OverflowError, ValueError, np.linalg.LinAlgError):
|
|
656
|
+
return -np.inf
|
|
657
|
+
|
|
658
|
+
current = likelihood(white)
|
|
659
|
+
if not np.isfinite(current):
|
|
660
|
+
raise ValueError("initial structured state has a nonfinite likelihood")
|
|
661
|
+
counters = {
|
|
662
|
+
name: dict(proposed=0, accepted=0, warmup_proposed=0, warmup_accepted=0)
|
|
663
|
+
for name in [*scales, "selection"]
|
|
664
|
+
}
|
|
665
|
+
evaluations = 1
|
|
666
|
+
saved = 0
|
|
667
|
+
|
|
668
|
+
def accept(name, old_ll, new_ll, extra, warm):
|
|
669
|
+
record = counters[name]
|
|
670
|
+
prefix = "warmup_" if warm else ""
|
|
671
|
+
record[prefix + "proposed"] += 1
|
|
672
|
+
ok = np.isfinite(new_ll) and math.log1p(-float(rng.random())) < new_ll - old_ll + extra
|
|
673
|
+
if ok:
|
|
674
|
+
record[prefix + "accepted"] += 1
|
|
675
|
+
return ok
|
|
676
|
+
|
|
677
|
+
for iteration in range(warmup + draws * thin):
|
|
678
|
+
warm = iteration < warmup
|
|
679
|
+
white, current, calls = elliptical_slice(white, likelihood, rng, current_loglik=current)
|
|
680
|
+
evaluations += calls
|
|
681
|
+
for cause in range(k):
|
|
682
|
+
candidate, jacobian = _log_proposal(amplitude[cause], scales["lambda"], rng)
|
|
683
|
+
if np.isfinite(candidate) and candidate > 0:
|
|
684
|
+
proposed = amplitude.copy()
|
|
685
|
+
proposed[cause] = candidate
|
|
686
|
+
new_ll = likelihood(white, amp=proposed)
|
|
687
|
+
evaluations += 1
|
|
688
|
+
extra = (
|
|
689
|
+
gamma_logpdf(candidate, prior.lambda_mean, prior.lambda_sd)
|
|
690
|
+
- gamma_logpdf(amplitude[cause], prior.lambda_mean, prior.lambda_sd)
|
|
691
|
+
+ jacobian
|
|
692
|
+
)
|
|
693
|
+
if accept("lambda", current, new_ll, extra, warm):
|
|
694
|
+
amplitude, current = proposed, new_ll
|
|
695
|
+
if fixed is None:
|
|
696
|
+
for j in np.flatnonzero(delta):
|
|
697
|
+
candidate, jacobian = _log_proposal(r[j], scales["r"], rng)
|
|
698
|
+
if not np.isfinite(prior.log_r(candidate)):
|
|
699
|
+
continue
|
|
700
|
+
proposed = r.copy()
|
|
701
|
+
proposed[j] = candidate
|
|
702
|
+
try:
|
|
703
|
+
new_factor = basis_matrix(data["Z"], proposed, basis)
|
|
704
|
+
except (FloatingPointError, OverflowError, np.linalg.LinAlgError):
|
|
705
|
+
continue
|
|
706
|
+
new_ll = likelihood(white, f=new_factor)
|
|
707
|
+
evaluations += 1
|
|
708
|
+
if accept(
|
|
709
|
+
"r",
|
|
710
|
+
current,
|
|
711
|
+
new_ll,
|
|
712
|
+
prior.log_r(candidate) - prior.log_r(r[j]) + jacobian,
|
|
713
|
+
warm,
|
|
714
|
+
):
|
|
715
|
+
r, factor, current = proposed, new_factor, new_ll
|
|
716
|
+
for members in units:
|
|
717
|
+
proposed_delta = delta.copy()
|
|
718
|
+
proposed_delta[members] = False
|
|
719
|
+
proposed_r = r.copy()
|
|
720
|
+
proposed_r[members] = 0
|
|
721
|
+
if rng.random() < pi:
|
|
722
|
+
j = int(rng.choice(members))
|
|
723
|
+
proposed_delta[j] = True
|
|
724
|
+
proposed_r[j] = prior.sample_r(rng)
|
|
725
|
+
if np.array_equal(proposed_r, r):
|
|
726
|
+
continue
|
|
727
|
+
new_factor = basis_matrix(data["Z"], proposed_r, basis)
|
|
728
|
+
new_ll = likelihood(white, f=new_factor)
|
|
729
|
+
evaluations += 1
|
|
730
|
+
# The proposal draws this unit from its conditional selection/relevance prior.
|
|
731
|
+
if accept("selection", current, new_ll, 0.0, warm):
|
|
732
|
+
r, delta, factor, current = proposed_r, proposed_delta, new_factor, new_ll
|
|
733
|
+
if units:
|
|
734
|
+
active = sum(bool(delta[u].any()) for u in units)
|
|
735
|
+
pi = float(
|
|
736
|
+
rng.beta(
|
|
737
|
+
prior.inclusion_alpha + active, prior.inclusion_beta + len(units) - active
|
|
738
|
+
)
|
|
739
|
+
)
|
|
740
|
+
if family == "gaussian":
|
|
741
|
+
_, _, eta, _, _, _, logvar, _, blocks = components(white)
|
|
742
|
+
residual = _standardized_residual(data["y"], eta[:, 0], 1.0, logvar)
|
|
743
|
+
if blocks:
|
|
744
|
+
quadratic = 0.0
|
|
745
|
+
for indices, _, lower in blocks:
|
|
746
|
+
transformed = solve_triangular(
|
|
747
|
+
lower, residual[indices], lower=True, check_finite=False
|
|
748
|
+
)
|
|
749
|
+
quadratic += float(transformed @ transformed)
|
|
750
|
+
else:
|
|
751
|
+
quadratic = float(residual @ residual)
|
|
752
|
+
sigma2 = 1 / float(
|
|
753
|
+
rng.gamma(prior.sigma2_shape + n / 2, 1 / (prior.sigma2_rate + quadratic / 2))
|
|
754
|
+
)
|
|
755
|
+
current = likelihood(white)
|
|
756
|
+
evaluations += 1
|
|
757
|
+
if family == "negative_binomial":
|
|
758
|
+
candidate, jacobian = _log_proposal(phi, scales["dispersion"], rng)
|
|
759
|
+
old = phi
|
|
760
|
+
phi = candidate
|
|
761
|
+
new_ll = likelihood(white)
|
|
762
|
+
evaluations += 1
|
|
763
|
+
extra = (
|
|
764
|
+
(
|
|
765
|
+
(prior.dispersion_shape - 1) * (math.log(candidate) - math.log(old))
|
|
766
|
+
- prior.dispersion_rate * (candidate - old)
|
|
767
|
+
+ jacobian
|
|
768
|
+
)
|
|
769
|
+
if np.isfinite(candidate) and candidate > 0
|
|
770
|
+
else -np.inf
|
|
771
|
+
)
|
|
772
|
+
if accept("dispersion", current, new_ll, extra, warm):
|
|
773
|
+
current = new_ll
|
|
774
|
+
else:
|
|
775
|
+
phi = old
|
|
776
|
+
if family == "survival":
|
|
777
|
+
for cause in range(k):
|
|
778
|
+
for j in range(baseline.shape[1]):
|
|
779
|
+
old = baseline[cause, j]
|
|
780
|
+
candidate, jacobian = _log_proposal(old, scales["baseline_hazard"], rng)
|
|
781
|
+
baseline[cause, j] = candidate
|
|
782
|
+
new_ll = likelihood(white)
|
|
783
|
+
evaluations += 1
|
|
784
|
+
extra = (
|
|
785
|
+
(
|
|
786
|
+
(prior.baseline_shape - 1) * (math.log(candidate) - math.log(old))
|
|
787
|
+
- prior.baseline_rate * (candidate - old)
|
|
788
|
+
+ jacobian
|
|
789
|
+
)
|
|
790
|
+
if np.isfinite(candidate) and candidate > 0
|
|
791
|
+
else -np.inf
|
|
792
|
+
)
|
|
793
|
+
if accept("baseline_hazard", current, new_ll, extra, warm):
|
|
794
|
+
current = new_ll
|
|
795
|
+
else:
|
|
796
|
+
baseline[cause, j] = old
|
|
797
|
+
if not np.isfinite(current):
|
|
798
|
+
raise FloatingPointError("structured state left finite likelihood support")
|
|
799
|
+
if not warm and (iteration - warmup + 1) % thin == 0:
|
|
800
|
+
gp, beta, _, coeff, covariance, gamma, _, corr, _ = components(white)
|
|
801
|
+
values = dict(
|
|
802
|
+
beta=beta,
|
|
803
|
+
r=r,
|
|
804
|
+
delta=delta,
|
|
805
|
+
pi=pi,
|
|
806
|
+
sigma2=sigma2,
|
|
807
|
+
gp_weight=layout.get(white, "gp"),
|
|
808
|
+
)
|
|
809
|
+
values["lambda"] = amplitude
|
|
810
|
+
if anchor_rows:
|
|
811
|
+
anchor = (
|
|
812
|
+
gp.T
|
|
813
|
+
if mode == "dense"
|
|
814
|
+
else (
|
|
815
|
+
basis_matrix(basis["knots"], r, basis)
|
|
816
|
+
@ layout.get(white, "gp").T
|
|
817
|
+
* np.sqrt(amplitude)[None, :]
|
|
818
|
+
).T
|
|
819
|
+
)
|
|
820
|
+
values["gp_anchor"] = anchor
|
|
821
|
+
if g:
|
|
822
|
+
values.update(random_coeff=coeff, random_covariance=covariance)
|
|
823
|
+
if gamma.size:
|
|
824
|
+
values["gamma"] = gamma
|
|
825
|
+
if corr_kind in ("ar1", "exponential"):
|
|
826
|
+
values["correlation_parameter"] = corr
|
|
827
|
+
if family == "negative_binomial":
|
|
828
|
+
values["dispersion"] = phi
|
|
829
|
+
if family == "survival":
|
|
830
|
+
values["baseline_hazard"] = baseline
|
|
831
|
+
for name, value in values.items():
|
|
832
|
+
samples[name][chain, saved] = value
|
|
833
|
+
saved += 1
|
|
834
|
+
for counter in counters.values():
|
|
835
|
+
for prefix in ("", "warmup_"):
|
|
836
|
+
count = counter[prefix + "proposed"]
|
|
837
|
+
counter[prefix + "rate"] = counter[prefix + "accepted"] / count if count else None
|
|
838
|
+
chain_info.append(
|
|
839
|
+
dict(
|
|
840
|
+
chain=chain,
|
|
841
|
+
acceptance=counters,
|
|
842
|
+
likelihood_evaluations=evaluations,
|
|
843
|
+
seed_spawn_key=list(stream.spawn_key),
|
|
844
|
+
)
|
|
845
|
+
)
|
|
846
|
+
settings = dict(
|
|
847
|
+
engine="structured",
|
|
848
|
+
family=family,
|
|
849
|
+
draws=draws,
|
|
850
|
+
warmup=warmup,
|
|
851
|
+
chains=chains,
|
|
852
|
+
thin=thin,
|
|
853
|
+
seed=seed,
|
|
854
|
+
standardize=bool(standardize),
|
|
855
|
+
intercept=bool(intercept),
|
|
856
|
+
selection=selection,
|
|
857
|
+
prior=asdict(prior),
|
|
858
|
+
r_fixed=None if fixed is None else fixed.tolist(),
|
|
859
|
+
clustered=bool(g),
|
|
860
|
+
random_scale=random_scale,
|
|
861
|
+
random_covariance_fixed=fixed_random is not None,
|
|
862
|
+
correlation=corr_kind,
|
|
863
|
+
correlation_fixed=not corr_inferred,
|
|
864
|
+
correlation_parameter=correlation_parameter,
|
|
865
|
+
kernel=kernel,
|
|
866
|
+
kernel_alpha=float(kernel_alpha),
|
|
867
|
+
approximation=mode,
|
|
868
|
+
n_causes=k,
|
|
869
|
+
active_cause=1,
|
|
870
|
+
amplitude_convention="direct_tau",
|
|
871
|
+
offset_used=offset is not None,
|
|
872
|
+
proposal_scales=scales,
|
|
873
|
+
beta_prior="normal",
|
|
874
|
+
entrant_cohort_conditional=True,
|
|
875
|
+
)
|
|
876
|
+
info = dict(
|
|
877
|
+
runtime_seconds=time.perf_counter() - started,
|
|
878
|
+
chains=chain_info,
|
|
879
|
+
algorithm="noncentered ESS/log-MH/Gaussian-variance-Gibbs",
|
|
880
|
+
proposal_adaptation=False,
|
|
881
|
+
seed_entropy=int(sequence.entropy),
|
|
882
|
+
warnings=messages,
|
|
883
|
+
)
|
|
884
|
+
from .structured_result import StructuredFit
|
|
885
|
+
|
|
886
|
+
return StructuredFit(data, samples, settings, info)
|