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,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
|