anchorboosting 0.2.0__tar.gz → 0.2.1__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.
Files changed (31) hide show
  1. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/CHANGELOG.rst +5 -0
  2. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/PKG-INFO +1 -1
  3. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/anchorboosting/models.py +104 -19
  4. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/anchorboosting.egg-info/PKG-INFO +1 -1
  5. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/anchorboosting.egg-info/SOURCES.txt +2 -3
  6. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/environment.yml +0 -1
  7. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/pyproject.toml +1 -1
  8. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/tests/test_anchor_boosting.py +29 -9
  9. anchorboosting-0.2.1/tests/test_proj.py +67 -0
  10. anchorboosting-0.2.0/anchorboosting/utils.py +0 -84
  11. anchorboosting-0.2.0/tests/test_utils.py +0 -74
  12. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/.github/workflows/build.yaml +0 -0
  13. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/.github/workflows/ci.yml +0 -0
  14. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/.gitignore +0 -0
  15. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/.pre-commit-config.yaml +0 -0
  16. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/LICENSE +0 -0
  17. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/README.md +0 -0
  18. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/anchorboosting/__init__.py +0 -0
  19. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/anchorboosting/simulate.py +0 -0
  20. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/anchorboosting.egg-info/dependency_links.txt +0 -0
  21. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/anchorboosting.egg-info/requires.txt +0 -0
  22. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/anchorboosting.egg-info/top_level.txt +0 -0
  23. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/docs/Makefile +0 -0
  24. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/docs/bib.bib +0 -0
  25. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/docs/changelog.rst +0 -0
  26. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/docs/conf.py +0 -0
  27. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/docs/index.rst +0 -0
  28. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/readthedocs.yaml +0 -0
  29. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/setup.cfg +0 -0
  30. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/tests/test_derivatives.py +0 -0
  31. {anchorboosting-0.2.0 → anchorboosting-0.2.1}/tests/test_refit.py +0 -0
@@ -1,6 +1,11 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ 0.2.1 (2025-07-01)
5
+ ------------------
6
+
7
+ - `AnchorBooster.refit` no longer raises an error if the model stopped boosting early during initial training.
8
+
4
9
  0.2.0 (2025-06-12)
5
10
  ------------------
6
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anchorboosting
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Efficient tree-based nonlinear anchor regression and classification
5
5
  Author-email: Malte Londschien <malte@londschien.ch>
6
6
  Project-URL: homepage, https://github.com/mlondschien/anchorboosting/
@@ -161,9 +161,6 @@ class AnchorBooster:
161
161
  if self.objective != "regression" and not np.isin(y, [0, 1]).all():
162
162
  raise ValueError("For binary classification, y values must be in {0, 1}.")
163
163
 
164
- if Z is None:
165
- Z = np.zeros(shape=(len(y), 0))
166
-
167
164
  y = y.flatten()
168
165
 
169
166
  if self.objective == "regression":
@@ -198,7 +195,7 @@ class AnchorBooster:
198
195
  f = np.ones(len(y), dtype=np.float64) * self.init_score_
199
196
  booster_preds = f.copy()
200
197
 
201
- Q, _ = np.linalg.qr(Z, mode="reduced") # P_Z f = Q @ (Q^T @ f)
198
+ proj = Proj(Z)
202
199
 
203
200
  if self.objective == "binary":
204
201
  y_tilde = np.where(y == 1, 1, -1).astype(np.float64)
@@ -231,7 +228,7 @@ class AnchorBooster:
231
228
  dr = -f * r + r**2 # d^2/df^2 loss(f, y)
232
229
  ddr = (f**2 - 1) * r - 3 * f * r**2 + 2 * r**3 # d^3/df^3 loss
233
230
 
234
- r_proj = Q @ (Q.T @ r)
231
+ r_proj = proj(r)
235
232
  grad = r + (self.gamma - 1) * r_proj * dr
236
233
 
237
234
  # We wish to fit one additional tree. Intuitively, one would use
@@ -348,19 +345,8 @@ class AnchorBooster:
348
345
  counts += self.params.get("lambda_l2", 0)
349
346
  counts[counts == 0] = 1 # counts==0 should never happen.
350
347
 
351
- # Mdr^T P_Z Mdr = (Mdr^T Q) @ (Mdr^T Q)^T
352
- # One could also compute this using bincount, but it appears this
353
- # version using a sparse matrix is faster.
354
- Mdr = scipy.sparse.csr_matrix(
355
- (
356
- dr,
357
- (np.arange(len(leaves)), leaves),
358
- ),
359
- shape=(len(leaves), num_leaves),
360
- dtype=np.float64,
361
- )
362
- B = Mdr.T.dot(Q)
363
- H = (self.gamma - 1) * B @ B.T
348
+ # (dr * M)^T P_Z (dr * M)
349
+ H = (self.gamma - 1) * proj.sandwich(leaves, num_leaves, weights=dr)
364
350
  H += np.diag(counts)
365
351
 
366
352
  # Compute the 2nd order update
@@ -449,7 +435,8 @@ class AnchorBooster:
449
435
 
450
436
  f = np.full(len(y), self.init_score_, dtype="float64")
451
437
 
452
- for idx in range(self.num_boost_round):
438
+ # The model might have stopped boosting prematurely.
439
+ for idx in range(leaves.shape[1]):
453
440
  n_obs = np.bincount(leaves[:, idx], minlength=num_leaves[idx])
454
441
 
455
442
  if self.objective == "regression":
@@ -505,3 +492,101 @@ def _suppress_stderr():
505
492
  os.dup2(old_stderr_fd, stderr_fd) # put the old stderr back
506
493
  os.close(old_stderr_fd)
507
494
  os.close(devnull_fd)
495
+
496
+
497
+ class Proj:
498
+ """
499
+ Cache the projection onto the subspace spanned by Z.
500
+
501
+ Parameters
502
+ ----------
503
+ Z: np.ndarray of dimension (n, d_Z) or (n,), optional, default=None
504
+ The `Z` matrix or 1d array of integers.
505
+ """
506
+
507
+ def __init__(self, Z):
508
+ if Z is None:
509
+ self._type = "none"
510
+
511
+ elif len(Z.shape) == 1 and np.issubdtype(Z.dtype, np.integer):
512
+ self._type = "categorical"
513
+ self._Z = Z
514
+ self._n_categories = np.max(Z) + 1
515
+ counts = np.bincount(Z, minlength=self._n_categories)
516
+ counts[counts == 0] = 1.0 # Avoid division by zero
517
+ self._one_over_counts = 1 / counts
518
+
519
+ elif len(Z.shape) == 2 and np.issubdtype(Z.dtype, np.floating):
520
+ self._type = "linear"
521
+ self._Q, _ = np.linalg.qr(Z, mode="reduced")
522
+ else:
523
+ raise ValueError(
524
+ "Z should be either a 1d array of integers or a 2d array of floats. "
525
+ f"Got shape {Z.shape} and dtype {Z.dtype}."
526
+ )
527
+
528
+ def __call__(self, f):
529
+ """
530
+ Project the input array `f` onto the subspace spanned by `Z`.
531
+
532
+ Parameters
533
+ ----------
534
+ f: np.ndarray of shape (n,)
535
+ The input array to project.
536
+
537
+ Returns
538
+ -------
539
+ np.ndarray of shape (n,)
540
+ The projected array.
541
+ """
542
+ if self._type == "none":
543
+ return np.zeros_like(f)
544
+
545
+ elif self._type == "categorical":
546
+ sums = np.bincount(self._Z, weights=f, minlength=self._n_categories)
547
+ means = sums * self._one_over_counts
548
+ return means[self._Z]
549
+
550
+ elif self._type == "linear":
551
+ return self._Q @ (self._Q.T @ f)
552
+
553
+ def sandwich(self, leaves, num_leaves, weights):
554
+ """
555
+ For M = weights * one_hot(leaves), return proj(Z, M).T @ proj(Z, M).
556
+
557
+ Parameters
558
+ ----------
559
+ leaves: np.ndarray of shape (n,)
560
+ The leaf indices for each sample in `f`. Integers in [0, num_leaves).
561
+ num_leaves: int
562
+ The number of leaves in the decision tree.
563
+ weights: np.ndarray of shape (n,)
564
+ The input array to project.
565
+
566
+ Returns
567
+ -------
568
+ np.ndarray of shape (d, d)
569
+ The sandwich product.
570
+ """
571
+ if self._type == "none":
572
+ return np.zeros((num_leaves, num_leaves), dtype=weights.dtype)
573
+
574
+ elif self._type == "categorical":
575
+ S = np.zeros((self._n_categories, num_leaves), dtype=weights.dtype)
576
+ np.add.at(S, (self._Z, leaves), weights)
577
+ return (S * self._one_over_counts[:, np.newaxis]).T @ S
578
+
579
+ elif self._type == "linear":
580
+ # M^T P_Z M = (M^T Q) @ (M^T Q)^T
581
+ # One could also compute this using bincount, but it appears this
582
+ # version using a sparse matrix is faster.
583
+ M = scipy.sparse.csr_matrix(
584
+ (
585
+ weights,
586
+ (np.arange(len(leaves)), leaves),
587
+ ),
588
+ shape=(len(leaves), num_leaves),
589
+ dtype=weights.dtype,
590
+ )
591
+ B = M.T @ self._Q
592
+ return B @ B.T
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anchorboosting
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Efficient tree-based nonlinear anchor regression and classification
5
5
  Author-email: Malte Londschien <malte@londschien.ch>
6
6
  Project-URL: homepage, https://github.com/mlondschien/anchorboosting/
@@ -11,7 +11,6 @@ readthedocs.yaml
11
11
  anchorboosting/__init__.py
12
12
  anchorboosting/models.py
13
13
  anchorboosting/simulate.py
14
- anchorboosting/utils.py
15
14
  anchorboosting.egg-info/PKG-INFO
16
15
  anchorboosting.egg-info/SOURCES.txt
17
16
  anchorboosting.egg-info/dependency_links.txt
@@ -24,5 +23,5 @@ docs/conf.py
24
23
  docs/index.rst
25
24
  tests/test_anchor_boosting.py
26
25
  tests/test_derivatives.py
27
- tests/test_refit.py
28
- tests/test_utils.py
26
+ tests/test_proj.py
27
+ tests/test_refit.py
@@ -9,7 +9,6 @@ dependencies:
9
9
  - lightgbm >=4.0.0
10
10
  - setuptools # build
11
11
  - pip # build
12
- - scikit-learn # tests
13
12
  - pytest-cov # tests
14
13
  - pytest # tests
15
14
  - polars # tests
@@ -5,7 +5,7 @@ requires = ['setuptools', 'setuptools-scm', 'wheel']
5
5
  name = "anchorboosting"
6
6
  description = "Efficient tree-based nonlinear anchor regression and classification"
7
7
  readme = "README.md"
8
- version = "0.2.0"
8
+ version = "0.2.1"
9
9
  requires-python = ">=3.9"
10
10
  authors = [
11
11
  { name = "Malte Londschien", email = "malte@londschien.ch" }
@@ -3,19 +3,23 @@ import numpy as np
3
3
  import pytest
4
4
  import scipy
5
5
 
6
- from anchorboosting import AnchorBooster
6
+ from anchorboosting.models import AnchorBooster, Proj
7
7
  from anchorboosting.simulate import f1, simulate
8
8
 
9
9
 
10
10
  @pytest.mark.parametrize("gamma", [1.0, 2.0, 100])
11
11
  @pytest.mark.parametrize("objective", ["regression", "binary"])
12
- def test_anchor_boosting_second_order(gamma, objective):
12
+ @pytest.mark.parametrize("categorical_z", [True, False])
13
+ def test_anchor_boosting_second_order(gamma, objective, categorical_z):
13
14
  learning_rate = 0.1
14
15
  num_leaves = 5
15
16
  n = 200
16
17
  num_boost_round = 10
17
18
 
18
19
  x, y, a = simulate(f1, n=n, shift=0, seed=0)
20
+ if categorical_z:
21
+ a = np.digitize(a[:, 0], bins=[-1, 0, 1, 2, 3])
22
+ assert np.issubdtype(a.dtype, np.integer)
19
23
 
20
24
  if objective == "binary":
21
25
  y = (y > 0).astype(int)
@@ -38,9 +42,11 @@ def test_anchor_boosting_second_order(gamma, objective):
38
42
  num_iteration=1,
39
43
  ).flatten()
40
44
 
45
+ proj = Proj(a)
46
+
41
47
  def regression_loss(leaf_values):
42
48
  residuals = y - f - leaf_values[leaves]
43
- Pa_residuals = a @ np.linalg.solve(a.T @ a, a.T @ residuals)
49
+ Pa_residuals = proj(residuals)
44
50
  return np.sum(np.square(residuals)) + (gamma - 1) * residuals.T @ Pa_residuals
45
51
 
46
52
  def probit_loss(leaf_values):
@@ -49,7 +55,7 @@ def test_anchor_boosting_second_order(gamma, objective):
49
55
  dp = scipy.stats.norm.pdf(scores)
50
56
  losses = -np.log(np.where(y == 1, p, 1 - p))
51
57
  dl = np.where(y == 1, -dp / p, dp / (1 - p))
52
- Pa_dl = a @ np.linalg.solve(a.T @ a, a.T @ dl)
58
+ Pa_dl = proj(dl)
53
59
  return np.sum(losses) + (gamma - 1) / 2 * dl.T @ Pa_dl
54
60
 
55
61
  if objective == "regression":
@@ -176,18 +182,32 @@ def test_compare_anchor_boosting_to_lgbm(parameters):
176
182
 
177
183
 
178
184
  @pytest.mark.parametrize("objective", ["regression", "binary"])
179
- def test_anchor_booster_init_score(objective):
185
+ def test_categorical_and_onehot_is_the_same(objective):
180
186
  X, y, a = simulate(f1, shift=0, seed=0)
181
187
 
188
+ a = np.digitize(a[:, 0], bins=[-1, 0, 1, 2, 3])
189
+
182
190
  if objective == "binary":
183
191
  y = (y > 0).astype(int)
184
192
 
185
- anchor_booster = AnchorBooster(
193
+ anchor_booster1 = AnchorBooster(
186
194
  gamma=1,
187
- num_boost_round=0,
195
+ num_boost_round=10,
188
196
  objective=objective,
189
197
  learning_rate=0.1,
190
198
  ).fit(X, y, Z=a)
191
199
 
192
- predictions = anchor_booster.predict(X)
193
- assert np.allclose(predictions, np.ones_like(y) * y.mean())
200
+ predictions1 = anchor_booster1.predict(X)
201
+
202
+ M = scipy.sparse.csr_matrix(
203
+ (np.ones(len(a), dtype=np.float64), (np.arange(len(a)), a)),
204
+ shape=(len(a), len(np.unique(a))),
205
+ )
206
+ anchor_booster2 = AnchorBooster(
207
+ gamma=1,
208
+ num_boost_round=10,
209
+ objective=objective,
210
+ learning_rate=0.1,
211
+ ).fit(X, y, Z=M.toarray())
212
+ predictions2 = anchor_booster2.predict(X)
213
+ np.testing.assert_allclose(predictions1, predictions2, rtol=1e-5)
@@ -0,0 +1,67 @@
1
+ import numpy as np
2
+ import pytest
3
+ import scipy.sparse
4
+
5
+ from anchorboosting.models import Proj
6
+
7
+ cases = [
8
+ (
9
+ np.array([1, 1, 3]),
10
+ np.array([1.0, 2.0, 3.0]),
11
+ np.array([1.5, 1.5, 3.0]),
12
+ ),
13
+ (
14
+ np.array([[0.0], [0.0], [1.0]]),
15
+ np.array([1.0, 2.0, 3.0]),
16
+ np.array([0.0, 0.0, 3.0]),
17
+ ),
18
+ (
19
+ np.array([[0.0, 1.0], [0.0, 1.0], [1.0, 0.0]]),
20
+ np.array([1.0, 2.0, 3.0]),
21
+ np.array([1.5, 1.5, 3]),
22
+ ),
23
+ (
24
+ np.array([2, 2, 1, 7]),
25
+ np.array([1.0, 2.0, 3.0, 1.0]),
26
+ np.array([1.5, 1.5, 3.0, 1.0]),
27
+ ),
28
+ (
29
+ np.array([1, 1, 0]),
30
+ np.array([1.0, 0.0, 3.0]),
31
+ np.array([0.5, 0.5, 3]),
32
+ ),
33
+ (np.array([0]), np.array([1.0]), np.array([1.0])),
34
+ (np.array([[0.0]]), np.array([0.0]), np.array([0.0])),
35
+ ]
36
+
37
+
38
+ @pytest.mark.parametrize("Z, f, result", cases)
39
+ def test_cached_proj_result(Z, f, result):
40
+ np.testing.assert_almost_equal(Proj(Z)(f), result)
41
+
42
+
43
+ @pytest.mark.parametrize("Z, f, _", cases)
44
+ def test_cached_proj_dot_product(Z, f, _):
45
+ np.testing.assert_almost_equal(
46
+ np.dot(Proj(Z)(f).T, f),
47
+ np.dot(Proj(Z)(f).T, Proj(Z)(f)),
48
+ )
49
+
50
+
51
+ def test_proj_sandwich():
52
+ n = 100
53
+ num_categories = 10
54
+ num_leaves = 5
55
+ rng = np.random.RandomState(0)
56
+
57
+ Z = rng.choice(np.arange(num_categories), size=n, replace=True)
58
+ leaves = rng.choice(np.arange(num_leaves), size=n, replace=True)
59
+ f = rng.normal(size=n)
60
+
61
+ M = scipy.sparse.csr_matrix((f, (np.arange(n), leaves)), (n, num_leaves)).toarray()
62
+ proj = Proj(Z)
63
+
64
+ sandwich1 = proj.sandwich(leaves, num_leaves, f)
65
+ for ldx in range(num_leaves):
66
+ M[:, ldx] = proj(M[:, ldx])
67
+ np.testing.assert_almost_equal(sandwich1, M.T @ M)
@@ -1,84 +0,0 @@
1
- # copy-pasted from https://github.com/mlondschien/ivmodels/blob/09f8bc5a78b3793ae5\
2
- # 1f0bb7f0331b70a0971ebc/ivmodels/utils.py
3
- import numpy as np
4
- import scipy
5
-
6
-
7
- def proj(Z, f, n_categories=None, copy=False):
8
- """
9
- Project f onto the subspace spanned by Z.
10
-
11
- If `Z` is a matrix of floats, uses a copy of `ivmodel`'s `proj:
12
- https://github.com/mlondschien/ivmodels/blob/main/ivmodels/utils.py#L12
13
- If `Z` is a 1d array of unsigned integers, assumes that `Z` is categorical with
14
- values 0, 1, ..., n_categories-1. In this case, the `n_categories` argument must be
15
- be supplied. The projection is done by averaging the values of f within each
16
- category. If `Z` is None, returns np.zeros_like(f).
17
-
18
- Parameters
19
- ----------
20
- Z: np.ndarray of dimension (n, d_Z) or (n,)
21
- The `Z` matrix or 1d array of integers. If None, returns np.zeros_like(f).
22
- *args: np.ndarrays of dimension (n, d_f) or (n,)
23
- Vectors or matrices to project.
24
- n_categories: int, optional, default=None
25
- If not None, then Z is assumed to be categorical with categories 0, 1, ...,
26
- n_categories-1. The projection is done by averaging the values of f within each
27
- category.
28
- copy: bool, default=False
29
- If False, overwrites *args.
30
-
31
- Returns
32
- -------
33
- np.ndarray of dimension (n, d_f) or (n,)
34
- Projection of args onto the subspace spanned by Z. Same number of
35
- outputs as args. Same dimension as args
36
- """
37
- if Z is None:
38
- return np.zeros_like(f)
39
-
40
- if len(f.shape) > 2:
41
- raise ValueError(f"*args should have shapes (n, d_f) or (n,). Got {f.shape}.")
42
- if f.shape[0] != Z.shape[0]:
43
- raise ValueError(f"Shape mismatch: Z.shape={Z.shape}, f.shape={f.shape}.")
44
-
45
- if n_categories is not None:
46
- f = f.copy() if copy else f
47
-
48
- if len(Z.shape) != 1 or not np.issubdtype(Z.dtype, np.integer):
49
- raise ValueError(
50
- "If n_categories is not None, then Z should be a single column of "
51
- f"integers. Got shape {Z.shape} and dtype {Z.dtype}."
52
- )
53
-
54
- if len(f.shape) == 1:
55
- means = np.zeros((n_categories,))
56
- counts = np.zeros(n_categories)
57
-
58
- # np.add.at(a, indices, b) is equivalent to a[indices] += b
59
- np.add.at(means, Z, f)
60
- np.add.at(counts, Z, 1)
61
-
62
- means[counts > 0] = means[counts > 0] / counts[counts > 0]
63
-
64
- f[:] = means[Z]
65
- else:
66
- means = np.zeros((n_categories, f.shape[1]))
67
- counts = np.zeros(n_categories)
68
-
69
- # np.add.at(a, indices, b) is equivalent to a[indices] += b
70
- np.add.at(means, Z, f)
71
- np.add.at(counts, Z, 1)
72
-
73
- means[counts > 0, :] = means[counts > 0, :] / counts[counts > 0, None]
74
-
75
- f[:] = means[Z, :]
76
-
77
- return f
78
-
79
- # The gelsy driver raises in this case - we handle it separately
80
- if len(f.shape) == 2 and f.shape[1] == 0:
81
- return np.zeros_like(f)
82
-
83
- # return np.dot(Z, scipy.linalg.pinv(Z.T @ Z) @ Z.T @ args[0])
84
- return np.dot(Z, scipy.linalg.lstsq(Z, f, cond=None, lapack_driver="gelsy")[0])
@@ -1,74 +0,0 @@
1
- import numpy as np
2
- import pytest
3
- from scipy.linalg import norm
4
- from sklearn.preprocessing import OneHotEncoder
5
-
6
- from anchorboosting.utils import proj
7
-
8
- cases = [
9
- (
10
- np.array([[0], [0], [1]]),
11
- np.array([[1.0], [2.0], [3.0]]),
12
- np.array([[1.5], [1.5], [3]]),
13
- ),
14
- (
15
- np.array([[2], [2], [1], [3]]),
16
- np.array([[1.0], [2.0], [3.0], [1.0]]),
17
- np.array([[1.5], [1.5], [3], [1]]),
18
- ),
19
- (
20
- np.array([[1], [1], [0]]),
21
- np.array([[1.0, 2.0], [0.0, 2.0], [3.0, 4.0]]),
22
- np.array([[0.5, 2.0], [0.5, 2.0], [3.0, 4.0]]),
23
- ),
24
- (
25
- np.array([[1], [1], [0]]),
26
- np.array([1.0, 0.0, 3.0]),
27
- np.array([0.5, 0.5, 3]),
28
- ),
29
- (
30
- np.array([[1], [1], [0]]),
31
- np.array([[1.0, 2.0], [0.0, 2.0], [3.0, 4.0]]),
32
- np.array([[0.5, 2.0], [0.5, 2.0], [3.0, 4.0]]),
33
- ),
34
- (
35
- np.array([[2], [2], [1]]),
36
- np.array([[1.0, 2.0], [0.0, 2.0], [3.0, 4.0]]),
37
- np.array([[0.5, 2.0], [0.5, 2.0], [3.0, 4.0]]),
38
- ),
39
- (np.array([[0]]), np.array([[1]]), np.array([[1]])),
40
- ]
41
-
42
-
43
- @pytest.mark.parametrize("anchor, residuals, result", cases)
44
- @pytest.mark.parametrize("n_categories", [4, None])
45
- def test_proj_result(n_categories, anchor, residuals, result):
46
- if n_categories is None:
47
- if anchor.shape[1] != 1:
48
- pytest.skip("one_hot only makes sense for 1D anchors")
49
- encoder = OneHotEncoder(sparse_output=False, drop=None)
50
- anchor = encoder.fit_transform(anchor).astype(float)
51
- else:
52
- anchor = anchor.flatten()
53
-
54
- np.testing.assert_almost_equal(
55
- proj(anchor, residuals.copy(), n_categories=n_categories), result
56
- )
57
-
58
-
59
- @pytest.mark.parametrize("anchor, residuals, _", cases)
60
- def test_proj_dot_product(anchor, residuals, _):
61
- np.testing.assert_almost_equal(
62
- np.dot(proj(anchor, residuals).T, residuals),
63
- np.dot(proj(anchor, residuals).T, proj(anchor, residuals)),
64
- )
65
-
66
-
67
- @pytest.mark.parametrize("anchor, residuals, result", cases)
68
- @pytest.mark.parametrize("gamma", [0.1, 1, 2, 10, 100, 1000])
69
- def test_proj_orthogonal(anchor, residuals, result, gamma):
70
- np.testing.assert_almost_equal(
71
- norm(residuals - proj(anchor, residuals)) ** 2
72
- + gamma * norm(proj(anchor, residuals)) ** 2,
73
- norm(residuals) ** 2 + (gamma - 1) * norm(proj(anchor, residuals)) ** 2,
74
- )
File without changes
File without changes
File without changes