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,517 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Taguchi Loss Function Analysis — Bishop Chapter 15.
|
|
3
|
+
|
|
4
|
+
Decomposes expected loss into 5 components that identify where variation
|
|
5
|
+
comes from: centering, unexplained (within-cell), process design conditions
|
|
6
|
+
(PDC), time, and PDC×time interaction. When multiple factors exist, PDC
|
|
7
|
+
further decomposes into per-factor main effect losses and factor interaction
|
|
8
|
+
loss.
|
|
9
|
+
|
|
10
|
+
References
|
|
11
|
+
----------
|
|
12
|
+
Bishop, T. *Variance Analysis System* — Chapter 15.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
from .exceptions import ValidationError
|
|
23
|
+
from .spc_constants import c4
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from .analysis_dataset import AnalysisDataSet
|
|
27
|
+
from .study import Study
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ============================================================================
|
|
31
|
+
# Data Class
|
|
32
|
+
# ============================================================================
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class LossResult:
|
|
37
|
+
"""
|
|
38
|
+
Result of a Taguchi Loss Function analysis (Bishop Ch. 15).
|
|
39
|
+
|
|
40
|
+
Decomposes expected loss into 5 components: centering, unexplained,
|
|
41
|
+
PDC, time, and PDC×time interaction. When multiple factors exist,
|
|
42
|
+
PDC further decomposes into per-factor losses and factor interaction.
|
|
43
|
+
|
|
44
|
+
Attributes
|
|
45
|
+
----------
|
|
46
|
+
target : float
|
|
47
|
+
Target value used for the analysis.
|
|
48
|
+
target_is_default : bool
|
|
49
|
+
True when target defaulted to grand mean (centering = 0).
|
|
50
|
+
y_bar : float
|
|
51
|
+
Grand mean of all observations.
|
|
52
|
+
n : int
|
|
53
|
+
Total observations.
|
|
54
|
+
K : int
|
|
55
|
+
Number of PDC levels (rsg_key groups).
|
|
56
|
+
T_periods : int
|
|
57
|
+
Number of time periods.
|
|
58
|
+
sds : int
|
|
59
|
+
Analytical SDS.
|
|
60
|
+
centering, unexplained, pdc, time, interaction : float
|
|
61
|
+
The 5 loss components (squared-loss units).
|
|
62
|
+
total : float
|
|
63
|
+
Sum of 5 components.
|
|
64
|
+
pct_centering, pct_unexplained, pct_pdc, pct_time, pct_interaction : float
|
|
65
|
+
Each component as percentage of total.
|
|
66
|
+
pdc_by_factor : dict[str, float] | None
|
|
67
|
+
Per-factor loss decomposition (multi-factor only).
|
|
68
|
+
pdc_factor_interaction : float | None
|
|
69
|
+
PDC minus sum of per-factor losses (multi-factor only).
|
|
70
|
+
round_to : int
|
|
71
|
+
Decimal places for display.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# Context
|
|
75
|
+
target: float
|
|
76
|
+
target_is_default: bool
|
|
77
|
+
y_bar: float
|
|
78
|
+
n: int
|
|
79
|
+
K: int
|
|
80
|
+
T_periods: int
|
|
81
|
+
sds: int
|
|
82
|
+
|
|
83
|
+
# 5 loss components (squared-loss units)
|
|
84
|
+
centering: float
|
|
85
|
+
unexplained: float
|
|
86
|
+
pdc: float
|
|
87
|
+
time: float
|
|
88
|
+
interaction: float
|
|
89
|
+
total: float
|
|
90
|
+
|
|
91
|
+
# Pareto (percentages of total)
|
|
92
|
+
pct_centering: float
|
|
93
|
+
pct_unexplained: float
|
|
94
|
+
pct_pdc: float
|
|
95
|
+
pct_time: float
|
|
96
|
+
pct_interaction: float
|
|
97
|
+
|
|
98
|
+
# PDC decomposition by factor (multi-factor only)
|
|
99
|
+
pdc_by_factor: dict[str, float] | None
|
|
100
|
+
pdc_factor_interaction: float | None
|
|
101
|
+
|
|
102
|
+
# Presentation
|
|
103
|
+
round_to: int = 3
|
|
104
|
+
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
# Visualization
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def plot(
|
|
110
|
+
self,
|
|
111
|
+
*,
|
|
112
|
+
structured: bool = False,
|
|
113
|
+
orientation: str = 'vertical',
|
|
114
|
+
theme=None,
|
|
115
|
+
width: int = 700,
|
|
116
|
+
height: int = 400,
|
|
117
|
+
title: str | None = None,
|
|
118
|
+
):
|
|
119
|
+
"""
|
|
120
|
+
Pareto bar chart of loss components.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
structured : bool, default False
|
|
125
|
+
False: 5 bars (MEAN, UNEXPLAINED, PDC, PT, PDCxPT INT).
|
|
126
|
+
True: PDC expanded into per-factor bars + PDF INT.
|
|
127
|
+
orientation : str, default 'vertical'
|
|
128
|
+
``'vertical'`` or ``'horizontal'``.
|
|
129
|
+
theme : ChartTheme, optional
|
|
130
|
+
Visual theme.
|
|
131
|
+
width, height : int
|
|
132
|
+
Figure dimensions.
|
|
133
|
+
title : str, optional
|
|
134
|
+
Chart title.
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
plotly.graph_objects.Figure
|
|
139
|
+
"""
|
|
140
|
+
import plotly.graph_objects as go
|
|
141
|
+
|
|
142
|
+
from .plotting.themes import ChartTheme, get_theme
|
|
143
|
+
|
|
144
|
+
if theme is None:
|
|
145
|
+
theme = ChartTheme()
|
|
146
|
+
elif isinstance(theme, str):
|
|
147
|
+
theme = get_theme(theme)
|
|
148
|
+
|
|
149
|
+
# Build (label, pct) pairs
|
|
150
|
+
if structured and self.pdc_by_factor and len(self.pdc_by_factor) >= 2:
|
|
151
|
+
items = [
|
|
152
|
+
('MEAN', self.pct_centering),
|
|
153
|
+
('UNEXPLAINED', self.pct_unexplained),
|
|
154
|
+
]
|
|
155
|
+
for factor, loss_val in self.pdc_by_factor.items():
|
|
156
|
+
pct = (loss_val / self.total * 100) if self.total > 0 else 0.0
|
|
157
|
+
items.append((factor, pct))
|
|
158
|
+
pdc_int_pct = (self.pdc_factor_interaction / self.total * 100) if self.total > 0 else 0.0
|
|
159
|
+
items.append(('PDF INT', pdc_int_pct))
|
|
160
|
+
items.append(('PT', self.pct_time))
|
|
161
|
+
items.append(('PDCxPT INT', self.pct_interaction))
|
|
162
|
+
else:
|
|
163
|
+
items = [
|
|
164
|
+
('MEAN', self.pct_centering),
|
|
165
|
+
('UNEXPLAINED', self.pct_unexplained),
|
|
166
|
+
('PDC', self.pct_pdc),
|
|
167
|
+
('PT', self.pct_time),
|
|
168
|
+
('PDCxPT INT', self.pct_interaction),
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
# Pareto order (descending)
|
|
172
|
+
items.sort(key=lambda x: x[1], reverse=True)
|
|
173
|
+
labels = [x[0] for x in items]
|
|
174
|
+
values = [x[1] for x in items]
|
|
175
|
+
|
|
176
|
+
r = self.round_to
|
|
177
|
+
text = [f'{round(v, r)}%' for v in values]
|
|
178
|
+
horizontal = orientation.startswith('h')
|
|
179
|
+
axis_max = max(values) * 1.1 if values else 100
|
|
180
|
+
|
|
181
|
+
value_label = 'Contribution to Average Loss (%)'
|
|
182
|
+
component_label = 'Components Contributing to Average Loss'
|
|
183
|
+
|
|
184
|
+
if horizontal:
|
|
185
|
+
fig = go.Figure(
|
|
186
|
+
go.Bar(
|
|
187
|
+
y=labels[::-1],
|
|
188
|
+
x=values[::-1],
|
|
189
|
+
orientation='h',
|
|
190
|
+
text=text[::-1],
|
|
191
|
+
textposition='outside',
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
fig.update_layout(
|
|
195
|
+
xaxis_title=value_label,
|
|
196
|
+
yaxis_title=component_label,
|
|
197
|
+
xaxis_range=[0, axis_max],
|
|
198
|
+
width=width,
|
|
199
|
+
height=height,
|
|
200
|
+
margin=dict(l=120),
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
fig = go.Figure(
|
|
204
|
+
go.Bar(
|
|
205
|
+
x=labels,
|
|
206
|
+
y=values,
|
|
207
|
+
orientation='v',
|
|
208
|
+
text=text,
|
|
209
|
+
textposition='outside',
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
fig.update_layout(
|
|
213
|
+
yaxis_title=value_label,
|
|
214
|
+
xaxis_title=component_label,
|
|
215
|
+
yaxis_range=[0, axis_max],
|
|
216
|
+
width=width,
|
|
217
|
+
height=height,
|
|
218
|
+
margin=dict(b=80),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
layout = theme.to_layout_dict()
|
|
222
|
+
layout['title']['text'] = title or 'Taguchi Loss Function Analysis'
|
|
223
|
+
fig.update_layout(**layout)
|
|
224
|
+
|
|
225
|
+
return fig
|
|
226
|
+
|
|
227
|
+
# ------------------------------------------------------------------
|
|
228
|
+
# Presentation helpers
|
|
229
|
+
# ------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
def as_dict(self, round_to: int | None = None) -> dict:
|
|
232
|
+
"""Return results as a dict with values rounded for display."""
|
|
233
|
+
r = round_to if round_to is not None else self.round_to
|
|
234
|
+
|
|
235
|
+
d = {
|
|
236
|
+
'target': round(self.target, r),
|
|
237
|
+
'target_is_default': self.target_is_default,
|
|
238
|
+
'y_bar': round(self.y_bar, r),
|
|
239
|
+
'n': self.n,
|
|
240
|
+
'K': self.K,
|
|
241
|
+
'T_periods': self.T_periods,
|
|
242
|
+
'sds': self.sds,
|
|
243
|
+
'centering': round(self.centering, r),
|
|
244
|
+
'unexplained': round(self.unexplained, r),
|
|
245
|
+
'pdc': round(self.pdc, r),
|
|
246
|
+
'time': round(self.time, r),
|
|
247
|
+
'interaction': round(self.interaction, r),
|
|
248
|
+
'total': round(self.total, r),
|
|
249
|
+
'pct_centering': round(self.pct_centering, r),
|
|
250
|
+
'pct_unexplained': round(self.pct_unexplained, r),
|
|
251
|
+
'pct_pdc': round(self.pct_pdc, r),
|
|
252
|
+
'pct_time': round(self.pct_time, r),
|
|
253
|
+
'pct_interaction': round(self.pct_interaction, r),
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if self.pdc_by_factor is not None:
|
|
257
|
+
d['pdc_by_factor'] = {k: round(v, r) for k, v in self.pdc_by_factor.items()}
|
|
258
|
+
d['pdc_factor_interaction'] = round(self.pdc_factor_interaction, r)
|
|
259
|
+
|
|
260
|
+
return d
|
|
261
|
+
|
|
262
|
+
def __repr__(self) -> str:
|
|
263
|
+
"""Formatted summary."""
|
|
264
|
+
d = self.as_dict()
|
|
265
|
+
r = self.round_to
|
|
266
|
+
lines = ['LossResult:']
|
|
267
|
+
tgt_note = ' (default=grand mean)' if self.target_is_default else ''
|
|
268
|
+
lines.append(f' Target={d["target"]}{tgt_note}, Ybar={d["y_bar"]}, N={d["n"]}')
|
|
269
|
+
lines.append(f' K={d["K"]} PDC levels, T={d["T_periods"]} periods, SDS={d["sds"]}')
|
|
270
|
+
lines.append('')
|
|
271
|
+
lines.append(f' Expected Loss (EL) = {d["total"]}')
|
|
272
|
+
lines.append('')
|
|
273
|
+
|
|
274
|
+
# Pareto-ordered components
|
|
275
|
+
components = [
|
|
276
|
+
('MEAN', d['pct_centering']),
|
|
277
|
+
('UNEXPLAINED', d['pct_unexplained']),
|
|
278
|
+
('PDC', d['pct_pdc']),
|
|
279
|
+
('PT', d['pct_time']),
|
|
280
|
+
('PDCxPT INT', d['pct_interaction']),
|
|
281
|
+
]
|
|
282
|
+
components.sort(key=lambda x: x[1], reverse=True)
|
|
283
|
+
|
|
284
|
+
lines.append(' Loss Decomposition:')
|
|
285
|
+
for label, pct in components:
|
|
286
|
+
lines.append(f' {label:>15s}: {round(pct, r)}%')
|
|
287
|
+
|
|
288
|
+
if self.pdc_by_factor and len(self.pdc_by_factor) >= 2:
|
|
289
|
+
lines.append('')
|
|
290
|
+
lines.append(' PDC Decomposition:')
|
|
291
|
+
pdc_items = sorted(self.pdc_by_factor.items(), key=lambda x: x[1], reverse=True)
|
|
292
|
+
for factor, loss_val in pdc_items:
|
|
293
|
+
pct = (loss_val / self.total * 100) if self.total > 0 else 0.0
|
|
294
|
+
lines.append(f' {factor:>15s}: {round(pct, r)}%')
|
|
295
|
+
pdc_int_pct = (self.pdc_factor_interaction / self.total * 100) if self.total > 0 else 0.0
|
|
296
|
+
lines.append(f' {"PDF INT":>15s}: {round(pdc_int_pct, r)}%')
|
|
297
|
+
|
|
298
|
+
return '\n'.join(lines)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ============================================================================
|
|
302
|
+
# Pure Functions
|
|
303
|
+
# ============================================================================
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _compute_centering(y_bar: float, target: float) -> float:
|
|
307
|
+
"""Eq 15.13 term 1: (Ȳ.. - T)²."""
|
|
308
|
+
return (y_bar - target) ** 2
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _compute_unexplained_replicated(df, response_var: str) -> float:
|
|
312
|
+
"""
|
|
313
|
+
Eq 15.16/15.17: within-cell variance for fully replicated designs.
|
|
314
|
+
|
|
315
|
+
unexplained = (1/KT) Σ (S_kt / c4(n_kt))²
|
|
316
|
+
"""
|
|
317
|
+
cell_stats = df.groupby('cell_key', observed=True)[response_var].agg(['std', 'count'])
|
|
318
|
+
cell_stats['sigma_sq'] = (cell_stats['std'] / cell_stats['count'].apply(c4)) ** 2
|
|
319
|
+
return float(cell_stats['sigma_sq'].mean())
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _compute_unexplained_pooled(df) -> float:
|
|
323
|
+
"""
|
|
324
|
+
Eq 15.18/15.19: pooled sigma from R2 for ADS 2/3.
|
|
325
|
+
|
|
326
|
+
Uses std(R2, ddof=1) / 0.7 then squares (Bishop Eq 15.18).
|
|
327
|
+
"""
|
|
328
|
+
r2 = df['R2'].dropna().to_numpy(dtype=float)
|
|
329
|
+
sigma_hat = np.std(r2, ddof=1) / 0.7
|
|
330
|
+
return float(sigma_hat**2)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _compute_pdc(df, rsg_var_name: str) -> float:
|
|
334
|
+
"""Eq 15.20: (1/K) Σ (Ȳ_k. - Ȳ..)²."""
|
|
335
|
+
ybar = df['Ybar'].iloc[0]
|
|
336
|
+
ybar_k = df.groupby(rsg_var_name, observed=True)['Ybar_k'].first()
|
|
337
|
+
rho_k = ybar_k - ybar
|
|
338
|
+
return float((rho_k**2).mean())
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _compute_time(df, time_var: str) -> float:
|
|
342
|
+
"""Eq 15.13 term 4: (1/T) Σ (Ȳ_.t - Ȳ..)²."""
|
|
343
|
+
ybar = df['Ybar'].iloc[0]
|
|
344
|
+
ybar_t = df.groupby(time_var, observed=True)['Ybar_t'].first()
|
|
345
|
+
tau_t = ybar_t - ybar
|
|
346
|
+
return float((tau_t**2).mean())
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _compute_interaction(df, time_var: str) -> float:
|
|
350
|
+
"""Eq 15.13 term 5: (1/KT) Σ (ρτ_kt)²."""
|
|
351
|
+
ybar = df['Ybar'].iloc[0]
|
|
352
|
+
cell = df.groupby('cell_key', observed=True)[['Ybar_kt', 'Ybar_k', 'Ybar_t']].first()
|
|
353
|
+
rho_tau = cell['Ybar_kt'] - cell['Ybar_k'] - cell['Ybar_t'] + ybar
|
|
354
|
+
return float((rho_tau**2).mean())
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _compute_pdc_decomposition(
|
|
358
|
+
effects: dict, rsg_vars: tuple[str, ...], pdc_total: float
|
|
359
|
+
) -> tuple[dict[str, float], float]:
|
|
360
|
+
"""
|
|
361
|
+
Decompose PDC into per-factor main effect losses and factor interaction.
|
|
362
|
+
|
|
363
|
+
Per-factor: Loss(Fm) = (1/Im) Σ [Main_Effect_i]²
|
|
364
|
+
Factor interaction = PDC - Σ Loss(Fm)
|
|
365
|
+
"""
|
|
366
|
+
pdc_by_factor: dict[str, float] = {}
|
|
367
|
+
for factor in rsg_vars:
|
|
368
|
+
me = effects[factor]['Main_Effect']
|
|
369
|
+
pdc_by_factor[factor] = float((me**2).mean())
|
|
370
|
+
|
|
371
|
+
pdc_factor_interaction = pdc_total - sum(pdc_by_factor.values())
|
|
372
|
+
return pdc_by_factor, pdc_factor_interaction
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _compute_pareto(
|
|
376
|
+
centering: float,
|
|
377
|
+
unexplained: float,
|
|
378
|
+
pdc: float,
|
|
379
|
+
time_loss: float,
|
|
380
|
+
interaction: float,
|
|
381
|
+
total: float,
|
|
382
|
+
) -> tuple[float, float, float, float, float]:
|
|
383
|
+
"""Each component as percentage of total. Zero-safe."""
|
|
384
|
+
if total == 0.0:
|
|
385
|
+
return 0.0, 0.0, 0.0, 0.0, 0.0
|
|
386
|
+
return (
|
|
387
|
+
centering / total * 100,
|
|
388
|
+
unexplained / total * 100,
|
|
389
|
+
pdc / total * 100,
|
|
390
|
+
time_loss / total * 100,
|
|
391
|
+
interaction / total * 100,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# ============================================================================
|
|
396
|
+
# Coercion
|
|
397
|
+
# ============================================================================
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _coerce_ads(source: Study | AnalysisDataSet) -> AnalysisDataSet:
|
|
401
|
+
"""Accept Study or AnalysisDataSet and return ADS."""
|
|
402
|
+
if hasattr(source, '_ads'):
|
|
403
|
+
return source._ads
|
|
404
|
+
return source
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# ============================================================================
|
|
408
|
+
# Orchestrator
|
|
409
|
+
# ============================================================================
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def assess_loss(
|
|
413
|
+
source: Study | AnalysisDataSet,
|
|
414
|
+
target: float | None = None,
|
|
415
|
+
round_to: int = 3,
|
|
416
|
+
) -> LossResult:
|
|
417
|
+
"""
|
|
418
|
+
Assess Taguchi Loss Function decomposition (Bishop Ch. 15).
|
|
419
|
+
|
|
420
|
+
Parameters
|
|
421
|
+
----------
|
|
422
|
+
source : Study | AnalysisDataSet
|
|
423
|
+
Data source with VAS residuals computed.
|
|
424
|
+
target : float, optional
|
|
425
|
+
Target value. Defaults to grand mean (centering = 0).
|
|
426
|
+
round_to : int, default 3
|
|
427
|
+
Decimal places for presentation.
|
|
428
|
+
|
|
429
|
+
Returns
|
|
430
|
+
-------
|
|
431
|
+
LossResult
|
|
432
|
+
Frozen dataclass with 5-component loss decomposition.
|
|
433
|
+
|
|
434
|
+
Raises
|
|
435
|
+
------
|
|
436
|
+
ValidationError
|
|
437
|
+
If VAS residuals are not available (requires factors + time).
|
|
438
|
+
"""
|
|
439
|
+
ads = _coerce_ads(source)
|
|
440
|
+
|
|
441
|
+
if not ads.has_vas_residuals:
|
|
442
|
+
raise ValidationError(
|
|
443
|
+
'Loss function analysis requires VAS residuals '
|
|
444
|
+
'(factors + time design). '
|
|
445
|
+
f'Current design (SDS {ads.observed_design_state}) '
|
|
446
|
+
'does not have VAS residuals.'
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
df = ads.analysis_dataset
|
|
450
|
+
spec = ads.spec
|
|
451
|
+
response_var = spec.response_var
|
|
452
|
+
rsg_var_name = spec.rsg_var_name
|
|
453
|
+
time_var = spec.time_var
|
|
454
|
+
rsg_vars = spec.rsg_vars
|
|
455
|
+
|
|
456
|
+
# Context
|
|
457
|
+
y_bar = float(df['Ybar'].iloc[0])
|
|
458
|
+
n = len(df)
|
|
459
|
+
K = df[rsg_var_name].nunique()
|
|
460
|
+
T_periods = df[time_var].nunique()
|
|
461
|
+
sds = ads.analytical_design_state.sds
|
|
462
|
+
|
|
463
|
+
# Default target
|
|
464
|
+
target_is_default = target is None
|
|
465
|
+
if target_is_default:
|
|
466
|
+
target = y_bar
|
|
467
|
+
|
|
468
|
+
# --- 5 components ---
|
|
469
|
+
centering = _compute_centering(y_bar, target)
|
|
470
|
+
|
|
471
|
+
# Unexplained: branch on cell replication
|
|
472
|
+
if ads._structure_stats.n_cell_min >= 2:
|
|
473
|
+
unexplained = _compute_unexplained_replicated(df, response_var)
|
|
474
|
+
else:
|
|
475
|
+
unexplained = _compute_unexplained_pooled(df)
|
|
476
|
+
|
|
477
|
+
pdc = _compute_pdc(df, rsg_var_name)
|
|
478
|
+
time_loss = _compute_time(df, time_var)
|
|
479
|
+
interaction = _compute_interaction(df, time_var)
|
|
480
|
+
|
|
481
|
+
total = centering + unexplained + pdc + time_loss + interaction
|
|
482
|
+
|
|
483
|
+
# Pareto
|
|
484
|
+
pct = _compute_pareto(centering, unexplained, pdc, time_loss, interaction, total)
|
|
485
|
+
|
|
486
|
+
# PDC decomposition by factor
|
|
487
|
+
pdc_by_factor = None
|
|
488
|
+
pdc_factor_interaction = None
|
|
489
|
+
if rsg_vars and len(rsg_vars) >= 2:
|
|
490
|
+
pdc_by_factor, pdc_factor_interaction = _compute_pdc_decomposition(ads.effects, rsg_vars, pdc)
|
|
491
|
+
elif rsg_vars and len(rsg_vars) == 1:
|
|
492
|
+
pdc_by_factor = {rsg_vars[0]: pdc}
|
|
493
|
+
pdc_factor_interaction = 0.0
|
|
494
|
+
|
|
495
|
+
return LossResult(
|
|
496
|
+
target=target,
|
|
497
|
+
target_is_default=target_is_default,
|
|
498
|
+
y_bar=y_bar,
|
|
499
|
+
n=n,
|
|
500
|
+
K=K,
|
|
501
|
+
T_periods=T_periods,
|
|
502
|
+
sds=sds,
|
|
503
|
+
centering=centering,
|
|
504
|
+
unexplained=unexplained,
|
|
505
|
+
pdc=pdc,
|
|
506
|
+
time=time_loss,
|
|
507
|
+
interaction=interaction,
|
|
508
|
+
total=total,
|
|
509
|
+
pct_centering=pct[0],
|
|
510
|
+
pct_unexplained=pct[1],
|
|
511
|
+
pct_pdc=pct[2],
|
|
512
|
+
pct_time=pct[3],
|
|
513
|
+
pct_interaction=pct[4],
|
|
514
|
+
pdc_by_factor=pdc_by_factor,
|
|
515
|
+
pdc_factor_interaction=pdc_factor_interaction,
|
|
516
|
+
round_to=round_to,
|
|
517
|
+
)
|