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.
Files changed (110) hide show
  1. shapeaudit-1.0.2/LICENSE +21 -0
  2. shapeaudit-1.0.2/PKG-INFO +180 -0
  3. shapeaudit-1.0.2/README.md +137 -0
  4. shapeaudit-1.0.2/modelcheck/__init__.py +59 -0
  5. shapeaudit-1.0.2/modelcheck/_validate.py +214 -0
  6. shapeaudit-1.0.2/modelcheck/ale.py +60 -0
  7. shapeaudit-1.0.2/modelcheck/ale_detector.py +277 -0
  8. shapeaudit-1.0.2/modelcheck/ale_fast.py +167 -0
  9. shapeaudit-1.0.2/modelcheck/ale_selective.py +157 -0
  10. shapeaudit-1.0.2/modelcheck/api.py +835 -0
  11. shapeaudit-1.0.2/modelcheck/audit.py +477 -0
  12. shapeaudit-1.0.2/modelcheck/backends.py +184 -0
  13. shapeaudit-1.0.2/modelcheck/baselines.py +213 -0
  14. shapeaudit-1.0.2/modelcheck/calibration.py +221 -0
  15. shapeaudit-1.0.2/modelcheck/canary.py +187 -0
  16. shapeaudit-1.0.2/modelcheck/causal.py +233 -0
  17. shapeaudit-1.0.2/modelcheck/causal_bootstrap.py +346 -0
  18. shapeaudit-1.0.2/modelcheck/ci.py +41 -0
  19. shapeaudit-1.0.2/modelcheck/cli.py +380 -0
  20. shapeaudit-1.0.2/modelcheck/compare/__init__.py +0 -0
  21. shapeaudit-1.0.2/modelcheck/compare/model_comparator.py +47 -0
  22. shapeaudit-1.0.2/modelcheck/confidence.py +158 -0
  23. shapeaudit-1.0.2/modelcheck/contradict.py +144 -0
  24. shapeaudit-1.0.2/modelcheck/dashboard.py +102 -0
  25. shapeaudit-1.0.2/modelcheck/drift.py +47 -0
  26. shapeaudit-1.0.2/modelcheck/experimental/__init__.py +28 -0
  27. shapeaudit-1.0.2/modelcheck/experimental/calibration.py +221 -0
  28. shapeaudit-1.0.2/modelcheck/experimental/gtsf.py +183 -0
  29. shapeaudit-1.0.2/modelcheck/experimental/llm.py +393 -0
  30. shapeaudit-1.0.2/modelcheck/experimental/llm_audit.py +597 -0
  31. shapeaudit-1.0.2/modelcheck/experimental/rhale.py +229 -0
  32. shapeaudit-1.0.2/modelcheck/experimental/topology.py +247 -0
  33. shapeaudit-1.0.2/modelcheck/experimental/vision.py +377 -0
  34. shapeaudit-1.0.2/modelcheck/experimental/zest.py +155 -0
  35. shapeaudit-1.0.2/modelcheck/explainers/__init__.py +0 -0
  36. shapeaudit-1.0.2/modelcheck/explainers/shap_explainer.py +67 -0
  37. shapeaudit-1.0.2/modelcheck/explorer.py +246 -0
  38. shapeaudit-1.0.2/modelcheck/fairness.py +120 -0
  39. shapeaudit-1.0.2/modelcheck/frechet.py +332 -0
  40. shapeaudit-1.0.2/modelcheck/geometry.py +186 -0
  41. shapeaudit-1.0.2/modelcheck/gtsf.py +183 -0
  42. shapeaudit-1.0.2/modelcheck/guard.py +144 -0
  43. shapeaudit-1.0.2/modelcheck/importance.py +389 -0
  44. shapeaudit-1.0.2/modelcheck/integrations/__init__.py +0 -0
  45. shapeaudit-1.0.2/modelcheck/integrations/mlflow_integration.py +40 -0
  46. shapeaudit-1.0.2/modelcheck/laudo.py +885 -0
  47. shapeaudit-1.0.2/modelcheck/leakage.py +43 -0
  48. shapeaudit-1.0.2/modelcheck/leakage_temporal.py +201 -0
  49. shapeaudit-1.0.2/modelcheck/narrate.py +36 -0
  50. shapeaudit-1.0.2/modelcheck/ood.py +146 -0
  51. shapeaudit-1.0.2/modelcheck/orchestrator.py +699 -0
  52. shapeaudit-1.0.2/modelcheck/paired.py +229 -0
  53. shapeaudit-1.0.2/modelcheck/pipeline.py +379 -0
  54. shapeaudit-1.0.2/modelcheck/plots.py +53 -0
  55. shapeaudit-1.0.2/modelcheck/psi_shap.py +196 -0
  56. shapeaudit-1.0.2/modelcheck/ranking.py +19 -0
  57. shapeaudit-1.0.2/modelcheck/report.py +72 -0
  58. shapeaudit-1.0.2/modelcheck/rhale.py +229 -0
  59. shapeaudit-1.0.2/modelcheck/rules/__init__.py +0 -0
  60. shapeaudit-1.0.2/modelcheck/rules/business_rules.py +36 -0
  61. shapeaudit-1.0.2/modelcheck/rules/rule_learner.py +154 -0
  62. shapeaudit-1.0.2/modelcheck/score.py +64 -0
  63. shapeaudit-1.0.2/modelcheck/shape.py +368 -0
  64. shapeaudit-1.0.2/modelcheck/shape_extensions.py +479 -0
  65. shapeaudit-1.0.2/modelcheck/shapedist.py +130 -0
  66. shapeaudit-1.0.2/modelcheck/silence.py +247 -0
  67. shapeaudit-1.0.2/modelcheck/slope.py +261 -0
  68. shapeaudit-1.0.2/modelcheck/snapshot.py +429 -0
  69. shapeaudit-1.0.2/modelcheck/stability.py +153 -0
  70. shapeaudit-1.0.2/modelcheck/studio.py +124 -0
  71. shapeaudit-1.0.2/modelcheck/temporal.py +74 -0
  72. shapeaudit-1.0.2/modelcheck/temporal_shape.py +72 -0
  73. shapeaudit-1.0.2/modelcheck/topology.py +247 -0
  74. shapeaudit-1.0.2/modelcheck/trending.py +205 -0
  75. shapeaudit-1.0.2/modelcheck/universal.py +278 -0
  76. shapeaudit-1.0.2/modelcheck/validation.py +297 -0
  77. shapeaudit-1.0.2/modelcheck/zest.py +155 -0
  78. shapeaudit-1.0.2/modelguard/__init__.py +41 -0
  79. shapeaudit-1.0.2/pyproject.toml +57 -0
  80. shapeaudit-1.0.2/setup.cfg +4 -0
  81. shapeaudit-1.0.2/shapeaudit/__init__.py +39 -0
  82. shapeaudit-1.0.2/shapeaudit.egg-info/PKG-INFO +180 -0
  83. shapeaudit-1.0.2/shapeaudit.egg-info/SOURCES.txt +108 -0
  84. shapeaudit-1.0.2/shapeaudit.egg-info/dependency_links.txt +1 -0
  85. shapeaudit-1.0.2/shapeaudit.egg-info/entry_points.txt +4 -0
  86. shapeaudit-1.0.2/shapeaudit.egg-info/requires.txt +25 -0
  87. shapeaudit-1.0.2/shapeaudit.egg-info/top_level.txt +3 -0
  88. shapeaudit-1.0.2/tests/test_chaos.py +144 -0
  89. shapeaudit-1.0.2/tests/test_classes_de_bug.py +272 -0
  90. shapeaudit-1.0.2/tests/test_comparabilidade.py +418 -0
  91. shapeaudit-1.0.2/tests/test_consistency.py +487 -0
  92. shapeaudit-1.0.2/tests/test_edge_cases.py +595 -0
  93. shapeaudit-1.0.2/tests/test_fairness.py +138 -0
  94. shapeaudit-1.0.2/tests/test_frechet.py +90 -0
  95. shapeaudit-1.0.2/tests/test_golden.py +61 -0
  96. shapeaudit-1.0.2/tests/test_importance_core.py +285 -0
  97. shapeaudit-1.0.2/tests/test_laudo.py +274 -0
  98. shapeaudit-1.0.2/tests/test_llm_experimental.py +450 -0
  99. shapeaudit-1.0.2/tests/test_modelcheck.py +769 -0
  100. shapeaudit-1.0.2/tests/test_performance.py +61 -0
  101. shapeaudit-1.0.2/tests/test_pipeline.py +253 -0
  102. shapeaudit-1.0.2/tests/test_properties.py +142 -0
  103. shapeaudit-1.0.2/tests/test_public_api.py +285 -0
  104. shapeaudit-1.0.2/tests/test_regression.py +561 -0
  105. shapeaudit-1.0.2/tests/test_robustez_snapshot.py +194 -0
  106. shapeaudit-1.0.2/tests/test_schema_compat.py +158 -0
  107. shapeaudit-1.0.2/tests/test_shape_core.py +330 -0
  108. shapeaudit-1.0.2/tests/test_snapshot_cli.py +205 -0
  109. shapeaudit-1.0.2/tests/test_tutorial.py +234 -0
  110. shapeaudit-1.0.2/tests/test_vision_experimental.py +159 -0
@@ -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)