validmind 2.0.7__py3-none-any.whl → 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. validmind/__init__.py +3 -3
  2. validmind/__version__.py +1 -1
  3. validmind/ai.py +7 -11
  4. validmind/api_client.py +29 -27
  5. validmind/client.py +10 -3
  6. validmind/datasets/credit_risk/__init__.py +11 -0
  7. validmind/datasets/credit_risk/datasets/lending_club_loan_data_2007_2014_clean.csv.gz +0 -0
  8. validmind/datasets/credit_risk/lending_club.py +394 -0
  9. validmind/logging.py +9 -2
  10. validmind/template.py +2 -2
  11. validmind/test_suites/__init__.py +4 -2
  12. validmind/tests/__init__.py +97 -50
  13. validmind/tests/data_validation/FeatureTargetCorrelationPlot.py +3 -1
  14. validmind/tests/data_validation/PiTCreditScoresHistogram.py +1 -1
  15. validmind/tests/data_validation/ScatterPlot.py +8 -2
  16. validmind/tests/decorator.py +138 -14
  17. validmind/tests/model_validation/BertScore.py +1 -1
  18. validmind/tests/model_validation/BertScoreAggregate.py +1 -1
  19. validmind/tests/model_validation/BleuScore.py +1 -1
  20. validmind/tests/model_validation/ClusterSizeDistribution.py +1 -1
  21. validmind/tests/model_validation/ContextualRecall.py +1 -1
  22. validmind/tests/model_validation/FeaturesAUC.py +110 -0
  23. validmind/tests/model_validation/MeteorScore.py +1 -1
  24. validmind/tests/model_validation/RegardHistogram.py +1 -1
  25. validmind/tests/model_validation/RegardScore.py +1 -1
  26. validmind/tests/model_validation/RegressionResidualsPlot.py +127 -0
  27. validmind/tests/model_validation/RougeMetrics.py +1 -1
  28. validmind/tests/model_validation/RougeMetricsAggregate.py +1 -1
  29. validmind/tests/model_validation/SelfCheckNLIScore.py +1 -1
  30. validmind/tests/model_validation/TokenDisparity.py +1 -1
  31. validmind/tests/model_validation/ToxicityHistogram.py +1 -1
  32. validmind/tests/model_validation/ToxicityScore.py +1 -1
  33. validmind/tests/model_validation/embeddings/ClusterDistribution.py +1 -1
  34. validmind/tests/model_validation/embeddings/CosineSimilarityDistribution.py +1 -3
  35. validmind/tests/model_validation/embeddings/DescriptiveAnalytics.py +1 -1
  36. validmind/tests/model_validation/embeddings/EmbeddingsVisualization2D.py +1 -1
  37. validmind/tests/model_validation/sklearn/ClassifierPerformance.py +15 -18
  38. validmind/tests/model_validation/sklearn/ClusterCosineSimilarity.py +1 -1
  39. validmind/tests/model_validation/sklearn/ClusterPerformance.py +2 -2
  40. validmind/tests/model_validation/sklearn/ConfusionMatrix.py +21 -3
  41. validmind/tests/model_validation/sklearn/MinimumAccuracy.py +1 -1
  42. validmind/tests/model_validation/sklearn/MinimumF1Score.py +1 -1
  43. validmind/tests/model_validation/sklearn/MinimumROCAUCScore.py +1 -1
  44. validmind/tests/model_validation/sklearn/ModelsPerformanceComparison.py +5 -4
  45. validmind/tests/model_validation/sklearn/OverfitDiagnosis.py +2 -2
  46. validmind/tests/model_validation/sklearn/ROCCurve.py +6 -12
  47. validmind/tests/model_validation/sklearn/RegressionErrors.py +2 -2
  48. validmind/tests/model_validation/sklearn/RegressionModelsPerformanceComparison.py +6 -4
  49. validmind/tests/model_validation/sklearn/RegressionR2Square.py +2 -2
  50. validmind/tests/model_validation/sklearn/SHAPGlobalImportance.py +27 -3
  51. validmind/tests/model_validation/sklearn/SilhouettePlot.py +1 -1
  52. validmind/tests/model_validation/sklearn/TrainingTestDegradation.py +2 -2
  53. validmind/tests/model_validation/sklearn/WeakspotsDiagnosis.py +2 -2
  54. validmind/tests/model_validation/statsmodels/CumulativePredictionProbabilities.py +140 -0
  55. validmind/tests/model_validation/statsmodels/GINITable.py +22 -45
  56. validmind/tests/model_validation/statsmodels/{LogisticRegPredictionHistogram.py → PredictionProbabilitiesHistogram.py} +67 -92
  57. validmind/tests/model_validation/statsmodels/RegressionModelForecastPlot.py +2 -2
  58. validmind/tests/model_validation/statsmodels/RegressionModelForecastPlotLevels.py +2 -2
  59. validmind/tests/model_validation/statsmodels/RegressionModelInsampleComparison.py +1 -1
  60. validmind/tests/model_validation/statsmodels/RegressionModelOutsampleComparison.py +1 -1
  61. validmind/tests/model_validation/statsmodels/RegressionModelSummary.py +1 -1
  62. validmind/tests/model_validation/statsmodels/RegressionModelsPerformance.py +1 -1
  63. validmind/tests/model_validation/statsmodels/RegressionPermutationFeatureImportance.py +128 -0
  64. validmind/tests/model_validation/statsmodels/ScorecardHistogram.py +70 -103
  65. validmind/tests/test_providers.py +14 -124
  66. validmind/unit_metrics/__init__.py +76 -69
  67. validmind/unit_metrics/classification/sklearn/Accuracy.py +14 -0
  68. validmind/unit_metrics/classification/sklearn/F1.py +13 -0
  69. validmind/unit_metrics/classification/sklearn/Precision.py +13 -0
  70. validmind/unit_metrics/classification/sklearn/ROC_AUC.py +13 -0
  71. validmind/unit_metrics/classification/sklearn/Recall.py +13 -0
  72. validmind/unit_metrics/composite.py +24 -71
  73. validmind/unit_metrics/regression/GiniCoefficient.py +20 -26
  74. validmind/unit_metrics/regression/HuberLoss.py +12 -16
  75. validmind/unit_metrics/regression/KolmogorovSmirnovStatistic.py +18 -24
  76. validmind/unit_metrics/regression/MeanAbsolutePercentageError.py +7 -13
  77. validmind/unit_metrics/regression/MeanBiasDeviation.py +5 -14
  78. validmind/unit_metrics/regression/QuantileLoss.py +6 -16
  79. validmind/unit_metrics/regression/sklearn/AdjustedRSquaredScore.py +12 -18
  80. validmind/unit_metrics/regression/sklearn/MeanAbsoluteError.py +6 -15
  81. validmind/unit_metrics/regression/sklearn/MeanSquaredError.py +5 -14
  82. validmind/unit_metrics/regression/sklearn/RSquaredScore.py +6 -15
  83. validmind/unit_metrics/regression/sklearn/RootMeanSquaredError.py +11 -14
  84. validmind/utils.py +18 -45
  85. validmind/vm_models/__init__.py +0 -2
  86. validmind/vm_models/dataset.py +255 -16
  87. validmind/vm_models/test/metric.py +1 -2
  88. validmind/vm_models/test/result_wrapper.py +12 -13
  89. validmind/vm_models/test/test.py +2 -1
  90. validmind/vm_models/test/threshold_test.py +1 -2
  91. validmind/vm_models/test_suite/summary.py +3 -3
  92. validmind/vm_models/test_suite/test_suite.py +2 -1
  93. {validmind-2.0.7.dist-info → validmind-2.1.0.dist-info}/METADATA +10 -6
  94. {validmind-2.0.7.dist-info → validmind-2.1.0.dist-info}/RECORD +97 -96
  95. validmind/tests/__types__.py +0 -62
  96. validmind/tests/model_validation/statsmodels/LogRegressionConfusionMatrix.py +0 -128
  97. validmind/tests/model_validation/statsmodels/LogisticRegCumulativeProb.py +0 -172
  98. validmind/tests/model_validation/statsmodels/ScorecardBucketHistogram.py +0 -181
  99. validmind/tests/model_validation/statsmodels/ScorecardProbabilitiesHistogram.py +0 -175
  100. validmind/unit_metrics/sklearn/classification/Accuracy.py +0 -22
  101. validmind/unit_metrics/sklearn/classification/F1.py +0 -24
  102. validmind/unit_metrics/sklearn/classification/Precision.py +0 -24
  103. validmind/unit_metrics/sklearn/classification/ROC_AUC.py +0 -22
  104. validmind/unit_metrics/sklearn/classification/Recall.py +0 -22
  105. validmind/vm_models/test/unit_metric.py +0 -88
  106. {validmind-2.0.7.dist-info → validmind-2.1.0.dist-info}/LICENSE +0 -0
  107. {validmind-2.0.7.dist-info → validmind-2.1.0.dist-info}/WHEEL +0 -0
  108. {validmind-2.0.7.dist-info → validmind-2.1.0.dist-info}/entry_points.txt +0 -0
validmind/utils.py CHANGED
@@ -238,40 +238,6 @@ def summarize_data_quality_results(results):
238
238
  )
239
239
 
240
240
 
241
- def clean_docstring(docstring: str) -> str:
242
- """
243
- Clean up docstrings by removing leading and trailing whitespace and
244
- replacing newlines with spaces.
245
- """
246
- description = (docstring or "").strip()
247
- paragraphs = description.split("\n\n") # Split into paragraphs
248
- paragraphs = [
249
- " ".join([line.strip() for line in paragraph.split("\n")])
250
- for paragraph in paragraphs
251
- ]
252
- paragraphs = [
253
- paragraph.replace(" - ", "\n- ") for paragraph in paragraphs
254
- ] # Add newline before list items
255
- # Join paragraphs with double newlines for markdown
256
- description = "\n\n".join(paragraphs)
257
-
258
- lines = description.split("\n")
259
- in_bullet_list = False
260
- for i, line in enumerate([line for line in lines]):
261
- if line.strip().startswith("-") and not in_bullet_list:
262
- if lines[i - 1] != "":
263
- lines[i] = "\n" + line
264
-
265
- in_bullet_list = True
266
- continue
267
- elif line.strip().startswith("-") and in_bullet_list:
268
- continue
269
- elif line.strip() == "" and in_bullet_list:
270
- in_bullet_list = False
271
-
272
- return "\n".join(lines)
273
-
274
-
275
241
  def format_number(number):
276
242
  """
277
243
  Format a number for display purposes. If the number is a float, round it
@@ -354,20 +320,27 @@ def fuzzy_match(string: str, search_string: str, threshold=0.7):
354
320
  return score >= threshold
355
321
 
356
322
 
357
- def test_id_to_name(test_id: str):
358
- """Convert a test ID to a human-readable name"""
359
- # Extract the last part of the ID string
360
- last_part = test_id.split(".")[-1]
323
+ def test_id_to_name(test_id: str) -> str:
324
+ """Convert a test ID to a human-readable name.
361
325
 
362
- # Use a regular expression to find words and acronyms in the CamelCase string
363
- words = re.findall(r"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z]|$)", last_part)
326
+ Args:
327
+ test_id (str): The test identifier, typically in CamelCase or snake_case.
364
328
 
365
- # Join the words with spaces and capitalize the first letter of each word, keeping acronyms unchanged
366
- title = " ".join(
367
- [word.capitalize() if not word.isupper() else word for word in words]
368
- )
329
+ Returns:
330
+ str: A human-readable name derived from the test ID.
331
+ """
332
+ last_part = test_id.split(".")[-1]
333
+ words = []
334
+
335
+ # Split on underscores and apply regex to each part to handle CamelCase and acronyms
336
+ for part in last_part.split("_"):
337
+ # Regex pattern to match uppercase acronyms, mixed-case words, or alphanumeric combinations
338
+ words.extend(
339
+ re.findall(r"[A-Z]+(?:_[A-Z]+)*(?=_|$|[A-Z][a-z])|[A-Z]?[a-z0-9]+", part)
340
+ )
369
341
 
370
- return title
342
+ # Join the words with spaces, capitalize non-acronym words
343
+ return " ".join(word.capitalize() if not word.isupper() else word for word in words)
371
344
 
372
345
 
373
346
  def get_model_info(model):
@@ -15,7 +15,6 @@ from .test.result_summary import ResultSummary, ResultTable, ResultTableMetadata
15
15
  from .test.test import Test
16
16
  from .test.threshold_test import ThresholdTest
17
17
  from .test.threshold_test_result import ThresholdTestResult, ThresholdTestResults
18
- from .test.unit_metric import UnitMetric
19
18
  from .test_context import TestContext, TestInput
20
19
  from .test_suite.runner import TestSuiteRunner
21
20
  from .test_suite.test_suite import TestSuite
@@ -30,7 +29,6 @@ __all__ = [
30
29
  "ResultTable",
31
30
  "ResultTableMetadata",
32
31
  "Test",
33
- "UnitMetric",
34
32
  "Metric",
35
33
  "MetricResult",
36
34
  "ThresholdTest",
@@ -6,6 +6,7 @@
6
6
  Dataset class wrapper
7
7
  """
8
8
 
9
+ import warnings
9
10
  from abc import ABC, abstractmethod
10
11
  from dataclasses import dataclass, field
11
12
 
@@ -13,6 +14,7 @@ import numpy as np
13
14
  import pandas as pd
14
15
  import polars as pl
15
16
 
17
+ from validmind.errors import MissingOrInvalidModelPredictFnError
16
18
  from validmind.logging import get_logger
17
19
  from validmind.vm_models.model import VMModel
18
20
 
@@ -40,7 +42,9 @@ class VMDataset(ABC):
40
42
  self,
41
43
  model,
42
44
  prediction_values: list = None,
45
+ prediction_probabilities: list = None,
43
46
  prediction_column=None,
47
+ probability_column=None,
44
48
  ):
45
49
  """
46
50
  Assigns predictions to the dataset for a given model or prediction values.
@@ -151,15 +155,24 @@ class VMDataset(ABC):
151
155
  pass
152
156
 
153
157
  @abstractmethod
154
- def y_pred(self, model_id) -> np.ndarray:
158
+ def y_pred(self, model) -> np.ndarray:
155
159
  """
156
- Returns the prediction values (y_pred) of the dataset for a given model_id.
160
+ Returns the prediction values (y_pred) of the dataset for a given model.
157
161
 
158
162
  Returns:
159
163
  np.ndarray: The prediction values.
160
164
  """
161
165
  pass
162
166
 
167
+ def y_prob(self, model) -> np.ndarray:
168
+ """
169
+ Returns the prediction probabilities (y_prob) of the dataset for a given model.
170
+
171
+ Returns:
172
+ np.ndarray: The prediction probabilities.
173
+ """
174
+ pass
175
+
163
176
  @property
164
177
  @abstractmethod
165
178
  def df(self):
@@ -200,7 +213,17 @@ class VMDataset(ABC):
200
213
  pass
201
214
 
202
215
  @abstractmethod
203
- def y_pred_df(self, model_id):
216
+ def y_pred_df(self, model):
217
+ """
218
+ Returns the target columns (y) of the dataset.
219
+
220
+ Returns:
221
+ pd.DataFrame: The target columns.
222
+ """
223
+ pass
224
+
225
+ @abstractmethod
226
+ def y_prob_df(self, model):
204
227
  """
205
228
  Returns the target columns (y) of the dataset.
206
229
 
@@ -210,7 +233,7 @@ class VMDataset(ABC):
210
233
  pass
211
234
 
212
235
  @abstractmethod
213
- def prediction_column(self, model_id) -> str:
236
+ def prediction_column(self, model) -> str:
214
237
  """
215
238
  Returns the prediction column name of the dataset.
216
239
 
@@ -219,6 +242,15 @@ class VMDataset(ABC):
219
242
  """
220
243
  pass
221
244
 
245
+ def probability_column(self, model) -> str:
246
+ """
247
+ Returns the probability column name of the dataset.
248
+
249
+ Returns:
250
+ str: The probability column name.
251
+ """
252
+ pass
253
+
222
254
  @abstractmethod
223
255
  def get_features_columns(self):
224
256
  """
@@ -270,6 +302,7 @@ class NumpyDataset(VMDataset):
270
302
  _extra_columns: dict = field(
271
303
  default_factory=lambda: {
272
304
  "prediction_columns": {},
305
+ "probability_columns": {},
273
306
  "group_by_column": None,
274
307
  }
275
308
  )
@@ -356,6 +389,7 @@ class NumpyDataset(VMDataset):
356
389
  if extra_columns is None:
357
390
  extra_columns = {
358
391
  "prediction_columns": {},
392
+ "probability_columns": {},
359
393
  "group_by_column": None,
360
394
  }
361
395
  self._extra_columns = extra_columns
@@ -395,6 +429,9 @@ class NumpyDataset(VMDataset):
395
429
 
396
430
  return df
397
431
 
432
+ def __model_id_in_probability_columns(self, model, probability_column):
433
+ return model.input_id in self._extra_columns.get("probability_columns", {})
434
+
398
435
  def __model_id_in_prediction_columns(self, model, prediction_column):
399
436
  return model.input_id in self._extra_columns.get("prediction_columns", {})
400
437
 
@@ -423,17 +460,60 @@ class NumpyDataset(VMDataset):
423
460
  if pred_column not in self._columns:
424
461
  self._columns.append(pred_column)
425
462
 
463
+ def __assign_prediction_probabilities(
464
+ self, model, prob_column, prediction_probabilities
465
+ ):
466
+ # Link the prediction column with the model
467
+ self._extra_columns.setdefault("probability_columns", {})[
468
+ model.input_id
469
+ ] = prob_column
470
+
471
+ # Check if the predictions are multi-dimensional (e.g., embeddings)
472
+ is_multi_dimensional = (
473
+ isinstance(prediction_probabilities, np.ndarray)
474
+ and prediction_probabilities.ndim > 1
475
+ )
476
+
477
+ if is_multi_dimensional:
478
+ # For multi-dimensional outputs, convert to a list of lists to store in DataFrame
479
+ self._df[prob_column] = list(map(list, prediction_probabilities))
480
+ else:
481
+ # If not multi-dimensional or a standard numpy array, reshape for compatibility
482
+ self._raw_dataset = np.hstack(
483
+ (self._raw_dataset, np.array(prediction_probabilities).reshape(-1, 1))
484
+ )
485
+ self._df[prob_column] = prediction_probabilities
486
+
487
+ # Update the dataset columns list
488
+ if prob_column not in self._columns:
489
+ self._columns.append(prob_column)
490
+
426
491
  def assign_predictions( # noqa: C901 - we need to simplify this method
427
492
  self,
428
493
  model,
429
494
  prediction_values: list = None,
495
+ prediction_probabilities: list = None,
430
496
  prediction_column=None,
497
+ probability_column=None,
431
498
  ):
499
+ def _is_probability(output):
500
+ """Check if the output from the predict method is probabilities."""
501
+ # This is a simple check that assumes output is probabilities if they lie between 0 and 1
502
+ if np.all((output >= 0) & (output <= 1)):
503
+ # Check if there is at least one element that is neither 0 nor 1
504
+ if np.any((output > 0) & (output < 1)):
505
+ return True
506
+ return np.all((output >= 0) & (output <= 1)) and np.any(
507
+ (output > 0) & (output < 1)
508
+ )
509
+
510
+ # Step 1: Check for Model Presence
432
511
  if not model:
433
512
  raise ValueError(
434
513
  "Model must be provided to link prediction column with the dataset"
435
514
  )
436
515
 
516
+ # Step 2: Prediction Column Provided
437
517
  if prediction_column:
438
518
  if prediction_column not in self.columns:
439
519
  raise ValueError(
@@ -448,6 +528,8 @@ class NumpyDataset(VMDataset):
448
528
  self._extra_columns.setdefault("prediction_columns", {})[
449
529
  model.input_id
450
530
  ] = prediction_column
531
+
532
+ # Step 4: Prediction Values Provided without Specific Column
451
533
  elif prediction_values is not None:
452
534
  if len(prediction_values) != self.df.shape[0]:
453
535
  raise ValueError(
@@ -455,13 +537,58 @@ class NumpyDataset(VMDataset):
455
537
  )
456
538
  pred_column = f"{model.input_id}_prediction"
457
539
  if pred_column in self.columns:
458
- raise ValueError(
459
- f"Prediction column {pred_column} already exists in the dataset"
540
+ warnings.warn(
541
+ f"Prediction column {pred_column} already exists in the dataset, overwriting the existing predictions",
542
+ UserWarning,
460
543
  )
544
+
545
+ logger.info(
546
+ f"Assigning prediction values to column '{pred_column}' and linked to model '{model.input_id}'"
547
+ )
461
548
  self.__assign_prediction_values(model, pred_column, prediction_values)
549
+
550
+ # Step 3: Probability Column Provided
551
+ if probability_column:
552
+ if probability_column not in self.columns:
553
+ raise ValueError(
554
+ f"Probability column {probability_column} doesn't exist in the dataset"
555
+ )
556
+ if self.__model_id_in_probability_columns(
557
+ model=model, probability_column=probability_column
558
+ ):
559
+ raise ValueError(
560
+ f"Probability column {probability_column} already linked to the VM model"
561
+ )
562
+ self._extra_columns.setdefault("probability_columns", {})[
563
+ model.input_id
564
+ ] = probability_column
565
+
566
+ # Step 5: Prediction Probabilities Provided without Specific Column
567
+ elif prediction_probabilities is not None:
568
+ if len(prediction_probabilities) != self.df.shape[0]:
569
+ raise ValueError(
570
+ "Length of prediction probabilities doesn't match number of rows of the dataset"
571
+ )
572
+ prob_column = f"{model.input_id}_probabilities"
573
+ if prob_column in self.columns:
574
+ warnings.warn(
575
+ f"Probability column {prob_column} already exists in the dataset, overwriting the existing probabilities",
576
+ UserWarning,
577
+ )
578
+
579
+ logger.info(
580
+ f"Assigning prediction probabilities to column '{prob_column}' and linked to model '{model.input_id}'"
581
+ )
582
+ self.__assign_prediction_probabilities(
583
+ model, prob_column, prediction_probabilities
584
+ )
585
+
586
+ # Step 6: Neither Specific Column Nor Values Provided
462
587
  elif not self.__model_id_in_prediction_columns(
463
588
  model=model, prediction_column=prediction_column
464
589
  ):
590
+
591
+ # Compute prediction values directly from the VM model
465
592
  pred_column = f"{model.input_id}_prediction"
466
593
  if pred_column in self.columns:
467
594
  logger.info(
@@ -479,7 +606,49 @@ class NumpyDataset(VMDataset):
479
606
  )
480
607
 
481
608
  prediction_values = np.array(model.predict(x_only))
482
- self.__assign_prediction_values(model, pred_column, prediction_values)
609
+
610
+ # Check if the prediction values are probabilities
611
+ if _is_probability(prediction_values):
612
+
613
+ threshold = 0.5
614
+
615
+ logger.info(
616
+ "Predict method returned probabilities instead of direct labels or regression values. "
617
+ + "This implies the model is likely configured for a classification task with probability output."
618
+ )
619
+ prob_column = f"{model.input_id}_probabilities"
620
+ logger.info(
621
+ f"Assigning probabilities to column '{prob_column}' and computing class labels using a threshold of {threshold}."
622
+ )
623
+ self.__assign_prediction_probabilities(
624
+ model, prob_column, prediction_values
625
+ )
626
+
627
+ # Convert probabilities to class labels based on the threshold
628
+ prediction_classes = (prediction_values > threshold).astype(int)
629
+ self.__assign_prediction_values(model, pred_column, prediction_classes)
630
+
631
+ else:
632
+
633
+ # If not assign the prediction values directly
634
+ pred_column = f"{model.input_id}_prediction"
635
+ self.__assign_prediction_values(model, pred_column, prediction_values)
636
+
637
+ try:
638
+ logger.info("Running predict_proba()... This may take a while")
639
+ prediction_probabilities = np.array(model.predict_proba(x_only))
640
+ prob_column = f"{model.input_id}_probabilities"
641
+ self.__assign_prediction_probabilities(
642
+ model, prob_column, prediction_probabilities
643
+ )
644
+ except MissingOrInvalidModelPredictFnError:
645
+ # Log that predict_proba is not available or failed
646
+ logger.warn(
647
+ f"Model class '{model.__class__}' does not have a compatible predict_proba implementation."
648
+ + " Please assign predictions directly with vm_dataset.assign_predictions(model, prediction_values)"
649
+ )
650
+
651
+ # Step 7: Prediction Column Already Linked
483
652
  else:
484
653
  logger.info(
485
654
  f"Prediction column {self._extra_columns['prediction_columns'][model.input_id]} already linked to the {model.input_id}"
@@ -673,19 +842,19 @@ class NumpyDataset(VMDataset):
673
842
  ],
674
843
  ]
675
844
 
676
- def y_pred(self, model_id) -> np.ndarray:
845
+ def y_pred(self, model) -> np.ndarray:
677
846
  """
678
- Returns the prediction variables for a given model_id, accommodating
847
+ Returns the prediction variables for a given model, accommodating
679
848
  both scalar predictions and multi-dimensional outputs such as embeddings.
680
849
 
681
850
  Args:
682
- model_id (str): The ID of the model whose predictions are sought.
851
+ model (VMModel): The model whose predictions are sought.
683
852
 
684
853
  Returns:
685
854
  np.ndarray: The prediction variables, either as a flattened array for
686
855
  scalar predictions or as an array of arrays for multi-dimensional outputs.
687
856
  """
688
- pred_column = self.prediction_column(model_id)
857
+ pred_column = self.prediction_column(model)
689
858
 
690
859
  # First, attempt to retrieve the prediction data from the DataFrame
691
860
  if hasattr(self, "_df") and pred_column in self._df.columns:
@@ -712,6 +881,45 @@ class NumpyDataset(VMDataset):
712
881
 
713
882
  return predictions
714
883
 
884
+ def y_prob(self, model) -> np.ndarray:
885
+ """
886
+ Returns the prediction variables for a given model, accommodating
887
+ both scalar predictions and multi-dimensional outputs such as embeddings.
888
+
889
+ Args:
890
+ model (str): The ID of the model whose predictions are sought.
891
+
892
+ Returns:
893
+ np.ndarray: The prediction variables, either as a flattened array for
894
+ scalar predictions or as an array of arrays for multi-dimensional outputs.
895
+ """
896
+ prob_column = self.probability_column(model)
897
+
898
+ # First, attempt to retrieve the prediction data from the DataFrame
899
+ if hasattr(self, "_df") and prob_column in self._df.columns:
900
+ probabilities = self._df[prob_column].to_numpy()
901
+
902
+ # Check if the predictions are stored as objects (e.g., lists for embeddings)
903
+ if self._df[prob_column].dtype == object:
904
+ # Attempt to convert lists to a numpy array
905
+ try:
906
+ probabilities = np.stack(probabilities)
907
+ except ValueError as e:
908
+ # Handling cases where predictions cannot be directly stacked
909
+ raise ValueError(f"Error stacking prediction arrays: {e}")
910
+ else:
911
+ # Fallback to using the raw numpy dataset if DataFrame is not available or suitable
912
+ try:
913
+ probabilities = self.raw_dataset[
914
+ :, self.columns.index(prob_column)
915
+ ].flatten()
916
+ except IndexError as e:
917
+ raise ValueError(
918
+ f"Prediction column '{prob_column}' not found in raw dataset: {e}"
919
+ )
920
+
921
+ return probabilities
922
+
715
923
  @property
716
924
  def type(self) -> str:
717
925
  """
@@ -757,22 +965,32 @@ class NumpyDataset(VMDataset):
757
965
  """
758
966
  return self._df[self.target_column]
759
967
 
760
- def y_pred_df(self, model_id):
968
+ def y_pred_df(self, model):
969
+ """
970
+ Returns the target columns (y) of the dataset.
971
+
972
+ Returns:
973
+ pd.DataFrame: The target columns.
974
+ """
975
+ return self._df[self.prediction_column(model)]
976
+
977
+ def y_prob_df(self, model):
761
978
  """
762
979
  Returns the target columns (y) of the dataset.
763
980
 
764
981
  Returns:
765
982
  pd.DataFrame: The target columns.
766
983
  """
767
- return self._df[self.prediction_column(model_id=model_id)]
984
+ return self._df[self.probability_column(model)]
768
985
 
769
- def prediction_column(self, model_id) -> str:
986
+ def prediction_column(self, model) -> str:
770
987
  """
771
988
  Returns the prediction column name of the dataset.
772
989
 
773
990
  Returns:
774
991
  str: The prediction column name.
775
992
  """
993
+ model_id = model.input_id
776
994
  pred_column = self._extra_columns.get("prediction_columns", {}).get(model_id)
777
995
  if pred_column is None:
778
996
  raise ValueError(
@@ -780,6 +998,21 @@ class NumpyDataset(VMDataset):
780
998
  )
781
999
  return pred_column
782
1000
 
1001
+ def probability_column(self, model) -> str:
1002
+ """
1003
+ Returns the prediction column name of the dataset.
1004
+
1005
+ Returns:
1006
+ str: The prediction column name.
1007
+ """
1008
+ model_id = model.input_id
1009
+ prob_column = self._extra_columns.get("probability_columns", {}).get(model_id)
1010
+ if prob_column is None:
1011
+ raise ValueError(
1012
+ f"Probability column is not linked with the given {model_id}"
1013
+ )
1014
+ return prob_column
1015
+
783
1016
  def serialize(self):
784
1017
  """
785
1018
  Serializes the dataset to a dictionary.
@@ -1023,12 +1256,16 @@ class TorchDataset(NumpyDataset):
1023
1256
  text_column (str, optional): The text column name of the dataset for nlp tasks. Defaults to None.
1024
1257
  target_class_labels (Dict, optional): The class labels for the target columns. Defaults to None.
1025
1258
  """
1026
- # if we can't import torch, then it's not a PyTorch model
1259
+
1027
1260
  try:
1028
1261
  import torch
1029
1262
  except ImportError:
1030
- return False
1263
+ raise ImportError(
1264
+ "PyTorch is not installed, please run `pip install validmind[pytorch]`"
1265
+ )
1266
+
1031
1267
  columns = []
1268
+
1032
1269
  for id, tens in zip(range(0, len(raw_dataset.tensors)), raw_dataset.tensors):
1033
1270
  if id == 0 and feature_columns is None:
1034
1271
  n_cols = tens.shape[1]
@@ -1039,9 +1276,11 @@ class TorchDataset(NumpyDataset):
1039
1276
  ).astype(str)
1040
1277
  ]
1041
1278
  columns.append(feature_columns)
1279
+
1042
1280
  elif id == 1 and target_column is None:
1043
1281
  target_column = "y"
1044
1282
  columns.append(target_column)
1283
+
1045
1284
  elif id == 2 and extra_columns is None:
1046
1285
  extra_columns.prediction_column = "y_pred"
1047
1286
  columns.append(extra_columns.prediction_column)
@@ -15,7 +15,6 @@ import pandas as pd
15
15
 
16
16
  from ...ai import generate_description
17
17
  from ...errors import MissingCacheResultsArgumentsError
18
- from ...utils import clean_docstring
19
18
  from ..figure import Figure
20
19
  from .metric_result import MetricResult
21
20
  from .result_wrapper import MetricResultWrapper
@@ -98,7 +97,7 @@ class Metric(Test):
98
97
  )
99
98
  else:
100
99
  revision_name = "Default Description"
101
- description = clean_docstring(self.description())
100
+ description = self.description()
102
101
 
103
102
  description_metadata = {
104
103
  "content_id": f"metric_description:{self.test_id}::{revision_name}",
@@ -13,7 +13,7 @@ from dataclasses import dataclass
13
13
  from typing import Dict, List, Optional, Union
14
14
 
15
15
  import ipywidgets as widgets
16
- import markdown
16
+ import mistune
17
17
  import pandas as pd
18
18
  from IPython.display import display
19
19
 
@@ -103,7 +103,7 @@ class ResultWrapper(ABC):
103
103
  """
104
104
  Convert a markdown string to html
105
105
  """
106
- return markdown.markdown(description, extensions=["markdown.extensions.tables"])
106
+ return mistune.html(description)
107
107
 
108
108
  def _summary_tables_to_widget(self, summary: ResultSummary):
109
109
  """
@@ -120,21 +120,19 @@ class ResultWrapper(ABC):
120
120
  [
121
121
  {
122
122
  "selector": "",
123
- "props": [
124
- ("width", "100%"),
125
- ],
123
+ "props": [("width", "100%")],
124
+ },
125
+ {
126
+ "selector": "th",
127
+ "props": [("text-align", "left")],
126
128
  },
127
129
  {
128
130
  "selector": "tbody tr:nth-child(even)",
129
- "props": [
130
- ("background-color", "#FFFFFF"),
131
- ],
131
+ "props": [("background-color", "#FFFFFF")],
132
132
  },
133
133
  {
134
134
  "selector": "tbody tr:nth-child(odd)",
135
- "props": [
136
- ("background-color", "#F5F5F5"),
137
- ],
135
+ "props": [("background-color", "#F5F5F5")],
138
136
  },
139
137
  {
140
138
  "selector": "td, th",
@@ -144,7 +142,8 @@ class ResultWrapper(ABC):
144
142
  ],
145
143
  },
146
144
  ]
147
- ) # add borders
145
+ )
146
+ .set_properties(**{"text-align": "left"})
148
147
  .to_html(escape=False)
149
148
  ) # table.data is an orient=records dump
150
149
 
@@ -217,7 +216,7 @@ class MetricResultWrapper(ResultWrapper):
217
216
  return ""
218
217
 
219
218
  vbox_children = [
220
- widgets.HTML(value=f"<h1>{test_id_to_name(self.result_id)}</h1>")
219
+ widgets.HTML(value=f"<h1>{test_id_to_name(self.result_id)}</h1>"),
221
220
  ]
222
221
 
223
222
  if self.result_metadata:
@@ -6,6 +6,7 @@
6
6
 
7
7
  from abc import abstractmethod
8
8
  from dataclasses import dataclass
9
+ from inspect import getdoc
9
10
  from typing import ClassVar, List, TypedDict
10
11
  from uuid import uuid4
11
12
 
@@ -66,7 +67,7 @@ class Test(TestUtils):
66
67
  Return the test description. May be overridden by subclasses. Defaults
67
68
  to returning the class' docstring
68
69
  """
69
- return self.__doc__.strip()
70
+ return getdoc(self).strip()
70
71
 
71
72
  @abstractmethod
72
73
  def summary(self, *args, **kwargs):
@@ -13,7 +13,6 @@ from dataclasses import dataclass
13
13
  from typing import ClassVar, List, Optional
14
14
 
15
15
  from ...ai import generate_description
16
- from ...utils import clean_docstring
17
16
  from ..figure import Figure
18
17
  from .result_summary import ResultSummary, ResultTable
19
18
  from .result_wrapper import ThresholdTestResultWrapper
@@ -94,7 +93,7 @@ class ThresholdTest(Test):
94
93
  )
95
94
  else:
96
95
  revision_name = "Default Description"
97
- description = clean_docstring(self.description())
96
+ description = self.description()
98
97
 
99
98
  description_metadata = {
100
99
  "content_id": f"test_description:{self.test_id}::{revision_name}",