binboost 0.2.3__tar.gz → 0.2.5__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.
- {binboost-0.2.3 → binboost-0.2.5}/PKG-INFO +1 -1
- {binboost-0.2.3 → binboost-0.2.5}/binboost/__init__.py +2 -2
- {binboost-0.2.3 → binboost-0.2.5}/binboost/binarizer.py +31 -23
- {binboost-0.2.3 → binboost-0.2.5}/binboost/binboost.py +181 -122
- binboost-0.2.5/binboost/loss.py +118 -0
- {binboost-0.2.3 → binboost-0.2.5}/binboost/multiclass.py +70 -78
- binboost-0.2.5/binboost/rule.py +282 -0
- {binboost-0.2.3 → binboost-0.2.5}/binboost.egg-info/PKG-INFO +1 -1
- {binboost-0.2.3 → binboost-0.2.5}/pyproject.toml +1 -1
- {binboost-0.2.3 → binboost-0.2.5}/setup.py +1 -1
- binboost-0.2.3/binboost/loss.py +0 -115
- binboost-0.2.3/binboost/rule.py +0 -220
- {binboost-0.2.3 → binboost-0.2.5}/LICENSE +0 -0
- {binboost-0.2.3 → binboost-0.2.5}/README.md +0 -0
- {binboost-0.2.3 → binboost-0.2.5}/binboost.egg-info/SOURCES.txt +0 -0
- {binboost-0.2.3 → binboost-0.2.5}/binboost.egg-info/dependency_links.txt +0 -0
- {binboost-0.2.3 → binboost-0.2.5}/binboost.egg-info/requires.txt +0 -0
- {binboost-0.2.3 → binboost-0.2.5}/binboost.egg-info/top_level.txt +0 -0
- {binboost-0.2.3 → binboost-0.2.5}/setup.cfg +0 -0
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
# binboost/binarizer.py
|
|
2
1
|
import numpy as np
|
|
3
2
|
|
|
4
3
|
|
|
5
4
|
class GradientBinarizer:
|
|
6
|
-
# Binarisasi
|
|
7
|
-
|
|
5
|
+
# 1. Binarisasi Fitur Numerik Berdasarkan Korelasi Dengan Gradient Saat Ini
|
|
8
6
|
def __init__(self, strategy='gradient', n_thresholds='auto'):
|
|
9
7
|
self.strategy = strategy
|
|
10
8
|
self.n_thresholds = n_thresholds
|
|
11
|
-
#
|
|
9
|
+
# 1. Simpan Threshold Tetap Per Fitur Buat Strategi Selain Gradient
|
|
12
10
|
self._fixed_thresholds = {}
|
|
13
11
|
|
|
14
|
-
def
|
|
15
|
-
# Kembalikan
|
|
16
|
-
unique = np.unique(values)
|
|
12
|
+
def _candidates_from_unique(self, unique):
|
|
13
|
+
# 1. Kembalikan Kandidat Threshold Dari Nilai Unik Yang Udah Terurut
|
|
17
14
|
if len(unique) <= 1:
|
|
18
15
|
return np.array([])
|
|
19
16
|
if self.n_thresholds == 'auto':
|
|
@@ -25,23 +22,28 @@ class GradientBinarizer:
|
|
|
25
22
|
idx = np.linspace(0, len(unique) - 2, max_t, dtype=int)
|
|
26
23
|
return (unique[idx] + unique[idx + 1]) / 2
|
|
27
24
|
|
|
25
|
+
def _get_candidates(self, values):
|
|
26
|
+
# 1. Kembalikan Kandidat Threshold Buat Satu Fitur
|
|
27
|
+
return self._candidates_from_unique(np.unique(values))
|
|
28
|
+
|
|
28
29
|
def _best_threshold_gradient(self, values, gradients):
|
|
29
|
-
# 1. Cari
|
|
30
|
-
|
|
30
|
+
# 1. Cari Threshold Yang Bikin Nilai Mutlak Jumlah Gradient Sampel Di Atasnya Paling Besar
|
|
31
|
+
unique, inverse = np.unique(values, return_inverse=True)
|
|
32
|
+
candidates = self._candidates_from_unique(unique)
|
|
31
33
|
if len(candidates) == 0:
|
|
32
34
|
return 0.0, np.zeros(len(values), dtype=np.float64)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
# a. Hitung Jumlah Gradient Per Nilai Unik Lalu Jumlahkan Kumulatif Dari Kanan
|
|
36
|
+
g_per_unique = np.bincount(inverse.ravel(), weights=gradients, minlength=len(unique))
|
|
37
|
+
suffix = np.concatenate([np.cumsum(g_per_unique[::-1])[::-1], [0.0]])
|
|
38
|
+
# b. Hitung Jumlah Gradient Sampel Yang Nilainya Lebih Besar Dari Tiap Kandidat
|
|
39
|
+
k = np.searchsorted(unique, candidates, side='right')
|
|
40
|
+
scores = np.abs(suffix[k])
|
|
41
|
+
# c. Ambil Kandidat Pertama Yang Skornya Paling Tinggi
|
|
42
|
+
best_t = candidates[int(np.argmax(scores))]
|
|
41
43
|
return best_t, (values > best_t).astype(np.float64)
|
|
42
44
|
|
|
43
45
|
def _best_threshold_quantile(self, col_idx, values):
|
|
44
|
-
# 1.
|
|
46
|
+
# 1. Pakai Threshold Tetap Dari Kuantil Distribusi Data
|
|
45
47
|
if col_idx not in self._fixed_thresholds:
|
|
46
48
|
candidates = self._get_candidates(values)
|
|
47
49
|
if len(candidates) == 0:
|
|
@@ -52,16 +54,15 @@ class GradientBinarizer:
|
|
|
52
54
|
return t, (values > t).astype(np.float64)
|
|
53
55
|
|
|
54
56
|
def _best_threshold_uniform(self, col_idx, values):
|
|
55
|
-
# 1.
|
|
57
|
+
# 1. Pakai Threshold Di Tengah Rentang Nilai Fitur
|
|
56
58
|
if col_idx not in self._fixed_thresholds:
|
|
57
59
|
self._fixed_thresholds[col_idx] = (values.min() + values.max()) / 2
|
|
58
60
|
t = self._fixed_thresholds[col_idx]
|
|
59
61
|
return t, (values > t).astype(np.float64)
|
|
60
62
|
|
|
61
63
|
def _best_threshold_kmeans(self, col_idx, values):
|
|
62
|
-
# 1.
|
|
64
|
+
# 1. Pakai Titik Tengah Dua Centroid KMeans Satu Dimensi Sebagai Threshold
|
|
63
65
|
if col_idx not in self._fixed_thresholds:
|
|
64
|
-
v = values.reshape(-1, 1)
|
|
65
66
|
c1 = np.percentile(values, 25)
|
|
66
67
|
c2 = np.percentile(values, 75)
|
|
67
68
|
for _ in range(100):
|
|
@@ -76,8 +77,8 @@ class GradientBinarizer:
|
|
|
76
77
|
return t, (values > t).astype(np.float64)
|
|
77
78
|
|
|
78
79
|
def transform(self, X, gradients, numeric_cols):
|
|
79
|
-
# Binarisasi
|
|
80
|
-
X_bin = X.
|
|
80
|
+
# 1. Binarisasi Semua Fitur Numerik Lalu Kembalikan Matriks Biner Dan Threshold Iterasi Ini
|
|
81
|
+
X_bin = X.astype(np.float64, copy=True)
|
|
81
82
|
thresholds_this_iter = {}
|
|
82
83
|
for col_idx in numeric_cols:
|
|
83
84
|
values = X[:, col_idx].astype(np.float64)
|
|
@@ -94,3 +95,10 @@ class GradientBinarizer:
|
|
|
94
95
|
X_bin[:, col_idx] = binary
|
|
95
96
|
thresholds_this_iter[col_idx] = t
|
|
96
97
|
return X_bin, thresholds_this_iter
|
|
98
|
+
|
|
99
|
+
def apply_thresholds(self, X, thresholds, numeric_cols):
|
|
100
|
+
# 1. Binarisasi Data Pakai Threshold Yang Udah Ditentukan Tanpa Nyari Ulang
|
|
101
|
+
X_bin = X.astype(np.float64, copy=True)
|
|
102
|
+
for col_idx in numeric_cols:
|
|
103
|
+
X_bin[:, col_idx] = (X[:, col_idx] > thresholds[col_idx]).astype(np.float64)
|
|
104
|
+
return X_bin
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# binboost/binboost.py
|
|
2
1
|
import numpy as np
|
|
3
2
|
|
|
4
3
|
from .loss import get_loss, sigmoid
|
|
@@ -63,6 +62,12 @@ class BinBoost:
|
|
|
63
62
|
lambda0 : float, default=3.0
|
|
64
63
|
Konstanta regularisasi dasar untuk bobot Newton adaptif (λ_R = lambda0 · L / sqrt(n_R+1)).
|
|
65
64
|
|
|
65
|
+
gamma : float, default=0.001
|
|
66
|
+
Ambang penurunan galat minimum yang sifatnya relatif. Aturan baru cuma diterima kalau
|
|
67
|
+
penurunan galat kuadratnya minimal gamma kali energi gradient saat itu. Kalau gak ada
|
|
68
|
+
aturan yang lolos training berhenti lebih awal. Nilai 0.0 mematikan pemangkasan ini.
|
|
69
|
+
Karena relatif satu nilai bisa dipakai buat berbagai dataset dan loss.
|
|
70
|
+
|
|
66
71
|
binarize_strategy : str, default='gradient'
|
|
67
72
|
Strategi binarisasi fitur numerik. Pilihan: 'gradient', 'quantile',
|
|
68
73
|
'uniform', 'kmeans'.
|
|
@@ -86,11 +91,28 @@ class BinBoost:
|
|
|
86
91
|
Threshold prediksi kelas. Nilai 'auto' menggunakan indeks Youden
|
|
87
92
|
untuk mencari threshold optimal pada data training.
|
|
88
93
|
|
|
94
|
+
ohe_policy : str, default='block'
|
|
95
|
+
Perlakuan kelompok fitur hasil one hot encoding. Nilai block melarang dua kategori dari
|
|
96
|
+
fitur yang sama masuk satu aturan. Nilai allow membolehkannya. Kelompok dikenali kalau
|
|
97
|
+
kolom biner saling eksklusif dan jumlahnya pas satu di tiap baris.
|
|
98
|
+
|
|
99
|
+
Bobot sampel pada training:
|
|
100
|
+
Bobot tidak negatif per sampel lewat argumen sample_weight di fit. Bobot dinormalisasi
|
|
101
|
+
ke rata-rata satu jadi cuma rasio antarbobot yang berpengaruh. Bobot dipakai di skor awal.
|
|
102
|
+
Bobot dipakai di gradient dan hessian. Bobot dipakai di pencarian aturan pakai kuadrat
|
|
103
|
+
terkecil berbobot. Bobot dipakai di bobot Newton aturan. Bobot dipakai di loss training
|
|
104
|
+
dan threshold Youden. min_samples_rule menghitung sampel yang bobotnya positif.
|
|
105
|
+
Subsample per iterasi tetap seragam. Bobot yang semuanya sama hasilnya identik dengan
|
|
106
|
+
tanpa bobot.
|
|
107
|
+
|
|
89
108
|
Atribut yang tersedia Setelah fit
|
|
90
109
|
--------------------------------
|
|
91
110
|
estimators_ : list of Rule
|
|
92
111
|
Daftar aturan yang dipelajari.
|
|
93
112
|
|
|
113
|
+
F0_ : float
|
|
114
|
+
Skor awal berupa log-odds dari proporsi kelas positif yang ditambahkan ke semua prediksi.
|
|
115
|
+
|
|
94
116
|
rule_weights_ : ndarray of float shape (n_estimators_, 2)
|
|
95
117
|
Bobot dua sisi setiap aturan. Kolom 0 adalah w0 (tidak memenuhi aturan)
|
|
96
118
|
dan kolom 1 adalah w1 (memenuhi aturan).
|
|
@@ -136,8 +158,8 @@ class BinBoost:
|
|
|
136
158
|
beam_width=5,
|
|
137
159
|
subsample=0.8,
|
|
138
160
|
min_samples_rule=5,
|
|
139
|
-
lambda0=3.0,
|
|
140
|
-
gamma=0.
|
|
161
|
+
lambda0=3.0,
|
|
162
|
+
gamma=0.001,
|
|
141
163
|
binarize_strategy='gradient',
|
|
142
164
|
n_thresholds='auto',
|
|
143
165
|
n_iter_no_change=None,
|
|
@@ -145,6 +167,7 @@ class BinBoost:
|
|
|
145
167
|
random_state=None,
|
|
146
168
|
warm_start=False,
|
|
147
169
|
threshold='auto',
|
|
170
|
+
ohe_policy='block',
|
|
148
171
|
):
|
|
149
172
|
self.n_estimators = n_estimators
|
|
150
173
|
self.learning_rate = learning_rate
|
|
@@ -160,7 +183,7 @@ class BinBoost:
|
|
|
160
183
|
self.beam_width = beam_width
|
|
161
184
|
self.subsample = subsample
|
|
162
185
|
self.min_samples_rule = min_samples_rule
|
|
163
|
-
self.lambda0 = lambda0
|
|
186
|
+
self.lambda0 = lambda0
|
|
164
187
|
self.gamma = gamma
|
|
165
188
|
self.binarize_strategy = binarize_strategy
|
|
166
189
|
self.n_thresholds = n_thresholds
|
|
@@ -169,10 +192,11 @@ class BinBoost:
|
|
|
169
192
|
self.random_state = random_state
|
|
170
193
|
self.warm_start = warm_start
|
|
171
194
|
self.threshold = threshold
|
|
195
|
+
self.ohe_policy = ohe_policy
|
|
172
196
|
self._threshold_fitted = 0.5
|
|
173
197
|
|
|
174
198
|
def _validate_input(self, X, y=None):
|
|
175
|
-
# Ubah
|
|
199
|
+
# 1. Ubah Input Jadi Array Dan Pastikan Dimensinya Bener
|
|
176
200
|
if hasattr(X, 'values'):
|
|
177
201
|
if hasattr(X, 'columns'):
|
|
178
202
|
self._input_feature_names = list(X.columns)
|
|
@@ -190,66 +214,85 @@ class BinBoost:
|
|
|
190
214
|
return X, y
|
|
191
215
|
|
|
192
216
|
def _detect_numeric_cols(self, X):
|
|
193
|
-
#
|
|
194
|
-
|
|
195
|
-
for
|
|
196
|
-
unique_vals = np.unique(X[:, col])
|
|
197
|
-
if not np.all(np.isin(unique_vals, [0.0, 1.0])):
|
|
198
|
-
numeric.append(col)
|
|
199
|
-
return numeric
|
|
217
|
+
# 1. Cari Kolom Yang Bukan Biner Murni Buat Dianggap Fitur Numerik
|
|
218
|
+
is_biner = ((X == 0.0) | (X == 1.0)).all(axis=0)
|
|
219
|
+
return [int(c) for c in np.where(~is_biner)[0]]
|
|
200
220
|
|
|
201
221
|
def _detect_ohe_groups(self, X):
|
|
202
|
-
# 1.
|
|
203
|
-
|
|
222
|
+
# 1. Cari Kelompok Kolom One Hot Encoding Yang Saling Eksklusif Dan Jumlahnya Pas Satu Per Baris
|
|
223
|
+
is_biner = ((X == 0.0) | (X == 1.0)).all(axis=0)
|
|
224
|
+
aktif = np.where(is_biner & (X.sum(axis=0) > 0))[0]
|
|
225
|
+
m = len(aktif)
|
|
226
|
+
if m < 2:
|
|
227
|
+
return None
|
|
228
|
+
Xb = X[:, aktif].astype(np.float32)
|
|
229
|
+
# a. Dua Kolom Eksklusif Kalau Gak Pernah Bernilai Satu Barengan
|
|
230
|
+
eksklusif = (Xb.T @ Xb) == 0
|
|
231
|
+
terpakai = np.zeros(m, dtype=bool)
|
|
204
232
|
groups = {}
|
|
205
|
-
used = set()
|
|
206
233
|
group_id = 0
|
|
207
|
-
for i in range(
|
|
208
|
-
if i
|
|
234
|
+
for i in range(m):
|
|
235
|
+
if terpakai[i]:
|
|
209
236
|
continue
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
#
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
groups[group_id] = group
|
|
237
|
+
anggota = [i]
|
|
238
|
+
jumlah = Xb[:, i].copy()
|
|
239
|
+
kandidat = np.where(eksklusif[i] & ~terpakai)[0]
|
|
240
|
+
for j in kandidat[kandidat > i]:
|
|
241
|
+
# b. Anggota Baru Harus Eksklusif Ke Semua Anggota Kelompok
|
|
242
|
+
if eksklusif[j, anggota].all():
|
|
243
|
+
anggota.append(int(j))
|
|
244
|
+
jumlah += Xb[:, j]
|
|
245
|
+
# c. Berhenti Kalau Kelompok Udah Nutup Semua Baris
|
|
246
|
+
if np.all(jumlah == 1.0):
|
|
247
|
+
break
|
|
248
|
+
if len(anggota) > 1 and np.all(jumlah == 1.0):
|
|
249
|
+
groups[group_id] = [int(aktif[a]) for a in anggota]
|
|
250
|
+
terpakai[anggota] = True
|
|
225
251
|
group_id += 1
|
|
226
|
-
used.add(i)
|
|
227
252
|
return groups if groups else None
|
|
228
253
|
|
|
254
|
+
def _validate_sample_weight(self, sample_weight, n_samples):
|
|
255
|
+
# 1. Kembalikan None Kalau Gak Ada Bobot Atau Bobot Yang Udah Dinormalisasi Ke Rata-Rata Satu
|
|
256
|
+
if sample_weight is None:
|
|
257
|
+
return None
|
|
258
|
+
s = np.asarray(sample_weight, dtype=np.float64).ravel()
|
|
259
|
+
if len(s) != n_samples:
|
|
260
|
+
raise ValueError("Panjang bobot sampel harus sama dengan jumlah baris X.")
|
|
261
|
+
if not np.all(np.isfinite(s)) or np.any(s < 0):
|
|
262
|
+
raise ValueError("Bobot sampel harus berhingga dan tidak negatif.")
|
|
263
|
+
total = s.sum()
|
|
264
|
+
if total <= 0:
|
|
265
|
+
raise ValueError("Jumlah bobot sampel harus positif.")
|
|
266
|
+
s = s * (n_samples / total)
|
|
267
|
+
# 2. Bobot Yang Semuanya Sama Dianggap Sama Dengan Tanpa Bobot
|
|
268
|
+
if np.ptp(s) <= 1e-12:
|
|
269
|
+
return None
|
|
270
|
+
return s
|
|
271
|
+
|
|
229
272
|
def _subsample_indices(self, n_samples, rng):
|
|
230
|
-
# Kembalikan
|
|
273
|
+
# 1. Kembalikan Indeks Subsample Acak Tanpa Pengembalian
|
|
231
274
|
if self.subsample >= 1.0:
|
|
232
275
|
return np.arange(n_samples)
|
|
233
276
|
n_sub = max(1, int(n_samples * self.subsample))
|
|
234
277
|
return rng.choice(n_samples, size=n_sub, replace=False)
|
|
235
278
|
|
|
236
279
|
def _build_operators(self):
|
|
237
|
-
#
|
|
280
|
+
# 1. Gabungin Daftar Operator Terus Tambahin XOR Kalau Diaktifin
|
|
238
281
|
ops = list(self.operators)
|
|
239
282
|
if self.use_xor and 'XOR' not in ops:
|
|
240
283
|
ops.append('XOR')
|
|
241
284
|
return ops
|
|
242
285
|
|
|
243
286
|
def _seleksi_fitur(self, X_bin, gradients, hessians):
|
|
244
|
-
dot_gf = X_bin.T @ gradients
|
|
245
|
-
H_f = X_bin.T @ hessians
|
|
246
|
-
n_f = X_bin.sum(axis=0)
|
|
247
|
-
lambda_f = self.lambda0 / np.sqrt(n_f + 1.0)
|
|
248
|
-
gains = (dot_gf ** 2) / (H_f + lambda_f + 1e-10)
|
|
249
|
-
|
|
250
287
|
if self.feature_selection_threshold <= 0.0:
|
|
251
288
|
return np.arange(X_bin.shape[1])
|
|
252
289
|
|
|
290
|
+
dot_gf = X_bin.T @ gradients
|
|
291
|
+
H_f = X_bin.T @ hessians
|
|
292
|
+
n_f = X_bin.sum(axis=0)
|
|
293
|
+
lambda_f = self.lambda0 / np.sqrt(n_f + 1.0)
|
|
294
|
+
gains = (dot_gf ** 2) / (H_f + lambda_f + 1e-10)
|
|
295
|
+
|
|
253
296
|
nilai_persentil = np.percentile(gains, self.feature_selection_threshold * 100)
|
|
254
297
|
selected = np.where(gains >= nilai_persentil)[0]
|
|
255
298
|
if len(selected) == 0:
|
|
@@ -283,14 +326,19 @@ class BinBoost:
|
|
|
283
326
|
X : array-like of shape (n_samples, n_features)
|
|
284
327
|
y : array-like of shape (n_samples,), nilai 0 atau 1
|
|
285
328
|
sample_weight : array-like of shape (n_samples,), opsional
|
|
329
|
+
Bobot tidak negatif per sampel. Penjelasan lengkap ada di docstring kelas.
|
|
286
330
|
|
|
287
331
|
Kembalian
|
|
288
332
|
----------
|
|
289
333
|
self
|
|
290
334
|
"""
|
|
335
|
+
if self.ohe_policy not in ('block', 'allow'):
|
|
336
|
+
raise ValueError("Parameter ohe_policy harus 'block' atau 'allow'.")
|
|
337
|
+
|
|
291
338
|
self._input_feature_names = None
|
|
292
339
|
X, y = self._validate_input(X, y)
|
|
293
340
|
n_samples, n_features = X.shape
|
|
341
|
+
s = self._validate_sample_weight(sample_weight, n_samples)
|
|
294
342
|
|
|
295
343
|
self.n_features_in_ = n_features
|
|
296
344
|
self.feature_names_in_ = (
|
|
@@ -309,6 +357,7 @@ class BinBoost:
|
|
|
309
357
|
)
|
|
310
358
|
|
|
311
359
|
numeric_cols = self._detect_numeric_cols(X)
|
|
360
|
+
self.numeric_cols_ = numeric_cols
|
|
312
361
|
self.ohe_groups_ = self._detect_ohe_groups(X)
|
|
313
362
|
|
|
314
363
|
self._binarizer = GradientBinarizer(
|
|
@@ -324,7 +373,7 @@ class BinBoost:
|
|
|
324
373
|
feature_selection_threshold=0.0,
|
|
325
374
|
min_samples_rule=self.min_samples_rule,
|
|
326
375
|
rule_complexity_penalty=self.rule_complexity_penalty,
|
|
327
|
-
ohe_groups=self.ohe_groups_,
|
|
376
|
+
ohe_groups=self.ohe_groups_ if self.ohe_policy == 'block' else None,
|
|
328
377
|
gamma=self.gamma,
|
|
329
378
|
)
|
|
330
379
|
|
|
@@ -333,31 +382,41 @@ class BinBoost:
|
|
|
333
382
|
self.rule_weights_ = []
|
|
334
383
|
self.thresholds_ = {col: [] for col in numeric_cols}
|
|
335
384
|
self.train_score_ = []
|
|
336
|
-
self._F = loss_fn.init_F(y)
|
|
385
|
+
self._F = loss_fn.init_F(y, sample_weight=s)
|
|
386
|
+
# 1. Simpan Skor Awal Biar Prediksi Sama Dengan Waktu Training
|
|
387
|
+
self.F0_ = float(self._F[0])
|
|
388
|
+
else:
|
|
389
|
+
# 2. Ubah Atribut Array Balik Jadi List Biar Bisa Ditambah
|
|
390
|
+
self.rule_weights_ = [np.asarray(w) for w in self.rule_weights_]
|
|
391
|
+
self.train_score_ = list(self.train_score_)
|
|
337
392
|
|
|
338
393
|
no_improve_count = 0
|
|
339
394
|
best_score = np.inf
|
|
340
395
|
|
|
341
396
|
for m in range(self.n_estimators):
|
|
342
|
-
#
|
|
397
|
+
# 3. Hitung Gradient Dari Loss Yang Dipilih
|
|
343
398
|
gradients = loss_fn.gradient(y, self._F)
|
|
344
|
-
hessians = loss_fn.hessian(y, self._F)
|
|
399
|
+
hessians = loss_fn.hessian(y, self._F)
|
|
345
400
|
|
|
346
|
-
#
|
|
401
|
+
# 4. Ambil Subsample Buat Iterasi Ini
|
|
347
402
|
idx = self._subsample_indices(n_samples, rng)
|
|
348
403
|
X_sub = X[idx]
|
|
349
404
|
g_sub = gradients[idx]
|
|
350
|
-
h_sub = hessians[idx]
|
|
405
|
+
h_sub = hessians[idx]
|
|
406
|
+
s_sub = s[idx] if s is not None else None
|
|
407
|
+
# a. Kalikan Gradient Dan Hessian Dengan Bobot Buat Binarisasi Dan Seleksi Fitur
|
|
408
|
+
gw_sub = g_sub * s_sub if s_sub is not None else g_sub
|
|
409
|
+
hw_sub = h_sub * s_sub if s_sub is not None else h_sub
|
|
351
410
|
|
|
352
|
-
#
|
|
353
|
-
X_bin, thresholds_iter = binarizer.transform(X_sub,
|
|
411
|
+
# 5. Binarisasi Fitur Numerik Pakai Strategi Yang Dipilih
|
|
412
|
+
X_bin, thresholds_iter = binarizer.transform(X_sub, gw_sub, numeric_cols)
|
|
354
413
|
|
|
355
|
-
#
|
|
356
|
-
candidate_features = self._seleksi_fitur(X_bin,
|
|
414
|
+
# 6. Pilih Fitur Kandidat Pakai Kriteria Penurunan Galat Yang Konsisten
|
|
415
|
+
candidate_features = self._seleksi_fitur(X_bin, gw_sub, hw_sub)
|
|
357
416
|
rule_finder.forced_candidate_features = candidate_features
|
|
358
417
|
|
|
359
|
-
#
|
|
360
|
-
rule = rule_finder.find_best_rule(X_bin, g_sub)
|
|
418
|
+
# 7. Cari Aturan Terbaik Pakai Beam Search
|
|
419
|
+
rule = rule_finder.find_best_rule(X_bin, g_sub, s_sub)
|
|
361
420
|
if rule is None:
|
|
362
421
|
break
|
|
363
422
|
|
|
@@ -365,24 +424,26 @@ class BinBoost:
|
|
|
365
424
|
list(self.feature_names_in_) if self.feature_names_in_ is not None else None
|
|
366
425
|
)
|
|
367
426
|
|
|
368
|
-
#
|
|
369
|
-
X_bin_full
|
|
427
|
+
# 8. Pakai Threshold Yang Sama Ke Seluruh Data Terus Evaluasi Aturannya
|
|
428
|
+
X_bin_full = binarizer.apply_thresholds(X, thresholds_iter, numeric_cols)
|
|
370
429
|
h_full = rule.evaluate(X_bin_full)
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
430
|
+
gw = gradients * s if s is not None else gradients
|
|
431
|
+
hw = hessians * s if s is not None else hessians
|
|
432
|
+
w1, w0 = self._hitung_bobot_dua_sisi(h_full, gw, hw, len(rule.features))
|
|
433
|
+
|
|
434
|
+
# 9. Update Skor Pakai Kontribusi Dua Sisi
|
|
374
435
|
self._F = self._F + self.learning_rate * (w1 * h_full + w0 * (1.0 - h_full))
|
|
375
436
|
|
|
376
|
-
#
|
|
437
|
+
# 10. Simpan Aturan Beserta Bobot Dua Sisi Dan Thresholdnya
|
|
377
438
|
self.estimators_.append(rule)
|
|
378
439
|
self.rule_weights_.append(np.array([w0, w1]))
|
|
379
|
-
for col, t in
|
|
440
|
+
for col, t in thresholds_iter.items():
|
|
380
441
|
self.thresholds_[col].append(t)
|
|
381
442
|
|
|
382
|
-
current_score = loss_fn.loss(y, self._F)
|
|
443
|
+
current_score = loss_fn.loss(y, self._F, sample_weight=s)
|
|
383
444
|
self.train_score_.append(current_score)
|
|
384
445
|
|
|
385
|
-
#
|
|
446
|
+
# 11. Cek Syarat Early Stopping Kalau Diaktifin
|
|
386
447
|
if self.n_iter_no_change is not None:
|
|
387
448
|
if current_score < best_score - self.tol:
|
|
388
449
|
best_score = current_score
|
|
@@ -394,22 +455,22 @@ class BinBoost:
|
|
|
394
455
|
|
|
395
456
|
self.n_estimators_ = len(self.estimators_)
|
|
396
457
|
self.train_score_ = np.array(self.train_score_)
|
|
397
|
-
self.rule_weights_ = np.array(self.rule_weights_)
|
|
458
|
+
self.rule_weights_ = np.array(self.rule_weights_).reshape(-1, 2)
|
|
398
459
|
self.rules_ = [r.to_string(self.feature_names_in_) for r in self.estimators_]
|
|
399
460
|
self._compute_feature_importances(n_features)
|
|
400
461
|
self.is_fitted_ = True
|
|
401
|
-
#
|
|
462
|
+
# 12. Tentuin Threshold Prediksi Terbaik Setelah Semua Iterasi Kelar
|
|
402
463
|
if self.threshold == 'auto':
|
|
403
|
-
self._threshold_fitted = self._cari_threshold_optimal(X, y)
|
|
464
|
+
self._threshold_fitted = self._cari_threshold_optimal(X, y, sample_weight=s)
|
|
404
465
|
else:
|
|
405
466
|
self._threshold_fitted = float(self.threshold)
|
|
406
467
|
return self
|
|
407
468
|
|
|
408
469
|
def _compute_feature_importances(self, n_features):
|
|
409
|
-
# Hitung
|
|
470
|
+
# 1. Hitung Feature Importance Dari Selisih Bobot Dua Sisi Tiap Aturan
|
|
410
471
|
importances = np.zeros(n_features)
|
|
411
472
|
for rule, weights in zip(self.estimators_, self.rule_weights_):
|
|
412
|
-
#
|
|
473
|
+
# a. Pakai Selisih Mutlak Bobot Memenuhi Dan Tidak Memenuhi Sebagai Ukuran Pengaruh
|
|
413
474
|
gain = np.abs(weights[1] - weights[0])
|
|
414
475
|
for f in rule.features:
|
|
415
476
|
importances[f] += gain
|
|
@@ -420,48 +481,60 @@ class BinBoost:
|
|
|
420
481
|
if not getattr(self, 'is_fitted_', False):
|
|
421
482
|
raise RuntimeError("Model belum dilatih. Panggil fit terlebih dahulu.")
|
|
422
483
|
|
|
423
|
-
def _cari_threshold_optimal(self, X, y):
|
|
424
|
-
# Cari
|
|
484
|
+
def _cari_threshold_optimal(self, X, y, sample_weight=None):
|
|
485
|
+
# 1. Cari Threshold Prediksi Terbaik Pakai Indeks Youden Di Data Training
|
|
425
486
|
proba = self.predict_proba(X)[:, 1]
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
487
|
+
bobot = np.ones(len(y)) if sample_weight is None else np.asarray(sample_weight, dtype=np.float64)
|
|
488
|
+
is_pos = (y == 1)
|
|
489
|
+
pos = bobot[is_pos].sum()
|
|
490
|
+
neg = bobot[~is_pos].sum()
|
|
491
|
+
if pos <= 0 or neg <= 0:
|
|
492
|
+
return 0.5
|
|
493
|
+
ambang = np.unique(proba)
|
|
494
|
+
# 2. Lewati Pencarian Kalau Semua Probabilitas Sama
|
|
495
|
+
if len(ambang) < 2:
|
|
432
496
|
return 0.5
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
497
|
+
|
|
498
|
+
def bobot_di_atas(p, w):
|
|
499
|
+
# 3. Hitung Jumlah Bobot Di Atas Tiap Threshold Lewat Sorting
|
|
500
|
+
urut = np.argsort(p, kind='stable')
|
|
501
|
+
kum = np.concatenate([[0.0], np.cumsum(w[urut])])
|
|
502
|
+
idx = np.searchsorted(p[urut], ambang, side='left')
|
|
503
|
+
return kum[-1] - kum[idx]
|
|
504
|
+
|
|
505
|
+
tp = bobot_di_atas(proba[is_pos], bobot[is_pos])
|
|
506
|
+
fp = bobot_di_atas(proba[~is_pos], bobot[~is_pos])
|
|
507
|
+
tn = neg - fp
|
|
508
|
+
sensitivity = tp / pos
|
|
509
|
+
specificity = tn / neg
|
|
510
|
+
# 4. Hitung Indeks Youden Dari Sensitivity Plus Specificity Dikurangi Satu
|
|
511
|
+
youden = sensitivity + specificity - 1
|
|
512
|
+
# 5. Ambil Threshold Terkecil Yang Indeks Youdennya Paling Tinggi
|
|
513
|
+
return float(ambang[int(np.argmax(youden))])
|
|
514
|
+
|
|
446
515
|
def _binarize_untuk_iterasi(self, X, iterasi_idx, numeric_cols):
|
|
447
|
-
|
|
516
|
+
# 1. Binarisasi Pakai Threshold Yang Disimpan Di Iterasi Itu
|
|
517
|
+
if not numeric_cols:
|
|
518
|
+
return X
|
|
519
|
+
X_bin = X.copy()
|
|
448
520
|
for col in numeric_cols:
|
|
449
521
|
t = self.thresholds_[col][iterasi_idx]
|
|
450
522
|
X_bin[:, col] = (X[:, col] > t).astype(np.float64)
|
|
451
523
|
return X_bin
|
|
452
524
|
|
|
525
|
+
def _aktivasi_per_aturan(self, X):
|
|
526
|
+
# 1. Hasilkan Tiap Aturan Beserta Bobot Dan Aktivasinya Di Data
|
|
527
|
+
numeric_cols = self.numeric_cols_
|
|
528
|
+
for i, (rule, weights) in enumerate(zip(self.estimators_, self.rule_weights_)):
|
|
529
|
+
X_bin = self._binarize_untuk_iterasi(X, i, numeric_cols)
|
|
530
|
+
yield i, rule, weights, rule.evaluate(X_bin)
|
|
531
|
+
|
|
453
532
|
def _decision_function(self, X):
|
|
454
533
|
self._check_is_fitted()
|
|
455
534
|
X, _ = self._validate_input(X)
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
for
|
|
459
|
-
if self.binarize_strategy == 'gradient' and numeric_cols:
|
|
460
|
-
X_bin = self._binarize_untuk_iterasi(X, i, numeric_cols)
|
|
461
|
-
else:
|
|
462
|
-
dummy_grad = np.ones(X.shape[0])
|
|
463
|
-
X_bin, _ = self._binarizer.transform(X, dummy_grad, numeric_cols)
|
|
464
|
-
h = rule.evaluate(X_bin)
|
|
535
|
+
# 1. Mulai Dari Skor Awal Yang Sama Kayak Waktu Training
|
|
536
|
+
F = np.full(X.shape[0], getattr(self, 'F0_', 0.0))
|
|
537
|
+
for _, _, weights, h in self._aktivasi_per_aturan(X):
|
|
465
538
|
w0, w1 = weights[0], weights[1]
|
|
466
539
|
F += self.learning_rate * (w1 * h + w0 * (1.0 - h))
|
|
467
540
|
return F
|
|
@@ -521,15 +594,8 @@ class BinBoost:
|
|
|
521
594
|
"""
|
|
522
595
|
self._check_is_fitted()
|
|
523
596
|
X, _ = self._validate_input(X)
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
for i, (rule, weights) in enumerate(zip(self.estimators_, self.rule_weights_)):
|
|
527
|
-
if self.binarize_strategy == 'gradient' and numeric_cols:
|
|
528
|
-
X_bin = self._binarize_untuk_iterasi(X, i, numeric_cols)
|
|
529
|
-
else:
|
|
530
|
-
dummy_grad = np.ones(X.shape[0])
|
|
531
|
-
X_bin, _ = self._binarizer.transform(X, dummy_grad, numeric_cols)
|
|
532
|
-
h = rule.evaluate(X_bin)
|
|
597
|
+
F = np.full(X.shape[0], getattr(self, 'F0_', 0.0))
|
|
598
|
+
for _, _, weights, h in self._aktivasi_per_aturan(X):
|
|
533
599
|
w0, w1 = weights[0], weights[1]
|
|
534
600
|
F += self.learning_rate * (w1 * h + w0 * (1.0 - h))
|
|
535
601
|
p = sigmoid(F)
|
|
@@ -549,24 +615,16 @@ class BinBoost:
|
|
|
549
615
|
def apply(self, X):
|
|
550
616
|
"""
|
|
551
617
|
Kembalikan nilai aktivasi setiap aturan untuk setiap sampel.
|
|
552
|
-
|
|
618
|
+
|
|
553
619
|
Kembalian
|
|
554
620
|
----------
|
|
555
621
|
ndarray of shape (n_samples, n_estimators_)
|
|
556
622
|
"""
|
|
557
623
|
self._check_is_fitted()
|
|
558
624
|
X, _ = self._validate_input(X)
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
for i, rule in enumerate(self.estimators_):
|
|
563
|
-
if self.binarize_strategy == 'gradient' and numeric_cols:
|
|
564
|
-
X_bin = self._binarize_untuk_iterasi(X, i, numeric_cols)
|
|
565
|
-
else:
|
|
566
|
-
dummy_grad = np.ones(X.shape[0])
|
|
567
|
-
X_bin, _ = self._binarizer.transform(X, dummy_grad, numeric_cols)
|
|
568
|
-
results.append(rule.evaluate(X_bin))
|
|
569
|
-
|
|
625
|
+
results = [h for _, _, _, h in self._aktivasi_per_aturan(X)]
|
|
626
|
+
if not results:
|
|
627
|
+
return np.zeros((X.shape[0], 0))
|
|
570
628
|
return np.column_stack(results)
|
|
571
629
|
|
|
572
630
|
def get_params(self, deep=True):
|
|
@@ -592,8 +650,8 @@ class BinBoost:
|
|
|
592
650
|
'beam_width': self.beam_width,
|
|
593
651
|
'subsample': self.subsample,
|
|
594
652
|
'min_samples_rule': self.min_samples_rule,
|
|
595
|
-
'lambda0': self.lambda0,
|
|
596
|
-
'gamma': self.gamma,
|
|
653
|
+
'lambda0': self.lambda0,
|
|
654
|
+
'gamma': self.gamma,
|
|
597
655
|
'binarize_strategy': self.binarize_strategy,
|
|
598
656
|
'n_thresholds': self.n_thresholds,
|
|
599
657
|
'n_iter_no_change': self.n_iter_no_change,
|
|
@@ -601,6 +659,7 @@ class BinBoost:
|
|
|
601
659
|
'random_state': self.random_state,
|
|
602
660
|
'warm_start': self.warm_start,
|
|
603
661
|
'threshold': self.threshold,
|
|
662
|
+
'ohe_policy': self.ohe_policy,
|
|
604
663
|
}
|
|
605
664
|
|
|
606
665
|
def set_params(self, **params):
|
|
@@ -670,4 +729,4 @@ class BinBoost:
|
|
|
670
729
|
else f"X{f}"
|
|
671
730
|
)
|
|
672
731
|
usage[name] = usage.get(name, 0) + 1
|
|
673
|
-
return usage
|
|
732
|
+
return usage
|