Skip to content

Commit d57eecb

Browse files
Share ordered complement helper
1 parent 4dc3d84 commit d57eecb

7 files changed

Lines changed: 58 additions & 24 deletions

File tree

examples/vignettes/03_correlation_and_dependence.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# %% [markdown]
22
# # 03. Getting Started with NNS: Correlation and Dependence
33
#
4-
# Section-for-section port of the R vignette. Figures are saved beside the
5-
# script under `output/03_correlation_and_dependence/`.
4+
# Section-for-section port of `NNSvignette_03_Correlation_and_Dependence.Rmd`.
5+
# Figures are saved beside the script under `output/03_correlation_and_dependence/`.
66

77
from __future__ import annotations
88

examples/vignettes/04_normalization_and_rescaling.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
"""04. Getting Started with NNS: Normalization and Rescaling.
1+
"""# %% [markdown]
2+
04. Getting Started with NNS: Normalization and Rescaling.
23
34
This is an instructional, section-for-section Python port of
45
``NNSvignette_04_Normalization_and_Rescaling.Rmd``. It preserves the R

src/nns/_indices.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Index helpers shared by resampling-heavy estimators."""
2+
3+
from __future__ import annotations
4+
5+
import numpy as np
6+
from numpy.typing import NDArray
7+
8+
9+
def ordered_complement(
10+
all_index: NDArray[np.int64],
11+
excluded: NDArray[np.int64],
12+
) -> NDArray[np.int64]:
13+
"""Return ``all_index`` values not present in ``excluded`` without sorting.
14+
15+
The stack/boost split builders already create unique validation indices
16+
against a dense ``0..n-1`` index vector. A boolean complement avoids
17+
``np.setdiff1d``'s sort/unique work while preserving the input row order.
18+
"""
19+
if all_index.size == 0 or excluded.size == 0:
20+
return all_index.copy()
21+
mask_size = int(max(np.max(all_index), np.max(excluded))) + 1
22+
excluded_mask = np.zeros(mask_size, dtype=bool)
23+
excluded_mask[excluded] = True
24+
return all_index[~excluded_mask[all_index]]

src/nns/boost.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import numpy as np
2626
from numpy.typing import NDArray
2727

28+
from nns._indices import ordered_complement
2829
from nns._reg_engine import _validate_dist, nns_reg_engine
2930
from nns._rrng import RRNG
3031
from nns.central_tendencies import nns_gravity
@@ -422,7 +423,7 @@ def fit_subset(
422423
trial_validation_index = random_validation_index()
423424
else:
424425
trial_validation_index = validation_index
425-
trial_train_index = np.setdiff1d(all_index, trial_validation_index)
426+
trial_train_index = ordered_complement(all_index, trial_validation_index)
426427
predicted = fit_subset(subset, trial_train_index, trial_validation_index)
427428
learner_results[i] = score(predicted, y[trial_validation_index])
428429

@@ -516,7 +517,7 @@ def best_trial_subset() -> list[tuple[int, ...]]:
516517

517518
if ts_test_value is None:
518519
epoch_validation_index = random_validation_index()
519-
epoch_train_index = np.setdiff1d(all_index, epoch_validation_index)
520+
epoch_train_index = ordered_complement(all_index, epoch_validation_index)
520521
else:
521522
assert epoch_split_id is not None
522523
epoch_train_index, epoch_validation_index = chronological_splits[

src/nns/diff.py

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -500,10 +500,6 @@ def _row_nanmean(bands: list[NDArray[np.float64]]) -> NDArray[np.float64]:
500500
)
501501
sample_size = grid.shape[0]
502502
k = eval_vec.size
503-
position = np.tile(
504-
np.repeat(np.asarray(["l", "m", "u"], dtype=object), sample_size), k
505-
)
506-
ids = np.repeat(np.arange(k), 3 * sample_size)
507503
for band in h_s:
508504
steps = np.array([_dydx_step(column, ev, float(band)) for ev in eval_vec])
509505
blocks: list[NDArray[np.float64]] = []
@@ -573,19 +569,16 @@ def reduce(parts: list[NDArray[np.float64]]) -> dict[str, NDArray[np.float64]]:
573569
block = parts[main_chunk_idx[bi]]
574570
if vector_branch:
575571
k = steps.size
576-
f = np.empty(k)
577-
s = np.empty(k)
578-
for g in range(k):
579-
lo = np.mean(block[(position == "l") & (ids == g)])
580-
mid = np.mean(block[(position == "m") & (ids == g)])
581-
up = np.mean(block[(position == "u") & (ids == g)])
582-
h = steps[g]
583-
if np.isfinite(h) and h != 0.0:
584-
f[g] = (up - lo) / (2.0 * h)
585-
s[g] = (up - 2.0 * mid + lo) / (h**2)
586-
else:
587-
f[g] = np.nan
588-
s[g] = np.nan
572+
means = np.mean(block.reshape(k, 3, sample_size), axis=2)
573+
lo = means[:, 0]
574+
mid = means[:, 1]
575+
up = means[:, 2]
576+
finite = np.isfinite(steps) & (steps != 0.0)
577+
with np.errstate(invalid="ignore", divide="ignore"):
578+
f = (up - lo) / (2.0 * steps)
579+
s = (up - 2.0 * mid + lo) / (steps**2)
580+
f = np.where(finite, f, np.nan)
581+
s = np.where(finite, s, np.nan)
589582
else:
590583
n_eval = steps.size
591584
lo = block[:n_eval]

src/nns/stack.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import numpy as np
2626
from numpy.typing import NDArray
2727

28+
from nns._indices import ordered_complement
2829
from nns._reg_engine import (
2930
_mreg_predict_path,
3031
_mreg_prepare,
@@ -540,7 +541,7 @@ def make_splits() -> list[dict[str, NDArray[np.int64]]]:
540541
validation = np.sort(
541542
rng.sample_int(n_obs, size, replace=False) - 1
542543
).astype(np.int64)
543-
training = np.setdiff1d(all_index, validation)
544+
training = ordered_complement(all_index, validation)
544545
if training.size < 3 or not has_all_classes(y[training]):
545546
raise ValueError(
546547
"Unable to create a repeated holdout retaining enough "
@@ -578,7 +579,7 @@ def make_splits() -> list[dict[str, NDArray[np.int64]]]:
578579
out = []
579580
for b in range(1, use_folds + 1):
580581
validation = np.flatnonzero(fold_id == b).astype(np.int64)
581-
training = np.setdiff1d(all_index, validation)
582+
training = ordered_complement(all_index, validation)
582583
if validation.size > 0 and training.size >= 3 and has_all_classes(y[training]):
583584
out.append({"train": training, "validation": validation})
584585
if len(out) < 2:

tests/invariants/test_indices.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from __future__ import annotations
2+
3+
import numpy as np
4+
5+
from nns._indices import ordered_complement
6+
7+
8+
def test_ordered_complement_preserves_source_index_order() -> None:
9+
all_index = np.array([4, 2, 0, 3, 1], dtype=np.int64)
10+
excluded = np.array([0, 3], dtype=np.int64)
11+
12+
result = ordered_complement(all_index, excluded)
13+
14+
np.testing.assert_array_equal(result, np.array([4, 2, 1], dtype=np.int64))

0 commit comments

Comments
 (0)