From 5139875e03557eeb47e5513def6ee8030e3a350a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 07:20:59 +0000 Subject: [PATCH 01/11] docs: fix t2_plot with_a docstring (was describing SPE, not T^2) The parameter description said 'shows the SPE after this number of model components' but t2_plot plots Hotelling's T^2. Reword to match the actual statistic shown. --- src/process_improve/multivariate/plots.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/process_improve/multivariate/plots.py b/src/process_improve/multivariate/plots.py index c78d17e6..6d5777b9 100644 --- a/src/process_improve/multivariate/plots.py +++ b/src/process_improve/multivariate/plots.py @@ -572,8 +572,8 @@ def t2_plot( # noqa: C901 model : MVmodel object (PCA, or PLS) A latent variable model generated by this library. with_a : int, optional - Uses this many number of latent variables, and therefore shows the SPE after this number of - model components. By default the total number of components fitted will be used. + Uses this many number of latent variables, and therefore shows Hotelling's T2 after this + number of model components. By default the total number of components fitted will be used. items_to_highlight : dict, optional Keys are JSON strings parseable by ``json.loads`` into a Plotly line specifier; values are lists of index names to highlight. For example:: From e068832bcfef33a7b06a20c30274edd586cf4ca7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 07:21:19 +0000 Subject: [PATCH 02/11] docs: fix center/scale NaN behaviour claims in _preprocessing Both docstrings claimed the defaults (np.mean / np.std) skip missing data, but those functions propagate NaN. Reword to describe the actual behaviour and point callers at np.nanmean / np.nanstd when NaN-skipping is desired. --- .../multivariate/_preprocessing.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/process_improve/multivariate/_preprocessing.py b/src/process_improve/multivariate/_preprocessing.py index 14f54a79..e7f7d7f8 100644 --- a/src/process_improve/multivariate/_preprocessing.py +++ b/src/process_improve/multivariate/_preprocessing.py @@ -158,9 +158,12 @@ def center( This specifies the axis along which the centering vector will be calculated if not provided. The function is applied along the `axis`: 0=down the columns; 1 = across the rows. - *Missing values*: The sample mean is computed by taking the sum along the `axis`, skipping - any missing data, and dividing by N = number of values which are present. Values which were - missing before, are left as missing after. + *Missing values*: The default ``func=np.mean`` propagates NaN, so any column containing a + missing value is centred with a NaN mean and the subtraction leaves the whole column as NaN. + To skip missing entries when computing the centring vector, pass a NaN-aware function such + as ``func=np.nanmean`` (which sums the present values along `axis` and divides by the count + of non-missing entries per column). Values which were missing before are left as missing + after. """ # pandas-stubs types apply()'s axis as a Literal, so a plain ``int`` axis does # not match any overload; the call is valid at runtime. @@ -186,8 +189,11 @@ def scale( `func` [optional; default=np.std] {a function} The default (np.std) uses NumPy to calculate the standard deviation of - the data along the required `axis`, skipping over any missing data, and - uses that as `scale`. + the data along the required `axis`, and uses that as `scale`. ``np.std`` + propagates NaN, so any column containing a missing value gets a NaN + scaling factor and the whole column becomes NaN in the output; pass a + NaN-aware function such as ``func=np.nanstd`` to skip missing entries + when computing the per-column standard deviation. `axis` [optional; default=0] {integer} Transformations are applied on slices of data. This specifies the From 7e04702d43fe0ffbe15b7284f6fe222cc1a820fe Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 07:21:36 +0000 Subject: [PATCH 03/11] docs: describe Resampler mutual-exclusion guard's actual behaviour The docstring said the three mode flags are mutually exclusive, but the __init__ guard only raises when all three are set at once. Document the actual behaviour (all-three-set trigger; resample() prefers jackknife over bootstrap over fractional) so users are not misled by the guarantee. The guard itself is left unchanged; the underlying bug is flagged in the PR body for the maintainer. --- src/process_improve/multivariate/_resampling.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/process_improve/multivariate/_resampling.py b/src/process_improve/multivariate/_resampling.py index 1de9f469..d897177b 100644 --- a/src/process_improve/multivariate/_resampling.py +++ b/src/process_improve/multivariate/_resampling.py @@ -50,12 +50,16 @@ def __init__( # noqa: PLR0913 The `accessor` is a callable that takes an estimator and returns the parameters of interest. - Mutually exclusive parameters: + The three mode selectors are intended to be mutually exclusive: * `use_jackknife` flag indicates whether to use jackknife resampling (leave out one sample; rebuild) * `bootstrap_rounds` specifies the number of bootstrap rounds if applicable (resample data with replacement) * `fraction_excluded` specifies the fraction of data to exclude in each resample (for fractional resampling) - Only one of these parameters should be set at a time. + The caller should set only one of these at a time. The current guard only raises + ``ValueError`` when *all three* are set simultaneously (``use_jackknife`` is truthy, + ``bootstrap_rounds > 0``, and ``fraction_excluded > 0.0``); pairwise conflicts are + not currently caught here. When more than one is active :meth:`resample` picks in the + order jackknife > bootstrap > fractional. Parameters ---------- From 6c5a22007481a221810df070d5b3b10d7421fddf Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 07:21:48 +0000 Subject: [PATCH 04/11] docs: replace bogus Settings.override reference with real API Settings has no override() method. Point users at the per-knob property setters (each property has a paired setter) and describe reload() / as_dict() in terms of what they actually do. --- src/process_improve/config.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/process_improve/config.py b/src/process_improve/config.py index 46b28e58..906e3275 100644 --- a/src/process_improve/config.py +++ b/src/process_improve/config.py @@ -139,8 +139,11 @@ class Settings: """Single-instance configuration store. Every attribute is a knob; reads are cached after the first access. + To set a single knob from code, assign to its property directly (each + knob exposes a matching setter, e.g. ``settings.tool_timeout = 30.0``). Call :meth:`reload` after mutating ``os.environ`` (typically inside a - test fixture); call :meth:`override` to set a single knob from code. + test fixture) so the next attribute access re-reads from the environment; + call :meth:`as_dict` for a snapshot of every knob's current value. """ __slots__ = ("_cache",) From 028bfa98daca9141f0842b3855e322733a04bc38 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 07:21:59 +0000 Subject: [PATCH 05/11] docs: correct DEFAULT_THEME attribute docstring The module docstring is explicit that importing themes does NOT set the Plotly default template, but the DEFAULT_THEME attribute docstring contradicted that. Reword to match: DEFAULT_THEME is the theme this library's own plots request explicitly, and set_theme() is the way to change the process-wide default. --- src/process_improve/visualization/themes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/process_improve/visualization/themes.py b/src/process_improve/visualization/themes.py index d4250129..51d86b01 100644 --- a/src/process_improve/visualization/themes.py +++ b/src/process_improve/visualization/themes.py @@ -65,7 +65,10 @@ #: Names of every theme registered by :func:`register_themes`. THEME_NAMES: tuple[str, ...] = (THEME_TUFTE, THEME_ECONOMIST, THEME_JOURNAL, THEME_BRAND) -#: Theme applied as the Plotly default when the package is imported. +#: Name of the theme this library's plots request explicitly (via the +#: ``template`` setting) when the caller has not overridden it. Importing +#: this module does **not** change ``plotly.io.templates.default``; call +#: :func:`set_theme` to opt a whole session into a process-improve theme. DEFAULT_THEME: str = THEME_JOURNAL # --------------------------------------------------------------------------- From 7f3e119903a22ceb6a2a70cdddb8ebc9547c2529 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 07:22:21 +0000 Subject: [PATCH 06/11] docs: mark melted_to_wide as a stub in its docstring The function validates the batch id column and then unconditionally returns an empty dict; the real pivot is commented out beneath. Add a warning admonition so callers do not silently receive an empty result believing they got a reshaped view. Flagged for maintainer follow-up in the PR body. --- src/process_improve/batch/data_input.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/process_improve/batch/data_input.py b/src/process_improve/batch/data_input.py index 943f9e4e..e990fb2d 100644 --- a/src/process_improve/batch/data_input.py +++ b/src/process_improve/batch/data_input.py @@ -171,7 +171,15 @@ def melted_to_dict(in_df: pd.DataFrame, batch_id_col: str) -> dict: def melted_to_wide(in_df: pd.DataFrame, batch_id_col: str) -> dict: - """Convert aligned melted data to wide format.""" + """Convert aligned melted data to wide format. + + .. warning:: + Stub. The implementation is not yet written: the function only validates + that ``batch_id_col`` is present in ``in_df`` and then returns an empty + dict. Do not rely on it to reshape data. See the commented-out sketch + below for the intended pivot; the tracking work is open for a + maintainer. + """ if batch_id_col not in in_df: raise ValueError(f"The `batch_id_col` column {batch_id_col!r} does not exist in the incoming dataframe.") return {} From eaffe48423e1e485188079f5b5cba4eb6e6085b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 07:22:37 +0000 Subject: [PATCH 07/11] docs: note that f_crossing ignores phase_col f_crossing's docstring promised per-phase evaluation via phase_col but the implementation hard-wires phase_col=None when calling _prepare_data, so crossings are computed across the whole batch instead. Add a note to make the current behaviour explicit; the underlying bug is flagged in the PR body for the maintainer. --- src/process_improve/batch/features.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/process_improve/batch/features.py b/src/process_improve/batch/features.py index 5f2c6076..bf231a9e 100644 --- a/src/process_improve/batch/features.py +++ b/src/process_improve/batch/features.py @@ -644,13 +644,18 @@ def f_crossing( # noqa: PLR0913 between the indices. If you prefer the index itself, use `only_index=True`, but the default for that setting is `False`. - Does this for each unique batch in the `batch_col` indicator column, and - within each unique phase, per batch, of the `phase_col` column. + Does this for each unique batch in the `batch_col` indicator column. + + .. note:: + The `phase_col` argument is accepted for API symmetry with the other + `f_*` helpers but is currently ignored: the implementation forwards + ``phase_col=None`` to the internal ``_prepare_data`` call, so the + crossing is computed across the whole batch rather than per phase. + Flagged for maintainer follow-up. `suffix`: what to add to the data tag, to name to this feature. - Note: NaN is returned for a given batch and phase, if the crossing is not - found. + Note: NaN is returned for a given batch, if the crossing is not found. """ base_name = f"cross-{int(threshold)}" if suffix is None else str(suffix) From 41cc4f44fb224ac53066185c32d727a35be45245 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 07:22:54 +0000 Subject: [PATCH 08/11] docs: clarify dispatch_ccd numeric-alpha behaviour under cube='full' The docstring implied any numeric alpha sets the axial distance directly, but under cube='full' the code discards numeric values and forces the pyDOE3 'orthogonal' alpha instead. Only cube='fractional' actually honours a numeric alpha. Document the split so callers do not silently get an 'orthogonal' design when they passed a specific number. The behavioural gap is flagged in the PR body for the maintainer. --- .../experiments/designs_response_surface.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/process_improve/experiments/designs_response_surface.py b/src/process_improve/experiments/designs_response_surface.py index cc0e58bc..b604c052 100644 --- a/src/process_improve/experiments/designs_response_surface.py +++ b/src/process_improve/experiments/designs_response_surface.py @@ -44,9 +44,18 @@ def dispatch_ccd( # noqa: PLR0913 center_points : int Number of center points (split between cube and axial portions). alpha : str, float, or None - Axial distance. Accepted string values: ``"rotatable"``, - ``"face_centered"``, ``"orthogonal"``. A numeric value sets - alpha directly. Defaults to ``"orthogonal"``. + Axial distance. Accepted string values: ``"rotatable"``, + ``"face_centered"``, ``"orthogonal"``. Defaults to ``"orthogonal"``. + + For ``cube="fractional"`` a numeric value is passed straight through + to the underlying design routine and sets alpha directly. For + ``cube="full"`` (the current default) a numeric value is currently + silently ignored: the code coerces every non-string alpha to the + ``"orthogonal"`` pyDOE3 setting, so the actual alpha comes from + pyDOE3 (reported back in the returned metadata's ``alpha_value``) + rather than from the number you passed. Prefer a string value under + ``cube="full"``; use ``cube="fractional"`` if you need to set alpha + numerically. Flagged for maintainer follow-up. cube : str How to build the cube (factorial) portion: ``"full"`` (default) uses the complete 2^k factorial; ``"fractional"`` uses a resolution-V (or From 987f1594b6ddc45450c8a0384fc5af92bb8d4c7c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 07:23:37 +0000 Subject: [PATCH 09/11] docs: document return_consensus + consensus Bunch keys; fix PA scale note Two docstring drifts in PCA: - select_n_components was missing the return_consensus parameter and the four extra Bunch keys it enables (minka_n_components, parallel_analysis_n_components, consensus, consensus_counts). Document both. - parallel_analysis said scale=True 'matches minka_mle', but minka_mle only mean-centres while parallel_analysis(scale=True) additionally unit-variance-scales via MCUVScaler. Describe the actual behaviour. --- src/process_improve/multivariate/_pca.py | 31 ++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/process_improve/multivariate/_pca.py b/src/process_improve/multivariate/_pca.py index 69189e45..f0c48c12 100644 --- a/src/process_improve/multivariate/_pca.py +++ b/src/process_improve/multivariate/_pca.py @@ -920,8 +920,12 @@ def parallel_analysis( the more conservative 95th-percentile threshold is the modern recommendation. scale : bool, default True - Mean-centre and unit-variance scale ``X`` before estimation - (matches :meth:`minka_mle`). + When ``True``, ``X`` is passed through :class:`MCUVScaler` + (mean-centring and unit-variance scaling) before the null + distribution is built; when ``False``, only the mean-centring + step of the eigendecomposition is applied to ``X``. This is + stricter than :meth:`minka_mle`, which only mean-centres and + does not offer unit-variance scaling. random_state : int, optional Seed for the null-matrix simulations. @@ -1069,6 +1073,16 @@ def select_n_components( # noqa: PLR0913, PLR0915, C901 step. Ignored under ``cv_scheme="row_wise"``. random_state : int, optional Seed for the ekf element-fold permutation. + return_consensus : bool, default False + When ``True``, also run :meth:`minka_mle` and + :meth:`parallel_analysis` on ``X`` and attach the extra + keys ``minka_n_components``, ``parallel_analysis_n_components``, + ``consensus`` (``"agree"`` when the three component counts are + within one of each other, else ``"disagree"``), and + ``consensus_counts`` (a 3-tuple of the ekf, Minka and PA + counts) to the returned Bunch. Parallel analysis is run with + ``scale=scale_inside_folds`` and the same ``random_state`` as + the ekf permutation. threshold : float, optional Deprecated. The original Wold PRESS-ratio cutoff. Passing it emits a :class:`DeprecationWarning`; the value is ignored. Use @@ -1108,6 +1122,19 @@ def select_n_components( # noqa: PLR0913, PLR0915, C901 - ``cv_scheme`` - the scheme used (``"ekf"`` or ``"row_wise"``). - ``selection_rule`` - the rule used to pick ``n_components``. + When ``return_consensus=True`` the Bunch additionally carries: + + - ``minka_n_components`` (int) - Minka's PPCA-MLE component count + on ``X`` (from :meth:`minka_mle`). + - ``parallel_analysis_n_components`` (int) - Horn's parallel- + analysis component count on ``X`` (from + :meth:`parallel_analysis`, run with + ``scale=scale_inside_folds`` and the same ``random_state``). + - ``consensus`` (str) - ``"agree"`` when the ekf / Minka / PA + counts are within one of each other, else ``"disagree"``. + - ``consensus_counts`` (tuple[int, int, int]) - the three + counts in ``(ekf, minka, parallel_analysis)`` order. + References ---------- Bro, R., Kjeldahl, K., Smilde, A. K., & Kiers, H. A. L. (2008). From ed399247f72628f642fb85b68426ab05c71739e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 07:23:54 +0000 Subject: [PATCH 10/11] docs: wrap TPLS.diagnose example input in DataFrameDict The example built new_data as a plain dict, but diagnose() raises TypeError unless the argument is a DataFrameDict. Update the example so the code actually runs. --- src/process_improve/multivariate/_tpls.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/process_improve/multivariate/_tpls.py b/src/process_improve/multivariate/_tpls.py index 62aa6b97..e8b451d3 100644 --- a/src/process_improve/multivariate/_tpls.py +++ b/src/process_improve/multivariate/_tpls.py @@ -553,8 +553,11 @@ def diagnose(self, X: DataFrameDict) -> Bunch: # noqa: C901, PLR0912, PLR0915 # Training phase: estimator = TPLS(n_components=2).fit(training_data) - # Testing/inference phase: - new_data = {"Z": ..., "F": ...} # you need at least the F block for a new prediction. "Z" is optional. + # Testing/inference phase. ``diagnose`` raises TypeError unless the + # input is a DataFrameDict, so wrap the raw ``{"Z": ..., "F": ...}`` + # mapping. You need at least the F block for a new prediction; "Z" + # is optional. + new_data = DataFrameDict({"Z": ..., "F": ...}) predictions = estimator.diagnose(new_data) Parameters From c52950c3dde6e225efafa97904acd2bc12f6898e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 07:24:38 +0000 Subject: [PATCH 11/11] chore: bump to 1.59.1 (docs-only fixes) Docs-only PATCH bump for the docstring-drift fixes across the codebase. CITATION.cff and CHANGELOG.md updated in the same commit per repo policy. --- CHANGELOG.md | 21 ++++++++++++++++++++- CITATION.cff | 4 ++-- pyproject.toml | 2 +- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c243b65..5161b6d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,24 @@ those changes. ## [Unreleased] +## [1.59.1] - 2026-07-24 + +### Fixed + +- Documentation-only fixes across the codebase: `t2_plot`'s `with_a` parameter + no longer describes SPE; `center` and `scale` no longer claim to skip NaNs + when the defaults (`np.mean`, `np.std`) propagate them; the `Settings` class + docstring points at real APIs (property setters, `reload`, `as_dict`) instead + of a non-existent `override` method; the `DEFAULT_THEME` attribute docstring + matches the module's stated non-side-effecting import behaviour; + `melted_to_wide` and `f_crossing` are labelled as stub / phase-agnostic; + `dispatch_ccd`'s numeric-alpha behaviour under `cube="full"` is called out; + `PCA.select_n_components` documents `return_consensus` and the extra Bunch + keys it enables; `PCA.parallel_analysis` describes its scaling honestly; and + `TPLS.diagnose`'s example wraps its input in a `DataFrameDict`. + `Resampler.__init__` describes the current all-three-set-only mutual- + exclusion guard rather than pretending pairwise conflicts are caught. + ## [1.59.0] - 2026-07-23 ### Added @@ -2625,7 +2643,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.59.0...HEAD +[Unreleased]: https://github.com/kgdunn/process-improve/compare/v1.59.1...HEAD +[1.59.1]: https://github.com/kgdunn/process-improve/compare/v1.59.0...v1.59.1 [1.59.0]: https://github.com/kgdunn/process-improve/compare/v1.58.0...v1.59.0 [1.58.0]: https://github.com/kgdunn/process-improve/compare/v1.57.0...v1.58.0 [1.57.0]: https://github.com/kgdunn/process-improve/compare/v1.56.0...v1.57.0 diff --git a/CITATION.cff b/CITATION.cff index fca9fa58..91a2fc82 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.59.0 -date-released: "2026-07-23" +version: 1.59.1 +date-released: "2026-07-24" keywords: - chemometrics - multivariate analysis diff --git a/pyproject.toml b/pyproject.toml index 4658d89b..e130c95d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "process-improve" -version = "1.59.0" +version = "1.59.1" description = 'Designed Experiments; Latent Variables (PCA, PLS, multivariate methods with missing data); Process Monitoring; Batch data analysis.' readme = "README.md" license = "MIT"