openpls-engine 1.2.0__tar.gz → 1.4.0__tar.gz

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.
Files changed (82) hide show
  1. openpls_engine-1.4.0/PKG-INFO +165 -0
  2. openpls_engine-1.4.0/README.md +134 -0
  3. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/bootstrap.py +51 -0
  4. openpls_engine-1.4.0/openpls/f_squared.py +121 -0
  5. openpls_engine-1.4.0/openpls/fornell_larcker.py +125 -0
  6. openpls_engine-1.4.0/openpls/higher_order.py +291 -0
  7. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/plspm.py +177 -0
  8. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/predict.py +146 -10
  9. openpls_engine-1.4.0/openpls/report.py +181 -0
  10. openpls_engine-1.4.0/openpls/specific_indirect.py +231 -0
  11. openpls_engine-1.4.0/openpls_engine.egg-info/PKG-INFO +165 -0
  12. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls_engine.egg-info/SOURCES.txt +10 -0
  13. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/pyproject.toml +2 -2
  14. openpls_engine-1.4.0/tests/test_f_squared.py +104 -0
  15. openpls_engine-1.4.0/tests/test_fornell_larcker.py +102 -0
  16. openpls_engine-1.4.0/tests/test_higher_order.py +355 -0
  17. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_predict.py +50 -1
  18. openpls_engine-1.4.0/tests/test_report.py +137 -0
  19. openpls_engine-1.4.0/tests/test_specific_indirect.py +158 -0
  20. openpls_engine-1.2.0/PKG-INFO +0 -186
  21. openpls_engine-1.2.0/README.md +0 -155
  22. openpls_engine-1.2.0/openpls_engine.egg-info/PKG-INFO +0 -186
  23. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/LICENSE +0 -0
  24. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/__init__.py +0 -0
  25. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/config.py +0 -0
  26. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/copula.py +0 -0
  27. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/cta.py +0 -0
  28. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/estimator.py +0 -0
  29. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/fimix.py +0 -0
  30. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/fit.py +0 -0
  31. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/htmt.py +0 -0
  32. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/htmt2.py +0 -0
  33. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/inner_model.py +0 -0
  34. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/inner_summary.py +0 -0
  35. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/ipma.py +0 -0
  36. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/long_bootstrap.py +0 -0
  37. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/mga.py +0 -0
  38. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/mode.py +0 -0
  39. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/moderation.py +0 -0
  40. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/outer_model.py +0 -0
  41. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/plsc.py +0 -0
  42. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/q_squared.py +0 -0
  43. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/scale.py +0 -0
  44. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/scheme.py +0 -0
  45. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/unidimensionality.py +0 -0
  46. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/util.py +0 -0
  47. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/vif.py +0 -0
  48. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/weights.py +0 -0
  49. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls_engine.egg-info/dependency_links.txt +0 -0
  50. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls_engine.egg-info/requires.txt +0 -0
  51. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls_engine.egg-info/top_level.txt +0 -0
  52. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/setup.cfg +0 -0
  53. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_config.py +0 -0
  54. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_copula.py +0 -0
  55. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_cta.py +0 -0
  56. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_estimator.py +0 -0
  57. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_fimix.py +0 -0
  58. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_fit.py +0 -0
  59. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_htmt.py +0 -0
  60. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_htmt2.py +0 -0
  61. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_inner_summary_metrics.py +0 -0
  62. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_ipma.py +0 -0
  63. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_long_bootstrap.py +0 -0
  64. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_lv_mv_name_collision.py +0 -0
  65. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_mga.py +0 -0
  66. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_missing_strategy.py +0 -0
  67. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_moderation.py +0 -0
  68. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_path_recovery_synthetic.py +0 -0
  69. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_plsc.py +0 -0
  70. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_q_squared.py +0 -0
  71. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_redundancy_analysis_mode_b.py +0 -0
  72. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_regression_bootstrap.py +0 -0
  73. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_regression_metric.py +0 -0
  74. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_regression_nonmetric.py +0 -0
  75. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_regression_seminr.py +0 -0
  76. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_scheme_equivalence_two_lv.py +0 -0
  77. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_scheme_newton.py +0 -0
  78. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_scheme_pca.py +0 -0
  79. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_sign_convention.py +0 -0
  80. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_unidimensionality_fallback.py +0 -0
  81. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_util.py +0 -0
  82. {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_vif.py +0 -0
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: openpls-engine
3
+ Version: 1.4.0
4
+ Summary: PLS-SEM compute engine, a maintained fork of plspm-python with advanced analyses (PLSc, IPMA, PLSpredict, moderation, FIMIX-PLS, Gaussian-copula endogeneity, disjoint two-stage HOC, specific indirect effects) and extended metrics (SRMR, d_ULS, HTMT, HTMT2, Q², MGA, Cohen f², Fornell-Larcker, publication-ready Report) powering OpenPLS.
5
+ Author-email: Johannes Jacob <johannespjacob@gmail.com>
6
+ License: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/jojacobsen/openpls-engine
8
+ Project-URL: Documentation, https://openpls.app/engine/
9
+ Project-URL: Issues, https://github.com/jojacobsen/openpls-engine/issues
10
+ Project-URL: OpenPLS web app, https://openpls.app
11
+ Project-URL: Upstream (plspm-python), https://github.com/googlecloudplatform/plspm-python
12
+ Keywords: pls-sem,structural-equation-modeling,statistics,pls,sem
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
21
+ Classifier: Intended Audience :: Science/Research
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: pandas>=1.5
26
+ Requires-Dist: numpy>=1.23
27
+ Requires-Dist: scipy>=1.10
28
+ Requires-Dist: statsmodels>=0.14
29
+ Requires-Dist: scikit-learn>=1.2
30
+ Dynamic: license-file
31
+
32
+ <p align="center">
33
+ <img src="assets/logo.svg" alt="openpls engine" width="320">
34
+ </p>
35
+
36
+ <p align="center">
37
+ <strong>A modern, maintained Python engine for Partial Least Squares Structural Equation Modeling (PLS-SEM).</strong>
38
+ </p>
39
+
40
+ <p align="center">
41
+ <a href="https://github.com/jojacobsen/openpls-engine/actions/workflows/ci.yml"><img src="https://github.com/jojacobsen/openpls-engine/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
42
+ <a href="https://pypi.org/project/openpls-engine/"><img src="https://img.shields.io/pypi/v/openpls-engine.svg" alt="PyPI version"></a>
43
+ <a href="https://pypi.org/project/openpls-engine/"><img src="https://img.shields.io/pypi/pyversions/openpls-engine.svg" alt="Python versions"></a>
44
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-GPLv3+-blue.svg" alt="License: GPL-3.0-or-later"></a>
45
+ <a href="https://doi.org/10.5281/zenodo.20509385"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.20509385.svg" alt="DOI"></a>
46
+ <a href="https://doi.org/10.5281/zenodo.20511533"><img src="https://img.shields.io/badge/validation%20data-10.5281%2Fzenodo.20511533-blue" alt="Validation DOI"></a>
47
+ </p>
48
+
49
+ <p align="center">
50
+ <a href="https://openpls.app/engine">Documentation</a>
51
+ &nbsp;·&nbsp;
52
+ <a href="https://openpls.app/engine/quickstart/">Quickstart</a>
53
+ &nbsp;·&nbsp;
54
+ <a href="https://openpls.app/engine/api/">API reference</a>
55
+ &nbsp;·&nbsp;
56
+ <a href="CHANGELOG.md">Changelog</a>
57
+ </p>
58
+
59
+ ---
60
+
61
+ `openpls-engine` is the compute core behind [OpenPLS](https://openpls.app) and a standalone PyPI package. It is a maintained fork of [`plspm-python`](https://github.com/googlecloudplatform/plspm-python) by Jez Humble (Google), with the original algorithm kept intact and modern PLS-SEM reporting, advanced analyses, and two new inner-weighting schemes layered on top.
62
+
63
+ > **Status: stable as of 1.0.0.** Public API follows semver. See [CHANGELOG.md](CHANGELOG.md) for the version history.
64
+
65
+ ## Install
66
+
67
+ ```sh
68
+ pip install openpls-engine
69
+ ```
70
+
71
+ Pin a specific version for reproducible analyses:
72
+
73
+ ```sh
74
+ pip install openpls-engine==1.4.0
75
+ ```
76
+
77
+ Or work from source:
78
+
79
+ ```sh
80
+ git clone https://github.com/jojacobsen/openpls-engine.git
81
+ cd openpls-engine
82
+ python3 -m pip install -e .
83
+ ```
84
+
85
+ ## Quickstart
86
+
87
+ ```py
88
+ import pandas as pd
89
+ from openpls import Plspm
90
+ import openpls.config as c
91
+ from openpls.scheme import Scheme
92
+ from openpls.mode import Mode
93
+
94
+ satisfaction = pd.read_csv("tests/data/satisfaction.csv", index_col=0)
95
+
96
+ structure = c.Structure()
97
+ structure.add_path(["IMAG"], ["EXPE", "SAT", "LOY"])
98
+ structure.add_path(["EXPE"], ["QUAL", "VAL", "SAT"])
99
+ structure.add_path(["QUAL"], ["VAL", "SAT"])
100
+ structure.add_path(["VAL"], ["SAT"])
101
+ structure.add_path(["SAT"], ["LOY"])
102
+
103
+ config = c.Config(structure.path(), scaled=False)
104
+ for lv in ["IMAG", "EXPE", "QUAL", "VAL", "SAT", "LOY"]:
105
+ config.add_lv_with_columns_named(lv, Mode.A, satisfaction, lv.lower())
106
+
107
+ fit = Plspm(satisfaction, config, Scheme.CENTROID)
108
+ print(fit.inner_summary())
109
+ print(fit.path_coefficients())
110
+ print(fit.report().reliability()) # alpha, rho_A, rho_C, AVE
111
+ ```
112
+
113
+ See the [Quickstart guide](https://openpls.app/engine/quickstart/) for the full walkthrough.
114
+
115
+ ## What's inside
116
+
117
+ **Quality criteria** — HTMT and HTMT2 (geometric-mean refinement, Roemer et al. 2021), SRMR, d_ULS, Cronbach α, Dijkstra-Henseler ρ_A and ρ_C, adjusted R², BIC, Stone-Geisser Q² (blindfolding), Cohen f² effect sizes, Fornell-Larcker discriminant validity, per-indicator and per-predictor VIF, CTA-PLS (confirmatory tetrad analysis for reflective measurement).
118
+
119
+ **Advanced analyses** — PLSc (consistent-PLS bias correction, Dijkstra & Henseler 2015), Gaussian-copula endogeneity test (Park & Gupta 2012; Hult et al. 2018), disjoint two-stage higher-order constructs covering all four canonical types (R-R / R-F / F-R / F-F), IPMA (Importance-Performance Map Analysis), PLSpredict with the complete Shmueli et al. 2019 panel (RMSE / MAE / MAPE, in-sample + out-of-sample, PLS + LM benchmark), two-stage moderation, FIMIX-PLS finite-mixture segmentation, specific indirect effects with bootstrap percentile CIs, multi-group analysis with Henseler permutation tests.
120
+
121
+ **Engine internals** — five inner-weighting schemes (Centroid, Factorial, Path plus the new **quasi-Newton/BFGS** and **Lohmöller PCA** schemes), mean-replacement missing-value strategy alongside the upstream casewise default, long-running bootstrap with BCa percentile CIs and progress streaming, publication-ready `Plspm.report()` that bundles every reviewer-standard panel for one-call export.
122
+
123
+ ## Documentation
124
+
125
+ Full docs at [**openpls.app/engine**](https://openpls.app/engine):
126
+
127
+ - [Introduction](https://openpls.app/engine/) — what the library does and who it is for.
128
+ - [Installation](https://openpls.app/engine/installation/) — pip and source installs.
129
+ - [Quickstart](https://openpls.app/engine/quickstart/) — end-to-end fit on the satisfaction dataset.
130
+ - [Core concepts](https://openpls.app/engine/concepts/) — PLS-SEM, Mode A/B, inner-weighting schemes, missing-value strategies.
131
+ - [API reference](https://openpls.app/engine/api/) — the full public surface.
132
+ - [Examples](https://openpls.app/engine/examples/) — runnable snippets for each advanced analysis.
133
+ - [Changelog](https://openpls.app/engine/changelog/) — version history with per-feature notes.
134
+
135
+ ## Development
136
+
137
+ ```sh
138
+ python3 -m venv .venv
139
+ source .venv/bin/activate
140
+ pip install -r requirements-dev.txt
141
+ pip install -e .
142
+
143
+ pytest # run the test suite
144
+ ruff check . # lint
145
+ ```
146
+
147
+ CI runs lint and tests on Python 3.10 through 3.13 against every push and pull request against `main`.
148
+
149
+ ## Versioning
150
+
151
+ `openpls-engine` follows [Semantic Versioning](https://semver.org). The public API is stable as of `1.0.0`. Tagged releases (`vX.Y.Z`) trigger a GitHub Actions workflow that builds the package and publishes it to PyPI via OIDC trusted publishing. The version is the single source of truth in [`pyproject.toml`](pyproject.toml) and is exposed at runtime as `openpls.__version__`. `1.0.0` renamed the import namespace from `plspm` to `openpls`; consumers upgrading from `0.7.x` must rewrite their imports (see CHANGELOG → Breaking).
152
+
153
+ ## Cite
154
+
155
+ If you use `openpls-engine` in academic work, please cite the software via its [Zenodo DOI](https://doi.org/10.5281/zenodo.20509385) and the validation dataset via [10.5281/zenodo.20511533](https://doi.org/10.5281/zenodo.20511533). See [`CITATION.cff`](CITATION.cff) for the structured metadata.
156
+
157
+ ## License
158
+
159
+ GNU General Public License v3.0, see [LICENSE](LICENSE). Inherited from upstream `plspm-python` (also GPL-3.0).
160
+
161
+ ## Attribution
162
+
163
+ This project is a fork of [googlecloudplatform/plspm-python](https://github.com/googlecloudplatform/plspm-python) by Jez Humble. The upstream R package [plspm](https://github.com/gastonstat/plspm) by Gaston Sanchez and the [seminr](https://github.com/sem-in-r/seminr) package by Soumya Ray and Nicholas Danks remain the conceptual references for the algorithm. See [ATTRIBUTION.md](ATTRIBUTION.md) for details.
164
+
165
+ OpenPLS is an independent project and not affiliated with Google.
@@ -0,0 +1,134 @@
1
+ <p align="center">
2
+ <img src="assets/logo.svg" alt="openpls engine" width="320">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <strong>A modern, maintained Python engine for Partial Least Squares Structural Equation Modeling (PLS-SEM).</strong>
7
+ </p>
8
+
9
+ <p align="center">
10
+ <a href="https://github.com/jojacobsen/openpls-engine/actions/workflows/ci.yml"><img src="https://github.com/jojacobsen/openpls-engine/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
11
+ <a href="https://pypi.org/project/openpls-engine/"><img src="https://img.shields.io/pypi/v/openpls-engine.svg" alt="PyPI version"></a>
12
+ <a href="https://pypi.org/project/openpls-engine/"><img src="https://img.shields.io/pypi/pyversions/openpls-engine.svg" alt="Python versions"></a>
13
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-GPLv3+-blue.svg" alt="License: GPL-3.0-or-later"></a>
14
+ <a href="https://doi.org/10.5281/zenodo.20509385"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.20509385.svg" alt="DOI"></a>
15
+ <a href="https://doi.org/10.5281/zenodo.20511533"><img src="https://img.shields.io/badge/validation%20data-10.5281%2Fzenodo.20511533-blue" alt="Validation DOI"></a>
16
+ </p>
17
+
18
+ <p align="center">
19
+ <a href="https://openpls.app/engine">Documentation</a>
20
+ &nbsp;·&nbsp;
21
+ <a href="https://openpls.app/engine/quickstart/">Quickstart</a>
22
+ &nbsp;·&nbsp;
23
+ <a href="https://openpls.app/engine/api/">API reference</a>
24
+ &nbsp;·&nbsp;
25
+ <a href="CHANGELOG.md">Changelog</a>
26
+ </p>
27
+
28
+ ---
29
+
30
+ `openpls-engine` is the compute core behind [OpenPLS](https://openpls.app) and a standalone PyPI package. It is a maintained fork of [`plspm-python`](https://github.com/googlecloudplatform/plspm-python) by Jez Humble (Google), with the original algorithm kept intact and modern PLS-SEM reporting, advanced analyses, and two new inner-weighting schemes layered on top.
31
+
32
+ > **Status: stable as of 1.0.0.** Public API follows semver. See [CHANGELOG.md](CHANGELOG.md) for the version history.
33
+
34
+ ## Install
35
+
36
+ ```sh
37
+ pip install openpls-engine
38
+ ```
39
+
40
+ Pin a specific version for reproducible analyses:
41
+
42
+ ```sh
43
+ pip install openpls-engine==1.4.0
44
+ ```
45
+
46
+ Or work from source:
47
+
48
+ ```sh
49
+ git clone https://github.com/jojacobsen/openpls-engine.git
50
+ cd openpls-engine
51
+ python3 -m pip install -e .
52
+ ```
53
+
54
+ ## Quickstart
55
+
56
+ ```py
57
+ import pandas as pd
58
+ from openpls import Plspm
59
+ import openpls.config as c
60
+ from openpls.scheme import Scheme
61
+ from openpls.mode import Mode
62
+
63
+ satisfaction = pd.read_csv("tests/data/satisfaction.csv", index_col=0)
64
+
65
+ structure = c.Structure()
66
+ structure.add_path(["IMAG"], ["EXPE", "SAT", "LOY"])
67
+ structure.add_path(["EXPE"], ["QUAL", "VAL", "SAT"])
68
+ structure.add_path(["QUAL"], ["VAL", "SAT"])
69
+ structure.add_path(["VAL"], ["SAT"])
70
+ structure.add_path(["SAT"], ["LOY"])
71
+
72
+ config = c.Config(structure.path(), scaled=False)
73
+ for lv in ["IMAG", "EXPE", "QUAL", "VAL", "SAT", "LOY"]:
74
+ config.add_lv_with_columns_named(lv, Mode.A, satisfaction, lv.lower())
75
+
76
+ fit = Plspm(satisfaction, config, Scheme.CENTROID)
77
+ print(fit.inner_summary())
78
+ print(fit.path_coefficients())
79
+ print(fit.report().reliability()) # alpha, rho_A, rho_C, AVE
80
+ ```
81
+
82
+ See the [Quickstart guide](https://openpls.app/engine/quickstart/) for the full walkthrough.
83
+
84
+ ## What's inside
85
+
86
+ **Quality criteria** — HTMT and HTMT2 (geometric-mean refinement, Roemer et al. 2021), SRMR, d_ULS, Cronbach α, Dijkstra-Henseler ρ_A and ρ_C, adjusted R², BIC, Stone-Geisser Q² (blindfolding), Cohen f² effect sizes, Fornell-Larcker discriminant validity, per-indicator and per-predictor VIF, CTA-PLS (confirmatory tetrad analysis for reflective measurement).
87
+
88
+ **Advanced analyses** — PLSc (consistent-PLS bias correction, Dijkstra & Henseler 2015), Gaussian-copula endogeneity test (Park & Gupta 2012; Hult et al. 2018), disjoint two-stage higher-order constructs covering all four canonical types (R-R / R-F / F-R / F-F), IPMA (Importance-Performance Map Analysis), PLSpredict with the complete Shmueli et al. 2019 panel (RMSE / MAE / MAPE, in-sample + out-of-sample, PLS + LM benchmark), two-stage moderation, FIMIX-PLS finite-mixture segmentation, specific indirect effects with bootstrap percentile CIs, multi-group analysis with Henseler permutation tests.
89
+
90
+ **Engine internals** — five inner-weighting schemes (Centroid, Factorial, Path plus the new **quasi-Newton/BFGS** and **Lohmöller PCA** schemes), mean-replacement missing-value strategy alongside the upstream casewise default, long-running bootstrap with BCa percentile CIs and progress streaming, publication-ready `Plspm.report()` that bundles every reviewer-standard panel for one-call export.
91
+
92
+ ## Documentation
93
+
94
+ Full docs at [**openpls.app/engine**](https://openpls.app/engine):
95
+
96
+ - [Introduction](https://openpls.app/engine/) — what the library does and who it is for.
97
+ - [Installation](https://openpls.app/engine/installation/) — pip and source installs.
98
+ - [Quickstart](https://openpls.app/engine/quickstart/) — end-to-end fit on the satisfaction dataset.
99
+ - [Core concepts](https://openpls.app/engine/concepts/) — PLS-SEM, Mode A/B, inner-weighting schemes, missing-value strategies.
100
+ - [API reference](https://openpls.app/engine/api/) — the full public surface.
101
+ - [Examples](https://openpls.app/engine/examples/) — runnable snippets for each advanced analysis.
102
+ - [Changelog](https://openpls.app/engine/changelog/) — version history with per-feature notes.
103
+
104
+ ## Development
105
+
106
+ ```sh
107
+ python3 -m venv .venv
108
+ source .venv/bin/activate
109
+ pip install -r requirements-dev.txt
110
+ pip install -e .
111
+
112
+ pytest # run the test suite
113
+ ruff check . # lint
114
+ ```
115
+
116
+ CI runs lint and tests on Python 3.10 through 3.13 against every push and pull request against `main`.
117
+
118
+ ## Versioning
119
+
120
+ `openpls-engine` follows [Semantic Versioning](https://semver.org). The public API is stable as of `1.0.0`. Tagged releases (`vX.Y.Z`) trigger a GitHub Actions workflow that builds the package and publishes it to PyPI via OIDC trusted publishing. The version is the single source of truth in [`pyproject.toml`](pyproject.toml) and is exposed at runtime as `openpls.__version__`. `1.0.0` renamed the import namespace from `plspm` to `openpls`; consumers upgrading from `0.7.x` must rewrite their imports (see CHANGELOG → Breaking).
121
+
122
+ ## Cite
123
+
124
+ If you use `openpls-engine` in academic work, please cite the software via its [Zenodo DOI](https://doi.org/10.5281/zenodo.20509385) and the validation dataset via [10.5281/zenodo.20511533](https://doi.org/10.5281/zenodo.20511533). See [`CITATION.cff`](CITATION.cff) for the structured metadata.
125
+
126
+ ## License
127
+
128
+ GNU General Public License v3.0, see [LICENSE](LICENSE). Inherited from upstream `plspm-python` (also GPL-3.0).
129
+
130
+ ## Attribution
131
+
132
+ This project is a fork of [googlecloudplatform/plspm-python](https://github.com/googlecloudplatform/plspm-python) by Jez Humble. The upstream R package [plspm](https://github.com/gastonstat/plspm) by Gaston Sanchez and the [seminr](https://github.com/sem-in-r/seminr) package by Soumya Ray and Nicholas Danks remain the conceptual references for the algorithm. See [ATTRIBUTION.md](ATTRIBUTION.md) for details.
133
+
134
+ OpenPLS is an independent project and not affiliated with Google.
@@ -26,6 +26,7 @@ import openpls.config as c
26
26
  import openpls.inner_model as im
27
27
  import openpls.outer_model as om
28
28
  from openpls.estimator import Estimator
29
+ from openpls.specific_indirect import specific_indirect_bootstrap
29
30
  from openpls.weights import WeightsCalculatorFactory
30
31
 
31
32
 
@@ -125,6 +126,11 @@ class Bootstrap:
125
126
  self.__total_effects = _create_summary(total_effects, inner_model.effects().loc[:, "total"])
126
127
  self.__paths = _create_summary(paths, inner_model.effects().loc[:, "direct"])
127
128
  self.__loading = _create_summary(loadings, outer_model.model().loc[:, "loading"])
129
+ # Retain raw per-iteration direct-path samples so specific indirect
130
+ # effects can be computed as chain products after the fact.
131
+ self.__raw_paths = paths.reset_index(drop=True)
132
+ self.__path_coefficients = inner_model.path_coefficients()
133
+ self.__structural_path = (self.__path_coefficients != 0).astype(int)
128
134
 
129
135
  def weights(self) -> pd.DataFrame:
130
136
  """Outer weights calculated from bootstrap validation."""
@@ -145,3 +151,48 @@ class Bootstrap:
145
151
  def loading(self) -> pd.DataFrame:
146
152
  """Loadings of manifest variables calculated from bootstrap validation."""
147
153
  return self.__loading
154
+
155
+ def specific_indirect_effects(
156
+ self,
157
+ source: str,
158
+ target: str,
159
+ through: list[str] | None = None,
160
+ alpha: float = 0.05,
161
+ ) -> pd.DataFrame:
162
+ """Specific indirect effects with bootstrap percentile CIs.
163
+
164
+ Each row reports one mediation chain ``source -> M1 -> ... -> target``
165
+ and the per-iteration product of its path coefficients summarised
166
+ as ``original``, ``mean``, ``std.error``, ``perc.lower``,
167
+ ``perc.upper`` (at level ``1 - alpha``), and the ``t`` statistic
168
+ (original / SE).
169
+
170
+ Args:
171
+ source: source LV name.
172
+ target: target LV name.
173
+ through: explicit mediator chain (e.g. ``["M1", "M2"]``) to
174
+ evaluate. If ``None`` (default), every indirect chain
175
+ from ``source`` to ``target`` in the structural model is
176
+ returned.
177
+ alpha: two-sided significance level for the percentile CI
178
+ (default 0.05 → 95% CI).
179
+
180
+ Returns:
181
+ DataFrame indexed by chain label (e.g. ``"A -> M -> Y"``).
182
+
183
+ Raises:
184
+ ValueError: if ``source == target``, if no indirect chain
185
+ exists, if ``through`` references nonexistent direct
186
+ edges, or if ``alpha`` is outside ``(0, 1)``.
187
+ KeyError: if ``source`` or ``target`` is not a latent
188
+ variable in the model.
189
+ """
190
+ return specific_indirect_bootstrap(
191
+ self.__raw_paths,
192
+ self.__path_coefficients,
193
+ self.__structural_path,
194
+ source,
195
+ target,
196
+ through=through,
197
+ alpha=alpha,
198
+ )
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/python3
2
+ #
3
+ # Copyright (C) 2026 Johannes Jacob
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ """Cohen's f² effect size for structural-model predictors.
19
+
20
+ For every directed edge ``predictor -> endogenous`` in the structural
21
+ model, ``f² = (R²_full − R²_reduced) / (1 − R²_full)`` measures the
22
+ proportionate change in R² when the predictor is removed (Cohen 1988;
23
+ Hair, Hult, Ringle & Sarstedt 2022). Conventional thresholds:
24
+
25
+ - ``f² >= 0.02`` — small effect
26
+ - ``f² >= 0.15`` — medium effect
27
+ - ``f² >= 0.35`` — large effect
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import numpy as np
33
+ import pandas as pd
34
+ import statsmodels.api as sm
35
+
36
+ from openpls.config import Config
37
+
38
+
39
+ def _effect_size(value: float) -> str:
40
+ if not np.isfinite(value) or value < 0.02:
41
+ return "none"
42
+ if value < 0.15:
43
+ return "small"
44
+ if value < 0.35:
45
+ return "medium"
46
+ return "large"
47
+
48
+
49
+ class FSquared:
50
+ """Cohen's f² effect size per structural-model edge.
51
+
52
+ Constructed lazily on first call to :meth:`~openpls.Plspm.f_squared`.
53
+ For each endogenous LV and each predictor in its structural equation,
54
+ the LV's OLS is refit on the standardized scores with that predictor
55
+ removed; the resulting reduced R² is combined with the full R² to
56
+ give f². Endogenous LVs with a single predictor return f² = R²_full /
57
+ (1 − R²_full) (reduced model is intercept-only on a standardized
58
+ response, R²_red = 0).
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ config: Config,
64
+ scores: pd.DataFrame,
65
+ path_coefficients: pd.DataFrame,
66
+ r_squared: pd.Series,
67
+ ):
68
+ path = config.path()
69
+ rows = []
70
+ endogenous = path.sum(axis=1).astype(bool)
71
+ endogenous_lvs = list(endogenous[endogenous].index)
72
+ for lv in endogenous_lvs:
73
+ predictors = list(path.loc[lv][path.loc[lv] == 1].index)
74
+ r2_full = float(r_squared.loc[lv])
75
+ for predictor in predictors:
76
+ reduced = [p for p in predictors if p != predictor]
77
+ if reduced:
78
+ exog = sm.add_constant(scores.loc[:, reduced])
79
+ r2_red = float(sm.OLS(scores.loc[:, lv], exog).fit().rsquared)
80
+ else:
81
+ r2_red = 0.0
82
+ if r2_full >= 1.0 - 1e-12:
83
+ f2 = np.inf
84
+ else:
85
+ f2 = (r2_full - r2_red) / (1.0 - r2_full)
86
+ rows.append({
87
+ "from": predictor,
88
+ "to": lv,
89
+ "r_squared_full": r2_full,
90
+ "r_squared_reduced": r2_red,
91
+ "f_squared": f2,
92
+ "effect_size": _effect_size(f2),
93
+ })
94
+ self.__table = pd.DataFrame(
95
+ rows,
96
+ index=[f"{r['from']} -> {r['to']}" for r in rows],
97
+ )
98
+ self.__matrix = self.__build_matrix(path, self.__table)
99
+
100
+ @staticmethod
101
+ def __build_matrix(path: pd.DataFrame, table: pd.DataFrame) -> pd.DataFrame:
102
+ m = pd.DataFrame(np.nan, index=path.index, columns=path.columns)
103
+ for _, row in table.iterrows():
104
+ m.loc[row["to"], row["from"]] = row["f_squared"]
105
+ return m
106
+
107
+ def table(self) -> pd.DataFrame:
108
+ """Long-format table indexed by ``"predictor -> endogenous"``.
109
+
110
+ Columns: ``from``, ``to``, ``r_squared_full``,
111
+ ``r_squared_reduced``, ``f_squared``, ``effect_size``.
112
+ """
113
+ return self.__table
114
+
115
+ def matrix(self) -> pd.DataFrame:
116
+ """Square matrix of f² values mirroring the path matrix.
117
+
118
+ Rows are targets (endogenous LVs), columns are sources. Cells
119
+ without a structural path are ``NaN``.
120
+ """
121
+ return self.__matrix
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/python3
2
+ #
3
+ # Copyright (C) 2026 Johannes Jacob
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ """Fornell-Larcker discriminant-validity criterion.
19
+
20
+ The classical Fornell and Larcker (1981) check: for each reflective
21
+ (Mode A) latent variable, the square root of its AVE must exceed its
22
+ absolute correlation with every other latent variable. The matrix
23
+ returned here has ``sqrt(AVE)`` on the diagonal and the inter-construct
24
+ correlations off-diagonal. Formative (Mode B) and single-indicator LVs
25
+ receive ``NaN`` on the diagonal because AVE is undefined for them; the
26
+ modern recommendation (Henseler, Ringle and Sarstedt 2015) is to use
27
+ HTMT for discriminant validity, so this criterion is provided alongside
28
+ HTMT, not in place of it.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import numpy as np
34
+ import pandas as pd
35
+
36
+ from openpls.config import Config
37
+ from openpls.mode import Mode
38
+
39
+
40
+ def _ave(config: Config, outer_model: pd.DataFrame) -> pd.Series:
41
+ """AVE per Mode-A LV (matches inner_summary.InnerSummary)."""
42
+ ave = pd.Series(np.nan, index=config.path().index, name="ave")
43
+ for lv in list(config.path()):
44
+ if config.mode(lv) != Mode.A:
45
+ continue
46
+ communality = outer_model.loc[:, "communality"].loc[config.mvs(lv)]
47
+ num = communality.sum()
48
+ denom = num + (1 - communality).sum()
49
+ if denom > 0:
50
+ ave.loc[lv] = num / denom
51
+ return ave
52
+
53
+
54
+ class FornellLarcker:
55
+ """Fornell-Larcker discriminant-validity matrix and verdict.
56
+
57
+ Constructed lazily on first call to
58
+ :meth:`~openpls.Plspm.fornell_larcker`. The criterion is satisfied
59
+ for a reflective LV when the diagonal entry ``sqrt(AVE_lv)`` exceeds
60
+ every absolute off-diagonal entry in the same row.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ config: Config,
66
+ scores: pd.DataFrame,
67
+ outer_model: pd.DataFrame,
68
+ ):
69
+ ave = _ave(config, outer_model)
70
+ lvs = list(config.path().index)
71
+ # Inter-LV correlations from the standardized scores.
72
+ corr = scores.loc[:, lvs].corr()
73
+ matrix = corr.copy()
74
+ for lv in lvs:
75
+ matrix.loc[lv, lv] = float(np.sqrt(ave.loc[lv])) if not np.isnan(ave.loc[lv]) else np.nan
76
+ self.__matrix = matrix
77
+ self.__ave = ave
78
+ self.__summary = self.__build_summary(matrix, ave, lvs)
79
+
80
+ @staticmethod
81
+ def __build_summary(matrix: pd.DataFrame, ave: pd.Series, lvs: list[str]) -> pd.DataFrame:
82
+ rows = []
83
+ for lv in lvs:
84
+ sqrt_ave = matrix.loc[lv, lv]
85
+ if np.isnan(sqrt_ave):
86
+ rows.append({
87
+ "lv": lv,
88
+ "sqrt_ave": np.nan,
89
+ "max_abs_corr": np.nan,
90
+ "passes": pd.NA,
91
+ "note": "no AVE (non Mode-A or single-indicator)",
92
+ })
93
+ continue
94
+ off = matrix.loc[lv, :].drop(lv).abs()
95
+ max_off = float(off.max()) if len(off) else 0.0
96
+ rows.append({
97
+ "lv": lv,
98
+ "sqrt_ave": float(sqrt_ave),
99
+ "max_abs_corr": max_off,
100
+ "passes": bool(sqrt_ave > max_off),
101
+ "note": "",
102
+ })
103
+ return pd.DataFrame(rows).set_index("lv")
104
+
105
+ def matrix(self) -> pd.DataFrame:
106
+ """Square Fornell-Larcker matrix.
107
+
108
+ Diagonal: ``sqrt(AVE)`` per LV (``NaN`` for Mode B / single-
109
+ indicator). Off-diagonal: latent-variable correlations from the
110
+ standardized scores.
111
+ """
112
+ return self.__matrix
113
+
114
+ def ave(self) -> pd.Series:
115
+ """Average Variance Extracted per LV (``NaN`` for Mode B /
116
+ single-indicator)."""
117
+ return self.__ave
118
+
119
+ def summary(self) -> pd.DataFrame:
120
+ """Per-LV discriminant-validity verdict.
121
+
122
+ Columns: ``sqrt_ave``, ``max_abs_corr``, ``passes`` (boolean,
123
+ ``NA`` when AVE is undefined), ``note``.
124
+ """
125
+ return self.__summary