Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/nns/_public_reg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
14 changes: 9 additions & 5 deletions src/nns/_reg_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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]:
Expand Down
7 changes: 5 additions & 2 deletions src/nns/boost.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/nns/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ def dy_dx(
x_values,
y_values,
plot=False,
dist="L2",
)
fitted = result["Fitted.xy"]
if not isinstance(fitted, dict):
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions src/nns/multivariate_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
16 changes: 8 additions & 8 deletions src/nns/regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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] = ...,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 3 additions & 11 deletions src/nns/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from nns._reg_engine import (
_mreg_predict_path,
_mreg_prepare,
_validate_dist,
_validate_order,
nns_reg_engine,
)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/nns/var.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down
Loading
Loading