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,841 @@
1
+ from dataclasses import field
2
+ import pandas as pd
3
+ import numpy as np
4
+ import seaborn as sns
5
+ import matplotlib.pyplot as plt
6
+ from umap import UMAP
7
+ import warnings
8
+ import torch
9
+ from sklearn.decomposition import PCA
10
+ from sklearn.manifold import TSNE
11
+
12
+ from typing import Any, Dict, Optional, Union, List, no_type_check
13
+ from autoencodix.base._base_visualizer import BaseVisualizer
14
+ from autoencodix.utils._result import Result
15
+ from autoencodix.utils._utils import nested_dict, show_figure
16
+ from autoencodix.configs.default_config import DefaultConfig
17
+ from autoencodix.data._datasetcontainer import DatasetContainer
18
+
19
+
20
+ class XModalVisualizer(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
+ ## TODO needs to be adjusted for X-Modalix ##
34
+ ## Plot Model weights for each sub-VAE ##
35
+ # self.plots["ModelWeights"] = self._plot_model_weights(model=result.model)
36
+
37
+ ## Make long format of losses
38
+ loss_df_melt = self._make_loss_format(result=result, config=config)
39
+
40
+ ## X-Modalix specific ##
41
+ # Filter loss terms which are specific for each modality VAE
42
+ # Plot only combined loss terms as in old autoencodix framework
43
+ if not hasattr(result.datasets, "train"):
44
+ raise ValueError("result.datasets has no attribute train")
45
+ if result.datasets.train is None:
46
+ raise ValueError("Train attribute of datasets is None")
47
+ loss_df_melt = loss_df_melt[
48
+ ~loss_df_melt["Loss Term"].str.startswith(
49
+ tuple(result.datasets.train.datasets.keys())
50
+ )
51
+ ]
52
+ if not result.losses._data:
53
+ import warnings
54
+
55
+ warnings.warn(
56
+ "No loss data: This usually happens if you try to visualize after saving and loading the pipeline object with `save_all=False`. This memory-efficient saving mode does not retain past training loss data."
57
+ )
58
+ return result
59
+ ## Make plot loss absolute
60
+ self.plots["loss_absolute"] = self._make_loss_plot(
61
+ df_plot=loss_df_melt, plot_type="absolute"
62
+ )
63
+ ## Make plot loss relative
64
+ self.plots["loss_relative"] = self._make_loss_plot(
65
+ df_plot=loss_df_melt, plot_type="relative"
66
+ )
67
+
68
+ return result
69
+
70
+ def show_latent_space(
71
+ self,
72
+ result: Result,
73
+ plot_type: str = "2D-scatter",
74
+ labels: Optional[Union[list, pd.Series, None]] = None,
75
+ param: Optional[Union[list, str]] = None,
76
+ epoch: Optional[Union[int, None]] = None,
77
+ split: str = "all",
78
+ ) -> None:
79
+ plt.ioff()
80
+ if plot_type == "Coverage-Correlation":
81
+ print("TODO: Implement Coverage-Correlation plot for X-Modalix")
82
+ # if "Coverage-Correlation" in self.plots:
83
+ # fig = self.plots["Coverage-Correlation"]
84
+ # show_figure(fig)
85
+ # plt.show()
86
+ # else:
87
+ # results = []
88
+ # for epoch in range(result.model.config.checkpoint_interval, result.model.config.epochs + 1, result.model.config.checkpoint_interval):
89
+ # for split in ["train", "valid"]:
90
+ # latent_df = result.get_latent_df(epoch=epoch-1, split=split)
91
+ # tc = self._total_correlation(latent_df)
92
+ # cov = self._coverage_calc(latent_df)
93
+ # results.append({"epoch": epoch, "split": split, "total_correlation": tc, "coverage": cov})
94
+
95
+ # df_metrics = pd.DataFrame(results)
96
+
97
+ # fig, axes = plt.subplots(1, 2, figsize=(12, 5))
98
+
99
+ # # Total Correlation plot
100
+ # ax1 = sns.lineplot(data=df_metrics, x="epoch", y="total_correlation", hue="split", ax=axes[0])
101
+ # axes[0].set_title("Total Correlation")
102
+ # axes[0].set_xlabel("Epoch")
103
+ # axes[0].set_ylabel("Total Correlation")
104
+
105
+ # # Coverage plot
106
+ # ax2 = sns.lineplot(data=df_metrics, x="epoch", y="coverage", hue="split", ax=axes[1])
107
+ # axes[1].set_title("Coverage")
108
+ # axes[1].set_xlabel("Epoch")
109
+ # axes[1].set_ylabel("Coverage")
110
+
111
+ # plt.tight_layout()
112
+ # self.plots["Coverage-Correlation"] = fig
113
+ # show_figure(fig)
114
+ # plt.show()
115
+ else:
116
+ # Set Defaults
117
+ if epoch is None:
118
+ epoch = -1
119
+
120
+ ## Collect all metadata and latent spaces from datasets
121
+ clin_data = []
122
+ latent_data = []
123
+
124
+ if split == "all":
125
+ split_list = ["train", "test", "valid"]
126
+ else:
127
+ split_list = [split]
128
+ for s in split_list:
129
+ split_ds = getattr(result.datasets, s, None)
130
+ if split_ds is not None:
131
+ for key, ds in split_ds.datasets.items():
132
+ if s == "test":
133
+ df_latent = result.get_latent_df(
134
+ epoch=-1, split=s, modality=key
135
+ )
136
+ else:
137
+ df_latent = result.get_latent_df(
138
+ epoch=epoch, split=s, modality=key
139
+ )
140
+ df_latent["modality"] = key
141
+ df_latent["sample_ids"] = (
142
+ df_latent.index
143
+ ) # Each sample can occur multiple times in latent space
144
+ latent_data.append(df_latent)
145
+ if hasattr(ds, "metadata") and ds.metadata is not None:
146
+ df = ds.metadata.copy()
147
+ df["sample_ids"] = df.index.astype(str)
148
+ df["split"] = s
149
+ df["modality"] = key
150
+ clin_data.append(df)
151
+
152
+ if latent_data and clin_data:
153
+ latent_data = pd.concat(latent_data, axis=0, ignore_index=True)
154
+ clin_data = pd.concat(clin_data, axis=0, ignore_index=True)
155
+ if "sample_ids" in clin_data.columns:
156
+ clin_data = clin_data.drop_duplicates(
157
+ subset="sample_ids"
158
+ ).set_index("sample_ids")
159
+ else:
160
+ latent_data = pd.DataFrame()
161
+ clin_data = pd.DataFrame()
162
+
163
+ ## Label options
164
+ if param is None:
165
+ modality = list(result.model.keys())[
166
+ 0
167
+ ] # Take the first since configs are same for all sub-VAEs
168
+ model = result.model.get(modality, None)
169
+ if model is None:
170
+ raise ValueError(
171
+ f"Model for modality {modality} not found in result.model"
172
+ )
173
+ param = model.config.data_config.annotation_columns
174
+
175
+ if labels is None and param is None:
176
+ labels = ["all"] * latent_data["sample_ids"].unique().shape[0]
177
+
178
+ if labels is None and isinstance(param, str):
179
+ if param == "all":
180
+ param = list(clin_data.columns)
181
+ else:
182
+ raise ValueError(
183
+ "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."
184
+ )
185
+
186
+ if labels is not None and param is not None:
187
+ raise ValueError(
188
+ "Please provide either labels or param, not both. If you want to plot all parameters, set param to 'all' and labels to None."
189
+ )
190
+
191
+ if labels is not None and param is None:
192
+ if isinstance(labels, pd.Series):
193
+ param = [labels.name]
194
+ # Order by index of latent_data first, fill missing with "unknown"
195
+ labels = labels.reindex( # ty: ignore
196
+ latent_data["sample_ids"], # ty: ignore
197
+ fill_value="unknown", # ty: ignore
198
+ ).tolist()
199
+ else:
200
+ param = ["user_label"] # Default label if none provided
201
+ if not isinstance(param, list):
202
+ raise ValueError(f"param: should be converted to list, got: {param}")
203
+ for p in param:
204
+ if p in clin_data.columns:
205
+ labels: List = clin_data.loc[
206
+ latent_data["sample_ids"], p
207
+ ].tolist() # ty: ignore
208
+ else:
209
+ if clin_data.shape[0] == len(labels): # ty: ignore
210
+ clin_data[p] = labels
211
+ else:
212
+ clin_data[p] = ["all"] * clin_data.shape[0]
213
+
214
+ if plot_type == "2D-scatter":
215
+ ## Make 2D Embedding with UMAP
216
+ if (
217
+ latent_data.drop(
218
+ columns=["sample_ids", "modality"]
219
+ ).shape[ # ty: ignore
220
+ 1
221
+ ] # ty: ignore
222
+ > 2
223
+ ):
224
+ reducer = UMAP(n_components=2)
225
+ embedding = pd.DataFrame(
226
+ reducer.fit_transform(
227
+ latent_data.drop(
228
+ columns=["sample_ids", "modality"]
229
+ ) # ty: ignore
230
+ )
231
+ )
232
+ embedding.columns = ["DIM1", "DIM2"]
233
+ embedding["sample_ids"] = latent_data["sample_ids"]
234
+ embedding["modality"] = latent_data["modality"]
235
+ else:
236
+ embedding = latent_data
237
+
238
+ # Merge with clinical data via sample_ids
239
+ clin_data["sample_ids"] = clin_data.index.astype(str)
240
+ clin_data.index = clin_data.index.astype(str) # Add this line
241
+ embedding["sample_ids"] = embedding["sample_ids"].astype(str)
242
+
243
+ embedding = embedding.merge(
244
+ clin_data.drop(columns=["modality"]), # ty: ignore
245
+ left_on="sample_ids",
246
+ right_index=True,
247
+ how="left",
248
+ )
249
+
250
+ self.plots["2D-scatter"][epoch][split][p] = (
251
+ self._plot_translate_latent(
252
+ embedding=embedding,
253
+ color_param=p,
254
+ style_param="modality",
255
+ )
256
+ )
257
+
258
+ fig = self.plots["2D-scatter"][epoch][split][p].figure
259
+ # show_figure(fig)
260
+ plt.show()
261
+
262
+ if plot_type == "Ridgeline":
263
+ ## Make ridgeline plot
264
+ if len(labels) != latent_data.shape[0]: # ty: ignore
265
+ if labels[0] == "all": # ty: ignore
266
+ labels = ["all"] * latent_data.shape[0] # ty: ignore
267
+ else:
268
+ raise ValueError(
269
+ "Labels must match the number of samples in the latent space."
270
+ )
271
+
272
+ self.plots["Ridgeline"][epoch][split][p] = (
273
+ self._plot_latent_ridge_multi(
274
+ lat_space=latent_data.drop(
275
+ columns=["sample_ids"]
276
+ ), # ty: ignore
277
+ labels=labels,
278
+ modality="modality",
279
+ param=p,
280
+ )
281
+ )
282
+
283
+ fig = self.plots["Ridgeline"][epoch][split][p].figure
284
+ show_figure(fig)
285
+ plt.show()
286
+
287
+ def show_weights(self) -> None:
288
+ ## TODO
289
+ raise NotImplementedError(
290
+ "Weight visualization for X-Modalix is not implemented."
291
+ )
292
+
293
+ @no_type_check
294
+ def show_image_translation( # ty: ignore
295
+ self,
296
+ result: Result,
297
+ from_key: str,
298
+ to_key: str,
299
+ n_sample_per_class: int = 3,
300
+ param: Optional[str] = None,
301
+ ) -> None: # ty: ignore
302
+ """Visualizes image translation results for a given dataset.
303
+
304
+ Split by displaying a grid of original, translated, and reference images,grouped by class values.
305
+ Args:
306
+ result:The result object containing datasets and reconstructions.
307
+ from_key: The source modality key (not directly used in visualization, but relevant for context).
308
+ to_key: The target modality key. Must correspond to an image dataset (must contain "IMG").
309
+ split: The dataset split to visualize ("test", "train", or "valid"). Default is "test".
310
+ n_sample_per_class: Number of samples to display per class value. Default is 3.
311
+ param: The metadata column name used to group samples by class.
312
+ Raises
313
+ ValueError: If `to_key` does not correspond to an image dataset.
314
+ """
315
+
316
+ if "img" not in to_key:
317
+ raise ValueError(
318
+ f"You provided as 'to_key' {to_key} a non-image dataset. "
319
+ "Image translation grid visualization is only possible for translation to IMG data type."
320
+ )
321
+ else:
322
+ split = "test" # Currently only test split is supported
323
+ ## Get n samples per class
324
+ if split == "test":
325
+ meta = result.datasets.test.datasets[to_key].metadata
326
+ paired_sample_ids = result.datasets.test.paired_sample_ids
327
+
328
+ # Restrict meta to only paired sample ids
329
+ meta = meta.loc[paired_sample_ids]
330
+
331
+ if param is None:
332
+ param = "user-label"
333
+ meta[param] = (
334
+ "all" # Default to all samples if no parameter is provided
335
+ )
336
+
337
+ # Get possible class values
338
+ class_values = meta[param].unique()
339
+ if len(class_values) > 10:
340
+ # Make warning
341
+ warnings.warn(
342
+ f"Found {len(class_values)} class values for parameter '{param}'. Only first 10 will be used to limit figure size"
343
+ )
344
+ class_values = class_values[:10]
345
+
346
+ # Build dictionary of sample_ids per class value (max n_sample_per_class per class)
347
+ sample_per_class = {
348
+ val: meta[meta[param] == val]
349
+ .sample(
350
+ n=min(n_sample_per_class, (meta[param] == val).sum()),
351
+ random_state=42,
352
+ )
353
+ .index.tolist()
354
+ for val in class_values
355
+ }
356
+
357
+ print(f"Sample per class: {sample_per_class}")
358
+
359
+ # Lookup of sample indices per modality
360
+ sample_ids_per_key = dict()
361
+
362
+ for key in result.sample_ids.get(epoch=-1, split="test").keys():
363
+ sample_ids_per_key[key] = result.sample_ids.get(epoch=-1, split="test")[
364
+ key
365
+ ]
366
+ # Original
367
+ sample_ids_per_key["original"] = result.datasets.test.datasets[
368
+ to_key
369
+ ].sample_ids
370
+
371
+ ## Generate Image Grid
372
+ # Number of test (or train or valid) samples from all values in sample_per_class dictionary
373
+ n_test_samples = sum(len(indices) for indices in sample_per_class.values())
374
+
375
+ # #
376
+ col_labels = []
377
+ for class_value in sample_per_class:
378
+ col_labels.extend(
379
+ [
380
+ class_value + " " + split + "-sample:" + s
381
+ for s in sample_per_class[class_value]
382
+ ]
383
+ )
384
+
385
+ row_labels = ["Original", "Translated", "Reference"]
386
+
387
+ fig, axes = plt.subplots(
388
+ ncols=n_test_samples, # Number of classes
389
+ nrows=3, # Original, translated, reference
390
+ figsize=(n_test_samples * 2, 3 * 2),
391
+ )
392
+
393
+ for i, ax in enumerate(axes.flat):
394
+ row = int(i / n_test_samples)
395
+ # test_sample = sample_idx_list[i % n_test_samples]
396
+ # print(f"Row: {row}, Column: {i % n_test_samples}")
397
+ # print(f"Current sample: {col_labels[i % n_test_samples]}")
398
+
399
+ if row == 0:
400
+ if split == "test":
401
+ idx_original = list(sample_ids_per_key["original"]).index(
402
+ col_labels[i % n_test_samples].split("sample:")[1]
403
+ )
404
+ img_temp = result.datasets.test.datasets[to_key][idx_original][
405
+ 1
406
+ ].squeeze() # Stored as Tuple (index, tensor, sample_id)
407
+
408
+ # Original image
409
+ ax.imshow(np.asarray(img_temp))
410
+ ax.axis("off")
411
+ # Sample label
412
+ ax.text(
413
+ 0.5,
414
+ 1.1,
415
+ col_labels[i],
416
+ va="bottom",
417
+ ha="center",
418
+ # rotation='vertical',
419
+ rotation=45,
420
+ transform=ax.transAxes,
421
+ )
422
+ # Row label
423
+ if i % n_test_samples == 0:
424
+ ax.text(
425
+ -0.1,
426
+ 0.5,
427
+ row_labels[0],
428
+ va="center",
429
+ ha="right",
430
+ transform=ax.transAxes,
431
+ )
432
+
433
+ if row == 1:
434
+ # Translated image
435
+ idx_translated = list(sample_ids_per_key["translation"]).index(
436
+ col_labels[i % n_test_samples].split("sample:")[1]
437
+ )
438
+ ax.imshow(
439
+ result.reconstructions.get(epoch=-1, split=split)[
440
+ "translation"
441
+ ][idx_translated].squeeze()
442
+ )
443
+ ax.axis("off")
444
+ # Row label
445
+ if i % n_test_samples == 0:
446
+ ax.text(
447
+ -0.1,
448
+ 0.5,
449
+ row_labels[1],
450
+ va="center",
451
+ ha="right",
452
+ transform=ax.transAxes,
453
+ )
454
+
455
+ if row == 2:
456
+ # Reference image reconstruction
457
+ idx_reference = list(
458
+ sample_ids_per_key[f"reference_{to_key}_to_{to_key}"]
459
+ ).index(col_labels[i % n_test_samples].split("sample:")[1])
460
+ ax.imshow(
461
+ result.reconstructions.get(epoch=-1, split=split)[
462
+ f"reference_{to_key}_to_{to_key}"
463
+ ][idx_reference].squeeze()
464
+ )
465
+ ax.axis("off")
466
+ # Row label
467
+ if i % n_test_samples == 0:
468
+ ax.text(
469
+ -0.1,
470
+ 0.5,
471
+ row_labels[2],
472
+ va="center",
473
+ ha="right",
474
+ transform=ax.transAxes,
475
+ )
476
+
477
+ self.plots["Image-translation"][to_key][split][param] = fig
478
+ # show_figure(fig)
479
+ plt.show()
480
+
481
+ @no_type_check
482
+ def show_2D_translation(
483
+ self,
484
+ result: Result,
485
+ translated_modality: str,
486
+ split: str = "test",
487
+ param: Optional[str] = None,
488
+ reducer: str = "UMAP",
489
+ ) -> None:
490
+ ## TODO add similar labels/param logic from other visualizations
491
+ dataset = result.datasets
492
+
493
+ ## Overwrite original datasets with new_datasets if available after predict with other data
494
+ if dataset is None:
495
+ dataset = DatasetContainer()
496
+
497
+ if bool(result.new_datasets.test):
498
+ dataset.test = result.new_datasets.test
499
+
500
+ if split not in ["train", "valid", "test", "all"]:
501
+ raise ValueError(f"Unknown split: {split}")
502
+
503
+ if dataset.test is None:
504
+ raise ValueError("test of dataset is None")
505
+
506
+ if split == "test":
507
+ df_processed = dataset.test._to_df(modality=translated_modality)
508
+ else:
509
+ raise NotImplementedError(
510
+ "2D translation visualization is currently only implemented for the 'test' split since reconstruction is only performed on test-split."
511
+ )
512
+
513
+ # Get translated reconstruction
514
+ tensor_list = result.reconstructions.get(epoch=-1, split=split)[ # ty: ignore
515
+ "translation"
516
+ ] # ty: ignore
517
+ print(f"len of tensor-list: {len(tensor_list)}")
518
+ tensor_ids = result.sample_ids.get(epoch=-1, split=split)["translation"]
519
+ print(f"len of tensor_ids: {len(tensor_ids)}")
520
+
521
+ # Flatten each tensor and collect as rows (for image case)
522
+ rows = [
523
+ t.flatten().cpu().numpy() if isinstance(t, torch.Tensor) else t.flatten()
524
+ for t in tensor_list
525
+ ]
526
+
527
+ # Create DataFrame
528
+ df_translate_flat = pd.DataFrame(
529
+ rows,
530
+ columns=["Feature_" + str(i) for i in range(len(rows[0]))],
531
+ index=tensor_ids,
532
+ )
533
+
534
+ if reducer == "UMAP":
535
+ reducer_model = UMAP(n_components=2)
536
+ elif reducer == "PCA":
537
+ reducer_model = PCA(n_components=2)
538
+ elif reducer == "TSNE":
539
+ reducer_model = TSNE(n_components=2)
540
+
541
+ # making sure of index alignemnt
542
+ common_ids = df_processed.index.intersection(df_translate_flat.index)
543
+ df_processed = df_processed.loc[common_ids]
544
+ df_translate_flat = df_translate_flat.loc[common_ids]
545
+ df_translate_flat = df_translate_flat.reindex(df_processed.index)
546
+ df_translate_flat.index = pd.Index([i for i in range(len(common_ids))])
547
+ X = np.vstack([df_processed.values, df_translate_flat.values])
548
+ df_red_comb = pd.DataFrame(reducer_model.fit_transform(X))
549
+
550
+ # df_comb = pd.concat(
551
+ # [df_processed, df_translate_flat], axis=0, ignore_index=True
552
+ # )
553
+
554
+ df_red_comb["origin"] = ["input"] * df_processed.shape[0] + [
555
+ "translated"
556
+ ] * df_translate_flat.shape[0]
557
+
558
+ # df_red_comb = pd.DataFrame(
559
+ # reducer_model.fit_transform(
560
+ # pd.concat([df_processed, df_translate_flat], axis=0)
561
+ # )
562
+ # )
563
+
564
+ labels = (
565
+ list(
566
+ result.datasets.test.datasets[translated_modality].metadata[param]
567
+ ) # ty: ignore
568
+ * 2
569
+ )
570
+ df_red_comb[param] = (
571
+ labels + labels[0 : df_red_comb.shape[0] - len(labels)]
572
+ ) ## TODO fix for not matching lengths
573
+
574
+ g = sns.FacetGrid(
575
+ df_red_comb,
576
+ col="origin",
577
+ hue=param,
578
+ sharex=True,
579
+ sharey=True,
580
+ height=8,
581
+ aspect=1,
582
+ )
583
+ g.map_dataframe(sns.scatterplot, x=0, y=1, alpha=0.7)
584
+ g.add_legend()
585
+ g.set_axis_labels(reducer + " DIM 1", reducer + " DIM 2")
586
+ g.set_titles(col_template="{col_name}")
587
+
588
+ self.plots["2D-translation"][translated_modality][split][param] = g
589
+ plt.show()
590
+
591
+ ## Utilities specific for X-Modalix
592
+ @staticmethod
593
+ def _plot_translate_latent(
594
+ embedding,
595
+ color_param,
596
+ style_param=None,
597
+ ):
598
+ """Creates a 2D visualization of the 2D embedding of the latent space.
599
+ Args:
600
+ embedding: embedding on which is visualized. Assumes prior 2D dimension reduction.
601
+ color_params: Clinical parameter to color scatter plot
602
+ style_param: Parameter e.g. "Translate" to facet scatter plot
603
+ Returns:
604
+ fig: Figure handle
605
+
606
+ """
607
+ labels = list(embedding[color_param])
608
+ # logger = getlogger(cfg)
609
+ numeric = False
610
+ if not isinstance(labels[0], str):
611
+ if len(np.unique(labels)) > 3:
612
+ # TODO Decide if numeric to category should be optional in new Package
613
+ # print(
614
+ # f"The provided label column is numeric and converted to categories."
615
+ # )
616
+ # labels = pd.qcut(
617
+ # labels, q=4, labels=["1stQ", "2ndQ", "3rdQ", "4thQ"]
618
+ # ).astype(str)
619
+ # else:
620
+ numeric = True
621
+ else:
622
+ labels = [str(x) for x in labels]
623
+
624
+ # check if label or embedding is longerm and duplicate the shorter one
625
+ if len(labels) < embedding.shape[0]:
626
+ print(
627
+ "Given labels do not have the same length as given sample size. Labels will be duplicated."
628
+ )
629
+ labels = [
630
+ label
631
+ for label in labels
632
+ for _ in range(embedding.shape[0] // len(labels))
633
+ ]
634
+ elif len(labels) > embedding.shape[0]:
635
+ labels = list(set(labels))
636
+
637
+ if style_param is not None:
638
+ embedding[color_param] = labels
639
+ if numeric:
640
+ palette = "bwr"
641
+ else:
642
+ palette = None
643
+ plot = sns.relplot(
644
+ data=embedding,
645
+ x="DIM1",
646
+ y="DIM2",
647
+ hue=color_param,
648
+ palette=palette,
649
+ col=style_param,
650
+ style=style_param,
651
+ markers=True,
652
+ alpha=0.4,
653
+ ec="black",
654
+ height=10,
655
+ aspect=1,
656
+ s=150,
657
+ )
658
+
659
+ return plot
660
+
661
+ @staticmethod
662
+ def _plot_latent_ridge_multi(
663
+ lat_space: pd.DataFrame,
664
+ modality: Optional[str] = None,
665
+ labels: Optional[Union[list, pd.Series, None]] = None,
666
+ param: Optional[Union[str, None]] = None,
667
+ ) -> sns.FacetGrid:
668
+ """Creates a ridge line plot of latent space dimension where each row shows the density of a latent dimension and groups (ridges).
669
+ Args:
670
+ lat_space: DataFrame containing the latent space intensities for samples (rows) and latent dimensions (columns)
671
+ labels: List of labels for each sample. If None, all samples are considered as one group.
672
+ param: Clinical parameter to create groupings and coloring of ridges. Must be a column name (str) of clin_data
673
+ Returns:
674
+ g (sns.FacetGrid): FacetGrid object containing the ridge line plot
675
+ """
676
+ sns.set_theme(
677
+ style="white", rc={"axes.facecolor": (0, 0, 0, 0)}
678
+ ) ## Necessary to enforce overplotting
679
+
680
+ df = pd.melt(
681
+ lat_space,
682
+ id_vars=modality, # ty: ignore
683
+ var_name="latent dim",
684
+ value_name="latent intensity",
685
+ )
686
+ # print(df)
687
+ df["sample"] = len(lat_space.drop(columns=modality).columns) * list(
688
+ lat_space.index
689
+ )
690
+
691
+ if labels is None:
692
+ param = "all"
693
+ labels = ["all"] * len(df)
694
+
695
+ # print(labels[0])
696
+ if not isinstance(labels[0], str):
697
+ if len(np.unique(labels)) > 3:
698
+ # Change all non-float labels to NaN
699
+ labels = [x if isinstance(x, float) else float("nan") for x in labels]
700
+ labels = pd.qcut(
701
+ x=pd.Series(labels),
702
+ q=4,
703
+ labels=["1stQ", "2ndQ", "3rdQ", "4thQ"],
704
+ ).astype(str)
705
+ else:
706
+ labels = [str(x) for x in labels]
707
+
708
+ df[param] = len(lat_space.drop(columns=modality).columns) * labels # type: ignore
709
+
710
+ exclude_missing_info = (df[param] == "unknown") | (df[param] == "nan")
711
+
712
+ xmin = (
713
+ df.loc[~exclude_missing_info, ["latent intensity", "latent dim", param]]
714
+ .groupby([param, "latent dim"], observed=False)
715
+ .quantile(0.05)
716
+ .min()
717
+ )
718
+ xmax = (
719
+ df.loc[~exclude_missing_info, ["latent intensity", "latent dim", param]]
720
+ .groupby([param, "latent dim"], observed=False)
721
+ .quantile(0.9)
722
+ .max()
723
+ )
724
+
725
+ if len(np.unique(df[param])) > 8:
726
+ cat_pal = sns.husl_palette(len(np.unique(df[param])))
727
+ else:
728
+ cat_pal = sns.color_palette(n_colors=len(np.unique(df[param])))
729
+
730
+ g = sns.FacetGrid(
731
+ df[~exclude_missing_info],
732
+ row="latent dim",
733
+ col=modality,
734
+ hue=param,
735
+ aspect=12,
736
+ height=0.8,
737
+ xlim=(xmin.iloc[0], xmax.iloc[0]),
738
+ palette=cat_pal,
739
+ )
740
+
741
+ g.map_dataframe(
742
+ sns.kdeplot,
743
+ "latent intensity",
744
+ bw_adjust=0.5,
745
+ clip_on=True,
746
+ fill=True,
747
+ alpha=0.5,
748
+ warn_singular=False,
749
+ ec="k",
750
+ lw=1,
751
+ )
752
+
753
+ def label(data, color, label, text="latent dim"):
754
+ ax = plt.gca()
755
+ label_text = data[text].unique()[0]
756
+ ax.text(
757
+ 0.0,
758
+ 0.2,
759
+ label_text,
760
+ fontweight="bold",
761
+ ha="right",
762
+ va="center",
763
+ transform=ax.transAxes,
764
+ )
765
+
766
+ g.map_dataframe(label, text="latent dim")
767
+
768
+ g.set(xlim=(xmin.iloc[0], xmax.iloc[0]))
769
+ # Set the subplots to overlap
770
+ g.figure.subplots_adjust(hspace=-0.5)
771
+
772
+ # Remove axes details that don't play well with overlap
773
+ g.set_titles("")
774
+ g.set(yticks=[], ylabel="")
775
+ g.despine(bottom=True, left=True)
776
+
777
+ for i, m in enumerate(df[modality].unique()):
778
+ g.fig.get_axes()[i].set_title(m)
779
+
780
+ g.add_legend()
781
+
782
+ plt.close()
783
+ return g
784
+
785
+ def _plot_evaluation(
786
+ self,
787
+ result: Result,
788
+ ) -> dict:
789
+ """Plots the evaluation results from the Result object.
790
+
791
+ Args:
792
+ result: The Result object containing evaluation data.
793
+
794
+ Returns:
795
+ The generated dictionary containing the evaluation plots.
796
+ """
797
+ ## Plot all results
798
+
799
+ ml_plots = dict()
800
+ plt.ioff()
801
+
802
+ for c in pd.unique(result.embedding_evaluation.CLINIC_PARAM):
803
+ ml_plots[c] = dict()
804
+ for m in pd.unique(
805
+ result.embedding_evaluation.loc[
806
+ result.embedding_evaluation.CLINIC_PARAM == c, "metric"
807
+ ]
808
+ ): # ty: ignore
809
+ ml_plots[c][m] = dict()
810
+ for alg in pd.unique(
811
+ result.embedding_evaluation.loc[
812
+ (result.embedding_evaluation.CLINIC_PARAM == c)
813
+ & (result.embedding_evaluation.metric == m),
814
+ "ML_ALG",
815
+ ]
816
+ ): # ty: ignore
817
+ data = result.embedding_evaluation[
818
+ (result.embedding_evaluation.metric == m)
819
+ & (result.embedding_evaluation.CLINIC_PARAM == c)
820
+ & (result.embedding_evaluation.ML_ALG == alg)
821
+ ]
822
+
823
+ sns_plot = sns.catplot(
824
+ data=data,
825
+ x="score_split",
826
+ y="value",
827
+ col="ML_TASK",
828
+ row="MODALITY",
829
+ hue="score_split",
830
+ kind="bar",
831
+ )
832
+
833
+ min_y = data.value.min()
834
+ if min_y > 0:
835
+ min_y = 0
836
+
837
+ ml_plots[c][m][alg] = sns_plot.set(ylim=(min_y, None))
838
+
839
+ self.plots["ML_Evaluation"] = ml_plots
840
+
841
+ return ml_plots