causaltune 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.
- causaltune/__init__.py +30 -0
- causaltune/data_utils.py +223 -0
- causaltune/dataset_processor.py +241 -0
- causaltune/datasets.py +754 -0
- causaltune/memoizer.py +82 -0
- causaltune/models/__init__.py +7 -0
- causaltune/models/dummy.py +156 -0
- causaltune/models/monkey_patch_flaml.py +0 -0
- causaltune/models/monkey_patches.py +117 -0
- causaltune/models/passthrough.py +100 -0
- causaltune/models/regression.py +86 -0
- causaltune/models/transformed_outcome.py +62 -0
- causaltune/models/wrapper.py +147 -0
- causaltune/optimiser.py +915 -0
- causaltune/remote.py +12 -0
- causaltune/score/__init__.py +0 -0
- causaltune/score/bite.py +147 -0
- causaltune/score/erupt.py +138 -0
- causaltune/score/erupt_core.py +128 -0
- causaltune/score/erupt_old.py +245 -0
- causaltune/score/frobenius.py +0 -0
- causaltune/score/r_score.py +282 -0
- causaltune/score/scoring.py +1362 -0
- causaltune/score/thompson.py +115 -0
- causaltune/search/__init__.py +0 -0
- causaltune/search/component.py +171 -0
- causaltune/search/params.py +625 -0
- causaltune/shap.py +44 -0
- causaltune/thirdparty/__init__.py +1 -0
- causaltune/thirdparty/causalml/LICENSE +13 -0
- causaltune/thirdparty/causalml/__init__.py +0 -0
- causaltune/thirdparty/causalml/metrics.py +410 -0
- causaltune/utils.py +152 -0
- causaltune/visualizer.py +182 -0
- causaltune-0.2.0.dist-info/METADATA +290 -0
- causaltune-0.2.0.dist-info/RECORD +39 -0
- causaltune-0.2.0.dist-info/WHEEL +4 -0
- causaltune-0.2.0.dist-info/licenses/LICENSE +176 -0
- causaltune-0.2.0.dist-info/licenses/NOTICE +13 -0
causaltune/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
# Message emitted by dowhy >=0.13 when an EconML estimator is passed by string.
|
|
4
|
+
_ECONML_STRING_DEPRECATION = r".*string to specify the value for econml_estimator.*"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _install_warning_filters() -> None:
|
|
8
|
+
"""Silence the one dowhy deprecation caused by causaltune's string dispatch.
|
|
9
|
+
|
|
10
|
+
causaltune dispatches EconML estimators to dowhy by string ``method_name``
|
|
11
|
+
(e.g. "backdoor.econml.dml.LinearDML"). dowhy >=0.13 deprecated passing the
|
|
12
|
+
EconML estimator as a string in favour of an instance, but the string dispatch
|
|
13
|
+
still works. Migrating to instance dispatch is tracked separately; silence just
|
|
14
|
+
this one deprecation so it does not spam every fit. Exposed as a function so it
|
|
15
|
+
can be re-applied in environments (e.g. pytest) that reset ``warnings.filters``.
|
|
16
|
+
"""
|
|
17
|
+
warnings.filterwarnings(
|
|
18
|
+
"ignore",
|
|
19
|
+
message=_ECONML_STRING_DEPRECATION,
|
|
20
|
+
category=DeprecationWarning,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_install_warning_filters()
|
|
25
|
+
|
|
26
|
+
from causaltune.optimiser import CausalTune # noqa: E402
|
|
27
|
+
from causaltune.visualizer import Visualizer # noqa: E402
|
|
28
|
+
from causaltune.score.scoring import Scorer # noqa: E402
|
|
29
|
+
|
|
30
|
+
__all__ = ["CausalTune", "Visualizer", "Scorer"]
|
causaltune/data_utils.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Any, Union, Optional
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from sklearn.preprocessing import RobustScaler
|
|
7
|
+
|
|
8
|
+
from causaltune.utils import is_sequence
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def featurize(
|
|
12
|
+
df: pd.DataFrame,
|
|
13
|
+
features: List[str],
|
|
14
|
+
exclude_cols: List[str],
|
|
15
|
+
drop_first: bool = False,
|
|
16
|
+
scale_floats: bool = False,
|
|
17
|
+
prune_min_categories: int = 50,
|
|
18
|
+
prune_thresh: float = 0.99,
|
|
19
|
+
) -> pd.DataFrame:
|
|
20
|
+
# fill all the NaNs
|
|
21
|
+
for col, t in zip(df.columns, df.dtypes):
|
|
22
|
+
if pd.api.types.is_float_dtype(t):
|
|
23
|
+
df[col] = df[col].fillna(0.0).astype("float32")
|
|
24
|
+
elif pd.api.types.is_integer_dtype(t):
|
|
25
|
+
df[col] = df[col].fillna(-1)
|
|
26
|
+
df[col] = otherize_tail(df[col], -2, prune_thresh, prune_min_categories)
|
|
27
|
+
else:
|
|
28
|
+
df[col] = df[col].fillna("NA")
|
|
29
|
+
df[col] = otherize_tail(
|
|
30
|
+
df[col], "OTHER", prune_thresh, prune_min_categories
|
|
31
|
+
).astype("category")
|
|
32
|
+
|
|
33
|
+
float_features = [f for f in features if pd.api.types.is_float_dtype(df.dtypes[f])]
|
|
34
|
+
if scale_floats:
|
|
35
|
+
float_df = pd.DataFrame(
|
|
36
|
+
RobustScaler().fit_transform(df[float_features]), columns=float_features
|
|
37
|
+
)
|
|
38
|
+
else:
|
|
39
|
+
float_df = df[float_features].reset_index(drop=True)
|
|
40
|
+
|
|
41
|
+
# cast 0/1 int columns to float single-column dummies
|
|
42
|
+
for col, t in zip(df.columns, df.dtypes):
|
|
43
|
+
if pd.api.types.is_integer_dtype(t):
|
|
44
|
+
if len(df[col].unique()) <= 2:
|
|
45
|
+
df[col] = df[col].fillna(0.0).astype("float32")
|
|
46
|
+
|
|
47
|
+
# for other categories, include first column dummy for easier interpretability
|
|
48
|
+
cat_df = df.drop(columns=exclude_cols + float_features)
|
|
49
|
+
if len(cat_df.columns):
|
|
50
|
+
dummy_df = pd.get_dummies(cat_df, drop_first=drop_first).reset_index(drop=True)
|
|
51
|
+
else:
|
|
52
|
+
dummy_df = pd.DataFrame()
|
|
53
|
+
|
|
54
|
+
out = pd.concat(
|
|
55
|
+
[df[exclude_cols].reset_index(drop=True), float_df, dummy_df], axis=1
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def frequent_values(x: pd.Series, thresh: float = 0.99) -> set:
|
|
62
|
+
# get the most frequent values of a pandas.Series, making up to the fraction thresh of total.
|
|
63
|
+
|
|
64
|
+
data = x.to_frame("value")
|
|
65
|
+
data["dummy"] = True
|
|
66
|
+
tmp = (
|
|
67
|
+
data[["dummy", "value"]]
|
|
68
|
+
.groupby("value", as_index=False)
|
|
69
|
+
.count()
|
|
70
|
+
.sort_values("dummy", ascending=False)
|
|
71
|
+
)
|
|
72
|
+
tmp["frac"] = tmp.dummy.cumsum() / tmp.dummy.sum()
|
|
73
|
+
return set(tmp["value"][tmp.frac <= thresh].unique())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def otherize_tail(
|
|
77
|
+
x: pd.Series, new_val: Any, thresh: float = 0.99, min_categories: int = 20
|
|
78
|
+
) -> pd.Series:
|
|
79
|
+
# convert infrequent values in a pandas.Series to a specified value.
|
|
80
|
+
|
|
81
|
+
uniques = x.unique()
|
|
82
|
+
if len(uniques) < min_categories:
|
|
83
|
+
return x
|
|
84
|
+
else:
|
|
85
|
+
x = x.copy()
|
|
86
|
+
freq = frequent_values(x, thresh)
|
|
87
|
+
x[~x.isin(freq)] = new_val
|
|
88
|
+
return x
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class CausalityDataset:
|
|
93
|
+
data: pd.DataFrame
|
|
94
|
+
treatment: str
|
|
95
|
+
outcomes: List[str]
|
|
96
|
+
common_causes: List[str]
|
|
97
|
+
effect_modifiers: List[str]
|
|
98
|
+
propensity_modifiers: List[str]
|
|
99
|
+
instruments: List[str]
|
|
100
|
+
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
data: pd.DataFrame,
|
|
104
|
+
treatment: str,
|
|
105
|
+
outcomes: Union[str, List[str]],
|
|
106
|
+
common_causes: Optional[List[str]] = None,
|
|
107
|
+
effect_modifiers: Optional[List[str]] = None,
|
|
108
|
+
propensity_modifiers: Optional[List[str]] = None,
|
|
109
|
+
instruments: Optional[List[str]] = None,
|
|
110
|
+
):
|
|
111
|
+
"""
|
|
112
|
+
Implements data logic for CausalTune.
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
assert isinstance(data, pd.DataFrame)
|
|
117
|
+
self.data = data
|
|
118
|
+
|
|
119
|
+
assert isinstance(
|
|
120
|
+
treatment, str
|
|
121
|
+
), "Only a single treatment supported at the moment"
|
|
122
|
+
self.treatment = treatment
|
|
123
|
+
|
|
124
|
+
assert is_sequence(outcomes)
|
|
125
|
+
self.outcomes = [outcomes] if isinstance(outcomes, str) else outcomes
|
|
126
|
+
|
|
127
|
+
# make sure common causes is nonempty
|
|
128
|
+
|
|
129
|
+
if not common_causes:
|
|
130
|
+
# this is a trick to bypass a DoWhy bug
|
|
131
|
+
if "random" not in data.columns:
|
|
132
|
+
self.data["random"] = np.random.randint(0, 2, size=len(data))
|
|
133
|
+
self.common_causes = ["random"]
|
|
134
|
+
else:
|
|
135
|
+
raise ValueError(
|
|
136
|
+
"Column name 'random' is not allowed if common_causes field is missing"
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
assert is_sequence(common_causes) and not isinstance(common_causes, str)
|
|
140
|
+
self.common_causes = common_causes
|
|
141
|
+
|
|
142
|
+
self.instruments = [] if instruments is None else instruments
|
|
143
|
+
|
|
144
|
+
self.propensity_modifiers = (
|
|
145
|
+
[] if propensity_modifiers is None else propensity_modifiers
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
cols_to_exclude = (
|
|
149
|
+
[self.treatment]
|
|
150
|
+
+ self.outcomes
|
|
151
|
+
+ self.instruments
|
|
152
|
+
+ self.common_causes
|
|
153
|
+
+ self.propensity_modifiers
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if effect_modifiers:
|
|
157
|
+
self.effect_modifiers = effect_modifiers
|
|
158
|
+
else:
|
|
159
|
+
self.effect_modifiers = [
|
|
160
|
+
c for c in data.columns if c not in cols_to_exclude
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
all_fields = cols_to_exclude + self.effect_modifiers
|
|
164
|
+
assert len(all_fields) == len(
|
|
165
|
+
set(all_fields)
|
|
166
|
+
), "Using the same column name in different fields is not allowed"
|
|
167
|
+
|
|
168
|
+
for col in all_fields:
|
|
169
|
+
assert col in data.columns, f"Field {col} missing in dataframe"
|
|
170
|
+
|
|
171
|
+
def preprocess_dataset(
|
|
172
|
+
self,
|
|
173
|
+
drop_first: bool = False,
|
|
174
|
+
scale_floats: bool = False,
|
|
175
|
+
prune_min_categories: int = 50,
|
|
176
|
+
prune_thresh: float = 0.99,
|
|
177
|
+
):
|
|
178
|
+
"""Preprocesses input dataset for CausalTune by
|
|
179
|
+
converting treatment and instrument columns to integer, normalizing, filling nans, and one-hot encoding.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
drop_first (bool): whether to drop the first dummy variable for each categorical feature (default False)
|
|
183
|
+
scale_floats (bool): whether to scale float features to have zero mean and unit variance (default False)
|
|
184
|
+
prune_min_categories (int): min number of categories to keep for each categorical feature (default 50)
|
|
185
|
+
prune_thresh (float): threshold for category frequency when pruning categories (default 0.99)
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
None. Modifies self.data in-place by replacing it with the preprocessed dataframe.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
self.data[self.treatment] = self.data[self.treatment].astype(int)
|
|
192
|
+
self.data[self.instruments] = self.data[self.instruments].astype(int)
|
|
193
|
+
|
|
194
|
+
# normalize, fill in nans, one-hot encode all the features
|
|
195
|
+
new_chunks = []
|
|
196
|
+
processed_cols = []
|
|
197
|
+
fields = ["common_causes", "effect_modifiers", "propensity_modifiers"]
|
|
198
|
+
|
|
199
|
+
for col_group in fields:
|
|
200
|
+
cols = self.__dict__[col_group]
|
|
201
|
+
if cols:
|
|
202
|
+
processed_cols += cols
|
|
203
|
+
re_df = featurize(
|
|
204
|
+
self.data[cols],
|
|
205
|
+
features=cols,
|
|
206
|
+
exclude_cols=[],
|
|
207
|
+
drop_first=drop_first,
|
|
208
|
+
scale_floats=scale_floats,
|
|
209
|
+
prune_min_categories=prune_min_categories,
|
|
210
|
+
prune_thresh=prune_thresh,
|
|
211
|
+
)
|
|
212
|
+
new_chunks.append(re_df)
|
|
213
|
+
self.__dict__[col_group] = list(re_df.columns)
|
|
214
|
+
|
|
215
|
+
remainder = self.data[[c for c in self.data.columns if c not in processed_cols]]
|
|
216
|
+
self.data = pd.concat([remainder.reset_index(drop=True)] + new_chunks, axis=1)
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def treatment_values(self):
|
|
220
|
+
return np.sort(self.data[self.treatment].unique())
|
|
221
|
+
|
|
222
|
+
def __len__(self):
|
|
223
|
+
return len(self.data)
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from sklearn.base import BaseEstimator, TransformerMixin
|
|
6
|
+
from category_encoders import OneHotEncoder, OrdinalEncoder, TargetEncoder, WOEEncoder
|
|
7
|
+
from causaltune.data_utils import CausalityDataset
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CausalityDatasetProcessor(BaseEstimator, TransformerMixin):
|
|
11
|
+
"""
|
|
12
|
+
A processor for CausalityDataset, designed to preprocess data for causal inference tasks by encoding, normalizing,
|
|
13
|
+
and handling missing values.
|
|
14
|
+
Attributes:
|
|
15
|
+
encoder_type (str): Type of encoder used for categorical feature encoding ('onehot', 'label', 'target', 'woe').
|
|
16
|
+
outcome (str): The target variable used for encoding.
|
|
17
|
+
encoder: Encoder object used during feature transformations.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
"""
|
|
22
|
+
Initializes CausalityDatasetProcessor with default attributes for encoder_type, outcome, and encoder.
|
|
23
|
+
"""
|
|
24
|
+
self.encoder_type = None
|
|
25
|
+
self.outcome = None
|
|
26
|
+
self.encoder = None
|
|
27
|
+
|
|
28
|
+
def fit(
|
|
29
|
+
self,
|
|
30
|
+
cd: CausalityDataset,
|
|
31
|
+
encoder_type: Optional[str] = "onehot",
|
|
32
|
+
outcome: str = None,
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Fits the processor by preprocessing the input CausalityDataset.
|
|
36
|
+
Args:
|
|
37
|
+
cd (CausalityDataset): The dataset for causal analysis.
|
|
38
|
+
encoder_type (str, optional): Encoder to use for categorical features. Default is 'onehot'.
|
|
39
|
+
outcome (str, optional): The target variable for encoding (needed for 'target' or 'woe'). Default is None.
|
|
40
|
+
Returns:
|
|
41
|
+
CausalityDatasetProcessor: The fitted processor instance.
|
|
42
|
+
"""
|
|
43
|
+
cd = copy.deepcopy(cd)
|
|
44
|
+
self.preprocess_dataset(
|
|
45
|
+
cd, encoder_type=encoder_type, outcome=outcome, fit_phase=True
|
|
46
|
+
)
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
def transform(self, cd: CausalityDataset):
|
|
50
|
+
"""
|
|
51
|
+
Transforms the CausalityDataset using the fitted encoder.
|
|
52
|
+
Args:
|
|
53
|
+
cd (CausalityDataset): Dataset to transform.
|
|
54
|
+
Returns:
|
|
55
|
+
CausalityDataset: Transformed dataset.
|
|
56
|
+
Raises:
|
|
57
|
+
ValueError: If processor has not been trained yet.
|
|
58
|
+
"""
|
|
59
|
+
if self.encoder:
|
|
60
|
+
cd = self.preprocess_dataset(
|
|
61
|
+
cd,
|
|
62
|
+
encoder_type=self.encoder_type,
|
|
63
|
+
outcome=self.outcome,
|
|
64
|
+
fit_phase=False,
|
|
65
|
+
)
|
|
66
|
+
return cd
|
|
67
|
+
else:
|
|
68
|
+
raise ValueError("CausalityDatasetProcessor has not been trained")
|
|
69
|
+
|
|
70
|
+
def featurize(
|
|
71
|
+
self,
|
|
72
|
+
cd: CausalityDataset,
|
|
73
|
+
df: pd.DataFrame,
|
|
74
|
+
features: List[str],
|
|
75
|
+
exclude_cols: List[str],
|
|
76
|
+
drop_first: bool = False,
|
|
77
|
+
encoder_type: str = "onehot",
|
|
78
|
+
outcome: str = None,
|
|
79
|
+
fit_phase: bool = True,
|
|
80
|
+
) -> pd.DataFrame:
|
|
81
|
+
# fill all the NaNs
|
|
82
|
+
categ_columns = []
|
|
83
|
+
for col, t in zip(df.columns, df.dtypes):
|
|
84
|
+
if pd.api.types.is_float_dtype(t):
|
|
85
|
+
df[col] = df[col].fillna(0.0).astype("float32")
|
|
86
|
+
elif pd.api.types.is_integer_dtype(t):
|
|
87
|
+
df[col] = df[col].fillna(-1)
|
|
88
|
+
else:
|
|
89
|
+
df[col] = df[col].fillna("NA").astype("category")
|
|
90
|
+
categ_columns.append(col)
|
|
91
|
+
|
|
92
|
+
float_features = [
|
|
93
|
+
f for f in features if pd.api.types.is_float_dtype(df.dtypes[f])
|
|
94
|
+
]
|
|
95
|
+
float_df = df[float_features].reset_index(drop=True)
|
|
96
|
+
|
|
97
|
+
# cast 0/1 int columns to float single-column dummies
|
|
98
|
+
for col, t in zip(df.columns, df.dtypes):
|
|
99
|
+
if pd.api.types.is_integer_dtype(t):
|
|
100
|
+
if len(df[col].unique()) <= 2:
|
|
101
|
+
df[col] = df[col].fillna(0.0).astype("float32")
|
|
102
|
+
|
|
103
|
+
# for other categories, include first column dummy for easier interpretability
|
|
104
|
+
cat_df = df.drop(columns=exclude_cols + float_features)
|
|
105
|
+
if len(cat_df.columns) and encoder_type:
|
|
106
|
+
if encoder_type == "onehot":
|
|
107
|
+
if fit_phase:
|
|
108
|
+
encoder = OneHotEncoder(
|
|
109
|
+
cols=categ_columns, drop_invariant=drop_first
|
|
110
|
+
)
|
|
111
|
+
dummy_df = encoder.fit_transform(X=cat_df).reset_index(drop=True)
|
|
112
|
+
else:
|
|
113
|
+
dummy_df = self.encoder.transform(X=cat_df).reset_index(drop=True)
|
|
114
|
+
elif encoder_type == "label":
|
|
115
|
+
if fit_phase:
|
|
116
|
+
encoder = OrdinalEncoder(cols=categ_columns)
|
|
117
|
+
dummy_df = encoder.fit_transform(X=cat_df).reset_index(drop=True)
|
|
118
|
+
else:
|
|
119
|
+
dummy_df = self.encoder.transform(X=cat_df).reset_index(drop=True)
|
|
120
|
+
elif encoder_type == "target":
|
|
121
|
+
if outcome:
|
|
122
|
+
y = cd.data[outcome]
|
|
123
|
+
else:
|
|
124
|
+
y = cd.data[cd.outcomes[0]]
|
|
125
|
+
assert (
|
|
126
|
+
len(set(y)) < 10
|
|
127
|
+
), "Using TargetEncoder with continuous target is not allowed"
|
|
128
|
+
if fit_phase:
|
|
129
|
+
encoder = TargetEncoder(cols=categ_columns)
|
|
130
|
+
dummy_df = encoder.fit_transform(X=cat_df, y=y).reset_index(
|
|
131
|
+
drop=True
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
dummy_df = self.encoder.transform(X=cat_df, y=y).reset_index(
|
|
135
|
+
drop=True
|
|
136
|
+
)
|
|
137
|
+
elif encoder_type == "woe":
|
|
138
|
+
if outcome:
|
|
139
|
+
y = cd.data[outcome]
|
|
140
|
+
else:
|
|
141
|
+
y = cd.data[cd.outcomes[0]]
|
|
142
|
+
assert (
|
|
143
|
+
len(set(y)) <= 2
|
|
144
|
+
), "WOEEncoder: the target column y must be binary"
|
|
145
|
+
if fit_phase:
|
|
146
|
+
encoder = WOEEncoder(cols=categ_columns)
|
|
147
|
+
dummy_df = encoder.fit_transform(X=cat_df, y=y).reset_index(
|
|
148
|
+
drop=True
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
dummy_df = self.encoder.transform(X=cat_df, y=y).reset_index(
|
|
152
|
+
drop=True
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
raise ValueError(f"Unsupported encoder type: {encoder_type}")
|
|
156
|
+
else:
|
|
157
|
+
encoder = "no"
|
|
158
|
+
dummy_df = pd.DataFrame()
|
|
159
|
+
|
|
160
|
+
out = pd.concat(
|
|
161
|
+
[df[exclude_cols].reset_index(drop=True), float_df, dummy_df], axis=1
|
|
162
|
+
)
|
|
163
|
+
if fit_phase:
|
|
164
|
+
self.encoder = encoder
|
|
165
|
+
self.encoder_type = encoder_type
|
|
166
|
+
self.outcome = outcome
|
|
167
|
+
|
|
168
|
+
return out
|
|
169
|
+
|
|
170
|
+
def preprocess_dataset(
|
|
171
|
+
self,
|
|
172
|
+
cd: CausalityDataset,
|
|
173
|
+
drop_first: Optional[bool] = False,
|
|
174
|
+
fit_phase: bool = True,
|
|
175
|
+
encoder_type: Optional[str] = "onehot",
|
|
176
|
+
outcome: Optional[str] = None,
|
|
177
|
+
):
|
|
178
|
+
"""Preprocesses input dataset for CausalTune by
|
|
179
|
+
converting treatment and instrument columns to integer, normalizing, filling nans, and one-hot encoding.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
drop_first (bool): whether to drop the first dummy variable for each categorical feature (default False)
|
|
183
|
+
encoder_type (str): Type of encoder to use for categorical features (default 'onehot').
|
|
184
|
+
Available options are:
|
|
185
|
+
- 'onehot': OneHotEncoder
|
|
186
|
+
- 'label': OrdinalEncoder
|
|
187
|
+
- 'target': TargetEncoder
|
|
188
|
+
- 'woe': WOEEncoder
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
None. Modifies self.data in-place by replacing it with the preprocessed dataframe.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
cd.data[cd.treatment] = cd.data[cd.treatment].astype(int)
|
|
195
|
+
cd.data[cd.instruments] = cd.data[cd.instruments].astype(int)
|
|
196
|
+
|
|
197
|
+
# normalize, fill in nans, one-hot encode all the features
|
|
198
|
+
new_chunks = []
|
|
199
|
+
processed_cols = []
|
|
200
|
+
original_columns = cd.data.columns.tolist()
|
|
201
|
+
cols = (
|
|
202
|
+
cd.__dict__["common_causes"]
|
|
203
|
+
+ cd.__dict__["effect_modifiers"]
|
|
204
|
+
+ cd.__dict__["propensity_modifiers"]
|
|
205
|
+
)
|
|
206
|
+
if cols:
|
|
207
|
+
processed_cols += cols
|
|
208
|
+
re_df = self.featurize(
|
|
209
|
+
cd,
|
|
210
|
+
cd.data[cols],
|
|
211
|
+
features=cols,
|
|
212
|
+
exclude_cols=[],
|
|
213
|
+
drop_first=drop_first,
|
|
214
|
+
fit_phase=fit_phase,
|
|
215
|
+
encoder_type=encoder_type,
|
|
216
|
+
outcome=outcome,
|
|
217
|
+
)
|
|
218
|
+
new_chunks.append(re_df)
|
|
219
|
+
|
|
220
|
+
remainder = cd.data[[c for c in cd.data.columns if c not in processed_cols]]
|
|
221
|
+
cd.data = pd.concat([remainder.reset_index(drop=True)] + new_chunks, axis=1)
|
|
222
|
+
|
|
223
|
+
# Columns after one-hot encoding
|
|
224
|
+
new_columns = cd.data.columns.tolist()
|
|
225
|
+
fields = ["common_causes", "effect_modifiers", "propensity_modifiers"]
|
|
226
|
+
# Mapping original columns to new (if one-hot) encoded columns
|
|
227
|
+
column_mapping = {}
|
|
228
|
+
for original_col in original_columns:
|
|
229
|
+
matches = [
|
|
230
|
+
col
|
|
231
|
+
for col in new_columns
|
|
232
|
+
if col.startswith(original_col + "_") or original_col == col
|
|
233
|
+
]
|
|
234
|
+
column_mapping[original_col] = matches
|
|
235
|
+
for col_group in fields:
|
|
236
|
+
updated_columns = []
|
|
237
|
+
for col in cd.__dict__[col_group]:
|
|
238
|
+
updated_columns.extend(column_mapping[col])
|
|
239
|
+
cd.__dict__[col_group] = updated_columns
|
|
240
|
+
|
|
241
|
+
return cd
|