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,1711 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Synthetic Data Generators for Statistical Process Control
|
|
3
|
+
|
|
4
|
+
This module provides data generators for all Sampling Design States (SDS)
|
|
5
|
+
defined in the Variance Analysis System framework by Dr. Donald Wheeler
|
|
6
|
+
and extended by Dr. Thomas A. Bishop.
|
|
7
|
+
|
|
8
|
+
Purpose:
|
|
9
|
+
--------
|
|
10
|
+
- Testing: Validate analysis algorithms with known ground truth
|
|
11
|
+
- Documentation: Provide reproducible examples
|
|
12
|
+
- Education: Demonstrate different data structures and their implications
|
|
13
|
+
- Development: Rapid prototyping of new features
|
|
14
|
+
|
|
15
|
+
Quick Start:
|
|
16
|
+
-----------
|
|
17
|
+
>>> from processbehavior.datasets import synthetic
|
|
18
|
+
>>>
|
|
19
|
+
>>> # Generate SDS1 data (full replication)
|
|
20
|
+
>>> df = synthetic.make_sds1(K1=3, K2=2, T=8, n_min=2, n_max=4)
|
|
21
|
+
>>>
|
|
22
|
+
>>> # Use with analysis
|
|
23
|
+
>>> from processbehavior.analysis import Analysis
|
|
24
|
+
>>> spec = {'analysis_type': 'Xbar', 'rsg_vars': ['factor 1'],
|
|
25
|
+
... 'response_var': 'y', 'time_var': 'time'}
|
|
26
|
+
>>> results = Analysis(df, spec).calculate()
|
|
27
|
+
|
|
28
|
+
Sampling Design States:
|
|
29
|
+
----------------------
|
|
30
|
+
- SDS 1: Full replication (n≥2 in all cells)
|
|
31
|
+
- SDS 2: No replication (n=1 in all cells)
|
|
32
|
+
- SDS 3: Partial replication (mixed n=1 and n≥2)
|
|
33
|
+
- SDS 4: Single condition over time (one factor, multiple time points)
|
|
34
|
+
- SDS 5: Nested design (hierarchical factors, asynchronous)
|
|
35
|
+
- SDS 6: Unstructured/regime changes (irregular patterns)
|
|
36
|
+
|
|
37
|
+
References:
|
|
38
|
+
-----------
|
|
39
|
+
Wheeler, D. J. (1995). Advanced Topics in Statistical Process Control.
|
|
40
|
+
SPC Press, Knoxville, TN.
|
|
41
|
+
|
|
42
|
+
Bishop, D. R. (2023). Personal communication and collaboration on
|
|
43
|
+
Variance Analysis System implementation.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
import logging
|
|
47
|
+
|
|
48
|
+
import numpy as np
|
|
49
|
+
import pandas as pd
|
|
50
|
+
|
|
51
|
+
from ..exceptions import ValidationError
|
|
52
|
+
from ..formulation_spec import FormulationSpec
|
|
53
|
+
|
|
54
|
+
logger = logging.getLogger(__name__)
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
'make_sds1',
|
|
58
|
+
'make_sds2',
|
|
59
|
+
'make_sds3',
|
|
60
|
+
'make_sds4',
|
|
61
|
+
'make_sds5',
|
|
62
|
+
'make_sds6',
|
|
63
|
+
'make_design',
|
|
64
|
+
'make_edge_cases',
|
|
65
|
+
'make_large_dataset',
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ============================================================================
|
|
70
|
+
# SDS 1: Full Replication - Most Statistically Powerful
|
|
71
|
+
# ============================================================================
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def make_sds1(
|
|
75
|
+
K1: int = 3,
|
|
76
|
+
K2: int = 2,
|
|
77
|
+
T: int = 8,
|
|
78
|
+
n_min: int = 2,
|
|
79
|
+
n_max: int = 5,
|
|
80
|
+
mu: float = 50.0,
|
|
81
|
+
sigma: float = 0.4,
|
|
82
|
+
factor1_effect_size: float = 2.0,
|
|
83
|
+
factor2_effect_size: float = 1.5,
|
|
84
|
+
time_effect_size: float = 1.0,
|
|
85
|
+
interaction_effect_size: float = 0.5,
|
|
86
|
+
seed: int | None = None,
|
|
87
|
+
include_truth: bool = False,
|
|
88
|
+
factor1_names: list[str] | None = None,
|
|
89
|
+
factor2_names: list[str] | None = None,
|
|
90
|
+
) -> pd.DataFrame:
|
|
91
|
+
"""
|
|
92
|
+
Generate Sampling Design State 1 data (full replication).
|
|
93
|
+
|
|
94
|
+
SDS1 is the gold standard for process studies - every combination of
|
|
95
|
+
factors and time has multiple observations, allowing true estimation of
|
|
96
|
+
within-cell variance. This is what designed experiments strive for.
|
|
97
|
+
|
|
98
|
+
Characteristics:
|
|
99
|
+
---------------
|
|
100
|
+
- Every (factor1, factor2, time) cell has n ≥ 2 observations
|
|
101
|
+
- True within-cell variance can be estimated directly
|
|
102
|
+
- Supports full interaction analysis (R3 calculations)
|
|
103
|
+
- Most statistically powerful design
|
|
104
|
+
- Enables calculation of all residuals (R1-R5) with precision
|
|
105
|
+
|
|
106
|
+
Use Cases:
|
|
107
|
+
---------
|
|
108
|
+
- Testing Xbar-S chart algorithms
|
|
109
|
+
- Validating variance decomposition (R1-R5)
|
|
110
|
+
- Demonstrating interaction effects
|
|
111
|
+
- Teaching full-factorial designs
|
|
112
|
+
- Baseline for comparing other SDS types
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
K1: Number of factor 1 levels (e.g., machines, lanes)
|
|
116
|
+
K2: Number of factor 2 levels (e.g., operators, shifts)
|
|
117
|
+
T: Number of time periods (e.g., days, shifts, batches)
|
|
118
|
+
n_min: Minimum observations per cell
|
|
119
|
+
n_max: Maximum observations per cell
|
|
120
|
+
mu: Grand mean (process center)
|
|
121
|
+
sigma: Within-cell standard deviation (pure error)
|
|
122
|
+
factor1_effect_size: Standard deviation of factor 1 main effects
|
|
123
|
+
factor2_effect_size: Standard deviation of factor 2 main effects
|
|
124
|
+
time_effect_size: Standard deviation of time main effects
|
|
125
|
+
interaction_effect_size: Standard deviation of interactions
|
|
126
|
+
seed: Random seed for reproducibility (None = random)
|
|
127
|
+
include_truth: If True, include true effects as columns for validation
|
|
128
|
+
factor1_names: Custom factor 1 level names (default: F1_1, F1_2, ...)
|
|
129
|
+
factor2_names: Custom factor 2 level names (default: F2_1, F2_2, ...)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
DataFrame with columns:
|
|
133
|
+
- time: Time period (1 to T)
|
|
134
|
+
- factor 1: Factor 1 level
|
|
135
|
+
- factor 2: Factor 2 level
|
|
136
|
+
- y: Response variable
|
|
137
|
+
|
|
138
|
+
Optional columns (if include_truth=True):
|
|
139
|
+
- true_factor1_effect: The ρ_i component
|
|
140
|
+
- true_factor2_effect: The φ_j component
|
|
141
|
+
- true_time_effect: The τ_t component
|
|
142
|
+
- true_interaction: The combined interaction component
|
|
143
|
+
- true_error: The ε component (pure error)
|
|
144
|
+
|
|
145
|
+
Mathematical Model:
|
|
146
|
+
Y_ijtr = μ + ρ_i + φ_j + τ_t + (ρφ)_ij + (ρτ)_it + (φτ)_jt + ε_ijtr
|
|
147
|
+
|
|
148
|
+
where:
|
|
149
|
+
μ: Grand mean (mu parameter)
|
|
150
|
+
ρ_i: Factor 1 main effect ~ N(0, factor1_effect_size²)
|
|
151
|
+
φ_j: Factor 2 main effect ~ N(0, factor2_effect_size²)
|
|
152
|
+
τ_t: Time main effect ~ N(0, time_effect_size²)
|
|
153
|
+
interactions: Combined effect ~ N(0, interaction_effect_size²)
|
|
154
|
+
ε_ijtr: Pure error for replicate r ~ N(0, σ²)
|
|
155
|
+
|
|
156
|
+
Examples:
|
|
157
|
+
>>> # Basic usage - 3 factor1 levels × 2 factor2 levels × 8 time periods
|
|
158
|
+
>>> df = make_sds1(K1=3, K2=2, T=8, seed=42)
|
|
159
|
+
>>> print(f"Generated {len(df)} observations")
|
|
160
|
+
>>> df.groupby(['factor 1', 'factor 2', 'time']).size().min()
|
|
161
|
+
2 # All cells have at least 2 observations
|
|
162
|
+
|
|
163
|
+
>>> # With ground truth for validation testing
|
|
164
|
+
>>> df = make_sds1(K1=2, K2=2, T=4, include_truth=True, seed=42)
|
|
165
|
+
|
|
166
|
+
>>> # Custom factor names
|
|
167
|
+
>>> df = make_sds1(K1=3, K2=2, T=6,
|
|
168
|
+
... factor1_names=['Machine_A', 'Machine_B', 'Machine_C'],
|
|
169
|
+
... factor2_names=['Operator_1', 'Operator_2'],
|
|
170
|
+
... seed=42)
|
|
171
|
+
|
|
172
|
+
Validation:
|
|
173
|
+
The function automatically validates that generated data meets SDS1 criteria:
|
|
174
|
+
- All (K1 × K2 × T) cells present (no missing combinations)
|
|
175
|
+
- All cells have n_min ≤ n ≤ n_max observations
|
|
176
|
+
- Response variable is numeric
|
|
177
|
+
- No missing values in critical columns
|
|
178
|
+
|
|
179
|
+
See Also:
|
|
180
|
+
make_sds2: For unreplicated designs (n=1 per cell)
|
|
181
|
+
make_sds3: For partial replication (mixed n=1 and n≥2)
|
|
182
|
+
make_sds4: For time series (single factor over time)
|
|
183
|
+
"""
|
|
184
|
+
rng = np.random.default_rng(seed)
|
|
185
|
+
|
|
186
|
+
# Generate true effects from the model
|
|
187
|
+
rho = rng.normal(0, factor1_effect_size, K1) # Factor 1 main effects
|
|
188
|
+
phi = rng.normal(0, factor2_effect_size, K2) # Factor 2 main effects
|
|
189
|
+
tau = rng.normal(0, time_effect_size, T) # Time main effects
|
|
190
|
+
inter = rng.normal(0, interaction_effect_size, (K1, K2, T)) # Interactions
|
|
191
|
+
|
|
192
|
+
# Use custom factor names if provided
|
|
193
|
+
if factor1_names is None:
|
|
194
|
+
factor1_names = [f'F1_{k + 1}' for k in range(K1)]
|
|
195
|
+
elif len(factor1_names) != K1:
|
|
196
|
+
raise ValidationError(f'Length of factor1_names ({len(factor1_names)}) must equal K1 ({K1})')
|
|
197
|
+
|
|
198
|
+
if factor2_names is None:
|
|
199
|
+
factor2_names = [f'F2_{k + 1}' for k in range(K2)]
|
|
200
|
+
elif len(factor2_names) != K2:
|
|
201
|
+
raise ValidationError(f'Length of factor2_names ({len(factor2_names)}) must equal K2 ({K2})')
|
|
202
|
+
|
|
203
|
+
rows = []
|
|
204
|
+
for k1 in range(K1):
|
|
205
|
+
for k2 in range(K2):
|
|
206
|
+
for t in range(T):
|
|
207
|
+
# Random number of observations per cell (uniform distribution)
|
|
208
|
+
n = rng.integers(n_min, n_max + 1)
|
|
209
|
+
|
|
210
|
+
for _i in range(n):
|
|
211
|
+
# Generate observation from true model
|
|
212
|
+
epsilon = rng.normal(0, sigma) # Pure error
|
|
213
|
+
y = mu + rho[k1] + phi[k2] + tau[t] + inter[k1, k2, t] + epsilon
|
|
214
|
+
|
|
215
|
+
row = {'time': t + 1, 'factor 1': factor1_names[k1], 'factor 2': factor2_names[k2], 'y': y}
|
|
216
|
+
|
|
217
|
+
# Optionally include ground truth for validation
|
|
218
|
+
if include_truth:
|
|
219
|
+
row.update(
|
|
220
|
+
{
|
|
221
|
+
'true_factor1_effect': rho[k1],
|
|
222
|
+
'true_factor2_effect': phi[k2],
|
|
223
|
+
'true_time_effect': tau[t],
|
|
224
|
+
'true_interaction': inter[k1, k2, t],
|
|
225
|
+
'true_error': epsilon,
|
|
226
|
+
'true_mean': mu + rho[k1] + phi[k2] + tau[t] + inter[k1, k2, t],
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
rows.append(row)
|
|
231
|
+
|
|
232
|
+
df = pd.DataFrame(rows)
|
|
233
|
+
|
|
234
|
+
# Validation - ensure SDS1 criteria met
|
|
235
|
+
cell_counts = df.groupby(['factor 1', 'factor 2', 'time']).size()
|
|
236
|
+
|
|
237
|
+
if cell_counts.min() < n_min:
|
|
238
|
+
raise ValidationError(f'SDS1 validation failed: min n={cell_counts.min()} < {n_min}')
|
|
239
|
+
if cell_counts.max() > n_max:
|
|
240
|
+
raise ValidationError(f'SDS1 validation failed: max n={cell_counts.max()} > {n_max}')
|
|
241
|
+
expected_cells = K1 * K2 * T
|
|
242
|
+
if len(cell_counts) != expected_cells:
|
|
243
|
+
raise ValidationError(
|
|
244
|
+
f'SDS1 validation failed: expected {expected_cells} cells, got {len(cell_counts)} '
|
|
245
|
+
'(some factor-time combinations missing)'
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
logger.debug(
|
|
249
|
+
f'Generated SDS1 data: {K1} × {K2} factors × {T} times × ~{(n_min + n_max) / 2:.1f} reps/cell '
|
|
250
|
+
f'= {len(df)} observations'
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return df
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ============================================================================
|
|
257
|
+
# SDS 2: No Replication - Common in Designed Experiments
|
|
258
|
+
# ============================================================================
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def make_sds2(
|
|
262
|
+
K1: int = 3,
|
|
263
|
+
K2: int = 2,
|
|
264
|
+
T: int = 10,
|
|
265
|
+
mu: float = 50.0,
|
|
266
|
+
sigma: float = 0.4,
|
|
267
|
+
factor1_effect_size: float = 2.0,
|
|
268
|
+
factor2_effect_size: float = 1.5,
|
|
269
|
+
time_effect_size: float = 1.2,
|
|
270
|
+
interaction_effect_size: float = 0.6,
|
|
271
|
+
seed: int | None = None,
|
|
272
|
+
include_truth: bool = False,
|
|
273
|
+
factor1_names: list[str] | None = None,
|
|
274
|
+
factor2_names: list[str] | None = None,
|
|
275
|
+
) -> pd.DataFrame:
|
|
276
|
+
"""
|
|
277
|
+
Generate Sampling Design State 2 data (no replication).
|
|
278
|
+
|
|
279
|
+
SDS2 is the classic unreplicated factorial design - exactly one observation
|
|
280
|
+
per cell. Common in designed experiments where resources are limited.
|
|
281
|
+
|
|
282
|
+
Characteristics:
|
|
283
|
+
---------------
|
|
284
|
+
- Exactly one observation per (factor1, factor2, time) cell
|
|
285
|
+
- Within-cell variance must be estimated indirectly (moving average)
|
|
286
|
+
- Cannot separate pure error from interaction effects
|
|
287
|
+
- More efficient data collection, less statistical power
|
|
288
|
+
|
|
289
|
+
Use Cases:
|
|
290
|
+
---------
|
|
291
|
+
- Testing moving average R2 calculation algorithms
|
|
292
|
+
- Validating SDS detection logic
|
|
293
|
+
- Demonstrating unreplicated factorial designs
|
|
294
|
+
- Cost-constrained or time-constrained data collection
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
K1: Number of factor 1 levels
|
|
298
|
+
K2: Number of factor 2 levels
|
|
299
|
+
T: Number of time periods
|
|
300
|
+
mu: Grand mean
|
|
301
|
+
sigma: Process standard deviation (confounded with interaction)
|
|
302
|
+
factor1_effect_size: Std dev of factor 1 effects
|
|
303
|
+
factor2_effect_size: Std dev of factor 2 effects
|
|
304
|
+
time_effect_size: Std dev of time effects
|
|
305
|
+
interaction_effect_size: Std dev of interactions
|
|
306
|
+
seed: Random seed for reproducibility
|
|
307
|
+
include_truth: Include true effects as columns
|
|
308
|
+
factor1_names: Custom factor 1 level names
|
|
309
|
+
factor2_names: Custom factor 2 level names
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
DataFrame with exactly K1 × K2 × T observations
|
|
313
|
+
|
|
314
|
+
Examples:
|
|
315
|
+
>>> # Basic usage
|
|
316
|
+
>>> df = make_sds2(K1=3, K2=2, T=8, seed=42)
|
|
317
|
+
>>> df.groupby(['factor 1', 'factor 2', 'time']).size().max()
|
|
318
|
+
1 # All cells have exactly 1 observation
|
|
319
|
+
>>> len(df)
|
|
320
|
+
48 # K1 × K2 × T = 3 × 2 × 8
|
|
321
|
+
|
|
322
|
+
Validation:
|
|
323
|
+
- Exactly n=1 per cell (all cells)
|
|
324
|
+
- Total observations = K1 × K2 × T
|
|
325
|
+
- No missing factor-time combinations
|
|
326
|
+
|
|
327
|
+
See Also:
|
|
328
|
+
make_sds1: For replicated designs
|
|
329
|
+
make_sds3: For mixed replication (most realistic)
|
|
330
|
+
"""
|
|
331
|
+
rng = np.random.default_rng(seed)
|
|
332
|
+
|
|
333
|
+
rho = rng.normal(0, factor1_effect_size, K1)
|
|
334
|
+
phi = rng.normal(0, factor2_effect_size, K2)
|
|
335
|
+
tau = rng.normal(0, time_effect_size, T)
|
|
336
|
+
inter = rng.normal(0, interaction_effect_size, (K1, K2, T))
|
|
337
|
+
|
|
338
|
+
if factor1_names is None:
|
|
339
|
+
factor1_names = [f'F1_{k + 1}' for k in range(K1)]
|
|
340
|
+
elif len(factor1_names) != K1:
|
|
341
|
+
raise ValidationError('Length of factor1_names must equal K1')
|
|
342
|
+
|
|
343
|
+
if factor2_names is None:
|
|
344
|
+
factor2_names = [f'F2_{k + 1}' for k in range(K2)]
|
|
345
|
+
elif len(factor2_names) != K2:
|
|
346
|
+
raise ValidationError('Length of factor2_names must equal K2')
|
|
347
|
+
|
|
348
|
+
rows = []
|
|
349
|
+
for k1 in range(K1):
|
|
350
|
+
for k2 in range(K2):
|
|
351
|
+
for t in range(T):
|
|
352
|
+
# Single observation per cell - this is the defining characteristic
|
|
353
|
+
epsilon = rng.normal(0, sigma)
|
|
354
|
+
y = mu + rho[k1] + phi[k2] + tau[t] + inter[k1, k2, t] + epsilon
|
|
355
|
+
|
|
356
|
+
row = {'time': t + 1, 'factor 1': factor1_names[k1], 'factor 2': factor2_names[k2], 'y': y}
|
|
357
|
+
|
|
358
|
+
if include_truth:
|
|
359
|
+
row.update(
|
|
360
|
+
{
|
|
361
|
+
'true_factor1_effect': rho[k1],
|
|
362
|
+
'true_factor2_effect': phi[k2],
|
|
363
|
+
'true_time_effect': tau[t],
|
|
364
|
+
'true_interaction': inter[k1, k2, t],
|
|
365
|
+
'true_confounded': inter[k1, k2, t] + epsilon,
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
rows.append(row)
|
|
370
|
+
|
|
371
|
+
df = pd.DataFrame(rows)
|
|
372
|
+
|
|
373
|
+
# Validation
|
|
374
|
+
cell_counts = df.groupby(['factor 1', 'factor 2', 'time']).size()
|
|
375
|
+
|
|
376
|
+
if not (cell_counts == 1).all():
|
|
377
|
+
raise ValidationError(
|
|
378
|
+
f'SDS2 validation failed: expected all n=1, got min={cell_counts.min()}, max={cell_counts.max()}'
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
expected_n = K1 * K2 * T
|
|
382
|
+
if len(df) != expected_n:
|
|
383
|
+
raise ValidationError(f'SDS2 validation failed: expected {expected_n} obs, got {len(df)}')
|
|
384
|
+
|
|
385
|
+
logger.debug(f'Generated SDS2 data: {K1} × {K2} factors × {T} times = {len(df)} observations')
|
|
386
|
+
|
|
387
|
+
return df
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# ============================================================================
|
|
391
|
+
# SDS 3: Partial Replication - MOST COMMON IN PRACTICE!
|
|
392
|
+
# ============================================================================
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def make_sds3( # noqa: C901
|
|
396
|
+
K1: int = 3,
|
|
397
|
+
K2: int = 2,
|
|
398
|
+
T: int = 8,
|
|
399
|
+
p_replicated: float = 0.5,
|
|
400
|
+
n_when_replicated: int = 3,
|
|
401
|
+
mu: float = 50.0,
|
|
402
|
+
sigma: float = 0.5,
|
|
403
|
+
factor1_effect_size: float = 2.0,
|
|
404
|
+
factor2_effect_size: float = 1.5,
|
|
405
|
+
time_effect_size: float = 1.0,
|
|
406
|
+
interaction_effect_size: float = 0.5,
|
|
407
|
+
seed: int | None = None,
|
|
408
|
+
replication_pattern: str = 'random',
|
|
409
|
+
include_truth: bool = False,
|
|
410
|
+
factor1_names: list[str] | None = None,
|
|
411
|
+
factor2_names: list[str] | None = None,
|
|
412
|
+
) -> pd.DataFrame:
|
|
413
|
+
"""
|
|
414
|
+
Generate Sampling Design State 3 data (partial replication).
|
|
415
|
+
|
|
416
|
+
SDS3 represents reality: some cells have replication, others don't.
|
|
417
|
+
This is the MOST COMMON real-world scenario and the HARDEST to analyze.
|
|
418
|
+
|
|
419
|
+
Characteristics:
|
|
420
|
+
---------------
|
|
421
|
+
- Some cells have n=1, others have n≥2
|
|
422
|
+
- Requires hybrid R2 estimation
|
|
423
|
+
- Reflects real-world data collection reality
|
|
424
|
+
- Most challenging to analyze correctly
|
|
425
|
+
|
|
426
|
+
Use Cases:
|
|
427
|
+
---------
|
|
428
|
+
- Testing hybrid variance estimation algorithms
|
|
429
|
+
- Demonstrating real-world data structures
|
|
430
|
+
- Validating robustness to missing data
|
|
431
|
+
- Development of SDS detection logic
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
K1: Number of factor 1 levels
|
|
435
|
+
K2: Number of factor 2 levels
|
|
436
|
+
T: Number of time periods
|
|
437
|
+
p_replicated: Proportion of cells with replication (0 to 1)
|
|
438
|
+
n_when_replicated: Number of observations in replicated cells
|
|
439
|
+
mu: Grand mean
|
|
440
|
+
sigma: Within-cell standard deviation
|
|
441
|
+
factor1_effect_size: Std dev of factor 1 effects
|
|
442
|
+
factor2_effect_size: Std dev of factor 2 effects
|
|
443
|
+
time_effect_size: Std dev of time effects
|
|
444
|
+
interaction_effect_size: Std dev of interactions
|
|
445
|
+
seed: Random seed
|
|
446
|
+
replication_pattern: How to assign replication:
|
|
447
|
+
- 'random': Random cells replicated (default)
|
|
448
|
+
- 'early_times': First p_replicated fraction of time periods
|
|
449
|
+
- 'late_times': Last p_replicated fraction of time periods
|
|
450
|
+
- 'checkerboard': Alternating pattern
|
|
451
|
+
- 'corners': Extreme combinations get replication
|
|
452
|
+
include_truth: Include true effects as columns
|
|
453
|
+
factor1_names: Custom factor 1 level names
|
|
454
|
+
factor2_names: Custom factor 2 level names
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
DataFrame with mixed replication structure, including 'cell_type' column
|
|
458
|
+
|
|
459
|
+
Examples:
|
|
460
|
+
>>> # 50% of cells replicated (balanced)
|
|
461
|
+
>>> df = make_sds3(K1=3, K2=2, T=8, p_replicated=0.5, seed=42)
|
|
462
|
+
>>> cell_sizes = df.groupby(['factor 1', 'factor 2', 'time']).size()
|
|
463
|
+
>>> print(f"Singles: {(cell_sizes == 1).sum()}, "
|
|
464
|
+
... f"Replicated: {(cell_sizes > 1).sum()}")
|
|
465
|
+
|
|
466
|
+
Validation:
|
|
467
|
+
- At least one cell with n=1
|
|
468
|
+
- At least one cell with n≥2
|
|
469
|
+
- Actual replication proportion within ±25% of target
|
|
470
|
+
- All factor-time combinations present
|
|
471
|
+
|
|
472
|
+
See Also:
|
|
473
|
+
make_sds1: For fully replicated designs
|
|
474
|
+
make_sds2: For unreplicated designs
|
|
475
|
+
"""
|
|
476
|
+
rng = np.random.default_rng(seed)
|
|
477
|
+
|
|
478
|
+
# Generate true effects
|
|
479
|
+
rho = rng.normal(0, factor1_effect_size, K1)
|
|
480
|
+
phi = rng.normal(0, factor2_effect_size, K2)
|
|
481
|
+
tau = rng.normal(0, time_effect_size, T)
|
|
482
|
+
inter = rng.normal(0, interaction_effect_size, (K1, K2, T))
|
|
483
|
+
|
|
484
|
+
if factor1_names is None:
|
|
485
|
+
factor1_names = [f'F1_{k + 1}' for k in range(K1)]
|
|
486
|
+
elif len(factor1_names) != K1:
|
|
487
|
+
raise ValidationError('Length of factor1_names must equal K1')
|
|
488
|
+
|
|
489
|
+
if factor2_names is None:
|
|
490
|
+
factor2_names = [f'F2_{k + 1}' for k in range(K2)]
|
|
491
|
+
elif len(factor2_names) != K2:
|
|
492
|
+
raise ValidationError('Length of factor2_names must equal K2')
|
|
493
|
+
|
|
494
|
+
rows = []
|
|
495
|
+
replicated_cells = []
|
|
496
|
+
unreplicated_cells = []
|
|
497
|
+
total_cells = K1 * K2 * T
|
|
498
|
+
|
|
499
|
+
for k1 in range(K1):
|
|
500
|
+
for k2 in range(K2):
|
|
501
|
+
for t in range(T):
|
|
502
|
+
# Determine replication based on pattern
|
|
503
|
+
if replication_pattern == 'random':
|
|
504
|
+
is_replicated = rng.random() < p_replicated
|
|
505
|
+
|
|
506
|
+
elif replication_pattern == 'early_times':
|
|
507
|
+
is_replicated = t < int(T * p_replicated)
|
|
508
|
+
|
|
509
|
+
elif replication_pattern == 'late_times':
|
|
510
|
+
is_replicated = t >= int(T * (1 - p_replicated))
|
|
511
|
+
|
|
512
|
+
elif replication_pattern == 'checkerboard':
|
|
513
|
+
is_replicated = (k1 + k2 + t) % 2 == 0
|
|
514
|
+
|
|
515
|
+
elif replication_pattern == 'corners':
|
|
516
|
+
is_corner = (k1 == 0 or k1 == K1 - 1) or (t == 0 or t == T - 1)
|
|
517
|
+
is_replicated = is_corner or (rng.random() < p_replicated / 2)
|
|
518
|
+
|
|
519
|
+
else:
|
|
520
|
+
raise ValidationError(
|
|
521
|
+
f'Unknown replication pattern: {replication_pattern}. '
|
|
522
|
+
f"Valid: 'random', 'early_times', 'late_times', "
|
|
523
|
+
f"'checkerboard', 'corners'"
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
# Determine n for this cell
|
|
527
|
+
n = n_when_replicated if is_replicated else 1
|
|
528
|
+
|
|
529
|
+
# Track cell types for validation
|
|
530
|
+
if is_replicated:
|
|
531
|
+
replicated_cells.append((k1, k2, t))
|
|
532
|
+
else:
|
|
533
|
+
unreplicated_cells.append((k1, k2, t))
|
|
534
|
+
|
|
535
|
+
# Generate observations
|
|
536
|
+
for _i in range(n):
|
|
537
|
+
epsilon = rng.normal(0, sigma)
|
|
538
|
+
y = mu + rho[k1] + phi[k2] + tau[t] + inter[k1, k2, t] + epsilon
|
|
539
|
+
|
|
540
|
+
row = {
|
|
541
|
+
'time': t + 1,
|
|
542
|
+
'factor 1': factor1_names[k1],
|
|
543
|
+
'factor 2': factor2_names[k2],
|
|
544
|
+
'y': y,
|
|
545
|
+
'cell_type': 'replicated' if is_replicated else 'unreplicated',
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if include_truth:
|
|
549
|
+
row.update(
|
|
550
|
+
{
|
|
551
|
+
'true_factor1_effect': rho[k1],
|
|
552
|
+
'true_factor2_effect': phi[k2],
|
|
553
|
+
'true_time_effect': tau[t],
|
|
554
|
+
'true_interaction': inter[k1, k2, t],
|
|
555
|
+
'true_error': epsilon,
|
|
556
|
+
}
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
rows.append(row)
|
|
560
|
+
|
|
561
|
+
df = pd.DataFrame(rows)
|
|
562
|
+
|
|
563
|
+
# Validation - ensure true SDS3 (mixed replication)
|
|
564
|
+
cell_counts = df.groupby(['factor 1', 'factor 2', 'time']).size()
|
|
565
|
+
has_singles = (cell_counts == 1).any()
|
|
566
|
+
has_multiples = (cell_counts >= 2).any()
|
|
567
|
+
|
|
568
|
+
if not (has_singles and has_multiples):
|
|
569
|
+
raise ValidationError(
|
|
570
|
+
f'SDS3 validation failed: must have both single and multiple observations. '
|
|
571
|
+
f'Got singles={has_singles}, multiples={has_multiples}. '
|
|
572
|
+
f'Try adjusting p_replicated or replication_pattern.'
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
# Check replication proportion
|
|
576
|
+
actual_p = len(replicated_cells) / total_cells
|
|
577
|
+
if abs(actual_p - p_replicated) > 0.25: # Allow 25% tolerance
|
|
578
|
+
logger.warning(f'SDS3: target replication {p_replicated:.1%}, actual {actual_p:.1%} (outside 25% tolerance)')
|
|
579
|
+
|
|
580
|
+
logger.debug(
|
|
581
|
+
f'Generated SDS3 data: {len(df)} obs, '
|
|
582
|
+
f'{len(replicated_cells)} replicated cells ({actual_p:.1%}), '
|
|
583
|
+
f'{len(unreplicated_cells)} unreplicated cells'
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
return df
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
# ============================================================================
|
|
590
|
+
# SDS 4: Incomplete grid, occupied cells replicated (N >= 2)
|
|
591
|
+
# ============================================================================
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def make_sds4(
|
|
595
|
+
K1: int = 3,
|
|
596
|
+
K2: int = 2,
|
|
597
|
+
T: int = 6,
|
|
598
|
+
p_drop: float = 0.25,
|
|
599
|
+
n_min: int = 2,
|
|
600
|
+
n_max: int = 5,
|
|
601
|
+
mu: float = 50.0,
|
|
602
|
+
sigma: float = 0.4,
|
|
603
|
+
factor1_effect_size: float = 2.0,
|
|
604
|
+
factor2_effect_size: float = 1.5,
|
|
605
|
+
time_effect_size: float = 1.0,
|
|
606
|
+
seed: int | None = None,
|
|
607
|
+
factor1_names: list[str] | None = None,
|
|
608
|
+
factor2_names: list[str] | None = None,
|
|
609
|
+
) -> pd.DataFrame:
|
|
610
|
+
"""
|
|
611
|
+
Generate Observed Design State 4 (ODS 4) data.
|
|
612
|
+
|
|
613
|
+
ODS 4 is Bishop's classification for incomplete designs where every
|
|
614
|
+
occupied (factor × time) cell carries at least 2 observations. Some
|
|
615
|
+
cells are missing entirely (no data was collected there). After NA
|
|
616
|
+
filtering during tidying, ODS 4 collapses to ADS 1 (full replication
|
|
617
|
+
on the surviving grid).
|
|
618
|
+
|
|
619
|
+
Characteristics
|
|
620
|
+
---------------
|
|
621
|
+
- K1 × K2 × T grid with a fraction of cells empty (no observations)
|
|
622
|
+
- Every occupied cell has n_min..n_max observations (n_min ≥ 2)
|
|
623
|
+
- Bishop's structural ODS 4 classification by the N_kt distribution
|
|
624
|
+
|
|
625
|
+
Args:
|
|
626
|
+
K1, K2: Number of levels for factor 1 and factor 2
|
|
627
|
+
T: Number of time periods
|
|
628
|
+
p_drop: Approximate fraction of cells to leave empty (0 < p_drop < 1).
|
|
629
|
+
The exact number is ``max(1, int(p_drop * K1 * K2 * T))``, clipped
|
|
630
|
+
so at least one cell remains occupied.
|
|
631
|
+
n_min, n_max: Replication range for occupied cells. ``n_min`` must be
|
|
632
|
+
at least 2 for the result to classify as ODS 4 (not ODS 6).
|
|
633
|
+
mu, sigma: Grand mean and within-cell error standard deviation
|
|
634
|
+
factor1_effect_size, factor2_effect_size, time_effect_size: Standard
|
|
635
|
+
deviations of the corresponding main effects
|
|
636
|
+
seed: Random seed for reproducibility
|
|
637
|
+
factor1_names, factor2_names: Custom factor level names
|
|
638
|
+
|
|
639
|
+
Returns:
|
|
640
|
+
DataFrame with columns ``time``, ``factor 1``, ``factor 2``, ``y``.
|
|
641
|
+
Empty cells appear as single rows with ``y = NaN`` so the SDS
|
|
642
|
+
detector observes them as N_kt = 0.
|
|
643
|
+
|
|
644
|
+
Examples:
|
|
645
|
+
>>> df = make_sds4(K1=3, K2=2, T=6, seed=42)
|
|
646
|
+
>>> # After formulate(), study.observed_design_state.sds == 4
|
|
647
|
+
>>> # After tidy/NA drop, study.analytical_design_state.sds == 1
|
|
648
|
+
"""
|
|
649
|
+
if not 0 < p_drop < 1:
|
|
650
|
+
raise ValidationError(f'p_drop must be in (0, 1), got {p_drop}')
|
|
651
|
+
if n_min < 2:
|
|
652
|
+
raise ValidationError(f'ODS 4 requires n_min >= 2 (no singletons), got {n_min}')
|
|
653
|
+
if n_max < n_min:
|
|
654
|
+
raise ValidationError(f'n_max ({n_max}) must be >= n_min ({n_min})')
|
|
655
|
+
|
|
656
|
+
rng = np.random.default_rng(seed)
|
|
657
|
+
|
|
658
|
+
rho = rng.normal(0, factor1_effect_size, K1)
|
|
659
|
+
phi = rng.normal(0, factor2_effect_size, K2)
|
|
660
|
+
tau = rng.normal(0, time_effect_size, T)
|
|
661
|
+
|
|
662
|
+
if factor1_names is None:
|
|
663
|
+
factor1_names = [f'F1_{k + 1}' for k in range(K1)]
|
|
664
|
+
elif len(factor1_names) != K1:
|
|
665
|
+
raise ValidationError(f'Length of factor1_names ({len(factor1_names)}) must equal K1 ({K1})')
|
|
666
|
+
if factor2_names is None:
|
|
667
|
+
factor2_names = [f'F2_{k + 1}' for k in range(K2)]
|
|
668
|
+
elif len(factor2_names) != K2:
|
|
669
|
+
raise ValidationError(f'Length of factor2_names ({len(factor2_names)}) must equal K2 ({K2})')
|
|
670
|
+
|
|
671
|
+
total_cells = K1 * K2 * T
|
|
672
|
+
n_drop = max(1, int(p_drop * total_cells))
|
|
673
|
+
n_drop = min(n_drop, total_cells - 1)
|
|
674
|
+
dropped = set(rng.choice(total_cells, size=n_drop, replace=False).tolist())
|
|
675
|
+
|
|
676
|
+
rows = []
|
|
677
|
+
cell_idx = 0
|
|
678
|
+
for k1 in range(K1):
|
|
679
|
+
for k2 in range(K2):
|
|
680
|
+
for t in range(T):
|
|
681
|
+
if cell_idx in dropped:
|
|
682
|
+
rows.append(
|
|
683
|
+
{
|
|
684
|
+
'time': t + 1,
|
|
685
|
+
'factor 1': factor1_names[k1],
|
|
686
|
+
'factor 2': factor2_names[k2],
|
|
687
|
+
'y': np.nan,
|
|
688
|
+
}
|
|
689
|
+
)
|
|
690
|
+
else:
|
|
691
|
+
n = int(rng.integers(n_min, n_max + 1))
|
|
692
|
+
for _ in range(n):
|
|
693
|
+
eps = rng.normal(0, sigma)
|
|
694
|
+
y = mu + rho[k1] + phi[k2] + tau[t] + eps
|
|
695
|
+
rows.append(
|
|
696
|
+
{
|
|
697
|
+
'time': t + 1,
|
|
698
|
+
'factor 1': factor1_names[k1],
|
|
699
|
+
'factor 2': factor2_names[k2],
|
|
700
|
+
'y': y,
|
|
701
|
+
}
|
|
702
|
+
)
|
|
703
|
+
cell_idx += 1
|
|
704
|
+
|
|
705
|
+
df = pd.DataFrame(rows)
|
|
706
|
+
|
|
707
|
+
logger.debug(
|
|
708
|
+
f'Generated ODS 4 data: {K1}x{K2}x{T} grid, {n_drop}/{total_cells} cells empty, '
|
|
709
|
+
f'{len(df)} total rows ({df["y"].notna().sum()} valid responses)'
|
|
710
|
+
)
|
|
711
|
+
return df
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
# ============================================================================
|
|
715
|
+
# SDS 5: Incomplete grid, occupied cells unreplicated (N = 1)
|
|
716
|
+
# ============================================================================
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def make_sds5(
|
|
720
|
+
K1: int = 3,
|
|
721
|
+
K2: int = 2,
|
|
722
|
+
T: int = 6,
|
|
723
|
+
p_drop: float = 0.25,
|
|
724
|
+
mu: float = 50.0,
|
|
725
|
+
sigma: float = 0.4,
|
|
726
|
+
factor1_effect_size: float = 2.0,
|
|
727
|
+
factor2_effect_size: float = 1.5,
|
|
728
|
+
time_effect_size: float = 1.0,
|
|
729
|
+
seed: int | None = None,
|
|
730
|
+
factor1_names: list[str] | None = None,
|
|
731
|
+
factor2_names: list[str] | None = None,
|
|
732
|
+
) -> pd.DataFrame:
|
|
733
|
+
"""
|
|
734
|
+
Generate Observed Design State 5 (ODS 5) data.
|
|
735
|
+
|
|
736
|
+
ODS 5 is Bishop's classification for incomplete designs where every
|
|
737
|
+
occupied (factor × time) cell carries exactly one observation. Some
|
|
738
|
+
cells are missing entirely. After NA filtering during tidying, ODS 5
|
|
739
|
+
collapses to ADS 2 (no replication on the surviving grid).
|
|
740
|
+
|
|
741
|
+
Characteristics
|
|
742
|
+
---------------
|
|
743
|
+
- K1 × K2 × T grid with a fraction of cells empty
|
|
744
|
+
- Every occupied cell has exactly one observation
|
|
745
|
+
- Bishop's structural ODS 5 classification by the N_kt distribution
|
|
746
|
+
|
|
747
|
+
Args:
|
|
748
|
+
K1, K2: Number of levels for factor 1 and factor 2
|
|
749
|
+
T: Number of time periods
|
|
750
|
+
p_drop: Approximate fraction of cells to leave empty (0 < p_drop < 1).
|
|
751
|
+
The exact number is ``max(1, int(p_drop * K1 * K2 * T))``, clipped
|
|
752
|
+
so at least one cell remains occupied.
|
|
753
|
+
mu, sigma: Grand mean and within-cell error standard deviation
|
|
754
|
+
factor1_effect_size, factor2_effect_size, time_effect_size: Standard
|
|
755
|
+
deviations of the corresponding main effects
|
|
756
|
+
seed: Random seed for reproducibility
|
|
757
|
+
factor1_names, factor2_names: Custom factor level names
|
|
758
|
+
|
|
759
|
+
Returns:
|
|
760
|
+
DataFrame with columns ``time``, ``factor 1``, ``factor 2``, ``y``.
|
|
761
|
+
Empty cells appear as single rows with ``y = NaN``.
|
|
762
|
+
|
|
763
|
+
Examples:
|
|
764
|
+
>>> df = make_sds5(K1=3, K2=2, T=6, seed=42)
|
|
765
|
+
>>> # After formulate(), study.observed_design_state.sds == 5
|
|
766
|
+
>>> # After tidy/NA drop, study.analytical_design_state.sds == 2
|
|
767
|
+
"""
|
|
768
|
+
if not 0 < p_drop < 1:
|
|
769
|
+
raise ValidationError(f'p_drop must be in (0, 1), got {p_drop}')
|
|
770
|
+
|
|
771
|
+
rng = np.random.default_rng(seed)
|
|
772
|
+
|
|
773
|
+
rho = rng.normal(0, factor1_effect_size, K1)
|
|
774
|
+
phi = rng.normal(0, factor2_effect_size, K2)
|
|
775
|
+
tau = rng.normal(0, time_effect_size, T)
|
|
776
|
+
|
|
777
|
+
if factor1_names is None:
|
|
778
|
+
factor1_names = [f'F1_{k + 1}' for k in range(K1)]
|
|
779
|
+
elif len(factor1_names) != K1:
|
|
780
|
+
raise ValidationError(f'Length of factor1_names ({len(factor1_names)}) must equal K1 ({K1})')
|
|
781
|
+
if factor2_names is None:
|
|
782
|
+
factor2_names = [f'F2_{k + 1}' for k in range(K2)]
|
|
783
|
+
elif len(factor2_names) != K2:
|
|
784
|
+
raise ValidationError(f'Length of factor2_names ({len(factor2_names)}) must equal K2 ({K2})')
|
|
785
|
+
|
|
786
|
+
total_cells = K1 * K2 * T
|
|
787
|
+
n_drop = max(1, int(p_drop * total_cells))
|
|
788
|
+
n_drop = min(n_drop, total_cells - 1)
|
|
789
|
+
dropped = set(rng.choice(total_cells, size=n_drop, replace=False).tolist())
|
|
790
|
+
|
|
791
|
+
rows = []
|
|
792
|
+
cell_idx = 0
|
|
793
|
+
for k1 in range(K1):
|
|
794
|
+
for k2 in range(K2):
|
|
795
|
+
for t in range(T):
|
|
796
|
+
if cell_idx in dropped:
|
|
797
|
+
rows.append(
|
|
798
|
+
{
|
|
799
|
+
'time': t + 1,
|
|
800
|
+
'factor 1': factor1_names[k1],
|
|
801
|
+
'factor 2': factor2_names[k2],
|
|
802
|
+
'y': np.nan,
|
|
803
|
+
}
|
|
804
|
+
)
|
|
805
|
+
else:
|
|
806
|
+
eps = rng.normal(0, sigma)
|
|
807
|
+
y = mu + rho[k1] + phi[k2] + tau[t] + eps
|
|
808
|
+
rows.append(
|
|
809
|
+
{
|
|
810
|
+
'time': t + 1,
|
|
811
|
+
'factor 1': factor1_names[k1],
|
|
812
|
+
'factor 2': factor2_names[k2],
|
|
813
|
+
'y': y,
|
|
814
|
+
}
|
|
815
|
+
)
|
|
816
|
+
cell_idx += 1
|
|
817
|
+
|
|
818
|
+
df = pd.DataFrame(rows)
|
|
819
|
+
|
|
820
|
+
logger.debug(
|
|
821
|
+
f'Generated ODS 5 data: {K1}x{K2}x{T} grid, {n_drop}/{total_cells} cells empty, '
|
|
822
|
+
f'every occupied cell has N=1, {df["y"].notna().sum()} valid responses'
|
|
823
|
+
)
|
|
824
|
+
return df
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
# ============================================================================
|
|
828
|
+
# SDS 6: Incomplete grid, mixed replication (some N=1, some N>=2)
|
|
829
|
+
# ============================================================================
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def make_sds6( # noqa: C901
|
|
833
|
+
K1: int = 3,
|
|
834
|
+
K2: int = 2,
|
|
835
|
+
T: int = 6,
|
|
836
|
+
p_drop: float = 0.25,
|
|
837
|
+
p_singleton: float = 0.4,
|
|
838
|
+
n_min: int = 2,
|
|
839
|
+
n_max: int = 5,
|
|
840
|
+
mu: float = 50.0,
|
|
841
|
+
sigma: float = 0.4,
|
|
842
|
+
factor1_effect_size: float = 2.0,
|
|
843
|
+
factor2_effect_size: float = 1.5,
|
|
844
|
+
time_effect_size: float = 1.0,
|
|
845
|
+
seed: int | None = None,
|
|
846
|
+
factor1_names: list[str] | None = None,
|
|
847
|
+
factor2_names: list[str] | None = None,
|
|
848
|
+
) -> pd.DataFrame:
|
|
849
|
+
"""
|
|
850
|
+
Generate Observed Design State 6 (ODS 6) data.
|
|
851
|
+
|
|
852
|
+
ODS 6 is Bishop's classification for incomplete designs where occupied
|
|
853
|
+
cells carry a mix of singleton (N=1) and replicated (N>=2) observations.
|
|
854
|
+
Some cells are missing entirely. After NA filtering during tidying,
|
|
855
|
+
ODS 6 collapses to ADS 3 (mixed replication on the surviving grid).
|
|
856
|
+
|
|
857
|
+
Characteristics
|
|
858
|
+
---------------
|
|
859
|
+
- K1 × K2 × T grid with a fraction of cells empty
|
|
860
|
+
- Among occupied cells, some have exactly 1 observation; others have
|
|
861
|
+
``n_min..n_max`` observations
|
|
862
|
+
- Bishop's structural ODS 6 classification by the N_kt distribution
|
|
863
|
+
(must have BOTH singletons AND replicated cells)
|
|
864
|
+
|
|
865
|
+
Args:
|
|
866
|
+
K1, K2: Number of levels for factor 1 and factor 2
|
|
867
|
+
T: Number of time periods
|
|
868
|
+
p_drop: Approximate fraction of cells to leave empty (0 < p_drop < 1)
|
|
869
|
+
p_singleton: Among occupied cells, probability of singleton vs
|
|
870
|
+
replicated (0 < p_singleton < 1). The generator guarantees at
|
|
871
|
+
least one of each so the result classifies as ODS 6, not ODS 4/5.
|
|
872
|
+
n_min, n_max: Replication range for the replicated cells (>= 2)
|
|
873
|
+
mu, sigma: Grand mean and within-cell error standard deviation
|
|
874
|
+
factor1_effect_size, factor2_effect_size, time_effect_size: Standard
|
|
875
|
+
deviations of the corresponding main effects
|
|
876
|
+
seed: Random seed for reproducibility
|
|
877
|
+
factor1_names, factor2_names: Custom factor level names
|
|
878
|
+
|
|
879
|
+
Returns:
|
|
880
|
+
DataFrame with columns ``time``, ``factor 1``, ``factor 2``, ``y``.
|
|
881
|
+
Empty cells appear as single rows with ``y = NaN``.
|
|
882
|
+
|
|
883
|
+
Examples:
|
|
884
|
+
>>> df = make_sds6(K1=3, K2=2, T=6, seed=42)
|
|
885
|
+
>>> # After formulate(), study.observed_design_state.sds == 6
|
|
886
|
+
>>> # After tidy/NA drop, study.analytical_design_state.sds == 3
|
|
887
|
+
"""
|
|
888
|
+
if not 0 < p_drop < 1:
|
|
889
|
+
raise ValidationError(f'p_drop must be in (0, 1), got {p_drop}')
|
|
890
|
+
if not 0 < p_singleton < 1:
|
|
891
|
+
raise ValidationError(f'p_singleton must be in (0, 1), got {p_singleton}')
|
|
892
|
+
if n_min < 2:
|
|
893
|
+
raise ValidationError(f'n_min must be >= 2 (replicated cells), got {n_min}')
|
|
894
|
+
if n_max < n_min:
|
|
895
|
+
raise ValidationError(f'n_max ({n_max}) must be >= n_min ({n_min})')
|
|
896
|
+
|
|
897
|
+
rng = np.random.default_rng(seed)
|
|
898
|
+
|
|
899
|
+
rho = rng.normal(0, factor1_effect_size, K1)
|
|
900
|
+
phi = rng.normal(0, factor2_effect_size, K2)
|
|
901
|
+
tau = rng.normal(0, time_effect_size, T)
|
|
902
|
+
|
|
903
|
+
if factor1_names is None:
|
|
904
|
+
factor1_names = [f'F1_{k + 1}' for k in range(K1)]
|
|
905
|
+
elif len(factor1_names) != K1:
|
|
906
|
+
raise ValidationError(f'Length of factor1_names ({len(factor1_names)}) must equal K1 ({K1})')
|
|
907
|
+
if factor2_names is None:
|
|
908
|
+
factor2_names = [f'F2_{k + 1}' for k in range(K2)]
|
|
909
|
+
elif len(factor2_names) != K2:
|
|
910
|
+
raise ValidationError(f'Length of factor2_names ({len(factor2_names)}) must equal K2 ({K2})')
|
|
911
|
+
|
|
912
|
+
total_cells = K1 * K2 * T
|
|
913
|
+
n_drop = max(1, int(p_drop * total_cells))
|
|
914
|
+
n_drop = min(n_drop, total_cells - 2) # Need at least 2 occupied cells (1 singleton, 1 replicated)
|
|
915
|
+
if n_drop < 1:
|
|
916
|
+
raise ValidationError(
|
|
917
|
+
f"Grid too small for ODS 6: K1*K2*T={total_cells} can't fit "
|
|
918
|
+
f'both empty cells and at least one singleton + one replicated cell'
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
occupied_indices = [
|
|
922
|
+
i for i in range(total_cells) if i not in set(rng.choice(total_cells, size=n_drop, replace=False).tolist())
|
|
923
|
+
]
|
|
924
|
+
# rebuild dropped set from indices not in occupied
|
|
925
|
+
occupied_set = set(occupied_indices)
|
|
926
|
+
dropped_set = set(range(total_cells)) - occupied_set
|
|
927
|
+
n_occupied = len(occupied_indices)
|
|
928
|
+
|
|
929
|
+
# Assign singleton vs replicated status to each occupied cell;
|
|
930
|
+
# guarantee at least one of each.
|
|
931
|
+
singleton_flags = rng.random(n_occupied) < p_singleton
|
|
932
|
+
if not singleton_flags.any():
|
|
933
|
+
singleton_flags[0] = True
|
|
934
|
+
if singleton_flags.all():
|
|
935
|
+
singleton_flags[-1] = False
|
|
936
|
+
|
|
937
|
+
occupied_to_singleton = dict(zip(occupied_indices, singleton_flags.tolist(), strict=False))
|
|
938
|
+
|
|
939
|
+
rows = []
|
|
940
|
+
cell_idx = 0
|
|
941
|
+
for k1 in range(K1):
|
|
942
|
+
for k2 in range(K2):
|
|
943
|
+
for t in range(T):
|
|
944
|
+
if cell_idx in dropped_set:
|
|
945
|
+
rows.append(
|
|
946
|
+
{
|
|
947
|
+
'time': t + 1,
|
|
948
|
+
'factor 1': factor1_names[k1],
|
|
949
|
+
'factor 2': factor2_names[k2],
|
|
950
|
+
'y': np.nan,
|
|
951
|
+
}
|
|
952
|
+
)
|
|
953
|
+
else:
|
|
954
|
+
n = 1 if occupied_to_singleton[cell_idx] else int(rng.integers(n_min, n_max + 1))
|
|
955
|
+
for _ in range(n):
|
|
956
|
+
eps = rng.normal(0, sigma)
|
|
957
|
+
y = mu + rho[k1] + phi[k2] + tau[t] + eps
|
|
958
|
+
rows.append(
|
|
959
|
+
{
|
|
960
|
+
'time': t + 1,
|
|
961
|
+
'factor 1': factor1_names[k1],
|
|
962
|
+
'factor 2': factor2_names[k2],
|
|
963
|
+
'y': y,
|
|
964
|
+
}
|
|
965
|
+
)
|
|
966
|
+
cell_idx += 1
|
|
967
|
+
|
|
968
|
+
df = pd.DataFrame(rows)
|
|
969
|
+
|
|
970
|
+
logger.debug(
|
|
971
|
+
f'Generated ODS 6 data: {K1}x{K2}x{T} grid, {n_drop}/{total_cells} cells empty, '
|
|
972
|
+
f'{int(singleton_flags.sum())} singletons + '
|
|
973
|
+
f'{int((~singleton_flags).sum())} replicated cells, '
|
|
974
|
+
f'{df["y"].notna().sum()} valid responses'
|
|
975
|
+
)
|
|
976
|
+
return df
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
# ============================================================================
|
|
980
|
+
# Convenience Functions
|
|
981
|
+
# ============================================================================
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def make_design(
|
|
985
|
+
state: int,
|
|
986
|
+
K1: int = 3,
|
|
987
|
+
K2: int = 2,
|
|
988
|
+
T: int = 8,
|
|
989
|
+
seed: int | None = None,
|
|
990
|
+
**kwargs,
|
|
991
|
+
) -> pd.DataFrame:
|
|
992
|
+
"""
|
|
993
|
+
Generate synthetic data classifying as the requested design state.
|
|
994
|
+
|
|
995
|
+
Bishop's design-state scale assigns an integer 1-6 to each (factor x
|
|
996
|
+
time) grid based on the N_kt distribution. ``make_design(state=N)``
|
|
997
|
+
generates data whose Observed Design State (ODS) classification
|
|
998
|
+
equals ``N``. Uses the three-state vocabulary (PDS / ODS / ADS);
|
|
999
|
+
the integer ``state`` is Bishop's reference scale.
|
|
1000
|
+
|
|
1001
|
+
Args:
|
|
1002
|
+
state: Bishop design-state code (1, 2, 3, 4, 5, or 6).
|
|
1003
|
+
K1: Number of factor 1 levels.
|
|
1004
|
+
K2: Number of factor 2 levels.
|
|
1005
|
+
T: Number of time periods.
|
|
1006
|
+
seed: Random seed for reproducibility.
|
|
1007
|
+
**kwargs: Additional keyword arguments forwarded to the
|
|
1008
|
+
state-specific generator (mix params like ``p_replicated``,
|
|
1009
|
+
``p_drop``, ``p_singleton``, ``n_min``, ``n_max``).
|
|
1010
|
+
|
|
1011
|
+
Returns:
|
|
1012
|
+
DataFrame with columns ``time``, ``factor 1``, ``factor 2``,
|
|
1013
|
+
``y`` whose ODS classification equals ``state``. After
|
|
1014
|
+
NA-filtering during tidying, ODS 4/5/6 collapse to ADS 1/2/3.
|
|
1015
|
+
|
|
1016
|
+
Examples:
|
|
1017
|
+
>>> # Round-trip: generated ODS equals requested state
|
|
1018
|
+
>>> for s in (1, 2, 3, 4, 5, 6):
|
|
1019
|
+
... df = make_design(state=s, seed=42)
|
|
1020
|
+
... study = pb.ProcessBehavior(df).formulate(
|
|
1021
|
+
... response='y',
|
|
1022
|
+
... factors=['factor 1', 'factor 2'],
|
|
1023
|
+
... time='time',
|
|
1024
|
+
... )
|
|
1025
|
+
... assert study.observed_design_state.sds == s
|
|
1026
|
+
|
|
1027
|
+
Raises:
|
|
1028
|
+
ValidationError: If state not in [1, 2, 3, 4, 5, 6].
|
|
1029
|
+
"""
|
|
1030
|
+
generators = {1: make_sds1, 2: make_sds2, 3: make_sds3, 4: make_sds4, 5: make_sds5, 6: make_sds6}
|
|
1031
|
+
|
|
1032
|
+
if state not in generators:
|
|
1033
|
+
raise ValidationError(
|
|
1034
|
+
f'Design state {state} not implemented. Available states: {sorted(generators.keys())}'
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
return generators[state](K1=K1, K2=K2, T=T, seed=seed, **kwargs)
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
# ============================================================================
|
|
1041
|
+
# Edge Cases and Special Scenarios
|
|
1042
|
+
# ============================================================================
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def make_edge_cases() -> dict[str, pd.DataFrame]:
|
|
1046
|
+
"""
|
|
1047
|
+
Generate challenging edge cases for testing robustness.
|
|
1048
|
+
|
|
1049
|
+
These datasets test how well the system handles unusual but real situations:
|
|
1050
|
+
- Missing cells (incomplete factorial)
|
|
1051
|
+
- Extreme imbalance (one cell with many obs, others with few)
|
|
1052
|
+
- Single observation (should fail gracefully)
|
|
1053
|
+
- All same value (zero variance)
|
|
1054
|
+
- Extreme outliers
|
|
1055
|
+
- Mixed data quality
|
|
1056
|
+
|
|
1057
|
+
Returns:
|
|
1058
|
+
Dictionary of DataFrames, keyed by edge case name
|
|
1059
|
+
|
|
1060
|
+
Examples:
|
|
1061
|
+
>>> edge_cases = make_edge_cases()
|
|
1062
|
+
>>> list(edge_cases.keys())
|
|
1063
|
+
['missing_cells', 'extreme_imbalance', 'single_obs',
|
|
1064
|
+
'zero_variance', 'outliers', 'mixed_quality']
|
|
1065
|
+
|
|
1066
|
+
>>> # Test each edge case
|
|
1067
|
+
>>> for name, df in edge_cases.items():
|
|
1068
|
+
... try:
|
|
1069
|
+
... result = Analysis(df, spec).calculate()
|
|
1070
|
+
... print(f"{name}: PASSED")
|
|
1071
|
+
... except Exception as e:
|
|
1072
|
+
... print(f"{name}: {type(e).__name__}")
|
|
1073
|
+
|
|
1074
|
+
Use Cases:
|
|
1075
|
+
- Regression testing for error handling
|
|
1076
|
+
- Validating input validation logic
|
|
1077
|
+
- Testing graceful degradation
|
|
1078
|
+
- Documentation of failure modes
|
|
1079
|
+
"""
|
|
1080
|
+
cases = {}
|
|
1081
|
+
|
|
1082
|
+
# 1. Missing cells (incomplete factorial)
|
|
1083
|
+
cases['missing_cells'] = pd.DataFrame(
|
|
1084
|
+
{
|
|
1085
|
+
'time': [1, 1, 2, 2, 3], # No time=3 for factor B
|
|
1086
|
+
'factor 1': ['A', 'A', 'A', 'B', 'A'],
|
|
1087
|
+
'factor 2': ['NA'] * 5,
|
|
1088
|
+
'y': [10.1, 10.2, 11.0, 12.0, 10.5],
|
|
1089
|
+
}
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
# 2. Extreme imbalance (one cell dominates)
|
|
1093
|
+
cases['extreme_imbalance'] = pd.DataFrame(
|
|
1094
|
+
{
|
|
1095
|
+
'time': [1] * 20 + [2] * 2 + [3] * 20, # Time 2 has only 2 obs
|
|
1096
|
+
'factor 1': ['A'] * 20 + ['B'] * 2 + ['A'] * 20,
|
|
1097
|
+
'factor 2': ['NA'] * 42,
|
|
1098
|
+
'y': np.random.normal(50, 2, 42),
|
|
1099
|
+
}
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
# 3. Single observation total (should fail)
|
|
1103
|
+
cases['single_obs'] = pd.DataFrame({'time': [1], 'factor 1': ['A'], 'factor 2': ['NA'], 'y': [50.0]})
|
|
1104
|
+
|
|
1105
|
+
# 4. Zero variance (all values identical)
|
|
1106
|
+
cases['zero_variance'] = pd.DataFrame(
|
|
1107
|
+
{
|
|
1108
|
+
'time': [1, 1, 2, 2, 3, 3],
|
|
1109
|
+
'factor 1': ['A', 'A', 'B', 'B', 'A', 'B'],
|
|
1110
|
+
'factor 2': ['NA'] * 6,
|
|
1111
|
+
'y': [50.0] * 6, # All identical
|
|
1112
|
+
}
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
# 5. Extreme outliers
|
|
1116
|
+
rng = np.random.default_rng(42)
|
|
1117
|
+
y_with_outliers = list(rng.normal(50, 1, 18))
|
|
1118
|
+
y_with_outliers[5] = 100.0 # Extreme high
|
|
1119
|
+
y_with_outliers[12] = 0.0 # Extreme low
|
|
1120
|
+
|
|
1121
|
+
cases['outliers'] = pd.DataFrame(
|
|
1122
|
+
{
|
|
1123
|
+
'time': [1, 1, 2, 2, 3, 3] * 3,
|
|
1124
|
+
'factor 1': ['A'] * 6 + ['B'] * 6 + ['C'] * 6,
|
|
1125
|
+
'factor 2': ['NA'] * 18,
|
|
1126
|
+
'y': y_with_outliers,
|
|
1127
|
+
}
|
|
1128
|
+
)
|
|
1129
|
+
|
|
1130
|
+
# 6. Mixed quality (some cells have high variance, others low)
|
|
1131
|
+
y_mixed = (
|
|
1132
|
+
list(rng.normal(50, 0.1, 6)) # Low variance
|
|
1133
|
+
+ list(rng.normal(50, 5.0, 6)) # High variance
|
|
1134
|
+
+ list(rng.normal(50, 0.5, 6))
|
|
1135
|
+
) # Medium variance
|
|
1136
|
+
|
|
1137
|
+
cases['mixed_quality'] = pd.DataFrame(
|
|
1138
|
+
{
|
|
1139
|
+
'time': [1, 1, 2, 2, 3, 3] * 3,
|
|
1140
|
+
'factor 1': ['A'] * 6 + ['B'] * 6 + ['C'] * 6,
|
|
1141
|
+
'factor 2': ['NA'] * 18,
|
|
1142
|
+
'y': y_mixed,
|
|
1143
|
+
}
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
# 7. Single cell with replication (rest empty)
|
|
1147
|
+
cases['single_cell_replicated'] = pd.DataFrame(
|
|
1148
|
+
{'time': [1] * 5, 'factor 1': ['A'] * 5, 'factor 2': ['NA'] * 5, 'y': [49.5, 50.0, 50.5, 50.2, 49.8]}
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
# 8. Nearly empty (very sparse data)
|
|
1152
|
+
cases['sparse'] = pd.DataFrame(
|
|
1153
|
+
{
|
|
1154
|
+
'time': [1, 5, 10, 15, 20],
|
|
1155
|
+
'factor 1': ['A', 'B', 'A', 'C', 'B'],
|
|
1156
|
+
'factor 2': ['NA'] * 5,
|
|
1157
|
+
'y': [50.1, 49.8, 50.3, 49.5, 50.0],
|
|
1158
|
+
}
|
|
1159
|
+
)
|
|
1160
|
+
|
|
1161
|
+
return cases
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
# ============================================================================
|
|
1165
|
+
# Validation and Comparison Utilities
|
|
1166
|
+
# ============================================================================
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def compare_sds_characteristics(
|
|
1170
|
+
sds_list: list[int] | None = None, K1: int = 3, K2: int = 2, T: int = 8, seed: int = 42
|
|
1171
|
+
) -> pd.DataFrame:
|
|
1172
|
+
"""
|
|
1173
|
+
Generate comparison table of SDS characteristics.
|
|
1174
|
+
|
|
1175
|
+
Creates a summary table comparing key features of each SDS type,
|
|
1176
|
+
useful for documentation and teaching.
|
|
1177
|
+
|
|
1178
|
+
Args:
|
|
1179
|
+
sds_list: List of SDS to compare (default: all)
|
|
1180
|
+
K1: Factor 1 levels for comparison
|
|
1181
|
+
K2: Factor 2 levels for comparison
|
|
1182
|
+
T: Time periods for comparison
|
|
1183
|
+
seed: Random seed
|
|
1184
|
+
|
|
1185
|
+
Returns:
|
|
1186
|
+
DataFrame with SDS comparison metrics
|
|
1187
|
+
|
|
1188
|
+
Example:
|
|
1189
|
+
>>> summary = compare_sds_characteristics()
|
|
1190
|
+
>>> print(summary)
|
|
1191
|
+
|
|
1192
|
+
sds n_obs n_cells min_n max_n has_replication complete_grid
|
|
1193
|
+
1 144 48 2 4 True True
|
|
1194
|
+
2 48 48 1 1 False True
|
|
1195
|
+
3 96 48 1 3 Mixed True
|
|
1196
|
+
4 40 40 1 1 False False
|
|
1197
|
+
5 38 38 1 1 False False
|
|
1198
|
+
6 84 84 1 1 False False
|
|
1199
|
+
"""
|
|
1200
|
+
if sds_list is None:
|
|
1201
|
+
sds_list = [1, 2, 3, 4, 5, 6]
|
|
1202
|
+
|
|
1203
|
+
rows = []
|
|
1204
|
+
for sds in sds_list:
|
|
1205
|
+
df = make_design(sds, K1=K1, K2=K2, T=T, seed=seed)
|
|
1206
|
+
|
|
1207
|
+
if 'factor 1' in df.columns and 'time' in df.columns:
|
|
1208
|
+
cell_counts = df.groupby(['factor 1', 'time']).size()
|
|
1209
|
+
complete = len(cell_counts) == df['factor 1'].nunique() * df['time'].nunique()
|
|
1210
|
+
else:
|
|
1211
|
+
cell_counts = pd.Series([1] * len(df))
|
|
1212
|
+
complete = False
|
|
1213
|
+
|
|
1214
|
+
min_n = cell_counts.min()
|
|
1215
|
+
max_n = cell_counts.max()
|
|
1216
|
+
|
|
1217
|
+
if min_n == max_n == 1:
|
|
1218
|
+
replication = 'False'
|
|
1219
|
+
elif min_n >= 2:
|
|
1220
|
+
replication = 'True'
|
|
1221
|
+
else:
|
|
1222
|
+
replication = 'Mixed'
|
|
1223
|
+
|
|
1224
|
+
rows.append(
|
|
1225
|
+
{
|
|
1226
|
+
'sds': sds,
|
|
1227
|
+
'n_obs': len(df),
|
|
1228
|
+
'n_cells': len(cell_counts),
|
|
1229
|
+
'min_n': min_n,
|
|
1230
|
+
'max_n': max_n,
|
|
1231
|
+
'has_replication': replication,
|
|
1232
|
+
'complete_grid': complete,
|
|
1233
|
+
}
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
return pd.DataFrame(rows)
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
def validate_sds_detection(df: pd.DataFrame, expected_sds: int, spec: 'FormulationSpec') -> bool:
|
|
1240
|
+
"""
|
|
1241
|
+
Validate that generated data is correctly classified as expected SDS.
|
|
1242
|
+
|
|
1243
|
+
This function is useful for testing SDS detection algorithms by
|
|
1244
|
+
generating data with known SDS and verifying correct classification.
|
|
1245
|
+
|
|
1246
|
+
Args:
|
|
1247
|
+
df: DataFrame to validate
|
|
1248
|
+
expected_sds: Expected SDS classification (1-6)
|
|
1249
|
+
spec: FormulationSpec object
|
|
1250
|
+
|
|
1251
|
+
Returns:
|
|
1252
|
+
True if detected SDS matches expected
|
|
1253
|
+
|
|
1254
|
+
Example:
|
|
1255
|
+
>>> df = make_sds1(K1=3, K2=2, T=8, seed=42)
|
|
1256
|
+
>>> spec = FormulationSpec(response_var='Y', rsg_vars=('F1', 'F2'), time_var='T')
|
|
1257
|
+
>>> validate_sds_detection(df, expected_sds=1, spec)
|
|
1258
|
+
True
|
|
1259
|
+
|
|
1260
|
+
Note:
|
|
1261
|
+
This requires importing from analysis_dataset module.
|
|
1262
|
+
Kept here for convenience in testing.
|
|
1263
|
+
"""
|
|
1264
|
+
try:
|
|
1265
|
+
from ..analysis_dataset import AnalysisDataSet
|
|
1266
|
+
|
|
1267
|
+
ads = AnalysisDataSet(df=df, spec=spec, observed_sds=expected_sds)
|
|
1268
|
+
detected_sds = ads.observed_design_state
|
|
1269
|
+
|
|
1270
|
+
if detected_sds == expected_sds:
|
|
1271
|
+
logger.info(f'✓ Correctly detected SDS {expected_sds}')
|
|
1272
|
+
return True
|
|
1273
|
+
else:
|
|
1274
|
+
logger.error(f'✗ SDS detection mismatch: expected {expected_sds}, detected {detected_sds}')
|
|
1275
|
+
return False
|
|
1276
|
+
|
|
1277
|
+
except ImportError:
|
|
1278
|
+
logger.warning('Cannot validate SDS detection: analysis_dataset module not available')
|
|
1279
|
+
return False
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
# ============================================================================
|
|
1283
|
+
# Batch Generation Utilities
|
|
1284
|
+
# ============================================================================
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
def generate_test_suite(output_dir: str = 'test_data', seed: int = 42) -> dict[str, str]:
|
|
1288
|
+
"""
|
|
1289
|
+
Generate complete suite of test datasets and save to disk.
|
|
1290
|
+
|
|
1291
|
+
Creates CSV files for all SDS types with multiple variations,
|
|
1292
|
+
useful for:
|
|
1293
|
+
- Regression testing
|
|
1294
|
+
- Benchmarking
|
|
1295
|
+
- Documentation examples
|
|
1296
|
+
- Training materials
|
|
1297
|
+
|
|
1298
|
+
Args:
|
|
1299
|
+
output_dir: Directory to save CSV files
|
|
1300
|
+
seed: Random seed for reproducibility
|
|
1301
|
+
|
|
1302
|
+
Returns:
|
|
1303
|
+
Dictionary mapping dataset names to file paths
|
|
1304
|
+
|
|
1305
|
+
Example:
|
|
1306
|
+
>>> files = generate_test_suite(output_dir='datasets/test')
|
|
1307
|
+
>>> print(f"Generated {len(files)} test datasets")
|
|
1308
|
+
>>> for name, path in files.items():
|
|
1309
|
+
... print(f" {name}: {path}")
|
|
1310
|
+
|
|
1311
|
+
File Structure:
|
|
1312
|
+
test_data/
|
|
1313
|
+
sds1_basic.csv
|
|
1314
|
+
sds1_large.csv
|
|
1315
|
+
sds1_with_truth.csv
|
|
1316
|
+
sds2_basic.csv
|
|
1317
|
+
sds2_large.csv
|
|
1318
|
+
...
|
|
1319
|
+
"""
|
|
1320
|
+
import os
|
|
1321
|
+
|
|
1322
|
+
# Create output directory
|
|
1323
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
1324
|
+
|
|
1325
|
+
files = {}
|
|
1326
|
+
|
|
1327
|
+
# SDS 1 variations
|
|
1328
|
+
files['sds1_basic'] = os.path.join(output_dir, 'sds1_basic.csv')
|
|
1329
|
+
df = make_sds1(K1=3, K2=2, T=8, seed=seed)
|
|
1330
|
+
df.to_csv(files['sds1_basic'], index=False)
|
|
1331
|
+
|
|
1332
|
+
files['sds1_large'] = os.path.join(output_dir, 'sds1_large.csv')
|
|
1333
|
+
df = make_sds1(K1=5, K2=2, T=12, n_min=3, n_max=5, seed=seed)
|
|
1334
|
+
df.to_csv(files['sds1_large'], index=False)
|
|
1335
|
+
|
|
1336
|
+
files['sds1_with_truth'] = os.path.join(output_dir, 'sds1_with_truth.csv')
|
|
1337
|
+
df = make_sds1(K1=3, K2=2, T=6, include_truth=True, seed=seed)
|
|
1338
|
+
df.to_csv(files['sds1_with_truth'], index=False)
|
|
1339
|
+
|
|
1340
|
+
# SDS 2 variations
|
|
1341
|
+
files['sds2_basic'] = os.path.join(output_dir, 'sds2_basic.csv')
|
|
1342
|
+
df = make_sds2(K1=3, K2=2, T=10, seed=seed)
|
|
1343
|
+
df.to_csv(files['sds2_basic'], index=False)
|
|
1344
|
+
|
|
1345
|
+
files['sds2_large'] = os.path.join(output_dir, 'sds2_large.csv')
|
|
1346
|
+
df = make_sds2(K1=6, K2=2, T=16, seed=seed)
|
|
1347
|
+
df.to_csv(files['sds2_large'], index=False)
|
|
1348
|
+
|
|
1349
|
+
# SDS 3 variations (critical!)
|
|
1350
|
+
files['sds3_balanced'] = os.path.join(output_dir, 'sds3_balanced.csv')
|
|
1351
|
+
df = make_sds3(K1=3, K2=2, T=8, p_replicated=0.5, seed=seed)
|
|
1352
|
+
df.to_csv(files['sds3_balanced'], index=False)
|
|
1353
|
+
|
|
1354
|
+
files['sds3_sparse'] = os.path.join(output_dir, 'sds3_sparse.csv')
|
|
1355
|
+
df = make_sds3(K1=4, K2=2, T=10, p_replicated=0.2, seed=seed)
|
|
1356
|
+
df.to_csv(files['sds3_sparse'], index=False)
|
|
1357
|
+
|
|
1358
|
+
files['sds3_early_times'] = os.path.join(output_dir, 'sds3_early_times.csv')
|
|
1359
|
+
df = make_sds3(K1=3, K2=2, T=12, replication_pattern='early_times', seed=seed)
|
|
1360
|
+
df.to_csv(files['sds3_early_times'], index=False)
|
|
1361
|
+
|
|
1362
|
+
# SDS 4 variations (ODS 4: incomplete grid, all occupied cells replicated)
|
|
1363
|
+
files['sds4_default'] = os.path.join(output_dir, 'sds4_default.csv')
|
|
1364
|
+
df = make_sds4(K1=3, K2=2, T=8, seed=seed)
|
|
1365
|
+
df.to_csv(files['sds4_default'], index=False)
|
|
1366
|
+
|
|
1367
|
+
files['sds4_sparse'] = os.path.join(output_dir, 'sds4_sparse.csv')
|
|
1368
|
+
df = make_sds4(K1=3, K2=2, T=10, p_drop=0.5, seed=seed)
|
|
1369
|
+
df.to_csv(files['sds4_sparse'], index=False)
|
|
1370
|
+
|
|
1371
|
+
# SDS 5 variations (ODS 5: incomplete grid, all occupied cells singleton)
|
|
1372
|
+
files['sds5_default'] = os.path.join(output_dir, 'sds5_default.csv')
|
|
1373
|
+
df = make_sds5(K1=3, K2=2, T=8, seed=seed)
|
|
1374
|
+
df.to_csv(files['sds5_default'], index=False)
|
|
1375
|
+
|
|
1376
|
+
files['sds5_sparse'] = os.path.join(output_dir, 'sds5_sparse.csv')
|
|
1377
|
+
df = make_sds5(K1=4, K2=3, T=12, p_drop=0.5, seed=seed)
|
|
1378
|
+
df.to_csv(files['sds5_sparse'], index=False)
|
|
1379
|
+
|
|
1380
|
+
# SDS 6 variations (ODS 6: incomplete grid, mixed singleton / replicated)
|
|
1381
|
+
files['sds6_default'] = os.path.join(output_dir, 'sds6_default.csv')
|
|
1382
|
+
df = make_sds6(K1=3, K2=2, T=8, seed=seed)
|
|
1383
|
+
df.to_csv(files['sds6_default'], index=False)
|
|
1384
|
+
|
|
1385
|
+
files['sds6_singleton_heavy'] = os.path.join(output_dir, 'sds6_singleton_heavy.csv')
|
|
1386
|
+
df = make_sds6(K1=4, K2=2, T=10, p_singleton=0.7, seed=seed)
|
|
1387
|
+
df.to_csv(files['sds6_singleton_heavy'], index=False)
|
|
1388
|
+
|
|
1389
|
+
# Edge cases
|
|
1390
|
+
edge_cases = make_edge_cases()
|
|
1391
|
+
for name, df in edge_cases.items():
|
|
1392
|
+
files[f'edge_{name}'] = os.path.join(output_dir, f'edge_{name}.csv')
|
|
1393
|
+
df.to_csv(files[f'edge_{name}'], index=False)
|
|
1394
|
+
|
|
1395
|
+
logger.info(f'Generated {len(files)} test datasets in {output_dir}')
|
|
1396
|
+
|
|
1397
|
+
return files
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
# ============================================================================
|
|
1401
|
+
# Documentation and Metadata
|
|
1402
|
+
# ============================================================================
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
def get_sds_info(sds: int) -> dict[str, str]:
|
|
1406
|
+
"""
|
|
1407
|
+
Get detailed information about a specific SDS type.
|
|
1408
|
+
|
|
1409
|
+
Returns metadata and usage guidance for each SDS.
|
|
1410
|
+
|
|
1411
|
+
Args:
|
|
1412
|
+
sds: Sampling Design State (1-6)
|
|
1413
|
+
|
|
1414
|
+
Returns:
|
|
1415
|
+
Dictionary with description, characteristics, use cases, etc.
|
|
1416
|
+
|
|
1417
|
+
Example:
|
|
1418
|
+
>>> info = get_sds_info(3)
|
|
1419
|
+
>>> print(info['description'])
|
|
1420
|
+
>>> print(info['when_to_use'])
|
|
1421
|
+
"""
|
|
1422
|
+
sds_metadata = {
|
|
1423
|
+
1: {
|
|
1424
|
+
'name': 'Full Replication',
|
|
1425
|
+
'description': 'Every (factor, time) cell has n≥2 observations',
|
|
1426
|
+
'characteristics': [
|
|
1427
|
+
'True within-cell variance estimation',
|
|
1428
|
+
'Complete (factor × time) grid',
|
|
1429
|
+
'Supports full interaction analysis',
|
|
1430
|
+
'Most statistically powerful',
|
|
1431
|
+
],
|
|
1432
|
+
'when_to_use': [
|
|
1433
|
+
'Testing Xbar-S chart algorithms',
|
|
1434
|
+
'Full factorial experiments',
|
|
1435
|
+
'When resources allow complete replication',
|
|
1436
|
+
'Baseline for comparing other SDS',
|
|
1437
|
+
],
|
|
1438
|
+
'chart_types': ['Xbar-S', 'Range charts on subgroups'],
|
|
1439
|
+
'vas_capabilities': ['All residuals R1-R5', 'Full interaction effects'],
|
|
1440
|
+
'common_in': 'Designed experiments, validation studies',
|
|
1441
|
+
},
|
|
1442
|
+
2: {
|
|
1443
|
+
'name': 'No Replication',
|
|
1444
|
+
'description': 'Exactly one observation per (factor, time) cell',
|
|
1445
|
+
'characteristics': [
|
|
1446
|
+
'Complete (factor × time) grid',
|
|
1447
|
+
'No within-cell variance',
|
|
1448
|
+
'Must use moving average for R2',
|
|
1449
|
+
'Error confounded with interaction',
|
|
1450
|
+
],
|
|
1451
|
+
'when_to_use': [
|
|
1452
|
+
'Resource-constrained experiments',
|
|
1453
|
+
'Screening studies',
|
|
1454
|
+
'Testing moving average algorithms',
|
|
1455
|
+
'When interaction assumed small',
|
|
1456
|
+
],
|
|
1457
|
+
'chart_types': ['IMR on cell means', 'Moving range methods'],
|
|
1458
|
+
'vas_capabilities': ['R1-R5 with approximated R2', 'Limited interaction analysis'],
|
|
1459
|
+
'common_in': 'Designed experiments, DOE, initial screening',
|
|
1460
|
+
},
|
|
1461
|
+
3: {
|
|
1462
|
+
'name': 'Partial Replication',
|
|
1463
|
+
'description': 'Mixed: some cells n=1, others n≥2',
|
|
1464
|
+
'characteristics': [
|
|
1465
|
+
'Most common in real-world data',
|
|
1466
|
+
'Requires hybrid R2 estimation',
|
|
1467
|
+
'Irregular replication pattern',
|
|
1468
|
+
'Challenging to analyze correctly',
|
|
1469
|
+
],
|
|
1470
|
+
'when_to_use': [
|
|
1471
|
+
'Testing hybrid variance estimation',
|
|
1472
|
+
'Real-world manufacturing data',
|
|
1473
|
+
'Unplanned replication scenarios',
|
|
1474
|
+
'Missing data situations',
|
|
1475
|
+
],
|
|
1476
|
+
'chart_types': ['Hybrid approaches', 'Adaptive methods'],
|
|
1477
|
+
'vas_capabilities': ['Hybrid R2 calculation', 'Partial interaction analysis'],
|
|
1478
|
+
'common_in': 'Most real production data, unplanned sampling',
|
|
1479
|
+
},
|
|
1480
|
+
4: {
|
|
1481
|
+
'name': 'Single Condition Over Time',
|
|
1482
|
+
'description': 'One factor level, multiple time points (K=1)',
|
|
1483
|
+
'characteristics': [
|
|
1484
|
+
'Time series structure',
|
|
1485
|
+
'No grouping by factors',
|
|
1486
|
+
'May show trends or drift',
|
|
1487
|
+
'Classic control chart scenario',
|
|
1488
|
+
],
|
|
1489
|
+
'when_to_use': [
|
|
1490
|
+
'Single process monitoring',
|
|
1491
|
+
'Time series analysis',
|
|
1492
|
+
'Trend detection',
|
|
1493
|
+
'Classic SPC applications',
|
|
1494
|
+
],
|
|
1495
|
+
'chart_types': ['IMR', 'Individuals chart', 'Moving range'],
|
|
1496
|
+
'vas_capabilities': ['Limited (no factor structure)', 'Time effects only'],
|
|
1497
|
+
'common_in': 'Single machine/process, continuous monitoring',
|
|
1498
|
+
},
|
|
1499
|
+
5: {
|
|
1500
|
+
'name': 'Nested Design',
|
|
1501
|
+
'description': 'Hierarchical factors with asynchronous coverage',
|
|
1502
|
+
'characteristics': [
|
|
1503
|
+
'Factor 2 nested in Factor 1',
|
|
1504
|
+
'Irregular temporal patterns',
|
|
1505
|
+
'Not all combinations at all times',
|
|
1506
|
+
'Requires variance components',
|
|
1507
|
+
],
|
|
1508
|
+
'when_to_use': [
|
|
1509
|
+
'Multi-head machines (heads in lanes)',
|
|
1510
|
+
'Hierarchical production structures',
|
|
1511
|
+
'Nested experimental designs',
|
|
1512
|
+
'Asynchronous operations',
|
|
1513
|
+
],
|
|
1514
|
+
'chart_types': ['Hierarchical charts', 'Nested variance components'],
|
|
1515
|
+
'vas_capabilities': ['Nested effects', 'Variance component estimation'],
|
|
1516
|
+
'common_in': 'Multi-head fillers, multi-spindle machines, nested processes',
|
|
1517
|
+
},
|
|
1518
|
+
6: {
|
|
1519
|
+
'name': 'Unstructured / Regime Changes',
|
|
1520
|
+
'description': 'Irregular patterns with process regime changes',
|
|
1521
|
+
'characteristics': [
|
|
1522
|
+
'Incomplete (factor × time) grid',
|
|
1523
|
+
'Mean shifts over time (regimes)',
|
|
1524
|
+
'Irregular sampling patterns',
|
|
1525
|
+
'Complex time structure',
|
|
1526
|
+
],
|
|
1527
|
+
'when_to_use': [
|
|
1528
|
+
'Long-term process studies',
|
|
1529
|
+
'Process with adjustments',
|
|
1530
|
+
'Regime detection problems',
|
|
1531
|
+
'Irregular production schedules',
|
|
1532
|
+
],
|
|
1533
|
+
'chart_types': ['Adaptive methods', 'Change-point detection'],
|
|
1534
|
+
'vas_capabilities': ['Limited', 'Regime-specific analysis'],
|
|
1535
|
+
'common_in': 'Long-term studies, processes with changes, irregular schedules',
|
|
1536
|
+
},
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
if sds not in sds_metadata:
|
|
1540
|
+
raise ValidationError(f'SDS {sds} not defined. Valid: 1-6')
|
|
1541
|
+
|
|
1542
|
+
return sds_metadata[sds]
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
def print_sds_summary():
|
|
1546
|
+
"""
|
|
1547
|
+
Print formatted summary of all SDS types.
|
|
1548
|
+
|
|
1549
|
+
Useful for documentation and teaching.
|
|
1550
|
+
|
|
1551
|
+
Example:
|
|
1552
|
+
>>> print_sds_summary()
|
|
1553
|
+
|
|
1554
|
+
SAMPLING DESIGN STATES SUMMARY
|
|
1555
|
+
==============================
|
|
1556
|
+
|
|
1557
|
+
SDS 1: Full Replication
|
|
1558
|
+
-----------------------
|
|
1559
|
+
Description: Every (factor, time) cell has n≥2 observations
|
|
1560
|
+
...
|
|
1561
|
+
"""
|
|
1562
|
+
print('\nSAMPLING DESIGN STATES SUMMARY')
|
|
1563
|
+
print('=' * 70)
|
|
1564
|
+
|
|
1565
|
+
for sds in range(1, 7):
|
|
1566
|
+
info = get_sds_info(sds)
|
|
1567
|
+
print(f'\nSDS {sds}: {info["name"]}')
|
|
1568
|
+
print('-' * 70)
|
|
1569
|
+
print(f'Description: {info["description"]}')
|
|
1570
|
+
print('\nCharacteristics:')
|
|
1571
|
+
for char in info['characteristics']:
|
|
1572
|
+
print(f' • {char}')
|
|
1573
|
+
print(f'\nCommon in: {info["common_in"]}')
|
|
1574
|
+
print(f'Chart types: {", ".join(info["chart_types"])}')
|
|
1575
|
+
|
|
1576
|
+
|
|
1577
|
+
# ============================================================================
|
|
1578
|
+
# Large Dataset Generator for Performance Testing
|
|
1579
|
+
# ============================================================================
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def make_large_dataset(
|
|
1583
|
+
n_rows: int = 1_000_000,
|
|
1584
|
+
n_extra_cols: int = 46,
|
|
1585
|
+
sds: int = 1,
|
|
1586
|
+
seed: int = 42,
|
|
1587
|
+
) -> pd.DataFrame:
|
|
1588
|
+
"""
|
|
1589
|
+
Generate large dataset for performance testing.
|
|
1590
|
+
|
|
1591
|
+
Creates dataset with specified row count plus extra columns that won't
|
|
1592
|
+
be used in analysis, simulating real-world data dumps with many unused
|
|
1593
|
+
columns.
|
|
1594
|
+
|
|
1595
|
+
This is useful for:
|
|
1596
|
+
- Performance benchmarking
|
|
1597
|
+
- Memory usage testing
|
|
1598
|
+
- Scalability validation
|
|
1599
|
+
- Stress testing the analysis pipeline
|
|
1600
|
+
|
|
1601
|
+
Args:
|
|
1602
|
+
n_rows: Target number of rows (approximate, actual may vary slightly
|
|
1603
|
+
due to SDS structure constraints)
|
|
1604
|
+
n_extra_cols: Extra columns to add beyond the 4 core analysis columns
|
|
1605
|
+
(factor 1, factor 2, time, y). Default 46 gives 50 total.
|
|
1606
|
+
sds: Sampling Design State to generate (1, 2, or 4 supported for large)
|
|
1607
|
+
seed: Random seed for reproducibility
|
|
1608
|
+
|
|
1609
|
+
Returns:
|
|
1610
|
+
DataFrame with approximately n_rows and (4 + n_extra_cols) columns
|
|
1611
|
+
|
|
1612
|
+
Examples:
|
|
1613
|
+
>>> # Generate 1M row dataset with 50 columns
|
|
1614
|
+
>>> df = make_large_dataset(n_rows=1_000_000, n_extra_cols=46)
|
|
1615
|
+
>>> len(df)
|
|
1616
|
+
1000000
|
|
1617
|
+
>>> len(df.columns)
|
|
1618
|
+
50
|
|
1619
|
+
|
|
1620
|
+
>>> # Generate 100K rows for faster testing
|
|
1621
|
+
>>> df = make_large_dataset(n_rows=100_000, seed=42)
|
|
1622
|
+
|
|
1623
|
+
>>> # Test SDS 2 (no replication) at scale
|
|
1624
|
+
>>> df = make_large_dataset(n_rows=500_000, sds=2)
|
|
1625
|
+
|
|
1626
|
+
Notes:
|
|
1627
|
+
- SDS 1: Uses K1 × K2 factors × T times × n replicates = n_rows
|
|
1628
|
+
- SDS 2: Uses K1 × K2 factors × T times = n_rows (no replication)
|
|
1629
|
+
- SDS 4: Uses T time points = n_rows (single factor)
|
|
1630
|
+
- Extra columns are randomly typed (60% float, 20% int, 20% string)
|
|
1631
|
+
to simulate realistic mixed-type data exports.
|
|
1632
|
+
"""
|
|
1633
|
+
rng = np.random.default_rng(seed)
|
|
1634
|
+
|
|
1635
|
+
if sds == 1:
|
|
1636
|
+
# Calculate K1, K2, T, n to hit target rows
|
|
1637
|
+
# K1 × K2 × T × n = n_rows
|
|
1638
|
+
n = 10 # Fixed replication
|
|
1639
|
+
K2 = 2 # Fixed K2
|
|
1640
|
+
cells = n_rows // (n * K2)
|
|
1641
|
+
K1 = int(np.sqrt(cells))
|
|
1642
|
+
T = cells // K1
|
|
1643
|
+
|
|
1644
|
+
df = make_sds1(K1=K1, K2=K2, T=T, n_min=n, n_max=n, seed=seed)
|
|
1645
|
+
|
|
1646
|
+
elif sds == 2:
|
|
1647
|
+
# K1 × K2 × T = n_rows (no replication)
|
|
1648
|
+
K2 = 2 # Fixed K2
|
|
1649
|
+
remaining = n_rows // K2
|
|
1650
|
+
K1 = int(np.sqrt(remaining))
|
|
1651
|
+
T = remaining // K1
|
|
1652
|
+
df = make_sds2(K1=K1, K2=K2, T=T, seed=seed)
|
|
1653
|
+
|
|
1654
|
+
elif sds == 4:
|
|
1655
|
+
# ODS 4: incomplete grid, all occupied cells replicated.
|
|
1656
|
+
# Scale K1, K2, T to roughly hit n_rows total observations.
|
|
1657
|
+
K1 = max(2, int(np.sqrt(n_rows // 8)))
|
|
1658
|
+
K2 = max(2, int(np.sqrt(n_rows // 8)))
|
|
1659
|
+
T = max(2, n_rows // (K1 * K2 * 3))
|
|
1660
|
+
df = make_sds4(K1=K1, K2=K2, T=T, seed=seed)
|
|
1661
|
+
|
|
1662
|
+
else:
|
|
1663
|
+
raise ValidationError(
|
|
1664
|
+
f'SDS {sds} not optimized for large datasets. '
|
|
1665
|
+
f'Supported: 1 (full replication), 2 (no replication), 4 (incomplete grid)'
|
|
1666
|
+
)
|
|
1667
|
+
|
|
1668
|
+
# Add extra columns with mixed data types
|
|
1669
|
+
for i in range(n_extra_cols):
|
|
1670
|
+
dtype_choice = rng.choice(['float', 'int', 'str'], p=[0.6, 0.2, 0.2])
|
|
1671
|
+
|
|
1672
|
+
if dtype_choice == 'float':
|
|
1673
|
+
df[f'extra_{i}'] = rng.normal(0, 100, len(df))
|
|
1674
|
+
elif dtype_choice == 'int':
|
|
1675
|
+
df[f'extra_{i}'] = rng.integers(0, 1000, len(df))
|
|
1676
|
+
else:
|
|
1677
|
+
df[f'extra_{i}'] = rng.choice(['A', 'B', 'C', 'D'], len(df))
|
|
1678
|
+
|
|
1679
|
+
logger.info(f'Generated large dataset: {len(df):,} rows × {len(df.columns)} cols')
|
|
1680
|
+
return df
|
|
1681
|
+
|
|
1682
|
+
|
|
1683
|
+
# ============================================================================
|
|
1684
|
+
# Module-level convenience
|
|
1685
|
+
# ============================================================================
|
|
1686
|
+
|
|
1687
|
+
# Quick access to all generators (SDS 1-6)
|
|
1688
|
+
# Note: SDS 0 was consolidated into SDS 4. Response-only data is now
|
|
1689
|
+
# treated as SDS 4 with implicit time ordering via obs_id.
|
|
1690
|
+
GENERATORS = {1: make_sds1, 2: make_sds2, 3: make_sds3, 4: make_sds4, 5: make_sds5, 6: make_sds6}
|
|
1691
|
+
|
|
1692
|
+
|
|
1693
|
+
if __name__ == '__main__':
|
|
1694
|
+
# Demo: Generate one dataset of each type
|
|
1695
|
+
print('Generating example datasets for each SDS...\n')
|
|
1696
|
+
|
|
1697
|
+
for sds in range(1, 7):
|
|
1698
|
+
print(f'SDS {sds}: {get_sds_info(sds)["name"]}')
|
|
1699
|
+
df = make_design(sds, K1=3, K2=2, T=6, seed=42)
|
|
1700
|
+
print(f' Generated {len(df)} observations')
|
|
1701
|
+
print(f' Columns: {df.columns.tolist()}')
|
|
1702
|
+
print(f' Preview:\n{df.head(3)}')
|
|
1703
|
+
print()
|
|
1704
|
+
|
|
1705
|
+
print('\nEdge cases:')
|
|
1706
|
+
edge_cases = make_edge_cases()
|
|
1707
|
+
for name, df in edge_cases.items():
|
|
1708
|
+
print(f' {name}: {len(df)} observations')
|
|
1709
|
+
|
|
1710
|
+
print('\nFor detailed info on any SDS, use get_sds_info(sds_number)')
|
|
1711
|
+
print('For complete summary, use print_sds_summary()')
|