tide-binning 0.6.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.
- tide_binning-0.6.0/.gitignore +15 -0
- tide_binning-0.6.0/CHANGELOG.md +48 -0
- tide_binning-0.6.0/LICENSE +21 -0
- tide_binning-0.6.0/PKG-INFO +194 -0
- tide_binning-0.6.0/README.md +145 -0
- tide_binning-0.6.0/pyproject.toml +75 -0
- tide_binning-0.6.0/src/tide/__init__.py +10 -0
- tide_binning-0.6.0/src/tide/_bins.py +178 -0
- tide_binning-0.6.0/src/tide/_categorical.py +200 -0
- tide_binning-0.6.0/src/tide/_estimator.py +464 -0
- tide_binning-0.6.0/src/tide/_fit.py +256 -0
- tide_binning-0.6.0/src/tide/_greedy.py +262 -0
- tide_binning-0.6.0/src/tide/_metrics.py +53 -0
- tide_binning-0.6.0/src/tide/_parse.py +228 -0
- tide_binning-0.6.0/src/tide/_stats.py +135 -0
- tide_binning-0.6.0/src/tide/plotting.py +138 -0
- tide_binning-0.6.0/tests/__init__.py +0 -0
- tide_binning-0.6.0/tests/conftest.py +27 -0
- tide_binning-0.6.0/tests/golden/data.npz +0 -0
- tide_binning-0.6.0/tests/golden/expected.json +810 -0
- tide_binning-0.6.0/tests/golden/generate.py +125 -0
- tide_binning-0.6.0/tests/test_algorithms.py +213 -0
- tide_binning-0.6.0/tests/test_estimator.py +378 -0
- tide_binning-0.6.0/tests/test_golden.py +62 -0
- tide_binning-0.6.0/tests/test_parse.py +83 -0
- tide_binning-0.6.0/tests/test_properties.py +95 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.6.0 (2026-10-16)
|
|
4
|
+
|
|
5
|
+
Complete rewrite. The greedy algorithm and its rules are unchanged: on the scenarios in
|
|
6
|
+
`tests/golden`, the bins are identical to v0.5.0.
|
|
7
|
+
|
|
8
|
+
### New
|
|
9
|
+
- scikit-learn estimator: `get_params` / `set_params` / `clone`, `Pipeline`,
|
|
10
|
+
`cross_validate(params={"tide__periods": ...})`, metadata routing,
|
|
11
|
+
`get_feature_names_out`.
|
|
12
|
+
- NaN / None / pd.NA / NaT are supported in features (bins key `None`).
|
|
13
|
+
- `cat_values` and `missing_values` turn numeric codes such as `-999` into categories or
|
|
14
|
+
missing values, globally or per feature.
|
|
15
|
+
- `trend` can be forced (`"pos"` / `"neg"`), globally or per feature.
|
|
16
|
+
- `max_unique` / `n_quantiles` (defaults 1000 / 200): features with more than 1000 unique values
|
|
17
|
+
are split at 200 quantiles instead of every unique value. This is faster, and with many
|
|
18
|
+
periods the exact search often finds no split at all. Pass a large `max_unique` for v0.5 behaviour.
|
|
19
|
+
- `user_bins`: apply your own bins (any shape), with a coverage warning.
|
|
20
|
+
- `summary_` table; a warning when a feature collapses to one continuous bin.
|
|
21
|
+
- `n_jobs` (parallel over features) and `verbose` (progress bar).
|
|
22
|
+
- `plot()` returns the figures and accepts `show=False`.
|
|
23
|
+
- Periods may be any sortable type and need not be sorted.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- `min_er_delta` is now also guaranteed for the last bin (it is merged otherwise).
|
|
27
|
+
- `min_er_delta` no longer applies to the first bin (v0.5 required its event rate ≥ delta).
|
|
28
|
+
- With `worst_bin` / `best_bin`, no split is made between the continuous part and the
|
|
29
|
+
attached missing values.
|
|
30
|
+
- `cat_strategy="tide"` now guarantees all constraints: continuous bins merge only with
|
|
31
|
+
adjacent ones (their intervals become one), and their monotonicity is re-checked.
|
|
32
|
+
The missing bin takes part in the merge.
|
|
33
|
+
- `transform` no longer mistakes values equal to a WoE for unknown values.
|
|
34
|
+
- `stats_per` no longer duplicates rows on refit.
|
|
35
|
+
- `"col_min"` / `"col_max"` bounds now use the next representable float instead of
|
|
36
|
+
`± 1e-8`, which failed for large values.
|
|
37
|
+
|
|
38
|
+
### Changed (breaking)
|
|
39
|
+
- WoE uses a symmetric epsilon, `ln((%nonevents + 1e-8) / (%events + 1e-8))`: a bin without
|
|
40
|
+
non-events no longer gets WoE = -inf and IV = inf. Other bins are unchanged up to ~1e-8.
|
|
41
|
+
- `fit(X, y, per)` → `fit(X, y, periods)`; `reset` and `return_bins` were removed.
|
|
42
|
+
- `handle_unknown` moved from `transform()` into the constructor (`set_params` changes it).
|
|
43
|
+
- `transform` requires the same columns as `fit`; `xnames` and ndarray input were removed.
|
|
44
|
+
- Attributes: `exog_bins` → `bins_`, `exog_woes` → `woes_`, `stats` → `stats_`,
|
|
45
|
+
`stats_per` → `stats_per_`; the stats tables have a new `bin_idx` column.
|
|
46
|
+
- `missing_categories` → `missing_values`; `n_prebins` → `n_trend_bins`.
|
|
47
|
+
- Bin labels (stats tables and plots) show integer bounds without decimals (`(18,25]`) and
|
|
48
|
+
other bounds with exactly 4 decimals (`(0.3400,1.1130]`). `bins_` keeps the full values.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Leonid Garin
|
|
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,194 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tide-binning
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: TIDE - Time Invariant Discretization Engine: time-stable binning for credit scorecards
|
|
5
|
+
Project-URL: Homepage, https://github.com/leonidgarin/TIDE
|
|
6
|
+
Project-URL: Issues, https://github.com/leonidgarin/TIDE/issues
|
|
7
|
+
Author-email: Leonid Garin <leonidgarin97@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: binning,credit risk,information value,psi,scorecard,weight of evidence,woe
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
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 :: Scientific/Engineering :: Information Analysis
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: joblib>=1.3
|
|
24
|
+
Requires-Dist: matplotlib>=3.7
|
|
25
|
+
Requires-Dist: numpy>=1.23
|
|
26
|
+
Requires-Dist: pandas>=2.0
|
|
27
|
+
Requires-Dist: scikit-learn>=1.4
|
|
28
|
+
Requires-Dist: scipy>=1.9
|
|
29
|
+
Requires-Dist: tqdm>=4.60
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: hypothesis>=6.80; extra == 'dev'
|
|
33
|
+
Requires-Dist: mkdocs-material>=9.5; extra == 'dev'
|
|
34
|
+
Requires-Dist: mkdocs<2,>=1.6; extra == 'dev'
|
|
35
|
+
Requires-Dist: mkdocstrings[python]>=0.25; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-cov>=4; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
38
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
39
|
+
Requires-Dist: twine>=5; extra == 'dev'
|
|
40
|
+
Provides-Extra: docs
|
|
41
|
+
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
|
|
42
|
+
Requires-Dist: mkdocs<2,>=1.6; extra == 'docs'
|
|
43
|
+
Requires-Dist: mkdocstrings[python]>=0.25; extra == 'docs'
|
|
44
|
+
Provides-Extra: test
|
|
45
|
+
Requires-Dist: hypothesis>=6.80; extra == 'test'
|
|
46
|
+
Requires-Dist: pytest-cov>=4; extra == 'test'
|
|
47
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
48
|
+
Description-Content-Type: text/markdown
|
|
49
|
+
|
|
50
|
+
# TIDE — Time Invariant Discretization Engine
|
|
51
|
+
|
|
52
|
+
[](https://github.com/leonidgarin/TIDE/actions/workflows/ci.yml)
|
|
53
|
+
[](https://pypi.org/project/tide-binning/)
|
|
54
|
+
[](https://pypi.org/project/tide-binning/)
|
|
55
|
+
[](LICENSE)
|
|
56
|
+
|
|
57
|
+
TIDE bins features for credit scorecards so that the bins are **stable over time**.
|
|
58
|
+
In every period of the training data, as hard constraints:
|
|
59
|
+
|
|
60
|
+
- the event rate is **monotonic** over the bins, in the **same direction** in all periods,
|
|
61
|
+
so WoE / event-rate curves of different bins never cross;
|
|
62
|
+
- adjacent bins differ in event rate by at least `min_er_delta`;
|
|
63
|
+
- every bin holds at least `min_sample_rate` of the period's rows and at least
|
|
64
|
+
`min_class_obs` events and non-events.
|
|
65
|
+
|
|
66
|
+
It works on **mixed-type** columns, e.g. `age = [18, 44, np.nan, "special_category", -999]`,
|
|
67
|
+
and encodes them with Weight of Evidence. It is a scikit-learn transformer.
|
|
68
|
+
|
|
69
|
+
## Installation
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install tide-binning
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Requires Python ≥ 3.10. All dependencies (numpy, pandas, scipy, scikit-learn, matplotlib,
|
|
76
|
+
joblib, tqdm) ship prebuilt wheels for Windows, macOS and Linux.
|
|
77
|
+
|
|
78
|
+
## Quick start
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import pandas as pd
|
|
82
|
+
from sklearn.linear_model import LogisticRegression
|
|
83
|
+
from sklearn.metrics import roc_auc_score
|
|
84
|
+
from tide import TIDE
|
|
85
|
+
|
|
86
|
+
X = pd.DataFrame(...) # features, may be mixed-type
|
|
87
|
+
y = pd.Series(...) # binary target, 0/1
|
|
88
|
+
periods = pd.Series(...) # e.g. application month: str, datetime, Period, int, ...
|
|
89
|
+
|
|
90
|
+
binner = TIDE(cat_values=[-999]) # -999 is a category, not a number
|
|
91
|
+
X_woe = binner.fit_transform(X, y, periods=periods)
|
|
92
|
+
|
|
93
|
+
lr = LogisticRegression().fit(X_woe, y)
|
|
94
|
+
print(roc_auc_score(y, lr.predict_proba(X_woe)[:, 1]))
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Results
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
binner.summary_ # one row per feature: trend, n_bins, IV, max PSI, warnings
|
|
101
|
+
binner.stats_ # per feature and bin: n_obs, event rate, WoE, IV
|
|
102
|
+
binner.stats_per_ # per period, feature and bin: n_obs, event rate, PSI
|
|
103
|
+
binner.bins_ # fitted bins (format below)
|
|
104
|
+
binner.woes_ # {feature: {bin_idx: WoE}}
|
|
105
|
+
binner.plot(["age"]) # bin sizes and event rates, event rate over time, PSI over time
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Bins format
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
{"trend": "pos",
|
|
112
|
+
"bins": {0: {(-np.inf, 25.0): "cont", "special_category": "cat"},
|
|
113
|
+
1: {(25.0, 40.0): "cont"},
|
|
114
|
+
2: {(40.0, np.inf): "cont"},
|
|
115
|
+
3: {None: "missing"}}} # None = NaN / None / pd.NA / NaT
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Intervals are `(a, b]` for a positive trend and `[a, b)` for a negative trend, so a value
|
|
119
|
+
that is unseen in training and lies between two borders always goes to the bin with the
|
|
120
|
+
**higher** event rate.
|
|
121
|
+
|
|
122
|
+
## Handling special values
|
|
123
|
+
|
|
124
|
+
| You have | Use |
|
|
125
|
+
|---|---|
|
|
126
|
+
| NaN, None, pd.NA, NaT | missing automatically |
|
|
127
|
+
| strings, bools | categories automatically |
|
|
128
|
+
| numeric codes such as `-999` that mean "no data" | `missing_values=[-999]` |
|
|
129
|
+
| numeric codes that are a category of their own | `cat_values=[-999]` |
|
|
130
|
+
| different codes per feature | `{"age": [-1], "income": [-999]}` |
|
|
131
|
+
|
|
132
|
+
`missing_strategy` (`"separate_bin"`, `"worst_bin"`, `"best_bin"`) and `cat_strategy`
|
|
133
|
+
(`"separate_bin"`, `"chi-squared"`, `"tide"`) control how they are binned. With
|
|
134
|
+
`cat_strategy="tide"`, all bins — continuous, categorical and missing — are merged until the
|
|
135
|
+
stability constraints hold for all of them.
|
|
136
|
+
|
|
137
|
+
## Your own bins
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
binner = TIDE(user_bins={"age": {"trend": "pos", "bins": {
|
|
141
|
+
0: {(-np.inf, 25.0): "cont"},
|
|
142
|
+
1: {(25.0, 60.0): "cont"},
|
|
143
|
+
2: {(60.0, np.inf): "cont", None: "missing"},
|
|
144
|
+
}}})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
User bins are applied as given (U-shapes are fine); a warning lists training values that
|
|
148
|
+
fall in no bin.
|
|
149
|
+
|
|
150
|
+
## In production
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
import pickle
|
|
154
|
+
binner = pickle.load(open("binner.pkl", "rb"))
|
|
155
|
+
binner.set_params(handle_unknown="worst") # change how unseen values are treated, no refit
|
|
156
|
+
X_woe = binner.transform(X_new)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`handle_unknown`: `"keep"` (leave the raw value), `"worst"` / `"best"` (lowest / highest
|
|
160
|
+
WoE), `"missing_worst"` / `"missing_best"` (WoE of the missing bin, else lowest / highest).
|
|
161
|
+
|
|
162
|
+
## scikit-learn
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from sklearn.pipeline import Pipeline
|
|
166
|
+
from sklearn.model_selection import cross_validate
|
|
167
|
+
|
|
168
|
+
pipe = Pipeline([("tide", TIDE()), ("lr", LogisticRegression())])
|
|
169
|
+
pipe.fit(X, y, tide__periods=periods)
|
|
170
|
+
cross_validate(pipe, X, y, params={"tide__periods": periods}, scoring="roc_auc")
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`get_params` / `set_params` / `clone`, `get_feature_names_out` and metadata routing
|
|
174
|
+
(`TIDE().set_fit_request(periods=True)`) work as usual.
|
|
175
|
+
|
|
176
|
+
## Performance
|
|
177
|
+
|
|
178
|
+
`fit` works on per-period counts of the sorted unique values, so after one sort per
|
|
179
|
+
feature its cost no longer depends on the number of rows. `n_jobs=-1` fits features in
|
|
180
|
+
parallel. Measured on a 2-core laptop, 5,000,000 rows and 24 periods, per feature:
|
|
181
|
+
|
|
182
|
+
| feature | fit | transform |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| integer, 100 unique values | ~1 s | ~0.5 s |
|
|
185
|
+
| float, 1,000 unique | ~1 s | ~0.5 s |
|
|
186
|
+
| float, all unique (undersampled to 200 quantiles) | ~5 s | ~0.5 s |
|
|
187
|
+
| float, 530k unique (exact search, `max_unique=10**6`) | ~5 s | ~0.5 s |
|
|
188
|
+
| mixed object column (numbers, strings, NaN) | ~3 s | ~2 s |
|
|
189
|
+
|
|
190
|
+
Run `python benchmarks/bench_fit.py` to measure on your machine.
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT © Leonid Garin
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# TIDE — Time Invariant Discretization Engine
|
|
2
|
+
|
|
3
|
+
[](https://github.com/leonidgarin/TIDE/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/tide-binning/)
|
|
5
|
+
[](https://pypi.org/project/tide-binning/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
TIDE bins features for credit scorecards so that the bins are **stable over time**.
|
|
9
|
+
In every period of the training data, as hard constraints:
|
|
10
|
+
|
|
11
|
+
- the event rate is **monotonic** over the bins, in the **same direction** in all periods,
|
|
12
|
+
so WoE / event-rate curves of different bins never cross;
|
|
13
|
+
- adjacent bins differ in event rate by at least `min_er_delta`;
|
|
14
|
+
- every bin holds at least `min_sample_rate` of the period's rows and at least
|
|
15
|
+
`min_class_obs` events and non-events.
|
|
16
|
+
|
|
17
|
+
It works on **mixed-type** columns, e.g. `age = [18, 44, np.nan, "special_category", -999]`,
|
|
18
|
+
and encodes them with Weight of Evidence. It is a scikit-learn transformer.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install tide-binning
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requires Python ≥ 3.10. All dependencies (numpy, pandas, scipy, scikit-learn, matplotlib,
|
|
27
|
+
joblib, tqdm) ship prebuilt wheels for Windows, macOS and Linux.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import pandas as pd
|
|
33
|
+
from sklearn.linear_model import LogisticRegression
|
|
34
|
+
from sklearn.metrics import roc_auc_score
|
|
35
|
+
from tide import TIDE
|
|
36
|
+
|
|
37
|
+
X = pd.DataFrame(...) # features, may be mixed-type
|
|
38
|
+
y = pd.Series(...) # binary target, 0/1
|
|
39
|
+
periods = pd.Series(...) # e.g. application month: str, datetime, Period, int, ...
|
|
40
|
+
|
|
41
|
+
binner = TIDE(cat_values=[-999]) # -999 is a category, not a number
|
|
42
|
+
X_woe = binner.fit_transform(X, y, periods=periods)
|
|
43
|
+
|
|
44
|
+
lr = LogisticRegression().fit(X_woe, y)
|
|
45
|
+
print(roc_auc_score(y, lr.predict_proba(X_woe)[:, 1]))
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Results
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
binner.summary_ # one row per feature: trend, n_bins, IV, max PSI, warnings
|
|
52
|
+
binner.stats_ # per feature and bin: n_obs, event rate, WoE, IV
|
|
53
|
+
binner.stats_per_ # per period, feature and bin: n_obs, event rate, PSI
|
|
54
|
+
binner.bins_ # fitted bins (format below)
|
|
55
|
+
binner.woes_ # {feature: {bin_idx: WoE}}
|
|
56
|
+
binner.plot(["age"]) # bin sizes and event rates, event rate over time, PSI over time
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Bins format
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
{"trend": "pos",
|
|
63
|
+
"bins": {0: {(-np.inf, 25.0): "cont", "special_category": "cat"},
|
|
64
|
+
1: {(25.0, 40.0): "cont"},
|
|
65
|
+
2: {(40.0, np.inf): "cont"},
|
|
66
|
+
3: {None: "missing"}}} # None = NaN / None / pd.NA / NaT
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Intervals are `(a, b]` for a positive trend and `[a, b)` for a negative trend, so a value
|
|
70
|
+
that is unseen in training and lies between two borders always goes to the bin with the
|
|
71
|
+
**higher** event rate.
|
|
72
|
+
|
|
73
|
+
## Handling special values
|
|
74
|
+
|
|
75
|
+
| You have | Use |
|
|
76
|
+
|---|---|
|
|
77
|
+
| NaN, None, pd.NA, NaT | missing automatically |
|
|
78
|
+
| strings, bools | categories automatically |
|
|
79
|
+
| numeric codes such as `-999` that mean "no data" | `missing_values=[-999]` |
|
|
80
|
+
| numeric codes that are a category of their own | `cat_values=[-999]` |
|
|
81
|
+
| different codes per feature | `{"age": [-1], "income": [-999]}` |
|
|
82
|
+
|
|
83
|
+
`missing_strategy` (`"separate_bin"`, `"worst_bin"`, `"best_bin"`) and `cat_strategy`
|
|
84
|
+
(`"separate_bin"`, `"chi-squared"`, `"tide"`) control how they are binned. With
|
|
85
|
+
`cat_strategy="tide"`, all bins — continuous, categorical and missing — are merged until the
|
|
86
|
+
stability constraints hold for all of them.
|
|
87
|
+
|
|
88
|
+
## Your own bins
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
binner = TIDE(user_bins={"age": {"trend": "pos", "bins": {
|
|
92
|
+
0: {(-np.inf, 25.0): "cont"},
|
|
93
|
+
1: {(25.0, 60.0): "cont"},
|
|
94
|
+
2: {(60.0, np.inf): "cont", None: "missing"},
|
|
95
|
+
}}})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
User bins are applied as given (U-shapes are fine); a warning lists training values that
|
|
99
|
+
fall in no bin.
|
|
100
|
+
|
|
101
|
+
## In production
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
import pickle
|
|
105
|
+
binner = pickle.load(open("binner.pkl", "rb"))
|
|
106
|
+
binner.set_params(handle_unknown="worst") # change how unseen values are treated, no refit
|
|
107
|
+
X_woe = binner.transform(X_new)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`handle_unknown`: `"keep"` (leave the raw value), `"worst"` / `"best"` (lowest / highest
|
|
111
|
+
WoE), `"missing_worst"` / `"missing_best"` (WoE of the missing bin, else lowest / highest).
|
|
112
|
+
|
|
113
|
+
## scikit-learn
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from sklearn.pipeline import Pipeline
|
|
117
|
+
from sklearn.model_selection import cross_validate
|
|
118
|
+
|
|
119
|
+
pipe = Pipeline([("tide", TIDE()), ("lr", LogisticRegression())])
|
|
120
|
+
pipe.fit(X, y, tide__periods=periods)
|
|
121
|
+
cross_validate(pipe, X, y, params={"tide__periods": periods}, scoring="roc_auc")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`get_params` / `set_params` / `clone`, `get_feature_names_out` and metadata routing
|
|
125
|
+
(`TIDE().set_fit_request(periods=True)`) work as usual.
|
|
126
|
+
|
|
127
|
+
## Performance
|
|
128
|
+
|
|
129
|
+
`fit` works on per-period counts of the sorted unique values, so after one sort per
|
|
130
|
+
feature its cost no longer depends on the number of rows. `n_jobs=-1` fits features in
|
|
131
|
+
parallel. Measured on a 2-core laptop, 5,000,000 rows and 24 periods, per feature:
|
|
132
|
+
|
|
133
|
+
| feature | fit | transform |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| integer, 100 unique values | ~1 s | ~0.5 s |
|
|
136
|
+
| float, 1,000 unique | ~1 s | ~0.5 s |
|
|
137
|
+
| float, all unique (undersampled to 200 quantiles) | ~5 s | ~0.5 s |
|
|
138
|
+
| float, 530k unique (exact search, `max_unique=10**6`) | ~5 s | ~0.5 s |
|
|
139
|
+
| mixed object column (numbers, strings, NaN) | ~3 s | ~2 s |
|
|
140
|
+
|
|
141
|
+
Run `python benchmarks/bench_fit.py` to measure on your machine.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT © Leonid Garin
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.18"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tide-binning"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "TIDE - Time Invariant Discretization Engine: time-stable binning for credit scorecards"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Leonid Garin", email = "leonidgarin97@gmail.com" }]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
keywords = ["binning", "scorecard", "credit risk", "weight of evidence", "woe", "information value", "psi"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"numpy>=1.23",
|
|
30
|
+
"pandas>=2.0",
|
|
31
|
+
"scipy>=1.9",
|
|
32
|
+
"scikit-learn>=1.4",
|
|
33
|
+
"matplotlib>=3.7",
|
|
34
|
+
"joblib>=1.3",
|
|
35
|
+
"tqdm>=4.60",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
test = ["pytest>=7", "pytest-cov>=4", "hypothesis>=6.80"]
|
|
40
|
+
docs = ["mkdocs>=1.6,<2", "mkdocs-material>=9.5", "mkdocstrings[python]>=0.25"]
|
|
41
|
+
dev = ["tide-binning[test,docs]", "ruff>=0.6", "build>=1.0", "twine>=5"]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/leonidgarin/TIDE"
|
|
45
|
+
Issues = "https://github.com/leonidgarin/TIDE/issues"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.version]
|
|
48
|
+
path = "src/tide/__init__.py"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["src/tide"]
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.sdist]
|
|
54
|
+
include = ["src/tide", "tests", "README.md", "LICENSE", "CHANGELOG.md"]
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
testpaths = ["tests"]
|
|
58
|
+
addopts = "-ra"
|
|
59
|
+
filterwarnings = ["error::FutureWarning", "error::DeprecationWarning:tide.*"]
|
|
60
|
+
|
|
61
|
+
[tool.coverage.run]
|
|
62
|
+
source = ["tide"]
|
|
63
|
+
branch = true
|
|
64
|
+
|
|
65
|
+
[tool.ruff]
|
|
66
|
+
line-length = 100
|
|
67
|
+
target-version = "py310"
|
|
68
|
+
|
|
69
|
+
[tool.ruff.lint]
|
|
70
|
+
select = ["E", "F", "W", "I", "B", "UP", "NPY", "PD", "RUF"]
|
|
71
|
+
ignore = ["RUF001", "RUF002", "RUF003", "PD011", "B905"]
|
|
72
|
+
|
|
73
|
+
[tool.ruff.lint.per-file-ignores]
|
|
74
|
+
"tests/*" = ["B", "PD", "RUF007", "RUF059"]
|
|
75
|
+
"benchmarks/*" = ["B", "PD"]
|