validmind 2.5.24__py3-none-any.whl → 2.6.7__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 (198) hide show
  1. validmind/__init__.py +8 -17
  2. validmind/__version__.py +1 -1
  3. validmind/ai/test_descriptions.py +66 -85
  4. validmind/ai/test_result_description/context.py +2 -2
  5. validmind/ai/utils.py +26 -1
  6. validmind/api_client.py +43 -79
  7. validmind/client.py +5 -7
  8. validmind/client_config.py +1 -1
  9. validmind/datasets/__init__.py +1 -1
  10. validmind/datasets/classification/customer_churn.py +7 -5
  11. validmind/datasets/nlp/__init__.py +2 -2
  12. validmind/errors.py +6 -10
  13. validmind/html_templates/content_blocks.py +18 -16
  14. validmind/logging.py +21 -16
  15. validmind/tests/__init__.py +28 -5
  16. validmind/tests/__types__.py +186 -170
  17. validmind/tests/_store.py +7 -21
  18. validmind/tests/comparison.py +362 -0
  19. validmind/tests/data_validation/ACFandPACFPlot.py +44 -73
  20. validmind/tests/data_validation/ADF.py +49 -83
  21. validmind/tests/data_validation/AutoAR.py +59 -96
  22. validmind/tests/data_validation/AutoMA.py +59 -96
  23. validmind/tests/data_validation/AutoStationarity.py +66 -114
  24. validmind/tests/data_validation/ClassImbalance.py +48 -117
  25. validmind/tests/data_validation/DatasetDescription.py +180 -209
  26. validmind/tests/data_validation/DatasetSplit.py +50 -75
  27. validmind/tests/data_validation/DescriptiveStatistics.py +59 -85
  28. validmind/tests/data_validation/{DFGLSArch.py → DickeyFullerGLS.py} +44 -76
  29. validmind/tests/data_validation/Duplicates.py +21 -90
  30. validmind/tests/data_validation/EngleGrangerCoint.py +53 -75
  31. validmind/tests/data_validation/HighCardinality.py +32 -80
  32. validmind/tests/data_validation/HighPearsonCorrelation.py +29 -97
  33. validmind/tests/data_validation/IQROutliersBarPlot.py +63 -94
  34. validmind/tests/data_validation/IQROutliersTable.py +40 -80
  35. validmind/tests/data_validation/IsolationForestOutliers.py +41 -63
  36. validmind/tests/data_validation/KPSS.py +33 -81
  37. validmind/tests/data_validation/LaggedCorrelationHeatmap.py +47 -95
  38. validmind/tests/data_validation/MissingValues.py +17 -58
  39. validmind/tests/data_validation/MissingValuesBarPlot.py +61 -87
  40. validmind/tests/data_validation/PhillipsPerronArch.py +56 -79
  41. validmind/tests/data_validation/RollingStatsPlot.py +50 -81
  42. validmind/tests/data_validation/SeasonalDecompose.py +102 -184
  43. validmind/tests/data_validation/Skewness.py +27 -64
  44. validmind/tests/data_validation/SpreadPlot.py +34 -57
  45. validmind/tests/data_validation/TabularCategoricalBarPlots.py +46 -65
  46. validmind/tests/data_validation/TabularDateTimeHistograms.py +23 -45
  47. validmind/tests/data_validation/TabularNumericalHistograms.py +27 -46
  48. validmind/tests/data_validation/TargetRateBarPlots.py +54 -93
  49. validmind/tests/data_validation/TimeSeriesFrequency.py +48 -133
  50. validmind/tests/data_validation/TimeSeriesHistogram.py +24 -3
  51. validmind/tests/data_validation/TimeSeriesLinePlot.py +29 -47
  52. validmind/tests/data_validation/TimeSeriesMissingValues.py +59 -135
  53. validmind/tests/data_validation/TimeSeriesOutliers.py +54 -171
  54. validmind/tests/data_validation/TooManyZeroValues.py +21 -70
  55. validmind/tests/data_validation/UniqueRows.py +23 -62
  56. validmind/tests/data_validation/WOEBinPlots.py +83 -109
  57. validmind/tests/data_validation/WOEBinTable.py +28 -69
  58. validmind/tests/data_validation/ZivotAndrewsArch.py +33 -75
  59. validmind/tests/data_validation/nlp/CommonWords.py +49 -57
  60. validmind/tests/data_validation/nlp/Hashtags.py +27 -49
  61. validmind/tests/data_validation/nlp/LanguageDetection.py +7 -13
  62. validmind/tests/data_validation/nlp/Mentions.py +32 -63
  63. validmind/tests/data_validation/nlp/PolarityAndSubjectivity.py +89 -14
  64. validmind/tests/data_validation/nlp/Punctuations.py +63 -47
  65. validmind/tests/data_validation/nlp/Sentiment.py +4 -0
  66. validmind/tests/data_validation/nlp/StopWords.py +62 -91
  67. validmind/tests/data_validation/nlp/TextDescription.py +116 -159
  68. validmind/tests/data_validation/nlp/Toxicity.py +12 -4
  69. validmind/tests/decorator.py +33 -242
  70. validmind/tests/load.py +212 -153
  71. validmind/tests/model_validation/BertScore.py +13 -7
  72. validmind/tests/model_validation/BleuScore.py +4 -0
  73. validmind/tests/model_validation/ClusterSizeDistribution.py +24 -47
  74. validmind/tests/model_validation/ContextualRecall.py +3 -0
  75. validmind/tests/model_validation/FeaturesAUC.py +43 -74
  76. validmind/tests/model_validation/MeteorScore.py +3 -0
  77. validmind/tests/model_validation/RegardScore.py +5 -1
  78. validmind/tests/model_validation/RegressionResidualsPlot.py +54 -75
  79. validmind/tests/model_validation/embeddings/ClusterDistribution.py +10 -33
  80. validmind/tests/model_validation/embeddings/CosineSimilarityDistribution.py +11 -29
  81. validmind/tests/model_validation/embeddings/DescriptiveAnalytics.py +19 -31
  82. validmind/tests/model_validation/embeddings/EmbeddingsVisualization2D.py +40 -49
  83. validmind/tests/model_validation/embeddings/StabilityAnalysisKeyword.py +29 -15
  84. validmind/tests/model_validation/embeddings/StabilityAnalysisRandomNoise.py +25 -11
  85. validmind/tests/model_validation/embeddings/StabilityAnalysisSynonyms.py +28 -13
  86. validmind/tests/model_validation/embeddings/StabilityAnalysisTranslation.py +67 -38
  87. validmind/tests/model_validation/embeddings/utils.py +53 -0
  88. validmind/tests/model_validation/ragas/AnswerCorrectness.py +37 -32
  89. validmind/tests/model_validation/ragas/{AspectCritique.py → AspectCritic.py} +33 -27
  90. validmind/tests/model_validation/ragas/ContextEntityRecall.py +44 -41
  91. validmind/tests/model_validation/ragas/ContextPrecision.py +40 -35
  92. validmind/tests/model_validation/ragas/ContextPrecisionWithoutReference.py +133 -0
  93. validmind/tests/model_validation/ragas/ContextRecall.py +40 -35
  94. validmind/tests/model_validation/ragas/Faithfulness.py +42 -30
  95. validmind/tests/model_validation/ragas/NoiseSensitivity.py +59 -35
  96. validmind/tests/model_validation/ragas/{AnswerRelevance.py → ResponseRelevancy.py} +52 -41
  97. validmind/tests/model_validation/ragas/{AnswerSimilarity.py → SemanticSimilarity.py} +39 -34
  98. validmind/tests/model_validation/sklearn/AdjustedMutualInformation.py +13 -16
  99. validmind/tests/model_validation/sklearn/AdjustedRandIndex.py +13 -16
  100. validmind/tests/model_validation/sklearn/ClassifierPerformance.py +51 -89
  101. validmind/tests/model_validation/sklearn/ClusterCosineSimilarity.py +31 -61
  102. validmind/tests/model_validation/sklearn/ClusterPerformanceMetrics.py +118 -83
  103. validmind/tests/model_validation/sklearn/CompletenessScore.py +13 -16
  104. validmind/tests/model_validation/sklearn/ConfusionMatrix.py +62 -94
  105. validmind/tests/model_validation/sklearn/FeatureImportance.py +7 -8
  106. validmind/tests/model_validation/sklearn/FowlkesMallowsScore.py +12 -15
  107. validmind/tests/model_validation/sklearn/HomogeneityScore.py +12 -15
  108. validmind/tests/model_validation/sklearn/HyperParametersTuning.py +23 -53
  109. validmind/tests/model_validation/sklearn/KMeansClustersOptimization.py +60 -74
  110. validmind/tests/model_validation/sklearn/MinimumAccuracy.py +16 -84
  111. validmind/tests/model_validation/sklearn/MinimumF1Score.py +22 -72
  112. validmind/tests/model_validation/sklearn/MinimumROCAUCScore.py +29 -78
  113. validmind/tests/model_validation/sklearn/ModelsPerformanceComparison.py +52 -82
  114. validmind/tests/model_validation/sklearn/OverfitDiagnosis.py +51 -145
  115. validmind/tests/model_validation/sklearn/PermutationFeatureImportance.py +60 -78
  116. validmind/tests/model_validation/sklearn/PopulationStabilityIndex.py +130 -172
  117. validmind/tests/model_validation/sklearn/PrecisionRecallCurve.py +26 -55
  118. validmind/tests/model_validation/sklearn/ROCCurve.py +43 -77
  119. validmind/tests/model_validation/sklearn/RegressionPerformance.py +41 -94
  120. validmind/tests/model_validation/sklearn/RobustnessDiagnosis.py +47 -136
  121. validmind/tests/model_validation/sklearn/SHAPGlobalImportance.py +164 -208
  122. validmind/tests/model_validation/sklearn/SilhouettePlot.py +54 -99
  123. validmind/tests/model_validation/sklearn/TrainingTestDegradation.py +50 -124
  124. validmind/tests/model_validation/sklearn/VMeasure.py +12 -15
  125. validmind/tests/model_validation/sklearn/WeakspotsDiagnosis.py +225 -281
  126. validmind/tests/model_validation/statsmodels/AutoARIMA.py +40 -45
  127. validmind/tests/model_validation/statsmodels/KolmogorovSmirnov.py +22 -47
  128. validmind/tests/model_validation/statsmodels/Lilliefors.py +17 -28
  129. validmind/tests/model_validation/statsmodels/RegressionFeatureSignificance.py +37 -81
  130. validmind/tests/model_validation/statsmodels/RegressionModelForecastPlot.py +37 -105
  131. validmind/tests/model_validation/statsmodels/RegressionModelForecastPlotLevels.py +62 -166
  132. validmind/tests/model_validation/statsmodels/RegressionModelSensitivityPlot.py +57 -119
  133. validmind/tests/model_validation/statsmodels/RegressionModelSummary.py +20 -57
  134. validmind/tests/model_validation/statsmodels/RegressionPermutationFeatureImportance.py +47 -80
  135. validmind/tests/ongoing_monitoring/PredictionCorrelation.py +2 -0
  136. validmind/tests/ongoing_monitoring/TargetPredictionDistributionPlot.py +4 -2
  137. validmind/tests/output.py +120 -0
  138. validmind/tests/prompt_validation/Bias.py +55 -98
  139. validmind/tests/prompt_validation/Clarity.py +56 -99
  140. validmind/tests/prompt_validation/Conciseness.py +63 -101
  141. validmind/tests/prompt_validation/Delimitation.py +48 -89
  142. validmind/tests/prompt_validation/NegativeInstruction.py +62 -96
  143. validmind/tests/prompt_validation/Robustness.py +80 -121
  144. validmind/tests/prompt_validation/Specificity.py +61 -95
  145. validmind/tests/prompt_validation/ai_powered_test.py +2 -2
  146. validmind/tests/run.py +314 -496
  147. validmind/tests/test_providers.py +109 -79
  148. validmind/tests/utils.py +91 -0
  149. validmind/unit_metrics/__init__.py +16 -155
  150. validmind/unit_metrics/classification/F1.py +1 -0
  151. validmind/unit_metrics/classification/Precision.py +1 -0
  152. validmind/unit_metrics/classification/ROC_AUC.py +1 -0
  153. validmind/unit_metrics/classification/Recall.py +1 -0
  154. validmind/unit_metrics/regression/AdjustedRSquaredScore.py +1 -0
  155. validmind/unit_metrics/regression/GiniCoefficient.py +1 -0
  156. validmind/unit_metrics/regression/HuberLoss.py +1 -0
  157. validmind/unit_metrics/regression/KolmogorovSmirnovStatistic.py +1 -0
  158. validmind/unit_metrics/regression/MeanAbsoluteError.py +1 -0
  159. validmind/unit_metrics/regression/MeanAbsolutePercentageError.py +1 -0
  160. validmind/unit_metrics/regression/MeanBiasDeviation.py +1 -0
  161. validmind/unit_metrics/regression/MeanSquaredError.py +1 -0
  162. validmind/unit_metrics/regression/QuantileLoss.py +1 -0
  163. validmind/unit_metrics/regression/RSquaredScore.py +2 -1
  164. validmind/unit_metrics/regression/RootMeanSquaredError.py +1 -0
  165. validmind/utils.py +66 -17
  166. validmind/vm_models/__init__.py +2 -17
  167. validmind/vm_models/dataset/dataset.py +31 -4
  168. validmind/vm_models/figure.py +7 -37
  169. validmind/vm_models/model.py +3 -0
  170. validmind/vm_models/result/__init__.py +7 -0
  171. validmind/vm_models/result/result.jinja +21 -0
  172. validmind/vm_models/result/result.py +337 -0
  173. validmind/vm_models/result/utils.py +160 -0
  174. validmind/vm_models/test_suite/runner.py +16 -54
  175. validmind/vm_models/test_suite/summary.py +3 -3
  176. validmind/vm_models/test_suite/test.py +43 -77
  177. validmind/vm_models/test_suite/test_suite.py +8 -40
  178. validmind-2.6.7.dist-info/METADATA +137 -0
  179. {validmind-2.5.24.dist-info → validmind-2.6.7.dist-info}/RECORD +182 -189
  180. validmind/tests/data_validation/AutoSeasonality.py +0 -190
  181. validmind/tests/metadata.py +0 -59
  182. validmind/tests/model_validation/embeddings/StabilityAnalysis.py +0 -176
  183. validmind/tests/model_validation/ragas/ContextUtilization.py +0 -161
  184. validmind/tests/model_validation/sklearn/ClusterPerformance.py +0 -80
  185. validmind/unit_metrics/composite.py +0 -238
  186. validmind/vm_models/test/metric.py +0 -98
  187. validmind/vm_models/test/metric_result.py +0 -61
  188. validmind/vm_models/test/output_template.py +0 -55
  189. validmind/vm_models/test/result_summary.py +0 -76
  190. validmind/vm_models/test/result_wrapper.py +0 -488
  191. validmind/vm_models/test/test.py +0 -103
  192. validmind/vm_models/test/threshold_test.py +0 -106
  193. validmind/vm_models/test/threshold_test_result.py +0 -75
  194. validmind/vm_models/test_context.py +0 -259
  195. validmind-2.5.24.dist-info/METADATA +0 -118
  196. {validmind-2.5.24.dist-info → validmind-2.6.7.dist-info}/LICENSE +0 -0
  197. {validmind-2.5.24.dist-info → validmind-2.6.7.dist-info}/WHEEL +0 -0
  198. {validmind-2.5.24.dist-info → validmind-2.6.7.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,362 @@
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 itertools import product
6
+ from typing import Any, Dict, List, Tuple, Union
7
+
8
+ import pandas as pd
9
+
10
+ from validmind.logging import get_logger
11
+ from validmind.utils import test_id_to_name
12
+ from validmind.vm_models.figure import (
13
+ is_matplotlib_figure,
14
+ is_plotly_figure,
15
+ is_png_image,
16
+ )
17
+ from validmind.vm_models.input import VMInput
18
+ from validmind.vm_models.result import ResultTable, TestResult
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ def _cartesian_product(grid: Dict[str, List[Any]]) -> List[Dict[str, Any]]:
24
+ """
25
+ Generate all possible combinations for a grid of inputs or parameters.
26
+
27
+ Args:
28
+ grid: A dictionary where each key corresponds to a parameter name and the associated list contains possible values.
29
+
30
+ Returns:
31
+ A list of dictionaries representing all combinations of the parameter values.
32
+
33
+ Example:
34
+ _cartesian_product({"a": [1, 2], "b": [3, 4]})
35
+ >>> [{'a': 1, 'b': 3}, {'a': 1, 'b': 4}, {'a': 2, 'b': 3}, {'a': 2, 'b': 4}]
36
+ """
37
+ if not grid:
38
+ return [{}]
39
+ return [dict(zip(grid.keys(), values)) for values in product(*grid.values())]
40
+
41
+
42
+ def _get_input_key(input_obj_or_list: Union[VMInput, List[VMInput]]) -> str:
43
+ """Create a key for a given input or list of inputs"""
44
+ if isinstance(input_obj_or_list, list):
45
+ return ",".join(item.input_id for item in input_obj_or_list)
46
+
47
+ return input_obj_or_list.input_id
48
+
49
+
50
+ def _get_unique_inputs(results: List[TestResult]) -> Dict[str, set]:
51
+ """Get only the inputs that are not the same across all results"""
52
+ unique_inputs = {}
53
+
54
+ for result in results:
55
+ if not result.inputs:
56
+ continue
57
+
58
+ for func_input_name, input_obj_or_list in result.inputs.items():
59
+ if isinstance(input_obj_or_list, list):
60
+ key = ",".join(_get_input_key(item) for item in input_obj_or_list)
61
+ else:
62
+ key = _get_input_key(input_obj_or_list)
63
+
64
+ unique_inputs.setdefault(func_input_name, set()).add(key)
65
+
66
+ return unique_inputs
67
+
68
+
69
+ def _get_unique_params(results: List[TestResult]) -> Dict[str, List[Any]]:
70
+ """Get only the params that are not the same across all results"""
71
+ param_values = {}
72
+ for result in results:
73
+ if not result.params:
74
+ continue
75
+
76
+ for name, value in result.params.items():
77
+ param_values.setdefault(name, []).append(value)
78
+
79
+ unique_params = {}
80
+ for name, values in param_values.items():
81
+ unique_values = []
82
+ for value in values:
83
+ if not any(value == x for x in unique_values):
84
+ unique_values.append(value)
85
+
86
+ unique_params[name] = unique_values
87
+
88
+ return unique_params
89
+
90
+
91
+ def _get_table_metadata(
92
+ result: TestResult, results: List[TestResult]
93
+ ) -> Dict[str, Any]:
94
+ """Create a metadata dict with unique inputs and params for a table row"""
95
+ metadata = {}
96
+ unique_inputs = _get_unique_inputs(results)
97
+ unique_params = _get_unique_params(results)
98
+
99
+ if result.inputs:
100
+ for input_name, input_obj in result.inputs.items():
101
+ if len(unique_inputs[input_name]) > 1:
102
+ metadata[input_name] = _get_input_key(input_obj)
103
+
104
+ if result.params:
105
+ for param_name, param_value in result.params.items():
106
+ if len(unique_params[param_name]) > 1:
107
+ metadata[param_name] = param_value
108
+
109
+ return metadata
110
+
111
+
112
+ def _combine_single_table(results: List[TestResult], table_index: int) -> pd.DataFrame:
113
+ """
114
+ Combine a single table across multiple test results.
115
+
116
+ Args:
117
+ results: A list of TestResult objects.
118
+ table_index: The index of the table to combine.
119
+
120
+ Returns:
121
+ A pandas DataFrame combining the tables with added metadata columns.
122
+ """
123
+ combined_tables = []
124
+
125
+ for result in results:
126
+ metadata = _get_table_metadata(result, results)
127
+ table_data = result.tables[table_index].data
128
+
129
+ if metadata:
130
+ metadata_df = pd.DataFrame([metadata] * len(table_data))
131
+ table_data = pd.concat([metadata_df, table_data], axis=1)
132
+
133
+ combined_tables.append(table_data)
134
+
135
+ return pd.concat(combined_tables, ignore_index=True)
136
+
137
+
138
+ def _combine_tables(results: List[TestResult]) -> List[pd.DataFrame]:
139
+ """Combine tables from multiple test results
140
+
141
+ # TODO: retain table titles
142
+ """
143
+ if not results[0].tables:
144
+ return []
145
+
146
+ return [_combine_single_table(results, i) for i in range(len(results[0].tables))]
147
+
148
+
149
+ def _build_input_param_string(result: TestResult, results: List[TestResult]) -> str:
150
+ """Build a string repr of unique inputs + params for a figure title"""
151
+ parts = []
152
+ unique_inputs = _get_unique_inputs(results)
153
+
154
+ # if theres only one unique value for an input or param, don't show it
155
+ # however, if there is only one unique value for all inputs then show it
156
+ if result.inputs:
157
+ should_show = all(
158
+ len(unique_inputs[input_name]) == 1 for input_name in unique_inputs
159
+ )
160
+ for input_name, input_obj in result.inputs.items():
161
+ if should_show or len(unique_inputs[input_name]) > 1:
162
+ input_val = _get_input_key(input_obj)
163
+ parts.append(f"{input_name}={input_val}")
164
+
165
+ # TODO: revisit this when we can create a value/title to show for params
166
+ # unique_params = _get_unique_params(results)
167
+ # # if theres only one unique value for a param, don't show it
168
+ # # however, if there is only one unique value for all params then show it as
169
+ # # long as there is no existing inputs in the parts list
170
+ # if result.params:
171
+ # should_show = (
172
+ # all(len(unique_params[param_name]) == 1 for param_name in unique_params)
173
+ # and not parts
174
+ # )
175
+ # for param_name, param_value in result.params.items():
176
+ # if should_show or len(unique_params[param_name]) > 1:
177
+ # parts.append(f"{param_name}={param_value}")
178
+
179
+ return ", ".join(parts)
180
+
181
+
182
+ def _update_figure_title(figure: Any, input_param_str: str) -> None:
183
+ """
184
+ Update the title of a figure with input and parameter information.
185
+
186
+ Args:
187
+ figure: A figure object (matplotlib, plotly, or PNG image).
188
+ input_param_str: A string of input and parameter information.
189
+
190
+ Raises:
191
+ ValueError: If the figure type is unsupported.
192
+ """
193
+ if not input_param_str:
194
+ return
195
+
196
+ new_title = f"{{curr_title}} (for {input_param_str})"
197
+
198
+ if is_matplotlib_figure(figure):
199
+ curr_title = figure._suptitle.get_text() if figure._suptitle else ""
200
+ figure.suptitle(new_title.format(curr_title=curr_title))
201
+ elif is_plotly_figure(figure):
202
+ curr_title = figure.layout.title.text
203
+ figure.layout.title.text = new_title.format(curr_title=curr_title)
204
+ elif is_png_image(figure):
205
+ logger.warning("Unable to update title for PNG image figure.")
206
+ else:
207
+ raise ValueError(f"Unsupported figure type: {type(figure)}")
208
+
209
+
210
+ def _combine_figures(results: List[TestResult]) -> List[Any]:
211
+ """Combine figures from multiple test results (gets raw figure objects, not vm Figures)"""
212
+ combined_figures = []
213
+
214
+ for result in results:
215
+ for figure in result.figures or []:
216
+ # update the figure object in-place with the new title
217
+ _update_figure_title(
218
+ figure=figure.figure,
219
+ input_param_str=_build_input_param_string(result, results),
220
+ )
221
+ combined_figures.append(figure)
222
+
223
+ return combined_figures
224
+
225
+
226
+ def _handle_metrics(results: List[TestResult]) -> List[Any]:
227
+ """Combine metrics from multiple test results"""
228
+ # add a table with the metric value so it is combined into a single table
229
+ for result in results:
230
+ if result.metric:
231
+ result.add_table(
232
+ ResultTable(
233
+ data=[
234
+ {
235
+ "Metric": test_id_to_name(result.result_id),
236
+ "Value": result.metric,
237
+ }
238
+ ],
239
+ title=None,
240
+ )
241
+ )
242
+
243
+
244
+ def _combine_dict_values(items_dict: Dict[str, Any]) -> Dict[str, Any]:
245
+ """Combine values for each key in a dictionary, keeping only unique values"""
246
+ combined = {}
247
+
248
+ for name, value in items_dict.items():
249
+ values = value if isinstance(value, list) else [value]
250
+
251
+ unique_values = []
252
+ for v in values:
253
+ if not any(v == x for x in unique_values):
254
+ unique_values.append(v)
255
+
256
+ combined[name] = unique_values[0] if len(unique_values) == 1 else unique_values
257
+
258
+ return combined
259
+
260
+
261
+ def get_comparison_test_configs(
262
+ input_grid: Union[Dict[str, List[Any]], List[Dict[str, Any]], None] = None,
263
+ param_grid: Union[Dict[str, List[Any]], List[Dict[str, Any]], None] = None,
264
+ inputs: Union[Dict[str, Union[VMInput, List[VMInput]]], None] = None,
265
+ params: Union[Dict[str, Any], None] = None,
266
+ ) -> List[Dict[str, Any]]:
267
+ """
268
+ Generate test configurations based on input and parameter grids.
269
+
270
+ Function inputs should be validated before calling this.
271
+
272
+ Args:
273
+ input_grid: A dictionary or list defining the grid of inputs.
274
+ param_grid: A dictionary or list defining the grid of parameters.
275
+ inputs: A dictionary of inputs.
276
+ params: A dictionary of parameters.
277
+
278
+ Returns:
279
+ A list of test configurations.
280
+ """
281
+
282
+ # Convert list of dicts to dict of lists if necessary
283
+ def list_to_dict(grid_list):
284
+ return {k: [d[k] for d in grid_list] for k in grid_list[0].keys()}
285
+
286
+ if isinstance(input_grid, list):
287
+ input_grid = list_to_dict(input_grid)
288
+
289
+ if isinstance(param_grid, list):
290
+ param_grid = list_to_dict(param_grid)
291
+
292
+ test_configs = []
293
+
294
+ if input_grid and param_grid:
295
+ input_combinations = _cartesian_product(input_grid)
296
+ param_combinations = _cartesian_product(param_grid)
297
+ test_configs = [
298
+ {"inputs": i, "params": p}
299
+ for i, p in product(input_combinations, param_combinations)
300
+ ]
301
+ elif input_grid:
302
+ input_combinations = _cartesian_product(input_grid)
303
+ test_configs = [
304
+ {"inputs": i, "params": params or {}} for i in input_combinations
305
+ ]
306
+ elif param_grid:
307
+ param_combinations = _cartesian_product(param_grid)
308
+ test_configs = [
309
+ {"inputs": inputs or {}, "params": p} for p in param_combinations
310
+ ]
311
+
312
+ return test_configs
313
+
314
+
315
+ def combine_results(
316
+ results: List[TestResult],
317
+ ) -> Tuple[List[Any], Dict[str, List[Any]], Dict[str, List[Any]]]:
318
+ """
319
+ Combine multiple test results into a single set of outputs.
320
+
321
+ Args:
322
+ results: A list of TestResult objects to combine.
323
+
324
+ Returns:
325
+ A tuple containing:
326
+ - A list of combined outputs (tables and figures).
327
+ - A dictionary of inputs with lists of all values.
328
+ - A dictionary of parameters with lists of all values.
329
+ """
330
+ # metrics are added as a table to each result so later they can be combined
331
+ _handle_metrics(results)
332
+
333
+ combined_outputs = []
334
+ # handle tables (if any)
335
+ combined_outputs.extend(_combine_tables(results))
336
+ # handle figures (if any)
337
+ combined_outputs.extend(_combine_figures(results))
338
+ # handle threshold tests (i.e. tests that have pass/fail bool status)
339
+ if results[0].passed is not None:
340
+ combined_outputs.append(all(result.passed for result in results))
341
+
342
+ # combine inputs and params
343
+ combined_inputs = {}
344
+ combined_params = {}
345
+
346
+ for result in results:
347
+ if result.inputs:
348
+ for input_name, input_obj_or_list in result.inputs.items():
349
+ combined_inputs.setdefault(input_name, []).extend(
350
+ input_obj_or_list
351
+ if isinstance(input_obj_or_list, list)
352
+ else [input_obj_or_list]
353
+ )
354
+
355
+ if result.params:
356
+ for param_name, param_value in result.params.items():
357
+ combined_params.setdefault(param_name, []).append(param_value)
358
+
359
+ combined_inputs = _combine_dict_values(combined_inputs)
360
+ combined_params = _combine_dict_values(combined_params)
361
+
362
+ return combined_outputs, combined_inputs, combined_params
@@ -6,10 +6,13 @@ import pandas as pd
6
6
  import plotly.graph_objects as go
7
7
  from statsmodels.tsa.stattools import acf, pacf
8
8
 
9
- from validmind.vm_models import Figure, Metric
9
+ from validmind import tags, tasks
10
+ from validmind.vm_models import VMDataset
10
11
 
11
12
 
12
- class ACFandPACFPlot(Metric):
13
+ @tags("time_series_data", "forecasting", "statistical_test", "visualization")
14
+ @tasks("regression")
15
+ def ACFandPACFPlot(dataset: VMDataset):
13
16
  """
14
17
  Analyzes time series data using Autocorrelation Function (ACF) and Partial Autocorrelation Function (PACF) plots to
15
18
  reveal trends and correlations.
@@ -49,74 +52,42 @@ class ACFandPACFPlot(Metric):
49
52
  - The plots can only represent linear correlations and fail to capture any non-linear relationships within the data.
50
53
  - The plots might be difficult for non-experts to interpret and should not replace more advanced analyses.
51
54
  """
52
-
53
- name = "acf_pacf_plot"
54
- required_inputs = ["dataset"]
55
- tasks = ["regression"]
56
- tags = [
57
- "time_series_data",
58
- "forecasting",
59
- "statistical_test",
60
- "visualization",
61
- ]
62
-
63
- def run(self):
64
- # Check if index is datetime
65
- if not pd.api.types.is_datetime64_any_dtype(self.inputs.dataset.df.index):
66
- raise ValueError("Index must be a datetime type")
67
-
68
- columns = list(self.inputs.dataset.df.columns)
69
-
70
- df = self.inputs.dataset.df.dropna()
71
-
72
- if not set(columns).issubset(set(df.columns)):
73
- raise ValueError("Provided 'columns' must exist in the dataset")
74
-
75
- figures = []
76
-
77
- for col in df.columns:
78
- series = df[col]
79
-
80
- # Calculate the maximum number of lags based on the size of the dataset
81
- max_lags = min(40, len(series) // 2 - 1)
82
-
83
- # Calculate ACF and PACF values
84
- acf_values = acf(series, nlags=max_lags)
85
- pacf_values = pacf(series, nlags=max_lags)
86
-
87
- # Create ACF plot using Plotly
88
- acf_fig = go.Figure()
89
- acf_fig.add_trace(go.Bar(x=list(range(len(acf_values))), y=acf_values))
90
- acf_fig.update_layout(
91
- title=f"ACF for {col}",
92
- xaxis_title="Lag",
93
- yaxis_title="ACF",
94
- font=dict(size=18),
95
- )
96
-
97
- # Create PACF plot using Plotly
98
- pacf_fig = go.Figure()
99
- pacf_fig.add_trace(go.Bar(x=list(range(len(pacf_values))), y=pacf_values))
100
- pacf_fig.update_layout(
101
- title=f"PACF for {col}",
102
- xaxis_title="Lag",
103
- yaxis_title="PACF",
104
- font=dict(size=18),
105
- )
106
-
107
- figures.append(
108
- Figure(
109
- for_object=self,
110
- key=f"{self.key}:{col}_acf",
111
- figure=acf_fig,
112
- )
113
- )
114
- figures.append(
115
- Figure(
116
- for_object=self,
117
- key=f"{self.key}:{col}_pacf",
118
- figure=pacf_fig,
119
- )
120
- )
121
-
122
- return self.cache_results(figures=figures)
55
+ if not pd.api.types.is_datetime64_any_dtype(dataset.df.index):
56
+ raise ValueError("Index must be a datetime type")
57
+
58
+ columns = list(dataset.df.columns)
59
+ df = dataset.df.dropna()
60
+
61
+ if not set(columns).issubset(set(df.columns)):
62
+ raise ValueError("Provided 'columns' must exist in the dataset")
63
+
64
+ figures = []
65
+ for col in df.columns:
66
+ series = df[col]
67
+ max_lags = min(40, len(series) // 2 - 1)
68
+
69
+ # Create ACF plot using Plotly
70
+ acf_values = acf(series, nlags=max_lags)
71
+ acf_fig = go.Figure()
72
+ acf_fig.add_trace(go.Bar(x=list(range(len(acf_values))), y=acf_values))
73
+ acf_fig.update_layout(
74
+ title=f"ACF for {col}",
75
+ xaxis_title="Lag",
76
+ yaxis_title="ACF",
77
+ font=dict(size=18),
78
+ )
79
+ figures.append(acf_fig)
80
+
81
+ # Create PACF plot using Plotly
82
+ pacf_values = pacf(series, nlags=max_lags)
83
+ pacf_fig = go.Figure()
84
+ pacf_fig.add_trace(go.Bar(x=list(range(len(pacf_values))), y=pacf_values))
85
+ pacf_fig.update_layout(
86
+ title=f"PACF for {col}",
87
+ xaxis_title="Lag",
88
+ yaxis_title="PACF",
89
+ font=dict(size=18),
90
+ )
91
+ figures.append(pacf_fig)
92
+
93
+ return tuple(figures)
@@ -2,19 +2,21 @@
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
- from dataclasses import dataclass
6
-
7
5
  import pandas as pd
8
6
  from statsmodels.tsa.stattools import adfuller
9
7
 
8
+ from validmind import tags, tasks
10
9
  from validmind.logging import get_logger
11
- from validmind.vm_models import Metric, ResultSummary, ResultTable, ResultTableMetadata
10
+ from validmind.vm_models import VMDataset
12
11
 
13
12
  logger = get_logger(__name__)
14
13
 
15
14
 
16
- @dataclass
17
- class ADF(Metric):
15
+ @tags(
16
+ "time_series_data", "statsmodels", "forecasting", "statistical_test", "stationarity"
17
+ )
18
+ @tasks("regression")
19
+ def ADF(dataset: VMDataset):
18
20
  """
19
21
  Assesses the stationarity of a time series dataset using the Augmented Dickey-Fuller (ADF) test.
20
22
 
@@ -51,84 +53,48 @@ class ADF(Metric):
51
53
  - It assumes the data follows an autoregressive process, which might not always be the case.
52
54
  - The test struggles with time series data that have structural breaks.
53
55
  """
56
+ df = dataset.df.dropna()
54
57
 
55
- name = "adf"
56
- required_inputs = ["dataset"]
57
- tasks = ["regression"]
58
- tags = [
59
- "time_series_data",
60
- "statsmodels",
61
- "forecasting",
62
- "statistical_test",
63
- "stationarity",
64
- ]
65
-
66
- def summary(self, metric_value: dict):
67
- table = pd.DataFrame.from_dict(metric_value, orient="index")
68
- table = table.reset_index()
69
- table.columns = [
70
- "Feature",
71
- "ADF Statistic",
72
- "P-Value",
73
- "Used Lag",
74
- "Number of Observations",
75
- "Critical Values",
76
- "IC Best",
77
- ]
78
- table = table.rename_axis("Index", axis=1)
79
-
80
- return ResultSummary(
81
- results=[
82
- ResultTable(
83
- data=table,
84
- metadata=ResultTableMetadata(
85
- title="ADF Test Results for Each Feature"
86
- ),
87
- ),
88
- ]
58
+ if not isinstance(df.index, (pd.DatetimeIndex, pd.PeriodIndex)):
59
+ raise ValueError(
60
+ "Dataset index must be a datetime or period index for time series analysis."
89
61
  )
90
62
 
91
- def run(self):
92
- """
93
- Calculates ADF metric for each of the dataset features
94
- """
95
- dataset = self.inputs.dataset.df
96
-
97
- # Check if the dataset is a time series
98
- if not isinstance(dataset.index, (pd.DatetimeIndex, pd.PeriodIndex)):
99
- raise ValueError(
100
- "Dataset index must be a datetime or period index for time series analysis."
101
- )
102
-
103
- # Preprocessing: Drop rows with any NaN values
104
- if dataset.isnull().values.any():
105
- logger.warning(
106
- "Dataset contains missing values. Rows with NaNs will be dropped."
107
- )
108
- dataset = dataset.dropna()
109
-
110
- adf_values = {}
111
- for col in dataset.columns:
112
- try:
113
- adf_result = adfuller(dataset[col].values)
114
- adf_values[col] = {
115
- "ADF Statistic": adf_result[0],
116
- "P-Value": adf_result[1],
117
- "Used Lag": adf_result[2],
118
- "Number of Observations": adf_result[3],
119
- "Critical Values": adf_result[4],
120
- "IC Best": adf_result[5],
121
- }
122
- except Exception as e:
123
- logger.error(f"Error processing column '{col}': {e}")
124
- adf_values[col] = {
125
- "ADF Statistic": None,
126
- "P-Value": None,
127
- "Used Lag": None,
128
- "Number of Observations": None,
129
- "Critical Values": None,
130
- "IC Best": None,
131
- "Error": str(e),
132
- }
133
-
134
- return self.cache_results(adf_values)
63
+ adf_values = {}
64
+ for col in df.columns:
65
+ try:
66
+ adf_result = adfuller(df[col].values)
67
+ adf_values[col] = {
68
+ "ADF Statistic": adf_result[0],
69
+ "P-Value": adf_result[1],
70
+ "Used Lag": adf_result[2],
71
+ "Number of Observations": adf_result[3],
72
+ "Critical Values": adf_result[4],
73
+ "IC Best": adf_result[5],
74
+ }
75
+ except Exception as e:
76
+ logger.error(f"Error processing column '{col}': {e}")
77
+ adf_values[col] = {
78
+ "ADF Statistic": None,
79
+ "P-Value": None,
80
+ "Used Lag": None,
81
+ "Number of Observations": None,
82
+ "Critical Values": None,
83
+ "IC Best": None,
84
+ "Error": str(e),
85
+ }
86
+
87
+ table = pd.DataFrame.from_dict(adf_values, orient="index")
88
+ table = table.reset_index()
89
+ table.columns = [
90
+ "Feature",
91
+ "ADF Statistic",
92
+ "P-Value",
93
+ "Used Lag",
94
+ "Number of Observations",
95
+ "Critical Values",
96
+ "IC Best",
97
+ ]
98
+ table = table.rename_axis("Index", axis=1)
99
+
100
+ return {"ADF Test Results for Each Feature": table}