processbehavior 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- processbehavior/__init__.py +133 -0
- processbehavior/analysis.py +2944 -0
- processbehavior/analysis_dataset.py +361 -0
- processbehavior/analysis_result.py +1276 -0
- processbehavior/calibration.py +70 -0
- processbehavior/capability.py +720 -0
- processbehavior/data_preparation.py +792 -0
- processbehavior/datasets/__init__.py +37 -0
- processbehavior/datasets/data/coffee_shop_demo_long.csv +385 -0
- processbehavior/datasets/loaders.py +66 -0
- processbehavior/datasets/synthetic.py +1711 -0
- processbehavior/derivations.py +602 -0
- processbehavior/effects_calculator.py +603 -0
- processbehavior/exceptions.py +216 -0
- processbehavior/exporters/__init__.py +5 -0
- processbehavior/exporters/excel_exporter.py +708 -0
- processbehavior/focused_result.py +329 -0
- processbehavior/formulation_spec.py +144 -0
- processbehavior/loss_function.py +517 -0
- processbehavior/maximum_information.py +285 -0
- processbehavior/plotting/__init__.py +46 -0
- processbehavior/plotting/_math.py +121 -0
- processbehavior/plotting/capability_chart.py +858 -0
- processbehavior/plotting/contracts.py +176 -0
- processbehavior/plotting/control_chart.py +206 -0
- processbehavior/plotting/effects_charts.py +640 -0
- processbehavior/plotting/lane_boundaries.py +78 -0
- processbehavior/plotting/limits.py +157 -0
- processbehavior/plotting/maximum_information_chart.py +395 -0
- processbehavior/plotting/plotter.py +1469 -0
- processbehavior/plotting/renderers.py +660 -0
- processbehavior/plotting/report.py +211 -0
- processbehavior/plotting/residuals.py +282 -0
- processbehavior/plotting/run_rules_viz.py +150 -0
- processbehavior/plotting/stats_box.py +183 -0
- processbehavior/plotting/themes.py +544 -0
- processbehavior/plotting/x_axis_layout.py +474 -0
- processbehavior/plotting/zones.py +127 -0
- processbehavior/process_behavior.py +1290 -0
- processbehavior/py.typed +0 -0
- processbehavior/residual_calculator.py +572 -0
- processbehavior/result_signals.py +149 -0
- processbehavior/result_tabular.py +296 -0
- processbehavior/sds_detector.py +1432 -0
- processbehavior/signals/__init__.py +14 -0
- processbehavior/signals/config.py +435 -0
- processbehavior/signals/detector.py +328 -0
- processbehavior/signals/detectors.py +368 -0
- processbehavior/signals/result.py +232 -0
- processbehavior/spc_constants.py +692 -0
- processbehavior/study.py +2733 -0
- processbehavior/types.py +154 -0
- processbehavior-0.2.0.dist-info/METADATA +279 -0
- processbehavior-0.2.0.dist-info/RECORD +56 -0
- processbehavior-0.2.0.dist-info/WHEEL +4 -0
- processbehavior-0.2.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Typed contracts for the plotting pipeline.
|
|
2
|
+
|
|
3
|
+
Defines the data structures that flow between the coordinator (plotter.py)
|
|
4
|
+
and the renderers (renderers.py). Validated at construction to catch
|
|
5
|
+
metadata-shape bugs before rendering starts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from ..exceptions import ChartNotAvailableError, ProcessBehaviorError
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
import pandas as pd
|
|
17
|
+
|
|
18
|
+
from .themes import ChartTheme
|
|
19
|
+
|
|
20
|
+
# Valid chart types that renderers accept
|
|
21
|
+
VALID_CHART_TYPES = frozenset({'Xbar', 'S', 'X', 'mR', 'Histogram'})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PlotError(ProcessBehaviorError):
|
|
25
|
+
"""Rendering-specific failure in the plotting pipeline.
|
|
26
|
+
|
|
27
|
+
Raised when plotting encounters an issue with chart metadata,
|
|
28
|
+
rendering state, or figure construction that prevents producing
|
|
29
|
+
a valid chart.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class ChartRenderSpec:
|
|
35
|
+
"""Validated metadata contract between analysis outputs and renderers.
|
|
36
|
+
|
|
37
|
+
Built from chart_info metadata and validated at construction.
|
|
38
|
+
Prevents silent metadata-shape bugs from reaching the renderer.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
chart_type: str
|
|
42
|
+
value_col: str
|
|
43
|
+
x_col: str | None
|
|
44
|
+
center_key: str | None
|
|
45
|
+
limits_vary: bool
|
|
46
|
+
run_rules_applicable: bool
|
|
47
|
+
lane_boundaries: list[dict[str, Any]] | None
|
|
48
|
+
phased: bool
|
|
49
|
+
|
|
50
|
+
def __post_init__(self) -> None:
|
|
51
|
+
if self.chart_type not in VALID_CHART_TYPES:
|
|
52
|
+
raise ChartNotAvailableError(
|
|
53
|
+
f"Unknown chart type: '{self.chart_type}'.\nValid types: {sorted(VALID_CHART_TYPES)}",
|
|
54
|
+
chart=self.chart_type,
|
|
55
|
+
available=sorted(VALID_CHART_TYPES),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class RenderContext:
|
|
61
|
+
"""Everything a renderer needs to draw one chart panel.
|
|
62
|
+
|
|
63
|
+
Built once by the coordinator, threaded through the rendering pipeline.
|
|
64
|
+
Immutable to prevent accidental mutation between faceted panels.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
spec: ChartRenderSpec
|
|
68
|
+
data: pd.DataFrame
|
|
69
|
+
stats: dict[str, Any]
|
|
70
|
+
theme: ChartTheme
|
|
71
|
+
result: Any # AnalysisResult — avoid circular import
|
|
72
|
+
chart_name: str
|
|
73
|
+
highlight_signals: bool
|
|
74
|
+
show_limits: bool
|
|
75
|
+
show_limit_values: bool
|
|
76
|
+
show_zones: bool
|
|
77
|
+
show_rules: bool
|
|
78
|
+
show_stats: bool
|
|
79
|
+
is_faceted: bool
|
|
80
|
+
marker_size: int
|
|
81
|
+
line_width: float
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def build_render_spec(
|
|
85
|
+
chart_info: dict[str, Any],
|
|
86
|
+
chart_name: str,
|
|
87
|
+
x_col: str | None,
|
|
88
|
+
center_key: str | None,
|
|
89
|
+
) -> ChartRenderSpec:
|
|
90
|
+
"""Build a validated ChartRenderSpec from raw chart_info.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
chart_info : dict
|
|
95
|
+
Chart info dict with 'data', 'statistics', and 'metadata' keys.
|
|
96
|
+
chart_name : str
|
|
97
|
+
Name of the chart (for error messages).
|
|
98
|
+
x_col : str | None
|
|
99
|
+
Resolved x-axis column (or None for index-based).
|
|
100
|
+
center_key : str | None
|
|
101
|
+
Key for the centerline statistic in stats dict.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
ChartRenderSpec
|
|
106
|
+
Validated render specification.
|
|
107
|
+
|
|
108
|
+
Raises
|
|
109
|
+
------
|
|
110
|
+
PlotError
|
|
111
|
+
If required metadata is missing.
|
|
112
|
+
"""
|
|
113
|
+
metadata = chart_info.get('metadata', {})
|
|
114
|
+
stats = chart_info.get('statistics', {})
|
|
115
|
+
|
|
116
|
+
if 'metadata' not in chart_info:
|
|
117
|
+
raise PlotError(
|
|
118
|
+
f"Chart '{chart_name}' missing metadata. "
|
|
119
|
+
f'This indicates a bug in chart calculation. '
|
|
120
|
+
f"All charts must have metadata with 'value_col'."
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
value_col = metadata.get('value_col')
|
|
124
|
+
if not value_col:
|
|
125
|
+
raise PlotError(
|
|
126
|
+
f"Chart '{chart_name}' metadata missing 'value_col'. This indicates a bug in chart calculation."
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
chart_type = metadata.get('chart_type', chart_name.split('_')[0])
|
|
130
|
+
limits_vary = bool(stats.get('limits_vary'))
|
|
131
|
+
|
|
132
|
+
return ChartRenderSpec(
|
|
133
|
+
chart_type=chart_type,
|
|
134
|
+
value_col=value_col,
|
|
135
|
+
x_col=x_col,
|
|
136
|
+
center_key=center_key,
|
|
137
|
+
limits_vary=limits_vary,
|
|
138
|
+
run_rules_applicable=metadata.get('run_rules_applicable', True),
|
|
139
|
+
lane_boundaries=metadata.get('lane_boundaries'),
|
|
140
|
+
phased=metadata.get('phased', False),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def build_render_context(
|
|
145
|
+
spec: ChartRenderSpec,
|
|
146
|
+
chart_info: dict[str, Any],
|
|
147
|
+
chart_name: str,
|
|
148
|
+
theme: ChartTheme,
|
|
149
|
+
result: Any,
|
|
150
|
+
*,
|
|
151
|
+
highlight_signals: bool,
|
|
152
|
+
show_limits: bool,
|
|
153
|
+
show_limit_values: bool,
|
|
154
|
+
show_zones: bool,
|
|
155
|
+
show_rules: bool,
|
|
156
|
+
show_stats: bool,
|
|
157
|
+
is_faceted: bool,
|
|
158
|
+
) -> RenderContext:
|
|
159
|
+
"""Build a RenderContext from a spec and display options."""
|
|
160
|
+
return RenderContext(
|
|
161
|
+
spec=spec,
|
|
162
|
+
data=chart_info['data'],
|
|
163
|
+
stats=chart_info['statistics'],
|
|
164
|
+
theme=theme,
|
|
165
|
+
result=result,
|
|
166
|
+
chart_name=chart_name,
|
|
167
|
+
highlight_signals=highlight_signals,
|
|
168
|
+
show_limits=show_limits,
|
|
169
|
+
show_limit_values=show_limit_values,
|
|
170
|
+
show_zones=show_zones,
|
|
171
|
+
show_rules=show_rules,
|
|
172
|
+
show_stats=show_stats,
|
|
173
|
+
is_faceted=is_faceted,
|
|
174
|
+
marker_size=theme.facet_marker_size if is_faceted else theme.data_marker_size,
|
|
175
|
+
line_width=theme.facet_line_width if is_faceted else theme.data_line_width,
|
|
176
|
+
)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom control chart figure wrapper.
|
|
3
|
+
|
|
4
|
+
Extends Plotly figures with domain-specific methods for ProcessBehavior.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
import plotly.graph_objects as go
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from processbehavior.analysis_result import AnalysisResult
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _translate_image_error(exc: Exception) -> Exception:
|
|
22
|
+
"""Turn a static-export failure into an error naming what to actually do.
|
|
23
|
+
|
|
24
|
+
Three outcomes, because they need three different fixes:
|
|
25
|
+
|
|
26
|
+
- kaleido missing -> ``ImportError`` telling you to install it.
|
|
27
|
+
- kaleido present but no browser -> ``RuntimeError`` about Chrome. This
|
|
28
|
+
used to be reported as "Image export requires kaleido" because the
|
|
29
|
+
handler substring-matched "kaleido" in the message, sending people to
|
|
30
|
+
reinstall a package they already had.
|
|
31
|
+
- anything else -> returned unchanged; not every failure here is about
|
|
32
|
+
the export backend.
|
|
33
|
+
"""
|
|
34
|
+
text = str(exc).lower()
|
|
35
|
+
origin = type(exc).__module__ or ''
|
|
36
|
+
|
|
37
|
+
if isinstance(exc, ImportError) or 'install kaleido' in text or 'requires kaleido' in text:
|
|
38
|
+
return ImportError(
|
|
39
|
+
'Image export requires kaleido.\n'
|
|
40
|
+
"Install with: pip install 'processbehavior[images]'\n"
|
|
41
|
+
'Or use .save_html() for interactive HTML export'
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if 'chrome' in text or 'chromium' in text or 'browser' in text or origin.startswith('kaleido'):
|
|
45
|
+
return RuntimeError(
|
|
46
|
+
f'Static image export needs a Chrome/Chromium browser, which kaleido could not find.\n'
|
|
47
|
+
f'Install one with: plotly_get_chrome\n'
|
|
48
|
+
f'Or use .save_html() for interactive HTML export.\n'
|
|
49
|
+
f'Underlying error: {exc}'
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return exc
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ControlChartFigure:
|
|
56
|
+
"""
|
|
57
|
+
Wrapper around plotly Figure with domain-specific methods.
|
|
58
|
+
|
|
59
|
+
This class extends Plotly figures with processbehavior-specific
|
|
60
|
+
functionality while maintaining full access to Plotly's API: anything
|
|
61
|
+
not defined here is delegated to the wrapped figure, so plotly methods
|
|
62
|
+
and attributes work as they would on the figure itself.
|
|
63
|
+
|
|
64
|
+
Examples
|
|
65
|
+
--------
|
|
66
|
+
>>> fig = result.plot()
|
|
67
|
+
>>> fig.show() # Display in browser/notebook
|
|
68
|
+
>>> fig.save_html('report.html') # Export to HTML
|
|
69
|
+
>>> fig.save_image('chart.png') # Export to image
|
|
70
|
+
>>> fig.update_layout(title='Custom Title') # Full Plotly API
|
|
71
|
+
|
|
72
|
+
Plotly's own spellings work too, via delegation:
|
|
73
|
+
|
|
74
|
+
>>> fig.write_html('report.html')
|
|
75
|
+
>>> fig.update_traces(marker_size=4)
|
|
76
|
+
>>> fig.add_hline(y=10)
|
|
77
|
+
>>> fig.data # the underlying traces
|
|
78
|
+
|
|
79
|
+
The wrapped figure is also available directly as ``fig.figure``.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, plotly_fig: go.Figure, analysis_result: AnalysisResult):
|
|
83
|
+
"""
|
|
84
|
+
Wrap a Plotly figure with enhanced functionality.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
plotly_fig : go.Figure
|
|
89
|
+
Underlying Plotly figure
|
|
90
|
+
analysis_result : AnalysisResult
|
|
91
|
+
Source analysis result for metadata
|
|
92
|
+
"""
|
|
93
|
+
self._fig = plotly_fig
|
|
94
|
+
self._result = analysis_result
|
|
95
|
+
|
|
96
|
+
def show(self) -> None:
|
|
97
|
+
"""Display figure in browser or notebook."""
|
|
98
|
+
self._fig.show()
|
|
99
|
+
|
|
100
|
+
def save_html(self, filepath: str | Path, include_plotlyjs: bool = True, auto_open: bool = False) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Save as standalone HTML file.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
filepath : str or Path
|
|
107
|
+
Output file path
|
|
108
|
+
include_plotlyjs : bool, default True
|
|
109
|
+
Whether to include plotly.js (makes file larger but standalone)
|
|
110
|
+
auto_open : bool, default False
|
|
111
|
+
Whether to open in browser after saving
|
|
112
|
+
"""
|
|
113
|
+
filepath = Path(filepath)
|
|
114
|
+
|
|
115
|
+
self._fig.write_html(
|
|
116
|
+
str(filepath), include_plotlyjs='cdn' if not include_plotlyjs else True, auto_open=auto_open
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
logger.info(f'✓ Saved interactive chart to: {filepath}')
|
|
120
|
+
|
|
121
|
+
def save_image(
|
|
122
|
+
self, filepath: str | Path, width: int | None = None, height: int | None = None, scale: float = 2.0
|
|
123
|
+
) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Save as static image (requires kaleido).
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
filepath : str or Path
|
|
130
|
+
Output file path (.png, .jpg, .svg, .pdf)
|
|
131
|
+
width : int, optional
|
|
132
|
+
Image width in pixels
|
|
133
|
+
height : int, optional
|
|
134
|
+
Image height in pixels
|
|
135
|
+
scale : float, default 2.0
|
|
136
|
+
Scale factor for resolution
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
self._fig.write_image(str(filepath), width=width, height=height, scale=scale)
|
|
140
|
+
logger.info(f'✓ Saved static image to: {filepath}')
|
|
141
|
+
except Exception as e:
|
|
142
|
+
raise _translate_image_error(e) from e
|
|
143
|
+
|
|
144
|
+
# Plotly's own spellings, so muscle memory works. write_image routes through
|
|
145
|
+
# the same error translation as save_image rather than surfacing kaleido's
|
|
146
|
+
# raw failure.
|
|
147
|
+
def write_html(self, *args, **kwargs):
|
|
148
|
+
"""Alias for plotly's ``write_html`` (see also :meth:`save_html`)."""
|
|
149
|
+
return self._fig.write_html(*args, **kwargs)
|
|
150
|
+
|
|
151
|
+
def write_image(self, *args, **kwargs):
|
|
152
|
+
"""Alias for plotly's ``write_image``, with translated export errors."""
|
|
153
|
+
try:
|
|
154
|
+
return self._fig.write_image(*args, **kwargs)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
raise _translate_image_error(e) from e
|
|
157
|
+
|
|
158
|
+
def add_annotation(self, text: str, x, y, **kwargs) -> ControlChartFigure:
|
|
159
|
+
"""Add text annotation to the figure.
|
|
160
|
+
|
|
161
|
+
Returns self for method chaining:
|
|
162
|
+
``fig.add_annotation(...).update_layout(...)``
|
|
163
|
+
"""
|
|
164
|
+
self._fig.add_annotation(text=text, x=x, y=y, **kwargs)
|
|
165
|
+
return self
|
|
166
|
+
|
|
167
|
+
def update_layout(self, **kwargs) -> ControlChartFigure:
|
|
168
|
+
"""Update figure layout (full Plotly API).
|
|
169
|
+
|
|
170
|
+
Returns self for method chaining:
|
|
171
|
+
``fig.update_layout(title='X').update_layout(width=800)``
|
|
172
|
+
"""
|
|
173
|
+
self._fig.update_layout(**kwargs)
|
|
174
|
+
return self
|
|
175
|
+
|
|
176
|
+
def __getattr__(self, name: str):
|
|
177
|
+
"""Delegate anything we do not define to the wrapped plotly figure.
|
|
178
|
+
|
|
179
|
+
Only called when normal lookup fails, so the methods defined above are
|
|
180
|
+
never shadowed. Dunder names are refused outright: letting them fall
|
|
181
|
+
through makes copy/pickle/IPython probing find bound methods of the
|
|
182
|
+
*figure* and behave erratically. ``_fig`` itself is refused for the
|
|
183
|
+
same reason — during unpickling ``__getattr__`` can run before the
|
|
184
|
+
instance dict exists, and delegating would recurse forever.
|
|
185
|
+
"""
|
|
186
|
+
if name.startswith('__') and name.endswith('__'):
|
|
187
|
+
raise AttributeError(name)
|
|
188
|
+
if name in ('_fig', '_result'):
|
|
189
|
+
raise AttributeError(name)
|
|
190
|
+
return getattr(self._fig, name)
|
|
191
|
+
|
|
192
|
+
def __dir__(self):
|
|
193
|
+
"""Own attributes plus the wrapped figure's, so completion finds both."""
|
|
194
|
+
return sorted(set(super().__dir__()) | set(dir(self._fig)))
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def figure(self) -> go.Figure:
|
|
198
|
+
"""Get underlying Plotly figure."""
|
|
199
|
+
return self._fig
|
|
200
|
+
|
|
201
|
+
def __repr__(self):
|
|
202
|
+
return f'ControlChartFigure(charts={list(self._result.charts.keys())})'
|
|
203
|
+
|
|
204
|
+
def _repr_html_(self) -> str:
|
|
205
|
+
"""Rich HTML display for Jupyter notebooks and nbconvert."""
|
|
206
|
+
return self._fig.to_html(full_html=False, include_plotlyjs='cdn')
|