kanboost 0.0.1__py3-none-any.whl
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.
- kanboost/__init__.py +23 -0
- kanboost/_base.py +198 -0
- kanboost/classifier.py +169 -0
- kanboost/encoders.py +81 -0
- kanboost/metrics.py +59 -0
- kanboost/regressor.py +113 -0
- kanboost-0.0.1.dist-info/METADATA +171 -0
- kanboost-0.0.1.dist-info/RECORD +11 -0
- kanboost-0.0.1.dist-info/WHEEL +5 -0
- kanboost-0.0.1.dist-info/licenses/LICENSE +21 -0
- kanboost-0.0.1.dist-info/top_level.txt +1 -0
kanboost/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
KANBoost: Gradient Boosting with Kolmogorov-Arnold Network learners.
|
|
3
|
+
|
|
4
|
+
An interpretable alternative to tree-based gradient boosting (XGBoost,
|
|
5
|
+
LightGBM, CatBoost), using shallow KAN networks as weak learners instead
|
|
6
|
+
of decision trees.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .classifier import KANBoostClassifier
|
|
10
|
+
from .regressor import KANBoostRegressor
|
|
11
|
+
from .metrics import classification_report_dict, print_classification_report
|
|
12
|
+
|
|
13
|
+
__version__ = "0.0.1"
|
|
14
|
+
__all__ = [
|
|
15
|
+
"KANBoostClassifier",
|
|
16
|
+
"KANBoostRegressor",
|
|
17
|
+
"classification_report_dict",
|
|
18
|
+
"print_classification_report",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
# Hyperparameter tuning lives in the separate `kantun` package, so that
|
|
22
|
+
# kanboost's core dependency footprint stays minimal and kantun can be
|
|
23
|
+
# used to tune other model types too. Install with: pip install kantun
|
kanboost/_base.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
kanboost._base -- shared machinery for KANBoostClassifier / KANBoostRegressor.
|
|
3
|
+
|
|
4
|
+
Contains:
|
|
5
|
+
- input validation
|
|
6
|
+
- the shared boosting loop skeleton
|
|
7
|
+
- sklearn-compatible get_params / set_params (so KANBoost estimators work
|
|
8
|
+
inside sklearn Pipelines, GridSearchCV, and kantun without adapters)
|
|
9
|
+
- feature importances (shared by both estimators)
|
|
10
|
+
- suppression of pykan's noisy checkpoint logging
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import contextlib
|
|
16
|
+
import io
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pandas as pd
|
|
20
|
+
import torch
|
|
21
|
+
import torch.nn as nn
|
|
22
|
+
from kan import KAN
|
|
23
|
+
from sklearn.base import BaseEstimator
|
|
24
|
+
|
|
25
|
+
from .encoders import TabularPreprocessor
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@contextlib.contextmanager
|
|
29
|
+
def _suppress_pykan_noise():
|
|
30
|
+
"""pykan prints checkpoint messages to stdout and tqdm progress bars
|
|
31
|
+
to stderr on every instantiation/fit; silence both."""
|
|
32
|
+
out, err = io.StringIO(), io.StringIO()
|
|
33
|
+
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
|
|
34
|
+
yield
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _validate_Xy(X, y):
|
|
38
|
+
"""Validate and normalize training inputs. Returns (X_df, y_arr)."""
|
|
39
|
+
if not isinstance(X, pd.DataFrame):
|
|
40
|
+
X = pd.DataFrame(np.asarray(X))
|
|
41
|
+
X.columns = [f"f{i}" for i in range(X.shape[1])]
|
|
42
|
+
|
|
43
|
+
y = np.asarray(y, dtype=float).ravel()
|
|
44
|
+
|
|
45
|
+
if len(X) != len(y):
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"X and y have inconsistent lengths: {len(X)} vs {len(y)}"
|
|
48
|
+
)
|
|
49
|
+
if len(X) == 0:
|
|
50
|
+
raise ValueError("X is empty.")
|
|
51
|
+
if np.isnan(y).any():
|
|
52
|
+
raise ValueError(
|
|
53
|
+
"y contains NaN values. Remove or impute them before fitting."
|
|
54
|
+
)
|
|
55
|
+
return X, y
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _BaseKANBoost(BaseEstimator):
|
|
59
|
+
"""Shared base for the boosting estimators. Not part of the public API.
|
|
60
|
+
|
|
61
|
+
Inherits sklearn's BaseEstimator, which provides get_params/set_params,
|
|
62
|
+
__sklearn_tags__, and full compatibility with sklearn Pipelines,
|
|
63
|
+
GridSearchCV, cross_val_score, and clone().
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
n_estimators: int = 100,
|
|
69
|
+
learning_rate: float = 0.1,
|
|
70
|
+
kan_hidden: int = 3,
|
|
71
|
+
kan_grid: int = 2,
|
|
72
|
+
kan_k: int = 3,
|
|
73
|
+
kan_steps: int = 20,
|
|
74
|
+
kan_lr: float = 0.02,
|
|
75
|
+
early_stopping_rounds: int | None = 10,
|
|
76
|
+
categorical_cols=None,
|
|
77
|
+
random_state: int = 42,
|
|
78
|
+
verbose: bool = False,
|
|
79
|
+
):
|
|
80
|
+
self.n_estimators = n_estimators
|
|
81
|
+
self.learning_rate = learning_rate
|
|
82
|
+
self.kan_hidden = kan_hidden
|
|
83
|
+
self.kan_grid = kan_grid
|
|
84
|
+
self.kan_k = kan_k
|
|
85
|
+
self.kan_steps = kan_steps
|
|
86
|
+
self.kan_lr = kan_lr
|
|
87
|
+
self.early_stopping_rounds = early_stopping_rounds
|
|
88
|
+
self.categorical_cols = categorical_cols
|
|
89
|
+
self.random_state = random_state
|
|
90
|
+
self.verbose = verbose
|
|
91
|
+
|
|
92
|
+
# fitted state
|
|
93
|
+
self.preprocessor_ = None
|
|
94
|
+
self.learners_ = []
|
|
95
|
+
self.init_pred_ = None
|
|
96
|
+
self.best_iteration_ = None
|
|
97
|
+
self.feature_names_in_ = None
|
|
98
|
+
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
# NOTE: get_params/set_params/__sklearn_tags__ come from BaseEstimator.
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
# shared internals
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
def _validate_hyperparams(self):
|
|
107
|
+
if self.n_estimators < 1:
|
|
108
|
+
raise ValueError("n_estimators must be >= 1")
|
|
109
|
+
if not (0 < self.learning_rate <= 1):
|
|
110
|
+
raise ValueError("learning_rate must be in (0, 1]")
|
|
111
|
+
if self.kan_hidden < 1 or self.kan_grid < 1 or self.kan_steps < 1:
|
|
112
|
+
raise ValueError("kan_hidden, kan_grid, kan_steps must be >= 1")
|
|
113
|
+
|
|
114
|
+
def _prepare_fit(self, X, y):
|
|
115
|
+
"""Common preamble: seeds, validation, preprocessing."""
|
|
116
|
+
self._validate_hyperparams()
|
|
117
|
+
torch.manual_seed(self.random_state)
|
|
118
|
+
np.random.seed(self.random_state)
|
|
119
|
+
|
|
120
|
+
X, y = _validate_Xy(X, y)
|
|
121
|
+
self.feature_names_in_ = list(X.columns)
|
|
122
|
+
|
|
123
|
+
self.preprocessor_ = TabularPreprocessor(
|
|
124
|
+
categorical_cols=self.categorical_cols or []
|
|
125
|
+
)
|
|
126
|
+
X_arr = self.preprocessor_.fit_transform(X, y)
|
|
127
|
+
|
|
128
|
+
if np.isnan(X_arr).any():
|
|
129
|
+
raise ValueError(
|
|
130
|
+
"Preprocessed X contains NaN values. KANBoost does not yet "
|
|
131
|
+
"handle missing values natively -- impute before fitting."
|
|
132
|
+
)
|
|
133
|
+
return X, y, X_arr
|
|
134
|
+
|
|
135
|
+
def _new_learner(self, n_features: int, seed_offset: int) -> KAN:
|
|
136
|
+
with _suppress_pykan_noise():
|
|
137
|
+
return KAN(
|
|
138
|
+
width=[n_features, self.kan_hidden, 1],
|
|
139
|
+
grid=self.kan_grid,
|
|
140
|
+
k=self.kan_k,
|
|
141
|
+
seed=self.random_state + seed_offset,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _fit_learner(self, learner: KAN, X_t: torch.Tensor, residual: np.ndarray):
|
|
145
|
+
r_t = torch.tensor(residual, dtype=torch.float32).unsqueeze(1)
|
|
146
|
+
dataset = {
|
|
147
|
+
"train_input": X_t, "train_label": r_t,
|
|
148
|
+
"test_input": X_t, "test_label": r_t,
|
|
149
|
+
}
|
|
150
|
+
with _suppress_pykan_noise():
|
|
151
|
+
learner.fit(
|
|
152
|
+
dataset, opt="Adam", steps=self.kan_steps, lr=self.kan_lr,
|
|
153
|
+
loss_fn=nn.MSELoss(),
|
|
154
|
+
)
|
|
155
|
+
with torch.no_grad():
|
|
156
|
+
return learner(X_t).numpy().flatten()
|
|
157
|
+
|
|
158
|
+
def _check_fitted(self):
|
|
159
|
+
if not self.learners_:
|
|
160
|
+
raise RuntimeError(
|
|
161
|
+
f"This {type(self).__name__} instance is not fitted yet. "
|
|
162
|
+
"Call fit() before predicting."
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def _transform_X(self, X) -> torch.Tensor:
|
|
166
|
+
self._check_fitted()
|
|
167
|
+
if not isinstance(X, pd.DataFrame):
|
|
168
|
+
X = pd.DataFrame(np.asarray(X), columns=self.feature_names_in_)
|
|
169
|
+
X_arr = self.preprocessor_.transform(X)
|
|
170
|
+
return torch.tensor(X_arr, dtype=torch.float32)
|
|
171
|
+
|
|
172
|
+
# ------------------------------------------------------------------
|
|
173
|
+
def feature_importances(self) -> np.ndarray:
|
|
174
|
+
"""
|
|
175
|
+
Approximate per-feature importance: L2 norm of each learner's
|
|
176
|
+
first-layer spline coefficients per input dimension, summed over
|
|
177
|
+
the ensemble and normalized to 1. A rough analogue of GBDT
|
|
178
|
+
'gain' importance.
|
|
179
|
+
"""
|
|
180
|
+
self._check_fitted()
|
|
181
|
+
n_features = self.learners_[0].width[0][0]
|
|
182
|
+
importances = np.zeros(n_features)
|
|
183
|
+
for learner in self.learners_[: self.best_iteration_]:
|
|
184
|
+
coef = learner.act_fun[0].coef.detach().numpy()
|
|
185
|
+
importances += np.linalg.norm(coef, axis=(1, 2))
|
|
186
|
+
total = importances.sum()
|
|
187
|
+
return importances / total if total > 0 else importances
|
|
188
|
+
|
|
189
|
+
def feature_importances_dict(self) -> dict:
|
|
190
|
+
"""Feature importances keyed by input column name, sorted desc."""
|
|
191
|
+
imps = self.feature_importances()
|
|
192
|
+
# preprocessor may reorder: numeric cols first, then categorical
|
|
193
|
+
ordered_names = (
|
|
194
|
+
list(self.preprocessor_.numeric_cols_)
|
|
195
|
+
+ list(self.preprocessor_.categorical_cols)
|
|
196
|
+
)
|
|
197
|
+
pairs = sorted(zip(ordered_names, imps), key=lambda kv: -kv[1])
|
|
198
|
+
return {name: float(v) for name, v in pairs}
|
kanboost/classifier.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
KANBoostClassifier: binary classification via gradient boosting with
|
|
3
|
+
shallow KAN networks as weak learners.
|
|
4
|
+
|
|
5
|
+
Follows the classic Friedman (2001) gradient boosting recipe:
|
|
6
|
+
|
|
7
|
+
F_0(x) = log-odds of the base rate
|
|
8
|
+
for t = 1..T:
|
|
9
|
+
r_t = pseudo-residuals = y - sigmoid(F_{t-1}(x)) (logloss)
|
|
10
|
+
f_t = a small KAN fit to (X, r_t)
|
|
11
|
+
F_t(x) = F_{t-1}(x) + learning_rate * f_t(x)
|
|
12
|
+
prediction = sigmoid(F_T(x))
|
|
13
|
+
|
|
14
|
+
Each weak learner is a small, shallow KAN so it plays the same structural
|
|
15
|
+
role a shallow decision tree plays in XGBoost/CatBoost: a cheap,
|
|
16
|
+
high-bias/low-variance component that is only useful in aggregate.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
import torch
|
|
23
|
+
|
|
24
|
+
from sklearn.base import ClassifierMixin
|
|
25
|
+
|
|
26
|
+
from ._base import _BaseKANBoost, _validate_Xy
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _sigmoid(z: np.ndarray) -> np.ndarray:
|
|
30
|
+
return 1.0 / (1.0 + np.exp(-np.clip(z, -30, 30)))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class KANBoostClassifier(ClassifierMixin, _BaseKANBoost):
|
|
34
|
+
"""Gradient-boosted KAN ensemble for binary classification.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
n_estimators : int, default=100
|
|
39
|
+
Maximum number of boosting iterations (weak learners).
|
|
40
|
+
learning_rate : float, default=0.1
|
|
41
|
+
Shrinkage applied to each learner's contribution.
|
|
42
|
+
kan_hidden : int, default=3
|
|
43
|
+
Hidden-layer width of each weak KAN learner.
|
|
44
|
+
kan_grid : int, default=2
|
|
45
|
+
Number of B-spline grid intervals per edge function.
|
|
46
|
+
kan_k : int, default=3
|
|
47
|
+
B-spline polynomial degree.
|
|
48
|
+
kan_steps : int, default=20
|
|
49
|
+
Optimizer steps used to fit each weak learner.
|
|
50
|
+
kan_lr : float, default=0.02
|
|
51
|
+
Learning rate of each weak learner's inner optimizer.
|
|
52
|
+
early_stopping_rounds : int or None, default=10
|
|
53
|
+
Stop if validation logloss hasn't improved for this many rounds.
|
|
54
|
+
Requires eval_set in fit(). None disables early stopping.
|
|
55
|
+
categorical_cols : list of str, optional
|
|
56
|
+
Column names to target-mean encode automatically.
|
|
57
|
+
random_state : int, default=42
|
|
58
|
+
verbose : bool, default=False
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def fit(self, X, y, eval_set: tuple | None = None):
|
|
63
|
+
"""Fit the boosted ensemble.
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
X : DataFrame or array of shape (n_samples, n_features)
|
|
68
|
+
y : array of shape (n_samples,) with values in {0, 1}
|
|
69
|
+
eval_set : (X_val, y_val) tuple, optional
|
|
70
|
+
Validation data for early stopping.
|
|
71
|
+
"""
|
|
72
|
+
X, y, X_arr = self._prepare_fit(X, y)
|
|
73
|
+
|
|
74
|
+
classes = np.unique(y)
|
|
75
|
+
if not np.array_equal(classes, [0, 1]) and not np.array_equal(classes, [0]) \
|
|
76
|
+
and not np.array_equal(classes, [1]):
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"KANBoostClassifier supports binary targets in {{0, 1}}; "
|
|
79
|
+
f"got classes {classes}. Multiclass is on the roadmap."
|
|
80
|
+
)
|
|
81
|
+
self.classes_ = np.array([0, 1])
|
|
82
|
+
|
|
83
|
+
X_t = torch.tensor(X_arr, dtype=torch.float32)
|
|
84
|
+
n_features = X_arr.shape[1]
|
|
85
|
+
|
|
86
|
+
p = float(np.clip(y.mean(), 1e-6, 1 - 1e-6))
|
|
87
|
+
self.init_pred_ = float(np.log(p / (1 - p)))
|
|
88
|
+
F = np.full(len(y), self.init_pred_)
|
|
89
|
+
|
|
90
|
+
X_val_t = y_val = F_val = None
|
|
91
|
+
if eval_set is not None:
|
|
92
|
+
X_val_df, y_val = eval_set
|
|
93
|
+
X_val_df, y_val = _validate_Xy(X_val_df, y_val)
|
|
94
|
+
X_val_arr = self.preprocessor_.transform(X_val_df)
|
|
95
|
+
X_val_t = torch.tensor(X_val_arr, dtype=torch.float32)
|
|
96
|
+
F_val = np.full(len(y_val), self.init_pred_)
|
|
97
|
+
|
|
98
|
+
best_val_loss = np.inf
|
|
99
|
+
rounds_since_best = 0
|
|
100
|
+
self.learners_ = []
|
|
101
|
+
self.best_iteration_ = None
|
|
102
|
+
|
|
103
|
+
for t in range(self.n_estimators):
|
|
104
|
+
residual = y - _sigmoid(F)
|
|
105
|
+
|
|
106
|
+
learner = self._new_learner(n_features, seed_offset=t)
|
|
107
|
+
update = self._fit_learner(learner, X_t, residual)
|
|
108
|
+
F += self.learning_rate * update
|
|
109
|
+
self.learners_.append(learner)
|
|
110
|
+
|
|
111
|
+
if X_val_t is not None:
|
|
112
|
+
with torch.no_grad():
|
|
113
|
+
F_val += self.learning_rate * learner(X_val_t).numpy().flatten()
|
|
114
|
+
val_prob = np.clip(_sigmoid(F_val), 1e-7, 1 - 1e-7)
|
|
115
|
+
val_loss = -float(np.mean(
|
|
116
|
+
y_val * np.log(val_prob) + (1 - y_val) * np.log(1 - val_prob)
|
|
117
|
+
))
|
|
118
|
+
|
|
119
|
+
if self.verbose:
|
|
120
|
+
print(f"[{t + 1}/{self.n_estimators}] val_logloss={val_loss:.5f}")
|
|
121
|
+
|
|
122
|
+
if val_loss < best_val_loss - 1e-5:
|
|
123
|
+
best_val_loss = val_loss
|
|
124
|
+
rounds_since_best = 0
|
|
125
|
+
self.best_iteration_ = t + 1
|
|
126
|
+
else:
|
|
127
|
+
rounds_since_best += 1
|
|
128
|
+
if (self.early_stopping_rounds is not None
|
|
129
|
+
and rounds_since_best >= self.early_stopping_rounds):
|
|
130
|
+
if self.verbose:
|
|
131
|
+
print(f"Early stopping at iteration {t + 1}")
|
|
132
|
+
break
|
|
133
|
+
elif self.verbose and (t + 1) % max(1, self.n_estimators // 10) == 0:
|
|
134
|
+
print(f"[{t + 1}/{self.n_estimators}] "
|
|
135
|
+
f"train residual std={residual.std():.4f}")
|
|
136
|
+
|
|
137
|
+
if self.best_iteration_ is None:
|
|
138
|
+
self.best_iteration_ = len(self.learners_)
|
|
139
|
+
return self
|
|
140
|
+
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
def _raw_score(self, X) -> np.ndarray:
|
|
143
|
+
X_t = self._transform_X(X)
|
|
144
|
+
F = np.full(X_t.shape[0], self.init_pred_)
|
|
145
|
+
for learner in self.learners_[: self.best_iteration_]:
|
|
146
|
+
with torch.no_grad():
|
|
147
|
+
F += self.learning_rate * learner(X_t).numpy().flatten()
|
|
148
|
+
return F
|
|
149
|
+
|
|
150
|
+
def predict_proba(self, X) -> np.ndarray:
|
|
151
|
+
"""Return array of shape (n_samples, 2): P(class 0), P(class 1)."""
|
|
152
|
+
prob_pos = _sigmoid(self._raw_score(X))
|
|
153
|
+
return np.vstack([1 - prob_pos, prob_pos]).T
|
|
154
|
+
|
|
155
|
+
def predict(self, X, threshold: float = 0.5) -> np.ndarray:
|
|
156
|
+
"""Return hard 0/1 predictions at the given probability threshold."""
|
|
157
|
+
return (self.predict_proba(X)[:, 1] >= threshold).astype(int)
|
|
158
|
+
|
|
159
|
+
def evaluate(self, X, y, threshold: float = 0.5, verbose: bool = True) -> dict:
|
|
160
|
+
"""Predict on X and report confusion matrix, accuracy, precision,
|
|
161
|
+
recall, F1, and ROC-AUC against y. Returns the metrics dict."""
|
|
162
|
+
from .metrics import classification_report_dict, print_classification_report
|
|
163
|
+
|
|
164
|
+
y_prob = self.predict_proba(X)[:, 1]
|
|
165
|
+
y_pred = (y_prob >= threshold).astype(int)
|
|
166
|
+
report = classification_report_dict(y, y_pred, y_prob)
|
|
167
|
+
if verbose:
|
|
168
|
+
print_classification_report(report)
|
|
169
|
+
return report
|
kanboost/encoders.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Preprocessing utilities for KANBoost.
|
|
3
|
+
|
|
4
|
+
KAN is sensitive to input scale (splines are defined on a bounded grid),
|
|
5
|
+
so numeric features must be scaled/clipped, and categorical features
|
|
6
|
+
must be encoded to numbers before reaching the model. This module
|
|
7
|
+
automates both steps so the end user can pass a raw-ish DataFrame,
|
|
8
|
+
similar to how CatBoost handles categorical columns internally.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
from sklearn.preprocessing import RobustScaler
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TabularPreprocessor:
|
|
17
|
+
"""
|
|
18
|
+
Automatic preprocessing for mixed numeric/categorical tabular data.
|
|
19
|
+
|
|
20
|
+
- Numeric columns: outlier clipping (1st/99th percentile) + RobustScaler
|
|
21
|
+
+ final clip to [-1, 1] (KAN's default spline grid range).
|
|
22
|
+
- Categorical columns: target-mean encoding (smoothed), computed on
|
|
23
|
+
the training fold only, to avoid leakage -- similar in spirit to
|
|
24
|
+
CatBoost's ordered target statistics (simplified, non-ordered version).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, categorical_cols=None, smoothing: float = 10.0):
|
|
28
|
+
self.categorical_cols = categorical_cols or []
|
|
29
|
+
self.smoothing = smoothing
|
|
30
|
+
self.numeric_cols_ = None
|
|
31
|
+
self.scaler_ = None
|
|
32
|
+
self.clip_low_ = None
|
|
33
|
+
self.clip_high_ = None
|
|
34
|
+
self.cat_maps_ = {}
|
|
35
|
+
self.global_mean_ = None
|
|
36
|
+
|
|
37
|
+
def fit(self, X: pd.DataFrame, y: np.ndarray):
|
|
38
|
+
self.numeric_cols_ = [c for c in X.columns if c not in self.categorical_cols]
|
|
39
|
+
self.global_mean_ = float(np.mean(y))
|
|
40
|
+
|
|
41
|
+
# --- numeric ---
|
|
42
|
+
if self.numeric_cols_:
|
|
43
|
+
X_num = X[self.numeric_cols_].to_numpy(dtype=float)
|
|
44
|
+
self.clip_low_ = np.nanpercentile(X_num, 1, axis=0)
|
|
45
|
+
self.clip_high_ = np.nanpercentile(X_num, 99, axis=0)
|
|
46
|
+
X_num = np.clip(X_num, self.clip_low_, self.clip_high_)
|
|
47
|
+
self.scaler_ = RobustScaler()
|
|
48
|
+
self.scaler_.fit(X_num)
|
|
49
|
+
|
|
50
|
+
# --- categorical: smoothed target-mean encoding ---
|
|
51
|
+
for col in self.categorical_cols:
|
|
52
|
+
stats = (
|
|
53
|
+
pd.DataFrame({col: X[col].astype(str), "y": y})
|
|
54
|
+
.groupby(col)["y"]
|
|
55
|
+
.agg(["mean", "count"])
|
|
56
|
+
)
|
|
57
|
+
smoothed = (
|
|
58
|
+
stats["mean"] * stats["count"] + self.global_mean_ * self.smoothing
|
|
59
|
+
) / (stats["count"] + self.smoothing)
|
|
60
|
+
self.cat_maps_[col] = smoothed.to_dict()
|
|
61
|
+
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
def transform(self, X: pd.DataFrame) -> np.ndarray:
|
|
65
|
+
parts = []
|
|
66
|
+
if self.numeric_cols_:
|
|
67
|
+
X_num = X[self.numeric_cols_].to_numpy(dtype=float)
|
|
68
|
+
X_num = np.clip(X_num, self.clip_low_, self.clip_high_)
|
|
69
|
+
X_num = self.scaler_.transform(X_num)
|
|
70
|
+
X_num = np.clip(X_num, -1, 1)
|
|
71
|
+
parts.append(X_num)
|
|
72
|
+
|
|
73
|
+
for col in self.categorical_cols:
|
|
74
|
+
mapping = self.cat_maps_[col]
|
|
75
|
+
encoded = X[col].astype(str).map(mapping).fillna(self.global_mean_)
|
|
76
|
+
parts.append(encoded.to_numpy(dtype=float).reshape(-1, 1))
|
|
77
|
+
|
|
78
|
+
return np.hstack(parts) if parts else np.empty((len(X), 0))
|
|
79
|
+
|
|
80
|
+
def fit_transform(self, X: pd.DataFrame, y: np.ndarray) -> np.ndarray:
|
|
81
|
+
return self.fit(X, y).transform(X)
|
kanboost/metrics.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Evaluation utilities for KANBoost models.
|
|
3
|
+
|
|
4
|
+
Kept as plain functions (not baked only into the classifier) so they can
|
|
5
|
+
also be reused by the tuning module and by users who just want metrics
|
|
6
|
+
on any set of predictions.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from sklearn.metrics import (
|
|
11
|
+
confusion_matrix,
|
|
12
|
+
f1_score,
|
|
13
|
+
precision_score,
|
|
14
|
+
recall_score,
|
|
15
|
+
accuracy_score,
|
|
16
|
+
roc_auc_score,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def classification_report_dict(y_true, y_pred, y_prob=None) -> dict:
|
|
21
|
+
"""
|
|
22
|
+
Returns a dict with confusion matrix + standard classification
|
|
23
|
+
metrics. If y_prob (probability of the positive class) is given,
|
|
24
|
+
also includes ROC-AUC.
|
|
25
|
+
"""
|
|
26
|
+
y_true = np.asarray(y_true)
|
|
27
|
+
y_pred = np.asarray(y_pred)
|
|
28
|
+
|
|
29
|
+
cm = confusion_matrix(y_true, y_pred)
|
|
30
|
+
report = {
|
|
31
|
+
"confusion_matrix": cm.tolist(),
|
|
32
|
+
"confusion_matrix_labels": ["true_negative_row0", "true_positive_row1"],
|
|
33
|
+
"accuracy": float(accuracy_score(y_true, y_pred)),
|
|
34
|
+
"precision": float(precision_score(y_true, y_pred, zero_division=0)),
|
|
35
|
+
"recall": float(recall_score(y_true, y_pred, zero_division=0)),
|
|
36
|
+
"f1": float(f1_score(y_true, y_pred, zero_division=0)),
|
|
37
|
+
}
|
|
38
|
+
if y_prob is not None:
|
|
39
|
+
try:
|
|
40
|
+
report["auc"] = float(roc_auc_score(y_true, y_prob))
|
|
41
|
+
except ValueError:
|
|
42
|
+
report["auc"] = None
|
|
43
|
+
return report
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def print_classification_report(report: dict, class_names=("0", "1")) -> None:
|
|
47
|
+
"""Pretty-print a report dict returned by classification_report_dict."""
|
|
48
|
+
cm = np.array(report["confusion_matrix"])
|
|
49
|
+
print("Confusion Matrix:")
|
|
50
|
+
print(f" predicted {class_names[0]} predicted {class_names[1]}")
|
|
51
|
+
print(f" actual {class_names[0]:>3} {cm[0, 0]:>10d} {cm[0, 1]:>10d}")
|
|
52
|
+
print(f" actual {class_names[1]:>3} {cm[1, 0]:>10d} {cm[1, 1]:>10d}")
|
|
53
|
+
print()
|
|
54
|
+
print(f"Accuracy : {report['accuracy']:.4f}")
|
|
55
|
+
print(f"Precision: {report['precision']:.4f}")
|
|
56
|
+
print(f"Recall : {report['recall']:.4f}")
|
|
57
|
+
print(f"F1 score : {report['f1']:.4f}")
|
|
58
|
+
if report.get("auc") is not None:
|
|
59
|
+
print(f"ROC-AUC : {report['auc']:.4f}")
|
kanboost/regressor.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
KANBoostRegressor: regression via gradient boosting with shallow KAN
|
|
3
|
+
learners, minimizing squared error (residual = y - F(x) directly).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import torch
|
|
10
|
+
|
|
11
|
+
from sklearn.base import RegressorMixin
|
|
12
|
+
|
|
13
|
+
from ._base import _BaseKANBoost, _validate_Xy
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class KANBoostRegressor(RegressorMixin, _BaseKANBoost):
|
|
17
|
+
"""Gradient-boosted KAN ensemble for regression (squared-error loss).
|
|
18
|
+
|
|
19
|
+
Parameters are identical to KANBoostClassifier; see its docstring.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def fit(self, X, y, eval_set: tuple | None = None):
|
|
24
|
+
"""Fit the boosted ensemble.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
X : DataFrame or array of shape (n_samples, n_features)
|
|
29
|
+
y : array of shape (n_samples,), continuous target
|
|
30
|
+
eval_set : (X_val, y_val) tuple, optional
|
|
31
|
+
Validation data for early stopping on MSE.
|
|
32
|
+
"""
|
|
33
|
+
X, y, X_arr = self._prepare_fit(X, y)
|
|
34
|
+
|
|
35
|
+
X_t = torch.tensor(X_arr, dtype=torch.float32)
|
|
36
|
+
n_features = X_arr.shape[1]
|
|
37
|
+
|
|
38
|
+
self.init_pred_ = float(y.mean())
|
|
39
|
+
F = np.full(len(y), self.init_pred_)
|
|
40
|
+
|
|
41
|
+
X_val_t = y_val = F_val = None
|
|
42
|
+
if eval_set is not None:
|
|
43
|
+
X_val_df, y_val = eval_set
|
|
44
|
+
X_val_df, y_val = _validate_Xy(X_val_df, y_val)
|
|
45
|
+
X_val_arr = self.preprocessor_.transform(X_val_df)
|
|
46
|
+
X_val_t = torch.tensor(X_val_arr, dtype=torch.float32)
|
|
47
|
+
F_val = np.full(len(y_val), self.init_pred_)
|
|
48
|
+
|
|
49
|
+
best_val_loss = np.inf
|
|
50
|
+
rounds_since_best = 0
|
|
51
|
+
self.learners_ = []
|
|
52
|
+
self.best_iteration_ = None
|
|
53
|
+
|
|
54
|
+
for t in range(self.n_estimators):
|
|
55
|
+
residual = y - F
|
|
56
|
+
|
|
57
|
+
learner = self._new_learner(n_features, seed_offset=t)
|
|
58
|
+
update = self._fit_learner(learner, X_t, residual)
|
|
59
|
+
F += self.learning_rate * update
|
|
60
|
+
self.learners_.append(learner)
|
|
61
|
+
|
|
62
|
+
if X_val_t is not None:
|
|
63
|
+
with torch.no_grad():
|
|
64
|
+
F_val += self.learning_rate * learner(X_val_t).numpy().flatten()
|
|
65
|
+
val_loss = float(np.mean((y_val - F_val) ** 2))
|
|
66
|
+
|
|
67
|
+
if self.verbose:
|
|
68
|
+
print(f"[{t + 1}/{self.n_estimators}] val_mse={val_loss:.5f}")
|
|
69
|
+
|
|
70
|
+
if val_loss < best_val_loss - 1e-6:
|
|
71
|
+
best_val_loss = val_loss
|
|
72
|
+
rounds_since_best = 0
|
|
73
|
+
self.best_iteration_ = t + 1
|
|
74
|
+
else:
|
|
75
|
+
rounds_since_best += 1
|
|
76
|
+
if (self.early_stopping_rounds is not None
|
|
77
|
+
and rounds_since_best >= self.early_stopping_rounds):
|
|
78
|
+
if self.verbose:
|
|
79
|
+
print(f"Early stopping at iteration {t + 1}")
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
if self.best_iteration_ is None:
|
|
83
|
+
self.best_iteration_ = len(self.learners_)
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def predict(self, X) -> np.ndarray:
|
|
87
|
+
"""Return continuous predictions of shape (n_samples,)."""
|
|
88
|
+
X_t = self._transform_X(X)
|
|
89
|
+
F = np.full(X_t.shape[0], self.init_pred_)
|
|
90
|
+
for learner in self.learners_[: self.best_iteration_]:
|
|
91
|
+
with torch.no_grad():
|
|
92
|
+
F += self.learning_rate * learner(X_t).numpy().flatten()
|
|
93
|
+
return F
|
|
94
|
+
|
|
95
|
+
def evaluate(self, X, y, verbose: bool = True) -> dict:
|
|
96
|
+
"""Predict on X and report MSE, RMSE, MAE, and R^2 against y."""
|
|
97
|
+
from sklearn.metrics import (
|
|
98
|
+
mean_squared_error, mean_absolute_error, r2_score,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
y = np.asarray(y, dtype=float).ravel()
|
|
102
|
+
y_pred = self.predict(X)
|
|
103
|
+
mse = float(mean_squared_error(y, y_pred))
|
|
104
|
+
report = {
|
|
105
|
+
"mse": mse,
|
|
106
|
+
"rmse": float(np.sqrt(mse)),
|
|
107
|
+
"mae": float(mean_absolute_error(y, y_pred)),
|
|
108
|
+
"r2": float(r2_score(y, y_pred)),
|
|
109
|
+
}
|
|
110
|
+
if verbose:
|
|
111
|
+
for k, v in report.items():
|
|
112
|
+
print(f"{k.upper():5s}: {v:.5f}")
|
|
113
|
+
return report
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kanboost
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Gradient boosting with Kolmogorov-Arnold Network (KAN) learners -- an interpretable alternative to tree-based boosting (XGBoost/LightGBM/CatBoost).
|
|
5
|
+
Author: Tuama M Hamzah
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Tuama M Hamzah
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/tuamah/kanboost
|
|
29
|
+
Classifier: Development Status :: 3 - Alpha
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Programming Language :: Python :: 3
|
|
32
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
33
|
+
Requires-Python: >=3.10
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: pykan>=0.2.5
|
|
37
|
+
Requires-Dist: torch>=2.0
|
|
38
|
+
Requires-Dist: numpy>=1.24
|
|
39
|
+
Requires-Dist: pandas>=1.5
|
|
40
|
+
Requires-Dist: scikit-learn>=1.3
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# KANBoost
|
|
44
|
+
|
|
45
|
+
**Gradient boosting with Kolmogorov-Arnold Network (KAN) learners** — an
|
|
46
|
+
interpretable, from-scratch alternative to tree-based boosting frameworks
|
|
47
|
+
(XGBoost, LightGBM, CatBoost).
|
|
48
|
+
|
|
49
|
+
Instead of decision trees as weak learners, KANBoost fits a sequence of
|
|
50
|
+
small, shallow [KAN](https://arxiv.org/abs/2404.19756) networks to the
|
|
51
|
+
pseudo-residuals of the previous stage, following the classic Friedman
|
|
52
|
+
(2001) gradient boosting recipe. Because each KAN edge is a learnable
|
|
53
|
+
univariate spline rather than an opaque weight, the resulting ensemble
|
|
54
|
+
exposes per-feature shape functions that are directly inspectable —
|
|
55
|
+
closer to a Generalized Additive Model than a black box.
|
|
56
|
+
|
|
57
|
+
> **Status: early-stage research project.** This is *not* a drop-in
|
|
58
|
+
> replacement for CatBoost/XGBoost in production. See
|
|
59
|
+
> [Benchmarks](#benchmarks) and [Honest limitations](#honest-limitations)
|
|
60
|
+
> below before using this for anything important.
|
|
61
|
+
|
|
62
|
+
## Why this exists
|
|
63
|
+
|
|
64
|
+
As of mid-2026, there is no widely-used, pip-installable library that
|
|
65
|
+
combines KAN with gradient boosting. A closely related idea was
|
|
66
|
+
published as **GB-KAN** (ICAART 2026), but no public code accompanies
|
|
67
|
+
that paper. KANBoost is an independent, from-scratch open-source
|
|
68
|
+
implementation of the same general idea, plus:
|
|
69
|
+
|
|
70
|
+
- automatic handling of categorical features (smoothed target-mean
|
|
71
|
+
encoding, done fold-safe), instead of requiring manual one-hot encoding
|
|
72
|
+
- built-in early stopping on a validation set
|
|
73
|
+
- approximate feature importances derived from learned spline coefficients
|
|
74
|
+
|
|
75
|
+
## Install
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
git clone https://github.com/tuamah/kanboost.git
|
|
79
|
+
cd kanboost
|
|
80
|
+
pip install -r requirements.txt
|
|
81
|
+
pip install -e .
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Quickstart
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import pandas as pd
|
|
88
|
+
from sklearn.model_selection import train_test_split
|
|
89
|
+
from kanboost import KANBoostClassifier
|
|
90
|
+
|
|
91
|
+
df = pd.read_csv("your_data.csv")
|
|
92
|
+
X = df.drop(columns=["target"])
|
|
93
|
+
y = df["target"].values
|
|
94
|
+
|
|
95
|
+
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2)
|
|
96
|
+
|
|
97
|
+
model = KANBoostClassifier(
|
|
98
|
+
n_estimators=100,
|
|
99
|
+
learning_rate=0.2,
|
|
100
|
+
kan_hidden=4,
|
|
101
|
+
kan_grid=3,
|
|
102
|
+
categorical_cols=["region", "plan_type"], # optional
|
|
103
|
+
early_stopping_rounds=10,
|
|
104
|
+
)
|
|
105
|
+
model.fit(X_train, y_train, eval_set=(X_val, y_val))
|
|
106
|
+
|
|
107
|
+
probs = model.predict_proba(X_val)[:, 1]
|
|
108
|
+
importances = model.feature_importances()
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Benchmarks
|
|
112
|
+
|
|
113
|
+
Preliminary results on a real-world telecom churn dataset (100K rows,
|
|
114
|
+
10 numeric features used, 8K-row sample for the KANBoost run due to
|
|
115
|
+
current training-speed limits):
|
|
116
|
+
|
|
117
|
+
| Model | Test AUC | Notes |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| CatBoost (tuned, full data, ~100 columns) | **0.6992** | production baseline |
|
|
120
|
+
| KANBoostClassifier (this repo, 10 features, 8K sample) | 0.64 | early prototype, untuned |
|
|
121
|
+
| Plain KAN (no boosting) | 0.65 | single model, same features |
|
|
122
|
+
| Plain MLP | 0.59–0.62 | same features |
|
|
123
|
+
|
|
124
|
+
**Read this table honestly**: KANBoost does not yet beat CatBoost on
|
|
125
|
+
this dataset. The goal of this repo, at this stage, is to establish a
|
|
126
|
+
working, extensible implementation and an honest baseline — not to claim
|
|
127
|
+
state-of-the-art results.
|
|
128
|
+
|
|
129
|
+
## Honest limitations
|
|
130
|
+
|
|
131
|
+
- **Speed**: each weak learner is a full KAN forward/backward pass in
|
|
132
|
+
pure PyTorch. This is currently far slower per-iteration than a
|
|
133
|
+
histogram-based tree split in XGBoost/CatBoost/LightGBM.
|
|
134
|
+
- **Tuning**: hyperparameters (`kan_grid`, `kan_hidden`, `kan_steps`,
|
|
135
|
+
`learning_rate`) interact in ways that are not yet well understood;
|
|
136
|
+
expect to need real tuning for your dataset.
|
|
137
|
+
- **Categorical encoding** is a simple smoothed target-mean encoder, not
|
|
138
|
+
CatBoost's ordered boosting scheme — it can leak on small folds if not
|
|
139
|
+
used carefully.
|
|
140
|
+
- **Missing values** are not yet handled natively; impute before fitting.
|
|
141
|
+
|
|
142
|
+
## Roadmap
|
|
143
|
+
|
|
144
|
+
See [`ROADMAP.md`](./ROADMAP.md) for the full project plan, including
|
|
145
|
+
planned speed optimizations (FastKAN-style RBF basis, `torch.compile`),
|
|
146
|
+
symbolic-formula extraction for the full ensemble, and benchmark
|
|
147
|
+
expansion to standard UCI datasets.
|
|
148
|
+
|
|
149
|
+
## Contributing
|
|
150
|
+
|
|
151
|
+
Issues and PRs welcome, especially:
|
|
152
|
+
- speed optimizations for the per-iteration KAN fit
|
|
153
|
+
- better categorical encoding
|
|
154
|
+
- benchmark results on additional public datasets
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT — see [`LICENSE`](./LICENSE).
|
|
159
|
+
|
|
160
|
+
## Citation / related work
|
|
161
|
+
|
|
162
|
+
If you use this, please also cite the KAN paper and, where relevant,
|
|
163
|
+
the GB-KAN paper this project is conceptually closest to:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
Liu, Z., Wang, Y., Vaidya, S., et al. (2024). KAN: Kolmogorov-Arnold
|
|
167
|
+
Networks. arXiv:2404.19756.
|
|
168
|
+
|
|
169
|
+
[GB-KAN authors] (2026). Gradient Boosting with Interpretable
|
|
170
|
+
Kolmogorov-Arnold Networks. ICAART 2026.
|
|
171
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
kanboost/__init__.py,sha256=bLGvCFaiCYC3ik9N1ScWH38UI5WJZfbvGnbL3wThhaY,775
|
|
2
|
+
kanboost/_base.py,sha256=Vmxzg1p2R3EdlIB8vUXIcNXNtLDbniHi1_Z7l7zku6o,7142
|
|
3
|
+
kanboost/classifier.py,sha256=6Vqv-xQIgusAnyee-Uk6kqIzg2PFpeEU04jzSDcL3Lw,6662
|
|
4
|
+
kanboost/encoders.py,sha256=FU_CFkGNQwoY7DJU_7b4bgvcYNcAien13Wm_kFie9Sc,3182
|
|
5
|
+
kanboost/metrics.py,sha256=YpgMHR1VpM1KMfMmNJOB6-W3cqVdg8pDa7EM_oXQH4c,2132
|
|
6
|
+
kanboost/regressor.py,sha256=uQjNrMzd4lHf6MqIrtP-i9e5fAkXGxAhJ__4buXlMDw,3981
|
|
7
|
+
kanboost-0.0.1.dist-info/licenses/LICENSE,sha256=TibUcjm19hWQsu8odGoBCn5xJmN3FV7n8cKriciimJ8,1071
|
|
8
|
+
kanboost-0.0.1.dist-info/METADATA,sha256=au9AODbHCp2W0EfLxGUVNpMsl9YpzWhHz1bYrZT7mg4,6729
|
|
9
|
+
kanboost-0.0.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
10
|
+
kanboost-0.0.1.dist-info/top_level.txt,sha256=Smj79RKLj6B69IA60ZJUtsYUQb5eYdZwOMIrwYVEHcg,9
|
|
11
|
+
kanboost-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tuama M Hamzah
|
|
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 @@
|
|
|
1
|
+
kanboost
|