modelbrief 0.2.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.
- modelbrief/__init__.py +5 -0
- modelbrief/adapters/__init__.py +0 -0
- modelbrief/adapters/base.py +26 -0
- modelbrief/adapters/computer_vision/__init__.py +0 -0
- modelbrief/adapters/computer_vision/image_models.py +5 -0
- modelbrief/adapters/computer_vision/pytorch_vision.py +2 -0
- modelbrief/adapters/computer_vision/tensorflow_vision.py +2 -0
- modelbrief/adapters/deep_learning/__init__.py +0 -0
- modelbrief/adapters/deep_learning/pytorch.py +26 -0
- modelbrief/adapters/deep_learning/tensorflow.py +10 -0
- modelbrief/adapters/nlp/__init__.py +0 -0
- modelbrief/adapters/nlp/pytorch_nlp.py +2 -0
- modelbrief/adapters/nlp/tensorflow_nlp.py +2 -0
- modelbrief/adapters/nlp/transformers.py +11 -0
- modelbrief/adapters/tabular/__init__.py +0 -0
- modelbrief/adapters/tabular/catboost.py +4 -0
- modelbrief/adapters/tabular/lightgbm.py +4 -0
- modelbrief/adapters/tabular/sklearn.py +17 -0
- modelbrief/adapters/tabular/xgboost.py +4 -0
- modelbrief/ai/__init__.py +0 -0
- modelbrief/ai/explainer.py +24 -0
- modelbrief/ai/providers/__init__.py +0 -0
- modelbrief/ai/providers/base.py +4 -0
- modelbrief/ai/providers/groq.py +45 -0
- modelbrief/ai/recommender.py +23 -0
- modelbrief/analysis/__init__.py +0 -0
- modelbrief/analysis/anomaly_detection.py +2 -0
- modelbrief/analysis/classification.py +2 -0
- modelbrief/analysis/clustering.py +2 -0
- modelbrief/analysis/computer_vision.py +2 -0
- modelbrief/analysis/dataset.py +12 -0
- modelbrief/analysis/dimensionality_reduction.py +2 -0
- modelbrief/analysis/image_segmentation.py +2 -0
- modelbrief/analysis/model.py +2 -0
- modelbrief/analysis/nlp.py +2 -0
- modelbrief/analysis/object_detection.py +3 -0
- modelbrief/analysis/regression.py +2 -0
- modelbrief/analysis/sentiment.py +1 -0
- modelbrief/analysis/text_classification.py +1 -0
- modelbrief/analysis/text_generation.py +2 -0
- modelbrief/analysis/time_series.py +2 -0
- modelbrief/core/__init__.py +0 -0
- modelbrief/core/context.py +20 -0
- modelbrief/core/detector.py +90 -0
- modelbrief/core/report.py +94 -0
- modelbrief/core/result.py +23 -0
- modelbrief/metrics/__init__.py +0 -0
- modelbrief/metrics/anomaly_detection.py +3 -0
- modelbrief/metrics/classification.py +10 -0
- modelbrief/metrics/clustering.py +6 -0
- modelbrief/metrics/nlp.py +2 -0
- modelbrief/metrics/object_detection.py +2 -0
- modelbrief/metrics/regression.py +6 -0
- modelbrief/metrics/segmentation.py +3 -0
- modelbrief/metrics/time_series.py +7 -0
- modelbrief/metrics/vision.py +1 -0
- modelbrief/output/__init__.py +0 -0
- modelbrief/output/console.py +9 -0
- modelbrief/output/html.py +128 -0
- modelbrief/output/pdf.py +159 -0
- modelbrief/visualization/__init__.py +0 -0
- modelbrief/visualization/confusion_matrix.py +7 -0
- modelbrief/visualization/detection.py +6 -0
- modelbrief/visualization/distributions.py +3 -0
- modelbrief/visualization/errors.py +1 -0
- modelbrief/visualization/feature_importance.py +3 -0
- modelbrief/visualization/image_predictions.py +2 -0
- modelbrief/visualization/image_samples.py +5 -0
- modelbrief/visualization/nlp.py +1 -0
- modelbrief/visualization/residuals.py +3 -0
- modelbrief/visualization/segmentation.py +3 -0
- modelbrief/visualization/text_errors.py +1 -0
- modelbrief/visualization/token_analysis.py +2 -0
- modelbrief/visualization/vision.py +3 -0
- modelbrief-0.2.1.dist-info/METADATA +214 -0
- modelbrief-0.2.1.dist-info/RECORD +78 -0
- modelbrief-0.2.1.dist-info/WHEEL +4 -0
- modelbrief-0.2.1.dist-info/licenses/LICENSE +9 -0
modelbrief/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
class BaseAdapter(ABC):
|
|
6
|
+
def __init__(self, model): self.model=model
|
|
7
|
+
@abstractmethod
|
|
8
|
+
def predict(self, X): raise NotImplementedError
|
|
9
|
+
def predict_proba(self, X):
|
|
10
|
+
if hasattr(self.model,"predict_proba"): return np.asarray(self.model.predict_proba(X))
|
|
11
|
+
return None
|
|
12
|
+
def parameters(self):
|
|
13
|
+
if hasattr(self.model,"get_params"):
|
|
14
|
+
try: return self.model.get_params(deep=False)
|
|
15
|
+
except Exception: pass
|
|
16
|
+
if hasattr(self.model,"count_params"):
|
|
17
|
+
try: return {"trainable_or_total_parameters":int(self.model.count_params())}
|
|
18
|
+
except Exception: pass
|
|
19
|
+
return {"class":type(self.model).__name__}
|
|
20
|
+
def feature_importance(self, feature_names=None):
|
|
21
|
+
values=getattr(self.model,"feature_importances_",None)
|
|
22
|
+
if values is None and hasattr(self.model,"coef_"):
|
|
23
|
+
values=np.asarray(self.model.coef_); values=np.mean(np.abs(values),axis=0) if values.ndim>1 else np.abs(values)
|
|
24
|
+
if values is None: return None
|
|
25
|
+
values=np.asarray(values).reshape(-1); names=feature_names or [f"feature_{i}" for i in range(len(values))]
|
|
26
|
+
return sorted(zip(names,values.tolist()),key=lambda x:abs(x[1]),reverse=True)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import numpy as np
|
|
3
|
+
from ..base import BaseAdapter
|
|
4
|
+
class PyTorchAdapter(BaseAdapter):
|
|
5
|
+
def __init__(self,model,device=None,batch_size=64): super().__init__(model); self.device=device; self.batch_size=batch_size
|
|
6
|
+
def _tensor(self,X):
|
|
7
|
+
import torch
|
|
8
|
+
if isinstance(X,torch.Tensor): return X
|
|
9
|
+
return torch.as_tensor(np.asarray(X),dtype=torch.float32)
|
|
10
|
+
def raw_predict(self,X):
|
|
11
|
+
import torch
|
|
12
|
+
self.model.eval(); device=self.device or next(self.model.parameters(),torch.empty(0)).device
|
|
13
|
+
out=[]; t=self._tensor(X)
|
|
14
|
+
with torch.no_grad():
|
|
15
|
+
for batch in torch.split(t,self.batch_size):
|
|
16
|
+
y=self.model(batch.to(device)); y=y.logits if hasattr(y,"logits") else y; out.append(y.detach().cpu())
|
|
17
|
+
return torch.cat(out).numpy()
|
|
18
|
+
def predict_proba(self,X):
|
|
19
|
+
z=self.raw_predict(X)
|
|
20
|
+
if z.ndim==1 or z.shape[-1]==1: return 1/(1+np.exp(-z.reshape(-1)))
|
|
21
|
+
z=z-z.max(axis=1,keepdims=True); e=np.exp(z); return e/e.sum(axis=1,keepdims=True)
|
|
22
|
+
def predict(self,X):
|
|
23
|
+
p=self.predict_proba(X); return (p>=.5).astype(int) if p.ndim==1 else p.argmax(axis=1)
|
|
24
|
+
def parameters(self):
|
|
25
|
+
total=sum(p.numel() for p in self.model.parameters()); trainable=sum(p.numel() for p in self.model.parameters() if p.requires_grad)
|
|
26
|
+
return {"total_parameters":total,"trainable_parameters":trainable,"architecture":str(self.model)}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from ..base import BaseAdapter
|
|
3
|
+
class TensorFlowAdapter(BaseAdapter):
|
|
4
|
+
def raw_predict(self,X): return np.asarray(self.model.predict(X,verbose=0))
|
|
5
|
+
def predict_proba(self,X):
|
|
6
|
+
z=self.raw_predict(X)
|
|
7
|
+
if z.ndim==1 or z.shape[-1]==1: return z.reshape(-1)
|
|
8
|
+
return z
|
|
9
|
+
def predict(self,X):
|
|
10
|
+
p=self.predict_proba(X); return (p>=.5).astype(int) if p.ndim==1 else p.argmax(axis=1)
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from ..base import BaseAdapter
|
|
3
|
+
class TransformersAdapter(BaseAdapter):
|
|
4
|
+
def predict(self,X):
|
|
5
|
+
outputs=self.model(X)
|
|
6
|
+
labels=[]
|
|
7
|
+
for item in outputs:
|
|
8
|
+
if isinstance(item,list): item=max(item,key=lambda z:z.get("score",0))
|
|
9
|
+
labels.append(item.get("label",item) if isinstance(item,dict) else item)
|
|
10
|
+
return np.asarray(labels,dtype=object)
|
|
11
|
+
def parameters(self): return {"pipeline_task":getattr(self.model,"task",None),"model_class":type(getattr(self.model,"model",self.model)).__name__}
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from ..base import BaseAdapter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SklearnAdapter(BaseAdapter):
|
|
7
|
+
"""Adapter for scikit-learn and compatible estimators."""
|
|
8
|
+
|
|
9
|
+
def predict(self, X):
|
|
10
|
+
return np.asarray(self.model.predict(X))
|
|
11
|
+
|
|
12
|
+
def predict_proba(self, X):
|
|
13
|
+
if hasattr(self.model, "predict_proba"):
|
|
14
|
+
return np.asarray(self.model.predict_proba(X))
|
|
15
|
+
|
|
16
|
+
return None
|
|
17
|
+
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def explain(result, provider):
|
|
5
|
+
prompt = (
|
|
6
|
+
"Explain the ML evaluation results accurately and concisely. "
|
|
7
|
+
"Return exactly 3 to 5 findings. "
|
|
8
|
+
"Use clean Markdown with one finding per line beginning with '- '. "
|
|
9
|
+
"Do not use numbered lists, nested bullets, bullet symbols such as '•', "
|
|
10
|
+
"or additional '-' characters. "
|
|
11
|
+
"Focus only on the most important findings, patterns, and limitations. "
|
|
12
|
+
"Do not repeat all metrics, tables, confusion matrices, or feature "
|
|
13
|
+
"importance values already shown in the report. "
|
|
14
|
+
"Do not provide code. "
|
|
15
|
+
"Base every statement only on the supplied results. "
|
|
16
|
+
"Do not invent facts or make unsupported claims."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
data = json.dumps(
|
|
20
|
+
result.to_dict(),
|
|
21
|
+
default=str,
|
|
22
|
+
)[:7000]
|
|
23
|
+
|
|
24
|
+
return provider.complete(prompt, data)
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from .base import AIProvider
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GroqProvider(AIProvider):
|
|
7
|
+
|
|
8
|
+
def __init__(self, api_key=None, model=None):
|
|
9
|
+
key = api_key or os.getenv("GROQ_API_KEY")
|
|
10
|
+
|
|
11
|
+
if not key:
|
|
12
|
+
raise ValueError("GROQ_API_KEY is required when ai=True")
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from groq import Groq
|
|
16
|
+
except ImportError as e:
|
|
17
|
+
raise ImportError(
|
|
18
|
+
'Install AI support with: pip install "modelbrief[ai]"'
|
|
19
|
+
) from e
|
|
20
|
+
|
|
21
|
+
self.client = Groq(api_key=key)
|
|
22
|
+
|
|
23
|
+
self.model = model or os.getenv(
|
|
24
|
+
"MODELBRIEF_GROQ_MODEL",
|
|
25
|
+
"openai/gpt-oss-20b",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def complete(self, system, user):
|
|
29
|
+
response = self.client.chat.completions.create(
|
|
30
|
+
model=self.model,
|
|
31
|
+
messages=[
|
|
32
|
+
{
|
|
33
|
+
"role": "system",
|
|
34
|
+
"content": system,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"role": "user",
|
|
38
|
+
"content": user,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
temperature=0.2,
|
|
42
|
+
max_completion_tokens=1200,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return response.choices[0].message.content
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def recommend(result, provider):
|
|
5
|
+
prompt = (
|
|
6
|
+
"Provide 3 to 5 concise, practical, evidence-based recommendations. "
|
|
7
|
+
"Use clean Markdown with one recommendation per line beginning with '- '. "
|
|
8
|
+
"Do not use numbered lists, nested bullets, bullet symbols such as '•', "
|
|
9
|
+
"or additional '-' characters. "
|
|
10
|
+
"Focus only on recommendations supported by the supplied results. "
|
|
11
|
+
"Do not repeat metrics or findings already shown in the report. "
|
|
12
|
+
"Do not claim the model was retrained or improved. "
|
|
13
|
+
"Do not provide code or tables. "
|
|
14
|
+
"Prioritize the most useful recommendations and avoid repetition."
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
data = json.dumps(
|
|
18
|
+
result.to_dict(),
|
|
19
|
+
default=str,
|
|
20
|
+
)[:7000]
|
|
21
|
+
|
|
22
|
+
return provider.complete(prompt, data)
|
|
23
|
+
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def describe_dataset(X,y=None):
|
|
4
|
+
if X is None: return {"available":False}
|
|
5
|
+
shape=getattr(X,"shape",None) or (len(X),)
|
|
6
|
+
info={"available":True,"rows":int(shape[0]),"shape":tuple(int(i) for i in shape),"type":type(X).__name__}
|
|
7
|
+
try:
|
|
8
|
+
a=np.asarray(X); info["missing_values"]=int(np.sum(a!=a)) if a.dtype.kind in "fc" else None
|
|
9
|
+
except Exception: info["missing_values"]=None
|
|
10
|
+
if y is not None:
|
|
11
|
+
ya=np.asarray(y); info["target_shape"]=tuple(ya.shape); info["target_unique"]=int(len(np.unique(ya))) if ya.size and ya.ndim==1 else None
|
|
12
|
+
return info
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .classification import analyse
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .classification import analyse
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class DataSplit:
|
|
7
|
+
X: Any = None
|
|
8
|
+
y: Any = None
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ReportContext:
|
|
12
|
+
model: Any
|
|
13
|
+
train: DataSplit = field(default_factory=DataSplit)
|
|
14
|
+
validation: DataSplit = field(default_factory=DataSplit)
|
|
15
|
+
test: DataSplit = field(default_factory=DataSplit)
|
|
16
|
+
task: str | None = None
|
|
17
|
+
feature_names: list[str] | None = None
|
|
18
|
+
target_names: list[str] | None = None
|
|
19
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
20
|
+
options: dict[str, Any] = field(default_factory=dict)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from ..adapters.tabular.sklearn import SklearnAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def detect_adapter(model, X=None):
|
|
9
|
+
"""Select the correct adapter for the model's framework."""
|
|
10
|
+
|
|
11
|
+
module_name = type(model).__module__.lower()
|
|
12
|
+
|
|
13
|
+
# Scikit-learn models and pipelines
|
|
14
|
+
if (
|
|
15
|
+
module_name == "sklearn.pipeline"
|
|
16
|
+
or module_name.startswith("sklearn.")
|
|
17
|
+
):
|
|
18
|
+
return SklearnAdapter(model)
|
|
19
|
+
|
|
20
|
+
# Genuine Hugging Face Transformers pipelines
|
|
21
|
+
if (
|
|
22
|
+
module_name == "transformers.pipelines.base"
|
|
23
|
+
or module_name.startswith("transformers.pipelines")
|
|
24
|
+
):
|
|
25
|
+
from ..adapters.nlp.transformers import TransformersAdapter
|
|
26
|
+
|
|
27
|
+
return TransformersAdapter(model)
|
|
28
|
+
|
|
29
|
+
# PyTorch models
|
|
30
|
+
if module_name.startswith("torch"):
|
|
31
|
+
from ..adapters.deep_learning.pytorch import PyTorchAdapter
|
|
32
|
+
|
|
33
|
+
return PyTorchAdapter(model)
|
|
34
|
+
|
|
35
|
+
# TensorFlow and Keras models
|
|
36
|
+
if module_name.startswith(("tensorflow", "keras")):
|
|
37
|
+
from ..adapters.deep_learning.tensorflow import TensorFlowAdapter
|
|
38
|
+
|
|
39
|
+
return TensorFlowAdapter(model)
|
|
40
|
+
|
|
41
|
+
# Default to the scikit-learn adapter
|
|
42
|
+
return SklearnAdapter(model)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def detect_task(model, y=None, requested=None):
|
|
46
|
+
"""Detect the task unless the user explicitly provides one."""
|
|
47
|
+
|
|
48
|
+
if requested:
|
|
49
|
+
return requested.lower().strip().replace(" ", "_")
|
|
50
|
+
|
|
51
|
+
# Older scikit-learn versions expose estimator type through
|
|
52
|
+
# _estimator_type.
|
|
53
|
+
estimator_type = getattr(model, "_estimator_type", None)
|
|
54
|
+
|
|
55
|
+
# Newer scikit-learn versions expose estimator type through
|
|
56
|
+
# __sklearn_tags__().
|
|
57
|
+
if estimator_type is None and hasattr(model, "__sklearn_tags__"):
|
|
58
|
+
try:
|
|
59
|
+
estimator_type = model.__sklearn_tags__().estimator_type
|
|
60
|
+
except (AttributeError, TypeError):
|
|
61
|
+
estimator_type = None
|
|
62
|
+
|
|
63
|
+
if estimator_type == "classifier":
|
|
64
|
+
return "classification"
|
|
65
|
+
|
|
66
|
+
if estimator_type == "regressor":
|
|
67
|
+
return "regression"
|
|
68
|
+
|
|
69
|
+
if estimator_type == "clusterer" or hasattr(model, "labels_"):
|
|
70
|
+
return "clustering"
|
|
71
|
+
|
|
72
|
+
if y is None:
|
|
73
|
+
return "clustering"
|
|
74
|
+
|
|
75
|
+
target = np.asarray(y)
|
|
76
|
+
|
|
77
|
+
if target.dtype.kind in "OUSb":
|
|
78
|
+
return "classification"
|
|
79
|
+
|
|
80
|
+
unique_values = len(np.unique(target))
|
|
81
|
+
|
|
82
|
+
classification_limit = max(
|
|
83
|
+
20,
|
|
84
|
+
int(np.sqrt(max(len(target), 1))),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if unique_values <= classification_limit:
|
|
88
|
+
return "classification"
|
|
89
|
+
|
|
90
|
+
return "regression"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import base64,io,json
|
|
3
|
+
import numpy as np
|
|
4
|
+
from .context import ReportContext,DataSplit
|
|
5
|
+
from .detector import detect_adapter,detect_task
|
|
6
|
+
from .result import ReportResult
|
|
7
|
+
from ..analysis.dataset import describe_dataset
|
|
8
|
+
from ..analysis.model import model_overview
|
|
9
|
+
|
|
10
|
+
class ModelBrief:
|
|
11
|
+
def __init__(self,model,X_train=None,X_val=None,X_test=None,y_train=None,y_val=None,y_test=None,task=None,ai=False,feature_names=None,target_names=None,adapter=None,**options):
|
|
12
|
+
self.context=ReportContext(model,DataSplit(X_train,y_train),DataSplit(X_val,y_val),DataSplit(X_test,y_test),task,feature_names,target_names,options=options)
|
|
13
|
+
self.adapter=adapter or detect_adapter(model,X_train if X_train is not None else X_test); self.ai=ai; self._result=None
|
|
14
|
+
# print(
|
|
15
|
+
# "Selected adapter:",
|
|
16
|
+
# type(self.adapter).__name__,
|
|
17
|
+
# "| Model:",
|
|
18
|
+
# type(model).__module__,
|
|
19
|
+
# type(model).__name__,
|
|
20
|
+
# )
|
|
21
|
+
def _split_analysis(self,name,split,task):
|
|
22
|
+
if split.X is None: return None
|
|
23
|
+
pred=np.asarray(self.adapter.predict(split.X)); out={"samples":len(pred)}
|
|
24
|
+
if split.y is None:
|
|
25
|
+
if task=="clustering":
|
|
26
|
+
from ..metrics.clustering import clustering_metrics; out.update(clustering_metrics(split.X,pred))
|
|
27
|
+
else: out["predictions_generated"]=True
|
|
28
|
+
return out
|
|
29
|
+
if task in ("classification","computer_vision","nlp","text_classification","sentiment"):
|
|
30
|
+
from ..metrics.classification import classification_metrics
|
|
31
|
+
try: proba=self.adapter.predict_proba(split.X)
|
|
32
|
+
except Exception: proba=None
|
|
33
|
+
out.update(classification_metrics(split.y,pred,proba))
|
|
34
|
+
elif task in ("time_series",):
|
|
35
|
+
from ..metrics.time_series import time_series_metrics; out.update(time_series_metrics(split.y,pred))
|
|
36
|
+
elif task in ("anomaly_detection",):
|
|
37
|
+
from ..metrics.anomaly_detection import anomaly_metrics; out.update(anomaly_metrics(split.y,pred))
|
|
38
|
+
elif task in ("image_segmentation","segmentation"):
|
|
39
|
+
from ..metrics.segmentation import segmentation_metrics; out.update(segmentation_metrics(split.y,pred))
|
|
40
|
+
else:
|
|
41
|
+
from ..metrics.regression import regression_metrics; out.update(regression_metrics(split.y,pred))
|
|
42
|
+
return out
|
|
43
|
+
def analyse(self,force=False):
|
|
44
|
+
if self._result is not None and not force: return self._result
|
|
45
|
+
c=self.context; yref=c.train.y if c.train.y is not None else (c.test.y if c.test.y is not None else c.validation.y); task=detect_task(c.model,yref,c.task); r=ReportResult(metadata={"task":task,"library_version":"0.2.0"})
|
|
46
|
+
r.add("MODEL OVERVIEW",model_overview(c.model,self.adapter,task)); r.add("DATASET",{"train":describe_dataset(c.train.X,c.train.y),"validation":describe_dataset(c.validation.X,c.validation.y),"test":describe_dataset(c.test.X,c.test.y)}); r.add("TASK",{"detected_or_requested":task}); r.add("PARAMETERS",self.adapter.parameters())
|
|
47
|
+
fi=self.adapter.feature_importance(c.feature_names)
|
|
48
|
+
r.add("FEATURE IMPORTANCE",{"available":fi is not None,"ranking":fi[:50] if fi else []})
|
|
49
|
+
perf={};
|
|
50
|
+
for name,split in (("train",c.train),("validation",c.validation),("test",c.test)):
|
|
51
|
+
try:
|
|
52
|
+
value=self._split_analysis(name,split,task)
|
|
53
|
+
if value is not None: perf[name]=value
|
|
54
|
+
except Exception as e: r.warnings.append(f"{name} analysis skipped: {type(e).__name__}: {e}")
|
|
55
|
+
r.add("MODEL PERFORMANCE",perf)
|
|
56
|
+
cm=next((v.get("confusion_matrix") for v in (perf.get("test",{}),perf.get("validation",{}),perf.get("train",{})) if "confusion_matrix" in v),None); r.add("CONFUSION MATRIX",{"available":cm is not None,"matrix":cm})
|
|
57
|
+
r.add("ERROR ANALYSIS",self._errors(task,c.test if c.test.X is not None else c.validation if c.validation.X is not None else c.train))
|
|
58
|
+
self._figures(r,fi,cm,task,c)
|
|
59
|
+
if self.ai:
|
|
60
|
+
from ..ai.providers.groq import GroqProvider
|
|
61
|
+
from ..ai.explainer import explain
|
|
62
|
+
from ..ai.recommender import recommend
|
|
63
|
+
provider=GroqProvider(); r.add("AI EXPLANATION",{"text":explain(r,provider)}); r.recommendations.append(recommend(r,provider))
|
|
64
|
+
self._result=r; return r
|
|
65
|
+
def _errors(self,task,split):
|
|
66
|
+
if split.X is None or split.y is None: return {"available":False}
|
|
67
|
+
try:
|
|
68
|
+
pred=np.asarray(self.adapter.predict(split.X)); y=np.asarray(split.y)
|
|
69
|
+
if task in ("classification","computer_vision","nlp","text_classification","sentiment","anomaly_detection"):
|
|
70
|
+
idx=np.flatnonzero(pred!=y)[:25]; return {"available":True,"error_count":int(np.sum(pred!=y)),"examples":[{"index":int(i),"actual":str(y[i]),"predicted":str(pred[i])} for i in idx]}
|
|
71
|
+
err=np.abs(y-pred); idx=np.argsort(err.reshape(-1))[::-1][:25]; return {"available":True,"mean_absolute_error":float(np.mean(err)),"largest":[{"index":int(i),"actual":float(y.reshape(-1)[i]),"predicted":float(pred.reshape(-1)[i]),"absolute_error":float(err.reshape(-1)[i])} for i in idx]}
|
|
72
|
+
except Exception as e: return {"available":False,"reason":str(e)}
|
|
73
|
+
def _add_fig(self,r,fig,title):
|
|
74
|
+
b=io.BytesIO(); fig.savefig(b,format="png",dpi=130,bbox_inches="tight"); r.figures.append({"title":title,"data":base64.b64encode(b.getvalue()).decode()});
|
|
75
|
+
import matplotlib.pyplot as plt; plt.close(fig)
|
|
76
|
+
def _figures(self,r,fi,cm,task,c):
|
|
77
|
+
try:
|
|
78
|
+
if fi:
|
|
79
|
+
from ..visualization.feature_importance import plot_feature_importance; self._add_fig(r,plot_feature_importance(fi),"Feature importance")
|
|
80
|
+
if cm:
|
|
81
|
+
from ..visualization.confusion_matrix import plot_confusion_matrix; self._add_fig(r,plot_confusion_matrix(cm,c.target_names),"Confusion matrix")
|
|
82
|
+
split=c.test if c.test.X is not None else c.validation
|
|
83
|
+
if task in ("regression","time_series") and split.X is not None and split.y is not None:
|
|
84
|
+
from ..visualization.residuals import plot_residuals; self._add_fig(r,plot_residuals(split.y,self.adapter.predict(split.X)),"Residuals")
|
|
85
|
+
except Exception as e: r.warnings.append(f"Visualisation skipped: {e}")
|
|
86
|
+
def show(self):
|
|
87
|
+
from ..output.console import render_console
|
|
88
|
+
text=render_console(self.analyse()); print(text); return text
|
|
89
|
+
def html(self,path="modelbrief_report.html"):
|
|
90
|
+
from ..output.html import render_html; return render_html(self.analyse(),path)
|
|
91
|
+
def pdf(self,path="modelbrief_report.pdf"):
|
|
92
|
+
from ..output.pdf import render_pdf; return render_pdf(self.analyse(),path)
|
|
93
|
+
@property
|
|
94
|
+
def result(self): return self.analyse()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Section:
|
|
7
|
+
title: str
|
|
8
|
+
content: dict[str, Any] = field(default_factory=dict)
|
|
9
|
+
narrative: str | None = None
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ReportResult:
|
|
13
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
14
|
+
sections: list[Section] = field(default_factory=list)
|
|
15
|
+
figures: list[dict[str, Any]] = field(default_factory=list)
|
|
16
|
+
warnings: list[str] = field(default_factory=list)
|
|
17
|
+
recommendations: list[str] = field(default_factory=list)
|
|
18
|
+
def add(self, title: str, content: dict[str, Any], narrative: str | None = None):
|
|
19
|
+
self.sections.append(Section(title, content, narrative)); return self
|
|
20
|
+
def section(self, title: str):
|
|
21
|
+
return next((s for s in self.sections if s.title == title), None)
|
|
22
|
+
def to_dict(self):
|
|
23
|
+
return {"metadata":self.metadata,"sections":[{"title":s.title,"content":s.content,"narrative":s.narrative} for s in self.sections],"figures":self.figures,"warnings":self.warnings,"recommendations":self.recommendations}
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def classification_metrics(y,pred,proba=None):
|
|
4
|
+
from sklearn.metrics import accuracy_score,balanced_accuracy_score,precision_recall_fscore_support,confusion_matrix,log_loss,roc_auc_score
|
|
5
|
+
y=np.asarray(y); pred=np.asarray(pred); pr,rc,f1,_=precision_recall_fscore_support(y,pred,average="weighted",zero_division=0)
|
|
6
|
+
out={"accuracy":float(accuracy_score(y,pred)),"balanced_accuracy":float(balanced_accuracy_score(y,pred)),"precision_weighted":float(pr),"recall_weighted":float(rc),"f1_weighted":float(f1),"confusion_matrix":confusion_matrix(y,pred).tolist()}
|
|
7
|
+
if proba is not None:
|
|
8
|
+
try: out["log_loss"]=float(log_loss(y,proba)); out["roc_auc"]=float(roc_auc_score(y,proba,multi_class="ovr") if np.asarray(proba).ndim>1 and np.asarray(proba).shape[1]>2 else roc_auc_score(y,np.asarray(proba)[:,1] if np.asarray(proba).ndim>1 else proba))
|
|
9
|
+
except Exception: pass
|
|
10
|
+
return out
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
def clustering_metrics(X,labels):
|
|
2
|
+
from sklearn.metrics import silhouette_score,calinski_harabasz_score,davies_bouldin_score
|
|
3
|
+
n=len(set(labels)); out={"clusters":n}
|
|
4
|
+
if n>1 and n<len(labels):
|
|
5
|
+
out.update(silhouette=float(silhouette_score(X,labels)),calinski_harabasz=float(calinski_harabasz_score(X,labels)),davies_bouldin=float(davies_bouldin_score(X,labels)))
|
|
6
|
+
return out
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def regression_metrics(y,pred):
|
|
4
|
+
from sklearn.metrics import mean_absolute_error,mean_squared_error,r2_score,median_absolute_error,explained_variance_score
|
|
5
|
+
y=np.asarray(y); pred=np.asarray(pred); mse=mean_squared_error(y,pred)
|
|
6
|
+
return {"mae":float(mean_absolute_error(y,pred)),"mse":float(mse),"rmse":float(np.sqrt(mse)),"r2":float(r2_score(y,pred)),"median_ae":float(median_absolute_error(y,pred)),"explained_variance":float(explained_variance_score(y,pred))}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
def segmentation_metrics(y,pred):
|
|
3
|
+
y=np.asarray(y).astype(bool); p=np.asarray(pred).astype(bool); inter=np.logical_and(y,p).sum(); union=np.logical_or(y,p).sum(); return {"iou":float(inter/union) if union else 1.0,"dice":float(2*inter/(y.sum()+p.sum())) if y.sum()+p.sum() else 1.0}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def time_series_metrics(y,pred):
|
|
4
|
+
from .regression import regression_metrics
|
|
5
|
+
out=regression_metrics(y,pred); y=np.asarray(y); p=np.asarray(pred); nz=np.abs(y)>1e-12
|
|
6
|
+
out["mape"]=float(np.mean(np.abs((y[nz]-p[nz])/y[nz]))*100) if nz.any() else None
|
|
7
|
+
out["smape"]=float(np.mean(2*np.abs(p-y)/(np.abs(y)+np.abs(p)+1e-12))*100); return out
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .classification import classification_metrics as image_classification_metrics
|