polynet 1.0.1__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.
- polynet/__init__.py +8 -0
- polynet/app/Welcome_to_PolyNet.py +82 -0
- polynet/app/components/experiments.py +22 -0
- polynet/app/components/figure_export.py +87 -0
- polynet/app/components/forms/analyse_results.py +825 -0
- polynet/app/components/forms/applicability_domain.py +108 -0
- polynet/app/components/forms/create_experiment.py +501 -0
- polynet/app/components/forms/explain_model.py +684 -0
- polynet/app/components/forms/explain_tml.py +481 -0
- polynet/app/components/forms/explanation_plots.py +441 -0
- polynet/app/components/forms/fragment_plots.py +187 -0
- polynet/app/components/forms/plot_customiser.py +102 -0
- polynet/app/components/forms/representation.py +816 -0
- polynet/app/components/forms/search_grid.py +158 -0
- polynet/app/components/forms/train_models.py +1180 -0
- polynet/app/components/graph_display.py +197 -0
- polynet/app/components/optional_dependencies.py +50 -0
- polynet/app/components/plots.py +238 -0
- polynet/app/options/allowable_sets.py +3 -0
- polynet/app/options/file_paths.py +80 -0
- polynet/app/options/plot.py +21 -0
- polynet/app/options/state_keys.py +331 -0
- polynet/app/pages/1_Create_Experiment.py +108 -0
- polynet/app/pages/2_Representation.py +202 -0
- polynet/app/pages/3_Train_Models.py +560 -0
- polynet/app/pages/4_Predict.py +264 -0
- polynet/app/pages/5_Explain_Models.py +295 -0
- polynet/app/pages/6_Analyse_Results.py +203 -0
- polynet/app/services/cache.py +47 -0
- polynet/app/services/configurations.py +9 -0
- polynet/app/services/experiments.py +3 -0
- polynet/app/services/explain_model.py +282 -0
- polynet/app/services/explain_source.py +143 -0
- polynet/app/services/explain_tml.py +212 -0
- polynet/app/services/explanation_results.py +345 -0
- polynet/app/services/graph_viz.py +494 -0
- polynet/app/services/model_training.py +19 -0
- polynet/app/services/predict_model.py +3 -0
- polynet/app/services/progress.py +85 -0
- polynet/app/services/reruns.py +274 -0
- polynet/app/services/workload.py +54 -0
- polynet/app/static/polynet.png +0 -0
- polynet/app/utils/__init__.py +13 -0
- polynet/applicability/__init__.py +27 -0
- polynet/applicability/assess.py +458 -0
- polynet/applicability/calibration.py +111 -0
- polynet/applicability/domain.py +166 -0
- polynet/applicability/reference.py +159 -0
- polynet/applicability/similarity.py +140 -0
- polynet/cli.py +146 -0
- polynet/config/_loader.py +447 -0
- polynet/config/column_names.py +154 -0
- polynet/config/constants.py +64 -0
- polynet/config/display_names.py +148 -0
- polynet/config/enums.py +501 -0
- polynet/config/experiment.py +306 -0
- polynet/config/from_app.py +279 -0
- polynet/config/from_yaml.py +145 -0
- polynet/config/io.py +57 -0
- polynet/config/paths.py +379 -0
- polynet/config/schemas/__init__.py +47 -0
- polynet/config/schemas/applicability_domain.py +88 -0
- polynet/config/schemas/base.py +170 -0
- polynet/config/schemas/data.py +129 -0
- polynet/config/schemas/explainability.py +128 -0
- polynet/config/schemas/feature_preprocessing.py +101 -0
- polynet/config/schemas/fingerprints.py +174 -0
- polynet/config/schemas/general.py +47 -0
- polynet/config/schemas/plotting.py +59 -0
- polynet/config/schemas/representation.py +165 -0
- polynet/config/schemas/split_data.py +229 -0
- polynet/config/schemas/target_preprocessing.py +43 -0
- polynet/config/schemas/tml_explainability.py +116 -0
- polynet/config/schemas/training.py +473 -0
- polynet/config/search_grid.py +420 -0
- polynet/data/__init__.py +24 -0
- polynet/data/benchmarks/fluorine_mri_snr.csv +419 -0
- polynet/data/creator.py +193 -0
- polynet/data/feature_transformer.py +344 -0
- polynet/data/loader.py +245 -0
- polynet/data/preprocessing.py +406 -0
- polynet/data/sampling.py +134 -0
- polynet/data/structures.py +206 -0
- polynet/experiment/__init__.py +13 -0
- polynet/experiment/manager.py +50 -0
- polynet/explainability/__init__.py +124 -0
- polynet/explainability/attributions.py +37 -0
- polynet/explainability/embeddings.py +121 -0
- polynet/explainability/explain.py +959 -0
- polynet/explainability/fragment_figures.py +224 -0
- polynet/explainability/masking.py +635 -0
- polynet/explainability/selection.py +177 -0
- polynet/explainability/shap_explain.py +1359 -0
- polynet/explainability/visualization.py +850 -0
- polynet/factories/__init__.py +51 -0
- polynet/factories/dataloader.py +571 -0
- polynet/factories/loss.py +102 -0
- polynet/factories/network.py +187 -0
- polynet/factories/optimizer.py +218 -0
- polynet/featurizer/__init__.py +45 -0
- polynet/featurizer/allowable_sets.py +82 -0
- polynet/featurizer/descriptors.py +838 -0
- polynet/featurizer/graph.py +208 -0
- polynet/featurizer/pmx.py +450 -0
- polynet/featurizer/polymer_graph.py +508 -0
- polynet/featurizer/selection.py +278 -0
- polynet/inference/__init__.py +25 -0
- polynet/inference/ensemble.py +108 -0
- polynet/inference/gnn.py +157 -0
- polynet/inference/predict.py +154 -0
- polynet/inference/tml.py +155 -0
- polynet/inference/utils.py +108 -0
- polynet/models/__init__.py +66 -0
- polynet/models/base.py +665 -0
- polynet/models/gnn/__init__.py +40 -0
- polynet/models/gnn/cgcnn.py +248 -0
- polynet/models/gnn/cggnn.py +12 -0
- polynet/models/gnn/gat.py +161 -0
- polynet/models/gnn/gcn.py +211 -0
- polynet/models/gnn/graphsage.py +210 -0
- polynet/models/gnn/mpnn.py +230 -0
- polynet/models/gnn/transformer.py +163 -0
- polynet/models/persistence.py +237 -0
- polynet/pipeline/__init__.py +53 -0
- polynet/pipeline/runner.py +777 -0
- polynet/pipeline/self_check.py +348 -0
- polynet/pipeline/stages.py +1550 -0
- polynet/plotting/data_analysis.py +75 -0
- polynet/training/__init__.py +34 -0
- polynet/training/cv.py +55 -0
- polynet/training/evaluate.py +199 -0
- polynet/training/gnn.py +698 -0
- polynet/training/hyperopt.py +617 -0
- polynet/training/metrics.py +276 -0
- polynet/training/tml.py +477 -0
- polynet/utils/__init__.py +104 -0
- polynet/utils/chem_utils.py +383 -0
- polynet/utils/data_preprocessing.py +127 -0
- polynet/utils/graph_analysis.py +222 -0
- polynet/utils/optional_dependencies.py +91 -0
- polynet/utils/statistical_analysis.py +303 -0
- polynet/utils/validation.py +211 -0
- polynet/visualization/__init__.py +35 -0
- polynet/visualization/plots.py +480 -0
- polynet/visualization/utils.py +34 -0
- polynet-1.0.1.dist-info/METADATA +320 -0
- polynet-1.0.1.dist-info/RECORD +150 -0
- polynet-1.0.1.dist-info/WHEEL +4 -0
- polynet-1.0.1.dist-info/entry_points.txt +3 -0
- polynet-1.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import streamlit as st
|
|
5
|
+
|
|
6
|
+
from polynet.app.options.state_keys import AnalyseResultsStateKeys, PlotCustomiserStateKeys
|
|
7
|
+
from polynet.config.column_names import (
|
|
8
|
+
get_iterator_name,
|
|
9
|
+
get_predicted_label_column_name,
|
|
10
|
+
get_true_label_column_name,
|
|
11
|
+
)
|
|
12
|
+
from polynet.config.constants import DataSet, ResultColumn
|
|
13
|
+
from polynet.config.display_names import prettify_label, prettify_metric
|
|
14
|
+
from polynet.config.enums import Plot, ProblemType, SplitType
|
|
15
|
+
from polynet.config.schemas import DataConfig
|
|
16
|
+
from polynet.utils.statistical_analysis import (
|
|
17
|
+
METRIC_COMPARISON_TESTS,
|
|
18
|
+
MIN_SPLITS_FOR_WILCOXON,
|
|
19
|
+
MULTIPLE_COMPARISON_METHODS,
|
|
20
|
+
correct_pvalue_matrix,
|
|
21
|
+
mcnemar_pvalue_matrix,
|
|
22
|
+
metrics_pvalue_matrix,
|
|
23
|
+
min_wilcoxon_pvalue,
|
|
24
|
+
regression_pvalue_matrix,
|
|
25
|
+
)
|
|
26
|
+
from polynet.visualization import (
|
|
27
|
+
plot_bootstrap_boxplots,
|
|
28
|
+
plot_confusion_matrix,
|
|
29
|
+
plot_parity,
|
|
30
|
+
plot_pvalue_matrix,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _prettify_series(series: pd.Series, column_name: str, abbreviate: bool) -> pd.Series:
|
|
35
|
+
"""Map model identifiers in a hue/style series to display labels.
|
|
36
|
+
|
|
37
|
+
Only the model column is translated (its values are raw model ids like
|
|
38
|
+
``"random_forest-polybert"``); other grouping columns (set, iteration) are
|
|
39
|
+
returned unchanged so their legends keep their original values.
|
|
40
|
+
"""
|
|
41
|
+
if column_name == ResultColumn.MODEL:
|
|
42
|
+
return series.map(lambda v: prettify_label(v, abbreviate=abbreviate))
|
|
43
|
+
return series
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _multiple_comparison_selector(key: str) -> str | None:
|
|
47
|
+
"""Render a correction-method selectbox and return the ``multipletests`` name.
|
|
48
|
+
|
|
49
|
+
Pairwise comparisons across many models inflate the family-wise error rate,
|
|
50
|
+
so a correction is applied by default (Holm-Bonferroni). The user can switch
|
|
51
|
+
to Bonferroni, Benjamini-Hochberg (FDR), or disable correction.
|
|
52
|
+
"""
|
|
53
|
+
labels = list(MULTIPLE_COMPARISON_METHODS.keys())
|
|
54
|
+
choice = st.selectbox(
|
|
55
|
+
"Multiple-comparison correction",
|
|
56
|
+
options=labels,
|
|
57
|
+
index=0, # Holm-Bonferroni
|
|
58
|
+
key=key,
|
|
59
|
+
help=(
|
|
60
|
+
"Adjusts the pairwise p-values for the number of simultaneous model "
|
|
61
|
+
"comparisons. Holm-Bonferroni and Bonferroni control the family-wise "
|
|
62
|
+
"error rate; Benjamini-Hochberg controls the false discovery rate."
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
return MULTIPLE_COMPARISON_METHODS[choice]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def compare_predictions_form(
|
|
69
|
+
predictions_df: pd.DataFrame, target_variable_name: str, data_options: DataConfig
|
|
70
|
+
):
|
|
71
|
+
|
|
72
|
+
st.write("### Model predictions pairwise comparison")
|
|
73
|
+
|
|
74
|
+
analyse_set = st.selectbox(
|
|
75
|
+
"Select a set to analyse",
|
|
76
|
+
options=[DataSet.Training, DataSet.Validation, DataSet.Test, "All sets"],
|
|
77
|
+
index=2,
|
|
78
|
+
key="analyse_set",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if analyse_set != "All sets":
|
|
82
|
+
predictions_df = predictions_df.loc[predictions_df[ResultColumn.SET] == analyse_set]
|
|
83
|
+
|
|
84
|
+
label_col_name = get_true_label_column_name(target_variable_name=target_variable_name)
|
|
85
|
+
true_vals = predictions_df[label_col_name].to_numpy()
|
|
86
|
+
|
|
87
|
+
trained_models = [
|
|
88
|
+
col.split(" ")[0] for col in predictions_df.columns if ResultColumn.PREDICTED in col
|
|
89
|
+
]
|
|
90
|
+
compare_models = st.multiselect(
|
|
91
|
+
"Select models to test statistically difference in predictions",
|
|
92
|
+
options=trained_models,
|
|
93
|
+
format_func=prettify_label,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if len(compare_models) > 1:
|
|
97
|
+
|
|
98
|
+
predicted_cols = [
|
|
99
|
+
get_predicted_label_column_name(
|
|
100
|
+
target_variable_name=target_variable_name, model_name=model
|
|
101
|
+
)
|
|
102
|
+
for model in compare_models
|
|
103
|
+
]
|
|
104
|
+
predictions_array = predictions_df[predicted_cols].to_numpy().T
|
|
105
|
+
|
|
106
|
+
if data_options.problem_type == ProblemType.Classification:
|
|
107
|
+
p_matrix = mcnemar_pvalue_matrix(y_true=true_vals, predictions=predictions_array)
|
|
108
|
+
elif data_options.problem_type == ProblemType.Regression:
|
|
109
|
+
p_matrix = regression_pvalue_matrix(y_true=true_vals, predictions=predictions_array)
|
|
110
|
+
|
|
111
|
+
correction = _multiple_comparison_selector(key="predictions_pvalue_correction")
|
|
112
|
+
p_matrix = correct_pvalue_matrix(p_matrix, method=correction)
|
|
113
|
+
|
|
114
|
+
config = get_plot_customisation_form(
|
|
115
|
+
plot_type=Plot.MatrixPlot,
|
|
116
|
+
data=None,
|
|
117
|
+
data_options=data_options,
|
|
118
|
+
color_by_opts=None,
|
|
119
|
+
model_pred_col=None,
|
|
120
|
+
model_true_cols=None,
|
|
121
|
+
key_prefix="predictions_",
|
|
122
|
+
)
|
|
123
|
+
abbreviate = config.pop("abbreviate_labels", False)
|
|
124
|
+
model_labels = [prettify_label(m, abbreviate=abbreviate) for m in compare_models]
|
|
125
|
+
plot = plot_pvalue_matrix(p_matrix=p_matrix, model_names=model_labels, **config)
|
|
126
|
+
|
|
127
|
+
return plot
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _metric_test_selector(
|
|
131
|
+
n_splits: int, analyse_set: str, split_sizes: dict[str, float] | None
|
|
132
|
+
) -> tuple[str, float | None] | None:
|
|
133
|
+
"""
|
|
134
|
+
Render the statistical-test choice for the metric-level comparison.
|
|
135
|
+
|
|
136
|
+
Warns when Wilcoxon cannot reach significance with the available number of
|
|
137
|
+
splits, and computes ``n_test / n_train`` for the Nadeau–Bengio corrected
|
|
138
|
+
t-test from the saved split sizes.
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
tuple[str, float | None] | None
|
|
143
|
+
``(test, test_train_ratio)`` for ``metrics_pvalue_matrix``, or ``None``
|
|
144
|
+
when the chosen test cannot be run.
|
|
145
|
+
"""
|
|
146
|
+
label = st.selectbox(
|
|
147
|
+
"Statistical test",
|
|
148
|
+
options=list(METRIC_COMPARISON_TESTS),
|
|
149
|
+
index=0,
|
|
150
|
+
key="metrics_statistical_test",
|
|
151
|
+
help=(
|
|
152
|
+
"Metrics come from repeated random splits of the same dataset, whose training "
|
|
153
|
+
"sets overlap, so the per-split values are not independent. The Nadeau–Bengio "
|
|
154
|
+
"corrected resampled t-test accounts for this overlap by inflating the variance "
|
|
155
|
+
"with n_test / n_train; the Wilcoxon signed-rank test does not."
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
test = METRIC_COMPARISON_TESTS[label]
|
|
159
|
+
|
|
160
|
+
if test == "wilcoxon":
|
|
161
|
+
if n_splits < MIN_SPLITS_FOR_WILCOXON:
|
|
162
|
+
st.warning(
|
|
163
|
+
f"Only {n_splits} split(s) are available. With {n_splits} paired values the "
|
|
164
|
+
f"smallest two-sided p-value the Wilcoxon test can return is "
|
|
165
|
+
f"{min_wilcoxon_pvalue(n_splits):.4f}, so no difference can be significant at "
|
|
166
|
+
f"0.05. Train with at least {MIN_SPLITS_FOR_WILCOXON} repeated splits, or use "
|
|
167
|
+
"the Nadeau–Bengio corrected t-test."
|
|
168
|
+
)
|
|
169
|
+
return test, None
|
|
170
|
+
|
|
171
|
+
if n_splits < 2:
|
|
172
|
+
st.warning("The corrected t-test needs at least 2 splits.")
|
|
173
|
+
return None
|
|
174
|
+
if not split_sizes or DataSet.Training not in split_sizes or analyse_set not in split_sizes:
|
|
175
|
+
st.error(
|
|
176
|
+
"Split sizes are unavailable (split_indices.json not found), so the corrected "
|
|
177
|
+
"t-test cannot be computed. Re-run training to save the split indices."
|
|
178
|
+
)
|
|
179
|
+
return None
|
|
180
|
+
ratio = split_sizes[analyse_set] / split_sizes[DataSet.Training]
|
|
181
|
+
st.caption(
|
|
182
|
+
f"Corrected t-test with n_test / n_train = {split_sizes[analyse_set]:.0f} / "
|
|
183
|
+
f"{split_sizes[DataSet.Training]:.0f} = {ratio:.3f} (mean set sizes across splits; "
|
|
184
|
+
"n_train is the training set without validation)."
|
|
185
|
+
)
|
|
186
|
+
if analyse_set == DataSet.Training:
|
|
187
|
+
st.info(
|
|
188
|
+
"The corrected t-test is meant for held-out data. On training-set metrics "
|
|
189
|
+
"n_test / n_train = 1, which makes it very conservative."
|
|
190
|
+
)
|
|
191
|
+
return test, ratio
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def compare_metrics_form(
|
|
195
|
+
metrics: dict,
|
|
196
|
+
data_options: DataConfig | None = None,
|
|
197
|
+
split_sizes: dict[str, float] | None = None,
|
|
198
|
+
):
|
|
199
|
+
|
|
200
|
+
st.write("### Model metrics comparison")
|
|
201
|
+
|
|
202
|
+
analyse_set = st.selectbox(
|
|
203
|
+
"Select a set to analyse",
|
|
204
|
+
options=[DataSet.Training, DataSet.Validation, DataSet.Test],
|
|
205
|
+
index=2,
|
|
206
|
+
key="analyse_set_metrics",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Build: data_dict[model][metric_name] -> list of bootstrap values
|
|
210
|
+
data_dict = {}
|
|
211
|
+
metrics_name = set()
|
|
212
|
+
for _, dictio in metrics.items():
|
|
213
|
+
for model, per_model_dict in dictio.items():
|
|
214
|
+
data_dict.setdefault(model, {})
|
|
215
|
+
for metric_name, val in per_model_dict[analyse_set].items():
|
|
216
|
+
data_dict[model].setdefault(metric_name, [])
|
|
217
|
+
data_dict[model][metric_name].append(val)
|
|
218
|
+
metrics_name.add(metric_name)
|
|
219
|
+
|
|
220
|
+
models = st.multiselect(
|
|
221
|
+
"Select models to compare",
|
|
222
|
+
options=list(data_dict.keys()),
|
|
223
|
+
key="comparemodelmetrics",
|
|
224
|
+
format_func=prettify_label,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
metric_choice = st.selectbox(
|
|
228
|
+
"Select a metric to compare",
|
|
229
|
+
options=sorted(metrics_name),
|
|
230
|
+
key="metriccomparemetric",
|
|
231
|
+
format_func=prettify_metric,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
selected = {m: data_dict[m][metric_choice] for m in models if m in data_dict}
|
|
235
|
+
|
|
236
|
+
plot_type = st.selectbox("Select a type of plot", options=["P-value Matrix", "Box Plot"])
|
|
237
|
+
|
|
238
|
+
if len(selected) > 1:
|
|
239
|
+
|
|
240
|
+
if plot_type == "P-value Matrix":
|
|
241
|
+
config = get_plot_customisation_form(
|
|
242
|
+
plot_type=Plot.MatrixPlot,
|
|
243
|
+
data=None,
|
|
244
|
+
data_options=data_options,
|
|
245
|
+
color_by_opts=None,
|
|
246
|
+
model_pred_col=None,
|
|
247
|
+
model_true_cols=None,
|
|
248
|
+
key_prefix="metrics_",
|
|
249
|
+
)
|
|
250
|
+
n_splits = min(len(v) for v in selected.values())
|
|
251
|
+
test_choice = _metric_test_selector(
|
|
252
|
+
n_splits=n_splits, analyse_set=analyse_set, split_sizes=split_sizes
|
|
253
|
+
)
|
|
254
|
+
if test_choice is None:
|
|
255
|
+
return None
|
|
256
|
+
test, test_train_ratio = test_choice
|
|
257
|
+
p_matrix, order = metrics_pvalue_matrix(
|
|
258
|
+
selected, test=test, test_train_ratio=test_train_ratio
|
|
259
|
+
)
|
|
260
|
+
correction = _multiple_comparison_selector(key="metrics_pvalue_correction")
|
|
261
|
+
p_matrix = correct_pvalue_matrix(p_matrix, method=correction)
|
|
262
|
+
abbreviate = config.pop("abbreviate_labels", False)
|
|
263
|
+
order_labels = [prettify_label(m, abbreviate=abbreviate) for m in order]
|
|
264
|
+
plot = plot_pvalue_matrix(p_matrix=p_matrix, model_names=order_labels, **config)
|
|
265
|
+
elif plot_type == "Box Plot":
|
|
266
|
+
config = get_plot_customisation_form(
|
|
267
|
+
plot_type=Plot.MetricsBoxPlot,
|
|
268
|
+
data=None,
|
|
269
|
+
data_options=data_options,
|
|
270
|
+
color_by_opts=None,
|
|
271
|
+
model_pred_col=None,
|
|
272
|
+
model_true_cols=None,
|
|
273
|
+
key_prefix="metrics_",
|
|
274
|
+
)
|
|
275
|
+
abbreviate = config.pop("abbreviate_labels", False)
|
|
276
|
+
selected = {
|
|
277
|
+
prettify_label(m, abbreviate=abbreviate): vals for m, vals in selected.items()
|
|
278
|
+
}
|
|
279
|
+
plot = plot_bootstrap_boxplots(
|
|
280
|
+
metrics_dict=selected, metric_name=prettify_metric(metric_choice), **config
|
|
281
|
+
)
|
|
282
|
+
return plot
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def get_plot_customisation_form(
|
|
286
|
+
plot_type: str,
|
|
287
|
+
data: pd.DataFrame,
|
|
288
|
+
data_options: DataConfig,
|
|
289
|
+
color_by_opts=None,
|
|
290
|
+
model_pred_col: str = None,
|
|
291
|
+
model_true_cols: str = None,
|
|
292
|
+
key_prefix: str = "",
|
|
293
|
+
):
|
|
294
|
+
"""
|
|
295
|
+
Renders a dynamic Streamlit form for customizing a plot, based on plot_type.
|
|
296
|
+
Returns a dictionary of kwargs for the plotting function.
|
|
297
|
+
"""
|
|
298
|
+
config = {}
|
|
299
|
+
widget_suffix = key_prefix + plot_type
|
|
300
|
+
|
|
301
|
+
with st.expander("Customise Plot", expanded=False):
|
|
302
|
+
|
|
303
|
+
# Display-only label style. Affects model/descriptor labels shown on the
|
|
304
|
+
# plot (legend, axis ticks); the underlying data keeps its raw names.
|
|
305
|
+
abbreviate_labels = st.checkbox(
|
|
306
|
+
"Use abbreviated labels",
|
|
307
|
+
value=False,
|
|
308
|
+
key=PlotCustomiserStateKeys.AbbreviateLabels + widget_suffix,
|
|
309
|
+
help="Show short model/descriptor names (e.g. 'RF polyBERT') instead of "
|
|
310
|
+
"full names (e.g. 'Random Forest polyBERT').",
|
|
311
|
+
)
|
|
312
|
+
config["abbreviate_labels"] = abbreviate_labels
|
|
313
|
+
|
|
314
|
+
config["title"] = st.text_input(
|
|
315
|
+
"Plot Title",
|
|
316
|
+
value=f"{data_options.target_variable_name} {plot_type.title()} Plot",
|
|
317
|
+
key=PlotCustomiserStateKeys.PlotTitle + widget_suffix,
|
|
318
|
+
help="Enter a title for the plot.",
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
config["title_fontsize"] = st.slider(
|
|
322
|
+
"Title Font Size",
|
|
323
|
+
min_value=8,
|
|
324
|
+
max_value=30,
|
|
325
|
+
value=16,
|
|
326
|
+
step=1,
|
|
327
|
+
key=PlotCustomiserStateKeys.PlotFontSize + widget_suffix,
|
|
328
|
+
help="Select the font size for the plot title.",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
columns = st.columns(2)
|
|
332
|
+
with columns[0]:
|
|
333
|
+
config["height"] = st.slider(
|
|
334
|
+
"Plot height",
|
|
335
|
+
min_value=4,
|
|
336
|
+
max_value=14,
|
|
337
|
+
value=7,
|
|
338
|
+
step=1,
|
|
339
|
+
key="plot_height" + widget_suffix,
|
|
340
|
+
)
|
|
341
|
+
if plot_type not in [Plot.MatrixPlot, Plot.MetricsBoxPlot]:
|
|
342
|
+
config["x_label"] = st.text_input(
|
|
343
|
+
"X-axis Label",
|
|
344
|
+
value=model_true_cols,
|
|
345
|
+
key=PlotCustomiserStateKeys.PlotXLabel + widget_suffix,
|
|
346
|
+
help="Enter a label for the X-axis.",
|
|
347
|
+
)
|
|
348
|
+
config["x_label_fontsize"] = st.slider(
|
|
349
|
+
"X-axis Label Font Size",
|
|
350
|
+
min_value=8,
|
|
351
|
+
max_value=30,
|
|
352
|
+
value=14,
|
|
353
|
+
step=1,
|
|
354
|
+
key=PlotCustomiserStateKeys.PlotXLabelFontSize + widget_suffix,
|
|
355
|
+
help="Select the font size for the X-axis label.",
|
|
356
|
+
)
|
|
357
|
+
with columns[1]:
|
|
358
|
+
|
|
359
|
+
config["width"] = st.slider(
|
|
360
|
+
"Plot width",
|
|
361
|
+
min_value=4,
|
|
362
|
+
max_value=14,
|
|
363
|
+
value=7,
|
|
364
|
+
step=1,
|
|
365
|
+
key="plot_width" + widget_suffix,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
if plot_type not in [Plot.MatrixPlot, Plot.MetricsBoxPlot]:
|
|
369
|
+
config["y_label"] = st.text_input(
|
|
370
|
+
"Y-axis Label",
|
|
371
|
+
value=model_pred_col,
|
|
372
|
+
key=PlotCustomiserStateKeys.PlotYLabel + widget_suffix,
|
|
373
|
+
help="Enter a label for the Y-axis.",
|
|
374
|
+
)
|
|
375
|
+
config["y_label_fontsize"] = st.slider(
|
|
376
|
+
"Y-axis Label Font Size",
|
|
377
|
+
min_value=8,
|
|
378
|
+
max_value=30,
|
|
379
|
+
value=14,
|
|
380
|
+
step=1,
|
|
381
|
+
key=PlotCustomiserStateKeys.PlotYLabelFontSize + widget_suffix,
|
|
382
|
+
help="Select the font size for the Y-axis label.",
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
config["tick_size"] = st.select_slider(
|
|
386
|
+
"Tick Size",
|
|
387
|
+
options=list(range(8, 21)),
|
|
388
|
+
value=12,
|
|
389
|
+
key=PlotCustomiserStateKeys.TickSize + widget_suffix,
|
|
390
|
+
help="Select the font size for the tick labels.",
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
if plot_type == Plot.Parity:
|
|
394
|
+
|
|
395
|
+
config["grid"] = st.checkbox(
|
|
396
|
+
"Show Grid",
|
|
397
|
+
value=True,
|
|
398
|
+
key=PlotCustomiserStateKeys.PlotGrid + widget_suffix,
|
|
399
|
+
help="Check to display grid lines on the plot.",
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
config["point_size"] = st.slider(
|
|
403
|
+
"Point Size",
|
|
404
|
+
min_value=1,
|
|
405
|
+
max_value=100,
|
|
406
|
+
value=50,
|
|
407
|
+
step=1,
|
|
408
|
+
key=PlotCustomiserStateKeys.PlotPointSize + widget_suffix,
|
|
409
|
+
help="Select the size of the points in the plot.",
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
config["border_color"] = st.color_picker(
|
|
413
|
+
"Point Border Color",
|
|
414
|
+
value="#000000",
|
|
415
|
+
key=PlotCustomiserStateKeys.PlotPointBorderColour + widget_suffix,
|
|
416
|
+
help="Select a color for the border of the plot.",
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
config["legend"] = st.checkbox(
|
|
420
|
+
"Show Legend",
|
|
421
|
+
value=True,
|
|
422
|
+
key=PlotCustomiserStateKeys.ShowLegend + widget_suffix,
|
|
423
|
+
help="Check to display the legend on the plot.",
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
if config["legend"]:
|
|
427
|
+
config["legend_fontsize"] = st.slider(
|
|
428
|
+
"Legend Font Size",
|
|
429
|
+
min_value=8,
|
|
430
|
+
max_value=20,
|
|
431
|
+
value=12,
|
|
432
|
+
step=1,
|
|
433
|
+
key=PlotCustomiserStateKeys.LegendFontSize + widget_suffix,
|
|
434
|
+
help="Select the font size for the legend.",
|
|
435
|
+
)
|
|
436
|
+
else:
|
|
437
|
+
config["legend_fontsize"] = None
|
|
438
|
+
|
|
439
|
+
if len(color_by_opts) > 1:
|
|
440
|
+
|
|
441
|
+
hue = st.selectbox(
|
|
442
|
+
"Color by",
|
|
443
|
+
options=color_by_opts,
|
|
444
|
+
key=PlotCustomiserStateKeys.ColourBy + widget_suffix,
|
|
445
|
+
index=1,
|
|
446
|
+
help="Select a column to color the points in the plot.",
|
|
447
|
+
)
|
|
448
|
+
config["hue"] = _prettify_series(data[hue], hue, abbreviate_labels) if hue else None
|
|
449
|
+
|
|
450
|
+
style_by = st.selectbox(
|
|
451
|
+
"Style by",
|
|
452
|
+
options=color_by_opts,
|
|
453
|
+
key=PlotCustomiserStateKeys.StyleBy + widget_suffix,
|
|
454
|
+
index=0,
|
|
455
|
+
help="Select a column to style the points in the plot.",
|
|
456
|
+
)
|
|
457
|
+
config["style_by"] = (
|
|
458
|
+
_prettify_series(data[style_by], style_by, abbreviate_labels)
|
|
459
|
+
if style_by
|
|
460
|
+
else None
|
|
461
|
+
)
|
|
462
|
+
else:
|
|
463
|
+
config["hue"] = None
|
|
464
|
+
config["style_by"] = None
|
|
465
|
+
|
|
466
|
+
if config["hue"] is not None:
|
|
467
|
+
cmap = st.selectbox(
|
|
468
|
+
"Color Map",
|
|
469
|
+
options=["tab10", "pastel", "colorblind", "Spectral", "Custom"],
|
|
470
|
+
key=PlotCustomiserStateKeys.CMap + widget_suffix,
|
|
471
|
+
help="Select a color map for the points in the plot.",
|
|
472
|
+
)
|
|
473
|
+
if cmap == "Custom":
|
|
474
|
+
cmap = []
|
|
475
|
+
for cat in config["hue"].unique():
|
|
476
|
+
color = st.color_picker(
|
|
477
|
+
f"Color for {cat}",
|
|
478
|
+
value="#1f77b4",
|
|
479
|
+
key=f"color_{cat}" + widget_suffix,
|
|
480
|
+
help=f"Select a color for the category '{cat}' in the plot.",
|
|
481
|
+
)
|
|
482
|
+
cmap.append(color)
|
|
483
|
+
config["palette"] = cmap
|
|
484
|
+
config["point_color"] = None
|
|
485
|
+
else:
|
|
486
|
+
config["palette"] = None
|
|
487
|
+
config["point_color"] = st.color_picker(
|
|
488
|
+
"Point Fill Color",
|
|
489
|
+
value="#1f77b4",
|
|
490
|
+
key="point_color" + widget_suffix,
|
|
491
|
+
help="Select a color for the points in the plot.",
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
elif plot_type == Plot.ConfusionMatrix:
|
|
495
|
+
config["cmap"] = st.selectbox(
|
|
496
|
+
"Color Map",
|
|
497
|
+
options=["Blues", "Greens", "Oranges"],
|
|
498
|
+
key=PlotCustomiserStateKeys.CMap + widget_suffix,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
config["label_fontsize"] = st.slider(
|
|
502
|
+
"Label Font Size",
|
|
503
|
+
min_value=8,
|
|
504
|
+
max_value=30,
|
|
505
|
+
value=16,
|
|
506
|
+
step=1,
|
|
507
|
+
key=PlotCustomiserStateKeys.PlotLabelFontSize + widget_suffix,
|
|
508
|
+
help="Select the font size for the labels in the confusion matrix.",
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
labels = data_options.class_names.keys()
|
|
512
|
+
config["display_labels"] = []
|
|
513
|
+
for label in labels:
|
|
514
|
+
name = st.text_input(
|
|
515
|
+
f"Name for label for {label}",
|
|
516
|
+
value=data_options.class_names[label],
|
|
517
|
+
key=f"{PlotCustomiserStateKeys.LabelNames}_{label}" + widget_suffix,
|
|
518
|
+
help=f"Enter the label for the class '{label}'.",
|
|
519
|
+
)
|
|
520
|
+
config["display_labels"].append(name)
|
|
521
|
+
|
|
522
|
+
elif plot_type == Plot.MatrixPlot:
|
|
523
|
+
|
|
524
|
+
config["mask_upper_triangle"] = st.checkbox(
|
|
525
|
+
"Mask upper triangle", key="mask_upper_triangle" + widget_suffix
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
col1, col2 = st.columns(2)
|
|
529
|
+
|
|
530
|
+
with col1:
|
|
531
|
+
config["non_signifficant_colour"] = st.color_picker(
|
|
532
|
+
"Non-signifficant colour",
|
|
533
|
+
value="#4575B4",
|
|
534
|
+
key="non_signifficant" + widget_suffix,
|
|
535
|
+
)
|
|
536
|
+
with col2:
|
|
537
|
+
config["signifficant_colour"] = st.color_picker(
|
|
538
|
+
"Non-signifficant colour", value="#D73027", key="signifficant" + widget_suffix
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
elif plot_type == Plot.MetricsBoxPlot:
|
|
542
|
+
|
|
543
|
+
cols = st.columns(3)
|
|
544
|
+
|
|
545
|
+
with cols[0]:
|
|
546
|
+
fill_colour = st.color_picker(
|
|
547
|
+
"Box Fill Colour",
|
|
548
|
+
value="#9aa0a5",
|
|
549
|
+
key="box_fill" + widget_suffix,
|
|
550
|
+
help="Select a fill color for the box in the box plot.",
|
|
551
|
+
)
|
|
552
|
+
with cols[1]:
|
|
553
|
+
border_colour = st.color_picker(
|
|
554
|
+
"Box Border Colour",
|
|
555
|
+
value="#000000",
|
|
556
|
+
key="box_border" + widget_suffix,
|
|
557
|
+
help="Select a border color for the box in the box plot.",
|
|
558
|
+
)
|
|
559
|
+
with cols[2]:
|
|
560
|
+
median_colour = st.color_picker(
|
|
561
|
+
"Median Line Colour",
|
|
562
|
+
value="#FF0000",
|
|
563
|
+
key="median_color" + widget_suffix,
|
|
564
|
+
help="Select a color for the median line in the box plot.",
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
config["fill_colour"] = fill_colour
|
|
568
|
+
config["border_colour"] = border_colour
|
|
569
|
+
config["median_colour"] = median_colour
|
|
570
|
+
|
|
571
|
+
config["dpi"] = st.slider(
|
|
572
|
+
"DPI",
|
|
573
|
+
min_value=100,
|
|
574
|
+
max_value=600,
|
|
575
|
+
value=300,
|
|
576
|
+
step=10,
|
|
577
|
+
key="dpi" + widget_suffix,
|
|
578
|
+
help="Select the DPI for the plot.",
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
return config
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def confusion_matrix_plot_form(
|
|
585
|
+
predictions_df: pd.DataFrame, split_type: SplitType | str, data_options: DataConfig
|
|
586
|
+
):
|
|
587
|
+
"""
|
|
588
|
+
Renders a Streamlit form for customizing and displaying a parity plot based on model predictions.
|
|
589
|
+
Returns a matplotlib figure object.
|
|
590
|
+
"""
|
|
591
|
+
|
|
592
|
+
iterator = get_iterator_name(split_type)
|
|
593
|
+
|
|
594
|
+
iteration = st.multiselect(
|
|
595
|
+
f"Select the {iterator} to display parity plot",
|
|
596
|
+
options=predictions_df[iterator].unique(),
|
|
597
|
+
key=AnalyseResultsStateKeys.PlotIterations,
|
|
598
|
+
default=predictions_df[iterator].unique().tolist()[0],
|
|
599
|
+
help="Select the iteration or fold for which you want to display the parity plot.",
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
# Derive the available models from the prediction columns so both GNN and
|
|
603
|
+
# TML models are listed (predictions.csv merges both frameworks).
|
|
604
|
+
models = [
|
|
605
|
+
col_name.split(" ")[0]
|
|
606
|
+
for col_name in predictions_df.columns
|
|
607
|
+
if ResultColumn.PREDICTED in col_name
|
|
608
|
+
]
|
|
609
|
+
model_name = st.multiselect(
|
|
610
|
+
"Select the model to display the confusion matrix",
|
|
611
|
+
options=models,
|
|
612
|
+
default=models[0],
|
|
613
|
+
key=AnalyseResultsStateKeys.PlotModels,
|
|
614
|
+
format_func=prettify_label,
|
|
615
|
+
help="Select the model for which you want to display the confusion matrix.",
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
model_pred_cols = [
|
|
619
|
+
get_predicted_label_column_name(
|
|
620
|
+
target_variable_name=data_options.target_variable_name, model_name=name
|
|
621
|
+
)
|
|
622
|
+
for name in model_name
|
|
623
|
+
]
|
|
624
|
+
|
|
625
|
+
model_true_cols = get_true_label_column_name(
|
|
626
|
+
target_variable_name=data_options.target_variable_name
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
set_name = st.multiselect(
|
|
630
|
+
"Select the set to display parity plot",
|
|
631
|
+
options=predictions_df[ResultColumn.SET].unique(),
|
|
632
|
+
key=AnalyseResultsStateKeys.PlotSet,
|
|
633
|
+
default=[DataSet.Test],
|
|
634
|
+
help="Select the set for which you want to display the parity plot.",
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
plot_data = reshape_predictions(
|
|
638
|
+
df=predictions_df,
|
|
639
|
+
pred_cols=model_pred_cols,
|
|
640
|
+
target_variable_name=data_options.target_variable_name,
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
model_pred_col = get_predicted_label_column_name(
|
|
644
|
+
target_variable_name=data_options.target_variable_name, model_name=None
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
plot_data = plot_data[
|
|
648
|
+
(plot_data[iterator].isin(iteration))
|
|
649
|
+
& (plot_data[ResultColumn.SET].isin(set_name))
|
|
650
|
+
& (plot_data[ResultColumn.MODEL].isin(model_name))
|
|
651
|
+
]
|
|
652
|
+
|
|
653
|
+
with st.expander("Show Data", expanded=False):
|
|
654
|
+
st.markdown(
|
|
655
|
+
f"**Showing data for model: {model_name}, set: {set_name}, iterations: {iteration}**"
|
|
656
|
+
)
|
|
657
|
+
st.markdown(
|
|
658
|
+
f"**True label column:** `{model_true_cols}` | **Predicted label column:** `{model_pred_col}`**"
|
|
659
|
+
)
|
|
660
|
+
st.dataframe(plot_data)
|
|
661
|
+
|
|
662
|
+
plot_config = get_plot_customisation_form(
|
|
663
|
+
plot_type=Plot.ConfusionMatrix,
|
|
664
|
+
data=plot_data,
|
|
665
|
+
data_options=data_options,
|
|
666
|
+
color_by_opts=[None],
|
|
667
|
+
model_pred_col=model_pred_col,
|
|
668
|
+
model_true_cols=model_true_cols,
|
|
669
|
+
)
|
|
670
|
+
# Not a label surface for this single-model plot; drop so it isn't splatted.
|
|
671
|
+
plot_config.pop("abbreviate_labels", None)
|
|
672
|
+
|
|
673
|
+
if not plot_data.empty:
|
|
674
|
+
|
|
675
|
+
# invert preds and true to match x and y axes title
|
|
676
|
+
fig = plot_confusion_matrix(
|
|
677
|
+
y_true=plot_data[model_pred_col], y_pred=plot_data[model_true_cols], **plot_config
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
return fig
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def parity_plot_form(
|
|
684
|
+
predictions_df: pd.DataFrame, split_type: SplitType | str, data_options: DataConfig
|
|
685
|
+
):
|
|
686
|
+
|
|
687
|
+
color_by_opts = [None]
|
|
688
|
+
|
|
689
|
+
iterator = get_iterator_name(split_type)
|
|
690
|
+
|
|
691
|
+
iteration = st.multiselect(
|
|
692
|
+
f"Select the {iterator} to display parity plot",
|
|
693
|
+
options=predictions_df[iterator].unique(),
|
|
694
|
+
key=AnalyseResultsStateKeys.PlotIterations,
|
|
695
|
+
default=predictions_df[iterator].unique().tolist()[0],
|
|
696
|
+
help="Select the iteration or fold for which you want to display the parity plot.",
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
if len(iteration) > 1:
|
|
700
|
+
color_by_opts.append(iterator)
|
|
701
|
+
|
|
702
|
+
# Derive the available models from the prediction columns so both GNN and
|
|
703
|
+
# TML models are listed (predictions.csv merges both frameworks).
|
|
704
|
+
models = [
|
|
705
|
+
col_name.split(" ")[0]
|
|
706
|
+
for col_name in predictions_df.columns
|
|
707
|
+
if ResultColumn.PREDICTED in col_name
|
|
708
|
+
]
|
|
709
|
+
|
|
710
|
+
model_name = st.multiselect(
|
|
711
|
+
"Select the model to display parity plot",
|
|
712
|
+
options=models,
|
|
713
|
+
default=models[0],
|
|
714
|
+
key=AnalyseResultsStateKeys.PlotModels,
|
|
715
|
+
format_func=prettify_label,
|
|
716
|
+
help="Select the model for which you want to display the parity plot.",
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
if len(model_name) > 1:
|
|
720
|
+
color_by_opts.append(ResultColumn.MODEL)
|
|
721
|
+
|
|
722
|
+
model_pred_cols = [
|
|
723
|
+
get_predicted_label_column_name(
|
|
724
|
+
target_variable_name=data_options.target_variable_name, model_name=name
|
|
725
|
+
)
|
|
726
|
+
for name in model_name
|
|
727
|
+
]
|
|
728
|
+
|
|
729
|
+
model_true_cols = get_true_label_column_name(
|
|
730
|
+
target_variable_name=data_options.target_variable_name
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
set_name = st.multiselect(
|
|
734
|
+
"Select the set to display parity plot",
|
|
735
|
+
options=predictions_df[ResultColumn.SET].unique(),
|
|
736
|
+
key=AnalyseResultsStateKeys.PlotSet,
|
|
737
|
+
default=[DataSet.Test],
|
|
738
|
+
help="Select the set for which you want to display the parity plot.",
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
if len(set_name) > 1:
|
|
742
|
+
color_by_opts.append(ResultColumn.SET)
|
|
743
|
+
|
|
744
|
+
plot_data = reshape_predictions(
|
|
745
|
+
df=predictions_df,
|
|
746
|
+
pred_cols=model_pred_cols,
|
|
747
|
+
target_variable_name=data_options.target_variable_name,
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
model_pred_col = get_predicted_label_column_name(
|
|
751
|
+
target_variable_name=data_options.target_variable_name, model_name=None
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
plot_data = plot_data[
|
|
755
|
+
(plot_data[iterator].isin(iteration))
|
|
756
|
+
& (plot_data[ResultColumn.SET].isin(set_name))
|
|
757
|
+
& (plot_data[ResultColumn.MODEL].isin(model_name))
|
|
758
|
+
]
|
|
759
|
+
|
|
760
|
+
with st.expander("Show Data", expanded=False):
|
|
761
|
+
st.markdown(
|
|
762
|
+
f"**Showing data for model: {model_name}, set: {set_name}, iterations: {iteration}**"
|
|
763
|
+
)
|
|
764
|
+
st.markdown(
|
|
765
|
+
f"**True label column:** `{model_true_cols}` | **Predicted label column:** `{model_pred_col}`**"
|
|
766
|
+
)
|
|
767
|
+
st.dataframe(plot_data)
|
|
768
|
+
|
|
769
|
+
plot_config = get_plot_customisation_form(
|
|
770
|
+
plot_type=Plot.Parity,
|
|
771
|
+
data=plot_data,
|
|
772
|
+
data_options=data_options,
|
|
773
|
+
color_by_opts=color_by_opts,
|
|
774
|
+
model_pred_col=model_pred_col,
|
|
775
|
+
model_true_cols=model_true_cols,
|
|
776
|
+
)
|
|
777
|
+
# Already applied to the hue/style legend inside the customiser; drop the
|
|
778
|
+
# flag so it isn't splatted into plot_parity.
|
|
779
|
+
plot_config.pop("abbreviate_labels", None)
|
|
780
|
+
|
|
781
|
+
if not plot_data.empty:
|
|
782
|
+
|
|
783
|
+
fig = plot_parity(
|
|
784
|
+
y_true=plot_data[model_true_cols], y_pred=plot_data[model_pred_col], **plot_config
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
return fig
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def extract_model_name_and_target(col_name, target_variable_name):
|
|
791
|
+
"""Extract model name from column like 'Model Predicted target'."""
|
|
792
|
+
pattern = rf"^(.*?)\s+Predicted\s+{re.escape(target_variable_name)}$"
|
|
793
|
+
match = re.match(pattern, col_name)
|
|
794
|
+
if match:
|
|
795
|
+
return match.group(1)
|
|
796
|
+
return None
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def reshape_predictions(
|
|
800
|
+
df: pd.DataFrame, pred_cols: list, target_variable_name: str
|
|
801
|
+
) -> pd.DataFrame:
|
|
802
|
+
# Columns to keep (everything that’s not a prediction column)
|
|
803
|
+
id_vars = [col for col in df.columns if col not in pred_cols]
|
|
804
|
+
|
|
805
|
+
# Melt the DataFrame
|
|
806
|
+
melted_df = df.melt(
|
|
807
|
+
id_vars=id_vars, value_vars=pred_cols, var_name="Prediction_Column", value_name="Prediction"
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
# Extract model name from the column name
|
|
811
|
+
melted_df[ResultColumn.MODEL] = melted_df["Prediction_Column"].apply(
|
|
812
|
+
lambda col: extract_model_name_and_target(col, target_variable_name)
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
new_col_name = get_predicted_label_column_name(
|
|
816
|
+
target_variable_name=target_variable_name, model_name=None
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
# Create new column name (without model name)
|
|
820
|
+
melted_df[new_col_name] = melted_df["Prediction"]
|
|
821
|
+
|
|
822
|
+
# Drop the old column
|
|
823
|
+
melted_df = melted_df.drop(columns=["Prediction_Column", "Prediction"])
|
|
824
|
+
|
|
825
|
+
return melted_df
|