shapeaudit 1.0.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.
- modelcheck/__init__.py +59 -0
- modelcheck/_validate.py +214 -0
- modelcheck/ale.py +60 -0
- modelcheck/ale_detector.py +277 -0
- modelcheck/ale_fast.py +167 -0
- modelcheck/ale_selective.py +157 -0
- modelcheck/api.py +835 -0
- modelcheck/audit.py +477 -0
- modelcheck/backends.py +184 -0
- modelcheck/baselines.py +213 -0
- modelcheck/calibration.py +221 -0
- modelcheck/canary.py +187 -0
- modelcheck/causal.py +233 -0
- modelcheck/causal_bootstrap.py +346 -0
- modelcheck/ci.py +41 -0
- modelcheck/cli.py +380 -0
- modelcheck/compare/__init__.py +0 -0
- modelcheck/compare/model_comparator.py +47 -0
- modelcheck/confidence.py +158 -0
- modelcheck/contradict.py +144 -0
- modelcheck/dashboard.py +102 -0
- modelcheck/drift.py +47 -0
- modelcheck/experimental/__init__.py +28 -0
- modelcheck/experimental/calibration.py +221 -0
- modelcheck/experimental/gtsf.py +183 -0
- modelcheck/experimental/llm.py +393 -0
- modelcheck/experimental/llm_audit.py +597 -0
- modelcheck/experimental/rhale.py +229 -0
- modelcheck/experimental/topology.py +247 -0
- modelcheck/experimental/vision.py +377 -0
- modelcheck/experimental/zest.py +155 -0
- modelcheck/explainers/__init__.py +0 -0
- modelcheck/explainers/shap_explainer.py +67 -0
- modelcheck/explorer.py +246 -0
- modelcheck/fairness.py +120 -0
- modelcheck/frechet.py +332 -0
- modelcheck/geometry.py +186 -0
- modelcheck/gtsf.py +183 -0
- modelcheck/guard.py +144 -0
- modelcheck/importance.py +389 -0
- modelcheck/integrations/__init__.py +0 -0
- modelcheck/integrations/mlflow_integration.py +40 -0
- modelcheck/laudo.py +882 -0
- modelcheck/leakage.py +43 -0
- modelcheck/leakage_temporal.py +201 -0
- modelcheck/narrate.py +36 -0
- modelcheck/ood.py +146 -0
- modelcheck/orchestrator.py +699 -0
- modelcheck/paired.py +229 -0
- modelcheck/pipeline.py +379 -0
- modelcheck/plots.py +53 -0
- modelcheck/psi_shap.py +196 -0
- modelcheck/ranking.py +19 -0
- modelcheck/report.py +72 -0
- modelcheck/rhale.py +229 -0
- modelcheck/rules/__init__.py +0 -0
- modelcheck/rules/business_rules.py +36 -0
- modelcheck/rules/rule_learner.py +154 -0
- modelcheck/score.py +64 -0
- modelcheck/shape.py +368 -0
- modelcheck/shape_extensions.py +479 -0
- modelcheck/shapedist.py +130 -0
- modelcheck/silence.py +247 -0
- modelcheck/slope.py +261 -0
- modelcheck/snapshot.py +429 -0
- modelcheck/stability.py +153 -0
- modelcheck/studio.py +124 -0
- modelcheck/temporal.py +74 -0
- modelcheck/temporal_shape.py +72 -0
- modelcheck/topology.py +247 -0
- modelcheck/trending.py +205 -0
- modelcheck/universal.py +278 -0
- modelcheck/validation.py +247 -0
- modelcheck/zest.py +155 -0
- modelguard/__init__.py +41 -0
- shapeaudit/__init__.py +39 -0
- shapeaudit-1.0.0.dist-info/LICENSE +21 -0
- shapeaudit-1.0.0.dist-info/METADATA +179 -0
- shapeaudit-1.0.0.dist-info/RECORD +82 -0
- shapeaudit-1.0.0.dist-info/WHEEL +5 -0
- shapeaudit-1.0.0.dist-info/entry_points.txt +4 -0
- shapeaudit-1.0.0.dist-info/top_level.txt +3 -0
modelcheck/ale_fast.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ale_fast.py — ALE vetorizado + embedding de modelo
|
|
3
|
+
|
|
4
|
+
Duas contribuições:
|
|
5
|
+
|
|
6
|
+
1. ale_curve_fast — ALE com 1 chamada a predict() por bin (em vez de
|
|
7
|
+
uma por amostra), tratamento correto de bins vazios e suporte a
|
|
8
|
+
features categóricas/binárias.
|
|
9
|
+
|
|
10
|
+
2. model_embedding_ale — representação vetorial do modelo construída a
|
|
11
|
+
partir dos perfis ALE de todas as features. Permite usar os detectores
|
|
12
|
+
geométricos (Fréchet, Mantel, eigenvalue) em QUALQUER arquitetura,
|
|
13
|
+
sem depender de shap.TreeExplainer.
|
|
14
|
+
"""
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
from typing import List, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def feature_kind(X: pd.DataFrame, feature: str) -> str:
|
|
21
|
+
"""Classifica a feature: binary, categorical ou continuous."""
|
|
22
|
+
nu = X[feature].nunique(dropna=True)
|
|
23
|
+
if nu <= 1: return 'constant'
|
|
24
|
+
if nu == 2: return 'binary'
|
|
25
|
+
if nu <= 10: return 'categorical'
|
|
26
|
+
return 'continuous'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _predict(model, X: pd.DataFrame) -> np.ndarray:
|
|
30
|
+
"""predict robusto: usa proba da classe positiva se classificador."""
|
|
31
|
+
if hasattr(model, 'predict_proba'):
|
|
32
|
+
try:
|
|
33
|
+
p = model.predict_proba(X)
|
|
34
|
+
if p.ndim == 2 and p.shape[1] == 2:
|
|
35
|
+
return np.asarray(p[:, 1], dtype=float)
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
return np.asarray(model.predict(X), dtype=float)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def ale_curve_fast(model, X: pd.DataFrame, feature: str,
|
|
42
|
+
n_bins: int = 5,
|
|
43
|
+
min_bin_n: int = 5) -> Optional[Tuple[np.ndarray, np.ndarray]]:
|
|
44
|
+
"""
|
|
45
|
+
ALE vetorizado. Retorna (centros, ale_acumulado_centrado) ou None.
|
|
46
|
+
|
|
47
|
+
Ganho de performance: monta os DataFrames deslocados UMA vez por bin
|
|
48
|
+
e faz duas chamadas a predict por bin, em vez de chamadas por amostra.
|
|
49
|
+
Para features categóricas usa efeito médio por categoria.
|
|
50
|
+
"""
|
|
51
|
+
kind = feature_kind(X, feature)
|
|
52
|
+
if kind == 'constant':
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
x = X[feature].values
|
|
56
|
+
|
|
57
|
+
# ── Categóricas/binárias: efeito médio por categoria ─────────────
|
|
58
|
+
if kind in ('binary', 'categorical'):
|
|
59
|
+
cats = np.sort(pd.unique(x))
|
|
60
|
+
base = _predict(model, X)
|
|
61
|
+
centers, effects = [], []
|
|
62
|
+
for c in cats:
|
|
63
|
+
Xc = X.copy()
|
|
64
|
+
Xc[feature] = c
|
|
65
|
+
effects.append(float((_predict(model, Xc) - base).mean()))
|
|
66
|
+
centers.append(float(c))
|
|
67
|
+
if len(effects) < 2:
|
|
68
|
+
return None
|
|
69
|
+
arr = np.array(effects, dtype=float)
|
|
70
|
+
return np.array(centers, dtype=float), arr - arr.mean()
|
|
71
|
+
|
|
72
|
+
# ── Contínuas: ALE clássico, vetorizado ──────────────────────────
|
|
73
|
+
x = np.asarray(x, dtype=float)
|
|
74
|
+
edges = np.unique(np.percentile(x, np.linspace(0, 100, n_bins + 1)))
|
|
75
|
+
if len(edges) < 3:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# digitize uma única vez para todas as amostras
|
|
79
|
+
idx = np.clip(np.digitize(x, edges[1:-1]), 0, len(edges) - 2)
|
|
80
|
+
|
|
81
|
+
centers, effects = [], []
|
|
82
|
+
for b in range(len(edges) - 1):
|
|
83
|
+
mask = idx == b
|
|
84
|
+
if mask.sum() < min_bin_n:
|
|
85
|
+
continue
|
|
86
|
+
sub = X[mask]
|
|
87
|
+
Xlo = sub.copy(); Xlo[feature] = edges[b]
|
|
88
|
+
Xhi = sub.copy(); Xhi[feature] = edges[b + 1]
|
|
89
|
+
# 2 chamadas a predict por bin, vetorizadas sobre o bin inteiro
|
|
90
|
+
d = _predict(model, Xhi) - _predict(model, Xlo)
|
|
91
|
+
effects.append(float(d.mean()))
|
|
92
|
+
centers.append(float((edges[b] + edges[b + 1]) / 2))
|
|
93
|
+
|
|
94
|
+
if len(effects) < 3:
|
|
95
|
+
return None
|
|
96
|
+
cum = np.cumsum(effects)
|
|
97
|
+
return np.array(centers), cum - cum.mean()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def model_embedding_ale(model, X: pd.DataFrame,
|
|
101
|
+
feature_names: Optional[List[str]] = None,
|
|
102
|
+
n_bins: int = 5,
|
|
103
|
+
n_points: int = 20,
|
|
104
|
+
normalize: bool = True) -> Optional[np.ndarray]:
|
|
105
|
+
"""
|
|
106
|
+
Embedding do modelo a partir dos perfis ALE.
|
|
107
|
+
|
|
108
|
+
Cada feature contribui com n_points valores interpolados do seu perfil
|
|
109
|
+
ALE. O vetor final tem dimensão n_features * n_points e representa o
|
|
110
|
+
"comportamento funcional" do modelo — comparável entre arquiteturas.
|
|
111
|
+
|
|
112
|
+
normalize=True escala cada perfil pela soma de |valores|, isolando a
|
|
113
|
+
FORMA da magnitude. normalize=False preserva magnitude (útil para
|
|
114
|
+
detectar colapso de importância).
|
|
115
|
+
"""
|
|
116
|
+
if feature_names is None:
|
|
117
|
+
feature_names = list(X.columns)
|
|
118
|
+
parts = []
|
|
119
|
+
for f in feature_names:
|
|
120
|
+
out = ale_curve_fast(model, X, f, n_bins=n_bins)
|
|
121
|
+
if out is None:
|
|
122
|
+
parts.append(np.zeros(n_points))
|
|
123
|
+
continue
|
|
124
|
+
centers, vals = out
|
|
125
|
+
if len(centers) < 2:
|
|
126
|
+
parts.append(np.zeros(n_points))
|
|
127
|
+
continue
|
|
128
|
+
xi = np.linspace(centers[0], centers[-1], n_points)
|
|
129
|
+
yi = np.interp(xi, centers, vals)
|
|
130
|
+
if normalize:
|
|
131
|
+
s = np.abs(yi).sum()
|
|
132
|
+
yi = yi / s if s > 1e-12 else yi
|
|
133
|
+
parts.append(yi)
|
|
134
|
+
if not parts:
|
|
135
|
+
return None
|
|
136
|
+
return np.concatenate(parts)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def sample_embeddings_ale(model, X: pd.DataFrame,
|
|
140
|
+
feature_names: Optional[List[str]] = None,
|
|
141
|
+
n_replicas: int = 30,
|
|
142
|
+
frac: float = 0.7,
|
|
143
|
+
n_bins: int = 5,
|
|
144
|
+
n_points: int = 8,
|
|
145
|
+
seed: int = 0) -> Optional[np.ndarray]:
|
|
146
|
+
"""
|
|
147
|
+
Matriz de embeddings (n_replicas × dim) por subamostragem.
|
|
148
|
+
|
|
149
|
+
Substitui a matriz de SHAP values como "nuvem de pontos" que representa
|
|
150
|
+
o modelo, permitindo Fréchet / Mantel / eigenvalue drift sem SHAP.
|
|
151
|
+
"""
|
|
152
|
+
if feature_names is None:
|
|
153
|
+
feature_names = list(X.columns)
|
|
154
|
+
rng = np.random.RandomState(seed)
|
|
155
|
+
n = max(int(len(X) * frac), 30)
|
|
156
|
+
rows = []
|
|
157
|
+
for r in range(n_replicas):
|
|
158
|
+
idx = rng.choice(len(X), min(n, len(X)), replace=False)
|
|
159
|
+
Xs = X.iloc[idx].reset_index(drop=True)
|
|
160
|
+
emb = model_embedding_ale(model, Xs, feature_names,
|
|
161
|
+
n_bins=n_bins, n_points=n_points,
|
|
162
|
+
normalize=False)
|
|
163
|
+
if emb is not None:
|
|
164
|
+
rows.append(emb)
|
|
165
|
+
if len(rows) < 3:
|
|
166
|
+
return None
|
|
167
|
+
return np.vstack(rows)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ale_selective.py — SDD via ALE com bootstrap seletivo
|
|
3
|
+
|
|
4
|
+
Problema medido (v1.23): o ALE vetorizado tem recall 1.00 mas
|
|
5
|
+
especificidade 0.17 — dispara divergência até em `v1 vs v1`.
|
|
6
|
+
Causa: classificar forma sobre ~5 pontos de perfil é instável.
|
|
7
|
+
|
|
8
|
+
Estratégia (bootstrap seletivo):
|
|
9
|
+
Passo 1 — triagem rápida: ALE vetorizado sem bootstrap em TODAS as
|
|
10
|
+
features. Barato.
|
|
11
|
+
Passo 2 — confirmação: bootstrap APENAS nas features que a triagem
|
|
12
|
+
marcou. Caro, mas em poucas features.
|
|
13
|
+
Passo 3 — confirma a divergência somente se a forma se mantiver
|
|
14
|
+
estável em >= min_agreement das réplicas em AMBOS os modelos.
|
|
15
|
+
|
|
16
|
+
Custo: proporcional ao nº de suspeitas, não ao nº de features.
|
|
17
|
+
"""
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pandas as pd
|
|
20
|
+
from typing import List, Optional
|
|
21
|
+
from collections import Counter
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _shape_of(model, X, feature):
|
|
25
|
+
from modelcheck.shape import _shape_profile_small_n
|
|
26
|
+
from modelcheck.ale_fast import ale_curve_fast
|
|
27
|
+
out = ale_curve_fast(model, X, feature)
|
|
28
|
+
if out is None:
|
|
29
|
+
return None
|
|
30
|
+
p = _shape_profile_small_n(out[0], out[1], min_n=3)
|
|
31
|
+
return p['shape'] if p else None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _amplitude(model, X, feature):
|
|
35
|
+
from modelcheck.ale_fast import ale_curve_fast
|
|
36
|
+
out = ale_curve_fast(model, X, feature)
|
|
37
|
+
return 0.0 if out is None else float(np.ptp(out[1]))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SelectiveALEDetector:
|
|
41
|
+
"""
|
|
42
|
+
SDD via ALE com confirmação por bootstrap apenas nas suspeitas.
|
|
43
|
+
|
|
44
|
+
det = SelectiveALEDetector()
|
|
45
|
+
res = det.compare(m1, m2, X_eval, feature_names)
|
|
46
|
+
print(det.report(res))
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, n_boot: int = 12,
|
|
50
|
+
frac: float = 0.75,
|
|
51
|
+
min_agreement: float = 0.70,
|
|
52
|
+
min_amplitude_ratio: float = 0.05,
|
|
53
|
+
seed: int = 0):
|
|
54
|
+
self.n_boot = n_boot
|
|
55
|
+
self.frac = frac
|
|
56
|
+
self.min_agreement = min_agreement
|
|
57
|
+
self.min_amplitude_ratio = min_amplitude_ratio
|
|
58
|
+
self.seed = seed
|
|
59
|
+
|
|
60
|
+
def _stable_shape(self, model, X, feature):
|
|
61
|
+
"""Forma modal e concordância entre réplicas."""
|
|
62
|
+
rng = np.random.RandomState(self.seed)
|
|
63
|
+
n = max(int(len(X) * self.frac), 40)
|
|
64
|
+
shapes = []
|
|
65
|
+
for _ in range(self.n_boot):
|
|
66
|
+
idx = rng.choice(len(X), min(n, len(X)), replace=False)
|
|
67
|
+
s = _shape_of(model, X.iloc[idx].reset_index(drop=True), feature)
|
|
68
|
+
if s:
|
|
69
|
+
shapes.append(s)
|
|
70
|
+
if not shapes:
|
|
71
|
+
return None, 0.0
|
|
72
|
+
sh, c = Counter(shapes).most_common(1)[0]
|
|
73
|
+
return sh, c / len(shapes)
|
|
74
|
+
|
|
75
|
+
def compare(self, model_v1, model_v2,
|
|
76
|
+
X_eval: pd.DataFrame,
|
|
77
|
+
feature_names: Optional[List[str]] = None) -> dict:
|
|
78
|
+
from modelcheck._validate import validate_inputs
|
|
79
|
+
X_eval, feature_names = validate_inputs(
|
|
80
|
+
model_v1, model_v2, X_eval, feature_names,
|
|
81
|
+
caller='SelectiveALEDetector')
|
|
82
|
+
if feature_names is None:
|
|
83
|
+
feature_names = list(X_eval.columns)
|
|
84
|
+
|
|
85
|
+
# ── Passo 1: triagem rápida ──────────────────────────────────
|
|
86
|
+
screened = []
|
|
87
|
+
amps = {}
|
|
88
|
+
for f in feature_names:
|
|
89
|
+
s1 = _shape_of(model_v1, X_eval, f)
|
|
90
|
+
s2 = _shape_of(model_v2, X_eval, f)
|
|
91
|
+
a1 = _amplitude(model_v1, X_eval, f)
|
|
92
|
+
a2 = _amplitude(model_v2, X_eval, f)
|
|
93
|
+
amps[f] = (a1, a2)
|
|
94
|
+
if s1 and s2 and s1 != s2:
|
|
95
|
+
screened.append((f, s1, s2))
|
|
96
|
+
|
|
97
|
+
# amplitude de referência: maior efeito observado
|
|
98
|
+
max_amp = max([max(a) for a in amps.values()] or [1e-9])
|
|
99
|
+
|
|
100
|
+
# ── Passo 2/3: confirmação só nas suspeitas ──────────────────
|
|
101
|
+
confirmed, rejected = [], []
|
|
102
|
+
for f, s1_fast, s2_fast in screened:
|
|
103
|
+
a1, a2 = amps[f]
|
|
104
|
+
# descarta features com efeito irrelevante (ruído numérico)
|
|
105
|
+
if max(a1, a2) / max_amp < self.min_amplitude_ratio:
|
|
106
|
+
rejected.append({'feature': f, 'reason': 'LOW_AMPLITUDE',
|
|
107
|
+
'shape_v1': s1_fast, 'shape_v2': s2_fast,
|
|
108
|
+
'amp_ratio': round(max(a1, a2) / max_amp, 4)})
|
|
109
|
+
continue
|
|
110
|
+
sh1, c1 = self._stable_shape(model_v1, X_eval, f)
|
|
111
|
+
sh2, c2 = self._stable_shape(model_v2, X_eval, f)
|
|
112
|
+
if sh1 is None or sh2 is None:
|
|
113
|
+
rejected.append({'feature': f, 'reason': 'UNSTABLE_PROFILE',
|
|
114
|
+
'shape_v1': s1_fast, 'shape_v2': s2_fast})
|
|
115
|
+
continue
|
|
116
|
+
if c1 < self.min_agreement or c2 < self.min_agreement:
|
|
117
|
+
rejected.append({'feature': f, 'reason': 'LOW_AGREEMENT',
|
|
118
|
+
'shape_v1': sh1, 'shape_v2': sh2,
|
|
119
|
+
'conf_v1': round(c1, 2), 'conf_v2': round(c2, 2)})
|
|
120
|
+
continue
|
|
121
|
+
if sh1 == sh2:
|
|
122
|
+
rejected.append({'feature': f, 'reason': 'NOT_CONFIRMED',
|
|
123
|
+
'shape_v1': sh1, 'shape_v2': sh2,
|
|
124
|
+
'conf_v1': round(c1, 2), 'conf_v2': round(c2, 2)})
|
|
125
|
+
continue
|
|
126
|
+
confirmed.append({'feature': f, 'shape_v1': sh1, 'shape_v2': sh2,
|
|
127
|
+
'conf_v1': round(c1, 2), 'conf_v2': round(c2, 2),
|
|
128
|
+
'amp_v1': round(a1, 5), 'amp_v2': round(a2, 5)})
|
|
129
|
+
|
|
130
|
+
score = max(0, 100 - len(confirmed) * 15)
|
|
131
|
+
return {
|
|
132
|
+
'divergences': confirmed,
|
|
133
|
+
'screened': [f for f, _, _ in screened],
|
|
134
|
+
'rejected': rejected,
|
|
135
|
+
'n_screened': len(screened),
|
|
136
|
+
'n_confirmed': len(confirmed),
|
|
137
|
+
'n_features': len(feature_names),
|
|
138
|
+
'health_score': score,
|
|
139
|
+
'method': 'ALE + bootstrap seletivo',
|
|
140
|
+
'bootstrap_calls': len(screened) * self.n_boot * 2,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
def report(self, r: dict) -> str:
|
|
144
|
+
L = [f"SelectiveALEDetector ({r['method']}):",
|
|
145
|
+
f" Triagem: {r['n_screened']}/{r['n_features']} suspeitas → "
|
|
146
|
+
f"confirmadas: {r['n_confirmed']}",
|
|
147
|
+
f" Chamadas de bootstrap: {r['bootstrap_calls']} "
|
|
148
|
+
f"(vs {r['n_features'] * 24} do bootstrap total)",
|
|
149
|
+
f" Health Score: {r['health_score']}/100"]
|
|
150
|
+
for d in r['divergences']:
|
|
151
|
+
L.append(f" ⚠ '{d['feature']}': {d['shape_v1']} → {d['shape_v2']} "
|
|
152
|
+
f"(conf {d['conf_v1']:.0%}/{d['conf_v2']:.0%})")
|
|
153
|
+
for x in r['rejected']:
|
|
154
|
+
L.append(f" ✓ '{x['feature']}' descartada: {x['reason']}")
|
|
155
|
+
if not r['divergences'] and not r['rejected']:
|
|
156
|
+
L.append(" ✅ Nenhuma suspeita na triagem.")
|
|
157
|
+
return "\n".join(L)
|