pycreditools 0.1.0__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.
- pycreditools-0.1.0/.github/workflows/workflow.yaml +34 -0
- pycreditools-0.1.0/.gitignore +38 -0
- pycreditools-0.1.0/LICENSE +21 -0
- pycreditools-0.1.0/PKG-INFO +155 -0
- pycreditools-0.1.0/README.md +113 -0
- pycreditools-0.1.0/pyproject.toml +75 -0
- pycreditools-0.1.0/src/pycreditools/__init__.py +43 -0
- pycreditools-0.1.0/src/pycreditools/_kernels/__init__.py +5 -0
- pycreditools-0.1.0/src/pycreditools/_kernels/iv.py +167 -0
- pycreditools-0.1.0/src/pycreditools/_kernels/tier_metrics.py +103 -0
- pycreditools-0.1.0/src/pycreditools/_kernels/ward.py +155 -0
- pycreditools-0.1.0/src/pycreditools/_parallel.py +32 -0
- pycreditools-0.1.0/src/pycreditools/_types.py +28 -0
- pycreditools-0.1.0/src/pycreditools/analysis.py +96 -0
- pycreditools-0.1.0/src/pycreditools/grouping.py +222 -0
- pycreditools-0.1.0/src/pycreditools/performance.py +141 -0
- pycreditools-0.1.0/src/pycreditools/policy.py +133 -0
- pycreditools-0.1.0/src/pycreditools/py.typed +1 -0
- pycreditools-0.1.0/src/pycreditools/sample_data.py +98 -0
- pycreditools-0.1.0/src/pycreditools/screening.py +224 -0
- pycreditools-0.1.0/src/pycreditools/simulation.py +185 -0
- pycreditools-0.1.0/src/pycreditools/stages.py +175 -0
- pycreditools-0.1.0/src/pycreditools/stress.py +119 -0
- pycreditools-0.1.0/test_script.py +24 -0
- pycreditools-0.1.0/tests/test_grouping.py +36 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
pypi-publish:
|
|
10
|
+
name: Build and publish to PyPI
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment:
|
|
13
|
+
name: pypi
|
|
14
|
+
url: https://pypi.org/p/pycreditools
|
|
15
|
+
permissions:
|
|
16
|
+
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
|
17
|
+
contents: read
|
|
18
|
+
steps:
|
|
19
|
+
- name: Checkout repository
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Set up Python
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.10"
|
|
26
|
+
|
|
27
|
+
- name: Install pypa/build
|
|
28
|
+
run: python -m pip install build
|
|
29
|
+
|
|
30
|
+
- name: Build a binary wheel and a source tarball
|
|
31
|
+
run: python -m build
|
|
32
|
+
|
|
33
|
+
- name: Publish package distributions to PyPI
|
|
34
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
env/
|
|
8
|
+
build/
|
|
9
|
+
develop-eggs/
|
|
10
|
+
dist/
|
|
11
|
+
downloads/
|
|
12
|
+
eggs/
|
|
13
|
+
.eggs/
|
|
14
|
+
lib/
|
|
15
|
+
lib64/
|
|
16
|
+
parts/
|
|
17
|
+
sdist/
|
|
18
|
+
var/
|
|
19
|
+
wheels/
|
|
20
|
+
*.egg-info/
|
|
21
|
+
.installed.cfg
|
|
22
|
+
*.egg
|
|
23
|
+
|
|
24
|
+
# Pytest
|
|
25
|
+
.pytest_cache/
|
|
26
|
+
|
|
27
|
+
# Virtual Environments
|
|
28
|
+
venv/
|
|
29
|
+
.venv/
|
|
30
|
+
env/
|
|
31
|
+
ENV/
|
|
32
|
+
env.bak/
|
|
33
|
+
venv.bak/
|
|
34
|
+
|
|
35
|
+
# IDE
|
|
36
|
+
.idea/
|
|
37
|
+
.vscode/
|
|
38
|
+
*.swp
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matheus Pasche
|
|
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,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pycreditools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Credit Risk Simulation and Policy Optimization — Python edition of creditools
|
|
5
|
+
Project-URL: Homepage, https://github.com/matheuspasche/pycreditools
|
|
6
|
+
Project-URL: Repository, https://github.com/matheuspasche/pycreditools
|
|
7
|
+
Project-URL: Issues, https://github.com/matheuspasche/pycreditools/issues
|
|
8
|
+
Author-email: Matheus Pasche <matheuspasche@outlook.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: credit-risk,credit-scoring,information-value,policy-optimization,risk-management,simulation,ward-clustering
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: numpy>=1.24
|
|
26
|
+
Requires-Dist: pandas>=2.0
|
|
27
|
+
Provides-Extra: accel
|
|
28
|
+
Requires-Dist: numba>=0.58; extra == 'accel'
|
|
29
|
+
Provides-Extra: all
|
|
30
|
+
Requires-Dist: joblib>=1.3; extra == 'all'
|
|
31
|
+
Requires-Dist: numba>=0.58; extra == 'all'
|
|
32
|
+
Requires-Dist: plotly>=5.0; extra == 'all'
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
37
|
+
Provides-Extra: parallel
|
|
38
|
+
Requires-Dist: joblib>=1.3; extra == 'parallel'
|
|
39
|
+
Provides-Extra: viz
|
|
40
|
+
Requires-Dist: plotly>=5.0; extra == 'viz'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
<div align="center">
|
|
44
|
+
<h1>📊 PyCrediTools</h1>
|
|
45
|
+
<p><i>Credit Risk Simulation and Policy Optimization for Python</i></p>
|
|
46
|
+
|
|
47
|
+
[](https://www.python.org/downloads/)
|
|
48
|
+
[](https://opensource.org/licenses/MIT)
|
|
49
|
+
[]()
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
**PyCrediTools** é uma biblioteca de ponta projetada para equipes de Risco de Crédito. Traduzida e evoluída a partir do pacote fundacional em R, ela fornece motores computacionais para **Simulação de Funis de Crédito (Trade-off de Políticas)** e **Agrupamento Autónomo de Risco (Risk Clustering)**.
|
|
55
|
+
|
|
56
|
+
Esqueça as aproximações por tentativas e erros. Com o PyCrediTools, você pode testar cortes de score contra taxas de aprovação, encontrar a política que maximiza a receita mantendo a inadimplência dentro do apetite ao risco, e recriar dinamicamente as faixas de Rating de forma matematicamente ótima.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 🚀 Instalação
|
|
61
|
+
|
|
62
|
+
Atualmente em fase final de testes, o pacote pode ser instalado diretamente do GitHub:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install git+https://github.com/matheuspasche/pycreditools.git
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
*(Em breve estará disponível no PyPI via `pip install pycreditools`)*
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 💡 Core Features
|
|
73
|
+
|
|
74
|
+
- **Credit Policy Simulation**: Monte estágios rigorosos (Filtros duros, Regras de Corte de Score, Probabilidades Variáveis) e estresse a carteira sob diferentes condições económicas (agravamentos macro, declínios monotónicos).
|
|
75
|
+
- **Automated Risk Clustering**: Agrupe milhares de combinações de scores numa arquitetura compacta de "Ratings de Risco". O algoritmo respeita limitações de negócio rigorosas (Tolerância a inversão de safra, Exigência Mínima de Volume).
|
|
76
|
+
- **Distance Linkage Engine**: (Novo!) Uma evolução do algoritmo Ward tradicional que prioriza a simetria orgânica e o distanciamento da probabilidade de inadimplência em vez da densidade volumétrica da carteira.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 📖 Quickstart (Exemplo de Uso)
|
|
81
|
+
|
|
82
|
+
O uso típico envolve duas fases: Simular a política para gerar a "População Aprovada", e depois agrupar essa população em Ratings estruturais.
|
|
83
|
+
|
|
84
|
+
### 1. Simulação do Funil
|
|
85
|
+
```python
|
|
86
|
+
import pandas as pd
|
|
87
|
+
from pycreditools.policy import CreditPolicy
|
|
88
|
+
from pycreditools.stages import CutoffStage
|
|
89
|
+
from pycreditools.simulation import simulate_policy
|
|
90
|
+
|
|
91
|
+
# Carregar Dados (O seu histórico de propostas e performance real)
|
|
92
|
+
df = pd.read_csv("minha_base.csv")
|
|
93
|
+
|
|
94
|
+
# Criar a Política
|
|
95
|
+
policy = (
|
|
96
|
+
CreditPolicy(score_cols=["meu_score_novo"], actual_default_col="inadimplencia")
|
|
97
|
+
.add_stage(CutoffStage("Aprovacao_Score", cutoffs={"meu_score_novo": 650}))
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Simular aprovação
|
|
101
|
+
df_simulado = simulate_policy(df, policy)
|
|
102
|
+
df_aprovados = df_simulado[df_simulado["_approved"]]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 2. Agrupamento Ótimo de Risco (Clustering)
|
|
106
|
+
Vamos pedir ao motor que encontre o número **ótimo** de curvas de risco (até um máximo de 5 grupos), garantindo que **nunca se cruzam no tempo** (`max_crossings=0`).
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from pycreditools.grouping import find_risk_groups
|
|
110
|
+
|
|
111
|
+
clustering = find_risk_groups(
|
|
112
|
+
df_aprovados,
|
|
113
|
+
score_cols="meu_score_novo",
|
|
114
|
+
default_col="inadimplencia",
|
|
115
|
+
time_col="safra_mes", # Para matriz de temporalidade
|
|
116
|
+
bins=20, # Granularidade da pesquisa
|
|
117
|
+
max_groups=5, # Teto Máximo
|
|
118
|
+
method="distance", # Heurística pura de Distância
|
|
119
|
+
max_crossings=0, # Tolerância Zero a inversão de curvas
|
|
120
|
+
min_vol_ratio=0.05 # Cada Rating deve ter >5% do volume
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Aplicar o modelo (nova coluna "risk_rating" gerada)
|
|
124
|
+
df_final = clustering.predict(df_aprovados)
|
|
125
|
+
|
|
126
|
+
print(f"O algoritmo agrupou os scores em {clustering.n_groups} Ratings de Risco Perfeitos.")
|
|
127
|
+
print(df_final.groupby("risk_rating")["inadimplencia"].mean())
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 🧠 Algoritmos de Agrupamento
|
|
133
|
+
|
|
134
|
+
Ao invocar o `find_risk_groups`, o motor aceita dois métodos principais (`method="ward"` ou `method="distance"`):
|
|
135
|
+
|
|
136
|
+
### Ward Method Tradicional (`method="ward"`)
|
|
137
|
+
Pesquisa aglomerativa que funde micro-faixas usando o critério de variância espacial (Ward). Este método tende a produzir faixas de risco **igualmente densas** em termos de volume (Ratings com 20% do volume cada, mesmo que o risco não esteja bem distribuído).
|
|
138
|
+
|
|
139
|
+
### Distance Linkage Autónomo (`method="distance"`)
|
|
140
|
+
Um critério de custo inovador que ignora o volume na hora de medir as pontes matemáticas, penalizando unicamente o `(Risco 1 - Risco 2)^2`. A consequência brilhante disto é que os Ratings finais ficam distribuídos pelas faixas de probabilidade com **distâncias perfeitamente equidistantes**, independentemente se um Rating ficar com 30% da carteira e outro com 8%. Ideal para mapas visuais limpos e estabilidade de risco orgânica.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 🛠️ Contribuir e Desenvolver
|
|
145
|
+
|
|
146
|
+
Para correr a suite de testes e submeter pull requests:
|
|
147
|
+
```bash
|
|
148
|
+
git clone https://github.com/matheuspasche/pycreditools.git
|
|
149
|
+
cd pycreditools
|
|
150
|
+
pip install -e .[dev]
|
|
151
|
+
pytest tests/
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## 📜 Licença
|
|
155
|
+
Distribuído sob licença MIT. Desenvolvido para a engenharia financeira moderna.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>📊 PyCrediTools</h1>
|
|
3
|
+
<p><i>Credit Risk Simulation and Policy Optimization for Python</i></p>
|
|
4
|
+
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[]()
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
**PyCrediTools** é uma biblioteca de ponta projetada para equipes de Risco de Crédito. Traduzida e evoluída a partir do pacote fundacional em R, ela fornece motores computacionais para **Simulação de Funis de Crédito (Trade-off de Políticas)** e **Agrupamento Autónomo de Risco (Risk Clustering)**.
|
|
13
|
+
|
|
14
|
+
Esqueça as aproximações por tentativas e erros. Com o PyCrediTools, você pode testar cortes de score contra taxas de aprovação, encontrar a política que maximiza a receita mantendo a inadimplência dentro do apetite ao risco, e recriar dinamicamente as faixas de Rating de forma matematicamente ótima.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 🚀 Instalação
|
|
19
|
+
|
|
20
|
+
Atualmente em fase final de testes, o pacote pode ser instalado diretamente do GitHub:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install git+https://github.com/matheuspasche/pycreditools.git
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
*(Em breve estará disponível no PyPI via `pip install pycreditools`)*
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 💡 Core Features
|
|
31
|
+
|
|
32
|
+
- **Credit Policy Simulation**: Monte estágios rigorosos (Filtros duros, Regras de Corte de Score, Probabilidades Variáveis) e estresse a carteira sob diferentes condições económicas (agravamentos macro, declínios monotónicos).
|
|
33
|
+
- **Automated Risk Clustering**: Agrupe milhares de combinações de scores numa arquitetura compacta de "Ratings de Risco". O algoritmo respeita limitações de negócio rigorosas (Tolerância a inversão de safra, Exigência Mínima de Volume).
|
|
34
|
+
- **Distance Linkage Engine**: (Novo!) Uma evolução do algoritmo Ward tradicional que prioriza a simetria orgânica e o distanciamento da probabilidade de inadimplência em vez da densidade volumétrica da carteira.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 📖 Quickstart (Exemplo de Uso)
|
|
39
|
+
|
|
40
|
+
O uso típico envolve duas fases: Simular a política para gerar a "População Aprovada", e depois agrupar essa população em Ratings estruturais.
|
|
41
|
+
|
|
42
|
+
### 1. Simulação do Funil
|
|
43
|
+
```python
|
|
44
|
+
import pandas as pd
|
|
45
|
+
from pycreditools.policy import CreditPolicy
|
|
46
|
+
from pycreditools.stages import CutoffStage
|
|
47
|
+
from pycreditools.simulation import simulate_policy
|
|
48
|
+
|
|
49
|
+
# Carregar Dados (O seu histórico de propostas e performance real)
|
|
50
|
+
df = pd.read_csv("minha_base.csv")
|
|
51
|
+
|
|
52
|
+
# Criar a Política
|
|
53
|
+
policy = (
|
|
54
|
+
CreditPolicy(score_cols=["meu_score_novo"], actual_default_col="inadimplencia")
|
|
55
|
+
.add_stage(CutoffStage("Aprovacao_Score", cutoffs={"meu_score_novo": 650}))
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Simular aprovação
|
|
59
|
+
df_simulado = simulate_policy(df, policy)
|
|
60
|
+
df_aprovados = df_simulado[df_simulado["_approved"]]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 2. Agrupamento Ótimo de Risco (Clustering)
|
|
64
|
+
Vamos pedir ao motor que encontre o número **ótimo** de curvas de risco (até um máximo de 5 grupos), garantindo que **nunca se cruzam no tempo** (`max_crossings=0`).
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from pycreditools.grouping import find_risk_groups
|
|
68
|
+
|
|
69
|
+
clustering = find_risk_groups(
|
|
70
|
+
df_aprovados,
|
|
71
|
+
score_cols="meu_score_novo",
|
|
72
|
+
default_col="inadimplencia",
|
|
73
|
+
time_col="safra_mes", # Para matriz de temporalidade
|
|
74
|
+
bins=20, # Granularidade da pesquisa
|
|
75
|
+
max_groups=5, # Teto Máximo
|
|
76
|
+
method="distance", # Heurística pura de Distância
|
|
77
|
+
max_crossings=0, # Tolerância Zero a inversão de curvas
|
|
78
|
+
min_vol_ratio=0.05 # Cada Rating deve ter >5% do volume
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Aplicar o modelo (nova coluna "risk_rating" gerada)
|
|
82
|
+
df_final = clustering.predict(df_aprovados)
|
|
83
|
+
|
|
84
|
+
print(f"O algoritmo agrupou os scores em {clustering.n_groups} Ratings de Risco Perfeitos.")
|
|
85
|
+
print(df_final.groupby("risk_rating")["inadimplencia"].mean())
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 🧠 Algoritmos de Agrupamento
|
|
91
|
+
|
|
92
|
+
Ao invocar o `find_risk_groups`, o motor aceita dois métodos principais (`method="ward"` ou `method="distance"`):
|
|
93
|
+
|
|
94
|
+
### Ward Method Tradicional (`method="ward"`)
|
|
95
|
+
Pesquisa aglomerativa que funde micro-faixas usando o critério de variância espacial (Ward). Este método tende a produzir faixas de risco **igualmente densas** em termos de volume (Ratings com 20% do volume cada, mesmo que o risco não esteja bem distribuído).
|
|
96
|
+
|
|
97
|
+
### Distance Linkage Autónomo (`method="distance"`)
|
|
98
|
+
Um critério de custo inovador que ignora o volume na hora de medir as pontes matemáticas, penalizando unicamente o `(Risco 1 - Risco 2)^2`. A consequência brilhante disto é que os Ratings finais ficam distribuídos pelas faixas de probabilidade com **distâncias perfeitamente equidistantes**, independentemente se um Rating ficar com 30% da carteira e outro com 8%. Ideal para mapas visuais limpos e estabilidade de risco orgânica.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 🛠️ Contribuir e Desenvolver
|
|
103
|
+
|
|
104
|
+
Para correr a suite de testes e submeter pull requests:
|
|
105
|
+
```bash
|
|
106
|
+
git clone https://github.com/matheuspasche/pycreditools.git
|
|
107
|
+
cd pycreditools
|
|
108
|
+
pip install -e .[dev]
|
|
109
|
+
pytest tests/
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## 📜 Licença
|
|
113
|
+
Distribuído sob licença MIT. Desenvolvido para a engenharia financeira moderna.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pycreditools"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Credit Risk Simulation and Policy Optimization — Python edition of creditools"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Matheus Pasche", email = "matheuspasche@outlook.com"},
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"credit-risk",
|
|
17
|
+
"simulation",
|
|
18
|
+
"policy-optimization",
|
|
19
|
+
"risk-management",
|
|
20
|
+
"ward-clustering",
|
|
21
|
+
"information-value",
|
|
22
|
+
"credit-scoring",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 3 - Alpha",
|
|
26
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
27
|
+
"Intended Audience :: Science/Research",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3.10",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
"Programming Language :: Python :: 3.13",
|
|
34
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
35
|
+
"Topic :: Office/Business :: Financial",
|
|
36
|
+
"Typing :: Typed",
|
|
37
|
+
]
|
|
38
|
+
dependencies = [
|
|
39
|
+
"pandas>=2.0",
|
|
40
|
+
"numpy>=1.24",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
viz = ["plotly>=5.0"]
|
|
45
|
+
parallel = ["joblib>=1.3"]
|
|
46
|
+
accel = ["numba>=0.58"]
|
|
47
|
+
all = [
|
|
48
|
+
"plotly>=5.0",
|
|
49
|
+
"joblib>=1.3",
|
|
50
|
+
"numba>=0.58",
|
|
51
|
+
]
|
|
52
|
+
dev = [
|
|
53
|
+
"pytest>=7.0",
|
|
54
|
+
"pytest-cov>=4.0",
|
|
55
|
+
"ruff>=0.4",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[project.urls]
|
|
59
|
+
Homepage = "https://github.com/matheuspasche/pycreditools"
|
|
60
|
+
Repository = "https://github.com/matheuspasche/pycreditools"
|
|
61
|
+
Issues = "https://github.com/matheuspasche/pycreditools/issues"
|
|
62
|
+
|
|
63
|
+
[tool.hatch.build.targets.wheel]
|
|
64
|
+
packages = ["src/pycreditools"]
|
|
65
|
+
|
|
66
|
+
[tool.pytest.ini_options]
|
|
67
|
+
testpaths = ["tests"]
|
|
68
|
+
addopts = "-v --tb=short"
|
|
69
|
+
|
|
70
|
+
[tool.ruff]
|
|
71
|
+
target-version = "py310"
|
|
72
|
+
line-length = 100
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint]
|
|
75
|
+
select = ["E", "F", "I", "W", "UP"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pycreditools: A Python library for credit risk policy simulation and analysis.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from ._types import SimulationMethod, ClusteringMethod, Quadrant, StageDirection, PolicySummary
|
|
6
|
+
from .stages import Stage, CutoffStage, FilterStage, RateStage
|
|
7
|
+
from .stress import StressScenario, AggravationStress, MonotonicStress, CustomStress
|
|
8
|
+
from .policy import CreditPolicy
|
|
9
|
+
from .simulation import CreditSimResults, run_simulation
|
|
10
|
+
from .performance import summarize_results, compare_policies
|
|
11
|
+
from .analysis import run_tradeoff_analysis
|
|
12
|
+
from .grouping import find_risk_groups, RiskGroupResult, GroupingRecipe
|
|
13
|
+
from .screening import screen_risk_segments, ScreeningResult, ScreeningRecipe
|
|
14
|
+
from .sample_data import generate_sample_data
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"SimulationMethod",
|
|
18
|
+
"ClusteringMethod",
|
|
19
|
+
"Quadrant",
|
|
20
|
+
"StageDirection",
|
|
21
|
+
"PolicySummary",
|
|
22
|
+
"Stage",
|
|
23
|
+
"CutoffStage",
|
|
24
|
+
"FilterStage",
|
|
25
|
+
"RateStage",
|
|
26
|
+
"StressScenario",
|
|
27
|
+
"AggravationStress",
|
|
28
|
+
"MonotonicStress",
|
|
29
|
+
"CustomStress",
|
|
30
|
+
"CreditPolicy",
|
|
31
|
+
"CreditSimResults",
|
|
32
|
+
"run_simulation",
|
|
33
|
+
"summarize_results",
|
|
34
|
+
"compare_policies",
|
|
35
|
+
"run_tradeoff_analysis",
|
|
36
|
+
"find_risk_groups",
|
|
37
|
+
"RiskGroupResult",
|
|
38
|
+
"GroupingRecipe",
|
|
39
|
+
"screen_risk_segments",
|
|
40
|
+
"ScreeningResult",
|
|
41
|
+
"ScreeningRecipe",
|
|
42
|
+
"generate_sample_data",
|
|
43
|
+
]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
def iv_cluster(
|
|
5
|
+
pd_values: np.ndarray,
|
|
6
|
+
volumes: np.ndarray,
|
|
7
|
+
max_groups: int,
|
|
8
|
+
min_vol_ratio: float,
|
|
9
|
+
lambda_cross: float = 0.5,
|
|
10
|
+
lambda_vol: float = 0.2,
|
|
11
|
+
monthly_vols: np.ndarray | None = None,
|
|
12
|
+
monthly_bads: np.ndarray | None = None,
|
|
13
|
+
) -> np.ndarray:
|
|
14
|
+
"""
|
|
15
|
+
IV-based agglomerative clustering with constraints.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
pd_values: float64[n_bins] - mean PD per bin
|
|
19
|
+
volumes: int64[n_bins] - volume per bin
|
|
20
|
+
max_groups: exact number of output clusters (algorithm will merge down to this)
|
|
21
|
+
min_vol_ratio: min fraction of total volume per cluster
|
|
22
|
+
lambda_cross: penalty weight for vintage crossings
|
|
23
|
+
lambda_vol: penalty weight for PD volatility
|
|
24
|
+
monthly_vols: int64[n_bins, n_months]
|
|
25
|
+
monthly_bads: int64[n_bins, n_months]
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
int64[n_bins] - 1-based group assignments
|
|
29
|
+
"""
|
|
30
|
+
n_bins = len(pd_values)
|
|
31
|
+
if n_bins == 0:
|
|
32
|
+
return np.array([], dtype=np.int64)
|
|
33
|
+
if n_bins <= max_groups and (volumes == 0).sum() == 0:
|
|
34
|
+
# Check if all other constraints hold? Actually if we just want to force merges
|
|
35
|
+
# when constraints are violated, we should still run the loop.
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
active = np.ones(n_bins, dtype=bool)
|
|
39
|
+
current_vol = volumes.copy().astype(np.float64)
|
|
40
|
+
current_bads = (pd_values * current_vol).astype(np.float64)
|
|
41
|
+
|
|
42
|
+
total_vol = current_vol.sum()
|
|
43
|
+
total_bads = current_bads.sum()
|
|
44
|
+
total_goods = total_vol - total_bads
|
|
45
|
+
|
|
46
|
+
if monthly_vols is not None and monthly_bads is not None:
|
|
47
|
+
curr_m_vols = monthly_vols.copy().astype(np.float64)
|
|
48
|
+
curr_m_bads = monthly_bads.copy().astype(np.float64)
|
|
49
|
+
else:
|
|
50
|
+
curr_m_vols = None
|
|
51
|
+
curr_m_bads = None
|
|
52
|
+
|
|
53
|
+
group_ids = np.arange(n_bins)
|
|
54
|
+
n_active = n_bins
|
|
55
|
+
|
|
56
|
+
def calc_iv(bads, vols):
|
|
57
|
+
if total_goods <= 0 or total_bads <= 0:
|
|
58
|
+
return 0.0
|
|
59
|
+
goods = vols - bads
|
|
60
|
+
p_b = bads / total_bads
|
|
61
|
+
p_g = goods / total_goods
|
|
62
|
+
if p_b <= 0 or p_g <= 0:
|
|
63
|
+
return 0.0
|
|
64
|
+
return (p_g - p_b) * np.log(p_g / p_b)
|
|
65
|
+
|
|
66
|
+
while True:
|
|
67
|
+
if n_active <= 1:
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
active_indices = np.where(active)[0]
|
|
71
|
+
n_curr = len(active_indices)
|
|
72
|
+
|
|
73
|
+
min_cost = np.inf
|
|
74
|
+
best_merge_idx = -1
|
|
75
|
+
|
|
76
|
+
for i in range(n_curr - 1):
|
|
77
|
+
idx1 = active_indices[i]
|
|
78
|
+
idx2 = active_indices[i+1]
|
|
79
|
+
|
|
80
|
+
v1 = current_vol[idx1]
|
|
81
|
+
v2 = current_vol[idx2]
|
|
82
|
+
b1 = current_bads[idx1]
|
|
83
|
+
b2 = current_bads[idx2]
|
|
84
|
+
|
|
85
|
+
p1 = b1 / v1 if v1 > 0 else 0.0
|
|
86
|
+
p2 = b2 / v2 if v2 > 0 else 0.0
|
|
87
|
+
|
|
88
|
+
# Hard skip for monotonicity violation unless it's a forced merge
|
|
89
|
+
# Monotonicity violation: p1 >= p2
|
|
90
|
+
violation = (p1 >= p2) and (v1 > 0) and (v2 > 0)
|
|
91
|
+
|
|
92
|
+
# Force merges if volume is 0
|
|
93
|
+
if v1 == 0 or v2 == 0:
|
|
94
|
+
cost = -1e9
|
|
95
|
+
else:
|
|
96
|
+
if violation:
|
|
97
|
+
cost = -1e6 # prioritize fixing monotonicity over normal merges
|
|
98
|
+
else:
|
|
99
|
+
# Calculate IV loss
|
|
100
|
+
iv1 = calc_iv(b1, v1)
|
|
101
|
+
iv2 = calc_iv(b2, v2)
|
|
102
|
+
iv_merged = calc_iv(b1 + b2, v1 + v2)
|
|
103
|
+
iv_loss = iv1 + iv2 - iv_merged
|
|
104
|
+
|
|
105
|
+
cross_penalty = 0.0
|
|
106
|
+
volatility_penalty = 0.0
|
|
107
|
+
|
|
108
|
+
if curr_m_vols is not None and curr_m_bads is not None:
|
|
109
|
+
mv = curr_m_vols[idx1] + curr_m_vols[idx2]
|
|
110
|
+
mb = curr_m_bads[idx1] + curr_m_bads[idx2]
|
|
111
|
+
|
|
112
|
+
valid = mv > 0
|
|
113
|
+
if valid.any():
|
|
114
|
+
mp = mb[valid] / mv[valid]
|
|
115
|
+
volatility_penalty = np.std(mp)
|
|
116
|
+
|
|
117
|
+
# crossings between new merged group and neighbors?
|
|
118
|
+
# To simplify, the C++ IV clustering engine penalizes crossings
|
|
119
|
+
# *within* the merged group (i.e. did the two groups cross each other?)
|
|
120
|
+
mv1 = curr_m_vols[idx1]
|
|
121
|
+
mv2 = curr_m_vols[idx2]
|
|
122
|
+
mb1 = curr_m_bads[idx1]
|
|
123
|
+
mb2 = curr_m_bads[idx2]
|
|
124
|
+
v_valid = (mv1 > 0) & (mv2 > 0)
|
|
125
|
+
if v_valid.any():
|
|
126
|
+
mp1 = mb1[v_valid] / mv1[v_valid]
|
|
127
|
+
mp2 = mb2[v_valid] / mv2[v_valid]
|
|
128
|
+
crossings = np.sum(mp1 >= mp2)
|
|
129
|
+
cross_penalty = crossings
|
|
130
|
+
|
|
131
|
+
cost = iv_loss + lambda_cross * cross_penalty + lambda_vol * volatility_penalty
|
|
132
|
+
|
|
133
|
+
# Force merge if volume below threshold
|
|
134
|
+
if (v1 / total_vol < min_vol_ratio) or (v2 / total_vol < min_vol_ratio):
|
|
135
|
+
cost -= 1000.0 # arbitrary large priority but less than monotonicity
|
|
136
|
+
|
|
137
|
+
if cost < min_cost:
|
|
138
|
+
min_cost = cost
|
|
139
|
+
best_merge_idx = i
|
|
140
|
+
|
|
141
|
+
# Stopping condition
|
|
142
|
+
# If no forced merges are required AND we reached max_groups, stop.
|
|
143
|
+
# Forced merges have cost < -100
|
|
144
|
+
if min_cost >= -100 and n_active <= max_groups:
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
# Execute merge
|
|
148
|
+
idx1 = active_indices[best_merge_idx]
|
|
149
|
+
idx2 = active_indices[best_merge_idx + 1]
|
|
150
|
+
|
|
151
|
+
current_vol[idx1] += current_vol[idx2]
|
|
152
|
+
current_bads[idx1] += current_bads[idx2]
|
|
153
|
+
|
|
154
|
+
if curr_m_vols is not None and curr_m_bads is not None:
|
|
155
|
+
curr_m_vols[idx1] += curr_m_vols[idx2]
|
|
156
|
+
curr_m_bads[idx1] += curr_m_bads[idx2]
|
|
157
|
+
|
|
158
|
+
active[idx2] = False
|
|
159
|
+
group_ids[group_ids == idx2] = idx1
|
|
160
|
+
n_active -= 1
|
|
161
|
+
|
|
162
|
+
# Remap to 1-based sequential integers
|
|
163
|
+
active_indices = np.where(active)[0]
|
|
164
|
+
final_mapping = {old_idx: new_idx for new_idx, old_idx in enumerate(active_indices, 1)}
|
|
165
|
+
|
|
166
|
+
result = np.array([final_mapping[g] for g in group_ids], dtype=np.int64)
|
|
167
|
+
return result
|