Skip to content

Commit 7175646

Browse files
committed
enforce positivity on sigmas in SEM model
Signed-off-by: Nathaniel <[email protected]>
1 parent acc0594 commit 7175646

File tree

2 files changed

+1733
-1876
lines changed

2 files changed

+1733
-1876
lines changed

examples/case_studies/CFA_SEM.ipynb

Lines changed: 1696 additions & 1849 deletions
Large diffs are not rendered by default.

examples/case_studies/CFA_SEM.myst.md

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,7 @@ drivers = [
722722
]
723723
724724
725-
def make_indirect_sem(priors):
725+
def make_indirect_sem(priors, advi=False):
726726
727727
coords = {
728728
"obs": list(range(len(df))),
@@ -741,7 +741,7 @@ def make_indirect_sem(priors):
741741
obs_idx = list(range(len(df)))
742742
with pm.Model(coords=coords) as model:
743743
744-
Psi = pm.InverseGamma("Psi", 5, 10, dims="indicators")
744+
Psi = pm.Gamma("Psi", priors["gamma"], 0.5, dims="indicators")
745745
lambdas_ = pm.Normal(
746746
"lambdas_1", priors["lambda"][0], priors["lambda"][1], dims=("indicators_1")
747747
)
@@ -772,9 +772,8 @@ def make_indirect_sem(priors):
772772
lambdas_5 = pm.Deterministic(
773773
"lambdas5", pt.set_subtensor(lambdas_[0], 1), dims=("indicators_5")
774774
)
775-
tau = pm.Normal("tau", 3, 0.5, dims="indicators")
776775
kappa = 0
777-
sd_dist = pm.Exponential.dist(1.0, shape=2)
776+
sd_dist = pm.Gamma.dist(priors["gamma"], 0.5, shape=2)
778777
chol, _, _ = pm.LKJCholeskyCov(
779778
"chol_cov", n=2, eta=priors["eta"], sd_dist=sd_dist, compute_corr=True
780779
)
@@ -784,25 +783,25 @@ def make_indirect_sem(priors):
784783
# Regression Components
785784
beta_r = pm.Normal("beta_r", 0, priors["beta_r"], dims="latent_regression")
786785
beta_r2 = pm.Normal("beta_r2", 0, priors["beta_r2"], dims="regression")
787-
sd_dist1 = pm.Exponential.dist(1.0, shape=2)
786+
sd_dist1 = pm.Gamma.dist(1, 0.5, shape=2)
788787
resid_chol, _, _ = pm.LKJCholeskyCov(
789788
"resid_chol", n=2, eta=3, sd_dist=sd_dist1, compute_corr=True
790789
)
791-
_ = pm.Deterministic("resid_cov", chol.dot(chol.T))
792-
sigmas_resid = pm.MvNormal("sigmas_resid", kappa, chol=resid_chol)
790+
_ = pm.Deterministic("resid_cov", resid_chol.dot(resid_chol.T))
791+
sigmas_resid = pm.MvNormal("sigmas_resid", 1, chol=resid_chol)
792+
sigma_regr = pm.HalfNormal("sigma_regr", 1)
793793
794794
# SE_ACAD ~ SUP_FRIENDS + SUP_PARENTS
795795
regression_se_acad = pm.Normal(
796796
"regr_se_acad",
797797
beta_r[0] * ksi[obs_idx, 0] + beta_r[1] * ksi[obs_idx, 1],
798-
sigmas_resid[0],
798+
pm.math.abs(sigmas_resid[0]), # ensuring positivity
799799
)
800800
# SE_SOCIAL ~ SUP_FRIENDS + SUP_PARENTS
801-
802801
regression_se_social = pm.Normal(
803802
"regr_se_social",
804803
beta_r[2] * ksi[obs_idx, 0] + beta_r[3] * ksi[obs_idx, 1],
805-
sigmas_resid[1],
804+
pm.math.abs(sigmas_resid[1]), # ensuring positivity
806805
)
807806
808807
# LS ~ SE_ACAD + SE_SOCIAL + SUP_FRIEND + SUP_PARENTS
@@ -812,9 +811,10 @@ def make_indirect_sem(priors):
812811
+ beta_r2[1] * regression_se_social
813812
+ beta_r2[2] * ksi[obs_idx, 0]
814813
+ beta_r2[3] * ksi[obs_idx, 1],
815-
1,
814+
sigma_regr,
816815
)
817816
817+
tau = pm.Normal("tau", 3, 0.5, dims="indicators")
818818
m0 = tau[0] + regression_se_acad * lambdas_1[0]
819819
m1 = tau[1] + regression_se_acad * lambdas_1[1]
820820
m2 = tau[2] + regression_se_acad * lambdas_1[2]
@@ -834,30 +834,36 @@ def make_indirect_sem(priors):
834834
mu = pm.Deterministic(
835835
"mu", pm.math.stack([m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14]).T
836836
)
837+
# independent_residuals_cov = pm.Deterministic('cov_residuals', pt.diag(Psi))
838+
# _ = pm.MvNormal('likelihood', mu, independent_residuals_cov, observed=df[drivers].values)
837839
_ = pm.Normal("likelihood", mu, Psi, observed=df[drivers].values)
838840
839-
idata = pm.sample(
840-
10_000,
841-
chains=4,
842-
nuts_sampler="numpyro",
843-
target_accept=0.99,
844-
tune=2000,
845-
idata_kwargs={"log_likelihood": True},
846-
random_seed=110,
841+
idata = pm.sample_prior_predictive()
842+
idata.extend(
843+
pm.sample(
844+
2000,
845+
chains=4,
846+
nuts_sampler="numpyro",
847+
target_accept=0.99,
848+
tune=1000,
849+
idata_kwargs={"log_likelihood": True},
850+
nuts_sampler_kwargs={"chain_method": "vectorized"},
851+
random_seed=110,
852+
)
847853
)
848854
idata.extend(pm.sample_posterior_predictive(idata))
849855
850856
return model, idata
851857
852858
853859
model_sem0, idata_sem0 = make_indirect_sem(
854-
priors={"eta": 2, "lambda": [1, 1], "beta_r": 0.1, "beta_r2": 0.1}
860+
priors={"eta": 1, "lambda": [1, 2], "beta_r": 3, "beta_r2": 3, "gamma": 1},
855861
)
856862
model_sem1, idata_sem1 = make_indirect_sem(
857-
priors={"eta": 2, "lambda": [1, 1], "beta_r": 0.2, "beta_r2": 0.2}
863+
priors={"eta": 3, "lambda": [1, 2], "beta_r": 2, "beta_r2": 2, "gamma": 2}
858864
)
859865
model_sem2, idata_sem2 = make_indirect_sem(
860-
priors={"eta": 2, "lambda": [1, 1], "beta_r": 0.5, "beta_r2": 0.5}
866+
priors={"eta": 1, "lambda": [1, 2], "beta_r": 10, "beta_r2": 10, "gamma": 3}
861867
)
862868
```
863869

@@ -880,11 +886,15 @@ Residual covariances
880886
SE_Academic ~~ SE_Social
881887
```
882888

889+
+++
890+
891+
Often you will see SEM models with a multivariate normal likelihood term, but here we've specified independent Normal distributions as the model doesn't call for a richly structured covariance matrix on the residuals terms. More complicated models are possible, but it's always a question of how much structure is needed?
892+
883893
```{code-cell} ipython3
884894
pm.model_to_graphviz(model_sem0)
885895
```
886896

887-
It's worth pausing to examine the nature of the dependencies sketched in this diagram. We can see here how we've replaced the simpler measurement model structure and added three regression functions that replace the draws from the multivariate normal $Ksi$. In other words we've expressed a dependency as a series of regressions all within the one model. Next we'll see how the parameter estimates change across our prior specifications for the model. Notice the relative stability of the factor loadings compared to the regression coefficients.
897+
It's worth pausing to examine the nature of the dependencies sketched in this diagram. We can see here how we've replaced the simpler measurement model structure and added three regression functions that replace the draws from the multivariate normal $Ksi$. In other words we've expressed a dependency as a series of regressions all within the one model. Next we'll see how the parameter estimates change across our prior specifications for the model. Notice the relative stability of the factor loadings and regression coefficients indicating a robustness in these parameter estimates.
888898

889899
```{code-cell} ipython3
890900
fig, ax = plt.subplots(figsize=(15, 15))
@@ -949,7 +959,7 @@ Similar diagnostic results hold for the other models. We now continue to assess
949959

950960
### Indirect and Direct Effects
951961

952-
We now turn the additional regression structures that we've encoded into the model graph. First we pull out the regression coefficients
962+
We now turn to the additional regression structures that we've encoded into the model graph. First we pull out the regression coefficients
953963

954964
```{code-cell} ipython3
955965
fig, axs = plt.subplots(1, 2, figsize=(20, 8))
@@ -961,7 +971,7 @@ axs[1].axvline(0, color="red", label="zero-effect")
961971
axs[1].legend();
962972
```
963973

964-
The coefficients indicate a smaller relative weight accorded to the effects of peer support than we see with parental support. This is borne out as we trace out the cumulative causal effects (direct and indirect) through our DAG or chain of regression coefficients.
974+
The coefficients indicate a strong effect of social self-efficacy on life satisfaction, and smaller relative weight accorded to the effects of peer support than we see with parental support. This is borne out as we trace out the cumulative causal effects (direct and indirect) through our DAG or chain of regression coefficients.
965975

966976
```{code-cell} ipython3
967977
def calculate_effects(idata, var="SUP_P"):
@@ -996,7 +1006,7 @@ def calculate_effects(idata, var="SUP_P"):
9961006
)
9971007
```
9981008

999-
Importantly we see here the effect of priors on the implied relationships. As we pull our priors closer to 0 the total effects of parental support is pulled downwards away from .5, while the peer support remains relatively stable ~.10. However it remains clear that the impact of parental support dwarfs the effects due to peer support.
1009+
It remains clear that the impact of parental support dwarfs the effects due to peer support.
10001010

10011011
```{code-cell} ipython3
10021012
summary_p = pd.concat(
@@ -1007,7 +1017,7 @@ summary_p.index = ["SEM0", "SEM1", "SEM2"]
10071017
summary_p
10081018
```
10091019

1010-
The sensitivity of the estimated impact due to parental support varies strongly as a function of our prior on the variances. Here is a substantive example of the role of theory choice in model design. How strongly should believe that parental and peer effects have 0 effect on life-satisfaction? I'm inclined to believe we're too conservative if we try and shrink the effect toward zero and should prefer a less conservative model. However, the example here is not to dispute the issue, but demonstrate the importance of sensitivity checks.
1020+
The sensitivity of the estimated impact due to parental support does not vary strongly as a function of our prior on the variances. However, the example here is not to dispute the issue at hand, but highlight the importance of sensitivity checks. We will not always find consistency of parameter identification across model specifications.
10111021

10121022
```{code-cell} ipython3
10131023
summary_f = pd.concat(

0 commit comments

Comments
 (0)