mumpy-toolkit 0.2.0__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.
mumpy/metrics.py ADDED
@@ -0,0 +1,187 @@
1
+ """mumpy.metrics: evaluation metrics for ML/DL (pure numpy)."""
2
+ from __future__ import annotations
3
+
4
+ import numpy as np
5
+
6
+ __all__ = [
7
+ "mse", "rmse", "mae", "mape", "r2_score", "explained_variance",
8
+ "accuracy", "precision", "recall", "f1", "confusion_matrix",
9
+ "classification_report", "log_loss", "roc_auc", "silhouette_score",
10
+ ]
11
+
12
+
13
+ def mse(y_true, y_pred):
14
+ y_true = np.asanyarray(y_true, dtype=float).ravel()
15
+ y_pred = np.asanyarray(y_pred, dtype=float).ravel()
16
+ return float(np.mean((y_true - y_pred) ** 2))
17
+
18
+
19
+ def rmse(y_true, y_pred):
20
+ return float(np.sqrt(mse(y_true, y_pred)))
21
+
22
+
23
+ def mae(y_true, y_pred):
24
+ y_true = np.asanyarray(y_true, dtype=float).ravel()
25
+ y_pred = np.asanyarray(y_pred, dtype=float).ravel()
26
+ return float(np.mean(np.abs(y_true - y_pred)))
27
+
28
+
29
+ def mape(y_true, y_pred):
30
+ y_true = np.asanyarray(y_true, dtype=float).ravel()
31
+ y_pred = np.asanyarray(y_pred, dtype=float).ravel()
32
+ mask = y_true != 0
33
+ return float(np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100)
34
+
35
+
36
+ def r2_score(y_true, y_pred):
37
+ y_true = np.asanyarray(y_true, dtype=float).ravel()
38
+ y_pred = np.asanyarray(y_pred, dtype=float).ravel()
39
+ ss_res = np.sum((y_true - y_pred) ** 2)
40
+ ss_tot = np.sum((y_true - y_true.mean()) ** 2)
41
+ return float(1 - ss_res / ss_tot) if ss_tot != 0 else 0.0
42
+
43
+
44
+ def explained_variance(y_true, y_pred):
45
+ y_true = np.asanyarray(y_true, dtype=float).ravel()
46
+ y_pred = np.asanyarray(y_pred, dtype=float).ravel()
47
+ return float(1 - np.var(y_true - y_pred) / np.var(y_true)) if np.var(y_true) != 0 else 0.0
48
+
49
+
50
+ def accuracy(y_true, y_pred):
51
+ y_true = np.asanyarray(y_true).ravel()
52
+ y_pred = np.asanyarray(y_pred).ravel()
53
+ return float((y_true == y_pred).mean())
54
+
55
+
56
+ def confusion_matrix(y_true, y_pred, labels=None):
57
+ y_true = np.asanyarray(y_true).ravel()
58
+ y_pred = np.asanyarray(y_pred).ravel()
59
+ if labels is None:
60
+ labels = np.unique(np.concatenate([y_true, y_pred]))
61
+ else:
62
+ labels = np.asanyarray(labels)
63
+ idx = {v: i for i, v in enumerate(labels.tolist())}
64
+ m = np.zeros((len(labels), len(labels)), dtype=int)
65
+ for t, p in zip(y_true.tolist(), y_pred.tolist()):
66
+ if t in idx and p in idx:
67
+ m[idx[t], idx[p]] += 1
68
+ return m, labels
69
+
70
+
71
+ def _prf(y_true, y_pred, average="binary", pos_label=1):
72
+ y_true = np.asanyarray(y_true).ravel()
73
+ y_pred = np.asanyarray(y_pred).ravel()
74
+ labels = np.unique(np.concatenate([y_true, y_pred]))
75
+ if average == "binary":
76
+ tp = int(((y_pred == pos_label) & (y_true == pos_label)).sum())
77
+ fp = int(((y_pred == pos_label) & (y_true != pos_label)).sum())
78
+ fn = int(((y_pred != pos_label) & (y_true == pos_label)).sum())
79
+ p = tp / (tp + fp) if (tp + fp) else 0.0
80
+ r = tp / (tp + fn) if (tp + fn) else 0.0
81
+ f = 2 * p * r / (p + r) if (p + r) else 0.0
82
+ return p, r, f
83
+ ps, rs, fs = [], [], []
84
+ for lab in labels:
85
+ tp = int(((y_pred == lab) & (y_true == lab)).sum())
86
+ fp = int(((y_pred == lab) & (y_true != lab)).sum())
87
+ fn = int(((y_pred != lab) & (y_true == lab)).sum())
88
+ p = tp / (tp + fp) if (tp + fp) else 0.0
89
+ r = tp / (tp + fn) if (tp + fn) else 0.0
90
+ f = 2 * p * r / (p + r) if (p + r) else 0.0
91
+ ps.append(p)
92
+ rs.append(r)
93
+ fs.append(f)
94
+ if average == "macro":
95
+ return float(np.mean(ps)), float(np.mean(rs)), float(np.mean(fs))
96
+ # micro
97
+ tp = sum(int(((y_pred == lab) & (y_true == lab)).sum()) for lab in labels)
98
+ fp = sum(int(((y_pred == lab) & (y_true != lab)).sum()) for lab in labels)
99
+ fn = sum(int(((y_pred != lab) & (y_true == lab)).sum()) for lab in labels)
100
+ p = tp / (tp + fp) if (tp + fp) else 0.0
101
+ r = tp / (tp + fn) if (tp + fn) else 0.0
102
+ f = 2 * p * r / (p + r) if (p + r) else 0.0
103
+ return p, r, f
104
+
105
+
106
+ def precision(y_true, y_pred, average="binary", pos_label=1):
107
+ return _prf(y_true, y_pred, average, pos_label)[0]
108
+
109
+
110
+ def recall(y_true, y_pred, average="binary", pos_label=1):
111
+ return _prf(y_true, y_pred, average, pos_label)[1]
112
+
113
+
114
+ def f1(y_true, y_pred, average="binary", pos_label=1):
115
+ return _prf(y_true, y_pred, average, pos_label)[2]
116
+
117
+
118
+ def classification_report(y_true, y_pred):
119
+ y_true = np.asanyarray(y_true).ravel()
120
+ y_pred = np.asanyarray(y_pred).ravel()
121
+ labels = np.unique(np.concatenate([y_true, y_pred]))
122
+ lines = {}
123
+ for lab in labels.tolist():
124
+ tp = int(((y_pred == lab) & (y_true == lab)).sum())
125
+ fp = int(((y_pred == lab) & (y_true != lab)).sum())
126
+ fn = int(((y_pred != lab) & (y_true == lab)).sum())
127
+ sup = int((y_true == lab).sum())
128
+ p = tp / (tp + fp) if (tp + fp) else 0.0
129
+ r = tp / (tp + fn) if (tp + fn) else 0.0
130
+ f = 2 * p * r / (p + r) if (p + r) else 0.0
131
+ lines[str(lab)] = {"precision": p, "recall": r, "f1": f, "support": sup}
132
+ lines["accuracy"] = float((y_true == y_pred).mean())
133
+ return lines
134
+
135
+
136
+ def log_loss(y_true, y_pred, eps=1e-15):
137
+ y_true = np.asanyarray(y_true, dtype=float)
138
+ y_pred = np.clip(np.asanyarray(y_pred, dtype=float), eps, 1 - eps)
139
+ if y_pred.ndim == 1 or y_pred.shape[1] == 1:
140
+ y_pred = y_pred.ravel()
141
+ y_true = y_true.ravel()
142
+ return float(-np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred)))
143
+ # multiclass: y_true = class indices
144
+ n = y_true.shape[0]
145
+ return float(-np.mean(np.log(y_pred[np.arange(n), y_true.astype(int).ravel()])))
146
+
147
+
148
+ def roc_auc(y_true, y_score):
149
+ y_true = np.asanyarray(y_true).ravel()
150
+ y_score = np.asanyarray(y_score, dtype=float).ravel()
151
+ order = np.argsort(y_score)
152
+ y_true = y_true[order]
153
+ n_pos = (y_true == 1).sum()
154
+ n_neg = (y_true == 0).sum()
155
+ if n_pos == 0 or n_neg == 0:
156
+ return 0.5
157
+ ranks = np.arange(1, len(y_true) + 1)
158
+ sum_rank_pos = ranks[y_true == 1].sum()
159
+ return float((sum_rank_pos - n_pos * (n_pos + 1) / 2) / (n_pos * n_neg))
160
+
161
+
162
+ def silhouette_score(X, labels):
163
+ X = np.asanyarray(X, dtype=float)
164
+ labels = np.asanyarray(labels).ravel()
165
+ uniq = np.unique(labels)
166
+ if len(uniq) < 2:
167
+ return 0.0
168
+ # pairwise distances (ok for moderate n; sample if huge)
169
+ n = len(X)
170
+ if n > 2000:
171
+ idx = np.random.default_rng(0).choice(n, 2000, replace=False)
172
+ X, labels = X[idx], labels[idx]
173
+ n = 2000
174
+ try:
175
+ from scipy.spatial.distance import cdist # optional fast path
176
+ D = cdist(X, X)
177
+ except Exception:
178
+ D = np.sqrt(((X[:, None, :] - X[None, :, :]) ** 2).sum(-1))
179
+ sils = []
180
+ for i in range(n):
181
+ same = (labels == labels[i])
182
+ same[i] = False
183
+ other = labels != labels[i]
184
+ a = D[i][same].mean() if same.any() else 0.0
185
+ b = min(D[i][labels == u].mean() for u in uniq if u != labels[i])
186
+ sils.append((b - a) / max(a, b) if max(a, b) > 0 else 0.0)
187
+ return float(np.mean(sils))
mumpy/ml.py ADDED
@@ -0,0 +1,443 @@
1
+ """mumpy.ml: classic machine learning in pure numpy (sklearn-like API).
2
+
3
+ Estimators: LinearRegression / Ridge / LogisticRegression / KNN /
4
+ NaiveBayes / DecisionTree / RandomForest-lite / KMeans / PCA / train_test_split
5
+
6
+ from mumpy import ml
7
+ model = ml.LogisticRegression().fit(X_train, y_train)
8
+ pred = model.predict(X_test)
9
+ ml.cross_val_score(model, X, y, cv=5)
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import numpy as np
14
+
15
+ __all__ = [
16
+ "LinearRegression", "Ridge", "LogisticRegression",
17
+ "KNNClassifier", "KNNRegressor", "GaussianNB",
18
+ "DecisionTreeClassifier", "RandomForestClassifier",
19
+ "KMeans", "PCA", "train_test_split", "cross_val_score",
20
+ "kfold", "standardize_for",
21
+ ]
22
+
23
+
24
+ def train_test_split(X, y, test_size=0.2, random_state=None, shuffle=True, stratify=None):
25
+ rng = np.random.default_rng(random_state)
26
+ X = np.asanyarray(X)
27
+ y = np.asanyarray(y)
28
+ n = len(X)
29
+ if stratify is not None:
30
+ # stratified split
31
+ st = np.asanyarray(stratify)
32
+ tr_idx, te_idx = [], []
33
+ for u in np.unique(st):
34
+ idx = np.where(st == u)[0]
35
+ if shuffle:
36
+ idx = rng.permutation(idx)
37
+ k = int(len(idx) * test_size)
38
+ te_idx += idx[:k].tolist()
39
+ tr_idx += idx[k:].tolist()
40
+ tr_idx, te_idx = np.array(tr_idx), np.array(te_idx)
41
+ if shuffle:
42
+ tr_idx = rng.permutation(tr_idx)
43
+ te_idx = rng.permutation(te_idx)
44
+ else:
45
+ idx = np.arange(n)
46
+ if shuffle:
47
+ idx = rng.permutation(idx)
48
+ k = int(n * test_size) if isinstance(test_size, float) else int(test_size)
49
+ te_idx, tr_idx = idx[:k], idx[k:]
50
+ return X[tr_idx], X[te_idx], y[tr_idx], y[te_idx]
51
+
52
+
53
+ def kfold(n, cv=5, shuffle=True, random_state=None):
54
+ idx = np.arange(n)
55
+ if shuffle:
56
+ idx = np.random.default_rng(random_state).permutation(idx)
57
+ folds = np.array_split(idx, cv)
58
+ for i in range(cv):
59
+ te = folds[i]
60
+ tr = np.concatenate([folds[j] for j in range(cv) if j != i])
61
+ yield tr, te
62
+
63
+
64
+ def cross_val_score(estimator, X, y, cv=5, scoring="accuracy"):
65
+ from .metrics import accuracy, r2_score
66
+ X = np.asanyarray(X)
67
+ y = np.asanyarray(y)
68
+ scores = []
69
+ for tr, te in kfold(len(X), cv=cv):
70
+ from copy import deepcopy
71
+ est = deepcopy(estimator)
72
+ est.fit(X[tr], y[tr])
73
+ p = est.predict(X[te])
74
+ scores.append(accuracy(y[te], p) if scoring == "accuracy" else r2_score(y[te], p))
75
+ return np.array(scores)
76
+
77
+
78
+ def standardize_for(X_train, X_test=None):
79
+ from .preprocessing import StandardScaler
80
+ sc = StandardScaler().fit(X_train)
81
+ if X_test is None:
82
+ return sc.transform(X_train), sc
83
+ return sc.transform(X_train), sc.transform(X_test), sc
84
+
85
+
86
+ def _add_bias(X):
87
+ return np.hstack([np.ones((X.shape[0], 1)), X])
88
+
89
+
90
+ class LinearRegression:
91
+ """Ordinary least squares via lstsq (stable) or normal equations."""
92
+
93
+ def __init__(self, fit_intercept=True):
94
+ self.fit_intercept = fit_intercept
95
+
96
+ def fit(self, X, y):
97
+ X = np.asanyarray(X, dtype=float)
98
+ y = np.asanyarray(y, dtype=float).ravel()
99
+ if X.ndim == 1:
100
+ X = X.reshape(-1, 1)
101
+ self.n_features_in_ = X.shape[1]
102
+ A = _add_bias(X) if self.fit_intercept else X
103
+ coef, *_ = np.linalg.lstsq(A, y, rcond=None)
104
+ if self.fit_intercept:
105
+ self.intercept_ = float(coef[0])
106
+ self.coef_ = coef[1:]
107
+ else:
108
+ self.intercept_ = 0.0
109
+ self.coef_ = coef
110
+ return self
111
+
112
+ def predict(self, X):
113
+ X = np.asanyarray(X, dtype=float)
114
+ if X.ndim == 1:
115
+ X = X.reshape(-1, 1)
116
+ return X @ self.coef_ + self.intercept_
117
+
118
+ def score(self, X, y):
119
+ from .metrics import r2_score
120
+ return r2_score(y, self.predict(X))
121
+
122
+
123
+ class Ridge(LinearRegression):
124
+ def __init__(self, alpha=1.0, fit_intercept=True):
125
+ super().__init__(fit_intercept)
126
+ self.alpha = alpha
127
+
128
+ def fit(self, X, y):
129
+ X = np.asanyarray(X, dtype=float)
130
+ y = np.asanyarray(y, dtype=float).ravel()
131
+ if X.ndim == 1:
132
+ X = X.reshape(-1, 1)
133
+ self.n_features_in_ = X.shape[1]
134
+ if self.fit_intercept:
135
+ mu_x, mu_y = X.mean(0), y.mean()
136
+ Xc, yc = X - mu_x, y - mu_y
137
+ A = Xc.T @ Xc + self.alpha * np.eye(X.shape[1])
138
+ self.coef_ = np.linalg.solve(A, Xc.T @ yc)
139
+ self.intercept_ = float(mu_y - mu_x @ self.coef_)
140
+ else:
141
+ A = X.T @ X + self.alpha * np.eye(X.shape[1])
142
+ self.coef_ = np.linalg.solve(A, X.T @ y)
143
+ self.intercept_ = 0.0
144
+ return self
145
+
146
+
147
+ def _sigmoid(z):
148
+ out = np.empty_like(z, dtype=float)
149
+ pos = z >= 0
150
+ out[pos] = 1 / (1 + np.exp(-z[pos]))
151
+ e = np.exp(z[~pos])
152
+ out[~pos] = e / (1 + e)
153
+ return out
154
+
155
+
156
+ class LogisticRegression:
157
+ """Binary (+ softmax multinomial) logistic regression with L2."""
158
+
159
+ def __init__(self, lr=0.1, epochs=1000, l2=1e-4, tol=1e-6, verbose=False, seed=0):
160
+ self.lr = lr
161
+ self.epochs = epochs
162
+ self.l2 = l2
163
+ self.tol = tol
164
+ self.verbose = verbose
165
+ self.seed = seed
166
+
167
+ def fit(self, X, y):
168
+ rng = np.random.default_rng(self.seed)
169
+ X = np.asanyarray(X, dtype=float)
170
+ y = np.asanyarray(y).ravel()
171
+ if X.ndim == 1:
172
+ X = X.reshape(-1, 1)
173
+ self.classes_ = np.unique(y)
174
+ n, d = X.shape
175
+ if len(self.classes_) == 2:
176
+ yc = (y == self.classes_[1]).astype(float)
177
+ w = np.zeros(d)
178
+ b = 0.0
179
+ prev = np.inf
180
+ for ep in range(self.epochs):
181
+ z = X @ w + b
182
+ p = _sigmoid(z)
183
+ err = p - yc
184
+ gw = X.T @ err / n + self.l2 * w
185
+ gb = err.mean()
186
+ w -= self.lr * gw
187
+ b -= self.lr * gb
188
+ loss = -(yc * np.log(p + 1e-12) + (1 - yc) * np.log(1 - p + 1e-12)).mean()
189
+ if abs(prev - loss) < self.tol:
190
+ break
191
+ prev = loss
192
+ self.coef_ = w
193
+ self.intercept_ = float(b)
194
+ else:
195
+ K = len(self.classes_)
196
+ W = rng.normal(0, 0.01, (d, K))
197
+ B = np.zeros(K)
198
+ idx = {v: i for i, v in enumerate(self.classes_.tolist())}
199
+ Y = np.zeros((n, K))
200
+ Y[np.arange(n), [idx[v] for v in y.tolist()]] = 1.0
201
+ for ep in range(self.epochs):
202
+ Z = X @ W + B
203
+ Z -= Z.max(1, keepdims=True)
204
+ P = np.exp(Z)
205
+ P /= P.sum(1, keepdims=True)
206
+ G = (P - Y) / n
207
+ W -= self.lr * (X.T @ G + self.l2 * W)
208
+ B -= self.lr * G.sum(0)
209
+ self.coef_ = W
210
+ self.intercept_ = B
211
+ return self
212
+
213
+ def predict_proba(self, X):
214
+ X = np.asanyarray(X, dtype=float)
215
+ if len(self.classes_) == 2:
216
+ p1 = _sigmoid(X @ self.coef_ + self.intercept_)
217
+ return np.column_stack([1 - p1, p1])
218
+ Z = X @ self.coef_ + self.intercept_
219
+ Z -= Z.max(1, keepdims=True)
220
+ P = np.exp(Z)
221
+ return P / P.sum(1, keepdims=True)
222
+
223
+ def predict(self, X):
224
+ P = self.predict_proba(X)
225
+ return self.classes_[np.argmax(P, axis=1)]
226
+
227
+
228
+ class _KNN:
229
+ def __init__(self, k=5):
230
+ self.k = k
231
+
232
+ def fit(self, X, y):
233
+ self.X_ = np.asanyarray(X, dtype=float)
234
+ self.y_ = np.asanyarray(y)
235
+ return self
236
+
237
+ def _neighbors(self, X):
238
+ X = np.asanyarray(X, dtype=float)
239
+ # chunked distances to save memory
240
+ idx = []
241
+ bs = 512
242
+ for s in range(0, len(X), bs):
243
+ d2 = ((X[s:s + bs, None, :] - self.X_[None, :, :]) ** 2).sum(-1)
244
+ idx.append(np.argpartition(d2, self.k, axis=1)[:, :self.k])
245
+ return np.vstack(idx)
246
+
247
+
248
+ class KNNClassifier(_KNN):
249
+ def predict(self, X):
250
+ idx = self._neighbors(X)
251
+ out = []
252
+ for row in idx:
253
+ vals, counts = np.unique(self.y_[row], return_counts=True)
254
+ out.append(vals[np.argmax(counts)])
255
+ return np.array(out)
256
+
257
+ def predict_proba(self, X):
258
+ idx = self._neighbors(X)
259
+ classes = np.unique(self.y_)
260
+ P = np.zeros((len(X), len(classes)))
261
+ for i, row in enumerate(idx):
262
+ for j, c in enumerate(classes):
263
+ P[i, j] = (self.y_[row] == c).mean()
264
+ return P
265
+
266
+
267
+ class KNNRegressor(_KNN):
268
+ def predict(self, X):
269
+ idx = self._neighbors(X)
270
+ return np.array([self.y_[row].astype(float).mean() for row in idx])
271
+
272
+
273
+ class GaussianNB:
274
+ def fit(self, X, y):
275
+ X = np.asanyarray(X, dtype=float)
276
+ y = np.asanyarray(y).ravel()
277
+ self.classes_ = np.unique(y)
278
+ self.theta_ = np.array([X[y == c].mean(0) for c in self.classes_])
279
+ self.sigma_ = np.array([X[y == c].var(0) + 1e-9 for c in self.classes_])
280
+ self.priors_ = np.array([(y == c).mean() for c in self.classes_])
281
+ return self
282
+
283
+ def predict(self, X):
284
+ X = np.asanyarray(X, dtype=float)
285
+ jll = []
286
+ for t, s, p in zip(self.theta_, self.sigma_, self.priors_):
287
+ jll.append(-0.5 * np.sum(np.log(2 * np.pi * s) + (X - t) ** 2 / s, axis=1) + np.log(p))
288
+ return self.classes_[np.argmax(np.column_stack(jll), axis=1)]
289
+
290
+
291
+ class DecisionTreeClassifier:
292
+ """CART binary tree (gini), max_depth + min_samples_split."""
293
+
294
+ def __init__(self, max_depth=8, min_samples_split=2, seed=0):
295
+ self.max_depth = max_depth
296
+ self.min_samples_split = min_samples_split
297
+ self.seed = seed
298
+
299
+ def fit(self, X, y):
300
+ self.n_classes_ = len(np.unique(y))
301
+ self.classes_ = np.unique(np.asanyarray(y).ravel())
302
+ self._map = {v: i for i, v in enumerate(self.classes_.tolist())}
303
+ yi = np.array([self._map[v] for v in np.asanyarray(y).ravel().tolist()])
304
+ self.tree_ = self._build(np.asanyarray(X, dtype=float), yi, 0)
305
+ return self
306
+
307
+ def _gini(self, y):
308
+ if len(y) == 0:
309
+ return 0
310
+ _, c = np.unique(y, return_counts=True)
311
+ p = c / c.sum()
312
+ return 1 - (p ** 2).sum()
313
+
314
+ def _build(self, X, y, depth):
315
+ n, d = X.shape
316
+ counts = np.bincount(y, minlength=len(self.classes_))
317
+ pred = int(np.argmax(counts))
318
+ if depth >= self.max_depth or n < self.min_samples_split or self._gini(y) == 0:
319
+ return {"leaf": True, "pred": pred}
320
+ best = (1e9, None, None)
321
+ rng = np.random.default_rng(self.seed + depth)
322
+ feats = rng.choice(d, min(d, max(1, int(np.sqrt(d)))), replace=False) if d > 4 else range(d)
323
+ for f in feats:
324
+ ths = np.unique(X[:, f])
325
+ if len(ths) > 20:
326
+ ths = np.percentile(ths, np.linspace(0, 100, 20))
327
+ for t in ths:
328
+ l, r = y[X[:, f] <= t], y[X[:, f] > t]
329
+ if len(l) == 0 or len(r) == 0:
330
+ continue
331
+ g = (len(l) * self._gini(l) + len(r) * self._gini(r)) / n
332
+ if g < best[0]:
333
+ best = (g, f, t)
334
+ if best[1] is None:
335
+ return {"leaf": True, "pred": pred}
336
+ _, f, t = best
337
+ lm = X[:, f] <= t
338
+ return {"leaf": False, "f": int(f), "t": float(t),
339
+ "l": self._build(X[lm], y[lm], depth + 1),
340
+ "r": self._build(X[~lm], y[~lm], depth + 1)}
341
+
342
+ def _predict_one(self, x, node):
343
+ while not node["leaf"]:
344
+ node = node["l"] if x[node["f"]] <= node["t"] else node["r"]
345
+ return node["pred"]
346
+
347
+ def predict(self, X):
348
+ X = np.asanyarray(X, dtype=float)
349
+ idx = np.array([self._predict_one(x, self.tree_) for x in X])
350
+ return self.classes_[idx]
351
+
352
+
353
+ class RandomForestClassifier:
354
+ def __init__(self, n_estimators=20, max_depth=8, seed=0):
355
+ self.n_estimators = n_estimators
356
+ self.max_depth = max_depth
357
+ self.seed = seed
358
+
359
+ def fit(self, X, y):
360
+ X = np.asanyarray(X)
361
+ y = np.asanyarray(y)
362
+ rng = np.random.default_rng(self.seed)
363
+ self.trees_ = []
364
+ n = len(X)
365
+ for i in range(self.n_estimators):
366
+ idx = rng.integers(0, n, n)
367
+ t = DecisionTreeClassifier(max_depth=self.max_depth, seed=self.seed + i)
368
+ t.fit(X[idx], y[idx])
369
+ self.trees_.append(t)
370
+ self.classes_ = self.trees_[0].classes_
371
+ return self
372
+
373
+ def predict(self, X):
374
+ votes = np.column_stack([t.predict(X) for t in self.trees_])
375
+ out = []
376
+ for row in votes:
377
+ vals, counts = np.unique(row, return_counts=True)
378
+ out.append(vals[np.argmax(counts)])
379
+ return np.array(out)
380
+
381
+
382
+ class KMeans:
383
+ def __init__(self, n_clusters=3, max_iter=100, tol=1e-4, seed=0, n_init=3):
384
+ self.n_clusters = n_clusters
385
+ self.max_iter = max_iter
386
+ self.tol = tol
387
+ self.seed = seed
388
+ self.n_init = n_init
389
+
390
+ def fit(self, X):
391
+ X = np.asanyarray(X, dtype=float)
392
+ best_inertia, best = np.inf, None
393
+ for init in range(self.n_init):
394
+ rng = np.random.default_rng(self.seed + init)
395
+ C = X[rng.choice(len(X), self.n_clusters, replace=False)]
396
+ for _ in range(self.max_iter):
397
+ d2 = ((X[:, None, :] - C[None, :, :]) ** 2).sum(-1)
398
+ lab = d2.argmin(1)
399
+ Cn = np.array([X[lab == k].mean(0) if (lab == k).any() else C[k]
400
+ for k in range(self.n_clusters)])
401
+ if np.linalg.norm(Cn - C) < self.tol:
402
+ C = Cn
403
+ break
404
+ C = Cn
405
+ inertia = float(((X - C[lab]) ** 2).sum())
406
+ if inertia < best_inertia:
407
+ best_inertia, best = inertia, (C, lab)
408
+ self.cluster_centers_, labels = best
409
+ self.labels_ = labels
410
+ self.inertia_ = best_inertia
411
+ return self
412
+
413
+ def predict(self, X):
414
+ X = np.asanyarray(X, dtype=float)
415
+ d2 = ((X[:, None, :] - self.cluster_centers_[None, :, :]) ** 2).sum(-1)
416
+ return d2.argmin(1)
417
+
418
+ def fit_predict(self, X):
419
+ return self.fit(X).labels_
420
+
421
+
422
+ class PCA:
423
+ def __init__(self, n_components=2):
424
+ self.n_components = n_components
425
+
426
+ def fit(self, X):
427
+ X = np.asanyarray(X, dtype=float)
428
+ self.mean_ = X.mean(0)
429
+ Xc = X - self.mean_
430
+ U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
431
+ self.components_ = Vt[:self.n_components]
432
+ self.explained_variance_ = (S ** 2) / (len(X) - 1)
433
+ self.explained_variance_ratio_ = self.explained_variance_ / self.explained_variance_.sum()
434
+ return self
435
+
436
+ def transform(self, X):
437
+ return (np.asanyarray(X, dtype=float) - self.mean_) @ self.components_.T
438
+
439
+ def fit_transform(self, X):
440
+ return self.fit(X).transform(X)
441
+
442
+ def inverse_transform(self, Z):
443
+ return np.asanyarray(Z) @ self.components_ + self.mean_