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.
Files changed (78) hide show
  1. modelbrief/__init__.py +5 -0
  2. modelbrief/adapters/__init__.py +0 -0
  3. modelbrief/adapters/base.py +26 -0
  4. modelbrief/adapters/computer_vision/__init__.py +0 -0
  5. modelbrief/adapters/computer_vision/image_models.py +5 -0
  6. modelbrief/adapters/computer_vision/pytorch_vision.py +2 -0
  7. modelbrief/adapters/computer_vision/tensorflow_vision.py +2 -0
  8. modelbrief/adapters/deep_learning/__init__.py +0 -0
  9. modelbrief/adapters/deep_learning/pytorch.py +26 -0
  10. modelbrief/adapters/deep_learning/tensorflow.py +10 -0
  11. modelbrief/adapters/nlp/__init__.py +0 -0
  12. modelbrief/adapters/nlp/pytorch_nlp.py +2 -0
  13. modelbrief/adapters/nlp/tensorflow_nlp.py +2 -0
  14. modelbrief/adapters/nlp/transformers.py +11 -0
  15. modelbrief/adapters/tabular/__init__.py +0 -0
  16. modelbrief/adapters/tabular/catboost.py +4 -0
  17. modelbrief/adapters/tabular/lightgbm.py +4 -0
  18. modelbrief/adapters/tabular/sklearn.py +17 -0
  19. modelbrief/adapters/tabular/xgboost.py +4 -0
  20. modelbrief/ai/__init__.py +0 -0
  21. modelbrief/ai/explainer.py +24 -0
  22. modelbrief/ai/providers/__init__.py +0 -0
  23. modelbrief/ai/providers/base.py +4 -0
  24. modelbrief/ai/providers/groq.py +45 -0
  25. modelbrief/ai/recommender.py +23 -0
  26. modelbrief/analysis/__init__.py +0 -0
  27. modelbrief/analysis/anomaly_detection.py +2 -0
  28. modelbrief/analysis/classification.py +2 -0
  29. modelbrief/analysis/clustering.py +2 -0
  30. modelbrief/analysis/computer_vision.py +2 -0
  31. modelbrief/analysis/dataset.py +12 -0
  32. modelbrief/analysis/dimensionality_reduction.py +2 -0
  33. modelbrief/analysis/image_segmentation.py +2 -0
  34. modelbrief/analysis/model.py +2 -0
  35. modelbrief/analysis/nlp.py +2 -0
  36. modelbrief/analysis/object_detection.py +3 -0
  37. modelbrief/analysis/regression.py +2 -0
  38. modelbrief/analysis/sentiment.py +1 -0
  39. modelbrief/analysis/text_classification.py +1 -0
  40. modelbrief/analysis/text_generation.py +2 -0
  41. modelbrief/analysis/time_series.py +2 -0
  42. modelbrief/core/__init__.py +0 -0
  43. modelbrief/core/context.py +20 -0
  44. modelbrief/core/detector.py +90 -0
  45. modelbrief/core/report.py +94 -0
  46. modelbrief/core/result.py +23 -0
  47. modelbrief/metrics/__init__.py +0 -0
  48. modelbrief/metrics/anomaly_detection.py +3 -0
  49. modelbrief/metrics/classification.py +10 -0
  50. modelbrief/metrics/clustering.py +6 -0
  51. modelbrief/metrics/nlp.py +2 -0
  52. modelbrief/metrics/object_detection.py +2 -0
  53. modelbrief/metrics/regression.py +6 -0
  54. modelbrief/metrics/segmentation.py +3 -0
  55. modelbrief/metrics/time_series.py +7 -0
  56. modelbrief/metrics/vision.py +1 -0
  57. modelbrief/output/__init__.py +0 -0
  58. modelbrief/output/console.py +9 -0
  59. modelbrief/output/html.py +128 -0
  60. modelbrief/output/pdf.py +159 -0
  61. modelbrief/visualization/__init__.py +0 -0
  62. modelbrief/visualization/confusion_matrix.py +7 -0
  63. modelbrief/visualization/detection.py +6 -0
  64. modelbrief/visualization/distributions.py +3 -0
  65. modelbrief/visualization/errors.py +1 -0
  66. modelbrief/visualization/feature_importance.py +3 -0
  67. modelbrief/visualization/image_predictions.py +2 -0
  68. modelbrief/visualization/image_samples.py +5 -0
  69. modelbrief/visualization/nlp.py +1 -0
  70. modelbrief/visualization/residuals.py +3 -0
  71. modelbrief/visualization/segmentation.py +3 -0
  72. modelbrief/visualization/text_errors.py +1 -0
  73. modelbrief/visualization/token_analysis.py +2 -0
  74. modelbrief/visualization/vision.py +3 -0
  75. modelbrief-0.2.1.dist-info/METADATA +214 -0
  76. modelbrief-0.2.1.dist-info/RECORD +78 -0
  77. modelbrief-0.2.1.dist-info/WHEEL +4 -0
  78. modelbrief-0.2.1.dist-info/licenses/LICENSE +9 -0
modelbrief/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """ModelBrief: reporting for already-trained ML models."""
2
+ from .core.report import ModelBrief
3
+ from .core.result import ReportResult, Section
4
+ __all__ = ["ModelBrief", "ReportResult", "Section"]
5
+ __version__ = "0.2.0"
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
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ def is_image_batch(X):
4
+ shape=getattr(X,"shape",None)
5
+ return bool(shape is not None and len(shape)==4 and (shape[1] in (1,3,4) or shape[-1] in (1,3,4)))
@@ -0,0 +1,2 @@
1
+ from ..deep_learning.pytorch import PyTorchAdapter
2
+ class PyTorchVisionAdapter(PyTorchAdapter): pass
@@ -0,0 +1,2 @@
1
+ from ..deep_learning.tensorflow import TensorFlowAdapter
2
+ class TensorFlowVisionAdapter(TensorFlowAdapter): pass
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,2 @@
1
+ from ..deep_learning.pytorch import PyTorchAdapter
2
+ class PyTorchNLPAdapter(PyTorchAdapter): pass
@@ -0,0 +1,2 @@
1
+ from ..deep_learning.tensorflow import TensorFlowAdapter
2
+ class TensorFlowNLPAdapter(TensorFlowAdapter): pass
@@ -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,4 @@
1
+ from .sklearn import SklearnAdapter
2
+ class CatBoostAdapter(SklearnAdapter):
3
+ """Adapter for an already-fitted catboost estimator."""
4
+ pass
@@ -0,0 +1,4 @@
1
+ from .sklearn import SklearnAdapter
2
+ class LightGBMAdapter(SklearnAdapter):
3
+ """Adapter for an already-fitted lightgbm estimator."""
4
+ pass
@@ -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
+
@@ -0,0 +1,4 @@
1
+ from .sklearn import SklearnAdapter
2
+ class XGBoostAdapter(SklearnAdapter):
3
+ """Adapter for an already-fitted xgboost estimator."""
4
+ pass
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,4 @@
1
+ from abc import ABC,abstractmethod
2
+ class AIProvider(ABC):
3
+ @abstractmethod
4
+ def complete(self,system,user): raise NotImplementedError
@@ -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,2 @@
1
+ from ..metrics.anomaly_detection import anomaly_metrics
2
+ def analyse(y,pred,scores=None): return anomaly_metrics(y,pred,scores)
@@ -0,0 +1,2 @@
1
+ from ..metrics.classification import classification_metrics
2
+ def analyse(y,pred,proba=None): return classification_metrics(y,pred,proba)
@@ -0,0 +1,2 @@
1
+ from ..metrics.clustering import clustering_metrics
2
+ def analyse(X,labels): return clustering_metrics(X,labels)
@@ -0,0 +1,2 @@
1
+ from ..metrics.vision import image_classification_metrics
2
+ def analyse(y,pred,proba=None): return image_classification_metrics(y,pred,proba)
@@ -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,2 @@
1
+ def analyse(X,embedded):
2
+ return {"input_dimensions":getattr(X,"shape",[None,None])[-1],"output_dimensions":getattr(embedded,"shape",[None,None])[-1],"samples":len(embedded)}
@@ -0,0 +1,2 @@
1
+ from ..metrics.segmentation import segmentation_metrics
2
+ def analyse(y,pred): return segmentation_metrics(y,pred)
@@ -0,0 +1,2 @@
1
+ def model_overview(model,adapter,task):
2
+ return {"model_class":type(model).__name__,"module":type(model).__module__,"task":task,"fitted_model_supplied":True,"retraining_performed":False,"parameters":adapter.parameters()}
@@ -0,0 +1,2 @@
1
+ def analyse_texts(texts):
2
+ lengths=[len(str(t).split()) for t in texts]; return {"documents":len(lengths),"mean_tokens":sum(lengths)/len(lengths) if lengths else 0,"max_tokens":max(lengths,default=0)}
@@ -0,0 +1,3 @@
1
+ from ..metrics.object_detection import intersection_over_union
2
+ def analyse(true_boxes,pred_boxes):
3
+ vals=[intersection_over_union(a,b) for a,b in zip(true_boxes,pred_boxes)]; return {"mean_iou":sum(vals)/len(vals) if vals else 0.0,"matched_boxes":len(vals)}
@@ -0,0 +1,2 @@
1
+ from ..metrics.regression import regression_metrics
2
+ def analyse(y,pred): return regression_metrics(y,pred)
@@ -0,0 +1 @@
1
+ from .classification import analyse
@@ -0,0 +1 @@
1
+ from .classification import analyse
@@ -0,0 +1,2 @@
1
+ def analyse(generated):
2
+ texts=[str(x) for x in generated]; return {"generations":len(texts),"mean_characters":sum(map(len,texts))/len(texts) if texts else 0}
@@ -0,0 +1,2 @@
1
+ from ..metrics.time_series import time_series_metrics
2
+ def analyse(y,pred): return time_series_metrics(y,pred)
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,3 @@
1
+ def anomaly_metrics(y,pred,scores=None):
2
+ from .classification import classification_metrics
3
+ return classification_metrics(y,pred,scores)
@@ -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,2 @@
1
+ def token_accuracy(y,pred):
2
+ total=sum(len(a) for a in y); correct=sum(sum(x==z for x,z in zip(a,b)) for a,b in zip(y,pred)); return correct/total if total else 0.0
@@ -0,0 +1,2 @@
1
+ def intersection_over_union(a,b):
2
+ x1=max(a[0],b[0]); y1=max(a[1],b[1]); x2=min(a[2],b[2]); y2=min(a[3],b[3]); inter=max(0,x2-x1)*max(0,y2-y1); union=max(0,a[2]-a[0])*max(0,a[3]-a[1])+max(0,b[2]-b[0])*max(0,b[3]-b[1])-inter; return inter/union if union else 0.0
@@ -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