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,232 @@
1
+ """
2
+ Signal detection result container.
3
+
4
+ Provides multiple ways to access and analyze detected violations.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from typing import TYPE_CHECKING
11
+
12
+ import pandas as pd
13
+
14
+ if TYPE_CHECKING:
15
+ from typing import Any
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class SignalResult:
21
+ """
22
+ Comprehensive signal detection results.
23
+
24
+ Provides multiple ways to access and analyze violations:
25
+ - By rule
26
+ - By observation
27
+ - Summary statistics
28
+ - Export options
29
+
30
+ Examples
31
+ --------
32
+ >>> signals = result.detect_signals()
33
+ >>> print(signals.summary)
34
+ >>> signals.by_rule['rule_2'] # Rule 2 violations
35
+ >>> signals.flagged_observations # Set of obs_ids
36
+ >>> signals.to_excel('violations.xlsx')
37
+ """
38
+
39
+ def __init__(self, violations: pd.DataFrame, chart_name: str, data: pd.DataFrame, stats: dict,
40
+ rules_skipped: dict[str, str] | None = None):
41
+ self.violations = violations
42
+ self.chart_name = chart_name
43
+ self.data = data
44
+ self.stats = stats
45
+ # Applicable rules that could NOT be evaluated because the group had too few
46
+ # points (rule_name -> reason). Lets callers distinguish "no signals found"
47
+ # from "not fully evaluated" (e.g. a small stratified subgroup).
48
+ self.rules_skipped = rules_skipped or {}
49
+
50
+ @property
51
+ def count(self) -> int:
52
+ """Total number of violations detected."""
53
+ return len(self.violations)
54
+
55
+ @property
56
+ def has_signals(self) -> bool:
57
+ """Whether any signals were detected."""
58
+ return self.count > 0
59
+
60
+ @property
61
+ def is_partial(self) -> bool:
62
+ """True when some applicable rules were skipped for too few observations."""
63
+ return bool(self.rules_skipped)
64
+
65
+ @property
66
+ def evaluation_status(self) -> str:
67
+ """'complete' when every applicable rule ran, else 'partial'."""
68
+ return 'partial' if self.rules_skipped else 'complete'
69
+
70
+ @property
71
+ def flagged_observations(self) -> set:
72
+ """Set of observation IDs that violated any rule."""
73
+ if self.violations.empty:
74
+ return set()
75
+ return set(self.violations['obs_id'].unique())
76
+
77
+ @property
78
+ def by_rule(self) -> dict[str, pd.DataFrame]:
79
+ """Group violations by rule."""
80
+ if self.violations.empty:
81
+ return {}
82
+
83
+ return {rule: group for rule, group in self.violations.groupby('rule_name', observed=True)}
84
+
85
+ @property
86
+ def by_observation(self) -> dict[Any, pd.DataFrame]:
87
+ """Group violations by observation ID."""
88
+ if self.violations.empty:
89
+ return {}
90
+
91
+ return {obs_id: group for obs_id, group in self.violations.groupby('obs_id', observed=True)}
92
+
93
+ def get_rule_violations(self, rule_name: str) -> pd.DataFrame:
94
+ """
95
+ Get all violations for a specific rule.
96
+
97
+ Parameters
98
+ ----------
99
+ rule_name : str
100
+ Name of the rule (e.g., 'rule_1')
101
+
102
+ Returns
103
+ -------
104
+ DataFrame
105
+ Violations for the specified rule
106
+ """
107
+ if self.violations.empty:
108
+ return pd.DataFrame()
109
+
110
+ return self.violations[self.violations['rule_name'] == rule_name].copy()
111
+
112
+ def get_observation_violations(self, obs_id: Any) -> pd.DataFrame:
113
+ """
114
+ Get all rule violations for a specific observation.
115
+
116
+ Parameters
117
+ ----------
118
+ obs_id : any
119
+ Observation identifier
120
+
121
+ Returns
122
+ -------
123
+ DataFrame
124
+ All rules violated by this observation
125
+ """
126
+ if self.violations.empty:
127
+ return pd.DataFrame()
128
+
129
+ return self.violations[self.violations['obs_id'] == obs_id].copy()
130
+
131
+ @property
132
+ def summary(self) -> str:
133
+ """Human-readable summary of detected signals."""
134
+ if not self.has_signals:
135
+ return f'✓ No signals detected in {self.chart_name}'
136
+
137
+ lines = [
138
+ f'\n{"=" * 70}',
139
+ f'Signal Detection Summary: {self.chart_name}',
140
+ f'{"=" * 70}',
141
+ f'Total violations: {self.count}',
142
+ f'Flagged observations: {len(self.flagged_observations)}',
143
+ '',
144
+ ]
145
+
146
+ # Breakdown by rule
147
+ rule_counts = self.violations['rule_name'].value_counts()
148
+ lines.append('Violations by rule:')
149
+ for rule, count in rule_counts.items():
150
+ lines.append(f' {rule}: {count}')
151
+
152
+ # Show first few violations
153
+ lines.append('\nFirst violations:')
154
+ for _, row in self.violations.head(5).iterrows():
155
+ lines.append(f' • Obs {row["obs_id"]}: {row["description"]} (value={row["value"]:.3f})')
156
+
157
+ if self.count > 5:
158
+ lines.append(f' ... and {self.count - 5} more')
159
+
160
+ lines.append(f'\n{"=" * 70}\n')
161
+ return '\n'.join(lines)
162
+
163
+ def to_dataframe(self) -> pd.DataFrame:
164
+ """
165
+ Get violations as DataFrame.
166
+
167
+ Returns
168
+ -------
169
+ DataFrame
170
+ Copy of violations DataFrame
171
+ """
172
+ return self.violations.copy()
173
+
174
+ def to_excel(self, filepath: str):
175
+ """
176
+ Export violations to Excel.
177
+
178
+ Creates two sheets:
179
+ - Violations: Detailed violation records
180
+ - Summary: High-level statistics
181
+
182
+ Parameters
183
+ ----------
184
+ filepath : str
185
+ Path to output Excel file
186
+
187
+ Examples
188
+ --------
189
+ >>> signals.to_excel('violations.xlsx')
190
+ """
191
+ if self.violations.empty:
192
+ logger.warning('No violations to export')
193
+ return
194
+
195
+ with pd.ExcelWriter(filepath, engine='openpyxl') as writer:
196
+ # Violations sheet
197
+ self.violations.to_excel(writer, sheet_name='Violations', index=False)
198
+
199
+ # Summary sheet
200
+ summary_data = {
201
+ 'Metric': ['Total Violations', 'Flagged Observations', 'Chart Name'],
202
+ 'Value': [self.count, len(self.flagged_observations), self.chart_name],
203
+ }
204
+ pd.DataFrame(summary_data).to_excel(writer, sheet_name='Summary', index=False)
205
+
206
+ logger.info(f'✓ Exported violations to: {filepath}')
207
+
208
+ def to_json(self, filepath: str):
209
+ """
210
+ Export violations to JSON.
211
+
212
+ Parameters
213
+ ----------
214
+ filepath : str
215
+ Path to output JSON file
216
+
217
+ Examples
218
+ --------
219
+ >>> signals.to_json('violations.json')
220
+ """
221
+ self.violations.to_json(filepath, orient='records', indent=2)
222
+ logger.info(f'✓ Exported violations to: {filepath}')
223
+
224
+ def __repr__(self):
225
+ return (
226
+ f'SignalResult(violations={self.count}, '
227
+ f'flagged_obs={len(self.flagged_observations)}, '
228
+ f"chart='{self.chart_name}')"
229
+ )
230
+
231
+ def __str__(self):
232
+ return self.summary