diff --git a/docs/api_reference.md b/docs/api_reference.md index e8d6bf41..6d34f4fc 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -227,7 +227,7 @@ Computes a matrix representation of causation relationships. Closest R API: `NNS.reg`. -Bivariate NNS regression and classification. Supports numeric, class-code, confidence interval, smoothing, dimension-reduction, and public factor-expansion paths. +Bivariate NNS regression and classification. `dist=None` is the default and selects the native blended NNS distance; `dist="NNS"` is the explicit alias. Supports numeric, class-code, confidence interval, smoothing, dimension-reduction, and public factor-expansion paths. Returns Python-native structures, generally dictionaries containing estimates, diagnostics, residuals, intervals, and related arrays. @@ -241,13 +241,13 @@ Multivariate regression and classification surface. Numeric and class paths are Closest R API: `NNS.stack`. -Stacked ensemble API for numeric and classification workflows. +Stacked ensemble API for numeric and classification workflows. `dist=None` is the default and selects the native blended NNS distance; `dist="NNS"` is the explicit alias. #### `nns_boost` Closest R API: `NNS.boost`. -Boosted ensemble API. Deterministic and stochastic structures are implemented. The high-feature stochastic threshold path is guarded to make an installed-R failure explicit. +Boosted ensemble API. `dist=None` is the default and selects the native blended NNS distance; `dist="NNS"` is the explicit alias. Deterministic and stochastic structures are implemented. The high-feature stochastic threshold path is guarded to make an installed-R failure explicit. ### Categorical and factor helpers diff --git a/docs/conventions.md b/docs/conventions.md index 8c2c8381..a34ba38e 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -158,6 +158,7 @@ dictionaries of NumPy arrays. `multivariate_call=True` returns R's internal two-column regression-point structure as `{"x": ..., "y": ...}` for `nns_m_reg`, including after dimension-reduction projection. Matrix `x` without dimension reduction dispatches to `nns_m_reg`. +`dist=None` is the default for `nns_reg`, `nns_m_reg`, `nns_stack`, and `nns_boost` and selects the native blended NNS distance, with `dist="NNS"` as the explicit alias. Continuous predictors use range-normalized coordinate differences and sum `abs(z) + z**2` across non-zero-range columns; explicit `dist="L1"`, `dist="L2"`, and `dist="FACTOR"` keep their legacy behavior. Classification is supported for numeric/logical/factor-like class-code targets. `smooth=True` follows installed R's ordinary piecewise fallback for univariate inputs with fewer than four observations and for univariate `order="max"`; R diff --git a/src/nns/_public_reg.py b/src/nns/_public_reg.py index a8f027eb..dc924282 100644 --- a/src/nns/_public_reg.py +++ b/src/nns/_public_reg.py @@ -27,7 +27,7 @@ def nns_reg( n_best: Any = None, smooth: bool = False, noise_reduction: Any = "off", - dist: str = "L2", + dist: str | None = None, ncores: int | None = None, point_only: bool = False, multivariate_call: bool = False, @@ -39,7 +39,7 @@ def nns_reg( Plot flags are handled strictly as side effects and therefore cannot alter the statistical return value used by the parity suite. """ - result = _nns_reg( + result = _nns_reg( # type: ignore[misc] x, y, factor_2_dummy=factor_2_dummy, diff --git a/src/nns/_reg_engine.py b/src/nns/_reg_engine.py index d8ece6d7..5ba7be81 100644 --- a/src/nns/_reg_engine.py +++ b/src/nns/_reg_engine.py @@ -4,7 +4,7 @@ R/Multivariate_Regression.R at the NNS 13.1 Beta repair revision): - one consistent prediction rule for fitted values and point estimates, -- real range-normalized L1/L2/FACTOR distance dispatch, +- real range-normalized NNS/L1/L2/FACTOR distance dispatch, - training-fitted encodings and dimension-reduction normalization (batch-independent predictions), - restricted automatic classification (factor/character/logical or exact @@ -78,11 +78,13 @@ def _validate_nbest(n_best: Any) -> NBest: def _validate_dist(dist: Any) -> str: + if dist is None: + return "NNS" if not isinstance(dist, str): - raise ValueError("[dist] must be one of 'L1', 'L2', or 'FACTOR'.") + raise ValueError("[dist] must be one of None, 'NNS', 'L1', 'L2', or 'FACTOR'.") value = dist.upper() - if value not in {"L1", "L2", "FACTOR"}: - raise ValueError("[dist] must be one of 'L1', 'L2', or 'FACTOR'.") + if value not in {"NNS", "L1", "L2", "FACTOR"}: + raise ValueError("[dist] must be one of None, 'NNS', 'L1', 'L2', or 'FACTOR'.") return value @@ -917,6 +919,8 @@ def _mreg_distances( if not np.any(active): return np.zeros((xtest.shape[0], rpm_x.shape[0]), dtype=np.float64) z = (xtest[:, None, active] - rpm_x[None, :, active]) / ranges[active] + if dist == "NNS": + return np.sum(np.abs(z) + z * z, axis=2) if dist == "L1": return np.sum(np.abs(z), axis=2) return np.sqrt(np.sum(z * z, axis=2)) @@ -1248,7 +1252,7 @@ def nns_reg_engine( n_best: Any = None, smooth: bool = False, noise_reduction: str = "off", - dist: str = "L2", + dist: str | None = None, point_only: bool = False, multivariate_call: bool = False, ) -> dict[str, Any]: diff --git a/src/nns/boost.py b/src/nns/boost.py index eed3855e..d6e75069 100644 --- a/src/nns/boost.py +++ b/src/nns/boost.py @@ -24,7 +24,7 @@ import numpy as np from numpy.typing import NDArray -from nns._reg_engine import nns_reg_engine +from nns._reg_engine import _validate_dist, nns_reg_engine from nns._rrng import RRNG from nns.central_tendencies import nns_gravity from nns.stack import nns_stack @@ -80,6 +80,7 @@ def nns_boost( threshold: float | None = None, obj_fn: Callable[[NDArray[np.float64], NDArray[np.float64]], float] | None = None, objective: Objective = "min", + dist: str | None = None, extreme: bool = False, features_only: bool = False, feature_importance: bool = False, @@ -113,6 +114,7 @@ def nns_boost( if not isinstance(objective, str) or objective.lower() not in {"min", "max"}: raise ValueError("[objective] must be exactly 'min' or 'max'.") objective_value: Objective = cast(Objective, objective.lower()) + dist_value = _validate_dist(dist) if type is not None: if not isinstance(type, str) or type.lower() != "class": @@ -355,6 +357,7 @@ def fit_subset( order=depth_value, type="CLASS" if is_class else None, point_only=True, + dist=dist_value, ) return sanitize_predictions( np.asarray(fit["Point.est"], dtype=np.float64), by @@ -595,7 +598,7 @@ def xstar_frame(v: NDArray[np.float64]) -> NDArray[np.float64]: obj_fn=obj_fn, objective=objective_value, optimize_threshold=False, - dist="L2", + dist=dist_value, cv_size=cv_fraction, balance=balance, ts_test=ts_test_value, diff --git a/src/nns/diff.py b/src/nns/diff.py index 046014cc..c20cc5da 100644 --- a/src/nns/diff.py +++ b/src/nns/diff.py @@ -186,6 +186,7 @@ def dy_dx( x_values, y_values, plot=False, + dist="L2", ) fitted = result["Fitted.xy"] if not isinstance(fitted, dict): @@ -527,6 +528,7 @@ def _dy_dx_numeric( point_only=True, smooth=True, plot=False, + dist="L2", ) estimates = np.asarray(reg_output["Point.est"], dtype=np.float64).reshape(3, -1).T eval_col = deriv_points[:, 1] @@ -703,6 +705,7 @@ def _dy_d_stack_estimates( order=None, folds=1, ncores=1, + dist="L2", ) return np.asarray(result["stack"], dtype=np.float64).reshape(-1) diff --git a/src/nns/multivariate_regression.py b/src/nns/multivariate_regression.py index d677d176..1851cbcd 100644 --- a/src/nns/multivariate_regression.py +++ b/src/nns/multivariate_regression.py @@ -64,7 +64,7 @@ def nns_m_reg( residual_plot: bool = False, location: object | None = None, noise_reduction: NoiseReduction = "off", - dist: str = "L2", + dist: str | None = None, return_values: bool = False, plot_regions: bool = False, ncores: int | None = None, @@ -78,7 +78,7 @@ def nns_m_reg( """ _warn_unsupported( location=location is not None, - dist=dist != "L2", + dist=False, return_values=return_values is not False, plot_regions=plot_regions, ncores=ncores is not None, diff --git a/src/nns/regression.py b/src/nns/regression.py index 86c6811d..e147de13 100644 --- a/src/nns/regression.py +++ b/src/nns/regression.py @@ -131,7 +131,7 @@ def nns_reg( n_best: object | None = ..., smooth: bool = ..., noise_reduction: NoiseReduction = ..., - dist: str = ..., + dist: str | None = ..., ncores: int | None = ..., point_only: bool = ..., multivariate_call: Literal[True], @@ -160,7 +160,7 @@ def nns_reg( n_best: object | None = ..., smooth: bool = ..., noise_reduction: NoiseReduction = ..., - dist: str = ..., + dist: str | None = ..., ncores: int | None = ..., point_only: bool = ..., multivariate_call: Literal[False] = ..., @@ -188,7 +188,7 @@ def nns_reg( n_best: object | None = None, smooth: bool = False, noise_reduction: NoiseReduction = "off", - dist: str = "L2", + dist: str | None = None, ncores: int | None = None, point_only: bool = False, multivariate_call: bool = False, @@ -243,7 +243,7 @@ def _nns_reg_legacy( n_best: object | None = None, smooth: bool = False, noise_reduction: NoiseReduction = "off", - dist: str = "L2", + dist: str | None = None, ncores: int | None = None, point_only: bool = False, multivariate_call: bool = False, @@ -815,7 +815,7 @@ def _nns_reg_dimred( n_best: object | None, smooth: bool, noise_reduction: NoiseReduction, - dist: str, + dist: str | None, point_only: bool, multivariate_call: bool, class_levels: list[object] | None = None, @@ -931,7 +931,7 @@ def _dimred_projection( tau: object | None, threshold: float, point_est: NDArray[np.float64] | None, - dist: str, + dist: str | None, variable_names: Sequence[str] | None = None, ) -> _DimredProjection: coef = _dimred_coefficients(x, y, dim_red_method=dim_red_method, tau=tau) @@ -1081,10 +1081,10 @@ def _project_dimred_points( coef: NDArray[np.float64], active_count: int, *, - dist: str, + dist: str | None, ) -> NDArray[np.float64]: joint = np.vstack((point_est, x)) - if dist.lower() != "factor": + if dist is None or dist.lower() != "factor": joint = _r_minmax_columns(joint, zero_guard=True) point_norm = joint[: point_est.shape[0]] return np.asarray(point_norm @ coef / active_count, dtype=np.float64) diff --git a/src/nns/stack.py b/src/nns/stack.py index 1567a503..563bcc49 100644 --- a/src/nns/stack.py +++ b/src/nns/stack.py @@ -28,6 +28,7 @@ from nns._reg_engine import ( _mreg_predict_path, _mreg_prepare, + _validate_dist, _validate_order, nns_reg_engine, ) @@ -124,7 +125,7 @@ def nns_stack( obj_fn: Callable[[NDArray[np.float64], NDArray[np.float64]], float] | None = None, objective: Objective = "min", optimize_threshold: bool = True, - dist: str = "L2", + dist: str | None = None, cv_size: float | None = None, balance: bool = False, ts_test: int | None = None, @@ -194,16 +195,7 @@ def nns_stack( if not math.isfinite(pred_int) or pred_int <= 0 or pred_int >= 1: raise ValueError("[pred.int] must be a finite scalar strictly between 0 and 1.") - if not isinstance(dist, str) or dist.lower() not in {"l2", "l1", "dtw", "factor"}: - raise ValueError("[dist] must be one character value among 'L2', 'L1', 'DTW', 'FACTOR'.") - if dist.lower() != "l2": - raise ValueError( - "The corrected NNS.stack currently supports dist = 'L2' only. " - "The production multivariate NNS.reg path does not yet implement " - "distinct L1, DTW, or FACTOR estimators, so those values are " - "rejected rather than silently treated as L2." - ) - dist_value = "L2" + dist_value = _validate_dist(dist) # ---------------------------------------------------------------------- # Input data and response coding diff --git a/src/nns/var.py b/src/nns/var.py index b84a9a84..823da111 100644 --- a/src/nns/var.py +++ b/src/nns/var.py @@ -157,6 +157,7 @@ def _var_interpolate_and_extrapolate( folds=5, method=1, status=False, + dist="L2", )["stack"] variable_interpolation[missing] = np.asarray(fill, dtype=np.float64) else: @@ -167,6 +168,7 @@ def _var_interpolate_and_extrapolate( point_est=np.asarray(missing, dtype=np.float64) + 1, plot=False, point_only=True, + dist="L2", )["Point.est"] if fitted_missing is not None and fitted_missing.size: variable_interpolation[missing] = np.asarray(fitted_missing, dtype=np.float64) diff --git a/tests/invariants/test_native_distance_defaults.py b/tests/invariants/test_native_distance_defaults.py new file mode 100644 index 00000000..a9bc9584 --- /dev/null +++ b/tests/invariants/test_native_distance_defaults.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +import inspect +from typing import Any + +import numpy as np +import pytest + +from nns import nns_boost, nns_reg, nns_stack +from nns._reg_engine import _mreg_distances, nns_reg_engine + + +def _assert_numeric_equal(a: Any, b: Any) -> None: + if isinstance(a, dict): + assert isinstance(b, dict) + assert a.keys() == b.keys() + for key in a: + _assert_numeric_equal(a[key], b[key]) + elif isinstance(a, np.ndarray): + np.testing.assert_allclose(a, b, equal_nan=True) + elif isinstance(a, (float, int, np.floating, np.integer)) or a is None: + assert a == pytest.approx(b) if a is not None else b is None + + +def test_public_signatures_default_dist_none() -> None: + assert inspect.signature(nns_reg).parameters["dist"].default is None + assert inspect.signature(nns_stack).parameters["dist"].default is None + assert inspect.signature(nns_boost).parameters["dist"].default is None + + +def test_nns_reg_none_nns_and_lowercase_are_equivalent() -> None: + x = np.array([[0.0, 0.0], [1.0, 3.0], [2.0, 1.0], [4.0, 5.0], [7.0, 2.0]]) + y = np.array([0.0, 1.0, 1.5, 2.2, 3.0]) + point = np.array([[1.5, 1.0], [5.0, 4.0]]) + base = nns_reg(x, y, point_est=point, point_only=True, dist=None) + explicit = nns_reg(x, y, point_est=point, point_only=True, dist="NNS") + lower = nns_reg(x, y, point_est=point, point_only=True, dist="nns") + np.testing.assert_allclose(base["Point.est"], explicit["Point.est"]) + np.testing.assert_allclose(base["Point.est"], lower["Point.est"]) + assert base["dist"] == explicit["dist"] == lower["dist"] == "NNS" + + +def test_native_distance_formula_differs_from_l1_and_l2() -> None: + rpm = np.array([[0.0, 0.0], [2.0, 3.0], [4.0, 1.0], [8.0, 6.0]]) + xtest = np.array([[1.0, 4.0]]) + mins = rpm.min(axis=0) + maxs = rpm.max(axis=0) + nns = _mreg_distances(rpm, xtest, "NNS", mins, maxs) + l1 = _mreg_distances(rpm, xtest, "L1", mins, maxs) + l2 = _mreg_distances(rpm, xtest, "L2", mins, maxs) + assert not np.allclose(nns, l1) + assert not np.allclose(nns, l2) + z = (xtest[:, None, :] - rpm[None, :, :]) / (maxs - mins) + np.testing.assert_allclose(nns, np.sum(np.abs(z) + z**2, axis=2)) + + +def test_invalid_distance_raises_clear_value_error() -> None: + with pytest.raises(ValueError, match=r"dist.*NNS.*L1.*L2.*FACTOR"): + nns_reg_engine([[0.0], [1.0]], [0.0, 1.0], dist="bad") + + +def test_explicit_l1_l2_factor_still_accepted() -> None: + x = np.array([[0.0, 0.0], [1.0, 1.0], [2.0, 1.0], [3.0, 0.0]]) + y = np.array([0.0, 1.0, 2.0, 3.0]) + for dist in ("L1", "L2", "FACTOR"): + out = nns_reg(x, y, point_est=np.array([[1.5, 0.5]]), point_only=True, dist=dist) + assert out["dist"] == dist + assert np.isfinite(out["Point.est"]).all() + + +def test_nns_stack_none_and_nns_are_equivalent() -> None: + x = np.array([[0.0, 0.0], [1.0, 2.0], [2.0, 1.0], [3.0, 4.0], [4.0, 3.0], [5.0, 5.0]]) + y = np.array([0.0, 1.0, 1.4, 2.2, 2.8, 3.5]) + kwargs = dict(method=(1,), stack=False, folds=2, seed=42, status=False) + none = nns_stack(x, y, x[:2], dist=None, **kwargs) + explicit = nns_stack(x, y, x[:2], dist="NNS", **kwargs) + np.testing.assert_allclose(none["reg"], explicit["reg"]) + + +def test_nns_boost_none_and_nns_are_equivalent_and_l2_propagates() -> None: + x = np.array([[0.0, 0.0], [1.0, 2.0], [2.0, 1.0], [3.0, 4.0], [4.0, 3.0], [5.0, 5.0]]) + y = np.array([0.0, 1.0, 1.4, 2.2, 2.8, 3.5]) + kwargs = dict(learner_trials=2, epochs=1, seed=7, status=False) + none = nns_boost(x, y, x[:2], dist=None, **kwargs) + explicit = nns_boost(x, y, x[:2], dist="NNS", **kwargs) + l2 = nns_boost(x, y, x[:2], dist="L2", **kwargs) + np.testing.assert_allclose(none["results"], explicit["results"]) + assert np.isfinite(l2["results"]).all() + + +def test_nns_predict_path_matches_batch_predict_for_native_distance() -> None: + from nns._reg_engine import _mreg_predict, _mreg_predict_path + + rpm_x = np.array([[0.0, 0.0], [2.0, 3.0], [4.0, 1.0], [8.0, 6.0]]) + rpm_y = np.array([0.0, 1.5, 2.0, 4.0]) + xtest = np.array([[1.0, 4.0], [6.0, 2.0]]) + mins = rpm_x.min(axis=0) + maxs = rpm_x.max(axis=0) + path = _mreg_predict_path(xtest, rpm_x, rpm_y, 3, "NNS", mins, maxs) + for k in range(1, 4): + batch = _mreg_predict(xtest, rpm_x, rpm_y, k, "NNS", mins, maxs, False) + np.testing.assert_allclose(path[:, k - 1], batch) + + +def test_nns_stack_native_scoring_matches_nns_alias_and_differs_from_l2_objective() -> None: + x = np.array( + [ + [0.0, 0.0], + [1.0, 4.0], + [2.0, 1.0], + [3.0, 8.0], + [4.0, 2.0], + [5.0, 7.0], + [6.0, 3.0], + [7.0, 6.0], + ] + ) + y = np.array([0.0, 1.0, 1.4, 2.2, 2.8, 3.5, 3.7, 4.1]) + kwargs = dict(method=(1,), stack=False, folds=2, cv_size=0.25, seed=3, status=False) + native = nns_stack(x, y, x[:3], dist=None, **kwargs) + alias = nns_stack(x, y, x[:3], dist="NNS", **kwargs) + l2 = nns_stack(x, y, x[:3], dist="L2", **kwargs) + assert native["NNS.reg.n.best"] == alias["NNS.reg.n.best"] + assert native["OBJfn.reg"] == pytest.approx(alias["OBJfn.reg"]) + np.testing.assert_allclose(native["reg"], alias["reg"]) + assert native["OBJfn.reg"] != pytest.approx(l2["OBJfn.reg"]) + assert not np.allclose(native["reg"], l2["reg"]) + + +def test_nns_boost_l2_propagates_to_learner_trials_and_final_stack( + monkeypatch: pytest.MonkeyPatch, +) -> None: + import nns.boost as boost_mod + + seen_engine: list[str | None] = [] + seen_stack: list[str | None] = [] + real_engine = boost_mod.nns_reg_engine + real_stack = boost_mod.nns_stack + + def wrapped_engine(*args: Any, **kwargs: Any) -> Any: + seen_engine.append(kwargs.get("dist")) + return real_engine(*args, **kwargs) + + def wrapped_stack(*args: Any, **kwargs: Any) -> Any: + seen_stack.append(kwargs.get("dist")) + return real_stack(*args, **kwargs) + + monkeypatch.setattr(boost_mod, "nns_reg_engine", wrapped_engine) + monkeypatch.setattr(boost_mod, "nns_stack", wrapped_stack) + x = np.array([[0.0, 0.0], [1.0, 2.0], [2.0, 1.0], [3.0, 4.0], [4.0, 3.0], [5.0, 5.0]]) + y = np.array([0.0, 1.0, 1.4, 2.2, 2.8, 3.5]) + nns_boost(x, y, x[:2], dist="L2", learner_trials=2, epochs=1, seed=7, status=False) + assert seen_engine + assert seen_stack + assert set(seen_engine) == {"L2"} + assert set(seen_stack) == {"L2"} diff --git a/tests/parity/test_boost.py b/tests/parity/test_boost.py index 23d07d5b..593edb1c 100644 --- a/tests/parity/test_boost.py +++ b/tests/parity/test_boost.py @@ -35,6 +35,7 @@ def test_nns_boost_numeric_matches_r(depth: int | None) -> None: cv_size=0.25, depth=depth, feature_importance=False, + dist="L2", ) _assert_boost_matches(actual, expected) @@ -67,6 +68,7 @@ def test_nns_boost_ivs_test_none_matches_r() -> None: learner_trials=10, cv_size=0.25, feature_importance=False, + dist="L2", ) _assert_boost_matches(actual, expected) @@ -89,6 +91,7 @@ def test_nns_boost_ivs_test_none_is_seed_invariant() -> None: learner_trials=10, cv_size=0.25, feature_importance=False, + dist="L2", )["results"], dtype=np.float64, ) @@ -101,6 +104,7 @@ def test_nns_boost_ivs_test_none_is_seed_invariant() -> None: cv_size=0.25, feature_importance=False, random_seed=seed, + dist="L2", )["results"], dtype=np.float64, ) @@ -129,6 +133,7 @@ def test_nns_boost_deterministic_wider_feature_set_matches_r() -> None: learner_trials=100, cv_size=0.25, feature_importance=False, + dist="L2", ) _assert_boost_matches(actual, expected) @@ -157,6 +162,7 @@ def test_nns_boost_features_only_matches_r() -> None: cv_size=0.25, features_only=True, feature_importance=False, + dist="L2", ) _assert_boost_matches(actual, expected) @@ -187,6 +193,7 @@ def test_nns_boost_ts_test_deterministic_matches_r(ts_test: int) -> None: cv_size=0.25, ts_test=ts_test, feature_importance=False, + dist="L2", ) _assert_boost_matches(actual, expected) @@ -217,6 +224,7 @@ def test_nns_boost_ts_test_features_only_matches_r() -> None: features_only=True, ts_test=5, feature_importance=False, + dist="L2", ) _assert_boost_matches(actual, expected) @@ -247,6 +255,7 @@ def test_nns_boost_stochastic_epoch_path_matches_r_structure() -> None: cv_size=0.25, random_seed=4, feature_importance=False, + dist="L2", ) assert set(actual) == set(cast(dict[str, object], expected)) @@ -289,6 +298,7 @@ def test_nns_boost_stochastic_epoch_ts_test_matches_r_structure() -> None: ts_test=5, random_seed=5, feature_importance=False, + dist="L2", ) assert set(actual) == set(cast(dict[str, object], expected)) @@ -330,6 +340,7 @@ def test_nns_boost_factor_predictor_matches_r() -> None: learner_trials=10, cv_size=0.25, feature_importance=False, + dist="L2", ) _assert_boost_matches(actual, expected) @@ -362,6 +373,7 @@ def test_nns_boost_factor_predictor_features_only_matches_r() -> None: cv_size=0.25, features_only=True, feature_importance=False, + dist="L2", ) _assert_boost_matches(actual, expected) @@ -408,6 +420,7 @@ def test_nns_boost_multiple_factor_predictors_match_r_positional( # No random_seed override: R's NNS.boost defaults to seed = 123L and the # R reference sets no external seed, so the port must use its matching # default seed (123) to reproduce the same Mersenne-Twister CV split. + dist="L2", ) _assert_boost_matches(actual, expected) @@ -440,6 +453,7 @@ def test_nns_boost_numeric_pred_int_matches_r(depth: int, pred_int: float) -> No depth=depth, pred_int=pred_int, feature_importance=False, + dist="L2", ) _assert_boost_matches(actual, expected) @@ -474,6 +488,7 @@ def test_nns_boost_features_only_ignores_pred_int_like_r() -> None: features_only=True, pred_int=0.95, feature_importance=False, + dist="L2", ) assert set(actual) == {"feature.weights", "feature.frequency"} @@ -507,6 +522,7 @@ def test_nns_boost_binary_class_matches_r(depth: int | None) -> None: depth=depth, type="class", feature_importance=False, + dist="L2", ) _assert_boost_matches(actual, expected) @@ -541,6 +557,7 @@ def test_nns_boost_binary_class_pred_int_matches_r(depth: int) -> None: type="class", pred_int=0.95, feature_importance=False, + dist="L2", ) _assert_boost_matches(actual, expected) @@ -575,6 +592,7 @@ def test_nns_boost_multiclass_matches_r(depth: int) -> None: depth=depth, type="class", feature_importance=False, + dist="L2", ) _assert_boost_matches(actual, expected) @@ -608,6 +626,7 @@ def test_nns_boost_features_only_ignores_class_pred_int_like_r() -> None: type="class", pred_int=0.95, feature_importance=False, + dist="L2", ) assert set(actual) == {"feature.weights", "feature.frequency"} @@ -641,6 +660,7 @@ def test_nns_boost_factor_like_class_matches_r() -> None: type="class", class_levels=["A", "B", "C"], feature_importance=False, + dist="L2", ) _assert_boost_matches(actual, expected) @@ -671,6 +691,7 @@ def test_nns_boost_class_stable_metadata_matches_r_when_n_best_is_structural() - depth=1, type="class", feature_importance=False, + dist="L2", ) assert isinstance(expected, dict) @@ -715,6 +736,7 @@ def test_nns_boost_class_features_only_matches_r() -> None: type="class", features_only=True, feature_importance=False, + dist="L2", ) _assert_boost_matches(actual, expected) @@ -752,6 +774,7 @@ def test_nns_boost_balance_binary_class_matches_r_structure(depth: int) -> None: balance=True, random_seed=42, feature_importance=False, + dist="L2", ) _assert_boost_class_structure(actual, expected, point_rows=point.shape[0], classes=np.unique(y)) @@ -790,6 +813,7 @@ def test_nns_boost_balance_multiclass_and_factor_structure() -> None: balance=True, random_seed=7, feature_importance=False, + dist="L2", ) _assert_boost_class_structure( @@ -833,6 +857,7 @@ def test_nns_boost_balance_class_pred_int_matches_r_structure() -> None: random_seed=42, pred_int=0.95, feature_importance=False, + dist="L2", ) _assert_boost_class_structure( @@ -874,6 +899,7 @@ def test_nns_boost_balance_type_none_forces_class_path() -> None: balance=True, random_seed=9, feature_importance=False, + dist="L2", ) _assert_boost_class_structure( diff --git a/tests/parity/test_multivariate_regression.py b/tests/parity/test_multivariate_regression.py index d994a4f0..a6cedd3e 100644 --- a/tests/parity/test_multivariate_regression.py +++ b/tests/parity/test_multivariate_regression.py @@ -43,7 +43,7 @@ def test_nns_reg_multivariate_call_matches_r(order: int | None) -> None: False, True, ) - actual = nns_reg(x, y, order=order, multivariate_call=True) + actual = nns_reg(x, y, order=order, multivariate_call=True, dist="L2") expected_dict = cast(dict[str, Any], expected) assert set(actual) == set(expected_dict) @@ -108,6 +108,7 @@ def test_nns_m_reg_matches_r( point_only=point_only, noise_reduction=cast(NoiseReduction, noise), ncores=1, + dist="L2", ) _assert_m_reg_matches(actual, expected) @@ -151,6 +152,7 @@ def test_nns_m_reg_confidence_interval_matches_r( point_est=point_est, confidence_interval=confidence_interval, ncores=1, + dist="L2", ) _assert_m_reg_matches(actual, expected) @@ -186,6 +188,7 @@ def test_nns_m_reg_classification_matches_r( type="class", point_est=point_est, ncores=1, + dist="L2", ) _assert_m_reg_matches(actual, expected) @@ -223,6 +226,7 @@ def test_nns_m_reg_class_confidence_interval_matches_r( point_est=point_est, confidence_interval=0.95, ncores=1, + dist="L2", ) _assert_m_reg_matches(actual, expected) @@ -254,6 +258,7 @@ def test_nns_m_reg_factor_levels_return_numeric_codes() -> None: type="class", point_est=point_est, class_levels=levels, + dist="L2", ) _assert_m_reg_matches(actual, expected) @@ -287,6 +292,7 @@ def test_nns_m_reg_factor_levels_class_confidence_interval_matches_r() -> None: point_est=point_est, confidence_interval=0.95, class_levels=levels, + dist="L2", ) _assert_m_reg_matches(actual, expected) @@ -310,7 +316,7 @@ def test_nns_reg_matrix_classification_dispatches_to_m_reg() -> None: "off", type="class", ) - actual = nns_reg(x, y, order=1, type="class", point_est=point_est) + actual = nns_reg(x, y, order=1, type="class", point_est=point_est, dist="L2") _assert_m_reg_matches(actual, expected) diff --git a/tests/parity/test_stack.py b/tests/parity/test_stack.py index 810da85a..5ddbab1e 100644 --- a/tests/parity/test_stack.py +++ b/tests/parity/test_stack.py @@ -39,6 +39,7 @@ def test_nns_stack_numeric_matches_r(method: list[int], stack: bool) -> None: method=method, stack=stack, dim_red_method="cor", + dist="L2", ) _assert_stack_matches(actual, expected, exact_probability_threshold=False) @@ -72,6 +73,7 @@ def test_nns_stack_equal_dim_red_matches_r() -> None: order=2, stack=False, dim_red_method="equal", + dist="L2", ) _assert_stack_matches(actual, expected, exact_probability_threshold=False) @@ -106,6 +108,7 @@ def test_nns_stack_factor_predictor_method1_matches_r() -> None: method=1, stack=True, dim_red_method="cor", + dist="L2", ) _assert_stack_matches(actual, expected, exact_probability_threshold=False) @@ -140,6 +143,7 @@ def test_nns_stack_factor_predictor_method2_factor_only_matches_r_fallback() -> method=2, stack=True, dim_red_method="cor", + dist="L2", ) _assert_stack_matches(actual, expected, exact_probability_threshold=False) @@ -174,6 +178,7 @@ def test_nns_stack_factor_predictor_method12_factor_only_matches_r_fallback() -> method=(1, 2), stack=True, dim_red_method="cor", + dist="L2", ) _assert_stack_matches(actual, expected, exact_probability_threshold=False) @@ -214,6 +219,7 @@ def test_nns_stack_mixed_factor_predictor_method2_matches_r() -> None: method=2, stack=True, dim_red_method="cor", + dist="L2", ) _assert_stack_matches(actual, expected, exact_probability_threshold=False) @@ -254,6 +260,7 @@ def test_nns_stack_mixed_factor_predictor_method12_matches_r() -> None: method=(1, 2), stack=True, dim_red_method="cor", + dist="L2", ) _assert_stack_matches(actual, expected, exact_probability_threshold=False) @@ -292,6 +299,7 @@ def test_nns_stack_ts_test_matches_r(method: list[int], ts_test: int) -> None: stack=True, dim_red_method="cor", ts_test=ts_test, + dist="L2", ) _assert_stack_matches(actual, expected, exact_probability_threshold=False) @@ -328,6 +336,7 @@ def test_nns_stack_var_like_ts_test_matches_r() -> None: stack=True, dim_red_method="cor", ts_test=ts_test, + dist="L2", ) _assert_stack_matches(actual, expected, exact_probability_threshold=False) @@ -363,6 +372,7 @@ def test_nns_stack_pred_int_matches_r(method: list[int]) -> None: stack=True, dim_red_method="cor", pred_int=0.95, + dist="L2", ) _assert_stack_matches(actual, expected, exact_probability_threshold=False) @@ -398,6 +408,7 @@ def test_nns_stack_binary_class_matches_r(method: list[int]) -> None: stack=True, dim_red_method="cor", type="class", + dist="L2", ) _assert_stack_matches(actual, expected, exact_probability_threshold=False) @@ -435,6 +446,7 @@ def test_nns_stack_binary_class_pred_int_matches_r(method: list[int]) -> None: dim_red_method="cor", type="class", pred_int=0.95, + dist="L2", ) _assert_stack_matches(actual, expected, exact_probability_threshold=False) @@ -477,6 +489,7 @@ def test_nns_stack_multiclass_matches_r(method: list[int]) -> None: stack=True, dim_red_method="cor", type="class", + dist="L2", ) _assert_stack_matches(actual, expected, exact_probability_threshold=False) @@ -516,6 +529,7 @@ def test_nns_stack_factor_like_class_pred_int_matches_r() -> None: type="class", class_levels=["A", "B", "C"], pred_int=0.95, + dist="L2", ) _assert_stack_matches(actual, expected, exact_probability_threshold=False) @@ -553,6 +567,7 @@ def test_nns_stack_factor_like_class_matches_r() -> None: dim_red_method="cor", type="class", class_levels=["A", "B", "C"], + dist="L2", ) _assert_stack_matches(actual, expected, exact_probability_threshold=False) @@ -607,6 +622,7 @@ def test_nns_stack_balance_binary_class_matches_r_structure(method: list[int]) - type="class", balance=True, random_seed=42, + dist="L2", ) _assert_stack_class_structure(actual, expected, point_rows=point.shape[0], classes=np.unique(y)) @@ -649,6 +665,7 @@ def test_nns_stack_balance_multiclass_and_factor_structure() -> None: class_levels=["A", "B", "C"], balance=True, random_seed=7, + dist="L2", ) _assert_stack_class_structure( @@ -695,6 +712,7 @@ def test_nns_stack_balance_class_pred_int_matches_r_structure() -> None: balance=True, random_seed=42, pred_int=0.95, + dist="L2", ) _assert_stack_class_structure( @@ -737,6 +755,7 @@ def test_nns_stack_balance_type_none_forces_class_path() -> None: method=1, balance=True, random_seed=9, + dist="L2", ) _assert_stack_class_structure( diff --git a/tools/NNS/R/Multivariate_Regression.R b/tools/NNS/R/Multivariate_Regression.R index 749213df..46add974 100644 --- a/tools/NNS/R/Multivariate_Regression.R +++ b/tools/NNS/R/Multivariate_Regression.R @@ -1,8 +1,8 @@ NNS.M.reg <- function (X_n, Y, factor.2.dummy = TRUE, order = NULL, n.best = NULL, type = NULL, point.est = NULL, point.only = FALSE, - plot = FALSE, residual.plot = TRUE, location = NULL, noise.reduction = 'off', dist = "L2", + plot = FALSE, residual.plot = TRUE, location = NULL, noise.reduction = 'off', dist = NULL, return.values = FALSE, plot.regions = FALSE, ncores = NULL, confidence.interval = NULL){ - dist <- tolower(dist) + dist <- if (is.null(dist)) "nns" else tolower(dist) ### For Multiple regressions ### Turn each column into numeric values diff --git a/tools/NNS/R/Regression.R b/tools/NNS/R/Regression.R index b04e5805..60b51b0a 100644 --- a/tools/NNS/R/Regression.R +++ b/tools/NNS/R/Regression.R @@ -21,7 +21,7 @@ #' \code{k Nearest Neighbors} algorithm. Different values of \code{n.best} are tested using cross-validation in \link{NNS.stack}. #' @param smooth logical; \code{FALSE} (default) Applies a smoothing spline instead of local linear fit to regression points. #' @param noise.reduction the method of determining regression points options: ("mean", "median", "mode", "off"); In low signal:noise situations,\code{(noise.reduction = "mean")} uses means for \link{NNS.dep} restricted partitions, \code{(noise.reduction = "median")} uses medians instead of means for \link{NNS.dep} restricted partitions, while \code{(noise.reduction = "mode")} uses modes instead of means for \link{NNS.dep} restricted partitions. \code{(noise.reduction = "off")} uses an overall central tendency measure for partitions. -#' @param dist options:("L1", "L2", "FACTOR") the method of distance calculation; Selects the distance calculation used. \code{dist = "L2"} (default) selects the Euclidean distance and \code{(dist = "L1")} selects the Manhattan distance; \code{(dist = "FACTOR")} uses a frequency. +#' @param dist options:(NULL, "NNS", "L1", "L2", "FACTOR") the method of distance calculation; \code{dist = NULL} is the default and selects the native blended NNS distance; \code{dist = "NNS"} is the explicit alias. \code{dist = "L2"} selects Euclidean distance, \code{dist = "L1"} selects Manhattan distance, and \code{dist = "FACTOR"} uses a frequency. #' @param ncores integer; value specifying the number of cores to be used in the parallelized procedure. If NULL (default), the number of cores to be used is equal to the number of cores of the machine - 1. #' @param multivariate.call Internal argument for multivariate regressions. #' @param point.only Internal argument for abbreviated output. @@ -143,7 +143,7 @@ NNS.reg = function (x, y, n.best = NULL, smooth = FALSE, noise.reduction = "off", - dist = "L2", + dist = NULL, ncores = NULL, point.only = FALSE, multivariate.call = FALSE){ @@ -155,7 +155,7 @@ NNS.reg = function (x, y, if(plot.regions && !is.null(order) && order == "max") stop('Please reduce the "order" or set "plot.regions = FALSE".') - dist <- tolower(dist) + dist <- if (is.null(dist)) "nns" else tolower(dist) if(any(class(x)%in%c("tbl","data.table")) && ncol(x)==1) x <- as.vector(unlist(x)) if(any(class(y)%in%c("tbl","data.table")) && ncol(y)==1) y <- as.vector(unlist(y)) diff --git a/tools/NNS/R/Stack.R b/tools/NNS/R/Stack.R index 9287edda..42271bb0 100644 --- a/tools/NNS/R/Stack.R +++ b/tools/NNS/R/Stack.R @@ -9,7 +9,7 @@ #' @param obj.fn expression; \code{expression(sum((predicted - actual)^2))} (default) Sum of squared errors is the default objective function. Any \code{expression()} using the specific terms \code{predicted} and \code{actual} can be used. #' @param objective options: ("min", "max") \code{"min"} (default) Select whether to minimize or maximize the objective function \code{obj.fn}. #' @param optimize.threshold logical; \code{TRUE} (default) Will optimize the probability threshold value for rounding in classification problems. If \code{FALSE}, returns 0.5. -#' @param dist options:("L1", "L2", "DTW", "FACTOR") the method of distance calculation; Selects the distance calculation used. \code{dist = "L2"} (default) selects the Euclidean distance and \code{(dist = "L1")} selects the Manhattan distance; \code{(dist = "DTW")} selects the dynamic time warping distance; \code{(dist = "FACTOR")} uses a frequency. +#' @param dist options:(NULL, "NNS", "L1", "L2", "FACTOR") the method of distance calculation; \code{dist = NULL} is the default and selects the native blended NNS distance; \code{dist = "NNS"} is the explicit alias. \code{dist = "L2"} selects Euclidean distance, \code{dist = "L1"} selects Manhattan distance, and \code{dist = "FACTOR"} uses a frequency. #' @param CV.size numeric [0, 1]; \code{NULL} (default) Sets the cross-validation size if \code{(IVs.test = NULL)}. Defaults to a random value between 0.2 and 0.33 for a random sampling of the training set. #' @param balance logical; \code{FALSE} (default) Uses both up and down sampling to balance the classes. \code{type="CLASS"} required. #' @param ts.test integer; NULL (default) Sets the length of the test set for time-series data; typically \code{2*h} parameter value from \link{NNS.ARMA} or double known periods to forecast. @@ -74,7 +74,7 @@ NNS.stack <- function(IVs.train, obj.fn = expression( sum((predicted - actual)^2) ), objective = "min", optimize.threshold = TRUE, - dist = "L2", + dist = NULL, CV.size = NULL, balance = FALSE, ts.test = NULL, @@ -133,7 +133,7 @@ NNS.stack <- function(IVs.train, if(is.null(dim(IVs.test))) IVs.test <- data.frame(t(IVs.test)) else IVs.test <- data.frame(IVs.test) - dist <- tolower(dist) + dist <- if (is.null(dist)) "nns" else tolower(dist) i_s <- numeric() THRESHOLDS <- vector(mode = "list", folds) diff --git a/tools/NNS/man/NNS.reg.Rd b/tools/NNS/man/NNS.reg.Rd index 820cfb15..866c71c0 100644 --- a/tools/NNS/man/NNS.reg.Rd +++ b/tools/NNS/man/NNS.reg.Rd @@ -23,7 +23,7 @@ NNS.reg( n.best = NULL, smooth = FALSE, noise.reduction = "off", - dist = "L2", + dist = NULL, ncores = NULL, point.only = FALSE, multivariate.call = FALSE @@ -67,7 +67,7 @@ NNS.reg( \item{noise.reduction}{the method of determining regression points options: ("mean", "median", "mode", "off"); In low signal:noise situations,\code{(noise.reduction = "mean")} uses means for \link{NNS.dep} restricted partitions, \code{(noise.reduction = "median")} uses medians instead of means for \link{NNS.dep} restricted partitions, while \code{(noise.reduction = "mode")} uses modes instead of means for \link{NNS.dep} restricted partitions. \code{(noise.reduction = "off")} uses an overall central tendency measure for partitions.} -\item{dist}{options:("L1", "L2", "FACTOR") the method of distance calculation; Selects the distance calculation used. \code{dist = "L2"} (default) selects the Euclidean distance and \code{(dist = "L1")} selects the Manhattan distance; \code{(dist = "FACTOR")} uses a frequency.} +\item{dist}{options:(NULL, "NNS", "L1", "L2", "FACTOR") the method of distance calculation; \code{dist = NULL} is the default and selects the native blended NNS distance; \code{dist = "NNS"} is the explicit alias. \code{dist = "L2"} selects Euclidean distance, \code{dist = "L1"} selects Manhattan distance, and \code{dist = "FACTOR"} uses a frequency.} \item{ncores}{integer; value specifying the number of cores to be used in the parallelized procedure. If NULL (default), the number of cores to be used is equal to the number of cores of the machine - 1.} diff --git a/tools/NNS/man/NNS.stack.Rd b/tools/NNS/man/NNS.stack.Rd index 7b4b0970..84387656 100644 --- a/tools/NNS/man/NNS.stack.Rd +++ b/tools/NNS/man/NNS.stack.Rd @@ -12,7 +12,7 @@ NNS.stack( obj.fn = expression(sum((predicted - actual)^2)), objective = "min", optimize.threshold = TRUE, - dist = "L2", + dist = NULL, CV.size = NULL, balance = FALSE, ts.test = NULL, @@ -41,7 +41,7 @@ NNS.stack( \item{optimize.threshold}{logical; \code{TRUE} (default) Will optimize the probability threshold value for rounding in classification problems. If \code{FALSE}, returns 0.5.} -\item{dist}{options:("L1", "L2", "DTW", "FACTOR") the method of distance calculation; Selects the distance calculation used. \code{dist = "L2"} (default) selects the Euclidean distance and \code{(dist = "L1")} selects the Manhattan distance; \code{(dist = "DTW")} selects the dynamic time warping distance; \code{(dist = "FACTOR")} uses a frequency.} +\item{dist}{options:(NULL, "NNS", "L1", "L2", "FACTOR") the method of distance calculation; \code{dist = NULL} is the default and selects the native blended NNS distance; \code{dist = "NNS"} is the explicit alias. \code{dist = "L2"} selects Euclidean distance, \code{dist = "L1"} selects Manhattan distance, and \code{dist = "FACTOR"} uses a frequency.} \item{CV.size}{numeric [0, 1]; \code{NULL} (default) Sets the cross-validation size if \code{(IVs.test = NULL)}. Defaults to a random value between 0.2 and 0.33 for a random sampling of the training set.}