autoencodix 0.2.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.

Potentially problematic release.


This version of autoencodix might be problematic. Click here for more details.

Files changed (91) hide show
  1. autoencodix/__init__.py +27 -0
  2. autoencodix/base/.ipynb_checkpoints/_base_pipeline-checkpoint.py +1274 -0
  3. autoencodix/base/.ipynb_checkpoints/_base_trainer-checkpoint.py +247 -0
  4. autoencodix/base/__init__.py +21 -0
  5. autoencodix/base/_base_autoencoder.py +138 -0
  6. autoencodix/base/_base_dataset.py +103 -0
  7. autoencodix/base/_base_evaluator.py +34 -0
  8. autoencodix/base/_base_loss.py +311 -0
  9. autoencodix/base/_base_pipeline.py +1355 -0
  10. autoencodix/base/_base_predictor.py +6 -0
  11. autoencodix/base/_base_preprocessor.py +1033 -0
  12. autoencodix/base/_base_trainer.py +259 -0
  13. autoencodix/base/_base_visualizer.py +578 -0
  14. autoencodix/base/sampleconfig.yaml +132 -0
  15. autoencodix/configs/__init__.py +21 -0
  16. autoencodix/configs/default_config.py +652 -0
  17. autoencodix/configs/disentanglix_config.py +15 -0
  18. autoencodix/configs/maskix_config.py +39 -0
  19. autoencodix/configs/ontix_config.py +35 -0
  20. autoencodix/configs/stackix_config.py +33 -0
  21. autoencodix/configs/vanillix_config.py +18 -0
  22. autoencodix/configs/varix_config.py +15 -0
  23. autoencodix/configs/xmodalix_config.py +43 -0
  24. autoencodix/disentanglix.py +72 -0
  25. autoencodix/evaluate/.ipynb_checkpoints/evaluate-checkpoint.py +532 -0
  26. autoencodix/evaluate/__init__.py +4 -0
  27. autoencodix/evaluate/_general_evaluator.py +730 -0
  28. autoencodix/evaluate/_xmodalix_evaluator.py +376 -0
  29. autoencodix/imagix.py +76 -0
  30. autoencodix/losses/disentanglix_loss.py +215 -0
  31. autoencodix/losses/maskix_loss.py +94 -0
  32. autoencodix/losses/vanillix_loss.py +28 -0
  33. autoencodix/losses/varix_loss.py +102 -0
  34. autoencodix/losses/xmodal_loss.py +304 -0
  35. autoencodix/maskix.py +108 -0
  36. autoencodix/modeling/.ipynb_checkpoints/__init__-checkpoint.py +16 -0
  37. autoencodix/modeling/.ipynb_checkpoints/_captum_forward-checkpoint.py +16 -0
  38. autoencodix/modeling/__init__.py +20 -0
  39. autoencodix/modeling/_captum_forward.py +20 -0
  40. autoencodix/modeling/_classifier.py +45 -0
  41. autoencodix/modeling/_imagevae_architecture.py +296 -0
  42. autoencodix/modeling/_imgfast_architecture.py +288 -0
  43. autoencodix/modeling/_layer_factory.py +71 -0
  44. autoencodix/modeling/_maskix_architecture.py +190 -0
  45. autoencodix/modeling/_ontix_architecture.py +305 -0
  46. autoencodix/modeling/_vanillix_architecture.py +136 -0
  47. autoencodix/modeling/_varix_architecture.py +192 -0
  48. autoencodix/notebooks/__init__.py +0 -0
  49. autoencodix/ontix.py +136 -0
  50. autoencodix/py.typed +0 -0
  51. autoencodix/stackix.py +122 -0
  52. autoencodix/trainers/__init__.py +6 -0
  53. autoencodix/trainers/_general_trainer.py +525 -0
  54. autoencodix/trainers/_maskix_trainer.py +116 -0
  55. autoencodix/trainers/_ontix_trainer.py +77 -0
  56. autoencodix/trainers/_stackix_orchestrator.py +522 -0
  57. autoencodix/trainers/_stackix_trainer.py +209 -0
  58. autoencodix/trainers/_xmodal_trainer.py +1006 -0
  59. autoencodix/utils/.ipynb_checkpoints/_explainer-checkpoint.py +118 -0
  60. autoencodix/utils/.ipynb_checkpoints/_model_output-checkpoint.py +30 -0
  61. autoencodix/utils/.ipynb_checkpoints/_utils-checkpoint.py +626 -0
  62. autoencodix/utils/.ipynb_checkpoints/adata_converter-checkpoint.py +99 -0
  63. autoencodix/utils/.ipynb_checkpoints/feature_importance-checkpoint.py +823 -0
  64. autoencodix/utils/__init__.py +22 -0
  65. autoencodix/utils/_annealer.py +193 -0
  66. autoencodix/utils/_bulkreader.py +232 -0
  67. autoencodix/utils/_explainer.py +246 -0
  68. autoencodix/utils/_imgreader.py +404 -0
  69. autoencodix/utils/_internals.py +23 -0
  70. autoencodix/utils/_llm_explainer.py +318 -0
  71. autoencodix/utils/_losses.py +20 -0
  72. autoencodix/utils/_model_output.py +30 -0
  73. autoencodix/utils/_result.py +393 -0
  74. autoencodix/utils/_screader.py +76 -0
  75. autoencodix/utils/_traindynamics.py +187 -0
  76. autoencodix/utils/_utils.py +690 -0
  77. autoencodix/utils/adata_converter.py +111 -0
  78. autoencodix/utils/example_data.py +398 -0
  79. autoencodix/utils/feature_importance.py +829 -0
  80. autoencodix/utils/prompts.py +21 -0
  81. autoencodix/vanillix.py +141 -0
  82. autoencodix/varix.py +80 -0
  83. autoencodix/visualize/__init__.py +5 -0
  84. autoencodix/visualize/_general_visualizer.py +913 -0
  85. autoencodix/visualize/_imagix_visualizer.py +111 -0
  86. autoencodix/visualize/_xmodal_visualizer.py +841 -0
  87. autoencodix/visualize/visualize.py +1214 -0
  88. autoencodix/xmodalix.py +318 -0
  89. autoencodix-0.2.0.dist-info/METADATA +143 -0
  90. autoencodix-0.2.0.dist-info/RECORD +91 -0
  91. autoencodix-0.2.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,1214 @@
1
+ import os
2
+ from dataclasses import field
3
+ from typing import Any, Dict, Optional, Union, Literal, no_type_check
4
+ import warnings
5
+
6
+ import matplotlib.figure
7
+ import numpy as np
8
+ import pandas as pd
9
+ import seaborn as sns # type: ignore
10
+ import torch
11
+ from matplotlib import pyplot as plt
12
+ from umap import UMAP # type: ignore
13
+
14
+ from autoencodix.base._base_visualizer import BaseVisualizer
15
+ from autoencodix.utils._result import Result
16
+ from autoencodix.utils._utils import nested_dict, nested_to_tuple, show_figure
17
+ from autoencodix.configs.default_config import DefaultConfig
18
+
19
+
20
+ class Visualizer(BaseVisualizer):
21
+ plots: Dict[str, Any] = field(
22
+ default_factory=nested_dict
23
+ ) ## Nested dictionary of plots as figure handles
24
+
25
+ def __init__(self):
26
+ self.plots = nested_dict()
27
+
28
+ def __setitem__(self, key, elem):
29
+ self.plots[key] = elem
30
+
31
+ def visualize(self, result: Result, config: DefaultConfig) -> Result:
32
+ ## Make Model Weights plot
33
+ self.plots["ModelWeights"] = self.plot_model_weights(model=result.model)
34
+
35
+ ## Make long format of losses
36
+ loss_df_melt = self.make_loss_format(result=result, config=config)
37
+
38
+ ## Make plot loss absolute
39
+ self.plots["loss_absolute"] = self.make_loss_plot(
40
+ df_plot=loss_df_melt, plot_type="absolute"
41
+ )
42
+ ## Make plot loss relative
43
+ self.plots["loss_relative"] = self.make_loss_plot(
44
+ df_plot=loss_df_melt, plot_type="relative"
45
+ )
46
+
47
+ return result
48
+
49
+ ## Plotting methods ##
50
+
51
+ def save_plots(
52
+ self, path: str, which: Union[str, list] = "all", format: str = "png"
53
+ ) -> None:
54
+ """Save specified plots to the given path in the specified format.
55
+
56
+ Args:
57
+ path: The directory path where the plots will be saved.
58
+ which: A list of plot names to save or a string specifying which plots to save.
59
+ If 'all', all plots in the plots dictionary will be saved.
60
+ If a single plot name is provided as a string, only that plot will be saved.
61
+ format: The file format in which to save the plots (e.g., 'png', 'jpg').
62
+
63
+ Raises:
64
+ ValueError: If the 'which' parameter is not a list or a string.
65
+ """
66
+ if not isinstance(which, list):
67
+ ## Case when which is a string
68
+ if which == "all":
69
+ ## Case when all plots are to be saved
70
+ if len(self.plots) == 0:
71
+ print("No plots found in the plots dictionary")
72
+ print("You need to run visualize() method first")
73
+ else:
74
+ for item in nested_to_tuple(self.plots):
75
+ fig = item[-1] ## Figure is in last element of the tuple
76
+ filename = "_".join(str(x) for x in item[0:-1])
77
+ fullpath = os.path.join(path, filename)
78
+ fig.savefig(f"{fullpath}.{format}")
79
+ else:
80
+ ## Case when a single plot is provided as string
81
+ if which not in self.plots.keys():
82
+ print(f"Plot {which} not found in the plots dictionary")
83
+ print(f"All available plots are: {list(self.plots.keys())}")
84
+ else:
85
+ for item in nested_to_tuple(
86
+ self.plots[which]
87
+ ): # Plot all epochs and splits of type which
88
+ fig = item[-1] ## Figure is in last element of the tuple
89
+ filename = (
90
+ which # ty: ignore
91
+ + "_"
92
+ + "_".join(str(x) for x in item[0:-1])
93
+ )
94
+ fullpath = os.path.join(path, filename)
95
+ fig.savefig(f"{fullpath}.{format}")
96
+ else:
97
+ ## Case when which is a list of plot specified as strings
98
+ for key in which:
99
+ if key not in self.plots.keys():
100
+ print(f"Plot {key} not found in the plots dictionary")
101
+ print(f"All available plots are: {list(self.plots.keys())}")
102
+ continue
103
+ else:
104
+ for item in nested_to_tuple(
105
+ self.plots[key]
106
+ ): # Plot all epochs and splits of type key
107
+ fig = item[-1] ## Figure is in last element of the tuple
108
+ filename = key + "_" + "_".join(str(x) for x in item[0:-1])
109
+ fullpath = os.path.join(path, filename)
110
+ fig.savefig(f"{fullpath}.{format}")
111
+
112
+ def show_loss(
113
+ self, plot_type: Literal["absolute", "relative"] = "absolute"
114
+ ) -> None:
115
+ """Display the loss plot.
116
+
117
+ Args:
118
+ plot_type: The type of loss plot to display. Defaults to "absolute".
119
+ """
120
+ if plot_type == "absolute":
121
+ if "loss_absolute" not in self.plots.keys():
122
+ print("Absolute loss plot not found in the plots dictionary")
123
+ print("You need to run visualize() method first")
124
+ else:
125
+ fig = self.plots["loss_absolute"]
126
+ show_figure(fig)
127
+ plt.show()
128
+ if plot_type == "relative":
129
+ if "loss_relative" not in self.plots.keys():
130
+ print("Relative loss plot not found in the plots dictionary")
131
+ print("You need to run visualize() method first")
132
+ else:
133
+ fig = self.plots["loss_relative"]
134
+ show_figure(fig)
135
+ plt.show()
136
+
137
+ if plot_type not in ["absolute", "relative"]:
138
+ print(
139
+ "Type of loss plot not recognized. Please use 'absolute' or 'relative'"
140
+ )
141
+
142
+ @no_type_check
143
+ def show_latent_space(
144
+ self,
145
+ result: Result,
146
+ plot_type: str = "2D-scatter",
147
+ labels: Optional[Union[list, pd.Series, None]] = None,
148
+ param: Optional[Union[list, str]] = None,
149
+ epoch: Optional[Union[int, None]] = None,
150
+ split: str = "all",
151
+ **kwargs,
152
+ ) -> None:
153
+ """Visualizes the latent space of the given result using different types of plots.
154
+
155
+ Args:
156
+ result: The result object containing latent spaces and losses.
157
+ plot_type The type of plot to generate. Options are "2D-scatter", "Ridgeline", and "Coverage-Correlation". Default is "2D-scatter".
158
+ labels: List of labels for the data points in the latent space. Default is None.
159
+ param : List of parameters provided and stored as metadata. Strings must match column names. If not a list, string "all" is expected for convenient way to make plots for all parameters available. Default is None where no colored labels are plotted.
160
+ epoch: The epoch number to visualize. If None, the last epoch is inferred from the losses. Default is None.
161
+ split: The data split to visualize. Options are "train", "valid", "test", and "all". Default is "all".
162
+
163
+ """
164
+ plt.ioff()
165
+ if plot_type == "Coverage-Correlation":
166
+ if "Coverage-Correlation" in self.plots:
167
+ fig = self.plots["Coverage-Correlation"]
168
+ show_figure(fig)
169
+ plt.show()
170
+ else:
171
+ results = []
172
+ for epoch in range(
173
+ result.model.config.checkpoint_interval,
174
+ result.model.config.epochs + 1,
175
+ result.model.config.checkpoint_interval,
176
+ ):
177
+ for split in ["train", "valid"]:
178
+ latent_df = result.get_latent_df(epoch=epoch - 1, split=split)
179
+ tc = self._total_correlation(latent_df)
180
+ cov = self._coverage_calc(latent_df)
181
+ results.append(
182
+ {
183
+ "epoch": epoch,
184
+ "split": split,
185
+ "total_correlation": tc,
186
+ "coverage": cov,
187
+ }
188
+ )
189
+
190
+ df_metrics = pd.DataFrame(results)
191
+
192
+ fig, axes = plt.subplots(1, 2, figsize=(12, 5))
193
+
194
+ # Total Correlation plot
195
+ _ = sns.lineplot(
196
+ data=df_metrics,
197
+ x="epoch",
198
+ y="total_correlation",
199
+ hue="split",
200
+ ax=axes[0],
201
+ )
202
+ axes[0].set_title("Total Correlation")
203
+ axes[0].set_xlabel("Epoch")
204
+ axes[0].set_ylabel("Total Correlation")
205
+
206
+ # Coverage plot
207
+ _ = sns.lineplot(
208
+ data=df_metrics, x="epoch", y="coverage", hue="split", ax=axes[1]
209
+ )
210
+ axes[1].set_title("Coverage")
211
+ axes[1].set_xlabel("Epoch")
212
+ axes[1].set_ylabel("Coverage")
213
+
214
+ plt.tight_layout()
215
+ self.plots["Coverage-Correlation"] = fig
216
+ show_figure(fig)
217
+ plt.show()
218
+
219
+ else:
220
+ # Set Defaults
221
+ if epoch is None:
222
+ epoch = result.model.config.epochs - 1
223
+
224
+ ## Getting clin_data
225
+ if not hasattr(result.datasets, "train"):
226
+ raise ValueError("no train split in datasets")
227
+
228
+ if not hasattr(result.datasets, "valid"):
229
+ raise ValueError("no valid split in datasets")
230
+ if result.datasets.train is None:
231
+ raise ValueError("train is None")
232
+ if result.datasets.valid is None:
233
+ raise ValueError("train is None")
234
+ if result.datasets.test is None:
235
+ raise ValueError("train is None")
236
+
237
+ if not hasattr(result.datasets.train, "metadata"):
238
+ raise ValueError("train dataset has no metadata")
239
+ if not hasattr(result.datasets.valid, "metadata"):
240
+ raise ValueError("valid dataset has no metadata")
241
+
242
+ # Check if metadata is a dictionary and contains 'paired'
243
+ if isinstance(result.datasets.train.metadata, dict):
244
+ if "paired" in result.datasets.train.metadata:
245
+ clin_data = result.datasets.train.metadata["paired"]
246
+ if hasattr(result.datasets, "test"):
247
+ clin_data = pd.concat(
248
+ [clin_data, result.datasets.test.metadata["paired"]],
249
+ axis=0,
250
+ )
251
+ if hasattr(result.datasets, "valid"):
252
+ clin_data = pd.concat(
253
+ [clin_data, result.datasets.valid.metadata["paired"]],
254
+ axis=0,
255
+ )
256
+ else:
257
+ # Raise error no annotation given
258
+ raise ValueError(
259
+ "Please provide paired annotation data with key 'paired' in metadata dictionary."
260
+ )
261
+ elif isinstance(result.datasets.train.metadata, pd.DataFrame):
262
+ clin_data = result.datasets.train.metadata
263
+ if hasattr(result.datasets, "test"):
264
+ clin_data = pd.concat(
265
+ [clin_data, result.datasets.test.metadata],
266
+ axis=0,
267
+ )
268
+ if hasattr(result.datasets, "valid"):
269
+ clin_data = pd.concat(
270
+ [clin_data, result.datasets.valid.metadata],
271
+ axis=0,
272
+ )
273
+ else:
274
+ # Raise error no annotation given
275
+ raise ValueError(
276
+ "Metadata is not a dictionary or DataFrame. Please provide a valid annotation data type."
277
+ )
278
+ else:
279
+ # Raise error no annotation given
280
+ raise ValueError(
281
+ "No annotation data found. Please provide a valid annotation data type."
282
+ )
283
+
284
+ if split == "all":
285
+ df_latent = pd.concat(
286
+ [
287
+ result.get_latent_df(epoch=epoch, split="train"),
288
+ result.get_latent_df(epoch=epoch, split="valid"),
289
+ result.get_latent_df(epoch=-1, split="test"),
290
+ ]
291
+ )
292
+ else:
293
+ if split == "test":
294
+ df_latent = result.get_latent_df(epoch=-1, split=split)
295
+ else:
296
+ df_latent = result.get_latent_df(epoch=epoch, split=split)
297
+
298
+ if labels is None and param is None:
299
+ labels = ["all"] * df_latent.shape[0]
300
+
301
+ if labels is None and isinstance(param, str):
302
+ if param == "all":
303
+ param = list(clin_data.columns)
304
+ else:
305
+ raise ValueError(
306
+ "Please provide parameter to plot as a list not as string. If you want to plot all parameters, set param to 'all' and labels to None."
307
+ )
308
+
309
+ if labels is not None and param is not None:
310
+ raise ValueError(
311
+ "Please provide either labels or param, not both. If you want to plot all parameters, set param to 'all' and labels to None."
312
+ )
313
+
314
+ if labels is not None and param is None:
315
+ if isinstance(labels, pd.Series):
316
+ param = [labels.name]
317
+ # Order by index of df_latent first, fill missing with "unknown"
318
+ labels = labels.reindex(
319
+ df_latent.index, fill_value="unknown"
320
+ ).tolist()
321
+ else:
322
+ param = ["user_label"] # Default label if none provided
323
+
324
+ for p in param:
325
+ if p in clin_data.columns:
326
+ labels = clin_data.loc[df_latent.index, p].tolist()
327
+
328
+ if plot_type == "2D-scatter":
329
+ ## Make 2D Embedding with UMAP
330
+ if df_latent.shape[1] > 2:
331
+ reducer = UMAP(n_components=2)
332
+ embedding = pd.DataFrame(reducer.fit_transform(df_latent))
333
+ else:
334
+ embedding = df_latent
335
+
336
+ self.plots["2D-scatter"][epoch][split][p] = self.plot_2D(
337
+ embedding=embedding,
338
+ labels=labels,
339
+ param=p,
340
+ layer=f"2D latent space (epoch {epoch + 1})", # we start counting epochs at 0, so add 1 for display
341
+ figsize=(12, 8),
342
+ center=True,
343
+ )
344
+
345
+ fig = self.plots["2D-scatter"][epoch][split][p]
346
+ show_figure(fig)
347
+ plt.show()
348
+
349
+ if plot_type == "Ridgeline":
350
+ ## Make ridgeline plot
351
+
352
+ self.plots["Ridgeline"][epoch][split][p] = self.plot_latent_ridge(
353
+ lat_space=df_latent, labels=labels, param=p
354
+ )
355
+
356
+ fig = self.plots["Ridgeline"][epoch][split][p].figure
357
+ show_figure(fig)
358
+ plt.show()
359
+
360
+ def show_weights(self) -> None:
361
+ """Display the model weights plot if it exists in the plots dictionary."""
362
+
363
+ if "ModelWeights" not in self.plots.keys():
364
+ print("Model weights not found in the plots dictionary")
365
+ print("You need to run visualize() method first")
366
+ else:
367
+ fig = self.plots["ModelWeights"]
368
+ show_figure(fig)
369
+ plt.show()
370
+
371
+ # def plot_model_weights(model: torch.nn.Module) -> matplotlib.figure.Figure:
372
+ # """
373
+ # Visualization of model weights in encoder and decoder layers as heatmap for each layer as subplot.
374
+ # ARGS:
375
+ # model (torch.nn.Module): PyTorch model instance.
376
+ # filepath (str): Path specifying save name and location.
377
+ # RETURNS:
378
+ # fig (matplotlib.figure): Figure handle (of last plot)
379
+ # """
380
+ # all_weights = []
381
+ # names = []
382
+ # if hasattr(model, "ontologies"):
383
+ # if model.ontologies is not None:
384
+ # # If model is Ontix
385
+ # # Get node names from ontologies
386
+ # node_names = list()
387
+ # for ontology in model.ontologies:
388
+ # node_names.append(ontology.keys())
389
+
390
+ # node_names.append(model.feature_order) # Add feature order as last layer
391
+
392
+ # for name, param in model.named_parameters():
393
+ # if "weight" in name and len(param.shape) == 2:
394
+ # if "var" not in name: ## For VAE plot only mu weights
395
+ # all_weights.append(param.detach().cpu().numpy())
396
+ # names.append(name[:-7])
397
+
398
+ # layers = int(len(all_weights) / 2)
399
+ # fig, axes = plt.subplots(2, layers, sharex=False, figsize=(20, 10))
400
+
401
+ # for layer in range(layers):
402
+ # ## Encoder Layer
403
+ # if layers > 1:
404
+ # sns.heatmap(
405
+ # all_weights[layer],
406
+ # cmap=sns.color_palette("Spectral", as_cmap=True),
407
+ # ax=axes[0, layer],
408
+ # ).set(title=names[layer])
409
+ # ## Decoder Layer
410
+ # sns.heatmap(
411
+ # all_weights[layers + layer],
412
+ # cmap=sns.color_palette("Spectral", as_cmap=True),
413
+ # ax=axes[1, layer],
414
+ # ).set(title=names[layers + layer])
415
+ # axes[1, layer].set_xlabel("In Node", size=12)
416
+ # if model.ontologies is not None:
417
+ # axes[1, layer].set_xticks(
418
+ # ticks=range(len(node_names[layer])),
419
+ # labels=node_names[layer],
420
+ # rotation=90,
421
+ # fontsize=8,
422
+ # )
423
+ # axes[1, layer].set_yticks(
424
+ # ticks=range(len(node_names[layer + 1])),
425
+ # labels=node_names[layer + 1],
426
+ # rotation=0,
427
+ # fontsize=8,
428
+ # )
429
+ # else:
430
+ # sns.heatmap(
431
+ # all_weights[layer],
432
+ # cmap=sns.color_palette("Spectral", as_cmap=True),
433
+ # ax=axes[layer],
434
+ # ).set(title=names[layer])
435
+ # ## Decoder Layer
436
+ # sns.heatmap(
437
+ # all_weights[layer + 2],
438
+ # cmap=sns.color_palette("Spectral", as_cmap=True),
439
+ # ax=axes[layer + 1],
440
+ # ).set(title=names[layer + 2])
441
+ # axes[1].set_xlabel("In Node", size=12)
442
+
443
+ # if layers > 1:
444
+ # axes[1, 0].set_ylabel("Out Node", size=12)
445
+ # axes[0, 0].set_ylabel("Out Node", size=12)
446
+ # else:
447
+ # axes[1].set_ylabel("Out Node", size=12)
448
+ # axes[0].set_ylabel("Out Node", size=12)
449
+
450
+ # ## Add title
451
+ # fig.suptitle("Model Weights", size=20)
452
+ # plt.close()
453
+ # return fig
454
+
455
+ ## NEW VERSION
456
+ # @staticmethod
457
+ # def plot_model_weights(model: torch.nn.Module) -> matplotlib.figure.Figure:
458
+ # """
459
+ # Visualization of model weights in encoder and decoder layers as heatmap for each layer as subplot.
460
+ # ARGS:
461
+ # model (torch.nn.Module): PyTorch model instance.
462
+ # filepath (str): Path specifying save name and location.
463
+ # RETURNS:
464
+ # fig (matplotlib.figure): Figure handle (of last plot)
465
+ # """
466
+ # all_weights = []
467
+ # names = []
468
+ # if hasattr(model, "ontologies"):
469
+ # if model.ontologies is not None:
470
+ # # If model is Ontix
471
+ # # Get node names from ontologies
472
+ # node_names = list()
473
+ # for ontology in model.ontologies:
474
+ # node_names.append(ontology.keys())
475
+
476
+ # node_names.append(model.feature_order) # Add feature order as last layer
477
+
478
+ # for name, param in model.named_parameters():
479
+ # if "weight" in name and len(param.shape) == 2:
480
+ # if "var" not in name: ## For VAE plot only mu weights
481
+ # all_weights.append(param.detach().cpu().numpy())
482
+ # names.append(name[:-7])
483
+
484
+ # layers = int(len(all_weights) / 2)
485
+ # fig, axes = plt.subplots(2, layers, sharex=False, figsize=(20, 10))
486
+
487
+ # for layer in range(layers):
488
+ # ## Encoder Layer
489
+ # if layers > 1:
490
+ # sns.heatmap(
491
+ # all_weights[layer],
492
+ # cmap=sns.color_palette("Spectral", as_cmap=True),
493
+ # ax=axes[0, layer],
494
+ # ).set(title=names[layer])
495
+ # ## Decoder Layer
496
+ # sns.heatmap(
497
+ # all_weights[layers + layer],
498
+ # cmap=sns.color_palette("Spectral", as_cmap=True),
499
+ # ax=axes[1, layer],
500
+ # ).set(title=names[layers + layer])
501
+ # axes[1, layer].set_xlabel("In Node", size=12)
502
+ # if model.ontologies is not None:
503
+ # axes[1, layer].set_xticks(
504
+ # ticks=range(len(node_names[layer])),
505
+ # labels=node_names[layer],
506
+ # rotation=90,
507
+ # fontsize=8,
508
+ # )
509
+ # axes[1, layer].set_yticks(
510
+ # ticks=range(len(node_names[layer + 1])),
511
+ # labels=node_names[layer + 1],
512
+ # rotation=0,
513
+ # fontsize=8,
514
+ # )
515
+ # else:
516
+ # sns.heatmap(
517
+ # all_weights[layer],
518
+ # cmap=sns.color_palette("Spectral", as_cmap=True),
519
+ # ax=axes[layer],
520
+ # ).set(title=names[layer])
521
+ # ## Decoder Layer
522
+ # sns.heatmap(
523
+ # all_weights[layer + 2],
524
+ # cmap=sns.color_palette("Spectral", as_cmap=True),
525
+ # ax=axes[layer + 1],
526
+ # ).set(title=names[layer + 2])
527
+ # axes[1].set_xlabel("In Node", size=12)
528
+
529
+ # if layers > 1:
530
+ # axes[1, 0].set_ylabel("Out Node", size=12)
531
+ # axes[0, 0].set_ylabel("Out Node", size=12)
532
+ # else:
533
+ # axes[1].set_ylabel("Out Node", size=12)
534
+ # axes[0].set_ylabel("Out Node", size=12)
535
+
536
+ # ## Add title
537
+ # fig.suptitle("Model Weights", size=20)
538
+ # plt.close()
539
+ # return fig
540
+
541
+ ## NEW VERSION
542
+ def plot_model_weights(model: torch.nn.Module) -> matplotlib.figure.Figure:
543
+ """Visualization of model weights in encoder and decoder layers as heatmap for each layer as subplot.
544
+
545
+ Handles non-symmetrical autoencoder architectures.
546
+ Plots _mu layer for encoder as well.
547
+ Uses node_names for decoder layers if model has ontologies.
548
+
549
+ Args:
550
+ model: PyTorch model instance.
551
+ Returns:
552
+ fig: Figure handle (of last plot)
553
+ """
554
+ all_weights = []
555
+ names = []
556
+ node_names = []
557
+ if hasattr(model, "ontologies"):
558
+ if model.ontologies is not None:
559
+ node_names = []
560
+ for ontology in model.ontologies:
561
+ node_names.append(list(ontology.keys()))
562
+ node_names.append(model.feature_order)
563
+
564
+ # Collect encoder and decoder weights separately
565
+ encoder_weights = []
566
+ encoder_names = []
567
+ decoder_weights = []
568
+ decoder_names = []
569
+ for name, param in model.named_parameters():
570
+ # print(name)
571
+ if "weight" in name and len(param.shape) == 2:
572
+ if "encoder" in name and "var" not in name and "_mu" not in name:
573
+ encoder_weights.append(param.detach().cpu().numpy())
574
+ encoder_names.append(name[:-7])
575
+ elif "_mu" in name:
576
+ encoder_weights.append(param.detach().cpu().numpy())
577
+ encoder_names.append(name[:-7])
578
+ elif "decoder" in name and "var" not in name:
579
+ decoder_weights.append(param.detach().cpu().numpy())
580
+ decoder_names.append(name[:-7])
581
+ elif (
582
+ "encoder" not in name
583
+ and "decoder" not in name
584
+ and "var" not in name
585
+ ):
586
+ # fallback for models without explicit encoder/decoder in name
587
+ all_weights.append(param.detach().cpu().numpy())
588
+ names.append(name[:-7])
589
+
590
+ if encoder_weights or decoder_weights:
591
+ n_enc = len(encoder_weights)
592
+ n_dec = len(decoder_weights)
593
+ n_cols = max(n_enc, n_dec)
594
+ fig, axes = plt.subplots(2, n_cols, sharex=False, figsize=(15 * n_cols, 15))
595
+ if n_cols == 1:
596
+ axes = axes.reshape(2, 1)
597
+ # Plot encoder weights
598
+ for i in range(n_enc):
599
+ ax = axes[0, i]
600
+ sns.heatmap(
601
+ encoder_weights[i],
602
+ cmap=sns.color_palette("Spectral", as_cmap=True),
603
+ center=0,
604
+ ax=ax,
605
+ ).set(title=encoder_names[i])
606
+ ax.set_ylabel("Out Node", size=12)
607
+ # Hide unused encoder subplots
608
+ for i in range(n_enc, n_cols):
609
+ axes[0, i].axis("off")
610
+ # Plot decoder weights
611
+ for i in range(n_dec):
612
+ ax = axes[1, i]
613
+ heatmap_kwargs = {}
614
+
615
+ sns.heatmap(
616
+ decoder_weights[i],
617
+ cmap=sns.color_palette("Spectral", as_cmap=True),
618
+ center=0,
619
+ ax=ax,
620
+ **heatmap_kwargs,
621
+ ).set(title=decoder_names[i])
622
+ if model.ontologies is not None:
623
+ axes[1, i].set_xticks(
624
+ ticks=range(len(node_names[i])),
625
+ labels=node_names[i],
626
+ rotation=90,
627
+ fontsize=8,
628
+ )
629
+ axes[1, i].set_yticks(
630
+ ticks=range(len(node_names[i + 1])),
631
+ labels=node_names[i + 1],
632
+ rotation=0,
633
+ fontsize=8,
634
+ )
635
+ ax.set_xlabel("In Node", size=12)
636
+ ax.set_ylabel("Out Node", size=12)
637
+ # Hide unused decoder subplots
638
+ for i in range(n_dec, n_cols):
639
+ axes[1, i].axis("off")
640
+ else:
641
+ # fallback: plot all weights in order, split in half for encoder/decoder
642
+ n_layers = len(all_weights) // 2
643
+ fig, axes = plt.subplots(
644
+ 2, n_layers, sharex=False, figsize=(5 * n_layers, 10)
645
+ )
646
+ for layer in range(n_layers):
647
+ sns.heatmap(
648
+ all_weights[layer],
649
+ cmap=sns.color_palette("Spectral", as_cmap=True),
650
+ center=0,
651
+ ax=axes[0, layer],
652
+ ).set(title=names[layer])
653
+ sns.heatmap(
654
+ all_weights[n_layers + layer],
655
+ cmap=sns.color_palette("Spectral", as_cmap=True),
656
+ center=0,
657
+ ax=axes[1, layer],
658
+ ).set(title=names[n_layers + layer])
659
+ axes[1, layer].set_xlabel("In Node", size=12)
660
+ axes[0, layer].set_ylabel("Out Node", size=12)
661
+ axes[1, layer].set_ylabel("Out Node", size=12)
662
+
663
+ fig.suptitle("Model Weights", size=20)
664
+ plt.close()
665
+ return fig
666
+
667
+ @staticmethod
668
+ def plot_2D(
669
+ embedding: pd.DataFrame,
670
+ labels: list,
671
+ param: Optional[Union[str, None]] = None,
672
+ layer: str = "latent space",
673
+ figsize: tuple = (24, 15),
674
+ center: bool = True,
675
+ plot_numeric: bool = False,
676
+ xlim: Optional[Union[tuple, None]] = None,
677
+ ylim: Optional[Union[tuple, None]] = None,
678
+ scale: Optional[Union[str, None]] = None,
679
+ no_leg: bool = False,
680
+ ) -> matplotlib.figure.Figure:
681
+ """Plots a 2D scatter plot of the given embedding with labels.
682
+
683
+ Args:
684
+ embedding: DataFrame containing the 2D embedding coordinates.
685
+ labels: List of labels corresponding to each point in the embedding.
686
+ param: Title for the legend. Defaults to None.
687
+ layer: Title for the plot. Defaults to "latent space".
688
+ figsize: Size of the figure. Defaults to (24, 15).
689
+ center: If True, centers the plot based on label means. Defaults to True.
690
+ plot_numeric Defaults to False.
691
+ xlim: Defaults to None.
692
+ ylim: Defaults to None.
693
+ scale: Defaults to None.
694
+ no_leg: Defaults to False.
695
+
696
+ Returns:
697
+ The resulting matplotlib figure.
698
+ """
699
+
700
+ numeric = False
701
+ if not isinstance(labels[0], str):
702
+ if len(np.unique(labels)) > 3:
703
+ if not plot_numeric:
704
+ print(
705
+ "The provided label column is numeric and converted to categories."
706
+ )
707
+ # Change non-float labels to NaN
708
+ labels = [
709
+ x if isinstance(x, float) else float("nan") for x in labels
710
+ ]
711
+ labels = (
712
+ pd.qcut(
713
+ x=pd.Series(labels),
714
+ q=4,
715
+ labels=["1stQ", "2ndQ", "3rdQ", "4thQ"],
716
+ )
717
+ .astype(str)
718
+ .to_list()
719
+ )
720
+ else:
721
+ center = False ## Disable centering for numeric params
722
+ numeric = True
723
+ else:
724
+ labels = [str(x) for x in labels]
725
+
726
+ fig, ax1 = plt.subplots(figsize=figsize)
727
+
728
+ # check if label or embedding is longerm and duplicate the shorter one
729
+ if len(labels) < embedding.shape[0]:
730
+ print(
731
+ "Given labels do not have the same length as given sample size. Labels will be duplicated."
732
+ )
733
+ labels = [
734
+ label
735
+ for label in labels
736
+ for _ in range(embedding.shape[0] // len(labels))
737
+ ]
738
+ elif len(labels) > embedding.shape[0]:
739
+ labels = list(set(labels))
740
+
741
+ if numeric:
742
+ ax2 = sns.scatterplot(
743
+ x=embedding.iloc[:, 0],
744
+ y=embedding.iloc[:, 1],
745
+ hue=labels,
746
+ palette="bwr",
747
+ s=40,
748
+ alpha=0.5,
749
+ ec="black",
750
+ )
751
+ else:
752
+ ax2 = sns.scatterplot(
753
+ x=embedding.iloc[:, 0],
754
+ y=embedding.iloc[:, 1],
755
+ hue=labels,
756
+ hue_order=np.unique(labels),
757
+ s=40,
758
+ alpha=0.5,
759
+ ec="black",
760
+ )
761
+ if center:
762
+ means = embedding.groupby(by=labels).mean()
763
+
764
+ ax2 = sns.scatterplot(
765
+ x=means.iloc[:, 0],
766
+ y=means.iloc[:, 1],
767
+ hue=np.unique(labels),
768
+ hue_order=np.unique(labels),
769
+ s=200,
770
+ ec="black",
771
+ alpha=0.9,
772
+ marker="*",
773
+ legend=False,
774
+ ax=ax2,
775
+ )
776
+
777
+ if xlim is not None:
778
+ ax2.set_xlim(xlim[0], xlim[1])
779
+
780
+ if ylim is not None:
781
+ ax2.set_ylim(ylim[0], ylim[1])
782
+
783
+ if scale is not None:
784
+ plt.yscale(scale)
785
+ plt.xscale(scale)
786
+ ax2.set_xlabel("Dim 1")
787
+ ax2.set_ylabel("Dim 2")
788
+ legend_cols = 1
789
+ if len(np.unique(labels)) > 10:
790
+ legend_cols = 2
791
+
792
+ if no_leg:
793
+ plt.legend([], [], frameon=False)
794
+ else:
795
+ sns.move_legend(
796
+ ax2,
797
+ "upper left",
798
+ bbox_to_anchor=(1, 1),
799
+ ncol=legend_cols,
800
+ title=param,
801
+ frameon=False,
802
+ )
803
+
804
+ # Add title to the plot
805
+ ax2.set_title(layer)
806
+
807
+ plt.close()
808
+ return fig
809
+
810
+ @staticmethod
811
+ def plot_latent_ridge(
812
+ lat_space: pd.DataFrame,
813
+ labels: Optional[Union[list, pd.Series, None]] = None,
814
+ param: Optional[Union[str, None]] = None,
815
+ ) -> sns.FacetGrid:
816
+ """Creates a ridge line plot of latent space dimension where each row shows the density of a latent dimension and groups (ridges).
817
+ Args:
818
+ lat_space: If None, all samples are considered as one group.
819
+ param: Must be a column name (str) of clin_data
820
+ Returns:
821
+ g: FacetGrid object containing the ridge line plot
822
+ """
823
+ sns.set_theme(
824
+ style="white", rc={"axes.facecolor": (0, 0, 0, 0)}
825
+ ) ## Necessary to enforce overplotting
826
+
827
+ df = pd.melt(lat_space, var_name="latent dim", value_name="latent intensity")
828
+ df["sample"] = len(lat_space.columns) * list(lat_space.index)
829
+
830
+ if labels is None:
831
+ param = "all"
832
+ labels = ["all"] * len(df)
833
+
834
+ # print(labels[0])
835
+ if not isinstance(labels[0], str):
836
+ if len(np.unique(labels)) > 3:
837
+ # Change non-float labels to NaN
838
+ labels = [x if isinstance(x, float) else float("nan") for x in labels]
839
+ labels = pd.qcut(
840
+ x=pd.Series(labels),
841
+ q=4,
842
+ labels=["1stQ", "2ndQ", "3rdQ", "4thQ"],
843
+ ).astype(str)
844
+ else:
845
+ labels = [str(x) for x in labels]
846
+
847
+ df[param] = len(lat_space.columns) * labels # type: ignore
848
+
849
+ exclude_missing_info = (df[param] == "unknown") | (df[param] == "nan")
850
+
851
+ xmin = (
852
+ df.loc[~exclude_missing_info, ["latent intensity", "latent dim", param]]
853
+ .groupby([param, "latent dim"], observed=False)
854
+ .quantile(0.05)
855
+ .min()
856
+ )
857
+ xmax = (
858
+ df.loc[~exclude_missing_info, ["latent intensity", "latent dim", param]]
859
+ .groupby([param, "latent dim"], observed=False)
860
+ .quantile(0.9)
861
+ .max()
862
+ )
863
+
864
+ if len(np.unique(df[param])) > 8:
865
+ cat_pal = sns.husl_palette(len(np.unique(df[param])))
866
+ else:
867
+ cat_pal = sns.color_palette(n_colors=len(np.unique(df[param])))
868
+
869
+ g = sns.FacetGrid(
870
+ df[~exclude_missing_info],
871
+ row="latent dim",
872
+ hue=param,
873
+ aspect=12,
874
+ height=0.8,
875
+ xlim=(xmin.iloc[0], xmax.iloc[0]),
876
+ palette=cat_pal,
877
+ )
878
+
879
+ g.map_dataframe(
880
+ sns.kdeplot,
881
+ "latent intensity",
882
+ bw_adjust=0.5,
883
+ clip_on=True,
884
+ fill=True,
885
+ alpha=0.5,
886
+ warn_singular=False,
887
+ ec="k",
888
+ lw=1,
889
+ )
890
+
891
+ def label(data, color, label, text="latent dim"):
892
+ ax = plt.gca()
893
+ label_text = data[text].unique()[0]
894
+ ax.text(
895
+ 0.0,
896
+ 0.2,
897
+ label_text,
898
+ fontweight="bold",
899
+ ha="right",
900
+ va="center",
901
+ transform=ax.transAxes,
902
+ )
903
+
904
+ g.map_dataframe(label, text="latent dim")
905
+
906
+ g.set(xlim=(xmin.iloc[0], xmax.iloc[0]))
907
+ # Set the subplots to overlap
908
+ g.figure.subplots_adjust(hspace=-0.5)
909
+
910
+ # Remove axes details that don't play well with overlap
911
+ g.set_titles("")
912
+ g.set(yticks=[], ylabel="")
913
+ g.despine(bottom=True, left=True)
914
+
915
+ g.add_legend()
916
+
917
+ plt.close()
918
+ return g
919
+
920
+ @staticmethod
921
+ def make_loss_plot(
922
+ df_plot: pd.DataFrame, plot_type: str
923
+ ) -> matplotlib.figure.Figure:
924
+ """Generates a plot for visualizing loss values from a DataFrame.
925
+
926
+ Args:
927
+ df_plot: DataFrame containing the loss values to be plotted. It should have the columns:
928
+ - "Loss Term": The type of loss term (e.g., "total_loss", "reconstruction_loss").
929
+ - "Epoch": The epoch number.
930
+ - "Loss Value": The value of the loss.
931
+ - "Split": The data split (e.g., "train", "validation").
932
+
933
+ plot_type: The type of plot to generate. It can be either "absolute" or "relative".
934
+ - "absolute": Generates a line plot for each unique loss term.
935
+ - "relative": Generates a density plot for each data split, excluding the "total_loss" term.
936
+
937
+ Returns:
938
+ The generated matplotlib figure containing the loss plots.
939
+ """
940
+ fig_width_abs = 5 * len(df_plot["Loss Term"].unique())
941
+ fig_width_rel = 5 * len(df_plot["Split"].unique())
942
+ if plot_type == "absolute":
943
+ fig, axes = plt.subplots(
944
+ 1,
945
+ len(df_plot["Loss Term"].unique()),
946
+ figsize=(fig_width_abs, 5),
947
+ sharey=False,
948
+ )
949
+ ax = 0
950
+ for term in df_plot["Loss Term"].unique():
951
+ axes[ax] = sns.lineplot(
952
+ data=df_plot[(df_plot["Loss Term"] == term)],
953
+ x="Epoch",
954
+ y="Loss Value",
955
+ hue="Split",
956
+ ax=axes[ax],
957
+ ).set_title(term)
958
+ ax += 1
959
+
960
+ plt.close()
961
+
962
+ if plot_type == "relative":
963
+ # Check if loss values are positive
964
+ if (df_plot["Loss Value"] < 0).any():
965
+ # Warning
966
+ warnings.warn(
967
+ "Loss values contain negative values. Check your loss function if correct. Loss will be clipped to zero for plotting."
968
+ )
969
+ df_plot["Loss Value"] = df_plot["Loss Value"].clip(lower=0)
970
+
971
+ # Exclude loss terms where all Loss Value are zero or NaN over all epochs
972
+ valid_terms = [
973
+ term
974
+ for term in df_plot["Loss Term"].unique()
975
+ if (
976
+ (df_plot[df_plot["Loss Term"] == term]["Loss Value"].notna().any())
977
+ and (df_plot[df_plot["Loss Term"] == term]["Loss Value"] != 0).any()
978
+ )
979
+ ]
980
+ exclude = (
981
+ (df_plot["Loss Term"] != "total_loss")
982
+ & ~(df_plot["Loss Term"].str.contains("_factor"))
983
+ & (df_plot["Loss Term"].isin(valid_terms))
984
+ )
985
+
986
+ fig, axes = plt.subplots(1, 2, figsize=(fig_width_rel, 5), sharey=True)
987
+
988
+ ax = 0
989
+
990
+ for split in df_plot["Split"].unique():
991
+ axes[ax] = sns.kdeplot(
992
+ data=df_plot[exclude & (df_plot["Split"] == split)],
993
+ x="Epoch",
994
+ hue="Loss Term",
995
+ multiple="fill",
996
+ weights="Loss Value",
997
+ clip=[0, df_plot["Epoch"].max()],
998
+ ax=axes[ax],
999
+ ).set_title(split)
1000
+ ax += 1
1001
+
1002
+ plt.close()
1003
+
1004
+ return fig
1005
+
1006
+ @staticmethod
1007
+ def make_loss_format(result: Result, config: DefaultConfig) -> pd.DataFrame:
1008
+ loss_df_melt = pd.DataFrame()
1009
+ for term in result.sub_losses.keys():
1010
+ # Get the loss values and ensure it's a dictionary
1011
+ loss_values = result.sub_losses.get(key=term).get()
1012
+
1013
+ # Add explicit type checking/conversion
1014
+ if not isinstance(loss_values, dict):
1015
+ # If it's not a dict, try to convert it or handle appropriately
1016
+ if hasattr(loss_values, "to_dict"):
1017
+ loss_values = loss_values.to_dict() # type: ignore
1018
+ else:
1019
+ # For non-convertible types, you might need a custom solution
1020
+ # For numpy arrays, you could do something like:
1021
+ if hasattr(loss_values, "shape"):
1022
+ # For numpy arrays, create a dict with indices as keys
1023
+ loss_values = {i: val for i, val in enumerate(loss_values)}
1024
+
1025
+ # Now create the DataFrame
1026
+ loss_df = pd.DataFrame.from_dict(loss_values, orient="index") # type: ignore
1027
+
1028
+ # Rest of your code remains the same
1029
+ if term == "var_loss":
1030
+ loss_df = loss_df * config.beta
1031
+ loss_df["Epoch"] = loss_df.index + 1
1032
+ loss_df["Loss Term"] = term
1033
+
1034
+ loss_df_melt = pd.concat(
1035
+ [
1036
+ loss_df_melt,
1037
+ loss_df.melt(
1038
+ id_vars=["Epoch", "Loss Term"],
1039
+ var_name="Split",
1040
+ value_name="Loss Value",
1041
+ ),
1042
+ ],
1043
+ axis=0,
1044
+ ).reset_index(drop=True)
1045
+
1046
+ # Similar handling for the total losses
1047
+ loss_values = result.losses.get()
1048
+ if not isinstance(loss_values, dict):
1049
+ if hasattr(loss_values, "to_dict"):
1050
+ loss_values = loss_values.to_dict() # ty: ignore
1051
+ else:
1052
+ if hasattr(loss_values, "shape"):
1053
+ loss_values = {i: val for i, val in enumerate(loss_values)}
1054
+
1055
+ loss_df = pd.DataFrame.from_dict(loss_values, orient="index") # type: ignore
1056
+ loss_df["Epoch"] = loss_df.index + 1
1057
+ loss_df["Loss Term"] = "total_loss"
1058
+
1059
+ loss_df_melt = pd.concat(
1060
+ [
1061
+ loss_df_melt,
1062
+ loss_df.melt(
1063
+ id_vars=["Epoch", "Loss Term"],
1064
+ var_name="Split",
1065
+ value_name="Loss Value",
1066
+ ),
1067
+ ],
1068
+ axis=0,
1069
+ ).reset_index(drop=True)
1070
+
1071
+ loss_df_melt["Loss Value"] = loss_df_melt["Loss Value"].astype(float)
1072
+ return loss_df_melt
1073
+
1074
+ @no_type_check
1075
+ def plot_evaluation(
1076
+ self,
1077
+ result: Result,
1078
+ ) -> dict:
1079
+ """Plots the evaluation results from the Result object.
1080
+
1081
+ Args:
1082
+ result: The Result object containing evaluation data.
1083
+
1084
+ Returns:
1085
+ The generated dictionary containing the evaluation plots.
1086
+ """
1087
+ ## Plot all results
1088
+
1089
+ ml_plots = dict()
1090
+ plt.ioff()
1091
+
1092
+ for c in pd.unique(result.embedding_evaluation.CLINIC_PARAM):
1093
+ ml_plots[c] = dict()
1094
+ for m in pd.unique(
1095
+ result.embedding_evaluation.loc[
1096
+ result.embedding_evaluation.CLINIC_PARAM == c, "metric"
1097
+ ]
1098
+ ):
1099
+ ml_plots[c][m] = dict()
1100
+ for alg in pd.unique(
1101
+ result.embedding_evaluation.loc[
1102
+ (result.embedding_evaluation.CLINIC_PARAM == c)
1103
+ & (result.embedding_evaluation.metric == m),
1104
+ "ML_ALG",
1105
+ ]
1106
+ ):
1107
+ data = result.embedding_evaluation[
1108
+ (result.embedding_evaluation.metric == m)
1109
+ & (result.embedding_evaluation.CLINIC_PARAM == c)
1110
+ & (result.embedding_evaluation.ML_ALG == alg)
1111
+ ]
1112
+
1113
+ sns_plot = sns.catplot(
1114
+ data=data,
1115
+ x="score_split",
1116
+ y="value",
1117
+ col="ML_TASK",
1118
+ hue="score_split",
1119
+ kind="bar",
1120
+ )
1121
+
1122
+ min_y = data.value.min()
1123
+ if min_y > 0:
1124
+ min_y = 0
1125
+
1126
+ ml_plots[c][m][alg] = sns_plot.set(ylim=(min_y, None))
1127
+
1128
+ self.plots["ML_Evaluation"] = ml_plots
1129
+
1130
+ return ml_plots
1131
+
1132
+ def show_evaluation(
1133
+ self,
1134
+ param: str,
1135
+ metric: str,
1136
+ ml_alg: Optional[str] = None,
1137
+ ) -> None:
1138
+ """Displays the evaluation plot for a specific clinical parameter, metric, and optionally ML algorithm.
1139
+
1140
+ Args:
1141
+ param: The clinical parameter to visualize.
1142
+ metric: The metric to visualize.
1143
+ ml_alg: If None, plots all available algorithms.
1144
+ """
1145
+ plt.ioff()
1146
+ if "ML_Evaluation" not in self.plots.keys():
1147
+ print("ML Evaluation plots not found in the plots dictionary")
1148
+ print("You need to run evaluate() method first")
1149
+ return None
1150
+ if param not in self.plots["ML_Evaluation"].keys():
1151
+ print(f"Parameter {param} not found in the ML Evaluation plots")
1152
+ print(f"Available parameters: {list(self.plots['ML_Evaluation'].keys())}")
1153
+ return None
1154
+ if metric not in self.plots["ML_Evaluation"][param].keys():
1155
+ print(f"Metric {metric} not found in the ML Evaluation plots for {param}")
1156
+ print(
1157
+ f"Available metrics: {list(self.plots['ML_Evaluation'][param].keys())}"
1158
+ )
1159
+ return None
1160
+
1161
+ algs = list(self.plots["ML_Evaluation"][param][metric].keys())
1162
+ if ml_alg is not None:
1163
+ if ml_alg not in algs:
1164
+ print(f"ML algorithm {ml_alg} not found for {param} and {metric}")
1165
+ print(f"Available ML algorithms: {algs}")
1166
+ return None
1167
+ fig = self.plots["ML_Evaluation"][param][metric][ml_alg].figure
1168
+ show_figure(fig)
1169
+ plt.show()
1170
+ else:
1171
+ for alg in algs:
1172
+ print(f"Showing plot for ML algorithm: {alg}")
1173
+ fig = self.plots["ML_Evaluation"][param][metric][alg].figure
1174
+ show_figure(fig)
1175
+ plt.show()
1176
+
1177
+ @staticmethod
1178
+ def _total_correlation(latent_space: pd.DataFrame) -> float:
1179
+ """Function to compute the total correlation as described here (Equation2): https://doi.org/10.3390/e21100921
1180
+
1181
+ Args:
1182
+ latent_space - (pd.DataFrame): latent space with dimension sample vs. latent dimensions
1183
+ Returns:
1184
+ tc - (float): total correlation across latent dimensions
1185
+ """
1186
+ lat_cov = np.cov(latent_space.T)
1187
+ tc = 0.5 * (np.sum(np.log(np.diag(lat_cov))) - np.linalg.slogdet(lat_cov)[1])
1188
+ return tc
1189
+
1190
+ @staticmethod
1191
+ def _coverage_calc(latent_space: pd.DataFrame) -> float:
1192
+ """Function to compute the coverage as described here (Equation3): https://doi.org/10.3390/e21100921
1193
+
1194
+ Args:
1195
+ latent_space: latent dimensions
1196
+ Returns:
1197
+ cov: coverage across latent dimensions
1198
+ """
1199
+ bins_per_dim = int(
1200
+ np.power(len(latent_space.index), 1 / len(latent_space.columns))
1201
+ )
1202
+ if bins_per_dim < 2:
1203
+ warnings.warn(
1204
+ "Coverage calculation fails since combination of sample size and latent dimension results in less than 2 bins."
1205
+ )
1206
+ cov = np.nan
1207
+ else:
1208
+ latent_bins = latent_space.apply(lambda x: pd.cut(x, bins=bins_per_dim))
1209
+ latent_bins = pd.Series(zip(*[latent_bins[col] for col in latent_bins]))
1210
+ cov = len(latent_bins.unique()) / np.power(
1211
+ bins_per_dim, len(latent_space.columns)
1212
+ )
1213
+
1214
+ return cov