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.
Files changed (56) hide show
  1. processbehavior/__init__.py +133 -0
  2. processbehavior/analysis.py +2944 -0
  3. processbehavior/analysis_dataset.py +361 -0
  4. processbehavior/analysis_result.py +1276 -0
  5. processbehavior/calibration.py +70 -0
  6. processbehavior/capability.py +720 -0
  7. processbehavior/data_preparation.py +792 -0
  8. processbehavior/datasets/__init__.py +37 -0
  9. processbehavior/datasets/data/coffee_shop_demo_long.csv +385 -0
  10. processbehavior/datasets/loaders.py +66 -0
  11. processbehavior/datasets/synthetic.py +1711 -0
  12. processbehavior/derivations.py +602 -0
  13. processbehavior/effects_calculator.py +603 -0
  14. processbehavior/exceptions.py +216 -0
  15. processbehavior/exporters/__init__.py +5 -0
  16. processbehavior/exporters/excel_exporter.py +708 -0
  17. processbehavior/focused_result.py +329 -0
  18. processbehavior/formulation_spec.py +144 -0
  19. processbehavior/loss_function.py +517 -0
  20. processbehavior/maximum_information.py +285 -0
  21. processbehavior/plotting/__init__.py +46 -0
  22. processbehavior/plotting/_math.py +121 -0
  23. processbehavior/plotting/capability_chart.py +858 -0
  24. processbehavior/plotting/contracts.py +176 -0
  25. processbehavior/plotting/control_chart.py +206 -0
  26. processbehavior/plotting/effects_charts.py +640 -0
  27. processbehavior/plotting/lane_boundaries.py +78 -0
  28. processbehavior/plotting/limits.py +157 -0
  29. processbehavior/plotting/maximum_information_chart.py +395 -0
  30. processbehavior/plotting/plotter.py +1469 -0
  31. processbehavior/plotting/renderers.py +660 -0
  32. processbehavior/plotting/report.py +211 -0
  33. processbehavior/plotting/residuals.py +282 -0
  34. processbehavior/plotting/run_rules_viz.py +150 -0
  35. processbehavior/plotting/stats_box.py +183 -0
  36. processbehavior/plotting/themes.py +544 -0
  37. processbehavior/plotting/x_axis_layout.py +474 -0
  38. processbehavior/plotting/zones.py +127 -0
  39. processbehavior/process_behavior.py +1290 -0
  40. processbehavior/py.typed +0 -0
  41. processbehavior/residual_calculator.py +572 -0
  42. processbehavior/result_signals.py +149 -0
  43. processbehavior/result_tabular.py +296 -0
  44. processbehavior/sds_detector.py +1432 -0
  45. processbehavior/signals/__init__.py +14 -0
  46. processbehavior/signals/config.py +435 -0
  47. processbehavior/signals/detector.py +328 -0
  48. processbehavior/signals/detectors.py +368 -0
  49. processbehavior/signals/result.py +232 -0
  50. processbehavior/spc_constants.py +692 -0
  51. processbehavior/study.py +2733 -0
  52. processbehavior/types.py +154 -0
  53. processbehavior-0.2.0.dist-info/METADATA +279 -0
  54. processbehavior-0.2.0.dist-info/RECORD +56 -0
  55. processbehavior-0.2.0.dist-info/WHEEL +4 -0
  56. 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')