linearrf 1.2.1__tar.gz → 1.2.3__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.1
2
2
  Name: linearrf
3
- Version: 1.2.1
3
+ Version: 1.2.3
4
4
  Summary: A python libary to build Random Forests with Linear Models at the leaves.
5
5
  Author-email: Marian Biermann <marianbiermann@gmx.de>
6
6
  Project-URL: homepage, https://github.com/marianbiermann/lrf
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "linearrf"
7
- version = "1.2.1"
7
+ version = "1.2.3"
8
8
  description = "A python libary to build Random Forests with Linear Models at the leaves."
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Marian Biermann", email = "marianbiermann@gmx.de" }]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: linearrf
3
- Version: 1.2.1
3
+ Version: 1.2.3
4
4
  Summary: A python libary to build Random Forests with Linear Models at the leaves.
5
5
  Author-email: Marian Biermann <marianbiermann@gmx.de>
6
6
  Project-URL: homepage, https://github.com/marianbiermann/lrf
@@ -184,7 +184,7 @@ class _LinearRandomForest:
184
184
 
185
185
  rng = np.random.default_rng(random_state)
186
186
 
187
- idx = rng.choice(np.arange(x.shape[0]), x.shape[0])
187
+ idx = rng.integers(0, x.shape[0], x.shape[0])
188
188
  x = x[idx]
189
189
  y = y[idx]
190
190
 
@@ -82,43 +82,61 @@ def neg_mcc(y_true: np.ndarray, y_pred: np.ndarray):
82
82
 
83
83
 
84
84
  def neg_roc_auc(y_true: np.ndarray, y_pred: np.ndarray):
85
- # Rank-based ROC AUC in O(n log n). Sort by descending score and sweep:
86
- # each step adds the next sample to "predicted positive", so cumulative
87
- # positives / negatives give TPR / FPR at every distinct cutoff.
85
+ # Rank-based ROC AUC in O(n log n). Sort by descending score, then evaluate
86
+ # TPR/FPR only at distinct-score endpoints collapsing each tied group
87
+ # into a single point. Without this, tied predictions produce results that
88
+ # depend on the (arbitrary) within-tie sort order.
88
89
  y_true = np.asarray(y_true)
89
90
  y_pred = np.asarray(y_pred)
90
91
 
91
92
  order = np.argsort(-y_pred, kind='stable')
92
93
  y_sorted = y_true[order].astype(np.float64)
94
+ pred_sorted = y_pred[order]
93
95
 
94
- positives = y_sorted.sum()
95
- negatives = y_sorted.size - positives
96
+ tps_full = np.cumsum(y_sorted)
97
+ fps_full = np.arange(1, y_sorted.size + 1, dtype=np.float64) - tps_full
98
+
99
+ # End of each tied group: positions where the next prediction differs,
100
+ # plus the very last position.
101
+ endpoints = np.concatenate((np.where(np.diff(pred_sorted))[0],
102
+ [y_sorted.size - 1]))
103
+ tps = tps_full[endpoints]
104
+ fps = fps_full[endpoints]
105
+
106
+ positives, negatives = tps[-1], fps[-1]
96
107
  if positives == 0 or negatives == 0:
97
108
  return 0.0
98
109
 
99
- tpr = np.concatenate(([0.0], np.cumsum(y_sorted) / positives))
100
- fpr = np.concatenate(([0.0], np.cumsum(1.0 - y_sorted) / negatives))
110
+ tpr = np.concatenate(([0.0], tps / positives))
111
+ fpr = np.concatenate(([0.0], fps / negatives))
101
112
 
102
113
  # negated so the caller minimizes AUC (matches the previous convention)
103
114
  return -_trapz(tpr, fpr)
104
115
 
105
116
 
106
117
  def neg_pr_auc(y_true: np.ndarray, y_pred: np.ndarray):
107
- # Rank-based PR AUC in O(n log n) same idea as neg_roc_auc above.
118
+ # Rank-based PR AUC in O(n log n). Same tie-collapsing as neg_roc_auc.
108
119
  y_true = np.asarray(y_true)
109
120
  y_pred = np.asarray(y_pred)
110
121
 
111
122
  order = np.argsort(-y_pred, kind='stable')
112
123
  y_sorted = y_true[order].astype(np.float64)
124
+ pred_sorted = y_pred[order]
125
+
126
+ tps_full = np.cumsum(y_sorted)
127
+ fps_full = np.arange(1, y_sorted.size + 1, dtype=np.float64) - tps_full
128
+
129
+ endpoints = np.concatenate((np.where(np.diff(pred_sorted))[0],
130
+ [y_sorted.size - 1]))
131
+ tps = tps_full[endpoints]
132
+ fps = fps_full[endpoints]
113
133
 
114
- positives = y_sorted.sum()
134
+ positives = tps[-1]
115
135
  if positives == 0:
116
136
  return 0.0
117
137
 
118
- pos_cum = np.cumsum(y_sorted)
119
- ranks = np.arange(1, y_sorted.size + 1, dtype=np.float64)
120
- precision = pos_cum / ranks
121
- recall = pos_cum / positives
138
+ precision = tps / (tps + fps)
139
+ recall = tps / positives
122
140
 
123
141
  return -_trapz(precision, recall)
124
142
 
@@ -0,0 +1,112 @@
1
+ import numpy as np
2
+
3
+
4
+ class IRLS:
5
+ """Iteratively Reweighted Least Squares for L2-regularized logistic regression.
6
+
7
+ Each iteration is a single Newton step using the closed-form Hessian for
8
+ the binary log-loss: H = XᵀWX + λR, with W = diag(p(1-p)). Typically
9
+ converges in 5-15 iterations from a zero start and 2-5 iterations from a
10
+ warm start, versus the previous BFGS implementation which could take
11
+ hundreds of function evaluations per fit.
12
+
13
+ Each Newton step is guarded by Armijo backtracking so the loss is
14
+ monotonically non-increasing — protects against overshoot when the warm
15
+ start lands in an ill-conditioned region (saturated probabilities make
16
+ XᵀWX nearly singular on the data side and Newton's quadratic model
17
+ misleads).
18
+
19
+ Args:
20
+ n_iter: maximum Newton iterations.
21
+ tol: stop when ||t·step|| / max(||β||, 1) < tol.
22
+ intercept: whether x[:, 0] is the intercept column. The intercept is
23
+ excluded from L2 regularization.
24
+ """
25
+
26
+ # Textbook Armijo constant. _backtrack_max = 20 → smallest accepted step
27
+ # is 2**-20 of full Newton; below that we treat the iter as a failure.
28
+ _armijo_c1 = 1e-4
29
+ _backtrack_max = 20
30
+
31
+ def __init__(self, n_iter: int = 25, tol: float = 1e-6, intercept: bool = True):
32
+ self.n_iter = n_iter
33
+ self.tol = tol
34
+ self.intercept = intercept
35
+
36
+ def classification(self, x: np.ndarray, y_true: np.ndarray, coef_: np.ndarray, C: float = 1.0):
37
+ # λ = 1/C, matching the sklearn convention. The intercept term sits at
38
+ # position 0 and stays unregularized.
39
+ lam = 1.0 / C
40
+ m = x.shape[1]
41
+
42
+ reg_diag = np.full(m, lam)
43
+ if self.intercept:
44
+ reg_diag[0] = 0.0
45
+
46
+ beta = coef_.astype(np.float64, copy=True)
47
+ y = y_true.astype(np.float64, copy=False)
48
+
49
+ loss = self._loss(x, y, beta, reg_diag)
50
+
51
+ for _ in range(self.n_iter):
52
+ p = self._sigmoid(x @ beta)
53
+
54
+ # Hessian weight; clipped so saturated probabilities don't collapse
55
+ # XᵀWX to singular.
56
+ w = np.clip(p * (1.0 - p), 1e-12, None)
57
+
58
+ grad = x.T @ (p - y) + reg_diag * beta
59
+
60
+ xtwx = (x * w[:, np.newaxis]).T @ x
61
+ # add λR on the diagonal in-place
62
+ xtwx.flat[::m + 1] += reg_diag
63
+
64
+ try:
65
+ step = np.linalg.solve(xtwx, grad)
66
+ except np.linalg.LinAlgError:
67
+ break
68
+
69
+ # Search direction d = -step; ∇fᵀd = -gᵀstep. Should be negative
70
+ # whenever the Hessian is SPD and grad ≠ 0. A non-negative value
71
+ # means the solve produced garbage — bail rather than ascend.
72
+ directional = -(grad @ step)
73
+ if directional >= 0:
74
+ break
75
+
76
+ # Armijo backtracking: shrink t until f(β + t·d) ≤ f(β) + c1·t·∇fᵀd.
77
+ t = 1.0
78
+ new_beta = beta
79
+ new_loss = loss
80
+ for _ in range(self._backtrack_max):
81
+ new_beta = beta - t * step
82
+ new_loss = self._loss(x, y, new_beta, reg_diag)
83
+ if new_loss <= loss + self._armijo_c1 * t * directional:
84
+ break
85
+ t *= 0.5
86
+ else:
87
+ # Could not find a step satisfying sufficient decrease; stop.
88
+ break
89
+
90
+ beta = new_beta
91
+ loss = new_loss
92
+
93
+ # use t·step (the step actually taken) so a heavily-backtracked iter
94
+ # is correctly recognized as small progress
95
+ if np.linalg.norm(t * step) / max(np.linalg.norm(beta), 1.0) < self.tol:
96
+ break
97
+
98
+ return beta
99
+
100
+ @staticmethod
101
+ def _loss(x: np.ndarray, y: np.ndarray, beta: np.ndarray, reg_diag: np.ndarray):
102
+ # L(β) = Σ logaddexp(0, z) - y·z + ½ Σ λ_j β_j², z = Xβ
103
+ # logaddexp form avoids log(p) blowups when p is near 0 or 1.
104
+ z = x @ beta
105
+ nll = np.logaddexp(0.0, z).sum() - y @ z
106
+ reg = 0.5 * (reg_diag * beta).dot(beta)
107
+ return nll + reg
108
+
109
+ @staticmethod
110
+ def _sigmoid(z: np.ndarray):
111
+ # σ(z) = exp(-logaddexp(0, -z)) — overflow-safe in both tails.
112
+ return np.exp(-np.logaddexp(0, -z))
@@ -21,6 +21,9 @@ class LRFRegressor(_LinearRandomForest):
21
21
  self.preprocessing = preprocessing
22
22
  self._estimator_type = 'regressor'
23
23
 
24
+ if criterion not in ('mse', 'rmse', 'mae', 'mape', 'wape', 'neg_explained_variance', 'neg_r2'):
25
+ print(' Metric "{}" is not implemented, MSE is used instead.'.format(criterion))
26
+
24
27
  if linear_model is None:
25
28
  linear_model = Regressor(alpha=self.alpha, preprocessing=self.preprocessing, intercept_in_input=True)
26
29
  else:
@@ -83,7 +86,6 @@ class LRFRegressor(_LinearRandomForest):
83
86
  elif self.criterion == 'neg_r2':
84
87
  val = neg_r2(y_true=y_true, y_pred=y_pred)
85
88
  else:
86
- print(' Metric "{}" is not implemented, MSE is used instead.'.format(self.criterion))
87
89
  val = mse(y_true=y_true, y_pred=y_pred)
88
90
 
89
91
  return val
@@ -114,6 +116,10 @@ class LRFClassifier(_LinearRandomForest):
114
116
  self.preprocessing = preprocessing
115
117
  self._estimator_type = 'classifier'
116
118
 
119
+ if criterion not in ('hamming', 'cross_entropy', 'neg_mcc', 'neg_roc_auc', 'neg_pr_auc'):
120
+ print(' Metric "{}" is not implemented, negative Matthews Correlation Coefficient is used instead.'
121
+ .format(criterion))
122
+
117
123
  if linear_model is None:
118
124
  linear_model = Classifier(C=self.C, preprocessing=self.preprocessing, intercept_in_input=True)
119
125
  else:
@@ -213,8 +219,6 @@ class LRFClassifier(_LinearRandomForest):
213
219
  elif self.criterion == 'neg_roc_auc':
214
220
  val = neg_roc_auc(y_true=y_true, y_pred=y_pred)
215
221
  else:
216
- print(' Metric "{}" is not implemented, the negative Matthews Correlation Coefficient is used '
217
- 'instead.'.format(self.criterion))
218
222
  val = neg_mcc(y_true=y_true, y_pred=y_pred)
219
223
 
220
224
  return val
@@ -1,66 +0,0 @@
1
- import numpy as np
2
-
3
-
4
- class IRLS:
5
- """Iteratively Reweighted Least Squares for L2-regularized logistic regression.
6
-
7
- Each iteration is a single Newton step using the closed-form Hessian for
8
- the binary log-loss: H = XᵀWX + λR, with W = diag(p(1-p)). Typically
9
- converges in 5-15 iterations from a zero start and 2-5 iterations from a
10
- warm start, versus the previous BFGS implementation which could take
11
- hundreds of function evaluations per fit.
12
-
13
- Args:
14
- n_iter: maximum Newton iterations.
15
- tol: stop when ||step|| / max(||β||, 1) < tol.
16
- intercept: whether x[:, 0] is the intercept column. The intercept is
17
- excluded from L2 regularization.
18
- """
19
-
20
- def __init__(self, n_iter: int = 25, tol: float = 1e-6, intercept: bool = True):
21
- self.n_iter = n_iter
22
- self.tol = tol
23
- self.intercept = intercept
24
-
25
- def classification(self, x: np.ndarray, y_true: np.ndarray, coef_: np.ndarray, C: float = 1.0):
26
- # λ = 1/C, matching the sklearn convention. The intercept term sits at
27
- # position 0 and stays unregularized.
28
- lam = 1.0 / C
29
- m = x.shape[1]
30
-
31
- reg_diag = np.full(m, lam)
32
- if self.intercept:
33
- reg_diag[0] = 0.0
34
-
35
- beta = coef_.astype(np.float64, copy=True)
36
- y = y_true.astype(np.float64, copy=False)
37
-
38
- for _ in range(self.n_iter):
39
- p = self._sigmoid(x @ beta)
40
-
41
- # Hessian weight; clipped so saturated probabilities don't collapse
42
- # XᵀWX to singular.
43
- w = np.clip(p * (1.0 - p), 1e-12, None)
44
-
45
- grad = x.T @ (p - y) + reg_diag * beta
46
-
47
- xtwx = (x * w[:, np.newaxis]).T @ x
48
- # add λR on the diagonal in-place
49
- xtwx.flat[::m + 1] += reg_diag
50
-
51
- try:
52
- step = np.linalg.solve(xtwx, grad)
53
- except np.linalg.LinAlgError:
54
- break
55
-
56
- beta -= step
57
-
58
- if np.linalg.norm(step) / max(np.linalg.norm(beta), 1.0) < self.tol:
59
- break
60
-
61
- return beta
62
-
63
- @staticmethod
64
- def _sigmoid(z: np.ndarray):
65
- # σ(z) = exp(-logaddexp(0, -z)) — overflow-safe in both tails.
66
- return np.exp(-np.logaddexp(0, -z))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes