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,285 @@
1
+ """
2
+ Maximum Information Analysis — Bishop.
3
+
4
+ Examines R2 residuals (the noise floor / within-cell variation) via two views:
5
+ an XmR process behavior chart ("Is the noise floor stable?") and a percentage
6
+ histogram ("What does the noise floor distribution look like?").
7
+
8
+ Together they give the analyst full insight into irreducible variation.
9
+
10
+ References
11
+ ----------
12
+ Bishop, T. *Variance Analysis System*.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from typing import TYPE_CHECKING, Literal
19
+
20
+ import numpy as np
21
+ import pandas as pd
22
+
23
+ from .exceptions import ValidationError
24
+ from .spc_constants import R_UPPER_LIMIT_MULTIPLIER, XMR_LIMIT_MULTIPLIER
25
+
26
+ if TYPE_CHECKING:
27
+ from .analysis_dataset import AnalysisDataSet
28
+
29
+
30
+ # ============================================================================
31
+ # Data Class
32
+ # ============================================================================
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class MaximumInformationResult:
37
+ """
38
+ Result of a Maximum Information Analysis on R2 residuals.
39
+
40
+ Attributes
41
+ ----------
42
+ n : int
43
+ Number of R2 values.
44
+ r2_mean : float
45
+ Mean of R2 (~0).
46
+ r2_mR : float
47
+ Average moving range of R2.
48
+ sigma_hat : float
49
+ mR / d2 — noise floor sigma estimate.
50
+ upl : float
51
+ Upper natural process limit (R2 mean + 2.66 * mR).
52
+ lpl : float
53
+ Lower natural process limit (R2 mean - 2.66 * mR).
54
+ n_signals : int
55
+ Points beyond limits on XmR.
56
+ round_to : int
57
+ Decimal places for display.
58
+ """
59
+
60
+ n: int
61
+ r2_mean: float
62
+ r2_mR: float
63
+ sigma_hat: float
64
+ upl: float
65
+ lpl: float
66
+ n_signals: int
67
+
68
+ # Internal — for plotting
69
+ _xmr_chart_info: dict
70
+ _histogram_chart_info: dict
71
+ _response_name: str
72
+
73
+ round_to: int = 3
74
+
75
+ # ------------------------------------------------------------------
76
+ # Visualization
77
+ # ------------------------------------------------------------------
78
+
79
+ def plot(
80
+ self,
81
+ *,
82
+ view: Literal['combined', 'xmr', 'histogram'] = 'combined',
83
+ bins: int = 10,
84
+ theme=None,
85
+ width: int = 900,
86
+ height: int = 700,
87
+ title: str | None = None,
88
+ ):
89
+ """
90
+ Plot the maximum information analysis.
91
+
92
+ Parameters
93
+ ----------
94
+ view : str, default ``'combined'``
95
+ ``'combined'`` — XmR (top) + percentage histogram (bottom).
96
+ ``'xmr'`` — XmR of R2 only.
97
+ ``'histogram'`` — Percentage histogram only.
98
+ bins : int, default 10
99
+ Number of histogram bins.
100
+ theme : ChartTheme, optional
101
+ Visual theme (default: processbehavior theme).
102
+ width, height : int
103
+ Figure dimensions in pixels.
104
+ title : str, optional
105
+ Override chart title.
106
+
107
+ Returns
108
+ -------
109
+ plotly.graph_objects.Figure
110
+ """
111
+ from .plotting.maximum_information_chart import create_maximum_information_chart
112
+
113
+ return create_maximum_information_chart(
114
+ self,
115
+ view=view,
116
+ bins=bins,
117
+ theme=theme,
118
+ width=width,
119
+ height=height,
120
+ title=title,
121
+ )
122
+
123
+ # ------------------------------------------------------------------
124
+ # Presentation helpers
125
+ # ------------------------------------------------------------------
126
+
127
+ def as_dict(self, round_to: int | None = None) -> dict:
128
+ """Return results as a dict with values rounded for display."""
129
+ r = round_to if round_to is not None else self.round_to
130
+
131
+ def _r(v: float) -> float:
132
+ if not np.isfinite(v):
133
+ return v
134
+ return round(v, r)
135
+
136
+ return {
137
+ 'n': self.n,
138
+ 'r2_mean': _r(self.r2_mean),
139
+ 'r2_mR': _r(self.r2_mR),
140
+ 'sigma_hat': _r(self.sigma_hat),
141
+ 'upl': _r(self.upl),
142
+ 'lpl': _r(self.lpl),
143
+ 'n_signals': self.n_signals,
144
+ }
145
+
146
+ def __repr__(self) -> str:
147
+ d = self.as_dict()
148
+ lines = [
149
+ 'MaximumInformationResult:',
150
+ f' R2 residuals (noise floor): n={d["n"]}',
151
+ f' Mean={d["r2_mean"]}, mR={d["r2_mR"]}, sigma_hat={d["sigma_hat"]}',
152
+ f' NPL: [{d["lpl"]}, {d["upl"]}]',
153
+ f' Signals: {d["n_signals"]}',
154
+ ]
155
+ return '\n'.join(lines)
156
+
157
+
158
+ # ============================================================================
159
+ # Pure Function
160
+ # ============================================================================
161
+
162
+ # d2 for n=2 (moving range of consecutive pairs)
163
+ _D2 = 1.128
164
+
165
+
166
+ def assess_maximum_information(
167
+ ads: AnalysisDataSet,
168
+ round_to: int = 3,
169
+ ) -> MaximumInformationResult:
170
+ """
171
+ Perform Maximum Information Analysis on R2 residuals.
172
+
173
+ Parameters
174
+ ----------
175
+ ads : AnalysisDataSet
176
+ Must have VAS residuals with R2 column.
177
+ round_to : int, default 3
178
+ Decimal places for presentation.
179
+
180
+ Returns
181
+ -------
182
+ MaximumInformationResult
183
+
184
+ Raises
185
+ ------
186
+ ValidationError
187
+ If R2 residuals are not available.
188
+ """
189
+ if not ads.has_vas_residuals or 'R2' not in ads.analysis_dataset.columns:
190
+ raise ValidationError(
191
+ 'Maximum information analysis requires R2 residuals. '
192
+ 'R2 is only available for designs with factors and time '
193
+ f'(current SDS: {ads.observed_design_state}).'
194
+ )
195
+
196
+ df = ads.analysis_dataset
197
+ r2_values = df['R2'].dropna().to_numpy(dtype=float)
198
+ n = len(r2_values)
199
+
200
+ if n < 2:
201
+ raise ValidationError(f'Maximum information analysis requires at least 2 R2 values, got {n}.')
202
+
203
+ # --- XmR statistics on R2 ---
204
+ r2_mean = float(np.mean(r2_values))
205
+ mr_values = np.abs(np.diff(r2_values))
206
+ r2_mR = float(np.mean(mr_values))
207
+ sigma_hat = r2_mR / _D2
208
+
209
+ # Limits: mean ± E2 * mR
210
+ upl = r2_mean + XMR_LIMIT_MULTIPLIER * r2_mR
211
+ lpl = r2_mean - XMR_LIMIT_MULTIPLIER * r2_mR
212
+
213
+ # Count signals (beyond limits)
214
+ beyond = (r2_values > upl) | (r2_values < lpl)
215
+ n_signals = int(np.sum(beyond))
216
+
217
+ # --- Build chart info dicts for renderers ---
218
+
219
+ # XmR chart info — matches the format render_control_chart expects via RenderContext
220
+ # We build a simplified dict for use by the maximum_information_chart module
221
+ mr_series = np.empty(n, dtype=float)
222
+ mr_series[0] = np.nan
223
+ mr_series[1:] = mr_values
224
+
225
+ # R chart limits
226
+ r_upl = R_UPPER_LIMIT_MULTIPLIER * r2_mR
227
+
228
+ xmr_data = pd.DataFrame(
229
+ {
230
+ 'R2': r2_values,
231
+ 'mr': mr_series,
232
+ 'obs': np.arange(1, n + 1),
233
+ 'beyond_limits': beyond.astype(int),
234
+ }
235
+ )
236
+
237
+ # Mark R beyond limits
238
+ r_beyond = np.zeros(n, dtype=int)
239
+ r_beyond[0] = 0 # first has NaN mR
240
+ r_beyond[1:] = (mr_values > r_upl).astype(int)
241
+ xmr_data['r_beyond_limits'] = r_beyond
242
+
243
+ xmr_chart_info = {
244
+ 'data': xmr_data,
245
+ 'statistics': {
246
+ 'x_mean': r2_mean,
247
+ 'x_upl': upl,
248
+ 'x_lpl': lpl,
249
+ 'mR': r2_mR,
250
+ 'r_upl': r_upl,
251
+ },
252
+ 'metadata': {
253
+ 'value_col': 'R2',
254
+ 'mr_col': 'mr',
255
+ 'x_col': 'obs',
256
+ },
257
+ }
258
+
259
+ # Histogram chart info
260
+ response_name = ads.spec.response_var
261
+ histogram_chart_info = {
262
+ 'data': pd.DataFrame({'R2': r2_values}),
263
+ 'statistics': {
264
+ 'mean': r2_mean,
265
+ 'n': n,
266
+ },
267
+ 'metadata': {
268
+ 'value_col': 'R2',
269
+ 'chart_type': 'Histogram',
270
+ },
271
+ }
272
+
273
+ return MaximumInformationResult(
274
+ n=n,
275
+ r2_mean=r2_mean,
276
+ r2_mR=r2_mR,
277
+ sigma_hat=sigma_hat,
278
+ upl=upl,
279
+ lpl=lpl,
280
+ n_signals=n_signals,
281
+ _xmr_chart_info=xmr_chart_info,
282
+ _histogram_chart_info=histogram_chart_info,
283
+ _response_name=response_name,
284
+ round_to=round_to,
285
+ )
@@ -0,0 +1,46 @@
1
+ """
2
+ Plotting framework for ProcessBehavior.
3
+
4
+ Provides an intuitive, extensible API for creating control charts
5
+ with built-in support for faceting, interactivity, and export.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .capability_chart import create_capability_chart
11
+ from .contracts import PlotError
12
+ from .control_chart import ControlChartFigure
13
+ from .effects_charts import (
14
+ create_factor_effects_chart,
15
+ create_factor_interaction_chart,
16
+ create_main_effects_chart,
17
+ create_time_effects_chart,
18
+ create_time_interaction_chart,
19
+ )
20
+
21
+ # Redundant alias marks this as a deliberate re-export that is intentionally
22
+ # absent from __all__ — the convention both ruff and type checkers understand.
23
+ from .plotter import Plotter as Plotter
24
+ from .themes import ChartTheme, get_theme, list_themes, register_theme
25
+
26
+ __all__ = [
27
+ 'PlotError',
28
+ 'ControlChartFigure',
29
+ 'ChartTheme',
30
+ 'get_theme',
31
+ 'list_themes',
32
+ 'register_theme',
33
+ 'create_capability_chart',
34
+ 'create_main_effects_chart',
35
+ 'create_factor_effects_chart',
36
+ 'create_time_effects_chart',
37
+ 'create_time_interaction_chart',
38
+ 'create_factor_interaction_chart',
39
+ ]
40
+
41
+ # `Plotter` is imported above and stays importable for library internals
42
+ # (analysis_result, excel_exporter) and for anyone who reaches for it
43
+ # deliberately. It is deliberately absent from __all__: it was already labelled
44
+ # internal, and listing an internal class in the star-import surface is how it
45
+ # becomes a semver commitment by accident.
46
+
@@ -0,0 +1,121 @@
1
+ """Pure Python normal distribution functions (scipy replacement)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+
7
+
8
+ def normal_pdf(x: float, mu: float = 0.0, sigma: float = 1.0) -> float:
9
+ """
10
+ Calculate normal probability density function.
11
+
12
+ Pure Python implementation to avoid scipy dependency.
13
+
14
+ Parameters
15
+ ----------
16
+ x : float
17
+ Point at which to evaluate the PDF
18
+ mu : float, default 0.0
19
+ Mean of the distribution
20
+ sigma : float, default 1.0
21
+ Standard deviation of the distribution
22
+
23
+ Returns
24
+ -------
25
+ float
26
+ PDF value at x
27
+ """
28
+ coefficient = 1.0 / (sigma * math.sqrt(2.0 * math.pi))
29
+ exponent = -0.5 * ((x - mu) / sigma) ** 2
30
+ return coefficient * math.exp(exponent)
31
+
32
+
33
+ def normal_ppf(p: float) -> float:
34
+ """
35
+ Calculate normal percent point function (inverse CDF / quantile function).
36
+
37
+ Uses Acklam's algorithm for rational approximation to the
38
+ inverse cumulative normal distribution. Accurate to approximately
39
+ 1.15e-9 in absolute error for p in (0, 1).
40
+
41
+ Parameters
42
+ ----------
43
+ p : float
44
+ Probability value in (0, 1)
45
+
46
+ Returns
47
+ -------
48
+ float
49
+ Quantile (z-score) corresponding to probability p
50
+
51
+ References
52
+ ----------
53
+ Acklam, P. J. (2010). An algorithm for computing the inverse normal
54
+ cumulative distribution function.
55
+ https://web.archive.org/web/20151030215612/http://home.online.no/~pjacklam/notes/invnorm/
56
+ """
57
+ # Coefficients for rational approximation
58
+ a = [
59
+ -3.969683028665376e01,
60
+ 2.209460984245205e02,
61
+ -2.759285104469687e02,
62
+ 1.383577518672690e02,
63
+ -3.066479806614716e01,
64
+ 2.506628277459239e00,
65
+ ]
66
+ b = [
67
+ -5.447609879822406e01,
68
+ 1.615858368580409e02,
69
+ -1.556989798598866e02,
70
+ 6.680131188771972e01,
71
+ -1.328068155288572e01,
72
+ ]
73
+ c = [
74
+ -7.784894002430293e-03,
75
+ -3.223964580411365e-01,
76
+ -2.400758277161838e00,
77
+ -2.549732539343734e00,
78
+ 4.374664141464968e00,
79
+ 2.938163982698783e00,
80
+ ]
81
+ d = [
82
+ 7.784695709041462e-03,
83
+ 3.224671290700398e-01,
84
+ 2.445134137142996e00,
85
+ 3.754408661907416e00,
86
+ ]
87
+
88
+ # Define break-points
89
+ p_low = 0.02425
90
+ p_high = 1 - p_low
91
+
92
+ if p <= 0 or p >= 1:
93
+ if p == 0:
94
+ return float('-inf')
95
+ elif p == 1:
96
+ return float('inf')
97
+ else:
98
+ raise ValueError(f'p must be in (0, 1), got {p}')
99
+
100
+ # Rational approximation for lower region
101
+ if p < p_low:
102
+ q = math.sqrt(-2 * math.log(p))
103
+ return (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) / (
104
+ (((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1
105
+ )
106
+
107
+ # Rational approximation for upper region
108
+ if p > p_high:
109
+ q = math.sqrt(-2 * math.log(1 - p))
110
+ return -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) / (
111
+ (((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1
112
+ )
113
+
114
+ # Rational approximation for central region
115
+ q = p - 0.5
116
+ r = q * q
117
+ return (
118
+ (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5])
119
+ * q
120
+ / (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1)
121
+ )