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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ Rplots.pdf

# MkDocs build output
/site/

# Generated vignette figures
/examples/vignettes/output/
191 changes: 191 additions & 0 deletions examples/_vignette_support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
"""Shared, non-vignette support for the canonical Python ports.

The numbered files in ``examples/vignettes`` are intentionally readable as
Jupytext-style instructional scripts. This module keeps repetitive concerns
(dataset loading, output folders, compact result rendering, and figure saving)
out of the statistical narrative.
"""
from __future__ import annotations

import csv
import os
from collections.abc import Iterable, Sequence
from pathlib import Path
from pprint import pformat
from typing import Any

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np

EXAMPLES_DIR = Path(__file__).resolve().parent
DATA_DIR = EXAMPLES_DIR / "data"
OUTPUT_ROOT = EXAMPLES_DIR / "vignettes" / "output"


def fast_mode() -> bool:
"""Return whether CI requested reduced repetitions for expensive sections."""
return os.environ.get("NNS_VIGNETTE_FAST", "0") == "1"


def output_dir(script_file: str) -> Path:
path = OUTPUT_ROOT / Path(script_file).stem
path.mkdir(parents=True, exist_ok=True)
return path


def section(title: str) -> None:
line = "=" * len(title)
print(f"\n{title}\n{line}")


def subsection(title: str) -> None:
print(f"\n--- {title} ---")


def gap(message: str) -> None:
"""Make a genuine R/Python API difference impossible to overlook."""
print(f"\nPYTHON API GAP: {message}")


def note(message: str) -> None:
print(f"\nNOTE: {message}")


def show(label: str, value: Any, *, precision: int = 6) -> None:
"""Display important numerical structures without dumping huge arrays."""
print(f"\n{label}:")
if isinstance(value, np.ndarray):
print(np.array2string(value, precision=precision, threshold=40, edgeitems=5))
return
if isinstance(value, dict):
compact = {key: _compact(item) for key, item in value.items()}
print(pformat(compact, width=100, sort_dicts=False))
return
print(value)


def _compact(value: Any) -> Any:
if isinstance(value, np.ndarray):
numeric = np.issubdtype(value.dtype, np.number)
if value.size <= 20:
return np.round(value, 6).tolist() if numeric else value.tolist()
flat = value.reshape(-1)
head, tail = flat[:5], flat[-5:]
if numeric:
head, tail = np.round(head, 6), np.round(tail, 6)
return {
"shape": list(value.shape),
"head": head.tolist(),
"tail": tail.tolist(),
}
if isinstance(value, dict):
return {key: _compact(item) for key, item in value.items()}
if isinstance(value, (np.floating, np.integer)):
return value.item()
return value


def table(headers: Sequence[str], rows: Iterable[Sequence[Any]], *, precision: int = 5) -> None:
rendered = []
for row in rows:
rendered.append(
[
f"{item:.{precision}f}" if isinstance(item, (float, np.floating)) else str(item)
for item in row
]
)
widths = [len(str(header)) for header in headers]
for row in rendered:
widths = [max(width, len(cell)) for width, cell in zip(widths, row, strict=True)]
print(
" ".join(str(header).ljust(width) for header, width in zip(headers, widths, strict=True))
)
print(" ".join("-" * width for width in widths))
for row in rendered:
print(" ".join(cell.ljust(width) for cell, width in zip(row, widths, strict=True)))


def save_figure(fig: plt.Figure, directory: Path, filename: str) -> Path:
path = directory / filename
fig.tight_layout()
fig.savefig(path, dpi=160, bbox_inches="tight")
plt.close(fig)
print(f"Saved figure: {path}")
return path


def load_iris() -> tuple[np.ndarray, np.ndarray, list[str]]:
rows = _read_csv("iris.csv")
feature_names = ["Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width"]
x = np.asarray([[float(row[name]) for name in feature_names] for row in rows], dtype=float)
levels = ["setosa", "versicolor", "virginica"]
y = np.asarray([levels.index(row["Species"]) + 1 for row in rows], dtype=float)
return x, y, levels


def load_mtcars() -> dict[str, np.ndarray]:
rows = _read_csv("mtcars.csv")
numeric = ["mpg", "cyl", "disp", "hp", "drat", "wt", "qsec", "vs", "am", "gear", "carb"]
return {name: np.asarray([float(row[name]) for row in rows], dtype=float) for name in numeric}


def load_air_passengers() -> np.ndarray:
rows = _read_csv("AirPassengers.csv")
return np.asarray([float(row["value"]) for row in rows], dtype=float)


def _read_csv(filename: str) -> list[dict[str, str]]:
with (DATA_DIR / filename).open(newline="", encoding="utf-8") as handle:
return list(csv.DictReader(handle))


def empirical_cdf(values: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
ordered = np.sort(np.asarray(values, dtype=float))
probs = np.arange(1, ordered.size + 1, dtype=float) / ordered.size
return ordered, probs


def partition_scatter(ax: plt.Axes, result: dict[str, Any], title: str) -> None:
dt = result["dt"]
labels = np.asarray(dt["quadrant"], dtype=str)
unique, codes = np.unique(labels, return_inverse=True)
ax.scatter(dt["x"], dt["y"], c=codes, cmap="tab20", s=14, alpha=0.7)
rp = result["regression.points"]
ax.scatter(rp["x"], rp["y"], marker="x", s=55, linewidths=1.6)
ax.set_title(f"{title}\n{len(unique)} terminal paths; order={result['order']}")
ax.set_xlabel("x")
ax.set_ylabel("y")


def regression_scatter(
ax: plt.Axes,
x: np.ndarray,
y: np.ndarray,
result: dict[str, Any],
title: str,
) -> None:
fitted = result.get("Fitted.xy")
ax.scatter(x, y, s=12, alpha=0.45, label="observed")
if isinstance(fitted, dict):
order = np.argsort(np.asarray(fitted["x"], dtype=float))
ax.plot(np.asarray(fitted["x"])[order], np.asarray(fitted["y.hat"])[order], label="NNS fit")
if "conf.int.neg" in fitted and "conf.int.pos" in fitted:
ax.fill_between(
np.asarray(fitted["x"])[order],
np.asarray(fitted["conf.int.neg"])[order],
np.asarray(fitted["conf.int.pos"])[order],
alpha=0.2,
label="confidence interval",
)
rp = result.get("regression.points")
if isinstance(rp, dict):
ax.scatter(rp["x"], rp["y"], marker="x", s=45, label="regression points")
ax.set_title(title)
ax.legend()


def figure_grid(nrows: int, ncols: int, *, width: float = 5.0, height: float = 4.0):
return plt.subplots(nrows, ncols, figsize=(width * ncols, height * nrows), squeeze=False)
145 changes: 145 additions & 0 deletions examples/data/AirPassengers.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
rownames,time,value
1,1949,112
2,1949.08333333333,118
3,1949.16666666667,132
4,1949.25,129
5,1949.33333333333,121
6,1949.41666666667,135
7,1949.5,148
8,1949.58333333333,148
9,1949.66666666667,136
10,1949.75,119
11,1949.83333333333,104
12,1949.91666666667,118
13,1950,115
14,1950.08333333333,126
15,1950.16666666667,141
16,1950.25,135
17,1950.33333333333,125
18,1950.41666666667,149
19,1950.5,170
20,1950.58333333333,170
21,1950.66666666667,158
22,1950.75,133
23,1950.83333333333,114
24,1950.91666666667,140
25,1951,145
26,1951.08333333333,150
27,1951.16666666667,178
28,1951.25,163
29,1951.33333333333,172
30,1951.41666666667,178
31,1951.5,199
32,1951.58333333333,199
33,1951.66666666667,184
34,1951.75,162
35,1951.83333333333,146
36,1951.91666666667,166
37,1952,171
38,1952.08333333333,180
39,1952.16666666667,193
40,1952.25,181
41,1952.33333333333,183
42,1952.41666666667,218
43,1952.5,230
44,1952.58333333333,242
45,1952.66666666667,209
46,1952.75,191
47,1952.83333333333,172
48,1952.91666666667,194
49,1953,196
50,1953.08333333333,196
51,1953.16666666667,236
52,1953.25,235
53,1953.33333333333,229
54,1953.41666666667,243
55,1953.5,264
56,1953.58333333333,272
57,1953.66666666667,237
58,1953.75,211
59,1953.83333333333,180
60,1953.91666666667,201
61,1954,204
62,1954.08333333333,188
63,1954.16666666667,235
64,1954.25,227
65,1954.33333333333,234
66,1954.41666666667,264
67,1954.5,302
68,1954.58333333333,293
69,1954.66666666667,259
70,1954.75,229
71,1954.83333333333,203
72,1954.91666666667,229
73,1955,242
74,1955.08333333334,233
75,1955.16666666667,267
76,1955.25,269
77,1955.33333333334,270
78,1955.41666666667,315
79,1955.5,364
80,1955.58333333334,347
81,1955.66666666667,312
82,1955.75,274
83,1955.83333333334,237
84,1955.91666666667,278
85,1956,284
86,1956.08333333334,277
87,1956.16666666667,317
88,1956.25,313
89,1956.33333333334,318
90,1956.41666666667,374
91,1956.5,413
92,1956.58333333334,405
93,1956.66666666667,355
94,1956.75,306
95,1956.83333333334,271
96,1956.91666666667,306
97,1957,315
98,1957.08333333334,301
99,1957.16666666667,356
100,1957.25,348
101,1957.33333333334,355
102,1957.41666666667,422
103,1957.5,465
104,1957.58333333334,467
105,1957.66666666667,404
106,1957.75,347
107,1957.83333333334,305
108,1957.91666666667,336
109,1958,340
110,1958.08333333334,318
111,1958.16666666667,362
112,1958.25,348
113,1958.33333333334,363
114,1958.41666666667,435
115,1958.5,491
116,1958.58333333334,505
117,1958.66666666667,404
118,1958.75,359
119,1958.83333333334,310
120,1958.91666666667,337
121,1959,360
122,1959.08333333334,342
123,1959.16666666667,406
124,1959.25,396
125,1959.33333333334,420
126,1959.41666666667,472
127,1959.5,548
128,1959.58333333334,559
129,1959.66666666667,463
130,1959.75,407
131,1959.83333333334,362
132,1959.91666666667,405
133,1960,417
134,1960.08333333334,391
135,1960.16666666667,419
136,1960.25,461
137,1960.33333333334,472
138,1960.41666666667,535
139,1960.5,622
140,1960.58333333334,606
141,1960.66666666667,508
142,1960.75,461
143,1960.83333333334,390
144,1960.91666666667,432
Loading
Loading