binboost 0.2.4__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.4 → binboost-0.2.5}/PKG-INFO +1 -1
- {binboost-0.2.4 → binboost-0.2.5}/binboost/__init__.py +1 -1
- {binboost-0.2.4 → binboost-0.2.5}/binboost/binarizer.py +31 -22
- {binboost-0.2.4 → binboost-0.2.5}/binboost/binboost.py +180 -122
- binboost-0.2.5/binboost/loss.py +118 -0
- {binboost-0.2.4 → binboost-0.2.5}/binboost/multiclass.py +70 -72
- binboost-0.2.5/binboost/rule.py +282 -0
- {binboost-0.2.4 → binboost-0.2.5}/binboost.egg-info/PKG-INFO +1 -1
- {binboost-0.2.4 → binboost-0.2.5}/pyproject.toml +1 -1
- {binboost-0.2.4 → binboost-0.2.5}/setup.py +1 -1
- binboost-0.2.4/binboost/loss.py +0 -115
- binboost-0.2.4/binboost/rule.py +0 -220
- {binboost-0.2.4 → binboost-0.2.5}/LICENSE +0 -0
- {binboost-0.2.4 → binboost-0.2.5}/README.md +0 -0
- {binboost-0.2.4 → binboost-0.2.5}/binboost.egg-info/SOURCES.txt +0 -0
- {binboost-0.2.4 → binboost-0.2.5}/binboost.egg-info/dependency_links.txt +0 -0
- {binboost-0.2.4 → binboost-0.2.5}/binboost.egg-info/requires.txt +0 -0
- {binboost-0.2.4 → binboost-0.2.5}/binboost.egg-info/top_level.txt +0 -0
- {binboost-0.2.4 → binboost-0.2.5}/setup.cfg +0 -0
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
# binboost/binarizer.py
|
|
2
1
|
import numpy as np
|
|
3
2
|
|
|
4
3
|
|
|
5
4
|
class GradientBinarizer:
|
|
6
|
-
# Binarisasi
|
|
5
|
+
# 1. Binarisasi Fitur Numerik Berdasarkan Korelasi Dengan Gradient Saat Ini
|
|
7
6
|
def __init__(self, strategy='gradient', n_thresholds='auto'):
|
|
8
7
|
self.strategy = strategy
|
|
9
8
|
self.n_thresholds = n_thresholds
|
|
10
|
-
#
|
|
9
|
+
# 1. Simpan Threshold Tetap Per Fitur Buat Strategi Selain Gradient
|
|
11
10
|
self._fixed_thresholds = {}
|
|
12
11
|
|
|
13
|
-
def
|
|
14
|
-
# Kembalikan
|
|
15
|
-
unique = np.unique(values)
|
|
12
|
+
def _candidates_from_unique(self, unique):
|
|
13
|
+
# 1. Kembalikan Kandidat Threshold Dari Nilai Unik Yang Udah Terurut
|
|
16
14
|
if len(unique) <= 1:
|
|
17
15
|
return np.array([])
|
|
18
16
|
if self.n_thresholds == 'auto':
|
|
@@ -24,23 +22,28 @@ class GradientBinarizer:
|
|
|
24
22
|
idx = np.linspace(0, len(unique) - 2, max_t, dtype=int)
|
|
25
23
|
return (unique[idx] + unique[idx + 1]) / 2
|
|
26
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
|
+
|
|
27
29
|
def _best_threshold_gradient(self, values, gradients):
|
|
28
|
-
# 1. Cari
|
|
29
|
-
|
|
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)
|
|
30
33
|
if len(candidates) == 0:
|
|
31
34
|
return 0.0, np.zeros(len(values), dtype=np.float64)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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))]
|
|
40
43
|
return best_t, (values > best_t).astype(np.float64)
|
|
41
44
|
|
|
42
45
|
def _best_threshold_quantile(self, col_idx, values):
|
|
43
|
-
# 1.
|
|
46
|
+
# 1. Pakai Threshold Tetap Dari Kuantil Distribusi Data
|
|
44
47
|
if col_idx not in self._fixed_thresholds:
|
|
45
48
|
candidates = self._get_candidates(values)
|
|
46
49
|
if len(candidates) == 0:
|
|
@@ -51,16 +54,15 @@ class GradientBinarizer:
|
|
|
51
54
|
return t, (values > t).astype(np.float64)
|
|
52
55
|
|
|
53
56
|
def _best_threshold_uniform(self, col_idx, values):
|
|
54
|
-
# 1.
|
|
57
|
+
# 1. Pakai Threshold Di Tengah Rentang Nilai Fitur
|
|
55
58
|
if col_idx not in self._fixed_thresholds:
|
|
56
59
|
self._fixed_thresholds[col_idx] = (values.min() + values.max()) / 2
|
|
57
60
|
t = self._fixed_thresholds[col_idx]
|
|
58
61
|
return t, (values > t).astype(np.float64)
|
|
59
62
|
|
|
60
63
|
def _best_threshold_kmeans(self, col_idx, values):
|
|
61
|
-
# 1.
|
|
64
|
+
# 1. Pakai Titik Tengah Dua Centroid KMeans Satu Dimensi Sebagai Threshold
|
|
62
65
|
if col_idx not in self._fixed_thresholds:
|
|
63
|
-
v = values.reshape(-1, 1)
|
|
64
66
|
c1 = np.percentile(values, 25)
|
|
65
67
|
c2 = np.percentile(values, 75)
|
|
66
68
|
for _ in range(100):
|
|
@@ -75,8 +77,8 @@ class GradientBinarizer:
|
|
|
75
77
|
return t, (values > t).astype(np.float64)
|
|
76
78
|
|
|
77
79
|
def transform(self, X, gradients, numeric_cols):
|
|
78
|
-
# Binarisasi
|
|
79
|
-
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)
|
|
80
82
|
thresholds_this_iter = {}
|
|
81
83
|
for col_idx in numeric_cols:
|
|
82
84
|
values = X[:, col_idx].astype(np.float64)
|
|
@@ -93,3 +95,10 @@ class GradientBinarizer:
|
|
|
93
95
|
X_bin[:, col_idx] = binary
|
|
94
96
|
thresholds_this_iter[col_idx] = t
|
|
95
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_ = (
|
|
@@ -325,7 +373,7 @@ class BinBoost:
|
|
|
325
373
|
feature_selection_threshold=0.0,
|
|
326
374
|
min_samples_rule=self.min_samples_rule,
|
|
327
375
|
rule_complexity_penalty=self.rule_complexity_penalty,
|
|
328
|
-
ohe_groups=self.ohe_groups_,
|
|
376
|
+
ohe_groups=self.ohe_groups_ if self.ohe_policy == 'block' else None,
|
|
329
377
|
gamma=self.gamma,
|
|
330
378
|
)
|
|
331
379
|
|
|
@@ -334,31 +382,41 @@ class BinBoost:
|
|
|
334
382
|
self.rule_weights_ = []
|
|
335
383
|
self.thresholds_ = {col: [] for col in numeric_cols}
|
|
336
384
|
self.train_score_ = []
|
|
337
|
-
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_)
|
|
338
392
|
|
|
339
393
|
no_improve_count = 0
|
|
340
394
|
best_score = np.inf
|
|
341
395
|
|
|
342
396
|
for m in range(self.n_estimators):
|
|
343
|
-
#
|
|
397
|
+
# 3. Hitung Gradient Dari Loss Yang Dipilih
|
|
344
398
|
gradients = loss_fn.gradient(y, self._F)
|
|
345
|
-
hessians = loss_fn.hessian(y, self._F)
|
|
399
|
+
hessians = loss_fn.hessian(y, self._F)
|
|
346
400
|
|
|
347
|
-
#
|
|
401
|
+
# 4. Ambil Subsample Buat Iterasi Ini
|
|
348
402
|
idx = self._subsample_indices(n_samples, rng)
|
|
349
403
|
X_sub = X[idx]
|
|
350
404
|
g_sub = gradients[idx]
|
|
351
|
-
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
|
|
352
410
|
|
|
353
|
-
#
|
|
354
|
-
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)
|
|
355
413
|
|
|
356
|
-
#
|
|
357
|
-
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)
|
|
358
416
|
rule_finder.forced_candidate_features = candidate_features
|
|
359
417
|
|
|
360
|
-
#
|
|
361
|
-
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)
|
|
362
420
|
if rule is None:
|
|
363
421
|
break
|
|
364
422
|
|
|
@@ -366,24 +424,26 @@ class BinBoost:
|
|
|
366
424
|
list(self.feature_names_in_) if self.feature_names_in_ is not None else None
|
|
367
425
|
)
|
|
368
426
|
|
|
369
|
-
#
|
|
370
|
-
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)
|
|
371
429
|
h_full = rule.evaluate(X_bin_full)
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
|
375
435
|
self._F = self._F + self.learning_rate * (w1 * h_full + w0 * (1.0 - h_full))
|
|
376
436
|
|
|
377
|
-
#
|
|
437
|
+
# 10. Simpan Aturan Beserta Bobot Dua Sisi Dan Thresholdnya
|
|
378
438
|
self.estimators_.append(rule)
|
|
379
439
|
self.rule_weights_.append(np.array([w0, w1]))
|
|
380
|
-
for col, t in
|
|
440
|
+
for col, t in thresholds_iter.items():
|
|
381
441
|
self.thresholds_[col].append(t)
|
|
382
442
|
|
|
383
|
-
current_score = loss_fn.loss(y, self._F)
|
|
443
|
+
current_score = loss_fn.loss(y, self._F, sample_weight=s)
|
|
384
444
|
self.train_score_.append(current_score)
|
|
385
445
|
|
|
386
|
-
#
|
|
446
|
+
# 11. Cek Syarat Early Stopping Kalau Diaktifin
|
|
387
447
|
if self.n_iter_no_change is not None:
|
|
388
448
|
if current_score < best_score - self.tol:
|
|
389
449
|
best_score = current_score
|
|
@@ -395,22 +455,22 @@ class BinBoost:
|
|
|
395
455
|
|
|
396
456
|
self.n_estimators_ = len(self.estimators_)
|
|
397
457
|
self.train_score_ = np.array(self.train_score_)
|
|
398
|
-
self.rule_weights_ = np.array(self.rule_weights_)
|
|
458
|
+
self.rule_weights_ = np.array(self.rule_weights_).reshape(-1, 2)
|
|
399
459
|
self.rules_ = [r.to_string(self.feature_names_in_) for r in self.estimators_]
|
|
400
460
|
self._compute_feature_importances(n_features)
|
|
401
461
|
self.is_fitted_ = True
|
|
402
|
-
#
|
|
462
|
+
# 12. Tentuin Threshold Prediksi Terbaik Setelah Semua Iterasi Kelar
|
|
403
463
|
if self.threshold == 'auto':
|
|
404
|
-
self._threshold_fitted = self._cari_threshold_optimal(X, y)
|
|
464
|
+
self._threshold_fitted = self._cari_threshold_optimal(X, y, sample_weight=s)
|
|
405
465
|
else:
|
|
406
466
|
self._threshold_fitted = float(self.threshold)
|
|
407
467
|
return self
|
|
408
468
|
|
|
409
469
|
def _compute_feature_importances(self, n_features):
|
|
410
|
-
# Hitung
|
|
470
|
+
# 1. Hitung Feature Importance Dari Selisih Bobot Dua Sisi Tiap Aturan
|
|
411
471
|
importances = np.zeros(n_features)
|
|
412
472
|
for rule, weights in zip(self.estimators_, self.rule_weights_):
|
|
413
|
-
#
|
|
473
|
+
# a. Pakai Selisih Mutlak Bobot Memenuhi Dan Tidak Memenuhi Sebagai Ukuran Pengaruh
|
|
414
474
|
gain = np.abs(weights[1] - weights[0])
|
|
415
475
|
for f in rule.features:
|
|
416
476
|
importances[f] += gain
|
|
@@ -421,48 +481,60 @@ class BinBoost:
|
|
|
421
481
|
if not getattr(self, 'is_fitted_', False):
|
|
422
482
|
raise RuntimeError("Model belum dilatih. Panggil fit terlebih dahulu.")
|
|
423
483
|
|
|
424
|
-
def _cari_threshold_optimal(self, X, y):
|
|
425
|
-
# Cari
|
|
484
|
+
def _cari_threshold_optimal(self, X, y, sample_weight=None):
|
|
485
|
+
# 1. Cari Threshold Prediksi Terbaik Pakai Indeks Youden Di Data Training
|
|
426
486
|
proba = self.predict_proba(X)[:, 1]
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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:
|
|
433
496
|
return 0.5
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
+
|
|
447
515
|
def _binarize_untuk_iterasi(self, X, iterasi_idx, numeric_cols):
|
|
448
|
-
|
|
516
|
+
# 1. Binarisasi Pakai Threshold Yang Disimpan Di Iterasi Itu
|
|
517
|
+
if not numeric_cols:
|
|
518
|
+
return X
|
|
519
|
+
X_bin = X.copy()
|
|
449
520
|
for col in numeric_cols:
|
|
450
521
|
t = self.thresholds_[col][iterasi_idx]
|
|
451
522
|
X_bin[:, col] = (X[:, col] > t).astype(np.float64)
|
|
452
523
|
return X_bin
|
|
453
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
|
+
|
|
454
532
|
def _decision_function(self, X):
|
|
455
533
|
self._check_is_fitted()
|
|
456
534
|
X, _ = self._validate_input(X)
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
for
|
|
460
|
-
if self.binarize_strategy == 'gradient' and numeric_cols:
|
|
461
|
-
X_bin = self._binarize_untuk_iterasi(X, i, numeric_cols)
|
|
462
|
-
else:
|
|
463
|
-
dummy_grad = np.ones(X.shape[0])
|
|
464
|
-
X_bin, _ = self._binarizer.transform(X, dummy_grad, numeric_cols)
|
|
465
|
-
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):
|
|
466
538
|
w0, w1 = weights[0], weights[1]
|
|
467
539
|
F += self.learning_rate * (w1 * h + w0 * (1.0 - h))
|
|
468
540
|
return F
|
|
@@ -522,15 +594,8 @@ class BinBoost:
|
|
|
522
594
|
"""
|
|
523
595
|
self._check_is_fitted()
|
|
524
596
|
X, _ = self._validate_input(X)
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
for i, (rule, weights) in enumerate(zip(self.estimators_, self.rule_weights_)):
|
|
528
|
-
if self.binarize_strategy == 'gradient' and numeric_cols:
|
|
529
|
-
X_bin = self._binarize_untuk_iterasi(X, i, numeric_cols)
|
|
530
|
-
else:
|
|
531
|
-
dummy_grad = np.ones(X.shape[0])
|
|
532
|
-
X_bin, _ = self._binarizer.transform(X, dummy_grad, numeric_cols)
|
|
533
|
-
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):
|
|
534
599
|
w0, w1 = weights[0], weights[1]
|
|
535
600
|
F += self.learning_rate * (w1 * h + w0 * (1.0 - h))
|
|
536
601
|
p = sigmoid(F)
|
|
@@ -550,24 +615,16 @@ class BinBoost:
|
|
|
550
615
|
def apply(self, X):
|
|
551
616
|
"""
|
|
552
617
|
Kembalikan nilai aktivasi setiap aturan untuk setiap sampel.
|
|
553
|
-
|
|
618
|
+
|
|
554
619
|
Kembalian
|
|
555
620
|
----------
|
|
556
621
|
ndarray of shape (n_samples, n_estimators_)
|
|
557
622
|
"""
|
|
558
623
|
self._check_is_fitted()
|
|
559
624
|
X, _ = self._validate_input(X)
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
for i, rule in enumerate(self.estimators_):
|
|
564
|
-
if self.binarize_strategy == 'gradient' and numeric_cols:
|
|
565
|
-
X_bin = self._binarize_untuk_iterasi(X, i, numeric_cols)
|
|
566
|
-
else:
|
|
567
|
-
dummy_grad = np.ones(X.shape[0])
|
|
568
|
-
X_bin, _ = self._binarizer.transform(X, dummy_grad, numeric_cols)
|
|
569
|
-
results.append(rule.evaluate(X_bin))
|
|
570
|
-
|
|
625
|
+
results = [h for _, _, _, h in self._aktivasi_per_aturan(X)]
|
|
626
|
+
if not results:
|
|
627
|
+
return np.zeros((X.shape[0], 0))
|
|
571
628
|
return np.column_stack(results)
|
|
572
629
|
|
|
573
630
|
def get_params(self, deep=True):
|
|
@@ -593,8 +650,8 @@ class BinBoost:
|
|
|
593
650
|
'beam_width': self.beam_width,
|
|
594
651
|
'subsample': self.subsample,
|
|
595
652
|
'min_samples_rule': self.min_samples_rule,
|
|
596
|
-
'lambda0': self.lambda0,
|
|
597
|
-
'gamma': self.gamma,
|
|
653
|
+
'lambda0': self.lambda0,
|
|
654
|
+
'gamma': self.gamma,
|
|
598
655
|
'binarize_strategy': self.binarize_strategy,
|
|
599
656
|
'n_thresholds': self.n_thresholds,
|
|
600
657
|
'n_iter_no_change': self.n_iter_no_change,
|
|
@@ -602,6 +659,7 @@ class BinBoost:
|
|
|
602
659
|
'random_state': self.random_state,
|
|
603
660
|
'warm_start': self.warm_start,
|
|
604
661
|
'threshold': self.threshold,
|
|
662
|
+
'ohe_policy': self.ohe_policy,
|
|
605
663
|
}
|
|
606
664
|
|
|
607
665
|
def set_params(self, **params):
|
|
@@ -671,4 +729,4 @@ class BinBoost:
|
|
|
671
729
|
else f"X{f}"
|
|
672
730
|
)
|
|
673
731
|
usage[name] = usage.get(name, 0) + 1
|
|
674
|
-
return usage
|
|
732
|
+
return usage
|