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.
- skxperiments/__init__.py +5 -0
- skxperiments/core/__init__.py +42 -0
- skxperiments/core/assignment.py +589 -0
- skxperiments/core/base.py +512 -0
- skxperiments/core/exceptions.py +145 -0
- skxperiments/core/potential_outcomes.py +168 -0
- skxperiments/core/results.py +624 -0
- skxperiments/design/__init__.py +22 -0
- skxperiments/design/balance.py +182 -0
- skxperiments/design/blocked_crd.py +157 -0
- skxperiments/design/crd.py +162 -0
- skxperiments/design/factorial.py +174 -0
- skxperiments/design/power.py +233 -0
- skxperiments/design/rerandomized_crd.py +319 -0
- skxperiments/diagnostics/__init__.py +21 -0
- skxperiments/diagnostics/aa_test.py +277 -0
- skxperiments/diagnostics/balance_report.py +224 -0
- skxperiments/diagnostics/srm.py +327 -0
- skxperiments/estimators/__init__.py +23 -0
- skxperiments/estimators/blocked_difference_in_means.py +197 -0
- skxperiments/estimators/cuped.py +280 -0
- skxperiments/estimators/difference_in_means.py +161 -0
- skxperiments/estimators/factorial_estimator.py +213 -0
- skxperiments/estimators/lin_estimator.py +298 -0
- skxperiments/inference/__init__.py +17 -0
- skxperiments/inference/bootstrap.py +450 -0
- skxperiments/inference/multiple.py +365 -0
- skxperiments/inference/neyman.py +386 -0
- skxperiments/inference/randomization_test.py +319 -0
- skxperiments/pipeline.py +366 -0
- skxperiments/reporting/__init__.py +30 -0
- skxperiments/reporting/plots.py +411 -0
- skxperiments/reporting/summary.py +185 -0
- skxperiments-0.1.0.dev0.dist-info/METADATA +272 -0
- skxperiments-0.1.0.dev0.dist-info/RECORD +36 -0
- skxperiments-0.1.0.dev0.dist-info/WHEEL +4 -0
skxperiments/__init__.py
ADDED
|
@@ -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
|