validmind 2.7.5__py3-none-any.whl → 2.7.6__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/__version__.py +1 -1
- validmind/datasets/credit_risk/lending_club.py +354 -88
- validmind/tests/data_validation/HighPearsonCorrelation.py +12 -2
- validmind/tests/ongoing_monitoring/CalibrationCurveDrift.py +218 -0
- validmind/tests/ongoing_monitoring/ClassDiscriminationDrift.py +153 -0
- validmind/tests/ongoing_monitoring/ClassImbalanceDrift.py +144 -0
- validmind/tests/ongoing_monitoring/ClassificationAccuracyDrift.py +146 -0
- validmind/tests/ongoing_monitoring/ConfusionMatrixDrift.py +191 -0
- validmind/tests/ongoing_monitoring/CumulativePredictionProbabilitiesDrift.py +176 -0
- validmind/tests/ongoing_monitoring/FeatureDrift.py +120 -121
- validmind/tests/ongoing_monitoring/PredictionAcrossEachFeature.py +18 -23
- validmind/tests/ongoing_monitoring/PredictionCorrelation.py +86 -45
- validmind/tests/ongoing_monitoring/PredictionProbabilitiesHistogramDrift.py +202 -0
- validmind/tests/ongoing_monitoring/PredictionQuantilesAcrossFeatures.py +97 -0
- validmind/tests/ongoing_monitoring/ROCCurveDrift.py +149 -0
- validmind/tests/ongoing_monitoring/ScoreBandsDrift.py +210 -0
- validmind/tests/ongoing_monitoring/ScorecardHistogramDrift.py +207 -0
- validmind/tests/ongoing_monitoring/TargetPredictionDistributionPlot.py +91 -14
- validmind/vm_models/dataset/dataset.py +0 -4
- {validmind-2.7.5.dist-info → validmind-2.7.6.dist-info}/METADATA +2 -2
- {validmind-2.7.5.dist-info → validmind-2.7.6.dist-info}/RECORD +24 -13
- {validmind-2.7.5.dist-info → validmind-2.7.6.dist-info}/LICENSE +0 -0
- {validmind-2.7.5.dist-info → validmind-2.7.6.dist-info}/WHEEL +0 -0
- {validmind-2.7.5.dist-info → validmind-2.7.6.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,210 @@
|
|
1
|
+
# Copyright © 2023-2024 ValidMind Inc. All rights reserved.
|
2
|
+
# See the LICENSE file in the root of this repository for details.
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0 AND ValidMind Commercial
|
4
|
+
|
5
|
+
import pandas as pd
|
6
|
+
import numpy as np
|
7
|
+
from typing import List
|
8
|
+
from validmind import tags, tasks
|
9
|
+
from validmind.vm_models import VMDataset, VMModel
|
10
|
+
|
11
|
+
|
12
|
+
@tags("visualization", "credit_risk", "scorecard")
|
13
|
+
@tasks("classification")
|
14
|
+
def ScoreBandsDrift(
|
15
|
+
datasets: List[VMDataset],
|
16
|
+
model: VMModel,
|
17
|
+
score_column: str = "score",
|
18
|
+
score_bands: list = None,
|
19
|
+
drift_threshold: float = 20.0,
|
20
|
+
):
|
21
|
+
"""
|
22
|
+
Analyzes drift in population distribution and default rates across score bands.
|
23
|
+
|
24
|
+
### Purpose
|
25
|
+
|
26
|
+
The Score Bands Drift test is designed to evaluate changes in score-based risk segmentation
|
27
|
+
over time. By comparing population distribution and default rates across score bands between
|
28
|
+
reference and monitoring datasets, this test helps identify whether the model's risk
|
29
|
+
stratification remains stable in production. This is crucial for understanding if the model's
|
30
|
+
scoring behavior maintains its intended risk separation and whether specific score ranges
|
31
|
+
have experienced significant shifts.
|
32
|
+
|
33
|
+
### Test Mechanism
|
34
|
+
|
35
|
+
This test proceeds by segmenting scores into predefined bands and analyzing three key metrics
|
36
|
+
across these bands: population distribution, predicted default rates, and observed default
|
37
|
+
rates. For each band, it computes these metrics for both reference and monitoring datasets
|
38
|
+
and quantifies drift as percentage changes. The test provides both detailed band-by-band
|
39
|
+
comparisons and overall stability assessment, with special attention to bands showing
|
40
|
+
significant drift.
|
41
|
+
|
42
|
+
### Signs of High Risk
|
43
|
+
|
44
|
+
- Large shifts in population distribution across bands
|
45
|
+
- Significant changes in default rates within bands
|
46
|
+
- Inconsistent drift patterns between adjacent bands
|
47
|
+
- Divergence between predicted and observed rates
|
48
|
+
- Systematic shifts in risk concentration
|
49
|
+
- Empty or sparse score bands in monitoring data
|
50
|
+
|
51
|
+
### Strengths
|
52
|
+
|
53
|
+
- Provides comprehensive view of score-based drift
|
54
|
+
- Identifies specific score ranges with instability
|
55
|
+
- Enables comparison of multiple risk metrics
|
56
|
+
- Includes both distribution and performance drift
|
57
|
+
- Supports business-relevant score segmentation
|
58
|
+
- Maintains interpretable drift thresholds
|
59
|
+
|
60
|
+
### Limitations
|
61
|
+
|
62
|
+
- Sensitive to choice of score band boundaries
|
63
|
+
- Requires sufficient samples in each band
|
64
|
+
- Cannot suggest optimal band adjustments
|
65
|
+
- May not capture within-band distribution changes
|
66
|
+
- Limited to predefined scoring metrics
|
67
|
+
- Complex interpretation with multiple drift signals
|
68
|
+
"""
|
69
|
+
# Validate score column
|
70
|
+
if score_column not in datasets[0].df.columns:
|
71
|
+
raise ValueError(
|
72
|
+
f"Score column '{score_column}' not found in reference dataset"
|
73
|
+
)
|
74
|
+
if score_column not in datasets[1].df.columns:
|
75
|
+
raise ValueError(
|
76
|
+
f"Score column '{score_column}' not found in monitoring dataset"
|
77
|
+
)
|
78
|
+
|
79
|
+
# Default score bands if none provided
|
80
|
+
if score_bands is None:
|
81
|
+
score_bands = [410, 440, 470]
|
82
|
+
|
83
|
+
# Create band labels
|
84
|
+
band_labels = [
|
85
|
+
f"{score_bands[i]}-{score_bands[i+1]}" for i in range(len(score_bands) - 1)
|
86
|
+
]
|
87
|
+
band_labels.insert(0, f"<{score_bands[0]}")
|
88
|
+
band_labels.append(f">{score_bands[-1]}")
|
89
|
+
|
90
|
+
# Process reference and monitoring datasets
|
91
|
+
def process_dataset(dataset, model):
|
92
|
+
df = dataset.df.copy()
|
93
|
+
df["score_band"] = pd.cut(
|
94
|
+
df[score_column],
|
95
|
+
bins=[-np.inf] + score_bands + [np.inf],
|
96
|
+
labels=band_labels,
|
97
|
+
)
|
98
|
+
y_pred = dataset.y_pred(model)
|
99
|
+
|
100
|
+
results = {}
|
101
|
+
total_population = len(df)
|
102
|
+
|
103
|
+
# Store min and max scores
|
104
|
+
min_score = df[score_column].min()
|
105
|
+
max_score = df[score_column].max()
|
106
|
+
|
107
|
+
for band in band_labels:
|
108
|
+
band_mask = df["score_band"] == band
|
109
|
+
population = band_mask.sum()
|
110
|
+
|
111
|
+
results[band] = {
|
112
|
+
"Population (%)": population / total_population * 100,
|
113
|
+
"Predicted Default Rate (%)": (
|
114
|
+
y_pred[band_mask].sum() / population * 100 if population > 0 else 0
|
115
|
+
),
|
116
|
+
"Observed Default Rate (%)": (
|
117
|
+
df[band_mask][dataset.target_column].sum() / population * 100
|
118
|
+
if population > 0
|
119
|
+
else 0
|
120
|
+
),
|
121
|
+
}
|
122
|
+
|
123
|
+
results["min_score"] = min_score
|
124
|
+
results["max_score"] = max_score
|
125
|
+
return results
|
126
|
+
|
127
|
+
# Get metrics for both datasets
|
128
|
+
ref_results = process_dataset(datasets[0], model)
|
129
|
+
mon_results = process_dataset(datasets[1], model)
|
130
|
+
|
131
|
+
# Create the three comparison tables
|
132
|
+
tables = {}
|
133
|
+
all_passed = True
|
134
|
+
|
135
|
+
metrics = [
|
136
|
+
("Population Distribution (%)", "Population (%)"),
|
137
|
+
("Predicted Default Rates (%)", "Predicted Default Rate (%)"),
|
138
|
+
("Observed Default Rates (%)", "Observed Default Rate (%)"),
|
139
|
+
]
|
140
|
+
|
141
|
+
for table_name, metric in metrics:
|
142
|
+
rows = []
|
143
|
+
metric_passed = True
|
144
|
+
|
145
|
+
for band in band_labels:
|
146
|
+
ref_val = ref_results[band][metric]
|
147
|
+
mon_val = mon_results[band][metric]
|
148
|
+
|
149
|
+
# Calculate drift - using absolute difference when reference is 0
|
150
|
+
drift = (
|
151
|
+
abs(mon_val - ref_val)
|
152
|
+
if ref_val == 0
|
153
|
+
else ((mon_val - ref_val) / abs(ref_val)) * 100
|
154
|
+
)
|
155
|
+
passed = abs(drift) < drift_threshold
|
156
|
+
metric_passed &= passed
|
157
|
+
|
158
|
+
rows.append(
|
159
|
+
{
|
160
|
+
"Score Band": band,
|
161
|
+
"Reference": round(ref_val, 4),
|
162
|
+
"Monitoring": round(mon_val, 4),
|
163
|
+
"Drift (%)": round(drift, 2),
|
164
|
+
"Pass/Fail": "Pass" if passed else "Fail",
|
165
|
+
}
|
166
|
+
)
|
167
|
+
|
168
|
+
# Add total row for all metrics
|
169
|
+
if metric == "Population (%)":
|
170
|
+
ref_total = 100.0
|
171
|
+
mon_total = 100.0
|
172
|
+
drift_total = 0.0
|
173
|
+
passed_total = True
|
174
|
+
else:
|
175
|
+
ref_total = sum(
|
176
|
+
ref_results[band][metric] * (ref_results[band]["Population (%)"] / 100)
|
177
|
+
for band in band_labels
|
178
|
+
)
|
179
|
+
mon_total = sum(
|
180
|
+
mon_results[band][metric] * (mon_results[band]["Population (%)"] / 100)
|
181
|
+
for band in band_labels
|
182
|
+
)
|
183
|
+
# Apply same drift calculation to totals
|
184
|
+
drift_total = (
|
185
|
+
abs(mon_total - ref_total)
|
186
|
+
if ref_total == 0
|
187
|
+
else ((mon_total - ref_total) / abs(ref_total)) * 100
|
188
|
+
)
|
189
|
+
passed_total = abs(drift_total) < drift_threshold
|
190
|
+
|
191
|
+
# Format total row with score ranges
|
192
|
+
total_label = (
|
193
|
+
f"Total ({ref_results['min_score']:.0f}-{ref_results['max_score']:.0f})"
|
194
|
+
)
|
195
|
+
|
196
|
+
rows.append(
|
197
|
+
{
|
198
|
+
"Score Band": total_label,
|
199
|
+
"Reference": round(ref_total, 4),
|
200
|
+
"Monitoring": round(mon_total, 4),
|
201
|
+
"Drift (%)": round(drift_total, 2),
|
202
|
+
"Pass/Fail": "Pass" if passed_total else "Fail",
|
203
|
+
}
|
204
|
+
)
|
205
|
+
|
206
|
+
metric_passed &= passed_total
|
207
|
+
tables[table_name] = pd.DataFrame(rows)
|
208
|
+
all_passed &= metric_passed
|
209
|
+
|
210
|
+
return tables, all_passed
|
@@ -0,0 +1,207 @@
|
|
1
|
+
# Copyright © 2023-2024 ValidMind Inc. All rights reserved.
|
2
|
+
# See the LICENSE file in the root of this repository for details.
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0 AND ValidMind Commercial
|
4
|
+
|
5
|
+
import numpy as np
|
6
|
+
import pandas as pd
|
7
|
+
import plotly.graph_objects as go
|
8
|
+
from plotly.subplots import make_subplots
|
9
|
+
from scipy import stats
|
10
|
+
from typing import List
|
11
|
+
from validmind import tags, tasks
|
12
|
+
from validmind.vm_models import VMDataset
|
13
|
+
|
14
|
+
|
15
|
+
@tags("visualization", "credit_risk", "logistic_regression")
|
16
|
+
@tasks("classification")
|
17
|
+
def ScorecardHistogramDrift(
|
18
|
+
datasets: List[VMDataset],
|
19
|
+
score_column: str = "score",
|
20
|
+
title: str = "Scorecard Histogram Drift",
|
21
|
+
drift_pct_threshold: float = 20.0,
|
22
|
+
):
|
23
|
+
"""
|
24
|
+
Compares score distributions between reference and monitoring datasets for each class.
|
25
|
+
|
26
|
+
### Purpose
|
27
|
+
|
28
|
+
The Scorecard Histogram Drift test is designed to evaluate changes in the model's scoring
|
29
|
+
patterns over time. By comparing score distributions between reference and monitoring datasets
|
30
|
+
for each class, this test helps identify whether the model's scoring behavior remains stable
|
31
|
+
in production. This is crucial for understanding if the model's risk assessment maintains
|
32
|
+
consistent patterns and whether specific score ranges have experienced significant shifts
|
33
|
+
in their distribution.
|
34
|
+
|
35
|
+
### Test Mechanism
|
36
|
+
|
37
|
+
This test proceeds by generating histograms of scores for each class in both reference and
|
38
|
+
monitoring datasets. It analyzes distribution characteristics through multiple statistical
|
39
|
+
moments: mean, variance, skewness, and kurtosis. The test quantifies drift as percentage
|
40
|
+
changes in these moments between datasets, providing both visual and numerical assessments
|
41
|
+
of distribution stability. Special attention is paid to class-specific distribution changes.
|
42
|
+
|
43
|
+
### Signs of High Risk
|
44
|
+
|
45
|
+
- Significant shifts in score distribution shapes
|
46
|
+
- Large drifts in distribution moments exceeding threshold
|
47
|
+
- Changes in the relative positioning of class distributions
|
48
|
+
- Appearance of new modes or peaks in monitoring data
|
49
|
+
- Unexpected changes in score spread or concentration
|
50
|
+
- Systematic shifts in class-specific scoring patterns
|
51
|
+
|
52
|
+
### Strengths
|
53
|
+
|
54
|
+
- Provides class-specific distribution analysis
|
55
|
+
- Identifies detailed changes in scoring patterns
|
56
|
+
- Enables visual comparison of distributions
|
57
|
+
- Includes comprehensive moment analysis
|
58
|
+
- Supports multiple class evaluation
|
59
|
+
- Maintains interpretable score scale
|
60
|
+
|
61
|
+
### Limitations
|
62
|
+
|
63
|
+
- Sensitive to binning choices in visualization
|
64
|
+
- Requires sufficient samples per class
|
65
|
+
- Cannot suggest score adjustments
|
66
|
+
- May not capture subtle distribution changes
|
67
|
+
- Complex interpretation with multiple classes
|
68
|
+
- Limited to univariate score analysis
|
69
|
+
"""
|
70
|
+
# Verify score column exists
|
71
|
+
if score_column not in datasets[0].df.columns:
|
72
|
+
raise ValueError(
|
73
|
+
f"Score column '{score_column}' not found in reference dataset"
|
74
|
+
)
|
75
|
+
if score_column not in datasets[1].df.columns:
|
76
|
+
raise ValueError(
|
77
|
+
f"Score column '{score_column}' not found in monitoring dataset"
|
78
|
+
)
|
79
|
+
|
80
|
+
# Get reference and monitoring data
|
81
|
+
df_ref = datasets[0].df
|
82
|
+
df_mon = datasets[1].df
|
83
|
+
|
84
|
+
# Get unique classes
|
85
|
+
classes = sorted(df_ref[datasets[0].target_column].unique())
|
86
|
+
|
87
|
+
# Create subplots with more horizontal space for legends
|
88
|
+
fig = make_subplots(
|
89
|
+
rows=len(classes),
|
90
|
+
cols=1,
|
91
|
+
subplot_titles=[f"Class {cls}" for cls in classes],
|
92
|
+
horizontal_spacing=0.15,
|
93
|
+
)
|
94
|
+
|
95
|
+
# Define colors
|
96
|
+
ref_color = "rgba(31, 119, 180, 0.8)" # Blue with 0.8 opacity
|
97
|
+
mon_color = "rgba(255, 127, 14, 0.8)" # Orange with 0.8 opacity
|
98
|
+
|
99
|
+
# Dictionary to store tables for each class
|
100
|
+
tables = {}
|
101
|
+
all_passed = True # Track overall pass/fail
|
102
|
+
|
103
|
+
# Add histograms and create tables for each class
|
104
|
+
for i, class_value in enumerate(classes, start=1):
|
105
|
+
# Get scores for current class
|
106
|
+
ref_scores = df_ref[df_ref[datasets[0].target_column] == class_value][
|
107
|
+
score_column
|
108
|
+
]
|
109
|
+
mon_scores = df_mon[df_mon[datasets[1].target_column] == class_value][
|
110
|
+
score_column
|
111
|
+
]
|
112
|
+
|
113
|
+
# Calculate distribution moments
|
114
|
+
ref_stats = {
|
115
|
+
"Mean": np.mean(ref_scores),
|
116
|
+
"Variance": np.var(ref_scores),
|
117
|
+
"Skewness": stats.skew(ref_scores),
|
118
|
+
"Kurtosis": stats.kurtosis(ref_scores),
|
119
|
+
}
|
120
|
+
|
121
|
+
mon_stats = {
|
122
|
+
"Mean": np.mean(mon_scores),
|
123
|
+
"Variance": np.var(mon_scores),
|
124
|
+
"Skewness": stats.skew(mon_scores),
|
125
|
+
"Kurtosis": stats.kurtosis(mon_scores),
|
126
|
+
}
|
127
|
+
|
128
|
+
# Create table for this class
|
129
|
+
table_data = []
|
130
|
+
class_passed = True # Track pass/fail for this class
|
131
|
+
|
132
|
+
for stat_name in ["Mean", "Variance", "Skewness", "Kurtosis"]:
|
133
|
+
ref_val = ref_stats[stat_name]
|
134
|
+
mon_val = mon_stats[stat_name]
|
135
|
+
drift = (
|
136
|
+
((mon_val - ref_val) / abs(ref_val)) * 100 if ref_val != 0 else np.inf
|
137
|
+
)
|
138
|
+
passed = abs(drift) < drift_pct_threshold
|
139
|
+
class_passed &= passed # Update class pass/fail
|
140
|
+
|
141
|
+
table_data.append(
|
142
|
+
{
|
143
|
+
"Statistic": stat_name,
|
144
|
+
"Reference": round(ref_val, 4),
|
145
|
+
"Monitoring": round(mon_val, 4),
|
146
|
+
"Drift (%)": round(drift, 2),
|
147
|
+
"Pass/Fail": "Pass" if passed else "Fail",
|
148
|
+
}
|
149
|
+
)
|
150
|
+
|
151
|
+
tables[f"Class {class_value}"] = pd.DataFrame(table_data)
|
152
|
+
all_passed &= class_passed # Update overall pass/fail
|
153
|
+
|
154
|
+
# Reference dataset histogram
|
155
|
+
fig.add_trace(
|
156
|
+
go.Histogram(
|
157
|
+
x=ref_scores,
|
158
|
+
name=f"Reference - Class {class_value}",
|
159
|
+
marker_color=ref_color,
|
160
|
+
showlegend=True,
|
161
|
+
legendrank=i * 2 - 1,
|
162
|
+
),
|
163
|
+
row=i,
|
164
|
+
col=1,
|
165
|
+
)
|
166
|
+
|
167
|
+
# Monitoring dataset histogram
|
168
|
+
fig.add_trace(
|
169
|
+
go.Histogram(
|
170
|
+
x=mon_scores,
|
171
|
+
name=f"Monitoring - Class {class_value}",
|
172
|
+
marker_color=mon_color,
|
173
|
+
showlegend=True,
|
174
|
+
legendrank=i * 2,
|
175
|
+
),
|
176
|
+
row=i,
|
177
|
+
col=1,
|
178
|
+
)
|
179
|
+
|
180
|
+
# Update layout
|
181
|
+
fig.update_layout(
|
182
|
+
title_text=title,
|
183
|
+
barmode="overlay",
|
184
|
+
height=300 * len(classes),
|
185
|
+
width=1000,
|
186
|
+
showlegend=True,
|
187
|
+
)
|
188
|
+
|
189
|
+
# Update axes labels and add separate legends for each subplot
|
190
|
+
for i in range(len(classes)):
|
191
|
+
fig.update_xaxes(title_text="Score", row=i + 1, col=1)
|
192
|
+
fig.update_yaxes(title_text="Frequency", row=i + 1, col=1)
|
193
|
+
|
194
|
+
# Add separate legend for each subplot
|
195
|
+
fig.update_layout(
|
196
|
+
**{
|
197
|
+
f'legend{i+1 if i > 0 else ""}': dict(
|
198
|
+
yanchor="middle",
|
199
|
+
y=1 - (i / len(classes)) - (0.5 / len(classes)),
|
200
|
+
xanchor="left",
|
201
|
+
x=1.05,
|
202
|
+
tracegroupgap=5,
|
203
|
+
)
|
204
|
+
}
|
205
|
+
)
|
206
|
+
|
207
|
+
return fig, tables, all_passed
|
@@ -2,15 +2,16 @@
|
|
2
2
|
# See the LICENSE file in the root of this repository for details.
|
3
3
|
# SPDX-License-Identifier: AGPL-3.0 AND ValidMind Commercial
|
4
4
|
|
5
|
-
import
|
6
|
-
import
|
7
|
-
|
5
|
+
import plotly.graph_objects as go
|
6
|
+
import plotly.figure_factory as ff
|
7
|
+
import pandas as pd
|
8
|
+
from scipy.stats import skew, kurtosis
|
8
9
|
from validmind import tags, tasks
|
9
10
|
|
10
11
|
|
11
12
|
@tags("visualization")
|
12
13
|
@tasks("monitoring")
|
13
|
-
def TargetPredictionDistributionPlot(datasets, model):
|
14
|
+
def TargetPredictionDistributionPlot(datasets, model, drift_pct_threshold=20):
|
14
15
|
"""
|
15
16
|
Assesses differences in prediction distributions between a reference dataset and a monitoring dataset to identify
|
16
17
|
potential data drift.
|
@@ -45,23 +46,99 @@ def TargetPredictionDistributionPlot(datasets, model):
|
|
45
46
|
- Less effective if the differences in distributions are subtle and not easily visible.
|
46
47
|
"""
|
47
48
|
|
49
|
+
# Get predictions
|
48
50
|
pred_ref = datasets[0].y_prob_df(model)
|
49
51
|
pred_ref.columns = ["Reference Prediction"]
|
50
52
|
pred_monitor = datasets[1].y_prob_df(model)
|
51
53
|
pred_monitor.columns = ["Monitoring Prediction"]
|
52
54
|
|
53
|
-
|
54
|
-
|
55
|
-
|
55
|
+
# Calculate distribution moments
|
56
|
+
moments = pd.DataFrame(
|
57
|
+
{
|
58
|
+
"Statistic": ["Mean", "Std", "Skewness", "Kurtosis"],
|
59
|
+
"Reference": [
|
60
|
+
pred_ref["Reference Prediction"].mean(),
|
61
|
+
pred_ref["Reference Prediction"].std(),
|
62
|
+
skew(pred_ref["Reference Prediction"]),
|
63
|
+
kurtosis(pred_ref["Reference Prediction"]),
|
64
|
+
],
|
65
|
+
"Monitoring": [
|
66
|
+
pred_monitor["Monitoring Prediction"].mean(),
|
67
|
+
pred_monitor["Monitoring Prediction"].std(),
|
68
|
+
skew(pred_monitor["Monitoring Prediction"]),
|
69
|
+
kurtosis(pred_monitor["Monitoring Prediction"]),
|
70
|
+
],
|
71
|
+
}
|
72
|
+
)
|
73
|
+
|
74
|
+
# Calculate drift percentage with direction
|
75
|
+
moments["Drift (%)"] = (
|
76
|
+
(moments["Monitoring"] - moments["Reference"])
|
77
|
+
/ moments["Reference"].abs()
|
78
|
+
* 100
|
79
|
+
).round(2)
|
80
|
+
|
81
|
+
# Add Pass/Fail column based on absolute drift
|
82
|
+
moments["Pass/Fail"] = (
|
83
|
+
moments["Drift (%)"]
|
84
|
+
.abs()
|
85
|
+
.apply(lambda x: "Pass" if x < drift_pct_threshold else "Fail")
|
86
|
+
)
|
87
|
+
|
88
|
+
# Set Statistic as index but keep it as a column
|
89
|
+
moments = moments.set_index("Statistic", drop=False)
|
90
|
+
|
91
|
+
# Create KDE for both distributions
|
92
|
+
ref_kde = ff.create_distplot(
|
93
|
+
[pred_ref["Reference Prediction"].values],
|
94
|
+
["Reference"],
|
95
|
+
show_hist=False,
|
96
|
+
show_rug=False,
|
56
97
|
)
|
57
|
-
|
58
|
-
pred_monitor["Monitoring Prediction"],
|
98
|
+
monitor_kde = ff.create_distplot(
|
99
|
+
[pred_monitor["Monitoring Prediction"].values],
|
100
|
+
["Monitoring"],
|
101
|
+
show_hist=False,
|
102
|
+
show_rug=False,
|
59
103
|
)
|
60
|
-
|
61
|
-
|
104
|
+
|
105
|
+
# Create new figure
|
106
|
+
fig = go.Figure()
|
107
|
+
|
108
|
+
# Add reference distribution
|
109
|
+
fig.add_trace(
|
110
|
+
go.Scatter(
|
111
|
+
x=ref_kde.data[0].x,
|
112
|
+
y=ref_kde.data[0].y,
|
113
|
+
fill="tozeroy",
|
114
|
+
name="Reference Prediction",
|
115
|
+
line=dict(color="blue", width=2),
|
116
|
+
opacity=0.6,
|
117
|
+
)
|
118
|
+
)
|
119
|
+
|
120
|
+
# Add monitoring distribution
|
121
|
+
fig.add_trace(
|
122
|
+
go.Scatter(
|
123
|
+
x=monitor_kde.data[0].x,
|
124
|
+
y=monitor_kde.data[0].y,
|
125
|
+
fill="tozeroy",
|
126
|
+
name="Monitor Prediction",
|
127
|
+
line=dict(color="red", width=2),
|
128
|
+
opacity=0.6,
|
129
|
+
)
|
130
|
+
)
|
131
|
+
|
132
|
+
# Update layout
|
133
|
+
fig.update_layout(
|
134
|
+
title="Distribution of Reference & Monitor Predictions",
|
135
|
+
xaxis_title="Prediction",
|
136
|
+
yaxis_title="Density",
|
137
|
+
showlegend=True,
|
138
|
+
template="plotly_white",
|
139
|
+
hovermode="x unified",
|
62
140
|
)
|
63
|
-
plot.legend()
|
64
141
|
|
65
|
-
|
142
|
+
pass_fail_bool = (moments["Pass/Fail"] == "Pass").all()
|
66
143
|
|
67
|
-
return fig
|
144
|
+
return ({"Distribution Moments": moments}, fig, pass_fail_bool)
|
@@ -369,10 +369,6 @@ class VMDataset(VMInput):
|
|
369
369
|
# reset feature columns to exclude the new extra column
|
370
370
|
self._set_feature_columns()
|
371
371
|
|
372
|
-
logger.info(
|
373
|
-
f"Extra column {column_name} with {len(column_values)} values added to the dataset"
|
374
|
-
)
|
375
|
-
|
376
372
|
@property
|
377
373
|
def df(self) -> pd.DataFrame:
|
378
374
|
"""
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: validmind
|
3
|
-
Version: 2.7.
|
3
|
+
Version: 2.7.6
|
4
4
|
Summary: ValidMind Library
|
5
5
|
License: Commercial License
|
6
6
|
Author: Andres Rodriguez
|
@@ -44,7 +44,7 @@ Requires-Dist: python-dotenv
|
|
44
44
|
Requires-Dist: ragas (>=0.2.3) ; extra == "all" or extra == "llm"
|
45
45
|
Requires-Dist: rouge (>=1)
|
46
46
|
Requires-Dist: rpy2 (>=3.5.10,<4.0.0) ; extra == "all" or extra == "r-support"
|
47
|
-
Requires-Dist: scikit-learn (
|
47
|
+
Requires-Dist: scikit-learn (<1.6.0)
|
48
48
|
Requires-Dist: scipy
|
49
49
|
Requires-Dist: scorecardpy (>=0.1.9.6,<0.2.0.0)
|
50
50
|
Requires-Dist: seaborn
|
@@ -1,5 +1,5 @@
|
|
1
1
|
validmind/__init__.py,sha256=U-S6pV31O3sVsbcEzlriz0tootyfvPnPOu4PHzXz9tM,2688
|
2
|
-
validmind/__version__.py,sha256=
|
2
|
+
validmind/__version__.py,sha256=6xG2XfctNZV_iMAbDf3PscewWwjPfwfmAC2zaeMR2KI,22
|
3
3
|
validmind/ai/test_descriptions.py,sha256=OpdMyLkZqlvegxjKfg2iJ0o4PwjnRv4_kEzePyuQiYs,7345
|
4
4
|
validmind/ai/test_result_description/config.yaml,sha256=E1gPd-uv-MzdrWZA_rP6LSk8pVmkYijx6v78hZ8ceL0,787
|
5
5
|
validmind/ai/test_result_description/context.py,sha256=ebKulFMpXTDLqd6lOHAsG200GmLNnhnu7sMDnbo2Dhc,2339
|
@@ -20,7 +20,7 @@ validmind/datasets/cluster/digits.py,sha256=E600pX6QPrqndfr73kwZ1sTNk0hC5kNj4Fhs
|
|
20
20
|
validmind/datasets/credit_risk/__init__.py,sha256=vK0wyUcA2mpjasNR-EaBj_0MdPhJw5KK8xlrKj_xl68,295
|
21
21
|
validmind/datasets/credit_risk/datasets/lending_club_biased.csv.gz,sha256=PdsyEqHtfShtfl_xoNWva2Ofyfx5hmrLhowPka4hLew,6266192
|
22
22
|
validmind/datasets/credit_risk/datasets/lending_club_loan_data_2007_2014_clean.csv.gz,sha256=bAgdfmUxjYOdZMPvoHtKr_GLoXNAX04KUTfjn2L62eE,5493810
|
23
|
-
validmind/datasets/credit_risk/lending_club.py,sha256=
|
23
|
+
validmind/datasets/credit_risk/lending_club.py,sha256=m8VnZSkNo5ja-y-WSc7MbUavlGKKY2YqfGOV3Pzcg_4,34630
|
24
24
|
validmind/datasets/credit_risk/lending_club_bias.py,sha256=8_Xf1qxCTUPv1wYHYkjabO2WtQsfVudJ6eje3phQUrc,4461
|
25
25
|
validmind/datasets/llm/rag/__init__.py,sha256=v8BygB6rGECoMIXv2_I1lVUAfPJ_gVo0GgVKhzk60h4,264
|
26
26
|
validmind/datasets/llm/rag/datasets/rfp_existing_questions_client_1.csv,sha256=8Ae8TD5Yh6rQ67HMCu7iKipj5tyOOhzylZqLppAeKzs,24095
|
@@ -113,7 +113,7 @@ validmind/tests/data_validation/Duplicates.py,sha256=HAEHRFwFZovJU-wBWea0KJREsJC
|
|
113
113
|
validmind/tests/data_validation/EngleGrangerCoint.py,sha256=kNBbxLYweF8qTF5JVRzcyXq3aKLhkN_1iv3mwwskTBU,4503
|
114
114
|
validmind/tests/data_validation/FeatureTargetCorrelationPlot.py,sha256=OwEEavtIY23HRH1CcdCJnTlbj1hn9mCLe9mG8Yw0EOs,4249
|
115
115
|
validmind/tests/data_validation/HighCardinality.py,sha256=nXANbDQPogdFzYLJ9xJIdjWjNJ7yhteAdkcUWrm4Lrg,3362
|
116
|
-
validmind/tests/data_validation/HighPearsonCorrelation.py,sha256=
|
116
|
+
validmind/tests/data_validation/HighPearsonCorrelation.py,sha256=KiWKgkqHY387GaYS7HmgHQFuwKiV95Su3eXS-h8paJs,3703
|
117
117
|
validmind/tests/data_validation/IQROutliersBarPlot.py,sha256=8E7onPlknLcci_zHrM1RbZJnYLrLhFMWblwadCqFozQ,4978
|
118
118
|
validmind/tests/data_validation/IQROutliersTable.py,sha256=_YoqYw-rATiPuY9sOYJAT1jKdGm_VFnYqfbugMkoM6w,3962
|
119
119
|
validmind/tests/data_validation/IsolationForestOutliers.py,sha256=bjkPF4Dczt-dzPZsElGZPv6G_fRkSB9sSeCwj42tZrg,3881
|
@@ -266,10 +266,21 @@ validmind/tests/model_validation/statsmodels/RegressionPermutationFeatureImporta
|
|
266
266
|
validmind/tests/model_validation/statsmodels/ScorecardHistogram.py,sha256=0hnB6icasRKT_Cl0YxMEpIuaUKgi5scXHmV_nP9RmkI,4650
|
267
267
|
validmind/tests/model_validation/statsmodels/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
268
268
|
validmind/tests/model_validation/statsmodels/statsutils.py,sha256=s1J7lHJ4kAcp_gGI0LAsaIFxbSqPrqXanxgtDI_Kig0,495
|
269
|
-
validmind/tests/ongoing_monitoring/
|
270
|
-
validmind/tests/ongoing_monitoring/
|
271
|
-
validmind/tests/ongoing_monitoring/
|
272
|
-
validmind/tests/ongoing_monitoring/
|
269
|
+
validmind/tests/ongoing_monitoring/CalibrationCurveDrift.py,sha256=VHWKOE317SMv6I_9Ua8unsV9CalpMVZqPLn8HGp9wqo,7843
|
270
|
+
validmind/tests/ongoing_monitoring/ClassDiscriminationDrift.py,sha256=ZdqrZf9Wr5gWhMICEx2RopL3CE-CJekDWNForo1JdHM,5706
|
271
|
+
validmind/tests/ongoing_monitoring/ClassImbalanceDrift.py,sha256=tdzhDbA2WrAFjUJmKYWraXCYArAK0hZOk9Fp2AyU0AY,5059
|
272
|
+
validmind/tests/ongoing_monitoring/ClassificationAccuracyDrift.py,sha256=C1FqCd_3C5J9BhgOlxtAtS6onOkxdF39-22XI-R5yEQ,4990
|
273
|
+
validmind/tests/ongoing_monitoring/ConfusionMatrixDrift.py,sha256=RokHTnbKGzDx5dsuXSuTT9kC-yIPEkjfq67bCM1UYug,6946
|
274
|
+
validmind/tests/ongoing_monitoring/CumulativePredictionProbabilitiesDrift.py,sha256=rXbK344BXCk-MI0ERylewMF8lL1jXdgoQZWdoYQexpk,6199
|
275
|
+
validmind/tests/ongoing_monitoring/FeatureDrift.py,sha256=Mgry31V2juaIWGyPVBJUxeHD5zC1PJpAWYQImwcrNJc,6191
|
276
|
+
validmind/tests/ongoing_monitoring/PredictionAcrossEachFeature.py,sha256=ngIS8Ybp4GzAqIZdVMxH4PvKtulMkxfsh6lSM6Gp1g0,3052
|
277
|
+
validmind/tests/ongoing_monitoring/PredictionCorrelation.py,sha256=W2nEdmGTvNN6v3y3Xscsh2V8lwtWxdcCHjwuyrn_91o,5020
|
278
|
+
validmind/tests/ongoing_monitoring/PredictionProbabilitiesHistogramDrift.py,sha256=qGLWPzWUHFatC1LC2JkMTSJaiPLR5G1KRzWe8cbWSLY,7130
|
279
|
+
validmind/tests/ongoing_monitoring/PredictionQuantilesAcrossFeatures.py,sha256=MMICygVYq2sN9w52apo3H6ajhWezNkKgTkTM93gYC_U,3251
|
280
|
+
validmind/tests/ongoing_monitoring/ROCCurveDrift.py,sha256=ST8QvyPnjSI4_qv7N0n-c157Ej6oGykCzaAVj35i3FQ,5121
|
281
|
+
validmind/tests/ongoing_monitoring/ScoreBandsDrift.py,sha256=FewqXl8eeZZR2Z_4qZxZGjnh8mRVcOsC7-prl3lBTT0,7496
|
282
|
+
validmind/tests/ongoing_monitoring/ScorecardHistogramDrift.py,sha256=7hD4JSyTKik1NHVG-FJ02IMcqWnoqAjox1JvYJZDoFg,7258
|
283
|
+
validmind/tests/ongoing_monitoring/TargetPredictionDistributionPlot.py,sha256=osC9QUPYJjR4hU3EnfVr7S5r5oLCh8txn3bgDl8CaB4,4863
|
273
284
|
validmind/tests/output.py,sha256=1kY9FJWUOpZ2BofxKQ5scxkg10Pvb24_OxypegHeh04,4029
|
274
285
|
validmind/tests/prompt_validation/Bias.py,sha256=UFtC7l8aXBkyzfpvZ2db2JlO5SZOssp2mCrUk5HKyTY,5702
|
275
286
|
validmind/tests/prompt_validation/Clarity.py,sha256=KA1hFtsUHO02epDEIc4W1LtuU3BoXCg3xkQsuIUKeuI,4825
|
@@ -303,7 +314,7 @@ validmind/unit_metrics/regression/RootMeanSquaredError.py,sha256=uIDsSpy75Z7W3zu
|
|
303
314
|
validmind/utils.py,sha256=WvjKXskGmVGupEVYvEiy5-0cBT_jwpKfpH2HsCfy_B8,18655
|
304
315
|
validmind/vm_models/__init__.py,sha256=lcqf9q2aRzrVrNN6R--81IkrnSa6BXPbhJ8SnkT_hcI,702
|
305
316
|
validmind/vm_models/dataset/__init__.py,sha256=U4CxZjdoc0dd9u2AqBl5PJh1UVbzXWNrmundmjLF-qE,346
|
306
|
-
validmind/vm_models/dataset/dataset.py,sha256=
|
317
|
+
validmind/vm_models/dataset/dataset.py,sha256=F6_rc5pjccRLnB7UcIMiGMbD-qMVUW5v4TnZTNSXTbo,26370
|
307
318
|
validmind/vm_models/dataset/utils.py,sha256=VMcPEgwW9oW5D0MCa_MqXCq_sEzzsLLRmS4RaYrsif0,5530
|
308
319
|
validmind/vm_models/figure.py,sha256=7VNOIsbOsUKyXvgxaY10H_Wvy2HEFte3nwdx09SZu20,6297
|
309
320
|
validmind/vm_models/input.py,sha256=qLdqz_bktr4v0YcPha2vFdDvmkC-btT1pH9zBIkt1OY,1046
|
@@ -316,8 +327,8 @@ validmind/vm_models/test_suite/runner.py,sha256=Cpl9WKwHzJD5Zvrh71FzbEhGZkHM0x0M
|
|
316
327
|
validmind/vm_models/test_suite/summary.py,sha256=Ug3nMvpPL2DSTDujWagWMCrFiW9oDy0AqJL_zXN8pH0,4642
|
317
328
|
validmind/vm_models/test_suite/test.py,sha256=uImjmPlBlLrlVPavsUzbaDK55bvpOn3PuFyWeyYyTac,3908
|
318
329
|
validmind/vm_models/test_suite/test_suite.py,sha256=5Jppt2UXSMgvJ6FO5LIAKA4oN_-hh9SMr8APAFJzk9g,5080
|
319
|
-
validmind-2.7.
|
320
|
-
validmind-2.7.
|
321
|
-
validmind-2.7.
|
322
|
-
validmind-2.7.
|
323
|
-
validmind-2.7.
|
330
|
+
validmind-2.7.6.dist-info/LICENSE,sha256=XonPUfwjvrC5Ombl3y-ko0Wubb1xdG_7nzvIbkZRKHw,35772
|
331
|
+
validmind-2.7.6.dist-info/METADATA,sha256=ce1HwawW3_UDY7y7PJjoRgx6Fp2Ta9FgIuJur5T3w1o,6123
|
332
|
+
validmind-2.7.6.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
|
333
|
+
validmind-2.7.6.dist-info/entry_points.txt,sha256=HuW7YyOv9u_OEWpViQXtv0nfoI67uieJHawKWA4Hv9A,76
|
334
|
+
validmind-2.7.6.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|