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,640 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Effects visualization charts for ProcessBehavior analysis.
|
|
3
|
+
|
|
4
|
+
This module provides chart functions for visualizing main effects and interactions:
|
|
5
|
+
- create_main_effects_chart: Horizontal bar chart of all main effects (combined)
|
|
6
|
+
- create_factor_effects_chart: Vertical bar chart of factor main effects only
|
|
7
|
+
- create_time_effects_chart: Horizontal bar chart of time effects only
|
|
8
|
+
- create_time_interaction_chart: Line plot of factor × time interaction
|
|
9
|
+
- create_factor_interaction_chart: Line chart for factor × factor interaction
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
import pandas as pd
|
|
19
|
+
import plotly.graph_objects as go
|
|
20
|
+
|
|
21
|
+
from processbehavior.exceptions import ChartNotAvailableError, ValidationError
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from .themes import ChartTheme
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
_FACTOR_COLORS = [
|
|
29
|
+
'#FF6B6B', # Coral
|
|
30
|
+
'#4ECDC4', # Teal
|
|
31
|
+
'#45B7D1', # Sky blue
|
|
32
|
+
'#96CEB4', # Sage
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
_INTERACTION_COLORS = _FACTOR_COLORS + [
|
|
36
|
+
'#FFEAA7', # Soft yellow
|
|
37
|
+
'#DDA0DD', # Plum
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _is_main_effect_key(name: str) -> bool:
|
|
42
|
+
"""Check if an effects dict key represents a main effect.
|
|
43
|
+
|
|
44
|
+
Convention: main effect keys end with '_MEs' suffix or are
|
|
45
|
+
exactly 'main_effect'. All other keys are filtered out when
|
|
46
|
+
building main effects charts.
|
|
47
|
+
|
|
48
|
+
See: EffectsCalculator output key naming.
|
|
49
|
+
"""
|
|
50
|
+
return name.endswith('_MEs') or name == 'main_effect'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def create_main_effects_chart(
|
|
54
|
+
effects: Mapping[str, Any],
|
|
55
|
+
theme: ChartTheme,
|
|
56
|
+
factors: list[str] | None = None,
|
|
57
|
+
width: int = 1000,
|
|
58
|
+
height: int | None = None,
|
|
59
|
+
) -> go.Figure:
|
|
60
|
+
"""
|
|
61
|
+
Create horizontal bar chart showing all main effects.
|
|
62
|
+
|
|
63
|
+
Displays main effect magnitudes for each factor level, with factors
|
|
64
|
+
grouped together. Includes a vertical reference line at 0.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
effects : dict
|
|
69
|
+
Effects dictionary from result.effects containing factor main effects.
|
|
70
|
+
Expected keys: factor names (e.g., 'Lane', 'Phase') mapping to DataFrames
|
|
71
|
+
with columns [factor_name, 'Main_Effect']
|
|
72
|
+
theme : ChartTheme
|
|
73
|
+
Visual theme for styling
|
|
74
|
+
factors : list of str, optional
|
|
75
|
+
Specific factors to include. If None, includes all factors.
|
|
76
|
+
width : int, default 1000
|
|
77
|
+
Figure width in pixels
|
|
78
|
+
height : int, optional
|
|
79
|
+
Figure height in pixels. Auto-calculated if None.
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
go.Figure
|
|
84
|
+
Plotly figure with horizontal bar chart
|
|
85
|
+
|
|
86
|
+
Raises
|
|
87
|
+
------
|
|
88
|
+
ValueError
|
|
89
|
+
If no main effects are found in the ``effects`` dictionary.
|
|
90
|
+
|
|
91
|
+
Examples
|
|
92
|
+
--------
|
|
93
|
+
>>> fig = create_main_effects_chart(result.effects, theme)
|
|
94
|
+
>>> fig.show()
|
|
95
|
+
"""
|
|
96
|
+
# Find factor effects (DataFrames with 'Main_Effect' column)
|
|
97
|
+
factor_effects = []
|
|
98
|
+
time_effects = None
|
|
99
|
+
|
|
100
|
+
for name, data in effects.items():
|
|
101
|
+
if not isinstance(data, pd.DataFrame):
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
# Skip non-effect DataFrames (MEs scores, etc.)
|
|
105
|
+
if _is_main_effect_key(name):
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
if 'Main_Effect' in data.columns and (factors is None or name in factors):
|
|
109
|
+
factor_effects.append((name, data))
|
|
110
|
+
elif 'PT_ME' in data.columns:
|
|
111
|
+
time_effects = (name, data)
|
|
112
|
+
|
|
113
|
+
if not factor_effects and time_effects is None:
|
|
114
|
+
raise ChartNotAvailableError(
|
|
115
|
+
f'No main effects found to plot.\nAvailable effects keys: {list(effects.keys())}',
|
|
116
|
+
chart='main_effects',
|
|
117
|
+
available=list(effects.keys()),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Build combined data for horizontal bar chart
|
|
121
|
+
all_labels = []
|
|
122
|
+
all_values = []
|
|
123
|
+
all_colors = []
|
|
124
|
+
|
|
125
|
+
# Color palette for factors
|
|
126
|
+
factor_colors = [theme.data_color, theme.center_color] + _FACTOR_COLORS
|
|
127
|
+
|
|
128
|
+
for i, (factor_name, data) in enumerate(factor_effects):
|
|
129
|
+
factor_col = data.columns[0]
|
|
130
|
+
color = factor_colors[i % len(factor_colors)]
|
|
131
|
+
|
|
132
|
+
for _, row in data.iterrows():
|
|
133
|
+
label = f'{factor_name}: {row[factor_col]}'
|
|
134
|
+
all_labels.append(label)
|
|
135
|
+
all_values.append(row['Main_Effect'])
|
|
136
|
+
all_colors.append(color)
|
|
137
|
+
|
|
138
|
+
# Add time effects if present
|
|
139
|
+
if time_effects is not None:
|
|
140
|
+
name, data = time_effects
|
|
141
|
+
time_col = data.columns[0]
|
|
142
|
+
for _, row in data.iterrows():
|
|
143
|
+
label = f'Time: {row[time_col]}'
|
|
144
|
+
all_labels.append(label)
|
|
145
|
+
all_values.append(row['PT_ME'])
|
|
146
|
+
all_colors.append(theme.pattern_signal_color)
|
|
147
|
+
|
|
148
|
+
# Calculate height if not specified
|
|
149
|
+
if height is None:
|
|
150
|
+
height = max(400, 25 * len(all_labels) + 100)
|
|
151
|
+
|
|
152
|
+
# Create horizontal bar chart
|
|
153
|
+
fig = go.Figure()
|
|
154
|
+
|
|
155
|
+
fig.add_trace(
|
|
156
|
+
go.Bar(
|
|
157
|
+
y=all_labels,
|
|
158
|
+
x=all_values,
|
|
159
|
+
orientation='h',
|
|
160
|
+
marker_color=all_colors,
|
|
161
|
+
text=[f'{v:.3f}' for v in all_values],
|
|
162
|
+
textposition='outside',
|
|
163
|
+
hovertemplate='%{y}<br>Effect: %{x:.4f}<extra></extra>',
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Add vertical line at 0
|
|
168
|
+
fig.add_vline(x=0, line_dash='dash', line_color='gray', line_width=1)
|
|
169
|
+
|
|
170
|
+
fig.update_layout(
|
|
171
|
+
title='Main Effects',
|
|
172
|
+
xaxis_title='Effect Magnitude',
|
|
173
|
+
yaxis_title='',
|
|
174
|
+
width=width,
|
|
175
|
+
height=height,
|
|
176
|
+
yaxis=dict(autorange='reversed'), # Top-to-bottom order
|
|
177
|
+
showlegend=False,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return fig
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def create_factor_effects_chart(
|
|
184
|
+
effects: Mapping[str, Any],
|
|
185
|
+
theme: ChartTheme,
|
|
186
|
+
factors: list[str] | None = None,
|
|
187
|
+
width: int = 1000,
|
|
188
|
+
height: int = 500,
|
|
189
|
+
) -> go.Figure:
|
|
190
|
+
"""
|
|
191
|
+
Create vertical bar chart showing factor main effects only.
|
|
192
|
+
|
|
193
|
+
Displays main effect magnitudes for each factor level, with factors
|
|
194
|
+
grouped together. Excludes time effects. Includes a horizontal
|
|
195
|
+
reference line at 0.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
effects : dict
|
|
200
|
+
Effects dictionary from result.effects containing factor main effects.
|
|
201
|
+
Expected keys: factor names (e.g., 'Lane', 'Phase') mapping to DataFrames
|
|
202
|
+
with columns [factor_name, 'Main_Effect']
|
|
203
|
+
theme : ChartTheme
|
|
204
|
+
Visual theme for styling
|
|
205
|
+
factors : list of str, optional
|
|
206
|
+
Specific factors to include. If None, includes all factors.
|
|
207
|
+
width : int, default 1000
|
|
208
|
+
Figure width in pixels
|
|
209
|
+
height : int, default 500
|
|
210
|
+
Figure height in pixels
|
|
211
|
+
|
|
212
|
+
Returns
|
|
213
|
+
-------
|
|
214
|
+
go.Figure
|
|
215
|
+
Plotly figure with vertical bar chart
|
|
216
|
+
|
|
217
|
+
Raises
|
|
218
|
+
------
|
|
219
|
+
ValueError
|
|
220
|
+
If no factor main effects are found in the ``effects`` dictionary.
|
|
221
|
+
|
|
222
|
+
Examples
|
|
223
|
+
--------
|
|
224
|
+
>>> fig = create_factor_effects_chart(result.effects, theme)
|
|
225
|
+
>>> fig.show()
|
|
226
|
+
"""
|
|
227
|
+
# Find factor effects (DataFrames with 'Main_Effect' column)
|
|
228
|
+
factor_effects = []
|
|
229
|
+
|
|
230
|
+
for name, data in effects.items():
|
|
231
|
+
if not isinstance(data, pd.DataFrame):
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
# Skip non-effect DataFrames (MEs scores, etc.)
|
|
235
|
+
if _is_main_effect_key(name):
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
if 'Main_Effect' in data.columns and (factors is None or name in factors):
|
|
239
|
+
factor_effects.append((name, data))
|
|
240
|
+
|
|
241
|
+
if not factor_effects:
|
|
242
|
+
raise ChartNotAvailableError(
|
|
243
|
+
f'No factor main effects found to plot.\nAvailable effects keys: {list(effects.keys())}',
|
|
244
|
+
chart='factor_effects',
|
|
245
|
+
available=list(effects.keys()),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Build combined data for vertical bar chart
|
|
249
|
+
all_labels = []
|
|
250
|
+
all_values = []
|
|
251
|
+
all_colors = []
|
|
252
|
+
|
|
253
|
+
# Color palette for factors
|
|
254
|
+
factor_colors = [theme.data_color, theme.center_color] + _FACTOR_COLORS
|
|
255
|
+
|
|
256
|
+
for i, (factor_name, data) in enumerate(factor_effects):
|
|
257
|
+
factor_col = data.columns[0]
|
|
258
|
+
color = factor_colors[i % len(factor_colors)]
|
|
259
|
+
|
|
260
|
+
for _, row in data.iterrows():
|
|
261
|
+
label = f'{factor_name}: {row[factor_col]}'
|
|
262
|
+
all_labels.append(label)
|
|
263
|
+
all_values.append(row['Main_Effect'])
|
|
264
|
+
all_colors.append(color)
|
|
265
|
+
|
|
266
|
+
# Create vertical bar chart
|
|
267
|
+
fig = go.Figure()
|
|
268
|
+
|
|
269
|
+
fig.add_trace(
|
|
270
|
+
go.Bar(
|
|
271
|
+
x=all_labels,
|
|
272
|
+
y=all_values,
|
|
273
|
+
marker_color=all_colors,
|
|
274
|
+
text=[f'{v:.3f}' for v in all_values],
|
|
275
|
+
textposition='outside',
|
|
276
|
+
hovertemplate='%{x}<br>Effect: %{y:.4f}<extra></extra>',
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Add horizontal line at 0
|
|
281
|
+
fig.add_hline(y=0, line_dash='dash', line_color='gray', line_width=1)
|
|
282
|
+
|
|
283
|
+
fig.update_layout(
|
|
284
|
+
title='Factor Main Effects',
|
|
285
|
+
xaxis_title='Factor Level',
|
|
286
|
+
yaxis_title='Effect Magnitude',
|
|
287
|
+
width=width,
|
|
288
|
+
height=height,
|
|
289
|
+
showlegend=False,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
return fig
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def create_time_effects_chart(
|
|
296
|
+
effects: Mapping[str, Any], theme: ChartTheme, width: int = 1000, height: int | None = None
|
|
297
|
+
) -> go.Figure:
|
|
298
|
+
"""
|
|
299
|
+
Create horizontal bar chart showing time effects only.
|
|
300
|
+
|
|
301
|
+
Displays time effect magnitudes for each time period. Excludes factor
|
|
302
|
+
main effects. Includes a vertical reference line at 0.
|
|
303
|
+
|
|
304
|
+
Parameters
|
|
305
|
+
----------
|
|
306
|
+
effects : dict
|
|
307
|
+
Effects dictionary from result.effects containing time effects.
|
|
308
|
+
Expected key: time variable name mapping to DataFrame with columns
|
|
309
|
+
[time_var, 'PT_ME']
|
|
310
|
+
theme : ChartTheme
|
|
311
|
+
Visual theme for styling
|
|
312
|
+
width : int, default 1000
|
|
313
|
+
Figure width in pixels
|
|
314
|
+
height : int, optional
|
|
315
|
+
Figure height in pixels. Auto-calculated if None.
|
|
316
|
+
|
|
317
|
+
Returns
|
|
318
|
+
-------
|
|
319
|
+
go.Figure
|
|
320
|
+
Plotly figure with horizontal bar chart
|
|
321
|
+
|
|
322
|
+
Raises
|
|
323
|
+
------
|
|
324
|
+
ValueError
|
|
325
|
+
If no time effects (PT_ME — Period-Time Main Effect) are found
|
|
326
|
+
in the ``effects`` dictionary.
|
|
327
|
+
|
|
328
|
+
Examples
|
|
329
|
+
--------
|
|
330
|
+
>>> fig = create_time_effects_chart(result.effects, theme)
|
|
331
|
+
>>> fig.show()
|
|
332
|
+
"""
|
|
333
|
+
# Find time effects (DataFrame with 'PT_ME' column)
|
|
334
|
+
time_effects = None
|
|
335
|
+
|
|
336
|
+
for name, data in effects.items():
|
|
337
|
+
if not isinstance(data, pd.DataFrame):
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
if 'PT_ME' in data.columns:
|
|
341
|
+
time_effects = (name, data)
|
|
342
|
+
break
|
|
343
|
+
|
|
344
|
+
if time_effects is None:
|
|
345
|
+
raise ChartNotAvailableError(
|
|
346
|
+
'No time effects found to plot.\n'
|
|
347
|
+
'This requires a time variable in the analysis.\n'
|
|
348
|
+
f'Available effects keys: {list(effects.keys())}',
|
|
349
|
+
chart='time_effects',
|
|
350
|
+
available=list(effects.keys()),
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Build data for horizontal bar chart
|
|
354
|
+
all_labels = []
|
|
355
|
+
all_values = []
|
|
356
|
+
|
|
357
|
+
name, data = time_effects
|
|
358
|
+
time_col = data.columns[0]
|
|
359
|
+
for _, row in data.iterrows():
|
|
360
|
+
label = f'Time: {row[time_col]}'
|
|
361
|
+
all_labels.append(label)
|
|
362
|
+
all_values.append(row['PT_ME'])
|
|
363
|
+
|
|
364
|
+
# Calculate height if not specified
|
|
365
|
+
if height is None:
|
|
366
|
+
height = max(400, 25 * len(all_labels) + 100)
|
|
367
|
+
|
|
368
|
+
# Create horizontal bar chart
|
|
369
|
+
fig = go.Figure()
|
|
370
|
+
|
|
371
|
+
fig.add_trace(
|
|
372
|
+
go.Bar(
|
|
373
|
+
y=all_labels,
|
|
374
|
+
x=all_values,
|
|
375
|
+
orientation='h',
|
|
376
|
+
marker_color=theme.pattern_signal_color,
|
|
377
|
+
text=[f'{v:.3f}' for v in all_values],
|
|
378
|
+
textposition='outside',
|
|
379
|
+
hovertemplate='%{y}<br>Effect: %{x:.4f}<extra></extra>',
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Add vertical line at 0
|
|
384
|
+
fig.add_vline(x=0, line_dash='dash', line_color='gray', line_width=1)
|
|
385
|
+
|
|
386
|
+
fig.update_layout(
|
|
387
|
+
title='Time Effects',
|
|
388
|
+
xaxis_title='Effect Magnitude',
|
|
389
|
+
yaxis_title='',
|
|
390
|
+
width=width,
|
|
391
|
+
height=height,
|
|
392
|
+
yaxis=dict(autorange='reversed'), # Top-to-bottom order
|
|
393
|
+
showlegend=False,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
return fig
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def create_time_interaction_chart(
|
|
400
|
+
interactions: Mapping[str, Any],
|
|
401
|
+
effects: Mapping[str, Any],
|
|
402
|
+
factors: list[str],
|
|
403
|
+
time_var: str,
|
|
404
|
+
dataset: pd.DataFrame,
|
|
405
|
+
theme: ChartTheme,
|
|
406
|
+
width: int = 1000,
|
|
407
|
+
height: int = 500,
|
|
408
|
+
) -> go.Figure:
|
|
409
|
+
"""
|
|
410
|
+
Create line plot showing factor × time interaction.
|
|
411
|
+
|
|
412
|
+
Displays one line per factor level combination, with x-axis as time
|
|
413
|
+
and y-axis as the interaction effect. Non-parallel lines indicate
|
|
414
|
+
interaction between factors and time.
|
|
415
|
+
|
|
416
|
+
Parameters
|
|
417
|
+
----------
|
|
418
|
+
interactions : dict
|
|
419
|
+
Interactions dictionary from result.interactions.
|
|
420
|
+
Expected key: 'factor_time' mapping to Series of PDC values.
|
|
421
|
+
effects : dict
|
|
422
|
+
Effects dictionary from result.effects.
|
|
423
|
+
factors : list of str
|
|
424
|
+
Factor variable names
|
|
425
|
+
time_var : str
|
|
426
|
+
Time variable name
|
|
427
|
+
dataset : pd.DataFrame
|
|
428
|
+
Full analysis dataset with factor and time columns
|
|
429
|
+
theme : ChartTheme
|
|
430
|
+
Visual theme for styling
|
|
431
|
+
width : int, default 1000
|
|
432
|
+
Figure width in pixels
|
|
433
|
+
height : int, default 500
|
|
434
|
+
Figure height in pixels
|
|
435
|
+
|
|
436
|
+
Returns
|
|
437
|
+
-------
|
|
438
|
+
go.Figure
|
|
439
|
+
Plotly figure with line plot
|
|
440
|
+
|
|
441
|
+
Raises
|
|
442
|
+
------
|
|
443
|
+
ValueError
|
|
444
|
+
If ``'factor_time'`` key is missing from ``interactions``.
|
|
445
|
+
ValueError
|
|
446
|
+
If the PDC (Predictable Difference of Cell means) series length
|
|
447
|
+
does not match the dataset length.
|
|
448
|
+
|
|
449
|
+
Examples
|
|
450
|
+
--------
|
|
451
|
+
>>> fig = create_time_interaction_chart(
|
|
452
|
+
... result.interactions, result.effects,
|
|
453
|
+
... factors=['Lane'], time_var='Pull',
|
|
454
|
+
... dataset=result.dataset, theme=theme
|
|
455
|
+
... )
|
|
456
|
+
>>> fig.show()
|
|
457
|
+
"""
|
|
458
|
+
if 'factor_time' not in interactions:
|
|
459
|
+
raise ChartNotAvailableError(
|
|
460
|
+
'Factor × time interaction not available.\nThis requires both factors and time variable in the analysis.',
|
|
461
|
+
chart='factor_time_interaction',
|
|
462
|
+
available=list(interactions.keys()),
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
pdc = interactions['factor_time']
|
|
466
|
+
|
|
467
|
+
# Get unique factor levels and time points from dataset
|
|
468
|
+
# Build aggregated data: mean PDC per (factor_combo, time)
|
|
469
|
+
agg_cols = list(factors) + [time_var]
|
|
470
|
+
|
|
471
|
+
# Add PDC to dataset for aggregation
|
|
472
|
+
df = dataset.copy()
|
|
473
|
+
if len(pdc) != len(df):
|
|
474
|
+
raise ValueError(
|
|
475
|
+
f'Interaction chart requires PDC aligned to plotted data: len(pdc)={len(pdc)} != len(df)={len(df)}'
|
|
476
|
+
)
|
|
477
|
+
df['_pdc'] = pdc.values
|
|
478
|
+
|
|
479
|
+
# Aggregate to cell level
|
|
480
|
+
agg_data = df.groupby(agg_cols, observed=True)['_pdc'].mean().reset_index()
|
|
481
|
+
|
|
482
|
+
# Create combined factor key for grouping lines
|
|
483
|
+
if len(factors) == 1:
|
|
484
|
+
agg_data['_factor_key'] = agg_data[factors[0]].astype(str)
|
|
485
|
+
else:
|
|
486
|
+
agg_data['_factor_key'] = agg_data[list(factors)].apply(lambda x: '_'.join(str(v) for v in x), axis=1)
|
|
487
|
+
|
|
488
|
+
# Sort by time
|
|
489
|
+
agg_data = agg_data.sort_values(time_var)
|
|
490
|
+
|
|
491
|
+
# Create figure
|
|
492
|
+
fig = go.Figure()
|
|
493
|
+
|
|
494
|
+
# Color palette for factor levels
|
|
495
|
+
colors = [theme.data_color, theme.center_color] + _INTERACTION_COLORS
|
|
496
|
+
|
|
497
|
+
factor_keys = agg_data['_factor_key'].unique()
|
|
498
|
+
|
|
499
|
+
for i, factor_key in enumerate(factor_keys):
|
|
500
|
+
mask = agg_data['_factor_key'] == factor_key
|
|
501
|
+
subset = agg_data[mask]
|
|
502
|
+
|
|
503
|
+
fig.add_trace(
|
|
504
|
+
go.Scatter(
|
|
505
|
+
x=subset[time_var],
|
|
506
|
+
y=subset['_pdc'],
|
|
507
|
+
mode='lines+markers',
|
|
508
|
+
name=str(factor_key),
|
|
509
|
+
line=dict(color=colors[i % len(colors)], width=2),
|
|
510
|
+
marker=dict(size=8, color=colors[i % len(colors)]),
|
|
511
|
+
hovertemplate=f'{factor_key}<br>Time: %{{x}}<br>Effect: %{{y:.4f}}<extra></extra>',
|
|
512
|
+
)
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# Add horizontal line at 0
|
|
516
|
+
fig.add_hline(y=0, line_dash='dash', line_color='gray', line_width=1)
|
|
517
|
+
|
|
518
|
+
factor_label = ', '.join(factors)
|
|
519
|
+
fig.update_layout(
|
|
520
|
+
title=f'Factor × Time Interaction ({factor_label})',
|
|
521
|
+
xaxis_title=time_var,
|
|
522
|
+
yaxis_title='Interaction Effect (PDC)',
|
|
523
|
+
width=width,
|
|
524
|
+
height=height,
|
|
525
|
+
legend_title=factor_label,
|
|
526
|
+
hovermode='closest',
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
return fig
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def create_factor_interaction_chart(
|
|
533
|
+
interactions: Mapping[str, Any], factors: list[str], theme: ChartTheme, width: int = 800, height: int = 600
|
|
534
|
+
) -> go.Figure:
|
|
535
|
+
"""
|
|
536
|
+
Create line chart for factor × factor interaction.
|
|
537
|
+
|
|
538
|
+
Displays one line per factor2 level, with x-axis as factor1 levels
|
|
539
|
+
and y-axis as the interaction effect (Rx). Non-parallel lines indicate
|
|
540
|
+
interaction between the two factors.
|
|
541
|
+
|
|
542
|
+
Parameters
|
|
543
|
+
----------
|
|
544
|
+
interactions : dict
|
|
545
|
+
Interactions dictionary from result.interactions.
|
|
546
|
+
Expected key: 'factor_factor' mapping to DataFrame with
|
|
547
|
+
columns [factor1, factor2, 'Rx']
|
|
548
|
+
factors : list of str
|
|
549
|
+
Factor variable names (at least 2)
|
|
550
|
+
theme : ChartTheme
|
|
551
|
+
Visual theme for styling
|
|
552
|
+
width : int, default 800
|
|
553
|
+
Figure width in pixels
|
|
554
|
+
height : int, default 600
|
|
555
|
+
Figure height in pixels
|
|
556
|
+
|
|
557
|
+
Returns
|
|
558
|
+
-------
|
|
559
|
+
go.Figure
|
|
560
|
+
Plotly figure with line chart
|
|
561
|
+
|
|
562
|
+
Raises
|
|
563
|
+
------
|
|
564
|
+
ValueError
|
|
565
|
+
If ``'factor_factor'`` key is missing from ``interactions``.
|
|
566
|
+
ValueError
|
|
567
|
+
If fewer than 2 factors are provided.
|
|
568
|
+
ValueError
|
|
569
|
+
If expected factor columns are not found in the interaction data.
|
|
570
|
+
|
|
571
|
+
Examples
|
|
572
|
+
--------
|
|
573
|
+
>>> fig = create_factor_interaction_chart(
|
|
574
|
+
... result.interactions,
|
|
575
|
+
... factors=['Lane', 'Phase'],
|
|
576
|
+
... theme=theme
|
|
577
|
+
... )
|
|
578
|
+
>>> fig.show()
|
|
579
|
+
"""
|
|
580
|
+
if 'factor_factor' not in interactions:
|
|
581
|
+
raise ChartNotAvailableError(
|
|
582
|
+
'Factor × factor interaction not available.\nThis requires at least 2 factors in the analysis.',
|
|
583
|
+
chart='factor_factor_interaction',
|
|
584
|
+
available=list(interactions.keys()),
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
if len(factors) < 2:
|
|
588
|
+
raise ValidationError(f'Factor interaction requires at least 2 factors, got {len(factors)}.')
|
|
589
|
+
|
|
590
|
+
fi = interactions['factor_factor']
|
|
591
|
+
factor1, factor2 = factors[0], factors[1]
|
|
592
|
+
|
|
593
|
+
if factor1 not in fi.columns or factor2 not in fi.columns:
|
|
594
|
+
raise ValueError(f'Expected columns {factor1}, {factor2} in interaction data.\nFound: {list(fi.columns)}')
|
|
595
|
+
|
|
596
|
+
fig = go.Figure()
|
|
597
|
+
|
|
598
|
+
# Get unique levels, sorted for consistent ordering
|
|
599
|
+
levels1 = sorted(fi[factor1].unique(), key=str)
|
|
600
|
+
levels2 = sorted(fi[factor2].unique(), key=str)
|
|
601
|
+
|
|
602
|
+
# Color palette for factor2 levels
|
|
603
|
+
colors = [theme.data_color, theme.center_color] + _INTERACTION_COLORS
|
|
604
|
+
|
|
605
|
+
# Create one line per factor2 level
|
|
606
|
+
for i, level2 in enumerate(levels2):
|
|
607
|
+
mask = fi[factor2] == level2
|
|
608
|
+
subset = fi[mask].set_index(factor1)
|
|
609
|
+
|
|
610
|
+
x_values = [str(lv) for lv in levels1]
|
|
611
|
+
# Missing factor combinations are filled with 0: mathematically correct
|
|
612
|
+
# for interaction residuals (Rx), since absence = no interaction effect.
|
|
613
|
+
y_values = [subset.loc[lv, 'Rx'] if lv in subset.index else 0 for lv in levels1]
|
|
614
|
+
|
|
615
|
+
fig.add_trace(
|
|
616
|
+
go.Scatter(
|
|
617
|
+
x=x_values,
|
|
618
|
+
y=y_values,
|
|
619
|
+
mode='lines+markers',
|
|
620
|
+
name=str(level2),
|
|
621
|
+
line=dict(color=colors[i % len(colors)], width=2),
|
|
622
|
+
marker=dict(size=8, color=colors[i % len(colors)]),
|
|
623
|
+
hovertemplate=(f'{factor1}: %{{x}}<br>{factor2}: {level2}<br>Interaction: %{{y:.4f}}<extra></extra>'),
|
|
624
|
+
)
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
# Add horizontal reference line at 0
|
|
628
|
+
fig.add_hline(y=0, line_dash='dash', line_color='gray', line_width=1)
|
|
629
|
+
|
|
630
|
+
fig.update_layout(
|
|
631
|
+
title=f'Factor Interaction: {factor1} × {factor2}',
|
|
632
|
+
xaxis_title=factor1,
|
|
633
|
+
yaxis_title='Interaction Effect (Rx)',
|
|
634
|
+
width=width,
|
|
635
|
+
height=height,
|
|
636
|
+
legend_title=factor2,
|
|
637
|
+
hovermode='closest',
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
return fig
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Lane boundary rendering for control charts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import plotly.graph_objects as go
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .themes import ChartTheme
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_lane_boundaries(
|
|
14
|
+
fig: go.Figure,
|
|
15
|
+
lane_boundaries: list[dict] | None,
|
|
16
|
+
y_range: tuple[float, float],
|
|
17
|
+
theme: ChartTheme,
|
|
18
|
+
row: int | None = None,
|
|
19
|
+
col: int | None = None,
|
|
20
|
+
show_labels: bool = True,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Add vertical lane boundary lines to a chart figure.
|
|
24
|
+
|
|
25
|
+
Unified function for both single charts and faceted subplots.
|
|
26
|
+
Lane boundaries show where collapsed factors change within the chart,
|
|
27
|
+
helping distinguish groups of observations from different factor levels.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
fig : go.Figure
|
|
32
|
+
Plotly figure to add shapes to
|
|
33
|
+
lane_boundaries : list[dict] or None
|
|
34
|
+
List of boundary dicts with 'position' and 'label' keys
|
|
35
|
+
y_range : tuple[float, float]
|
|
36
|
+
(y_min, y_max) for vertical line extent
|
|
37
|
+
theme : ChartTheme
|
|
38
|
+
Theme with lane boundary styling
|
|
39
|
+
row : int, optional
|
|
40
|
+
Subplot row (1-indexed). None for single charts.
|
|
41
|
+
col : int, optional
|
|
42
|
+
Subplot column (1-indexed). None for single charts.
|
|
43
|
+
show_labels : bool, default True
|
|
44
|
+
Whether to show factor labels at boundary positions
|
|
45
|
+
"""
|
|
46
|
+
if not lane_boundaries:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
y_min, y_max = y_range
|
|
50
|
+
|
|
51
|
+
subplot_kwargs = {}
|
|
52
|
+
if row is not None and col is not None:
|
|
53
|
+
subplot_kwargs = {'row': row, 'col': col}
|
|
54
|
+
|
|
55
|
+
for boundary in lane_boundaries:
|
|
56
|
+
x_pos = boundary['position']
|
|
57
|
+
label = boundary.get('label', '')
|
|
58
|
+
|
|
59
|
+
fig.add_shape(
|
|
60
|
+
type='line',
|
|
61
|
+
x0=x_pos,
|
|
62
|
+
x1=x_pos,
|
|
63
|
+
y0=y_min,
|
|
64
|
+
y1=y_max,
|
|
65
|
+
line=dict(color=theme.lane_boundary_color, dash=theme.lane_boundary_dash, width=theme.lane_boundary_width),
|
|
66
|
+
**subplot_kwargs,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if show_labels and label:
|
|
70
|
+
fig.add_annotation(
|
|
71
|
+
x=x_pos,
|
|
72
|
+
y=y_max,
|
|
73
|
+
text=label,
|
|
74
|
+
showarrow=False,
|
|
75
|
+
yanchor='bottom',
|
|
76
|
+
font=dict(size=theme.lane_boundary_annotation_size, color=theme.lane_boundary_color),
|
|
77
|
+
**subplot_kwargs,
|
|
78
|
+
)
|