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.
- adamops/__init__.py +40 -0
- adamops/cli.py +163 -0
- adamops/data/__init__.py +24 -0
- adamops/data/feature_engineering.py +284 -0
- adamops/data/loaders.py +922 -0
- adamops/data/preprocessors.py +227 -0
- adamops/data/splitters.py +218 -0
- adamops/data/validators.py +148 -0
- adamops/deployment/__init__.py +21 -0
- adamops/deployment/api.py +237 -0
- adamops/deployment/cloud.py +191 -0
- adamops/deployment/containerize.py +262 -0
- adamops/deployment/exporters.py +148 -0
- adamops/evaluation/__init__.py +24 -0
- adamops/evaluation/comparison.py +133 -0
- adamops/evaluation/explainability.py +143 -0
- adamops/evaluation/metrics.py +233 -0
- adamops/evaluation/reports.py +165 -0
- adamops/evaluation/visualization.py +238 -0
- adamops/models/__init__.py +21 -0
- adamops/models/automl.py +277 -0
- adamops/models/ensembles.py +228 -0
- adamops/models/modelops.py +308 -0
- adamops/models/registry.py +250 -0
- adamops/monitoring/__init__.py +21 -0
- adamops/monitoring/alerts.py +200 -0
- adamops/monitoring/dashboard.py +117 -0
- adamops/monitoring/drift.py +212 -0
- adamops/monitoring/performance.py +195 -0
- adamops/pipelines/__init__.py +15 -0
- adamops/pipelines/orchestrators.py +183 -0
- adamops/pipelines/workflows.py +212 -0
- adamops/utils/__init__.py +18 -0
- adamops/utils/config.py +457 -0
- adamops/utils/helpers.py +663 -0
- adamops/utils/logging.py +412 -0
- adamops-0.1.0.dist-info/METADATA +310 -0
- adamops-0.1.0.dist-info/RECORD +42 -0
- adamops-0.1.0.dist-info/WHEEL +5 -0
- adamops-0.1.0.dist-info/entry_points.txt +2 -0
- adamops-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|