PyProcessMacro 1.0.13__py3-none-any.whl → 2.0.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.
- pyprocessmacro/__init__.py +3 -2
- pyprocessmacro/models.py +67 -30
- pyprocessmacro/process.py +132 -72
- pyprocessmacro/utils.py +34 -16
- {pyprocessmacro-1.0.13.dist-info → pyprocessmacro-2.0.0.dist-info}/METADATA +656 -608
- pyprocessmacro-2.0.0.dist-info/RECORD +9 -0
- {pyprocessmacro-1.0.13.dist-info → pyprocessmacro-2.0.0.dist-info}/WHEEL +1 -1
- {pyprocessmacro-1.0.13.dist-info → pyprocessmacro-2.0.0.dist-info}/top_level.txt +0 -1
- pyprocessmacro/models.pyi +0 -228
- pyprocessmacro/process.pyi +0 -163
- pyprocessmacro/utils.pyi +0 -92
- pyprocessmacro-1.0.13.dist-info/RECORD +0 -14
- tests/__init__.py +0 -0
- tests/test_models_accuracy.py +0 -150
- {pyprocessmacro-1.0.13.dist-info → pyprocessmacro-2.0.0.dist-info}/licenses/LICENSE.txt +0 -0
pyprocessmacro/__init__.py
CHANGED
pyprocessmacro/models.py
CHANGED
|
@@ -16,11 +16,32 @@ from .utils import (
|
|
|
16
16
|
eval_expression,
|
|
17
17
|
bias_corrected_ci,
|
|
18
18
|
z_score,
|
|
19
|
+
t_score,
|
|
19
20
|
percentile_ci,
|
|
20
21
|
find_significance_region,
|
|
22
|
+
ConvergenceError,
|
|
21
23
|
)
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
def _coerce_numeric(df):
|
|
27
|
+
"""
|
|
28
|
+
Convert the columns of a summary table that hold numbers stored as strings back to numbers.
|
|
29
|
+
|
|
30
|
+
The tables are assembled with np.concatenate, which turns every statistic into a string as soon as a
|
|
31
|
+
column of names sits next to it. pd.to_numeric(errors="ignore") used to undo that but was removed in
|
|
32
|
+
pandas 3, so this reproduces its behaviour: convert a column when every entry parses as a number, and
|
|
33
|
+
leave it untouched otherwise.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def convert(column):
|
|
37
|
+
try:
|
|
38
|
+
return pd.to_numeric(column)
|
|
39
|
+
except (ValueError, TypeError):
|
|
40
|
+
return column
|
|
41
|
+
|
|
42
|
+
return df.apply(convert)
|
|
43
|
+
|
|
44
|
+
|
|
24
45
|
class BaseLogit(object):
|
|
25
46
|
"""
|
|
26
47
|
A convenience parent class for the methods used in Logistic models.
|
|
@@ -84,14 +105,26 @@ class BaseLogit(object):
|
|
|
84
105
|
return -self._hessian(params) / self._n_obs
|
|
85
106
|
|
|
86
107
|
oldparams = np.inf
|
|
87
|
-
newparams = np.
|
|
108
|
+
newparams = np.zeros(self._n_vars)
|
|
88
109
|
while iterations < max_iter and np.any(
|
|
89
110
|
np.abs(newparams - oldparams) > tolerance
|
|
90
111
|
):
|
|
91
112
|
oldparams = newparams
|
|
92
|
-
|
|
93
|
-
|
|
113
|
+
try:
|
|
114
|
+
H = hess(oldparams)
|
|
115
|
+
newparams = oldparams - dot(inv(H), score(oldparams))
|
|
116
|
+
except LinAlgError:
|
|
117
|
+
raise ConvergenceError("The Hessian of the logistic regression is singular.")
|
|
94
118
|
iterations += 1
|
|
119
|
+
if not np.all(np.isfinite(newparams)):
|
|
120
|
+
raise ConvergenceError(
|
|
121
|
+
"The logistic regression diverged (the outcome may be perfectly separated)."
|
|
122
|
+
)
|
|
123
|
+
if np.any(np.abs(newparams - oldparams) > tolerance):
|
|
124
|
+
raise ConvergenceError(
|
|
125
|
+
f"The logistic regression did not converge in {max_iter} iterations "
|
|
126
|
+
"(increase 'iterate', relax 'convergence', or check the outcome for separation)."
|
|
127
|
+
)
|
|
95
128
|
return newparams
|
|
96
129
|
|
|
97
130
|
|
|
@@ -267,7 +300,7 @@ class OLSOutcomeModel(BaseOutcomeModel):
|
|
|
267
300
|
resid = y - dot(x, betas)
|
|
268
301
|
mse = (resid ** 2).sum() / df_e
|
|
269
302
|
sse = dot(resid.T, resid) / df_e
|
|
270
|
-
errortype = "
|
|
303
|
+
errortype = self._options.get("cov_type") or ("HC3" if self._options.get("hc3") else "standard")
|
|
271
304
|
if errortype == "standard":
|
|
272
305
|
vcv = np.true_divide(1, n_obs - n_vars) * dot(resid.T, resid) * inv_xx
|
|
273
306
|
elif errortype == "HC0":
|
|
@@ -275,7 +308,7 @@ class OLSOutcomeModel(BaseOutcomeModel):
|
|
|
275
308
|
vcv = dot(dot(dot(inv_xx, x.T) * sq_resid, x), inv_xx)
|
|
276
309
|
elif errortype == "HC1":
|
|
277
310
|
sq_resid = (resid ** 2).squeeze()
|
|
278
|
-
vcv = np.true_divide(n_obs, n_obs - n_vars
|
|
311
|
+
vcv = np.true_divide(n_obs, n_obs - n_vars) * dot( # n_vars counts the constant (#52)
|
|
279
312
|
dot(dot(inv_xx, x.T) * sq_resid, x), inv_xx
|
|
280
313
|
)
|
|
281
314
|
elif errortype == "HC2":
|
|
@@ -297,13 +330,13 @@ class OLSOutcomeModel(BaseOutcomeModel):
|
|
|
297
330
|
t = betas / se
|
|
298
331
|
p = stats.t.sf(np.abs(t), df_e) * 2
|
|
299
332
|
conf = self._options["conf"]
|
|
300
|
-
|
|
333
|
+
tcrit = t_score(conf, df_e) # OLS intervals use the t distribution, as PROCESS does (#40)
|
|
301
334
|
R2 = 1 - resid.var() / y.var()
|
|
302
|
-
adjR2 = 1 - (1 - R2) * ((n_obs - 1) /
|
|
335
|
+
adjR2 = 1 - (1 - R2) * ((n_obs - 1) / df_e) # n_vars already counts the constant (#41)
|
|
303
336
|
F = (R2 / df_r) / ((1 - R2) / df_e)
|
|
304
|
-
F_pval =
|
|
305
|
-
llci = betas - (se *
|
|
306
|
-
ulci = betas + (se *
|
|
337
|
+
F_pval = stats.f.sf(F, df_r, df_e)
|
|
338
|
+
llci = betas - (se * tcrit)
|
|
339
|
+
ulci = betas + (se * tcrit)
|
|
307
340
|
names = [self._symb_to_var.get(x, x) for x in self._exogvars]
|
|
308
341
|
estimation_results = {
|
|
309
342
|
"betas": betas,
|
|
@@ -399,19 +432,19 @@ class LogitOutcomeModel(BaseOutcomeModel, BaseLogit):
|
|
|
399
432
|
|
|
400
433
|
# GOF statistics
|
|
401
434
|
llmodel = self._loglike(betas)
|
|
402
|
-
lmodel = np.exp(llmodel)
|
|
403
435
|
minus2ll = -2 * llmodel
|
|
404
436
|
|
|
405
437
|
null_model = NullLogitModel(self._endog, self._options)
|
|
406
438
|
betas_null = null_model._optimize()
|
|
407
439
|
llnull = null_model._loglike(betas_null)
|
|
408
|
-
lnull = np.exp(llnull)
|
|
409
440
|
|
|
410
441
|
d = 2 * (llmodel - llnull)
|
|
411
442
|
pvalue = stats.chi2.sf(d, self._n_vars - 1)
|
|
412
443
|
mcfadden = 1 - llmodel / llnull
|
|
413
|
-
|
|
414
|
-
|
|
444
|
+
# Likelihood ratios are taken in log space: exp(llnull) underflows to 0 beyond about a
|
|
445
|
+
# thousand observations, which turned both pseudo R-squared into NaN (#42).
|
|
446
|
+
coxsnell = 1 - np.exp(2 * (llnull - llmodel) / self._n_obs)
|
|
447
|
+
nagelkerke = coxsnell / (1 - np.exp(2 * llnull / self._n_obs))
|
|
415
448
|
names = [self._symb_to_var.get(x, x) for x in self._exogvars]
|
|
416
449
|
estimation_results = {
|
|
417
450
|
"betas": betas,
|
|
@@ -631,6 +664,7 @@ class ParallelMediationModel(object):
|
|
|
631
664
|
boot_betas_y = np.empty((n_boots, len(self._exog_terms_y)))
|
|
632
665
|
boot_betas_m = np.empty((self._n_meds, n_boots, len(self._exog_terms_m)))
|
|
633
666
|
n_fail_samples = 0
|
|
667
|
+
max_failures = n_boots # give up once more resamples failed than were requested (#49)
|
|
634
668
|
boot_ind = 0
|
|
635
669
|
sampler = bootstrap_sampler(self._n_obs, seed)
|
|
636
670
|
while boot_ind < n_boots:
|
|
@@ -647,8 +681,14 @@ class ParallelMediationModel(object):
|
|
|
647
681
|
m_b = self._compute_betas_m(m_e, m_x)
|
|
648
682
|
boot_betas_m[j][boot_ind] = m_b
|
|
649
683
|
boot_ind += 1
|
|
650
|
-
except LinAlgError: # Hessian
|
|
684
|
+
except (LinAlgError, ConvergenceError): # X'X or the Hessian is singular, or the logit diverged
|
|
651
685
|
n_fail_samples += 1
|
|
686
|
+
if n_fail_samples > max_failures:
|
|
687
|
+
raise RuntimeError(
|
|
688
|
+
f"{n_fail_samples} bootstrap samples failed to estimate before {n_boots} succeeded. "
|
|
689
|
+
"The model is probably not estimable on resamples of this data (check for separation, "
|
|
690
|
+
"collinearity, or a very small sample)."
|
|
691
|
+
)
|
|
652
692
|
|
|
653
693
|
return boot_betas_y, boot_betas_m, n_fail_samples
|
|
654
694
|
|
|
@@ -1184,8 +1224,7 @@ class ParallelMediationModel(object):
|
|
|
1184
1224
|
rows = np.concatenate([rows_levels, rows_stats], axis=1)
|
|
1185
1225
|
cols = cols_levels + cols_stats
|
|
1186
1226
|
df = pd.DataFrame(rows, columns=cols, index=[""] * rows.shape[0])
|
|
1187
|
-
|
|
1188
|
-
return df.apply(lambda s: pd.to_numeric(s, errors='coerce').fillna(s))
|
|
1227
|
+
return _coerce_numeric(df)
|
|
1189
1228
|
|
|
1190
1229
|
def _simple_ind_effects_wrapper(self):
|
|
1191
1230
|
"""
|
|
@@ -1218,8 +1257,7 @@ class ParallelMediationModel(object):
|
|
|
1218
1257
|
rows = np.concatenate([rows_levels, rows_stats], axis=1)
|
|
1219
1258
|
cols = ["", "Effect", "Boot SE", "BootLLCI", "BootULCI"]
|
|
1220
1259
|
df = pd.DataFrame(rows, columns=cols, index=[""] * rows.shape[0])
|
|
1221
|
-
|
|
1222
|
-
return df.apply(lambda s: pd.to_numeric(s, errors='coerce').fillna(s))
|
|
1260
|
+
return _coerce_numeric(df)
|
|
1223
1261
|
|
|
1224
1262
|
def _MM_index_wrapper(self):
|
|
1225
1263
|
"""
|
|
@@ -1248,8 +1286,7 @@ class ParallelMediationModel(object):
|
|
|
1248
1286
|
rows = np.concatenate([rows_levels, rows_stats], axis=1)
|
|
1249
1287
|
cols = cols_levels + cols_stats
|
|
1250
1288
|
df = pd.DataFrame(rows, columns=cols, index=[""] * rows.shape[0])
|
|
1251
|
-
|
|
1252
|
-
return df.apply(pd.to_numeric, args=["ignore"])
|
|
1289
|
+
return _coerce_numeric(df)
|
|
1253
1290
|
|
|
1254
1291
|
def _PMM_index_wrapper(self):
|
|
1255
1292
|
"""
|
|
@@ -1278,8 +1315,7 @@ class ParallelMediationModel(object):
|
|
|
1278
1315
|
rows = np.concatenate([rows_levels, rows_stats], axis=1)
|
|
1279
1316
|
cols = cols_levels + cols_stats
|
|
1280
1317
|
df = pd.DataFrame(rows, columns=cols, index=[""] * rows.shape[0])
|
|
1281
|
-
|
|
1282
|
-
return df.apply(pd.to_numeric, args=["ignore"])
|
|
1318
|
+
return _coerce_numeric(df)
|
|
1283
1319
|
|
|
1284
1320
|
def _CMM_index_wrapper(self):
|
|
1285
1321
|
"""
|
|
@@ -1328,8 +1364,7 @@ class ParallelMediationModel(object):
|
|
|
1328
1364
|
cols = cols_levels + cols_stats
|
|
1329
1365
|
df = pd.DataFrame(rows, columns=cols, index=[""] * rows.shape[0])
|
|
1330
1366
|
|
|
1331
|
-
|
|
1332
|
-
return df.apply(pd.to_numeric, args=["ignore"])
|
|
1367
|
+
return _coerce_numeric(df)
|
|
1333
1368
|
|
|
1334
1369
|
def _MMM_index_wrapper(self):
|
|
1335
1370
|
"""
|
|
@@ -1356,8 +1391,7 @@ class ParallelMediationModel(object):
|
|
|
1356
1391
|
rows = np.concatenate([rows_levels, rows_stats], axis=1)
|
|
1357
1392
|
cols = cols_levels + cols_stats
|
|
1358
1393
|
df = pd.DataFrame(rows, columns=cols, index=[""] * rows.shape[0])
|
|
1359
|
-
|
|
1360
|
-
return df.apply(pd.to_numeric, args=["ignore"])
|
|
1394
|
+
return _coerce_numeric(df)
|
|
1361
1395
|
|
|
1362
1396
|
def MM_index_summary(self):
|
|
1363
1397
|
if "MM" in self._analysis_list:
|
|
@@ -1572,9 +1606,12 @@ class DirectEffectModel(object):
|
|
|
1572
1606
|
dot(grad, vcv), np.transpose(grad)
|
|
1573
1607
|
) # V(Grad(X)) = Grad(X).V(X).Grad'(X)
|
|
1574
1608
|
se = np.sqrt(var)
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1609
|
+
if self._is_logit:
|
|
1610
|
+
crit = z_score(conf)
|
|
1611
|
+
else: # OLS intervals use the t distribution, as PROCESS does (#40)
|
|
1612
|
+
crit = t_score(conf, self._model.estimation_results["df_e"])
|
|
1613
|
+
llci = betas - (se * crit)
|
|
1614
|
+
ulci = betas + (se * crit)
|
|
1578
1615
|
return betas, se, llci, ulci
|
|
1579
1616
|
|
|
1580
1617
|
def coeff_summary(self):
|
pyprocessmacro/process.py
CHANGED
|
@@ -16,8 +16,6 @@ from .models import (
|
|
|
16
16
|
)
|
|
17
17
|
from .utils import plot_conditional_effects, gen_moderators
|
|
18
18
|
|
|
19
|
-
warnings.simplefilter("default")
|
|
20
|
-
|
|
21
19
|
|
|
22
20
|
class Process(object):
|
|
23
21
|
__var_kws__ = {"x", "m", "w", "z", "v", "q", "y"}
|
|
@@ -31,6 +29,7 @@ class Process(object):
|
|
|
31
29
|
"effsize",
|
|
32
30
|
"jn",
|
|
33
31
|
"hc3",
|
|
32
|
+
"cov_type",
|
|
34
33
|
"controls_in",
|
|
35
34
|
"total",
|
|
36
35
|
"center",
|
|
@@ -564,6 +563,7 @@ class Process(object):
|
|
|
564
563
|
effsize=False,
|
|
565
564
|
jn=False,
|
|
566
565
|
hc3=False,
|
|
566
|
+
cov_type="standard",
|
|
567
567
|
controls=None,
|
|
568
568
|
controls_in="all",
|
|
569
569
|
total=False,
|
|
@@ -601,7 +601,8 @@ class Process(object):
|
|
|
601
601
|
:param boot: int
|
|
602
602
|
The number of bootstrap repetitions for the estimation of the SE and CI in indirect effects.
|
|
603
603
|
:param seed: int
|
|
604
|
-
The seed
|
|
604
|
+
The seed of the bootstrap sampler: an integer between 0 and 2**32 - 1 for reproducible samples,
|
|
605
|
+
or None for a different draw on every run.
|
|
605
606
|
:param conf: int
|
|
606
607
|
A value between 51 and 99, representing the desired level of confidence for the confidence intervals
|
|
607
608
|
:param effsize: bool
|
|
@@ -610,6 +611,11 @@ class Process(object):
|
|
|
610
611
|
If True, the Johnson-Neymann region of significance will be reported.
|
|
611
612
|
:param hc3: bool
|
|
612
613
|
If True, the HC3 estimator will be used for the variance/covariance matrix of the parameters.
|
|
614
|
+
Shorthand for cov_type="HC3".
|
|
615
|
+
:param cov_type: "standard", "HC0", "HC1", "HC2" or "HC3"
|
|
616
|
+
The estimator of the variance/covariance matrix of the OLS parameters: the standard homoskedastic
|
|
617
|
+
estimator, or one of the heteroskedasticity-consistent estimators. Logistic outcome models always
|
|
618
|
+
use the inverse of the Hessian.
|
|
613
619
|
:param controls: list of string
|
|
614
620
|
A list of control variables to include to the model(s).
|
|
615
621
|
:param controls_in: "all", "x_to_m", "all_to_y"
|
|
@@ -642,55 +648,65 @@ class Process(object):
|
|
|
642
648
|
:param precision:
|
|
643
649
|
The number of decimal places to display in the summary of the model results.
|
|
644
650
|
"""
|
|
645
|
-
if
|
|
651
|
+
if mc:
|
|
646
652
|
warnings.warn(
|
|
647
|
-
"The argument 'mc' for Monte-Carlo
|
|
648
|
-
|
|
653
|
+
"The argument 'mc' for Monte-Carlo confidence intervals is not supported; "
|
|
654
|
+
"bootstrap confidence intervals are used.",
|
|
655
|
+
UserWarning,
|
|
656
|
+
stacklevel=2,
|
|
649
657
|
)
|
|
650
658
|
if kwargs.pop("normal", None):
|
|
651
659
|
warnings.warn(
|
|
652
660
|
"The argument 'normal' for normal theory tests is not supported. "
|
|
653
661
|
"Bootstrapped CI are recommended.",
|
|
654
|
-
|
|
662
|
+
UserWarning,
|
|
663
|
+
stacklevel=2,
|
|
655
664
|
)
|
|
656
665
|
if kwargs.pop("varorder", None):
|
|
657
666
|
warnings.warn(
|
|
658
667
|
"The argument 'varorder' for normal theory tests is not supported. "
|
|
659
668
|
"Bootstrapped CI are recommended.",
|
|
660
|
-
|
|
669
|
+
UserWarning,
|
|
670
|
+
stacklevel=2,
|
|
661
671
|
)
|
|
662
672
|
if kwargs.pop("varlist", None):
|
|
663
673
|
warnings.warn(
|
|
664
674
|
"The 'varlist' is not required. To specify controls, use the 'controls' arguments",
|
|
665
|
-
|
|
675
|
+
UserWarning,
|
|
676
|
+
stacklevel=2,
|
|
666
677
|
)
|
|
667
678
|
if kwargs.pop("coeffci", None):
|
|
668
679
|
warnings.warn(
|
|
669
|
-
"The argument 'coeffci' is not supported.",
|
|
680
|
+
"The argument 'coeffci' is not supported.",
|
|
681
|
+
UserWarning,
|
|
682
|
+
stacklevel=2,
|
|
670
683
|
)
|
|
671
684
|
if kwargs.pop("plot", None):
|
|
672
685
|
warnings.warn(
|
|
673
686
|
"The argument 'plot' is not supported. Check the 'plot_conditional_direct_effects() and"
|
|
674
687
|
"'plot_conditional_indirect_effects()' methods instead.",
|
|
675
|
-
|
|
688
|
+
UserWarning,
|
|
689
|
+
stacklevel=2,
|
|
676
690
|
)
|
|
677
691
|
if kwargs.pop("save", None):
|
|
678
692
|
warnings.warn(
|
|
679
693
|
"The argument 'save' is not supported. Call the 'get_bootstrap_estimates() method to recover"
|
|
680
694
|
"the bootstrap samples instead.",
|
|
681
|
-
|
|
695
|
+
UserWarning,
|
|
696
|
+
stacklevel=2,
|
|
682
697
|
)
|
|
683
|
-
if
|
|
698
|
+
if effsize:
|
|
684
699
|
warnings.warn(
|
|
685
|
-
"The argument 'effsize' for effect sizes is not supported
|
|
686
|
-
|
|
687
|
-
|
|
700
|
+
"The argument 'effsize' for effect sizes is not supported and is ignored.",
|
|
701
|
+
UserWarning,
|
|
702
|
+
stacklevel=2,
|
|
688
703
|
)
|
|
689
|
-
if
|
|
704
|
+
if jn:
|
|
690
705
|
warnings.warn(
|
|
691
|
-
"The argument 'jn' for the Johnson-Neyman region of significance is not supported
|
|
692
|
-
"Call the 'floodlight_direct_effect()' and 'floodlight_indirect_effect()' methods instead.",
|
|
693
|
-
|
|
706
|
+
"The argument 'jn' for the Johnson-Neyman region of significance is not supported and is "
|
|
707
|
+
"ignored. Call the 'floodlight_direct_effect()' and 'floodlight_indirect_effect()' methods instead.",
|
|
708
|
+
UserWarning,
|
|
709
|
+
stacklevel=2,
|
|
694
710
|
)
|
|
695
711
|
|
|
696
712
|
if model == 6:
|
|
@@ -720,13 +736,21 @@ class Process(object):
|
|
|
720
736
|
|
|
721
737
|
# Check the congruence between the model specifications, the model number, and the data, and store the final
|
|
722
738
|
# list of variables used
|
|
739
|
+
unknown_kwargs = set(kwargs) - self.__var_kws__
|
|
740
|
+
if unknown_kwargs:
|
|
741
|
+
raise TypeError(
|
|
742
|
+
f"Process() got unexpected keyword argument(s): {', '.join(sorted(unknown_kwargs))}. "
|
|
743
|
+
"Variables are x, y, m, w, z, v and q; check the spelling of the options."
|
|
744
|
+
)
|
|
723
745
|
var_kwargs = {k: v for k, v in kwargs.items() if k in self.__var_kws__}
|
|
724
746
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
747
|
+
# _gen_valid_varlist normalizes every variable argument to a list, so the mediator and
|
|
748
|
+
# outcome names must be read after it runs (#35).
|
|
728
749
|
self.varlist = self._gen_valid_varlist(var_kwargs)
|
|
729
750
|
|
|
751
|
+
self.mediators = list(var_kwargs["m"]) if self.model_num > 3 else []
|
|
752
|
+
self.iv = var_kwargs["y"][0]
|
|
753
|
+
|
|
730
754
|
# Check the number of mediators supplied to the model:
|
|
731
755
|
if self.model_num > 3:
|
|
732
756
|
self.n_meds = len(self.mediators)
|
|
@@ -761,7 +785,8 @@ class Process(object):
|
|
|
761
785
|
self.outcome_models = self._gen_outcome_models()
|
|
762
786
|
|
|
763
787
|
# Rename the dictionary of custom spotlight values, and generating the spotlight values.
|
|
764
|
-
|
|
788
|
+
self._check_moderator_names(modval, "modval")
|
|
789
|
+
modval_symb = {self._var_to_symb[k]: v for k, v in modval.items()}
|
|
765
790
|
self._spotlight_values = self._gen_spotlight_values(modval_symb)
|
|
766
791
|
|
|
767
792
|
# Generate the direct model.
|
|
@@ -777,6 +802,20 @@ class Process(object):
|
|
|
777
802
|
if not suppr_init:
|
|
778
803
|
self._print_init()
|
|
779
804
|
|
|
805
|
+
def _check_moderator_names(self, names, argument):
|
|
806
|
+
"""
|
|
807
|
+
Raise a ValueError if any of the names is not a moderator of the model (#46).
|
|
808
|
+
:param names: iterable of variable names
|
|
809
|
+
:param argument: the name of the argument being validated, for the error message
|
|
810
|
+
"""
|
|
811
|
+
moderators = {self._symb_to_var[s] for s in self._moderators["all"]}
|
|
812
|
+
unknown = [str(n) for n in names if n not in moderators]
|
|
813
|
+
if unknown:
|
|
814
|
+
raise ValueError(
|
|
815
|
+
f"The variable(s) {', '.join(unknown)} in '{argument}' are not moderators of Model "
|
|
816
|
+
f"{self.model_num}. Moderators of this model: {', '.join(sorted(moderators)) or 'none'}."
|
|
817
|
+
)
|
|
818
|
+
|
|
780
819
|
def _gen_valid_options(self, arguments):
|
|
781
820
|
"""
|
|
782
821
|
Validate the arguments specified for the different options used in Process.
|
|
@@ -791,8 +830,8 @@ class Process(object):
|
|
|
791
830
|
"The option 'conf' must be an integer between 50 and 100, exclusive.\n"
|
|
792
831
|
)
|
|
793
832
|
|
|
794
|
-
if not isinstance(seed, int) or (
|
|
795
|
-
errstr += "The option 'seed' must be
|
|
833
|
+
if seed is not None and (not isinstance(seed, (int, np.integer)) or not (0 <= seed <= 2**32 - 1)):
|
|
834
|
+
errstr += "The option 'seed' must be None or an integer between 0 and 2**32 - 1.\n"
|
|
796
835
|
|
|
797
836
|
if options["contrast"] not in [True, False]:
|
|
798
837
|
errstr += "The option 'contrast' must be 'True' or 'False'.\n"
|
|
@@ -802,6 +841,12 @@ class Process(object):
|
|
|
802
841
|
errstr += "The option 'jn' must be 'True' or 'False'.\n"
|
|
803
842
|
if options["hc3"] not in [True, False]:
|
|
804
843
|
errstr += "The option 'hc3' must be 'True' or 'False'.\n"
|
|
844
|
+
if options["cov_type"] not in ["standard", "HC0", "HC1", "HC2", "HC3"]:
|
|
845
|
+
errstr += "The option 'cov_type' must be one of 'standard', 'HC0', 'HC1', 'HC2' or 'HC3'.\n"
|
|
846
|
+
elif options["hc3"] is True:
|
|
847
|
+
if options["cov_type"] not in ["standard", "HC3"]:
|
|
848
|
+
errstr += "The options hc3=True and cov_type disagree; use one or the other.\n"
|
|
849
|
+
options["cov_type"] = "HC3" # hc3 is shorthand for cov_type="HC3" (#52)
|
|
805
850
|
if options["center"] not in [True, False]:
|
|
806
851
|
errstr += "The option 'center' must be 'True' or 'False'.\n"
|
|
807
852
|
if options["quantile"] not in [True, False]:
|
|
@@ -810,7 +855,7 @@ class Process(object):
|
|
|
810
855
|
errstr += "The option 'jn' must be 'True' or 'False'.\n"
|
|
811
856
|
if options["logit"] not in [True, False]:
|
|
812
857
|
errstr += "The option 'logit' must be 'True' or 'False'.\n"
|
|
813
|
-
if options["controls_in"] not in ["all", "
|
|
858
|
+
if options["controls_in"] not in ["all", "x_to_m", "all_to_y"]:
|
|
814
859
|
errstr += "The option 'controls_in' should be one of 'all', 'x_to_m', 'all_to_y'\n"
|
|
815
860
|
if not isinstance(options["modval"], dict):
|
|
816
861
|
errstr += "The option 'modval' must be a dictionary.\n"
|
|
@@ -994,9 +1039,9 @@ class Process(object):
|
|
|
994
1039
|
"""
|
|
995
1040
|
# Subset the data to the columns used in the model
|
|
996
1041
|
data = self._data[self.varlist].copy()
|
|
997
|
-
n_obs_before =
|
|
998
|
-
data = data.dropna().reset_index()
|
|
999
|
-
n_obs_after =
|
|
1042
|
+
n_obs_before = data.shape[0]
|
|
1043
|
+
data = data.dropna().reset_index(drop=True)
|
|
1044
|
+
n_obs_after = data.shape[0] # rows that survived dropna (#44)
|
|
1000
1045
|
n_obs_null = n_obs_before - n_obs_after
|
|
1001
1046
|
|
|
1002
1047
|
# Map each variable name to a unique variable code, and rename the columns in the data.)
|
|
@@ -1222,7 +1267,9 @@ class Process(object):
|
|
|
1222
1267
|
1. If the two moderators are on two different paths (X to M, or M to Y): both CMM and MMM are reported.
|
|
1223
1268
|
2. If the two moderators are on the same path and form a 3-way interaction: both CMM and MMM are reported.
|
|
1224
1269
|
3. If the two moderators are on the same path and do not form a 3-way: the PMM is reported.
|
|
1225
|
-
4. If
|
|
1270
|
+
4. If a moderator is present on both paths (models 58 to 73, 75 and 76): no index is reported, because
|
|
1271
|
+
the indirect effect is not linear in that moderator and the indices assume it is.
|
|
1272
|
+
MM and PMM match PROCESS 2.16. MMM and CMM follow PROCESS 3 (Hayes, 2018), which 2.16 did not report.
|
|
1226
1273
|
|
|
1227
1274
|
This function returns the list of additional analysis to report. If no additional analysis must be performed,
|
|
1228
1275
|
this list is empty.
|
|
@@ -1238,6 +1285,11 @@ class Process(object):
|
|
|
1238
1285
|
if n_mods_ind == 0: # No moderators on indirect path, so no additional analysis.
|
|
1239
1286
|
return []
|
|
1240
1287
|
|
|
1288
|
+
# Rule 4: a moderator on both the X-to-M and the M-to-Y paths makes the indirect effect quadratic in
|
|
1289
|
+
# that moderator, and every index below assumes linearity. PROCESS reports no index then (#43).
|
|
1290
|
+
if self._moderators["x_indirect"] & self._moderators["m"]:
|
|
1291
|
+
return []
|
|
1292
|
+
|
|
1241
1293
|
terms = y_exogvars + m_exogvars
|
|
1242
1294
|
threeway = any(
|
|
1243
1295
|
[1 if len(term.split("*")) == 3 else 0 for term in terms]
|
|
@@ -1267,6 +1319,7 @@ class Process(object):
|
|
|
1267
1319
|
:param path:
|
|
1268
1320
|
:return:
|
|
1269
1321
|
"""
|
|
1322
|
+
self._check_moderator_names(modval, "modval")
|
|
1270
1323
|
modval_symb = {self._var_to_symb[k]: v for k, v in modval.items()}
|
|
1271
1324
|
spotlight_values_symb = self._spotlight_values.copy()
|
|
1272
1325
|
|
|
@@ -1307,17 +1360,28 @@ class Process(object):
|
|
|
1307
1360
|
hue1_values = modval_symb.get(huesymb1,
|
|
1308
1361
|
spotlight_values_symb[huesymb1])
|
|
1309
1362
|
hue2_values = [0]
|
|
1363
|
+
else:
|
|
1364
|
+
raise ValueError(
|
|
1365
|
+
f"'hue' accepts one or two moderator names, got {len(hue)}."
|
|
1366
|
+
)
|
|
1310
1367
|
else:
|
|
1311
1368
|
huevar1 = None
|
|
1312
1369
|
huevar2 = None
|
|
1313
1370
|
hue1_values = [0]
|
|
1314
1371
|
hue2_values = [0]
|
|
1315
1372
|
|
|
1316
|
-
|
|
1317
|
-
|
|
1373
|
+
# Values for the column and row facets (#36)
|
|
1374
|
+
if col is not None:
|
|
1375
|
+
col_symb = self._var_to_symb[col]
|
|
1376
|
+
col_values = modval_symb.get(col_symb, spotlight_values_symb.get(col_symb, [0]))
|
|
1377
|
+
else:
|
|
1378
|
+
col_values = [0]
|
|
1318
1379
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1380
|
+
if row is not None:
|
|
1381
|
+
row_symb = self._var_to_symb[row]
|
|
1382
|
+
row_values = modval_symb.get(row_symb, spotlight_values_symb.get(row_symb, [0]))
|
|
1383
|
+
else:
|
|
1384
|
+
row_values = [0]
|
|
1321
1385
|
|
|
1322
1386
|
mod_names = [x, huevar1, huevar2, col, row]
|
|
1323
1387
|
mod_values = [x_values, hue1_values, hue2_values, col_values, row_values]
|
|
@@ -1337,13 +1401,19 @@ class Process(object):
|
|
|
1337
1401
|
m_var = self._symb_to_var[m]
|
|
1338
1402
|
if modval_parsed.get(m_var) is None:
|
|
1339
1403
|
warnings.warn(
|
|
1340
|
-
f"The moderator {m_var} exerts an influence on the effect
|
|
1341
|
-
|
|
1342
|
-
|
|
1404
|
+
f"The moderator {m_var} exerts an influence on the effect but is not a factor of the graph; "
|
|
1405
|
+
"its value has been set to 0.",
|
|
1406
|
+
UserWarning,
|
|
1407
|
+
stacklevel=3,
|
|
1343
1408
|
)
|
|
1344
1409
|
modval_parsed[m_var] = [0]
|
|
1345
1410
|
return modval_parsed
|
|
1346
1411
|
|
|
1412
|
+
@property
|
|
1413
|
+
def dv(self):
|
|
1414
|
+
"""The name of the dependent variable (the outcome Y). `iv` holds the same value for compatibility."""
|
|
1415
|
+
return self.iv
|
|
1416
|
+
|
|
1347
1417
|
# API
|
|
1348
1418
|
def summary(self):
|
|
1349
1419
|
"""
|
|
@@ -1393,17 +1463,18 @@ class Process(object):
|
|
|
1393
1463
|
boot_betas_y = iem._boot_betas_y
|
|
1394
1464
|
boot_betas_m = iem._boot_betas_m
|
|
1395
1465
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1466
|
+
frames = []
|
|
1467
|
+
df_y = pd.DataFrame(boot_betas_y, columns=[stv[t] for t in iem._exog_terms_y])
|
|
1468
|
+
df_y.insert(0, "OutcomeName", stv["y"])
|
|
1469
|
+
frames.append(df_y)
|
|
1470
|
+
cols_m = [stv[t] for t in iem._exog_terms_m]
|
|
1399
1471
|
for i in range(self.n_meds):
|
|
1400
|
-
cols_m = [stv[t] for t in iem._exog_terms_m]
|
|
1401
1472
|
df_m = pd.DataFrame(boot_betas_m[i], columns=cols_m)
|
|
1402
|
-
df_m
|
|
1403
|
-
|
|
1473
|
+
df_m.insert(0, "OutcomeName", stv[f"m{i + 1}"])
|
|
1474
|
+
frames.append(df_m)
|
|
1475
|
+
# One block per outcome. DataFrame.append was removed in pandas 2.0 (#33).
|
|
1476
|
+
df = pd.concat(frames)
|
|
1404
1477
|
df.index.name = "BootSample"
|
|
1405
|
-
df.insert(0, "OutcomeName", df["___"].values)
|
|
1406
|
-
del df["___"]
|
|
1407
1478
|
return df.reset_index()
|
|
1408
1479
|
|
|
1409
1480
|
def floodlight_indirect_effect(
|
|
@@ -1432,9 +1503,9 @@ class Process(object):
|
|
|
1432
1503
|
if other_modval:
|
|
1433
1504
|
for k, v in other_modval.items():
|
|
1434
1505
|
symb = self._var_to_symb.get(k)
|
|
1435
|
-
if not
|
|
1506
|
+
if not symb:
|
|
1436
1507
|
raise ValueError(
|
|
1437
|
-
f"The variable {
|
|
1508
|
+
f"The variable {k} is not a variable in the model."
|
|
1438
1509
|
)
|
|
1439
1510
|
if isinstance(v, list):
|
|
1440
1511
|
raise ValueError(
|
|
@@ -1502,9 +1573,9 @@ class Process(object):
|
|
|
1502
1573
|
if other_modval:
|
|
1503
1574
|
for k, v in other_modval.items():
|
|
1504
1575
|
symb = self._var_to_symb.get(k)
|
|
1505
|
-
if not
|
|
1576
|
+
if not symb:
|
|
1506
1577
|
raise ValueError(
|
|
1507
|
-
f"The variable {
|
|
1578
|
+
f"The variable {k} is not a variable in the model."
|
|
1508
1579
|
)
|
|
1509
1580
|
if isinstance(v, list):
|
|
1510
1581
|
raise ValueError(
|
|
@@ -1674,14 +1745,15 @@ class Process(object):
|
|
|
1674
1745
|
If 'ci', confidence intervals are drawn at each discrete value of the moderator on the x-axis
|
|
1675
1746
|
If 'none', no confidence interval is drawn.
|
|
1676
1747
|
:param hue_format: string or None
|
|
1677
|
-
By default, the color-
|
|
1748
|
+
By default, the color-codes are labeled:
|
|
1678
1749
|
'Mod1 at val1' if there is one moderator for 'hue'.
|
|
1679
|
-
'
|
|
1680
|
-
Alternatively, a string
|
|
1750
|
+
'Mod1 at val1, Mod2 at val2' if there are two moderators for 'hue'.
|
|
1751
|
+
Alternatively, a format string can be passed. It receives the keyword arguments:
|
|
1681
1752
|
var1 (the name of the first moderator)
|
|
1682
1753
|
var2 (the name of the second moderator, if it exists)
|
|
1683
1754
|
val1 (the value of the first moderator)
|
|
1684
|
-
val2 (the value of the second moderator)
|
|
1755
|
+
val2 (the value of the second moderator, if it exists)
|
|
1756
|
+
hue1 and hue2 are accepted as aliases of val1 and val2.
|
|
1685
1757
|
A valid string would for instance look like this: '{var1} = {val1:.4f}, {var2} = {val2:.4f}'
|
|
1686
1758
|
:param facet_kws: dict
|
|
1687
1759
|
A dictionary of arguments that should be passed to the FacetGrid object (such as sharex, sharey, size,
|
|
@@ -1757,14 +1829,15 @@ class Process(object):
|
|
|
1757
1829
|
If 'ci', confidence intervals are drawn at each discrete value of the moderator on the x-axis
|
|
1758
1830
|
If 'none', no confidence interval is drawn.
|
|
1759
1831
|
:param hue_format: string or None
|
|
1760
|
-
By default, the color-
|
|
1761
|
-
'Mod1 at val1' if there is
|
|
1762
|
-
'
|
|
1763
|
-
Alternatively, a string
|
|
1832
|
+
By default, the color-codes are labeled:
|
|
1833
|
+
'Mod1 at val1' if there is one moderator for 'hue'.
|
|
1834
|
+
'Mod1 at val1, Mod2 at val2' if there are two moderators for 'hue'.
|
|
1835
|
+
Alternatively, a format string can be passed. It receives the keyword arguments:
|
|
1764
1836
|
var1 (the name of the first moderator)
|
|
1765
1837
|
var2 (the name of the second moderator, if it exists)
|
|
1766
1838
|
val1 (the value of the first moderator)
|
|
1767
|
-
val2 (the value of the second moderator)
|
|
1839
|
+
val2 (the value of the second moderator, if it exists)
|
|
1840
|
+
hue1 and hue2 are accepted as aliases of val1 and val2.
|
|
1768
1841
|
A valid string would for instance look like this: '{var1} = {val1:.4f}, {var2} = {val2:.4f}'
|
|
1769
1842
|
:param facet_kws: dict
|
|
1770
1843
|
A dictionary of arguments that should be passed to the FacetGrid object (such as sharex, sharey, size,
|
|
@@ -1800,16 +1873,3 @@ class Process(object):
|
|
|
1800
1873
|
plot_kws,
|
|
1801
1874
|
err_kws,
|
|
1802
1875
|
)
|
|
1803
|
-
|
|
1804
|
-
# DEPRECATED METHODS
|
|
1805
|
-
def plot_indirect_effects(self, *args, **kwargs):
|
|
1806
|
-
raise DeprecationWarning(
|
|
1807
|
-
"The method 'plot_indirect_effects' has been deprecated. Please use the equivalent method named \
|
|
1808
|
-
'plot_conditional_indirect_effects."
|
|
1809
|
-
)
|
|
1810
|
-
|
|
1811
|
-
def plot_direct_effects(self, *args, **kwargs):
|
|
1812
|
-
raise DeprecationWarning(
|
|
1813
|
-
"The method 'plot_direct_effects' has been deprecated. Please use the equivalent method named \
|
|
1814
|
-
'plot_conditional_direct_effects."
|
|
1815
|
-
)
|