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" 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 {} 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) 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",) 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 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). 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 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 ---------- 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 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:: 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 # ---------------------------------------------------------------------------