validmind 2.8.29__py3-none-any.whl → 2.9.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/__version__.py +1 -1
- validmind/ai/utils.py +4 -24
- validmind/api_client.py +6 -17
- validmind/logging.py +48 -0
- validmind/tests/__init__.py +2 -0
- validmind/tests/__types__.py +18 -0
- validmind/tests/output.py +9 -2
- validmind/tests/plots/BoxPlot.py +260 -0
- validmind/tests/plots/CorrelationHeatmap.py +235 -0
- validmind/tests/plots/HistogramPlot.py +233 -0
- validmind/tests/plots/ViolinPlot.py +125 -0
- validmind/tests/plots/__init__.py +0 -0
- validmind/tests/stats/CorrelationAnalysis.py +251 -0
- validmind/tests/stats/DescriptiveStats.py +197 -0
- validmind/tests/stats/NormalityTests.py +147 -0
- validmind/tests/stats/OutlierDetection.py +173 -0
- validmind/tests/stats/__init__.py +0 -0
- validmind/unit_metrics/classification/individual/AbsoluteError.py +42 -0
- validmind/unit_metrics/classification/individual/BrierScore.py +56 -0
- validmind/unit_metrics/classification/individual/CalibrationError.py +77 -0
- validmind/unit_metrics/classification/individual/ClassBalance.py +65 -0
- validmind/unit_metrics/classification/individual/Confidence.py +52 -0
- validmind/unit_metrics/classification/individual/Correctness.py +41 -0
- validmind/unit_metrics/classification/individual/LogLoss.py +61 -0
- validmind/unit_metrics/classification/individual/OutlierScore.py +86 -0
- validmind/unit_metrics/classification/individual/ProbabilityError.py +54 -0
- validmind/unit_metrics/classification/individual/Uncertainty.py +60 -0
- validmind/unit_metrics/classification/individual/__init__.py +0 -0
- validmind/vm_models/dataset/dataset.py +147 -1
- validmind/vm_models/result/result.py +26 -4
- {validmind-2.8.29.dist-info → validmind-2.9.1.dist-info}/METADATA +2 -2
- {validmind-2.8.29.dist-info → validmind-2.9.1.dist-info}/RECORD +35 -14
- {validmind-2.8.29.dist-info → validmind-2.9.1.dist-info}/LICENSE +0 -0
- {validmind-2.8.29.dist-info → validmind-2.9.1.dist-info}/WHEEL +0 -0
- {validmind-2.8.29.dist-info → validmind-2.9.1.dist-info}/entry_points.txt +0 -0
validmind/__version__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "2.
|
1
|
+
__version__ = "2.9.1"
|
validmind/ai/utils.py
CHANGED
@@ -3,9 +3,8 @@
|
|
3
3
|
# SPDX-License-Identifier: AGPL-3.0 AND ValidMind Commercial
|
4
4
|
|
5
5
|
import os
|
6
|
-
from urllib.parse import urljoin
|
7
6
|
|
8
|
-
from openai import AzureOpenAI,
|
7
|
+
from openai import AzureOpenAI, OpenAI
|
9
8
|
|
10
9
|
from ..logging import get_logger
|
11
10
|
from ..utils import md_to_html
|
@@ -83,28 +82,9 @@ def get_client_and_model():
|
|
83
82
|
logger.debug(f"Using Azure OpenAI {__model} for generating descriptions")
|
84
83
|
|
85
84
|
else:
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
response = get_ai_key()
|
91
|
-
__client = Client(
|
92
|
-
base_url=(
|
93
|
-
# TODO: improve this to be a bit more dynamic
|
94
|
-
"http://localhost:4000/genai"
|
95
|
-
if "localhost" in get_api_host()
|
96
|
-
else urljoin(get_api_host(), "/genai")
|
97
|
-
),
|
98
|
-
api_key=response["key"],
|
99
|
-
)
|
100
|
-
__model = "gpt-4o" # TODO: backend should tell us which model to use
|
101
|
-
logger.debug(f"Using ValidMind {__model} for generating descriptions")
|
102
|
-
except Exception as e:
|
103
|
-
logger.debug(f"Failed to get API key: {e}")
|
104
|
-
raise ValueError(
|
105
|
-
"OPENAI_API_KEY, AZURE_OPENAI_KEY must be set, or your account "
|
106
|
-
"must be setup to use ValidMind's LLM in order to use LLM features"
|
107
|
-
)
|
85
|
+
raise ValueError(
|
86
|
+
"OPENAI_API_KEY, AZURE_OPENAI_KEY must be setup to use LLM features"
|
87
|
+
)
|
108
88
|
|
109
89
|
return __client, __model
|
110
90
|
|
validmind/api_client.py
CHANGED
@@ -22,7 +22,7 @@ from ipywidgets import HTML, Accordion
|
|
22
22
|
|
23
23
|
from .client_config import client_config
|
24
24
|
from .errors import MissingAPICredentialsError, MissingModelIdError, raise_api_error
|
25
|
-
from .logging import get_logger, init_sentry, send_single_error
|
25
|
+
from .logging import get_logger, init_sentry, log_api_operation, send_single_error
|
26
26
|
from .utils import NumpyEncoder, is_html, md_to_html, run_async
|
27
27
|
from .vm_models import Figure
|
28
28
|
|
@@ -85,7 +85,7 @@ def _get_session() -> aiohttp.ClientSession:
|
|
85
85
|
if not __api_session or __api_session.closed:
|
86
86
|
__api_session = aiohttp.ClientSession(
|
87
87
|
headers=_get_api_headers(),
|
88
|
-
timeout=aiohttp.ClientTimeout(total=30),
|
88
|
+
timeout=aiohttp.ClientTimeout(total=int(os.getenv("VM_API_TIMEOUT", 30))),
|
89
89
|
)
|
90
90
|
|
91
91
|
return __api_session
|
@@ -304,6 +304,10 @@ async def alog_metadata(
|
|
304
304
|
raise e
|
305
305
|
|
306
306
|
|
307
|
+
@log_api_operation(
|
308
|
+
operation_name="Sending figure to ValidMind API",
|
309
|
+
extract_key=lambda figure: figure.key,
|
310
|
+
)
|
307
311
|
async def alog_figure(figure: Figure) -> Dict[str, Any]:
|
308
312
|
"""Logs a figure.
|
309
313
|
|
@@ -525,21 +529,6 @@ def log_metric(
|
|
525
529
|
)
|
526
530
|
|
527
531
|
|
528
|
-
def get_ai_key() -> Dict[str, Any]:
|
529
|
-
"""Calls the API to get an API key for our LLM proxy."""
|
530
|
-
r = requests.get(
|
531
|
-
url=_get_url("ai/key"),
|
532
|
-
headers=_get_api_headers(),
|
533
|
-
)
|
534
|
-
|
535
|
-
if r.status_code != 200:
|
536
|
-
# TODO: improve error handling when there's no Open AI API or AI key available
|
537
|
-
# logger.error("Could not get AI key from ValidMind API")
|
538
|
-
raise_api_error(r.text)
|
539
|
-
|
540
|
-
return r.json()
|
541
|
-
|
542
|
-
|
543
532
|
def generate_test_result_description(test_result_data: Dict[str, Any]) -> str:
|
544
533
|
r = requests.post(
|
545
534
|
url=_get_url("ai/generate/test_result_description"),
|
validmind/logging.py
CHANGED
@@ -170,6 +170,54 @@ async def log_performance_async(
|
|
170
170
|
return wrap
|
171
171
|
|
172
172
|
|
173
|
+
def log_api_operation(
|
174
|
+
operation_name: Optional[str] = None,
|
175
|
+
logger: Optional[logging.Logger] = None,
|
176
|
+
extract_key: Optional[Callable] = None,
|
177
|
+
force: bool = False,
|
178
|
+
) -> Callable[[F], F]:
|
179
|
+
"""Decorator to log API operations like figure uploads.
|
180
|
+
|
181
|
+
Args:
|
182
|
+
operation_name (str, optional): The name of the operation. Defaults to function name.
|
183
|
+
logger (logging.Logger, optional): The logger to use. Defaults to None.
|
184
|
+
extract_key (Callable, optional): Function to extract a key from args for logging.
|
185
|
+
force (bool, optional): Whether to force logging even if env var is off.
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
Callable: The decorated function.
|
189
|
+
"""
|
190
|
+
|
191
|
+
def decorator(func: F) -> F:
|
192
|
+
# check if log level is set to debug
|
193
|
+
if _get_log_level() != logging.DEBUG and not force:
|
194
|
+
return func
|
195
|
+
|
196
|
+
nonlocal logger
|
197
|
+
if logger is None:
|
198
|
+
logger = get_logger()
|
199
|
+
|
200
|
+
nonlocal operation_name
|
201
|
+
if operation_name is None:
|
202
|
+
operation_name = func.__name__
|
203
|
+
|
204
|
+
async def wrapped(*args: Any, **kwargs: Any) -> Any:
|
205
|
+
# Try to extract a meaningful identifier from the arguments
|
206
|
+
identifier = ""
|
207
|
+
if extract_key and args:
|
208
|
+
try:
|
209
|
+
identifier = f": {extract_key(args[0])}"
|
210
|
+
except (AttributeError, IndexError):
|
211
|
+
pass
|
212
|
+
|
213
|
+
logger.debug(f"{operation_name}{identifier}")
|
214
|
+
return await func(*args, **kwargs)
|
215
|
+
|
216
|
+
return wrapped
|
217
|
+
|
218
|
+
return decorator
|
219
|
+
|
220
|
+
|
173
221
|
def send_single_error(error: Exception) -> None:
|
174
222
|
"""Send a single error to Sentry.
|
175
223
|
|
validmind/tests/__init__.py
CHANGED
validmind/tests/__types__.py
CHANGED
@@ -187,6 +187,10 @@ TestID = Union[
|
|
187
187
|
"validmind.ongoing_monitoring.ScoreBandsDrift",
|
188
188
|
"validmind.ongoing_monitoring.ScorecardHistogramDrift",
|
189
189
|
"validmind.ongoing_monitoring.TargetPredictionDistributionPlot",
|
190
|
+
"validmind.plots.BoxPlot",
|
191
|
+
"validmind.plots.CorrelationHeatmap",
|
192
|
+
"validmind.plots.HistogramPlot",
|
193
|
+
"validmind.plots.ViolinPlot",
|
190
194
|
"validmind.prompt_validation.Bias",
|
191
195
|
"validmind.prompt_validation.Clarity",
|
192
196
|
"validmind.prompt_validation.Conciseness",
|
@@ -194,11 +198,25 @@ TestID = Union[
|
|
194
198
|
"validmind.prompt_validation.NegativeInstruction",
|
195
199
|
"validmind.prompt_validation.Robustness",
|
196
200
|
"validmind.prompt_validation.Specificity",
|
201
|
+
"validmind.stats.CorrelationAnalysis",
|
202
|
+
"validmind.stats.DescriptiveStats",
|
203
|
+
"validmind.stats.NormalityTests",
|
204
|
+
"validmind.stats.OutlierDetection",
|
197
205
|
"validmind.unit_metrics.classification.Accuracy",
|
198
206
|
"validmind.unit_metrics.classification.F1",
|
199
207
|
"validmind.unit_metrics.classification.Precision",
|
200
208
|
"validmind.unit_metrics.classification.ROC_AUC",
|
201
209
|
"validmind.unit_metrics.classification.Recall",
|
210
|
+
"validmind.unit_metrics.classification.individual.AbsoluteError",
|
211
|
+
"validmind.unit_metrics.classification.individual.BrierScore",
|
212
|
+
"validmind.unit_metrics.classification.individual.CalibrationError",
|
213
|
+
"validmind.unit_metrics.classification.individual.ClassBalance",
|
214
|
+
"validmind.unit_metrics.classification.individual.Confidence",
|
215
|
+
"validmind.unit_metrics.classification.individual.Correctness",
|
216
|
+
"validmind.unit_metrics.classification.individual.LogLoss",
|
217
|
+
"validmind.unit_metrics.classification.individual.OutlierScore",
|
218
|
+
"validmind.unit_metrics.classification.individual.ProbabilityError",
|
219
|
+
"validmind.unit_metrics.classification.individual.Uncertainty",
|
202
220
|
"validmind.unit_metrics.regression.AdjustedRSquaredScore",
|
203
221
|
"validmind.unit_metrics.regression.GiniCoefficient",
|
204
222
|
"validmind.unit_metrics.regression.HuberLoss",
|
validmind/tests/output.py
CHANGED
@@ -45,7 +45,13 @@ class BooleanOutputHandler(OutputHandler):
|
|
45
45
|
|
46
46
|
class MetricOutputHandler(OutputHandler):
|
47
47
|
def can_handle(self, item: Any) -> bool:
|
48
|
-
|
48
|
+
# Accept individual numbers
|
49
|
+
if isinstance(item, (int, float)):
|
50
|
+
return True
|
51
|
+
# Accept lists/arrays of numbers for per-row metrics
|
52
|
+
if isinstance(item, (list, tuple, np.ndarray)):
|
53
|
+
return all(isinstance(x, (int, float, np.number)) for x in item)
|
54
|
+
return False
|
49
55
|
|
50
56
|
def process(self, item: Any, result: TestResult) -> None:
|
51
57
|
if result.metric is not None:
|
@@ -169,11 +175,12 @@ def process_output(item: Any, result: TestResult) -> None:
|
|
169
175
|
"""Process a single test output item and update the TestResult."""
|
170
176
|
handlers = [
|
171
177
|
BooleanOutputHandler(),
|
172
|
-
MetricOutputHandler(),
|
173
178
|
FigureOutputHandler(),
|
174
179
|
TableOutputHandler(),
|
175
180
|
RawDataOutputHandler(),
|
176
181
|
StringOutputHandler(),
|
182
|
+
# Unit metrics should be processed last
|
183
|
+
MetricOutputHandler(),
|
177
184
|
]
|
178
185
|
|
179
186
|
for handler in handlers:
|
@@ -0,0 +1,260 @@
|
|
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
|
+
from typing import List, Optional
|
6
|
+
|
7
|
+
import plotly.graph_objects as go
|
8
|
+
from plotly.subplots import make_subplots
|
9
|
+
|
10
|
+
from validmind import tags, tasks
|
11
|
+
from validmind.errors import SkipTestError
|
12
|
+
from validmind.vm_models import VMDataset
|
13
|
+
|
14
|
+
|
15
|
+
def _validate_inputs(
|
16
|
+
dataset: VMDataset, columns: Optional[List[str]], group_by: Optional[str]
|
17
|
+
):
|
18
|
+
"""Validate inputs and return validated columns."""
|
19
|
+
if columns is None:
|
20
|
+
columns = dataset.feature_columns_numeric
|
21
|
+
else:
|
22
|
+
available_columns = set(dataset.feature_columns_numeric)
|
23
|
+
columns = [col for col in columns if col in available_columns]
|
24
|
+
|
25
|
+
if not columns:
|
26
|
+
raise SkipTestError("No numerical columns found for box plotting")
|
27
|
+
|
28
|
+
if group_by is not None:
|
29
|
+
if group_by not in dataset.df.columns:
|
30
|
+
raise SkipTestError(f"Group column '{group_by}' not found in dataset")
|
31
|
+
if group_by in columns:
|
32
|
+
columns.remove(group_by)
|
33
|
+
|
34
|
+
return columns
|
35
|
+
|
36
|
+
|
37
|
+
def _create_grouped_boxplot(
|
38
|
+
dataset, columns, group_by, colors, show_outliers, title_prefix, width, height
|
39
|
+
):
|
40
|
+
"""Create grouped box plots."""
|
41
|
+
fig = go.Figure()
|
42
|
+
groups = dataset.df[group_by].dropna().unique()
|
43
|
+
|
44
|
+
for col_idx, column in enumerate(columns):
|
45
|
+
for group_idx, group_value in enumerate(groups):
|
46
|
+
data_subset = dataset.df[dataset.df[group_by] == group_value][
|
47
|
+
column
|
48
|
+
].dropna()
|
49
|
+
|
50
|
+
if len(data_subset) > 0:
|
51
|
+
color = colors[group_idx % len(colors)]
|
52
|
+
fig.add_trace(
|
53
|
+
go.Box(
|
54
|
+
y=data_subset,
|
55
|
+
name=f"{group_value}",
|
56
|
+
marker_color=color,
|
57
|
+
boxpoints="outliers" if show_outliers else False,
|
58
|
+
jitter=0.3,
|
59
|
+
pointpos=-1.8,
|
60
|
+
legendgroup=f"{group_value}",
|
61
|
+
showlegend=(col_idx == 0),
|
62
|
+
offsetgroup=group_idx,
|
63
|
+
x=[column] * len(data_subset),
|
64
|
+
)
|
65
|
+
)
|
66
|
+
|
67
|
+
fig.update_layout(
|
68
|
+
title=f"{title_prefix} Features by {group_by}",
|
69
|
+
xaxis_title="Features",
|
70
|
+
yaxis_title="Values",
|
71
|
+
boxmode="group",
|
72
|
+
width=width,
|
73
|
+
height=height,
|
74
|
+
template="plotly_white",
|
75
|
+
)
|
76
|
+
return fig
|
77
|
+
|
78
|
+
|
79
|
+
def _create_single_boxplot(
|
80
|
+
dataset, column, colors, show_outliers, title_prefix, width, height
|
81
|
+
):
|
82
|
+
"""Create single column box plot."""
|
83
|
+
data = dataset.df[column].dropna()
|
84
|
+
if len(data) == 0:
|
85
|
+
raise SkipTestError(f"No data available for column {column}")
|
86
|
+
|
87
|
+
fig = go.Figure()
|
88
|
+
fig.add_trace(
|
89
|
+
go.Box(
|
90
|
+
y=data,
|
91
|
+
name=column,
|
92
|
+
marker_color=colors[0],
|
93
|
+
boxpoints="outliers" if show_outliers else False,
|
94
|
+
jitter=0.3,
|
95
|
+
pointpos=-1.8,
|
96
|
+
)
|
97
|
+
)
|
98
|
+
|
99
|
+
fig.update_layout(
|
100
|
+
title=f"{title_prefix} {column}",
|
101
|
+
yaxis_title=column,
|
102
|
+
width=width,
|
103
|
+
height=height,
|
104
|
+
template="plotly_white",
|
105
|
+
showlegend=False,
|
106
|
+
)
|
107
|
+
return fig
|
108
|
+
|
109
|
+
|
110
|
+
def _create_multiple_boxplots(
|
111
|
+
dataset, columns, colors, show_outliers, title_prefix, width, height
|
112
|
+
):
|
113
|
+
"""Create multiple column box plots in subplot layout."""
|
114
|
+
n_cols = min(3, len(columns))
|
115
|
+
n_rows = (len(columns) + n_cols - 1) // n_cols
|
116
|
+
|
117
|
+
subplot_titles = [f"{title_prefix} {col}" for col in columns]
|
118
|
+
fig = make_subplots(
|
119
|
+
rows=n_rows,
|
120
|
+
cols=n_cols,
|
121
|
+
subplot_titles=subplot_titles,
|
122
|
+
vertical_spacing=0.1,
|
123
|
+
horizontal_spacing=0.1,
|
124
|
+
)
|
125
|
+
|
126
|
+
for idx, column in enumerate(columns):
|
127
|
+
row = (idx // n_cols) + 1
|
128
|
+
col = (idx % n_cols) + 1
|
129
|
+
data = dataset.df[column].dropna()
|
130
|
+
|
131
|
+
if len(data) > 0:
|
132
|
+
color = colors[idx % len(colors)]
|
133
|
+
fig.add_trace(
|
134
|
+
go.Box(
|
135
|
+
y=data,
|
136
|
+
name=column,
|
137
|
+
marker_color=color,
|
138
|
+
boxpoints="outliers" if show_outliers else False,
|
139
|
+
jitter=0.3,
|
140
|
+
pointpos=-1.8,
|
141
|
+
showlegend=False,
|
142
|
+
),
|
143
|
+
row=row,
|
144
|
+
col=col,
|
145
|
+
)
|
146
|
+
fig.update_yaxes(title_text=column, row=row, col=col)
|
147
|
+
else:
|
148
|
+
fig.add_annotation(
|
149
|
+
text=f"No data available<br>for {column}",
|
150
|
+
x=0.5,
|
151
|
+
y=0.5,
|
152
|
+
xref=f"x{idx+1} domain" if idx > 0 else "x domain",
|
153
|
+
yref=f"y{idx+1} domain" if idx > 0 else "y domain",
|
154
|
+
showarrow=False,
|
155
|
+
row=row,
|
156
|
+
col=col,
|
157
|
+
)
|
158
|
+
|
159
|
+
fig.update_layout(
|
160
|
+
title="Dataset Feature Distributions",
|
161
|
+
width=width,
|
162
|
+
height=height,
|
163
|
+
template="plotly_white",
|
164
|
+
showlegend=False,
|
165
|
+
)
|
166
|
+
return fig
|
167
|
+
|
168
|
+
|
169
|
+
@tags("tabular_data", "visualization", "data_quality")
|
170
|
+
@tasks("classification", "regression", "clustering")
|
171
|
+
def BoxPlot(
|
172
|
+
dataset: VMDataset,
|
173
|
+
columns: Optional[List[str]] = None,
|
174
|
+
group_by: Optional[str] = None,
|
175
|
+
width: int = 1200,
|
176
|
+
height: int = 600,
|
177
|
+
colors: Optional[List[str]] = None,
|
178
|
+
show_outliers: bool = True,
|
179
|
+
title_prefix: str = "Box Plot of",
|
180
|
+
) -> go.Figure:
|
181
|
+
"""
|
182
|
+
Generates customizable box plots for numerical features in a dataset with optional grouping using Plotly.
|
183
|
+
|
184
|
+
### Purpose
|
185
|
+
|
186
|
+
This test provides a flexible way to visualize the distribution of numerical features
|
187
|
+
through interactive box plots, with optional grouping by categorical variables. Box plots are
|
188
|
+
effective for identifying outliers, comparing distributions across groups, and
|
189
|
+
understanding the spread and central tendency of the data.
|
190
|
+
|
191
|
+
### Test Mechanism
|
192
|
+
|
193
|
+
The test creates interactive box plots for specified numerical columns (or all numerical columns
|
194
|
+
if none specified). It supports various customization options including:
|
195
|
+
- Grouping by categorical variables
|
196
|
+
- Customizable colors and styling
|
197
|
+
- Outlier display options
|
198
|
+
- Interactive hover information
|
199
|
+
- Zoom and pan capabilities
|
200
|
+
|
201
|
+
### Signs of High Risk
|
202
|
+
|
203
|
+
- Presence of many outliers indicating data quality issues
|
204
|
+
- Highly skewed distributions
|
205
|
+
- Large differences in variance across groups
|
206
|
+
- Unexpected patterns in grouped data
|
207
|
+
|
208
|
+
### Strengths
|
209
|
+
|
210
|
+
- Clear visualization of distribution statistics (median, quartiles, outliers)
|
211
|
+
- Interactive Plotly plots with hover information and zoom capabilities
|
212
|
+
- Effective for comparing distributions across groups
|
213
|
+
- Handles missing values appropriately
|
214
|
+
- Highly customizable appearance
|
215
|
+
|
216
|
+
### Limitations
|
217
|
+
|
218
|
+
- Limited to numerical features only
|
219
|
+
- May not be suitable for continuous variables with many unique values
|
220
|
+
- Visual interpretation may be subjective
|
221
|
+
- Less effective with very large datasets
|
222
|
+
"""
|
223
|
+
# Validate inputs
|
224
|
+
columns = _validate_inputs(dataset, columns, group_by)
|
225
|
+
|
226
|
+
# Set default colors
|
227
|
+
if colors is None:
|
228
|
+
colors = [
|
229
|
+
"steelblue",
|
230
|
+
"orange",
|
231
|
+
"green",
|
232
|
+
"red",
|
233
|
+
"purple",
|
234
|
+
"brown",
|
235
|
+
"pink",
|
236
|
+
"gray",
|
237
|
+
"olive",
|
238
|
+
"cyan",
|
239
|
+
]
|
240
|
+
|
241
|
+
# Create appropriate plot type
|
242
|
+
if group_by is not None:
|
243
|
+
return _create_grouped_boxplot(
|
244
|
+
dataset,
|
245
|
+
columns,
|
246
|
+
group_by,
|
247
|
+
colors,
|
248
|
+
show_outliers,
|
249
|
+
title_prefix,
|
250
|
+
width,
|
251
|
+
height,
|
252
|
+
)
|
253
|
+
elif len(columns) == 1:
|
254
|
+
return _create_single_boxplot(
|
255
|
+
dataset, columns[0], colors, show_outliers, title_prefix, width, height
|
256
|
+
)
|
257
|
+
else:
|
258
|
+
return _create_multiple_boxplots(
|
259
|
+
dataset, columns, colors, show_outliers, title_prefix, width, height
|
260
|
+
)
|