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.
Files changed (82) hide show
  1. modelcheck/__init__.py +59 -0
  2. modelcheck/_validate.py +214 -0
  3. modelcheck/ale.py +60 -0
  4. modelcheck/ale_detector.py +277 -0
  5. modelcheck/ale_fast.py +167 -0
  6. modelcheck/ale_selective.py +157 -0
  7. modelcheck/api.py +835 -0
  8. modelcheck/audit.py +477 -0
  9. modelcheck/backends.py +184 -0
  10. modelcheck/baselines.py +213 -0
  11. modelcheck/calibration.py +221 -0
  12. modelcheck/canary.py +187 -0
  13. modelcheck/causal.py +233 -0
  14. modelcheck/causal_bootstrap.py +346 -0
  15. modelcheck/ci.py +41 -0
  16. modelcheck/cli.py +380 -0
  17. modelcheck/compare/__init__.py +0 -0
  18. modelcheck/compare/model_comparator.py +47 -0
  19. modelcheck/confidence.py +158 -0
  20. modelcheck/contradict.py +144 -0
  21. modelcheck/dashboard.py +102 -0
  22. modelcheck/drift.py +47 -0
  23. modelcheck/experimental/__init__.py +28 -0
  24. modelcheck/experimental/calibration.py +221 -0
  25. modelcheck/experimental/gtsf.py +183 -0
  26. modelcheck/experimental/llm.py +393 -0
  27. modelcheck/experimental/llm_audit.py +597 -0
  28. modelcheck/experimental/rhale.py +229 -0
  29. modelcheck/experimental/topology.py +247 -0
  30. modelcheck/experimental/vision.py +377 -0
  31. modelcheck/experimental/zest.py +155 -0
  32. modelcheck/explainers/__init__.py +0 -0
  33. modelcheck/explainers/shap_explainer.py +67 -0
  34. modelcheck/explorer.py +246 -0
  35. modelcheck/fairness.py +120 -0
  36. modelcheck/frechet.py +332 -0
  37. modelcheck/geometry.py +186 -0
  38. modelcheck/gtsf.py +183 -0
  39. modelcheck/guard.py +144 -0
  40. modelcheck/importance.py +389 -0
  41. modelcheck/integrations/__init__.py +0 -0
  42. modelcheck/integrations/mlflow_integration.py +40 -0
  43. modelcheck/laudo.py +882 -0
  44. modelcheck/leakage.py +43 -0
  45. modelcheck/leakage_temporal.py +201 -0
  46. modelcheck/narrate.py +36 -0
  47. modelcheck/ood.py +146 -0
  48. modelcheck/orchestrator.py +699 -0
  49. modelcheck/paired.py +229 -0
  50. modelcheck/pipeline.py +379 -0
  51. modelcheck/plots.py +53 -0
  52. modelcheck/psi_shap.py +196 -0
  53. modelcheck/ranking.py +19 -0
  54. modelcheck/report.py +72 -0
  55. modelcheck/rhale.py +229 -0
  56. modelcheck/rules/__init__.py +0 -0
  57. modelcheck/rules/business_rules.py +36 -0
  58. modelcheck/rules/rule_learner.py +154 -0
  59. modelcheck/score.py +64 -0
  60. modelcheck/shape.py +368 -0
  61. modelcheck/shape_extensions.py +479 -0
  62. modelcheck/shapedist.py +130 -0
  63. modelcheck/silence.py +247 -0
  64. modelcheck/slope.py +261 -0
  65. modelcheck/snapshot.py +429 -0
  66. modelcheck/stability.py +153 -0
  67. modelcheck/studio.py +124 -0
  68. modelcheck/temporal.py +74 -0
  69. modelcheck/temporal_shape.py +72 -0
  70. modelcheck/topology.py +247 -0
  71. modelcheck/trending.py +205 -0
  72. modelcheck/universal.py +278 -0
  73. modelcheck/validation.py +247 -0
  74. modelcheck/zest.py +155 -0
  75. modelguard/__init__.py +41 -0
  76. shapeaudit/__init__.py +39 -0
  77. shapeaudit-1.0.0.dist-info/LICENSE +21 -0
  78. shapeaudit-1.0.0.dist-info/METADATA +179 -0
  79. shapeaudit-1.0.0.dist-info/RECORD +82 -0
  80. shapeaudit-1.0.0.dist-info/WHEEL +5 -0
  81. shapeaudit-1.0.0.dist-info/entry_points.txt +4 -0
  82. 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)