From 6af488b811fb86b6965005349119f645ff525ab7 Mon Sep 17 00:00:00 2001 From: Keith Battocchi Date: Thu, 13 May 2021 11:07:42 -0400 Subject: [PATCH 1/8] Fix ortholearner and causal analysis pickling --- econml/_ortho_learner.py | 6 ++-- .../causal_analysis/_causal_analysis.py | 29 ++++++++++--------- econml/tests/test_causal_analysis.py | 17 +++++++++++ 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/econml/_ortho_learner.py b/econml/_ortho_learner.py index b4d98126c..d8de89cef 100644 --- a/econml/_ortho_learner.py +++ b/econml/_ortho_learner.py @@ -193,9 +193,9 @@ def predict(self, X, y, W=None): return nuisances, model_list, np.sort(fitted_inds.astype(int)), (scores if calculate_scores else None) -CachedValues = namedtuple('_CachedValues', ['nuisances', - 'Y', 'T', 'X', 'W', 'Z', 'sample_weight', 'freq_weight', - 'sample_var', 'groups']) +CachedValues = namedtuple('CachedValues', ['nuisances', + 'Y', 'T', 'X', 'W', 'Z', 'sample_weight', 'freq_weight', + 'sample_var', 'groups']) class _OrthoLearner(TreatmentExpansionMixin, LinearCateEstimator): diff --git a/econml/solutions/causal_analysis/_causal_analysis.py b/econml/solutions/causal_analysis/_causal_analysis.py index 9695560db..50e3cb0ee 100644 --- a/econml/solutions/causal_analysis/_causal_analysis.py +++ b/econml/solutions/causal_analysis/_causal_analysis.py @@ -198,6 +198,13 @@ def get_feature_names(self, names=None): return rest +# named tuple type for storing results inside CausalAnalysis class; +# must be lifted to module level to enable pickling +_result = namedtuple("_result", field_names=[ + "feature_index", "feature_name", "feature_baseline", "feature_levels", "hinds", + "X_transformer", "W_transformer", "estimator", "global_inference"]) + + class CausalAnalysis: """ Note: this class is experimental and the API may evolve over our next few releases. @@ -248,10 +255,6 @@ class CausalAnalysis: Degree of parallelism to use when training models via joblib.Parallel """ - _result_data = namedtuple("_result", field_names=[ - "feature_index", "feature_name", "feature_baseline", "feature_levels", "hinds", - "X_transformer", "W_transformer", "estimator", "global_inference"]) - def __init__(self, feature_inds, categorical, heterogeneity_inds=None, feature_names=None, classification=False, upper_bound_on_cat_expansion=5, nuisance_models='linear', heterogeneity_model='linear', n_jobs=-1): self.feature_inds = feature_inds @@ -487,15 +490,15 @@ def process_feature(name, feat_ind): _CausalInsightsConstants.CategoricalColumnKey: [name], _CausalInsightsConstants.EngineeredNameKey: [name] } - result = CausalAnalysis._result_data(feature_index=feat_ind, - feature_name=name, - feature_baseline=baseline, - feature_levels=cats, - hinds=hinds, - X_transformer=X_transformer, - W_transformer=W_transformer, - estimator=est, - global_inference=global_inference) + result = _result(feature_index=feat_ind, + feature_name=name, + feature_baseline=baseline, + feature_levels=cats, + hinds=hinds, + X_transformer=X_transformer, + W_transformer=W_transformer, + estimator=est, + global_inference=global_inference) return insights, result diff --git a/econml/tests/test_causal_analysis.py b/econml/tests/test_causal_analysis.py index bd1fc5096..c386b3766 100644 --- a/econml/tests/test_causal_analysis.py +++ b/econml/tests/test_causal_analysis.py @@ -424,3 +424,20 @@ def test_empty_hinds(self): ca.fit(X_df, y) eff = ca.global_causal_effect(alpha=0.05) eff = ca.local_causal_effect(X_df, alpha=0.05) + + def test_can_serialize(self): + import pickle + y = pd.Series(np.random.choice([0, 1], size=(500,))) + X = pd.DataFrame({'a': np.random.normal(size=500), + 'b': np.random.normal(size=500), + 'c': np.random.choice([0, 1], size=500), + 'd': np.random.choice(['a', 'b', 'c'], size=500)}) + inds = ['a', 'b', 'c', 'd'] + cats = ['c', 'd'] + hinds = ['a', 'd'] + + ca = CausalAnalysis(inds, cats, hinds, heterogeneity_model='linear') + ca = pickle.loads(pickle.dumps(ca)) + ca.fit(X, y) + ca = pickle.loads(pickle.dumps(ca)) + eff = ca.global_causal_effect() From ce8825c868f968073bf657233303c790c9cf4633 Mon Sep 17 00:00:00 2001 From: Keith Battocchi Date: Fri, 14 May 2021 13:02:10 -0400 Subject: [PATCH 2/8] Enable categorical range checking --- .../causal_analysis/_causal_analysis.py | 26 ++++++++++-- econml/tests/test_causal_analysis.py | 40 +++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/econml/solutions/causal_analysis/_causal_analysis.py b/econml/solutions/causal_analysis/_causal_analysis.py index 50e3cb0ee..3e993eab0 100644 --- a/econml/solutions/causal_analysis/_causal_analysis.py +++ b/econml/solutions/causal_analysis/_causal_analysis.py @@ -309,11 +309,13 @@ def fit(self, X, y, warm_start=False): raise ValueError( f"Can't warm start: previous X had {self._d_x} columns, new X has {X.shape[1]} columns") - # TODO: implement check for upper bound on categoricals - # work with numeric feature indices, so that we can easily compare with categorical ones train_inds = _get_column_indices(X, self.feature_inds) + if len(train_inds) == 0: + raise ValueError( + "No features specified. At least one feature index must be specified so that a model can be trained.") + heterogeneity_inds = self.heterogeneity_inds if heterogeneity_inds is None: heterogeneity_inds = [None for ind in train_inds] @@ -351,7 +353,7 @@ def fit(self, X, y, warm_start=False): new_inds = [ind for ind in train_inds if (ind not in self._cache or heterogeneity_inds[ind] != self._cache[ind][1].hinds)] else: - new_inds = train_inds + new_inds = list(train_inds) self._cache = {} # store mapping from feature to insights, results @@ -394,6 +396,24 @@ def fit(self, X, y, warm_start=False): # convert categorical indicators to numeric indices categorical_inds = _get_column_indices(X, self.categorical) + # check for indices over the categorical expansion bound + over_bound_inds = [] + for ind in new_inds: + n_cats = len(np.unique(_safe_indexing(X, ind, axis=1))) + if ind in categorical_inds and n_cats > self.upper_bound_on_cat_expansion: + warnings.warn(f"Column {ind} has more than {self.upper_bound_on_cat_expansion} values " + "so no heterogeneity model will be fit for it; increase 'upper_bound_on_cat_expansion' " + "to change this behavior.") + # can't remove in place while iterating over new_inds, so store in separate list + over_bound_inds.append(ind) + for ind in over_bound_inds: + new_inds.remove(ind) + # also remove from train_inds so we don't try to access the result later + train_inds.remove(ind) + if len(train_inds) == 0: + raise ValueError("No features remain; increase the upper_bound_on_cat_expansion so that at least " + "one feature model can be trained.") + def process_feature(name, feat_ind): discrete_treatment = feat_ind in categorical_inds hinds = heterogeneity_inds[feat_ind] diff --git a/econml/tests/test_causal_analysis.py b/econml/tests/test_causal_analysis.py index c386b3766..b2ba06891 100644 --- a/econml/tests/test_causal_analysis.py +++ b/econml/tests/test_causal_analysis.py @@ -441,3 +441,43 @@ def test_can_serialize(self): ca.fit(X, y) ca = pickle.loads(pickle.dumps(ca)) eff = ca.global_causal_effect() + + def test_over_cat_limit(self): + y = pd.Series(np.random.choice([0, 1], size=(500,))) + X = pd.DataFrame({'a': np.random.normal(size=500), + 'b': np.random.normal(size=500), + 'c': np.random.choice([0, 1], size=500), + 'd': np.random.choice(['a', 'b', 'c', 'd'], size=500), + 'e': np.random.choice([7, 8, 9, 10, 11], size=500), + 'f': np.random.choice(['x', 'y'], size=500), + 'g': np.random.choice([0, 1], size=500), + 'h': np.random.choice(['q', 'r', 's'], size=500)}) + inds = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] + cats = ['c', 'd', 'e', 'f', 'g', 'h'] + hinds = ['a', 'd'] + ca = CausalAnalysis(inds, cats, hinds, upper_bound_on_cat_expansion=2) + ca.fit(X, y) + + # columns 'd', 'e', 'h' have too many values + self.assertEqual([res.feature_name for res in ca._results], ['a', 'b', 'c', 'f', 'g']) + + inds = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] + cats = ['c', 'd', 'e', 'f', 'g', 'h'] + hinds = ['a', 'd'] + ca = CausalAnalysis(inds, cats, hinds, upper_bound_on_cat_expansion=3) + ca.fit(X, y) + + # columns 'd', 'e' have too many values + self.assertEqual([res.feature_name for res in ca._results], ['a', 'b', 'c', 'f', 'g', 'h']) + + ca.upper_bound_on_cat_expansion = 2 + ca.fit(X, y, warm_start=True) + + # lowering bound shouldn't affect already fit columns when warm starting + self.assertEqual([res.feature_name for res in ca._results], ['a', 'b', 'c', 'f', 'g', 'h']) + + ca.upper_bound_on_cat_expansion = 4 + ca.fit(X, y, warm_start=True) + + # column d is now okay, too + self.assertEqual([res.feature_name for res in ca._results], ['a', 'b', 'c', 'd', 'f', 'g', 'h']) From 51b4dd6b42bd52398e13a7ebbe4106421e1752cf Mon Sep 17 00:00:00 2001 From: Keith Battocchi Date: Fri, 14 May 2021 14:06:14 -0400 Subject: [PATCH 3/8] Add init args to causal insights dictionary --- .../causal_analysis/_causal_analysis.py | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/econml/solutions/causal_analysis/_causal_analysis.py b/econml/solutions/causal_analysis/_causal_analysis.py index 3e993eab0..9c1903c35 100644 --- a/econml/solutions/causal_analysis/_causal_analysis.py +++ b/econml/solutions/causal_analysis/_causal_analysis.py @@ -45,6 +45,7 @@ class _CausalInsightsConstants: CausalComputationTypeKey = 'causal_computation_type' ConfoundingIntervalKey = 'confounding_interval' ViewKey = 'view' + InitArgsKey = 'init_args' ALL = [RawFeatureNameKey, EngineeredNameKey, @@ -59,7 +60,8 @@ class _CausalInsightsConstants: Version, CausalComputationTypeKey, ConfoundingIntervalKey, - ViewKey] + ViewKey, + InitArgsKey] def _get_default_shared_insights_output(): @@ -78,6 +80,7 @@ def _get_default_shared_insights_output(): _CausalInsightsConstants.Version: '1.0', _CausalInsightsConstants.CausalComputationTypeKey: "simple", _CausalInsightsConstants.ConfoundingIntervalKey: None, + _CausalInsightsConstants.InitArgsKey: {} } @@ -198,6 +201,19 @@ def get_feature_names(self, names=None): return rest +# Convert python objects to (possibly nested) types that can easily be represented as literals +def _sanitize(obj): + if obj is None or isinstance(obj, (bool, int, str, float)): + return obj + elif isinstance(obj, dict): + return {_sanitize(key): _sanitize(obj[key]) for key in obj} + else: + try: + return [_sanitize(item) for item in obj] + except e: + raise ValueError(f"Could not sanitize input {obj}") + + # named tuple type for storing results inside CausalAnalysis class; # must be lifted to module level to enable pickling _result = namedtuple("_result", field_names=[ @@ -392,6 +408,17 @@ def fit(self, X, y, warm_start=False): # start with empty results and default shared insights self._results = [] self._shared = _get_default_shared_insights_output() + self._shared[_CausalInsightsConstants.InitArgsKey] = { + 'feature_inds': _sanitize(self.feature_inds), + 'categorical': _sanitize(self.categorical), + 'heterogeneity_inds': _sanitize(self.heterogeneity_inds), + 'feature_names': _sanitize(self.feature_names), + 'classification': _sanitize(self.classification), + 'upper_bound_on_cat_expansion': _sanitize(self.upper_bound_on_cat_expansion), + 'nuisance_models': _sanitize(self.nuisance_models), + 'heterogeneity_model': _sanitize(self.heterogeneity_model), + 'n_jobs': _sanitize(self.n_jobs) + } # convert categorical indicators to numeric indices categorical_inds = _get_column_indices(X, self.categorical) From 2b89aa5b71a02db1a6a4119f10849a88045d55a6 Mon Sep 17 00:00:00 2001 From: Keith Battocchi Date: Mon, 17 May 2021 11:30:33 -0400 Subject: [PATCH 4/8] Correct statsmodels version bound --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 024a86b5a..d34567dfc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,7 +37,7 @@ install_requires = sparse joblib >= 0.13.0 numba != 0.42.1 - statsmodels >= 0.9 + statsmodels >= 0.10 graphviz pandas shap ~= 0.38.1 From 13ed8f71a7ef3f6ad96452f8465da9fef506d035 Mon Sep 17 00:00:00 2001 From: Keith Battocchi Date: Mon, 17 May 2021 11:41:20 -0400 Subject: [PATCH 5/8] Correct installation and testing instructions --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a5169695f..be98a2893 100644 --- a/README.md +++ b/README.md @@ -552,12 +552,13 @@ To see more complex examples, go to the [notebooks](https://github.com/Microsoft You can get started by cloning this repository. We use [setuptools](https://setuptools.readthedocs.io/en/latest/index.html) for building and distributing our package. We rely on some recent features of setuptools, so make sure to upgrade to a recent version with -`pip install setuptools --upgrade`. Then from your local copy of the repository you can run `python setup.py develop` to get started. +`pip install setuptools --upgrade`. Then from your local copy of the repository you can run `pip install -e .` to get started (but depending on what you're doing you might want to install with extras instead, like `pip install -e .[plt]` if you want to use matplotlib integration, or you can use `pip install -e .[all]` to include all extras). ## Running the tests -This project uses [pytest](https://docs.pytest.org/) for testing. To run tests locally after installing the package, -you can use `python setup.py pytest`. +This project uses [pytest](https://docs.pytest.org/) for testing. To run tests locally after installing the package, you can use `pip install pytest-runner` followed by `python setup.py pytest`. + +We have added pytest marks to some tests to make it easier to run a subset, and you can set the PYTEST_ADDOPTS environment variable to take advantage of this. For instance, you can set it to `-m "not (notebook or automl)"` to skip notebook and automl tests that have some additional dependencies. ## Generating the documentation From 7006cfd1dc0680c647fd0d569df960cbdc05c187 Mon Sep 17 00:00:00 2001 From: Keith Battocchi Date: Tue, 18 May 2021 11:31:03 -0400 Subject: [PATCH 6/8] Fix ortho_learner docs --- econml/_ortho_learner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/econml/_ortho_learner.py b/econml/_ortho_learner.py index d8de89cef..9ccdd88b3 100644 --- a/econml/_ortho_learner.py +++ b/econml/_ortho_learner.py @@ -236,7 +236,7 @@ class _OrthoLearner(TreatmentExpansionMixin, LinearCateEstimator): .. math :: \\mathbb{E}_n[\\ell(V; \\theta(X), \\hat{h}(V))]\ - = \\frac{1}{n} \\sum_{i=1}^n \\sum_i \\ell(V_i; \\theta(X_i), \\hat{U}_i) + = \\frac{1}{n} \\sum_{i=1}^n \\ell(V_i; \\theta(X_i), \\hat{U}_i) The method is a bit more general in that the final step does not need to be a loss minimization step. The class takes as input a model for fitting an estimate of the nuisance h given a set of samples From 2e1cf8b184367e5ffb830be30dd128311103e476 Mon Sep 17 00:00:00 2001 From: Keith Battocchi Date: Tue, 18 May 2021 17:30:41 -0400 Subject: [PATCH 7/8] Add treatment recommendations to causal analysis --- .../causal_analysis/_causal_analysis.py | 61 ++++++++++--------- econml/tests/test_causal_analysis.py | 22 +++---- 2 files changed, 44 insertions(+), 39 deletions(-) diff --git a/econml/solutions/causal_analysis/_causal_analysis.py b/econml/solutions/causal_analysis/_causal_analysis.py index 9c1903c35..c25be41f2 100644 --- a/econml/solutions/causal_analysis/_causal_analysis.py +++ b/econml/solutions/causal_analysis/_causal_analysis.py @@ -997,10 +997,12 @@ def _tree(self, is_policy, Xtest, feature_index, *, treatment_cost=0, if is_policy: intrp.interpret(result.estimator, Xtest, sample_treatment_costs=treatment_cost) + treat = intrp.treat(Xtest) else: # no treatment cost for CATE trees intrp.interpret(result.estimator, Xtest) + treat = None - return intrp, result.X_transformer.get_feature_names(self.feature_names_), treatment_names + return intrp, result.X_transformer.get_feature_names(self.feature_names_), treatment_names, treat # TODO: it seems like it would be better to just return the tree itself rather than plot it; # however, the tree can't store the feature and treatment names we compute here... @@ -1027,18 +1029,21 @@ def plot_policy_tree(self, Xtest, feature_index, *, treatment_cost=0, Confidence level of the confidence intervals displayed in the leaf nodes. A (1-alpha)*100% confidence interval is displayed. """ - intrp, feature_names, treatment_names = self._tree(True, Xtest, feature_index, - treatment_cost=treatment_cost, - max_depth=max_depth, - min_samples_leaf=min_samples_leaf, - min_impurity_decrease=min_value_increase, - alpha=alpha) + intrp, feature_names, treatment_names, _ = self._tree(True, Xtest, feature_index, + treatment_cost=treatment_cost, + max_depth=max_depth, + min_samples_leaf=min_samples_leaf, + min_impurity_decrease=min_value_increase, + alpha=alpha) return intrp.plot(feature_names=feature_names, treatment_names=treatment_names) - def _policy_tree_string(self, Xtest, feature_index, *, treatment_cost=0, + def _policy_tree_output(self, Xtest, feature_index, *, treatment_cost=0, max_depth=3, min_samples_leaf=2, min_value_increase=1e-4, alpha=.1): """ - Get a recommended policy tree in graphviz format as a string. + Get a tuple policy outputs. + + The first item in the tuple is the recommended policy tree in graphviz format as a string. + The second item is the recommended treatment for each sample as a list. Parameters ---------- @@ -1060,18 +1065,18 @@ def _policy_tree_string(self, Xtest, feature_index, *, treatment_cost=0, Returns ------- - tree : string - The policy tree represented as a graphviz string + tree : tuple of string, list of int + The policy tree represented as a graphviz string and the recommended treatment for each row """ - intrp, feature_names, treatment_names = self._tree(True, Xtest, feature_index, - treatment_cost=treatment_cost, - max_depth=max_depth, - min_samples_leaf=min_samples_leaf, - min_impurity_decrease=min_value_increase, - alpha=alpha) + intrp, feature_names, treatment_names, treat = self._tree(True, Xtest, feature_index, + treatment_cost=treatment_cost, + max_depth=max_depth, + min_samples_leaf=min_samples_leaf, + min_impurity_decrease=min_value_increase, + alpha=alpha) return intrp.export_graphviz(feature_names=feature_names, - treatment_names=treatment_names) + treatment_names=treatment_names), treat.tolist() # TODO: it seems like it would be better to just return the tree itself rather than plot it; # however, the tree can't store the feature and treatment names we compute here... @@ -1099,11 +1104,11 @@ def plot_heterogeneity_tree(self, Xtest, feature_index, *, A (1-alpha)*100% confidence interval is displayed. """ - intrp, feature_names, treatment_names = self._tree(False, Xtest, feature_index, - max_depth=max_depth, - min_samples_leaf=min_samples_leaf, - min_impurity_decrease=min_impurity_decrease, - alpha=alpha) + intrp, feature_names, treatment_names, _ = self._tree(False, Xtest, feature_index, + max_depth=max_depth, + min_samples_leaf=min_samples_leaf, + min_impurity_decrease=min_impurity_decrease, + alpha=alpha) return intrp.plot(feature_names=feature_names, treatment_names=treatment_names) @@ -1131,10 +1136,10 @@ def _heterogeneity_tree_string(self, Xtest, feature_index, *, A (1-alpha)*100% confidence interval is displayed. """ - intrp, feature_names, treatment_names = self._tree(False, Xtest, feature_index, - max_depth=max_depth, - min_samples_leaf=min_samples_leaf, - min_impurity_decrease=min_impurity_decrease, - alpha=alpha) + intrp, feature_names, treatment_names, _ = self._tree(False, Xtest, feature_index, + max_depth=max_depth, + min_samples_leaf=min_samples_leaf, + min_impurity_decrease=min_impurity_decrease, + alpha=alpha) return intrp.export_graphviz(feature_names=feature_names, treatment_names=treatment_names) diff --git a/econml/tests/test_causal_analysis.py b/econml/tests/test_causal_analysis.py index b2ba06891..9387852dc 100644 --- a/econml/tests/test_causal_analysis.py +++ b/econml/tests/test_causal_analysis.py @@ -44,13 +44,13 @@ def test_basic_array(self): coh_point_est = np.array(coh_dict[_CausalInsightsConstants.PointEstimateKey]) loc_point_est = np.array(loc_dict[_CausalInsightsConstants.PointEstimateKey]) - ca._policy_tree_string(X, 1) + ca._policy_tree_output(X, 1) ca._heterogeneity_tree_string(X, 1) ca._heterogeneity_tree_string(X, 3) # Can't handle multi-dimensional treatments with self.assertRaises(AssertionError): - ca._policy_tree_string(X, 3) + ca._policy_tree_output(X, 3) # global shape is (d_y, sum(d_t)) assert glo_point_est.shape == coh_point_est.shape == (1, 5) @@ -133,13 +133,13 @@ def test_basic_pandas(self): assert glo_point_est.shape == coh_point_est.shape == (1, 5) assert loc_point_est.shape == (2,) + glo_point_est.shape - ca._policy_tree_string(X, inds[1]) + ca._policy_tree_output(X, inds[1]) ca._heterogeneity_tree_string(X, inds[1]) ca._heterogeneity_tree_string(X, inds[3]) # Can't handle multi-dimensional treatments with self.assertRaises(AssertionError): - ca._policy_tree_string(X, inds[3]) + ca._policy_tree_output(X, inds[3]) if not classification: # ExitStack can be used as a "do nothing" ContextManager @@ -199,13 +199,13 @@ def test_automl_first_stage(self): coh_point_est = np.array(coh_dict[_CausalInsightsConstants.PointEstimateKey]) loc_point_est = np.array(loc_dict[_CausalInsightsConstants.PointEstimateKey]) - ca._policy_tree_string(X, 1) + ca._policy_tree_output(X, 1) ca._heterogeneity_tree_string(X, 1) ca._heterogeneity_tree_string(X, 3) # Can't handle multi-dimensional treatments with self.assertRaises(AssertionError): - ca._policy_tree_string(X, 3) + ca._policy_tree_output(X, 3) # global shape is (d_y, sum(d_t)) assert glo_point_est.shape == coh_point_est.shape == (1, 5) @@ -279,7 +279,7 @@ def test_one_feature(self): assert glo_point_est.shape == coh_point_est.shape == (1, 1) assert loc_point_est.shape == (2,) + glo_point_est.shape - ca._policy_tree_string(X, inds[0]) + ca._policy_tree_output(X, inds[0]) ca._heterogeneity_tree_string(X, inds[0]) def test_final_models(self): @@ -302,13 +302,13 @@ def test_final_models(self): coh_dict = ca._cohort_causal_effect_dict(X[:2]) loc_dict = ca._local_causal_effect_dict(X[:2]) - ca._policy_tree_string(X, 1) + ca._policy_tree_output(X, 1) ca._heterogeneity_tree_string(X, 1) ca._heterogeneity_tree_string(X, 3) # Can't handle multi-dimensional treatments with self.assertRaises(AssertionError): - ca._policy_tree_string(X, 3) + ca._policy_tree_output(X, 3) if not classification: # ExitStack can be used as a "do nothing" ContextManager @@ -370,13 +370,13 @@ def test_forest_with_pandas(self): assert glo_point_est.shape == coh_point_est.shape == (1, 5) assert loc_point_est.shape == (2,) + glo_point_est.shape - ca._policy_tree_string(X, inds[1]) + ca._policy_tree_output(X, inds[1]) ca._heterogeneity_tree_string(X, inds[1]) ca._heterogeneity_tree_string(X, inds[3]) # Can't handle multi-dimensional treatments with self.assertRaises(AssertionError): - ca._policy_tree_string(X, inds[3]) + ca._policy_tree_output(X, inds[3]) def test_warm_start(self): for classification in [True, False]: From b391eef69c85757f62abca93188d116ef42ea395 Mon Sep 17 00:00:00 2001 From: Keith Battocchi Date: Mon, 17 May 2021 11:48:40 -0400 Subject: [PATCH 8/8] Prepare 0.11.1 release --- README.md | 4 +++- econml/__init__.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index be98a2893..a67beceef 100644 --- a/README.md +++ b/README.md @@ -45,10 +45,12 @@ For information on use cases and background material on causal inference and het # News -**May 8, 2021:** Release v0.11.0, see release notes [here](https://github.com/Microsoft/EconML/releases/tag/v0.11.0) +**May 18, 2021:** Release v0.11.1, see release notes [here](https://github.com/Microsoft/EconML/releases/tag/v0.11.1)
Previous releases +**May 8, 2021:** Release v0.11.0, see release notes [here](https://github.com/Microsoft/EconML/releases/tag/v0.11.0) + **March 22, 2021:** Release v0.10.0, see release notes [here](https://github.com/Microsoft/EconML/releases/tag/v0.10.0) **March 11, 2021:** Release v0.9.2, see release notes [here](https://github.com/Microsoft/EconML/releases/tag/v0.9.2) diff --git a/econml/__init__.py b/econml/__init__.py index 84fef7159..df915d3c6 100644 --- a/econml/__init__.py +++ b/econml/__init__.py @@ -26,4 +26,4 @@ 'dowhy', '__version__'] -__version__ = '0.11.0' +__version__ = '0.11.1'