credit_tools 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.
@@ -0,0 +1,47 @@
1
+ name: docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+ pages: write
11
+ id-token: write
12
+
13
+ concurrency:
14
+ group: pages
15
+ cancel-in-progress: false
16
+
17
+ jobs:
18
+ build:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - uses: astral-sh/setup-uv@v3
24
+
25
+ - name: Install docs dependencies
26
+ run: uv sync --extra docs
27
+
28
+ - name: Build site
29
+ run: uv run jupyter-book build --execute --html --ci
30
+ env:
31
+ BASE_URL: /${{ github.event.repository.name }}
32
+
33
+ - uses: actions/configure-pages@v5
34
+
35
+ - uses: actions/upload-pages-artifact@v3
36
+ with:
37
+ path: _build/html
38
+
39
+ deploy:
40
+ needs: build
41
+ runs-on: ubuntu-latest
42
+ environment:
43
+ name: github-pages
44
+ url: ${{ steps.deployment.outputs.page_url }}
45
+ steps:
46
+ - id: deployment
47
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,43 @@
1
+ name: publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: astral-sh/setup-uv@v3
19
+
20
+ - name: Build package
21
+ run: uv build
22
+
23
+ - uses: actions/upload-artifact@v4
24
+ with:
25
+ name: dist
26
+ path: dist/
27
+
28
+ publish:
29
+ needs: build
30
+ runs-on: ubuntu-latest
31
+ environment:
32
+ name: pypi
33
+ url: https://pypi.org/project/credit_tools/
34
+ steps:
35
+ - uses: actions/download-artifact@v4
36
+ with:
37
+ name: dist
38
+ path: dist/
39
+
40
+ - uses: astral-sh/setup-uv@v3
41
+
42
+ - name: Publish to PyPI
43
+ run: uv publish
@@ -0,0 +1,18 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .uv/
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .coverage
14
+ htmlcov/
15
+ .DS_Store
16
+
17
+ # MyST build outputs
18
+ _build/
@@ -0,0 +1,38 @@
1
+ # Contributing
2
+
3
+ ## Setup
4
+
5
+ ```bash
6
+ uv sync
7
+ ```
8
+
9
+ ## Adding a rating scale resource
10
+
11
+ Rating scales live as plain data in [src/credit_tools/resources/](../src/credit_tools/resources/), one JSON file per scale (e.g. `moody.json`, `fitch.json`), mapping `{rating: expected_default_rate}`. `credit_tools.rating.assign_rating` is independent of any specific agency's scale — it just needs a dict like this. To add a new one, drop `resources/<name>.json` in and load it with `load_rating_scale("<name>")`.
12
+
13
+ ## Docs
14
+
15
+ Examples live as Jupyter notebooks under [docs/](../docs/) and are built with [MyST/Jupyter Book v2](https://mystmd.org), configured by [myst.yml](../myst.yml) at the repo root.
16
+
17
+ ```bash
18
+ uv sync --extra docs
19
+ uv run jupyter-book build --execute --html # static site in _build/html
20
+ uv run jupyter-book start # live preview with hot reload
21
+ ```
22
+
23
+ `docs/` should only contain notebooks (no hand-written markdown/rst pages) — `myst.yml` is the one config file that lives outside it.
24
+
25
+ ### Adding a new notebook
26
+
27
+ 1. Add the `.ipynb` file under `docs/` (or `docs/examples/`).
28
+ 2. Add an entry for it under `project.toc` in `myst.yml`, or regenerate the whole table of contents from what's on disk:
29
+
30
+ ```bash
31
+ uv run jupyter-book init --project --site --write-toc
32
+ ```
33
+
34
+ This only rewrites the `toc:` list — it won't touch metadata you've already filled in (title, description, authors). Run it from the repo root; it refuses to run from inside `docs/`.
35
+
36
+ Docs are rebuilt with `--execute`, so a notebook that no longer runs against the current code will fail CI — that's intentional, it's what keeps the examples honest.
37
+
38
+ Docs deploy to GitHub Pages automatically on push to `main` via [.github/workflows/docs.yml](../.github/workflows/docs.yml).
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.5
2
+ Name: credit_tools
3
+ Version: 0.1.0
4
+ Summary: Out-of-the-box credit functionalities (risk rating, scoring, and more)
5
+ Author-email: Rafael Saraiva <rsa.saraiva94@gmail.com>
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Provides-Extra: docs
9
+ Requires-Dist: jupyter-book>=1.0; extra == 'docs'
10
+ Description-Content-Type: text/markdown
11
+
12
+ # credit_tools
13
+
14
+ Multiple out-of-the-box functionalities for credit, installable via pip/uv.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pip install credit_tools
20
+ # or
21
+ uv add credit_tools
22
+ ```
23
+
24
+ ## Structure
25
+
26
+ ```
27
+ src/credit_tools/
28
+ rating/ # generic borrower-rating assignment (independent of any one scale)
29
+ resources/ # bundled rating scales as data, e.g. resources/moody.json
30
+ ```
31
+
32
+ ## Docs
33
+
34
+ Examples live as Jupyter notebooks in [docs/](docs/), published to GitHub Pages on every push to `main`: https://jumpingdino.github.io/credit_tools/
35
+
36
+ ## Contributing
37
+
38
+ See [CONTRIBUTING/README.md](CONTRIBUTING/README.md) for dev setup, adding rating scale resources, and building/updating the docs.
@@ -0,0 +1,27 @@
1
+ # credit_tools
2
+
3
+ Multiple out-of-the-box functionalities for credit, installable via pip/uv.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install credit_tools
9
+ # or
10
+ uv add credit_tools
11
+ ```
12
+
13
+ ## Structure
14
+
15
+ ```
16
+ src/credit_tools/
17
+ rating/ # generic borrower-rating assignment (independent of any one scale)
18
+ resources/ # bundled rating scales as data, e.g. resources/moody.json
19
+ ```
20
+
21
+ ## Docs
22
+
23
+ Examples live as Jupyter notebooks in [docs/](docs/), published to GitHub Pages on every push to `main`: https://jumpingdino.github.io/credit_tools/
24
+
25
+ ## Contributing
26
+
27
+ See [CONTRIBUTING/README.md](CONTRIBUTING/README.md) for dev setup, adding rating scale resources, and building/updating the docs.
@@ -0,0 +1,130 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Rating assignment\n",
8
+ "\n",
9
+ "`credit_tools.rating.assign_rating` maps borrowers to rating buckets given:\n",
10
+ "\n",
11
+ "- a `borrower_id`, `ml_score`, and observed `defaulted` outcome per borrower, and\n",
12
+ "- a `rating_scale`: a `{rating: expected_default_rate}` dictionary.\n",
13
+ "\n",
14
+ "It fits a monotonic realized-default-rate curve against `ml_score` (higher score = higher risk), then maps each borrower's calibrated default probability to the rating with the closest expected default rate. The scale itself is just data — Moody's idealized default rates ship as a bundled resource, but any `{rating: edr}` mapping works."
15
+ ]
16
+ },
17
+ {
18
+ "cell_type": "code",
19
+ "execution_count": null,
20
+ "metadata": {},
21
+ "outputs": [],
22
+ "source": [
23
+ "import random\n",
24
+ "\n",
25
+ "from credit_tools.rating import assign_rating\n",
26
+ "from credit_tools.resources import load_rating_scale\n",
27
+ "\n",
28
+ "random.seed(0)"
29
+ ]
30
+ },
31
+ {
32
+ "cell_type": "markdown",
33
+ "metadata": {},
34
+ "source": [
35
+ "## Load a rating scale\n",
36
+ "\n",
37
+ "`load_rating_scale` reads any bundled `resources/<name>.json` file."
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "code",
42
+ "execution_count": null,
43
+ "metadata": {},
44
+ "outputs": [],
45
+ "source": [
46
+ "scale = load_rating_scale(\"moody\")\n",
47
+ "dict(list(scale.items())[:5])"
48
+ ]
49
+ },
50
+ {
51
+ "cell_type": "markdown",
52
+ "metadata": {},
53
+ "source": [
54
+ "## Simulate a borrower portfolio\n",
55
+ "\n",
56
+ "In practice `ml_scores` and `defaulted` come from your model's predictions and observed outcomes. Here we simulate them so the notebook is self-contained."
57
+ ]
58
+ },
59
+ {
60
+ "cell_type": "code",
61
+ "execution_count": null,
62
+ "metadata": {},
63
+ "outputs": [],
64
+ "source": [
65
+ "n = 2000\n",
66
+ "borrower_ids = [f\"b{i}\" for i in range(n)]\n",
67
+ "ml_scores = [random.random() for _ in range(n)]\n",
68
+ "defaulted = [random.random() < score for score in ml_scores]"
69
+ ]
70
+ },
71
+ {
72
+ "cell_type": "markdown",
73
+ "metadata": {},
74
+ "source": [
75
+ "## Assign ratings"
76
+ ]
77
+ },
78
+ {
79
+ "cell_type": "code",
80
+ "execution_count": null,
81
+ "metadata": {},
82
+ "outputs": [],
83
+ "source": [
84
+ "ratings = assign_rating(borrower_ids, ml_scores, defaulted, scale)\n",
85
+ "{borrower_ids[i]: ratings[borrower_ids[i]] for i in range(5)}"
86
+ ]
87
+ },
88
+ {
89
+ "cell_type": "markdown",
90
+ "metadata": {},
91
+ "source": [
92
+ "## Check calibration\n",
93
+ "\n",
94
+ "For each rating, the realized default rate among the borrowers assigned to it should track the target expected default rate from the scale."
95
+ ]
96
+ },
97
+ {
98
+ "cell_type": "code",
99
+ "execution_count": null,
100
+ "metadata": {},
101
+ "outputs": [],
102
+ "source": [
103
+ "from collections import defaultdict\n",
104
+ "\n",
105
+ "realized = defaultdict(list)\n",
106
+ "for i, borrower_id in enumerate(borrower_ids):\n",
107
+ " realized[ratings[borrower_id]].append(defaulted[i])\n",
108
+ "\n",
109
+ "for rating in sorted(realized, key=lambda r: scale[r]):\n",
110
+ " outcomes = realized[rating]\n",
111
+ " print(\n",
112
+ " f\"{rating:5s} target_edr={scale[rating]:.4f} n={len(outcomes):4d} \"\n",
113
+ " f\"realized_dr={sum(outcomes) / len(outcomes):.4f}\"\n",
114
+ " )"
115
+ ]
116
+ }
117
+ ],
118
+ "metadata": {
119
+ "kernelspec": {
120
+ "display_name": "Python 3",
121
+ "language": "python",
122
+ "name": "python3"
123
+ },
124
+ "language_info": {
125
+ "name": "python"
126
+ }
127
+ },
128
+ "nbformat": 4,
129
+ "nbformat_minor": 5
130
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# credit_tools\n",
8
+ "\n",
9
+ "Out-of-the-box credit functionalities, installable via pip/uv.\n",
10
+ "\n",
11
+ "```bash\n",
12
+ "pip install credit_tools\n",
13
+ "```\n",
14
+ "\n",
15
+ "See the examples in the sidebar for each functionality, e.g. [rating assignment](examples/rating.ipynb)."
16
+ ]
17
+ },
18
+ {
19
+ "cell_type": "code",
20
+ "execution_count": null,
21
+ "metadata": {},
22
+ "outputs": [],
23
+ "source": [
24
+ "import credit_tools\n",
25
+ "\n",
26
+ "credit_tools.__version__"
27
+ ]
28
+ }
29
+ ],
30
+ "metadata": {
31
+ "kernelspec": {
32
+ "display_name": "Python 3",
33
+ "language": "python",
34
+ "name": "python3"
35
+ },
36
+ "language_info": {
37
+ "name": "python"
38
+ }
39
+ },
40
+ "nbformat": 4,
41
+ "nbformat_minor": 5
42
+ }
@@ -0,0 +1,19 @@
1
+ # See docs at: https://mystmd.org/guide/frontmatter
2
+ version: 1
3
+ project:
4
+ id: ad4e1e45-29ba-4bf5-917d-5f149e4469d5
5
+ title: credit_tools
6
+ description: Out-of-the-box credit functionalities
7
+ authors:
8
+ - name: Rafael Saraiva
9
+ github: https://github.com/JumpingDino/credit_tools
10
+ toc:
11
+ - file: docs/index.ipynb
12
+ - title: Examples
13
+ children:
14
+ - file: docs/examples/rating.ipynb
15
+ site:
16
+ template: book-theme
17
+ # options:
18
+ # favicon: favicon.ico
19
+ # logo: site_logo.png
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "credit_tools"
7
+ version = "0.1.0"
8
+ description = "Out-of-the-box credit functionalities (risk rating, scoring, and more)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Rafael Saraiva", email = "rsa.saraiva94@gmail.com" }]
13
+ dependencies = []
14
+
15
+ [project.optional-dependencies]
16
+ docs = ["jupyter-book>=1.0"]
17
+
18
+ [tool.hatch.build.targets.wheel]
19
+ packages = ["src/credit_tools"]
20
+
21
+ [tool.uv]
22
+ package = true
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("credit_tools")
File without changes
@@ -0,0 +1,3 @@
1
+ from credit_tools.rating.core import assign_rating
2
+
3
+ __all__ = ["assign_rating"]
@@ -0,0 +1,64 @@
1
+ from collections.abc import Sequence
2
+
3
+
4
+ def assign_rating(
5
+ borrower_ids: Sequence[str],
6
+ ml_scores: Sequence[float],
7
+ defaulted: Sequence[bool],
8
+ rating_scale: dict[str, float],
9
+ ) -> dict[str, str]:
10
+ if not (len(borrower_ids) == len(ml_scores) == len(defaulted)):
11
+ raise ValueError("borrower_ids, ml_scores, and defaulted must have the same length")
12
+ if not rating_scale:
13
+ raise ValueError("rating_scale must not be empty")
14
+
15
+ order = sorted(range(len(ml_scores)), key=lambda i: ml_scores[i])
16
+ sorted_scores = [ml_scores[i] for i in order]
17
+ sorted_targets = [float(defaulted[i]) for i in order]
18
+
19
+ calibrated_pd = _isotonic_regression(sorted_scores, sorted_targets)
20
+ ratings_by_edr = sorted(rating_scale.items(), key=lambda item: item[1])
21
+
22
+ assigned: dict[str, str] = {}
23
+ for i, pd in zip(order, calibrated_pd):
24
+ rating, _ = min(ratings_by_edr, key=lambda item: abs(item[1] - pd))
25
+ assigned[borrower_ids[i]] = rating
26
+ return assigned
27
+
28
+
29
+ def _isotonic_regression(sorted_scores: Sequence[float], sorted_targets: Sequence[float]) -> list[float]:
30
+ """Pool-adjacent-violators fit of an increasing curve through (score, target)."""
31
+ unique_scores: list[float] = []
32
+ group_sums: list[float] = []
33
+ group_weights: list[float] = []
34
+ for score, target in zip(sorted_scores, sorted_targets):
35
+ if unique_scores and unique_scores[-1] == score:
36
+ group_sums[-1] += target
37
+ group_weights[-1] += 1
38
+ else:
39
+ unique_scores.append(score)
40
+ group_sums.append(target)
41
+ group_weights.append(1)
42
+
43
+ values = [s / w for s, w in zip(group_sums, group_weights)]
44
+ weights = list(group_weights)
45
+ sizes = [1] * len(values)
46
+
47
+ i = 0
48
+ while i < len(values) - 1:
49
+ if values[i] <= values[i + 1]:
50
+ i += 1
51
+ continue
52
+ merged_weight = weights[i] + weights[i + 1]
53
+ merged_value = (values[i] * weights[i] + values[i + 1] * weights[i + 1]) / merged_weight
54
+ values[i : i + 2] = [merged_value]
55
+ weights[i : i + 2] = [merged_weight]
56
+ sizes[i : i + 2] = [sizes[i] + sizes[i + 1]]
57
+ i = max(i - 1, 0)
58
+
59
+ fitted_by_group: list[float] = []
60
+ for value, size in zip(values, sizes):
61
+ fitted_by_group.extend([value] * size)
62
+
63
+ fitted_by_score = dict(zip(unique_scores, fitted_by_group))
64
+ return [fitted_by_score[score] for score in sorted_scores]
@@ -0,0 +1,8 @@
1
+ import json
2
+ from importlib import resources
3
+
4
+
5
+ def load_rating_scale(name: str) -> dict[str, float]:
6
+ path = resources.files(__package__).joinpath(f"{name}.json")
7
+ with path.open("r", encoding="utf-8") as f:
8
+ return json.load(f)
@@ -0,0 +1,23 @@
1
+ {
2
+ "AAA": 0.0001,
3
+ "AA+": 0.0002,
4
+ "AA": 0.0003,
5
+ "AA-": 0.0004,
6
+ "A+": 0.0006,
7
+ "A": 0.0008,
8
+ "A-": 0.0012,
9
+ "BBB+": 0.0020,
10
+ "BBB": 0.0030,
11
+ "BBB-": 0.0045,
12
+ "BB+": 0.0075,
13
+ "BB": 0.0110,
14
+ "BB-": 0.0170,
15
+ "B+": 0.0270,
16
+ "B": 0.0400,
17
+ "B-": 0.0600,
18
+ "CCC+": 0.0900,
19
+ "CCC": 0.1300,
20
+ "CCC-": 0.1900,
21
+ "CC": 0.3000,
22
+ "C": 0.5000
23
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "Aaa": 0.0001,
3
+ "Aa1": 0.0002,
4
+ "Aa2": 0.0003,
5
+ "Aa3": 0.0004,
6
+ "A1": 0.0006,
7
+ "A2": 0.0008,
8
+ "A3": 0.0012,
9
+ "Baa1": 0.0020,
10
+ "Baa2": 0.0030,
11
+ "Baa3": 0.0045,
12
+ "Ba1": 0.0075,
13
+ "Ba2": 0.0110,
14
+ "Ba3": 0.0170,
15
+ "B1": 0.0270,
16
+ "B2": 0.0400,
17
+ "B3": 0.0600,
18
+ "Caa1": 0.0900,
19
+ "Caa2": 0.1300,
20
+ "Caa3": 0.1900,
21
+ "Ca": 0.3000,
22
+ "C": 0.5000
23
+ }