diff --git a/README.md b/README.md index 5433a072..5a179925 100644 --- a/README.md +++ b/README.md @@ -144,40 +144,46 @@ Important boundaries: See [behavior conventions](docs/conventions.md) for detailed compatibility notes. -## Examples and notebooks +## Examples -Runnable examples live in [`docs/examples`](docs/examples): +Runnable, self-checking example scripts live in +[`examples/vignettes`](examples/vignettes), mirroring the R NNS vignettes. They +are exercised in CI by `tests/docs/test_vignette_examples.py`, so they stay in +sync with the package. | Topic | Script | |---|---| -| Partial moments | [`partial_moments.py`](docs/examples/partial_moments.py) | -| Dependence | [`dependence.py`](docs/examples/dependence.py) | -| Distributions and ANOVA | [`distributions_anova.py`](docs/examples/distributions_anova.py) | -| Regression | [`regression.py`](docs/examples/regression.py) | -| Classification | [`classification.py`](docs/examples/classification.py) | -| Forecasting | [`forecasting.py`](docs/examples/forecasting.py) | +| Overview | [`overview.py`](examples/vignettes/overview.py) | +| Partial moments | [`partial_moments.py`](examples/vignettes/partial_moments.py) | +| Descriptive and distributional tools | [`descriptive_distributional_tools.py`](examples/vignettes/descriptive_distributional_tools.py) | +| Dependence and nonlinear association | [`dependence_nonlinear_association.py`](examples/vignettes/dependence_nonlinear_association.py) | +| Normalization and rescaling | [`normalization_rescaling.py`](examples/vignettes/normalization_rescaling.py) | +| Hypothesis, ANOVA and stochastic superiority | [`hypothesis_anova_stochastic_superiority.py`](examples/vignettes/hypothesis_anova_stochastic_superiority.py) | +| Regression, boosting, stacking and causality | [`regression_boosting_stacking_causality.py`](examples/vignettes/regression_boosting_stacking_causality.py) | +| Time series forecasting | [`time_series_forecasting.py`](examples/vignettes/time_series_forecasting.py) | +| Simulation, bootstrap and risk-neutral | [`simulation_bootstrap_riskneutral.py`](examples/vignettes/simulation_bootstrap_riskneutral.py) | +| Portfolio and stochastic dominance | [`portfolio_stochastic_dominance.py`](examples/vignettes/portfolio_stochastic_dominance.py) | Run one example: ```bash -uv run python docs/examples/partial_moments.py +uv run python examples/vignettes/partial_moments.py ``` -Run all script examples: +Run all of them with a PASS/FAIL summary: ```bash -for example in docs/examples/*.py; do uv run python "$example"; done +uv run python examples/run_all_vignettes.py ``` -Notebook workflows are also available under [`docs/examples/notebooks`](docs/examples/notebooks). - ## Documentation - [API reference manual](docs/api_reference.md) - [API status and known gaps](docs/api_status.md) - [Behavior conventions and intentional divergences](docs/conventions.md) +- [Parity target, cache regeneration, and automation](docs/parity.md) - [Benchmarks](docs/benchmarks.md) -- [Examples](docs/examples/README.md) +- [Examples](examples/vignettes) ## Development diff --git a/docs/api_reference.md b/docs/api_reference.md index e28123b3..aa6f1126 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -440,5 +440,5 @@ When a public API changes: 1. Update or add the function docstring in `src/nns`. 2. Update implementation status in `docs/api_status.md` if parity or support changed. 3. Run `uv run python scripts/generate_api_reference.py`. -4. Review examples in `docs/examples` if the signature or return shape changed. +4. Review examples in `examples/vignettes` if the signature or return shape changed. 5. Confirm `README.md` still points to the manual and the correct API status page. diff --git a/docs/examples/README.md b/docs/examples/README.md deleted file mode 100644 index a5fa4854..00000000 --- a/docs/examples/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# NNS Python Examples - -These examples are Python-native companions to the upstream R NNS documentation, -not one-for-one copies of the R reports. Each script is runnable, -deterministic, topic-focused, and covered by `tests/invariants/test_examples.py`. - -The upstream R repository contains several kinds of material: - -- `reference/NNS/man/`: function reference pages. -- `reference/NNS/doc/` and `reference/NNS/vignettes/`: CRAN-style tutorials. -- `reference/NNS/book/`: conceptual book chapters. -- `reference/NNS/examples/`: larger applied reports, PDFs, HTML demos, and case - studies. - -Use those upstream files as conceptual references. Use the examples here when -you want short Python call patterns that are kept in sync with NNS Python. - -## Runnable Examples - -| Topic | Script | What it demonstrates | Upstream analogue | -|---|---|---|---| -| Partial moments | [partial_moments.py](partial_moments.py) | `lpm`, `upm`, degree-zero probability split, variance decomposition, `nns_moments` | `NNSvignette_Partial_Moments.Rmd` | -| Dependence | [dependence.py](dependence.py) | `nns_dep`, `nns_cor`, linear vs nonlinear relationships | `NNSvignette_Correlation_and_Dependence.Rmd` | -| Distributions / ANOVA | [distributions_anova.py](distributions_anova.py) | `nns_cdf`, `nns_anova`, certainty output | `NNSvignette_Comparing_Distributions.Rmd` | -| Regression | [regression.py](regression.py) | `nns_reg`, fitted values, point estimates, regression output shape | `NNSvignette_Clustering_and_Regression.Rmd` | -| Classification | [classification.py](classification.py) | `nns_reg(..., type="class")`, numeric class-code predictions | `NNSvignette_Classification.Rmd` | -| Forecasting | [forecasting.py](forecasting.py) | `nns_arma`, `nns_arma_optim`, `nns_var` | `NNSvignette_Forecasting.Rmd` | - -## Notebooks - -| Topic | Notebook | -|---|---| -| Partial-moment risk workflow | [01_partial_moments_risk_workflow.ipynb](notebooks/01_partial_moments_risk_workflow.ipynb) | -| Regression, classification, factors | [02_regression_classification_workflow.ipynb](notebooks/02_regression_classification_workflow.ipynb) | -| Distribution, dominance, simulation | [04_distribution_dominance_simulation_workflow.ipynb](notebooks/04_distribution_dominance_simulation_workflow.ipynb) | -| Boston Housing regression parity example | [05_boston_housing_regression_workflow.ipynb](notebooks/05_boston_housing_regression_workflow.ipynb) | - -Run one example: - -```bash -uv run python docs/examples/partial_moments.py -``` - -Run all examples: - -```bash -for example in docs/examples/*.py; do uv run python "$example"; done -``` - -The main R parity guarantees still live in `tests/parity/`. These examples and -notebooks are usage references, not a replacement for the parity suite. diff --git a/docs/examples/classification.py b/docs/examples/classification.py deleted file mode 100644 index 313d7ae7..00000000 --- a/docs/examples/classification.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from nns import nns_m_reg, nns_reg, nns_stack - - -def main() -> None: - x = np.linspace(-2.0, 2.0, 72, dtype=np.float64) - second_feature = np.cos(2.0 * x) - features = np.column_stack((x, second_feature)) - y = np.where(x < -0.6, 1.0, np.where(x > 0.65, 3.0, 2.0)) - - one_dim_points = np.array([-1.0, 0.0, 1.25], dtype=np.float64) - one_dim = nns_reg( - x, - y, - type="class", - point_est=one_dim_points, - confidence_interval=None, - ) - - two_dim_points = np.array( - [ - [-1.25, np.cos(-2.5)], - [0.1, np.cos(0.2)], - [1.2, np.cos(2.4)], - ], - dtype=np.float64, - ) - multi = nns_m_reg( - features, - y, - type="class", - point_est=two_dim_points, - confidence_interval=None, - ) - - # Stacking uses a simple cross-validation split to choose between candidate methods. - stacked = nns_stack( - features, - y, - two_dim_points, - type="class", - method=(1, 2), - folds=1, - cv_size=0.25, - random_seed=7, - ) - - one_dim_predictions = np.asarray(one_dim["Point.est"], dtype=np.float64) - multi_predictions = np.asarray(multi["Point.est"], dtype=np.float64) - stack_predictions = np.asarray(stacked["stack"], dtype=np.float64) - classes = set(np.unique(y)) - - assert one_dim_predictions.shape == one_dim_points.shape - assert multi_predictions.shape == (two_dim_points.shape[0],) - assert stack_predictions.shape == (two_dim_points.shape[0],) - assert set(one_dim_predictions).issubset(classes) - assert set(multi_predictions).issubset(classes) - assert set(stack_predictions).issubset(classes) - assert 0.0 <= multi["R2"] <= 1.0 - - print("1D points:", one_dim_points) - print("1D class predictions:", one_dim_predictions) - print("2D points:") - print(two_dim_points) - print("multivariate class predictions:", multi_predictions) - print("stacked class predictions:", stack_predictions) - print("training accuracy proxy:", multi["R2"]) - - -if __name__ == "__main__": - main() diff --git a/docs/examples/dependence.py b/docs/examples/dependence.py deleted file mode 100644 index 9c9e2a13..00000000 --- a/docs/examples/dependence.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from nns import causal_matrix, nns_causation, nns_copula, nns_cor, nns_dep - - -def main() -> None: - x = np.linspace(-2.0, 2.0, 101, dtype=np.float64) - linear_y = 2.0 * x - nonlinear_y = x**2 - cyclic_y = np.sin(np.pi * x) - - linear = nns_dep(x, linear_y) - nonlinear = nns_dep(x, nonlinear_y) - cyclic = nns_dep(x, cyclic_y) - copula_value = nns_copula(x, nonlinear_y) - causation = nns_causation(x[:-1], nonlinear_y[1:], tau=1) - causes = causal_matrix(np.column_stack((x, linear_y, nonlinear_y)), tau=0) - - np.testing.assert_allclose(nns_cor(x, linear_y), linear["Correlation"]) - assert linear["Dependence"] > 0.95 - assert nonlinear["Dependence"] > abs(nonlinear["Correlation"]) - assert cyclic["Dependence"] > abs(cyclic["Correlation"]) - assert 0.0 <= copula_value <= 1.0 - assert any(key.startswith("C(") for key in causation) - np.testing.assert_allclose(causes, -causes.T) - - print("linear relationship:", linear) - print("nonlinear relationship:", nonlinear) - print("cyclic relationship:", cyclic) - print("copula dependence:", copula_value) - print("lagged causation summary:", causation) - print("causal matrix:") - print(causes) - - -if __name__ == "__main__": - main() diff --git a/docs/examples/distributions_anova.py b/docs/examples/distributions_anova.py deleted file mode 100644 index 9f60ce5a..00000000 --- a/docs/examples/distributions_anova.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from nns import nns_anova, nns_cdf - - -def main() -> None: - control = np.linspace(-1.0, 1.0, 25, dtype=np.float64) - treatment = control + 0.35 - wider_treatment = 1.2 * control + 0.55 - - cdf = nns_cdf(control, degree=0) - survival = nns_cdf(control, degree=0, type="survival") - cumulative_hazard = nns_cdf(control, degree=0, type="cumulative hazard", target=0.0) - comparison = nns_anova(control, treatment, confidence_interval=None) - robust = nns_anova( - control, - treatment, - robust=True, - n_boot=64, - random_seed=11, - confidence_interval=None, - ) - pairwise = nns_anova( - [control, treatment, wider_treatment], - pairwise=True, - confidence_interval=None, - ) - - function = cdf["Function"] - survival_function = survival["Function"] - assert isinstance(function, dict) - assert isinstance(survival_function, dict) - assert set(function) == {"x", "CDF"} - assert set(survival_function) == {"x", "S(x)"} - assert 0.0 <= comparison["Certainty"] <= 1.0 - assert 0.0 <= robust["Certainty"] <= 1.0 - np.testing.assert_allclose(function["CDF"] + survival_function["S(x)"], 1.0) - np.testing.assert_allclose(pairwise, pairwise.T, equal_nan=True) - np.testing.assert_allclose(np.diag(pairwise), 1.0) - - print("first CDF rows:") - print(np.column_stack((function["x"][:5], function["CDF"][:5]))) - print("cumulative hazard at target 0:", cumulative_hazard["target.value"]) - print("ANOVA certainty:", comparison["Certainty"]) - print("robust ANOVA certainty:", robust["Certainty"]) - print("pairwise certainty matrix:") - print(pairwise) - - -if __name__ == "__main__": - main() diff --git a/docs/examples/forecasting.py b/docs/examples/forecasting.py deleted file mode 100644 index 60c6ed5c..00000000 --- a/docs/examples/forecasting.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from nns import nns_arma, nns_arma_optim, nns_seas, nns_var - - -def main() -> None: - t = np.arange(1, 60, dtype=np.float64) - series = 10.0 + np.sin(t / 3.0) + 0.05 * t - - seasonality = nns_seas(series, modulo=[3, 4, 6], mod_only=True) - arma = nns_arma(series, h=3, seasonal_factor=4, method="lin") - arma_both = nns_arma(series, h=3, seasonal_factor=4, method="both") - optim = nns_arma_optim( - series, - h=3, - seasonal_factor=[3, 4, 5], - lin_only=True, - print_trace=False, - ) - - panel = np.column_stack( - ( - series, - 0.8 * series + np.cos(t / 5.0), - 4.0 + 0.03 * t + np.sin(t / 4.0), - ) - ) - var = nns_var(panel, h=2, tau=[1, 2, 3], dim_red_method="cor", naive_weights=False) - interpolated = nns_var(panel, h=0, tau=2) - - assert seasonality["periods"].ndim == 1 - assert seasonality["best.period"] in set(seasonality["periods"]) - assert arma.shape == (3,) - assert arma_both.shape == (3,) - assert optim["results"].shape == (3,) - assert var["ensemble"].shape == (2, panel.shape[1]) - assert interpolated["interpolated_and_extrapolated"].shape == panel.shape - - print("best seasonal period:", seasonality["best.period"]) - print("candidate seasonal periods:", seasonality["periods"]) - print("ARMA forecast:", arma) - print("ARMA both-method forecast:", arma_both) - print("optimized ARMA forecast:", optim["results"]) - print("VAR ensemble forecast:") - print(var["ensemble"]) - print("interpolated panel head:") - print(interpolated["interpolated_and_extrapolated"][:3]) - - -if __name__ == "__main__": - main() diff --git a/docs/examples/notebooks/01_partial_moments_risk_workflow.ipynb b/docs/examples/notebooks/01_partial_moments_risk_workflow.ipynb deleted file mode 100644 index f4f40d74..00000000 --- a/docs/examples/notebooks/01_partial_moments_risk_workflow.ipynb +++ /dev/null @@ -1,297 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Partial Moments: Risk Workflow\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "from nns import (\n", - " lpm,\n", - " lpm_ratio,\n", - " mean_pm,\n", - " nns_anova,\n", - " nns_cdf,\n", - " nns_dep,\n", - " nns_gravity,\n", - " nns_mode,\n", - " nns_rescale,\n", - " pm_matrix,\n", - " skew_pm,\n", - " upm,\n", - " upm_ratio,\n", - " var_pm,\n", - ")\n", - "\n", - "np.set_printoptions(precision=4, suppress=True)\n", - "rng = np.random.default_rng(42)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Strategy returns\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "strategy ann_mean ann_vol p(loss) LPM2@0 UPM2@0 mean/sqrt(LPM2) skew\n", - "quality 0.043 0.133 0.458 0.000040 0.000031 0.027 -0.669\n", - "barbell -0.253 0.238 0.488 0.000139 0.000086 -0.085 -0.556\n", - "defensive 0.085 0.077 0.442 0.000011 0.000012 0.101 -0.187\n", - "market -0.007 0.134 0.527 0.000032 0.000039 -0.005 0.385\n" - ] - } - ], - "source": [ - "n = 260\n", - "market = rng.normal(0.0004, 0.0090, n)\n", - "quality = 0.0007 + 0.55 * market + rng.normal(0.0, 0.0060, n)\n", - "quality[::41] -= 0.025\n", - "barbell = 0.0007 + 0.35 * market + rng.normal(0.0, 0.0110, n)\n", - "barbell[::31] -= 0.045\n", - "barbell[17::53] += 0.035\n", - "defensive = 0.00045 + 0.25 * market + rng.normal(0.0, 0.0045, n)\n", - "\n", - "returns = np.column_stack((quality, barbell, defensive, market))\n", - "names = (\"quality\", \"barbell\", \"defensive\", \"market\")\n", - "\n", - "def row(name: str, values: np.ndarray) -> tuple[object, ...]:\n", - " target = 0.0\n", - " lower2 = float(lpm(2, target, values))\n", - " upper2 = float(upm(2, target, values))\n", - " sortino_like = float(mean_pm(values) / np.sqrt(lower2)) if lower2 > 0 else np.nan\n", - " return (\n", - " name,\n", - " mean_pm(values) * 252.0,\n", - " np.sqrt(var_pm(values)) * np.sqrt(252.0),\n", - " float(lpm(0, target, values)),\n", - " lower2,\n", - " upper2,\n", - " sortino_like,\n", - " skew_pm(values),\n", - " )\n", - "\n", - "print(\"strategy ann_mean ann_vol p(loss) LPM2@0 UPM2@0 mean/sqrt(LPM2) skew\")\n", - "for item in [row(name, returns[:, i]) for i, name in enumerate(names)]:\n", - " print(f\"{item[0]:<11} {item[1]:>8.3f} {item[2]:>8.3f} {item[3]:>8.3f} {item[4]:>9.6f} {item[5]:>9.6f} {item[6]:>15.3f} {item[7]:>7.3f}\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Variance decomposition\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "name var_pm LPM2(mean)+UPM2(mean) LPM_ratio@0 UPM_ratio@0\n", - "quality 0.0000704 0.0000704 0.563 0.437\n", - "barbell 0.0002246 0.0002246 0.618 0.382\n", - "defensive 0.0000235 0.0000235 0.472 0.528\n", - "market 0.0000709 0.0000709 0.453 0.547\n" - ] - } - ], - "source": [ - "print(\"name var_pm LPM2(mean)+UPM2(mean) LPM_ratio@0 UPM_ratio@0\")\n", - "for i, name in enumerate(names):\n", - " values = returns[:, i]\n", - " center = float(np.mean(values))\n", - " reconstructed = float(lpm(2, center, values) + upm(2, center, values))\n", - " print(\n", - " f\"{name:<10} {var_pm(values):>9.7f} {reconstructed:>20.7f}\"\n", - " f\" {float(lpm_ratio(2, 0.0, values)):>12.3f} {float(upm_ratio(2, 0.0, values)):>12.3f}\"\n", - " )\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Degree-zero probability checks\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "targets: [-0.02 -0.01 0. 0.01]\n", - "quality [0.0269 0.1038 0.4577 0.8808]\n", - "barbell [0.0769 0.2308 0.4885 0.7923]\n", - "defensive [0. 0.0269 0.4423 0.9692]\n", - "\n", - "NNS.CDF degree=1 target value for quality: [0.4867]\n", - "first five CDF rows:\n", - "[[-0.0306 0. ]\n", - " [-0.0271 0.0005]\n", - " [-0.0254 0.001 ]\n", - " [-0.0253 0.0011]\n", - " [-0.0249 0.0014]]\n" - ] - } - ], - "source": [ - "targets = np.array([-0.02, -0.01, 0.0, 0.01], dtype=np.float64)\n", - "print(\"targets:\", targets)\n", - "for i, name in enumerate(names[:3]):\n", - " print(f\"{name:<10}\", np.asarray(lpm(0, targets, returns[:, i])))\n", - "\n", - "cdf = nns_cdf(quality, degree=1, target=0.0)\n", - "print(\"\\nNNS.CDF degree=1 target value for quality:\", cdf[\"target.value\"])\n", - "print(\"first five CDF rows:\")\n", - "fn = cdf[\"Function\"]\n", - "print(np.column_stack((fn[\"x\"][:5], fn[\"CDF\"][:5])))\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Partial-moment covariance\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "normalized covariance-style matrix:\n", - "[[1. 0.1016 0.2289 0.6782]\n", - " [0.1016 1. 0.1491 0.3259]\n", - " [0.2289 0.1491 1. 0.614 ]\n", - " [0.6782 0.3259 0.614 1. ]]\n", - "\n", - "co-lower share matrix (both assets below target together):\n", - "[[0.5633 0.2736 0.2249 0.3532]\n", - " [0.2736 0.6182 0.2535 0.3213]\n", - " [0.2249 0.2535 0.4715 0.3483]\n", - " [0.3532 0.3213 0.3483 0.4532]]\n" - ] - } - ], - "source": [ - "pm = pm_matrix(1, 1, 0.0, returns, pop_adj=True, norm=True)\n", - "print(\"normalized covariance-style matrix:\")\n", - "print(pm[\"cov.matrix\"])\n", - "print(\"\\nco-lower share matrix (both assets below target together):\")\n", - "print(pm[\"clpm\"])\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Nonlinear dependence\n" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Pearson correlation: -0.7089\n", - "NNS correlation/dependence: {'Correlation': 0.0499, 'Dependence': 0.6151}\n" - ] - } - ], - "source": [ - "drawdown_pressure = np.where(market < 0.0, (market * 100.0) ** 2, 0.15 * market) + rng.normal(0.0, 0.05, n)\n", - "dep = nns_dep(market, drawdown_pressure)\n", - "pearson = float(np.corrcoef(market, drawdown_pressure)[0, 1])\n", - "print(\"Pearson correlation:\", round(pearson, 4))\n", - "print(\"NNS correlation/dependence:\", {key: round(value, 4) for key, value in dep.items()})\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Distribution comparison\n" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "quality vs barbell certainty: 0.6059\n", - "lower semivariance: {'quality': 3.97e-05, 'barbell': 0.0001394, 'defensive': 1.11e-05}\n", - "higher-is-better downside score: {'quality': 77.75, 'barbell': 0.0, 'defensive': 100.0}\n", - "quality gravity: 0.001097\n", - "rounded daily return mode: [-0.]\n" - ] - } - ], - "source": [ - "comparison = nns_anova(quality, barbell, confidence_interval=None)\n", - "lower_semis = np.array([float(lpm(2, 0.0, returns[:, i])) for i in range(3)])\n", - "risk_score = 100.0 - nns_rescale(lower_semis, 0.0, 100.0)\n", - "\n", - "print(\"quality vs barbell certainty:\", round(comparison[\"Certainty\"], 4))\n", - "lower_summary = {name: round(float(value), 7) for name, value in zip(names[:3], lower_semis)}\n", - "score_summary = {name: round(float(value), 2) for name, value in zip(names[:3], risk_score)}\n", - "print(\"lower semivariance:\", lower_summary)\n", - "print(\"higher-is-better downside score:\", score_summary)\n", - "print(\"quality gravity:\", round(nns_gravity(quality), 6))\n", - "print(\"rounded daily return mode:\", nns_mode(np.round(quality, 3), discrete=True, multi=True))\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "pygments_lexer": "ipython3", - "version": "3.12.7" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/examples/notebooks/02_regression_classification_workflow.ipynb b/docs/examples/notebooks/02_regression_classification_workflow.ipynb deleted file mode 100644 index 69c00fe6..00000000 --- a/docs/examples/notebooks/02_regression_classification_workflow.ipynb +++ /dev/null @@ -1,388 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Regression and Classification Workflow\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "from nns import (\n", - " dy_d,\n", - " dy_dx,\n", - " nns_boost,\n", - " nns_diff,\n", - " nns_m_reg,\n", - " nns_norm,\n", - " nns_part,\n", - " nns_reg,\n", - " nns_stack,\n", - " prepare_factor_predictors,\n", - ")\n", - "\n", - "np.set_printoptions(precision=4, suppress=True)\n", - "rng = np.random.default_rng(7)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Training data\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "first rows of raw predictors:\n", - "[[62.54072848592086 0.8776913666495335 65.0 'pro']\n", - " [74.84394054545453 0.5233041529751773 50.0 'plus']\n", - " [65.51369392846219 0.9156354351007324 53.0 'pro']\n", - " [73.78175775716163 0.04665223795388718 63.0 'plus']\n", - " [55.379276610098046 0.030288833931601977 48.0 'basic']]\n", - "spend range: 52.89 to 122.2\n" - ] - } - ], - "source": [ - "n = 120\n", - "age = rng.integers(22, 68, size=n).astype(float)\n", - "income = rng.normal(72.0, 14.0, size=n)\n", - "activity = rng.uniform(0.0, 1.0, size=n)\n", - "plan = np.where(activity > 0.68, \"pro\", np.where(income < 67.0, \"basic\", \"plus\"))\n", - "plan_levels = (\"basic\", \"plus\", \"pro\")\n", - "\n", - "spend = (\n", - " 18.0\n", - " + 0.72 * income\n", - " - 0.12 * age\n", - " + 22.0 * np.sin(np.pi * activity)\n", - " + np.where(plan == \"pro\", 24.0, np.where(plan == \"plus\", 10.0, 0.0))\n", - " + rng.normal(0.0, 4.0, size=n)\n", - ")\n", - "\n", - "raw_x = np.empty((n, 4), dtype=object)\n", - "raw_x[:, 0] = income\n", - "raw_x[:, 1] = activity\n", - "raw_x[:, 2] = age\n", - "raw_x[:, 3] = plan\n", - "\n", - "new_customers = np.array(\n", - " [\n", - " [82.0, 0.72, 38.0, \"pro\"],\n", - " [58.0, 0.20, 55.0, \"basic\"],\n", - " [70.0, 0.50, 44.0, \"plus\"],\n", - " ],\n", - " dtype=object,\n", - ")\n", - "\n", - "print(\"first rows of raw predictors:\")\n", - "print(raw_x[:5])\n", - "print(\"spend range:\", round(float(spend.min()), 2), \"to\", round(float(spend.max()), 2))\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Univariate nonlinear regression\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "R2: 0.3934\n", - "activity points: [0.1 0.5 0.9]\n", - "predicted spend: [78.1 98.8 94.02]\n", - "first fitted rows: x, y, y.hat, gradient\n", - "[[ 0.8777 86.6618 93.5385 -29.1402]\n", - " [ 0.5233 98.0459 98.0459 -706.5967]\n", - " [ 0.9156 94.62 95.1022 68.9965]\n", - " [ 0.0467 78.9985 77.4832 -426.5899]\n", - " [ 0.0303 53.9695 63.0654 1234.707 ]]\n" - ] - } - ], - "source": [ - "activity_points = np.array([0.10, 0.50, 0.90], dtype=np.float64)\n", - "one_feature = nns_reg(\n", - " activity,\n", - " spend,\n", - " point_est=activity_points,\n", - " confidence_interval=None,\n", - " noise_reduction=\"median\",\n", - ")\n", - "print(\"R2:\", round(float(one_feature[\"R2\"]), 4))\n", - "print(\"activity points:\", activity_points)\n", - "print(\"predicted spend:\", np.round(one_feature[\"Point.est\"], 2))\n", - "print(\"first fitted rows: x, y, y.hat, gradient\")\n", - "fitted = one_feature[\"Fitted.xy\"]\n", - "print(np.column_stack((fitted[\"x\"][:5], fitted[\"y\"][:5], fitted[\"y.hat\"][:5], fitted[\"gradient\"][:5])))\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Partition map\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "selected order: 3\n", - "first 12 quadrant ids: ['q222' 'q122' 'q222' 'q111' 'q111' 'q111' 'q121' 'q121' 'q112' 'q211'\n", - " 'q111' 'q211']\n", - "regression points: x, y\n", - "[[ 0.1069 77.3254]\n", - " [ 0.3285 95.0548]\n", - " [ 0.6429 93.6009]\n", - " [ 0.8485 96.2964]]\n" - ] - } - ], - "source": [ - "part = nns_part(activity, spend, order=3, obs_req=8, type=\"XONLY\", noise_reduction=\"median\")\n", - "print(\"selected order:\", part[\"order\"])\n", - "print(\"first 12 quadrant ids:\", part[\"dt\"][\"quadrant\"][:12])\n", - "print(\"regression points: x, y\")\n", - "rp = part[\"regression.points\"]\n", - "print(np.column_stack((rp[\"x\"], rp[\"y\"])))\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Factor encoding\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "feature names: ('income', 'activity', 'age', 'plan_basic', 'plan_plus', 'plan_pro')\n", - "design shape: (120, 6)\n", - "new-customer design rows:\n", - "[[82. 0.72 38. 0. 0. 1. ]\n", - " [58. 0.2 55. 1. 0. 0. ]\n", - " [70. 0.5 44. 0. 1. 0. ]]\n" - ] - } - ], - "source": [ - "design = prepare_factor_predictors(\n", - " raw_x,\n", - " point_est=new_customers,\n", - " factor_levels=[None, None, None, plan_levels],\n", - " names=[\"income\", \"activity\", \"age\", \"plan\"],\n", - ")\n", - "print(\"feature names:\", design.feature_names)\n", - "print(\"design shape:\", design.x.shape)\n", - "print(\"new-customer design rows:\")\n", - "print(design.point_est)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Multivariate regression and stacking\n" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "nns_m_reg R2: 0.9833\n", - "nns_m_reg point estimates: [112.94 55.37 90.91]\n", - "stacked point estimates: [100.17 59.37 93.94]\n", - "stack parameters: n_best= 1.0 threshold= 0.37\n" - ] - } - ], - "source": [ - "mreg = nns_m_reg(\n", - " design.x,\n", - " spend,\n", - " point_est=design.point_est,\n", - " n_best=3,\n", - " confidence_interval=None,\n", - ")\n", - "stacked = nns_stack(\n", - " design.x,\n", - " spend,\n", - " design.point_est,\n", - " method=(1, 2),\n", - " folds=2,\n", - " cv_size=0.25,\n", - " pred_int=None,\n", - " random_seed=11,\n", - ")\n", - "print(\"nns_m_reg R2:\", round(float(mreg[\"R2\"]), 4))\n", - "print(\"nns_m_reg point estimates:\", np.round(mreg[\"Point.est\"], 2))\n", - "print(\"stacked point estimates:\", np.round(stacked[\"stack\"], 2))\n", - "print(\"stack parameters: n_best=\", stacked[\"NNS.reg.n.best\"], \"threshold=\", round(float(stacked[\"NNS.dim.red.threshold\"]), 4))\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Classification\n" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "class counts: {1: 55, 2: 51, 3: 14}\n", - "nns_m_reg class accuracy proxy: 1.0\n", - "class_model predictions: [1. 3. 2.]\n", - "class_stack predictions: [1. 2. 1.]\n" - ] - } - ], - "source": [ - "state = np.where((activity < 0.25) & (plan == \"basic\"), 3.0, np.where(spend > 92.0, 1.0, 2.0))\n", - "class_model = nns_m_reg(\n", - " design.x,\n", - " state,\n", - " type=\"class\",\n", - " point_est=design.point_est,\n", - " n_best=1,\n", - " confidence_interval=None,\n", - ")\n", - "class_stack = nns_stack(\n", - " design.x,\n", - " state,\n", - " design.point_est,\n", - " type=\"class\",\n", - " method=(1, 2),\n", - " folds=1,\n", - " cv_size=0.25,\n", - " pred_int=None,\n", - " random_seed=3,\n", - ")\n", - "print(\"class counts:\", {int(label): int(np.sum(state == label)) for label in np.unique(state)})\n", - "print(\"nns_m_reg class accuracy proxy:\", round(float(class_model[\"R2\"]), 4))\n", - "print(\"class_model predictions:\", class_model[\"Point.est\"])\n", - "print(\"class_stack predictions:\", class_stack[\"stack\"])\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Diagnostics\n" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "boost keys: ['feature.frequency', 'feature.weights', 'n.best', 'pred.int', 'results']\n", - "boost predictions: [102.88 79.01 90.66]\n", - "overall dy/dactivity: 2.657\n", - "local partial derivatives: {'First': array([[0.1595, 0.3088, 0.1823],\n", - " [0.1653, 0.8723, 0.1475],\n", - " [0.2073, 1.7184, 0.1591]]), 'Second': array([[-0.0016, 0.217 , 0.0021],\n", - " [ 0.0027, -1.1047, 0.0017],\n", - " [-0.0048, -2.9808, 0.0013]])}\n", - "nns_diff derivative for z^3 + 2z at 1.5: 8.750000003672\n", - "normalized column means: [54.6659 54.6659 54.6659]\n" - ] - } - ], - "source": [ - "small_x = design.x[:, :4]\n", - "small_points = design.point_est[:, :4]\n", - "boost = nns_boost(\n", - " small_x,\n", - " spend,\n", - " small_points,\n", - " learner_trials=8,\n", - " epochs=2,\n", - " random_seed=5,\n", - " pred_int=None,\n", - " feature_importance=True,\n", - ")\n", - "overall_activity_slope = dy_dx(activity, spend, eval_point=\"overall\")\n", - "local_partials = dy_d(\n", - " np.column_stack((income, activity, age)),\n", - " spend,\n", - " wrt=np.array([1, 2, 3]),\n", - " eval_points=np.mean(np.column_stack((income, activity, age)), axis=0),\n", - ")\n", - "derivative = nns_diff(lambda z: z**3 + 2.0 * z, 1.5)\n", - "normalized = nns_norm(np.column_stack((income, activity * 100.0, age)), linear=True)\n", - "\n", - "print(\"boost keys:\", sorted(boost.keys()))\n", - "print(\"boost predictions:\", np.round(boost[\"results\"], 2))\n", - "print(\"overall dy/dactivity:\", round(float(overall_activity_slope), 4))\n", - "print(\"local partial derivatives:\", {key: np.round(value, 4) for key, value in local_partials.items()})\n", - "print(\"nns_diff derivative for z^3 + 2z at 1.5:\", derivative[\"DERIVATIVE\"])\n", - "print(\"normalized column means:\", np.round(np.mean(normalized, axis=0), 4))\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/examples/notebooks/04_distribution_dominance_simulation_workflow.ipynb b/docs/examples/notebooks/04_distribution_dominance_simulation_workflow.ipynb deleted file mode 100644 index 1555c56c..00000000 --- a/docs/examples/notebooks/04_distribution_dominance_simulation_workflow.ipynb +++ /dev/null @@ -1,302 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Distribution, Dominance, and Simulation Workflow\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "from nns import (\n", - " fsd,\n", - " nns_anova,\n", - " nns_cdf,\n", - " nns_mc,\n", - " nns_meboot,\n", - " nns_norm,\n", - " nns_sd_cluster,\n", - " nns_ss,\n", - " sd_efficient_set,\n", - " ssd,\n", - " tsd,\n", - ")\n", - "\n", - "np.set_printoptions(precision=4, suppress=True)\n", - "rng = np.random.default_rng(99)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Return distributions\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "name mean vol min max\n", - "defensive 0.00069 0.00580 -0.01641 0.01615\n", - "balanced 0.00009 0.00914 -0.02656 0.02474\n", - "aggressive -0.00010 0.01596 -0.08513 0.03241\n" - ] - } - ], - "source": [ - "n = 180\n", - "defensive = rng.normal(0.00045, 0.0060, n)\n", - "balanced = rng.normal(0.00065, 0.0080, n)\n", - "aggressive = rng.normal(0.00095, 0.0130, n)\n", - "aggressive[::29] -= 0.045\n", - "balanced[::47] -= 0.020\n", - "returns = np.column_stack((defensive, balanced, aggressive))\n", - "names = (\"defensive\", \"balanced\", \"aggressive\")\n", - "\n", - "print(\"name mean vol min max\")\n", - "for i, name in enumerate(names):\n", - " x = returns[:, i]\n", - " print(f\"{name:<10} {np.mean(x):>8.5f} {np.std(x):>8.5f} {np.min(x):>8.5f} {np.max(x):>8.5f}\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## CDF, survival, and hazard\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "first five aggressive CDF/survival rows:\n", - "[[-0.0851 0.0056 0.9944]\n", - " [-0.0595 0.0111 0.9889]\n", - " [-0.0373 0.0167 0.9833]\n", - " [-0.0349 0.0222 0.9778]\n", - " [-0.0335 0.0278 0.9722]]\n", - "CDF at 0: [0.4778]\n", - "cumulative hazard at 0: [0.6529]\n" - ] - } - ], - "source": [ - "cdf = nns_cdf(aggressive, degree=0, target=0.0)\n", - "survival = nns_cdf(aggressive, degree=0, type=\"survival\")\n", - "hazard = nns_cdf(aggressive, degree=0, type=\"cumulative hazard\", target=0.0)\n", - "\n", - "fn = cdf[\"Function\"]\n", - "sfn = survival[\"Function\"]\n", - "print(\"first five aggressive CDF/survival rows:\")\n", - "print(np.column_stack((fn[\"x\"][:5], fn[\"CDF\"][:5], sfn[\"S(x)\"][:5])))\n", - "print(\"CDF at 0:\", cdf[\"target.value\"])\n", - "print(\"cumulative hazard at 0:\", hazard[\"target.value\"])\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## ANOVA-style comparison\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "pairwise certainty matrix:\n", - "[[1. 0.6674 0.4353]\n", - " [0.6674 1. 0.6606]\n", - " [0.4353 0.6606 1. ]]\n", - "defensive vs aggressive robust certainty: 0.4353\n" - ] - } - ], - "source": [ - "pairwise = nns_anova([defensive, balanced, aggressive], pairwise=True, confidence_interval=None)\n", - "robust = nns_anova(defensive, aggressive, robust=True, n_boot=128, random_seed=101, confidence_interval=None)\n", - "print(\"pairwise certainty matrix:\")\n", - "print(pairwise)\n", - "print(\"defensive vs aggressive robust certainty:\", round(float(robust[\"Certainty\"]), 4))\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Stochastic dominance\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "pairwise dominance result codes: 1 means first dominates, -1 means second dominates, 0 means neither\n", - "defensive vs balanced: FSD=0, SSD=1, TSD=1\n", - "defensive vs aggressive: FSD=0, SSD=1, TSD=1\n", - "balanced vs aggressive: FSD=0, SSD=1, TSD=1\n", - "degree-2 efficient set names: ['defensive']\n", - "degree-2 SD clusters: {'Cluster_1': ['defensive'], 'Cluster_2': ['balanced'], 'Cluster_3': ['aggressive']}\n" - ] - } - ], - "source": [ - "print(\"pairwise dominance result codes: 1 means first dominates, -1 means second dominates, 0 means neither\")\n", - "for i in range(len(names)):\n", - " for j in range(i + 1, len(names)):\n", - " print(\n", - " f\"{names[i]} vs {names[j]}:\"\n", - " f\" FSD={fsd(returns[:, i], returns[:, j])},\"\n", - " f\" SSD={ssd(returns[:, i], returns[:, j])},\"\n", - " f\" TSD={tsd(returns[:, i], returns[:, j])}\"\n", - " )\n", - "\n", - "efficient = sd_efficient_set(returns, degree=2)\n", - "clusters = nns_sd_cluster(returns, degree=2, names=names, min_cluster=1)\n", - "print(\"degree-2 efficient set names:\", [names[index] for index in efficient])\n", - "print(\"degree-2 SD clusters:\", clusters[\"Clusters\"])\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Stochastic superiority\n" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "x vs y stochastic superiority: p_gt, p_tie, p_star\n", - "defensive > balanced: {'p_gt': 0.5175, 'p_tie': 0.0, 'p_star': 0.5175}\n", - "defensive > aggressive: {'p_gt': 0.4899, 'p_tie': 0.0, 'p_star': 0.4899}\n", - "balanced > aggressive: {'p_gt': 0.4806, 'p_tie': 0.0, 'p_star': 0.4806}\n" - ] - } - ], - "source": [ - "print(\"x vs y stochastic superiority: p_gt, p_tie, p_star\")\n", - "for i in range(len(names)):\n", - " for j in range(i + 1, len(names)):\n", - " ss = nns_ss(returns[:, i], returns[:, j])\n", - " print(f\"{names[i]} > {names[j]}:\", {key: round(value, 4) for key, value in ss.items()})\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Bootstrap and Monte Carlo\n" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "meboot replicate matrix shape: (60, 5)\n", - "meboot ensemble head: [-0.0122 -0.0081 -0.0004 -0.0069 -0.0013 -0.0023]\n", - "mc rho labels: ['rho = 0.5', 'rho = 0', 'rho = -0.5']\n", - "mc ensemble head: [-0.0132 -0.0032 -0.0024 -0.0021 -0.001 -0.0044]\n" - ] - } - ], - "source": [ - "meboot = nns_meboot(balanced[:60], reps=5, rho=0.25, random_seed=202)\n", - "mc = nns_mc(balanced[:60], reps=4, lower_rho=-0.5, upper_rho=0.5, by=0.5, random_seed=303)\n", - "print(\"meboot replicate matrix shape:\", meboot[\"replicates\"].shape)\n", - "print(\"meboot ensemble head:\", np.round(meboot[\"ensemble\"][:6], 5))\n", - "print(\"mc rho labels:\", list(mc[\"replicates\"].keys()))\n", - "print(\"mc ensemble head:\", np.round(mc[\"ensemble\"][:6], 5))\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Normalization\n" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "original means: [ 100.079 5000.378 1.9 ]\n", - "normalized means: [1211.59 1690.712 914.769]\n", - "normalized first row: [1210.642 1690.135 922.033]\n" - ] - } - ], - "source": [ - "macro_panel = np.column_stack(\n", - " (\n", - " 100.0 + np.cumsum(returns[:, 0]),\n", - " 5000.0 + 50.0 * np.cumsum(returns[:, 1]),\n", - " 2.0 + np.cumsum(returns[:, 2]),\n", - " )\n", - ")\n", - "normalized = nns_norm(macro_panel, linear=False)\n", - "print(\"original means:\", np.round(np.mean(macro_panel, axis=0), 3))\n", - "print(\"normalized means:\", np.round(np.mean(normalized, axis=0), 3))\n", - "print(\"normalized first row:\", np.round(normalized[0], 3))\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/examples/notebooks/05_boston_housing_regression_workflow.ipynb b/docs/examples/notebooks/05_boston_housing_regression_workflow.ipynb deleted file mode 100644 index 55d2158e..00000000 --- a/docs/examples/notebooks/05_boston_housing_regression_workflow.ipynb +++ /dev/null @@ -1,685 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "2b2b6d9c", - "metadata": {}, - "source": [ - "# Boston Housing Regression Workflow\n" - ] - }, - { - "cell_type": "markdown", - "id": "1ca3062f", - "metadata": {}, - "source": [ - "## Dataset\n", - "Included for upstream NNS example parity. The historical `b` variable has known ethical concerns.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "f8d2942d", - "metadata": {}, - "outputs": [], - "source": [ - "from pathlib import Path\n", - "import csv\n", - "\n", - "import numpy as np\n", - "\n", - "from nns import nns_dep, nns_m_reg, nns_part, nns_reg, nns_stack\n", - "\n", - "np.set_printoptions(precision=4, suppress=True)\n", - "\n", - "DATA_PATH = Path('docs/examples/notebooks/data/boston_housing.csv')\n", - "if not DATA_PATH.exists():\n", - " DATA_PATH = Path('data/boston_housing.csv')\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "833b37ab", - "metadata": {}, - "outputs": [], - "source": [ - "def load_boston_csv(path: Path) -> tuple[list[str], np.ndarray, np.ndarray]:\n", - " with path.open(newline='') as handle:\n", - " reader = csv.DictReader(handle)\n", - " rows = list(reader)\n", - "\n", - " if not rows or reader.fieldnames is None:\n", - " raise ValueError(f'No rows found in {path}')\n", - "\n", - " columns = list(reader.fieldnames)\n", - " if columns[-1] != 'medv':\n", - " raise ValueError('Expected medv to be the final target column')\n", - "\n", - " values = np.array(\n", - " [[float(row[column]) for column in columns] for row in rows],\n", - " dtype=np.float64,\n", - " )\n", - " return columns[:-1], values[:, :-1], values[:, -1]\n", - "\n", - "\n", - "def rmse(predicted: np.ndarray, actual: np.ndarray) -> float:\n", - " predicted = np.asarray(predicted, dtype=np.float64)\n", - " actual = np.asarray(actual, dtype=np.float64)\n", - " return float(np.sqrt(np.mean((predicted - actual) ** 2)))\n", - "\n", - "\n", - "def mae(predicted: np.ndarray, actual: np.ndarray) -> float:\n", - " predicted = np.asarray(predicted, dtype=np.float64)\n", - " actual = np.asarray(actual, dtype=np.float64)\n", - " return float(np.mean(np.abs(predicted - actual)))\n", - "\n", - "\n", - "def fit_linear(x_train: np.ndarray, y_train: np.ndarray, x_test: np.ndarray) -> np.ndarray:\n", - " train_design = np.column_stack((np.ones(x_train.shape[0]), x_train))\n", - " test_design = np.column_stack((np.ones(x_test.shape[0]), x_test))\n", - " coefficients = np.linalg.lstsq(train_design, y_train, rcond=None)[0]\n", - " return test_design @ coefficients\n", - "\n", - "\n", - "def take_columns(x: np.ndarray, names: list[str], wanted: tuple[str, ...]) -> np.ndarray:\n", - " indices = [names.index(name) for name in wanted]\n", - " return x[:, indices]\n", - "\n", - "\n", - "def print_table(headers: tuple[str, ...], rows: list[tuple[object, ...]]) -> None:\n", - " rendered_rows = [[format_value(value) for value in row] for row in rows]\n", - " widths = [len(header) for header in headers]\n", - " for row in rendered_rows:\n", - " widths = [max(width, len(value)) for width, value in zip(widths, row)]\n", - "\n", - " header_line = ' '.join(header.ljust(width) for header, width in zip(headers, widths))\n", - " rule_line = ' '.join('-' * width for width in widths)\n", - " print(header_line)\n", - " print(rule_line)\n", - " for row in rendered_rows:\n", - " print(' '.join(value.ljust(width) for value, width in zip(row, widths)))\n", - "\n", - "\n", - "def format_value(value: object) -> str:\n", - " if isinstance(value, (float, np.floating)):\n", - " return f'{float(value):.4f}'\n", - " if isinstance(value, (int, np.integer)):\n", - " return str(int(value))\n", - " return str(value)\n" - ] - }, - { - "cell_type": "markdown", - "id": "d76a4c7d", - "metadata": {}, - "source": [ - "## Load data\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "d137e67f", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "data path: docs/examples/notebooks/data/boston_housing.csv\n", - "rows: 506\n", - "predictors: 13\n", - "target: medv\n", - "features: crim, zn, indus, chas, nox, rm, age, dis, rad, tax, ptratio, b, lstat\n", - "row lstat rm nox medv \n", - "--- ------ ------ ------ -------\n", - "0 4.9800 6.5750 0.5380 24.0000\n", - "1 9.1400 6.4210 0.4690 21.6000\n", - "2 4.0300 7.1850 0.4690 34.7000\n", - "3 2.9400 6.9980 0.4580 33.4000\n", - "4 5.3300 7.1470 0.4580 36.2000\n" - ] - } - ], - "source": [ - "feature_names, x, y = load_boston_csv(DATA_PATH)\n", - "\n", - "print('data path:', DATA_PATH)\n", - "print('rows:', x.shape[0])\n", - "print('predictors:', x.shape[1])\n", - "print('target:', 'medv')\n", - "print('features:', ', '.join(feature_names))\n", - "\n", - "preview_rows = []\n", - "for row_index in range(5):\n", - " preview_rows.append(\n", - " (\n", - " row_index,\n", - " x[row_index, feature_names.index('lstat')],\n", - " x[row_index, feature_names.index('rm')],\n", - " x[row_index, feature_names.index('nox')],\n", - " y[row_index],\n", - " )\n", - " )\n", - "\n", - "print_table(('row', 'lstat', 'rm', 'nox', 'medv'), preview_rows)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "47bda36a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "column min q25 median q75 max \n", - "------- ------- ------- ------- ------- -------\n", - "medv 5.0000 17.0250 21.2000 25.0000 50.0000\n", - "lstat 1.7300 6.9500 11.3600 16.9550 37.9700\n", - "rm 3.5610 5.8855 6.2085 6.6235 8.7800 \n", - "nox 0.3850 0.4490 0.5380 0.6240 0.8710 \n", - "ptratio 12.6000 17.4000 19.0500 20.2000 22.0000\n" - ] - } - ], - "source": [ - "summary_rows = []\n", - "for name, values in (\n", - " ('medv', y),\n", - " ('lstat', x[:, feature_names.index('lstat')]),\n", - " ('rm', x[:, feature_names.index('rm')]),\n", - " ('nox', x[:, feature_names.index('nox')]),\n", - " ('ptratio', x[:, feature_names.index('ptratio')]),\n", - "):\n", - " summary_rows.append(\n", - " (\n", - " name,\n", - " float(np.min(values)),\n", - " float(np.quantile(values, 0.25)),\n", - " float(np.median(values)),\n", - " float(np.quantile(values, 0.75)),\n", - " float(np.max(values)),\n", - " )\n", - " )\n", - "\n", - "print_table(('column', 'min', 'q25', 'median', 'q75', 'max'), summary_rows)\n" - ] - }, - { - "cell_type": "markdown", - "id": "e762ebc9", - "metadata": {}, - "source": [ - "## Train/test split\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "b400ed5d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "train rows: 356\n", - "test rows: 150\n", - "split mean medv std medv min medv max medv\n", - "----- --------- -------- -------- --------\n", - "train 22.5907 9.3852 5.0000 50.0000 \n", - "test 22.3953 8.7005 5.6000 50.0000 \n" - ] - } - ], - "source": [ - "def stratified_train_test_split(\n", - " target: np.ndarray,\n", - " *,\n", - " train_fraction: float = 0.70,\n", - " bins: int = 10,\n", - " seed: int = 12345,\n", - ") -> tuple[np.ndarray, np.ndarray]:\n", - " rng = np.random.default_rng(seed)\n", - " ordered = np.argsort(target + rng.normal(0.0, 1e-9, size=target.size))\n", - " train_parts: list[np.ndarray] = []\n", - " test_parts: list[np.ndarray] = []\n", - "\n", - " for bin_indices in np.array_split(ordered, bins):\n", - " shuffled = bin_indices.copy()\n", - " rng.shuffle(shuffled)\n", - " cutoff = int(np.ceil(train_fraction * shuffled.size))\n", - " train_parts.append(shuffled[:cutoff])\n", - " test_parts.append(shuffled[cutoff:])\n", - "\n", - " train = np.concatenate(train_parts).astype(np.int64)\n", - " test = np.concatenate(test_parts).astype(np.int64)\n", - " rng.shuffle(train)\n", - " rng.shuffle(test)\n", - " return train, test\n", - "\n", - "\n", - "train_idx, test_idx = stratified_train_test_split(y)\n", - "x_train, x_test = x[train_idx], x[test_idx]\n", - "y_train, y_test = y[train_idx], y[test_idx]\n", - "\n", - "print('train rows:', train_idx.size)\n", - "print('test rows:', test_idx.size)\n", - "print_table(\n", - " ('split', 'mean medv', 'std medv', 'min medv', 'max medv'),\n", - " [\n", - " ('train', float(np.mean(y_train)), float(np.std(y_train)), float(np.min(y_train)), float(np.max(y_train))),\n", - " ('test', float(np.mean(y_test)), float(np.std(y_test)), float(np.min(y_test)), float(np.max(y_test))),\n", - " ],\n", - ")\n" - ] - }, - { - "cell_type": "markdown", - "id": "e36ea323", - "metadata": {}, - "source": [ - "## Dependence scan\n" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "68b4e151", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "feature NNS cor NNS dep Pearson |Pearson|\n", - "------- ------- ------- ------- ---------\n", - "lstat -0.2953 0.4846 -0.7377 0.7377 \n", - "rm 0.2525 0.5128 0.6954 0.6954 \n", - "dis 0.2739 0.5478 0.2499 0.2499 \n", - "b 0.2367 0.5369 0.3335 0.3335 \n", - "nox -0.1276 0.5318 -0.4273 0.4273 \n", - "indus 0.1694 0.5244 -0.4837 0.4837 \n", - "crim -0.0465 0.5213 -0.3883 0.3883 \n", - "ptratio -0.1333 0.4583 -0.5078 0.5078 \n", - "tax 0.0447 0.4887 -0.4685 0.4685 \n", - "rad 0.0925 0.4866 -0.3816 0.3816 \n" - ] - } - ], - "source": [ - "dependence_rows = []\n", - "for column_index, name in enumerate(feature_names):\n", - " dep = nns_dep(x[:, column_index], y)\n", - " pearson = float(np.corrcoef(x[:, column_index], y)[0, 1])\n", - " dependence_rows.append(\n", - " (\n", - " name,\n", - " float(dep['Correlation']),\n", - " float(dep['Dependence']),\n", - " pearson,\n", - " abs(pearson),\n", - " )\n", - " )\n", - "\n", - "ranked = sorted(dependence_rows, key=lambda row: max(row[2], row[4]), reverse=True)\n", - "print_table(('feature', 'NNS cor', 'NNS dep', 'Pearson', '|Pearson|'), ranked[:10])\n" - ] - }, - { - "cell_type": "markdown", - "id": "30437f55", - "metadata": {}, - "source": [ - "## R-example stack path\n" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "bc522c22", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "model RMSE MAE \n", - "---------------------------------- ------ ------\n", - "linear least squares, all features 4.7785 3.3324\n", - "NNS.stack reg, all features 5.7667 4.0573\n", - "NNS.stack dim.red, all features 5.6867 3.8826\n", - "NNS.stack combined, all features 5.6705 3.9355\n", - "selected n.best: 1.0\n", - "selected dim.red threshold: 0.68\n" - ] - } - ], - "source": [ - "full_linear_pred = fit_linear(x_train, y_train, x_test)\n", - "full_stack = nns_stack(\n", - " x_train,\n", - " y_train,\n", - " x_test,\n", - " obj_fn=rmse,\n", - " objective='min',\n", - " folds=3,\n", - " cv_size=0.25,\n", - " method=(1, 2),\n", - " random_seed=12345,\n", - ")\n", - "\n", - "full_rows = [\n", - " ('linear least squares, all features', rmse(full_linear_pred, y_test), mae(full_linear_pred, y_test)),\n", - " ('NNS.stack reg, all features', rmse(full_stack['reg'], y_test), mae(full_stack['reg'], y_test)),\n", - " ('NNS.stack dim.red, all features', rmse(full_stack['dim.red'], y_test), mae(full_stack['dim.red'], y_test)),\n", - " ('NNS.stack combined, all features', rmse(full_stack['stack'], y_test), mae(full_stack['stack'], y_test)),\n", - "]\n", - "\n", - "print_table(('model', 'RMSE', 'MAE'), full_rows)\n", - "print('selected n.best:', full_stack['NNS.reg.n.best'])\n", - "print('selected dim.red threshold:', full_stack['NNS.dim.red.threshold'])\n" - ] - }, - { - "cell_type": "markdown", - "id": "08914dea", - "metadata": {}, - "source": [ - "## Focused multivariate stack\n" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "84c8eee6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "selected features: lstat, rm, ptratio, tax, nox, dis\n", - "model RMSE MAE \n", - "------------------------------------------ ------ ------\n", - "linear least squares, selected features 4.9348 3.4345\n", - "NNS direct multivariate, selected features 3.8150 2.7360\n", - "selected n.best: 1.0\n" - ] - } - ], - "source": [ - "selected_features = ('lstat', 'rm', 'ptratio', 'tax', 'nox', 'dis')\n", - "x_selected = take_columns(x, feature_names, selected_features)\n", - "x_selected_train = x_selected[train_idx]\n", - "x_selected_test = x_selected[test_idx]\n", - "\n", - "selected_linear_pred = fit_linear(x_selected_train, y_train, x_selected_test)\n", - "selected_nns = nns_stack(\n", - " x_selected_train,\n", - " y_train,\n", - " x_selected_test,\n", - " obj_fn=rmse,\n", - " objective='min',\n", - " folds=3,\n", - " cv_size=0.25,\n", - " method=(1,),\n", - " random_seed=12345,\n", - ")\n", - "\n", - "print('selected features:', ', '.join(selected_features))\n", - "print_table(\n", - " ('model', 'RMSE', 'MAE'),\n", - " [\n", - " ('linear least squares, selected features', rmse(selected_linear_pred, y_test), mae(selected_linear_pred, y_test)),\n", - " ('NNS direct multivariate, selected features', rmse(selected_nns['stack'], y_test), mae(selected_nns['stack'], y_test)),\n", - " ],\n", - ")\n", - "print('selected n.best:', selected_nns['NNS.reg.n.best'])\n" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "17882073", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "row actual linear NNS NNS error\n", - "--- ------- ------- ------- ---------\n", - "0 8.8000 14.6498 11.0000 2.2000 \n", - "1 44.8000 39.6457 48.3000 3.5000 \n", - "2 20.5000 19.5585 18.8000 -1.7000 \n", - "3 14.9000 17.0042 16.1000 1.2000 \n", - "4 24.8000 25.7686 22.9000 -1.9000 \n", - "5 35.1000 34.7110 35.4000 0.3000 \n", - "6 13.1000 18.8582 12.5000 -0.6000 \n", - "7 19.9000 16.1999 19.0000 -0.9000 \n", - "8 37.0000 31.8112 30.5000 -6.5000 \n", - "9 18.5000 19.0314 19.5000 1.0000 \n" - ] - } - ], - "source": [ - "comparison_rows = []\n", - "for row_number in range(10):\n", - " comparison_rows.append(\n", - " (\n", - " row_number,\n", - " y_test[row_number],\n", - " selected_linear_pred[row_number],\n", - " selected_nns['stack'][row_number],\n", - " selected_nns['stack'][row_number] - y_test[row_number],\n", - " )\n", - " )\n", - "\n", - "print_table(('row', 'actual', 'linear', 'NNS', 'NNS error'), comparison_rows)\n" - ] - }, - { - "cell_type": "markdown", - "id": "36fdeac1", - "metadata": {}, - "source": [ - "## Direct multivariate regression\n" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "63a4b7c5", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "training fitted R2: 0.9995\n", - "n_best used: 1\n", - "row actual nns_m_reg point estimate\n", - "--- ------- ------------------------\n", - "0 8.8000 11.0000 \n", - "1 44.8000 48.3000 \n", - "2 20.5000 18.8000 \n", - "3 14.9000 16.1000 \n", - "4 24.8000 22.9000 \n" - ] - } - ], - "source": [ - "n_best = int(round(float(selected_nns['NNS.reg.n.best'])))\n", - "direct_model = nns_m_reg(\n", - " x_selected_train,\n", - " y_train,\n", - " point_est=x_selected_test[:5],\n", - " n_best=n_best,\n", - " confidence_interval=None,\n", - ")\n", - "\n", - "direct_rows = []\n", - "for row_number, prediction in enumerate(direct_model['Point.est']):\n", - " direct_rows.append((row_number, y_test[row_number], prediction))\n", - "\n", - "print('training fitted R2:', round(float(direct_model['R2']), 4))\n", - "print('n_best used:', n_best)\n", - "print_table(('row', 'actual', 'nns_m_reg point estimate'), direct_rows)\n" - ] - }, - { - "cell_type": "markdown", - "id": "6f5a222e", - "metadata": {}, - "source": [ - "## Univariate view\n" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "0d9d22a1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "lstat-only R2: 0.6822\n", - "lstat quantile point estimated medv\n", - "-------------------- --------------\n", - "4.6800 33.0533 \n", - "11.3600 21.6140 \n", - "23.0350 13.5108 \n", - "partition order: 3\n", - "partition x partition y\n", - "----------- -----------\n", - "5.0400 31.2000 \n", - "9.2350 22.8500 \n", - "14.0000 19.6000 \n", - "21.2300 13.8000 \n" - ] - } - ], - "source": [ - "lstat = x[:, feature_names.index('lstat')]\n", - "lstat_points = np.quantile(lstat, [0.10, 0.50, 0.90])\n", - "lstat_fit = nns_reg(\n", - " lstat,\n", - " y,\n", - " point_est=lstat_points,\n", - " order=3,\n", - " confidence_interval=None,\n", - " noise_reduction='median',\n", - ")\n", - "lstat_partitions = nns_part(\n", - " lstat,\n", - " y,\n", - " order=3,\n", - " obs_req=20,\n", - " type='XONLY',\n", - " noise_reduction='median',\n", - ")\n", - "\n", - "print('lstat-only R2:', round(float(lstat_fit['R2']), 4))\n", - "print_table(\n", - " ('lstat quantile point', 'estimated medv'),\n", - " [(point, estimate) for point, estimate in zip(lstat_points, lstat_fit['Point.est'])],\n", - ")\n", - "print('partition order:', lstat_partitions['order'])\n", - "rp = lstat_partitions['regression.points']\n", - "print_table(\n", - " ('partition x', 'partition y'),\n", - " [(x_value, y_value) for x_value, y_value in zip(rp['x'][:8], rp['y'][:8])],\n", - ")\n" - ] - }, - { - "cell_type": "markdown", - "id": "679a7e44", - "metadata": {}, - "source": [ - "## Classification path\n" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "e2742a56", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "NNS classification accuracy: 0.88\n", - "test-set majority-class accuracy: 0.7467\n", - "actual class predicted class count\n", - "------------ --------------- -----\n", - "1 1 101 \n", - "1 2 11 \n", - "2 1 7 \n", - "2 2 31 \n", - "selected n.best: 1.0\n" - ] - } - ], - "source": [ - "high_value = np.where(y >= 25.0, 2.0, 1.0)\n", - "high_train = high_value[train_idx]\n", - "high_test = high_value[test_idx]\n", - "\n", - "class_model = nns_stack(\n", - " x_selected_train,\n", - " high_train,\n", - " x_selected_test,\n", - " type='class',\n", - " folds=3,\n", - " cv_size=0.25,\n", - " method=(1,),\n", - " random_seed=12345,\n", - ")\n", - "class_pred = class_model['stack']\n", - "accuracy = float(np.mean(class_pred == high_test))\n", - "majority_accuracy = float(max(np.mean(high_test == 1.0), np.mean(high_test == 2.0)))\n", - "\n", - "counts = []\n", - "for actual in (1.0, 2.0):\n", - " for predicted in (1.0, 2.0):\n", - " counts.append((int(actual), int(predicted), int(np.sum((high_test == actual) & (class_pred == predicted)))))\n", - "\n", - "print('NNS classification accuracy:', round(accuracy, 4))\n", - "print('test-set majority-class accuracy:', round(majority_accuracy, 4))\n", - "print_table(('actual class', 'predicted class', 'count'), counts)\n", - "print('selected n.best:', class_model['NNS.reg.n.best'])\n" - ] - }, - { - "cell_type": "markdown", - "id": "2862f6ea", - "metadata": {}, - "source": [ - "## Summary\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/examples/partial_moments.py b/docs/examples/partial_moments.py deleted file mode 100644 index 9c0b19d2..00000000 --- a/docs/examples/partial_moments.py +++ /dev/null @@ -1,65 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from nns import ( - co_lpm, - co_upm, - d_lpm, - d_upm, - lpm, - lpm_ratio, - nns_moments, - pm_matrix, - upm, - upm_ratio, -) - - -def main() -> None: - x = np.array([-2.0, -1.0, 0.5, 3.0, 4.5], dtype=np.float64) - y = np.array([4.0, 2.5, 1.0, 1.5, 3.0], dtype=np.float64) - target = float(np.mean(x)) - target_y = float(np.mean(y)) - - lower_degree_zero = lpm(0, target, x) - upper_degree_zero = upm(0, target, x) - variance_from_partials = lpm(2, target, x) + upm(2, target, x) - downside_share = lpm_ratio(2, target, x) - upside_share = upm_ratio(2, target, x) - - # Co-partial moments split joint movement into same-side and opposite-side terms. - same_lower = co_lpm(1, x, y, target, target_y) - same_upper = co_upm(1, x, y, target, target_y) - lower_x_upper_y = d_upm(1, 1, x, y, target, target_y) - upper_x_lower_y = d_lpm(1, 1, x, y, target, target_y) - - matrix = pm_matrix( - 1, - 1, - "mean", - np.column_stack((x, y)), - pop_adj=True, - norm=True, - ) - - np.testing.assert_allclose(lower_degree_zero + upper_degree_zero, 1.0) - np.testing.assert_allclose(variance_from_partials, np.var(x, ddof=0)) - np.testing.assert_allclose(downside_share + upside_share, 1.0) - assert set(matrix) == {"cupm", "dupm", "dlpm", "clpm", "cov.matrix"} - assert matrix["cov.matrix"].shape == (2, 2) - - print("target:", target) - print("P(x <= target):", lower_degree_zero) - print("P(x > target):", upper_degree_zero) - print("downside/upside variance shares:", downside_share, upside_share) - print("population variance from partial moments:", variance_from_partials) - print("same-side co-moments:", same_lower, same_upper) - print("opposite-side co-moments:", lower_x_upper_y, upper_x_lower_y) - print("normalized partial-moment covariance matrix:") - print(matrix["cov.matrix"]) - print("NNS moments:", nns_moments(x)) - - -if __name__ == "__main__": - main() diff --git a/docs/examples/regression.py b/docs/examples/regression.py deleted file mode 100644 index f021c598..00000000 --- a/docs/examples/regression.py +++ /dev/null @@ -1,47 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from nns import nns_m_reg, nns_part, nns_reg - - -def main() -> None: - x = np.linspace(-3.0, 3.0, 80, dtype=np.float64) - y = np.sin(x) + 0.2 * x - points = np.array([-1.5, 0.0, 1.5], dtype=np.float64) - - fit = nns_reg(x, y, point_est=points, confidence_interval=None) - partition = nns_part(x, y, order=3, obs_req=6) - - features = np.column_stack((x, x**2)) - multi_points = np.array([[-2.0, 4.0], [0.0, 0.0], [2.0, 4.0]], dtype=np.float64) - multi_fit = nns_m_reg( - features, - y, - point_est=multi_points, - order=3, - n_best=2, - confidence_interval=None, - ) - - fitted = fit["Fitted.xy"] - assert fitted["x"].shape == x.shape - assert fitted["y.hat"].shape == y.shape - assert fit["Point.est"].shape == points.shape - assert 0.0 <= fit["R2"] <= 1.0 - assert partition["dt"]["quadrant"].shape == x.shape - assert multi_fit["Point.est"].shape == (multi_points.shape[0],) - assert 0.0 <= multi_fit["R2"] <= 1.0 - - print("univariate R2:", fit["R2"]) - print("univariate point estimates:") - print(np.column_stack((points, fit["Point.est"]))) - print("partition order:", partition["order"]) - print("first partition labels:", partition["dt"]["quadrant"][:8]) - print("multivariate R2:", multi_fit["R2"]) - print("multivariate point estimates:") - print(np.column_stack((multi_points, multi_fit["Point.est"]))) - - -if __name__ == "__main__": - main() diff --git a/docs/parity.md b/docs/parity.md new file mode 100644 index 00000000..73399d3a --- /dev/null +++ b/docs/parity.md @@ -0,0 +1,166 @@ +# Parity + +## Target and status + +R NNS 13.0 is the release parity target. It is the tensorized architecture +target; the earlier R NNS 12.1 cache has been superseded. NNS-core is v13.0.0 +and remains the native C++ foundation for accelerated partial-moment routines. + +Full package parity is **not** claimed. Parity is bounded by the committed tests +and cache: + +- `tests/_r_cache.json` — cache-only R result fixtures (2,406 keyed entries, + schema version `1`, `nns_version == "13.0"`), +- `tests/parity/` — public behavior parity checks, +- `tests/invariants/` — Python-native contracts and invariants, and +- `tests/fixtures/original_tests_expected.json` — adopted original R tests + (`tests/parity/test_original_*`). + +Any cache miss under `PYNNS_R_CACHE_ONLY=1` is a parity-data gap until the cache +is regenerated with Rscript and installed R NNS 13.0. + +Plot artifact policy is unchanged: plots and `Rplots.pdf` artifacts are not +parity outputs in pytest; returned values are. See +[`plot_parity_policy.md`](plot_parity_policy.md). + +### Known retarget fix + +The univariate `NNS.reg(..., multivariate.call = TRUE)` regression-point path +used internally by nonlinear ARMA now follows R NNS 13.0's central-point +weighting: R appends the central regression point again during endpoint +consolidation, and Python preserves that contribution. The airline nonseasonal +nonlinear ARMA smoke forecast changed from the old Python value +`[125.25, 107.75, 158.75, 213.6667]` to the R NNS 13.0 value +`[128.5, 113.5, 155.5, 213.6667]`. + +## Verifying parity + +```bash +python -m pytest -q tests/invariants +PYNNS_R_CACHE_ONLY=1 python -m pytest -q tests/parity +PYNNS_R_CACHE_ONLY=1 python -m pytest -q tests/parity/test_original_* +ruff check . +mypy +python -m build # packaging check +``` + +## Installing R NNS 13.0 from local source + +The R package source is vendored in this repository, so NNS is installed from +local source, **never from CRAN**: + +- Extracted package directory: `tools/NNS` (`tools/NNS/DESCRIPTION` reports + `Version: 13.0`). +- Vendored tarball: `tools/NNS_13.0.tar.gz`. + +Install with the helper script (prefers `tools/NNS`, falls back to the tarball, +and verifies the loaded version): + +```bash +python scripts/install_local_r_nns.py +``` + +Or run the commands directly: + +```bash +R CMD INSTALL tools/NNS +Rscript -e "suppressPackageStartupMessages(library(NNS)); cat(as.character(packageVersion('NNS')))" +# expected output: 13.0 +``` + +Do not run `install.packages("NNS")`; the parity target is the local `tools/NNS` +source, not the CRAN release. + +## Regenerating the parity cache + +After confirming `packageVersion("NNS") == "13.0"`, regenerate the committed +cache with cache-only/offline toggles unset: + +```bash +unset PYNNS_R_CACHE_ONLY PYNNS_OFFLINE CI +python scripts/regenerate_r_cache.py -- -n 0 tests/parity +``` + +If full regeneration is slow or unstable, regenerate deterministic chunks one +file at a time, for example +`python scripts/regenerate_r_cache.py -- -n 0 tests/parity/test_core.py`, then +continue through the remaining parity files. The committed result must remain a +single valid `tests/_r_cache.json` with `nns_version == "13.0"`, +`schema_version == 1`, and non-empty `entries`; `scripts/regenerate_r_cache.py` +enforces those guardrails after the pytest run. + +Validate the regenerated cache offline: + +```bash +PYNNS_R_CACHE_ONLY=1 python -m pytest -q -n 0 tests/parity +``` + +A `RuntimeError: R cache miss ...` means the cache is incomplete (regenerate the +missing live R entries); an `AssertionError`/numeric mismatch means Python +behavior differs from R NNS 13.0 and the Python implementation must be fixed +without loosening tolerances. + +Where an R toolchain is unavailable (for example sandboxed CI or +proxy-restricted runners that cannot install R), the cache cannot be regenerated +live; rerun the local-source install and `scripts/regenerate_r_cache.py` on a +host with R. + +## Automated parity check (with deferred autofix agent) + +`NNS-python` automates R-behavior fidelity detection. The automated **fixing +agent is deferred**; for now the chain verifies against live R and hands +divergences to a maintainer via a parity-review PR. + +1. **Check** — when upstream `OVVO-Financial/NNS` changes an R API, the + `inspect-r-api-update` workflow plans which Python modules / parity tests are + affected and records a report. +2. **Verify** — the `parity-autofix` workflow installs **live R** at the + recorded R commit and re-runs the mapped parity. If behavior diverged (or + live R could not be verified), it opens a **parity-review PR** carrying the + reports for a maintainer to fix. + +```text +NNS R API change + -> inspect-r-api-update.yml (plan + cache gates + inspection PR) + -> dispatch nns-parity-divergence (via OVVO_SYNC_TOKEN) + -> parity-autofix.yml (live-R verify -> parity-review PR) + -> maintainer applies reviewed fix -> merge +``` + +### Fix rules (applied by the maintainer; later by the agent) + +- Edit **`src/nns/**` only**. Never edit `extern/NNS-core/**`, `tools/NNS/**`, + or `tests/_r_cache.json` to make a check pass. +- Classify the root cause and act accordingly: + - **Python port bug** → fix in `src/nns/**`. + - **R changed behavior** → do not chase a cache value; cache regeneration is a + separate, reviewed step. + - **Native kernel change** → do not edit; native code enters Python only + through accepted `NNS-core` commits. +- Verify against **live R**, not the committed cache. +- **Human-merge only.** The workflow never merges. + +### Tokens + +Everything runs on a single GitHub secret while the agent is deferred: + +| Secret | Kind | Used for | Required? | +| --- | --- | --- | --- | +| `OVVO_SYNC_TOKEN` | GitHub fine-grained PAT — **Contents: R/W**, **Pull requests: R/W** on `NNS-python` | emit the `inspect -> autofix` `repository_dispatch` and open all sync / inspection / parity-review PRs (so they trigger `native-backend-ci`) | yes (workflows fall back to `github.token`, but then PRs won't trigger CI and the auto-chain is skipped) | + +Add it at repo **Settings → Secrets and variables → Actions → New repository +secret**. Without it, `inspect-r-api-update` prints the manual trigger command +and you run `parity-autofix` yourself via **workflow_dispatch** (inputs: +`r_commit`, `r_version`). + +### Enabling the autofix agent later + +When you want the agent to draft fixes automatically, re-add the +`anthropics/claude-code-action` step to `parity-autofix.yml` (gated on a live-R +divergence) with the fix rules above as its prompt, and add an +**`ANTHROPIC_API_KEY`** (`sk-ant-…`) from `console.anthropic.com` (or run +`/install-github-app`). This authenticates the agent to Claude and is **not** +substitutable by a GitHub token. The agent would open a `src/nns/**`-only +parity-correction PR, still human-merged. Bedrock / Vertex are alternatives via +the action's `use_bedrock` / `use_vertex` inputs with OIDC; see the +[cloud providers docs](https://github.com/anthropics/claude-code-action/blob/main/docs/cloud-providers.md). diff --git a/docs/parity_autofix.md b/docs/parity_autofix.md deleted file mode 100644 index 22d199c0..00000000 --- a/docs/parity_autofix.md +++ /dev/null @@ -1,63 +0,0 @@ -# Automated parity check (with deferred autofix agent) - -`NNS-python` automates R-behavior fidelity detection. The automated **fixing -agent is deferred for a later date**; for now the chain verifies against live R -and hands divergences to a maintainer via a parity-review PR (the current -method). - -1. **Check** — when upstream `OVVO-Financial/NNS` changes an R API, the - `inspect-r-api-update` workflow plans which Python modules / parity tests are - affected and records a report. -2. **Verify** — the `parity-autofix` workflow installs **live R** at the - recorded R commit and re-runs the mapped parity. If behavior diverged (or live - R could not be verified), it opens a **parity-review PR** carrying the reports - for a maintainer to fix. - -```text -NNS R API change - -> inspect-r-api-update.yml (plan + cache gates + inspection PR) - -> dispatch nns-parity-divergence (via OVVO_SYNC_TOKEN) - -> parity-autofix.yml (live-R verify -> parity-review PR) - -> maintainer applies reviewed fix -> merge -``` - -## Fix rules (applied by the maintainer; later by the agent) - -* Edit **`src/nns/**` only**. Never edit `extern/NNS-core/**`, `tools/NNS/**`, - or `tests/_r_cache.json` to make a check pass. -* Classify the root cause and act accordingly: - * **Python port bug** → fix in `src/nns/**`. - * **R changed behavior** → do not chase a cache value; cache regeneration is a - separate, reviewed step. - * **Native kernel change** → do not edit; native code enters Python only - through accepted `NNS-core` commits. -* Verify against **live R**, not the committed cache. -* **Human-merge only.** The workflow never merges. - -## Tokens - -Everything runs on a single GitHub secret while the agent is deferred: - -| Secret | Kind | Used for | Required? | -| --- | --- | --- | --- | -| `OVVO_SYNC_TOKEN` | GitHub fine-grained PAT — **Contents: R/W**, **Pull requests: R/W** on `NNS-python` | emit the `inspect -> autofix` `repository_dispatch` and open all sync / inspection / parity-review PRs (so they trigger `native-backend-ci`) | yes (workflows fall back to `github.token`, but then PRs won't trigger CI and the auto-chain is skipped) | - -Add it at repo **Settings → Secrets and variables → Actions → New repository -secret**. Without it, `inspect-r-api-update` prints the manual trigger command -and you run `parity-autofix` yourself via **workflow_dispatch** (inputs: -`r_commit`, `r_version`). - -## Enabling the autofix agent later - -When you want the agent to draft fixes automatically, re-add the -`anthropics/claude-code-action` step to `parity-autofix.yml` (gated on a live-R -divergence) with the fix rules above as its prompt, and add: - -* **`ANTHROPIC_API_KEY`** — an Anthropic key (`sk-ant-…`) from - `console.anthropic.com` (or run `/install-github-app`). This authenticates the - agent to Claude and is **not** substitutable by a GitHub token. - -The agent would open a `src/nns/**`-only parity-correction PR, still -human-merged. Bedrock / Vertex are alternatives via the action's `use_bedrock` / -`use_vertex` inputs with OIDC; see the -[cloud providers docs](https://github.com/anthropics/claude-code-action/blob/main/docs/cloud-providers.md). diff --git a/docs/parity_plan.md b/docs/parity_plan.md deleted file mode 100644 index a7d7e982..00000000 --- a/docs/parity_plan.md +++ /dev/null @@ -1,83 +0,0 @@ -# Parity Plan - -## Target - -Retarget Python parity to R NNS 13.0. R NNS 13.0 is the tensorized architecture target, and R NNS 12.1 cache data is superseded. NNS-core is v13.0.0 and remains the native C++ foundation. - -## Plan - -1. Install R and R dependencies. -2. Install R NNS 13.0 from the vendored package source under `tools/` (never from CRAN). -3. Confirm `packageVersion("NNS") == "13.0"`. -4. Validate the R NNS 13.0 smoke values for partial moments, copula, ARMA, regression points, PM matrix naming, and seeded stack behavior. -5. Regenerate `tests/_r_cache.json` with R NNS 13.0 metadata and values. -6. Run cache-only parity, capture the full failure inventory, and fix Python behavior to R NNS 13.0 without loosening tolerances. -7. Keep full parity claims bounded by tests and cache. -8. Keep plot artifact policy unchanged. - -## Installing R NNS 13.0 from local source - -The vendored R package source is committed in this repository, so NNS is installed -from local source, not CRAN: - -- Extracted package directory: `tools/NNS` (`tools/NNS/DESCRIPTION` reports `Version: 13.0`). -- Vendored tarball: `tools/NNS_13.0.tar.gz`. - -Install with the helper script (prefers `tools/NNS`, falls back to the tarball, and -verifies the loaded version): - -```bash -python scripts/install_local_r_nns.py -``` - -Or run the exact command sequence directly: - -```bash -R CMD INSTALL tools/NNS -Rscript -e "suppressPackageStartupMessages(library(NNS)); cat(as.character(packageVersion('NNS')))" -# expected output: 13.0 -``` - -Do not run `install.packages("NNS")`; the parity target is the local `tools/NNS` -source, not the CRAN release. - -## Regenerating the parity cache - -After confirming `packageVersion("NNS") == "13.0"`, regenerate the committed cache -with cache-only/offline toggles unset: - -```bash -unset PYNNS_R_CACHE_ONLY PYNNS_OFFLINE CI -python scripts/regenerate_r_cache.py -- -n 0 tests/parity -``` - -If full regeneration is slow or unstable, regenerate deterministic chunks one file -at a time, for example `python scripts/regenerate_r_cache.py -- -n 0 tests/parity/test_core.py`, -then continue through the remaining parity files. The committed result must remain a -single valid `tests/_r_cache.json` with `nns_version == "13.0"`, `schema_version == 1`, -and non-empty `entries`. `scripts/regenerate_r_cache.py` enforces those guardrails after -the pytest run. - -Validate the regenerated cache offline: - -```bash -PYNNS_R_CACHE_ONLY=1 python -m pytest -q -n 0 tests/parity -``` - -A `RuntimeError: R cache miss ...` means the cache is incomplete (regenerate the -missing live R entries); an `AssertionError`/numeric mismatch means Python behavior -differs from R NNS 13.0 and the Python implementation must be fixed without loosening -tolerances. - -## Current retarget focus - -The first fixed root cause is the `NNS.reg(..., multivariate.call = TRUE)` regression-point construction used by nonlinear ARMA. Python now preserves R NNS 13.0's duplicate central-point contribution during endpoint consolidation. - -## Environment note - -The committed `tests/_r_cache.json` carries `nns_version == "13.0"` and `schema_version == 1` -with non-empty `entries`, and the full cache-only parity suite passes against it. Where an R -toolchain is unavailable (for example, sandboxed CI or proxy-restricted runners that cannot -install R), the cache cannot be regenerated live; rerun the local-source install and -`scripts/regenerate_r_cache.py` on a host with R when refreshing the cache. Always install NNS -from `tools/NNS` (or `tools/NNS_13.0.tar.gz`), never from CRAN. diff --git a/docs/parity_results.md b/docs/parity_results.md deleted file mode 100644 index 89e5454c..00000000 --- a/docs/parity_results.md +++ /dev/null @@ -1,47 +0,0 @@ -# Parity Results - -## Executive summary - -R NNS 13.0 is now the release parity target for NNS Python because R NNS 13.0 is the tensorized architecture target. The earlier R NNS 12.1 cache has been superseded. NNS-core is v13.0.0 and remains the native C++ foundation for accelerated partial-moment routines; Python parity is still bounded by the committed tests and cache rather than a claim of full package equivalence. - -During this retarget, cache generation was prepared against the vendored R NNS 13.0 source tarball committed under `tools/`. The local environment could not complete apt installation of R because Ubuntu package downloads were blocked by the proxy with HTTP 403 responses, so the committed cache metadata is retargeted to 13.0 but the full R-backed cache refresh must be rerun in an environment where apt/R package installation can complete. - -Plot artifact policy is unchanged: parity tests compare returned values and do not adopt R graphics-device artifacts. See `docs/plot_parity_policy.md`. - -## Expected verification commands - -```bash -python -m pytest -q tests/invariants -PYNNS_R_CACHE_ONLY=1 python -m pytest -q tests/parity -PYNNS_R_CACHE_ONLY=1 python -m pytest -q tests/parity/test_original_* -ruff check . -mypy -python -m build -``` - -`python -m build` is a packaging check. If the local environment lacks build tooling and cannot install dependencies, record that as an environment limitation. - -## R NNS 13.0 retarget notes - -- Target version: R NNS 13.0. -- Superseded target: R NNS 12.1. -- Native foundation: NNS-core v13.0.0. -- Cache file: `tests/_r_cache.json`. -- Cache schema: version `1`. -- Cache entries: 2,406 keyed R result entries. -- Tarball used for retarget setup: vendored R NNS 13.0 source in `tools/`. - -## Fixed behavior in this retarget - -The first R NNS 13.0 root-cause fix is in the univariate `NNS.reg(..., multivariate.call = TRUE)` regression-point path used internally by ARMA. R NNS 13.0 appends the central regression point again when final endpoint points are consolidated. Python now preserves that weighting, which changes the airline nonseasonal nonlinear ARMA smoke forecast from the old Python value `[125.25, 107.75, 158.75, 213.6667]` to the R NNS 13.0 value `[128.5, 113.5, 155.5, 213.6667]`. - -## Coverage boundaries - -Full package parity is not claimed. The current evidence is bounded by: - -- cache-backed tests in `tests/parity/`, -- invariant/API tests in `tests/invariants/`, -- original-test fixture adoption under `tests/parity/test_original_*`, and -- the committed R-cache contents. - -Any cache miss under `PYNNS_R_CACHE_ONLY=1` remains a parity-data gap until the cache is regenerated with Rscript and installed R NNS 13.0. diff --git a/docs/parity_status.md b/docs/parity_status.md deleted file mode 100644 index 7ece1694..00000000 --- a/docs/parity_status.md +++ /dev/null @@ -1,24 +0,0 @@ -# Parity Status - -## Current target - -R NNS 13.0 is the release parity target. R NNS 12.1 cache data has been superseded because R NNS 13.0 is the tensorized architecture target. NNS-core is v13.0.0 and is the native C++ foundation for the Python package. - -## What this status does and does not claim - -The project does not claim full package parity. Parity status is bounded by the committed tests and cache: - -- `tests/_r_cache.json` for cache-only R result fixtures, -- `tests/parity/` for public behavior parity checks, -- `tests/invariants/` for Python-native contracts and invariants, and -- `tests/fixtures/original_tests_expected.json` for adopted original R tests. - -Plot artifact policy remains unchanged: plots and `Rplots.pdf` artifacts are not parity outputs in pytest; returned values are. - -## R NNS 13.0 cache - -The parity cache metadata now records R NNS 13.0. The cache contains 2,406 keyed entries under schema version 1. Cache generation for this retarget used the vendored R NNS 13.0 source tarball during setup, but local R installation was blocked by apt proxy HTTP 403 responses; rerun `python scripts/regenerate_r_cache.py` in an environment with a working R NNS 13.0 installation to refresh every cached value from R. - -## Known retarget fix - -The univariate regression-point construction path now follows R NNS 13.0's central-point weighting when `multivariate_call=True`. This path is used by nonlinear ARMA. The airline nonseasonal nonlinear smoke case now matches the R NNS 13.0 target `[128.5, 113.5, 155.5, 213.6667]` instead of preserving the older Python/R-12.1-incompatible behavior. diff --git a/docs/vignettes/00_overview.md b/docs/vignettes/00_overview.md deleted file mode 100644 index e2d7c05c..00000000 --- a/docs/vignettes/00_overview.md +++ /dev/null @@ -1,60 +0,0 @@ -# 00 — Overview - -NNS (Nonlinear Nonparametric Statistics) builds its entire toolkit on **partial -moments** — the pieces of variance that lie above and below a target. Because -partial moments make no assumption of symmetry, linearity, or a parametric -distribution, the same primitives reconstruct classical statistics (variance, -covariance, the CDF) *and* extend naturally to nonlinear dependence, -regression, forecasting, and stochastic dominance. - -This curriculum follows the R NNS vignettes: - -1. Partial moments — the foundational LPM/UPM primitives. -2. Descriptive and distributional tools. -3. Dependence and nonlinear association. -4. Normalization and rescaling. -5. Hypothesis testing: ANOVA and stochastic superiority. -6. Regression, boosting, stacking, and causality. -7. Time series forecasting. -8. Simulation, bootstrap, and risk-neutral sampling. -9. Portfolios and stochastic dominance. - -## Quick import - -```python -import numpy as np -from nns import ( - lpm, upm, lpm_ratio, - nns_moments, nns_dep, nns_copula, pm_matrix, -) -``` - -## A one-screen tour - -```python -rng = np.random.default_rng(42) - -# Variance is the sum of second-degree partial moments about the mean. -y = rng.normal(size=3000) -mu = float(np.mean(y)) -n = y.size -pm_variance = (lpm(2, mu, y) + upm(2, mu, y)) * (n / (n - 1)) -assert np.isclose(pm_variance, np.var(y, ddof=1)) - -# The empirical CDF is LPM.ratio with degree 0. -assert np.isclose(lpm_ratio(0, 0.0, y), np.mean(y <= 0.0)) - -# Pearson correlation misses y = x**2; partial-moment dependence does not. -x = rng.uniform(-1, 1, size=2000) -yq = x**2 + rng.normal(scale=0.05, size=2000) -print("Pearson:", np.corrcoef(x, yq)[0, 1]) # ~0 -print("Dependence:", nns_dep(x, yq)["Dependence"]) # clearly positive -``` - -Run the full tour: - -```bash -python examples/vignettes/overview.py -``` - -The remaining vignettes unpack each of these ideas in turn. diff --git a/docs/vignettes/01_partial_moments.md b/docs/vignettes/01_partial_moments.md deleted file mode 100644 index f65b767e..00000000 --- a/docs/vignettes/01_partial_moments.md +++ /dev/null @@ -1,74 +0,0 @@ -# 01 — Partial moments - -Partial moments split a distribution at a target `t`. The **lower partial -moment** `lpm(degree, t, x)` accumulates deviations below `t`; the **upper -partial moment** `upm(degree, t, x)` accumulates deviations above it. Their -ratios give probabilities, and their inverses give quantiles. Everything else -in NNS is built from these. - -```python -import numpy as np -from nns import lpm, upm, lpm_ratio, upm_ratio, lpm_var, upm_var - -rng = np.random.default_rng(123) -x = rng.normal(size=100) -mu = float(np.mean(x)) -n = x.size -``` - -## The mean as a partial-moment balance point - -The first-degree upper and lower partial moments about 0 balance at the mean: - -```python -mean_via_pm = upm(1, 0.0, x) - lpm(1, 0.0, x) -assert np.isclose(mean_via_pm, np.mean(x)) -``` - -## Variance decomposition around the mean - -Second-degree partial moments about the mean sum to the **population** -variance; multiply by `n / (n - 1)` for the sample variance: - -```python -population_variance = upm(2, mu, x) + lpm(2, mu, x) -sample_variance = population_variance * (n / (n - 1)) -assert np.isclose(sample_variance, np.var(x, ddof=1)) -``` - -This is the central NNS idea: variance is not a monolithic quantity but the sum -of an upside and a downside piece, each measurable on its own. - -## Empirical CDF via `lpm_ratio(0, t, x)` - -The degree-0 lower partial moment ratio is the proportion of mass at or below -`t` — exactly the empirical CDF. `upm_ratio` is the complementary survival -function: - -```python -for t in (-1.0, 0.0, 1.0): - assert np.isclose(lpm_ratio(0, t, x), np.mean(x <= t)) - assert np.isclose(upm_ratio(0, t, x), 1.0 - lpm_ratio(0, t, x)) -``` - -## Value-at-risk quantiles - -`lpm_var(p, 0, x)` inverts the degree-0 CDF, returning the `p`-quantile; -`upm_var(p, 0, x)` returns the right-tail `(1 - p)` quantile: - -```python -p = np.array([0.05, 0.25, 0.5, 0.75, 0.95]) -left = np.array([lpm_var(q, 0.0, x) for q in p]) -assert np.allclose(left, np.quantile(x, p, method="linear")) - -right = np.array([upm_var(q, 0.0, x) for q in p]) -assert np.allclose(right, np.quantile(x, 1.0 - p, method="linear")) -``` - -For integer degrees 1–4, `lpm_var`/`upm_var` solve the exact partial-moment -ratio inversion (the polynomial root-finder ported from R NNS 13.0 in PR #3), -producing continuous VaR estimates rather than raw order statistics. - -```bash -python examples/vignettes/partial_moments.py -``` diff --git a/docs/vignettes/02_descriptive_distributional_tools.md b/docs/vignettes/02_descriptive_distributional_tools.md deleted file mode 100644 index 504eefc7..00000000 --- a/docs/vignettes/02_descriptive_distributional_tools.md +++ /dev/null @@ -1,64 +0,0 @@ -# 02 — Descriptive and distributional tools - -Partial moments give a full descriptive toolkit: moment summaries, robust -modes, covariance matrices, and quantile tables — all without distributional -assumptions. - -```python -import numpy as np -from nns import nns_moments, nns_mode, pm_matrix, lpm_var, lpm_ratio - -rng = np.random.default_rng(123) -x = rng.normal(size=200) -y = rng.normal(size=200) -``` - -## Moment summaries - -`nns_moments` returns mean, variance, skewness, and kurtosis. The `population` -flag toggles the `n/(n-1)` rescaling: - -```python -nns_moments(x, population=True) # {'mean', 'variance', 'skewness', 'kurtosis'} -nns_moments(x, population=False) # sample variance is larger -``` - -## Modes (continuous and discrete) - -`nns_mode` estimates a continuous mode by default, or returns discrete / -multiple modes: - -```python -nns_mode(x) # continuous estimate -nns_mode(np.array([1, 2, 2, 3, 3, 4, 4, 5], dtype=float), - discrete=True, multi=True) # several modes -``` - -## Covariance reconstruction from a partial moment matrix - -`pm_matrix` returns the four co-partial-moment blocks (`clpm`, `cupm`, `dlpm`, -`dupm`). The covariance matrix is recovered as `clpm + cupm - dlpm - dupm`: - -```python -pm = pm_matrix(1, 1, "mean", np.column_stack((x, y)), True, names=["x", "y"]) -reconstructed = pm["clpm"] + pm["cupm"] - pm["dlpm"] - pm["dupm"] -assert np.allclose(reconstructed, np.cov(x, y)) -``` - -This mirrors the R vignette's covariance-matrix reassembly and shows that the -classical covariance is just a difference of co-partial moments. - -## Quantile table via `lpm_var` - -A quantile table is a sweep of `lpm_var` over percentiles; `lpm_ratio` recovers -the CDF at each threshold as a round-trip check: - -```python -p = np.arange(0.05, 0.96, 0.1) -thresholds = np.array([lpm_var(q, 0.0, x) for q in p]) -recovered = np.array([lpm_ratio(0, t, x) for t in thresholds]) # equals p -``` - -```bash -python examples/vignettes/descriptive_distributional_tools.py -``` diff --git a/docs/vignettes/03_dependence_nonlinear_association.md b/docs/vignettes/03_dependence_nonlinear_association.md deleted file mode 100644 index 79739e1d..00000000 --- a/docs/vignettes/03_dependence_nonlinear_association.md +++ /dev/null @@ -1,64 +0,0 @@ -# 03 — Dependence and nonlinear association - -Pearson correlation measures *linear* co-movement. When a relationship is -nonlinear, correlation can collapse toward zero even though the variables are -perfectly dependent. NNS measures dependence directly from partial moments, so -it sees structure the linear coefficient misses. - -```python -import numpy as np -from nns import nns_dep, nns_copula, pm_matrix -``` - -## Linear baseline - -For `y = 2x`, both correlation and dependence are ~1: - -```python -x = np.arange(0.0, 3.01, 0.01) -lin = nns_dep(x, 2.0 * x) -# lin["Correlation"] ~ 1, lin["Dependence"] ~ 1 -``` - -## Where Pearson collapses - -For `y = sin(x)` over many periods, Pearson correlation is weak while -partial-moment dependence stays high: - -```python -xs = np.arange(0.0, 12.0 * np.pi, np.pi / 100.0) -ys = np.sin(xs) -sine = nns_dep(xs, ys) -# sine["Correlation"] ~ 0.20 (weak), sine["Dependence"] ~ 0.81 (strong) -assert sine["Dependence"] > 3.0 * abs(sine["Correlation"]) -``` - -The partial moment dependence vs Pearson correlation contrast is the whole -point: correlation answers "how linear?", dependence answers "how related?". - -## Asymmetric dependence - -Dependence need not be symmetric — `D(y | x)` can differ from `D(x | y)`: - -```python -asym_xy = nns_dep(xs, ys, asym=True)["Dependence"] -asym_yx = nns_dep(ys, xs, asym=True)["Dependence"] -``` - -## Multivariate dependence and copulas - -`pm_matrix` exposes the co-partial-moment blocks for a frame, and `nns_copula` -summarizes the joint dependence structure (near 0.5 for independent columns): - -```python -rng = np.random.default_rng(123) -frame = np.column_stack((rng.normal(size=1000), - rng.normal(size=1000), - rng.normal(size=1000))) -pm = pm_matrix(1, 1, "mean", frame, True, names=["a", "b", "c"]) -nns_copula(frame, continuous=True) -``` - -```bash -python examples/vignettes/dependence_nonlinear_association.py -``` diff --git a/docs/vignettes/04_normalization_rescaling.md b/docs/vignettes/04_normalization_rescaling.md deleted file mode 100644 index e9ec6df4..00000000 --- a/docs/vignettes/04_normalization_rescaling.md +++ /dev/null @@ -1,68 +0,0 @@ -# 04 — Normalization and rescaling - -NNS provides two complementary transforms: `nns_norm` aligns variables onto a -common scale (linearly or nonlinearly), and `nns_rescale` maps a vector onto an -explicit interval or a risk-neutral target. - -```python -import numpy as np -from nns import nns_norm, nns_rescale -``` - -## `nns_norm` — linear and nonlinear - -Given columns with wildly different means and spreads, linear normalization -brings them onto a shared mean scale: - -```python -rng = np.random.default_rng(123) -X = np.column_stack(( - rng.normal(0, 1, 100), - rng.normal(0, 5, 100), - rng.normal(10, 1, 100), - rng.normal(10, 10, 100), -)) - -linear = nns_norm(X, linear=True) # columns share a common mean -nonlinear = nns_norm(X, linear=False) # partial-moment normalization -``` - -After linear normalization every column has the same mean — the precondition -for comparing series on one axis. - -## `nns_rescale` — min-max - -Map a vector onto an explicit `[a, b]` interval: - -```python -raw = np.array([-2.5, 0.2, 1.1, 3.7, 5.0]) -scaled = nns_rescale(raw, a=5.0, b=10.0, method="minmax") -# scaled.min() == 5.0, scaled.max() == 10.0 -``` - -## `nns_rescale` — risk-neutral - -The `"riskneutral"` method rescales a price path so its mean matches a -risk-neutral target. With `type="Terminal"` the rescaled mean equals the -forward `S0 * exp(r * T)`; with `type="Discounted"` it equals `S0`: - -```python -s0, r, t = 100.0, 0.03, 1.0 -prices = s0 * np.exp(np.cumsum(rng.normal(0.0005, 0.02, 250))) - -terminal = nns_rescale(prices, a=s0, b=r, method="riskneutral", - time_to_maturity=t, type="Terminal") -assert np.isclose(terminal.mean(), s0 * np.exp(r * t)) - -discounted = nns_rescale(prices, a=s0, b=r, method="riskneutral", - time_to_maturity=t, type="Discounted") -assert np.isclose(discounted.mean(), s0) -``` - -> Note: the R vignette also illustrates these transforms with overlaid -> histograms. Plotting is optional and omitted from the docs tests; the -> numeric invariants above are what matter for parity. - -```bash -python examples/vignettes/normalization_rescaling.py -``` diff --git a/docs/vignettes/05_hypothesis_anova_stochastic_superiority.md b/docs/vignettes/05_hypothesis_anova_stochastic_superiority.md deleted file mode 100644 index ab66c638..00000000 --- a/docs/vignettes/05_hypothesis_anova_stochastic_superiority.md +++ /dev/null @@ -1,68 +0,0 @@ -# 05 — Hypothesis testing: ANOVA and stochastic superiority - -NNS reframes hypothesis testing around **certainty** and **stochastic -superiority** rather than p-values. `nns_anova` reports a certainty that groups -share a distribution; `nns_ss` reports the probability that one sample exceeds -another, ties included. - -```python -import numpy as np -from nns import nns_anova, nns_ss - -rng = np.random.default_rng(123) -``` - -## `nns_anova` certainty - -Certainty is high when groups share a center and low when they are shifted -apart: - -```python -x = rng.normal(0, 1, 1000) -y_equal = rng.normal(0, 2, 1000) # same mean, different spread -y_shifted = rng.normal(1, 1, 1000) # shifted mean - -nns_anova(x, y_equal, means_only=True)["Certainty"] # higher -nns_anova(x, y_shifted, means_only=True)["Certainty"] # lower -``` - -Interpretation: certainty near 1 means the partial-moment evidence cannot -distinguish the groups; certainty near 0 means it clearly can. - -## `nns_ss` stochastic superiority - -`nns_ss(x, y)` returns `p_gt` (the probability a `y` draw exceeds an `x` draw), -`p_tie` (the tie mass), and `p_star` (the tie-adjusted superiority): - -```python -ss = nns_ss(x, y_shifted) -ss["p_gt"], ss["p_tie"], ss["p_star"] -``` - -### Stochastic superiority with ties - -On discrete data, ties carry real probability mass, so `p_tie` is nonzero: - -```python -xd = rng.integers(1, 6, 100).astype(float) -yd = rng.integers(1, 6, 100).astype(float) -nns_ss(xd, yd)["p_tie"] # > 0 -``` - -### Confidence intervals are stochastic - -Requesting `confidence_interval=True` runs a bootstrap. **Test these by range, -not by exact value** — the `lower`/`upper` bounds are sampled and will vary run -to run: - -```python -ss_ci = nns_ss(x, y_shifted, confidence_interval=True, reps=199, ci=0.95, random_seed=1) -assert 0.0 <= ss_ci["lower"] <= ss_ci["upper"] <= 1.0 -``` - -The same caution applies to the `nns_anova` robust interval and its -`Effect_Size_LB`/`Effect_Size_UB` fields. - -```bash -python examples/vignettes/hypothesis_anova_stochastic_superiority.py -``` diff --git a/docs/vignettes/06_regression_boosting_stacking_causality.md b/docs/vignettes/06_regression_boosting_stacking_causality.md deleted file mode 100644 index 80725577..00000000 --- a/docs/vignettes/06_regression_boosting_stacking_causality.md +++ /dev/null @@ -1,82 +0,0 @@ -# 06 — Regression, boosting, stacking, and causality - -NNS regression partitions the predictor space by dependence and fits locally, -so it captures nonlinear structure without a model formula. The same base -learner powers boosting and stacking, and the dependence machinery gives a -directional causality measure. - -```python -import numpy as np -from nns import nns_reg, nns_boost, nns_stack, nns_causation -``` - -## `nns_reg` — nonlinear regression - -```python -x = np.arange(-5.0, 5.05, 0.05) -y = x**3 -reg = nns_reg(x, y, point_est=np.array([-2.0, 0.0, 2.0])) -reg["R2"], reg["Point.est"] -``` - -`nns_reg` returns the fit quality (`R2`), the regression points, fitted values -(`Fitted.xy`), and point estimates (`Point.est`). - -## Deterministic numeric stack and boost - -The following numeric design is deterministic and was verified against live R -NNS 13.0 (PR #3). It is a good regression-test fixture because the outputs are -exactly reproducible. - -```python -xb = np.linspace(-2.0, 2.0, 30) -variable = np.column_stack((xb, np.sin(xb), np.cos(xb))) -target = xb + np.sin(xb) + 0.25 * np.cos(xb) -point = variable[:5] -``` - -`nns_stack` returns the base regression (`reg`), the dimension-reduction -ensemble (`dim.red`), and the stacked ensemble (`stack`): - -```python -stack = nns_stack(variable, target, point, method=(1, 2), cv_size=0.25, folds=1) -stack["reg"] -# [-3.013334 -2.821165 -2.821165 -2.410226 -2.410226] -stack["dim.red"] -# [-3.013334 -2.914306 -2.781248 -2.589941 -2.429359] -stack["stack"] -# [-3.013334 -2.913733 -2.781494 -2.588834 -2.429242] -``` - -`nns_boost` returns `results`, `feature.weights`, and `feature.frequency` -(R NNS 13.0 no longer returns `n.best`): - -```python -boost = nns_boost(variable, target, point, - learner_trials=10, cv_size=0.25, depth=None, - feature_importance=False) -boost["results"] # [-3.013334 -2.821165 -2.821165 -2.410226 -2.410226] -boost["feature.weights"] # [0.6666667 0.3333333] -boost["feature.frequency"] # [2. 1.] -``` - -A classification example is only included when it is deterministic and stable; -the balanced-`type="CLASS"` Iris boost from the R vignette is RNG-driven and is -therefore left out of the docs tests (see PR #3's documented stochastic gap). - -## `nns_causation` — directional causality - -Causation is directional. `nns_causation` returns the conditional causation in -each direction plus a net-direction summary whose key (`C(x--->y)` or -`C(y--->x)`) names whichever direction dominates: - -```python -caus = nns_causation(driver, response) -caus["Causation.x.given.y"], caus["Causation.y.given.x"] -net_key = next(k for k in caus if k.startswith("C(") and "--->" in k) -caus[net_key] -``` - -```bash -python examples/vignettes/regression_boosting_stacking_causality.py -``` diff --git a/docs/vignettes/07_time_series_forecasting.md b/docs/vignettes/07_time_series_forecasting.md deleted file mode 100644 index fec7442c..00000000 --- a/docs/vignettes/07_time_series_forecasting.md +++ /dev/null @@ -1,77 +0,0 @@ -# 07 — Time series forecasting - -NNS forecasting detects seasonality nonparametrically, then projects component -series forward with linear or nonlinear partial-moment regression. The same -machinery extends to multivariate forecasting through `nns_var`. - -```python -import numpy as np -from nns import nns_seas, nns_arma, nns_arma_optim, nns_var -``` - -## `nns_arma` — deterministic forecasts - -Using the AirPassengers-style 24-point series, the nonseasonal nonlinear and -the seasonal linear forecasts are fully deterministic and match live R NNS 13.0 -(PR #3): - -```python -series = np.array( - [112, 118, 132, 129, 121, 135, 148, 148, 136, 119, 104, 118, - 115, 126, 141, 135, 125, 149, 170, 170, 158, 133, 114, 140], - dtype=float, -) - -nns_arma(series, h=4, seasonal_factor=False, method="nonlin") -# [128.5, 113.5, 155.5, 213.6667] - -nns_arma(series, h=6, seasonal_factor=12, method="lin") -# [118., 134., 150., 141., 129., 163.] -``` - -## `nns_seas` — seasonality detection - -`nns_seas` returns the full period table (`all.periods`), the single -`best.period`, and the selected `periods`: - -```python -z = np.sin(np.arange(1, 121) / 8.0) -seas = nns_seas(z, plot=False) -seas["periods"] -``` - -## `nns_arma_optim` — validated forecasting - -`nns_arma_optim` searches candidate seasonal factors and methods, returning the -selected configuration and prediction bands. This deterministic run matches the -structure verified in PR #3: - -```python -optim = nns_arma_optim(z, h=12, seasonal_factor=[10, 20, 30], - plot=False, print_trace=False) -optim["periods"] # selected seasonal factor(s) -optim["obj.fn"] # objective value at the optimum -optim["method"] # 'lin' | 'nonlin' | 'both' -optim["results"] # length-h forecast -optim["lower.pred.int"] # lower band (<= results) -optim["upper.pred.int"] # upper band (>= results) -``` - -The `results` vector has length `h`, and `lower.pred.int <= upper.pred.int` -element-wise. - -## `nns_var` — multivariate forecasting - -`nns_var` forecasts a panel of series jointly, returning per-series univariate -and ensemble forecasts shaped `(h, n_series)`: - -```python -t = np.arange(1, 61) -panel = np.column_stack((np.sin(t / 6.0), np.cos(t / 5.0), np.sin(t / 4.0) + 0.5)) -var = nns_var(panel, h=4, tau=3, ncores=1, status=False) -var["ensemble"].shape # (4, 3) -``` - -```bash -python examples/vignettes/time_series_forecasting.py -``` diff --git a/docs/vignettes/08_simulation_bootstrap_riskneutral.md b/docs/vignettes/08_simulation_bootstrap_riskneutral.md deleted file mode 100644 index b3b5f573..00000000 --- a/docs/vignettes/08_simulation_bootstrap_riskneutral.md +++ /dev/null @@ -1,49 +0,0 @@ -# 08 — Simulation, bootstrap, and risk-neutral sampling - -NNS resampling preserves the dependence structure of the original data. The -maximum-entropy bootstrap `nns_meboot` generates replicates that retain the -series' shape and rank ordering, and `nns_mc` draws Monte Carlo paths targeting -a chosen rank correlation with the original series. - -```python -import numpy as np -from nns import nns_meboot, nns_mc - -rng = np.random.default_rng(123) -x = np.cumsum(rng.normal(scale=0.7, size=80)) -``` - -## `nns_meboot` — maximum-entropy bootstrap - -```python -mb = nns_meboot(x, reps=10, rho=0.95, random_seed=1) -mb["ensemble"] # ensemble series aligned to the original length -mb["replicates"] # the individual bootstrap replicates -``` - -`rho` controls how tightly each replicate tracks the original ordering. - -## `nns_mc` — dependence-preserving Monte Carlo - -`nns_mc` sweeps a grid of target rank correlations and returns replicates keyed -by their `rho`, plus an averaged `ensemble`: - -```python -mc = nns_mc(x, reps=1, lower_rho=-1.0, upper_rho=1.0, by=0.5, random_seed=1) -list(mc["replicates"].keys()) # ['rho = 1', 'rho = 0.5', 'rho = 0', 'rho = -0.5', 'rho = -1'] -mc["ensemble"] -``` - -Higher target `rho` produces replicates more positively rank-correlated with -`x`; negative `rho` inverts the ordering. - -## Stochastic output caveat - -Both routines are **stochastic**. Seed the RNG for reproducibility within a -run, but validate outputs by **structure and rank** (lengths, key sets, -correlation sign), never by exact resampled values. The example script asserts -only on structure for exactly this reason. - -```bash -python examples/vignettes/simulation_bootstrap_riskneutral.py -``` diff --git a/docs/vignettes/09_portfolio_stochastic_dominance.md b/docs/vignettes/09_portfolio_stochastic_dominance.md deleted file mode 100644 index 48ffb737..00000000 --- a/docs/vignettes/09_portfolio_stochastic_dominance.md +++ /dev/null @@ -1,63 +0,0 @@ -# 09 — Portfolios and stochastic dominance - -Stochastic dominance ranks distributions without assuming a utility function. -NNS provides fast univariate dominance tests and portfolio-level routines that -build efficient sets and dominance-based clusters from a return panel. - -```python -import numpy as np -from nns import fsd_uni, ssd_uni, tsd_uni, sd_efficient_set, nns_sd_cluster - -rng = np.random.default_rng(123) -``` - -## Pairwise dominance tests - -`fsd_uni`, `ssd_uni`, and `tsd_uni` return `1` when the first argument -dominates the second at first, second, or third order, and `0` otherwise. A -constant upward shift is a textbook first-order dominance, and first-order -dominance implies the higher orders: - -```python -x = rng.normal(size=1000) -y = x + 1.0 # y dominates x by a constant shift - -fsd_uni(y, x) # 1 -fsd_uni(x, y) # 0 -ssd_uni(y, x) # 1 (FSD implies SSD) -tsd_uni(y, x) # 1 (FSD implies TSD) -``` - -## A small portfolio return example - -```python -ra = rng.normal(0.005, 0.03, 240) -rb = rng.normal(0.003, 0.02, 240) -rc = rng.normal(0.006, 0.04, 240) -returns = np.column_stack((ra, rb, rc)) -``` - -### Efficient set - -`sd_efficient_set` returns the indices of assets not dominated at the chosen -degree — the dominance-efficient frontier: - -```python -sd_efficient_set(returns, degree=1) # e.g. [2, 0, 1] -``` - -### Dominance clustering - -`nns_sd_cluster` groups assets by their dominance relationships: - -```python -clusters = nns_sd_cluster(returns, degree=1, names=["A", "B", "C"]) -clusters["Clusters"] -``` - -These portfolio tools let you screen and group assets purely on distributional -dominance, with no mean-variance or utility assumptions. - -```bash -python examples/vignettes/portfolio_stochastic_dominance.py -``` diff --git a/docs/vignettes/README.md b/docs/vignettes/README.md deleted file mode 100644 index cbcc04a8..00000000 --- a/docs/vignettes/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# NNS Python vignettes - -Python translations of the R NNS vignette curriculum, written against the -public `nns` API. Each topic has a Markdown explainer here and a matching -runnable script under [`examples/vignettes/`](../../examples/vignettes). - -These examples are Python translations of the R NNS vignette curriculum -(the vendored sources under `tools/NNS/vignettes` and `tools/NNS/inst/doc`). -The code assumes the fresh R NNS 13.0 parity fixes from PR #3 (or the latest -`main` after PR #3 merged). - -## Contents - -| Vignette | Topic | -| --- | --- | -| [00 Overview](00_overview.md) | What NNS is and a one-screen tour of every pillar. | -| [01 Partial moments](01_partial_moments.md) | LPM/UPM, variance and CDF reconstruction, value-at-risk. | -| [02 Descriptive & distributional tools](02_descriptive_distributional_tools.md) | Moments, modes, covariance from partial moment matrices, quantile tables. | -| [03 Dependence & nonlinear association](03_dependence_nonlinear_association.md) | Partial-moment dependence vs Pearson correlation, copulas. | -| [04 Normalization & rescaling](04_normalization_rescaling.md) | `nns_norm` and `nns_rescale` (min-max and risk-neutral). | -| [05 Hypothesis: ANOVA & stochastic superiority](05_hypothesis_anova_stochastic_superiority.md) | `nns_anova` certainty and `nns_ss` superiority probabilities. | -| [06 Regression, boosting, stacking, causality](06_regression_boosting_stacking_causality.md) | `nns_reg`, `nns_boost`, `nns_stack`, `nns_causation`. | -| [07 Time series forecasting](07_time_series_forecasting.md) | `nns_seas`, `nns_arma`, `nns_arma_optim`, `nns_var`. | -| [08 Simulation, bootstrap, risk-neutral](08_simulation_bootstrap_riskneutral.md) | `nns_meboot` and `nns_mc`. | -| [09 Portfolios & stochastic dominance](09_portfolio_stochastic_dominance.md) | `fsd_uni`/`ssd_uni`/`tsd_uni`, `sd_efficient_set`, `nns_sd_cluster`. | - -## Running the examples - -Every script is self-contained and deterministic where possible (seeded RNG, -small data, no plotting in the default path): - -```bash -python examples/vignettes/partial_moments.py -``` - -The whole set is exercised by `tests/docs/test_vignette_examples.py`, which -runs each script and fails on a nonzero exit code: - -```bash -python -m pytest -q tests/docs/test_vignette_examples.py -``` - -## A note on stochastic outputs - -Bootstrap and Monte Carlo routines (`nns_meboot`, `nns_mc`, the `nns_ss` -confidence interval, the `nns_anova` robust interval) produce sampled outputs. -The vignettes and their tests validate these by structure and range, never by -exact value. Deterministic routines (partial moments, dependence, regression -points, the numeric ARMA/stack/boost designs shown here) are compared exactly. diff --git a/examples/run_all_vignettes.py b/examples/run_all_vignettes.py index 15760446..1599c616 100644 --- a/examples/run_all_vignettes.py +++ b/examples/run_all_vignettes.py @@ -4,8 +4,7 @@ ``examples/vignettes/``. Open it in IDLE and press **F5** (or run ``python examples/run_all_vignettes.py`` from a terminal) to execute all vignettes in the documented order and print each one's output, so you can -compare it against the R NNS vignettes PDF and the markdown in -``docs/vignettes/``. +compare it against the R NNS vignettes PDF. Each vignette also self-checks with assertions, so this driver reports PASS/FAIL per vignette and a final summary, and exits non-zero if any vignette fails. @@ -24,29 +23,27 @@ REPO_ROOT = Path(__file__).resolve().parents[1] VIGNETTE_DIR = REPO_ROOT / "examples" / "vignettes" -# Ordered to match docs/vignettes/NN_*.md and the R NNS vignettes PDF. -# (number, title, example-script stem, docs/vignettes markdown file) +# Ordered to match the R NNS vignettes PDF. +# (number, title, example-script stem) VIGNETTES = [ - ("00", "Overview", "overview", "00_overview.md"), - ("01", "Partial moments", "partial_moments", "01_partial_moments.md"), + ("00", "Overview", "overview"), + ("01", "Partial moments", "partial_moments"), ("02", "Descriptive & distributional tools", - "descriptive_distributional_tools", "02_descriptive_distributional_tools.md"), + "descriptive_distributional_tools"), ("03", "Dependence & nonlinear association", - "dependence_nonlinear_association", "03_dependence_nonlinear_association.md"), + "dependence_nonlinear_association"), ("04", "Normalization & rescaling", - "normalization_rescaling", "04_normalization_rescaling.md"), + "normalization_rescaling"), ("05", "Hypothesis, ANOVA & stochastic superiority", - "hypothesis_anova_stochastic_superiority", - "05_hypothesis_anova_stochastic_superiority.md"), + "hypothesis_anova_stochastic_superiority"), ("06", "Regression, boosting, stacking & causality", - "regression_boosting_stacking_causality", - "06_regression_boosting_stacking_causality.md"), + "regression_boosting_stacking_causality"), ("07", "Time series forecasting", - "time_series_forecasting", "07_time_series_forecasting.md"), + "time_series_forecasting"), ("08", "Simulation, bootstrap & risk-neutral", - "simulation_bootstrap_riskneutral", "08_simulation_bootstrap_riskneutral.md"), + "simulation_bootstrap_riskneutral"), ("09", "Portfolio & stochastic dominance", - "portfolio_stochastic_dominance", "09_portfolio_stochastic_dominance.md"), + "portfolio_stochastic_dominance"), ] @@ -66,13 +63,12 @@ def run() -> int: os.chdir(REPO_ROOT) results = [] - for number, title, stem, doc in VIGNETTES: + for number, title, stem in VIGNETTES: banner = f" Vignette {number}: {title} " rule = "=" * max(len(banner), 60) print("\n" + rule) print(banner) print(f" script: examples/vignettes/{stem}.py") - print(f" doc: docs/vignettes/{doc}") print(rule) start = time.perf_counter() diff --git a/scripts/generate_api_reference.py b/scripts/generate_api_reference.py index 5edd518c..e48fc5c1 100644 --- a/scripts/generate_api_reference.py +++ b/scripts/generate_api_reference.py @@ -192,7 +192,7 @@ def render() -> str: 1. Update or add the function docstring in `src/nns`. 2. Update implementation status in `docs/api_status.md` if parity or support changed. 3. Run `uv run python scripts/generate_api_reference.py`. - 4. Review examples in `docs/examples` if the signature or return shape changed. + 4. Review examples in `examples/vignettes` if the signature or return shape changed. 5. Confirm `README.md` still points to this manual and the API status page. """).strip()) lines.append("") diff --git a/docs/examples/notebooks/data/boston_housing.csv b/tests/data/boston_housing.csv similarity index 100% rename from docs/examples/notebooks/data/boston_housing.csv rename to tests/data/boston_housing.csv diff --git a/tests/parity/test_practical_examples.py b/tests/parity/test_practical_examples.py index 25ba9df4..85153dd8 100644 --- a/tests/parity/test_practical_examples.py +++ b/tests/parity/test_practical_examples.py @@ -26,7 +26,7 @@ ) ROOT = Path(__file__).resolve().parents[2] -BOSTON_CSV = ROOT / "docs" / "examples" / "notebooks" / "data" / "boston_housing.csv" +BOSTON_CSV = ROOT / "tests" / "data" / "boston_housing.csv" _IRIS_CLASS_LEVELS = ["setosa", "versicolor", "virginica"]