Skip to content

Commit 6572ac5

Browse files
committed
Make nns_m_reg plot the faithful R residual plot
R's NNS.M.reg plot output is its residual plot (Multivariate_Regression.R:367-377): actual y over the observation index as steelblue open circles, fitted y.hat as a red line, with a pink (alpha 0.375) confidence band when present. The previous fitted-vs-actual scatter / residuals-about-zero scatter did not match R. Both plot=True and residual_plot=True now render this single faithful figure. Strengthens the m_reg plot test to assert the steelblue-actual / red-fitted colors.
1 parent 70cd2d3 commit 6572ac5

2 files changed

Lines changed: 41 additions & 27 deletions

File tree

src/nns/multivariate_regression.py

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -116,41 +116,40 @@ def nns_m_reg(
116116
"Fitted.xy": fitted,
117117
}
118118
if plot or residual_plot:
119-
_render_m_reg(fitted, plot=plot, residual_plot=residual_plot)
119+
_render_m_reg(fitted)
120120
return result
121121

122122

123-
def _render_m_reg(
124-
fitted: dict[str, NDArray[np.float64] | NDArray[np.str_]],
125-
*,
126-
plot: bool,
127-
residual_plot: bool,
128-
) -> None:
129-
"""Render multivariate fitted-vs-actual / residual diagnostics on ``plot=True``.
130-
131-
The synthetic predictors make a single x-axis ill-defined, so ``plot`` shows
132-
fitted vs actual (steelblue points, red 1:1 line) and ``residual_plot`` shows
133-
residuals about zero -- a figure is still produced, faithful to R's colors.
123+
def _render_m_reg(fitted: dict[str, NDArray[np.float64] | NDArray[np.str_]]) -> None:
124+
"""Render R's NNS.M.reg residual plot (Multivariate_Regression.R:367-377).
125+
126+
The multivariate plot output is the residual plot: actual ``y`` over the
127+
observation index as ``steelblue`` open circles, fitted ``y.hat`` as a
128+
``red`` line, and a pink (alpha 0.375) confidence band when present.
134129
"""
130+
from nns.plotting import palette
135131
from nns.plotting._mpl import resolve_ax
136132

137133
y = np.asarray(fitted["y"], dtype=np.float64)
138134
y_hat = np.asarray(fitted["y.hat"], dtype=np.float64)
139-
if plot and y.size and y.size == y_hat.size:
140-
ax = resolve_ax(None)
141-
ax.scatter(y, y_hat, color="steelblue")
142-
lo, hi = float(min(y.min(), y_hat.min())), float(max(y.max(), y_hat.max()))
143-
ax.plot([lo, hi], [lo, hi], color="red")
144-
ax.set_xlabel("y")
145-
ax.set_ylabel("y.hat")
146-
ax.set_title("NNS.M.reg Fitted vs Actual")
147-
if residual_plot:
148-
residuals = np.asarray(fitted.get("residuals", []), dtype=np.float64)
149-
if residuals.size:
150-
ax = resolve_ax(None)
151-
ax.scatter(np.arange(1, residuals.size + 1), residuals, color="steelblue")
152-
ax.axhline(0.0, color="red")
153-
ax.set_title("NNS.M.reg Residual Plot")
135+
if y.size == 0 or y.size != y_hat.size:
136+
return
137+
ax = resolve_ax(None)
138+
index = np.arange(1, y.size + 1)
139+
ax.scatter(index, y, facecolors="none", edgecolors="steelblue", marker="o")
140+
ax.plot(index, y_hat, color="red", linewidth=2)
141+
if "conf.int.pos" in fitted and "conf.int.neg" in fitted:
142+
pos = np.asarray(fitted["conf.int.pos"], dtype=np.float64)
143+
neg = np.asarray(fitted["conf.int.neg"], dtype=np.float64)
144+
mask = np.isfinite(pos) & np.isfinite(neg)
145+
if mask.any():
146+
ax.fill_between(
147+
index[mask], neg[mask], pos[mask],
148+
color=palette.PINK, alpha=palette.CI_ALPHA_REG, linewidth=0.0,
149+
)
150+
ax.set_xlabel("Index")
151+
ax.set_ylabel("y (blue) y.hat (red)")
152+
ax.set_title("NNS.M.reg Residual Plot")
154153

155154

156155
def _validate_inputs(

tests/plotting/test_compute_plot_flag.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
matplotlib.use("Agg")
1414

15+
from typing import Any
16+
1517
import matplotlib.pyplot as plt
1618
import numpy as np
1719
import pytest
@@ -88,12 +90,25 @@ def test_nns_seas_plot_true_creates_figure() -> None:
8890

8991

9092
def test_nns_m_reg_plot_true_creates_figure() -> None:
93+
import matplotlib.colors as mcolors
94+
9195
rng = np.random.default_rng(6)
9296
x = np.sort(rng.normal(size=40))
9397
y = 2.0 * x + rng.normal(scale=0.3, size=40)
9498
features = np.column_stack([x, x**2])
9599
nns.nns_m_reg(features, y, plot=True)
96100
assert _fig_count() > 0
101+
ax: Any = plt.gca()
102+
# R's M.reg residual plot: actual y is steelblue, fitted y.hat is a red line.
103+
edge_hexes = {
104+
mcolors.to_hex(row)
105+
for coll in ax.collections
106+
for row in coll.get_edgecolor()
107+
if len(row)
108+
}
109+
line_hexes = {mcolors.to_hex(line.get_color()) for line in ax.get_lines()}
110+
assert "#4682b4" in edge_hexes
111+
assert "#ff0000" in line_hexes
97112

98113

99114
def test_plot_false_creates_no_figure() -> None:

0 commit comments

Comments
 (0)