Source code for process_improve.experiments.visualization.api

"""Public API for DOE visualization (Tool 6).

:func:`visualize_doe` is the single entry point.  It dispatches to the
correct plot class, builds the ChartSpec IR, runs the requested backend
adapters, and returns a JSON-serialisable dict.

:func:`main_effects_plot` is a convenience wrapper that accepts either
a fitted :class:`~process_improve.experiments.models.Model` or a
DataFrame, and returns a Plotly figure directly.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, cast

import pandas as pd

try:
    import plotly.graph_objects as go
except ImportError:  # pragma: no cover - exercised via env-without-plotly
    from process_improve._extras import _MissingExtra

    go = _MissingExtra("plotly", "plotting")  # type: ignore[assignment]

if TYPE_CHECKING:
    from process_improve.experiments.models import Model
    from process_improve.experiments.region import DesignRegion


_BACKENDS = ("both", "plotly", "echarts")


[docs] def visualize_doe( # noqa: PLR0913 *, plot_type: str, analysis_results: dict[str, Any] | None = None, design_data: list[dict[str, Any]] | None = None, response_column: str | None = None, factors_to_plot: list[str] | None = None, hold_values: dict[str, float] | None = None, highlight_significant: bool = True, confidence_level: float = 0.95, factor_labels: dict[str, str] | None = None, backend: str = "both", model: str | None = None, region: DesignRegion | dict[str, Any] | None = None, ) -> dict[str, Any]: """Generate a DOE visualisation. Parameters ---------- plot_type : str One of the supported DOE plot types (see :mod:`~process_improve.experiments.visualization.plots`). analysis_results : dict or None Output dict from :func:`analyze_experiment`. Required for most plot types that need fitted model data. ``"reference_distribution"`` takes the output of :func:`~process_improve.experiments.least_squares_means` instead. design_data : list[dict] or None Raw design matrix as a list of row-dicts. Used when *analysis_results* is not provided (e.g. main-effects from raw data). response_column : str or None Name of the response column in *design_data*. factors_to_plot : list[str] or None Subset of factors to display (2 for contour/interaction). hold_values : dict or None Fixed values for factors not being plotted. highlight_significant : bool Auto-highlight significant effects (Pareto / half-normal). confidence_level : float Confidence level for reference lines (default 0.95). factor_labels : dict or None Mapping ``{factor_symbol: full_name}`` used for axis labels and bar/legend entries (e.g. ``{"A": "Temperature [°C]"}``). backend : str ``"both"``, ``"plotly"``, or ``"echarts"``. model : str or None Model the design-quality plots (``"fds_plot"``, ``"power_curve"``, ``"prediction_variance"``) are computed for, as in :func:`~process_improve.experiments.evaluate_design`. When *None*, the formula of *analysis_results* is used, else the richest of ``"quadratic"``, ``"interactions"`` and ``"main_effects"`` that the design can estimate. region : DesignRegion, dict or None The settings that can be run, as a :class:`~process_improve.experiments.region.DesignRegion` or its ``to_dict()`` form; its factor ranges define the coded units. The ``"contour"`` and ``"prediction_variance"`` plots shade the settings it rules out (with the factors not plotted held at *hold_values*), and ``"surface_3d"`` leaves them out; the other plot types ignore it. With *None* the figures are those of the whole cube. Returns ------- dict[str, Any] Keys: ``plot_type``, ``title``, ``plotly`` (Plotly figure dict), ``echarts`` (ECharts option dict), ``data`` (raw computed data). Raises ------ ValueError If *backend* is not one of ``"both"``, ``"plotly"`` or ``"echarts"``. """ from process_improve.experiments.visualization.plots.registry import create_plot # noqa: PLC0415 if backend not in _BACKENDS: raise ValueError(f"backend must be one of {', '.join(map(repr, _BACKENDS))}; got backend={backend!r}.") plot = create_plot( plot_type=plot_type, analysis_results=analysis_results, design_data=design_data, response_column=response_column, factors_to_plot=factors_to_plot, hold_values=hold_values, highlight_significant=highlight_significant, confidence_level=confidence_level, factor_labels=factor_labels, model=model, region=region, ) spec = plot.to_spec() result: dict[str, Any] = { "plot_type": plot_type, "title": spec.title, "data": spec.to_data_dict(), } if backend in ("both", "plotly"): result["plotly"] = plot.to_plotly() else: result["plotly"] = None if backend in ("both", "echarts"): result["echarts"] = plot.to_echarts() else: result["echarts"] = None return result
[docs] def main_effects_plot( model_or_data: Model | pd.DataFrame, response_column: str | None = None, factors_to_plot: list[str] | None = None, *, factor_labels: dict[str, str] | None = None, ) -> go.Figure: """Generate a main-effects plot from a fitted model or a design DataFrame. For each factor, the mean response at every observed level is drawn as a line. A flat line indicates no main effect; a steep line indicates a strong one. The grand mean is shown as a horizontal dotted reference line. Parameters ---------- model_or_data : Model or pandas.DataFrame Either a fitted ``Model`` returned by :func:`~process_improve.experiments.models.lm`, or a DataFrame containing one column per factor plus a response column. response_column : str, optional Name of the response column. Required when *model_or_data* is a DataFrame; ignored (and inferred from the model formula) when a fitted ``Model`` is supplied. factors_to_plot : list of str, optional Subset of factors to display. By default all main-effect factors are shown - for a ``Model`` these are the level-1 factors from the formula; for a DataFrame they are all columns other than *response_column*. factor_labels : dict, optional Mapping ``{factor_symbol: full_name}`` used for legend entries (e.g. ``{"A": "Temperature [°C]"}``). Returns ------- plotly.graph_objects.Figure Plotly figure ready for display or further customisation. Examples -------- From a fitted model:: from process_improve.experiments import c, gather, lm, main_effects_plot A = c(-1, +1, -1, +1) B = c(-1, -1, +1, +1) y = c(52, 74, 62, 80, name="y") expt = gather(A=A, B=B, y=y) model = lm("y ~ A + B", expt) fig = main_effects_plot(model) From a raw DataFrame:: fig = main_effects_plot(df, response_column="yield") See Also -------- visualize_doe : General DOE plotting entry point. """ from process_improve.experiments.models import Model as _Model # noqa: PLC0415 if isinstance(model_or_data, _Model): df = pd.DataFrame(model_or_data.data).copy() response_column = model_or_data.get_response_name() if factors_to_plot is None: factors_to_plot = model_or_data.get_factor_names(level=1) elif isinstance(model_or_data, pd.DataFrame): if response_column is None: raise ValueError( "response_column must be provided when passing a DataFrame.", ) df = pd.DataFrame(model_or_data).copy() else: raise TypeError( f"model_or_data must be a fitted Model or a pandas DataFrame; got {type(model_or_data).__name__}.", ) if response_column not in df.columns: raise ValueError( f"response_column {response_column!r} not found in data columns: {list(df.columns)}.", ) design_data = cast("list[dict[str, Any]]", df.to_dict(orient="records")) result = visualize_doe( plot_type="main_effects", design_data=design_data, response_column=response_column, factors_to_plot=factors_to_plot, factor_labels=factor_labels, backend="plotly", ) return go.Figure(result["plotly"])