Skip to content

Commit 3bed387

Browse files
Make the adjustment_model() priors= escape hatch reachable
The beta_{outcome} guard ran inside _inherit_outcome_priors, which is called before the caller's priors= argument is merged, so the workaround the error message advertised raised the very same error. Split the guard into its own helper that also sees the user's overrides, so an explicit priors={"beta_Y": ...} satisfies it and is used for the reduced equation. Coefficient priors are still not inherited: beta is a vector over the outcome's predictors, and a reduced equation's predictor set can differ from the structural one. Dispersion inheritance is unchanged. Fixes #463. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 61275b3 commit 3bed387

4 files changed

Lines changed: 79 additions & 14 deletions

File tree

pathmc/_model.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -876,8 +876,11 @@ def adjustment_model(
876876
families : dict[str, str] | None
877877
Per-variable families for the reduced model only.
878878
priors : dict | None
879-
Prior overrides for the reduced model, merged with inherited
880-
outcome dispersion priors.
879+
Prior overrides for the reduced equation, merged with the
880+
outcome dispersion priors inherited from this model.
881+
Coefficient priors are never inherited, since the reduced
882+
predictor set can differ from the structural one — pass
883+
``beta_{outcome}`` here to set it on the reduced equation.
881884
882885
Returns
883886
-------

pathmc/adjustment.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -227,20 +227,31 @@ def _validate_reduced_spec(
227227
)
228228

229229

230-
def _inherit_outcome_priors(
231-
parent_priors: dict[str, Any],
230+
def _reject_uninheritable_beta_prior(
232231
construction_priors: dict[str, Any] | None,
232+
user_priors: dict[str, Any] | None,
233233
outcome: str,
234-
) -> dict[str, Any]:
235-
"""Copy outcome dispersion priors from the parent; reject beta overrides."""
234+
) -> None:
235+
"""Raise when a parent beta prior would be silently replaced by defaults."""
236236
beta_key = f"beta_{outcome}"
237-
if construction_priors and beta_key in construction_priors:
238-
raise ValueError(
239-
f"The structural model has a custom prior on '{beta_key}'. "
240-
f"Adjustment models do not inherit coefficient priors. Pass "
241-
f"priors= on adjustment_model() for the reduced equation."
242-
)
237+
if not construction_priors or beta_key not in construction_priors:
238+
return
239+
if user_priors is not None and beta_key in user_priors:
240+
return
241+
raise ValueError(
242+
f"The structural model has a custom prior on '{beta_key}'. "
243+
f"Adjustment models do not inherit coefficient priors because the "
244+
f"reduced predictor set can differ. Pass "
245+
f"priors={{'{beta_key}': ...}} to adjustment_model() to set it on "
246+
f"the reduced equation."
247+
)
248+
243249

250+
def _inherit_outcome_priors(
251+
parent_priors: dict[str, Any],
252+
outcome: str,
253+
) -> dict[str, Any]:
254+
"""Copy outcome dispersion priors from the parent."""
244255
inherited: dict[str, Any] = {}
245256
for suffix in _OUTCOME_PRIOR_SUFFIXES:
246257
key = f"{suffix}_{outcome}"
@@ -395,11 +406,12 @@ def from_path_model(
395406
construction_priors = (
396407
parent._construction.get("priors") if parent._construction else None
397408
)
398-
inherited_priors = _inherit_outcome_priors(
399-
parent._priors,
409+
_reject_uninheritable_beta_prior(
400410
construction_priors,
411+
priors,
401412
outcome_name,
402413
)
414+
inherited_priors = _inherit_outcome_priors(parent._priors, outcome_name)
403415
reduced_defaults = default_priors(
404416
reduced_spec,
405417
families=outcome_families or None,

pathmc/skills/pathmc/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ The DSL is lavaan-inspired:
152152
sets exist, pass `adjustment_set=` explicitly; pathmc does not pick
153153
among them. Pass `data=` when the parent structural model is
154154
data-free. Panel models are not supported on the adjustment path.
155+
Outcome dispersion priors (`sigma_Y`, `nu_Y`, `alpha_disp_Y`) are
156+
inherited from the parent, but coefficient priors are not, because
157+
the reduced predictor set can differ: if the parent set a custom
158+
`beta_Y`, pass `priors={"beta_Y": ...}` to `adjustment_model()`.
155159
10. **`predictions()` / `comparisons()` / `slopes()` share one API on
156160
`PathModel` and `AdjustmentModel`.** On the structural model they
157161
use truncated-factorization g-computation; on

tests/test_adjustment.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,52 @@ def test_user_priors_on_adjustment_model(self, rng):
250250
== 3.0
251251
)
252252

253+
def test_parent_beta_override_satisfied_by_user_priors(self, rng):
254+
df = _fork_df(rng)
255+
model = pathmc.model(
256+
"X ~ Z\nY ~ X + Z",
257+
data=df,
258+
priors={"beta_Y": Prior("Normal", mu=0, sigma=2.0)},
259+
)
260+
adjusted = model.adjustment_model(
261+
"X -> Y",
262+
priors={"beta_Y": Prior("Normal", mu=0, sigma=2.0)},
263+
)
264+
beta = adjusted.outcome_model._priors["beta_Y"].to_dict()
265+
assert beta["dist"] == "Normal"
266+
assert beta["kwargs"]["sigma"] == 2.0
267+
268+
def test_parent_beta_override_without_user_beta_still_raises(self, rng):
269+
df = _fork_df(rng)
270+
model = pathmc.model(
271+
"X ~ Z\nY ~ X + Z",
272+
data=df,
273+
priors={"beta_Y": Prior("Normal", mu=0, sigma=2.0)},
274+
)
275+
with pytest.raises(ValueError, match="beta_Y"):
276+
model.adjustment_model(
277+
"X -> Y",
278+
priors={"sigma_Y": Prior("HalfNormal", sigma=3.0)},
279+
)
280+
281+
def test_user_beta_override_keeps_inherited_dispersion(self, rng):
282+
df = _fork_df(rng)
283+
model = pathmc.model(
284+
"X ~ Z\nY ~ X + Z",
285+
data=df,
286+
priors={
287+
"beta_Y": Prior("Normal", mu=0, sigma=2.0),
288+
"sigma_Y": Prior("HalfNormal", sigma=4.0),
289+
},
290+
)
291+
adjusted = model.adjustment_model(
292+
"X -> Y",
293+
priors={"beta_Y": Prior("Normal", mu=0, sigma=5.0)},
294+
)
295+
priors = adjusted.outcome_model._priors
296+
assert priors["beta_Y"].to_dict()["kwargs"]["sigma"] == 5.0
297+
assert priors["sigma_Y"].to_dict()["kwargs"]["sigma"] == 4.0
298+
253299

254300
class TestInnerModelTypes:
255301
def test_inner_is_pathmodel_with_pymc_model(self, rng):

0 commit comments

Comments
 (0)