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.
- openpls_engine-1.4.0/PKG-INFO +165 -0
- openpls_engine-1.4.0/README.md +134 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/bootstrap.py +51 -0
- openpls_engine-1.4.0/openpls/f_squared.py +121 -0
- openpls_engine-1.4.0/openpls/fornell_larcker.py +125 -0
- openpls_engine-1.4.0/openpls/higher_order.py +291 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/plspm.py +177 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/predict.py +146 -10
- openpls_engine-1.4.0/openpls/report.py +181 -0
- openpls_engine-1.4.0/openpls/specific_indirect.py +231 -0
- openpls_engine-1.4.0/openpls_engine.egg-info/PKG-INFO +165 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls_engine.egg-info/SOURCES.txt +10 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/pyproject.toml +2 -2
- openpls_engine-1.4.0/tests/test_f_squared.py +104 -0
- openpls_engine-1.4.0/tests/test_fornell_larcker.py +102 -0
- openpls_engine-1.4.0/tests/test_higher_order.py +355 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_predict.py +50 -1
- openpls_engine-1.4.0/tests/test_report.py +137 -0
- openpls_engine-1.4.0/tests/test_specific_indirect.py +158 -0
- openpls_engine-1.2.0/PKG-INFO +0 -186
- openpls_engine-1.2.0/README.md +0 -155
- openpls_engine-1.2.0/openpls_engine.egg-info/PKG-INFO +0 -186
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/LICENSE +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/__init__.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/config.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/copula.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/cta.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/estimator.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/fimix.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/fit.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/htmt.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/htmt2.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/inner_model.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/inner_summary.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/ipma.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/long_bootstrap.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/mga.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/mode.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/moderation.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/outer_model.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/plsc.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/q_squared.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/scale.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/scheme.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/unidimensionality.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/util.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/vif.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls/weights.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls_engine.egg-info/dependency_links.txt +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls_engine.egg-info/requires.txt +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/openpls_engine.egg-info/top_level.txt +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/setup.cfg +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_config.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_copula.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_cta.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_estimator.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_fimix.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_fit.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_htmt.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_htmt2.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_inner_summary_metrics.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_ipma.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_long_bootstrap.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_lv_mv_name_collision.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_mga.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_missing_strategy.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_moderation.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_path_recovery_synthetic.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_plsc.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_q_squared.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_redundancy_analysis_mode_b.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_regression_bootstrap.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_regression_metric.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_regression_nonmetric.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_regression_seminr.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_scheme_equivalence_two_lv.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_scheme_newton.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_scheme_pca.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_sign_convention.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_unidimensionality_fallback.py +0 -0
- {openpls_engine-1.2.0 → openpls_engine-1.4.0}/tests/test_util.py +0 -0
- {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
|
+
·
|
|
52
|
+
<a href="https://openpls.app/engine/quickstart/">Quickstart</a>
|
|
53
|
+
·
|
|
54
|
+
<a href="https://openpls.app/engine/api/">API reference</a>
|
|
55
|
+
·
|
|
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
|
+
·
|
|
21
|
+
<a href="https://openpls.app/engine/quickstart/">Quickstart</a>
|
|
22
|
+
·
|
|
23
|
+
<a href="https://openpls.app/engine/api/">API reference</a>
|
|
24
|
+
·
|
|
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
|