skxperiments 0.1.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. skxperiments/__init__.py +5 -0
  2. skxperiments/core/__init__.py +42 -0
  3. skxperiments/core/assignment.py +589 -0
  4. skxperiments/core/base.py +512 -0
  5. skxperiments/core/exceptions.py +145 -0
  6. skxperiments/core/potential_outcomes.py +168 -0
  7. skxperiments/core/results.py +624 -0
  8. skxperiments/design/__init__.py +22 -0
  9. skxperiments/design/balance.py +182 -0
  10. skxperiments/design/blocked_crd.py +157 -0
  11. skxperiments/design/crd.py +162 -0
  12. skxperiments/design/factorial.py +174 -0
  13. skxperiments/design/power.py +233 -0
  14. skxperiments/design/rerandomized_crd.py +319 -0
  15. skxperiments/diagnostics/__init__.py +21 -0
  16. skxperiments/diagnostics/aa_test.py +277 -0
  17. skxperiments/diagnostics/balance_report.py +224 -0
  18. skxperiments/diagnostics/srm.py +327 -0
  19. skxperiments/estimators/__init__.py +23 -0
  20. skxperiments/estimators/blocked_difference_in_means.py +197 -0
  21. skxperiments/estimators/cuped.py +280 -0
  22. skxperiments/estimators/difference_in_means.py +161 -0
  23. skxperiments/estimators/factorial_estimator.py +213 -0
  24. skxperiments/estimators/lin_estimator.py +298 -0
  25. skxperiments/inference/__init__.py +17 -0
  26. skxperiments/inference/bootstrap.py +450 -0
  27. skxperiments/inference/multiple.py +365 -0
  28. skxperiments/inference/neyman.py +386 -0
  29. skxperiments/inference/randomization_test.py +319 -0
  30. skxperiments/pipeline.py +366 -0
  31. skxperiments/reporting/__init__.py +30 -0
  32. skxperiments/reporting/plots.py +411 -0
  33. skxperiments/reporting/summary.py +185 -0
  34. skxperiments-0.1.0.dev0.dist-info/METADATA +272 -0
  35. skxperiments-0.1.0.dev0.dist-info/RECORD +36 -0
  36. skxperiments-0.1.0.dev0.dist-info/WHEEL +4 -0
@@ -0,0 +1,5 @@
1
+ """skxperiments: Randomization-based experimental design and causal inference."""
2
+
3
+ __version__ = "0.1.0-dev"
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,42 @@
1
+ """Core module providing base classes, exceptions, and fundamental data structures.
2
+
3
+ This module contains the foundational components of skxperiments:
4
+ - Custom exceptions for clear error reporting
5
+ - PotentialOutcomes for representing unit-level causal quantities
6
+ - Assignment classes as the contract between designs and estimators
7
+ - Results as the uniform output object
8
+ - Abstract base classes for designs, estimators, and inference methods
9
+ """
10
+
11
+ from skxperiments.core.exceptions import (
12
+ DesignEstimatorMismatch,
13
+ InsufficientDataError,
14
+ InvalidDesignError,
15
+ NotFittedError,
16
+ SkxperimentsError,
17
+ )
18
+ from skxperiments.core.potential_outcomes import PotentialOutcomes
19
+ from skxperiments.core.assignment import BaseAssignment, CRDAssignment
20
+ from skxperiments.core.results import Results
21
+ from skxperiments.core.base import (
22
+ BaseDesign,
23
+ BaseEstimator,
24
+ BaseInference,
25
+ DiagnosticsReport,
26
+ )
27
+
28
+ __all__ = [
29
+ "SkxperimentsError",
30
+ "DesignEstimatorMismatch",
31
+ "NotFittedError",
32
+ "InsufficientDataError",
33
+ "InvalidDesignError",
34
+ "PotentialOutcomes",
35
+ "BaseAssignment",
36
+ "CRDAssignment",
37
+ "Results",
38
+ "BaseDesign",
39
+ "BaseEstimator",
40
+ "BaseInference",
41
+ "DiagnosticsReport",
42
+ ]
@@ -0,0 +1,589 @@
1
+ """Assignment classes representing the contract between designs and estimators.
2
+
3
+ An Assignment object is always created by a Design via randomize() — never
4
+ instantiated directly by the user. It carries the treatment assignment
5
+ alongside a copy of the original data, ensuring no side effects.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import Any
10
+
11
+ import numpy as np
12
+ import pandas as pd
13
+
14
+ from skxperiments.core.exceptions import InvalidDesignError
15
+
16
+
17
+ class BaseAssignment(ABC):
18
+ """Abstract base class for all assignment objects.
19
+
20
+ An Assignment is the contract between a design and an estimator.
21
+ Estimators receive Assignment objects, not loose DataFrames.
22
+
23
+ Notes
24
+ -----
25
+ Assignment objects are intended to be immutable after construction.
26
+ Designs are responsible for passing a defensive copy of the input
27
+ DataFrame (i.e., ``df.copy()``) when constructing an Assignment.
28
+ The outcome variable is **not** part of the Assignment contract:
29
+ estimators receive the outcome column name as a parameter at __init__
30
+ and resolve it against ``assignment.data_`` at fit time.
31
+
32
+ Parameters
33
+ ----------
34
+ data : pd.DataFrame
35
+ Copy of the original DataFrame with treatment column added.
36
+ treatment_col : str
37
+ Name of the treatment column.
38
+ design : Any
39
+ Reference to the design that generated this assignment.
40
+
41
+ Attributes
42
+ ----------
43
+ data_ : pd.DataFrame
44
+ DataFrame with treatment column added.
45
+ treatment_col_ : str
46
+ Name of the treatment column.
47
+ design_ : Any
48
+ Reference to the generating design.
49
+ n_units_ : int
50
+ Total number of units.
51
+ n_treated_ : int
52
+ Number of units assigned to treatment.
53
+ n_control_ : int
54
+ Number of units assigned to control.
55
+ """
56
+
57
+ def __init__(
58
+ self,
59
+ data: pd.DataFrame,
60
+ treatment_col: str,
61
+ design: Any,
62
+ ) -> None:
63
+ if treatment_col not in data.columns:
64
+ raise InvalidDesignError(
65
+ f"Treatment column '{treatment_col}' not found in DataFrame. "
66
+ f"Available columns: {list(data.columns)}."
67
+ )
68
+
69
+ self.data_: pd.DataFrame = data
70
+ self.treatment_col_: str = treatment_col
71
+ self.design_: Any = design
72
+ self.n_units_: int = len(data)
73
+ self.n_treated_: int = int((data[treatment_col] == 1).sum())
74
+ self.n_control_: int = int((data[treatment_col] == 0).sum())
75
+
76
+ def _validate_treatment_col(self) -> None:
77
+ """Validate that the treatment column contains only 0 and 1.
78
+
79
+ Raises
80
+ ------
81
+ InvalidDesignError
82
+ If the treatment column contains values other than 0 and 1.
83
+ """
84
+ unique_values = set(self.data_[self.treatment_col_].unique())
85
+ allowed = {0, 1}
86
+ if not unique_values.issubset(allowed):
87
+ invalid = unique_values - allowed
88
+ raise InvalidDesignError(
89
+ f"Treatment column '{self.treatment_col_}' must contain only "
90
+ f"0 and 1, but found values: {sorted(invalid)}."
91
+ )
92
+
93
+ @abstractmethod
94
+ def treated_ids(self) -> np.ndarray:
95
+ """Return iloc positions of treated units.
96
+
97
+ Returns
98
+ -------
99
+ np.ndarray
100
+ Array of integer positions where treatment == 1.
101
+ """
102
+
103
+ @abstractmethod
104
+ def control_ids(self) -> np.ndarray:
105
+ """Return iloc positions of control units.
106
+
107
+ Returns
108
+ -------
109
+ np.ndarray
110
+ Array of integer positions where treatment == 0.
111
+ """
112
+
113
+ @abstractmethod
114
+ def draw(self, seed: int | None = None) -> "BaseAssignment":
115
+ """Generate a new realization of the assignment under the same mechanism.
116
+
117
+ This method enables randomization-based inference: each call returns
118
+ a fresh Assignment object whose treatment vector is a new draw from
119
+ the same randomization mechanism that produced this Assignment
120
+ (same N, same probabilities, same constraints), but with a different
121
+ random seed.
122
+
123
+ The returned Assignment must be of the same concrete type as self
124
+ and reference the same underlying data. Implementations must not
125
+ mutate self.
126
+
127
+ Parameters
128
+ ----------
129
+ seed : int or None, optional
130
+ Random seed for the new draw. If None, a non-deterministic
131
+ draw is performed.
132
+
133
+ Returns
134
+ -------
135
+ BaseAssignment
136
+ New Assignment of the same concrete type, with a freshly drawn
137
+ treatment vector.
138
+
139
+ Notes
140
+ -----
141
+ Consumed by RandomizationTest to generate the null distribution.
142
+ Not intended for direct use by end users.
143
+ """
144
+
145
+ def __repr__(self) -> str:
146
+ """Return string representation.
147
+
148
+ Returns
149
+ -------
150
+ str
151
+ Format: ClassName(n_treated=N, n_control=N)
152
+ """
153
+ class_name = type(self).__name__
154
+ return f"{class_name}(n_treated={self.n_treated_}, n_control={self.n_control_})"
155
+
156
+
157
+ class CRDAssignment(BaseAssignment):
158
+ """Assignment resulting from a Completely Randomized Design.
159
+
160
+ Parameters
161
+ ----------
162
+ data : pd.DataFrame
163
+ Copy of the original DataFrame with treatment column added.
164
+ treatment_col : str
165
+ Name of the treatment column.
166
+ design : Any
167
+ Reference to the design that generated this assignment.
168
+ seed : int or None, optional
169
+ Random seed used for the randomization, by default None.
170
+ rerandomization_metadata : dict or None, optional
171
+ When the assignment was produced by a rerandomization design
172
+ (e.g., ReRandomizedCRD), this dict carries the information
173
+ needed to replay the acceptance/rejection mechanism in
174
+ ``draw()``. Expected keys:
175
+
176
+ - ``"covariates"``: list[str]
177
+ - ``"threshold"``: float
178
+ - ``"cov_matrix"``: np.ndarray (sample covariance, ddof=1, of
179
+ the full DataFrame)
180
+ - ``"attempts"``: int
181
+ - ``"scaling_factor"``: float (1/n_treated + 1/n_control)
182
+
183
+ When None, this is a plain CRD assignment.
184
+
185
+ Attributes
186
+ ----------
187
+ seed_ : int or None
188
+ Seed used in the randomization.
189
+ rerandomization_metadata : dict or None
190
+ Rerandomization metadata, or None if this is a plain CRD
191
+ assignment.
192
+
193
+ Examples
194
+ --------
195
+ >>> import pandas as pd
196
+ >>> df = pd.DataFrame({"x": [1, 2, 3, 4], "treatment": [1, 0, 1, 0]})
197
+ >>> assignment = CRDAssignment(
198
+ ... data=df, treatment_col="treatment", design=None, seed=42
199
+ ... )
200
+ >>> assignment.n_treated_
201
+ 2
202
+ """
203
+
204
+ def __init__(
205
+ self,
206
+ data: pd.DataFrame,
207
+ treatment_col: str,
208
+ design: Any,
209
+ seed: int | None = None,
210
+ rerandomization_metadata: dict | None = None,
211
+ ) -> None:
212
+ super().__init__(data=data, treatment_col=treatment_col, design=design)
213
+ self.seed_: int | None = seed
214
+ self.rerandomization_metadata: dict | None = rerandomization_metadata
215
+ self._validate_treatment_col()
216
+
217
+ def treated_ids(self) -> np.ndarray:
218
+ """Return iloc positions of treated units."""
219
+ return np.where(self.data_[self.treatment_col_].values == 1)[0]
220
+
221
+ def control_ids(self) -> np.ndarray:
222
+ """Return iloc positions of control units."""
223
+ return np.where(self.data_[self.treatment_col_].values == 0)[0]
224
+
225
+ def draw(self, seed: int | None = None) -> "CRDAssignment":
226
+ """Generate a new realization under the same mechanism.
227
+
228
+ For plain CRD, delegates to ``design_.randomize(df_clean)``.
229
+ For rerandomized CRD, delegates to
230
+ ``design_._randomize_with_cached_cov`` so the covariance matrix
231
+ is reused without recomputation.
232
+
233
+ Parameters
234
+ ----------
235
+ seed : int or None, optional
236
+ Random seed for the new draw, by default None.
237
+
238
+ Returns
239
+ -------
240
+ CRDAssignment
241
+ Fresh assignment under the same mechanism.
242
+
243
+ Raises
244
+ ------
245
+ InvalidDesignError
246
+ If ``design_`` is None.
247
+ """
248
+ if self.design_ is None:
249
+ raise InvalidDesignError(
250
+ "Cannot draw a new realization: this CRDAssignment was "
251
+ "constructed without a reference to a generating design."
252
+ )
253
+
254
+ df_clean = self.data_.drop(columns=[self.treatment_col_])
255
+
256
+ if self.rerandomization_metadata is not None:
257
+ # Rerandomized CRD: reuse cached covariance matrix.
258
+ cov_matrix = self.rerandomization_metadata["cov_matrix"]
259
+ rng = np.random.default_rng(seed)
260
+ return self.design_._randomize_with_cached_cov(
261
+ df=df_clean,
262
+ cov_matrix=cov_matrix,
263
+ rng=rng,
264
+ )
265
+
266
+ # Plain CRD: delegate to design.randomize via temporary seed override.
267
+ original_seed = getattr(self.design_, "seed", None)
268
+ try:
269
+ self.design_.seed = seed
270
+ return self.design_.randomize(df_clean)
271
+ finally:
272
+ self.design_.seed = original_seed
273
+
274
+
275
+ class BlockedAssignment(BaseAssignment):
276
+ """Assignment resulting from a Blocked Completely Randomized Design.
277
+
278
+ Treatment is randomized independently within each block, preserving
279
+ the treatment proportion within every block.
280
+
281
+ Parameters
282
+ ----------
283
+ data : pd.DataFrame
284
+ Copy of the original DataFrame with treatment column added.
285
+ treatment_col : str
286
+ Name of the treatment column.
287
+ design : Any
288
+ Reference to the BlockedCRD design that generated this assignment.
289
+ block_col : str
290
+ Name of the column identifying blocks.
291
+ block_sizes : dict
292
+ Mapping from block label to number of units in that block.
293
+ seed : int or None, optional
294
+ Random seed used for the randomization, by default None.
295
+
296
+ Attributes
297
+ ----------
298
+ block_col_ : str
299
+ Name of the block column.
300
+ block_sizes_ : dict
301
+ Mapping from block label to block size.
302
+ seed_ : int or None
303
+ Seed used in the randomization.
304
+
305
+ Notes
306
+ -----
307
+ The block column must exist in ``data``. The treatment proportion
308
+ is preserved within each block; see BlockedCRD for the design that
309
+ produces this Assignment.
310
+ """
311
+
312
+ def __init__(
313
+ self,
314
+ data: pd.DataFrame,
315
+ treatment_col: str,
316
+ design: Any,
317
+ block_col: str,
318
+ block_sizes: dict,
319
+ seed: int | None = None,
320
+ ) -> None:
321
+ super().__init__(data=data, treatment_col=treatment_col, design=design)
322
+ if block_col not in data.columns:
323
+ raise InvalidDesignError(
324
+ f"Block column '{block_col}' not found in DataFrame. "
325
+ f"Available columns: {list(data.columns)}."
326
+ )
327
+ self.block_col_: str = block_col
328
+ self.block_sizes_: dict = dict(block_sizes)
329
+ self.seed_: int | None = seed
330
+ self._validate_treatment_col()
331
+
332
+ def treated_ids(self) -> np.ndarray:
333
+ """Return iloc positions of treated units."""
334
+ return np.where(self.data_[self.treatment_col_].values == 1)[0]
335
+
336
+ def control_ids(self) -> np.ndarray:
337
+ """Return iloc positions of control units."""
338
+ return np.where(self.data_[self.treatment_col_].values == 0)[0]
339
+
340
+ def draw(self, seed: int | None = None) -> "BlockedAssignment":
341
+ """Generate a new realization under the same blocked mechanism.
342
+
343
+ Delegates to the generating design, ensuring the same block
344
+ structure and within-block proportion are respected.
345
+
346
+ Parameters
347
+ ----------
348
+ seed : int or None, optional
349
+ Random seed for the new draw.
350
+
351
+ Returns
352
+ -------
353
+ BlockedAssignment
354
+ New BlockedAssignment with a freshly drawn treatment vector.
355
+
356
+ Raises
357
+ ------
358
+ InvalidDesignError
359
+ If this Assignment was constructed without a reference to
360
+ a generating design.
361
+ """
362
+ if self.design_ is None:
363
+ raise InvalidDesignError(
364
+ "Cannot draw a new realization: this BlockedAssignment "
365
+ "was constructed without a reference to a generating "
366
+ "design."
367
+ )
368
+
369
+ df_clean = self.data_.drop(columns=[self.treatment_col_])
370
+
371
+ original_seed = getattr(self.design_, "seed", None)
372
+ try:
373
+ self.design_.seed = seed
374
+ return self.design_.randomize(df_clean)
375
+ finally:
376
+ self.design_.seed = original_seed
377
+
378
+
379
+ class FactorialAssignment(BaseAssignment):
380
+ """Assignment resulting from a 2^K Factorial Design.
381
+
382
+ Each unit is assigned to one of 2^K cells defined by the values of
383
+ K binary factors. The synthetic column ``"_cell"`` carries an
384
+ integer in [0, 2^K - 1] encoding the cell of each unit.
385
+
386
+ Cell encoding convention (little-endian):
387
+
388
+ cell_index = sum(factor_value * 2**i
389
+ for i, factor_value in enumerate(factor_cols))
390
+
391
+ For K=2 with factors ``["A", "B"]``:
392
+ A=0, B=0 -> cell 0
393
+ A=1, B=0 -> cell 1
394
+ A=0, B=1 -> cell 2
395
+ A=1, B=1 -> cell 3
396
+
397
+ This convention is part of the contract: FactorialEstimator
398
+ (Phase 3) relies on it to reconstruct factor effects from cell
399
+ indices.
400
+
401
+ Notes
402
+ -----
403
+ Unlike CRDAssignment and BlockedAssignment, FactorialAssignment
404
+ does **not** call ``self._validate_treatment_col()`` in __init__.
405
+ The synthetic ``"_cell"`` column carries values in
406
+ [0, 2^K - 1], not a binary 0/1 treatment indicator. Validating it
407
+ against {0, 1} would always fail for K >= 1.
408
+
409
+ Consequently, ``treated_ids()`` and ``control_ids()`` are not
410
+ meaningful for factorial designs and raise NotImplementedError.
411
+ Use ``cell_ids(**factor_values)`` to select units by cell.
412
+
413
+ Parameters
414
+ ----------
415
+ data : pd.DataFrame
416
+ Copy of the original DataFrame with factor columns and
417
+ ``"_cell"`` added.
418
+ design : Any
419
+ Reference to the FactorialDesign that generated this assignment.
420
+ factor_cols : list of str
421
+ Names of the K factors, in the order used to compute cell indices.
422
+ cell_sizes : dict
423
+ Mapping from cell index to number of units in that cell.
424
+ seed : int or None, optional
425
+ Random seed used in the randomization, by default None.
426
+
427
+ Attributes
428
+ ----------
429
+ factor_cols : list of str
430
+ Names of the K factors.
431
+ n_cells_ : int
432
+ Number of cells (2^K).
433
+ cell_sizes_ : dict
434
+ Mapping from cell index to cell size.
435
+ seed_ : int or None
436
+ Seed used in the randomization.
437
+ """
438
+
439
+ def __init__(
440
+ self,
441
+ data: pd.DataFrame,
442
+ design: Any,
443
+ factor_cols: list[str],
444
+ cell_sizes: dict,
445
+ seed: int | None = None,
446
+ ) -> None:
447
+ # Pass "_cell" as treatment_col to satisfy BaseAssignment's
448
+ # interface, but do NOT validate it against {0, 1}: see
449
+ # class docstring.
450
+ super().__init__(data=data, treatment_col="_cell", design=design)
451
+ # Override the binary-coded n_treated_ / n_control_ set by
452
+ # BaseAssignment; they are not meaningful here.
453
+ self.n_treated_ = 0
454
+ self.n_control_ = 0
455
+
456
+ self.factor_cols: list[str] = list(factor_cols)
457
+ self.n_cells_: int = 2 ** len(factor_cols)
458
+ self.cell_sizes_: dict = dict(cell_sizes)
459
+ self.seed_: int | None = seed
460
+
461
+ def treated_ids(self) -> np.ndarray:
462
+ """Not applicable to factorial designs.
463
+
464
+ Raises
465
+ ------
466
+ NotImplementedError
467
+ Always. Use ``cell_ids(**factor_values)`` instead.
468
+ """
469
+ # TODO Fase 3: revisar contrato de treated_ids/control_ids
470
+ # quando FactorialEstimator for implementado — atual
471
+ # NotImplementedError é solução de transição.
472
+ raise NotImplementedError(
473
+ "FactorialAssignment não tem tratamento binário único — "
474
+ "use cell_ids(**factor_values) para selecionar unidades "
475
+ "por célula"
476
+ )
477
+
478
+ def control_ids(self) -> np.ndarray:
479
+ """Not applicable to factorial designs.
480
+
481
+ Raises
482
+ ------
483
+ NotImplementedError
484
+ Always. Use ``cell_ids(**factor_values)`` instead.
485
+ """
486
+ # TODO Fase 3: revisar contrato de treated_ids/control_ids
487
+ # quando FactorialEstimator for implementado — atual
488
+ # NotImplementedError é solução de transição.
489
+ raise NotImplementedError(
490
+ "FactorialAssignment não tem tratamento binário único — "
491
+ "use cell_ids(**factor_values) para selecionar unidades "
492
+ "por célula"
493
+ )
494
+
495
+ def cell_ids(self, **factor_values: int) -> np.ndarray:
496
+ """Return iloc positions of units in the given cell.
497
+
498
+ Parameters
499
+ ----------
500
+ **factor_values : int
501
+ Mapping of factor name to its value (0 or 1). All factors
502
+ in ``factor_cols`` must be specified.
503
+
504
+ Returns
505
+ -------
506
+ np.ndarray
507
+ Array of iloc positions of units matching all specified
508
+ factor values.
509
+
510
+ Raises
511
+ ------
512
+ InvalidDesignError
513
+ If a kwarg is not a known factor name, if a value is not
514
+ 0 or 1, or if not all factors are specified.
515
+
516
+ Examples
517
+ --------
518
+ >>> # For a 2x2 design with factors ["A", "B"]:
519
+ >>> # assignment.cell_ids(A=1, B=0) -> iloc positions of cell 1
520
+ """
521
+ # Validate factor names
522
+ unknown = [k for k in factor_values if k not in self.factor_cols]
523
+ if unknown:
524
+ raise InvalidDesignError(
525
+ f"Unknown factor(s) in cell_ids: {unknown}. "
526
+ f"Known factors: {self.factor_cols}."
527
+ )
528
+
529
+ # Validate all factors specified
530
+ missing = [k for k in self.factor_cols if k not in factor_values]
531
+ if missing:
532
+ raise InvalidDesignError(
533
+ f"cell_ids requires values for all factors. "
534
+ f"Missing: {missing}. Provided: {list(factor_values.keys())}."
535
+ )
536
+
537
+ # Validate values
538
+ for name, value in factor_values.items():
539
+ if value not in (0, 1):
540
+ raise InvalidDesignError(
541
+ f"Factor '{name}' value must be 0 or 1, "
542
+ f"received {value!r}."
543
+ )
544
+
545
+ # Build mask: all factors must match.
546
+ mask = np.ones(self.n_units_, dtype=bool)
547
+ for name in self.factor_cols:
548
+ mask &= self.data_[name].values == factor_values[name]
549
+
550
+ return np.where(mask)[0]
551
+
552
+ def draw(self, seed: int | None = None) -> "FactorialAssignment":
553
+ """Generate a new realization preserving cell sizes.
554
+
555
+ Drops factor columns and ``"_cell"`` from the data before
556
+ delegating to ``design_.randomize`` to avoid name-collision
557
+ errors in randomize().
558
+
559
+ Parameters
560
+ ----------
561
+ seed : int or None, optional
562
+ Random seed for the new draw, by default None.
563
+
564
+ Returns
565
+ -------
566
+ FactorialAssignment
567
+ New assignment with freshly drawn cell allocation.
568
+
569
+ Raises
570
+ ------
571
+ InvalidDesignError
572
+ If ``design_`` is None.
573
+ """
574
+ if self.design_ is None:
575
+ raise InvalidDesignError(
576
+ "Cannot draw a new realization: this FactorialAssignment "
577
+ "was constructed without a reference to a generating "
578
+ "design."
579
+ )
580
+
581
+ cols_to_drop = ["_cell"] + list(self.factor_cols)
582
+ df_clean = self.data_.drop(columns=cols_to_drop)
583
+
584
+ original_seed = getattr(self.design_, "seed", None)
585
+ try:
586
+ self.design_.seed = seed
587
+ return self.design_.randomize(df_clean)
588
+ finally:
589
+ self.design_.seed = original_seed