eemkit 0.1.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.
- eemkit/__init__.py +51 -0
- eemkit/absorbance.py +216 -0
- eemkit/campaigns.py +276 -0
- eemkit/core/__init__.py +1 -0
- eemkit/core/dataset.py +91 -0
- eemkit/core/grid.py +100 -0
- eemkit/core/history.py +78 -0
- eemkit/corrections/__init__.py +44 -0
- eemkit/corrections/_common.py +58 -0
- eemkit/corrections/blank.py +66 -0
- eemkit/corrections/crop.py +33 -0
- eemkit/corrections/inner_filter.py +123 -0
- eemkit/corrections/raman.py +107 -0
- eemkit/corrections/scatter.py +191 -0
- eemkit/corrections/spectral.py +117 -0
- eemkit/errors.py +37 -0
- eemkit/gui/__init__.py +30 -0
- eemkit/gui/__main__.py +5 -0
- eemkit/gui/campaigns_tab.py +275 -0
- eemkit/gui/common.py +395 -0
- eemkit/gui/corrections_tab.py +334 -0
- eemkit/gui/export_tab.py +137 -0
- eemkit/gui/indices_tab.py +228 -0
- eemkit/gui/main_window.py +258 -0
- eemkit/gui/parafac_tab.py +399 -0
- eemkit/gui/project_tab.py +332 -0
- eemkit/gui/state.py +149 -0
- eemkit/gui/theme.py +125 -0
- eemkit/indices.py +256 -0
- eemkit/io/__init__.py +1 -0
- eemkit/io/_cary_csv.py +174 -0
- eemkit/io/cary60.py +120 -0
- eemkit/io/cary_eclipse.py +140 -0
- eemkit/io/discover.py +176 -0
- eemkit/io/loader.py +158 -0
- eemkit/io/pairing.py +115 -0
- eemkit/parafac/__init__.py +28 -0
- eemkit/parafac/_cp.py +125 -0
- eemkit/parafac/diagnostics.py +56 -0
- eemkit/parafac/export.py +54 -0
- eemkit/parafac/model.py +191 -0
- eemkit/parafac/validation.py +111 -0
- eemkit/pipeline.py +560 -0
- eemkit/plot.py +437 -0
- eemkit-0.1.0.dist-info/METADATA +195 -0
- eemkit-0.1.0.dist-info/RECORD +49 -0
- eemkit-0.1.0.dist-info/WHEEL +4 -0
- eemkit-0.1.0.dist-info/entry_points.txt +2 -0
- eemkit-0.1.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""Tab 4: exploratory PARAFAC models, outliers, and the final model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from PySide6.QtCore import Qt
|
|
7
|
+
from PySide6.QtWidgets import (
|
|
8
|
+
QAbstractItemView,
|
|
9
|
+
QComboBox,
|
|
10
|
+
QFormLayout,
|
|
11
|
+
QGroupBox,
|
|
12
|
+
QLabel,
|
|
13
|
+
QLineEdit,
|
|
14
|
+
QListWidget,
|
|
15
|
+
QMessageBox,
|
|
16
|
+
QPushButton,
|
|
17
|
+
QScrollArea,
|
|
18
|
+
QSpinBox,
|
|
19
|
+
QSplitter,
|
|
20
|
+
QTableView,
|
|
21
|
+
QTabWidget,
|
|
22
|
+
QVBoxLayout,
|
|
23
|
+
QWidget,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from eemkit import pipeline, plot
|
|
27
|
+
from eemkit.gui.common import DataFrameModel, PlotPanel, TaskBar, note, pair, primary, spin
|
|
28
|
+
from eemkit.gui.state import Project
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _int_spin(value: int, lo: int, hi: int) -> QSpinBox:
|
|
32
|
+
s = QSpinBox()
|
|
33
|
+
s.setRange(lo, hi)
|
|
34
|
+
s.setValue(int(value))
|
|
35
|
+
return s
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _sci(value: float) -> QLineEdit:
|
|
39
|
+
e = QLineEdit(f"{float(value):g}")
|
|
40
|
+
e.setMaximumWidth(90)
|
|
41
|
+
return e
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _float(edit: QLineEdit) -> float:
|
|
45
|
+
return float(edit.text().strip().replace(",", "."))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ParafacTab(QWidget):
|
|
49
|
+
def __init__(self, project: Project, tasks: TaskBar, parent=None):
|
|
50
|
+
super().__init__(parent)
|
|
51
|
+
self.project, self.tasks = project, tasks
|
|
52
|
+
p = pipeline.DEFAULTS["parafac"]
|
|
53
|
+
|
|
54
|
+
self.r_from = _int_spin(2, 1, 20)
|
|
55
|
+
self.r_to = _int_spin(7, 1, 20)
|
|
56
|
+
self.n_init = _int_spin(p["n_init"], 1, 500)
|
|
57
|
+
self.norm = QComboBox()
|
|
58
|
+
self.norm.addItems(["norm", "none"])
|
|
59
|
+
self.tol = _sci(p["tol"])
|
|
60
|
+
self.max_iter = _int_spin(p["max_iter"], 10, 1_000_000)
|
|
61
|
+
self.seed = _int_spin(p["random_state"], 0, 2**31 - 1)
|
|
62
|
+
g_exp = QGroupBox("Exploratory models (Murphy et al. 2013)")
|
|
63
|
+
f = QFormLayout(g_exp)
|
|
64
|
+
f.addRow("Components", pair(self.r_from, QLabel("to"), self.r_to))
|
|
65
|
+
f.addRow("Random starts", self.n_init)
|
|
66
|
+
f.addRow("Sample scaling", self.norm)
|
|
67
|
+
f.addRow("Convergence (rel. SSE change)", self.tol)
|
|
68
|
+
f.addRow("Max iterations", self.max_iter)
|
|
69
|
+
f.addRow("Random seed", self.seed)
|
|
70
|
+
|
|
71
|
+
sh = p["split_half"]
|
|
72
|
+
self.sh_init = _int_spin(sh["n_init"], 1, 500)
|
|
73
|
+
self.sh_strat = QLineEdit()
|
|
74
|
+
self.sh_strat.setPlaceholderText("e.g. river, campaign")
|
|
75
|
+
self.sh_thr = spin(sh["threshold"], 0.5, 1.0, 0.01, 2)
|
|
76
|
+
self.sh_seed = _int_spin(sh["random_state"], 0, 2**31 - 1)
|
|
77
|
+
g_sh = QGroupBox("Split-half validation (S4C6T3)")
|
|
78
|
+
f = QFormLayout(g_sh)
|
|
79
|
+
f.addRow("Random starts per half", self.sh_init)
|
|
80
|
+
f.addRow("Balance halves by", self.sh_strat)
|
|
81
|
+
f.addRow("Tucker congruence threshold", self.sh_thr)
|
|
82
|
+
f.addRow("Split seed", self.sh_seed)
|
|
83
|
+
|
|
84
|
+
self.excluded = QListWidget()
|
|
85
|
+
self.excluded.setMaximumHeight(110)
|
|
86
|
+
self.b_unexclude = QPushButton("Put back selected")
|
|
87
|
+
self.b_unexclude.clicked.connect(self.unexclude)
|
|
88
|
+
g_exc = QGroupBox("Samples left out of the model")
|
|
89
|
+
v = QVBoxLayout(g_exc)
|
|
90
|
+
v.addWidget(self.excluded)
|
|
91
|
+
v.addWidget(self.b_unexclude)
|
|
92
|
+
v.addWidget(note("Add samples from the Outliers view. Record why in your notes: "
|
|
93
|
+
"the list is saved in the project file."))
|
|
94
|
+
|
|
95
|
+
self.b_explore = primary(QPushButton("Run exploratory models"))
|
|
96
|
+
self.b_explore.clicked.connect(self.explore)
|
|
97
|
+
|
|
98
|
+
fin = p["final"]
|
|
99
|
+
self.f_rank = _int_spin(3, 1, 20)
|
|
100
|
+
self.f_init = _int_spin(fin["n_init"], 1, 500)
|
|
101
|
+
self.f_tol = _sci(fin["tol"])
|
|
102
|
+
self.f_iter = _int_spin(fin["max_iter"], 10, 1_000_000)
|
|
103
|
+
g_fin = QGroupBox("Final model")
|
|
104
|
+
f = QFormLayout(g_fin)
|
|
105
|
+
f.addRow("Components", self.f_rank)
|
|
106
|
+
f.addRow("Random starts", self.f_init)
|
|
107
|
+
f.addRow("Convergence", self.f_tol)
|
|
108
|
+
f.addRow("Max iterations", self.f_iter)
|
|
109
|
+
self.b_final = primary(QPushButton("Fit final model"))
|
|
110
|
+
self.b_final.clicked.connect(self.fit_final)
|
|
111
|
+
f.addRow(self.b_final)
|
|
112
|
+
|
|
113
|
+
form = QWidget()
|
|
114
|
+
fl = QVBoxLayout(form)
|
|
115
|
+
for w in (g_exp, g_sh, g_exc, self.b_explore, g_fin):
|
|
116
|
+
fl.addWidget(w)
|
|
117
|
+
fl.addStretch(1)
|
|
118
|
+
scroll = QScrollArea()
|
|
119
|
+
scroll.setWidget(form)
|
|
120
|
+
scroll.setWidgetResizable(True)
|
|
121
|
+
scroll.setMinimumWidth(360)
|
|
122
|
+
|
|
123
|
+
# results
|
|
124
|
+
self.summary_model = DataFrameModel()
|
|
125
|
+
summary_view = QTableView()
|
|
126
|
+
summary_view.setModel(self.summary_model)
|
|
127
|
+
self.diag_plot = PlotPanel()
|
|
128
|
+
page_summary = QWidget()
|
|
129
|
+
v = QVBoxLayout(page_summary)
|
|
130
|
+
v.addWidget(summary_view, 1)
|
|
131
|
+
v.addWidget(self.diag_plot, 2)
|
|
132
|
+
|
|
133
|
+
self.load_rank = QComboBox()
|
|
134
|
+
self.load_rank.currentIndexChanged.connect(self.draw_loadings)
|
|
135
|
+
self.load_plot = PlotPanel()
|
|
136
|
+
page_load = QWidget()
|
|
137
|
+
v = QVBoxLayout(page_load)
|
|
138
|
+
v.addWidget(pair(QLabel("Model"), self.load_rank))
|
|
139
|
+
v.addWidget(self.load_plot, 1)
|
|
140
|
+
|
|
141
|
+
self.out_rank = QComboBox()
|
|
142
|
+
self.out_rank.currentIndexChanged.connect(self.draw_outliers)
|
|
143
|
+
self.out_by = QComboBox()
|
|
144
|
+
self.out_by.currentIndexChanged.connect(self.draw_outliers)
|
|
145
|
+
self.out_plot = PlotPanel()
|
|
146
|
+
self.out_model = DataFrameModel(float_format="{:.4f}")
|
|
147
|
+
self.out_view = QTableView()
|
|
148
|
+
self.out_view.setModel(self.out_model)
|
|
149
|
+
self.out_view.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
150
|
+
b_exclude = QPushButton("Leave selected samples out")
|
|
151
|
+
b_exclude.clicked.connect(self.exclude_selected)
|
|
152
|
+
page_out = QWidget()
|
|
153
|
+
v = QVBoxLayout(page_out)
|
|
154
|
+
v.addWidget(pair(QLabel("Model"), self.out_rank, QLabel("colour by"), self.out_by))
|
|
155
|
+
h = QSplitter()
|
|
156
|
+
h.addWidget(self.out_plot)
|
|
157
|
+
side = QWidget()
|
|
158
|
+
sv = QVBoxLayout(side)
|
|
159
|
+
sv.setContentsMargins(0, 0, 0, 0)
|
|
160
|
+
sv.addWidget(self.out_view, 1)
|
|
161
|
+
sv.addWidget(b_exclude)
|
|
162
|
+
h.addWidget(side)
|
|
163
|
+
v.addWidget(h, 1)
|
|
164
|
+
|
|
165
|
+
self.final_plot = PlotPanel()
|
|
166
|
+
self.final_text = QLabel("")
|
|
167
|
+
self.final_text.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
|
168
|
+
page_final = QWidget()
|
|
169
|
+
v = QVBoxLayout(page_final)
|
|
170
|
+
v.addWidget(self.final_text)
|
|
171
|
+
v.addWidget(self.final_plot, 1)
|
|
172
|
+
|
|
173
|
+
self.results = QTabWidget()
|
|
174
|
+
self.results.addTab(page_summary, "Number of components")
|
|
175
|
+
self.results.addTab(page_load, "Components by model")
|
|
176
|
+
self.results.addTab(page_out, "Outliers")
|
|
177
|
+
self.results.addTab(page_final, "Final model")
|
|
178
|
+
|
|
179
|
+
split = QSplitter()
|
|
180
|
+
split.addWidget(scroll)
|
|
181
|
+
split.addWidget(self.results)
|
|
182
|
+
split.setStretchFactor(1, 1)
|
|
183
|
+
QVBoxLayout(self).addWidget(split)
|
|
184
|
+
project.changed.connect(self._on_changed)
|
|
185
|
+
tasks.busy_changed.connect(self._busy)
|
|
186
|
+
self.load_settings()
|
|
187
|
+
|
|
188
|
+
# settings ----------------------------------------------------------- #
|
|
189
|
+
|
|
190
|
+
def _busy(self, busy: bool) -> None:
|
|
191
|
+
self.b_explore.setEnabled(not busy)
|
|
192
|
+
self.b_final.setEnabled(not busy)
|
|
193
|
+
|
|
194
|
+
def load_settings(self) -> None:
|
|
195
|
+
p = self.project.settings["parafac"]
|
|
196
|
+
ranks = [int(r) for r in p["ranks"]] or [2, 7]
|
|
197
|
+
self.r_from.setValue(min(ranks))
|
|
198
|
+
self.r_to.setValue(max(ranks))
|
|
199
|
+
self.n_init.setValue(int(p["n_init"]))
|
|
200
|
+
self.norm.setCurrentText(p["normalization"] or "none")
|
|
201
|
+
self.tol.setText(f"{float(p['tol']):g}")
|
|
202
|
+
self.max_iter.setValue(int(p["max_iter"]))
|
|
203
|
+
self.seed.setValue(int(p["random_state"]))
|
|
204
|
+
sh = p["split_half"]
|
|
205
|
+
self.sh_init.setValue(int(sh["n_init"]))
|
|
206
|
+
self.sh_strat.setText(", ".join(sh["stratify"]))
|
|
207
|
+
self.sh_thr.setValue(float(sh["threshold"]))
|
|
208
|
+
self.sh_seed.setValue(int(sh["random_state"]))
|
|
209
|
+
self.excluded.clear()
|
|
210
|
+
self.excluded.addItems([str(s) for s in p["exclude"]])
|
|
211
|
+
fin = p["final"]
|
|
212
|
+
if fin.get("rank"):
|
|
213
|
+
self.f_rank.setValue(int(fin["rank"]))
|
|
214
|
+
self.f_init.setValue(int(fin["n_init"]))
|
|
215
|
+
self.f_tol.setText(f"{float(fin['tol']):g}")
|
|
216
|
+
self.f_iter.setValue(int(fin["max_iter"]))
|
|
217
|
+
|
|
218
|
+
def _exploration_settings(self) -> dict | None:
|
|
219
|
+
try:
|
|
220
|
+
tol = _float(self.tol)
|
|
221
|
+
except ValueError:
|
|
222
|
+
QMessageBox.information(self, "eemkit", "Convergence must be a number, e.g. 1e-6.")
|
|
223
|
+
return None
|
|
224
|
+
if self.r_to.value() < self.r_from.value():
|
|
225
|
+
QMessageBox.information(self, "eemkit", "The component range is empty.")
|
|
226
|
+
return None
|
|
227
|
+
old = self.project.settings["parafac"]
|
|
228
|
+
return {**old, "ranks": list(range(self.r_from.value(), self.r_to.value() + 1)),
|
|
229
|
+
"n_init": self.n_init.value(),
|
|
230
|
+
"normalization": None if self.norm.currentText() == "none" else "norm",
|
|
231
|
+
"tol": tol, "max_iter": self.max_iter.value(), "random_state": self.seed.value(),
|
|
232
|
+
"exclude": [self.excluded.item(i).text() for i in range(self.excluded.count())],
|
|
233
|
+
"split_half": {**old["split_half"], "n_init": self.sh_init.value(),
|
|
234
|
+
"stratify": [s.strip() for s in self.sh_strat.text().split(",")
|
|
235
|
+
if s.strip()],
|
|
236
|
+
"threshold": self.sh_thr.value(),
|
|
237
|
+
"random_state": self.sh_seed.value()}}
|
|
238
|
+
|
|
239
|
+
def _store(self, values: dict) -> None:
|
|
240
|
+
old = self.project.settings["parafac"]
|
|
241
|
+
same_model = {k: v for k, v in old.items() if k != "final"} == \
|
|
242
|
+
{k: v for k, v in values.items() if k != "final"}
|
|
243
|
+
self.project.set_settings("parafac", values, None if same_model else "exploration")
|
|
244
|
+
if values.get("final") != old.get("final") and same_model:
|
|
245
|
+
self.project.invalidate("final")
|
|
246
|
+
|
|
247
|
+
# actions ------------------------------------------------------------ #
|
|
248
|
+
|
|
249
|
+
def _input(self) -> object | None:
|
|
250
|
+
if self.project.corrected is None:
|
|
251
|
+
QMessageBox.information(self, "eemkit", "Apply the corrections first.")
|
|
252
|
+
return None
|
|
253
|
+
try:
|
|
254
|
+
return pipeline.parafac_input(self.project.corrected, self.project.settings)
|
|
255
|
+
except Exception as exc: # noqa: BLE001 - settings problems shown to the user
|
|
256
|
+
QMessageBox.warning(self, "eemkit", str(exc))
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
def explore(self) -> None:
|
|
260
|
+
values = self._exploration_settings()
|
|
261
|
+
if values is None:
|
|
262
|
+
return
|
|
263
|
+
self._store(values)
|
|
264
|
+
ds = self._input()
|
|
265
|
+
if ds is None:
|
|
266
|
+
return
|
|
267
|
+
settings = self.project.settings
|
|
268
|
+
|
|
269
|
+
def work(progress, cancelled):
|
|
270
|
+
return pipeline.explore(ds, settings, progress=progress, cancelled=cancelled)
|
|
271
|
+
|
|
272
|
+
def done(exploration):
|
|
273
|
+
self.project.exploration = exploration
|
|
274
|
+
self.project.changed.emit("exploration")
|
|
275
|
+
self.show_exploration()
|
|
276
|
+
|
|
277
|
+
n = len(values["ranks"])
|
|
278
|
+
self.tasks.run(work, done, f"Fitting {n} models with split-half validation…")
|
|
279
|
+
|
|
280
|
+
def fit_final(self) -> None:
|
|
281
|
+
values = self._exploration_settings()
|
|
282
|
+
if values is None:
|
|
283
|
+
return
|
|
284
|
+
try:
|
|
285
|
+
values["final"] = {**values["final"], "rank": self.f_rank.value(),
|
|
286
|
+
"n_init": self.f_init.value(), "tol": _float(self.f_tol),
|
|
287
|
+
"max_iter": self.f_iter.value()}
|
|
288
|
+
except ValueError:
|
|
289
|
+
QMessageBox.information(self, "eemkit", "Convergence must be a number, e.g. 1e-8.")
|
|
290
|
+
return
|
|
291
|
+
self._store(values)
|
|
292
|
+
ds = self._input()
|
|
293
|
+
if ds is None:
|
|
294
|
+
return
|
|
295
|
+
settings = self.project.settings
|
|
296
|
+
|
|
297
|
+
def work(progress, cancelled):
|
|
298
|
+
return pipeline.fit_final(ds, settings, progress=progress, cancelled=cancelled)
|
|
299
|
+
|
|
300
|
+
def done(result):
|
|
301
|
+
self.project.model, self.project.split = result
|
|
302
|
+
self.project.changed.emit("final")
|
|
303
|
+
self.show_final()
|
|
304
|
+
self.results.setCurrentIndex(3)
|
|
305
|
+
|
|
306
|
+
self.tasks.run(work, done, "Fitting the final model…")
|
|
307
|
+
|
|
308
|
+
def exclude_selected(self) -> None:
|
|
309
|
+
rows = sorted({i.row() for i in self.out_view.selectionModel().selectedRows()})
|
|
310
|
+
df = self.out_model.df
|
|
311
|
+
if not rows or df.empty:
|
|
312
|
+
return
|
|
313
|
+
current = {self.excluded.item(i).text() for i in range(self.excluded.count())}
|
|
314
|
+
for name in df.index[rows]:
|
|
315
|
+
if name not in current:
|
|
316
|
+
self.excluded.addItem(str(name))
|
|
317
|
+
self._store(self._exploration_settings() or self.project.settings["parafac"])
|
|
318
|
+
QMessageBox.information(self, "eemkit", "Samples set aside. Run the exploratory models "
|
|
319
|
+
"again to see the effect.")
|
|
320
|
+
|
|
321
|
+
def unexclude(self) -> None:
|
|
322
|
+
for item in self.excluded.selectedItems():
|
|
323
|
+
self.excluded.takeItem(self.excluded.row(item))
|
|
324
|
+
values = self._exploration_settings()
|
|
325
|
+
if values is not None:
|
|
326
|
+
self._store(values)
|
|
327
|
+
|
|
328
|
+
# views -------------------------------------------------------------- #
|
|
329
|
+
|
|
330
|
+
def _on_changed(self, what: str) -> None:
|
|
331
|
+
if what == "settings":
|
|
332
|
+
self.load_settings()
|
|
333
|
+
if what in ("exploration", "corrected") and self.project.exploration is None:
|
|
334
|
+
self.summary_model.set_frame(pd.DataFrame())
|
|
335
|
+
for panel in (self.diag_plot, self.load_plot, self.out_plot):
|
|
336
|
+
panel.clear("Run the exploratory models.")
|
|
337
|
+
self.out_model.set_frame(pd.DataFrame())
|
|
338
|
+
if what in ("final", "corrected") and self.project.model is None:
|
|
339
|
+
self.final_plot.clear("Fit the final model.")
|
|
340
|
+
self.final_text.setText("")
|
|
341
|
+
if what == "display":
|
|
342
|
+
self.draw_loadings()
|
|
343
|
+
if self.project.model is not None:
|
|
344
|
+
self.show_final()
|
|
345
|
+
|
|
346
|
+
def show_exploration(self) -> None:
|
|
347
|
+
ex = self.project.exploration
|
|
348
|
+
t = ex.table.copy()
|
|
349
|
+
self.summary_model.set_frame(t.round(4))
|
|
350
|
+
fig = plot.rank_diagnostics(
|
|
351
|
+
t, threshold=self.project.settings["parafac"]["split_half"]["threshold"], width=7.5)
|
|
352
|
+
self.diag_plot.show_figure(fig)
|
|
353
|
+
ranks = [str(r) for r in ex.models]
|
|
354
|
+
meta = [c for c in next(iter(ex.models.values())).coords
|
|
355
|
+
if c not in ("sample", "ex", "em", "component", "init", "stratum")
|
|
356
|
+
and next(iter(ex.models.values()))[c].dims == ("sample",)
|
|
357
|
+
and not c.endswith(("_file", "_time")) and not c.startswith(("eem_", "abs_"))]
|
|
358
|
+
for box, items in ((self.load_rank, ranks), (self.out_rank, ranks),
|
|
359
|
+
(self.out_by, ["(none)"] + meta)):
|
|
360
|
+
box.blockSignals(True)
|
|
361
|
+
box.clear()
|
|
362
|
+
box.addItems(items)
|
|
363
|
+
box.blockSignals(False)
|
|
364
|
+
self.draw_loadings()
|
|
365
|
+
self.draw_outliers()
|
|
366
|
+
|
|
367
|
+
def draw_loadings(self) -> None:
|
|
368
|
+
ex = self.project.exploration
|
|
369
|
+
if ex is None or not self.load_rank.currentText():
|
|
370
|
+
return
|
|
371
|
+
r = int(self.load_rank.currentText())
|
|
372
|
+
self.load_plot.show_figure(plot.components(ex.models[r], ex.splits[r], width=8.0,
|
|
373
|
+
cmap=self.project.eem_colours))
|
|
374
|
+
|
|
375
|
+
def draw_outliers(self) -> None:
|
|
376
|
+
ex = self.project.exploration
|
|
377
|
+
if ex is None or not self.out_rank.currentText():
|
|
378
|
+
return
|
|
379
|
+
m = ex.models[int(self.out_rank.currentText())]
|
|
380
|
+
by = self.out_by.currentText()
|
|
381
|
+
by = None if by in ("", "(none)") or m[by].to_series().nunique() > len(plot.CATEGORICAL) \
|
|
382
|
+
else by
|
|
383
|
+
self.out_plot.show_figure(plot.leverage(m, by=by, width=5.0))
|
|
384
|
+
df = pd.DataFrame({"leverage": m["leverage_sample"].values,
|
|
385
|
+
"residual SS": m["sse_sample_normalized"].values},
|
|
386
|
+
index=m["sample"].values).sort_values("leverage", ascending=False)
|
|
387
|
+
self.out_model.set_frame(df)
|
|
388
|
+
|
|
389
|
+
def show_final(self) -> None:
|
|
390
|
+
m, sh = self.project.model, self.project.split
|
|
391
|
+
a = m.attrs
|
|
392
|
+
verdict = "VALIDATED" if sh.attrs["validated"] else "NOT validated"
|
|
393
|
+
self.final_text.setText(
|
|
394
|
+
f"{m.sizes['component']} components, {m.sizes['sample']} samples. Explained variance "
|
|
395
|
+
f"{a['explained_variance_pct']:.3f} %, core consistency {a['core_consistency_pct']:.1f}"
|
|
396
|
+
f" %, {a['n_init_at_best_minimum']}/{a['n_init']} starts at the best minimum. "
|
|
397
|
+
f"Split-half minimum Tucker congruence {sh.attrs['min_tcc']:.4f}: {verdict}.")
|
|
398
|
+
self.final_plot.show_figure(plot.components(m, sh, width=8.0,
|
|
399
|
+
cmap=self.project.eem_colours))
|