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.
Files changed (150) hide show
  1. polynet/__init__.py +8 -0
  2. polynet/app/Welcome_to_PolyNet.py +82 -0
  3. polynet/app/components/experiments.py +22 -0
  4. polynet/app/components/figure_export.py +87 -0
  5. polynet/app/components/forms/analyse_results.py +825 -0
  6. polynet/app/components/forms/applicability_domain.py +108 -0
  7. polynet/app/components/forms/create_experiment.py +501 -0
  8. polynet/app/components/forms/explain_model.py +684 -0
  9. polynet/app/components/forms/explain_tml.py +481 -0
  10. polynet/app/components/forms/explanation_plots.py +441 -0
  11. polynet/app/components/forms/fragment_plots.py +187 -0
  12. polynet/app/components/forms/plot_customiser.py +102 -0
  13. polynet/app/components/forms/representation.py +816 -0
  14. polynet/app/components/forms/search_grid.py +158 -0
  15. polynet/app/components/forms/train_models.py +1180 -0
  16. polynet/app/components/graph_display.py +197 -0
  17. polynet/app/components/optional_dependencies.py +50 -0
  18. polynet/app/components/plots.py +238 -0
  19. polynet/app/options/allowable_sets.py +3 -0
  20. polynet/app/options/file_paths.py +80 -0
  21. polynet/app/options/plot.py +21 -0
  22. polynet/app/options/state_keys.py +331 -0
  23. polynet/app/pages/1_Create_Experiment.py +108 -0
  24. polynet/app/pages/2_Representation.py +202 -0
  25. polynet/app/pages/3_Train_Models.py +560 -0
  26. polynet/app/pages/4_Predict.py +264 -0
  27. polynet/app/pages/5_Explain_Models.py +295 -0
  28. polynet/app/pages/6_Analyse_Results.py +203 -0
  29. polynet/app/services/cache.py +47 -0
  30. polynet/app/services/configurations.py +9 -0
  31. polynet/app/services/experiments.py +3 -0
  32. polynet/app/services/explain_model.py +282 -0
  33. polynet/app/services/explain_source.py +143 -0
  34. polynet/app/services/explain_tml.py +212 -0
  35. polynet/app/services/explanation_results.py +345 -0
  36. polynet/app/services/graph_viz.py +494 -0
  37. polynet/app/services/model_training.py +19 -0
  38. polynet/app/services/predict_model.py +3 -0
  39. polynet/app/services/progress.py +85 -0
  40. polynet/app/services/reruns.py +274 -0
  41. polynet/app/services/workload.py +54 -0
  42. polynet/app/static/polynet.png +0 -0
  43. polynet/app/utils/__init__.py +13 -0
  44. polynet/applicability/__init__.py +27 -0
  45. polynet/applicability/assess.py +458 -0
  46. polynet/applicability/calibration.py +111 -0
  47. polynet/applicability/domain.py +166 -0
  48. polynet/applicability/reference.py +159 -0
  49. polynet/applicability/similarity.py +140 -0
  50. polynet/cli.py +146 -0
  51. polynet/config/_loader.py +447 -0
  52. polynet/config/column_names.py +154 -0
  53. polynet/config/constants.py +64 -0
  54. polynet/config/display_names.py +148 -0
  55. polynet/config/enums.py +501 -0
  56. polynet/config/experiment.py +306 -0
  57. polynet/config/from_app.py +279 -0
  58. polynet/config/from_yaml.py +145 -0
  59. polynet/config/io.py +57 -0
  60. polynet/config/paths.py +379 -0
  61. polynet/config/schemas/__init__.py +47 -0
  62. polynet/config/schemas/applicability_domain.py +88 -0
  63. polynet/config/schemas/base.py +170 -0
  64. polynet/config/schemas/data.py +129 -0
  65. polynet/config/schemas/explainability.py +128 -0
  66. polynet/config/schemas/feature_preprocessing.py +101 -0
  67. polynet/config/schemas/fingerprints.py +174 -0
  68. polynet/config/schemas/general.py +47 -0
  69. polynet/config/schemas/plotting.py +59 -0
  70. polynet/config/schemas/representation.py +165 -0
  71. polynet/config/schemas/split_data.py +229 -0
  72. polynet/config/schemas/target_preprocessing.py +43 -0
  73. polynet/config/schemas/tml_explainability.py +116 -0
  74. polynet/config/schemas/training.py +473 -0
  75. polynet/config/search_grid.py +420 -0
  76. polynet/data/__init__.py +24 -0
  77. polynet/data/benchmarks/fluorine_mri_snr.csv +419 -0
  78. polynet/data/creator.py +193 -0
  79. polynet/data/feature_transformer.py +344 -0
  80. polynet/data/loader.py +245 -0
  81. polynet/data/preprocessing.py +406 -0
  82. polynet/data/sampling.py +134 -0
  83. polynet/data/structures.py +206 -0
  84. polynet/experiment/__init__.py +13 -0
  85. polynet/experiment/manager.py +50 -0
  86. polynet/explainability/__init__.py +124 -0
  87. polynet/explainability/attributions.py +37 -0
  88. polynet/explainability/embeddings.py +121 -0
  89. polynet/explainability/explain.py +959 -0
  90. polynet/explainability/fragment_figures.py +224 -0
  91. polynet/explainability/masking.py +635 -0
  92. polynet/explainability/selection.py +177 -0
  93. polynet/explainability/shap_explain.py +1359 -0
  94. polynet/explainability/visualization.py +850 -0
  95. polynet/factories/__init__.py +51 -0
  96. polynet/factories/dataloader.py +571 -0
  97. polynet/factories/loss.py +102 -0
  98. polynet/factories/network.py +187 -0
  99. polynet/factories/optimizer.py +218 -0
  100. polynet/featurizer/__init__.py +45 -0
  101. polynet/featurizer/allowable_sets.py +82 -0
  102. polynet/featurizer/descriptors.py +838 -0
  103. polynet/featurizer/graph.py +208 -0
  104. polynet/featurizer/pmx.py +450 -0
  105. polynet/featurizer/polymer_graph.py +508 -0
  106. polynet/featurizer/selection.py +278 -0
  107. polynet/inference/__init__.py +25 -0
  108. polynet/inference/ensemble.py +108 -0
  109. polynet/inference/gnn.py +157 -0
  110. polynet/inference/predict.py +154 -0
  111. polynet/inference/tml.py +155 -0
  112. polynet/inference/utils.py +108 -0
  113. polynet/models/__init__.py +66 -0
  114. polynet/models/base.py +665 -0
  115. polynet/models/gnn/__init__.py +40 -0
  116. polynet/models/gnn/cgcnn.py +248 -0
  117. polynet/models/gnn/cggnn.py +12 -0
  118. polynet/models/gnn/gat.py +161 -0
  119. polynet/models/gnn/gcn.py +211 -0
  120. polynet/models/gnn/graphsage.py +210 -0
  121. polynet/models/gnn/mpnn.py +230 -0
  122. polynet/models/gnn/transformer.py +163 -0
  123. polynet/models/persistence.py +237 -0
  124. polynet/pipeline/__init__.py +53 -0
  125. polynet/pipeline/runner.py +777 -0
  126. polynet/pipeline/self_check.py +348 -0
  127. polynet/pipeline/stages.py +1550 -0
  128. polynet/plotting/data_analysis.py +75 -0
  129. polynet/training/__init__.py +34 -0
  130. polynet/training/cv.py +55 -0
  131. polynet/training/evaluate.py +199 -0
  132. polynet/training/gnn.py +698 -0
  133. polynet/training/hyperopt.py +617 -0
  134. polynet/training/metrics.py +276 -0
  135. polynet/training/tml.py +477 -0
  136. polynet/utils/__init__.py +104 -0
  137. polynet/utils/chem_utils.py +383 -0
  138. polynet/utils/data_preprocessing.py +127 -0
  139. polynet/utils/graph_analysis.py +222 -0
  140. polynet/utils/optional_dependencies.py +91 -0
  141. polynet/utils/statistical_analysis.py +303 -0
  142. polynet/utils/validation.py +211 -0
  143. polynet/visualization/__init__.py +35 -0
  144. polynet/visualization/plots.py +480 -0
  145. polynet/visualization/utils.py +34 -0
  146. polynet-1.0.1.dist-info/METADATA +320 -0
  147. polynet-1.0.1.dist-info/RECORD +150 -0
  148. polynet-1.0.1.dist-info/WHEEL +4 -0
  149. polynet-1.0.1.dist-info/entry_points.txt +3 -0
  150. 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