From 4d37d0e4a7936d5ef0584a6c7fde99ce1dbb587e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 07:20:08 +0000 Subject: [PATCH 1/6] Fix docstring inconsistencies in find_elbow_point, rho, psi From c5c2a6871e15f76bc1b76140b7673047e82bef97 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 07:20:51 +0000 Subject: [PATCH 2/6] Document both return paths of find_elbow_point in NumPy style The signature is int | float and the body can return either -1 (when every value in x or y is missing) or np.nan (when the consensus intersection is degenerate because every fitted pair of lines was effectively parallel). The free-form docstring previously documented only the -1 case. Rewrite to NumPy style with Parameters and Returns sections that cover both paths, the length validation, and the >10-finite-values minimum that ValueError enforces. --- src/process_improve/bivariate/_elbow_peak.py | 35 ++++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/process_improve/bivariate/_elbow_peak.py b/src/process_improve/bivariate/_elbow_peak.py index 28347f61..924b9149 100644 --- a/src/process_improve/bivariate/_elbow_peak.py +++ b/src/process_improve/bivariate/_elbow_peak.py @@ -15,9 +15,6 @@ def find_elbow_point(x: np.ndarray, y: np.ndarray, max_iter: int = 41) -> int | """ Find the elbow point when plotting numeric entries in `x` vs numeric values in list `y`. - Return the index into the vectors `x` and `y` [the vectors must have the same length], where - the elbow point occurs. Returns -1 if every value in `x` or `y` is missing. - Using a robust linear fit, sorts the samples in X (independent variable) and takes the first 5 samples from the left, and the last 5 from the right, then fits two linear regressions and computes the intersection of the two @@ -32,6 +29,38 @@ def find_elbow_point(x: np.ndarray, y: np.ndarray, max_iter: int = 41) -> int | Will probably not work well on few data points. If so, try fitting a spline to the raw data and then repeat with the interpolated data. + Parameters + ---------- + x : np.ndarray + Independent-variable values. Must have the same length as `y`. NaN + entries are dropped pairwise with `y` before fitting. After dropping + missing values, more than 10 finite values must remain. + y : np.ndarray + Dependent-variable values. Must have the same length as `x`. NaN + entries are dropped pairwise with `x` before fitting. + max_iter : int, optional + Number of evenly spaced window sizes to sweep when accumulating + intersection points. Default is 41. + + Returns + ------- + int or float + The integer index into the (sorted, NaN-stripped) `x` / `y` vectors of + the data point closest to the consensus elbow location. Two non-integer + sentinel return values are possible: + + * ``-1`` is returned when every value in `x` or `y` is missing (the + median of `x` or `y` is NaN), so no elbow can be estimated. + * ``np.nan`` is returned when the accumulated intersection points are + themselves degenerate (every fitted pair of lines was effectively + parallel, so the median intersection in `x` or `y` is NaN). + + Raises + ------ + ValueError + If `x` and `y` have different lengths, or if fewer than 11 finite + values remain after NaN entries are dropped. + """ start = 5 # assert divmod(max_iter, 2)[1] # must be odd number; to ensure we calculate the median later From eeb838526164ea89b8a8e6bb4d3dcf786e91e3b3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 07:21:04 +0000 Subject: [PATCH 3/6] Add NumPy-style Parameters and Returns sections to rho The bi-weight rho function had a free-form docstring with just the paper reference. Add Parameters (documenting x and the k=2.52 tuning constant) and Returns sections, and move the paper link into a proper References section. --- .../monitoring/control_charts.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/process_improve/monitoring/control_charts.py b/src/process_improve/monitoring/control_charts.py index 168ae317..d2b2ddba 100644 --- a/src/process_improve/monitoring/control_charts.py +++ b/src/process_improve/monitoring/control_charts.py @@ -16,8 +16,24 @@ def rho(x: float, k: float = 2.52) -> float: """ Bi-weight rho function. - Fixed constant of k=2.52 is from p 289 of the paper + Parameters + ---------- + x : float + Value at which to evaluate the bi-weight rho function. + k : float, optional + Bi-weight tuning constant. The default of 2.52 is from p 289 of the + referenced paper. + + Returns + ------- + float + The value of the bi-weight rho function evaluated at `x` with tuning + constant `k`. For ``|x| > k`` the function saturates at `k`. + + References + ---------- https://onlinelibrary.wiley.com/doi/abs/10.1002/for.1125 + """ return k if np.abs(x) > k else k * (1 - np.power(1 - np.power(x / k, 2), 3)) From 4bc10c803c17a1887255b30f7c8ce83f66ba58de Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 07:21:19 +0000 Subject: [PATCH 4/6] Add NumPy-style Parameters and Returns sections to psi The Huber-style psi function had a free-form docstring with just the paper reference. Add Parameters (documenting x and the k=2.0 tuning constant) and Returns sections, and move the paper link into a proper References section. --- .../monitoring/control_charts.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/process_improve/monitoring/control_charts.py b/src/process_improve/monitoring/control_charts.py index d2b2ddba..6574c5aa 100644 --- a/src/process_improve/monitoring/control_charts.py +++ b/src/process_improve/monitoring/control_charts.py @@ -43,8 +43,25 @@ def psi(x: float, k: float = 2.0) -> float: Pre-clean based on the Huber y-function. Can be interpreted as replacing unexpected high or low values by a more likely value. - From p 288 of the paper + + Parameters + ---------- + x : float + Value to clean. + k : float, optional + Huber tuning constant: the threshold beyond which `x` is clipped to + ``k * sign(x)``. The default of 2.0 follows p 288 of the referenced + paper. + + Returns + ------- + float + ``x`` itself when ``|x| < k``; otherwise ``k * sign(x)``. + + References + ---------- https://onlinelibrary.wiley.com/doi/abs/10.1002/for.1125 + """ return x if abs(x) < k else k * np.sign(x) From 59f2f9d6b715e2b6b08c291b46eab35bb1280a91 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 07:22:06 +0000 Subject: [PATCH 5/6] Apply ruff format to touched files Picks up pre-existing wrap inconsistencies in lines that fit within the 120-char limit. No behaviour change. --- src/process_improve/bivariate/_elbow_peak.py | 9 ++------- src/process_improve/monitoring/control_charts.py | 13 +++++++------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/process_improve/bivariate/_elbow_peak.py b/src/process_improve/bivariate/_elbow_peak.py index 924b9149..c99eb302 100644 --- a/src/process_improve/bivariate/_elbow_peak.py +++ b/src/process_improve/bivariate/_elbow_peak.py @@ -76,9 +76,7 @@ def calculate_line_length(x1: float, y1: float, x2: np.ndarray, y2: np.ndarray) x = np.array(x.copy()) y = np.array(y.copy()) if len(x) != len(y): - raise ValueError( - f"x and y must have the same length; got len(x)={len(x)}, len(y)={len(y)}." - ) + raise ValueError(f"x and y must have the same length; got len(x)={len(x)}, len(y)={len(y)}.") # Stop if everything is missing: if np.isnan(np.nanmedian(x)) or np.isnan(np.nanmedian(y)): @@ -87,9 +85,7 @@ def calculate_line_length(x1: float, y1: float, x2: np.ndarray, y2: np.ndarray) # Eliminate missing values in x and y simultaneously. x, y = x[~(np.isnan(x) | np.isnan(y))], y[~(np.isnan(x) | np.isnan(y))] if len(x) <= 10: - raise ValueError( - "Requires more than 10 values in the vectors (not including missing data)." - ) + raise ValueError("Requires more than 10 values in the vectors (not including missing data).") idx_sort = x.argsort() x = x[idx_sort] y = y[idx_sort] @@ -147,7 +143,6 @@ def find_line_intersection(m1: float, b1: float, m2: float, b2: float) -> tuple: return x, y - # ENG-23 (#305): explicit ``__all__`` so the thin re-exporter ``methods.py`` # can do ``from ._elbow_peak import *`` without triggering CodeQL's # py/polluting-import warning. diff --git a/src/process_improve/monitoring/control_charts.py b/src/process_improve/monitoring/control_charts.py index 6574c5aa..9d877c74 100644 --- a/src/process_improve/monitoring/control_charts.py +++ b/src/process_improve/monitoring/control_charts.py @@ -118,7 +118,11 @@ def __init__(self, style: str = "robust", variant: str = "HW") -> None: self.df = pd.DataFrame(columns=columns, dtype=np.float64) def calculate_limits( # noqa: C901 - branch count is mostly simple input-validation guard clauses - self, y: np.ndarray | pd.Series, target: float | None = None, s: float | None = None, **kwargs, + self, + y: np.ndarray | pd.Series, + target: float | None = None, + s: float | None = None, + **kwargs, ) -> None: """ Find for a given vector `y`, the control chart target and limits. @@ -149,8 +153,7 @@ def calculate_limits( # noqa: C901 - branch count is mostly simple input-valid self.s = float(s) if not 0.0 < s < 1e300: raise ValueError( - "The given standard deviation must be positive and not excessively large " - f"(0 < s < 1e300); got {s}." + f"The given standard deviation must be positive and not excessively large (0 < s < 1e300); got {s}." ) if target is not None: @@ -348,9 +351,7 @@ def _holt_winters_warmup_fit(self, ld_1: float = 0.5, ld_2: float = 0.8, ld_s: f warm_up_residuals = y_warm_up - self.warm_up["alpha_0"] - self.warm_up["beta_0"] # Some other method that does not rely on SciPy for 1 function. - self.warm_up["sigma_0"] = median_absolute_deviation( - np.asarray(warm_up_residuals), nan_policy="omit" - ) + self.warm_up["sigma_0"] = median_absolute_deviation(np.asarray(warm_up_residuals), nan_policy="omit") self.warm_up["residuals"] = warm_up_residuals # A constant (zero-variance) warm-up window gives sigma_0 = MAD = 0, which From 0ba8ddd928110d5a45108a4f8f46333b9e0f2b69 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 07:22:38 +0000 Subject: [PATCH 6/6] Bump to 1.40.2 for docstring fixes Patch bump covering the NumPy-style docstring corrections to find_elbow_point, rho, and psi. No behaviour change. CITATION.cff version + date-released and CHANGELOG.md updated in the same commit. --- CHANGELOG.md | 17 ++++++++++++++++- CITATION.cff | 4 ++-- pyproject.toml | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 355ea7f6..05124888 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,20 @@ those changes. ## [Unreleased] +## [1.40.2] - 2026-06-12 + +### Fixed + +- Docstring inconsistencies in three numerical helpers (no behaviour + change): `bivariate.find_elbow_point` now documents both of its + non-integer return sentinels (`-1` when every value in `x` or `y` is + missing, `np.nan` when the accumulated intersection points are + degenerate) in a proper NumPy-style `Returns` section, alongside a + new `Parameters` and `Raises` section; `monitoring.control_charts.rho` + and `monitoring.control_charts.psi` gain NumPy-style `Parameters`, + `Returns`, and `References` sections documenting `x`, the tuning + constant `k`, and the return value (the paper reference is preserved). + ## [1.40.1] - 2026-06-11 ### Fixed @@ -1997,7 +2011,8 @@ this entry records them together. - Reworked the README with a sharper value proposition and a "Why not scikit-learn?" comparison table. -[Unreleased]: https://github.com/kgdunn/process-improve/compare/v1.40.1...HEAD +[Unreleased]: https://github.com/kgdunn/process-improve/compare/v1.40.2...HEAD +[1.40.2]: https://github.com/kgdunn/process-improve/compare/v1.40.1...v1.40.2 [1.40.1]: https://github.com/kgdunn/process-improve/compare/v1.40.0...v1.40.1 [1.40.0]: https://github.com/kgdunn/process-improve/compare/v1.39.0...v1.40.0 [1.39.0]: https://github.com/kgdunn/process-improve/compare/v1.38.4...v1.39.0 diff --git a/CITATION.cff b/CITATION.cff index 4dc0b91a..0b083797 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -12,8 +12,8 @@ authors: repository-code: "https://github.com/kgdunn/process-improve" url: "https://kgdunn.github.io/process-improve/" license: MIT -version: 1.40.1 -date-released: "2026-06-11" +version: 1.40.2 +date-released: "2026-06-12" keywords: - chemometrics - multivariate analysis diff --git a/pyproject.toml b/pyproject.toml index 7820b86d..81dd0a96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "process-improve" -version = "1.40.1" +version = "1.40.2" description = 'Designed Experiments; Latent Variables (PCA, PLS, multivariate methods with missing data); Process Monitoring; Batch data analysis.' readme = "README.md" license = "MIT"