adamops 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. adamops/__init__.py +40 -0
  2. adamops/cli.py +163 -0
  3. adamops/data/__init__.py +24 -0
  4. adamops/data/feature_engineering.py +284 -0
  5. adamops/data/loaders.py +922 -0
  6. adamops/data/preprocessors.py +227 -0
  7. adamops/data/splitters.py +218 -0
  8. adamops/data/validators.py +148 -0
  9. adamops/deployment/__init__.py +21 -0
  10. adamops/deployment/api.py +237 -0
  11. adamops/deployment/cloud.py +191 -0
  12. adamops/deployment/containerize.py +262 -0
  13. adamops/deployment/exporters.py +148 -0
  14. adamops/evaluation/__init__.py +24 -0
  15. adamops/evaluation/comparison.py +133 -0
  16. adamops/evaluation/explainability.py +143 -0
  17. adamops/evaluation/metrics.py +233 -0
  18. adamops/evaluation/reports.py +165 -0
  19. adamops/evaluation/visualization.py +238 -0
  20. adamops/models/__init__.py +21 -0
  21. adamops/models/automl.py +277 -0
  22. adamops/models/ensembles.py +228 -0
  23. adamops/models/modelops.py +308 -0
  24. adamops/models/registry.py +250 -0
  25. adamops/monitoring/__init__.py +21 -0
  26. adamops/monitoring/alerts.py +200 -0
  27. adamops/monitoring/dashboard.py +117 -0
  28. adamops/monitoring/drift.py +212 -0
  29. adamops/monitoring/performance.py +195 -0
  30. adamops/pipelines/__init__.py +15 -0
  31. adamops/pipelines/orchestrators.py +183 -0
  32. adamops/pipelines/workflows.py +212 -0
  33. adamops/utils/__init__.py +18 -0
  34. adamops/utils/config.py +457 -0
  35. adamops/utils/helpers.py +663 -0
  36. adamops/utils/logging.py +412 -0
  37. adamops-0.1.0.dist-info/METADATA +310 -0
  38. adamops-0.1.0.dist-info/RECORD +42 -0
  39. adamops-0.1.0.dist-info/WHEEL +5 -0
  40. adamops-0.1.0.dist-info/entry_points.txt +2 -0
  41. adamops-0.1.0.dist-info/licenses/LICENSE +21 -0
  42. adamops-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,148 @@
1
+ """
2
+ AdamOps Model Exporters Module
3
+
4
+ Export models to ONNX, PMML, and other formats.
5
+ """
6
+
7
+ from typing import Any, Dict, List, Optional, Tuple
8
+ from pathlib import Path
9
+ import joblib
10
+ import pickle
11
+
12
+ from adamops.utils.logging import get_logger
13
+ from adamops.utils.helpers import ensure_dir
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ def export_pickle(model: Any, filepath: str) -> str:
19
+ """Export model as pickle file."""
20
+ filepath = Path(filepath)
21
+ ensure_dir(filepath.parent)
22
+
23
+ with open(filepath, 'wb') as f:
24
+ pickle.dump(model, f)
25
+
26
+ logger.info(f"Exported model to {filepath}")
27
+ return str(filepath)
28
+
29
+
30
+ def export_joblib(model: Any, filepath: str, compress: int = 3) -> str:
31
+ """Export model using joblib (better for large arrays)."""
32
+ filepath = Path(filepath)
33
+ ensure_dir(filepath.parent)
34
+
35
+ joblib.dump(model, filepath, compress=compress)
36
+ logger.info(f"Exported model to {filepath}")
37
+ return str(filepath)
38
+
39
+
40
+ def export_onnx(
41
+ model: Any, filepath: str,
42
+ initial_types: Optional[List[Tuple[str, Any]]] = None,
43
+ n_features: Optional[int] = None
44
+ ) -> str:
45
+ """
46
+ Export sklearn model to ONNX format.
47
+
48
+ Args:
49
+ model: Sklearn model.
50
+ filepath: Output path.
51
+ initial_types: Input type specification.
52
+ n_features: Number of input features.
53
+ """
54
+ try:
55
+ import onnx
56
+ from skl2onnx import convert_sklearn
57
+ from skl2onnx.common.data_types import FloatTensorType
58
+ except ImportError:
59
+ raise ImportError("onnx and skl2onnx required. Install with: pip install onnx skl2onnx")
60
+
61
+ filepath = Path(filepath)
62
+ ensure_dir(filepath.parent)
63
+
64
+ if initial_types is None:
65
+ if n_features is None:
66
+ raise ValueError("Either initial_types or n_features must be provided")
67
+ initial_types = [('input', FloatTensorType([None, n_features]))]
68
+
69
+ onnx_model = convert_sklearn(model, initial_types=initial_types)
70
+
71
+ with open(filepath, 'wb') as f:
72
+ f.write(onnx_model.SerializeToString())
73
+
74
+ logger.info(f"Exported ONNX model to {filepath}")
75
+ return str(filepath)
76
+
77
+
78
+ def export_pmml(model: Any, filepath: str, feature_names: Optional[List[str]] = None) -> str:
79
+ """Export model to PMML format."""
80
+ try:
81
+ from sklearn2pmml import sklearn2pmml
82
+ from sklearn2pmml.pipeline import PMMLPipeline
83
+ except ImportError:
84
+ raise ImportError("sklearn2pmml required. Install with: pip install sklearn2pmml")
85
+
86
+ filepath = Path(filepath)
87
+ ensure_dir(filepath.parent)
88
+
89
+ pipeline = PMMLPipeline([("model", model)])
90
+ sklearn2pmml(pipeline, str(filepath))
91
+
92
+ logger.info(f"Exported PMML model to {filepath}")
93
+ return str(filepath)
94
+
95
+
96
+ def load_model(filepath: str, format: str = "auto") -> Any:
97
+ """
98
+ Load a saved model.
99
+
100
+ Args:
101
+ filepath: Model file path.
102
+ format: 'pickle', 'joblib', 'onnx', or 'auto'.
103
+ """
104
+ filepath = Path(filepath)
105
+
106
+ if format == "auto":
107
+ suffix = filepath.suffix.lower()
108
+ if suffix in ['.pkl', '.pickle']:
109
+ format = "pickle"
110
+ elif suffix in ['.joblib']:
111
+ format = "joblib"
112
+ elif suffix == '.onnx':
113
+ format = "onnx"
114
+ else:
115
+ format = "joblib" # Default
116
+
117
+ if format == "pickle":
118
+ with open(filepath, 'rb') as f:
119
+ return pickle.load(f)
120
+ elif format == "joblib":
121
+ return joblib.load(filepath)
122
+ elif format == "onnx":
123
+ import onnxruntime as ort
124
+ return ort.InferenceSession(str(filepath))
125
+ else:
126
+ raise ValueError(f"Unknown format: {format}")
127
+
128
+
129
+ def export(model: Any, filepath: str, format: str = "joblib", **kwargs) -> str:
130
+ """
131
+ Export model to specified format.
132
+
133
+ Args:
134
+ model: Model to export.
135
+ filepath: Output path.
136
+ format: 'pickle', 'joblib', 'onnx', 'pmml'.
137
+ """
138
+ exporters = {
139
+ "pickle": export_pickle,
140
+ "joblib": export_joblib,
141
+ "onnx": export_onnx,
142
+ "pmml": export_pmml,
143
+ }
144
+
145
+ if format not in exporters:
146
+ raise ValueError(f"Unknown format: {format}. Available: {list(exporters.keys())}")
147
+
148
+ return exporters[format](model, filepath, **kwargs)
@@ -0,0 +1,24 @@
1
+ """
2
+ AdamOps Evaluation Module
3
+
4
+ Provides model evaluation capabilities:
5
+ - metrics: Compute classification, regression, and clustering metrics
6
+ - visualization: Plot confusion matrices, ROC curves, feature importance
7
+ - explainability: SHAP and LIME explanations
8
+ - comparison: Compare multiple models
9
+ - reports: Generate HTML/PDF reports
10
+ """
11
+
12
+ from adamops.evaluation import metrics
13
+ from adamops.evaluation import visualization
14
+ from adamops.evaluation import explainability
15
+ from adamops.evaluation import comparison
16
+ from adamops.evaluation import reports
17
+
18
+ __all__ = [
19
+ "metrics",
20
+ "visualization",
21
+ "explainability",
22
+ "comparison",
23
+ "reports",
24
+ ]
@@ -0,0 +1,133 @@
1
+ """
2
+ AdamOps Model Comparison Module
3
+
4
+ Provides tools to compare multiple models.
5
+ """
6
+
7
+ from typing import Any, Dict, List, Optional
8
+ import numpy as np
9
+ import pandas as pd
10
+ from sklearn.model_selection import cross_val_score
11
+
12
+ from adamops.utils.logging import get_logger
13
+ from adamops.evaluation.metrics import evaluate
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ def compare_models(
19
+ models: Dict[str, Any], X_test: np.ndarray, y_test: np.ndarray,
20
+ task: str = "classification"
21
+ ) -> pd.DataFrame:
22
+ """
23
+ Compare multiple models on test data.
24
+
25
+ Args:
26
+ models: Dict mapping model names to fitted models.
27
+ X_test: Test features.
28
+ y_test: Test labels.
29
+ task: 'classification' or 'regression'.
30
+
31
+ Returns:
32
+ DataFrame with comparison results.
33
+ """
34
+ results = []
35
+
36
+ for name, model in models.items():
37
+ y_pred = model.predict(X_test)
38
+ y_prob = None
39
+
40
+ if hasattr(model, 'predict_proba') and task == "classification":
41
+ try:
42
+ y_prob = model.predict_proba(X_test)
43
+ except:
44
+ pass
45
+
46
+ metrics = evaluate(y_test, y_pred, task, y_prob)
47
+ metrics['model'] = name
48
+ results.append(metrics)
49
+
50
+ df = pd.DataFrame(results)
51
+ cols = ['model'] + [c for c in df.columns if c != 'model']
52
+ return df[cols]
53
+
54
+
55
+ def compare_cv(
56
+ models: Dict[str, Any], X: np.ndarray, y: np.ndarray,
57
+ cv: int = 5, scoring: Optional[str] = None
58
+ ) -> pd.DataFrame:
59
+ """
60
+ Compare models using cross-validation.
61
+
62
+ Returns:
63
+ DataFrame with CV scores for each model.
64
+ """
65
+ results = []
66
+
67
+ for name, model in models.items():
68
+ scores = cross_val_score(model, X, y, cv=cv, scoring=scoring)
69
+ results.append({
70
+ 'model': name,
71
+ 'cv_mean': scores.mean(),
72
+ 'cv_std': scores.std(),
73
+ 'cv_min': scores.min(),
74
+ 'cv_max': scores.max(),
75
+ })
76
+
77
+ return pd.DataFrame(results).sort_values('cv_mean', ascending=False)
78
+
79
+
80
+ def rank_models(comparison_df: pd.DataFrame, metrics: List[str],
81
+ ascending: Optional[List[bool]] = None) -> pd.DataFrame:
82
+ """
83
+ Rank models by multiple metrics.
84
+
85
+ Args:
86
+ comparison_df: Comparison DataFrame.
87
+ metrics: Metrics to rank by.
88
+ ascending: Whether each metric should be ascending (lower is better).
89
+
90
+ Returns:
91
+ DataFrame with rankings.
92
+ """
93
+ df = comparison_df.copy()
94
+
95
+ if ascending is None:
96
+ ascending = [False] * len(metrics)
97
+
98
+ for metric, asc in zip(metrics, ascending):
99
+ df[f'{metric}_rank'] = df[metric].rank(ascending=asc)
100
+
101
+ rank_cols = [f'{m}_rank' for m in metrics]
102
+ df['avg_rank'] = df[rank_cols].mean(axis=1)
103
+
104
+ return df.sort_values('avg_rank')
105
+
106
+
107
+ def statistical_test(
108
+ scores_a: np.ndarray, scores_b: np.ndarray,
109
+ test: str = "wilcoxon"
110
+ ) -> Dict[str, float]:
111
+ """
112
+ Perform statistical test between two sets of scores.
113
+
114
+ Args:
115
+ scores_a: Scores for model A.
116
+ scores_b: Scores for model B.
117
+ test: 'wilcoxon', 'ttest', or 'mannwhitney'.
118
+
119
+ Returns:
120
+ Dict with statistic and p-value.
121
+ """
122
+ from scipy import stats
123
+
124
+ if test == "wilcoxon":
125
+ stat, pval = stats.wilcoxon(scores_a, scores_b)
126
+ elif test == "ttest":
127
+ stat, pval = stats.ttest_rel(scores_a, scores_b)
128
+ elif test == "mannwhitney":
129
+ stat, pval = stats.mannwhitneyu(scores_a, scores_b)
130
+ else:
131
+ raise ValueError(f"Unknown test: {test}")
132
+
133
+ return {"statistic": stat, "p_value": pval, "significant": pval < 0.05}
@@ -0,0 +1,143 @@
1
+ """
2
+ AdamOps Explainability Module
3
+
4
+ Provides SHAP and LIME model explanations.
5
+ """
6
+
7
+ from typing import Any, Dict, List, Optional, Union
8
+ import numpy as np
9
+ import pandas as pd
10
+ from adamops.utils.logging import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+ try:
15
+ import shap
16
+ SHAP_AVAILABLE = True
17
+ except ImportError:
18
+ SHAP_AVAILABLE = False
19
+
20
+ try:
21
+ import lime
22
+ import lime.lime_tabular
23
+ LIME_AVAILABLE = True
24
+ except ImportError:
25
+ LIME_AVAILABLE = False
26
+
27
+
28
+ class ShapExplainer:
29
+ """SHAP-based model explainer."""
30
+
31
+ def __init__(self, model: Any, X: Union[pd.DataFrame, np.ndarray],
32
+ feature_names: Optional[List[str]] = None):
33
+ if not SHAP_AVAILABLE:
34
+ raise ImportError("SHAP required. Install with: pip install shap")
35
+
36
+ self.model = model
37
+ self.X = X
38
+ self.feature_names = feature_names or (
39
+ X.columns.tolist() if isinstance(X, pd.DataFrame) else None
40
+ )
41
+
42
+ # Auto-select explainer type
43
+ model_type = type(model).__name__.lower()
44
+ if 'tree' in model_type or 'forest' in model_type or 'xgb' in model_type or 'lgb' in model_type:
45
+ self.explainer = shap.TreeExplainer(model)
46
+ else:
47
+ background = shap.sample(X, min(100, len(X)))
48
+ self.explainer = shap.KernelExplainer(model.predict, background)
49
+
50
+ def explain(self, X: Optional[np.ndarray] = None) -> Any:
51
+ """Generate SHAP values."""
52
+ X = X if X is not None else self.X[:100]
53
+ return self.explainer(X)
54
+
55
+ def plot_summary(self, X: Optional[np.ndarray] = None, max_display: int = 20):
56
+ """Plot SHAP summary."""
57
+ shap_values = self.explain(X)
58
+ shap.summary_plot(shap_values, feature_names=self.feature_names, max_display=max_display)
59
+
60
+ def plot_waterfall(self, idx: int = 0, X: Optional[np.ndarray] = None):
61
+ """Plot waterfall for single prediction."""
62
+ shap_values = self.explain(X)
63
+ shap.waterfall_plot(shap_values[idx])
64
+
65
+ def plot_force(self, idx: int = 0, X: Optional[np.ndarray] = None):
66
+ """Plot force plot for single prediction."""
67
+ shap_values = self.explain(X)
68
+ shap.force_plot(shap_values[idx])
69
+
70
+ def get_feature_importance(self, X: Optional[np.ndarray] = None) -> pd.DataFrame:
71
+ """Get feature importance from SHAP values."""
72
+ shap_values = self.explain(X)
73
+ importance = np.abs(shap_values.values).mean(axis=0)
74
+
75
+ return pd.DataFrame({
76
+ 'feature': self.feature_names or [f'f{i}' for i in range(len(importance))],
77
+ 'importance': importance
78
+ }).sort_values('importance', ascending=False)
79
+
80
+
81
+ class LimeExplainer:
82
+ """LIME-based model explainer."""
83
+
84
+ def __init__(self, model: Any, X_train: Union[pd.DataFrame, np.ndarray],
85
+ feature_names: Optional[List[str]] = None,
86
+ mode: str = "classification"):
87
+ if not LIME_AVAILABLE:
88
+ raise ImportError("LIME required. Install with: pip install lime")
89
+
90
+ self.model = model
91
+ self.mode = mode
92
+
93
+ if isinstance(X_train, pd.DataFrame):
94
+ feature_names = feature_names or X_train.columns.tolist()
95
+ X_train = X_train.values
96
+
97
+ self.explainer = lime.lime_tabular.LimeTabularExplainer(
98
+ X_train, feature_names=feature_names, mode=mode, discretize_continuous=True
99
+ )
100
+
101
+ def explain(self, instance: np.ndarray, num_features: int = 10):
102
+ """Explain a single prediction."""
103
+ if self.mode == "classification":
104
+ return self.explainer.explain_instance(
105
+ instance, self.model.predict_proba, num_features=num_features
106
+ )
107
+ else:
108
+ return self.explainer.explain_instance(
109
+ instance, self.model.predict, num_features=num_features
110
+ )
111
+
112
+ def explain_multiple(self, X: np.ndarray, num_features: int = 10) -> List:
113
+ """Explain multiple instances."""
114
+ return [self.explain(x, num_features) for x in X]
115
+
116
+
117
+ def explain_shap(model: Any, X: Union[pd.DataFrame, np.ndarray], **kwargs) -> ShapExplainer:
118
+ """Create SHAP explainer."""
119
+ return ShapExplainer(model, X, **kwargs)
120
+
121
+
122
+ def explain_lime(model: Any, X_train: Union[pd.DataFrame, np.ndarray], **kwargs) -> LimeExplainer:
123
+ """Create LIME explainer."""
124
+ return LimeExplainer(model, X_train, **kwargs)
125
+
126
+
127
+ def get_feature_importance(model: Any, feature_names: Optional[List[str]] = None) -> pd.DataFrame:
128
+ """Get feature importance from model."""
129
+ importance = None
130
+
131
+ if hasattr(model, 'feature_importances_'):
132
+ importance = model.feature_importances_
133
+ elif hasattr(model, 'coef_'):
134
+ importance = np.abs(model.coef_).flatten()
135
+
136
+ if importance is None:
137
+ raise ValueError("Model does not have feature_importances_ or coef_")
138
+
139
+ names = feature_names or [f'feature_{i}' for i in range(len(importance))]
140
+
141
+ return pd.DataFrame({
142
+ 'feature': names, 'importance': importance
143
+ }).sort_values('importance', ascending=False)
@@ -0,0 +1,233 @@
1
+ """
2
+ AdamOps Evaluation Metrics Module
3
+
4
+ Provides classification, regression, and clustering metrics.
5
+ """
6
+
7
+ from typing import Any, Dict, List, Optional, Union
8
+ import numpy as np
9
+ import pandas as pd
10
+ from sklearn import metrics as sklearn_metrics
11
+
12
+ from adamops.utils.logging import get_logger
13
+ from adamops.utils.helpers import infer_task_type
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ # =============================================================================
19
+ # Classification Metrics
20
+ # =============================================================================
21
+
22
+ def classification_metrics(
23
+ y_true: np.ndarray, y_pred: np.ndarray,
24
+ y_prob: Optional[np.ndarray] = None, average: str = "weighted"
25
+ ) -> Dict[str, float]:
26
+ """
27
+ Compute classification metrics.
28
+
29
+ Args:
30
+ y_true: True labels.
31
+ y_pred: Predicted labels.
32
+ y_prob: Probability predictions (for ROC-AUC).
33
+ average: Averaging method for multiclass.
34
+
35
+ Returns:
36
+ Dict with accuracy, precision, recall, f1, etc.
37
+ """
38
+ results = {
39
+ "accuracy": sklearn_metrics.accuracy_score(y_true, y_pred),
40
+ "precision": sklearn_metrics.precision_score(y_true, y_pred, average=average, zero_division=0),
41
+ "recall": sklearn_metrics.recall_score(y_true, y_pred, average=average, zero_division=0),
42
+ "f1": sklearn_metrics.f1_score(y_true, y_pred, average=average, zero_division=0),
43
+ }
44
+
45
+ # Binary classification specific
46
+ unique_classes = np.unique(y_true)
47
+ if len(unique_classes) == 2:
48
+ results["mcc"] = sklearn_metrics.matthews_corrcoef(y_true, y_pred)
49
+
50
+ if y_prob is not None:
51
+ if y_prob.ndim == 2:
52
+ y_prob = y_prob[:, 1]
53
+ results["roc_auc"] = sklearn_metrics.roc_auc_score(y_true, y_prob)
54
+ results["pr_auc"] = sklearn_metrics.average_precision_score(y_true, y_prob)
55
+
56
+ # Multiclass with probabilities
57
+ elif y_prob is not None and len(unique_classes) > 2:
58
+ try:
59
+ results["roc_auc_ovr"] = sklearn_metrics.roc_auc_score(
60
+ y_true, y_prob, multi_class="ovr", average=average
61
+ )
62
+ except ValueError:
63
+ pass
64
+
65
+ return results
66
+
67
+
68
+ def confusion_matrix(
69
+ y_true: np.ndarray, y_pred: np.ndarray, labels: Optional[List] = None
70
+ ) -> np.ndarray:
71
+ """Compute confusion matrix."""
72
+ return sklearn_metrics.confusion_matrix(y_true, y_pred, labels=labels)
73
+
74
+
75
+ def classification_report(
76
+ y_true: np.ndarray, y_pred: np.ndarray,
77
+ target_names: Optional[List[str]] = None, output_dict: bool = True
78
+ ) -> Union[str, Dict]:
79
+ """Generate classification report."""
80
+ return sklearn_metrics.classification_report(
81
+ y_true, y_pred, target_names=target_names, output_dict=output_dict
82
+ )
83
+
84
+
85
+ # =============================================================================
86
+ # Regression Metrics
87
+ # =============================================================================
88
+
89
+ def regression_metrics(y_true: np.ndarray, y_pred: np.ndarray) -> Dict[str, float]:
90
+ """
91
+ Compute regression metrics.
92
+
93
+ Returns:
94
+ Dict with mse, rmse, mae, r2, mape, etc.
95
+ """
96
+ mse = sklearn_metrics.mean_squared_error(y_true, y_pred)
97
+
98
+ results = {
99
+ "mse": mse,
100
+ "rmse": np.sqrt(mse),
101
+ "mae": sklearn_metrics.mean_absolute_error(y_true, y_pred),
102
+ "r2": sklearn_metrics.r2_score(y_true, y_pred),
103
+ "explained_variance": sklearn_metrics.explained_variance_score(y_true, y_pred),
104
+ }
105
+
106
+ # MAPE (avoid division by zero)
107
+ mask = y_true != 0
108
+ if mask.any():
109
+ mape = np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100
110
+ results["mape"] = mape
111
+
112
+ # SMAPE
113
+ denom = np.abs(y_true) + np.abs(y_pred)
114
+ mask = denom != 0
115
+ if mask.any():
116
+ smape = np.mean(2 * np.abs(y_true[mask] - y_pred[mask]) / denom[mask]) * 100
117
+ results["smape"] = smape
118
+
119
+ # Adjusted R2
120
+ n = len(y_true)
121
+ p = 1 # Assumes 1 feature, should be passed in for accuracy
122
+ if n > p + 1:
123
+ results["adjusted_r2"] = 1 - (1 - results["r2"]) * (n - 1) / (n - p - 1)
124
+
125
+ return results
126
+
127
+
128
+ # =============================================================================
129
+ # Clustering Metrics
130
+ # =============================================================================
131
+
132
+ def clustering_metrics(
133
+ X: np.ndarray, labels: np.ndarray,
134
+ y_true: Optional[np.ndarray] = None
135
+ ) -> Dict[str, float]:
136
+ """
137
+ Compute clustering metrics.
138
+
139
+ Args:
140
+ X: Feature data.
141
+ labels: Cluster labels.
142
+ y_true: True labels (if available).
143
+
144
+ Returns:
145
+ Dict with silhouette, davies_bouldin, calinski_harabasz, etc.
146
+ """
147
+ results = {}
148
+
149
+ # Internal metrics (don't need ground truth)
150
+ n_labels = len(np.unique(labels))
151
+ if n_labels > 1 and n_labels < len(X):
152
+ results["silhouette"] = sklearn_metrics.silhouette_score(X, labels)
153
+ results["davies_bouldin"] = sklearn_metrics.davies_bouldin_score(X, labels)
154
+ results["calinski_harabasz"] = sklearn_metrics.calinski_harabasz_score(X, labels)
155
+
156
+ # External metrics (need ground truth)
157
+ if y_true is not None:
158
+ results["adjusted_rand"] = sklearn_metrics.adjusted_rand_score(y_true, labels)
159
+ results["normalized_mutual_info"] = sklearn_metrics.normalized_mutual_info_score(y_true, labels)
160
+ results["homogeneity"] = sklearn_metrics.homogeneity_score(y_true, labels)
161
+ results["completeness"] = sklearn_metrics.completeness_score(y_true, labels)
162
+ results["v_measure"] = sklearn_metrics.v_measure_score(y_true, labels)
163
+
164
+ return results
165
+
166
+
167
+ # =============================================================================
168
+ # Unified Evaluation Interface
169
+ # =============================================================================
170
+
171
+ def evaluate(
172
+ y_true: np.ndarray, y_pred: np.ndarray,
173
+ task: str = "auto", y_prob: Optional[np.ndarray] = None,
174
+ X: Optional[np.ndarray] = None
175
+ ) -> Dict[str, float]:
176
+ """
177
+ Unified evaluation function.
178
+
179
+ Args:
180
+ y_true: True values/labels.
181
+ y_pred: Predicted values/labels.
182
+ task: 'classification', 'regression', 'clustering', or 'auto'.
183
+ y_prob: Probability predictions.
184
+ X: Feature data (for clustering).
185
+
186
+ Returns:
187
+ Dict with relevant metrics.
188
+ """
189
+ if task == "auto":
190
+ task = infer_task_type(y_true)
191
+
192
+ if task in ["classification", "multiclass"]:
193
+ return classification_metrics(y_true, y_pred, y_prob)
194
+ elif task == "regression":
195
+ return regression_metrics(y_true, y_pred)
196
+ elif task == "clustering":
197
+ if X is None:
198
+ raise ValueError("X required for clustering metrics")
199
+ return clustering_metrics(X, y_pred, y_true)
200
+ else:
201
+ raise ValueError(f"Unknown task: {task}")
202
+
203
+
204
+ def evaluate_model(
205
+ model: Any, X_test: np.ndarray, y_test: np.ndarray,
206
+ task: str = "auto"
207
+ ) -> Dict[str, float]:
208
+ """Evaluate a model on test data."""
209
+ y_pred = model.predict(X_test)
210
+ y_prob = None
211
+
212
+ if hasattr(model, "predict_proba"):
213
+ try:
214
+ y_prob = model.predict_proba(X_test)
215
+ except:
216
+ pass
217
+
218
+ return evaluate(y_test, y_pred, task, y_prob)
219
+
220
+
221
+ def results_to_dataframe(results: Dict[str, float]) -> pd.DataFrame:
222
+ """Convert results dict to DataFrame."""
223
+ return pd.DataFrame([results]).T.reset_index().rename(
224
+ columns={"index": "metric", 0: "value"}
225
+ )
226
+
227
+
228
+ def compare_results(
229
+ results_list: List[Dict[str, float]], names: List[str]
230
+ ) -> pd.DataFrame:
231
+ """Compare multiple evaluation results."""
232
+ data = {name: results for name, results in zip(names, results_list)}
233
+ return pd.DataFrame(data).T