Skip to content

Commit d8f451c

Browse files
authored
PPE: add alternative family option, remove "method" (#673)
* add families, clean code * fix test
1 parent c3f3580 commit d8f451c

3 files changed

Lines changed: 65 additions & 59 deletions

File tree

preliz/ppls/pymc_io.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
except ModuleNotFoundError:
1515
pass
1616

17+
from preliz.distributions import Gamma, HalfNormal, Normal
1718
from preliz.internal.distribution_helper import get_distributions
19+
from preliz.unidimensional import mle
1820

1921

20-
def back_fitting_pymc(prior, preliz_model, var_info):
22+
def back_fitting_pymc(prior, preliz_model, var_info, new_families=None):
2123
"""
2224
Fit the samples from prior into user provided model's prior.
2325
@@ -32,20 +34,36 @@ def back_fitting_pymc(prior, preliz_model, var_info):
3234
params = []
3335
for i in range(size):
3436
opt_values = prior[rv_name][:, i]
37+
# Not sure how to fit alternative families.
3538
dist = preliz_model[rv_name]
3639
dist._fit_mle(opt_values)
3740
params.append(dist.params)
3841
dist._parametrization(*[np.array(x) for x in zip(*params)])
3942
else:
4043
opt_values = prior[rv_name]
41-
dist = preliz_model[rv_name]
42-
dist._fit_mle(opt_values)
44+
dists = set_families(preliz_model[rv_name], rv_name, new_families)
45+
idx, _ = mle(dists, opt_values, plot=False)
46+
dist = dists[idx[0]]
4347

4448
new_priors[rv_name] = dist
4549

4650
return new_priors
4751

4852

53+
def set_families(dist, var, new_families):
54+
dists = [dist]
55+
if new_families is not None:
56+
if new_families == "auto":
57+
alt = [Normal(), HalfNormal(), Gamma()]
58+
dists += [a for a in alt if dist.__class__.__name__ != a.__class__.__name__]
59+
elif isinstance(new_families, list):
60+
dists += new_families
61+
elif isinstance(new_families, dict):
62+
dists += new_families.get(var, [])
63+
64+
return dists
65+
66+
4967
def compile_mllk(model):
5068
"""
5169
Compile the log-likelihood for a pymc model.

preliz/predictive/ppe.py

Lines changed: 43 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import numpy as np
66

77
from preliz.internal.optimization import optimize_pymc_model
8-
from preliz.ppls.agnostic import back_fitting_idata, get_engine
8+
from preliz.ppls.agnostic import get_engine
99
from preliz.ppls.bambi_io import get_pymc_model, write_bambi_string
1010
from preliz.ppls.pymc_io import (
1111
back_fitting_pymc,
@@ -18,36 +18,39 @@
1818
)
1919

2020

21-
def ppe(model, target, method="projective", engine="auto", random_state=0):
21+
def ppe(model, target, engine="auto", new_families=None, random_state=0):
2222
"""
2323
Prior Predictive Elicitation.
2424
2525
This method is experimental and under development. It does not offers guarantees of
2626
correctness. Use with caution and triple-check the results.
2727
28+
With the projective method we attempt to find a prior that induces
29+
a prior predictive distribution as close as possible to the target distribution
30+
2831
Parameters
2932
----------
3033
model : a probabilistic model
3134
Currently it only works with PyMC model. More PPls coming soon.
32-
method : str
33-
Method used to generate samples that match the target distribution.
34-
Defaults to `"projective"`, another option is `"pathfinder"`.
35-
If `"projective"`, the parameters of the priors are only used to provide an initial
36-
guess for the optimization routine. Thus their effect on the result is smaller than in
37-
traditional Bayesian inference, unless the priors are very vague or very strong.
38-
If `"projective"`, the observed values are ignored, but not their size.
39-
Pathfinder is a variational inference method so the role of the priors and observed values
40-
is what is expected in Bayesian inference.
41-
engine : str
42-
Library used to define the model. Either `"auto"` (default), `"pymc"` or `"bambi"`.
43-
Ig `"auto"`, the library is automatically detected.
4435
target : a PreliZ distribution or list
4536
Instance of a PreliZ distribution or a list of tuples where each tuple contains a PreliZ
4637
distribution and a weight.
4738
This represents the prior predictive distribution **previously** elicited by the user,
4839
possibly using other PreliZ's methods to obtain this distribution, such as maxent,
4940
roulette, quartile, etc.
5041
This should represent the domain-knowledge of the user and not any observed dataset.
42+
engine : str
43+
Library used to define the model. Either `"auto"` (default), `"pymc"` or `"bambi"`.
44+
Ig `"auto"`, the library is automatically detected.
45+
new_families : "auto", list or dict
46+
Defaults to None, the samples are fit to the original prior distribution.
47+
If "auto", the method evaluates the fit to the original prior plus a set of
48+
predefined distributions.
49+
Use a list of PreliZ distribution to specify the alternative distributions
50+
you want to consider.
51+
Use a dict with variables names in ``model`` as keys and a list of PreliZ
52+
distributions as values. This allows to specify alternative distributions
53+
per variable.
5154
random_state : {None, int, numpy.random.Generator, numpy.random.RandomState}
5255
Defaults to 0. Ignored if `method` is `"pathfinder"`.
5356
@@ -73,44 +76,31 @@ def ppe(model, target, method="projective", engine="auto", random_state=0):
7376
preliz_model = extract_preliz_distributions(model)
7477
var_info, num_draws = retrieve_variable_info(model)
7578

76-
# With the projective method we attempt to find a prior that induces
77-
# a prior predictive distribution as close as possible to the target distribution
78-
if method == "projective":
79-
# Initial point for optimization
80-
initial_guess = get_initial_guess(model)
81-
# compile PyMC model
82-
fmodel, old_y_value, obs_rvs = compile_mllk(model)
83-
projection_raveled = optimize_pymc_model(
84-
fmodel,
85-
target,
86-
num_draws,
87-
opt_iterations,
88-
initial_guess,
89-
rng,
90-
)
91-
# restore obs_rvs value in the model
92-
model.rvs_to_values[obs_rvs] = old_y_value
93-
94-
projection_unraveled = unravel_projection(projection_raveled, var_info, opt_iterations)
95-
96-
# Backfit `projected_posterior` into the model's prior-families
97-
projection_backfitted = back_fitting_pymc(projection_unraveled, preliz_model, var_info)
98-
99-
if engine == "bambi":
100-
new_priors = write_bambi_string(projection_backfitted, var_info)
101-
elif engine == "pymc":
102-
new_priors = write_pymc_string(projection_backfitted, var_info)
103-
104-
elif method == "pathfinder":
105-
from pymc_experimental import fit
106-
107-
with model:
108-
idata = fit(method="pathfinder", num_samples=opt_iterations)
109-
110-
projection_backfitted = back_fitting_idata(idata, preliz_model, new_families=False)
111-
if engine == "bambi":
112-
new_priors = write_bambi_string(projection_backfitted, var_info)
113-
elif engine == "pymc":
114-
new_priors = write_pymc_string(projection_backfitted, var_info)
79+
# Initial point for optimization
80+
initial_guess = get_initial_guess(model)
81+
# compile PyMC model
82+
fmodel, old_y_value, obs_rvs = compile_mllk(model)
83+
projection_raveled = optimize_pymc_model(
84+
fmodel,
85+
target,
86+
num_draws,
87+
opt_iterations,
88+
initial_guess,
89+
rng,
90+
)
91+
# restore obs_rvs value in the model
92+
model.rvs_to_values[obs_rvs] = old_y_value
93+
94+
projection_unraveled = unravel_projection(projection_raveled, var_info, opt_iterations)
95+
96+
# Backfit `projected_posterior` into the model's prior-families
97+
projection_backfitted = back_fitting_pymc(
98+
projection_unraveled, preliz_model, var_info, new_families
99+
)
100+
101+
if engine == "bambi":
102+
new_priors = write_bambi_string(projection_backfitted, var_info)
103+
elif engine == "pymc":
104+
new_priors = write_pymc_string(projection_backfitted, var_info)
115105

116106
return new_priors

preliz/tests/test_ppe.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,7 @@ def test_ppe(params):
6868
)
6969
pm.Normal("y", x_idx, z, observed=Y)
7070

71-
new_prior = (
72-
pz.ppe(model, target, method="projective").replace("\x1b[1m", "").replace("\x1b[0m", "")
73-
)
71+
new_prior = pz.ppe(model, target).replace("\x1b[1m", "").replace("\x1b[0m", "")
7472
exec_context = {}
7573
exec(new_prior, globals(), exec_context)
7674
model = exec_context.get("model")

0 commit comments

Comments
 (0)