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,14 @@
1
+ """
2
+ Signal detection framework for control charts.
3
+
4
+ Provides flexible, declarative API for detecting Western Electric rules
5
+ and custom patterns in process behavior data.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .config import RuleSet, SignalConfig, ZoneDefinition
11
+ from .detector import SignalDetector
12
+ from .result import SignalResult
13
+
14
+ __all__ = ['SignalConfig', 'ZoneDefinition', 'RuleSet', 'SignalDetector', 'SignalResult']
@@ -0,0 +1,435 @@
1
+ """
2
+ Configuration classes for signal detection.
3
+
4
+ Provides flexible configuration through dataclasses and fluent API.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Callable
10
+ from dataclasses import dataclass, field
11
+ from typing import Literal
12
+
13
+ from ..exceptions import ValidationError
14
+
15
+ # Chart-type-based rule defaults
16
+ # ================================
17
+ # WECO rules 2-8 require sequential/temporal ordering of observations.
18
+ # - Xbar/S: Categorical comparison of subgroups → Only Rule 1 applies
19
+ # - X/mR: Sequential observations over time → All rules apply
20
+ # Easy to extend for new chart types (CUSUM, EWMA, etc.)
21
+ CHART_TYPE_RULES = {
22
+ 'Xbar': ['rule_1'], # Rational subgroup means (categorical)
23
+ 'S': ['rule_1'], # Rational subgroup variation (categorical)
24
+ 'X': ['rule_1', 'rule_2', 'rule_3', 'rule_4', 'rule_5', 'rule_6', 'rule_7', 'rule_8'],
25
+ 'mR': ['rule_1', 'rule_2', 'rule_3', 'rule_4', 'rule_5', 'rule_6', 'rule_7', 'rule_8'],
26
+ }
27
+
28
+
29
+ @dataclass
30
+ class ZoneDefinition:
31
+ """
32
+ Zone boundary definition in standard deviations from centerline.
33
+
34
+ Zones are defined as multiples of sigma (standard deviation):
35
+ - Zone A: 2σ to 3σ from centerline
36
+ - Zone B: 1σ to 2σ from centerline
37
+ - Zone C: 0σ to 1σ from centerline
38
+
39
+ Examples
40
+ --------
41
+ Default zones:
42
+
43
+ >>> zones = ZoneDefinition()
44
+ >>> zones.A # (2.0, 3.0)
45
+
46
+ Custom zones:
47
+
48
+ >>> zones = ZoneDefinition(A=(2.5, 3.5), B=(1.5, 2.5))
49
+ """
50
+
51
+ A: tuple[float, float] = (2.0, 3.0)
52
+ B: tuple[float, float] = (1.0, 2.0)
53
+ C: tuple[float, float] = (0.0, 1.0)
54
+
55
+ def get_boundaries(self, center: float, sigma: float) -> dict:
56
+ """
57
+ Calculate actual zone boundaries for given center and sigma.
58
+
59
+ Parameters
60
+ ----------
61
+ center : float
62
+ Centerline value
63
+ sigma : float
64
+ Standard deviation
65
+
66
+ Returns
67
+ -------
68
+ dict
69
+ Zone boundaries with keys like 'A_upper', 'A_lower', etc.
70
+ """
71
+ return {
72
+ 'A_upper': (center + self.A[0] * sigma, center + self.A[1] * sigma),
73
+ 'A_lower': (center - self.A[1] * sigma, center - self.A[0] * sigma),
74
+ 'B_upper': (center + self.B[0] * sigma, center + self.B[1] * sigma),
75
+ 'B_lower': (center - self.B[1] * sigma, center - self.B[0] * sigma),
76
+ 'C_upper': (center + self.C[0] * sigma, center + self.C[1] * sigma),
77
+ 'C_lower': (center - self.C[1] * sigma, center - self.C[0] * sigma),
78
+ }
79
+
80
+
81
+ @dataclass
82
+ class SignalConfig:
83
+ """
84
+ Configuration for signal detection.
85
+
86
+ Provides flexible control over which rules to apply and how to apply them.
87
+
88
+ Parameters
89
+ ----------
90
+ enabled_rules : list or str, default 'default'
91
+ Which rules to apply:
92
+ - 'default': Use chart-type-based defaults (Xbar/S: Rule 1, X/mR: Rules 1-8)
93
+ - 'standard': Rules 1-4 (filtered by chart type)
94
+ - 'extended': Rules 1-8 (filtered by chart type)
95
+ - 'all': Same as extended
96
+ - List of rule names: ['rule_1', 'rule_2', ...] (explicit override)
97
+ zone_definition : ZoneDefinition, optional
98
+ Custom zone definitions
99
+ min_observations : int, default 20
100
+ Minimum observations required
101
+ ignore_first_n : int, default 0
102
+ Ignore first N observations (startup period)
103
+ ignore_last_n : int, default 0
104
+ Ignore last N observations
105
+ use_vectorized : bool, default True
106
+ Use vectorized operations (faster)
107
+
108
+ Examples
109
+ --------
110
+ Standard configuration:
111
+
112
+ >>> config = SignalConfig(enabled_rules='standard')
113
+
114
+ Custom configuration:
115
+
116
+ >>> config = SignalConfig(
117
+ ... enabled_rules=['rule_1', 'rule_2', 'rule_5'],
118
+ ... zone_definition=ZoneDefinition(A=(2.5, 3.5)),
119
+ ... min_observations=30
120
+ ... )
121
+
122
+ Extended rules:
123
+
124
+ >>> config = SignalConfig(enabled_rules='extended')
125
+ """
126
+
127
+ enabled_rules: list[str] | Literal['default', 'standard', 'extended', 'all'] = 'default'
128
+ zone_definition: ZoneDefinition = field(default_factory=ZoneDefinition)
129
+ min_observations: int = 20
130
+ ignore_first_n: int = 0
131
+ ignore_last_n: int = 0
132
+ use_vectorized: bool = True
133
+
134
+ def __post_init__(self):
135
+ """Resolve rule presets to actual rule lists."""
136
+ if isinstance(self.enabled_rules, str) and self.enabled_rules != 'default':
137
+ # Resolve presets except 'default' (resolved per chart type)
138
+ self.enabled_rules = self._resolve_preset(self.enabled_rules)
139
+
140
+ def _resolve_preset(self, preset: str) -> list[str]:
141
+ """Convert preset name to rule list."""
142
+ presets = {
143
+ 'standard': ['rule_1', 'rule_2', 'rule_3', 'rule_4'],
144
+ 'extended': [f'rule_{i}' for i in range(1, 9)],
145
+ 'all': [f'rule_{i}' for i in range(1, 9)],
146
+ }
147
+
148
+ if preset not in presets:
149
+ raise ValidationError(f"Unknown preset '{preset}'.\\nValid presets: {list(presets.keys())}")
150
+
151
+ return presets[preset]
152
+
153
+ def get_rules_for_chart(self, chart_type: str) -> list[str]:
154
+ """
155
+ Get applicable rules for a chart type.
156
+
157
+ Uses chart-type-based defaults or applies filtering to user-specified rules.
158
+
159
+ Parameters
160
+ ----------
161
+ chart_type : str
162
+ Chart type ('Xbar', 'S', 'X', 'mR')
163
+
164
+ Returns
165
+ -------
166
+ list of str
167
+ Rule names to apply
168
+
169
+ Examples
170
+ --------
171
+ Use defaults:
172
+
173
+ >>> config = SignalConfig(enabled_rules='default')
174
+ >>> config.get_rules_for_chart('Xbar')
175
+ ['rule_1']
176
+ >>> config.get_rules_for_chart('X')
177
+ ['rule_1', 'rule_2', ..., 'rule_8']
178
+
179
+ Explicit rules (no filtering):
180
+
181
+ >>> config = SignalConfig(enabled_rules=['rule_1', 'rule_2'])
182
+ >>> config.get_rules_for_chart('Xbar')
183
+ ['rule_1', 'rule_2']
184
+ """
185
+ if isinstance(self.enabled_rules, list):
186
+ # User provided explicit list - use as-is
187
+ return self.enabled_rules
188
+
189
+ if self.enabled_rules == 'default':
190
+ # Use chart-type defaults
191
+ return CHART_TYPE_RULES.get(chart_type, ['rule_1'])
192
+
193
+ # This shouldn't happen (already resolved in __post_init__)
194
+ # but handle just in case
195
+ raise ValidationError(f"Invalid enabled_rules state: {self.enabled_rules}. Expected list or 'default'.")
196
+
197
+
198
+ class RuleSet:
199
+ """
200
+ Fluent API for building rule configurations.
201
+
202
+ Provides a chainable interface for configuring detection rules.
203
+
204
+ Examples
205
+ --------
206
+ Build a custom rule set:
207
+
208
+ >>> rules = (
209
+ ... RuleSet()
210
+ ... .beyond_limits()
211
+ ... .zone_a(consecutive=2, window=3)
212
+ ... .trend(length=6, direction='both')
213
+ ... )
214
+
215
+ Convert to list:
216
+
217
+ >>> rule_list = rules.get_rules()
218
+ >>> print(rule_list)
219
+ ['rule_1', 'rule_2', 'rule_5']
220
+ """
221
+
222
+ def __init__(self):
223
+ self._rules: list[str] = []
224
+ self._parameters: dict[str, dict] = {}
225
+
226
+ def beyond_limits(self) -> RuleSet:
227
+ """
228
+ Add Rule 1: Points beyond control limits.
229
+
230
+ Detects any point beyond ±3σ from centerline.
231
+
232
+ Returns
233
+ -------
234
+ RuleSet
235
+ Self for chaining
236
+ """
237
+ self._rules.append('rule_1')
238
+ return self
239
+
240
+ def zone_a(self, consecutive: int = 2, window: int = 3) -> RuleSet:
241
+ """
242
+ Add Rule 2: Consecutive points in Zone A.
243
+
244
+ Detects when 2 out of 3 consecutive points fall in Zone A
245
+ (between 2σ and 3σ from centerline).
246
+
247
+ Parameters
248
+ ----------
249
+ consecutive : int, default 2
250
+ Number of points that must be in zone
251
+ window : int, default 3
252
+ Window size to check
253
+
254
+ Returns
255
+ -------
256
+ RuleSet
257
+ Self for chaining
258
+ """
259
+ self._rules.append('rule_2')
260
+ self._parameters['rule_2'] = {'consecutive': consecutive, 'window': window}
261
+ return self
262
+
263
+ def zone_b(self, consecutive: int = 4, window: int = 5) -> RuleSet:
264
+ """
265
+ Add Rule 3: Consecutive points in Zone B or beyond.
266
+
267
+ Detects when 4 out of 5 consecutive points fall in Zone B or beyond
268
+ (beyond 1σ from centerline).
269
+
270
+ Parameters
271
+ ----------
272
+ consecutive : int, default 4
273
+ Number of points that must be in zone
274
+ window : int, default 5
275
+ Window size to check
276
+
277
+ Returns
278
+ -------
279
+ RuleSet
280
+ Self for chaining
281
+ """
282
+ self._rules.append('rule_3')
283
+ self._parameters['rule_3'] = {'consecutive': consecutive, 'window': window}
284
+ return self
285
+
286
+ def run(self, length: int = 8) -> RuleSet:
287
+ """
288
+ Add Rule 4: Run above or below centerline.
289
+
290
+ Detects when 8 or more consecutive points fall on the same side
291
+ of the centerline.
292
+
293
+ Parameters
294
+ ----------
295
+ length : int, default 8
296
+ Number of consecutive points required
297
+
298
+ Returns
299
+ -------
300
+ RuleSet
301
+ Self for chaining
302
+ """
303
+ self._rules.append('rule_4')
304
+ self._parameters['rule_4'] = {'length': length}
305
+ return self
306
+
307
+ def trend(self, length: int = 6, direction: Literal['up', 'down', 'both'] = 'both') -> RuleSet:
308
+ """
309
+ Add Rule 5: Trending points.
310
+
311
+ Detects when 6 or more consecutive points are steadily
312
+ increasing or decreasing.
313
+
314
+ Parameters
315
+ ----------
316
+ length : int, default 6
317
+ Number of consecutive points required
318
+ direction : {'up', 'down', 'both'}, default 'both'
319
+ Which direction trends to detect
320
+
321
+ Returns
322
+ -------
323
+ RuleSet
324
+ Self for chaining
325
+ """
326
+ self._rules.append('rule_5')
327
+ self._parameters['rule_5'] = {'length': length, 'direction': direction}
328
+ return self
329
+
330
+ def oscillation(self, length: int = 14) -> RuleSet:
331
+ """
332
+ Add Rule 6: Alternating pattern.
333
+
334
+ Detects when 14 or more consecutive points alternate up and down.
335
+
336
+ Parameters
337
+ ----------
338
+ length : int, default 14
339
+ Number of consecutive points required
340
+
341
+ Returns
342
+ -------
343
+ RuleSet
344
+ Self for chaining
345
+ """
346
+ self._rules.append('rule_6')
347
+ self._parameters['rule_6'] = {'length': length}
348
+ return self
349
+
350
+ def reduced_variation(self, length: int = 15) -> RuleSet:
351
+ """
352
+ Add Rule 7: Points in Zone C (low variation).
353
+
354
+ Detects when 15 or more consecutive points fall within Zone C
355
+ (within ±1σ of centerline), indicating reduced variation.
356
+
357
+ Parameters
358
+ ----------
359
+ length : int, default 15
360
+ Number of consecutive points required
361
+
362
+ Returns
363
+ -------
364
+ RuleSet
365
+ Self for chaining
366
+ """
367
+ self._rules.append('rule_7')
368
+ self._parameters['rule_7'] = {'length': length}
369
+ return self
370
+
371
+ def avoiding_center(self, length: int = 8) -> RuleSet:
372
+ """
373
+ Add Rule 8: Points avoiding Zone C.
374
+
375
+ Detects when 8 or more consecutive points fall outside Zone C
376
+ (beyond ±1σ from centerline).
377
+
378
+ Parameters
379
+ ----------
380
+ length : int, default 8
381
+ Number of consecutive points required
382
+
383
+ Returns
384
+ -------
385
+ RuleSet
386
+ Self for chaining
387
+ """
388
+ self._rules.append('rule_8')
389
+ self._parameters['rule_8'] = {'length': length}
390
+ return self
391
+
392
+ def custom(self, name: str, detector: Callable, min_observations: int = 1) -> RuleSet:
393
+ """
394
+ Add a custom detection rule.
395
+
396
+ Parameters
397
+ ----------
398
+ name : str
399
+ Rule name
400
+ detector : callable
401
+ Detection function with signature:
402
+ detector(data: DataFrame, stats: dict, value_col: str) -> Series[bool]
403
+ min_observations : int, default 1
404
+ Minimum observations required
405
+
406
+ Returns
407
+ -------
408
+ RuleSet
409
+ Self for chaining
410
+ """
411
+ self._rules.append(name)
412
+ self._parameters[name] = {'detector': detector, 'min_observations': min_observations}
413
+ return self
414
+
415
+ def to_config(self) -> SignalConfig:
416
+ """
417
+ Convert to SignalConfig.
418
+
419
+ Returns
420
+ -------
421
+ SignalConfig
422
+ Configuration object
423
+ """
424
+ return SignalConfig(enabled_rules=self._rules)
425
+
426
+ def get_rules(self) -> list[str]:
427
+ """
428
+ Get list of enabled rules.
429
+
430
+ Returns
431
+ -------
432
+ list of str
433
+ Rule names
434
+ """
435
+ return self._rules.copy()