shapeaudit 1.0.2__tar.gz
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.
- shapeaudit-1.0.2/LICENSE +21 -0
- shapeaudit-1.0.2/PKG-INFO +180 -0
- shapeaudit-1.0.2/README.md +137 -0
- shapeaudit-1.0.2/modelcheck/__init__.py +59 -0
- shapeaudit-1.0.2/modelcheck/_validate.py +214 -0
- shapeaudit-1.0.2/modelcheck/ale.py +60 -0
- shapeaudit-1.0.2/modelcheck/ale_detector.py +277 -0
- shapeaudit-1.0.2/modelcheck/ale_fast.py +167 -0
- shapeaudit-1.0.2/modelcheck/ale_selective.py +157 -0
- shapeaudit-1.0.2/modelcheck/api.py +835 -0
- shapeaudit-1.0.2/modelcheck/audit.py +477 -0
- shapeaudit-1.0.2/modelcheck/backends.py +184 -0
- shapeaudit-1.0.2/modelcheck/baselines.py +213 -0
- shapeaudit-1.0.2/modelcheck/calibration.py +221 -0
- shapeaudit-1.0.2/modelcheck/canary.py +187 -0
- shapeaudit-1.0.2/modelcheck/causal.py +233 -0
- shapeaudit-1.0.2/modelcheck/causal_bootstrap.py +346 -0
- shapeaudit-1.0.2/modelcheck/ci.py +41 -0
- shapeaudit-1.0.2/modelcheck/cli.py +380 -0
- shapeaudit-1.0.2/modelcheck/compare/__init__.py +0 -0
- shapeaudit-1.0.2/modelcheck/compare/model_comparator.py +47 -0
- shapeaudit-1.0.2/modelcheck/confidence.py +158 -0
- shapeaudit-1.0.2/modelcheck/contradict.py +144 -0
- shapeaudit-1.0.2/modelcheck/dashboard.py +102 -0
- shapeaudit-1.0.2/modelcheck/drift.py +47 -0
- shapeaudit-1.0.2/modelcheck/experimental/__init__.py +28 -0
- shapeaudit-1.0.2/modelcheck/experimental/calibration.py +221 -0
- shapeaudit-1.0.2/modelcheck/experimental/gtsf.py +183 -0
- shapeaudit-1.0.2/modelcheck/experimental/llm.py +393 -0
- shapeaudit-1.0.2/modelcheck/experimental/llm_audit.py +597 -0
- shapeaudit-1.0.2/modelcheck/experimental/rhale.py +229 -0
- shapeaudit-1.0.2/modelcheck/experimental/topology.py +247 -0
- shapeaudit-1.0.2/modelcheck/experimental/vision.py +377 -0
- shapeaudit-1.0.2/modelcheck/experimental/zest.py +155 -0
- shapeaudit-1.0.2/modelcheck/explainers/__init__.py +0 -0
- shapeaudit-1.0.2/modelcheck/explainers/shap_explainer.py +67 -0
- shapeaudit-1.0.2/modelcheck/explorer.py +246 -0
- shapeaudit-1.0.2/modelcheck/fairness.py +120 -0
- shapeaudit-1.0.2/modelcheck/frechet.py +332 -0
- shapeaudit-1.0.2/modelcheck/geometry.py +186 -0
- shapeaudit-1.0.2/modelcheck/gtsf.py +183 -0
- shapeaudit-1.0.2/modelcheck/guard.py +144 -0
- shapeaudit-1.0.2/modelcheck/importance.py +389 -0
- shapeaudit-1.0.2/modelcheck/integrations/__init__.py +0 -0
- shapeaudit-1.0.2/modelcheck/integrations/mlflow_integration.py +40 -0
- shapeaudit-1.0.2/modelcheck/laudo.py +885 -0
- shapeaudit-1.0.2/modelcheck/leakage.py +43 -0
- shapeaudit-1.0.2/modelcheck/leakage_temporal.py +201 -0
- shapeaudit-1.0.2/modelcheck/narrate.py +36 -0
- shapeaudit-1.0.2/modelcheck/ood.py +146 -0
- shapeaudit-1.0.2/modelcheck/orchestrator.py +699 -0
- shapeaudit-1.0.2/modelcheck/paired.py +229 -0
- shapeaudit-1.0.2/modelcheck/pipeline.py +379 -0
- shapeaudit-1.0.2/modelcheck/plots.py +53 -0
- shapeaudit-1.0.2/modelcheck/psi_shap.py +196 -0
- shapeaudit-1.0.2/modelcheck/ranking.py +19 -0
- shapeaudit-1.0.2/modelcheck/report.py +72 -0
- shapeaudit-1.0.2/modelcheck/rhale.py +229 -0
- shapeaudit-1.0.2/modelcheck/rules/__init__.py +0 -0
- shapeaudit-1.0.2/modelcheck/rules/business_rules.py +36 -0
- shapeaudit-1.0.2/modelcheck/rules/rule_learner.py +154 -0
- shapeaudit-1.0.2/modelcheck/score.py +64 -0
- shapeaudit-1.0.2/modelcheck/shape.py +368 -0
- shapeaudit-1.0.2/modelcheck/shape_extensions.py +479 -0
- shapeaudit-1.0.2/modelcheck/shapedist.py +130 -0
- shapeaudit-1.0.2/modelcheck/silence.py +247 -0
- shapeaudit-1.0.2/modelcheck/slope.py +261 -0
- shapeaudit-1.0.2/modelcheck/snapshot.py +429 -0
- shapeaudit-1.0.2/modelcheck/stability.py +153 -0
- shapeaudit-1.0.2/modelcheck/studio.py +124 -0
- shapeaudit-1.0.2/modelcheck/temporal.py +74 -0
- shapeaudit-1.0.2/modelcheck/temporal_shape.py +72 -0
- shapeaudit-1.0.2/modelcheck/topology.py +247 -0
- shapeaudit-1.0.2/modelcheck/trending.py +205 -0
- shapeaudit-1.0.2/modelcheck/universal.py +278 -0
- shapeaudit-1.0.2/modelcheck/validation.py +297 -0
- shapeaudit-1.0.2/modelcheck/zest.py +155 -0
- shapeaudit-1.0.2/modelguard/__init__.py +41 -0
- shapeaudit-1.0.2/pyproject.toml +57 -0
- shapeaudit-1.0.2/setup.cfg +4 -0
- shapeaudit-1.0.2/shapeaudit/__init__.py +39 -0
- shapeaudit-1.0.2/shapeaudit.egg-info/PKG-INFO +180 -0
- shapeaudit-1.0.2/shapeaudit.egg-info/SOURCES.txt +108 -0
- shapeaudit-1.0.2/shapeaudit.egg-info/dependency_links.txt +1 -0
- shapeaudit-1.0.2/shapeaudit.egg-info/entry_points.txt +4 -0
- shapeaudit-1.0.2/shapeaudit.egg-info/requires.txt +25 -0
- shapeaudit-1.0.2/shapeaudit.egg-info/top_level.txt +3 -0
- shapeaudit-1.0.2/tests/test_chaos.py +144 -0
- shapeaudit-1.0.2/tests/test_classes_de_bug.py +272 -0
- shapeaudit-1.0.2/tests/test_comparabilidade.py +418 -0
- shapeaudit-1.0.2/tests/test_consistency.py +487 -0
- shapeaudit-1.0.2/tests/test_edge_cases.py +595 -0
- shapeaudit-1.0.2/tests/test_fairness.py +138 -0
- shapeaudit-1.0.2/tests/test_frechet.py +90 -0
- shapeaudit-1.0.2/tests/test_golden.py +61 -0
- shapeaudit-1.0.2/tests/test_importance_core.py +285 -0
- shapeaudit-1.0.2/tests/test_laudo.py +274 -0
- shapeaudit-1.0.2/tests/test_llm_experimental.py +450 -0
- shapeaudit-1.0.2/tests/test_modelcheck.py +769 -0
- shapeaudit-1.0.2/tests/test_performance.py +61 -0
- shapeaudit-1.0.2/tests/test_pipeline.py +253 -0
- shapeaudit-1.0.2/tests/test_properties.py +142 -0
- shapeaudit-1.0.2/tests/test_public_api.py +285 -0
- shapeaudit-1.0.2/tests/test_regression.py +561 -0
- shapeaudit-1.0.2/tests/test_robustez_snapshot.py +194 -0
- shapeaudit-1.0.2/tests/test_schema_compat.py +158 -0
- shapeaudit-1.0.2/tests/test_shape_core.py +330 -0
- shapeaudit-1.0.2/tests/test_snapshot_cli.py +205 -0
- shapeaudit-1.0.2/tests/test_tutorial.py +234 -0
- shapeaudit-1.0.2/tests/test_vision_experimental.py +159 -0
shapeaudit-1.0.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 modelcheck
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shapeaudit
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: Descubra se o modelo novo aprendeu a mesma coisa que o antigo — auditoria entre duas versões, com laudo em HTML
|
|
5
|
+
Author-email: Riviane Maria Albuquerque Donha <riviane.donha@ufpr.br>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Rivianee/shapeaudit
|
|
8
|
+
Keywords: machine-learning,mlops,model-monitoring,explainability,shap,model-audit,ci-cd
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: shap<0.53,>=0.44
|
|
24
|
+
Requires-Dist: pandas>=2.0.0
|
|
25
|
+
Requires-Dist: numpy>=1.24.0
|
|
26
|
+
Requires-Dist: pyyaml>=6.0
|
|
27
|
+
Requires-Dist: scipy>=1.10
|
|
28
|
+
Requires-Dist: matplotlib>=3.7
|
|
29
|
+
Requires-Dist: scikit-learn>=1.3
|
|
30
|
+
Provides-Extra: mlflow
|
|
31
|
+
Requires-Dist: mlflow-skinny>=2.10; extra == "mlflow"
|
|
32
|
+
Provides-Extra: xgboost
|
|
33
|
+
Requires-Dist: xgboost<3,>=1.7; extra == "xgboost"
|
|
34
|
+
Provides-Extra: lightgbm
|
|
35
|
+
Requires-Dist: lightgbm>=4.0; extra == "lightgbm"
|
|
36
|
+
Provides-Extra: evidently
|
|
37
|
+
Requires-Dist: evidently>=0.7; extra == "evidently"
|
|
38
|
+
Provides-Extra: fairlearn
|
|
39
|
+
Requires-Dist: fairlearn>=0.10; extra == "fairlearn"
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# ShapeAudit
|
|
45
|
+
|
|
46
|
+
**Seu modelo novo tem a mesma acurácia do antigo. Mas será que ele aprendeu a
|
|
47
|
+
mesma coisa?**
|
|
48
|
+
|
|
49
|
+
Toda vez que um modelo é retreinado, alguém precisa decidir se promove a versão
|
|
50
|
+
nova. Na prática, a decisão costuma ser uma só: *a acurácia caiu?* Se não caiu,
|
|
51
|
+
sobe.
|
|
52
|
+
|
|
53
|
+
O problema é que essa pergunta não cobre tudo. Duas versões podem acertar na
|
|
54
|
+
mesma proporção e **usar as variáveis de um jeito diferente** — inclusive uma
|
|
55
|
+
delas ter perdido uma coluna no caminho, sem que a métrica acuse.
|
|
56
|
+
|
|
57
|
+
Um exemplo real, com dados públicos de churn de telecom:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
versão em produção: 0.815
|
|
61
|
+
versão candidata: 0.807
|
|
62
|
+
diferença: 0.008 ← praticamente nada
|
|
63
|
+
|
|
64
|
+
ShapeAudit: NÃO PROMOVA — a variável `Contract` deixou de
|
|
65
|
+
influenciar a previsão e chega constante no treino
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Uma coluna virou constante depois de uma mudança no pipeline. **Um gate por
|
|
69
|
+
métrica aprovaria essa versão.**
|
|
70
|
+
|
|
71
|
+
## Instalar
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install shapeaudit
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Usar
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from shapeaudit import Guard
|
|
81
|
+
|
|
82
|
+
r = Guard().audit(modelo_antigo, modelo_novo, X_avaliacao, y_avaliacao,
|
|
83
|
+
X_train=dados_de_treino_do_modelo_novo)
|
|
84
|
+
|
|
85
|
+
print(r.verdict) # APROVADO | REVISAR | ATENCAO | BLOQUEADO
|
|
86
|
+
print(r.root_cause_features) # ['Contract']
|
|
87
|
+
open('laudo.html', 'w').write(r.to_html()) # laudo para abrir no navegador
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`X_train` é o treino do **modelo novo**. É esse argumento que separa "a coluna
|
|
91
|
+
sumiu do pipeline" de "o modelo deixou de usar a coluna". Sem ele a auditoria
|
|
92
|
+
roda, mas avisa que não verificou isso.
|
|
93
|
+
|
|
94
|
+
## O que você recebe
|
|
95
|
+
|
|
96
|
+
| veredito | o que aconteceu | o que fazer |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| **BLOQUEADO** | uma variável sumiu do modelo e está constante no treino | não promova: verifique o pipeline |
|
|
99
|
+
| **ATENÇÃO** | os dados chegam diferentes do treino, ou uma variável quase idêntica assumiu o lugar de outra | promova com monitoramento, recalibre |
|
|
100
|
+
| **REVISAR** | com os mesmos dados, o modelo novo responde de outro jeito | olhe as curvas antes de promover |
|
|
101
|
+
| **APROVADO** | nenhuma verificação encontrou mudança | pode promover |
|
|
102
|
+
|
|
103
|
+
E um laudo HTML que responde, nesta ordem: o que aconteceu, o que mudou de
|
|
104
|
+
peso e de comportamento, onde exatamente mudou, se isso muda decisões, por onde
|
|
105
|
+
começar a investigar, e **o que não foi verificado**.
|
|
106
|
+
|
|
107
|
+
## Experimentar em dois minutos
|
|
108
|
+
|
|
109
|
+
- **No Colab**, sem instalar nada: abra `ShapeAudit_Colab.ipynb` e rode tudo.
|
|
110
|
+
- **Com uma planilha sua**: `python demo_excel.py meus_dados.xlsx nome_do_alvo`
|
|
111
|
+
|
|
112
|
+
Os dois terminam com o laudo aberto e um caso de pipeline quebrado, para você
|
|
113
|
+
ver os dois extremos.
|
|
114
|
+
|
|
115
|
+
## Três perguntas, três camadas
|
|
116
|
+
|
|
117
|
+
1. **Alguma variável sumiu do modelo?** Compara o peso de cada variável nas
|
|
118
|
+
duas versões e distingue "a coluna sumiu" de "o modelo deixou de usar".
|
|
119
|
+
2. **Os dados chegam como no treino?** Compara o treino do modelo novo com os
|
|
120
|
+
dados que ele vai pontuar.
|
|
121
|
+
3. **O modelo passou a reagir de outro jeito?** Compara a curva de efeito de
|
|
122
|
+
cada variável entre as duas versões. Uma distância detecta a mudança e uma
|
|
123
|
+
classificação em cinco formas a nomeia.
|
|
124
|
+
|
|
125
|
+
O veredito usa só essas três. Extremos, subgrupos e impacto em grupos de
|
|
126
|
+
pessoas aparecem como verificações adicionais.
|
|
127
|
+
|
|
128
|
+
## Calibrar no seu caso
|
|
129
|
+
|
|
130
|
+
O limiar padrão veio de uma bancada sintética. Calibre com retreinos que você
|
|
131
|
+
**teria promovido** — não precisa de rótulo nem de falha conhecida:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from shapeaudit.frechet import calibrar_limiar
|
|
135
|
+
print(calibrar_limiar([(modelo_jan, modelo_fev), ...], X_avaliacao)['limiar'])
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Medido: de 0,12 a 0,47 em seis conjuntos de dados, e o limiar de modelos de
|
|
139
|
+
boosting é cinco a seis vezes o de florestas nos mesmos dados. Calibre por
|
|
140
|
+
domínio **e** por família de modelo.
|
|
141
|
+
|
|
142
|
+
## O que ele não faz
|
|
143
|
+
|
|
144
|
+
- **Não bloqueia degradação parcial:** com até 80% das linhas de uma variável
|
|
145
|
+
corrompidas, o modelo perde 4 pontos de R² e nada dispara.
|
|
146
|
+
- **Em bases pequenas** (menos de ~400 linhas de avaliação) pede revisão em
|
|
147
|
+
cerca de um terço dos retreinos saudáveis.
|
|
148
|
+
- **Fora de árvores** (MLP, ridge) é experimental: bloqueia falha de pipeline,
|
|
149
|
+
mas alarma mais e é cerca de dez vezes mais lento.
|
|
150
|
+
- **Não distingue sazonalidade de mudança real.**
|
|
151
|
+
- **Não diz qual das duas versões está certa** — só que elas discordam.
|
|
152
|
+
|
|
153
|
+
`LIMITATIONS.md` traz cada limite com o número medido.
|
|
154
|
+
|
|
155
|
+
## Confiança no veredito
|
|
156
|
+
|
|
157
|
+
Em 240 controles saudáveis de benchmark, 161 retreinos reais de séries públicas
|
|
158
|
+
do setor elétrico brasileiro e 12 conjuntos públicos, **nenhum retreino saudável
|
|
159
|
+
foi bloqueado**. Quando o pipeline quebrou de verdade, bloqueou 16 de 18 casos
|
|
160
|
+
nomeando a variável.
|
|
161
|
+
|
|
162
|
+
A recíproca não vale: não bloquear não garante que está tudo bem.
|
|
163
|
+
|
|
164
|
+
## Nomes
|
|
165
|
+
|
|
166
|
+
A distribuição é `shapeaudit`. Três nomes de import funcionam, e apontam para
|
|
167
|
+
a mesma implementação:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
import shapeaudit # nome atual
|
|
171
|
+
import modelguard # nome anterior (pacote `modelguard-ml`), mantido
|
|
172
|
+
import modelcheck # nome original, mantido por compatibilidade
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Quem já usava `modelguard-ml` não precisa mudar código: troque a instalação
|
|
176
|
+
para `shapeaudit` e os imports antigos continuam funcionando.
|
|
177
|
+
|
|
178
|
+
## Licença
|
|
179
|
+
|
|
180
|
+
MIT. README em português; a documentação de limites também.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# ShapeAudit
|
|
2
|
+
|
|
3
|
+
**Seu modelo novo tem a mesma acurácia do antigo. Mas será que ele aprendeu a
|
|
4
|
+
mesma coisa?**
|
|
5
|
+
|
|
6
|
+
Toda vez que um modelo é retreinado, alguém precisa decidir se promove a versão
|
|
7
|
+
nova. Na prática, a decisão costuma ser uma só: *a acurácia caiu?* Se não caiu,
|
|
8
|
+
sobe.
|
|
9
|
+
|
|
10
|
+
O problema é que essa pergunta não cobre tudo. Duas versões podem acertar na
|
|
11
|
+
mesma proporção e **usar as variáveis de um jeito diferente** — inclusive uma
|
|
12
|
+
delas ter perdido uma coluna no caminho, sem que a métrica acuse.
|
|
13
|
+
|
|
14
|
+
Um exemplo real, com dados públicos de churn de telecom:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
versão em produção: 0.815
|
|
18
|
+
versão candidata: 0.807
|
|
19
|
+
diferença: 0.008 ← praticamente nada
|
|
20
|
+
|
|
21
|
+
ShapeAudit: NÃO PROMOVA — a variável `Contract` deixou de
|
|
22
|
+
influenciar a previsão e chega constante no treino
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Uma coluna virou constante depois de uma mudança no pipeline. **Um gate por
|
|
26
|
+
métrica aprovaria essa versão.**
|
|
27
|
+
|
|
28
|
+
## Instalar
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install shapeaudit
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usar
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from shapeaudit import Guard
|
|
38
|
+
|
|
39
|
+
r = Guard().audit(modelo_antigo, modelo_novo, X_avaliacao, y_avaliacao,
|
|
40
|
+
X_train=dados_de_treino_do_modelo_novo)
|
|
41
|
+
|
|
42
|
+
print(r.verdict) # APROVADO | REVISAR | ATENCAO | BLOQUEADO
|
|
43
|
+
print(r.root_cause_features) # ['Contract']
|
|
44
|
+
open('laudo.html', 'w').write(r.to_html()) # laudo para abrir no navegador
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`X_train` é o treino do **modelo novo**. É esse argumento que separa "a coluna
|
|
48
|
+
sumiu do pipeline" de "o modelo deixou de usar a coluna". Sem ele a auditoria
|
|
49
|
+
roda, mas avisa que não verificou isso.
|
|
50
|
+
|
|
51
|
+
## O que você recebe
|
|
52
|
+
|
|
53
|
+
| veredito | o que aconteceu | o que fazer |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| **BLOQUEADO** | uma variável sumiu do modelo e está constante no treino | não promova: verifique o pipeline |
|
|
56
|
+
| **ATENÇÃO** | os dados chegam diferentes do treino, ou uma variável quase idêntica assumiu o lugar de outra | promova com monitoramento, recalibre |
|
|
57
|
+
| **REVISAR** | com os mesmos dados, o modelo novo responde de outro jeito | olhe as curvas antes de promover |
|
|
58
|
+
| **APROVADO** | nenhuma verificação encontrou mudança | pode promover |
|
|
59
|
+
|
|
60
|
+
E um laudo HTML que responde, nesta ordem: o que aconteceu, o que mudou de
|
|
61
|
+
peso e de comportamento, onde exatamente mudou, se isso muda decisões, por onde
|
|
62
|
+
começar a investigar, e **o que não foi verificado**.
|
|
63
|
+
|
|
64
|
+
## Experimentar em dois minutos
|
|
65
|
+
|
|
66
|
+
- **No Colab**, sem instalar nada: abra `ShapeAudit_Colab.ipynb` e rode tudo.
|
|
67
|
+
- **Com uma planilha sua**: `python demo_excel.py meus_dados.xlsx nome_do_alvo`
|
|
68
|
+
|
|
69
|
+
Os dois terminam com o laudo aberto e um caso de pipeline quebrado, para você
|
|
70
|
+
ver os dois extremos.
|
|
71
|
+
|
|
72
|
+
## Três perguntas, três camadas
|
|
73
|
+
|
|
74
|
+
1. **Alguma variável sumiu do modelo?** Compara o peso de cada variável nas
|
|
75
|
+
duas versões e distingue "a coluna sumiu" de "o modelo deixou de usar".
|
|
76
|
+
2. **Os dados chegam como no treino?** Compara o treino do modelo novo com os
|
|
77
|
+
dados que ele vai pontuar.
|
|
78
|
+
3. **O modelo passou a reagir de outro jeito?** Compara a curva de efeito de
|
|
79
|
+
cada variável entre as duas versões. Uma distância detecta a mudança e uma
|
|
80
|
+
classificação em cinco formas a nomeia.
|
|
81
|
+
|
|
82
|
+
O veredito usa só essas três. Extremos, subgrupos e impacto em grupos de
|
|
83
|
+
pessoas aparecem como verificações adicionais.
|
|
84
|
+
|
|
85
|
+
## Calibrar no seu caso
|
|
86
|
+
|
|
87
|
+
O limiar padrão veio de uma bancada sintética. Calibre com retreinos que você
|
|
88
|
+
**teria promovido** — não precisa de rótulo nem de falha conhecida:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from shapeaudit.frechet import calibrar_limiar
|
|
92
|
+
print(calibrar_limiar([(modelo_jan, modelo_fev), ...], X_avaliacao)['limiar'])
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Medido: de 0,12 a 0,47 em seis conjuntos de dados, e o limiar de modelos de
|
|
96
|
+
boosting é cinco a seis vezes o de florestas nos mesmos dados. Calibre por
|
|
97
|
+
domínio **e** por família de modelo.
|
|
98
|
+
|
|
99
|
+
## O que ele não faz
|
|
100
|
+
|
|
101
|
+
- **Não bloqueia degradação parcial:** com até 80% das linhas de uma variável
|
|
102
|
+
corrompidas, o modelo perde 4 pontos de R² e nada dispara.
|
|
103
|
+
- **Em bases pequenas** (menos de ~400 linhas de avaliação) pede revisão em
|
|
104
|
+
cerca de um terço dos retreinos saudáveis.
|
|
105
|
+
- **Fora de árvores** (MLP, ridge) é experimental: bloqueia falha de pipeline,
|
|
106
|
+
mas alarma mais e é cerca de dez vezes mais lento.
|
|
107
|
+
- **Não distingue sazonalidade de mudança real.**
|
|
108
|
+
- **Não diz qual das duas versões está certa** — só que elas discordam.
|
|
109
|
+
|
|
110
|
+
`LIMITATIONS.md` traz cada limite com o número medido.
|
|
111
|
+
|
|
112
|
+
## Confiança no veredito
|
|
113
|
+
|
|
114
|
+
Em 240 controles saudáveis de benchmark, 161 retreinos reais de séries públicas
|
|
115
|
+
do setor elétrico brasileiro e 12 conjuntos públicos, **nenhum retreino saudável
|
|
116
|
+
foi bloqueado**. Quando o pipeline quebrou de verdade, bloqueou 16 de 18 casos
|
|
117
|
+
nomeando a variável.
|
|
118
|
+
|
|
119
|
+
A recíproca não vale: não bloquear não garante que está tudo bem.
|
|
120
|
+
|
|
121
|
+
## Nomes
|
|
122
|
+
|
|
123
|
+
A distribuição é `shapeaudit`. Três nomes de import funcionam, e apontam para
|
|
124
|
+
a mesma implementação:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
import shapeaudit # nome atual
|
|
128
|
+
import modelguard # nome anterior (pacote `modelguard-ml`), mantido
|
|
129
|
+
import modelcheck # nome original, mantido por compatibilidade
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Quem já usava `modelguard-ml` não precisa mudar código: troque a instalação
|
|
133
|
+
para `shapeaudit` e os imports antigos continuam funcionando.
|
|
134
|
+
|
|
135
|
+
## Licença
|
|
136
|
+
|
|
137
|
+
MIT. README em português; a documentação de limites também.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from importlib.metadata import version as _v
|
|
3
|
+
try:
|
|
4
|
+
__version__ = _v("shapeaudit")
|
|
5
|
+
except Exception: # instalado pelo nome anterior
|
|
6
|
+
__version__ = _v("modelguard-ml")
|
|
7
|
+
except Exception:
|
|
8
|
+
__version__ = "0.0.0+dev"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from .ale_detector import ALEShapeDetector, ale_shape_profile
|
|
12
|
+
|
|
13
|
+
from .audit import audit
|
|
14
|
+
from .geometry import GeometryDivergenceDetector
|
|
15
|
+
|
|
16
|
+
from .paired import PairedDivergenceAnalyzer
|
|
17
|
+
|
|
18
|
+
from .rhale import RHALEDetector, rhale_profile
|
|
19
|
+
from .zest import ArchitectureAgnosticComparator
|
|
20
|
+
|
|
21
|
+
from .trending import ShapeTrendingDetector
|
|
22
|
+
from .ood import OODBehavioralDetector
|
|
23
|
+
|
|
24
|
+
from .canary import GeometricCanaryDetector
|
|
25
|
+
from .explorer import InteractiveDivergenceExplorer
|
|
26
|
+
|
|
27
|
+
from .topology import TopologicalDriftDetector
|
|
28
|
+
from .stability import SupervisedStabilityAnalyzer
|
|
29
|
+
from .gtsf import GTSF
|
|
30
|
+
|
|
31
|
+
from .orchestrator import ModelGuardOrchestrator
|
|
32
|
+
|
|
33
|
+
from .importance import ImportanceCollapseDetector
|
|
34
|
+
|
|
35
|
+
from .confidence import score_with_ci, compare_scores_ci
|
|
36
|
+
|
|
37
|
+
from .baselines import run_all_baselines
|
|
38
|
+
|
|
39
|
+
from .shapedist import shape_cost, severity, profile_distance
|
|
40
|
+
|
|
41
|
+
from .ale_fast import ale_curve_fast, model_embedding_ale, feature_kind
|
|
42
|
+
|
|
43
|
+
from .universal import UniversalCanaryDetector, TopologicalDriftGudhi
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
from .ale_selective import SelectiveALEDetector
|
|
47
|
+
|
|
48
|
+
from .validation import TrainServingValidator
|
|
49
|
+
|
|
50
|
+
from .guard import ModelGuard
|
|
51
|
+
from .api import Guard, AuditResult
|
|
52
|
+
|
|
53
|
+
from .leakage_temporal import TemporalLeakageDetector
|
|
54
|
+
|
|
55
|
+
from .psi_shap import PSIShapDetector
|
|
56
|
+
|
|
57
|
+
from .backends import importance as feature_importance, BACKENDS
|
|
58
|
+
|
|
59
|
+
from modelcheck import snapshot # noqa: E402,F401
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
_validate.py — Validação de entrada compartilhada (v1.47)
|
|
3
|
+
|
|
4
|
+
BUG 17: sondando a API pública com entradas inválidas, vários detectores
|
|
5
|
+
devolviam resultado NORMAL em vez de erro:
|
|
6
|
+
- `features=[]` -> resultado como se tudo estivesse bem
|
|
7
|
+
- `X` vazio -> frechet_distance = nan, score 100
|
|
8
|
+
- `X` com 1 linha-> score 100
|
|
9
|
+
- `X` como numpy -> AttributeError cru
|
|
10
|
+
- modelo None -> AttributeError cru
|
|
11
|
+
|
|
12
|
+
Um resultado plausível para entrada inválida é a pior falha possível: o
|
|
13
|
+
usuário confia num número que não significa nada.
|
|
14
|
+
"""
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
from typing import List, Optional
|
|
18
|
+
|
|
19
|
+
MIN_ROWS = 10
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def validate_inputs(model_v1, model_v2,
|
|
23
|
+
X, feature_names: Optional[List[str]] = None,
|
|
24
|
+
min_rows: int = MIN_ROWS,
|
|
25
|
+
caller: str = "detector"):
|
|
26
|
+
"""
|
|
27
|
+
Normaliza e valida. Devolve (X_df, feature_names).
|
|
28
|
+
Levanta ValueError/TypeError com instrução acionável.
|
|
29
|
+
"""
|
|
30
|
+
if model_v1 is None or model_v2 is None:
|
|
31
|
+
qual = "model_v1" if model_v1 is None else "model_v2"
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"{caller}: {qual} é None. Passe dois modelos treinados com "
|
|
34
|
+
f"método predict().")
|
|
35
|
+
for nome, m in (("model_v1", model_v1), ("model_v2", model_v2)):
|
|
36
|
+
if not hasattr(m, "predict"):
|
|
37
|
+
raise TypeError(
|
|
38
|
+
f"{caller}: {nome} ({type(m).__name__}) não tem método "
|
|
39
|
+
f"predict(). Passe um modelo treinado.")
|
|
40
|
+
|
|
41
|
+
if X is None:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"{caller}: X é None. Passe um DataFrame com as features de "
|
|
44
|
+
f"avaliação.")
|
|
45
|
+
if not isinstance(X, pd.DataFrame):
|
|
46
|
+
arr = np.asarray(X)
|
|
47
|
+
if arr.ndim != 2:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"{caller}: X deve ser 2D, recebido ndim={arr.ndim}.")
|
|
50
|
+
X = pd.DataFrame(arr, columns=[f"f{i}" for i in range(arr.shape[1])])
|
|
51
|
+
|
|
52
|
+
if len(X) == 0:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"{caller}: X está vazio (0 linhas). Não é possível avaliar.")
|
|
55
|
+
if len(X) < min_rows:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"{caller}: X tem apenas {len(X)} linhas (mínimo {min_rows}). "
|
|
58
|
+
f"Resultados com n menor não são interpretáveis.")
|
|
59
|
+
|
|
60
|
+
if feature_names is None:
|
|
61
|
+
feature_names = list(X.columns)
|
|
62
|
+
if isinstance(feature_names, str):
|
|
63
|
+
feature_names = [feature_names]
|
|
64
|
+
feature_names = list(feature_names)
|
|
65
|
+
if not feature_names:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"{caller}: feature_names está vazio. Passe ao menos uma "
|
|
68
|
+
f"feature, ou None para usar todas as colunas de X.")
|
|
69
|
+
faltando = [f for f in feature_names if f not in X.columns]
|
|
70
|
+
if faltando:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"{caller}: features ausentes em X: {faltando[:5]}. "
|
|
73
|
+
f"Colunas disponíveis: {list(X.columns)[:8]}.")
|
|
74
|
+
return X, feature_names
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ══ Condições que invalidam ou enfraquecem a comparação (v1.71) ══════
|
|
78
|
+
#
|
|
79
|
+
# Encontradas por varredura sistemática depois do caso da imputação. A
|
|
80
|
+
# classe é sempre a mesma: a ferramenta produz um veredito num caso onde
|
|
81
|
+
# o veredito não significa o que o leitor supõe.
|
|
82
|
+
#
|
|
83
|
+
# recusa a comparação não é válida — nenhum número salva
|
|
84
|
+
# aviso é válida mas frágil; o resultado sai com a ressalva
|
|
85
|
+
|
|
86
|
+
N_MINIMO_CONFIAVEL = 60 # abaixo disto a classificação de forma oscila
|
|
87
|
+
N_MINIMO_CLASSE = 20 # abaixo disto nada se conclui sobre a classe
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def natureza_da_saida(model, X):
|
|
91
|
+
"""'classe', 'continuo' ou None se não for possível determinar."""
|
|
92
|
+
import numpy as np
|
|
93
|
+
try:
|
|
94
|
+
p = np.asarray(model.predict(X[:200]), dtype=float)
|
|
95
|
+
except Exception:
|
|
96
|
+
return None
|
|
97
|
+
if p.ndim > 1:
|
|
98
|
+
p = p.ravel()
|
|
99
|
+
p = p[np.isfinite(p)]
|
|
100
|
+
if len(p) == 0:
|
|
101
|
+
return None
|
|
102
|
+
unicos = np.unique(p)
|
|
103
|
+
if len(unicos) <= max(2, len(p) * 0.02) and np.all(unicos == unicos.astype(int)):
|
|
104
|
+
return 'classe'
|
|
105
|
+
return 'continuo'
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def checar_comparabilidade(model_v1, model_v2, X, feature_names=None,
|
|
109
|
+
caller='audit', y_eval=None):
|
|
110
|
+
"""
|
|
111
|
+
Levanta ValueError quando a comparação não é válida; devolve a lista de
|
|
112
|
+
ressalvas quando é válida mas frágil.
|
|
113
|
+
"""
|
|
114
|
+
import numpy as np
|
|
115
|
+
ressalvas = []
|
|
116
|
+
|
|
117
|
+
# ── recusa: ordem de colunas diferente da vista no treino ────────
|
|
118
|
+
# BUG 32 (v1.73): o sklearn recusa `X[colunas_reordenadas]` com
|
|
119
|
+
# "Feature names must be in the same order as they were in fit". Alguma
|
|
120
|
+
# camada engolia esse erro e o veredito saía mesmo assim — apontando a
|
|
121
|
+
# variável ERRADA (corrompemos x0, o laudo dizia x3).
|
|
122
|
+
#
|
|
123
|
+
# Reordenar colunas é acidente comum: um `select` diferente, um join
|
|
124
|
+
# que muda a ordem, um CSV regravado.
|
|
125
|
+
for rotulo, m in (('v1', model_v1), ('v2', model_v2)):
|
|
126
|
+
esperado = getattr(m, 'feature_names_in_', None)
|
|
127
|
+
if esperado is None:
|
|
128
|
+
continue
|
|
129
|
+
recebido = list(X.columns)
|
|
130
|
+
esperado = [str(f) for f in esperado]
|
|
131
|
+
if recebido == esperado:
|
|
132
|
+
continue
|
|
133
|
+
if sorted(recebido) == sorted(esperado):
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"{caller}: as colunas de X estão em ordem diferente da "
|
|
136
|
+
f"usada no treino do modelo {rotulo}. O modelo casa colunas "
|
|
137
|
+
f"por posição, então isto entrega valores trocados e o "
|
|
138
|
+
f"diagnóstico apontaria a variável errada. Reordene com "
|
|
139
|
+
f"X[list(model.feature_names_in_)].")
|
|
140
|
+
faltando = [f for f in esperado if f not in recebido]
|
|
141
|
+
sobrando = [f for f in recebido if f not in esperado]
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"{caller}: as colunas de X não correspondem às do treino do "
|
|
144
|
+
f"modelo {rotulo}. Faltando: {faltando[:4] or '—'}. "
|
|
145
|
+
f"Sobrando: {sobrando[:4] or '—'}.")
|
|
146
|
+
|
|
147
|
+
# ── recusa: naturezas de saída diferentes ────────────────────────
|
|
148
|
+
n1, n2 = natureza_da_saida(model_v1, X), natureza_da_saida(model_v2, X)
|
|
149
|
+
if n1 and n2 and n1 != n2:
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"{caller}: os dois modelos produzem saídas de naturezas "
|
|
152
|
+
f"diferentes ({n1} e {n2}). Um regressor e um classificador não "
|
|
153
|
+
f"são comparáveis — as atribuições estão em unidades distintas e "
|
|
154
|
+
f"qualquer veredito seria sobre a diferença de unidade, não sobre "
|
|
155
|
+
f"o modelo. Compare modelos do mesmo tipo.")
|
|
156
|
+
|
|
157
|
+
# ── aviso: conjunto de avaliação pequeno ─────────────────────────
|
|
158
|
+
if len(X) < N_MINIMO_CONFIAVEL:
|
|
159
|
+
ressalvas.append({
|
|
160
|
+
'layer': 'evaluation size',
|
|
161
|
+
'reason': f'apenas {len(X)} amostras de avaliação',
|
|
162
|
+
'consequence': (f'abaixo de {N_MINIMO_CONFIAVEL} a classificação '
|
|
163
|
+
f'de forma não é estável; o veredito pode mudar '
|
|
164
|
+
f'com outra amostra'),
|
|
165
|
+
'severity': 'alta',
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
# ── aviso: classe rara demais para sustentar conclusão ───────────
|
|
169
|
+
# v1.73: um classificador avaliado com 2 exemplos de uma das classes
|
|
170
|
+
# produzia APROVADO sem ressalva. Com 2 amostras nada pode ser
|
|
171
|
+
# concluído sobre o comportamento naquela classe — e o leitor de um
|
|
172
|
+
# APROVADO supõe que houve verificação.
|
|
173
|
+
if y_eval is not None:
|
|
174
|
+
try:
|
|
175
|
+
vc = pd.Series(y_eval).value_counts()
|
|
176
|
+
if 1 < len(vc) <= 20: # parece classificação
|
|
177
|
+
# o repr de um np.int64 polui a mensagem do usuário
|
|
178
|
+
_r = vc.index[-1]
|
|
179
|
+
rara = _r.item() if hasattr(_r, 'item') else _r
|
|
180
|
+
n_rara = int(vc.iloc[-1])
|
|
181
|
+
if n_rara < N_MINIMO_CLASSE:
|
|
182
|
+
ressalvas.append({
|
|
183
|
+
'layer': 'class representation',
|
|
184
|
+
'reason': (f'a classe {rara!r} tem apenas {n_rara} '
|
|
185
|
+
f'exemplos na avaliação'),
|
|
186
|
+
'consequence': ('nada pode ser concluído sobre o '
|
|
187
|
+
'comportamento do modelo nessa '
|
|
188
|
+
'classe'),
|
|
189
|
+
'severity': 'alta',
|
|
190
|
+
})
|
|
191
|
+
except Exception:
|
|
192
|
+
pass # noqa: lacuna-nao-aplicavel — heurística de aviso
|
|
193
|
+
|
|
194
|
+
# ── aviso: avaliação fora da faixa vista no treino ───────────────
|
|
195
|
+
fora = []
|
|
196
|
+
for m in (model_v1, model_v2):
|
|
197
|
+
faixa = getattr(m, '_modelcheck_train_range', None)
|
|
198
|
+
if faixa:
|
|
199
|
+
for col, (lo, hi) in faixa.items():
|
|
200
|
+
if col in X.columns:
|
|
201
|
+
v = X[col].to_numpy(dtype=float)
|
|
202
|
+
v = v[np.isfinite(v)]
|
|
203
|
+
if len(v) and (v.min() > hi or v.max() < lo):
|
|
204
|
+
fora.append(col)
|
|
205
|
+
if fora:
|
|
206
|
+
ressalvas.append({
|
|
207
|
+
'layer': 'evaluation range',
|
|
208
|
+
'reason': f'avaliação fora da faixa de treino em {sorted(set(fora))[:3]}',
|
|
209
|
+
'consequence': ('os dois modelos estão extrapolando; a "concordância" '
|
|
210
|
+
'entre eles não diz nada sobre o comportamento na '
|
|
211
|
+
'faixa de operação'),
|
|
212
|
+
'severity': 'alta',
|
|
213
|
+
})
|
|
214
|
+
return ressalvas
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""ALE (Accumulated Local Effects) como alternativa ao SHAP no SDD."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ALEProfile:
|
|
7
|
+
"""Resultado de perfil ALE — mesma interface que SHAP values."""
|
|
8
|
+
def __init__(self, values):
|
|
9
|
+
self.values = values # shape (n, p) — compatível com ShapExplainer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def ale_explain(model, X: pd.DataFrame, n_bins: int = 5) -> ALEProfile:
|
|
13
|
+
"""Calcula ALE 1D para todas as features numéricas.
|
|
14
|
+
|
|
15
|
+
ALE mede o efeito local médio da feature em cada bin,
|
|
16
|
+
acumulando para obter o efeito total — robusto a correlações.
|
|
17
|
+
Retorna matriz (n_rows, n_features) compatível com ShapeDivergenceDetector.
|
|
18
|
+
"""
|
|
19
|
+
n, p = X.shape
|
|
20
|
+
ale_matrix = np.zeros((n, p))
|
|
21
|
+
|
|
22
|
+
for j, col in enumerate(X.columns):
|
|
23
|
+
if not pd.api.types.is_numeric_dtype(X[col]):
|
|
24
|
+
continue
|
|
25
|
+
vals = X[col].to_numpy(dtype=float)
|
|
26
|
+
if np.std(vals) == 0:
|
|
27
|
+
continue
|
|
28
|
+
try:
|
|
29
|
+
_, edges = pd.qcut(vals, n_bins, duplicates="drop", retbins=True)
|
|
30
|
+
except ValueError:
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
# efeito local em cada bin
|
|
34
|
+
bin_effects = []
|
|
35
|
+
bin_centers = []
|
|
36
|
+
for i in range(len(edges) - 1):
|
|
37
|
+
lo, hi = edges[i], edges[i + 1]
|
|
38
|
+
mask = (vals >= lo) & (vals <= hi)
|
|
39
|
+
if mask.sum() < 3:
|
|
40
|
+
bin_effects.append(0.0)
|
|
41
|
+
else:
|
|
42
|
+
X_lo = X.copy(); X_lo.iloc[:, j] = lo
|
|
43
|
+
X_hi = X.copy(); X_hi.iloc[:, j] = hi
|
|
44
|
+
eff = (model.predict(X_hi[mask]) -
|
|
45
|
+
model.predict(X_lo[mask])).mean()
|
|
46
|
+
bin_effects.append(float(eff))
|
|
47
|
+
bin_centers.append((lo + hi) / 2)
|
|
48
|
+
|
|
49
|
+
# acumular e centrar
|
|
50
|
+
cumulative = np.cumsum(bin_effects)
|
|
51
|
+
cumulative -= cumulative.mean()
|
|
52
|
+
|
|
53
|
+
# interpolar para cada linha
|
|
54
|
+
for idx in range(n):
|
|
55
|
+
v = vals[idx]
|
|
56
|
+
bin_idx = np.searchsorted(edges[1:-1], v)
|
|
57
|
+
bin_idx = int(np.clip(bin_idx, 0, len(cumulative) - 1))
|
|
58
|
+
ale_matrix[idx, j] = cumulative[bin_idx]
|
|
59
|
+
|
|
60
|
+
return ALEProfile(ale_matrix)
|