binboost 0.2.1__tar.gz → 0.2.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: binboost
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Gradient Boosting Berbasis Aturan Logika Adaptif untuk Fitur Biner
5
5
  Author: Rangga Wahyu Pratama
6
6
  License: MIT
@@ -1,5 +1,5 @@
1
1
  from .binboost import BinBoost
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.2"
4
4
  __author__ = "Rangga Wahyu Pratama"
5
5
  __all__ = ["BinBoost"]
@@ -9,7 +9,7 @@ class BinBoost:
9
9
  """
10
10
  BinBoost: Gradient Boosting Berbasis Aturan Logika Adaptif.
11
11
 
12
- Algoritma klasifikasi biner yang membangun ensemble aturan logika
12
+ Algoritma klasifikasi biner yang membangun ensemble aturan logika
13
13
  murni (AND, OR, XOR) dengan binarisasi fitur numerik adaptif
14
14
  berbasis gradien pada setiap iterasi boosting.
15
15
 
@@ -18,7 +18,7 @@ class BinBoost:
18
18
  n_estimators : int, default=100
19
19
  Jumlah iterasi boosting.
20
20
 
21
- learning_rate : float, default=0.1
21
+ learning_rate : float, default=0.2
22
22
  Faktor penyusutan kontribusi setiap aturan.
23
23
 
24
24
  loss : str, default='logistic'
@@ -33,7 +33,7 @@ class BinBoost:
33
33
  focal_alpha : float, default=0.25
34
34
  Parameter alpha pada Focal Loss. Hanya berlaku saat loss='focal'.
35
35
 
36
- max_rule_length : int, default=3
36
+ max_rule_length : int, default=4
37
37
  Jumlah maksimum fitur dalam satu aturan.
38
38
 
39
39
  operators : list, default=['AND', 'OR']
@@ -59,7 +59,7 @@ class BinBoost:
59
59
  min_samples_rule : int or float, default=5
60
60
  Jumlah minimum sampel yang harus memenuhi sebuah aturan.
61
61
 
62
- lambda0 : float, default=1.0
62
+ lambda0 : float, default=3.0
63
63
  Konstanta regularisasi dasar untuk bobot Newton adaptif (λ_R = lambda0 · L / sqrt(n_R+1)).
64
64
 
65
65
  binarize_strategy : str, default='gradient'
@@ -122,12 +122,12 @@ class BinBoost:
122
122
  def __init__(
123
123
  self,
124
124
  n_estimators=100,
125
- learning_rate=0.1,
125
+ learning_rate=0.2,
126
126
  loss='logistic',
127
127
  poly_epsilon=1.0,
128
128
  focal_gamma=2.0,
129
129
  focal_alpha=0.25,
130
- max_rule_length=3,
130
+ max_rule_length=4,
131
131
  operators=None,
132
132
  use_xor=False,
133
133
  rule_complexity_penalty=0.0,
@@ -135,7 +135,7 @@ class BinBoost:
135
135
  beam_width=5,
136
136
  subsample=0.8,
137
137
  min_samples_rule=5,
138
- lambda0=1.0,
138
+ lambda0=3.0,
139
139
  binarize_strategy='gradient',
140
140
  n_thresholds='auto',
141
141
  n_iter_no_change=None,
@@ -0,0 +1,78 @@
1
+ # multiclass.py
2
+ import numpy as np
3
+ from binboost import BinBoost
4
+
5
+
6
+ class BinBoostOvR:
7
+ def __init__(self, mode='multiclass', **binboost_params):
8
+ self.mode = mode
9
+ self.binboost_params = binboost_params
10
+
11
+ def fit(self, X, y):
12
+ X = np.asarray(X, dtype=np.float64)
13
+ y = np.asarray(y)
14
+ self.classes_ = np.unique(y)
15
+ self.n_classes_ = len(self.classes_)
16
+ if self.n_classes_ < 2:
17
+ raise ValueError("Minimal harus ada 2 kelas berbeda pada label y.")
18
+
19
+ self.estimators_ovr_ = []
20
+ for kelas in self.classes_:
21
+ y_bin = (y == kelas).astype(np.float64)
22
+ model_k = BinBoost(**self.binboost_params)
23
+ model_k.fit(X, y_bin)
24
+ self.estimators_ovr_.append(model_k)
25
+ return self
26
+
27
+ def _raw_scores(self, X):
28
+ return np.column_stack([m.predict_proba(X)[:, 1] for m in self.estimators_ovr_])
29
+
30
+ def predict_proba(self, X):
31
+ raw = self._raw_scores(X)
32
+ if self.mode == 'multilabel':
33
+ # Tidak dinormalisasi — tiap kelas independen
34
+ return raw
35
+ # mode='multiclass' — normalisasi supaya tiap baris berjumlah 1
36
+ total = raw.sum(axis=1, keepdims=True)
37
+ semua_nol = (total.flatten() <= 1e-10)
38
+ total_aman = np.where(total <= 1e-10, 1.0, total)
39
+ probs = raw / total_aman
40
+ if semua_nol.any():
41
+ probs[semua_nol] = 1.0 / self.n_classes_
42
+ return probs
43
+
44
+ def predict(self, X):
45
+ if self.mode == 'multilabel':
46
+ # Tiap kelas pakai threshold Youden J miliknya sendiri
47
+ preds = np.column_stack([m.predict(X) for m in self.estimators_ovr_])
48
+ return preds
49
+ probs = self.predict_proba(X)
50
+ idx = np.argmax(probs, axis=1)
51
+ return self.classes_[idx]
52
+
53
+ def score(self, X, y):
54
+ y = np.asarray(y)
55
+ preds = self.predict(X)
56
+ return np.mean(preds == y)
57
+
58
+ @property
59
+ def rule_summary_(self):
60
+ rows = []
61
+ for kelas, model_k in zip(self.classes_, self.estimators_ovr_):
62
+ for r in model_k.rule_summary_:
63
+ r = dict(r)
64
+ r['kelas'] = kelas
65
+ rows.append(r)
66
+ return rows
67
+
68
+ @property
69
+ def feature_usage_(self):
70
+ usage_total = {}
71
+ for model_k in self.estimators_ovr_:
72
+ for k, v in model_k.feature_usage_.items():
73
+ usage_total[k] = usage_total.get(k, 0) + v
74
+ return usage_total
75
+
76
+ @property
77
+ def n_rules_(self):
78
+ return sum(m.n_rules_ for m in self.estimators_ovr_)
@@ -2,22 +2,26 @@
2
2
  import numpy as np
3
3
 
4
4
  class Rule:
5
- # Satu aturan logika dalam ensemble BinBoost
6
- def __init__(self, features, operators, weight, feature_names=None):
7
- # 1. Simpan komponen aturan
8
- # a. Indeks fitur yang terlibat dalam aturan
5
+ # Satu aturan logika dalam ensemble BinBoost, dengan dukungan negasi (NOT) per fitur
6
+ def __init__(self, features, operators, weight, negations=None, feature_names=None):
9
7
  self.features = features
10
- # b. Daftar operator logika antar fitur
11
8
  self.operators = operators
12
- # c. Bobot optimal hasil perhitungan w_m*
13
9
  self.weight = weight
10
+ # Daftar boolean sepanjang features: True berarti fitur itu dipakai dalam bentuk negasi (NOT)
11
+ self.negations = negations if negations is not None else [False] * len(features)
14
12
  self.feature_names = feature_names
15
13
 
14
+ def _kolom(self, X_bin, idx_dalam_features):
15
+ fitur_idx = self.features[idx_dalam_features]
16
+ kolom = X_bin[:, fitur_idx].astype(bool)
17
+ if self.negations[idx_dalam_features]:
18
+ kolom = ~kolom
19
+ return kolom
20
+
16
21
  def evaluate(self, X_bin):
17
- # Hitung keluaran aturan untuk setiap sampel dan kembalikan larik biner
18
- result = X_bin[:, self.features[0]].astype(bool)
22
+ result = self._kolom(X_bin, 0)
19
23
  for i, op in enumerate(self.operators):
20
- next_col = X_bin[:, self.features[i + 1]].astype(bool)
24
+ next_col = self._kolom(X_bin, i + 1)
21
25
  if op == 'AND':
22
26
  result = result & next_col
23
27
  elif op == 'OR':
@@ -27,12 +31,12 @@ class Rule:
27
31
  return result.astype(np.float64)
28
32
 
29
33
  def to_string(self, feature_names=None):
30
- # Kembalikan representasi teks aturan yang dapat dibaca manusia
31
34
  names = feature_names if feature_names is not None else self.feature_names
32
35
  if names is None:
33
- parts = [f"X{f}" for f in self.features]
36
+ base_parts = [f"X{f}" for f in self.features]
34
37
  else:
35
- parts = [str(names[f]) for f in self.features]
38
+ base_parts = [str(names[f]) for f in self.features]
39
+ parts = [f"NOT {p}" if neg else p for p, neg in zip(base_parts, self.negations)]
36
40
  if len(parts) == 1:
37
41
  return parts[0]
38
42
  expr = parts[0]
@@ -45,11 +49,9 @@ class Rule:
45
49
 
46
50
 
47
51
  class BeamSearchRuleFinder:
48
- # Pencari aturan terbaik menggunakan beam search tervektorisasi berbasis numpy
49
-
50
52
  def __init__(self, operators, max_rule_length, beam_width,
51
53
  feature_selection_threshold, min_samples_rule,
52
- rule_complexity_penalty, ohe_groups=None):
54
+ rule_complexity_penalty, ohe_groups=None, allow_negation=True):
53
55
  self.operators = operators
54
56
  self.max_rule_length = max_rule_length
55
57
  self.beam_width = beam_width
@@ -57,8 +59,8 @@ class BeamSearchRuleFinder:
57
59
  self.min_samples_rule = min_samples_rule
58
60
  self.rule_complexity_penalty = rule_complexity_penalty
59
61
  self.ohe_groups = ohe_groups
62
+ self.allow_negation = allow_negation # False -> perilaku identik versi sebelum ini
60
63
 
61
- # Bangun peta indeks fitur ke grup OHE untuk pencarian constraint yang cepat
62
64
  self._feature_to_group = {}
63
65
  if ohe_groups is not None:
64
66
  for gid, members in ohe_groups.items():
@@ -66,8 +68,6 @@ class BeamSearchRuleFinder:
66
68
  self._feature_to_group[f] = gid
67
69
 
68
70
  def _select_features(self, X_bin, gradients):
69
- # 1. Pilih fitur kandidat berdasarkan skor korelasi gradien secara tervektorisasi
70
- # a. Hitung dot product gradien dengan semua kolom sekaligus dalam satu operasi
71
71
  dot_products = np.abs(X_bin.T @ gradients)
72
72
  col_sums = X_bin.sum(axis=0) + 1e-10
73
73
  scores = dot_products / col_sums
@@ -77,29 +77,23 @@ class BeamSearchRuleFinder:
77
77
  return selected
78
78
 
79
79
  def _compute_scores_batch(self, H_batch, gradients):
80
- # 1. Hitung bobot optimal dan skor untuk banyak kandidat aturan sekaligus
81
- # a. H_batch berukuran (n_kandidat, n_sampel)
82
80
  dot_gh = H_batch @ gradients
83
81
  dot_hh = (H_batch * H_batch).sum(axis=1)
84
82
  valid = dot_hh > 1e-10
85
83
  w = np.where(valid, dot_gh / np.where(dot_hh > 1e-10, dot_hh, 1.0), 0.0)
86
- # b. Kurangi gradien dengan prediksi berbobot untuk semua kandidat sekaligus
87
84
  residual = gradients[np.newaxis, :] - w[:, np.newaxis] * H_batch
88
85
  scores = (residual * residual).sum(axis=1)
89
86
  scores = np.where(valid, scores, np.inf)
90
87
  return w, scores, valid
91
88
 
92
89
  def _min_samples_int(self, n_samples):
93
- # Kembalikan jumlah sampel minimum dalam bentuk bilangan bulat
94
90
  if isinstance(self.min_samples_rule, float) and self.min_samples_rule < 1.0:
95
91
  return max(1, int(self.min_samples_rule * n_samples))
96
92
  return int(self.min_samples_rule)
97
93
 
98
94
  def find_best_rule(self, X_bin, gradients):
99
- # 1. Jalankan beam search tervektorisasi untuk menemukan aturan terbaik
100
95
  n_samples, n_features = X_bin.shape
101
96
  min_s = self._min_samples_int(n_samples)
102
- # Gunakan candidate features yang dikirim dari luar jika tersedia
103
97
  if hasattr(self, 'forced_candidate_features') and self.forced_candidate_features is not None:
104
98
  candidate_features = self.forced_candidate_features
105
99
  else:
@@ -109,42 +103,48 @@ class BeamSearchRuleFinder:
109
103
  return None
110
104
 
111
105
  X_bin_bool = X_bin.astype(bool)
112
-
113
- # 2. Inisialisasi beam dengan evaluasi semua aturan panjang satu secara batch
114
- H_init = X_bin_bool[:, candidate_features].T.astype(np.float64)
106
+ polaritas_list = [False, True] if self.allow_negation else [False]
107
+
108
+ # 1. Inisialisasi beam panjang satu — coba dua polaritas (asli & negasi) per fitur
109
+ H_list, meta_list = [], []
110
+ for cf in candidate_features:
111
+ kolom_pos = X_bin_bool[:, int(cf)]
112
+ for neg in polaritas_list:
113
+ kolom = ~kolom_pos if neg else kolom_pos
114
+ H_list.append(kolom.astype(np.float64))
115
+ meta_list.append((int(cf), neg))
116
+
117
+ H_init = np.array(H_list)
115
118
  support_init = H_init.sum(axis=1)
116
119
  valid_init = support_init >= min_s
117
-
118
120
  if not valid_init.any():
119
121
  return None
120
122
 
121
123
  w_init, scores_init, w_valid = self._compute_scores_batch(H_init, gradients)
122
124
  valid_mask = valid_init & w_valid
123
-
124
125
  if not valid_mask.any():
125
126
  return None
126
127
 
127
128
  candidates = []
128
- for i, cf in enumerate(candidate_features):
129
+ for i, (cf, neg) in enumerate(meta_list):
129
130
  if valid_mask[i]:
130
131
  pen = scores_init[i] * (1 + self.rule_complexity_penalty * 1)
131
- candidates.append((pen, [int(cf)], [], X_bin_bool[:, int(cf)]))
132
+ candidates.append((pen, [cf], [], [neg], H_init[i].astype(bool)))
132
133
 
133
134
  if not candidates:
134
135
  return None
135
136
 
136
137
  candidates.sort(key=lambda x: x[0])
137
138
  beam = candidates[:self.beam_width]
138
- best_score, best_feats, best_ops, best_h = beam[0]
139
+ best_score, best_feats, best_ops, best_negs, best_h = beam[0]
139
140
 
140
- # 3. Perluas aturan hingga panjang maksimum menggunakan operasi numpy batch
141
+ # 2. Perluas aturan hingga panjang maksimum
141
142
  for length in range(2, self.max_rule_length + 1):
142
143
  new_candidates = []
143
144
 
144
- for beam_score, beam_feats, beam_ops, beam_h in beam:
145
+ for beam_score, beam_feats, beam_ops, beam_negs, beam_h in beam:
145
146
  beam_feat_set = set(beam_feats)
146
147
 
147
- # a. Tentukan fitur yang dapat diperluas dengan mempertimbangkan constraint OHE
148
148
  expandable = []
149
149
  for cf in candidate_features:
150
150
  cf_int = int(cf)
@@ -159,42 +159,42 @@ class BeamSearchRuleFinder:
159
159
  if not expandable:
160
160
  continue
161
161
 
162
- # b. Ambil semua kolom yang bisa diperluas sekaligus
163
- cols = X_bin_bool[:, expandable].T
164
-
165
- for op in self.operators:
166
- # c. Terapkan operator ke semua fitur sekaligus dalam satu operasi numpy
167
- beam_h_2d = np.broadcast_to(beam_h, cols.shape)
168
- if op == 'AND':
169
- H_new = (beam_h_2d & cols).astype(np.float64)
170
- elif op == 'OR':
171
- H_new = (beam_h_2d | cols).astype(np.float64)
172
- elif op == 'XOR':
173
- H_new = (beam_h_2d ^ cols).astype(np.float64)
174
- else:
175
- continue
176
-
177
- support_new = H_new.sum(axis=1)
178
- valid_sup = support_new >= min_s
179
-
180
- if not valid_sup.any():
181
- continue
182
-
183
- w_new, scores_new, w_valid_new = self._compute_scores_batch(
184
- H_new, gradients
185
- )
186
- valid_combined = valid_sup & w_valid_new
162
+ cols_pos = X_bin_bool[:, expandable].T
163
+ kandidat_kolom = [(cols_pos, False)]
164
+ if self.allow_negation:
165
+ kandidat_kolom.append((~cols_pos, True))
166
+
167
+ for cols, neg_flag in kandidat_kolom:
168
+ for op in self.operators:
169
+ beam_h_2d = np.broadcast_to(beam_h, cols.shape)
170
+ if op == 'AND':
171
+ H_new = (beam_h_2d & cols).astype(np.float64)
172
+ elif op == 'OR':
173
+ H_new = (beam_h_2d | cols).astype(np.float64)
174
+ elif op == 'XOR':
175
+ H_new = (beam_h_2d ^ cols).astype(np.float64)
176
+ else:
177
+ continue
187
178
 
188
- for j, cf_int in enumerate(expandable):
189
- if not valid_combined[j]:
179
+ support_new = H_new.sum(axis=1)
180
+ valid_sup = support_new >= min_s
181
+ if not valid_sup.any():
190
182
  continue
191
- pen = scores_new[j] * (1 + self.rule_complexity_penalty * length)
192
- new_candidates.append((
193
- pen,
194
- beam_feats + [cf_int],
195
- beam_ops + [op],
196
- H_new[j].astype(bool)
197
- ))
183
+
184
+ w_new, scores_new, w_valid_new = self._compute_scores_batch(H_new, gradients)
185
+ valid_combined = valid_sup & w_valid_new
186
+
187
+ for j, cf_int in enumerate(expandable):
188
+ if not valid_combined[j]:
189
+ continue
190
+ pen = scores_new[j] * (1 + self.rule_complexity_penalty * length)
191
+ new_candidates.append((
192
+ pen,
193
+ beam_feats + [cf_int],
194
+ beam_ops + [op],
195
+ beam_negs + [neg_flag],
196
+ H_new[j].astype(bool)
197
+ ))
198
198
 
199
199
  if not new_candidates:
200
200
  break
@@ -203,16 +203,15 @@ class BeamSearchRuleFinder:
203
203
  new_candidates = new_candidates[:self.beam_width]
204
204
 
205
205
  if new_candidates[0][0] < best_score:
206
- best_score, best_feats, best_ops, best_h = new_candidates[0]
206
+ best_score, best_feats, best_ops, best_negs, best_h = new_candidates[0]
207
207
  beam = new_candidates
208
208
  else:
209
209
  break
210
210
 
211
- # 4. Hitung ulang bobot optimal untuk aturan terbaik yang dipilih
212
211
  h_final = best_h.astype(np.float64)
213
212
  denom = np.dot(h_final, h_final)
214
213
  if denom < 1e-10:
215
214
  return None
216
215
  w_final = np.dot(gradients, h_final) / denom
217
216
 
218
- return Rule(best_feats, best_ops, w_final)
217
+ return Rule(best_feats, best_ops, w_final, negations=best_negs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: binboost
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Gradient Boosting Berbasis Aturan Logika Adaptif untuk Fitur Biner
5
5
  Author: Rangga Wahyu Pratama
6
6
  License: MIT
@@ -6,6 +6,7 @@ binboost/__init__.py
6
6
  binboost/binarizer.py
7
7
  binboost/binboost.py
8
8
  binboost/loss.py
9
+ binboost/multiclass.py
9
10
  binboost/rule.py
10
11
  binboost.egg-info/PKG-INFO
11
12
  binboost.egg-info/SOURCES.txt
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "binboost"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "Gradient Boosting Berbasis Aturan Logika Adaptif untuk Fitur Biner"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="binboost",
5
- version="0.2.1",
5
+ version="0.2.2",
6
6
  author="Rangga Wahyu Pratama",
7
7
  description="Gradient Boosting Berbasis Aturan Logika Adaptif untuk Fitur Biner",
8
8
  long_description=open("README.md", encoding="utf-8").read(),
File without changes
File without changes
File without changes
File without changes
File without changes