fde-tabular 1.0.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.
- fde_tabular-1.0.0/LICENSE +21 -0
- fde_tabular-1.0.0/PKG-INFO +307 -0
- fde_tabular-1.0.0/README.md +273 -0
- fde_tabular-1.0.0/pyproject.toml +50 -0
- fde_tabular-1.0.0/setup.cfg +4 -0
- fde_tabular-1.0.0/src/fde/__init__.py +12 -0
- fde_tabular-1.0.0/src/fde/classifier.py +308 -0
- fde_tabular-1.0.0/src/fde/cli.py +216 -0
- fde_tabular-1.0.0/src/fde/combiners.py +129 -0
- fde_tabular-1.0.0/src/fde/deploy_test.py +96 -0
- fde_tabular-1.0.0/src/fde/device.py +29 -0
- fde_tabular-1.0.0/src/fde/extended.py +202 -0
- fde_tabular-1.0.0/src/fde/learners.py +202 -0
- fde_tabular-1.0.0/src/fde/members.py +161 -0
- fde_tabular-1.0.0/src/fde_tabular.egg-info/PKG-INFO +307 -0
- fde_tabular-1.0.0/src/fde_tabular.egg-info/SOURCES.txt +19 -0
- fde_tabular-1.0.0/src/fde_tabular.egg-info/dependency_links.txt +1 -0
- fde_tabular-1.0.0/src/fde_tabular.egg-info/entry_points.txt +2 -0
- fde_tabular-1.0.0/src/fde_tabular.egg-info/requires.txt +16 -0
- fde_tabular-1.0.0/src/fde_tabular.egg-info/top_level.txt +1 -0
- fde_tabular-1.0.0/tests/test_fde.py +65 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dwi Sunaryono
|
|
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,307 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fde-tabular
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: FDE: Feasible Diverse Ensembles for tabular classification
|
|
5
|
+
Author: Dwi Sunaryono, Misbakhul Munir Irfan Subakti, Laras Suciningtyas, Irzal Ahmad Sabilla, Muchamad Kurniawan
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/DwiSunaryono/fde-tabular
|
|
8
|
+
Project-URL: Repository, https://github.com/DwiSunaryono/fde-tabular
|
|
9
|
+
Project-URL: Issues, https://github.com/DwiSunaryono/fde-tabular/issues
|
|
10
|
+
Keywords: ensemble learning,class imbalance,tabular data,classification,stacking
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: numpy>=1.26
|
|
19
|
+
Requires-Dist: pandas>=2.1
|
|
20
|
+
Requires-Dist: scipy>=1.11
|
|
21
|
+
Requires-Dist: scikit-learn>=1.5
|
|
22
|
+
Requires-Dist: imbalanced-learn>=0.12
|
|
23
|
+
Requires-Dist: xgboost>=2.0
|
|
24
|
+
Requires-Dist: lightgbm>=4.0
|
|
25
|
+
Requires-Dist: catboost>=1.2
|
|
26
|
+
Requires-Dist: optuna>=3.6
|
|
27
|
+
Requires-Dist: joblib>=1.3
|
|
28
|
+
Requires-Dist: torch>=2.1
|
|
29
|
+
Requires-Dist: psutil>=5.9
|
|
30
|
+
Requires-Dist: rich>=13
|
|
31
|
+
Provides-Extra: test
|
|
32
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# FDE: Feasible Diverse Ensembles for tabular classification
|
|
36
|
+
|
|
37
|
+
[](https://doi.org/10.5281/zenodo.23997869)
|
|
38
|
+
|
|
39
|
+
FDE wraps the best tuned single learner in a diverse ensemble and hands prediction to the ensemble only when the
|
|
40
|
+
training data support it over that learner. When they do not, the best tuned learner is used on its own.
|
|
41
|
+
|
|
42
|
+
- **Best tuned learner.** Optuna tunes twelve learner types on inner cross-validation (CatBoost, LightGBM, XGBoost,
|
|
43
|
+
HistGradientBoosting, random forest, extra trees, SVC, k-nearest neighbours, logistic regression, naive Bayes, MLP
|
|
44
|
+
and decision tree) and the highest-scoring type is kept. Above 3,000 rows, SVC is left out.
|
|
45
|
+
- **Members that handle class imbalance.** Three groups join the ensemble: the five next-best learner types refitted
|
|
46
|
+
with class weights; BalancedRandomForest, EasyEnsemble and RUSBoost; and the same five learner types refitted on
|
|
47
|
+
rebalanced samples, with their probabilities corrected to equal class priors.
|
|
48
|
+
- **Extended members.** Eight members work with a larger budget: CatBoost, LightGBM and XGBoost (ordinal or native
|
|
49
|
+
categorical coding, up to 5,000 trees with early stopping), an MLP with three hidden layers of 256 units, and
|
|
50
|
+
random forest and extra trees with 500 trees. Each is bagged over 3 folds of its training rows.
|
|
51
|
+
- **Combiner.** A stack (multinomial logistic regression) over the probabilities of the best learner and the
|
|
52
|
+
members, with a per-class rescaling of the resulting probabilities on top.
|
|
53
|
+
- **Cross-fitted acceptance rule.** Three outer cross-fitted folds of the training rows decide. The ensemble is kept
|
|
54
|
+
only if the lower 5% bootstrap quantile of its gain over the best tuned learner lies above 0, if its weight in the
|
|
55
|
+
all-rows fit is non-zero, and if it changes at least one outer-fold prediction. Otherwise the best tuned learner is
|
|
56
|
+
kept. Every imbalance level follows the same rule; on balanced data the best tuned learner is usually the one kept.
|
|
57
|
+
|
|
58
|
+
A run is scored with macro-F1 when there are two classes and the minority share is below 0.15, and with weighted-F1
|
|
59
|
+
in every other case.
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
Python 3.10 or later. Installing into a fresh virtual environment keeps FDE apart from other projects. FDE runs on the
|
|
64
|
+
CPU by default, so the CPU build of PyTorch is enough and much smaller than the GPU build. Choose your system:
|
|
65
|
+
|
|
66
|
+
<details open>
|
|
67
|
+
<summary><b>Linux</b></summary>
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python3 -m venv ~/fde-env
|
|
71
|
+
source ~/fde-env/bin/activate
|
|
72
|
+
pip install torch --index-url https://download.pytorch.org/whl/cpu
|
|
73
|
+
pip install fde-tabular
|
|
74
|
+
fde demo
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If `python3 -m venv` reports that `ensurepip` is missing (Debian, Ubuntu), install it first with
|
|
78
|
+
`sudo apt install python3-venv`.
|
|
79
|
+
</details>
|
|
80
|
+
|
|
81
|
+
<details>
|
|
82
|
+
<summary><b>macOS</b></summary>
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
brew install libomp
|
|
86
|
+
python3 -m venv ~/fde-env
|
|
87
|
+
source ~/fde-env/bin/activate
|
|
88
|
+
pip install torch
|
|
89
|
+
pip install fde-tabular
|
|
90
|
+
fde demo
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
LightGBM and XGBoost need the OpenMP library (`libomp`) from Homebrew. On macOS the default PyTorch build already runs
|
|
94
|
+
on the CPU.
|
|
95
|
+
</details>
|
|
96
|
+
|
|
97
|
+
<details>
|
|
98
|
+
<summary><b>Windows (Command Prompt)</b></summary>
|
|
99
|
+
|
|
100
|
+
Type or paste **one line at a time** and press Enter after each line (if `py` is not found, use `python` instead):
|
|
101
|
+
|
|
102
|
+
```bat
|
|
103
|
+
py -m venv %USERPROFILE%\fde-env
|
|
104
|
+
%USERPROFILE%\fde-env\Scripts\activate
|
|
105
|
+
pip install torch --index-url https://download.pytorch.org/whl/cpu
|
|
106
|
+
pip install fde-tabular
|
|
107
|
+
fde demo
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
In PowerShell, activate with `$env:USERPROFILE\fde-env\Scripts\Activate.ps1` instead (if scripts are blocked, run
|
|
111
|
+
`Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` once). The Command Prompt does not treat `#` as a comment, so do
|
|
112
|
+
not paste comments after a command. Commands written for Linux, such as `source` or `~/`, do not work here.
|
|
113
|
+
</details>
|
|
114
|
+
|
|
115
|
+
<details>
|
|
116
|
+
<summary><b>With a CUDA GPU (any system, optional)</b></summary>
|
|
117
|
+
|
|
118
|
+
Install the PyTorch build for your CUDA version from <https://pytorch.org> instead of the CPU build, then
|
|
119
|
+
`pip install fde-tabular`, and pass `--device gpu` (or `device="gpu"`). See the GPU note under Options before using it.
|
|
120
|
+
</details>
|
|
121
|
+
|
|
122
|
+
Each time you open a new terminal, activate the environment again (the second line above) before using `fde`.
|
|
123
|
+
|
|
124
|
+
Everything runs on the CPU unless `--device gpu` is given: each tuned learner gets one thread, each extended member
|
|
125
|
+
gets four (`member_threads`). Before fitting, FDE checks the free CPU cores and memory and runs that many tasks in
|
|
126
|
+
parallel (`n_jobs`); the progress bar shows the three stages (tuning the learners, building the members, fitting the
|
|
127
|
+
combiners) with an estimate of the remaining time.
|
|
128
|
+
|
|
129
|
+
## Quick start
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
fde demo # breast cancer data: the best tuned learner alone is enough
|
|
133
|
+
fde demo --example imbalanced # synthetic imbalanced data: the ensemble is used
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The first demo evaluates FDE on the breast cancer data that ships with scikit-learn, the second on synthetic data with a
|
|
137
|
+
10% minority class (no file or download needed for either). The summary explains why the ensemble was used or not. The demos use
|
|
138
|
+
quick settings (5 Optuna trials per learner type and the light member set), so it finishes in about a minute on a
|
|
139
|
+
laptop; `fde demo --trials 20 --members full` runs the settings evaluated in the paper. A progress bar shows the
|
|
140
|
+
stage, the elapsed time and an estimate of the remaining time, and the result compares FDE with the best tuned
|
|
141
|
+
learner on held-out rows.
|
|
142
|
+
|
|
143
|
+
## Use
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from fde import FDEClassifier
|
|
147
|
+
|
|
148
|
+
clf = FDEClassifier(random_state=0).fit(X_train, y_train) # X_train: pandas DataFrame, y_train: labels
|
|
149
|
+
print(clf.report_) # best learner, acceptance rule, whether the ensemble is used
|
|
150
|
+
pred = clf.predict(X_test)
|
|
151
|
+
proba = clf.predict_proba(X_test)
|
|
152
|
+
clf.save("model.fde"); clf = FDEClassifier.load("model.fde")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
From the command line:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
fde fit --data train.csv --target label --out model.fde
|
|
159
|
+
fde predict --model model.fde --data new.csv --proba --out scores.csv
|
|
160
|
+
fde evaluate --data data.csv --target label --test-size 0.2 # holdout scores for FDE and the best tuned learner
|
|
161
|
+
fde fit --data train.csv --target label --out light.fde --members light # fewer members: faster prediction
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The command line prints a short summary: the best tuned learner, whether the ensemble was needed, the model size, the
|
|
165
|
+
fit time and, for `demo` and `evaluate`, the held-out scores. Add `--json` for the full report as JSON, and `--quiet`
|
|
166
|
+
to turn off the progress messages (they go to the standard error stream).
|
|
167
|
+
|
|
168
|
+
Text columns can be left as they are: every learner imputes and encodes them using only its own training rows, and
|
|
169
|
+
treats missing values the same way.
|
|
170
|
+
|
|
171
|
+
## Examples
|
|
172
|
+
|
|
173
|
+
Each example runs as it is (`pip install fde-tabular` first). Times are for a 32-core laptop; with fewer cores they
|
|
174
|
+
grow roughly in proportion. Predicted labels are returned as text (for example `"0"` and `"1"`).
|
|
175
|
+
|
|
176
|
+
**Two classes, quick settings (about 25 s).** Breast cancer data, shipped with scikit-learn:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from sklearn.datasets import load_breast_cancer
|
|
180
|
+
from sklearn.model_selection import train_test_split
|
|
181
|
+
from fde import FDEClassifier
|
|
182
|
+
|
|
183
|
+
X, y = load_breast_cancer(return_X_y=True, as_frame=True)
|
|
184
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=0)
|
|
185
|
+
clf = FDEClassifier(trials=5, members="light", verbose=1).fit(X_train, y_train)
|
|
186
|
+
print("accuracy:", (clf.predict(X_test) == y_test.astype(str)).mean())
|
|
187
|
+
print("ensemble used:", clf.uses_ensemble_, "| best learner:", clf.report_["best_learner"])
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Several classes, with probabilities (about 10 s).** Wine data, shipped with scikit-learn:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from sklearn.datasets import load_wine
|
|
194
|
+
from sklearn.metrics import f1_score
|
|
195
|
+
from sklearn.model_selection import train_test_split
|
|
196
|
+
from fde import FDEClassifier
|
|
197
|
+
|
|
198
|
+
X, y = load_wine(return_X_y=True, as_frame=True)
|
|
199
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, stratify=y, random_state=0)
|
|
200
|
+
clf = FDEClassifier(trials=5, members="light", verbose=1).fit(X_train, y_train)
|
|
201
|
+
print("macro-F1:", round(f1_score(y_test.astype(str), clf.predict(X_test), average="macro"), 4))
|
|
202
|
+
print(clf.classes_)
|
|
203
|
+
print(clf.predict_proba(X_test)[:3].round(3)) # one column per class, in the order of clf.classes_
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Imbalanced classes, a text column and missing values; save and reload (about 15 s).** Synthetic data:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
import numpy as np, pandas as pd
|
|
210
|
+
from sklearn.datasets import make_classification
|
|
211
|
+
from sklearn.metrics import balanced_accuracy_score
|
|
212
|
+
from sklearn.model_selection import train_test_split
|
|
213
|
+
from fde import FDEClassifier
|
|
214
|
+
|
|
215
|
+
X, y = make_classification(n_samples=3000, n_features=12, n_informative=5, weights=[0.95, 0.05], random_state=1)
|
|
216
|
+
X = pd.DataFrame(X, columns=[f"x{i}" for i in range(12)])
|
|
217
|
+
X["region"] = np.where(X["x0"] > 0, "north", "south") # text columns can stay as text
|
|
218
|
+
X.loc[::25, "x1"] = np.nan # missing values are imputed inside each learner
|
|
219
|
+
y = np.where(y == 1, "fraud", "normal")
|
|
220
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=0)
|
|
221
|
+
clf = FDEClassifier(trials=5, members="light", verbose=1).fit(X_train, y_train)
|
|
222
|
+
print("balanced accuracy:", round(balanced_accuracy_score(y_test, clf.predict(X_test)), 4))
|
|
223
|
+
clf.save("fraud.fde")
|
|
224
|
+
clf = FDEClassifier.load("fraud.fde")
|
|
225
|
+
print(clf.predict(X_test.head()))
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Real imbalanced data with the settings of the paper (about 50 s).** The pc4 software-defect data from OpenML
|
|
229
|
+
(downloaded once, about 0.1 MB; 1,458 rows, 12% defects). Here the ensemble is used:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from sklearn.datasets import fetch_openml
|
|
233
|
+
from sklearn.metrics import f1_score
|
|
234
|
+
from sklearn.model_selection import train_test_split
|
|
235
|
+
from fde import FDEClassifier
|
|
236
|
+
|
|
237
|
+
X, y = fetch_openml(data_id=1049, return_X_y=True, as_frame=True)
|
|
238
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=0)
|
|
239
|
+
clf = FDEClassifier(verbose=1).fit(X_train, y_train)
|
|
240
|
+
print("macro-F1:", round(f1_score(y_test.astype(str), clf.predict(X_test), average="macro"), 4))
|
|
241
|
+
print("ensemble used:", clf.uses_ensemble_, "| models in predictor:", clf.report_["models_in_predictor"])
|
|
242
|
+
print(clf.report_["acceptance_rule"]) # lower bound of the gain on the cross-fitted training folds
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Command line with your own CSV file.** This writes a CSV from the wine data, then fits, predicts and evaluates:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
python -c "from sklearn.datasets import load_wine; d = load_wine(as_frame=True).frame; d.to_csv('wine.csv', index=False)"
|
|
249
|
+
fde fit --data wine.csv --target target --out wine.fde --trials 5 --members light
|
|
250
|
+
fde predict --model wine.fde --data wine.csv --proba --out wine_scores.csv
|
|
251
|
+
fde evaluate --data wine.csv --target target --trials 5 --members light
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
On Windows, run each line separately.
|
|
255
|
+
|
|
256
|
+
## How the data are split
|
|
257
|
+
|
|
258
|
+
No row is used both to choose a model and to score it:
|
|
259
|
+
|
|
260
|
+
1. `fde evaluate` and `fde demo` set aside a stratified share of the rows (20% by default) before fitting; these rows
|
|
261
|
+
are used only for the held-out scores.
|
|
262
|
+
2. On the training rows, 3 outer folds decide whether the ensemble is used. The decision is made on rows that each
|
|
263
|
+
fold's models have not seen (the cross-fitted acceptance rule).
|
|
264
|
+
3. Inside every fold, the learners are tuned and the combiner is fitted on 3 inner folds, and every imputer, encoder
|
|
265
|
+
and scaler is fitted on its own training rows only.
|
|
266
|
+
|
|
267
|
+
To compare FDE with another tool, score both on the same held-out rows. A score computed on rows that were also used
|
|
268
|
+
for tuning comes out higher than the score on new data.
|
|
269
|
+
|
|
270
|
+
## Options
|
|
271
|
+
|
|
272
|
+
| Option | Default | Meaning |
|
|
273
|
+
|---|---|---|
|
|
274
|
+
| `random_state` (`--seed`) | 0 | seed shared by every split, search and member |
|
|
275
|
+
| `n_jobs` (`--jobs`) | -1 | parallel tasks; -1 chooses from the free CPU cores and memory (results do not depend on it) |
|
|
276
|
+
| `members` (`--members`) | `full` | `light` drops the extended members: 14 models instead of 22, several times faster at prediction, with somewhat lower accuracy |
|
|
277
|
+
| `trials` (`--trials`) | 20 | Optuna trials for each learner type |
|
|
278
|
+
| `member_threads` | 4 | threads per extended member (Python API only) |
|
|
279
|
+
| `device` (`--device`) | `cpu` | `gpu` trains XGBoost, CatBoost and the MLP on the first CUDA GPU (LightGBM stays on the CPU: its GPU mode was slower in our tests). Results can differ slightly from the CPU; see the note below |
|
|
280
|
+
| `verbose` | 0 (1 on the command line) | 1 shows the system check and a progress bar with the stage and the remaining time; 2 also prints the report |
|
|
281
|
+
|
|
282
|
+
These defaults are the configuration evaluated in the accompanying paper.
|
|
283
|
+
|
|
284
|
+
**GPU note.** The results of the accompanying paper were obtained on the CPU, and the CPU is the default. On our test
|
|
285
|
+
machine (32 CPU cores, one RTX 4070) fitting was faster on the CPU than with `--device gpu`: 26 s against 139 s for
|
|
286
|
+
`fde demo`, and 365 s against 704 s for 40,000 training rows, because many CPU tasks run at the same time while GPU
|
|
287
|
+
tasks wait for the one GPU.
|
|
288
|
+
The GPU option is offered for machines with few CPU cores; check the time on your own data. Models fitted with
|
|
289
|
+
`--device gpu` predict on the CPU, so they also run on machines without a GPU.
|
|
290
|
+
|
|
291
|
+
## Cost
|
|
292
|
+
|
|
293
|
+
Fitting costs more than tuning one learner, because up to twelve learner types are tuned four times (once per outer
|
|
294
|
+
fold and once on all rows). Prediction is cheap when the best tuned learner is kept: one model. When the ensemble is
|
|
295
|
+
used, the best learner, 21 members (eight of them bagged over 3 folds, three models each) and the combiner all run,
|
|
296
|
+
so prediction takes longer than with a single model.
|
|
297
|
+
|
|
298
|
+
## Citation
|
|
299
|
+
|
|
300
|
+
Please cite the accompanying article if you use FDE; the full reference will be added once it is published. The
|
|
301
|
+
software itself is archived on Zenodo: version 1.0.0 has DOI
|
|
302
|
+
[10.5281/zenodo.23997869](https://doi.org/10.5281/zenodo.23997869), and
|
|
303
|
+
[10.5281/zenodo.23997868](https://doi.org/10.5281/zenodo.23997868) always points to the latest version. See `CITATION.cff`.
|
|
304
|
+
|
|
305
|
+
## License
|
|
306
|
+
|
|
307
|
+
MIT.
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# FDE: Feasible Diverse Ensembles for tabular classification
|
|
2
|
+
|
|
3
|
+
[](https://doi.org/10.5281/zenodo.23997869)
|
|
4
|
+
|
|
5
|
+
FDE wraps the best tuned single learner in a diverse ensemble and hands prediction to the ensemble only when the
|
|
6
|
+
training data support it over that learner. When they do not, the best tuned learner is used on its own.
|
|
7
|
+
|
|
8
|
+
- **Best tuned learner.** Optuna tunes twelve learner types on inner cross-validation (CatBoost, LightGBM, XGBoost,
|
|
9
|
+
HistGradientBoosting, random forest, extra trees, SVC, k-nearest neighbours, logistic regression, naive Bayes, MLP
|
|
10
|
+
and decision tree) and the highest-scoring type is kept. Above 3,000 rows, SVC is left out.
|
|
11
|
+
- **Members that handle class imbalance.** Three groups join the ensemble: the five next-best learner types refitted
|
|
12
|
+
with class weights; BalancedRandomForest, EasyEnsemble and RUSBoost; and the same five learner types refitted on
|
|
13
|
+
rebalanced samples, with their probabilities corrected to equal class priors.
|
|
14
|
+
- **Extended members.** Eight members work with a larger budget: CatBoost, LightGBM and XGBoost (ordinal or native
|
|
15
|
+
categorical coding, up to 5,000 trees with early stopping), an MLP with three hidden layers of 256 units, and
|
|
16
|
+
random forest and extra trees with 500 trees. Each is bagged over 3 folds of its training rows.
|
|
17
|
+
- **Combiner.** A stack (multinomial logistic regression) over the probabilities of the best learner and the
|
|
18
|
+
members, with a per-class rescaling of the resulting probabilities on top.
|
|
19
|
+
- **Cross-fitted acceptance rule.** Three outer cross-fitted folds of the training rows decide. The ensemble is kept
|
|
20
|
+
only if the lower 5% bootstrap quantile of its gain over the best tuned learner lies above 0, if its weight in the
|
|
21
|
+
all-rows fit is non-zero, and if it changes at least one outer-fold prediction. Otherwise the best tuned learner is
|
|
22
|
+
kept. Every imbalance level follows the same rule; on balanced data the best tuned learner is usually the one kept.
|
|
23
|
+
|
|
24
|
+
A run is scored with macro-F1 when there are two classes and the minority share is below 0.15, and with weighted-F1
|
|
25
|
+
in every other case.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Python 3.10 or later. Installing into a fresh virtual environment keeps FDE apart from other projects. FDE runs on the
|
|
30
|
+
CPU by default, so the CPU build of PyTorch is enough and much smaller than the GPU build. Choose your system:
|
|
31
|
+
|
|
32
|
+
<details open>
|
|
33
|
+
<summary><b>Linux</b></summary>
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python3 -m venv ~/fde-env
|
|
37
|
+
source ~/fde-env/bin/activate
|
|
38
|
+
pip install torch --index-url https://download.pytorch.org/whl/cpu
|
|
39
|
+
pip install fde-tabular
|
|
40
|
+
fde demo
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If `python3 -m venv` reports that `ensurepip` is missing (Debian, Ubuntu), install it first with
|
|
44
|
+
`sudo apt install python3-venv`.
|
|
45
|
+
</details>
|
|
46
|
+
|
|
47
|
+
<details>
|
|
48
|
+
<summary><b>macOS</b></summary>
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
brew install libomp
|
|
52
|
+
python3 -m venv ~/fde-env
|
|
53
|
+
source ~/fde-env/bin/activate
|
|
54
|
+
pip install torch
|
|
55
|
+
pip install fde-tabular
|
|
56
|
+
fde demo
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
LightGBM and XGBoost need the OpenMP library (`libomp`) from Homebrew. On macOS the default PyTorch build already runs
|
|
60
|
+
on the CPU.
|
|
61
|
+
</details>
|
|
62
|
+
|
|
63
|
+
<details>
|
|
64
|
+
<summary><b>Windows (Command Prompt)</b></summary>
|
|
65
|
+
|
|
66
|
+
Type or paste **one line at a time** and press Enter after each line (if `py` is not found, use `python` instead):
|
|
67
|
+
|
|
68
|
+
```bat
|
|
69
|
+
py -m venv %USERPROFILE%\fde-env
|
|
70
|
+
%USERPROFILE%\fde-env\Scripts\activate
|
|
71
|
+
pip install torch --index-url https://download.pytorch.org/whl/cpu
|
|
72
|
+
pip install fde-tabular
|
|
73
|
+
fde demo
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
In PowerShell, activate with `$env:USERPROFILE\fde-env\Scripts\Activate.ps1` instead (if scripts are blocked, run
|
|
77
|
+
`Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` once). The Command Prompt does not treat `#` as a comment, so do
|
|
78
|
+
not paste comments after a command. Commands written for Linux, such as `source` or `~/`, do not work here.
|
|
79
|
+
</details>
|
|
80
|
+
|
|
81
|
+
<details>
|
|
82
|
+
<summary><b>With a CUDA GPU (any system, optional)</b></summary>
|
|
83
|
+
|
|
84
|
+
Install the PyTorch build for your CUDA version from <https://pytorch.org> instead of the CPU build, then
|
|
85
|
+
`pip install fde-tabular`, and pass `--device gpu` (or `device="gpu"`). See the GPU note under Options before using it.
|
|
86
|
+
</details>
|
|
87
|
+
|
|
88
|
+
Each time you open a new terminal, activate the environment again (the second line above) before using `fde`.
|
|
89
|
+
|
|
90
|
+
Everything runs on the CPU unless `--device gpu` is given: each tuned learner gets one thread, each extended member
|
|
91
|
+
gets four (`member_threads`). Before fitting, FDE checks the free CPU cores and memory and runs that many tasks in
|
|
92
|
+
parallel (`n_jobs`); the progress bar shows the three stages (tuning the learners, building the members, fitting the
|
|
93
|
+
combiners) with an estimate of the remaining time.
|
|
94
|
+
|
|
95
|
+
## Quick start
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
fde demo # breast cancer data: the best tuned learner alone is enough
|
|
99
|
+
fde demo --example imbalanced # synthetic imbalanced data: the ensemble is used
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The first demo evaluates FDE on the breast cancer data that ships with scikit-learn, the second on synthetic data with a
|
|
103
|
+
10% minority class (no file or download needed for either). The summary explains why the ensemble was used or not. The demos use
|
|
104
|
+
quick settings (5 Optuna trials per learner type and the light member set), so it finishes in about a minute on a
|
|
105
|
+
laptop; `fde demo --trials 20 --members full` runs the settings evaluated in the paper. A progress bar shows the
|
|
106
|
+
stage, the elapsed time and an estimate of the remaining time, and the result compares FDE with the best tuned
|
|
107
|
+
learner on held-out rows.
|
|
108
|
+
|
|
109
|
+
## Use
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from fde import FDEClassifier
|
|
113
|
+
|
|
114
|
+
clf = FDEClassifier(random_state=0).fit(X_train, y_train) # X_train: pandas DataFrame, y_train: labels
|
|
115
|
+
print(clf.report_) # best learner, acceptance rule, whether the ensemble is used
|
|
116
|
+
pred = clf.predict(X_test)
|
|
117
|
+
proba = clf.predict_proba(X_test)
|
|
118
|
+
clf.save("model.fde"); clf = FDEClassifier.load("model.fde")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
From the command line:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
fde fit --data train.csv --target label --out model.fde
|
|
125
|
+
fde predict --model model.fde --data new.csv --proba --out scores.csv
|
|
126
|
+
fde evaluate --data data.csv --target label --test-size 0.2 # holdout scores for FDE and the best tuned learner
|
|
127
|
+
fde fit --data train.csv --target label --out light.fde --members light # fewer members: faster prediction
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The command line prints a short summary: the best tuned learner, whether the ensemble was needed, the model size, the
|
|
131
|
+
fit time and, for `demo` and `evaluate`, the held-out scores. Add `--json` for the full report as JSON, and `--quiet`
|
|
132
|
+
to turn off the progress messages (they go to the standard error stream).
|
|
133
|
+
|
|
134
|
+
Text columns can be left as they are: every learner imputes and encodes them using only its own training rows, and
|
|
135
|
+
treats missing values the same way.
|
|
136
|
+
|
|
137
|
+
## Examples
|
|
138
|
+
|
|
139
|
+
Each example runs as it is (`pip install fde-tabular` first). Times are for a 32-core laptop; with fewer cores they
|
|
140
|
+
grow roughly in proportion. Predicted labels are returned as text (for example `"0"` and `"1"`).
|
|
141
|
+
|
|
142
|
+
**Two classes, quick settings (about 25 s).** Breast cancer data, shipped with scikit-learn:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from sklearn.datasets import load_breast_cancer
|
|
146
|
+
from sklearn.model_selection import train_test_split
|
|
147
|
+
from fde import FDEClassifier
|
|
148
|
+
|
|
149
|
+
X, y = load_breast_cancer(return_X_y=True, as_frame=True)
|
|
150
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=0)
|
|
151
|
+
clf = FDEClassifier(trials=5, members="light", verbose=1).fit(X_train, y_train)
|
|
152
|
+
print("accuracy:", (clf.predict(X_test) == y_test.astype(str)).mean())
|
|
153
|
+
print("ensemble used:", clf.uses_ensemble_, "| best learner:", clf.report_["best_learner"])
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Several classes, with probabilities (about 10 s).** Wine data, shipped with scikit-learn:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from sklearn.datasets import load_wine
|
|
160
|
+
from sklearn.metrics import f1_score
|
|
161
|
+
from sklearn.model_selection import train_test_split
|
|
162
|
+
from fde import FDEClassifier
|
|
163
|
+
|
|
164
|
+
X, y = load_wine(return_X_y=True, as_frame=True)
|
|
165
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, stratify=y, random_state=0)
|
|
166
|
+
clf = FDEClassifier(trials=5, members="light", verbose=1).fit(X_train, y_train)
|
|
167
|
+
print("macro-F1:", round(f1_score(y_test.astype(str), clf.predict(X_test), average="macro"), 4))
|
|
168
|
+
print(clf.classes_)
|
|
169
|
+
print(clf.predict_proba(X_test)[:3].round(3)) # one column per class, in the order of clf.classes_
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Imbalanced classes, a text column and missing values; save and reload (about 15 s).** Synthetic data:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
import numpy as np, pandas as pd
|
|
176
|
+
from sklearn.datasets import make_classification
|
|
177
|
+
from sklearn.metrics import balanced_accuracy_score
|
|
178
|
+
from sklearn.model_selection import train_test_split
|
|
179
|
+
from fde import FDEClassifier
|
|
180
|
+
|
|
181
|
+
X, y = make_classification(n_samples=3000, n_features=12, n_informative=5, weights=[0.95, 0.05], random_state=1)
|
|
182
|
+
X = pd.DataFrame(X, columns=[f"x{i}" for i in range(12)])
|
|
183
|
+
X["region"] = np.where(X["x0"] > 0, "north", "south") # text columns can stay as text
|
|
184
|
+
X.loc[::25, "x1"] = np.nan # missing values are imputed inside each learner
|
|
185
|
+
y = np.where(y == 1, "fraud", "normal")
|
|
186
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=0)
|
|
187
|
+
clf = FDEClassifier(trials=5, members="light", verbose=1).fit(X_train, y_train)
|
|
188
|
+
print("balanced accuracy:", round(balanced_accuracy_score(y_test, clf.predict(X_test)), 4))
|
|
189
|
+
clf.save("fraud.fde")
|
|
190
|
+
clf = FDEClassifier.load("fraud.fde")
|
|
191
|
+
print(clf.predict(X_test.head()))
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Real imbalanced data with the settings of the paper (about 50 s).** The pc4 software-defect data from OpenML
|
|
195
|
+
(downloaded once, about 0.1 MB; 1,458 rows, 12% defects). Here the ensemble is used:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from sklearn.datasets import fetch_openml
|
|
199
|
+
from sklearn.metrics import f1_score
|
|
200
|
+
from sklearn.model_selection import train_test_split
|
|
201
|
+
from fde import FDEClassifier
|
|
202
|
+
|
|
203
|
+
X, y = fetch_openml(data_id=1049, return_X_y=True, as_frame=True)
|
|
204
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=0)
|
|
205
|
+
clf = FDEClassifier(verbose=1).fit(X_train, y_train)
|
|
206
|
+
print("macro-F1:", round(f1_score(y_test.astype(str), clf.predict(X_test), average="macro"), 4))
|
|
207
|
+
print("ensemble used:", clf.uses_ensemble_, "| models in predictor:", clf.report_["models_in_predictor"])
|
|
208
|
+
print(clf.report_["acceptance_rule"]) # lower bound of the gain on the cross-fitted training folds
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Command line with your own CSV file.** This writes a CSV from the wine data, then fits, predicts and evaluates:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
python -c "from sklearn.datasets import load_wine; d = load_wine(as_frame=True).frame; d.to_csv('wine.csv', index=False)"
|
|
215
|
+
fde fit --data wine.csv --target target --out wine.fde --trials 5 --members light
|
|
216
|
+
fde predict --model wine.fde --data wine.csv --proba --out wine_scores.csv
|
|
217
|
+
fde evaluate --data wine.csv --target target --trials 5 --members light
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
On Windows, run each line separately.
|
|
221
|
+
|
|
222
|
+
## How the data are split
|
|
223
|
+
|
|
224
|
+
No row is used both to choose a model and to score it:
|
|
225
|
+
|
|
226
|
+
1. `fde evaluate` and `fde demo` set aside a stratified share of the rows (20% by default) before fitting; these rows
|
|
227
|
+
are used only for the held-out scores.
|
|
228
|
+
2. On the training rows, 3 outer folds decide whether the ensemble is used. The decision is made on rows that each
|
|
229
|
+
fold's models have not seen (the cross-fitted acceptance rule).
|
|
230
|
+
3. Inside every fold, the learners are tuned and the combiner is fitted on 3 inner folds, and every imputer, encoder
|
|
231
|
+
and scaler is fitted on its own training rows only.
|
|
232
|
+
|
|
233
|
+
To compare FDE with another tool, score both on the same held-out rows. A score computed on rows that were also used
|
|
234
|
+
for tuning comes out higher than the score on new data.
|
|
235
|
+
|
|
236
|
+
## Options
|
|
237
|
+
|
|
238
|
+
| Option | Default | Meaning |
|
|
239
|
+
|---|---|---|
|
|
240
|
+
| `random_state` (`--seed`) | 0 | seed shared by every split, search and member |
|
|
241
|
+
| `n_jobs` (`--jobs`) | -1 | parallel tasks; -1 chooses from the free CPU cores and memory (results do not depend on it) |
|
|
242
|
+
| `members` (`--members`) | `full` | `light` drops the extended members: 14 models instead of 22, several times faster at prediction, with somewhat lower accuracy |
|
|
243
|
+
| `trials` (`--trials`) | 20 | Optuna trials for each learner type |
|
|
244
|
+
| `member_threads` | 4 | threads per extended member (Python API only) |
|
|
245
|
+
| `device` (`--device`) | `cpu` | `gpu` trains XGBoost, CatBoost and the MLP on the first CUDA GPU (LightGBM stays on the CPU: its GPU mode was slower in our tests). Results can differ slightly from the CPU; see the note below |
|
|
246
|
+
| `verbose` | 0 (1 on the command line) | 1 shows the system check and a progress bar with the stage and the remaining time; 2 also prints the report |
|
|
247
|
+
|
|
248
|
+
These defaults are the configuration evaluated in the accompanying paper.
|
|
249
|
+
|
|
250
|
+
**GPU note.** The results of the accompanying paper were obtained on the CPU, and the CPU is the default. On our test
|
|
251
|
+
machine (32 CPU cores, one RTX 4070) fitting was faster on the CPU than with `--device gpu`: 26 s against 139 s for
|
|
252
|
+
`fde demo`, and 365 s against 704 s for 40,000 training rows, because many CPU tasks run at the same time while GPU
|
|
253
|
+
tasks wait for the one GPU.
|
|
254
|
+
The GPU option is offered for machines with few CPU cores; check the time on your own data. Models fitted with
|
|
255
|
+
`--device gpu` predict on the CPU, so they also run on machines without a GPU.
|
|
256
|
+
|
|
257
|
+
## Cost
|
|
258
|
+
|
|
259
|
+
Fitting costs more than tuning one learner, because up to twelve learner types are tuned four times (once per outer
|
|
260
|
+
fold and once on all rows). Prediction is cheap when the best tuned learner is kept: one model. When the ensemble is
|
|
261
|
+
used, the best learner, 21 members (eight of them bagged over 3 folds, three models each) and the combiner all run,
|
|
262
|
+
so prediction takes longer than with a single model.
|
|
263
|
+
|
|
264
|
+
## Citation
|
|
265
|
+
|
|
266
|
+
Please cite the accompanying article if you use FDE; the full reference will be added once it is published. The
|
|
267
|
+
software itself is archived on Zenodo: version 1.0.0 has DOI
|
|
268
|
+
[10.5281/zenodo.23997869](https://doi.org/10.5281/zenodo.23997869), and
|
|
269
|
+
[10.5281/zenodo.23997868](https://doi.org/10.5281/zenodo.23997868) always points to the latest version. See `CITATION.cff`.
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
MIT.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fde-tabular"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "FDE: Feasible Diverse Ensembles for tabular classification"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Dwi Sunaryono" }, { name = "Misbakhul Munir Irfan Subakti" }, { name = "Laras Suciningtyas" },
|
|
13
|
+
{ name = "Irzal Ahmad Sabilla" }, { name = "Muchamad Kurniawan" }]
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
keywords = ["ensemble learning", "class imbalance", "tabular data", "classification", "stacking"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"numpy>=1.26",
|
|
24
|
+
"pandas>=2.1",
|
|
25
|
+
"scipy>=1.11",
|
|
26
|
+
"scikit-learn>=1.5",
|
|
27
|
+
"imbalanced-learn>=0.12",
|
|
28
|
+
"xgboost>=2.0",
|
|
29
|
+
"lightgbm>=4.0",
|
|
30
|
+
"catboost>=1.2",
|
|
31
|
+
"optuna>=3.6",
|
|
32
|
+
"joblib>=1.3",
|
|
33
|
+
"torch>=2.1",
|
|
34
|
+
"psutil>=5.9",
|
|
35
|
+
"rich>=13",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/DwiSunaryono/fde-tabular"
|
|
40
|
+
Repository = "https://github.com/DwiSunaryono/fde-tabular"
|
|
41
|
+
Issues = "https://github.com/DwiSunaryono/fde-tabular/issues"
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
test = ["pytest>=7"]
|
|
45
|
+
|
|
46
|
+
[project.scripts]
|
|
47
|
+
fde = "fde.cli:main"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["src"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""FDE: Feasible Diverse Ensembles for tabular classification."""
|
|
2
|
+
__version__ = "1.0.0"
|
|
3
|
+
__all__ = ["FDEClassifier", "__version__"]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def __getattr__(name):
|
|
7
|
+
# FDEClassifier is imported on first use, so that the command line can print its first message before the
|
|
8
|
+
# learner libraries load
|
|
9
|
+
if name == "FDEClassifier":
|
|
10
|
+
from .classifier import FDEClassifier
|
|
11
|
+
return FDEClassifier
|
|
12
|
+
raise AttributeError(name)
|