Core¶
The pyem.core subpackage implements the hierarchical Expectation-Maximization
(EM) machinery that pyem.api.EMModel wraps: the outer EM loop, the
subject-level (E-step) optimizer, the group-level (M-step) distribution
families, prior objects, model-comparison utilities, parameter-recovery
diagnostics, and the ModelSpec bundle used to register a model's identity
and entry points. Most users will interact with these through EMModel
rather than calling them directly, but they are documented here for anyone
building custom fitting pipelines or new group-distribution families.
Hierarchical EM loop¶
EMfit is the low-level hierarchical-EM routine: it alternates a parallel
per-subject E-step (single_subject_minimize, MAP estimation against the
current group prior) with a group-level M-step (see
Group M-step families) until a convergence
criterion is met, returning subject-level estimates, inverse-Hessians, the
final group posterior, and per-subject (negative) log-likelihood terms.
EMConfig bundles the loop's tunables — iteration cap, convergence method
and criterion, parallelism, RNG seed, and which M-step family to use.
EMfit ¶
EMfit(all_data: Sequence[Sequence[Any]] | Sequence[DataFrame], objfunc: Callable[..., float], param_names: Sequence[str], *, verbose: int = 1, config: EMConfig | None = None, prior: Prior | None = None, **kwargs) -> dict[str, Any]
Hierarchical EM with MAP. Compatible with legacy signature.
list where item i contains args for objfunc for subject i
(e.g., [choices, rewards]) or a pandas DataFrame for subject i.
objfunc: callable like f(params, *subject_args, prior=None, output='npl') -> float param_names: list of parameter names (nparams)
Source code in pyem/core/em.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | |
EMConfig
dataclass
¶
EMConfig(mstep_maxit: int = 200, convergence_method: ConvergenceMethod = 'sum', convergence_custom: Literal['relative_npl', 'running_average', None] = None, convergence_crit: float = 0.001, convergence_precision: int = 6, njobs: int = -2, optim: OptimConfig = OptimConfig(), seed: int | None = None, mstep: str = 'gaussian')
Subject-level optimizer¶
single_subject_minimize performs the E-step for a single subject: it
minimizes the negative log-posterior (objfunc(...) - logprior) over that
subject's data with scipy.optimize.minimize, retrying from random restarts
until a successful optimization is found or the restart budget
(OptimConfig.max_restarts) is exhausted. OptimConfig holds the optimizer
method, solver options, restart budget, and the scale of the random restart
initializations.
OptimConfig
dataclass
¶
OptimConfig(method: str = 'BFGS', options: dict | None = None, max_restarts: int = 2, x_scale: float = 0.1)
single_subject_minimize ¶
single_subject_minimize(objfunc: ObjectiveFn, obj_args: Iterable[Any], nparams: int, prior: Prior, config: OptimConfig, rng: Generator) -> tuple[np.ndarray, np.ndarray, float, float, bool, OptimizeResult]
Returns (q_est, inv_h, fval, nl_prior, success, result)
Source code in pyem/core/optim.py
Model comparison¶
ModelComparison wraps one or more fitted EMModel instances and computes
per-model fit metrics — log model evidence (LME), integrated BIC (BICint),
and pseudo-R² — via compare(), and can additionally run a full
simulate-and-refit identifiability analysis across models via identify()
(with plot_identifiability() for visualizing the resulting confusion
matrix). compare_models is the underlying function that produces the list
of ComparisonRow results that ModelComparison.compare() turns into a
DataFrame.
compare_models ¶
compare_models(models, model_names: List[str] | None = None, metric_order: Sequence[str] = ('LME', 'BICint', 'R2'), bicint_kwargs: dict | None = None, r2_kwargs: dict | None = None) -> List[ComparisonRow]
Source code in pyem/core/compare.py
ComparisonRow
dataclass
¶
ModelComparison ¶
Class for performing model comparison and identifiability analysis.
Initialize ModelComparison.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
models
|
List[Any]
|
List of EMModel instances or model tuples |
required |
model_names
|
Optional[List[str]]
|
Optional list of model names |
None
|
Source code in pyem/core/compare.py
compare ¶
Run model comparison and return a pandas DataFrame indexed by model name.
Columns
- "LME (largest is best)"
- "BICint (smallest is best)"
- "pseudoR^2 (largest is best)"
Source code in pyem/core/compare.py
identify ¶
identify(mi_inputs: List[str], nrounds: int = 10, nsubjects: int = 100, *, sim_kwargs: dict | None = None, fit_kwargs: dict | None = None, bicint_kwargs: dict | None = None, r2_kwargs: dict | None = None, seed: int | None = None, verbose: int = 0) -> pd.DataFrame
For each round (outer loop), for each EMModel in self.models: - simulate using that model's simulate_func - fit every model to the simulated data using its fit_func
Returns a DataFrame with columns
['Simulated','Estimated','LME','BICint','pseudoR2','bestlme','bestbic','bestR2']
Note
"True" parameters for each round are drawn as raw standard-normal
values and mapped into natural space via each model's
param_xform before simulating. Construct every model with
param_xform set (as you would for .recover()) — without
it, models whose simulate_func validates its natural-space
bounds (e.g. every RW-family model) will raise on the
untransformed Gaussian values.
pseudoR2 requires ntrials_total and noptions (see
:func:pyem.utils.stats.pseudo_r2_from_nll), which have no
defaults. Pass them via r2_kwargs (e.g.
r2_kwargs={"ntrials_total": ntrials*nblocks, "noptions": 2})
or the pseudoR2 column will be all-NaN.
Every model's simulate_func must return a dict of named
arrays (as the RW and Bayes model families do). This is
incompatible with the GLM family (glm_sim, logit_sim,
glm_ar_sim, etc.), whose simulate_func returns an
(X, Y) tuple — using a GLM model here raises ValueError.
Source code in pyem/core/compare.py
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 | |
identifiability_analysis ¶
plot_identifiability ¶
plot_identifiability(df: DataFrame | None = None, metric: str = 'LME', *, nrounds: int | None = None, cmap: str = 'viridis', annotate: bool = True, figsize: tuple = (8, 6)) -> plt.Figure
Plot a heatmap of identifiability as the PROPORTION of rounds that the Estimated model won for each (Simulated, Estimated) pair, using the winner-count fields produced by identify().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame | None
|
DataFrame returned by identify(); if None, uses self.identifiability_matrix |
None
|
metric
|
str
|
which metric's winners to visualize: {"LME","BICint","pseudoR2"} |
'LME'
|
nrounds
|
int | None
|
total rounds per Simulated model (if None, inferred per row by sum of winners) |
None
|
cmap
|
str
|
colormap |
'viridis'
|
annotate
|
bool
|
whether to add cell annotations |
True
|
figsize
|
tuple
|
figure size |
(8, 6)
|
Returns:
| Type | Description |
|---|---|
Figure
|
matplotlib Figure |
Source code in pyem/core/compare.py
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 | |
Group M-step families¶
Each group-distribution family (Gaussian, Laplace, Student-t, Cauchy)
implements the same three-method interface — update(m, inv_h) to run the
empirical-Bayes M-step over subject-level MAP means and inverse-Hessians,
moments(hyper) to return the group mean/variance and a validity flag, and
make_prior(hyper) to build the Prior object fed back into the next
E-step — so EMfit can swap between them via the EMConfig.mstep name.
make_group is the factory that dispatches a family name (and, for
Student-t, a degrees-of-freedom df) to the corresponding class.
make_group ¶
Factory for group-distribution families.
df is ignored when name == "cauchy" (Cauchy is df=1 by definition).
Source code in pyem/core/groupdist.py
GaussianGroup ¶
Gaussian group distribution (the classic MFX M-step).
LaplaceGroup ¶
Laplace group distribution.
StudentTGroup ¶
CauchyGroup ¶
Priors¶
Prior is the minimal protocol (a logpdf(x) method) that every prior
object satisfies; GaussianPrior is the default and is used throughout the
EM loop as the per-iteration regularizer built from the group posterior.
Note: GaussianPrior.sigma holds the per-parameter variance
(sigma², not the standard deviation) — this matches what the empirical-Bayes
group M-step reports and what EMModel.fit(prior_sigma=...) expects, so use
the .variance property alias when you want the intent to read explicitly.
default_prior builds a broad, weakly-informative GaussianPrior (variance
100) for a given number of parameters. UniformPrior, LaplacePrior,
StudentTPrior, and CauchyPrior provide alternative independent priors,
and IndependentPrior composes a list of 1-D priors (one per parameter)
into a single multi-parameter prior.
Prior ¶
GaussianPrior
dataclass
¶
Independent Gaussian prior.
IMPORTANT: sigma holds the per-parameter VARIANCE (sigma^2), not the
standard deviation — logpdf divides (x-mu)**2 by sigma directly.
This matches the empirical-Bayes group M-step, which reports variances, and
the values in EMModel.fit(prior_sigma=...) are likewise variances. Use
the variance alias when you want the intent to be explicit.
default_prior ¶
UniformPrior
dataclass
¶
Independent bounded-uniform prior. logpdf = -sum(log(hi-lo)) inside the box, -inf outside.
LaplacePrior
dataclass
¶
Independent Laplace prior. scale is the diversity b (std = b*sqrt(2)).
StudentTPrior
dataclass
¶
Independent Student-t prior. scale is std-like; df = degrees of freedom.
CauchyPrior
dataclass
¶
Independent Cauchy prior (Student-t df=1). scale is half-width gamma. Cauchy has no finite variance; init_moments returns a large finite fallback.
IndependentPrior
dataclass
¶
Per-parameter composer: one 1-D prior per coordinate.
Parameter recovery¶
parameter_recovery compares ground-truth subject-level parameters against
their EM-estimated counterparts, returning per-parameter Pearson correlation
and RMSE (plus the estimates themselves) bundled in a RecoveryResult
dataclass — the standard diagnostic for validating that a model's parameters
are identifiable from simulated data.
parameter_recovery ¶
Compare true vs. estimated subject-level parameters: return per-parameter Pearson r and RMSE.
Source code in pyem/core/posterior.py
Model specification¶
ModelSpec is a small, self-describing bundle (id, spec dict, description,
and the model's params/sim/fit callables) used to register a model's
identity and entry points in one place. It is purely additive — neither
EMModel nor ModelComparison requires a ModelSpec to function — but
model modules use it to expose a consistent, discoverable interface.
ModelSpec
dataclass
¶
Self-describing bundle for a model's identity and entry points.
id and spec are hand-authored per model (there is no shared
taxonomy to derive them from) — pick whatever short id and descriptive
dict shape makes sense for that model family. Purely additive: nothing
in :class:pyem.api.EMModel or :class:pyem.core.compare.ModelComparison
requires a ModelSpec to function.