Skip to content
Open
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
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
10 changes: 9 additions & 1 deletion src/process_improve/batch/data_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down
13 changes: 9 additions & 4 deletions src/process_improve/batch/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion src/process_improve/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",)
Expand Down
15 changes: 12 additions & 3 deletions src/process_improve/experiments/designs_response_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 29 additions & 2 deletions src/process_improve/multivariate/_pca.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down
16 changes: 11 additions & 5 deletions src/process_improve/multivariate/_preprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions src/process_improve/multivariate/_resampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------
Expand Down
7 changes: 5 additions & 2 deletions src/process_improve/multivariate/_tpls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/process_improve/multivariate/plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -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::
Expand Down
5 changes: 4 additions & 1 deletion src/process_improve/visualization/themes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

# ---------------------------------------------------------------------------
Expand Down
Loading