validmind 2.0.7__py3-none-any.whl → 2.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- validmind/__init__.py +3 -3
- validmind/__version__.py +1 -1
- validmind/ai.py +7 -11
- validmind/api_client.py +29 -27
- validmind/client.py +10 -3
- validmind/datasets/credit_risk/__init__.py +11 -0
- validmind/datasets/credit_risk/datasets/lending_club_loan_data_2007_2014_clean.csv.gz +0 -0
- validmind/datasets/credit_risk/lending_club.py +394 -0
- validmind/logging.py +9 -2
- validmind/template.py +2 -2
- validmind/test_suites/__init__.py +4 -2
- validmind/tests/__init__.py +97 -50
- validmind/tests/data_validation/FeatureTargetCorrelationPlot.py +3 -1
- validmind/tests/data_validation/PiTCreditScoresHistogram.py +1 -1
- validmind/tests/data_validation/ScatterPlot.py +8 -2
- validmind/tests/decorator.py +138 -14
- validmind/tests/model_validation/BertScore.py +1 -1
- validmind/tests/model_validation/BertScoreAggregate.py +1 -1
- validmind/tests/model_validation/BleuScore.py +1 -1
- validmind/tests/model_validation/ClusterSizeDistribution.py +1 -1
- validmind/tests/model_validation/ContextualRecall.py +1 -1
- validmind/tests/model_validation/FeaturesAUC.py +110 -0
- validmind/tests/model_validation/MeteorScore.py +1 -1
- validmind/tests/model_validation/RegardHistogram.py +1 -1
- validmind/tests/model_validation/RegardScore.py +1 -1
- validmind/tests/model_validation/RegressionResidualsPlot.py +127 -0
- validmind/tests/model_validation/RougeMetrics.py +1 -1
- validmind/tests/model_validation/RougeMetricsAggregate.py +1 -1
- validmind/tests/model_validation/SelfCheckNLIScore.py +1 -1
- validmind/tests/model_validation/TokenDisparity.py +1 -1
- validmind/tests/model_validation/ToxicityHistogram.py +1 -1
- validmind/tests/model_validation/ToxicityScore.py +1 -1
- validmind/tests/model_validation/embeddings/ClusterDistribution.py +1 -1
- validmind/tests/model_validation/embeddings/CosineSimilarityDistribution.py +1 -3
- validmind/tests/model_validation/embeddings/DescriptiveAnalytics.py +1 -1
- validmind/tests/model_validation/embeddings/EmbeddingsVisualization2D.py +1 -1
- validmind/tests/model_validation/sklearn/ClassifierPerformance.py +15 -18
- validmind/tests/model_validation/sklearn/ClusterCosineSimilarity.py +1 -1
- validmind/tests/model_validation/sklearn/ClusterPerformance.py +2 -2
- validmind/tests/model_validation/sklearn/ConfusionMatrix.py +21 -3
- validmind/tests/model_validation/sklearn/MinimumAccuracy.py +1 -1
- validmind/tests/model_validation/sklearn/MinimumF1Score.py +1 -1
- validmind/tests/model_validation/sklearn/MinimumROCAUCScore.py +1 -1
- validmind/tests/model_validation/sklearn/ModelsPerformanceComparison.py +5 -4
- validmind/tests/model_validation/sklearn/OverfitDiagnosis.py +2 -2
- validmind/tests/model_validation/sklearn/ROCCurve.py +6 -12
- validmind/tests/model_validation/sklearn/RegressionErrors.py +2 -2
- validmind/tests/model_validation/sklearn/RegressionModelsPerformanceComparison.py +6 -4
- validmind/tests/model_validation/sklearn/RegressionR2Square.py +2 -2
- validmind/tests/model_validation/sklearn/SHAPGlobalImportance.py +33 -3
- validmind/tests/model_validation/sklearn/SilhouettePlot.py +1 -1
- validmind/tests/model_validation/sklearn/TrainingTestDegradation.py +2 -2
- validmind/tests/model_validation/sklearn/WeakspotsDiagnosis.py +2 -2
- validmind/tests/model_validation/statsmodels/CumulativePredictionProbabilities.py +140 -0
- validmind/tests/model_validation/statsmodels/GINITable.py +22 -45
- validmind/tests/model_validation/statsmodels/{LogisticRegPredictionHistogram.py → PredictionProbabilitiesHistogram.py} +67 -92
- validmind/tests/model_validation/statsmodels/RegressionModelForecastPlot.py +2 -2
- validmind/tests/model_validation/statsmodels/RegressionModelForecastPlotLevels.py +2 -2
- validmind/tests/model_validation/statsmodels/RegressionModelInsampleComparison.py +1 -1
- validmind/tests/model_validation/statsmodels/RegressionModelOutsampleComparison.py +1 -1
- validmind/tests/model_validation/statsmodels/RegressionModelSummary.py +1 -1
- validmind/tests/model_validation/statsmodels/RegressionModelsPerformance.py +1 -1
- validmind/tests/model_validation/statsmodels/RegressionPermutationFeatureImportance.py +128 -0
- validmind/tests/model_validation/statsmodels/ScorecardHistogram.py +70 -103
- validmind/tests/test_providers.py +14 -124
- validmind/unit_metrics/__init__.py +76 -69
- validmind/unit_metrics/classification/sklearn/Accuracy.py +14 -0
- validmind/unit_metrics/classification/sklearn/F1.py +13 -0
- validmind/unit_metrics/classification/sklearn/Precision.py +13 -0
- validmind/unit_metrics/classification/sklearn/ROC_AUC.py +13 -0
- validmind/unit_metrics/classification/sklearn/Recall.py +13 -0
- validmind/unit_metrics/composite.py +24 -71
- validmind/unit_metrics/regression/GiniCoefficient.py +20 -26
- validmind/unit_metrics/regression/HuberLoss.py +12 -16
- validmind/unit_metrics/regression/KolmogorovSmirnovStatistic.py +18 -24
- validmind/unit_metrics/regression/MeanAbsolutePercentageError.py +7 -13
- validmind/unit_metrics/regression/MeanBiasDeviation.py +5 -14
- validmind/unit_metrics/regression/QuantileLoss.py +6 -16
- validmind/unit_metrics/regression/sklearn/AdjustedRSquaredScore.py +12 -18
- validmind/unit_metrics/regression/sklearn/MeanAbsoluteError.py +6 -15
- validmind/unit_metrics/regression/sklearn/MeanSquaredError.py +5 -14
- validmind/unit_metrics/regression/sklearn/RSquaredScore.py +6 -15
- validmind/unit_metrics/regression/sklearn/RootMeanSquaredError.py +11 -14
- validmind/utils.py +18 -45
- validmind/vm_models/__init__.py +0 -2
- validmind/vm_models/dataset.py +255 -16
- validmind/vm_models/test/metric.py +1 -2
- validmind/vm_models/test/result_wrapper.py +12 -13
- validmind/vm_models/test/test.py +2 -1
- validmind/vm_models/test/threshold_test.py +1 -2
- validmind/vm_models/test_suite/summary.py +3 -3
- validmind/vm_models/test_suite/test_suite.py +2 -1
- {validmind-2.0.7.dist-info → validmind-2.1.1.dist-info}/METADATA +10 -6
- {validmind-2.0.7.dist-info → validmind-2.1.1.dist-info}/RECORD +97 -96
- validmind/tests/__types__.py +0 -62
- validmind/tests/model_validation/statsmodels/LogRegressionConfusionMatrix.py +0 -128
- validmind/tests/model_validation/statsmodels/LogisticRegCumulativeProb.py +0 -172
- validmind/tests/model_validation/statsmodels/ScorecardBucketHistogram.py +0 -181
- validmind/tests/model_validation/statsmodels/ScorecardProbabilitiesHistogram.py +0 -175
- validmind/unit_metrics/sklearn/classification/Accuracy.py +0 -22
- validmind/unit_metrics/sklearn/classification/F1.py +0 -24
- validmind/unit_metrics/sklearn/classification/Precision.py +0 -24
- validmind/unit_metrics/sklearn/classification/ROC_AUC.py +0 -22
- validmind/unit_metrics/sklearn/classification/Recall.py +0 -22
- validmind/vm_models/test/unit_metric.py +0 -88
- {validmind-2.0.7.dist-info → validmind-2.1.1.dist-info}/LICENSE +0 -0
- {validmind-2.0.7.dist-info → validmind-2.1.1.dist-info}/WHEEL +0 -0
- {validmind-2.0.7.dist-info → validmind-2.1.1.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
|
-
|
363
|
-
|
326
|
+
Args:
|
327
|
+
test_id (str): The test identifier, typically in CamelCase or snake_case.
|
364
328
|
|
365
|
-
|
366
|
-
|
367
|
-
|
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
|
-
|
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):
|
validmind/vm_models/__init__.py
CHANGED
@@ -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",
|
validmind/vm_models/dataset.py
CHANGED
@@ -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,
|
158
|
+
def y_pred(self, model) -> np.ndarray:
|
155
159
|
"""
|
156
|
-
Returns the prediction values (y_pred) of the dataset for a given
|
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,
|
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,
|
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
|
-
|
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
|
-
|
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,
|
845
|
+
def y_pred(self, model) -> np.ndarray:
|
677
846
|
"""
|
678
|
-
Returns the prediction variables for a given
|
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
|
-
|
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(
|
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,
|
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.
|
984
|
+
return self._df[self.probability_column(model)]
|
768
985
|
|
769
|
-
def prediction_column(self,
|
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
|
-
|
1259
|
+
|
1027
1260
|
try:
|
1028
1261
|
import torch
|
1029
1262
|
except ImportError:
|
1030
|
-
|
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 =
|
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
|
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
|
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
|
-
|
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
|
-
)
|
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:
|
validmind/vm_models/test/test.py
CHANGED
@@ -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.
|
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 =
|
96
|
+
description = self.description()
|
98
97
|
|
99
98
|
description_metadata = {
|
100
99
|
"content_id": f"test_description:{self.test_id}::{revision_name}",
|