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.
Files changed (39) hide show
  1. causaltune/__init__.py +30 -0
  2. causaltune/data_utils.py +223 -0
  3. causaltune/dataset_processor.py +241 -0
  4. causaltune/datasets.py +754 -0
  5. causaltune/memoizer.py +82 -0
  6. causaltune/models/__init__.py +7 -0
  7. causaltune/models/dummy.py +156 -0
  8. causaltune/models/monkey_patch_flaml.py +0 -0
  9. causaltune/models/monkey_patches.py +117 -0
  10. causaltune/models/passthrough.py +100 -0
  11. causaltune/models/regression.py +86 -0
  12. causaltune/models/transformed_outcome.py +62 -0
  13. causaltune/models/wrapper.py +147 -0
  14. causaltune/optimiser.py +915 -0
  15. causaltune/remote.py +12 -0
  16. causaltune/score/__init__.py +0 -0
  17. causaltune/score/bite.py +147 -0
  18. causaltune/score/erupt.py +138 -0
  19. causaltune/score/erupt_core.py +128 -0
  20. causaltune/score/erupt_old.py +245 -0
  21. causaltune/score/frobenius.py +0 -0
  22. causaltune/score/r_score.py +282 -0
  23. causaltune/score/scoring.py +1362 -0
  24. causaltune/score/thompson.py +115 -0
  25. causaltune/search/__init__.py +0 -0
  26. causaltune/search/component.py +171 -0
  27. causaltune/search/params.py +625 -0
  28. causaltune/shap.py +44 -0
  29. causaltune/thirdparty/__init__.py +1 -0
  30. causaltune/thirdparty/causalml/LICENSE +13 -0
  31. causaltune/thirdparty/causalml/__init__.py +0 -0
  32. causaltune/thirdparty/causalml/metrics.py +410 -0
  33. causaltune/utils.py +152 -0
  34. causaltune/visualizer.py +182 -0
  35. causaltune-0.2.0.dist-info/METADATA +290 -0
  36. causaltune-0.2.0.dist-info/RECORD +39 -0
  37. causaltune-0.2.0.dist-info/WHEEL +4 -0
  38. causaltune-0.2.0.dist-info/licenses/LICENSE +176 -0
  39. 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"]
@@ -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